Files
larksuite__cli/shortcuts/slides/slides_errors.go
R0bynZhu ba104380ee feat(slides): add +add-slide and +delete-slide shortcuts (#2120)
Add two single-page slide shortcuts on top of the raw
xml_presentation.slide create/delete APIs.

slides +add-slide appends or inserts one page into an existing
presentation. It accepts --presentation as a token, a /slides/ URL or a
/wiki/ URL (resolved via wiki.spaces.get_node and checked for
obj_type=slides), takes the page XML through --slide as a literal, @file
or stdin so the document never has to be escaped into JSON and then into
the shell, and auto-uploads <img src="@./local.png"> placeholders,
replacing them with the returned file_token. Omitting --before-slide-id
appends to the end; the field is dropped from the body rather than sent
empty, which the backend rejects as an unknown slide.

slides +delete-slide removes one page by slide_id with the same
--presentation resolution. It is deliberately Risk "write" rather than
the raw command's high-risk-write, so it does not require --yes: it
targets a single explicit page and the deck keeps its version history.

Both take one page at a time so that batching stays an explicit loop and
every call has an unambiguous outcome.

The image placeholder validation used by +create is extracted into a
shared helper so both commands fail before any API call when a referenced
file is missing, is not a regular file or exceeds the 20 MB upload limit.

Covered by unit tests and by dry-run e2e tests through the built binary,
which is the only layer that proves a full <slide> document survives flag
parsing intact. Reference docs are added for both commands and the
existing slides skill docs now route to them.
2026-08-03 20:03:01 +08:00

64 lines
2.6 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package slides
import (
"errors"
"io/fs"
"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/extension/fileio"
)
// slidesInputStatError maps a FileIO.Stat error for an input image path to a
// typed validation error, prefixing the caller's context message and tagging
// the offending flag via param so callers route on the typed Param rather than
// parsing the message. Both path validation failures and other stat errors are
// user-actionable input problems (exit code 2). Already-typed errors are not
// expected here (Stat returns raw fs errors), so this always classifies as
// validation.
//
// Why the "file not found" wording lives here and not at the call sites: Stat
// fails for a missing path, an unreadable parent directory and a rejected path
// shape alike, so a caller-supplied "file not found" suffix turns a permission
// error into the self-contradicting "file not found: permission denied". context
// names what was being read (the flag, the @-placeholder); this helper decides
// what actually went wrong.
func slidesInputStatError(err error, param, context string) error {
if err == nil {
return nil
}
switch {
case errors.Is(err, fileio.ErrPathValidation):
return errs.NewValidationError(errs.SubtypeInvalidArgument, "%s: unsafe file path: %s", context, err).WithParam(param).WithCause(err)
case errors.Is(err, fs.ErrNotExist):
// The raw error only restates the path, which context already names.
return errs.NewValidationError(errs.SubtypeInvalidArgument, "%s: file not found", context).WithParam(param).WithCause(err)
default:
return errs.NewValidationError(errs.SubtypeInvalidArgument, "%s: cannot read file: %s", context, err).WithParam(param).WithCause(err)
}
}
// appendSlidesProgressHint preserves err's typed classification (per
// ERROR_CONTRACT.md "propagate typed errors unchanged") and appends an
// orchestration-progress hint — e.g. "presentation was created; N image(s)
// uploaded before failure" — so a failure mid-sequence still tells the caller
// what partial state exists. An unclassified error (e.g. surfaced from a shared
// helper boundary before it can be classified) falls back to a typed internal
// error carrying the hint.
func appendSlidesProgressHint(err error, hint string) error {
if err == nil {
return nil
}
if p, ok := errs.ProblemOf(err); ok {
if p.Hint != "" {
p.Hint = p.Hint + "\n" + hint
} else {
p.Hint = hint
}
return err
}
return errs.NewInternalError(errs.SubtypeUnknown, "%s", err.Error()).WithHint(hint).WithCause(err)
}