diff --git a/CHANGELOG.md b/CHANGELOG.md index dfbf20b6..6d9494c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,15 @@ The format is inspired by [Keep a Changelog](https://keepachangelog.com/) and th ### Added +- **Streaming-card mentions** — `chat +messages-send-card` now accepts + `--at-open-dingtalk-ids` and `--at-all` for group cards, passing mention + targets to the initial card-creation request and prepending its returned + `atTag` to the automatic streaming update. + +## [1.0.58-beta.2] - 2026-08-10 + +### Added + - **`dws sheet create-with-data`(新命令)** — 建表并写入初始数据与样式:`--values`(二维数组写默认表)/ `--sheets`(typed table 多工作表,二者必须给一个)/ `--styles`(`cell_styles` / `row_sizes` / `col_sizes` / `cell_merges`,顶层键对齐飞书 snake_case、列表项内字段兼容 camelCase)。所有结构、字段类型与枚举在创建文档之前校验,非法配置不会留下白建的空文档:`--sheets` 按 `table_put` 的输入契约逐字段校验(`columns` 必填且列名非空不重复、`data` 为二维且行宽与 `columns` 一致、单元格仅限字符串/数字/布尔/null、`dtypes`/`formats` 的键须是列名、`mode`/`header`/`allowOverwrite`/`startCell` 类型与取值、单表 30000 单元格上限),并拒绝未知键、snake_case 变体、`{"sheets":"bad"}` 这类畸形包装与 `sheetId`(服务端会静默丢弃写错的键,导致"只写了表头却报成功"的静默丢数据);`--values` 校验单元格为标量并受 30000 单元格 / 2000000 字符上限约束;`--styles` 的顶层键与列表项内字段同样拒绝未知键,避免样式只应用一半。写入后回读校验按 `startCell` / `header` / `mode` 推算的首个预期非空单元格,而非固定 A1。该命令是多步编排(建文档 → 探活 → 定位默认工作表 → 写数据 → 回读 → 可选样式),因此如实声明为独立叶子 `sheet.create_with_data` + `interface_mode: composite`(附评审 reason,按契约不带 `interface_ref`);**`dws sheet create` 保持原样不变**——仍是一次 `create_workspace_sheet` 直连(`interface_mode: mcp`),不新增 flag,避免让 Schema 消费者把编排步骤的参数误当成该 RPC 的入参。 - **`dws sheet export-csv`(新命令)** — 同步导出单个工作表为 RFC4180 CSV,支持 `--sheet-id` 选表、`--range` 限定范围、`--value-render-option` 选取值模式;`--output` 落盘(为目录时按 `sheet-export.csv` 命名,落盘走 `AtomicWrite` 原子替换,写入失败时已有文件保持原样;父目录不存在按错误处理,不会自动创建),不传则把纯 CSV 打到 stdout(警告只走 stderr)。数据超出单次读取上限时默认报错、既不输出也不写文件,需 `--allow-truncated` 显式接受不完整结果。响应缺 `csv` 字段或类型不对一律报错,不会用 0 字节覆盖已有文件。该分支读的是 `get_range_as_csv`、与 xlsx 的异步导出任务毫无关系,因此独立成叶子 `sheet.export_csv` 并如实声明 `interface_mode: mcp` + `interface_ref: get_range_as_csv`;**`dws sheet export` 保持原样不变**——仍只导 xlsx(`interface_ref: submit_export_job`),flag 面仍是 `--node` / `--output`,csv 专属 flag 不会出现在它上面(此前挂在同一条命令上时,漏写 `--export-format csv` 会让 `--range` 被静默丢弃而导出整篇工作簿)。 - **`sheet update-dimension --size-type`** — `pixel` / `standard`(恢复默认行高列宽)/ `auto`(按内容自适应行高,仅 ROWS)。 @@ -17,10 +26,19 @@ The format is inspired by [Keep a Changelog](https://keepachangelog.com/) and th ### Changed +- **Chat message post-send ID handoff** (#897) — CLI Help and bundled Skills + now document the `send` → `query-send-status` → `edit`/`recall` workflow, + so callers can reuse returned task, message, and conversation IDs instead + of searching message history by content. +- **Sheet mono/multi Skill alignment** — replaces the oversized mono Sheet + reference with the progressive routing layout, aligns all 20 Sheet topic + references across the mono and multi bundles, and adds a content-policy guard + that prevents the paired topic trees from drifting again. - **`sheet range set-style` 后端切换为 `set_cell_range`** — 样式统一走 cellStyles 路径(仅设样式、保留原值),这是斜体/下划线删除线/字体族/边框唯一可用的通道。`interface_ref` 由 `update_range` 变为 `set_cell_range`,12 个样式 flag 改为 reviewed mapping exclusion。CLI 用法向后兼容、无 flag 删除;schema-compatibility 经 reviewed 豁免判定为兼容(0 changed fields)。 - **`sheet range batch-set-style` 改为单次原子提交** — 由本地循环多次 `update_range` 改为一次 `batch_update`,任一项失败默认整批回滚;`--continue-on-error` 由本地控制改为透传服务端。新增批量上限:最多 100 个区域且累计不超过 200000 个单元格。 - **`sheet range batch-clear` / `batch-set-style` 的 `--ranges` 拒绝空白工作表前缀**(用户可见行为变更)— 此前只按原始串里 `!` 的位置判断,`" !A1:B2"` 修剪后工作表名成了空串,操作却照样带着 `sheetId: ""` 提交:服务端要么让整批 `batch_update` 失败,要么更糟——落到默认工作表而不是用户指定的那张表,且命令报成功。现在工作表名与范围都必须在修剪之后仍非空,否则在发起任何请求之前报错。`batch-set-style --batch` 的纯空白 `sheetId` / `range` 同样拒绝(此前只挡空字符串);`--batch` 下发仍用原值不替用户修剪,因为 `sheetId` 可以是允许带首尾空格的工作表**名**。两条 `--ranges` 路径现在共用同一个拆分器。 - **`sheet insert-dimension` / `delete-dimension` / `update-dimension` 的 `--length` 严格校验**(用户可见行为变更)— 解析由 `fmt.Sscanf("%d")` 改为 `strconv.Atoi`。此前只消费前缀数字,`--length 2x` / `3foo` 会被静默当成 `2` / `3` 并对错误的行列数执行操作(删除方向不可回滚);现在整个值必须是合法正整数,否则报错「`--length` 必须为正整数(>= 1)」且不发起任何请求。**升级影响**:原先依赖这种宽松解析、在传畸形 `--length` 的脚本会开始报错,请把参数修正为纯数字。合法数字值行为不变,上限仍为 5000。`add-dimension` 的 `--length` 是 `Int` 类型 flag,一直由 cobra 严格校验,不受影响。 +- **CLI 接口兼容门禁支持 reviewed flag 类型豁免**(无用户可见变更)— `authoritative-interface-integrity` 与 `check-command-compatibility.sh` 此前一律拒绝历史命令的 flag 类型变更,即使新类型只是把同一套校验从 RunE 前移到解析期,也没有任何评审通道。现在两道门禁各带一张精确豁免表:命令路径 + flag 名 + 旧类型 → 新类型四元组全等才命中、方向敏感(`string`→`int` 与 `int`→`string` 是两个不同的键,只有被评审的方向可用),且仅当该 flag 的其他契约(shorthand / required / hidden / no-opt / scope)纹丝不动时才放行,因此豁免夹带不了别的破坏。首条也是目前唯一一条登记的是 `dws minutes permission apply --policy` 的 `string` → `int`(配合 #912):旧实现在 RunE 里做 `strconv.ParseInt(v, 10, 64)` 再校验 `[2,4]`,新实现由 pflag 以 `strconv.ParseInt(s, 0, 64)` 解析后仍校验 `[2,4]`,**历史上能成功的调用集是新调用集的子集**(base 0 额外接受 `0x3` 这类写法,只放宽不收紧),非法值依然失败、只是报错文案与时机前移;flag 默认值由 `""` 变 `"0"` 是类型的必然结果,两道门禁都不比较默认值,且该 flag 必须显式给出、默认值不可达。两张表必须逐字一致并有守卫测试锚定漂移——重复是被迫的而非选择:`check-authoritative-interface-baselines.sh` 会把整个 `scripts/policy/interface-baseline` 目录复制进检出历史版本的 worktree 再编译,那份拷贝不能 import 本分支新增的包。 ## [1.0.58-beta.1] - 2026-08-07 diff --git a/Formula/dingtalk-workspace-cli-beta.rb b/Formula/dingtalk-workspace-cli-beta.rb index 0836f571..e3fef1f0 100644 --- a/Formula/dingtalk-workspace-cli-beta.rb +++ b/Formula/dingtalk-workspace-cli-beta.rb @@ -1,33 +1,33 @@ class DingtalkWorkspaceCliBeta < Formula desc "Automate DingTalk workspace tasks from the terminal (beta channel)" homepage "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli" - version "1.0.58-beta.1" + version "1.0.58-beta.2" license "Apache-2.0" keg_only "it is the beta channel and conflicts with dingtalk-workspace-cli" on_macos do if Hardware::CPU.arm? - url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.58-beta.1/dws-darwin-arm64.tar.gz" - sha256 "34f7dabf1a97ed950c1cd331e5fb873a62c87632db249ca1b42bd2da925927f0" + url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.58-beta.2/dws-darwin-arm64.tar.gz" + sha256 "1b2b6953f7f1ae1ca6ecb0702424ac0e1a976a6a5ff91e8ffc3b5ae495d98c7c" else - url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.58-beta.1/dws-darwin-amd64.tar.gz" - sha256 "c812ad070ac76f8ab5aefe2ae1b9cdf9217389b383296acb36070fc2f40b9890" + url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.58-beta.2/dws-darwin-amd64.tar.gz" + sha256 "a1c1b3c58b48e04c0ae520062f9d6ab0dc961eddb635497bdb9b4345316e45f6" end end on_linux do if Hardware::CPU.arm? - url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.58-beta.1/dws-linux-arm64.tar.gz" - sha256 "7dd5e38647c88e4b7936e0f41315cd612128faeb7960f50b28dbbd1fa9683d68" + url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.58-beta.2/dws-linux-arm64.tar.gz" + sha256 "7f35e3c4734f17b125a8c32f3c95e05d1410f683cf6956be857ee9349f8e4d36" else - url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.58-beta.1/dws-linux-amd64.tar.gz" - sha256 "b384671440a62f1e5dca5dcdb49e51ef1aad816b601021ba363c8c69986ebd2f" + url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.58-beta.2/dws-linux-amd64.tar.gz" + sha256 "37beb9e39790563cf0584ac23376f713bf2eb2c50cff4222965e831ac9adbb0e" end end resource "skills" do - url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.58-beta.1/dws-skills.zip" - sha256 "2cf96aa2d35a01a4b92db2bd2d1760476d62d7528de5ec50a997c9a7713c3513" + url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.58-beta.2/dws-skills.zip" + sha256 "7e10fead4192059c98d596c5b1886f77fd550526de5cd18c425cdad6fd64cd3a" end def install diff --git a/docs/skill-content-framework.md b/docs/skill-content-framework.md index 1b179e8c..3730a8a3 100644 --- a/docs/skill-content-framework.md +++ b/docs/skill-content-framework.md @@ -101,5 +101,5 @@ skills/mono/ ## 7. 变更流程 1. 改 / 增内容 → 更新 `skills/content-qa/mono-multi-coverage.yaml`(coverage 或 omit) -2. 跑 `make skill-mono-multi-content`(或 `make policy`) +2. 跑 `make skill-mono-multi-content`(该独立门禁不包含在默认 `make policy` 中) 3. 失败则修内容或更新 reviewed omit(disposition + 原因),**禁止**用安装默认值绕过 diff --git a/docs/skill-mono-multi-qa.md b/docs/skill-mono-multi-qa.md index aa2432a7..2c885474 100644 --- a/docs/skill-mono-multi-qa.md +++ b/docs/skill-mono-multi-qa.md @@ -2,7 +2,7 @@ > 对照基准:`skills/mono`(单 skill)。被测主体:`skills/multi`。 > 机读合同:`skills/content-qa/mono-multi-coverage.yaml`。 -> 执行:`make skill-mono-multi-content`(已挂入 `make policy`)。 +> 执行:`make skill-mono-multi-content`(独立门禁;默认 `make policy` 按设计不包含该检查)。 ## 1. 质检矩阵 @@ -11,7 +11,8 @@ | **G1 形状** | 结构 | `skills/multi/*` | 仅 `dingtalk-*`(含必选 `dingtalk-shared`);每目录有 `SKILL.md` | | **G2 结构** | 结构 | 各 `SKILL.md` frontmatter | `name`==目录名;非空 `description`;`category`∈{product,shared};`requires.bins` 含 `dws` | | **G3 覆盖** | 覆盖 | mono `references/products/*` 顶层 stem | 每 stem ∈ `coverage` 或 `omit_coverage`;coverage 目标 skill/refs 存在 | -| **G4 漂移** | 漂移 | scripts、成对文件、全局协议 | orphan 脚本 ∈ allowlist;paired 一致;全局协议存在或 ∈ `omit_global` | +| **G4 漂移** | 漂移 | scripts、成对文件、全局协议 | orphan 脚本 ∈ allowlist;paired 内容一致(允许合同声明的布局链接替换);全局协议存在或 ∈ `omit_global` | +| **G5 链接** | 可达性 | 合同覆盖的 paired Markdown | 内联相对链接目标文件或目录存在且不逃出仓库;外链、纯锚点和锚点内容不在检查范围 | 已有门禁(继续复用,不替代本矩阵):`check-skill-commands`、`check-skill-context-budget`、`check-multi-im-skill-chain`、`skill_docs_policy`、whiteboard 成对测试。 @@ -37,6 +38,16 @@ orphan_scripts_allowlist: - path: dingtalk-misc/scripts/report_received_today.py disposition: defer reason: "pending report.md reference" + +paired_files: + - mono: references/products/sheet.md + multi: dingtalk-misc/references/sheet.md + mode: link-normalized + link_substitutions: + - mono: "../url-patterns.md" + multi: "../../dingtalk-shared/references/url-patterns.md" + - mono: "../intent-guide.md" + multi: "sheet-intent-guide.md" ``` **处置原则**:质检失败 → 修**内容**或更新 reviewed omit;**不**改安装/升级默认。 diff --git a/docs/skill-wukong-align-plan.md b/docs/skill-wukong-align-plan.md index de280a76..b2d64d0c 100644 --- a/docs/skill-wukong-align-plan.md +++ b/docs/skill-wukong-align-plan.md @@ -4,7 +4,7 @@ > 合同短文:[skill-content-framework.md](skill-content-framework.md) > 质检规格:[skill-mono-multi-qa.md](skill-mono-multi-qa.md) > 机读合同:`skills/content-qa/mono-multi-coverage.yaml` -> 门禁:`make skill-mono-multi-content`(已入 `make policy`) +> 门禁:`make skill-mono-multi-content`(独立门禁;默认 `make policy` 按设计不包含该检查) > > 撰写 / 收窄 / 质检增补 / 执行:2026-08-05 > 工作树:`/Users/john/GolandProjects/open-source/dws-multi-skill-align` diff --git a/internal/helpers/chat.go b/internal/helpers/chat.go index 0003918e..c922c596 100644 --- a/internal/helpers/chat.go +++ b/internal/helpers/chat.go @@ -2042,6 +2042,11 @@ func newChatCommand() *cobra.Command { 纯文本 / Markdown 消息(默认): 无需指定 --msg-type,直接传消息内容即可。推荐使用 --text flag 传递内容(尤其当内容含换行、引号等特殊字符时),也支持位置参数。可选 --title 作为消息标题。 +返回值与后续操作: + 发送后会返回 openTaskId。如需编辑或撤回刚发送的消息,使用 + dws chat message query-send-status --open-task-id + 获取 openMessageId 和 openConversationId,无需再按消息内容反查。 + 本地图片 / 文件消息: 统一使用 --msg-type file --file-path <本地路径>。CLI 会完成上传并按 file 消息发送; .png/.jpg 也会显示为可下载的文件附件,不会生成 mediaId 或渲染成内联图片。 @@ -3304,7 +3309,10 @@ func newChatCommand() *cobra.Command { chatMessageQuerySendStatusCmd := &cobra.Command{ Use: "query-send-status", Short: "查询消息发送状态", - Long: `查询以当前用户身份发送的消息的发送状态。需要传入发送消息时返回的 openTaskId。`, + Long: `查询以当前用户身份发送的消息的发送状态。需要传入 chat message send 返回的 openTaskId。 + +发送成功后,查询结果会返回 openMessageId 和 openConversationId,可直接作为 +chat message edit 或 chat message recall 的 --msg-id 和 --conversation-id。`, Example: ` dws chat message query-send-status --open-task-id # openTaskId 由 dws chat message send 返回`, RunE: func(cmd *cobra.Command, args []string) error { @@ -3351,8 +3359,11 @@ func newChatCommand() *cobra.Command { Short: "撤回用户发送的消息", Long: `撤回当前用户发送的消息。需要指定会话 ID 和消息 ID。`, Example: ` dws chat message recall --conversation-id --msg-id - # 查询会话 ID: dws chat search --query "群名" - # 消息 ID 可通过 dws chat message list 获取`, + + # 发送后撤回:send -> query-send-status -> recall + dws chat message send --group --text "待撤回的内容" + dws chat message query-send-status --open-task-id <上一步返回的openTaskId> + dws chat message recall --conversation-id <上一步返回的openConversationId> --msg-id <上一步返回的openMessageId>`, RunE: func(cmd *cobra.Command, args []string) error { if err := validateRequiredFlagWithAliases(cmd, "conversation-id", "group", "id", "chat"); err != nil { return err @@ -3409,12 +3420,16 @@ func newChatCommand() *cobra.Command { 推荐使用 --text 和可选 --title,CLI 会按 Markdown 消息规则生成 content:{"title":"标题","text":"正文"}。 也可以直接使用 --content 传入完整 Markdown content JSON。--text 和 --content 二选一。`, Example: ` dws chat message edit --conversation-id --msg-id --text "更新后的内容" + + # 发送后编辑:send -> query-send-status -> edit + dws chat message send --group --text "原始内容" + dws chat message query-send-status --open-task-id <上一步返回的openTaskId> + dws chat message edit --conversation-id <上一步返回的openConversationId> --msg-id <上一步返回的openMessageId> --text "更新后的内容" + dws chat message edit --group --msg-id --title "标题" --text "更新后的内容" dws chat message edit --group --msg-id --text "<@all> 请查看" --at-all dws chat message edit --group --msg-id --text "<@openDingTalkId1> 请查看" --at-open-dingtalk-ids - dws chat message edit --group --msg-id --content '{"title":"标题","text":"更新后的内容"}' - # 查询会话 ID: dws chat search --query "群名" - # 消息 ID 可通过 dws chat message list 获取`, + dws chat message edit --group --msg-id --content '{"title":"标题","text":"更新后的内容"}'`, RunE: func(cmd *cobra.Command, args []string) error { if err := validateRequiredFlagWithAliases(cmd, "conversation-id", "group", "id", "chat"); err != nil { return err diff --git a/internal/helpers/chat_message_help_test.go b/internal/helpers/chat_message_help_test.go new file mode 100644 index 00000000..fcf1548c --- /dev/null +++ b/internal/helpers/chat_message_help_test.go @@ -0,0 +1,92 @@ +// Copyright 2026 Alibaba Group +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package helpers + +import ( + "bytes" + "strings" + "testing" +) + +func TestCrossPlatformCoverageChatMessageHelpDocumentsPostSendIDChain(t *testing.T) { + tests := []struct { + name string + command string + contains []string + notContain string + }{ + { + name: "send returns task ID", + command: "send", + contains: []string{ + "openTaskId", + "query-send-status --open-task-id ", + }, + }, + { + name: "query returns message and conversation IDs", + command: "query-send-status", + contains: []string{ + "openTaskId", + "openMessageId", + "openConversationId", + "chat message edit", + "chat message recall", + }, + }, + { + name: "edit includes post-send workflow", + command: "edit", + contains: []string{ + "send -> query-send-status -> edit", + "query-send-status --open-task-id <上一步返回的openTaskId>", + "edit --conversation-id <上一步返回的openConversationId> --msg-id <上一步返回的openMessageId>", + }, + notContain: "chat message list", + }, + { + name: "recall includes post-send workflow", + command: "recall", + contains: []string{ + "send -> query-send-status -> recall", + "query-send-status --open-task-id <上一步返回的openTaskId>", + "recall --conversation-id <上一步返回的openConversationId> --msg-id <上一步返回的openMessageId>", + }, + notContain: "chat message list", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + cmd := newChatCommand() + var output bytes.Buffer + cmd.SetOut(&output) + cmd.SetErr(&output) + cmd.SetArgs([]string{"message", test.command, "--help"}) + if err := cmd.Execute(); err != nil { + t.Fatalf("chat message %s --help: %v\n%s", test.command, err, output.String()) + } + + help := output.String() + for _, want := range test.contains { + if !strings.Contains(help, want) { + t.Errorf("chat message %s help missing %q:\n%s", test.command, want, help) + } + } + if test.notContain != "" && strings.Contains(help, test.notContain) { + t.Errorf("chat message %s help still contains %q:\n%s", test.command, test.notContain, help) + } + }) + } +} diff --git a/internal/interfacesnapshot/compare.go b/internal/interfacesnapshot/compare.go index 79fcd2d3..17546ab2 100644 --- a/internal/interfacesnapshot/compare.go +++ b/internal/interfacesnapshot/compare.go @@ -73,6 +73,10 @@ func CompareAll(current Snapshot, references map[string]Snapshot) Report { // become required; an existing path may not gain a new required flag, // - new commands and flags are allowed. // +// The single exception to the type rule is an individually reviewed migration +// listed in reviewedFlagTypeChanges, and only when nothing else about the flag +// changed. See reviewed.go. +// // Comparing the effective local + inherited set is intentional. It catches a // persistent flag whose scope is accidentally narrowed to its declaring // command, while allowing a local flag to move to an ancestor without breaking @@ -183,13 +187,20 @@ func compareEffectiveFlags(result *Comparison, acceptedPath string, oldCommand, continue } if oldFlag.Type != newFlag.Type { - result.Blocking = append(result.Blocking, Change{ - Kind: "flag_type_changed", - Path: acceptedPath, - Flag: name, - Before: oldFlag.Type, - After: newFlag.Type, - }) + // Resolve the exemption against the canonical path, not acceptedPath. + // An aliased command reaches this function once per accepted spelling, + // so keying on acceptedPath would let every alias spelling re-report a + // reviewed migration. + if !reviewedFlagTypeChange(newCommand.Path, name, oldFlag.Type, newFlag.Type) || + flagContractOtherwiseChanged(oldFlag, newFlag) { + result.Blocking = append(result.Blocking, Change{ + Kind: "flag_type_changed", + Path: acceptedPath, + Flag: name, + Before: oldFlag.Type, + After: newFlag.Type, + }) + } } if !oldFlag.Required && newFlag.Required { result.Blocking = append(result.Blocking, Change{ diff --git a/internal/interfacesnapshot/compare_test.go b/internal/interfacesnapshot/compare_test.go index 5e5b0ba0..a74fca27 100644 --- a/internal/interfacesnapshot/compare_test.go +++ b/internal/interfacesnapshot/compare_test.go @@ -13,7 +13,11 @@ package interfacesnapshot -import "testing" +import ( + "os" + "regexp" + "testing" +) func TestCrossPlatformCoverageCompareAdmissionPolicy(t *testing.T) { base := testSnapshot( @@ -310,6 +314,225 @@ func TestCrossPlatformCoverageCompareAllRequiresBothReferencesToPass(t *testing. } } +// registerReviewedFixture puts a fixture command in the reviewed table for the +// duration of one test, so the behaviour tests exercise the real lookup instead +// of depending on whichever production entries happen to exist. +func registerReviewedFixture(t *testing.T, change flagTypeChange) { + t.Helper() + if _, exists := reviewedFlagTypeChanges[change]; exists { + t.Fatalf("fixture %+v collides with a production entry", change) + } + reviewedFlagTypeChanges[change] = struct{}{} + t.Cleanup(func() { delete(reviewedFlagTypeChanges, change) }) +} + +func TestCrossPlatformCoverageCompareAcceptsReviewedFlagTypeChange(t *testing.T) { + base := testSnapshot( + testCommand("dws"), + testCommand("dws search", testFlag("query", "string", false)), + ) + current := testSnapshot( + testCommand("dws"), + testCommand("dws search", testFlag("query", "int", false)), + ) + + // Without an entry the migration is still a blocking change. + if comparison := Compare(current, base, "base"); !hasChangeKind(comparison.Blocking, "flag_type_changed") { + t.Fatalf("an unreviewed type change should block: %#v", comparison.Blocking) + } + + registerReviewedFixture(t, flagTypeChange{CommandPath: "dws search", Flag: "query", From: "string", To: "int"}) + if comparison := Compare(current, base, "base"); !comparison.Compatible { + t.Fatalf("a reviewed string->int migration should pass: %#v", comparison.Blocking) + } +} + +func TestCrossPlatformCoverageCompareReviewedFlagTypeChangeIsDirectionSensitive(t *testing.T) { + registerReviewedFixture(t, flagTypeChange{CommandPath: "dws search", Flag: "query", From: "string", To: "int"}) + + // The reverse migration shares command and flag but not direction, so the + // string->int entry must not admit it. + base := testSnapshot( + testCommand("dws"), + testCommand("dws search", testFlag("query", "int", false)), + ) + current := testSnapshot( + testCommand("dws"), + testCommand("dws search", testFlag("query", "string", false)), + ) + if comparison := Compare(current, base, "base"); comparison.Compatible { + t.Fatal("int->string must not be admitted by a string->int entry") + } +} + +func TestCrossPlatformCoverageCompareReviewedFlagTypeChangeRejectsOtherCommandsAndFlags(t *testing.T) { + base := testSnapshot( + testCommand("dws"), + testCommand("dws search", testFlag("query", "string", false)), + ) + current := testSnapshot( + testCommand("dws"), + testCommand("dws search", testFlag("query", "int", false)), + ) + + // Same flag and direction, different command. + registerReviewedFixture(t, flagTypeChange{CommandPath: "dws status", Flag: "query", From: "string", To: "int"}) + if comparison := Compare(current, base, "base"); comparison.Compatible { + t.Fatal("an entry for another command must not admit this migration") + } + + // Same command and direction, different flag. + registerReviewedFixture(t, flagTypeChange{CommandPath: "dws search", Flag: "elsewhere", From: "string", To: "int"}) + if comparison := Compare(current, base, "base"); comparison.Compatible { + t.Fatal("an entry for another flag must not admit this migration") + } +} + +// A reviewed migration is only accepted when the rest of the flag's contract +// held still. Each case bundles one unrelated regression with the reviewed type +// change and requires the type failure to reappear, which also anchors +// flagContractOtherwiseChanged against the checks it mirrors: a condition that +// drifts out of sync fails here instead of silently widening the exemption. +func TestCrossPlatformCoverageCompareReviewedFlagTypeChangeRejectsBundledRegression(t *testing.T) { + registerReviewedFixture(t, flagTypeChange{CommandPath: "dws search", Flag: "query", From: "string", To: "int"}) + + tests := []struct { + name string + base Flag + next Flag + kind string + }{ + { + name: "became required", + base: Flag{Name: "query", Type: "string"}, + next: Flag{Name: "query", Type: "int", Required: true}, + kind: "flag_became_required", + }, + { + name: "lost shorthand", + base: Flag{Name: "query", Type: "string", Shorthand: "q"}, + next: Flag{Name: "query", Type: "int"}, + kind: "flag_shorthand_changed", + }, + { + name: "changed no-opt", + base: Flag{Name: "query", Type: "string", NoOpt: "all"}, + next: Flag{Name: "query", Type: "int", NoOpt: "1"}, + kind: "flag_no_opt_changed", + }, + { + name: "became hidden", + base: Flag{Name: "query", Type: "string"}, + next: Flag{Name: "query", Type: "int", Hidden: true}, + kind: "flag_became_hidden", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + base := testSnapshot(testCommand("dws"), testCommand("dws search", test.base)) + current := testSnapshot(testCommand("dws"), testCommand("dws search", test.next)) + comparison := Compare(current, base, "base") + if !hasChangeKind(comparison.Blocking, test.kind) { + t.Fatalf("blocking=%#v, want kind %q", comparison.Blocking, test.kind) + } + if !hasChangeKind(comparison.Blocking, "flag_type_changed") { + t.Fatalf("a bundled regression must re-report the type change: %#v", comparison.Blocking) + } + }) + } +} + +// The exemption is resolved against the canonical Command.Path, never the +// alias-expanded accepted path. An aliased command enters compareEffectiveFlags +// once per accepted spelling, so keying on the accepted path would let every +// alias spelling re-report a reviewed migration. +func TestCrossPlatformCoverageCompareResolvesReviewedFlagTypeChangeByCanonicalPath(t *testing.T) { + base := testSnapshot( + testCommand("dws"), + testCommandWithAliases("dws search", []string{"find"}, testFlag("query", "string", false)), + ) + current := testSnapshot( + testCommand("dws"), + testCommandWithAliases("dws search", []string{"find"}, testFlag("query", "int", false)), + ) + + // The canonical path admits the migration for every accepted spelling. + canonical := flagTypeChange{CommandPath: "dws search", Flag: "query", From: "string", To: "int"} + registerReviewedFixture(t, canonical) + if comparison := Compare(current, base, "base"); !comparison.Compatible { + t.Fatalf("canonical path should admit the migration on every alias: %#v", comparison.Blocking) + } + delete(reviewedFlagTypeChanges, canonical) + + // An entry spelled with the alias path matches nothing, because the lookup + // never sees the accepted spelling. + registerReviewedFixture(t, flagTypeChange{CommandPath: "dws find", Flag: "query", From: "string", To: "int"}) + if comparison := Compare(current, base, "base"); comparison.Compatible { + t.Fatal("an alias-spelled entry must not admit the migration") + } +} + +// A string -> int migration always changes the recorded default ("" to "0"). +// Defaults are not part of what this gate enforces, and folding them into +// flagContractOtherwiseChanged would make every table entry dead on arrival. +func TestCrossPlatformCoverageCompareReviewedFlagTypeChangeToleratesDefaultDrift(t *testing.T) { + registerReviewedFixture(t, flagTypeChange{CommandPath: "dws search", Flag: "query", From: "string", To: "int"}) + + base := testSnapshot( + testCommand("dws"), + testCommand("dws search", Flag{Name: "query", Type: "string", Default: ""}), + ) + current := testSnapshot( + testCommand("dws"), + testCommand("dws search", Flag{Name: "query", Type: "int", Default: "0"}), + ) + if comparison := Compare(current, base, "base"); !comparison.Compatible { + t.Fatalf("default drift must not defeat a reviewed migration: %#v", comparison.Blocking) + } +} + +// reviewedEntryPattern matches one table entry as gofmt writes it on a single +// line. It is used to read the sibling copy of the table out of the policy +// helper, which cannot be imported: that directory is copied into a worktree at +// a historical revision and built there, so it must not depend on this package. +var reviewedEntryPattern = regexp.MustCompile( + `\{CommandPath:\s*"([^"]*)",\s*Flag:\s*"([^"]*)",\s*From:\s*"([^"]*)",\s*To:\s*"([^"]*)"\}`) + +const interfaceBaselineReviewedPath = "../../scripts/policy/interface-baseline/reviewed.go" + +// The two copies of the table must stay identical, otherwise a migration passes +// one gate and is blocked by the other in the same CI job. +func TestCrossPlatformCoverageReviewedFlagTypeTableMatchesInterfaceBaseline(t *testing.T) { + source, err := os.ReadFile(interfaceBaselineReviewedPath) + if err != nil { + t.Fatalf("读取 %s 失败: %v", interfaceBaselineReviewedPath, err) + } + + sibling := make(map[flagTypeChange]struct{}) + for _, match := range reviewedEntryPattern.FindAllStringSubmatch(string(source), -1) { + sibling[flagTypeChange{CommandPath: match[1], Flag: match[2], From: match[3], To: match[4]}] = struct{}{} + } + + // Without this the whole guard degrades into comparing two empty sets the + // moment the pattern stops matching, which is exactly how a duplicated + // allowlist goes stale unnoticed. + if len(sibling) == 0 { + t.Fatalf("未能从 %s 提取到任何条目:正则或表的书写形态已漂移,这条守卫已失效", interfaceBaselineReviewedPath) + } + + for change := range reviewedFlagTypeChanges { + if _, ok := sibling[change]; !ok { + t.Errorf("%+v 只存在于 interfacesnapshot,缺在 %s", change, interfaceBaselineReviewedPath) + } + } + for change := range sibling { + if _, ok := reviewedFlagTypeChanges[change]; !ok { + t.Errorf("%+v 只存在于 %s,缺在 interfacesnapshot", change, interfaceBaselineReviewedPath) + } + } +} + func testSnapshot(commands ...Command) Snapshot { return Snapshot{ SchemaVersion: SchemaVersion, diff --git a/internal/interfacesnapshot/reviewed.go b/internal/interfacesnapshot/reviewed.go new file mode 100644 index 00000000..6fcc5049 --- /dev/null +++ b/internal/interfacesnapshot/reviewed.go @@ -0,0 +1,121 @@ +// Copyright 2026 Alibaba Group +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package interfacesnapshot + +// flagTypeChange identifies one flag type migration exactly: which command, +// which flag, and in which direction. Nothing is matched by wildcard — an entry +// that differs in any of the four fields does not apply. +// +// CommandPath is the canonical Command.Path form, which always includes the root +// command name ("dws minutes permission apply"). It is deliberately *not* the +// alias-expanded accepted path: a command reachable through an alias enters +// compareEffectiveFlags once per accepted spelling, and keying on the accepted +// path would let every alias spelling bypass the exemption and re-report the +// change. +// +// This table is duplicated in scripts/policy/interface-baseline/reviewed.go and +// the two must stay identical; TestCrossPlatformCoverageReviewedFlagTypeTableMatchesInterfaceBaseline +// fails if they drift. The duplication is forced rather than chosen: +// check-authoritative-interface-baselines.sh copies the whole +// scripts/policy/interface-baseline directory into a worktree checked out at a +// historical revision and builds it there, so that copy cannot import a package +// this branch adds. +type flagTypeChange struct { + CommandPath string + Flag string + From string + To string +} + +// reviewedFlagTypeChanges enumerates the individually reviewed flag type +// migrations this gate accepts. A flag type is part of the published CLI +// contract, and a type swap cannot be proven safe from the type names alone — +// whether old invocations still parse depends on the concrete value domain the +// command enforces. So a migration is accepted only when this exact command, +// flag, and old→new pair appear here. Adding an entry is a contract decision +// and belongs in review, not in a feature change. +// +// Entries are direction-sensitive by construction: "string" → "int" is a +// separate key from "int" → "string" and only the reviewed direction is +// accepted. The reverse is not a candidate for this table — widening an int +// flag back to string lets values the parser used to reject reach RunE, which +// is a genuine loosening of validation rather than a type migration. +// +// The table alone does not admit a change: reviewedFlagTypeChange is consulted +// only when nothing else about the flag moved. See compareEffectiveFlags. +var reviewedFlagTypeChanges = map[flagTypeChange]struct{}{ + // "dws minutes permission apply --policy" moved from a String flag parsed + // with strconv.ParseInt(v, 10, 64) in RunE to a native Int flag, keeping the + // same [2,4] domain check in RunE. + // + // This is admissible because the historical set of *successful* invocations + // is a subset of the new one, not merely similar to it: + // + // - Old: the value had to parse as base-10 and land in [2,4]. That is + // exactly "2", "3", "4" plus sign/leading-zero spellings of them + // ("+3", "03", "003", ...). + // - New: pflag's intValue.Set parses with strconv.ParseInt(s, 0, 64) — + // base 0 — and RunE still enforces [2,4]. Every base-10 spelling above + // resolves to the same number under base 0, so no historical success + // becomes a failure. + // - Base 0 additionally accepts "0x3" and friends, which the old parser + // rejected. That widens the accepted set; it cannot break a caller who + // was already succeeding. + // - Values outside [2,4] and non-numeric values still fail. Only the + // failure *message and timing* move, from RunE to flag parsing. + // + // The flag's default value changes from "" to "0" as an unavoidable + // consequence of the type. Neither gate compares defaults, and a default is + // not reachable by a caller here because RunE requires the flag to be + // explicitly set, so this is not a contract change. + {CommandPath: "dws minutes permission apply", Flag: "policy", From: "string", To: "int"}: {}, +} + +// reviewedFlagTypeChange reports whether this exact command, flag and direction +// is a reviewed migration. Callers must additionally establish that the rest of +// the flag's contract is unchanged before skipping the failure. +func reviewedFlagTypeChange(commandPath, flag, from, to string) bool { + _, reviewed := reviewedFlagTypeChanges[flagTypeChange{ + CommandPath: commandPath, + Flag: flag, + From: from, + To: to, + }] + return reviewed +} + +// flagContractOtherwiseChanged reports whether anything this gate checks about a +// flag, other than its type, differs between the two revisions. A reviewed type +// migration is only accepted when this returns false, so the exemption cannot +// carry an unrelated regression in behind it. +// +// Only the fields compareEffectiveFlags actually enforces are compared. Default +// is deliberately excluded: a string → int migration always changes it ("" to +// "0"), so including it would make every entry in the table dead. Deprecated is +// excluded for the same reason it is not a blocking change on its own. +func flagContractOtherwiseChanged(oldFlag, newFlag Flag) bool { + if !oldFlag.Required && newFlag.Required { + return true + } + if oldFlag.Shorthand != "" && newFlag.Shorthand != oldFlag.Shorthand { + return true + } + if oldFlag.NoOpt != "" && newFlag.NoOpt != oldFlag.NoOpt { + return true + } + if !oldFlag.Hidden && newFlag.Hidden { + return true + } + return false +} diff --git a/internal/shortcut/chat/chat_message.go b/internal/shortcut/chat/chat_message.go index 72328efd..5a1f7576 100644 --- a/internal/shortcut/chat/chat_message.go +++ b/internal/shortcut/chat/chat_message.go @@ -1439,8 +1439,8 @@ var MessagesSendCard = shortcut.Shortcut{ Service: "chat", Command: "+messages-send-card", Product: "im", - Description: "创建流式卡片,可在同一次调用中写入内容并结束", - Intent: "当你要发送一张流式文本卡片时使用;群 openConversationId、单聊 userId、单聊 openDingTalkId 严格三选一,分别使用 --group、--receiver、--receiver-open-dingtalk-id。--receiver 始终按 userId 通过通讯录关键词搜索做精确匹配,即使值以 D/d 开头也不会猜成 openDingTalkId;已有 openDingTalkId 时必须用显式参数直传。userId 包括在 --dry-run 时也会先解析。只传目标时创建卡片并返回 bizId,供后续 messages-update-card 流式更新;同时传 --content 时会自动串联创建和更新,默认以 flowStatus=3 完成。当前只支持 streaming text,不支持 Card JSON 组件或 action callback。", + Description: "创建流式卡片,可在同一次调用中写入内容并结束;群聊创建时可 @成员或 @所有人", + Intent: "当你要发送一张流式文本卡片时使用;群 openConversationId、单聊 userId、单聊 openDingTalkId 严格三选一,分别使用 --group、--receiver、--receiver-open-dingtalk-id。群聊可用 --at-open-dingtalk-ids 或 --at-all 在创建卡片时设置 @对象;同时传 --content 时,Runtime 会把创建响应中的 atTag 自动加在正文前,后续更新不重复传递 @参数。--receiver 始终按 userId 通过通讯录关键词搜索做精确匹配,即使值以 D/d 开头也不会猜成 openDingTalkId;已有 openDingTalkId 时必须用显式参数直传。userId 包括在 --dry-run 时也会先解析。只传目标时创建卡片并返回 bizId,供后续 messages-update-card 流式更新;同时传 --content 时会自动串联创建和更新,默认以 flowStatus=3 完成。当前只支持 streaming text,不支持 Card JSON 组件或 action callback。", Risk: shortcut.RiskWrite, Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", @@ -1454,30 +1454,34 @@ var MessagesSendCard = shortcut.Shortcut{ CLIPath: "chat +messages-send-card", PrimaryCLIPath: "chat +messages-send-card", }, - Description: "创建流式卡片,可在同一次调用中写入内容并结束", + Description: "创建流式卡片,可在同一次调用中写入内容并结束;群聊创建时可 @成员或 @所有人", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", - Reason: "Reviewed card lifecycle adapter: it can resolve a userId through contact search with exact matching, call create_and_send_card alone, or compose creation with update_streaming_card after extracting the returned bizId.", + Reason: "Reviewed card lifecycle adapter: it can resolve a userId through contact search with exact matching, call create_and_send_card alone, or compose creation with update_streaming_card after extracting the returned bizId and atTag.", }, Selection: contract.SelectionSpec{ - AgentSummary: "创建流式卡片,可在同一次调用中写入内容并结束", - UseWhen: []string{"当你要发送一张流式文本卡片时使用;群 openConversationId、单聊 userId、单聊 openDingTalkId 严格三选一,分别使用 --group、--receiver、--receiver-open-dingtalk-id。--receiver 始终按 userId 通过通讯录关键词搜索做精确匹配,即使值以 D/d 开头也不会猜成 openDingTalkId;已有 openDingTalkId 时必须用显式参数直传。userId 包括在 --dry-run 时也会先解析。只传目标时创建卡片并返回 bizId,供后续 messages-update-card 流式更新;同时传 --content 时会自动串联创建和更新,默认以 flowStatus=3 完成。当前只支持 streaming text,不支持 Card JSON 组件或 action callback。"}, + AgentSummary: "创建流式卡片,可在同一次调用中写入内容并结束;群聊创建时可 @成员或 @所有人", + UseWhen: []string{"当你要发送一张流式文本卡片时使用;群 openConversationId、单聊 userId、单聊 openDingTalkId 严格三选一,分别使用 --group、--receiver、--receiver-open-dingtalk-id。群聊可用 --at-open-dingtalk-ids 或 --at-all 在创建卡片时设置 @对象;同时传 --content 时,Runtime 会把创建响应中的 atTag 自动加在正文前,后续更新不重复传递 @参数。--receiver 始终按 userId 通过通讯录关键词搜索做精确匹配,即使值以 D/d 开头也不会猜成 openDingTalkId;已有 openDingTalkId 时必须用显式参数直传。userId 包括在 --dry-run 时也会先解析。只传目标时创建卡片并返回 bizId,供后续 messages-update-card 流式更新;同时传 --content 时会自动串联创建和更新,默认以 flowStatus=3 完成。当前只支持 streaming text,不支持 Card JSON 组件或 action callback。"}, AvoidWhen: []string{"已有 bizId、只需要追加或更新现有卡片内容时使用 +messages-update-card"}, Examples: []string{ - "dws chat +messages-send-card --group --content \"任务已完成\"", + "dws chat +messages-send-card --group --at-open-dingtalk-ids --content \"任务已完成\"", "dws chat +messages-send-card --receiver ", }, }, Parameters: []contract.ParamDecl{ {Name: "receiver-open-dingtalk-id", Property: "receiverOpenDingTalkId"}, + {Name: "at-open-dingtalk-ids", Property: "atOpenDingTalkIds"}, + {Name: "at-all", Property: "atAll"}, }, }, Flags: []shortcut.Flag{ - {Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId(与两个单聊接收者参数互斥)"}, + {Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId(与两个单聊接收者参数互斥);艾特参数仅支持群聊 --group"}, {Name: "receiver", Type: shortcut.FlagString, Desc: "单聊接收者 userId(与 --group/--receiver-open-dingtalk-id 互斥);始终通过通讯录搜索精确匹配 openDingTalkId,包括 --dry-run 和 D/d 开头的 userId"}, {Name: "receiver-open-dingtalk-id", Type: shortcut.FlagString, Desc: "单聊接收者 openDingTalkId(与 --group/--receiver 互斥);显式直传且不做通讯录解析"}, - {Name: "content", Type: shortcut.FlagString, Desc: "创建后立即写入的卡片内容;省略时仅创建并返回 bizId"}, + {Name: "at-open-dingtalk-ids", Type: shortcut.FlagStringSlice, Desc: "群聊创建卡片时 @ 的 openDingTalkId 列表;仅随 create_and_send_card 发送;艾特参数仅支持群聊 --group"}, + {Name: "at-all", Type: shortcut.FlagBool, Desc: "群聊创建卡片时 @ 所有人;仅随 create_and_send_card 发送;艾特参数仅支持群聊 --group"}, + {Name: "content", Type: shortcut.FlagString, Desc: "创建后立即写入的卡片正文;群聊 @ 时 Runtime 自动前置 create 返回的 atTag;省略时仅创建并返回 bizId"}, {Name: "flow-status", Type: shortcut.FlagInt, Default: "3", Desc: "自动更新状态:1处理中/2输入中/3完成/4执行中/5错误;--flow-status 必须在 1-5 之间,且显式指定时必须同时提供 --content"}, }, Constraints: []shortcut.Constraint{ @@ -1487,10 +1491,15 @@ var MessagesSendCard = shortcut.Shortcut{ Flags: []string{"flow-status"}, Description: "--flow-status 必须在 1-5 之间,且显式指定时必须同时提供 --content", }, + { + Kind: shortcut.ConstraintCustom, + Flags: []string{"group", "at-open-dingtalk-ids", "at-all"}, + Description: "艾特参数仅支持群聊 --group", + }, }, Tips: []string{ `dws chat +messages-send-card --group `, - `dws chat +messages-send-card --group --content "任务已完成"`, + `dws chat +messages-send-card --group --at-open-dingtalk-ids --content "任务已完成"`, }, Validate: func(rt *shortcut.RuntimeContext) error { if status := rt.Int("flow-status"); !validCardFlowStatus(status) { @@ -1499,6 +1508,9 @@ var MessagesSendCard = shortcut.Shortcut{ if rt.Changed("flow-status") && rt.Str("content") == "" { return fmt.Errorf("--flow-status 只有与 --content 一起使用才有意义") } + if rt.Str("group") == "" && (len(uniqueShortcutStrings(rt.StrSlice("at-open-dingtalk-ids"))) > 0 || rt.Bool("at-all")) { + return fmt.Errorf("--at-open-dingtalk-ids 和 --at-all 仅支持群聊 --group") + } return nil }, Execute: func(rt *shortcut.RuntimeContext) error { @@ -1506,9 +1518,18 @@ var MessagesSendCard = shortcut.Shortcut{ receiver := rt.Str("receiver") receiverOpenID := rt.Str("receiver-open-dingtalk-id") params := map[string]any{} + mentionsRequested := false switch { case group != "": params["openConversationId"] = group + if atOpenIDs := uniqueShortcutStrings(rt.StrSlice("at-open-dingtalk-ids")); len(atOpenIDs) > 0 { + params["atOpenDingTalkIds"] = atOpenIDs + mentionsRequested = true + } + if rt.Bool("at-all") { + params["atAll"] = true + mentionsRequested = true + } case receiver != "": openID, err := resolveUserOpenDingTalkID(rt, receiver) if err != nil { @@ -1524,6 +1545,10 @@ var MessagesSendCard = shortcut.Shortcut{ } status := rt.Int("flow-status") if rt.DryRun() { + plannedContent := content + if mentionsRequested { + plannedContent = "" + content + } return rt.Output(map[string]any{ "contractVersion": currentCardWorkflowContract.Version, "dry_run": true, @@ -1540,7 +1565,7 @@ var MessagesSendCard = shortcut.Shortcut{ "tool": "update_streaming_card", "arguments": map[string]any{ "bizId": "", - "msgContent": content, + "msgContent": plannedContent, "flowStatus": status, }, }, @@ -1555,9 +1580,13 @@ var MessagesSendCard = shortcut.Shortcut{ if bizID == "" { return fmt.Errorf("卡片已创建但下层未返回 bizId,无法自动更新;请检查 create_and_send_card 响应") } + atTag := findCardAtTag(created) + if mentionsRequested && atTag == "" { + return fmt.Errorf("卡片已创建(bizId=%s),但下层未返回 atTag,无法保证请求的 @ 生效;未执行自动更新", bizID) + } updated, err := rt.CallMCPWriteData("im", "update_streaming_card", map[string]any{ "bizId": bizID, - "msgContent": content, + "msgContent": atTag + content, "flowStatus": status, }) if err != nil { @@ -1575,12 +1604,18 @@ var MessagesSendCard = shortcut.Shortcut{ } func findCardBizID(value any) string { + return strings.TrimSpace(findCardResponseString(value, []string{"bizId", "bizID", "biz_id"})) +} + +func findCardAtTag(value any) string { + return findCardResponseString(value, []string{"atTag"}) +} + +func findCardResponseString(value any, directKeys []string) string { switch typed := value.(type) { case map[string]any: - directKeys := []string{"bizId", "bizID", "biz_id"} for _, key := range directKeys { if candidate, ok := typed[key].(string); ok && strings.TrimSpace(candidate) != "" { - candidate = strings.TrimSpace(candidate) return candidate } } @@ -1595,7 +1630,7 @@ func findCardBizID(value any) string { } for _, key := range envelopeKeys { visited[key] = struct{}{} - if candidate := findCardBizID(typed[key]); candidate != "" { + if candidate := findCardResponseString(typed[key], directKeys); candidate != "" { return candidate } } @@ -1608,13 +1643,13 @@ func findCardBizID(value any) string { } sort.Strings(remainingKeys) for _, key := range remainingKeys { - if candidate := findCardBizID(typed[key]); candidate != "" { + if candidate := findCardResponseString(typed[key], directKeys); candidate != "" { return candidate } } case []any: for _, child := range typed { - if candidate := findCardBizID(child); candidate != "" { + if candidate := findCardResponseString(child, directKeys); candidate != "" { return candidate } } @@ -1623,7 +1658,7 @@ func findCardBizID(value any) string { if strings.HasPrefix(trimmed, "{") || strings.HasPrefix(trimmed, "[") { var nested any if json.Unmarshal([]byte(trimmed), &nested) == nil { - return findCardBizID(nested) + return findCardResponseString(nested, directKeys) } } } diff --git a/internal/shortcut/chat/gap_fill_test.go b/internal/shortcut/chat/gap_fill_test.go index d2c618b9..a118157a 100644 --- a/internal/shortcut/chat/gap_fill_test.go +++ b/internal/shortcut/chat/gap_fill_test.go @@ -609,7 +609,7 @@ func TestCrossPlatformCoverageMessagesSendTextModesAndExecuteGuard(t *testing.T) func TestCrossPlatformCoverageMessagesSendCardOneCallLifecycle(t *testing.T) { fake := &larkAlignmentCaller{responses: map[string]string{ - "im/create_and_send_card": `{"result":{"card":{"biz_id":"biz-1"}}}`, + "im/create_and_send_card": `{"result":{"card":{"biz_id":"biz-1","atTag":"甲 乙 "}}}`, "im/update_streaming_card": `{"result":{"updated":true}}`, }} helpers.InitDeps(fake) @@ -619,6 +619,8 @@ func TestCrossPlatformCoverageMessagesSendCardOneCallLifecycle(t *testing.T) { root.SetArgs([]string{ "chat", "+messages-send-card", "--group", "cid", + "--at-open-dingtalk-ids", "D-one,D-two,D-one", + "--at-all", "--content", "完成", "--flow-status", "3", "--yes", @@ -630,7 +632,20 @@ func TestCrossPlatformCoverageMessagesSendCardOneCallLifecycle(t *testing.T) { fake.calls[1].tool != "update_streaming_card" { t.Fatalf("card calls = %#v", fake.calls) } - if fake.calls[1].args["bizId"] != "biz-1" || fake.calls[1].args["msgContent"] != "完成" || + if got, want := fake.calls[0].args["atOpenDingTalkIds"], []string{"D-one", "D-two"}; !reflect.DeepEqual(got, want) { + t.Fatalf("card create atOpenDingTalkIds = %#v, want %#v", got, want) + } + if fake.calls[0].args["atAll"] != true { + t.Fatalf("card create atAll = %#v", fake.calls[0].args["atAll"]) + } + if _, exists := fake.calls[1].args["atOpenDingTalkIds"]; exists { + t.Fatalf("card update leaked atOpenDingTalkIds: %#v", fake.calls[1].args) + } + if _, exists := fake.calls[1].args["atAll"]; exists { + t.Fatalf("card update leaked atAll: %#v", fake.calls[1].args) + } + if fake.calls[1].args["bizId"] != "biz-1" || + fake.calls[1].args["msgContent"] != "甲 乙 完成" || fake.calls[1].args["flowStatus"] != 3 { t.Fatalf("card update args = %#v", fake.calls[1].args) } @@ -643,6 +658,49 @@ func TestCrossPlatformCoverageMessagesSendCardOneCallLifecycle(t *testing.T) { } } +func TestCrossPlatformCoverageMessagesSendCardKeepsContentWithoutAtTag(t *testing.T) { + fake := &larkAlignmentCaller{responses: map[string]string{ + "im/create_and_send_card": `{"result":{"bizId":"biz-no-mention"}}`, + "im/update_streaming_card": `{"result":{"updated":true}}`, + }} + helpers.InitDeps(fake) + root := newPlatformCoverageRoot() + root.SetArgs([]string{ + "chat", "+messages-send-card", + "--group", "cid", + "--content", "正文", + "--yes", + }) + if err := root.Execute(); err != nil { + t.Fatal(err) + } + if len(fake.calls) != 2 || fake.calls[1].args["msgContent"] != "正文" { + t.Fatalf("card calls = %#v", fake.calls) + } +} + +func TestCrossPlatformCoverageMessagesSendCardRejectsMissingRequestedAtTag(t *testing.T) { + fake := &larkAlignmentCaller{responses: map[string]string{ + "im/create_and_send_card": `{"result":{"bizId":"biz-missing-at-tag"}}`, + }} + helpers.InitDeps(fake) + root := newPlatformCoverageRoot() + root.SetArgs([]string{ + "chat", "+messages-send-card", + "--group", "cid", + "--at-open-dingtalk-ids", "D-mentioned", + "--content", "正文", + "--yes", + }) + err := root.Execute() + if err == nil || !strings.Contains(err.Error(), "biz-missing-at-tag") || !strings.Contains(err.Error(), "atTag") { + t.Fatalf("error = %v, want recoverable missing-atTag error containing bizId", err) + } + if len(fake.calls) != 1 || fake.calls[0].tool != "create_and_send_card" { + t.Fatalf("card calls = %#v, want create only", fake.calls) + } +} + func TestCrossPlatformCoverageMessagesSendCardResolvesReceiverForLowerTool(t *testing.T) { fake := &larkAlignmentCaller{} helpers.InitDeps(fake) @@ -748,6 +806,50 @@ func TestCrossPlatformCoverageMessagesSendCardDryRunAndFailureBoundaries(t *test } }) + t.Run("dry run mentions only in create", func(t *testing.T) { + fake := &larkAlignmentCaller{} + helpers.InitDeps(fake) + root := newPlatformCoverageRoot() + var output bytes.Buffer + root.SetOut(&output) + root.SetArgs([]string{ + "chat", "+messages-send-card", + "--group", "cid", + "--at-open-dingtalk-ids", "D-mentioned", + "--at-all", + "--content", "处理中", + "--dry-run", + "--yes", + }) + if err := root.Execute(); err != nil { + t.Fatal(err) + } + if len(fake.calls) != 0 { + t.Fatalf("card group dry-run calls = %#v", fake.calls) + } + var payload map[string]any + if err := json.Unmarshal(output.Bytes(), &payload); err != nil { + t.Fatal(err) + } + actions, _ := payload["actions"].([]any) + create, _ := actions[0].(map[string]any) + createArguments, _ := create["arguments"].(map[string]any) + update, _ := actions[1].(map[string]any) + updateArguments, _ := update["arguments"].(map[string]any) + if !reflect.DeepEqual(createArguments["atOpenDingTalkIds"], []any{"D-mentioned"}) || createArguments["atAll"] != true { + t.Fatalf("card create dry-run mentions = %#v", createArguments) + } + if _, exists := updateArguments["atOpenDingTalkIds"]; exists { + t.Fatalf("card update dry-run leaked atOpenDingTalkIds: %#v", updateArguments) + } + if _, exists := updateArguments["atAll"]; exists { + t.Fatalf("card update dry-run leaked atAll: %#v", updateArguments) + } + if updateArguments["msgContent"] != "处理中" { + t.Fatalf("card update dry-run content = %#v", updateArguments["msgContent"]) + } + }) + t.Run("receiver resolution error", func(t *testing.T) { fake := &larkAlignmentCaller{failProductTool: "contact/search_contact_by_key_word"} helpers.InitDeps(fake) @@ -815,6 +917,8 @@ func TestCrossPlatformCoverageMessagesSendCardDryRunAndFailureBoundaries(t *test {"--group", "cid", "--flow-status", "2"}, {"--group", "cid", "--receiver-open-dingtalk-id", "D-direct"}, {"--receiver", "user-id", "--receiver-open-dingtalk-id", "D-direct"}, + {"--receiver", "user-id", "--at-open-dingtalk-ids", "D-mentioned"}, + {"--receiver-open-dingtalk-id", "D-direct", "--at-all"}, } { helpers.InitDeps(&larkAlignmentCaller{}) root := newPlatformCoverageRoot() @@ -858,6 +962,24 @@ func TestCrossPlatformCoverageFindCardBizIDResponseShapes(t *testing.T) { } } +func TestCrossPlatformCoverageFindCardAtTagResponseShapes(t *testing.T) { + for _, tc := range []struct { + value any + want string + }{ + {map[string]any{"atTag": "甲 "}, "甲 "}, + {map[string]any{"result": map[string]any{"atTag": "乙 "}}, "乙 "}, + {`{"result":{"card":{"atTag":"丙 "}}}`, "丙 "}, + {map[string]any{"atTag": " "}, ""}, + {map[string]any{"atTag": 42}, ""}, + {map[string]any{"result": map[string]any{"created": true}}, ""}, + } { + if got := findCardAtTag(tc.value); got != tc.want { + t.Errorf("findCardAtTag(%#v) = %q, want %q", tc.value, got, tc.want) + } + } +} + func TestCrossPlatformCoverageMessageResourceDownloadKeepsNestedMessageContext(t *testing.T) { resetResourceDownloadHooks(t) t.Chdir(t.TempDir()) diff --git a/scripts/policy/check-mono-multi-skill-content.sh b/scripts/policy/check-mono-multi-skill-content.sh index a560b38e..c5f2501a 100755 --- a/scripts/policy/check-mono-multi-skill-content.sh +++ b/scripts/policy/check-mono-multi-skill-content.sh @@ -1,5 +1,5 @@ #!/bin/sh -# Mono↔multi skill content QA (G1–G4). See docs/skill-mono-multi-qa.md +# Mono↔multi skill content QA (G1–G5). See docs/skill-mono-multi-qa.md set -eu ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)" diff --git a/scripts/policy/interface-baseline/contract.go b/scripts/policy/interface-baseline/contract.go index bee33435..a8b010aa 100644 --- a/scripts/policy/interface-baseline/contract.go +++ b/scripts/policy/interface-baseline/contract.go @@ -312,10 +312,13 @@ func checkCompatibility(root *cobra.Command, baseline interfaceContract) []strin continue } if actual.Value.Type() != expectedFlag.Type { - failures = append(failures, fmt.Sprintf( - "%q flag --%s changed type from %s to %s", - displayPath(path), expectedFlag.Name, expectedFlag.Type, actual.Value.Type(), - )) + if !reviewedFlagTypeChange(displayPath(path), expectedFlag.Name, expectedFlag.Type, actual.Value.Type()) || + flagContractOtherwiseChanged(expectedFlag, actual, actualScope) { + failures = append(failures, fmt.Sprintf( + "%q flag --%s changed type from %s to %s", + displayPath(path), expectedFlag.Name, expectedFlag.Type, actual.Value.Type(), + )) + } } if expectedFlag.Shorthand != "" && actual.Shorthand != expectedFlag.Shorthand { failures = append(failures, fmt.Sprintf( @@ -443,11 +446,20 @@ func mergeContracts(historical, current interfaceContract) (interfaceContract, [ continue } if oldFlag.Type != newFlag.Type { - failures = append(failures, fmt.Sprintf( - "%q flag --%s changed type from %s to %s", - displayPath(path), name, oldFlag.Type, newFlag.Type, - )) - continue + if !reviewedFlagTypeChange(displayPath(path), name, oldFlag.Type, newFlag.Type) || + mergedFlagContractOtherwiseChanged(oldFlag, newFlag) { + failures = append(failures, fmt.Sprintf( + "%q flag --%s changed type from %s to %s", + displayPath(path), name, oldFlag.Type, newFlag.Type, + )) + continue + } + // A reviewed migration keeps merging, and the merged entry keeps + // the historical type below, exactly as this function keeps the + // historical shorthand, required and hidden values. The baseline + // records what was promised, so a later --check still resolves + // this flag through the reviewed table rather than silently + // adopting the new type as history. } if oldFlag.Shorthand != "" && oldFlag.Shorthand != newFlag.Shorthand { failures = append(failures, fmt.Sprintf( diff --git a/scripts/policy/interface-baseline/contract_test.go b/scripts/policy/interface-baseline/contract_test.go index 9911bd1a..c2838d5f 100644 --- a/scripts/policy/interface-baseline/contract_test.go +++ b/scripts/policy/interface-baseline/contract_test.go @@ -10,6 +10,7 @@ import ( "strings" "testing" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/app" "github.com/spf13/cobra" ) @@ -239,6 +240,286 @@ func assertFailureContains(t *testing.T, failures []string, want string) { t.Fatalf("failures %v do not contain %q", failures, want) } +// policyRoot builds "dws old" with a single --policy flag declared by declare, +// so a baseline and a candidate can differ only in how that flag is typed. +func policyRoot(declare func(*cobra.Command)) *cobra.Command { + root := &cobra.Command{Use: "dws"} + old := &cobra.Command{Use: "old", Run: func(*cobra.Command, []string) {}} + declare(old) + root.AddCommand(old) + root.InitDefaultHelpCmd() + return root +} + +// registerReviewedFixture puts a fixture command in the reviewed table for the +// duration of one test, so the behaviour tests exercise the real lookup instead +// of depending on whichever production entries happen to exist. +func registerReviewedFixture(t *testing.T, change flagTypeChange) { + t.Helper() + if _, exists := reviewedFlagTypeChanges[change]; exists { + t.Fatalf("fixture %+v collides with a production entry", change) + } + reviewedFlagTypeChanges[change] = struct{}{} + t.Cleanup(func() { delete(reviewedFlagTypeChanges, change) }) +} + +const typeChangedFailure = "changed type from string to int" + +func TestReviewedFlagTypeChangeAcceptsRegisteredMigration(t *testing.T) { + baseline := snapshot(policyRoot(func(command *cobra.Command) { + command.Flags().String("policy", "", "policy") + })) + current := policyRoot(func(command *cobra.Command) { + command.Flags().Int("policy", 0, "policy") + }) + + // Without an entry the migration is still a blocking change. + assertFailureContains(t, checkCompatibility(current, baseline), typeChangedFailure) + + registerReviewedFixture(t, flagTypeChange{CommandPath: "dws old", Flag: "policy", From: "string", To: "int"}) + if failures := checkCompatibility(current, baseline); len(failures) != 0 { + t.Fatalf("a reviewed string->int migration should pass: %v", failures) + } +} + +func TestReviewedFlagTypeChangeIsDirectionSensitive(t *testing.T) { + registerReviewedFixture(t, flagTypeChange{CommandPath: "dws old", Flag: "policy", From: "string", To: "int"}) + + // The reverse migration shares command and flag but not direction, so the + // string->int entry must not admit it. + baseline := snapshot(policyRoot(func(command *cobra.Command) { + command.Flags().Int("policy", 0, "policy") + })) + current := policyRoot(func(command *cobra.Command) { + command.Flags().String("policy", "", "policy") + }) + assertFailureContains(t, checkCompatibility(current, baseline), "changed type from int to string") +} + +func TestReviewedFlagTypeChangeRejectsOtherCommandsAndFlags(t *testing.T) { + registerReviewedFixture(t, flagTypeChange{CommandPath: "dws other", Flag: "policy", From: "string", To: "int"}) + baseline := snapshot(policyRoot(func(command *cobra.Command) { + command.Flags().String("policy", "", "policy") + })) + current := policyRoot(func(command *cobra.Command) { + command.Flags().Int("policy", 0, "policy") + }) + // Same flag and direction, different command. + assertFailureContains(t, checkCompatibility(current, baseline), typeChangedFailure) + + registerReviewedFixture(t, flagTypeChange{CommandPath: "dws old", Flag: "elsewhere", From: "string", To: "int"}) + // Same command and direction, different flag. + assertFailureContains(t, checkCompatibility(current, baseline), typeChangedFailure) +} + +// A reviewed migration is only accepted when the rest of the flag's contract +// held still. Each case bundles one unrelated regression with the reviewed type +// change and requires the type failure to reappear, which also anchors +// flagContractOtherwiseChanged against the checks it mirrors: a condition that +// drifts out of sync fails here instead of silently widening the exemption. +func TestReviewedFlagTypeChangeRejectsBundledRegression(t *testing.T) { + registerReviewedFixture(t, flagTypeChange{CommandPath: "dws old", Flag: "policy", From: "string", To: "int"}) + + tests := []struct { + name string + baseline func(*cobra.Command) + current func(*cobra.Command) + want string + }{ + { + name: "became required", + baseline: func(command *cobra.Command) { command.Flags().String("policy", "", "policy") }, + current: func(command *cobra.Command) { + command.Flags().Int("policy", 0, "policy") + _ = command.MarkFlagRequired("policy") + }, + want: "became required", + }, + { + name: "became hidden", + baseline: func(command *cobra.Command) { command.Flags().String("policy", "", "policy") }, + current: func(command *cobra.Command) { + command.Flags().Int("policy", 0, "policy") + _ = command.Flags().MarkHidden("policy") + }, + want: "became hidden", + }, + { + name: "lost shorthand", + baseline: func(command *cobra.Command) { command.Flags().StringP("policy", "p", "", "policy") }, + current: func(command *cobra.Command) { command.Flags().Int("policy", 0, "policy") }, + want: "lost shorthand", + }, + { + name: "changed no-opt value", + baseline: func(command *cobra.Command) { command.Flags().String("policy", "", "policy") }, + current: func(command *cobra.Command) { + command.Flags().Int("policy", 0, "policy") + command.Flags().Lookup("policy").NoOptDefVal = "4" + }, + want: "changed no-opt value", + }, + { + name: "narrowed persistent scope", + baseline: func(command *cobra.Command) { command.PersistentFlags().String("policy", "", "policy") }, + current: func(command *cobra.Command) { command.Flags().Int("policy", 0, "policy") }, + want: "narrowed persistent scope", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + failures := checkCompatibility(policyRoot(test.current), snapshot(policyRoot(test.baseline))) + assertFailureContains(t, failures, test.want) + assertFailureContains(t, failures, typeChangedFailure) + }) + } +} + +// mergeContracts runs the same admission decision on --merge. A reviewed +// migration keeps merging and the merged entry keeps the historical type, so a +// later --check still resolves the flag through the reviewed table. +func TestMergeAcceptsReviewedFlagTypeChangeAndKeepsHistoricalType(t *testing.T) { + historical := snapshot(policyRoot(func(command *cobra.Command) { + command.Flags().String("policy", "", "policy") + })) + current := snapshot(policyRoot(func(command *cobra.Command) { + command.Flags().Int("policy", 0, "policy") + })) + + if _, failures := mergeContracts(historical, current); len(failures) == 0 { + t.Fatal("an unreviewed merge should report the type change") + } + + registerReviewedFixture(t, flagTypeChange{CommandPath: "dws old", Flag: "policy", From: "string", To: "int"}) + merged, failures := mergeContracts(historical, current) + if len(failures) != 0 { + t.Fatalf("a reviewed merge should pass: %v", failures) + } + if got := merged.Commands["old"].Flags["policy"].Type; got != "string" { + t.Fatalf("merged type = %q, want the historical %q", got, "string") + } +} + +// mergeContracts applies the same "nothing else moved" condition as +// checkCompatibility, but against two recorded contracts rather than a live +// pflag definition. Each case bundles one unrelated regression with the reviewed +// type change and requires the type failure to reappear, anchoring +// mergedFlagContractOtherwiseChanged against the checks it mirrors. +func TestMergeRejectsReviewedFlagTypeChangeWithBundledRegression(t *testing.T) { + registerReviewedFixture(t, flagTypeChange{CommandPath: "dws old", Flag: "policy", From: "string", To: "int"}) + + tests := []struct { + name string + historical func(*cobra.Command) + current func(*cobra.Command) + }{ + { + name: "lost shorthand", + historical: func(command *cobra.Command) { command.Flags().StringP("policy", "p", "", "policy") }, + current: func(command *cobra.Command) { command.Flags().Int("policy", 0, "policy") }, + }, + { + name: "became required", + historical: func(command *cobra.Command) { command.Flags().String("policy", "", "policy") }, + current: func(command *cobra.Command) { + command.Flags().Int("policy", 0, "policy") + _ = command.MarkFlagRequired("policy") + }, + }, + { + name: "became hidden", + historical: func(command *cobra.Command) { command.Flags().String("policy", "", "policy") }, + current: func(command *cobra.Command) { + command.Flags().Int("policy", 0, "policy") + _ = command.Flags().MarkHidden("policy") + }, + }, + { + name: "changed no-opt value", + historical: func(command *cobra.Command) { command.Flags().String("policy", "", "policy") }, + current: func(command *cobra.Command) { + command.Flags().Int("policy", 0, "policy") + command.Flags().Lookup("policy").NoOptDefVal = "4" + }, + }, + { + name: "narrowed persistent scope", + historical: func(command *cobra.Command) { command.PersistentFlags().String("policy", "", "policy") }, + current: func(command *cobra.Command) { command.Flags().Int("policy", 0, "policy") }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + _, failures := mergeContracts( + snapshot(policyRoot(test.historical)), + snapshot(policyRoot(test.current)), + ) + assertFailureContains(t, failures, typeChangedFailure) + }) + } +} + +// The behaviour tests above register their own fixtures, so they cannot catch a +// production entry whose key is spelled in the wrong form — that mistake +// disables the exemption silently and only a real gate run reports it. Recompute +// every key through displayPath instead of trusting the spelling in the table. +func TestReviewedFlagTypeChangeKeysUseDisplayPaths(t *testing.T) { + if len(reviewedFlagTypeChanges) == 0 { + t.Fatal("豁免表为空:若确已清空,请连这条守卫一起删除") + } + for change := range reviewedFlagTypeChanges { + if got := displayPath(internalCommandPath(change.CommandPath)); got != change.CommandPath { + t.Errorf("命令路径不是 displayPath 的规范形态\n 登记: %s\n 规范: %s", change.CommandPath, got) + } + if change.Flag == "" || strings.HasPrefix(change.Flag, "-") { + t.Errorf("%s: flag 名应是裸名(lookupFlag 用裸名查找),登记为 %q", change.CommandPath, change.Flag) + } + if change.From == "" || change.To == "" || change.From == change.To { + t.Errorf("%s --%s: %q -> %q 不构成类型迁移", change.CommandPath, change.Flag, change.From, change.To) + } + } +} + +// Anchor every production entry to the real command tree. A misspelled command +// or flag name would leave the exemption permanently inert, and the behaviour +// tests cannot see that because they register their own paths. +func TestReviewedFlagTypeChangeEntriesResolveInTheRealCommandTree(t *testing.T) { + if len(reviewedFlagTypeChanges) == 0 { + t.Fatal("豁免表为空:若确已清空,请连这条守卫一起删除") + } + root := app.NewRootCommand() + for change := range reviewedFlagTypeChanges { + command, resolved := resolveCommand(root, internalCommandPath(change.CommandPath)) + if !resolved { + t.Errorf("%q 在真实命令树中不存在", change.CommandPath) + continue + } + flag, _ := lookupFlag(command, change.Flag) + if flag == nil { + t.Errorf("%q 上不存在 --%s", change.CommandPath, change.Flag) + continue + } + // Exactly one side holds depending on whether the migration has landed: + // From before it does, To afterwards. Asserting membership catches a + // misspelled type name without failing while the migration is pending. + if actual := flag.Value.Type(); actual != change.From && actual != change.To { + t.Errorf("%q --%s 当前类型 %q 既不是 %q 也不是 %q", + change.CommandPath, change.Flag, actual, change.From, change.To) + } + } +} + +// internalCommandPath inverts displayPath so a table key can be fed back into +// resolveCommand and displayPath. +func internalCommandPath(displayed string) string { + if displayed == "dws" { + return "root" + } + return strings.ReplaceAll(strings.TrimPrefix(displayed, "dws "), " ", ".") +} + func testRoot() *cobra.Command { root := &cobra.Command{Use: "dws"} old := &cobra.Command{Use: "old"} diff --git a/scripts/policy/interface-baseline/reviewed.go b/scripts/policy/interface-baseline/reviewed.go new file mode 100644 index 00000000..cb05c225 --- /dev/null +++ b/scripts/policy/interface-baseline/reviewed.go @@ -0,0 +1,140 @@ +// Copyright 2026 Alibaba Group +// Licensed under the Apache License, Version 2.0 (the "License"); + +package main + +import "github.com/spf13/pflag" + +// flagTypeChange identifies one flag type migration exactly: which command, +// which flag, and in which direction. Nothing is matched by wildcard — an entry +// that differs in any of the four fields does not apply. +// +// CommandPath is the display form produced by displayPath ("dws minutes +// permission apply"), not the dot-separated internal path. Writing the internal +// form here would silently disable the exemption instead of failing loudly, so +// TestReviewedFlagTypeChangeKeysUseDisplayPaths recomputes every key through +// displayPath rather than trusting the spelling below. +// +// This table is duplicated in internal/interfacesnapshot/reviewed.go and the two +// must stay identical; the interfacesnapshot package has a test that fails if +// they drift. The duplication is forced rather than chosen: +// check-authoritative-interface-baselines.sh copies this whole directory into a +// worktree checked out at a historical revision and builds it there, so nothing +// here may import a package this branch adds — that build would fail with an +// unresolved import instead of reporting a compatibility result. +type flagTypeChange struct { + CommandPath string + Flag string + From string + To string +} + +// reviewedFlagTypeChanges enumerates the individually reviewed flag type +// migrations this gate accepts. A flag type is part of the published CLI +// contract, and a type swap cannot be proven safe from the type names alone — +// whether old invocations still parse depends on the concrete value domain the +// command enforces. So a migration is accepted only when this exact command, +// flag, and old→new pair appear here. Adding an entry is a contract decision +// and belongs in review, not in a feature change. +// +// Entries are direction-sensitive by construction: "string" → "int" is a +// separate key from "int" → "string" and only the reviewed direction is +// accepted. The reverse is not a candidate for this table — widening an int +// flag back to string lets values the parser used to reject reach RunE, which +// is a genuine loosening of validation rather than a type migration. +// +// The table alone does not admit a change: reviewedFlagTypeChange is consulted +// only when nothing else about the flag moved. See the callers in +// checkCompatibility and mergeContracts. +var reviewedFlagTypeChanges = map[flagTypeChange]struct{}{ + // "dws minutes permission apply --policy" moved from a String flag parsed + // with strconv.ParseInt(v, 10, 64) in RunE to a native Int flag, keeping the + // same [2,4] domain check in RunE. + // + // This is admissible because the historical set of *successful* invocations + // is a subset of the new one, not merely similar to it: + // + // - Old: the value had to parse as base-10 and land in [2,4]. That is + // exactly "2", "3", "4" plus sign/leading-zero spellings of them + // ("+3", "03", "003", ...). + // - New: pflag's intValue.Set parses with strconv.ParseInt(s, 0, 64) — + // base 0 — and RunE still enforces [2,4]. Every base-10 spelling above + // resolves to the same number under base 0, so no historical success + // becomes a failure. + // - Base 0 additionally accepts "0x3" and friends, which the old parser + // rejected. That widens the accepted set; it cannot break a caller who + // was already succeeding. + // - Values outside [2,4] and non-numeric values still fail. Only the + // failure *message and timing* move, from RunE to flag parsing. + // + // The flag's default value changes from "" to "0" as an unavoidable + // consequence of the type. Neither gate compares defaults, and a default is + // not reachable by a caller here because RunE requires the flag to be + // explicitly set, so this is not a contract change. + {CommandPath: "dws minutes permission apply", Flag: "policy", From: "string", To: "int"}: {}, +} + +// reviewedFlagTypeChange reports whether this exact command, flag and direction +// is a reviewed migration. Callers must additionally establish that the rest of +// the flag's contract is unchanged before skipping the failure. +func reviewedFlagTypeChange(commandPath, flag, from, to string) bool { + _, reviewed := reviewedFlagTypeChanges[flagTypeChange{ + CommandPath: commandPath, + Flag: flag, + From: from, + To: to, + }] + return reviewed +} + +// flagContractOtherwiseChanged reports whether anything checkCompatibility +// enforces about a flag, other than its type, regressed against the baseline. A +// reviewed type migration is only accepted when this returns false, so the +// exemption cannot carry an unrelated regression in behind it. +// +// Every condition here mirrors one of the checks that follow the type check in +// checkCompatibility and must stay in step with them. The pairing is anchored by +// tests that bundle each regression with the reviewed type change and require +// the type failure to reappear, so a condition that drifts out of sync fails +// loudly instead of silently widening the exemption. +func flagContractOtherwiseChanged(expected flagContract, actual *pflag.Flag, actualScope string) bool { + if expected.Shorthand != "" && actual.Shorthand != expected.Shorthand { + return true + } + if expected.RequiredSet && !expected.Required && isRequiredFlag(actual) { + return true + } + if expected.HiddenSet && !expected.Hidden && actual.Hidden { + return true + } + if expected.NoOptSet && actual.NoOptDefVal != expected.NoOpt { + return true + } + if expected.ScopeSet && expected.Scope == "persistent" && actualScope == "local" { + return true + } + return false +} + +// mergedFlagContractOtherwiseChanged is the mergeContracts counterpart of +// flagContractOtherwiseChanged, comparing two recorded contracts rather than a +// contract against a live pflag definition. It mirrors the checks that follow +// the type check in mergeContracts. +func mergedFlagContractOtherwiseChanged(oldFlag, newFlag flagContract) bool { + if oldFlag.Shorthand != "" && oldFlag.Shorthand != newFlag.Shorthand { + return true + } + if oldFlag.RequiredSet && !oldFlag.Required && newFlag.Required { + return true + } + if oldFlag.HiddenSet && !oldFlag.Hidden && newFlag.Hidden { + return true + } + if oldFlag.NoOptSet && newFlag.NoOptSet && oldFlag.NoOpt != newFlag.NoOpt { + return true + } + if oldFlag.ScopeSet && oldFlag.Scope == "persistent" && newFlag.Scope == "local" { + return true + } + return false +} diff --git a/skills/content-qa/mono-multi-coverage.yaml b/skills/content-qa/mono-multi-coverage.yaml index c7a6da11..96b8b55c 100644 --- a/skills/content-qa/mono-multi-coverage.yaml +++ b/skills/content-qa/mono-multi-coverage.yaml @@ -142,12 +142,27 @@ omit_global: disposition: covered_by reason: "AI 表格字段规则已下沉到 dingtalk-aitable;G3/coverage 不强制全局同名" -# Byte-identical pairs (content surfaces that must stay in sync). +# Reviewed file pairs. Most are byte-identical; layout-only link differences +# must be declared as data-driven substitutions. paired_files: + - mono: references/products/sheet.md + multi: dingtalk-misc/references/sheet.md + mode: link-normalized + link_substitutions: + - mono: "../url-patterns.md" + multi: "../../dingtalk-shared/references/url-patterns.md" + - mono: "../intent-guide.md" + multi: "sheet-intent-guide.md" - mono: references/products/whiteboard/recipes.md multi: dingtalk-misc/references/whiteboard/recipes.md mode: identical +# Directory pairs whose relative file sets and contents must stay byte-identical. +paired_trees: + - mono: references/products/sheet + multi: dingtalk-misc/references/sheet + mode: identical + # Scripts under skills/multi/*/scripts not referenced by any .md in that skill tree. # yida/finance/aiapp are referenced via dingtalk-misc/references/unsupported-scripts.md orphan_scripts_allowlist: diff --git a/skills/mono/references/intent-guide.md b/skills/mono/references/intent-guide.md index 9b25b102..c8b48011 100644 --- a/skills/mono/references/intent-guide.md +++ b/skills/mono/references/intent-guide.md @@ -46,6 +46,7 @@ | "查看这条话题的所有回复" | 话题回复 | `chat +thread-replies` | `chat message list-topic-replies` | 已知 threadId/topicId 时自动投影回复人、正文和时间 | | "把这些消息里的附件下载下来" | 消息资源下载 | 查询 Shortcut 的 `--download-resources` | `resourceRefs.shortcut` 返回的下载命令 | 先用查询结果的 resourceRefs;单资源再执行返回的可运行参数 | | "消息发没发成功/查询消息发送状态" | 查询消息发送状态 | `chat message query-send-status` | — | 需要 send 返回的 openTaskId | +| "编辑/撤回我刚发的消息" | 发后编辑/撤回 | `send` → `query-send-status` → `edit` / `recall` | 按消息内容反查 | 使用 send 返回的 openTaskId 直接获取两个后续 ID | | "撤回我发的消息/撤回消息" | 撤回个人消息 | `chat message recall` | `chat message recall-by-bot` | recall 撤回个人消息,recall-by-bot 撤回机器人消息 | | "未读消息会话/未读会话列表/我的未读会话" | 未读会话列表 | `chat message list-unread-conversations` | `chat message read-status` | list-unread-conversations 查哪些会话有未读;read-status 查具体消息的已读状态 | | "谁看了这条消息/消息已读未读/查读状态" | 查询消息已读状态 | `chat message read-status` | `chat message list-unread-conversations` | read-status 查具体消息的已读人员;list-unread-conversations 查未读会话列表 | @@ -312,6 +313,7 @@ dws chat +messages-send --as user --open-dingtalk-id --msg-type 用 `chat message query-send-status` 的场景: - "消息发没发成功/查询消息发送状态" — 查询个人发送消息的状态,需要 send 返回的 openTaskId +- 发送成功时返回 openMessageId 和 openConversationId,可直接用于 edit/recall 用 `chat +search-msg` 的场景(推荐首选): - "按发送者搜索消息/指定多个群搜索/@我的消息多维度搜" — 支持关键词、发送者、@我、@指定人、多个会话等维度组合搜索 @@ -325,7 +327,7 @@ dws chat +messages-send --as user --open-dingtalk-id --msg-type 上述五个查询 Shortcut 与 `+messages-resource-download` 都沿用安全本地下载的 `read/not_required` 契约,不应添加 `--yes` 或触发交互确认。输出只允许工作目录内相对路径、默认不覆盖并原子落盘;需要覆盖时必须由用户显式传 `--overwrite`。 **不支持的场景**: -- "撤回我刚发的消息"(但不知道消息 ID) — 需先通过消息拉取或搜索接口(如 `chat message list`、`chat message search-advanced` 等)获取 openMessageId,再调用 `chat message recall` +- "撤回历史消息"(且已丢失发送返回的 openTaskId) — 通过消息拉取或搜索接口获取 openMessageId,再调用 `chat message recall` 判断关键:普通发送统一走 `+messages-send` 并显式选择 `--as`;个人撤回→ `recall`;机器人撤回→ `recall-by-bot`;查发送状态→ `query-send-status`;消息搜索类意图优先路由到 `+search-msg`,仅在不适用时降级到原子命令。 diff --git a/skills/mono/references/products/chat.md b/skills/mono/references/products/chat.md index f3421650..25daa8a0 100644 --- a/skills/mono/references/products/chat.md +++ b/skills/mono/references/products/chat.md @@ -653,6 +653,8 @@ dws chat message send --open-dingtalk-id --msg-type file --file #### 查询消息发送状态 — 查询以当前用户身份发送的消息的发送状态 查询以当前用户身份发送的消息的发送状态。需要传入发送消息时返回的 openTaskId。 + +发送成功时返回 openMessageId 和 openConversationId,可直接用于后续编辑或撤回。 ``` Usage: dws chat message query-send-status [flags] @@ -666,6 +668,23 @@ Flags: - openTaskId 由 `dws chat message send` 发送消息成功后返回 - 用于确认消息是否已成功发送或获取发送失败的原因 - 返回结果中含发送成功消息的 openMessageId,可用于后续 recall(撤回)、read-status(查已读)等命令 + - 返回结果同时含 openConversationId;与 openMessageId 组合后可直接用于 edit(编辑)或 recall(撤回) +``` + +发送后编辑/撤回时,优先使用下列 ID 链,无需按消息内容反查: + +```bash +# 1. 发送后保留 openTaskId +dws chat message send --group --text "原始内容" +# 2. 查询得到 openMessageId 和 openConversationId +dws chat message query-send-status --open-task-id +# 3. 编辑消息 +dws chat message edit --conversation-id --msg-id --text "更新后的内容" + +# 发送后撤回使用同一 ID 链 +dws chat message send --group --text "待撤回的内容" +dws chat message query-send-status --open-task-id +dws chat message recall --conversation-id --msg-id ``` #### 撤回消息 — 撤回当前用户自己发出的消息 @@ -676,15 +695,13 @@ Usage: dws chat message recall [flags] Example: dws chat message recall --conversation-id --msg-id - # 查询会话 ID: dws chat search --query "群名" - # 消息 ID 可通过 dws chat message list 获取 Flags: --conversation-id string 会话 openConversationId (必填,支持单聊/群聊,别名: --group / --id / --chat) --msg-id string 消息 openMessageId (必填) 注意: - --conversation-id 的别名: --group, --id, --chat (均可替代 --conversation-id) - - 消息 ID 可通过 `dws chat message list` 命令获取 + - 刚由 `chat message send` 发出的消息,使用 `query-send-status` 返回的 openConversationId 和 openMessageId;只有历史消息或已丢失 openTaskId 时才通过消息拉取/搜索获取 ID - 仅支持撤回当前用户以个人身份发出的消息,不能撤回他人发送的消息,也不能撤回机器人发出的消息 - 与 `recall-by-bot` 的区别:本命令通过 IM 接口撤回用户自己发出的消息(需要 openConversationId + openMessageId),`recall-by-bot` 通过机器人接口撤回机器人发出的消息(需要 robot-code + processQueryKey) ``` @@ -2373,7 +2390,8 @@ Flags: | `chat bot find` | `botOpenDingTalkId` | 给机器人发单聊消息(send --open-dingtalk-id;字段名是 botOpenDingTalkId,非 openDingTalkId) | | `chat message send-by-bot` | `processQueryKey` | recall-by-bot 的 --keys | | `chat message send` | `openTaskId` | query-send-status 的 --open-task-id | -| `chat message list` | `openMessageId` | recall 的 --msg-id | +| `chat message query-send-status` | `openMessageId` + `openConversationId` | 刚发送消息的 edit / recall | +| `chat message list` | `openMessageId` | 历史消息或已丢失 openTaskId 时的 recall | | `chat message search` | `nextCursor` | 下次 message search 的 --cursor | | `chat message search-advanced` | `nextCursor` | 下次 message search-advanced 的 --cursor | | `chat search-common` | `openConversationId` | message send/list 等的 --group | diff --git a/skills/mono/references/products/sheet.md b/skills/mono/references/products/sheet.md index 40ccbe53..4276456d 100644 --- a/skills/mono/references/products/sheet.md +++ b/skills/mono/references/products/sheet.md @@ -1,2160 +1,264 @@ # 电子表格 (sheet) 命令参考 -## 适用范围(重要) +> **渐进式文档**:本文件为路由层(索引 + 意图判断 + 全局约束),各命令的详细参数、示例和注意事项在 [sheet/](./sheet/) 目录下按需加载。 -`sheet` 的单元格与工作表操作仅支持钉钉在线电子表格(`contentType=ALIDOC`、`extension=axls`)。`sheet import create` 是 Agent 可发现的文件入口:它接收本地 `xlsx` / `xls`,转换并新建在线电子表格;不能把已上传的 xlsx 节点直接当作 axls 操作。 +## 适用范围 + +`sheet` 仅支持钉钉在线电子表格(`contentType=ALIDOC`、`extension=axls`)。 | 文件类型 | 处理方式 | |---------|---------| -| 在线电子表格(`axls`) | 走 `sheet` 全部命令(读/写/筛选/合并/导出等服务端原子操作) | -| 本地路径上的 `xlsx` / `xls` | 用 `dws sheet import create --file <路径> --folder-token ` 或 `--workspace ` 转换为新的在线电子表格 | -| 钉盘/文档中已有的 `xlsx` / `xls` / `xlsm` / `csv` 节点 | 不能直接调用工作表/单元格命令;先用 `dws doc download --node --output <路径>` 下载。若用户要转为在线表格,再对下载文件执行 `sheet import` | -| 想把在线表格导出为 xlsx | 用 `dws sheet export` ——输入是 `axls`,输出是 xlsx(axls → xlsx 的格式转换) | - -> 用户直接粘贴原始 `alidocs` URL 时必须先 probe:先执行 `dws doc info --node --format json`,按 [链接规范](../url-patterns.md#alidocs-url-类型探测流程) 校验 `contentType` 和 `extension`: -> - 仅当 `contentType=ALIDOC` 且 `extension=axls` 时,才继续走 `sheet` -> - 如果是 `xlsx` / `xls` / `xlsm` / `csv`,立即转向 `dws doc download`,并告知用户"这是本地表格文件,已为你下载到本地处理" - -## URL 识别与 NODE_ID 提取 - -当用户输入包含钉钉文档 URL 时,必须先识别并提取 NODE_ID,再判断意图。 - -硬性规则:对用户直接给出的原始 `alidocs` URL,不允许直接走 `sheet` 命令,必须先执行: - -```bash -dws doc info --node "" --format json -``` - -根据返回路由: -- `contentType=ALIDOC` + `extension=axls` → 继续走 `sheet` -- `contentType≠ALIDOC` + `extension=xlsx` / `xls` / `xlsm` / `csv` → 转向 `dws doc download --node --output <路径>`,禁止调用任何 sheet 子命令 -- 其他类型 → 按 [链接规范](../url-patterns.md#alidocs-url-类型探测流程) 路由 - -补充:如果这是用户直接提供的原始 `alidocs` URL,先按 [链接规范](../url-patterns.md#alidocs-url-类型探测流程) probe 一次确认真实类型,再判断是否继续走 `sheet`。 - -### 支持的 URL 格式 - -| 格式 | 示例 | NODE_ID 提取方式 | -|------|------|----------------| -| `alidocs.dingtalk.com/i/nodes/{id}` | `https://alidocs.dingtalk.com/i/nodes/9E05BDRVQePjzLkZt2p2vE7kV63zgkYA` | 取 URL 路径最后一段 | -| `alidocs.dingtalk.com/i/nodes/{id}?queryParams` | `https://alidocs.dingtalk.com/i/nodes/abc123?doc_type=wiki_doc` | 忽略 query 参数,取路径最后一段 | -| `alidocs.dingtalk.com/spreadsheetv2/{key}/...` | `https://alidocs.dingtalk.com/spreadsheetv2/vKJngl50tJN1v3a3/...?dentryKey=vKJngl50tJN1v3a3&type=s` | **不要提取 path segment**,将完整 URL 直接传给 `--node` 参数,由 MCP 服务端解析 | - -### 提取规则 - -1. 匹配 URL 中 `alidocs.dingtalk.com` 域名 -2. **判断 URL 路径格式**: - - 路径包含 `/i/nodes/` → 取 URL path 的最后一段作为 NODE_ID(去掉 query string 和 fragment) - - 路径包含 `/spreadsheetv2/` → **不要提取 path segment**,必须将完整 URL 原样传给 `--node` 参数(因为 path 中的短 ID 不是合法的 nodeId,MCP 服务端会自行解析完整 URL) -3. 对于 `/i/nodes/` 格式,提取出的 NODE_ID 可直接用于所有 `--node` 参数,也可将完整 URL 传给 `--node`(CLI 会自动解析) -4. 对用户直接提供的原始 `alidocs` URL,先按 [链接规范](../url-patterns.md#alidocs-url-类型探测流程) probe;只有 probe 确认 `contentType=ALIDOC` 且 `extension=axls` 时,才继续留在 `sheet`;如果 `extension=xlsx` / `xls` / `xlsm` / `csv`,必须转向 `dws doc download`,不能走任何 sheet 命令 - -## 查询命令帮助 - -当你不确定某个命令的具体参数、格式或可选项时,**优先执行 `--help` 查询**,不要猜测参数名或凭记忆编造。 - -```bash -# 查看 sheet 下所有子命令 -dws sheet --help - -# 查看具体命令的完整参数说明 -dws sheet range update --help -dws sheet filter-view create --help -dws sheet insert-dimension --help - -# 查看子命令组下的所有命令 -dws sheet range --help -dws sheet filter-view --help -``` - -规则: -- 参数名不确定时 → 先 `--help`,再调用 -- 报错 "unknown flag" 时 → `--help` 确认正确的 flag 名称 -- 不确定某个功能是否存在时 → `dws sheet --help` 查看命令列表 - -## 命令速查表 - -| 命令 | 用途 | -|------|------| -| `sheet create` | 创建钉钉表格文档 | -| `sheet list` | 获取全部工作表列表 | -| `sheet info` | 获取指定工作表详情 | -| `sheet new` | 新建工作表 | -| `sheet update` | 更新工作表属性(标题/位置/隐藏/冻结) | -| `sheet copy` | 复制工作表 | -| `sheet show-gridline` | 显示工作表网格线 | -| `sheet hide-gridline` | 隐藏工作表网格线 | -| `sheet range read` | 读取工作表数据(别名: range get) | -| `sheet range update` | 更新指定区域内容(值/公式/超链接) | -| `sheet range clear` | 清除区域(值/格式/全部) | -| `sheet range sort` | 对区域排序 | -| `sheet range fill` | 自动填充区域 | -| `sheet range copy-to` | 复制区域到目标位置 | -| `sheet range move-to` | 移动区域到目标位置 | -| `sheet range batch-clear` | 批量清除多个区域(原子事务) | -| `sheet batch-update` | 批量执行多个写操作(原子事务) | -| `sheet csv-get` | 以 CSV 格式读取区域数据 | -| `sheet table-get` | 读取结构化 table 数据(别名: table-read) | -| `sheet range set-style` | 设置单元格样式 | -| `sheet range batch-set-style` | 按配置文件批量设置样式 | -| `sheet find` | 搜索单元格内容 | -| `sheet append` | 在末尾追加数据行 | -| `sheet csv-put` | 将 CSV 数据写入指定位置(支持公式,自动扩容) | -| `sheet table-put` | 写入一个或多个结构化 table(别名: table-write) | -| `sheet pivot-table list` | 列出透视表或获取指定透视表详情 | -| `sheet pivot-table create` | 创建原生透视表 | -| `sheet pivot-table update` | 更新透视表配置 | -| `sheet pivot-table delete` | 删除透视表(不可逆,删除前必须确认) | -| `sheet delete-sheet` | 删除工作表(不可逆,删除前必须确认) | -| `sheet replace` | 全局查找替换文本 | -| `sheet merge-cells` | 合并单元格 | -| `sheet unmerge-cells` | 取消合并单元格 | -| `sheet set-dropdown` | 设置下拉列表 | -| `sheet get-dropdown` | 获取下拉列表配置 | -| `sheet delete-dropdown` | 删除下拉列表 | -| `sheet insert-dimension` | 在指定位置插入行或列 | -| `sheet delete-dimension` | 删除指定位置的行或列 | -| `sheet update-dimension` | 更新行/列属性(显隐/行高/列宽) | -| `sheet move-dimension` | 移动行或列到指定位置 | -| `sheet add-dimension` | 在末尾追加空行或空列 | -| `sheet group-dimension` | 创建行/列分组 | -| `sheet ungroup-dimension` | 取消行/列分组 | -| `sheet media-upload` | 上传附件到表格 | -| `sheet write-image` | 上传图片并写入单元格 | -| `sheet create-float-image` | 创建浮动图片 | -| `sheet get-float-image` | 获取浮动图片详情 | -| `sheet list-float-images` | 列出工作表所有浮动图片 | -| `sheet update-float-image` | 更新浮动图片属性 | -| `sheet delete-float-image` | 删除浮动图片 | -| `sheet import create` | 导入本地 xlsx/xls 为新的在线电子表格(`sheet import` 保留兼容) | -| `sheet export` | 导出表格为 xlsx | -| `sheet filter get` | 获取全局筛选信息 | -| `sheet filter create` | 创建全局筛选 | -| `sheet filter delete` | 删除全局筛选 | -| `sheet filter update` | 批量更新筛选条件 | -| `sheet filter clear-criteria` | 清除单列筛选条件 | -| `sheet filter sort` | 筛选排序 | -| `sheet filter-view list` | 获取所有筛选视图 | -| `sheet filter-view create` | 创建筛选视图 | -| `sheet filter-view update` | 更新筛选视图属性 | -| `sheet filter-view delete` | 删除筛选视图 | -| `sheet filter-view update-criteria` | 更新筛选视图列条件 | -| `sheet filter-view delete-criteria` | 删除筛选视图列条件 | -| `sheet filter-view info` | 获取单个筛选视图详情 | -| `sheet filter-view list-criteria` | 列出筛选视图所有列条件 | -| `sheet filter-view get-criteria` | 获取单列筛选条件详情 | -| `sheet cond-format list` | 获取条件格式规则 | -| `sheet cond-format create` | 创建条件格式规则 | -| `sheet cond-format update` | 更新条件格式规则 | -| `sheet cond-format delete` | 删除条件格式规则 | -| `sheet chart list` | 获取浮动图表 | -| `sheet chart create` | 创建浮动图表 | -| `sheet chart update` | 更新浮动图表 | -| `sheet chart delete` | 删除浮动图表 | -| `sheet template list` | 获取表格模板列表 | -| `sheet template search` | 搜索表格模板 | -| `sheet template apply` | 应用模板创建新表格文档 | - -> 不确定参数?对任意命令执行 `dws sheet <命令> --help` 查看完整用法。 - -## 子文档索引(更多命令详细参考) - -本文档覆盖高频命令。以下命令的完整参数、示例与工作流在 `sheet/` 子目录,需要时按主题查阅: - -| 主题 | 子文档 | 覆盖命令 | -|------|--------|---------| -| 表格与工作表管理 | [sheet/sheet-workbook.md](./sheet/sheet-workbook.md) | create / list / info / new / update / copy / show-gridline / hide-gridline / delete-sheet | -| 透视表 | [sheet/sheet-pivot-table.md](./sheet/sheet-pivot-table.md) | pivot-table list / create / update / delete | -| 写入数据 | [sheet/sheet-write-data.md](./sheet/sheet-write-data.md) | table-put / range update(对象协议详解)/ append / csv-put | -| 读取数据 | [sheet/sheet-read-data.md](./sheet/sheet-read-data.md) | table-get / range read / csv-get | -| 区域操作 | [sheet/sheet-range-operations.md](./sheet/sheet-range-operations.md) | range clear / sort / fill / copy-to / move-to | -| 批量操作 | [sheet/sheet-batch-operations.md](./sheet/sheet-batch-operations.md) | range batch-clear / batch-update | -| 行列操作 | [sheet/sheet-dimension-operations.md](./sheet/sheet-dimension-operations.md) | insert / delete / update / move / add-dimension / group-dimension / ungroup-dimension | -| 样式与合并 | [sheet/sheet-style-format.md](./sheet/sheet-style-format.md) | range set-style / batch-set-style / merge-cells / unmerge-cells | -| 条件格式 | [sheet/sheet-conditional-format.md](./sheet/sheet-conditional-format.md) | cond-format list / create / update / delete | -| 浮动图表 | [sheet/sheet-chart.md](./sheet/sheet-chart.md) | chart list / create / update / delete | -| 下拉列表 | [sheet/sheet-dropdown.md](./sheet/sheet-dropdown.md) | set / get / delete-dropdown | -| 媒体与图片 | [sheet/sheet-media-image.md](./sheet/sheet-media-image.md) | media-upload / write-image / 浮动图片 | -| 查找替换 | [sheet/sheet-search-replace.md](./sheet/sheet-search-replace.md) | find / replace | -| 全局筛选 | [sheet/sheet-filter.md](./sheet/sheet-filter.md) | filter get / create / delete / update / clear-criteria / sort | -| 筛选视图 | [sheet/sheet-filter-view.md](./sheet/sheet-filter-view.md) | filter-view 全系列 | -| 导入 | [sheet/sheet-import.md](./sheet/sheet-import.md) | import / import get | -| 导出 | [sheet/sheet-export.md](./sheet/sheet-export.md) | export | - -> `template list/search/apply` 无独立子文档,直接执行 `dws sheet template <子命令> --help` 查看用法。 - -## 意图判断 - -### 表格与工作表管理 - -用户说"创建表格/新建电子表格": -- 创建表格文档 → `create` - -用户说"看工作表/有哪些工作表/表格结构": -- 列出工作表 → `list` -- 工作表详情 → `info` - -用户说"加工作表/新增Sheet": -- 新建工作表 → `new` - -用户说"修改工作表名称/重命名工作表/移动工作表位置/隐藏工作表/显示工作表/冻结行/冻结列/取消冻结/更新工作表属性": -- 更新工作表属性 → `update` -- 重命名工作表 → `update --title "新名称"` -- 移动工作表位置 → `update --index N` -- 隐藏工作表 → `update --hidden` -- 显示工作表 → `update --hidden=false` -- 冻结行列 → `update --frozen-row-count N --frozen-column-count M` -- 取消冻结 → `update --frozen-row-count 0 --frozen-column-count 0` - -用户说"复制工作表/拷贝工作表/克隆工作表/工作表副本": -- 复制工作表 → `copy` -- 复制并指定名称 → `copy --title "副本名称"` -- 复制并指定位置 → `copy --index N` - -### 数据读写 - -用户说"读数据/看表格内容": -- 读取数据 → `range read` - -用户说"写数据/填表/更新单元格/写入公式": -- 更新数据 → `range update` -- 【强制】`--sheet-id` 必填:即使是单工作表也不能省略,不要参照 `range read` 的默认行为;未知时先执行 `dws sheet list --node --format json` 获取 `sheetId`,禁止凭空臆测为 `Sheet1`、`sheet1`、`0`、`default` 等 -- 注意:如果用户的目的是替换文本、移动行列或追加空行空列,请勿使用 `range update`,必须使用对应的专用命令(`replace`/`move-dimension`/`add-dimension`) -- **批量 CSV 值/公式写入优先用 `csv-put`**:当数据可表达为 CSV(可含 `=` 开头的公式)、不需要超链接或富格式且数据量较大(超过 5 行或超过 20 个单元格)时,必须优先使用 `csv-put`。它无需手动构造二维 JSON 数组并支持自动扩容;要写以 `=` 开头的字面文本时在字段值前加单引号 - -用户说"追加数据/添加行/在末尾加数据/新增记录": -- 追加数据 → `append` - -用户说"批量写入CSV/导入CSV/CSV写入表格/把CSV贴到表格里": -- 写入 CSV → `csv-put` -- 与 `range update` 的区别:`csv-put` 接受 CSV 文本直接写入,无需手动构造二维 JSON 数组;适合大批量值或公式写入,但不支持超链接和富格式对象 -- 与 `append` 的区别:`csv-put` 写入指定位置(--start-cell),`append` 在末尾追加 - -用户说"搜索/查找/找单元格/搜内容/精确搜索/精确匹配/完全匹配/全字匹配": -- 搜索单元格 → `find` -- 精确匹配(只匹配完全等于的,不匹配包含的) → `find --match-entire-cell` -- 正则搜索 → `find --use-regexp` -- 搜索公式 → `find --match-formula` -- 不要用 `range read` 读取全量数据后在客户端过滤来替代 `find`,必须使用 `find` 命令的服务端搜索能力 - -用户说"替换/查找替换/全局替换/批量替换/把A替换成B/把所有的X改成Y": -- 查找替换 → `replace` -- 精确匹配后替换(只替换内容完全等于的单元格) → `replace --match-entire-cell` -- 正则替换 → `replace --use-regexp` -- 删除匹配内容 → `replace --replacement ""` -- 请勿用 `find` + `range update`、`range read` + `range update` 等组合来模拟替换,`replace` 是服务端原子操作,效率更高且返回替换计数 - -### 行列操作 - -用户说"插入行/插入列/在某行前插入/在某列前插入": -- 插入行或列 → `insert-dimension` -- 在末尾追加 → `append`(insert-dimension 不支持末尾追加) - -用户说"删除行/删除列/删掉第几行/删掉某列/移除行/移除列": -- 删除行或列 → `delete-dimension` -- 仅清空内容但保留行/列 → `range clear`(整片区域清除,比逐格写空更简洁) - -用户说"隐藏行/隐藏列/显示行/显示列/设置行高/设置列宽/调整行高/调整列宽/行列属性": -- 隐藏/显示行或列 → `update-dimension --hidden` / `--hidden=false` -- 设置行高/列宽 → `update-dimension --pixel-size` -- 同时修改尺寸与显隐 → `update-dimension --pixel-size --hidden` - -用户说"移动行/移动列/调整行顺序/调整列顺序/行列拖拽/把第N行移到第M行": -- 移动行或列 → `move-dimension` -- 请勿用 `range read` + `range update` 读取再重写来模拟移动,`move-dimension` 是原子操作,能保留格式和合并状态 - -用户说"追加空行/追加空列/增加行数/增加列数/扩展表格/在末尾加空行": -- 追加空行/空列 → `add-dimension` -- 注意与 `append`(追加数据行)区分:`add-dimension` 追加的是空行/空列,`append` 追加的是带数据的行 -- 请勿用 `range update` 写空数据来模拟追加,`add-dimension` 直接扩展表格维度 - -用户说"创建行分组/创建列分组/折叠几行/折叠几列/取消行列分组": -- 创建分组 → `group-dimension --range <整行或整列范围>` -- 取消分组 → `ungroup-dimension --range <整行或整列范围>` -- `--range` 只能传整行或整列范围,如 `3:7`、`C:F`、`Sheet1!3:7`,不要传 `A1:C5` - -### 单元格格式 - -用户说"设置样式/改颜色/设背景色/加粗/居中/换行/字体颜色/字号": -- 设置单元格样式 → `range set-style` -- 批量设置不同 range 的样式 → `range batch-set-style --batch ./styles.json`(内部顺序循环调 `update_range`) -- 请勿用 `range update --values` 写空/重写来模拟样式变更;也请勿把样式变更混在 `range update` 里、再故意清空 `--values` - -用户说"设置数字格式/改成百分比/用人民币显示/按日期显示/文本格式/保留几位小数": -- 设置数字格式 → `range set-style --number-format <格式代码>`(如 `0%` / `¥#,##0.00` / `yyyy/m/d` / `@`) -- 请勿用 `range update` 传递数字格式,`range update` 仅负责写入值与超链接;数字格式属于单元格样式,统一走 `range set-style` - -用户说"合并单元格/合并/合并区域/按行合并/按列合并": -- 合并所有单元格 → `merge-cells`(默认 mergeAll) -- 按行合并 → `merge-cells --merge-type mergeRows` -- 按列合并 → `merge-cells --merge-type mergeColumns` - -用户说"取消合并/拆分单元格/还原合并": -- 取消合并单元格 → `unmerge-cells` - -### 下拉列表 - -用户说"设置下拉列表/下拉选项/下拉菜单/添加下拉/配置下拉": -- 设置下拉列表 → `set-dropdown` -- 设置多选下拉 → `set-dropdown --multi-select` - -用户说"查看下拉列表/获取下拉配置/下拉列表有哪些选项": -- 获取下拉列表配置 → `get-dropdown` - -用户说"删除下拉列表/移除下拉/取消下拉/清除下拉": -- 删除下拉列表 → `delete-dropdown` - -### 媒体上传 - -用户说"上传附件/传文件到表格/上传文件到表格/上传到表格": -- 上传附件 → `media-upload`(需表格 ID 或 URL + 本地文件路径) -- 用户指定了上传后的名称 → `media-upload --name "自定义名称"` -- `media-upload` 的 `--name` 参数用于指定附件在表格中显示的名称(不改变本地文件名);不传时默认使用本地文件名 - -用户说"写入图片/插入图片/加图片/放图片到单元格/嵌入图片到表格": -- 写入图片 → `write-image`(需表格 ID + 工作表 ID + 单元格范围 + 本地图片路径) -- 禁止使用 `range update` 写入图片,因为 `update_range` 的 MCP 工具不支持图片类型参数,调用必定失败。必须使用 `write-image` 命令 -- 用户指定了图片尺寸 → `write-image --width N --height M` - -### 浮动图片 - -用户说"浮动图片/悬浮图片/在表格上放一张图/加个浮动的图": -- 创建浮动图片 → 先 `media-upload` 上传图片获取 `resourceUrl`,再 `create-float-image` -- 浮动图片悬浮于单元格之上,不占用单元格内容,与 `write-image`(写入单元格内部的图片)不同 - -用户说"查看浮动图片/有哪些浮动图片/浮动图片列表": -- 列出所有浮动图片 → `list-float-images` -- 查看某个浮动图片详情 → `get-float-image` - -用户说"移动浮动图片/调整浮动图片大小/修改浮动图片/更新浮动图片": -- 更新浮动图片属性 → `update-float-image`(可更新锚点位置、尺寸、偏移量、图片资源路径) - -用户说"删除浮动图片/移除浮动图片": -- 删除浮动图片 → `delete-float-image` - -关键区分:`write-image`(单元格内嵌图片,占据单元格内容)vs `create-float-image`(浮动图片,悬浮于单元格之上,不占内容) - -### 筛选视图 - -用户说"筛选/过滤/只看某些值/只显示满足条件的行/筛选数据/创建筛选/删除筛选/设置筛选条件/清除筛选/排序": -- 查看当前筛选 → `filter get` -- 创建筛选 → `filter create` -- 删除筛选 → `filter delete` -- 批量设置多列条件 → `filter update` -- 清除某一列条件 → `filter clear-criteria` -- 按列排序 → `filter sort` -- **区分全局筛选与筛选视图**:如果用户说"筛选视图"则走 `filter-view` 系列;如果只说"筛选/过滤/只看"则默认走全局 `filter` 系列 -- **禁止替代方案**:当用户要求"筛选/只看/仅保留某些行"时,必须通过 `filter create` / `filter update` 创建真实的筛选器。禁止用"删除不符合条件的行"或"新建工作表只放符合条件的行"来代替——这些做法会让原数据丢失或不可恢复 - -用户说"筛选视图/查看筛选视图/有哪些筛选视图/筛选视图列表": -- 获取所有筛选视图 → `filter-view list` - -用户说"筛选视图详情/查看某个筛选视图/筛选视图信息/筛选视图配置": -- 获取单个筛选视图详情 → `filter-view info` - -用户说"创建筛选视图/新建筛选视图/添加筛选视图": -- 创建筛选视图 → `filter-view create` - -用户说"更新筛选视图/修改筛选视图/改筛选视图名称/改筛选视图范围": -- 更新筛选视图属性 → `filter-view update` - -用户说"删除筛选视图/移除筛选视图": -- 删除筛选视图 → `filter-view delete` - -用户说"设置筛选条件/添加筛选条件/配置筛选视图条件/按值筛选/按条件筛选/按颜色筛选": -- 设置筛选视图列条件 → `filter-view update-criteria` - -用户说"查看筛选条件/有哪些筛选条件/筛选视图设了什么条件/列出筛选条件": -- 列出所有列条件 → `filter-view list-criteria` -- 查看某一列的条件 → `filter-view get-criteria --column N` - -用户说"清除筛选条件/移除筛选条件/取消筛选条件": -- 清除筛选视图列条件 → `filter-view delete-criteria` -- 注意与 `filter-view delete`(删除整个筛选视图)区分:`delete-criteria` 仅清除指定列的条件,不删除筛选视图本身 - -### 导入 - -用户说"导入Excel/把xlsx转为在线表格/上传本地表格并在线编辑": -- 使用 `sheet import`,本地文件仅支持 `xlsx` / `xls` -- `--folder-token` 与 `--workspace` 至少提供一个;不指定 `--name` 时使用文件名 -- CLI 内部完成上传、转换和轮询,Agent 不要自行拆分流程或重复提交 -- 若返回 `timed_out:true`,执行返回的 `next_command` 续查,不要重新执行 import -- 详见 [sheet/sheet-import.md](./sheet/sheet-import.md) - -### 导出 - -用户说"导出/下载xlsx/存为Excel/存成表格文件/把表格变成xlsx/导出表格/下载表格/导出为 excel": -- 导出表格 → `export`(单命令一站式,内部自动完成提交、轮询、可选下载) -- 仅需传 `--node`,可选 `--output` 指定本地文件/目录(不传则返回 downloadUrl) -- 需要落盘到本地 → `dws sheet export --node --output `,命令自动下载 xlsx -- 禁止用 `range read` 全量读取后自行拼接 xlsx 来模拟导出,必须使用 `export` 命令(服务端原子导出,保留格式/合并/公式等属性) -- 禁止在 AI Agent 侧实现轮询或重试,CLI 内部已按渐进式退避策略完成(最多 30 次约 5 分钟) - -### URL 粘贴场景 +| 在线电子表格(`axls`) | 走 `sheet` 全部命令 | +| `xlsx` / `xls` / `xlsm` / `csv` | `dws drive download --node --output <路径>` 下载到本地处理,禁止调用任何 `sheet` 子命令 | +| 本地 xlsx/xls 导入为在线表格 | `dws sheet import create --file <路径>`(单命令一站式:创建会话→上传→确认→轮询,产出在线电子表格;`sheet import` 保留兼容) | +| 在线表格导出为 xlsx | `dws sheet export`(axls → xlsx 格式转换) | + +用户贴原始 `alidocs` URL 时必须先 probe:`dws drive info --node --format json`,按 [链接规范](../url-patterns.md#alidocs-url-类型探测流程) 校验: +- `extension=axls` → 继续走 `sheet` +- `extension=xlsx` / `xls` / `xlsm` / `csv` → 转 `dws drive download`,告知用户"这是本地表格文件,已为你下载到本地处理" + +## URL → NODE_ID + +| URL 格式 | 提取方式 | +|----------|---------| +| `.../i/nodes/{id}` 或 `.../i/nodes/{id}?query` | 取路径末段作 NODE_ID(忽略 query) | +| `.../spreadsheetv2/{key}/...` | **完整 URL 原样传 `--node`**,禁止提取 path segment | + +参数不确定时先 `dws sheet <命令> --help`。 + +## Agent 编辑总则 + +1. **先定位再编辑**:未知 `sheet-id` 时必须先 `dws sheet list --node --format json`,禁止猜 `Sheet1` / `sheet1` / `0` / `default`。用户只给工作表名称时,也先用 `list` 确认真实名称或 ID。 +2. **先读结构再动结构**:合并、冻结、行列分组、行高列宽、隐藏行列、最后非空边界都属于工作表结构信息,先读 `dws sheet info --node --sheet-id --format json`;分组回读加 `--include groups`,行高、列宽和隐藏行列按需加 `--include row_heights,col_widths,hidden_rows,hidden_cols`。 +3. **按目的选择读取方式**:快速看值和大表分批用 `csv-get`;需要 `columns` / `data` / `dtypes` / `formats` 用 `table-get`;需要公式、样式、数据验证、超链接、富文本等 per-cell 元数据用 `range read`。 +4. **写返回不等于完成**:任何写操作完成后都要用独立读命令回读确认。值写入用 `csv-get` / `range read` / `table-get`,结构变更用 `sheet info`,对象类操作用对应 `list` / `get`。 +5. **大批量 CSV 值/公式不要拼大 JSON**:超过 5 行或 20 个单元格、且不需要富格式对象时优先 `csv-put`;字段值以 `=` 开头按公式解析,前加单引号写入以 `=` 开头的字面文本;需要 table/dataframe 语义时用 `table-put`。 +6. **公式写入后分层校验**:写公式前读 [sheet-formula](./sheet/sheet-formula.md)。写后先用 `range read --value-render-option formula` 确认公式文本,再用 `formula-verify` 聚合扫描计算错误;需要确认业务数值时再用 `raw_value` 抽样对账。 +7. **专用操作用专用命令**:搜索用 `find`、替换用 `replace`、清空用 `range clear`、排序用 `range sort`、复制/移动区域用 `range copy-to` / `range move-to`,不要用 `range read` + `range update` 客户端模拟。 +8. **大整数按文本写**:超过 `9007199254740991` 的整数、长数字 ID、订单号、手机号等需要逐位精确的值,不要按 JSON number 或 `int64` / `uint64` 写入;用字符串值 + `object` dtype + 文本格式 `@`。 + +## 场景速查 + +| 用户要做 | 正确命令 | 动手前读 | 禁止写法 | +|---------|---------|----------|----------| +| 快速查看数据 / 大表分批读取 | `csv-get` | [sheet-read-data](sheet/sheet-read-data.md) | 不传 `--range` 全量 `range read` 大表 | +| 按表格结构读写列名、类型、格式 | `table-get` / `table-put` | [sheet-read-data](sheet/sheet-read-data.md)、[sheet-write-data](sheet/sheet-write-data.md) | 把 table spec 塞进 `batch-update` | +| 少量精确写入、公式对象、超链接、富文本、数据验证 | `range update` | [sheet-write-data](sheet/sheet-write-data.md)、[sheet-formula](sheet/sheet-formula.md) | 用 `append` 写公式,或用 `csv-put` 写富格式 | +| 校验公式错误 / 全表公式检查 | `formula-verify` | [sheet-formula](sheet/sheet-formula.md) | 全表 `range read` 后由 Agent 自行枚举错误值 | +| 批量 CSV 值/公式写入 / CSV 粘贴 | `csv-put` | [sheet-write-data](sheet/sheet-write-data.md)、[sheet-formula](sheet/sheet-formula.md) | 为大块 CSV 数据手写巨大 `--values` JSON | +| 追加记录到末尾 | `append` | [sheet-write-data](sheet/sheet-write-data.md) | 手算最后一行后 `range update` | +| 查找 / 替换 | `find` / `replace` | [sheet-search-replace](sheet/sheet-search-replace.md) | 读全表后本地过滤或手写替换 | +| 清空 / 排序 / 填充 / 复制移动区域 | `range clear` / `range sort` / `range fill` / `range copy-to` / `range move-to` | [sheet-range-operations](sheet/sheet-range-operations.md) | 用读写组合模拟服务端原子操作 | +| 合并、冻结、分组、行高列宽 | `sheet info` + 结构命令 | [sheet-workbook](sheet/sheet-workbook.md)、[sheet-dimension-operations](sheet/sheet-dimension-operations.md) | 从 `range read` / CSV 空值推断结构 | +| 多个原子写操作组合 | `batch-update` | [sheet-batch-operations](sheet/sheet-batch-operations.md) | 多次独立调用导致半成品 | +| 图片写入单元格 / 浮动图片 | `write-image` / `media-upload` + `create-float-image` | [sheet-media-image](sheet/sheet-media-image.md) | 用 `range update` 写图片 | +| 条件高亮 / 标红 / 数据条 / 色阶 | `cond-format` | [sheet-conditional-format](sheet/sheet-conditional-format.md) | 用静态 `set-style` 冒充条件格式 | +| 单元格评论 / 批注 / @人讨论 | `comment list/create/reply/update/delete` | [sheet-comment](sheet/sheet-comment.md) | 把评论内容写进单元格值 | + +## 触发必读 + +- 涉及公式、计算列、辅助列、占比、增长率、查找计算、公式校验、公式错误扫描:先读 [sheet-formula](./sheet/sheet-formula.md)。 +- 涉及 `range update --values`、超链接、富文本、数据验证、少量样式随值写入:先读 [sheet-write-data](./sheet/sheet-write-data.md)。 +- 涉及公式文本 / 原始值 / 格式化值回读、per-cell 元数据、大表分页:先读 [sheet-read-data](./sheet/sheet-read-data.md)。 +- 涉及合并、冻结、工作表增删改、最后非空边界:先读 [sheet-workbook](./sheet/sheet-workbook.md)。 +- 涉及行列插删、隐藏、尺寸、移动、分组:先读 [sheet-dimension-operations](./sheet/sheet-dimension-operations.md)。 +- 涉及多个写操作一次提交:先读 [sheet-batch-operations](./sheet/sheet-batch-operations.md)。 + +## Reference 索引 + +| Reference | 描述 | +|-----------|------| +| [sheet-workbook](sheet/sheet-workbook.md) | 管理表格文档与工作表。当用户说"创建表格"、"有哪些工作表"、"新建/重命名/隐藏/冻结/复制/删除工作表"、"显示/隐藏网格线"时使用;`info` 可回读冻结行列,`--include groups` 回读分组,其他行列结构按需使用对应 `--include`。命令:`create`/`list`/`info`/`new`/`update`/`copy`/`delete-sheet`/`show-gridline`/`hide-gridline` | +| [sheet-read-data](sheet/sheet-read-data.md) | 读取工作表数据。当用户说"读数据"、"看表格内容"、"查看数据"时使用。推荐 `csv-get`(CSV 格式、token 低、防爆保护);需按 table/dataframe 结构读取列名、data、dtypes、formats 时用 `table-get`;需 value + dataValidation / hyperlink / richText / cellStyles 等 per-cell 元数据时用 `range read`。大范围数据建议分页读取(单次 ≤5000 单元格)。命令:`csv-get`/`table-get`/`range read` | +| [sheet-write-data](sheet/sheet-write-data.md) | 写入数据到工作表。当用户说"写数据"、"填表"、"更新单元格"、"写公式"、"超链接"、"写值同时设样式/数据验证"、"追加数据"、"导入CSV"、"写入结构化 table"时使用。大批量 CSV 值/公式(>5行或>20单元格,且无需富格式)必须用 `csv-put` 而非 `range update`;结构化 table/dataframe 输入用 `table-put`。命令:`range update`/`append`/`csv-put`/`table-put` | +| [sheet-formula](sheet/sheet-formula.md) | 公式写入、文本回读、错误聚合与结果抽样。当用户说"写公式"、"计算列"、"辅助列"、"总计/占比/增长率/查找计算"、"校验公式"、"检查公式错误"时使用。命令:`range update` / `csv-put` / `range read --value-render-option formula/raw_value` / `formula-verify` | +| [sheet-search-replace](sheet/sheet-search-replace.md) | 搜索和替换文本。当用户说"搜索"、"查找"、"替换"、"把A改成B"时使用。禁止用 `range read` 全量读取后客户端过滤代替 `find`,禁止用 `range update` 模拟 `replace`。命令:`find`/`replace` | +| [sheet-range-operations](sheet/sheet-range-operations.md) | 区域结构性操作。当用户说"清空"、"排序"、"自动填充"、"复制区域到"、"移动数据到"时使用。均为服务端原子操作,禁止 `range read`+`range update` 组合模拟。排序前必须先读前几行判断表头。命令:`range clear`/`range sort`/`range fill`/`range copy-to`/`range move-to` | +| [sheet-batch-operations](sheet/sheet-batch-operations.md) | 批量操作。当用户说"批量清除多个区域"、"组合多个写操作"、"先清除再写入"、"插行列再写数据"、"批量创建/取消分组"时使用。`batch-update` 只支持已列明的原子写操作;`table-put` / `table-get` 不放进 batch,结构化 table 请用独立 `table-put`。命令:`range batch-clear`/`batch-update` | +| [sheet-dimension-operations](sheet/sheet-dimension-operations.md) | 行列增删移动、属性设置与分组。当用户说"插入行/列"、"删除行/列"、"隐藏/显示行列"、"设行高/列宽"、"移动行/列"、"追加空行/空列"、"创建/取消行列分组"、"新建分组并设为折叠/展开"时使用。命令:`insert-dimension`/`delete-dimension`/`update-dimension`/`move-dimension`/`add-dimension`/`group-dimension`/`ungroup-dimension` | +| [sheet-style-format](sheet/sheet-style-format.md) | 单元格样式与合并。当用户说"设样式"、"改颜色/字体/对齐"、"数字格式(百分比/货币/日期)"、"合并/取消合并"时使用。纯样式/批量样式走 `set-style`;写值同时设置少量 cell 样式可用 `range update` 的 `cellStyles`。命令:`range set-style`/`range batch-set-style`/`merge-cells`/`unmerge-cells` | +| [sheet-dropdown](sheet/sheet-dropdown.md) | 下拉列表管理。当用户说"设置下拉"、"下拉选项"、"删除下拉"时使用。命令:`set-dropdown`/`get-dropdown`/`delete-dropdown` | +| [sheet-media-image](sheet/sheet-media-image.md) | 附件上传与图片。当用户说"上传附件"、"写入图片到单元格"、"浮动图片"时使用。单元格图片用 `write-image`(禁止 `range update`);浮动图片需先 `media-upload` 再 `create-float-image`。命令:`media-upload`/`write-image`/`create-float-image`/`get-float-image`/`list-float-images`/`update-float-image`/`delete-float-image` | +| [sheet-filter](sheet/sheet-filter.md) | 全局筛选。当用户说"筛选"、"过滤"、"只看某些行"(未说"筛选视图")时使用。禁止用"删除不符合条件的行"代替筛选。命令:`filter get`/`create`/`delete`/`update`/`clear-criteria`/`sort` | +| [sheet-filter-view](sheet/sheet-filter-view.md) | 筛选视图(个人化,不影响协作者)。当用户明确说"筛选视图"时使用,与全局筛选相互独立。命令:`filter-view list`/`create`/`update`/`delete`/`info`/`update-criteria`/`delete-criteria`/`list-criteria`/`get-criteria` | +| [sheet-conditional-format](sheet/sheet-conditional-format.md) | 条件格式规则。触发词:标红/标黄/高亮/突出/标记/数据条/色阶/颜色随数据变 → **强制**走条件格式,禁止 `range set-style` 静态样式替代。命令:`cond-format list`/`create`/`update`/`delete` | +| [sheet-export](sheet/sheet-export.md) | 导出表格为 xlsx。当用户说“导出”、“下载xlsx”、“存为Excel”时使用。单命令一站式,CLI 内部自动轮询,禁止 Agent 侧重试。命令:`export` | +| [sheet-import](sheet/sheet-import.md) | 导入本地表格文件为在线电子表格。当用户说“导入表格”、“把xlsx传成在线表格”、“上传Excel变在线表格”时使用。Agent 入口为 `import create`,`import` 保留兼容;超时后只用 `import get` 续查,禁止重新创建或用 `drive upload` 代替。命令:`import create`/`import get` | +| [sheet-chart](sheet/sheet-chart.md) | 浮动图表管理。当用户说“画图/数据可视化/趋势图/对比图/占比图/柱形图/折线图/饼图”时使用。禁止用本地脚本生成静态图片替代。命令:`chart list`/`create`/`update`/`delete` | +| [sheet-pivot-table](sheet/sheet-pivot-table.md) | 透视表管理。当用户说“透视表/分组汇总/交叉分析/按X统计Y”时使用。禁止用公式拼汇总表替代。命令:`pivot-table list`/`create`/`update`/`delete` | +| [sheet-comment](sheet/sheet-comment.md) | 单元格评论管理。当用户说“给单元格加评论/批注”、“看某格的评论”、“回复/更新/删除评论”、“@某人讨论这个数据”时使用。评论锚定单元格(`--sheet-id`+`--range`),禁止把评论写进单元格值。命令:`comment list`/`create`/`reply`/`update`/`delete` | +| sheet-template | 表格模板管理。当用户说“用模板创建表格”、“搜索模板”、“模板列表”时使用。命令:`template list`/`template search`/`template apply` | +| [sheet-version](./sheet/sheet-version.md) | 表格历史版本管理。当用户说“保存版本/存个快照/看历史版本/版本列表/回滚到某个版本/恢复到之前的表格”时使用。回滚为危险操作,默认二次确认;版本号从 `version list` 获取。命令:`version save`/`version list`/`version revert` | + +## 全局硬约束 + +1. **`--sheet-id` 禁止臆测**:未知时必须 `dws sheet list --node --format json` 查询,禁止编造 `Sheet1`/`sheet1`/`0`/`default` +2. **合并单元格是结构信息**:`dws sheet info --node --sheet-id --format json` 返回 `mergedRanges`(如 `["C7:D11"]`);不要在 `range read` / `csv-get` 里寻找合并信息 +3. **冻结行列是工作表元数据**:`sheet info` 顶层返回 `frozenRowCount` / `frozenColumnCount`,分别表示从顶部第 1 行、左侧第 A 列开始冻结的数量;`0` 表示未冻结。不要在 `range read` / `csv-get` 里寻找冻结信息 +4. **结构变更后按需 include 回读**:创建/取消分组后,用 `dws sheet info --node --sheet-id --include groups --format json` 回读 `rowGroups` / `columnGroups`;行高、列宽、隐藏行列分别使用 `--include row_heights` / `col_widths` / `hidden_rows` / `hidden_cols`。裸 `sheet info` 不承诺返回这些可选结构字段。 +5. **最后非空坐标只用 A1 语义**:`sheet info` 通过 `nonEmptyRange` 返回 A1/UI 边界;优先使用 `nonEmptyRange.range`,需要追加行/列时使用 `nonEmptyRange.lastRow` / `nonEmptyRange.lastColumn`。不要使用旧的 0-based 字段 `lastNonEmptyRow` / `lastNonEmptyColumn` +6. **`range update` 维度校验**:`--values` 行列数必须与 `--range` 完全一致;只接 `--values` 一个数据参数,cell `type` 仅支持 `text` / `richText`;整格超链接通过 cell-level `hyperlink` 表达,富文本片段链接才使用 `richText.texts[].type="link"` +7. **dataValidation 三语义**:不传 `dataValidation` 字段=保留原 DV;`dataValidation:{type:"none"}`=显式清除;`dataValidation:{type:"dropdown"/"checkbox",...}`=覆盖。`{}` 跳过亦保留原 DV +8. **hyperlink 三语义**:不传 `hyperlink` 字段=保留原整格超链接;`hyperlink:{type:"none"}`=显式清除;`hyperlink:{type:"path"/"sheet"/"range",link,...}`=覆盖。Agent 调用不要用 `hyperlink:null`,避免网关/Schema 过滤 null 字段 +9. **样式写法**:cell-level 样式用 `cellStyles` 或 `range set-style`;richText 片段级样式才用子项 `style`。不要在 `type:"text"` 顶层使用旧 `style` 字段 +10. **用专用命令不用组合模拟**:搜索→`find`、替换→`replace`、清空→`range clear`、排序→`range sort`、填充→`range fill`、复制区域→`range copy-to`、移动区域→`range move-to`、移动行列→`move-dimension` +11. **大批量 CSV 值/公式用 `csv-put`**(>5 行或 >20 单元格,且无需富格式),不用 `range update`;需要 dataframe/table 语义(列名、dtypes、formats、跨 sheet specs)时用 `table-get` / `table-put` +12. **单元格图片用 `write-image`**(`range update` 不支持图片参数) +13. **`export` / `import create` 禁止自行轮询**(CLI 内部已完成渐进式退避,最多 30 次约 5 分钟);导入超时只按返回的 `next_command` 调用 `import get`,导出失败或超时直接转述,不自动重调 +14. **单次调用上限**:`range update` / `set-style` 行数 ≤ 1000,单元格总数建议 ≤ 5000(硬限 30000) +15. **大整数精度保护**:超过 `9007199254740991` 的整数和长数字标识符必须按文本写入;`table-put` 中不要使用 JSON number 或 `int64` / `uint64` dtype,改用字符串值 + `object` dtype + `formats`/`cellStyles` 的 `@` +16. **批量写操作推荐用 `batch-update`**:对多个区域重复调用同一写入工具时,推荐合并为单次原子请求(详见 [sheet-batch-operations](./sheet/sheet-batch-operations.md));逐个调用非原子,中途失败会留下半成品。结构化 table 例外,`table-put` 不进 `batch-update` +17. **关键区分**:sheet(电子表格/单元格读写)vs aitable(AI多维表/结构化记录)vs doc(文档) + +## 当前能力边界 + +- **公式**:支持通过 `range update` 写入、通过 `range read --value-render-option formula` 回读公式文本、通过 `formula-verify` 按错误类型聚合扫描、通过 `raw_value` / `formatted_value` 回读计算结果。只有 `formula-verify` 返回 `status=success`、`hasMore=false`、`totalErrors=0` 时,才能声称本次目标范围未发现公式错误;这仍不等价于业务数值一定正确。 +- **视觉规范**:当前不维护独立的表格视觉方案文档;样式、条件格式、图表分别按对应子文档执行。 +- **结构化 table**:`table-get` / `table-put` 是 table/dataframe 语义入口,不嵌入 `batch-update`;需要原子组合时只组合已支持的单元格/结构写操作。 +- **历史版本**:支持通过 `version save`/`list`/`revert` 手动保存快照、查看版本列表、回滚到指定版本(底层复用 doc 域版本能力);回滚为危险操作,需二次确认。 +- **单元格评论**:支持通过 `comment create`/`list`/`reply`/`update`/`delete` 管理锚定在 `--sheet-id` + `--range` 的评论;评论与单元格值相互独立,详见 [sheet-comment](sheet/sheet-comment.md)。 +- **未暴露或未确认能力**:迷你图、历史 changeset 等能力未在本入口承诺;只有出现稳定 DWS 命令和可回读语义后再补充。 + +## URL 粘贴场景 用户直接粘贴表格 URL(无其他指令): -- 先 probe:`dws doc info --node --format json` 校验 `contentType` 和 `extension` -- `extension=axls` → `list`(列出工作表)+ `range read`(读取第一个工作表数据) -- `extension=xlsx` / `xls` / `xlsm` / `csv` → 转 `dws doc download --node --output ./`,告知用户"这是本地表格文件,已为你下载到本地",然后基于本地文件继续后续处理 +- 先 probe:`dws drive info --node --format json` +- `extension=axls` → `list` + `range read`(读取第一个工作表数据) +- `extension=xlsx`/`xls`/`xlsm`/`csv` → 转 `dws drive download --node --output ./` 用户粘贴 URL + 附加指令: -- 已 probe 为 `axls` 时: - - "帮我看看这个表格有什么数据" → `range read` - - "这个表格有哪些工作表" → `list` - - "往这个表格写入数据" → `range update` - - "帮我找一下表格里的XXX" → `find` -- probe 为 xlsx/xls/xlsm/csv 时:无论用户说"读数据/查看/分析",先走 `dws doc download` 下载到本地,由用户或后续步骤对本地 xlsx 进行解析,严禁调用 `sheet list` / `range read` 等命令 +- probe 为 `axls` → 按 Reference 索引路由到对应命令 +- probe 为 xlsx/csv → 先 `dws drive download` 下载到本地,严禁调用 sheet 命令 -关键区分: sheet(电子表格/单元格读写) vs aitable(AI多维表/结构化记录) vs doc(文档编辑/阅读) - -## 关键注意事项 - -以下是最容易出错的规则,**必须严格遵守**: - -- ★ **`--sheet-id` 获取规范(强制)**:`sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等) -- ★ **`range update` 单元格协议(强制)**:`--values` 是二维 JSON 数组,**每个单元格必须是 object**(如 `{"type":"text","text":"张三"}`),不再支持裸值 `"张三"` / `90` / `null`。裸值会报错「不支持原始值……每个单元格必须是 object」。数字/布尔也写成字符串 object(如 `{"type":"text","text":"90"}`),服务端自动识别类型。超链接写在单元格 object 的 `hyperlink` 字段,**没有 `--hyperlinks` flag**(传了报 unknown flag) -- ★ **`range update` 维度校验(强制)**:`--values` 的行列数必须与 `--range` 完全一致。例如 `--range "A1:C3"` → `--values` 必须是 3×3 的 object 数组 -- ★ **`range update` 清空规范(强制)**:清空单个单元格用 `{"type":"text","text":""}`;清空整片区域用 `range clear`。跳过某格保留原值用 `{}` 空对象 -- ★ **单次调用上限(强制)**:`range update` / `set-style` 行数 ≤ 1000,单元格总数建议 ≤ 5000(硬限 30000) -- ★ **大批量 CSV 值/公式写入用 `csv-put` 不用 `range update`**:当数据可表达为 CSV(可含 `=` 开头的公式)、不需要超链接或富格式且数据量较大时(>5 行或 >20 单元格),必须使用 `csv-put`。需要超链接、富格式对象、`{}` 跳过,或仅更新少量单元格时使用 `range update` -- ★ **搜索用 `find` 不用 `range read`**:`find` 是服务端搜索,禁止用 `range read` 全量读取后客户端过滤 -- ★ **替换用 `replace` 不用 `range update`**:`replace` 是服务端原子操作,返回替换计数 -- ★ **移动用 `move-dimension` 不用 `range update`**:原子操作,保留格式和合并状态 -- ★ **单元格图片用 `write-image` 不用 `range update`**:`update_range` MCP 不支持图片参数,调用必失败 -- ★ **浮动图片用 `create-float-image` 不用 `write-image`**:两者用途不同——`write-image` 写入单元格内部,`create-float-image` 创建悬浮于单元格之上的浮动图片;`--src` 必须来自 `media-upload` 的 `resourceUrl` -- ★ **`export` 禁止自行轮询/重试**:CLI 内部已完成渐进式退避轮询(最多 30 次约 5 分钟),失败时直接告知用户稍后再试 -- ★ **关键区分**:sheet(在线电子表格/单元格读写) vs aitable(AI多维表/结构化记录/字段定义) vs doc(文档编辑/阅读) - -> 完整注意事项请参见本文档末尾「注意事项(完整版)」章节。 - -## 命令详细参考 - -> 以下为各命令的完整 Usage、Flags 和示例。参数不确定时也可直接执行 `dws sheet <命令> --help` 在线查看。 - -### 创建钉钉表格文档 -``` -Usage: - dws sheet create [flags] -Example: - dws sheet create --name "销售数据" - dws sheet create --name "Q1 数据" --folder - dws sheet create --name "知识库表格" --workspace -Flags: - --name string 表格名称 (必填) - --folder string 目标文件夹 ID (dentryUuid 格式) 或 URL;禁止传入纯数字 dentryId - --workspace string 目标知识库 ID -``` - -> **ID 格式约束**:`--folder` 只接受 UUID 格式的 `fileId`(如 `ZgpG2NdyVXYOR2D5UGDok65MJMwvDqPk`)或 alidocs 文件夹 URL。`drive list` 返回中有 `dentryId`(纯数字,如 `218595998810`)和 `fileId`(UUID 格式)两个字段,**必须使用 `fileId`,禁止使用 `dentryId`**,传入纯数字会导致命令失败。 - -### 获取全部工作表列表 -``` -Usage: - dws sheet list [flags] -Example: - dws sheet list --node - dws sheet list --node "https://alidocs.dingtalk.com/i/nodes/" -Flags: - --node string 表格文档 ID 或 URL (必填) -``` - -### 获取指定工作表详情 -``` -Usage: - dws sheet info [flags] -Example: - dws sheet info --node - dws sheet info --node --sheet-id - dws sheet info --node --sheet-id "Sheet1" -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (不传则返回第一个工作表) -``` - -### 新建工作表 -``` -Usage: - dws sheet new [flags] -Example: - dws sheet new --node --name "Sheet2" - dws sheet new --node --name "数据汇总" -Flags: - --node string 表格文档 ID (必填) - --name string 工作表名称 (必填) -``` - -### 更新工作表属性 -``` -Usage: - dws sheet update [flags] -Example: - # 改名 + 调整冻结 - dws sheet update --node --sheet-id --title "汇总表" --frozen-row-count 2 --frozen-column-count 1 - - # 隐藏工作表 - dws sheet update --node --sheet-id --hidden=true - - # 显示工作表 - dws sheet update --node --sheet-id --hidden=false - - # 移动工作表到第一个位置 - dws sheet update --node --sheet-id --index 0 - - # 取消冻结 - dws sheet update --node --sheet-id --frozen-row-count 0 --frozen-column-count 0 -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --name string 新名称,最长 100 字符,不能包含 / \ ? * [ ] : - --title string --name 的别名(兼容) - --index int 新位置(从 0 开始) - --hidden --hidden=true 隐藏,--hidden=false 取消隐藏 - --tab-color string 工作表标签颜色,Hex 如 #FF0000;传空字符串清除颜色 - --frozen-row-count int 冻结行数,0 表示取消冻结 - --frozen-column-count int 冻结列数,0 表示取消冻结 -``` - -更新工作表名称、位置、隐藏状态、标签颜色、冻结行列。 -`--name`(别名 `--title`)/ `--index` / `--hidden` / `--tab-color` / `--frozen-row-count` / `--frozen-column-count` 至少提供一个;多个属性可同时传入,将在同一次请求中更新。 - -注意: -- 至少需要保留一个可见的工作表,不能将所有工作表都隐藏 -- 冻结行数/列数不能超过工作表的总行数/列数 - -### 复制工作表 -``` -Usage: - dws sheet copy [flags] -Example: - # 按默认位置复制 - dws sheet copy --node --sheet-id - - # 指定副本名称和位置 - dws sheet copy --node --sheet-id --title "销售副本" --index 2 - - # 只指定名称 - dws sheet copy --node --sheet-id --title "备份" -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 源工作表 ID 或名称 (必填) - --title string 副本名称,最长 100 字符,不能包含 / \ ? * [ ] : (不传则系统自动生成) - --index int 副本位置(从 0 开始)(不传则放在源工作表之后) -``` - -复制指定工作表,在同一表格中创建一个副本。 -复制操作会将源工作表的所有内容(包括数据、格式、公式等)完整复制到新工作表中。 -传 `--index` 时,CLI 会先复制,再追加一次位置更新,把副本移动到目标索引。 -名称与已有工作表重复时系统会自动重命名。 - -### 读取工作表数据 -``` -Usage: - dws sheet range read [flags] # 别名: dws sheet range get -Example: - dws sheet range read --node - dws sheet range read --node --sheet-id - dws sheet range read --node --sheet-id "Sheet1" --range "A1:D10" - dws sheet range read --node --range "Sheet1!A1:D10" - - # 使用 get 别名,与 read 等价 - dws sheet range get --node --sheet-id --range "A1:D10" - - # 指定取值模式:原始值 / 公式文本 - dws sheet range read --node --value-render-option raw_value - dws sheet range read --node --value-render-option formula -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (不传则默认第一个工作表) - --range string 读取范围,A1 表示法 (如 A1:D10,不传则读取全部数据) - --value-render-option string 取值模式: formatted_value(格式化展示值,默认) | raw_value(原始值) | formula(公式文本,无公式回退原始值) -``` - -**超时处理建议**:读取大范围数据时若出现超时或响应过慢,请主动缩小 `--range` 查询范围,**建议单次读取的单元格数量控制在 5000 个以内**(例如 50 行 × 100 列、100 行 × 50 列)。对于大表可采用分页读取策略: -- 先通过 `info` 获取 `nonEmptyRange.range` / `nonEmptyRange.lastRow` / `nonEmptyRange.lastColumn` 确定数据边界(空表时这些字段为 null) -- 按行分批读取,如 `A1:J500`、`A501:J1000`、`A1001:J1500` …… -- 避免不传 `--range` 直接读取整个大工作表 - -### 更新工作表指定区域内容 -``` -Usage: - dws sheet range update [flags] -Example: - # 写入文本(每个单元格必须是 object) - dws sheet range update --node --sheet-id --range "A1:B2" \ - --values '[[{"type":"text","text":"姓名"},{"type":"text","text":"分数"}],[{"type":"text","text":"张三"},{"type":"text","text":"90"}]]' - - # 写入公式(text 以 = 开头识别为公式) - dws sheet range update --node --sheet-id --range "C2" \ - --values '[[{"type":"text","text":"=A2&B2"}]]' - - # 写入单元格级超链接(写在 cell 的 hyperlink 字段,没有 --hyperlinks flag) - dws sheet range update --node --sheet-id --range "A1" \ - --values '[[{"type":"text","text":"钉钉","hyperlink":{"type":"path","link":"https://dingtalk.com"}}]]' - - # 只更新部分单元格:用 {} 空对象占位保留原值 - dws sheet range update --node --sheet-id --range "A1:B1" \ - --values '[[{"type":"text","text":"新值"},{}]]' - - # 清空单个单元格(text 为空字符串) - dws sheet range update --node --sheet-id --range "A1" \ - --values '[[{"type":"text","text":""}]]' - - # 清空整片区域请改用 range clear - dws sheet range clear --node --sheet-id --range "A1:B3" -Flags: - --node string 表格文档 ID (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --range string 目标单元格区域地址,如 A1:B3 (必填) - --values string 单元格内容,二维 JSON 数组 (必填);每个元素必须是 object -``` - -**单元格对象协议(重要)**:`--values` 每个元素必须是 object,支持以下形态: -- `{"type":"text","text":"内容"}`:普通文本;`text` 以 `=` 开头识别为公式;`text` 为 `""` 清空该格;数字/布尔写成字符串(`{"type":"text","text":"100"}`),服务端自动识别类型 -- `{"type":"text","text":"内容","cellStyles":{...}}`:文本 + 整格样式(`fontWeight`/`fontColor`/`backgroundColor`/`numberFormat` 等) -- `{"type":"richText","texts":[...]}`:富文本,子项可为 text/link/attachment/image,片段样式写在子项 `style` -- `{"type":"text","text":"钉钉","hyperlink":{"type":"path","link":"https://..."}}`:整格超链接(`type` 可为 `path`/`sheet`/`range`,或 `{"type":"none"}` 清除) -- `{"dataValidation":{...}}`:写下拉/复选框数据验证 -- `{}`:空对象,跳过该格保留原值(只改部分单元格时占位用) - -不再支持裸值(`"张三"` / `90` / `null`),原样传入会报「不支持原始值……每个单元格必须是 object」。 - -**单次调用建议**:行数 ≤ 1000,单元格总数(行×列)≤ 5000;超过时请拆分多次调用。 - -**何时该用 `csv-put` 替代**:如果你准备用 `range update` 写入大批量值或公式,数据可以表达为 CSV,且不需要超链接或富文本,应改用 `csv-put`——它无需手动拼装 object 数组,且支持自动扩容行列。需要富格式对象、`{}` 跳过或修改少量单元格时使用 `range update`。 - -**范围职责**:`range update` 负责写入单元格的值、超链接、per-cell 样式与数据验证。批量刷整片区域的统一样式或数字格式(百分比 / 货币 / 日期 / 文本等)请使用 `dws sheet range set-style --number-format <格式代码>`。 - -### 设置单元格样式 -``` -Usage: - dws sheet range set-style [flags] -Example: - # 给 A1:B3 打上黄底粗体居中 - dws sheet range set-style --node --sheet-id --range "A1:B3" \ - --bg-color "#FFF2CC" --font-weight bold --h-align center - - # 给 C1:C5 逐单元格设置不同背景色 - dws sheet range set-style --node --sheet-id --range "C1:C5" \ - --bg-colors-json '[["#FF0000"],["#00FF00"],["#0000FF"],["#FFFF00"],["#FF00FF"]]' - - # 整片 range 启用自动换行 - dws sheet range set-style --node --sheet-id --range "A1:E10" --word-wrap autoWrap -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --range string 目标区域,如 A1:B3 (必填) - --bg-color string 背景色(#RRGGBB),一键刷整个 range;与 --bg-colors-json 二选一 - --bg-colors-json string 背景色二维 JSON 数组,维度需与 --range 一致 - --font-size int 字号,一键刷整个 range;与 --font-sizes-json 二选一 - --font-sizes-json string 字号二维 JSON 数组 - --h-align string 水平对齐:left/center/right/general - --h-aligns-json string 水平对齐二维 JSON 数组 - --v-align string 垂直对齐:top/middle/bottom - --v-aligns-json string 垂直对齐二维 JSON 数组 - --font-color string 字体颜色(#RRGGBB) - --font-colors-json string 字体颜色二维 JSON 数组 - --font-weight string 字体粗细:bold/normal - --font-weights-json string 字体粗细二维 JSON 数组 - --word-wrap string 换行方式:overflow/clip/autoWrap(整个 range 共用) - --number-format string 数字格式,如 General/@/#,##0/0%/yyyy/m/d -``` - -**特性说明**: -- 每个样式维度提供两种写法,二选一:`--xxx`(单值刷整个 range,CLI 本地展开为二维数组)vs `--xxx-json`(逐单元格指定,维度需与 `--range` 完全一致) -- 至少需传入一个样式参数。单次调用建议:行数 ≤ 1000,单元格总数 ≤ 5000 -- 枚举值按驼峰书写:`autoWrap`、`bold`、`normal`、`center` 等 - -### 批量设置单元格样式 -``` -Usage: - dws sheet range batch-set-style [flags] -Example: - dws sheet range batch-set-style --node --batch ./styles.json - dws sheet range batch-set-style --node --batch ./styles.json --continue-on-error -Flags: - --node string 表格文档 ID 或 URL (必填) - --batch string 批次配置 JSON 文件路径 (必填) - --continue-on-error 遇到失败时继续执行后续条目(默认遇错即停) -``` - -配置文件格式(JSON 数组,每个元素一条批次项): -```json -[ - { - "sheetId": "Sheet1", - "range": "A1:B3", - "bgColor": "#FFF2CC", - "fontSize": 12, - "hAlign": "center", - "vAlign": "middle", - "fontColor": "#333333", - "fontWeight": "bold", - "wordWrap": "autoWrap", - "numberFormat": "General" - }, - { - "sheetId": "Sheet1", - "range": "C1:C5", - "bgColorsJson": "[[\"#FF0000\"],[\"#00FF00\"],[\"#0000FF\"],[\"#FFFF00\"],[\"#FF00FF\"]]" - } -] -``` - -**特性说明**: -- CLI 侧顺序循环逐条调用 `update_range`(非服务端批量),运行时输出 `[N/M]` 进度 -- 每条记录执行与 `set-style` 一致的校验:至少一项样式字段 + rows ≤ 1000 + rows×cols ≤ 30000 + 枚举合法 -- 默认遇错即停(返回非 0),`--continue-on-error` 时所有条目跑完再返回首个错误 - -### 在工作表中搜索单元格内容 -``` -Usage: - dws sheet find [flags] -Example: - # 基本搜索 - dws sheet find --node --sheet-id --find "销售额" - - # 在指定范围内搜索 - dws sheet find --node --sheet-id --find "合计" --range "A1:D100" - - # 正则表达式搜索(不区分大小写) - dws sheet find --node --sheet-id --find "^total" --use-regexp --match-case=false - - # 精确匹配整个单元格内容 - dws sheet find --node --sheet-id --find "完成" --match-entire-cell - - # 搜索公式文本 - dws sheet find --node --sheet-id --find "SUM" --match-formula -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --find string 搜索文本 (必填) - --range string 搜索范围,A1 表示法 (如 A1:D10) - --match-case 区分大小写 (默认 true) - --match-entire-cell 精确匹配整个单元格内容 - --use-regexp 启用正则表达式搜索 - --match-formula 搜索公式文本而非显示值 - --include-hidden 包含隐藏单元格 -``` - -### 在工作表末尾追加数据 -``` -Usage: - dws sheet append [flags] -Example: - dws sheet append --node --sheet-id --values '[["张三","销售部",50000]]' - dws sheet append --node --sheet-id "Sheet1" \ - --values '[["李四","市场部",38000],["王五","销售部",62000]]' -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --values string 追加数据,二维 JSON 数组 (必填) -``` - -`--values` 为二维 JSON 数组,外层每个元素代表一行,内层每个元素代表一个单元格值。 -追加的数据列数应与工作表已有数据的列数保持一致。 - -### 将 CSV 数据写入指定位置 -``` -Usage: - dws sheet csv-put [flags] -Example: - # 内联多行 CSV:必须用 $'...' 让 \n 变成真换行;普通单引号 '...\n...' 里的 \n 是字面量,会写成一行含字面 \n 的错误数据 - dws sheet csv-put --node --sheet-id --start-cell A1 \ - --csv $'name,score\nAlice,95\nBob,87' - - dws sheet csv-put --node --sheet-id --start-cell A1 \ - --csv "=1+1,'=1+1" - - # 多行数据推荐用 @文件,最稳妥 - dws sheet csv-put --node --sheet-id --start-cell B2 \ - --csv @data.csv --allow-overwrite - - cat data.csv | dws sheet csv-put --node --sheet-id \ - --start-cell A1 --csv - - - dws sheet csv-put --node --sheet-id --start-cell A1 \ - --csv @data.csv --dry-run -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --csv string CSV 文本、@文件路径 或 - 表示 stdin (必填) - --start-cell string 起始单元格,A1 表示法 (必填) - --allow-overwrite 允许覆盖已有数据 (默认 false) -``` - -将 RFC 4180 格式的 CSV 文本写入指定工作表的指定单元格位置。 -- 写入值和公式,不支持样式/批注。字段值以 `=` 开头时默认按公式解析;如需写入以 `=` 开头的字面文本,在字段值前加单引号(例如 `'=1+1`) -- 数字/日期/百分数由表格引擎自动识别类型(如 `95` 存为数字,`2025-03-01` 存为日期) -- 自动扩容行列:CSV 数据超出当前工作表维度时自动追加行/列 -- 目标区域如含合并单元格,合并将被打散,值正常写入 -- `--allow-overwrite` 默认 false,目标区域有数据时需显式传 `--allow-overwrite` 才能覆盖 -- `--csv` 支持三种输入:直接传文本、`@filepath` 从本地文件读取、`-` 从 stdin 管道读取 -- CSV 文本上限 2M 字符,单元格总数上限 30000 -- 特殊字符处理:CLI 会自动过滤 `\r`(Windows 换行符)和 BOM(UTF-8 文件头标记),Excel/Windows 导出的 CSV 可直接使用;如 CSV 数据中含零宽字符(U+200B 等)或 Bidi 控制符,CLI 会拒绝并报错 - -### 在指定位置插入行或列 -``` -Usage: - dws sheet insert-dimension [flags] -Example: - # 在第 3 行之前插入 2 行 - dws sheet insert-dimension --node --sheet-id --dimension ROWS --position "3" --length 2 - - # 在 A 列之前插入 1 列 - dws sheet insert-dimension --node --sheet-id --dimension COLUMNS --position "A" --length 1 - - # 使用工作表前缀(忽略 --sheet-id) - dws sheet insert-dimension --node --sheet-id --dimension ROWS --position "Sheet1!3" --length 5 - - # 在 AB 列之前插入 3 列 - dws sheet insert-dimension --node --sheet-id --dimension COLUMNS --position "AB" --length 3 -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --dimension string 插入维度: ROWS 或 COLUMNS (必填) - --position string 插入位置,A1 表示法 (必填)。ROWS 时为行号如 "3";COLUMNS 时为列字母如 "A" - --length string 插入数量,正整数 (必填),最大 5000 -``` - -在钉钉表格指定工作表的指定位置之前插入若干空行或空列。 -`--dimension ROWS` 时,`--position` 为 1-based 行号字符串;`--dimension COLUMNS` 时,`--position` 为列字母。 -支持在 `--position` 中携带工作表前缀(如 `Sheet1!3`),此时忽略 `--sheet-id`。 -若需要在末尾追加行/列,请使用 `append` 命令。 - -### 删除指定位置的行或列 - -> **CAUTION:** 不可逆操作 — 执行前必须向用户确认。 - -``` -Usage: - dws sheet delete-dimension [flags] -Example: - # 从第 3 行开始删除 2 行 - dws sheet delete-dimension --node --sheet-id --dimension ROWS --position "3" --length 2 - - # 从 A 列开始删除 1 列 - dws sheet delete-dimension --node --sheet-id --dimension COLUMNS --position "A" --length 1 - - # 使用工作表前缀(忽略 --sheet-id) - dws sheet delete-dimension --node --sheet-id --dimension ROWS --position "Sheet1!3" --length 5 - - # 从 AB 列开始删除 3 列 - dws sheet delete-dimension --node --sheet-id --dimension COLUMNS --position "AB" --length 3 -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --dimension string 删除维度: ROWS 或 COLUMNS (必填) - --position string 删除起始位置,A1 表示法 (必填)。ROWS 时为行号如 "3";COLUMNS 时为列字母如 "A" - --length string 删除数量,正整数 (必填),最大 5000 -``` - -在钉钉表格指定工作表中,从指定位置起删除若干连续的行或列。 -`--dimension ROWS` 时,`--position` 为 1-based 行号字符串;`--dimension COLUMNS` 时,`--position` 为列字母。 -支持在 `--position` 中携带工作表前缀(如 `Sheet1!3`),此时忽略 `--sheet-id`。 -删除后后续的行/列会向前移动填补空位;若需要仅清空内容但保留行/列占位,请使用 `range clear`(整片区域清除,比逐格写空更简洁)。 - -### 更新指定范围行/列属性 -``` -Usage: - dws sheet update-dimension [flags] -Example: - # 隐藏第 3~4 行 - dws sheet update-dimension --node --sheet-id --dimension ROWS --start-index "3" --length 2 --hidden - - # 显示 A~B 列 - dws sheet update-dimension --node --sheet-id --dimension COLUMNS --start-index "A" --length 2 --hidden=false - - # 设置第 1~5 行行高为 40px - dws sheet update-dimension --node --sheet-id --dimension ROWS --start-index "1" --length 5 --pixel-size 40 - - # 设置 C 列列宽为 200px 并隐藏 - dws sheet update-dimension --node --sheet-id --dimension COLUMNS --start-index "C" --length 1 --pixel-size 200 --hidden - - # 使用工作表前缀(忽略 --sheet-id) - dws sheet update-dimension --node --sheet-id --dimension ROWS --start-index "Sheet1!3" --length 2 --hidden -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --dimension string 更新维度: ROWS 或 COLUMNS (必填) - --start-index string 起始位置,A1 表示法 (必填)。ROWS 时为行号如 "3";COLUMNS 时为列字母如 "A" - --length string 更新数量,正整数 (必填),最大 5000 - --hidden 是否隐藏 (true=隐藏, false=显示),与 --pixel-size 至少填其一 - --pixel-size int 行高或列宽(像素),ROWS 时为行高,COLUMNS 时为列宽,与 --hidden 至少填其一 -``` - -批量更新钉钉表格指定工作表中连续多行/多列的属性,支持设置显隐状态(hidden)与行高/列宽(pixelSize)。 -`--dimension ROWS` 时,`--start-index` 为 1-based 行号字符串;`--dimension COLUMNS` 时,`--start-index` 为列字母。 -支持在 `--start-index` 中携带工作表前缀(如 `Sheet1!3`),此时忽略 `--sheet-id`。 -`--hidden` 与 `--pixel-size` 至少必须提供一个。当同时提供时,将先应用尺寸再应用显隐,任一失败整体失败。 -`--pixel-size` 单位为像素,`dimension=ROWS` 时表示行高、`dimension=COLUMNS` 时表示列宽。 - -### 合并单元格 -``` -Usage: - dws sheet merge-cells [flags] -Example: - # 合并所有单元格(默认) - dws sheet merge-cells --node --sheet-id --range "A1:B3" - - # 按行合并 - dws sheet merge-cells --node --sheet-id --range "A1:C3" --merge-type mergeRows - - # 按列合并 - dws sheet merge-cells --node --sheet-id --range "A1:C3" --merge-type mergeColumns - - # 使用带工作表前缀的范围(忽略 --sheet-id) - dws sheet merge-cells --node --sheet-id --range "Sheet1!A1:B3" -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --range string 目标单元格区域地址,如 A1:B3 (必填) - --merge-type string 合并方式: mergeAll(默认)/mergeRows/mergeColumns -``` - -支持三种合并方式: -- `mergeAll`(默认):合并所有单元格,将选定区域内的所有单元格合并成一个 -- `mergeRows`:按行合并,在选定区域内将同一行相邻的单元格合并 -- `mergeColumns`:按列合并,在选定区域内将同一列相邻的单元格合并 - -注意:合并时只保留左上角单元格的值,其他单元格的值会被丢弃。 -`--range` 支持带工作表前缀的写法(如 `Sheet1!A1:B3`),此时将优先使用前缀解析出的工作表,忽略 `--sheet-id`。 - -### 上传附件到表格 -``` -Usage: - dws sheet media-upload [flags] -Example: - dws sheet media-upload --node --file ./report.pdf - dws sheet media-upload --node --file ./data.bin --name "数据文件.dat" --mime-type application/octet-stream -Flags: - --node string 目标表格文档的标识,支持传入 URL 或 ID (必填) - --file string 本地文件路径 (必填) - --name string 附件显示名称 (默认使用文件名) - --mime-type string 文件 MIME 类型 (默认根据扩展名推断) -``` - -`--format json` 输出规整 JSON:`{success, resourceId, resourceUrl, fileName, fileSize}`。`resourceUrl` 可用于 `create-float-image` 的 `--src`。 - -### 上传图片并写入表格单元格 -``` -Usage: - dws sheet write-image [flags] -Example: - dws sheet write-image --node --sheet-id --range A1:A1 --file ./chart.png - dws sheet write-image --node --sheet-id --range B2:B2 --file ./logo.png --width 200 --height 100 -Flags: - --node string 目标表格文档的标识,支持传入 URL 或 ID (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --range string 目标单元格区域地址,如 A1:A1 (必填) - --file string 本地图片文件路径 (必填) - --name string 图片显示名称 (默认使用文件名) - --mime-type string 文件 MIME 类型 (默认根据扩展名推断) - --width int 图片显示宽度 (可选) - --height int 图片显示高度 (可选) -``` - -### 创建浮动图片 -``` -Usage: - dws sheet create-float-image [flags] -Example: - # 先上传图片获取 resourceUrl - dws sheet media-upload --node --file ./chart.png - # 输出: resourceUrl: /core/api/resources/img/xxxx... - - # 再创建浮动图片 - dws sheet create-float-image --node --sheet-id \ - --src "/core/api/resources/img/xxxx..." --range A1 --width 400 --height 300 - - # 带偏移量 - dws sheet create-float-image --node --sheet-id \ - --src "/core/api/resources/img/xxxx..." --range B2 --width 200 --height 150 --offset-x 10 --offset-y 20 -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --src string 图片资源路径,通过 media-upload 获取的 resourceUrl (必填) - --range string 锚点单元格,A1 表示法,如 A1、B3 (必填) - --width int 图片宽度,像素,正整数 (必填) - --height int 图片高度,像素,正整数 (必填) - --offset-x int 水平偏移量,像素 (默认 0) - --offset-y int 垂直偏移量,像素 (默认 0) -``` - -浮动图片悬浮于单元格之上,不占用单元格内容,可自由定位和调整大小。 -- `--src` 必须是 `media-upload` 返回的 `resourceUrl`(格式为 `/core/api/resources/img/...`),不能直接传外部 URL -- `--range` 使用 A1 表示法指定锚点单元格(如 `A1`、`B3`),支持带工作表前缀(如 `Sheet1!A1`) -- `--width` / `--height` 为必填,单位像素,必须为正整数 -- `--offset-x` / `--offset-y` 表示相对锚点单元格左上角的偏移量(像素),默认 0,不能为负数 - -### 获取浮动图片详情 -``` -Usage: - dws sheet get-float-image [flags] -Example: - dws sheet get-float-image --node --sheet-id --float-image-id -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --float-image-id string 浮动图片 ID (必填) -``` - -获取单个浮动图片的详细信息,包括 ID、图片资源路径、锚点位置、尺寸和偏移量。 -`--float-image-id` 可通过 `list-float-images` 获取。 - -### 列出工作表所有浮动图片 -``` -Usage: - dws sheet list-float-images [flags] -Example: - dws sheet list-float-images --node --sheet-id -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) -``` - -列出指定工作表中所有浮动图片,返回 `floatImages` 数组和 `totalCount`。 - -### 更新浮动图片属性 -``` -Usage: - dws sheet update-float-image [flags] -Example: - # 移动浮动图片到新位置 - dws sheet update-float-image --node --sheet-id --float-image-id --range C5 - - # 调整尺寸 - dws sheet update-float-image --node --sheet-id --float-image-id --width 600 --height 400 - - # 替换图片(需先 media-upload 新图片获取 resourceUrl) - dws sheet update-float-image --node --sheet-id --float-image-id \ - --src "/core/api/resources/img/xxxx..." -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --float-image-id string 浮动图片 ID (必填) - --src string 新的图片资源路径,通过 media-upload 获取的 resourceUrl - --range string 新的锚点单元格,A1 表示法 - --width int 新的图片宽度,像素 - --height int 新的图片高度,像素 - --offset-x int 新的水平偏移量,像素 - --offset-y int 新的垂直偏移量,像素 -``` - -更新浮动图片的属性,`--src` / `--range` / `--width` / `--height` / `--offset-x` / `--offset-y` 至少传入一个。 -`--float-image-id` 可通过 `list-float-images` 获取。 - -### 删除浮动图片 -``` -Usage: - dws sheet delete-float-image [flags] -Example: - dws sheet delete-float-image --node --sheet-id --float-image-id -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --float-image-id string 浮动图片 ID (必填) -``` - -删除指定的浮动图片,操作不可恢复。`--float-image-id` 可通过 `list-float-images` 获取。 - -### 全局查找替换 -``` -Usage: - dws sheet replace [flags] -Example: - dws sheet replace --node --sheet-id --find "旧文本" --replacement "新文本" - dws sheet replace --node --sheet-id --find "待处理" --replacement "已完成" --match-entire-cell - dws sheet replace --node --sheet-id --find "\\d{4}" --replacement "****" --use-regexp - dws sheet replace --node --sheet-id --find "旧" --replacement "新" --range "A1:D100" - dws sheet replace --node --sheet-id --find "临时" --replacement "" -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --find string 查找文本 (必填) - --replacement string 替换文本 (必填,可为空字符串表示删除) - --range string 替换范围,A1 表示法 (如 A1:D100) - --match-case 区分大小写 (默认 false) - --match-entire-cell 完整单元格匹配 - --use-regexp 启用正则表达式匹配 - --include-hidden 包含隐藏行/列 -``` - -返回被替换的单元格数量。`--replacement` 可以为空字符串,表示删除匹配内容。 - -### 移动行或列 -``` -Usage: - dws sheet move-dimension [flags] -Example: - # 将第 2 行移动到第 5 行的位置(索引从 0 开始) - dws sheet move-dimension --node --sheet-id \ - --dimension ROWS --start-index 1 --end-index 1 --destination-index 4 - - # 将第 2~4 行移动到第 1 行之前 - dws sheet move-dimension --node --sheet-id \ - --dimension ROWS --start-index 1 --end-index 3 --destination-index 0 - - # 将 B~C 列移动到 E 列的位置 - dws sheet move-dimension --node --sheet-id \ - --dimension COLUMNS --start-index 1 --end-index 2 --destination-index 4 -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --dimension string 维度类型: ROWS 或 COLUMNS (必填) - --start-index int 源起始索引,0-based (必填) - --end-index int 源结束索引,0-based,包含 (必填) - --destination-index int 目标位置索引,0-based (必填) -``` - -索引均为 0-based(第 1 行/列的索引为 0)。destination-index 不能在 [start-index, end-index] 范围内。 - -**destination-index 计算规则:** -destination-index 是目标位置的 0-based 索引,即移动到第 n 行/列则传 n-1: -- 通用公式:`destination-index = 目标行号(1-based) - 1` -- 例如:将第 2 行移到第 5 行位置 → `destination-index = 5 - 1 = 4`,即 `start-index=1, end-index=1, destination-index=4` -- 例如:将第 4 行移到第 1 行(最前面)→ `destination-index = 1 - 1 = 0`,即 `start-index=3, end-index=3, destination-index=0` - -### 追加空行或空列 -``` -Usage: - dws sheet add-dimension [flags] -Example: - dws sheet add-dimension --node --sheet-id --dimension ROWS --length 5 - dws sheet add-dimension --node --sheet-id --dimension COLUMNS --length 3 -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --dimension string 维度类型: ROWS 或 COLUMNS (必填) - --length int 追加数量,正整数,最多 5000 (必填) -``` - -在工作表末尾追加指定数量的空行或空列。 - -### 创建行或列分组 -``` -Usage: - dws sheet group-dimension [flags] -Example: - dws sheet group-dimension --node --sheet-id --range "3:7" - dws sheet group-dimension --node --sheet-id --range "C:F" --group-state fold - dws sheet group-dimension --node --sheet-id --range "Sheet1!3:7" -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --range string 整行/整列范围,如 "3:7" 或 "C:F" (必填) - --group-state string 创建后的分组状态: expand 或 fold,默认 expand -``` - -在工作表中为连续行或连续列创建分组。`--range` 必须是整行或整列范围,不支持普通单元格矩形范围。 - -### 取消行或列分组 -``` -Usage: - dws sheet ungroup-dimension [flags] -Example: - dws sheet ungroup-dimension --node --sheet-id --range "3:7" - dws sheet ungroup-dimension --node --sheet-id --range "C:F" -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --range string 整行/整列范围,如 "3:7" 或 "C:F" (必填) -``` - -取消指定连续行或连续列的分组。 - -### 取消合并单元格 -``` -Usage: - dws sheet unmerge-cells [flags] -Example: - dws sheet unmerge-cells --node --sheet-id --range "A1:D5" -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --range string 取消合并的范围,A1 表示法 (必填) -``` - -取消指定范围内所有合并的单元格,恢复为独立单元格。 - -### 设置下拉列表 -``` -Usage: - dws sheet set-dropdown [flags] -Example: - # 设置单选下拉列表 - dws sheet set-dropdown --node --sheet-id --range "A2:A100" \ - --options '[{"value":"选项1"},{"value":"选项2"},{"value":"选项3"}]' - - # 设置带颜色的多选下拉列表 - dws sheet set-dropdown --node --sheet-id --range "B2:B50" \ - --options '[{"value":"高","color":"#ff0000"},{"value":"中","color":"#ffaa00"},{"value":"低","color":"#00ff00"}]' \ - --multi-select -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --range string 目标单元格范围,A1 表示法,如 A2:A100 (必填) - --options string 下拉选项 JSON 数组 (必填),如 '[{"value":"选项1","color":"#ff0000"}]' - --multi-select 是否允许多选(默认单选) -``` - -在指定单元格范围内设置下拉列表。设置后用户可从预定义选项中选择值。 -- **用途**:为单元格配置下拉列表,支持自定义选项颜色和多选。 -- **场景**:规范数据输入,如状态选择(完成/进行中/待处理)、优先级(高/中/低)等。 -- **注意**:选项值不能包含英文逗号;如果目标范围已存在下拉列表,会被新配置覆盖。 - -### 获取下拉列表配置 -``` -Usage: - dws sheet get-dropdown [flags] -Example: - dws sheet get-dropdown --node --sheet-id --range "A2:A100" - dws sheet get-dropdown --node --sheet-id --range "A1" -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --range string 查询范围,A1 表示法,如 A1:A100 (必填) -``` - -查询指定范围内的下拉列表配置信息,包括选项值、颜色和是否多选。 -- **用途**:查看单元格已设置的下拉列表选项和配置。 -- **场景**:在修改下拉列表前先查询现有配置;确认下拉列表是否设置成功。 -- **返回**:`dataValidations` 数组,相同选项的单元格聚合为一组,每组包含 `conditionValues`(选项值)、`ranges`(覆盖范围)、`options`(含 `multipleValues` 和 `colorValueMap`)。**判空以 `hasDropdown` 为准**:无下拉列表时 `hasDropdown` 为 false,但 `dataValidations` 里仍会带 1 个全 null 的幽灵条目,不要用数组长度判空。 -- **已知限制**:`options.multipleValues` 服务端恒返回 `null`(即使该下拉是用 `--multi-select` 建的),且不返回 `enableMultiSelect` 字段——**`get-dropdown` 读回无法判断下拉是否多选**。要判断是否多选,改用 `range read`:其 `cells[].dataValidation.enableMultiSelect` 字段准确(实测有效)。 - -### 删除下拉列表 -``` -Usage: - dws sheet delete-dropdown [flags] -Example: - dws sheet delete-dropdown --node --sheet-id --range "A2:A100" - dws sheet delete-dropdown --node --sheet-id --range "B1:D10" -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --range string 要删除下拉列表的范围,A1 表示法 (必填) -``` - -删除指定范围内的下拉列表配置,单元格恢复为普通文本格式。 -- **用途**:移除不再需要的下拉列表约束。 -- **注意**:已填写的单元格值不会被清除;目标范围不存在下拉列表时操作仍返回成功。 - -### 获取筛选信息 -``` -Usage: - dws sheet filter get [flags] -Example: - dws sheet filter get --node --sheet-id - dws sheet filter get --node "https://alidocs.dingtalk.com/i/nodes/" --sheet-id "Sheet1" -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) -``` - -获取指定工作表的全局筛选信息,返回筛选范围和各列的筛选条件详情。 -- **用途**:查看当前工作表上是否存在全局筛选及其配置。 -- **场景**:在修改或删除筛选前,先读取当前筛选配置;创建筛选前先确认是否已存在(每个工作表只能有一个筛选)。 -- **区分**:全局筛选(filter)影响所有协作者看到的数据展示;筛选视图(filter-view)是个人化的。 -- **返回**:`range`(筛选范围,A1 表示法)、`id`(筛选 ID)和 `criteria`(各列条件对象,key 为列偏移量;无条件时为 `{}`)。如果未设置筛选,返回筛选信息为空。 - -### 创建筛选 -``` -Usage: - dws sheet filter create [flags] -Example: - # 创建筛选框架(不设条件) - dws sheet filter create --node --sheet-id --range "A1:E100" - - # 创建筛选并同时设置条件(按值筛选) - dws sheet filter create --node --sheet-id --range "A1:E100" --criteria '[{"column":1,"filterType":"values","visibleValues":["北京","上海"]}]' - - # 创建筛选并设置条件筛选 - dws sheet filter create --node --sheet-id --range "A1:E100" --criteria '[{"column":2,"filterType":"condition","conditions":[{"operator":"greater","value":"100"}]}]' -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --range string 筛选范围,A1 表示法,须包含表头行 (必填) - --criteria string 筛选条件 JSON 数组 (可选) -``` - -在工作表中创建全局筛选。 -- **用途**:为工作表建立筛选器,使数据可按条件过滤展示。 -- **约束**:每个工作表只能有一个全局筛选,已存在时会报错。应先 `filter get` 确认不存在后再创建。 -- **range 规范**:必须包含表头行(如 `A1:E100`),不能只包含数据行。 -- **criteria 格式**:JSON 数组,每个元素含 `column`(列偏移量,从 0 开始)和筛选条件字段。不传则仅创建空筛选框架,后续可通过 `filter update` 设置条件。 - -### 删除筛选 - -> **CAUTION:** 不可逆操作 — 执行前必须向用户确认。 - -``` -Usage: - dws sheet filter delete [flags] -Example: - dws sheet filter delete --node --sheet-id --yes -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) -``` - -删除工作表的全局筛选。 -- **用途**:移除筛选器,所有被隐藏的行将重新显示。 -- **不可逆**:删除后所有筛选条件丢失,需重新创建。 -- **前置**:工作表没有筛选时调用会报错,应先 `filter get` 确认存在。 - -### 批量更新筛选条件 -``` -Usage: - dws sheet filter update [flags] -Example: - # 同时设置多列的筛选条件 - dws sheet filter update --node --sheet-id --criteria '[{"column":0,"filterType":"values","visibleValues":["已完成","进行中"]},{"column":2,"filterType":"condition","conditions":[{"operator":"greater","value":"50"}]}]' - - # 按颜色筛选 - dws sheet filter update --node --sheet-id --criteria '[{"column":1,"filterType":"color","backgroundColor":"#FF0000"}]' -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --criteria string 筛选条件 JSON 数组 (必填) -``` - -批量更新筛选条件,可同时设置多列的筛选条件。 -- **用途**:一次性设置或替换多列的筛选条件。 -- **前置**:工作表必须已创建筛选(通过 `filter create`)。 -- **覆盖式**:指定列的条件会被替换,未指定的列保持不变。如只想修改某一列,建议先 `filter get` 读取现有配置。 -- **criteria 格式**:JSON 数组,支持三种 `filterType`: - - `values`:按值筛选,指定 `visibleValues` 数组 - - `condition`:按条件筛选,指定 `conditions` 数组(最多 2 个)和可选的 `conditionOperator`(`and`/`or`) - - `color`:按颜色筛选,指定 `backgroundColor` 或 `fontColor`(二选一) - -### 清除单列筛选条件 -``` -Usage: - dws sheet filter clear-criteria [flags] -Example: - # 清除第 2 列(B 列)的筛选条件 - dws sheet filter clear-criteria --node --sheet-id --column 1 - - # 清除第 1 列(A 列)的筛选条件 - dws sheet filter clear-criteria --node --sheet-id --column 0 -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --column number 列偏移量,从 0 开始 (必填) -``` - -清除筛选中某一列的筛选条件。 -- **用途**:移除某列的筛选条件,该列不再参与筛选计算。 -- **区分**:仅清除指定列的条件,不删除整个筛选。如需删除整个筛选,使用 `filter delete`。 -- **幂等**:指定列没有设置筛选条件时调用不会报错。 - -### 筛选排序 -``` -Usage: - dws sheet filter sort [flags] -Example: - # 按第 1 列(A 列)升序排序 - dws sheet filter sort --node --sheet-id --column 0 --ascending - - # 按第 3 列(C 列)降序排序 - dws sheet filter sort --node --sheet-id --column 2 --ascending=false -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --column number 排序列偏移量,从 0 开始 (必填) - --ascending 是否升序,默认 true (可选) -``` - -对筛选范围内的数据按指定列排序。 -- **用途**:对数据行按某一列的值进行升序或降序排列。 -- **前置**:工作表必须已创建筛选(通过 `filter create`)。 -- **注意**:排序会实际改变工作表中数据行的物理顺序,不可撤销。 -- **column**:列偏移量从 0 开始,相对于筛选范围首列。 - -### 获取所有筛选视图 -``` -Usage: - dws sheet filter-view list [flags] -Example: - dws sheet filter-view list --node --sheet-id - dws sheet filter-view list --node "https://alidocs.dingtalk.com/i/nodes/" --sheet-id "Sheet1" -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) -``` - -获取指定工作表的所有筛选视图列表,返回每个筛选视图的 ID、名称和范围信息。 -- **用途**:查看当前工作表上已创建的所有筛选视图,获取视图 ID、名称和范围。 -- **场景**:在对筛选视图进行 update / delete / update-criteria 等操作前,先用 list 获取可用的 filterViewId。 -- **区分**:筛选视图(filter-view)是个人化的数据过滤方式,与全局筛选不同。每个用户可以创建自己的筛选视图,互不影响原始数据。如果没有筛选视图,返回空列表。 - -### 创建筛选视图 -``` -Usage: - dws sheet filter-view create [flags] -Example: - # 创建不带筛选条件的筛选视图 - dws sheet filter-view create --node --sheet-id --name "我的视图" --range "A1:E10" - - # 创建带按值筛选条件的筛选视图 - dws sheet filter-view create --node --sheet-id --name "销售筛选" --range "A1:E10" \ - --criteria '[{"column":0,"filterType":"values","visibleValues":["销售部"]}]' - - # 创建带按条件筛选的筛选视图(大于等于 200000) - dws sheet filter-view create --node --sheet-id --name "高预算" --range "A1:C10" \ - --criteria '[{"column":1,"filterType":"condition","conditions":[{"operator":"greater-equal","value":"200000"}]}]' -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --name string 筛选视图名称 (必填) - --range string 筛选视图范围,A1 表示法,如 A1:E10 (必填) - --criteria string 筛选条件,JSON 数组 (可选) -``` - -在指定工作表中创建一个筛选视图。 -- **用途**:为指定数据区域创建一个可命名的个人化筛选视图,可选同时设置筛选条件。 -- **场景**:用户需要针对某个数据区域建立固定的筛选视角(如"高绩效员工""研发部数据"),方便反复查看。 -- **区分**:与全局筛选不同,筛选视图是个人化的,不影响其他用户看到的数据。如果只需创建视图不设条件,后续可通过 `update-criteria` 单独设置;如果要一步到位,可通过 `--criteria` 在创建时直接设置。 -`--criteria` 为 JSON 数组,每个元素包含 `column`(列偏移量,从 0 开始)和筛选条件字段。支持三种筛选类型: -- `values`:按值筛选,通过 `visibleValues` 指定允许显示的值列表 -- `condition`:按条件筛选,通过 `conditions` 指定条件列表(最多 2 个),每个条件包含 `operator` 和 `value`。支持的操作符(kebab-case):`equal`、`not-equal`、`contains`、`not-contains`、`starts-with`、`not-starts-with`、`ends-with`、`not-ends-with`、`greater`、`greater-equal`、`less`、`less-equal`。多条件之间通过 `conditionOperator` 指定逻辑关系:`and`(且,默认)或 `or`(或) -- `color`:按颜色筛选,通过 `backgroundColor` 或 `fontColor` 指定颜色值(十六进制,如 `#FF0000`),二选一 - -### 更新筛选视图属性 -``` -Usage: - dws sheet filter-view update [flags] -Example: - # 更新筛选视图名称 - dws sheet filter-view update --node --sheet-id --filter-view-id --name "新名称" - - # 更新筛选视图范围 - dws sheet filter-view update --node --sheet-id --filter-view-id --range "A1:F20" - - # 更新筛选条件 - dws sheet filter-view update --node --sheet-id --filter-view-id \ - --criteria '[{"column":1,"filterType":"condition","conditions":[{"operator":"greater","value":"100"}]}]' -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --filter-view-id string 筛选视图 ID (必填) - --name string 筛选视图新名称 - --range string 筛选视图新范围,A1 表示法 - --criteria string 筛选条件,JSON 数组 -``` - -更新筛选视图的名称、范围和/或筛选条件,`--name`、`--range`、`--criteria` 至少传入一个。 -- **用途**:修改已有筛选视图的名称、数据范围或筛选条件。 -- **场景**:数据区域扩展后需要扩大筛选视图范围,或重命名视图,或通过 `--criteria` 一次性批量更新多列筛选条件。 -- **区分**:`update` 可同时修改名称、范围和条件,适合批量更新;`update-criteria` 只能设置单列条件,适合精确控制某一列的筛选逻辑。`--criteria` 指定列的条件会被替换,未指定的列保持不变。 - -`--criteria` 为 JSON 数组,格式与 `filter-view create` 的 `--criteria` 相同,支持的筛选类型和操作符参见「创建筛选视图」说明。 - -### 删除筛选视图 - -> **CAUTION:** 不可逆操作 — 执行前必须向用户确认。 - -``` -Usage: - dws sheet filter-view delete [flags] -Example: - dws sheet filter-view delete --node --sheet-id --filter-view-id -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --filter-view-id string 筛选视图 ID (必填) -``` - -删除指定的筛选视图。 -- **用途**:永久删除一个不再需要的筛选视图及其所有筛选条件。 -- **场景**:筛选视图已过时或不再需要时,清理无用的视图。 -- **区分**:`delete` 删除整个筛选视图(包括所有列的条件),操作不可恢复;`delete-criteria` 只删除某一列的筛选条件,视图本身保留。此操作不影响全局筛选或其他筛选视图,也不影响原始数据。 - -### 更新筛选视图列条件 -``` -Usage: - dws sheet filter-view update-criteria [flags] -Example: - # 按值筛选:只显示"销售部"和"市场部" - dws sheet filter-view update-criteria --node --sheet-id --filter-view-id \ - --column 0 --filter-criteria '{"filterType":"values","visibleValues":["销售部","市场部"]}' - - # 按条件筛选:大于 100 - dws sheet filter-view update-criteria --node --sheet-id --filter-view-id \ - --column 2 --filter-criteria '{"filterType":"condition","conditions":[{"operator":"greater","value":"100"}]}' - - # 按条件筛选:大于等于 200000 - dws sheet filter-view update-criteria --node --sheet-id --filter-view-id \ - --column 1 --filter-criteria '{"filterType":"condition","conditions":[{"operator":"greater-equal","value":"200000"}]}' - - # 按条件筛选:小于 100 - dws sheet filter-view update-criteria --node --sheet-id --filter-view-id \ - --column 1 --filter-criteria '{"filterType":"condition","conditions":[{"operator":"less","value":"100"}]}' - - # 多条件筛选:大于等于 60 且 小于等于 90 - dws sheet filter-view update-criteria --node --sheet-id --filter-view-id \ - --column 2 --filter-criteria '{"filterType":"condition","conditionOperator":"and","conditions":[{"operator":"greater-equal","value":"60"},{"operator":"less-equal","value":"90"}]}' - - # 按颜色筛选:背景色为红色 - dws sheet filter-view update-criteria --node --sheet-id --filter-view-id \ - --column 1 --filter-criteria '{"filterType":"color","backgroundColor":"#FF0000"}' -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --filter-view-id string 筛选视图 ID (必填) - --column int 列偏移量,从 0 开始 (必填) - --filter-criteria string 筛选条件,JSON 对象 (必填) -``` - -更新筛选视图中某一列的筛选条件。 -- **用途**:为筛选视图的指定列创建或更新筛选条件,控制该列哪些数据行可见。 -- **场景**:只显示某些特定值的行(如"只看研发部")→ `filterType: values`;按数值条件筛选(如"绩效 ≥ 85")→ `filterType: condition` + `operator: greater-equal`;按文本条件筛选(如"名称包含关键字")→ `filterType: condition` + `operator: contains`。 -- **区分**:`update-criteria` 精确控制单列条件,适合逐列设置不同的筛选逻辑;`filter-view update --criteria` 可以批量更新多列条件;`delete-criteria` 是 `update-criteria` 的逆操作,删除指定列的条件。 - -`--column` 为列偏移量(从 0 开始),相对于筛选视图范围首列。 -例如筛选视图范围为 `B1:E10`,则 `--column 0` 代表 B 列,`--column 1` 代表 C 列。 - -`--filter-criteria` 为 JSON 对象,支持三种筛选类型: -- `values`:按值筛选,通过 `visibleValues` 指定允许显示的值列表 -- `condition`:按条件筛选,通过 `conditions` 指定条件列表(最多 2 个),每个条件包含 `operator` 和 `value`。支持的操作符:`equal`、`not-equal`、`contains`、`not-contains`、`starts-with`、`not-starts-with`、`ends-with`、`not-ends-with`、`greater`、`greater-equal`、`less`、`less-equal`。多条件之间通过 `conditionOperator` 指定逻辑关系:`and`(且,默认)或 `or`(或) -- `color`:按颜色筛选,通过 `backgroundColor` 或 `fontColor` 指定颜色值(十六进制,如 `#FF0000`),二选一 - -### 删除筛选视图列条件 -``` -Usage: - dws sheet filter-view delete-criteria [flags] -Example: - # 删除第 1 列(A 列)的筛选条件 - dws sheet filter-view delete-criteria --node --sheet-id --filter-view-id --column 0 - - # 删除第 3 列(C 列)的筛选条件 - dws sheet filter-view delete-criteria --node --sheet-id --filter-view-id --column 2 -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --filter-view-id string 筛选视图 ID (必填) - --column int 列偏移量,从 0 开始 (必填) -``` - -清除筛选视图中指定列的筛选条件。 -- **用途**:移除筛选视图中指定列的筛选条件,使该列不再参与过滤。 -- **场景**:之前通过 `update-criteria` 设置了某列的筛选条件,现在需要取消该列的筛选以显示全部数据。 -- **区分**:`delete-criteria` 只清除指定列的条件,筛选视图本身和其他列的条件保持不变;`delete` 会删除整个筛选视图。如果指定列没有设置筛选条件,调用此命令不会报错(幂等操作)。 - -### 获取单个筛选视图详情 -``` -Usage: - dws sheet filter-view info [flags] -Example: - # 查看指定筛选视图的详情 - dws sheet filter-view info --node --sheet-id --filter-view-id -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --filter-view-id string 筛选视图 ID (必填) -``` - -获取指定筛选视图的完整信息,包括 ID、名称、范围和筛选条件。 -- **用途**:查看某个筛选视图的当前配置,包括已设置的所有筛选条件详情。 -- **场景**:在修改或删除筛选视图前,先确认其当前状态;或在 `update-criteria` 后验证条件是否生效。 -- **区分**:`info` 返回单个视图的完整信息(含 criteria);`list` 返回所有视图的列表概要。`info` 需要指定 `--filter-view-id`,ID 可通过 `list` 获取。 -- **实现**:内部调用 `get_filter_views` 获取全部列表后按 ID 过滤。 - -### 列出筛选视图所有列条件 -``` -Usage: - dws sheet filter-view list-criteria [flags] -Example: - # 列出筛选视图的所有条件 - dws sheet filter-view list-criteria --node --sheet-id --filter-view-id -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --filter-view-id string 筛选视图 ID (必填) -``` - -列出指定筛选视图中已设置的所有列筛选条件。 -- **用途**:查看某个筛选视图当前设置了哪些列的筛选条件,包括每列的条件类型和具体规则。 -- **场景**:在管理筛选条件(修改/删除特定列条件)前,先了解当前视图有哪些条件;或排查筛选结果不符合预期时检查条件配置。 -- **区分**:`list-criteria` 返回所有列的条件(按列偏移量为 key 的对象);`get-criteria` 只返回指定列的条件。如果没有设置任何条件,返回空对象 `{}`。 -- **实现**:内部调用 `get_filter_views` 获取视图详情后提取 `criteria` 字段。 - -### 获取单列筛选条件 -``` -Usage: - dws sheet filter-view get-criteria [flags] -Example: - # 查看第 1 列(偏移量 0)的筛选条件 - dws sheet filter-view get-criteria --node --sheet-id --filter-view-id --column 0 - - # 查看第 3 列(偏移量 2)的筛选条件 - dws sheet filter-view get-criteria --node --sheet-id --filter-view-id --column 2 -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --filter-view-id string 筛选视图 ID (必填) - --column int 列偏移量,从 0 开始 (必填) -``` - -获取指定筛选视图中某一列的筛选条件详情。 -- **用途**:查看某个筛选视图中指定列当前设置的筛选条件,包括条件类型、运算符和比较值。 -- **场景**:在修改某列条件前,先查看其当前配置;或验证 `update-criteria` 后该列条件是否正确。 -- **区分**:`get-criteria` 只返回指定列的条件;`list-criteria` 返回所有列的条件。`--column` 为列偏移量(从 0 开始),相对于筛选视图范围首列。 -- **实现**:内部调用 `get_filter_views` 获取视图详情后按列偏移量过滤 `criteria` 中的对应条件。 - -### 导出表格为 xlsx(异步任务一站式) -``` -Usage: - dws sheet export [flags] # 一站式:提交 → 轮询 → 可选下载 -Example: - # 仅导出,返回 downloadUrl(链接有时效性,请尽快下载) - dws sheet export --node - dws sheet export --node "https://alidocs.dingtalk.com/i/nodes/" - - # 导出并自动下载为本地文件 - dws sheet export --node --output ./report.xlsx - - # --output 为目录时,自动按下载链接中的文件名保存 - dws sheet export --node --output ./ - -Flags: - --node string 表格文档 ID 或 URL (必填) - --output string 本地保存路径(可选,支持文件路径或目录) -``` - -将钉钉在线电子表格导出为 Office xlsx 格式。**单命令一站式**:命令内部自动完成「提交任务 → 渐进式退避轮询 → (可选)下载文件」全流程,AI Agent 无需自行拆分步骤或实现轮询。 - -**内部流程**: -1. 调 `submit_export_job` 获取 `jobId` -2. 按渐进式退避策略轮询 `query_export_job` 直至任务终态或超时 -3. 任务成功后取得 `downloadUrl`;若指定了 `--output`,自动 HTTP GET 下载 xlsx 到本地文件 - -**内置轮询策略(CLI 内实现,无需关心)**: -- 第 1~5 次:每次间隔 2 秒 -- 第 6~10 次:每次间隔 5 秒 -- 第 11~20 次:每次间隔 10 秒 -- 第 21~30 次:每次间隔 15 秒 -- **硬上限:最多轮询 30 次(约 5 分钟)**,超时后命令返回错误 - -**命令返回**(`--format json`,默认):输出规整 JSON `{success, jobId, downloadUrl[, outputPath]}`。轮询进度以 `[INFO] [N/30] 状态: ...` 打到 stderr,不污染 stdout 的 JSON。 -- `--output` 未指定:JSON 含 `jobId` + `downloadUrl`(链接有时效性,请尽快下载) -- `--output` 指定为文件路径:下载到该路径,JSON 额外含 `outputPath` -- `--output` 指定为已存在目录:自动从 `downloadUrl` 推断文件名并保存到该目录,JSON 含 `outputPath` - -**失败处理(命令内部已处理,Agent 仅需转述)**: -- MCP 返回 `FAILED`:命令立即返回错误并附带失败原因,**禁止自动重试 `dws sheet export`**,告知用户稍后再试 -- 轮询 30 次仍 `PROCESSING`:命令返回超时错误,告知用户稍后再试 - -**限制**:仅支持钉钉在线电子表格(axls)→ xlsx。导出钉钉文字文档请使用 `doc` 产品对应的导出工具。 - -### 表格模板(list / search / apply) -``` -Usage: - dws sheet template list [flags] # 列出可用模板(--cursor / --limit 分页) - dws sheet template search [flags] # 按关键词搜索模板 - dws sheet template apply [flags] # 应用模板创建新表格文档 - -template search Flags: - --query string 搜索关键词 (必填) - --source string 模板来源: MY(我的模版,默认) / PUBLIC(公开模版) - --cursor string 分页游标 - --limit int 返回数量上限 - -template apply Flags: - --template-id string 模板 ID (必填,来自 list/search) - --name string 新表格文档名称 (可选) - --folder string 目标文件夹 ID (可选,UUID 或 URL) - --workspace string 知识库 ID (可选) -``` - -- 用户说"用模板建表 / 有哪些模板 / 找个 XX 模板"时走 template 系列 -- 典型链路:`template search --query "..."` 或 `template list` 拿到 `templateId` → `template apply --template-id --name "..."` 创建新表格,返回新文档的 `nodeId` - -## 核心工作流 +## 导入本地表格 +用户说"导入Excel/把xlsx转为在线表格/上传表格并在线编辑"时,用 `dws sheet import create`(单命令一站式;`sheet import` 保留兼容,详见 [sheet-import](./sheet/sheet-import.md)): ```bash -# ── 工作流 1: 创建表格并写入数据 ── +# 导入本地表格文件为在线电子表格(默认表格名取文件名) +dws sheet import create --file ./data.xlsx -# 1. 创建表格文档 — 提取 nodeId -dws sheet create --name "销售数据" --format json +# 指定目标文件夹与导入后表格名 +dws sheet import create --file ./report.xls --folder-token --name "月度报表" -# 2. 查看工作表列表 — 提取 sheetId -dws sheet list --node --format json +# 导入到指定知识库 +dws sheet import create --file ./data.xls --workspace +``` +- 支持 `.xlsx` / `.xls`,固定产出**在线电子表格**;只新建、不覆盖已有表格 +- CLI 内部自动完成「创建会话→上传→确认→渐进式退避轮询」(最多 30 次约 5 分钟),**禁止 Agent 侧再实现轮询或重试** +- 导入结果返回 `documentUrl` / `nodeId`,`nodeId` 可直接作为后续 `sheet` 命令的 `--node` +- **禁止用 `dws drive upload --convert` 代替**:drive upload 只上传文件、不在表格导入闭环内,产出不保证为可编辑在线电子表格 -# 3. 写入表头和数据(每个单元格必须是 object;数字也写成字符串) -dws sheet range update --node --sheet-id --range "A1:C1" \ - --values '[[{"type":"text","text":"姓名"},{"type":"text","text":"部门"},{"type":"text","text":"销售额"}]]' --format json +## nodeId 说明 -dws sheet range update --node --sheet-id --range "A2:C4" \ - --values '[[{"type":"text","text":"张三"},{"type":"text","text":"销售部"},{"type":"text","text":"50000"}],[{"type":"text","text":"李四"},{"type":"text","text":"市场部"},{"type":"text","text":"38000"}],[{"type":"text","text":"王五"},{"type":"text","text":"销售部"},{"type":"text","text":"62000"}]]' --format json +`--node` 同时支持文档 ID、URL、分享链接。`drive list` 返回中必须用 `fileId`(UUID 格式),禁止用 `dentryId`(纯数字)。 -# ── 工作流 2: 读取已有表格数据 ── +## 模板管理 -# 1. 获取工作表列表 -dws sheet list --node --format json +当用户说“用模板创建表格”、“搜索表格模板”、“有哪些表格模板”时使用。 -# 2. 查看工作表详情(行列数、最后非空位置等) -dws sheet info --node --sheet-id --format json +### 获取表格模板列表 -# 3. 读取全部数据 -dws sheet range read --node --sheet-id --format json - -# 4. 读取指定区域 -dws sheet range read --node --sheet-id --range "A1:D10" --format json - -# ── 工作流 3: 多工作表管理 ── - -# 1. 新建工作表 -dws sheet new --node --name "汇总" --format json - -# 2. 在新工作表中写入汇总公式(公式写在 text,以 = 开头) -dws sheet range update --node --sheet-id --range "A1:B1" \ - --values '[[{"type":"text","text":"指标"},{"type":"text","text":"数值"}]]' --format json - -dws sheet range update --node --sheet-id --range "A2:B2" \ - --values '[[{"type":"text","text":"总销售额"},{"type":"text","text":"=SUM(Sheet1!C2:C100)"}]]' --format json - -# ── 工作流 4: 写入数据并设置样式 ── - -# 1. 写入数据 -dws sheet range update --node --sheet-id --range "A1:C3" \ - --values '[[{"type":"text","text":"商品"},{"type":"text","text":"单价"},{"type":"text","text":"数量"}],[{"type":"text","text":"苹果"},{"type":"text","text":"5.5"},{"type":"text","text":"100"}],[{"type":"text","text":"香蕉"},{"type":"text","text":"3.2"},{"type":"text","text":"200"}]]' --format json - -# 2. 设置数字格式(人民币)——批量刷整列走 set-style -dws sheet range set-style --node --sheet-id --range "B2:B3" \ - --number-format "¥#,##0.00" --format json - -# 3. 写入超链接(写在 cell 的 hyperlink 字段) -dws sheet range update --node --sheet-id --range "D1" \ - --values '[[{"type":"text","text":"详情","hyperlink":{"type":"path","link":"https://dingtalk.com"}}]]' --format json - -# ── 工作流 5: 追加数据 ── - -# 1. 获取工作表列表 -dws sheet list --node --format json - -# 2. 查看工作表详情(确认列结构) -dws sheet info --node --sheet-id --format json - -# 3. 追加单行数据 -dws sheet append --node --sheet-id \ - --values '[["张三","销售部",50000]]' --format json - -# 4. 追加多行数据 -dws sheet append --node --sheet-id \ - --values '[["李四","市场部",38000],["王五","销售部",62000]]' --format json +``` +Usage: + dws sheet template list [flags] +Example: + dws sheet template list + dws sheet template list --source MY + dws sheet template list --source PUBLIC + dws sheet template list --limit 20 +Flags: + --source string 模板来源: MY(我的模版)/PUBLIC(公开模版),不传默认 MY + --limit int 返回数量上限 (可选) + --cursor string 分页游标 (可选) ``` -```bash -# ── 工作流 6: 插入行或列 ── +### 搜索表格模板 -# 1. 获取工作表列表 -dws sheet list --node --format json - -# 2. 在第 3 行之前插入 2 行 -dws sheet insert-dimension --node --sheet-id \ - --dimension ROWS --position "3" --length 2 --format json - -# 3. 在 A 列之前插入 1 列 -dws sheet insert-dimension --node --sheet-id \ - --dimension COLUMNS --position "A" --length 1 --format json - -# 4. 使用工作表前缀指定位置 -dws sheet insert-dimension --node --sheet-id \ - --dimension ROWS --position "Sheet1!5" --length 3 --format json +``` +Usage: + dws sheet template search [flags] +Example: + dws sheet template search --query "预算" + dws sheet template search --query "排班表" --limit 10 + dws sheet template search --query "财务" --source PUBLIC +Flags: + --query string 搜索关键词 (必填) + --source string 模板来源: MY(我的模版)/PUBLIC(公开模版),不传默认 MY + --limit int 返回数量上限 (可选) + --cursor string 分页游标 (可选) ``` -```bash -# ── 工作流 6b: 删除行或列 ── +### 应用表格模板 -# 1. 获取工作表列表 -dws sheet list --node --format json - -# 2. 从第 3 行开始删除 2 行 -dws sheet delete-dimension --node --sheet-id \ - --dimension ROWS --position "3" --length 2 --format json - -# 3. 从 A 列开始删除 1 列 -dws sheet delete-dimension --node --sheet-id \ - --dimension COLUMNS --position "A" --length 1 --format json - -# 4. 使用工作表前缀指定位置 -dws sheet delete-dimension --node --sheet-id \ - --dimension ROWS --position "Sheet1!5" --length 3 --format json +``` +Usage: + dws sheet template apply [flags] +Example: + dws sheet template apply --template-id TPL_ID --name "月度预算表" + dws sheet template apply --template-id TPL_ID --name "排班表" --folder FOLDER_ID +Flags: + --template-id string 模板 ID (必填,从 template list/search 获取) + --name string 新表格文档名称 (可选) + --folder string 目标文件夹 ID (可选) + --workspace string 知识库 ID (可选) ``` -```bash -# ── 工作流 6c: 更新行/列属性(显隐、行高/列宽) ── +--- -# 1. 获取工作表列表 -dws sheet list --node --format json +## SKILL 摘要(原 dingtalk-sheet/SKILL.md 正文) -# 2. 隐藏第 3~4 行 -dws sheet update-dimension --node --sheet-id \ - --dimension ROWS --start-index "3" --length 2 --hidden --format json + +## Shortcuts(无专用脚本/recipe 时优先) -# 3. 显示 A~B 列 -dws sheet update-dimension --node --sheet-id \ - --dimension COLUMNS --start-index "A" --length 2 --hidden=false --format json +以下 shortcut 同时进入公开 catalog 与 Runtime Schema。先按本 skill 的意图表、脚本和 recipe 路由:存在精确覆盖该场景的专用脚本/recipe 时按其执行;否则用户意图命中时,shortcut 优先于手写原子命令。命令已选中时直接执行;只在参数或安全语义不确定时读取 Agent leaf Schema(例如 `dws schema --cli-path "sheet +" --compact --format json`),在当前 Cobra flags 不确定时读取 `dws sheet --help`。只有参数映射、接口绑定或 provenance 审计才省略 `--compact`。仅当现有路由和 reference 都无法定位低频能力时,才用 `dws shortcut list --service sheet --format json` 批量发现。 -# 4. 设置第 1~5 行行高为 40px -dws sheet update-dimension --node --sheet-id \ - --dimension ROWS --start-index "1" --length 5 --pixel-size 40 --format json +| Shortcut | 风险 | 适用场景 | +|---|---|---| +| `dws sheet +list-sheets` | read | 获取表格文档中全部工作表列表 | +| `dws sheet +read` | read | 读取工作表指定范围的结构化单元格数据 | + -# 5. 设置 C 列列宽为 200px 并隐藏 -dws sheet update-dimension --node --sheet-id \ - --dimension COLUMNS --start-index "C" --length 1 --pixel-size 200 --hidden --format json -``` +## 意图表 -```bash -# ── 工作流 7: 搜索表格数据 ── +| 用户说 | 命令 | +|--------|------| +| "创建电子表格" | `dws sheet create --name "<标题>"` | +| "新建工作表" | `dws sheet new --node --name ""` | +| "读取单元格" | `dws sheet range read --node --sheet-id --range A1:B2` | +| "写入单元格" | `dws sheet range update --node --sheet-id --range A1:B2 --values '[[..]]'` | +| "写入超链接" | `dws sheet range update --node --sheet-id --range A1 --values '[[{"type":"text","text":"显示文本","hyperlink":{"type":"path","link":"https://..."}}]]'` | +| "追加一行" | `dws sheet append --node --sheet-id --values '[[..]]'` | +| "查找 / 替换" | `dws sheet find --node --sheet-id --query "<关键词>"` / `dws sheet replace --node --sheet-id --find "<旧值>" --replacement "<新值>"` | +| "精确匹配搜索 / 完全等于" | `dws sheet find --node --sheet-id --query "<关键词>" --match-entire-cell` | +| "搜索公式文本" | `dws sheet find --node --sheet-id --query "<公式片段>" --match-formula` | +| "正则搜索 / 不区分大小写" | `dws sheet find --node --sheet-id --query "" --use-regexp --match-case=false` | +| "插入图片到单元格" | `dws sheet write-image --node --sheet-id --range A1 --file <图片路径>` | +| "创建浮动图片" | 先 `dws sheet media-upload --node --file <图片路径>` 获取 `resourceUrl`,再 `dws sheet create-float-image --node --sheet-id --src "" --range A1 --width <宽> --height <高>` | -# 1. 获取工作表列表 -dws sheet list --node --format json +## URL 与 ID 前置 -# 2. 基本搜索 — 在指定工作表中查找文本 -dws sheet find --node --sheet-id --find "销售额" --format json +- 用户直接粘贴 `alidocs.dingtalk.com` URL 且意图不明确时,先执行 + `dws drive info --node "" --format json` 探测;只有 `extension=axls` + 才继续走 `sheet`。 +- `spreadsheetv2` 链接不要截取短 path segment,必须把完整 URL 原样传给 `--node`。 +- 写入类命令必须先用 `dws sheet list --node --format json` 取得真实 `sheetId`;不要猜 `Sheet1`、`sheet1`、`0`。 +- 所有 sheet 子命令使用 `--node` 参数;不要写 `--node-id`、`--file-id` 或把 JSON 字段名 `id` 当成节点值传入。 -# 3. 在指定范围内搜索 -dws sheet find --node --sheet-id --find "合计" --range "A1:D100" --format json +## 高频硬约束 -# 4. 正则搜索(不区分大小写) -dws sheet find --node --sheet-id --find "^total" --use-regexp --match-case=false --format json +- `sheet create` 后必须从返回结果提取真实 `nodeId` 或文档 URL,后续 `range update/read` 原样传给 `--node`;如果返回里同时有 `nodeId` 和 `url`,优先用 `nodeId`。 +- 写入区域前先 `sheet list --node --format json` 获取真实 `sheetId`;`range update` 成功后默认用独立 `range read` / `csv-get` 回读受影响区域,不能只凭 `success` / `updatedRows` / `updatedCells` / `message` 宣称完成。仅当用户明确要求“不要再读/信任写入返回”时,才跳过回读并说明验证边界。 +- `range update` 的 `--values` 必须是二维 JSON,行列数量与 `--range` 完全一致;批量样式用 `range batch-set-style --batch `。 +- 整格超链接只能用 `range update` 的 cell-level `hyperlink` 字段:`{"type":"text","text":"显示文本","hyperlink":{"type":"path","link":"https://..."}}`。不要走 `doc`,不要用 `--hyperlinks`,不要把整格链接写成 richText 片段链接。 +- 搜索必须用 `sheet find`,不要用 `range read` 后本地过滤;“精确/完全匹配/等于”必须加 `--match-entire-cell`,“搜公式”必须加 `--match-formula`,“正则且不区分大小写”必须同时加 `--use-regexp --match-case=false`。 +- 同一参数错误最多纠正 2 次;若持续 `nodeId 格式不合法`,回到 + `drive info --node ` 或 `sheet create` 返回重新取 ID,不要无意义重试。 -# 5. 精确匹配整个单元格 -dws sheet find --node --sheet-id --find "完成" --match-entire-cell --format json +## 跨产品协作 -# 6. 搜索公式文本 -dws sheet find --node --sheet-id --find "SUM" --match-formula --format json -``` +- 多维表 / 字段类型 → 切到 `dingtalk-aitable` +- 把表数据写进文档 → 切到 `dingtalk-doc` -```bash -# ── 工作流 8: 合并单元格 ── +## 局部意图与 Recipe -# 1. 获取工作表列表 -dws sheet list --node --format json - -# 2. 合并所有单元格(默认 mergeAll) -dws sheet merge-cells --node --sheet-id --range "A1:B3" --format json - -# 3. 按行合并 -dws sheet merge-cells --node --sheet-id --range "A1:C3" --merge-type mergeRows --format json - -# 4. 按列合并 -dws sheet merge-cells --node --sheet-id --range "A1:C3" --merge-type mergeColumns --format json -``` - -```bash -# ── 工作流 9: 上传附件到表格 ── - -# 1. 基本用法: 上传本地文件到表格 -dws sheet media-upload --node --file ./report.pdf -f json - -# 2. 自定义附件显示名称 (--name 指定上传后在表格中显示的名称) -dws sheet media-upload --node --file ./data.csv --name "销售数据.csv" -f json - -# 3. 指定 MIME 类型 (文件扩展名无法推断时) -dws sheet media-upload --node --file ./data.bin --name "导出数据.dat" --mime-type application/octet-stream -f json - -# 4. 完整流程: 创建表格 → 上传附件 -dws sheet create --name "项目资料" -f json -# 提取 nodeId 后: -dws sheet media-upload --node --file ./design.pdf -f json -dws sheet media-upload --node --file ./timeline.xlsx --name "项目时间线.xlsx" -f json - -# ── 工作流 10: 写入图片到表格单元格 ── - -# 1. 基本用法: 写入图片到指定单元格 -dws sheet write-image --node --sheet-id --range A1:A1 --file ./chart.png -f json - -# 2. 指定显示尺寸 -dws sheet write-image --node --sheet-id --range B2:B2 --file ./logo.png --width 200 --height 100 -f json - -# 3. 自定义图片名称 -dws sheet write-image --node --sheet-id --range C3:C3 --file ./photo.jpg --name "产品图.jpg" -f json - -# 4. 完整流程: 创建表格 → 写表头 → 写入图片 -dws sheet create --name "产品目录" -f json -# 提取 nodeId 后: -dws sheet range update --node --sheet-id Sheet1 --range "A1:B1" --values '[[{"type":"text","text":"产品名称"},{"type":"text","text":"产品图片"}]]' -f json -dws sheet range update --node --sheet-id Sheet1 --range "A2:A2" --values '[[{"type":"text","text":"MacBook Pro"}]]' -f json -dws sheet write-image --node --sheet-id Sheet1 --range B2:B2 --file ./macbook.png --width 150 --height 100 -f json -``` - -```bash -# ── 工作流 11: 筛选视图管理 ── - -# 1. 获取工作表列表 -dws sheet list --node -f json - -# 2. 查看已有筛选视图 -dws sheet filter-view list --node --sheet-id -f json - -# 3. 创建筛选视图(不带条件) -dws sheet filter-view create --node --sheet-id \ - --name "我的筛选" --range "A1:E100" -f json - -# 4. 为筛选视图设置列条件(按值筛选) -dws sheet filter-view update-criteria --node --sheet-id --filter-view-id \ - --column 0 --filter-criteria '{"filterType":"values","visibleValues":["销售部","市场部"]}' -f json - -# 5. 为筛选视图设置列条件(按条件筛选) -dws sheet filter-view update-criteria --node --sheet-id --filter-view-id \ - --column 2 --filter-criteria '{"filterType":"condition","conditions":[{"operator":"greater","value":"100"}]}' -f json - -# 6. 更新筛选视图名称和范围 -dws sheet filter-view update --node --sheet-id --filter-view-id \ - --name "销售数据筛选" --range "A1:F200" -f json - -# 7. 清除某列的筛选条件 -dws sheet filter-view delete-criteria --node --sheet-id --filter-view-id \ - --column 0 -f json - -# 8. 删除筛选视图 -dws sheet filter-view delete --node --sheet-id --filter-view-id -f json -``` - -```bash -# ── 工作流 11b: 创建带条件的筛选视图(一步完成) ── - -# 创建筛选视图时直接指定筛选条件 -dws sheet filter-view create --node --sheet-id \ - --name "高销售额视图" --range "A1:E100" \ - --criteria '[{"column":0,"filterType":"values","visibleValues":["销售部"]},{"column":2,"filterType":"condition","conditions":[{"operator":"greater","value":"50000"}]}]' \ - -f json -``` - -```bash -# ── 工作流 12: 导出表格为 xlsx(单命令一站式)── - -# 场景 A:仅获取下载链接(命令内部自动完成提交+轮询,最终返回 downloadUrl) -dws sheet export --node --format json -# 传入 URL 也可: -# dws sheet export --node "https://alidocs.dingtalk.com/i/nodes/" --format json - -# 场景 B:导出并自动下载为本地文件 -dws sheet export --node --output ./report.xlsx - -# 场景 C:下载到目录,自动按链接推断文件名 -dws sheet export --node --output ./ - -# 禁止在 Agent 侧实现任何轮询或重试,CLI 内部已按 2s/5s/10s/15s 渐进式退避自动完成(最多 30 次)。 -# 若命令返回失败或超时,直接告知用户稍后再试,不要自动重调 dws sheet export。 -``` - -## 上下文传递表 - -| 操作 | 从返回中提取 | 用于 | -|------|-------------|------| -| `create` | `nodeId` | list / info / new / range read / range update / find 的 --node | -| `list` | 工作表的 `sheetId` | info / range read / range update / find 的 --sheet-id | -| `new` | 新工作表的 `sheetId` | range read / range update / find 的 --sheet-id | -| `info` | `rowCount` / `nonEmptyRange.range` / `nonEmptyRange.lastRow` / `nonEmptyRange.lastColumn` / `mergedRanges` | 确定数据范围、追加写入起始行、判断合并结构(空表时 nonEmptyRange.* 为 null) | -| `find` | `matchedCells` 中的 `a1Notation` | 定位目标单元格,用于 range read / range update | -| `append` | `a1Notation` 追加数据所在范围 | 确认追加位置 | -| `csv-put` | `a1Notation` 实际写入的单元格范围 | 确认写入位置和范围 | -| `insert-dimension` | `a1Notation` 新插入区域范围 | 确认插入位置和范围 | -| `delete-dimension` | `a1Notation` 被删除区域范围 | 确认删除位置和范围 | -| `update-dimension` | `a1Notation` 被更新区域范围、`hidden` 生效的显隐状态、`pixelSize` 生效的尺寸 | 确认更新结果 | -| `merge-cells` | `a1Notation` 实际被合并的范围、`mergeType` 生效的合并方式 | 确认合并结果 | -| `media-upload` | `resourceId`、`resourceUrl` | 附件已上传到表格;`resourceUrl` 可用于 `create-float-image` 的 `--src` | -| `write-image` | `resourceId` | 图片已写入指定单元格 | -| `create-float-image` | `floatImage`(含 `id`、`src`、`range`、`width`、`height`、`offsetX`、`offsetY`) | `id` 用于后续 get / update / delete 的 `--float-image-id` | -| `get-float-image` | `floatImage`(完整信息) | 查看单个浮动图片详情 | -| `list-float-images` | `floatImages` 数组、`totalCount` | 获取所有浮动图片的 `id`,用于后续操作 | -| `update-float-image` | `floatImage`(更新后的完整信息) | 确认更新结果 | -| `delete-float-image` | `message` | 确认删除完成 | -| `replace` | `replaceCount` 被替换的单元格数量 | 确认替换结果 | -| `move-dimension` | `sheetId` 工作表 ID | 确认操作完成 | -| `add-dimension` | `sheetId` 工作表 ID | 确认操作完成 | -| `group-dimension` | `range` / `level` / `groupState` | 确认行列分组层级和展开状态 | -| `ungroup-dimension` | `range` / `level` | 确认行列分组已取消 | -| `unmerge-cells` | `sheetId` 工作表 ID | 确认操作完成 | -| `set-dropdown` | `range` 实际设置范围、`optionCount` 选项数量、`enableMultiSelect` 是否多选 | 确认下拉列表设置成功 | -| `get-dropdown` | `hasDropdown` 是否存在下拉、`dataValidations` 下拉配置列表(含 `conditionValues`、`ranges`、`options`) | 查看已有下拉配置 | -| `delete-dropdown` | `range` 实际删除范围 | 确认下拉列表删除完成 | -| `filter-view list` | `filterViews` 筛选视图列表(含 `id`、`name`、`range`) | 获取 filterViewId 用于 info / update / delete / update-criteria / delete-criteria / list-criteria / get-criteria | -| `filter-view info` | `id`、`name`、`range`、`criteria` | 查看单个视图完整配置,确认条件是否生效 | -| `filter-view create` | `id` 筛选视图 ID、`name`、`range` | 用于后续 update / delete / update-criteria / delete-criteria 的 --filter-view-id | -| `filter-view update` | `id`、`name`、`range`、`criteria` | 确认更新结果 | -| `filter-view delete` | `id` 被删除的筛选视图 ID | 确认删除完成 | -| `filter-view update-criteria` | `id` 筛选视图 ID | 确认条件设置完成 | -| `filter-view delete-criteria` | `id` 筛选视图 ID | 确认条件清除完成 | -| `filter-view list-criteria` | 所有列条件(按列偏移量为 key 的对象) | 了解当前视图已设置哪些列的条件 | -| `filter-view get-criteria` | 指定列的条件详情(`filterType`、`conditions` 等) | 查看某列的具体筛选规则 | -| `export` | `downloadUrl`(未指定 --output)/ `outputPath`(指定 --output) | 直接下发给用户或告知文件已保存到本地。命令内部已完成轮询,不要再调用其他 export 相关命令 | - -## nodeId 多格式说明 - -所有 `--node` 参数同时支持文档 ID、文档 URL、表格分享链接,系统自动识别。详细格式和提取规则请参见前文「URL 识别与 NODE_ID 提取」章节。 - -> ** 禁止使用 `dentryId`**:`drive list` 返回结果中同时包含 `dentryId`(纯数字,如 `218595998810`)和 `fileId`(UUID 格式,如 `ZgpG2NdyVXYOR2D5UGDok65MJMwvDqPk`)两个字段。sheet 的 `--node` 和 `--folder` 参数**只能使用 `fileId`(UUID 格式)**,不能使用纯数字的 `dentryId`,传入纯数字会导致命令失败。 - -## values 参数格式说明 - -`--values` 为二维 JSON 数组,第一维为行,第二维为列。**每个单元格必须是 object**,不支持裸值 `"文本"` / `100` / `null`: -- 文本: `{"type":"text","text":"文本"}` -- 数字/布尔: 写成字符串 `{"type":"text","text":"100"}`,服务端按内容自动识别为数字/布尔 -- 公式: `{"type":"text","text":"=SUM(B2:B4)"}`(`text` 以 `=` 开头识别为公式) -- 清空单个单元格: `{"type":"text","text":""}`;清空整片区域用 `range clear` -- 跳过某格保留原值: `{}` 空对象 -- 整格样式: `{"type":"text","text":"重要","cellStyles":{"fontWeight":"bold","fontColor":"#FF0000","numberFormat":"@"}}` -- 富文本: `{"type":"richText","texts":[...]}`(子项 text/link/attachment/image,片段样式写在子项 `style`) - -维度必须与 `--range` 范围一致,例如 `--range "A1:B3"` 需要 3 行 2 列的 object 数组。 - -## 超链接写法说明 - -**没有 `--hyperlinks` flag**(旧写法已废弃,传入报 unknown flag)。单元格级超链接写在 `--values` 里 cell object 的 `hyperlink` 字段: -- `{"type":"text","text":"钉钉","hyperlink":{"type":"path","link":"https://dingtalk.com"}}`:写外部链接 -- `hyperlink.type` 可为 `path`(外部链接)/ `sheet`(工作表跳转,link 为工作表名)/ `range`(区域跳转,link 为 `Sheet2!A1`) -- `{"hyperlink":{"type":"none"}}`:清除整格超链接(保留原值) -- 富文本片段内的链接写在 `richText` 子项的 `link` 字段,与整格 `hyperlink` 不同 - -## number-format 常用值 - -适用范围:`number-format` 在 `range set-style` / `range batch-set-style` 中作为 `--number-format` 参数;`range update` 没有 `--number-format` flag,但可在每个 cell object 的 `cellStyles.numberFormat` 里写同样的格式代码。批量刷整片区域优先用 `set-style`。 - -| 格式代码 | 说明 | 示例 | -|----------|------|------| -| `General` | 常规 | 1234.5 | -| `@` | 文本 | 001234 | -| `#,##0` | 整数千分位 | 1,235 | -| `#,##0.00` | 两位小数 | 1,234.50 | -| `0%` | 百分比 | 85% | -| `yyyy/m/d` | 日期 | 2026/3/15 | -| `hh:mm:ss` | 时间 | 14:30:00 | -| `¥#,##0` | 人民币 | ¥1,235 | - -## 注意事项(完整版) - -> 标 ★ 的条目已在前文「关键注意事项」中列出,此处为完整说明。 - -- ★ `--sheet-id` 获取规范(强制):所有涉及 `--sheet-id` 参数的命令(`info` / `new` / `range read` / `range update` / `find` / `append` / `insert-dimension` / `delete-dimension` / `update-dimension` / `move-dimension` / `add-dimension` / `merge-cells` / `unmerge-cells` / `replace` / `write-image` / `set-dropdown` / `get-dropdown` / `delete-dropdown` / `filter-view *` 等),除非用户主动提供了工作表 ID 或工作表名称,否则在 `sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询真实的 `sheetId` / 工作表名称后再调用,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等);用户仅给出工作表名称时,也应通过 `list` 校验该名称是否存在,避免名称大小写或拼写不一致导致失败 -- ★ `range update` 单元格对象协议(强制):`--values` 每个单元格必须是 object(`{"type":"text","text":...}` / `{"type":"richText",...}` / `{}` 等),不支持裸值 `"张三"` / `90` / `null`,裸值报「不支持原始值……每个单元格必须是 object」。数字/布尔写成字符串 object,服务端自动识别类型。超链接写在 cell object 的 `hyperlink` 字段,没有 `--hyperlinks` flag -- ★ `range update` 维度校验(强制):`--values` 二维数组的行数与列数必须与 `--range` 完全一致: - - 例如 `--range "A1:C3"` 表示 3 行 × 3 列,`--values` 必须是 3×3 的 object 数组 - - `--range "A1"` 表示 1 行 × 1 列,`--values` 必须是 `[[{...}]]` - - 不改的格用 `{}` 空对象占位补齐;禁止各行列数不一致或与 `--range` 不匹配,否则报错 -- ★ `range update` 清空单元格规范(强制):清空单个单元格用 `{"type":"text","text":""}`(不是裸 `""`,也不是 `null`);清空整片区域用 `range clear`;跳过某格保留原值用 `{}` 空对象 -- `create` 不传 `--folder` 和 `--workspace` 时,默认创建在"我的文档"根目录 -- `list` 返回所有工作表的 ID 和名称,是后续操作的必要前置步骤 -- `info` 不传 `--sheet-id` 时默认返回第一个工作表的详情 -- `range read` 不传 `--range` 时默认读取整个工作表的全部非空数据 -- `range read` 的 `--range` 支持 `Sheet1!A1:D10` 格式直接指定工作表(此时忽略 `--sheet-id`) -- `range read` 遇到超时或响应过慢时,应缩小 `--range` 查询范围,**单次读取的单元格数量建议控制在 5000 个以内**;数据量较大时通过 `info` 获取边界后分批读取,避免不传 `--range` 直接读取整个大工作表 -- `range update` 只有 `--values` 一个内容参数(必填),超链接/样式/数据验证都写进 cell object 内部字段 -- `range update` 职责边界:可写值、超链接、per-cell 样式(`cellStyles`)与数据验证(`dataValidation`)。批量刷整片区域的统一样式或数字格式优先用 `dws sheet range set-style --number-format <格式代码>`;批量多区域走 `dws sheet range batch-set-style --batch `(配置文件用 `numberFormat` 字段) -- ★ `range update` / `range set-style` / `range batch-set-style` 单次调用上限(强制):行数 ≤ 1000,单元格总数(行×列)建议≤ 5000(底层硬限 30000);超限请拆分多次调用。CLI 会在调用前做本地预校验,底层超 30000 会直接报错 -- `range set-style` / `range batch-set-style` 的样式枚举按驼峰书写:`wordWrap` 取 `overflow`/`clip`/`autoWrap`,`fontWeight` 取 `bold`/`normal`,`hAlign` 取 `left`/`center`/`right`/`general`,`vAlign` 取 `top`/`middle`/`bottom`;背景色/字体颜色统一使用 `#RRGGBB` 格式 -- `new` 创建工作表时,如名称与已有工作表重复,系统会自动重命名 -- `find` 返回匹配单元格的地址(A1 表示法)和值,无匹配时返回空数组 -- `find` 的 `--match-entire-cell` 用于精确匹配:只返回单元格内容完全等于搜索文本的结果,不会匹配包含该文本的单元格(例如搜索"苹果"时,只匹配"苹果",不匹配"苹果手机""苹果汁"等)。用户说"精确搜索/完全匹配/只搜等于XX的"时必须使用此参数 -- `find` 的 `--match-case` 默认为 true(区分大小写),设为 false 可忽略大小写 -- `find` 的 `--use-regexp` 启用后,`--find` 参数作为正则表达式处理 -- ★ 当用户要求搜索/查找表格数据时,使用 `find` 命令,不要用 `range read` 读取全量数据后自行过滤——`find` 支持服务端搜索,效率更高、语义更准确 -- `append` 自动定位到最后一行有数据的位置下方插入,无需手动计算行号 -- `append` 的 `--values` 二维数组中每行的列数必须一致,否则会报错。如果用户提供的数据中各行长度不同,必须先将短行用空字符串 `""` 补齐到与最长行相同的列数后再调用。追加的数据列数也应与工作表已有数据列数保持一致 -- `append` vs `range update`:追加新行用 `append`,修改已有单元格用 `range update` -- `insert-dimension` 在指定位置之前插入空行或空列,不写入数据;如需在末尾追加行/列,使用 `append` -- `insert-dimension` 的 `--dimension` 只接受 `ROWS` 或 `COLUMNS` -- `insert-dimension` 的 `--position` 支持工作表前缀(如 `Sheet1!3`),此时忽略 `--sheet-id` -- `insert-dimension` 的 `--length` 最大为 5000 -- `delete-dimension` 从指定位置起删除若干连续的行或列,删除后后续行/列向前移动填补空位 -- `delete-dimension` 的 `--dimension` 只接受 `ROWS` 或 `COLUMNS` -- `delete-dimension` 的 `--position` 支持工作表前缀(如 `Sheet1!3`),此时忽略 `--sheet-id` -- `delete-dimension` 的 `--length` 最大为 5000 -- `delete-dimension` 若需仅清空内容但保留行/列占位,请使用 `range clear`(整片区域清除,比逐格写空更简洁) -- `update-dimension` 批量更新连续行/列的显隐状态与行高/列宽 -- `update-dimension` 的 `--dimension` 只接受 `ROWS` 或 `COLUMNS` -- `update-dimension` 的 `--start-index` 支持工作表前缀(如 `Sheet1!3`),此时忽略 `--sheet-id` -- `update-dimension` 的 `--length` 最大为 5000 -- `update-dimension` 的 `--hidden` 与 `--pixel-size` 至少必须提供一个 -- `update-dimension` 的 `--pixel-size` 单位为像素,`dimension=ROWS` 时表示行高、`dimension=COLUMNS` 时表示列宽 -- `update-dimension` 当同时提供 `--hidden` 与 `--pixel-size` 时,将先应用尺寸再应用显隐,任一失败整体失败 -- `merge-cells` 合并时只保留左上角单元格的值,其他单元格的值会被丢弃 -- `merge-cells` 的 `--merge-type` 不传时默认为 `mergeAll`(合并所有单元格) -- `merge-cells` 的 `--range` 支持带工作表前缀的写法(如 `Sheet1!A1:B3`),此时忽略 `--sheet-id` -- `merge-cells` 如果目标区域与其他合并单元格、锁定区域或表格区域存在交集,合并将失败 -- `media-upload` 是两步自动完成的流程 (获取附件上传凭证 → OSS 上传),无需手动分步操作 -- `write-image` 是三步自动完成的流程 (获取附件上传凭证 → OSS 上传 → 写入图片到单元格),无需手动分步操作 -- ★ 向表格单元格中写入图片必须使用 `write-image`,禁止使用 `range update`。`range update` 底层调用的 `update_range` MCP 工具不支持图片类型参数,调用会失败 -- `write-image` 与 `media-upload` 的区别:`media-upload` 仅上传附件到表格获取 resourceId;`write-image` 在上传后还会将图片写入指定单元格 -- `create-float-image` 创建浮动图片前必须先通过 `media-upload` 上传图片获取 `resourceUrl`,再将其作为 `--src` 传入。`--src` 的格式为 `/core/api/resources/img/...`,不能直接传外部 URL -- `create-float-image` 的 `--range` 使用 A1 表示法指定锚点单元格(如 `A1`、`B3`),支持带工作表前缀(如 `Sheet1!A1`) -- `create-float-image` 的 `--width` / `--height` 为必填,单位像素,必须为正整数;`--offset-x` / `--offset-y` 可选,默认 0,不能为负数 -- `write-image`(单元格内嵌图片)vs `create-float-image`(浮动图片):`write-image` 将图片写入单元格内部,占据单元格内容;`create-float-image` 创建悬浮于单元格之上的浮动图片,不占用单元格内容,可自由调整位置和大小 -- `update-float-image` 的 `--src` / `--range` / `--width` / `--height` / `--offset-x` / `--offset-y` 至少必须提供一个 -- `list-float-images` 返回 `floatImages` 数组和 `totalCount`,每个元素包含 `id`(用于后续 get / update / delete) -- `delete-float-image` 操作不可恢复,删除后图片将从工作表中移除 -- `replace` 的 `--find` 不能为空字符串,`--replace` 可以为空字符串(表示删除匹配内容) -- `replace` 的 `--match-case` 默认为 false(不区分大小写),与 `find` 的默认行为不同 -- ★ `replace` vs `range update`:需要批量替换文本时,必须使用 `replace` 命令,禁止用 `range update` 手动重写单元格来实现替换效果。`replace` 支持服务端全局替换,效率更高且会返回替换计数 -- ★ `move-dimension` vs `range update`:需要移动行或列时,必须使用 `move-dimension` 命令,禁止用 `range update` 读取数据后手动重写来模拟移动效果。`move-dimension` 是原子操作,能保留单元格的格式、合并状态等属性 -- `move-dimension` 的索引均为 0-based(第 1 行/列的索引为 0),`endIndex` 包含在移动范围内 -- `move-dimension` 的 `--destination-index` 不能在 [start-index, end-index] 范围内 -- `move-dimension` 的移动跨度(end-index - start-index + 1)不超过 5000 -- `move-dimension` 的 `--destination-index` 是目标位置的 0-based 索引,即移动到第 n 行/列则传 `n - 1`(通用公式:`destination-index = 目标行号(1-based) - 1`) -- `add-dimension` vs `range update`:需要在末尾追加空行/空列时,必须使用 `add-dimension` 命令,禁止用 `range update` 写空数据来模拟追加效果 -- `add-dimension` 追加的是空行/空列,与 `append`(追加带数据的行)不同 -- `add-dimension` 的 `--length` 必须为正整数(>= 1),行列均不超过 5000 -- `group-dimension` / `ungroup-dimension` 的 `--range` 只接受整行或整列范围,如 `3:7`、`C:F`、`Sheet1!3:7`,不支持 `A1:C5` -- `group-dimension` 的 `--group-state` 只接受 `expand` 或 `fold`,默认 `expand` -- `batch-update` 支持 `group-dimension` / `ungroup-dimension`;需要创建后立即折叠时,优先使用独立 `group-dimension --group-state fold` -- `unmerge-cells` 取消指定范围内所有合并单元格,使用 A1 表示法指定范围 -- `set-dropdown` 在指定范围内设置下拉列表,`--options` 为 JSON 数组,每个元素包含 `value`(必填)和 `color`(可选,`#RRGGBB` 格式)。选项值不能包含英文逗号。`--multi-select` 启用多选模式。如果目标范围已存在下拉列表,会被新配置覆盖 -- `get-dropdown` 查询指定范围内的下拉列表配置,返回 `dataValidations` 数组,相同选项的单元格聚合为一组。无下拉列表时 `hasDropdown` 为 false -- `delete-dropdown` 删除指定范围内的下拉列表配置,单元格恢复为普通文本格式。已填写的值不会被清除。目标范围不存在下拉列表时操作仍返回成功 -- ★ **全局筛选(filter)与筛选视图(filter-view)的区别**:全局筛选影响所有协作者看到的数据展示,每个工作表最多一个;筛选视图是个人化的,互不影响。用户只说"筛选"时默认走 `filter` 系列 -- `filter get` 获取工作表的全局筛选信息,返回 `range`(筛选范围)、`id` 和 `criteria`(各列条件对象,无条件时为 `{}`)。无筛选时返回空 -- `filter create` 创建全局筛选时 `--range` 必须包含表头行(如 `A1:E100`),不能只包含数据行。每个工作表只能有一个筛选,已存在时报错 -- `filter create` 的 `--criteria` 可选,不传则仅创建空筛选框架,后续通过 `filter update` 设置条件 -- `filter delete` 删除后所有筛选条件丢失且所有被隐藏行重新显示,不可恢复 -- `filter delete` 工作表没有筛选时调用会报错,应先 `filter get` 确认存在 -- `filter update` 是覆盖式:指定列的条件会被替换,未指定的列保持不变。如只想修改某一列,建议先 `filter get` 读取现有配置再 patch -- `filter update` 前置:工作表必须已创建筛选 -- `filter clear-criteria` 仅清除指定列的条件,不删除整个筛选。指定列无条件时不报错(幂等) -- `filter sort` 会实际改变数据行的物理顺序,不可撤销。前置:工作表必须已创建筛选 -- ★ **筛选操作规范**(参照飞书 core-operations): - - 当用户要求"筛选/只看/仅保留 X"时,**必须**通过 `filter create` / `filter update` 创建真实的筛选器。**禁止**用"删除不符合条件的行"或"新建工作表只放符合条件的行"来代替 - - 创建/更新筛选后**必须** `filter get` 回读验证配置正确 - - 更新已有筛选前先 `filter get` 读取当前配置,确认目标存在且了解现有条件后再操作 - - 筛选条件的列索引(`column`)必须与实际数据列精确对应,不要凭猜测填写 - - 筛选不支持正则表达式,传入正则会当成普通文本处理 -- `filter-view list` 获取指定工作表的所有筛选视图列表,返回的 `id` 可用于后续 info / update / delete / update-criteria / delete-criteria / list-criteria / get-criteria 的 `--filter-view-id` -- `filter-view info` 获取单个筛选视图的完整信息(含 criteria),内部复用 `get_filter_views` MCP 按 ID 过滤 -- `filter-view list-criteria` 列出指定筛选视图已设置的所有列条件,返回按列偏移量为 key 的对象;无条件时返回空对象 `{}` -- `filter-view get-criteria` 获取指定列的条件详情,`--column` 为列偏移量(从 0 开始);该列无条件时返回错误提示 -- `filter-view create` 创建筛选视图时 `--range` 应包含表头行。`--criteria` 可选,不传则创建后无筛选条件,后续可通过 `filter-view update-criteria` 设置 -- `filter-view update` 的 `--name`、`--range`、`--criteria` 至少需要传入一个,未指定的字段保持不变 -- `filter-view update` 的 `--criteria` 中指定列的条件会被替换,未指定的列保持不变 -- `filter-view delete` 删除后该视图及其所有筛选条件将被永久移除,不可恢复 -- `filter-view delete` 不影响全局筛选或其他筛选视图 -- `filter-view update-criteria` 的 `--column` 为列偏移量(从 0 开始),相对于筛选视图范围首列。例如筛选视图范围为 `B1:E10`,则 `--column 0` 代表 B 列 -- `filter-view update-criteria` 设置条件后立即在该筛选视图中生效,仅影响当前视图,不影响全局筛选或其他筛选视图 -- `filter-view update-criteria` 的 `--filter-criteria` 中 `conditions` 最多 2 个条件,多条件之间通过 `conditionOperator` 指定逻辑关系(`and` 或 `or`) -- `filter-view delete-criteria` 仅清除指定列的条件,不会删除整个筛选视图。如需删除整个筛选视图,请使用 `filter-view delete` -- `filter-view delete-criteria` 如果指定列没有设置筛选条件,调用不会报错 -- 筛选视图相关操作需要"可阅读"权限(list / info / list-criteria / get-criteria)或"可编辑"权限(create / update / delete / update-criteria / delete-criteria),不支持跨组织操作 -- ★ `export` 仅支持钉钉在线电子表格(axls)→ xlsx;传入钉钉文字文档会报 `invalidRequest.document.typeIllegal` -- ★ `export` 为单命令一站式,CLI 内部已自动完成「提交 → 渐进式退避轮询 → 可选下载」,**Agent 不得在外部实现轮询或重试**;命令返回成功后不再调用其他 export 相关命令 -- `export` 内置轮询策略:1~5 次间隔 2s、6~10 次间隔 5s、11~20 次间隔 10s、21~30 次间隔 15s,硬上限 30 次(约 5 分钟);超时后命令返回错误,告知用户稍后再试即可 -- ★ `export` 命令返回失败或超时时,**禁止自动重调 `dws sheet export`**;直接告知用户导出失败并建议稍后再试 -- `export` 未指定 `--output` 时,返回的 `downloadUrl` 具有时效性,获取后请尽快下载;若用户需要本地文件,优先直接传 `--output` 让 CLI 代为下载 -- `export` 的 `--output` 可为文件路径或已存在目录;为目录时自动从 `downloadUrl` 推断文件名,为文件路径时直接按该路径保存 -- 用户要求"导出表格/下载 xlsx"时,必须使用 `export` 单命令,禁止用 `range read` 读全量数据后自行拼 xlsx 模拟导出(服务端导出会保留格式/合并/公式等完整属性) -- `update` 的 `--title`、`--index`、`--hidden`、`--frozen-row-count`、`--frozen-column-count` 至少必须提供一个 -- `update` 的 `--title` 最长 100 字符,不能包含 `/ \ ? * [ ] :` 等特殊字符 -- `update` 的 `--index` 为 0-based 非负整数,0 表示移动到最前面 -- `update` 的 `--hidden` 设为 true 时,至少需要保留一个可见的工作表,不能将所有工作表都隐藏 -- `update` 的 `--frozen-row-count` / `--frozen-column-count` 为非负整数,不能超过工作表的总行数/列数,设为 0 表示取消冻结 -- `update` 当同时提供多个属性时,所有属性将在同一次请求中更新 -- `copy` 复制操作会将源工作表的所有内容(包括数据、格式、公式等)完整复制到新工作表 -- `copy` 的 `--title` 可选,不传时系统自动生成名称(通常为"源名称 副本"或类似格式) -- `copy` 的 `--title` 最长 100 字符,不能包含 `/ \ ? * [ ] :` 等特殊字符 -- `copy` 当指定名称与已有工作表重复时,系统会自动重命名为合法值 -- `copy` 的 `--index` 可选,不传时副本将放置在源工作表之后的默认位置 -- ★ 关键区分: sheet(电子表格/单元格读写) vs aitable(AI多维表/结构化记录/字段定义) vs doc(文档编辑/阅读) -- 工作表与单元格命令仅支持 `axls`;本地 `xlsx` / `xls` 可通过 `sheet import` 转换为新的在线电子表格 -- 遇到未知 `alidocs` URL 时,必须先 probe(`dws doc info --node --format json`)确认 `contentType` 和 `extension`,才能决定是否走 sheet -- 当节点 `extension=xlsx` / `xls` / `xlsm` / `csv`(`contentType≠ALIDOC`)时,必须用 `dws doc download --node --output <路径>` 先下载到本地再处理,禁止调用任何 sheet 子命令(sheet 底层 MCP 工具只识别 axls,调用 xlsx 节点必失败) -- 要把在线表格导出为 xlsx 文件,走 `dws sheet export`;要把本地 xlsx/xls 转为在线表格,走 `dws sheet import`;要读取已上传但未转换的 xlsx 节点,先走 `dws doc download` +- [局部意图消歧](../intent-guide.md)。 diff --git a/skills/mono/references/products/sheet/sheet-batch-operations.md b/skills/mono/references/products/sheet/sheet-batch-operations.md index 4096e71b..8d922bec 100644 --- a/skills/mono/references/products/sheet/sheet-batch-operations.md +++ b/skills/mono/references/products/sheet/sheet-batch-operations.md @@ -5,9 +5,8 @@ `batch-update` 把多次写入打包成单次请求,但每个子操作仍受编辑类任务硬性默认规则约束: 1. **目标 range 必须落在用户授权范围内**:除用户明示要修改的区域外,子操作禁止扩张到无关单元格 / 列 / 工作表。规划 range 时先确认每个子操作的边界。 -2. **批次完成后必须回读校验**:整个 `batch-update` 执行成功后,用 `range read` 或 `csv-get` 抽样回读受影响区域,至少校验 3-5 个代表性单元格(首 / 中 / 末),与本地预先计算的预期值对照。 +2. **批次完成后必须回读校验**:整个 `batch-update` 执行成功后,单元格值用 `range read` 或 `csv-get` 抽样回读受影响区域;合并用裸 `sheet info`,行列分组用 `sheet info --include groups` 回读校验。 3. **预期条数前置断言**:涉及"批量填充 N 行"或"对 M 个区域分别写入"时,先把 N、M 硬编码进代码,回读后断言实际等于预期;不一致就再发一轮 `batch-update` 补齐,禁止交付半成品。 -4. **统一确认语义**:`range batch-clear` 和 `batch-update` 都可删除内容或结构;先用 `--dry-run` 预览,真实执行必须获得用户确认后追加 `--yes`。 ## 使用场景 @@ -23,17 +22,28 @@ - 原子事务:任一操作失败则整批回滚 - 传 `--continue-on-error` 可切换为宽松模式(失败继续) +用户说"批量写入多个结构化 table/跨 sheet 写入 dataframe/table": +- 不使用 `batch-update`;直接调用 `dws sheet table-put --node --sheets '{"sheets":[...]}'` +- `table-put` 自身支持一次写入多个 sheet spec,写入后用 `table-get` 回读 dtype / format / 数据行数 + +用户说"批量创建行列分组/批量取消行列分组/同时调整尺寸并分组": +- 连续行/列分组 → `group-dimension` +- 取消连续行/列分组 → `ungroup-dimension` +- 分组回读用 `sheet info --include groups` +- `group-dimension` 在 batch 中只适合默认展开分组;需要创建后立即折叠时,用独立 `dws sheet group-dimension --group-state fold` + **何时推荐使用 `batch-update`**: - 需要对**多个**不同区域执行 `merge-cells` / `unmerge-cells` 时(如按分组合并多列相同内容) - 需要对**多个**不同区域执行 `update-dimension` 时(如统一调整多列列宽或多行行高,含 hidden + pixelSize) +- 需要对**多个**连续行/列范围执行 `group-dimension` / `ungroup-dimension` 时(默认展开分组) - 需要先插入行列再写入数据时(`add-dimension` + `range update` / `csv-put`) -- 需要批量创建或取消行列分组时(`group-dimension` / `ungroup-dimension`) - 需要对多个区域执行不同写入操作时(多次 `range update` + `range clear` 等组合) 当同一工具需要对多个区域重复调用时,**推荐**改用 `batch-update` 合并为单次请求——`batch-update` 是原子提交(要么全成功要么整批回滚);逐个调用非原子,中途失败会留下半成品。 **不可放进 `--operations` 的操作**(强行写入会被校验拒或行为未定义): -- `range read` / `csv-get`(只读操作,不属于写入) +- `range read` / `csv-get` / `table-get`(读取操作,不在 batch dispatch 表中;结构化 table 回读需在 batch 外单独调用) +- `table-put`(当前 `batch-update` 不支持结构化 table 写入;请直接调用独立 `dws sheet table-put`) - `range sort` / `range move-to`(尚未支持,后续版本补充) - `write-image` / `media-upload` / `create-float-image` / `update-float-image`(需本地上传或依赖前置上传句柄) - `set-style` / `range batch-set-style`(自身已是批量入口,不可再嵌套) @@ -50,9 +60,9 @@ Usage: dws sheet range batch-clear [flags] Example: - dws sheet range batch-clear --node --ranges '["Sheet1!A1:B3","Sheet2!C1:D5"]' --yes - dws sheet range batch-clear --node --ranges '["Sheet1!A1:Z1000"]' --type all --yes - dws sheet range batch-clear --node --ranges '["Sheet1!A2:A100","Sheet1!C2:C100"]' --type format --yes + dws sheet range batch-clear --node --ranges '["Sheet1!A1:B3","Sheet2!C1:D5"]' + dws sheet range batch-clear --node --ranges '["Sheet1!A1:Z1000"]' --type all + dws sheet range batch-clear --node --ranges '["Sheet1!A2:A100","Sheet1!C2:C100"]' --type format Flags: --node string 表格文档 ID 或 URL (必填) --ranges string 目标区域 JSON 数组,每项带 sheet 前缀 (必填) @@ -72,9 +82,10 @@ Example: {"toolName":"range clear","input":{"sheet-id":"Sheet1","range":"A1:B3","type":"content"}}, {"toolName":"range update","input":{"sheet-id":"Sheet1","range":"A1","values":[[{"type":"text","text":"hello"}]]}}, {"toolName":"merge-cells","input":{"sheet-id":"Sheet1","range":"A1:B1","merge-type":"mergeAll"}}, - {"toolName":"update-dimension","input":{"sheet-id":"Sheet1","dimension":"ROWS","start-index":"1","length":1,"pixel-size":40}} - ]' --yes - dws sheet batch-update --node --continue-on-error --operations '[...]' --yes + {"toolName":"update-dimension","input":{"sheet-id":"Sheet1","dimension":"ROWS","start-index":"1","length":1,"pixel-size":40}}, + {"toolName":"group-dimension","input":{"sheet-id":"Sheet1","range":"3:7","group-state":"expand"}} + ]' + dws sheet batch-update --node --continue-on-error --operations '[...]' Flags: --node string 表格文档 ID 或 URL (必填) --operations string 操作数组 JSON (必填,每项 {toolName, input}) @@ -91,7 +102,8 @@ Notes: - 当需要对多个区域执行相同清除时,优先使用 `range batch-clear`(更简洁) - `csv-put` 子操作与独立命令语义一致:CSV 字段值以 `=` 开头时按公式解析;前加单引号时写入以 `=` 开头的字面文本 - 典型场景:先插入行列再写入数据、先清除再写入、批量合并+调整行高列宽 - - 真实执行必须获得用户确认后加 --yes;--dry-run 不需要 --yes + - `group-dimension` 在 batch 中只适合默认展开分组;需要 `--group-state fold` 时请使用独立 `dws sheet group-dimension` + - `table-put` 不支持放进 batch-update;结构化 table 请用独立 `dws sheet table-put` ``` ### 子操作定位规则 @@ -107,6 +119,8 @@ Notes: ] ``` +`table-put` / `table-get` 不支持放进 `batch-update`。结构化 table 写入请直接调用 `dws sheet table-put`,写入完成后在 batch 外单独调用 `table-get` 回读校验。 + ## 校验与预览 ### Validate 阶段 @@ -121,27 +135,10 @@ CLI 在发送请求前执行以下本地校验(不消耗网络请求): | 每项必须是 object | `operations[N] 不是 object` | `--operations '["string"]'` | | `toolName` 必须是支持的 CLI 命令名 | `unsupported toolName "xxx"` | 传 `"batch-update"`(禁止嵌套)或拼写错误 | | 禁止嵌套 `batch-update` | 同上(`batch-update` 不在 dispatch 表中,自动拦截) | `toolName: "batch-update"` | +| 禁止 `table-put` / `table-get` | `unsupported toolName "table-put"` | 结构化 table 应使用独立 `table-put` | > `input` 内的字段(如 `sheet-id`、`range`、`values` 等)由服务端校验,CLI 不提前拦截——传入空值或缺失必填字段时,请求会到达服务端再返回错误。 -### DryRun 阶段 - -加 `--dry-run` 预览翻译后的完整 MCP 请求,不实际执行任何操作: - -```bash -dws sheet batch-update --node NODE_ID --dry-run --operations '[...]' -``` - -DryRun 输出内容: -- `Tool: batch_update`(最终调用的 MCP 工具名) -- `Arguments:` 完整 JSON(含翻译后的 `operations` 数组,展示每个子操作的 MCP toolName + 参数名 + 参数值) -- 若传了 `--continue-on-error`,输出 JSON 中会包含 `"continueOnError": true` - -DryRun 适用场景: -- 确认 CLI 命令名 → MCP toolName 的翻译是否正确 -- 确认参数名转换(如 `--range` → `rangeAddress`)是否符合预期 -- 检查 `csv-put` 的 `@filepath` 是否被正确解析为文件内容,并确认 `=...` / `'=...` 字段原样进入子操作 - ### --continue-on-error 行为 | 模式 | flag | 子操作失败时 | 返回格式 | @@ -149,7 +146,7 @@ DryRun 适用场景: | 严格事务(默认) | 不传 | 整批回滚到初始状态,后续子操作不执行 | 顶层 `error` 对象 | | 宽松模式 | `--continue-on-error` | 已执行的子操作不回滚,继续执行后续子操作 | `results` 数组 | -**严格事务失败的错误信息**由服务端(flex-table-app)生成,包含以下内容: +**严格事务失败的错误信息**由服务端生成,包含以下内容: - 失败操作索引:`operations[N]`(0-based,即 `--operations` JSON 数组中的下标) - 回滚告知:"All previously executed operations have been rolled back." - 失败原因:原始错误码和错误消息 @@ -177,13 +174,13 @@ Cause: The requested resource was not found by the identifier 'NonExistentSheet' | `merge-cells` | [sheet-style-format](./sheet-style-format.md) | | `unmerge-cells` | [sheet-style-format](./sheet-style-format.md) | | `update-dimension` | [sheet-dimension-operations](./sheet-dimension-operations.md) | +| `group-dimension` | [sheet-dimension-operations](./sheet-dimension-operations.md) | +| `ungroup-dimension` | [sheet-dimension-operations](./sheet-dimension-operations.md) | | `range fill` | [sheet-range-operations](./sheet-range-operations.md) | | `range copy-to` | [sheet-range-operations](./sheet-range-operations.md) | | `add-dimension` | [sheet-dimension-operations](./sheet-dimension-operations.md) | | `delete-dimension` | [sheet-dimension-operations](./sheet-dimension-operations.md) | | `move-dimension` | [sheet-dimension-operations](./sheet-dimension-operations.md) | -| `group-dimension` | [sheet-dimension-operations](./sheet-dimension-operations.md) | -| `ungroup-dimension` | [sheet-dimension-operations](./sheet-dimension-operations.md) | | `set-dropdown` | [sheet-dropdown](./sheet-dropdown.md) | | `delete-dropdown` | [sheet-dropdown](./sheet-dropdown.md) | | `csv-put` | [sheet-write-data](./sheet-write-data.md) | @@ -237,32 +234,44 @@ Cause: The requested resource was not found by the identifier 'NonExistentSheet' ] ``` -### 批量创建行列分组 +### 批量创建/取消行列分组 + +分组属于工作表结构元数据,批次完成后用 `sheet info --include groups` 回读 `rowGroups` / `columnGroups`,不要用裸 `sheet info` 或 `range read` 校验。回读项中的 `level` 是 1-based,`collapsed` 表示当前折叠状态。 ```jsonc [ {"toolName":"group-dimension", "input":{"sheet-id":"...","range":"3:7","group-state":"expand"}}, {"toolName":"group-dimension", - "input":{"sheet-id":"...","range":"C:F"}}, - {"toolName":"ungroup-dimension", - "input":{"sheet-id":"...","range":"10:12"}} + "input":{"sheet-id":"...","range":"C:F","group-state":"expand"}} ] ``` +```jsonc +[ + {"toolName":"ungroup-dimension", + "input":{"sheet-id":"...","range":"3:7"}}, + {"toolName":"ungroup-dimension", + "input":{"sheet-id":"...","range":"C:F"}} +] +``` + +注意:batch 中不要用 `group-state:"fold"` 来创建折叠分组;需要折叠初始状态时,用独立 `dws sheet group-dimension --group-state fold`。 + ## 上下文传递 | 操作 | 从返回中提取 | 用于 | |------|-------------|------| | `list` | 工作表的 `sheetId` | batch-clear 的 --ranges sheet 前缀 / batch-update 子操作 input 的 sheet-id | -| `batch-update` | 各子操作的执行结果 | 回读校验(range read / csv-get) | +| `batch-update` | 各子操作的执行结果 | 回读校验(值用 range read / csv-get;合并用 sheet info;分组用 sheet info --include groups) | ## 注意事项 - ★ **`--sheet-id` 获取规范(强制)**:`sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询真实的 `sheetId` / 工作表名称后再调用,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等) - ★ **需要对多个区域执行相同清除操作时,用 `range batch-clear`**:一次原子请求清除多个区域(可跨工作表),失败时整批回滚 - ★ **需要组合多个不同写操作(清除+写入等)时,用 `batch-update`**:原子事务,任一操作失败则整批回滚,避免留下半成品 -- ★ **批次完成后必须回读校验**:用 `range read` 或 `csv-get` 抽样回读受影响区域,至少校验 3-5 个代表性单元格(首 / 中 / 末),与预期值对照 +- ★ **table 写入不进 batch-update**:当前 `batch-update` 不支持结构化 table 写入;结构化 table 请直接调用 `dws sheet table-put` +- ★ **批次完成后必须回读校验**:值写入用 `range read` 或 `csv-get` 抽样回读受影响区域;结构变更用 `sheet info` 回读 - ★ **`batch-update` 不支持嵌套**:`--operations` 中的 `toolName` 必须是原子操作,不可再嵌套 `batch-update` -- `batch-update` 支持 `group-dimension` / `ungroup-dimension`;`input.range` 只接受整行或整列范围,如 `3:7`、`C:F` -- 需要创建后立即折叠分组时,优先使用独立 `dws sheet group-dimension --group-state fold` +- `batch-update` 支持 `group-dimension` / `ungroup-dimension`;分组结果用 `sheet info --include groups` 回读 +- `batch-update` 中 `group-dimension` 只适合默认展开分组;需要 `fold` 初始状态时使用独立 `group-dimension` diff --git a/skills/mono/references/products/sheet/sheet-chart.md b/skills/mono/references/products/sheet/sheet-chart.md index 0295faae..df84da69 100644 --- a/skills/mono/references/products/sheet/sheet-chart.md +++ b/skills/mono/references/products/sheet/sheet-chart.md @@ -34,6 +34,7 @@ | "百分比面积" | 百分比堆积面积图(areaPercentStacked) | 占比式面积 | | "散点图"、"相关性" | 散点图(scatter) | 两变量相关性 | | "雷达图"、"多维度" | 雷达图(radar) | 多维度对比 | +| "柱+线"、"组合图"、"双轴" | 组合图(combo) | 多种图表叠加,支持副轴 | **多图表需求**:当用户同时提到多种分析(如"统计占比 + 对比数量"),必须创建多个图表,每个对应一种类型,不要只做一个。 @@ -55,8 +56,6 @@ Flags: --chart-id string 浮动图表 ID (可选,不传则返回全部) ``` -- **判空看 `totalCount` / `message`,不要看数组长度**:无图表时 `floatCharts` 数组里仍会带 1 个全 null 的幽灵元素(`chart.category`/`chart.series` 全 null,无 id),但 `totalCount` 为 0、`message` 为「Successfully retrieved 0 float chart(s).」。判断有没有图表以 `totalCount` 为准,或过滤掉没有 `id` 的元素。 - ### 创建浮动图表 ``` Usage: @@ -130,7 +129,7 @@ Flags: | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | `row` | number | 是 | 锚点行索引(0-based),如 0 表示第一行 | -| `col` | string | 是 | 锚点列。使用字母表示法(`"A"`=第1列, `"B"`=第2列, `"Z"`=第26列, `"AA"`=第27列),不支持数字形式 | +| `col` | string/number | 是 | 锚点列。支持字母表示法(`"A"`=第1列, `"B"`=第2列, `"Z"`=第26列, `"AA"`=第27列)或 0-based 数字(`0`=第1列) | **offset 字段**(可选,不传时默认 0): @@ -150,9 +149,10 @@ Flags: | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| -| `type` | string | 是 | 图表类型,枚举值:`column` / `bar` / `columnStacked` / `barStacked` / `line` / `lineStacked` / `area` / `areaStacked` / `areaPercentStacked` / `pie` / `doughnut` / `scatter` / `radar` | -| `series` | object[] | 是 | 数据系列数组,至少一项,详见下方 series 字段表 | -| `category` | string[] | 否 | 分类轴(X轴)数据区域,A1 表示法字符串数组(如 `["A2:A10"]`) | +| `type` | string | 是 | 图表类型,枚举值:`column` / `bar` / `columnStacked` / `barStacked` / `line` / `lineStacked` / `area` / `areaStacked` / `areaPercentStacked` / `pie` / `doughnut` / `scatter` / `radar` / `combo` | +| `series` | object[] | 非 combo 必填 | 数据系列数组,至少一项,详见下方 series 字段表。combo 类型不使用此字段 | +| `subCharts` | object[] | combo 必填 | 组合图子图数组,至少一项,详见下方 subCharts 字段表。仅 combo 类型使用 | +| `category` | string[] | 否 | 分类轴(X轴)数据区域,A1 表示法字符串数组(如 `["A2:A10"]`)。combo 类型在 subCharts 内声明 | | `title` | object | 否 | 图表标题配置,详见下方 title 字段表 | | `legend` | object | 否 | 图例配置,详见下方 legend 字段表 | | `catAx` | object | 否 | 分类轴(X轴)样式配置,详见下方轴配置表 | @@ -165,6 +165,15 @@ Flags: | `name` | string | 否 | 系列名称的单元格引用,A1 表示法(如 `"B1"`)。不传时图例显示为"系列1"等默认名 | | `value` | string[] | 是 | 系列数据区域,A1 表示法字符串数组(如 `["B2:B10"]`)。每个元素为一段连续区域 | +**subCharts 数组项**(仅 combo 类型): + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `type` | string | 是 | 子图类型,仅允许笛卡尔坐标系类型:`column` / `bar` / `columnStacked` / `barStacked` / `line` / `lineStacked` / `area` / `areaStacked` / `areaPercentStacked`。不允许 pie/doughnut/scatter/radar/combo | +| `series` | object[] | 是 | 子图数据系列,格式同上方 series 数组项 | +| `category` | string[] | 是 | 子图分类轴数据区域(笛卡尔坐标系必须提供) | +| `useSecondaryAxis` | boolean | 否 | 是否使用副轴(右侧 Y 轴),默认 false | + **title 字段**: | 字段 | 类型 | 必填 | 说明 | @@ -199,7 +208,7 @@ Flags: - 格式:`"A2:B10"`(默认使用 `--sheet-id` 对应的工作表) - 跨 sheet 引用:`"SHEET_ID!A2:B10"`(前缀为工作表 ID) - 单元格引用:`"B1"`(等同于 `"B1:B1"`) -- MCP 内部自动将 A1 引用转换为 ISheetRange 结构,CLI 直接透传即可 +- 直接传 A1 表示法字符串即可,命令会处理成图表所需的区域引用 ## 图表位置选择(创建前必做) @@ -277,32 +286,19 @@ dws sheet chart create --node --sheet-id --properties '{ } }' -# ── 工作流 4: 多图表组合分析 ── -# 当用户需要"柱+线"等多种图表组合分析时,分别创建对应类型的图表: +# ── 工作流 4: 创建组合图(combo)── +# 当用户需要"柱+线"叠加、双轴对比时,使用 combo 类型: -# 创建柱形图 dws sheet chart create --node --sheet-id --properties '{ "position": {"row": 40, "col": "A"}, - "dimensions": {"width": 700, "height": 450}, + "dimensions": {"width": 800, "height": 500}, "chart": { - "type": "column", - "series": [{"name": "B1", "value": ["B2:B10"]}], - "category": ["A2:A10"], - "title": {"show": true, "text": "销售额"}, - "legend": {"show": true, "pos": "t"} - } -}' - -# 创建折线图 -dws sheet chart create --node --sheet-id --properties '{ - "position": {"row": 40, "col": "J"}, - "dimensions": {"width": 700, "height": 450}, - "chart": { - "type": "line", - "series": [{"name": "C1", "value": ["C2:C10"]}], - "category": ["A2:A10"], - "title": {"show": true, "text": "增长率"}, - "legend": {"show": true, "pos": "t"} + "type": "combo", + "subCharts": [ + {"type": "column", "series": [{"name": "B1", "value": ["B2:B10"]}], "category": ["A2:A10"]}, + {"type": "line", "series": [{"name": "C1", "value": ["C2:C10"]}], "category": ["A2:A10"], "useSecondaryAxis": true} + ], + "title": {"show": true, "text": "销售额与增长率"} } }' ``` @@ -344,10 +340,10 @@ dws sheet chart update --node --sheet-id --chart-id --format json` 查询,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等) - [强制] **创建后必须验证**:图表创建后必须调用 `chart list` 验证配置是否正确 -- ⚠️ **`chart list` 判空看 `totalCount` 不看数组长度**:无图表时 `floatCharts` 仍含 1 个全 null 幽灵元素(无 id),`totalCount` 为 0、`message` 说明为空。判断有没有图表以 `totalCount` 为准或过滤无 `id` 的元素 - [强制] **chart-id 禁止臆测**:必须通过 `chart list` 获取真实的图表 ID,不可编造 -- **图表类型映射**:用户说"柱形图"用 `column`,"条形图"(横向)用 `bar`,"散点图"用 `scatter`,"雷达图"用 `radar`,"环形图"用 `doughnut` +- **图表类型映射**:用户说"柱形图"用 `column`,"条形图"(横向)用 `bar`,"散点图"用 `scatter`,"雷达图"用 `radar`,"环形图"用 `doughnut`,"柱+线/组合/双轴"用 `combo` - **堆积类型后缀**:堆叠版本的图表在基本类型后加 `Stacked`(如 `columnStacked`、`lineStacked`、`areaStacked`) +- **combo 组合图**:不用顶层 `series`/`category`,改用 `chart.subCharts` 数组,每个子图独立声明 `type`/`series`/`category`,可选 `useSecondaryAxis: true` 指定副轴。子图 type 仅允许笛卡尔坐标系类型(column/bar/line/area 及其 Stacked 变体),不允许 pie/doughnut/scatter/radar/combo - **位置不得压在数据区上**:图表浮层会遮挡原始数据,必须放在空白区域 - **多图表需求**:多次调用 `chart create`,不要试图用一个图表包含所有分析 - **跨 sheet 引用数据**:在 A1 区域前加 `SHEET_ID!` 前缀,如 `"sheetXXX!A2:B10"` diff --git a/skills/mono/references/products/sheet/sheet-comment.md b/skills/mono/references/products/sheet/sheet-comment.md new file mode 100644 index 00000000..985e9813 --- /dev/null +++ b/skills/mono/references/products/sheet/sheet-comment.md @@ -0,0 +1,166 @@ +# sheet comment(表格单元格评论:list / create / reply / update / delete) + +> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件: +> 1. [`../sheet.md`](../sheet.md) — 命令路由 + 场景索引 + 意图判断 + 全局约束 +> +> **同任务常配合**:`dws aisearch person`(查 `--mention` 用 userId)/ [`sheet-workbook.md`](sheet-workbook.md)(未知工作表名称时先 `list` 确认真实名称) + +--- + +## 适用范围 + +- 仅支持钉钉在线电子表格(`extension=axls`)。`xlsx` / `xls` / `csv` 等本地表格不支持评论。 +- 评论锚定在**单元格位置**:`create` / `list` 通过 `--sheet-id`(工作表 ID 或名称)+ `--range`(单元格坐标)定位;`reply` / `update` / `delete` 通过 `--comment-key` 操作,不依赖单元格位置。 +- `create` / `list` 通过单元格位置定位;`reply` / `update` / `delete` 通过前一步返回的 `commentKey` 定位评论线程,不需要重新传单元格位置。 +- 评论正文与单元格位置由服务端关联。Agent 只使用命令返回的 `commentKey` 继续回复、更新或删除,不解析或拼接内部关联字段。 + +--- + +## sheet comment list(查询表格评论列表) + +``` +Usage: + dws sheet comment list [flags] +Example: + dws sheet comment list --node + dws sheet comment list --node --sheet-id Sheet1 --range A2 + dws sheet comment list --node --resolve-status unresolved + dws sheet comment list --node --cursor +Flags: + --node string 目标表格的标识,支持传入 URL 或 ID (必填) + --limit int 每页返回的评论数量,默认 50,最大 50 + --cursor string 分页游标,从上一次请求的返回结果中获取 (首次请求不传) + --resolve-status string 按解决状态过滤: resolved (已解决) / unresolved (未解决) + --sheet-id string 工作表 ID 或名称,如 Sheet1(与 --range 一起指定时按单元格过滤) + --range string 单元格位置,A1 表示法,如 A2、B5:C10(与 --sheet-id 一起指定时按单元格过滤) +``` + +- 同时传入 `--sheet-id` 和 `--range` 时,仅返回该单元格的评论(服务端按单元格精确过滤);不传则返回表格全部单元格评论。 + +--- + +## sheet comment create(创建单元格评论) + +``` +Usage: + dws sheet comment create [flags] +Example: + dws sheet comment create --node --sheet-id Sheet1 --range A2 --content "这个数字有问题" + dws sheet comment create --node --sheet-id Sheet1 --range A2 --content "请核实" --mention uid1,uid2 +Flags: + --node string 目标表格的标识,支持传入 URL 或 ID (必填) + --sheet-id string 工作表 ID 或名称,如 Sheet1 (必填) + --range string 单元格位置,A1 表示法,仅支持单个单元格,如 A2 (必填) + --content string 评论的文字内容,纯文本 (必填) + --mention string 被 @ 的用户 uid 列表,逗号分隔 +``` + +- 未知工作表名称时,先 `dws sheet list --node --format json` 确认真实名称,禁止臆测 `Sheet1` / `0` / `default`。 + +--- + +## sheet comment reply(回复评论) + +``` +Usage: + dws sheet comment reply [flags] +Example: + dws sheet comment reply --node --comment-key --content "已核实" + dws sheet comment reply --node --comment-key --content "比心" --emoji + dws sheet comment reply --node --comment-key --content "请确认" --mention uid1,uid2 +Flags: + --node string 目标表格的标识,支持传入 URL 或 ID (必填) + --content string 回复的文字内容,表情回复时填写表情名称 (必填) + --comment-key string 被回复评论的 commentKey,格式: {13位毫秒时间戳}{32位UUID},可从 list/create 结果获取 (必填) + --emoji 设为 true 时作为表情贴图回复 (默认 false) + --mention string 被 @ 的用户 uid 列表,逗号分隔 +``` + +- 回复自动归属到被回复评论所在的单元格线程,无需再传 `--sheet-id` / `--range`。 + +--- + +## sheet comment update(更新评论) + +``` +Usage: + dws sheet comment update [flags] +Example: + dws sheet comment update --node --comment-key --content "已按最新数据修正" +Flags: + --node string 目标表格的标识,支持传入 URL 或 ID (必填) + --comment-key string 待更新评论的 commentKey,格式: {13位毫秒时间戳}{32位UUID},可从 list/create 结果获取 (必填) + --content string 更新后的评论文字内容,纯文本 (必填) +``` + +--- + +## sheet comment delete(删除评论) + +> [强制] 危险操作:删除不可恢复。必须先向用户展示操作摘要并获得明确同意,用户同意后才加 `--yes` 执行。 + +``` +Usage: + dws sheet comment delete [flags] +Example: + dws sheet comment delete --node --comment-key --yes +Flags: + --node string 目标表格的标识,支持传入 URL 或 ID (必填) + --comment-key string 待删除评论的 commentKey,格式: {13位毫秒时间戳}{32位UUID},可从 list/create 结果获取 (必填) +``` + +## 关键说明 + +- `--mention` 接受 `userId` 列表(逗号分隔),需要先用 `dws aisearch person --keyword "<姓名>" --dimension name` 拿到 userId。 +- `--comment-key` 是 13 位毫秒时间戳 + 32 位 UUID 的拼接字符串,从 `list` / `create` 返回中提取,用于 `reply` / `update` / `delete`。 +- `create` / `list`(按单元格过滤)的 `--sheet-id` 是工作表 ID 或名称;未知时先 `dws sheet list` 确认,禁止臆测。 +- `reply` 加 `--emoji` 时 `--content` 填表情名称(如 `比心`、`赞`),不是文字内容。 +- `delete` 是不可逆操作;AI Agent 必须先向用户展示操作摘要并获得明确同意,同意后才追加 `--yes`。 + +## 上下文传递 + +| 从返回中提取 | 用于 | +|-------------|------| +| `commentList[].commentKey` | `comment reply/update/delete` 的 `--comment-key` | +| `comment create` 的 `commentKey` | `comment reply/update/delete` 的 `--comment-key` | +| [`sheet-workbook.md`](sheet-workbook.md) `sheet list` 的工作表名称 | `comment create/list` 的 `--sheet-id` | +| `dws aisearch person` 的 `userId` | `comment create/reply` 的 `--mention` | + +## 常用模板 + +```bash +# 查看表格全部单元格评论 +dws sheet comment list --node --format json + +# 仅看某个单元格的评论 +dws sheet comment list --node --sheet-id Sheet1 --range A2 --format json + +# 仅看未解决的评论 +dws sheet comment list --node --resolve-status unresolved --format json + +# 在单元格上创建评论(未知工作表名先 sheet list 确认) +dws sheet list --node --format json +dws sheet comment create --node --sheet-id Sheet1 --range A2 --content "这个数字有问题" --format json + +# 创建评论 + @人(先 aisearch person 拿 userId) +dws aisearch person --keyword "张三" --dimension name --format json +dws sheet comment create --node --sheet-id Sheet1 --range A2 --content "请确认" --mention , --format json + +# 文字回复 +dws sheet comment reply --node --comment-key --content "已核实" --format json + +# 表情回复(--content 填表情名称) +dws sheet comment reply --node --comment-key --content "比心" --emoji --format json + +# 更新评论 +dws sheet comment update --node --comment-key --content "已按最新数据修正" --format json + +# 删除评论(不可逆;必须用户确认后再加 --yes) +dws sheet comment delete --node --comment-key --yes --format json +``` + +## 参考 + +- [`../sheet.md`](../sheet.md)(如何路由到本命令族) +- [`./sheet-workbook.md`](sheet-workbook.md)(取工作表名称) +- `dws aisearch person`(取 mention 用的 userId,跨产品命令) diff --git a/skills/mono/references/products/sheet/sheet-conditional-format.md b/skills/mono/references/products/sheet/sheet-conditional-format.md index 70bc77b7..ccee67c4 100644 --- a/skills/mono/references/products/sheet/sheet-conditional-format.md +++ b/skills/mono/references/products/sheet/sheet-conditional-format.md @@ -64,14 +64,13 @@ **正确做法(两步走)**: ```bash -# Step 1: 用 range update 在新列写判断公式(形成"是/否"辅助列) -# 每个单元格必须是 object,公式写在 text(以 = 开头);不支持裸字符串 -dws sheet range update --node NODE_ID --sheet-id SHEET_ID --range "H2:H100" \ - --values '[[{"type":"text","text":"=IF(A2>B2,\"是\",\"否\")"}],...]' +# Step 1: 用 range update 在新列写判断公式(形成"是/否"辅助列,以 H2:H4 为例) +dws sheet range update --node NODE_ID --sheet-id SHEET_ID --range "H2:H4" \ + --values '[[{"type":"text","text":"=IF(A2>B2,\"是\",\"否\")"}],[{"type":"text","text":"=IF(A3>B3,\"是\",\"否\")"}],[{"type":"text","text":"=IF(A4>B4,\"是\",\"否\")"}]]' # Step 2: 基于辅助列值做条件格式(用 formulaCondition 引用辅助列) dws sheet cond-format create --node NODE_ID --sheet-id SHEET_ID \ - --ranges '["A2:H100"]' \ + --ranges '["A2:H4"]' \ --condition '{"formulaCondition":{"formula":"=$H2=\"是\""}}' \ --cell-style '{"backgroundColor":"#FFECEC"}' ``` @@ -143,8 +142,7 @@ Flags: - **用途**:查看指定工作表中已有的条件格式规则,或获取单个规则的详情。 - **场景**:创建/更新/删除条件格式前后验证规则状态;获取 ruleId 供后续 update/delete 使用。 -- **返回**:`rules` 数组,每条规则包含 id、type、ranges、条件参数、cellStyle/dataBarStyle 等。 -- **判空要看 `message`,不要看数组长度**:无规则时 `rules` 数组里仍会带 1 个全 null 的幽灵元素(`colorScaleCondition`/`dataBarStyle`/`iconSetCondition` 全 null,无 id),`message` 会是「No conditional formatting rules found.」。该命令**没有 `totalCount` 字段**,判空以 message 为准,或过滤掉没有 `id` 的元素。 +- **返回**:rules 数组,每条规则包含 id、type、ranges、条件参数、cellStyle/dataBarStyle 等。 ### 创建条件格式规则 ``` @@ -271,4 +269,3 @@ Flags: - **辅助列+条件格式两步走**:用户明确要求"辅助列"时,必须按两步走(先建辅助列 → 再基于辅助列做条件格式),禁止直接用 `formulaCondition` 一步绕过 - **大数据量优势**:当数据量 > 1000 行时,条件格式是首选——它由服务端自身渲染,不需要逐行调用 `range set-style`,性能远优于静态样式写入 - **判断标准**:交付后 `cond-format list` 必须能返回该规则;否则视为违规 -- ⚠️ **`cond-format list` 判空看 `message` 不看数组长度**:无规则时 `rules` 仍含 1 个全 null 幽灵元素(无 id),`message` 为「No conditional formatting rules found.」。该命令无 `totalCount`,判空以 message 为准或过滤无 `id` 的元素 diff --git a/skills/mono/references/products/sheet/sheet-dimension-operations.md b/skills/mono/references/products/sheet/sheet-dimension-operations.md index 21f33859..c039f548 100644 --- a/skills/mono/references/products/sheet/sheet-dimension-operations.md +++ b/skills/mono/references/products/sheet/sheet-dimension-operations.md @@ -352,6 +352,6 @@ dws sheet update-dimension --node --sheet-id \ - `add-dimension` 的 `--length` 必须为正整数(>= 1),行列均不超过 5000 - `group-dimension` / `ungroup-dimension` 的 `--range` 只接受整行或整列范围,不接受普通单元格矩形 - `group-dimension` 输出和 `sheet info --include groups` 回读均使用 `level`,且为 1-based;不要使用旧的 `depth` 字段 -- `sheet info --include groups` 是分组回读入口;`range read` / `csv-get` 不返回行列分组 +- `sheet info --include groups` 是分组回读入口;裸 `sheet info` 不承诺返回行列分组,`range read` / `csv-get` 不返回行列分组 - 当前不能直接调整已有分组的 `collapsed` 状态;`--group-state fold` 只在创建分组时生效 - `batch-update` 支持 `group-dimension` / `ungroup-dimension`,但不适合创建后立即折叠;需要 `fold` 时使用独立 `group-dimension` diff --git a/skills/mono/references/products/sheet/sheet-dropdown.md b/skills/mono/references/products/sheet/sheet-dropdown.md index 173594e2..0b135aa4 100644 --- a/skills/mono/references/products/sheet/sheet-dropdown.md +++ b/skills/mono/references/products/sheet/sheet-dropdown.md @@ -55,11 +55,10 @@ Flags: --range string 查询范围,A1 表示法,如 A1:A100 (必填) ``` -查询指定范围内的下拉列表配置信息,包括选项值和颜色。 +查询指定范围内的下拉列表配置信息,包括选项值、颜色和是否多选。 - **用途**:查看单元格已设置的下拉列表选项和配置。 - **场景**:在修改下拉列表前先查询现有配置;确认下拉列表是否设置成功。 -- **返回**:`dataValidations` 数组,相同选项的单元格聚合为一组,每组包含 `conditionValues`(选项值)、`ranges`(覆盖范围)、`options`(含 `multipleValues` 和 `colorValueMap`)。**判空以 `hasDropdown` 为准**:范围内无下拉列表时 `hasDropdown` 为 false,但 `dataValidations` 里仍会带 1 个全 null 的幽灵条目,不要用数组长度判空。 -- ⚠️ **已知限制**:`options.multipleValues` 服务端恒返回 `null`(即使该下拉是用 `--multi-select` 建的),也不返回 `enableMultiSelect` 字段——**`get-dropdown` 读回无法判断该下拉是否多选**。要判断是否多选,改用 `range read`:它返回的 `cells[].dataValidation.enableMultiSelect` 字段是准确的(实测有效)。 +- **返回**:`dataValidations` 数组,相同选项的单元格聚合为一组,每组包含 `conditionValues`(选项值)、`ranges`(覆盖范围)、`options`(含 `enableMultiSelect` 和 `colorValueMap`)。范围内无下拉列表时 `hasDropdown` 为 false。 ### 删除下拉列表 ``` @@ -91,6 +90,5 @@ Flags: - ★ **`--sheet-id` 获取规范(强制)**:`sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等) - `set-dropdown` 在指定范围内设置下拉列表,`--options` 为 JSON 数组,每个元素包含 `value`(必填)和 `color`(可选,`#RRGGBB` 格式)。选项值不能包含英文逗号。`--multi-select` 启用多选模式。如果目标范围已存在下拉列表,会被新配置覆盖 -- `get-dropdown` 查询指定范围内的下拉列表配置,返回 `dataValidations` 数组,相同选项的单元格聚合为一组。判空以 `hasDropdown` 为准(无下拉时为 false,但 `dataValidations` 仍含 1 个全 null 幽灵条目) -- ⚠️ `get-dropdown` 的 `options.multipleValues` 恒为 `null`、无 `enableMultiSelect` 字段,读回无法判断是否多选;要判断是否多选改用 `range read`(其 `dataValidation.enableMultiSelect` 准确) +- `get-dropdown` 查询指定范围内的下拉列表配置,返回 `dataValidations` 数组,相同选项的单元格聚合为一组。无下拉列表时 `hasDropdown` 为 false - `delete-dropdown` 删除指定范围内的下拉列表配置,单元格恢复为普通文本格式。已填写的值不会被清除。目标范围不存在下拉列表时操作仍返回成功 diff --git a/skills/mono/references/products/sheet/sheet-export.md b/skills/mono/references/products/sheet/sheet-export.md index 42e77703..ba0880c6 100644 --- a/skills/mono/references/products/sheet/sheet-export.md +++ b/skills/mono/references/products/sheet/sheet-export.md @@ -52,13 +52,13 @@ Flags: - 第 21~30 次:每次间隔 15 秒 - **硬上限:最多轮询 30 次(约 5 分钟)**,超时后命令返回错误 -**命令返回**(`--format json`,默认):输出规整 JSON `{success, jobId, downloadUrl[, outputPath]}`。轮询进度以 `[INFO] [N/30] 状态: ...` 打到 stderr,不污染 stdout 的 JSON(可直接 `python3 -m json.tool` 解析 stdout)。 -- `--output` 未指定:JSON 含 `jobId` + `downloadUrl`(链接有时效性,请尽快下载) -- `--output` 指定为文件路径:下载到该路径,JSON 额外含 `outputPath` -- `--output` 指定为已存在目录:自动从 `downloadUrl` 推断文件名保存,JSON 含 `outputPath` +**命令返回**: +- `--output` 未指定:进度日志 + 末尾输出 `jobId` 和 `downloadUrl`(链接有时效性,请尽快下载) +- `--output` 指定为文件路径:下载到该路径并输出 `导出完成: ` +- `--output` 指定为已存在目录:自动从 `downloadUrl` 推断文件名并保存到该目录下 **失败处理(命令内部已处理,Agent 仅需转述)**: -- MCP 返回 `FAILED`:命令立即返回错误并附带失败原因,**禁止自动重试 `dws sheet export`**,告知用户稍后再试 +- 导出任务返回 `FAILED`:命令立即返回错误并附带失败原因,**禁止自动重试 `dws sheet export`**,告知用户稍后再试 - 轮询 30 次仍 `PROCESSING`:命令返回超时错误,告知用户稍后再试 **限制**:仅支持钉钉在线电子表格(axls)→ xlsx。导出钉钉文字文档请使用 `doc` 产品对应的导出工具。 @@ -131,6 +131,7 @@ dws sheet export --node --output ./ | 操作 | 从返回中提取 | 用于 | |------|-------------|------| | `export` | `downloadUrl`(未指定 --output)/ `outputPath`(指定 --output) | 直接下发给用户或告知文件已保存到本地。命令内部已完成轮询,不要再调用其他 export 相关命令 | +| `export` 超时中断 | 错误信息 | 直接报告失败或超时;当前没有独立续查命令,不自动重新提交导出 | | `export-csv` | CSV 正文(未指定 --output,走 stdout)/ `导出完成: `(指定 --output) | 直接把 CSV 交给下游处理,或告知文件已保存到本地。命令是同步的,无任务/轮询概念 | ## 注意事项 diff --git a/skills/mono/references/products/sheet/sheet-filter-view.md b/skills/mono/references/products/sheet/sheet-filter-view.md index 631a6c6f..89b1e0d3 100644 --- a/skills/mono/references/products/sheet/sheet-filter-view.md +++ b/skills/mono/references/products/sheet/sheet-filter-view.md @@ -219,7 +219,6 @@ Flags: - **用途**:查看某个筛选视图的当前配置,包括已设置的所有筛选条件详情。 - **场景**:在修改或删除筛选视图前,先确认其当前状态;或在 `update-criteria` 后验证条件是否生效。 - **区分**:`info` 返回单个视图的完整信息(含 criteria);`list` 返回所有视图的列表概要。`info` 需要指定 `--filter-view-id`,ID 可通过 `list` 获取。 -- **实现**:内部调用 `get_filter_views` 获取全部列表后按 ID 过滤。 ### 列出筛选视图所有列条件 ``` @@ -238,7 +237,6 @@ Flags: - **用途**:查看某个筛选视图当前设置了哪些列的筛选条件,包括每列的条件类型和具体规则。 - **场景**:在管理筛选条件(修改/删除特定列条件)前,先了解当前视图有哪些条件;或排查筛选结果不符合预期时检查条件配置。 - **区分**:`list-criteria` 返回所有列的条件(按列偏移量为 key 的对象);`get-criteria` 只返回指定列的条件。如果没有设置任何条件,返回空对象 `{}`。 -- **实现**:内部调用 `get_filter_views` 获取视图详情后提取 `criteria` 字段。 ### 获取单列筛选条件 ``` @@ -261,7 +259,6 @@ Flags: - **用途**:查看某个筛选视图中指定列当前设置的筛选条件,包括条件类型、运算符和比较值。 - **场景**:在修改某列条件前,先查看其当前配置;或验证 `update-criteria` 后该列条件是否正确。 - **区分**:`get-criteria` 只返回指定列的条件;`list-criteria` 返回所有列的条件。`--column` 为列偏移量(从 0 开始),相对于筛选视图范围首列。 -- **实现**:内部调用 `get_filter_views` 获取视图详情后按列偏移量过滤 `criteria` 中的对应条件。 ## 核心工作流 @@ -328,7 +325,7 @@ dws sheet filter-view create --node --sheet-id \ - ★ **`--sheet-id` 获取规范(强制)**:`sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等) - ★ **全局筛选(filter)与筛选视图(filter-view)的区别**:全局筛选影响所有协作者看到的数据展示,每个工作表最多一个;筛选视图是个人化的,互不影响。用户只说"筛选"时默认走 `filter` 系列 - `filter-view list` 获取指定工作表的所有筛选视图列表,返回的 `id` 可用于后续 info / update / delete / update-criteria / delete-criteria / list-criteria / get-criteria 的 `--filter-view-id` -- `filter-view info` 获取单个筛选视图的完整信息(含 criteria),内部复用 `get_filter_views` MCP 按 ID 过滤 +- `filter-view info` 获取单个筛选视图的完整信息(含 criteria),按 `--filter-view-id` 返回对应视图 - `filter-view list-criteria` 列出指定筛选视图已设置的所有列条件,返回按列偏移量为 key 的对象;无条件时返回空对象 `{}` - `filter-view get-criteria` 获取指定列的条件详情,`--column` 为列偏移量(从 0 开始);该列无条件时返回错误提示 - `filter-view create` 创建筛选视图时 `--range` 应包含表头行。`--criteria` 可选,不传则创建后无筛选条件,后续可通过 `filter-view update-criteria` 设置 diff --git a/skills/mono/references/products/sheet/sheet-filter.md b/skills/mono/references/products/sheet/sheet-filter.md index 36d69dc4..0e65607b 100644 --- a/skills/mono/references/products/sheet/sheet-filter.md +++ b/skills/mono/references/products/sheet/sheet-filter.md @@ -32,7 +32,7 @@ Flags: - **用途**:查看当前工作表上是否存在全局筛选及其配置。 - **场景**:在修改或删除筛选前,先读取当前筛选配置;创建筛选前先确认是否已存在(每个工作表只能有一个筛选)。 - **区分**:全局筛选(filter)影响所有协作者看到的数据展示;筛选视图(filter-view)是个人化的。 -- **返回**:`range`(筛选范围,A1 表示法)、`id`(筛选 ID)和 `criteria`(各列条件对象,key 为列偏移量;无条件时为 `{}`)。如果未设置筛选,返回筛选信息为空。 +- **返回**:`range`(筛选范围,A1 表示法)和 `columnFilterCriteria`(各列条件,key 为列偏移量)。如果未设置筛选,返回筛选信息为空。 ### 创建筛选 ``` @@ -152,7 +152,7 @@ Flags: | 操作 | 从返回中提取 | 用于 | |------|-------------|------| -| `filter get` | `range`(筛选范围)、`id`、`criteria`(各列条件对象,无条件为 `{}`) | 查看当前筛选配置,确认筛选是否存在 | +| `filter get` | `range`(筛选范围)、`columnFilterCriteria`(各列条件) | 查看当前筛选配置,确认筛选是否存在 | | `filter create` | 筛选创建成功的确认 | 确认筛选已建立,后续可通过 `filter update` 设置条件 | | `filter delete` | 删除成功的确认 | 确认筛选已删除 | | `filter update` | 更新成功的确认 | 确认条件已设置 | @@ -164,7 +164,7 @@ Flags: - ★ **`--sheet-id` 获取规范(强制)**:`sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等) - ★ **全局筛选(filter)与筛选视图(filter-view)的区别**:全局筛选影响所有协作者看到的数据展示,每个工作表最多一个;筛选视图是个人化的,互不影响。用户只说"筛选"时默认走 `filter` 系列 -- `filter get` 获取工作表的全局筛选信息,返回 `range`(筛选范围)、`id` 和 `criteria`(各列条件对象,无条件时为 `{}`)。无筛选时返回空 +- `filter get` 获取工作表的全局筛选信息,返回 `range`(筛选范围)和 `columnFilterCriteria`(各列条件)。无筛选时返回空 - `filter create` 创建全局筛选时 `--range` 必须包含表头行(如 `A1:E100`),不能只包含数据行。每个工作表只能有一个筛选,已存在时报错 - `filter create` 的 `--criteria` 可选,不传则仅创建空筛选框架,后续通过 `filter update` 设置条件 - `filter delete` 删除后所有筛选条件丢失且所有被隐藏行重新显示,不可恢复 diff --git a/skills/mono/references/products/sheet/sheet-formula.md b/skills/mono/references/products/sheet/sheet-formula.md new file mode 100644 index 00000000..cf63e322 --- /dev/null +++ b/skills/mono/references/products/sheet/sheet-formula.md @@ -0,0 +1,192 @@ +# 公式写入、回读与错误校验 + +## 使用场景 + +用户说"写公式/计算列/辅助列/总计/占比/增长率/查找计算/自动计算/校验公式/检查公式错误"时使用本页。 + +- 能由表内其他单元格推导的派生值,优先写公式,不要写一次性的静态结果。 +- 写公式前先读表头和 3-5 行样本,确认列含义、数据类型、真实行号和目标范围。 +- 用户明确要求"辅助列"时,需要真实写入辅助列公式;不要只用条件格式或本地计算绕过。 + +## 当前能力边界 + +- 写少量或需要单元格对象的公式:使用 `dws sheet range update`。 +- 从 CSV/表格文本批量写公式:使用 `dws sheet csv-put`,字段值以 `=` 开头时默认按公式解析;如需写入以 `=` 开头的字面文本,在字段值前加单引号。 +- 公式载体:公式写在 cell object 的 `text` 字段中,例如 `{"type":"text","text":"=SUM(B2:B10)"}`。 +- 读取公式文本:使用 `dws sheet range read --value-render-option formula`。 +- 读取计算结果:使用 `dws sheet range read --value-render-option raw_value` 或默认 `formatted_value`。 +- 聚合错误校验:使用 `dws sheet formula-verify`,支持整本表格、单个目标和多个目标。 +- `formula-verify` 扫描已经落表的公式计算结果,按 `#ERROR!` / `#NAME?` / `#DIV/0!` 等错误类型汇总;它不判断一个正常数值是否符合业务预期。 +- `append` / `table-put` 不作为公式写入协议;需要公式时用 `range update` 或 `csv-put`。 + +## 命令选择 + +| 目的 | 命令 | 说明 | +|------|------|------| +| 写入少量或中等范围公式 | `range update` | `--values` 必须是二维 cell object,维度与 `--range` 完全一致 | +| 从 CSV/表格文本批量写公式 | `csv-put` | `=` 开头按公式;前导单引号写入以 `=` 开头的字面文本;不支持富格式对象 | +| 查看已写入的公式文本 | `range read --value-render-option formula` | 确认公式本身是否落表、范围和引用是否正确 | +| 查看公式计算结果 | `range read --value-render-option raw_value` | 用于数值对账、错误值检查 | +| 查看格式化展示结果 | `range read` 或 `csv-get` 默认模式 | 用于用户肉眼看到的展示值检查 | +| 扫描整本表格公式错误 | `formula-verify --node ` | 不传目标时扫描全部工作表的非空范围 | +| 扫描单个工作表或范围 | `formula-verify --sheet-id ... [--range ...]` | `--range` 只传 A1 范围,不带工作表前缀 | +| 扫描多个目标 | `formula-verify --targets ...` | 必须是非空数组;每项为 `{"sheetId":"...","range":"..."}`,`range` 可省略 | + +## 推荐流程 + +1. 用 `dws sheet list --node --format json` 获取真实 `sheetId`。 +2. 用 `range read` 或 `csv-get` 读取表头和样本数据,确认目标列与行号。 +3. 明确相对引用和绝对引用:向下填充时检查固定汇率、税率、查找表、标题行是否需要 `$` 锁定。 +4. 按数据形态写入公式:精确 cell object 用 `range update`,CSV/表格文本用 `csv-put`。`range update` 的矩阵行列数必须与 `--range` 完全一致。 +5. 用 `range read --value-render-option formula` 回读公式文本,确认实际公式、范围和引用。 +6. 对本次写入目标运行 `formula-verify`;若返回 `partial` / `hasMore=true`,缩小目标或提高 `--max-cells` 后继续扫描,直到结果完整。 +7. 用 `range read --value-render-option raw_value` 抽样对账业务数值;正常数值不会被 `formula-verify` 判定为业务计算错误。 +8. 若发现错误,先定位依赖单元格、空值、除数为 0、引用范围越界或函数名错误,再重写公式并重新执行文本回读、错误扫描和数值抽样。 + +## 聚合式公式校验 + +### 整本表格 + +不指定 `--sheet-id`、`--range` 或 `--targets` 时,扫描整本表格的全部工作表: + +```bash +dws sheet formula-verify --node --format json +``` + +### 单个工作表或范围 + +`--sheet-id` 支持工作表 ID 或名称;省略 `--range` 时扫描该工作表的非空范围: + +```bash +dws sheet formula-verify --node --sheet-id --format json + +dws sheet formula-verify --node --sheet-id \ + --range "D2:D100" --format json +``` + +`--range` 必须和 `--sheet-id` 一起使用,且只传 `D2:D100` 这类 A1 范围,不能传 `Sheet1!D2:D100`。 + +### 多个目标 + +```bash +dws sheet formula-verify --node \ + --targets '[{"sheetId":"Sheet1","range":"D2:D100"},{"sheetId":"Summary"}]' \ + --format json +``` + +`--targets` 也支持 `@targets.json` 和 `-`(stdin)。数组必须至少包含一个目标,不能传 `[]`;每项只允许非空 `sheetId` 和可选的字符串 `range`,`range` 只写 A1 范围且不能带工作表前缀。使用 `--targets` 时不能再传 `--sheet-id` 或 `--range`。 + +### 扫描限制与自动化 + +```bash +dws sheet formula-verify --node \ + --max-locations-per-error 20 --max-cells 30000 --format json + +dws sheet formula-verify --node --exit-on-error --format json +``` + +- `--max-locations-per-error` 只限制每类错误返回的 `locations` 和 `samples` 数量,`count` 与 `totalErrors` 仍保留实际扫描到的总数。 +- `--max-cells` 是本次调用跨全部 targets 共享的扫描预算;预算不足时返回 `status=partial`、`hasMore=true`。 +- `--exit-on-error` 适合 CI/自动化:发现公式错误时打印 JSON 结果并返回非 0;`partial` 结果中已经发现错误时同样返回非 0。 + +### 结果判定 + +| 字段 | 含义 | +|------|------| +| `status` | `success` / `errors_found` / `partial` | +| `totalErrors` | 实际扫描到的错误公式单元格总数 | +| `totalFormulas` | 实际扫描到的公式单元格总数 | +| `scannedCells` | 实际扫描的单元格数 | +| `hasMore` | `true` 表示结果不完整,不能据此声称目标范围零错误 | +| `errorSummary` | 按错误类型聚合的 `count`、`locations` 和 `samples` | +| `warningMessage` | `partial` 等情况下的扫描限制提示 | + +判定规则: + +- `status=success`、`hasMore=false`、`totalErrors=0`:本次目标范围未发现公式错误。 +- `status=errors_found`:按 `errorSummary` 修复后重新校验。 +- `status=partial` 或 `hasMore=true`:当前结果不完整;缩小 targets/range 或提高 `--max-cells` 后继续校验。 + +## 写入示例 + +### 单格公式 + +```bash +dws sheet range update --node --sheet-id --range "D2" \ + --values '[[{"type":"text","text":"=B2*C2"}]]' --format json +``` + +### 整列公式 + +```bash +dws sheet range update --node --sheet-id --range "D2:D5" \ + --values '[ + [{"type":"text","text":"=B2*C2"}], + [{"type":"text","text":"=B3*C3"}], + [{"type":"text","text":"=B4*C4"}], + [{"type":"text","text":"=B5*C5"}] + ]' --format json +``` + +### 含绝对引用 + +税率在 `G1` 时,向下填充应锁定税率单元格: + +```bash +dws sheet range update --node --sheet-id --range "E2:E5" \ + --values '[ + [{"type":"text","text":"=D2*$G$1"}], + [{"type":"text","text":"=D3*$G$1"}], + [{"type":"text","text":"=D4*$G$1"}], + [{"type":"text","text":"=D5*$G$1"}] + ]' --format json +``` + +## 公式文本与结果回读 + +### 1. 回读公式文本 + +```bash +dws sheet range read --node --sheet-id --range "D2:D5" \ + --value-render-option formula --format json +``` + +检查点: +- `value` 应返回以 `=` 开头的公式文本。 +- 行号、列号、相对引用、绝对引用应与写入计划一致。 +- 无公式的单元格在 `formula` 模式下可能回退为原始值,不能把这种回退误判为公式已写入。 + +### 2. 回读计算结果 + +```bash +dws sheet range read --node --sheet-id --range "D2:D5" \ + --value-render-option raw_value --format json +``` + +检查点: +- 数值结果应与样本手算或本地复算一致。 +- 检查结果中是否出现 `#REF!` / `#DIV/0!` / `#VALUE!` / `#NAME?` / `#NULL!` / `#NUM!` / `#N/A`。 +- 对大范围公式,至少抽样检查首行、末行、边界行和异常数据行;用户要求全量处理时,应分批回读并断言处理数量。 + +### 3. 数值正确性边界 + +`formula-verify` 负责聚合已经落表的公式错误值;`range read` 负责确认实际公式文本和具体计算结果。即使 `formula-verify` 返回 `success`,也仍需对金额、比例、汇率、边界行等关键业务结果做 `raw_value` 抽样对账,因为一个公式可能计算出合法数值但业务逻辑仍然写错。 + +## 常见错误 + +- 想用 `csv-put` 写入 `=SUM(...)` 文本却忘记加前导单引号,导致内容被解析为公式。 +- 用原始二维数组 `--values '[["=B2*C2"]]'`,而不是 cell object。 +- 写整列公式时只写第一行,忘记把 `--range` 和 `--values` 扩成同样行数。 +- 复制公式时没有锁定固定引用,例如税率、汇率、查找表范围。 +- 没有回读 `formula` 模式,只看写入返回 `success`。 +- 只回读展示值,不运行 `formula-verify` 聚合扫描错误。 +- 把 `max-locations-per-error` 误解为错误计数上限;它只截断位置和样本。 +- 看到 `status=partial` 或 `hasMore=true` 仍声称整本表公式零错误。 +- 在 `--range` 中传 `Sheet1!A1:D10`,或把 `--targets` 与 `--sheet-id` 混用。 +- 显式传空的 `--targets '[]'`;这不是“无目标”,应改为至少一个目标,整本扫描则完全省略 `--targets`。 + +## 关联文档 + +- [sheet-write-data](./sheet-write-data.md):`range update` 的 `--values` cell object 结构、维度校验、富格式能力。 +- [sheet-read-data](./sheet-read-data.md):`value-render-option` 的 `formatted_value` / `raw_value` / `formula` 读取模式。 +- [sheet-conditional-format](./sheet-conditional-format.md):条件格式中的 `formulaCondition` 与辅助列公式的职责边界。 diff --git a/skills/mono/references/products/sheet/sheet-import.md b/skills/mono/references/products/sheet/sheet-import.md index 2aa0a946..d50b27b4 100644 --- a/skills/mono/references/products/sheet/sheet-import.md +++ b/skills/mono/references/products/sheet/sheet-import.md @@ -6,7 +6,7 @@ - 支持本地 `xlsx` / `xls`,文件上限 20MB - `--folder-token` 与 `--workspace` 至少提供一个 -- CLI 内部完成创建会话、OSS 上传、确认转换和轮询,Agent 不要自行拆分或重试 +- CLI 内部完成创建会话、上传文件、确认转换和轮询,Agent 不要自行拆分或重试 - `drive upload` 只上传二进制文件,不会转换为可编辑的在线表格,不能替代本命令 ## 命令 @@ -71,6 +71,6 @@ dws sheet import get --task-id --format json ## 边界 - 本命令接受的是本地文件路径;它把 xlsx/xls 转换成新的 axls 在线表格 -- 已经存在于钉盘或文档中的 xlsx 节点不能直接传给工作表/单元格命令;先用 `doc download` 下载,需要在线编辑时再执行 `sheet import create` +- 已经存在于钉盘或文档中的 xlsx 节点不能直接传给工作表/单元格命令;先用 `drive download` 下载,需要在线编辑时再执行 `sheet import create` - 向已有在线表格写数据应使用 `range update`、`append`、`csv-put` 或 `table-put` - md/doc/docx 等文字文档导入使用 `dws doc import` diff --git a/skills/mono/references/products/sheet/sheet-media-image.md b/skills/mono/references/products/sheet/sheet-media-image.md index 060c55c9..06aba4b7 100644 --- a/skills/mono/references/products/sheet/sheet-media-image.md +++ b/skills/mono/references/products/sheet/sheet-media-image.md @@ -11,7 +11,7 @@ 用户说"写入图片/插入图片/加图片/放图片到单元格/嵌入图片到表格": - 写入图片 → `write-image`(需表格 ID + 工作表 ID + 单元格范围 + 本地图片路径) -- 禁止使用 `range update` 写入图片,因为 `update_range` 的 MCP 工具不支持图片类型参数,调用必定失败。必须使用 `write-image` 命令 +- 禁止使用 `range update` 写入图片;图片对象必须使用 `write-image` 命令 - 用户指定了图片尺寸 → `write-image --width N --height M` ### 浮动图片 @@ -48,8 +48,6 @@ Flags: --mime-type string 文件 MIME 类型 (默认根据扩展名推断) ``` -`--format json` 输出规整 JSON:`{success, resourceId, resourceUrl, fileName, fileSize}`。`resourceUrl` 可用于 `create-float-image` 的 `--src`。 - ### 上传图片并写入表格单元格 ``` Usage: @@ -205,10 +203,13 @@ dws sheet write-image --node --sheet-id --range C3:C3 --fil # 4. 完整流程: 创建表格 → 写表头 → 写入图片 dws sheet create --name "产品目录" -f json -# 提取 nodeId 后: -dws sheet range update --node --sheet-id Sheet1 --range "A1:B1" --values '[[{"type":"text","text":"产品名称"},{"type":"text","text":"产品图片"}]]' -f json -dws sheet range update --node --sheet-id Sheet1 --range "A2:A2" --values '[[{"type":"text","text":"MacBook Pro"}]]' -f json -dws sheet write-image --node --sheet-id Sheet1 --range B2:B2 --file ./macbook.png --width 150 --height 100 -f json +# 提取 nodeId 后,先用 list 获取真实 sheetId: +dws sheet list --node -f json +dws sheet range update --node --sheet-id --range "A1:B1" \ + --values '[[{"type":"text","text":"产品名称"},{"type":"text","text":"产品图片"}]]' -f json +dws sheet range update --node --sheet-id --range "A2:A2" \ + --values '[[{"type":"text","text":"MacBook Pro"}]]' -f json +dws sheet write-image --node --sheet-id --range B2:B2 --file ./macbook.png --width 150 --height 100 -f json ``` ## 上下文传递 @@ -229,7 +230,7 @@ dws sheet write-image --node --sheet-id Sheet1 --range B2:B2 --file ./ - ★ **`--sheet-id` 获取规范(强制)**:`sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等) - `media-upload` 是两步自动完成的流程 (获取附件上传凭证 → OSS 上传),无需手动分步操作 - `write-image` 是三步自动完成的流程 (获取附件上传凭证 → OSS 上传 → 写入图片到单元格),无需手动分步操作 -- ★ 向表格单元格中写入图片必须使用 `write-image`,禁止使用 `range update`。`range update` 底层调用的 `update_range` MCP 工具不支持图片类型参数,调用会失败 +- ★ 向表格单元格中写入图片必须使用 `write-image`,禁止使用 `range update`。`range update` 不支持图片对象 - `write-image` 与 `media-upload` 的区别:`media-upload` 仅上传附件到表格获取 resourceId;`write-image` 在上传后还会将图片写入指定单元格 - `create-float-image` 创建浮动图片前必须先通过 `media-upload` 上传图片获取 `resourceUrl`,再将其作为 `--src` 传入。`--src` 的格式为 `/core/api/resources/img/...`,不能直接传外部 URL - `create-float-image` 的 `--range` 使用 A1 表示法指定锚点单元格(如 `A1`、`B3`),支持带工作表前缀(如 `Sheet1!A1`) diff --git a/skills/mono/references/products/sheet/sheet-pivot-table.md b/skills/mono/references/products/sheet/sheet-pivot-table.md index 88547ca0..6b8f93d2 100644 --- a/skills/mono/references/products/sheet/sheet-pivot-table.md +++ b/skills/mono/references/products/sheet/sheet-pivot-table.md @@ -1,67 +1,292 @@ -# 透视表管理 +# 透视表 (pivot-table) + +## 真对象硬约束 + +当用户要求"透视表 / 分组汇总 / 交叉分析 / 按 X 统计 Y"时,**必须**通过 `pivot-table create` 创建真实的透视表对象。**禁止**用 `SUMIFS` / `COUNTIFS` 等普通公式 + `csv-put` 在原表中拼一张"看起来像透视表的汇总表"来代替——静态公式无法随源数据动态更新,且失去交互能力。判断标准:交付后 `pivot-table list` 必须能返回该对象。 ## 使用场景 -- 用户要按部门、月份等维度汇总数据 → `pivot-table create` -- 用户要查看现有透视表或取得 `pivotTableId` → `pivot-table list` -- 用户要调整行列、聚合方式或显示选项 → `pivot-table update` -- 用户明确要求删除透视表 → `pivot-table delete`(不可逆,必须先确认) +读写透视表对象。本 reference 覆盖 4 个命令: -透视表是原生对象,会随源数据更新;不要用一组普通汇总公式冒充透视表。 +| 操作需求 | 使用命令 | 说明 | +|---------|---------|------| +| 查看已有透视表 | `pivot-table list` | 获取透视表的结构、数据源和配置 | +| 创建透视表 | `pivot-table create` | 创建透视表对象 | +| 更新透视表 | `pivot-table update` | 更新透视表配置(行/列/值/筛选字段) | +| 删除透视表 | `pivot-table delete` | 删除透视表 | -## 列出与查看 +典型工作流:先读取现有透视表了解配置 -> 执行创建/更新/删除 -> 再次读取验证结果。 -```bash -dws sheet pivot-table list --node --sheet-id -dws sheet pivot-table list --node --sheet-id --pivot-table-id +## 行/值字段映射(创建前必做) + +创建透视表前先识别用户需求中的分组维度和聚合指标,**不要搞反**: + +- **rows(行字段)** = 分组维度,即"按什么分组"。例:部门、地区、医生、产品类别 +- **values(值字段)** = 聚合指标,即"统计什么数值"。例:销售额(`summarize_by: "sum"`)、订单数(`summarize_by: "count"`) +- **columns(列字段)** = 交叉维度(可选),即"再按什么横向展开"。例:月份、性别 + +| 用户说 | rows | values | columns | +|--------|------|--------|---------| +| "按部门统计人数" | 部门 | 姓名(`"count"`) | -- | +| "按医生统计费用和结余" | 主管医生 | 费用(`"sum"`)、结余(`"sum"`) | -- | +| "各部门男女人数" | 部门 | 姓名(`"count"`) | 性别 | + +**常见配置错误(必须注意)**: +- **数据源范围必须精确**:透视表的数据源范围必须包含表头行,且精确覆盖全部数据行列。范围过大(包含空行/空列)或过小(遗漏数据列)都会导致透视表结果错误 +- **行列字段选择要匹配用户意图**:用户说"按商品统计金额" -> 行字段=商品,值字段=金额(`summarize_by: "sum"`)。不要把行列字段搞反 +- **聚合类型要匹配**:用户说"统计数量" -> `"count"`;"统计总额" -> `"sum"`;"统计平均" -> `"average"` +- **创建后必须验证**:调用 `pivot-table list` 确认透视表结构正确 + +## 命令详细参考 + +### 获取透视表 +``` +Usage: + dws sheet pivot-table list [flags] +Example: + # 列出所有透视表 + dws sheet pivot-table list --node NODE_ID --sheet-id SHEET_ID + + # 获取单个透视表详情 + dws sheet pivot-table list --node NODE_ID --sheet-id SHEET_ID --pivot-table-id PT_ID +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --pivot-table-id string 透视表 ID (可选,不传则返回全部) ``` -不传 `--pivot-table-id` 时列出工作表中的全部透视表;传入时返回单个透视表详情。 +### 创建透视表 +``` +Usage: + dws sheet pivot-table create [flags] +Example: + # 按部门统计销售额(默认自动新建工作表存放) + dws sheet pivot-table create --node NODE_ID \ + --source "'Sheet1'!A1:D100" \ + --properties '{ + "rows": [{"field": "部门"}], + "values": [{"field": "销售额", "summarize_by": "sum"}], + "show_row_grand_total": true + }' -## 创建 + # 指定放置到已有工作表的特定位置 + dws sheet pivot-table create --node NODE_ID \ + --source "'Sheet1'!A1:E200" \ + --target-sheet-id TARGET_SHEET_ID --target-position "A1" \ + --properties '{ + "rows": [{"field": "部门"}], + "values": [{"field": "销售额", "summarize_by": "sum"}] + }' + + # 通过文件传入配置 + dws sheet pivot-table create --node NODE_ID \ + --source "'Sheet1'!A1:D50" --properties @pivot.json +Flags: + --node string 表格文档 ID 或 URL (必填) + --source string 数据源区域,A1 表示法含 sheet 前缀 (必填,如 "'Sheet1'!A1:D100") + --properties string 透视表配置 JSON (必填,含 rows/columns/values/filters) + --target-sheet-id string 目标工作表 ID 或名称 (可选,不传则自动新建工作表) + --target-position string 透视表放置位置 (可选,A1 格式单个 cell,如 "B5",不传默认 A1) +``` + +### 更新透视表 +``` +Usage: + dws sheet pivot-table update [flags] +Example: + # 先获取现有配置 + dws sheet pivot-table list --node NODE_ID --sheet-id SHEET_ID --pivot-table-id PT_ID + + # 修改后回写 + dws sheet pivot-table update --node NODE_ID --sheet-id SHEET_ID --pivot-table-id PT_ID \ + --properties '{ + "rows": [{"field": "部门"}], + "values": [ + {"field": "销售额", "summarize_by": "sum"}, + {"field": "订单号", "summarize_by": "count", "display_name": "订单数"} + ], + "show_row_grand_total": true + }' +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --pivot-table-id string 透视表 ID (必填,可通过 pivot-table list 获取) + --properties string 透视表配置 JSON (必填) +``` + +### 删除透视表 +``` +Usage: + dws sheet pivot-table delete [flags] +Example: + dws sheet pivot-table delete --node NODE_ID --sheet-id SHEET_ID --pivot-table-id PT_ID +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --pivot-table-id string 透视表 ID (必填,可通过 pivot-table list 获取) +``` + +> [强制] 危险操作:删除不可恢复。必须先向用户展示操作摘要并获得明确同意,用户同意后才加 `--yes` 执行。 + +## `--properties` JSON Schema 速查 + +**顶层字段**: + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `rows` | object[] | 否 | 行字段数组(分组维度),详见下方「rows/columns 字段项」表 | +| `columns` | object[] | 否 | 列字段数组(交叉维度),结构同 rows | +| `values` | object[] | 是 | 值字段数组(聚合指标,至少一项),详见下方「values 字段项」表 | +| `filters` | object[] | 否 | 筛选字段数组,详见下方「filters 字段项」表 | +| `show_row_grand_total` | boolean | 否 | 是否显示行总计,默认 true | +| `show_col_grand_total` | boolean | 否 | 是否显示列总计,默认 true | +| `show_subtotals` | boolean | 否 | 是否显示分类小计,默认 true | +| `repeat_row_labels` | boolean | 否 | 是否显示重复项标签,默认 false | +| `collapse` | object | 否 | 行字段折叠状态:字段名 -> 要折叠的项目列表,如 `{"部门": ["A组", "B组"]}` | + +**rows/columns 字段项**: + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `field` | string | 是 | 列名(表头文本),必须与数据源首行的列名完全匹配 | +| `display_name` | string | 否 | 显示名称(不传时使用 field 值) | + +**filters 字段项**: + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `field` | string | 是 | 列名(表头文本),必须与数据源首行的列名完全匹配 | +| `display_name` | string | 否 | 显示名称(不传时使用 field 值) | + +**values 字段项**: + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `field` | string | 是 | 列名(表头文本),必须与数据源首行的列名完全匹配 | +| `summarize_by` | string | 否 | 聚合方式,默认 sum,详见下方枚举表 | +| `display_name` | string | 否 | 显示名称(不传时自动生成,如"求和 - 销售额") | +| `show_data_as` | string | 否 | 值显示方式:`normal`/`percent_of_row_total`/`percent_of_col_total`/`percent_of_grand_total` | + +**summarize_by 枚举值**: + +| 值 | 说明 | +|----|------| +| `sum` | 求和(默认) | +| `count` | 计数 | +| `average` | 平均值 | +| `max` | 最大值 | +| `min` | 最小值 | +| `product` | 乘积 | +| `count_numbers` | 数值计数 | +| `std_dev` | 标准偏差 | +| `std_dev_p` | 总体标准偏差 | +| `var` | 方差 | +| `var_p` | 总体方差 | +| `distinct` | 去重计数 | +| `median` | 中位数 | + +## `--source` 数据源格式 + +- 格式:`'SheetName'!StartCell:EndCell` +- 示例:`'Sheet1'!A1:D100`、`'销售数据'!A1:F500` +- 必须包含表头行(通常从第 1 行开始) +- sheet 名称用单引号包裹(含空格或特殊字符时必须) +- `source` 直接使用上述 A1 表示法;不要自行改写成内部对象结构 + +## 高级功能示例 ```bash +# 含折叠 + show_data_as dws sheet pivot-table create --node \ - --source "'Data'!A1:D100" \ + --source "'Sheet1'!A1:E200" \ --properties '{ - "rows":[{"field":"部门"}], - "columns":[{"field":"月份"}], - "values":[{"field":"销售额","summarize_by":"sum"}], - "show_row_grand_total":true + "rows": [{"field": "部门"}], + "values": [ + {"field": "销售额", "summarize_by": "sum"}, + {"field": "订单数", "summarize_by": "count", "show_data_as": "percent_of_col_total"} + ], + "collapse": {"部门": ["A组"]}, + "show_row_grand_total": true }' ``` -可选参数: +> collapse/show_data_as 为可选透传字段,CLI 和 MCP 层不做额外校验,直接传递给底层引擎处理。 -- `--target-sheet-id`:将透视表放到指定工作表;不传时由服务端创建目标工作表。 -- `--target-position`:目标起始单元格,例如 `B2`。 -- `--properties @pivot.json`:从文件读取配置。 - -`--source` 必须包含工作表前缀并覆盖表头和数据,例如 `"'Data'!A1:D100"`。创建时 `properties.values` 必须是非空数组。 - -## properties 结构 - -- `rows` / `columns` / `filters`:字段数组,每项至少包含非空 `field`。 -- `values`:聚合字段数组,每项至少包含 `field`;创建时至少一项。 -- `summarize_by`:`sum`、`count`、`average`、`max`、`min`、`product`、`count_numbers`、`std_dev`、`std_dev_p`、`var`、`var_p`、`distinct`、`median`。 -- `collapse`:字段折叠配置,可为对象或数组。 -- 显示选项:`show_row_grand_total`、`show_col_grand_total`、`show_subtotals`、`repeat_row_labels`。 - -## 更新 +## 核心工作流 ```bash -dws sheet pivot-table update --node --sheet-id \ - --pivot-table-id \ - --properties '{"show_subtotals":false}' +# -- 工作流 1: 创建简单分组汇总 -- + +# 1. 先查 sheetId +dws sheet list --node -f json + +# 2. 查看数据范围确认列名和边界 +dws sheet csv-get --node --sheet-id --range "A1:F5" + +# 3. 创建透视表(按部门统计销售额) +dws sheet pivot-table create --node \ + --source "'Sheet1'!A1:D100" \ + --properties '{ + "rows": [{"field": "部门"}], + "values": [{"field": "销售额", "summarize_by": "sum"}], + "show_row_grand_total": true + }' + +# 4. 验证创建结果(用 create 返回的 targetSheetId 查询) +dws sheet pivot-table list --node --sheet-id + +# -- 工作流 2: 多维度交叉分析 -- + +dws sheet pivot-table create --node \ + --source "'Sheet1'!A1:E200" \ + --properties '{ + "rows": [{"field": "部门"}, {"field": "产品"}], + "columns": [{"field": "季度"}], + "values": [ + {"field": "销售额", "summarize_by": "sum"}, + {"field": "订单号", "summarize_by": "count", "display_name": "订单数"} + ], + "show_row_grand_total": true, + "show_col_grand_total": true, + "show_subtotals": true + }' + +# -- 工作流 3: 更新透视表配置 -- + +# 先获取现有配置 +dws sheet pivot-table list --node --sheet-id --pivot-table-id + +# 修改后回写(增加一个值字段) +dws sheet pivot-table update --node --sheet-id --pivot-table-id \ + --properties '{ + "rows": [{"field": "部门"}], + "values": [ + {"field": "销售额", "summarize_by": "sum"}, + {"field": "利润", "summarize_by": "average", "display_name": "平均利润"} + ], + "show_row_grand_total": true + }' ``` -Open 接口支持部分更新,因此更新显示选项时不需要重复传入 `values`。更新行列或聚合字段时,应先用 `list --pivot-table-id` 读取当前配置,再传入需要变更的字段。 +## 上下文传递 -## 删除 +| 操作 | 从返回中提取 | 用于 | +|------|-------------|------| +| `pivot-table list` | `pivotTables[].id` | 后续 update / delete 的 `--pivot-table-id` | +| `pivot-table list --pivot-table-id` | 完整配置(rows/columns/values/filters/collapse/options) | update 时作为基础配置修改后回写 | +| `pivot-table create` | `pivotTable.id` | 后续 update / delete 的 `--pivot-table-id` | +| `pivot-table create` | `pivotTable.targetSheetId` | 后续 list / update / delete 的 `--sheet-id`(透视表所在工作表) | +| `pivot-table delete` | `message` | 确认删除完成 | +| `sheet list` | 工作表的 `sheetId` | 所有 pivot-table 命令的 `--sheet-id` | -```bash -dws sheet pivot-table delete --node --sheet-id \ - --pivot-table-id --yes -``` +## 注意事项 -删除不可恢复。Agent 必须先展示目标文档、工作表和 `pivotTableId`,获得用户明确同意后才能附加 `--yes`。 +- [强制] **`--sheet-id` 获取规范**:`sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等) +- [强制] **创建后必须验证**:透视表创建后必须调用 `pivot-table list` 验证配置是否正确 +- [强制] **pivot-table-id 禁止臆测**:必须通过 `pivot-table list` 获取真实的透视表 ID,不可编造 +- [强制] **source 必须精确**:数据源范围必须从表头行开始,精确覆盖数据区域,先用 `csv-get` 确认数据边界 +- **field 名称必须准确**:rows/columns/values/filters 中的 field 值必须与源数据表头完全一致(区分大小写) +- **透视表自动新建子表**:创建的透视表默认放置在自动新建的子表中,不会覆盖源数据。可通过 `--target-sheet-id` 和 `--target-position` 指定放置到已有工作表 +- **不支持修改数据源**:update 仅可修改字段配置和显示选项,不可修改 source +- **折叠状态**:collapse 字段用于控制行字段的展开/折叠,格式为 {字段名: [要折叠的项]} +- **大 JSON 用 @file**:`--properties` 支持 `@文件路径` 读取本地 JSON 文件 diff --git a/skills/mono/references/products/sheet/sheet-range-operations.md b/skills/mono/references/products/sheet/sheet-range-operations.md index af6c076d..54bc630e 100644 --- a/skills/mono/references/products/sheet/sheet-range-operations.md +++ b/skills/mono/references/products/sheet/sheet-range-operations.md @@ -22,6 +22,11 @@ - 自动填充 → `range fill` - 请勿用 `range read` 读取源数据后手动计算规律再 `range update` 写入,`range fill` 支持服务端智能填充 +用户说"批量清除/批量操作/一次执行多个写操作/原子批量/先清除再写入": +- 批量清除多个区域 → `range batch-clear` +- 组合多个不同写操作 → `batch-update` +- 详见 [sheet-batch-operations](./sheet-batch-operations.md) + 用户说"复制区域/把这块数据复制到/复制到另一个工作表": - 复制区域 → `range copy-to` - 跨工作表 → `range copy-to --target-sheet-id Sheet2` 或 `--target-range "Sheet2!A1"` @@ -82,11 +87,9 @@ Flags: --sheet-id string 工作表 ID 或名称 (必填) --source-range string 源数据范围,A1 表示法 (必填) --target-range string 目标填充范围,A1 表示法 (必填) - --fill-type string 填充类型: 不传则自动检测 / copy(复制) / onlystyle(仅格式) / withoutstyle(仅值) + --fill-type string 填充类型: series(序列,默认) / copy(复制) / onlystyle(仅格式) / withoutstyle(仅值) ``` -`--fill-type` **不传时自动检测**(根据源数据智能判断:数值序列递增、日期递增、文本复制等),这是填序列的默认行为,无需显式传值。`--help` 枚举的可选值只有 `copy` / `onlystyle` / `withoutstyle`;服务端另外也接受 `series`(强制按序列填充),但它不在 `--help` 列表里,一般用不到——要序列递增直接不传 `--fill-type` 即可。 - 目标范围须与源范围在行或列维度对齐(不支持对角填充)。 ### 复制区域 @@ -144,6 +147,8 @@ Flags: |------|-------------|------| | `list` | 工作表的 `sheetId` | range clear / range sort / range fill / range copy-to / range move-to 的 --sheet-id | +> **批量操作**(`range batch-clear` / `batch-update`)已拆分至 [sheet-batch-operations](./sheet-batch-operations.md),含写入边界 + 回读校验规范、典型组合场景示例等。 + ## 注意事项 - ★ **`--sheet-id` 获取规范(强制)**:`sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询真实的 `sheetId` / 工作表名称后再调用,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等);用户仅给出工作表名称时,也应通过 `list` 校验该名称是否存在,避免名称大小写或拼写不一致导致失败 @@ -153,3 +158,4 @@ Flags: - ★ **排序用 `range sort` 不用 `range read` + 客户端排序 + `range update`**:服务端原子操作,支持多级排序 - ★ **排序前必须 `range read` 前几行判断表头**:读取排序范围前 3-5 行,对比首行与后续行的数据模式(类型、语义)来判断是否有表头。禁止不读就排,表头被排入数据不可撤销 - ★ **填充用 `range fill` 不用 `range read` + 手动计算 + `range update`**:服务端智能填充,支持序列递增、公式扩展等 +- ★ **批量操作详见 [sheet-batch-operations](./sheet-batch-operations.md)**:`range batch-clear` 多区域清除、`batch-update` 组合写操作,均原子事务 diff --git a/skills/mono/references/products/sheet/sheet-read-data.md b/skills/mono/references/products/sheet/sheet-read-data.md index 03d52d67..0e2ae054 100644 --- a/skills/mono/references/products/sheet/sheet-read-data.md +++ b/skills/mono/references/products/sheet/sheet-read-data.md @@ -3,43 +3,34 @@ ## 使用场景 用户说"读数据/看表格内容": -- 需要列名、二维数据、dtype 和 number format,或准备与 `table-put` 配套处理 → `table-get` - 快速查看纯值数据、批量处理、大表分批读 → `csv-get`(token 消耗低,防爆保护) +- 需要 dataframe/table 结构(列名、二维 data、pandas-style dtypes、number formats)→ `table-get` - 需要结构化信息(值+样式+数据验证+富文本+单元格级超链接)、查看公式或原始值 → `range read` +- 需要校验公式写入结果 → 先 `range read --value-render-option formula` 回读公式文本,再用 `formula-verify` 聚合扫描错误;需要确认业务数值时用 `raw_value` 抽样对账。完整流程见 [sheet-formula](./sheet-formula.md) - 需要查看合并单元格 / 表头合并结构 → `sheet info`,读取返回的 `mergedRanges`;不要在 `csv-get` 或 `range read` 里找合并信息 ## 命令选择 | 读取目的 | 推荐命令 | 说明 | |---------|---------|------| -| DataFrame 风格结构化读取 | `table-get` | 返回 columns / data / dtypes / formats,可与 table-put 配套 | | 快速查看纯值、数据分析、大表分批读取 | `csv-get` | CSV 格式,token 消耗约为 JSON 的 1/3,内置 maxChars 防爆 | +| 按 table/dataframe 协议读取 | `table-get` | 返回 `columns` / `data` / `dtypes` / `formats`;默认首行为表头 | | 查看数据验证配置(下拉/复选框) | `range read` | 返回 per-cell 结构,含 dataValidation | | 查看单元格样式(背景色/字体/对齐等) | `range read` | 返回 per-cell 结构,含 cellStyles(仅显式设置的样式) | | 查看单元格级超链接 | `range read` | 返回 per-cell 结构,含 hyperlink;富文本片段链接仍在 richText 内 | | 查看公式文本 | `range read --value-render-option formula` | value 返回公式 | +| 公式写后结果校验 | `range read --value-render-option formula` + `formula-verify` + `raw_value` | 先确认公式文本,再聚合扫描错误,最后按需抽样对账业务数值 | | 获取原始值(数字/布尔而非格式化字符串) | `range read --value-render-option raw_value` | value 返回原始类型 | | 查看合并单元格范围 | `sheet info` | 返回 `mergedRanges`,这是工作表结构信息,不属于单元格值读取 | +## 与现有读取能力的区别 + +- `csv-get` 面向快速浏览和大表纯值读取:返回 CSV 文本、真实行号/列号映射和截断标记,token 低;不表达列类型、number format 或 per-cell 元数据 +- `table-get` 面向 dataframe/table 数据交换:返回 `columns` / `data` / `dtypes` / `formats`,适合后续结构化分析、类型保持和 `table-put` 回写;不返回合并单元格、dataValidation、hyperlink、richText、per-cell `cellStyles` +- `range read` 面向精确单元格读取:返回二维 per-cell 对象,可看公式、原始值、dataValidation、hyperlink、richText、cellStyles;适合少量或中等范围的细节检查,但大范围读取 token 成本更高 + ## 命令详细参考 -### 读取结构化 table 数据 -``` -Usage: - dws sheet table-get [flags] # 别名: dws sheet table-read -Example: - dws sheet table-get --node - dws sheet table-get --node --sheet-id --range "A1:D20" - dws sheet table-get --node --sheet-id --no-header -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 - --range string 读取范围,A1 表示法;可带 sheet 前缀 - --no-header 首行不作为表头,自动生成 col1/col2/... -``` - -`table-get` 面向结构化数据处理,返回 `columns`、二维 `data`、`dtypes` 和可用时的 `formats`。未传 `--sheet-id` / `--range` 时由服务端读取目标文档中的 table 数据;大表仍应通过 `--range` 限定范围。需要单元格样式、公式、数据验证或富文本细节时,继续使用 `range read`。 - ### 以 CSV 格式读取工作表数据(推荐) ``` Usage: @@ -77,6 +68,46 @@ Flags: - 分批读取:`--range "A1:J500"`、`--range "A501:J1000"` …… - 单次建议 ≤5000 单元格 +### 以 table/dataframe 协议读取结构化数据 +``` +Usage: + dws sheet table-get [flags] +Example: + dws sheet table-get --node + dws sheet table-get --node --sheet-id --range "A1:D20" + dws sheet table-get --node --sheet-id "Sheet1" --no-header + dws sheet table-get --node --range "Sheet1!A1:D20" +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称;默认不指定 + --range string 读取范围,A1 表示法;可带 sheet 前缀,如 Sheet1!A1:D10;默认读取非空范围 + --no-header 首行不作为表头,自动生成 col1/col2/...;默认 false +``` + +**默认行为**: +- 不传 `--sheet-id` / `--range` 时,读取文档内所有工作表 +- `--range` 带 sheet 前缀时,以 `range` 中的 sheet 为读取目标 +- 不传 `--range` 时,每个目标工作表读取实际非空范围,并裁掉前导全空行/列;显式传 `--range` 时按用户范围原样读取 +- 默认首行作为表头;传 `--no-header` 时不消费表头行,并生成 `col1` / `col2` / ... +- 大表或多 sheet 文档建议显式传 `--sheet-id` 和 `--range`,避免一次返回所有工作表造成超时或 token 过大 + +**返回字段说明**: +- `sheets` — 工作表数组;每项表示一个 table 结果 +- `name` / `sheetId` — 工作表名称与 ID +- `range` — 实际读取范围,A1 表示法 +- `columns` — 列名数组;默认来自第一行,传 `--no-header` 时生成 `col1` / `col2` / ... +- `data` — 二维数组;默认不含表头行。空单元格 / 缺失值在 table 语义上是 `null`;部分返回链路中也可能表现为空对象 `{}`,此时 `{}` 只表示空位,不是 JSON object 单元格值 +- `dtypes` — 按列返回 pandas-style dtype,例如 `object` / `float64` / `bool` / `datetime64[ns]`。普通文本列返回 `object`,当前不返回 `string` +- `formats` — 按列返回 number format code,例如 `@` / `#,##0.00` / `yyyy-mm-dd` + +**适用边界**: +- 不传 `--range` 时读取实际非空范围;如需控制数据量,请显式传 `--range` 分批读取 +- 首行作为表头时,表头必须非空且不重复;没有稳定表头时传 `--no-header` +- `data[][]` 中的 `{}` 表示空位 / 空单元格,按 `null` 处理;不要把它理解成可写入的对象值 +- `table-get` 返回的是值级 table 结构,不返回合并单元格、dataValidation、hyperlink、richText 或 per-cell `cellStyles`。这些信息请使用 `sheet info` 或 `range read` +- `dtypes` / `formats` 是列级推断结果;单列混合类型通常按 `object` 处理。即使 `table-put` 写入时声明 `string`,文本列回读也通常是 `object` +- 大整数/长数字 ID 如果在 `table-get` 中回读为 `float64`,不要把它当作逐位精确值使用;超过 `9007199254740991` 的整数必须在写入时按文本保存,回读应表现为字符串 / `object`,必要时用 `range read --value-render-option raw_value` 复核 + ### 读取工作表数据(per-cell 结构化信息) ``` Usage: @@ -107,7 +138,7 @@ Flags: | `dataValidation` | object | 仅有数据验证时出现,无则省略 | 数据验证配置,见下表 | | `hyperlink` | object | 仅有单元格级超链接时出现,无则省略 | 整格超链接,结构为 `{type, link, text?}`,见下表 | | `richText` | object | 仅富文本单元格出现 | 富文本结构(含超链接、附件、图片、样式片段等),普通纯文本不含此字段 | -| `cellStyles` | object | 仅有显式设置的样式时出现;MCP 序列化层也可能返回全 null 空壳 | cell-level 样式,见下表。读取时只看非 null 字段;全 null 等同不存在 | +| `cellStyles` | object | 仅有显式设置的样式时出现;部分返回链路也可能返回全 null 空壳 | cell-level 样式,见下表。读取时只看非 null 字段;全 null 等同不存在 | `range read` / `range get` 不返回合并单元格结构。要看合并单元格,请先或另行调用 `dws sheet info --node --sheet-id --format json`,使用其中的 `mergedRanges`。 @@ -174,7 +205,7 @@ Flags: } ``` -说明:第一行表头有 `cellStyles`(加粗 + 背景色),第二行第二格有单元格级 `hyperlink`。注意:MCP 平台序列化会将未设置的字段填充为 null(如 `"fontStyle": null`),读取时应忽略值为 null 的字段,仅关注非 null 的属性;如果 `cellStyles` 全字段都是 null,视同不存在。`richText` 字段同理——无富文本的普通单元格可能返回 `{"type": null, "texts": null}`,视同不存在。 +说明:第一行表头有 `cellStyles`(加粗 + 背景色),第二行第二格有单元格级 `hyperlink`。注意:部分返回链路会将未设置的字段填充为 null(如 `"fontStyle": null`),读取时应忽略值为 null 的字段,仅关注非 null 的属性;如果 `cellStyles` 全字段都是 null,视同不存在。`richText` 字段同理——无富文本的普通单元格可能返回 `{"type": null, "texts": null}`,视同不存在。 **取值模式说明**: | 模式 | value 返回内容 | 适用场景 | @@ -183,6 +214,8 @@ Flags: | `raw_value` | 原始值(如 1000、45808) | 数据处理、计算 | | `formula` | 公式文本(如 =SUM(A1:A10)),无公式时回退原始值 | 查看/复制公式 | +**公式校验建议**:写公式后不要只看写入返回结果。先用 `formula` 模式确认公式文本已落表,再运行 `formula-verify` 聚合扫描错误;关键业务数值继续用 `raw_value` 抽样对账。详见 [sheet-formula](./sheet-formula.md)。 + **超时处理建议**:读取大范围数据时若出现超时或响应过慢,请主动缩小 `--range` 查询范围,**建议单次读取的单元格数量控制在 5000 个以内**(例如 50 行 × 100 列、100 行 × 50 列)。对于大表可采用分页读取策略: - 先通过 `info` 获取 `nonEmptyRange.range`,或用 `nonEmptyRange.lastRow` / `nonEmptyRange.lastColumn` 确定 A1 边界 - 按行分批读取,如 `A1:J500`、`A501:J1000`、`A1001:J1500` …… @@ -219,5 +252,7 @@ dws sheet range read --node --sheet-id --range "A1:D10" --f - `range read` 不传 `--range` 时默认读取整个工作表的全部非空数据 - `range read` 的 `--range` 支持 `Sheet1!A1:D10` 格式直接指定工作表(此时忽略 `--sheet-id`) - ★ `csv-get` / `range read` / `range get` 不返回合并单元格结构;查看合并范围必须用 `sheet info` 的 `mergedRanges` -- `range read` 遇到超时或响应过慢时,应缩小 `--range` 查询范围,**单次读取的单元格数量建议控制在 5000 个以内**;数据量较大时通过 `info` 获取边界后分批读取,避免不传 `--range` 直接读取整个大工作表 +- `table-get` 返回 table/dataframe 结构,适合需要 `columns` / `data` / `dtypes` / `formats` 的场景;它不返回 per-cell 元数据,也不替代 `range read` +- ★ 大整数和长数字标识符回读校验:精确 ID 应是字符串 / `object`;若看到 `float64`,说明它已经按数值路径处理,超过 `9007199254740991` 时可能已丢精度 +- `range read` 遇到超时或响应过慢时,应缩小 `--range` 查询范围,**单次读取的单元格数量建议控制在 5000 个以内**;数据量较大时通过 `info` 的 `nonEmptyRange.range` 获取 A1 边界后分批读取,避免不传 `--range` 直接读取整个大工作表 - ★ 当用户要求搜索/查找表格数据时,使用 `find` 命令,不要用 `range read` 读取全量数据后自行过滤——`find` 支持服务端搜索,效率更高、语义更准确 diff --git a/skills/mono/references/products/sheet/sheet-version.md b/skills/mono/references/products/sheet/sheet-version.md new file mode 100644 index 00000000..3a119745 --- /dev/null +++ b/skills/mono/references/products/sheet/sheet-version.md @@ -0,0 +1,69 @@ +# 历史版本 (version) + +## 使用场景 + +管理钉钉在线电子表格的历史版本快照。当用户说"保存版本/存个快照/看历史版本/版本列表/回滚到某个版本/恢复到之前的表格"时使用。 + +- 手动保存当前表格为一个版本快照 → `version save` +- 查看表格的历史版本列表(含版本号、名称、创建人、创建时间) → `version list`(别名 `ls`) +- 把表格回滚到指定历史版本 → `version revert`(危险操作,默认需二次确认) + +三个命令统一用 `--node` 指定表格文档(ID 或 URL)。回滚用的**版本号 `--version` 从 `version list` 的返回中获取**,禁止臆测。 + +## 命令详细参考 + +### 保存表格版本快照 +``` +Usage: + dws sheet version save [flags] +Example: + dws sheet version save --node SHEET_ID + +Flags: + --node string 表格文档 ID 或 URL (必填) +``` +手动为当前在线电子表格生成一个历史版本快照,便于后续查看或回滚。 + +### 查看表格历史版本列表 +``` +Usage: + dws sheet version list [flags] + dws sheet version ls [flags] +Example: + dws sheet version list --node SHEET_ID + dws sheet version list --node SHEET_ID --limit 10 + +Flags: + --node string 表格文档 ID 或 URL (必填) + --limit int 返回版本数量上限 (可选) + --cursor string 分页游标 (可选,游标分页) +``` +返回表格的历史版本列表;回滚前先用它拿到目标 `version`(版本号)。 + +### 回滚表格到指定版本 +``` +Usage: + dws sheet version revert [flags] +Example: + dws sheet version revert --node SHEET_ID --version 3 --yes + +Flags: + --node string 表格文档 ID 或 URL (必填) + --version int 目标版本号 (必填,从 version list 获取) +``` +把表格回滚到指定历史版本。**危险操作**:会覆盖当前内容,默认弹出二次确认;AI Agent 或脚本执行时可加全局 `--yes` 跳过确认。 + +## 上下文传递 + +| 操作 | 从返回中提取 | 用于 | +|------|-------------|------| +| `version list` | `version`(版本号) | 作为 `version revert --version` 的入参 | +| `version save` | 版本快照结果 | 确认已生成快照 | +| `version revert` | 回滚结果 | 确认回滚完成,回读表格内容验证 | + +## 注意事项 + +- ★ 回滚 `version revert` 会**覆盖当前表格内容**,属危险操作,默认二次确认;确认无误再执行,脚本/Agent 场景加 `--yes` +- ★ `--version` 必须来自 `version list` 的真实返回,禁止臆测版本号 +- 底层复用 doc 域的历史版本能力(`save_doc_version` / `list_doc_versions` / `revert_doc_version`),`--node` 传在线电子表格文档即可 +- 回滚后应用独立读命令(`csv-get` / `range read`)回读确认,避免"写返回不等于完成" diff --git a/skills/mono/references/products/sheet/sheet-workbook.md b/skills/mono/references/products/sheet/sheet-workbook.md index 6cbe121c..54c4ca48 100644 --- a/skills/mono/references/products/sheet/sheet-workbook.md +++ b/skills/mono/references/products/sheet/sheet-workbook.md @@ -27,13 +27,15 @@ - 复制并指定名称 → `copy --name "副本名称"` - 复制并指定位置 → `copy --index N` +用户说"显示网格线/显示网格/恢复网格线": +- 显示网格线 → `show-gridline`(连续多次调用幂等,不报错) + +用户说"隐藏网格线/去掉网格/关闭网格线/展示模式/看板模式": +- 隐藏网格线 → `hide-gridline`(连续多次调用幂等,不报错) + 用户说"删除工作表/移除工作表/删掉这个Sheet": - 删除工作表 → `delete-sheet`(不可逆操作,执行前必须向用户确认) -用户说"显示网格线/隐藏网格线/去掉单元格网格": -- 显示网格线 → `show-gridline` -- 隐藏网格线 → `hide-gridline` - ## 命令详细参考 ### 创建钉钉表格文档 @@ -141,13 +143,34 @@ Example: dws sheet info --node dws sheet info --node --sheet-id dws sheet info --node --sheet-id "Sheet1" + dws sheet info --node --sheet-id --include groups + dws sheet info --node --sheet-id --include row_heights,col_widths,hidden_rows,hidden_cols Flags: --node string 表格文档 ID 或 URL (必填) --sheet-id string 工作表 ID 或名称 (不传则返回第一个工作表) + --include strings 可选扩展信息,逗号分隔;支持 groups / row_heights / col_widths / hidden_rows / hidden_cols / frozen ``` 返回字段中 `mergedRanges` 是当前工作表的合并单元格范围列表(A1 表示法,如 `["C7:D11"]`)。它属于工作表结构/布局元数据:读写单元格内容前,如需判断表头、分组标题、续写位置或避开合并冲突,应先看 `sheet info`,不要在 `range read` / `csv-get` 的单元格值里寻找合并信息。 +返回字段中 `frozenRowCount` / `frozenColumnCount` 是冻结行列数量:冻结总是从工作表顶部第 1 行、左侧第 A 列开始连续计算;`0` 表示未冻结。它们是工作表级元数据,默认随 `sheet info` 返回,不需要额外 `include`。 + +最后非空数据边界通过 `nonEmptyRange` 返回,字段均为 A1/UI 语义:`range` 是从 `A1` 到最后非空单元格的范围,`lastCell` 是最后非空单元格地址,`lastRow` 是 1-based 行号,`lastColumn` 是列字母。空表时 `nonEmptyRange` 为 `null`。不要使用旧的 0-based 字段 `lastNonEmptyRow` / `lastNonEmptyColumn`。 + +需要读取行列分组时,加 `--include groups`。返回字段: +- `rowGroups`:行分组列表,单项包含 `range`、`startRow`、`endRow`、`count`、`level`、`collapsed` +- `columnGroups`:列分组列表,单项包含 `range`、`startColumn`、`endColumn`、`count`、`level`、`collapsed` + +其中 `range` 使用 A1 行/列范围(如 `"3:7"` / `"C:F"`),起止行号为 1-based,列使用字母;`level` 是 1-based 展示层级,不返回 `depth`;`collapsed` 表示当前折叠状态。`range read` / `csv-get` 不返回 `mergedRanges`、冻结或分组等结构元数据。 + +需要读取行高、列宽、隐藏行列时,按需加对应 `--include` 值(可组合,逗号分隔)。返回字段: +- `--include row_heights` → `rowHeights`(逐条行高,单项 `{row, pixelSize}`,`row` 为 1-based)+ `defaultRowHeight`(默认行高像素) +- `--include col_widths` → `colWidths`(逐条列宽,单项 `{column, pixelSize}`,`column` 为列字母)+ `defaultColumnWidth`(默认列宽像素) +- `--include hidden_rows` → `hiddenRows`(隐藏行区间列表,单项 `{startRow, endRow}`,1-based,连续隐藏行合并为一个区间) +- `--include hidden_cols` → `hiddenCols`(隐藏列区间列表,单项 `{startColumn, endColumn}`,列字母,连续隐藏列合并为一个区间) + +注意:`rowHeights` / `colWidths` 只返回**显式设置过**尺寸的行列(未自定义的行列不出现,用 `defaultRowHeight` / `defaultColumnWidth` 兜底);隐藏行列即使未改尺寸也会出现在 `hiddenRows` / `hiddenCols`。`frozen` 也在 include 白名单中,但冻结字段本就默认返回,传 `frozen` 与否结果一致。 + ### 新建工作表 ``` Usage: @@ -183,16 +206,14 @@ Flags: --node string 表格文档 ID 或 URL (必填) --sheet-id string 工作表 ID 或名称 (必填) --name string 新名称,最长 100 字符,不能包含 / \ ? * [ ] : - --title string --name 的别名(兼容) --index int 新位置(从 0 开始) --hidden --hidden=true 隐藏,--hidden=false 取消隐藏 - --tab-color string 工作表标签颜色,Hex 如 #FF0000;传空字符串清除颜色 --frozen-row-count int 冻结行数,0 表示取消冻结 --frozen-column-count int 冻结列数,0 表示取消冻结 ``` -更新工作表名称、位置、隐藏状态、标签颜色、冻结行列。 -`--name`(别名 `--title`)/ `--index` / `--hidden` / `--tab-color` / `--frozen-row-count` / `--frozen-column-count` 至少提供一个;多个属性可同时传入,将在同一次请求中更新。 +更新工作表名称、位置、隐藏状态、冻结行列。 +`--name` / `--index` / `--hidden` / `--frozen-row-count` / `--frozen-column-count` 至少提供一个;多个属性可同时传入,将在同一次请求中更新。 注意: - 至少需要保留一个可见的工作表,不能将所有工作表都隐藏 @@ -240,17 +261,33 @@ Flags: - 不能删除隐藏的工作表(需先通过 `sheet update --hidden false` 取消隐藏再删除) - 不能删除最后一个可见工作表(至少保留一个可见工作表) -### 显示或隐藏网格线 +### 显示工作表网格线 ``` Usage: + dws sheet show-gridline [flags] +Example: dws sheet show-gridline --node --sheet-id - dws sheet hide-gridline --node --sheet-id + dws sheet show-gridline --node --sheet-id "Sheet1" Flags: --node string 表格文档 ID 或 URL (必填) --sheet-id string 工作表 ID 或名称 (必填) ``` -这两个命令只修改指定工作表的网格线显示状态,不修改单元格内容或边框样式。执行后可用同一命令反向恢复。 +### 隐藏工作表网格线 +``` +Usage: + dws sheet hide-gridline [flags] +Example: + dws sheet hide-gridline --node --sheet-id + dws sheet hide-gridline --node --sheet-id "Sheet1" +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) +``` + +切换子表网格线显隐;二态语义在命令名里,无需额外参数(同 `update --hidden` / `update --hidden=false` 的隐藏/显示工作表模式)。 +网格线默认显示;隐藏后工作表背景为纯白色,适合截图、演示、仪表盘/看板场景(不影响打印和数据)。 +连续多次 show 或多次 hide 均为幂等操作,不会报错。 ## 核心工作流 @@ -263,13 +300,16 @@ dws sheet create --name "销售数据" --format json # 2. 查看工作表列表 — 提取 sheetId dws sheet list --node --format json -# 3. 写入表头和数据(每个单元格必须是 object;数字也写成字符串) +# 3. 写入表头和数据 dws sheet range update --node --sheet-id --range "A1:C1" \ --values '[[{"type":"text","text":"姓名"},{"type":"text","text":"部门"},{"type":"text","text":"销售额"}]]' --format json dws sheet range update --node --sheet-id --range "A2:C4" \ --values '[[{"type":"text","text":"张三"},{"type":"text","text":"销售部"},{"type":"text","text":"50000"}],[{"type":"text","text":"李四"},{"type":"text","text":"市场部"},{"type":"text","text":"38000"}],[{"type":"text","text":"王五"},{"type":"text","text":"销售部"},{"type":"text","text":"62000"}]]' --format json +# 4. 独立回读实际写入范围 +dws sheet range read --node --sheet-id --range "A1:C4" --format json + # ── 工作流 2: 读取已有表格数据 ── # 1. 获取工作表列表 @@ -289,7 +329,7 @@ dws sheet range read --node --sheet-id --range "A1:D10" --f # 1. 新建工作表 dws sheet new --node --name "汇总" --format json -# 2. 在新工作表中写入汇总公式(公式写在 text,以 = 开头) +# 2. 在新工作表中写入汇总公式 dws sheet range update --node --sheet-id --range "A1:B1" \ --values '[[{"type":"text","text":"指标"},{"type":"text","text":"数值"}]]' --format json @@ -304,7 +344,7 @@ dws sheet range update --node --sheet-id --range "A2:B2 | `create` | `nodeId` | list / info / new / range read / range update / find 的 --node | | `list` | 工作表的 `sheetId` | info / range read / range update / find 的 --sheet-id | | `new` | 新工作表的 `sheetId` | range read / range update / find 的 --sheet-id | -| `info` | `rowCount` / `nonEmptyRange.range` / `nonEmptyRange.lastRow` / `nonEmptyRange.lastColumn` / `mergedRanges` | 确定数据范围、追加写入起始行、判断合并单元格结构 | +| `info` | `rowCount` / `nonEmptyRange.range` / `nonEmptyRange.lastRow` / `nonEmptyRange.lastColumn` / `mergedRanges` / `frozenRowCount` / `frozenColumnCount` / `rowGroups` / `columnGroups` / `rowHeights` / `colWidths` / `hiddenRows` / `hiddenCols` | 确定数据范围、追加写入起始行、判断合并单元格结构、回读冻结与行列分组、回读行高列宽与隐藏行列 | ## 注意事项 @@ -328,4 +368,6 @@ dws sheet range update --node --sheet-id --range "A2:B2 - `delete-sheet` 为不可逆操作,执行前必须向用户确认 - `delete-sheet` 不能删除隐藏的工作表,需先通过 `update --hidden=false` 取消隐藏再删除 - `delete-sheet` 不能删除最后一个可见工作表,至少保留一个可见工作表 +- `show-gridline` / `hide-gridline` 为幂等写操作:连续调用同一命令不会报错,适合 Agent 不确定当前状态时直接调用 +- `show-gridline` / `hide-gridline` 仅控制网格线视觉显示,不影响数据、打印、冻结行列等任何其他属性 - ★ 关键区分: sheet(电子表格/单元格读写) vs aitable(AI多维表/结构化记录/字段定义) vs doc(文档编辑/阅读) diff --git a/skills/mono/references/products/sheet/sheet-write-data.md b/skills/mono/references/products/sheet/sheet-write-data.md index 81944993..eaa5e695 100644 --- a/skills/mono/references/products/sheet/sheet-write-data.md +++ b/skills/mono/references/products/sheet/sheet-write-data.md @@ -3,12 +3,18 @@ ## 使用场景 用户说"写数据/填表/更新单元格/写入公式": -- 从 DataFrame/结构化对象一次写入一个或多个工作表 → `table-put` - 更新数据 → `range update` +- 写入公式前先读 [sheet-formula](./sheet-formula.md);写完必须回读公式文本并运行 `formula-verify`,关键业务数值还要抽样对账,不能只看写入返回成功 - 【强制】`--sheet-id` 必填:即使是单工作表也不能省略,不要参照 `range read` 的默认行为;未知时先执行 `dws sheet list --node --format json` 获取 `sheetId`,禁止凭空臆测为 `Sheet1`、`sheet1`、`0`、`default` 等 - 注意:如果用户的目的是替换文本、移动行列、追加空行空列、清空区域、排序、填充、复制区域或移动区域,请勿使用 `range update`,必须使用对应的专用命令(`replace`/`move-dimension`/`add-dimension`/`range clear`/`range sort`/`range fill`/`range copy-to`/`range move-to`) - **批量 CSV 值/公式写入优先用 `csv-put`**:当写入场景同时满足以下条件时,必须优先使用 `csv-put` 而非 `range update`:(1) 不需要超链接、dataValidation、cellStyles 或 richText;(2) 数据量较大(超过 5 行或超过 20 个单元格);(3) 数据来源为表格/CSV 文本/结构化文本。`csv-put` 无需手动构造二维 JSON 数组,字段值以 `=` 开头时按公式解析,并支持自动扩容 +用户说"写入结构化 table/dataframe/带列类型和格式的数据/跨 sheet 写入表格": +- 写入结构化 table → `table-put` +- `table-put` 的 `--sheets` 是 table spec JSON,支持一次写入一个或多个工作表;适合有明确 `columns` / `data` / `dtypes` / `formats` 的数据 +- table spec 较长或来自文件时,优先用 `--sheets @table.json`,避免 shell 转义长 JSON 出错 +- 如果只是一段 CSV 文本且没有 dtype/format 需求,用 `csv-put` 更简单(可含 `=` 开头的公式);如果需要超链接、dataValidation、richText 或 cellStyles,用 `range update` + 用户说"追加数据/添加行/在末尾加数据/新增记录": - 追加数据 → `append` @@ -17,38 +23,34 @@ - 与 `range update` 的区别:`csv-put` 接受 CSV 文本直接写入,无需手动构造二维 JSON 数组;适合大批量值或公式写入,但不支持富格式对象 - 与 `append` 的区别:`csv-put` 写入指定位置(--start-cell),`append` 在末尾追加 +**与现有写入能力的区别**: + +- `range update` 面向精确单元格对象写入:适合少量公式、超链接、dataValidation、richText、少量 cellStyles 和 `{}` 跳过;必须自己提供 `--sheet-id`、`--range` 和维度完全匹配的二维 cell 对象 +- `append` 面向简单追加行:自动定位到末尾,只写原始值,不支持样式、公式、超链接或数据验证 +- `csv-put` 面向大批量 CSV 值/公式导入:输入 CSV 文本,写入指定起点;字段值以 `=` 开头时按公式解析,前加单引号时写入以 `=` 开头的字面文本;不保留 dtype/format 协议,也不支持富格式 +- `table-put` 面向 dataframe/table 数据交换:输入 `columns` / `data` / `dtypes` / `formats`,可一次写多个 sheet,适合和 `table-get` 往返;不支持 dataValidation、hyperlink、richText、附件或图片 + **四种写入命令能力对比**: -| 能力 | `table-put` | `range update` | `append` | `csv-put` | -|------|-------------|----------------|----------|-----------| -| 多工作表结构化写入 | 支持 | 不支持 | 不支持 | 不支持 | -| columns / dtypes / formats | 支持 | 不支持 | 不支持 | 不支持 | -| 公式(`=` 开头) | 按 dtype/底层能力 | 支持 | 不支持 | 支持;前导单引号写入以 `=` 开头的字面文本 | -| 单元格级超链接(`hyperlink`) | 不使用此命令 | 支持 | 不支持 | 不支持 | -| 富文本(片段链接/附件/图片) | 不使用此命令 | 支持 | 不支持 | 不支持 | -| 原始值(纯数字/字符串) | 支持 | 支持 | 支持 | 支持 | -| 自动定位末尾 | mode=append | 不支持 | 支持 | 不支持 | -| 自动扩容行列 | 支持 | 不支持 | 支持 | 支持 | +| 能力 | `range update` | `append` | `csv-put` | `table-put` | +|------|---------------|----------|-----------|-------------| +| 公式(`=` 开头) | 支持 | 不支持 | 支持;前导单引号写入以 `=` 开头的字面文本 | 不作为公式协议;公式请用 `range update` 或 `csv-put` | +| 单元格级超链接(`hyperlink`) | 支持 | 不支持 | 不支持 | 不支持 | +| 富文本(片段链接/附件/图片) | 支持 | 不支持 | 不支持 | 不支持 | +| richText 片段样式(bold/color) | 支持 | 不支持 | 不支持 | 不支持 | +| `cellStyles`(背景色/字号/对齐等 cell-level 样式) | 支持 | 不支持 | 不支持 | 支持基础 cell-level 样式 | +| `{}` 跳过(保留原值) | 支持 | 不适用 | 不适用 | 不支持,按 table 矩阵写入 | +| `dataValidation`(下拉/复选框) | 支持 | 不支持 | 不支持 | 不支持 | +| 原始值(纯数字/字符串/布尔/null) | 支持 | 支持 | 支持 | 支持 | +| 列级 dtype / number format | 手工写 cellStyles | 不支持 | 自动识别为主 | 支持 `dtypes` / `formats` | +| 自动定位末尾 | 不支持 | 支持 | 不支持 | `mode:"append"` 支持 | +| 自动扩容行列 | 不支持 | 支持 | 支持 | 支持 | +| 跨 sheet 一次写入 | 不支持 | 不支持 | 不支持 | 支持 `sheets[]` | + +公式写入与校验的完整流程见 [sheet-formula](./sheet-formula.md)。写公式后先回读 `formula` 模式确认公式文本,再运行 `formula-verify` 聚合扫描错误;需要确认业务数值时继续回读 `raw_value` 抽样对账。 ## 命令详细参考 -### 写入结构化 table 数据 -``` -Usage: - dws sheet table-put [flags] # 别名: dws sheet table-write -Example: - dws sheet table-put --node \ - --sheets '[{"name":"Data","columns":["name","score"],"data":[["Alice",95]],"dtypes":{"score":"float64"}}]' - - dws sheet table-put --node --sheets @table.json - cat table.json | dws sheet table-put --node --sheets - -Flags: - --node string 表格文档 ID 或 URL (必填) - --sheets string sheet table JSON、@文件路径 或 - 表示 stdin (必填) -``` - -`--sheets` 接受 JSON 数组、`{"sheets":[...]}` 包装对象或单个 sheet 对象,CLI 会统一转换为非空数组。每个 sheet 至少需要 `columns`,并提供 `sheetId` 或 `name`;`name` 必须非空且少于 31 个字符。写入后必须用 `table-get` 回读验证。`table-get/table-put` 不支持放入 `sheet batch-update`。 - ### 更新工作表指定区域内容 ``` Usage: @@ -80,7 +82,7 @@ Flags: --values string 单元格内容,二维 JSON 数组 (必填);每个元素必须是 object:{type:text,text:...}、{type:richText,texts:[...]}、{dataValidation:...}、{cellStyles:...}、{hyperlink:...} 或 {}(详见下文 values 参数格式说明) ``` -**合并单元格注意(`range update`)**:这里说的是 `range update` 写入单元格对象这一路径,不是所有写入命令的统一行为。目标范围与已有合并区域冲突时,MCP 服务端会拦截并返回 `MERGED_CELLS_CONFLICT` 错误,错误消息中通常会列出具体冲突的合并区域地址。收到此错误时按以下流程处理: +**合并单元格注意(`range update`)**:这里说的是 `range update` 写入单元格对象这一路径,不是所有写入命令的统一行为。目标范围与已有合并区域冲突时,服务端会拦截并返回 `MERGED_CELLS_CONFLICT` 错误,错误消息中通常会列出具体冲突的合并区域地址。收到此错误时按以下流程处理: 1. 从错误消息中获取冲突的合并区域地址(如 `A1:B2, C3:D4`),或通过 `dws sheet info --node --sheet-id --format json` 查询完整的合并区域列表(`mergedRanges` 数组) 2. 用 `dws sheet unmerge-cells --range <冲突区域>` 取消这些合并 3. 执行 `range update` 写入数据 @@ -94,6 +96,8 @@ Flags: **范围职责**:`range update` 负责写入单元格内容(原始值/公式/富文本对象),并支持通过 `cellStyles` 附带 per-cell 样式。如需批量设置整片区域的样式(不写值),请使用 `dws sheet range set-style`。 +**公式校验要求**:公式用 `{"type":"text","text":"=..."}` 写入。写完后先执行 `range read --value-render-option formula` 确认公式文本,再执行 `formula-verify` 聚合扫描错误;关键业务结果继续用 `range read --value-render-option raw_value` 抽样对账。详见 [sheet-formula](./sheet-formula.md)。 + ### 在工作表末尾追加数据 ``` Usage: @@ -116,14 +120,12 @@ Flags: Usage: dws sheet csv-put [flags] Example: - # 内联多行 CSV:必须用 $'...' 让 \n 变成真换行;普通单引号 '...\n...' 里的 \n 是字面量,会写成一行含字面 \n 的错误数据 dws sheet csv-put --node --sheet-id --start-cell A1 \ - --csv $'name,score\nAlice,95\nBob,87' + --csv 'name,score\nAlice,95\nBob,87' dws sheet csv-put --node --sheet-id --start-cell A1 \ --csv "=1+1,'=1+1" - # 多行数据推荐用 @文件,最稳妥 dws sheet csv-put --node --sheet-id --start-cell B2 \ --csv @data.csv --allow-overwrite @@ -143,7 +145,7 @@ Flags: 将 RFC 4180 格式的 CSV 文本写入指定工作表的指定单元格位置。 - **分隔符必须是英文逗号 `,`**(ASCII 0x2C),禁止使用中文逗号 `,`(U+FF0C)。中文逗号不会被识别为分隔符,会导致整行被写入同一个单元格。生成 CSV 内容时务必检查分隔符 - 写入值和公式,不支持样式/批注。字段值以 `=` 开头时默认按公式解析;如需写入以 `=` 开头的字面文本,在字段值前加单引号(例如 `'=1+1`) -- 数字/日期/百分数由表格引擎自动识别类型(如 `95` 存为数字,`2025-03-01` 存为日期) +- 数字/日期/百分数由表格系统自动识别类型(如 `95` 存为数字,`2025-03-01` 存为日期) - 自动扩容行列:CSV 数据超出当前工作表维度时自动追加行/列 - 与 `range update` 不同,目标区域如含合并单元格,`csv-put` 会打散合并并写入 CSV 数据 - 若需要保留原有合并结构,写入前先用 `sheet info` 记录 `mergedRanges`,写入后用 `merge-cells` 恢复对应区域 @@ -152,6 +154,121 @@ Flags: - CSV 文本上限 2M 字符,单元格总数上限 30000 - 特殊字符处理:CLI 会自动过滤 `\r`(Windows 换行符)和 BOM(UTF-8 文件头标记),Excel/Windows 导出的 CSV 可直接使用;如 CSV 数据中含零宽字符(U+200B 等)或 Bidi 控制符,CLI 会拒绝并报错 +### 写入结构化 table 数据 +``` +Usage: + dws sheet table-put [flags] +Example: + dws sheet table-put --node \ + --sheets '[{"name":"Sheet1","columns":["name","score"],"data":[["Alice",95]],"dtypes":{"score":"float64"},"formats":{"score":"0"}}]' + + dws sheet table-put --node --sheets @table.json + cat table.json | dws sheet table-put --node --sheets - +Flags: + --node string 表格文档 ID 或 URL (必填) + --sheets string sheet table JSON、@文件路径 或 - 表示 stdin (必填) +``` + +`--sheets` 支持三种形态: +```json +[ + {"name":"Sheet1","columns":["name"],"data":[["Alice"]]} +] +``` +```json +{ + "sheets": [ + {"name":"Sheet1","columns":["name"],"data":[["Alice"]]} + ] +} +``` +```json +{"name":"Sheet1","columns":["name"],"data":[["Alice"]]} +``` + +**单个 sheet spec 字段**: + +| 字段 | 必填 | 说明 | +|------|------|------| +| `name` | 与 `sheetId` 二选一 | 工作表名称。名称已存在时写入已有 sheet;不存在时创建同名 sheet | +| `sheetId` | 与 `name` 二选一 | 工作表 ID;传入时优先于 `name`,不会按 `name` 创建新 sheet | +| `startCell` | 否 | overwrite 起始单元格,或 append 起始列;默认 `A1` | +| `mode` | 否 | `overwrite`(默认)或 `append` | +| `header` | 否 | 是否写入表头。overwrite 默认 true;append 默认 false;append 到空表且未显式指定时写表头 | +| `allowOverwrite` | 否 | 是否允许覆盖已有值。默认 true;显式传 false 时,目标区域已有值会报错 | +| `columns` | 是 | 有序列名数组;不可为空、不可重复、不可包含空列名 | +| `data` | 否 | 行优先二维数组,默认 `[]`;每行宽度必须等于 `columns.length`;值支持 string / number / boolean / null。写入空位建议传 `null`;若复用 `table-get` 结果,`data[][]` 中的 `{}` 表示空位 / 空单元格,按 `null` 对待 | +| `dtypes` | 否 | 按列名指定 pandas-style dtype;默认 `{}`,未指定列按 object/string 写入。普通文本建议声明 `object`;`string` 会按文本写入,但 `table-get` 回读通常仍是 `object` | +| `formats` | 否 | 按列名指定 number format code;默认 `{}`,系统按 dtype 补日期/文本默认格式 | +| `cellStyles` | 否 | 基础 cell-level 样式,默认不设置样式;支持二维样式矩阵,或按列名指定样式对象。字段范围与下文 `cellStyles 子结构` 一致 | + +**完整示例**: +```json +{ + "sheets": [ + { + "name": "销售", + "startCell": "A1", + "mode": "overwrite", + "allowOverwrite": true, + "columns": ["日期", "金额", "渠道"], + "data": [ + ["2026-07-06", 1500.5, "门店"], + ["2026-07-07", 2300.75, "线上"] + ], + "dtypes": { + "日期": "datetime64[ns]", + "金额": "float64", + "渠道": "object" + }, + "formats": { + "日期": "yyyy-mm-dd", + "金额": "¥#,##0.00" + }, + "cellStyles": { + "金额": {"numberFormat": "¥#,##0.00"} + } + } + ] +} +``` + +**dtype 与 format 规则**: +- `datetime64[ns]` / `date` 按日期写入;日期值建议使用 ISO 字符串(如 `2026-07-06`),未传 format 时默认 `yyyy-mm-dd` +- `bool` / `boolean` 按布尔写入 +- `int*` / `uint*` / `float*` / `complex*` 按数值写入;读回时数值列通常统一表现为 `float64` +- `object` 是普通文本列的推荐 dtype;`string` 等其他 dtype 按字符串列处理,未传 format 时默认文本格式 `@`。读回时文本列通常表现为 `object`,不要期待返回 `string` +- `formats` 使用表格 number format code,含义与 `range set-style --number-format` 一致 +- 大整数精度保护:超过 `9007199254740991` 的整数、长数字 ID、订单号、手机号、SKU 等需要逐位精确的值,必须按文本写入。`table-put` 中即使把值写成字符串,只要 dtype 声明为 `int64` / `uint64` / `float64`,链路仍会按 number 处理并可能丢精度;应使用字符串值 + `object` dtype + 文本格式 `@` + +**大整数/长数字 ID 示例**: +```json +{ + "name": "订单", + "columns": ["order_id", "amount"], + "data": [["9007199254740993", 128.5]], + "dtypes": {"order_id": "object", "amount": "float64"}, + "formats": {"order_id": "@", "amount": "#,##0.00"} +} +``` + +**返回字段说明**: +- `sheets` — 写入结果数组,每项对应一个 sheet spec +- `name` / `sheetId` — 实际写入的工作表名称与 ID +- `range` — 实际写入范围 +- `dataRows` — 写入的数据行数,不含表头 +- `columns` — 写入列数 +- `writes` — 实际写入次数 +- `mode` — 实际写入模式,`overwrite` 或 `append` + +**限制与边界**: +- `table-put` 支持跨 sheet:一个 `sheets[]` 可包含多个 sheet spec +- 单次 table 写入单元格数上限 30000,计算包含表头行;超过上限需拆分 +- 大整数和长数字标识符不应声明为 `int64` / `uint64`。例如 `9007199254740993` 按数值写入会回读为 `9007199254740992`;要保精度必须写成 `"9007199254740993"`,并把该列声明为 `object` 且设置 `formats: {"列名":"@"}` +- `table-put` 不支持 dataValidation、hyperlink、richText、附件、单元格图片;这些能力用 `range update` / `write-image` +- 样式能力以 `cellStyles` 基础 cell-level 样式为准;复杂结构样式、合并单元格、行高列宽请另用 `range set-style` / `merge-cells` / `update-dimension` +- 写入后需要用 `table-get` 回读 `columns` / `data` / `dtypes` / `formats` 校验;`table-get` 不返回 `cellStyles`,需要校验 table 写入样式时用 `range read`;需要校验合并或结构元数据时用 `sheet info` + ## values 参数格式说明 `range update` 只接受 `--values` 一个数据参数,为二维 JSON 数组,第一维为行,第二维为列。每个 cell 是以下之一: @@ -187,7 +304,7 @@ Flags: `hyperlink` 作用于整个单元格,适合“这个单元格整体可点击跳转”的场景。它和 richText 的片段级 `link` 不同。 > **hyperlink 三种语义**: -> - **不传 `hyperlink` 字段** → 保留原超链接(引擎自动 readback 回写) +> - **不传 `hyperlink` 字段** → 保留原超链接(系统自动保留) > - **`hyperlink: {"type":"none"}`** → 显式清除单元格超链接 > - **`hyperlink: {"type":"path"/"sheet"/"range", link, text?}`** → 写新超链接(覆盖) > @@ -208,7 +325,7 @@ Flags: 注意: - 不传 `hyperlink` 字段同于 “保留原超链接”,无需先 read 再回传 -- Agent 调用统一使用 `hyperlink: {type:"none"}` 清除超链接;底层 REST 兼容 `hyperlink:null`,但 MCP schema / 网关可能过滤 null 字段,不要把 null 当默认写法 +- Agent 调用统一使用 `hyperlink: {type:"none"}` 清除超链接;不要把 `hyperlink:null` 当默认写法,避免空字段在调用链路中被过滤后语义不清 - `hyperlink` 可以不带 `type/text` cell 单独出现,用于只设置或清理链接并保留原值 - 不要把 `hyperlink` 和 `type:"richText"` 混用;整格链接用 `hyperlink`,片段链接用 richText 子项 `type:"link"` @@ -341,7 +458,7 @@ dws sheet range update --node NODE_ID --sheet-id SHEET_ID --range "A1:C1" \ ### 重要约束 -- 不再支持 `{type:"number"}` / `{type:"boolean"}` / `{type:"null"}` —— MCP `complexValues` 仅接受 `text` / `richText` 两种 type,或 `{}` 跳过。数字 / 布尔走 `{type:"text","text":"<字符串形式>"}` +- 不再支持 `{type:"number"}` / `{type:"boolean"}` / `{type:"null"}` —— 当前单元格对象协议仅接受 `text` / `richText` 两种 type,或 `{}` 跳过。数字 / 布尔走 `{type:"text","text":"<字符串形式>"}` - 不支持直接传入原始值(字符串、数字、布尔、null、空字符串);`null` 不等同于 `{}`,`null` 会报错 - 维度必须与 `--range` 范围完全一致,例如 `--range "A1:B3"` 需要 3 行 2 列的数组 - 清理整格超链接使用 `{"hyperlink":{"type":"none"}}`;不要使用 `{"hyperlink":null}` 作为 agent 默认调用形态 @@ -366,6 +483,9 @@ dws sheet range update --node --sheet-id --range "A1:C1" \ dws sheet range update --node --sheet-id --range "A2:C4" \ --values '[[{"type":"text","text":"张三"},{"type":"text","text":"销售部"},{"type":"text","text":"50000"}],[{"type":"text","text":"李四"},{"type":"text","text":"市场部"},{"type":"text","text":"38000"}],[{"type":"text","text":"王五"},{"type":"text","text":"销售部"},{"type":"text","text":"62000"}]]' --format json +# 4. 独立回读实际写入范围 +dws sheet range read --node --sheet-id --range "A1:C4" --format json + # ── 工作流 4: 写入数据并设置样式 ── # 1. 写入数据 @@ -407,12 +527,13 @@ dws sheet append --node --sheet-id \ | 操作 | 从返回中提取 | 用于 | |------|-------------|------| -| `create` | `nodeId` | list / info / new / range update / append / csv-put 的 --node | -| `list` | 工作表的 `sheetId` | range update / append / csv-put 的 --sheet-id | -| `new` | 新工作表的 `sheetId` | range update / append / csv-put 的 --sheet-id | +| `create` | `nodeId` | list / info / new / range update / append / csv-put / table-put 的 --node | +| `list` | 工作表的 `sheetId` | range update / append / csv-put 的 --sheet-id;table-put 的 sheetId | +| `new` | 新工作表的 `sheetId` | range update / append / csv-put 的 --sheet-id;table-put 的 sheetId | | `info` | `rowCount` / `nonEmptyRange.range` / `nonEmptyRange.lastRow` / `nonEmptyRange.lastColumn` / `mergedRanges` | 确定数据范围、追加写入起始行、识别合并单元格结构 | | `append` | `a1Notation` 追加数据所在范围 | 确认追加位置 | | `csv-put` | `a1Notation` 实际写入的单元格范围 | 确认写入位置和范围 | +| `table-put` | `sheets[].sheetId` / `sheets[].range` / `sheets[].dataRows` | 确认实际写入的 sheet、范围和行数;再用 `table-get` 回读类型与格式 | ## 注意事项 @@ -423,14 +544,17 @@ dws sheet append --node --sheet-id \ - 维度不足请按行 / 列补齐为同等大小;不需要修改的位置用 `{}` 跳过(保留原值),需要清空的位置用 `{"type":"text","text":""}`;禁止出现各行列数不一致或与 `--range` 不匹配的情况,否则调用会直接报错 - 如需写整格超链接,把 `{"type":"text","text":"...","hyperlink":{"type":"path","link":"..."}}` 放进 `--values` 二维数组对应的单元格里;富文本片段链接才使用 richText 子项 `type:"link"` - ★ **清空区域优先用 `range clear`(强制)**:需要清空整片区域时必须使用 `range clear`,禁止用 `range update` 模拟。仅在 `range update` 写入混合数据时个别 cell 需要清空,才在 `--values` 中用 `{"type":"text","text":""}` -- ★ **不再支持 `{type:"number"}` / `{type:"boolean"}` / `{type:"null"}`(强制)**:MCP `complexValues` 仅接受 `type:"text"` 与 `type:"richText"` 两种,CLI 会在本地直接拦截非法 type 并报错。写数字 / 布尔请用 `{"type":"text","text":"<字符串形式>"}`(服务端按内容自动识别),不要再用旧的 `value` 字段 -- **dataValidation 三语义**:不传字段=保留;`{type:"none"}`=清除;`{type:"dropdown"/"checkbox",...}`=覆盖。无需先 read 再回传,引擎自动保留原 DV +- ★ **不再支持 `{type:"number"}` / `{type:"boolean"}` / `{type:"null"}`(强制)**:当前单元格对象协议仅接受 `type:"text"` 与 `type:"richText"` 两种,CLI 会在本地直接拦截非法 type 并报错。写数字 / 布尔请用 `{"type":"text","text":"<字符串形式>"}`(服务端按内容自动识别),不要再用旧的 `value` 字段 +- **dataValidation 三语义**:不传字段=保留;`{type:"none"}`=清除;`{type:"dropdown"/"checkbox",...}`=覆盖。无需先 read 再回传,系统会保留原 DV - **hyperlink 三语义**:不传字段=保留;`{type:"none"}`=清除;`{type:"path"/"sheet"/"range",...}`=覆盖。Agent 调用不要使用 `hyperlink:null` - ★ **单次调用上限(强制)**:`range update` / `set-style` 行数 ≤ 1000,单元格总数建议 ≤ 5000(硬限 30000) - ★ **大批量 CSV 值/公式写入用 `csv-put` 不用 `range update`**:当数据可表达为 CSV(可含 `=` 开头的公式)、不需要超链接或富文本对象且数据量较大时(>5 行或 >20 单元格),必须使用 `csv-put`。它无需构造二维 JSON 数组并支持自动扩容。需要单元格级超链接、富文本对象、样式、数据验证、`{}` 跳过,或仅更新少量单元格时使用 `range update` +- ★ **公式写完必须分层校验**:先用 `range read --value-render-option formula` 确认公式文本,再用 `formula-verify` 聚合扫描错误;若返回 `partial` / `hasMore=true` 必须继续校验,关键业务数值还要用 `raw_value` 抽样对账 +- ★ **结构化 table 写入用 `table-put`**:需要列名、dtype、number format、跨 sheet specs 或 `table-get` 回写形态时使用 `table-put`;单次写入单元格数硬限 30000,包含表头 +- ★ **大整数/长数字 ID 必须按文本写(强制)**:超过 `9007199254740991` 的整数、订单号、手机号、SKU 等需要逐位精确的值,不要用 JSON number,也不要声明 `int64` / `uint64`。用字符串值 + `object` dtype + 文本格式 `@`;写后用 `table-get` 或 `range read --value-render-option raw_value` 回读确认 - `range update` 必填 `--values`;单元格级超链接通过 cell 的 `hyperlink` 字段表达,附件 / 图片 / 带样式片段通过 `--values` 内的 richText 富格式表达,CLI 不再有 `--hyperlinks` 参数 - `range update` 职责边界:`range update` 写入单元格内容(文本 / 公式 / 富文本对象),支持通过 `cellStyles` 附带 per-cell 样式(背景色 / 字号 / 对齐等)。但批量刷整片区域的统一样式时,应使用 `dws sheet range set-style`(如 "给表头加粗居中")或 `dws sheet range batch-set-style --batch `。两种方式各有适用场景:少量 cell 写值 + 样式一步到位用 `cellStyles`;大面积统一样式用 `set-style` - `append` 自动定位到最后一行有数据的位置下方插入,无需手动计算行号 - `append` 的 `--values` 二维数组中每行的列数必须一致,否则会报错。如果用户提供的数据中各行长度不同,必须先将短行用空字符串 `""` 补齐到与最长行相同的列数后再调用。追加的数据列数也应与工作表已有数据列数保持一致 - `append` vs `range update`:追加新行用 `append`,修改已有单元格用 `range update` -- ★ **`append` / `csv-put` 不支持 `{}` skip、`dataValidation`、富文本或超链接**:这些能力仅限 `range update`。`append` 只接受原始值;`csv-put` 接受 CSV 值和公式,字段值以 `=` 开头时按公式解析,前加单引号时写入以 `=` 开头的字面文本。需要下拉列表、富格式或跳过部分单元格时,必须使用 `range update` +- ★ **`append` / `csv-put` / `table-put` 不支持 `{}` skip、dataValidation、富文本、超链接**:这些能力仅限 `range update`。`append` 只接受原始值;`csv-put` 接受 CSV 值和公式;`table-put` 接受 table 矩阵、dtypes、formats 和基础 `cellStyles`。需要超链接、下拉列表、富文本或跳过部分单元格时,必须使用 `range update` diff --git a/skills/multi/dingtalk-chat/SKILL.md b/skills/multi/dingtalk-chat/SKILL.md index 2f3c0b36..8e1e3b3b 100644 --- a/skills/multi/dingtalk-chat/SKILL.md +++ b/skills/multi/dingtalk-chat/SKILL.md @@ -75,7 +75,7 @@ metadata: - `+messages-send`:文件、Bot、Webhook、复杂 @ 或幂等控制。user 已知 ID 可直接传,也可用 `--user-query` / `--chat-query` 运行同一只读解析链;Bot 多群使用 `--groups/--groups-file`,返回 `im.batch-write.v1`;bot/webhook 只使用下层真实支持的文本/Markdown 能力。 - 文件直接传 `+messages-send --file <相对路径>`;不要先独立上传并提取 mediaId。 - Webhook 使用 `+messages-send --as webhook --webhook-token `;不要退回原子 Webhook 命令。 -- 流式卡片不是普通消息内容,使用 `+messages-send-card`;当前只支持 streaming text create/update,不支持 Card JSON 或 callback。 +- 流式卡片用 `+messages-send-card`;群聊@传 ID/`--at-all`,Runtime 把 create 返回前缀加到 `--content`;禁写占位符;仅 text。 ## 关键结果语义 @@ -93,7 +93,7 @@ metadata: | 场景 | Reference | |---|---| | 复杂发送、跨会话转发、共同群或组合流程 | [01-messaging.md](references/01-messaging.md) | -| 引用、转发、卡片、reaction、Pin/Top/Favorite | [chat-message.md](references/chat/chat-message.md) | +| 编辑/撤回/引用/转发/卡片/reaction/Pin/Top/Favorite | [chat-message.md](references/chat/chat-message.md) | | 建群、成员、管理员、群公告、群设置 | [chat-group.md](references/chat/chat-group.md) | | Bot 搜索、入群、群发和撤回 | [chat-bot.md](references/chat/chat-bot.md) | | 会话置顶、分类、红点、免打扰和隐藏 | [chat-conversation.md](references/chat/chat-conversation.md) | diff --git a/skills/multi/dingtalk-chat/references/card/create.md b/skills/multi/dingtalk-chat/references/card/create.md index 41a8ed33..e7a1fbed 100644 --- a/skills/multi/dingtalk-chat/references/card/create.md +++ b/skills/multi/dingtalk-chat/references/card/create.md @@ -7,9 +7,18 @@ Runtime 会唯一解析为 openDingTalkId;已有 openDingTalkId 时传 - 只传目标:创建卡片并从真实结果取得 `bizId`,供后续更新。 - 同时传 `--content`:Runtime 串行执行 create → 从返回提取 `bizId` → update;默认 `--flow-status 3`。 +- 群聊可传 `--at-open-dingtalk-ids` 或 `--at-all`;艾特对象只进入初始 + `create_and_send_card`。同一次调用带 `--content` 时,Runtime 将 create 返回的 + `atTag` 自动加在正文前,再调用 `update_streaming_card`;调用方不要拼 ID + 或艾特占位符。 - `--dry-run` 仍执行只读 userId 解析,只输出两步计划,不执行写入。 创建成功但自动更新失败时,错误会保留真实 `bizId`。不要重复创建;使用该 `bizId` 继续 `+messages-update-card` 或人工处理。 当前内容仅为 streaming text,不接受 Lark Card JSON、组件树或按钮 callback。 + +```bash +dws chat +messages-send-card --group --at-open-dingtalk-ids --content "请确认" +dws chat +messages-send-card --group --at-all --content "请大家确认" +``` diff --git a/skills/multi/dingtalk-chat/references/chat/chat-message.md b/skills/multi/dingtalk-chat/references/chat/chat-message.md index f04f923d..89099201 100644 --- a/skills/multi/dingtalk-chat/references/chat/chat-message.md +++ b/skills/multi/dingtalk-chat/references/chat/chat-message.md @@ -227,6 +227,22 @@ dws chat message search --query "codereview" --group --star | `message edit` | 编辑已发送消息内容 | `--conversation-id` `--msg-id`,并在 `--text` / `--content` 中二选一;可选 `--title` `--at-all` `--at-open-dingtalk-ids` | | `message read-status` | 查消息已读/未读状态 | `--group` `--message-id`;可选目标用户 | +刚由 `message send` 发出的消息会返回 `openTaskId`。先用 `message query-send-status` 查询,成功结果中的 `openMessageId` 和 `openConversationId` 可直接传给 `message edit` 或 `message recall`,无需再按消息内容从列表反查 ID。 + +```bash +# 1. 发送后保留 openTaskId +dws chat message send --group --text "原始内容" +# 2. 查询得到 openMessageId 和 openConversationId +dws chat message query-send-status --open-task-id +# 3. 编辑消息 +dws chat message edit --conversation-id --msg-id --text "更新后的内容" + +# 发送后撤回使用同一 ID 链 +dws chat message send --group --text "待撤回的内容" +dws chat message query-send-status --open-task-id +dws chat message recall --conversation-id --msg-id +``` + `+messages-recall` 与 `recall-by-bot` 不同:前者使用 `openMessageId`,缺少会话 ID 时先只读查询消息详情;后者撤回机器人消息,需要 `robot-code + processQueryKey`。不要把 `processQueryKey` 当 `openMessageId`。 编辑消息使用 `message edit`。推荐传 `--text`,CLI 会生成 markdown content JSON:`{"title":"标题","text":"正文"}`;可选 `--title`,不传时会从正文自动生成标题。高级场景可直接传 `--content`,此时必须是完整 markdown content JSON,且不能同时传 `--text`。 @@ -270,12 +286,14 @@ dws chat message reply --conversation-id --ref-msg-id --content "开始处理" --flow-status 1 +dws chat +messages-send-card --group --at-open-dingtalk-ids --content "开始处理" --flow-status 1 dws chat +messages-update-card --biz-id --content "更新的卡片内容" --flow-status 2 dws chat +messages-update-card --biz-id --content "最终内容" --flow-status 3 ``` `flow-status`:1=处理中,2=输入中,3=完成,4=执行中,5=错误,Runtime 拒绝范围外值。 +群聊还可传 `--at-all`;两种艾特参数只随创建请求发送。send-card 同时带正文时, +Runtime 会把创建响应的 `atTag` 自动放在正文前;不要自行写 ID 或占位符。 当前只支持 streaming text;不支持 Card JSON 组件或 action callback。精确边界见 [card references](../card/schema.md)。 diff --git a/skills/multi/dingtalk-misc/references/sheet.md b/skills/multi/dingtalk-misc/references/sheet.md index bc0ee825..c884f44a 100644 --- a/skills/multi/dingtalk-misc/references/sheet.md +++ b/skills/multi/dingtalk-misc/references/sheet.md @@ -10,7 +10,7 @@ |---------|---------| | 在线电子表格(`axls`) | 走 `sheet` 全部命令 | | `xlsx` / `xls` / `xlsm` / `csv` | `dws drive download --node --output <路径>` 下载到本地处理,禁止调用任何 `sheet` 子命令 | -| 本地 xlsx/xls 导入为在线表格 | `dws sheet import --file <路径>`(单命令一站式:创建会话→上传→确认→轮询,产出在线电子表格) | +| 本地 xlsx/xls 导入为在线表格 | `dws sheet import create --file <路径>`(单命令一站式:创建会话→上传→确认→轮询,产出在线电子表格;`sheet import` 保留兼容) | | 在线表格导出为 xlsx | `dws sheet export`(axls → xlsx 格式转换) | 用户贴原始 `alidocs` URL 时必须先 probe:`dws drive info --node --format json`,按 [链接规范](../../dingtalk-shared/references/url-patterns.md#alidocs-url-类型探测流程) 校验: @@ -29,7 +29,7 @@ ## Agent 编辑总则 1. **先定位再编辑**:未知 `sheet-id` 时必须先 `dws sheet list --node --format json`,禁止猜 `Sheet1` / `sheet1` / `0` / `default`。用户只给工作表名称时,也先用 `list` 确认真实名称或 ID。 -2. **先读结构再动结构**:合并、冻结、行列分组、行高列宽、隐藏行列、最后非空边界都属于工作表结构信息,先读 `dws sheet info --node --sheet-id --format json`。 +2. **先读结构再动结构**:合并、冻结、行列分组、行高列宽、隐藏行列、最后非空边界都属于工作表结构信息,先读 `dws sheet info --node --sheet-id --format json`;分组回读加 `--include groups`,行高、列宽和隐藏行列按需加 `--include row_heights,col_widths,hidden_rows,hidden_cols`。 3. **按目的选择读取方式**:快速看值和大表分批用 `csv-get`;需要 `columns` / `data` / `dtypes` / `formats` 用 `table-get`;需要公式、样式、数据验证、超链接、富文本等 per-cell 元数据用 `range read`。 4. **写返回不等于完成**:任何写操作完成后都要用独立读命令回读确认。值写入用 `csv-get` / `range read` / `table-get`,结构变更用 `sheet info`,对象类操作用对应 `list` / `get`。 5. **大批量 CSV 值/公式不要拼大 JSON**:超过 5 行或 20 个单元格、且不需要富格式对象时优先 `csv-put`;字段值以 `=` 开头按公式解析,前加单引号写入以 `=` 开头的字面文本;需要 table/dataframe 语义时用 `table-put`。 @@ -68,7 +68,7 @@ | Reference | 描述 | |-----------|------| -| [sheet-workbook](sheet/sheet-workbook.md) | 管理表格文档与工作表。当用户说"创建表格"、"有哪些工作表"、"新建/重命名/隐藏/冻结/复制/删除工作表"、"显示/隐藏网格线"时使用;`info` 可回读冻结行列等工作表结构信息。命令:`create`/`list`/`info`/`new`/`update`/`copy`/`delete-sheet`/`show-gridline`/`hide-gridline` | +| [sheet-workbook](sheet/sheet-workbook.md) | 管理表格文档与工作表。当用户说"创建表格"、"有哪些工作表"、"新建/重命名/隐藏/冻结/复制/删除工作表"、"显示/隐藏网格线"时使用;`info` 可回读冻结行列,`--include groups` 回读分组,其他行列结构按需使用对应 `--include`。命令:`create`/`list`/`info`/`new`/`update`/`copy`/`delete-sheet`/`show-gridline`/`hide-gridline` | | [sheet-read-data](sheet/sheet-read-data.md) | 读取工作表数据。当用户说"读数据"、"看表格内容"、"查看数据"时使用。推荐 `csv-get`(CSV 格式、token 低、防爆保护);需按 table/dataframe 结构读取列名、data、dtypes、formats 时用 `table-get`;需 value + dataValidation / hyperlink / richText / cellStyles 等 per-cell 元数据时用 `range read`。大范围数据建议分页读取(单次 ≤5000 单元格)。命令:`csv-get`/`table-get`/`range read` | | [sheet-write-data](sheet/sheet-write-data.md) | 写入数据到工作表。当用户说"写数据"、"填表"、"更新单元格"、"写公式"、"超链接"、"写值同时设样式/数据验证"、"追加数据"、"导入CSV"、"写入结构化 table"时使用。大批量 CSV 值/公式(>5行或>20单元格,且无需富格式)必须用 `csv-put` 而非 `range update`;结构化 table/dataframe 输入用 `table-put`。命令:`range update`/`append`/`csv-put`/`table-put` | | [sheet-formula](sheet/sheet-formula.md) | 公式写入、文本回读、错误聚合与结果抽样。当用户说"写公式"、"计算列"、"辅助列"、"总计/占比/增长率/查找计算"、"校验公式"、"检查公式错误"时使用。命令:`range update` / `csv-put` / `range read --value-render-option formula/raw_value` / `formula-verify` | @@ -82,8 +82,8 @@ | [sheet-filter](sheet/sheet-filter.md) | 全局筛选。当用户说"筛选"、"过滤"、"只看某些行"(未说"筛选视图")时使用。禁止用"删除不符合条件的行"代替筛选。命令:`filter get`/`create`/`delete`/`update`/`clear-criteria`/`sort` | | [sheet-filter-view](sheet/sheet-filter-view.md) | 筛选视图(个人化,不影响协作者)。当用户明确说"筛选视图"时使用,与全局筛选相互独立。命令:`filter-view list`/`create`/`update`/`delete`/`info`/`update-criteria`/`delete-criteria`/`list-criteria`/`get-criteria` | | [sheet-conditional-format](sheet/sheet-conditional-format.md) | 条件格式规则。触发词:标红/标黄/高亮/突出/标记/数据条/色阶/颜色随数据变 → **强制**走条件格式,禁止 `range set-style` 静态样式替代。命令:`cond-format list`/`create`/`update`/`delete` | -| [sheet-export](sheet/sheet-export.md) | 导出表格为 xlsx。当用户说“导出”、“下载xlsx”、“存为Excel”时使用。单命令一站式,CLI 内部自动轮询,禁止 Agent 侧重试。命令:`export` / `export get` | -| [sheet-import](sheet/sheet-import.md) | 导入本地表格文件为在线电子表格。当用户说“导入表格”、“把xlsx传成在线表格”、“上传Excel变在线表格”时使用。单命令一站式,CLI 内部自动轮询,禁止 Agent 侧重试,禁止用 `drive upload` 代替。命令:`import`/`import get` | +| [sheet-export](sheet/sheet-export.md) | 导出表格为 xlsx。当用户说“导出”、“下载xlsx”、“存为Excel”时使用。单命令一站式,CLI 内部自动轮询,禁止 Agent 侧重试。命令:`export` | +| [sheet-import](sheet/sheet-import.md) | 导入本地表格文件为在线电子表格。当用户说“导入表格”、“把xlsx传成在线表格”、“上传Excel变在线表格”时使用。Agent 入口为 `import create`,`import` 保留兼容;超时后只用 `import get` 续查,禁止重新创建或用 `drive upload` 代替。命令:`import create`/`import get` | | [sheet-chart](sheet/sheet-chart.md) | 浮动图表管理。当用户说“画图/数据可视化/趋势图/对比图/占比图/柱形图/折线图/饼图”时使用。禁止用本地脚本生成静态图片替代。命令:`chart list`/`create`/`update`/`delete` | | [sheet-pivot-table](sheet/sheet-pivot-table.md) | 透视表管理。当用户说“透视表/分组汇总/交叉分析/按X统计Y”时使用。禁止用公式拼汇总表替代。命令:`pivot-table list`/`create`/`update`/`delete` | | [sheet-comment](sheet/sheet-comment.md) | 单元格评论管理。当用户说“给单元格加评论/批注”、“看某格的评论”、“回复/更新/删除评论”、“@某人讨论这个数据”时使用。评论锚定单元格(`--sheet-id`+`--range`),禁止把评论写进单元格值。命令:`comment list`/`create`/`reply`/`update`/`delete` | @@ -95,7 +95,7 @@ 1. **`--sheet-id` 禁止臆测**:未知时必须 `dws sheet list --node --format json` 查询,禁止编造 `Sheet1`/`sheet1`/`0`/`default` 2. **合并单元格是结构信息**:`dws sheet info --node --sheet-id --format json` 返回 `mergedRanges`(如 `["C7:D11"]`);不要在 `range read` / `csv-get` 里寻找合并信息 3. **冻结行列是工作表元数据**:`sheet info` 顶层返回 `frozenRowCount` / `frozenColumnCount`,分别表示从顶部第 1 行、左侧第 A 列开始冻结的数量;`0` 表示未冻结。不要在 `range read` / `csv-get` 里寻找冻结信息 -4. **结构变更后回读**:创建/取消分组、调整行高列宽或隐藏行列后,用 `dws sheet info --node --sheet-id --format json` 回读工作表结构确认。 +4. **结构变更后按需 include 回读**:创建/取消分组后,用 `dws sheet info --node --sheet-id --include groups --format json` 回读 `rowGroups` / `columnGroups`;行高、列宽、隐藏行列分别使用 `--include row_heights` / `col_widths` / `hidden_rows` / `hidden_cols`。裸 `sheet info` 不承诺返回这些可选结构字段。 5. **最后非空坐标只用 A1 语义**:`sheet info` 通过 `nonEmptyRange` 返回 A1/UI 边界;优先使用 `nonEmptyRange.range`,需要追加行/列时使用 `nonEmptyRange.lastRow` / `nonEmptyRange.lastColumn`。不要使用旧的 0-based 字段 `lastNonEmptyRow` / `lastNonEmptyColumn` 6. **`range update` 维度校验**:`--values` 行列数必须与 `--range` 完全一致;只接 `--values` 一个数据参数,cell `type` 仅支持 `text` / `richText`;整格超链接通过 cell-level `hyperlink` 表达,富文本片段链接才使用 `richText.texts[].type="link"` 7. **dataValidation 三语义**:不传 `dataValidation` 字段=保留原 DV;`dataValidation:{type:"none"}`=显式清除;`dataValidation:{type:"dropdown"/"checkbox",...}`=覆盖。`{}` 跳过亦保留原 DV @@ -104,7 +104,7 @@ 10. **用专用命令不用组合模拟**:搜索→`find`、替换→`replace`、清空→`range clear`、排序→`range sort`、填充→`range fill`、复制区域→`range copy-to`、移动区域→`range move-to`、移动行列→`move-dimension` 11. **大批量 CSV 值/公式用 `csv-put`**(>5 行或 >20 单元格,且无需富格式),不用 `range update`;需要 dataframe/table 语义(列名、dtypes、formats、跨 sheet specs)时用 `table-get` / `table-put` 12. **单元格图片用 `write-image`**(`range update` 不支持图片参数) -13. **`export` / `import` 禁止自行轮询**(CLI 内部已完成渐进式退避,最多 30 次约 5 分钟);`export get` / `import get`为单次查询,非终态时可手动再次调用 +13. **`export` / `import create` 禁止自行轮询**(CLI 内部已完成渐进式退避,最多 30 次约 5 分钟);导入超时只按返回的 `next_command` 调用 `import get`,导出失败或超时直接转述,不自动重调 14. **单次调用上限**:`range update` / `set-style` 行数 ≤ 1000,单元格总数建议 ≤ 5000(硬限 30000) 15. **大整数精度保护**:超过 `9007199254740991` 的整数和长数字标识符必须按文本写入;`table-put` 中不要使用 JSON number 或 `int64` / `uint64` dtype,改用字符串值 + `object` dtype + `formats`/`cellStyles` 的 `@` 16. **批量写操作推荐用 `batch-update`**:对多个区域重复调用同一写入工具时,推荐合并为单次原子请求(详见 [sheet-batch-operations](./sheet/sheet-batch-operations.md));逐个调用非原子,中途失败会留下半成品。结构化 table 例外,`table-put` 不进 `batch-update` @@ -116,7 +116,7 @@ - **视觉规范**:当前不维护独立的表格视觉方案文档;样式、条件格式、图表分别按对应子文档执行。 - **结构化 table**:`table-get` / `table-put` 是 table/dataframe 语义入口,不嵌入 `batch-update`;需要原子组合时只组合已支持的单元格/结构写操作。 - **历史版本**:支持通过 `version save`/`list`/`revert` 手动保存快照、查看版本列表、回滚到指定版本(底层复用 doc 域版本能力);回滚为危险操作,需二次确认。 -- **单元格评论**:支持通过 `comment create`/`list`/`reply`/`update`/`delete` 管理单元格评论(锚定 `--sheet-id`+`--range`,内容复用 `doc-comment` 服务、与前端评论同桶,单元格位置由 flex-table-app 承载并以 `customCategory`(cmt-*) 关联),详见 [sheet-comment](sheet/sheet-comment.md)。 +- **单元格评论**:支持通过 `comment create`/`list`/`reply`/`update`/`delete` 管理锚定在 `--sheet-id` + `--range` 的评论;评论与单元格值相互独立,详见 [sheet-comment](sheet/sheet-comment.md)。 - **未暴露或未确认能力**:迷你图、历史 changeset 等能力未在本入口承诺;只有出现稳定 DWS 命令和可回读语义后再补充。 ## URL 粘贴场景 @@ -132,16 +132,16 @@ ## 导入本地表格 -用户说"导入Excel/把xlsx转为在线表格/上传表格并在线编辑"时,用 `dws sheet import`(单命令一站式,详见 [sheet-import](./sheet/sheet-import.md)): +用户说"导入Excel/把xlsx转为在线表格/上传表格并在线编辑"时,用 `dws sheet import create`(单命令一站式;`sheet import` 保留兼容,详见 [sheet-import](./sheet/sheet-import.md)): ```bash # 导入本地表格文件为在线电子表格(默认表格名取文件名) -dws sheet import --file ./data.xlsx +dws sheet import create --file ./data.xlsx # 指定目标文件夹与导入后表格名 -dws sheet import --file ./report.xls --folder-token --name "月度报表" +dws sheet import create --file ./report.xls --folder-token --name "月度报表" # 导入到指定知识库 -dws sheet import --file ./data.xls --workspace +dws sheet import create --file ./data.xls --workspace ``` - 支持 `.xlsx` / `.xls`,固定产出**在线电子表格**;只新建、不覆盖已有表格 - CLI 内部自动完成「创建会话→上传→确认→渐进式退避轮询」(最多 30 次约 5 分钟),**禁止 Agent 侧再实现轮询或重试** @@ -258,6 +258,7 @@ Flags: - 多维表 / 字段类型 → 切到 `dingtalk-aitable` - 把表数据写进文档 → 切到 `dingtalk-doc` + ## 局部意图与 Recipe - [局部意图消歧](sheet-intent-guide.md)。 diff --git a/skills/multi/dingtalk-misc/references/sheet/sheet-batch-operations.md b/skills/multi/dingtalk-misc/references/sheet/sheet-batch-operations.md index 08f7ba7e..8d922bec 100644 --- a/skills/multi/dingtalk-misc/references/sheet/sheet-batch-operations.md +++ b/skills/multi/dingtalk-misc/references/sheet/sheet-batch-operations.md @@ -5,7 +5,7 @@ `batch-update` 把多次写入打包成单次请求,但每个子操作仍受编辑类任务硬性默认规则约束: 1. **目标 range 必须落在用户授权范围内**:除用户明示要修改的区域外,子操作禁止扩张到无关单元格 / 列 / 工作表。规划 range 时先确认每个子操作的边界。 -2. **批次完成后必须回读校验**:整个 `batch-update` 执行成功后,单元格值用 `range read` 或 `csv-get` 抽样回读受影响区域;合并、行列分组等结构元数据用 `sheet info` 回读校验。 +2. **批次完成后必须回读校验**:整个 `batch-update` 执行成功后,单元格值用 `range read` 或 `csv-get` 抽样回读受影响区域;合并用裸 `sheet info`,行列分组用 `sheet info --include groups` 回读校验。 3. **预期条数前置断言**:涉及"批量填充 N 行"或"对 M 个区域分别写入"时,先把 N、M 硬编码进代码,回读后断言实际等于预期;不一致就再发一轮 `batch-update` 补齐,禁止交付半成品。 ## 使用场景 @@ -29,7 +29,7 @@ 用户说"批量创建行列分组/批量取消行列分组/同时调整尺寸并分组": - 连续行/列分组 → `group-dimension` - 取消连续行/列分组 → `ungroup-dimension` -- 分组回读用 `sheet info` +- 分组回读用 `sheet info --include groups` - `group-dimension` 在 batch 中只适合默认展开分组;需要创建后立即折叠时,用独立 `dws sheet group-dimension --group-state fold` **何时推荐使用 `batch-update`**: @@ -236,7 +236,7 @@ Cause: The requested resource was not found by the identifier 'NonExistentSheet' ### 批量创建/取消行列分组 -分组属于工作表结构元数据,批次完成后用 `sheet info` 回读 `rowGroups` / `columnGroups`,不要用 `range read` 校验。回读项中的 `level` 是 1-based,`collapsed` 表示当前折叠状态。 +分组属于工作表结构元数据,批次完成后用 `sheet info --include groups` 回读 `rowGroups` / `columnGroups`,不要用裸 `sheet info` 或 `range read` 校验。回读项中的 `level` 是 1-based,`collapsed` 表示当前折叠状态。 ```jsonc [ @@ -263,7 +263,7 @@ Cause: The requested resource was not found by the identifier 'NonExistentSheet' | 操作 | 从返回中提取 | 用于 | |------|-------------|------| | `list` | 工作表的 `sheetId` | batch-clear 的 --ranges sheet 前缀 / batch-update 子操作 input 的 sheet-id | -| `batch-update` | 各子操作的执行结果 | 回读校验(值用 range read / csv-get;合并、分组等结构用 sheet info) | +| `batch-update` | 各子操作的执行结果 | 回读校验(值用 range read / csv-get;合并用 sheet info;分组用 sheet info --include groups) | ## 注意事项 @@ -273,5 +273,5 @@ Cause: The requested resource was not found by the identifier 'NonExistentSheet' - ★ **table 写入不进 batch-update**:当前 `batch-update` 不支持结构化 table 写入;结构化 table 请直接调用 `dws sheet table-put` - ★ **批次完成后必须回读校验**:值写入用 `range read` 或 `csv-get` 抽样回读受影响区域;结构变更用 `sheet info` 回读 - ★ **`batch-update` 不支持嵌套**:`--operations` 中的 `toolName` 必须是原子操作,不可再嵌套 `batch-update` -- `batch-update` 支持 `group-dimension` / `ungroup-dimension`;分组结果用 `sheet info` 回读 +- `batch-update` 支持 `group-dimension` / `ungroup-dimension`;分组结果用 `sheet info --include groups` 回读 - `batch-update` 中 `group-dimension` 只适合默认展开分组;需要 `fold` 初始状态时使用独立 `group-dimension` diff --git a/skills/multi/dingtalk-misc/references/sheet/sheet-comment.md b/skills/multi/dingtalk-misc/references/sheet/sheet-comment.md index c28f538e..985e9813 100644 --- a/skills/multi/dingtalk-misc/references/sheet/sheet-comment.md +++ b/skills/multi/dingtalk-misc/references/sheet/sheet-comment.md @@ -11,8 +11,8 @@ - 仅支持钉钉在线电子表格(`extension=axls`)。`xlsx` / `xls` / `csv` 等本地表格不支持评论。 - 评论锚定在**单元格位置**:`create` / `list` 通过 `--sheet-id`(工作表 ID 或名称)+ `--range`(单元格坐标)定位;`reply` / `update` / `delete` 通过 `--comment-key` 操作,不依赖单元格位置。 -- `create` / `list` 为表格专属工具(`doc-comment` server 的 `create_sheet_comment` / `list_sheet_comments`);`reply` / `update` / `delete` 位置无关,复用 `doc-comment` server 的通用评论工具(`reply_comment` / `update_comment` / `delete_comment`)。五个命令统一走 `doc-comment` server。 -- 底层评论内容与文档评论同源存储(bizCode=DOCS),单元格位置由表格引擎(flex-table-app CP)承载,二者以 `cmt-{uuid}` 关联。 +- `create` / `list` 通过单元格位置定位;`reply` / `update` / `delete` 通过前一步返回的 `commentKey` 定位评论线程,不需要重新传单元格位置。 +- 评论正文与单元格位置由服务端关联。Agent 只使用命令返回的 `commentKey` 继续回复、更新或删除,不解析或拼接内部关联字段。 --- diff --git a/skills/multi/dingtalk-misc/references/sheet/sheet-dimension-operations.md b/skills/multi/dingtalk-misc/references/sheet/sheet-dimension-operations.md index 21f33859..c039f548 100644 --- a/skills/multi/dingtalk-misc/references/sheet/sheet-dimension-operations.md +++ b/skills/multi/dingtalk-misc/references/sheet/sheet-dimension-operations.md @@ -352,6 +352,6 @@ dws sheet update-dimension --node --sheet-id \ - `add-dimension` 的 `--length` 必须为正整数(>= 1),行列均不超过 5000 - `group-dimension` / `ungroup-dimension` 的 `--range` 只接受整行或整列范围,不接受普通单元格矩形 - `group-dimension` 输出和 `sheet info --include groups` 回读均使用 `level`,且为 1-based;不要使用旧的 `depth` 字段 -- `sheet info --include groups` 是分组回读入口;`range read` / `csv-get` 不返回行列分组 +- `sheet info --include groups` 是分组回读入口;裸 `sheet info` 不承诺返回行列分组,`range read` / `csv-get` 不返回行列分组 - 当前不能直接调整已有分组的 `collapsed` 状态;`--group-state fold` 只在创建分组时生效 - `batch-update` 支持 `group-dimension` / `ungroup-dimension`,但不适合创建后立即折叠;需要 `fold` 时使用独立 `group-dimension` diff --git a/skills/multi/dingtalk-misc/references/sheet/sheet-export.md b/skills/multi/dingtalk-misc/references/sheet/sheet-export.md index 1fd50367..ba0880c6 100644 --- a/skills/multi/dingtalk-misc/references/sheet/sheet-export.md +++ b/skills/multi/dingtalk-misc/references/sheet/sheet-export.md @@ -61,7 +61,7 @@ Flags: - 导出任务返回 `FAILED`:命令立即返回错误并附带失败原因,**禁止自动重试 `dws sheet export`**,告知用户稍后再试 - 轮询 30 次仍 `PROCESSING`:命令返回超时错误,告知用户稍后再试 -**限制**:仅支持钉钉在线电子表格(alxs)→ xlsx。导出钉钉文字文档请使用 `doc` 产品对应的导出工具。 +**限制**:仅支持钉钉在线电子表格(axls)→ xlsx。导出钉钉文字文档请使用 `doc` 产品对应的导出工具。 ### 导出单个工作表为纯 CSV(同步) ``` @@ -126,25 +126,17 @@ dws sheet export --node --output ./ # 若命令返回失败或超时,直接告知用户稍后再试,不要自动重调 dws sheet export。 ``` -```bash -# ── 工作流 13: 导出超时后查询任务状态(手动兜底)── - -# 1. 执行导出(一体化命令,内部自动轮询约 5 分钟) -dws sheet export --node --format json -# 若超时,命令返回错误;等待一段时间后重新执行导出即可 -``` - ## 上下文传递 | 操作 | 从返回中提取 | 用于 | |------|-------------|------| -| `export` | `downloadUrl`(未指定 --output)/ `导出完成: `(指定 --output) | 直接下发给用户或告知文件已保存到本地。命令内部已完成轮询,不要再调用其他 export 相关命令 | -| `export` 超时中断 | 错误信息 | 告知用户稍后重试 | +| `export` | `downloadUrl`(未指定 --output)/ `outputPath`(指定 --output) | 直接下发给用户或告知文件已保存到本地。命令内部已完成轮询,不要再调用其他 export 相关命令 | +| `export` 超时中断 | 错误信息 | 直接报告失败或超时;当前没有独立续查命令,不自动重新提交导出 | | `export-csv` | CSV 正文(未指定 --output,走 stdout)/ `导出完成: `(指定 --output) | 直接把 CSV 交给下游处理,或告知文件已保存到本地。命令是同步的,无任务/轮询概念 | ## 注意事项 -- ★ `export` 仅支持钉钉在线电子表格(alxs)→ xlsx;传入钉钉文字文档会报 `invalidRequest.document.typeIllegal` +- ★ `export` 仅支持钉钉在线电子表格(axls)→ xlsx;传入钉钉文字文档会报 `invalidRequest.document.typeIllegal` - ★ `export` 为单命令一站式,CLI 内部已自动完成「提交 → 渐进式退避轮询 → 可选下载」,**Agent 不得在外部实现轮询或重试**;命令返回成功后不再调用其他 export 相关命令 - `export` 内置轮询策略:1~5 次间隔 2s、6~10 次间隔 5s、11~20 次间隔 10s、21~30 次间隔 15s,硬上限 30 次(约 5 分钟);超时后命令返回错误,告知用户稍后再试即可 - ★ `export` 命令返回失败或超时时,**禁止自动重调 `dws sheet export`**;直接告知用户导出失败并建议稍后再试 diff --git a/skills/multi/dingtalk-misc/references/sheet/sheet-formula.md b/skills/multi/dingtalk-misc/references/sheet/sheet-formula.md index 5ff635e1..cf63e322 100644 --- a/skills/multi/dingtalk-misc/references/sheet/sheet-formula.md +++ b/skills/multi/dingtalk-misc/references/sheet/sheet-formula.md @@ -30,7 +30,7 @@ | 查看格式化展示结果 | `range read` 或 `csv-get` 默认模式 | 用于用户肉眼看到的展示值检查 | | 扫描整本表格公式错误 | `formula-verify --node ` | 不传目标时扫描全部工作表的非空范围 | | 扫描单个工作表或范围 | `formula-verify --sheet-id ... [--range ...]` | `--range` 只传 A1 范围,不带工作表前缀 | -| 扫描多个目标 | `formula-verify --targets ...` | 每项为 `{"sheetId":"...","range":"..."}`,`range` 可省略 | +| 扫描多个目标 | `formula-verify --targets ...` | 必须是非空数组;每项为 `{"sheetId":"...","range":"..."}`,`range` 可省略 | ## 推荐流程 @@ -74,7 +74,7 @@ dws sheet formula-verify --node \ --format json ``` -`--targets` 也支持 `@targets.json` 和 `-`(stdin)。每项只允许 `sheetId` 和可选的 `range`;使用 `--targets` 时不能再传 `--sheet-id` 或 `--range`。 +`--targets` 也支持 `@targets.json` 和 `-`(stdin)。数组必须至少包含一个目标,不能传 `[]`;每项只允许非空 `sheetId` 和可选的字符串 `range`,`range` 只写 A1 范围且不能带工作表前缀。使用 `--targets` 时不能再传 `--sheet-id` 或 `--range`。 ### 扫描限制与自动化 @@ -183,6 +183,7 @@ dws sheet range read --node --sheet-id --range "D2:D5" \ - 把 `max-locations-per-error` 误解为错误计数上限;它只截断位置和样本。 - 看到 `status=partial` 或 `hasMore=true` 仍声称整本表公式零错误。 - 在 `--range` 中传 `Sheet1!A1:D10`,或把 `--targets` 与 `--sheet-id` 混用。 +- 显式传空的 `--targets '[]'`;这不是“无目标”,应改为至少一个目标,整本扫描则完全省略 `--targets`。 ## 关联文档 diff --git a/skills/multi/dingtalk-misc/references/sheet/sheet-import.md b/skills/multi/dingtalk-misc/references/sheet/sheet-import.md index a03379c5..d50b27b4 100644 --- a/skills/multi/dingtalk-misc/references/sheet/sheet-import.md +++ b/skills/multi/dingtalk-misc/references/sheet/sheet-import.md @@ -1,115 +1,76 @@ -# 导入 (import) +# 导入本地表格(import) ## 使用场景 -### 导入 +用户说“导入 Excel”“把 xlsx 变成在线表格”“上传本地表格并在线编辑”时,使用 Agent 可发现入口 `dws sheet import create`。它只新建在线电子表格,不覆盖已有表格。旧入口 `dws sheet import` 继续兼容人工调用。 -用户说"导入/上传本地表格/把 xlsx 变成在线表格/把 Excel 传成在线电子表格/供应商发来的报价单传成在线表格": -- 导入本地表格文件 → `import`(单命令一站式,内部自动完成创建会话、上传、确认、轮询) -- 需传 `--file` 与目标位置(`--folder-token` 或 `--workspace` 至少一个),`--name` 自定义表格名 -- 固定产出**在线电子表格**;只**新建**文档,不写入/覆盖已有表格 -- 支持格式:`xlsx` / `xls`(按扩展名识别);文件大小上限 20MB -- 禁止用 `drive upload` 代替:`drive upload` 只上传文件,不转换为可编辑的在线电子表格,语义不同且不在表格任务闭环内 -- 禁止在 AI Agent 侧实现轮询或重试,CLI 内部已按渐进式退避策略完成(最多 30 次约 5 分钟) -- 导入后如需 @同事,由 Agent 拿到 `documentUrl` 后再走 `chat`/`doc` 相关命令编排,本命令不负责通知 +- 支持本地 `xlsx` / `xls`,文件上限 20MB +- `--folder-token` 与 `--workspace` 至少提供一个 +- CLI 内部完成创建会话、上传文件、确认转换和轮询,Agent 不要自行拆分或重试 +- `drive upload` 只上传二进制文件,不会转换为可编辑的在线表格,不能替代本命令 -## 命令详细参考 - -### 导入本地表格文件为在线电子表格(异步任务一站式) -``` -Usage: - dws sheet import [flags] # 一站式:创建会话 → 上传 → 确认 → 轮询 - dws sheet import get [flags] # 兜底:按 taskId 手动查询导入结果 -Example: - # 导入 xlsx 为在线电子表格(默认表格名取文件名) - dws sheet import --file ./quote.xlsx - - # 指定目标文件夹与导入后表格名称 - dws sheet import --file ./report.xls --folder-token --name "月度报表" - - # 导入到指定知识库 - dws sheet import --file ./data.xls --workspace - -Flags: - --file string 本地表格文件路径 (必填,支持 xlsx/xls) - --folder-token string 目标文件夹 ID 或 URL (与 --workspace 至少传一个) - --workspace string 目标知识库 ID 或 URL (与 --folder-token 至少传一个) - -n, --name string 导入后表格名称 (可选,默认取文件名去扩展名) -``` - -将本地表格文件(xlsx/xls)导入为一个新的钉钉在线电子表格,与 `dws sheet export`(导出)对称,构成表格"进出闭环"。**单命令一站式**:命令内部自动完成「创建导入会话 → 上传文件到 OSS → 确认导入 → 渐进式退避轮询」全流程,AI Agent 无需自行拆分步骤或实现轮询。 - -**内部流程**: -1. 调 `create_import_session` 获取 `sessionId` 与 OSS `uploadUrl`(入参 `fileName`/`suffix`/`fileSize`,可选 `targetFolderId`/`workspaceId`) -2. HTTP PUT 上传本地文件到 `uploadUrl` -3. 调 `confirm_import` 触发格式转换,获取 `taskId` -4. 按渐进式退避策略轮询 `query_import_task` 直至任务终态或超时 - -**内置轮询策略(CLI 内实现,无需关心)**: -- 第 1~5 次:每次间隔 2 秒 -- 第 6~10 次:每次间隔 5 秒 -- 第 11~20 次:每次间隔 10 秒 -- 第 21~30 次:每次间隔 15 秒 -- **硬上限:最多轮询 30 次(约 5 分钟)**,超时后命令输出结构化 `timed_out:true` + `next_command` 并以成功退出(exit 0) - -**命令返回**: -- 成功:进度日志(`[1/4]`~`[4/4]`)+ 末尾 JSON,含 `success` / `taskId` / `documentUrl` / `documentName` / `documentType` / `nodeId` -- 超时:输出结构化 JSON,含 `success:false` / `timed_out:true` / `taskId` / `status:"processing"` / `next_command`(可直接复制执行的续查命令 `dws sheet import get --task-id `);命令以成功退出(exit 0),业务态由 `success:false` 表达,便于 Agent 编排续查 - -**失败处理(命令内部已处理,Agent 仅需转述)**: -- 转换返回失败状态:命令立即返回错误并附带原因,**禁止自动重试**,告知用户检查文件内容后再试 -- 轮询 30 次仍处理中:命令输出 `timed_out:true` + `next_command` 结构化字段(exit 0),Agent 检测到后按 `next_command` 用 `import get --task-id` 续查 - -**限制**:仅支持 xlsx/xls → 在线电子表格。导入为钉钉在线文档(md/docx 等 → 文字文档)请使用 `doc` 产品的 `import` 命令。 - -### 查询导入任务结果(兜底) -``` -Usage: - dws sheet import get [flags] -Example: - dws sheet import get --task-id - -Flags: - --task-id string 导入任务 ID (必填) -``` - -按 `taskId` 查询导入任务状态。通常不需要手动调用,`import` 会自动轮询到终态;仅在导入命令超时或中断后用于兜底查询。任务状态:`processing`(转换中)/ `completed`(成功,返回 documentUrl)/ `failed`(失败)。 - -## 核心工作流 +## 命令 ```bash -# ── 工作流 13: 导入本地表格文件为在线电子表格(单命令一站式)── +# 导入到指定文件夹 +dws sheet import create \ + --file ./quote.xlsx \ + --folder-token \ + --format json -# 场景 A:导入 xlsx,默认表格名取文件名(命令内部自动完成上传+转换+轮询) -dws sheet import --file ./quote.xlsx --format json +# 导入到指定知识库,并自定义名称 +dws sheet import create \ + --file ./report.xls \ + --workspace \ + --name "月度报表" \ + --format json -# 场景 B:导入 xls 到指定文件夹并自定义表格名 -dws sheet import --file ./report.xls --folder-token --name "月度报表" - -# 场景 C:导入到指定知识库 -dws sheet import --file ./data.xls --workspace - -# 场景 D:导入超时后兜底查询(用命令输出的 taskId) -dws sheet import get --task-id - -# 禁止在 Agent 侧实现任何轮询或重试,CLI 内部已按 2s/5s/10s/15s 渐进式退避自动完成(最多 30 次)。 -# 若命令返回失败或超时,按提示用 import get 查询,不要自动重调 import。 +# 导入超时或中断后续查 +dws sheet import get --task-id --format json ``` -## 上下文传递 +公开参数: -| 操作 | 从返回中提取 | 用于 | -|------|-------------|------| -| `import` | `documentUrl` / `nodeId`(导入成功后返回) | 下发给用户,或作为后续 `sheet` 操作的 `--node`;如需 @同事再走 `chat` 命令编排 | -| `import`(超时) | `timed_out` / `taskId` / `next_command`(结构化输出) | 检测到 `timed_out:true` 后执行 `next_command`(即 `import get --task-id`)续查 | -| `import get` | `status` / `documentUrl` | 判断任务是否完成并取回文档链接 | +| 命令 | 参数 | 说明 | +|------|------|------| +| `sheet import create` | `--file` | 本地 xlsx/xls 文件(必填) | +| `sheet import create` | `--folder-token` | 目标文件夹 ID 或 URL,与 `--workspace` 至少传一个 | +| `sheet import create` | `--workspace` | 目标知识库 ID 或 URL,与 `--folder-token` 至少传一个 | +| `sheet import create` | `--name`, `-n` | 导入后的名称;默认取文件名并去掉扩展名 | +| `sheet import get` | `--task-id` | 导入任务 ID | -## 注意事项 +## 返回与续查 -- ★ `import` 仅支持 xlsx/xls → 在线电子表格;导入为文字文档(md/docx 等)请用 `doc import` -- ★ `import` 只**新建**在线电子表格,不写入或覆盖已有表格;要往已有表格追加数据请用 `range update` / `range append` / `csv put` -- ★ `import` 为单命令一站式,CLI 内部已自动完成「创建会话 → 上传 → 确认 → 渐进式退避轮询」,**Agent 不得在外部实现轮询或重试** -- ★ 命令失败或超时时,**禁止自动重调 `import`**;超时会输出 `timed_out:true`,按 `next_command` 用 `import get --task-id` 续查,失败则告知用户检查文件后再试 -- 文件大小上限 20MB;空文件、目录、非白名单格式均会在上传前被 CLI 拦截报错 -- `--name` 未指定时默认取文件名去掉扩展名;`--folder-token` 与 `--workspace` 用于指定目标位置,至少提供一个,均不传会被 CLI 层拦截报错 -- 用户要求"导入表格/上传 xlsx 变在线表格"时,必须使用 `import`,禁止用 `drive upload`(只上传不转换,产出不可编辑,且不在表格闭环内) +成功时返回: + +```json +{ + "success": true, + "taskId": "", + "documentUrl": "", + "documentName": "月度报表", + "documentType": "1", + "nodeId": "" +} +``` + +轮询达到上限时命令以退出码 0 返回业务态,避免 Agent 重复创建文档: + +```json +{ + "success": false, + "timed_out": true, + "taskId": "", + "status": "processing", + "next_command": "dws sheet import get --task-id " +} +``` + +检测到 `timed_out:true` 后,只执行 `next_command` 续查。不要重新执行 `sheet import create`。 + +## 边界 + +- 本命令接受的是本地文件路径;它把 xlsx/xls 转换成新的 axls 在线表格 +- 已经存在于钉盘或文档中的 xlsx 节点不能直接传给工作表/单元格命令;先用 `drive download` 下载,需要在线编辑时再执行 `sheet import create` +- 向已有在线表格写数据应使用 `range update`、`append`、`csv-put` 或 `table-put` +- md/doc/docx 等文字文档导入使用 `dws doc import` diff --git a/skills/multi/dingtalk-misc/references/sheet/sheet-pivot-table.md b/skills/multi/dingtalk-misc/references/sheet/sheet-pivot-table.md index 2073a4e5..6b8f93d2 100644 --- a/skills/multi/dingtalk-misc/references/sheet/sheet-pivot-table.md +++ b/skills/multi/dingtalk-misc/references/sheet/sheet-pivot-table.md @@ -191,7 +191,7 @@ Flags: - 示例:`'Sheet1'!A1:D100`、`'销售数据'!A1:F500` - 必须包含表头行(通常从第 1 行开始) - sheet 名称用单引号包裹(含空格或特殊字符时必须) -- MCP 内部自动将 source 解析为内部 sheetId + ISheetRange,CLI 直接透传即可 +- `source` 直接使用上述 A1 表示法;不要自行改写成内部对象结构 ## 高级功能示例 diff --git a/skills/multi/dingtalk-misc/references/sheet/sheet-workbook.md b/skills/multi/dingtalk-misc/references/sheet/sheet-workbook.md index b070f13f..54c4ca48 100644 --- a/skills/multi/dingtalk-misc/references/sheet/sheet-workbook.md +++ b/skills/multi/dingtalk-misc/references/sheet/sheet-workbook.md @@ -143,6 +143,7 @@ Example: dws sheet info --node dws sheet info --node --sheet-id dws sheet info --node --sheet-id "Sheet1" + dws sheet info --node --sheet-id --include groups dws sheet info --node --sheet-id --include row_heights,col_widths,hidden_rows,hidden_cols Flags: --node string 表格文档 ID 或 URL (必填) @@ -156,7 +157,7 @@ Flags: 最后非空数据边界通过 `nonEmptyRange` 返回,字段均为 A1/UI 语义:`range` 是从 `A1` 到最后非空单元格的范围,`lastCell` 是最后非空单元格地址,`lastRow` 是 1-based 行号,`lastColumn` 是列字母。空表时 `nonEmptyRange` 为 `null`。不要使用旧的 0-based 字段 `lastNonEmptyRow` / `lastNonEmptyColumn`。 -返回字段: +需要读取行列分组时,加 `--include groups`。返回字段: - `rowGroups`:行分组列表,单项包含 `range`、`startRow`、`endRow`、`count`、`level`、`collapsed` - `columnGroups`:列分组列表,单项包含 `range`、`startColumn`、`endColumn`、`count`、`level`、`collapsed` diff --git a/test/unit/mono_multi_skill_content_test.go b/test/unit/mono_multi_skill_content_test.go index 0e4c03d3..ec2ab5b5 100644 --- a/test/unit/mono_multi_skill_content_test.go +++ b/test/unit/mono_multi_skill_content_test.go @@ -1,7 +1,7 @@ // Copyright 2026 Alibaba Group // Licensed under the Apache License, Version 2.0 (the "License"); // -// Mono↔multi skill content QA (G1–G4). Spec: docs/skill-mono-multi-qa.md +// Mono↔multi skill content QA (G1–G5). Spec: docs/skill-mono-multi-qa.md // Contract: skills/content-qa/mono-multi-coverage.yaml package unit_test @@ -9,8 +9,11 @@ package unit_test import ( "bytes" "fmt" + "net/url" "os" "path/filepath" + "regexp" + "sort" "strings" "testing" @@ -41,10 +44,16 @@ type monoMultiQAContract struct { ExpectedMulti string `yaml:"expected_multi"` } `yaml:"omit_global"` PairedFiles []struct { + Mono string `yaml:"mono"` + Multi string `yaml:"multi"` + Mode string `yaml:"mode"` + LinkSubstitutions []monoMultiLinkSubstitution `yaml:"link_substitutions"` + } `yaml:"paired_files"` + PairedTrees []struct { Mono string `yaml:"mono"` Multi string `yaml:"multi"` Mode string `yaml:"mode"` - } `yaml:"paired_files"` + } `yaml:"paired_trees"` OrphanScriptsAllowlist []struct { Path string `yaml:"path"` Disposition string `yaml:"disposition"` @@ -53,6 +62,24 @@ type monoMultiQAContract struct { SkillsWithoutReferencesAllowlist []string `yaml:"skills_without_references_allowlist"` } +type monoMultiLinkSubstitution struct { + Mono string `yaml:"mono"` + Multi string `yaml:"multi"` +} + +type markdownLink struct { + Line int + Target string +} + +type markdownLinkIssue struct { + Link markdownLink + Resolved string + Err error +} + +var inlineMarkdownLinkPattern = regexp.MustCompile(`!?\[[^\]\n]*\]\(\s*(<[^>\n]+>|[^)\s\n]+)(?:\s+[^)\n]+)?\)`) + func loadMonoMultiQAContract(t *testing.T) (string, monoMultiQAContract) { t.Helper() root := repoRoot(t) @@ -255,13 +282,50 @@ func TestMonoMultiSkillContentG4Drift(t *testing.T) { if mode == "" { mode = "identical" } - switch mode { - case "identical": + if err := comparePairedFileContent(monoData, multiData, mode, pair.LinkSubstitutions); err != nil { + t.Errorf("G4: paired file mono %s vs multi %s: %v", pair.Mono, pair.Multi, err) + } + } + + for _, pair := range c.PairedTrees { + if pair.Mono == "" || pair.Multi == "" { + t.Fatalf("G4: paired tree requires mono and multi paths: %+v", pair) + } + mode := pair.Mode + if mode == "" { + mode = "identical" + } + if mode != "identical" { + t.Errorf("G4: unknown paired tree mode %q", mode) + continue + } + + monoDir := filepath.Join(root, "skills", "mono", filepath.FromSlash(pair.Mono)) + multiDir := filepath.Join(multiRoot, filepath.FromSlash(pair.Multi)) + monoFiles, err := readRelativeFileTree(monoDir) + if err != nil { + t.Errorf("G4: paired mono tree %s: %v", pair.Mono, err) + continue + } + multiFiles, err := readRelativeFileTree(multiDir) + if err != nil { + t.Errorf("G4: paired multi tree %s: %v", pair.Multi, err) + continue + } + for rel, monoData := range monoFiles { + multiData, ok := multiFiles[rel] + if !ok { + t.Errorf("G4: paired multi tree %s missing %s from mono %s", pair.Multi, rel, pair.Mono) + continue + } if !bytes.Equal(monoData, multiData) { - t.Errorf("G4: paired files differ: mono %s vs multi %s", pair.Mono, pair.Multi) + t.Errorf("G4: paired tree file differs: mono %s/%s vs multi %s/%s", pair.Mono, rel, pair.Multi, rel) + } + } + for rel := range multiFiles { + if _, ok := monoFiles[rel]; !ok { + t.Errorf("G4: paired mono tree %s missing %s from multi %s", pair.Mono, rel, pair.Multi) } - default: - t.Errorf("G4: unknown paired mode %q", mode) } } @@ -318,6 +382,396 @@ func TestMonoMultiSkillContentG4Drift(t *testing.T) { } } +func TestMonoMultiSkillContentG5RelativeLinks(t *testing.T) { + root, c := loadMonoMultiQAContract(t) + multiRoot := filepath.Join(root, "skills", "multi") + sources := map[string]bool{} + + for _, pair := range c.PairedFiles { + if pair.Mono == "" || pair.Multi == "" { + t.Fatalf("G5: paired file requires mono and multi paths: %+v", pair) + } + sources[filepath.Join(root, "skills", "mono", filepath.FromSlash(pair.Mono))] = true + sources[filepath.Join(multiRoot, filepath.FromSlash(pair.Multi))] = true + } + for _, pair := range c.PairedTrees { + if pair.Mono == "" || pair.Multi == "" { + t.Fatalf("G5: paired tree requires mono and multi paths: %+v", pair) + } + trees := []string{ + filepath.Join(root, "skills", "mono", filepath.FromSlash(pair.Mono)), + filepath.Join(multiRoot, filepath.FromSlash(pair.Multi)), + } + for _, tree := range trees { + files, err := readRelativeFileTree(tree) + if err != nil { + t.Errorf("G5: read paired Markdown tree %s: %v", tree, err) + continue + } + for rel := range files { + sources[filepath.Join(tree, filepath.FromSlash(rel))] = true + } + } + } + + paths := make([]string, 0, len(sources)) + for path := range sources { + paths = append(paths, path) + } + sort.Strings(paths) + for _, source := range paths { + if !strings.EqualFold(filepath.Ext(source), ".md") { + continue + } + data, err := os.ReadFile(source) + if err != nil { + t.Errorf("G5: read %s: %v", source, err) + continue + } + rel, relErr := filepath.Rel(root, source) + if relErr != nil { + rel = source + } + for _, issue := range unresolvedRelativeMarkdownLinks(root, source, data) { + resolved := issue.Resolved + if resolved != "" { + if resolvedRel, err := filepath.Rel(root, resolved); err == nil { + resolved = filepath.ToSlash(resolvedRel) + } + } + t.Errorf("G5: %s:%d relative link %q resolves to %q: %v", filepath.ToSlash(rel), issue.Link.Line, issue.Link.Target, resolved, issue.Err) + } + } +} + +func TestMonoMultiSkillContentLinkNormalization(t *testing.T) { + rules := []monoMultiLinkSubstitution{ + {Mono: "../url-patterns.md", Multi: "../../dingtalk-shared/references/url-patterns.md"}, + {Mono: "../intent-guide.md", Multi: "sheet-intent-guide.md"}, + } + mono := []byte("url=../url-patterns.md\nintent=../intent-guide.md\n") + multi := []byte("url=../../dingtalk-shared/references/url-patterns.md\nintent=sheet-intent-guide.md\n") + if err := comparePairedFileContent(mono, multi, "link-normalized", rules); err != nil { + t.Fatalf("valid substitutions rejected: %v", err) + } + if err := comparePairedFileContent(mono, append(multi, []byte("drift\n")...), "link-normalized", rules); err == nil { + t.Fatal("unknown content drift must not be normalized away") + } + if err := comparePairedFileContent(mono, multi, "identical", rules); err == nil { + t.Fatal("identical mode must reject link substitutions") + } + if err := comparePairedFileContent(mono, multi, "unknown", nil); err == nil { + t.Fatal("unknown paired-file mode must fail closed") + } +} + +func TestMonoMultiSkillContentLinkNormalizationRejectsInvalidRules(t *testing.T) { + tests := []struct { + name string + mono string + multi string + rules []monoMultiLinkSubstitution + }{ + {name: "no rules", mono: "m1", multi: "x1"}, + {name: "empty mono", mono: "m1", multi: "x1", rules: []monoMultiLinkSubstitution{{Multi: "x1"}}}, + {name: "empty multi", mono: "m1", multi: "x1", rules: []monoMultiLinkSubstitution{{Mono: "m1"}}}, + {name: "same sides", mono: "m1", multi: "m1", rules: []monoMultiLinkSubstitution{{Mono: "m1", Multi: "m1"}}}, + {name: "duplicate mono", mono: "m1", multi: "x1 x2", rules: []monoMultiLinkSubstitution{{Mono: "m1", Multi: "x1"}, {Mono: "m1", Multi: "x2"}}}, + {name: "duplicate multi", mono: "m1 m2", multi: "x1", rules: []monoMultiLinkSubstitution{{Mono: "m1", Multi: "x1"}, {Mono: "m2", Multi: "x1"}}}, + {name: "overlapping mono", mono: "m1 m", multi: "x1 x2", rules: []monoMultiLinkSubstitution{{Mono: "m", Multi: "x1"}, {Mono: "m1", Multi: "x2"}}}, + {name: "missing occurrence", mono: "m1", multi: "x1", rules: []monoMultiLinkSubstitution{{Mono: "m2", Multi: "x1"}}}, + {name: "multiple occurrences", mono: "m1 m1", multi: "x1", rules: []monoMultiLinkSubstitution{{Mono: "m1", Multi: "x1"}}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if _, _, err := normalizePairedFileLinks([]byte(tt.mono), []byte(tt.multi), tt.rules); err == nil { + t.Fatal("invalid substitution rules must fail closed") + } + }) + } +} + +func TestMonoMultiSkillContentRelativeLinkScanner(t *testing.T) { + root := t.TempDir() + docs := filepath.Join(root, "docs") + if err := os.MkdirAll(filepath.Join(docs, "sheet"), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(docs, "topic.md"), []byte("# Topic\n"), 0o644); err != nil { + t.Fatal(err) + } + body := []byte("[topic](./topic.md#section)\n[directory](./sheet/)\n[external](https://example.com/a)\n[email](mailto:test@example.com)\n[anchor](#section)\n`[inline](missing-inline.md)`\n```text\n[fenced](missing-fenced.md)\n```\n[missing](missing.md)\n[escape](../../escape.md)\n") + issues := unresolvedRelativeMarkdownLinks(root, filepath.Join(docs, "source.md"), body) + if len(issues) != 2 { + t.Fatalf("got %d unresolved links, want 2: %+v", len(issues), issues) + } + if issues[0].Link.Target != "missing.md" || issues[0].Link.Line != 10 { + t.Fatalf("unexpected unresolved link: %+v", issues[0]) + } + if issues[1].Link.Target != "../../escape.md" || issues[1].Link.Line != 11 || !strings.Contains(issues[1].Err.Error(), "escapes repository") { + t.Fatalf("unexpected escaping link: %+v", issues[1]) + } +} + +func TestMonoMultiSkillContentReadRelativeFileTreeFiltersNonMarkdown(t *testing.T) { + root := t.TempDir() + for _, dir := range []string{"nested", ".hidden"} { + if err := os.MkdirAll(filepath.Join(root, dir), 0o755); err != nil { + t.Fatal(err) + } + } + files := map[string]string{ + "guide.md": "visible", + "nested/topic.md": "nested", + "notes.txt": "ignored", + ".DS_Store": "ignored", + ".draft.md": "ignored", + ".hidden/topic.md": "ignored", + "nested/README.MD": "accepted", + "nested/.hidden.md": "ignored", + } + for rel, body := range files { + if err := os.WriteFile(filepath.Join(root, filepath.FromSlash(rel)), []byte(body), 0o644); err != nil { + t.Fatal(err) + } + } + got, err := readRelativeFileTree(root) + if err != nil { + t.Fatal(err) + } + want := []string{"guide.md", "nested/README.MD", "nested/topic.md"} + gotNames := make([]string, 0, len(got)) + for rel := range got { + gotNames = append(gotNames, rel) + } + sort.Strings(gotNames) + if strings.Join(gotNames, "\n") != strings.Join(want, "\n") { + t.Fatalf("visible Markdown tree = %v, want %v", gotNames, want) + } +} + +func comparePairedFileContent(monoData, multiData []byte, mode string, substitutions []monoMultiLinkSubstitution) error { + switch mode { + case "identical": + if len(substitutions) != 0 { + return fmt.Errorf("identical mode does not allow link_substitutions") + } + if !bytes.Equal(monoData, multiData) { + return fmt.Errorf("contents differ") + } + return nil + case "link-normalized": + monoNormalized, multiNormalized, err := normalizePairedFileLinks(monoData, multiData, substitutions) + if err != nil { + return err + } + if !bytes.Equal(monoNormalized, multiNormalized) { + return fmt.Errorf("contents differ after declared link substitutions") + } + return nil + default: + return fmt.Errorf("unknown paired mode %q", mode) + } +} + +func normalizePairedFileLinks(monoData, multiData []byte, substitutions []monoMultiLinkSubstitution) ([]byte, []byte, error) { + if len(substitutions) == 0 { + return nil, nil, fmt.Errorf("link-normalized mode requires link_substitutions") + } + for i, substitution := range substitutions { + if substitution.Mono == "" || substitution.Multi == "" { + return nil, nil, fmt.Errorf("link_substitutions[%d] requires non-empty mono and multi values", i) + } + if substitution.Mono == substitution.Multi { + return nil, nil, fmt.Errorf("link_substitutions[%d] must describe a layout difference", i) + } + for j := 0; j < i; j++ { + previous := substitutions[j] + if strings.Contains(substitution.Mono, previous.Mono) || strings.Contains(previous.Mono, substitution.Mono) { + return nil, nil, fmt.Errorf("link_substitutions[%d].mono overlaps link_substitutions[%d].mono", i, j) + } + if strings.Contains(substitution.Multi, previous.Multi) || strings.Contains(previous.Multi, substitution.Multi) { + return nil, nil, fmt.Errorf("link_substitutions[%d].multi overlaps link_substitutions[%d].multi", i, j) + } + } + if count := bytes.Count(monoData, []byte(substitution.Mono)); count != 1 { + return nil, nil, fmt.Errorf("link_substitutions[%d].mono occurs %d times, want exactly 1", i, count) + } + if count := bytes.Count(multiData, []byte(substitution.Multi)); count != 1 { + return nil, nil, fmt.Errorf("link_substitutions[%d].multi occurs %d times, want exactly 1", i, count) + } + } + + monoNormalized := bytes.Clone(monoData) + multiNormalized := bytes.Clone(multiData) + for i, substitution := range substitutions { + marker := []byte(fmt.Sprintf("\x00DWS_LINK_SUBSTITUTION_%d\x00", i)) + if bytes.Contains(monoNormalized, marker) || bytes.Contains(multiNormalized, marker) { + return nil, nil, fmt.Errorf("link_substitutions[%d] normalization marker already exists in content", i) + } + monoNormalized = bytes.Replace(monoNormalized, []byte(substitution.Mono), marker, 1) + multiNormalized = bytes.Replace(multiNormalized, []byte(substitution.Multi), marker, 1) + } + return monoNormalized, multiNormalized, nil +} + +func unresolvedRelativeMarkdownLinks(repoRoot, source string, body []byte) []markdownLinkIssue { + var issues []markdownLinkIssue + for _, link := range extractMarkdownLinks(body) { + targetPath, check, err := relativeMarkdownTargetPath(link.Target) + if err != nil { + issues = append(issues, markdownLinkIssue{Link: link, Err: err}) + continue + } + if !check { + continue + } + resolved := filepath.Clean(filepath.Join(filepath.Dir(source), filepath.FromSlash(targetPath))) + rel, err := filepath.Rel(repoRoot, resolved) + if err != nil { + issues = append(issues, markdownLinkIssue{Link: link, Resolved: resolved, Err: err}) + continue + } + if rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) { + issues = append(issues, markdownLinkIssue{Link: link, Resolved: resolved, Err: fmt.Errorf("target escapes repository")}) + continue + } + if _, err := os.Stat(resolved); err != nil { + issues = append(issues, markdownLinkIssue{Link: link, Resolved: resolved, Err: err}) + continue + } + realRoot, err := filepath.EvalSymlinks(repoRoot) + if err != nil { + issues = append(issues, markdownLinkIssue{Link: link, Resolved: resolved, Err: fmt.Errorf("resolve repository path: %w", err)}) + continue + } + realTarget, err := filepath.EvalSymlinks(resolved) + if err != nil { + issues = append(issues, markdownLinkIssue{Link: link, Resolved: resolved, Err: fmt.Errorf("resolve target path: %w", err)}) + continue + } + realRel, err := filepath.Rel(realRoot, realTarget) + if err != nil || realRel == ".." || strings.HasPrefix(realRel, ".."+string(os.PathSeparator)) { + if err == nil { + err = fmt.Errorf("target escapes repository through a symbolic link") + } + issues = append(issues, markdownLinkIssue{Link: link, Resolved: resolved, Err: err}) + } + } + return issues +} + +func extractMarkdownLinks(body []byte) []markdownLink { + var links []markdownLink + var fence byte + for i, line := range strings.Split(string(body), "\n") { + if marker := markdownFenceMarker(line); marker != 0 { + if fence == 0 { + fence = marker + } else if fence == marker { + fence = 0 + } + continue + } + if fence != 0 { + continue + } + masked := maskInlineMarkdownCode(line) + for _, match := range inlineMarkdownLinkPattern.FindAllStringSubmatch(masked, -1) { + target := match[1] + if strings.HasPrefix(target, "<") && strings.HasSuffix(target, ">") { + target = strings.TrimSuffix(strings.TrimPrefix(target, "<"), ">") + } + links = append(links, markdownLink{Line: i + 1, Target: target}) + } + } + return links +} + +func markdownFenceMarker(line string) byte { + trimmed := strings.TrimLeft(line, " \t") + if strings.HasPrefix(trimmed, "```") { + return '`' + } + if strings.HasPrefix(trimmed, "~~~") { + return '~' + } + return 0 +} + +func maskInlineMarkdownCode(line string) string { + masked := []byte(line) + for i := 0; i < len(masked); { + if masked[i] != '`' { + i++ + continue + } + endOfOpener := i + for endOfOpener < len(masked) && masked[endOfOpener] == '`' { + endOfOpener++ + } + marker := masked[i:endOfOpener] + closingOffset := bytes.Index(masked[endOfOpener:], marker) + if closingOffset < 0 { + i = endOfOpener + continue + } + end := endOfOpener + closingOffset + len(marker) + for j := i; j < end; j++ { + masked[j] = ' ' + } + i = end + } + return string(masked) +} + +func relativeMarkdownTargetPath(target string) (string, bool, error) { + parsed, err := url.Parse(target) + if err != nil { + return "", false, fmt.Errorf("parse target: %w", err) + } + if parsed.Scheme != "" || parsed.Host != "" || strings.HasPrefix(target, "//") || strings.HasPrefix(parsed.Path, "/") { + return "", false, nil + } + if parsed.Path == "" { + return "", false, nil + } + path, err := url.PathUnescape(parsed.Path) + if err != nil { + return "", false, fmt.Errorf("decode target path: %w", err) + } + return path, true, nil +} + +func readRelativeFileTree(root string) (map[string][]byte, error) { + files := map[string][]byte{} + err := filepath.WalkDir(root, func(path string, entry os.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + if entry.IsDir() { + if path != root && strings.HasPrefix(entry.Name(), ".") { + return filepath.SkipDir + } + return nil + } + if strings.HasPrefix(entry.Name(), ".") || !strings.EqualFold(filepath.Ext(entry.Name()), ".md") { + return nil + } + rel, err := filepath.Rel(root, path) + if err != nil { + return err + } + data, err := os.ReadFile(path) + if err != nil { + return err + } + files[filepath.ToSlash(rel)] = data + return nil + }) + return files, err +} + type skillFrontmatter struct { Name string `yaml:"name"` Description string `yaml:"description"`