* refactor: retire legacy error envelopes and enforce typed contract
Consolidate all command error reporting onto the typed errs.* contract, remove
the legacy error surface that predated it, and tighten the lint guards so the
contract holds across the whole repository going forward.
Every failure now reaches stderr as one envelope shape: a category, an
optional subtype, a human- and agent-readable message, and a recovery hint,
with invalid parameters listed under `params`. The legacy ExitError envelope,
its constructors, and the boundary bridge that promoted untyped config and
authorization errors are deleted, leaving a single path from error to wire.
Predicate commands keep their silent-exit behavior through a dedicated signal
that carries only an exit code.
Infrastructure paths that still emitted ad-hoc envelopes — flag parsing,
unknown commands and subcommands, plugin and policy guards, confirmation
prompts, and auth/config failures — now classify into the same taxonomy.
Business, API, auth, and config exit codes are preserved; the one behavioral
change is that Cobra usage failures (missing required flag, unknown command,
bad arguments) now emit the typed validation envelope and exit 2, matching the
explicit flag and subcommand guards, instead of Cobra's plain-text exit 1.
Enforcement is repo-wide rather than per-path:
- The errscontract guards run by default everywhere instead of through a
migration allowlist, so legacy envelopes cannot be reintroduced anywhere.
- errorlint runs across the whole repository: every error wrap must use %w and
every comparison must use errors.Is/errors.As, so interior wraps stay legal
but can no longer break the chain the typed boundary relies on.
- The errs-no-bare-wrap guard is keyed by structural prefix instead of an
explicit per-domain allowlist, so new shortcut domains are covered without
editing a list. It runs where forbidigo is enabled (the shortcut domains and
the auth/config/service command groups); repo-wide chain integrity for the
remaining command paths is carried by errorlint above.
* test: align cli_e2e success assertions to the ok envelope
The api and service success path now emits the {"ok":true} envelope, so the
cli_e2e workflow assertions that still expected the old {"code":0} shape via
AssertStdoutStatus(t, 0) fail once they run with live credentials. Switch those
workflow assertions to AssertStdoutStatus(t, true); the fake-payload helper test
in core_test.go keeps its code-shape assertion.
Emit structured validation, API, network, file, and internal error envelopes for Slides shortcuts so users and agents can recover from failed presentation workflows using stable type, subtype, param, and code fields.
Add Slides domain errscontract and golangci guards to prevent legacy envelope and common helper regressions.
slides +create finished by calling /drive/v1/metas/batch_query just to
fetch the presentation URL. That call needs a drive scope the shortcut
never declares, so it 403'd for users who only authorized slides scopes
(both UserAccessToken re-auth and TenantAccessToken scope-not-opened),
producing a large share of the shortcut's failure telemetry — even though
the presentation itself was already created successfully.
slides creation never otherwise touches drive, so rather than gating a
drive-free operation behind a drive scope, build the URL locally from the
token via common.BuildResourceURL (the same brand-standard-host fallback
already used by drive +upload / wiki +node-create). The URL is now always
returned, no extra scope is required, and creation never blocks.
Tests are updated to match: drop the registerBatchQueryStub helper and its
call sites (the httpmock Verify cleanup was failing on the now-unconsumed
batch_query stubs), point url assertions at the brand-standard host, and
replace TestSlidesCreateURLFetchBestEffort with TestSlidesCreateURLBuiltLocally,
which asserts the url is produced with no drive call registered.
When a resource is created with bot identity, the CLI attempts to
auto-grant full_access to the current user. If the user open_id is
missing or the grant API call fails, the result was only written to
the JSON permission_grant field and easily overlooked.
Changes:
- Add stderr warnings when auto-grant is skipped or fails
- Add 'hint' field to permission_grant JSON output with failure reason
and actionable next step (e.g. auth login, check scope, retry)
- Add end-to-end skipped/failed tests across all affected shortcuts
(doc, drive, sheets, slides, wiki, markdown, base)
Closes#963
Introduces `lark-cli slides +replace-slide`, a shortcut over the
native `xml_presentation.slide.replace` API for element-level editing
of existing Lark Slides pages. Callers pass a JSON array of parts and
the CLI handles URL resolution, XML hygiene, client-side validation,
and 3350001 hint enrichment.
Why a dedicated shortcut
The native API has three sharp edges every caller hits:
1. URL formats. Users have /slides/<token> or /wiki/<token> URLs, not
bare xml_presentation_id.
2. Undocumented XML hygiene. `block_replace` requires id=<block_id> on
the replacement root; <shape> requires <content/>. Missing either
returns a catch-all 3350001 with no guidance.
3. 3350001 is a catch-all on the backend with no actionable message.
Code
shortcuts/slides/slides_replace_slide.go (new)
- Flags: --presentation (bare token | /slides/ URL | /wiki/ URL),
--slide-id, --parts (JSON array, max 200), --revision-id (-1 for
current, specific number for optimistic locking), --tid,
--as user|bot.
- Validation (pre-API): [1,200] item cap; action restricted to
block_replace / block_insert (str_replace rejected); per-action
required fields (block_id for block_replace, insertion for
block_insert); per-field string type-assertion guards on the
decoded JSON so a numeric/bool payload fails fast with a targeted
error.
- XML hygiene:
* injects id="<block_id>" on block_replace replacement roots;
* auto-expands self-closing <shape/> and injects <content/> on
shapes for SML 2.0 compliance.
Dry-run surfaces injection errors and renders the same
path-encoded presentationID that Execute sends.
- On backend 3350001 attaches a generic common-causes checklist
(missing block_id / invalid XML / coords out of 960×540).
shortcuts/slides/helpers.go
- ensureXMLRootID: regex tightened to `(?:^|\s)id` so data-id and
xml:id are not matched as root id.
- ensureShapeHasContent: regex `<content(?:\s|/|>)` avoids false
positives like <contention/>; self-closing branch preserves
trailing siblings.
shortcuts/slides/shortcuts.go: register SlidesReplaceSlide.
Tests (package coverage 89.4%; parseReplaceParts and
injectBlockReplaceIDs both reach 100%)
- helpers_test.go: regex edge cases, id override semantics, content
auto-inject across self-closing and open-tag shapes.
- slides_replace_slide_test.go: parameter validation table, URL
resolution (slides / wiki), mixed block_replace + block_insert,
size boundaries, auto-inject behavior, 3350001 hint enrichment,
per-field type-assertion guards, whitespace-only --parts guard
(distinct from the `[]` "at least 1 item" path), replacement
without root element surfaces pre-flight instead of reaching the
backend, and a tight negative assertion that non-3350001 errors
get no slides-specific hint.
Docs (skills/lark-slides)
- SKILL.md: add +replace-slide to the Shortcuts table, register the
new xml_presentation.slide.get / .replace native endpoints,
update core rule 7 to prefer block-level replace over full-page
rebuild now that element-level editing exists, extend the error
table with 3350001 / 3350002 pointing at the replace-slide doc,
add "add image to existing slide via block_insert" as an explicit
Workflow step and symptom-table entry, and refresh the reference
index to include the three new docs below. The old "整页替换" 4-rule
checklist is retired — its one still-relevant guard (new <img>
avoiding overlap) is preserved in the symptom table.
- New references:
* lark-slides-replace-slide.md — flags, parts schema, auto-inject
notes, mixed-action support, 200-item cap, revision_id
semantics, error table, and a "合法根元素速查" cheatsheet for
the eight supported root elements (shape / line / polyline /
img / icon / table / td / chart) with minimal verified XML
snippets. Explicit unsupported list: video / audio / whiteboard
(these appear only as <undefined> export placeholders in SML 2.0).
* lark-slides-edit-workflows.md — recipe-style edit flows covering
the read → modify → write loop and the block_replace vs
block_insert decision tree.
* lark-slides-xml-presentation-slide-get.md — native read API with
block_id extraction examples.
- Fixes across existing references:
* replace / create / delete / presentations.get: add the .data
wrapper in return-value examples, correct jq paths.
* media-upload: fix jq path .file_token → .data.file_token.
* examples.md: annotate auto-inject behavior, replace the
incorrect failed_part_index example with the actual 3350001
error shape.
Empirical corrections (BOE-verified)
- revision_id: stale-but-existing values are accepted; only values
greater than current return 3350002.
- Wrong block_id returns 3350001, not a 200 with failed_part_index.
- Mixed block_replace + block_insert in one call is supported.
- Type-mismatched block_replace (e.g. shape id with a <td>
replacement) is silently accepted by the backend and may destroy
content; 3350001 specifically signals a missing block_id.
- New `slides +media-upload` shortcut: upload a local image to a slides
presentation and return the file_token for use in <img src="...">.
- `slides +create --slides` now supports `@./path.png` placeholders that
are auto-uploaded and replaced with file_tokens.
- Reject images >20 MB (multipart upload not supported for slide_file).
- Support wiki URL resolution for --presentation flag.
After creating the presentation, call drive batch_query (with_url=true)
to fetch the document URL and include it in the output. The fetch is
best-effort so it won't break creation if the API call fails.
Also update the skill reference doc to document the new optional url
return field.