Commit Graph

10 Commits

Author SHA1 Message Date
ethan-zhx 7874dc144b feat(slides): add media download shortcut (#2446) 2026-08-24 18:26:23 +08:00
R0bynZhu 1f53f6e2f5 refactor(slides): assert dry-run parent_type instead of deriving it from a placeholder (#2461)
* refactor(slides): assert dry-run parent_type instead of deriving it from a placeholder

A wiki --presentation cannot be resolved during a dry-run: the real
presentation token only exists after a get_node call a preview must not
make, so parent_node shows a "<resolved_slides_token>" placeholder.
appendSlidesUploadDryRun derived parent_type from parent_node, which sent
that placeholder through the office-token check.

The value it produced was correct. A placeholder matches no office token
shape, so it fell through to slide_file, and slide_file is right here: a
wiki ref that reaches an upload is native by construction, because
resolvePresentationID rejects any wiki node whose obj_type is not
"slides" and an imported office deck sits in drive as a "file" node.

It was correct by accident, though, which left the preview hostage to the
placeholder's spelling and to every rule later added to
isOfficePresentation. Pass parent_type in explicitly instead, so
slidesDryRunParentType states the wiki case as a decision with its reason
recorded, and the placeholder is never classified.

No behaviour change: dry-run output is byte-identical for native tokens,
imported office tokens, legacy office prefixes, slides URLs, wiki URLs,
and +create, across +media-upload / +add-slide / +update-slide.

Tests pin the contract the refactor protects, including a wiki ref whose
node token is itself office-shaped -- a wiki node token and the deck token
it points at are different tokens in different namespaces, so classifying
ref.Token would be wrong for a wiki ref even though it is right for every
other kind. That is the regression this makes impossible.

* test(slides): use the fixture hostname for the wiki dry-run probes

domaincontract rejects "bytedance.larkoffice.com": it is in neither
allowlist, and a real tenant host does not belong in a fixture. Use
example.feishu.cn, already in fixture-domains.txt and the hostname the
rest of the slides wiki tests use.

The URL is only a parse fixture -- nothing resolves it -- so only the
hostname changes.
2026-08-24 15:55:37 +08:00
R0bynZhu b343e67639 feat(slides): use office_slide_file parent_type for imported office presentations (#2441)
Image uploads to a presentation hard-coded parent_type=slide_file at every
entry point. Imported "office" presentations carry either a legacy synthetic
token prefix ("fake_office_" / "local_office_") or a 28-character token whose
interleaved product/region marker is "OFL0X", and for those the drive backend
requires parent_type=office_slide_file. This mirrors the office_sheet_file rule
the sheets domain already applies: the token shapes are identical, because an
imported office file is an imported office file whether it backs a spreadsheet
or a deck.

Funnel the selection through one slides-domain helper so the rule lives in a
single place and every image-upload path stays consistent with its own dry-run
preview. As in sheets, the rule stays inside the domain rather than leaking
into common.UploadDriveMediaAllTyped, which mail/doc/drive/base/calendar share.

- Replace the slidesMediaParentType const with slidesMediaParentType(token),
  backed by isOfficePresentation(token); keep the native and office values as
  named constants.
- Route both parent_type call sites through it: uploadSlidesMedia (the Execute
  path shared by +media-upload and the <img src="@path"> placeholder pipeline
  behind +create / +add-slide / +update-slide) and appendSlidesUploadDryRun.
- Known gap, documented at the helper: when --presentation is a wiki URL the
  dry-run only has a "<resolved_slides_token>" placeholder, since the real
  token needs a get_node call the preview must not make, so such a preview
  shows slide_file regardless. Execute is unaffected -- it resolves first.

The negative half of the mapping is what the tests weight most heavily. The
backend does not validate parent_node against parent_type, so a native deck
misread as office still uploads successfully and only surfaces later as an
image that will not render, far from its cause; the marker check is therefore
pinned at its exact length and offsets rather than a looser "contains OFL0X".

Tests:
- shortcuts/slides/slides_media_parent_type_test.go: 14-case pure-function
  table (off-by-one length, prefix appearing mid-string, wiki placeholder),
  a real-multipart Execute assertion across four token shapes, and the
  +add-slide / +update-slide placeholder dry-run previews.
- tests/cli_e2e/slides/slides_image_upload_dryrun_test.go: five cases through
  the built binary, covering every surface a local file can enter through.
- Verified non-vacuous: short-circuiting the office branch fails all three
  package tests plus the e2e lane.

Evidence note: office_slide_file is confirmed accepted by upload_all, and the
symmetry with office_sheet_file is exact, but this has not been exercised
against a real imported-pptx presentation to confirm slide_file fails there.
2026-08-21 15:12:35 +08:00
tianyouskrrr 7cfbea68d9 feat(slides): normalize replace-slide part aliases (#2225)
Normalize deterministic +replace-slide part aliases (replace→block_replace,
target_id→block_id, and payload field folding), and reject semantically
different actions up front.

Point whole-page actions at +update-slide now that it is GA:
- page_replace / slide_replace recovery guidance and the reference error
  table now direct callers to `slides +update-slide` (in-place whole-page
  rewrite) instead of the deprecated +replace-pages.
- drop the +replace-pages reference doc, completing the #2227 deprecation
  (the command stays; its deprecation signal is carried by --help and the
  output JSON `deprecated` field).

Generalize whole-page validation gating to the behavior rather than a
command name (SKILL.md, workflow/validation-xml.md): "整页写回后" instead of
"每次通过 +update-slide 整页写回后". Folds in PR #2255.
2026-08-10 19:28:03 +08:00
R0bynZhu 46e2186adf feat(slides): accept slide XML files in +create (#2197)
Assembling the --slides JSON array by hand is what callers keep getting
wrong. A page of SML is multi-line and quote-heavy, and shell has no
built-in way to JSON-escape it, so callers reached for `jq -n --rawfile`
to build the array. In environments without jq the substitution silently
became an empty string and the command ran on to create an empty deck,
or the half-escaped XML reached the backend and came back as an opaque
3350001 after the presentation already existed.

Two input forms remove the escaping step:

  --slides now declares Input{file, stdin}, so a finished array can be
  read with `--slides @deck.json` or piped in with `--slides -`.

  --slide is repeatable, takes one complete <slide> document (or @path),
  and the CLI assembles the array. Repetition order is page order.

The forms are mutually exclusive: merging them would make page order
depend on flag-parsing rules nobody wants to reason about.

Notes on the repeatable flag: the framework only resolves Flag.Input for
single-valued string flags, so --slide resolves @path itself, through
the same cmdutil.ReadInputFile the framework uses, keeping the
"relative path under the current directory" rule identical. It rejects
"-" outright, because a process has one stdin and that cannot mean "this
occurrence" on a repeatable flag; the error names both forms that work.

Structural validation now runs on the assembled array, so both forms
fail the same way, and it runs before the create call so a malformed
page can no longer leave an orphaned empty presentation behind.

Three inputs the first round of review found still slipping through are
now rejected or normalized before the create call. `--slides null` is
valid JSON for a slice, so it parsed without error and left the array
nil, which read as "no pages given" and produced the blank deck
reported as success that the empty-value check exists to prevent. An
`<?xml ...?>` prolog is well-formed XML, so the parser accepted it and
only the backend rejected it, with 4001000 buildSnNode, after the
presentation already existed; it is now caught in the shared slide
validator, which covers +add-slide and +replace-pages too. And a
leading UTF-8 BOM made `--slide @page.xml` reject a file that
`--slides @deck.json` accepted, because the framework strips it for
Input flags and the repeatable flag resolved @path itself;
StripUTF8BOM is exported so both paths normalize the same way.

Also threads the source flag name through uploadSlidesPlaceholders,
which previously reported +add-slide upload failures as --slides.

Docs: the create/troubleshooting references now teach the file inputs
instead of the jq array-building template, and the follow-up snippet
uses the CLI's own --jq instead of piping to an external jq.
The lint gate in SKILL.md now names `slides +create` as a whole rather
than only its --slide form.
2026-08-07 17:03:03 +08:00
tianyouskrrr bc0ba2252a fix(slides): restore update-slide skill guidance (#2227) 2026-08-07 16:36:17 +08:00
tianyouskrrr b20f374c18 fix(slides): name the wrong --parts field instead of "non-empty replacement" (#2174)
XML written into a field name this shortcut does not accept — most often
"content", because <shape> nests a <content> child — was silently dropped,
so the part failed the required-field check and reported "requires
non-empty replacement". That reads as "the value is empty", which sends
callers rewriting the value instead of the key.

Reject fields outside the action's own set and name the field the caller
most likely meant, with a correct one-liner attached as a hint. Matching
folds case and separators so "Content", "newXml" and "block-id" resolve
too, while the whitelist itself stays exact: the API accepts only
snake_case, so "Replacement" must be rejected rather than slip through.
Only block_replace and block_insert parts are checked, so missing /
str_replace / unknown actions keep their existing errors, and an
actually-empty payload still reports the non-empty wording.

The alias list covers only names that plausibly carry a fragment. A shape
attribute like "fill" is deliberately absent: whoever writes it means
"recolor this block", not "here is my XML", so answering did-you-mean
"replacement" would be guessing. The unknown-field error already names the
valid set, which is true under either reading.

Docs carry the same constraint at the three points a caller can hit first:
SKILL.md, the +replace-slide reference (warning + counter-examples + error
table), and the read-modify-write workflow. The --parts flag description
now spells the field names out instead of eliding them behind "...".

Note: this tightens parsing. Extra keys inside a part used to be ignored;
they are now rejected.
2026-08-04 18:06:30 +08:00
tianyouskrrr 56fd29e611 feat(slides): add +update-slide for whole-page updates (#2143)
Add an in-place whole-page slide update shortcut with validation, aliases, docs, unit tests, and dry-run E2E coverage.

Deprecate the superseded +replace-pages: the binary keeps the command working for a deprecation window, with the replacement named in its --help description and in a `deprecated` field on every output (dry-run, validate-only and real runs), while the skill no longer routes to it. Multi-page updates now call +update-slide once per page. The XML/revision helpers it shared with +add-slide / +delete-slide move to slides_shared.go so its eventual removal cannot break them.

+add-slide and +delete-slide now declare --presentation through the shared presentation-ref flag, so they accept the same alias spellings (--token, --url, ...) as every other slides shortcut.
2026-08-04 11:06:59 +08:00
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
Yuxuan Zhao 5280517d4b Feat/cli e2e tests with UAT (#528)
* test: expand and stabilize cli e2e workflows

* ci: run deadcode with test entrypoints
2026-04-17 16:57:17 +08:00