feat(mail): HTML lint lib + Larksuite-native autofix + lark-mail skill#5
Open
bubbmon233 wants to merge 61 commits into
Open
feat(mail): HTML lint lib + Larksuite-native autofix + lark-mail skill#5bubbmon233 wants to merge 61 commits into
bubbmon233 wants to merge 61 commits into
Conversation
3 tasks
…suite#392) Introduce three new wiki shortcuts that wrap the corresponding raw APIs with structured flags, formatted output, my_library alias handling, and unified envelope shape, replacing the bare `lark-cli wiki spaces list` / `wiki nodes list` / `wiki nodes copy` flows for the common cases. Shortcuts - wiki +space-list (read, scopes: wiki:space:retrieve): lists wiki spaces. Default fetches a single page; --page-all walks every page capped by --page-limit (default 10, 0 = unlimited). Supports --page-size / --page-token / --format json|pretty|table|csv|ndjson. Output: {spaces, has_more, page_token} + Meta.Count. Pretty mode distinguishes "no spaces" from "empty page with has_more" and hints the caller to resume. - wiki +node-list (read, scopes: wiki:node:retrieve): lists nodes in a space or under a parent. Same pagination + format story as +space-list. Accepts the my_library alias for --space-id with --as user (resolved via a shared resolveMyLibrarySpaceID helper extracted from +node-create); rejects my_library upfront for --as bot. - wiki +node-copy (high-risk-write, scopes: wiki:node:copy): copies a node into a target space or parent. --target-space-id and --target-parent-node-token are mutually exclusive. Risk is marked high-risk-write to match the upstream API's danger: true flag, so the framework requires --yes. Source is preserved; subtree is copied. Both list shortcuts pick the narrowest scope the upstream API accepts. The framework's preflight (internal/auth/scope.go MissingScopes) does exact-string scope matching, so declaring the broader wiki:wiki:readonly form would wrongly reject tokens that carry only the per-API scope — which the API itself accepts — and emit a misleading missing-scope hint. Shared changes - shortcuts/wiki/wiki_node_create.go: factor out resolveMyLibrarySpaceID so +node-list and +node-create share one my_library resolution path. - shortcuts/wiki/shortcuts.go: register the three new shortcuts. - skills/lark-wiki/SKILL.md and references/lark-wiki-{space,node-list, node-copy}.md: documentation for the new shortcuts. Tooling - scripts/check-doc-tokens.sh + Makefile gitleaks target: pre-commit check that scans skill reference docs for realistic-looking Lark token values without the _EXAMPLE_TOKEN placeholder convention, preventing gitleaks false positives. - .gitleaks.toml: allowlist tuning. - .gitignore: ignore .tmp/. Tests - shortcuts/wiki/wiki_list_copy_test.go: unit tests covering registry membership, declared-narrow-scope pinning, flag validation (page-size range, page-limit >= 0, target flag exclusivity, my_library + bot rejection), auto-pagination merging, --page-limit truncation surfacing next cursor, --page-token single-page mode, empty-slice serialisation, has_more hint pretty rendering, my_library user-path resolution, +node-copy copy-to-space / copy-to-parent + body shape, pretty rendering, and the high-risk-write --yes gate. - tests/cli_e2e/wiki/wiki_shortcut_workflow_test.go: live end-to-end workflow exercising the shortcut layer against a real tenant. Reuses an existing my_library node as a host so the test never adds to the top-layer quota; the copy is placed under the same host node. - tests/cli_e2e/wiki/coverage.md: shortcut coverage entries added. Minor cleanups - skills/lark-doc/references/lark-doc-search.md and skills/lark-minutes/references/lark-minutes-search.md: replace realistic-looking example ou_ tokens with _EXAMPLE_ placeholders so scripts/check-doc-tokens.sh passes. Change-Id: I9efb0557f477d369d7f26a09c1e154d4ab15b253 Co-authored-by: liujinkun <liujinkun@bytedance.com>
Change-Id: Icada6fb894aaf9a00187fa68c132d3ade8223b99
…e#832) * feat(doc): add width/height params to buildBatchUpdateData Extend buildBatchUpdateData signature with width and height int params. When mediaType is "image" and either dimension is positive, the value is included in the replace_image payload. Existing call sites pass 0, 0. * feat(doc): add --width/--height flags with validation to docs +media-insert * feat(doc): add aspect-ratio auto-calculation helpers Add computeMissingDimension (pure ratio math) and detectImageDimensions (header-only image.DecodeConfig) with PNG/JPEG/GIF blank-import decoders, plus imageDimensions struct; drive with two new TDD tests. * feat(doc): wire --width/--height into Execute with aspect-ratio calculation * feat(doc): add best-effort dimension computation to DryRun * docs: add --width/--height to docs +media-insert SKILL.md * fix: add SafeInputPath validation to detectImageDimensionsFromPath * fix: guard computeMissingDimension against division by zero and add rounding * fix: add dimension upper bound, fix err variable reuse in Execute * refactor: use early-return guard for zero native dimensions per review * fix: add pixels unit to dimension validation error messages * fix: surface dimension detection failures in dry-run to match Execute behavior * fix: move dimension detection before upload to fail fast * fix: restore withRollbackWarning on dimension detection errors in Execute Dimension detection runs after the placeholder block is created (Step 2), so failures must clean up the block to avoid leaving an empty placeholder in the document.
* fix(drive): preserve parent token on nested overwrite Ensure drive +push overwrite requests for nested files keep parent_node aligned with the actual remote parent folder and report parent resolution failures explicitly. * test(drive): cover nested overwrite push workflow Add a live drive +push workflow case for overwriting a nested remote file so the PR parent-token fix is exercised against the real backend and verified to converge via +status.
Change-Id: I3d1a8ec4faf1ce585fb9eae45287bf02586e3e90
The skill doc claimed wiki list/copy shortcuts default to --as user, but the CLI --as default is `auto` (no --as commonly resolves to bot, listing the app's spaces instead of the user's). Running `wiki +space-list` without --as therefore returns app-scoped data, contradicting the doc. Following the established lark-mail convention (concise user-centric guidance, not a precedence essay): - add a short "优先使用 user 身份" section to SKILL.md - fix the --as rows in lark-wiki-space-list / node-list / node-copy references to show the real `auto` default and steer to --as user Change-Id: I539f8d622c1bbad57f8a64c2fc7b7ecc0dfe2116
9783a06 to
f4e4e5e
Compare
…ite#910) * feat(extension): introduce Plugin / Hook framework with command pruning Add a single public extension contract under extension/platform: integrators implement the Plugin interface and register Observers, Wrappers, Lifecycle handlers, and pruning Rules through the Registrar in one Install call. Command pruning: - Rule (Allow / Deny / MaxRisk / Identities) with doublestar globs - 4-axis AND evaluation, parent-group aggregation, unknown-risk allow - Sources: Plugin.Restrict (single-rule) and ~/.lark-cli/policy.yml - Plugin path is fail-closed (envelope on rule error / multiple Restrict); yaml path is fail-open (warning, CLI continues) - strict-mode stubs now also write the denial annotation so the hook layer's denial guard physically isolates Wrap chains on them - HOME path never leaked through policy_source label Hook framework: - Observer (panic-safe, Before/After), Wrapper (middleware, may short-circuit via AbortError), Lifecycle (Startup + Shutdown only) - Recover guards every plugin entry point: Capabilities(), Install(), Wrapper factory composition AND inner Handler, Lifecycle handlers - namespacedWrap copies AbortError so a plugin's package-level sentinel is never mutated across concurrent invocations - Selector unknown-risk uniform: ByExactRisk / ByWrite / ByReadOnly never match unannotated commands; safety-side hooks opt in via ByWrite().Or(ByUnknownRisk()) Bootstrap orchestration (cmd/build.go + cmd/policy.go): - InstallAll uses a staging Registrar + atomic commit - FailClosed plugin install / Plugin.Restrict conflict / Startup handler failure each install a structured envelope guard at every dispatch path - walkGuard neutralises every cobra bypass we know of (PersistentPreRunE first-wins, ValidateArgs, ParseFlags, legacyArgs, __complete / __completeNoDesc, non-runnable groups, required-arg subcommands) - cmd/root.go::Execute calls hook.Emit(Shutdown, runErr) after rootCmd.Execute; isCompletionCommand skips both __complete and __completeNoDesc so Tab completion never triggers Shutdown handlers Capabilities consistency: - Restricts=true must declare FailurePolicy=FailClosed - RequiredCLIVersion (semver constraint) is validated against build.Version; a malformed constraint is treated as untrusted-config and aborts unconditionally, regardless of FailurePolicy (DEV builds included) JSON envelope contract: - error.type closed enum: pruning / strict_mode / hook / plugin_install / plugin_conflict / plugin_lifecycle - reason_code closed enums per type, all referenced by structured tests Bootstrap surfaces (new user commands): - lark-cli config policy show -- JSON view of the active Rule + source - lark-cli config policy validate -- parse + schema + glob check, no apply Coverage: - extension/platform: every public type has a unit test - internal/{pruning,hook,platformhost,policydecision,cmdmeta}: full coverage of denial guard isolation, AbortError sentinel safety, observer panic safety, lifecycle error/panic typing, staging atomic rollback - cmd/plugin_integration_test.go: end-to-end through buildInternal with synthetic and real command trees - cmd/install_guard_test.go: walkGuard covers auth / config / __complete / __completeNoDesc / non-runnable parents * fix(pruning): deny stub must override Args + PersistentPreRunE The pruning denyStub and the strict-mode stub previously only swapped RunE plus Hidden + DisableFlagParsing. Cobra's dispatch order means several pre-RunE gates can fire BEFORE the stub's RunE ever runs: 1. Args validator: shortcut commands often declare cobra.NoArgs. With DisableFlagParsing=true the user's `--doc xxx --mode append` looks like positional args, so ValidateArgs surfaces a usage error instead of the pruning / strict_mode envelope. Observer hooks also miss the dispatch entirely. 2. Parent PersistentPreRunE: cmd/auth/auth.go declares a PersistentPreRunE that returns external_provider when env credentials are set. Cobra's "first PersistentPreRunE wins walking up from the leaf" then short-circuits with external_provider instead of the leaf's denial envelope. Both stubs now also set: - Args = cobra.ArbitraryArgs (bypass gate 1) - PersistentPreRunE = no-op leaf hook (bypass gate 2) - PreRunE / PreRun / PersistentPreRun = nil (defensive) Effect: dispatch reaches the wrapped RunE, observers fire, the real pruning / strict_mode envelope is emitted regardless of credential provider or flag count. Adds regression tests covering both gates on both stub paths. * fix(config): policy subcommand bypasses parent's credential check cmd/config/config.go::NewCmdConfig declares a PersistentPreRunE that calls f.RequireBuiltinCredentialProvider; with env credentials set, it returns external_provider for every config subcommand. `config policy show` and `config policy validate` are READ-ONLY diagnostic commands -- they inspect or parse the user-layer rule without touching credentials. They MUST work regardless of which credential provider is active, otherwise users on env-credential deployments cannot debug their policy. Same shape as the codex C11/C13 fix: install a no-op leaf-level PersistentPreRunE on the `policy` group so cobra's "first walking up from leaf" rule picks ours over the config parent's. Regression caught by divergent e2e (F1-F6 all returned external_provider before this fix; all pass after). Adds a unit test pinning the PersistentPreRunE override. * feat(shortcuts): tag service groups with cmdmeta.Domain RegisterShortcutsWithContext now calls cmdmeta.SetDomain on each service-level cobra.Command (im, docs, drive, calendar, ...) so the business-domain axis is actually populated on every shortcut leaf via parent-chain inheritance. Before this change, platform.ByDomain("docs") never matched any command: the domain annotation was unset across the entire shortcut tree, so the selector's d != "" guard always failed and risk-style selectors silently degraded to no-op. The SetDomain call is placed AFTER the create-or-reuse branch so it fires whether the service command was freshly created here or had already been added by cmd/service/service.go's OpenAPI auto- registration (which runs first and creates im, drive, calendar, etc.). Without this placement only pure-shortcut services like docs would have been tagged. Adds a regression test asserting: - service-group cobra.Command carries the cmdmeta.domain annotation - leaf shortcuts inherit the domain via parent-chain walk * feat(diagnostic): add unconditionally allowed command paths for introspection * feat(plugins): add diagnostic command to inspect installed plugins and their contributions * fix(cli): surface unknown_subcommand error instead of silent help fallback When a user passed an unknown subcommand or shortcut (e.g. `lark-cli drive +bogus`), cobra returned `flag.ErrHelp` for the non-runnable group command, printed the parent help, and exited 0. AI agents couldn't distinguish a typo from an intentional help request. Install a tree-wide guard that attaches a RunE to every group command without its own Run/RunE. The RunE forwards no-args invocations to help (preserving prior behavior) and emits a structured unknown_subcommand ExitError (exit 2) listing available subcommands when args are present. * refactor(envelope): rename error.type pruning/strict_mode to command_denied The envelope's `type` field was leaking implementation terms ("pruning", "strict_mode") that describe enforcement mechanism rather than the user- facing semantic. It also duplicated `detail.layer`, and forced consumers to branch on two values for the same conceptual error ("a command was denied by policy"). Collapse both into a single semantic type "command_denied". The enforcement layer ("pruning" / "strict_mode") is preserved in `detail.layer` so debugging and per-layer diagnostics still work. * feat(platform): fail closed on unannotated/invalid risk when a Rule is active The pruning engine used to treat any command without a risk annotation as ALLOW even when a Rule with MaxRisk was set, and would silently skip the MaxRisk comparison whenever the command's risk string was outside the closed taxonomy. Both gaps let an unannotated or typo'd write command slip past an "agent read-only" pruning rule. Engine now denies before any other axis when a Rule is registered: - reason_code "risk_not_annotated" for commands with no risk - reason_code "risk_invalid" for commands whose risk is outside the read | write | high-risk-write taxonomy (e.g. typo "wrtie") Main-flow is preserved: a nil Rule still returns Allowed=true unconditionally, so a CLI with no pruning plugin behaves identically to before. ByUnknownRisk() is removed from the public surface since the Unknown state is no longer reachable through risk-based selectors when any Rule is active; safety-side widening composition is no longer needed. * chore(config): hide diagnostic policy/plugins commands from --help `config policy show`, `config policy validate`, and `config plugins show` are local-introspection-only commands kept behind the pruning diagnostic whitelist so operators can always inspect why a command was denied. They do not need to surface in `--help` for AI agents and were contributing to help noise. Hide the `policy` and `plugins` parent groups and both `show` / `validate` leaves. Commands remain callable by exact name and continue to bypass user-layer pruning via diagnosticPaths. * style: gofmt * fix(platform): nil Selector honours None contract; reject multi-doc policy yaml - selector.go: And/Or/Not now treat nil Selector as None() per godoc, preventing runtime panic when composed selectors are invoked. - schema.go: Parse rejects multi-document YAML input so a stray '---' separator can't silently drop trailing policy constraints. * chore: go mod tidy * feat(extension/platform): plugin SDK with policy engine, hooks, and Builder Introduces extension/platform — the in-process plugin SDK external Go forks of lark-cli use to extend or restrict the command surface. Plugins compile in via blank import; there is no dynamic loading and no RPC isolation. Public SDK (extension/platform): - Plugin interface (Name / Version / Capabilities / Install). - Registrar verbs: Observe, Wrap, On, Restrict. - Hook types: Observer (side-effect, panic-safe, fires Before/After RunE), Wrapper (middleware, may short-circuit via AbortError), LifecycleHandler (Startup / Shutdown), Selector with nil-safe And/Or/Not composition. - Risk / Identity are defined string types with closed taxonomies; ParseRisk / ParseIdentity convert raw strings with the absent-vs-invalid distinction the engine relies on. - Builder ergonomic constructor (NewPlugin().Observer().Wrap() ...MustBuild()) that enforces name/hookName grammar, hookName uniqueness, and the Restrict ↔ FailClosed pairing regardless of call order. - Invocation is a read-only interface; the framework's concrete invocation type lives in internal/hook so plugins cannot fabricate denial / strict-mode / identity state. Args() returns a defensive copy on every call so hook mutation cannot leak into the original RunE. - CommandDeniedError + AbortError carry structured fields for the closed `command_denied` / `hook` envelope contract. - ResetForTesting gated behind //go:build testing. - README + godoc examples (Observer / Wrapper / Restrict) + two runnable example forks (audit-observer, readonly-policy). Host (internal/platform, internal/hook, internal/cmdpolicy): - InstallAll: staged plugin registration with atomic commit, panic isolation, FailOpen / FailClosed semantics, RequiredCLIVersion semver check, single-Restrict invariant, duplicate-plugin-name detection. - hook.Install wraps every runnable cmd.RunE with: Before observers (panic-safe) → denial guard → composed Wrap chain → original RunE → After observers (always fire, even on err). Denied commands physically bypass the Wrap chain so a plugin Wrapper cannot suppress or rewrite a denial; observers still see the attempt for audit. - Recover shim around plugin Wrappers converts panics (including the factory call) into a structured `hook` envelope with reason_code=panic; namespacing shim attributes AbortError to the namespaced hook name. - cmdpolicy (renamed from internal/pruning) is the user-layer command policy engine: walks the cobra tree, evaluates each runnable command against a Rule's four-axis filter (Allow / Deny / MaxRisk / Identities), produces parent-group aggregate denials, and installs denyStubs. Rule.AllowUnannotated opts out of the unannotated-deny gate for gradual adoption; risk_invalid typos always deny with an edit-distance "did you mean" suggestion. - Strict-mode stub in cmd/prune.go composes the shared detail.* / wrapped CommandDeniedError shape via cmdpolicy helpers (BuildDenialError / CommandDeniedFromDenial / DenialDetailMap), so command_denied envelopes from strict-mode and user-layer policy carry the same closed-enum fields (detail.layer / reason_code / policy_source). The historical short Message + independent Hint are preserved unchanged. - cmdpolicy/yaml: structural parsing of ~/.lark-cli/policy.yml with KnownFields strict mode, including allow_unannotated. - `config policy show` / `config policy validate` and the plugin inventory diagnostic surface the resolved Rule (allow, deny, max_risk, identities, allow_unannotated) and the hook contributions per plugin. Envelope contract (docs/extension/reason-codes.md): - error.type is a closed set: command_denied, hook, plugin_install, plugin_conflict, plugin_lifecycle. - reason_code is a closed enum per error.type, dispatched on by external agents and CI integrations. - detail.layer = "policy" | "strict_mode" attributes the rejection. Build / CI: - Makefile unit-test / vet / coverage and ci.yml fast-gate + unit-test + coverage now pass -tags testing so register_testing.go is visible; ./extension/... is in the package list so the SDK's own tests actually run. - fmt-check and examples-build Makefile targets. - bmatcuk/doublestar/v4 added as a direct dependency for `**` glob matching in Rule.Allow / Rule.Deny. Author-facing material: - docs/extension/ (quickstart, plugin-author-guide, reason-codes) is provided in the working tree but kept out of git tracking per repo convention (.gitignore covers docs/). Change-Id: I3b8ecc2923bd54c2dff19e5dce8a0855a6f9e703 * feat(extension/platform): plugin SDK with policy engine, hooks, and Builder Introduces extension/platform — the in-process plugin SDK external Go forks of lark-cli use to extend or restrict the command surface. Plugins compile in via blank import; there is no dynamic loading and no RPC isolation. Public SDK (extension/platform): - Plugin interface (Name / Version / Capabilities / Install). - Registrar verbs: Observe, Wrap, On, Restrict. - Hook types: Observer (side-effect, panic-safe, fires Before/After RunE), Wrapper (middleware, may short-circuit via AbortError), LifecycleHandler (Startup / Shutdown), Selector with nil-safe And/Or/Not composition. - Risk / Identity are defined string types with closed taxonomies; ParseRisk / ParseIdentity convert raw strings with the absent-vs-invalid distinction the engine relies on. - Builder ergonomic constructor (NewPlugin().Observer().Wrap() ...MustBuild()) that enforces name/hookName grammar, hookName uniqueness, and the Restrict ↔ FailClosed pairing regardless of call order. - Invocation is a read-only interface; the framework's concrete invocation type lives in internal/hook so plugins cannot fabricate denial / strict-mode / identity state. Args() returns a defensive copy on every call so hook mutation cannot leak into the original RunE. - CommandDeniedError + AbortError carry structured fields for the closed `command_denied` / `hook` envelope contract. - ResetForTesting gated behind //go:build testing. - README + godoc examples (Observer / Wrapper / Restrict) + two runnable example forks (audit-observer, readonly-policy). Host (internal/platform, internal/hook, internal/cmdpolicy): - InstallAll: staged plugin registration with atomic commit, panic isolation, FailOpen / FailClosed semantics, RequiredCLIVersion semver check, single-Restrict invariant, duplicate-plugin-name detection. - hook.Install wraps every runnable cmd.RunE with: Before observers (panic-safe) → denial guard → composed Wrap chain → original RunE → After observers (always fire, even on err). Denied commands physically bypass the Wrap chain so a plugin Wrapper cannot suppress or rewrite a denial; observers still see the attempt for audit. - Recover shim around plugin Wrappers converts panics (including the factory call) into a structured `hook` envelope with reason_code=panic; namespacing shim attributes AbortError to the namespaced hook name. - cmdpolicy (renamed from internal/pruning) is the user-layer command policy engine: walks the cobra tree, evaluates each runnable command against a Rule's four-axis filter (Allow / Deny / MaxRisk / Identities), produces parent-group aggregate denials, and installs denyStubs. Rule.AllowUnannotated opts out of the unannotated-deny gate for gradual adoption; risk_invalid typos always deny with an edit-distance "did you mean" suggestion. - Strict-mode stub in cmd/prune.go composes the shared detail.* / wrapped CommandDeniedError shape via cmdpolicy helpers (BuildDenialError / CommandDeniedFromDenial / DenialDetailMap), so command_denied envelopes from strict-mode and user-layer policy carry the same closed-enum fields (detail.layer / reason_code / policy_source). The historical short Message + independent Hint are preserved unchanged. - cmdpolicy/yaml: structural parsing of ~/.lark-cli/policy.yml with KnownFields strict mode, including allow_unannotated. - `config policy show` / `config policy validate` and the plugin inventory diagnostic surface the resolved Rule (allow, deny, max_risk, identities, allow_unannotated) and the hook contributions per plugin. Envelope contract (docs/extension/reason-codes.md): - error.type is a closed set: command_denied, hook, plugin_install, plugin_conflict, plugin_lifecycle. - reason_code is a closed enum per error.type, dispatched on by external agents and CI integrations. - detail.layer = "policy" | "strict_mode" attributes the rejection. Build / CI: - Makefile unit-test / vet / coverage and ci.yml fast-gate + unit-test + coverage now pass -tags testing so register_testing.go is visible; ./extension/... is in the package list so the SDK's own tests actually run. - fmt-check and examples-build Makefile targets. - bmatcuk/doublestar/v4 added as a direct dependency for `**` glob matching in Rule.Allow / Rule.Deny. Author-facing material: - docs/extension/ (quickstart, plugin-author-guide, reason-codes) is provided in the working tree but kept out of git tracking per repo convention (.gitignore covers docs/). Change-Id: I3b8ecc2923bd54c2dff19e5dce8a0855a6f9e703 * refactor(policy): remove validate command and update diagnostics * fix(extension/platform): address PR review must-fix items - cmdpolicy: skip AnnotationPureGroup commands in EvaluateAll, aggregateParents, and hasRunnableDescendant so user-layer policy no longer blocks `<group> --help` after the unknown-subcommand guard attaches RunE to every parent - cmd/root: tag guarded parent groups with AnnotationPureGroup - extension/platform: drop `//go:build testing` from register_testing.go so `go test ./...` works without an extra build tag - extension/platform/README: inline reason_code reference, fix plugin lifecycle diagram order (init/Register precede RegisteredPlugins) - cmd/platform_bootstrap: route userPolicyPath through core.GetBaseConfigDir so LARKSUITE_CLI_CONFIG_DIR is honoured - cmdpolicy: add RedactHomeDir helper, fold base config dir and $HOME prefixes for config policy show + resolver errors - internal/platform: reject unrecognised FailurePolicy values with invalid_capability instead of silently fail-open - cmd/config: surface diagnostic policy/plugins commands in `config --help` Long text - CHANGELOG: document command_denied error.type rename and unknown_subcommand exit-2 behavior change * fix(extension/platform): address CodeRabbit review comments + CI gofmt - hook/install: propagate wrapper-injected ctx to invokeOriginal so RunE/Run see context values added by upstream Wrappers - hook/testing: SetStderrForTesting returns a restore func; tests now defer it via t.Cleanup to avoid cross-test sink leakage - cmdpolicy/active: deep-copy ActivePolicy.Rule on SetActive/GetActive so callers can't mutate the stored global through shared slices - platform/inventory: deep-copy Inventory + nested Plugins / HookEntry / RuleView slices on SetActiveInventory / GetActiveInventory - platform/staging: Restrict clones the plugin-supplied Rule before retaining it so the plugin can't mutate it after Install returns - platform/version: reject RequiredCLIVersion with more than three numeric components instead of silently truncating 1.2.3.4 to 1.2.3 - cmd/platform_bootstrap: clear cmdpolicy.SetActive on yaml resolver error so config policy show doesn't surface a stale rule - cmd/platform_bootstrap_test: tmpHome pins LARKSUITE_CLI_CONFIG_DIR so host env can't bleed into the policy test fixtures - cmdpolicy/apply: installDenyStub returns bool; Apply count no longer over-reports when strict-mode short-circuits the install - cmdpolicy/engine: aggregateParents now returns the runnable hybrid's own denial status when all children are placeholder branches - cmdpolicy/resolver_test: use t.TempDir()-rooted missing path instead of hardcoded /nonexistent for hermetic missing-file assertion - cmd/config/plugins: empty-inventory branch emits total: 0 so the JSON schema stays stable across populated/empty cases - cmd/platform_guards_test: select leaf by RunE != nil (not Runnable) so the test doesn't nil-deref on Run-only commands - gofmt run on previously committed cmdpolicy/path*.go (CI fast-gate) * fix(cmdpolicy): replace filepath.Abs with filepath.Clean for lint policy The depguard / forbidigo rule blocks filepath.Abs in internal/ on the grounds that it accesses the filesystem (Getwd) directly. Switch RedactHomeDir + foldPrefix to operate on filepath.Clean strings; real callers pass already-absolute paths (resolver builds yamlPath via filepath.Join on the absolute config root), so the redaction outcome is unchanged for production inputs. Relative inputs fall through to the unchanged branch — filepath.Rel rejects the mixed-absoluteness case with an error, which the foldPrefix helper already treats as "not a hit". * refactor(cmdpolicy): pure Resolve + drop path redaction & verbose comments - Resolve becomes a pure function; I/O moves to LoadYAMLPolicy so precedence selection can be unit-tested without vfs mocks - ActivePolicy drops YAMLPath; config policy show JSON loses yaml_path and yaml_shadowed (and the TOCTOU stat that surfaced them) - RedactHomeDir and path_test.go removed: the home-dir folding was only earning its keep through the now-deleted yaml_path field - cmd/build.go bootstrap block trimmed from 71 to 39 lines by cutting PR-rationale comments; one note kept for the fail-CLOSED-vs-fail-OPEN business rule - cmd/config/config.go: parent Long no longer hard-codes hidden command hints, matching their Hidden:true intent Change-Id: Icfbb818ce3ef523c63286bfbed34c49be08ed6a2 * refactor(platform): drop StrictMode/Identity from Invocation interface These two accessors were documented in the public SDK as "After observers always see ok=true" but the framework never plumbed values to them, so they always returned ("", false). Zero internal/example/test callers; a plugin author trusting the doc would silently get wrong behaviour. Identity is also fundamentally unsuited for Before observers (per-command identity resolves inside RunE via f.AuthFor, after Before fires). StrictMode is a global value better placed on a Framework/Environment interface than per-Invocation. Removing is non-breaking now (no callers); adding later is non-breaking too. Change-Id: Ice200543e9bca3bda759ad98a6e34a56df69e915 * fix(prune): preserve original metadata on strict-mode denial stubs strictModeStubFrom built a fresh *cobra.Command from scratch, dropping the original command's annotations (risk_level, lark:supportedIdentities, cmdmeta.domain) and help text. cobraCommandView is a live proxy walking parent annotations, so after the Remove+Add replacement, audit observers firing on a strict-mode-denied command saw Cmd().Risk()=("",false) and Cmd().Identities()=nil -- breaking the first-class use case for audit/compliance plugins. Copy child.Annotations into the stub (stamping the denial annotations on top) and propagate Short/Long for help-text parity with cmdpolicy/apply.go::installDenyStub, which preserves these by virtue of mutating in place. Regression test asserts risk_level / supportedIdentities / Short / Long all survive replacement, alongside the denial annotations. Change-Id: I19810a34575996344b63e839066888c154d69335 * chore(platform): align docs with implementation; fold home in yaml warnings Followup cleanup to the previous three refactor commits, addressing review fallout where public docs / examples / contract notes still pointed at deleted symbols or unimplemented designs: - cmd/build.go: Build() docstring now mentions the plugin install + Startup emit side effects; Shutdown only fires on Execute path - extension/platform/doc.go, lifecycle.go, invocation.go: drop references to the deleted StrictMode/Identity methods, restore minimal Godoc on Cmd/Args/Started - extension/platform/view.go, cmd/platform_bootstrap.go, internal/hook/install.go: rewrite "snapshot before pruning" promise to match the actual contract (live view + strict-mode stub metadata preservation) - cmd/platform_guards_test.go: stubInvocation drops the two old methods - cmd/platform_bootstrap.go: redactHome() last-mile folds $HOME -> ~ in warnPolicyError so an os.PathError carrying the absolute policy path does not leak the user's home dir to stderr / agent / CI logs - examples/readonly-policy/README.md: drop yaml_path from the sample `config policy show` envelope (the field was removed in 52cbb92) Change-Id: I2874cc2cf9225dfa44a9c07b2449149181b387cb * chore(build): drop vestigial -tags testing from Makefile and CI The `testing` build tag was introduced in 461e3c6 to gate extension/platform/register_testing.go (ResetForTesting); PR review 0efee93 then dropped the //go:build testing directive from that file so downstream `go test ./...` would work without the tag, but never cleaned the matching tag references out of Makefile and ci.yml. The result: 8 places passing -tags testing for a tag that nothing in the repo actually gates, plus a Makefile comment that confidently claims a gate exists. Net behaviour is identical to omitting the flag; the only effect is misleading developers into believing there is a test-only surface separation. Drop the flag from vet / unit-test / lint / coverage / deadcode (head + base worktree) and remove the misleading comment. ResetForTesting's public-API exposure was the conscious trade-off taken in 0efee93 and is left untouched. Change-Id: If0cd78c87d4aec2a2533419fe75b01aae6b165fd * feat(cmdpolicy): enrich denial Reason with attempted value + rule constraint The envelope reason for command_denied previously told the caller WHAT axis failed but not the concrete values on each side, so an AI agent reading the envelope could not tell which command identity / risk / path was attempted vs. which the rule permits. The natural temptation was then to recommend modifying the rule -- exactly the wrong nudge, since policy exists to prevent the agent from rewriting its own limits. Each Reason now carries both the attempted value and the rule's constraint: identity_mismatch: "command supports identities [user]; rule allows [bot]" domain_not_allowed: "command path \"drive/+upload\" not in allow list [docs/** contact/**]" command_denylisted: "command path \"docs/+delete-doc\" matched deny pattern \"docs/+delete-*\"" risk_too_high / write_not_allowed: "command risk \"high-risk-write\" exceeds rule max_risk \"write\"" risk_not_annotated: "command has no risk_level annotation; rule denies unannotated commands" (drops the prescriptive "set allow_unannotated=true" hint -- that belongs in docs, not in the engine's denial path) Adds firstMatch() helper so command_denylisted can name the specific glob that fired; matchesAny() now wraps firstMatch. Regression test pins the substring contract per reason_code so future "comment cleanup" cannot silently strip the values out again. Change-Id: I17c7cc9411f58e3e43ade5e1ce875f3b7fe3e5ea * fix(cmdpolicy): gofmt engine_test.go CI fast-gate flagged the test added in 2eb0c2b as unformatted. Local make unit-test had it cached; should have run `make vet` (which runs gofmt-equivalent check via fmt-check) before pushing. Trivial 3-line indent fix. Change-Id: I42297ae59f607b97b32e976c9ec1c9ec4ab7de21 * feat(cmd): annotate risk_level on all hand-written cobra commands Without this, any non-empty user-layer policy.yml (default allow_unannotated=false) denies these commands with reason_code risk_not_annotated -- bricking auth login, config init, profile use etc. on first contact with a policy. cmdpolicy/engine evaluation now resolves to the intended axis (deny list / allow list / max_risk / identities) instead of failing closed on the unannotated gate. Policy authors can write `max_risk: write` or `allow: [auth/** config/** ...]` to express real intent. Classification: read auth status/check/list/scopes, config show / policy show / plugins show, doctor, completion, schema, profile list, event list/status/schema/ consume write auth login/logout, config init/bind/remove/ default-as/strict-mode, profile add/remove/ rename/use, event stop/_bus, api (raw transit) high-risk-write update (replaces the CLI binary; failure can leave the install broken) Notes: - api standalone is conservatively `write`; per-call risk is unknown at parse time (raw transit), so static gating only enforces the write-class minimum. - event _bus is the hidden IPC daemon forked by consume; standalone invocation by users is not expected, but the annotation keeps policy evaluation consistent with the other event subcommands. - The two diagnostic-allowlisted commands (config policy show / plugins show) still bypass the engine via diagnosticPaths; the read annotation is for consistency with surrounding leaves. --------- Co-authored-by: liangshuo-1 <266696938+liangshuo-1@users.noreply.github.com>
Change-Id: I87bb32c86e3c3362f541ccc6320c656eb795ec9b
…larksuite#935) Two DryRun functions in the sheets shortcuts called json.Unmarshal without checking the return value. This looks like a bug, but Validate already parses and validates the same --style / --data JSON before DryRun runs, so the error is structurally impossible at this point. Use _ = assignment + comment to silence the unchecked-error lint warning and make the safety invariant explicit to future readers. Co-authored-by: KhanCold <KhanCold@users.noreply.github.com>
Bidirectional sync between a local directory and a Drive folder with diff detection (new_local, new_remote, modified, unchanged) and conflict resolution strategies (--on-conflict: remote-wins, local-wins, keep-both, ask). Key behaviors: - Type conflict detection: hard-fail when local file vs remote non-file or local directory vs remote file - Keep-both: rename local with __lark_<hash> suffix, then pull remote; occupied map includes localDirs to prevent suffix collision - Local-wins partial-success: prefer returned file_token on upload failure - Empty directory mirroring: pre-create local dirs on Drive via drivePushWalkLocal before scope preflight - Structured errors throughout (output.Errorf / output.ErrWithHint) Includes unit tests and E2E tests (dry-run + live workflow).
* feat(auth): add QR code support for device auth flow * docs: update login QR code display hints for AI agent * feat(auth): add ASCII QR code support for auth flow * docs: add comments for login and auth helper functions * chore: remove unused qrCodeToBase64 helper function * fix(auth/login): clarify verification_url handling in login hint
…ite#847) refactor(slides): rename slide layout lint scope Change-Id: I1b0e42b6508ec2c5f6ae6dc0d1b7ac23c5bbe2e3 feat(slides): improve lark slides skill guidance Change-Id: I49563da4ca623a89f5391f36ceb8f5a31417e321 feat(slides): strengthen lark slides planning guidance Change-Id: If49330e1f9b779bc76a919565ed61a31c255f508 feat(slides): remove lark slides layout lint rules Change-Id: I64f1fc3b33d05c069c9ef58e61d00aa57ac18ecd refactor(slides): streamline skill guidance Change-Id: I3b39faaab7dcac52fac1572590fc5d8934428da5 feat(slides): add slides asset planning guidance Change-Id: I37303043f7704e4ba484552158390a4e24bf9c42 feat(slides): add visual planning guidance Change-Id: Idee7c392d41ff02124313d572c547d0a086d9c35 feat(slides): add lark slides planning layer Change-Id: I3f0765aa53656070d9ba9b388dade19355e7bc6f
* feat: add markdown +patch shortcut Change-Id: I8159941ff9dec4e5cbf0c757ec19ee172b302224 * fix: align markdown patch validation and dry-run Change-Id: I98079901e980b74998938afc4917b91a79689948
…te#942)" (larksuite#950) This reverts commit 7af616b.
Change-Id: Iea77769a6a0f4e77e8946b72ddb619782be3ea42
Change-Id: I3e04a82f622853549f11ac49cbd6fefa194c7c56
…arksuite#904) - +node-get: wrap wiki.spaces.get_node; accepts node_token, obj_token, or a Lark URL (URL path auto-infers obj_type); formatted output with creator / updated_at. No synthesized url — get_node returns none and a BuildResourceURL fallback is a non-canonical link that misleads in a read/confirm command (sibling read shortcuts omit it too) - +node-delete: wrap space.node delete; high-risk-write (--yes gated), async delete-node task polling, auto-resolves space_id via get_node when --space-id omitted, actionable hints for codes 131011 / 131003. The delete-node task result lives under the gateway's generic `simple_task_result` key (NOT `delete_node_result`) - +space-create: wrap spaces.create; user-only identity, --name required (no empty-name spaces), flattened space output, no url - factor the shared wiki async-task poll loop into wiki_async_task.go; preserve upstream Lark Detail.Code on poll exhaustion (no longer rebuilt via lossy ErrWithHint) - drive +task_result: add wiki_delete_node scenario so +node-delete's async-timeout next_command actually resolves - skill docs: reference pages for the 3 new shortcuts + SKILL.md shortcuts table (no raw nodes.delete API exists — it's shortcut-only, so it is intentionally absent from API Resources / permission table); drop the circular TestWikiShortcutsIncludeAllCommands change-detector Change-Id: I316f78290cec5bc50f80d629173e3bf2a35dd005
* feat: support base attachment APIs * fix: handle duplicate base attachment downloads * fix: remove unused attachment token helper
Test files legitimately need to construct dangerous Unicode inputs (RLO, ZWSP, BOM, etc.) to verify validation logic rejects them. bidichk treats decoded \u escape literals as Trojan Source risks, which is a false positive for intentional test data. Change-Id: I555028a992ab008da16129eb41075c333d0099b8
…t --set-priority (larksuite#779) Add a Priority field to DraftProjection populated from the EML header pair X-Cli-Priority (CLI/OAPI primary) → X-Priority (RFC fallback for IMAP-回灌 historical drafts), with case-insensitive lookup via the existing headerValue helper and a local mapping table aligned with the backend gopkg/mail_priority.PriorityValueToType vocabulary. When neither header is present (the symmetric read of --set-priority normal=remove_header) the projection emits "unknown" so agents have a stable read-side surface. Append one notes entry to buildDraftEditPatchTemplate documenting the --set-priority flag and the X-Cli-Priority translation contract. The write-side (--set-priority flag, parsePriority helper, translation branch in mail_draft_edit.go, EML header target) is unchanged — already shipped on master. sprint: S4
* docs(lark-im): clarify message activity search Change-Id: I2a9a928aab2354dfaf103cdf53add435088ff9e2 * docs(lark-im): keep bot history guidance additive Change-Id: I6d89610db9f9d1488f207dcc6b92f7aada839f8b
Change-Id: I637cfaf2d6a228c43e3b3041fef8e030bc80b9d0
* docs(lark-vc): clarify meeting search evidence flow Change-Id: I997ec0654b9448eb0cc6ed7c15493dd2316ffa39 * docs(lark-vc): clarify pagination precedence Change-Id: Icdcc38db2ce3db3a3371c6451624fd52a71170e3
Change-Id: I0908c20f6ab9cf76a5d75cc1c81871591aa6a841
… file blocks (larksuite#825) * feat(doc): warn before overwrite when document contains whiteboard or file blocks Before executing an overwrite in v1 mode, pre-fetch the current document and scan the Markdown for <whiteboard> and <file> resource blocks. If any are found, print a warning to stderr listing the counts and suggesting the user take a backup with `docs +fetch` first. Overwrite replaces the entire document and cannot reconstruct these blocks from Markdown; previously the data was lost with no indication to the caller. The check is best-effort: a failed pre-fetch silently skips the guard rather than blocking the overwrite. * test(doc): add validateSelectionByTitleV1 tests and drop redundant empty-md guard in warnOverwriteResourceBlocks * fix(doc): use regex for resource block detection, add latency/coverage comments, document skip_task_detail purpose
* feat: add markdown +diff shortcut Change-Id: I7da27889517707ac6f1d5e8c429e4bdfb49fdcf8 * fix: harden markdown diff downloads Change-Id: I0020e14ebee780617d790836af1368db851b8cf1 * refactor: address markdown diff review feedback Change-Id: I0ddb852218ec4784c0f9491896796c3007f04122
Change-Id: If08f236c8ae351f92683f2b861cc999eb6f1d22d
* docs: prefer local comments for drive reviews Change-Id: Ie2eaa54320cd2612b66b2d617750d23b950e38db * docs: align drive comment fallback guidance Change-Id: Ia7512babe3656b57374c86068198c8192871ff81
…ds owner semantic (larksuite#951) docs +search is in maintenance and will be removed; cloud-space resource discovery is consolidated onto drive +search. Two related doc/help fixes: 1. Redirect guidance: docs +search -> drive +search - skill-template/domains/{doc,sheets}.md - lark-base/SKILL.md: --filter '{"doc_types":["BITABLE"]}' -> --doc-types bitable - lark-sheets/SKILL.md: body + frontmatter description, add drive-search ref link Same server API, equivalent capability; only flattens the entry from nested --filter JSON to flags. reference links repointed to lark-drive. 2. Fix creator_ids/--mine semantic: creator -> owner The server matches creator_ids (incl. --mine / --creator-ids) by owner (document owner), not original creator, despite the OpenAPI field name. - shortcuts/drive/drive_search.go: --help Desc and Tip - lark-drive/references/lark-drive-search.md: identity section, params, rules, examples - lark-drive/SKILL.md: top-level guidance - lark-doc/references/lark-doc-search.md: creator_ids usage note (now self-consistent) Wire field name creator_ids kept (aligned with the server). Docs/help strings only, no logic change; gofmt / go vet / package build pass. Change-Id: If3ebf5a247b7e38b58050c677dc888a310f1c6b6
Change-Id: I5ba1991874e262fb98f3421e61503b58bb71d861
* feat: add incremental skills sync * fix: address skills sync review feedback
Add an HTML lint library + Larksuite-native autofix to lark-cli mail, plus the skills/lark-mail/ skill bundle (2 reference docs, 5 HTML templates, the +lint-html shortcut, and writing-path lint integration across all 6 compose shortcuts). Lint library (shortcuts/mail/lint/) - Error: drop dangerous tags (<script> / <iframe> / <form> / <input> / <link> / <object> / <embed>), on* event handlers, javascript: / vbscript: / file: URLs. - Warning + autofix: rewrite HTML4-era <font> / <center> / <marquee> / <blink>. - Larksuite-native autofix: rewrite <p> / <ul> / <ol> / <li> / <blockquote> / <a> to mail-editor native markup so AI can write the simplest HTML and still produce native-quality rendering. - Inline-style and URL-scheme allow-list filtering. - <style> block passthrough (server adds CSS scope class). +lint-html shortcut (preview / CI) Read-only HTML preview tool. Default envelope returns only cleaned_html; --show-lint-details adds full warnings[] / errors[]. --strict exits non- zero on any finding (CI gate). Writing-path lint in 6 compose shortcuts +send / +draft-create / +reply / +reply-all / +forward / +draft-edit body op all run lint before drafting: - lint_applied_count / original_blocked_count: always present. - lint_applied[] / original_blocked[]: only with --show-lint-details. - compose_hint: points AI consumers to the HTML writing guide. skills/lark-mail/ skill bundle 5 pre-rendered Larksuite-native HTML templates: weekly newsletter, personal weekly report, team weekly report, market research report, résumé. 2 reference docs: - references/lark-mail-html.md: writing rules + format primitives + template-usage flow. - references/lark-mail-lint-html.md: +lint-html usage + return-value contract + 9 worked examples. SKILL.md updates linking the new docs and templates. Sealed conventions - @user mention chip: id="at-user-N" is the only hard requirement; do not write data-user-id. - Highlight palette: 3 colors (pink milestones, yellow follow-ups, green completed); black text, no bold / padding / border-radius. - Brand color palette: main black, 3 levels of grey, Lark blue / deep blue, alert red, emergency orange, light pink / light grey backgrounds, border grey. - URL scheme allow-list: http(s): / mailto: / cid: / data:image/* only. - Inline-style + tag allow-lists. - Writing-style floor: subject <= 50 chars, decision-first, lists instead of mechanical numbering, emoji only as status tags. Tests - shortcuts/mail/lint/...: unit tests for every rule. - shortcuts/mail/mail_lint_html_test.go: +lint-html envelope contract. - shortcuts/mail/mail_lint_writepath_test.go: writing-path envelope contract. - 5 templates verified via +draft-create smoke test.
Move data["lint_applied_count"] / data["original_blocked_count"] assignments inside the showDetails branch in applyLintToEnvelope so all four lint fields enter and leave the envelope together. This restores the default 3-key envelope (compose_hint / draft_id / reference) for the compose-family shortcuts (+send / +reply / +reply-all / +forward / +draft-create / +draft-edit) and keeps the four lint fields behind --show-lint-details as the tech design intends. sprint: S2
Remove tip field from buildDraftSavedOutput's returned map and flip the test assertions in TestBuildDraftSavedOutputIncludesReferenceOnlyWhenPresent to require tip absence. The compose-family default envelope (+send / +reply / +reply-all / +forward) now stays within the 3-key contract (compose_hint / draft_id / reference) defined in tech-design v1 §4.1.5. hintSendDraft already writes the equivalent guidance to stderr, so no UX regression — the message reaches the user via the dedicated stderr hint channel instead of the structured stdout envelope. sprint: S3
This reverts commit fe001f2.
+draft-create now always attaches a fixed draft_edit_hint to its stdout envelope (alongside compose_hint + draft_id), guiding callers to edit the existing draft via +draft-edit --draft-id <id> instead of re-running +draft-create and producing duplicate drafts. The hint is single-target: only MailDraftCreate emits it; the other 5 compose shortcuts (+send, +reply, +reply-all, +forward, +draft-edit) keep their existing envelope shape unchanged. applyLintToEnvelope no longer writes lint_applied_count or original_blocked_count. Under --show-lint-details the envelope returns only the two Finding arrays (lint_applied[] / original_blocked[]) — callers needing a count compute it via len(arr). The change propagates to all 6 compose shortcuts via the shared helper. Tests, the shortcut flag description, and the lark-mail-html / lark-mail-lint-html reference docs are updated to match. sprint: S2
…tcut structs (PR 787 followup) The 4 compose shortcuts (+send, +reply, +reply-all, +forward) were missing the `HasFormat: true` field in their Shortcut struct literals, so cobra parse rejected `--format json` with `unknown flag: --format`. This blocked the JSON envelope output path (compose_hint / lint envelope) that the verify suite exercises. The fix mirrors the existing positive siblings in the same package (mail_draft_create.go:43, mail_draft_edit.go:29, mail_lint_html.go:43): add a single line `HasFormat: true,` between `AuthTypes:` and `Flags:` in each of the 4 Shortcut struct literals. No new manual --format flag entry is added; the framework auto-registers --format via runner.go when HasFormat == true. Refs: verification_report §3.1 (fail_code_bug for INT-CLI-Send-DefaultHidesStrip-01)
+draft-edit only accepted body edits via --patch-file (set_body op), causing "unknown flag: --body" when called the same way as +draft-create. This adds --body as a convenience flag that translates directly into a set_body patch op, making all mail compose shortcuts consistent. - Add --body flag to MailDraftEdit.Flags - In buildDraftEditPatch: if --body is set, prepend a set_body op; mutual-exclusion check prevents combining with --patch-file body ops - Update patch template notes to reflect the new --body shorthand - Existing lint pipeline (Execute loop over ops) already handles the new op: HTML is sanitized, envelope hides lint_applied/original_blocked by default unless --show-lint-details is passed Fixes: INT-CLI-DraftEdit-DefaultHidesStrip-01 and dependent cases
Bug 1: Template HTML <li> elements with class "temp-li bullet2" or "temp-li bullet3" were missing the "bullet1" class required by the STYLE_LIST_ITEM_NATIVE_INLINE_APPLIED lint rule, causing 18+ warnings in --strict mode for PersonalWeekly/TeamWeekly/Resume templates. research--market-report.html used native <li> elements which were fully converted to Feishu-native list format (ul/ol + li with all required class, data-* attrs, style props and text span wrapping). Bug 2: +send lacked --body-file flag, breaking the E2E workflow: +lint-html → save cleaned.html → +send --body-file ./cleaned.html Added --body-file flag (mutually exclusive with --body) that reads the HTML body from a file. Matches the pattern in +lint-html.
- Add class="not-doclink" to all 20 mention-chip <a id="at-user-N"> elements (fixes 20× STYLE_LINK_NATIVE_INLINE_APPLIED warnings in --strict mode) - Change class="temp-li number2" → "temp-li number1 number2" on the two <li data-start="a|b"> sub-items in 下周工作 nested ol (fixes 2× STYLE_LIST_ITEM_NATIVE_INLINE_APPLIED warnings) - Add start="1" to the outer wrapper <ol> of the nested weekly-next sub-list (fixes 1× STYLE_LIST_NATIVE_INLINE_APPLIED warning; ensureAttr saw start absent) All 23 warnings eliminated; +lint-html --strict should now exit 0.
Replace 11 <p> tags with <div> elements (preserving inline styles) to eliminate STYLE_PARA_WRAPPER_REWRITTEN warnings. The lint engine rewrites <p> to Lark-native double-wrapped div paragraphs and emits a warning for each; using <div> directly avoids the rewrite. Also add class="not-doclink" + native link inline styles to 3 bare <a> tags in the PR-reference section, eliminating STYLE_LINK_NATIVE_INLINE_APPLIED warnings. Verified with lint.Run() directly: blocked=0 applied=0 (0 findings). All 5 templates now pass +lint-html --strict with 0 findings. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Block + Major + selected Nit items from the 2026-05-20 code review. lint lib (shortcuts/mail/lint): - Drop opts.Strict / opts.AutoFix from Options; lib always autofixes warnings and removes errors (writing-path safety contract). Strip the corresponding branches from Run / processElement / processAttributes / applyFeishuNativeStyles. - Make data-ol-id deterministic: per-Run nativeCtx maps each <ol> node to a positional index and nodeShortID hashes that index instead of the heap pointer. cleaned_html is now byte-stable across runs. - processAttributes style branch: preserve attr.Val verbatim when no property dropped (avoid spurious whitespace differences on idempotent input). - Add LIST_DIRECT_CHILD_NON_LI rule + autofix: wrap non-<li> direct children of <ul>/<ol> (notably <ul><ul> nesting) in a synthetic <li>. - Scrub user-facing "RemoteSanitizer" / "server-side sanitizer" / "Lark mail-editor" wording from hints + code comments. Hint text now describes the client-side action only. +lint-html (shortcuts/mail/mail_lint_html.go): - Remove --strict and --auto-fix flags. cleaned_html always emitted; command never bumps exit code (preview / advisory tool). - Drop the corresponding strict / autofix=false tests. Writing-path (compose 5 + draft-edit): - Extract shared readBodyFile() + validateBodyFileMutex() helpers with a 32 MB size cap (io.LimitReader). Both --body-file callers route through them. - Add --body-file flag to +draft-create / +draft-edit / +reply / +reply-all / +forward (was only on +send). Each implements mutual exclusion with --body + cwd-subtree path safety. - Fix +send body-file + --inline silent inline-image drop: Validate now resolves the body content first so the plain-text + inline combination errors out the same way --body 'plain' + --inline does. - Remove unused lintFinding type alias; callers reference lint.Finding directly. Templates (skills/lark-mail/assets/templates): - Wrap inner <ul>/<ol> in <li> in weekly--team-report.html (4 places) and job-application--resume.html (4 places) so they conform to the new ul/ol direct-child rule and have valid HTML structure regardless. Tests: - Update TestRunWritePathLint_HTMLAlwaysAutofixedWarningNeverElevated contract (was the +strict variant) to assert never-elevated behaviour. - Add e2e write-path lint tests for the 5 other compose shortcuts: +send / +reply / +reply-all / +forward / +draft-edit — each asserts the <font> autofix reaches the captured EML before the API call. - Add plain-text + --show-lint-details corner case test that locks empty-but-non-nil arrays in the envelope. - Add ul/ol direct-child-non-li lint test (3 cases). Docs: - skill-template/domains/mail.md: fix broken references (the two separate allowlist / feishu-native docs were merged into one lark-mail-html.md long ago); JSON example no longer mentions internal service names. - skills/lark-mail/SKILL.md:63: section pointer now lands on a real references/lark-mail-html.md anchor instead of a phantom in-file section. - 6 references/lark-mail-*.md: add --body-file row alongside --body.
Previous rgb(225,77,42) = #E14D2A has hue 11.5° which falls in the red-orange boundary and looks almost identical to the 警示红 (h=1.7°) in real-world mail-client rendering. Visual reviewer (用户在飞书 mail 客户端测 A23 调色盘 case) confirmed they could not perceive the orange semantic. Bump to rgb(255,140,40) = #FF8C28 (hue 30°, saturation 84%) which is unambiguously orange and clearly distinct from 警示红 (h≈2°) and 紧急 橙 (h≈30°) now have ~28° hue spacing. References: - A23 V2 testcase 实测视觉反馈 - HSV 色相分析: standard orange hue is 20-50°
06ab8f7 to
c7f9192
Compare
之前的示例把多级列表写成"独立 ol + 独立 ul + 独立 ol(接续编号)"
兄弟堆叠的形式,且内部 ul 直接套 ul(违反 HTML 规范要求 <ul>/<ol>
的直接子节点必须是 <li>)。
实测视觉效果(lcpr +send 发文档原样示例到飞书 mail 客户端):
- 编号 1 / 2 的子项不缩进于父项(独立列表跟编号项是兄弟)
- lint autofix 检测到 <ul><ul> 不合规,wrap 出空 <li class="bullet1">
导致显示空圆点 marker("点后没文字"现象)
修正:合并 3 个独立 ol/ul 为单一 ol,子 ul 嵌套在父 <li> 内:
<ol>
<li>第一级
<ul>
<li>第二级
<ul><li>第三级</li></ul> ← 嵌套在父 li 内,不是兄弟
</li>
</ul>
</li>
<li>第一级接续编号</li>
</ol>
同时去掉显式 start="1" / start="2" / data-start,同一 ol 内 li
顺序自动编号,无需手动指定。
注释里补充说明"<ul>/<ol> 的直接子节点必须是 <li>"以防再次误用。
之前修正后的示例混用 ol+ul(外层 ol,内层 ul/ul),读者不容易看清 "全 ol 多级"和"全 ul 多级"分别该怎么写。拆成两个独立示例: - 示例 1:全 ol 三级(decimal → lower-alpha → lower-roman) class 用 number1/number2/number3 区分层级 - 示例 2:全 ul 三级(disc → circle → square) class 用 bullet1/bullet2/bullet3 区分层级 两个示例都遵循同样的嵌套规则(子列表放在父 <li> 内、子级 margin-left 24px 视觉缩进),通用规则单独提到注释顶部不重复。
之前两个模板把多级列表写成"独立 ol(start=N) + 独立 blockquote +
独立 <ul><li><ul>...</ul></li></ul> 三明治"的兄弟堆叠模式,结构上:
- 多个独立 ol 拆开导致编号靠 start="N" 硬编码续接,写错就乱
- 三明治外层 ul 内只有一个空 li 包子 ul(违反 HTML 规范 ul 不能直接套 ul)
- autofix wrap 出空 <li class="bullet1"> 显示默认圆点 marker("点后没文字"现象)
修正:每段合并成单 ol,子项 ul 嵌套在父 <li> 内,去掉 start="N" /
data-start="N"(同 ol 内 li 自动连续编号)。三级使用
list-style-type: decimal → lower-alpha → lower-roman 区分层级。
具体变动:
- weekly--team-report.html
- 本周工作段:3 ol + 3 blockquote + 3 三明治 ul = 9 个兄弟块
→ 单 ol 含 3 li(事件 1/2/3),blockquote / 子项 ul / 孙子项 ul
都嵌入父 li 内
- 下周工作段:2 ol + 1 三明治 ol = 3 个兄弟块
→ 单 ol 含 4 li(重点 1/2/3/4),重点 2 li 内嵌 lower-alpha 子 ol
- job-application--resume.html
- 工作经历 + 项目经历 段同模式重写(单 ol 含多 li 嵌套)
- 顶部 cover letter 简化:3 行"尊敬的 xxx:/ 您好!/ 长正式介绍"
→ 2 行"[称呼],您好:/ 1 句直接介绍",符合标准中文求职邮件开头风格
验证:两个模板分别跑 lcpr +lint-html --show-lint-details:
- 关键结构 bug 0 个(无 LIST_DIRECT_CHILD_NON_LI 触发)
- cleaned_html 回写再 lint = 0/0 idempotent
- 剩余 STYLE_LIST_NATIVE_INLINE_APPLIED warning 是 lcpr 加 canonical
属性顺序的 autofix 提示,跟 reference doc 自身列表示例同行为
将 spec §/S2 contract/KB Pitfall/KB conventions/technical-design/editor-kit/ renderer-side CSP/服务端 sanitizer 等内部引用全部改写为公开读者可理解的中立表述: - spec §4.x 章节号:删除引用锚点,保留注释描述的规则 - S2 contract «XXX»:删除规范名和书名号,有价值的描述改为普通自然语言 - KB Pitfall N / KB conventions:删除知识库引用,保留实际技术说明 - technical-design §4.4:改为 three-tier tag classification - editor-kit:改为 Feishu mail-editor's renderer - renderer-side CSP:改为 the rendering layer's CSP - 服务端 sanitizer:改为客户端兼容性与安全沙箱约束
1. 删除 lark-mail-lint-html.md 中不存在的 --auto-fix 和 --strict 两个 flag(参数表、示例命令、字段说明共 5 处),改成符合"autofix 始终启用"现状的描述 2. 将未知 URL scheme(webcal:// 等)的 lint finding 从 Applied(SeverityWarning)改为 Blocked(SeverityError)——行为是"删除属性",应与其他删除类 finding 语义一致;同步更新 linter_test.go 中对应测试(TestRun_UnknownSchemeWarning → TestRun_UnknownSchemeBlocked) 3. 删除 mail_send.go::resolveSendBody 和 mail_draft_create.go::resolveDraftCreateBody 两个与 body_file.go::resolveBodyFromFlags 完全等价的重复函数,调用点改为 resolveBodyFromFlags
bubbmon233
pushed a commit
that referenced
this pull request
Jun 5, 2026
…package (larksuite#1220) * refactor(sheets): rebuild lark-sheets on sheet-skill-spec canonical + One-OpenAPI Restart lark-sheets as a spec-driven downstream. Skill content (SKILL.md and 16 references covering 13 operations skills + 3 workflow skills, including the standalone filter-view skill) is mirrored from the sheet-skill-spec canonical-spec; do not hand-edit, change upstream and rerun npm run sync:consumers. Drop the 11 legacy shortcut sources (spreadsheet / sheet management, cell ops, dropdown, filter-view, float image, etc.) and 10 associated tests. Wire up the new sheet_ai/v2 One-OpenAPI single entry that dispatches by tool_name with JSON-string input/output, and land the first canonical shortcut +workbook-info as a template that exercises the public token XOR pair, Risk tiering, and zero-side-effect DryRun. sheet_ai_api.go provides callTool / invokeToolDryRun and bypasses runtime.CallAPI's silent swallowing of non-envelope responses so gateway and business errors from the new endpoint surface precisely. The remaining 55 shortcuts will be designed and landed separately, canonical skill by canonical skill. * feat(sheets): implement lark_sheet_workbook shortcuts (B1) Land the 8 modify_workbook_structure shortcuts that round out the lark_sheet_workbook canonical skill alongside the existing +workbook-info: +sheet-create / +sheet-delete / +sheet-rename / +sheet-move / +sheet-copy / +sheet-hide / +sheet-unhide / +sheet-set-tab-color. All eight call modify_workbook_structure via the One-OpenAPI invoke_write endpoint, dispatched by the `operation` enum. Helpers in helpers.go grow publicSheetFlags() / resolveSheetSelector() / sheetSelectorForToolInput() / sheetSelectorPlaceholder() so future sheet-level shortcuts share the public --sheet-id / --sheet-name XOR treatment. +sheet-create intentionally drops the sheet selector pair since create has no existing-sheet anchor (matches the spec fix in tool-shortcut-map.json). +sheet-delete is the first high-risk-write shortcut in the canonical package; the framework requires --yes (exit code 10 otherwise). +sheet-move's tool requires source_index in addition to target_index. The CLI accepts an optional --source-index override and falls back to a single get_workbook_structure read to derive it (and to resolve sheet_id from --sheet-name). DryRun stays network-free by rendering <resolve> placeholders for any field that would need that read. * feat(sheets): implement lark_sheet_sheet_structure shortcuts (B2) Add 8 shortcuts under the lark_sheet_sheet_structure canonical skill: +sheet-info (get_sheet_structure) plus +dim-insert / +dim-delete / +dim-hide / +dim-unhide / +dim-freeze / +dim-group / +dim-ungroup (modify_sheet_structure, dispatched by operation enum). Two reusable conversion helpers cover the impedance mismatch between the CLI surface and the tool input: - dimRange / dimPosition translate the CLI's 0-based exclusive-end range into the tool's 1-based A1 notation. row 5..8 becomes position "6" + count 3 (insert) or range "6:8" (range ops); column 26..29 becomes "AA:AC". - infoTypeFromInclude maps the fine-grained --include vocabulary (row_heights / col_widths / merges / hidden_rows / hidden_cols / groups / frozen) to the coarse info_type enum the tool accepts; mixed categories collapse to "all". +dim-delete is high-risk-write (irreversible row/column removal). +dim-freeze --count 0 auto-dispatches to operation=unfreeze. +dim-group accepts --depth for forward-compat with a future server-side nested group endpoint but does not pass it through today. * feat(sheets): implement read_data / search_replace / write_cells shortcuts (B3) Land 11 shortcuts across three canonical skills: - lark_sheet_read_data (3): +cells-get / +csv-get / +dropdown-get - lark_sheet_search_replace (2): +cells-search / +cells-replace - lark_sheet_write_cells (6): +cells-set / +cells-set-style / +csv-put / +dropdown-set / +dropdown-update / +dropdown-delete +dropdown-get reads the data_validation field via get_cell_ranges with the range carrying its own sheet prefix (no --sheet-id needed). The fine-grained --include vocabulary (value / formula / style / comment / data_validation) maps to the tool's coarse include_styles bool plus value_render_option enum. +csv-get's --include-row-prefix=false strips the [row=N] prefix client-side because the tool only emits the annotated form. +cells-search / +cells-replace flatten the tool's options sub-object into four independent flags (--match-case / --match-entire-cell / --regex / --include-formulas) per the flat-flag rule, then repack them on the way in. +cells-set takes a raw --data JSON body whose `cells` array must match the --range dimensions. +cells-set-style fans a single --style block out to every cell in the range via a new fillCellsMatrix helper; the range parser (rangeDimensions / splitCellRef / letterToColumnIndex) only accepts rectangular A1:B2 forms — whole-column / whole-row need sheet totals and are deferred. +dropdown-set fans the validation block out to one range; +dropdown- update / +dropdown-delete iterate sheet-prefixed --ranges and call set_cell_range sequentially (partial failure leaves earlier ranges already mutated; the Tip calls this out). +dropdown-delete is high-risk-write and requires --yes. +cells-set-image stays deferred to the cli-only batch (needs the shared local-file upload helper alongside +workbook-create / +dim-move / +workbook-export). * refactor(sheets): move +dropdown-update / +dropdown-delete to lark_sheet_batch_update Follow-up to B3 after the spec re-mapped these two shortcuts to the batch_update tool (atomic multi-range CRUD) instead of fan-out via set_cell_range. Drop their Go implementations + helper validateDropdownRanges + splitSheetPrefixedRange from lark_sheet_write_cells.go and remove the registrations from Shortcuts(); the shortcuts will reappear under lark_sheet_batch_update during B7. Also pull in the re-rendered reference docs: - skills/lark-sheets/references/lark-sheets-write-cells.md - skills/lark-sheets/references/lark-sheets-batch-update.md * feat(sheets): implement lark_sheet_range_operations shortcuts (B4) Land 8 shortcuts across four canonical tools: - clear_cell_range → +cells-clear (high-risk-write) - merge_cells → +cells-merge / +cells-unmerge - resize_range → +dim-resize - transform_range → +range-move / +range-copy / +range-fill / +range-sort Three CLI↔tool vocabulary bridges live in this file: - +cells-clear: --scope content normalizes to the tool's clear_type "contents" (singular/plural spec mismatch is absorbed in the CLI). - +dim-resize: --size <px> wraps as resize_{height,width}:{value:N}; --reset wraps as {reset:true}. The two flags are mutually exclusive and at least one is required. - +range-fill: CLI's five-valued --series-type collapses to the tool's binary fill_type — `copy` → "copyCells", anything else → "fillSeries" (the actual series progression is inferred server-side from the seed cells in --source-range). - +range-copy: --paste-type {values, formulas, formats} maps to the tool's {value_only, formula_only, format_only}; "all" omits the field entirely so the server applies its default. +cells-clear is the second high-risk-write shortcut in the package; the framework enforces --yes with exit code 10 as usual. * feat(sheets): implement object-list shortcuts (B5) Land 7 read shortcuts, one per object skill — chart / pivot table / conditional format / filter / filter view / sparkline / float image. All share the same shape (public sheet selector + optional <obj>-id filter) so they're declared via newObjectListShortcut + an objectListSpec. Notes: - +cond-format-list exposes --rule-id, which is renamed to conditional_format_id on the wire (the tool's full field name). - +sparkline-list exposes --group-id (the higher-level handle); the tool also accepts sparkline_id, intentionally not surfaced. - +filter-list takes no id filter — at most one sheet-level filter per sheet, so the listing is already unique. - +filter-view-list is `cli_status: cli-only` but get_filter_view_objects is in mcp-tools.json and dispatches through the same One-OpenAPI endpoint; no special path required. * feat(sheets): implement object CRUD shortcuts (B6) Land 21 shortcuts — three (create / update / delete) per object skill — backed by the manage_<obj>_object tools dispatched on the operation enum. Five standard objects (chart / cond-format / sparkline / float-image / filter-view) share an objectCRUDSpec factory; pivot and filter are special-cased. Shared wire contract: excel_id + sheet_id|sheet_name + operation + [<obj>_id] + [properties] CLI --data is passed through as the tool's `properties` field as-is, so callers shape it per each object's spec doc. Special cases: - pivot adds optional --target-sheet-id / --target-position on create (siblings of properties, not inside it). - cond-format exposes --rule-id (short CLI name) wired to the tool's conditional_format_id on the wire. - sparkline uses --group-id (higher-level object handle) instead of sparkline_id. - filter has no separate id flag — at most one filter per sheet, so filter_id is implicit. +filter-create promotes --range to a first- class flag (instead of burying it inside --data). - filter-view CRUD are `cli_status: cli-only` but manage_filter_view_object is in mcp-tools.json, so they go through callTool / One-OpenAPI alongside everything else. All delete shortcuts are high-risk-write and require --yes. * feat(sheets): implement lark_sheet_batch_update shortcuts (B7) Land 4 shortcuts that all funnel through the batch_update tool's atomic operations array: - +batch-update raw passthrough; --data carries the full { operations: [{tool, params}, ...] } payload plus optional continue_on_error. high-risk-write since the caller may stuff anything inside. - +cells-batch-set-style --data is [{ranges, style}, ...]; CLI flattens each (entry × range) pair into a set_cell_range op with a fan-out cells matrix carrying cell_styles + border_styles. - +dropdown-update --ranges + --options (+ --colors / --multiple / --highlight) — installs/replaces one dropdown across many ranges, each becoming a separate set_cell_range op with data_validation in cells. - +dropdown-delete --ranges — clears data_validation across many ranges (high-risk-write). Default is strict transaction: if any sub-tool fails the whole batch rolls back. +batch-update exposes --continue-on-error to flip the policy; the three fan-out shortcuts leave it strict (they're meant to be all-or-nothing). Reinstates validateDropdownRanges + splitSheetPrefixedRange that were removed during B3 → B7 relocation. * feat(sheets): implement cli-only shortcuts (B8) — 70/70 complete Land the four cli-only shortcuts that can't route through the One-OpenAPI dispatcher (their backing capabilities aren't in mcp-tools.json): - +workbook-create POST /open-apis/sheets/v3/spreadsheets + optional set_cell_range follow-up that zips --headers and --data into the first sheet starting at A1. - +workbook-export POST /open-apis/drive/v1/export_tasks (type=sheet) → poll /export_tasks/:ticket up to ~30s → optional GET /export_tasks/file/:file_token/download. CSV mode requires --sheet-id (single sheet export). - +dim-move POST /open-apis/sheets/v2/spreadsheets/:token /dimension_range CLI is 0-indexed inclusive (--start / --end); the v2 endpoint expects half-open [startIndex, endIndex) so the body uses endIndex = --end + 1. --sheet-name is resolved client-side to sheet_id via lookupSheetIndex when needed. - +cells-set-image common.UploadDriveMediaAll (parent_type=sheet_image, parent_node=token) then callTool set_cell_range with cells carrying rich_text: [{type:"embed-image", attachment_token, attachment_name}]. --range must be exactly one cell. All four use runtime.CallAPI / DoAPI directly; only +cells-set-image combines a legacy upload with the new One-OpenAPI for the second step (set_cell_range is in mcp-tools.json so callTool is the right path). This closes the migration: 70 shortcuts × 17 canonical skills × matching the sheet-skill-spec v0.5.0 tool-shortcut-map. * test(sheets): cover all 70 shortcuts with dry-run + execute-path tests Twelve _test.go files alongside the implementation, mirroring the legacy package's coverage style: - testhelpers_test.go shared rig: TestFactory + Mount + dry-run capture + JSON-input decode + envelope helpers. - lark_sheet_*_test.go one test file per implementation file (9 files), table-driven dry-run cases per shortcut plus targeted validation guards. - execute_paths_test.go end-to-end execute paths via httpmock stubs. Covers callTool unwrap, JSON-string output decoding, two-step lookup (+sheet-move), batch_update fan-out, dropdown atomic writes, and the legacy OAPI shortcuts (+workbook-create, +dim-move) including CLI inclusive → API half-open index conversion. Test coverage on the sheets package is 60.5 % of statements with -race clean, meeting the dev manual's ≥ 60 % patch-coverage gate. * refactor(sheets): inline cli-only shortcuts into their canonical skill files Two naming cleanups: - lark_sheet_cli_only.go is gone. The four shortcuts it grouped (+workbook-create / +workbook-export / +dim-move / +cells-set-image) were bundled by their implementation pattern (legacy OAPI direct calls) rather than by canonical skill. The whole sheets package IS the CLI implementation, so "cli only" wasn't a meaningful grouping at the Go layer. Each shortcut now lives next to its skill peers: +workbook-create / +workbook-export → lark_sheet_workbook.go +dim-move → lark_sheet_sheet_structure.go +cells-set-image → lark_sheet_write_cells.go Per-skill shortcut counts now match tool-shortcut-map.json exactly (workbook: 11, sheet_structure: 9, write_cells: 5). Helpers (buildInitialFillInput, pollExportTask, downloadExportFile, dimMoveBody) move with their shortcuts; nothing else in the package referenced them. - testhelpers_test.go → helpers_test.go. The _test.go suffix already conveys "test"; the leading "test" was redundant. Matches the helpers.go naming convention. Behavior unchanged. go test -race -cover stays at 60.5 %. * refactor(sheets): sync shortcut flags with sheet-skill-spec v0.5.0 Upstream hoisted a batch of high-frequency scalar fields out of --data into independent flags and renamed several composite-JSON flags to match their semantic content. CLI catches up. Renames (drop-in, same payload semantics): - +cells-replace --replace → --replacement - +cells-set --data → --cells - +workbook-create --data → --values - +batch-update --data → --operations (now a bare array; still accepts the envelope form for back-compat with continue_on_error) Flat-flag hoists out of --style / --data: - +cells-set-style / +cells-batch-set-style --style JSON drops; replaced by 11 flat style flags (--background-color / --font-color / --font-size / --font-style / --font-weight / --font-line / --horizontal-alignment / --vertical-alignment / --word-wrap / --number-format) plus --border-styles for the one field that's still nested. Both shortcuts share styleFlatFlags() + buildCellStyleFromFlags(). - +cells-batch-set-style also drops the [{ranges, style}] array shape in favor of one --ranges + the same flat style flags applied to all of them. Object CRUD --data → --properties everywhere (chart / pivot / cond-format / filter / filter-view / sparkline / float-image). Per-skill scalar hoists merged into properties via an enhanceCreate/UpdateInput callback: - +pivot-create adds --source (required), --range (and continues to expose --target-sheet-id / --target-position at top level) - +cond-format-{create,update} adds --rule-type (enum) + --ranges (JSON array); merged into properties.rule.type and properties.ranges respectively - +filter-view-{create,update} adds --view-name and --range; both override their properties.* counterparts - +filter-update adds first-class --range (was buried in --data) Float-image is fully hoisted — no --properties flag at all. Ten flat flags (--image-name / --image-token | --image-uri / --position-row / --position-col / --size-width / --size-height / --offset-row / --offset-col / --z-index) compose the properties block. Implemented as its own factory (newFloatImageWriteShortcut) since it diverges from the shared CRUD spec. Tests track every flag renamed and add explicit cases for the new flag combos. go test -race -cover stays at 60.3 %. * refactor(sheets): align batch_update + cells-set with synced reference docs Sync to upstream reference doc updates for 9 skills: - batch_update sub-ops: rewrite wire fields tool/params -> tool_name/input in CellsBatchSetStyle and DropdownUpdate/Delete fan-out (the actual server contract per Schemas section); update --operations flag desc and tests. - +cells-set --cells: accept bare 2D matrix [[{cell},...],...] instead of envelope {"cells":[[...]]}; spec example shows bare-array form. - sparkline createDataDesc enum: win_loss -> winLoss (camelCase). All other doc changes (float-image flat flags, cond-format --rule-type/--ranges, pivot create-only --source/--range, filter / filter-view extra flags, chart --properties) were already aligned in commit ce33315. * fix(sheets): repair cells-set-image rich_text embed payload The server rejected set_cell_range calls from +cells-set-image with three distinct errors: missing "text" property, missing image_width/image_height, and unknown attachment_token field. Realign the rich_text element to the embed-image schema (text/image_token/image_width/image_height) and decode PNG/JPEG/GIF dimensions from the local file before the write. * refactor(sheets)!: split +dim-resize into +rows-resize and +cols-resize Sync to upstream spec change that splits the legacy +dim-resize shortcut into +rows-resize and +cols-resize. Reasoning is that row vs column resize has divergent semantics (only rows support auto-fit) and the shared --dimension flag was hiding that. Behavior changes (BREAKING): - +dim-resize is removed; use +rows-resize or +cols-resize. - --dimension and --reset flags are gone. - --type enum replaces --size/--reset: pixel (requires --size) standard (reset to sheet default; no --size) auto (auto-fit row height; +rows-resize only) - --end is now inclusive (was exclusive). Old "--start 0 --end 5" (5 rows) becomes "--start 0 --end 4". - Wire payload for resize_height / resize_width changes from {value: N} | {reset: true} to {type: "pixel", value: N} | {type: "standard"} | {type: "auto"}. Tests cover both shortcuts across pixel / standard / auto and the new guard surface (--type pixel needs --size; standard/auto reject --size; +cols-resize rejects --type auto; --end < --start). Also pulls in synced reference docs for 5 skills (batch-update, core-operations, range-operations, sheet-structure, visual-standards) that update prose mentions of +dim-resize. * feat(sheets): add --print-schema runtime introspection for composite JSON flags Composite JSON flags (--cells / --properties / --operations / --border-styles / --sort-keys / --options) carry non-trivial structured payloads. Reference docs cover top-level fields but agents writing those flags often need the full JSON Schema to build a valid payload. This adds a system-level introspection contract so any shortcut whose flags are tracked upstream can serve its schemas locally: lark-cli sheets <shortcut> --print-schema --flag-name <name> lark-cli sheets <shortcut> --print-schema # list flags The schema data is embedded at build time from a synced artifact (shortcuts/sheets/data/flag-schemas.json). Upstream is the source of truth — never hand-edit the JSON; update the source Base table and rerun the sheet-skill-spec sync. Framework changes (shortcuts/common): - types.go: Shortcut gains an opt-in PrintFlagSchema hook (flagName -> bytes/error). When non-nil the framework auto-injects --print-schema / --flag-name and short-circuits Validate/Execute. - runner.go: register the two system flags when PrintFlagSchema is set; intercept in runShortcut before identity/scope/config so pure-local lookups don't trigger auth or network. Install a PreRunE that relaxes cobra's required-flag gate when --print-schema is set, since asking for a schema shouldn't need unrelated required flags. Sheets surface (shortcuts/sheets): - flag_schema.go (new): go:embed data/flag-schemas.json; expose printFlagSchemaFor(command) closure. When flagName is empty it emits a JSON listing of introspectable flags for discovery; otherwise it returns the schema subtree as pretty JSON. - flag_schema_test.go (new): cover embed parsing, listing / by-name lookup, unknown-flag error path, registration via Shortcuts(), and the full system-flag short-circuit through cobra (required flags relaxed, schema printed on stdout). - shortcuts.go: Shortcuts() now wraps shortcutList() and attaches PrintFlagSchema to every command present in flag-schemas.json, so shortcuts opt in by being listed upstream — no per-shortcut boilerplate. - data/flag-schemas.json (new, synced from sheet-skill-spec): 19 entries, schema_version "2". Generated upstream from the Lark Base source-of-truth (see sheet-skill-spec scripts/fetch_cli_flag_schema_map.mjs); ships only per-flag subtrees (not the full mcp-tools.json) to keep tool internals out of the open-source repo. Skill docs (skills/lark-sheets): - SKILL.md: system-flag table gains --print-schema / --flag-name and an "Agent 使用提示" note steering agents to prefer --print-schema over guessing JSON shape from the cheatsheet. - references/*.md: regenerated by upstream sync (Schemas-section boilerplate updated, plus accumulated upstream prose refinements). * docs(sheets): remove sandbox references and normalize tool names to CLI shortcuts Replace export_sheet_to_sandbox / import_sandbox_to_sheet / doubao_code_interpreter with local-script + batch csv-get/csv-put workflows; unify legacy MCP tool names (set_cell_range, get_range_as_csv, etc.) to CLI shortcut format (+cells-set, +csv-get). * feat(sheets): add flag-descriptions.en.json and wire applyFlagDescs into Shortcuts() Embed data/flag-descriptions.en.json (synced from upstream spec) and apply it at shortcut assembly time so every Flag.Desc is sourced from the canonical JSON rather than hardcoded Go strings. Existing hardcoded Desc values serve as fallback for flags not yet in the JSON. Also sync reference doc updates from upstream. * feat(shortcuts): support int64 and float64 flag types Flag.Type previously could not express non-integer numbers. Add int64 and float64 cases to flag registration plus Int64/Float64 runtime accessors. * refactor(sheets): build shortcut flags generically from flag-defs.json Replace flag-descriptions.en.json with the richer flag-defs.json (full flag definitions: type / default / enum / input / hidden / required / kind) synced from sheet-skill-spec. Add flagsFor(command) to materialize each shortcut's []common.Flag straight from the JSON, skipping system-kind flags the framework injects. Migrate every sheets shortcut (including the CRUD/list/dim/merge/ visibility factories) to Flags: flagsFor("+command"), dropping all hand-written flag literals plus the now-dead publicTokenFlags / publicSheetFlags / styleFlatFlags helpers and enum vars. A coverage test locks the Go-flags-match-JSON contract. Align Go with the new spec where they diverged: +cells-get --ranges → --range, font-size int → float64, +filter-view-create --range now required, +sheet-create row/col-count defaults 200/20. * docs(sheets): sync +batch-update CLI override schema (shortcut/input form) Pulled from sheet-skill-spec: - skills/lark-sheets/references/lark-sheets-batch-update.md: --operations now documents the {shortcut, input} form; tool_name references gone - shortcuts/sheets/data/flag-schemas.json: --operations resolves to the CLI-side array<{shortcut(enum), input}> schema, sourced from spec's canonical-spec/tool-schemas/cli-schemas.json (cli: prefix). +dropdown --options also drilled one level deeper NOTE: the binary still raw-passes --operations to MCP batch_update which expects {tool_name, input}. A follow-up will add a shortcut→tool_name translation layer (with per-shortcut operation field) before the docs become actionable. * feat(sheets): translate +batch-update sub-ops {shortcut,input} → MCP shape Users now hand +batch-update --operations a CLI-shape array ([{shortcut, input}, ...]) and the binary translates each sub-op to the underlying MCP batch_update shape ({tool_name, input(+operation)}) via a new dispatch table in shortcuts/sheets/batch_op_dispatch.go. Dispatch table covers 50 batchable write shortcuts. Excluded by design: - all read ops - fan-out wrappers (+batch-update self, +cells-batch-set-style, +dropdown-update, +dropdown-delete) — nesting these = nested batch - +dim-move — single shortcut uses legacy v2 /dimension_range endpoint, not MCP, can't be batched - +cells-set-image — multi-step image upload, not atomic-batch friendly - +workbook-create — new workbook, not batch-on-existing semantics Translator also rejects sub-ops that hand-fill input.operation (implied by shortcut name) or input.excel_id / spreadsheet_token / url (set once at +batch-update top level). +dim-freeze always injects operation=freeze; the count==0 unfreeze path of the single shortcut is intentionally not supported in batch — callers should use the single shortcut for unfreeze. Tests cover: end-to-end translation, --continue-on-error propagation, 13 rejection cases (banned shortcuts, malformed shapes, reserved keys). Sync'd from sheet-skill-spec: skills/lark-sheets/references/ lark-sheets-batch-update.md + shortcuts/sheets/data/flag-schemas.json pick up the corrected enum (+cells-set-style / +dropdown-set added, +dim-move removed). * fix(sheets): make +batch-update sub-ops reuse standalone flag→body translators Sub-ops previously near-passed-through their input, so any shortcut whose standalone translator renames fields broke inside a batch: +range-copy lost range/destination_range (transform_range errored "range missing") and +rows-resize lost range/resize_height ("No resize operation specified"). Introduce a flagView interface (satisfied by *common.RuntimeContext) and a map-backed mapFlagView, then route every batchable sub-op through the SAME *Input builder the standalone shortcut uses. mapFlagView seeds flag-defs.json defaults for value reads while keeping Changed() user-driven, so a sub-op body is byte-identical to the standalone body — locked by a batch-vs-standalone contract test over all ~40 batchable shortcuts. Also fix single-row/column resize: start==end now formats as "23:23" / "C:C" (resize_range rejects a bare "23"); dimRangeFull keeps both sides while dimRange's collapse stays for modify_sheet_structure consumers. * fix(sheets): align +cells-get/+csv-get range flags with synced spec sheet-skill-spec now declares +cells-get --range as a single string (was string_array) and +csv-get --range as required. Match the flag→body translators: - +cells-get wraps the single --range into the tool's `ranges` array and validates with Str() instead of StrArray(), which silently returned nil against the now-String flag and broke the command. - +csv-get gains a trim-based required-range guard. Update read-data dry-run tests to single-range form and add a guard test for the empty --range path. * fix(sheets): push +batch-update sub-op validation down into xxxInput builders Sub-ops that omit --sheet-id (or any other required flag) used to slip past CLI validation — Validate ran only against the standalone shortcut path, and batchOpDispatch's translators built bodies from whatever flagView returned, so a structurally broken sub-op surfaced as an opaque server "sheet undefined not found" after a network round-trip. Push each batchable shortcut's check trio down into its xxxInput builder: 1. resolveSpreadsheetToken — stays in Validate (batch already does it once at the top level; sub-ops don't repeat). 2. requireSheetSelector(sheetID, sheetName) — new helper; flagView- agnostic XOR + control-char check, called at the top of every xxxInput. 3. shortcut-specific required / range / enum checks (--dimension, --range, --start <= --end, --type pixel needs --size, --float-image-id, image-token XOR image-uri, ...) — moved out of Validate into the builder body. All ~30 batchable xxxInput builders now return (map, error). Standalone Validate shrinks to validateViaInput(xxxInput); DryRun / Execute propagate the error. batch_op_dispatch entries drop the noErrTranslate wrapper and pass the builder directly — its error bubbles up wrapped with "operations[N] (+shortcut):" context. Tests: - TestBatchOp_ErrorEquivalence (7 cases): XOR / logical-constraint errors fire identically from standalone and batch sub-op paths. - TestBatchOp_RejectsBadSubOpInput (8 cases): cobra-required flags that standalone catches via MarkFlagRequired now also get rejected CLI-side on the batch path (where cobra is not in the loop). - TestBatchOp_BodyMatchesStandalone (~40 cases) and TestBatchOp_DispatchCoversReportedBugs continue to pass — bodies stay byte-identical. - BOE smoke (spreadsheet ICFwstkUGheyfptGWS2bB7RgcDf, sheet 51991c): +batch-update with a sub-op missing --sheet-id now returns "operations[0] (+dim-insert): specify at least one of --sheet-id or --sheet-name" before any network call. sheetMoveBatchInput (xiongyuanwen's batch-only explicit-source-index requirement) is preserved — it's an orthogonal batch-specific constraint not affected by this push-down. * fix(sheets): align +cond-format / +filter with server schema (#4 + #5) Two latent bugs in the object_crud translator surfaced during BOE smoke testing of +batch-update. Both are schema-alignment fixes against manage_conditional_format_object / manage_filter_object as declared in sheet-skill-spec/canonical-spec/tool-schemas/mcp-tools.json. #4 +cond-format: rule_type path + enum vocabulary --------------------------------------------------- condFormatEnhance used to write the user's --rule-type value into `properties.rule.type` (nested under a `rule` object). The server schema actually puts it at flat `properties.rule_type` and silently drops the nested form — so every conditional-format create/update secretly built the wrong document. Worse, the CLI enum exposed via flag-defs.json was its own invented vocabulary (cellValue / formula / duplicate / unique / topBottom / aboveBelowAverage / dataBar / colorScale / iconSet / textContains / dateOccurring / blankCell / errorCell) — none of those values were the strings the server accepts. Fix: - condFormatEnhance now writes `properties.rule_type = <value>` directly (no nested `rule` object). - Synced flag-defs.json + lark-sheets-conditional-format.md enum vocabulary from base to match the server: duplicateValues, uniqueValues, cellIs, containsText, timePeriod, containsBlanks, notContainsBlanks, dataBar, colorScale, rank, aboveAverage, expression, iconSet. - ⚠️ Breaking: scripts passing the old CLI-invented enum values (e.g. --rule-type cellValue) now get a cobra "invalid value … allowed: …" error listing the new vocabulary. No alias layer. - TestObjectCRUDShortcuts_DryRun's +cond-format-update case updated to assert the flat properties.rule_type shape + new enum. #5 +filter-{update,delete}: auto-inject filter_id = sheet_id ------------------------------------------------------------- manage_filter_object's contract is "filter_id === sheet_id" for the sheet-scoped filter (per per-tool description in mcp-tools.json), and update / delete operations MUST carry filter_id. Standalone filterUpdateInput / filterDeleteInput never set it, so the server rejected with "filter_id is required for update/delete operation" on every call — both standalone AND inside +batch-update. Fix: - filterUpdateInput / filterDeleteInput now set input["filter_id"] = sheetID. - Because filter_id must equal sheet_id (not sheet_name), update / delete reject when only --sheet-name is given — there's no network lookup available inside the builder. The friendly error points at +workbook-info for resolving sheet-name → sheet-id. - create still omits filter_id (server requires that — id is server-allocated on creation). - New tests: * TestObjectCRUDShortcuts_DryRun gains a +filter-update happy-path case asserting filter_id is auto-injected + --range hoisting. * +filter-delete case updated to assert filter_id presence. * TestBatchOp_RejectsBadSubOpInput gains two cases asserting both +filter-update and +filter-delete reject --sheet-name-only with the friendly error. Docs (#2 + #3 + #8) synced from sheet-skill-spec ------------------------------------------------- Companion doc fixes that landed via npm run generate:cli + sync:cli in sheet-skill-spec; included here because the regenerated flag-defs and references markdown are byte-tracked in this repo: - #2: lark-sheets-sheet-structure.md — +dim-{hide,unhide,group, ungroup} --start/--end desc changed from "(0-based, inclusive)" to "(0-based)" / "(exclusive)" to match the half-open range semantics the code has always implemented (requireDimRange: end > start; dimRange uses end - 1 for column end letters). - #3: lark-sheets-workbook.md — +sheet-move section gains a note about the batch-internal requirement to pass --sheet-id AND --source-index explicitly (sheetMoveBatchInput's constraint). - #8: lark-sheets-pivot-table.md — +pivot-create --properties example drops the stale data_range field (the actual server schema uses --source as a hoisted flag; properties only carries rows / columns / values / filters / show_*_grand_total). * feat(sheets): add +cells-batch-clear fan-out over batch_update Clear content/formats across many sheet-prefixed ranges in a single atomic batch_update (one clear_cell_range op per range), mirroring the existing +cells-batch-set-style / +dropdown-{update,delete} fan-out wrappers. The --scope to clear_type normalization is shared with standalone +cells-clear (normalizeClearType) so the two stay in lockstep. high-risk-write (requires --yes); rejected as a batch sub-op like the other fan-out wrappers. flag-defs/flag-schemas and skill docs updated to match. * docs(sheets): sync stdin guidance and sparkline reference - skills/lark-shared/SKILL.md: drop the generic "prefer stdin" section - skills/lark-sheets/SKILL.md: add expanded stdin guidance (use stdin over @file abs paths; don't cd or write into the project dir) - skills/lark-sheets/references/lark-sheets-sparkline.md: document the group_id / sparkline_id two-tier model with worked examples * fix(sheets): require sparkline_id on +sparkline-update items (#6) manage_sparkline_object uses two layers of IDs: --group-id picks the sparkline group, and properties.sparklines[i].sparkline_id picks each item inside the group. The server contract requires sparkline_id on every update item (server maps each entry back to an existing sparkline by this id). Agents that called +sparkline-update without the per-item ids hit an opaque server-side rejection that didn't mention sparkline_id at all, then got stuck in a try-fail-list-retry loop. Pre-check CLI-side in objectUpdateInput via a new validateUpdateInput hook on objectCRUDSpec. sparklineSpec wires validateSparklineUpdateItems, which walks properties.sparklines[] and rejects with a message that points at +sparkline-list: +sparkline-update properties.sparklines[N] missing sparkline_id (run `+sparkline-list --group-id <id>` first to read sparkline_id for each item, then echo each id back on the corresponding update entry) Scope is update-only. config-only updates (properties.config without sparklines) stay legal — the validator skips when sparklines is absent. Delete is not pre-checked: objectDeleteInput doesn't pass properties through, so the partial-delete branch can't be reached today (separate follow-up). Tests: - TestObjectCRUDShortcuts_DryRun: positive case for update with sparkline_id present. - TestSparklineUpdate_MissingSparklineID: standalone path — error contains both "missing sparkline_id" and "+sparkline-list". - TestBatchOp_RejectsBadSubOpInput: batch sub-op missing sparkline_id rejected with the same friendly error. Docs synced from sheet-skill-spec (canonical change committed there): skills/lark-sheets/references/lark-sheets-sparkline.md documents the two-layer id model, the three "+sparkline-list first" cases, and both delete modes. * docs(sheets): sync lark-sheets skill from spec (audit 20260521) Pull latest spec from sheet-skill-spec (PR ee/sheet-skill-spec!6 + earlier develop commits) into skills/lark-sheets/ and shortcuts/sheets/data/. Audit findings now reflected in CLI docs: - A2 +cond-format-create example: --rule-type duplicate → duplicateValues - A3 +cond-format-create Validate: cellValue/formula → cellIs/expression - A5 +csv-put examples: --range → --start-cell; drop redundant --allow-overwrite - A7 +sparkline-create: Validate / Examples aligned with real schema (config/sparklines), executable JSON example added - B13 cross-doc dead links: lark_sheet_*/cli-shortcuts.md → lark-sheets-*.md - C2 +csv-put: `=` literal warning next to Examples - CC5 +rows-resize/+cols-resize --type auto: single point of truth in range-operations reference flag-defs.json description / required sync (from base): - A4 +float-image-update: image-name/position-*/size-* required → optional (patch mode) - A8 +dim-move --start/--end description cleanup - B3 +pivot-create --properties: data_range → source (real field name) Also picks up the +cells-batch-clear shortcut doc (introduced in spec develop). Go-side implementation for that shortcut is intentionally not in this PR — docs-only preview; runtime dispatch will land in a follow-up. `go test ./shortcuts/sheets/...` passes. * feat(sheets): add +cells-set --copy-to-range and sync skill spec Sync lark-sheets skill references and flag schemas from upstream sheet-skill-spec, and wire the newly-specced --copy-to-range flag into +cells-set: it passes copy_to_range to the set_cell_range tool so a template block written via --cells fans out across a larger range with auto-shifted formula refs. * docs(sheets): sync lark-sheets skill spec (chart/pivot wire mappings, --end semantics) Sync skill references and flag-defs descriptions from upstream sheet-skill-spec: clarify +chart-create properties structure (snapshot.data), +pivot-create --target-position / --range wire-field mappings, add a cross-command --end endpoint-semantics table (insert/delete/hide/group exclusive vs move/resize inclusive), note --group-state default, and rename reference identifiers to lark-sheets-*. Description-only refinement; the existing CLI implementation already matches the clarified wire mappings and --end semantics. * fix(sheets): make --max-chars the single read cap for +cells-get / +csv-get Drop --cell-limit (+cells-get) and --max-rows (+csv-get) from the CLI surface and pin the underlying tool's cell_limit / max_rows to a very large sentinel so the tool's own defaults never truncate before --max-chars. --max-chars stays the only knob (default 200000, unchanged). - lark_sheet_read_data.go: add unboundedReadLimit (1e9); cellsGetInput pins cell_limit, csvGetInput pins max_rows; --max-chars still passed through - data/flag-defs.json: synced from spec (drops the two flags) - tests: spot-check moved to --max-chars; dry-run wantInput asserts cell_limit / max_rows are pinned high Mirrors sheet-skill-spec (Base flag records removed). go build ./... + go test ./shortcuts/sheets/ green. * docs(sheets): sync lark-sheets read docs — --max-chars as single read cap Sync skills/lark-sheets references from spec: drop --cell-limit / --max-rows guidance; 大表分批读 switches to --range row windows + --max-chars auto cap + has_more. Mirrors sheet-skill-spec 58e7456 and handler change 2befc49. * docs(sheets): sync lark-sheets skill spec from upstream Refine reference docs and flag-defs descriptions from upstream sheet-skill-spec (--depth wording for +dim-group / +dim-ungroup, plus assorted reference clarifications). Description-only; no CLI behavior or flag surface change. * docs(sheets): sync chart properties schema (position/size required) Regenerate flag-schemas.json from upstream sheet-skill-spec: the chart properties schema now marks position and size as required, and the chart reference doc reflects the same. flag-schemas.json is print-schema-only (no client-side validation), so this is a generated-artifact + doc sync with no CLI behavior change. * docs(sheets): sync lark-sheets skill spec from upstream Refine reference docs and flag-defs descriptions from upstream sheet-skill-spec: clarify +workbook-export sheet flag scope, +filter-* --properties optionality (omitted => empty filter on --range; rules must be non-empty when provided), float-image reference_id wording, and assorted reference cleanups. Description-only; existing CLI behavior (filter passthrough, properties optional) already matches. * docs(sheets): sync lark-sheets skill spec from upstream Trim and refine reference docs from upstream sheet-skill-spec (condense core-operations workflow, tidy write-cells / range-operations / float-image / SKILL guidance). Description-only; no flag or CLI behavior change. * docs(sheets): sync lark-sheets skill spec from upstream Refine reference docs from upstream sheet-skill-spec (core-operations, formula-translation, visual-standards, SKILL guidance). Description-only; no flag or CLI behavior change. * fix(sheets): correct +workbook-create initial fill and +dim-move endpoint +workbook-create: the v3 create response does not echo the default sheet's id, so the initial-fill set_cell_range was sent with an empty sheet_id and rejected ("sheet_id or sheet_name is required"). Resolve the workbook's first sheet via get_workbook_structure before filling. +dim-move: the move request was POSTed to the v2 dimension_range endpoint (the add/update/delete surface, which requires a `dimension` object) and rejected with "[9499] Missing required parameter: Dimension". Switch to the native v3 move_dimension endpoint (sheet_id in path; snake_case source.{major_dimension,start_index,end_index} + destination_index). CLI --end and v3 end_index are both 0-based inclusive, so they pass through unchanged. * fix(sheets): align +workbook-create, +dropdown-*, +dim-move, +range-sort with server schema Five separate E2E failures in shortcuts/sheets/ that all trace back to a CLI ↔ server contract mismatch. Each is independently scoped; bundling them because they share the test-report citation and the same one-line fix shape in most cases. buildInitialFillInput sent {"sheet_id": ""} on the secondary set_cell_range call after creating the workbook. The empty value was a holdover from "...otherwise server picks first sheet" — but set_cell_range rejects an empty selector with "sheet_id or sheet_name is required" rather than falling back to the default sheet. Use sheet_name "Sheet1" instead. POST /sheets/v3/spreadsheets always creates that sheet on workbook creation, and set_cell_range accepts sheet_name as an equivalent selector — saves an extra get_workbook_structure round-trip just to learn the auto-generated id. buildDropdownValidation emitted four fields that don't exist in the canonical set_cell_range.data_validation schema: - "values" (options list) → renamed to "items" - "multiple_values" → renamed to "support_multiple_values" - "colors" (per-option color) → removed (not in schema; flag also removed from data/flag-defs.json for +dropdown-set / -update) - "highlight_options" → removed (not in schema; flag also removed) The canonical schema lives at sheet-skill-spec/canonical-spec/tool- schemas/mcp-tools.json (set_cell_range tool, data_validation property); the colors / highlight knobs were CLI inventions the server never accepted, so removing the flags is correct (renaming would leave the flags broken). Skill reference docs (write-cells.md, batch-update.md) synced. validateDropdownOptionsColors lost its colors check; renamed to validateDropdownOptions to reflect the narrower contract. dropdownGetInput sent "Sheet1!C2:C6" verbatim as a ranges[] entry. get_cell_ranges expects sheet_id / sheet_name as separate fields and ranges entries without the sheet prefix; the server bounced with "sheet not found, sheetId:" (empty). Use the existing splitSheetPrefixedRange helper (declared in lark_sheet_batch_update.go) to break "Sheet1!C2:C6" into ("Sheet1", "C2:C6"), then thread the sheet name through sheetSelectorForToolInput exactly like +cells-get does. The shortcut was POSTing to /sheets/v2/spreadsheets/{token}/dimension_ range, which is the v2 insert-dimension endpoint and requires a top- level {"dimension": {...}} body. Move uses a separate endpoint: POST /sheets/v2/spreadsheets/{token}/move_dimension body: { "source": {...}, "destination_index": N } (camelCase "destinationIndex" → snake_case "destination_index" to match the v2 contract.) Both DryRun and Execute updated, plus the TestDimMove_DryRun and TestExecute_DimMove assertions. transform_range.sort_conditions[i] requires both `column` (string) and `ascending` (bool); rangeSortInput passed the --sort-keys array through to the server unvalidated, so missing fields surfaced as opaque "required property X missing" errors with no per-item context. Walk the parsed array client-side, reject with item-pointing messages. Test fixtures and a contract-test fixture switched from the historical {col, order} vocabulary (which the server has never accepted) to the correct {column, ascending}. Server-schema citations and test-report case mapping in this branch's plan file. * revert(sheets): drop direct flag-defs.json edits — generated from spec data/flag-defs.json is regenerated from the upstream sheet-skill-spec canonical-spec; editing it here gets clobbered on the next sync. The schema realignment for +dropdown-set / -update --colors / --highlight removal needs to land on the base table first, then flow back through sheet-skill-spec → larksuite-cli sync, not via a direct CLI-side edit. Restore the previous flag entries verbatim. The Go-side change in buildDropdownValidation still drops the wire fields, so: - users passing --colors / --highlight today see the flag accepted silently (no effect on the wire) until the upstream removal lands; - after upstream removal + sync, both the flag declarations and the Go-side handling will be in sync. Functional fixes (#1 workbook-create, #3 dropdown-get, #4 dim-move, #5 range-sort) and dropdown wire-shape rename (#2) are unaffected. * revert(sheets): drop direct edits to skills/lark-sheets/references/ These md files are sync targets generated from sheet-skill-spec; editing them here gets clobbered on the next sync, same as data/flag-defs.json. The --colors / --highlight row removals belong on the upstream base table → canonical-spec sync, not here. Restore the previous --colors / --highlight rows in both lark-sheets-write-cells.md (+dropdown-set) and lark-sheets-batch-update.md (+dropdown-update). The Go-side change in buildDropdownValidation still drops the wire fields, so: - users passing --colors / --highlight today see the flag accepted silently (no effect on the wire) until upstream removes the flag; - after upstream removal + sync, both flag declarations, ref docs, and Go-side handling will be in sync. Functional fixes (#1 workbook-create, #3 dropdown-get, #4 dim-move, #5 range-sort) and dropdown wire-shape rename (#2) are unaffected. * docs(sheets): sync from sheet-skill-spec — remove dropdown --colors / --highlight Upstream sheet-skill-spec base table deleted the --colors and --highlight flags on +dropdown-set / +dropdown-update (the corresponding wire fields data_validation.colors / .highlight_options were never accepted by the server schema; see prior fix in this branch). Re-running the sync from canonical-spec brings the CLI flag-defs and skill reference docs back in line with the Go-side handling that already drops these fields. Generated by `npm run sync:cli` in sheet-skill-spec @ ac7acef. * fix(sheets): restore +dropdown --colors / --highlight, map to canonical fields Reverses the --colors / --highlight removal from 7932ab2 (item #2 of the batch-1 schema-alignment commit). That commit dropped both flags after the test report flagged data_validation.colors / highlight_options as "unexpected property" — at the time the canonical set_cell_range.data_validation schema listed only help_text / items / operator / range / support_multiple_values / type / values, so the flags had no server-side target and the removal was correct. Since then, set_cell_range.data_validation has gained two fields explicitly modelling the dropdown highlight UI (mcp-tools.json in sheet-skill-spec 2026-05-22 base sync): enable_highlight (bool) — show pill backgrounds highlight_colors (string[]) — hex pill colors, length must match items So the flags are back, but rewired: --colors -> data_validation.highlight_colors (was: colors) --highlight -> data_validation.enable_highlight (was: highlight_options) --options -> items and --multiple -> support_multiple_values renames from 7932ab2 are kept. Changes: - buildDropdownValidation: re-add --colors / --highlight handling against the new field names; --colors length check stays inline (so dropdownSetInput Validate path catches it via validateViaInput, no separate guard needed). - validateDropdownOptions -> validateDropdownOptionsColors: restore the Validate-time --colors length check on +dropdown-update / +dropdown-delete (called from lark_sheet_batch_update.go). - TestDropdownSet_CellsShape: extend to assert highlight_colors / enable_highlight emitted; assert legacy `colors` / `highlight_options` absent. - TestDropdownSet_ColorsLengthMismatch: new — covers the early Validate error path. - TestDropdownUpdate_BatchPayload: extend to cover dropdownBatchInput propagation of --colors / --highlight through batch_update. - skills/lark-sheets/references/lark-sheets-{write-cells,batch-update}.md, shortcuts/sheets/data/flag-defs.json, flag-schemas.json: synced from sheet-skill-spec generate output (MR !7). * chore(sheets): re-sync from spec + loosen --colors length check Catches up to sheet-skill-spec's 2026-05-25 base sync (MR !7) after rebasing onto upstream feat/lark-sheets-refactor (12 new upstream commits including the lark-sheets skill refactor + tools-schema migration). Spec changes flowing in: - highlight_colors description loosened: length may be **shorter than** --options (server cycles remaining slots through a built-in 10-color palette); previously the tool errored on any length mismatch. - shortcuts/sheets/data/flag-schemas.json: mass re-mirror — generator now emits `type` before `properties` and adds explicit `additionalProperties: false` on object schemas (cosmetic, no behavior change). - skills/lark-sheets/references/lark-sheets-{batch-update,chart,write-cells}.md: --options gains the type='list' tag; data_validation inline field-count goes 7 → 9 (catches up the highlight schema in the summary); chart position / size marked optional per upstream. Go-side adjustment: - buildDropdownValidation / validateDropdownOptionsColors: change the --colors length check from strict-equal to "must not exceed --options" to match the relaxed schema. - TestDropdownSet_ColorsLengthMismatch -> TestDropdownSet_ColorsLongerThanOptions (now hits the overflow path with 3 colors vs 2 options). - New TestDropdownSet_ColorsShorterAccepted: 2 colors vs 4 options is legal and forwarded as-is. * docs(sheets): sync dropdown --colors/--highlight clarification from spec Mirrors sheet-skill-spec MR !7 changes: - skills/lark-sheets/references/lark-sheets-write-cells.md: new "Dropdown 配色" section explaining how --colors (→ data_validation.highlight_colors) and --highlight (→ data_validation.enable_highlight) compose — length rule (shorter ok, longer rejected), --highlight gating, palette fallback behavior, minimal +dropdown-set example. - skills/lark-sheets/references/lark-sheets-batch-update.md: one-line pointer to the write_cells section for +dropdown-update / -delete (same rules). - shortcuts/sheets/data/flag-defs.json: --colors / --highlight `desc` fields gain the long-form server-field / length-rule descriptions used by `--help`. No Go-side change — earlier commit 538eb2e already loosened the buildDropdownValidation length check to "must not exceed"; this PR step just makes the docs / `--help` text catch up. * feat(sheets): +dropdown-set/-update --source-range for listFromRange mode Previously +dropdown-set / +dropdown-update only emitted data_validation.type=list — agents wanting listFromRange (dropdown options sourced from existing cells, kept in sync with that range) had to drop down to +cells-set and hand-build a data_validation map. The flag now exposes it natively as --source-range, paired with --options under XOR. CLI changes: - shortcuts/sheets/lark_sheet_write_cells.go: * new dropdownTypeAndItems(runtime) — central XOR resolver: rejects 0 or 2 of {--options, --source-range}, returns (sourceSize, partial dv with type+items|range filled in). Source size = options length for list mode, rangeDimensions(--source-range) cell count for listFromRange. * buildDropdownValidation rewritten to call the resolver, then layer --colors / --multiple / --highlight on top — semantics unchanged for callers, just two modes instead of one. * validateDropdownOptions / -Colors renamed to validateDropdownSourceOrOptions so the XOR + length check fires at +dropdown-update Validate time too. * --colors length error message generalized: "must not exceed dropdown source size (N)" (covers both modes). - shortcuts/sheets/lark_sheet_batch_update.go: rename call site. - shortcuts/sheets/lark_sheet_write_cells_test.go: 4 new tests — ListFromRange (happy path: range + items absent + colors + highlight all emit), ListFromRange_ColorsLongerThanCells (overflow against T1:T3 cell count), XorBothSet, XorNeitherSet. Updated the existing ColorsLongerThanOptions assertion to match the new "source size" wording. Spec-driven changes (synced via npm run sync:cli from sheet-skill-spec MR !7 2c298b6): - shortcuts/sheets/data/flag-defs.json: --options Required flips to xor on +dropdown-set/-update; new --source-range row gains long-form description pointing at server data_validation.range + the XOR semantics. - skills/lark-sheets/references/lark-sheets-write-cells.md: "Dropdown 配色" section reorganized into "Dropdown 选项 + 配色" — XOR comparison table (list vs listFromRange), shared config flag table (--highlight / --colors), explicit length rule covering both modes, side-by-side minimal examples, server-range-normalization gotcha callout. - skills/lark-sheets/references/lark-sheets-batch-update.md pointer updated to mention both modes + that +dropdown-delete is unaffected. PPE smoke (ppe_lark_cli_sheet) on UFJxszjrZhZ1LVtc9FdcICSbn6b C column: - +cells-set C1 → "性别" (bold + centered): updated_cells_count=1 - +dropdown-set --range C2:C21 --source-range "Sheet1!T1:T3" --colors '["#cce8ff","#ffd6e7","#e6e6e6"]' --highlight: updated_cells_count=20 - read-back: data_validation.type=listFromRange + range=$T$1:$T$3 (server normalizes the prefix away on storage; highlight_colors / enable_highlight not echoed by get_cell_ranges, see byted-sheet read projection TODO). - error-path replay (both XOR violations + colors > source-size) all rejected at Validate stage with the expected messages. * docs(sheets): sync agent-voice rewrite of Dropdown 选项+配色 from spec Mirrors sheet-skill-spec MR !7 60df610 — narrative now describes how the flags interact (XOR, colors length rule, highlight gating, sheet-prefix read-back gotcha) without exposing the underlying data_validation field names or server-side normalization details that agents don't act on. No Go-side change, no shortcut behavior change. * chore(sheets): restore --colors in parseJSONFlag docstring example list The earlier commit 49104ec swapped --colors out of parseJSONFlag's "Used by" example list when it deleted the flag (item #2 there removed --colors / --highlight from +dropdown-set/-update). Subsequent commits 8672d8e / 538eb2e / fb90c8b reinstated --colors (and added --source-range) but did not roll back this docstring tweak — leaving an orphan reference to --properties where --colors used to be. This restores the example list to its pre-49104ec form so the docstring matches what the helper actually services on this branch's HEAD. Pure docstring change — function behavior unaffected, no test movement. * fix(sheets): post-rebase test fixups after dropping superseded fix #1 Two test fallouts from rebasing onto upstream 4be06c8 (which independently re-fixed +workbook-create and +dim-move with a more thorough approach): - shortcuts/sheets/lark_sheet_workbook_test.go: our PR's earlier TestWorkbookCreate_DryRun "with headers and data → 2-step plan" subtest asserted the expedient sheet_name="Sheet1" / no-sheet_id wire body that matched our dropped fix #1 implementation. Upstream's fix #1 resolves the workbook's first sheet via get_workbook_structure and fills with the real sheet_id instead. Reset this file to upstream's version — our superseded assertions disappear, upstream's tests cover the new wire shape. - shortcuts/sheets/execute_paths_test.go: TestExecute_RangeSort fixture still used the legacy {col, order} sort-key shape because the rebase resolution picked the upstream version of this file wholesale (it contained other unrelated changes). Re-apply just the one fixture update to {column, ascending} so fix #5's CLI-side rejection logic exercises a valid input — server-side sort_conditions has required fields `column` (string) and `ascending` (bool); the historical {col, order} vocabulary was never accepted. go build ./... + go test ./shortcuts/sheets/... -count=1 both green. * feat(sheets): +dropdown --highlight tri-state via Changed() for opt-out The server-side default for data_validation.enable_highlight flipped from false to true (aligning with the UI behavior). With the previous code path if runtime.Bool("highlight") { dv["enable_highlight"] = true } omitting --highlight and passing --highlight=false both produced the same "enable_highlight key absent" body, leaving CLI users with no way to opt out of the (now-default) highlighting. Switch to runtime.Changed() so the translator can distinguish all three input shapes: - omitted -> no enable_highlight key (server applies default=true) - --highlight=true -> enable_highlight: true (explicit no-op vs default) - --highlight=false -> enable_highlight: false (the only opt-out path) flagView already exposes Changed() and mapFlagView (the +batch-update sub-op adapter) implements it via raw-key presence — same pattern other translators use for "Changed-only" branching (e.g. omit target_index unless --index was set), so no interface surface change is needed. Test coverage: - TestDropdownSet_HighlightTriState pins all four shapes (omit / presence form / explicit true / explicit false) and asserts the enable_highlight key's presence/value - TestBatchOp_BodyMatchesStandalone adds a --highlight=false sub-op case so the batch sub-op path produces a body byte-identical to the standalone +dropdown-set --highlight=false body * chore(sheets): sync +dropdown flag desc + write-cells narrative from spec Mirror sheet-skill-spec generated/ into shortcuts/sheets/data/ and skills/lark-sheets/ for the +dropdown-set / +dropdown-update path. No hand edits in this repo. The +dropdown flag desc and the Dropdown 配色 narrative now match the server-side enable_highlight default flip (true) and the tri-state --highlight semantics introduced in the sibling commit: * --highlight desc: 不传 = 开(按内置 10 色色板循环上色), --highlight=false 关闭得到纯白下拉 * --colors desc: 单独传即生效(高亮默认开),--highlight=false 时忽略 * write-cells reference: 三种意图三条线(默认色板 / 指定颜色 / 纯白下拉)+ 新增 --highlight=false 示例 Source upstream: sheet-skill-spec MR !8. * fix(sheets): validate +cells-set-image --image path in Validate The unsafe-path check only ran at Execute (via FileIO.Stat), so --dry-run printed a misleading success preview for an absolute / out-of-cwd --image path that a real run would then reject. Move the path-safety check into Validate (validate.SafeLocalFlagPath), so --dry-run and Execute fail identically and both name the real --image flag. File existence stays deferred to Execute, so legitimate relative paths still preview cleanly. Add TestCellsSetImage_DryRunRejectsUnsafePath. * feat(sheets): support local --image in +float-image-create +float-image-create now accepts a local file via --image (XOR with --image-token / --image-uri): the CLI uploads it as a sheet_image and embeds the returned file_token, removing the previous "upload elsewhere to get a token first" workaround. Path safety is checked in Validate, --dry-run previews the extra upload step, and +batch-update rejects --image (no upload phase). +float-image-update is unchanged (it does not register --image). Also syncs the lark-sheets skill docs/flag-defs from sheet-skill-spec: the new --image flag, partial-merge / border-per-side / bare sheet-prefix clarifications, and refreshed dropdown --colors/--highlight descriptions (already pending in the source Base table). * fix(sheets): +dropdown-get accepts --sheet-id/--sheet-name + bare --range Align +dropdown-get with its get_cell_ranges siblings (+cells-get / +csv-get): sheet selection is now via --sheet-id / --sheet-name (XOR) and --range is a bare A1 reference. The previous shape required the sheet prefix inside --range (e.g. "Sheet1!A2:A100") and was the odd one out among the read-data wrappers; callers pasting the sheet-id form straight from the URL hit a misleading "sheet not found, sheetId: , sheetName: <id>" error because the prefix was unconditionally treated as sheet_name. Flag schema + skill reference regenerated from the upstream Lark Base Shortcut-flags table. * fix(sheets): drop Sheet1! prefix from +cells-get / +csv-get / +csv-put flag examples Server tools-schema.json for get_cell_ranges, get_range_as_csv and set_range_from_csv does not accept a sheet prefix on --range / --start-cell; the sheet is selected via --sheet-id / --sheet-name. +csv-put --start-cell also now states it must be a single cell (no range notation). Synced from spec repo. * feat: 把环境变量提交上去 * fix(sheets): clarify batch --ranges prefix must be sheet display name E2E test cases repeatedly trip on this: $ lark-cli sheets +cells-batch-set-style \ --ranges '["7f8fba!A2:B3","7f8fba!C2:D3"]' --font-color '#3366FF' ... → tool "batch_update" failed: [900015206] sheet "7f8fba" not foun…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds an HTML lint library + Larksuite-native autofix to
lark-cli mail, plus theskills/lark-mail/skill bundle (2 reference docs, 5 HTML templates, the+lint-htmlshortcut, and writing-path lint integration across all 6 compose shortcuts).What's in this PR
1. Lint library (
shortcuts/mail/lint/)3-tier rule set:
<script>/<iframe>/<form>/<input>/<link>/<object>/<embed>),on*event handlers, andjavascript:/vbscript:/file:URLs.<font>/<center>/<marquee>/<blink>).<p>/<ul>/<ol>/<li>/<blockquote>/<a>to mail-editor native markup so AI can write the simplest HTML and still produce native-quality rendering.<style>block passthrough (server adds CSS scope class).2.
+lint-htmlshortcut (preview / CI)Read-only HTML preview tool. Default envelope returns only
cleaned_html;--show-lint-detailsadds fullwarnings[]/errors[].--strictexits non-zero on any finding (CI gate).3. Writing-path lint in the 6 compose shortcuts
+send/+draft-create/+reply/+reply-all/+forward/+draft-editbody op all run lint before drafting:lint_applied_count/original_blocked_count— always present.lint_applied[]/original_blocked[]— only with--show-lint-details.compose_hint— points AI consumers to the HTML writing guide.4.
skills/lark-mail/skill bundlereferences/lark-mail-html.md— writing rules + format primitives + template-usage flow.references/lark-mail-lint-html.md—+lint-htmlusage + return-value contract + 9 examples.SKILL.mdupdates linking the new docs and templates.5. Sealed conventions
Fixed writing conventions enforced by the lint library, the Larksuite mail-editor data model, or the upstream service-side sanitiser.
id="at-user-N"is the only hard requirement; do not writedata-user-id.http(s):/mailto:/cid:/data:image/*only.<p>/<div>/<span>/<a>/<img>/<table>(with<thead>/<tbody>/<tfoot>/<tr>/<td>/<th>/<caption>/<colgroup>/<col>) /<ul>/<ol>/<li>/<blockquote>/<h1>-<h6>/<b>/<i>/<em>/<strong>/<u>/<s>/<sub>/<sup>/<pre>/<code>/<style>.Tests
shortcuts/mail/lint/...— unit tests for every rule.shortcuts/mail/mail_lint_html_test.go—+lint-htmlenvelope contract.shortcuts/mail/mail_lint_writepath_test.go— writing-path envelope contract.+draft-createsmoke test.Test plan
go test ./shortcuts/mail/lint/... ./shortcuts/mail/...+draft-createsmoke--show-lint-detailsenvelope verified for both+lint-htmland+draft-create