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
+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 |