first commit

This commit is contained in:
2026-09-02 11:44:52 +08:00
commit 0c8fa2653e
309 changed files with 57278 additions and 0 deletions
+83
View File
@@ -0,0 +1,83 @@
---
name: dingtalk-aisearch
description: AI搜问:人员语义搜索与跨源定位。Use when 按姓名/工号/部门/职责/上下级或手机号线索找人,跨文档/消息/邮件/听记检索,或回溯“我发过/收到过”。完整手机号反查走 dingtalk-contact;找到 userId 后由 contact 补详情。命令前缀:dws aisearch。
metadata:
cli_version: ">=0.2.14"
category: product
requires:
bins:
- dws
---
# 钉钉 AI 搜问 Skill
## 前置条件 — 执行操作前必读
> **CRITICAL — 执行任何 `dws` 操作前,MUST 先用 Read 工具完整读取 [`dingtalk-shared`](../dingtalk-shared/SKILL.md)。**该轻量文件包含全局执行契约、安全底线及 shared references 的按需加载导航;不要预加载其全部 references。
> 命令参考:[aisearch.md](references/aisearch.md)。
## 意图表
| 用户说 | 命令 |
|--------|------|
| "找张三 / 张三是谁" | `dws aisearch person --query "张三" --dimension name` |
| "谁负责 XX / XX 负责人是谁" | `dws aisearch person --query "<XX>" --dimension duty` |
| "张三的上级 / 下级" | `dws aisearch person --query "张三" --dimension supervisor`(或 `subordinate` |
| "X 部门有哪些人" | `dws aisearch person --query "<部门>" --dimension department` |
| "工号 12345 是谁" | `dws aisearch person --query "<工号>" --dimension jobNumber` |
| "按手机号线索语义搜人" | `dws aisearch person --query "<手机号线索>" --dimension phone` |
| "完整手机号精确反查" | `dws contact user search-mobile --mobile "<完整手机号>"` |
| "最近 OKR 相关邮件 / 项目相关文档" | `dws aisearch enterprise --queries "<主题>" --types mail/document --time-range "<时间>"` |
| "我发过/创建过/分享过/收到过什么" | `dws aisearch behavior --queries "<主题>" --behavior-type <动作> --direction <方向>` |
## 标准 SOP(必遵流程)
> 命中以下意图**必须**按对应 SOP 顺序执行;**禁止**跳步、替换命令、编造 flag/ID。每条命令必须带 `--format json`,执行后必须按"解析"步取真实字段,不得凭返回结构猜测。
### SOP-1 搜人 → 拿 userIdsearch-person
**触发**:姓名模糊找人/谁负责/查上下级/部门成员/工号反查/手机号线索语义搜人。
1. **定维度(必须)**:姓名→`name`、"谁负责 XX"→`duty`、部门成员→`department`、上级/下级→`supervisor`/`subordinate`、工号→`jobNumber`、手机号语义线索→`phone`;完整手机号精确反查切到 `dingtalk-contact``user search-mobile`;不确定→`all``--query` 必须按用户原文**完整保真**,切勿截断、改昵称、扩同音字。
2. **执行(必须)**`dws aisearch person --query "<完整值>" --dimension <维度> --format json`
3. **解析(必须)**:从 JSON 取 `userId` / `openDingTalkId`;**多候选必须输出让用户选,禁止默认取第一个、禁止编造**未返回的人员字段。
4. **衔接(必须)**:要邮箱/部门/职位/主管等详情 → 切 `dingtalk-contact` 执行 `dws contact user get --ids <userId> --format json`;发消息 → `dingtalk-chat`;发 DING → `dingtalk-misc``references/ding.md`)。
5. **失败(必须)**:未命中最多换 1 个维度重试一次(如 `name``department`/`jobNumber`/`phone`),仍保留完整目标值;仍无果**必须如实告知**。
**禁止**:用半截姓名扩大搜索、跳过 `--format json`、取首个候选、凭空补全人员信息。
### SOP-2 跨源搜内容(search-content
**触发**:跨文档/邮件/消息按主题找内容。
1. **执行(必须)**`dws aisearch enterprise --queries "<主题>" --types <document,mail,...> --time-range "<时间>" --format json`;多主题逗号分隔。
2. **衔接(必须)**:按命中来源切到对应产品 skill 读写。**aisearch 只负责"找到",不做读写。**
**禁止**:把 aisearch 当作读写入口、跳过下游 skill 直接改数据。
### SOP-3 行为回溯(search-behavior
**触发**:"我发过/收到过/创建过/分享过什么"。
1. **执行(必须)**`dws aisearch behavior --queries "<主题>" --behavior-type <动作> --direction <方向> --format json`
2. **衔接(必须)**:按记录类型切对应 skill 操作;aisearch 不做读写。
**禁止**:编造行为结果、跳过 `--format json`
## 高频硬约束
- 搜索目标必须完整保真:姓名、工号、手机号、部门名按用户原文完整传入 `--query`,严禁自行截断、拆字、改昵称或扩展同音字。
- 首次未命中时最多换维度重试一次(如 name → department/jobNumber/phone),仍必须保留完整目标值;不要用半截姓名扩大搜索。
- 找到候选后,如用户要邮箱、部门、职位、主管等详情,必须切到 `dingtalk-contact` 执行 `contact user get --ids <userId> --format json` 补全。
- 多候选且无法唯一判断时输出候选并询问;不要默认取第一个,也不要编造未返回的人员信息。
- 所有 `dws aisearch` 命令加 `--format json`
## 跨产品协作
- 拿到 userId 后查详情 / 部门 → 切到 `dingtalk-contact`
- 拿到 userId 发消息 → 切到 `dingtalk-chat`
- 拿到 userId 发 DING → 切到 `dingtalk-misc``references/ding.md`
## 局部意图与短流程
- [局部意图消歧](references/intent-guide.md)[短流程](references/lite-recipes.md)。
@@ -0,0 +1,249 @@
# aisearch - AI 搜问
> `aisearch` 模块当前有三个规范子命令:`person`(搜人)、`enterprise`(搜企业内部知识内容)和 `behavior`(搜企业内部行为记录)。
>
> **搜人容错说明**(无需主动使用):CLI 兼容下列 alias 兜底,模型偶尔写 `search` / `find` / `query` / `contact` / `people` 等也能跑通——但**搜人输出和文档以 `person` 为准**。
>
> 搜人的规范参数是 `--query`;旧 `--keyword`(含 `-w`)及历史隐藏参数 `--name` / `--q` / `--text` 继续兼容,新的示例统一使用 `--query`。
## 企业人员搜索
通过关键词搜索企业内人员信息,支持按维度筛选。
```
Usage:
dws aisearch person [flags]
Example:
dws aisearch person --query "张三" --dimension name --format json
dws aisearch person --query "产品部" --dimension department --format json
dws aisearch person --query "五道" --dimension supervisor --format json
dws aisearch person --query "AI搜问" --dimension duty --format json
dws aisearch person --query "李四" --dimension name,department --format json
dws aisearch person --query "<手机号线索>" --dimension phone --format json
dws aisearch person --query "W12345" --dimension jobNumber --format json
Flags:
--query string 搜索关键词 (必填,如人名、技能关键词等)
--dimension string 查询维度,多个用逗号分隔 (默认 "all")
```
### dimension 可选值
| 值 | 含义 | 触发词 |
|----|------|--------|
| `all` | 全部维度(默认) | — |
| `name` | 姓名 | "叫什么"、"是谁" |
| `department` | 部门 | "部门"、"团队"、"哪个部门" |
| `position` | 职位 | "职位"、"岗位"、"职级" |
| `duty` | 职责/技能 | "负责什么"、"职责"、"技能"、"负责人" |
| `supervisor` | 上级 | "上级"、"领导"、"主管" |
| `subordinate` | 下级 | "下级"、"下属"、"团队成员" |
| `phone` | 手机号语义线索 | "电话线索"、"联系方式相关";完整手机号精确反查走 `contact user search-mobile` |
| `jobNumber` | 工号 | "工号"、"工号是多少"、"员工编号" |
### keyword 提取规则
仅填入实际的搜索目标(人名、技能关键词等),不包含查询维度词。维度词必须映射到 `--dimension`
| 用户说 | keyword | dimension |
|--------|---------|-----------|
| "五道的上级是谁" | 五道 | supervisor |
| "张三负责什么" | 张三 | duty |
| "AI搜问的负责人是谁" | AI搜问 | duty |
| "产品部有谁" | 产品部 | department |
| "李四是哪个部门的" | 李四 | department |
| "按手机号线索找人" | 用户提供的手机号线索 | phone |
| "工号W12345是谁" | W12345 | jobNumber |
---
## 意图判断
- 用户说"搜人/找人/谁负责/上级是谁/哪个部门的人" → `aisearch person`
- 用户提供完整手机号精确反查 → `contact user search-mobile`
- 用户说"搜资料/找方案/查文档/搜企业知识/项目相关内容/工作总结/周报总结" → `aisearch enterprise`
- 用户说"最近/本周/今天 + XX相关消息/文档/邮件有哪些" → `aisearch enterprise`,时间词进 `--time-range`,类型词进 `--types`
- 用户说"我发过/谁发给我/创建过/分享过/收到过/今天我干了什么" → `aisearch behavior`
- 用户说"搜同事" → `aisearch person`;查部门详情/部门成员/通讯录精确信息 → `contact`
**关键区分**`aisearch person`(姓名模糊、工号、职责、手机号线索、上下级等搜索)vs `aisearch enterprise`(按内容找企业内部知识)vs `aisearch behavior`(按动作找发送/创建/分享/编辑/接收记录)vs `contact`(完整手机号反查、已知 userId 详情、部门成员列表)
### 高优先级抽取规则
- `enterprise` 抽槽顺序固定为:先抽时间词到 `--time-range`,再抽类型词到 `--types`,最后把剩余主题词放进 `--queries`
- 所有类型词都必须从 `queries` 中剥离,不能写进 `--query/--queries`。例如“最近 OKR 相关邮件”中,`queries=OKR``types=mail``time-range=最近`
- 错误示例:不要生成 `--query "搜索问题"``--query "OKR 邮件"``--query "AI 搜问 日程"``--query "项目 待办"` 这类丢失或混入类型词的命令。
- 也不要把完整自然语言原句塞进 `--query``enterprise` 不做自然语言解析,必须显式拆出 `--queries``--types``--time-range`
- “相关消息/相关文档/相关邮件有哪些”默认是按内容找企业知识,走 `aisearch enterprise`;只有出现“我发过/某人发给我/我收到/我创建/我分享/我编辑”等行为动作时,才走 `aisearch behavior`
- `queries` 只放主题词,不放“最近/本周/消息/文档/邮件/日程/待办/纪要/图片/链接/有哪些/相关”等时间、类型、语气词。
- 只要用户显式说“最近/本周/今天/昨天/本月/过去一周/Q3”等时间词,就必须填写 `--time-range`
- 只要用户显式说出任一类型词,就必须填写对应 `--types`;多个类型同时出现时用逗号分隔,如 `--types im,mail`
### enterprise 类型词映射
| 用户类型词 | types |
|------------|-------|
| 全部、所有、工作总结、日报总结、周报总结、月报总结 | `all` |
| 文档、资料、方案、模板 | `document` |
| 消息、聊天记录、群消息、群里说了什么 | `im` |
| 邮件、邮箱、mail、email | `mail` |
| 日程、会议邀请、会议安排 | `calendar` |
| 待办、任务、TODO | `todo` |
| 会议纪要、纪要、听记、闪记、录音摘要 | `minute` |
| 日志 | `report` |
| 图片、截图 | `image` |
| 链接、URL、网址 | `link` |
| AI 表格、多维表、notable | `notable` |
| 企业百科、百科 | `baike` |
## 上下文传递表
| 操作 | 从返回中提取 | 用于 |
|------|-------------|------|
| `aisearch person` | `userId`(用户ID)、`title`(姓名) | 展示搜索结果、后续操作(发消息/建待办等) |
## 重名消歧
> **CAUTION:** 多人同名时禁止默认选第一个 — 须追加 `contact user get --ids userId1,userId2,...` 获取部门/职位后请用户确认。详见 [08-directory.md](../../dingtalk-contact/references/08-directory.md)「多命中」。
---
## 企业内部知识搜索
用于检索企业内部知识内容,例如文档、消息、日程、待办、听记、日志、图片、链接、AI 表格、企业百科、邮件等。它关注内容本身,适合查找某个主题的资料、搜索包含特定关键词的内容、了解项目/产品相关信息、准备汇报材料等场景。
```
Usage:
dws aisearch enterprise [flags]
Example:
dws aisearch enterprise --queries "智能化方案" --types document --format json
dws aisearch enterprise --queries "搜索问题" --types im --time-range "最近" --format json
dws aisearch enterprise --queries "搜问" --types im --time-range "最近" --format json
dws aisearch enterprise --queries "OKR" --types mail --time-range "最近" --format json
dws aisearch enterprise --queries "AI搜问" --types calendar --time-range "本周" --format json
dws aisearch enterprise --queries "项目" --types todo,minute --time-range "最近" --format json
dws aisearch enterprise --queries "发版" --types im --time-range "本周" --format json
dws aisearch enterprise --types all --time-range "本周" --format json
dws aisearch enterprise --queries "OKR" --types document,im,mail --format json
Flags:
--queries string 内容关键词,多个用逗号分隔;汇总类场景可留空
--types string 搜索类型,多个用逗号分隔 (默认 "all")
--time-range string 时间范围,仅当用户显式给出时间词时填写
```
### enterprise types 可选值
| 值 | 含义 | 触发词 |
|----|------|--------|
| `all` | 全部类型(默认) | 全部、所有、工作总结、日报总结、周报总结、月报总结 |
| `document` | 文档 | 文档、资料、方案、模板 |
| `im` | 消息 | 消息、聊天记录、群消息、群里发了什么 |
| `calendar` | 日程 | 日程、会议邀请、会议安排 |
| `todo` | 待办 | 待办、任务、TODO |
| `minute` | 会议纪要/闪记/听记 | 会议纪要、纪要、听记、闪记、录音摘要 |
| `report` | 日志 | 仅显式出现“日志”时使用 |
| `image` | 图片 | 图片、截图 |
| `link` | 链接 | 链接、URL、网址 |
| `notable` | 多维表 / AI 表格 | AI表格、多维表、notable |
| `baike` | 企业百科 | 企业百科、百科 |
| `mail` | 邮件 | 邮件、邮箱、mail、email |
### enterprise 参数提取规则
- `queries` 只放内容关键词,不放时间、类型词。比如“本周的 OKR 文档”中,`queries=OKR`;“最近 OKR 相关邮件”中,`queries=OKR`,不要写成 `queries=OKR 邮件`
- 时间信息放到 `--time-range`,仅当用户显式给出“今天/本周/最近/9月/Q3/过去一周”等时间词时填写。
- 类型词放到 `--types`,所有类型词都不能留在 `--query/--queries`;多类型用逗号分隔。文档/资料/方案类型使用底层枚举 `document`(注意不是 `doc`)。`report` 仅在用户显式说“日志”时触发;“周报/日报/月报/工作汇报”不要自动映射为 `report`
- `mail` 仅在用户显式说“邮件/邮箱/mail/email”时触发;一旦触发,必须进入 `--types mail`,不能留在 `--query/--queries`
- “工作总结/日报总结/周报总结/月报总结”这类汇总场景,用 `--types all``--queries` 可留空。
| 用户说 | queries | types | time-range |
|--------|---------|-------|------------|
| “智能化方案相关文档” | 智能化方案 | document | 空 |
| “最近搜索问题相关的消息都有哪些” | 搜索问题 | im | 最近 |
| “最近搜问相关的消息都有哪些” | 搜问 | im | 最近 |
| “最近 OKR 相关邮件” | OKR | mail | 最近 |
| “本周 AI 搜问相关日程” | AI 搜问 | calendar | 本周 |
| “最近项目相关待办和纪要” | 项目 | todo,minute | 最近 |
| “本周的 OKR 文档” | OKR | document | 本周 |
| “最近发版相关消息” | 发版 | im | 最近 |
| “AI 搜问相关图片和链接” | AI 搜问 | image,link | 空 |
| “OKR 相关 AI 表格和百科” | OKR | notable,baike | 空 |
| “2025-12-06 到 2025-12-19 工作总结” | 空 | all | 2025-12-06 到 2025-12-19 |
| “本周的日志” | 空 | report | 本周 |
| “我收到的邮件” | 空 | mail | 空 |
---
## 企业内部行为记录搜索
用于检索“谁对什么做了什么”的企业内部行为记录,例如发过、创建过、分享过、编辑过、收到过的文档、消息、日程、待办、听记、日志、图片、链接、AI 表格、企业百科、邮件等。它关注行为流向和动作,不是按内容本身找知识。
```
Usage:
dws aisearch behavior [flags]
Example:
dws aisearch behavior --queries "智能化方案" --types document --format json
dws aisearch behavior --types mail --behavior-type send --direction "我->汐峰" --format json
dws aisearch behavior --types all --behavior-type create --time-range "本周" --format json
dws aisearch behavior --types im --chat-scope "scrum群" --behavior-type send --time-range "今天" --format json
Flags:
--queries string 内容关键词,多个用逗号分隔;汇总类场景可留空
--types string 搜索类型,多个用逗号分隔 (默认 "all")
--chat-scope string 消息所在会话/群范围,仅 IM 类型且用户明确指定群名时填写
--behavior-type string 行为类型 (默认 "all")
--time-range string 时间范围,仅当用户显式给出时间词时填写
--direction string 交互方向,如 "我->汐峰"、"汐峰->我"、"我<->汐峰"
```
### behavior types 可选值
| 值 | 含义 | 触发词 |
|----|------|--------|
| `all` | 全部类型(默认) | 今天我干了什么、我最近做过什么 |
| `document` | 文档 | 文档、资料、方案、模板 |
| `im` | 消息 | 消息、聊天记录、群消息、群里发了什么 |
| `calendar` | 日程 | 日程、会议邀请、会议安排 |
| `todo` | 待办 | 待办、任务、TODO |
| `minute` | 会议纪要/闪记/听记 | 会议纪要、纪要、听记、闪记、录音摘要 |
| `report` | 日志 | 仅显式出现“日志”时使用 |
| `image` | 图片 | 图片、截图 |
| `link` | 链接 | 链接、URL、网址 |
| `notable` | 多维表 / AI 表格 | AI表格、多维表、notable |
| `baike` | 企业百科 | 企业百科、百科 |
| `mail` | 邮件 | 邮件、邮箱、mail、email |
### behavior-type 可选值
| 值 | 含义 | 示例 |
|----|------|------|
| `all` | 全部行为(默认) | “智能化方案相关内容” |
| `send` | 发送 | “我发给汐峰的消息/邮件” |
| `create` | 创建 | “我创建过哪些文档” |
| `share` | 分享 | “我分享过的资料” |
| `edit` | 编辑 | “我编辑过的文档” |
| `receive` | 接收 | “汐峰发给我的文档”、“我收到的邮件” |
### 参数提取规则
- `queries` 只放内容关键词,不放时间、类型词、行为词。比如“本周我创建的智能化方案文档”中,`queries=智能化方案`
- `types` 放内容类型,映射规则与 enterprise 相同;所有类型词都不能留在 `--query/--queries`,多类型用逗号分隔。
- 文档/资料/方案类型使用底层枚举 `document`(注意不是 `doc`)。`report` 仅在用户显式说“日志”时触发;“周报/日报/月报/工作汇报”不要自动映射为 `report`
- `mail` 仅在用户显式说“邮件/邮箱/mail/email”时触发;一旦触发,必须进入 `--types mail`
- `time-range` 仅当用户显式给出时间词时填写,不要根据语义猜时间。
- `direction` 仅当用户明确指定交互对象时填写,格式为 `发起者->接收者``我<->某人`;无具体对象时留空。
- “今天我干了什么/我最近做过什么”这类行为汇总场景,用 `--types all``--queries` 可留空。
| 用户说 | queries | types | behavior-type | time-range | direction | chat-scope |
|--------|---------|-------|---------------|------------|-----------|------------|
| “我发给汐峰的邮件” | 空 | mail | send | 空 | 我->汐峰 | 空 |
| “我发给汐峰的消息和邮件” | 空 | im,mail | send | 空 | 我->汐峰 | 空 |
| “汐峰发给我的文档” | 空 | document | receive | 空 | 汐峰->我 | 空 |
| “我创建过哪些文档” | 空 | document | create | 空 | 空 | 空 |
| “本周我创建的智能化方案文档” | 智能化方案 | document | create | 本周 | 空 | 空 |
| “我分享过的项目链接和图片” | 项目 | link,image | share | 空 | 空 | 空 |
| “我在 scrum 群里发了什么” | 空 | im | send | 空 | 空 | scrum群 |
| “帮我总结今天干了什么” | 空 | all | all | 今天 | 空 | 空 |
## 行为搜索 vs 知识搜索
- `aisearch behavior`:用户问“我/某人做过什么动作”,有发送、创建、分享、编辑、接收、收到、发给等行为词。
- `aisearch enterprise`:用户问“是什么/怎么做/在哪里/模板/方案/总结”,目标是内容本身或跨类型知识内容汇总。
@@ -0,0 +1,15 @@
# aisearch 局部意图消歧
本文件从单 Skill `intent-guide.md` 拆分而来,仅保留与本产品相关的跨产品消歧规则。
| 用户说... | 真实意图 | 应该用 | 不要用 | 理由 |
|---|---|---|---|---|
| "张三在哪个部门/张三的工号是多少" | 搜人后查通讯录详情 | `aisearch person``contact user get` | 直接 `contact user search` | 姓名或工号先由 aisearch 获取 userId,再由 contact 补部门、工号等详情 |
| "找一下张三/搜同事/找人" | 人员语义搜索 | `aisearch person` | `contact user search` | 姓名模糊搜索、工号、部门、职责和上下级走 aisearchcontact 在拿到 userId 后补详情 |
| "五道的上级是谁/谁负责XX/XX的下属有谁" | AI语义搜人 | `aisearch person` | `contact` | 涉及上下级、职责、负责人等语义维度搜索,用 aisearch |
| "222020这个工号是谁/查工号" | 按工号搜人 | `aisearch person --dimension jobNumber` | `contact` | 工号查人走 aisearchdimension=jobNumber |
| "13800138000是谁/完整手机号反查" | 精确手机号反查 | `contact user search-mobile` | `contact user search` | 完整手机号精确匹配使用 search-mobile |
| "按手机号线索找人" | 手机号语义搜人 | `aisearch person --dimension phone` | `contact user search` | 非精确手机号匹配走 aisearch 的 phone 维度 |
| "搜一下智能化方案/最近 OKR 相关邮件/最近发版相关消息" | 搜企业知识内容 | `aisearch enterprise` | `doc search` / `mail search` / `chat message search` | 跨文档、消息、日程、听记、邮件等企业内容语义检索走 enterprise;具体 `queries/types/time-range` 抽槽见 `aisearch.md` |
| "我发给某人的消息/邮件/文档/今天我干了什么" | 搜行为记录 | `aisearch behavior` | `chat` / `mail` / `doc` / `report` | 关注“谁对什么做过什么”,走 behavior;具体 `behavior-type/direction/chat-scope` 抽槽见 `aisearch.md` |
| "把这段文字翻译成英文/translate this" | 通用文本翻译 | `chat text translate` | `doc` / `aisearch` | 纯文本翻译,不是文档编辑或语义搜索 |
@@ -0,0 +1,31 @@
# aisearch Lite Recipe
本文件从单 Skill `lite-recipes.md` 拆分而来,仅保留与本产品相关的轻量流程。
## #8 通讯录
### get-contact-self
`contact user get-self` → 当前用户 userId、部门、主管等
### search-person
**搜人首选入口**。凡是“找人/搜人/找同事/谁负责/上级/下级/负责人/团队成员”均优先用 `aisearch person`
1. 从用户问题中提取 keyword(人名/业务关键词)和 dimension(维度),规则见 [aisearch.md](./aisearch.md)。
2. `aisearch person --query "<关键词>" --dimension <维度>`
3. 结果中提取 `userId``title`(姓名)展示给用户。
4. 若需要 userId 做后续操作(发消息/建待办),可直接使用结果中的 `userId`
5. **重名消歧**:多人同名时禁止默认选第一个,须追加 `contact user get --ids` 获取部门/职位后请用户确认,详见 [08-directory.md](../../dingtalk-contact/references/08-directory.md)「多命中」。
### search-user
仅在以下**精确查询**场景使用,搜人请优先用 `search-person`
- 需要获取 userId 给其他产品使用(发消息/建待办/约日程)
- 已有 userId 需查完整详情(`contact user get --ids`
- 完整手机号精确反查(`contact user search-mobile --mobile`
1. 完整手机号精确反查:`contact user search-mobile --mobile "<手机号>"`;其他搜人:`aisearch person --query "<关键词>" --dimension <维度>`
2. **重名消歧**:多人同名时禁止默认选第一个,须追加 `contact user get --ids` 获取部门/职位后请用户确认,详见 [08-directory.md](../../dingtalk-contact/references/08-directory.md)「多命中」。
3. 需详情时:`contact user get --ids <userId>`(多人可 `--ids id1,id2,...`)。
+130
View File
@@ -0,0 +1,130 @@
---
name: dingtalk-aitable
description: 钉钉 AI 表格(多维表)。Use when 用户说 AI表格/多维表/数据表/base/table/建表/查记录/写数据/字段/记录增删改查/筛选/排序/公式/模板搜索/批量导入CSV或JSON/导出/仪表盘/图表/上传附件到表格/按字段类型建表/数据源/创建数据源/更新数据源配置/触发数据源同步/按任务 ID 查询同步状态/获取数据源配置/列出数据源可用来源/获取数据源可同步字段/审批数据同步。不做电子表格单元格读写(走 dingtalk-misc)、文档编辑(走 dingtalk-doc);听记待办入表先用 dingtalk-minutes 提取,再由本 skill 写入。命令前缀:dws aitable。
metadata:
cli_version: ">=0.2.14"
category: product
requires:
bins:
- dws
---
# 钉钉 AI 表格 Skill
<!-- DWS_RUNTIME_CONTRACT_START -->
## 最小 DWS 执行契约
- 只通过 `dws` CLI 操作钉钉;结构化读取使用 `--format json`,按真实返回判断结果。
- 已知 leaf 直接执行。只有参数不确定时,最多读取一次 `dws schema --cli-path "aitable <leaf>" --compact --format json`;仅当该 compact leaf Schema 与 Cobra 实际不一致时,才读取同一 leaf 的 `dws aitable <leaf> --help`。禁止通过父级 Help、`dws aitable --help` 或完整 Catalog 探索命令。
- 不猜命令、flag、字段、ID、账号或时间。后续 ID 必须来自真实返回;零命中、多候选或类型不明时停止并消歧。
- 解析目标、读取上下文和最终执行必须使用同一 profile;不得跨组织复用 userId、openDingTalkId 或 openConversationId。多账号组织只使用明确的 `isOrgCurrent=true` 默认账号;没有默认账号时要求用户指定,禁止选择第一项、最近登录或最近使用账号。
- 不输出或记录 token、refresh token、appSecret、webhook token 等凭据;宿主已注入认证时不要索要凭据。
- 写操作必须符合用户明确意图。是否需要确认以最终 Runtime gate 和 Schema 为准;本轮用户已明确要求执行、目标与影响无歧义的非破坏性写操作时,该明确指令就是本次确认,首次调用直接携带 Runtime 所需的 `--yes`,不先制造 `confirmation_required`。删除、停用自动化等破坏性或高风险动作仍须先说明对象、动作与影响并取得独立确认。
- 写后按任务结果契约验证;不能仅凭退出码宣称成功。部分结果、未知投递状态和失败项必须如实保留。
- 时间戳面向用户展示时转换为带时区的可读时间;默认使用当前会话时区,必要时同时保留原值。
- 遇到认证、权限、profile、confirmation 或未知错误时,只加载 `dingtalk-shared` 中对应 reference;不要连续猜测替代命令。
<!-- DWS_RUNTIME_CONTRACT_END -->
<!-- VISIBLE_SHORTCUTS_START -->
## Shortcut 发现(按需)
`aitable` 当前有 100 条公开 shortcut,完整清单保留在 Runtime Catalog 与 Schema,不在高频产品根 Skill 中重复展开。已知 leaf 直接执行。只有参数不确定时,最多读取一次 `dws schema --cli-path "aitable <leaf>" --compact --format json`;仅当该 compact leaf Schema 与 Cobra 实际不一致时,才读取同一 leaf 的 `dws aitable <leaf> --help`。禁止用父级 Help、产品 Help 或完整 Catalog 探索命令;一个 Case 一旦读取 Reference,就不再读取 Help 或第二个 Reference。
仅当根路由、精确 task reference 和 `references/aitable.md` 的低频原子索引都无法定位能力时,才执行 `dws shortcut list --service aitable --format json` 做最终回退;不要为已知意图加载完整 Shortcut Catalog 或产品级 Schema。
<!-- VISIBLE_SHORTCUTS_END -->
## Golden Route
已有 ID 直接使用;完整 URL 先解析;名称先唯一解析为稳定 ID。零命中或多候选时停止,不默认选第一项。
| 用户意图 | 唯一推荐入口 | 关键边界 |
|---|---|---|
| 从 URL 解析稳定 ID | `dws aitable +url-resolve --url <URL>` | 只解析 URL 中已有的 baseId/tableId/viewId/recordId,不做远端名称搜索 |
| 按名称唯一定位并操作 Base/Table | `dws aitable +resolve-base --name <名称>``dws aitable +resolve-table --base <ID> --name <表名>` | 默认精确匹配;只有用户明确接受模糊匹配时才加 `--fuzzy` |
| 浏览 Base 下的数据表 | `dws aitable +list-tables --base <ID>` | 只返回 tableId/tableName,不加载字段 |
| 搜索 Base 候选或检查是否存在 | `dws aitable +base-search --query <关键词>` | 用户说“搜索/找一下/候选/如果没有就创建”时直接走本入口,不先调用 `+resolve-base`AITable 上下文中的 Base 名称不得路由到 `dws aisearch person` |
| 新建 Base 与整套表字段 | `dws aitable +base-bootstrap --name <名称> --tables '[{"name":"<表名>","fields":[{"fieldName":"<字段名>","type":"text"}]}]'` | 表对象键必须是 `name`,不是 `tableName`;字段使用 `fieldName/type/config`;参数已足够时直接执行,不读 Reference 或 Help |
| 已有 Base 新建一张表与字段 | `dws aitable +table-bootstrap --base-id <ID> --name <表名> --fields '<JSON数组>'` | 字段使用 `fieldName/type/config`;自动按 15 个字段分片并读回验证 |
| 读取字段目录或完整配置 | `dws aitable field list --base-id <B> --table-id <T>` / `dws aitable +field-get --base-id <B> --table-id <T>` | 只需 fieldId/name/type 用 `field list`;需要 config 用 `+field-get`;不存在 `+field-list``+list-fields` |
| 查询、筛选、排序或字段投影 | `dws aitable +record-query --base-id <ID> --table-id <ID> [--record-ids <IDs>] [--field-ids <IDs>] [--filters <JSON>] [--sort <JSON>] [--query <关键词>]` | 用户要求“只返回/仅查看”指定字段时必须传对应 `--field-ids`,不能只在最终文本删列;明确要求全量时改用原子 `record query --all --page-limit <N>` |
| 查询一条记录的变更历史 | `dws aitable +record-history-list --base-id <ID> --table-id <ID> --record-id <ID>` | 已知 recordId 时直接执行;不要调用 Help、产品 Catalog 或全量 Schema 寻找 history 命令 |
| 新增单条或批量记录 | `dws aitable record create --base-id <ID> --table-id <ID> --records <JSON>` | 当前无 `+record-create`;写前取字段定义,写后按新 ID 回读 |
| 更新已知 recordId | `dws aitable +record-update --base-id <ID> --table-id <ID> --records <JSON>` | 自动分片并读回;只传需修改字段 |
| 按业务唯一键同步 | `dws aitable +record-upsert-by-key --base-id <ID> --table-id <ID> --key-field-id <ID> --key-value <值> --cells <JSON>` | 0 条创建、1 条更新、多条停止;非字符串键改用 `--key-value-json` |
| 按条件批量修改 | `dws aitable +record-bulk-patch --base-id <ID> --table-id <ID> --query <关键词> --patch <JSON> --max-matches <N>` | 也可用 filters/record-ids 选范围;禁止无边界整表写 |
| 删除整个 Base | `dws aitable +base-delete --base-id <ID>` | 先通过只读命令确认真实 ID;按 Runtime confirmation 执行,不用 Drive 删除同名节点 |
| 删除字段 | `dws aitable +field-delete --base-id <ID> --table-id <ID> --field-id <ID>` | 先读取字段目录并确认非主字段;按 Runtime confirmation 执行 |
| 查询/创建记录主键文档 | `dws aitable +record-primary-doc-get|+record-primary-doc-create ...` | create 必须传 primaryDoc 类型的 `--field-id`;正文操作切到 Doc |
| 生成记录分享链接并发送给联系人 | `dws aitable +record-share-links --base <B> --table <T> --record-ids <IDs>``dws chat +dm --to <姓名> --text <完整链接文本>` | AITable 只生成链接;用户要求“发送”时必须加载 `dingtalk-chat` 并对每位收件人完成真实发送,不能停在联系人解析 |
| 创建 View / Dashboard / Chart 或导入文件 | 对应 leaf / `+import-*` | 根 Skill 参数足够则直接执行;复杂配置最多读取一个对应操作 Reference,不读取通用索引 |
| 调整视图列顺序 | `dws aitable view update visible-fields --base-id <ID> --table-id <ID> --view-id <ID> --field-ids <完整有序IDs>` | 先读取字段和当前完整列数组,固定主字段在首位,写后回读精确校验 |
| 创建/修改图表前取配置 | `dws aitable +chart-widgets-example` | 命令返回所有图表类型示例;已有合法 config 时直接 create/update |
| Base 内 Section/节点移动 | `dws aitable +section-*` | Table/Dashboard/Section 是 Base 内 nsheet 节点,不是独立 Drive 节点 |
| 接入外部数据源(审批等) | `dws aitable +datasource-list-sources --base-id <ID> --datasource-type OA` → 解析 result 构造 sourceConfig → `dws aitable +datasource-create --base-id <ID> --datasource-type OA --source-config '<JSON>'` | 当前仅支持 OA 审批;sourceConfig 中 processCode/name/iconUrl/url 须从 list-sources 原样透传;创建后用 `+datasource-sync-status` 查同步结果 |
### 常用 leaf 直达
参数已知时直接执行,不探测 Help/CatalogBase 查看/改名用 `+base-get` / `+base-update`;模板搜索用 `+template-search`,再把真实 templateId 交给 `base create --template-id`Table 查看/更新用 `+table-get` / `+table-update`;视图创建/复制用 `view create` / `+view-duplicate`;仪表盘创建/更新/读回用 `dashboard create` / `+dashboard-update` / `+dashboard-get`;表单分享用 `+form-share-update` / `+form-share-get`;查看自动化用 `+workflow-list`;数据源查看来源用 `+datasource-list-sources`,获取字段用 `+datasource-get-fields`,创建/更新/同步/查状态/查配置用 `+datasource-create` / `+datasource-update` / `+datasource-sync` / `+datasource-sync-status` / `+datasource-get-config`
### 低频入口
字段配置用 `+field-*`;删记录用 `+record-delete`;附件用 `+attachment-*`。批量分享记录用 `+record-share-links --base <B> --table <T> --record-ids <IDs>`。其余能力使用同名前缀 leaf。
## 当前最短路径
- 已有 ID 直接使用;URL 只解析一次;“唯一定位并操作”用 `+resolve-base` / `+resolve-table`,“搜索候选/存在性检查”直接用 `+base-search`,两条路径不要串行探测。filters/sort 缺 fieldId 时才读取字段目录。
- Golden Route 已给出准确命令和参数时直接执行;不预读或默认读取通用 `references/aitable.md`。只有操作参数、JSON 结构或恢复语义确实缺失时,才读取下方一个精确操作 Reference。
- Shortcut 已含分片或验证时不重复拆步;已有 Base 新建完整表结构直接用 `+table-bootstrap`
- 单产品线性任务直接执行,不创建 TodoWrite;只有跨产品或多个独立分支的长任务才建计划,并且只在阶段切换时更新,不在每条 CLI 后刷新状态。
- 用户要求资源名带当前时间戳时只取一次并在 Base、Table、Dashboard 等名称中复用同一值;不要为每个资源分别取时间。
- JSON 已返回所需字段时立即复用;不得为寻找同一字段改用 `--verbose``raw``pretty` 重复请求。
- 数据源创建前必须先 `+datasource-list-sources` 获取 processCode 等透传字段,不要凭记忆或猜测构造 sourceConfig。
## 记录输入与结果
- `cells` key 用当前 fieldId;大 JSON 用相对 `--records-file`。filters 顶层为 `and|or`sort 使用 `direction`;复杂条件读 [filter-sort](references/aitable/aitable-filter-sort.md)。
- 建表字段类型使用真实枚举:单选为 `singleSelect`;人民币货币字段使用 `type:"currency"``config:{"currencyType":"CNY","formatter":"FLOAT_2"}`,不要猜 `select``config.symbol`
- 用户限定返回字段时,先复用当前字段目录中的真实 fieldId,最终 `+record-query` 必须带 `--field-ids <ID1,ID2>`;工具层投影是业务要求和 token 控制的一部分,不能用最终答复二次过滤替代。
- 按真实字段类型写值,只读字段不得写入。
- 新建从 `data.newRecordIds[]` 取 ID,再用 `+record-query --record-ids` 回读;若用户同时限定列,回读命令一并传 `--field-ids`
- 批量结果检查 completed/failed、verification、checkpoint`partial_success` 不是完成。全量查询使用原子 `record query --all` 并检查 `hasMore`;只有 `hasMore=false`,或按指定 ID 全命中时,才声称结果完整。
- 写入效果未知时回读,不重放成功批次。
## 安全边界
- 删除不可逆,按 Runtime confirmation 核对真实目标;`base list` 只是最近访问。字段零/多候选、类型不明时停止;多批写保留已完成批次和续跑位置。
- 数据源 `+datasource-create` / `+datasource-update` 会触发真实数据同步(全量),执行前确认目标 Base 和 sourceConfig 无误。`+datasource-sync` 同理,单次最多 5 张表。
## 按需加载
每个 Case 最多读取一个操作 Reference。Golden Route 参数足够时读取零个并直接执行;一旦读取了一个 Reference,本 Case 不再读取第二个 Reference、通用 `aitable.md`、产品级 Catalog 或 Help。
| 触发条件 | Reference |
|---|---|
| 记录 CRUD、字段值格式 | [record-ops](references/aitable-record-ops.md) |
| 记录主键文档 | [primary-doc](references/aitable/aitable-primary-doc.md) |
| filters/sort/date 操作符 | [filter-sort](references/aitable/aitable-filter-sort.md) |
| 字段创建或复杂配置 | [field](references/aitable/aitable-field.md) |
| 导入导出任务恢复 | [export-import](references/aitable/aitable-export-import.md) |
| 视图列顺序、筛选、排序、冻结 | [view-config](references/aitable/aitable-view-config.md) |
| Base 内 Section/节点移动或清理 | [section](references/aitable-section.md) |
| 图表配置 | [dashboard-chart](references/aitable/aitable-dashboard-chart.md) |
| 附件、表单、工作流 | 读取 `references/aitable/` 下对应的一个精确文件 |
| 数据源接入、同步管理、sourceConfig 构造、同步审批数据到 AI 表格 | [datasource](references/aitable/aitable-datasource.md) |
| 产品边界不明确 | [intent-guide](references/intent-guide.md) |
| 无法匹配上述任何精确 reference 的原子能力 | [aitable.md](references/aitable.md) 的对应章节 |
不要预加载这些 reference。`references/aitable.md` 只在根路由和精确 task reference 都无法定位原子能力时读取对应章节;完整 Shortcut Catalog 仅在该索引仍无法定位时使用。每个 Case 最多读取一个 Reference,禁止连读。
## 错误最短路径
1. 零/多候选、字段歧义或分页不完整:停止并返回证据;需要后续页时只透传真实 `nextCursor`
2. 类型错误只复核目标字段,不删字段或丢输入;`partial_success` 从 checkpoint 续跑,未知写入先回读。
3. 错误包含 `actions` / `available_flags` 时只执行其中的 `next_command`;同一操作最多做一次有证据的参数修正。`retryable=false` 或目标 ID 类型不符时停止,不把 Drive/Wiki/Space/子节点 ID 轮流代入试错。
4. 数据源同步 `errorCode=4014` 为幂等冲突(同步运行中重复触发),标记 FAILED 但可稍后重试;非数据源表(sync=false)触发 sync 会返回参数错误,先用 `+base-get` 确认 sync=true。
## 跨产品边界
- Excel 式单元格、区域和公式操作 → `dingtalk-misc` 的 Sheet。
- Base 作为整体在普通文件夹间移动或做外层存储重命名 → Drive;Base 结构复制/删除,以及 Base 内 Table、Dashboard、Section 的创建、复制、移动、重命名、删除 → AITable。
- 记录主键文档正文 → 取得真实 nodeId 后切 `dingtalk-doc`
@@ -0,0 +1,11 @@
# 数据分析
> 本场景所有 recipe 均为 full。
| Recipe | 行动指南(固定路线) |
|--------|-------------------|
| read-aitable | 1. `aitable base search --query "<表格名>"` → 取 `baseId`/`tableId`<br>2. `aitable field get --base-id <baseId> --table-id <tableId>` → 取 `fieldId`<br>3. `aitable record query --base-id <baseId> --table-id <tableId>` → 取记录(分页)<br>  需要筛选时 `--filters` 格式见 [aitable-filter-sort.md](./aitable/aitable-filter-sort.md),根节点必须是 `{"operator":"and\|or","operands":[...]}`<br>4. 总结数据 |
| generate-data-report | 1. 同 read-aitable 步骤 1-3<br>2. 按[「多源并行采集」](recipes/conventions.md#多源并行采集公共模式)执行 → 补充背景<br>3. `doc create --name "<报告名>" --content "<分析报告>"` |
| create-aitable-record | **批量导入优先**`python scripts/import_records.py <baseId> <tableId> data.csv\|data.json [batch_size]`(自动分批创建)<br>单条/少量:1. `aitable base search --query "<表格名>"` → 取 `baseId`/`tableId`<br>2. `aitable field get --base-id <baseId> --table-id <tableId>` → 取 `fieldId` 与类型<br>3. `aitable record create --base-id <baseId> --table-id <tableId> --records '[{"cells":{"<fieldId>":"值"}}]'` |
| update-aitable-record | 1. `aitable base search --query "<表格名>"` → 取 `baseId`/`tableId`<br>2. `aitable record query --base-id <baseId> --table-id <tableId>` → 取 `recordId`**先展示让用户确认**<br>3. `aitable record update --base-id <baseId> --table-id <tableId> --records '[{"recordId":"<recordId>","cells":{...}}]'` |
| search-aitable-template | 1. `aitable template search --query "<关键词>"` → 取 `templateId`<br>2. 用户选定<br>3. `aitable base create --name "<表格名>" --template-id <templateId>` → 取 `baseId` |
@@ -0,0 +1,78 @@
# AI 表格记录操作
仅在根 Skill 的记录 Golden Route 参数不足,或需要字段值格式、删除、历史、分享、附件细节时读取。本文件不负责 Base/Table 选路。
## 查询
```bash
dws aitable +record-query --base-id <B> --table-id <T> --record-ids <R1,R2>
dws aitable +record-query --base-id <B> --table-id <T> --record-ids <R1,R2> --field-ids <F_NAME,F_STATUS>
dws aitable +record-query --base-id <B> --table-id <T> --query "关键词" --limit 100
dws aitable +record-query --base-id <B> --table-id <T> --filters '<JSON>' --sort '<JSON>'
dws aitable record query --base-id <B> --table-id <T> --filters '<JSON>' --all --page-limit 50
```
- `record-ids` 用于稳定 ID 精确读取;`query` 用于全文搜索;复杂条件用 `filters`
- 用户要求“只返回/仅查看”指定列时,查询必须在工具层传 `--field-ids <ID1,ID2>`。不要先拉取全部字段再只在最终答复中删列;字段投影既是结果契约,也是降低响应 token 的手段。
- filters 使用 `{"operator":"and|or","operands":[...]}`,字段引用使用 fieldId。若 Case 明确需要复杂操作符,应一开始把 [filter-sort](aitable/aitable-filter-sort.md) 选为唯一 Reference,而不是先读本文件后继续加载。
- `+record-query` 必须传真实 `base-id``table-id`。URL 先用 `+url-resolve`;名称先用 `+resolve-base` / `+resolve-table` 唯一解析,禁止自动选第一项。
- 单页 `limit` 为 1-100。返回 `data.records``data.hasMore`,存在后续页时还会返回 `data.nextCursor`;把该值原样传给下一次 `--cursor`
- `+record-query` 不提供 `--all`;明确要求全量时改用原子 `record query --all --page-limit <N>`。达到页上限后仍有 `hasMore=true` 代表截断,应从返回 cursor 续跑;只有 `hasMore=false`,或按 `record-ids` 查询且所有请求 ID 均已返回时,才能声称结果完整。
## 新增
当前没有 `+record-create`,使用原子命令:
```bash
dws aitable record create --base-id <B> --table-id <T> \
--records '[{"cells":{"fldText":"内容"}}]' --format json
```
长 JSON 写到 cwd 内相对文件后使用 `--records-file ./records.json`。从真实返回的 `data.newRecordIds[]` 取 recordId,再用 `+record-query --record-ids` 回读;用户限定返回列时同时传 `--field-ids`。不要从输入顺序、名称或行号推断 ID。
## 更新、同步与批量修改
已知 recordId
```bash
dws aitable +record-update --base-id <B> --table-id <T> \
--records '[{"recordId":"<R>","cells":{"fldStatus":"完成"}}]'
```
`+record-update` 自动按 100 条分片并逐批回读;该 shortcut 只接受 `--records`。超长文件输入需要改用原子 `record update --records-file`,不要给 shortcut 猜造 flag。
单选、多选等字段的写入值可能是名称字符串,读回则是 `{id,name}` 或对象数组。若 shortcut 因原始类型不同返回 commit-unknown/read-back mismatch,禁止重放更新;只按返回的 recordId 做一次 `+record-query`,把单选按 `name`、多选按名称集合归一化比较。归一化后与目标一致时,按“独立读回已确认写入”报告,并保留 shortcut 的误报信息供排障。
按业务唯一键同步使用 `+record-upsert-by-key`:0 条创建、1 条更新、多条冲突停止。按条件批改使用 `+record-bulk-patch`,必须提供 filters/query/record-ids 中至少一种选择条件,或显式 `--all`,并设置合理 `--max-matches`
## 删除
```bash
dws aitable +record-delete --base-id <B> --table-id <T> --record-ids <R1,R2>
```
删除不可逆。只使用已确认的真实 recordId;shortcut 自动分片并验证记录已不存在。未知结果按 recordId 回读,不重放已完成批次。
## 常用字段值
| 字段类型 | 写入值 |
|---|---|
| 文本、单选 | 字符串;单选使用已有选项名称 |
| 多选 | 选项名称数组 |
| 数字、评分 | JSON number |
| 复选框 | boolean |
| 日期 | 按字段配置要求的时间值;不凭展示文本猜格式 |
| URL | 按当前字段 Schema 要求的对象或字符串 |
| 人员、关联记录 | 使用真实 userId/recordId,不用姓名代替 |
| 附件 | 先用 `+attachment-put` 获得 AITable 附件 token,再写字段 |
公式、查找引用、创建人/时间、修改人/时间等只读字段不得写入。字段类型不明时只读取目标字段配置一次。
## 历史、分享与主键文档
- 记录历史:`dws aitable +record-history-list --base-id <B> --table-id <T> --record-id <R>`。已有真实 recordId 直接执行,不扫描 Help 或产品 Catalog。
- 批量记录分享:`dws aitable +record-share-links --base <B> --table <T> --record-ids <R1,R2>`;单条也可用 `+record-share-url`
- 用户要求把分享链接“发给”联系人时,AITable 的职责在链接生成后结束;随后加载 `dingtalk-chat`,用 `dws chat +dm --to <姓名> --text <包含全部链接的文本>` 对每位收件人分别发送并检查真实回执。只解析联系人或只生成 URL 都不算完成。
- 主键文档:`+record-primary-doc-get` / `+record-primary-doc-create`
创建主键文档必须显式传 primaryDoc 类型的 `--field-id`;字段类型不明时先读取目标字段。正文读写切到 Doc;这里仅管理记录与文档关联。
@@ -0,0 +1,28 @@
# AI 表格 Section 与内部节点
只在用户操作 Base 内文件夹(Section)或把 Table/Dashboard 移入、移出 Section 时读取。这里的节点是 nsheet 业务节点,不是独立 Drive dentry;不要加载 Drive Skill 或尝试 Drive move。
## 高频闭环
创建 Section 并移动节点:
```bash
dws aitable +section-create --base-id <B> --name "归档区" --format json
dws aitable +section-move-node --base-id <B> --node-id <TABLE_OR_DASHBOARD_ID> --new-parent-section-id <S> --format json
dws aitable +section-list-nodes --base-id <B> --format json
```
移动回 Base 根目录时显式传空字符串,不能省略该参数或改用 Drive:
```bash
dws aitable +section-move-node --base-id <B> --node-id <N> --new-parent-section-id '' --format json
```
## 删除空 Section
1.`+section-list-nodes` 核对目标 Section 内节点;需要移出的节点逐个 `+section-move-node`
2.`+section-list-empty --base-id <B>` 验证目标 sectionId 确实为空。
3. 执行 `+section-delete --base-id <B> --section-id <S>`;按 Runtime confirmation 处理。
4. 再次 `+section-list-nodes``+section-list-empty`,确认 Section 已不存在且被移动节点仍在预期父级。
Table 本身的创建、复制、改名、删除分别使用 `+table-*`Section 只管理 Base 内目录关系。
@@ -0,0 +1,138 @@
# AITable 低频原子能力索引
> 返回入口:[DingTalk AITable Skill](../SKILL.md)
本文件只用于根 Skill 和精确 task reference 都未覆盖的低频底层能力。Base/Table
定位、建表、记录查询与写入、字段配置、视图编排、导入导出等常见任务必须回到根 Skill
的 Golden Route 或对应 task reference,不在这里重新选路。
## 使用边界
1. 先确认任务确实需要 Shortcut 未发布的底层字段、原始响应或运维控制;
2. 只读取精确原子 leaf Schema/Help,不加载产品级 Catalog 猜参数;
3. URL、名称和自然目标仍须解析为唯一稳定 ID,禁止选第一项;
4. 原子写 leaf 的 confirmation 若与对应 Golden Shortcut 不一致,停止并报告交付漂移;
5. 后续 ID 只使用当前 profile 的真实返回,不跨组织复用;
6. 完成后保留 verification、partial failure、checkpoint 和可继续编排的稳定 ID。
## 高频任务返回表
| 用户终点 | 返回入口 |
|---|---|
| URL/名称解析、Base 搜索、建 Base/Table、记录查询与 CRUD | 根 Skill Golden Route |
| 记录值格式、批量写、历史、分享和统计 | [record-ops](aitable-record-ops.md) |
| 筛选、排序和日期操作符 | [filter-sort](aitable/aitable-filter-sort.md) |
| 字段类型、创建与复杂配置 | [field](aitable/aitable-field.md) |
| 视图列顺序、筛选、排序、冻结和展示配置 | [view-config](aitable/aitable-view-config.md) |
| 表单字段、题目与分享 | [form](aitable/aitable-form.md) |
| Dashboard 与 Chart 配置 | [dashboard-chart](aitable/aitable-dashboard-chart.md) |
| 导入、导出和异步任务恢复 | [export-import](aitable/aitable-export-import.md) |
| 附件、工作流与高级权限 | 对应的 [attachment](aitable/aitable-attachment.md)、[workflow](aitable/aitable-workflow.md) 或 [advperm](aitable/aitable-advperm.md) |
| 记录主键文档 | [primary-doc](aitable/aitable-primary-doc.md) |
| Base 内 Section/节点编排 | [section](aitable-section.md) |
| 相邻产品或低频意图仍需消歧 | [intent-guide](intent-guide.md) |
## Base、Table 与 Field 底层能力
| 原子命令 | 仅用于 |
|---|---|
| `aitable base get` / `list` / `search` | Shortcut 未投影的 Base 原始详情、最近访问列表或原始搜索响应 |
| `aitable base create` / `copy` / `update` / `delete` | Shortcut 未发布的底层创建、复制或变更字段;普通整套创建回根 Skill |
| `aitable base get-primary-doc-id` | 需要 Base 视角的记录主键文档 ID |
| `aitable table get` / `list` | 需要原始 Table 结构或目录响应 |
| `aitable table create` / `update` / `delete` | Shortcut 未发布的底层 Table 字段;完整建表回根 Skill |
| `aitable field get` / `list` / `search-options` | 字段原始配置、目录或选项搜索 |
| `aitable field create` / `update` / `delete` | 精确字段原子写入,且字段配置已由 task reference 校验 |
| `aitable template search` | 需要模板原始响应;普通模板检索使用 `+template-search` |
`base list` 仅表示最近访问,不是组织内全量 Base。Table、Field 和记录 ID 均属于指定
Base;同名对象零命中或多候选时停止,不能把名称、URL 末段或其他产品节点 ID 直接当稳定 ID。
## Record 底层能力
| 原子命令 | 仅用于 |
|---|---|
| `aitable record get` / `list` / `query` | Shortcut 未投影的原始记录响应、显式 continuation 或窄 ID 查询 |
| `aitable record query-empty` | 查找未填写用户字段的空行 |
| `aitable record history-list` | 已知 recordId 的原始变更历史 |
| `aitable record share-url` | 已知记录的原始分享链接响应 |
| `aitable record create` / `update` / `batch-update` / `upsert` | Shortcut 未发布的底层写参数;写前须有真实 fieldId 和字段类型 |
| `aitable record delete` | 删除已唯一确认的记录,按最终 Runtime gate 执行 |
| `aitable record primary-doc-get` / `primary-doc-create` | 取得或创建记录主键文档;正文编辑转交 Doc |
`cells` 使用真实 fieldId;公式、查找引用、创建人和修改时间等只读字段不得写入。批量结果
必须检查 completed/failed/checkpoint,写入效果未知时先按稳定 ID 或业务唯一键回读,不盲目重放。
## View、Form 与可视化底层能力
| 原子命令 | 用途 |
|---|---|
| `aitable view list` / `get` | 原始视图目录或完整配置 |
| `aitable view create` / `duplicate` / `delete` / `lock` | Shortcut 未发布的视图生命周期与锁定字段 |
| `aitable view get aggregate` / `card` / `field-widths` / `fill-color-rule` / `filter` / `frozen-cols` / `group` / `lock` / `row-height` / `sort` / `timebar` / `visible-fields` | 读取单个视图配置面 |
| `aitable view update aggregate` / `card` / `field-widths` / `fill-color-rule` / `filter` / `frozen-cols` / `group` / `name` / `row-height` / `sort` / `timebar` / `visible-fields` | 更新一个已完整读取的视图配置面 |
| `aitable form list` / `get` / `create` / `update` / `delete` | 表单视图原子生命周期 |
| `aitable form field list` / `update` / `hide` | 表单字段顺序、展示和隐藏 |
| `aitable form questions create` / `delete` | 表单题目原子写入 |
| `aitable form share get` / `update` | 表单分享配置 |
| `aitable dashboard get` / `create` / `update` / `delete` / `arrange` | 仪表盘原始配置和布局 |
| `aitable dashboard share get` / `update` | 仪表盘分享配置 |
| `aitable chart get` / `create` / `update` / `delete` | 图表原始配置和生命周期 |
| `aitable chart share get` / `update` | 图表分享配置 |
视图更新是配置面写入,不是字段本体修改。调整可见列前读取完整有序 fieldId 数组并固定主字段;
创建或更新图表前使用 `aitable chart widgets-example` 获取当前合法配置,不猜 config。
## 导入导出、附件与自动化
| 原子命令 | 用途 |
|---|---|
| `aitable import upload` / `data` | 申请导入上传凭证并用真实 importId 发起导入 |
| `aitable export data` | 发起或恢复底层导出任务 |
| `aitable attachment upload` | 准备 AI 表格附件上传;不是 Drive 文件上传 |
| `aitable workflow list` / `get` / `edit-example` | 工作流目录、详情与当前 DSL 示例 |
| `aitable workflow create` / `update` | 校验完整 DSL 后创建或全量更新工作流 |
| `aitable workflow enable` / `disable` | 启停已唯一确认的工作流 |
上传、导入、导出和工作流可能异步完成;accepted/pending 不等于成功。保留 taskId/importId、轮询状态、
超时和真实 next command。非幂等创建在提交状态未知时只核对,不自动重试。
旧版 Runtime 缺少当前导出或分片建字段能力时,才分别使用
[aitable_export_via_task.py](../scripts/aitable_export_via_task.py) 或
[bulk_add_fields.py](../scripts/bulk_add_fields.py);当前 Runtime 已有对应 Shortcut 时不得绕回脚本。
## 权限与 Base 内节点
| 原子命令 | 用途 |
|---|---|
| `aitable advperm role-list` / `role-get` | 高级权限角色读取 |
| `aitable advperm enable` / `disable` | Base 高级权限总开关 |
| `aitable advperm role-create` / `role-update` / `role-delete` | 自定义角色原子管理 |
| `aitable section list-nodes` / `list-empty` | Base 导航树和空 Section 读取 |
| `aitable section create` / `rename` / `reorder` | Section 创建、重命名和排序 |
| `aitable section move-node` | 在 Base 内移动 Table、Dashboard 等 nsheet 节点 |
| `aitable section delete` | 删除已确认的 Section |
高级权限角色 ID、Section ID 与 Drive 节点 ID 不可互换。整个 Base 在普通文件夹中的外层移动或重命名
归 DriveBase 内 Table、Dashboard、Section 的结构操作仍归 AITable。
## 稳定 ID 传递
| 来源 | 只可用于 |
|---|---|
| `+url-resolve` / 唯一 Base 解析 | 当前 profile 下的 baseId,以及 URL 实际携带的 tableId/viewId/recordId |
| Base/Table/Field 读取 | 同一 Base 下后续命令的 tableId、fieldId |
| Record 查询或写入回执 | recordId、主键文档 nodeId、分享链接与历史查询 |
| View/Form/Dashboard/Chart 创建或读取 | 对应对象自己的稳定 ID,不以名称或列表序号替代 |
| 导入导出回执 | importId/taskId 及其 continuation;不能当 Base/Table ID |
| Workflow/AdvPerm/Section 读取 | workflowId、roleId、sectionId,仅限原资源与 profile |
## 故障处理
- `unknown command` / `unknown flag`:读取精确 leaf Help,最多修正一次;
- confirmation 或参数约束不清:读取精确 leaf Schema,以最终 Runtime gate 为准;
- 自然目标零命中、多候选或类型不明:停止并展示候选,不选择第一项;
- `partial_success`:保留已完成项、失败 ledger 和 checkpoint,只从真实 continuation 继续;
- commit unknown:按稳定 ID 或业务唯一键核对远端效果,未确认前不重放写入;
- 权限、认证或 profile:按 `dingtalk-shared` 对应 reference 分流;
- 本索引仍无法定位命令时,才用 `dws shortcut list --service aitable --format json` 做最终回退。
@@ -0,0 +1,251 @@
# advperm — 高级权限管理
控制 Base 的高级权限总开关,并管理自定义角色(增删改查 + 子角色权限规则)。
适用场景:"如何控制谁能看/改 Base 数据"、"开启/关闭高级权限"、"新建/修改/删除角色"、"按字段或行配置权限"。
## 命令一览
| 命令 | 用途 |
|------|------|
| `advperm enable` | 开启 Base 高级权限总开关 |
| `advperm disable` | 关闭 Base 高级权限总开关(高危) |
| `advperm role-list` | 列出 Base 下全部角色 |
| `advperm role-get` | 获取单角色完整配置 |
| `advperm role-create` | 创建自定义角色 |
| `advperm role-update` | 增量更新自定义角色(PATCH 语义) |
| `advperm role-delete` | 删除自定义角色(不可逆) |
> 所有子命令的 `--base-id` 必填,可用隐藏别名 `--base`。
## 命令详情
### advperm enable — 开启高级权限
```bash
dws aitable advperm enable --base-id BASE_ID --format json
```
返回 `{baseId, enabled: true}`
只有开启后角色配置才会真正限制成员的可访问范围;关闭状态下角色配置仍可读但不生效。
### advperm disable — 关闭高级权限(高危)
```bash
dws aitable advperm disable --base-id BASE_ID --yes --format json
```
返回 `{baseId, enabled: false}`。关闭后所有角色配置即刻失效,全员回退到默认权限。涉及多人协作或敏感数据务必和用户二次确认,建议先 `role-list` 留底。
### advperm role-list — 列出全部角色
```bash
dws aitable advperm role-list --base-id BASE_ID --format json
```
返回结构:
```json
{
"data": {
"enabled": true,
"defaultRole": { "mode": 0 },
"roles": [
{
"roleId": "10685308981",
"name": "可查看角色",
"roleType": "custom",
"system": false,
"subRoles": [
{
"authLevel": "read",
"targetId": "HMEaRQ4",
"targetType": "sheet",
"config": { "actions": 268435455 },
"display": {
"authLevelLabel": "仅查看",
"targetTypeLabel": "数据表",
"permissionScopeNote": "...",
"actionsLabels": ["新增视图", "删除视图", "修改视图"],
"actionsNote": "..."
}
}
]
}
]
}
}
```
关键字段:
- `roleType``custom`(自定义) / `system_editor` / `system_reader` / `5000`owner / `4000`manager)。
- `system`boolean,true 表示系统角色(不可删)。
- `subRoles[].display.*`:服务端返回的人类可读标签,可直接拼接给用户阅读,无需自行映射枚举。
- 不返回角色成员列表;如需"成员-角色"映射请去 AI 表格 Web 端。
- 新建 Base 默认 `enabled=false`,开启后只有 `owner` / `manager` 两个 meta 角色;`system_editor` / `system_reader` 需要在 Web UI 给成员授权"可编辑/可查看"后才会被服务端自动生成。
`role-list` / `role-get` 不需要管理员权限,普通成员也可读。
### advperm role-get — 获取单角色配置
```bash
dws aitable advperm role-get --base-id BASE_ID --role-id ROLE_ID --format json
```
返回结构同 `role-list` 中单个 role 对象(含完整 `subRoles[].config` 字段/行级规则与 `display.*` 标签)。
### advperm role-create — 创建自定义角色
```bash
# 仅指定 name,子角色由服务端按默认(none)填充
dws aitable advperm role-create --base-id BASE_ID --name "市场可读" --format json
# 创建时即指定 sub-roles(推荐——避免再走一次 role-update
dws aitable advperm role-create --base-id BASE_ID --name "市场可读" \
--sub-roles '[{"targetId":"<sheetId>","targetType":"sheet","authLevel":"read"}]' --format json
```
| flag | 必填 | 说明 |
|------|:---:|------|
| `--name` | ✅ | 角色名称 |
| `--role-type` | | 角色类型字符串(留空由服务端决定默认值,如 `custom` |
| `--flow-type` | | 流程类型字符串(按业务需要) |
| `--sub-roles` | | JSON 数组:`[{targetId, targetType, authLevel, appId?, config?}]`,详见下方"sub-roles 子字段"段 |
返回新建角色的完整配置(同 `role-get` 出参格式,含自动生成的 default subRoles)。
系统角色无法通过本命令创建。
### advperm role-update — 增量更新自定义角色(PATCH 语义)
```bash
# 只改名
dws aitable advperm role-update --base-id BASE_ID --role-id ROLE_ID --name "新名字"
# 只改 sheet 子角色 authLevelname 不传保持不变
dws aitable advperm role-update --base-id BASE_ID --role-id ROLE_ID \
--sub-roles '[{"targetId":"<sheetId>","targetType":"sheet","authLevel":"edit-own"}]'
```
| flag | 必填 | 说明 |
|------|:---:|------|
| `--role-id` | ✅ | 目标自定义角色 ID(数字 long 字符串) |
| `--name` | | 新角色名称;不传不修改 |
| `--role-type` / `--flow-type` | | 可选 |
| `--sub-roles` | | JSON 数组,**PATCH 合并语义**:按 `(targetId, targetType)` 合并到现有 subRoles,入参中的 sub 整体替换该 sub,**入参未提及的 sub 保留不变**(无需先调 `role-get` 自行 merge |
**系统角色禁止更新**(包括 owner / manager / system_editor / system_reader)。
### sub-roles 子字段
每个 sub-role 描述「角色对某个权限目标的访问粒度」:
| 字段 | 类型 | 说明 |
|------|------|------|
| `targetId` | string | 目标资源 ID(数据表 → `tableId`;仪表盘 → `dashboardId`;应用 → `appId` |
| `targetType` | string | `sheet` / `dashboard` / `app` |
| `authLevel` | string | `manage` / `edit-own` / `edit-custom-field` / `edit-field-range` / `read` / `none` |
| `appId` | string(可选) | 仅 `targetType=app` 时使用 |
| `config` | object(可选) | 字段/行级细化规则;含 `actions`(位图)/ `rows` / `cells`。结构与 `role-get` 出参 `subRoles[].config` 对齐 |
### advperm role-delete — 删除自定义角色(不可逆)
```bash
dws aitable advperm role-delete --base-id BASE_ID --role-id ROLE_ID --yes --format json
```
要求同时满足:
1. 该 Base 已开启高级权限(`role-list` 返回 `enabled=true`)。
2. 当前 dws 登录用户是该 Base 的管理员/Owner。
3. `--role-id``role-list` 返回的数字 long 字符串(如 `"10685308981"`),且对应角色 `system=false`
不可逆,删前先 `role-get` 留底。
## 能力边界
| 能力 | 状态 |
|------|------|
| 开/关高级权限 | ✅ 需管理员 |
| 列出 / 读取角色 | ✅ 普通成员也可读 |
| 创建自定义角色 | ✅ 需管理员 |
| 增量修改角色(PATCH 语义,不清空未传字段) | ✅ 需管理员 |
| 删除自定义角色 | ✅ 需管理员 |
| 修改/删除系统角色 | ❌ 服务端禁止;只能在 AI 表格 Web 端操作 |
| 角色 ↔ 成员绑定 | ❌ CLI 暂不支持,需在 AI 表格 Web 端 → Base 设置 → 高级权限 → 角色管理面板手动完成 |
## 错误码速查
| 场景 | code | type | message |
|------|------|------|---------|
| advperm 关闭时调用写接口(如 `role-delete` / `role-create` / `role-update` | `ADVANCED_PERMISSION_DISABLED` | `USER_ERROR` | `Advanced permission is disabled for base <BASE>, please enable it via setAdvancedPermission before managing roles` |
| 非管理员调用 `enable` / `disable` / `role-create` / `role-update` / `role-delete` | `401` | `AUTH_ERROR` | `the current user must be a manager (administrator) of this base to manage roles or advanced permission` |
| 删除/更新系统角色(`system=true` | `600` | `USER_ERROR` | `Illegal argument` |
| 操作不存在的数字 roleIdget/update/delete | `600` | `USER_ERROR` | `Illegal argument` |
| 传非数字 roleId(如 `owner` / `manager` | `INVALID_PARAMS` | `INPUT_ERROR` | `roleId is required` |
| `role-create``--name` | `INVALID_PARAMS` | `INPUT_ERROR` | `name is required` |
| `--sub-roles` JSON 不是数组 / 解析失败 | (CLI 层拦截) | — | `--sub-roles 解析失败 ...` / `--sub-roles 必须是 JSON 数组` |
| `--base-id` 无法解析 | `INVALID_BASE_ID` | `INPUT_ERROR` | `baseId cannot be resolved to docId` |
> `600 / Illegal argument` 同时覆盖"操作系统角色"和"操作不存在 roleId"两种情况。拿到 `600` 时先 `role-list` 自查目标 roleId 是否存在、是否 `system=true`,再据此引导用户。
## 典型工作流
### 排查"成员看不到某些字段/记录"
```bash
dws aitable advperm role-list --base-id BASE_ID --format json
# 若 enabled=false:高级权限未开,所有规则不生效,与用户确认是否需要 enable
dws aitable advperm enable --base-id BASE_ID --format json
dws aitable advperm role-list --base-id BASE_ID --format json
# 看 roles[] 里有哪些自定义角色
dws aitable advperm role-get --base-id BASE_ID --role-id ROLE_ID --format json
# 检查 subRoles[].config 中的字段/行级权限规则
```
### 新建一个"市场可读"角色
```bash
# 1. 确保高级权限已开
dws aitable advperm enable --base-id BASE_ID --format json
# 2. 拿目标 sheet 的 tableId
dws aitable table get --base-id BASE_ID --format json
# 3. 创建角色 + 指定 sheet 子角色 authLevel=read
dws aitable advperm role-create --base-id BASE_ID --name "市场可读" \
--sub-roles '[{"targetId":"<tableId>","targetType":"sheet","authLevel":"read"}]' \
--format json
# → 返回新角色完整配置,含 roleId,记下后续 patch / delete 使用
```
### 升级角色权限(read → edit-own),保留其他配置
```bash
# 只传 sub-rolesname 等其他字段保持不变(PATCH 语义)
dws aitable advperm role-update --base-id BASE_ID --role-id ROLE_ID \
--sub-roles '[{"targetId":"<tableId>","targetType":"sheet","authLevel":"edit-own"}]' \
--format json
```
### 改角色名(不影响权限规则)
```bash
dws aitable advperm role-update --base-id BASE_ID --role-id ROLE_ID --name "新名字"
```
### 清理废弃角色
```bash
dws aitable advperm role-list --base-id BASE_ID --format json
dws aitable advperm role-delete --base-id BASE_ID --role-id ROLE_ID --yes --format json
```
### 关闭高级权限(恢复全员可见)
```bash
dws aitable advperm role-list --base-id BASE_ID --format json > /tmp/roles-backup.json
dws aitable advperm disable --base-id BASE_ID --yes --format json
```
@@ -0,0 +1,49 @@
# attachment — 附件上传
> **STOP — 不要使用钉盘 (drive) 上传!** 钉盘 fileId 无法写入 attachment 字段。必须使用以下流程。
>
> **STOP — 严禁在 record create/update 的 cells 里直接传图片 URL** 直传 `{"url":"https://..."}` 会导致服务端同步下载图片,批量写入时触发 TIMEOUT_ERROR。正确做法:先 `attachment upload` 获取 `fileToken`,再用 `{"fileToken":"ft_xxx"}` 写入。
## 准备附件上传
```
Usage:
dws aitable attachment upload [flags]
Example:
dws aitable attachment upload --base-id <BASE_ID> --file-name report.xlsx --size 204800
dws aitable attachment upload --base-id <BASE_ID> --file-name photo.png --size 1024 --mime-type image/png
Flags:
--base-id string Base ID (必填)
--file-name string 文件名,必须含扩展名 (必填)
--size int 文件大小(字节),>0 (必填)
--mime-type string MIME type(不传时根据扩展名推断)
```
## 附件上传完整流程(推荐:使用脚本,2 步完成)
```bash
# 步骤 1: 使用脚本一键上传(内部自动完成 prepare + PUT
python3 scripts/upload_attachment.py <BASE_ID> /path/to/report.pdf
# 输出: { "fileToken": "ft_xxx", "fileName": "report.pdf", "size": 204800 }
# 步骤 2: 在 record create/update 中使用 fileToken 写入
dws aitable record create --base-id <BASE_ID> --table-id <TABLE_ID> \
--records '[{"cells":{"fldAttachId":[{"fileToken":"ft_xxx"}]}}]' --format json
```
> `uploadUrl` 有时效性(`expiresAt`),脚本会自动在获取后立即上传。
## 手动流程(不使用脚本)
```bash
# 1. 获取上传凭证
dws aitable attachment upload --base-id <BASE_ID> --file-name report.pdf --size 204800 --format json
# → 返回 uploadUrl、fileToken
# 2. PUT 上传(Content-Type 必须是文件的具体 MIME type)
curl -X PUT "<uploadUrl>" -H "Content-Type: application/pdf" --data-binary @report.pdf
# 3. 写入记录
dws aitable record update --base-id <BASE_ID> --table-id <TABLE_ID> \
--records '[{"recordId":"recXXX","cells":{"fldAttachId":[{"fileToken":"ft_xxx"}]}}]' --format json
```
@@ -0,0 +1,48 @@
# AI 表格最佳实践
## 1. 字段可写性分类
| 字段类型 | 可写 | 正确方式 |
|----------|------|----------|
| 文本/数字/日期/单选/多选/复选框/URL | ✅ | record create/update |
| 附件 | ⚠️ | 必须先走 [attachment upload 流程](./aitable-attachment.md) |
| 创建人/修改人/创建时间/修改时间 | ❌ | 系统字段,只读 |
| 公式/查找引用 | ❌ | 只读,由系统计算 |
| AI 字段 | ❌ | 只读,由 AI 自动计算 |
## 2. 查询执行契约
1. **不要拉全量后在 context 里手动统计** — 标量聚合用 `record stats`,分组/去重用 `record group-stats`
2. **has_more=true 时不能做全局结论** — 数据可能不完整
3. **优先用 `--filters` 在服务端过滤** — 不要拉全量后在本地 jq/grep
4. **fieldId 必须来自 `field get` 真实返回** — 不要猜测 fieldId
5. **减少响应体积** — 用 `--field-ids` 仅返回需要的字段
## 3. 任务选路
| 用户诉求 | 优先方案 | 不要误走 |
|---------|----------|----------|
| 查看几条数据 | `record query` | 不要用 `--all` |
| 全量拉取明细 | `record query --all` | 不要手动循环 cursor |
| 标量统计 | `record stats` | 不要先拉全量再本地计算 |
| 分组/去重统计 | `record group-stats` | 不要先拉全量再本地 groupby |
| 全量导出为文件 | `export data` | 不要 `--all` 拉全量再写文件 |
| 批量写入 | `record create`(分批 100 条) | 不要一次传超过 100 条 |
| 附件/图片上传 | `attachment upload` 获取 fileToken → `record create/update` 用 fileToken 写入 | **严禁直接传图片 URL 到附件字段**(服务端同步下载会超时) |
| 文件级导入 | `import upload` + `import data` | 不要手动解析 xlsx 再逐条写入 |
## 4. 创建/修改后回读确认
执行写操作后,建议立即回读确认结果:
| 写操作 | 建议回读命令 | 确认内容 |
|--------|-------------|----------|
| `table create` | `table get --table-ids <新tableId>` | 表名、字段列表是否符合预期 |
| `field create` | `field get --table-id <tableId>` | 新字段是否出现在字段列表中 |
| `record create/update` | `record query --record-ids <新recordId>` | 写入值是否正确 |
## 5. AI 字段注意事项
- AI 字段的 prompt **必须至少包含一个 `fieldRef` 引用**,纯文本 prompt 会被后端拒绝
- 先创建/确认被引用字段的 fieldId,再在 prompt 中引用
- `outputType` 必须与字段类型一致(如 `outputType=text``--type text`
@@ -0,0 +1,346 @@
# cells 写入/读取格式规范(cellValue 数据结构)
> 适用命令:`dws aitable record create --records`、`dws aitable record update --records`、`dws aitable record query` 返回
>
> 本文件是 DWS AI 表格 cellValue 的 **source of truth**。写入记录时,必须严格按此格式构造 cells 对象。
## 顶层规则
- cells 的 key **必须是 fieldId**(如 `fldXXX`),不是字段名称
- fieldId 必须从 `field get` 返回中获取
- 不同字段类型的 value 格式不同,混用会报错
- 系统只读字段(creator/lastModifier/createdTime/lastModifiedTime/formula)不可写入
## 各字段类型详解
### text(文本)
**写入**:字符串
```json
{"fldTextId": "这是一段文本"}
```
**读取**:字符串
```json
{"fldTextId": "这是一段文本"}
```
---
### number(数字)
**写入**:数字或数字字符串
```json
{"fldNumId": 123.45}
{"fldNumId": "123.45"}
```
**读取**:字符串形式的数字
```json
{"fldNumId": "123.45"}
```
---
### singleSelect(单选)
**写入**:选项名称字符串(推荐),或对象形式 `{id, name}`
```json
{"fldSelectId": "进行中"}
{"fldSelectId": {"id": "opt_xxx", "name": "进行中"}}
```
> 写入不存在的选项名称时,系统会自动创建该选项。
> 对象写入时 id 为准,服务端会校验 id 是否存在。
**读取**:对象 `{id, name}`
```json
{"fldSelectId": {"id": "opt_abc123", "name": "进行中"}}
```
---
### multipleSelect(多选)
**写入**:选项名称数组(推荐),或对象数组
```json
{"fldMultiId": ["标签A", "标签B"]}
{"fldMultiId": [{"id": "opt_a", "name": "标签A"}, {"id": "opt_b", "name": "标签B"}]}
```
> 写入时每项需带 id(对象模式)或直接传 name 字符串。不存在的 name 会自动补入选项配置。
**读取**:对象数组
```json
{"fldMultiId": [{"id": "opt_a", "name": "标签A"}, {"id": "opt_b", "name": "标签B"}]}
```
---
### date(日期)
**写入**:日期字符串、RFC3339 字符串、或毫秒时间戳
```json
{"fldDateId": "2026-03-15"}
{"fldDateId": "2026-03-15 09:00"}
{"fldDateId": "2026-03-15T09:00+08:00"}
```
**读取**RFC3339 字符串(带时区)
```json
{"fldDateId": "2026-03-15T09:00:00+08:00"}
```
**过滤**`record query --filters`):日期字段**只能用日期专用操作符** `date_eq` / `before` / `after` / `not_before` / `not_after` / `exist` / `un_exist`,比较值用日期字符串(如 `"2026-03-15"`)。
- ❌ 通用 `eq` / `ne` / `gt` / `gte` / `lt` / `lte` / `contain` 对日期字段无效,会静默返回 0 条;
- ❌ 不支持区间 `date_between` 与相对 `from_now`CLI 会直接拒绝),范围查询用 `not_before` + `not_after` 组合。
- 详见 [aitable-filter-sort.md](./aitable-filter-sort.md) §日期字段过滤。
---
### currency(货币)
**写入**:数字(与 number 相同)
```json
{"fldCurrencyId": 99.5}
```
**读取**:字符串形式的数字(小数位数取决于 formatter 配置)
```json
{"fldCurrencyId": "99.5"}
```
---
### progress(进度)
**写入**:0~1 之间的浮点数(0 表示 0%,1 表示 100%)
```json
{"fldProgressId": 0.75}
```
> ⚠️ **常见错误**:写入 75 不会报错,但会被存储为 7500%(因为系统将其理解为 75 倍)。
> 正确做法:75% 应写入 0.75。API 不会拒绝超出 [0,1] 的值,但显示会异常。
> 如果字段配置了 `customizeRange`,则按自定义范围传值。
**读取**:字符串形式的数字
```json
{"fldProgressId": "0.75"}
```
---
### rating(评分)
**写入**:整数,必须在字段配置的 min~max 范围内
```json
{"fldRatingId": 4}
```
> ⚠️ 超出 max 范围的值(如 max=5 时写入 6)会被服务端拒绝并返回错误。
**读取**:数字(字符串形式)
```json
{"fldRatingId": "4"}
```
---
### checkbox(勾选)
**写入**:布尔值
```json
{"fldCheckId": true}
{"fldCheckId": false}
```
**读取**:布尔值
```json
{"fldCheckId": true}
```
---
### user(人员)
**写入**:对象数组,每项必须含 `userId``corpId`
```json
{"fldUserId": [{"userId": "staff_001", "corpId": "dingxxxxxxxx"}]}
```
> 单选字段(`multiple=false`)也必须传数组,只是数组长度为 1。
> 如果目标用户不在当前请求组织内,回退为 `[{"userRef": "ur_0AaZ19"}]`。
**读取**:对象数组
```json
{"fldUserId": [{"userId": "staff_001", "corpId": "dingxxxxxxxx"}]}
```
---
### department(部门)
**写入**:对象数组,每项含 `deptId`
```json
{"fldDeptId": [{"deptId": "52528700"}]}
```
**读取**:对象数组
```json
{"fldDeptId": [{"deptId": "52528700"}]}
```
---
### group(群组)
**写入**:对象数组,每项含 `cid`
```json
{"fldGroupId": [{"cid": "74577067501"}]}
```
> ⚠️ key 是 **`cid`**,不是 `openConversationId`
**读取**:对象数组
```json
{"fldGroupId": [{"cid": "74577067501"}]}
```
---
### url(链接)
**写入**:对象 `{text, link}` 或纯 URL 字符串
```json
{"fldUrlId": {"text": "钉钉官网", "link": "https://dingtalk.com"}}
{"fldUrlId": "https://dingtalk.com"}
```
> 纯字符串写入时,服务端自动补齐为 `{"text":"原字符串","link":"原字符串"}`
**读取**:对象 `{text, link}`
```json
{"fldUrlId": {"text": "钉钉官网", "link": "https://dingtalk.com"}}
```
---
### richText(富文本)
**写入**:对象 `{markdown: "..."}`
```json
{"fldRichId": {"markdown": "**加粗**\n普通文字\n"}}
```
**读取**:对象 `{markdown: "..."}`(有损,颜色/@人等信息可能丢失
```json
{"fldRichId": {"markdown": "**加粗**\n普通文字\n"}}
```
---
### attachment(附件)
**写入**:对象数组,**必须使用 `fileToken`**
```json
{"fldAttachId": [{"fileToken": "ft_xxx"}]}
```
> ⚠️ **必须先通过 [attachment upload 流程](./aitable-attachment.md) 上传文件获取 `fileToken`,再将 `fileToken` 写入 cells。**
> ❌ **严禁直接传 `{"url": "https://..."}` 形式写入附件/图片字段** — 服务端会同步下载图片,10 条记录即触发 TIMEOUT_ERROR 超时。
> 写入会**整体覆盖**原附件列表,不是追加。
**读取**:对象数组(含下载链接、文件名、大小)
```json
{"fldAttachId": [{"url": "https://...", "filename": "report.pdf", "size": 204800}]}
```
---
### telephone / email / barcode / idCard(电话/邮箱/条码/身份证)
**写入**:字符串
```json
{"fldPhoneId": "13800138000"}
{"fldEmailId": "test@example.com"}
{"fldBarcodeId": "978-3-16-148410-0"}
{"fldIdCardId": "520402196001067498"}
```
> idCard 必须是后端认可的合法身份证号格式
**读取**:字符串
```json
{"fldPhoneId": "13800138000"}
```
---
### geolocation(地理位置)
**写入**:对象,包含 `address``name``location`
```json
{
"fldGeoId": {
"address": "浙江省杭州市思凯路与爱橙街交叉口东南200米",
"name": "阿里中心·未科D1幢",
"location": ["120.007852", "30.271194"]
}
}
```
> `location` 按 **[经度, 纬度]** 传**字符串数组**
**读取**:对象(含额外的 `fullAddress` 字段,由服务端自动拼接)
```json
{
"fldGeoId": {
"address": "浙江省杭州市",
"fullAddress": "阿里中心-浙江省杭州市",
"name": "阿里中心",
"location": ["120.007852", "30.271194"]
}
}
```
---
### unidirectionalLink / bidirectionalLink(关联字段)
**写入**:对象 `{linkedRecordIds: [...]}`
```json
{"fldLinkId": {"linkedRecordIds": ["recXXX", "recYYY"]}}
```
**读取**:对象 `{linkedRecordIds: [...]}`
```json
{"fldLinkId": {"linkedRecordIds": ["recXXX", "recYYY"]}}
```
---
### 只读字段(禁止写入)
以下字段类型由系统自动填充,`record create/update` 时**禁止传入**
| 类型 | 说明 |
|------|------|
| `creator` | 创建人 |
| `lastModifier` | 最后编辑人 |
| `createdTime` | 创建时间 |
| `lastModifiedTime` | 最后编辑时间 |
| `formula` | 公式字段(系统计算) |
| AI 字段 | 由 AI 自动计算 |
## 常见错误速查
| 错误 | 正确做法 |
|------|----------|
| cells key 用字段名称 `"课程名称"` | 用 fieldId `"fldXXX"` |
| progress 写入 `75` | 写入 `0.75`(范围 0~1 |
| attachment 直接传文件路径或图片 URL | 必须先 `attachment upload` 获取 fileToken,再用 fileToken 写入(直传 URL 会超时) |
| user 字段传用户名字符串 | 传对象数组 `[{"userId":"...", "corpId":"..."}]` |
| group 字段用 `openConversationId` | 用 `cid` |
| singleSelect 传 option id 字符串 | 传 name 字符串或 `{"id":"...", "name":"..."}` 对象 |
| 对只读字段写入值 | 不传该字段,由系统自动填充 |
@@ -0,0 +1,55 @@
# dashboard & chart — 仪表盘与图表
## 建议操作顺序
```bash
# 1) 只在缺少配置结构时读取模板
dws aitable dashboard config-example --format json
dws aitable +chart-widgets-example --format json
# 2) 先拿 dashboard,再拿 chart 详情
dws aitable dashboard get --base-id <BASE_ID> --dashboard-id <DASHBOARD_ID> --format json
dws aitable chart get --base-id <BASE_ID> --dashboard-id <DASHBOARD_ID> --chart-id <CHART_ID> --format json
```
只按名称创建、改名并确认时,不需要读取配置示例或 Help:
```bash
dws aitable dashboard create --base-id <BASE_ID> --name <名称> --format json
dws aitable +dashboard-update --base-id <BASE_ID> --dashboard-id <DASHBOARD_ID> --name <新名称> --format json
dws aitable +dashboard-get --base-id <BASE_ID> --dashboard-id <DASHBOARD_ID> --format json
```
部分服务端更新回执可能仍回显更新前名称;只做一次 `+dashboard-get` 读回,以该后续权威读回为最终状态。读回已是目标名称时判定更新完成,不重放写操作,也不因旧回执继续探测 Help。
## 要点
- `dashboard get` 返回的 `charts[].chartId` 可直接给 `chart get` 使用
- 删除 dashboard 会级联删除其全部 chart;确认前必须说明该影响
- `dashboard share get` 可能返回 `404`(资源不存在或未开通),需按可重试错误处理,不要误判为参数拼错
- `chart share get` 可正常返回 `enabled/shareUrl`,用于分享状态判断
## dashboard 子命令
| 命令 | 用途 | 必填参数 | 说明 |
|------|------|----------|------|
| `dashboard get` | 获取仪表盘详情(含 charts 列表) | `--base-id` `--dashboard-id` | — |
| `dashboard create` | 创建仪表盘 | `--base-id` + (`--config``--name`) | `--name` 简化版创建空看板;`--config` 传完整 JSON |
| `dashboard update` | 更新仪表盘 | `--base-id` `--dashboard-id` + (`--config``--name`) | `--name` 仅改名;`--config` 更新完整配置 |
| `dashboard delete` | 删除仪表盘 | `--base-id` `--dashboard-id` | 级联删除全部 chart,不可逆;由 Runtime 请求确认,Reference 不携带确认绕过参数 |
| `dashboard config-example` | 查看仪表盘配置模板 | 无 | 创建前先调此命令了解 config 结构 |
| `dashboard arrange` | 自动重排图表布局 | `--base-id` `--dashboard-id` | 把图表按行铺满网格,避免某行只占半幅、留下大片空白;返回 `{totalColumns, layout, alignedChartCount}` |
## chart 子命令
| 命令 | 用途 | 必填参数 |
|------|------|----------|
| `chart get` | 获取图表详情 | `--base-id` `--dashboard-id` `--chart-id` |
| `chart create` | 创建图表 | `--base-id` `--dashboard-id` `--config` `--layout` |
| `chart update` | 更新图表配置 | `--base-id` `--dashboard-id` `--chart-id` `--config` |
| `chart delete` | 删除图表 | `--base-id` `--dashboard-id` `--chart-id` | 不可逆;由 Runtime 请求确认,Reference 不携带确认绕过参数 |
| `+chart-widgets-example` | 查看所有图表类型的 widgets 模板 | 无 |
## 配置获取流程
已有符合当前 leaf Schema 的合法 config 时直接创建/更新,不读取模板。只有缺少结构时调用一次 `+chart-widgets-example`;该命令当前返回所有图表类型示例,随后只使用目标类型,并按真实 tableId/fieldId 填充后执行。
@@ -0,0 +1,131 @@
# AI 表格数据分析 SOP
> 当用户诉求涉及查询、筛选、排序、统计、Top/Bottom N、分组聚合、判断全局结论时,必须先读本文档再执行。
## 1. 查询决策树
```
用户要做什么?
├─ 查看/导出原始记录明细
│ → record query [--filters] [--sort] [--field-ids] [--limit]
├─ 按条件筛选记录(如"状态=进行中的记录"
│ → record query --filters '{"operator":"and","operands":[...]}'
├─ 取 Top N / Bottom N(如"销售额最高的5条"
│ → record query --sort '[{"fieldId":"xxx","direction":"desc"}]' --limit 5
├─ 全量统计(如"一共多少条"、"所有记录的总销售额")
│ → record stats --stats '[{"fieldId":"...","statsType":"COUNT|SUM|AVG|..."}]'
├─ 分组统计(如"每个状态各有多少条")
│ → record group-stats --group '[...]' --stats '[{"fieldId":"...","statsType":"count"}]'
├─ 条件唯一实体数(如"有索赔单的门店共有多少家")
│ → record group-stats --filters '{...}' --stats '[{"fieldId":"<实体字段>","statsType":"distinct"}]'
└─ 判断全局结论(如"是否所有记录都满足条件")
→ record stats 对反向过滤结果 COUNT;只有需要行级证据时再 record query
```
## 2. 核心规则
### 2.1 禁止基于默认分页下全局结论
`record query` 默认返回 100 条。如果返回 JSON 中 `data.nextCursor` 非空,表示还有后续数据,当前结果**不是全量**。
```json
{"data": {"nextCursor": "3hf5MtLbLZ", "records": [...]}}
```
- ❌ 错误:只查了默认 100 条就说"共 100 条记录"
- ✅ 正确:使用 `--all` 自动翻页拿全量,再统计
### 2.2 能在服务端过滤的,不要拉到本地再过滤
| 需求 | 正确做法 | 错误做法 |
|------|---------|---------|
| 筛选"状态=已完成" | `--filters '{"operator":"and","operands":[{"operator":"eq","operands":["fldXXX","已完成"]}]}'` | `--all` 拉全量再本地 filter |
| 按日期降序取最新5条 | `--sort '[...]' --limit 5` | `--all` 拉全量再本地 sort + slice |
| 模糊搜索标题含"Q1" | `--filters``contain` 操作符 | 全量拉取再本地 grep |
### 2.3 聚合必须优先在服务端完成
以下场景均有服务端聚合入口,不得先用 `record query --all` 下载全表计算:
- `record stats`:不分组的 COUNT / SUM / AVG / MAX / MIN / MEDIAN / DISTINCT / 完整率等
- `record group-stats`:分组统计,以及不带 group 的条件 DISTINCT 唯一计数
- 任意条件比率:分别聚合分子与分母,再对齐范围和 `dataVersion` 计算
只有用户要求记录明细、少量样本校验、精确分位数输入,或聚合接口明确返回不支持/错误时,才允许使用 `record query`。需要先逐行运算再聚合的指标(例如两个日期相减)必须使用表内已有且可聚合的公式字段;没有该字段时停止并请用户先在 AI 表格页面创建,不能下载全表本地二次计算。
### 2.4 --all 使用注意
```bash
dws aitable record query \
--base-id <baseId> \
--table-id <tableId> \
--all \
--field-ids <只取需要的字段> \
--format json
```
- **必须配合 `--field-ids`** 限制返回字段,减少数据量
- 对于大表(>1000条),先告知用户可能耗时
- `--all` 会自动处理分页,无需手动翻页
## 3. filters 快速参考
详细语法见 [aitable-filter-sort.md](./aitable-filter-sort.md)。
### 常用操作符速查
| 操作符 | 适用类型 | 含义 | 示例 operands |
|--------|---------|------|-------------|
| `eq` | 通用 | 等于 | `["fldXXX", "值"]` |
| `ne` | 通用 | 不等于 | `["fldXXX", "值"]` |
| `gt` / `lt` | 数值/日期 | 大于/小于 | `["fldXXX", "25"]` |
| `gte` / `lte` | 数值/日期 | 大于等于/小于等于 | `["fldXXX", "100"]` |
| `contain` | 文本 | 包含 | `["fldXXX", "关键词"]` |
| `exist` / `un_exist` | 通用 | 有值/为空 | `["fldXXX"]`(无第二参数) |
| `any_of` | 多选 | 包含任一 | `["fldXXX", "选项A"]` |
### filters 结构模板
```json
{
"operator": "and",
"operands": [
{"operator": "eq", "operands": ["<fieldId>", "<值>"]},
{"operator": "gt", "operands": ["<fieldId>", "<数值>"]}
]
}
```
## 4. 分析结果呈现规范
### 4.1 必须包含的信息
- **数据范围**:基于哪个表、哪些筛选条件、查询了多少条记录
- **计算方法**:用了什么聚合方式(sum/count/avg 等)
- **结果值**:精确到合理小数位
### 4.2 示例
> 基于「销售数据」表,筛选条件:日期 ≥ 2026-01-01,共查询到 342 条记录。
> - 总销售额:¥1,234,567.89SUM
> - 平均单价:¥3,610.46AVG
> - 最大单笔:¥89,000.00MAX
## 5. 任务选路心智模型
| 用户诉求 | 优先方案 | 不要误走 |
|---------|---------|---------|
| 一次性标量统计 | `record stats` | 不要 `record query --all` 后本地聚合 |
| 分组/去重统计 | `record group-stats` | 不要下载全表 groupby / 去重 |
| 长期展示派生指标 | 创建 formula 字段(见 [formula-guide](./aitable-formula-guide.md) | 不要每次手算再手动写入 |
| 按条件筛选记录 | `record query --filters` | 不要 `--all` 拉全量再本地 filter |
| 取最新/最大/前N | `--sort + --limit` | 不要 `--all` 再本地排序取前N |
| 关键词检索 | `record query --filters``contain` | 不要把表格当搜索引擎全文检索 |
| 验证"是否全部满足" | 反向 filters(筛不满足的),看是否有结果 | 不要 `--all` 逐条遍历 |
@@ -0,0 +1,272 @@
# datasource — 数据源同步管理
将外部数据源(当前仅支持 OA 审批)同步到 AI 表格。完整链路:list-sources → (OA) 选择模板 → get-fields → create → sync-status → sync / update / get-config。
## 命令一览
| 命令 | 用途 | 读/写 |
|------|------|-------|
| `+datasource-list-sources` | 列出可用数据源条目,获取 processCode/name/iconUrl/url | 读 |
| `+datasource-get-fields` | 获取可同步字段列表,用于决定 field-ids | 读 |
| `+datasource-create` | 创建数据源表并触发首次全量同步 | 写 |
| `+datasource-update` | 更新已有数据源表的同步配置 | 写 |
| `+datasource-sync` | 手动触发一次同步(最多 5 张表) | 写 |
| `+datasource-sync-status` | 查询同步任务状态(RUNNING/FINISHED/FAILED | 读 |
| `+datasource-get-config` | 获取数据源表当前同步配置 | 读 |
## 典型工作流
```
Step 0 列出可用来源 +datasource-list-sources --base-id <B> --datasource-type OA
→ 返回 approvals 数组(通常含多个审批模板)
Step 0.5 (OA 审批) 选择模板 list-sources 返回的 approvals 数组通常含多个审批模板,需确定目标:
- 用户已指定名称(如"采购申请")→ 按 name 精确/模糊匹配
· 唯一命中 → 提取该条的 processCode/name/iconUrl/url,继续
· 多候选 → 列出匹配项让用户消歧
· 零命中 → 停止,提示用户确认名称或从完整列表选择
- 用户未指定 → 列出候选模板清单(name + processCode),等用户选择
- 禁止:未匹配直接选第一项、或凭记忆猜 processCode
注:此步骤针对 OA 审批数据源(多模板场景);其他数据源类型的
list-sources 返回结构可能不同,按实际 result 解析即可,
不一定需要选择步骤。
Step 1 (可选) 获取可同步字段 +datasource-get-fields --base-id <B> --datasource-type OA --source-config '<JSON>'
→ 决定需要同步哪些字段,得到 field-ids
Step 2 创建数据源 +datasource-create --base-id <B> --datasource-type OA --source-config '<JSON>'
→ sourceConfig 中的 processCode/name/iconUrl/url 来自 Step 0.5 选中的模板
→ 返回 tableId + taskId
Step 3 查询同步结果 +datasource-sync-status --base-id <B> --table-id <T> --task-ids <TASK_ID>
→ FINISHED=完成,FAILED=看 errorCode 排查,RUNNING=轮询
Step 4 (后续) 手动触发同步 +datasource-sync --base-id <B> --table-ids <T1>,<T2>
→ 返回新 taskId,再用 sync-status 查结果
Step 5 (后续) 更新配置 +datasource-update --base-id <B> --table-id <T> --source-config '<JSON>' --auto
→ 更新后自动触发一次同步
查看当前配置 +datasource-get-config --base-id <B> --table-id <T>
```
## sourceConfig 字段协议
以下字段协议仅适用于 OA 审批数据源(datasourceType=OA),其他数据源类型待后续开放。
### 须从 list-sources 原样透传(必填)
| 字段 | 类型 | 说明 |
|------|------|------|
| processCode | String | OA 审批流程编码 |
| name | String | 展示名称 |
| iconUrl | String | 图标 URL |
| url | String | 跳转链接 |
### 调用方自行设置
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| dataType | String | 是 | 数据范围类型:`time_range` / `start_time` / `recent_time` |
| recentDays | String | 条件 | dataType=recent_time 时有效,取值 `7d`/`30d`/`1y`,默认 `30d` |
| startDate | String | 条件 | dataType=time_range 或 start_time 时有效,`yyyy-MM-dd`,默认 30 天前 |
| endDate | String | 条件 | dataType=time_range 时有效,`yyyy-MM-dd`,默认当天 |
| keepRemovedFields | Boolean | 否 | 是否保留已删除字段,默认 false |
| splitParentTableField | Boolean | 否 | 是否拆分父表字段 |
> list-sources 返回的 keepRemovedFields / splitParentTableField / enableDataSyncOaDetailList 不要透传,由调用方按需设置。
### 三种 dataType 最小示例
```json
// recent_time — 最近一段时间
{"processCode":"PROC-xxxx","name":"采购申请","dataType":"recent_time","recentDays":"30d","iconUrl":"...","url":"..."}
// time_range — 指定起止日期
{"processCode":"PROC-xxxx","name":"采购申请","dataType":"time_range","startDate":"2025-01-01","endDate":"2025-12-31","iconUrl":"...","url":"..."}
// start_time — 从某日期至今
{"processCode":"PROC-xxxx","name":"采购申请","dataType":"start_time","startDate":"2025-06-01","iconUrl":"...","url":"..."}
```
## autoSyncSetting 频率配置
仅在 `--auto=true` 时生效。不传时使用下游默认自动同步策略。
| 字段 | 必填 | 说明 |
|------|------|------|
| syncType | 是 | `hourly`(按小时间隔)/ `scheduled`(定时触发) |
| hourlyInterval | hourly 时 | 正整数,小时间隔 |
| scheduleType | scheduled 时 | `daily` / `weekly` / `monthly` |
| timeValue | scheduled 时 | `HH:mm` 触发时间 |
| selectedMonthDays | monthly 时必填 | 每月几号触发,1-31 |
| selectedWeekdays | weekly 时必填 | 每周哪几天触发,1=周一…7=周日 |
| skipNonWorkingDay | 否 | 是否跳过非工作日,默认 false |
示例:`{"syncType":"scheduled","scheduleType":"daily","timeValue":"09:00"}`
## 命令详情
### +datasource-list-sources — 列出可用数据源条目
```bash
dws aitable +datasource-list-sources --base-id BASE_ID --datasource-type OA --format json
```
| flag | 必填 | 说明 |
|------|------|------|
| `--base-id` | 是 | 目标 Base ID |
| `--datasource-type` | 是 | 数据源类型,当前仅支持 `OA` |
返回每条条目包含 `result`(下游原始 JSON 字符串)和 `sourceType`(OA 审批对应 2)。OA 审批场景下 result 为 approvals 数组:
```json
{
"approvals": [
{
"processCode": "PROC-xxxx",
"name": "采购申请",
"iconUrl": "https://...",
"url": "https://...",
"keepRemovedFields": false,
"splitParentTableField": false,
"enableDataSyncOaDetailList": false
}
]
}
```
调用方应自行解析 result,提取目标模板字段后构造 sourceConfig。
### +datasource-get-fields — 获取可同步字段列表
```bash
dws aitable +datasource-get-fields --base-id BASE_ID --datasource-type OA \
--source-config '{"processCode":"PROC-xxxx","name":"采购申请","dataType":"recent_time","recentDays":"30d","iconUrl":"...","url":"..."}' \
--format json
```
| flag | 必填 | 说明 |
|------|------|------|
| `--base-id` | 是 | 目标 Base ID |
| `--datasource-type` | 是 | 数据源类型,当前仅支持 `OA` |
| `--source-config` | 是 | 源配置 JSON 字符串,结构同 create 的 --source-config |
返回字段列表(字段 ID、名称、类型等),用于在 create/update 中指定 `--field-ids`
### +datasource-create — 创建数据源表
```bash
dws aitable +datasource-create --base-id BASE_ID --datasource-type OA \
--source-config '{"processCode":"PROC-xxxx","name":"采购申请","dataType":"recent_time","recentDays":"30d","iconUrl":"...","url":"..."}' \
--format json
# 开启自动同步 + 自定义频率
dws aitable +datasource-create --base-id BASE_ID --datasource-type OA \
--source-config '...' --auto \
--auto-sync-setting '{"syncType":"scheduled","scheduleType":"daily","timeValue":"09:00"}' \
--format json
```
| flag | 必填 | 说明 |
|------|------|------|
| `--base-id` | 是 | 目标 Base ID |
| `--datasource-type` | 是 | 数据源类型,当前仅支持 `OA` |
| `--source-config` | 是 | 源配置 JSON 字符串(见上方字段协议) |
| `--auto` | 否 | 是否开启自动同步,默认 false;无论是否传入,CLI 都会把该字段下发给下游 |
| `--auto-sync-setting` | 否 | 自动同步频率配置 JSON 字符串,仅 --auto=true 时生效 |
| `--field-ids` | 否 | 需要同步的字段 ID 列表,不传时同步全部字段 |
返回新建数据源表 tableId 和同步任务 taskId。创建后自动触发一次全量同步,需用 `+datasource-sync-status` 查最终结果。
### +datasource-update — 更新数据源配置
```bash
# 仅开启自动同步
dws aitable +datasource-update --base-id BASE_ID --table-id TABLE_ID --auto --format json
# 更新源配置
dws aitable +datasource-update --base-id BASE_ID --table-id TABLE_ID \
--source-config '{"processCode":"PROC-yyyy","name":"出差申请","dataType":"recent_time","recentDays":"30d","iconUrl":"...","url":"..."}' \
--format json
```
| flag | 必填 | 说明 |
|------|------|------|
| `--base-id` | 是 | 目标 Base ID |
| `--table-id` | 是 | 已有数据源表 IDsync=true |
| `--source-config` | 否 | 新的源配置 JSON 字符串,不传时保持原配置;传入时整体覆盖 |
| `--auto` | 否 | 是否开启自动同步,不传时保持原设置 |
| `--auto-sync-setting` | 否 | 自动同步频率配置 JSON 字符串,仅 --auto=true 时生效;不传时保持原频率配置 |
| `--field-ids` | 否 | 需要同步的字段 ID 列表,不传时保持现有字段配置 |
更新后自动触发一次全量同步,返回新 taskId。
### +datasource-sync — 手动触发同步
```bash
dws aitable +datasource-sync --base-id BASE_ID --table-ids TBL1,TBL2 --format json
```
| flag | 必填 | 说明 |
|------|------|------|
| `--base-id` | 是 | 目标 Base ID |
| `--table-ids` | 是 | 待同步的数据源表 ID 列表(sync=true),1-5 个 |
返回结果包含文档链接,可打开查看同步进度。每张表独立提交,部分失败不影响其他表。
### +datasource-sync-status — 按任务 ID 查询同步状态
```bash
dws aitable +datasource-sync-status --base-id BASE_ID --table-id TABLE_ID --task-ids TASK1,TASK2 --format json
```
| flag | 必填 | 说明 |
|------|------|------|
| `--base-id` | 是 | 目标 Base ID |
| `--table-id` | 是 | 数据源表 IDsync=true |
| `--task-ids` | 是 | 同步任务 ID 列表(由 create/update/sync 返回),1-5 个 |
任务状态:`RUNNING`(进行中)、`FINISHED`(完成)、`FAILED`(失败,含 errorCode + errorMessage)。
### +datasource-get-config — 获取数据源配置
```bash
dws aitable +datasource-get-config --base-id BASE_ID --table-id TABLE_ID --format json
```
| flag | 必填 | 说明 |
|------|------|------|
| `--base-id` | 是 | 目标 Base ID |
| `--table-id` | 是 | 数据源表 IDsync=true |
返回当前同步配置详情(sourceConfig、是否自动同步、同步状态等)。仅适用于数据源表,普通表会报错。
## 错误码与排查
| 场景 | 表现 | 排查 |
|------|------|------|
| 同步运行中重复触发 | errorCode=4014status=FAILED | 幂等冲突,稍后重试即可 |
| 非数据源表触发 sync | 参数错误返回 | 确认 table 的 sync=true,用 `+base-get` / `+table-list` 检查 |
| sourceConfig 缺必填字段 | 创建/更新失败 | 检查 processCode/name/iconUrl/url 是否从 list-sources 原样透传 |
| dataType 与时间字段不匹配 | 创建失败 | recent_time 需 recentDaystime_range 需 startDate+endDatestart_time 需 startDate |
## 能力边界
| 能力 | 状态 |
|------|------|
| OA 审批数据源 | 已支持 |
| 其他数据源类型 | 待后续开放 |
| 全量同步 | 已支持 |
| 增量同步 | 待后续开放 |
| 自动同步 | 已支持(--auto + autoSyncSetting |
| 删除数据源表 | 走普通表删除,不走 datasource 命令 |
## 注意事项
- sourceConfig 是 **JSON 字符串**(不是 JSON 对象),CLI flag 传入时需要用单引号包裹
- list-sources 返回的 keepRemovedFields / splitParentTableField / enableDataSyncOaDetailList 不要透传,由调用方按需设置
- create/update 后自动触发一次同步,返回 taskId;用 sync-status 查最终结果
- sync 单次最多 5 张表,超出拆分多次调用
- sync-status 单次最多 5 个 taskId,超出拆分多次调用
- get-config 仅适用于数据源表(sync=true),普通表会报错
@@ -0,0 +1,128 @@
# AI 表格错误恢复指南
> 当 CLI 命令返回错误时,按本文档的映射表判断恢复动作。
## 1. 错误响应结构
```json
{
"status": "error",
"summary": "Failed to create records",
"trace_id": "2104a64c17790723347215232e085e"
}
```
- `status: "error"` 表示操作失败
- `summary` 包含错误摘要信息
- `trace_id` 用于问题追踪
## 2. 常见错误与恢复动作
### 2.1 记录操作错误
| 错误现象 / summary | 原因 | 恢复动作 |
|-------------------|------|---------|
| `Failed to create records` | cellValue 格式错误或字段类型不匹配 | 先 `field get` 确认字段类型,再按 [cell-value](./aitable-cell-value.md) 规范重构值 |
| `record not found` | record-id 不存在或已删除 | 用 `record query` 重新查询确认目标记录 |
| rating 字段写入超出 max | 值超出字段配置范围 | 检查字段 config 的 min/max,确保值在范围内 |
| singleSelect 写入对象格式但 id 不存在 | option id 无效 | 改用 name 字符串写入(推荐),或先 `field get` 获取有效 option id |
### 2.2 字段操作错误
| 错误现象 / summary | 原因 | 恢复动作 |
|-------------------|------|---------|
| `Failed to create field` | config 格式错误或必填项缺失 | 检查 [field-properties](./aitable-field-properties.md) 中该类型的必填 config |
| `field not found` | field-id 不存在 | 用 `field get` 获取最新字段列表 |
| formula 创建失败 | 公式语法错误或引用字段名不匹配 | 先 `field get` 确认字段精确名称,再检查公式语法(见 [formula-guide](./aitable-formula-guide.md) |
| 删除主字段失败 | 主字段(第一列)不可删除 | 改为更新字段名或类型,不能删除 |
### 2.3 Base/Table 操作错误
| 错误现象 / summary | 原因 | 恢复动作 |
|-------------------|------|---------|
| `base not found` | base-id 错误或无权限 | 确认 base-id 正确;尝试 `base list``base search` 重新定位 |
| `table not found` | table-id 错误 | 用 `table get --base-id <baseId>` 不带 table-ids 查看所有表 |
| 表名重复 | 同 Base 下已存在同名表 | 系统会自动续号(如"原名 1"),无需额外处理 |
### 2.4 视图操作错误
| 错误现象 / summary | 原因 | 恢复动作 |
|-------------------|------|---------|
| `view not found` | view-id 错误 | 用 `view get --base-id <baseId> --table-id <tableId>` 查看所有视图 |
| 删除最后一个视图 | 表至少保留一个视图 | 不可删除唯一视图 |
### 2.5 filters/sort 错误
| 错误现象 / summary | 原因 | 恢复动作 |
|-------------------|------|---------|
| filters 无效被忽略 | 根节点不是 and/or,或 operands 格式错误 | 确保 filters 根节点是 `{"operator":"and"/"or", "operands":[...]}` 结构 |
| sort 无效 | fieldId 不存在 | 先 `field get` 确认字段 ID |
| 筛选结果为空 | 条件过严或字段值不匹配 | 放宽条件验证;注意 singleSelect 筛选值用 option name 或 id |
### 2.6 导入导出错误
| 错误现象 / summary | 原因 | 恢复动作 |
|-------------------|------|---------|
| 导出任务超时 | 数据量大,异步任务未完成 | 用 `export data --task-id <taskId>` 轮询直到完成 |
| 导入文件格式错误 | 不支持的文件格式或文件损坏 | 确认文件为 .xlsx 格式且未加密 |
## 3. 重试策略
### 3.1 可重试的错误
| 错误类型 | 重试方式 | 最大重试次数 |
|---------|---------|------------|
| 网络超时 / 5xx | 等待 2s 后原样重试 | 2 |
| 导出任务未完成 | 轮询 task-id | 5(间隔 3s |
| 并发写入冲突 | 串行重试 | 1 |
### 3.2 不可重试的错误(立即停止)
| 错误类型 | 原因 | 处理方式 |
|---------|------|---------|
| 权限不足 / 403 | 用户对该 Base 无权限 | 停止操作,提示用户确认权限 |
| 参数格式错误 | 请求结构不合法 | 修正参数后重试,不要原样重试 |
| 资源不存在 / 404 | ID 错误或资源已删除 | 重新查询定位资源 |
| 配额超限 / 429 | API 调用频率过高 | 等待后重试,并降低并发 |
### 3.3 重试前检查清单
在重试前,先确认:
1. ❓ 错误是暂时性的还是永久性的?
2. ❓ 参数有没有明显错误需要修正?
3. ❓ 是否需要先查询最新状态再重试?
## 4. 调试技巧
### 4.1 使用 --verbose 获取详细信息
```bash
dws aitable record create \
--base-id <baseId> \
--table-id <tableId> \
--records '[...]' \
--verbose --format json
```
`--verbose` 会输出请求/响应的详细信息,帮助定位问题。
### 4.2 使用 --dry-run 预览
```bash
dws aitable record create \
--base-id <baseId> \
--table-id <tableId> \
--records '[...]' \
--dry-run --format json
```
`--dry-run` 只预览不执行,适合在不确定参数是否正确时先验证。
## 5. 错误预防最佳实践
1. **写记录前先读字段结构**`field get` 确认字段类型和 ID
2. **写字段前先读 field-properties** — 确认 config 的必填项和格式
3. **formula 字段先确认引用字段名**`[字段名]` 必须精确匹配
4. **options 更新传完整列表** — 更新 singleSelect/multipleSelect 的 options 是全量覆盖
5. **大批量操作分批执行** — 单次最多 100 条记录
6. **使用 --format json** — 确保输出可解析,方便错误判断
@@ -0,0 +1,113 @@
# export & import — 导入导出
## 导出数据(两阶段轮询)
`export data` 为异步任务:首次调用可能只返回 `taskId`,需要继续轮询。
> ⚠️ **`--format` 冲突警告**`export data` 的 `--format` 是**导出格式**excel/attachment 等),不是全局输出格式。**此命令禁止追加全局 `--format json`**,否则会覆盖导出格式导致 `INVALID_EXPORT_FORMAT` 错误。输出默认就是 JSON,无需额外指定。
```bash
# 第一步:创建任务(按 scope 传必要参数)——注意:不要加 --format json
dws aitable export data --base-id <BASE_ID> --scope table --table-id <TABLE_ID> --format excel --timeout-ms 1000
# 第二步:拿 taskId 继续轮询,直到返回 downloadUrl
dws aitable export data --base-id <BASE_ID> --task-id <TASK_ID> --timeout-ms 3000
```
### 参数约束
| scope | 必传参数 |
|-------|----------|
| `all` | 只需 `--base-id` |
| `table` | 必须 `--table-id` |
| `view` | 必须 `--table-id` + `--view-id` |
## 导入文件(三步流程)
当用户要求将 Excel`.xlsx`)或 CSV 文件完整导入 AI 表格时,**不需要自己解析文件内容**,直接使用文件级导入。
> **无需手动解析 CSV/Excel 再逐条 record create**,效率极低且容易出错。
```bash
# 第 1 步:申请上传凭证
dws aitable import upload --base-id <BASE_ID> \
--file-name data.xlsx --file-size <字节数> --format json
# → 返回 uploadUrl 和 importId
# 第 2 步:上传文件到 OSS(注意:Content-Type 必须设为空)
curl -X PUT "<uploadUrl>" -H "Content-Type:" --data-binary @data.xlsx
# 第 3 步:触发导入(新建表模式)
dws aitable import data --import-id <importId> --format json
# → 返回 status: success 和新建的 tableIds
# 第 3 步(替代):追加到已有表
dws aitable import data --import-id <importId> --table-id <TABLE_ID> --format json
# → 数据作为新行追加到指定表中
```
### 步骤说明
| 步骤 | 命令 | 说明 |
|------|------|------|
| 申请上传凭证 | `import upload --base-id <ID> --file-name <名称> --file-size <字节>` | `--file-size` 必须与实际文件大小一致 |
| 上传文件 | HTTP PUTcurl 等) | **必须**`-H "Content-Type:"` 将 Content-Type 设为空,否则 OSS 返回 403 |
| 触发导入 | `import data --import-id <ID> [--table-id <TABLE_ID>]` | 同步等待,大多一次调用即返回结果;超时可用相同 importId 重试 |
### import data 参数
| 参数 | 必填 | 说明 |
|------|------|------|
| `--import-id` | ✅ | `import upload` 返回的 importId |
| `--table-id` | ❌ | 传入时数据追加到该已有表;不传则每个 Sheet 新建独立的数据表 |
| `--timeout` | ❌ | 最长等待秒数,默认且推荐 30 |
| `--header-row` | ❌ | 表头所在行号(从 1 开始),数据从下一行读取。不传则自动识别 |
| `--src-sheet-name` | ❌ | 源文件中的 Sheet 名称,多 Sheet 文件时指定。不传则用第一个 Sheet |
| `--field-mapping` | ❌ | 字段映射 JSON`{"目标字段名":"源列名"}`)。不传则按列名自动匹配 |
### 两种导入模式
| 模式 | 触发条件 | 效果 |
|------|----------|------|
| **新建表导入** | 不传 `--table-id` | 每个 Sheet 自动新建为独立数据表 |
| **追加导入** | 传入 `--table-id` | 数据作为新行追加到指定已有表,按列名自动匹配字段 |
### 支持的文件格式:xlsx vs csv
| 特性 | xlsx | csv |
|------|------|-----|
| 新建表导入 | ✅ | ✅ |
| 追加导入(`--table-id` | ✅ | ✅ |
| `--header-row` | ✅ | ❌ 不支持 |
| `--src-sheet-name` | ✅(多 Sheet 支持) | ❌ 无 Sheet 概念 |
| `--field-mapping` | ✅ | ✅ |
> **CSV 限制**CSV 没有 Sheet 概念,且表头固定为第一行,因此 `--header-row` 和 `--src-sheet-name` 对 CSV 均不可用。
>
> **建议**:需要指定表头行或多 Sheet 选择时,**必须使用 xlsx 格式**。CSV 仅适用于表头在第一行的简单导入场景。
### 追加导入的字段匹配规则
追加导入时,系统按以下规则将 Excel 列映射到目标表字段:
1. **不传 `--field-mapping`(自动匹配)**:按字段名**精确匹配** Excel 列名和目标表字段名。如果没有任何一列匹配上,导入会失败。
2. **传 `--field-mapping`(显式映射)**:按映射关系指定对应关系,key 为目标表字段名,value 为 Excel 列名。
> **追加导入失败常见原因**:Excel 列名与目标表字段名不一致(如 Excel 是"销售姓名"但表字段是"姓名"),导致自动匹配 0 个字段,报错 `"Failed to build import sheet infos from preview data"`。
>
> **解决方案**
> 1. **首选**:创建目标表时,字段名与 Excel 表头列名**保持完全一致**
> 2. **备选**:传 `--field-mapping '{"目标字段名":"Excel列名"}'` 手动指定映射
> 3. **兜底**:如果 import data 多次失败,改用 `record create` 逐条写入
### 适用场景
- **新建表导入**:首次导入 Excel/CSV,让系统自动建表建字段
- **追加到已有表**:已有数据表结构,需要把 Excel 数据批量写入 → 传 `--table-id`(推荐 xlsx
- **需要指定表头行**:源文件前几行非数据(如注释行)→ `--header-row`(必须用 xlsx
- **多 Sheet 文件**:只导入特定 Sheet → `--src-sheet-name`(必须用 xlsx
- **不适用**:需要复杂字段级控制(如只导入部分列、数据转换)→ 解析后用 `record create`
> **导入数据无法整体撤销**:文件一旦导入成功,数据即写入表中,没有"撤销导入"操作。如需清理导入的测试数据,只能手动通过 `record delete` 逐条或批量删除记录;如果是新建表模式导入的,可以直接 `table delete` 删除整张表。因此:
> - 测试/验证场景建议导入到**独立的测试表或测试 Base**,用完后整体删除
> - 如果用户明确表示不想导入测试数据或要求先预览内容再决定,应先解析文件内容展示给用户确认,而非直接导入
@@ -0,0 +1,238 @@
# 字段类型 config 规范(field create / table create / field update
> 适用命令:`dws aitable field create`、`dws aitable table create --fields`、`dws aitable field update --config`
>
> 本文件是 DWS AI 表格字段 config 的 **source of truth**。创建/更新字段时,必须严格按此规范构造 JSON。
## 1. 顶层规则
- `table create --fields``field create --fields` 中每个字段对象:`{"fieldName":"xxx", "type":"xxx", "config":{...}}`
- `field create --name --type --config` 中 config 单独传 JSON 字符串
- `field update --config` 只传 config 部分
- 不需要 config 的类型(如 text、checkbox、attachment)可省略 config 字段
## 2. 字段类型速查
| type | 需要 config | config 核心字段 | 说明 |
|------|-------------|----------------|------|
| `text` | ❌ | — | 纯文本 |
| `number` | 可选 | `formatter` | 数字格式 |
| `singleSelect` | ✅ | `options` | 单选 |
| `multipleSelect` | ✅ | `options` | 多选 |
| `date` | 可选 | `formatter` | 日期格式 |
| `currency` | 可选 | `currencyType`, `formatter` | 货币 |
| `progress` | 可选 | `formatter`, `min`, `max`, `customizeRange` | 进度条 |
| `rating` | 可选 | `min`, `max`, `icon` | 评分 |
| `checkbox` | ❌ | — | 勾选框 |
| `user` | 可选 | `multiple` | 人员 |
| `department` | 可选 | `multiple` | 部门 |
| `group` | 可选 | `multiple` | 群组 |
| `url` | ❌ | — | 链接 |
| `richText` | ❌ | — | 富文本 |
| `telephone` | ❌ | — | 电话 |
| `email` | ❌ | — | 邮箱 |
| `attachment` | ❌ | — | 附件 |
| `geolocation` | ❌ | — | 地理位置 |
| `formula` | ✅ | `formula` | 公式(只读字段) |
| `unidirectionalLink` | ✅ | `linkedTableId`, `multiple` | 单向关联 |
| `bidirectionalLink` | ✅ | `linkedTableId`, `multiple` | 双向关联 |
| `creator` | ❌ | — | 系统字段:创建人(只读) |
| `lastModifier` | ❌ | — | 系统字段:最后编辑人(只读) |
| `createdTime` | ❌ | — | 系统字段:创建时间(只读) |
| `lastModifiedTime` | ❌ | — | 系统字段:最后编辑时间(只读) |
## 3. 各类型 config 详解
### 3.1 number(数字)
config 字段:`formatter`
可选值:
- `INT` — 整数
- `FLOAT_1` — 1 位小数
- `FLOAT_2` — 2 位小数(默认)
- `FLOAT_3` — 3 位小数
- `FLOAT_4` — 4 位小数
- `THOUSAND` — 千分位整数
- `THOUSAND_FLOAT` — 千分位 + 小数
- `PERCENT` — 百分比(整数)
- `PERCENT_FLOAT` — 百分比(小数)
```json
{"fieldName": "工时", "type": "number", "config": {"formatter": "FLOAT_2"}}
```
```json
{"fieldName": "完成率", "type": "number", "config": {"formatter": "PERCENT"}}
```
### 3.2 singleSelect / multipleSelect(单选 / 多选)
config 字段:`options`(必填)
options 结构:
- `options` 是数组,每项至少包含 `name`
- 创建时只传 `name``id` 由系统生成
- **更新时**:已有选项必须回传原 `id`(从 `field get` 获取),新增选项不传 id
```json
{
"fieldName": "优先级",
"type": "singleSelect",
"config": {
"options": [
{"name": "紧急"},
{"name": "高"},
{"name": "中"},
{"name": "低"}
]
}
}
```
更新已有字段时(保留原选项 + 新增):
```json
{
"options": [
{"id": "opt_existing_1", "name": "紧急"},
{"id": "opt_existing_2", "name": "高"},
{"id": "opt_existing_3", "name": "中"},
{"name": "极低"}
]
}
```
> 更新 options 是**全量覆盖**,不是追加!不传的旧选项会被删除,关联的单元格数据丢失。
### 3.3 date(日期)
config 字段:`formatter`
可选值:
- `YYYY-MM-DD`(默认)
- `YYYY-MM-DD HH:mm`
- `YYYY-MM-DD HH:mm:ss`
- `YYYY/MM/DD`
- `YYYY/MM/DD HH:mm`
```json
{"fieldName": "截止日期", "type": "date", "config": {"formatter": "YYYY-MM-DD"}}
```
```json
{"fieldName": "创建时间", "type": "date", "config": {"formatter": "YYYY-MM-DD HH:mm"}}
```
### 3.4 currency(货币)
config 字段:`currencyType`(必填)、`formatter`(可选)
currencyType 可选值:
`CNY` | `HKD` | `USD` | `EUR` | `GBP` | `MOP` | `VND` | `JPY` | `KRW` | `AED` | `AUD` | `BRL` | `CAD` | `CHF` | `INR` | `IDR` | `MXN` | `MYR` | `PHP` | `PLN` | `RUB` | `SGD` | `THB` | `TRY` | `TWD`
formatter 可选值(控制小数位):`INT` | `FLOAT_1` | `FLOAT_2`(默认)| `FLOAT_3` | `FLOAT_4`
```json
{"fieldName": "预算", "type": "currency", "config": {"currencyType": "CNY", "formatter": "FLOAT_2"}}
```
### 3.5 progress(进度)
config 字段:`formatter`(固定为 `PERCENT`)、`customizeRange``min``max`
- 默认范围:0~1(即 0%~100%
- 自定义范围时 `customizeRange` 必须为 `true`
```json
{"fieldName": "完成度", "type": "progress", "config": {"formatter": "PERCENT"}}
```
自定义范围:
```json
{"fieldName": "进度", "type": "progress", "config": {"formatter": "PERCENT", "customizeRange": true, "min": 0, "max": 1}}
```
### 3.6 rating(评分)
config 字段:`min``max``icon`
- `min`:固定为 `1`
- `max`1~10,默认 `5`
- `icon`:默认 `star`
```json
{"fieldName": "满意度", "type": "rating", "config": {"min": 1, "max": 5, "icon": "star"}}
```
### 3.7 user / department / group(人员 / 部门 / 群组)
config 字段:`multiple`
- `multiple``true`(多选,默认)| `false`(单选)
```json
{"fieldName": "负责人", "type": "user", "config": {"multiple": false}}
```
```json
{"fieldName": "协作部门", "type": "department", "config": {"multiple": true}}
```
### 3.8 formula(公式)
config 字段:`formula`(必填)
- 公式中引用字段使用**方括号 + 字段名**:`[字段名]`
- 支持的函数:参考钉钉 AI 表格公式文档
```json
{"fieldName": "合计", "type": "formula", "config": {"formula": "[单价] * [数量]"}}
```
```json
{"fieldName": "是否逾期", "type": "formula", "config": {"formula": "IF([截止日期] < NOW(), \"是\", \"否\")"}}
```
> ⚠️ formula 字段创建后为**只读**,不能通过 record create/update 写入值。
### 3.9 unidirectionalLink(单向关联)
config 字段:`linkedTableId`(必填)、`multiple`
- `linkedTableId`:目标表的 tableId
- `multiple``true`(多选,默认)| `false`(单选)
```json
{"fieldName": "关联项目", "type": "unidirectionalLink", "config": {"linkedTableId": "tblXXXXXX", "multiple": true}}
```
### 3.10 bidirectionalLink(双向关联)
config 字段:`linkedTableId`(必填)、`multiple`
- 与单向关联参数相同
- 创建后系统会**自动**在被关联表创建反向字段
```json
{"fieldName": "关联任务", "type": "bidirectionalLink", "config": {"linkedTableId": "tblYYYYYY", "multiple": true}}
```
## 4. AI 字段(ai-config
AI 字段不使用 config,而使用独立的 `--ai-config` 参数。详见 [aitable-field.md](./aitable-field.md) 中的 AI 字段创建示例。
核心规则:
- `outputType` 必须与 `--type` 对应:text→text, select→singleSelect, multiSelect→multipleSelect, number→number, currency→currency, image/video→attachment
- `prompt` 中必须至少包含一个 `fieldRef` 引用
- 纯文本 prompt 会被后端拒绝
## 5. 常见错误
| 错误 | 说明 |
|------|------|
| options 更新时不传已有选项的 id | 会被视为新选项,旧选项被删除,关联数据丢失 |
| options 更新时只传新增项 | 全量覆盖,旧选项全部丢失 |
| formula 字段尝试写入值 | 只读字段,record create/update 会报错 |
| linkedTableId 传表名而非 ID | 必须传 tableId(如 `tblXXX`),不接受表名 |
| progress 值写入 50 表示 50% | 实际应写入 0.5range 0~1 |
| rating 值超出 max | 写入会报错 |
@@ -0,0 +1,174 @@
# field — 字段管理
## field get — 获取字段详情
```
Usage:
dws aitable field get [flags]
Example:
dws aitable field get --base-id <BASE_ID> --table-id <TABLE_ID>
dws aitable field get --base-id <BASE_ID> --table-id <TABLE_ID> --field-ids fld1,fld2
Flags:
--base-id string Base ID (必填)
--field-ids string 字段 ID 列表,逗号分隔,单次最多 10 个
--table-id string Table ID (必填)
```
返回字段的完整配置(含 options 等)。不要假设未指定 `--table-ids``table get` 枚举结果含字段;字段目录和配置以 `field get` 返回为准。
## field create — 创建字段
```
Usage:
dws aitable field create [flags]
Example:
dws aitable field create --base-id <BASE_ID> --table-id <TABLE_ID> \
--name "状态" --type "singleSelect" --config '{"options":[{"name":"待办"},{"name":"进行中"},{"name":"已完成"}]}'
# 或者使用批量创建模式:
dws aitable field create --base-id <BASE_ID> --table-id <TABLE_ID> \
--fields '[{"fieldName":"状态","type":"singleSelect","config":{"options":[{"name":"待办"}]}}]'
Flags:
--base-id string Base ID (必填)
--name string 要创建的单字段名称(与 --type 配合使用,替代 --fields
--type string 要创建的单字段类型(需要配合 --name,参考 table create 的内置类型)
--config string 单字段配置 JSON(需要配合 --name/--type,结构参考 table create
--ai-config string 单字段 AI 配置 JSON(需要配合 --name/--type
--fields string 批量新增字段 JSON 数组,单次最多 15 个;每个字段的配置写在其 config/aiConfig 内
--table-id string Table ID (必填)
```
`field create` 有且只有两种输入模式:
- 单字段模式:必须同时传 `--name``--type``--config``--ai-config` 只作为该字段的附加配置。
- 批量模式:只传 `--fields`;字段配置写在数组内各对象的 `config` / `aiConfig` 中。
两种模式严格互斥。`--fields` 不能与 `--name``--type``--config``--ai-config` 混用;单独传 `--config` 也会报错,不会被静默忽略。
例如创建单选字段时,单字段模式的 `--config` 是一个配置对象;批量模式则把同一对象放入对应字段元素的 `config`
```bash
# 单字段模式
dws aitable field create --base-id <BASE_ID> --table-id <TABLE_ID> \
--name "部门" --type singleSelect \
--config '{"options":[{"name":"技术部"},{"name":"产品部"}]}'
# 批量模式
dws aitable field create --base-id <BASE_ID> --table-id <TABLE_ID> \
--fields '[{"fieldName":"部门","type":"singleSelect","config":{"options":[{"name":"技术部"},{"name":"产品部"}]}}]'
```
允许部分成功,返回结果逐项标明成功/失败状态。
### AI 字段创建示例
```bash
dws aitable field create --base-id <BASE_ID> --table-id <TABLE_ID> \
--name "AI摘要" --type text \
--ai-config '{
"outputType":"text",
"prompt":[
{"type":"text","value":"请将下面内容总结成不超过80字的中文摘要:"},
{"type":"fieldRef","fieldId":"fld_content"}
],
"autoRecompute":true,
"enableWebSearch":false,
"enableThinking":true
}' --format json
```
说明:
- `outputType` 与字段类型需一致(如 `outputType=text``--type text`
- `prompt` 里通过 `fieldRef` 引用已有字段
- `autoRecompute=true` 表示引用字段变化后自动重算
- **AI 字段的 prompt 必须至少包含一个 `fieldRef` 引用**,纯文本 prompt 会被后端拒绝
### 关联字段与跨表引用字段
创建 `lookup`(关联引用)和 `filterUp`(查找引用)字段时,config 格式有严格要求:
#### bidirectionalLink / unidirectionalLink(关联字段)
```bash
dws aitable field create --base-id <BASE_ID> --table-id <TABLE_ID> \
--name "关联客户" --type bidirectionalLink \
--config '{"linkedTableId":"<目标表tableId>","multiple":true}' --format json
```
#### lookup(关联引用,通过已有关联字段取值)
**前置条件**:本表必须已有一个 bidirectionalLink 或 unidirectionalLink 类型的关联字段。
```bash
dws aitable field create --base-id <BASE_ID> --table-id <TABLE_ID> \
--name "客户城市" --type lookup \
--config '{"associateField":"<本表关联字段的fieldId>","valuesField":"<关联目标表中要取值的字段fieldId>","aggregator":"CONCATENATE"}' --format json
```
config 必填字段:
- `associateField`:**本表中**已有的关联字段(bidirectionalLink/unidirectionalLink)的 fieldId
- `valuesField`:**关联目标表中**要取值的字段 fieldId
- `aggregator`:聚合方式,可选 `SUM`|`AVERAGE`|`COUNT`|`MAX`|`MIN`|`CONCATENATE`
> 常见错误:`associateField` 不是目标表的 tableId,也不是目标表的字段 ID,而是**本表中关联字段自身的 fieldId**。
#### filterUp(查找引用,无需关联字段,直接跨表取值)
```bash
# 基本用法:字段对常量匹配
dws aitable field create --base-id <BASE_ID> --table-id <TABLE_ID> \
--name "客户总金额" --type filterUp \
--config '{"targetSheet":"<目标表tableId>","filters":[{"fieldId":"<目标表字段Id>","operator":"equal","value":"匹配值","link":"AND"}],"valuesField":"<目标表中要取值的字段Id>","aggregator":"SUM"}' --format json
# 进阶用法:字段对字段动态匹配(currentSheetFieldId
dws aitable field create --base-id <BASE_ID> --table-id <TABLE_ID> \
--name "本城市订单金额" --type filterUp \
--config '{"targetSheet":"<目标表tableId>","filters":[{"fieldId":"<目标表字段Id>","operator":"equal","currentSheetFieldId":"<本表字段Id>","link":"AND"}],"valuesField":"<目标表中要取值的字段Id>","aggregator":"SUM"}' --format json
```
config 必填字段:
- `targetSheet`:目标表的 tableId
- `filters`:至少一条筛选规则
- `fieldId`:目标表中用于匹配的字段 fieldId
- `operator`:仅支持 `equal``contain`(不支持 not_equal/not_contain
- `value`:常量匹配值(与 `currentSheetFieldId` 二选一)
- `currentSheetFieldId`:本表中用于动态匹配的字段 fieldId(与 `value` 二选一,实现每行按本表字段值去目标表筛选)
- `link`:多条件时的逻辑关系,`AND``OR`(单条件时可省略,多条件时建议显式指定;所有 filter 的 link 必须统一)
- `valuesField`:目标表中要取值的字段 fieldId
- `aggregator`:聚合方式,可选 `SUM`|`AVERAGE`|`COUNT`|`MAX`|`MIN`|`CONCATENATE`
## field update — 更新字段
```
Usage:
dws aitable field update [flags]
Example:
dws aitable field update --base-id <BASE_ID> --table-id <TABLE_ID> --field-id <FIELD_ID> --name "新字段名"
dws aitable field update --base-id <BASE_ID> --table-id <TABLE_ID> --field-id <FIELD_ID> --config '{"options":[{"name":"A"},{"name":"B"}]}'
Flags:
--base-id string Base ID (必填)
--config string 字段配置 JSON (不修改时省略)
--ai-config string AI 配置 JSON (不修改时省略)
--field-id string Field ID (必填)
--name string 新字段名称 (不修改时省略)
--table-id string Table ID (必填)
```
- 不可变更字段类型
- 更新 singleSelect/multipleSelect 的 options 时需传入完整列表,已有选项应回传原 id
- `--name` / `--config` / `--ai-config` 至少传一个
## field delete — 删除字段
```
Usage:
dws aitable field delete [flags]
Example:
dws aitable field delete --base-id <BASE_ID> --table-id <TABLE_ID> --field-id <FIELD_ID> --yes
Flags:
--base-id string Base ID (必填)
--field-id string 待删除字段 ID (必填)
--table-id string Table ID (必填)
```
不可逆。禁止删除主字段和最后一个字段。
@@ -0,0 +1,169 @@
# filters & sort — 筛选排序语法参考
> 视图(view)配置的 filter/sort/group **整体写入**请优先用 `view update filter` / `view update sort` / `view update group` 子命令,详见 [aitable-view-config.md](./aitable-view-config.md)。本文件聚焦于 `record query --filters` 与 view config filter 的语法和差异。
## filters 结构规范
### 强制规则
1. **根节点必须是逻辑操作符**`"operator"` 必须是 `"and"``"or"`,不能是 `"eq"` 等比较操作符
2. 比较操作必须放在根节点的 `"operands"` 数组内的对象中
3. `singleSelect``multipleSelect` 字段,推荐使用 **选项的 exact String 名称 (name)** 作为比较值
4. fieldId 必须通过 `field get` 获取,不能直接用字段名称
### 精简防呆模板
CLI 同时兼容两种子条件写法(推荐格式 A):
**格式 Aoperands 数组,推荐):**
```json
{
"operator": "and",
"operands": [
{"operator": "eq", "operands": ["fld_state", "进行中"]}
]
}
```
**格式 BfieldId/value 对象,CLI 自动转换):**
```json
{
"operator": "and",
"operands": [
{"fieldId": "fld_state", "operator": "eq", "value": "进行中"}
]
}
```
4 种衍生:
- **OR 查询**:根节点 `"operator"` 改为 `"or"`
- **多条件 AND**:在 `"operands"` 数组中增加对象
- **文本包含**:内层 `"operator"` 改为 `"contain"`
- **为空判断**`"operator":"un_exist"`operands 只需 `["fieldId"]`
### 支持的操作符(已验证完整列表)
| 操作符 | 含义 | operands 格式 |
|--------|------|--------------|
| `eq` / `ne` | 等于 / 不等于 | `["fieldId", "value"]` |
| `contain` / `exclusive` | 包含 / 不包含(文本模糊) | `["fieldId", "value"]` |
| `gt` / `gte` / `lt` / `lte` | 大于 / ≥ / 小于 / ≤ | `["fieldId", "numStr"]` |
| `exist` / `un_exist` | 有值 / 为空 | `["fieldId"]`(无需第二项) |
| `any_of` / `none_of` / `all_of` | 包含任一 / 不包含任一 / 全包含(多选字段) | `["fieldId", "optionName"]` |
| `date_eq` / `before` / `after` | 日期等于 / 早于 / 晚于 | `["fieldId", "dateStr"]` |
| `not_before` / `not_after` | 不早于(≥) / 不晚于(≤) | `["fieldId", "2026-05-22"]` |
> **操作符拼写必须严格匹配上表**,CLI 会在调用前校验,错误拼写会被拒绝。
>
> **没有 `date_between`(区间)操作符**,也**不支持 `from_now`**——date 字段不支持区间/相对过滤,传了会被 CLI 拒绝。范围查询用 `not_before` + `not_after` 组合,见下方专节。
### 日期字段过滤(date / 创建时间 / 修改时间)
日期类字段的过滤规则与其它字段**不同**,是线上反馈最高频的踩坑点。**经集成测试实测**确认的规则:
1. **只能用日期专用操作符**`date_eq` / `before` / `after` / `not_before` / `not_after` / `exist` / `un_exist`(与前端筛选 UI 的「等于 / 早于 / 晚于 / 早于或等于 / 晚于或等于 / 不为空 / 为空」一一对应)。
2. **比较值用日期字符串**,如 `"2026-05-22"`(也接受 RFC3339 / 毫秒时间戳,内部统一转成毫秒比较)。读取返回的是带时区 RFC3339(如 `"2026-05-22T00:00:00+08:00"`)。
3. **通用操作符 `eq` / `ne` / `gt` / `gte` / `lt` / `lte` / `contain` 对 date 字段无效**——无论传 ISO 字符串还是毫秒时间戳,都会**静默返回 0 条**。这是后端 date 字段的比较规则,不是 bug,CLI 也无法在本地拦截(不知道字段类型),务必用对操作符。
4. **没有区间操作符 `date_between`**,也**不支持 `from_now`(相对天数)**——均会静默返回 0 条,CLI 已直接拒绝。范围查询用 `not_before`(≥起点)+ `not_after`(≤终点)两个条件 `and` 组合。
| 需求 | 操作符 | 示例 operands |
|------|--------|--------------|
| 等于某天 | `date_eq` | `["fldDate", "2026-05-22"]` |
| 早于 / 晚于(不含当天) | `before` / `after` | `["fldDate", "2026-05-22"]` |
| 不早于(≥) / 不晚于(≤) | `not_before` / `not_after` | `["fldDate", "2026-05-22"]` |
| 有值 / 为空 | `exist` / `un_exist` | `["fldDate"]` |
**日期区间查询(替代 between)**——查 `2026-05-01 ~ 2026-05-31`(含端点):
```bash
dws aitable record query --base-id X --table-id Y \
--filters '{"operator":"and","operands":[{"operator":"not_before","operands":["fldDate","2026-05-01"]},{"operator":"not_after","operands":["fldDate","2026-05-31"]}]}'
```
### 常见错误拼写(CLI 会自动提示纠正)
| 错误写法 | 正确写法 | 说明 |
|------------|-----------|------|
| `equal` / `equals` / `is` / `==` | `eq` | 等于 |
| `not_equal` / `not_equals` / `is_not` / `!=` | `ne` | 不等于 |
| `like` / `contains` / `include` | `contain` | 文本包含 |
| `greater_than` | `gt` | 大于 |
| `less_than` | `lt` | 小于 |
| `not_eq` / `not_contain` / `is_empty` | `ne` / `exclusive` / `un_exist` | 其他易混淆 |
### 错误示例
**缺失根节点 and/or**(API 将忽略该 filter,返回全表):
```json
{"operator":"eq","operands":["fldXXX","本科"]}
```
**传入选项 ID 而非名称**(可能导致匹配不到 0 记录):
```json
{"operator":"and","operands":[{"operator":"eq","operands":["fldXXX","CXzrOHK9JI"]}]}
```
### 完整示例
单条件:
```bash
dws aitable record query --base-id X --table-id Y \
--filters '{"operator":"and","operands":[{"operator":"eq","operands":["fldStatusId","进行中"]}]}'
```
多条件 AND
```bash
dws aitable record query --base-id X --table-id Y \
--filters '{"operator":"and","operands":[{"operator":"eq","operands":["fldStatusId","进行中"]},{"operator":"gt","operands":["fldStockId","0"]}]}'
```
## sort 结构规范
`--sort` 传 JSON 数组,排序方向字段**必须是 `direction`**,不要使用 `order`
```bash
--sort '[{"fieldId":"fldXXX","direction":"desc"}]'
```
多字段排序:
```bash
--sort '[{"fieldId":"fldPriority","direction":"desc"},{"fieldId":"fldCreatedAt","direction":"asc"}]'
```
---
## view update --config 中的 filter / sort 格式
> **重要区分**`record query --filters` 和 `view update --config` 中的 filter **格式不同**
| 场景 | filter 格式 | 说明 |
|------|-------------|------|
| `record query --filters` | **对象**`{"operator":"and","operands":[...]}` | 直接传最外层逻辑对象 |
| `view update --config` 的 filter | **数组**`[{"operator":"and","operands":[...]}]` | 外面多一层数组包裹 |
| `view update --config` 的 sort | **数组**`[{"fieldId":"X","direction":"asc"}]` | 与 record query --sort 一致 |
### 正确示例
```bash
# view update 设置筛选(filter 是数组)
dws aitable view update --base-id X --table-id Y --view-id Z \
--config '{"filter":[{"operator":"and","operands":[{"operator":"eq","operands":["fldStatus","待处理"]}]}]}'
# view update 设置排序(sort 是数组)
dws aitable view update --base-id X --table-id Y --view-id Z \
--config '{"sort":[{"fieldId":"fldPriority","direction":"desc"}]}'
# 同时设置 filter + sort + visibleFieldIds
dws aitable view update --base-id X --table-id Y --view-id Z \
--config '{"filter":[{"operator":"and","operands":[{"operator":"eq","operands":["fldStatus","进行中"]}]}],"sort":[{"fieldId":"fldDate","direction":"asc"}],"visibleFieldIds":["fld1","fld2","fld3"]}'
```
### CLI 自动容错
CLI 会自动修正以下常见错误格式(不会报错,但建议直接使用正确格式):
| 错误写法 | CLI 自动修正为 |
|----------|---------------|
| `"filter":{"operator":"and",...}` (对象) | `"filter":[{"operator":"and",...}]` (数组) |
| `"sort":{"fieldId":"X","direction":"asc"}` (对象) | `"sort":[{"fieldId":"X","direction":"asc"}]` (数组) |
| 子条件用 MCP 简写 `{"fieldId":"X","operator":"eq","value":"Y"}` | 自动转为 `{"operator":"eq","operands":["X","Y"]}` |
@@ -0,0 +1,130 @@
# form — 表单管理
## 命令一览
| 命令 | 用途 |
|------|------|
| `form list` | 列出数据表下所有表单视图 |
| `form get` | 按 viewId 取单个表单详情(list_form_views + viewIds 过滤) |
| `form create` | 创建表单视图(等价于 `view create --view-type FormDesigner` |
| `form update` | 更新表单标题或描述 |
| `form delete` | 删除表单视图(不可逆) |
| `form field list` | 列出表单可见字段 |
| `form field update` | 更新字段必填/描述 |
| `form field hide` | 在表单中隐藏/显示字段(不影响底层数据表字段) |
| `form share get` | 获取分享配置 |
| `form share update` | 开启/关闭分享 |
| `form questions create` | 添加题目(等价于 `field create`,命令位置上的别名) |
| `form questions delete` | 删除题目(等价于 `field delete`,命令位置上的别名) |
## 建议操作顺序
```bash
# 1) 列出数据表下的表单视图
dws aitable form list --base-id BASE_ID --table-id TABLE_ID --format json
# 2) 查看单个表单详情
dws aitable form get --base-id BASE_ID --table-id TABLE_ID --view-id VIEW_ID --format json
# 3) 查看表单字段配置
dws aitable form field list --base-id BASE_ID --table-id TABLE_ID --view-id VIEW_ID --format json
# 4) 查看分享配置
dws aitable form share get --base-id BASE_ID --table-id TABLE_ID --view-id VIEW_ID --format json
```
开启并取得可发送链接的最短闭环使用 canonical shortcut
```bash
dws aitable form create --base-id BASE_ID --table-id TABLE_ID --name "表单名" --format json
dws aitable +form-share-update --base-id BASE_ID --table-id TABLE_ID --view-id VIEW_ID --enabled true --format json
dws aitable +form-share-get --base-id BASE_ID --table-id TABLE_ID --view-id VIEW_ID --format json
```
从最后一次返回直接读取 `data.shareFormUuid`,分享地址为 `https://alidocs.dingtalk.com/i/form/{shareFormUuid}`。字段已存在时不要再调用 Help、Catalog、`form get`,也不要换 `--verbose``raw``pretty` 重复请求。用户还要求发送时,把该完整 URL 交给 Chat 的发送命令并检查真实发送回执。
## 要点
- **创建表单**有两种等价方式:
- `form create --name "表单名"`(推荐,语义清晰)
- `view create --view-type FormDesigner --name "表单名"`(底层一致)
- `form update` 支持 `--title``--name` 两个等价参数;至少需传一项
- `form field update` 必须传 `--required``--field-description` 至少一项
- `form field hide` 仅控制字段在表单中的可见性,不影响底层数据表字段
- **题目管理**与字段管理本质相同(题目 = 表格字段):
- `form questions create``field create` 入参完全一致(`--fields` JSON 或 `--name --type`
- `form questions delete``field delete` 入参完全一致(必传 `--field-id`
- 设置必填要在 create 后用 `form field update --required true` 单独调一次
## form 子命令
| 命令 | 用途 | 必填参数 | 说明 |
|------|------|----------|------|
| `form list` | 列出表单视图 | `--base-id` `--table-id` | 返回 viewId/name/title/createdAt |
| `form get` | 按 viewId 取单个表单 | `--base-id` `--table-id` `--view-id` | 内部基于 list_form_views 过滤 |
| `form create` | 创建表单视图 | `--base-id` `--table-id` `--name` | viewType=FormDesigner |
| `form update` | 更新表单 | `--base-id` `--table-id` `--view-id` | `--title`/`--name`(等价)和 `--description` 至少传一项;同时传 title/name 时 title 优先 |
| `form delete` | 删除表单 | `--base-id` `--table-id` `--view-id` `--yes` | 不可逆 |
## form field 子命令
| 命令 | 用途 | 必填参数 | 说明 |
|------|------|----------|------|
| `form field list` | 列出表单字段 | `--base-id` `--table-id` `--view-id` | 返回 fieldId/name/type/required/hidden/descriptionhidden=true 的字段不在此返回) |
| `form field update` | 更新表单字段 | `--base-id` `--table-id` `--view-id` `--field-id` | `--required``--field-description` 至少一项 |
| `form field hide` | 切换字段隐藏 | `--base-id` `--table-id` `--view-id` `--field-id` `--hidden` | `--hidden true` 隐藏 / `--hidden false` 显示 |
## form questions 子命令
`form questions create/delete``field create/delete` 入参、行为完全一致,只是命令位置归属于 `form` 命令组,方便从表单视角操作题目。
| 命令 | 用途 | 必填参数 | 说明 |
|------|------|----------|------|
| `form questions create` | 添加题目 | `--base-id` `--table-id` + (`--fields``--name --type`) | 入参与 `field create` 完全一致 |
| `form questions delete` | 删除题目 | `--base-id` `--table-id` `--field-id` `--yes` | 入参与 `field delete` 完全一致;不可逆;批量需多次调用 |
## form share 子命令
| 命令 | 用途 | 必填参数 | 说明 |
|------|------|----------|------|
| `form share get` | 获取分享配置 | `--base-id` `--table-id` `--view-id` | 返回 enabled/status/shareFormUuid |
| `form share update` | 开启/关闭分享 | `--base-id` `--table-id` `--view-id` `--enabled` | `--enabled true` 开启 / `--enabled false` 关闭。注意:UI 上"发布并分享"按钮是另一概念,本命令只切换内部 enabled 标志,开启后需在 UI 刷新页面才会看到分享面板 |
## 完整工作流示例
> **占位符约定**
> - `BASE_ID` 来自 `dws aitable base list` / `base search` 返回的 `data.bases[].baseId`
> - `TABLE_ID` 来自 `dws aitable base get --base-id BASE_ID` 返回的 `data.tables[].tableId`
> - `VIEW_ID` 来自步骤 1 `form create` 返回的 `data.viewId`
> - `FIELD_ID` 来自步骤 2 `form questions create` 返回的 `data.results[].fieldId`
```bash
# 1) 创建表单 → 取返回的 data.viewId 作为 VIEW_ID
dws aitable form create --base-id BASE_ID --table-id TABLE_ID --name "员工信息收集" --format json
# 2) 添加题目 → 取返回的 data.results[].fieldId 作为 FIELD_ID
dws aitable form questions create --base-id BASE_ID --table-id TABLE_ID \
--fields '[{"fieldName":"姓名","type":"text"},{"fieldName":"邮箱","type":"text"}]' --format json
# 3) 配置表单标题与描述
dws aitable form update --base-id BASE_ID --table-id TABLE_ID --view-id VIEW_ID \
--title "员工信息收集" --description "请填写您的基本信息" --format json
# 4) 设置题目必填(FIELD_ID 来自步骤 2)
dws aitable form field update --base-id BASE_ID --table-id TABLE_ID --view-id VIEW_ID \
--field-id FIELD_ID --required true --format json
# 5) 隐藏不需要的题目
dws aitable form field hide --base-id BASE_ID --table-id TABLE_ID --view-id VIEW_ID \
--field-id FIELD_ID --hidden true --format json
# 6) 开启分享(注意:开启后需 UI 刷新页面才会看到分享面板)
dws aitable form share update --base-id BASE_ID --table-id TABLE_ID --view-id VIEW_ID \
--enabled true --format json
```
## 返回结构补充
- `form list` 返回 `data.formViews[]`**每条仅含** `viewId/name/title/createdAt``shareFormUuid` 不在此返回,请用 `form share get` 单独获取。
- `form get` 返回结构与 `form list` 完全一致(`data.formViews[]`),仅含一条记录(与请求 viewId 一致)。Agent 提取时仍走 `data.formViews[0]`
- `form field list` 仅返回**未隐藏**的字段;`hidden=true` 的字段不在此返回,如需查看全部字段请用 `field get`
@@ -0,0 +1,224 @@
# AI 表格公式字段指南
> 当用户要创建 formula 类型字段、编写表内计算公式、做派生指标时,必须先读本文档。
## 1. 何时使用 formula 字段
| 场景 | 用 formula | 不用 formula |
|------|-----------|-------------|
| 长期展示在表中的派生值(如"总价=单价×数量" | ✅ | |
| 条件标记(如"超期=IF(截止日期<TODAY(),'是','否')" | ✅ | |
| 文本拼接(如"全名=姓&名" | ✅ | |
| 一次性统计分析(如"本月总销售额" | | ✅ 用 record stats 服务端聚合 |
| 跨表查找引用 | | ✅ 用 lookup 字段(见下方说明) |
## 2. 创建 formula 字段
```bash
dws aitable field create \
--base-id <baseId> \
--table-id <tableId> \
--name "总价" \
--type formula \
--config '{"formula": "[单价] * [数量]"}' \
--format json
```
### config 结构
```json
{
"formula": "<公式表达式>"
}
```
- `formula` 是唯一必填字段
- 表达式中引用字段使用 **方括号 + 字段名**`[字段名]`
- 字段名必须精确匹配(含空格、大小写)
## 3. 公式语法
### 3.1 引用规则
| 引用方式 | 语法 | 说明 |
|---------|------|------|
| 引用本表字段 | `[字段名]` | 字段名必须精确匹配 |
| 引用关联表字段 | 不支持 | 需要用 lookup 字段 |
### 3.2 常用函数分类
#### 数值计算
| 函数 | 用途 | 示例 |
|------|------|------|
| `+` `-` `*` `/` | 四则运算 | `[单价] * [数量]` |
| `SUM(...)` | 求和 | `SUM([Q1], [Q2], [Q3], [Q4])` |
| `ROUND(value, digits)` | 四舍五入 | `ROUND([金额] * 0.1, 2)` |
| `ABS(value)` | 绝对值 | `ABS([差额])` |
| `MAX(a, b, ...)` | 最大值 | `MAX([成绩1], [成绩2])` |
| `MIN(a, b, ...)` | 最小值 | `MIN([报价1], [报价2])` |
#### 文本处理
| 函数 | 用途 | 示例 |
|------|------|------|
| `&` | 文本拼接 | `[姓] & [名]` |
| `CONCATENATE(...)` | 拼接多个值 | `CONCATENATE([城市], "-", [区])` |
| `LEFT(text, n)` | 取左侧 n 字符 | `LEFT([编号], 4)` |
| `RIGHT(text, n)` | 取右侧 n 字符 | `RIGHT([手机], 4)` |
| `LEN(text)` | 文本长度 | `LEN([备注])` |
| `UPPER(text)` / `LOWER(text)` | 大小写转换 | `UPPER([代码])` |
#### 逻辑判断
| 函数 | 用途 | 示例 |
|------|------|------|
| `IF(条件, 真值, 假值)` | 条件判断 | `IF([金额] > 1000, "大额", "普通")` |
| `AND(a, b, ...)` | 逻辑与 | `IF(AND([状态]="完成", [评分]>=4), "优秀", "")` |
| `OR(a, b, ...)` | 逻辑或 | `IF(OR([等级]="A", [等级]="B"), "通过", "未通过")` |
| `NOT(expr)` | 逻辑非 | `NOT([已归档])` |
| `SWITCH(expr, v1, r1, v2, r2, ..., default)` | 多条件匹配 | `SWITCH([状态], "待办","🔴", "进行中","🟡", "完成","🟢", "")` |
#### 日期函数
| 函数 | 用途 | 示例 |
|------|------|------|
| `TODAY()` | 当前日期 | `IF([截止日期] < TODAY(), "已逾期", "正常")` |
| `NOW()` | 当前时间 | `NOW()` |
| `YEAR(date)` / `MONTH(date)` / `DAY(date)` | 提取年/月/日 | `YEAR([创建时间])` |
| `DATEDIF(start, end, unit)` | 日期差 | `DATEDIF([开始], [结束], "d")` 返回天数 |
| `DATEADD(date, count, unit)` | 日期加减 | `DATEADD([创建时间], 7, "d")` |
> `DATEDIF` 的 unit 参数:`"y"`=年, `"m"`=月, `"d"`=天
#### 空值处理
| 函数 | 用途 | 示例 |
|------|------|------|
| `BLANK()` | 空值常量 | `IF([备注] = BLANK(), "无", [备注])` |
| `IF(field, ...)` | 字段为空时视为 false | `IF([评分], [评分], 0)` |
## 4. 常见公式模板
### 4.1 计算类
```
// 含税价格
[不含税价] * (1 + [税率])
// 完成率百分比
[已完成数] / [总数]
// 折扣后价格
[原价] * (1 - [折扣率])
```
### 4.2 状态标记类
```
// 逾期标记
IF([截止日期] < TODAY(), "⚠️ 已逾期", "正常")
// 优先级标签
SWITCH([优先级], "紧急","🔴P0", "高","🟠P1", "中","🟡P2", "低","🟢P3", "")
// 进度状态
IF([进度] >= 1, "✅ 已完成", IF([进度] > 0, "🔄 进行中", "⏳ 未开始"))
```
### 4.3 文本拼接类
```
// 编号生成
"PRJ-" & [项目编码] & "-" & [序号]
// 地址拼接
[省] & [市] & [区] & [详细地址]
```
## 5. 注意事项与限制
### 5.1 formula 字段是只读的
- formula 字段的值由系统自动计算,**不能通过 `record create/update` 写入**
- 如果用户要"设置某个计算结果",应引导其修改源字段
### 5.2 字段名必须精确
- 公式中的 `[字段名]` 必须与表中实际字段名完全一致
- 创建 formula 字段前,先通过 `field get` 确认字段名
### 5.3 循环引用
- formula 字段不能引用自身
- 不能形成 A→B→A 的循环引用
### 5.4 与跨表引用字段的区别
钉钉 AI 表格有两种跨表取值方式:`lookup`(关联引用)和 `filterUp`(查找引用)。
| 维度 | formula | lookup (关联引用) | filterUp (查找引用) |
|------|---------|-----------------|-------------------|
| 字段类型 | `formula` | `lookup` | `filterUp` |
| 数据来源 | 本表字段 | 通过已有关联字段(bidirectionalLink/unidirectionalLink)取关联表字段 | 直接指定目标表 + 筛选条件取值 |
| 前置条件 | 无 | 必须先有关联字段 | 无需关联字段 |
| 适用场景 | 本表内计算、条件判断 | "我关联了某条记录,取它的某个字段值" | "在另一张表里按条件查找记录并聚合取值" |
#### lookup config(已验证)
```json
{
"associateField": "<本表中的关联字段 fieldIdbidirectionalLink/unidirectionalLink 类型)>",
"valuesField": "<关联目标表中要取值的字段 fieldId>",
"aggregator": "SUM|AVERAGE|COUNT|MAX|MIN|CONCATENATE"
}
```
创建示例:
```bash
dws aitable field create --base-id <baseId> --table-id <tableId> \
--name "关联名称" --type lookup \
--config '{"associateField":"<linkFieldId>","valuesField":"<targetFieldId>","aggregator":"CONCATENATE"}'
```
#### filterUp config(已验证)
```json
{
"targetSheet": "<目标表 tableId>",
"filters": [
{
"fieldId": "<目标表字段Id>",
"operator": "equal|contain",
"value": "<匹配值>",
"link": "AND"
}
],
"valuesField": "<目标表中要取值的字段Id>",
"aggregator": "SUM|AVERAGE|COUNT|MAX|MIN|CONCATENATE"
}
```
> `filters` 必须非空(至少一条筛选规则)。
> `filters[].operator` 仅支持:`equal`、`contain``not_equal`/`not_contain`/`is_empty` 等均不支持)。
> `filters[].link` 统一为 `"AND"` 或 `"OR"`。
### 5.5 创建前检查清单
1. 已通过 `field get` 确认所有引用字段的精确名称
2. 引用字段不包含 formula/lookup 等只读字段(可能导致二次计算延迟)
3. 公式语法正确(括号匹配、函数名正确)
4. 字段类型兼容(数值运算的字段确实是 number 类型)
## 6. 更新 formula 字段
```bash
dws aitable field update \
--base-id <baseId> \
--table-id <tableId> \
--field-id <fieldId> \
--config '{"formula": "[新字段A] + [新字段B]"}' \
--format json
```
更新时只需传新的 `formula` 表达式,系统会自动重新计算所有记录。
@@ -0,0 +1,56 @@
# 主键文档管理
## 适用场景
当需要为 AI 表格中的记录创建或查询关联的主键文档时使用。主键文档是 primaryDoc 类型字段对应的钉钉在线文档,可通过 `dws doc` 进行内容读写。
## 命令
### 查询主键文档
```bash
dws aitable +record-primary-doc-get --base-id BASE_ID --table-id TABLE_ID --record-id RECORD_ID
```
**参数:**
- `--base-id`(必填):Base ID
- `--table-id`(必填):Table ID
- `--record-id`(必填):Record ID
**返回:** `data.nodeId` — 主键文档的 nodeId,可直接传给 `dws doc read/update``--node` 参数。若该记录尚未创建主键文档,`nodeId` 为 null。
### 创建主键文档
```bash
dws aitable +record-primary-doc-create --base-id BASE_ID --table-id TABLE_ID --field-id FIELD_ID --record-id RECORD_ID
```
**参数:**
- `--base-id`(必填):Base ID
- `--table-id`(必填):Table ID
- `--field-id`(必填):主键字段 ID,必须是 primaryDoc 类型(通过 `dws aitable field get` 查看字段类型)
- `--record-id`(必填):Record ID
**返回:** `data.nodeId` — 创建或已存在的主键文档 nodeId。
**幂等性:** 若该记录已有主键文档,直接返回已有文档的 nodeId,不会重复创建。
## 注意事项
- `fieldId` 必须是 primaryDoc 类型,否则返回 `INVALID_FIELD_TYPE` 错误
- `primaryDoc` 是建表时的首字段能力,不能在已有普通首字段之后补建,也不能把普通字段改成 primaryDoc。需要该能力时应新建以 primaryDoc 为首字段的数据表并迁移数据;未经用户明确授权不要自动迁移。
- 传入不存在的 `recordId` 会返回 `RECORD_NOT_FOUND` 错误
- 创建后可通过 `dws doc update --node <nodeId>` 写入文档内容,或 `dws doc read --node <nodeId>` 读取
## 典型工作流
```bash
# 1. 查询字段目录,拿到 primaryDoc 字段的 fieldId
dws aitable field get --base-id BASE_ID --table-id TABLE_ID
# 2. 为记录创建主键文档
dws aitable +record-primary-doc-create --base-id BASE_ID --table-id TABLE_ID --field-id FIELD_ID --record-id RECORD_ID
# 3. 拿到返回的 nodeId,用 dws doc 写入内容
dws doc update --node <data.nodeId> --content "# 项目方案\n\n文档正文内容..."
```
@@ -0,0 +1,52 @@
# record create — 新增记录
## 命令格式
```
Usage:
dws aitable record create [flags]
Example:
dws aitable record create --base-id <BASE_ID> --table-id <TABLE_ID> \
--records '[{"cells":{"fldTextId":"文本内容","fldNumId":123}}]'
Flags:
--base-id string Base ID (必填)
--records string 记录列表 JSON 数组,单次最多 100 条 (必填,与 --records-file 二选一)
--records-file string 从文件读取 records JSON(替代 --records,适合超长数据或 Windows 环境)
--table-id string Table ID (必填)
```
## Windows / 超长 JSON 推荐
将 records JSON 写入文件,用 `--records-file ./records.json` 传入,避免命令行截断和引号转义问题。
## 常见错误(严格避免)
| 错误 | 说明 |
|------|------|
| 参数名用 `--data` | ❌ 参数名是 `--records`,不是 `--data` |
| cells key 用字段名 | ❌ cells key 必须是 fieldId(如 `fldXXX`),不是字段名称(如 `"课程名称"` |
| 不先获取 fieldId | ❌ 必须先 `field get` 获取 fieldId,再写入记录 |
| 单次超 100 条 | ❌ 单次最多 100 条,超过需分批 |
| 附件/图片字段直传 URL | ❌ 严禁 `{"url":"https://..."}` — 会触发 TIMEOUT_ERROR。必须先 `attachment upload` 获取 `fileToken`,再用 `{"fileToken":"ft_xxx"}` 写入。详见 [aitable-attachment.md](./aitable-attachment.md) |
## 正确流程
```bash
# 先获取 fieldId
dws aitable field get --base-id <BASE_ID> --table-id <TABLE_ID> --format json
# 从返回中提取 fieldId(如 fldABC123
# 再用 fieldId 写入记录
dws aitable record create --base-id <BASE_ID> --table-id <TABLE_ID> \
--records '[{"cells":{"fldABC123":"Python入门"}}]' --format json
# 从创建响应的 data.newRecordIds[] 提取新 ID,并回读确认真实写入值
dws aitable record query --base-id <BASE_ID> --table-id <TABLE_ID> \
--record-ids <NEW_RECORD_ID> --format json
```
创建成功以 `data.newRecordIds[]` 为 ID 来源;不要把整个 `data` 当作单个 recordId,也不要只以退出码作为写入成功证据。
## cells 写入格式
各字段类型的写入格式见 [aitable-cell-value.md](./aitable-cell-value.md)。
@@ -0,0 +1,20 @@
# record delete — 删除记录
## 命令格式
```
Usage:
dws aitable record delete [flags]
Example:
dws aitable record delete --base-id <BASE_ID> --table-id <TABLE_ID> --record-ids rec1,rec2 --yes
Flags:
--base-id string Base ID (必填)
--record-ids string 待删除记录 ID 列表,逗号分隔,最多 100 条 (必填)
--table-id string Table ID (必填)
```
## 注意事项
- **不可逆操作**,调用前建议先 `record query` 确认目标记录
- 需要先通过 `record query` 获取 recordId
- 单次最多删除 100 条记录
@@ -0,0 +1,97 @@
# 行记录变更历史(record history-list
按 recordId 查询单条记录的全部变更历史,用于审计、回溯字段变更、定位操作人。
## 命令
```
dws aitable record history-list \
--base-id BASE_ID --table-id TABLE_ID --record-id REC_ID \
[--offset N] [--limit M]
```
| flag | 说明 |
|------|------|
| `--base-id` | 所属 Base ID(必填,可用 `--base` 别名) |
| `--table-id` | 所属 Table ID(必填) |
| `--record-id` | 目标记录 ID(必填,单条;不支持批量) |
| `--offset` | 分页偏移量,默认 0 |
| `--limit` | 每页返回数量,范围 [1, 50],默认 20 |
## 返回结构
```jsonc
{
"data": {
"histories": [
{
"type": "field_change", // 变更类型
"action": "update", // 操作动作: create / update / delete
"newValue": "{\"...\":\"...\"}", // 变更后的值(JSON 字符串)
"oldValue": "{\"...\":\"...\"}", // 变更前的值(JSON 字符串)
"operateTime": 1733123456789, // 操作时间(毫秒级时间戳)
"typeChangedFields": "{...}", // 类型变更的字段信息(JSON 字符串)
"version": 7 // 版本号(单调递增)
}
]
}
}
```
`newValue` / `oldValue` / `typeChangedFields` 是 JSON 字符串(不是 JSON 对象),需要二次 `JSON.parse` 才能拿到结构化值。
## 字段含义速查
| 字段 | 用途 |
|------|------|
| `type` | 高层分类:`record_create` / `field_change` / `record_delete` 等。先按 type 过滤大类。 |
| `action` | 三态:`create` / `update` / `delete`。比 type 粗,但便于按"动作"统计。 |
| `version` | 单调递增整数;同一 record 越新值越大。**用作"上一条 vs 这一条"的稳定排序键**。 |
| `operateTime` | 毫秒时间戳;可格式化成可读时间。多条同 version 的极端场景用 operateTime 兜底排序。 |
## 典型用法
### 1. 看一条记录被改过几次
```bash
dws aitable record history-list --base-id BASE --table-id TBL --record-id REC --format json \
| jq '.data.histories[] | {version, action, operateTime}'
```
### 2. 翻页拉全量历史
```bash
# 第 1 页(最新 20 条)
dws aitable record history-list --base-id BASE --table-id TBL --record-id REC --limit 50 --offset 0
# 第 2 页
dws aitable record history-list --base-id BASE --table-id TBL --record-id REC --limit 50 --offset 50
```
`limit` 上限 50,需要更多请增加 `offset` 翻页。
### 3. 回溯某字段最近一次值
```bash
dws aitable record history-list --base-id BASE --table-id TBL --record-id REC --limit 50 --format json \
| jq '[.data.histories[] | select(.action == "update")][0].oldValue'
```
### 4. 找出删除事件(如果存在 delete history
```bash
dws aitable record history-list --base-id BASE --table-id TBL --record-id REC --format json \
| jq '.data.histories[] | select(.action == "delete") | {version, operateTime}'
```
## 注意事项
- 一次只能查一条 record;如需批量审计多条记录请循环调用。
- 仅返回**字段值变更**与**记录生命周期事件**;视图、字段定义、表结构变更不在此 history 里。
- 历史保留时长由 server 决定,过老的记录可能不再返回。
## 与其他 record 命令的关系
- 想看记录"现在长什么样" → `record query` / `record get`
- 想看记录"过去长什么样、什么时候改的" → `record history-list`(本命令)
- 想看"这张表整体改过什么" → 当前 CLI 不支持表级 history;只能逐 record 查
@@ -0,0 +1,47 @@
# 行命名规则枚举键(recordNameKey)映射
`dws aitable table update --record-name-key <枚举键>` 用于设置数据表的"行命名规则"——卡片/详情页里"行"的展示别名。**取值是固定枚举,不是字段 ID**;传非法值服务端返回 `INVALID_RECORD_NAME_KEY`
## 中文 → 枚举键(按 UI 下拉顺序)
| 用户说 | --record-name-key | 用户说 | --record-name-key |
|---|---|---|---|
| 记录 | `ji_lu`(默认) | 项目 | `project` |
| 任务 | `task` | 事件 | `event` |
| 请求 | `request` | 活动 | `campaign` |
| 目标 | `objective` | 交付物 | `deliverable` |
| 资产 | `asset` | 客户 | `customer` |
| 订单 | `order` | 联系人 | `contact` |
| 物料/物品 | `item` | 问题 | `question``issue` |
| 工单 | `ticket` | 候选人 | `candidate` |
| 商机/机会 | `opportunity` | 会议 | `meeting` |
| 成员 | `member` | OKR | `okr` |
## 其他常用键(按场景分组)
- **业务流程**`approval` / `application` / `case` / `decision` / `delivery` / `payment` / `purchase_order` / `quote` / `release`
- **HR / 财务**`employee` / `expense` / `budget` / `invoice`
- **产品 / 研发**`feature` / `feedback` / `idea` / `bug` / `requirement` / `risk` / `sprint` / `story` / `subtask` / `epic`
- **CRM**`account` / `lead` / `prospect` / `deal`
- **运营 / 支持**`note` / `report` / `topic` / `session` / `service`
- **资源 / 通用**`file` / `document` / `product` / `team` / `user` / `vendor` / `key_result` / `metric`
完整集合较大(共 273 个),服务端校验;以上未列出的合法键也可直接传(如 `goal` / `okr` / `pillar` / `phase` / `milestone` 等)。
## 使用示例
```bash
# 用户说"把这张表的行叫'任务'吧" → 传 task
dws aitable table update --base-id BASE --table-id TBL --record-name-key task
# 用户说"换成项目" → 传 project
dws aitable table update --base-id BASE --table-id TBL --record-name-key project
# 用户说"恢复成默认(记录)" → 传 ji_lu
dws aitable table update --base-id BASE --table-id TBL --record-name-key ji_lu
```
## 注意
- recordNameKey **不会在 `table get` 响应里回显**`get_tables` DTO 设计上不暴露该字段);写入是否成功以 `table update` 的 set response 是否回填 `recordNameKey` 字段为准。
- 中文别名是 server 内置 i18n,UI 显示用户对应的国际化文案,CLI 必须传英文枚举键。
@@ -0,0 +1,120 @@
# record query — 查询记录
## 命令格式
```
Usage:
dws aitable record query [flags]
Example:
dws aitable record query --base-id <BASE_ID> --table-id <TABLE_ID>
dws aitable record query --base-id <BASE_ID> --table-id <TABLE_ID> --record-ids rec1,rec2
dws aitable record query --base-id <BASE_ID> --table-id <TABLE_ID> --query "关键词" --limit 50
Flags:
--base-id string Base ID (必填)
--cursor string 分页游标,首次不传
--field-ids string 返回字段 ID 列表,逗号分隔,单次最多 100 个
--filters string 结构化过滤条件 JSON
--query string 全文关键词搜索
--limit int 单次最大记录数,默认 100,最大 100
--record-ids string 指定记录 ID 列表,逗号分隔,单次最多 100 个
--sort string 排序条件 JSON 数组
--table-id string Table ID (必填)
--all 启用自动翻页,循环获取并合并所有记录后统一输出
--page-limit int 自动翻页最大页数(仅 --all 时生效)。默认 50,设为 0 表示无限制
```
两种模式: 按 ID 取(传 record-ids,忽略 filters/sort)或条件查(filters+sort+cursor 分页)。
## 自动翻页(--all + --page-limit
- 传入 `--all` 启用自动翻页,CLI 自动循环获取并合并所有记录后统一输出
- `--page-limit` 控制最大翻页次数,默认 50 页(5000 条),设为 0 表示无限制
- 页间间隔 200ms,中途网络错误会 graceful stop 并输出已获取的数据
- **被截断时**(达到 page-limit 但仍有数据):输出中包含 `"hasMore": true``"cursor": "..."` 字段,可通过 `--cursor` 从断点继续拉取
- 适用于需要一次性获取全量数据的场景(如导出、统计、批量处理)
```bash
# 默认(最多 50 页 = 5000 条)
dws aitable record query --base-id X --table-id Y --all
# 无限制(拉完为止)
dws aitable record query --base-id X --table-id Y --all --page-limit 0
# 从上次断点继续
dws aitable record query --base-id X --table-id Y --all --cursor "上次返回的cursor"
```
## 排序参数规范
`--sort` 需要传 JSON 数组,排序方向字段必须是 `direction``asc``desc`),不要使用 `order`
正确示例:
```bash
--sort '[{"fieldId":"wm8ns9bw2vmucb45xj3ix","direction":"desc"}]'
```
## filters 结构
详细语法见 [aitable-filter-sort.md](./aitable-filter-sort.md)。
快速模板:
```json
{"operator":"and","operands":[{"operator":"eq","operands":["<fieldId>","<value>"]}]}
```
> **singleSelect/multipleSelect 过滤**filters 中可传 option id 或 option name,但建议优先用 **option id**(通过 `field get` 获取),更可靠。
## 减少响应体积
字段较多时,用 `--field-ids` 仅返回需要的字段,可显著减少返回数据量。
## 常见错误
- `--filters` 根节点直接用 `"operator":"eq"` → API 静默忽略,返回全表
- `--sort``"order":"desc"` → 必须用 `"direction":"desc"`
- 不加 `--field-ids` 拉全字段 → 大表响应体积过大
- 全量拉取后在 context 里手动统计 → 应优先用 `--filters` 服务端过滤
## record query-empty — 找空行
`record query-empty` 是与 `record query` 平行的独立子命令,专门按表内顺序扫描出"完全没填用户字段"的空行。
```bash
dws aitable record query-empty --base-id BASE_ID --table-id TABLE_ID
```
| flag | 说明 |
|------|------|
| `--base-id` / `--base` | 必填 |
| `--table-id` | 必填 |
| `--limit` | 单次**扫描预算**(不是返回数);范围 [1, 100],默认 100 |
| `--cursor` | 分页游标。响应中 `nextCursor` 非空 → 用它翻页继续扫;nextCursor 为空(或不存在)→ 已扫完整表 |
返回结构:
```jsonc
{ "data": { "records": [...], "nextCursor": "..." } }
```
### 关键语义
1. **`--limit` 是扫描预算不是返回数**:可能扫了 100 条但全部非空,本页 `records: []`
2. **本页空 records ≠ 全表无空行**:必须看 `nextCursor`nextCursor 还在就要继续翻。
3. **空行定义**:除系统字段(recordId / 创建人 / 创建时间 / 修改人 / 修改时间)外,所有 cell 都是 null、空字符串、空集合或空 Map。一般是用户在 UI 上"插入空行"产生的。
### 典型用法
```bash
# 扫一页,看本页有没有空行
dws aitable record query-empty --base-id BASE --table-id TBL
# 翻页
dws aitable record query-empty --base-id BASE --table-id TBL --cursor <上次的nextCursor>
# 把整表扫完(手动循环 cursor)
NC=""
while : ; do
R=$(dws aitable record query-empty --base-id BASE --table-id TBL ${NC:+--cursor "$NC"} --format json)
echo "$R" | jq '.data.records[] | .recordId'
NC=$(echo "$R" | jq -r '.data.nextCursor // empty')
[ -z "$NC" ] && break
done
```
@@ -0,0 +1,54 @@
# 行记录分享链接(record share-url
按 recordId 批量获取记录的分享链接,把某行单独发给同事查看。
## 命令
```
dws aitable record share-url \
--base-id BASE_ID --table-id TABLE_ID \
--record-ids rec1,rec2,rec3 \
[--view-id VIEW_ID]
```
| flag | 说明 |
|------|------|
| `--base-id` | 所属 Base ID(必填,可用 `--base` 别名) |
| `--table-id` | 所属 Table ID(必填) |
| `--record-ids` | 目标 Record ID 列表,CSV 逗号分隔,**单次最多 20 条**(必填) |
| `--view-id` | 视图 ID(可选)。带上后链接打开会落在该视图上下文里 |
## 返回结构
```jsonc
{
"data": {
"items": [
{ "recordId": "rec1", "shareUrl": "https://..." },
{ "recordId": "rec2", "shareUrl": "https://..." }
]
}
}
```
`shareUrl` 为 null 表示该条获取失败(不影响其他条目)。
## 典型用法
```bash
# 一次拿一条记录的链接
dws aitable record share-url --base-id BASE --table-id TBL --record-ids rec1
# 批量拿,配合 jq 过滤出 url
dws aitable record share-url --base-id BASE --table-id TBL --record-ids rec1,rec2,rec3 --format json \
| jq '.data.items[] | {recordId, shareUrl}'
# 带视图上下文(链接打开时落在指定视图)
dws aitable record share-url --base-id BASE --table-id TBL --record-ids rec1 --view-id viw_VIP
```
## 注意事项
- **单次最多 20 条**,超出请客户端拆批。
- 该链接是分享链接(不是源文档链接),打开后看到的是该 record 的只读详情页。
- 取消单条分享 / 关闭整表分享当前 CLI 不支持,需要在 AI 表格 Web 端操作。
@@ -0,0 +1,59 @@
# record stats / group-stats — 服务端聚合统计
统计任务优先使用服务端聚合,不要先用 `record query --all` 下载全表再计算。
## 命令选择
| 需求 | 命令 | 底层接口 |
|------|------|----------|
| 总数、求和、平均值、最大/最小值、中位数、完整率等标量统计 | `record stats` | `query_records_stats` |
| 按字段分组统计 | `record group-stats` + `--group` | `query_stats` |
| 满足条件的唯一门店/客户/商品数量 | `record group-stats` + `distinct`,不传 `--group` | `query_stats` |
## 不分组统计
```bash
dws aitable record stats \
--base-id <BASE_ID> \
--table-id <TABLE_ID> \
--stats '[{"fieldId":"<FIELD_ID>","statsType":"COUNT"}]' \
--format json
```
- `statsType` 必须大写。
- `--stats` 单次最多 20 项,同一 `fieldId` 不得重复;同字段多个指标拆成多次调用。
- 支持基础类型 `COUNT``COUNT_COLUMN``SUM``AVG``MAX``MIN`,以及运行时支持的 `MEDIAN``STANDARD_DEVIATION``RANGE``DISTINCT``DISTINCT_RATIO`、完整率、勾选率和日期统计类型。
- 统计全部匹配记录时省略 `--limit`;传入 limit 会改变统计范围。
- 可选参数:`--filters``--sort``--keyword``--search-field-ids``--data-version`
## 分组或去重统计
```bash
dws aitable record group-stats \
--base-id <BASE_ID> \
--table-id <TABLE_ID> \
--group '[{"fieldId":"<GROUP_FIELD_ID>","direction":"ASC","fieldConfig":null,"arraySplitMode":true}]' \
--stats '[{"fieldId":"<VALUE_FIELD_ID>","statsType":"avg"}]' \
--limit 1000 \
--format json
```
- `statsType` 必须小写;基础类型为 `sum``avg``count``max``min`,后端还可能支持 `median``distinct``distinct_ratio` 等高级类型。
- `--group``--sort` 都是 JSON 数组编码后的字符串,CLI 会原样映射到 MCP 的 `group` / `sortDsl`
- 分组结果最多 1000 行。不要依赖服务端 limit 选择 Top N;应在基数不超过 1000 时取完整分组结果后排序。
- 条件唯一实体计数不传 `--group`,对实体字段使用 `distinct`
## 过滤条件
```bash
--filters '{"operator":"and","operands":[{"operator":"gt","operands":["fldAmount",0]}]}'
```
- 根节点必须是 `and` / `or`
- `lt``gt``lte``gte` 的值必须是 JSON 数字,不能写成数字字符串。
- 单选/多选字段建议使用 `field get` 返回的 option ID。
- 所有 Base、table、field 和 option ID 都必须从当前目标 Base 的实时元数据取得,不能复用示例或历史 ID。
## 降级边界
只有用户要求记录明细、少量校验样本、精确分位数输入,或聚合接口明确失败时,才使用 `record query`。需要先逐行运算再聚合的指标必须依赖表内已有且可直接聚合的公式字段;没有该字段时停止并请用户先在 AI 表格页面创建,不能遍历全表本地二次计算。
@@ -0,0 +1,66 @@
# record update — 更新记录
## 命令格式
```
Usage:
dws aitable record update [flags]
Example:
dws aitable record update --base-id <BASE_ID> --table-id <TABLE_ID> \
--records '[{"recordId":"recXXX","cells":{"fldStatusId":"已完成"}}]'
Flags:
--base-id string Base ID (必填)
--records string 待更新记录 JSON 数组,单次最多 100 条;cells key 支持 fieldId 或当前表内唯一字段名,推荐 fieldId (必填,与 --records-file 二选一)
--records-file string 从文件读取 records JSON(替代 --records,适合超长数据或 Windows 环境)
--table-id string Table ID (必填)
```
只需传入需修改的字段,未传入的保持原值。每条记录必须含 recordId 和 cells。
## cells key:优先使用 fieldId,也支持唯一字段名
`cells` 的 key 有两种写法:
- fieldId(推荐):不受字段重命名或重名影响,通过 `field get` 获取。
- 当前表内唯一的字段名:按名称精确匹配;如果存在同名字段,必须改用 fieldId。
同一字段同时通过 fieldId 和字段名传入时,fieldId 对应的值优先。
```bash
# 推荐:fieldId
dws aitable record update --base-id <BASE_ID> --table-id <TABLE_ID> \
--records '[{"recordId":"recXXX","cells":{"fldStatusId":"已完成"}}]' --format json
# 便捷写法:当前表内唯一字段名
dws aitable record update --base-id <BASE_ID> --table-id <TABLE_ID> \
--records '[{"recordId":"recXXX","cells":{"状态":"已完成"}}]' --format json
```
## 推荐参数形式
公开、稳定的批量入口是 `--records`(或 `--records-file`),格式为 JSON 数组;即使只改一条记录,推荐也包在数组里。CLI 仍保留隐藏的 `--record-id` + `--cells` 兼容入口,但它不会出现在常规帮助中,自动化脚本应优先使用 `--records`
| 不推荐或无效写法 | 推荐写法 |
|---|---|
| `--record-id recXXX --cells '{"fldX":"值"}'`(隐藏兼容入口) | `--records '[{"recordId":"recXXX","cells":{"fldX":"值"}}]'` |
| `--id recXXX --data '{"fldX":"值"}'` | 同上 |
| `--record-id recXXX --field fldX --value "新值"` | 同上 |
## 单条更新模板(直接复制)
```bash
dws aitable record update --base-id <BASE_ID> --table-id <TABLE_ID> \
--records '[{"recordId":"<RECORD_ID>","cells":{"<FIELD_ID>":"新值"}}]' --format json
# 从更新响应的 data.recordIds[] 提取成功记录 ID,并回读确认真实值
dws aitable record query --base-id <BASE_ID> --table-id <TABLE_ID> \
--record-ids <RECORD_ID> --format json
```
更新响应不返回“受影响字段”;以 `data.recordIds[]` 确定成功记录,再用查询回读验证。
## 引号转义提示
- Linux/macOS:外层用单引号 `'[...]'`,内部 JSON 用双引号即可
- Windows PowerShell:外层用双引号 `"[...]"`,内部双引号需转义为 `\"`
- 或将 JSON 写入临时文件,用 `--records-file ./records.json` 规避转义
@@ -0,0 +1,89 @@
# 行记录 Upsertrecord upsert
`recordId` 是否存在,自动把入参拆分到 update 链路或 create 链路:批次混合"已存在改 + 新出现建"时用,省掉客户端按 ID 分批的逻辑。
## 命令
```
dws aitable record upsert \
--base-id BASE_ID --table-id TABLE_ID \
--records '[{"recordId":"<可选>","cells":{...}}, ...]'
```
| flag | 说明 |
|------|------|
| `--base-id` | 必填(可用 `--base` 别名) |
| `--table-id` | 必填 |
| `--records` | 待 upsert 的记录 JSON 数组,**单次最多 100 条**(必填)|
| `--records-file` | 从文件读入(命令行 JSON 太长时用),与 `--records` 互斥优先级更高 |
## --records 结构
每项 JSON
```jsonc
{
"recordId": "rec1", // 可选;带 → update,缺省 → create
"cells": { // 必填;key 是 fieldIdvalue 按字段类型
"fldTitleId": "新标题",
"fldNumberId": 42
}
}
```
`cells` 写入格式与 `record create` / `record update` **完全一致**key 必须是 fieldId 不是字段名;按字段类型见 [aitable-cell-value.md](./aitable-cell-value.md))。
## 返回结构
```jsonc
{
"data": {
"createdRecordIds": ["recX", "recY"], // 不带 recordId 的项产出
"updatedRecordIds": ["recA", "recB"] // 带 recordId 的项产出
}
}
```
`createdRecordIds` 顺序对应入参里**不带 recordId**的项(按出现顺序汇总),同理 `updatedRecordIds` 对应**带 recordId**的项。
## 典型用法
```bash
# 1) 全部新建:所有项都不带 recordId
dws aitable record upsert --base-id BASE --table-id TBL --records '[
{"cells":{"fldTitleId":"任务1","fldStatusId":"待办"}},
{"cells":{"fldTitleId":"任务2","fldStatusId":"待办"}}
]'
# 2) 全部更新:所有项都带 recordId
dws aitable record upsert --base-id BASE --table-id TBL --records '[
{"recordId":"rec1","cells":{"fldStatusId":"已完成"}},
{"recordId":"rec2","cells":{"fldStatusId":"已完成"}}
]'
# 3) 混合:第 1 条更新(带 recordId),第 2 条创建(不带)
dws aitable record upsert --base-id BASE --table-id TBL --records '[
{"recordId":"rec1","cells":{"fldStatusId":"已完成"}},
{"cells":{"fldTitleId":"新增任务","fldStatusId":"待办"}}
]'
# 4) 长 JSON 用文件
dws aitable record upsert --base-id BASE --table-id TBL --records-file ./batch.json
```
## 与 record create / record update 的关系
| 场景 | 命令 |
|------|------|
| 确定全是新增 | `record create` |
| 确定全是更新(每条独立 cells) | `record update` |
| 确定全是更新(共享同一 cells) | `record batch-update` |
| **不确定有没有,按 recordId 自动分流** | `record upsert`(本命令) |
`record upsert``--records` 入参格式与 `record update` 完全相同,唯一差别是 `recordId` 字段在 upsert 里是可选的。如果批次确定全是更新或全是新建,用专用命令更清晰;批次混合时(典型场景:定时同步外部数据,源里既有已存在的也有新出现的),用 upsert。
## 注意事项
- **单次最多 100 条**(创建 + 更新合计),超出请客户端拆批。
- `cells` 的 key 必须是 fieldId 不是字段名(先用 `record query``field get` 拿 fieldId)。
- 只读字段(formula / lookup / 系统字段)不能写入 — upsert 链路与 update 链路同样限制。
@@ -0,0 +1,243 @@
# 视图配置(view get/update <attr>
按属性局部读/写视图配置。每个属性独立子命令,typed flag 友好,agent 不必拼 JSON。
向后兼容:`view update --config '{...}'` 一次多属性入口仍可用。
## viewType × 支持矩阵
| viewType | card | timebar | aggregate | filter / sort / group | visible-fields | field-widths | name |
|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
| Grid | | | ✅ | ✅ | ✅ | ✅ | ✅ |
| Kanban | ✅ | | | ✅ | ✅ | | ✅ |
| Gallery | ✅ | | | ✅ | ✅ | | ✅ |
| Gantt | | ✅ | | ✅ | ✅ | | ✅ |
| Calendar | | | | ✅ | ✅ | | ✅ |
| FormDesigner | (走 `form` 系列命令) | | | | | | |
> card 在 Kanban 走 `kanbanCard`,在 Gallery 走 `galleryCard`CLI 自动按 viewType dispatchpreflight 1 次 `get_views`)。timebar 仅 Gantt 支持;Calendar 服务端未暴露任何 timebar 配置。
> **Gantt 视图必须两步创建**`view create --view-type Gantt` 只创建空壳(`ganttTimebar: {}`),**必须**紧跟 `view update timebar --start-field <日期字段ID>` 绑定时间轴字段,否则视图打开是空白。`view create --config` 不接受 `ganttTimebar`,请在创建后使用专属子命令。
## 创建:view create
`--view-type` 支持 `Grid``Kanban``Gantt``Calendar``Gallery``FormDesigner`。创建时通过 `--config` JSON 设置可见字段:
```bash
# --config JSON:可同时配置可见字段、筛选、排序和分组;主字段必须排第一
dws aitable view create --base-id BASE_ID --table-id TABLE_ID \
--view-type Grid --name "任务视图" \
--config '{"visibleFieldIds":["fldPrimary","fldStatus","fldOwner"],"sort":[{"fieldId":"fldStatus","direction":"asc"}]}'
```
创建阶段的 `--config` 是 JSON 对象,并且只接受以下 4 个 key:
| key | 类型 | 说明 |
|---|---|---|
| `visibleFieldIds` | `string[]` | fieldId 数组,不接受字段名;至少一个,主字段必须排第一 |
| `filter` | `object[]` | 筛选规则数组;兼容单个 object,CLI 会自动包装为数组 |
| `sort` | `object[]` | 排序规则数组;兼容单个 object,CLI 会自动包装为数组 |
| `group` | `object[]` | 分组规则数组;兼容单个 object,CLI 会自动包装为数组 |
描述使用独立的 `--desc '{"content":[]}'``description``fieldWidths``aggregate``kanbanCard``ganttTimebar``galleryCard` 等其他 key 会在调用服务端前被拒绝,并提示对应的 `view update` 子命令。
## 读取:view get <attr>
所有 `view get <attr>` 共用 `--base-id` / `--table-id` / `--view-id`,输出是该属性子块的 JSON(不存在时输出 `{}`)。viewType 不匹配会报错并指明应该选哪种视图。
```bash
dws aitable view get card --view-id VIEW_ID --format json # Kanban / Gallery
dws aitable view get timebar --view-id VIEW_ID --format json # Gantt
dws aitable view get aggregate --view-id VIEW_ID --format json # Grid
dws aitable view get filter --view-id VIEW_ID --format json # 所有
dws aitable view get sort --view-id VIEW_ID --format json
dws aitable view get group --view-id VIEW_ID --format json
dws aitable view get visible-fields --view-id VIEW_ID --format json
dws aitable view get field-widths --view-id VIEW_ID --format json # Grid
```
## 写入:view update <attr>
所有 `view update <attr>` 共用 `--base-id` / `--table-id` / `--view-id`
**typed flag + `--json` 可混用**;冲突时 typed flag 优先并 stderr 提示。
card / timebar / aggregate 三类写入有 viewType 校验(preflight 1 次 get_views)。
### view update cardKanban / Gallery
服务端按 viewType 分发到 `kanbanCard``galleryCard`。typed flag 共享。
| flag | 类型 | 说明 |
|------|------|------|
| `--cover-field-id` | string | 封面字段 IDKanban / Gallery 通用),与 `--no-cover` 互斥 |
| `--no-cover` | bool | 清除封面(等价 `coverFieldId="NONE"` |
| `--cover-resize-mode` | string | `cover` / `contain` / `stretch` |
| `--hidden-field-title` | bool | 隐藏字段名标题(仅 Kanban 生效) |
| `--cover-mode` | string | `none` / `auto` / `custom`(仅 Gallery 生效) |
| `--display-field-name` | bool | 是否显示字段名(仅 Gallery 生效) |
| `--json` | JSON | 完整 card 子块对象 |
```bash
dws aitable view update card --view-id KANBAN_ID --cover-field-id fldAttachment --cover-resize-mode contain
dws aitable view update card --view-id KANBAN_ID --no-cover
dws aitable view update card --view-id GALLERY_ID --cover-mode auto
dws aitable view update card --view-id GALLERY_ID --json '{"coverMode":"custom","coverFieldId":"fldX","displayFieldName":true}'
```
### view update timebar(仅 Gantt
| flag | 类型 | 说明 |
|------|------|------|
| `--start-field` | string (date fieldId) | 开始日期字段 |
| `--end-field` | string (date fieldId) | 结束日期字段 |
| `--display-field-id` | string | 时间条上显示的标题字段 |
| `--timeline-scale` | string | `year` / `quarter` / `month` / `weeks` |
| `--color-configs` | JSON 数组 | 颜色配置数组(结构由下游协议定义;清空传 `[]` |
| `--official-holiday` | bool | 是否标注法定节假日 |
| `--json` | JSON | 完整 ganttTimebar 子块 |
```bash
dws aitable view update timebar --view-id GANTT_ID --start-field fldStart --end-field fldEnd --timeline-scale month
dws aitable view update timebar --view-id GANTT_ID --official-holiday=true
```
### view update aggregate(仅 Grid
值是 `map[fieldId]→AggregateAction string`;传 null 清除某个字段聚合。
| flag | 类型 | 说明 |
|------|------|------|
| `--field-id` | string | 配合 `--action` 设置**单字段**聚合 |
| `--action` | string | `SUM`/`AVG`/`MAX`/`MIN`/`MEDIAN`/`RANGE`/`TOTAL`/`DISTINCT`/`EXIST`/`UN_EXIST`/`CHECKED`/`EARLIEST_DATE` 等(按字段类型可用) |
| `--clear-field-id` | string (CSV) | 一/多个字段 ID,清除其聚合 |
| `--json` | JSON | 完整 aggregate map |
```bash
dws aitable view update aggregate --view-id GRID_ID --field-id fldX --action SUM
dws aitable view update aggregate --view-id GRID_ID --clear-field-id fldA,fldB
dws aitable view update aggregate --view-id GRID_ID --json '{"fldX":"AVG","fldY":null}'
```
### view update field-widths(仅 Grid
| flag | 类型 |
|------|------|
| `--field-id` + `--width` | string + int(单字段) |
| `--json` | `{fldId: width, ...}` |
```bash
dws aitable view update field-widths --view-id GRID_ID --field-id fldX --width 200
dws aitable view update field-widths --view-id GRID_ID --json '{"fldA":120,"fldB":200}'
```
### view update visible-fields(通用)
整组替换可见字段列表与顺序。`field get` 返回的第一个字段是系统行索引/主字段;无论它显示为 text 还是 primaryDoc,都必须保留在数组第一位,且不能隐藏。不要仅凭字段类型猜主字段。
> ⚠️ 注意:服务端**只接受 reorder,不接受真"隐藏字段"**——如果传入的列表比当前 columns 短,缺失的字段不会被隐藏。需要真正隐藏字段请到 AI 表格 Web UI。
| flag | 类型 |
|------|------|
| `--field-ids` | string (CSV) |
| `--json` | string 数组 JSON(与 `--field-ids` 同传时 `--json` 优先) |
```bash
dws aitable view update visible-fields --view-id VIEW_ID --field-ids fldPrimary,fldA,fldB
dws aitable view update visible-fields --view-id VIEW_ID --json '["fldPrimary","fldA","fldB"]'
```
### 列顺序最短闭环
用户说“客户名称最左、状态在金额前”时,不要用通用 `+view-update --config` 探索:
1. `dws aitable field get --base-id <B> --table-id <T> --format json` 取字段有序列表;第一个 fieldId 固定为数组第 1 项。目标 viewId 从真实上下文或 `view get` 返回中取得。
2. `dws aitable view get visible-fields ...` 取当前完整列数组;必须保留全部现有字段,因为该接口只支持 reorder,不是真隐藏。
3. 只重排目标:`[主字段, 客户名称, ..., 状态, 金额, ...]`,其他字段保持相对顺序;一次执行 `view update visible-fields`
4. 再次 `view get visible-fields`,数组完全一致才算完成。遇到 `PRIMARY_FIELD_CANNOT_BE_MOVED/HIDDEN` 立即停止,重新按步骤 1 构造一次;禁止继续猜排列。
“固定/冻结左侧列”与“放到最左边”不是同一操作。只有 Grid 支持冻结;若要冻结主字段后的目标列,需要冻结前 N 列(例如目标位于第 2 列则 count=2):
```bash
dws aitable +view-set-frozen-cols --base-id <B> --table-id <T> --view-id <V> --count <N>
dws aitable +view-get-frozen-cols --base-id <B> --table-id <T> --view-id <V>
```
Kanban/Gallery 等视图只调整列顺序,不尝试冻结。
### view update filter / sort / group(通用,纯 --json
```bash
dws aitable view update filter --view-id VIEW_ID --json '[{"operator":"and","operands":[{"operator":"eq","operands":["fldX","value"]}]}]'
dws aitable view update sort --view-id VIEW_ID --json '[{"fieldId":"fldX","direction":"asc"}]'
dws aitable view update group --view-id VIEW_ID --json '[{"fieldId":"fldX","direction":"asc"}]'
```
> filter/sort/group 入参格式与 `record query --filters`(对象格式)**不同**view config 这边外层必须是数组。传对象 CLI 会自动 wrap,建议直接用数组。详见 [aitable-filter-sort.md](./aitable-filter-sort.md)。
### view update name(重命名)
```bash
dws aitable view update name --view-id VIEW_ID --name "新视图名"
```
等价于 `dws aitable view update --view-id VIEW_ID --name "新视图名"`,无 `config` 参数。
## 服务端字段速查(与 dws CLI 关系)
| dws 子命令 | 服务端 `update_view.config` 子键 | 服务端 Java 模型 |
|---|---|---|
| `view update card`Kanban | `kanbanCard` | `KanbanCardUpdateInput` |
| `view update card`Gallery | `galleryCard` | `GalleryCardUpdateInput` |
| `view update timebar` | `ganttTimebar` | `GanttTimebarUpdateInput` |
| `view update aggregate` | `aggregate` | `Map<fieldId, AggregateAction>` |
| `view update visible-fields` | `visibleFieldIds` | `List<String>` |
| `view update filter / sort / group` | `filter` / `sort` / `group` | `List<Object>` |
| `view update field-widths` | `fieldWidths` | `Map<String, Object>` |
| `view update name` | (不在 config 内)`newViewName` 顶层 | — |
## 典型工作流
### 排查"Kanban 卡片为啥不显示封面"
```bash
dws aitable view get card --view-id KANBAN_ID --format json
# → 看 coverFieldId 是不是 "NONE" 或缺失;不是再看 coverResizeMode 是不是 contain 导致裁掉
```
### 创建可用的 Gantt 视图(必须两步)
```bash
# 第 1 步:创建 Gantt 视图
dws aitable view create --base-id BASE_ID --table-id TABLE_ID \
--view-type Gantt --name "项目甘特图" -f json
# → 记录返回的 viewId
# 第 2 步(必须):绑定日期字段,否则视图为空
dws aitable view update timebar --base-id BASE_ID --table-id TABLE_ID \
--view-id VIEW_ID --start-field fldDateStart
# 可选:加结束日期、标题字段、时间尺度
# --end-field fldDateEnd --display-field-id fldName --timeline-scale month
```
### 把 Gantt 时间轴改成季度尺度并加节假日
```bash
dws aitable view update timebar --view-id GANTT_ID \
--timeline-scale quarter --official-holiday=true
```
### 用 dws 脚本批量替换 Kanban 封面字段
```bash
for v in viw1 viw2 viw3; do
dws aitable view update card --view-id $v --cover-field-id fldNewCover --cover-resize-mode cover --format json | jq .status
done
```
### 一次性多属性更新(仍走 legacy --config
```bash
dws aitable view update --view-id VIEW_ID --config '{
"visibleFieldIds":["fldPrimary","fldA","fldB"],
"filter":[{"operator":"and","operands":[]}],
"kanbanCard":{"coverFieldId":"fldImg","coverResizeMode":"contain"}
}'
```
@@ -0,0 +1,198 @@
# 视图扩展操作(lock / frozen-cols / row-height / fill-color-rule / duplicate
本文档讲 5 项视图操作命令:
- 锁定 / 解锁视图:`view lock` / `view get lock`
- 冻结列:`view update frozen-cols` / `view get frozen-cols`
- 行高:`view update row-height` / `view get row-height`
- 数据高亮规则(条件填色):`view update fill-color-rule` / `view get fill-color-rule`
- 复制视图:`view duplicate`
> **与 [aitable-view-config.md](./aitable-view-config.md) 的分工**
> - `aitable-view-config.md` 讲 `view get/update <attr>` 中 8 个属性:filter / sort / group / visible-fields / field-widths / aggregate / card / timebar。
> - 本文档讲上面 5 项额外能力(包括 attr 形式的 frozen-cols / row-height / fill-color-rule,以及顶层独立的 lock / duplicate)。
> 这 5 项**不能**通过 `view update --config '{...}'` 写入,必须用各自专属子命令。
## 命令矩阵
| 子命令 | 用途 | 必填参数 | 适用 viewType |
|---|---|---|---|
| `view lock [--off]` | 锁定(默认)/ 解锁视图 | `--base-id --table-id --view-id` | 全部 |
| `view get lock` | 读取锁定状态 | `--base-id --table-id --view-id` | 全部 |
| `view update frozen-cols --count N` | 冻结左侧 N 列(0 取消) | `--base-id --table-id --view-id --count` | Grid |
| `view get frozen-cols` | 读取冻结列数 | `--base-id --table-id --view-id` | Grid |
| `view update row-height --cell-height N` | 设置单元格高度(像素) | `--base-id --table-id --view-id --cell-height` | Grid |
| `view get row-height` | 读取单元格高度 | `--base-id --table-id --view-id` | Grid |
| `view update fill-color-rule --json '[...]'` | 全量覆盖条件填色规则 | `--base-id --table-id --view-id --json` | Grid |
| `view get fill-color-rule` | 读取条件填色规则 | `--base-id --table-id --view-id` | 全部(其他视图返回 `[]` |
| `view duplicate [--new-name X]` | 复制视图 | `--base-id --table-id --view-id` | 全部 |
## 视图锁定 / 解锁
```bash
# 锁定(默认)
dws aitable view lock --view-id VIEW_ID
# 解锁
dws aitable view lock --view-id VIEW_ID --off
# 查询当前是否锁定
dws aitable view get lock --view-id VIEW_ID --format json
# → {"data": {"baseId": ..., "tableId": ..., "viewId": ..., "locked": true|false}}
```
锁定的视图禁止他人修改其配置(filter/sort/group/字段顺序等),但记录读写不受影响。锁定状态可重复 set,幂等。
## 冻结列(仅 Grid
```bash
# 冻结从首列起 1 列
dws aitable view update frozen-cols --view-id VIEW_ID --count 1
# 取消冻结
dws aitable view update frozen-cols --view-id VIEW_ID --count 0
# 查询当前冻结列数
dws aitable view get frozen-cols --view-id VIEW_ID --format json
# → {"data": {..., "count": 1}} count 为 null 表示视图未显式设置
```
`--count` 必须 ≥ 0;负数会被拒绝。
## 行高(仅 Grid
⚠️ **`--cell-height` 只接受 4 档枚举:32 / 56 / 88 / 128**(与前端 CELL_HEIGHTS 约定一致),其他值会被拒绝。默认值为 32。
```bash
# 设置行高 — 推荐档位 32 / 56 / 88 / 128
dws aitable view update row-height --view-id VIEW_ID --cell-height 56
# 查询当前行高
dws aitable view get row-height --view-id VIEW_ID --format json
# → {"data": {..., "cellHeight": 56}} cellHeight 为 null 表示视图未显式设置(前端按 32 渲染)
```
## 数据高亮规则(条件填色,仅 Grid)
`view update fill-color-rule` **整组覆盖**,传 `--json '[]'` 清空所有规则。
### 规则结构
每条规则 JSON 结构:
```jsonc
{
"type": "cell" | "row" | "column" | "preRow",
"formatFieldId": "fldX", // 命中规则后被高亮的字段(cell/column 类型有意义)
"format": { "color": "firstLine5" }, // ⚠️ 必须用 FORMAT_COLORS 代号,不接受 hex
"filters": [ // 当前固定 1 条
{
"fieldId": "fldX", // ⚠️ 不是 operands[0]
"symbol": "GT", // ⚠️ 不是 operator;大写枚举
"value": 100 // 部分 symbolEXIST/UN_EXIST)不需要 value
}
]
}
```
### color 合法值(FORMAT_COLORS
`firstLine1` `firstLine11`(共 11 档色码,对应前端调色盘)。**不接受 `#FF0000` 这种 hex**。
### filter.symbol 合法值
| 类别 | symbol |
|---|---|
| 数值/通用比较 | `GT` / `LT` / `GTE` / `LTE` / `EQ` / `NE` |
| 文本 | `CONTAIN` / `EXCLUSIVE` |
| 存在性(无 value | `EXIST` / `UN_EXIST` |
| 多选 / 集合 | `ALL_OF` / `ANY_OF` / `NONE_OF` |
| 日期 | `BEFORE` / `AFTER` / `NOT_BEFORE` / `NOT_AFTER` / `DATE_EQ` / `FROM_NOW` / `DATE_BETWEEN` |
> **与 `record query --filters` / `view update filter` 的格式不同**:那两处用 `{operator, operands}` 结构;这里是 `{fieldId, symbol, value}`。不要混用。
### 典型用法
```bash
# 1) 给金额字段 > 100 的单元格上 firstLine5 色
dws aitable view update fill-color-rule --view-id GRID_ID --json '[
{
"type":"cell",
"formatFieldId":"fldAmount",
"format":{"color":"firstLine5"},
"filters":[{"fieldId":"fldAmount","symbol":"GT","value":100}]
}
]'
# 2) 清空所有规则
dws aitable view update fill-color-rule --view-id GRID_ID --json '[]'
# 3) 查询当前规则
dws aitable view get fill-color-rule --view-id GRID_ID --format json
# → {"data": [...]} 数组
```
> **写入后请用 `view get fill-color-rule` 二次确认实际生效**,以读到的 `data` 数组为准。
## 复制视图
```bash
# 显式命名
dws aitable view duplicate --view-id VIEW_ID --new-name "副本视图"
# 系统自动命名(一般是 "原视图名 (副本)"
dws aitable view duplicate --view-id VIEW_ID --format json
# → {"data": {..., "viewId": "<新视图ID>", "sourceViewId": "<原视图ID>", "viewName": "..."}}
```
复制会保留源视图的 filter / sort / group / visible-fields / card / timebar 等全部配置;新视图的 viewId 与源视图独立。
## 这些字段不能用 `view update --config '{...}'` 写
下列字段必须用对应的专属子命令;如果错塞进 `view update --config`CLI 会在 stderr 提示对应子命令并拒绝把字段当 view config 处理:
| 错误用法 | 应改用 |
|---|---|
| `--config '{"flags":1}'` | `view lock` / `view lock --off` |
| `--config '{"frozenColCount":2}'` | `view update frozen-cols --count N` |
| `--config '{"cellHeight":56}'` | `view update row-height --cell-height N` |
| `--config '{"rowHeightLevel":"tall"}'` | `view update row-height --cell-height N`(合法档位 32/56/88/128 |
| `--config '{"conditionalFormats":[...]}'` | `view update fill-color-rule --json '[...]'` |
## 典型工作流
### 配置一个"金额超阈值红色高亮"的 Grid 视图
```bash
BASE=baseXXX; TABLE=tblYYY; VIEW=viwGridZZ; FLD=fldAmount
# 1) 关键字段冻结,避免横向滚动看不到
dws aitable view update frozen-cols --base-id $BASE --table-id $TABLE --view-id $VIEW --count 1
# 2) 加大行高让数据更易读
dws aitable view update row-height --base-id $BASE --table-id $TABLE --view-id $VIEW --cell-height 56
# 3) 金额 > 100 的单元格上色
dws aitable view update fill-color-rule --base-id $BASE --table-id $TABLE --view-id $VIEW --json "[
{\"type\":\"cell\",\"formatFieldId\":\"$FLD\",\"format\":{\"color\":\"firstLine5\"},
\"filters\":[{\"fieldId\":\"$FLD\",\"symbol\":\"GT\",\"value\":100}]}
]"
# 4) 锁定视图,防止他人改坏
dws aitable view lock --base-id $BASE --table-id $TABLE --view-id $VIEW
```
### 复制一个"金牌客户"视图给销售团队
```bash
dws aitable view duplicate --view-id viw_VIP_template --new-name "金牌客户-华东区"
# 取返回里 data.viewId 进一步定制
```
### 排查"我设置了高亮规则为啥没生效"
```bash
# 看实际生效的 conditionalFormats
dws aitable view get fill-color-rule --view-id VIEW_ID --format json
# → 如果是 [] 说明上次写入失败;常见原因:color 用了 hex(必须 firstLineN/ filter 用了 operator(必须 symbol
```
@@ -0,0 +1,384 @@
# workflow — 自动化工作流管理
创建 / 更新 / 启停 / 手动执行 / 查询执行历史 / 查看 / 列出 Base 下的自动化工作流("当 X 时自动 Y" 流程)。
适用场景:用户要求创建自动化、修改流程、停掉或恢复流程、立即执行流程、核对执行结果或查询已有流程。
## 命令一览
| 命令 | 用途 |
|------|------|
| `workflow edit-example` | 获取工作流编辑文档与 workflow-dsl/v1 示例 |
| `workflow create` | 创建并发布自动化工作流 |
| `workflow update` | 更新并发布已有自动化工作流 |
| `workflow list` | 列出 Base 下所有工作流(含状态/创建人/最后修改时间),支持分页 |
| `workflow get` | 获取单个工作流详情(含 flowSchema 完整节点定义) |
| `workflow enable` | 启用指定工作流(按配置的触发条件自动执行) |
| `workflow disable` | 禁用指定工作流(高危,建议 `--yes` 二次确认) |
| `workflow run` | 立即执行指定工作流(会产生真实副作用,需确认) |
| `workflow history` | 按状态、时间和分页条件查询工作流执行历史 |
> `workflow edit-example` 无参数;其他子命令的 `--base-id` 必填(可用隐藏别名 `--base`)。
## DSL 入参格式与最小 Demo
先运行 `workflow edit-example` 获取服务端提供的最新编辑文档和示例。`workflow create/update``--dsl` 接收钉钉 AI 表格 `workflow-dsl/v1` JSON object。
复杂工作流还应注意:
1. 使用 `workflow edit-example` 获取最新 DSL Guide、结构和示例。
2. 涉及数据表、字段或视图的节点,先用 `table get` / `field get` / `view list` 确认真实 `sheetId``fieldId``viewId`
3. create 和 update 都提交完整的 workflow-dsl/v1 JSON object,并检查所有 `next``loopEntry`、branch `to` 和 ref。
以下 Demo 表示“每天 09:00 触发,并向 Base 所有者发送消息”,不依赖数据表、字段或视图 ID:
```json
{
"version": "workflow-dsl/v1",
"name": "每日提醒",
"description": "可选说明",
"trigger": "start",
"steps": {
"start": {
"type": "Scheduled",
"next": "send",
"data": {
"mode": "daily",
"time": "09:00",
"timezone": "GMT+08:00"
}
},
"send": {
"type": "SendMessage",
"data": {
"title": "定时任务已触发",
"to": {
"users": [{"ref": "$.system_node.ownerUserId"}]
}
}
}
}
}
```
将上述 JSON 保存为 `workflow.json` 后创建工作流:
```bash
dws aitable workflow create \
--base-id BASE_ID \
--dsl @workflow.json \
--locale zh-CN \
--format json
```
保存创建结果中的 `data.flowId`。更新时修改 `workflow.json` 中的完整目标定义,例如修改 `name``description` 或消息 `title`,然后调用:
```bash
dws aitable workflow update \
--base-id BASE_ID \
--workflow-id FLOW_ID \
--dsl @workflow.json \
--locale zh-CN \
--format json
```
create 和 update 都必须同时满足 `status=success``data.valid=true``data.issues=[]` 才表示发布成功;update 返回的 `data.flowId` 应与传入的 `FLOW_ID` 一致。以上仅为最小 Demo,复杂节点的 `type``data` 结构以钉钉 AI 表格 MCP 最新 DSL 文档为准。
## 命令详情
### workflow edit-example — 获取编辑文档与示例
```bash
dws aitable workflow edit-example --format json
```
该命令无业务参数,调用 `aitable/edit_workflow_example` 返回服务端提供的工作流编辑文档和示例。创建或更新复杂工作流前优先调用它,避免依赖可能过期的本地 DSL 结构。
### workflow create — 创建并发布工作流
```bash
# 大 DSL 推荐从文件读取
dws aitable workflow create \
--base-id BASE_ID \
--dsl @workflow.json \
--locale zh-CN \
--format json
# 也支持 stdin
cat workflow.json | dws aitable workflow create --base-id BASE_ID --dsl - --format json
```
| flag | 必填 | 说明 |
|------|------|------|
| `--base-id` | 是 | 所属 Base ID |
| `--dsl` | 是 | workflow-dsl/v1 JSON object;支持内联 JSON、`@filepath``-` stdin |
| `--locale` | 否 | 请求语言,如 `zh-CN` / `zh_CN` |
创建成功返回发布结果:
```json
{
"status": "success",
"data": {
"valid": true,
"flowId": "G-FLOW-XXXXXX",
"flowSchema": {},
"stepNodeIds": {},
"referenceMap": {},
"issues": []
}
}
```
关键语义:
- `create` 非幂等,CLI 不自动重试。若网络中断导致结果不确定,先 `workflow list` 按名称确认是否已创建,再决定是否重试。
- `status=success` 只说明 workflow-edit 正常返回;如果 `data.valid=false`,仍表示 DSL 未通过校验或发布,必须读取 `issues` 修正。
- 创建并发布后,用 `workflow list` 确认 `status`;需要运行但状态为 `STOP` 时再调用 `workflow enable`
### workflow update — 更新并发布工作流
```bash
# 先留底现有详情,再提交完整目标 DSL
dws aitable workflow get --base-id BASE_ID --workflow-id WORKFLOW_ID --format json > /tmp/workflow-backup.json
dws aitable workflow update \
--base-id BASE_ID \
--workflow-id WORKFLOW_ID \
--dsl @workflow.json \
--locale zh_CN \
--format json
```
| flag | 必填 | 说明 |
|------|------|------|
| `--base-id` | 是 | 所属 Base ID |
| `--workflow-id` | 是 | 目标工作流 ID,对应 list 的 `flowId` |
| `--dsl` | 是 | 完整目标 workflow-dsl/v1 JSON object;支持内联、`@filepath``-` stdin |
| `--locale` | 否 | 请求语言,如 `zh-CN` / `zh_CN` |
返回结构与 create 相同,成功时 `flowId` 应为目标工作流。update 使用 AI 表格瞬态错误重试;最终仍必须检查 `data.valid``issues`,并用 `workflow get/list` 验证发布结果与运行状态。
### workflow run — 立即执行工作流
```bash
# 记录类触发器
dws aitable workflow run --base-id BASE_ID --workflow-id WORKFLOW_ID \
--table-id TABLE_ID --record-ids RECORD_ID_1,RECORD_ID_2
# 定时触发器不传 table-id / record-ids
dws aitable workflow run --base-id BASE_ID --workflow-id WORKFLOW_ID
```
| flag | 必填 | 说明 |
|------|------|------|
| `--base-id` | 是 | 所属 Base ID |
| `--workflow-id` | 是 | 目标工作流 ID |
| `--table-id` | 条件必填 | 记录类触发器绑定的数据表;必须与触发器配置一致 |
| `--record-ids` | 条件必填 | 记录类触发器的记录 ID,1–5 个、逗号分隔且不可重复 |
`run` 启动真实异步执行,工作流中的发消息、写记录等动作会实际发生;执行前必须取得用户确认。返回项中的 `executionId` 是本次执行标识,可与 `workflow history` 项目的 `instanceId` 匹配。网络结果不确定时不要直接重复执行,先按该标识查询历史。
### workflow history — 查询执行历史
```bash
dws aitable workflow history --base-id BASE_ID --workflow-id WORKFLOW_ID \
--status failed --after-time 1786000000000 --before-time 1787000000000 \
--page 0 --size 50
```
| flag | 说明 |
|------|------|
| `--base-id` | 必填 |
| `--workflow-id` | 必填;CLI 会映射为 MCP 的 `flowId` |
| `--status` | 可选:`success` / `failed` / `running` / `break` / `untrigger` |
| `--after-time` | 可选,Unix 毫秒开始时间 |
| `--before-time` | 可选,Unix 毫秒结束时间;与 after-time 同传时必须更大 |
| `--page` | 可选,从 0 开始,默认 0 |
| `--size` | 可选,默认 20,范围 `[1, 100]` |
返回 `totalCount``list``running` 是非终态;`success``failed``break``untrigger` 是终态。
### workflow list — 列出工作流
```bash
dws aitable workflow list --base-id BASE_ID --format json
dws aitable workflow list --base-id BASE_ID --limit 50 --offset 100
```
| flag | 说明 |
|------|------|
| `--base-id` | 必填 |
| `--limit` | 可选,分页大小 `[1, 100]`,不传走服务端默认 20 |
| `--offset` | 可选,分页偏移量 `>= 0`,不传走服务端默认 0 |
返回结构:
```json
{
"data": {
"list": [
{
"flowId": "G-FLOW-XXXXXX", // ★ 注意字段名是 flowId
"name": "流程1",
"description": "当创建记录时,就更新记录",
"status": "RUNNING", // RUNNING / STOP
"creatorStaffId": "281493",
"lastModifier": { "name": "李普阳", "staffId": "281493" },
"gmtModified": 1780318540000,
"versionId": "G-FLOW-VER-XXXXXX",
"icons": ["..."], // 触发器+动作的图标
"isSubFlow": false,
"opPermissions": { "canEdit": true }
}
],
"recordCount": 1, // Base 下总数
"runningCount": 1 // RUNNING 状态的数量
}
}
```
**注意**
- 标识字段服务端在 `list` 里叫 **`flowId`**,但在 `enable` / `disable` 出参里叫 **`workflowId`**。CLI `--workflow-id` 传任一即可(同值)。
- `status` 是字符串枚举:`RUNNING`(启用中)/ `STOP`(已禁用),**不是** boolean。
- `runningCount` 是当前 Base 下 status=RUNNING 的工作流数,方便快速判断「有几个流程在跑」。
### workflow get — 获取单个工作流详情
```bash
dws aitable workflow get --base-id BASE_ID --workflow-id WORKFLOW_ID --format json
```
| flag | 说明 |
|------|------|
| `--base-id` | 必填 |
| `--workflow-id` | 必填,对应 list 出参里的 `flowId` |
返回完整工作流配置:
```json
{
"data": {
"name": "流程1",
"namespace": "...",
"status": "RUNNING",
"versionId": "G-FLOW-VER-XXXXXX",
"versionNo": 14,
"versionStatus": "...",
"accessor": {...}, // 访问者信息
"corpId": "...",
"flowAttribute": {...}, // 流程顶层属性
"flowSchema": {...}, // ★ 流程节点定义(触发器/动作/分支等)
"gmtCreate": 1780317804000,
"gmtModified": 1780318540000
}
}
```
`flowSchema` 是完整的节点 DAG,结构因流程而异(条件触发器 vs 定时触发器、单分支 vs 多分支等)。agent 应按需读取关心字段,不要试图建静态 schema。
### workflow enable — 启用工作流
```bash
dws aitable workflow enable --base-id BASE_ID --workflow-id WORKFLOW_ID --format json
```
返回 `{workflowId, enabled: true}` —— **`enabled: true` 是动作确认,不是当前状态查询**。要确认真启用了,必须再 `workflow list``status` 是否变成 `"RUNNING"``runningCount` 是否加 1。
### workflow disable — 禁用工作流(高危)
```bash
dws aitable workflow disable --base-id BASE_ID --workflow-id WORKFLOW_ID --yes --format json
```
返回 `{workflowId, disabled: true}` —— 同样是动作确认。禁用后该工作流不再自动触发。
**风险**:直接影响业务自动化(如停掉「记录创建后自动发通知」会让通知断流)。建议:
- 操作前先 `workflow get` 留底当前配置
- 脚本场景显式传 `--yes`;交互场景让用户在 prompt 中再次确认
## 能力边界
| 能力 | 状态 |
|------|------|
| 新建工作流 | ✅ 创建并发布 |
| 修改工作流配置 | ✅ 更新并发布 |
| 列出工作流 | ✅ |
| 看工作流详情(含 flowSchema | ✅ |
| 启用/禁用 | ✅ |
| 查看运行历史/执行日志 | ✅ `workflow history` |
| 手动触发/单次运行 | ✅ `workflow run`(需确认) |
| 删除工作流 | ❌ 暂未开放 |
## 错误码速查
| 场景 | code | type | 备注 |
|------|------|------|------|
| create/update 返回 `valid=false` | — | success envelope | 读取 `data.issues` 修正 DSL,不能当作发布成功 |
| create 下游失败 | `CREATE_WORKFLOW_ERROR` | `SYSTEM_ERROR` | create 不自动重试;先 list 排查是否已创建 |
| update 下游失败 | `UPDATE_WORKFLOW_ERROR` | `SYSTEM_ERROR` | update 会重试瞬态错误,最终失败时保留 DSL 和 workflowId 排查 |
| `workflow-id` 不存在调 get | `GET_WORKFLOW_ERROR` | `SYSTEM_ERROR` | message 可能为 null,先 `workflow list` 核对 ID |
| `workflow-id` 不存在调 enable | `ENABLE_WORKFLOW_ERROR` | `SYSTEM_ERROR` | message 含 "场域中不存在该 namespace" |
| `workflow-id` 不存在调 disable | `DISABLE_WORKFLOW_ERROR` | `SYSTEM_ERROR` | 同上 |
| `--limit` < 1 或 > 100 | CLI 层拦截) | — | `--limit 必须在 [1, 100] 范围内,got N` |
| `--offset` < 0 | CLI 层拦截) | — | `--offset 必须 >= 0got N` |
> 拿到 `*_WORKFLOW_ERROR / SYSTEM_ERROR` 时,先 `workflow list` 自查目标 ID 是否还存在、是否在当前 Base 下。
## 典型工作流
### 创建并确认一个工作流
```bash
# 1. 按本文 DSL Demo 生成 /tmp/workflow.json
dws aitable workflow create --base-id BASE_ID --dsl @/tmp/workflow.json --locale zh-CN --format json \
| tee /tmp/workflow-result.json
# 2. valid 必须为 true;保存 flowId
jq '{valid: .data.valid, flowId: .data.flowId, issues: .data.issues}' /tmp/workflow-result.json
# 3. 确认运行状态,需要时显式启用
FLOW_ID=$(jq -r '.data.flowId' /tmp/workflow-result.json)
dws aitable workflow list --base-id BASE_ID --format json \
| jq --arg id "$FLOW_ID" '.data.list[] | select(.flowId == $id) | {flowId, name, status}'
```
### 看看 Base 里有哪些自动化在跑
```bash
dws aitable workflow list --base-id BASE_ID --format json | jq '.data | {total: .recordCount, running: .runningCount, items: .list | map({name, status, flowId})}'
```
### 临时停掉某个流程做调试
```bash
# 1. 留底当前状态
dws aitable workflow get --base-id BASE_ID --workflow-id WORKFLOW_ID --format json > /tmp/wf-backup.json
# 2. 禁用
dws aitable workflow disable --base-id BASE_ID --workflow-id WORKFLOW_ID --yes --format json
# 3. 调试做完后重启
dws aitable workflow enable --base-id BASE_ID --workflow-id WORKFLOW_ID --format json
# 4. 确认 status=RUNNING
dws aitable workflow list --base-id BASE_ID --format json | jq '.data.list[] | select(.flowId == "WORKFLOW_ID") | .status'
```
### 批量关掉某个 Base 下所有 workflow(调试 / 迁移前清场)
```bash
for WF in $(dws aitable workflow list --base-id BASE_ID --limit 100 --format json | jq -r '.data.list[] | select(.status == "RUNNING") | .flowId'); do
dws aitable workflow disable --base-id BASE_ID --workflow-id "$WF" --yes --format json | jq .status
done
```
## 注意事项
- `--workflow-id` 接受的就是 `list` 返回里的 `flowId`(同值,CLI 屏蔽了服务端字段名差异)。
- create / update 的 `--dsl` 必须是 JSON object,不能传数组、字符串化的二次 JSON 或 FlowSchema。
- 本文 Demo 可直接用于最小定时消息工作流;复杂节点应以钉钉 AI 表格 MCP 最新 DSL 文档为准。
- `status=success``data.valid=false` 仍是 DSL 校验失败;`issues` 才是下一步修复依据。
- create 不自动重试;update 仅对网络/5xx/`retryable:true` 瞬态错误自动重试。
- enable / disable 出参里的 `enabled` / `disabled`**动作确认 flag**,不是当前状态字段。要确认真生效请走 `workflow list``status`
- `workflow get``flowSchema` 结构随触发器/动作类型变化,不要假设固定字段。
- `workflow run` 不自动重试;结果不确定时用 `workflow history` 按 executionId / instanceId 核对。
- 删除工作流当前仍未开放。
@@ -0,0 +1,104 @@
# 易混淆操作与字段规则
## 易混淆操作 (高风险场景必读)
| 用户说的 | 正确命令 | 不是这个 |
|---------|----------|---------|
| "创建一个新表格 (Base)" | `base create` | 不是 `table create` |
| "在表格里加一个数据表" | `table create` | 不是 `base create` |
| "看看表格里有哪些表" | `base get` | 不是 `field get` |
| "看看表里有哪些列" | `field get` | 不是 `base get` |
| "搜索表格" (找 Base) | `base search` | 不是 `record query` |
| "搜索记录" (查表内数据) | `record query` | 不是 `base search` |
| "删掉这个数据表" | `table delete` | 不是 `record delete` |
| "删掉这条数据" | `record delete` | 不是 `table delete` |
| "删掉这个列" | `field delete` | 不是 `record delete` |
| "改字段类型" | 先 `field delete``field create` | `field update` **不能改类型** |
| "移动字段/调整字段顺序" | `view update --config '{"visibleFieldIds":[...]}'`(视图层重排,首列主字段必须保留在第一位) | 没有 `field reorder`/`field move` 命令;不能改字段在元数据里的"原始定义顺序" |
## field 子命令总览
> ⚠️ field 有且仅有以下 **4 个** 子命令,没有 `list`、`reorder`、`move`
| 子命令 | 用途 |
|-------|------|
| `field get` | 获取字段详情(含完整 config/options)。**不是 `field list`** |
| `field create` | **创建字段(支持通过 config.options 设置选项)** |
| `field update` | 更新字段名称或配置(**不能改类型**,**不能改顺序**) |
| `field delete` | 删除字段(不可逆) |
> **想"调整字段顺序"?请使用 `view update`**(视图层操作,不属于 `field` 子命令):
> - 通过 `--config '{"visibleFieldIds":["fld1","fld2",...]}'` 传入 fieldId 数组,数组顺序即视图中的字段显示顺序
> - 仅影响**该视图**的列排列,同一 table 的其他视图与字段元数据原始顺序不变
> - **首列字段(主字段)必须保留在数组第一位**,不能移动到非首位
> - **漏传的字段不会被隐藏**,而是会被 API 自动追加到列表末尾
> - **读写命名不一致**:写入键名 `visibleFieldIds`,但读出(`view get`)时该字段在 view 里叫 `columns`——校验顺序时请看 `views[].columns`
## 字段创建时设置 config(重要)
创建 singleSelect/multipleSelect 字段时,**必须设置选项 (options)**
```bash
# 创建带选项的单选字段 (推荐新语法: --name/--type/--config)
dws aitable field create --base-id <BASE_ID> --table-id <TABLE_ID> \
--name "优先级" --type "singleSelect" \
--config '{"options":[{"name":"高"},{"name":"中"},{"name":"低"}]}' \
--format json
# 建表时也可以直接通过 --fields 批量带选项字段
dws aitable table create --base-id <BASE_ID> --name "任务表" \
--fields '[{"fieldName":"任务","type":"text"},{"fieldName":"状态","type":"singleSelect","config":{"options":[{"name":"待办"},{"name":"进行中"},{"name":"已完成"}]}}]' \
--format json
```
> ⚠️ **不要混淆**
> - **字段创建**`field create` 的 `--config` 或 `table create` 的 `--fields`):创建时就指定 `options`
> - **记录写入**`record create` / `record update` 的 `--records`):只能写入已存在的选项名称
## 主字段约束(table create 必读)
> ⚠️ `table create` 的 `--fields` 中,**第一个字段自动成为主字段**。
> 主字段只能是 **text** 类型,不能是 attachment、checkbox、formula 等。
**实际影响**:当用户要求创建的字段不适合做主字段时(如附件、复选框),必须:
1. 先放一个 text 字段作为第一个字段(主字段)
2. 再放用户要求的字段
3. **告知用户**为何多了一个字段
```bash
# 例: 用户要求只创建附件字段 → 附件不能做主字段,必须先加 text 主字段
dws aitable table create --base-id <BASE_ID> --name "产品图片" \
--fields '[{"fieldName":"名称","type":"text"},{"fieldName":"产品图片","type":"attachment"}]' \
--format json
```
## 只读字段 (不可写入)
以下类型的字段不可写入, 执行 `field get` 后识别并跳过:
- 创建时间 / 修改时间 (系统自动)
- 创建人 / 修改人 (系统自动)
- 自动编号
- 公式字段
- 引用字段
## 记录写入格式(record create / record update
> 各字段类型的完整写入/读取格式规范请参考:[aitable-cell-value.md](./aitable/aitable-cell-value.md)
>
> 该文件是 cellValue 格式的 **source of truth**,包含所有字段类型的详细示例和注意事项。
## ⚠️ 附件上传完整流程(必读!)
> **不要**使用钉盘 (drive) 上传来替代此流程!钉盘 fileId **无法**写入 attachment 字段。
附件字段写入使用 `upload_attachment.py` 脚本,**2 步**完成:
```bash
# 步骤 1: 一键上传文件(脚本内部自动完成 prepare + PUT to OSS
python3 scripts/upload_attachment.py <BASE_ID> /path/to/photo.png
# 输出: { "fileToken": "ft_xxx", "fileName": "photo.png", "size": 1024 }
# 步骤 2: 在 record create/update 中使用 fileToken
dws aitable record create --base-id <BASE_ID> --table-id <TABLE_ID> \
--records '[{"cells":{"fldAttachId":[{"fileToken":"ft_xxx"}]}}]' --format json
```
@@ -0,0 +1,17 @@
# AITable 局部意图消歧
| 用户表达 | 归属 | 理由 |
|---|---|---|
| AI 表格、多维表、Base、Table、字段、记录、视图、表单、仪表盘、自动化 | AITable | 操作 AITable 的业务数据与配置 |
| 搜索 Base 候选、按关键词找 Base、检查某 Base 是否存在 | AITable | 直接使用 `+base-search --query`;即使关键词像人名,只要对象是 Base,也不得改走 `aisearch person` |
| 表格链接,需要读取记录 | AITable | 先用 `+url-resolve` 取稳定 ID,再用 `+record-query` |
| 只有 Base/Table 名称,需要读取记录 | AITable | 先用 `+resolve-base` / `+resolve-table` 唯一解析,再查询记录 |
| 只复制 Base 结构、删除整个 Base | AITable | 复制到已知文档文件夹用 `+base-copy --target-folder-id ... --only-struct`;删除用真实 baseId。不要 Drive 完整复制后逐表删数据 |
| Base 整体移动到普通文件夹、外层存储重命名 | Drive | 这是 Base 作为单个存储节点的外层位置/名称动作 |
| Base 内 Table、Dashboard、Section 的复制/移动/重命名/删除 | AITable | 这些是 Base 内 nsheet/业务结构,不是独立 Drive dentry |
| Base 角色、高级权限 | AITable | `+role-*` / `+advperm-*`;仅普通文件 ACL 才走 Drive |
| 记录主键文档正文 | Doc | AITable 只取/建关联,正文由 Doc 处理 |
| Excel 式单元格、区域、工作表、公式 | Sheetdingtalk-misc | 二维电子表格,不是多维表记录模型 |
| CSV/JSON 数据进入现有 AI 表格 | AITable import 或 record create | 需要保留导入任务语义时用 import;已映射字段时直接写记录 |
若链接类型不明确,先做 URL 类型预检;不要仅凭 URL 文本猜产品。明确是 AI 表格后才加载本 Skill。
@@ -0,0 +1,44 @@
# 业务域通用规范
> 仅服务本 skill 已迁入的行动指南。安全门控、危险操作确认、`--format json` 等已在本 skill 的 `SKILL.md` 中定义,此处不重复。
## 批量查询规范
| # | 规范 |
|---|------|
| 1 | **并行查详情**:拿到多个 ID 后,用 `&` 合并到同一条 Shell 命令并行执行 + `wait`**严禁逐条串行** |
| 2 | **翻页**:分页接口须拉全直至无更多 |
| 3 | **优先批量 API**:有批量接口则用批量;无则按 #1 并行 |
| 4 | **群消息**:必须先 `chat search --query``openConversationId`,再 `chat message list --group <openConversationId> --time "<yyyy-MM-dd HH:mm:ss>" --direction older`;多群同条命令并行 |
| 5 | **列表少轮次**:带条件搜索/列表 → 一次采全详情;**禁止**无新参数时重复同一 `list` / `search` |
## 多源并行采集(公共模式)
> recipe 引用方式:`按「多源并行采集」执行(关键词=<X>,时间=<Y>至<Z>`。
- 同条 Shell`&` 并行 + `wait`;分页须采全。
- 只保留与主题相关的数据,无关丢弃。
- 有批量详情接口优先;否则并行拉详情(见上表 #1)。
- 具体采哪些产品列表由对应 **行动指南 recipe** 与当前产品参考决定;不要引入本文档未覆盖的产品路线。
## 字段术语与 ID 传递
> list 返回 JSON 后,必须提取下表字段传给后续命令。**禁止用其他字段替代。**
| 字段 | 来源 | 传递给 |
|------|------|--------|
| `taskUuid` | `minutes list` | `minutes get summary/info/batch --id(s)` |
| `userId` | `aisearch person` / `contact user search` / `contact dept list-members` | `contact user get --ids``todo --executors``calendar --users` |
| `deptId` | `contact dept search` | `contact dept list-members --ids <deptId1,deptId2...>`;多子部门时对每个子部门分别 `dept search` 取 id |
| `nodeId` | `drive search` / `wiki node search` | `doc read/update --node``drive copy/move/rename/delete --node` |
| `nodeId` | `wiki node list` 中的 folder 类型节点 / `wiki node create --type folder` | `wiki node list --folder``wiki node create --folder``drive upload --folder``drive copy/move --folder` |
| `eventId` | `calendar event list` | `calendar event get/update --id` |
| `processInstanceId` | `oa approval list-*` | `oa approval detail/approve --instance-id` |
| `openConversationId` | `chat search` | `chat message list/send --group` |
| `todoTaskId` | `todo task list` | `todo task update/done --task-id` |
| `reportId` | `report inbox list` / `report outbox list` | `report entry get/stats --report-id` |
| `baseId` / `tableId` | `aitable base search` | `aitable record query --base-id --table-id` |
| `dentryUuid` | `drive list` / `drive mkdir` | `drive info/download/copy/move/rename/delete --node``drive list/mkdir/upload/copy/move --folder` |
| `dentryId` | `drive info` 的数字字段 | 仅用于 `chat message send --dentry-id` |
**ID 边界硬约束**`dentryId` 通常是纯数字,只表示聊天文件消息需要的钉盘条目数字 ID;它不是父目录 ID。遇到 `drive --node/--folder``doc --node``wiki node --folder` 时,只能使用 `dentryUuid` / `nodeId` / 文档 URL。若当前上下文只有数字型 `dentryId`,必须先重新 `drive list` / `drive search` / `wiki node list` 获取正确 ID,不能把该数字直接代入后续命令。
@@ -0,0 +1,144 @@
# URL 格式与处理规范
## 路由第 0 步:意图直达(优先级高于 URL 探测)
用户已经明确表达某产品的内容意图时,直接进入对应产品场域,不要先做 URL 类型
探测。尤其:
- 明确提到 Markdown / `.md` 文件的读取或修改,按普通文件走 `drive` 场域:
`dws drive download` 下载到本地处理,再用 `dws drive upload` 回传。
- 明确“读这篇文档 / 编辑文档正文”进入 `doc`;明确“看这个在线表格数据”进入
`sheet`
仅当用户只粘贴 URL、没有明确产品意图,或意图与链接类型可能冲突时,才执行下方
类型探测。
## alidocs URL 分流决策(意图不明确时执行)
收到 `alidocs.dingtalk.com` URL 且无法从指令判断产品时,必须按以下顺序判断:
1. URL 路径含 `/i/p/`**分享短链**,禁止调用 `dws doc` 任何子命令 → 按下方 [分享短链处理](#分享短链处理) 执行
2. URL 路径含 `/i/nodes/`**节点链接**,需探测类型 → 按下方 [alidocs URL 类型探测流程](#alidocs-url-类型探测流程) 执行
3. URL 路径含 `/spreadsheetv2/`**电子表格直链**,直接路由到 `sheet`,将完整 URL 原样传给 `--node` 参数
4. URL 路径含 `/document/edit``/document/preview` 且 query 参数包含 `dentryKey`**文档链接**,直接路由到 `doc`,将完整 URL 原样传给 `--node` 参数(URL 中不一定有 `type=d`,只需匹配路径和 `dentryKey` 参数即可)
5. 其他 alidocs URL 格式 → 告知用户当前暂不支持该链接格式
---
## 已知 URL 格式
需要自行拼接链接时,只能使用以下模板:
| 产品 | 用途 | URL 格式 | ID 来源 |
|------|------|----------|---------|
| `aitable` | AI表格 Base 链接 | `https://alidocs.dingtalk.com/i/nodes/{baseId}` | `base list/search/create/get` 返回的 `baseId` |
| `aitable` | AI表格指定数据表链接 | `https://alidocs.dingtalk.com/i/nodes/{baseId}?iframeQuery=sheetId%3D{tableId}` | `baseId` + `table create/get``base get` 返回的 `tableId` |
| `aitable` | AI表格指定数据表+视图链接 | `https://alidocs.dingtalk.com/i/nodes/{baseId}?iframeQuery=sheetId%3D{tableId}%26viewId%3D{viewId}` | `baseId` + `tableId` + `view create/get` 返回的 `viewId` |
| `aitable` | AI表格模板预览 | `https://docs.dingtalk.com/table/template/{templateId}` | `template search` 返回的 `templateId` |
| `doc` | 文档链接 | `https://alidocs.dingtalk.com/i/nodes/{dentryUuid}` | `doc` 命令返回的 `dentryUuid` |
| `sheet` | 电子表格链接 | `https://alidocs.dingtalk.com/i/nodes/{dentryUuid}` | `sheet create` 返回的 `dentryUuid` |
| `sheet` | 电子表格直链 | `https://alidocs.dingtalk.com/spreadsheetv2/{key}/...?dentryKey={key}&type=s` | 用户提供的完整 URL,直接传给 `--node` |
| `doc` | 文档链接(edit/preview | `https://alidocs.dingtalk.com/document/{edit\|preview}?...&dentryKey={key}` | 用户提供的完整 URL,直接传给 `--node` |
| `minutes` | 听记链接 | `https://shanji.dingtalk.com/app/transcribes/{taskUuid}` | `list mine/shared` 返回的 `taskUuid` |
不在此表中的产品,禁止自行拼接 URL。命令返回中包含完整链接时直接使用,否则告知用户无法提供。
## 分享短链处理
`alidocs.dingtalk.com/i/p/{shortKey}` 是钉钉文档的**对外分享短链**`dws doc` 命令无法解析此格式。
### 识别规则
URL 路径中包含 `/i/p/` 即为分享短链(无论后面是否还有子路径),例如:
- `https://alidocs.dingtalk.com/i/p/Y7kmbokZp3pgGLq2`
- `https://alidocs.dingtalk.com/i/p/Y7kmbokZp3pgGLq2/docs/AY39rGpMPmeVNpXZevZm8OZkXKnaoNQ7`
- `https://alidocs.dingtalk.com/i/p/AbCdEfGh1234`
- `https://alidocs.dingtalk.com/i/p/AbCdEfGh1234/sheets/XYZ789`
> **关键**:只要 URL 中出现 `/i/p/`,无论后面跟什么子路径(`/docs/...`、`/sheets/...` 等),都属于分享短链,一律禁止调用 `dws doc`。
### 处理方式
**不要调用 `dws doc` 任何子命令**(包括 `doc info``doc read` 等),`dws` 无法解析此格式。
- **需要获取文档内容时**:使用 `read_url` 工具直接读取该链接
- **其他操作(如移动、复制、权限管理等)**:告知用户此链接为分享短链,无法直接执行复制、移动、权限管理等操作。如需保存该文档内容,建议用户在钉钉客户端中打开该页面,手动复制文本内容,然后可通过 `dws doc create` 创建一篇新文档并将内容写入
```
# 需要读取文档内容时(无论 /i/p/ 后面有没有子路径,都用 read_url)
read_url("https://alidocs.dingtalk.com/i/p/Y7kmbokZp3pgGLq2")
read_url("https://alidocs.dingtalk.com/i/p/Y7kmbokZp3pgGLq2/docs/AY39rGpMPmeVNpXZevZm8OZkXKnaoNQ7")
# 禁止(以下全部会失败,dws 无法解析任何含 /i/p/ 的 URL)
dws doc info --node "https://alidocs.dingtalk.com/i/p/Y7kmbokZp3pgGLq2" --format json
dws doc read --node "https://alidocs.dingtalk.com/i/p/Y7kmbokZp3pgGLq2/docs/AY39rGpMPmeVNpXZevZm8OZkXKnaoNQ7" --format json
```
### 当 `read_url` 返回内容不完整时
钉钉文档分享页是动态渲染的,`read_url` 可能只能获取到页面标题等有限信息,无法获取文档正文。此时**禁止猜测原因**(如"权限不足""文档为空""文档已删除"等),**禁止建议用户"提供 `/i/nodes/` 格式链接"**(分享短链和节点链接是不同体系,普通用户无法自行转换)。应直接告知用户:
> 这个链接是钉钉文档的分享短链,由于页面是动态渲染的,我无法通过该链接直接获取文档的完整正文内容。
>
> 你可以:
> 1. 在钉钉客户端中打开该文档,将正文内容复制粘贴给我
> 2. 如果文档已保存在你的文档空间中,可以告诉我文档名称,我通过 `dws drive search` 搜索后再读取
---
## alidocs URL 类型探测流程
`alidocs.dingtalk.com/i/nodes/{id}` 是钉钉文档空间的统一 URL,可能指向**文档、电子表格、多维表、文件、文件夹**等不同类型。**禁止仅凭 URL 就假定为文档**,必须先探测类型再路由到正确的产品。
### 探测步骤
```
Step 1 → dws drive info --node "<URL>" --format json
Step 2 → 从返回中提取 extension、nodeType 字段
Step 3 → 按下方路由规则映射到对应产品
```
> 路由依据是 `extension`,不是 `contentType`。`drive info` 检测到
> `adoc` / `axls` / `able` 时会自动补充在线文档信息。
### 路由映射表
| 条件 | 路由到产品 | 后续操作 |
|------|-----------|---------|
| `extension=adoc` | `doc` | 加载 `dingtalk-doc` 操作内容 |
| `extension=axls` | `sheet` | 加载 `dingtalk-misc``references/sheet.md` 操作(仅 `axls` 在线电子表格) |
| `extension=able` | `aitable` | 将 nodeId 作为 baseId,加载 `dingtalk-aitable` 操作 |
| `extension=xlsx` / `xls` / `xlsm` / `csv` | `drive` | 必须用 `dws drive download` 下载到本地处理,禁止走 `sheet` |
| `nodeType=file`(非在线文档扩展名,含 `md` | `drive` | 下载用 `dws drive download --node <ID> --output <PATH> --format json`;上传/覆盖用 `dws drive upload` |
| `nodeType=folder` | `drive` / `wiki` | 调用 `dws drive list --workspace <WS_ID>``dws wiki node list` 列出子节点 |
| 以上均不匹配 | — | 告知用户当前暂不支持该类型 |
> axls vs xlsx 关键区分:
> - `axls`(钉钉在线电子表格,`contentType=ALIDOC`)→ 走 `sheet` 产品线(读/写/筛选/导出等服务端原子操作)
> - `xlsx` / `xls` / `xlsm` / `csv`(上传到文档空间的本地表格文件,`contentType=DOCUMENT`)→ 必须走 `dws drive download` 下载到本地后再解析处理,严禁错误路由到 `sheet` 产品线(sheet 命令只支持在线表格,调用 xlsx 节点会直接报错)
> - 用户想把在线表格导出为 xlsx 文件 → 用 `dws sheet export`(输入是 `axls`,输出是 xlsx,这是 axls → xlsx 的格式转换,不属于 xlsx 读取场景)
### 示例
```bash
# 用户传入: https://alidocs.dingtalk.com/i/nodes/abc123
dws drive info --node "https://alidocs.dingtalk.com/i/nodes/abc123" --format json
# 返回 extension=axls → 在线电子表格,路由到 sheet
dws sheet list --node "https://alidocs.dingtalk.com/i/nodes/abc123" --format json
# 返回 extension=xlsx/xls/csv → 本地表格文件,必须下载处理(禁止走 sheet)
dws drive download --node "https://alidocs.dingtalk.com/i/nodes/xlsx456" --output <PATH> --format json
# 返回 nodeType=file → 普通文件,下载
dws drive download --node "https://alidocs.dingtalk.com/i/nodes/def456" --output <PATH> --format json
# 返回 nodeType=folder → 文件夹,列出子节点
dws drive list --workspace <WS_ID> --format json
```
### 何时可跳过探测
当用户指令中已明确指定产品(如"帮我读这个文档"、"看下这个表格的数据"),可结合用户意图**跳过探测**直接路由。仅在以下情况**必须执行探测**:
- 用户只粘贴 URL,无其他上下文
- 用户指令与 URL 实际类型可能不一致(如说"文档"但实际是表格)
@@ -0,0 +1,210 @@
#!/usr/bin/env python3
"""
通过 MCP 导出任务(export_data)导出 AI 表格,并可自动下载文件。
与普通命令的区别:
- 自动处理 taskId 轮询(直到拿到 downloadUrl 或达到轮询上限)。
- 自动保存导出文件到本地(可选 --output)。
用法:
python scripts/aitable_export_via_task.py <baseId> --scope all
python scripts/aitable_export_via_task.py <baseId> --scope table --table-id <tableId>
python scripts/aitable_export_via_task.py <baseId> --scope view --table-id <tableId> --view-id <viewId>
"""
from __future__ import annotations
import argparse
import json
import re
import subprocess
import sys
import time
from pathlib import Path
from typing import Any, Dict, Optional, Tuple
from urllib.error import HTTPError, URLError
from urllib.parse import urlparse
from urllib.request import Request, urlopen
RESOURCE_ID_PATTERN = re.compile(r"^[A-Za-z0-9_-]{8,128}$")
ALLOWED_FORMATS = {"excel", "attachment", "excel_and_attachment", "excel_with_inline_images"}
def validate_resource_id(resource_id: str) -> bool:
return bool(resource_id and RESOURCE_ID_PATTERN.match(resource_id.strip()))
def run_dws(dws_bin: str, args: list[str], timeout_sec: int = 120) -> Tuple[int, str, str]:
cmd = [dws_bin] + args
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout_sec)
return result.returncode, result.stdout.strip(), result.stderr.strip()
except subprocess.TimeoutExpired:
return 124, "", f"dws command timeout after {timeout_sec}s"
except FileNotFoundError:
return 127, "", f"dws binary not found: {dws_bin}"
def parse_json_output(raw: str) -> Optional[Dict[str, Any]]:
try:
obj = json.loads(raw)
return obj if isinstance(obj, dict) else None
except json.JSONDecodeError:
return None
def normalize_download_url(url: str) -> str:
if url.startswith("http://") or url.startswith("https://"):
return url
return f"https://{url}"
def download_file(url: str, output_path: Path) -> Tuple[bool, str]:
req = Request(url, method="GET")
try:
with urlopen(req, timeout=180) as resp:
if resp.status != 200:
return False, f"download http status: {resp.status}"
output_path.write_bytes(resp.read())
return True, ""
except HTTPError as e:
body = e.read().decode("utf-8", "ignore")
return False, f"HTTP {e.code}: {body[:300]}"
except URLError as e:
return False, f"URL error: {e.reason}"
def fail(msg: str, code: int = 1) -> None:
print(f"错误:{msg}", file=sys.stderr)
sys.exit(code)
def build_start_args(args: argparse.Namespace) -> list[str]:
cmd = [
"aitable",
"export",
"data",
"--base-id",
args.base_id,
"--scope",
args.scope,
"--format",
args.export_format,
"--timeout-ms",
str(args.timeout_ms),
]
if args.table_id:
cmd.extend(["--table-id", args.table_id])
if args.view_id:
cmd.extend(["--view-id", args.view_id])
return cmd
def main() -> None:
parser = argparse.ArgumentParser(description="通过 MCP 导出任务导出 AI 表格")
parser.add_argument("base_id", help="目标 AI 表格 baseId")
parser.add_argument("--scope", choices=["all", "table", "view"], required=True, help="导出范围")
parser.add_argument("--table-id", help="scope=table/view 时必填")
parser.add_argument("--view-id", help="scope=view 时必填")
parser.add_argument("--export-format", default="excel", choices=sorted(ALLOWED_FORMATS), help="导出格式")
parser.add_argument("--timeout-ms", type=int, default=1000, help="单次等待毫秒数,默认 1000")
parser.add_argument("--poll-timeout-ms", type=int, default=3000, help="轮询等待毫秒数,默认 3000")
parser.add_argument("--max-polls", type=int, default=10, help="最大轮询次数,默认 10")
parser.add_argument("--output", help="本地保存路径(不传则按 fileName 保存到当前目录)")
parser.add_argument("--dws", default="dws", help="dws 可执行文件路径,默认 dws")
parser.add_argument("--no-download", action="store_true", help="仅返回 downloadUrl,不下载文件")
args = parser.parse_args()
if not validate_resource_id(args.base_id):
fail("无效的 baseId 格式")
if args.scope in ("table", "view") and not args.table_id:
fail("scope=table/view 时必须传 --table-id")
if args.scope == "view" and not args.view_id:
fail("scope=view 时必须传 --view-id")
print("[1/2] start export task", file=sys.stderr)
rc, out, err = run_dws(args.dws, build_start_args(args), timeout_sec=120)
if rc != 0:
fail(f"export_data 启动失败: {err or out}", rc)
obj = parse_json_output(out)
if not obj:
fail(f"export_data 返回非 JSON: {out[:300]}")
data = obj.get("data", {}) or {}
status = obj.get("status")
if status == "error":
fail(f"export_data 返回失败: {json.dumps(obj, ensure_ascii=False)}")
download_url = data.get("downloadUrl")
task_id = data.get("taskId")
file_name = data.get("fileName") or "export_result.bin"
polls = 0
while not download_url and task_id and polls < args.max_polls:
polls += 1
print(f"[2/2] polling task ({polls}/{args.max_polls})", file=sys.stderr)
rc2, out2, err2 = run_dws(
args.dws,
[
"aitable",
"export",
"data",
"--base-id",
args.base_id,
"--task-id",
task_id,
"--timeout-ms",
str(args.poll_timeout_ms),
],
timeout_sec=max(120, int(args.poll_timeout_ms / 1000) + 60),
)
if rc2 != 0:
fail(f"export_data 轮询失败: {err2 or out2}", rc2)
obj2 = parse_json_output(out2)
if not obj2:
fail(f"export_data 轮询返回非 JSON: {out2[:300]}")
if obj2.get("status") == "error":
fail(f"export_data 轮询返回失败: {json.dumps(obj2, ensure_ascii=False)}")
d2 = obj2.get("data", {}) or {}
download_url = d2.get("downloadUrl") or download_url
file_name = d2.get("fileName") or file_name
task_id = d2.get("taskId") or task_id
if not download_url:
time.sleep(0.2)
result: Dict[str, Any] = {
"baseId": args.base_id,
"scope": args.scope,
"exportFormat": args.export_format,
"taskId": task_id,
"fileName": file_name,
"downloadUrl": download_url,
"polledTimes": polls,
}
if not download_url:
result["status"] = "pending"
result["summary"] = "导出任务仍在处理中,请继续用 taskId 轮询。"
print(json.dumps(result, ensure_ascii=False, indent=2))
sys.exit(3)
if args.no_download:
result["status"] = "success"
result["summary"] = "导出完成(未下载文件)。"
print(json.dumps(result, ensure_ascii=False, indent=2))
return
norm_url = normalize_download_url(download_url)
output_path = Path(args.output).expanduser().resolve() if args.output else Path.cwd() / file_name
ok, dl_err = download_file(norm_url, output_path)
if not ok:
fail(f"downloadUrl 下载失败: {dl_err}")
result["status"] = "success"
result["summary"] = "导出完成并已下载。"
result["savedPath"] = str(output_path)
print(json.dumps(result, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
@@ -0,0 +1,173 @@
#!/usr/bin/env python3
"""
通过 MCP 文件导入任务(prepare_import_upload -> PUT -> import_data)导入 AI 表格。
与 import_records.py 的区别:
- 本脚本:走“文件导入任务”链路,通常会新建导入数据表。
- import_records.py:走 create_records,写入已有 table。
用法:
python scripts/aitable_import_via_task.py <baseId> <filePath>
python scripts/aitable_import_via_task.py <baseId> <filePath> --timeout 30
python scripts/aitable_import_via_task.py <baseId> <filePath> --dws /tmp/dws
"""
from __future__ import annotations
import argparse
import json
import re
import subprocess
import sys
from pathlib import Path
from typing import Any, Dict, Optional, Tuple
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
RESOURCE_ID_PATTERN = re.compile(r"^[A-Za-z0-9_-]{8,128}$")
ALLOWED_EXTENSIONS = {".csv", ".xlsx", ".xls"}
def validate_resource_id(resource_id: str) -> bool:
return bool(resource_id and RESOURCE_ID_PATTERN.match(resource_id.strip()))
def run_dws(dws_bin: str, args: list[str], timeout_sec: int = 120) -> Tuple[int, str, str]:
cmd = [dws_bin] + args
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout_sec)
return result.returncode, result.stdout.strip(), result.stderr.strip()
except subprocess.TimeoutExpired:
return 124, "", f"dws command timeout after {timeout_sec}s"
except FileNotFoundError:
return 127, "", f"dws binary not found: {dws_bin}"
def parse_json_output(raw: str) -> Optional[Dict[str, Any]]:
try:
obj = json.loads(raw)
return obj if isinstance(obj, dict) else None
except json.JSONDecodeError:
return None
def put_file(upload_url: str, file_path: Path) -> Tuple[bool, str]:
payload = file_path.read_bytes()
req = Request(upload_url, data=payload, method="PUT")
# 关键:清空 Content-Type,避免 SignatureDoesNotMatch。
req.add_header("Content-Type", "")
try:
with urlopen(req, timeout=180) as resp:
if resp.status == 200:
return True, ""
return False, f"unexpected HTTP status: {resp.status}"
except HTTPError as e:
body = e.read().decode("utf-8", "ignore")
return False, f"HTTP {e.code}: {body[:300]}"
except URLError as e:
return False, f"URL error: {e.reason}"
def fail(msg: str, exit_code: int = 1) -> None:
print(f"错误:{msg}", file=sys.stderr)
sys.exit(exit_code)
def main() -> None:
parser = argparse.ArgumentParser(description="通过文件导入任务导入 AI 表格")
parser.add_argument("base_id", help="目标 AI 表格 baseId")
parser.add_argument("file_path", help="待导入文件路径(.csv/.xlsx/.xls")
parser.add_argument("--timeout", type=int, default=30, help="import_data 等待秒数,默认 30")
parser.add_argument("--dws", default="dws", help="dws 可执行文件路径,默认 dws")
args = parser.parse_args()
base_id = args.base_id.strip()
file_path = Path(args.file_path).expanduser().resolve()
if not validate_resource_id(base_id):
fail("无效的 baseId 格式")
if not file_path.exists() or not file_path.is_file():
fail(f"文件不存在或不可读: {file_path}")
if file_path.suffix.lower() not in ALLOWED_EXTENSIONS:
fail(f"仅支持 {sorted(ALLOWED_EXTENSIONS)},当前文件: {file_path.name}")
file_size = file_path.stat().st_size
if file_size <= 0:
fail("文件为空")
print(f"[1/3] prepare import upload: {file_path.name} ({file_size} bytes)", file=sys.stderr)
rc, out, err = run_dws(
args.dws,
[
"aitable",
"import",
"upload",
"--base-id",
base_id,
"--file-name",
file_path.name,
"--file-size",
str(file_size),
"--format",
"json",
],
)
if rc != 0:
fail(f"prepare_import_upload 失败: {err or out}", rc)
prepare_obj = parse_json_output(out)
if not prepare_obj:
fail(f"prepare_import_upload 返回非 JSON: {out[:300]}")
if prepare_obj.get("status") != "success":
fail(f"prepare_import_upload 返回失败: {json.dumps(prepare_obj, ensure_ascii=False)}")
pdata = prepare_obj.get("data") or {}
upload_url = pdata.get("uploadUrl")
import_id = pdata.get("importId")
if not upload_url or not import_id:
fail(f"prepare_import_upload 缺少 uploadUrl/importId: {json.dumps(pdata, ensure_ascii=False)}")
print("[2/3] upload file bytes via PUT", file=sys.stderr)
ok, put_err = put_file(upload_url, file_path)
if not ok:
fail(f"PUT 上传失败: {put_err}")
print("[3/3] trigger import_data", file=sys.stderr)
rc2, out2, err2 = run_dws(
args.dws,
[
"aitable",
"import",
"data",
"--import-id",
import_id,
"--timeout",
str(args.timeout),
"--format",
"json",
],
timeout_sec=max(120, args.timeout + 30),
)
if rc2 != 0:
fail(f"import_data 调用失败: {err2 or out2}", rc2)
import_obj = parse_json_output(out2)
if not import_obj:
fail(f"import_data 返回非 JSON: {out2[:300]}")
result = {
"baseId": base_id,
"fileName": file_path.name,
"fileSize": file_size,
"importId": import_id,
"status": import_obj.get("status"),
"summary": import_obj.get("summary"),
"data": import_obj.get("data", {}),
"error": import_obj.get("error", {}),
}
print(json.dumps(result, ensure_ascii=False, indent=2))
if import_obj.get("status") != "success":
sys.exit(2)
if __name__ == "__main__":
main()
@@ -0,0 +1,273 @@
#!/usr/bin/env python3
"""
批量添加字段到钉钉 AI 表格数据表(新版 schema)
用法:
python bulk_add_fields.py <baseId> <tableId> fields.json
fields.json 格式:
[
{"fieldName": "字段 1", "type": "text"},
{"fieldName": "字段 2", "type": "number", "config": {"formatter": "INT"}},
{"fieldName": "字段 3", "type": "singleSelect", "config": {"options": [{"name": ""}]}}
]
兼容写法:
- name 会自动映射为 fieldName
- phone 会自动映射为 telephone
"""
import sys
import json
import subprocess
import os
import re
from pathlib import Path
from typing import Union, List, Dict, Any, Optional, Tuple
JsonData = Union[List[Any], Dict[str, Any]]
MAX_FILE_SIZE = 10 * 1024 * 1024
ALLOWED_FILE_EXTENSIONS = ['.json']
RESOURCE_ID_PATTERN = re.compile(r'^[A-Za-z0-9_-]{8,128}$')
ALLOWED_FIELD_TYPES = {
'text', 'number', 'singleSelect', 'multipleSelect', 'date', 'currency',
'user', 'department', 'group', 'progress', 'rating', 'checkbox',
'attachment', 'url', 'richText', 'telephone', 'email', 'idCard',
'barcode', 'geolocation', 'address', 'primaryDoc', 'formula',
'unidirectionalLink', 'bidirectionalLink', 'lookup', 'filterUp',
'creator', 'lastModifier', 'createdTime', 'lastModifiedTime',
}
FIELD_TYPE_ALIASES = {
'phone': 'telephone',
}
def resolve_safe_path(path: str, allowed_root: Optional[str] = None) -> Path:
if allowed_root is None:
allowed_root = os.environ.get('OPENCLAW_WORKSPACE', os.getcwd())
allowed_root = Path(allowed_root).resolve()
target_path = (
Path(path).resolve()
if Path(path).is_absolute()
else (Path.cwd() / path).resolve()
)
try:
target_path.relative_to(allowed_root)
return target_path
except ValueError:
raise ValueError(
f"路径超出允许范围:{path}\n"
f"目标路径:{target_path}\n"
f"允许根目录:{allowed_root}\n"
f"提示:设置 OPENCLAW_WORKSPACE 环境变量或确保文件在工作目录内"
)
def validate_resource_id(resource_id: str) -> bool:
return bool(resource_id and RESOURCE_ID_PATTERN.match(resource_id.strip()))
def validate_file_extension(filename: str, allowed_extensions: list) -> bool:
return any(filename.lower().endswith(ext) for ext in allowed_extensions)
def safe_json_load(file_path: Path, max_size: int = MAX_FILE_SIZE) -> JsonData:
file_size = file_path.stat().st_size
if file_size > max_size:
raise ValueError(
f"文件过大:{file_size:,} 字节 (限制:{max_size:,} 字节)"
)
with open(file_path, 'r', encoding='utf-8') as f:
return json.load(f)
def normalize_field_config(field: Dict[str, Any]) -> Dict[str, Any]:
normalized = dict(field)
if 'fieldName' not in normalized and 'name' in normalized:
normalized['fieldName'] = normalized.pop('name')
normalized['type'] = FIELD_TYPE_ALIASES.get(
normalized.get('type', 'text'), normalized.get('type', 'text')
)
return normalized
def validate_field_config(field: Dict[str, Any]) -> Tuple[bool, str]:
if not isinstance(field, dict):
return False, '字段配置必须是对象'
field = normalize_field_config(field)
if 'fieldName' not in field:
return False, '缺少必需字段:fieldName'
if not isinstance(field['fieldName'], str) or not field['fieldName'].strip():
return False, 'fieldName 必须是非空字符串'
field_type = field.get('type', 'text')
if field_type not in ALLOWED_FIELD_TYPES:
return False, f"不支持的字段类型:{field_type}"
config = field.get('config')
if config is not None and not isinstance(config, dict):
return False, 'config 必须是对象'
if field_type in {'singleSelect', 'multipleSelect'}:
options = (config or {}).get('options')
if not options or not isinstance(options, list):
return False, (
'singleSelect / multipleSelect 必须提供 config.options 数组'
)
if field_type in {'unidirectionalLink', 'bidirectionalLink'}:
linked_table_id = (config or {}).get('linkedTableId')
if not linked_table_id or not validate_resource_id(linked_table_id):
return False, (
'关联字段必须提供合法的 config.linkedTableId(目标 Table ID'
)
if field_type == 'lookup':
cfg = config or {}
if not cfg.get('associateField'):
return False, 'lookup 必须提供 config.associateField(本表关联字段的 fieldId'
if not cfg.get('valuesField'):
return False, 'lookup 必须提供 config.valuesField(关联目标表中要取值的字段 fieldId)'
if not cfg.get('aggregator'):
return False, 'lookup 必须提供 config.aggregatorSUM/AVERAGE/COUNT/MAX/MIN/CONCATENATE'
if field_type == 'filterUp':
cfg = config or {}
if not cfg.get('targetSheet'):
return False, 'filterUp 必须提供 config.targetSheet(目标 Table ID'
filters = cfg.get('filters')
if not filters or not isinstance(filters, list):
return False, 'filterUp 必须提供 config.filters(至少一条筛选规则)'
if not cfg.get('valuesField'):
return False, 'filterUp 必须提供 config.valuesField(目标表中要取值的字段 fieldId)'
if not cfg.get('aggregator'):
return False, 'filterUp 必须提供 config.aggregatorSUM/AVERAGE/COUNT/MAX/MIN/CONCATENATE'
return True, ''
def build_fields_json(fields: List[Dict[str, Any]]) -> str:
"""构建 --fields 参数的 JSON 字符串。"""
payload_fields = []
for field in fields:
normalized = normalize_field_config(field)
item: Dict[str, Any] = {
'fieldName': normalized['fieldName'].strip(),
'type': normalized.get('type', 'text'),
}
if 'config' in normalized and normalized['config'] is not None:
item['config'] = normalized['config']
payload_fields.append(item)
return json.dumps(payload_fields, ensure_ascii=False)
def run_dws(args: List[str]) -> Optional[Dict[str, Any]]:
if not args:
print('错误:空命令')
return None
cmd = ['dws'] + args
try:
result = subprocess.run(
cmd, capture_output=True, text=True, timeout=60
)
if result.returncode != 0:
print(f"错误:{result.stderr.strip()}")
return None
try:
return json.loads(result.stdout)
except json.JSONDecodeError as e:
print(f"无法解析响应:{result.stdout[:200]}...")
print(f"JSON 解析错误:{e}")
return None
except subprocess.TimeoutExpired:
print('错误:命令执行超时(60 秒)')
return None
except FileNotFoundError:
print('错误:未找到 dws 命令,请确认已安装')
return None
def bulk_add_fields(
base_id: str, table_id: str, fields_file: str
) -> bool:
try:
safe_path = resolve_safe_path(fields_file)
except ValueError as e:
print(f"路径验证失败:{e}")
return False
if not validate_file_extension(fields_file, ALLOWED_FILE_EXTENSIONS):
print(f"错误:只允许 {', '.join(ALLOWED_FILE_EXTENSIONS)} 文件")
return False
if not safe_path.exists():
print(f"错误:文件不存在:{safe_path}")
return False
try:
fields = safe_json_load(safe_path)
except ValueError as e:
print(f"错误:{e}")
return False
except json.JSONDecodeError as e:
print(f"错误:JSON 格式无效:{e}")
return False
if not isinstance(fields, list) or not fields:
print('错误:fields.json 必须是非空 JSON 数组')
return False
if len(fields) > 15:
print('错误:单次最多创建 15 个字段,请拆分后重试')
return False
for i, field in enumerate(fields):
valid, error = validate_field_config(field)
if not valid:
print(f"错误:字段 #{i+1} 配置无效:{error}")
return False
fields_json = build_fields_json(fields)
result = run_dws([
'aitable', 'field', 'create',
'--base-id', base_id,
'--table-id', table_id,
'--fields', fields_json,
'--format', 'json',
])
if not result:
return False
print(json.dumps(result, ensure_ascii=False, indent=2))
return True
def main():
if len(sys.argv) != 4:
print(__doc__)
print('用法示例:')
print(' python bulk_add_fields.py basexxx tablexxx fields.json')
sys.exit(1)
base_id = sys.argv[1]
table_id = sys.argv[2]
fields_file = sys.argv[3]
if not validate_resource_id(base_id):
print('错误:无效的 baseId 格式')
sys.exit(1)
if not validate_resource_id(table_id):
print('错误:无效的 tableId 格式')
sys.exit(1)
success = bulk_add_fields(base_id, table_id, fields_file)
sys.exit(0 if success else 1)
if __name__ == '__main__':
main()
@@ -0,0 +1,333 @@
#!/usr/bin/env python3
"""
从 CSV / JSON 批量导入记录到钉钉 AI 表格(新版 schema)
用法:
python import_records.py <baseId> <tableId> data.csv [batch_size]
python import_records.py <baseId> <tableId> data.json [batch_size]
说明:
- CSV 表头默认视为 fieldId
- JSON 支持两种格式:
1. [{"cells": {"fldxxx": "value"}}, ...]
2. [{"fldxxx": "value"}, ...] # 会自动包装成 cells
⚠️ CSV 自动类型转换风险:
CSV 读入的所有 cell 都是 string,本脚本会尝试自动识别 'true'/'false'/数字
并转成对应类型(避免 text 字段塞入纯文本数字)。但当 fieldId 对应的字段是
text / telephone / idCard / barcode 这类"字符串形数字"字段时,自动转 int / float
会让 server 拒绝(字段类型不匹配)。这种情况建议改用 JSON 格式(自己显式控制类型),
或在 CSV 写入前给字段值前缀加引号 / 改为非纯数字。
"""
import sys
import csv
import json
import subprocess
import os
import re
from pathlib import Path
from typing import Union, List, Dict, Any, Optional, Tuple
JsonData = Union[List[Any], Dict[str, Any]]
RecordDict = Dict[str, str]
MAX_FILE_SIZE = 50 * 1024 * 1024
ALLOWED_CSV_EXTENSIONS = ['.csv']
ALLOWED_JSON_EXTENSIONS = ['.json']
RESOURCE_ID_PATTERN = re.compile(r'^[A-Za-z0-9_-]{8,128}$')
MAX_RECORDS_PER_BATCH = 100
DEFAULT_BATCH_SIZE = 50
def resolve_safe_path(
path: str, allowed_root: Optional[str] = None
) -> Path:
if allowed_root is None:
allowed_root = os.environ.get('OPENCLAW_WORKSPACE', os.getcwd())
allowed_root = Path(allowed_root).resolve()
target_path = (
Path(path).resolve()
if Path(path).is_absolute()
else (Path.cwd() / path).resolve()
)
try:
target_path.relative_to(allowed_root)
return target_path
except ValueError:
raise ValueError(
f"路径超出允许范围:{path}\n"
f"目标路径:{target_path}\n"
f"允许根目录:{allowed_root}\n"
f"提示:设置 OPENCLAW_WORKSPACE 环境变量或确保文件在工作目录内"
)
def validate_resource_id(resource_id: str) -> bool:
return bool(
resource_id and RESOURCE_ID_PATTERN.match(resource_id.strip())
)
def validate_file_extension(
filename: str, allowed_extensions: list
) -> bool:
return any(filename.lower().endswith(ext) for ext in allowed_extensions)
def safe_csv_load(
file_path: Path, max_size: int = MAX_FILE_SIZE
) -> List[RecordDict]:
file_size = file_path.stat().st_size
if file_size > max_size:
raise ValueError(
f"文件过大:{file_size:,} 字节 (限制:{max_size:,} 字节)"
)
with open(file_path, 'r', encoding='utf-8', newline='') as f:
return list(csv.DictReader(f))
def safe_json_load(
file_path: Path, max_size: int = MAX_FILE_SIZE
) -> JsonData:
file_size = file_path.stat().st_size
if file_size > max_size:
raise ValueError(
f"文件过大:{file_size:,} 字节 (限制:{max_size:,} 字节)"
)
with open(file_path, 'r', encoding='utf-8') as f:
return json.load(f)
def sanitize_record_value(
value: Any,
) -> Optional[Union[str, int, float, bool, list, dict]]:
if value is None:
return None
if isinstance(value, (bool, int, float, list, dict)):
return value
if not isinstance(value, str):
return value
if not value.strip():
return None
value = value.strip()
if value.lower() == 'true':
return True
if value.lower() == 'false':
return False
try:
if '.' in value:
return float(value)
return int(value)
except ValueError:
return value
def normalize_record(record: Dict[str, Any]) -> Dict[str, Any]:
if 'cells' in record and isinstance(record['cells'], dict):
cells = record['cells']
else:
cells = record
normalized = {}
for key, value in cells.items():
sanitized = sanitize_record_value(value)
if sanitized is not None:
normalized[key] = sanitized
return {'cells': normalized}
def validate_record(record: Dict[str, Any]) -> Tuple[bool, str]:
if not isinstance(record, dict):
return False, '记录必须是对象'
normalized = normalize_record(record)
cells = normalized.get('cells', {})
if not cells or not isinstance(cells, dict):
return False, '记录必须包含非空 cells 对象'
return True, ''
def run_dws(args: List[str]) -> Optional[Dict[str, Any]]:
if not args:
print('错误:空命令')
return None
cmd = ['dws'] + args
try:
result = subprocess.run(
cmd, capture_output=True, text=True, timeout=120
)
if result.returncode != 0:
print(f"错误:{result.stderr.strip()}")
return None
try:
return json.loads(result.stdout)
except json.JSONDecodeError as e:
print(f"无法解析响应:{result.stdout[:200]}...")
print(f"JSON 解析错误:{e}")
return None
except subprocess.TimeoutExpired:
print('错误:命令执行超时(120 秒)')
return None
except FileNotFoundError:
print('错误:未找到 dws 命令,请确认已安装')
return None
def import_from_csv(
base_id: str, table_id: str, csv_file: str,
batch_size: int = DEFAULT_BATCH_SIZE,
) -> bool:
try:
safe_path = resolve_safe_path(csv_file)
except ValueError as e:
print(f"路径验证失败:{e}")
return False
if not validate_file_extension(csv_file, ALLOWED_CSV_EXTENSIONS):
print(f"错误:只允许 {', '.join(ALLOWED_CSV_EXTENSIONS)} 文件")
return False
if not safe_path.exists():
print(f"错误:文件不存在:{safe_path}")
return False
try:
rows = safe_csv_load(safe_path)
except ValueError as e:
print(f"错误:{e}")
return False
except csv.Error as e:
print(f"错误:CSV 格式无效:{e}")
return False
if not rows:
print('错误:CSV 文件为空或没有有效数据行')
return False
records = [
normalize_record(row)
for row in rows
if normalize_record(row)['cells']
]
return import_records(base_id, table_id, records, batch_size)
def import_from_json(
base_id: str, table_id: str, json_file: str,
batch_size: int = DEFAULT_BATCH_SIZE,
) -> bool:
try:
safe_path = resolve_safe_path(json_file)
except ValueError as e:
print(f"路径验证失败:{e}")
return False
if not validate_file_extension(json_file, ALLOWED_JSON_EXTENSIONS):
print(f"错误:只允许 {', '.join(ALLOWED_JSON_EXTENSIONS)} 文件")
return False
if not safe_path.exists():
print(f"错误:文件不存在:{safe_path}")
return False
try:
records = safe_json_load(safe_path)
except ValueError as e:
print(f"错误:{e}")
return False
except json.JSONDecodeError as e:
print(f"错误:JSON 格式无效:{e}")
return False
if not isinstance(records, list) or not records:
print('错误:JSON 文件必须是非空数组')
return False
for i, record in enumerate(records):
valid, error = validate_record(record)
if not valid:
print(f"错误:记录 #{i+1} 格式无效:{error}")
return False
return import_records(
base_id, table_id,
[normalize_record(r) for r in records], batch_size,
)
def import_records(
base_id: str, table_id: str,
records: List[Dict[str, Any]], batch_size: int,
) -> bool:
if batch_size <= 0:
print('错误:batch_size 必须大于 0')
return False
if batch_size > MAX_RECORDS_PER_BATCH:
batch_size = MAX_RECORDS_PER_BATCH
total_batches = (len(records) + batch_size - 1) // batch_size
success = True
for i in range(0, len(records), batch_size):
batch = records[i:i + batch_size]
batch_num = (i // batch_size) + 1
records_json = json.dumps(batch, ensure_ascii=False)
result = run_dws([
'aitable', 'record', 'create',
'--base-id', base_id,
'--table-id', table_id,
'--records', records_json,
'--format', 'json',
])
if result:
print(
f"[{batch_num}/{total_batches}] "
f"✓ 已提交 {len(batch)} 条记录"
)
else:
print(f"[{batch_num}/{total_batches}] ✗ 导入失败")
success = False
return success
def main():
if len(sys.argv) < 4 or len(sys.argv) > 5:
print(__doc__)
print('用法示例:')
print(
' python import_records.py basexxx tablexxx data.csv 50'
)
sys.exit(1)
base_id = sys.argv[1]
table_id = sys.argv[2]
input_file = sys.argv[3]
batch_size = (
int(sys.argv[4]) if len(sys.argv) == 5
else DEFAULT_BATCH_SIZE
)
if not validate_resource_id(base_id):
print('错误:无效的 baseId 格式')
sys.exit(1)
if not validate_resource_id(table_id):
print('错误:无效的 tableId 格式')
sys.exit(1)
if input_file.lower().endswith('.csv'):
success = import_from_csv(
base_id, table_id, input_file, batch_size
)
elif input_file.lower().endswith('.json'):
success = import_from_json(
base_id, table_id, input_file, batch_size
)
else:
print('错误:仅支持 .csv 或 .json 文件')
sys.exit(1)
sys.exit(0 if success else 1)
if __name__ == '__main__':
main()
@@ -0,0 +1,190 @@
#!/usr/bin/env python3
"""
上传附件到钉钉 AI 表格 attachment 字段
完整流程(内部自动执行 3 步):
1. dws aitable attachment upload → 获取 uploadUrl + fileToken
2. HTTP PUT 上传文件到 OSS
3. 返回 fileToken,可直接用于 record create/update
用法:
python upload_attachment.py <baseId> <filePath>
输出 (JSON):
{ "fileToken": "ft_xxx", "fileName": "report.pdf", "size": 204800 }
然后在 record create/update 中使用:
dws aitable record create --base-id <BASE_ID> --table-id <TABLE_ID> \
--records '[{"cells":{"fldAttachId":[{"fileToken":"ft_xxx"}]}}]' --format json
"""
import sys
import json
import subprocess
import os
import mimetypes
import re
from pathlib import Path
from typing import Optional, Dict, Any
from urllib.request import Request, urlopen
from urllib.error import HTTPError, URLError
RESOURCE_ID_PATTERN = re.compile(r'^[A-Za-z0-9_-]{8,128}$')
MAX_FILE_SIZE = 100 * 1024 * 1024 # 100MB
def validate_resource_id(resource_id: str) -> bool:
return bool(resource_id and RESOURCE_ID_PATTERN.match(resource_id.strip()))
def detect_mime_type(file_path: Path) -> str:
"""根据文件扩展名推断 MIME type。"""
mime_type, _ = mimetypes.guess_type(str(file_path))
return mime_type or 'application/octet-stream'
def run_dws(args: list) -> Optional[Dict[str, Any]]:
"""调用 dws 命令并返回解析后的 JSON 结果。"""
cmd = ['dws'] + args
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
if result.returncode != 0:
print(f"错误:dws 命令失败: {result.stderr.strip()}", file=sys.stderr)
return None
try:
return json.loads(result.stdout)
except json.JSONDecodeError:
print(f"错误:无法解析 dws 响应: {result.stdout[:300]}", file=sys.stderr)
return None
except subprocess.TimeoutExpired:
print('错误:dws 命令超时(60 秒)', file=sys.stderr)
return None
except FileNotFoundError:
print('错误:未找到 dws 命令,请确认已安装并在 PATH 中', file=sys.stderr)
return None
def upload_to_oss(upload_url: str, file_path: Path, mime_type: str) -> bool:
"""通过 HTTP PUT 上传文件到 OSS。"""
file_data = file_path.read_bytes()
req = Request(upload_url, data=file_data, method='PUT')
req.add_header('Content-Type', mime_type)
try:
with urlopen(req, timeout=120) as resp:
if resp.status == 200:
return True
print(f"错误:OSS 上传失败,HTTP {resp.status}", file=sys.stderr)
return False
except HTTPError as e:
print(f"错误:OSS 上传 HTTP 错误 {e.code}: {e.reason}", file=sys.stderr)
return False
except URLError as e:
print(f"错误:OSS 上传网络错误: {e.reason}", file=sys.stderr)
return False
def upload_attachment(base_id: str, file_path_str: str) -> Optional[Dict[str, Any]]:
"""
执行完整的附件上传流程:
1. prepare_attachment_upload → uploadUrl + fileToken
2. PUT 文件到 OSS
3. 返回 fileToken 信息
"""
# 验证文件
file_path = Path(file_path_str).resolve()
if not file_path.exists():
print(f"错误:文件不存在: {file_path}", file=sys.stderr)
return None
if not file_path.is_file():
print(f"错误:不是文件: {file_path}", file=sys.stderr)
return None
file_size = file_path.stat().st_size
if file_size <= 0:
print("错误:文件为空", file=sys.stderr)
return None
if file_size > MAX_FILE_SIZE:
print(f"错误:文件过大 ({file_size:,} 字节,限制 {MAX_FILE_SIZE:,} 字节)", file=sys.stderr)
return None
file_name = file_path.name
mime_type = detect_mime_type(file_path)
# 步骤 1: prepare_attachment_upload
print(f"步骤 1/3: 准备上传 {file_name} ({file_size:,} 字节, {mime_type})...", file=sys.stderr)
dws_args = [
'aitable', 'attachment', 'upload',
'--base-id', base_id,
'--file-name', file_name,
'--size', str(file_size),
'--mime-type', mime_type,
'--format', 'json',
]
result = run_dws(dws_args)
if not result:
return None
status = result.get('status', '')
if status != 'success':
error = result.get('error', {})
print(f"错误:准备上传失败: {error.get('message', json.dumps(error, ensure_ascii=False))}", file=sys.stderr)
return None
data = result.get('data', {})
upload_url = data.get('uploadUrl', '')
file_token = data.get('fileToken', '')
if not upload_url or not file_token:
print(f"错误:返回数据缺少 uploadUrl 或 fileToken: {json.dumps(data, ensure_ascii=False)}", file=sys.stderr)
return None
# 步骤 2: PUT 文件到 OSS
print(f"步骤 2/3: 上传文件到 OSS...", file=sys.stderr)
if not upload_to_oss(upload_url, file_path, mime_type):
return None
# 步骤 3: 返回 fileToken
print(f"步骤 3/3: 上传完成!", file=sys.stderr)
output = {
"fileToken": file_token,
"fileName": file_name,
"size": file_size,
"mimeType": mime_type,
}
return output
def main():
if len(sys.argv) != 3:
print(__doc__)
print('用法:')
print(' python upload_attachment.py <baseId> <filePath>')
print()
print('示例:')
print(' python upload_attachment.py G1DKw2zgV2bEk6PMSBooNxlEVB5r9YAn ./report.pdf')
print()
print('然后在 record create 中使用返回的 fileToken:')
print(' dws aitable record create --base-id <BASE_ID> --table-id <TABLE_ID> \\')
print(' --records \'[{"cells":{"fldAttachId":[{"fileToken":"ft_xxx"}]}}]\' --format json')
sys.exit(1)
base_id = sys.argv[1]
file_path = sys.argv[2]
if not validate_resource_id(base_id):
print('错误:无效的 baseId 格式', file=sys.stderr)
sys.exit(1)
result = upload_attachment(base_id, file_path)
if result is None:
sys.exit(1)
# 正常输出到 stdout(JSON 格式,方便解析)
print(json.dumps(result, ensure_ascii=False, indent=2))
sys.exit(0)
if __name__ == '__main__':
main()
+116
View File
@@ -0,0 +1,116 @@
---
name: dingtalk-calendar
description: 钉钉日历与会议室。Use when 用户说 约会议/查日程/订会议室/查闲忙/加参会人/改期/取消会议/今天的日程/本周日程/共同空闲。不做视频会议发起/邀请入会/会中控制(走 dingtalk-misc)、AI 听记(走 dingtalk-minutes)、待办任务(走 dingtalk-todo)。命令前缀:dws calendar。
metadata:
cli_version: ">=0.2.14"
category: product
requires:
bins:
- dws
---
# 钉钉日历 Skill
## 前置条件 — 执行操作前必读
> **CRITICAL — 执行任何 `dws` 操作前,MUST 先用 Read 工具完整读取 [`dingtalk-shared`](../dingtalk-shared/SKILL.md)。**该轻量文件包含全局执行契约、安全底线及 shared references 的按需加载导航;不要预加载其全部 references。
> 命令参考:[calendar.md](references/calendar.md);剧本:[03-meeting.md](references/03-meeting.md)。
<!-- VISIBLE_SHORTCUTS_START -->
## Shortcuts(无专用脚本/recipe 时优先)
以下 shortcut 同时进入公开 catalog 与 Runtime Schema。先按本 skill 的意图表、脚本和 recipe 路由:存在精确覆盖该场景的专用脚本/recipe 时按其执行;否则用户意图命中时,shortcut 优先于手写原子命令。命令已选中时直接执行;只在参数或安全语义不确定时读取 Agent leaf Schema(例如 `dws schema --cli-path "calendar +<shortcut>" --compact --format json`),在当前 Cobra flags 不确定时读取 `dws calendar <shortcut> --help`。只有参数映射、接口绑定或 provenance 审计才省略 `--compact`。仅当现有路由和 reference 都无法定位低频能力时,才用 `dws shortcut list --service calendar --format json` 批量发现。
| Shortcut | 风险 | 适用场景 |
|---|---|---|
| `dws calendar +agenda` | read | 查询日程列表(不传时间默认查询今天) |
| `dws calendar +attendee-list` | read | 查看日程参会人 |
| `dws calendar +book` | write | 创建日程,并可按姓名邀请参会人(自动解析 userId,失败自动回滚删除日程) |
| `dws calendar +book-list` | read | 查询用户的日历本列表 |
| `dws calendar +book-search` | read | 按名称模糊搜索日历本 |
| `dws calendar +cancel-event` | high-risk-write | 取消(删除)一个已有日程(删除前先确认它真实存在) |
| `dws calendar +conflicts` | read | 检测我某天日程的时间冲突(重叠/双重预订,默认今天) |
| `dws calendar +free` | read | 按姓名查询某人在指定时间段内的忙闲状态(自动解析 userId) |
| `dws calendar +free-slots` | read | 找我某天工作时段内的空闲时间段(默认今天 09:00-18:00 |
| `dws calendar +freebusy` | read | 查询用户 / 会议室闲忙状态(--users 与 --rooms 至少其一) |
| `dws calendar +invite` | write | 按姓名把参会人加入已有日程(自动解析 userId 后批量添加) |
| `dws calendar +my-free` | read | 查我自己在某时间段的忙闲(默认今天,无需输入姓名) |
| `dws calendar +next-event` | read | 查看接下来最近的一个日程(默认扫描未来 7 天) |
| `dws calendar +reschedule` | write | 改一个已有日程的时间(只动开始/结束时间,其他字段不变) |
| `dws calendar +room-find` | read | 按时间段搜索可用会议室(不传时间默认当前起 1 小时) |
| `dws calendar +room-groups` | read | 会议室分组列表 |
| `dws calendar +room-search` | read | 按名称模糊搜索会议室(不检查可用性) |
| `dws calendar +suggest-time` | read | 按姓名解析多位参与者,推荐大家都有空的可开会时间段(自动解析 userId) |
| `dws calendar +today` | read | 列出我今天的日程(自动计算今天的起止时间,无需手动填时间范围) |
| `dws calendar +tomorrow` | read | 列出我明天的日程(自动计算明天的起止时间,无需手动填时间范围) |
| `dws calendar +week` | read | 列出我本周的日程(自动按周一为周首计算本周起止时间,无需手动填时间范围) |
<!-- VISIBLE_SHORTCUTS_END -->
## 意图表
| 用户说 | 命令 |
|--------|------|
| "今天 / 明天 / 本周日程" | `python scripts/calendar_today_agenda.py [today\|tomorrow\|week]` |
| "约会议(含参会人 + 会议室)" | `python scripts/calendar_schedule_meeting.py --title "<主题>" --start "<起>" --end "<止>" [--users <ids>] [--book-room]` |
| "多人共同空闲" | `python scripts/calendar_free_slot_finder.py --users <ids> --date <yyyy-MM-dd>` |
| "查闲忙" | `dws calendar busy search --users <userIds> --start "<ISO>" --end "<ISO>"` |
| "加参会人" / "订房" / "取消" | `dws calendar attendee add` / `room add` / `event delete` |
## 标准 SOP(必遵流程)
> 命中以下意图**必须**按对应 SOP 顺序执行;**禁止**跳步、替换命令、编造 userId/eventId。每条命令必须带 `--format json`,时间参数**必须**是 ISO-8601(如 `2026-07-03T14:00:00+08:00`)。
### SOP-1 查日程(list-events
**触发**:今天/明天/本周日程/我有什么会/某时段日程。
1. **首选脚本(必须)**`python scripts/calendar_today_agenda.py today|tomorrow|week`(聚合今日议程)。
2. **降级 CLI(必须)**:脚本不可用时 `dws calendar event list --start "<起始ISO>" --end "<结束ISO>" --format json`;不传 `--start/--end` 默认查今天(00:00:00~23:59:59)。`hasMore=true``--limit`/翻页。
3. **解析(必须)**:取真实 `eventId``attendees[]``start/end`;按需抽取,**禁止**把整段 JSON 原样贴出。
**禁止**:用 `event list` 替代闲忙查询(查闲忙走 SOP-3)、编造时间窗口、用非 ISO 时间格式。
### SOP-2 建日程(create-event
**触发**:建日程/约会议/加日程。
1. **解析与会人(必须)**:对每个姓名 `dws aisearch person --query "<姓名>" --dimension name --format json``userId`,多人逗号拼接。
2. **执行(必须)**`dws calendar event create --title "<主题>" --start "<ISO>" --end "<ISO>" --attendees <userId1,userId2> --format json`(按需加 `--location`/`--desc`/`--rooms`)。
3. **验证(必须)**:从返回 `result.id` 取日程 ID(下游参数语义称 `eventId`),再执行 `dws calendar event list --start "<ISO>" --end "<ISO>" --format json` 复核标题、描述和时段。
**禁止**:跳过与会人 userId 解析直接传姓名、编造会议室 roomId。
### SOP-3 查闲忙(check-busy
**触发**:某人/会议室是否有空/找空闲时段/避免冲突。
1. **解析对象(必须)**:姓名 → `dws aisearch person --query "<姓名>" --dimension name --format json``userId`;会议室用 `roomId`
2. **收敛时段(必须)**`--start`/`--end` **必须**由用户给出或明确收敛;时段不明确**必须先追问****禁止**默认全天窗口。
3. **执行(必须)**`dws calendar busy search --users <userId1,userId2> --start "<ISO>" --end "<ISO>" --format json`(查会议室换 `--rooms <roomId...>`,可同时传)。**禁止**用 `event list` 扫日程替代闲忙查询。
4. **空闲时段(必须)**:找共同空闲用 `python scripts/calendar_free_slot_finder.py`
**禁止**:用 `event list` 冒充 `busy search`、未确认时段就默认全天查询。
## 执行硬约束
- 多轮日程任务必须保留 `eventId`,后续加人、移人、订房、换房、改描述、删除都基于同一个 `eventId` 执行;不要重新创建重复日程。
- 用户明确说"帮我订一个空闲会议室"时,`room search` 返回可用会议室后直接选择第一个可预订且不需要自定义审批的 `roomId` 执行 `room add`;不要把选择权抛回用户导致任务停住。
- 已有日程订房:`dws calendar room search --start ... --end ... --format json``dws calendar room add --event <EVENT_ID> --rooms <ROOM_ID> --format json``event get``room/busy` 验证。
- 换会议室:先 `room delete --event <EVENT_ID> --rooms <OLD_ROOM_ID>`,再 `room add --event <EVENT_ID> --rooms <NEW_ROOM_ID>`,最后回查;不要只更新 `--location`
- 参会人变化用 `attendee add/delete`,日程描述变化用 `event update --desc`,删除日程用 `event delete --id`。用户当前消息已明确要求删除/取消时可直接执行;否则先确认。
- 脚本失败或参数不完整时,立即降级到明确的 `dws calendar event/attendee/room` 命令,不要停在"我要查看用法"。
- 所有 dws 命令带 `--format json`;查询时间必须显式 `--start` / `--end`
## 跨产品协作
- 视频会议发起 / 入会链接 / 邀请入会 / 会中控制 → 当前 CLI **不支持**;请在钉钉客户端完成
- 会后摘要 / 待办 → 切到 `dingtalk-minutes`
- 参会人按人名 → 先用 `dingtalk-aisearch` 解析
## 注意
`schedule-meeting` 必须读 [03-meeting.md](references/03-meeting.md) 中的「两准则」「搜房失败硬门禁」,禁止假设 `roomId`
## 局部意图与短流程
- [局部意图消歧](references/intent-guide.md)[短流程](references/lite-recipes.md)。
@@ -0,0 +1,56 @@
# 会议管理(日程与会议室)
> lite recipe`list-today-meetings`、`check-users-busy`)见 [lite-recipes.md](./lite-recipes.md);视频会议 `start-conference` 当前 CLI 不支持。列表类操作须遵循 [calendar.md](./calendar.md) **「CLI 命令树与黄金路径」**,禁止无子命令的 `dws calendar` 或臆造 `calendar list`(见该文 **「反模式(禁止)」**)。**`schedule-meeting` 不做内联**:须读本文件 **「两准则」「搜房失败硬门禁」** 及下表 **schedule-meeting** 行全文。
> **听记、会后待办、摘要分享** 见 `dingtalk-minutes/references/07-minutes.md`。
## 日程与会议室两准则(强制)
1. **时段**:用户已明确会议起止时间 → **禁止**自动改期、禁止用闲忙结果或「推荐时段」覆盖用户给定时段;只能在此时段内建日程、订会议室;该时段内无可用或指定资源不可用 → **立刻如实告知**,不得偷偷换时间段再试。
2. **会议室**:用户点名具体会议室 → **禁止**换其他会议室;在用户给定时段内查无该房 → **立刻告知**。**用户未给出时段时,必须先显式向用户追问具体开始/结束时间;禁止默认用「当前时刻至当日 23:59:59」之类窗口代查。** **`calendar_schedule_meeting.py`**:仅需 `--title``--start``--end`;先创建日程,再邀请参会人,最后搜房/订房。未给会议室范围时,`--book-room`**单次**无 `--group-id` 的 `room search --available`,取返回的**第一个**会议室并 `room add`;无结果则告警、不删日程。**若用户明确限定楼层/楼宇/园区/分组,应先用 `room list-groups` 解析允许的 `group-id`,再把这些 `group-id` 传给脚本 `--room-group-id`(或手工 `room search --group-id ...`);脚本只会在这些 group 内查找,若无空房则直接返回。对于同一地点(同园区/楼栋/楼层)的会议室,必须优先锁定最相关、最贴近该地点的承载 group;该 group 查无 roomId/空房,即可判定该地点当前时段无可订会议室,**不得**再去别的无关 group 继续碰运气,因为同一地点的会议室只会挂在其所属 group 下。** **在组织内、按早停规则已把应查的分组(或未限范围时的根目录一次查询)全部查完仍无可用会议室时,必须立即向用户说明「当前时段没有可预订的会议室」或「范围内未检索到可用会议室/资源」并收束,禁止继续扩区、换参重试或虚构有房。** 手工 `room search` **禁止**为试出空闲擅自改日或拉长时间窗。
### 会议室搜索早停
> 专用于 `calendar room list-groups` / `room search` / `room add`;与通用规范「无新参数不重复 search」一致。
**`room search --available`**(与传入的 `--start` / `--end` 配对):返回的是在**该整段时段内**可被预订的空闲会议室(不是「有一段空就算」);脚本与用户手工选房均应沿用同一时间窗,避免误以为分段凑满即等价于整段可用。
**`dws calendar room search` 合法参数**(与 [calendar.md](./calendar.md) 一致):仅 `--start``--end``--group-id`(可选)、`--available`(可选)、`--format json` 等;**禁止使用 `--query`**,否则会报 `unknown flag: --query`
**地点归组早停**:若用户给的是同一地点范围(如“西溪园区 C6 楼 3-5 层”或具体楼层/楼栋),先用 `room list-groups` 找到**最相关的承载 group**(通常是该楼层;若楼层下无会议室则为直接挂会议室的上一级)。在这个最相关 group 下查不到有效 `rooms[].roomId` 或空房时,**不得**再跳去别的同级/异地 group 继续搜;同一地点的会议室不会散落在别的 group 里。只有用户明确放宽到别的楼层、楼栋或园区,才能重新解析新的 group 并继续。
**用户点名具体会议室(如「C6-4-06-N / 贡嘎山」)****不要**尝试 `room search --query "<名称>"`;**禁止**把用户原文(含「C6-4-06-N 贡嘎山」整句)或展示名当作 `room add --rooms``roomId`。用户输入**几乎从不会是**有效 `roomId`。须先 `dws calendar room list-groups` 定位所在楼层/分组的 `group-id`,再 `dws calendar room search --start "<ISO>" --end "<ISO>" --group-id <GROUP_ID> [--available] --format json`,在返回 `rooms[]` 中对 `roomName``name` 等与用户表述匹配,**仅**取 JSON 里的 `roomId`(典型为小写十六进制串,长度以返回为准),最后 `dws calendar room add --event <eventId> --rooms <roomId>`。该时段无匹配或房间忙 → 如实告知;**禁止**为通过校验而编造、拼接或猜测 `roomId`
### 搜房失败硬门禁(园区/范围搜尽仍无 roomId)
在用户限定的园区、楼宇、楼层或固定分组内,已按早停规则**逐组 `room search` 查完**仍得不到任何有效 `rooms[].roomId`(或无任何空闲房)→ **立即停止**,向用户**明确报错/失败结论**(例如:该时段在指定范围内未检索到可预订会议室或无法获得 roomId),**本回合订房流程结束**。
**用户汇报硬门禁**:一旦触发上面的失败条件,**下一条对外输出必须直接面向用户汇报结果**,不得继续在会话里自言自语式地延长推理。允许的后续只有两类:
1. **失败汇报**:明确说明“指定范围/指定会议室在该时段未找到可预订会议室,因此当前无法完成预订”
2. **确认放宽条件**:仅在需要继续推进时,明确问用户是否放宽地点范围、改时间或接受不订会议室
以下表述/行为视为**违例**:继续写“让我再试一次”“也许是 Mock/测试环境”“可能存在预设 roomId 映射”“我去别的 group 看看”“我换个时间验证一下”“我先看看脚本/示例还能不能推断出 roomId”。
以下行为**一律禁止**(与是否「想多试一次」无关):编造/假设 `roomId` 格式做「预订测试」;在**没有**合法 `roomId` 时调用 `room add` 试探错误详情;拉 `event get` / 日程详情等试图**绕开** `room search` 推断 roomId;换无关园区、扩大关键词、换工具名做未经用户授权的新搜索。
**失败后强制回读**:若出现以下任一信号,下一步**必须重新读取本文件本节与 `schedule-meeting` recipe**,不得沿着当前假设继续试:
1. 连续 **2 次** `room search` 空结果/无 `roomId`
2. 任意一次 `roomId invalid`
3. 已开始尝试「换园区 / 换楼栋 / 看 event 详情 / 猜 roomId」
回读后只允许二选一:
1. **报错收束**:已搜尽允许范围/整园仍无 `roomId` 或无空房
2. **用户确认**:明确询问是否放宽范围、换时间,或接受不订会议室
| # | 规范 |
|---|------|
| 1 | **一键脚本**`calendar_schedule_meeting.py` 做「建日程 → 加人 → 可选搜房/订房」;未限范围时可直接 `--book-room`,脚本按根目录单次 `room search --available` 订第一家。**若搜房失败,脚本应输出明确失败原因并返回非零退出码,促使上层立即向用户汇报,而不是继续试探。** |
| 2 | **要限范围/具名**:先 `list-groups` 解析允许的 `group-id`。若用户说的是同一地点(同园区/楼栋/楼层),应优先锁定**最相关的承载 group** 并只查它;该 group 无结果即可按该地点无房收束,不再试别的无关 group。仅当用户明确给出多个允许地点时,才分别对这些 group 各 **1 次** `room search --available``room add` |
| 3 | **禁止**:无新信息时反复 `--verbose`、反复切 `--available`、父组子组试探、在最相关 group 无结果后改搜别的同级/异地 group、超 100 条后仍根分组或未授权区域全量搜;**禁止**对 `room search` 使用不存在的 `--query` |
| 4 | **`roomId` 门禁**`room add --rooms` **只能**填 `room search` 返回 JSON 中的 `rooms[].roomId`;**禁止**将用户说的会议室名、编号文案、或「假 UUID / 试数字」当作 `roomId` |
| 5 | **全量无结果即收束**:在用户允许的搜索范围内(含**整园/全 campus** 若用户要求已逐组查尽)仍无任何可用会议室或有效 `roomId`**直接报错/告知失败并结束订房**,且**下一条消息必须汇报给用户**;**不得**假设 ID、不得用 `room add` 试探、不得绕路查日程、不得继续自说自话分析 Mock/测试环境 |
| 6 | **失败触发回读**:连续 2 次空结果、任意一次 `roomId invalid`、或开始换园区/绕路时 → **必须回读本节**;回读后只允许「报错收束」或「向用户确认是否放宽条件」 |
| Recipe | 行动指南(固定路线) |
| ------------------ | ------------------- |
| schedule-meeting | **见上文「两准则」**、**「搜房失败硬门禁」**。**未给时段且仅说「发起/开个会」**→ 不走本 recipe;当前 CLI 不支持实时视频会议,告知用户请在钉钉客户端操作。**未给时段但有预约意图**("安排""约""定"等词):追问具体开始/结束时间。**已有时段后**,按固定顺序执行:1. `dws calendar event create` 建日程;2. 有参会人则 `dws calendar participant add`;3. 再处理会议室。**无明确会议室范围**:可直接 `python scripts/calendar_schedule_meeting.py --title "<主题>" --start "<起始>" --end "<结束>" [--users <userIds>] [--book-room] [--dry-run]`。**有明确范围(某楼/层)**:先 `dws calendar room list-groups`,锁定该地点**最相关的承载 group**;若只有一个地点,`--room-group-id` 应只传这个最相关 group,**不要**把同楼内多个楼层 group 打包传入碰运气。只有用户明确给出多个允许地点时,才把这些 `group-id` 一并传给 `python scripts/calendar_schedule_meeting.py ... --book-room --room-group-id "<id1,id2,...>"`。**用户点名具体会议室**:须手工 `dws calendar room search --start "<ISO>" --end "<ISO>" --group-id <GROUP_ID> [--available] --format json`**无** `--query`),在 JSON 中匹配名称取 **`rooms[].roomId` 唯一真值** → `dws calendar room add --event <eventId> --rooms <roomId>`**不得**把用户输入的会议室名当 `roomId`。**一旦连续 2 次空结果 / 任意一次 `roomId invalid`**:**必须回读本节并立即收束判断**;若整园/限定范围内搜尽仍无 roomId 或无空房 → **下一条消息必须直接向用户汇报失败结论**;否则只能向用户确认是否放宽范围/改时间。**禁止**假设 roomId、禁止无 ID 调用 `room add`、禁止用日程详情绕路、禁止继续猜测 Mock/测试环境。细则见「会议室搜索早停」。 |
| reschedule-meeting | 1. `calendar event list --start "<起始ISO>" --end "<结束ISO>"` → 取 `eventId` 2. `calendar event update --id <eventId> --start "<新起始ISO>" --end "<新结束ISO>"` 更新时间 3. `chat search --query "<群名>"` → 取 `openConversationId``chat message send --conversation-id <openConversationId> --content "<变更通知>"` 通知变更 |
@@ -0,0 +1,756 @@
# 日历 (calendar) 命令参考
## CLI 命令树与黄金路径
- **二级子命令(必选其一)**`event`(日程)、`attendee`(参会人)、`room`(会议室)、`busy`(闲忙)、`attachment`(日程附件)、`book`(我能看哪些日历本)、`acl`(我的日历共享给了谁)。`dws calendar` 后**必须**紧跟上述之一;**禁止**只执行 `dws calendar`(无子命令)。
- **个人日程 / 给自己留时间块 / 专注时段**:统一走 **`dws calendar event create`**。当前**没有**单独的 `personal schedule create` / `calendar create` 命令。
- **查日程列表**`dws calendar event list --start "<ISO-8601>" --end "<ISO-8601>" --format json`,或优先使用脚本 `python scripts/calendar_today_agenda.py [today|tomorrow|week]`(见文末「自动化脚本」)。
- **查循环日程实例**`dws calendar event instances --id <EVENT_ID> --start "<ISO-8601>" --end "<ISO-8601>" --format json`,用于按时间范围展开重复日程的每一个实例。**注意:此接口只能查询重复性日程;普通非循环日程将查不到任何实例信息。**
- **查用户日历本列表**`dws calendar book list`(返回主日历 `id == "primary"` 等)。**重要**可以查询他人共享给自己的日历本,根据日历本id可以进一步查询对方的日程信息。
- **CLI 不存在**独立的 `dws calendar list`;若误跑无子命令的 `dws calendar`,会打印整段 Usage,**切勿**将该段 help 当作工具结果再次塞进对话(会急剧增加 token 与首字延迟)。
- **必须**遵循指令说明进行调用。**绝对禁止**使用虚构指令,使用虚构参数。
## 反模式(禁止)
1. **禁止**执行 `dws calendar` 且不带二级子命令(会刷出大量帮助文本)。合法二级子命令:`event` / `attendee` / `room` / `busy` / `attachment` / `book` / `acl`
2. **禁止**使用不存在的子命令试探(如臆造 `dws calendar list`);需要日程列表时一律使用 **`dws calendar event list`**(带 `--start` / `--end`,见下文「查询日程列表」示例);需要日历本列表时使用 **`dws calendar book list`**。
3. **禁止**将完整 `--help`/Usage 输出作为「观察」重复提交给模型;若误触,应直接改用本节黄金路径中的合法命令并重试。
5. **禁止**为已有日程重新创建日程来预订会议室。若日程已存在(同一会话中刚创建、或用户明确指向某日程),必须使用 `room add --event <已有EVENT_ID> --rooms <ROOM_ID>` 追加会议室,**绝不能**再调一次 `event create --rooms`(会创建重复日程)。
6. **禁止**用 `--location` 替代会议室预订。`--location` 是纯文本地点备注字段,填入会议室名称**不会**完成任何预订或占用。预订会议室必须通过 `room add --rooms <roomId>``event create --rooms <roomId>`roomId 来自 `room search` 返回;`--location``--rooms` 是两个独立字段,用途完全不同。
7. **禁止**只传 `--recurrence-*` 部分 flag **并不是彼此独立的参数**:只传其中一项(比如只改 `--recurrence-count`、只设 `--recurrence-type`)会让服务端收到不完整的 recurrence 结构,CLI 现已前置校验并直接拒绝这类调用。**修改已有周期日程的任何一个循环字段时,都必须重新提供完整的 pattern+range 字段集合**——必要时先 `event get` 读取现有 `recurrence`,再在命令中整体重传。
8. **禁止**用一条 指令 实现串行调用。比如当用户要求一次性安排多场不同的日程(例如「上午 10 点开项目评审、下午 2 点开复盘会、晚上 7 点聚餐」)时,必须**拆解成 N 条独立的 `event create`,依次串行执行**;每条命令自己写完整的 `--title` / `--start` / `--end`,绝不能把多个标题或多段时间塞进同一行。
## 核心概念
日历(calendar):日程的容器。每个用户有一个主日历(我的日历,id: primary),还可以订阅公共/团队日历,以及他人共享的日历。
日程(event):日历中的单个日程,包含起止时间、地点、标题、参会人等属性。支持单次日程和重复日程(有recurrence rule的日程,又称SeriesMaster),遵循RFC5545 iCalendar国际标准。
日程实例(event instance):日程的具体时间实例,可以通过event list指令查询时间段内的所有实例。1个普通日程和对应1个Instance,而1个重复性日程(SeriesMaster)对应N个Instance(同属一个日程序列)。
- 同一个日程序列具有相同的iCalUid,并且重复性日程,其eventId和iCalUid的值相同。因此可以通过重复性日程实例的iCalUid得到重复性日程(SeriesMaster)的eventId
重复规则(recurrence rule):定义重复性日程的重复规则。
参会人(attendee):日程的参与者。按姓名统一使用 `dws aisearch person --query "姓名" --dimension name --format json` 查询 userId。
响应状态(response):参会人对日程的回应,包括:未响应、接受、待定、拒绝。
忙闲时间(busy):查询用户在指定时间段的忙闲状态,查询会议室在指定时间段的预定状态,用于会议时间协调。
会议室(room):room是 会议室 ,room可视为日程的资源类参会人,需要加入日程完成预订。注意和location区分,location只是地点,和room不同。
访问控制 (acl):用户可通过设置acl将自己的日历访问权限授予给他人(即:共享日历),授予后,他人就可以查看日历下的日程信息。通过 `dws calendar acl list` 可查看当前要用户已经授权出去的权限。若 privilege >= reader,可查询此日历下的日程数据。若 privilege >= writer ,可操作(创建/修改/删除/响应等)此日历下的日程数据。
共享日历:将自己日历的访问权限授予给他人,用于协作。通过 `dws calendar book list` 查询当前用户日历列表,其中type == `shared` 即他人共享给当前用户的日历。
订阅日历:创建一个公共日历供他人订阅,他人订阅后即可查询日历下的日程数据。通过 `dws calendar book list` 查询当前用户日历列表,其中type == `subscribed` 即当前用户已订阅的日历。注意:订阅日历下的日程无参会人概念,无法执行 attendee 相关指令,也无法添加会议室。
## 命令概览
### event 相关三级子命令
```
# 针对单个日程: 创建 | 修改 | 单查询 | 删除 | 响应日程(接受、暂定、拒绝)
dws calendar event [create|update|get|delete|respond] [flags]
# 按时间范围批量查询
dws calendar event list [flags]
# 查询循环日程的实例列表(按时间范围展开重复日程)
dws calendar event instances [flags]
# 获取日程的分享信息(日程主题、组织人、地点、入会信息等,用于向他人分享日程)
dws calendar event share-info [flags]
# 对于非明确时间或一段时间范围的约会场景,可基于所有参会人的忙闲状态,推荐多个可用的时间块方案
dws calendar event suggest [flags]
```
### attendee 相关三级子命令
```
# 日程中参会人操作:添加 | 删除 | 查询
dws calendar attendee [add|delete|list] [flags]
```
### room 相关三级子命令
```
# 查询分组
dws calendar room list-groups [flags]
# 会议室搜索
dws calendar room search [flags]
# 预定会议室
dws calendar room add [flags]
# 释放会议室
dws calendar room delete [flags]
```
> room是会议室,用于线下开会场景。
### busy 相关三级子命令
```
# 按用户 / 会议室 + 时间窗查闲忙状态(--users 与 --rooms 至少其一),会议室的忙闲等同于预定记录
dws calendar busy search [flags]
```
### attachment 相关三级子命令
```
# 把已上传到钉盘的文件挂到日程上(不负责上传,只负责挂载)
dws calendar attachment add [flags]
```
### book 相关三级子命令(查"我能看哪些日历本")
```
# 查询我拥有和可访问的所有日历本(含他人共享给我的)
dws calendar book list [flags]
# 查询指定日历本信息
dws calendar book get [flags]
# 按名称模糊搜索日历本
dws calendar book search [flags]
# 更新日历本信息(需 owner 权限)
dws calendar book update [flags]
```
### acl 相关三级子命令(查"我的日历共享给了谁")
```
# 查询我的日历共享给了哪些人、各自什么权限
dws calendar acl list [flags]
# 把我的日历共享给某人
dws calendar acl add [flags]
# 取消我的日历对某人的共享
dws calendar acl delete [flags]
```
> **说明**: 可以通过 --help 进一步查看指令明细,也可以继续查看下一节 命令总览
## 命令总览
### 查询日程列表
```
Usage:
dws calendar event list [flags]
Example:
dws calendar event list --start "2026-03-10T14:00:00+08:00" --end "2026-03-10T18:00:00+08:00"
dws calendar event list --start "2026-03-10T00:00:00+08:00" --end "2026-03-31T23:59:59+08:00" --limit 50
dws calendar event list --calendar-id primary
dws calendar event list --cursor "<nextCursor从上一次查询结果获取>"
Flags:
--calendar-id string 日历 ID (默认 primary 主日历,仅在查询其他日历本时填写;通过 `book list` 获取)
--cursor string 分页游标 (从上一次返回的 nextCursor 获取,首次查询无需传入)
--end string 结束时间 ISO-8601 (例如 2026-03-10T18:00:00+08:00)
--limit int 每页返回条数 (默认 100,最大 100)
--start string 开始时间 ISO-8601 (例如 2026-03-10T14:00:00+08:00)
```
**默认行为**:不传 `--start` / `--end` 时,默认返回今天的日程(00:00:00 ~ 23:59:59)。
**权限**:查询共享日历下的日程时,至少要有reader权限。
**分页**:单次最多返回 `--limit` 指定的条数(默认/最大 100);当结果超过 limit 时,返回体包含 `nextCursor` 字段。首次查询无需传 `--cursor`,仅在翻页时将上一次返回的 `nextCursor` 作为 `--cursor` 传入。
### 获取日程详情
```
Usage:
dws calendar event get [flags]
Example:
dws calendar event get --id <EVENT_ID>
dws calendar event get --id <EVENT_ID> --calendar-id primary
Flags:
--id string 日程 ID (必填)
--calendar-id string 日历 ID (默认 primary 主日历)
```
### 查询循环日程实例
```
Usage:
dws calendar event instances [flags]
Example:
dws calendar event instances --id <EVENT_ID>
dws calendar event instances --id <EVENT_ID> --start "2026-03-10T00:00:00+08:00" --end "2026-03-31T23:59:59+08:00"
dws calendar event instances --id <EVENT_ID> --limit 50
dws calendar event instances --id <EVENT_ID> --cursor "<nextCursor>"
Flags:
--id string 日程 ID (必填,重复性日程 SeriesMaster 的 eventId)
--calendar-id string 日历 ID (默认 primary 主日历,仅在查询其他日历本时填写;通过 `book list` 获取)
--start string 开始时间 ISO-8601 (例如 2026-03-10T00:00:00+08:00,不传则默认今天 00:00:00)
--end string 结束时间 ISO-8601 (例如 2026-03-31T23:59:59+08:00,不传则默认今天 23:59:59)
--limit int 每页返回条数 (默认 100,最大 100)
--cursor string 分页游标 (从上一次返回的 nextCursor 获取,首次查询无需传入)
```
> **说明**:用于按时间范围展开重复日程(SeriesMaster)的每一个实例。**此接口只能查询重复性日程;若传入普通非循环日程,将查不到任何实例信息。**`--id` 必须是重复性日程的 eventId,可通过 `event list` 获取。
> **默认行为**:不传 `--start` / `--end` 时,默认返回今天的实例(00:00:00 ~ 23:59:59)。
> **分页**:单次最多返回 `--limit` 指定的条数(默认/最大 100);当结果超过 limit 时,返回体包含 `nextCursor` 字段。
### 获取日程分享信息
```
Usage:
dws calendar event share-info [flags]
Example:
dws calendar event share-info --id <EVENT_ID>
dws calendar event share-info --id <EVENT_ID> --language zh-CN
dws calendar event share-info --id <EVENT_ID> --calendar-id primary
Flags:
--id string 日程 ID (必填)
--calendar-id string 日历 ID (可选,默认 primary 主日历)
--language string 语言代码 (可选,如 zh-CN)
```
> **说明**:根据日程 ID 获取日程的分享信息,展示日程主题、组织人、地点、入会信息等,用于向他人分享日程(如发送到群聊、邮件)。中文内容建议传 `--language zh-CN`。
### 创建日程
```
Usage:
dws calendar event create [flags]
Example:
dws calendar event create --title "Q1 复盘会" \
--start "2026-03-10T14:00:00+08:00" --end "2026-03-10T15:00:00+08:00"
dws calendar event create --title "周会" \
--start "2026-03-10T14:00:00+08:00" --end "2026-03-10T15:00:00+08:00" \
--attendees userId1,userId2
dws calendar event create --title "项目评审" \
--start "2026-03-10T14:00:00+08:00" --end "2026-03-10T15:00:00+08:00" \
--rooms roomId1,roomId2 # 创建时直接预定会议室
dws calendar event create --title "每日站会" \
--start "2026-03-10T09:00:00+08:00" --end "2026-03-10T09:30:00+08:00" \
--recurrence-type daily --recurrence-interval 1 --recurrence-range-type numbered --recurrence-count 10
dws calendar event create --title "团队周会" \
--start "2026-03-10T14:00:00+08:00" --end "2026-03-10T15:00:00+08:00" \
--calendar-id <SHARED_CALENDAR_ID> # 在指定日历本下创建日程
dws calendar event create --title "重要会议" \
--start "2026-03-10T14:00:00+08:00" --end "2026-03-10T15:00:00+08:00" \
--remind-minutes 5,10 # 开始前5分钟和10分钟各提醒一次
Flags:
--title string 日程标题 (必填,最大2048字符)
--start string 开始时间 ISO-8601 (必填,例如 2026-03-10T14:00:00+08:00)
--end string 结束时间 ISO-8601 (必填,例如 2026-03-10T15:00:00+08:00)
--calendar-id string 日历 ID (可选,默认 primary 主日历;仅在共享/订阅日历本下创建时填写,通过 `book list` 获取)
--timezone string 时区 IANA 格式 (例如 Asia/Shanghai,默认 Asia/Shanghai)
--desc string 日程描述 (最大5000字符)
--attendees string 参会人 userId 列表,逗号分隔 (最多500人) 日程组织人自动放入参会人列表,无需传入userId
--open-dingtalk-ids string openDingTalkId 列表,逗号分隔 (与 --attendees 至少传一个)
--rooms string 会议室 roomId 列表,逗号分隔 (创建时直接预定,roomId 必须来自 `room search` 返回,若是循环会议,必须设置recurrence-end-date,避免长期预订)
# 以下 --recurrence-* 一旦使用任一 flag,必须同时提供完整的 pattern+range 字段(至少 --recurrence-type、--recurrence-interval(>0) 与 --recurrence-range-type
# 否则 CLI 会报 "recurrence 结构不完整" 并拒绝执行
--recurrence-type string 循环类型: daily|weekly|absoluteMonthly|relativeMonthly|absoluteYearly
--recurrence-interval int 循环间隔 (如 daily 时表示每N天)
--recurrence-days-of-week string 周几: sunday,monday,...,saturday (weekly/relativeMonthly 时必填)
--recurrence-day-of-month int 每月第几天 (absoluteMonthly/absoluteYearly 时必填)
--recurrence-index string 每月第几周: first|second|third|fourth|last (relativeMonthly 时必填)
--recurrence-first-day-of-week string 一周起始日,默认 sunday
--recurrence-range-type string 循环范围: noEnd|endDate|numbered (与 --recurrence-type 必须成对出现)
--recurrence-end-date string 循环结束时间 ISO-8601 (range-type=endDate 时必填)
--recurrence-count int 循环次数 (range-type=numbered 时必填)
--rich-text-desc string html格式的富文本类型日程描述,用于复杂内容的展示
--location string 地点信息(纯文本备注,如‘3号楼A区’;**不等于**预订会议室)
--free-busy string 此日程的忙碌状态,默认值为busy。busy - 在忙闲视图中,此日程时间段为忙碌; free - 此日程不占用忙闲
--remind-minutes string 日程开始前提醒,逗号分隔分钟数 (可选,例如 5,10,15 表示开始前5/10/15分钟提醒;不传则默认15分钟提醒)
```
> **说明**:个人日程也走 `event create`。如果只是给自己安排时间,不传 `--attendees` / `--open-dingtalk-ids` 即可。
### 修改日程
```
Usage:
dws calendar event update [flags]
Example:
dws calendar event update --id <EVENT_ID> --title "新标题"
dws calendar event update --id <EVENT_ID> --desc "新描述" --timezone Asia/Tokyo
dws calendar event update --id <EVENT_ID> --recurrence-type daily --recurrence-interval 1 \
--recurrence-range-type numbered --recurrence-count 5
dws calendar event update --id <EVENT_ID> --calendar-id <SHARED_CALENDAR_ID> --title "新标题" # 修改其他日历本下的日程
Flags:
--id string 日程 ID (必填)
--calendar-id string 日历 ID (可选,默认 primary 主日历;指定其他日历本时填写,可通过 `book list` 获取)
--title string 新标题
--start string 新开始时间 ISO-8601
--end string 新结束时间 ISO-8601
--desc string 新描述 (最大5000字符)
--timezone string 时区 IANA 格式 (例如 Asia/Shanghai)
# 以下 --recurrence-* 在 修改周期日程的循环规则时必须**整体**传入:MCP 不合并部分字段,只改其中一项(例如只传 --recurrence-count)会把规则覆盖成不完整状态
# 若只想微调已有规则,请先 `event get --id <ID>` 读取现有 recurrence,再在本命令重传完整的 pattern+range
--recurrence-type string 循环类型: daily|weekly|absoluteMonthly|relativeMonthly|absoluteYearly
--recurrence-interval int 循环间隔 (如 daily 时表示每N天)
--recurrence-days-of-week string 周几: sunday,monday,...,saturday (weekly/relativeMonthly 时必填)
--recurrence-day-of-month int 每月第几天 (absoluteMonthly/absoluteYearly 时必填)
--recurrence-index string 每月第几周: first|second|third|fourth|last (relativeMonthly 时必填)
--recurrence-first-day-of-week string 一周起始日,默认 sunday
--recurrence-range-type string 循环范围: noEnd|endDate|numbered (与 --recurrence-type 必须成对出现)
--recurrence-end-date string 循环结束时间 ISO-8601 (range-type=endDate 时必填)
--recurrence-count int 循环次数 (range-type=numbered 时必填)
--rich-text-desc string html格式的富文本类型日程描述,用于复杂内容的展示
--location string 地点信息(纯文本备注,如‘3号楼A区’;**不等于**预订会议室)
--free-busy string 修改此日程的忙碌状态,无需修改则不传。busy - 在忙闲视图中,此日程时间段为忙碌; free - 此日程不占用忙闲
```
> 支持修改标题、描述、时间、地点、忙碌状态等。如需修改会议室,请使用 dws calendar room [add|delete];如需修改参会人,请使用 dws calendar attendee [add|delete]
### 删除日程
> **CAUTION:** 不可逆操作 — 所有参会人同步取消,必须先向用户确认。
```
Usage:
dws calendar event delete [flags]
Example:
dws calendar event delete --id <EVENT_ID> --yes
dws calendar event delete --id <EVENT_ID> --calendar-id <SHARED_CALENDAR_ID> --yes # 删除其他日历本下的日程
Flags:
--id string 日程 ID (必填)
--calendar-id string 日历 ID (可选,默认 primary 主日历;指定其他日历本时填写,可通过 `book list` 获取)
```
### 查看参会人
```
Usage:
dws calendar attendee list [flags]
Example:
dws calendar attendee list --event <EVENT_ID>
dws calendar attendee list --event <EVENT_ID> --calendar-id <SHARED_CALENDAR_ID> # 查看其他日历本下日程的参会人
Flags:
--event string 日程 ID (必填)
--calendar-id string 日历 ID (可选,默认 primary 主日历;指定其他日历本时填写,可通过 `book list` 获取, 注意:订阅日历下的日程无参会人,因此不可查看)
```
### 添加参会人
```
Usage:
dws calendar attendee add [flags]
Example:
dws calendar attendee add --event <EVENT_ID> --attendees <USER_ID_1>,<USER_ID_2>
dws calendar attendee add --event <EVENT_ID> --attendees <USER_ID> --optional
dws calendar attendee add --event <EVENT_ID> --attendees <USER_ID> --calendar-id <SHARED_CALENDAR_ID> # 给其他日历本下的日程添加参会人
Flags:
--event string 日程 ID (必填)
--attendees string 参会人 userId 列表,逗号分隔 (必填,最多500人)
--optional 参会人可选 (默认必选参会人)
--calendar-id string 日历 ID (可选,默认 primary 主日历;指定其他日历本时填写,可通过 `book list` 获取,注意:订阅日历下的日程无参会人,因此不可添加)
```
### 移除参会人
> **CAUTION:** 写操作 — 执行前须用户确认。
```
Usage:
dws calendar attendee delete [flags]
Example:
dws calendar attendee delete --event <EVENT_ID> --attendees <USER_ID> --yes
dws calendar attendee delete --event <EVENT_ID> --attendees <USER_ID> --calendar-id <SHARED_CALENDAR_ID> --yes # 移除其他日历本下日程的参会人
Flags:
--event string 日程 ID (必填)
--attendees string 参会人 userId 列表,逗号分隔 (必填)
--calendar-id string 日历 ID (可选,默认 primary 主日历;指定其他日历本时填写,可通过 `book list` 获取,注意:订阅日历下的日程无参会人,因此不可移除)
```
### 搜索会议室
> 此指令支持两种模式:**按名称搜索**(不传 --start/--end)和**按时间段搜索可用会议室**(传 --start/--end 或不传任何参数)。
> 此指令搜索到的会议室结果中,有两个值需要注意:
> - customApprovalProcess: true - 表示该会议室设置了自定义审批流程,只能通过客户端完成预订。
> - supportRecurring: true - 表示该会议室支持循环预定;false - 表示不支持循环预定,直接加入到循环日程会失败。
**模式路由规则**
- **按名称搜索**:仅传 `--room-name`,不传 `--start`/`--end` → 返回所有匹配名称的会议室,**不检查可用性**。适用于「找到某个会议室」的场景。
- **按时间段搜索可用会议室**:传 `--start`/`--end`,或不传任何参数 → 返回指定时间段内**可用**的会议室。不传时间时默认当前时间起 1 小时。
```
Usage:
dws calendar room search [flags]
Example:
# 按名称搜索(不检查可用性,返回所有匹配的会议室)
dws calendar room search --room-name 永澄亭 # 注意:用户即使说「永澄亭会议室」,也应仅传「永澄亭」
# 按时间段搜索可用会议室
dws calendar room search --start "2026-03-10T14:00:00+08:00" --end "2026-03-10T15:00:00+08:00"
dws calendar room search --start "2026-03-10T14:00:00+08:00" --end "2026-03-10T15:00:00+08:00" --group-id <GROUP_ID>
dws calendar room search # 不传 --start/--end 时默认当前时间起 1 小时
# 名称 + 时间段:搜索指定名称的可用会议室
dws calendar room search --room-name 永澄亭 --start "2026-03-10T14:00:00+08:00" --end "2026-03-10T15:00:00+08:00"
# 分页(仅按时间段搜索时有效)
dws calendar room search --start "2026-03-10T14:00:00+08:00" --end "2026-03-10T15:00:00+08:00" --limit 20 --page 0
Flags:
--start string 开始时间 ISO-8601 (可选,不传则默认当前时间+1分钟缓冲)
--end string 结束时间 ISO-8601 (可选,不传则默认当前时间+1 小时)
--group-id string 会议室分组ID(可选,留空查根目录;超100条时需按分组查询;仅按时间段搜索时有效)
--room-name string 会议室名称(按名称搜索时必填;按时间段搜索时可选,用于过滤)
--limit string 页大小 (可选,不填默认 100,超过 100 按 100 处理;仅按时间段搜索时有效)
--page string 分页起始位置 (可选,不填默认 0;仅按时间段搜索时有效)
```
> **时间约束(API 限制)**:`start` 必须是未来的时间(服务端校验:start can not less current time)。
> - 若传入的 `--start` 早于当前时间,CLI 会自动修正为 `now + 1min`,调用方无需额外处理。
> - 若传入的 `--end` 早于当前时间,CLI 直接报错——无法检索已过去的时间段。
> - **最佳实践**:调用方在组装时间参数时应确保 start/end 都是未来时间;若不确定,可省略 `--start`/`--end` 让 CLI 使用默认值(当前时间起 1 小时)。
> - **注意**:时间约束仅在「按时间段搜索」模式下生效。按名称搜索(仅传 `--room-name`)不受时间约束。
**名称过滤使用规范**`--room-name` 适用于用户说「预定永澄亭」「约西湖厅」这类按名找会议室的场景。
- **服务端是模糊匹配,但匹配词越精简命中率越高**,关键疗法:**调用方必须在调用 CLI 前自行精简名称,CLI 不会再做任何删减**。
- 常见需要剔除的用户口语后缀(仅示例,实际场景由模型自行判断):「会议室」「大会议室」「小会议室」「厅」「房」等。
- 示例对映:
- 用户:「帮我订永澄亭会议室」 → `--room-name 永澄亭`
- 用户:「西湖厅有空吗」 → `--room-name 西湖厅`(本身就是专名,不删即可)
- 用户:「预定贡嘎山大会议室」 → `--room-name 贡嘎山`
**优先路径**
- 当用户仅给出会议室名称、未指定时间段时,用 `room search --room-name <核心专名>` 快速找到会议室(不检查可用性)。
- 当用户给出会议室名称且指定了时间段时,用 `room search --room-name <核心专名> --start <开始时间> --end <结束时间>` 查询该名称的可用会议室。
- 若返回空列表,再降级使用 `list-groups` 定位分组再查。`--room-name` 可与 `--group-id` 同时使用(仅按时间段搜索时),表示「在指定分组内按名称过滤」。
**`roomId` 与用户说的话不是一回事**:用户说的「C6-4-06-N 贡嘎山」等是**展示名/编号文案**,**绝不能**直接填进 `room add --rooms``--rooms` 只接受上一步 `room search`(或同类接口)返回 JSON 里的 **`rooms[].roomId`**。形态上多为**小写十六进制串**(长度以接口为准,例如 `e6b7b65b8b30fb707afcf6c3b699f028003e6834fdd7fee7`)。含**中文、空格、连字符拼接的楼层编号**、或凭空调 UUID/纯数字「试格式」——一律视为非法,必须先搜房再取返回字段。
> 如果知道roomId,想查该会议室的预订记录,直接用dws calendar busy search 指令
---
### 预定会议室
```
Usage:
dws calendar room add [flags]
Example:
dws calendar room add --event <EVENT_ID> --rooms <ROOM_ID>
dws calendar room add --event <EVENT_ID> --rooms <ROOM_ID> --calendar-id <SHARED_CALENDAR_ID> # 给其他日历本下的日程预定会议室
Flags:
--event string 日程 ID (必填)
--rooms string 会议室 ID 列表 (必填)
--calendar-id string 日历 ID (可选,默认 primary 主日历;指定其他日历本时填写,可通过 `book list` 获取,注意:订阅日历下的日程不可添加会议室)
```
> room是会议室,用于线下开会场景。将room加入到日程完成预订
> 重复性日程,预订会议室时,必须设置 循环结束时间(recurrence-end-date),noEnd 或者 指定循环次数 都无法完成预定。
### 移除会议室
> **CAUTION:** 写操作 — 执行前须用户确认。
```
Usage:
dws calendar room delete [flags]
Example:
dws calendar room delete --event <EVENT_ID> --rooms <ROOM_ID> --yes
dws calendar room delete --event <EVENT_ID> --rooms <ROOM_ID> --calendar-id <SHARED_CALENDAR_ID> --yes # 移除其他日历本下日程的会议室
Flags:
--event string 日程 ID (必填)
--rooms string 会议室 ID 列表 (必填)
--calendar-id string 日历 ID (可选,默认 primary 主日历;指定其他日历本时填写,可通过 `book list` 获取。注意:订阅日历下的日程不可添加会议室)
```
### 会议室分组列表
```
Usage:
dws calendar room list-groups [flags]
Example:
dws calendar room list-groups
dws calendar room list-groups --limit 20 --page 0
Flags:
--limit string 页大小 (可选,不填默认 100,超过 100 按 100 处理)
--page string 分页起始位置 (可选,不填默认 0)
```
### 添加日程附件
```
Usage:
dws calendar attachment add [flags]
Example:
dws calendar attachment add --event <EVENT_ID> --files <FILE_ID>:report.pdf,<FILE_ID2>:slides.pptx
dws calendar attachment add --event <EVENT_ID> --files <FILE_ID>:report.pdf --calendar-id <SHARED_CALENDAR_ID> # 给其他日历本下的日程添加附件
Flags:
--event string 日程 ID (必填)
--files string 附件列表,格式 <fileId>:<name>,多项逗号分隔 (必填)
--calendar-id string 日历 ID (可选,默认 primary 主日历;指定其他日历本时填写,可通过 `book list` 获取,注意:订阅日历下的日程不可添加附件)
```
> 上传文件得到 `fileId` 需配合钉盘相关流程;本命令只负责把已上传的文件挂载到日程上。
### 查询我能看的所有日历本
```
Usage:
dws calendar book list [flags]
Example:
dws calendar book list
```
> 查询"我能看哪些日历本",包含:我的主日历、他人共享**给我**的日历、订阅的公共/团队日历。注意区分:`acl list` 是查"我的日历共享**给了谁**",方向相反。
> 共享日历本中有来自 xxx 的,且权限大于reader,那么通过 `event list --calendar-id <xxx的日历本id> `可查到xxx完整的日程安排。
> 主日历 `id` 固定为 `primary`,绝大多数日程操作都默认走主日历,只有当用户明确要求查/写其他日历本时才需要带 `--calendar-id`。
### 查询指定日历本
```
Usage:
dws calendar book get [flags]
Example:
dws calendar book get --id primary
dws calendar book get --id CALENDAR_ID
Flags:
--id string 日历 ID (必填,主日历固定为 primary)
```
> **说明**:根据日历 id 查询指定日历的详细信息。用户主日历本 id 固定为 `primary`。
### 搜索日历本
```
Usage:
dws calendar book search [flags]
Example:
dws calendar book search --query "项目"
dws calendar book search --query "团队周报"
Flags:
--query string 按日历本名称模糊检索 (必填)
```
> **说明**:搜索当前用户拥有的日历本,支持按日历本名模糊搜索。获取全部日历请使用 `book list`。
### 更新日历本
```
Usage:
dws calendar book update [flags]
Example:
dws calendar book update --id CALENDAR_ID --summary "新日历名"
dws calendar book update --id CALENDAR_ID --desc "日历描述"
Flags:
--id string 日历 ID (必填)
--summary string 日历标题
--desc string 日历描述
```
> **说明**:更新日历信息,最低权限要求:privilege == "owner"。注意:用户主日历本 以及 他人共享的日历本 **不支持更新**。
### 查询我的日历共享给了谁
```
Usage:
dws calendar acl list [flags]
Example:
dws calendar acl list
```
> **说明**:查询"我的日历共享给了哪些人、各自什么权限"(即主日历的访问控制列表)。注意区分:`book list` 是查"我能看哪些日历本",方向相反。
### 把我的日历共享给某人
```
Usage:
dws calendar acl add [flags]
Example:
dws calendar acl add --user USER_ID --privilege reader
dws calendar acl add --user USER_ID --privilege writer --no-notification
Flags:
--user string 授予权限的目标用户 ID (必填)
--privilege string 授予的日历权限 (必填): free_busy_reader(查看忙闲)|title_reader(查看标题)|reader(查看详情)|writer(创建和编辑)
--no-notification 不向被授权用户发送提醒 (默认发送)
```
> **说明**:把我的日历共享给指定用户,授予对方相应权限。`--privilege` 可选值:`free_busy_reader`(查看忙闲)、`title_reader`(查看标题)、`reader`(查看详情)、`writer`(创建和编辑)。
### 取消我的日历对某人的共享
```
Usage:
dws calendar acl delete [flags]
Example:
dws calendar acl delete --acl-id ACL_ID
Flags:
--acl-id string 已授予权限的 ID (必填,可通过 acl list 查询)
```
> **说明**:取消我的日历对某人的共享(撤回已授予的访问权限)。aclId 可通过 `acl list` 获取。
### 查询用户 / 会议室闲忙状态
```
Usage:
dws calendar busy search [flags]
Example:
# 查用户闲忙
dws calendar busy search --users <USER_ID_1>,<USER_ID_2> \
--start "2026-03-10T14:00:00+08:00" --end "2026-03-10T18:00:00+08:00"
# 查会议室闲忙
dws calendar busy search --rooms <ROOM_ID_1>,<ROOM_ID_2> \
--start "2026-03-10T14:00:00+08:00" --end "2026-03-10T18:00:00+08:00"
# 同时查用户 + 会议室
dws calendar busy search --users <USER_ID> --rooms <ROOM_ID> \
--start "2026-03-10T14:00:00+08:00" --end "2026-03-10T18:00:00+08:00"
Flags:
--end string 结束时间 ISO-8601 (必填)
--start string 开始时间 ISO-8601 (必填)
--users string 用户 ID 列表,逗号分隔 (与 --rooms 至少其一)
--rooms string 会议室 ID 列表,逗号分隔 (与 --users 至少其一)
```
> **说明**
> - `--users` 与 `--rooms` 必须至少指定其一,可以同时指定;CLI 会做前置校验,两者都为空会直接报错。
> - 查询会议室闲忙前,可先用 `dws calendar room search` 或 `dws calendar room list-groups` 拿到 roomId。
> - 返回结果中的忙碌时段仅包含粗粒度的时间信息,不包含日程内容细节(如标题、参会人、地点),以保护隐私。
### 建议日程时间
```
Usage:
dws calendar event suggest [flags]
Example:
dws calendar event suggest --users userId1,userId2 --duration 60
dws calendar event suggest --start "2026-03-10T09:00:00+08:00" --end "2026-03-10T18:00:00+08:00" --users userId1
dws calendar event suggest --users userId1 --duration 30 --timezone Asia/Tokyo
Flags:
--start string 推荐时间范围开始 ISO-8601 (默认当前时间)
--end string 推荐时间范围结束 ISO-8601 (默认次日18点)
--timezone string 时区 IANA 格式 (默认 Asia/Shanghai)
--users string 参会人 userId 列表,逗号分隔
--duration string 日程持续时间,单位分钟 (默认30)
> 对于非明确时间或一段时间范围的约会场景,可基于所有参会人的忙闲状态,推荐多个可用的时间块方案,用于解决会议时间协调问题。
```
### 响应日程
```
Usage:
dws calendar event respond [flags]
Example:
dws calendar event respond --id <EVENT_ID> --status accepted
dws calendar event respond --id <EVENT_ID> --status declined
dws calendar event respond --id <EVENT_ID> --status tentative
dws calendar event respond --id <EVENT_ID> --status accepted --calendar-id <SHARED_CALENDAR_ID> # 响应其他日历本下的日程
Flags:
--id string 日程 ID (必填)
--status string 响应状态: needsAction(未操作)|accepted(接受)|declined(拒绝)|tentative(暂定) (必填)
--calendar-id string 日历 ID (可选,默认 primary 主日历;指定其他日历本时填写,可通过 `book list` 获取。注意:订阅日历下的日程无参会人,因此不可响应)
```
> **说明**:作为日程参会人,设置自己的响应状态(接受、拒绝、暂定)。`--status` 可选值:`needsAction`(未操作,默认值)、`accepted`(接受)、`declined`(拒绝)、`tentative`(暂定)。
## 意图判断
用户说"日程/会议/约会/日历":
- 查看 → `event list`
- 详情 → `event get`
- 创建/约/给自己留时间块/个人日程 → `event create`(带参会人时加 `--attendees`,循环日程加 `--recurrence-*`,自定义提醒加 `--remind-minutes`
- 修改/改时间/改描述 → `event update`(支持修改标题、时间、描述、时区、循环规则)
- 取消/删除 → `event delete`
- 推荐时间/什么时候有空/协调时间 → `event suggest`
- 接受/拒绝/暂定日程 → `event respond`
- 查询循环日程/重复日程的每次实例/展开循环日程 → `event instances`
- 分享日程/把日程发给别人/获取日程分享信息或入会信息 → `event share-info`
用户说"参会人/与会者":
- 查看 → `attendee list`
- 邀请/添加 → `attendee add --attendees <USER_ID>`(可选参会人加 `--optional`
- 移除 → `attendee delete --attendees <USER_ID>`
用户说"会议室/订会议室":
- 哪个空闲 → `room search`(默认查当前时间起 1 小时内可用会议室)
- 按名找会议室(如「永澄亭」「永澄亭会议室」「约西湖厅」,未提时间段)→ 先在模型层精简名称(剔除「会议室」等通用后缀),再用 `room search --room-name <核心专名>`(按名称搜索,不检查可用性)
- 按名找可用会议室(如「永澄亭下午 2 点有空吗」)→ `room search --room-name <核心专名> --start <开始时间> --end <结束时间>`(按名称+时间段搜索可用会议室)
- 预订
- 给已有日程订会议室 → `room add --event <已有EVENT_ID> --rooms <ROOM_ID>`
- 创建新日程并订会议室 → `event create --rooms`(仅当日程尚不存在时)
- 取消预定 → `room delete`
- 分组 → `room list-groups`,取 groupId 后 `room search --group-id`(需配合 `--start`/`--end` 按时间段搜索;可再叠加 `--room-name` 在分组内过滤)
用户说"有空吗/忙不忙/闲忙":
- 查询用户闲忙 → `busy search --users <USER_ID>`
- 查询会议室闲忙 → `busy search --rooms <ROOM_ID>`
- 用户 + 会议室一起查 → `busy search --users <USER_ID> --rooms <ROOM_ID>`
用户说"日程附件/给会议加文件/上传日程材料":
- 添加 → `attachment add`(先用钉盘上传得 fileId,再 `attachment add --files <fileId>:<name>`
用户说"我有几个日历/查所有日历/别人共享给我的日历/他人共享给我的日历本":
- 列表 → `book list`(返回用户拥有和订阅的所有日历本,包括他人共享给自己的;主日历 id 固定为 `primary`
- 查指定日历本 → `book get --id <CALENDAR_ID>`
- 按名称搜索日历本 → `book search --query "关键词"`
- 修改日历本名称/描述 → `book update --id <CALENDAR_ID> --summary "新名"`
用户说"我的日历共享给了谁/谁能看我日历/日历权限/取消共享/把日历分享给xxx":
- 查看我共享出去的情况(即谁有权访问我的日历) → `acl list`
- 把我的日历共享给他人 → `acl add --user <USER_ID> --privilege reader`
- 取消我的日历对某人的共享 → `acl delete --acl-id <ACL_ID>`aclId 来自 `acl list`
> **易混淆辨析**`book list` 查的是"我能看哪些日历本"(包含别人共享**给我**的);`acl list` 查的是"我的日历共享**给了谁**"(我的主日历的访问控制列表)。两者方向相反,不可混用。
用户说"查下xxx的日程安排":
- 查询是否有共享关系 -> `book list`
- 场景1: 共享日历本中有来自 xxx 的,且权限大于reader,那么通过 `event list --calendar-id <xxx的日历本id> `可查到xxx完整的日程安排
- 场景2: 共享日历本中没有来自 xxx 的。那么通过 `busy search -- <USER_ID>`,查询xxx的忙闲安排
## 核心工作流
### 创建会议 + 邀请参会人 + 预订会议室
`event create` 支持 `--attendees` 在创建时直接指定参会人,**自 calendar MCP v2 起**也支持 `--rooms` 在创建时一并预定会议室;旧流程的「先创建日程再 `room add`」依然有效。
**关键区分**`event create --rooms` 仅在**日程尚不存在**时使用;若日程已存在(同一会话刚创建、或用户指向已有日程),必须走「给已有日程订会议室」流程(见下方),**禁止**重复 `event create`
**方式一:创建时一步完成(仅当日程尚不存在时推荐)**
```bash
# Step 1: 搜索空闲会议室,记下 roomId
dws calendar room search --start "2026-03-10T14:00:00+08:00" --end "2026-03-10T15:00:00+08:00" --format json
# 若返回错误(会议室超100条),先查分组再按分组搜索:
# dws calendar room list-groups --format json
# dws calendar room search --start ... --end ... --group-id <GROUP_ID> --format json
# Step 2: 创建日程时直接指定参会人 + 会议室
dws calendar event create --title "Q1 复盘会" \
--start "2026-03-10T14:00:00+08:00" --end "2026-03-10T15:00:00+08:00" \
--attendees userId1,userId2 \
--rooms <ROOM_ID_FROM_STEP1> --format json
```
**方式二:先创建日程,再单独添加参会人 / 会议室**
```bash
# Step 1: 创建日程 — 从 result.id 提取日程 ID
dws calendar event create --title "Q1 复盘会" \
--start "2026-03-10T14:00:00+08:00" --end "2026-03-10T15:00:00+08:00" --format json
# Step 2: 添加参会人(必须用 Step 1 返回的 result.id
dws calendar attendee add --event <EVENT_ID> --attendees userId1,userId2 --format json
# Step 3: 搜索空闲会议室
dws calendar room search --start ... --end ... --format json
# Step 4: 预定会议室
dws calendar room add --event <EVENT_ID> --rooms <ROOM_ID> --format json
```
### 给已存在的日程加附件
```bash
# Step 1: 用钉盘上传文件,得到 fileId(参见 dws drive 系列命令)
# Step 2: 把附件挂到指定日程
dws calendar attachment add --event <EVENT_ID> --files <FILE_ID>:report.pdf,<FILE_ID2>:slides.pptx --format json
```
### 查看日程列表
```bash
dws calendar event list --start "2026-03-10T14:00:00+08:00" --end "2026-03-10T15:00:00+08:00" --format json
```
## 上下文传递表
| 操作 | 从返回中提取 | 用于 |
|------|-------------|------|
| `event create` | `result.id` | attendee/room/attachment 操作的 --event |
| `event list` | `result.events[].id`, `nextCursor` | event get/update/delete/respond 的 --id;下一页 --cursor |
| `event suggest` | 推荐的时间段 | event create 的 --start/--end |
| `event respond` | 响应结果 | — |
| `event instances` | `result.events[].id`, `nextCursor` | event get/update/delete/respond 的 --id;下一页 --cursor |
| `event share-info` | 日程分享信息(主题、组织人、地点、入会信息等) | 分享给他人(如 chat/mail 发送) |
| `room search` | `rooms[].roomId` | room add 的 --rooms 或 event create 的 --rooms |
| `room list-groups` | `groups[].groupId` | room search 的 --group-id |
| `book list` | `id`(如 `primary` | event list/get 的 --calendar-id, book get/update 的 --id |
| `book get` | 日历详细信息 | — |
| `book search` | 匹配的日历列表 | book get/update 的 --id |
| `acl list` | `aclId` | acl delete 的 --acl-id |
| `acl add` | 新增的权限记录 | — |
| 钉盘上传 | 文件 `fileId` | attachment add 的 --files `<fileId>:<name>` |
## 注意事项
- 时间格式: `event create/update``event list``busy search``event suggest` 用 ISO-8601
- 时区: `event create/update``event suggest` 支持 `--timezone` 指定 IANA 时区(如 `Asia/Shanghai``America/New_York`),不传默认 `Asia/Shanghai`
- 创建日程时可通过 `--attendees` 直接指定参会人(最多500人),也可创建后用 `attendee add --attendees ...` 单独添加
- `--attendees``--open-dingtalk-ids` 至少传一个(如果需要指定参会人)
- 添加参会人时可通过 `--optional` 设为可选参会人(默认必选)
- `event suggest` 根据参会人闲忙自动推荐合适时间,适合会议时间未确定时使用
- 创建日程**支持**通过 `--rooms` 一步预定会议室(`event create --rooms roomId1,roomId2`);若创建后再加,仍可用 `room add`
- `room search` 不带 `--group-id` 时查根目录;企业会议室超过 100 条会报错,此时需先 `room list-groups` 获取分组,再按分组逐一查询
- `room list-groups` 支持 `--limit` / `--page` 分页(schema 类型为字符串)
- **`event create --rooms` / `room add --rooms` 的唯一合法来源**:最近一次(同一会话、同一时段窗口)`room search` 返回体中的 `roomId`;禁止把用户自然语言会议室名当 `roomId` 传入(否则会 `roomId invalid` 等错误)
- **搜房无结果**:在符合早停/用户限定范围内,`room search`(含按分组逐组查)全部返回空或无空闲 → 应**直接向用户报错/说明失败**并结束订房;**禁止**假设 roomId、禁止无合法 `roomId` 时调用 `room add` / `event create --rooms` 试探、禁止用 `event get` 等绕路推断 roomId
- **自动化校验**:凡涉及 `room add` / `event create --rooms` 的流程,`--rooms` 只能填上游 `room search`(或等价接口)返回 JSON 中的 **`rooms[].roomId`**;不得以会议室展示名、楼层文案或用户口语当作 `roomId`
- **附件**`attachment add` 仅负责挂载,**不上传**文件;fileId 必须先通过钉盘流程取得;`--files` 多附件用 `<fileId>:<name>` 元素逗号分隔
- **日历本**`book list` 返回的 `id` 才是合法 `calendarId`;如无明确说明,`event list` / `event get` 都不要带 `--calendar-id`,让接口默认走 primary 主日历
- **分页查询**`event list` / `event instances` 均支持 `--limit`(控制每页条数,默认/最大 100)和 `--cursor`(翻页游标);**首次查询无需传 `--cursor`**,仅当返回体中包含 `nextCursor` 时,将其作为 `--cursor` 传入可获取下一页
- **循环日程实例**`event instances` 用于按时间范围展开重复日程(SeriesMaster)的每一个实例;**普通非循环日程调用该命令将查不到任何实例信息**
- **日程分享**`event share-info` 获取日程的分享信息(主题、组织人、地点、入会信息等);`--language` 控制文案语言(中文场景传 zh-CN)
- **日程提醒**`event create` 支持 `--remind-minutes` 设置开始前提醒,逗号分隔多个分钟数(如 `--remind-minutes 5,10,15`),不传则默认15分钟提醒
- **会议室分页**`room search` 支持 `--limit`(每页条数,默认100,最大100)和 `--page`(分页起始位置,默认0),与 `room list-groups` 分页风格一致
## 自动化脚本
| 脚本 | 场景 | 用法 |
|------|------|------|
| [calendar_today_agenda.py](../scripts/calendar_today_agenda.py) | 查看今天/明天/本周日程安排 | `python calendar_today_agenda.py today` |
| [calendar_schedule_meeting.py](../scripts/calendar_schedule_meeting.py) | 一键创建日程+添加参会人+预定会议室;搜房失败时输出明确原因并返回非零退出码 | `python calendar_schedule_meeting.py --title "复盘会" --start "2026-03-15T14:00" --end "2026-03-15T15:00" --users userId1 --book-room` |
| [calendar_free_slot_finder.py](../scripts/calendar_free_slot_finder.py) | 查询多人共同空闲时段 | `python calendar_free_slot_finder.py --users userId1,userId2 --date 2026-03-15` |
## 相关产品
- conference(视频会议预约) — 仅视频会议预约(返回入会链接),不含参会人/会议室管理
- [contact](../../dingtalk-contact/references/contact.md) — 搜索同事 userId,用于 attendee add --attendees
@@ -0,0 +1,9 @@
# calendar 局部意图消歧
本文件从单 Skill `intent-guide.md` 拆分而来,仅保留与本产品相关的跨产品消歧规则。
| 用户说... | 真实意图 | 应该用 | 不要用 | 理由 |
|---|---|---|---|---|
| "给自己留一个明天下午的时间块/建个个人日程" | 创建个人日程 | `calendar event create` | `todo` | 个人 schedule 仍属于日历事件,不是待办 |
| "帮我建一个明天下午的日程" | 日历日程 | `calendar` | — | 日历日程管理(可含参与者/会议室);视频会议(conference)当前 CLI 不支持 |
| "明早 9 点提醒我提交周报" | 创建个人待办,但需先声明 reminder 边界 | `todo` | `calendar` | todo 当前只支持 dueTime 截止时间,不支持独立精确 reminder |
@@ -0,0 +1,24 @@
# calendar Lite Recipe
本文件从单 Skill `lite-recipes.md` 拆分而来,仅保留与本产品相关的轻量流程。
## #3 会议日程
### list-today-meetings
**优先**`python scripts/calendar_today_agenda.py [today|tomorrow|week]`
备选:`dws calendar event list --start "<今日起始ISO>" --end "<今日结束ISO>"`(须加 `--format json`
### check-users-busy
查询多人在某时段内的闲忙(**busy**,不是用 `event list` 扫日程):
1. 解析用户:对每个姓名执行 `aisearch person --query "<姓名>" --dimension name``userId`;多人将 `userId` 用英文逗号拼接(无空格或按 [calendar.md](./calendar.md) `busy search` 要求)。
2. 确认时段:用户须给出或可收敛为明确的 `--start` / `--end`(ISO-8601);若未给出,**先追问**起止时间,禁止用任意默认全天窗口代替用户意图。
3. 执行:`dws calendar busy search --users <userId1,userId2,...> --start "<ISO>" --end "<ISO>" --format json`
详见 [calendar.md](./calendar.md) 中「查询用户闲忙状态」。
### start-conference
> 当前 CLI 不提供视频会议(conference)发起/入会/会中控制能力。触发「发起会议」「开个会」「创建会议」且**没有给出具体时间**时,不要构造 `conference` 命令;直接告知用户请在钉钉客户端操作。
@@ -0,0 +1,199 @@
#!/usr/bin/env python3
"""
查询多人共同空闲时段,推荐最佳会议时间
用法:
python calendar_free_slot_finder.py \
--users userId1,userId2,userId3 \
--date 2026-03-15 \
--duration 60
python calendar_free_slot_finder.py \
--users userId1,userId2 \
--date 2026-03-15 \
--start-hour 9 --end-hour 18 \
--duration 30 --dry-run
"""
import sys
import json
import subprocess
import argparse
from datetime import datetime, timedelta, timezone
from typing import List, Dict, Any, Optional, Tuple
TZ = timezone(timedelta(hours=8))
SLOT_STEP_MIN = 30
def run_dws(
args: List[str], dry_run: bool = False,
) -> Optional[Any]:
cmd = ['dws'] + args
if dry_run:
print(f"[dry-run] {' '.join(cmd)}")
return None
try:
result = subprocess.run(
cmd, capture_output=True, text=True, timeout=60
)
if result.returncode != 0:
print(f"错误:{result.stderr.strip()}", file=sys.stderr)
return None
return json.loads(result.stdout)
except (subprocess.TimeoutExpired, json.JSONDecodeError,
FileNotFoundError) as e:
print(f"错误:{e}", file=sys.stderr)
return None
def fmt_iso(dt: datetime) -> str:
return dt.strftime('%Y-%m-%dT%H:%M:%S+08:00')
def parse_busy_intervals(
data: Any,
) -> List[Tuple[datetime, datetime]]:
intervals = []
if not data:
return intervals
items = []
if isinstance(data, list):
items = data
elif isinstance(data, dict):
inner = data.get('result', data)
if isinstance(inner, list):
items = inner
elif isinstance(inner, dict):
for user_data in inner.values():
if isinstance(user_data, list):
items.extend(user_data)
elif isinstance(user_data, dict):
items.extend(
user_data.get('busyTimes', [])
)
for item in items:
start_str = item.get('startTime') or item.get('start', '')
end_str = item.get('endTime') or item.get('end', '')
if not start_str or not end_str:
continue
for fmt in (
'%Y-%m-%dT%H:%M:%S%z', '%Y-%m-%dT%H:%M:%S',
'%Y-%m-%dT%H:%M%z',
):
try:
s = datetime.strptime(start_str, fmt)
e = datetime.strptime(end_str, fmt)
if s.tzinfo is None:
s = s.replace(tzinfo=TZ)
if e.tzinfo is None:
e = e.replace(tzinfo=TZ)
intervals.append((s, e))
break
except ValueError:
continue
return intervals
def find_free_slots(
day_start: datetime, day_end: datetime,
busy: List[Tuple[datetime, datetime]],
duration_min: int,
) -> List[Tuple[datetime, datetime]]:
busy_sorted = sorted(busy, key=lambda x: x[0])
merged: List[Tuple[datetime, datetime]] = []
for s, e in busy_sorted:
if merged and s <= merged[-1][1]:
merged[-1] = (merged[-1][0], max(merged[-1][1], e))
else:
merged.append((s, e))
free: List[Tuple[datetime, datetime]] = []
cursor = day_start
for bs, be in merged:
if cursor < bs:
gap = (bs - cursor).total_seconds() / 60
if gap >= duration_min:
free.append((cursor, bs))
cursor = max(cursor, be)
if cursor < day_end:
gap = (day_end - cursor).total_seconds() / 60
if gap >= duration_min:
free.append((cursor, day_end))
return free
def main():
parser = argparse.ArgumentParser(
description='查询多人共同空闲时段'
)
parser.add_argument(
'--users', required=True, help='用户 ID 列表,逗号分隔'
)
parser.add_argument(
'--date', required=True, help='查询日期 YYYY-MM-DD'
)
parser.add_argument(
'--duration', type=int, default=60,
help='会议时长(分钟),默认 60',
)
parser.add_argument(
'--start-hour', type=int, default=9,
help='工作日开始小时,默认 9',
)
parser.add_argument(
'--end-hour', type=int, default=18,
help='工作日结束小时,默认 18',
)
parser.add_argument(
'--dry-run', action='store_true', help='仅显示命令'
)
args = parser.parse_args()
try:
date = datetime.strptime(args.date, '%Y-%m-%d')
except ValueError:
print('错误:日期格式应为 YYYY-MM-DD')
sys.exit(1)
day_start = date.replace(
hour=args.start_hour, tzinfo=TZ
)
day_end = date.replace(hour=args.end_hour, tzinfo=TZ)
data = run_dws([
'calendar', 'busy', 'search',
'--users', args.users,
'--start', fmt_iso(day_start),
'--end', fmt_iso(day_end),
'--format', 'json',
], dry_run=args.dry_run)
if args.dry_run:
return
busy = parse_busy_intervals(data)
free = find_free_slots(day_start, day_end, busy, args.duration)
users_list = args.users.split(',')
print(f"\n🕐 空闲时段查询 ({args.date})")
print(f" 参与人: {len(users_list)}")
print(f" 会议时长: {args.duration} 分钟")
print(f" 工作时间: {args.start_hour}:00 ~ "
f"{args.end_hour}:00")
print('=' * 50)
if not free:
print(' ❌ 该日无共同空闲时段')
return
print(f"\n✅ 找到 {len(free)} 个可用时段:\n")
for i, (s, e) in enumerate(free, 1):
gap_min = int((e - s).total_seconds() / 60)
label = '⭐ 推荐' if i == 1 else f' 备选{i-1}'
print(f" {label} {s.strftime('%H:%M')} ~ "
f"{e.strftime('%H:%M')} ({gap_min}分钟)")
if __name__ == '__main__':
main()
@@ -0,0 +1,233 @@
#!/usr/bin/env python3
"""
一键创建日程(可选:带参与者 + 预定空闲会议室)
流程:
1. 若需预定会议室 (--book-room),先搜索空闲会议室;无可用则提前报错退出
2. 使用 event create 一次性完成日程创建 + 添加参与者 + 预定会议室
用法:
python calendar_schedule_meeting.py \
--title "Q1 复盘会" \
--start "2026-03-15T14:00" \
--end "2026-03-15T15:00" \
--users userId1,userId2 \
--book-room
python calendar_schedule_meeting.py --dry-run \
--title "测试" --start "2026-03-15T14:00" --end "2026-03-15T15:00"
"""
import sys
import json
import subprocess
import argparse
from datetime import datetime, timedelta, timezone
from typing import List, Any, Optional, Tuple
TZ = timezone(timedelta(hours=8))
def run_dws(
args: List[str], dry_run: bool = False,
) -> Optional[Any]:
cmd = ['dws'] + args
if dry_run:
print(f"[dry-run] {' '.join(cmd)}")
return {'dry_run': True}
try:
result = subprocess.run(
cmd, capture_output=True, text=True, timeout=60
)
if result.returncode != 0:
print(f" ✗ 错误:{result.stderr.strip()}")
return None
return json.loads(result.stdout)
except (subprocess.TimeoutExpired, json.JSONDecodeError,
FileNotFoundError) as e:
print(f" ✗ 错误:{e}")
return None
def normalize_time(time_str: str) -> str:
for fmt in ('%Y-%m-%dT%H:%M', '%Y-%m-%d %H:%M',
'%Y-%m-%dT%H:%M:%S'):
try:
dt = datetime.strptime(time_str, fmt)
dt = dt.replace(tzinfo=TZ)
return dt.strftime('%Y-%m-%dT%H:%M:%S+08:00')
except ValueError:
continue
if '+' in time_str or time_str.endswith('Z'):
return time_str
raise ValueError(f"无法解析时间:{time_str}")
def parse_group_ids(raw: str) -> List[str]:
if raw is None:
return []
return [part.strip() for part in raw.split(',') if part.strip()]
def extract_room_candidates(payload: Any) -> Tuple[List[dict], str]:
candidates: Any = []
if isinstance(payload, list):
candidates = payload
elif isinstance(payload, dict):
if isinstance(payload.get('rooms'), list):
candidates = payload.get('rooms', [])
elif isinstance(payload.get('result'), dict):
nested = payload.get('result', {})
if isinstance(nested.get('rooms'), list):
candidates = nested.get('rooms', [])
elif isinstance(nested.get('result'), list):
candidates = nested.get('result', [])
elif isinstance(payload.get('result'), list):
candidates = payload.get('result', [])
if not isinstance(candidates, list):
return [], '返回结构中未找到会议室列表'
valid_rooms: List[dict] = []
placeholder_count = 0
for item in candidates:
if not isinstance(item, dict):
continue
if item.get('roomId') or item.get('id'):
valid_rooms.append(item)
continue
if item.get('labels') is None and len(item) == 1:
placeholder_count += 1
if valid_rooms:
return valid_rooms, f'返回 {len(valid_rooms)} 个有效会议室'
if placeholder_count:
return [], '仅返回占位结果(如 labels:null),无有效 roomId'
if candidates:
return [], '返回了对象列表,但均不含有效 roomId'
return [], '未返回任何会议室'
def main():
parser = argparse.ArgumentParser(
description='一键创建日程(可选:带参与者 + 预定会议室)'
)
parser.add_argument('--title', required=True, help='日程标题')
parser.add_argument('--start', required=True, help='开始时间')
parser.add_argument('--end', required=True, help='结束时间')
parser.add_argument('--desc', default='', help='日程描述')
parser.add_argument('--users', default='', help='参与者 userId,逗号分隔')
parser.add_argument(
'--book-room', action='store_true', help='自动搜索并预定空闲会议室'
)
parser.add_argument(
'--room-group-id', default='',
help='允许搜索的 groupId;同一地点请只传最相关 group,多个仅用于用户明确允许的多个地点'
)
parser.add_argument(
'--dry-run', action='store_true', help='仅显示命令'
)
args = parser.parse_args()
try:
start_iso = normalize_time(args.start)
end_iso = normalize_time(args.end)
except ValueError as e:
print(f"错误:{e}")
sys.exit(1)
# ── Step 1: 若需预定会议室,先搜索空闲会议室 ──────────────────
room_id: Optional[str] = None
room_name: Optional[str] = None
if args.book_room:
print('🏢 搜索空闲会议室...')
group_ids = parse_group_ids(args.room_group_id)
search_scopes = group_ids or [None]
selected_room = None
failure_reasons: List[str] = []
for group_id in search_scopes:
scope_label = f'group {group_id}' if group_id else '根目录'
print(f' - 查询范围: {scope_label}')
search_args = [
'calendar', 'room', 'search',
'--start', start_iso,
'--end', end_iso,
'--available',
'--format', 'json',
]
if group_id:
search_args.extend(['--group-id', group_id])
rooms_data = run_dws(search_args, dry_run=args.dry_run)
if args.dry_run:
continue
if not rooms_data:
failure_reasons.append(f'{scope_label}: room search 执行失败')
continue
rooms, detail = extract_room_candidates(rooms_data)
if rooms:
selected_room = rooms[0]
break
failure_reasons.append(f'{scope_label}: {detail}')
if not args.dry_run:
if selected_room:
room_id = selected_room.get('roomId') or selected_room.get('id')
room_name = selected_room.get('roomName') or selected_room.get('name')
print(f' ✓ 找到空闲会议室: {room_name} ({room_id})')
else:
print(f'{start_iso} ~ {end_iso} 时段内无可用会议室')
for reason in failure_reasons:
print(f' - {reason}')
print(' 请向用户汇报失败,或询问是否放宽范围/改时间。')
sys.exit(2)
# ── Step 2: 一次性创建日程(含参与者 + 会议室) ────────────────
print('\n📅 创建日程...')
create_args = [
'calendar', 'event', 'create',
'--title', args.title,
'--start', start_iso,
'--end', end_iso,
'--format', 'json',
]
if args.desc:
create_args.extend(['--desc', args.desc])
if args.users:
create_args.extend(['--attendees', args.users])
if room_id:
create_args.extend(['--rooms', str(room_id)])
result = run_dws(create_args, dry_run=args.dry_run)
if not result:
sys.exit(1)
# 解析响应
event_id = None
if not args.dry_run and isinstance(result, dict):
# MCP 响应通常嵌套在 result 字段内: {"result": {"id": "..."}}
inner = result.get('result', result)
if isinstance(inner, dict):
event_id = inner.get('eventId') or inner.get('id')
else:
event_id = result.get('eventId') or result.get('id')
# 输出结果摘要
parts = []
if event_id:
parts.append(f'eventId: {event_id}')
if args.users:
parts.append(f'参与者: {args.users}')
if room_name:
parts.append(f'会议室: {room_name}')
detail_str = f" ({', '.join(parts)})" if parts else ''
print(f' ✓ 日程已创建{detail_str}')
print('\n✅ 完成!')
if __name__ == '__main__':
main()
@@ -0,0 +1,139 @@
#!/usr/bin/env python3
"""
查看今天/明天/本周的日程安排
用法:
python calendar_today_agenda.py # 今天
python calendar_today_agenda.py today # 今天
python calendar_today_agenda.py tomorrow # 明天
python calendar_today_agenda.py week # 本周
python calendar_today_agenda.py --dry-run # 仅显示命令
"""
import sys
import json
import subprocess
from datetime import datetime, timedelta, timezone
from typing import List, Dict, Any, Optional
TZ = timezone(timedelta(hours=8))
def run_dws(
args: List[str], dry_run: bool = False,
) -> Optional[Any]:
cmd = ['dws'] + args
if dry_run:
print(f"[dry-run] {' '.join(cmd)}")
return None
try:
result = subprocess.run(
cmd, capture_output=True, text=True, timeout=60
)
if result.returncode != 0:
print(f"错误:{result.stderr.strip()}", file=sys.stderr)
return None
return json.loads(result.stdout)
except (subprocess.TimeoutExpired, json.JSONDecodeError,
FileNotFoundError) as e:
print(f"错误:{e}", file=sys.stderr)
return None
def get_range(scope: str):
now = datetime.now(TZ)
today = now.replace(hour=0, minute=0, second=0, microsecond=0)
if scope == 'today':
return today, today + timedelta(days=1)
elif scope == 'tomorrow':
t = today + timedelta(days=1)
return t, t + timedelta(days=1)
elif scope == 'week':
ws = today - timedelta(days=today.weekday())
return ws, ws + timedelta(days=7)
return today, today + timedelta(days=1)
def fmt_iso(dt: datetime) -> str:
return dt.strftime('%Y-%m-%dT%H:%M:%S+08:00')
def fmt_time(iso_str: str) -> str:
if not iso_str:
return '??:??'
try:
for fmt in ('%Y-%m-%dT%H:%M:%S%z', '%Y-%m-%dT%H:%M:%S'):
try:
dt = datetime.strptime(iso_str, fmt)
return dt.strftime('%H:%M')
except ValueError:
continue
return iso_str[:16]
except Exception:
return iso_str[:16]
def main():
dry_run = '--dry-run' in sys.argv
args = [a for a in sys.argv[1:] if a != '--dry-run']
scope = args[0] if args else 'today'
if scope not in ('today', 'tomorrow', 'week'):
print(__doc__)
sys.exit(1)
start, end = get_range(scope)
data = run_dws([
'calendar', 'event', 'list',
'--start', fmt_iso(start),
'--end', fmt_iso(end),
'--format', 'json',
], dry_run=dry_run)
if dry_run:
return
events = []
if isinstance(data, list):
events = data
elif isinstance(data, dict):
inner = data.get('result', data)
if isinstance(inner, dict):
events = inner.get('events', [])
elif isinstance(inner, list):
events = inner
label = {'today': '今天', 'tomorrow': '明天', 'week': '本周'
}.get(scope, scope)
print(f"\n📅 {label}日程 ({start.strftime('%m-%d')} ~ "
f"{end.strftime('%m-%d')})")
print('=' * 50)
if not events:
print(' ✅ 暂无日程,自由安排!')
return
for e in events:
if not isinstance(e, dict):
print(f" 🕐 {e}")
continue
title = e.get('summary') or e.get('title', '无标题')
s = e.get('start', {})
ed = e.get('end', {})
start_t = fmt_time(
s.get('dateTime', '') if isinstance(s, dict) else str(s)
)
end_t = fmt_time(
ed.get('dateTime', '') if isinstance(ed, dict) else str(ed)
)
loc = e.get('location', {})
loc_str = (loc.get('displayName', '')
if isinstance(loc, dict) else str(loc or ''))
line = f" 🕐 {start_t}-{end_t} {title}"
if loc_str:
line += f" 📍{loc_str}"
print(line)
print(f"\n合计: {len(events)} 场日程")
if __name__ == '__main__':
main()
+126
View File
@@ -0,0 +1,126 @@
---
name: dingtalk-chat
description: 钉钉群聊与消息。Use when 用户提到 发消息/编辑或撤回消息/单聊/群聊/建群/普通群升级外部群/群昵称/会话分组/群成员管理/@消息/搜索聊天记录/话题回复/收藏消息/机器人群发/Webhook通知/发送或下载消息图片与文件。不做紧急 DING/短信/电话(走 dingtalk-misc)、邮件(走 dingtalk-mail)、班级群(走 dingtalk-misc)。命令前缀:dws chat。
metadata:
cli_version: ">=0.2.14"
category: product
requires:
bins:
- dws
---
# 钉钉群聊 / 消息 Skill
<!-- DWS_RUNTIME_CONTRACT_START -->
## 最小 DWS 执行契约
- 只通过 `dws` CLI 操作钉钉;结构化读取使用 `--format json`,按真实返回判断结果。
- 已知命令直接执行。只有 leaf 参数或安全语义不确定时读取精确 Schema,只有 Cobra flag 不确定时读取精确 leaf Help;不要加载产品级 Catalog 代替选路。
- 不猜命令、flag、字段、ID、账号或时间。后续 ID 必须来自真实返回;零命中、多候选或类型不明时停止并消歧。
- 解析目标、读取上下文和最终执行必须使用同一 profile;不得跨组织复用 userId、openDingTalkId 或 openConversationId。多账号组织只使用明确的 `isOrgCurrent=true` 默认账号;没有默认账号时要求用户指定,禁止选择第一项、最近登录或最近使用账号。
- 不输出或记录 token、refresh token、appSecret、webhook token 等凭据;宿主已注入认证时不要索要凭据。
- 写操作必须符合用户明确意图。是否需要确认以最终 Runtime gate 和 Schema 为准;需要确认时先说明对象、动作与影响,再追加 `--yes`
- 写后按任务结果契约验证;不能仅凭退出码宣称成功。部分结果、未知投递状态和失败项必须如实保留。
- 时间戳面向用户展示时转换为带时区的可读时间;默认使用当前会话时区,必要时同时保留原值。
- 遇到认证、权限、profile、confirmation 或未知错误时,只加载 `dingtalk-shared` 中对应 reference;不要连续猜测替代命令。
<!-- DWS_RUNTIME_CONTRACT_END -->
<!-- VISIBLE_SHORTCUTS_START -->
## Shortcut 发现(按需)
`chat` 当前有 98 条公开 shortcut,完整清单保留在 Runtime Catalog 与 Schema,不在高频产品根 Skill 中重复展开。已知意图直接使用下方的优先路由、意图表或任务 reference;命令已选中时直接执行,只在参数/安全语义不确定时读取 leaf Schema,在当前 Cobra flags 不确定时读取 leaf Help。
仅当现有路由和 reference 都无法定位低频能力时,才执行 `dws shortcut list --service chat --format json` 做最后回退;不要为已知高频意图加载完整 Shortcut Catalog 或产品级 Schema。
<!-- VISIBLE_SHORTCUTS_END -->
## Golden Route
按用户任务选择最小充分入口。公开层按意图分流;Resolver、发送执行、消息投影和错误契约在 Runtime 内复用,不把所有能力塞进一个万能命令。
| 用户意图 | 唯一推荐入口 | 关键边界 |
|---|---|---|
| <!-- dws-intent: chat.send.dm -->按姓名发简单文本或 Markdown | `dws chat +dm --to <姓名> --content <内容>` | CLI 解析唯一用户;多候选时停止,不先手工查 ID |
| <!-- dws-intent: chat.send.group -->按群名或 ID 发简单文本或 Markdown | `dws chat +send-to-group --group <群名或ID> --content <内容>` | 稳定 ID 直接使用;群名多候选时停止 |
| <!-- dws-intent: chat.send.advanced -->文件、Bot、Webhook、复杂 @ 或高级发送 | `dws chat +messages-send` | Bot 多群用 `--groups/--groups-file` 并检查逐项 ledger |
| <!-- dws-intent: chat.read.conversation -->读取指定会话、返回较多消息 | `dws chat +chat-messages` | 粗粒度读取;目标条件明确时优先 `+search-msg` |
| <!-- dws-intent: chat.search.filtered -->多维度条件搜索(发送者/关键词/@/类型,单/跨会话) | `dws chat +search-msg` | 目标条件明确时使用 |
| 查看指定群成员(用户/机器人) | `dws chat +chat-members-list --group <群名或ID>` | 唯一解析并全量读取 |
| 获取群邀请链接 | `dws chat +chat-invite-url --group <群名或ID>` | 多候选时停止 |
| 查看群机器人 | `dws chat +chat-bots --group <群名或ID>` | 返回稳定 `bots[]` |
| 个人收藏表情列表/发送/收藏 | `dws chat emotion list/send/favorite` | 约束见 leaf Schema |
| 修改群名称 | `dws chat group rename --id <openConversationId> --name <新名称>` | 只知群名时先用 `+chat-search --query <群名>` 唯一解析 ID;不猜 `+chat-rename` |
| 查看指定群内 @我的消息 | `dws chat +at-me --group <群名> --page-all` | 检查 `complete`;空结果仍返回数组 |
| 查看全部会话 | `dws chat +conversation-list --page-all` | 检查 `complete` / `failures` |
| 读取并下载消息资源 | 查询命令加 `--download-resources` | 不另起手工下载循环;下载失败项保留在结果中 |
| <!-- dws-intent: chat.conversation.list-top -->查看置顶会话 | `dws chat +conversation-list-top` | 会话 Top 与消息 Pin、消息 Top、Favorite 不同 |
| 监听未来 IM 事件 | [`dingtalk-event`](../dingtalk-event/SKILL.md) | 常规监听走 `+listen-im`;生命周期/高级控制走 `consume` |
以下次级入口在意图明确时直接使用,不需要先加载完整 Catalog:
| 用户意图 | 入口 |
|---|---|
| 已知消息 ID 批量读取详情 | `dws chat +messages-mget` |
| 已知资源引用单独下载 | `dws chat +messages-resource-download` |
| 按关键词搜索群 | `dws chat +chat-search` |
| 查看消息收藏 | `dws chat +flag-list` |
| <!-- dws-intent: chat.reply.quote -->引用回复 | 人:`dws chat +messages-reply`;成功结果保留新消息/会话/投递与原消息来源上下文。Bot 群:`dws chat message send-by-bot --conversation-id <cid> --reply <mid> --ref-sender <sid>` |
| 撤回当前用户消息 | `dws chat +messages-recall --msg-id <openMessageId>`;可省略会话 ID,由 CLI 只读补齐;兼容单值 `--message-ids` |
| 已知话题主消息 ID 或 thread/topic ID 读取回复 | `dws chat +thread-replies` |
| <!-- dws-intent: chat.create.group -->按成员 ID 或姓名创建群聊 | `dws chat +chat-create`;成员/群主均可自然解析,任一歧义都会在创建前整体停止 |
| 跨全部会话查看 @我的消息 | `dws chat +at-me --page-all` |
### 发送入口边界
- `+dm`:姓名目标的简单文本/Markdown,参数空间最小。
- `+send-to-group`:群名或稳定 ID 目标的简单文本/Markdown,避免暴露无关身份矩阵。
- Markdown 中的公网图片必须写成 `![图片标题](https://example.com/image.png)` 才会内联展示;
省略开头的 `!` 时只会显示为链接。
- `+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 <token>`;不要退回原子 Webhook 命令。
- 流式卡片用 `+messages-send-card`;群聊@传 ID/`--at-all`Runtime 把 create 返回前缀加到 `--content`;禁写占位符;仅 text。
## 关键结果语义
- `openTaskId` 是发送任务 ID,不是回复或撤回所需的消息 ID;消息 ID 必须来自真实查询结果。
- 消息查询默认保留稳定 ID、会话/thread、发送者、文本、时间、reaction、引用、转发和 `resourceRefs``--no-reactions` 可关闭 reaction。
- 查询结果必须检查 `complete``hasMore``failures` 和资源下载 ledgerpartial result 不得表述为完整成功。
- 子消息使用自己的 `messageId`;仅缺会话 ID 时继承父消息的 `conversationId`
- 下载只允许工作目录内安全相对路径,默认不覆盖并原子落盘;覆盖必须由用户显式传 `--overwrite`。读取和下载不需要 `--yes`
- Favorite、消息 Pin、消息 Top、会话 Top 是不同对象层级,不能互换。
## 按需加载
只在任务命中时读取一个精确 reference:
[话题与话题圈](references/chat/thread.md)
| 场景 | Reference |
|---|---|
| 需要跨步骤传递真实结果的消息/群组合流程 | [01-messaging.md](references/01-messaging.md) |
| 消息读取与查询 | [message-query](references/chat/message-query.md) |
| 编辑、撤回、回复、转发、Pin、Top、Favorite 或 reaction 写入 | [message-actions](references/chat/message-actions.md) |
| 位置、联系人名片、底层媒体与资源下载 | [message-media](references/chat/message-media.md) |
| 群列表、群搜索、共同群、成员与群内机器人读取 | [group-discovery](references/chat/group-discovery.md) |
| 建群、成员或已知机器人增删、管理员、公告与群设置 | [group-admin](references/chat/group-admin.md) |
| 搜索未知机器人、机器人消息发送/撤回与 Webhook | [chat-bot.md](references/chat/chat-bot.md) |
| 会话置顶、分类、红点、免打扰和隐藏 | [chat-conversation.md](references/chat/chat-conversation.md) |
| 低频意图之间仍需消歧 | [intent-guide.md](references/intent-guide.md) |
| 表情名称与 ID | [chat-emoji-list.md](references/chat-emoji-list.md) |
| 稳定结果、身份矩阵与能力边界 | [contracts.md](references/contracts.md) |
| 流式卡片创建 | [card/create.md](references/card/create.md) |
| 流式卡片更新 | [card/update.md](references/card/update.md) |
| 卡片 callback 是否可用 | [card/callback.md](references/card/callback.md) |
| 卡片公开 Schema 边界 | [card/schema.md](references/card/schema.md) |
| 只有上述 reference 仍无法定位的原子能力 | [chat.md](references/chat.md) 的对应章节 |
不要预加载 reference。Shortcut Catalog 只在根路由和精确 reference 都无法定位低频能力时使用。
## 错误最短路径
1. resolution 返回零命中或多候选:停止写操作,展示候选并让用户消歧;禁止默认第一项。
2. `unknown command` / `unknown flag`:读取精确 leaf Help,修正后最多重试一次。
3. 参数约束或 confirmation 不清楚:读取精确 leaf Schema,以 Runtime gate 为准。
4. 认证、权限、profile 或 confirmation 错误:读取 `dingtalk-shared` 的对应 reference;正常 IM 不读取完整 shared Skill。
5. `backend_dependency_unavailable`:保持原参数,对只读命令最多重试一次;不要改 flag、猜认证命令或切换同义原子命令,持续失败时保留 Trace ID。
6. 其他错误:保留真实错误和已完成/失败项;不要连续尝试同义原子命令。
@@ -0,0 +1,103 @@
# 消息任务级流程
只在单个 Golden Route 不能完成任务、需要跨步骤传递真实结果时读取本文件。简单姓名/群名文本发送、单会话读取和跨会话搜索直接按根 Skill 执行。
## 选择路线
1. 先选择任务语义最窄的 Shortcut。
2. 只有 Shortcut 暂不接受自然目标或目标类型时,才用一个只读 leaf/Shortcut 解析 ID。
3. 解析全部完成并消歧后再写入;不要边解析边产生部分副作用。
4. 后续步骤只使用真实返回字段,不从名称、URL 或上下文猜 ID。
## 群聊消息
<!-- dws-intent: chat.read.conversation -->读取或导出指定群聊/单聊的消息记录时使用 `dws chat +chat-messages`。可附带非必填的 `--sender-query <姓名>`:唯一解析成功后按稳定 `senderId` 筛选同一次读取结果并返回 `resolvedFilters`;解析失败、不完整或存在歧义时抑制未过滤消息并返回 `sender_resolution_failed`,不要补跑 `+search-msg`。混合姓名/ID 入口 `--sender` 无法经通讯录确认类型时可按原值 userId 精确过滤,但必须保留 `identity_unverified`
用户以发送者、关键词、@对象或消息类型为主要条件直接检索时优先使用 `+search-msg`;范围可以是单个、多个或全部会话。
指定会话按时间读取时,`+chat-messages` 使用公开可选的 `--start/--end/--order`,范围固定为
`[start,end)`;仅开始时间表示到本次执行当前时间,仅结束时间只支持 `desc`,升序必须有开始时间。
兼容别名为 `--start-time/--end-time/--sort`。旧 `--time/--direction` 只用于单边界兼容模式,不能混用。
已知群 ID 时直接读取:
```bash
dws chat +chat-messages --group <openConversationId> --format json
```
要求全量或导出时直接使用 Runtime 能力:
```bash
dws chat +chat-messages --group <openConversationId> \
--page-all --page-limit 50 \
--output ./exports/messages.json \
--format json
```
```bash
dws chat +chat-messages --group <openConversationId> \
--start "2026-08-01T00:00:00+08:00" --end "2026-08-02T00:00:00+08:00" \
--order asc --page-all --format json
```
必须检查 `complete/hasMore/nextPage/stopReason/failures`;达到页数或结果上限不是来源完整。
只有群名时,读取历史直接用 `+chat-messages --group <群名>`,普通文本发送直接用 `+send-to-group`。其它尚不接受群名的高级动作才先用 `+chat-search --query <群名>`;只有唯一候选才把 `openConversationId` 传给下一步。查询结果需要资源时在读取命令上加 `--download-resources`,不要让 Agent 手工遍历资源引用。按姓名读取单聊时先解析唯一用户 ID,再传给 `+chat-messages --user`
## 发送消息
- <!-- dws-intent: chat.send.dm -->姓名 + 简单文本:`dws chat +dm`
- <!-- dws-intent: chat.send.group -->群名 + 简单文本:`dws chat +send-to-group`
- <!-- dws-intent: chat.send.advanced -->已知 ID、文件、Bot、Webhook、复杂 @ 或幂等:`dws chat +messages-send`
- 姓名 + 文件/高级控制:`+messages-send --as user --user-query <姓名> --file <相对路径>`
- 群名 + 文件/高级控制:`+messages-send --as user --chat-query <群名> --file <相对路径>`
- Bot 多群文本/Markdown`+messages-send --as bot --robot-code <code> --groups <cid1,cid2>`
Runtime 去重并返回 `im.batch-write.v1` 逐目标 ledger,最多 100 个稳定群 ID。
`--user-query``--chat-query` 会在 CLI 内运行真实只读解析;零命中或多候选时在上传或发送前停止。Bot/Webhook 不接受这两个自然目标参数。
文件直接交给 `+messages-send --file`。不要恢复“独立上传 → 提取 mediaId → 发送”的旧默认链路。
## 创建群聊
<!-- dws-intent: chat.create.group -->基础建群默认使用 `dws chat +chat-create`;它同时接受 `--users` 稳定 ID 和 `--member-query` 姓名/花名。群主默认当前用户,也可用 `--owner-open-dingtalk-id``--owner-query` 明确指定。自然身份解析、候选消歧、稳定 ID 去重和创建前预检都由 CLI 完成:
```text
传入全部姓名
→ 对零命中和多候选统一消歧
→ 按稳定 ID 去重
→ 全部成功后执行一次 +chat-create
```
任一成员或群主未唯一解析时不会创建群;显式群主会加入初始成员且不再读取当前用户,省略群主时才以当前用户兜底。`--dry-run` 也走同一解析链。不要用群名预搜索伪装幂等,因为业务上允许同名群。
## 机器人消息
已知 `robotCode` 时使用 `+messages-send --as bot`;单群用 `--group`,多群用 `--groups` 或工作目录内安全的 `--groups-file`。未知机器人、机器人入群或撤回读取 [chat-bot.md](chat/chat-bot.md)。Bot 不继承 user 的文件/图片能力;只使用 leaf Schema 明确发布的文本/Markdown 能力。
## 引用与转发
- <!-- dws-intent: chat.reply.quote -->引用回复:`dws chat +messages-reply`;优先继续使用结果中的 `messageId``conversationId``deliveryStatus``referencedMessage`,未知投递状态不得写成成功送达。
- 单条转发:`+messages-forward`
- 合并转发:`+messages-combine-forward`
- 话题转发:`+messages-forward-topic`
先用 `+chat-messages``+search-msg``+messages-mget` 取得真实 `messageId` 和 conversation/thread 上下文。引用或合并消息中的子消息优先使用自己的 `messageId`,不要拿父消息 ID 代替。
## 上下文传递表
| 上一步 | 真实返回 | 下一步用途 |
|---|---|---|
| `+chat-search` | `openConversationId` | 高级发送、读取、群管理 |
| `dingtalk-contact` 唯一用户解析 | `userId` / `openDingTalkId` | 单聊、建群、@、按发送者搜索 |
| `+messages-send` | `openTaskId` / 投递结果 | 查询投递状态;不是回复/撤回消息 ID |
| `+chat-messages` / `+search-msg` / `+messages-mget` | `messageId`、conversation/thread、`resourceRefs` | 回复、转发、撤回、资源下载 |
| `+chat-create` | `openConversationId` | 新群后续消息与群管理 |
| 分页查询 | `hasMore` / `nextCursor` / `complete` | 继续翻页和完整性判断 |
## 完成判断
- 写操作检查任务级结果或可查询状态,不只看退出码。
- 读取检查 `complete``hasMore``failures`
- 下载检查每项 ledger;单项失败不抹掉已取得消息。
- 投递状态未知时报告 unknown 并保留幂等键,不自动换目标重发。
@@ -0,0 +1,10 @@
# 卡片回调边界
当前 DWS lower interface 没有卡片按钮/action callback 的订阅、验签或回复能力,
`callback_supported=false`。因此:
- 不生成 callback URL、签名密钥或虚构的监听命令;
- 不把 `dws event consume` 当作卡片 callback 的替代;
- 用户必须使用按钮交互时,停止并说明当前不支持,等待平台接口和 Runtime 正式发布。
卡片的 create/update 能力不代表 callback 可用。
@@ -0,0 +1,27 @@
# 创建流式卡片
使用 `dws chat +messages-send-card`。群目标传 `--group`;单聊 userId 传 `--receiver`
Runtime 会唯一解析为 openDingTalkId;已有 openDingTalkId 时传
`--receiver-open-dingtalk-id`。三种目标严格三选一。
- 只传目标:创建卡片并从真实结果取得 `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`。自动更新返回 `verified=true` 时已有明确生效证据;返回
`accepted=true, verified=false` 时仅表示服务端已接受请求但未提供独立更新证据,应如实说明,
不要重复创建或重复执行相同更新。只有错误明确标记 `retryable=true` 时,才使用原 `bizId`
重试;明确未应用或 `bizId` 不一致时停止并保留真实错误。若结果中已经包含 `openTaskId`
可以按用户需要查询一次投递状态;该查询只确认消息投递,不代表卡片正文已经更新成功。
当前内容仅为 streaming text,不接受 Lark Card JSON、组件树或按钮 callback。
```bash
dws chat +messages-send-card --group <openConversationId> --at-open-dingtalk-ids <mentionedOpenDingTalkId> --content "请确认"
dws chat +messages-send-card --group <openConversationId> --at-all --content "请大家确认"
```
@@ -0,0 +1,14 @@
# 流式卡片 Schema
DWS 当前公开的是 `im.streaming-card.v1` 工作流契约,不是任意组件 Schema:
- targetgroup、direct user、direct openDingTalkId
- contentstreaming text
- lifecyclecreate 可选串联 update,后续按 `bizId` update
- flowStatus15
- callback:不支持。
参数、required 和 confirmation 读取
`dws schema --cli-path "chat +messages-send-card" --compact -f json`
`dws schema --cli-path "chat +messages-update-card" --compact -f json`。不要把 Lark card JSON 字段翻译成
未发布的 DWS flags。
@@ -0,0 +1,13 @@
# 更新流式卡片
使用 `dws chat +messages-update-card --biz-id <bizId> --content <文本> --flow-status <1..5>`
状态为:1 processing、2 typing、3 completed、4 executing、5 error。Runtime 拒绝范围外的
状态;正常完成的最后一次更新应为 3。`bizId` 必须来自真实创建结果,不能用消息 ID 代替。
更新是写操作,confirmation 以精确 leaf Schema 与 Runtime gate 为准。失败后保留原
`bizId` 和状态,不创建新卡片来掩盖更新失败。
结果中 `verified=true` 表示已有明确更新证据;`accepted=true, verified=false` 仅表示服务端
接受了请求但未提供独立生效证据,应如实说明且不得重复执行相同更新。只有错误明确标记
`retryable=true` 时才重试;明确未应用或 `bizId` 不一致时停止。
@@ -0,0 +1,216 @@
# 钉钉默认表情列表(emoji 回应可用)
> 本文件列出钉钉支持的所有用户可见默认表情(showType=1),共 199 个。
>
> **来源与维护合同(reviewed snapshot**:表格来自钉钉默认表情 catalog 的 showType=1
> 人工复核快照;当前 lower interface 没有可靠的“列出默认 emoji”命令,因此本文件不是从
> Runtime 或旧 Catalog 反向生成。更新时必须取得新的官方/真实客户端 catalog,核对
> emotionId、中文 name、en_US、showType 和总数,说明来源与复核日期,再整体替换表格;禁止
> 根据用户输入或单次 reaction 响应增补。生成策略为 **non-generated reviewed reference**
> Schema/Skill 生成器不得机械重写它。最后复核:2026-08-03。
>
> 使用规则:
> - 用户描述的表情命中下表中的 `name` → 使用 `chat message add-emoji --emoji <name>` 贴 emoji 回应
> - 用户描述的表情未命中下表 → 先 `chat message create-text-emotion` 创建文字表情获取 emotionId,再 `chat message add-text-emotion` 贴文字表情回应
| # | emotionId | name | en_US |
|---|-----------|------|-------|
| 1 | emotion_001 | 微笑 | Smile |
| 2 | emotion_099 | 可爱 | Lovely |
| 3 | emotion_002 | 憨笑 | Wow |
| 4 | emotion_003 | 色 | Yum |
| 5 | emotion_004 | 发呆 | Dazed |
| 6 | emotion_005 | 老板 | Boss |
| 7 | emotion_036 | 傻笑 | Oops |
| 8 | emotion_006 | 流泪 | Sob |
| 9 | emotion_007 | 害羞 | Shy |
| 10 | emotion_008 | 闭嘴 | Silence |
| 11 | emotion_009 | 睡 | Sleepy |
| 12 | emotion_010 | 大哭 | Cry |
| 13 | emotion_011 | 尴尬 | Awkward |
| 14 | emotion_080 | 感谢 | Thanks |
| 15 | emotion_204 | 拒绝 | SayNo |
| 16 | emotion_078 | 赞 | Like |
| 17 | emotion_024 | 鼓掌 | Clap |
| 18 | emotion_105 | 打招呼 | Hi |
| 19 | emotion_159 | 666 | 666 |
| 20 | emotion_079 | 抱拳 | Salute |
| 21 | emotion_025 | 握手 | Shake |
| 22 | emotion_023 | OK | OK |
| 23 | emotion_033 | 胜利 | Peace |
| 24 | emotion_142 | 向左 | Left |
| 25 | emotion_143 | 向右 | Right |
| 26 | emotion_144 | 向上 | Up |
| 27 | emotion_145 | 向下 | Down |
| 28 | emotion_185 | 来呀 | Come |
| 29 | emotion_155 | 一点点 | ALittle |
| 30 | emotion_179 | 捏住 | Pinch |
| 31 | emotion_140 | 比心 | FingerHeart |
| 32 | emotion_106 | 送花花 | Flower |
| 33 | emotion_178 | 加油干 | MakeEffort |
| 34 | emotion_013 | 调皮 | Tongueout |
| 35 | emotion_014 | 大笑 | Laugh |
| 36 | emotion_015 | 惊讶 | Scowl |
| 37 | emotion_016 | 流汗 | Sweat |
| 38 | emotion_017 | 奋斗 | Fight |
| 39 | emotion_018 | 口罩 | Mask |
| 40 | emotion_019 | 生病 | Sick |
| 41 | emotion_020 | 吐 | Barf |
| 42 | emotion_021 | 难过 | Bummed |
| 43 | emotion_022 | 抓狂 | Crazy |
| 44 | emotion_026 | 右哼哼 | Humph |
| 45 | emotion_027 | 太阳 | Sunny |
| 46 | emotion_028 | 月亮 | Moon |
| 47 | emotion_029 | 强 | Thumbsup |
| 48 | emotion_030 | 弱 | Thumbsdown |
| 49 | emotion_031 | 彩带 | Tada |
| 50 | emotion_032 | 蛋糕 | Cake |
| 51 | emotion_034 | 骷髅 | Skull |
| 52 | emotion_035 | 撇嘴 | Pout |
| 53 | emotion_037 | 鄙视 | Dislike |
| 54 | emotion_038 | 嘘 | Shhh |
| 55 | emotion_040 | 思考 | Hmm… |
| 56 | emotion_041 | 亲亲 | Kiss |
| 57 | emotion_042 | 无奈 | Disappointed |
| 58 | emotion_043 | 感冒 | Pollution |
| 59 | emotion_044 | 对不起 | Sorry |
| 60 | emotion_045 | 再见 | Wave |
| 61 | emotion_046 | 投降 | GiveUp |
| 62 | emotion_047 | 哼 | Grumpy |
| 63 | emotion_048 | 欠扁 | FaceSlap |
| 64 | emotion_049 | 拜托 | Please |
| 65 | emotion_050 | 可怜 | Aww… |
| 66 | emotion_051 | 舒服 | Relax |
| 67 | emotion_052 | 爱意 | Romantic |
| 68 | emotion_054 | 财迷 | MoneyMoney |
| 69 | emotion_055 | 迷惑 | Puzzled |
| 70 | emotion_056 | 委屈 | Worried |
| 71 | emotion_057 | 灵感 | Idea |
| 72 | emotion_058 | 天使 | Angel |
| 73 | emotion_059 | 鬼脸 | SillyFace |
| 74 | emotion_060 | 凄凉 | Phew |
| 75 | emotion_061 | 郁闷 | Tired |
| 76 | emotion_063 | 坏笑 | Trick |
| 77 | emotion_064 | 算账 | SoMuch |
| 78 | emotion_206 | PK | PK |
| 79 | emotion_066 | 忍者 | Sneaky |
| 80 | emotion_039 | 衰 | Grr |
| 81 | emotion_067 | 炸弹 | Uh-Oh |
| 82 | emotion_081 | 笑哭 | LaughAndCry |
| 83 | emotion_082 | 嘿嘿 | Smirk |
| 84 | emotion_083 | 捂脸哭 | Facepalm |
| 85 | emotion_084 | 抠鼻 | NosePick |
| 86 | emotion_085 | 流鼻血 | BloodyNose |
| 87 | emotion_090 | 呲牙 | Grin |
| 88 | emotion_091 | 吃瓜 | EatingMelon |
| 89 | emotion_092 | 彩虹 | Rainbow |
| 90 | emotion_098 | 耶 | Yeah |
| 91 | emotion_012 | 发怒 | Steamed |
| 92 | emotion_100 | 捂眼睛 | CannotLook |
| 93 | emotion_101 | 推眼镜 | PushGlasses |
| 94 | emotion_102 | 暗中观察 | Peep |
| 95 | emotion_103 | 脑暴 | Brainstorming |
| 96 | emotion_112 | 冷笑 | Distressed |
| 97 | emotion_208 | 热 | HotFace |
| 98 | emotion_113 | 开心 | Happy |
| 99 | emotion_114 | 惊喜 | Surprised |
| 100 | emotion_115 | 回头 | LookBack |
| 101 | emotion_116 | 白眼 | RollEyes |
| 102 | emotion_117 | 一团乱麻 | Overwhelmed |
| 103 | emotion_149 | 黑眼圈 | DarkCircle |
| 104 | emotion_225 | 裂开 | Broken |
| 105 | emotion_156 | 恭喜 | Congrats |
| 106 | emotion_160 | 费解 | Confuse |
| 107 | emotion_167 | 收到 | RogerThat |
| 108 | emotion_186 | 快来 | ComeOn |
| 109 | emotion_086 | 敲打 | Hammer |
| 110 | emotion_176 | 捧脸 | HoldFace |
| 111 | emotion_194 | Get | Get |
| 112 | emotion_207 | 客服 | CustomerService |
| 113 | emotion_210 | AR | AR |
| 114 | emotion_221 | 小蜜蜂 | Bee |
| 115 | emotion_211 | 虎虎生威 | MajesticTiger |
| 116 | emotion_222 | 兔飞猛进 | Rabbit |
| 117 | emotion_226 | 龙头老大 | Dragon Face |
| 118 | emotion_236 | 蛇来运转 | Good luck |
| 119 | emotion_238 | 马上来财 | Wealth is coming |
| 120 | emotion_093 | 专注 | Concentrate |
| 121 | emotion_166 | 忙疯了 | CrazyBusy |
| 122 | emotion_187 | 等一等 | Wait |
| 123 | emotion_227 | 一脸苦笑 | Wry Smile |
| 124 | emotion_228 | 王之蔑视 | Unamused |
| 125 | emotion_229 | 洪荒之力 | Amazing |
| 126 | emotion_234 | 向左看 | Thinking |
| 127 | emotion_235 | 向右看 | Pondering |
| 128 | emotion_230 | YYDS | YYDS |
| 129 | emotion_231 | 这边请 | ThisWayPlease |
| 130 | emotion_232 | 弹射下班 | OffDuty |
| 131 | emotion_233 | 退退退 | Back |
| 132 | emotion_188 | 在吗 | Hello? |
| 133 | emotion_189 | 让人头大 | Hard |
| 134 | emotion_089 | 摊手 | Smugshrug |
| 135 | emotion_088 | 抱抱 | Hug |
| 136 | emotion_158 | 举手 | RaiseHand |
| 137 | emotion_177 | 开车 | Driving |
| 138 | emotion_191 | 抱大腿 | Follow |
| 139 | emotion_087 | 跪了 | YouWin |
| 140 | emotion_162 | 鞠躬 | Bow |
| 141 | emotion_180 | 选我 | PickMe |
| 142 | emotion_209 | 元气满满 | FullOfVitality |
| 143 | emotion_168 | 会议 | Meeting |
| 144 | emotion_095 | 猫咪 | Kitty |
| 145 | emotion_094 | 二哈 | Doggy |
| 146 | emotion_097 | 狗子 | Puppy |
| 147 | emotion_111 | 三多 | SanDuo |
| 148 | emotion_153 | 承让 | LetMeWin |
| 149 | emotion_154 | 撒花 | Celebration |
| 150 | emotion_070 | 礼物 | Present |
| 151 | emotion_104 | 生日快乐 | Birthday |
| 152 | emotion_071 | 爱心 | Love |
| 153 | emotion_072 | 心碎 | BrokenHeart |
| 154 | emotion_073 | 嘴唇 | Lips |
| 155 | emotion_074 | 鲜花 | Rose |
| 156 | emotion_075 | 残花 | Wilted |
| 157 | emotion_077 | 干杯 | Cheers |
| 158 | emotion_151 | 咖啡 | Coffee |
| 159 | emotion_152 | 奶茶 | MilkTea |
| 160 | emotion_202 | 茶 | Tea |
| 161 | emotion_218 | OKR | OKR |
| 162 | emotion_109 | KPI | KPI |
| 163 | emotion_108 | 100分 | 100 |
| 164 | emotion_110 | 对勾 | Check |
| 165 | emotion_192 | 打叉 | Wrong |
| 166 | emotion_174 | 气泡 | Bubble |
| 167 | emotion_157 | 加一 | PlusOne |
| 168 | emotion_193 | Done | Done |
| 169 | emotion_146 | 钉子 | Staple |
| 170 | emotion_076 | 出差 | BusinessTrip |
| 171 | emotion_181 | 高铁 | HighSpeedTrain |
| 172 | emotion_184 | 火箭 | Rocket |
| 173 | emotion_068 | 邮件 | Mail |
| 174 | emotion_163 | 文档 | Document |
| 175 | emotion_164 | 演示 | Presentation |
| 176 | emotion_165 | 表格 | Sheet |
| 177 | emotion_213 | 废纸篓 | Wastebasket |
| 178 | emotion_237 | 手机 | MobilePhone |
| 179 | emotion_203 | 时间 | Time |
| 180 | emotion_217 | 静音 | mute |
| 181 | emotion_201 | 公文包 | Briefcase |
| 182 | emotion_214 | 地球 | Earth |
| 183 | emotion_215 | 碳减排 | CarbonReduction |
| 184 | emotion_216 | 回收标志 | RecyclingSymbol |
| 185 | emotion_205 | 幼苗 | Seedling |
| 186 | emotion_096 | 红包 | RedPacket |
| 187 | emotion_150 | 锦鲤 | LuckyDog |
| 188 | emotion_148 | 福 | Luck |
| 189 | emotion_198 | 灯笼 | Lantern |
| 190 | emotion_199 | 爆竹 | Firecrackers |
| 191 | emotion_197 | 烟花 | Fireworks |
| 192 | emotion_195 | 恭喜发财 | Prosperity |
| 193 | emotion_161 | 月饼 | MoonCake |
| 194 | emotion_173 | 鸡腿 | ChickenLeg |
| 195 | emotion_169 | 休假 | Vacation |
| 196 | emotion_175 | 火 | Hot |
| 197 | emotion_223 | 点赞 | LikeHeartAndTripleSix |
| 198 | emotion_196 | 平安健康 | Peace&Health |
| 199 | emotion_200 | 定胜 | Victory |
@@ -0,0 +1,140 @@
# Chat 低频原子能力索引
> 返回入口:[DingTalk Chat Skill](../SKILL.md)
本文件只用于根 Skill 和精确 task reference 都未覆盖的低频底层能力。普通发送、读取、搜索、
建群、引用回复和查看置顶会话必须回到根 Skill 的 Golden Route,不在这里重新选路。
## 使用边界
1. 先确认任务确实需要 Shortcut 未发布的底层字段、原始响应或运维控制;
2. 读取精确原子 leaf Schema/Help,不加载产品级 Catalog 猜参数;
3. 自然目标仍必须唯一解析,禁止选择搜索结果第一项;
4. 原子写 leaf 的 confirmation 若与对应 Golden Shortcut 不一致,停止并报告交付漂移;
5. 后续 ID 只使用当前 profile 的真实返回,不跨组织复用;
6. 完成后保留原始结果、partial failure 和可继续编排的稳定 ID。
## 高频任务返回表
| 用户终点 | 返回入口 |
|---|---|
| 姓名/群名简单发送、文件、Bot、Webhook、复杂 @ | 根 Skill Golden Route |
| 消息读取、条件搜索、@我、Favorite/reaction 查询和批量详情 | [message-query](chat/message-query.md) |
| 编辑、撤回、引用、转发、reaction/Pin/Top/Favorite 写入 | [message-actions](chat/message-actions.md) |
| 位置、名片、资源下载和特殊媒体 fallback | [message-media](chat/message-media.md) |
| 群列表、群搜索、成员读取、Bot 列表和邀请链接 | [group-discovery](chat/group-discovery.md) |
| 建群、改群、成员写入、管理员、禁言、公告和群设置 | [group-admin](chat/group-admin.md) |
| 跨步骤消息/群组合流程 | [消息任务级流程](01-messaging.md) |
| Bot 搜索、进群和撤回 | [chat-bot](chat/chat-bot.md) |
| 会话置顶、状态和分组 | [chat-conversation](chat/chat-conversation.md) |
| 话题与话题圈的创建、发布、浏览、回复、互动和整条转发 | [thread](chat/thread.md) |
| 相邻低频意图仍需消歧 | [intent-guide](intent-guide.md) |
## 消息底层能力
| 原子命令 | 仅用于 |
|---|---|
| `chat message send` | `+messages-send` 尚未发布的位置、名片等真实底层消息类型 |
| `chat message list` | 需要原始响应或显式手工 continuation;普通浏览使用 `+chat-messages` |
| `chat message list-all` | 指定时间范围的原始全会话分页接口 |
| `chat message list-by-sender` | 需要原始按发送者响应;普通组合搜索使用 `+search-msg` |
| `chat message list-mentions` / `list-focused` | 精确的 @我或特别关注原始列表 |
| `chat message search` / `search-advanced` | `+search-msg` 未发布的底层过滤字段或原始响应 |
| `chat message query-send-status` | 使用真实 `openTaskId` 查询用户消息投递任务 |
| `chat message recall` / `edit` | 撤回或编辑已知消息 |
| `chat message read-status` | 查询已知消息的已读/未读状态 |
| `chat message reply` | `+messages-reply` 未发布的底层引用字段,且安全门禁已对齐 |
| `chat message forward` / `combine-forward` | Shortcut 未覆盖的精确转发字段 |
| `chat message download-media` | Shortcut 无法消费的已知底层 mediaId/fileId 引用 |
消息对象管理:
| 原子命令 | 对象 |
|---|---|
| `message set-pin-msg` / `unset-pin-msg` / `list-pin-msg` | 消息 Pin |
| `message set-top-msg` / `unset-top-msg` | 会话内消息 Top |
| `message add-favorite` / `remove-favorite` / `list-favorites` | 当前用户 Favorite |
| `message add-emoji` / `remove-emoji` | 默认 emoji reaction |
| `message create-text-emotion` / `add-text-emotion` / `update-text-emotion` / `remove-text-emotion` | 文字表情 |
| `message list-emotion-replies` | 批量 reaction/文字回应 |
| `emotion list` / `send` / `favorite` | 当前用户个人收藏表情列表、发送和新增 |
Favorite、消息 Pin、消息 Top 与会话 Top 是四种对象,不能互换。
个人收藏表情与消息 reaction/文字回应不同;发送收藏表情使用 `chat emotion send`,给已有消息贴表情使用 `chat message add-emoji``chat message add-text-emotion`
## 群与成员底层能力
| 原子命令 | 用途 |
|---|---|
| `chat search` / `search-common` | 群管理前解析唯一群、查询共同群 |
| `chat group get-by-group-id` | 数字群号转 `openConversationId` |
| `chat group create` | `+chat-create` 尚未发布的真实底层创建字段;显式群主已由 Shortcut 覆盖 |
| `chat group members` / `members list-by-ids` | 群成员分页和精确详情 |
| `chat group members add` / `remove` | 添加/移除已知成员 ID |
| `chat group members add-bot` / `remove-bot` / `group bots` | 机器人进群、移除和列表 |
| `chat group rename` / `update-icon` | 群名和群头像 |
| `chat group transfer-owner` / `set-admin` | 群主和管理员 |
| `chat group upgrade-to-external` | 普通群升级外部群;不可逆 |
| `chat group invite-url` / `share-invite` | 群邀请链接及分享 |
| `chat group update-settings` / `user-settings query|set` | 管理员群开关或当前用户群偏好 |
| `chat group update-nick` / `update-alias` | 当前用户群昵称和群备注 |
| `chat group set-history` | 新成员历史消息可见范围 |
| `chat group-mute` / `group-mute-member` | 全员或指定成员禁言 |
| `chat group notice create|edit|get|list` | 群公告 |
| `chat group list-my-groups` / `list-all` | 当前用户相关群列表 |
| `chat group list-join-validations` / `audit-join-validation` | 入群审批 |
| `chat group-role *` | 群身份定义与成员分配 |
退出、解散群、踢人、转让群主、升级外部群、禁言、管理员和公告写入都属于高影响操作;
必须以最终 Runtime gate/Schema 为准确认对象与影响。
## Bot 与 Webhook 底层能力
| 原子命令 | 用途 |
|---|---|
| `chat bot search` | 搜索当前用户创建的机器人并取得 `robotCode` |
| `chat bot find` | 搜索可用机器人并取得机器人 `openDingTalkId` |
| `chat message send-by-bot` | `+messages-send --as bot` 未发布的真实底层字段,包括机器人群聊引用回复的 `--reply` / `--ref-sender` |
| `chat message recall-by-bot` | 使用 `processQueryKey` 撤回机器人消息 |
| `chat message send-by-webhook` | `+messages-send --as webhook` 未发布的真实底层字段 |
新发送流程统一使用 `+messages-send`。不得因看见 bot/webhook 原子命令就绕开统一身份能力矩阵。
## 会话状态与分组
| 原子命令 | 用途 |
|---|---|
| `chat conversation-info` | 已知稳定用户/群 ID 的会话详情 |
| `chat list-all-conversations` | 全部会话原始分页列表 |
| `chat list-top-conversations` | 需要原始响应时的置顶会话 fallback;普通查看使用 `+conversation-list-top` |
| `chat set-top` | 设置/取消整个会话置顶 |
| `chat mute` / `hide` / `mute-at-all` / `mute-red-envelope` | 会话通知与可见状态 |
| `chat mark-unread` / `mark-read` | 会话未读或消息已读状态 |
| `chat clear-red-point` / `clear-all-red-point` | 清除会话红点 |
| `chat clear-messages` | 清空当前用户视角的会话记录 |
| `chat category *` | 自定义/智能会话分组 |
消息 Top 使用 `message set-top-msg`,整个会话 Top 使用 `chat set-top`,查看置顶会话使用
`+conversation-list-top`
## 稳定 ID 传递
| 来源 | 只可用于 |
|---|---|
| 唯一群解析 / `+chat-create` | 当前 profile 下的 `openConversationId` |
| 唯一人员解析 | 当前 profile 下的 `userId` / `openDingTalkId` |
| `+messages-send` | `openTaskId` 查询投递状态;它不是消息 ID |
| `+chat-messages` / `+search-msg` / `+messages-mget` | 回复、转发、撤回、资源操作使用的真实消息/会话/thread ID |
| `chat bot search` | `robotCode`;不能当机器人 `openDingTalkId` |
| `chat message send-by-bot` | `processQueryKey` 用于机器人撤回;群聊引用回复还需消息查询返回的 `openMessageId` 与原发送者 `openDingTalkId` |
显式稳定 ID 当前不携带可验证的 profile provenance;调用方必须保证来源,不得宣称所有
跨 profile 误用都会在本地写入前被拦截。
## 故障处理
- `unknown command` / `unknown flag`:读取精确 leaf Help,最多修正一次;
- confirmation 或参数约束不清:读取精确 leaf Schema,以最终 Runtime gate 为准;
- 自然目标零命中/多候选:停止并展示候选,不选择第一项;
- 权限、认证或 profile:按 `dingtalk-shared` 对应 reference 分流;
- partial result:保留已完成项、失败 ledger、continuation 和真实错误,不换同义原子命令重试。
@@ -0,0 +1,187 @@
# chat-bot:机器人与 Webhook
> 返回入口:[chat.md](../chat.md)
## 适用场景
用于搜索机器人、机器人发送/撤回消息、Webhook 告警、机器人加入/移出群,以及给机器人发单聊消息。
<!-- dws-intent: chat.send.advanced -->Bot/Webhook 默认统一使用 `dws chat +messages-send`,通过
`--as bot|webhook` 选择身份;原子发送命令只保留 Shortcut 未发布字段的底层 fallback。
## 必读约束
- 用户明确要求“用机器人/机器人身份/robot”发送时,使用
`dws chat +messages-send --as bot --robot-code <robotCode>`;不得改成当前用户身份。
- `chat bot search` 只返回我创建的机器人,没有 `openDingTalkId`;给机器人发单聊必须用 `chat bot find`
- 机器人发群消息前需确认机器人已在群中;报“机器人不存在”时先 `group members add-bot`
- `send-by-bot` 支持 Markdown、图片 URL 和文件,具体参数见下方消息类型路由。
- 机器人在群聊中引用回复已有消息时,使用原子命令 `send-by-bot --reply --ref-sender`;该能力仅支持 Markdown,不走 `+messages-reply` 的当前用户身份。
- 公网图片 URL 使用 `--msg-type image --image-url`,按图片消息发送。
- 本地图片和其他本地文件一样使用 `--msg-type file --file-path`,由 CLI 上传并按文件附件发送。
- 群聊传 `--group`;单聊可传 `--users``--open-dingtalk-ids` 或两者组合。
- Markdown 必须同时传 `--title``--text`;需要稳定换行时用空行分隔段落。若以转义形式组织文本,写 `\n\n`,不要只写 `\n`
- `recall-by-bot` 使用 `processQueryKey`,不是 `openMessageId`
- Bot 多群文本/Markdown 直接使用 `+messages-send --groups <cid...>`
`--groups-file <工作目录内相对文件>`;最多 100 个稳定 IDRuntime 去重并返回
`im.batch-write.v1` 逐目标 ledger。
- `+messages-send` 的 Bot 路由和 Webhook 没有与 current-user 等价的文件、图片、音视频发送接口;
机器人富媒体必须使用原子 `chat message send-by-bot`,不得转成文本或换身份静默发送。
## 命令明细
### 机器人搜索
| 命令 | 范围 | 返回 openDingTalkId | 典型触发词 |
|------|------|---------------------|------------|
| `chat bot search` | 仅当前用户自己创建的机器人 | 否 | “我的机器人”“我创建的机器人” |
| `chat bot find` | 当前用户可用的全部机器人(含他人/官方) | 是 | “找机器人”“搜索机器人”“给机器人发单聊” |
```bash
dws chat bot search --page 1 --size 10 --name "日报"
dws chat bot find --query "日报" --limit 20
dws chat bot find --query "日报" --limit 20 --cursor <nextCursor>
```
`bot find` 翻页时 `cursor` 必须使用上次返回的 `nextCursor` 字符串原值,不要传 `"0"` 或数字字面量。
### 机器人发送与撤回
多群正式入口:
```bash
dws chat +messages-send --as bot --robot-code <robot-code> \
--groups <openConversationId1>,<openConversationId2> \
--markdown "## 通知\n\n请提交周报" --format json
```
读取 `requestedCount/succeededCount/failedCount/results/failures`;unknown 或失败目标不自动重发。
#### `dws chat message send-by-bot`(底层 fallback
普通文本/Markdown、群聊/批量单聊和 @ 已由 `+messages-send --as bot` 覆盖。只有 Shortcut
缺失真实必需字段且精确 leaf Schema 允许时才使用以下原子命令。
```bash
# 群聊
dws chat message send-by-bot --robot-code <robot-code> --conversation-id <openConversationId> --title "日报" --text "## 今日完成\n\n- 事项 A\n\n- 事项 B"
dws chat message send-by-bot --robot-code <robot-code> --conversation-id <openConversationId> --reply <openMessageId> --ref-sender <senderOpenDingTalkId> --text "收到"
dws chat message send-by-bot --robot-code <robot-code> --conversation-id <openConversationId> --msg-type image --image-url "https://example.com/image.png"
dws chat message send-by-bot --robot-code <robot-code> --conversation-id <openConversationId> --msg-type file --file-path ./report.pdf
# 单聊 userId
dws chat message send-by-bot --robot-code <robot-code> --users userId1,userId2 --title "提醒" --text "请提交周报"
# 单聊 openDingTalkId
dws chat message send-by-bot --robot-code <robot-code> --open-dingtalk-ids openDingTalkId1,openDingTalkId2 --title "提醒" --text "请提交周报"
# 群聊 @ 人
dws chat message send-by-bot --robot-code <robot-code> --group <openConversationId> --at-user-ids userId1,userId2 --title "提醒" --text "@userId1 @userId2 请查收"
dws chat message send-by-bot --robot-code <robot-code> --group <openConversationId> --at-open-dingtalk-ids openDingTalkId1,openDingTalkId2 --title "提醒" --text "@openDingTalkId1 @openDingTalkId2 请查收"
dws chat message send-by-bot --robot-code <robot-code> --group <openConversationId> --at-all --title "通知" --text "请所有人注意"
```
关键 flags
| Flag | 说明 |
|------|------|
| `--robot-code` | 机器人 Code,必填 |
| `--conversation-id` | 群聊 openConversationId`--group` 为兼容别名 |
| `--users` | 单聊 userId 列表,逗号分隔,最多 20 个 |
| `--open-dingtalk-ids` | 单聊 openDingTalkId 列表 |
| `--msg-type` | `markdown``image``file`;省略时为 Markdown;公网图片使用 `image --image-url`,本地图片和文件使用 `file --file-path` |
| `--text` | Markdown 消息内容,Markdown 模式必填;换行用空行,转义表示为 `\n\n` |
| `--title` | 普通 Markdown 消息标题;引用回复省略时由 CLI 从正文生成 |
| `--image-url` | 公网图片 URL`--msg-type image` 时必填 |
| `--file-path` | 本地图片或文件路径,`--msg-type file` 时由 CLI 上传并按文件附件发送 |
| `--at-user-ids` / `--at-open-dingtalk-ids` | 群聊 @ 指定成员,正文需含对应 `@id` 文本 |
| `--at-all` | 群聊 @所有人 |
| `--reply` | 被引用消息的 `openMessageId`;仅群聊 Markdown,必须与 `--ref-sender` 同时使用 |
| `--ref-sender` | 被引用消息发送者的 `openDingTalkId`;仅群聊 Markdown,必须与 `--reply` 同时使用 |
引用回复不会设置 `msgType=reply`CLI 在普通群消息参数顶层透传 `referenceOpenMessageId``srcMsgSendOpenDingTalkId`。只传其中一个参数、用于单聊或用于图片/文件消息都会在本地失败。
#### `dws chat message recall-by-bot`
```bash
dws chat message recall-by-bot --robot-code <robot-code> --group <openConversationId> --keys <processQueryKey>
dws chat message recall-by-bot --robot-code <robot-code> --keys key1,key2
```
群聊撤回传 `--group`;单聊撤回不传 `--group``--keys` 来自 `send-by-bot` 返回的 `processQueryKey`
### Webhook
默认使用:
```bash
dws chat +messages-send --as webhook --webhook-token <webhook-token> --title "告警" --text "CPU 超 90% @10" --at-all
```
以下是底层 fallback,不作为默认选路:
```bash
dws chat message send-by-webhook --token <webhook-token> --title "告警" --content "CPU 超 90% @10" --at-all
dws chat message send-by-webhook --token <webhook-token> --title "test" --content "hi @118785" --at-users 118785
```
关键规则:
- `--token``--title``--content` 必填。
- `--at-all``--content` 中需包含 `@10`
- `--at-users``--at-mobiles` 时,`--content` 中需包含对应 `@userId``@手机号`,否则 @ 不生效。
### 机器人进群
| 命令 | 用途 | 必填参数 |
|------|------|----------|
| `group members add-bot` | 将自定义机器人加入群 | `--id` `--robot-code` |
| `group members remove-bot` | 从群移除机器人 | `--id` `--bot-id` |
| `+chat-bots` | 查看群内机器人列表 | `--group <群名或openConversationId>`;自然群名内部唯一解析 |
```bash
dws chat group members add-bot --id <openConversationId> --robot-code <robot-code>
dws chat +chat-bots --group "项目群"
dws chat group members remove-bot --id <openConversationId> --bot-id <openBotId>
```
## 常见工作流
### 机器人发消息后撤回
```bash
dws chat bot search --name "日报" --format json
dws chat +messages-send --as bot --robot-code <robot-code> --group <openConversationId> --title "日报" --markdown "## 今日完成\n\n- 事项 A\n\n- 事项 B" --format json
dws chat message recall-by-bot --robot-code <robot-code> --group <openConversationId> --keys <processQueryKey> --format json
```
### 机器人不在群内时先邀请再发送
```bash
dws chat bot search --name "日报" --format json
dws chat group members add-bot --id <openConversationId> --robot-code <robot-code> --format json
dws chat +messages-send --as bot --robot-code <robot-code> --group <openConversationId> --title "通知" --text "内容" --format json
```
### 给机器人发单聊
```bash
dws chat bot find --query "玉澜" --format json
dws chat message send --open-dingtalk-id <openDingTalkId> --content "你好" --format json
```
### 机器人 @ 指定人
```bash
dws aisearch person --query "张三" --dimension name --format json
dws chat +messages-send --as bot --robot-code <robot-code> --group <openConversationId> --at-user-ids userId1 --title "提醒" --text "@userId1 请查收" --format json
```
## 常见错误与回退
- 机器人单聊没有 openDingTalkId:改用 `chat bot find`,不要用 `bot search`
- 机器人发群消息报“机器人不存在”:先 `group members add-bot`
- 撤回失败:确认使用 `processQueryKey`,不是 `openMessageId`
- 机器人引用回复失败:确认目标是群聊 Markdown,且 `--reply` 来自被引用消息的 `openMessageId``--ref-sender` 来自同一消息的发送者 `openDingTalkId`
- @ 不生效:检查正文是否包含 `@userId` / `@openDingTalkId` / `@10`
- 需要 Bot 图片/文件/音视频:停止并说明当前身份矩阵不支持;只有用户明确同意改为当前用户身份时,才重新确认目标和内容。
@@ -0,0 +1,154 @@
# chat-conversation:会话状态、红点、置顶与分组
> 返回入口:[chat.md](../chat.md)
## 适用场景
用于获取会话基础信息、全部会话/置顶会话列表、会话置顶、免打扰、隐藏、红点、已读未读、清空聊天记录、自定义会话分组和智能会话分组。
- <!-- dws-intent: chat.conversation.list-top -->查看置顶会话默认使用 `dws chat +conversation-list-top`
原子 `list-top-conversations` 只在需要原始响应时作为 fallback。
- <!-- dws-intent: chat.read.conversation -->取得目标会话后读取或导出消息记录,默认使用 `dws chat +chat-messages`
可附带非必填的 `--sender-query` 解析姓名:唯一解析成功后按 `senderId` 筛选同一次读取结果;
解析失败、不完整或存在歧义时抑制未过滤消息并返回 `sender_resolution_failed`。不要回到原子
`message list`,也不要补跑 `+search-msg`。直接条件检索优先使用 `+search-msg`
## 必读约束
- 会话状态类命令通常需要 `openConversationId`。群聊只用 `+chat-search --query` 获取唯一候选,单聊可由 `chat conversation-info --user/--open-dingtalk-id` 获取。
- `set-top` 是会话置顶;`message set-top-msg` 是会话内消息置顶,二者不能混用。
- `clear-messages` 只清空当前用户视角的消息,不影响其他成员。
- 智能分组规则中的成员使用 openDingTalkId;如果用户只给姓名,先用 `aisearch person --dimension name` 获取。
## 命令明细
### 会话基础信息
```bash
dws chat conversation-info --group <openConversationId> --format json
dws chat conversation-info --user <userId> --format json
dws chat conversation-info --open-dingtalk-id <openDingTalkId> --format json
```
`--group``--user``--open-dingtalk-id` 互斥且必须指定一个。文件/音视频发送不再依赖调用方读取 spaceId;直接用 `message send --msg-type file|audio|video --file`
### 会话列表与红点
| 命令 | 用途 | 参数 |
|------|------|------|
| `+conversation-list` | 获取当前用户会话 | 要求“全部”时加 `--page-all`;检查 `complete` / `failures` |
| `+chat-list` | 列出当前用户会话(默认群聊,可选单聊) | 默认只返回群聊;`--types group,p2p` 可包含单聊;要求全部时加 `--page-all`,合并去重后再过滤类型 |
| `+chat-list-all` | 获取当前用户加入的全部群 | 要求全部时加 `--page-all`;沿数字 `nextCursor` 去重聚合 |
| `+my-groups` | 获取并投影当前用户加入的群 | 要求全部时加 `--page-all`;读完后再应用 `--type` 本地过滤 |
| `+conversation-list-top` | 获取置顶会话列表 | 可选 `--limit` `--cursor` `--exclude-muted`;使用稳定 `conversations[]` |
| `message list-unread-conversations` | 获取未读会话列表 | 可选 `--count` `--exclude-muted` |
| `clear-red-point` | 清除指定会话红点 | `--conversation-id`,别名 `--id` / `--chat` |
| `clear-all-red-point` | 清除所有会话红点,一键全部已读 | 无参数 |
翻页时,`hasMore=true` 用返回的 `nextCursor` 作为下次 `--cursor`。Shortcut 全量读取应检查
`complete``stopReason``failures`;达到 `--page-limit` 时会保留可继续的 `nextCursor`
### 会话置顶与通知
| 命令 | 用途 | 必填参数 |
|------|------|----------|
| `set-top` | 设置/取消会话置顶 | `--conversation-id`;默认置顶,`--off` 取消 |
| `mute` | 开启/关闭会话免打扰 | `--conversation-id`;默认开启,`--off` 关闭 |
| `hide` | 隐藏会话 | `--conversation-id` |
| `mute-at-all` | 关闭/恢复 @所有人通知 | `--conversation-id`;默认关闭,`--off` 恢复;必须先开启会话总免打扰 |
| `mute-red-envelope` | 关闭/恢复红包通知 | `--conversation-id`;默认关闭,`--off` 恢复;必须先开启会话总免打扰 |
若连续操作两个子开关,优先操作红包通知;恢复 @所有人通知后,平台可能清除子开关所需的
总免打扰状态,此时要先重新开启总免打扰,再操作红包通知。
```bash
dws chat set-top --conversation-id <openConversationId>
dws chat set-top --conversation-id <openConversationId> --off
dws chat mute --conversation-id <openConversationId>
dws chat mute --conversation-id <openConversationId> --off
dws chat hide --conversation-id <openConversationId>
```
### 已读未读与清理
| 命令 | 用途 | 必填参数 |
|------|------|----------|
| `mark-unread` | 标记指定会话为未读 | `--conversation-id` |
| `mark-read` | 将指定消息及之前消息标记为已读 | `--conversation-id` `--message-id` |
| `clear-messages` | 清空当前用户指定会话的消息 | `--conversation-id` |
```bash
dws chat mark-unread --conversation-id <openConversationId>
dws chat mark-read --conversation-id <openConversationId> --message-id <openMessageId>
dws chat clear-messages --conversation-id <openConversationId>
```
### 会话分组
| 命令 | 用途 | 必填参数 |
|------|------|----------|
| `category list` | 获取用户自定义会话分组 | 无 |
| `category list-conversations` | 拉取指定分组下会话 | `--category-id`,可选 `--exclude-muted` |
| `category list-by-conv` | 拉取指定会话所属的用户自定义会话分组 | `--group` |
| `category batch-info` | 批量拉取用户自定义会话分组信息 | `--category-ids` |
| `category create` | 创建会话分组 | `--title` |
| `category create-smart` | 创建智能会话分组,可按群名称关键词和群内成员匹配 | `--name`,可选 `--keywords` `--members` |
| `category delete` | 删除会话分组 | `--category-id` |
| `category rename` | 修改分组名称 | `--category-id` `--title` |
| `category add-conv` | 将会话加入分组 | `--group` `--category-ids` |
| `category remove-conv` | 将会话移出分组 | `--group` `--category-ids` |
```bash
dws chat category list
dws chat category list-by-conv --group <openConversationId>
dws chat category batch-info --category-ids 123,456
dws chat category create --title "工作群"
dws chat category create-smart --name "重点群" --keywords "重点,项目" --members openDingTalkId1,openDingTalkId2
dws chat category add-conv --group <openConversationId> --category-ids 123,456
```
`create-smart``--keywords` 是群名称关键词列表,`--members` 是群内成员 openDingTalkId 列表;两者可单独使用,也可组合使用。
## 常见工作流
### 获取单聊会话 ID 后置顶
```bash
dws aisearch person --query "张三" --dimension name --format json
dws chat conversation-info --user <userId> --format json
dws chat set-top --conversation-id <openConversationId> --format json
```
### 查看置顶会话并拉消息
```bash
dws chat +conversation-list-top --limit 100 --format json
dws chat +chat-messages --group <openConversationId> --time "2026-03-10 00:00:00" --direction older --format json
```
### 会话分组
```bash
dws chat category create --title "重点项目" --format json
dws chat category add-conv --group <openConversationId> --category-ids <categoryId> --format json
dws chat category list-by-conv --group <openConversationId> --format json
dws chat category batch-info --category-ids <categoryId> --format json
dws chat category list-conversations --category-id <categoryId> --format json
```
### 智能会话分组
```bash
dws aisearch person --query "张三" --dimension name --format json
dws chat category create-smart --name "项目组" --keywords "项目,开发" --format json
dws chat category create-smart --name "团队群" --members openDingTalkId1,openDingTalkId2 --format json
dws chat category create-smart --name "重点群" --keywords "重点" --members openDingTalkId1 --format json
```
## 常见错误与回退
- 用户说“置顶消息”:用 `message set-top-msg`,不是 `chat set-top`
- 用户说“置顶会话”:设置/取消用 `chat set-top`,查看列表用 `+conversation-list-top`
- 单聊没有会话 ID:先 `conversation-info --user``--open-dingtalk-id`
- 清空聊天记录前必须确认目标会话;该操作只影响当前用户视角。
- 智能分组没有匹配条件:至少确认分组名称;关键词和成员规则不明确时先向用户确认,不要自行猜成员。
@@ -0,0 +1,144 @@
# group-admin:群创建、成员写入与管理
> 返回入口:[DingTalk Chat Skill](../../SKILL.md)
用于建群、修改群资料、成员增删、邀请卡片分享、群主和管理员、禁言、公告、群设置、
入群审批、群身份、退出、解散和升级外部群。只读群发现、成员读取和邀请链接使用
[group-discovery.md](group-discovery.md)。
## 安全与目标
- 群目标统一使用当前 profile 下真实 `openConversationId`;支持自然群名的 Shortcut 由 CLI
唯一解析,多候选时停止。
- 解散群、踢人、转让群主、禁言、管理员和外部群升级都是高影响操作;以最终 Runtime gate
和精确 leaf Schema 为准确认对象、动作与影响。
- 所有自然成员和群主必须先完成唯一解析并按稳定 ID 去重,再开始任何写入;不得边解析边
产生部分副作用。
- 群公告会触达成员;`notice edit` 是整体替换,必须有完整新正文。
## 建群与基础资料
<!-- dws-intent: chat.create.group -->基础建群使用 `dws chat +chat-create`。已知成员 ID 传 `--users`
姓名/花名传 `--member-query`;群主默认当前用户,也可传 `--owner-open-dingtalk-id`
`--owner-query`。任一自然身份未唯一解析时,创建前整体停止。
```bash
dws chat +chat-create --name "项目冲刺群" --member-query "测试用户甲,测试用户乙" --format json
dws chat +chat-create --name "合作群" --member-query "测试用户甲" \
--owner-query "测试用户乙" --type EXTERNAL --format json
```
修改群名称优先使用接受群名或稳定 ID 的 `+chat-update`
```bash
dws chat +chat-update --group <群名或openConversationId> --name "新群名" --format json
```
群头像和管理员级群开关使用 `+chat-update-icon``+chat-update-settings`;只有 Shortcut
尚未发布真实必需字段时才评估原子 `group rename/update-icon/update-settings`
原子 `chat group create` 只用于 `+chat-create` 未发布的真实底层字段,并先读取精确 leaf
Schema。普通内部/外部群、话题群和显式群主已经由 `+chat-create` 覆盖,不回流到手工
`aisearch → group create` 链路。
## 成员与机器人写入
| 动作 | 入口与关键参数 |
|---|---|
| 添加成员 | `group members add --id <cid> --users <userIds>` |
| 移除成员 | `group members remove --id <cid> --users <userIds>` |
| 添加已知机器人 | `+chat-add-bot` 或精确原子 `group members add-bot` |
| 查看群内机器人 | `+chat-bots --group <群名或cid>` |
| 移除群内机器人 | `+chat-remove-bot` 或精确原子 `group members remove-bot` |
普通成员增删的 `--users` 只接受组织 `userId`,必须来自真实人员解析结果;不得把
`+chat-members-list` / `+chat-members-get` 返回的 `openDingTalkId` 直接传入。添加已知机器人
使用 `robotCode`;移除机器人使用当前群 `+chat-bots` 返回的真实 `openBotId`,两者不能互换。
缺少 `openBotId` 时在同一流程中先执行 `+chat-bots`,不必额外读取群发现 reference。只有需要
搜索未知机器人、区分 `bot search` / `bot find`、机器人发送或撤回、Webhook 时,才读取
[chat-bot.md](chat-bot.md)。
## 邀请卡片、群主、管理员与禁言
邀请链接只读走 `+chat-invite-url`。实际分享邀请卡片使用 `group share-invite``--source`
是被分享群,接收端在 `--target` 会话和 `--receiver` 单聊用户之间二选一。
```bash
dws chat group share-invite --source <sourceCid> --target <targetCid> --format json
dws chat group share-invite --source <sourceCid> --receiver <openDingTalkId> --format json
```
| 动作 | 入口与关键参数 |
|---|---|
| 转让群主 | `+chat-transfer-owner --group <cid> --new-owner <稳定ID>` |
| 设置/取消管理员 | `group set-admin --group <cid> --users <ids> [--off]` |
| 全员禁言/解除 | `group-mute --group <cid> [--off]` |
| 成员禁言/解除 | `+chat-mute-member``group-mute-member` |
| 查询禁言配置 | `group get-mute-config --group <cid>` |
原子 `group-mute-member --mute-time` 单位为毫秒。不要用展示名称代替稳定用户 ID,也不要
在未确认影响时执行转让、踢人或禁言。
## 群设置与当前用户偏好
管理员级群开关使用 `+chat-update-settings` 或原子 `group update-settings`。常见 settingKey
包括 `authority``joinValidation``onlyAdminCanAtAll``searchable`
`addFriendForbidden``onlyAdminCanDING``onlyAdminCanPinMsg`
`onlyAdminCanSendFile``groupEmailDisabled``groupLiveAuthority`
`groupBillAuthority`;只修改用户明确要求的字段。
新成员历史消息可见范围使用 `group set-history --group <cid> --option <值>``option` 只取
精确 leaf Schema 发布值,不按自然语言猜枚举。
当前登录用户自己的置顶、免打扰、群昵称和群备注使用 `group user-settings query/set`
不是管理员群开关。单个群昵称/备注优先 `group update-nick/update-alias`
```bash
dws chat group user-settings query --groups <cid1>,<cid2> --format json
dws chat group user-settings set \
--items '[{"openConversationId":"cid1","top":true,"mute":false}]' --format json
```
批量设置只传本次要改的字段;空字符串清除昵称或备注,不补用户未要求的值。
## 群公告
| 动作 | 原子入口 |
|---|---|
| 发布公告 | `group notice create --group <cid> --content <完整Markdown>` |
| 修改公告 | `group notice edit --group <cid> --notice-id <id> --content <完整Markdown>` |
| 查询公告 | `group notice get/list` |
定时公告 `--run-at` 使用带时区时间;`notice list --scheduled` 查询待发布公告。分页时沿真实
`nextPageCursor` 继续。修改前必须取得完整替换正文,不把增量片段当整篇公告。
## 入群审批与群身份
先用 `group list-join-validations` 取得真实 `record-id/applicant/inviter`,再执行
`group audit-join-validation``+chat-audit-join`。审批状态只使用精确 leaf Schema 发布值。
群身份使用 `group-role` / `+chat-role-*`
- `list/add/update/remove` 管理身份定义;
- `set-user/remove-user/query-user` 管理成员身份;
- `openRoleId` 必须来自真实身份列表。
覆盖或清除成员身份前确认用户、群和完整角色集合,不能用展示名称猜 `openRoleId`
## 退出、解散与外部群升级
- 当前用户退出群:`+chat-quit` 或精确原子 `group quit`
- 解散群:`group dismiss`,不可逆。
- 普通群升级外部群:`group upgrade-to-external`,不可逆。
这些动作必须以最终 Runtime gate 为准,不把示例中的确认参数当固定事实。
## 完成与错误
- 创建或更新后保留真实 `openConversationId` 和任务结果;只对查询结果真实返回的字段执行读回验证。
- 写接口成功但现有查询未返回目标设置时,报告真实写入回执和不可独立读回的边界;不用群名、
成员数等其他字段代替验证,也不猜未发布的读回命令。
- 任一自然目标零命中或多候选时,在写入前整体停止。
- 逐项写入保留 succeeded/failed/unknown ledger,不用重试抹掉失败项。
- 分享邀请时 `--target``--receiver` 只能二选一;接收对象不明确时先确认。
- 机器人进群失败时确认机器人身份和当前用户管理权限,不连续切换同义原子命令。
@@ -0,0 +1,121 @@
# group-discovery:群发现、列表与成员读取
> 返回入口:[DingTalk Chat Skill](../../SKILL.md)
用于只读的群列表、群搜索、共同群、群成员、群机器人和邀请链接。建群、改群、成员增删、
邀请卡片分享、公告、禁言和其他群管理写操作读取 [group-admin.md](group-admin.md)。
## 入口选择
| 用户终点 | 唯一推荐入口 |
|---|---|
| 我加入的全部群 | `dws chat +my-groups --page-all` |
| 我创建或管理的群 | `dws chat +chat-list-mine` |
| 只看群主群或管理员群 | `+chat-list-mine --role OWNER|ADMIN` |
| 按关键词搜索群 | `dws chat +chat-search --query <关键词>` |
| 查看指定群全部成员 | `dws chat +chat-members-list --group <群名或ID>` |
| 已知成员 openDingTalkId 批量查群内详情 | `dws chat +chat-members-get --id <cid> --users <ids>` |
| 获取群邀请链接 | `dws chat +chat-invite-url --group <群名或ID>` |
| 查看群机器人 | `dws chat +chat-bots --group <群名或ID>` |
“全部群”与“全部会话”不同:`+my-groups` 只列当前用户加入的群;
`+conversation-list --page-all` 可能同时包含单聊和群聊,不能替代群成员关系。
## 群列表、分页与角色
`+my-groups` 返回当前用户加入的群,包括作为群主、管理员和普通成员加入的群:
- 要求完整列表时使用 `--page-all`Runtime 沿真实 `nextCursor` 读取后续页,并按
`openConversationId` 合并去重,读完后再应用可选 `--type` 本地过滤。
- `--limit` 是每页数量,不是最终结果上限;`--cursor` 只用于从已有 `nextCursor` 手工续读。
- `--page-limit` 只与 `--page-all` 一起使用,用于限制最多读取页数。达到上限后仍有下一页时,
结果不完整。
- 只有 `complete=true``hasMore=false` 才能声称已经读取全部;否则保留 `nextCursor`
`stopReason``failures` 并说明结果不完整。
```bash
dws chat +my-groups --page-all --page-limit 50 --format json
```
`+chat-list-mine` 只返回当前用户作为群主或管理员的群。只要群集合时不传 `--role`
一次取得 OWNER 和 ADMIN。要求逐项标明身份时,直接分别查询 `--role OWNER`
`--role ADMIN`,不先执行无角色查询或读取 Help;按 `openConversationId` 合并去重后,
再应用一次全局数量上限,不得把两个分支直接拼接。
```bash
dws chat +chat-list-mine --limit 20 --format json
dws chat +chat-list-mine --role OWNER --format json
dws chat +chat-list-mine --role ADMIN --exclude-muted --format json
```
`+my-groups` 不提供当前用户角色。用户明确要求普通成员群时,使用
`chat group list-all --limit 200`;返回 `hasMore=true` 时,必须把真实 `nextCursor`
传给下一次调用并继续读取,直到 `hasMore=false`,不得把继续翻页交给用户。读完后按
`openConversationId` 去重,仅筛选真实返回的 `myRole=普通成员`;不得给 `+my-groups`
编造 `--role MEMBER`,也不得用“全部群减去 OWNER/ADMIN 群”推断。
## 群搜索与稳定 ID
群搜索默认使用 `+chat-search`。要求全部候选时加 `--page-all`;可用
`--page-size/--page-token` 或兼容 `--limit/--cursor`。零命中或多候选时停止并展示候选,
不要选择第一项。
```bash
dws chat +chat-search --query "项目冲刺" --page-all --format json
```
只有数字群号时,使用 `chat group get-by-group-id --group-id <数字>` 转换为
`openConversationId`。需要搜索共同群时使用原子 `chat search-common``AND` 表示所有人
都在群里,`OR` 表示任一人在群里。自然人员必须先解析为当前 profile 的真实身份。
```bash
dws chat search-common --nicks "测试用户甲,测试用户乙" --match-mode AND --limit 20 --cursor 0
```
## 群成员
`+chat-members-list` 接受群名或 `openConversationId`,唯一解析后全量读取,并把用户与机器人
分桶。结果必须检查 `buckets/complete/failures`
```bash
dws chat +chat-members-list --group "项目群" --format json
dws chat +chat-members-list --conversation-id <openConversationId> --format json
```
先检查 `+chat-members-list` 的稳定结果。只有结果未包含用户要求的群昵称、角色或其他群内字段时,
才使用其中的真实 `openDingTalkId` 批量调用:
```bash
dws chat +chat-members-get --id <openConversationId> \
--users <openDingTalkId1>,<openDingTalkId2> --format json
```
不要为了群内详情默认切换到企业通讯录;只有用户明确要求部门、岗位、直属主管等企业资料时,
才把真实 userId 交给 `dingtalk-contact`
## 邀请链接与机器人
`+chat-invite-url` 是只读获取链接,可选 `--expires-seconds``group share-invite` 会实际把
邀请卡片发送给另一个会话或用户,属于 [group-admin.md](group-admin.md)。
`+chat-bots` 返回稳定 `bots[]``openBotId`,供后续移除。搜索可用机器人、机器人发送和
撤回读取 [chat-bot.md](chat-bot.md)。
## 原子 fallback
| 原子命令 | 仅用于 |
|---|---|
| `chat search` / `search-common` | Shortcut 未发布的搜索字段或共同群 |
| `chat group get-by-group-id` | 数字群号转换 |
| `chat group members` | 需要原始成员分页响应 |
| `chat group members list-by-ids` | 需要原始批量成员详情 |
| `chat group list-all` / `list-my-groups` | 需要 Shortcut 未投影的真实底层字段 |
使用原子 fallback 前读取精确 leaf Schema;不得把 fallback 写成与 Shortcut 并列的默认路线。
## 完成与错误
- 分页完成只以真实 `complete/hasMore/nextCursor/failures` 判断,不看过滤后的 `count` 猜测。
- 所有稳定 ID 必须来自同一 profile 的真实返回。
- 找不到群或出现多候选时停止,不臆测 `openConversationId`
- 任务从只读发现转为写操作时,使用 [group-admin.md](group-admin.md) 的目标和安全规则。
@@ -0,0 +1,132 @@
# message-actions:消息编辑、撤回、回复与对象操作
> 返回入口:[DingTalk Chat Skill](../../SKILL.md)
用于对真实消息执行编辑、撤回、引用回复、转发、Pin、Top、Favorite 和表情回应写操作,
并包含必要的紧邻验证。需要跨多个阶段传递真实结果的组合流程由
[01-messaging.md](../01-messaging.md) 说明;本文件不重复完整工作流。
## 入口选择
| 用户终点 | 推荐入口 |
|---|---|
| 撤回当前用户消息 | `dws chat +messages-recall --msg-id <openMessageId>` |
| <!-- dws-intent: chat.reply.quote -->引用回复 | `dws chat +messages-reply` |
| 编辑已发送消息 | `dws chat message edit` |
| 单条/合并/话题转发 | `+messages-forward` / `+messages-combine-forward` / `+messages-forward-topic` |
| Pin / Unpin | `+messages-set-pin` / `+messages-unset-pin` |
| 消息 Top / 取消 Top | `+messages-set-top` / `+messages-unset-top` |
| Favorite / 取消 Favorite | `+flag-create` / `+flag-cancel` |
| 默认 emoji 回应 | `+messages-add-emoji` / `+messages-remove-emoji` |
所有写操作以最终 Runtime gate 和精确 leaf Schema 为准。确认对象、消息和影响后再执行;
不要因为文档示例自行制造或省略 confirmation。
## 稳定 ID 规则
- `openTaskId` 是发送任务 ID,不是消息 ID。
- 撤回、编辑、回复、转发、Pin、Top 和 reaction 使用真实查询结果中的 `messageId`
- 同时保留消息的 `conversationId`、thread、发送者和引用上下文。
- 子消息使用自己的 `messageId`;只在缺会话 ID 时继承父消息 `conversationId`
- Bot 撤回使用 `processQueryKey`,不使用本文件的 `openMessageId` 路线。
刚由用户身份发送的消息如果只得到 `openTaskId`,先查询发送状态:
```text
+messages-send 或 message send
→ openTaskId
→ message query-send-status
→ openMessageId + openConversationId
→ 编辑或撤回
```
## 撤回与编辑
`+messages-recall` 可只传 `--msg-id`;省略会话 ID 时 CLI 会通过只读消息详情补齐。
兼容单值 `--message-ids`,但不要把 `processQueryKey` 当消息 ID。
```bash
dws chat +messages-recall --msg-id <openMessageId> --format json
dws chat +messages-recall --conversation-id <openConversationId> --msg-id <openMessageId> --format json
```
编辑使用 `message edit --conversation-id <cid> --msg-id <id>`,并在 `--text``--content`
中二选一。`--text` 由 CLI 生成 Markdown content`--content` 必须是完整 content JSON。
```bash
dws chat message edit --conversation-id <openConversationId> --msg-id <openMessageId> --text "更新后的内容"
dws chat message edit --conversation-id <openConversationId> --msg-id <openMessageId> --content '{"title":"标题","text":"更新后的内容"}'
```
群聊 @所有人使用 `--at-all`;指定人员使用 `--at-open-dingtalk-ids`。正文中的占位符以
Runtime 规范化结果为准,不把裸展示名当稳定身份。
## 引用回复与转发
引用回复默认使用 `+messages-reply``--group` 和消息 ID 来自真实查询;
`--ref-sender` 可省略时让 CLI 只读补齐,不手工猜发送者身份。
```bash
dws chat +messages-reply --group <openConversationId> \
--message-id <openMessageId> --content "收到" --format json
```
| 动作 | 入口 | 关键上下文 |
|---|---|---|
| 单条转发 | `+messages-forward` | 源消息 ID、源会话、目标会话 |
| 合并转发 | `+messages-combine-forward` | 多个真实消息 ID、源/目标会话 |
| 话题转发 | `+messages-forward-topic` | 源消息、源会话、源 thread、目标会话 |
只有 Shortcut 尚未发布真实必需字段时,才评估原子 `message reply``forward`
`combine-forward``forward-topic`,并先读取精确 leaf Schema。不要复制正文伪装原生转发。
## Pin、Top 与 Favorite
| 对象 | 写入入口 | 说明 |
|---|---|---|
| 消息 Pin | `+messages-set-pin` / `+messages-unset-pin` | 作用于一条消息 |
| 消息 Top | `+messages-set-top` / `+messages-unset-top` | 作用于会话内一条消息 |
| Favorite | `+flag-create` / `+flag-cancel` | 当前用户收藏 |
| 会话 Top | `+conversation-set-top` | 作用于整个会话,不属于本文件 |
用户要求确认 Pin 已生效时,使用 `+messages-list-pin` 检查真实结果中的 `messageId`;取消 Pin
后仅在用户要求确认取消结果时再次查询。典型短链为:`+messages-set-pin`
`+messages-list-pin``+messages-unset-pin`
需要原子 fallback 时,消息 Pin 对应 `message set-pin-msg/unset-pin-msg`,消息 Top 对应
`message set-top-msg/unset-top-msg`Favorite 对应 `message add-favorite/remove-favorite`
四种对象不能互换,即使用户都使用“收藏、钉住、置顶”等自然语言。
## 表情回应
优先在 [chat-emoji-list.md](../chat-emoji-list.md) 按表情名称查默认 emoji,不必全文理解表格。
| 场景 | 入口 |
|---|---|
| 添加/移除默认 emoji | `+messages-add-emoji` / `+messages-remove-emoji` |
| 默认表情无合适项时创建文字表情 | `+messages-create-text-emotion` |
| 添加/移除文字表情 | `+messages-add-text-emotion` / `+messages-remove-text-emotion` |
| 替换文字表情 | `message update-text-emotion` |
reaction 查询属于 [message-query.md](message-query.md),不要为了查看回应执行写命令。
## 流式卡片与文本工具
流式卡片使用根 Skill 直接链接的 [card/create.md](../card/create.md)、
[card/update.md](../card/update.md) 和 [card/schema.md](../card/schema.md);本文件不复制卡片参数。
纯文本翻译使用:
```bash
dws chat text translate --query "你好世界" --to en_US
```
用户只要求翻译文本时不要误走消息发送。
## 完成与错误
- 写操作检查任务级结果、投递状态和失败项,不只看退出码。
- 投递状态 unknown 时保留幂等键,不自动换目标重发。
- `unknown flag` 时读取精确 leaf Help,最多修正一次。
- 目标消息不存在、会话不匹配或发送者上下文缺失时停止,不猜 ID。
- 已知稳定消息 ID 的单一动作及其紧邻验证均在本文件完成。
@@ -0,0 +1,76 @@
# message-media:特殊消息与资源下载
> 返回入口:[DingTalk Chat Skill](../../SKILL.md)
只用于位置、联系人名片、底层 mediaId/fileId 和消息资源下载。普通文本、Markdown、文件、
图片、音频和视频发送继续按根 Skill 使用 `+dm``+send-to-group``+messages-send --file`
不读取本文件。
## 默认边界
- 用户身份普通文件/音视频:`dws chat +messages-send --as user --file <相对路径>`
- 已知资源引用单独下载:`dws chat +messages-resource-download`
- 从消息中定位并下载资源:在定位消息的 `+chat-messages``+search-msg`
`+messages-mget` 同一次调用中加 `--download-resources`
- 只有 Shortcut 尚未发布的位置、联系人名片或真实底层媒体字段,才使用原子 fallback。
<!-- dws-intent: chat.send.advanced -->`dws chat +messages-send` 的 user 文件能力不能外推给 Bot/Webhook
机器人富媒体边界读取 [chat-bot.md](chat-bot.md),不得静默改成当前用户身份。
## 位置与联系人名片
位置消息必须确认纬度、经度、地址名称和地图缩略图 mediaId:
```bash
dws chat message send --conversation-id <openConversationId> --msg-type location \
--latitude <纬度> --longitude <经度> --location-name <地址名称> \
--map-thumbnail-url "@mediaId"
```
联系人名片的 `--contact-id` 必须是联系人 `openDingTalkId`,不能把 userId 直接代入:
```bash
dws chat message send --conversation-id <openConversationId> \
--msg-type profile --contact-id <openDingTalkId>
```
用户要求真实发送结果时,保留发送返回的 `openTaskId`,再执行:
```bash
dws chat message query-send-status --open-task-id <openTaskId> --format json
```
检查真实 `sendStatus``openMessageId``openConversationId`
原子 `message send` 只在 Shortcut 缺少真实必需字段时使用。群聊目标用 `--conversation-id`;单聊目标
`--user``--open-dingtalk-id`,三者通常互斥。发送前核对接收对象、消息类型和资源来源。
## 资源下载
公开 `+messages-resource-download` 使用工作目录内安全相对路径,默认不覆盖;完整文件先写入
临时落盘再原子发布。覆盖必须由用户显式传 `--overwrite`,读取和下载不需要 `--yes`
任务要求从某条消息中定位并下载资源时,优先在限定会话、消息或时间范围的查询中加
`--download-resources --output-dir <目录>`,并检查下载 ledger。`+messages-resource-download`
只用于已经持有完整、真实且属于当前组织/profile 的独立资源引用、无需再定位消息的场景。
`fileId` 返回 `RESOURCE_NOT_FOUND`,不得把同一个 ID 改称 `mediaId` 重试,也不得原样
重复调用;应回到消息查询并使用 `--download-resources`
底层 fallback
```bash
dws chat message download-media --type mediaId --resource-id <mediaId> \
--message-id <openMessageId> --open-conversation-id <openConversationId> \
--output ./downloads/
```
`resource-id``message-id` 和会话 ID 必须来自同一 profile 下的真实消息查询结果。
当前没有 Range/断点续传;失败时保留 ledger 或错误,显式重试整个文件,不拼接残片。
## 完成与错误
- 查询并下载时同时检查消息完整性和每项下载 ledger;单项失败不抹掉已取得消息。
- 文件/音视频发送失败先确认工作目录内相对路径可读,不恢复独立上传再提取 mediaId 的旧默认链路。
- 位置参数不完整时先向用户确认,不猜经纬度或缩略图。
- 名片发送失败时确认 `--contact-id` 是 openDingTalkId。
- 下载目标存在时默认停止;只有用户明确允许覆盖时才传 `--overwrite`
@@ -0,0 +1,135 @@
# message-query:消息读取、搜索与查询
> 返回入口:[DingTalk Chat Skill](../../SKILL.md)
用于浏览或导出指定会话、按条件搜索消息、按消息 ID 读取详情、查看 @我、话题回复、
Favorite、Pin 和 reaction。只读任务优先使用 Shortcut;只有 Shortcut 未发布所需底层字段、
原始响应或手工 continuation 时才读取精确原子 leaf Schema。
## 入口选择
| 用户终点 | 唯一推荐入口 |
|---|---|
| <!-- dws-intent: chat.read.conversation -->浏览或导出一个指定群聊/单聊 | `dws chat +chat-messages` |
| <!-- dws-intent: chat.search.filtered -->发送者、关键词、@对象或消息类型是主要条件 | `dws chat +search-msg` |
| 已知消息 IDs 读取详情 | `dws chat +messages-mget` |
| 查看 @我的消息 | `dws chat +at-me` |
| 查看 Favorite | `dws chat +flag-list` |
| 已知话题主消息或 thread/topic ID 读取回复 | `dws chat +thread-replies` |
`+chat-messages` 是指定会话的粗粒度读取;`+search-msg` 是目标条件明确的单/跨会话检索。
不要先读完整会话再补跑搜索,也不要把群名或姓名直接填入只接受稳定 ID 的参数。
## 指定会话读取
群聊 `--group` 可传群名或 `openConversationId`;也可用 `--chat-query` 显式解析群名、
`--conversation-id` 显式传稳定 ID。单聊使用 `--user``--open-dingtalk-id`
```bash
dws chat +chat-messages --group <群名或openConversationId> --format json
dws chat +chat-messages --group <openConversationId> --page-all --page-limit 50 --format json
```
可附带非必填的 `--sender-query <姓名>`:未传时返回全部消息;唯一解析出
userId/openDingTalkId 后,按消息 `senderId` 筛选同一次读取结果,覆盖最终
`messages/count` 并返回 `resolvedFilters`。解析失败、不完整或存在歧义时抑制未过滤消息,
返回 `sender_resolution_failed`,不得把全量消息当作发送者筛选结果。
`--sender` 是姓名、userId 或 openDingTalkId 的混合入口。通讯录无法确认输入类型时,可按
原值 userId 精确过滤并交付命中消息,但结果必须保留 `identity_unverified`,不得把字符串命中
升级为已验证身份,也不得据此作完整否定结论。
```bash
dws chat +chat-messages --group "项目群" --sender-query "测试用户甲" --page-all --format json
```
时间范围使用公开可选的 `--start``--end``--order asc|desc`,兼容别名为
`--start-time/--end-time/--sort`。范围为 `[start,end)`;仅开始时间表示到本次执行当前时间;
仅结束时间只支持 `desc``asc` 必须提供开始时间。旧 `--time/--direction` 只用于兼容的
单边界模式,不能与范围模式混用。
```bash
dws chat +chat-messages --group <openConversationId> \
--start "2026-08-01T00:00:00+08:00" --end "2026-08-02T00:00:00+08:00" \
--order asc --page-all --format json
```
完整读取后只需消息字段可判断的子集时,在同一次调用中使用全局 `--jq`,保留根信封并
同步改写 `messages/count`;不得丢失 `complete``hasMore``failures` 等 ledger。
发送者姓名仍使用 `--sender-query` 解析稳定身份,不用 `--jq` 比较展示名。
```bash
dws chat +chat-messages --group "项目群" --page-all --format json \
--jq '. as $root | [.messages[] | select((.reactions // []) | length > 0)] as $matched | $root | .messages = $matched | .count = ($matched | length)'
```
要求导出时用 `--output <工作目录内相对.json>` 原子写入;需要资源时在读取命令上加
`--download-resources`,不要让 Agent 先输出全量 JSON 再手工遍历资源引用。
## 多维度搜索
- 关键词使用公开 `--query`
- 已知稳定会话 ID 使用 `--group` / `--groups`;稳定发送者 ID 使用 `--senders`
- 只有群名时使用 `--chat-query`,由 CLI 唯一解析会话。
- 只有发送者姓名时使用 `--sender-query`,由 CLI 唯一解析人员。
- 不传会话过滤时搜索全部会话;默认时间范围为最近 7 天。
- `--page-all` 只翻完当前时间范围内的游标页;精确范围使用成对的 `--start/--end`
- `--order` 只稳定排列已经取得的结果;未全量或 `complete=false` 时不得称为完整范围全局排序。
```bash
dws chat +search-msg --chat-query "项目群" --sender-query "测试用户甲" --page-all --format json
dws chat +search-msg --chat-query "项目群" --query "发布计划" --page-all --format json
dws chat +search-msg --sender-query "测试用户甲" --page-all --format json
```
需要 Shortcut 未发布的原始过滤字段或响应时,才评估 `message search-advanced`。它支持
发送者、@对象、多个会话、消息类型、会话类型、机器人消息和时间范围,但不是默认入口。
至少提供一种真实过滤条件,完整遍历只有 `--page-all` 会触发。
## 其他查询
### 已知消息、@我与话题回复
- `+messages-mget --msg-ids <id...>`:最多 50 条;结果可直接用于回复、转发、撤回和资源下载。
- `+at-me [--group <群名或ID>] --page-all`:群内或跨全部会话查看 @我的消息
- `+thread-replies --message-id <rootMessageId>`:自动只读解析 conversation/thread。
- `+thread-replies --group <cid> --thread-id <threadId>`:显式稳定上下文。
话题回复默认 `desc``asc` 必须与 `--page-all` 一起使用。自动续页使用下层毫秒级
`nextCursor`,不得使用只有秒精度的展示时间手工拼 continuation。检查 `complete`
`hasMore``stopReason``failures`
### Favorite、Pin 与 reaction 查询
| 任务 | 入口 |
|---|---|
| Favorite 列表 | `+flag-list`;要求全部时加 `--page-all`,页大小 130 |
| 消息 Pin 列表 | `message list-pin-msg --open-conversation-id <cid>` |
| 批量 reaction/文字回应 | `message list-emotion-replies --msg-ids <id...>` |
| 已读/未读状态 | `message read-status --group <cid> --message-id <id>` |
Favorite、消息 Pin、消息 Top 和会话 Top 是不同对象。写入或取消这些状态读取
[message-actions.md](message-actions.md),这里只负责查询。
## 原子 fallback
| 原子命令 | 仅用于 |
|---|---|
| `message list` | 指定会话原始响应或显式手工 continuation |
| `message list-all` | 时间范围内全部会话的原始分页响应 |
| `message list-by-sender` | 已有稳定发送者 ID 且需要底层原始响应 |
| `message list-mentions` / `list-focused` | @我或特别关注的原始列表 |
| `message search` / `search-advanced` | Shortcut 未发布的真实过滤字段 |
| `message list-by-ids` | 已知消息 ID 的原始详情响应 |
Typed `chat message` 自动翻页只由 `--page-all` 触发;只传 `--page-limit``--max-items`
`--page-delay` 仍是单页。非第一页失败时保留 partial 结果、失败页和 continuation,不能
把 partial result 表述成完整成功。
## 完成与错误
- 查询必须检查 `complete``hasMore``stopReason``failures` 和下载 ledger。
- 发送者/群名零命中或多候选时停止,不选择第一项。
- `unknown flag` 时读取精确 leaf Help,修正后最多重试一次。
- 子消息优先使用自己的 `messageId`;只在缺会话 ID 时继承父消息的 `conversationId`
- 查到真实消息后需要写操作时,使用 [message-actions.md](message-actions.md) 中的稳定 ID 规则。
@@ -0,0 +1,43 @@
# 话题与话题圈
> 返回入口:[DingTalk Chat Skill](../../SKILL.md)
话题(Thread)是一条主消息及其回复线,可以位于普通群或话题圈中,使用 `openConvThreadId` 标识;承载话题的父群使用 `openConversationId`。命令沿用拆分前 `chat group` / `chat message` 的参数名称。只有需要新建话题圈时才使用 `create-group`,已有群中的话题可直接使用其余 `chat thread` 命令。
## 入口选择
| 用户终点 | 推荐入口 |
|---|---|
| 已有稳定成员 ID 创建话题圈 | `dws chat thread create-group --name <名称> --users <userId,...>` |
| 发布新话题 | `dws chat thread send --conversation-id <openConversationId>` |
| 浏览话题主消息 | `dws chat thread list --conversation-id <openConversationId>` |
| 向具体话题直接追加回复 | `dws chat thread reply --conversation-id <openConvThreadId>` |
| 分页读取一个话题的回复 | `dws chat thread list-replies --conversation-id <openConversationId> --topic-id <openConvThreadId>` |
| 转发整条话题 | `dws chat thread forward --src-msg-id <openMessageId> --src-conversation-id <openConversationId> --src-thread-id <openConvThreadId> --dest-conversation-id <openConversationId>` |
| 撤回话题中的一条消息 | `dws chat thread recall-message --conversation-id <openConversationId> --message-id <openMessageId>` |
| 添加或移除 emoji | `dws chat thread add-emoji` / `remove-emoji` |
| 查询 Thread 消息的表情回复 | `dws chat thread list-emotion-replies --msg-ids <openMessageId,...>` |
| 添加、移除或更新文字表情 | `dws chat thread add-text-emotion` / `remove-text-emotion` / `update-text-emotion` |
## 发布、回复与读取
`thread send``--conversation-id` 是承载话题的会话 `openConversationId`,用于发布新的顶层话题。
`thread reply` 沿用原发送命令的 `--conversation-id`,但这里传 Thread 子会话的 `openConvThreadId`。它直接追加回复,不使用消息引用回复,也不创建新的顶层 Thread。
已有父会话 `openConversationId` 和 Thread `openConvThreadId` 且只需读取一页时,使用 `thread list-replies --conversation-id ... --topic-id ...`。需要按主消息自动解析、全量翻页、排序或下载资源时,使用 `+thread-replies` Shortcut。
用户需要逐条查看、列出或概括具体回复内容时,使用 `thread list-replies`;只浏览话题主消息时使用 `thread list`。需要自动读取全部页面、排序或下载资源时,使用 `+thread-replies` Shortcut。
整条 Thread 可转发到普通群;当前不支持从话题圈向另一个话题圈转发整条 Thread。
## 消息操作
撤回、emoji 和文字表情命令沿用对应 `chat message` 命令的主参数。Runtime 会先读取消息并校验其属于 Thread,再执行操作;批量查询会逐条校验 `--msg-ids`。文字表情的 `emotionId``backgroundId`、名称和文字使用 `chat message create-text-emotion` 返回的实际值;移除时使用已添加的值,更新时用 `--old-emotion-id` 传当前值、其余表情参数传新值。
## 完成与错误
- 创建话题圈返回真实群会话结果,不额外制造 `openTopicId` 字段。
- 发布和回复沿用异步发送结果;`openTaskId` 是任务 ID,后续消息操作需要从发送状态或消息查询中取得真实消息 ID。
- Thread 主消息和回复均保留 `openConvThreadId`,父容器继续使用 `openConversationId`
- 标识缺失、类型不明或消息不属于 Thread 时停止,不猜 ID。
@@ -0,0 +1,70 @@
# IM 稳定契约与能力边界
本页只记录需要跨命令复用的 Runtime 契约。下面四个 marker 区块由
`check-multi-im-skill-chain.sh` 对照 Go typed descriptor 逐字校验;修改能力时先改 Runtime、
测试与 descriptor,再同步本页。命令参数仍以精确 leaf Schema 为准。
## 消息结果 `im.message-list.v1`
`+chat-messages``+search-msg``+messages-mget` 及相关消息读取使用同一兼容版本。
字段可能为空,但不能换名猜测;全量任务必须结合完整性 ledger 判断。
<!-- DWS_MESSAGE_RESULT_CONTRACT_START -->
- `version`: `im.message-list.v1`
- `message_fields`: `messageId`, `conversationId`, `threadId`, `sender`, `senderId`, `senderType`, `messageType`, `text`, `createTime`, `updateTime`, `reactions`, `quotedMessage`, `forwarded`, `resourceRefs`
- `envelope_fields`: `contractVersion`, `messages`, `count`, `resolvedFilters`, `queryRange`, `pagesFetched`, `paginationKnown`, `complete`, `hasMore`, `nextPage`, `stopReason`, `truncated`, `truncatedByPageLimit`, `truncatedByResultLimit`, `failedCount`, `failures`, `partial`, `scope`, `resourceDownloads`
<!-- DWS_MESSAGE_RESULT_CONTRACT_END -->
`complete=false` 时不能称为全量成功。`nextPage` 只能来自真实 lower boundary
`failedCount/failures``partial`、总 `truncated` 和两个原因字段必须原样保留。
当 Runtime 解析并应用自然发送者条件时,`resolvedFilters.senders[]` 保留原查询及选中的
`userId/openDingTalkId`。消息展示名可以与通讯录姓名不同;只能用稳定 `senderId` 与解析结果关联,
不得重新做姓名字符串比较。
当查询声明时间范围或顺序时,`queryRange` 保留规范化的 `startTime``endTime``order`
`semantics=[start,end)`。排序只覆盖本次实际取得的 `messages``complete=false` 时不得把它描述为完整范围的全局排序。
## `+messages-send` 身份矩阵
<!-- DWS_IDENTITY_CAPABILITY_CONTRACT_START -->
| identity | targets | content types | natural targets | mention targets | idempotency keys | batch ledger |
|---|---|---|---|---|---:|---:|
| `user` | `group`<br>`direct-user`<br>`direct-open-dingtalk-id` | `text`<br>`markdown`<br>`image-media-id`<br>`file`<br>`audio-as-file`<br>`video-as-file` | `chat-query`<br>`user-query` | `open-dingtalk-id`<br>`all` | `true` | `false` |
| `bot` | `group`<br>`groups`<br>`direct-users`<br>`direct-open-dingtalk-ids` | `text`<br>`markdown` | — | `user-id`<br>`open-dingtalk-id`<br>`all` | `false` | `true` |
| `webhook` | `token-owned-group` | `text`<br>`markdown` | — | `user-id`<br>`mobile`<br>`all` | `false` | `false` |
<!-- DWS_IDENTITY_CAPABILITY_CONTRACT_END -->
Bot 多群用 `--groups``--groups-file`Runtime 去重后输出
`im.batch-write.v1` 逐目标 ledger。Bot/Webhook 不支持的内容类型会在写前失败,不能降级为
另一身份或偷偷改成纯文本。
## 流式卡片
<!-- DWS_CARD_WORKFLOW_CONTRACT_START -->
- `version`: `im.streaming-card.v1`
- `targets`: `group`, `direct-user`, `direct-open-dingtalk-id`
- `content_types`: `streaming-text`
- `flow_statuses`: `1=processing`, `2=typing`, `3=completed`, `4=executing`, `5=error`
- `callback_supported`: `false`
<!-- DWS_CARD_WORKFLOW_CONTRACT_END -->
发送目标与状态范围由 Runtime 校验。当前不是 Lark Card JSON 编译器,也不消费按钮 callback;
具体创建和更新流程见 `card/` 下的精确 reference。
## 正向能力与负向边界
<!-- DWS_CAPABILITY_BOUNDARY_CONTRACT_START -->
| capability | supported | current route / boundary |
|---|---:|---|
| `thread-write` | `false` | quote reply with +messages-reply; thread reading with +thread-replies |
| `bot-rich-media` | `false` | bot text/markdown, or current-user file/image send |
| `card-action-callback` | `false` | streaming text card create/update only |
| `resource-resume` | `false` | atomic whole-file download with explicit retry |
| `group-member-full-pagination` | `true` | +chat-members-list or +group-members |
| `group-owner-selection` | `true` | +chat-create owner flags |
<!-- DWS_CAPABILITY_BOUNDARY_CONTRACT_END -->
`supported=false` 是执行门禁,不是待猜测字段。只有 lower interface、Runtime、测试、Schema 和
此页同时升级后,才能改变对外承诺。
话题圈会话仍禁止引用消息回复;向 Thread 追加回复使用 `chat thread reply --conversation-id <openConvThreadId>`
@@ -0,0 +1,56 @@
# Chat 低频意图消歧
只在根 Skill 的 Golden Route 无法区分相邻能力时读取。命令 flags 和安全语义以精确 leaf Schema/Runtime 为准。
## 消息选择
| 用户终点 | 选择 | 不要混用 |
|---|---|---|
| 给姓名发简单文本 | `+dm` | 先查人再原子发送 |
| 给群名发简单文本 | `+send-to-group` | 先搜群再原子发送 |
| 文件、Bot、Webhook、复杂 @、幂等 | `+messages-send` | 为不同身份各走一套原子入口 |
| 读取或导出指定会话,可附带发送者姓名 | `+chat-messages`;姓名用非必填 `--sender-query` | 解析成功后读后筛选;解析失败抑制未过滤消息并返回错误;不补跑搜索 |
| 直接按发送者、关键词、@对象或消息类型搜索 | `+search-msg` | 条件检索优先,可限定单个或跨多个会话 |
| 已知消息 IDs 取详情 | `+messages-mget` | 重新搜索关键词 |
| @我的消息 | `+at-me` | 全量消息后本地猜测 @ |
| 已知 thread/topic ID 的回复 | `+thread-replies` | 普通消息列表 |
| 引用回复一条消息 | `+messages-reply` | 普通发送 |
| 单条/合并/话题转发 | `+messages-forward` / `+messages-combine-forward` / `+messages-forward-topic` | 复制正文重新发送 |
| 流式卡片创建或更新 | `+messages-send-card` / `+messages-update-card` | 普通 text/Markdown 发送 |
## Topic 选择
话题与话题圈的创建、发布、浏览、回复、互动和整条转发统一读取 [thread.md](chat/thread.md),不从普通消息入口选路。
## 对象层级
| 用户终点 | 对象 | Reference |
|---|---|---|
| 收藏或取消收藏 | 当前用户的 Favorite | [message-actions.md](chat/message-actions.md) |
| Pin/Unpin 一条消息 | 消息 Pin | [message-actions.md](chat/message-actions.md) |
| 置顶/取消置顶一条消息 | 消息 Top | [message-actions.md](chat/message-actions.md) |
| 置顶/取消置顶整个会话 | 会话 Top | [chat-conversation.md](chat/chat-conversation.md) |
| 查看置顶会话 | 会话列表 | `+conversation-list-top` |
| 标记消息已读 | 消息读取状态 | [message-actions.md](chat/message-actions.md) |
| 清红点、标记会话未读 | 会话状态 | [chat-conversation.md](chat/chat-conversation.md) |
Favorite、消息 Pin、消息 Top 和会话 Top 不能互换,即使用户都说“收藏/钉住/置顶”。
## 群与机器人
| 用户终点 | 选择 |
|---|---|
| 已有成员 IDs 创建群 | `+chat-create` |
| 查群、查看成员、邀请链接 | [group-discovery.md](chat/group-discovery.md) |
| 加人、踢人、管理员、群公告、群设置 | [group-admin.md](chat/group-admin.md) |
| 找可用机器人并取得单聊 ID | `chat bot find`,不是只查自己创建机器人的 `bot search` |
| 已知 robotCode 发送 | `+messages-send --as bot` |
| 机器人入群、移除、批量群发或撤回 | [chat-bot.md](chat/chat-bot.md) |
## 跨产品边界
- 紧急 DING、短信或电话:切 `dingtalk-misc`,不要当普通 Chat 消息。
- 邮件:切 `dingtalk-mail`
- 只查询人员资料或把姓名解析成 ID:切 `dingtalk-contact`;若终点只是简单发消息,直接留在 Chat 使用 `+dm`
- 企业知识跨文档、消息、邮件搜索:切 `dingtalk-aisearch`;明确只搜聊天消息时使用 `+search-msg`
- 文档翻译先由文档产品读取正文;`chat text translate` 只处理纯文本。
+122
View File
@@ -0,0 +1,122 @@
---
name: dingtalk-contact
description: 钉钉通讯录精确查询。Use when 已有 userId 后查详情、部门、职位或邮箱,按完整手机号反查用户,或查询自己、部门成员及角色。姓名模糊搜索、工号、职责、上下级走 dingtalk-aisearch,拿到 userId 后用本 skill 补详情。命令前缀:dws contact。
metadata:
cli_version: ">=0.2.14"
category: product
requires:
bins:
- dws
---
# 钉钉通讯录 Skill
## 前置条件 — 执行操作前必读
> **CRITICAL — 执行任何 `dws` 操作前,MUST 先用 Read 工具完整读取 [`dingtalk-shared`](../dingtalk-shared/SKILL.md)。**该轻量文件包含全局执行契约、安全底线及 shared references 的按需加载导航;不要预加载其全部 references。
> 命令参考:[contact.md](references/contact.md);剧本:[08-directory.md](references/08-directory.md)。
<!-- VISIBLE_SHORTCUTS_START -->
## Shortcuts(无专用脚本/recipe 时优先)
以下 shortcut 同时进入公开 catalog 与 Runtime Schema。先按本 skill 的意图表、脚本和 recipe 路由:存在精确覆盖该场景的专用脚本/recipe 时按其执行;否则用户意图命中时,shortcut 优先于手写原子命令。命令已选中时直接执行;只在参数或安全语义不确定时读取 Agent leaf Schema(例如 `dws schema --cli-path "contact +<shortcut>" --compact --format json`),在当前 Cobra flags 不确定时读取 `dws contact <shortcut> --help`。只有参数映射、接口绑定或 provenance 审计才省略 `--compact`。仅当现有路由和 reference 都无法定位低频能力时,才用 `dws shortcut list --service contact --format json` 批量发现。
| Shortcut | 风险 | 适用场景 |
|---|---|---|
| `dws contact +by-mobile` | read | 按手机号查询某人的完整资料(自动解析 userId 后取详情) |
| `dws contact +dept-members` | read | 按部门名列出部门成员(自动解析 deptId) |
| `dws contact +list-dept-members` | read | 查看部门成员(仅本部门,不含下级) |
| `dws contact +list-followings` | read | 获取当前用户的特别关注列表 |
| `dws contact +list-role-members` | read | 查询角色下的成员列表 |
| `dws contact +list-sub-depts` | read | 查看指定部门的子部门 |
| `dws contact +lookup` | read | 按姓名查询某人的完整资料(自动解析 userId 后取详情) |
| `dws contact +me` | read | 查看我自己的通讯录资料(姓名/userId/手机/部门/组织,干净投影) |
| `dws contact +org` | read | 按姓名查某人所在部门的详情(自动解析 userId 与 deptId |
| `dws contact +resolve-dept` | read | 按名称搜索部门并解析出唯一 deptId(只读) |
| `dws contact +search-mobile` | read | 按手机号搜索通讯录用户 |
| `dws contact +search-user` | read | 按关键词搜索通讯录用户 |
| `dws contact +team` | read | 按姓名列出某人所在部门的成员(自动解析 userId 与 deptId |
<!-- VISIBLE_SHORTCUTS_END -->
## 意图表
| 用户说 | 命令 |
|--------|------|
| "查我自己的信息" | `dws contact user get-self` |
| "按 userId 查详情" | `dws contact user get --ids <userId1>,<userId2>,...`(多个并行) |
| "完整手机号反查用户" | `dws contact user search-mobile --mobile <手机号>` |
| "按部门名拉成员" | `python scripts/contact_dept_members.py --query "<部门名>"` |
| "搜部门" | `dws contact dept search --query "<关键词>"` |
| "部门成员列表" | `dws contact dept list-members --ids <deptId>` |
| "列出企业角色 / 有哪些角色" | `dws contact label list` |
| "按角色名查角色ID" | `dws contact label get --names "<角色名>"` |
| "查某角色下有哪些成员" | `dws contact label list-members --id <labelId>` |
## 标准 SOP(必遵流程)
> 命中以下意图**必须**按对应 SOP 顺序执行;**禁止**跳步、替换命令、编造 userId。每条命令必须带 `--format json`。姓名模糊搜索、工号、职责与上下级走 `dingtalk-aisearch`;完整手机号精确反查走 contact;拿到 userId 后由 contact 补详情。
### SOP-1 搜人(search-person
**触发**:按姓名/工号/部门/职责/上下级找人,或用手机号线索做语义搜索。
1. **切 aisearch(必须)**`dws aisearch person --query "<关键词>" --dimension <维度> --format json`(姓名→`name`、工号→`jobNumber`、手机号语义线索→`phone`、负责人→`duty`、部门→`department`、上下级→`supervisor`/`subordinate`)。
2. **解析(必须)**:从结果取 `userId``title`;**多人同名禁止默认选第一个**,必须批量 `dws contact user get --ids <id1,id2,...> --format json` 拿部门/职位后让用户确认。
3. **补详情(必须)**:要完整部门/职位/邮箱/主管时 `dws contact user get --ids <userId> --format json`
**禁止**:用 `contact user search` 做姓名或工号搜索、默认取首个候选、编造人员字段。完整手机号精确反查是 `search-mobile` 的唯一搜索例外。
### SOP-1A 完整手机号精确反查(search-person-by-mobile
**触发**:用户提供完整手机号并要求确认是谁或取得 userId。
1. **执行(必须)**`dws contact user search-mobile --mobile "<完整手机号>" --format json`
2. **补详情(按需)**:从结果取 `userId`,需要部门、职位或邮箱时继续 `dws contact user get --ids <userId> --format json`
**禁止**:把完整手机号精确反查改走姓名搜索,或在未返回 userId 时猜测人员。
### SOP-2 精确查人/补详情(search-user
**触发**:已有 userId 要查完整详情,或要拿 userId 给下游(发消息/建待办/约日程)。
1. **拿 userId(必须)**`dws aisearch person --query "<姓名>" --dimension name --format json``userId`;多命中必须列候选请用户确认。
2. **查详情(必须)**`dws contact user get --ids <userId> --format json`,按返回字段(`orgEmployeeModel` 下部门/职位/邮箱)答复。
**禁止**:用模糊关键词直接调 `contact user search` 凑数、编造未返回字段。
### SOP-3 查自己(get-contact-self
**触发**:我的信息/我的 userId/我的部门。
1. **执行(必须)**`dws contact user get-self --format json`,取 `orgEmployeeModel.userId` / `orgUserName` / `depts[].deptName` / 主管等。
**禁止**:把自己 userId 写死或猜测。
### SOP-4 查部门 / 角色(dept-and-relation
**触发**:部门列表/部门成员/角色/角色成员。
1. **执行(必须)**:搜部门 `dws contact dept search --query "<部门名>" --format json`;某部门下子部门 `dws contact dept list-children --dept <父部门ID> --format json`;部门成员 `dws contact dept list-members --ids <部门ID>[,<部门ID2>...] --format json`;部门详情 `dws contact dept get-info --dept <部门ID> --format json`。角色:`dws contact label list` / `dws contact label get --names "<角色名>"` / `dws contact label list-members --id <labelId>`。搜索企业根部门时服务端可能返回 `deptId=-1` 哨兵,后续 `list-children` / `list-members` / `get-info` 必须规范化为真实根部门 `deptId=1`
2. **补详情(必须)**:拿到 userId 后用 `contact user get --ids` 补部门/职位;上下级关系优先经 `dingtalk-aisearch``supervisor`/`subordinate` 维度。
**禁止**:使用不存在的 `contact dept list`(已废弃/歧义)、编造 deptId/labelId、跳过 aisearch 维度直接猜上下级。
## 高频硬约束
- 通讯录问题必须调用 `dws contact``dws aisearch` 获取实时结果;严禁只读 `USER.md`、环境身份或静态上下文后直接回答。
- 查自己用 `dws contact user get-self --format json`,不要把 `me/self/current` 当作 `userId` 传给 `user get`
- 姓名模糊搜索、工号反查、职责或上下级搜索走 `dws aisearch person`;完整手机号精确反查走 `dws contact user search-mobile --mobile "<手机号>" --format json`。拿到 `userId` 后按需 `dws contact user get --ids <userId> --format json` 补部门/职位/邮箱。
- 查询直属主管/上下级时,如果 `contact user get` 没返回明确主管字段,必须继续 `dws aisearch person --query "<完整姓名或工号>" --dimension supervisor --format json`,不要停在"可能需要进一步查询"。
- 多个同名候选时,批量 `contact user get --ids id1,id2,... --format json` 获取部门/职位后再消歧;不要默认取第一个。
- 用户查询企业角色、角色ID、角色成员,或“管理员/财务/HR/主管”等角色类型人员时,走 `contact label list/get/list-members`;不要用 `dept list-members` 筛字段替代。
## 跨产品协作
- 姓名模糊搜索、上下级、谁负责、工号反查、手机号语义搜索 → `dingtalk-aisearch`
- 完整手机号精确反查 → `dws contact user search-mobile`
- 拿到 email 发邮件 → 切到 `dingtalk-mail`
- 拿到 userId 发消息 → 切到 `dingtalk-chat`
## 局部意图与短流程
- [局部意图消歧](references/intent-guide.md)[短流程](references/lite-recipes.md)。
@@ -0,0 +1,51 @@
# 通讯录(组织架构)
> **SKILL.md** 中 #8 内联 3 条 **lite**`get-contact-self`、`search-person`、`search-user`。下列 recipe、专用规则与消歧请在命中 #8 且**超出**上述 lite 时阅读本文。
> 产品命令见 [contact.md](./contact.md)。通用批量/并行见 [recipes/conventions.md](recipes/conventions.md)。
## 专用规则(#8 非 lite 步骤必守)
- **角色类查人优先 label**:用户说"角色为XX的员工/XX角色的员工/XX角色的人员""所有主管/主管理员/财务/HR/总经理"等角色类型人员时,**优先** `contact label list` 获取全部角色 → 匹配目标角色 → `contact label list-members --id <labelId>`;若用户明确指定了角色名称(如"角色为总经理"),则先用 `contact label get --names <XX>` 精确匹配,**若精确匹配无结果,降级 `label list` 模糊匹配**(如用户说"管理员"可匹配到"主管理员"和"子管理员")。
- **脚本优先**:按部门拉成员**优先** `python scripts/contact_dept_members.py --query "<部门名>"``--dry-run` / `--format json`);失败再 `dept search``dept list-members --ids`
- **详情链路**:用户要子部门、职位、联系方式、汇报关系等,在 `aisearch person` 找到 `userId` 后**必须**再 `contact user get --ids <userId>`;禁止仅用搜索结果的浅表字段交差。
- **`user get` 后部门仍空**:不得过早结束或只建议用户去 App;须在 CLI 能力内尝试 **用户点名的部门** `dept search` + `dept list-members` 等与 `userId` 交叉核对,再结构化汇总「返回中有哪些字段 / 哪些为空及可能原因」。
- **多命中**`aisearch person``dept search` 返回多条时须列候选(姓名、title、部门线索)请用户确认,禁止默认猜一人。具体消歧流程:
1. 从搜索结果中提取所有同名/多命中用户的 `userId`
2. 调用 `contact user get --ids userId1,userId2,...` 获取每人详情(含 `depts` 部门列表、职位等)
3. 将「姓名 + 部门 + 职位」列表展示给用户,请用户确认选择哪一位
4. 使用用户确认的 `userId` 继续后续操作
> **根因**`aisearch person` 不返回完整部门信息,无法仅凭搜索结果区分同名用户。**必须**追加 `contact user get` 获取部门信息才能消歧。
- **批量**:多个 `userId``contact user get --ids id1,id2,...`;多部门成员列表按需并行,遵守单次批量上限与 [recipes/conventions.md](recipes/conventions.md)。
- **子部门枚举**:已知父部门 `deptId` 时**优先** `contact dept list-children --dept <父deptId>` 直接拿到完整子部门列表;只知道部门名时先 `contact dept search --query "<父部门名>"``deptId``list-children`;用户明示的子部门名(无需枚举)可直接 `dept search` 命中。多子部门展开见 `explore-subdepts-and-members`
## 与其他场景消歧
- **按角色/职位类型查人(主管/管理员/财务等)** → 优先 `contact label list` + `label list-members`;label 精确命中角色维度,返回完整名单;aisearch 是语义模糊搜索不保证完整性。
- **搜人/找人/找同事/查工号/手机号语义搜索** → **`aisearch person`**(支持姓名/部门/职责/上下级/手机号线索/工号维度),见 `dingtalk-aisearch`
- **完整手机号精确反查** → `contact user search-mobile --mobile "<手机号>"`
- **已有 userId 后查详情 / 查自己 / 查部门与角色** → `contact`(精确查询)。
- **纯查部门与子部门成员 / 验证归属 / 组织关系** → `contact`
- **终点是发消息、待办、日程** → 先用 `search-person``search-user``userId`,再进入 #1 / #2 / #3
- **联系客户 + 发邮件** → 先用 `contact``orgAuthEmail`,再走 `dingtalk-mail`
## Recipe 速查(本表步骤,非 SKILL lite)
| Recipe | 步骤 |
|--------|------|
| `lookup-label-members` | 1. `contact label list` → 浏览全部角色,匹配目标角色的 labelId<br>2. `contact label list-members --id <labelId>` → 该角色下的成员列表 |
| `search-user-by-mobile` | 1. `contact user search-mobile --mobile "<完整手机号>"` → 按需 `contact user get --ids <userId>` |
| `lookup-dept-id` | 1. `contact dept search --query "<部门关键词>"` → 回显 `deptId`(多命中须消歧) |
| `list-subdepts` | 1. 已有父 `deptId``contact dept list-children --dept <父deptId>` 直接取直属子部门列表<br>2. 只有部门名 → 先 `lookup-dept-id``deptId`,再 `list-children` |
| `list-dept-members` | 1. **优先** `python scripts/contact_dept_members.py --query "<部门名>"`<br>2. 备选:`lookup-dept-id``contact dept list-members --ids <deptId>`<br>3. 若要每人档案字段:对 `userId` 批量 `contact user get --ids …` |
| `list-multi-dept-members` | 1. 对每个部门名 `contact dept search --query "<名>"` → 各 `deptId`<br>2. `contact dept list-members --ids <id1>,<id2>,...`(多部门并行/批量见 conventions<br>3. 需要档案再 `contact user get --ids …` |
| `verify-user-dept` | 1. `contact dept search --query "<部门名>"``deptId`<br>2. `contact dept list-members --ids <deptId>` 中匹配姓名;或先 `search-user` lite 再 `user get` 核对部门字段 |
## Full / 多步组合
| Recipe | 行动指南(固定路线) |
|--------|---------------------|
| explore-subdepts-and-members | 1. 取父部门 `deptId`:用户给了 ID 直接用;只给名字则 `contact dept search --query "<父部门名>"` → 父 `deptId`(多命中先消歧)<br>2. **优先** `contact dept list-children --dept <父deptId>` 拿到全部直属子 `deptId` 列表;若用户只点名了部分子部门,则改为对每个子部门名 `contact dept search --query "<子部门名>"`<br>3. 对子 `deptId``contact dept list-members --ids <id1>,<id2>,...`(多部门按 conventions **并行/批量**<br>4. 若还要成员详情:汇总 `userId``contact user get --ids …`(≤30 条/批,超出分批 + 用户确认) |
| verify-user-in-dept | 同速查表 `verify-user-dept`;多轮对话中用户追加「是否在某部门」时叠加本路线 |
| cross-level-dept-members | 1. `contact dept search --query "<父部门关键词>"` → 父 `deptId`<br>2. **优先** `contact dept list-children --dept <父deptId>` 枚举全部直属子 `deptId`;用户已点名的子部门则用 `dept search` 精确命中<br>3. 需要逐层下钻时,对上一步拿到的子 `deptId` 继续 `dept list-children` 递归(注意控制深度,避免一次拉太多)<br>4. `contact dept list-members --ids <id1,id2,…>` → 按需 `user get` |
| user-detail-organization | 1. `aisearch person --query "<关键词>" --dimension <维度>``userId`(多结果先消歧)<br>2. **必须** `contact user get --ids <userId>`<br>3. 若用户同时给出部门语境:叠加 `verify-user-dept` |
| batch-users-by-keyword | 1. `aisearch person --query "<职位或技能关键词>" --dimension position,duty` → 多条<br>2. 提取 `userId`(≤30)→ `contact user get --ids …`<br>3. 结果过多时汇总或请用户收窄 |
@@ -0,0 +1,574 @@
# 通讯录 (contact) 命令参考
> **CRITICAL — 命令合法性**contact 二级子命令包括 `user` / `dept` / `label` / `relation` / `org` / `account`。
> 不存在 `contact search`、`contact find`、`contact list`、`contact get`、`contact user find/list`。
> 构造命令前必须确认路径在下方「命令总览」中存在;不确定时,**根据意图对照下方「意图判断」选择正确命令**。
>
> **CRITICAL — 创建企业 vs 创建企业账号(必须优先匹配长模式)**:
> - 用户说"创建企业**账号** / 新建企业**账号** / 开通企业**账号** / **专属账号** / **企业登录账号**" → **`account create`**(创建企业专属登录账号)
> - 用户说"创建企业 / 新建企业 / 开通企业 / 初始化企业"**不含"账号"二字**)→ **`org create`**(创建企业组织本身)
> - **判断口径**:先检查 query 是否含"账号"关键词;含则必须路由 `account create`**禁止**路由 `org create`。
>
> **CRITICAL — 根部门**:钉钉根部门 `deptId=1`。单部门命令查根部门通常传 `--dept 1``dept list-members` 传 `--ids 1`。`dept search` 精确命中企业根部门时可能返回 `deptId=-1` 哨兵,后续部门命令必须规范化为 `1`。不要传 `self / me / root / 0`。
## 命令总览
### user (人员查询)
#### 获取当前用户信息
```
Usage:
dws contact user get-self [flags]
Aliases:
get-self, self, me, whoami, current
Example:
dws contact user get-self
dws contact user self # 别名
dws contact user me # 别名
dws contact user whoami # 别名
dws contact user current # 别名
Notes:
- 触发词:我是谁 / 我的信息 / 我的 userId / 当前用户 / 本人 / self / me / whoami
- 顶层亦已挂 `dws contact get-self / user-self / current-user` 提示,误写会引导到正确命令
- **禁止**用 `dws contact user get --ids me/self/current` 代替(会报错);正确用法是 `get-self` 或其别名
```
#### 按关键词搜索用户
```
Usage:
dws contact user search [flags]
Example:
dws contact user search --query "张三"
Flags:
--query string 搜索关键词 (必填)
Returns: (列表,每项包含以下字段)
name string 成员姓名
nick string 成员昵称
userId string 成员 ID(仅同事关系时返回)
title string 员工职位(仅同事关系时返回)
openDingTalkId string 当前用户视角下的目标用户唯一标识,不可跨用户共享;可用于发消息等好友关系场景的操作
```
> **CAUTION:** 多人同名时禁止默认选第一个 — `user search` 不返回部门信息,须追加 `contact user get --ids userId1,userId2,...` 获取部门/职位后请用户确认。详见 [08-directory.md](08-directory.md)「多命中」。
#### 按手机号搜索用户
```
Usage:
dws contact user search-mobile [flags]
Example:
dws contact user search-mobile --mobile 13800138000
Flags:
--mobile string 手机号 (必填)
```
#### 批量获取用户详情
```
Usage:
dws contact user get [flags]
Example:
dws contact user get --ids userId1,userId2
Flags:
--ids string 用户 ID 列表,逗号分隔 (必填)
Notes:
- **禁止**将 `self/me/current/whoami` 作为 userId 传入;查自己请用 `dws contact user get-self`
```
#### 邀请员工加入企业
```
Usage:
dws contact user invite [flags]
Example:
dws contact user invite --org-user-name "张三" --org-user-mobile "13800138000" --depts '[{"deptId":1}]'
Flags:
--org-user-name string 员工在企业内的名称 (必填)
--org-user-mobile string 员工手机号 (必填)
--depts string 员工所属部门列表 JSON 数组(可选),格式: [{"deptId":1}]
Notes:
- 通过手机号邀请单个员工加入当前企业
- 认证信息(corpId、optUserId)由系统自动注入,无需手动传入
```
#### 修改员工信息
```
Usage:
dws contact user update [flags]
Aliases:
update, modify, edit
Example:
dws contact user update --user-id user001 --org-user-name "张三三"
dws contact user update --user-id user001 --depts '[{"deptId":1}]'
dws contact user update --user-id user001 --master-user-id manager001 --yes
Flags:
--user-id string 要修改的员工 userId (必填)
--org-user-name string 员工在企业内的名称(可选)
--depts string 员工所属部门列表 JSON 数组(可选),格式: [{"deptId":1}]
--master-user-id string 直属主管 userId(可选)
--yes 跳过二次确认(可选)
Notes:
- 至少需要一个修改项(--org-user-name、--depts 或 --master-user-id
- 未加 --yes 时会交互式提示确认;脚本/自动化场景请显式带上 --yes
- 认证信息(corpId、optUserId)由系统自动注入,无需手动传入
```
#### 更新自己的 profile 信息
```
Usage:
dws contact user update-self [flags]
Aliases:
update-self, update-me, update-self-profile, edit-self, modify-self
Example:
dws contact user update-self --nick "新昵称"
dws contact user update-self --avatar-file-id "xxxxxx" --yes
dws contact user update-self --nick "新昵称" --avatar-file-id "xxxxxx" --yes
Flags:
--nick string 新昵称(可选)
--avatar-file-id string 新头像在钉盘的 fileId(可选)
--yes 跳过二次确认(可选)
Notes:
- 更新当前登录用户自己的个人 profile 信息(昵称 / 头像),不是修改员工组织信息
- 至少需要一个修改项(--nick 或 --avatar-file-id
- 头像 fileId 需要先上传头像到钉盘获取
- 未加 --yes 时会交互式提示确认;脚本/自动化场景请显式带上 --yes
- 认证信息(corpId、optUserId)由系统自动注入,无需手动传入
```
### profile (用户档案 / 花名册)
#### 查询花名册有权限的字段列表
```
Usage:
dws contact user profile fields
Example:
dws contact user profile fields
Flags:
```
查询花名册有权限的字段列表,根据当前用户查询花名册有权限的字段列表。认证信息(corpId、optUserId)由系统自动注入,无需手动传入。
#### 查询员工花名册字段信息(个人档案)
```
Usage:
dws contact user profile get [flags]
Example:
dws contact user profile get --staff-id STAFF_ID
dws contact user profile get --staff-id STAFF_ID --fields fieldCode1,fieldCode2
Flags:
--staff-id string 查询员工 ID(可选)
--fields string 指定字段集合, 逗号分隔, 可通过 profile fields 获取(可选)
```
查询员工花名册字段信息,根据当前用户指定员工和字段列表,查询相应管理范围内员工的字段值信息。
花名册字段包含:试用/转正信息、个人/家庭信息、学历信息、银行卡/合同信息、紧急联系人和其他企业自定义信息。
> **与 `contact user get` 的区别**`user get` 返回组织管理信息(部门、主管、管理员权限),`user profile get` 返回个人档案信息(学历、家庭、银行卡等)。
### dismission (离职员工)
#### 分页获取离职员工列表
```
Usage:
dws contact user dismission search [flags]
Example:
dws contact user dismission search
dws contact user dismission search --name "张三"
dws contact user dismission search --start 2026-01-01 --end 2026-03-31
dws contact user dismission search --depts 123456,789012 --page 1 --limit 50
Flags:
--name string 员工姓名,模糊搜索(可选)
--start string 离职日期查询范围开始,格式 YYYY-MM-DD(可选)
--end string 离职日期查询范围结束,格式 YYYY-MM-DD(可选)
--depts string 部门 ID 列表,逗号分隔(可选)
--hide-retirement 是否隐藏退休,默认 true(可选)
--hide-partner 是否隐藏合作伙伴,默认 false(可选)
--page int 页码,从 1 开始(可选,默认 1)
--limit int 页大小,200 以内(可选,默认 20)
```
查询离职员工列表,支持按员工姓名、离职日期范围、部门进行过滤。认证信息(corpId、optUserId)由系统自动注入,无需手动传入。
`--start``--end` 必须同时设置或同时不设置,不允许只传其中一个。
### dept (部门查询与管理)
#### 搜索部门
```
Usage:
dws contact dept search [flags]
Example:
dws contact dept search --query "技术部"
Flags:
--query string 搜索关键词 (必填)
```
#### 获取部门详情
```
Usage:
dws contact dept get-info [flags]
Example:
dws contact dept get-info --dept 12345
Flags:
--dept string 部门 ID (必填)
Notes:
- **钉钉根部门 `deptId=1`**;查根部门用 `--dept 1`
```
#### 查看子部门
```
Usage:
dws contact dept list-children [flags]
Example:
dws contact dept list-children --dept 1 # 枚举根部门下的一级部门
dws contact dept list-children --dept 12345 # 枚举指定部门的直属子部门
Flags:
--dept string 父部门 ID (必填)
Returns:
success bool 调用是否成功
result list 直属子部门列表,每项包含以下字段:
deptId int 子部门 ID
deptName string 子部门名称
Notes:
- **钉钉根部门 `deptId=1`**;查询一级部门请用 `--dept 1`
- 仅返回**直属**(直接下一级)子部门,不递归;需要逐层下钻请对子 deptId 继续调用本命令
- 受组织架构可见性控制:仅返回调用者**有权限查看**的子部门
- 父部门不可见或无子部门时返回 result=[] 空列表(非错误)
```
#### 查看部门成员
```
Usage:
dws contact dept list-members [flags]
Example:
dws contact dept list-members --ids 12345,67890
dws contact dept list-members --ids 1 # 根部门
Flags:
--ids string 部门 ID 列表,逗号分隔 (必填)
Notes:
- **钉钉根部门 `deptId=1`**;查根部门直属成员用 `--ids 1`
- 仅返回**本部门**直接成员,**不含下级部门**成员;需含下级请先 `dept list-children` 枚举子部门,再对子 deptId 分别/合并调用 `list-members`
- 受组织架构可见性控制;`--ids` 支持逗号分隔批量查询多个部门
- 跨层级成员展开见 [08-directory.md](08-directory.md) 的 `cross-level-dept-members` recipe
```
#### 创建部门
```
Usage:
dws contact dept create [flags]
Example:
dws contact dept create --name "新产品部" --create-dept-group true
dws contact dept create --name "研发一组" --parent 12345 --create-dept-group false --yes
Flags:
--name string 部门名称 (必填)
--parent string 父部门 ID(可选),不传默认根部门
--create-dept-group bool 是否创建部门群(必填)
--yes 跳过二次确认(可选)
Notes:
- 父部门不传时默认钉钉根部门 deptId=1
- --create-dept-group 必须显式指定 true 或 false
- 未加 --yes 时会交互式提示确认;脚本/自动化场景请显式带上 --yes
- 认证信息(corpId、optUserId)由系统自动注入,无需手动传入
```
#### 更新部门
```
Usage:
dws contact dept update [flags]
Example:
dws contact dept update --dept 12345 --name "新部门名"
dws contact dept update --dept 12345 --name "新名称" --parent 67890 --yes
Flags:
--dept string 部门 ID (必填)
--name string 新部门名称(必填)
--parent string 新父部门 ID(可选)
--yes 跳过二次确认(可选)
Notes:
- --dept 为要更新的部门 ID,可通过 dept search 或 dept list-children 获取
- --name 必填;--parent 可选,未指定时只更新部门名称
- 未加 --yes 时会交互式提示确认;脚本/自动化场景请显式带上 --yes
- 认证信息(corpId、optUserId)由系统自动注入,无需手动传入
```
### label (角色查询)
> **角色ID = labelId**:用户提到"角色ID"时,均指通讯录 label 系统中的角色ID,**不是部门ID也不是userId**。查角色成员用 `label list-members --id <角色ID>`,不要传给 `dept get-info` 或 `user get`。
#### 获取企业所有角色列表
```
Usage:
dws contact label list
Example:
dws contact label list
Flags:
Notes:
- 无需参数,返回当前企业全部角色列表(labelId、labelName等)
- 用于不知道准确角色名称时先浏览全部角色
- 典型场景:用户说“企业所有主管/查所有管理员/财务人员有哪些”→ 先 label list 浏览全部角色,匹配目标角色后 label list-members 获取成员
```
#### 根据角色名称查询角色
```
Usage:
dws contact label get [flags]
Example:
dws contact label get --names "管理员"
dws contact label get --names "管理员,财务"
Flags:
--names string 角色名称,逗号分隔 (必填)
Notes:
- 精确匹配角色名称,不支持模糊搜索
- 支持同时查询多个角色名称,逗号分隔
- 无需分页
```
#### 查询角色下的成员
```
Usage:
dws contact label list-members [flags]
Example:
dws contact label list-members --id 12345
Flags:
--id string 角色 ID (必填)
Notes:
- 根据角色ID直接查询成员列表;已有角色ID时直接用 `--id <labelId>`
- 不知道角色ID时:先 `dws contact label get --names "角色名"` 或 `dws contact label list` 获取 labelId
```
### org (企业管理)
#### 创建企业
```
Usage:
dws contact org create [flags]
Example:
dws contact org create --org-name "我的企业" --creator-username "张三"
Flags:
--org-name string 企业名称 (必填)
--creator-username string 创建者在企业内的名称,对应 creatorUsername (必填)
Notes:
- 创建一个新的钉钉企业,当前用户将成为该企业的创建者
- 认证信息(corpId、optUserId)由系统自动注入,无需手动传入
```
### account (企业账号管理)
#### 创建企业专属账号
```
Usage:
dws contact account create [flags]
Example:
dws contact account create --org-user-name "张三" --login-id "zhangsan001" --org-user-mobile "13800138000" --email "zhangsan@example.com" --dept-ids "1,2,3" --send-pwd-via-sms
Flags:
--org-user-name string 员工在企业内的名称 (必填)
--login-id string 登录号 (必填),请勿包含手机号等联系方式
--org-user-mobile string 员工手机号(可选)
--email string 邮箱(可选)
--dept-ids string 要加入的部门 ID 列表,逗号分隔(可选)
--send-pwd-via-sms 是否通过手机短信/邮件发送登录邀请(可选,默认 false)
Notes:
- 为当前企业创建一个专属登录账号
- 登录号请勿包含手机号,否则可能被运营商拦截短信
- 认证信息(corpId、optUserId)由系统自动注入,无需手动传入
```
#### 更新企业账号用户信息
```
Usage:
dws contact account update [flags]
Aliases:
update, modify, edit
Example:
dws contact account update --user-id user001 --org-user-name "张三"
dws contact account update --user-id user001 --depts '[{"deptId":1}]'
dws contact account update --user-id user001 --nick "新昵称" --avatar-file-id "xxxxxx" --yes
Flags:
--user-id string 被修改企业账号的 userId (必填)
--org-user-name string 企业账号在企业内的员工姓名(可选)
--depts string 部门列表 JSON 数组(可选),格式: [{"deptId":1}]
--master-user-id string 直属主管 userId(可选)
--nick string 企业账号自身昵称,用于 profile 展示(可选)
--avatar-file-id string 企业账号头像在钉盘的 fileId(可选)
--yes 跳过二次确认(可选)
Notes:
- 更新指定企业账号用户的信息,不是修改普通员工信息
- 至少需要一个修改项(--org-user-name / --depts / --master-user-id / --nick / --avatar-file-id
- 头像 fileId 需要先上传头像到钉盘获取
- 未加 --yes 时会交互式提示确认;脚本/自动化场景请显式带上 --yes
- 认证信息(corpId、optUserId)由系统自动注入,无需手动传入
```
## 意图判断
> **按搜索性质分流**:姓名模糊搜索、工号、部门、职责和上下级使用 [aisearch person](../../dingtalk-aisearch/references/aisearch.md);完整手机号精确反查使用 `user search-mobile`;拿到 userId 后,以下详情、部门和角色场景再用 contact。
用户说"我是谁/我的信息/我的 userId/当前用户/本人/self/me/whoami" → `user get-self`(无需参数;禁止用 `user get --ids me/self` 代替)
用户需要 userId 给其他产品使用(发消息/建待办/约日程)→ `aisearch person` 按对应维度搜索
用户提供完整手机号并要求反查用户 → `user search-mobile --mobile "<完整手机号>"`
用户说"查用户详情/部门/主管/管理员" → `user get`(需 userId,返回组织管理信息)
用户说"修改员工/更新员工/改员工姓名/改员工部门/换部门/改直属主管/换主管/调整员工信息" → `user update`(需 userId;姓名 / 部门 / 主管至少改一项)
用户说"改昵称/改头像/更新我的资料/更新我的profile/更新我的个人信息/修改我的昵称" → `user update-self`(昵称 / 头像至少改一项;头像 fileId 需先上传钉盘)
用户说"邀请员工/添加员工/批量邀请/加人/新员工入职/拉人进企业" → `user invite`(需手机号 + 企业内名称 + 部门)
用户说"花名册字段/有哪些字段/字段列表" → `user profile fields`
用户说"花名册/员工档案/学历/家庭/银行卡/紧急联系人/合同" → `user profile get`(需 staffId,返回个人档案信息)
用户说"离职员工/离职名单/离职人员/已离职" → `user dismission search`
用户说"创建企业账号/新建企业账号/开通企业账号/专属账号/企业登录账号"(含"账号")→ `account create`(需员工名称 + 登录号;手机号可选)
用户说"更新企业账号/修改企业账号/改企业账号信息/改企业账号姓名/改企业账号部门/改企业账号主管/改企业账号昵称/改企业账号头像"(含"账号"且含"改/更新/修改")→ `account update`(需 userId;至少改一项)
用户说"创建企业/新建企业/开通企业/初始化企业"(不含"账号")→ `org create`(需企业名称 + 创建者名称)
用户说"找部门/哪个部门" → `dept search`
用户说"部门详情/部门信息/部门多少人" → `dept get-info`(返回部门ID、部门名称、部门人数;需 deptId,若只有部门名称需先 `dept search`
用户说"子部门/下设部门/部门有哪些下级部门/枚举二级部门" → `dept list-children`(需父 deptId;只有部门名先 `dept search`
用户说"部门有谁/部门成员/人员名单" → `dept list-members`(需 deptId;**仅本部门不含下级**,含下级先 `dept list-children` 再合并查)
用户说"创建部门/新建部门/添加部门/成立部门/建部门" → `dept create`(需部门名;父部门可选,默认根部门)
用户说"更新部门/修改部门/改部门名/改部门名称/换部门名/改父部门/换上级部门" → `dept update`(需 deptId + nameparent 可选)
用户查询涵盖"角色"(主管/管理员/财务/HR/总经理等任意角色名)→ 统一走 `contact label` 链路,按下方决策树选命令:
- 不知道角色名 / 枚举所有角色 → `label list`
- 已知角色名,查ID或成员 → 先 `label get --names <名>` 拿ID,查成员再调 `label list-members --id <ID>`**精确匹配无结果时降级 `label list` 模糊匹配**
- 已知角色ID 查成员 → `label list-members --id <ID>`
> [!IMPORTANT]
> **角色查询 3 步决策树**(不依赖字串匹配,按语义判定):
> 1. 不知道角色名 / 要枚举所有角色(列出企业有哪些角色、每个角色的名称和ID、不确定叫什么角色、负责XX的人有哪些等) → `label list`
> 2. 已知角色名 要查角色ID → `label get --names`
> 3. 已知角色ID 要查成员 → `label list-members --id`
>
> 任何含"角色"一词的查询默认走 `contact label` 链路。**唯一例外**:终点是"某个人是否具备某权限"(如"张三是不是管理员")→ `user get`。禁止路由到 `user profile fields`(那是花名册字段)、`dept list-members` 筛选、或 OA/chat 模块。
> [!IMPORTANT]
> **角色查人 vs 查某人的角色信息 — 判断口径**:先判断用户的终点是"人"还是"属性"
> - 终点是**人**"管理员角色有哪些人""管理员下都有谁""查XX角色的成员")→ 角色维度查人,**必须**走 `contact label` 链路(`label get`/`label list` → `label list-members`),**禁止**通过 `dept list-members` 筛选 `isAdmin` 等字段替代
> - 终点是**属性**"张三是不是管理员""查某人的主管/管理员权限")→ 已知 userId 查个人详情,走 `user get`(返回 isAdmin/leader 等字段)
>
> 反例对照:
> - "管理员角色下都有哪些人" → `label get --names 管理员` → `label list-members`(终点=角色下的**人员列表**)
> - "我想知道管理员这个角色下都有谁" → `label get --names 管理员` → `label list-members`(终点=角色下的**人员列表**)
> - "张三是不是管理员" → `user get --ids <userId>`(终点=某个人的**属性**
> - "查一下张三的管理员权限" → `user get --ids <userId>`(终点=某个人的**属性**
> - "角色ID为55808858的角色下有哪些成员" → `label list-members --id 55808858`(已有角色ID直接查成员)
>
> **角色 = 通讯录 label**:用户提到"角色"(角色ID/角色成员/角色名称/企业角色/查角色下的人)时,均指通讯录组织角色,应走 `contact label` 链路。OA 审批只管审批流程(待审批/同意/拒绝),**不支持**查询角色成员;群角色(chat group-role)只管群内身份,不涉及企业组织角色。
用户说"我关注了谁/我的特别关注列表/我的星标联系人/特别关注的人有哪些" → `relation list-my-followings`
> [!IMPORTANT]
> **易混淆硬规则**`relation list-my-followings` **只**返回"我特别关注的人员列表"(一组 openDingTalkId),**不**返回任何消息内容。
>
> **禁止路由到本命令的场景**(query 中同时包含『关注/特别关注/星标』和以下任一消息域动词/名词时,必须路由到 [`chat message list-focused`](../../dingtalk-chat/references/chat.md)):
> - 动词类:**发**了什么、**说**了什么/啥、**聊**了什么、**讲**了什么
> - 名词类:**消息**、**聊天**、**动态**、**最新内容**
>
> **判断口径**:先扫描 query 是否含上述动词/名词;含则路由到 `chat message list-focused`**不论** query 主语是否为"我特别关注的人"。
>
> 反例对照:
> - "我特别关注的人有哪些" → `relation list-my-followings`(终点=人员列表)
> - "我特别关注的人**最近发了什么消息**" → `chat message list-focused`(含"发""消息"
> - "我关注的人**最近都说了啥**" → `chat message list-focused`(含"说"
组合场景(多子部门、跨层级成员、强消歧)见 [08-directory.md](08-directory.md)。
## 核心工作流
```bash
# 1. 查看自己的信息 — 提取 userId
dws contact user get-self --format json
# 2. 按名字搜索人员 — 统一从 AI 搜问取得 userId/openDingTalkId
dws aisearch person --query "张三" --dimension name --format json
# 3. 查看部门结构 — 提取 deptId
dws contact dept search --query "技术部" --format json
# 4. 查看部门详情(部门ID、名称、人数)
dws contact dept get-info --dept <deptId> --format json
# 5. 查看直属子部门 — 提取子 deptId 列表
dws contact dept list-children --dept <父deptId> --format json
# 6. 查看部门成员
dws contact dept list-members --ids <deptId> --format json
# 7. 获取企业所有角色列表 — 不知道角色名时先浏览
dws contact label list --format json
# 8. 根据角色名称查询角色
dws contact label get --names "管理员" --format json
# 9. 查询角色下的成员
dws contact label list-members --id <labelId> --format json
# 10. 查询花名册有权限的字段列表
dws contact user profile fields --format json
# 11. 根据字段 code 查询指定员工的花名册信息
dws contact user profile get --staff-id <STAFF_ID> --fields fieldCode1,fieldCode2 --format json
# 12. 查询所有可见字段的花名册信息
dws contact user profile get --staff-id <STAFF_ID> --format json
# 13. 查询全部离职员工
dws contact user dismission search --format json
# 14. 按姓名/时间范围/部门筛选离职员工
dws contact user dismission search --name "张三" --format json
dws contact user dismission search --start 2026-01-01 --end 2026-03-31 --format json
dws contact user dismission search --depts 123456,789012 --hide-retirement=false --format json
# 15. 创建企业
dws contact org create --org-name "我的企业" --creator-username "张三" --format json
# 16. 创建企业专属账号
dws contact account create --org-user-name "张三" --login-id "zhangsan001" --org-user-mobile "13800138000" --email "zhangsan@example.com" --dept-ids "1,2,3" --send-pwd-via-sms --format json
# 17. 更新企业账号用户信息
dws contact account update --user-id user001 --org-user-name "张三三" --depts '[{"deptId":1}]' --master-user-id manager001 --nick "新昵称" --avatar-file-id "xxxxxx" --yes --format json
# 18. 邀请员工加入企业
dws contact user invite --org-user-name "张三" --org-user-mobile "13800138000" --depts '[{"deptId":1}]' --format json
# 19. 修改员工信息
dws contact user update --user-id user001 --org-user-name "张三三" --depts '[{"deptId":1}]' --master-user-id manager001 --yes --format json
# 20. 更新自己的 profile 信息
dws contact user update-self --nick "新昵称" --avatar-file-id "xxxxxx" --yes --format json
# 21. 创建部门
dws contact dept create --name "新产品部" --parent 12345 --create-dept-group true --yes --format json
# 22. 更新部门
dws contact dept update --dept 12345 --name "新部门名" --parent 67890 --yes --format json
```
## 上下文传递表
| 操作 | 提取 | 用于 |
|------|------|------|
| `user get-self/search` | `userId` | 其他产品中的 --users/--executor 参数 |
| `user get-self/search` | `orgAuthEmail` | mail message send 的 --to/--cc (跨产品) |
| `user get-self/search` | `userId` | profile get 的 --staff-id |
| `user profile fields` | `fieldCode` | profile get 的 --fields |
| `label list` | `labelId` / `labelName` | `label get --names``label list-members --id` |
| `label get` | `labelId` | `label list-members` 的 --id |
| `dept search/list-children` | `deptId` | dept get-info/list-children/update 的 --deptdept list-members 的 --ids |
| `dept search/list-children` | `deptId` | dismission search 的 --depts |
| `dept create` | `deptId` | dept get-info/list-children/update 的 --deptdept list-members 的 --ids |
## 注意事项
- `user get-self` 是获取 userId 的最快方式,其他产品的 --users/--executor 都需要 userId
- `user get --ids``dept list-members --ids` 都支持批量查询,逗号分隔
- `user get` 返回组织管理信息(部门、主管、管理员权限),`user profile get` 返回个人档案信息(学历、家庭、银行卡等),注意区分
- `user profile get``--staff-id` 可通过 `user get-self``aisearch person` 获取
- `user profile get``--fields` 可通过 `user profile fields` 获取可用字段 code 列表;不填则查询所有可见字段
- 建议先执行 `user profile fields` 获取可用字段列表,再根据需要的字段 code 执行 `user profile get`
- `user dismission search``--start`/`--end` 必须同时设置或同时不设置,不允许只传其中一个
- `user dismission search` 默认隐藏退休人员(`--hide-retirement` 默认 true),默认展示合作伙伴(`--hide-partner` 默认 false
- `label list` 无需参数,适用于不知道准确角色名称的场景;当用户说“查所有主管/主管理员/财务”等角色类型人员时,优先 `label list` 列出企业全部角色,LLM 灵活匹配目标角色后调用 `label list-members`
- 角色类查询(主管、管理员、财务、HR 等任意角色)优先走 label 链路,而非 dept list-members 或 aisearch personlabel 精确命中角色维度,返回完整名单
- `label get` 是精确匹配角色名称,不支持模糊搜索;支持逗号分隔同时查询多个角色名称
- **`label get` 精确匹配无结果时的降级策略**:若 `label get --names "XX"` 返回空结果,必须降级调用 `label list` 获取全部角色列表,从中模糊匹配包含XX关键词的角色(如用户说"管理员"可匹配到"主管理员"和"子管理员"),再对匹配到的角色调用 `label list-members`
- `label list-members` 需要先通过 `label list``label get` 获取 labelId,再用 --id 查询角色下的成员
- `user update-self` 用于更新当前用户自己的昵称/头像;头像 fileId 需先上传头像到钉盘获取
- `account update` 用于更新企业账号用户信息;`--depts` 为 JSON 数组格式,头像 fileId 需先上传钉盘获取
## 自动化脚本
| 脚本 | 场景 | 用法 |
|------|------|------|
| [contact_dept_members.py](../scripts/contact_dept_members.py) | 按部门名称搜索并列出所有成员 | `python contact_dept_members.py --query "技术部"` |
@@ -0,0 +1,14 @@
# contact 局部意图消歧
本文件从单 Skill `intent-guide.md` 拆分而来,仅保留与本产品相关的跨产品消歧规则。
| 用户说... | 真实意图 | 应该用 | 不要用 | 理由 |
|---|---|---|---|---|
| "张三在哪个部门/张三的工号是多少" | 搜人后查通讯录详情 | `aisearch person``contact user get` | 直接 `contact user search` | 姓名或工号先由 aisearch 获取 userId,再由 contact 补部门、工号等详情 |
| "研发部的详细信息/部门信息" | 查部门详情 | `contact dept get-info` | `contact dept list-members` | 查部门属性(ID、名称、人数)用 get-info;查成员列表用 list-members |
| "研发部有多少人" | 查部门人数 | `contact dept get-info` | `contact dept list-members` | 问人数用 get-info(返回 memberCount);问有哪些人用 list-members |
| "找一下张三/搜同事/找人" | 人员语义搜索 | `aisearch person` | `contact user search` | 姓名模糊搜索、工号、部门、职责和上下级走 aisearchcontact 在拿到 userId 后补详情 |
| "五道的上级是谁/谁负责XX/XX的下属有谁" | AI语义搜人 | `aisearch person` | `contact` | 涉及上下级、职责、负责人等语义维度搜索,用 aisearch |
| "222020这个工号是谁/查工号" | 按工号搜人 | `aisearch person --dimension jobNumber` | `contact` | 工号查人走 aisearchdimension=jobNumber |
| "13800138000是谁/完整手机号反查" | 精确手机号反查 | `contact user search-mobile` | `contact user search` | 完整手机号精确匹配使用 search-mobile |
| "按手机号线索找人" | 手机号语义搜人 | `aisearch person --dimension phone` | `contact user search` | 非精确手机号匹配走 aisearch 的 phone 维度 |
@@ -0,0 +1,30 @@
# contact Lite Recipe
本文件从单 Skill `lite-recipes.md` 拆分而来,仅保留与本产品相关的轻量流程。
## #8 通讯录
### get-contact-self
`contact user get-self` → 当前用户 userId、部门、主管等
### search-person
**搜人首选入口**。凡是“找人/搜人/找同事/谁负责/上级/下级/负责人/团队成员”均优先用 `aisearch person`
1. 从用户问题中提取 keyword(人名/业务关键词)和 dimension(维度),规则见 [aisearch.md](../../dingtalk-aisearch/references/aisearch.md)。
2. `aisearch person --query "<关键词>" --dimension <维度>`
3. 结果中提取 `userId``title`(姓名)展示给用户。
4. 若需要 userId 做后续操作(发消息/建待办),可直接使用结果中的 `userId`
5. **重名消歧**:多人同名时禁止默认选第一个,须追加 `contact user get --ids` 获取部门/职位后请用户确认,详见 [08-directory.md](../../dingtalk-contact/references/08-directory.md)「多命中」。
### search-user
仅在以下**精确查询**场景使用,搜人请优先用 `search-person`
- 需要获取 userId 给其他产品使用(发消息/建待办/约日程)
- 已有 userId 需查完整详情(`contact user get --ids`
1. `aisearch person --query "<姓名>" --dimension name``userId`;**多命中须列出候选请用户确认**。
2. **重名消歧**:多人同名时禁止默认选第一个,须追加 `contact user get --ids` 获取部门/职位后请用户确认,详见 [08-directory.md](../../dingtalk-contact/references/08-directory.md)「多命中」。
3. 需详情时:`contact user get --ids <userId>`(多人可 `--ids id1,id2,...`
@@ -0,0 +1,44 @@
# 业务域通用规范
> 仅服务本 skill 已迁入的行动指南。安全门控、危险操作确认、`--format json` 等已在本 skill 的 `SKILL.md` 中定义,此处不重复。
## 批量查询规范
| # | 规范 |
|---|------|
| 1 | **并行查详情**:拿到多个 ID 后,用 `&` 合并到同一条 Shell 命令并行执行 + `wait`**严禁逐条串行** |
| 2 | **翻页**:分页接口须拉全直至无更多 |
| 3 | **优先批量 API**:有批量接口则用批量;无则按 #1 并行 |
| 4 | **群消息**:必须先 `chat search --query``openConversationId`,再 `chat message list --group <openConversationId> --time "<yyyy-MM-dd HH:mm:ss>" --direction older`;多群同条命令并行 |
| 5 | **列表少轮次**:带条件搜索/列表 → 一次采全详情;**禁止**无新参数时重复同一 `list` / `search` |
## 多源并行采集(公共模式)
> recipe 引用方式:`按「多源并行采集」执行(关键词=<X>,时间=<Y>至<Z>`。
- 同条 Shell`&` 并行 + `wait`;分页须采全。
- 只保留与主题相关的数据,无关丢弃。
- 有批量详情接口优先;否则并行拉详情(见上表 #1)。
- 具体采哪些产品列表由对应 **行动指南 recipe** 与当前产品参考决定;不要引入本文档未覆盖的产品路线。
## 字段术语与 ID 传递
> list 返回 JSON 后,必须提取下表字段传给后续命令。**禁止用其他字段替代。**
| 字段 | 来源 | 传递给 |
|------|------|--------|
| `taskUuid` | `minutes list` | `minutes get summary/info/batch --id(s)` |
| `userId` | `aisearch person` / `contact user search` / `contact dept list-members` | `contact user get --ids``todo --executors``calendar --users` |
| `deptId` | `contact dept search` | `contact dept list-members --ids <deptId1,deptId2...>`;多子部门时对每个子部门分别 `dept search` 取 id |
| `nodeId` | `drive search` / `wiki node search` | `doc read/update --node``drive copy/move/rename/delete --node` |
| `nodeId` | `wiki node list` 中的 folder 类型节点 / `wiki node create --type folder` | `wiki node list --folder``wiki node create --folder``drive upload --folder``drive copy/move --folder` |
| `eventId` | `calendar event list` | `calendar event get/update --id` |
| `processInstanceId` | `oa approval list-*` | `oa approval detail/approve --instance-id` |
| `openConversationId` | `chat search` | `chat message list/send --group` |
| `todoTaskId` | `todo task list` | `todo task update/done --task-id` |
| `reportId` | `report inbox list` / `report outbox list` | `report entry get/stats --report-id` |
| `baseId` / `tableId` | `aitable base search` | `aitable record query --base-id --table-id` |
| `dentryUuid` | `drive list` / `drive mkdir` | `drive info/download/copy/move/rename/delete --node``drive list/mkdir/upload/copy/move --folder` |
| `dentryId` | `drive info` 的数字字段 | 仅用于 `chat message send --dentry-id` |
**ID 边界硬约束**`dentryId` 通常是纯数字,只表示聊天文件消息需要的钉盘条目数字 ID;它不是父目录 ID。遇到 `drive --node/--folder``doc --node``wiki node --folder` 时,只能使用 `dentryUuid` / `nodeId` / 文档 URL。若当前上下文只有数字型 `dentryId`,必须先重新 `drive list` / `drive search` / `wiki node list` 获取正确 ID,不能把该数字直接代入后续命令。
@@ -0,0 +1,130 @@
#!/usr/bin/env python3
"""
按部门名称搜索并列出所有成员(自动 deptId 解析)
用法:
python contact_dept_members.py --query "技术部"
python contact_dept_members.py --query "产品" --dry-run
"""
import sys
import json
import subprocess
import argparse
from typing import List, Any, Optional
def run_dws(
args: List[str], dry_run: bool = False,
) -> Optional[Any]:
cmd = ['dws'] + args
if dry_run:
print(f"[dry-run] {' '.join(cmd)}")
return None
try:
result = subprocess.run(
cmd, capture_output=True, text=True, timeout=60
)
if result.returncode != 0:
print(f"错误:{result.stderr.strip()}", file=sys.stderr)
return None
return json.loads(result.stdout)
except (subprocess.TimeoutExpired, json.JSONDecodeError,
FileNotFoundError) as e:
print(f"错误:{e}", file=sys.stderr)
return None
def main():
parser = argparse.ArgumentParser(
description='按部门名称搜索并列出所有成员'
)
parser.add_argument(
'--query', required=True, help='部门名称关键词'
)
parser.add_argument('--dry-run', action='store_true')
args = parser.parse_args()
print(f'🔍 搜索部门: {args.query}')
dept_data = run_dws([
'contact', 'dept', 'search',
'--query', args.query, '--format', 'json',
], dry_run=args.dry_run)
if args.dry_run:
run_dws([
'contact', 'dept', 'list-members',
'--ids', '<DEPT_ID>', '--format', 'json',
], dry_run=True)
return
if not dept_data:
print('未找到匹配部门')
sys.exit(1)
if isinstance(dept_data, list):
depts = dept_data
elif isinstance(dept_data, dict):
inner = dept_data.get('result', dept_data)
if isinstance(inner, dict):
depts = inner.get('items', inner.get('depts', []))
elif isinstance(inner, list):
depts = inner
else:
depts = []
else:
depts = []
if not depts:
print('未找到匹配部门')
sys.exit(1)
for dept in depts:
dept_id = dept.get('id') or dept.get('deptId')
dept_name = dept.get('name') or dept.get('deptName', '未知')
if not dept_id:
continue
print(f"\n📂 {dept_name} (ID: {dept_id})")
print('-' * 40)
members_data = run_dws([
'contact', 'dept', 'list-members',
'--ids', str(dept_id), '--format', 'json',
])
if not members_data:
print(' 无法获取成员列表')
continue
if isinstance(members_data, list):
members = members_data
elif isinstance(members_data, dict):
inner = members_data.get('result', members_data)
if isinstance(inner, dict):
members = inner.get('userlist',
inner.get('list', []))
elif isinstance(inner, list):
members = inner
else:
members = []
else:
members = []
if not members:
print(' (暂无成员)')
continue
for m in members:
name = m.get('name') or m.get('userName', '未知')
title = m.get('title') or m.get('position', '')
uid = m.get('userId') or m.get('userid', '')
line = f" 👤 {name}"
if title:
line += f" ({title})"
if uid:
line += f" [ID: {uid}]"
print(line)
print(f"{len(members)}")
if __name__ == '__main__':
main()
+111
View File
@@ -0,0 +1,111 @@
---
name: dingtalk-doc
description: 钉钉在线文字文档(adoc)本体及其内容的操作:查找、创建、读取、文档信息、编辑、块、评论、附件与媒体、白板卡片、导入、导出(docx/markdown/pdf)、版本、模板、协作者权限、分享及Markdown/JSONML写入。不包括:文档空间与钉盘的文件管理(归 dingtalk-drive,doc 同名原子命令已弃用)、知识库空间与节点管理(归 dingtalk-wiki)、原生 .md 文件读写(归 dingtalk-misc)、电子表格 axls(归 dingtalk-misc)、AI 表格 able(归 dingtalk-aitable)。命令前缀:dws doc。
metadata:
cli_version: ">=0.2.14"
category: product
requires:
bins:
- dws
---
# 钉钉文档 Skill
<!-- DWS_RUNTIME_CONTRACT_START -->
## 最小 DWS 执行契约
- 只通过 `dws` CLI 操作钉钉;结构化读取使用 `--format json`,按真实返回判断结果。
- 已知命令直接执行。只有 leaf 参数或安全语义不确定时读取精确 Schema,只有 Cobra flag 不确定时读取精确 leaf Help;不要加载产品级 Catalog 代替选路。
- 不猜命令、flag、字段、ID、账号或时间。后续 ID 必须来自真实返回;零命中、多候选或类型不明时停止并消歧。
- 解析目标、读取上下文和最终执行必须使用同一 profile;不得跨组织复用 userId、openDingTalkId 或 openConversationId。多账号组织只使用明确的 `isOrgCurrent=true` 默认账号;没有默认账号时要求用户指定,禁止选择第一项、最近登录或最近使用账号。
- 不输出或记录 token、refresh token、appSecret、webhook token 等凭据;宿主已注入认证时不要索要凭据。
- 写操作必须符合用户明确意图。是否需要确认以最终 Runtime gate 和 Schema 为准;需要确认时先说明对象、动作与影响,再追加 `--yes`
- 写后按任务结果契约验证;不能仅凭退出码宣称成功。部分结果、未知投递状态和失败项必须如实保留。
- 时间戳面向用户展示时转换为带时区的可读时间;默认使用当前会话时区,必要时同时保留原值。
- 遇到认证、权限、profile、confirmation 或未知错误时,只加载 `dingtalk-shared` 中对应 reference;不要连续猜测替代命令。
<!-- DWS_RUNTIME_CONTRACT_END -->
<!-- VISIBLE_SHORTCUTS_START -->
## Shortcut 发现(按需)
`doc` 当前有 45 条公开 shortcut,完整清单保留在 Runtime Catalog 与 Schema,不在高频产品根 Skill 中重复展开。已知意图按下方路由。
仅当现有路由和 reference 都无法定位低频能力时,才执行 `dws shortcut list --service doc --format json` 做最后回退;不要为已知高频意图加载完整 Shortcut Catalog 或产品级 Schema。
<!-- VISIBLE_SHORTCUTS_END -->
## Golden Route
ID/URL 直用;标题先搜索,唯一命中再执行.顺序:稳定 ID → shortcut → 局部读 → 精确写;禁以产品 Schema、全文或原子命令起步.
| 用户意图 | 唯一推荐入口 | 关键边界 |
|---|---|---|
| <!-- dws-intent: doc.search.by_title -->按标题或主题定位文档 | `dws doc +search --query <精确标题>` | `complete=true,count=0,failures=[]` 即权威零命中:如实报告;禁缩词、跨产品、无 query/无端 `--page-all` |
| 最近访问或最近编辑文档 | 加载 `dingtalk-drive`,执行 `dws drive +recent [--operate-type 1] --limit <N>` | 默认最近访问,`1` 为最近编辑;不要用 `doc +search` 替代最近列表 |
| <!-- dws-intent: doc.content.read -->已知 ID/URL 读取正文或局部内容 | `dws doc +fetch --node <ID或URL>` | 术语用 `keyword`;章节 `outline``section`;整篇才用 `full` |
| 聚合查看信息、权限、版本、媒体或评论 | `dws doc +inspect --node <ID或URL>` | 基础元信息默认返回;样式、权限、历史、媒体、评论才用对应 `--include-*`,无 `--include-info` |
| 新建在线文字文档并写入内容 | `dws doc +create --name <标题> --content <文本\|-\|@文件> [--folder <ID>\|--workspace <ID>]` | 指定位置复用真实 ID,二者互斥;`-`=stdin,禁 `@-`;Runtime 分片回读,不拆写 |
| <!-- dws-intent: doc.content.update -->追加、覆盖或精确编辑 block | `dws doc +update --node <ID或URL> --command <动作>` | 唯一文本 `str_replace`;章节/block 局部取 ID;整篇才 overwrite |
| 重要内容更新且需要恢复点 | `dws doc +checkpoint-update` | 自动保存版本,更新并回读;检查 `steps``compensation` |
| 版本操作 | `dws doc +version-save --node` / `dws doc +version-list --node` / `dws doc +version-revert --node --version` | 快照/列表/回滚 |
| <!-- dws-intent: doc.export.format -->导出为 docx/markdown/pdf | `dws doc +export --export-format <格式>` | 格式必须显式指定;普通文件下载切 `dingtalk-drive` |
| <!-- dws-intent: doc.import.local_file -->本地文件转在线文档 | `dws doc +import --file <相对路径> [--folder <ID>\|--workspace <ID>]` | 指定位置复用真实 ID,二者互斥;未指定才由 Runtime 取默认根(唯一组织根目录);成功须回读验证落点;仅保留原文件走 `dingtalk-drive` |
| 封面/背景 | `+resource-update/+resource-delete``+background-update/+background-delete` | 写后 `+inspect --include-style`;禁查 Catalog |
| 浏览模板 | `dws doc +template-list [--source MY\|PUBLIC] [--page-all]` | “我的/我这边”只查 MY;明确公开才查 PUBLIC;“有哪些/全部”加 `--page-all` 并检查 `complete` |
| 搜索模板 | `dws doc +template-search --query <名称或关键词>` | 来源可选 MY/PUBLIC;零命中停止,禁止拿无关模板替代;多候选消歧 |
| 从模板创建 | `dws doc +create-from-template --template-id <唯一ID>` | 已有唯一 templateId 才创建;不重复 list/search |
| 创建/查评论 | `dws doc +comment-create --node <ID或URL> --content <文字> [--selection <原文>]` / `+review --node <ID或URL>` | node/content 必填;划词也用 `+comment-create`;续操作复用 `commentKey` |
| <!-- dws-intent: doc.access.grant -->添加/调整/移除协作者权限 | `dws doc +access-grant/+access-change/+access-revoke` | `--to` 必填;`--role` 默认 READERREADER\|DOWNLOADER\|EDITOR\|MANAGER);无 `--user-ids`;先读权限,歧义/profile 不一致禁写 |
| <!-- dws-intent: doc.share.link_only -->只发链接不改权限 | `dws doc +share --to <姓名[,姓名]> --url <URL> [--note <附言>]` | 内置姓名解析;仅歧义时 aisearch,禁预查人;普通私信用 chat |
| 授权后向多人分享链接 | `dws doc +grant-and-share` | 仅需改权限时用(必填 `--node`role 默认 READER);检查逐人账本和部分失败 |
| <!-- dws-intent: doc.media.insert -->把文件/PPT/PDF 作为正文附件 | `dws doc +media-insert --node <DOC_ID> --file <相对路径>` | 正文附件走 Doc`drive +upload` 仅入库存储,不会插入正文 |
## 关键结果语义
- 保留真实 `nodeId`/URL/类型/容器;复用 ID,禁标题/钉盘重搜。
- 用完整回执;Runtime 回读后不复读;仅局部验收、`partial_success`/commit-unknown 再 `+fetch`
- 恢复:`partial_success` 只补未完成;`unknown` 先回读、禁重写;`retryable` 仅限明确未开始;权限/参数/认证失败即停。
- 结果明确且回读匹配才报完成。
- 搜索/列表检查 `complete`/`hasMore`/cursor/失败项;“全部”翻完页,前 N 条须声明范围。
- `+import` 检查 `success=true``verified=true``taskId/nodeId/documentUrl`;复用返回 ID,禁 Drive 重找;中断查原任务,禁重导。
- 导出/下载用 cwd 相对路径;`+export``localPath``sizeBytes>0` 即终态,禁 `ls/stat`
## 参数与安全边界
- `@file`:已有或临时文件先暂存到 cwd;传 `@相对路径`,生成文本优先 `--content -`;禁绝对路径和 `..`
- `doc +update``--command` 指定动作;block ID 必须来自 `+fetch --detail with-ids` 或真实列表。
- Schema 门禁:不确定时仅查一次精确 leaf:`--fields use_when,avoid_when,parameters,constraints,confirmation`;禁用产品级/`--all`。准备 Help 时,本轮仅查一次。
- 消费本页或精确 Schema 的 `confirmation``user_required` 且原请求/预授权已确认目标、动作、参数时,首调即加 `--yes`;否则预览/询问;禁止靠失败探测门禁。
- JSONML 顶层必须是单个非空元素;禁止 `[[...]]` 元素数组包裹。
## 按需加载
Golden Route 已给出命令且参数足够时,禁止读取 reference;其余仅遇下表语义时才最多读取一个 reference:
| 触发条件 | Reference |
|---|---|
| 低频/无 shortcut 意图消歧 | [intent-guide.md](references/intent-guide.md) / [doc.md](references/doc.md) 对应章节 |
| 分页、`partial_success``status=unknown` 或恢复 | [contracts.md](references/contracts.md) |
| 复杂 JSONML、长文或局部精准读写 | [create](references/doc/doc-create.md) / [read](references/doc/doc-read.md) / [update](references/doc/doc-update.md) |
| block/划词评论/媒体/封面/背景高级参数 | [block](references/doc/doc-block.md) / [comment](references/doc/doc-comment.md) / [media](references/doc/doc-media.md) |
| 导出/导入失败恢复 | [export](references/doc/doc-export.md) / [import](references/doc/doc-import.md) |
常规 `+create``+fetch``+update` append/overwrite、`+export``+import` 禁止读取 reference;禁预加载/连读。
## 错误最短路径
1. 零命中、多候选、类型不明或分页不完整:停止写入,展示候选或 continuation;禁止默认第一项。
2. Help 不参与选路;先读一次精确 leaf Schema。仅真实 `unknown flag`/契约漂移查一次 leaf Help`unknown command` 只查一次 shortcut 清单,禁止试探后缀和 `dws doc --help | grep/head`
3. `REVISION_CONFLICT`:重新读取当前 revision,展示差异;未经用户确认不得改成无 revision 覆盖。
4. `doc_write_commit_unknown`:先回读;禁止自动重试创建或追加。
5. 认证、权限或 profile 错误:只读 `dingtalk-shared` 对应 reference,禁底层命令绕过。
6. 导出/媒体失败:保留稳定 ID 后停止;禁网络请求/安装依赖/本地文档库兜底。
## 产品边界
- 姓名/工号/部门/职责找人或解析 userId → `dingtalk-aisearch`;已有完整 userId 补详情才用 `dingtalk-contact`
- 普通文件/目录存储与上传下载 → `dingtalk-drive`;“放进/附到这篇文档”是正文附件 → `doc +media-insert`;在线转换 → `doc +import`
- 文档节点复制、移动、模板另存 → `dingtalk-drive +copy/+move`doc 同名命令仅兼容
- 知识库空间、节点层级和成员管理 → `dingtalk-wiki`
- 原生 `.md` 文件读取和编辑 → `dingtalk-misc`
- `axls` / `able` → 对应电子表格或多维表 Skill
- 持续监听文档事件 → `dingtalk-misc`
@@ -0,0 +1,57 @@
# 文档组合任务
本页只描述跨步骤组合任务。单一创建、读取、更新、导出或媒体操作直接使用根 Skill Golden Route,不要加载本页。
## 创建成稿
1. 把最终正文写到工作目录内的 `body.md``body.json`
2. 执行一次 `dws doc +create --name "<标题>" --content @body.md --format json`
3. 使用返回的 `verified/nodeId` 判断完成;只有用户明确要求额外结构验收时再做局部 `+fetch`
复杂排版才按需读取 style/JSONML Reference;执行入口仍使用 `+create`。禁止调用已删除的创建脚本。
## 导入文件
```bash
dws doc +import --file ./report.docx --folder <FOLDER_ID> --format json
```
导入是服务端格式转换。禁止先读文件内容再走 `create + update`。单纯保存普通文件切换到 `dingtalk-drive` 上传。
## 查找并读取
```bash
dws doc +fetch --query "<唯一标题>" --format json
```
`+fetch --query` 会跨页解析唯一在线文字文档。零命中、多候选或类型不是 `adoc` 时停止并按返回候选消歧;不要继续调用 drive/wiki/aisearch 做无界穷举。
## 更新章节
1. `+fetch --node <ID> --scope section|keyword` 读取最小必要上下文。
2. 普通编辑用 `+update`;重要覆盖用 `+checkpoint-update`
3. 使用 shortcut 的验证结果;`partial_success/unknown` 时只恢复缺失步骤。
## 从模板创建
1. `+template-search --query <名称>`
2. 唯一命中才取得 `templateId`;多候选要求用户选择。
3. `+create-from-template --template-id <ID>` 只执行一次。
模板保形复制已有文档是另一种任务:使用 drive copy 复制源文档,再只修改副本。禁止 `doc read → doc create` 重建富格式模板。
## 导出并归档
1. 在线文字文档使用 `+export` 导出到工作目录。
2. 检查 `localPath/sizeBytes`
3. 用户要求归档到钉盘时,再用 `dingtalk-drive` 上传该文件。
导出失败不得安装本地转换依赖或直接下载临时 URL。
## 文档转消息/待办
1. 使用局部 `+fetch` 提取必要内容。
2. 在目标产品中解析真实用户/群/任务标识。
3. 根据目标产品 Runtime gate 确认后写入。
跨产品步骤必须复用稳定 ID;失败后不能重放已经成功的文档写入步骤。
@@ -0,0 +1,40 @@
# Doc Runtime Contracts
本文只定义 Agent 需要稳定消费的文档运行时语义。字段以 leaf Schema 和实际 JSON 返回为准;不要从终端展示文本反推状态。
## 目标
文档目标至少保留 `nodeId`、资源类型、canonical URL(服务返回时)和容器信息。名称或标题不是稳定身份。按自然标题解析出现零命中、多候选或分页不完整时,写操作必须停止。
## 写操作状态
| status | 含义 | 后续动作 |
|---|---|---|
| `success` | 所有计划步骤完成,且需要验证的内容已经回读 | 可以向用户报告完成 |
| `partial_success` | 已发生部分副作用,后续步骤失败 | 检查 `steps``compensation`,不得重放成功步骤 |
| `unknown` | 请求已发出但无法确定服务端是否提交 | 先回读目标;创建和追加禁止自动重试 |
| `retryable` | 服务端明确业务执行尚未开始,且允许重试 | 遵循 `retry_after_seconds`,最多有界重试一次 |
| `failed` | 已确认没有完成目标动作 | 根据 `retryable``actions` 和 details 决定是否重试 |
进程退出码为零不能替代业务证据。采用 `doc.operation.v1` 的 shortcut 回执固定提供 `contractVersion/ok/status/complete/operation/steps/data/warnings/compensation``target/failures/verification` 只有实际操作返回时才能消费,不是通用字段。`+import` 使用现有导入回执,成功时检查 `success=true``taskId``documentUrl``documentName``documentType`,不要要求不存在的 `status/steps`。业务 `status` 不应与框架外层 `outcome` 混为一谈。
稳定返回 ID/任务 ID 只用于定位目标、恢复流程或继续查询,不能单独证明写操作成功。成功证据优先级从高到低为:与该操作匹配的明确服务端成功终态 → 契约要求的写后读回匹配 → 仅 transport/退出码;异步任务必须使用返回的任务 ID 查询到成功终态。只有成功终态成立且所有必要回读均匹配时才报告成功;`partial_success``unknown`、未完成任务或仅返回资源/任务 ID 都不得报告完成。Runtime 已给出充分回读证据时,不为“再确认一次”重复请求。
## 分页与完整性
列表和搜索结果需要区分:
- `complete=true`:已证明覆盖请求范围;
- `hasMore=true`:仍有后续页,必须保留有效 continuation
- `truncated=true`:成功返回因 `max_pages``max_items` 边界停止,并通过 `stopReason` 说明原因;
- 后续页请求失败、cursor 缺失/停滞/循环时返回 typed error;部分结果位于 `details.items`,并保留 `details.status=partial_success``complete=false``reason/page/nextCursor/count`。不要期待成功回执的空 `failures[]` 承载这类错误,也不要把 timeout 写成 `truncated`
只有 `complete=true` 且没有未处理失败时,才能把结果描述为完整集合。用户问“有哪些”“全部”或要求“列出结果”时,使用返回的 cursor/continuation 继续取页直至完整;只有用户明确要示例、前 N 条或接受部分结果时才可提前停止,并说明已覆盖范围。分页应复用原查询与过滤条件,不得换命令或放宽关键词。
## 错误
结构化错误至少区分 validation、not_found、ambiguous、type_mismatch、revision_conflict、confirmation_required、permission_denied、partial_success 和 commit_unknown,并提供 failure stage、retryable 与已有的 `actions`/details。权限、认证和参数错误直接进入 `failed`;写请求只有明确 `execution_started=false` 才能进入 `retryable`,其余传输异常进入 `unknown``retryable=false` 表示自动重放不安全,不代表用户检查状态后永远不能重新发起。不要发明 `suggestedAction` 或顶层 `nextCommand` 字段。
## 安全落盘
导出、下载和媒体预览只使用工作目录内相对路径。完成条件包括临时文件写入成功、内容校验、文件关闭和原子发布;失败时不得留下看似最终产物的半成品。默认 no-clobber,覆盖必须由用户显式选择。
@@ -0,0 +1,82 @@
# dingtalk-doc 低频能力索引
本页只在根 Skill 的 Golden Route 和精确任务 Reference 都无法选路时加载。它不是创建、读取或更新任务的前置必读,也不要求预加载样式、JSONML 或完整产品帮助。
## 高频入口
| 意图 | 推荐命令 | 精确 Reference |
|---|---|---|
| 搜索在线文字文档 | `dws doc +search --query <关键词>` | [doc-info.md](doc/doc-info.md) |
| 读取正文或局部内容 | `dws doc +fetch --node <ID或URL>` | [doc-read.md](doc/doc-read.md) |
| 创建并写入 | `dws doc +create` | [doc-create.md](doc/doc-create.md) |
| 追加、覆盖、block 编辑 | `dws doc +update` | [doc-update.md](doc/doc-update.md) |
| 重要更新与恢复点 | `dws doc +checkpoint-update` | [doc-update.md](doc/doc-update.md) |
| 导出本地文件 | `dws doc +export` | [doc-export.md](doc/doc-export.md) |
| 导入为在线对象 | `dws doc +import` | [doc-import.md](doc/doc-import.md) |
| 列出文档空间/文件夹下的文档 | `dws doc +list --workspace <WS_ID>` | 知识库层级管理切 `dingtalk-wiki` |
| 评论聚合与操作 | `dws doc +review/+comment-*` | [doc-comment.md](doc/doc-comment.md) |
| 媒体插入、列表、下载 | `dws doc +media-*` | [doc-media.md](doc/doc-media.md) |
命令已选定但参数不确定时读取精确 leaf Schema;只有 Cobra flag 与 Schema 冲突时读取精确 leaf Help。不要加载 `dws doc --help` 或完整 Catalog 代替选路。
## 模板
只有名称时先只读搜索:
```bash
dws doc +template-search --query "周报" --source PUBLIC --format json
```
来源按用户原话守门:“我的模板/我这边”只查 `MY`,明确“公开/钉钉模板库”才查 `PUBLIC`;不得为了凑结果跨来源扩展。未指定来源时保持默认 `MY`
- `selection.status=resolved`:取唯一候选的 `templateId`
- `selection.status=not_found`:报告零命中后停止;不得改用语义不相干的热门模板,更不得擅自创建文档。
- `selection.status=selection_required`:展示候选并要求用户选择,禁止默认第一项。
若返回 `hasMore=true`,沿原 query/source 使用 cursor 继续搜索;只有服务端返回完整结果后才能判断零命中或完整候选集。
选定后只创建一次:
```bash
dws doc +create-from-template --template-id <TEMPLATE_ID> --name "我的周报" --format json
```
禁止通过实际创建多个候选文档来预览模板。`+create-from-template --query` 仅保留兼容,不能作为新的 Agent Golden Route。
## 历史版本
```bash
dws doc +version-save --node <DOC_ID> --format json
dws doc +version-list --node <DOC_ID> --limit 20 --format json
dws doc +version-revert --node <DOC_ID> --version <N> --format json
```
`+version-save/list/revert` 分别用于快照、浏览和恢复,命中后直接执行,不预读 Help。`+history-*` 仅兼容已有调用,不用于新的 Agent 选路。重要内容更新优先使用 `+checkpoint-update`,不要手工编排保存、写入和回读。回滚必须确认,以 leaf Schema 与 Runtime gate 为准。
只读某个历史版本的内容时,用 `dws doc +fetch --node <DOC_ID> --version <N>`(版本号同样来自 `+version-list``0` 表示初始版本,需要文档编辑权限);整体恢复到历史版本才用 `+version-revert`(危险操作,需确认)。互联网公开文档(含密码保护)的读取见 [doc-read.md](doc/doc-read.md) 的 `--password`
## 权限与分享
- 查询或聚合权限:`+inspect --include-permissions`
- 新增、变更、移除权限:`+access-grant/+access-change/+access-revoke`
- 授权后发链接:`+grant-and-share`
- 姓名、群聊或组织 profile 多候选时必须停止消歧。
## 高级原子能力
以下能力在 shortcut 未公开所需参数时才使用原子 leaf:
- 特殊 JSONML block、白板或样式字段
- 需要原始 MCP 响应的诊断
- shortcut 明确返回 capability unavailable 的低频操作
进入高级通道前只读取对应 leaf Schema 和一个精确 Reference。原子命令不是 shortcut 失败后的自动兜底,不能用来绕过权限、确认、类型或路径检查。
## 本地与跨产品边界
- 普通文件上传、下载、目录和文件树:`dingtalk-drive`
- 知识库空间与节点层级:`dingtalk-wiki`
- 原生 Markdown 文件:`dingtalk-misc`
- `axls` / `able`:对应电子表格或多维表 Skill
导出或媒体错误必须保留稳定 ID 后停止。禁止隐式执行 `curl/wget``pip/brew install`、Python Office 库、本地 OCR 或手写 HTTP 来伪造 DWS 任务结果。
@@ -0,0 +1,39 @@
# 块级编辑 Golden Route
## 普通路径
先读取最小必要范围并取得稳定 block ID:
```bash
dws doc +fetch --node <DOC_ID> --detail with-ids --scope section --start-block-id <KNOWN_BLOCK_ID> --format json
```
再按意图使用统一更新入口:
```bash
dws doc +update --node <DOC_ID> --command block_replace --block-id <BLOCK_ID> --content "新内容"
dws doc +update --node <DOC_ID> --command block_insert_after --after-block-id <BLOCK_ID> --content "补充内容"
dws doc +update --node <DOC_ID> --command block_delete --block-id <BLOCK_ID>
```
`block-id` 必须来自真实 `+fetch --detail with-ids``+review` 或原子 block 列表返回。确认、写入与验证统一由 `+update` 处理,正常成功不追加整篇回读。
## 标题块例外
新增 heading 必须使用结构化插入,不能把 `# 标题` 作为 Markdown 传给 `block_replace`。例如在首块前新增一级标题:
```bash
dws doc block insert --node <DOC_ID> --heading "发布说明 v1.0" --level 1 --ref-block <FIRST_BLOCK_ID> --where before --format json
```
插入回执有稳定新 block ID 时,用 `dws doc block list --node <DOC_ID> --block-id <NEW_BLOCK_ID> --format json` 定点验证;回执只有插入 index 时,只执行一次 `dws doc block list --node <DOC_ID> --content-format jsonml --format json`,按该 index 验证 `blockType=heading``heading.level="heading-1"` 和标题文字。`block list` 没有 `--limit`,禁止 Help/试错;一次完整 JSONML 列表已满足结构验收时立即终止。CLI 写入仍使用数值参数 `--level 1`。修改现有标题才使用 `doc block update --block-id ... --heading ... --level ...`;不要用普通文字替换改变块类型。
## 富结构专家路径
只有需要 shortcut 未公开的 callout、分栏、复杂表格或 JSONML element 参数时:
1. 按需读取 [JSONML schema](format/doc-jsonml-schema.md) 或 [cookbook](format/doc-jsonml-cookbook.md),不要两者都预加载;用户已明确结构时优先 cookbook 的可执行样例。
2. 读取精确 `doc block` leaf Schema,确认当前 flags。
3. 用原子 block 命令只改目标块;JSONML update 的 uuid 必须等于目标 block ID。
图片和附件不得手写临时 URL 或 OSS 请求,统一走 [`doc-media.md`](doc-media.md) 的 `+media-insert/+media-download`。删除与覆盖的确认以 Runtime gate 为准,示例不得预填 `--yes`
@@ -0,0 +1,29 @@
# 文档评论 Golden Route
## Review 聚合
```bash
dws doc +review --node <DOC_ID_OR_URL> --format json
```
需要一次看到未解决评论、引用原文和 block 上下文时优先使用 `+review`。从真实返回取得 `commentKey``blockId`,禁止按数组位置或猜测 ID。
## 精确评论动作
```bash
dws doc +comment-list --node <DOC_ID> --resolve-status unresolved --limit 20 --format json
dws doc +comment-list --node <DOC_ID> --limit 20 --cursor <NEXT_TOKEN> --format json
dws doc +comment-create --node <DOC_ID> --content "这里需要补充证据"
dws doc +comment-create --node <DOC_ID> --selection "计划下周发布" --content "请确认日期"
dws doc +comment-reply --node <DOC_ID> --comment-key <COMMENT_KEY> --content "已补充"
dws doc +comment-update --node <DOC_ID> --comment-key <COMMENT_KEY> --content "修订后的意见"
dws doc +comment-delete --node <DOC_ID> --comment-key <COMMENT_KEY>
```
- 全文与划词评论统一使用公开的 `+comment-create`:优先传唯一 `--selection`;已知真实 block 时传 `--block-id --start --end`CLI 自动回读并校验 `selectedText`。不要选用未公开的 `+comment-create-inline`
- `--mention` 传单个 uid 或逗号分隔列表,例如 `--mention 550582,123456`;不要传 JSON 数组。
- 列表用 `--limit` 控制页大小(兼容 `--page-size`),有 `nextToken` 时原样传给 `--cursor`;不能把单页当作全部评论。
- 创建、回复、删除等写操作执行前消费 leaf Schema `confirmation`;需确认时先询问,示例不得预填 `--yes`
- 删除不可恢复,必须核对 node 与 commentKey。部分或未知结果不得自动重试。
只有 shortcut 未公开必要字段时,才读取精确原子 leaf Schema;不要加载整份评论参考或产品 Catalog 来猜参数。
@@ -0,0 +1,54 @@
# 创建在线文字文档
本页只处理钉钉在线文字文档(`adoc`)创建。普通文件上传走 `dingtalk-drive`,表格和多维表分别走对应产品。
## 唯一推荐入口
```bash
dws doc +create --name "<文档名>" [--content "短文本"] [--folder <ID> | --workspace <ID>] --format json
dws doc +create --name "<文档名>" --content @body.md --format json
dws doc +create --name "<文档名>" --content @body.json --doc-format jsonml --format json
```
- 统一输入协议:已有或临时文件先暂存到当前工作目录后传 `@相对文件`;单次生成文本可用 `--content -` 从 stdin 读取。
- `@file` 禁止绝对路径和 `..` 逃逸;不要直接引用宿主临时目录。
- `--name` 是文档名称;默认不要再在正文开头重复同名 H1,只有用户明确要求正文一级标题时才保留。
- JSONML 顶层必须是数组。用户已经明确要求 callout、代码块、表格等具体富结构时,直接构造一份完整 JSONML 并只调用一次 `+create`;这不是开放式风格设计,不读取 `style/`。字段不确定时只读 [JSONML cookbook](format/doc-jsonml-cookbook.md),不再连读 create/update/style。
`+create` 负责创建、长 Markdown 分片和最终回读验证。正常成功结果至少包含:
```json
{
"status": "success",
"complete": true,
"data": {
"nodeId": "...",
"verified": true
}
}
```
## 结果处理
- `status=success``verified=true`:可以报告创建完成,并保留真实 `nodeId`/URL。
- `status=partial_success`:文档或部分分片已经创建;按 `steps` 回读现状,禁止重跑整条创建。
- `status=unknown`:服务端可能已经提交;先定位并读取文档,禁止自动重试。
- 没有真实 `nodeId` 或写回执时,禁止声称“已创建”。
## 创作与执行策略
采用 Plan → Execute → Observe → Iterate,将循环放在本地内容与定点修正上,不把长文拆成一串远程创建/追加:
1. **Plan**:明确受众、目的、范围和结构;正文由一个主上下文串行维护,避免按章节并行生成造成重复、矛盾和语气漂移。
2. **Draft**:短内容可直接传;多行、长文或含特殊字符时先形成 cwd 内相对文件。用户未要求富结构时优先 Markdown;只有样式/引用/嵌套结构确有必要时才用 JSONML。
3. **Execute**:只调用一次 `+create`。明确富结构用一份完整 JSONML 一次创建;DWS Runtime 会记录每步并回读验证。Agent 不采用“先骨架、再逐块远程插入”流程,以减少网络次数、顺序错误和 commit-unknown 面。
4. **Observe**:先检查回执中的 `nodeId``verified``steps` 和失败状态。回执已证明完整时不重复拉全文;需要内容质量验收时,只用 `+fetch --scope section/keyword` 读取待检查部分。
5. **Iterate**:后续修正复用同一 `nodeId`,按 [`doc-update.md`](doc-update.md) 做最小 block/文本修改,禁止重新创建整篇。
交付前检查标题是否重复、段落是否连贯、编号是否统一;只有真实行列数据才使用表格,富组件服务于理解而不是装饰。明确字数要求时应在写入前完成本地统计,不能凭模型估算宣称达标。
## 高级通道
只有 shortcut 未公开所需的底层参数或需要原始响应时,才读取精确 leaf Schema 后使用 `dws doc create`。不要因为熟悉旧参数就默认退回原子命令,也不要使用已删除的 Python 创建脚本。
复杂排版按需读取 [doc-style-guideline.md](style/doc-style-guideline.md);需要 JSONML 起稿判定与起稿前设计规划时读 [doc-create-workflow.md](style/doc-create-workflow.md);两者只按当前需求选一,命令选路仍以本页的 `+create` 为准。
@@ -0,0 +1,30 @@
# 导出在线文档
## 唯一推荐入口
```bash
dws doc +export --node <DOC_ID_OR_URL> --export-format docx --output ./exports/ --format json
dws doc +export --node <DOC_ID_OR_URL> --export-format markdown --output ./document.md --format json
dws doc +export --node <DOC_ID_OR_URL> --export-format pdf --output ./document.pdf --format json
```
`+export` 一次完成提交、轮询和原子下载。输出路径必须位于工作目录内,默认 no-clobber;目标已存在时返回 `LOCAL_FILE_EXISTS`,更换 `--output` 路径重试,没有覆盖用 flag。
`--export-format` 必填;全局 `--format json` 只控制 CLI 输出,不能代替业务导出格式。禁止依赖默认 docx,也不要猜测 `--type`
异步状态 `INIT``PROCESSING` 都表示任务仍可继续轮询;只有终态失败才停止,不能把 `INIT` 当导出失败。
## 类型边界
- 在线文字文档(`adoc`)转为 docx/markdown/pdf`+export`
- 已存在的普通文件原样下载:切换到 `dingtalk-drive`,使用 drive download。
- 不要为了导出先把正文读到本地再重新生成文件。
## 失败处理
- 提交前权限/认证失败:原样报告并停止;不要尝试同义底层命令。
- 已返回 `jobId` 后轮询失败或超时:保留 `jobId`,只用 `+export-get --job-id <JOB_ID>` 恢复查询,禁止重新提交导出。
- 下载阶段失败:保留 `jobId` 和目标相对路径,用 `+export-get --job-id <JOB_ID> --output ./目标文件` 通过同一安全下载器恢复;不要直接 `curl` 临时 URL。
- 禁止安装 `pandoc``python-docx` 或其他依赖来隐式伪造导出结果。只有用户明确改成“本地生成文件”任务时,才可作为一个新的独立工作流处理。
只有需要显式接管异步 job 的恢复场景才使用 `+export-submit/+export-get`;正常导出不得手工编排它们。
@@ -0,0 +1,28 @@
# 导入本地文件:`+import` Golden Route
## 唯一推荐入口
```bash
dws doc +import --file ./report.docx --format json
dws doc +import --file ./report.docx --folder <FOLDER_ID> --format json
dws doc +import --file ./notes.md --workspace <WORKSPACE_ID> --name "会议纪要" --format json
```
`+import` 一次完成创建会话、上传、确认转换和终态轮询。支持 `doc/docx/xls/xlsx/md/txt/xmind/mark`,文件大小上限 20MB。转换成功回执包含 `success=true``taskId``documentUrl``documentName``documentType`,不包含 `status``steps`;成功返回即表示本次内部轮询已到终态。超时或中断时保留错误中的 `taskId`,只查询原任务。
## 本地文件边界
- `--file` 只接受当前工作目录内已存在的相对路径;禁止绝对路径、`..` 或符号链接逃逸。
- `--folder``--workspace` 都是可选位置且互斥。对支持在线转换的格式,两者都不传时,Runtime 先读取当前组织唯一 `orgSpace`,把其 `rootFolderId` 作为 `targetFolderId` 后再创建导入会话;若空间为零个、多个、无权限或缺少 `rootFolderId`,会在写入前停止并要求显式提供目标,禁止选择第一项或猜 ID。`--folder` 取值首选用户提供的 alidocs URL 或真实 `nodeId`;不得使用普通文件 `drive info` 返回的父级 `folderId`
- CLI 负责上传和格式转换。不要先用 Python/Office 库解析文件,不要安装本地依赖来伪造在线导入结果,也不要手写 HTTP 上传。
- 白名单外格式(如 HTML/PDF)自动改走原文件上传,返回 `fallback=upload``converted=false`;不得报告成已经转换为可编辑在线文档。
- “在线改/协作编辑/转在线文档”属于导入转换;“存着/归档/保留原文件/不要转换”属于 `dingtalk-drive` 纯上传。目标为文档空间时,纯上传使用 `drive upload --workspace <WORKSPACE_ID>`,不要因容器叫“文档空间”就误报为在线文档。
## 失败处理
- 发起前的格式、大小或路径校验失败:修正输入后再执行。
- 已返回 `taskId` 后超时或中断:保留该 `taskId`,读取精确恢复命令 Schema 后只查询原任务;禁止重新提交导入。
- 返回状态未知时原样报告,不把本地文件内容改走 `+create`,因为这会改变格式保真和任务语义。
- 白名单外格式如果目标是钉盘而非文档空间,切换到 `dingtalk-drive` 上传。
正常导入不得手工编排原子 `doc import` 子步骤。只有 shortcut 未公开必要的恢复参数时,才按精确 leaf Schema 使用原子查询命令。
@@ -0,0 +1,12 @@
# 查看文档信息:`+inspect` Golden Route
```bash
dws doc +inspect --node <DOC_ID_OR_URL> --format json
dws doc +inspect --node <DOC_ID_OR_URL> --include-permissions --include-history --format json
```
只打开任务需要的 `--include-style/--include-permissions/--include-history/--include-media/--include-comments`,不要默认全取。正文读取使用 `+fetch`,不要用信息查询替代正文接口。
检查返回的真实 `nodeId`、类型、URL 和各可选步骤。若 URL 指向的不是在线文字文档,停止 doc 流程并切换到 drive、sheet、aitable、slides 或 wiki;禁止凭 URL 外形猜类型。
只有 `+inspect` 未公开所需的底层字段或必须获得原始响应时,才读取精确 leaf Schema 后使用原子信息命令。
@@ -0,0 +1,46 @@
# 文档正文媒体
## Golden Route
```bash
# 列出图片和附件,取得真实 resourceId/blockId
dws doc +media-list --node <DOC_ID> --format json
# 插入工作目录内的本地文件
dws doc +media-insert --node <DOC_ID> --file ./image.png --format json
# 下载到工作目录,默认不覆盖
dws doc +media-download --node <DOC_ID> --resource-id <RESOURCE_ID> --output ./downloads/ --format json
# 只为临时查看下载到受控临时目录
dws doc +media-preview --node <DOC_ID> --resource-id <RESOURCE_ID> --format json
```
## 封面与背景 Shortcut
```bash
dws doc +resource-update --node <DOC_ID> --file ./cover.png --format json
dws doc +resource-download --node <DOC_ID> --output ./cover.png --format json
dws doc +resource-delete --node <DOC_ID> --format json
dws doc +background-update --node <DOC_ID> --color "#E8F2FE" --format json
dws doc +background-delete --node <DOC_ID> --format json
```
封面不是正文媒体 block;背景仅接受 `#RRGGBB` 纯色。设置/清除后用一次 `+inspect --include-style` 验证,禁止为这些已知能力查询 shortcut Catalog。
## 稳定 ID 与结果
- `resourceId``blockId``nodeId` 必须来自真实 media/block 返回,不能从标题或本地文件名猜测。
- 插入成功回执在 `data.blockId` 返回已回读验证的媒体块 ID;后续定位只复用该 `blockId`。回执不提供相邻空块字段,不得据此猜测或自动删除其他块。插入回执成功后禁止重传媒体。下载必须检查 `localPath``sizeBytes > 0`
- 下载输出只接受工作目录内相对路径,默认 no-clobber。
- 删除源文件是独立的破坏性本地操作,不属于媒体下载;只有用户明确要求且下载验证成功后才能执行。
## 失败最短路径
- `download_doc_attachment` 失败:保留 `nodeId/resourceId` 和服务端错误,停止。
- 临时链接过期:重新调用 `+media-download` 获取新链接,由 CLI 内部下载。
- 禁止把 `+fetch` 返回的临时 OSS URL 交给 `curl/wget`
- 禁止安装图片、Office 或 Python 依赖作为隐式降级。
- 不得把纯数字 dentryId 当作 drive folder 重试;需要文件树定位时切换到 `dingtalk-drive` 并使用真实 dentryUuid。
只有 shortcut 缺少必要的底层定位参数时,才读取精确原子 leaf Schema;不要把 `doc media insert/download` 作为默认入口。
@@ -0,0 +1,56 @@
# 读取文档:`+fetch` Golden Route
## 唯一推荐入口
```bash
dws doc +fetch --node <DOC_ID_OR_URL> --format json
dws doc +fetch --query "项目周报" --scope keyword --keyword "结论" --format json
dws doc +fetch --node <PUBLIC_URL> --password <ACCESS_PASSWORD> --format json
dws doc +fetch --node <DOC_ID> --version <VERSION> --format json
```
- 已知 ID 或 URL:传 `--node`
- 只知道标题:传 `--query`;跨页解析必须唯一命中,否则停止并要求用户选择。
- `--node``--query` 必须且只能提供一个。
- 默认 `--detail simple --scope full`,适合普通阅读,避免加载不必要的 JSONML。
## 互联网公开文档与历史版本
- **互联网公开文档**:公开链接直接传 `--node`;文档开启了密码保护时,通过 `--password <ACCESS_PASSWORD>` 提供访问密码,普通文档无需传入。密码只进入读取请求,不会回显在读取结果里(`--dry-run` 预览会包含所传参数,注意输出环境)。
- **历史版本**`--version <N>` 读取指定历史版本内容;版本号从 `dws doc +version-list` 获取,`0` 表示文档初始版本,需要文档编辑权限(EDITOR 及以上);缺省读最新版。注意区分:`revision` 是文档编辑版本号(JSONML 读取响应返回、供 `+update --expected-revision` 条件写使用),不是历史版本号,`+fetch` 不支持 `--revision`
- **跨组织文档**:非互联网公开的跨组织文档,`+fetch` 整体被组织边界拦截(提示「不支持跨组织访问数据」),最新版与历史版本内容都读不到;互联网公开(含密码)文档的 `+fetch` 不受影响,但 `+version-list` 与写操作仍会被拦截,版本号无法从列表获取。
- 只要看历史内容、不打算恢复时用 `--version`;要把文档整体恢复到历史版本才用 `+version-revert`(危险操作,需确认)。
## 局部读取
```bash
dws doc +fetch --node <DOC_ID> --scope outline --detail with-ids --format json
dws doc +fetch --node <DOC_ID> --scope section --start-block-id <BLOCK_ID> --detail full --format json
dws doc +fetch --node <DOC_ID> --scope range --start-block-id <A> --end-block-id <B> --detail full --format json
dws doc +fetch --node <DOC_ID> --scope tags --tags table,img --detail full --format json
dws doc +fetch --node <DOC_ID> --scope keyword --keyword "风险|结论" --context-before 120 --context-after 240 --format json
```
只有需要块 ID、revision 或 JSONML 保真结构时才提高 `--detail`;先读取最小必要范围,避免把整篇大文档放入上下文。
整篇读取使用默认 scope 或 `--scope full``full` 不是关键词,禁止写成 `--keyword full`
## 最小读取漏斗
按用户已经给出的线索选最短路径,不先拉全文:
1. 已知稳定 ID/URL,且任务确实涉及整篇:直接默认 `full + simple`,一次返回 Markdown。
2. 用户给出具体术语、错误码或同义词:直接 `keyword``foo|bar` 是 OR`context-before/after` 是字符数。该模式会缩小 Agent 输出与 token,但当前 Runtime 仍需取得正文后做本地投影,不把它误述为减少上游传输。
3. 用户指向某章但没有 block ID:先 `outline --detail with-ids`,再用返回的真实标题 ID 执行 `section`。通常两次小结果比反复处理整篇更稳定。
4. 已知起止 block:直接 `range`;只找表格、图片等结构时用 `tags`
5. 只有确需全文总结、全局一致性检查或整篇保真改写时,才读取完整内容。
`simple` 用于阅读;`with-ids` 用于定位下一次写操作;`full` 只用于必须保留样式、引用或 JSONML 结构的编辑。局部结果只能证明所选范围,不得据此声称已检查整篇。
## 后续路由
- 读后修改:把稳定的 `nodeId` 交给 [`doc-update.md`](doc-update.md) 的 `+update``+checkpoint-update`
- 附件/图片:先用 [`doc-media.md`](doc-media.md) 的 `+media-list` 取得稳定 `resourceId`,再下载;不要复用正文中的临时签名 URL。
- 富结构专家编辑:确实需要原始 JSONML 或 shortcut 未公开的参数时,先读取精确 leaf Schema,再使用原子 `doc read`/`doc block`
禁止把原子 `doc read` 当作默认入口,也不要在读取后无条件整篇回写。筛选结果只用于读取,不能把虚拟 fragment 容器整体写回文档;同一次任务里已拿到稳定 `nodeId` 后,后续调用直接复用,避免再次按标题搜索。
@@ -0,0 +1,87 @@
# 更新在线文字文档
## 唯一推荐入口
普通追加、覆盖和 block 编辑统一使用 `+update`
`--command` 只接受下列枚举值,不接受 JSON、自然语言或拼接子命令;动作参数必须分别传给 `--content/--old/--new/--block-id/--before-block-id/--after-block-id`
```bash
dws doc +update --node <DOC_ID> --command append --content "补充说明" --format json
dws doc +update --node <DOC_ID> --command append --content @append.md --format json
dws doc +update --node <DOC_ID> --command overwrite --content @full.md --format json
dws doc +update --node <DOC_ID> --command overwrite --doc-format jsonml --content @full.json --expected-revision <REVISION> --format json
dws doc +update --node <DOC_ID> --command block_insert_before --before-block-id <BLOCK_ID> --content "发布说明" --heading-level 1 --format json
dws doc +update --node <DOC_ID> --command block_replace --block-id <BLOCK_ID> --content "新内容" --format json
```
重要覆盖或明确要求恢复点时使用:
```bash
dws doc +checkpoint-update --node <DOC_ID> --mode overwrite --content @full.md --format json
```
`+checkpoint-update` 负责保存版本、写入和回读,不要手工编排 `version save → update → read`
## 动作与输入
| `--command` | 用途 | 必要参数 |
|---|---|---|
| `append` | 末尾追加 | `--content` |
| `overwrite` | 整篇覆盖 | `--content`JSONML 可加 `--expected-revision` 做服务端原子条件写 |
| `block_insert_before` | 在指定 block 前插入段落或标题 | `--before-block-id --content`;标题加 `--heading-level 1..6` |
| `block_insert_after` | 在指定 block 后插入段落或标题 | `--after-block-id --content`;标题加 `--heading-level 1..6` |
| `block_replace` | 替换指定 block | `--block-id --content` |
| `block_delete` | 删除指定 block | `--block-id` |
| `str_replace` | 唯一普通文本替换 | `--old --new` |
| `block_copy_insert_after` | 复制 block 后插入 | `--block-id --after-block-id` |
统一输入协议:已有或临时文件先暂存到当前工作目录后传 `@相对文件`;单次生成文本可用 `--content -` 从 stdin 读取。禁止绝对路径、`..`,也不要猜测 `--content-file``--content-format``replace_all`
block ID 必须来自 `+fetch --detail with-ids` 或真实 block 列表,禁止编造。
`--expected-revision` 只允许 `--command overwrite --doc-format jsonml`。Markdown、append 和 block 接口没有服务端原子 revision 契约,禁止用写前读取模拟乐观锁。
## 新增结构化标题
“新增/插入标题”与“修改现有块文字”不是同一动作。`+update block_replace --content "# 标题"` 会替换原块并可能落成普通 paragraph,不能用它冒充 heading。
需要在已有首块前新增标题时,先用 `+fetch --detail with-ids` 取得真实首块 ID,再走结构化插入:
```bash
dws doc block insert --node <DOC_ID> --heading "发布说明 v1.0" --level 1 --ref-block <FIRST_BLOCK_ID> --where before --format json
```
插入回执有新 block ID 时定点 `block list --block-id`;只有插入 index 时执行一次完整 `block list --content-format jsonml` 并按 index 验证 `blockType=heading`、回读投影 `heading.level="heading-1"` 和文字。`block list` 没有 `--limit`,禁止通过 Help 猜参数;CLI 写入仍用 `--level 1`。只核对可见文字不算结构验收。已有标题改级别或改文字时使用结构化 `doc block update --heading/--level`;更多块边界见 [`doc-block.md`](doc-block.md)。
## 最小改写决策
| 已知条件 | 推荐路径 | 成本与成功率理由 |
|---|---|---|
| 用户明确要求在末尾追加 | 直接 `append` | 不为找末尾先拉全文;需要语气衔接时只读末节 |
| 已知唯一旧文本与新文本 | 直接 `str_replace --old --new` | 省掉 block 解析;旧文本不唯一时 Runtime 必须失败,不放宽匹配 |
| 指定章节但没有 block ID | `+fetch outline``+fetch section --detail with-ids` → block 动作 | 两个小读取换取稳定锚点,避免全文 token 和误改相邻章节 |
| 已知真实 block ID | 直接 `block_replace/delete/insert_before/insert_after` | 最小副作用;不改无关 block |
| 多个待删除 block ID 已全部取得 | 在同一个 fail-fast 工具轮中按顺序执行全部 `block_delete` | 中间不重读 Reference、不重复 fetch;任一失败立即停止并保留未执行清单 |
| 多处富结构保真修改 | `+fetch --detail full` 后定点 JSONML 更新 | 保留图片、附件、引用、表格和样式;不要从 Markdown 有损重建 |
| 整篇重要覆盖 | `+checkpoint-update --mode overwrite` | 自动保存恢复点、执行并回读;普通 overwrite 只用于明确不需恢复点的场景 |
同一篇文档的正文由一个主上下文串行维护:Plan(确定最小变更)→ Execute(一次写)→ Observe(先消费回执)→ Iterate(只修未达标部分)。不要按章节并行写同一文档,也不要每次迭代都重新读取全文或 Schema。
## Block ID 生命周期与保真
- `block_replace` 成功后 Runtime 使用同一 `blockId` 回读验证,该 ID 可继续作为锚点;验证失败时先局部 `+fetch` 核对现状。`block_delete` 成功后旧 ID 失效,不得继续复用。
- `block_insert_before` / `block_insert_after` / `block_copy_insert_after` 后,原锚点通常仍可识别,但新 block 的 ID 必须来自真实返回或局部回读,禁止按顺序猜测。
- `str_replace` 的简单行内替换通常不要求重新取 ID;若后续依赖块结构,仍以局部回读为准。
- 从 Markdown 读取后覆盖整篇可能丢失图片、附件、@人/@文档、评论锚点、表格样式和嵌套块。只改局部时使用 block 手术;确需整篇保真改写时使用 `full` JSONML,并以 `--expected-revision` 防止覆盖并发修改。
## 确认与验证
- `+update``+checkpoint-update` 当前都要求用户确认。目标、动作、内容范围或参数变化后必须重新确认;只有发现本地文档与 live leaf 漂移时才查一次精确 Schema,禁止每次写入都重复发现。
- `+update` 已负责回读验证。除非结果为 `unknown` 或任务需要额外结构验收,不要再做一次全篇读取。
- `doc_write_verification_failed` 表示写入已经发生,必须先读取现状;禁止直接重复写入。
- `partial_success` 只恢复未完成步骤,不能重放成功步骤。
## 高级通道
只有 shortcut 未公开底层参数或必须保留原始响应时,才读取精确 leaf Schema 后使用 `dws doc update` / `doc block`。富结构保真按需读取 [doc-update-workflow.md](style/doc-update-workflow.md),但执行入口仍优先使用 `+update/+checkpoint-update`
@@ -0,0 +1,469 @@
# JSONML Cookbook
> 本文档提供 JSONML **结构范例**。正常执行入口是 `+create --content @./相对文件 --doc-format jsonml` 与 `+update --content @./相对文件 --doc-format jsonml`;原子 `doc create/update/block` 仅用于 shortcut 未公开字段的专家路径,使用前读取精确 leaf Schema。
> 所有示例均基于真实文档 serialize 输出验证。节点结构详细定义见 [doc-jsonml-schema.md](./doc-jsonml-schema.md)。
> 合法节点类型和属性的权威参考为 `wukong/products/jsonml-schema-v2.json`。
## 决策型文档骨架范例(doc create 用)
以下是一个"方案对比汇报"的完整 JSONML 文件内容,展示摘要 callout + 彩色表格 + 状态高亮。
**可保存到工作目录内的 `./drafts/<name>.json`,再用 `dws doc +create --name "..." --content @./drafts/<name>.json --doc-format jsonml` 创建。**
```json
["root", {},
["container", {"subType": "colorBlocks", "metadata": {"bgcolor": "#E8F5E9", "border": "left"}},
["p", {}, ["span", {"data-type": "text"},
["span", {"data-type": "leaf", "bold": true, "sz": 14, "szUnit": "pt"}, "✅ 推荐方案 A:上线快、依赖已有流程"]
]],
["p", {}, ["span", {"data-type": "text"},
["span", {"data-type": "leaf"}, "主要风险:权限配置需补 | 决策时限:本周五前"]
]]
],
["h2", {}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "方案对比"]]],
["table", {"colsWidth": [120, 200, 200]},
["tr", {},
["tc", {"fill": "#F5F5F5"}, ["p", {}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf", "bold": true}, "维度"]]]],
["tc", {"fill": "#E8F5E9"}, ["p", {}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf", "bold": true}, "方案 A(推荐)"]]]],
["tc", {}, ["p", {}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf", "bold": true}, "方案 B"]]]]
],
["tr", {},
["tc", {}, ["p", {}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "上线周期"]]]],
["tc", {}, ["p", {}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf", "color": "#2E7D32"}, "1 周"]]]],
["tc", {}, ["p", {}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "3 周"]]]]
],
["tr", {},
["tc", {}, ["p", {}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "风险"]]]],
["tc", {}, ["p", {}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "低"]]]],
["tc", {}, ["p", {}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf", "color": "#C62828"}, "高:需新流程审批"]]]]
]
],
["h2", {}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "下一步"]]],
["p", {}, ["span", {"data-type": "text"},
["span", {"data-type": "leaf"}, "• "],
["span", {"data-type": "leaf", "highlight": "#FFF9C4"}, "待确认"],
["span", {"data-type": "leaf"}, " @负责人 完成权限配置"]
]]
]
```
**设计要点**
- 根节点固定 `"root"`,不是 `"body"`
- 摘要用 `container`callout),`metadata.bgcolor` 选浅绿表示"推荐结论"
- 表格用 `table → tr → tc`(无 th/td),表头底色用 `tc``"fill"` 属性
- 关键数据着色用 leaf 的 `"color"`(绿=好 / 红=风险)
- 状态标记用 leaf 的 `"highlight"`(黄=待确认、绿=完成、红=阻塞)
- uuid 必须显式提供——CLI 不再自动补充
## ⚠️ JSONML 结构严格约束(生成时必须遵守)
每个节点是一个 JSON 数组:`[tagName, attributes?, ...children]`
- **第一个元素**是字符串,表示标签名(如 `"p"`, `"h1"`, `"span"`, `"container"`
- **第二个元素**(可选)是一个 JSON 对象,表示属性(如 `{"uuid": "abc"}`)。如果无属性,可以直接进入子节点
- **随后的元素**是子节点,可以是纯字符串(仅限 leaf span 内),也可以是另一个 JSONML 数组
- **所有 `[` 必须有对应 `]`,所有 `{` 必须有对应 `}`,数组元素之间用 `,` 分隔,最后一个元素后不加 `,`**
常见 LLM 生成错误(务必避免):
| 错误类型 | 示例 | 后果 |
|---------|------|------|
| 缺少闭合 `]` | `["p", {}, ["span", ...]` | JSON 解析失败 |
| 多余逗号 | `["p", {},]` | JSON 解析失败 |
| 缺少逗号 | `["p", {} ["span"]]` | JSON 解析失败 |
| 引号不匹配 | `["p", {"uuid": "abc}]` | JSON 解析失败 |
## 文本节点格式(最重要)
钉钉文档的文本是**三层结构**,不是裸字符串:
```json
["p", {"uuid": "xxx"},
["span", {"data-type": "text"},
["span", {"data-type": "leaf"}, "文本内容"]
]
]
```
- **text 容器**`["span", {"data-type": "text"}, ...leaves]` — 包裹所有文本 leaf
- **leaf 节点**`["span", {"data-type": "leaf", ...格式属性}, "文字"]` — 实际文本,可带 bold/italic 等
- 一个 block 节点只有一个 text 容器,但可以有多个 leaf(不同格式的文字片段)
**简写**:无格式纯文本可以省略格式属性:
```json
["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "纯文本"]]
```
## 核心规则
1. **每个 block 节点应有 uuid**`["tag", {"uuid": "唯一ID"}, ...children]`
- insert 时必须提供 uuid(可自行生成任意唯一字符串,后端会自动分配正式 uuid)
- update 时 uuid **必须**与 `--block-id` 一致
- uuid 必须显式提供,不再自动补充
2. **文本必须用 span + leaf 三层结构**,不要直接写裸字符串
-`["p", {"uuid": "x"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "hello"]]]`
-`["p", {"uuid": "x"}, "hello"]` — validator 会报错,请手动包成 ✅ 的形式
- ⚠️ `["p", {"uuid": "x"}, ["text", {}, "hello"]]``text` 是历史 inline tagvalidator 不会报错,但建议改写为 ✅ 形式以与 `dws doc read --content-format jsonml` 的输出保持一致
3. **attrs 对象必须存在**(即使为空):`["p", {}, ...]` 不能省略 `{}`
> **严格模式(缺省)**:CLI 不做结构修复,裸字符串等错误会被 validator 以 `JSONPath + Suggestion` 形式逐条报错。如果输入来自 LLM 且可能有 JSON 语法错误(缺括号/逗号),用 `--fix-jsonml` 启用 JSON 语法修复。
## 段落 (p)
```bash
# 纯文本段落
dws doc block insert --node <DOC_ID> --content-format jsonml \
--element '["p", {"uuid": "new1"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "这是一段普通文本"]]]'
# 带格式文本(多个 leaf
dws doc block insert --node <DOC_ID> --content-format jsonml \
--element '["p", {"uuid": "new2"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf", "bold": true}, "加粗"], ["span", {"data-type": "leaf"}, "普通"], ["span", {"data-type": "leaf", "italic": true}, "斜体"]]]'
# 多行文本(每行一个 p,同一 p 内的多个 span 不会换行)
dws doc block insert --node <DOC_ID> --content-format jsonml \
--element '["p", {"uuid": "line1"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf", "bold": true}, "第一行标题"]]]' \
--element '["p", {"uuid": "line2"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "第二行正文内容"]]]'
# 带链接(link 是与 text 并列的子节点)
dws doc block insert --node <DOC_ID> --content-format jsonml \
--element '["p", {"uuid": "new3"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "请访问"]], ["a", {"href": "https://example.com"}, "链接文字"]]'
```
**leaf 支持的格式属性**
- `bold: true` — 加粗
- `italic: true` — 斜体
- `underline: {"value": "single"}` — 下划线(value: `single`/`dash`/`wave`/`double`/`none`,可选 `color`
- `strike: true` — 删除线
- `dstrike: true` — 双删除线
- `color: "#ff0000"` — 文字颜色(`#rrggbb` 格式)
- `highlight: "#ffff00"` — 高亮背景色
- `sz: 14` / `szUnit: "pt"` — 字号(szUnit 默认 `"px"`,推荐显式写 `"pt"`
`fonts: {"ascii": "Arial", "eastAsia": "SimHei"}` — 字体(四分区:ascii/hAnsi/cs/eastAsia,值必须使用 font-family 名称,见下方字体表)
- `vertAlign: "superscript"` — 上标(`"subscript"` 下标,`"baseline"` 基线)
- `spacing: 2` — 字间距(单位 pt
**字体名称映射**`fonts` 字段必须使用 font-family 值,不能写中文名):
| 用户说法 | font-family 值 | 用户说法 | font-family 值 |
|---------|---------------|---------|---------------|
| 宋体 | `SimSun` | 黑体 | `SimHei` |
| 微软雅黑 | `Microsoft YaHei` | 微软雅黑UI | `Microsoft YaHei UI` |
| 仿宋 | `FangSong` | 仿宋_GB2312 | `FangSong_GB2312` |
| 楷体 | `KaiTi` | 楷体_GB2312 | `KaiTi_GB2312` |
| 等线 | `DengXian` | 新宋体 | `NSimSun` |
| 宋体-简 | `SimSun SC` | 宋体-繁 | `SimSun TC` |
| 黑体-简 | `Heiti SC` | 黑体-繁 | `Heiti TC` |
| 华文宋体 | `STSong` | 华文黑体 | `STHeiti` |
| 华文楷体 | `STKaiti` | 华文仿宋 | `STFangsong` |
| 华文中宋 | `STZhongsong` | 华文行楷 | `STXingkai` |
| 华文隶书 | `STLiti` | 华文新魏 | `STXinwei` |
| 华文细黑 | `STXihei` | 华文琥珀 | `STHupo` |
| 苹方-简 | `PingFang SC` | 苹方-繁 | `PingFang TC` |
| 苹方-港 | `PingFang HK` | 冬青黑-简 | `Hiragino Sans GB` |
| 兰亭黑-简 | `Lantinghei SC` | 兰亭黑-繁 | `Lantinghei TC` |
| 凌慧体-简 | `LingWai SC` | 幼圆 | `YouYuan` |
| 思源黑体 | `Source Han Sans CN` | 思源宋体 | `Source Han Serif CN` |
| 思源等宽 | `Source Han Mono SC` | 思源黑体Regular | `Source Han Sans CN Regular` |
| 阿里普惠体2.0 | `"Alibaba PuHuiTi 2.0"` | 阿里普惠体3.0 | `"Alibaba PuHuiTi 3.0"` |
| 钉钉进步体 | `DingTalk JinBuTi` | Adobe仿宋 | `Adobe 仿宋 Std` |
| 方正小标宋_GBK | `FZXiaoBiaoSong-B05` | 方正小标宋简体 | `FZXiaoBiaoSong-B05S` |
| 方正黑体 | `FZHei-B01S` | 方正楷体 | `FZKai-Z03S` |
| 方正仿宋 | `FZFangSong-Z02S` | 方正仿宋_GBK | `FZFangSong-Z02` |
| PMingLiU | `PMingLiU` | — | — |
**英文字体**font-family 值即为字体名):
`Arial``Calibri``Cambria``Centaur``Comfortaa``Comic Sans MS``Courier New``Franklin Gothic``Garamond``Georgia``Helvetica``Impact``Lora``Lucida Sans``Merriweather``Montserrat``Nunito``Oswald``Playfair Display``Roboto``Spectral``Times New Roman``Trebuchet MS``Verdana`
> **规则**:优先从上表匹配;用户指定的字体不在列表时,使用该字体在操作系统中的真实 font-family 名称(如"更纱黑体" → `Sarasa Gothic SC`)。
**leaf 组合示例**
```json
["span", {"data-type": "leaf", "bold": true, "color": "#C62828", "sz": 16, "szUnit": "pt"}, "红色加粗大字"]
["span", {"data-type": "leaf", "strike": true, "color": "#9E9E9E"}, "已废弃内容"]
["span", {"data-type": "leaf", "vertAlign": "superscript"}, "[1]"]
["span", {"data-type": "leaf", "fonts": {"ascii": "Courier New", "eastAsia": "DengXian"}}, "等宽字体"]
```
**段落级排版属性**(写在 p/h1-h6 的 attrs 上):
- `jc: "center"` — 对齐(`left`/`center`/`right`/`both`/`justify`
- `spacing: {"line": 1.5, "lineRule": "auto"}` — 行距(lineRule=auto 时 line 为倍数:1=单倍、1.5=1.5倍、2=双倍)
- `spacing: {"before": 12, "after": 8}` — 段前/段后间距(单位 pt
- `ind: {"firstLine": 32}` — 首行缩进(≈ 2 中文字符)
- `ind: {"left": 96}` — 左缩进
**段落排版示例**
```json
["p", {"uuid": "p1", "jc": "center"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "居中段落"]]]
["p", {"uuid": "p2", "spacing": {"line": 1.5, "lineRule": "auto"}}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "1.5倍行距"]]]
["p", {"uuid": "p3", "spacing": {"line": 2, "lineRule": "auto", "before": 12, "after": 8}}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "双倍行距+段前后间距"]]]
```
## 标题 (h1-h6)
```bash
dws doc block insert --node <DOC_ID> --content-format jsonml \
--element '["h1", {"uuid": "new4"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "一级标题"]]]'
dws doc block insert --node <DOC_ID> --content-format jsonml \
--element '["h2", {"uuid": "new5"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "二级标题"]]]'
# 更新已有标题
dws doc block update --node <DOC_ID> --block-id <BLOCK_ID> --content-format jsonml \
--element '["h2", {"uuid": "<BLOCK_ID>"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "修改后的标题"]]]'
```
## 列表 (list)
列表在 JSONML 中是 **带 `list` 属性的 `p` 节点**,不是独立 tag。
```bash
# 无序列表项
dws doc block insert --node <DOC_ID> --content-format jsonml \
--element '["p", {"uuid": "li1", "list": {"listId": "mylist1", "level": 0, "isOrdered": false}}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "无序列表第一项"]]]'
# 有序列表项(仅第一项设 start,后续项不设,系统自动递增)
dws doc block insert --node <DOC_ID> --content-format jsonml \
--element '["p", {"uuid": "li2", "list": {"listId": "mylist2", "level": 0, "isOrdered": true, "start": 1}}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "有序列表第一项"]]]'
dws doc block insert --node <DOC_ID> --content-format jsonml \
--element '["p", {"uuid": "li2b", "list": {"listId": "mylist2", "level": 0, "isOrdered": true}}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "有序列表第二项"]]]'
# 缩进子项(level: 1
dws doc block insert --node <DOC_ID> --content-format jsonml \
--element '["p", {"uuid": "li3", "list": {"listId": "mylist2", "level": 1, "isOrdered": true}}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "子列表项"]]]'
# 待办列表(checkbox
dws doc block insert --node <DOC_ID> --content-format jsonml \
--element '["p", {"uuid": "li4", "list": {"listId": "todo1", "level": 0, "isOrdered": false, "isTaskList": true, "isChecked": false}}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "待办事项"]]]'
```
## 引用 (blockquote)
引用是 **带 `quote` 属性的 `p` 节点**
```bash
dws doc block insert --node <DOC_ID> --content-format jsonml \
--element '["p", {"uuid": "q1", "quote": true}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "这是一段引用文字"]]]'
```
## 高亮块 / Callout (container)
```bash
# 蓝色高亮块
dws doc block insert --node <DOC_ID> --content-format jsonml \
--element '["container", {"uuid": "co1", "subType": "colorBlocks", "metadata": {"bgcolor": "#E8F2FE", "border": "#B3D4FC"}}, ["p", {"uuid": "co1p1"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "这是一段提示内容"]]]]'
# 黄色警告块(多段落)
dws doc block insert --node <DOC_ID> --content-format jsonml \
--element '["container", {"uuid": "co2", "subType": "colorBlocks", "metadata": {"bgcolor": "#FFF2CC", "border": "#FFE599"}}, ["p", {"uuid": "co2p1"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "⚠️ 注意事项"]]], ["p", {"uuid": "co2p2"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "请仔细阅读以下内容"]]]]'
```
**常用颜色预设**
| 含义 | bgcolor | border |
|------|---------|--------|
| 信息(蓝) | `#E8F2FE` | `#B3D4FC` |
| 成功(绿) | `#E6F7E6` | `#B7EB8F` |
| 警告(黄) | `#FFF2CC` | `#FFE599` |
| 危险(红) | `#FFF1F0` | `#FFA39E` |
| 紫色 | `#F3E8FF` | `#D3ADF7` |
## 代码块 (code)
代码内容存在 attrs.code 中,不需要 text/leaf 子节点。
```bash
dws doc block insert --node <DOC_ID> --content-format jsonml \
--element '["code", {"uuid": "cd1", "syntax": "javascript", "code": "function hello() {\n return \"world\";\n}"}]'
dws doc block insert --node <DOC_ID> --content-format jsonml \
--element '["code", {"uuid": "cd2", "syntax": "python", "code": "print(\"hello\")", "showLineNumber": true, "theme": "dracula"}]'
```
## 分割线 (hr)
```bash
dws doc block insert --node <DOC_ID> --content-format jsonml \
--element '["hr", {"uuid": "hr1"}]'
```
## 表格 (table)
> colsWidth 单位为 **pt**(页宽约 650pt)。如配合 `tblW: {"type": "pct"}` 则为百分比权重。
```bash
# 2行2列表格(各列 200pt
dws doc block insert --node <DOC_ID> --content-format jsonml \
--element '["table", {"uuid": "tb1", "colsWidth": [200, 200]}, ["tr", {"uuid": "tr1"}, ["tc", {"uuid": "tc1", "colSpan": 1, "rowSpan": 1}, ["p", {"uuid": "tcp1"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "标题A"]]]], ["tc", {"uuid": "tc2", "colSpan": 1, "rowSpan": 1}, ["p", {"uuid": "tcp2"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "标题B"]]]]], ["tr", {"uuid": "tr2"}, ["tc", {"uuid": "tc3", "colSpan": 1, "rowSpan": 1}, ["p", {"uuid": "tcp3"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "数据1"]]]], ["tc", {"uuid": "tc4", "colSpan": 1, "rowSpan": 1}, ["p", {"uuid": "tcp4"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "数据2"]]]]]]]'
```
> 表格较复杂时建议写入文件后用 `--element "$(cat table.json)"` 传入。
## 图片 (img)
> `img` 是 inline 元素,必须包裹在 `p` 段落中才能作为 block 插入。
```bash
dws doc block insert --node <DOC_ID> --content-format jsonml \
--element '["p", {"uuid": "p-img1"}, ["img", {"uuid": "img1", "src": "https://example.com/photo.png", "width": 400, "height": 300}], ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, ""]]]'
```
## 分栏布局 (columns)
分栏复用 table tag,通过 `sr: true` 区分。分栏的 `tc` 可设置 `fill`(背景色)和 `border`(边框)属性提升视觉效果。
```bash
# 两栏布局(带背景色)
dws doc block insert --node <DOC_ID> --content-format jsonml \
--element '["table", {"uuid": "col1", "sr": true, "colsWidth": [300, 300]}, ["tr", {"uuid": "coltr"}, ["tc", {"uuid": "coltc1", "fill": "#EEF6FF", "vAlign": "top"}, ["p", {"uuid": "colp1"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "左栏内容"]]]], ["tc", {"uuid": "coltc2", "fill": "#FFF3E0", "vAlign": "top"}, ["p", {"uuid": "colp2"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "右栏内容"]]]]]]'
```
**分栏视觉属性**
- `fill` — 单元格背景色(推荐淡色,如 `#EEF6FF` / `#FFF8E1` / `#F3E5F5`
- `border` — 边框配置(可选)
- 分栏建议始终设置 `fill` 背景色,纯白底分栏视觉上与普通段落无异,读者无法感知分栏结构
## 嵌入块 (embed)
通用文件/iframe 嵌入。`embed` 是 void 块,仅含 attrs,无子节点。
```bash
# 嵌入文件预览
dws doc block insert --node <DOC_ID> --content-format jsonml \
--element '["embed", {"uuid": "em1", "name": "design.pdf", "type": "pdf", "src": "https://example.com/design.pdf", "size": 524288, "viewType": "preview", "previewSize": {"height": 600}}]'
```
**关键 attrs**
- `src`**必填**)— 资源 URL
- `type``pdf` / `xlsx` / `html`
- `name` — 展示名
- `previewSize.height` — 预览高度(px
## 在线视频 (onlineVideo)
外链视频(B 站 / 优酷 / 自定义 mp4 等)。
```bash
dws doc block insert --node <DOC_ID> --content-format jsonml \
--element '["onlineVideo", {"uuid": "ov1", "src": "https://player.bilibili.com/player.html?aid=12345", "type": "bilibili", "poster": "https://example.com/poster.jpg"}]'
```
**关键 attrs**
- `src`(**必填**)— 视频播放页或 mp4 URL
- `type` — 平台标识(`bilibili` / `youku` / `mp4` 等)
- `poster` — 封面图 URL
## 卡片 (card)
群名片 / 应用卡片等富交互卡片。`cardType` 决定渲染形态,`metadata` 内容随类型变化。
```bash
# 群名片
dws doc block insert --node <DOC_ID> --content-format jsonml \
--element '["card", {"uuid": "cd1", "cardType": "groupChatCard", "metadata": {"id": "63953109506", "name": "测试组", "inviteUrl": "https://qr.dingtalk.com/...", "expires": 1810865989}}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, ""]]]'
```
> 注意:服务端 serialize 出来的 card 通常带一个空的 span/leaf 占位子节点,建议保留以避免反序列化差异。
## 目录 (toc)
目录块 attrs 上必带 4 个字段:`title` / `mode` / `styles` / `content`。如果不知道怎么填,**最简方式**是先在 web 端插入一个 toc,然后 `block list --content-format jsonml` 把现成结构拿下来改。
```bash
dws doc block insert --node <DOC_ID> --content-format jsonml --element '
["toc", {
"uuid": "toc1",
"title": "目录",
"mode": "outline",
"styles": {
"global": {"maxLevel": 5, "bgColor": "#F0EBF7", "css": {}},
"title": {"font": "DingTalk JinBuTi", "color": "#6940A5", "numbering": true, "css": {"fontWeight": "normal"}},
"item": {"symbol": "disc", "css": {}}
},
"content": []
}]'
```
**关键 attrs**
- `mode``outline`(大纲)/ `column`(分栏)
- `styles.global.maxLevel` — 最大显示层级
- `content` — 目录条目数组;**留空数组即可**,服务端会基于文档 heading 自动重建
## 引用块 (refblock)
引用另一文档的内容片段。`refblock` 像容器一样包子节点。
```bash
dws doc block insert --node <DOC_ID> --content-format jsonml --element '
["refblock", {"uuid": "rb1", "docKey": "OTHER_DOC_NODE_ID", "refblockUUID": "BLOCK_UUID_IN_OTHER_DOC"},
["p", {"uuid": "rb1p1"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "(引用预览内容,服务端会回填)"]]]
]'
```
**关键 attrs**
- `docKey` — 被引用文档的 nodeId
- `refblockUUID` — 被引用块的 uuid
> 引用块的子节点是「快照」,真实内容由服务端按 docKey/refblockUUID 拉取覆写。
## 表格单元格嵌套块 (tableCell with nested blocks)
`tc` 的子节点是**块级节点**(不仅是 `p`)。可以塞多个段落、列表、代码块、甚至嵌套表格。
```bash
# 单元格内含多段落 + 代码块
dws doc block insert --node <DOC_ID> --content-format jsonml --element '
["table", {"uuid": "tbn1", "colsWidth": [400]},
["tr", {"uuid": "tbn1r1"},
["tc", {"uuid": "tbn1c1", "colSpan": 1, "rowSpan": 1},
["p", {"uuid": "tbn1p1"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "标题段落"]]],
["p", {"uuid": "tbn1p2", "list": {"listId": "tbn1list", "level": 0, "isOrdered": false}}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "列表项 1"]]],
["p", {"uuid": "tbn1p3", "list": {"listId": "tbn1list", "level": 0, "isOrdered": false}}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "列表项 2"]]],
["code", {"uuid": "tbn1code", "syntax": "bash", "code": "echo hello"}]
]
]
]'
```
**规则**
- `tc` 子节点 **必须是块节点数组**,不能直接放 `span` 或裸字符串
- 至少包含一个 `["p", {...}, ...]`(即使空也要),否则单元格无法渲染光标
- 单元格可嵌套 `table`,但嵌套时务必保证内层每个 `tc` 也满足上述规则
## Update 操作注意事项
1. **uuid 必须与 --block-id 一致**
2. Update 是**整块替换**,不是 patch — 需提供完整节点结构
3. 推荐流程:先 `block list --content-format jsonml --block-id <ID>` 获取当前结构,修改后写回
```bash
# 典型 update 流程
# 1. 获取当前结构
dws doc block list --node <DOC_ID> --content-format jsonml --block-id <BLOCK_ID>
# 2. 修改后写回(uuid 不变)
dws doc block update --node <DOC_ID> --block-id <BLOCK_ID> --content-format jsonml \
--element '["p", {"uuid": "<BLOCK_ID>"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "修改后的内容"]]]'
```
## 常见错误
| 错误写法 | 问题 | 正确写法 |
|---------|------|---------|
| `["p", {}, "文字"]` | 裸字符串。validator 会报 `段落子节点不能是裸字符串`,请手动包成右侧形式 | `["p", {}, ["span", {"data-type":"text"}, ["span", {"data-type":"leaf"}, "文字"]]]` |
| `["p", {}, ["text", {}, "文字"]]` | `text` 是历史 inline tagvalidator 不报错但服务端实际渲染的 canonical 形式是 span/leaf;为与 `doc read` 输出一致,建议改写 | 同上,用 span + data-type |
| `["callout", {}, ...]` | 不存在 callout tag | `["container", {"subType": "colorBlocks", ...}, ...]` |
| `["list", {}, ...]` | 不存在 list tag | `["p", {"list": {...}}, ...]` |
| `["blockquote", {}, ...]` | 不存在 blockquote tag | `["p", {"quote": true}, ...]` |
| `["ul", {}, ["li", ...]]` | 不存在 ul/li tag | 多个 `["p", {"list": {...}}, ...]` |
## 快捷模板
为方便使用,以下是最常用节点的最小完整模板:
```
纯文本段落: ["p", {"uuid":"U"}, ["span", {"data-type":"text"}, ["span", {"data-type":"leaf"}, "TEXT"]]]
标题: ["h2", {"uuid":"U"}, ["span", {"data-type":"text"}, ["span", {"data-type":"leaf"}, "TITLE"]]]
代码块: ["code", {"uuid":"U", "syntax":"LANG", "code":"CODE"}]
分割线: ["hr", {"uuid":"U"}]
```
@@ -0,0 +1,526 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "DingTalk Document JSONML Body Schema",
"description": "钉钉文档 body JSONML 的结构校验 schema",
"type": "array",
"items": { "$ref": "#/definitions/blockNode" },
"definitions": {
"blockNode": {
"oneOf": [
{ "$ref": "#/definitions/paragraph" },
{ "$ref": "#/definitions/heading" },
{ "$ref": "#/definitions/hr" },
{ "$ref": "#/definitions/table" },
{ "$ref": "#/definitions/code" },
{ "$ref": "#/definitions/container" },
{ "$ref": "#/definitions/embed" },
{ "$ref": "#/definitions/onlineVideo" },
{ "$ref": "#/definitions/card" },
{ "$ref": "#/definitions/toc" },
{ "$ref": "#/definitions/refblock" }
]
},
"inlineContent": {
"oneOf": [
{ "type": "string" },
{ "$ref": "#/definitions/textNode" },
{ "$ref": "#/definitions/link" },
{ "$ref": "#/definitions/image" },
{ "$ref": "#/definitions/mention" },
{ "$ref": "#/definitions/formula" },
{ "$ref": "#/definitions/sticker" },
{ "$ref": "#/definitions/inlineCode" },
{ "$ref": "#/definitions/br" }
]
},
"textNode": {
"type": "array",
"minItems": 3,
"maxItems": 3,
"items": [
{ "type": "string", "const": "text" },
{ "$ref": "#/definitions/textMarks" },
{ "type": "string" }
]
},
"textMarks": {
"type": "object",
"properties": {
"bold": { "type": "boolean" },
"italic": { "type": "boolean", "const": true },
"strike": { "type": "boolean" },
"dstrike": { "type": "boolean" },
"underline": {
"type": "object",
"properties": {
"value": { "type": "string", "enum": ["single", "dash", "wave", "double", "none"] },
"color": { "type": "string" }
},
"required": ["value"]
},
"color": { "type": "string" },
"highlight": { "type": "string" },
"shd": {
"type": "object",
"properties": {
"val": { "type": "string" },
"color": { "type": "string" },
"fill": { "type": "string" }
}
},
"sz": { "type": "number" },
"szUnit": { "type": "string", "enum": ["px", "pt"], "default": "px" },
"fonts": {
"type": "object",
"properties": {
"ascii": { "type": "string" },
"hAnsi": { "type": "string" },
"cs": { "type": "string" },
"eastAsia": { "type": "string" }
}
},
"vertAlign": { "type": "string", "enum": ["superscript", "subscript", "baseline"] },
"spacing": { "type": "number", "description": "字间距,单位 pt" }
},
"additionalProperties": false
},
"paragraphAttrs": {
"type": "object",
"properties": {
"jc": { "type": "string", "enum": ["left", "center", "right", "both", "distribute", "justify"] },
"ind": {
"type": "object",
"properties": {
"left": { "type": "number" },
"start": { "type": "number" },
"leftChars": { "type": "number" },
"right": { "type": "number" },
"end": { "type": "number" },
"rightChars": { "type": "number" },
"hanging": { "type": "number" },
"hangingChars": { "type": "number" },
"firstLine": { "type": "number" },
"firstLineChars": { "type": "number" }
},
"additionalProperties": false
},
"spacing": {
"type": "object",
"properties": {
"line": { "type": "number" },
"before": { "type": "number" },
"beforeLines": { "type": "number" },
"beforeAutospacing": { "type": "boolean" },
"after": { "type": "number" },
"afterLines": { "type": "number" },
"afterAutospacing": { "type": "boolean" },
"lineRule": { "type": "string", "enum": ["atLeast", "auto", "exact"] }
},
"additionalProperties": false
},
"shd": {
"type": "object",
"properties": {
"val": { "type": "string" },
"color": { "type": "string" },
"fill": { "type": "string" }
}
},
"blockquote": { "type": "boolean" },
"list": { "$ref": "#/definitions/listProperties" },
"refs": { "type": "array", "items": { "type": "string" } }
},
"additionalProperties": true
},
"listProperties": {
"type": "object",
"required": ["listId", "level"],
"properties": {
"listId": { "type": "string" },
"level": { "type": "integer", "minimum": 0 },
"isOrdered": { "type": "boolean", "default": false },
"isTaskList": { "type": "boolean", "default": false },
"isChecked": { "type": "boolean" },
"isCanceled": { "type": "boolean" },
"start": { "type": "integer", "minimum": 1 },
"listStyleType": { "type": "string" },
"hideSymbol": { "type": "boolean" },
"listStyle": {
"type": "object",
"properties": {
"format": { "type": "string", "enum": ["bullet", "decimal", "decimalZero", "lowerLetter", "lowerRoman", "upperLetter", "upperRoman", "chineseCountingThousand"] },
"text": { "type": "string" },
"align": { "type": "string", "enum": ["left", "start", "end", "center", "both", "right", "distribute"] }
},
"required": ["format", "text", "align"]
}
},
"additionalProperties": true
},
"paragraph": {
"type": "array",
"minItems": 2,
"items": [
{ "type": "string", "const": "p" },
{ "$ref": "#/definitions/paragraphAttrs" }
],
"additionalItems": { "$ref": "#/definitions/inlineContent" }
},
"heading": {
"type": "array",
"minItems": 2,
"items": [
{ "type": "string", "enum": ["h1", "h2", "h3", "h4", "h5", "h6"] },
{ "$ref": "#/definitions/paragraphAttrs" }
],
"additionalItems": { "$ref": "#/definitions/inlineContent" }
},
"hr": {
"type": "array",
"minItems": 1,
"maxItems": 2,
"items": [
{ "type": "string", "const": "hr" },
{
"type": "object",
"properties": {
"type": { "type": "string", "enum": ["single", "dotted", "dashed", "double", "wave", "doubleWave", "dotDash", "dotDotDash", "custom", "thickThinSmallGap", "thinThickThinMediumGap", "dashStroked", "dashDotStroked", "widthDoubleWave", "widthWave", "roundDot"] },
"sz": { "type": "number", "default": 1 },
"color": { "type": "string" },
"width": { "oneOf": [{ "type": "number" }, { "type": "string" }] }
},
"additionalProperties": true
}
]
},
"table": {
"type": "array",
"minItems": 2,
"items": [
{ "type": "string", "const": "table" },
{
"type": "object",
"properties": {
"colsWidth": { "type": "array", "items": { "type": "number" } },
"sr": { "type": "boolean" },
"jc": { "type": "string" },
"spacing": { "type": "number" }
}
}
],
"additionalItems": { "$ref": "#/definitions/tableRow" }
},
"tableRow": {
"type": "array",
"minItems": 2,
"items": [
{ "type": "string", "const": "tr" },
{ "type": "object" }
],
"additionalItems": { "$ref": "#/definitions/tableCell" }
},
"tableCell": {
"type": "array",
"minItems": 2,
"items": [
{ "type": "string", "const": "tc" },
{
"type": "object",
"properties": {
"colSpan": { "type": "integer", "minimum": 1, "default": 1 },
"rowSpan": { "type": "integer", "minimum": 1, "default": 1 },
"fill": { "type": "string" },
"vAlign": { "type": "string", "enum": ["top", "middle", "bottom"], "default": "middle" }
},
"additionalProperties": true
}
],
"additionalItems": { "$ref": "#/definitions/blockNode" }
},
"code": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": [
{ "type": "string", "const": "code" },
{
"type": "object",
"properties": {
"code": { "type": "string", "default": "" },
"syntax": { "type": "string", "default": "plaintext" },
"theme": { "type": "string", "enum": ["default", "light", "dracula", "github", "cobalt", "atomOneDark", "oneLightPro", "nightOwl", "githubDark", "realDracula"], "default": "default" },
"wrap": { "type": "boolean", "default": true },
"showLineNumber": { "type": "boolean", "default": true },
"title": { "type": "string", "maxLength": 1000 },
"fold": { "type": "boolean", "default": false }
},
"additionalProperties": true
}
]
},
"container": {
"type": "array",
"minItems": 2,
"items": [
{ "type": "string", "const": "container" },
{
"type": "object",
"required": ["subType"],
"properties": {
"subType": { "type": "string" },
"metadata": { "type": "object" }
}
}
],
"additionalItems": { "$ref": "#/definitions/blockNode" }
},
"embed": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": [
{ "type": "string", "const": "embed" },
{
"type": "object",
"properties": {
"name": { "type": "string" },
"type": { "type": "string" },
"src": { "type": "string" },
"size": { "type": "number" },
"viewType": { "type": "string" },
"previewSize": { "type": "object", "properties": { "height": { "type": "number" } } }
},
"additionalProperties": true
}
]
},
"onlineVideo": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": [
{ "type": "string", "const": "onlineVideo" },
{
"type": "object",
"properties": {
"src": { "type": "string" },
"type": { "type": "string" },
"poster": { "type": "string" }
},
"additionalProperties": true
}
]
},
"card": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": [
{ "type": "string", "const": "card" },
{
"type": "object",
"properties": {
"cardType": { "type": "string" },
"metadata": { "type": "object" },
"height": { "type": "number" }
},
"additionalProperties": true
}
]
},
"toc": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": [
{ "type": "string", "const": "toc" },
{
"type": "object",
"required": ["title", "mode", "styles", "content"],
"properties": {
"title": { "type": "string" },
"mode": { "type": "string", "enum": ["outline", "column"] },
"styles": {
"type": "object",
"required": ["global", "title", "item"],
"properties": {
"global": {
"type": "object",
"required": ["maxLevel", "bgColor"],
"properties": {
"maxLevel": { "type": "integer", "minimum": 1 },
"bgColor": { "type": "string" },
"css": { "type": "object" }
}
},
"title": {
"type": "object",
"properties": {
"font": { "type": "string" },
"color": { "type": "string" },
"numbering": { "type": "boolean" },
"css": { "type": "object" }
}
},
"item": {
"type": "object",
"properties": {
"symbol": { "type": "string", "enum": ["disc", "none"] },
"css": { "type": "object" }
}
}
}
},
"content": {
"type": "array",
"items": { "$ref": "#/definitions/tocItem" }
}
}
}
]
},
"tocItem": {
"type": "object",
"required": ["uuid", "anchorId", "level", "children"],
"properties": {
"uuid": { "type": "string" },
"anchorId": { "oneOf": [{ "type": "string" }, { "type": "null" }] },
"level": { "type": "integer" },
"children": { "type": "array", "items": { "$ref": "#/definitions/tocItem" } },
"text": { "type": "string" }
}
},
"refblock": {
"type": "array",
"minItems": 2,
"items": [
{ "type": "string", "const": "refblock" },
{
"type": "object",
"properties": {
"docKey": { "type": "string" },
"refblockUUID": { "type": "string" }
},
"additionalProperties": true
}
],
"additionalItems": { "$ref": "#/definitions/blockNode" }
},
"link": {
"type": "array",
"minItems": 2,
"items": [
{ "type": "string", "const": "a" },
{
"type": "object",
"properties": {
"href": { "type": "string" },
"cardInfo": { "type": "object" },
"metadata": { "type": "object" }
},
"additionalProperties": true
}
],
"additionalItems": { "$ref": "#/definitions/inlineContent" }
},
"image": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": [
{ "type": "string", "const": "img" },
{
"type": "object",
"properties": {
"src": { "type": "string" },
"width": { "type": "number" },
"height": { "type": "number" },
"rectClip": { "type": "object" },
"rotation": { "type": "number" },
"radius": { "type": "number" },
"shadow": { "type": "string" }
},
"additionalProperties": true
}
]
},
"mention": {
"type": "array",
"minItems": 2,
"items": [
{ "type": "string", "const": "span" },
{
"type": "object",
"required": ["data-type"],
"properties": {
"data-type": { "type": "string", "const": "mention" },
"id": { "type": "string" },
"name": { "type": "string" },
"login": { "type": "string" },
"metadata": { "type": "object" }
},
"additionalProperties": true
}
],
"additionalItems": { "type": "string" }
},
"formula": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": [
{ "type": "string", "const": "tag" },
{
"type": "object",
"required": ["tagType", "metadata"],
"properties": {
"tagType": { "type": "string", "const": "formula" },
"metadata": {
"type": "object",
"required": ["formula"],
"properties": {
"formula": { "type": "string" }
}
}
}
}
]
},
"sticker": {
"type": "array",
"minItems": 2,
"items": [
{ "type": "string", "const": "span" },
{
"type": "object",
"required": ["data-type"],
"properties": {
"data-type": { "type": "string", "const": "emoji" },
"code": { "type": "string" },
"newCode": { "type": "object" }
},
"additionalProperties": true
}
]
},
"inlineCode": {
"type": "array",
"minItems": 2,
"items": [
{ "type": "string", "const": "inlineCode" },
{ "type": "object", "properties": { "bgColor": { "type": "string" } }, "additionalProperties": true }
],
"additionalItems": { "$ref": "#/definitions/inlineContent" }
},
"br": {
"type": "array",
"minItems": 2,
"maxItems": 3,
"items": [
{ "type": "string", "const": "br" },
{ "type": "object" },
{ "type": "string" }
]
}
}
}
@@ -0,0 +1,711 @@
# 文档 JSONML 节点结构参考
> **权威定义**:合法节点类型、允许的子节点和属性约束以 `wukong/products/jsonml-schema-v2.json` 为准。本文为可读版摘要,若与 schema-v2.json 冲突以后者为准。
本文档定义钉钉文档 body JSONML 中所有节点类型的结构,供 agent 编辑文档时参考。
**写法范例**见 [doc-jsonml-cookbook.md](./doc-jsonml-cookbook.md);本文聚焦字段定义、枚举与约束。
## 格式说明
JSONML 是文档内容树的序列化格式:
```
[tag, attrs?, ...children]
```
- `tag` — 字符串,节点类型标识
- `attrs` — 可选对象,节点属性(写入时**强烈建议**始终传 `{}` 而非省略)
- `children` — 子节点数组;可以是嵌套节点或(仅 inline 上下文中)字符串
文档 body 是一个以 `"root"` 为根的 JSONML 节点,`dws doc read --content-format jsonml` 返回此格式:
```json
["root", {"sectPr": {"pgSz": {"w": 11906, "h": 16838}}},
["p", {"uuid": "p1"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "第一段"]]],
["p", {"uuid": "p2"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "第二段"]]]
]
```
- 第一个元素固定为 `"root"`
- 第二个元素为文档级属性对象(如 `sectPr` 页面设置),可选
- 后续元素为块级节点(每个 block 节点应带 `uuid`
全量覆写(overwrite)时,CLI 要求 body 必须以 `["root", ...]` 为根节点:
1. `["root", {sectPr}, ...blocks]` — 服务端 canonical 形式,`doc read` 输出
2. `["root", {}, ...blocks]` — 无页面设置时用空 attrs
## CLI 行为概览(validator
写入端(`doc create/update``block insert/update`)走 **validate** 一步,不做结构修复:
| 行为 | 缺省 | `--fix-jsonml` |
|------|------|----------------|
| JSON 语法修复(括号/逗号补全) | ✗ | ✓(打印 `[FIX]` |
| validator 阻断(HasErrors → 拒绝发送) | ✓ | ✓ |
| validator 警告(warnings → 仅 stderr | ✓ | ✓ |
| root 校验(仅 doc create/update | ✓ | ✓ |
> `doc create/update` 要求 body 必须以 `["root", {attrs?}, ...blocks]` 为根节点。缺少 root 会报错而非自动包装。`doc block insert/update` 不要求 root。
报错格式(面向 agent):
```
$[2][2]: paragraph child must be span wrapper, got raw string.
Suggestion: ["span",{"data-type":"text"},["span",{"data-type":"leaf"},"<your text>"]]
```
`$` 表示输入根,`[i]` 是数组下标,`.attrs.k` 是属性名。
## 文本节点(Text
**Canonical 形式**(服务端 serialize 输出、`doc read --content-format jsonml` 返回的就是这个):
```json
["span", {"data-type": "text"},
["span", {"data-type": "leaf", "bold": true}, "加粗文本"],
["span", {"data-type": "leaf"}, "普通文本"]
]
```
- **text 容器**`["span", {"data-type": "text"}, ...leaves]` — 包裹所有 leaf
- **leaf 节点**`["span", {"data-type": "leaf", ...marks}, "<文本>"]` — 实际承载文字与样式
- 一个 block 通常只有一个 text 容器,可以含多个 leaf(不同样式片段)
- text 容器与 `["a", ...]``["img", ...]``["tag", ...]` 等其他 inline 节点**并列**作为 block 的子节点
### 文本样式属性(Marks,写在 leaf 的 attrs 上)
所有 marks 均为 optional,按需组合。
| 属性 | 类型 | 格式/枚举 | 说明 |
|------|------|-----------|------|
| `bold` | `boolean` | `true` / `false` | 加粗。`false` 可反向取消继承 |
| `italic` | `boolean` | `true` | 斜体 |
| `strike` | `boolean` | `true` / `false` | 单删除线 |
| `dstrike` | `boolean` | `true` / `false` | 双删除线(独立于 strike) |
| `underline` | `object` | `{value, color?}` | 下划线。value: `"single"` \| `"dash"` \| `"wave"` \| `"double"` \| `"none"` |
| `color` | `string` | `"#rrggbb"` | 文字颜色 |
| `highlight` | `string` | CSS 颜色 | 文字高亮背景色 |
| `shd` | `object` | `{val?, color?, fill?}` | OOXML 底纹(Word 导入保留) |
| `sz` | `number` | 数值 | 字号,配合 `szUnit` |
| `szUnit` | `string` | `"px"` \| `"pt"` | 字号单位,默认 `"px"` |
| `fonts` | `object` | `{ascii, hAnsi, cs, eastAsia}` | OOXML 四分区字体,值为 font-family 名称(如 `SimHei`),不能写中文名 |
| `vertAlign` | `string` | `"superscript"` \| `"subscript"` \| `"baseline"` | 上标/下标/基线 |
| `spacing` | `number` | 数值(pt | 字间距 |
示例:
```json
["span", {"data-type": "text"},
["span", {"data-type": "leaf", "bold": true, "italic": true, "color": "#1a73e8"}, "加粗斜体蓝字"]
]
["span", {"data-type": "text"},
["span", {"data-type": "leaf", "underline": {"value": "single", "color": "#ff0000"}, "sz": 14, "szUnit": "px"}, "红色下划线"]
]
["span", {"data-type": "text"},
["span", {"data-type": "leaf", "strike": true, "color": "#9E9E9E"}, "删除线灰字"]
]
["span", {"data-type": "text"},
["span", {"data-type": "leaf", "fonts": {"ascii": "Arial", "eastAsia": "SimSun"}, "sz": 12, "szUnit": "pt"}, "指定字体"]
]
["span", {"data-type": "text"},
["span", {"data-type": "leaf"}, "H"],
["span", {"data-type": "leaf", "vertAlign": "subscript"}, "2"],
["span", {"data-type": "leaf"}, "O"]
]
```
### 历史/兼容形式
| 写法 | validator | 服务端 | 建议 |
|------|-----------|--------|------|
| `["span", {"data-type":"text"}, ["span", {"data-type":"leaf"}, "x"]]` | ✓ canonical | ✓ | ✅ 新内容首选 |
| `["text", {marks}, "x"]` | ✓(`text` 在 inline 白名单中) | ✓(兼容) | ⚠️ 历史 inline tag。`doc read` 不会输出这种形式;如需复制粘贴回写、保持与现有内容一致,建议改写为 canonical |
| `"raw string"` 作为 block 子节点 | ✗ 报错 `段落子节点不能是裸字符串` | — | 不要直接写。validator 会报错,请手动包成 canonical 形式 |
> Marks 表的属性集对 canonical 的 leaf 和 legacy 的 text 都适用;差别仅在承载位置(leaf 的 attrs vs text 的 attrs)。
---
## 块级节点
所有 block 节点的 tag 白名单(validator `validBlockTags`):
`p` / `h1` / `h2` / `h3` / `h4` / `h5` / `h6` / `hr` / `table` / `code` / `container` / `embed` / `onlineVideo` / `card` / `toc` / `refblock` / `cangjie-voidblock` / `cangjie-container`
未在白名单的 tag 会触发 `未知的块级 tag` 警告,并给出基于编辑距离 (Levenshtein ≤2) 的最接近建议(如 `"containr"``did you mean "container"?`)。
### paragraph(段落)
- **tag**: `"p"`
- **attrs**(全部 optional:
- `jc?: "left" | "center" | "right" | "both" | "distribute" | "justify"` — 对齐
- `ind?` — 缩进
- `left?: number`, `right?: number`, `firstLine?: number`, `firstLineChars?: number`, `hanging?: number`
- `spacing?` — 行间距(`lineRule=auto``line` 为倍数:1=单倍、1.5=1.5倍、2=双倍;`before`/`after` 单位 pt
- `line?: number`, `before?: number`, `after?: number`
- `lineRule?: "atLeast" | "auto" | "exact"`
- `shd?: {val?, fill?, color?}` — 底纹
- `quote?: boolean` — 引用标识(注:服务端也接受 `blockquote: true` 的别名)
- `list?: object` — 列表标识(见下方 list 节点)
- `refs?: string[]` — 脚注引用 IDfootnote 标识)
- **children**: 一个 text 容器 + 可选的 inline 节点(link/img/tag/mention 等)
- **示例**:
```json
["p", {"uuid": "p1"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "普通段落"]]]
["p", {"uuid": "p2", "jc": "center"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "居中段落"]]]
["p", {"uuid": "p3", "ind": {"firstLine": 32}}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "首行缩进段落"]]]
["p", {"uuid": "p4", "spacing": {"line": 1.5, "lineRule": "auto"}}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "1.5倍行距"]]]
```
### heading(标题)
- **tag**: `"h1"` | `"h2"` | `"h3"` | `"h4"` | `"h5"` | `"h6"`
- **attrs**: 同 paragraph
- **children**: 同 paragraph
- **示例**:
```json
["h1", {"uuid": "h1a"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "一级标题"]]]
["h3", {"uuid": "h3a", "jc": "center"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "居中三级标题"]]]
```
### blockquote(引用)
- **tag**: `"p"``"h1"`~`"h6"`(不是独立 tag,是属性装饰)
- **标识**: `attrs.quote: true`(也接受 `blockquote: true`
- **示例**:
```json
["p", {"uuid": "q1", "quote": true}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "这是一段引用"]]]
["h2", {"uuid": "q2", "quote": true}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "引用标题"]]]
```
### list(列表)
- **tag**: `"p"`(列表项在 JSONML 层面是扁平的段落,通过 `attrs.list` 标识)
- **attrs.list**:
- `listId: string`**必传**。同一列表的项共享相同 ID。validator 报错:`必传字段缺失`
- `level: number`**必传**。缩进层级(0-based,≥0)。validator 报错:`必传字段缺失` / `必须 ≥0`
- `isOrdered?: boolean` — 是否有序,默认 `false`
- `isTaskList?: boolean` — 是否任务列表,默认 `false`
- `isChecked?: boolean` — 任务是否完成(仅 isTaskList=true 时有意义)
- `isCanceled?: boolean` — 任务是否取消
- `start?: number` — 有序列表起始序号(≥1)。**仅在列表第一项设置**,后续项不设置此字段(系统自动递增)。validator 报错:`必须 ≥1`
- `listStyleType?: string` — 样式类型(31 种预设)
- `hideSymbol?: boolean` — 隐藏列表符号
- **示例**:
```json
["p", {"uuid": "li1", "list": {"listId": "abc", "level": 0, "isOrdered": false}},
["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "无序列表项"]]]
["p", {"uuid": "li2", "list": {"listId": "def", "level": 0, "isOrdered": true, "start": 1}},
["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "有序第一项"]]]
["p", {"uuid": "li2b", "list": {"listId": "def", "level": 0, "isOrdered": true}},
["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "有序第二项(不设 start,自动编号 2)"]]]
["p", {"uuid": "li3", "list": {"listId": "ghi", "level": 1, "isOrdered": false}},
["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "二级缩进"]]]
["p", {"uuid": "li4", "list": {"listId": "jkl", "level": 0, "isTaskList": true, "isChecked": false}},
["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "待办事项"]]]
```
- **注意**: 列表是扁平结构,不是嵌套的 ol/ul/li
### hr(分割线)
- **tag**: `"hr"`
- **attrs**(全部 optional:
- `type?: TLineStyle` — 16 种枚举(`"single"` / `"dotted"` / `"dashed"` / `"double"` / `"wave"` 等),默认 `"single"`
- `sz?: number` — 粗细(px),默认 `1`
- `color?: string` — 颜色
- **children**: 构造时无需传子节点;服务端返回的真实文档中可能含内部配置数据子节点
- **示例**:
```json
["hr", {"uuid": "hr1"}]
["hr", {"uuid": "hr2", "type": "dashed", "color": "#ccc", "sz": 2}]
```
### table(表格)
- **tag**: `"table"`
- **attrs**:
- `colsWidth: number[]` — 列宽(语义必传)。缺失时 validator 警告 `table 应提供 colsWidth`
- 默认模式:值为各列绝对宽度,单位 **pt**(如 `[325, 325]` 总和≈页宽 650pt
- 比例模式(配合 `tblW: {"type": "pct"}`):值为百分比权重(如 `[33.3, 33.3, 33.4]` 总和=100
- `tblW?: {w?: number, type?: string}` — 表格宽度模式。`type: "pct"` 时 colsWidth 按比例解析
- `sr?: boolean``true` 表示这是分栏布局(columns),不是普通表格
- `jc?: string` — 对齐(源码注释"目前无消费")
- **children**: `["tr", ...]` 行节点
- **示例**:
```json
["table", {"uuid": "tb1", "colsWidth": [200, 200]},
["tr", {"uuid": "tr1"},
["tc", {"uuid": "tc1", "colSpan": 1, "rowSpan": 1},
["p", {"uuid": "tcp1"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "单元格1"]]]],
["tc", {"uuid": "tc2", "colSpan": 1, "rowSpan": 1},
["p", {"uuid": "tcp2"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "单元格2"]]]]
]
]
```
#### tr(表格行)
- **tag**: `"tr"`
- **attrs**: `{h?: number, isTblHeader?: boolean}`
#### tc(表格单元格)
- **tag**: `"tc"`
- **attrs**:
- `colSpan?: number` — 横跨列数,默认 `1`。validator 报错:`必须 ≥1`
- `rowSpan?: number` — 横跨行数,默认 `1`。validator 报错:`必须 ≥1`
- `fill?: string` — 填充色
- `vAlign?: "top" | "middle" | "bottom"` — 垂直对齐,默认 `"middle"`。validator 报错枚举不符(注意 `"center"` 不是合法值,应用 `"middle"`
- `bdr?: object` — 单元格边框
- **children**: 任意块级节点数组;**至少包含一个 `p`**(即使空),否则单元格无法渲染光标
### code(代码块)
- **tag**: `"code"`
- **attrs**(全部 optional:
- `code?: string` — 代码内容,默认 `""`
- `syntax?: string` — 语言标识,默认 `"plaintext"`
- `theme?: string` — 主题枚举:`"default"` / `"light"` / `"dracula"` / `"github"` / `"cobalt"` / `"atomOneDark"` / `"oneLightPro"` / `"nightOwl"` / `"githubDark"` / `"realDracula"`。未知值 validator 警告 `未知主题`
- `wrap?: boolean` — 自动换行,默认 `true`
- `showLineNumber?: boolean` — 显示行号,默认 `true`
- `title?: string` — 标题(≤1000 字符)
- `fold?: boolean` — 是否折叠,默认 `false`
- **children**: 构造时无需传子节点(代码存在 `attrs.code` 中)
- **示例**:
```json
["code", {"uuid": "cd1", "syntax": "javascript", "code": "console.log('hello');"}]
["code", {"uuid": "cd2", "syntax": "python", "code": "print('hello')", "theme": "dracula"}]
```
### container(容器/高亮块)
- **tag**: `"container"`
- **attrs**:
- `subType: string`**必传**。callout 为 `"colorBlocks"`。缺失时 validator 报错 `container 必须包含 subType`
- `metadata?: object` — 自定义元数据(callout 用 `{bgcolor, border}`
- **children**: 任意块级节点
- **示例(callout**:
```json
["container", {"uuid": "co1", "subType": "colorBlocks", "metadata": {"bgcolor": "#E8F2FE", "border": "#B3D4FC"}},
["p", {"uuid": "co1p1"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "这是一段提示内容"]]]
]
```
### columns(分栏布局)
- **tag**: `"table"`(复用 table tag,通过 `sr: true` 区分)
- **attrs**:
- `sr: true` — 固定标识
- `colsWidth?: number[]` — 各列宽度(pt),前端按比例换算为页面宽度
- `spacing?: number` — 栏间距
- **children**: 单个 `["tr", ...]`,内含多个 `["tc", ...]`
- **示例**:
```json
["table", {"uuid": "col1", "sr": true, "colsWidth": [300, 300]},
["tr", {"uuid": "colr"},
["tc", {"uuid": "colc1"},
["p", {"uuid": "colp1"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "左栏"]]]],
["tc", {"uuid": "colc2"},
["p", {"uuid": "colp2"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "右栏"]]]]
]
]
```
### embed(嵌入文件)
- **tag**: `"embed"`void 块)
- **attrs**:
- `src?: string` — 文件来源 URL(语义必传)
- `name?: string` — 文件名
- `type?: string` — 文件类型(`"pdf"` / `"xlsx"` / `"html"` / `"file"` 等)
- `size?: number` — 文件大小
- `viewType?: string` — 视图类型(如 `"preview"`
- `previewSize?: {height: number}` — 预览高度(px
- **children**: 无
- **示例**:
```json
["embed", {"uuid": "em1", "name": "report.pdf", "type": "pdf", "src": "https://example.com/report.pdf", "size": 2048, "viewType": "preview", "previewSize": {"height": 600}}]
```
### onlineVideo(在线视频)
- **tag**: `"onlineVideo"`void 块)
- **attrs**(全部 optional,但 src 语义必传):
- `src?: string` — 视频地址
- `type?: string` — 平台标识(`"bilibili"` / `"youku"` / `"mp4"` 等)
- `poster?: string` — 封面图 URL
- **children**: 无
- **示例**:
```json
["onlineVideo", {"uuid": "ov1", "src": "https://example.com/video.mp4"}]
["onlineVideo", {"uuid": "ov2", "src": "https://player.bilibili.com/player.html?aid=1", "type": "bilibili", "poster": "https://example.com/poster.jpg"}]
```
### card(河图组件 / 富卡片)
- **tag**: `"card"`
- **attrs**:
- `cardType: string` — 卡片类型标识(如 `"groupChatCard"``"vote"`
- `metadata: {id: string, ...}` — 组件元数据,必须包含 id
- `height?: number`
- **children**: 服务端 serialize 通常返回带一个空的 span/leaf 占位子节点,写入时建议保留以避免反序列化差异
- **示例**:
```json
["card", {"uuid": "cd1", "cardType": "vote", "metadata": {"id": "card_abc123"}},
["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, ""]]]
```
- **注意**: card 节点的重数据存储在独立 parts 层,body 中只存轻量引用
### toc(目录)
- **tag**: `"toc"`void 块)
- **attrs**(全部 **required**,缺失逐个 validator 报错):
- `title: string` — 目录标题
- `mode: "outline" | "column"` — 展示模式(其他值 validator 报错 `无效值`
- `styles: object` — 样式配置
- `styles.global: {maxLevel: number, bgColor: string, css: object}`
- `styles.title: {font: string, color: string, numbering: boolean, css: object}`
- `styles.item: {symbol: "disc" | "none", css: object}`
- `content: TocItem[]` — 目录条目数组;**写入时填 `[]` 即可**,服务端基于文档 heading 自动重建
- 每项: `{uuid, anchorId: string|null, level, children: TocItem[]}`
- **children**: 无
- **示例**:
```json
["toc", {"uuid": "toc1", "title": "目录", "mode": "outline",
"styles": {
"global": {"maxLevel": 5, "bgColor": "#F0EBF7", "css": {}},
"title": {"font": "", "color": "#000", "numbering": false, "css": {}},
"item": {"symbol": "disc", "css": {}}
},
"content": []
}]
```
### refblock(引用块)
- **tag**: `"refblock"`
- **attrs**:
- `docKey?: string` — 所属/被引文档标识(语义必传)
- `refblockUUID?: string` — 引用块唯一标识(语义必传)
- **children**: 块级节点(降级显示快照;真实内容由服务端按 docKey/refblockUUID 拉取覆写)
- **示例**:
```json
["refblock", {"uuid": "rb1", "docKey": "doc123", "refblockUUID": "block456"},
["p", {"uuid": "rb1p1"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "(引用预览内容,服务端会回填)"]]]
]
```
- **注意**: 主块 (host)`docKey === refblockUUID`;副块 (copy)`docKey !== refblockUUID`
---
## Inline 节点
所有 inline 节点 tag 白名单(validator `validInlineTags`):
`text`legacy / `a` / `img` / `span` / `tag` / `inlineCode` / `br` / `cangjie-textinline` / `cangjie-voidinline`
block tag 也可出现在 inline 上下文(如 `img` 既是 block 又是 inline)。未在白名单的 tag 会触发警告并给出 Levenshtein 建议。
### a(链接)
- **tag**: `"a"`
- **attrs**(全部 optional,但 href 语义必传):
- `href?: string` — 链接地址(缺失时 validator 警告 `link 缺少 href`
- `cardInfo?: object` — 链接卡片信息
- `cardInfo.displayType?: "link" | "card"` — 展示模式
- `cardInfo.title?: string`, `cardInfo.desc?: string`, `cardInfo.imgURL?: string`
- `metadata?: object` — 扩展业务信息
- **children**: 链接的展示文本(直接字符串,与 block 上下文不同)
- **示例**(作为 paragraph 的 inline 子节点):
```json
["p", {"uuid": "p1"},
["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "请访问"]],
["a", {"href": "https://example.com"}, "链接文字"]
]
```
### img(图片)
- **tag**: `"img"`(既可作 block 也可作 inline 子节点)
- **attrs**(全部 optional,但 src 语义必传):
- `src?: string` — 图片地址
- `width?: number` — 宽度(px
- `height?: number` — 高度(px
- `rectClip?: {left?, right?, top?, bottom?}` — 裁剪比例(0-1
- `rotation?: number` — 旋转角度
- `radius?: number` — 圆角(px
- `shadow?: string` — CSS shadow
- `outline?: {width?, type?, color?}` — 边框
- **children**: 构造时无需传子节点;真实文档中可能含内部配置数据
- **示例**:
```json
["img", {"uuid": "img1", "src": "https://example.com/photo.png", "width": 400, "height": 300}]
```
- **注意**:
- 不存在 `layout` 属性,布局由 UI 层控制
- `img` 是 inline 元素,必须包裹在 `p` 段落中
### mention@提及)
- **tag**: `"span"`
- **attrs**:
- `data-type: "mention"`**必传**,固定标识
- `id?: string` — 被@用户 ID(语义必传)
- `name?: string` — 被@用户名(语义必传)
- `login?: string` — 登录名
- `metadata?: object` — 扩展元数据
- **children**: 显示文本
- **示例**:
```json
["span", {"data-type": "mention", "id": "user123", "name": "张三"}, "@张三"]
```
### tag(通用标签节点)
- **tag**: `"tag"`
- **attrs**:
- `tagType: string`**必传**。子类型标识(如 `"formula"` / `"imTag"`
- 其他属性依 tagType 而异
- **示例**:
```json
["tag", {"tagType": "imTag", "text": "#标签名"}]
```
### formula(公式)— `tag` 节点的特化
- **tag**: `"tag"`
- **attrs**:
- `tagType: "formula"`**必传**,固定值
- `metadata: {formula: string}`**必传**。LaTeX 代码(空串表示空公式)。缺失或类型错时 validator 报错
- **children**: 无
- **示例**:
```json
["tag", {"tagType": "formula", "metadata": {"formula": "E=mc^2"}}]
```
### sticker(表情贴纸)
- **tag**: `"span"`
- **attrs**:
- `data-type: "emoji"`**必传**(注意:`"emoji"` 不是 `"sticker"`
- `code?: string` — 表情纯文本标识(如 `"[微笑]"`
- `newCode?: IEmoji` — 完整表情对象,5 种子类型:
- `{type: "dingding", id, name, url}`
- `{type: "unicode", value}`
- `{type: "custom", url}`
- `{type: "icon", id, color?}`
- `{type: "svg", id, url, color?}`
- **children**: 无或空文本
- **示例**:
```json
["span", {"data-type": "emoji", "code": "[微笑]", "newCode": {"type": "unicode", "value": "😊"}}]
```
### inlineCode(行内代码)
- **tag**: `"inlineCode"`
- **attrs**: `{bgColor?: string}``{}`
- **children**: 文本(inline 子节点)
- **示例**:
```json
["inlineCode", {}, "const x = 1"]
```
### br(换行符)
- **tag**: `"br"`void inline
- **attrs**: `{}`
- **children**: 空文本占位
- **示例**:
```json
["br", {}, ""]
```
### refer(行内引用)
- **tag**: `"span"`
- **attrs**:
- `data-type: "refer"` — 固定标识
- 其他业务属性(自由 key-value)
- **示例**:
```json
["span", {"data-type": "refer", "docId": "xxx", "blockId": "yyy"}, "引用内容"]
```
---
## Cangjie 系列(扩展块)
### attachment(附件)
- **tag**: `"cangjie-voidblock"``"cangjie-voidinline"`
- **attrs**:
- `subType: "attachment"` — 固定标识
- `data.viewType: "preview" | "abstractCard"` — 视图类型
- `data.fileData: {name?, src?, size?, fileType?, category: "media" | "file"}` — 文件数据
- `data.previewSize?: {width?, height?}` — 预览尺寸
- **children**: 无
- **示例**:
```json
["cangjie-voidblock", {"subType": "attachment", "data": {"viewType": "abstractCard", "fileData": {"name": "report.pdf", "src": "https://...", "size": 2048, "category": "file"}}}]
```
- **注意**: 反序列化时匹配 `"embed"` tag 并转换为 attachment
### footnote(脚注)
- **tag**: 宿主节点 tag`"p"` / `"h1"`~`"h6"`),不是独立节点
- **标识**: `attrs.refs: string[]`(脚注引用 ID 数组)
- **示例**:
```json
["p", {"uuid": "fn1", "refs": ["footnote-id-1"]},
["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "正文文本"]]]
```
- **注意**: footnote 将 `refs` 属性注入到宿主 block 节点中,类似 blockquote 的属性装饰模式
### calendar(日程)
- **tag**: `"cangjie-voidinline"``"cangjie-voidblock"`
- **attrs**:
- `subType: "calendar"` — 固定标识
- `data.viewType: "inlineCalendar" | "blockCalendar"` — 形态
- `data.calendarId: string` — 日程 ID
- `data.subject: string` — 日程名称
- `data.detailUrl: string` — 日程详情链接
- **示例**:
```json
["cangjie-voidinline", {"subType": "calendar", "data": {"viewType": "inlineCalendar", "calendarId": "cal-001", "subject": "周会", "detailUrl": "https://..."}}]
```
### label(标签)
- **tag**: `"cangjie-textinline"`
- **attrs**:
- `subType: "label"` — 固定标识
- `data.bgColor?: string` — 标签背景色
- `data.color?: string` — 标签文字颜色
- `data.labelType?: "normal" | "note" | "spoiler"` — 标签模式
- **children**: 文本内容
- **示例**:
```json
["cangjie-textinline", {"subType": "label", "data": {"bgColor": "#FFE8CC", "color": "#D46B08", "labelType": "normal"}}, "重要"]
```
### templateButton(模板按钮)
- **tag**: `"cangjie-container"`
- **attrs**:
- `subType: "templateButton"` — 固定标识
- `metadata.direction: "top" | "bottom"` — 按钮方向
- `metadata.isOnce: boolean` — 是否一次性
- `metadata.title: string` — 按钮标题
- **children**: 块级节点数组
- **示例**:
```json
["cangjie-container", {"subType": "templateButton", "metadata": {"direction": "bottom", "isOnce": false, "title": "添加待办"}},
["p", {"uuid": "tb1p1", "list": {"level": 0, "isChecked": false, "isOrdered": false, "isTaskList": true, "listId": "abc"}},
["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, ""]]]
]
```
### textSlot(文本插槽)
- **tag**: `"cangjie-textinline"`
- **attrs**:
- `subType: "textSlot"` — 固定标识
- `data.slotInfo.style.color: string` — 文字颜色(支持渐变色)
- **children**: 文本内容
- **示例**:
```json
["cangjie-textinline", {"subType": "textSlot", "data": {"slotInfo": {"style": {"color": "#1890ff"}}}}, "插槽文本"]
```
---
## 完整文档示例
```json
["root", {"sectPr": {"pgSz": {"w": 11906, "h": 16838}}},
["h1", {"uuid": "h1"},
["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "文档标题"]]],
["p", {"uuid": "p1"},
["span", {"data-type": "text"},
["span", {"data-type": "leaf"}, "这是一段普通文本,包含"],
["span", {"data-type": "leaf", "bold": true}, "加粗"],
["span", {"data-type": "leaf"}, "和"]],
["a", {"href": "https://example.com"}, "链接"],
["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "。"]]],
["h2", {"uuid": "h2"},
["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "列表示例"]]],
["p", {"uuid": "li1", "list": {"listId": "l1", "level": 0, "isOrdered": true, "start": 1}},
["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "第一项"]]],
["p", {"uuid": "li2", "list": {"listId": "l1", "level": 0, "isOrdered": true}},
["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "第二项"]]],
["p", {"uuid": "li3", "list": {"listId": "l1", "level": 1, "isOrdered": false}},
["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "子项"]]],
["hr", {"uuid": "hr1"}],
["code", {"uuid": "cd1", "syntax": "javascript", "code": "function hello() {\n return 'world';\n}"}],
["container", {"uuid": "co1", "subType": "colorBlocks", "metadata": {"bgcolor": "#E8F2FE"}},
["p", {"uuid": "co1p1"},
["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "这是一个提示块"]]]],
["table", {"uuid": "tb1", "colsWidth": [200, 200]},
["tr", {"uuid": "tr1"},
["tc", {"uuid": "tc1", "colSpan": 1, "rowSpan": 1},
["p", {"uuid": "tcp1"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "A"]]]],
["tc", {"uuid": "tc2", "colSpan": 1, "rowSpan": 1},
["p", {"uuid": "tcp2"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "B"]]]]
]
]
]
```
## 设计要点
1. **Canonical 文本是 span/leaf**:每段文字 = `["span", {"data-type":"text"}, ["span", {"data-type":"leaf", ...marks}, "..."]]`。legacy `["text", {marks}, "..."]` 仍被接受但不建议新写。
2. **裸字符串作 block 子节点违法**:validator 报错,请手动包成 canonical 形式。
3. **每个 block 必带 `uuid`**:手写 JSONML 时建议每个 block 自带 `uuid`base32 alphanumericdws CLI 用 `dws` 前缀)。
4. **扁平列表**: 列表不嵌套,通过 `listId` + `level` 表达层级。
5. **属性装饰**: blockquote / list / footnote 不是独立 tag,是 paragraph 的属性。
6. **Void 节点**: hr / code / img / card / toc / embed / onlineVideo 在构造时不需要传子节点;服务端返回的真实文档中这些节点**可能包含内部配置数据子节点**,解析时应兼容。
7. **columns = table + sr:true**: 分栏复用表格结构。
8. **card 轻引用**: body 中只存 cardType + metadata.id,重数据在 parts 层。
9. **root 节点**: 服务端返回的完整 body 以 `["root", {sectPr...}, ...blocks]` 包裹。`doc create/update` 写入时必须以 root 为根节点,缺少会报错。`doc block insert/update` 不要求 root。
@@ -0,0 +1,414 @@
# 钉钉文档创建流程
本文只处理文档结构和排版设计。普通创建的执行入口固定为 `dws doc +create`,命令内置分片与回读验证;原子 `doc create/read/update` 仅用于 shortcut 未公开所需参数的专家路径,使用前必须读取精确 leaf Schema。
> **路由硬约束:** 正文文件使用 `--content @./相对路径 --doc-format markdown|jsonml`;禁止 `/tmp`、绝对路径、已删除的 Python 脚本以及“创建后再额外 read”固定流水。
> 改写已有文档见 [doc-update-workflow.md](./doc-update-workflow.md)。排版规范见 [doc-style-guideline.md](./doc-style-guideline.md)。
## 前置必读
> **同时读取 [doc-style-guideline.md](./doc-style-guideline.md)**
> - **§2.0 类型判断决策表** → 锁定文档类型(决策型 / 执行型 / 说明型 / 知识沉淀型)和骨架
> - **§1 硬规则** → 全程生效(`--name` 已是 H1、不编造 URL、Markdown 草稿不写 callout 等)
### 关键词速查(用户意图 → 起稿路径)
| 用户关键词 | 文档类型 | 起稿路径 |
|-----------|---------|---------|
| 汇报 / 周报 / 月报 / 复盘 / 方案选型 / 决策 / 对比 | §2.1 决策型 | **→ JSONML 起稿** |
| 调研 / 技术方案 / 复盘报告(含对比/数据) | §2.4 知识沉淀型 | **→ JSONML 起稿** |
| SOP / Runbook / 接入指南 / 升级 / 操作手册 | §2.2 执行型 | → Markdown 起稿 |
| 接口文档 / 能力清单 / 参数说明 / 错误码 | §2.3 说明型 | → Markdown 起稿 |
| 用户原文含:颜色/高亮/美观/醒目/重点突出/像PPT | 任意类型 | **→ JSONML 起稿** |
## 适用边界
进入本文前,必须已经确认用户要创建的是钉钉文档 (`adoc`)。如果用户要的是钉钉表格、AI表格、文件上传、知识库空间管理或消息发送,不要套用本文。
本文覆盖:
- 文档标题和创建位置确认
- 正文草稿准备
- `+create` 写入并自动回读验收
- 内容缺失时的补救写入
本文不覆盖:
- 从群聊、日志、听记、表格等来源采集资料
- 生成日报、周报、月报等业务报告口径
- 文档权限分享、消息通知或待办创建
- 非钉钉文档的新建流程
## 创建前检查
创建前先锁定四个输入:
| 项目 | 要求 |
|------|------|
| 标题 | 用 `--name` 传入;正文不要再重复同名一级标题 |
| 位置 | 默认创建到我的文档;指定目录时只接受文档文件夹 `nodeId` 或 alidocs 文件夹 URL |
| 正文 | 多行、表格、代码块、特殊字符或长度 >= 2KB 时必须写入 UTF-8 临时 `.md` 文件 |
| 格式 | 按 §JSONML 起稿判定 决定起稿路径:命中 JSONML 起稿条件时**直接用 JSONML 构造**(跳过 markdown);未命中时用 Markdown 起稿,创建后按 [doc-update-workflow.md](./doc-update-workflow.md) 精修 |
禁止把纯数字 `dentryId`、drive `parent-id` 或 spaceId 填进 `--folder`
## JSONML 起稿判定
在正文准备之前,先判断是否直接用 JSONML 起稿。**命中以下任一条件即走 JSONML 起稿路径**(跳过 markdown 草稿阶段):
### 文档类型触发
| 类型 | 触发条件 |
|------|---------|
| 决策型(§2.1 | **默认触发** — 汇报/方案/对比需要摘要 callout、彩色表头、决策时限标注 |
| 知识沉淀型(§2.4) | 含对比分析、多维度数据可视化、需要关键节点彩色 callout |
### 意图关键词触发
用户原文或需求描述中出现以下任一关键词:
- 颜色 / 配色 / 上色 / 高亮 / 醒目
- 字号 / 字体 / 加大 / 缩小
- 视觉效果 / 排版精美 / 好看 / 美观
- callout / 分栏 / 对比色 / 彩色表头
- "像 PPT 那样" / "有设计感" / "重点突出"
---
## 设计规划(JSONML 起稿前必做)
判定走 JSONML 路径后,**禁止立即动手写 JSONML**。先完成以下两阶段规划,各自产出一个持久文件作为后续阶段的锚点。
### 设计原则(全程生效)
1. **结构即信息** — 标题层级、表格 vs 分栏 vs 列表、callout 位置都应编码内容逻辑。问自己:“去掉这个结构元素,读者会丢失信息或体验变差吗?”— 丢失信息则必保留;不丢失信息但能提升可读性或美观度(如分割线分隔章节、分栏对比排版)也应保留;既不携带信息也不提升体验的装饰元素才删除。
2. **视觉层级引导阅读** — 每一屏必须让读者瞥一眼就能回答:“这块最重要的是什么?”字号/粗体/颜色形成明确梯度:标题 > 重点数据 > 正文 > 辅助信息。
3. **克制产生质感** — 遵循 60-30-10 配色比例:60% 中性底色(白/浅灰)、30% 辅助色、10% 强调色。多色系共存时需保持**同等饱和度**并各有语义角色(如淡蓝=信息、淡黄=提示、淡红=风险),同一色系内深浅变化自由。callout 不超过 2 个。
4. **设计先于执行** — 从规划阶段起每个结构块的样式就已确定,执行时(无论直接 JSONML 还是脚手架精修)只是落地已有设计,不是边写边想。
5. **同类同色、一色多阶** — 同类信息必须使用相同色系;单一色系按元素角色展开为深/中/浅/极浅四级(标题文字用深色、强调用中色、高亮/表头用浅色、背景用极浅色),不要全篇只用一个 hex 值。
### Phase 1RFC — 需求理解与设计方向
**目标**:明确“做什么”和“为什么这样做”,形成方向性锚点。
#### 1.1 需求提取(全量列出用户显式要求)
通读用户 prompt,抽取两类要求并编为清单:
**内容要求**
- 标题、字数、章节划分
- 数据来源、受众
- 语气/风格(如“大气”“专业”“轻松”)
**样式要求**(每一条都必须在最终输出中体现,不得遗漏):
- 字体:映射为 font-family 名称(参照 cookbook 字体映射表),区分“全文字体”和“特定元素字体”
- 字号、行距、对齐、颜色
- 强调手段(加粗、高亮、配色…)
- 约束(如“每部分不省略”)
> 用户没有明确指定的维度(如未指定行距、未指定表格样式)由 Phase 2 补充设计决策。
#### 1.2 内容-表现适配(每个章节的内容适合用什么元素)
对每个章节回答:“这个内容的核心是什么类型的信息?”→ 选择最佳元素:
**块级结构元素**
| 信息类型 | 首选元素 | 不适合 |
|----------|---------|--------|
| 多个同类实体对比(≥ 4 项或 ≥ 3 维度) | 彩色表头表格 | 纯文本段落 |
| 少量实体对比(2-3 项× 少量维度) | 分栏(每栏一个实体,可设边框/背景色) | 大宽表格 |
| 时间序列/流程(行程/步骤) | 有序列表 + 粗体时间标签 | 无序列表 |
| 单个结论/推荐/重要提示 | callout(“花大胆”的地方) | 普通段落 |
| 描述性文字(背景/说明) | 正文段落 + 关键词粗体 | 表格 |
| 分类列举(特色/亮点) | 无序列表 | 表格(数据不够多列时) |
| 数值强调(评分/价格/统计) | 加粗 + 着色 | 跳过不强调 |
| 引用原文(用户评价/网友点评/官方说明) | 引用块 | 普通段落 |
| 任务/待办清单 | checklist`- [ ]` | 普通列表 |
| 章节分隔/主题转换 | 分割线(`hr` | 空行 |
| 板块内子区域分隔(同一单元格/容器内多个逻辑段) | hr 内部分隔(在 tc 或 container 内部使用) | 空行或留白 |
| 结构化元信息(人/时间/地点/属性清单) | 键值对表格(窄标签列 ~15-20% + 宽内容列) | 多行段落 |
| 分类标签/状态标记 | 标签元素(tag) | 纯文本标记 |
**行内强调元素**
- **emoji** — 用于 callout 前缀、状态标记、H2/H3 标题前;不在普通段落和列表项中滥用
- **加粗/高亮/着色** — 强调关键数据和结论
- **highlight 色带** — 在标题 span 上设 `"highlight": "#浅色"` 形成轻量色条标记,比 callout 更轻,适合区分多个并列板块的主题色
- **灰色辅助文字** — 用浅灰色(如 `#979A9B`)标记示例/说明/占位文字,与正文形成明确的主次层级
- **图片** — 实景照片、截图、示意图能显著提升理解时使用
**分栏选型补充**:分栏栏数无上限,但推荐 2-4 栏(≥5 栏会比较拥挤),可设置边框和背景色。**建议设置 `fill` 背景色**(纯白底分栏视觉上与普通段落无异,读者不易感知分栏结构)。适合场景:
- 2-3 个同类实体并排展示(每栏一个实体,含标题 + 描述 + 关键数据)
- 轻量对比:“优点 vs 缺点”、“方案 A vs B”、“Day 1 vs Day 2”
- 网格布局:多次插入同栏数分栏可形成卡片网格(如 2×3、3×2),适合 4-9 个结构相同、内容等长的卡片式实体。**约束**:每个格子内容必须结构一致且长度相近,否则高低不齐会很丑;实体数不能整除栏数时不使用
例如:“5 个实体多维度对比” → **彩色表头表格**;“两组信息并排” → **分栏**;“6 个结构相同的卡片” → **2×3 分栏网格**
#### 1.3 起稿策略选择
根据文档复杂度和上方适配结果,确定起稿路径:直接 JSONML / Markdown 脚手架 + JSONML 精修 / 直接 JSONML 分段构造。具体条件和流程见下方「起稿」节的策略表。
#### 落盘
将以上内容写入 `<name>-rfc.md`,包括:
- 需求摘要(内容要求 + 样式要求)
- 每章展现策略 + 选择理由
- 起稿策略
---
### Phase 2Spec — 精确设计参数
**目标**:将 RFC 的方向决策转化为可直接执行的参数。
#### 2.1 视觉体系设计(确定全局设计变量)
在以下四个维度做出明确选择,每个选择都必须能解释为什么适合这篇文档:
**色彩**(选定主色后展开为色阶):
用户指定颜色时(如"蓝色""绿色"),不要全篇只用一个 hex 值。将其展开为 4 级色阶,按元素角色分配:
| 角色 | 用途 | 色阶要求 |
|------|------|----------|
| 深色 | 标题文字、重点数据 `color` | 白底上高对比可读 |
| 中色 | 正文强调、链接 `color` | 辨识度高但不抢标题 |
| 浅色 | highlight 色带、表头 fill | 底色柔和,上方深色文字可读 |
| 极浅 | container bgcolor、大面积背景 | 接近白色,仅提供区域感 |
**规则**
- 深→浅的层级关系不可颠倒(不能用极浅色做标题文字、不能用深色做背景)
- 同类板块/同类标题必须使用完全相同的色阶组合,通过色彩的重复形成视觉韵律
- 不同类别可用不同色系区分(如:任务=蓝系、风险=红系、成果=绿系)
- 遵循 60-30-10 配色比例:60% 中性底色、30% 辅助色、10% 强调色;用户指定的颜色值优先
**字体梯度**(形成明确层级):
- 主标题(h2):字体 / 字号 / 粗体 / 颜色
- 正文:字体 / 字号 / 行距
- 强调文字:粗体 + 颜色或高亮
- 辅助信息(注释/来源):字号偏小 / 灰色
**表格风格**(有表格时):
- 表头:底色 + 文字色 + 是否粗体
- 单元格:默认对齐 / 字号
**视觉重心**(克制原则落地):
全篇选 1-2 处给予最强视觉处理(配色 callout / 彩色表头 / 分栏对比),其余元素保持朴素。不要处处强调 — 处处强调等于没有强调。
callout 可通过 `"showstk": true, "sticker": "图标名"` 配置顶部贴纸图标(如“灯泡”“火”“钉子”),增强语义标识。设置了 sticker 后,高亮块内首个段落不要再以 emoji 开头,避免紧邻的位置出现两个图标。
#### 落盘与回验
1. 将以上内容写入 `<name>-design.md`,包括:
- 逐章元素映射(用什么标签、什么属性)
- 色阶具体 hex 值
- 字体梯度参数
- 表格风格细节
2. **回验**Read `<name>-rfc.md`,逐条确认:
- [ ] RFC 中每条需求在 Spec 中有对应实现
- [ ] 展现策略在 Spec 中有具体参数支撑
- [ ] 配色遵循 60-30-10 比例、callout ≤ 2 个、多色系饱和度一致且有语义角色
回验通过后进入下一节开始构造 JSONML。
---
## JSONML 起稿(命中判定时使用)
根据 RFC 中确定的起稿策略执行:
| 策略 | 条件 | 执行流程 |
|------|------|----------|
| **直接 JSONML** | 短文档(≤ 15 块级节点)且结构简单 | 在 `./drafts/<name>.json` 编写完整 JSONML 树 → `+create --content @./drafts/<name>.json --doc-format jsonml` |
| **Markdown 脚手架 + JSONML 精修** | 长文档且结构较线性 | ① Markdown 建立内容骨架 → `+create` ② 需要精修时用 `+fetch --detail full` 拉回 JSONML ③ 逐章执行结构变换与样式叠加 |
| **直接 JSONML 分段构造** | 长文档且含大量富结构 | 按章节分段构造 JSONML,每段写完校验通过后再写下一段,最后拼接 |
> **脚手架策略警示**Markdown 无法表达分栏/callout/色彩表头,拉回的 JSONML 只有纯文本骨架。精修阶段不是“在现有结构上加色”,而是“参照 RFC/Spec 重组结构”。
> **MUST READ**:动手写 JSONML 前,必须先用 Read 工具读取 [doc-jsonml-cookbook.md](../format/doc-jsonml-cookbook.md) — 其中 §决策型文档骨架范例 有可直接复制修改的完整模板。
> 节点类型和属性的权威定义见 [doc-jsonml-schema.md](../format/doc-jsonml-schema.md)。
### ⚠️ JSONML 降级约束
**禁止因一次校验失败就放弃 JSONML 降级为 Markdown。** 当用户需求已触发 JSONML 起稿判定时,JSONML 是实现其样式要求的首选路径。失败时的处理策略:
1. **校验报错** → 读取错误信息,定位具体节点,修复后重试
2. **JSON 语法错误** → 检查括号匹配、逗号、引号,修复后重试
3. **反复失败(≥3 次)** → 尝试简化结构(减少嵌套、拆分复杂节点)再试
4. **仍然失败** → 退化为「Markdown 脚手架 + JSONML 精修」路径(流程同上方策略表),并告知用户当前状况
> “由于 JSONML 结构复杂且容易出错,改用 Markdown” — 这不是合法降级理由。必须先充分重试,且降级后仍需通过精修补回样式。
### ⚠️ JSONML 结构严格约束(生成时必须遵守)
每个节点是一个 JSON 数组:`[tagName, attributes?, ...children]`
- **第一个元素**是字符串,表示标签名(如 `"p"`, `"h1"`, `"span"`, `"container"`
- **第二个元素**(可选)是一个 JSON 对象,表示属性(如 `{"uuid": "abc"}`)。如果无属性,可以直接进入子节点
- **随后的元素**是子节点,可以是纯字符串(仅限 leaf span 内),也可以是另一个 JSONML 数组
- **所有 `[` 必须有对应 `]`,所有 `{` 必须有对应 `}`,数组元素之间用 `,` 分隔,最后一个元素后不加 `,`**
常见 LLM 生成错误(务必避免):
| 错误类型 | 示例 | 后果 |
|---------|------|------|
| 缺少闭合 `]` | `["p", {}, ["span", ...]` | JSON 解析失败 |
| 多余逗号 | `["p", {},]` | JSON 解析失败 |
| 缺少逗号 | `["p", {} ["span"]]` | JSON 解析失败 |
| 引号不匹配 | `["p", {"uuid": "abc}]` | JSON 解析失败 |
| 有序列表每项都设 `start:1` | `{"start":1}` 在每项重复 | 所有项编号重置为 1(显示为 a/a/a) |
| 列表 `level` 从 1 开始 | `"level": 1` 作为顶级 | 顶级列表项多一层缩进;建议:顶级 `"level": 0`,子级 `"level": 1` |
| 用 `fontFamily` 设字体 | `"fontFamily": "Arial"` | 校验报错;正确写法:`"fonts": {"ascii": "Arial", "eastAsia": "..."}` |
| 用多个 span “换行” | 同一 `p` 内放两个 `span` | 不会产生换行;每个换行必须是独立的 `p` 节点 |
### 基础结构
文件内容是一个裸 JSONML 数组,根节点为 `"root"`
```json
["root", {},
["h2", {}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "章节标题"]]],
["p", {}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "正文段落"]]]
]
```
- 根节点固定 `"root"`(不是 `"body"`
- `--name` 已是 H1JSONML 从 `h2` 开始
- 表格结构是 `table → tr → tc`(无 `th`/`td`
- 分栏是 `table` + `"sr": true``tc` 建议设 `fill` 背景色
- 有序列表:仅第一项设 `"start": 1`,后续项不设 `start`(系统自动递增)
- 列表 `level` 建议从 0 开始:顶级项 `"level": 0`,子项 `"level": 1`,以此类推
- uuid 必须显式提供(CLI 不自动生成)
- **每行内容对应一个 `p` 节点** — 同一 `p` 内的多个 `span` 不会换行,只会横向拼接;需要换行时必须拆分为多个 `p`
### 视觉设计要点
构造时主动使用这些属性实现视觉效果:
- **文字着色**leaf 上 `"color": "#hex"``"highlight": "#hex"`
- **highlight 色带**:标题 leaf 上 `"highlight": "#浅色"` 可形成色条效果(比 callout 更轻量的板块标记)
- **字号**leaf 上 `"sz": 14, "szUnit": "pt"`
- **callout**`["container", {"subType": "colorBlocks", "metadata": {"bgcolor": "#E8F5E9", "border": "left"}}, ...blocks]`
- **callout + sticker**`["container", {"subType": "colorBlocks", "metadata": {"bgcolor": "#FEF3F3", "showstk": true, "sticker": "火"}}, ...blocks]`
- **表格单元格底色**tc 上 `"fill": "#hex"`
### 写入
```bash
dws doc +create --name "<文档名>" --content @./drafts/<name>.json --doc-format jsonml
```
### 验收
读取 `+create` 返回的 `verified``steps``nodeId` 和验证结果。只有返回 `verified=false` 或结构化 partial/unknown 错误时,才进入恢复流程。
---
## 正文准备(未命中 JSONML 判定时)
正文草稿先在工作目录内的 Markdown 文件中完成,推荐路径形如 `./drafts/<name>.md`
准备规则:
- 只使用用户已提供或对话中已确认的正文素材。
- 如果正文素材不足,先补齐文档目标、受众、章节和缺口;不要在本文中临时扩展跨产品采集流程。
- **先按 [doc-style-guideline.md §2.0 类型判断决策表](./doc-style-guideline.md) 确定文档类型,再用对应类型的骨架样板(§2.1 决策型 / §2.2 执行型 / §2.3 说明型 / §2.4 知识沉淀型)**。不要套通用三段式。
- **`--name` 已是 H1,正文从 `##` 开始**;正文内不要再写 `#` 一级标题(除非确实需要正文内再造一级 H1 并说明动机)。
- 摘要、bullet、引用块、callout 等元素的使用边界以 style-guideline §3-§7 为准。
- 同类信息保持一致:风险、状态、行动项各用一种元素 + 一种视觉语义(style-guideline §1.2 / §5)。
- 临时文件必须保留真实换行,不能把换行写成字面量 `\n`
- Markdown 草稿阶段**不要**写 callout / 分栏 / 附件——这些留到「创建后的精修」用 `doc block insert` 操作(style-guideline §1.3)。
- **图片素材闭环(硬规则)**:正文需求含图片/截图/图文并茂时,Markdown 只写文本骨架和图片占位说明;创建后用 `dws doc +media-insert --node <nodeId> --file ./相对路径` 插入,再用 `+media-list` 验证稳定 `resourceId`。禁止正文临时 URL、绝对路径、curl/wget 和本地依赖安装兜底。
## 创建写入
优先用工作目录相对文件一次创建并写入:
```bash
dws doc +create --name "<文档名>" --content @./drafts/<name>.md --doc-format markdown
```
创建到指定文件夹:
```bash
dws doc +create --name "<文档名>" --content @./drafts/<name>.md --folder <DOC_FOLDER_NODE_ID> --doc-format markdown
```
创建到知识库:
```bash
dws doc +create --name "<文档名>" --content @./drafts/<name>.md --workspace <WS_ID> --doc-format markdown
```
短纯文本才允许直接传 `--content`
```bash
dws doc +create --name "<文档名>" --content "短内容" --doc-format markdown
```
返回后立即记录:
| 字段 | 用法 |
|------|------|
| `nodeId` | 后续 `doc read``doc update``doc block``doc media` 的目标 |
| `docUrl` | 最终交付给用户的链接;缺失时用 `doc info` 补查 |
| `chunksWritten` | 判断是否触发自动分片;大于 1 时重点检查章节顺序 |
## 内置回读验收
`+create` 已在同一执行内回读验证,禁止再固定追加一次 `doc read`。检查结构化返回:
验收要点:
- 开头摘要、关键章节、表格表头、末尾章节都存在。
- 回读文本顺序和临时 Markdown 一致。
- 没有把字面量 `\n` 渲染成一整行。
- 如果返回 `chunksWritten > 1`,看 `degradations`:为空即表示分片没有改变渲染结构,无需人工核对边界;非空时按其中的 `kind``line` 定点检查(如 `table_split` 表示该表被拆成多张、每张带重发的表头)。
- 最终回复必须给用户 `docUrl`;如果只拿到 `nodeId`,说明链接字段未返回,并报告已尝试 `doc info`
## 缺失补救
DWS 写入管道会自动处理长内容分片。只有出现以下情况才手工补片:
- 返回 `doc_write_commit_unknown`(分片超时,提交状态未知)
- 命令超时或只写入部分分片
- 回读发现后半段缺失、章节乱序或表格损坏
补救流程:
1.`+fetch` 的最小 scope 确认已经写到哪个章节。
2. 从原始 Markdown 中截取缺失部分,写入 `./drafts/<name>-resume.md`
3. 追加缺失内容:
```bash
dws doc +update --node <nodeId> --command append --content @./drafts/<name>-resume.md --doc-format markdown
```
4. 使用 `+update` 返回的验证结果确认缺失章节已补齐;结果未知时再定点 `+fetch`
## 创建后的精修
创建流程本身优先完成整篇正文。只有需要局部补充、插入附件、加 callout / 分栏、或无损结构调整时,才进入精修——**精修路径统一走 [doc-update-workflow.md](./doc-update-workflow.md)**。
精修常见入口(**按 [doc-update-workflow.md §1.3](./doc-update-workflow.md) 优先级排序:JSONML 首选**):
- 单 block JSONML 精修(首选):`doc block list --node <id> --content-format jsonml --block-id <uuid>` 取子树 → `doc block update --node <id> --block-id <uuid> --content-format jsonml --element '[...]'` 写回(uuid 必须 == --block-id;写入端默认执行 schema validate,详见 [doc-update-workflow.md §4.4](./doc-update-workflow.md)
- 整篇 JSONML 无损:`doc update --content-format jsonml --mode overwrite`(默认直接覆盖,适合一次改多处或改 root sectPr;担心并发覆盖时加 `--revision <N>` 触发并发检查)
- 插入附件 / 图片:`+media-insert`,之后用 `+media-list` 验证稳定 `resourceId`
- element JSON 次选:`doc block insert` / `doc block update` 不带 `--content-format jsonml` 时按老接口 JSON 解析;仅在 JSONML 不支持某字段时使用
- markdown 兜底:`doc update --mode append`(末尾追加纯文本段落,无富结构需保留时)
字段结构以 [`doc.md`](../../doc.md) 为准;何时用何种精修路径见 [doc-update-workflow.md §3「改写路径速查」](./doc-update-workflow.md)。
## 交付口径
只报告已经验证过的信息:
- 文档标题
- `docUrl``nodeId`
- 已写入的正文范围
- 回读验收结果
- 如有缺失,说明缺失位置和补救状态
未回读前,不要说内容完整或任务完成。
@@ -0,0 +1,404 @@
# 钉钉文档排版规范
本文规定 DWS 创建或编辑钉钉文档时的排版判断方法。核心流程:**确定文档类型 → 选骨架 → 按读者任务选元素 → 按视觉语义统一表达 → 软约束自检**。
> 本文只定义内容结构和视觉规范,不定义命令路由。写入统一使用 `+create/+update/+checkpoint-update`,读取使用 `+fetch`,媒体使用 `+media-*`;原子命令仅用于精确 Schema 支持的专家路径。流程见 [doc-create-workflow.md](./doc-create-workflow.md) 和 [doc-update-workflow.md](./doc-update-workflow.md)。
## 快速入口
按任务定位章节,不必通读全文:
| 任务 | 必读章节 |
|------|---------|
| 起稿前必读 | §2.0 + §2.0.1 + §3.0 |
| 不确定文档类型 | §2.0 类型判断决策表 |
| 写决策型(日报/汇报/方案选型) | §2.1 + §3 + §5 + §7 |
| 写执行型(SOP/Runbook/接入指南) | §2.2 + §3 + §4.4 + §5 + §7 |
| 写说明型(接口文档/能力清单) | §2.3 + §3 + §4.3/§4.4 + §7 |
| 写知识沉淀(调研/技术方案/复盘) | §2.4 + §3 + §4.6 |
| 颜色 / emoji 选择 | §5 |
| 何时插图 | §6 |
| 改写老文档 | 直接看 [doc-update-workflow.md](./doc-update-workflow.md) |
| 写完自检 | §8 判定表 |
---
## 一、硬规则
1. **`--name` 是 H1**:正文从 `##` 开始;正文内不写 `#`(除非确需正文内再造一级 H1 并说明动机)
2. **同类信息同表达**:风险、状态、行动项、证据,每类只用一种元素 + 一种视觉语义(见 §5)
3. **Markdown 草稿阶段只用稳定元素**:标题、段落、列表、checklist、表格、代码块;callout / 分栏 / 附件 / 复杂嵌套留到创建后用 `doc block insert` / `doc media insert` 精修
4. **引用块只用于原文**:用户原话、会议摘录、外部材料原文;不许包装作者自己的结论
5. **不编造 URL**:图片、链接、文档 ID 不确定时留 TODO 占位,向用户求证
6. **以 shortcut 内置验证为准**:正常成功不追加整篇回读;partial/unknown 或富结构定点检查才使用最小范围 `+fetch`
---
## 二、按文档类型选骨架
### 2.0 类型判断决策表
按读者**第一个动作**选类型。若同时符合多类,按表中第一行优先:
| 读者第一个动作 | 类型 | 推荐格式 | 视觉锚点 | 跳转 |
|----|----|----|----|----|
| 按步骤操作(升级、部署、接入、上手) | 执行型 | markdown 起稿 + JSONML 精修(callout 标高风险) | 有序列表 / 代码块 / ⚠️ 高风险 callout | §2.2 |
| 拿结论做选择 / 决策 / 汇报判断 | 决策型 | **直接 JSONML 起稿**(不走 markdown → 精修;见 [doc-create-workflow.md §JSONML 起稿](./doc-create-workflow.md#jsonml-起稿判定) | ✅ 推荐 callout / 对比表 / 数据加粗 | §2.1 |
| 查参数 / 能力 / 限制 / 错误码 | 说明型 | markdown(表格密集、callout 偶尔) | 参数表 / 错误码表 | §2.3 |
| 看推理链路 / 分析 / 调研过程 | 知识沉淀型 | 含对比/数据可视化时**直接 JSONML 起稿**;纯叙事时 markdown + 精修(见 [doc-create-workflow.md §JSONML 起稿](./doc-create-workflow.md#jsonml-起稿判定) | ️ 信息 callout / 引用块(原话)/ 流程图 | §2.4 |
| 以上都不像 | 兜底走知识沉淀型 §2.4 | — | — | — |
> **推荐格式列**:起稿统一用 markdown,富结构(callout/分栏/带颜色的对比/sectPr)一律走精修阶段的 JSONML;JSONML 形态优先级与命令见 [doc-update-workflow.md §1.3](./doc-update-workflow.md)。决策型默认进 JSONML 优先,因为汇报/方案的视觉锚点(callout + 彩色表头)markdown 表达不出来。
### 2.0.1 写前三问(草稿前 30 秒自答)
下笔前先答三句话;答不出第二、三句说明信息不足,回 [doc-create-workflow.md «创建前检查»](./doc-create-workflow.md) 补齐:
1. **读者**:谁打开这篇文档?读完要做什么动作(操作 / 选择 / 查参数 / 看推理)?
2. **唯一记忆点**:读者关掉文档后,最想让他记住的一句话是什么?这句话决定开头摘要 / callout 该写什么。
3. **形态**:按 §2.0 推荐格式列 + [doc-create-workflow.md §JSONML 起稿判定](./doc-create-workflow.md#jsonml-起稿判定) 决定路径。命中判定条件(决策型 / 含对比的知识沉淀型 / 用户意图关键词)→ **直接 JSONML 起稿**(但必须先完成 [doc-create-workflow.md §设计规划](./doc-create-workflow.md#设计规划jsonml-起稿前必做) 的 4 步规划);未命中 → markdown 起稿 + 创建后精修。
写完自检时回看这三个答案:开头有没有兑现「记忆点」、形态有没有兑现「推荐格式」。两条任一不兑现,按 §8 自检表对应行动。
### 2.1 决策型(日报、月报、复盘、方案选型、汇报)
**适用**:读者读完要拿到判断或做选择。
| 段位 | 内容 | 推荐元素 |
|------|------|----------|
| 开头 | 结论、推荐方案、关键数据 | 2-4 条 bullet 摘要 |
| 主体 | 选项 / 维度 / 风险 / 数据 | 对比表、风险表、关键指标 |
| 收尾 | 下一步、需用户决策事项 | callout(仅决策有时限或重大风险)|
**反推荐**:长背景铺垫、连续叙事、结论藏在文末。
样板:
~~~~markdown
## 摘要
- 推荐方案 A:上线快、依赖已有流程
- 主要风险:权限配置需补
- 决策时限:本周五前
## 方案对比
| 维度 | 方案 A | 方案 B | 建议 |
|------|--------|--------|------|
| ... | ... | ... | ... |
## 下一步
- [ ] @负责人 完成权限配置
~~~~
### 2.2 执行型(SOP、TODO、行动方案、接入指南、Runbook)
**适用**:读者读完要按步骤操作。
| 段位 | 内容 | 推荐元素 |
|------|------|----------|
| 开头 | 目标、范围、前置条件 | 短段落 + checklist(前置条件)|
| 主体 | 顺序步骤、操作命令、校验方法 | 有序列表、代码块、流程截图 |
| 收尾 | 异常处理、回滚方法 | 表格(错误码 → 处理)或 callout(高风险动作)|
**反推荐**:多动作压成一段、缺负责人、缺校验方法。
样板:
~~~~markdown
## 目标
将服务 X 从 v1 升级到 v2,零宕机切换。
## 前置条件
- [ ] 备份当前配置
- [ ] 通知下游
## 操作步骤
1. 拉取最新镜像:`docker pull x:v2`
2. 灰度切流:5% → 50% → 100%
3. 每步校验:观察 dashboard,错误率 < 0.1%
## 异常处理
| 错误码 | 含义 | 处理 |
|--------|------|------|
| ... | ... | ... |
~~~~
### 2.3 说明型(产品说明、新人手册、接口文档、能力清单)
**适用**:读者按需查阅,不一定从头读到尾。
| 段位 | 内容 | 推荐元素 |
|------|------|----------|
| 开头 | 适用对象、能力概要 | 短段落或 bullet |
| 主体 | 功能矩阵、参数表、使用示例 | 表格、代码块(带语言标识)|
| 收尾 | 限制、注意事项、变更记录 | callout(限制)、表格(变更记录)|
**反推荐**:长结论、未分类的功能混排、缺示例。
样板(其中"调用示例"位置应放一个 `bash` 语言标识的代码块演示 dws 命令):
~~~~markdown
## 适用对象
本接口供 DWS 内部模块调用,不暴露给外部租户。
## 能力清单
| 能力 | 说明 | 必要参数 |
|------|------|----------|
| ... | ... | ... |
## 调用示例
(此处放一个 bash 代码块演示 dws 命令)
## 限制
> ⚠️ 单次返回最多 1000 个 block,超出请分页。
~~~~
### 2.4 知识沉淀型(调研报告、技术方案、项目复盘、学习笔记)
**适用**:读者要看到推理链路,理解为什么是这个结论。
| 段位 | 内容 | 推荐元素 |
|------|------|----------|
| 开头 | 背景、问题、目标 | 短段落 |
| 主体 | 分析过程、对比、推理 | 小标题分层、表格、引用块(外部原文)|
| 收尾 | 结论、证据链、附件 | 附件(原始材料)|
**反推荐**:结论先行但缺证据链、引用块包装作者自己的判断。
---
## 三、按读者任务选元素(五列表)
### 3.0 AI 文档常见反模式
下笔前快速扫一遍,命中任何一条立刻按右列改:
| 反模式 | 信号 | 改法 |
|------|------|------|
| 万篇一律的「摘要-细节-总结」三段式 | 每篇文档第一节都是「## 摘要」+ 三条 bullet | 按 §2.0 选骨架;执行型不要写摘要,开门见山列前置条件 |
| 全篇 H2 平铺 | 标题层级单一、没有 H3 收纳同主题 block | 按 §7 量化标尺拆 §N.N 子标题 |
| 全列表无表无 callout | 风险、对比、数据全部塞进 bullet | 对比改表(§4.2)、风险改 callout(§4.7)、数据加粗(§5|
| 结论藏文末 | 关键判断在最后一段才出现 | 按 §2.1 决策型骨架,结论 / 推荐方案前置到摘要 bullet |
| markdown 包不住富结构却硬包 | 草稿里出现 `> ⚠️ ...``【callout】`、表格里塞颜色 hex | 按 §2.0 推荐格式切到 JSONML 精修阶段;草稿留占位 |
| 同一语义两种视觉表达 | 风险既用 ⚠️ 又用红色文字也加 callout | §5 收敛到一组(emoji + 颜色 + 元素都按表对齐)|
| 装饰性 emoji 满天飞 | 段落、列表项、普通段每行都带 emoji | §5 规则:emoji 只在 callout / 状态标记 / `H2/H3` 标题前 |
| 编造 URL / 文档 ID / 图片地址 | 出现 `https://example.com/...``docs.dingtalk.com/xxx` 占位形 | §4.5 / §6 留 TODO 占位,向用户求证 |
### 3.1 读者任务对照
骨架定好后按读者要完成的具体动作选元素。扫"常见误用"列,命中则按"修复"列调整。
| 读者任务 | 推荐版式 | 避免 | 常见误用 | 修复方法 |
|----------|----------|------|----------|----------|
| 快速了解重点 | 开头 2-4 条 bullet 摘要;重大风险用 callout | 长背景;引用块包装摘要 | 三段式硬套,背景写了 5 段才到结论 | 结论提前到第一条 bullet,背景挪到末尾或删 |
| 做选择 | 多维比较用表格;轻量两项可分栏(精修阶段)| 只两个对象做大宽表 | 两个方案做 6 列对比表,每列一句话 | 改为两段并列短文,或保留 3 维核心对比 |
| 看状态 | 状态表(事项/状态/阻塞/下一步)或 checklist | 长段落描述多个状态 | "A 在做、B 卡住、C 完成…" 写一段 | 转为状态表,每行一个事项 |
| 执行动作 | 有序列表;待办用 checklist;分支用条件表 | 一段话写多个动作 | "先备份再升级然后切流" 写一段 | 拆为有序列表,每步独立 |
| 理解关系 | 小标题分层;复杂关系用图示或附件 | 把复杂关系压成连续段落 | 系统依赖关系写了三段叙事 | 建议用户上传架构图(§6)|
| 查证事实 | 引用块保留原文;真实文件用附件 | 引用块包装改写后的总结 | 作者自己的结论加 `>` 装成引用 | 改为普通段落或加粗 |
| 阅读背景 | 小标题 + 短段落;过长时拆列表 | 为结构化把叙事硬塞表格 | 把"项目历史"硬做时间表 | 用小标题分段叙事 |
**推荐表头**
- 风险表:`风险 / 影响 / 缓解 / 负责人`
- 状态表:`事项 / 状态 / 阻塞点 / 下一步`
- 对比表:`维度 / 方案 A / 方案 B / 建议`
---
## 四、元素边界规范
### 4.1 标题与段落
- 正文从 `##` 开始(H1 已被 `--name` 占用)
- 标题层级 ≤ 4 层(§7
- 单段过长先拆段,再考虑换元素
### 4.2 列表与 checklist
- 普通列表:并列要点
- 有序列表:顺序步骤
- checklist:待办状态(含 `- [ ]` / `- [x]`
列表项里开始出现"负责人 / 截止时间 / 状态"这类字段时,改用表格。
### 4.3 表格
表格用于字段稳定的信息。
- 单元格写短句;解释超过两行放表格下方段落
- 列数 ≤ 6,行数 ≤ 20(§7 给具体拆法)
### 4.4 代码块
必须带语言标识(`bash` / `python` / `json` / `go` 等);无对应语言时用 `text`
形如(用 \`\`\` 三反引号围栏 + 紧跟语言名 + 闭合 \`\`\`):
~~~~markdown
```bash
dws doc +fetch --node abc123
```
~~~~
约束:
- 单块 ≤ 80 行;超长时拆为多个语义独立的块,或作为附件上传
- 与正文有强关联时,在代码块前后用一句话说明用途
- **禁止**在代码块里写敏感信息(token、密码、内部 IP)
### 4.5 链接与卡片
| 场景 | 用法 | 典型例 |
|------|------|--------|
| 行内引用 | `[文字](url)` | PR/Issue/外部博客 |
| 强调外部资源 | 创建后 `doc block insert` 插入卡片 | 外部 PRD、Figma、Notion 主页 |
| 引用钉钉文档 | 直接粘贴 alidocs URL | @文档DWS 自动渲染卡片)|
**禁止**:编造看似真实的 URL;不确定的链接留 TODO。
### 4.6 引用块
只用于需保留原貌的内容:用户原话、会议摘录、外部材料原文、API 错误信息原文。
**禁止**把作者自己的摘要、推荐结论或判断写成引用块。
### 4.7 Callout
每篇文档 0-2 个(§7)。
| 场景 | 处理 |
|------|------|
| 创建前必须确认的限制 | ✅ 用 callout |
| 会改变结论的风险 | ✅ 用 callout |
| 需要用户立即决策的分歧 | ✅ 用 callout |
| 普通章节说明 | ❌ 普通段落 |
| 装饰性章节开头提示 | ❌ 删除 |
| 可放进摘要 bullet 的一般结论 | ❌ 放摘要 |
callout 是块级元素,**Markdown 草稿阶段不支持**。创建后用 `doc block insert` 精修。
**形态优先级(按 [doc-update-workflow.md §1.3](./doc-update-workflow.md)**
1. **首选 JSONML**`doc block insert --content-format jsonml --element '["container",{"uuid":"...","subType":"colorBlocks","metadata":{"bgcolor":"...","border":"..."}},["p",...]]'`bgcolor/border 取本文 §5 颜色表
2. **次选 element JSON**`doc block insert --element '{"blockType":"callout","callout":{...}}'`,字段以 [doc.md](../../doc.md) 为准;JSONML 节点完整结构见 [doc-jsonml-schema.md](../format/doc-jsonml-schema.md) 的 `container[subType="colorBlocks"]`,可复制范例见 [doc-jsonml-cookbook.md](../format/doc-jsonml-cookbook.md)
### 4.8 分栏(精修阶段)
分栏适合两个对象的轻量并置。**Markdown 草稿阶段无法直接写入**,必须创建后用 `doc block insert`
**形态优先级(按 [doc-update-workflow.md §1.3](./doc-update-workflow.md)**
1. **首选 JSONML**(保真度最高、和现有 block 结构一致):
```bash
dws doc block insert --node <nodeId> --content-format jsonml \
--element '["container",{"uuid":"cols1","subType":"columns","metadata":{"size":"2"}},["container",{"uuid":"cols1c1","subType":"column"},["p",{"uuid":"cols1c1p"},["span",{"data-type":"text"},["span",{"data-type":"leaf"},"左栏"]]]],["container",{"uuid":"cols1c2","subType":"column"},["p",{"uuid":"cols1c2p"},["span",{"data-type":"text"},["span",{"data-type":"leaf"},"右栏"]]]]]'
```
2. **次选 element JSON**(老接口,仅当 JSONML 不便构造时):
```bash
dws doc block insert --node <nodeId> --content-format element \
--element '{"blockType":"columns","columns":{"size":2},"children":[{"blockType":"paragraph","paragraph":{"text":"左栏"}},{"blockType":"paragraph","paragraph":{"text":"右栏"}}]}'
```
每栏需要多个字段时**改用表格**。轻量两项对比优先并列短段落,分栏只在视觉强对照需要时使用。完整 JSONML 字段见 [doc-jsonml-schema.md](../format/doc-jsonml-schema.md) 的 `container[subType="columns"]`,可复制范例见 [doc-jsonml-cookbook.md](../format/doc-jsonml-cookbook.md)。
### 4.9 附件与图片
```bash
dws doc +media-insert --node <nodeId> --file ./diagram.png
```
- 插入后在前后用一句话说明它支持哪个结论
- 插入后用 `doc block list` 验证存在
- **禁止**在 Markdown 里编造无法访问的图片 URL
- **禁止**把 `alidocs.dingtalk.com/i/nodes/...``alidocs.dingtalk.com/i/document/...` 或任何文档/节点页面 URL 当作图片 src。图片必须位于工作目录内,再通过 `dws doc +media-insert --node <docId> --file ./相对路径` 插入
- 何时主动建议用户提供图见 §6
---
## 五、颜色与视觉语义
**全篇必须保持语义一致**——同一语义只用同一组视觉表达。
| 语义 | emoji 前缀 | callout bgcolor (hex) | callout border (hex) | 文字加粗 | 典型用法 |
|------|----------|------------------------|----------------------|---------|----------|
| 信息说明 | ️ | `#E8F2FE`(淡蓝)| `#B3D4FC` | — | 普通提示、说明性补充 |
| 推荐结论 | ✅ | `#E3F8E2`(淡绿)| `#B7E4B5` | 是 | 推荐方案、已确认结论 |
| 风险/错误 | ⚠️ / ❌ | `#FDE2E0`(淡红)| `#F5C2C7` | 是 | 风险、错误码、不可逆操作 |
| 待确认 | ❗ | `#FFF6D9`(淡黄)| `#FFE69C` | — | 待用户决策、待补齐信息 |
| 中性辅助 | — | `#F4F5F7`(淡灰)| `#DEE2E6` | — | 非关键背景、变更记录 |
调用规则:
- callout 字段格式以 [doc.md](../../doc.md) / [doc-jsonml-schema.md](../format/doc-jsonml-schema.md) 为准
-`doc block insert --element` 插入 callout 前若字段名不确定,先用 `doc block list --node <id> --block-type callout` 抓现有实例确认字段
- 颜色属性值只接受 hex;**禁止**把语义名(如 `light-blue`)当属性值
- 关键指标用加粗 + ↑↓ 或 +/- 同时标注方向(不仅依赖颜色,兼容色觉无障碍)
- emoji 只在 callout、状态标记、`H2/H3` 标题前使用,不在普通段落和列表项里滥用
---
## 六、何时使用图示
以下信息特征出现时,**主动建议用户提供截图或示意图**,不用纯文本承载:
| 内容特征 | 信号词 | 建议图示 |
|----------|--------|----------|
| 多步骤流程(≥4 步)| "先…然后…最后"、"步骤 1/2/3" | 流程图截图 |
| 系统/模块依赖 | "调用"、"依赖"、"上游/下游"、"请求→响应" | 架构图截图 |
| 时间线/里程碑 | "Q1/Q2"、"阶段一→阶段二"、日期序列 | 时间线图 |
| 数值趋势 | 带数字的时间序列、"增长/下降"、百分比变化 | 折线图/柱状图截图 |
| 占比分布 | "占比"、"份额"、百分比加总 ≈100% | 饼图/树状图截图 |
| 层级递进 | "基础→进阶→高级"、"L1/L2/L3"、"核心→外围" | 金字塔图 |
| 因果/根因 | "导致"、"根因"、"原因"、"影响因素" | 鱼骨图 |
| 闭环/飞轮 | "正循环"、"驱动"、"闭环"、"反馈" | 飞轮图 |
**规则**
1. 关键流程/架构/趋势能图示就图示,不用纯文本承载
2. **禁止**在 Markdown 里编造图片 URL(如 `![](https://example.com/diagram.png)`
3. 正文里留占位(如 `📌 待补充:架构图`),向用户主动询问能否提供截图
4. 用户提供后用 `dws doc +media-insert --node <id> --file ./xxx.png` 插入,并在图前后补一句说明
---
## 七、量化标尺(软约束)
超出时不一定要重写,但要按"超出处理"列采取具体动作:
| 维度 | 建议上限 | 超出处理(具体动作)|
|------|---------|---------------------|
| 单段行数 | ≤ 5 行 | 按句号拆段;或抽出并列要点转 bullet 列表 |
| 单章节 block 数 | ≤ 12 | 按子主题拆 `### N.N` 子标题;合并冗余 block |
| 标题层级 | ≤ 4 层 | 把最深层标题降级为加粗段落或列表 |
| 表格列数 | ≤ 6 | 按"维度类别"拆为多个子表,前 2 列保留作锚 |
| 表格行数 | ≤ 20 | 拆表(按类别);或转附件 `doc media insert --file table.xlsx` |
| callout 数量 | ≤ 2 / 篇 | 同语义 callout 合并;非关键 callout 降级为加粗或行内 emoji |
| 代码块行数 | ≤ 80 | 按"功能段"拆为多个独立块;超长输出转附件 |
| 连续纯文本段 | ≤ 3 段 | 中间穿插 `##/###` 标题、bullet、或表格分组 |
---
## 八、自检判定表
写完后逐条扫描,命中"判定"列的情况按"动作"列处理:
| 判定 | 动作 |
|------|------|
| 命中 §3.0 任一反模式 | 按 §3.0 对应行改 |
| 文档类型不属于 §2 四类之一 | 回 §2.0 决策表归类;仍归不出走 §2.4 兜底 |
| `--name` 之外正文里还有 `#` 一级标题 | 改为 `##`,除非确需且已说明动机(§4.1|
| callout 数 > 2 | 按 §4.7 合并同语义;非关键 callout 降级 |
| 单段 > 5 行 | 按 §7 拆段或转列表 |
| 单章节 block 数 > 12 | 按 §7 拆 `### N.N` 子标题 |
| 表格列 > 6 或行 > 20 | 按 §7 拆表或转附件 |
| 代码块缺语言标识 | 补语言标识;无对应语言用 `text`(§4.4|
| 代码块 > 80 行 | 按功能段拆,或转附件(§7)|
| 同一语义出现 ≥2 种视觉表达 | 按 §5 收敛到同一组 |
| 引用块里写的是作者自己的结论 | 改为加粗段落或普通段落(§4.6)|
| Markdown 草稿里写了 callout / 分栏 | 删掉,转到精修阶段用 `doc block insert`(§4.7 / §4.8|
| 内容含 "先…然后…"、"调用/依赖"、"Q1/Q2"、"导致/根因" 等信号词但写成纯文本 | 按 §6 询问用户能否补图 |
| 出现编造的图片 URL / 文档 URL | 删除或改为 TODO 占位(§4.5 / §6|
| 写入完成但未回读 | 按 [doc-create-workflow.md «回读验收»](./doc-create-workflow.md) 或 [doc-update-workflow.md §6](./doc-update-workflow.md) 回读 |
@@ -0,0 +1,316 @@
# 钉钉文档改写流程
本文只处理已有文档的结构和排版策略。普通执行入口固定为 `+fetch``+update``+checkpoint-update`;这些 shortcut 统一确认、写入与验证。原子 `doc read/update/block` 只保留给 shortcut 未公开参数的 JSONML 专家路径,使用前必须读取精确 leaf Schema。
## 适用边界
进入本文前,必须已经确认用户要改写的是已有钉钉文档 (`adoc`)。如果用户要新建、要操作表格 / AI 表格 / 文件 / 知识库空间 / 发消息,不要套用本文。
本文覆盖:
- 已有文档的局部改写、润色、章节补充
- 段落 ↔ 列表 ↔ 表格 的形态转换
- 块级精修(callout、分栏、附件插入)
- overwrite 整篇改写的风险提示与执行
- JSONML 无损结构改写的入口
本文不覆盖:
- 新建文档(见 [doc-create-workflow.md](./doc-create-workflow.md)
- 知识库空间管理
- 文档权限、消息分发、待办分派
---
## 一、核心原则
### 1.1 精准手术优于全量覆盖
**默认走精准手术**——只改用户指定的章节或 block,不动其他内容。具体路径见 §3 速查表。
### 1.2 保真约束
改写时必须**原样保留**以下要素,**不许**替换为纯文本/姓名/链接/占位符:
- `@人` 引用(用户、机器人、群)
- `@文档` / `@群` 等卡片引用
- 已上传的附件、图片
- 用户原话引用块
- 表格表头(除非语义错误且用户确认)
JSONML 模式下这些元素的节点结构见 [doc-jsonml-schema.md](../format/doc-jsonml-schema.md)。
### 1.3 编辑形态优先级
**改写已有文档优先 JSONMLmarkdown / element 只在 JSONML 不适用时兜底**
| 优先级 | 形态 | 适用 |
|--------|------|------|
| ① 首选 | `--content-format jsonml` | 保真度最高;callout / 分栏 / 表格 / @人 / 附件 / 颜色 / 嵌套结构都能 1:1 round-trip;写入端有 validator 兜底(§4.4 |
| ② 次选 | `--content-format element`JSON,老接口) | JSONML 不支持某个块字段时;或快速插入 callout / 分栏不想构造 JSONML 时;不保真改写正文 |
| ③ 兜底 | markdown(不带 `--content-format` 即默认)| 纯文本追加、整篇重排骨架;callout / 分栏 / 颜色 / 部分属性会被 markdown 还原过程丢失 |
实操判断:
- 用户给已有 nodeId 要「改一段、改属性、加 callout、动结构」——走 §4.4 JSONML 路径
- 用户要「在末尾追加一节纯文本 / 整篇按新骨架重写」——走 §4.2 / §4.5 markdown 路径
- 同一次任务里两类需求都有——分别走对应路径,**不要**为了省事全部 markdown overwrite
### 1.4 写入风险提示
`doc update` 在以下场景可能产生**静默失败**(返回 success=true 但实际写入不完整):
- **overwrite 降级为 append**:大文档 overwrite 被后端静默降级,导致旧内容未清除、新内容追加在末尾
- **分块 append 内容截断**:超长文档分片写入时部分片段丢失或顺序错乱
- **编码/通道问题**:特殊终端下 UTF-8 内容传输乱码
因此写入必须通过带确认与验证的 shortcut。`+update/+checkpoint-update` 返回的验证结果是主证据;禁止无条件追加一次原子 `doc read`,只有 partial/unknown 或需要定点结构检查时才用 `+fetch`
---
## 二、读取策略
改写前必须先读现有内容,但要节省上下文。按粒度选读取方式(按 §1.3 优先级排序,**优先 JSONML**):
| 用户需求 | 读取方式 | 定位方法 |
|----------|----------|----------|
| 单块精修(首选)| `doc block list --node <id> --content-format jsonml` → 拿 uuid → `doc block list --node <id> --content-format jsonml --block-id <uuid>` 读子树 | 节点结构见 [doc-jsonml-schema.md](../format/doc-jsonml-schema.md) |
| 多处保真改写 / 改 root sectPr | `+fetch --node <id> --detail full` | 解析 JSONML;担心并发覆盖时记下 revision |
| 整篇按新骨架重写(纯文本场景)| `+fetch --node <id>` | 直接处理 markdown 全文 |
| 末尾追加纯文本章节 | 不必读全文,直接 §4.2 append | 必要时 `+fetch --scope section` 看末尾衔接 |
| 老接口快速找 BLOCK_ID(无需 jsonml 时)| `doc block list --node <id>` | 默认输出 JSON;用 `grep -B2 -A2 "<关键词>"` 在 children 里定位(结构 `{"blocks":[{...,"children":[...]}]}`jq 需 `..\|.text? // empty` 递归查文本) |
读取后,把改写计划告诉用户(要改哪几节、走 JSONML 还是 markdown、改成什么形态),等用户确认后再写。
---
## 三、改写路径速查
按用户请求形态查表,跳到对应详细节执行(**按 §1.3 优先级排序:JSONML 路径在前,markdown / element 兜底在后**):
| 用户请求 | 推荐路径 | 详细节 |
|----------|----------|--------|
| 改某一章 / 某一节(首选) | block list 拿 uuid → block update --content-format jsonml | §4.4 路径 B |
| 改属性 / 改 mark / 改颜色不动文本 | block update --content-format jsonml | §4.4 路径 B |
| 插入 callout / 分栏 / 嵌套结构(首选) | block insert --content-format jsonml --element '[...]' | §4.4 路径 B |
| 多处保真改写 / 改 root sectPr | 整篇 JSONML overwrite(默认不带 --revision;并发敏感时再加) | §4.4 路径 A |
| 中间插一段纯文本 | block insertelement JSON 或 jsonml | §4.3 / §4.4 |
| 末尾追加一节纯文本 | doc update --mode appendmarkdown | §4.2 |
| 整篇按新骨架重写 | overwrite 全文(优先 JSONML;纯文本可用 markdown | §4.5 |
| 段落转表格 / 表格转段落 | block update --content-format jsonml;或 markdown overwrite 单段 | §4.4 / §4.1 |
| 插入附件 / 图片 | doc media insert | §4.3 |
| 一次追加 >200KB 内容 | 分块 append + 用户风险确认 + 逐片记录 | §4.6 |
| 兜底:纯文本快速替换某段 | doc update --content overwritemarkdown | §4.1 |
---
## 四、改写路径详细
> **首选 JSONML(§4.4**——保真度最高且 validator 兜底;本节其余路径(markdown / element)仅在 §1.3 列出的"次选 / 兜底"场景下使用。
### 4.1 段落级 overwritemarkdown 兜底路径)
> 适用范围:**纯文本**改写一段或替换某节内容。若该段含 callout / 分栏 / 颜色 / @人 / 附件 / 嵌套结构,**改走 §4.4 路径 B**——markdown 还原会丢失这些元素。
```bash
dws doc +update --node <nodeId> --command overwrite --content "<新内容>" --doc-format markdown
```
或写入临时文件:
```bash
dws doc +update --node <nodeId> --command overwrite --content @./drafts/<name>-section.md --doc-format markdown
```
> ⚠️ **overwrite 须用户确认**——尤其是整篇文档 overwrite。
### 4.2 追加章节(markdown
> 适用范围:在文档末尾加 X 章 / 补充纯文本段落。追加内容若含 callout / 分栏等富结构,先用本节 append 一个占位段落,再用 §4.4 路径 B 的 `block insert --content-format jsonml` 替换/精修。
```bash
dws doc +update --node <nodeId> --command append --content @./drafts/<name>-append.md --doc-format markdown
```
按 [doc-style-guideline.md](./doc-style-guideline.md) 的元素选择规则准备追加内容。
### 4.3 块级精修(element JSON 次选路径)
> 适用范围:JSONML 不支持某个字段时,或快速插入 callout / 分栏不想构造 JSONML 时。**默认优先 §4.4 路径 B**block update/insert `--content-format jsonml`),本节是老接口次选路径。
```bash
# 列出所有 block,定位 BLOCK_ID
dws doc block list --node <nodeId>
# 改一个 block 的文本
dws doc block update --node <nodeId> --block-id <BLOCK_ID> --content "替换后的内容" --content-format element
# 在某个 block 后插入
dws doc block insert --node <nodeId> --ref-block <BLOCK_ID> --where after --heading "补充说明" --level 2 --content-format element
# 插入复杂块(callout / 分栏)—— element 默认按 JSON 解析
dws doc block insert --node <nodeId> --ref-block <BLOCK_ID> --where after --content-format element \
--element '{"blockType":"callout","callout":{"emoji":"⚠️","bgColor":"#FDE2E0","content":[{"text":"高风险操作,先备份"}]}}'
# 若改写过程已经在用 JSONML,整段精修也可走 jsonml 路径(uuid 必须 == --block-id
dws doc block insert --node <nodeId> --ref-block <BLOCK_ID> --where after --content-format jsonml \
--element '["container",{"uuid":"co_new","subType":"colorBlocks","metadata":{"bgcolor":"#FDE2E0","border":"#F5C2C7"}},["p",{"uuid":"co_new_p1"},["span",{"data-type":"text"},["span",{"data-type":"leaf"},"高风险操作,先备份"]]]]'
```
字段结构以 [doc-block.md](../doc-block.md) 为准,不要猜。callout 字段名不确定时,先用 `doc block list --node <id> --block-type callout` 抓现有 callout 实例看真实字段。整段 JSONML 形态与可复制范例见 §4.4 与 [doc-jsonml-cookbook.md](../format/doc-jsonml-cookbook.md)。
### 4.4 JSONML 无损改写(**首选路径**
> 改写已有文档**默认走本节**——保真度最高,callout / 分栏 / 表格 / @人 / 附件 / 颜色 / 嵌套都能 1:1 round-trip;写入端有 validator 兜底。其他路径(§4.1/4.2/4.3/4.5 markdown)仅在 §1.3 列出的"次选 / 兜底"场景下使用。
两条子路径:
**路径 B:单 block JSONML 精修(最常用——只动一个 block 时的默认选择)**
```bash
# 1. 列出所有 block 拿到 uuid
dws doc block list --node <nodeId> --content-format jsonml
# 2. 读单个 block 完整子树
dws doc block list --node <nodeId> --content-format jsonml --block-id <BLOCK_UUID>
# 3. 改完后写回(uuid 必须 == --block-id
dws doc block update --node <nodeId> --block-id <BLOCK_UUID> --content-format jsonml \
--element '["p", {"uuid": "<BLOCK_UUID>"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "新内容"]]]'
# 在某个 block 前/后插入新 block
dws doc block insert --node <nodeId> --ref-block <BLOCK_UUID> --where after --content-format jsonml \
--element '["container", {"uuid": "co1", "subType": "colorBlocks", "metadata": {"bgcolor": "#E8F2FE", "border": "#B3D4FC"}}, ["p", {"uuid": "co1p1"}, ["span", {"data-type": "text"}, ["span", {"data-type": "leaf"}, "提示内容"]]]]'
```
**路径 A:整篇 JSONML overwrite(一次改多处、改 root 级 sectPr 才用)**
```bash
# 1. 读出完整 JSONML 结构(输出含 revision,普通改写场景下不需要)
dws doc +fetch --node <nodeId> --detail full --format json
# 2. 解析 JSON,修改 jsonml 数组中的目标节点
# 节点结构见 doc-jsonml-schema.md,可复制范例见 doc-jsonml-cookbook.md
# 3. 写回工作目录内相对文件 ./drafts/doc_modified.json,格式 {"jsonml": [...]}
# 4. 提交修改(默认直接覆盖,不做并发检查)
dws doc +update --node <nodeId> --command overwrite --content @./drafts/doc_modified.json \
--doc-format jsonml
```
> **并发安全模式(担心被并发覆盖时使用)**:如果担心多 agent 同时改这篇文档,可以把第 1 步 read 返回的 `revision` 通过 `--revision <N>` 透传给第 4 步:服务端会做并发检查,版本不一致返回 `VersionConflict`,此时回到第 1 步重读重写即可。普通单 agent 改写场景默认不传 `--revision`。
#### JSONML 写入端的 validator
写入命令(`doc create/update` + `doc block insert/update`)走 **validate** 一步,不做结构修复:
| 行为 | 缺省 | `--fix-jsonml` |
|------|------|----------------|
| JSON 语法修复(括号/逗号补全) | ✗ | ✓(打印 `[FIX]` |
| validator 阻断(HasErrors → 拒发) | ✓ | ✓ |
| root 校验(仅 doc create/update | ✓ | ✓ |
报错格式(agent 友好):
```
$[2][2]: paragraph child must be span wrapper, got raw string.
Suggestion: ["span",{"data-type":"text"},["span",{"data-type":"leaf"},"<your text>"]]
```
设计要点:
- 缺省为严格模式:不做结构修复,裸字符串、缺 uuid 等错误会被 validator 抦下。
- `doc create/update` 要求 body 必须以 `["root", ...]` 为根节点,缺少会报错。`doc block insert/update` 不要求 root。
- `--fix-jsonml`:启用 JSON 语法修复(修复 LLM 遗漏的括号/逗号),推荐 agent 调用。
**何时不走本节、改用 markdown**:纯文本追加章节(§4.2)、整篇按全新骨架重写(§4.5,且无富结构需要保留时)、只在乎"加一段文字"且确认目标段落无 callout / 分栏 / 颜色 / @人 / 附件。其余场景默认本节。
字段细节见 [doc-jsonml-schema.md](../format/doc-jsonml-schema.md);可复制范例见 [doc-jsonml-cookbook.md](../format/doc-jsonml-cookbook.md)。
### 4.5 整篇 overwrite
适合「按新风格重写整篇」「按新骨架重组结构」。
**形态选择(按 §1.3 优先级)**
- 若原文档含 callout / 分栏 / 颜色 / @人 / 附件 / 嵌套结构且需要保留——**走 §4.4 路径 A**(整篇 JSONML overwrite;默认不带 `--revision`,担心并发时再加)
- 若是纯文本骨架重写、原文档没有富结构需要保真——走本节 markdown overwrite
执行前必须先向用户**显式提示**
> 注意:本次操作将覆盖整篇文档内容(约 {size})。可能存在以下风险:
> - 大文档 overwrite 可能被后端静默降级为 append,导致**旧内容残留 + 新内容追加在末尾**
> - markdown overwrite 会丢失原文档的 callout / 分栏 / 颜色等富结构;如需保真改走 §4.4 路径 A
> - 写入完成后我会回读校验,发现异常会主动报告
>
> 是否继续?
得到确认后执行(markdown 兜底路径):
```bash
dws doc +checkpoint-update --node <nodeId> --mode overwrite --content @./drafts/<name>-full.md
```
读取 `+checkpoint-update` 的 checkpoint、write、verify 步骤;只有 partial/unknown 时才按 §6 恢复。
### 4.6 超长内容追加(分块 append)
当一次性追加内容 **超过 200KB** 时,必须拆分为多片 `--mode append`,并在执行第一片**之前**向用户发出截断风险提示等待确认。
完整规范(提示话术模板、触发条件、失败处理)见 [04-document.md «分块 append 截断风险提示»](../../04-document.md)。
update 场景下的额外约束:
1. 按段落/标题边界切分,**禁止**在表格、代码块、列表内部截断
2. 每写一片记录已写入的最后一个标题/段落标记,供 §6 回读比对
3. 与既有内容衔接位置不能产生悬空标题或断列表
---
## 五、改写时的样式约束
按 [doc-style-guideline.md](./doc-style-guideline.md) 处理:
- **文档类型保持不变**;用户明确要求转型时除外(如从「执行型 SOP」改成「说明型接口文档」)
- **同类信息保持一致**:改写时不要把原本统一的元素改为多种表达
- **颜色/emoji 语义**:改写后仍满足 style guideline §5「颜色与视觉语义」的一致性
- **不删除附件/图片**;用户明确要求时除外
---
## 六、回读验收
`+update/+checkpoint-update` 已统一确认与验证。正常成功禁止额外整篇读取;partial/unknown 或确需检查富结构时,使用最小范围 `+fetch`
校验要点:
- 改写章节的关键标题、段落首句、表格表头是否符合预期
- overwrite 后旧内容是否真的被清除
- append 后新内容是否在期望位置
- 表格、代码块、列表跨块元素是否完整
- @人、附件、图片等保真要素是否原样保留
### 异常处理
| 现象 | 可能原因 | 处理 |
|------|----------|------|
| overwrite 后旧内容残留 + 新内容追加在末尾 | overwrite 被静默降级为 append | 告知用户 overwrite 降级,按下方「先清空再重建」路径修复 |
| append 后部分片段缺失 | 分块写入丢失 | 定位缺失片段,针对该段单独再 append 一次 |
| @人 / 附件被替换为纯文本 | 改写时未走保真约束 | 用 JSONML 无损编辑修复(§4.4|
| 整篇内容乱序 | 写入顺序异常 | 报告给用户;若可重做,按下方「先清空再重建」路径修复 |
**禁止**在未回读的情况下向用户报告"已完成"。
---
## 七、交付口径
只报告已经验证过的信息:
- 改写涉及的章节范围
- 改写后的 nodeId 与 docUrl
- 回读验收结果(哪些章节确认改写成功、保真要素是否完整)
- 如有缺失或异常,说明具体位置和已采取的修复动作
未回读前,不要说「内容完整」「改写完成」。
@@ -0,0 +1,20 @@
# doc 局部意图消歧
本文件从单 Skill `intent-guide.md` 拆分而来,仅保留与本产品相关的跨产品消歧规则。
| 用户说... | 真实意图 | 应该用 | 不要用 | 理由 |
|---|---|---|---|---|
| "搜一下 OAuth2 接入文档" | 搜索开发文档 | `devdoc` | `doc search` | 搜索开放平台技术文档,不是钉钉内部内容 |
| "帮我建一个项目跟踪表" | 创建数据表格 | `aitable` | `doc` / `sheet` | 涉及结构化数据/行列操作,不是富文本文档或电子表格 |
| "帮我写个项目周报" | 创建钉钉文档 | `doc` | `aitable` | 富文本内容创作,不是数据表 |
| "参照这个生成同样的 / 按模板生成 / 复刻 X / 同样的模板 X 月份的" + 已有 alidocs URL | 模板保形生成同形态变体 | `drive copy + drive rename + doc block update` → 见 [best_practices/04-document.md `template-based-generation`](../../dingtalk-doc/references/04-document.md#template-based-generation) | `doc read + doc create`(重写链) | adoc → markdown 是有损投影,read+create 会丢行高/单元格背景色/字号;copy 在 adoc 层保形复制后只在副本上局部修改 |
| "复制一份这个文档 / 把这篇文档挂到 X 文件夹"(不涉模板变体) | 在线文档节点的存储层复制/移动 | `dws drive +copy --node <ID> [--folder <目标ID>]` / `dws drive +move --node <ID> --folder <目标ID>` | doc 同名 `+copy`/`+move``wiki node copy` | 复制/移动属节点存储管理,归 drive;drive 版先 probe 对象类型并拒绝普通文件,doc 裸版对普通文件会生成 `.dlink` 快捷方式而非副本 |
| "这个 alidocs 表格链接帮我看下"(粘贴原始 URL) | 先 probe 节点类型 | `dws drive info --node` → 按 `extension` 路由 | 直接调 `sheet` | `alidocs/i/nodes/{id}` 可能是文档/axls/able/xlsx 等,禁止凭 URL 猜类型 |
| "帮我记一下明天要做的事" | 创建个人待办 | `todo` | `doc` | 个人待办提醒,非文档内容 |
| "在知识库里创建一个文档" | 创建空文件实体 | `wiki node create --type adoc` | `doc create` | 空间内创建节点归 wiki;doc create 是向已有文档写入内容,不是创建文件节点 |
| "帮我看看收到的日报" | 收到的日志 | `report` | `doc` | 钉钉日志系统(日报/周报),不是文档 |
| "整理一下XX项目的所有讨论" | 跨源主题归档 | #5 generate-topic-report | #4 write-doc | #4 侧重单篇文档创作;按主题跨听记/群消息汇总属于工作汇报 |
| "搜一下智能化方案/最近 OKR 相关邮件/最近发版相关消息" | 搜企业知识内容 | `aisearch enterprise` | `doc search` / `mail search` / `chat message search` | 跨文档、消息、日程、听记、邮件等企业内容语义检索走 enterprise;具体 `queries/types/time-range` 抽槽见 `aisearch.md` |
| "我发给某人的消息/邮件/文档/今天我干了什么" | 搜行为记录 | `aisearch behavior` | `chat` / `mail` / `doc` / `report` | 关注“谁对什么做过什么”,走 behavior;具体 `behavior-type/direction/chat-scope` 抽槽见 `aisearch.md` |
| "把这段文字翻译成英文/translate this" | 通用文本翻译 | `chat text translate` | `doc` / `aisearch` | 纯文本翻译,不是文档编辑或语义搜索 |
| "帮我把这个文档翻译成日文" | 文档内容翻译 | 先 `dws doc +fetch --node``chat text translate` | `chat text translate` 直接传文件 | translate 仅支持纯文本,需先提取文档内容;普通读取统一走 shortcut |
+126
View File
@@ -0,0 +1,126 @@
---
name: dingtalk-drive
description: 钉钉文件管理(存储层,覆盖钉盘与文档空间)。Use when 用户说 钉盘/文档空间/我的文档中的普通文件或文件夹、查找/上传/下载/复制/移动/重命名/删除/回收站/权限/评论/元信息,或本地与钉盘文件夹比较、拉取、推送、双向同步;也承接在线文档节点的存储管理。文档正文编辑与导出走 dingtalk-doc;明确的知识库空间及空间内节点组织走 dingtalk-wiki。命令前缀:dws drive。
metadata:
cli_version: ">=0.2.14"
category: product
requires:
bins:
- dws
---
# 钉盘
<!-- DWS_RUNTIME_CONTRACT_START -->
## 最小 DWS 执行契约
- 只通过 `dws` CLI 操作钉钉;结构化读取使用 `--format json`,按真实返回判断结果。
- 已知命令直接执行。只有 leaf 参数或安全语义不确定时读取精确 Schema,只有 Cobra flag 不确定时读取精确 leaf Help;不要加载产品级 Catalog 代替选路。
- 不猜命令、flag、字段、ID、账号或时间。后续 ID 必须来自真实返回;零命中、多候选或类型不明时停止并消歧。
- 解析目标、读取上下文和最终执行必须使用同一 profile;不得跨组织复用 userId、openDingTalkId 或 openConversationId。多账号组织只使用明确的 `isOrgCurrent=true` 默认账号;没有默认账号时要求用户指定,禁止选择第一项、最近登录或最近使用账号。
- 不输出或记录 token、refresh token、appSecret、webhook token 等凭据;宿主已注入认证时不要索要凭据。
- 写操作必须符合用户明确意图。是否需要确认以最终 Runtime gate 和 Schema 为准;需要确认时先说明对象、动作与影响,再追加 `--yes`
- 写后按任务结果契约验证;不能仅凭退出码宣称成功。部分结果、未知投递状态和失败项必须如实保留。
- 时间戳面向用户展示时转换为带时区的可读时间;默认使用当前会话时区,必要时同时保留原值。
- 遇到认证、权限、profile、confirmation 或未知错误时,只加载 `dingtalk-shared` 中对应 reference;不要连续猜测替代命令。
<!-- DWS_RUNTIME_CONTRACT_END -->
<!-- VISIBLE_SHORTCUTS_START -->
## Shortcut 发现(按需)
`drive` 当前有 28 条公开 shortcut,完整清单保留在 Runtime Catalog 与 Schema,不在高频产品根 Skill 中重复展开。已知意图按下方路由。
仅当现有路由和 reference 都无法定位低频能力时,才执行 `dws shortcut list --service drive --format json` 做最后回退;不要为已知高频意图加载完整 Shortcut Catalog 或产品级 Schema。
<!-- VISIBLE_SHORTCUTS_END -->
## Golden Route
| 用户意图 | 唯一推荐入口 | 关键边界 |
|---|---|---|
| 全局按名称或关键词找文件 | `dws drive +search --query <关键词>` | 多候选停止;Drive 搜索没有 Doc 的 `--page-all`,按真实 nextCursor 翻页;在线文档正文搜索走 `doc +search` |
| 浏览根目录或已知文件夹 | `dws drive +list [--folder <dentryUuid>]` | 默认一页,处理 nextCursor |
| 发现钉盘企业空间或“我的文件”空间 | `dws wiki space list --type <orgSpace\|mySpace> --format json` | Drive 只读前置;orgSpace 按 nextToken 续页,取 spaceId/rootFolderId 后回到 Drive |
| 查看最近访问/编辑 | `dws drive +recent [--operate-type 1] --limit <N>` | 1=最近编辑;默认最近访问 |
| 查看节点类型和元数据 | `dws drive +inspect --node <dentryUuid>` | 按需加 stats/publish/cover,不为普通列表强制调用 |
| 下载普通文件 | `dws drive +download --node <dentryUuid> --output <相对路径>` | 当前 shortcut 接受 ID;在线文档用 `doc +export` |
| 上传新文件或覆盖普通文件 | `dws drive +upload --file <相对路径>` | 新建可加 folder;覆盖改加 node,二者互斥 |
| 管理普通文件全局评论 | `dws drive comment list-v2/create-v2/reply/update/delete/batch-query/list-replies/resolve/restore/react-reply` | 复用 Doc/Sheet 新评论链路;旧 `list/create` 已 deprecated;固定全文 `global`,不支持划词、单元格或 mention |
| 创建文件夹 | `dws drive +create-folder --name <名称> [--folder <ID>]` | Shortcut 已提交并读回 |
| 复制在线文档节点 | `dws drive +copy --node <ID> [--folder <目标ID>]` | 普通钉盘文件会被拒绝;Base 结构复制走 AITable `+base-copy --base-id <ID> --target-folder-id <真实ID> --only-struct` |
| 移动节点 | `dws drive +move --node <ID> --folder <目标ID>` | 破坏性变更,按 Runtime confirmation |
| 重命名节点 | `dws drive +rename --node <ID> --name <新名称>` | 写后检查最终名称 |
| 比较本地与钉盘文件夹 | `dws drive status --local-folder <绝对路径> --remote-folder <folderId>` | 只读;默认精确 MD5,不先拉取或推送 |
| 钉盘文件夹拉到本地 | `dws drive pull --local-folder <绝对路径> --remote-folder <folderId> --if-exists skip` | 安全默认不覆盖;先以相同参数 `--dry-run`,再按确认执行 |
| 本地文件夹推到钉盘 | `dws drive push --local-folder <绝对路径> --remote-folder <folderId> --if-exists skip` | 安全默认不覆盖;先 dry-run;不会删除远端多余文件 |
| 双向补齐文件夹 | `dws drive sync --local-folder <绝对路径> --remote-folder <folderId> --on-conflict skip` | 先 dry-run;冲突策略必须显式保留 |
### 低频入口
- 删除已确认节点:`dws drive +delete --node <dentryUuid>`;恢复:`+recycle-list/+recycle-restore`;版本:`+version-history/+version-get/+version-download/+version-revert`
- 收藏:`+star-*`;公开状态:`+publish-get/+publish-unset``+publish-set` 不进入 Agent 路由);统计/封面用 `+inspect`;快捷方式用 `+create-shortcut`
- 目录树只用有界 `+list` 逐层遍历。
兼容别名不选路:`+info``+inspect``+find-file``+search``+search-docs``doc +search`
## 当前最短路径
- 已知 dentryUuid:直接执行 inspect/download/list/move/rename,禁止先 search;仅确认是受支持的在线文档节点后才执行 copy。
- 目标 Drive 空间未知:先明确企业空间 `orgSpace` 或“我的文件”`mySpace`,用 `dws wiki space list --type <类型> --format json` 发现空间;`orgSpace``nextToken` 非空时以 `--cursor <nextToken>` 续页,`mySpace` 固定单条且不分页。按后续命令取真实 spaceId 或 rootFolderId 后立即回到 Drive;已知这些 ID 时不做空间发现。
- 只有名称:`+search` → 唯一候选的 nodeId → 目标命令;不得自动选择第一项。
- 只有文件夹层级:从最近的已知 folder ID 开始 `+list`,不要从根目录无界递归。
- 上传新文件:单条 `+upload`;不要退回 upload-info + 手写 HTTP + commit。
- 导出后上传:`doc +export` 首次就指定最终本地文件名,直接复用回执 `localPath`,首次正式 `drive +upload` 带已获授权的 `--yes`;禁止上传后再 rename。
- copy/move/rename/create-folder 已内置写后读取时,不再由 Agent重复执行 `+inspect`
- 已知 nodeId 的重命名直接 `+rename`,不先 Catalog、Help 或 searchALIDOC 的逻辑标题由 shortcut 内部文档读回验证。
- 文件夹方向已明确时直接 `status/pull/push/sync`,不先 status;写操作先用完全相同参数 dry-run,再正式执行。
- 搜索结果 `type=able` 后按业务动词重路由:结构复制/删除/Base 内操作走 AITable。结构复制按当前 leaf 提供源 Base ID 和真实 `--target-folder-id`;缺少目标 ID 时停止,不猜根 ID或发明 `--target-root`
- `+inspect/+download/+list` 只保证 dentryUuid;只有 URL 时先用 `dws drive info --node <URL> --format json` 解析并核对 nodeId。
最短路径不省略类型检查、确认、传输验证或写后校验。
## 关键结果语义
- `+list/+search/+recent` 检查集合、hasMore 和 nextCursor;缺少集合不能当空结果,多候选禁止默认第一项。
- `+download` 验证相对路径存在且 sizeBytes > 0`+upload` 检查最终 nodeId、名称、类型和大小。只有源端与结果都提供可比哈希时才核对 checksum;缺失时保留现有证据,不虚构端到端校验和。
- copy/move/rename/create-folder 检查 `ok/outcome` 和读回;`partial_success` 不是完成。
- status 检查分类集合;pull/push/sync 检查 summary 和逐项结果,failed/unknown 必须保留。
- 分页未结束时返回 continuation;目录树或大列表必须有最大深度、页数和条目数。
- 未知写入效果先 inspect/list 回读,不盲目重放写操作。
## 参数与安全边界
- `--node``--folder` 使用 dentryUuid/fileId,不使用数字型 dentryId;回收站 restore 使用 recycleItemId。
- `+list --limit` 最大 50`+search --limit` 最大 30;超过时分页,不以非法参数反复试错。
- 写操作只按精确 leaf Runtime 判定确认;已明确授权具体对象、动作与影响时,首次正式执行直接带 `--yes`,否则先确认。预览不带,参数变化重新确认;禁止用缺少 `--yes` 的失败探测。
- 普通文件覆盖前确认真实类型和原名称;adoc/axls/able 不按普通文件覆盖。
- 单文件 Shortcut 的本地输入输出使用 cwd 相对路径且禁止 `..`;文件夹 `status/pull/push/sync` 按 leaf 契约使用绝对 `--local-folder`
- 文件夹同步默认精确 MD5;仅在用户接受时间戳近似时用 `--quick`,且不会删除任一侧多余文件。
- 参数不确定时只查一次精确 leaf Schema;禁止产品级 Schema。
## 按需加载
Golden Route 参数足够时禁止读取 reference。其余最多读取一个精确 reference:
| 触发条件 | Reference |
|---|---|
| URL、文件类型或跨产品边界 | [intent-guide](references/intent-guide.md) |
| 文件夹比较、拉取、推送或双向同步 | [folder-sync](references/folder-sync.md) |
| 低频权限、版本、回收站、公开状态 | [drive reference](references/drive.md) 的对应章节 |
| 文档查询、导入和模板保形流程 | [lite-recipes](references/lite-recipes.md) |
## 错误最短路径
1. 零/多候选、类型不明或分页不完整:停止写入,返回候选或 continuation。
2. `unknown flag`:只查一次当前 leaf Help`unknown command`:只查一次 Drive shortcut 清单。
3. 普通下载遇到在线文档类型:切 `doc +export`,不重复尝试 Drive download。
4. 传输中断:保留本地临时状态或 checkpoint;先判断能否续传。
5. 写入效果未知:按 nodeId 回读;无法证明时报告 unknown。
6. 普通文件 `+copy` 被拒绝时不要重试或伪装成功;独立副本改走经用户授权的 download→upload。AITable 结构复制缺少或无法验证目标文件夹时停止,不猜 ID或创建测试文件夹。
## 跨产品边界
- 普通文件/文件夹及在线文档节点的存储管理 → Drive;把文件作为附件放进某篇文档正文走 Doc `+media-insert`,其他正文/内容分别走 Doc、Sheet、AITable。
- able 外层移动/重命名走 Drive;结构复制、Base 删除(`+base-delete`)及 Base 内操作走 AITable。
- 明确知识库 workspace 层级 → Wiki;泛称“文档空间/我的文档”仍走 Drive。
- 钉盘存储空间发现例外地复用 managed `dws wiki space list --type orgSpace|mySpace`;只取真实 spaceId/rootFolderId 后回到 Drive。spaceId 用于空间参数,rootFolderId 才可作为空间根目录 folder;`orgWikiSpace/myWikiSpace` 返回 workspaceId,不能混入 Drive 参数。
- Word/Markdown/Text 转在线文档用 `doc +import`Drive upload 只保留原文件。
@@ -0,0 +1,114 @@
# Drive 低频能力参考
仅在根 Skill 的 Golden Route 不足时读取本文件的一个相关章节。高频搜索、列表、检查、单文件传输和文件夹同步直接按根 Skill 执行。
## 身份与目标位置
- `--node``--folder``--remote-folder` 使用 dentryUuid/fileId;数字 dentryId 不能替代。
- 回收站恢复使用 `recycleItemId`,不能复用删除前的 nodeId。
- URL 类型不明时只执行一次 `dws drive info --node <URL> --format json`,按真实 nodeId/nodeType 分流。
- 普通文件夹目标使用 `--folder`;明确知识库 workspace 才使用 `--workspace`。零命中、多候选或类型不明时停止。
## Runtime 确认与首次执行
- 先解析唯一目标,再以精确 leaf Schema 和 Runtime gate 判断是否需要确认;不要通过一次缺少 `--yes` 的失败调用探测确认要求。
- Runtime 要求确认且当前请求已明确授权具体节点/文件夹、动作与影响时,首次正式远端写调用直接追加 `--yes`。诸如“整理一下”“处理这些文件”不能视为对删除、覆盖、移动或公开状态变更的精确授权。
- Runtime 不要求确认时不添加 `--yes`。对象、范围或影响不完整时先询问;确认后必须保持同一 profile、目标、动作、范围和关键参数,任一项变化都要重新确认。
- `--dry-run`、差异预览和只读检查不加 `--yes`;预览结果符合授权范围后,正式执行才追加。收到 `confirmation_required` 仅表示尚未通过预执行门禁,不代表业务写入成功,也不能据此盲目重放。
## 高级目录列表
普通浏览优先 `+list`。需要递归、名称模式、节点类型或修改时间过滤时使用 managed leaf
```bash
dws drive list --folder <dentryUuid> --depth 2 --pattern "*周报*" --format json
dws drive list --folder <dentryUuid> --type file --start 7d --format json
```
- `--depth` 最大 5;递归总量上限以结果中的 `truncated/errors` 为准。
- `--type file|folder` 是节点类型;`search --file-types` 是内容类型,二者不同。
- `--start/--end` 接受 `24h/7d/2w`、日期或 RFC3339,按修改时间过滤。
- 过滤模式是客户端有界扫描;`truncated=true` 不能声称全量。需要关键词检索时改用 `+search`
- `--latest` 遇到截断或目录读取失败会拒绝给出不完整 Top-N,按错误中的目录和恢复命令缩小范围。
## 回收站
```bash
dws drive +delete --node <dentryUuid>
dws drive +recycle-list --limit 20
dws drive +recycle-restore --id <recycleItemId>
```
删除前核对名称、类型和 ID;恢复从列表真实返回取 `id`。恢复后使用返回的新 nodeId,不沿用旧 ID。
## 普通文件历史版本
| 意图 | 入口 | 完成证据 |
|---|---|---|
| 列版本 | `+version-history` | 版本集合与分页字段 |
| 查看版本 | `+version-get` | 请求的 version |
| 下载版本 | `+version-download` | 相对路径存在且 sizeBytes > 0 |
| 回滚版本 | `+version-revert` | Runtime 确认后读回最新版本 |
这些入口只用于普通文件。adoc 版本走 Docaxls 版本走 Sheet。
## 收藏、统计与封面
- 收藏列表:`+star-list`;收藏/取消:`+star-add` / `+star-remove`
- 节点统计和封面优先并入 `+inspect --include-stats` / `--include-cover`;只取单项才用 `+stats` / `+cover`
- 收藏是个人状态,不代表共享或权限变化。
## 普通文件评论
普通 PDF、DOCX、XLSX 等本地文件使用 `dws drive comment`,复用 Doc/Sheet 的新评论服务链路。当前固定为文件级全文评论 `topicId=global`,不支持划词、单元格、页码、anchor 或 mention。
`drive comment list/create` 保留旧评论服务的行为和输出,仅作 deprecated 兼容入口。Agent 必须使用下面的 `list-v2/create-v2` 进入新评论体系。
```bash
dws drive comment list-v2 --node <dentryUuid> --format json
dws drive comment create-v2 --node <dentryUuid> --content "请补充结论"
dws drive comment list-replies --node <dentryUuid> --comment-key <commentKey> --format json
```
完整生命周期包括 `list-v2/create-v2/reply/update/delete/batch-query/list-replies/resolve/restore/react-reply`。分页游标必须原样回传;`list-v2` 每页上限为 50,超过上限直接报错;写操作按 Runtime confirmation 执行,后续操作的 `commentKey` 必须来自真实返回。
## 公开状态
```bash
dws drive +publish-get --node <dentryUuid>
dws drive +publish-unset --node <dentryUuid>
```
`+publish-get` 只读;`+publish-unset` 为高风险写。Runtime 虽注册了 `+publish-set`,但当前普通文件和在线文档都没有经过验证的开启公开闭环,因此根 Skill 明确不将它开放给 Agent。用户要求开启公开时,说明当前 Agent 路由不支持并停止;不要查询或执行 `+publish-set``drive publish set` 或其他替代写入口。只有补齐受支持节点上的真实 set→get→unset 闭环证据并更新 Agent 路由后,才重新开放该能力。
## 权限
| 意图 | managed leaf |
|---|---|
| 查看成员权限 | `drive permission list` |
| 查询节点权限设置(权限模式/分享范围/策略) | `permission get-setting` |
| 添加、修改、移除成员 | `permission add` / `update` / `remove` |
| 转移所有者 | `permission transfer-owner` |
| 查看可申请权限和审批人 | `permission apply-info` |
| 发起权限申请 | `permission apply` |
只在意图命中时读取一个精确 leaf Schema。成员变更、转移所有者、发起申请和公开状态变更必须明确节点、用户、角色与影响范围。转移所有者时,在构造最终命令前必须让用户分别明确决定 `--reserve-role <MANAGER|EDITOR|DOWNLOADER|READER|NONE>``--recursive=<true|false>`;Agent 不得根据默认值、对象类型或便捷性自行选择任一项。两项决策与目标、新所有者均明确后,才按 Runtime confirmation 构造首次正式调用。
`permission get-setting` 返回 `permissionMode`INHERITED/INDEPENDENT,未知时为 null)、`shareScope`(可见范围与链接分享,密码明文不返回;`partnerIncluded``defaultRole` 等仅 ORGANIZATION 有意义,`linkShare` 仅开启链接分享时返回)和 `policies[]`code/name/description/value/disabledValues/allowedValuesname/description 为中文名与值语义说明,随行必带;未下发的策略不返回,`node_spread_scope` 仅文件夹)。`disabledValues` 为不可设置取值列表(恒返回,无被禁档位时为空数组),每项含 `value`(被禁档位取值,与 value 同一值域)与 `reason`(服务端按请求语言返回的禁用原因文案,仅供展示理解,可为 null),与 allowedValues 互斥;示例:`{"value": "READER_AND_ABOVE", "reason": "企业安全策略要求不可低于可下载角色"}``value` 按策略分型:开关型为 ENABLED/DISABLEDmember_invite、comment 为 READER_AND_ABOVE/DOWNLOADER_AND_ABOVE/EDITOR_AND_ABOVE/MANAGER_AND_ABOVEnode_spread、online_content_copy 为 DOWNLOADER_AND_ABOVE/EDITOR_AND_ABOVE/MANAGER_AND_ABOVE 或 NOBODYnode_spread_scope 为 ALL_NODES(限制对所有文档生效)/ PREVIEWABLE_ONLY(仅对可预览的文档生效)。NOBODY=该操作对所有人禁止;XXX_AND_ABOVE=不低于该角色才允许。name/description 示例(文案与产品权限设置页一致):external_share「添加企业外协作者」:是否允许添加企业外的人为协作者(ENABLED=允许,DISABLED=禁止);node_spread「谁可以下载、创建副本、打印」:允许哪些角色及以上的用户下载、创建副本、打印;NOBODY=所有人禁止下载、创建副本、打印;node_move_forbidden「禁止移动」:是否禁止移动到其他知识库或团队共享文件夹(ENABLED=禁止移动,DISABLED=允许移动)。
发起权限申请先只读执行 `permission apply-info`。正式 `permission apply` 会通知审批人;调用前必须向用户逐项回显并确认资源、申请角色、审批人和理由。Agent 不得默认选择第一位审批人、最高/最低角色或代写申请理由;用户未明确同意完整申请内容时停在确认环节。
## 快捷方式节点
`+create-shortcut --node <源ID> [--folder <目标ID>|--workspace <知识库ID>]` 创建链接。在线文档节点需要保留版式的独立副本时使用 `+copy`;普通钉盘文件的独立副本必须经用户授权后走 download→upload,因为当前 `+copy` 会拒绝普通文件。源/目标类型不兼容时停止,不把快捷方式当普通文件继续覆盖。
## 错误恢复
1. `unknown flag` 只查当前 leaf Help`unknown command` 只查一次 Drive Shortcut 清单。
2. 分页、递归或过滤未完成时保留 cursor/truncated/errors,不包装成全量成功。
3. 写入超时或响应丢失先按 nodeId、名称、路径和大小回读,不能盲目重放。
4. 在线文档误入普通下载/覆盖时切 Doc、Sheet 或 AITable;不要用 Drive 重试改变内容。
## 辅助脚本
- [drive_tree_list.py](../scripts/drive_tree_list.py):递归列出钉盘目录树;普通浏览仍优先使用 `dws drive +list`

Some files were not shown because too many files have changed in this diff Show More