Compare commits

...
Author SHA1 Message Date
南润 9f5923736e fix(aitable): validate field contracts before MCP 2026-08-10 17:06:41 +08:00
南润 dbe4b5f388 hint优化 2026-08-10 10:08:17 +08:00
瑞达 405a027002 fix(doc): preserve explicit headings and shorten workflows 2026-08-07 15:42:53 +08:00
南润 09643fc6a1 doc hint 优化 2026-08-06 16:40:05 +08:00
南润 a3e5145322 doc hint 优化 2026-08-06 16:24:04 +08:00
瑞达 0e4ec9af34 fix(doc): drop unnecessary fidelity guards 2026-08-06 11:56:55 +08:00
瑞达 9b62989ddb fix(doc): enforce workflow and result fidelity 2026-08-06 11:45:38 +08:00
南润 e81e040579 hint优化 2026-08-06 10:08:25 +08:00
南润 4f4fd4eb66 im优化 2026-08-05 16:25:26 +08:00
南润 8510cb8b91 im修复 2026-08-05 16:15:44 +08:00
63 changed files with 2099 additions and 702 deletions
+68
View File
@@ -82,6 +82,74 @@ func TestFlagErrorWithSuggestions_unknownFlagHintAndFlags(t *testing.T) {
}
}
func TestFlagErrorWithSuggestionsDocParameterFamilies(t *testing.T) {
for _, tc := range []struct {
name string
path []string
flag string
wantHint string
wantText string
}{
{"shortcut-create-file", []string{"doc", "+create"}, "content-file", "doc create", "--content-file"},
{"inspect-info", []string{"doc", "+inspect"}, "include-info", "移除 --include-info", "默认返回"},
{"inspect-versions", []string{"doc", "+inspect"}, "include-versions", "--include-history", "版本历史"},
{"inspect-generic", []string{"doc", "+inspect"}, "include", "具体 --include-*", "block list"},
} {
t.Run(tc.name, func(t *testing.T) {
root := &cobra.Command{Use: "dws"}
parent := root
for _, name := range tc.path {
child := &cobra.Command{Use: name}
parent.AddCommand(child)
parent = child
}
cmd := parent
err := flagErrorWithSuggestions(cmd, fmt.Errorf("unknown flag: --%s", tc.flag))
var typed *apperrors.Error
if !stderrors.As(err, &typed) {
t.Fatalf("want structured validation, got %T: %v", err, err)
}
if typed.Reason != "doc_parameter_family_mismatch" || !strings.Contains(typed.Hint, tc.wantHint) || !strings.Contains(typed.Message, tc.wantText) {
t.Fatalf("error = reason %q hint %q message %q", typed.Reason, typed.Hint, typed.Message)
}
})
}
}
func TestDocParameterFamilyErrorsStopBeforeDispatch(t *testing.T) {
for _, tc := range []struct {
path []string
flag string
}{
{[]string{"doc", "+create"}, "content-file"},
{[]string{"doc", "+inspect"}, "include-info"},
{[]string{"doc", "+inspect"}, "include-versions"},
{[]string{"doc", "+inspect"}, "include"},
} {
t.Run(strings.Join(tc.path, "/")+"/"+tc.flag, func(t *testing.T) {
calls := 0
root := &cobra.Command{Use: "dws", SilenceUsage: true, SilenceErrors: true}
root.SetFlagErrorFunc(flagErrorWithSuggestions)
parent := root
for _, name := range tc.path {
child := &cobra.Command{Use: name}
parent.AddCommand(child)
parent = child
}
parent.RunE = func(*cobra.Command, []string) error { calls++; return nil }
root.SetArgs(append(tc.path, "--"+tc.flag))
err := root.Execute()
var typed *apperrors.Error
if !stderrors.As(err, &typed) || typed.Reason != "doc_parameter_family_mismatch" {
t.Fatalf("error = %#v", err)
}
if calls != 0 {
t.Fatalf("dispatch calls = %d, want 0", calls)
}
})
}
}
// TestFlagErrorWithSuggestions_fallbackTailHint 验证 fallback 路径(非 unknown flag 类错误,
// 如 missing required flag / ambiguous shorthand)也带尾部 See '<cmd> --help' for usage.
// 这是 wukong / docker / kubectl 的通用 UX——任何 flag 解析错误都给用户一条 help 入口。
+58
View File
@@ -621,6 +621,61 @@ func enrichChatWorkbookError(cmd *cobra.Command, err error) error {
)
}
// enrichDocFlagError turns common native/shortcut parameter-family mixups into
// actionable local errors. It only uses the current Cobra command path and
// parse error, so it always runs before any helper RunE or MCP dispatch.
func enrichDocFlagError(cmd *cobra.Command, err error) error {
if cmd == nil || err == nil {
return err
}
path := cmd.CommandPath()
if fields := strings.Fields(path); len(fields) > 1 {
path = strings.Join(fields[1:], " ")
}
message := err.Error()
var guide chatWorkbookGuidance
switch {
case path == "doc +create" && (strings.Contains(message, "unknown flag: --content-file") || strings.Contains(message, "unknown flag: --content-format")):
guide = chatWorkbookGuidance{
"文档创建命令与参数不属于同一组",
"+create 是精简 Shortcut,不支持 --content-file/--content-format;需要从文件写入或指定 Markdown/JSONML 时应使用原生命令 doc create",
[]string{"改用 dws doc create,并保留 --content-file/--content-format", "只创建短文本时才继续使用 +create 的 --content"},
[]string{`dws doc create --name "<标题>" --content-file ./body.md --content-format markdown --format json`},
}
case path == "doc +inspect" && strings.Contains(message, "unknown flag: --include-info"):
guide = chatWorkbookGuidance{
"+inspect 不需要 --include-info",
"文档基本信息会由 +inspect 默认返回;移除 --include-info,只按需增加真实存在的 --include-history、--include-media、--include-comments、--include-permissions 或 --include-style",
[]string{"移除 --include-info", "只保留任务需要的 --include-* 参数"},
[]string{`dws doc +inspect --node <DOC_ID> --format json`},
}
case path == "doc +inspect" && strings.Contains(message, "unknown flag: --include-versions"):
guide = chatWorkbookGuidance{
"+inspect 不支持 --include-versions",
"版本历史对应的参数名是 --include-history;如果只需要版本列表,也可以使用 dws doc version list",
[]string{"将 --include-versions 改为 --include-history", "只查询版本时使用 doc version list"},
[]string{`dws doc +inspect --node <DOC_ID> --include-history --format json`, `dws doc version list --node <DOC_ID> --format json`},
}
case path == "doc +inspect" && strings.Contains(message, "unknown flag: --include"):
guide = chatWorkbookGuidance{
"+inspect 没有通用 --include 参数",
"请直接使用具体开关:--include-history、--include-media、--include-comments、--include-permissions 或 --include-style;文档块请使用 doc block list",
[]string{"把 --include <类型> 改成对应的具体 --include-* 开关", "需要 blockId 时改用 doc block list"},
[]string{`dws doc +inspect --node <DOC_ID> --include-history --format json`, `dws doc block list --node <DOC_ID> --format json`},
}
default:
return err
}
return apperrors.NewValidation(
guide.message+": "+guide.reason,
apperrors.WithReason("doc_parameter_family_mismatch"),
apperrors.WithHint(guide.actions[0]),
apperrors.WithActions(guide.actions...),
apperrors.WithExamples(guide.examples...),
apperrors.WithCause(err),
)
}
// newPreParseValidationError keeps pipeline handler identity in internal logs
// while exposing only the underlying parameter-domain error to CLI users.
func newPreParseValidationError(err error) error {
@@ -703,6 +758,9 @@ func flagErrorWithSuggestions(cmd *cobra.Command, err error) error {
if enriched := enrichChatWorkbookError(cmd, err); enriched != err {
return enriched
}
if enriched := enrichDocFlagError(cmd, err); enriched != err {
return enriched
}
// Common flag aliases and suggestions
suggestions := map[string]string{
+114 -93
View File
@@ -4712,8 +4712,9 @@
"agent_summary_source": "dws-agent-selection/doc",
"availability": "available",
"avoid_when": [
"整篇追加 Markdown 优先 doc update --mode append",
"插入本地文件附件优先 doc media insert",
"仅在用户没有指定块操作、只要整篇追加 Markdown 时优先 doc update --mode append;显式 insert/标题块/列表块不能折叠",
"插入本地文件附件优先 doc media insert;不要为普通附件手写 attachment JSON",
"整段 Markdown 或长内容优先 doc update --mode append,不要拆成大量 block insert",
"删块用 block delete;改已有块用 block update"
],
"confirmation": "not_required",
@@ -4729,14 +4730,14 @@
"source": "internal/cli/schema_hints/selection/doc.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐显式 block insert 的可观察操作语义和有序列表原生结构;不改变命令身份、参数契约、接口绑定或 Runtime 门禁。",
"candidates": [
{
"value": "向文档插入块元素",
"source": "internal/cli/schema_hints/selection/doc.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。"
"review_reason": "人工对齐显式 block insert 的可观察操作语义和有序列表原生结构;不改变命令身份、参数契约、接口绑定或 Runtime 门禁。"
}
]
},
@@ -4764,25 +4765,27 @@
},
"avoid_when": {
"value": [
"整篇追加 Markdown 优先 doc update --mode append",
"插入本地文件附件优先 doc media insert",
"仅在用户没有指定块操作、只要整篇追加 Markdown 时优先 doc update --mode append;显式 insert/标题块/列表块不能折叠",
"插入本地文件附件优先 doc media insert;不要为普通附件手写 attachment JSON",
"整段 Markdown 或长内容优先 doc update --mode append,不要拆成大量 block insert",
"删块用 block delete;改已有块用 block update"
],
"source": "internal/cli/schema_hints/selection/doc.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐显式 block insert 的可观察操作语义和有序列表原生结构;不改变命令身份、参数契约、接口绑定或 Runtime 门禁。",
"candidates": [
{
"value": [
"整篇追加 Markdown 优先 doc update --mode append",
"插入本地文件附件优先 doc media insert",
"仅在用户没有指定块操作、只要整篇追加 Markdown 时优先 doc update --mode append;显式 insert/标题块/列表块不能折叠",
"插入本地文件附件优先 doc media insert;不要为普通附件手写 attachment JSON",
"整段 Markdown 或长内容优先 doc update --mode append,不要拆成大量 block insert",
"删块用 block delete;改已有块用 block update"
],
"source": "internal/cli/schema_hints/selection/doc.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。"
"review_reason": "人工对齐显式 block insert 的可观察操作语义和有序列表原生结构;不改变命令身份、参数契约、接口绑定或 Runtime 门禁。"
}
]
},
@@ -4822,7 +4825,7 @@
"source": "internal/cli/schema_hints/selection/doc.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐显式 block insert 的可观察操作语义和有序列表原生结构;不改变命令身份、参数契约、接口绑定或 Runtime 门禁。",
"candidates": [
{
"value": [
@@ -4832,7 +4835,7 @@
"source": "internal/cli/schema_hints/selection/doc.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。"
"review_reason": "人工对齐显式 block insert 的可观察操作语义和有序列表原生结构;不改变命令身份、参数契约、接口绑定或 Runtime 门禁。"
}
]
},
@@ -4905,7 +4908,7 @@
"source": "internal/cli/schema_hints/selection/doc.json",
"precedence": "reviewed_explicit",
"selected": false,
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。"
"review_reason": "人工对齐显式 block insert 的可观察操作语义和有序列表原生结构;不改变命令身份、参数契约、接口绑定或 Runtime 门禁。"
}
]
},
@@ -4925,21 +4928,25 @@
},
"use_when": {
"value": [
"在文档中插入新块(段落/标题等);简单场景用 --text/--heading,复杂块用 --element JSON"
"用户明确要求在文档中插入新块(段落/标题/列表等)时;简单段落/标题用 --text/--heading,复杂块用 --element JSON",
"用户要求有序列表块时使用 JSONML p.list.isOrdered=true(同一 listId)或等价原生 orderedList 结构,不能用普通 Markdown 代替",
"相对插入时先 block list 获取真实 blockId,再同时传 --ref-block 与 --where before|after"
],
"source": "internal/cli/schema_hints/selection/doc.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐显式 block insert 的可观察操作语义和有序列表原生结构;不改变命令身份、参数契约、接口绑定或 Runtime 门禁。",
"candidates": [
{
"value": [
"在文档中插入新块(段落/标题等);简单场景用 --text/--heading,复杂块用 --element JSON"
"用户明确要求在文档中插入新块(段落/标题/列表等)时;简单段落/标题用 --text/--heading,复杂块用 --element JSON",
"用户要求有序列表块时使用 JSONML p.list.isOrdered=true(同一 listId)或等价原生 orderedList 结构,不能用普通 Markdown 代替",
"相对插入时先 block list 获取真实 blockId,再同时传 --ref-block 与 --where before|after"
],
"source": "internal/cli/schema_hints/selection/doc.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。"
"review_reason": "人工对齐显式 block insert 的可观察操作语义和有序列表原生结构;不改变命令身份、参数契约、接口绑定或 Runtime 门禁。"
}
]
}
@@ -4968,7 +4975,9 @@
"structured-hint:internal/cli/schema_hints/selection-review.json#doc.insert_document_block"
],
"use_when": [
"在文档中插入新块(段落/标题等);简单场景用 --text/--heading,复杂块用 --element JSON"
"用户明确要求在文档中插入新块(段落/标题/列表等)时;简单段落/标题用 --text/--heading,复杂块用 --element JSON",
"用户要求有序列表块时使用 JSONML p.list.isOrdered=true(同一 listId)或等价原生 orderedList 结构,不能用普通 Markdown 代替",
"相对插入时先 block list 获取真实 blockId,再同时传 --ref-block 与 --where before|after"
]
},
"doc block list": {
@@ -4992,14 +5001,14 @@
"source": "internal/cli/schema_hints/selection/doc.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐 block list 的显式可观察步骤和真实 blockId 上下文传递;不改变命令身份、参数契约或接口绑定。",
"candidates": [
{
"value": "查询文档一级块元素列表",
"source": "internal/cli/schema_hints/selection/doc.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。"
"review_reason": "人工对齐 block list 的显式可观察步骤和真实 blockId 上下文传递;不改变命令身份、参数契约或接口绑定。"
}
]
},
@@ -5033,7 +5042,7 @@
"source": "internal/cli/schema_hints/selection/doc.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐 block list 的显式可观察步骤和真实 blockId 上下文传递;不改变命令身份、参数契约或接口绑定。",
"candidates": [
{
"value": [
@@ -5043,7 +5052,7 @@
"source": "internal/cli/schema_hints/selection/doc.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。"
"review_reason": "人工对齐 block list 的显式可观察步骤和真实 blockId 上下文传递;不改变命令身份、参数契约或接口绑定。"
}
]
},
@@ -5083,7 +5092,7 @@
"source": "internal/cli/schema_hints/selection/doc.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐 block list 的显式可观察步骤和真实 blockId 上下文传递;不改变命令身份、参数契约或接口绑定。",
"candidates": [
{
"value": [
@@ -5093,7 +5102,7 @@
"source": "internal/cli/schema_hints/selection/doc.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。"
"review_reason": "人工对齐 block list 的显式可观察步骤和真实 blockId 上下文传递;不改变命令身份、参数契约或接口绑定。"
}
]
},
@@ -5172,7 +5181,7 @@
"source": "internal/cli/schema_hints/selection/doc.json",
"precedence": "reviewed_explicit",
"selected": false,
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。"
"review_reason": "人工对齐 block list 的显式可观察步骤和真实 blockId 上下文传递;不改变命令身份、参数契约或接口绑定。"
}
]
},
@@ -5192,21 +5201,21 @@
},
"use_when": {
"value": [
"查看文档一级块结构、拿 blockId,供 insert/update/delete 或划词评论定位时"
"查看文档一级块结构、拿 blockId,供 insert/update/delete 或划词评论定位时;用户在显式工作流中点名 list 时必须真实执行并使用返回结果"
],
"source": "internal/cli/schema_hints/selection/doc.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐 block list 的显式可观察步骤和真实 blockId 上下文传递;不改变命令身份、参数契约或接口绑定。",
"candidates": [
{
"value": [
"查看文档一级块结构、拿 blockId,供 insert/update/delete 或划词评论定位时"
"查看文档一级块结构、拿 blockId,供 insert/update/delete 或划词评论定位时;用户在显式工作流中点名 list 时必须真实执行并使用返回结果"
],
"source": "internal/cli/schema_hints/selection/doc.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。"
"review_reason": "人工对齐 block list 的显式可观察步骤和真实 blockId 上下文传递;不改变命令身份、参数契约或接口绑定。"
}
]
}
@@ -5239,7 +5248,7 @@
"structured-hint:internal/cli/schema_hints/selection-review.json#doc.list_document_blocks"
],
"use_when": [
"查看文档一级块结构、拿 blockId,供 insert/update/delete 或划词评论定位时"
"查看文档一级块结构、拿 blockId,供 insert/update/delete 或划词评论定位时;用户在显式工作流中点名 list 时必须真实执行并使用返回结果"
]
},
"doc block update": {
@@ -5248,7 +5257,8 @@
"availability": "available",
"avoid_when": [
"插入新块用 block insert;删除用 block delete",
"改文档显示名用 rename;整篇覆盖用 update overwrite"
"改文档显示名用 rename;整篇覆盖用 update overwrite",
"不要跨文档复用 blockId,也不要把 nodeId/commentKey 当作 blockId"
],
"confirmation": "not_required",
"effect": "write",
@@ -5298,7 +5308,8 @@
"avoid_when": {
"value": [
"插入新块用 block insert;删除用 block delete",
"改文档显示名用 rename;整篇覆盖用 update overwrite"
"改文档显示名用 rename;整篇覆盖用 update overwrite",
"不要跨文档复用 blockId,也不要把 nodeId/commentKey 当作 blockId"
],
"source": "internal/cli/schema_hints/selection/doc.json",
"precedence": "reviewed_explicit",
@@ -5308,7 +5319,8 @@
{
"value": [
"插入新块用 block insert;删除用 block delete",
"改文档显示名用 rename;整篇覆盖用 update overwrite"
"改文档显示名用 rename;整篇覆盖用 update overwrite",
"不要跨文档复用 blockId,也不要把 nodeId/commentKey 当作 blockId"
],
"source": "internal/cli/schema_hints/selection/doc.json",
"precedence": "reviewed_explicit",
@@ -5454,7 +5466,7 @@
},
"use_when": {
"value": [
"修改已有块的文本/标题/样式(已知 blockId)时"
"修改已有块的文本/标题/样式时;blockId 必须来自目标文档本次 block list 结果"
],
"source": "internal/cli/schema_hints/selection/doc.json",
"precedence": "reviewed_explicit",
@@ -5463,7 +5475,7 @@
"candidates": [
{
"value": [
"修改已有块的文本/标题/样式(已知 blockId)时"
"修改已有块的文本/标题/样式时;blockId 必须来自目标文档本次 block list 结果"
],
"source": "internal/cli/schema_hints/selection/doc.json",
"precedence": "reviewed_explicit",
@@ -5497,7 +5509,7 @@
"structured-hint:internal/cli/schema_hints/selection-review.json#doc.update_document_block"
],
"use_when": [
"修改已有块的文本/标题/样式(已知 blockId)时"
"修改已有块的文本/标题/样式时;blockId 必须来自目标文档本次 block list 结果"
]
},
"doc comment create": {
@@ -6644,7 +6656,8 @@
"agent_summary_source": "dws-agent-selection/doc",
"availability": "available",
"avoid_when": [
"新建评论用 create/create-inline;删评论用 delete"
"新建评论用 create/create-inline;删评论用 delete",
"不要把 commentId、blockId 或示例占位符传给 --comment-key"
],
"confirmation": "not_required",
"effect": "write",
@@ -6694,7 +6707,8 @@
},
"avoid_when": {
"value": [
"新建评论用 create/create-inline;删评论用 delete"
"新建评论用 create/create-inline;删评论用 delete",
"不要把 commentId、blockId 或示例占位符传给 --comment-key"
],
"source": "internal/cli/schema_hints/selection/doc.json",
"precedence": "reviewed_explicit",
@@ -6703,7 +6717,8 @@
"candidates": [
{
"value": [
"新建评论用 create/create-inline;删评论用 delete"
"新建评论用 create/create-inline;删评论用 delete",
"不要把 commentId、blockId 或示例占位符传给 --comment-key"
],
"source": "internal/cli/schema_hints/selection/doc.json",
"precedence": "reviewed_explicit",
@@ -6897,7 +6912,7 @@
},
"use_when": {
"value": [
"回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 来自 list/create"
"回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 必须来自本次 list/create 返回"
],
"source": "internal/cli/schema_hints/selection/doc.json",
"precedence": "reviewed_explicit",
@@ -6906,7 +6921,7 @@
"candidates": [
{
"value": [
"回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 来自 list/create"
"回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 必须来自本次 list/create 返回"
],
"source": "internal/cli/schema_hints/selection/doc.json",
"precedence": "reviewed_explicit",
@@ -6941,7 +6956,7 @@
"structured-hint:internal/cli/schema_hints/selection-review.json#doc.reply_comment"
],
"use_when": [
"回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 来自 list/create"
"回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 必须来自本次 list/create 返回"
]
},
"doc comment update": {
@@ -7500,8 +7515,8 @@
"availability": "available",
"avoid_when": [
"创建表格/脑图/白板/多维表/演示改用 dws wiki node create --type \u003ctype\u003e(勿用 doc create)",
"只管理知识库空节点或层级时用 wiki node create;本命令侧重创建并写入 adoc",
"导入本地 Word/Markdown 并保留服务端转换语义时用 dws doc import;不要用自写分片脚本替代原生 create"
"在知识库建空节点实体也可用 wiki node create;本命令侧重可写初始内容的 adoc",
"导入本地 Word/Markdown 为在线文档用 dws doc import,并显式提供 --folder 或 --workspace;不要用已迁移的 doc upload --convert"
],
"confirmation": "not_required",
"effect": "write",
@@ -7516,14 +7531,14 @@
"source": "internal/cli/schema_hints/selection/doc.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工对齐当前 Cobra 原生内容写入管道、doc Skill 渐进路由与 workspace/folder 边界;强调长内容无需外部分片,不改变命令身份、参数或安全事实。",
"review_reason": "人工对齐当前 Cobra 原生内容写入管道、显式正文 H1、同请求新 nodeId 绑定和可观察块操作边界;强调长内容无需外部分片,不改变命令身份、参数或安全事实。",
"candidates": [
{
"value": "在默认根目录、文档文件夹或知识库根创建带可选初始内容的 adoc",
"source": "internal/cli/schema_hints/selection/doc.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工对齐当前 Cobra 原生内容写入管道、doc Skill 渐进路由与 workspace/folder 边界;强调长内容无需外部分片,不改变命令身份、参数或安全事实。"
"review_reason": "人工对齐当前 Cobra 原生内容写入管道、显式正文 H1、同请求新 nodeId 绑定和可观察块操作边界;强调长内容无需外部分片,不改变命令身份、参数或安全事实。"
}
]
},
@@ -7552,24 +7567,24 @@
"avoid_when": {
"value": [
"创建表格/脑图/白板/多维表/演示改用 dws wiki node create --type \u003ctype\u003e(勿用 doc create)",
"只管理知识库空节点或层级时用 wiki node create;本命令侧重创建并写入 adoc",
"导入本地 Word/Markdown 并保留服务端转换语义时用 dws doc import;不要用自写分片脚本替代原生 create"
"在知识库建空节点实体也可用 wiki node create;本命令侧重可写初始内容的 adoc",
"导入本地 Word/Markdown 为在线文档用 dws doc import,并显式提供 --folder 或 --workspace;不要用已迁移的 doc upload --convert"
],
"source": "internal/cli/schema_hints/selection/doc.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工对齐当前 Cobra 原生内容写入管道、doc Skill 渐进路由与 workspace/folder 边界;强调长内容无需外部分片,不改变命令身份、参数或安全事实。",
"review_reason": "人工对齐当前 Cobra 原生内容写入管道、显式正文 H1、同请求新 nodeId 绑定和可观察块操作边界;强调长内容无需外部分片,不改变命令身份、参数或安全事实。",
"candidates": [
{
"value": [
"创建表格/脑图/白板/多维表/演示改用 dws wiki node create --type \u003ctype\u003e(勿用 doc create)",
"只管理知识库空节点或层级时用 wiki node create;本命令侧重创建并写入 adoc",
"导入本地 Word/Markdown 并保留服务端转换语义时用 dws doc import;不要用自写分片脚本替代原生 create"
"在知识库建空节点实体也可用 wiki node create;本命令侧重可写初始内容的 adoc",
"导入本地 Word/Markdown 为在线文档用 dws doc import,并显式提供 --folder 或 --workspace;不要用已迁移的 doc upload --convert"
],
"source": "internal/cli/schema_hints/selection/doc.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工对齐当前 Cobra 原生内容写入管道、doc Skill 渐进路由与 workspace/folder 边界;强调长内容无需外部分片,不改变命令身份、参数或安全事实。"
"review_reason": "人工对齐当前 Cobra 原生内容写入管道、显式正文 H1、同请求新 nodeId 绑定和可观察块操作边界;强调长内容无需外部分片,不改变命令身份、参数或安全事实。"
}
]
},
@@ -7609,7 +7624,7 @@
"source": "internal/cli/schema_hints/selection/doc.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工对齐当前 Cobra 原生内容写入管道、doc Skill 渐进路由与 workspace/folder 边界;强调长内容无需外部分片,不改变命令身份、参数或安全事实。",
"review_reason": "人工对齐当前 Cobra 原生内容写入管道、显式正文 H1、同请求新 nodeId 绑定和可观察块操作边界;强调长内容无需外部分片,不改变命令身份、参数或安全事实。",
"candidates": [
{
"value": [
@@ -7619,7 +7634,7 @@
"source": "internal/cli/schema_hints/selection/doc.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工对齐当前 Cobra 原生内容写入管道、doc Skill 渐进路由与 workspace/folder 边界;强调长内容无需外部分片,不改变命令身份、参数或安全事实。"
"review_reason": "人工对齐当前 Cobra 原生内容写入管道、显式正文 H1、同请求新 nodeId 绑定和可观察块操作边界;强调长内容无需外部分片,不改变命令身份、参数或安全事实。"
}
]
},
@@ -7698,7 +7713,7 @@
"source": "internal/cli/schema_hints/selection/doc.json",
"precedence": "reviewed_explicit",
"selected": false,
"review_reason": "人工对齐当前 Cobra 原生内容写入管道、doc Skill 渐进路由与 workspace/folder 边界;强调长内容无需外部分片,不改变命令身份、参数或安全事实。"
"review_reason": "人工对齐当前 Cobra 原生内容写入管道、显式正文 H1、同请求新 nodeId 绑定和可观察块操作边界;强调长内容无需外部分片,不改变命令身份、参数或安全事实。"
}
]
},
@@ -7718,23 +7733,23 @@
},
"use_when": {
"value": [
"用户要新建文字在线文档(adoc),可空文档或用 --content-file 写入 Markdown/JSONML 时",
"已知目标文档文件夹 --folder 或知识库 --workspace;长 Markdown 仍直接使用本命令,由原生写入管道自动分片"
"用户要新建文字在线文档(adoc),可空文档或用 --content-file 写入明确要求的初始 Markdown/JSONML;用户显式要求正文 H1 时必须保留,不能用 --name 替代",
"已知目标文档文件夹 --folder 或知识库 --workspace;长 Markdown 仍直接使用本命令,由原生写入管道自动分片;返回 nodeId 绑定同一请求的后续 list/insert/export,后续可观察操作仍须分别执行"
],
"source": "internal/cli/schema_hints/selection/doc.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工对齐当前 Cobra 原生内容写入管道、doc Skill 渐进路由与 workspace/folder 边界;强调长内容无需外部分片,不改变命令身份、参数或安全事实。",
"review_reason": "人工对齐当前 Cobra 原生内容写入管道、显式正文 H1、同请求新 nodeId 绑定和可观察块操作边界;强调长内容无需外部分片,不改变命令身份、参数或安全事实。",
"candidates": [
{
"value": [
"用户要新建文字在线文档(adoc),可空文档或用 --content-file 写入 Markdown/JSONML 时",
"已知目标文档文件夹 --folder 或知识库 --workspace;长 Markdown 仍直接使用本命令,由原生写入管道自动分片"
"用户要新建文字在线文档(adoc),可空文档或用 --content-file 写入明确要求的初始 Markdown/JSONML;用户显式要求正文 H1 时必须保留,不能用 --name 替代",
"已知目标文档文件夹 --folder 或知识库 --workspace;长 Markdown 仍直接使用本命令,由原生写入管道自动分片;返回 nodeId 绑定同一请求的后续 list/insert/export,后续可观察操作仍须分别执行"
],
"source": "internal/cli/schema_hints/selection/doc.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工对齐当前 Cobra 原生内容写入管道、doc Skill 渐进路由与 workspace/folder 边界;强调长内容无需外部分片,不改变命令身份、参数或安全事实。"
"review_reason": "人工对齐当前 Cobra 原生内容写入管道、显式正文 H1、同请求新 nodeId 绑定和可观察块操作边界;强调长内容无需外部分片,不改变命令身份、参数或安全事实。"
}
]
}
@@ -7766,8 +7781,8 @@
"structured-hint:internal/cli/schema_hints/selection-review.json#doc.create_document"
],
"use_when": [
"用户要新建文字在线文档(adoc),可空文档或用 --content-file 写入 Markdown/JSONML 时",
"已知目标文档文件夹 --folder 或知识库 --workspace;长 Markdown 仍直接使用本命令,由原生写入管道自动分片"
"用户要新建文字在线文档(adoc),可空文档或用 --content-file 写入明确要求的初始 Markdown/JSONML;用户显式要求正文 H1 时必须保留,不能用 --name 替代",
"已知目标文档文件夹 --folder 或知识库 --workspace;长 Markdown 仍直接使用本命令,由原生写入管道自动分片;返回 nodeId 绑定同一请求的后续 list/insert/export,后续可观察操作仍须分别执行"
]
},
"doc delete": {
@@ -7775,7 +7790,7 @@
"agent_summary_source": "dws-agent-selection/doc",
"availability": "available",
"avoid_when": [
"常规文件删除优先 dws drive delete;本入口仅为兼容",
"常规文件删除使用 dws drive delete;不要继续选择已弃用的 doc delete 兼容入口",
"用户未确认或目标不清时不要删",
"删块用 doc block delete;删评论用 doc comment delete"
],
@@ -7826,7 +7841,7 @@
},
"avoid_when": {
"value": [
"常规文件删除优先 dws drive delete;本入口仅为兼容",
"常规文件删除使用 dws drive delete;不要继续选择已弃用的 doc delete 兼容入口",
"用户未确认或目标不清时不要删",
"删块用 doc block delete;删评论用 doc comment delete"
],
@@ -7837,7 +7852,7 @@
"candidates": [
{
"value": [
"常规文件删除优先 dws drive delete;本入口仅为兼容",
"常规文件删除使用 dws drive delete;不要继续选择已弃用的 doc delete 兼容入口",
"用户未确认或目标不清时不要删",
"删块用 doc block delete;删评论用 doc comment delete"
],
@@ -9095,7 +9110,7 @@
"agent_summary_source": "dws-agent-selection/doc",
"availability": "available",
"avoid_when": [
"发起导入用 doc import(若入口可用);不要用本命令代替导入"
"发起导入用 doc import;不要先调用 import get,也不要使用示例占位 taskId"
],
"confirmation": "not_required",
"effect": "write",
@@ -9138,7 +9153,7 @@
},
"avoid_when": {
"value": [
"发起导入用 doc import(若入口可用);不要用本命令代替导入"
"发起导入用 doc import;不要先调用 import get,也不要使用示例占位 taskId"
],
"source": "internal/cli/schema_hints/selection/doc.json",
"precedence": "reviewed_explicit",
@@ -9147,7 +9162,7 @@
"candidates": [
{
"value": [
"发起导入用 doc import(若入口可用);不要用本命令代替导入"
"发起导入用 doc import;不要先调用 import get,也不要使用示例占位 taskId"
],
"source": "internal/cli/schema_hints/selection/doc.json",
"precedence": "reviewed_explicit",
@@ -9337,7 +9352,7 @@
},
"use_when": {
"value": [
"查询文档导入任务结果(已有 taskId,导入超时/中断后兜底)时"
"仅在 doc import 已返回真实 taskId 且自动轮询超时/中断后,查询导入任务结果时"
],
"source": "internal/cli/schema_hints/selection/doc.json",
"precedence": "reviewed_explicit",
@@ -9346,7 +9361,7 @@
"candidates": [
{
"value": [
"查询文档导入任务结果(已有 taskId,导入超时/中断后兜底)时"
"仅在 doc import 已返回真实 taskId 且自动轮询超时/中断后,查询导入任务结果时"
],
"source": "internal/cli/schema_hints/selection/doc.json",
"precedence": "reviewed_explicit",
@@ -9373,7 +9388,7 @@
"structured-hint:internal/cli/schema_hints/products/doc.json"
],
"use_when": [
"查询文档导入任务结果(已有 taskId,导入超时/中断后兜底)时"
"仅在 doc import 已返回真实 taskId 且自动轮询超时/中断后,查询导入任务结果时"
]
},
"doc info": {
@@ -9922,7 +9937,8 @@
"agent_summary_source": "dws-agent-selection/doc",
"availability": "available",
"avoid_when": [
"下载钉盘普通文件用 drive download;导出在线文档用 doc export"
"下载钉盘普通文件用 drive download;导出在线文档用 doc export",
"不要把 blockId/nodeId 当作 resourceId,也不要编造 --output:本命令返回临时 downloadUrl"
],
"confirmation": "not_required",
"effect": "read",
@@ -9971,7 +9987,8 @@
},
"avoid_when": {
"value": [
"下载钉盘普通文件用 drive download;导出在线文档用 doc export"
"下载钉盘普通文件用 drive download;导出在线文档用 doc export",
"不要把 blockId/nodeId 当作 resourceId,也不要编造 --output:本命令返回临时 downloadUrl"
],
"source": "internal/cli/schema_hints/selection/doc.json",
"precedence": "reviewed_explicit",
@@ -9980,7 +9997,8 @@
"candidates": [
{
"value": [
"下载钉盘普通文件用 drive download;导出在线文档用 doc export"
"下载钉盘普通文件用 drive download;导出在线文档用 doc export",
"不要把 blockId/nodeId 当作 resourceId,也不要编造 --output:本命令返回临时 downloadUrl"
],
"source": "internal/cli/schema_hints/selection/doc.json",
"precedence": "reviewed_explicit",
@@ -10126,7 +10144,7 @@
},
"use_when": {
"value": [
"获取文档正文中附件的临时下载 URL(resourceId 来自 block list attachment)时"
"获取文档正文中附件的临时下载 URL 时;resourceId 必须来自本次 block list 返回的 attachment 块"
],
"source": "internal/cli/schema_hints/selection/doc.json",
"precedence": "reviewed_explicit",
@@ -10135,7 +10153,7 @@
"candidates": [
{
"value": [
"获取文档正文中附件的临时下载 URL(resourceId 来自 block list attachment)时"
"获取文档正文中附件的临时下载 URL 时;resourceId 必须来自本次 block list 返回的 attachment 块"
],
"source": "internal/cli/schema_hints/selection/doc.json",
"precedence": "reviewed_explicit",
@@ -10167,7 +10185,7 @@
"structured-hint:internal/cli/schema_hints/selection-review.json#doc.download_doc_attachment"
],
"use_when": [
"获取文档正文中附件的临时下载 URL(resourceId 来自 block list attachment)时"
"获取文档正文中附件的临时下载 URL 时;resourceId 必须来自本次 block list 返回的 attachment 块"
]
},
"doc media insert": {
@@ -14672,10 +14690,10 @@
"agent_summary_source": "dws-agent-selection/doc",
"availability": "available",
"avoid_when": [
"只在末尾补一小段纯文本优先 +doc-append;只改一个块用 doc block update",
"目标不是 adoc 时按 drive info 的 extension 切对应产品",
"覆盖模式用户未确认前不要执行;可先 --dry-run 预览",
"创建新文档用 doc create;不要预先手工分片或循环重试覆盖"
"目标不是 adoc 或只要改单个块时改用 doc block update",
"覆盖模式用户未确认前不要执行;先 --dry-run 预览,确认后再加 --yes",
"--content 与 --content-file 二选一;--index 仅用于 mode=append",
"创建新文档用 doc create,不要用 update 冒充创建"
],
"confirmation": "not_required",
"effect": "write",
@@ -14725,10 +14743,10 @@
},
"avoid_when": {
"value": [
"只在末尾补一小段纯文本优先 +doc-append;只改一个块用 doc block update",
"目标不是 adoc 时按 drive info 的 extension 切对应产品",
"覆盖模式用户未确认前不要执行;可先 --dry-run 预览",
"创建新文档用 doc create;不要预先手工分片或循环重试覆盖"
"目标不是 adoc 或只要改单个块时改用 doc block update",
"覆盖模式用户未确认前不要执行;先 --dry-run 预览,确认后再加 --yes",
"--content 与 --content-file 二选一;--index 仅用于 mode=append",
"创建新文档用 doc create,不要用 update 冒充创建"
],
"source": "internal/cli/schema_hints/selection/doc.json",
"precedence": "reviewed_explicit",
@@ -14737,10 +14755,10 @@
"candidates": [
{
"value": [
"只在末尾补一小段纯文本优先 +doc-append;只改一个块用 doc block update",
"目标不是 adoc 时按 drive info 的 extension 切对应产品",
"覆盖模式用户未确认前不要执行;可先 --dry-run 预览",
"创建新文档用 doc create;不要预先手工分片或循环重试覆盖"
"目标不是 adoc 或只要改单个块时改用 doc block update",
"覆盖模式用户未确认前不要执行;先 --dry-run 预览,确认后再加 --yes",
"--content 与 --content-file 二选一;--index 仅用于 mode=append",
"创建新文档用 doc create,不要用 update 冒充创建"
],
"source": "internal/cli/schema_hints/selection/doc.json",
"precedence": "reviewed_explicit",
@@ -14920,7 +14938,8 @@
"value": [
"用户要向已有 adoc 追加长、多行或文件内容时用 --mode append + --content-file;CLI 自动分片",
"用户明确要求整篇覆盖替换时用 --mode overwrite(破坏性)",
"append 且要插到第 N 个 block 前时加 --index N(先 block list)"
"append 且要插到第 N 个 block 前时加 --index N(先 block list)",
"长文本、多行内容或表格优先写入 UTF-8 文件并使用 --content-file,避免 shell 转义损坏"
],
"source": "internal/cli/schema_hints/selection/doc.json",
"precedence": "reviewed_explicit",
@@ -14931,7 +14950,8 @@
"value": [
"用户要向已有 adoc 追加长、多行或文件内容时用 --mode append + --content-file;CLI 自动分片",
"用户明确要求整篇覆盖替换时用 --mode overwrite(破坏性)",
"append 且要插到第 N 个 block 前时加 --index N(先 block list)"
"append 且要插到第 N 个 block 前时加 --index N(先 block list)",
"长文本、多行内容或表格优先写入 UTF-8 文件并使用 --content-file,避免 shell 转义损坏"
],
"source": "internal/cli/schema_hints/selection/doc.json",
"precedence": "reviewed_explicit",
@@ -14970,7 +14990,8 @@
"use_when": [
"用户要向已有 adoc 追加长、多行或文件内容时用 --mode append + --content-file;CLI 自动分片",
"用户明确要求整篇覆盖替换时用 --mode overwrite(破坏性)",
"append 且要插到第 N 个 block 前时加 --index N(先 block list)"
"append 且要插到第 N 个 block 前时加 --index N(先 block list)",
"长文本、多行内容或表格优先写入 UTF-8 文件并使用 --content-file,避免 shell 转义损坏"
]
},
"doc upload": {
+15 -12
View File
@@ -10481,6 +10481,7 @@
"用户要把文件发送到群聊或单聊时使用 chat message send --msg-type file --file-path;drive upload 不会发送聊天消息",
"常规场景不要拆成 upload-info + 手动 PUT + commit;仅自定义流式上传才用三步",
"要把文件作为文档正文附件插入改用 dws doc media insert",
"Word/Excel/Markdown 要求上传后在线编辑、大家直接在线改或转钉钉文档时改用 dws doc import;普通 drive upload 返回 docx/xlsx 文件不能宣称可在线编辑",
"用户明确说文档空间且走 doc 兼容入口时可用 dws doc upload,默认仍推荐本命令",
"用户没有明确同意替换目标文件时不要使用 --node;新建上传应使用 --folder 或目标根目录"
],
@@ -10497,14 +10498,14 @@
"source": "internal/cli/schema_hints/selection/drive.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
"candidates": [
{
"value": "上传本地文件到钉盘或文档空间,或按节点 ID 确认覆盖已有文件",
"source": "internal/cli/schema_hints/selection/drive.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。"
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。"
}
]
},
@@ -10529,26 +10530,28 @@
"用户要把文件发送到群聊或单聊时使用 chat message send --msg-type file --file-path;drive upload 不会发送聊天消息",
"常规场景不要拆成 upload-info + 手动 PUT + commit;仅自定义流式上传才用三步",
"要把文件作为文档正文附件插入改用 dws doc media insert",
"Word/Excel/Markdown 要求上传后在线编辑、大家直接在线改或转钉钉文档时改用 dws doc import;普通 drive upload 返回 docx/xlsx 文件不能宣称可在线编辑",
"用户明确说文档空间且走 doc 兼容入口时可用 dws doc upload,默认仍推荐本命令",
"用户没有明确同意替换目标文件时不要使用 --node;新建上传应使用 --folder 或目标根目录"
],
"source": "internal/cli/schema_hints/selection/drive.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
"candidates": [
{
"value": [
"用户要把文件发送到群聊或单聊时使用 chat message send --msg-type file --file-path;drive upload 不会发送聊天消息",
"常规场景不要拆成 upload-info + 手动 PUT + commit;仅自定义流式上传才用三步",
"要把文件作为文档正文附件插入改用 dws doc media insert",
"Word/Excel/Markdown 要求上传后在线编辑、大家直接在线改或转钉钉文档时改用 dws doc import;普通 drive upload 返回 docx/xlsx 文件不能宣称可在线编辑",
"用户明确说文档空间且走 doc 兼容入口时可用 dws doc upload,默认仍推荐本命令",
"用户没有明确同意替换目标文件时不要使用 --node;新建上传应使用 --folder 或目标根目录"
],
"source": "internal/cli/schema_hints/selection/drive.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。"
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。"
}
]
},
@@ -10604,7 +10607,7 @@
"source": "internal/cli/schema_hints/selection/drive.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
"candidates": [
{
"value": [
@@ -10614,7 +10617,7 @@
"source": "internal/cli/schema_hints/selection/drive.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。"
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。"
}
]
},
@@ -10707,7 +10710,7 @@
"source": "internal/cli/schema_hints/selection/drive.json",
"precedence": "reviewed_explicit",
"selected": false,
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。"
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。"
}
]
},
@@ -10737,25 +10740,25 @@
"value": [
"用户要把本地文件上传到钉盘/我的文件(首选一条命令自动完成凭证+PUT+提交)时",
"用户明确要求只上传或暂存附件、暂时不要发送到任何聊天会话时",
"上传到知识库/文档空间时加 --workspace;需要转在线文档时加 --convert",
"用户只要保留原始文件供存储或下载时使用;上传到知识库/文档空间时可加 --workspace",
"用户明确要求用本地文件替换已有钉盘/文档空间文件时传 --node;该模式会覆盖远端内容并要求确认"
],
"source": "internal/cli/schema_hints/selection/drive.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
"candidates": [
{
"value": [
"用户要把本地文件上传到钉盘/我的文件(首选一条命令自动完成凭证+PUT+提交)时",
"用户明确要求只上传或暂存附件、暂时不要发送到任何聊天会话时",
"上传到知识库/文档空间时加 --workspace;需要转在线文档时加 --convert",
"用户只要保留原始文件供存储或下载时使用;上传到知识库/文档空间时可加 --workspace",
"用户明确要求用本地文件替换已有钉盘/文档空间文件时传 --node;该模式会覆盖远端内容并要求确认"
],
"source": "internal/cli/schema_hints/selection/drive.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。"
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。"
}
]
}
@@ -10780,7 +10783,7 @@
"use_when": [
"用户要把本地文件上传到钉盘/我的文件(首选一条命令自动完成凭证+PUT+提交)时",
"用户明确要求只上传或暂存附件、暂时不要发送到任何聊天会话时",
"上传到知识库/文档空间时加 --workspace;需要转在线文档时加 --convert",
"用户只要保留原始文件供存储或下载时使用;上传到知识库/文档空间时可加 --workspace",
"用户明确要求用本地文件替换已有钉盘/文档空间文件时传 --node;该模式会覆盖远端内容并要求确认"
]
},
@@ -1,6 +1,6 @@
{
"version": 1,
"source_hash": "sha256:b3c33641f91b69b73f3e319e0927e8654b6ba1e24f99071e22ee813a4e9f861d",
"source_hash": "sha256:35727a6c324fbffde5271e96d972843abdf780391151ba858f6d04384268cd51",
"surface_hash": "sha256:4897d582a472d9d3596f88fb4546e7795d6b9bc334d020712f5d895ce894f7a7",
"coverage": {
"surface_products": 26,
@@ -12,8 +12,8 @@
"tools_with_avoid_when": 875,
"tools_with_examples": 875,
"tools_with_interface_mode": 875,
"unmatched_skill_tools": 96,
"unreviewed_skill_tools": 11
"unmatched_skill_tools": 97,
"unreviewed_skill_tools": 12
},
"products": {
"aisearch": {
+79 -31
View File
@@ -1,6 +1,6 @@
{
"version": 1,
"source_hash": "sha256:b3c33641f91b69b73f3e319e0927e8654b6ba1e24f99071e22ee813a4e9f861d",
"source_hash": "sha256:35727a6c324fbffde5271e96d972843abdf780391151ba858f6d04384268cd51",
"surface_hash": "sha256:4897d582a472d9d3596f88fb4546e7795d6b9bc334d020712f5d895ce894f7a7",
"source_files": 160,
"hint_files": 54,
@@ -36,8 +36,8 @@
"tools_with_avoid_when": 875,
"tools_with_examples": 875,
"tools_with_interface_mode": 875,
"unmatched_skill_tools": 96,
"unreviewed_skill_tools": 11
"unmatched_skill_tools": 97,
"unreviewed_skill_tools": 12
},
"source_products": [
"agoal",
@@ -1301,6 +1301,30 @@
"reason": "旧版 Skill 或说明性引用,当前公开命令面无等价 leaf,禁止词法模糊映射"
}
},
{
"tool_path": "doc drive upload",
"source": "skills/mono/references/products/doc.md",
"line": 668,
"candidates": [
"doc upload",
"drive upload",
"doc +comment-create"
]
},
{
"tool_path": "doc import",
"source": "skills/mono/references/products/doc.md",
"line": 669,
"candidates": [
"doc import get",
"doc +comment-create",
"doc +comment-list"
],
"review": {
"status": "stale",
"reason": "旧版 Skill 或说明性引用,当前公开命令面无等价 leaf,禁止词法模糊映射"
}
},
{
"tool_path": "doc import",
"source": "skills/mono/references/products/doc.md",
@@ -1315,6 +1339,30 @@
"reason": "旧版 Skill 或说明性引用,当前公开命令面无等价 leaf,禁止词法模糊映射"
}
},
{
"tool_path": "doc drive upload",
"source": "skills/mono/references/products/doc.md",
"line": 745,
"candidates": [
"doc upload",
"drive upload",
"doc +comment-create"
]
},
{
"tool_path": "doc import",
"source": "skills/mono/references/products/doc.md",
"line": 746,
"candidates": [
"doc import get",
"doc +comment-create",
"doc +comment-list"
],
"review": {
"status": "stale",
"reason": "旧版 Skill 或说明性引用,当前公开命令面无等价 leaf,禁止词法模糊映射"
}
},
{
"tool_path": "doc import",
"source": "skills/mono/references/products/doc.md",
@@ -1357,34 +1405,6 @@
"reason": "旧版 Skill 或说明性引用,当前公开命令面无等价 leaf,禁止词法模糊映射"
}
},
{
"tool_path": "doc import",
"source": "skills/mono/references/products/doc/doc-import.md",
"line": 12,
"candidates": [
"doc import get",
"doc +comment-create",
"doc +comment-list"
],
"review": {
"status": "stale",
"reason": "旧版 Skill 或说明性引用,当前公开命令面无等价 leaf,禁止词法模糊映射"
}
},
{
"tool_path": "doc import",
"source": "skills/mono/references/products/doc/doc-import.md",
"line": 13,
"candidates": [
"doc import get",
"doc +comment-create",
"doc +comment-list"
],
"review": {
"status": "stale",
"reason": "旧版 Skill 或说明性引用,当前公开命令面无等价 leaf,禁止词法模糊映射"
}
},
{
"tool_path": "doc import",
"source": "skills/mono/references/products/doc/doc-import.md",
@@ -1413,6 +1433,34 @@
"reason": "旧版 Skill 或说明性引用,当前公开命令面无等价 leaf,禁止词法模糊映射"
}
},
{
"tool_path": "doc import",
"source": "skills/mono/references/products/doc/doc-import.md",
"line": 16,
"candidates": [
"doc import get",
"doc +comment-create",
"doc +comment-list"
],
"review": {
"status": "stale",
"reason": "旧版 Skill 或说明性引用,当前公开命令面无等价 leaf,禁止词法模糊映射"
}
},
{
"tool_path": "doc import",
"source": "skills/mono/references/products/doc/doc-import.md",
"line": 17,
"candidates": [
"doc import get",
"doc +comment-create",
"doc +comment-list"
],
"review": {
"status": "stale",
"reason": "旧版 Skill 或说明性引用,当前公开命令面无等价 leaf,禁止词法模糊映射"
}
},
{
"tool_path": "event consume user_im_message_receive_o2o_all",
"source": "skills/mono/references/products/event.md",
+34 -26
View File
@@ -1,18 +1,18 @@
{
"version": 1,
"surface_hash": "sha256:4897d582a472d9d3596f88fb4546e7795d6b9bc334d020712f5d895ce894f7a7",
"source_hash": "sha256:3246b6b86cec02dd90b7cd093310667d926d62d3237d3408cfccc13ad3751b45",
"source_hash": "sha256:807d11cf6c62a0d4aaca4b5a6b438e6654b2792d3a32b4e271032202d45ff8fa",
"catalog": {
"agent_metadata": {
"products_with_metadata": 26,
"source": "embedded-skill-metadata",
"source_hash": "sha256:b3c33641f91b69b73f3e319e0927e8654b6ba1e24f99071e22ee813a4e9f861d",
"source_hash": "sha256:35727a6c324fbffde5271e96d972843abdf780391151ba858f6d04384268cd51",
"surface_hash": "sha256:4897d582a472d9d3596f88fb4546e7795d6b9bc334d020712f5d895ce894f7a7",
"surface_products": 26,
"surface_tools": 875,
"tools_with_agent_summary": 875,
"tools_with_metadata": 875,
"unmatched_skill_tools": 96,
"unmatched_skill_tools": 97,
"version": 1
},
"count": 26,
@@ -15383,8 +15383,8 @@
"availability": "available",
"avoid_when": [
"创建表格/脑图/白板/多维表/演示改用 dws wiki node create --type \u003ctype\u003e(勿用 doc create)",
"只管理知识库空节点或层级时用 wiki node create;本命令侧重创建并写入 adoc",
"导入本地 Word/Markdown 并保留服务端转换语义时用 dws doc import;不要用自写分片脚本替代原生 create"
"在知识库建空节点实体也可用 wiki node create;本命令侧重可写初始内容的 adoc",
"导入本地 Word/Markdown 为在线文档用 dws doc import,并显式提供 --folder 或 --workspace;不要用已迁移的 doc upload --convert"
],
"canonical_path": "doc.create_document",
"cli_name": "create",
@@ -15405,8 +15405,8 @@
"risk": "medium",
"title": "创建文档",
"use_when": [
"用户要新建文字在线文档(adoc),可空文档或用 --content-file 写入 Markdown/JSONML 时",
"已知目标文档文件夹 --folder 或知识库 --workspace;长 Markdown 仍直接使用本命令,由原生写入管道自动分片"
"用户要新建文字在线文档(adoc),可空文档或用 --content-file 写入明确要求的初始 Markdown/JSONML;用户显式要求正文 H1 时必须保留,不能用 --name 替代",
"已知目标文档文件夹 --folder 或知识库 --workspace;长 Markdown 仍直接使用本命令,由原生写入管道自动分片;返回 nodeId 绑定同一请求的后续 list/insert/export,后续可观察操作仍须分别执行"
]
},
{
@@ -15537,7 +15537,7 @@
"agent_summary_source": "dws-agent-selection/doc",
"availability": "available",
"avoid_when": [
"常规文件删除优先 dws drive delete;本入口仅为兼容",
"常规文件删除使用 dws drive delete;不要继续选择已弃用的 doc delete 兼容入口",
"用户未确认或目标不清时不要删",
"删块用 doc block delete;删评论用 doc comment delete"
],
@@ -15601,7 +15601,8 @@
"agent_summary_source": "dws-agent-selection/doc",
"availability": "available",
"avoid_when": [
"下载钉盘普通文件用 drive download;导出在线文档用 doc export"
"下载钉盘普通文件用 drive download;导出在线文档用 doc export",
"不要把 blockId/nodeId 当作 resourceId,也不要编造 --output:本命令返回临时 downloadUrl"
],
"canonical_path": "doc.download_doc_attachment",
"cli_name": "download",
@@ -15623,7 +15624,7 @@
"risk": "low",
"title": "下载文档附件",
"use_when": [
"获取文档正文中附件的临时下载 URL(resourceId 来自 block list attachment)时"
"获取文档正文中附件的临时下载 URL 时;resourceId 必须来自本次 block list 返回的 attachment 块"
]
},
{
@@ -15761,7 +15762,7 @@
"agent_summary_source": "dws-agent-selection/doc",
"availability": "available",
"avoid_when": [
"发起导入用 doc import(若入口可用);不要用本命令代替导入"
"发起导入用 doc import;不要先调用 import get,也不要使用示例占位 taskId"
],
"canonical_path": "doc.import_get",
"cli_name": "get",
@@ -15782,7 +15783,7 @@
"risk": "medium",
"title": "查询导入任务结果(手动兜底)",
"use_when": [
"查询文档导入任务结果(已有 taskId,导入超时/中断后兜底)时"
"仅在 doc import 已返回真实 taskId 且自动轮询超时/中断后,查询导入任务结果时"
]
},
{
@@ -15791,8 +15792,9 @@
"agent_summary_source": "dws-agent-selection/doc",
"availability": "available",
"avoid_when": [
"整篇追加 Markdown 优先 doc update --mode append",
"插入本地文件附件优先 doc media insert",
"仅在用户没有指定块操作、只要整篇追加 Markdown 时优先 doc update --mode append;显式 insert/标题块/列表块不能折叠",
"插入本地文件附件优先 doc media insert;不要为普通附件手写 attachment JSON",
"整段 Markdown 或长内容优先 doc update --mode append,不要拆成大量 block insert",
"删块用 block delete;改已有块用 block update"
],
"canonical_path": "doc.insert_document_block",
@@ -15815,7 +15817,9 @@
"risk": "medium",
"title": "插入块元素",
"use_when": [
"在文档中插入新块(段落/标题等);简单场景用 --text/--heading,复杂块用 --element JSON"
"用户明确要求在文档中插入新块(段落/标题/列表等)时;简单段落/标题用 --text/--heading,复杂块用 --element JSON",
"用户要求有序列表块时使用 JSONML p.list.isOrdered=true(同一 listId)或等价原生 orderedList 结构,不能用普通 Markdown 代替",
"相对插入时先 block list 获取真实 blockId,再同时传 --ref-block 与 --where before|after"
]
},
{
@@ -15878,7 +15882,7 @@
"risk": "low",
"title": "查询块元素",
"use_when": [
"查看文档一级块结构、拿 blockId,供 insert/update/delete 或划词评论定位时"
"查看文档一级块结构、拿 blockId,供 insert/update/delete 或划词评论定位时;用户在显式工作流中点名 list 时必须真实执行并使用返回结果"
]
},
{
@@ -16107,7 +16111,8 @@
"agent_summary_source": "dws-agent-selection/doc",
"availability": "available",
"avoid_when": [
"新建评论用 create/create-inline;删评论用 delete"
"新建评论用 create/create-inline;删评论用 delete",
"不要把 commentId、blockId 或示例占位符传给 --comment-key"
],
"canonical_path": "doc.reply_comment",
"cli_name": "reply",
@@ -16129,7 +16134,7 @@
"risk": "medium",
"title": "回复评论",
"use_when": [
"回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 来自 list/create"
"回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 必须来自本次 list/create 返回"
]
},
{
@@ -16826,10 +16831,10 @@
"agent_summary_source": "dws-agent-selection/doc",
"availability": "available",
"avoid_when": [
"只在末尾补一小段纯文本优先 +doc-append;只改一个块用 doc block update",
"目标不是 adoc 时按 drive info 的 extension 切对应产品",
"覆盖模式用户未确认前不要执行;可先 --dry-run 预览",
"创建新文档用 doc create;不要预先手工分片或循环重试覆盖"
"目标不是 adoc 或只要改单个块时改用 doc block update",
"覆盖模式用户未确认前不要执行;先 --dry-run 预览,确认后再加 --yes",
"--content 与 --content-file 二选一;--index 仅用于 mode=append",
"创建新文档用 doc create,不要用 update 冒充创建"
],
"canonical_path": "doc.update_document",
"cli_name": "update",
@@ -16852,7 +16857,8 @@
"use_when": [
"用户要向已有 adoc 追加长、多行或文件内容时用 --mode append + --content-file;CLI 自动分片",
"用户明确要求整篇覆盖替换时用 --mode overwrite(破坏性)",
"append 且要插到第 N 个 block 前时加 --index N(先 block list)"
"append 且要插到第 N 个 block 前时加 --index N(先 block list)",
"长文本、多行内容或表格优先写入 UTF-8 文件并使用 --content-file,避免 shell 转义损坏"
]
},
{
@@ -16862,7 +16868,8 @@
"availability": "available",
"avoid_when": [
"插入新块用 block insert;删除用 block delete",
"改文档显示名用 rename;整篇覆盖用 update overwrite"
"改文档显示名用 rename;整篇覆盖用 update overwrite",
"不要跨文档复用 blockId,也不要把 nodeId/commentKey 当作 blockId"
],
"canonical_path": "doc.update_document_block",
"cli_name": "update",
@@ -16884,7 +16891,7 @@
"risk": "medium",
"title": "更新块元素",
"use_when": [
"修改已有块的文本/标题/样式(已知 blockId)时"
"修改已有块的文本/标题/样式时;blockId 必须来自目标文档本次 block list 结果"
]
},
{
@@ -18289,6 +18296,7 @@
"用户要把文件发送到群聊或单聊时使用 chat message send --msg-type file --file-path;drive upload 不会发送聊天消息",
"常规场景不要拆成 upload-info + 手动 PUT + commit;仅自定义流式上传才用三步",
"要把文件作为文档正文附件插入改用 dws doc media insert",
"Word/Excel/Markdown 要求上传后在线编辑、大家直接在线改或转钉钉文档时改用 dws doc import;普通 drive upload 返回 docx/xlsx 文件不能宣称可在线编辑",
"用户明确说文档空间且走 doc 兼容入口时可用 dws doc upload,默认仍推荐本命令",
"用户没有明确同意替换目标文件时不要使用 --node;新建上传应使用 --folder 或目标根目录"
],
@@ -18312,7 +18320,7 @@
"use_when": [
"用户要把本地文件上传到钉盘/我的文件(首选一条命令自动完成凭证+PUT+提交)时",
"用户明确要求只上传或暂存附件、暂时不要发送到任何聊天会话时",
"上传到知识库/文档空间时加 --workspace;需要转在线文档时加 --convert",
"用户只要保留原始文件供存储或下载时使用;上传到知识库/文档空间时可加 --workspace",
"用户明确要求用本地文件替换已有钉盘/文档空间文件时传 --node;该模式会覆盖远端内容并要求确认"
]
}
+117 -117
View File
@@ -2321,8 +2321,8 @@
"availability": "available",
"avoid_when": [
"创建表格/脑图/白板/多维表/演示改用 dws wiki node create --type \u003ctype\u003e(勿用 doc create)",
"只管理知识库空节点或层级时用 wiki node create;本命令侧重创建并写入 adoc",
"导入本地 Word/Markdown 并保留服务端转换语义时用 dws doc import;不要用自写分片脚本替代原生 create"
"在知识库建空节点实体也可用 wiki node create;本命令侧重可写初始内容的 adoc",
"导入本地 Word/Markdown 为在线文档用 dws doc import,并显式提供 --folder 或 --workspace;不要用已迁移的 doc upload --convert"
],
"canonical_path": "doc.create_document",
"cli_name": "create",
@@ -2341,7 +2341,7 @@
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "人工对齐当前 Cobra 原生内容写入管道、doc Skill 渐进路由与 workspace/folder 边界;强调长内容无需外部分片,不改变命令身份、参数或安全事实。",
"review_reason": "人工对齐当前 Cobra 原生内容写入管道、显式正文 H1、同请求新 nodeId 绑定和可观察块操作边界;强调长内容无需外部分片,不改变命令身份、参数或安全事实。",
"selected": true,
"source": "internal/cli/schema_hints/selection/doc.json",
"value": "在默认根目录、文档文件夹或知识库根创建带可选初始内容的 adoc"
@@ -2349,7 +2349,7 @@
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工对齐当前 Cobra 原生内容写入管道、doc Skill 渐进路由与 workspace/folder 边界;强调长内容无需外部分片,不改变命令身份、参数或安全事实。",
"review_reason": "人工对齐当前 Cobra 原生内容写入管道、显式正文 H1、同请求新 nodeId 绑定和可观察块操作边界;强调长内容无需外部分片,不改变命令身份、参数或安全事实。",
"source": "internal/cli/schema_hints/selection/doc.json",
"value": "在默认根目录、文档文件夹或知识库根创建带可选初始内容的 adoc"
},
@@ -2379,24 +2379,24 @@
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "人工对齐当前 Cobra 原生内容写入管道、doc Skill 渐进路由与 workspace/folder 边界;强调长内容无需外部分片,不改变命令身份、参数或安全事实。",
"review_reason": "人工对齐当前 Cobra 原生内容写入管道、显式正文 H1、同请求新 nodeId 绑定和可观察块操作边界;强调长内容无需外部分片,不改变命令身份、参数或安全事实。",
"selected": true,
"source": "internal/cli/schema_hints/selection/doc.json",
"value": [
"创建表格/脑图/白板/多维表/演示改用 dws wiki node create --type \u003ctype\u003e(勿用 doc create)",
"只管理知识库空节点或层级时用 wiki node create;本命令侧重创建并写入 adoc",
"导入本地 Word/Markdown 并保留服务端转换语义时用 dws doc import;不要用自写分片脚本替代原生 create"
"在知识库建空节点实体也可用 wiki node create;本命令侧重可写初始内容的 adoc",
"导入本地 Word/Markdown 为在线文档用 dws doc import,并显式提供 --folder 或 --workspace;不要用已迁移的 doc upload --convert"
]
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工对齐当前 Cobra 原生内容写入管道、doc Skill 渐进路由与 workspace/folder 边界;强调长内容无需外部分片,不改变命令身份、参数或安全事实。",
"review_reason": "人工对齐当前 Cobra 原生内容写入管道、显式正文 H1、同请求新 nodeId 绑定和可观察块操作边界;强调长内容无需外部分片,不改变命令身份、参数或安全事实。",
"source": "internal/cli/schema_hints/selection/doc.json",
"value": [
"创建表格/脑图/白板/多维表/演示改用 dws wiki node create --type \u003ctype\u003e(勿用 doc create)",
"只管理知识库空节点或层级时用 wiki node create;本命令侧重创建并写入 adoc",
"导入本地 Word/Markdown 并保留服务端转换语义时用 dws doc import;不要用自写分片脚本替代原生 create"
"在知识库建空节点实体也可用 wiki node create;本命令侧重可写初始内容的 adoc",
"导入本地 Word/Markdown 为在线文档用 dws doc import,并显式提供 --folder 或 --workspace;不要用已迁移的 doc upload --convert"
]
},
"canonical_path": {
@@ -2467,7 +2467,7 @@
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "人工对齐当前 Cobra 原生内容写入管道、doc Skill 渐进路由与 workspace/folder 边界;强调长内容无需外部分片,不改变命令身份、参数或安全事实。",
"review_reason": "人工对齐当前 Cobra 原生内容写入管道、显式正文 H1、同请求新 nodeId 绑定和可观察块操作边界;强调长内容无需外部分片,不改变命令身份、参数或安全事实。",
"selected": true,
"source": "internal/cli/schema_hints/selection/doc.json",
"value": [
@@ -2478,7 +2478,7 @@
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工对齐当前 Cobra 原生内容写入管道、doc Skill 渐进路由与 workspace/folder 边界;强调长内容无需外部分片,不改变命令身份、参数或安全事实。",
"review_reason": "人工对齐当前 Cobra 原生内容写入管道、显式正文 H1、同请求新 nodeId 绑定和可观察块操作边界;强调长内容无需外部分片,不改变命令身份、参数或安全事实。",
"source": "internal/cli/schema_hints/selection/doc.json",
"value": [
"dws doc create --name \"项目周报\" --format json",
@@ -2572,7 +2572,7 @@
},
{
"precedence": "reviewed_explicit",
"review_reason": "人工对齐当前 Cobra 原生内容写入管道、doc Skill 渐进路由与 workspace/folder 边界;强调长内容无需外部分片,不改变命令身份、参数或安全事实。",
"review_reason": "人工对齐当前 Cobra 原生内容写入管道、显式正文 H1、同请求新 nodeId 绑定和可观察块操作边界;强调长内容无需外部分片,不改变命令身份、参数或安全事实。",
"selected": false,
"source": "internal/cli/schema_hints/selection/doc.json",
"value": true
@@ -2622,22 +2622,22 @@
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "人工对齐当前 Cobra 原生内容写入管道、doc Skill 渐进路由与 workspace/folder 边界;强调长内容无需外部分片,不改变命令身份、参数或安全事实。",
"review_reason": "人工对齐当前 Cobra 原生内容写入管道、显式正文 H1、同请求新 nodeId 绑定和可观察块操作边界;强调长内容无需外部分片,不改变命令身份、参数或安全事实。",
"selected": true,
"source": "internal/cli/schema_hints/selection/doc.json",
"value": [
"用户要新建文字在线文档(adoc),可空文档或用 --content-file 写入 Markdown/JSONML 时",
"已知目标文档文件夹 --folder 或知识库 --workspace;长 Markdown 仍直接使用本命令,由原生写入管道自动分片"
"用户要新建文字在线文档(adoc),可空文档或用 --content-file 写入明确要求的初始 Markdown/JSONML;用户显式要求正文 H1 时必须保留,不能用 --name 替代",
"已知目标文档文件夹 --folder 或知识库 --workspace;长 Markdown 仍直接使用本命令,由原生写入管道自动分片;返回 nodeId 绑定同一请求的后续 list/insert/export,后续可观察操作仍须分别执行"
]
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工对齐当前 Cobra 原生内容写入管道、doc Skill 渐进路由与 workspace/folder 边界;强调长内容无需外部分片,不改变命令身份、参数或安全事实。",
"review_reason": "人工对齐当前 Cobra 原生内容写入管道、显式正文 H1、同请求新 nodeId 绑定和可观察块操作边界;强调长内容无需外部分片,不改变命令身份、参数或安全事实。",
"source": "internal/cli/schema_hints/selection/doc.json",
"value": [
"用户要新建文字在线文档(adoc),可空文档或用 --content-file 写入 Markdown/JSONML 时",
"已知目标文档文件夹 --folder 或知识库 --workspace;长 Markdown 仍直接使用本命令,由原生写入管道自动分片"
"用户要新建文字在线文档(adoc),可空文档或用 --content-file 写入明确要求的初始 Markdown/JSONML;用户显式要求正文 H1 时必须保留,不能用 --name 替代",
"已知目标文档文件夹 --folder 或知识库 --workspace;长 Markdown 仍直接使用本命令,由原生写入管道自动分片;返回 nodeId 绑定同一请求的后续 list/insert/export,后续可观察操作仍须分别执行"
]
}
},
@@ -3335,8 +3335,8 @@
"source": "reviewed_command_registry",
"title": "创建文档",
"use_when": [
"用户要新建文字在线文档(adoc),可空文档或用 --content-file 写入 Markdown/JSONML 时",
"已知目标文档文件夹 --folder 或知识库 --workspace;长 Markdown 仍直接使用本命令,由原生写入管道自动分片"
"用户要新建文字在线文档(adoc),可空文档或用 --content-file 写入明确要求的初始 Markdown/JSONML;用户显式要求正文 H1 时必须保留,不能用 --name 替代",
"已知目标文档文件夹 --folder 或知识库 --workspace;长 Markdown 仍直接使用本命令,由原生写入管道自动分片;返回 nodeId 绑定同一请求的后续 list/insert/export,后续可观察操作仍须分别执行"
]
},
"doc.create_file": {
@@ -6450,7 +6450,7 @@
"agent_summary_source": "dws-agent-selection/doc",
"availability": "available",
"avoid_when": [
"常规文件删除优先 dws drive delete;本入口仅为兼容",
"常规文件删除使用 dws drive delete;不要继续选择已弃用的 doc delete 兼容入口",
"用户未确认或目标不清时不要删",
"删块用 doc block delete;删评论用 doc comment delete"
],
@@ -6512,7 +6512,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/doc.json",
"value": [
"常规文件删除优先 dws drive delete;本入口仅为兼容",
"常规文件删除使用 dws drive delete;不要继续选择已弃用的 doc delete 兼容入口",
"用户未确认或目标不清时不要删",
"删块用 doc block delete;删评论用 doc comment delete"
]
@@ -6523,7 +6523,7 @@
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"source": "internal/cli/schema_hints/selection/doc.json",
"value": [
"常规文件删除优先 dws drive delete;本入口仅为兼容",
"常规文件删除使用 dws drive delete;不要继续选择已弃用的 doc delete 兼容入口",
"用户未确认或目标不清时不要删",
"删块用 doc block delete;删评论用 doc comment delete"
]
@@ -7557,7 +7557,8 @@
"agent_summary_source": "dws-agent-selection/doc",
"availability": "available",
"avoid_when": [
"下载钉盘普通文件用 drive download;导出在线文档用 doc export"
"下载钉盘普通文件用 drive download;导出在线文档用 doc export",
"不要把 blockId/nodeId 当作 resourceId,也不要编造 --output:本命令返回临时 downloadUrl"
],
"canonical_path": "doc.download_doc_attachment",
"cli_name": "download",
@@ -7617,7 +7618,8 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/doc.json",
"value": [
"下载钉盘普通文件用 drive download;导出在线文档用 doc export"
"下载钉盘普通文件用 drive download;导出在线文档用 doc export",
"不要把 blockId/nodeId 当作 resourceId,也不要编造 --output:本命令返回临时 downloadUrl"
]
}
],
@@ -7626,7 +7628,8 @@
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"source": "internal/cli/schema_hints/selection/doc.json",
"value": [
"下载钉盘普通文件用 drive download;导出在线文档用 doc export"
"下载钉盘普通文件用 drive download;导出在线文档用 doc export",
"不要把 blockId/nodeId 当作 resourceId,也不要编造 --output:本命令返回临时 downloadUrl"
]
},
"canonical_path": {
@@ -7848,7 +7851,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/doc.json",
"value": [
"获取文档正文中附件的临时下载 URL(resourceId 来自 block list attachment)时"
"获取文档正文中附件的临时下载 URL 时;resourceId 必须来自本次 block list 返回的 attachment 块"
]
}
],
@@ -7857,7 +7860,7 @@
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"source": "internal/cli/schema_hints/selection/doc.json",
"value": [
"获取文档正文中附件的临时下载 URL(resourceId 来自 block list attachment)时"
"获取文档正文中附件的临时下载 URL 时;resourceId 必须来自本次 block list 返回的 attachment 块"
]
}
},
@@ -8095,7 +8098,7 @@
"source": "reviewed_command_registry",
"title": "下载文档附件",
"use_when": [
"获取文档正文中附件的临时下载 URL(resourceId 来自 block list attachment)时"
"获取文档正文中附件的临时下载 URL 时;resourceId 必须来自本次 block list 返回的 attachment 块"
]
},
"doc.download_file": {
@@ -10942,7 +10945,7 @@
"agent_summary_source": "dws-agent-selection/doc",
"availability": "available",
"avoid_when": [
"发起导入用 doc import(若入口可用);不要用本命令代替导入"
"发起导入用 doc import;不要先调用 import get,也不要使用示例占位 taskId"
],
"canonical_path": "doc.import_get",
"cli_name": "get",
@@ -10999,7 +11002,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/doc.json",
"value": [
"发起导入用 doc import(若入口可用);不要用本命令代替导入"
"发起导入用 doc import;不要先调用 import get,也不要使用示例占位 taskId"
]
}
],
@@ -11008,7 +11011,7 @@
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"source": "internal/cli/schema_hints/selection/doc.json",
"value": [
"发起导入用 doc import(若入口可用);不要用本命令代替导入"
"发起导入用 doc import;不要先调用 import get,也不要使用示例占位 taskId"
]
},
"canonical_path": {
@@ -11264,7 +11267,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/doc.json",
"value": [
"查询文档导入任务结果(已有 taskId,导入超时/中断后兜底)时"
"仅在 doc import 已返回真实 taskId 且自动轮询超时/中断后,查询导入任务结果时"
]
}
],
@@ -11273,7 +11276,7 @@
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"source": "internal/cli/schema_hints/selection/doc.json",
"value": [
"查询文档导入任务结果(已有 taskId,导入超时/中断后兜底)时"
"仅在 doc import 已返回真实 taskId 且自动轮询超时/中断后,查询导入任务结果时"
]
}
},
@@ -11392,7 +11395,7 @@
"source": "reviewed_command_registry",
"title": "查询导入任务结果(手动兜底)",
"use_when": [
"查询文档导入任务结果(已有 taskId,导入超时/中断后兜底)时"
"仅在 doc import 已返回真实 taskId 且自动轮询超时/中断后,查询导入任务结果时"
]
},
"doc.insert_document_block": {
@@ -11416,8 +11419,9 @@
"agent_summary_source": "dws-agent-selection/doc",
"availability": "available",
"avoid_when": [
"整篇追加 Markdown 优先 doc update --mode append",
"插入本地文件附件优先 doc media insert",
"仅在用户没有指定块操作、只要整篇追加 Markdown 时优先 doc update --mode append;显式 insert/标题块/列表块不能折叠",
"插入本地文件附件优先 doc media insert;不要为普通附件手写 attachment JSON",
"整段 Markdown 或长内容优先 doc update --mode append,不要拆成大量 block insert",
"删块用 block delete;改已有块用 block update"
],
"canonical_path": "doc.insert_document_block",
@@ -11446,7 +11450,7 @@
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐显式 block insert 的可观察操作语义和有序列表原生结构;不改变命令身份、参数契约、接口绑定或 Runtime 门禁。",
"selected": true,
"source": "internal/cli/schema_hints/selection/doc.json",
"value": "向文档插入块元素"
@@ -11454,7 +11458,7 @@
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐显式 block insert 的可观察操作语义和有序列表原生结构;不改变命令身份、参数契约、接口绑定或 Runtime 门禁。",
"source": "internal/cli/schema_hints/selection/doc.json",
"value": "向文档插入块元素"
},
@@ -11484,23 +11488,25 @@
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐显式 block insert 的可观察操作语义和有序列表原生结构;不改变命令身份、参数契约、接口绑定或 Runtime 门禁。",
"selected": true,
"source": "internal/cli/schema_hints/selection/doc.json",
"value": [
"整篇追加 Markdown 优先 doc update --mode append",
"插入本地文件附件优先 doc media insert",
"仅在用户没有指定块操作、只要整篇追加 Markdown 时优先 doc update --mode append;显式 insert/标题块/列表块不能折叠",
"插入本地文件附件优先 doc media insert;不要为普通附件手写 attachment JSON",
"整段 Markdown 或长内容优先 doc update --mode append,不要拆成大量 block insert",
"删块用 block delete;改已有块用 block update"
]
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐显式 block insert 的可观察操作语义和有序列表原生结构;不改变命令身份、参数契约、接口绑定或 Runtime 门禁。",
"source": "internal/cli/schema_hints/selection/doc.json",
"value": [
"整篇追加 Markdown 优先 doc update --mode append",
"插入本地文件附件优先 doc media insert",
"仅在用户没有指定块操作、只要整篇追加 Markdown 时优先 doc update --mode append;显式 insert/标题块/列表块不能折叠",
"插入本地文件附件优先 doc media insert;不要为普通附件手写 attachment JSON",
"整段 Markdown 或长内容优先 doc update --mode append,不要拆成大量 block insert",
"删块用 block delete;改已有块用 block update"
]
},
@@ -11572,7 +11578,7 @@
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐显式 block insert 的可观察操作语义和有序列表原生结构;不改变命令身份、参数契约、接口绑定或 Runtime 门禁。",
"selected": true,
"source": "internal/cli/schema_hints/selection/doc.json",
"value": [
@@ -11583,7 +11589,7 @@
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐显式 block insert 的可观察操作语义和有序列表原生结构;不改变命令身份、参数契约、接口绑定或 Runtime 门禁。",
"source": "internal/cli/schema_hints/selection/doc.json",
"value": [
"dws doc block insert --node \u003cDOC_ID\u003e --text \"这是一段文字\" --format json",
@@ -11671,7 +11677,7 @@
},
{
"precedence": "reviewed_explicit",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐显式 block insert 的可观察操作语义和有序列表原生结构;不改变命令身份、参数契约、接口绑定或 Runtime 门禁。",
"selected": false,
"source": "internal/cli/schema_hints/selection/doc.json",
"value": true
@@ -11721,20 +11727,24 @@
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐显式 block insert 的可观察操作语义和有序列表原生结构;不改变命令身份、参数契约、接口绑定或 Runtime 门禁。",
"selected": true,
"source": "internal/cli/schema_hints/selection/doc.json",
"value": [
"在文档中插入新块(段落/标题等);简单场景用 --text/--heading,复杂块用 --element JSON"
"用户明确要求在文档中插入新块(段落/标题/列表等)时;简单段落/标题用 --text/--heading,复杂块用 --element JSON",
"用户要求有序列表块时使用 JSONML p.list.isOrdered=true(同一 listId)或等价原生 orderedList 结构,不能用普通 Markdown 代替",
"相对插入时先 block list 获取真实 blockId,再同时传 --ref-block 与 --where before|after"
]
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐显式 block insert 的可观察操作语义和有序列表原生结构;不改变命令身份、参数契约、接口绑定或 Runtime 门禁。",
"source": "internal/cli/schema_hints/selection/doc.json",
"value": [
"在文档中插入新块(段落/标题等);简单场景用 --text/--heading,复杂块用 --element JSON"
"用户明确要求在文档中插入新块(段落/标题/列表等)时;简单段落/标题用 --text/--heading,复杂块用 --element JSON",
"用户要求有序列表块时使用 JSONML p.list.isOrdered=true(同一 listId)或等价原生 orderedList 结构,不能用普通 Markdown 代替",
"相对插入时先 block list 获取真实 blockId,再同时传 --ref-block 与 --where before|after"
]
}
},
@@ -12876,7 +12886,9 @@
"source": "reviewed_command_registry",
"title": "插入块元素",
"use_when": [
"在文档中插入新块(段落/标题等);简单场景用 --text/--heading,复杂块用 --element JSON"
"用户明确要求在文档中插入新块(段落/标题/列表等)时;简单段落/标题用 --text/--heading,复杂块用 --element JSON",
"用户要求有序列表块时使用 JSONML p.list.isOrdered=true(同一 listId)或等价原生 orderedList 结构,不能用普通 Markdown 代替",
"相对插入时先 block list 获取真实 blockId,再同时传 --ref-block 与 --where before|after"
]
},
"doc.list_comments": {
@@ -13811,7 +13823,7 @@
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐 block list 的显式可观察步骤和真实 blockId 上下文传递;不改变命令身份、参数契约或接口绑定。",
"selected": true,
"source": "internal/cli/schema_hints/selection/doc.json",
"value": "查询文档一级块元素列表"
@@ -13819,7 +13831,7 @@
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐 block list 的显式可观察步骤和真实 blockId 上下文传递;不改变命令身份、参数契约或接口绑定。",
"source": "internal/cli/schema_hints/selection/doc.json",
"value": "查询文档一级块元素列表"
},
@@ -13849,7 +13861,7 @@
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐 block list 的显式可观察步骤和真实 blockId 上下文传递;不改变命令身份、参数契约或接口绑定。",
"selected": true,
"source": "internal/cli/schema_hints/selection/doc.json",
"value": [
@@ -13860,7 +13872,7 @@
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐 block list 的显式可观察步骤和真实 blockId 上下文传递;不改变命令身份、参数契约或接口绑定。",
"source": "internal/cli/schema_hints/selection/doc.json",
"value": [
"只要全文 Markdown 用 doc read",
@@ -13935,7 +13947,7 @@
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐 block list 的显式可观察步骤和真实 blockId 上下文传递;不改变命令身份、参数契约或接口绑定。",
"selected": true,
"source": "internal/cli/schema_hints/selection/doc.json",
"value": [
@@ -13946,7 +13958,7 @@
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐 block list 的显式可观察步骤和真实 blockId 上下文传递;不改变命令身份、参数契约或接口绑定。",
"source": "internal/cli/schema_hints/selection/doc.json",
"value": [
"dws doc block list --node \u003cDOC_ID\u003e --format json",
@@ -14040,7 +14052,7 @@
},
{
"precedence": "reviewed_explicit",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐 block list 的显式可观察步骤和真实 blockId 上下文传递;不改变命令身份、参数契约或接口绑定。",
"selected": false,
"source": "internal/cli/schema_hints/selection/doc.json",
"value": true
@@ -14090,20 +14102,20 @@
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐 block list 的显式可观察步骤和真实 blockId 上下文传递;不改变命令身份、参数契约或接口绑定。",
"selected": true,
"source": "internal/cli/schema_hints/selection/doc.json",
"value": [
"查看文档一级块结构、拿 blockId,供 insert/update/delete 或划词评论定位时"
"查看文档一级块结构、拿 blockId,供 insert/update/delete 或划词评论定位时;用户在显式工作流中点名 list 时必须真实执行并使用返回结果"
]
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐 block list 的显式可观察步骤和真实 blockId 上下文传递;不改变命令身份、参数契约或接口绑定。",
"source": "internal/cli/schema_hints/selection/doc.json",
"value": [
"查看文档一级块结构、拿 blockId,供 insert/update/delete 或划词评论定位时"
"查看文档一级块结构、拿 blockId,供 insert/update/delete 或划词评论定位时;用户在显式工作流中点名 list 时必须真实执行并使用返回结果"
]
}
},
@@ -14747,7 +14759,7 @@
"source": "reviewed_command_registry",
"title": "查询块元素",
"use_when": [
"查看文档一级块结构、拿 blockId,供 insert/update/delete 或划词评论定位时"
"查看文档一级块结构、拿 blockId,供 insert/update/delete 或划词评论定位时;用户在显式工作流中点名 list 时必须真实执行并使用返回结果"
]
},
"doc.list_nodes": {
@@ -19705,7 +19717,8 @@
"agent_summary_source": "dws-agent-selection/doc",
"availability": "available",
"avoid_when": [
"新建评论用 create/create-inline;删评论用 delete"
"新建评论用 create/create-inline;删评论用 delete",
"不要把 commentId、blockId 或示例占位符传给 --comment-key"
],
"canonical_path": "doc.reply_comment",
"cli_name": "reply",
@@ -19766,7 +19779,8 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/doc.json",
"value": [
"新建评论用 create/create-inline;删评论用 delete"
"新建评论用 create/create-inline;删评论用 delete",
"不要把 commentId、blockId 或示例占位符传给 --comment-key"
]
}
],
@@ -19775,7 +19789,8 @@
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"source": "internal/cli/schema_hints/selection/doc.json",
"value": [
"新建评论用 create/create-inline;删评论用 delete"
"新建评论用 create/create-inline;删评论用 delete",
"不要把 commentId、blockId 或示例占位符传给 --comment-key"
]
},
"canonical_path": {
@@ -20053,7 +20068,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/doc.json",
"value": [
"回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 来自 list/create"
"回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 必须来自本次 list/create 返回"
]
}
],
@@ -20062,7 +20077,7 @@
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"source": "internal/cli/schema_hints/selection/doc.json",
"value": [
"回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 来自 list/create"
"回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 必须来自本次 list/create 返回"
]
}
},
@@ -20723,7 +20738,7 @@
"source": "reviewed_command_registry",
"title": "回复评论",
"use_when": [
"回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 来自 list/create"
"回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 必须来自本次 list/create 返回"
]
},
"doc.search_documents": {
@@ -38105,10 +38120,10 @@
"agent_summary_source": "dws-agent-selection/doc",
"availability": "available",
"avoid_when": [
"只在末尾补一小段纯文本优先 +doc-append;只改一个块用 doc block update",
"目标不是 adoc 时按 drive info 的 extension 切对应产品",
"覆盖模式用户未确认前不要执行;可先 --dry-run 预览",
"创建新文档用 doc create;不要预先手工分片或循环重试覆盖"
"目标不是 adoc 或只要改单个块时改用 doc block update",
"覆盖模式用户未确认前不要执行;先 --dry-run 预览,确认后再加 --yes",
"--content 与 --content-file 二选一;--index 仅用于 mode=append",
"创建新文档用 doc create,不要用 update 冒充创建"
],
"canonical_path": "doc.update_document",
"cli_name": "update",
@@ -38183,10 +38198,10 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/doc.json",
"value": [
"只在末尾补一小段纯文本优先 +doc-append;只改一个块用 doc block update",
"目标不是 adoc 时按 drive info 的 extension 切对应产品",
"覆盖模式用户未确认前不要执行;可先 --dry-run 预览",
"创建新文档用 doc create;不要预先手工分片或循环重试覆盖"
"目标不是 adoc 或只要改单个块时改用 doc block update",
"覆盖模式用户未确认前不要执行;先 --dry-run 预览,确认后再加 --yes",
"--content 与 --content-file 二选一;--index 仅用于 mode=append",
"创建新文档用 doc create,不要用 update 冒充创建"
]
}
],
@@ -38195,10 +38210,10 @@
"review_reason": "人工对齐 Runtime 的 10000 字符自动分片、append/overwrite 动态门禁与根 Skill 的写后回读流程;不改变参数和安全事实。",
"source": "internal/cli/schema_hints/selection/doc.json",
"value": [
"只在末尾补一小段纯文本优先 +doc-append;只改一个块用 doc block update",
"目标不是 adoc 时按 drive info 的 extension 切对应产品",
"覆盖模式用户未确认前不要执行;可先 --dry-run 预览",
"创建新文档用 doc create;不要预先手工分片或循环重试覆盖"
"目标不是 adoc 或只要改单个块时改用 doc block update",
"覆盖模式用户未确认前不要执行;先 --dry-run 预览,确认后再加 --yes",
"--content 与 --content-file 二选一;--index 仅用于 mode=append",
"创建新文档用 doc create,不要用 update 冒充创建"
]
},
"canonical_path": {
@@ -38454,7 +38469,8 @@
"value": [
"用户要向已有 adoc 追加长、多行或文件内容时用 --mode append + --content-file;CLI 自动分片",
"用户明确要求整篇覆盖替换时用 --mode overwrite(破坏性)",
"append 且要插到第 N 个 block 前时加 --index N(先 block list)"
"append 且要插到第 N 个 block 前时加 --index N(先 block list)",
"长文本、多行内容或表格优先写入 UTF-8 文件并使用 --content-file,避免 shell 转义损坏"
]
}
],
@@ -38465,7 +38481,8 @@
"value": [
"用户要向已有 adoc 追加长、多行或文件内容时用 --mode append + --content-file;CLI 自动分片",
"用户明确要求整篇覆盖替换时用 --mode overwrite(破坏性)",
"append 且要插到第 N 个 block 前时加 --index N(先 block list)"
"append 且要插到第 N 个 block 前时加 --index N(先 block list)",
"长文本、多行内容或表格优先写入 UTF-8 文件并使用 --content-file,避免 shell 转义损坏"
]
}
},
@@ -39080,23 +39097,8 @@
"type": "integer"
},
"mode": {
"cli_required": true,
"description": "更新模式: overwrite=覆盖, append=追加 (必填)",
"field_provenance": {
"cli_required": {
"candidates": [
{
"precedence": "cobra_contract",
"selected": true,
"source": "cobra_hard_required",
"value": true
}
],
"precedence": "cobra_contract",
"resolution": "highest_precedence",
"source": "cobra_hard_required",
"value": true
},
"description": {
"candidates": [
{
@@ -39139,15 +39141,9 @@
},
"required": {
"candidates": [
{
"precedence": "cobra_contract",
"selected": true,
"source": "cobra_hard_required",
"value": true
},
{
"precedence": "inference",
"selected": false,
"selected": true,
"source": "usage_required_inference",
"value": true
},
@@ -39158,9 +39154,9 @@
"value": false
}
],
"precedence": "cobra_contract",
"precedence": "inference",
"resolution": "highest_precedence",
"source": "cobra_hard_required",
"source": "usage_required_inference",
"value": true
},
"required_when": {
@@ -39526,7 +39522,8 @@
"use_when": [
"用户要向已有 adoc 追加长、多行或文件内容时用 --mode append + --content-file;CLI 自动分片",
"用户明确要求整篇覆盖替换时用 --mode overwrite(破坏性)",
"append 且要插到第 N 个 block 前时加 --index N(先 block list)"
"append 且要插到第 N 个 block 前时加 --index N(先 block list)",
"长文本、多行内容或表格优先写入 UTF-8 文件并使用 --content-file,避免 shell 转义损坏"
]
},
"doc.update_document_block": {
@@ -39551,7 +39548,8 @@
"availability": "available",
"avoid_when": [
"插入新块用 block insert;删除用 block delete",
"改文档显示名用 rename;整篇覆盖用 update overwrite"
"改文档显示名用 rename;整篇覆盖用 update overwrite",
"不要跨文档复用 blockId,也不要把 nodeId/commentKey 当作 blockId"
],
"canonical_path": "doc.update_document_block",
"cli_name": "update",
@@ -39621,7 +39619,8 @@
"source": "internal/cli/schema_hints/selection/doc.json",
"value": [
"插入新块用 block insert;删除用 block delete",
"改文档显示名用 rename;整篇覆盖用 update overwrite"
"改文档显示名用 rename;整篇覆盖用 update overwrite",
"不要跨文档复用 blockId,也不要把 nodeId/commentKey 当作 blockId"
]
}
],
@@ -39631,7 +39630,8 @@
"source": "internal/cli/schema_hints/selection/doc.json",
"value": [
"插入新块用 block insert;删除用 block delete",
"改文档显示名用 rename;整篇覆盖用 update overwrite"
"改文档显示名用 rename;整篇覆盖用 update overwrite",
"不要跨文档复用 blockId,也不要把 nodeId/commentKey 当作 blockId"
]
},
"canonical_path": {
@@ -39853,7 +39853,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/doc.json",
"value": [
"修改已有块的文本/标题/样式(已知 blockId)时"
"修改已有块的文本/标题/样式时;blockId 必须来自目标文档本次 block list 结果"
]
}
],
@@ -39862,7 +39862,7 @@
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"source": "internal/cli/schema_hints/selection/doc.json",
"value": [
"修改已有块的文本/标题/样式(已知 blockId)时"
"修改已有块的文本/标题/样式时;blockId 必须来自目标文档本次 block list 结果"
]
}
},
@@ -40704,7 +40704,7 @@
"source": "reviewed_command_registry",
"title": "更新块元素",
"use_when": [
"修改已有块的文本/标题/样式(已知 blockId)时"
"修改已有块的文本/标题/样式时;blockId 必须来自目标文档本次 block list 结果"
]
},
"doc.update_permission": {
+15 -12
View File
@@ -27245,6 +27245,7 @@
"用户要把文件发送到群聊或单聊时使用 chat message send --msg-type file --file-path;drive upload 不会发送聊天消息",
"常规场景不要拆成 upload-info + 手动 PUT + commit;仅自定义流式上传才用三步",
"要把文件作为文档正文附件插入改用 dws doc media insert",
"Word/Excel/Markdown 要求上传后在线编辑、大家直接在线改或转钉钉文档时改用 dws doc import;普通 drive upload 返回 docx/xlsx 文件不能宣称可在线编辑",
"用户明确说文档空间且走 doc 兼容入口时可用 dws doc upload,默认仍推荐本命令",
"用户没有明确同意替换目标文件时不要使用 --node;新建上传应使用 --folder 或目标根目录"
],
@@ -27268,7 +27269,7 @@
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
"selected": true,
"source": "internal/cli/schema_hints/selection/drive.json",
"value": "上传本地文件到钉盘或文档空间,或按节点 ID 确认覆盖已有文件"
@@ -27276,7 +27277,7 @@
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
"source": "internal/cli/schema_hints/selection/drive.json",
"value": "上传本地文件到钉盘或文档空间,或按节点 ID 确认覆盖已有文件"
},
@@ -27300,13 +27301,14 @@
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
"selected": true,
"source": "internal/cli/schema_hints/selection/drive.json",
"value": [
"用户要把文件发送到群聊或单聊时使用 chat message send --msg-type file --file-path;drive upload 不会发送聊天消息",
"常规场景不要拆成 upload-info + 手动 PUT + commit;仅自定义流式上传才用三步",
"要把文件作为文档正文附件插入改用 dws doc media insert",
"Word/Excel/Markdown 要求上传后在线编辑、大家直接在线改或转钉钉文档时改用 dws doc import;普通 drive upload 返回 docx/xlsx 文件不能宣称可在线编辑",
"用户明确说文档空间且走 doc 兼容入口时可用 dws doc upload,默认仍推荐本命令",
"用户没有明确同意替换目标文件时不要使用 --node;新建上传应使用 --folder 或目标根目录"
]
@@ -27314,12 +27316,13 @@
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
"source": "internal/cli/schema_hints/selection/drive.json",
"value": [
"用户要把文件发送到群聊或单聊时使用 chat message send --msg-type file --file-path;drive upload 不会发送聊天消息",
"常规场景不要拆成 upload-info + 手动 PUT + commit;仅自定义流式上传才用三步",
"要把文件作为文档正文附件插入改用 dws doc media insert",
"Word/Excel/Markdown 要求上传后在线编辑、大家直接在线改或转钉钉文档时改用 dws doc import;普通 drive upload 返回 docx/xlsx 文件不能宣称可在线编辑",
"用户明确说文档空间且走 doc 兼容入口时可用 dws doc upload,默认仍推荐本命令",
"用户没有明确同意替换目标文件时不要使用 --node;新建上传应使用 --folder 或目标根目录"
]
@@ -27432,7 +27435,7 @@
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
"selected": true,
"source": "internal/cli/schema_hints/selection/drive.json",
"value": [
@@ -27443,7 +27446,7 @@
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
"source": "internal/cli/schema_hints/selection/drive.json",
"value": [
"dws drive upload --file ./report.pdf --format json",
@@ -27531,7 +27534,7 @@
},
{
"precedence": "reviewed_explicit",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
"selected": false,
"source": "internal/cli/schema_hints/selection/drive.json",
"value": true
@@ -27583,25 +27586,25 @@
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
"selected": true,
"source": "internal/cli/schema_hints/selection/drive.json",
"value": [
"用户要把本地文件上传到钉盘/我的文件(首选一条命令自动完成凭证+PUT+提交)时",
"用户明确要求只上传或暂存附件、暂时不要发送到任何聊天会话时",
"上传到知识库/文档空间时加 --workspace;需要转在线文档时加 --convert",
"用户只要保留原始文件供存储或下载时使用;上传到知识库/文档空间时可加 --workspace",
"用户明确要求用本地文件替换已有钉盘/文档空间文件时传 --node;该模式会覆盖远端内容并要求确认"
]
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
"source": "internal/cli/schema_hints/selection/drive.json",
"value": [
"用户要把本地文件上传到钉盘/我的文件(首选一条命令自动完成凭证+PUT+提交)时",
"用户明确要求只上传或暂存附件、暂时不要发送到任何聊天会话时",
"上传到知识库/文档空间时加 --workspace;需要转在线文档时加 --convert",
"用户只要保留原始文件供存储或下载时使用;上传到知识库/文档空间时可加 --workspace",
"用户明确要求用本地文件替换已有钉盘/文档空间文件时传 --node;该模式会覆盖远端内容并要求确认"
]
}
@@ -28325,7 +28328,7 @@
"use_when": [
"用户要把本地文件上传到钉盘/我的文件(首选一条命令自动完成凭证+PUT+提交)时",
"用户明确要求只上传或暂存附件、暂时不要发送到任何聊天会话时",
"上传到知识库/文档空间时加 --workspace;需要转在线文档时加 --convert",
"用户只要保留原始文件供存储或下载时使用;上传到知识库/文档空间时可加 --workspace",
"用户明确要求用本地文件替换已有钉盘/文档空间文件时传 --node;该模式会覆盖远端内容并要求确认"
]
}
+32 -25
View File
@@ -84,20 +84,20 @@
"doc.create_document": {
"agent_summary": "在默认根目录、文档文件夹或知识库根创建带可选初始内容的 adoc",
"use_when": [
"用户要新建文字在线文档(adoc),可空文档或用 --content-file 写入 Markdown/JSONML 时",
"已知目标文档文件夹 --folder 或知识库 --workspace;长 Markdown 仍直接使用本命令,由原生写入管道自动分片"
"用户要新建文字在线文档(adoc),可空文档或用 --content-file 写入明确要求的初始 Markdown/JSONML;用户显式要求正文 H1 时必须保留,不能用 --name 替代",
"已知目标文档文件夹 --folder 或知识库 --workspace;长 Markdown 仍直接使用本命令,由原生写入管道自动分片;返回 nodeId 绑定同一请求的后续 list/insert/export,后续可观察操作仍须分别执行"
],
"avoid_when": [
"创建表格/脑图/白板/多维表/演示改用 dws wiki node create --type <type>(勿用 doc create)",
"只管理知识库空节点或层级时用 wiki node create;本命令侧重创建并写入 adoc",
"导入本地 Word/Markdown 并保留服务端转换语义时用 dws doc import;不要用自写分片脚本替代原生 create"
"在知识库建空节点实体也可用 wiki node create;本命令侧重可写初始内容的 adoc",
"导入本地 Word/Markdown 为在线文档用 dws doc import,并显式提供 --folder 或 --workspace;不要用已迁移的 doc upload --convert"
],
"examples": [
"dws doc create --name \"项目周报\" --format json",
"dws doc create --name \"Q1 总结\" --content-file ./q1.md --workspace <WORKSPACE_ID> --format json"
],
"reviewed": true,
"review_reason": "人工对齐当前 Cobra 原生内容写入管道、doc Skill 渐进路由与 workspace/folder 边界;强调长内容无需外部分片,不改变命令身份、参数或安全事实。",
"review_reason": "人工对齐当前 Cobra 原生内容写入管道、显式正文 H1、同请求新 nodeId 绑定和可观察块操作边界;强调长内容无需外部分片,不改变命令身份、参数或安全事实。",
"source_refs": [
"CommandRegistry:canonical_path=doc.create_document",
"cobra-help:dws doc create",
@@ -205,7 +205,7 @@
"用户明确要求用 doc delete 兼容入口将文档/文件移入回收站,且已确认目标时"
],
"avoid_when": [
"常规文件删除优先 dws drive delete;本入口仅为兼容",
"常规文件删除使用 dws drive delete;不要继续选择已弃用的 doc delete 兼容入口",
"用户未确认或目标不清时不要删",
"删块用 doc block delete;删评论用 doc comment delete"
],
@@ -250,10 +250,11 @@
"doc.download_doc_attachment": {
"agent_summary": "获取文档附件的临时下载链接",
"use_when": [
"获取文档正文中附件的临时下载 URL(resourceId 来自 block list attachment)时"
"获取文档正文中附件的临时下载 URL 时;resourceId 必须来自本次 block list 返回的 attachment 块"
],
"avoid_when": [
"下载钉盘普通文件用 drive download;导出在线文档用 doc export"
"下载钉盘普通文件用 drive download;导出在线文档用 doc export",
"不要把 blockId/nodeId 当作 resourceId,也不要编造 --output:本命令返回临时 downloadUrl"
],
"examples": [
"dws doc media download --node <DOC_ID> --resource-id <RESOURCE_ID> --format json"
@@ -351,10 +352,10 @@
"doc.import_get": {
"agent_summary": "根据 taskId 查询文档导入任务的执行结果",
"use_when": [
"查询文档导入任务结果(已有 taskId,导入超时/中断后兜底)时"
"仅在 doc import 已返回真实 taskId 且自动轮询超时/中断后,查询导入任务结果时"
],
"avoid_when": [
"发起导入用 doc import(若入口可用);不要用本命令代替导入"
"发起导入用 doc import;不要先调用 import get,也不要使用示例占位 taskId"
],
"examples": [
"dws doc import get --task-id <TASK_ID> --format json"
@@ -372,11 +373,14 @@
"doc.insert_document_block": {
"agent_summary": "向文档插入块元素",
"use_when": [
"在文档中插入新块(段落/标题等);简单场景用 --text/--heading,复杂块用 --element JSON"
"用户明确要求在文档中插入新块(段落/标题/列表等)时;简单段落/标题用 --text/--heading,复杂块用 --element JSON",
"用户要求有序列表块时使用 JSONML p.list.isOrdered=true(同一 listId)或等价原生 orderedList 结构,不能用普通 Markdown 代替",
"相对插入时先 block list 获取真实 blockId,再同时传 --ref-block 与 --where before|after"
],
"avoid_when": [
"整篇追加 Markdown 优先 doc update --mode append",
"插入本地文件附件优先 doc media insert",
"仅在用户没有指定块操作、只要整篇追加 Markdown 时优先 doc update --mode append;显式 insert/标题块/列表块不能折叠",
"插入本地文件附件优先 doc media insert;不要为普通附件手写 attachment JSON",
"整段 Markdown 或长内容优先 doc update --mode append,不要拆成大量 block insert",
"删块用 block delete;改已有块用 block update"
],
"examples": [
@@ -384,7 +388,7 @@
"dws doc block insert --node <DOC_ID> --heading \"二级标题\" --level 2 --format json"
],
"reviewed": true,
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐显式 block insert 的可观察操作语义和有序列表原生结构;不改变命令身份、参数契约、接口绑定或 Runtime 门禁。",
"source_refs": [
"CommandRegistry:canonical_path=doc.insert_document_block",
"cobra-help:dws doc block insert",
@@ -421,7 +425,7 @@
"doc.list_document_blocks": {
"agent_summary": "查询文档一级块元素列表",
"use_when": [
"查看文档一级块结构、拿 blockId,供 insert/update/delete 或划词评论定位时"
"查看文档一级块结构、拿 blockId,供 insert/update/delete 或划词评论定位时;用户在显式工作流中点名 list 时必须真实执行并使用返回结果"
],
"avoid_when": [
"只要全文 Markdown 用 doc read",
@@ -432,7 +436,7 @@
"dws doc block list --node <DOC_ID> --start-index 0 --end-index 5 --format json"
],
"reviewed": true,
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐 block list 的显式可观察步骤和真实 blockId 上下文传递;不改变命令身份、参数契约或接口绑定。",
"source_refs": [
"CommandRegistry:canonical_path=doc.list_document_blocks",
"cobra-help:dws doc block list",
@@ -607,10 +611,11 @@
"doc.reply_comment": {
"agent_summary": "回复文档评论",
"use_when": [
"回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 来自 list/create"
"回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 必须来自本次 list/create 返回"
],
"avoid_when": [
"新建评论用 create/create-inline;删评论用 delete"
"新建评论用 create/create-inline;删评论用 delete",
"不要把 commentId、blockId 或示例占位符传给 --comment-key"
],
"examples": [
"dws doc comment reply --node <DOC_ID> --comment-key <COMMENT_KEY> --content \"同意\" --mentioned-open-conversation-id <openConversationId> --format json",
@@ -744,13 +749,14 @@
"use_when": [
"用户要向已有 adoc 追加长、多行或文件内容时用 --mode append + --content-file;CLI 自动分片",
"用户明确要求整篇覆盖替换时用 --mode overwrite(破坏性)",
"append 且要插到第 N 个 block 前时加 --index N(先 block list)"
"append 且要插到第 N 个 block 前时加 --index N(先 block list)",
"长文本、多行内容或表格优先写入 UTF-8 文件并使用 --content-file,避免 shell 转义损坏"
],
"avoid_when": [
"只在末尾补一小段纯文本优先 +doc-append;只改一个块用 doc block update",
"目标不是 adoc 时按 drive info 的 extension 切对应产品",
"覆盖模式用户未确认前不要执行;可先 --dry-run 预览",
"创建新文档用 doc create;不要预先手工分片或循环重试覆盖"
"目标不是 adoc 或只要改单个块时改用 doc block update",
"覆盖模式用户未确认前不要执行;先 --dry-run 预览,确认后再加 --yes",
"--content 与 --content-file 二选一;--index 仅用于 mode=append",
"创建新文档用 doc create,不要用 update 冒充创建"
],
"examples": [
"dws doc update --node <DOC_ID> --content \"# 追加内容\" --mode append --format json",
@@ -771,11 +777,12 @@
"doc.update_document_block": {
"agent_summary": "更新文档中的指定块",
"use_when": [
"修改已有块的文本/标题/样式(已知 blockId)时"
"修改已有块的文本/标题/样式时;blockId 必须来自目标文档本次 block list 结果"
],
"avoid_when": [
"插入新块用 block insert;删除用 block delete",
"改文档显示名用 rename;整篇覆盖用 update overwrite"
"改文档显示名用 rename;整篇覆盖用 update overwrite",
"不要跨文档复用 blockId,也不要把 nodeId/commentKey 当作 blockId"
],
"examples": [
"dws doc block update --node <DOC_ID> --block-id <BLOCK_ID> --text \"新内容\" --format json"
@@ -911,13 +911,14 @@
"use_when": [
"用户要把本地文件上传到钉盘/我的文件(首选一条命令自动完成凭证+PUT+提交)时",
"用户明确要求只上传或暂存附件、暂时不要发送到任何聊天会话时",
"上传到知识库/文档空间时加 --workspace;需要转在线文档时加 --convert",
"用户只要保留原始文件供存储或下载时使用;上传到知识库/文档空间时可加 --workspace",
"用户明确要求用本地文件替换已有钉盘/文档空间文件时传 --node;该模式会覆盖远端内容并要求确认"
],
"avoid_when": [
"用户要把文件发送到群聊或单聊时使用 chat message send --msg-type file --file-path;drive upload 不会发送聊天消息",
"常规场景不要拆成 upload-info + 手动 PUT + commit;仅自定义流式上传才用三步",
"要把文件作为文档正文附件插入改用 dws doc media insert",
"Word/Excel/Markdown 要求上传后在线编辑、大家直接在线改或转钉钉文档时改用 dws doc import;普通 drive upload 返回 docx/xlsx 文件不能宣称可在线编辑",
"用户明确说文档空间且走 doc 兼容入口时可用 dws doc upload,默认仍推荐本命令",
"用户没有明确同意替换目标文件时不要使用 --node;新建上传应使用 --folder 或目标根目录"
],
@@ -926,7 +927,7 @@
"dws drive upload --file ./README.md --node <dentryUuid> --format json"
],
"reviewed": true,
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
"source_refs": [
"CommandRegistry:canonical_path=drive.upload",
"cobra-help:dws drive upload",
+18
View File
@@ -363,6 +363,24 @@ func TestSuggestBusinessHintChatRecovery(t *testing.T) {
}
}
func TestSuggestBusinessHintDocRecovery(t *testing.T) {
cases := []struct{ message, want string }{
{"nodeId not found", "drive search"},
{"blockId 不存在", "block list"},
{"commentKey invalid", "comment list/create"},
{"resourceId not found", "attachment"},
{"workspaceId invalid", "wiki space list"},
{"folderId invalid", "dentryId"},
{"templateId not found", "template search"},
{"version 不存在", "version list"},
}
for _, tc := range cases {
if got := SuggestBusinessHint(map[string]any{"message": tc.message}); !strings.Contains(got, tc.want) {
t.Errorf("SuggestBusinessHint(%q) = %q, want containing %q", tc.message, got, tc.want)
}
}
}
func TestCrossPlatformCoveragePATSerializationAndPolicyEdges(t *testing.T) {
oldHost := hostControlProvider
oldBrowser := patBrowserProvider
+16
View File
@@ -340,6 +340,22 @@ func suggestForBusinessErrorText(body map[string]any) string {
switch {
case strings.Contains(msg, "搜索内容不能为空"):
return "请提供非空搜索关键词: dws doc search --query \"关键词\""
case strings.Contains(msg, "nodeId") && (strings.Contains(msg, "not found") || strings.Contains(msg, "不存在")):
return "目标文档 nodeId 不存在或当前账号不可见。请用 dws drive search 或 dws wiki node search 重新获取真实 nodeId;不要复用示例占位符。"
case strings.Contains(msg, "blockId") && (strings.Contains(msg, "not found") || strings.Contains(msg, "不存在")):
return "目标 blockId 不存在或已变化。请重新执行 dws doc block list --node <DOC_ID> --format json,并从本次结果取 blockId。"
case strings.Contains(msg, "commentKey") && (strings.Contains(msg, "invalid") || strings.Contains(msg, "不存在") || strings.Contains(msg, "not found")):
return "commentKey 无效或评论已不存在。请从 dws doc comment list/create 的当前返回结果中提取 commentKey,不要使用 commentId 或占位符。"
case strings.Contains(msg, "resourceId") && (strings.Contains(msg, "invalid") || strings.Contains(msg, "不存在") || strings.Contains(msg, "not found")):
return "resourceId 无效。请用 dws doc block list 查找 attachment 块并读取真实 resourceId;不要把 blockId 当作 resourceId。"
case strings.Contains(msg, "workspaceId") && (strings.Contains(msg, "invalid") || strings.Contains(msg, "不存在") || strings.Contains(msg, "not found")):
return "workspaceId 无效或无权访问。请用 dws wiki space list --type myWikiSpace --format json 获取当前账号可用的 workspaceId。"
case strings.Contains(msg, "folderId") && (strings.Contains(msg, "invalid") || strings.Contains(msg, "不存在") || strings.Contains(msg, "not found")):
return "folderId 无效。doc 的 --folder 需要文档文件夹 nodeId/URL,不是 drive 的纯数字 dentryId;请重新列出目标空间节点。"
case strings.Contains(msg, "templateId") && (strings.Contains(msg, "invalid") || strings.Contains(msg, "不存在") || strings.Contains(msg, "not found")):
return "templateId 无效或模板不可见。请先用 dws doc template search --query <关键词> --format json 获取当前账号可用的真实 templateId。"
case strings.Contains(msg, "version") && (strings.Contains(msg, "不存在") || strings.Contains(msg, "not found")):
return "目标版本不存在。请先执行 dws doc version list --node <DOC_ID> --format json,并从当前版本列表选择可回滚版本。"
case strings.Contains(msg, "User has no permission to access this email"):
return "请确认邮箱地址正确,查看可用邮箱: dws mail mailbox list"
case strings.Contains(msg, "频率超限") || strings.Contains(msg, "rate limit"):
+177 -2
View File
@@ -9,6 +9,7 @@ import (
"strconv"
"strings"
"time"
"unicode/utf8"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
"github.com/spf13/cobra"
@@ -132,6 +133,8 @@ func recordQueryFetchAll(toolArgs map[string]any, pageLimit int) error {
var allRecords []any
page := 0
lastCursor := ""
seenCursors := map[string]struct{}{}
stopReason := ""
for {
page++
@@ -201,6 +204,19 @@ func recordQueryFetchAll(toolArgs map[string]any, pageLimit int) error {
lastCursor = ""
break
}
if current, _ := toolArgs["cursor"].(string); cursor == current {
lastCursor = cursor
stopReason = "cursor_not_advanced"
fmt.Fprintf(os.Stderr, "[pagination] cursor did not advance (%q), stopping to avoid an infinite loop. Resume with --cursor %q after checking the service response.\n", cursor, cursor)
break
}
if _, exists := seenCursors[cursor]; exists {
lastCursor = cursor
stopReason = "cursor_cycle_detected"
fmt.Fprintf(os.Stderr, "[pagination] cursor cycle detected (%q), stopping to avoid an infinite loop. Resume with --cursor %q after checking the service response.\n", cursor, cursor)
break
}
seenCursors[cursor] = struct{}{}
// Check page limit (0 = unlimited)
if pageLimit > 0 && page >= pageLimit {
@@ -223,6 +239,10 @@ func recordQueryFetchAll(toolArgs map[string]any, pageLimit int) error {
if lastCursor != "" {
mergedData["cursor"] = lastCursor
mergedData["hasMore"] = true
if stopReason != "" {
mergedData["incomplete"] = true
mergedData["stopReason"] = stopReason
}
} else {
mergedData["hasMore"] = false
}
@@ -781,6 +801,20 @@ func isAitableRetryableError(err error) bool {
}
msg := strings.ToLower(err.Error())
// 确定性业务错误优先于外层 category/retryable 标记,避免对不存在的资源、
// 参数错误或权限错误做无意义重试。部分 MCP 响应会同时携带
// "category: internal" 或 "retryable: true",因此必须先判定终止类错误。
nonRetryablePatterns := []string{
"input_error", "user_error", "auth_error", "permission denied",
"invalid parameter", "invalid argument", "bad request",
"not found", "no record", "does not exist", "不存在",
}
for _, p := range nonRetryablePatterns {
if strings.Contains(msg, p) {
return false
}
}
// 网络瞬态错误
retryablePatterns := []string{
"timeout", "deadline exceeded", "connection reset",
@@ -814,17 +848,154 @@ func isAitableRetryableError(err error) bool {
func parseFieldsJSON(raw string) ([]any, error) {
var fields []any
if err := json.Unmarshal([]byte(raw), &fields); err == nil {
return fields, nil
return validateAitableCreateFields(fields)
}
var wrapper map[string]any
if err := json.Unmarshal([]byte(raw), &wrapper); err == nil {
if arr, ok := wrapper["fields"].([]any); ok {
return arr, nil
return validateAitableCreateFields(arr)
}
}
return nil, fmt.Errorf("--fields JSON parse failed: expect a JSON array [...]\n hint: example: '[{\"fieldName\":\"名称\",\"type\":\"text\"}]'")
}
// validateAitableCreateFields catches stable field-contract mistakes before MCP dispatch
// and returns a directly actionable correction. It intentionally does not normalize aliases:
// silently changing a field type can alter business semantics.
func validateAitableCreateFields(fields []any) ([]any, error) {
if len(fields) > 15 {
return nil, fmt.Errorf("--fields contains %d items; AI Table accepts at most 15 fields per request\n hint: split the fields into batches of 15 or fewer", len(fields))
}
for i, rawField := range fields {
field, ok := rawField.(map[string]any)
if !ok {
return nil, fmt.Errorf("--fields[%d] must be a JSON object\n hint: example: {\"fieldName\":\"状态\",\"type\":\"singleSelect\"}", i)
}
if _, hasAlias := field["fieldType"]; hasAlias {
return nil, fmt.Errorf("--fields[%d] uses unsupported key fieldType\n hint: use fieldName + type, for example {\"fieldName\":\"状态\",\"type\":\"singleSelect\"}", i)
}
fieldName, nameOK := field["fieldName"].(string)
if !nameOK || strings.TrimSpace(fieldName) == "" {
return nil, fmt.Errorf("--fields[%d].fieldName must be a non-empty string\n hint: example: {\"fieldName\":\"任务名称\",\"type\":\"text\"}", i)
}
if strings.ContainsAny(fieldName, "\r\n") {
return nil, fmt.Errorf("--fields[%d].fieldName must not contain line breaks\n hint: use a single-line field name with at most 100 characters", i)
}
if utf8.RuneCountInString(fieldName) > 100 {
return nil, fmt.Errorf("--fields[%d].fieldName is %d characters; maximum is 100\n hint: shorten the field name before retrying", i, utf8.RuneCountInString(fieldName))
}
fieldType, typeOK := field["type"].(string)
fieldType = strings.TrimSpace(fieldType)
if !typeOK || fieldType == "" {
return nil, fmt.Errorf("--fields[%d].type must be a non-empty string\n hint: common types: text, number, singleSelect, multipleSelect, date, user, attachment", i)
}
if strings.EqualFold(strings.TrimSpace(fieldType), "select") {
return nil, fmt.Errorf("--fields[%d].type %q is not a valid AI Table field type\n hint: use \"singleSelect\" for single choice or \"multipleSelect\" for multiple choice; do not use \"select\"", i, fieldType)
}
if !aitableFieldTypes[fieldType] {
return nil, fmt.Errorf("--fields[%d].type %q is unsupported\n hint: use a documented type such as text, number, singleSelect, multipleSelect, date, user, attachment, url, or richText", i, fieldType)
}
if fieldType == "primaryDoc" && i != 0 {
return nil, fmt.Errorf("--fields[%d] uses primaryDoc outside the first column\n hint: primaryDoc is allowed only as --fields[0]", i)
}
config, hasConfig := field["config"]
var configMap map[string]any
if hasConfig {
var configOK bool
configMap, configOK = config.(map[string]any)
if !configOK {
return nil, fmt.Errorf("--fields[%d].config must be a JSON object\n hint: example: {\"options\":[{\"name\":\"高\"},{\"name\":\"低\"}]}", i)
}
}
if err := validateAitableFieldConfig(i, fieldType, configMap); err != nil {
return nil, err
}
}
return fields, nil
}
var aitableFieldTypes = map[string]bool{
"text": true, "number": true, "singleSelect": true, "multipleSelect": true, "date": true,
"currency": true, "user": true, "department": true, "group": true, "progress": true,
"rating": true, "checkbox": true, "attachment": true, "url": true, "richText": true,
"telephone": true, "email": true, "idCard": true, "barcode": true, "geolocation": true,
"address": true, "primaryDoc": true, "formula": true, "filterUp": true, "lookup": true,
"unidirectionalLink": true, "bidirectionalLink": true, "creator": true, "lastModifier": true,
"createdTime": true, "lastModifiedTime": true,
}
func validateAitableFieldConfig(index int, fieldType string, config map[string]any) error {
if fieldType == "singleSelect" || fieldType == "multipleSelect" {
if config == nil {
return fmt.Errorf("--fields[%d].config.options is required for %s\n hint: use {\"options\":[{\"name\":\"选项A\"},{\"name\":\"选项B\"}]}", index, fieldType)
}
options, ok := config["options"].([]any)
if !ok || len(options) == 0 {
return fmt.Errorf("--fields[%d].config.options must be a non-empty JSON array\n hint: example: {\"options\":[{\"name\":\"高\"},{\"name\":\"低\"}]}", index)
}
for optionIndex, rawOption := range options {
option, ok := rawOption.(map[string]any)
if !ok {
return fmt.Errorf("--fields[%d].config.options[%d] must be a JSON object\n hint: each option must look like {\"name\":\"选项名\"}", index, optionIndex)
}
name, ok := option["name"].(string)
if !ok || strings.TrimSpace(name) == "" {
return fmt.Errorf("--fields[%d].config.options[%d].name must be a non-empty string\n hint: example: {\"name\":\"高\"}", index, optionIndex)
}
}
}
if config == nil {
if fieldType == "formula" {
return fmt.Errorf("--fields[%d].config.formula is required for formula\n hint: example: {\"formula\":\"[单价] * [数量]\"}", index)
}
if fieldType == "unidirectionalLink" || fieldType == "bidirectionalLink" {
return fmt.Errorf("--fields[%d].config.linkedTableId is required for %s\n hint: get the target table ID first, then use {\"linkedTableId\":\"<TABLE_ID>\",\"multiple\":true}", index, fieldType)
}
return nil
}
if formatter, ok := config["formatter"].(string); ok && formatter != "" {
allowed := map[string]map[string]bool{
"number": {"INT": true, "FLOAT_1": true, "FLOAT_2": true, "FLOAT_3": true, "FLOAT_4": true, "THOUSAND": true, "THOUSAND_FLOAT": true, "PERCENT": true, "PERCENT_FLOAT": true},
"date": {"YYYY-MM-DD": true, "YYYY-MM-DD HH:mm": true, "YYYY-MM-DD HH:mm:ss": true, "YYYY/MM/DD": true, "YYYY/MM/DD HH:mm": true},
"currency": {"INT": true, "FLOAT_1": true, "FLOAT_2": true, "FLOAT_3": true, "FLOAT_4": true},
"progress": {"PERCENT": true},
}
if typeAllowed, applies := allowed[fieldType]; applies && !typeAllowed[formatter] {
return fmt.Errorf("--fields[%d].config.formatter %q is invalid for %s\n hint: run 'dws aitable table create --help' and use a formatter listed for %s", index, formatter, fieldType, fieldType)
}
}
if fieldType == "currency" {
currency, _ := config["currencyType"].(string)
currencies := map[string]bool{"CNY": true, "HKD": true, "USD": true, "EUR": true, "GBP": true, "MOP": true, "VND": true, "JPY": true, "KRW": true, "AED": true, "AUD": true, "BRL": true, "CAD": true, "CHF": true, "INR": true, "IDR": true, "MXN": true, "MYR": true, "PHP": true, "PLN": true, "RUB": true, "SGD": true, "THB": true, "TRY": true, "TWD": true}
if currency != "" && !currencies[currency] {
return fmt.Errorf("--fields[%d].config.currencyType %q is unsupported\n hint: use an ISO currency from table create --help, for example CNY, USD, EUR, JPY, or HKD", index, currency)
}
}
if fieldType == "rating" {
if max, ok := config["max"].(float64); ok && (max < 1 || max > 10) {
return fmt.Errorf("--fields[%d].config.max must be between 1 and 10 for rating\n hint: example: {\"min\":1,\"max\":5,\"icon\":\"star\"}", index)
}
}
if multiple, exists := config["multiple"]; exists {
if _, ok := multiple.(bool); !ok {
return fmt.Errorf("--fields[%d].config.multiple must be true or false\n hint: use a JSON boolean without quotes, for example {\"multiple\":false}", index)
}
}
if fieldType == "formula" {
formula, _ := config["formula"].(string)
if strings.TrimSpace(formula) == "" {
return fmt.Errorf("--fields[%d].config.formula is required for formula\n hint: example: {\"formula\":\"[单价] * [数量]\"}", index)
}
}
if fieldType == "unidirectionalLink" || fieldType == "bidirectionalLink" {
linkedTableID, _ := config["linkedTableId"].(string)
if strings.TrimSpace(linkedTableID) == "" {
return fmt.Errorf("--fields[%d].config.linkedTableId is required for %s\n hint: get the target table ID first, then use {\"linkedTableId\":\"<TABLE_ID>\",\"multiple\":true}", index, fieldType)
}
}
return nil
}
// resolveFormUpdateTitle 折叠 form update 的 --title / --name 别名为单个 title 值。
// 优先 --title,未设置时回退到 --name;两者都未传返回 ""。
// 抽出独立函数便于单测覆盖。
@@ -1370,6 +1541,10 @@ config 结构参考:
} else {
return fmt.Errorf("must specify either --fields OR both --name and --type")
}
fields, err := validateAitableCreateFields(fields)
if err != nil {
return err
}
baseID, err := mustFlagOrFallback(cmd, "base-id", "base")
if err != nil {
@@ -88,6 +88,87 @@ func TestCrossPlatformCoverageAitableRetryWrappersExhaustAndRecover(t *testing.T
}
}
func TestAitableTableCreateInvalidFieldContractDoesNotDispatchMCP(t *testing.T) {
oldDeps, oldArgs := deps, os.Args
t.Cleanup(func() { deps, os.Args = oldDeps, oldArgs })
longName := strings.Repeat("字", 101)
manyFields := make([]string, 16)
for i := range manyFields {
manyFields[i] = fmt.Sprintf(`{"fieldName":"F%d","type":"text"}`, i)
}
cases := []struct {
name string
fields string
hint string
}{
{"field must be object", `[1]`, "must be a JSON object"},
{"fieldType key", `[{"fieldName":"状态","fieldType":"singleSelect"}]`, "fieldName + type"},
{"missing fieldName", `[{"type":"text"}]`, "fieldName must be"},
{"empty fieldName", `[{"fieldName":" ","type":"text"}]`, "fieldName must be"},
{"fieldName line break", `[{"fieldName":"任务\n名称","type":"text"}]`, "must not contain line breaks"},
{"fieldName too long", fmt.Sprintf(`[{"fieldName":%q,"type":"text"}]`, longName), "maximum is 100"},
{"missing type", `[{"fieldName":"任务"}]`, "type must be"},
{"unknown type", `[{"fieldName":"任务","type":"string"}]`, "type \"string\" is unsupported"},
{"select alias", `[{"fieldName":"状态","type":"select"}]`, "singleSelect"},
{"too many fields", `[` + strings.Join(manyFields, ",") + `]`, "at most 15"},
{"config scalar", `[{"fieldName":"任务","type":"text","config":"bad"}]`, "config must be a JSON object"},
{"single select missing options", `[{"fieldName":"状态","type":"singleSelect"}]`, "config.options is required"},
{"select options not array", `[{"fieldName":"状态","type":"singleSelect","config":{"options":{}}}]`, "non-empty JSON array"},
{"select options empty", `[{"fieldName":"状态","type":"singleSelect","config":{"options":[]}}]`, "non-empty JSON array"},
{"select option scalar", `[{"fieldName":"状态","type":"singleSelect","config":{"options":["高"]}}]`, "must be a JSON object"},
{"select option empty name", `[{"fieldName":"状态","type":"singleSelect","config":{"options":[{"name":""}]}}]`, "name must be a non-empty string"},
{"number formatter", `[{"fieldName":"金额","type":"number","config":{"formatter":"CURRENCY_YUAN"}}]`, "invalid for number"},
{"date formatter", `[{"fieldName":"日期","type":"date","config":{"formatter":"MM-DD-YYYY"}}]`, "invalid for date"},
{"currency formatter", `[{"fieldName":"金额","type":"currency","config":{"formatter":"THOUSAND"}}]`, "invalid for currency"},
{"currency type", `[{"fieldName":"金额","type":"currency","config":{"currencyType":"RMB"}}]`, "currencyType \"RMB\" is unsupported"},
{"progress formatter", `[{"fieldName":"进度","type":"progress","config":{"formatter":"PERCENT_FLOAT"}}]`, "invalid for progress"},
{"rating max", `[{"fieldName":"评分","type":"rating","config":{"max":11}}]`, "between 1 and 10"},
{"multiple must be boolean", `[{"fieldName":"负责人","type":"user","config":{"multiple":"false"}}]`, "must be true or false"},
{"formula missing formula", `[{"fieldName":"总价","type":"formula","config":{}}]`, "config.formula is required"},
{"link missing table", `[{"fieldName":"关联","type":"unidirectionalLink","config":{"multiple":true}}]`, "linkedTableId is required"},
{"primary doc not first", `[{"fieldName":"名称","type":"text"},{"fieldName":"文档","type":"primaryDoc"}]`, "only as --fields[0]"},
}
for _, test := range cases {
t.Run(test.name, func(t *testing.T) {
for _, args := range [][]string{
{"table", "create", "--base-id=b", "--name=n", "--fields=" + test.fields},
{"field", "create", "--base-id=b", "--table-id=t", "--fields=" + test.fields},
} {
caller := &aitableTestCaller{}
err := runAitableCoverageCommand(t, caller, args...)
if err == nil || !strings.Contains(err.Error(), test.hint) {
t.Fatalf("invalid fields error = %v, want hint %q", err, test.hint)
}
if len(caller.calls) != 0 {
t.Fatalf("invalid fields dispatched %d MCP call(s), want zero", len(caller.calls))
}
}
})
}
for _, test := range []struct {
name string
args []string
hint string
}{
{"typed unknown type", []string{"field", "create", "--base-id=b", "--table-id=t", "--name=字段", "--type=string"}, "type \"string\" is unsupported"},
{"typed select alias", []string{"field", "create", "--base-id=b", "--table-id=t", "--name=状态", "--type=select"}, "singleSelect"},
{"typed select no options", []string{"field", "create", "--base-id=b", "--table-id=t", "--name=状态", "--type=singleSelect"}, "config.options is required"},
{"typed invalid formatter", []string{"field", "create", "--base-id=b", "--table-id=t", "--name=金额", "--type=number", `--config={"formatter":"CURRENCY_YUAN"}`}, "invalid for number"},
} {
t.Run(test.name, func(t *testing.T) {
caller := &aitableTestCaller{}
err := runAitableCoverageCommand(t, caller, test.args...)
if err == nil || !strings.Contains(err.Error(), test.hint) {
t.Fatalf("typed field error = %v, want hint %q", err, test.hint)
}
if len(caller.calls) != 0 {
t.Fatalf("typed invalid field dispatched %d MCP call(s), want zero", len(caller.calls))
}
})
}
}
func TestCrossPlatformCoverageAitableCommandValidationEdges(t *testing.T) {
oldDeps, oldArgs, oldStdin, oldSleep := deps, os.Args, os.Stdin, helperSleep
t.Cleanup(func() {
+24 -3
View File
@@ -180,7 +180,9 @@ func TestCrossPlatformCoverageAitableViewConfigAndHelpers(t *testing.T) {
for _, tc := range []struct {
err error
want bool
}{{nil, false}, {errors.New("timeout"), true}, {errors.New("SYSTEM_ERROR"), true}, {errors.New("retryable: true"), true}, {errors.New("bad request"), false}} {
}{{nil, false}, {errors.New("timeout"), true}, {errors.New("SYSTEM_ERROR"), true}, {errors.New("retryable: true"), true}, {errors.New("bad request"), false},
{errors.New(`category: internal_error, type: INPUT_ERROR, retryable: true, message: no record`), false},
{errors.New(`category: internal_error, retryable: true, message: resource not found`), false}} {
if got := isAitableRetryableError(tc.err); got != tc.want {
t.Errorf("isAitableRetryableError(%v) = %v", tc.err, got)
}
@@ -248,15 +250,26 @@ func TestCrossPlatformCoverageAitableViewConfigAndHelpers(t *testing.T) {
t.Fatalf("typed-only update = %#v, %v", merged, err)
}
for _, raw := range []string{`[1]`, `{"fields":[1]}`, `{}`, `{`} {
for _, raw := range []string{`[{"fieldName":"N","type":"text"}]`, `{"fields":[{"fieldName":"N","type":"text"}]}`, `{}`, `{`} {
fields, err := parseFieldsJSON(raw)
if (raw == `[1]` || strings.Contains(raw, "fields")) && (err != nil || len(fields) != 1) {
if (strings.HasPrefix(raw, `[{`) || strings.Contains(raw, "fields")) && (err != nil || len(fields) != 1) {
t.Errorf("parseFieldsJSON(%q) = %#v, %v", raw, fields, err)
}
if (raw == `{}` || raw == `{`) && err == nil {
t.Errorf("parseFieldsJSON(%q) should fail", raw)
}
}
for _, tc := range []struct {
raw string
hint string
}{
{`[{"fieldName":"状态","type":"select"}]`, "singleSelect"},
{`[{"fieldName":"状态","fieldType":"singleSelect"}]`, "fieldName + type"},
} {
if _, err := parseFieldsJSON(tc.raw); err == nil || !strings.Contains(err.Error(), tc.hint) {
t.Errorf("parseFieldsJSON(%q) error = %v, want hint %q", tc.raw, err, tc.hint)
}
}
}
func TestCrossPlatformCoverageAitableToolResponseAndPaginationHelpers(t *testing.T) {
@@ -312,4 +325,12 @@ func TestCrossPlatformCoverageAitableToolResponseAndPaginationHelpers(t *testing
if err := recordQueryFetchAll(map[string]any{}, 1); err == nil {
t.Fatal("first-page pagination error should fail")
}
caller = &aitableTestCaller{responses: []string{
`{"data":{"records":[{"id":1}],"nextCursor":"same"}}`,
`{"data":{"records":[{"id":2}],"nextCursor":"same"}}`,
}}
out = installAitableDeps(t, caller)
if err := recordQueryFetchAll(map[string]any{}, 0); err != nil || !strings.Contains(out.String(), `"stopReason"`) || !strings.Contains(out.String(), `"cursor_not_advanced"`) {
t.Fatalf("repeated cursor guard = %q, %v", out.String(), err)
}
}
+2 -2
View File
@@ -3776,8 +3776,8 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成
return err
}
iconMediaID := strings.TrimSpace(mustGetFlag(cmd, "icon-media-id"))
if iconMediaID == "" {
return fmt.Errorf("invalid --icon-media-id: mediaId 不能为空\n hint: 请使用上游媒体上传能力返回的有效 mediaId;DWS CLI 不提供本地文件到 mediaId 的上传命令")
if err := ValidateChatMediaID(iconMediaID); err != nil {
return fmt.Errorf("invalid --icon-media-id: %w", err)
}
return callMCPToolOnServer("im", "update_group_icon", map[string]any{
"openConversationId": mustGetFlag(cmd, "group"),
@@ -85,6 +85,22 @@ func TestCrossPlatformCoverageChatGroupUpdateIconRejectsBlankMediaID(t *testing.
}
}
func TestCrossPlatformCoverageChatGroupUpdateIconRejectsLocalPathBeforeMCP(t *testing.T) {
previousDeps, previousArgs := deps, os.Args
os.Args = []string{"dws", "chat"}
t.Cleanup(func() { deps, os.Args = previousDeps, previousArgs })
caller := &productExampleCaller{}
err := runChatCoverageCommand(t, caller,
"group", "update-icon", "--group=cid", "--icon-media-id=./logo.png")
if err == nil {
t.Fatal("update group icon with local path succeeded, want validation error")
}
if caller.calls != 0 {
t.Fatalf("tool calls = %d, want 0", caller.calls)
}
}
func TestCrossPlatformCoverageChatCommandValidationAndSuccessEdges(t *testing.T) {
previousDeps, previousArgs := deps, os.Args
os.Args = []string{"dws", "chat"}
+24 -1
View File
@@ -4,7 +4,12 @@
package helpers
import "context"
import (
"context"
"fmt"
"path/filepath"
"strings"
)
// ConversationLocalFileMeta exposes the already-reviewed native chat upload
// metadata to built-in semantic Shortcuts. It remains an alias so the native
@@ -41,3 +46,21 @@ func BuildConversationFileContent(
) (string, error) {
return buildConversationFileContent(dentryID, spaceID, meta)
}
// ValidateChatMediaID rejects values that are deterministically not an
// uploaded DingTalk mediaId before an MCP request is dispatched.
func ValidateChatMediaID(value string) error {
value = strings.TrimSpace(value)
if value == "" {
return fmt.Errorf("mediaId 不能为空")
}
lower := strings.ToLower(value)
if strings.HasPrefix(lower, "file://") || filepath.IsAbs(value) ||
strings.ContainsAny(value, `/\\`) || strings.HasPrefix(lower, "dentry") {
return fmt.Errorf("%q 是本地文件路径或文件标识,不是 mediaId;请使用可信上游返回的 mediaId,DWS CLI 当前不能把本地图片转换为群头像 mediaId", value)
}
if value[0] != '@' && value[0] != '$' {
return fmt.Errorf("%q 不是有效 mediaId:群头像 mediaId 应以 @ 或 $ 开头;不要传本地路径、dentryId 或 uploadKey", value)
}
return nil
}
+25 -52
View File
@@ -33,6 +33,27 @@ func SetHTTPPutFile(fn func(ctx context.Context, url string, headers map[string]
httpPutFile = fn
}
func callDocCommentUpdate(toolArgs map[string]any) error {
text, err := callMCPToolReturnTextOnServer(context.Background(), "doc-comment", "update_comment", toolArgs)
if err != nil {
return err
}
trimmed := strings.TrimSpace(text)
if trimmed == "" || trimmed == "null" {
return &CLIError{
Code: CodeMCPToolError,
Message: "评论更新接口未返回可验证的更新结果,不能判定为成功",
Suggestion: "请用 dws doc comment list --node <DOC_ID> --format json 回查评论内容;若未变化,保留原 commentKey 并重试",
}
}
var payload any
if json.Unmarshal([]byte(trimmed), &payload) == nil {
return deps.Out.PrintJSON(payload)
}
deps.Out.PrintRaw(text)
return nil
}
func docVersionExists(ctx context.Context, nodeID string, version int) (bool, error) {
// 注意: 不传 maxResults —— 服务端实际接受的上限小于 schema 声明的 1-50,
// 传大值会直接报错 (与悟空实现一致: 默认分页大小 + 游标翻页)。
@@ -1070,9 +1091,6 @@ func newDocCommand() *cobra.Command {
})
}
if md != "" {
if name, ok := toolArgs["name"].(string); ok && name != "" {
md = stripDuplicateTitle(md, name)
}
toolArgs["markdown"] = md
}
if md != "" {
@@ -1652,7 +1670,8 @@ WARNING: --mode overwrite 为破坏性写入,会清空原文档全部内容。
updateCmd.Flags().String("markdown", "", "已弃用,请使用 --content 代替")
_ = updateCmd.Flags().MarkHidden("markdown")
updateCmd.Flags().String("mode", "", "更新模式: overwrite=覆盖, append=追加 (必填)")
_ = updateCmd.MarkFlagRequired("mode")
// Kept out of Cobra's generic required-flag validator so doc local
// preflight can return a structured, actionable error before MCP dispatch.
updateCmd.Flags().Int("index", -1, "插入位置(从 0 开始),仅在 mode=append 时生效。指定将内容插入到文档第几个 block 之前。不传时追加到末尾")
updateCmd.Flags().Bool("yes", false, "确认执行破坏性写入 (仅 --mode overwrite 需要)")
updateCmd.Flags().Bool("dry-run", false, "预览覆盖写入差异,不调用远端 update")
@@ -2046,7 +2065,7 @@ commentKey可从 dws doc comment create 或 dws doc comment list 返回结果中
if err := appendCommentGroupMentions(cmd, toolArgs); err != nil {
return err
}
return callMCPToolOnServer("doc-comment", "update_comment", toolArgs)
return callDocCommentUpdate(toolArgs)
},
}
commentUpdateCmd.Flags().String("node", "", "目标文档的标识,支持传入 URL 或 ID (必填)")
@@ -2902,6 +2921,7 @@ CLI 内部自动完成全部流程:
permissionCmd.Hidden = true
root.AddCommand(searchCmd, listCmd, infoCmd, readCmd, createCmd, updateCmd, uploadCmd, downloadCmd, copyCmd, moveCmd, renameCmd, deleteCmd, fileCmd, folderCmd, blockCmd, commentCmd, mediaCmd, permissionCmd, exportCmd, importCmd, versionCmd, templateCmd, newDocStyleCommand())
attachDocLocalPreflight(root)
return root
}
@@ -3218,53 +3238,6 @@ func pollDocExportJob(ctx context.Context, jobID string) (downloadURL string, er
return "", fmt.Errorf("导出任务超时:已轮询 %d 次仍在处理中 (jobId=%s),请稍后使用 dws doc export get --job-id %s 手动查询", maxPolls, jobID, jobID)
}
// stripDuplicateTitle removes the leading H1 heading from markdown content
// when it matches the document name (set via --name). This prevents the title
// from appearing twice: once as document metadata and once in the body.
func stripDuplicateTitle(markdown, name string) string {
trimmed := strings.TrimLeft(markdown, " \t\n\r")
if !strings.HasPrefix(trimmed, "# ") {
return markdown
}
newlineIdx := strings.Index(trimmed, "\n")
var headingRaw string
if newlineIdx < 0 {
headingRaw = trimmed[2:]
} else {
headingRaw = trimmed[2:newlineIdx]
}
if normalizeHeadingText(headingRaw) != normalizeHeadingText(name) {
return markdown
}
if newlineIdx < 0 {
return ""
}
rest := trimmed[newlineIdx+1:]
rest = strings.TrimLeft(rest, "\n")
return rest
}
// normalizeHeadingText strips trailing ATX hashes, inline markdown formatting
// markers, then returns a lowercased, trimmed string for comparison.
func normalizeHeadingText(s string) string {
s = strings.TrimSpace(s)
if s == "" {
return ""
}
if i := strings.LastIndexByte(s, ' '); i >= 0 {
suffix := s[i+1:]
if len(suffix) > 0 && strings.Trim(suffix, "#") == "" {
s = strings.TrimSpace(s[:i])
}
}
for _, m := range []string{"**", "__", "~~", "*", "_", "`"} {
s = strings.ReplaceAll(s, m, "")
}
return strings.TrimSpace(strings.ToLower(s))
}
// parseCommentMentionIds splits a comma-separated string of user IDs into a slice.
func parseCommentMentionIds(raw string) []string {
parts := strings.Split(raw, ",")
@@ -295,12 +295,6 @@ func TestCrossPlatformCoverageDocCreateUpdateAndBlockCommandEdges(t *testing.T)
})
}
for _, value := range []string{"plain", "# Other\nbody", "# Name", "# **Name** ###\n\nbody"} {
_ = stripDuplicateTitle(value, "Name")
}
for _, value := range []string{"", " Name ### ", "**Bold**", "__Under__ ~~Strike~~ `Code`"} {
_ = normalizeHeadingText(value)
}
for _, name := range []string{"file.pdf", "file.md", "file.unknown"} {
_ = inferMimeType(name)
}
@@ -18,6 +18,7 @@ import (
"io"
"os"
"reflect"
"strings"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
@@ -30,12 +31,29 @@ type docCommentMutationCall struct {
}
type docCommentMutationCaller struct {
calls []docCommentMutationCall
calls []docCommentMutationCall
response string
}
func (c *docCommentMutationCaller) CallTool(_ context.Context, productID, toolName string, args map[string]any) (*edition.ToolResult, error) {
c.calls = append(c.calls, docCommentMutationCall{productID: productID, toolName: toolName, args: args})
return &edition.ToolResult{Content: []edition.ContentBlock{{Type: "text", Text: `{}`}}}, nil
response := c.response
if response == "" {
response = `{}`
}
return &edition.ToolResult{Content: []edition.ContentBlock{{Type: "text", Text: response}}}, nil
}
func TestDocCommentUpdateRejectsNullAcknowledgement(t *testing.T) {
caller := &docCommentMutationCaller{response: `null`}
err := executeDocCommentMutationCommand(t, caller, []string{"dws", "doc"},
"comment", "update", "--node", "doc-1", "--comment-key", "comment-1", "--content", "updated")
if err == nil || !strings.Contains(err.Error(), "未返回可验证的更新结果") {
t.Fatalf("error = %v, want unverifiable update error", err)
}
if len(caller.calls) != 1 {
t.Fatalf("remote calls = %d, want 1", len(caller.calls))
}
}
func (*docCommentMutationCaller) Format() string { return "json" }
@@ -0,0 +1,74 @@
package helpers
import (
"context"
"io"
"os"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
)
type docCreateRecordingCall struct {
tool string
args map[string]any
}
type docCreateRecordingCaller struct {
calls []docCreateRecordingCall
}
func (c *docCreateRecordingCaller) CallTool(_ context.Context, _ string, tool string, args map[string]any) (*edition.ToolResult, error) {
copied := make(map[string]any, len(args))
for key, value := range args {
copied[key] = value
}
c.calls = append(c.calls, docCreateRecordingCall{tool: tool, args: copied})
return textToolResult(`{"nodeId":"node-1","success":true}`), nil
}
func (*docCreateRecordingCaller) Format() string { return "json" }
func (*docCreateRecordingCaller) DryRun() bool { return false }
func (*docCreateRecordingCaller) Fields() string { return "" }
func (*docCreateRecordingCaller) JQ() string { return "" }
func TestDocCreatePreservesExplicitLeadingH1MatchingName(t *testing.T) {
oldArgs := os.Args
os.Args = []string{"dws", "doc"}
t.Cleanup(func() { os.Args = oldArgs })
for _, content := range []string{
"# 需求清单",
"# 需求清单\n\n以上需求已与产品确认",
} {
t.Run(content, func(t *testing.T) {
previous := deps
caller := &docCreateRecordingCaller{}
InitDeps(caller)
deps.Out.w = io.Discard
deps.Out.errW = io.Discard
t.Cleanup(func() { deps = previous })
root := newDocCommand()
root.SilenceErrors = true
root.SilenceUsage = true
root.SetOut(io.Discard)
root.SetErr(io.Discard)
root.SetArgs([]string{"create", "--name", "需求清单", "--content", content})
if err := root.ExecuteContext(context.Background()); err != nil {
t.Fatal(err)
}
if len(caller.calls) != 1 {
t.Fatalf("tool calls = %#v, want one create_document call", caller.calls)
}
call := caller.calls[0]
if call.tool != "create_document" {
t.Fatalf("tool = %q, want create_document", call.tool)
}
if got := call.args["markdown"]; got != content {
t.Fatalf("markdown = %#v, want exact explicit body H1 %#v", got, content)
}
})
}
}
+446
View File
@@ -0,0 +1,446 @@
package helpers
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/spf13/cobra"
)
// attachDocLocalPreflight wraps every doc leaf before its RunE reaches MCP.
// Only facts provable from local argv/filesystem state belong here; resource
// existence, permissions and business state remain server-owned.
func attachDocLocalPreflight(root *cobra.Command) {
var walk func(*cobra.Command)
walk = func(cmd *cobra.Command) {
for _, child := range cmd.Commands() {
walk(child)
}
if cmd.RunE == nil {
return
}
original := cmd.RunE
cmd.RunE = func(c *cobra.Command, args []string) error {
if err := validateDocLocalArgs(c); err != nil {
return err
}
return original(c, args)
}
}
walk(root)
}
func docLocalError(cmd *cobra.Command, code, message, suggestion string) error {
return &CLIError{Code: code, Message: message, Suggestion: suggestion, Operation: strings.TrimPrefix(cmd.CommandPath(), "dws ")}
}
func docRequire(cmd *cobra.Command, example string, names ...string) error {
missing := make([]string, 0, len(names))
for _, name := range names {
flag := cmd.Flags().Lookup(name)
if flag == nil || strings.TrimSpace(flag.Value.String()) == "" {
missing = append(missing, "--"+name)
}
}
if len(missing) == 0 {
return nil
}
return docLocalError(cmd, CodeMissingParam, "缺少必填参数: "+strings.Join(missing, ", "), "示例: "+example)
}
func docRequireNode(cmd *cobra.Command, example string) error {
if flagOrFallback(cmd, "node", "url", "id", "node-id", "doc-id", "file-id") != "" {
return nil
}
return docLocalError(cmd, CodeMissingParam, "缺少目标文档 --node(可传文档 URL 或 nodeId)", "先用 dws drive search 或 dws wiki node search 获取 nodeId。示例: "+example)
}
func docValidateLocalFile(cmd *cobra.Command, flagName, example string) error {
path, _ := cmd.Flags().GetString(flagName)
if strings.TrimSpace(path) == "" {
return docRequire(cmd, example, flagName)
}
info, err := os.Stat(path)
if err != nil {
return docLocalError(cmd, CodeFileNotFound, fmt.Sprintf("本地文件 %q 不可读取", path), "检查路径后重试。示例: "+example)
}
if info.IsDir() {
return docLocalError(cmd, CodeInvalidPath, fmt.Sprintf("%q 是目录,不是文件", path), "请传入具体文件路径。示例: "+example)
}
return nil
}
func docValidateWhere(cmd *cobra.Command, example string) error {
where, _ := cmd.Flags().GetString("where")
ref, _ := cmd.Flags().GetString("ref-block")
if where != "" && where != "before" && where != "after" {
return docLocalError(cmd, CodeInvalidParam, fmt.Sprintf("--where %q 无效,仅支持 before 或 after", where), "示例: "+example)
}
if where != "" && ref == "" {
return docLocalError(cmd, CodeMissingParam, "使用 --where 时必须同时提供 --ref-block", "先用 dws doc block list --node <DOC_ID> 获取 blockId。示例: "+example)
}
return nil
}
func docValidateEnum(cmd *cobra.Command, name string, allowed []string, example string) error {
value, _ := cmd.Flags().GetString(name)
if value == "" {
return nil
}
for _, candidate := range allowed {
if strings.EqualFold(value, candidate) {
return nil
}
}
return docLocalError(cmd, CodeInvalidParam, fmt.Sprintf("--%s %q 无效,仅支持 %s", name, value, strings.Join(allowed, "、")), "示例: "+example)
}
func docValidateLimit(cmd *cobra.Command, max int, example string) error {
for _, name := range []string{"limit", "page-size", "max-results"} {
if flag := cmd.Flags().Lookup(name); flag != nil && cmd.Flags().Changed(name) {
value, err := cmd.Flags().GetInt(name)
if err == nil && (value < 1 || value > max) {
return docLocalError(cmd, CodeInvalidParam, fmt.Sprintf("--%s 必须在 1 到 %d 之间", name, max), "示例: "+example)
}
}
}
return nil
}
func docValidateBlockContent(cmd *cobra.Command, example string) error {
changed := make([]string, 0, 3)
for _, name := range []string{"text", "heading", "element"} {
if flag := cmd.Flags().Lookup(name); flag != nil && cmd.Flags().Changed(name) && strings.TrimSpace(flag.Value.String()) != "" {
changed = append(changed, "--"+name)
}
}
if len(changed) == 0 {
return docLocalError(cmd, CodeMissingParam, "必须提供一种块内容:--text、--heading 或 --element", "示例: "+example)
}
if len(changed) > 1 {
return docLocalError(cmd, CodeInvalidParam, "块内容参数不能同时使用: "+strings.Join(changed, ", "), "三选一。示例: "+example)
}
format, _ := cmd.Flags().GetString("content-format")
if format == "jsonml" && changed[0] != "--element" {
return docLocalError(cmd, CodeInvalidParam, "--content-format jsonml 必须通过 --element 提供 JSONML 节点", "示例: "+example)
}
return nil
}
func docValidatePermissionUsers(cmd *cobra.Command, example string) error {
raw := flagOrFallback(cmd, "users", "user")
users := parseCommentMentionIds(raw)
if len(users) == 0 {
return docLocalError(cmd, CodeMissingParam, "缺少至少一个用户 ID:--users", "示例: "+example)
}
if len(users) > 30 {
return docLocalError(cmd, CodeInvalidParam, fmt.Sprintf("单次最多处理 30 个用户,当前为 %d 个", len(users)), "请拆分为多次调用")
}
return nil
}
func validateDocLocalArgs(cmd *cobra.Command) error {
path := strings.TrimPrefix(cmd.CommandPath(), "dws ")
switch path {
case "doc info":
return docRequireNode(cmd, "dws "+path+" --node <DOC_ID> --format json")
case "doc search":
if err := docValidateLimit(cmd, 30, "dws doc search --query \"周报\" --limit 10"); err != nil {
return err
}
createdFrom, _ := cmd.Flags().GetInt64("created-from")
createdTo, _ := cmd.Flags().GetInt64("created-to")
visitedFrom, _ := cmd.Flags().GetInt64("visited-from")
visitedTo, _ := cmd.Flags().GetInt64("visited-to")
if createdFrom < 0 || createdTo < 0 || visitedFrom < 0 || visitedTo < 0 {
return docLocalError(cmd, CodeInvalidParam, "时间过滤值必须是非负毫秒时间戳", "示例: dws doc search --created-from 1700000000000 --created-to 1710000000000")
}
if cmd.Flags().Changed("created-from") && cmd.Flags().Changed("created-to") && createdFrom > createdTo {
return docLocalError(cmd, CodeInvalidParam, "--created-from 不能晚于 --created-to", "请交换起止时间")
}
if cmd.Flags().Changed("visited-from") && cmd.Flags().Changed("visited-to") && visitedFrom > visitedTo {
return docLocalError(cmd, CodeInvalidParam, "--visited-from 不能晚于 --visited-to", "请交换起止时间")
}
case "doc list":
return docValidateLimit(cmd, 50, "dws doc list --workspace <WORKSPACE_ID> --limit 50")
case "doc read":
if err := docRequireNode(cmd, "dws doc read --node <DOC_ID> --format json"); err != nil {
return err
}
if depth, _ := cmd.Flags().GetInt("max-depth"); depth < 0 {
return docLocalError(cmd, CodeInvalidParam, "--max-depth 不能为负数", "示例: dws doc read --node <DOC_ID> --content-format jsonml --scope outline --max-depth 3")
}
if scopeValue, _ := cmd.Flags().GetString("scope"); scopeValue != "" {
valid := false
for _, candidate := range []string{"outline", "range", "section", "tags"} {
if scopeValue == candidate {
valid = true
}
}
if !valid {
return docLocalError(cmd, CodeInvalidParam, fmt.Sprintf("invalid --scope %q: must be one of outline|range|section|tags", scopeValue), "示例: dws doc read --node <DOC_ID> --content-format jsonml --scope outline")
}
}
format, _ := cmd.Flags().GetString("content-format")
scope, _ := cmd.Flags().GetString("scope")
tags, _ := cmd.Flags().GetString("tags")
startBlockID, _ := cmd.Flags().GetString("start-block-id")
if (scope == "range" || scope == "section") && strings.TrimSpace(startBlockID) == "" {
return docLocalError(cmd, CodeMissingParam, "--scope "+scope+" 必须提供 --start-block-id", "先用 dws doc block list --node <DOC_ID> --format json 获取真实 blockId")
}
if cmd.Flags().Changed("output") && format != "jsonml" {
return docLocalError(cmd, CodeInvalidParam, "--output 仅支持 --content-format jsonml", "Markdown 内容会直接显示在终端;如需保存为文件,请执行: dws doc read --node <DOC_ID> --content-format markdown --format raw > body.md")
}
if (scope != "" || tags != "") && format != "jsonml" {
return docLocalError(cmd, CodeInvalidParam, "--scope/--tags requires --content-format jsonml", "示例: dws doc read --node <DOC_ID> --content-format jsonml --scope tags --tags h1,h2")
}
if scope == "tags" && strings.TrimSpace(tags) == "" {
return docLocalError(cmd, CodeMissingParam, "--tags is required when --scope=tags", "示例: dws doc read --node <DOC_ID> --content-format jsonml --scope tags --tags h1,h2")
}
if scope != "tags" && tags != "" {
if scope == "" {
return docLocalError(cmd, CodeInvalidParam, "--tags requires --scope tags", "移除 --tags 或改用 --scope tags")
}
return docLocalError(cmd, CodeInvalidParam, "--tags only works with --scope tags", "移除 --tags 或改用 --scope tags")
}
case "doc create":
if strings.TrimSpace(flagOrFallback(cmd, "name", "title")) == "" {
return docLocalError(cmd, CodeMissingParam, "缺少文档名称 --name", "示例: dws doc create --name \"项目周报\" --format json")
}
if cmd.Flags().Changed("content") && cmd.Flags().Changed("content-file") {
return docLocalError(cmd, CodeInvalidParam, "--content 与 --content-file 不能同时使用", "短文本用 --content;长文本或表格用 --content-file")
}
if cmd.Flags().Changed("content-file") {
return docValidateLocalFile(cmd, "content-file", "dws doc create --name \"周报\" --content-file ./weekly.md")
}
case "doc update":
if err := docRequireNode(cmd, "dws doc update --node <DOC_ID> --content \"追加内容\" --mode append"); err != nil {
return err
}
if cmd.Flags().Changed("content") && cmd.Flags().Changed("content-file") {
return docLocalError(cmd, CodeInvalidParam, "--content 与 --content-file 不能同时使用", "二选一;长内容优先 --content-file")
}
if cmd.Flags().Changed("content-file") {
if err := docValidateLocalFile(cmd, "content-file", "dws doc update --node <DOC_ID> --content-file ./body.md --mode append"); err != nil {
return err
}
}
mode, _ := cmd.Flags().GetString("mode")
if mode != "append" && mode != "overwrite" {
return docLocalError(cmd, CodeInvalidParam, "--mode 必须是 append 或 overwrite", "追加优先使用: dws doc update --node <DOC_ID> --content \"内容\" --mode append")
}
if idx, _ := cmd.Flags().GetInt("index"); cmd.Flags().Changed("index") && (mode != "append" || idx < 0) {
return docLocalError(cmd, CodeInvalidParam, "--index 仅支持 mode=append 且必须大于等于 0", "示例: dws doc update --node <DOC_ID> --content \"内容\" --mode append --index 0")
}
content := flagOrFallback(cmd, "content", "markdown")
if strings.TrimSpace(content) == "" && !cmd.Flags().Changed("content-file") {
return docLocalError(cmd, CodeMissingParam, "必须通过 --content 或 --content-file 提供非空内容", "示例: dws doc update --node <DOC_ID> --content \"追加内容\" --mode append")
}
dryRun, _ := cmd.Flags().GetBool("dry-run")
yes, _ := cmd.Flags().GetBool("yes")
if dryRun && mode != "overwrite" {
return docLocalError(cmd, CodeInvalidParam, "--dry-run 仅用于预览 overwrite 覆盖写入", "append 本身不覆盖全文,请移除 --dry-run")
}
if yes && mode != "overwrite" {
return docLocalError(cmd, CodeInvalidParam, "--yes 仅用于确认 overwrite 覆盖写入", "append 无需 --yes,请移除该参数")
}
if mode == "overwrite" && !dryRun && !yes {
return docLocalError(cmd, CodeInvalidParam, "--mode overwrite 必须加 --yes 确认,或使用 --dry-run 预览", "示例: dws doc update --node <DOC_ID> --content-file ./body.md --mode overwrite --dry-run")
}
case "doc block list":
if err := docRequireNode(cmd, "dws doc block list --node <DOC_ID> --format json"); err != nil {
return err
}
start, _ := cmd.Flags().GetInt("start-index")
end, _ := cmd.Flags().GetInt("end-index")
if start < 0 || end < 0 || (cmd.Flags().Changed("end-index") && end < start) {
return docLocalError(cmd, CodeInvalidParam, "块索引必须非负,且 --end-index 不能小于 --start-index", "示例: dws doc block list --node <DOC_ID> --start-index 0 --end-index 5")
}
case "doc block insert":
if err := docRequireNode(cmd, "dws doc block insert --node <DOC_ID> --text \"内容\""); err != nil {
return err
}
if err := docValidateWhere(cmd, "dws doc block insert --node <DOC_ID> --text \"内容\" --ref-block <BLOCK_ID> --where after"); err != nil {
return err
}
if level, _ := cmd.Flags().GetInt("level"); level < 1 || level > 6 {
return docLocalError(cmd, CodeInvalidParam, "--level 必须在 1 到 6 之间", "示例: dws doc block insert --node <DOC_ID> --heading \"标题\" --level 2")
}
if idx, _ := cmd.Flags().GetInt("index"); cmd.Flags().Changed("index") && idx < 0 {
return docLocalError(cmd, CodeInvalidParam, "--index 不能为负数", "示例: dws doc block insert --node <DOC_ID> --text \"内容\" --index 0")
}
return docValidateBlockContent(cmd, "dws doc block insert --node <DOC_ID> --text \"内容\"")
case "doc block update":
if err := docRequireNode(cmd, "dws doc block update --node <DOC_ID> --block-id <BLOCK_ID> --text \"新内容\""); err != nil {
return err
}
if err := docRequire(cmd, "dws doc block update --node <DOC_ID> --block-id <BLOCK_ID> --text \"新内容\"", "block-id"); err != nil {
return err
}
if level, _ := cmd.Flags().GetInt("level"); level < 1 || level > 6 {
return docLocalError(cmd, CodeInvalidParam, "--level 必须在 1 到 6 之间", "示例: dws doc block update --node <DOC_ID> --block-id <BLOCK_ID> --heading \"标题\" --level 2")
}
return docValidateBlockContent(cmd, "dws doc block update --node <DOC_ID> --block-id <BLOCK_ID> --text \"新内容\"")
case "doc block delete":
if err := docRequireNode(cmd, "dws doc block delete --node <DOC_ID> --block-id <BLOCK_ID> --yes"); err != nil {
return err
}
return docRequire(cmd, "dws doc block delete --node <DOC_ID> --block-id <BLOCK_ID> --yes", "block-id")
case "doc media download":
if err := docRequireNode(cmd, "dws doc media download --node <DOC_ID> --resource-id <RESOURCE_ID>"); err != nil {
return err
}
return docRequire(cmd, "dws doc media download --node <DOC_ID> --resource-id <RESOURCE_ID>", "resource-id")
case "doc media insert":
if err := docRequireNode(cmd, "dws doc media insert --node <DOC_ID> --file ./report.pdf"); err != nil {
return err
}
if err := docValidateWhere(cmd, "dws doc media insert --node <DOC_ID> --file ./report.pdf --ref-block <BLOCK_ID> --where after"); err != nil {
return err
}
return docValidateLocalFile(cmd, "file", "dws doc media insert --node <DOC_ID> --file ./report.pdf")
case "doc comment list":
if err := docRequireNode(cmd, "dws doc comment list --node <DOC_ID> --format json"); err != nil {
return err
}
if err := docValidateLimit(cmd, 50, "dws doc comment list --node <DOC_ID> --limit 50"); err != nil {
return err
}
if err := docValidateEnum(cmd, "type", []string{"global", "inline"}, "dws doc comment list --node <DOC_ID> --type inline"); err != nil {
return err
}
return docValidateEnum(cmd, "resolve-status", []string{"resolved", "unresolved"}, "dws doc comment list --node <DOC_ID> --resolve-status unresolved")
case "doc comment create":
if err := docRequireNode(cmd, "dws doc comment create --node <DOC_ID> --content \"评论\""); err != nil {
return err
}
return docRequire(cmd, "dws doc comment create --node <DOC_ID> --content \"评论\"", "content")
case "doc comment reply", "doc comment update":
if err := docRequireNode(cmd, "dws "+path+" --node <DOC_ID> --comment-key <COMMENT_KEY> --content \"内容\""); err != nil {
return err
}
if err := docRequire(cmd, "dws "+path+" --node <DOC_ID> --comment-key <COMMENT_KEY> --content \"内容\"", "comment-key", "content"); err != nil {
return err
}
if path == "doc comment reply" {
emoji, _ := cmd.Flags().GetBool("emoji")
groups, groupErr := commentGroupMentionIDs(cmd)
if groupErr != nil {
return groupErr
}
if emoji && len(groups) > 0 {
return docLocalError(cmd, CodeInvalidParam, "--emoji cannot be used with --mentioned-open-conversation-id: emoji replies do not support group mentions", "表情回复请移除群 @;需要 @群时使用普通文字回复")
}
}
case "doc comment delete":
if err := docRequireNode(cmd, "dws doc comment delete --node <DOC_ID> --comment-key <COMMENT_KEY> --yes"); err != nil {
return err
}
return docRequire(cmd, "dws doc comment delete --node <DOC_ID> --comment-key <COMMENT_KEY> --yes", "comment-key")
case "doc comment create-inline":
if err := docRequireNode(cmd, "dws doc comment create-inline --node <DOC_ID> --block-id <BLOCK_ID> --start 0 --end 5 --content \"评论\""); err != nil {
return err
}
if err := docRequire(cmd, "dws doc comment create-inline --node <DOC_ID> --block-id <BLOCK_ID> --start 0 --end 5 --content \"评论\"", "block-id", "content"); err != nil {
return err
}
start, _ := cmd.Flags().GetInt("start")
end, _ := cmd.Flags().GetInt("end")
if !cmd.Flags().Changed("start") || !cmd.Flags().Changed("end") || start < 0 || end <= start {
return docLocalError(cmd, CodeInvalidParam, "划词范围必须显式提供 --start/--end,且满足 0 <= start < end", "先读取块文本确认字符偏移。示例: dws doc comment create-inline --node <DOC_ID> --block-id <BLOCK_ID> --start 0 --end 5 --content \"评论\"")
}
case "doc export":
if err := docRequireNode(cmd, "dws doc export --node <DOC_ID> --export-format pdf --output ./report.pdf"); err != nil {
return err
}
if err := docRequire(cmd, "dws doc export --node <DOC_ID> --export-format pdf --output ./report.pdf", "output"); err != nil {
return err
}
return docValidateEnum(cmd, "export-format", []string{"docx", "markdown", "md", "pdf"}, "dws doc export --node <DOC_ID> --export-format pdf --output ./report.pdf")
case "doc export get":
return docRequire(cmd, "dws doc export get --job-id <JOB_ID>", "job-id")
case "doc import":
if err := docValidateLocalFile(cmd, "file", "dws doc import --file ./report.docx --workspace <WORKSPACE_ID>"); err != nil {
return err
}
if !deps.Caller.DryRun() && flagOrFallback(cmd, "folder", "folder-id") == "" && flagOrFallback(cmd, "workspace", "workspace-id") == "" {
return docLocalError(cmd, CodeMissingParam, "导入文档必须提供 --folder 或 --workspace 作为目标位置", "先用 dws wiki space list --type myWikiSpace --format json 获取 workspaceId")
}
case "doc import get":
return docRequire(cmd, "dws doc import get --task-id <TASK_ID>", "task-id")
case "doc version save", "doc version list", "doc version revert":
if err := docRequireNode(cmd, "dws "+path+" --node <DOC_ID> --format json"); err != nil {
return err
}
if path == "doc version revert" && (!cmd.Flags().Changed("version")) {
return docLocalError(cmd, CodeMissingParam, "缺少目标版本 --version", "先用 dws doc version list --node <DOC_ID> 获取真实版本号")
}
if path == "doc version revert" {
version, _ := cmd.Flags().GetInt("version")
if version < 1 {
return docLocalError(cmd, CodeInvalidParam, "--version 必须是大于 0 的真实版本号", "先用 dws doc version list --node <DOC_ID> 获取版本号")
}
}
if path == "doc version list" {
return docValidateLimit(cmd, 50, "dws doc version list --node <DOC_ID> --limit 10")
}
case "doc permission add", "doc permission update":
if err := docRequireNode(cmd, "dws "+path+" --node <DOC_ID> --users uid1 --role EDITOR"); err != nil {
return err
}
if err := docValidatePermissionUsers(cmd, "dws "+path+" --node <DOC_ID> --users uid1 --role EDITOR"); err != nil {
return err
}
if err := docRequire(cmd, "dws "+path+" --node <DOC_ID> --users uid1 --role EDITOR", "role"); err != nil {
return err
}
return docValidateEnum(cmd, "role", []string{"MANAGER", "EDITOR", "DOWNLOADER", "READER"}, "dws "+path+" --node <DOC_ID> --users uid1 --role EDITOR")
case "doc permission remove":
if err := docRequireNode(cmd, "dws doc permission remove --node <DOC_ID> --users uid1"); err != nil {
return err
}
return docValidatePermissionUsers(cmd, "dws doc permission remove --node <DOC_ID> --users uid1")
case "doc permission list":
if err := docRequireNode(cmd, "dws doc permission list --node <DOC_ID> --limit 30"); err != nil {
return err
}
if err := docValidateLimit(cmd, 200, "dws doc permission list --node <DOC_ID> --limit 30"); err != nil {
return err
}
if roles, _ := cmd.Flags().GetString("filter-role"); roles != "" {
for _, role := range parseRoleList(roles) {
valid := false
for _, candidate := range []string{"OWNER", "MANAGER", "EDITOR", "DOWNLOADER", "READER"} {
if role == candidate {
valid = true
}
}
if !valid {
return docLocalError(cmd, CodeInvalidParam, "--filter-role 包含非法角色 "+role, "仅支持 OWNER、MANAGER、EDITOR、DOWNLOADER、READER")
}
}
}
case "doc template list":
if err := docValidateEnum(cmd, "source", []string{"MY", "PUBLIC"}, "dws doc template list --source MY"); err != nil {
return err
}
return docValidateLimit(cmd, 50, "dws doc template list --limit 20")
case "doc template search":
if flagOrFallback(cmd, "query", "keyword", "name") == "" {
return docLocalError(cmd, CodeMissingParam, "缺少非空模板关键词 --query", "示例: dws doc template search --query \"周报\" --format json")
}
if err := docValidateEnum(cmd, "source", []string{"MY", "PUBLIC"}, "dws doc template search --query \"周报\" --source PUBLIC"); err != nil {
return err
}
return docValidateLimit(cmd, 50, "dws doc template search --query \"周报\" --limit 20")
case "doc template apply":
if flagOrFallback(cmd, "template-id", "template", "tpl-id") == "" {
return docLocalError(cmd, CodeMissingParam, "缺少模板 ID --template-id", "先用 dws doc template search --query \"关键词\" 获取真实 templateId")
}
}
return nil
}
func docLocalFileExtension(path string) string { return strings.ToLower(filepath.Ext(path)) }
@@ -0,0 +1,137 @@
package helpers
import (
"errors"
"strings"
"testing"
)
func TestDocLocalPreflightRejectsBeforeMCP(t *testing.T) {
cases := []struct {
name string
args []string
want string
}{
{"info-node", []string{"info"}, "--node"},
{"read-node", []string{"read"}, "--node"},
{"create-name", []string{"create"}, "--name"},
{"create-content-conflict", []string{"create", "--name", "x", "--content", "a", "--content-file", "b"}, "不能同时"},
{"create-file-missing", []string{"create", "--name", "x", "--content-file", "/definitely/not/found.md"}, "不可读取"},
{"update-node", []string{"update", "--content", "x", "--mode", "append"}, "--node"},
{"update-content-conflict", []string{"update", "--node", "n", "--content", "a", "--content-file", "b", "--mode", "append"}, "不能同时"},
{"update-mode-missing", []string{"update", "--node", "n", "--content", "a"}, "--mode"},
{"update-mode-invalid", []string{"update", "--node", "n", "--content", "a", "--mode", "merge"}, "append 或 overwrite"},
{"update-index-mode", []string{"update", "--node", "n", "--content", "a", "--mode", "overwrite", "--index", "1"}, "--index"},
{"block-list-node", []string{"block", "list"}, "--node"},
{"block-list-range", []string{"block", "list", "--node", "n", "--start-index", "4", "--end-index", "2"}, "不能小于"},
{"block-insert-node", []string{"block", "insert", "--text", "x"}, "--node"},
{"block-insert-where", []string{"block", "insert", "--node", "n", "--text", "x", "--where", "middle"}, "before 或 after"},
{"block-insert-ref", []string{"block", "insert", "--node", "n", "--text", "x", "--where", "before"}, "--ref-block"},
{"block-insert-level", []string{"block", "insert", "--node", "n", "--heading", "x", "--level", "7"}, "1 到 6"},
{"block-update-node", []string{"block", "update", "--block-id", "b", "--text", "x"}, "--node"},
{"block-update-id", []string{"block", "update", "--node", "n", "--text", "x"}, "--block-id"},
{"block-delete-id", []string{"block", "delete", "--node", "n"}, "--block-id"},
{"media-download-resource", []string{"media", "download", "--node", "n"}, "--resource-id"},
{"media-insert-file", []string{"media", "insert", "--node", "n"}, "--file"},
{"comment-create-content", []string{"comment", "create", "--node", "n"}, "--content"},
{"comment-reply-key", []string{"comment", "reply", "--node", "n", "--content", "x"}, "--comment-key"},
{"comment-update-content", []string{"comment", "update", "--node", "n", "--comment-key", "k"}, "--content"},
{"comment-delete-key", []string{"comment", "delete", "--node", "n"}, "--comment-key"},
{"inline-block", []string{"comment", "create-inline", "--node", "n", "--content", "x", "--start", "0", "--end", "1"}, "--block-id"},
{"inline-range", []string{"comment", "create-inline", "--node", "n", "--block-id", "b", "--content", "x", "--start", "2", "--end", "1"}, "start < end"},
{"export-output", []string{"export", "--node", "n"}, "--output"},
{"export-job", []string{"export", "get"}, "--job-id"},
{"import-file", []string{"import", "--workspace", "w"}, "--file"},
{"import-target", []string{"import", "--file", "/definitely/not/found.docx"}, "不可读取"},
{"import-task", []string{"import", "get"}, "--task-id"},
{"version-node", []string{"version", "list"}, "--node"},
{"version-number", []string{"version", "revert", "--node", "n"}, "--version"},
{"template-query", []string{"template", "search"}, "--query"},
{"template-id", []string{"template", "apply"}, "--template-id"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
cmd := newDocCommand()
cmd.SetArgs(tc.args)
cmd.SilenceUsage = true
cmd.SilenceErrors = true
err := cmd.Execute()
if err == nil || !strings.Contains(err.Error(), tc.want) {
t.Fatalf("error = %v, want containing %q", err, tc.want)
}
var cliErr *CLIError
if !errors.As(err, &cliErr) || cliErr.ExitCode() != ExitValidation {
t.Fatalf("error = %#v, want validation CLIError", err)
}
})
}
}
func TestDocLocalPreflightSecondBatchRejectsBeforeMCP(t *testing.T) {
users31 := strings.TrimSuffix(strings.Repeat("u,", 31), ",")
cases := []struct {
name string
args []string
want string
}{
{"search-created-negative", []string{"search", "--created-from", "-1"}, "非负毫秒"},
{"search-created-range", []string{"search", "--created-from", "20", "--created-to", "10"}, "created-from"},
{"search-visited-negative", []string{"search", "--visited-to", "-1"}, "非负毫秒"},
{"search-visited-range", []string{"search", "--visited-from", "20", "--visited-to", "10"}, "visited-from"},
{"search-limit-zero", []string{"search", "--limit", "0"}, "1 到 30"},
{"search-limit-large", []string{"search", "--limit", "31"}, "1 到 30"},
{"list-limit-zero", []string{"list", "--limit", "0"}, "1 到 50"},
{"list-limit-large", []string{"list", "--limit", "51"}, "1 到 50"},
{"read-depth-negative", []string{"read", "--node", "n", "--max-depth", "-1"}, "不能为负"},
{"read-scope-invalid", []string{"read", "--node", "n", "--content-format", "jsonml", "--scope", "all"}, "outline"},
{"read-scope-format", []string{"read", "--node", "n", "--scope", "outline"}, "content-format jsonml"},
{"read-tags-missing", []string{"read", "--node", "n", "--content-format", "jsonml", "--scope", "tags"}, "--tags"},
{"read-tags-wrong-scope", []string{"read", "--node", "n", "--content-format", "jsonml", "--scope", "outline", "--tags", "h1"}, "--tags only works"},
{"read-range-start-missing", []string{"read", "--node", "n", "--content-format", "jsonml", "--scope", "range"}, "--start-block-id"},
{"read-section-start-missing", []string{"read", "--node", "n", "--content-format", "jsonml", "--scope", "section"}, "--start-block-id"},
{"read-markdown-output", []string{"read", "--node", "n", "--content-format", "markdown", "--output", "body.json"}, "Markdown 内容会直接显示在终端"},
{"create-whitespace-name", []string{"create", "--name", " "}, "--name"},
{"update-empty-content", []string{"update", "--node", "n", "--content", " ", "--mode", "append"}, "非空内容"},
{"update-dry-run-append", []string{"update", "--node", "n", "--content", "x", "--mode", "append", "--dry-run"}, "仅用于预览 overwrite"},
{"update-yes-append", []string{"update", "--node", "n", "--content", "x", "--mode", "append", "--yes"}, "仅用于确认 overwrite"},
{"update-overwrite-confirm", []string{"update", "--node", "n", "--content", "x", "--mode", "overwrite"}, "必须加 --yes"},
{"block-insert-content", []string{"block", "insert", "--node", "n"}, "必须提供一种块内容"},
{"block-insert-text-heading", []string{"block", "insert", "--node", "n", "--text", "x", "--heading", "h"}, "不能同时"},
{"block-insert-heading-element", []string{"block", "insert", "--node", "n", "--heading", "h", "--element", "{}"}, "不能同时"},
{"block-insert-jsonml-text", []string{"block", "insert", "--node", "n", "--content-format", "jsonml", "--text", "x"}, "必须通过 --element"},
{"block-insert-index", []string{"block", "insert", "--node", "n", "--text", "x", "--index", "-1"}, "--index 不能"},
{"block-update-content", []string{"block", "update", "--node", "n", "--block-id", "b"}, "必须提供一种块内容"},
{"block-update-text-element", []string{"block", "update", "--node", "n", "--block-id", "b", "--text", "x", "--element", "{}"}, "不能同时"},
{"block-update-level", []string{"block", "update", "--node", "n", "--block-id", "b", "--heading", "h", "--level", "0"}, "1 到 6"},
{"comment-limit-zero", []string{"comment", "list", "--node", "n", "--limit", "0"}, "1 到 50"},
{"comment-limit-large", []string{"comment", "list", "--node", "n", "--limit", "51"}, "1 到 50"},
{"comment-type", []string{"comment", "list", "--node", "n", "--type", "all"}, "global"},
{"comment-status", []string{"comment", "list", "--node", "n", "--resolve-status", "open"}, "resolved"},
{"reply-emoji-group", []string{"comment", "reply", "--node", "n", "--comment-key", "k", "--content", "x", "--emoji", "--mentioned-open-conversation-id", "g"}, "emoji replies do not support group mentions"},
{"permission-users-missing", []string{"permission", "add", "--node", "n", "--role", "READER"}, "--users"},
{"permission-users-large", []string{"permission", "add", "--node", "n", "--role", "READER", "--users", users31}, "最多处理 30"},
{"permission-role", []string{"permission", "add", "--node", "n", "--role", "OWNER", "--users", "u"}, "MANAGER"},
{"permission-filter-role", []string{"permission", "list", "--node", "n", "--filter-role", "ADMIN"}, "非法角色"},
{"export-format", []string{"export", "--node", "n", "--output", "x", "--export-format", "html"}, "docx"},
}
if len(cases) != 39 {
t.Fatalf("second batch has %d cases, want 39", len(cases))
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
cmd := newDocCommand()
cmd.SetArgs(tc.args)
cmd.SilenceUsage = true
cmd.SilenceErrors = true
err := cmd.Execute()
if err == nil || !strings.Contains(err.Error(), tc.want) {
t.Fatalf("error = %v, want containing %q", err, tc.want)
}
var cliErr *CLIError
if !errors.As(err, &cliErr) || cliErr.ExitCode() != ExitValidation {
t.Fatalf("error = %#v, want validation CLIError", err)
}
})
}
}
@@ -0,0 +1,27 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
package helpers
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestMultiWikiSkillDoesNotRequireYesForAppend(t *testing.T) {
path := filepath.Join("..", "..", "skills", "multi", "dingtalk-wiki", "SKILL.md")
content, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read wiki skill: %v", err)
}
text := string(content)
if strings.Contains(text, "--mode overwrite|append --content-file <tmp.md> --yes") {
t.Fatal("wiki skill still applies --yes to append and overwrite indiscriminately")
}
if !strings.Contains(text, "--mode append --content-file <tmp.md>") ||
!strings.Contains(text, "--mode overwrite --content-file <tmp.md> --yes") {
t.Fatal("wiki skill must publish separate append and confirmed overwrite examples")
}
}
+1
View File
@@ -326,6 +326,7 @@ func runImportCommand(cmd *cobra.Command, args []string, cfg importFlowConfig) e
documentType, _ := result["documentType"].(string)
finalResult := map[string]any{
"success": true,
"status": "completed",
"taskId": taskID,
"documentUrl": documentURL,
"documentName": documentName,
+1 -1
View File
@@ -181,7 +181,7 @@ func TestCrossPlatformCoverageSheetImportRunsSharedDocImportFlow(t *testing.T) {
if err := json.Unmarshal([]byte(output), &payload); err != nil {
t.Fatalf("sheet import stdout must be one JSON document: %v\n%s", err, output)
}
if payload["nodeId"] != "node-1" || payload["success"] != true {
if payload["nodeId"] != "node-1" || payload["success"] != true || payload["status"] != "completed" {
t.Fatalf("output missing success contract: %#v", payload)
}
}
+43 -1
View File
@@ -18,6 +18,8 @@ import (
"strconv"
"strings"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/helpers"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/chatmsg"
)
@@ -82,6 +84,18 @@ var ChatMembersGet = shortcut.Shortcut{
{Kind: shortcut.ConstraintExactlyOne, Flags: []string{"users", "open-dingtalk-ids"}},
},
Tips: []string{`dws chat +chat-members-get --id <openConversationId> --users odid1,odid2`},
Validate: func(rt *shortcut.RuntimeContext) error {
for _, value := range rt.StrSliceFirst("users", "open-dingtalk-ids") {
value = strings.TrimSpace(value)
if value == "" || !isOpenID(value) {
return apperrors.NewValidation(fmt.Sprintf(
"--users/--open-dingtalk-ids 只接受成员 openDingTalkId;%q 不是 openDingTalkId。请先查询人员并传返回的 openDingTalkId",
value,
))
}
}
return nil
},
Execute: func(rt *shortcut.RuntimeContext) error {
conversationID := rt.StrFirst("id", "group", "chat-id", "conversation-id", "open-conversation-id")
return rt.CallMCP("list_group_member_by_ids", map[string]any{
@@ -186,6 +200,12 @@ var ChatUpdateIcon = shortcut.Shortcut{
{Name: "icon-media-id", Type: shortcut.FlagString, Desc: "群头像 mediaId(以 @ 开头)", Required: true},
},
Tips: []string{`dws chat +chat-update-icon --group <openConversationId> --icon-media-id <mediaId>`},
Validate: func(rt *shortcut.RuntimeContext) error {
if err := helpers.ValidateChatMediaID(rt.Str("icon-media-id")); err != nil {
return apperrors.NewValidation("invalid --icon-media-id: " + err.Error())
}
return nil
},
Execute: func(rt *shortcut.RuntimeContext) error {
return rt.CallMCP("update_group_icon", map[string]any{
"openConversationId": rt.Str("group"),
@@ -207,7 +227,8 @@ var ChatUpdateSettings = shortcut.Shortcut{
{Name: "setting-key", Type: shortcut.FlagString, Desc: "群设置项 key,如 searchable / onlyAdminCanAtAll", Required: true},
{Name: "status", Type: shortcut.FlagInt, Desc: "设置值:0=关闭,1=开启", Required: true},
},
Tips: []string{`dws chat +chat-update-settings --group <openConversationId> --setting-key searchable --status 1`},
Tips: []string{`dws chat +chat-update-settings --group <openConversationId> --setting-key searchable --status 1`},
Validate: validateChatUpdateSettings,
Execute: func(rt *shortcut.RuntimeContext) error {
return rt.CallMCP("update_group_settings", map[string]any{
"openConversationId": rt.Str("group"),
@@ -217,6 +238,27 @@ var ChatUpdateSettings = shortcut.Shortcut{
},
}
var supportedChatSettingKeys = map[string]struct{}{
"authority": {}, "joinValidation": {}, "onlyAdminCanAtAll": {}, "searchable": {},
"addFriendForbidden": {}, "toolbarStatus": {}, "pluginCustomizeVerify": {},
"onlyAdminCanDING": {}, "allMembersCanCreateMcsConf": {}, "onlyAdminCanSetMsgTop": {},
"onlyAdminCanPinMsg": {}, "onlyAdminCanSendFile": {}, "allMembersCanCreateCalendar": {},
"groupEmailDisabled": {}, "groupRedEnvelopeSwitch": {}, "groupLiveAuthority": {},
"groupBillAuthority": {},
}
func validateChatUpdateSettings(rt *shortcut.RuntimeContext) error {
key := strings.TrimSpace(rt.Str("setting-key"))
if _, ok := supportedChatSettingKeys[key]; !ok {
return apperrors.NewValidation(fmt.Sprintf("不支持的 --setting-key %q;请使用 chat group update-settings --help 中列出的设置项", key))
}
status := rt.Int("status")
if status != 0 && status != 1 {
return apperrors.NewValidation("--status 只允许 0(关闭)或 1(开启)")
}
return nil
}
// ChatDismiss dismisses (destroys) a group (dismiss_group, im).
var ChatDismiss = shortcut.Shortcut{
Service: "chat",
+38
View File
@@ -54,6 +54,15 @@ func TestMessagesSendPublishesCompleteIdentityConstraintInputs(t *testing.T) {
}
}
func TestMessagesSendUploadTargetUsesUploadInterfaceFields(t *testing.T) {
if got := messagesSendUploadTarget("cid-1", ""); !reflect.DeepEqual(got, map[string]any{"openConversationId": "cid-1"}) {
t.Fatalf("group upload target = %#v", got)
}
if got := messagesSendUploadTarget("", "D-open-1"); !reflect.DeepEqual(got, map[string]any{"openDingTalkId": "D-open-1"}) {
t.Fatalf("direct upload target = %#v", got)
}
}
func TestCrossPlatformCoverageSafeResourceDownloadsStayReadOnly(t *testing.T) {
for _, command := range []shortcut.Shortcut{MessagesMget, MessagesResourceDownload} {
if command.Risk != shortcut.RiskRead {
@@ -155,6 +164,9 @@ func TestCrossPlatformCoverageMessagesSendCurrentUserLocalFileFlow(t *testing.T)
fake.calls[2].tool != "send_personal_message" {
t.Fatalf("file flow calls = %#v", fake.calls)
}
if fake.calls[0].args["openConversationId"] != "cid" || fake.calls[1].args["openConversationId"] != "cid" {
t.Fatalf("upload target args = %#v / %#v, want openConversationId", fake.calls[0].args, fake.calls[1].args)
}
send := fake.calls[2]
if send.args["msgType"] != "file" || send.args["openConversationId"] != "cid" ||
send.args["uuid"] != "file-key" {
@@ -177,6 +189,32 @@ func TestCrossPlatformCoverageMessagesSendCurrentUserLocalFileFlow(t *testing.T)
}
}
func TestChatShortcutInvalidInputsStopBeforeMCP(t *testing.T) {
tests := []struct {
name string
args []string
}{
{name: "member numeric user id", args: []string{"chat", "+chat-members-get", "--id", "cid", "--users", "489149"}},
{name: "icon local path", args: []string{"chat", "+chat-update-icon", "--group", "cid", "--icon-media-id", "./logo.png", "--yes"}},
{name: "setting unknown key", args: []string{"chat", "+chat-update-settings", "--group", "cid", "--setting-key", "unknown", "--status", "1", "--yes"}},
{name: "setting invalid status", args: []string{"chat", "+chat-update-settings", "--group", "cid", "--setting-key", "searchable", "--status", "2", "--yes"}},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
fake := &larkAlignmentCaller{}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
root.SetArgs(tc.args)
if err := root.Execute(); err == nil {
t.Fatal("invalid command succeeded, want validation error")
}
if len(fake.calls) != 0 {
t.Fatalf("MCP calls = %#v, want zero", fake.calls)
}
})
}
}
func TestCrossPlatformCoverageMessagesSendCurrentUserLocalFileDryRunAndFailures(t *testing.T) {
t.Chdir(t.TempDir())
if err := os.WriteFile("fixture.bin", []byte("x"), 0o600); err != nil {
+8 -2
View File
@@ -420,8 +420,7 @@ func executeMessagesSendUserFile(
if err != nil {
return err
}
targetArgs := map[string]any{}
addMessagesSendUserTarget(targetArgs, group, openID)
targetArgs := messagesSendUploadTarget(group, openID)
idempotencyKey := messagesSendIdempotencyKey(rt)
if rt.DryRun() {
return rt.Output(map[string]any{
@@ -490,6 +489,13 @@ func executeMessagesSendUserFile(
})
}
func messagesSendUploadTarget(group, openID string) map[string]any {
if group != "" {
return map[string]any{"openConversationId": group}
}
return map[string]any{"openDingTalkId": openID}
}
func addMessagesSendUserTarget(params map[string]any, group, openID string) {
if group != "" {
params["openConversationId"] = group
@@ -2,6 +2,12 @@
> 通用规范见 [_common/conventions.md](_common/conventions.md)。
## 显式工作流
- 用户点名的 `create → list → insert/append/update` 是可观察命令链,必须保持顺序逐项执行;create 只承载明确的初始正文。有序列表块必须验证回读结构中的 `list.isOrdered=true`。
- `--name` 不替代用户显式要求的正文 H1;新建资源返回 ID 后,同一请求的指代绑定该新资源,禁止搜索同名旧资源替换。
- Word/Excel 需要“在线编辑/直接在线改”时使用 `doc import`,普通 `drive upload` 只保留原文件。
| Recipe | 行动指南(固定路线) |
|--------|-------------------|
| write-doc | 1. 按[「多源并行采集」](_common/conventions.md#多源并行采集公共模式)执行<br>2. **先把内容写入临时文件**(Linux/Mac `/tmp/<name>.md`,Windows `%TEMP%\<name>.md`)—— 含多行/表格/长文本必须走文件,不要把 markdown 直接作为命令行字符串<br>3. **单步创建**(< 200KB):`doc create --name "<文档名>" --content-file <tmp> [--folder <DOC_FOLDER_NODE_ID>] [--workspace <WS_ID>]`(`--folder` 只传文档文件夹 nodeId / alidocs 文件夹 URL,不传数字 dentryId)<br>4. **超长兜底**(> 200KB):**必须先向用户提示截断风险**(详见下方「分块 append 截断风险提示」),用户确认后再执行:`doc create --name "<文档名>" [--folder/--workspace]` → `nodeId` → 按段落切 ≤200KB 片段(不断表格) → 每片 `doc update --node <nodeId> --content-file <part> --mode append`<br>5. **回读校验**(必须):所有写入完成后,执行 `doc read --node <nodeId>` 回读文档,校验关键标题/段落是否完整写入(详见下方「doc update 回读校验规范」)<br>备选(仅短内容 <2KB 且无换行/表格):`doc create --name "..." --content "..."` |
+9 -7
View File
@@ -665,8 +665,8 @@ Flags:
- 知识库内 → `dws wiki node create --workspace <WS_ID> --type folder`(`doc folder create` / `doc file create --type folder` 已弃用)
用户说"上传文件/传文件/上传到文档/上传到知识库":
- 上传 → `upload`(需本地文件路径)
- 上传并转换 → `upload --convert`
- 仅保留原始文件用于存储/下载 → `drive upload`(需本地文件路径)
- 用户明确要求“在线编辑/大家直接在线改/转在线文档” → `doc import --file <本地路径>`;不得用普通 upload 的成功响应宣称可在线编辑
用户说"导入文件/导入为在线文档/导入 Word/导入 Excel/导入 xmind/导入 Markdown/把本地文件转在线文档":
- 导入并转换为在线文档 → `doc import --file <本地路径>`
@@ -742,8 +742,8 @@ Flags:
关键区分: doc(文档编辑/阅读) vs aitable(数据表格操作) vs drive(钉盘文件管理)
用户说"上传文件/传文件/上传到文档/上传到知识库":
- 上传 → `upload`(需本地文件路径)
- 上传并转换 → `upload --convert`
- 仅保留原始文件用于存储/下载 → `drive upload`(需本地文件路径)
- 要转换为可在线编辑文档 → `doc import --file <本地路径>`,导入后验证在线类型与目标文件夹
用户说"下载文件/导出文件/下载到本地":
- 下载 → `download`(需文件节点 ID 或 URL)
@@ -1050,14 +1050,16 @@ EOF
- `read` 返回的内容中,文档里的附件会以 OSS 临时下载链接形式给出(如 `https://alidocs2.oss-cn-zhangjiakou.aliyuncs.com/res/.../att/<resourceId>.ext?Expires=...`),该链接会过期。链接过期后,可从 URL 路径中提取 `<resourceId>`(即 `/att/` 后、扩展名前的 UUID 部分),然后使用 `media download --node <DOC_ID> --resource-id <resourceId>` 重新获取下载链接
- `create` 不传 `--folder` 和 `--workspace` 时,默认创建在"我的文档"根目录
- `create` 只能建"文档"(adoc);要建表格/脑图/白板/多维表/演示,用 `dws wiki node create --workspace <id> --type <type>`(`doc file create` 已弃用);建普通文件夹用 `dws drive mkdir`
- `block list/insert/update/delete` 是块级精细编辑,适合结构化修改;简单内容追加建议用 `update --mode append`
- `block list/insert/update/delete` 是块级精细编辑,适合结构化修改;只有用户未指定块操作的纯文本追加才建议 `update --mode append`。用户点名 list/insert/update/append 时必须逐项真实调用,不得折叠进 create
- `block insert` 优先使用 `--text` 或 `--heading` 快捷方式;复杂块类型 (table, callout 等) 使用 `--element` JSON
- 用户要求“有序列表块”时必须写真实列表结构(JSONML `p.list.isOrdered=true` 或等价 orderedList element),普通 Markdown/数字前缀段落不算完成
- `--content` 参数中的换行必须使用**真实换行符**(即实际的换行字符,Unicode `U+000A`),而不是字面量字符串 `\n`(反斜杠加字母 n)。在通过程序或大模型构造此参数时,请确保字符串在发送前已正确反转义。如果传入的是两个字符的字面量 `\n`,所有内容将渲染在同一行,导致标题、段落和表格格式全部错乱。**含多行/表格/长文本时优先用 `--content-file path.md` 或 `--content -`(stdin),不经过 shell escape,换行和表格都保持原样**(详见下方「长 Markdown 写入」)。
- 块类型包括: paragraph, heading, blockquote, callout, columns, orderedList, unorderedList, table, sheet, attachment, slot
- 关键区分: doc(文档内容级操作) vs wiki(知识库空间级管理) vs aitable(数据表格操作) vs drive(钉盘文件管理)
- wiki 是知识库容器,doc 是知识库中的文档内容;需要 `workspaceId` 时,先用 `dws wiki space list/search` 获取,再传给 doc 的 `--workspace` 参数
- `doc upload vs drive upload`:用户提到"知识库/文档空间/workspace" → `doc upload`;提到"钉盘/网盘/我的文件" → `drive upload`;未明确目标时默认 `drive upload`
- `upload` 支持上传任意类型文件 (PDF、Office、图片等) 到钉钉文档空间或知识库;`--convert` 可将 Office 文件转换为钉钉在线文档
- `drive upload` / `doc upload` 是普通文件存储路径;用户要求 Word/Excel “在线编辑/直接在线改”时硬路由到 `doc import`,并验证导入后的在线类型和文件夹。只有用户明确同时要原文件与在线版时才分别 upload + import
- 同一请求中新建、复制或导入返回的 `nodeId` 必须绑定后续“这篇/刚才那篇/上次那篇”;禁止搜索同名旧资源覆盖绑定
- `--name` 只是文档外壳标题,不能替代用户显式要求的正文 H1;用户说“正文先起一级标题”时必须写入或插入真实 H1
- `upload` 是三步自动完成的流程 (获取凭证 → OSS 上传 → 提交入库),无需手动分步操作
- `download` 是两步自动完成的流程 (获取下载链接 → HTTP GET 下载),支持自动推断文件名;`--output` 可指定文件路径或目录
- `media insert` 是三步自动完成的流程 (获取附件上传凭证 → OSS 上传 → 插入附件块到文档),无需手动分步操作
@@ -1,15 +1,11 @@
# doc block(块级精细编辑:list / insert / update / delete)
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
> 2. [`./style/doc-update-workflow.md`](./style/doc-update-workflow.md) — 改写流程(编辑形态优先级、JSONML validator 行为)
> 3. [`./format/doc-jsonml-cookbook.md`](./format/doc-jsonml-cookbook.md) — JSONML 范例(含 callout / 分栏 / 表格 / 标题等节点的完整命令)
> 4. [`./format/doc-jsonml-schema.md`](./format/doc-jsonml-schema.md) — JSONML 节点结构字段定义
>
> **同任务常配合**:[`doc-update.md`](./doc-update.md)(整篇 overwrite / 末尾追加纯文本)/ [`./format/doc-jsonml-cookbook.md`](./format/doc-jsonml-cookbook.md)(JSONML 复制范例)
> 本文件自包含简单 list/insert/update/delete 契约,不要递归预读路由或 style reference。只有实际构造复杂 JSONML 节点时,才读取 [`doc-jsonml-cookbook.md`](./format/doc-jsonml-cookbook.md);字段仍不确定时再查 [`doc-jsonml-schema.md`](./format/doc-jsonml-schema.md)。整篇 overwrite 或纯文本 append 才转读 [`doc-update.md`](./doc-update.md)。
> **改写已有文档优先 JSONML**:保真度最高、callout / 分栏 / 表格 / @人 / 附件 / 颜色 / 嵌套都能 1:1 round-trip;写入端有 validator 兜底。详见 [`./style/doc-update-workflow.md` §1.3 编辑形态优先级](./style/doc-update-workflow.md)。
> **显式块操作不可折叠**:用户说“先 create,再 list/insert/update/append”时按原顺序真实调用;不能因为最终正文相似,就把后续块操作合并进 create 或一次 Markdown 写入。
---
## doc block list(查询块元素)
@@ -173,7 +169,8 @@ dws doc block delete --node DOC_ID --block-id UUID
- **块类型**:paragraph、heading、blockquote、callout、columns、orderedList、unorderedList、table、sheet、attachment、slot。
- **快捷 vs --element**:`block insert` 优先使用 `--text` 或 `--heading` 快捷方式;复杂块类型(table、callout、columns 等)使用 `--element` JSON 或 `--content-format jsonml`。
- **简单内容追加**:建议用 [`./doc-update.md`](./doc-update.md) `--mode append`,不必走 block insert。
- **有序列表块**:用户明确要求 ordered list / 有序列表块时,必须用 JSONML `p` 节点的 `list.isOrdered=true`(同一 `listId`;仅首项设 `start:1`)或等价原生 orderedList element;带 `1.` 前缀的普通段落、普通 Markdown 或一次 create 不满足要求。
- **简单内容追加**:用户只说追加纯文本且不强调块操作时可用 [`./doc-update.md`](./doc-update.md) `--mode append`;用户明确说 block insert / 插入段落 / 插入标题 / 插入列表块时必须走 block insert。
- **JSONML validator**(写入端默认行为):
- 裸字符串、缺 uuid 等结构错误会被 validator 抦下并返回带 path 的错误(如 `$[2][2]: paragraph child must be span wrapper, got raw string.`)。
- `--fix-jsonml` 开启 JSON 语法修复,推荐 agent 调用。
@@ -241,6 +238,12 @@ dws doc block list --node <DOC_ID> --content-format jsonml --block-id <UUID>
dws doc block insert --node <DOC_ID> --content-format jsonml --ref-block <UUID> --where after \
--element '["p",{},["span",{"data-type":"text"},["span",{"data-type":"leaf"},"新段落"]]]'
# 插入有序列表块(3 项共用 listId,仅首项有 start)
dws doc block insert --node <DOC_ID> --content-format jsonml \
--element '["p",{"uuid":"ol1","list":{"listId":"actions","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":"ol2","list":{"listId":"actions","level":0,"isOrdered":true}},["span",{"data-type":"text"},["span",{"data-type":"leaf"},"第二项"]]]'
# 插入 callout(colorBlocks)
dws doc block insert --node <DOC_ID> --content-format jsonml --ref-block <UUID> --where after \
--element '["container",{"uuid":"co1","subType":"colorBlocks","metadata":{"bgcolor":"#FDE2E0","border":"#F5C2C7"}},["p",{"uuid":"co1p1"},["span",{"data-type":"text"},["span",{"data-type":"leaf"},"高风险操作,先备份"]]]]'
@@ -1,9 +1,6 @@
# doc comment(文档评论:list / create / reply / update / delete / create-inline)
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
>
> **同任务常配合**:`dws contact user search`(查 `--mention` 用 userId)/ `dws chat search`(查群用 openConversationId)/ [`doc-block.md`](./doc-block.md)(划词评论必须先取 blockId 与 paragraph 文本)
> 本文件自包含评论命令契约。仅在需要 mention 时查询真实 userId/openConversationId;仅在划词评论尚无 blockId 与 paragraph 文本时读取 [`doc-block.md`](./doc-block.md) 并执行 block list。
---
@@ -1,11 +1,6 @@
# doc create(创建文档)
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
> 2. [`./style/doc-create-workflow.md`](./style/doc-create-workflow.md) — 创建工作流(标题、位置、骨架、回读校验)
> 3. [`./style/doc-style-guideline.md`](./style/doc-style-guideline.md) — 排版规范(草稿元素清单、骨架样板)
> 4. [`./doc-update.md` §内容写入管道](./doc-update.md#内容写入管道createupdate-共用) — 长内容自动分片、`--content-file` vs `--content` 选择
> 5. [`./format/doc-jsonml-cookbook.md`](./format/doc-jsonml-cookbook.md) — 仅当使用 `--content-format jsonml` 时必读
> 本文件自包含普通 Markdown 创建契约,不要递归预读 `doc.md`、style 或 update reference。仅当用户要求复杂版式并实际选择 JSONML 时,读取 [`doc-jsonml-cookbook.md`](./format/doc-jsonml-cookbook.md);需要文档骨架建议时才读取对应 style 章节。
## 创建路由前置判断(必看)
@@ -40,7 +35,7 @@ Flags:
## 关键说明
- **`--name` 是 H1**:正文从 `##` 开始;正文内不要再写 `#` 一级标题(除非确需且已说明动机)。
- **标题优先级**:`--name` 是文档外壳标题,默认可视作 H1;但它不能替代用户显式要求的正文一级标题。用户说“正文写 `# ...`”“正文先起个一级标题”时,必须在初始内容中保留该 `#` H1;用户未要求正文 H1 时,正文默认从 `##` 开始以避免重复。
- 不传 `--folder` 和 `--workspace` 时,默认创建在「我的文档」根目录。
- `--folder` 仅接受文档文件夹 `nodeId` / `dentryUuid` / alidocs 文件夹 URL;**禁止**传入 drive `dentryId`、`parentId`、`spaceId` 这类纯数字 ID。
- 输入方式选择见 [`./doc-update.md` §内容写入管道](./doc-update.md#内容写入管道createupdate-共用)(与 update 共用)。短文本字面量可 `--content`,多行/表格/特殊字符必须 `--content-file` 或 `--content -`。
@@ -54,6 +49,12 @@ Flags:
| `docUrl` | 最终交付给用户的链接;缺失时用 [`./doc-info.md`](./doc-info.md) 补查 |
| `chunksWritten` | 判断是否触发自动分片;> 1 时重点检查章节顺序 |
同一请求后续出现“这篇/刚才那篇/上次那篇”时,直接续用本次 create 返回的 `nodeId`;禁止先搜索同名文档再把后续操作指向旧节点。
## 显式操作序列
用户点名 `block list`、插入、追加、更新等后续动作时,必须按原顺序逐项执行。`doc create` 只写用户指定的初始内容,不能为了减少调用把后续标题、列表或段落提前塞进 create。例:`创建 → 查看块结构 → 末尾插入段落` 必须真实执行 create、block list、block insert 三步。
## 回读验收(必读)
CLI **不会**自动回读校验。**每次创建后**都必须执行 `doc read --node <nodeId>` 校验关键标题、段落首句、表格表头是否完整。详见 [`./style/doc-create-workflow.md` «回读验收»](./style/doc-create-workflow.md)。
@@ -1,7 +1,6 @@
# doc export(在线文档导出为 docx)
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
> 本文件自包含 export 契约。已有当前请求返回的 adoc nodeId 时直接导出;目标类型未知时只执行一次 info 检查,不要递归读取 `doc.md`。
> **路由前置判断**:用户说「下载/导出」时**必须**先用 [`./doc-info.md`](./doc-info.md) `info --node <ID> --format json` 查 `contentType`:
> - `contentType` 为 `ALIDOC`(在线文档)→ **必须用 `export`**,禁止用 `download`
@@ -45,6 +44,7 @@ Flags:
## 关键说明
- 同一请求中刚执行 create/copy/import 并紧接着说“这篇/刚才那篇/上次那篇”时,`--node` 必须使用该写操作真实返回的新 `nodeId`;不得预先搜索同名文档,也不得用搜索结果中的旧节点替换它。
- `export` 是一体化命令,一条命令自动完成提交→轮询→下载,**无需手动编排轮询**。CLI 内部使用渐进式退避轮询(最多约 5 分钟)。
- `export` 超时或中断后,CLI 会输出 `jobId`,可用 `dws doc export get --job-id <jobId>` 手动查询任务状态。
- `export` 当前仅支持钉钉在线文档(alidocs,`contentType=ALIDOC`)导出为 `docx`,**在线表格导出请使用其他命令**。
@@ -1,7 +1,6 @@
# doc 文件操作(upload / download / copy / move / rename / delete + folder create)
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
> **按需使用**:本文件自包含弃用命令的兼容说明,不要求先读总路由;优先按下方提示改用 `drive` / `wiki`。
> **弃用提示(文件管理命令正在迁移到 drive / wiki)**:本文所列 `doc` 文件管理命令虽仍能跑,但执行时会打印弃用警告,请优先改用 `drive` / `wiki` 对应命令:
> - `doc download` → **`dws drive download`**(下载已有文件;在线文档导出 docx 仍走 `doc export`)
@@ -32,6 +31,7 @@ Flags:
- `upload` 是三步自动完成的流程(获取凭证 → OSS 上传 → 提交入库),无需手动分步操作。
- 支持上传任意类型文件(PDF、Office、图片等)到钉钉文档空间或知识库。
- `--convert` 可将 Office 文件转换为钉钉在线文档。
- **在线编辑硬路由**:用户说“大家直接在线改/上传后在线编辑/转成钉钉文档”时使用 [`./doc-import.md`](./doc-import.md) `doc import`,并回查在线类型;普通 `doc/drive upload` 只用于保留文件,不能据此承诺可在线编辑。
- **`doc upload` vs `drive upload`**:用户提到「知识库 / 文档空间 / workspace」→ `doc upload`;提到「钉盘 / 网盘 / 我的文件」→ `drive upload`;未明确目标时默认 `drive upload`。
- 与 [`./doc-media.md`](./doc-media.md) `media insert` 的区别:`upload` 上传到文档空间作为**独立文件**;`media insert` 作为**附件块插入到文档正文中**。
@@ -6,6 +6,8 @@
不要先读取文件内容再调用 `doc create` 或 `doc update`。`doc import` 会按文件格式走导入任务,保留更完整的原始结构。
> **在线编辑硬路由**:用户说“上传后在线编辑/大家直接在线改/转成钉钉文档”时必须使用 `doc import`。`drive upload` 只保留原始 `.docx/.xlsx/...` 普通文件,不能据此宣称已可在线编辑。若用户明确要同时保留原文件和在线版,才先 `drive upload`,再单独 `doc import`,并分别验证两个返回节点。
## 命令
```bash
@@ -38,6 +40,7 @@ dws doc import get --task-id <TASK_ID> --format json
3. 执行 `dws doc import --file ... --format json`。
4. 正常情况下 CLI 会自动提交、上传并轮询导入任务。
5. 如果命令超时或中断,从输出中提取 `taskId`,再执行 `dws doc import get --task-id <TASK_ID> --format json`。
6. 用返回的 `documentUrl`/`nodeId` 执行 `drive info` 或 `doc info`,确认在线类型和目标文件夹;验证通过后才能说“可直接在线编辑”。
## 上下文传递
@@ -1,8 +1,6 @@
# doc info(获取文档元信息 + URL 解析)
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
> 2. [`../../url-patterns.md`](../../url-patterns.md) — 仅当用户原始 `alidocs` URL 需要 probe 时
> 本文件自包含已知 nodeId/URL 的 info 契约。只有原始 alidocs URL 类型仍不明确时,才读取 [`url-patterns.md`](../../url-patterns.md);不要递归读取 `doc.md`。
>
> **同任务常配合**:`dws drive search` / `dws wiki node search`(先定位 nodeId)/ [`doc-read.md`](./doc-read.md)(确认是 ALIDOC 后读正文)
@@ -1,7 +1,6 @@
# doc media(附件 / 图片:download / insert)
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
> 本文件自包含 media insert/download 契约。nodeId、文件路径或 resourceId 已知时直接执行;只在需要相对块定位且 blockId 未知时读取 [`doc-block.md`](./doc-block.md)。
> ⚠️ **图片插入硬规则**:
> - 图片来源如果是钉盘/文档空间中的文件,**必须先下载到本地**(`dws drive download --node <图片nodeId> --output /tmp/xxx.png`),再执行 `media insert`
@@ -1,7 +1,6 @@
# doc permission(文档权限:add / update / list)
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
> **按需使用**:本文件自包含文档节点权限命令;不要求先读总路由。知识库整体成员权限改读 `dingtalk-wiki`。
> **关键区分**:
> - "把**某篇文档**授权给某人" → `doc permission add`(节点级,包括「我的文档」下的文档都支持)
@@ -1,10 +1,6 @@
# doc read(读取文档内容)
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
> 2. [`./format/doc-jsonml-cookbook.md`](./format/doc-jsonml-cookbook.md) — 仅当使用 `--content-format jsonml` 时必读
>
> **同任务常配合**:[`doc-info.md`](./doc-info.md)(先解析 URL,确认 contentType=ALIDOC、extension=adoc)/ [`doc-update.md`](./doc-update.md)(读后改写)/ [`doc-block.md`](./doc-block.md)(块级精修前先读结构)
> 本文件自包含普通 read 契约。用户已给当前 adoc nodeId/URL 时直接读取;类型未知时才先执行 info。选择 JSONML 只为获取结构,不要求预读 cookbook;实际构造 JSONML 写入时再按需加载。
## 命令格式
@@ -1,12 +1,6 @@
# doc update(更新文档内容)
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
> 2. [`./style/doc-update-workflow.md`](./style/doc-update-workflow.md) — 改写流程(编辑形态优先级、分片 append、回读验收)
> 3. [`./style/doc-style-guideline.md`](./style/doc-style-guideline.md) — 排版规范
> 4. [`./format/doc-jsonml-cookbook.md`](./format/doc-jsonml-cookbook.md) — 仅当使用 `--content-format jsonml` 时必读
>
> **同任务常配合**:[`doc-read.md`](./doc-read.md)(改写前必读,jsonml 模式拿当前结构;担心被并发覆盖时再取 revision)/ [`doc-block.md`](./doc-block.md)(单 block 改写优先;本命令更适合追加 / 整篇 overwrite)
> 本文件自包含普通 append/overwrite 契约,不要递归预读路由或 style reference。纯文本 append 可直接执行;overwrite 先 read/dry-run/确认。只有保真改写或复杂 JSONML 才读取 [`doc-update-workflow.md`](./style/doc-update-workflow.md) 与 cookbook;单块修改改用 [`doc-block.md`](./doc-block.md)。
## 命令格式
@@ -4,11 +4,9 @@
> 改写已有文档见 [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 等)
普通 Markdown 创建不需要先读本文件或 style guideline。只有用户要求设计文档骨架或复杂版式时,才查看下方对应章节;实际选择 JSONML 后再读取 cookbook。需要按文档类型选骨架时,按需读取 [doc-style-guideline.md](./doc-style-guideline.md) 的对应一节,不要通读。
### 关键词速查(用户意图 → 起稿路径)
@@ -45,7 +43,7 @@
| 项目 | 要求 |
|------|------|
| 标题 | 用 `--name` 传入;正文不要再重复同名一级标题 |
| 标题 | 用 `--name` 传入;用户显式要求正文 H1 时原样保留,未要求时正文默认从 H2 开始 |
| 位置 | 默认创建到我的文档;指定目录时只接受文档文件夹 `nodeId` 或 alidocs 文件夹 URL |
| 正文 | 多行、表格、代码块、特殊字符或长度 >= 2KB 时必须写入 UTF-8 临时 `.md` 文件 |
| 格式 | 按 §JSONML 起稿判定 决定起稿路径:命中 JSONML 起稿条件时**直接用 JSONML 构造**(跳过 markdown);未命中时用 Markdown 起稿,创建后按 [doc-update-workflow.md](./doc-update-workflow.md) 精修 |
@@ -226,7 +224,7 @@ callout 可通过 `"showstk": true, "sticker": "图标名"` 配置顶部贴纸
> **脚手架策略警示**:Markdown 无法表达分栏/callout/色彩表头,拉回的 JSONML 只有纯文本骨架。精修阶段不是“在现有结构上加色”,而是“参照 RFC/Spec 重组结构”。
> **MUST READ**:动手写 JSONML 前,必须先用 Read 工具读取 [doc-jsonml-cookbook.md](../format/doc-jsonml-cookbook.md) — 其中 §决策型文档骨架范例 有可直接复制修改的完整模板。
> **JSONML 条件加载**:仅在确定使用 JSONML 后读取 [doc-jsonml-cookbook.md](../format/doc-jsonml-cookbook.md);其中“决策型文档骨架范例”可直接改写。
> 节点类型和属性的权威定义见 [doc-jsonml-schema.md](../format/doc-jsonml-schema.md)。
### ⚠️ JSONML 降级约束
@@ -274,7 +272,7 @@ callout 可通过 `"showstk": true, "sticker": "图标名"` 配置顶部贴纸
```
- 根节点固定 `"root"`(不是 `"body"`)
- `--name` 已是 H1,JSONML 从 `h2` 开始
- 用户未要求正文 H1 时,JSONML 从 `h2` 开始;用户明确要求“正文一级标题/插入一级标题”时必须构造 `h1`
- 表格结构是 `table → tr → tc`(无 `th`/`td`)
- 分栏是 `table` + `"sr": true`,`tc` 建议设 `fill` 背景色
- 有序列表:仅第一项设 `"start": 1`,后续项不设 `start`(系统自动递增)
@@ -315,7 +313,7 @@ dws doc read --node <nodeId> --content-format jsonml --output /tmp/<name>-readba
- 只使用用户已提供或对话中已确认的正文素材。
- 如果正文素材不足,先补齐文档目标、受众、章节和缺口;不要在本文中临时扩展跨产品采集流程。
- **先按 [doc-style-guideline.md §2.0 类型判断决策表](./doc-style-guideline.md) 确定文档类型,再用对应类型的骨架样板(§2.1 决策型 / §2.2 执行型 / §2.3 说明型 / §2.4 知识沉淀型)**。不要套通用三段式。
- **`--name` 已是 H1,正文从 `##` 开始**;正文内不要再写 `#` 一级标题(除非确实需要正文内再造一级 H1 并说明动机)。
- **`--name` 是外壳标题,不覆盖显式正文 H1**:用户未要求正文一级标题时从 `##` 开始;用户明确给出 `# ...` 或要求“先起一级标题”时,正文必须保留该 H1。
- 摘要、bullet、引用块、callout 等元素的使用边界以 style-guideline §3-§7 为准。
- 同类信息保持一致:风险、状态、行动项各用一种元素 + 一种视觉语义(style-guideline §1.2 / §5)。
- 临时文件必须保留真实换行,不能把换行写成字面量 `\n`。
@@ -25,7 +25,7 @@
## 一、硬规则
1. **`--name` 是 H1**:正文从 `##` 开始;正文内不写 `#`(除非确需正文内再造一级 H1 并说明动机)
1. **用户显式正文 H1 优先**:`--name` 是文档外壳标题;用户未要求正文一级标题时从 `##` 开始。用户明确说“正文写 `# ...` / 先起一级标题 / 插入一级标题”时必须保留或插入真实 H1,不得用 `--name` 代替
2. **同类信息同表达**:风险、状态、行动项、证据,每类只用一种元素 + 一种视觉语义(见 §5)
3. **Markdown 草稿阶段只用稳定元素**:标题、段落、列表、checklist、表格、代码块;callout / 分栏 / 附件 / 复杂嵌套留到创建后用 `doc block insert` / `doc media insert` 精修
4. **引用块只用于原文**:用户原话、会议摘录、外部材料原文;不许包装作者自己的结论
@@ -209,7 +209,7 @@
### 4.1 标题与段落
- 正文从 `##` 开始(H1 已被 `--name` 占用)
- 用户未要求正文 H1 时从 `##` 开始;用户明确要求正文 H1 时按原文使用 `#` 或 heading level 1
- 标题层级 ≤ 4 层(§7)
- 单段过长先拆段,再考虑换元素
@@ -217,6 +217,7 @@
- 普通列表:并列要点
- 有序列表:顺序步骤
- 用户明确要求“有序列表块”时必须使用真实列表结构;JSONML 为带 `list.isOrdered=true` 的多个 `p` 节点,不能只写带数字前缀的普通段落或以整篇 Markdown 代替显式 block insert
- checklist:待办状态(含 `- [ ]` / `- [x]`)
列表项里开始出现"负责人 / 截止时间 / 状态"这类字段时,改用表格。
+150 -152
View File
@@ -1,181 +1,179 @@
#!/usr/bin/env python3
"""
在指定目录创建文档并写入 Markdown 内容(一键完成)
"""用原生 dws 写入管道创建文档,并回读验证。"""
用法:
python doc_create_and_write.py \
--name "项目周报" \
--content "# 本周总结\n\n## 完成事项\n- 任务A"
from __future__ import annotations
python doc_create_and_write.py \
--name "会议纪要" \
--content-file notes.md
python doc_create_and_write.py \
--name "知识库文档" --content "# 内容" --folder FOLDER_ID
python doc_create_and_write.py --name "test" --content "hello" --dry-run
"""
import sys
import json
import time
import subprocess
import argparse
import json
import shlex
import subprocess
import sys
import tempfile
from pathlib import Path
from typing import List, Any, Optional
from typing import Any, Optional, Sequence
def run_dws(
args: List[str], dry_run: bool = False,
) -> Optional[Any]:
cmd = ['dws'] + args
class ScriptError(RuntimeError):
"""可预期的脚本执行错误。"""
def decode_json_output(output: str) -> Any:
"""解析 JSON;兼容长内容写入前置的进度行。"""
text = output.strip()
if not text:
raise ScriptError("dws 未返回 JSON")
try:
return json.loads(text)
except json.JSONDecodeError:
decoder = json.JSONDecoder()
for offset, character in enumerate(text):
if character not in "[{":
continue
try:
value, end = decoder.raw_decode(text, offset)
except json.JSONDecodeError:
continue
if not text[end:].strip():
return value
raise ScriptError("dws 返回的不是合法 JSON")
def run_dws(args: Sequence[str], dry_run: bool = False) -> Any:
"""执行一条 dws 命令,并把命令/业务失败统一转成 ScriptError。"""
command = ["dws", *args]
if dry_run:
print(f"[dry-run] {' '.join(cmd)}")
return {'dry_run': True}
print(f"[dry-run] {shlex.join(command)}")
return {"dry_run": True}
try:
result = subprocess.run(
cmd, capture_output=True, text=True, timeout=60
command,
capture_output=True,
text=True,
timeout=120,
check=False,
)
if result.returncode != 0:
print(f" ✗ 错误:{result.stderr.strip()}")
return None
return json.loads(result.stdout)
except (subprocess.TimeoutExpired, json.JSONDecodeError,
FileNotFoundError) as e:
print(f" ✗ 错误:{e}")
return None
except (subprocess.TimeoutExpired, FileNotFoundError) as exc:
raise ScriptError(f"执行 dws 失败:{exc}") from exc
if result.returncode != 0:
detail = result.stderr.strip() or result.stdout.strip()
raise ScriptError(
f"dws 命令失败:{detail or f'退出码 {result.returncode}'}"
)
data = decode_json_output(result.stdout)
if isinstance(data, dict) and data.get("success") is False:
detail = data.get("errorMsg") or data.get("message") or "未知错误"
raise ScriptError(f"dws 业务调用失败:{detail}")
return data
def run_dws_with_retry(
args: List[str],
dry_run: bool = False,
max_retries: int = 3,
retry_delay: float = 1.0,
) -> Optional[Any]:
"""带重试机制的 dws 命令执行"""
last_error = None
for attempt in range(1, max_retries + 1):
result = run_dws(args, dry_run=dry_run)
if result is not None:
return result
if attempt < max_retries:
print(f" ⚠️ 第 {attempt} 次尝试失败,{retry_delay}秒后重试...")
time.sleep(retry_delay)
retry_delay *= 1.5 # 指数退避
return None
def first_value(payload: Any, keys: Sequence[str]) -> str:
"""从嵌套响应中提取第一个非空稳定字段。"""
if isinstance(payload, dict):
for key in keys:
value = payload.get(key)
if value is not None and str(value).strip():
return str(value).strip()
for value in payload.values():
found = first_value(value, keys)
if found:
return found
elif isinstance(payload, list):
for value in payload:
found = first_value(value, keys)
if found:
return found
return ""
def main():
def run(argv: Optional[Sequence[str]] = None) -> int:
parser = argparse.ArgumentParser(
description='创建文档并写入内容'
description="使用 dws doc create 创建文档并回读验证"
)
parser.add_argument('--name', required=True, help='文档名称')
parser.add_argument('--content', default='', help='Markdown 内容')
parser.add_argument('--content-file', default='', help='内容文件')
parser.add_argument('--folder', default='', help='目标文件夹 ID 或 URL')
parser.add_argument('--workspace', default='', help='目标知识库 ID')
parser.add_argument(
'--mode', default='append', choices=['overwrite', 'append'],
help='写入模式: overwrite=覆盖, append=追加 (默认 append)',
parser.add_argument("--name", required=True, help="文档名称")
content_group = parser.add_mutually_exclusive_group(required=True)
content_group.add_argument("--content", help="Markdown 内容")
content_group.add_argument("--content-file", help="UTF-8 Markdown 文件")
location_group = parser.add_mutually_exclusive_group()
location_group.add_argument(
"--folder", default="", help="目标文档文件夹 ID 或 URL"
)
parser.add_argument(
'--max-retries', type=int, default=3,
help='每块写入失败时的最大重试次数 (默认 3)',
location_group.add_argument(
"--workspace", default="", help="目标知识库 ID 或 URL"
)
parser.add_argument('--dry-run', action='store_true')
args = parser.parse_args()
parser.add_argument("--dry-run", action="store_true")
args = parser.parse_args(argv)
content = args.content
supplied_path: Optional[Path] = None
temporary_path: Optional[Path] = None
if args.content_file:
p = Path(args.content_file)
if not p.exists():
print(f"错误:文件不存在: {p}")
sys.exit(1)
content = p.read_text(encoding='utf-8')
if not content:
print('错误:需要 --content 或 --content-file')
sys.exit(1)
chunk_size = 30000
supplied_path = Path(args.content_file)
if not supplied_path.is_file():
raise ScriptError(f"内容文件不存在:{supplied_path}")
elif not args.content or not args.content.strip():
raise ScriptError("--content 不能为空")
create_args = ['doc', 'create', '--name', args.name, '--format', 'json']
if args.folder:
create_args.extend(['--folder', args.folder])
if args.workspace:
create_args.extend(['--workspace', args.workspace])
try:
if supplied_path is None and not args.dry_run:
with tempfile.NamedTemporaryFile(
mode="w", encoding="utf-8", suffix=".md", delete=False
) as handle:
handle.write(args.content)
temporary_path = Path(handle.name)
supplied_path = temporary_path
print(f'\n📝 创建文档: {args.name}')
create_data = run_dws(create_args, dry_run=args.dry_run)
content_path = str(supplied_path) if supplied_path else "<TEMP_CONTENT.md>"
create_args = [
"doc", "create",
"--name", args.name,
"--content-file", content_path,
"--content-format", "markdown",
"--format", "json",
]
if args.folder:
create_args.extend(["--folder", args.folder])
if args.workspace:
create_args.extend(["--workspace", args.workspace])
node_id = None
if not args.dry_run:
if not create_data:
sys.exit(1)
node_id = (create_data.get('nodeId')
or create_data.get('dentryUuid')
or create_data.get('id', ''))
print(f" ✓ 文档已创建 (ID: {node_id})")
created = run_dws(create_args, dry_run=args.dry_run)
node_id = "<NODE_ID>" if args.dry_run else first_value(
created, ("nodeId", "dentryUuid")
)
if not node_id:
raise ScriptError("文档创建响应缺少 nodeId,无法验证")
if len(content) <= chunk_size:
mode_label = '追加' if args.mode == 'append' else '覆盖'
print(f'\n✍️ 写入内容 (模式: {mode_label}, {len(content)} 字符)...')
write_data = run_dws([
'doc', 'update',
'--node', node_id or '<NODE_ID>',
'--content', content,
'--mode', args.mode,
'--format', 'json',
], dry_run=args.dry_run)
if write_data:
print(f" ✓ 内容已写入 ({len(content)} 字符)")
else:
chunks = []
pos = 0
while pos < len(content):
end = min(pos + chunk_size, len(content))
if end < len(content):
newline_pos = content.rfind('\n', pos, end)
if newline_pos > pos:
end = newline_pos + 1
chunks.append(content[pos:end])
pos = end
info = run_dws(
["doc", "info", "--node", node_id, "--format", "json"],
dry_run=args.dry_run,
)
run_dws(
["doc", "read", "--node", node_id, "--format", "json"],
dry_run=args.dry_run,
)
if args.dry_run:
return 0
total_chunks = len(chunks)
print(f'\n✍️ 内容较长 ({len(content)} 字符), 分 {total_chunks} 块写入...')
success_chunks = 0
for idx, chunk in enumerate(chunks):
chunk_mode = args.mode if idx == 0 else 'append'
write_data = run_dws_with_retry(
[
'doc', 'update',
'--node', node_id or '<NODE_ID>',
'--content', chunk,
'--mode', chunk_mode,
'--format', 'json',
],
dry_run=args.dry_run,
max_retries=args.max_retries,
)
if write_data:
print(f" ✓ 块 {idx + 1}/{total_chunks} 已写入 ({len(chunk)} 字符)")
success_chunks += 1
elif not args.dry_run:
# 写入失败,报告部分写入状态
print(f"\n❌ 块 {idx + 1}/{total_chunks} 写入失败(已重试 {args.max_retries} 次)")
print(f"\n⚠️ 文档处于部分写入状态:")
print(f" - 文档 ID: {node_id}")
print(f" - 已写入: {success_chunks}/{total_chunks} 块")
print(f" - 失败位置: 第 {idx + 1} 块")
if args.mode == 'overwrite':
print(f" - 模式: 覆盖模式,文档可能包含不完整内容")
print(f" - 建议: 手动检查文档内容,或删除后重新创建")
else:
print(f" - 模式: 追加模式,已写入内容已保存")
print(f" - 建议: 可手动补充剩余内容,或重新运行脚本")
sys.exit(1)
print('\n✅ 完成!')
summary = {
"success": True,
"nodeId": node_id,
"docUrl": first_value(info, ("docUrl", "documentUrl", "url"))
or first_value(created, ("docUrl", "documentUrl", "url")),
"chunksWritten": first_value(created, ("chunksWritten",)),
"verified": True,
}
print(json.dumps(summary, ensure_ascii=False))
return 0
finally:
if temporary_path is not None:
temporary_path.unlink(missing_ok=True)
if __name__ == '__main__':
def main() -> None:
try:
raise SystemExit(run())
except ScriptError as exc:
print(f"错误:{exc}", file=sys.stderr)
raise SystemExit(1) from exc
if __name__ == "__main__":
main()
+19 -33
View File
@@ -11,11 +11,9 @@ metadata:
# 钉钉文档 Skill
## Preconditions
## 执行入口
> **CRITICAL — Before any `dws` operation, MUST fully read [`dws-shared`](../dws-shared/SKILL.md).** It defines the global execution contract, safety floor, and on-demand shared-reference routing. Do not preload all references.
> Atomic command router: [doc.md](references/doc.md); document workflows: [04-document.md](references/04-document.md).
执行前完整读取 [`dws-shared`](../dws-shared/SKILL.md)。高频意图用本文件骨架;仅特殊参数、复杂格式或边界不明时读取一个 branch reference。优先级:`骨架/recipe > Shortcut > atomic fallback`。
<!-- VISIBLE_SHORTCUTS_START -->
## Shortcut 发现(按需)
@@ -25,11 +23,7 @@ metadata:
仅当现有路由和 reference 都无法定位低频能力时,才执行 `dws shortcut list --service doc --compact --format json` 做最后回退;不要为已知高频意图加载完整 Shortcut Catalog 或产品级 Schema。
<!-- VISIBLE_SHORTCUTS_END -->
## Atomic 回退与执行契约
没有 Shortcut 时,才按需读取 [doc.md](references/doc.md) 指向的一个 atomic branch reference。路由优先级为 `已评审直接骨架/精确 recipe > 匹配的公开 Shortcut > atomic fallback`。`doc_create_and_write.py` 只在用户明确需要可复用本地包装器且 `python3` 可用时使用;普通创建直接调用 `dws doc create`。
命令确定且参数清楚时直接执行,不重复发现。只使用真实 `cli_path`,不猜参数。`confirmation=user_required` 时先确认,再添加 `--yes`;来源冲突时采用更安全解释并报告契约漂移。
命令和参数清楚时直接执行。只用真实 `cli_path`;`confirmation=user_required` 时先确认再加 `--yes`。普通创建直接调用 `dws doc create`,仅用户要求本地包装器时用 `doc_create_and_write.py`。
## 核心对象、位置与格式
@@ -40,28 +34,38 @@ metadata:
| 块 | `blockId` / JSONML `uuid` 必须来自 `block list`,更新节点的 uuid 必须与目标块一致 |
| 评论 | `commentKey` 来自评论 list/create;划词评论还需同一块的真实 `start/end` |
| 异步任务 | 导出 `jobId` 与导入 `taskId` 只查询对应任务,不能替代 nodeId |
| 新建资源续用 | create/mkdir/import/copy 返回的新 `nodeId` / `fileId` 立即绑定后续“这篇/刚才那篇/这个文件夹”;禁止同名搜索改用旧资源 |
| 内容格式 | Markdown 适合线性正文;已有富结构优先 JSONML/块级编辑,禁止用 Markdown overwrite 误称保真 |
| 普通文件 | adoc 才用 `doc read/export`;`.md`、axls、able 和普通文件按真实 `extension` 切对应 Skill |
## 核心意图与执行骨架
所有结构化命令加 `--format json`,下游 ID 只取真实输出。创建、更新、块、附件和样式写入后回读;返回成功但未回读,不能宣称内容完整。
结构化命令加 `--format json`,ID 只取真实输出。写入后回读;未回读不能宣称内容完整。
### 短链路 Fast Path
- 不超过 5 个确定性 DWS 操作时,不创建 Todo、不逐步汇报、不预读 Reference;保存真实 ID 连续执行,最终回查后答复。
- 按“先/再/然后”切分操作阶段。阶段中的 `insert/插入`、`append/追加/补一段`、`update/改成`、`list/查看块`、`delete/删除` 必须映射为对应真实命令,不能提前折叠进 create。
- create 只承载首阶段的初始正文,后续续用其 `nodeId`。当前请求将先创建资源时,禁止预先搜索同名资源解析“这篇/那篇”。
| 用户意图 | 精确骨架 | 必须保留的执行边界 |
|---|---|---|
| 按名称找文档 | `+find-doc --query <关键词>`;需最近访问/扩展名/创建者等过滤用 `+search` | 候选不唯一先消歧;随后 `drive info --node <nodeId>` 判 `extension` |
| 读取 adoc | `drive info --node <nodeId>` → `doc read --node <nodeId>` | 用户已给 nodeId/URL 时不再搜索;非 adoc 不调用 `doc read` |
| 创建文档 | `doc create --name <标题> --content-file <tmp.md> [--folder <folder> | --workspace <ws>]` | 原生写入管道自动分片;取 `nodeId` 后 `doc read`,缺链接再 `doc info` |
| 显式块工作流 | 按用户原顺序执行 `create → block insert/list/update/delete` | 每个阶段是真实调用;标题、段落、列表等显式插入走 block insert |
| 末尾补短文本 | `+doc-append --doc <nodeId> --text <内容>` | 该 Shortcut 为 write/user_required;确认后执行并 `doc read` 核对 |
| 改写正文 | `doc read --content-format jsonml` → `block update` 或 `doc update --content-format jsonml --mode overwrite` | 单块优先块级编辑;整篇 overwrite 先预览/确认,Markdown overwrite 不保富结构 |
| 评论与回复 | `+comment-list --node <nodeId>` → `+comment-create` / `+comment-reply` | `commentKey` 来自真实结果;写 Shortcut 先确认;划词评论走 atomic `comment create-inline` |
| 导入 / 导出 | `doc import --file <path> ...` / `doc export --node <nodeId> --export-format <fmt> --output <path>` | 一体化命令优先;仅超时/中断后用 `import get` / `export get` |
| 导入 / 导出 | `doc import --file <path> ...` / `doc export --node <nodeId> --export-format <fmt> --output <path>` | Word/Excel 等本地文件要求“在线编辑/转在线文档”必须 import;drive upload 只保留普通文件。仅超时/中断后用 `import get` / `export get` |
| 版本操作 | `+version-list --node <nodeId>` → `+version-save` / `+version-revert --version <N>` | save/revert 先确认;revert 版本号必须来自 list,完成后回读 |
| 模板创建 | `+template-list` / `+template-search --query <词>` → atomic `template apply --template-id <id>` | templateId 来自真实列表;要复刻已有文档形态时用 drive copy + 副本块级更新 |
| 分享链接给某人 | `+share-doc --to <姓名> --url <docUrl> [--note <附言>]` | 会真实发消息,确认后执行;同名人员必须消歧,不改变文档权限 |
## 写入与验证边界
- `--name` 是文档外壳标题,但不能覆盖用户显式要求的正文 H1。用户说“正文写 `# ...` / 先起一级标题 / 插入一级标题”时,必须原样创建正文 H1;只有用户未要求正文 H1 时才默认从 H2 开始以避免重复。
- 用户显式列出的操作是验收步骤:create 只能承载明确要求的初始内容;后续 `list`、`insert`、`append`、`update` 必须逐项真实调用。若要求“有序列表块”,必须写入 JSONML `p.list.isOrdered=true`(或等价原生列表块),普通 Markdown/普通段落不算完成。
- 创建只用 `--name`;内容只用 `--content` / `--content-file`。长、多行、表格或特殊字符必须用临时 UTF-8 文件和 `--content-file`。
- 原生 Markdown 写入管道在内容超过 10,000 个 Unicode 字符时自动按结构分片;不要在 Skill 或脚本中预先复制分片循环。仅在 `CONTENT_TRUNCATED`、中断或回读缺失时按 [04-document.md](references/04-document.md) 恢复。
- `doc update --mode append` 不清空原文;`--mode overwrite` 会清空后重写,先 `--dry-run`,得到确认后才加 `--yes`。
@@ -69,35 +73,17 @@ metadata:
- `block insert` 默认追加;只有明确相对位置时才传真实 `--ref-block` / `--parent-block`。`block delete` 和评论删除必须确认。
- 写后按对象验证:正文用 `doc read`,块/附件用 `doc block list`,元信息/链接用 `doc info`,版本用 `version list`。
## 低频 atomic 路由
## 低频 Reference
```text
dws doc
├── info / read
├── create / update
├── block # list, insert, update, delete
├── comment # list, create, reply, update, delete, create-inline
├── media # insert, download
├── import / export
├── template / version / style
├── +shortcut
└── deprecated file operations → drive/wiki
```
按需读取,不要预加载:定位与 URL 边界读 [doc-info.md](references/doc/doc-info.md);创建/改写读 [doc.md](references/doc.md) 的场景索引所列文件组;块、评论、附件、导入、导出分别读对应 branch reference。JSONML schema/cookbook 仅在实际选择 JSONML 写入后加载。
[doc.md](references/doc.md) 只是 atomic 分支索引。每次只读一个对应 branch reference;JSONML workflow/cookbook/schema 仅在构造复杂 JSONML 后加载,不递归预读。
## 错误恢复
- 路径或参数错误:按既定顺序查 leaf Schema、再查 leaf Help,校正一次;不要连续尝试近似参数。
- 始终从实际输出重新提取 `nodeId`、`blockId`、`commentKey`、`jobId` 或 `taskId`。
- 始终从实际输出提取并续用 `nodeId`、`blockId`、`commentKey`、`jobId` 或 `taskId`,不得搜索同名项覆盖当前请求的新 ID。
- 部分写入或回读缺失:保留已创建的 nodeId,报告已完成范围与缺失位置;先读回,再只补缺失内容,禁止无条件重新创建副本。
- 权限不足、候选未消歧、目标类型不符、没有可推进的任务 ID 或 Schema/Help 冲突时停止并报告。
## 跨产品协作
- 原生 `.md` 文件内容 → `dingtalk-markdown`。
- 普通文件存储、搜索、复制、移动、重命名、删除、上传下载与权限 → `dingtalk-drive`。
- 知识库空间、节点和成员管理 → `dingtalk-wiki`;`doc create --workspace` 可直接在已知知识库根创建带内容的 adoc。
- 在线电子表格 / AI 表格 → `dingtalk-sheet` / `dingtalk-aitable`。
- 人名消歧 → `dingtalk-aisearch` 或 `dingtalk-contact`;文档评论 mention 使用真实 userId。
- 局部意图边界见 [intent-guide.md](references/intent-guide.md),固定短流程见 [lite-recipes.md](references/lite-recipes.md)。
`.md` 走 `dingtalk-markdown`;普通文件走 `dingtalk-drive`;知识库走 `dingtalk-wiki`;表格走 `dingtalk-sheet` / `dingtalk-aitable`;评论 userId 走人员 Skill。边界读 [intent-guide.md](references/intent-guide.md),固定流程读 [lite-recipes.md](references/lite-recipes.md)。
@@ -5,7 +5,7 @@
| Recipe | 行动指南(固定路线) |
|--------|-------------------|
| import-file | 1. **直接执行** `dws doc import --file <本地文件路径> --format json`(一条命令完成上传+转换+创建)<br>2. 从返回中提取 `documentUrl` 并告知用户<br>3. **禁止先 Read 文件内容再 `doc create` + `doc update`**——`doc import` 是服务端格式转换,客户端无需解析文件内容<br>4. 可选参数:`--folder <文件夹ID>` 指定目标文件夹、`--workspace <知识库ID>` 指定目标知识库、`--name "文档名"` 自定义名称<br>5. 格式映射:docx/doc→文档, xlsx/xls→表格, xmind/mark→脑图, md/txt→文档<br>6. 超时或中断时 CLI 返回 `taskId`,用 `dws doc import get --task-id <taskId>` 手动查询<br>详见 [doc-import.md](./doc/doc-import.md) |
| write-doc | 0. 阅读 [doc-create-workflow.md](./doc/style/doc-create-workflow.md) 的 §前置必读 + §关键词速查表,锁定文档类型和起稿路径:**决策型/含对比的知识沉淀型/用户要求美观 → JSONML 起稿**(`.json`);执行型/说明型 → Markdown 起稿(`.md`)<br>1. 按选定路径执行 doc-create-workflow.md(JSONML 路径有骨架范例可直接复制修改)<br>2. `doc create --content-file /tmp/<name>.json --content-format jsonml`(或 `.md` + `--content-format markdown`)<br>3. 大内容默认依赖 DWS 自动分片;只有 `CONTENT_TRUNCATED`、部分写入失败或回读发现缺失时,才按 workflow 的恢复流程手工补片<br>4. **回读校验(必须)**:所有写入完成后,执行 `doc read --node <nodeId>`,校验关键标题/段落/表格是否完整写入 |
| write-doc | 1. 普通线性正文直接写入 UTF-8 `.md`,执行 `doc create --name <标题> --content-file <tmp.md> --content-format markdown --format json`<br>2. 仅当用户要求复杂版式且确实选择 JSONML 时,读取 [doc-create-workflow.md](./doc/style/doc-create-workflow.md) 对应章节,不预读整套 style/reference<br>3. 大内容依赖 DWS 自动分片;仅在 `CONTENT_TRUNCATED`、中断或回读缺失时恢复<br>4. 取 create 返回的 `nodeId` 执行 `doc read --node <nodeId> --format json`,核对明确要求的标题、段落和结构 |
| search-docs-and-share | 1. `dws drive search --query "<关键词>" --format json` → 取候选 `nodeId` + 标题建索引(不读全文)<br>2. 对追问选中的候选执行 `dws drive info --node <nodeId> --format json`<br>3. 仅 `extension=adoc` 使用 `dws doc read --node <nodeId> --format json`(最多 2 篇);`md` / `axls` / `able` / 普通文件分别切到 markdown / sheet / aitable / drive,禁止固定执行 `doc read` |
| create-knowledge-base | 1. 创建知识库空间取 `WS_ID`<br>2. `wiki node create --workspace <WS_ID> --name "<文档名>"` → 取 `nodeId`<br>3. `wiki node list --workspace <WS_ID>` 确认 |
| migrate-doc | 1. `doc read --node <源nodeId>` → 取正文并写入临时文件 `<tmp>.md`<br>2. `doc create --name "<文档名>" --folder <DOC_FOLDER_NODE_ID> --content-file <tmp>.md` → 取新 `nodeId`;所有长度都先走这一条原生命令,由 CLI 自动分片(`--folder` 只传文档文件夹 nodeId / alidocs 文件夹 URL,不传数字 dentryId)<br>3. **回读校验**:`doc read --node <nodeId>` 校验内容完整性;仅在 `CONTENT_TRUNCATED`、中断或回读缺失时,从真实断点补写缺失部分 |
@@ -57,3 +57,8 @@ dws doc read --node <nodeId> # 校验关键标题、段落首句、表格、@
```
**禁止**在未回读的情况下向用户报告「已完成」。
## 显式工作流
- 用户点名的 `create → list → insert/append/update` 是可观察命令链,必须保持顺序逐项执行;create 只承载明确的初始正文。有序列表块必须验证回读结构中的 `list.isOrdered=true`。
- 新建资源返回 ID 后,同一请求的指代默认绑定该新资源;禁止搜索同名旧资源替换绑定。
+2 -13
View File
@@ -11,20 +11,9 @@
> **操作后请返回文档 URI**:每次执行 create / read / update 等操作后,从返回数据中提取 `docUrl` 直接返回;缺失时用 `doc info --node <ID>` 补查。
## 前置条件 — 执行操作前必读
## 按需加载边界
**CRITICAL — 执行对应操作前,MUST 先用 Read 工具读取以下子文件:**
1. **解析 URL / 定位文档**(几乎所有命令都需要先拿 nodeId)
→ 必读 [`doc/doc-info.md`](doc/doc-info.md)(URL/dentryKey 提取规则、ID 边界、extension 路由、**获取 nodeId 三种方式 A/B/C**)
2. **创建或编辑文档内容**(`doc create` / `doc update` / `doc block insert|update`)
→ 必读 [`doc/style/doc-update-workflow.md`](./doc/style/doc-update-workflow.md)(**形态优先级硬规则:JSONML > element JSON > markdown**;markdown overwrite 会丢富结构)
- 从零创建时加读 [`doc/style/doc-create-workflow.md`](./doc/style/doc-create-workflow.md)
- **任何 `doc create` 都必须先读 [`doc/style/doc-style-guideline.md`](./doc/style/doc-style-guideline.md) §2.0 类型决策表 + §1 硬规则**(决定骨架 + 全局约束,不读就不知道用哪种骨架)
- 涉及 callout / 分栏 / 富 block 精修时再加读 style-guideline §4-§7 + [`doc/format/doc-jsonml-cookbook.md`](./doc/format/doc-jsonml-cookbook.md)
**未读以上文件就改写已有文档会导致富结构丢失、参数错误或样式不达标。其他命令(阅读 / 评论 / 权限 / 附件 / 下载导出 / 文件操作)按需查下方 §命令索引表跳转对应子文件加载,不必提前加载。**
本文件只做低频 atomic 路由,不是任何命令的前置必读。根 Skill 已覆盖的普通 create/read/block/import/export 直接执行;仅在命令已选中但特殊参数或边界仍不明确时,读取下方对应的一个 branch reference。只有实际执行复杂 JSONML 保真写入时才加载 workflow/cookbook/schema,禁止递归预读整组文件。
## Atomic 命令加载契约
@@ -1,15 +1,11 @@
# doc block(块级精细编辑:list / insert / update / delete)
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
> 2. [`./style/doc-update-workflow.md`](./style/doc-update-workflow.md) — 改写流程(编辑形态优先级、JSONML validator 行为)
> 3. [`./format/doc-jsonml-cookbook.md`](./format/doc-jsonml-cookbook.md) — JSONML 范例(含 callout / 分栏 / 表格 / 标题等节点的完整命令)
> 4. [`./format/doc-jsonml-schema.md`](./format/doc-jsonml-schema.md) — JSONML 节点结构字段定义
>
> **同任务常配合**:[`doc-update.md`](./doc-update.md)(整篇 overwrite / 末尾追加纯文本)/ [`./format/doc-jsonml-cookbook.md`](./format/doc-jsonml-cookbook.md)(JSONML 复制范例)
> 本文件自包含简单 list/insert/update/delete 契约,不要递归预读路由或 style reference。只有实际构造复杂 JSONML 节点时,才读取 [`doc-jsonml-cookbook.md`](./format/doc-jsonml-cookbook.md);字段仍不确定时再查 [`doc-jsonml-schema.md`](./format/doc-jsonml-schema.md)。整篇 overwrite 或纯文本 append 才转读 [`doc-update.md`](./doc-update.md)。
> **改写已有文档优先 JSONML**:保真度最高、callout / 分栏 / 表格 / @人 / 附件 / 颜色 / 嵌套都能 1:1 round-trip;写入端有 validator 兜底。详见 [`./style/doc-update-workflow.md` §1.3 编辑形态优先级](./style/doc-update-workflow.md)。
> **显式块操作不可折叠**:用户说“先 create,再 list/insert/update/append”时按原顺序真实调用;不能因为最终正文相似,就把后续块操作合并进 create 或一次 Markdown 写入。
---
## doc block list(查询块元素)
@@ -173,7 +169,8 @@ dws doc block delete --node DOC_ID --block-id UUID
- **块类型**:paragraph、heading、blockquote、callout、columns、orderedList、unorderedList、table、sheet、attachment、slot。
- **快捷 vs --element**:`block insert` 优先使用 `--text` 或 `--heading` 快捷方式;复杂块类型(table、callout、columns 等)使用 `--element` JSON 或 `--content-format jsonml`。
- **简单内容追加**:建议用 [`./doc-update.md`](./doc-update.md) `--mode append`,不必走 block insert。
- **有序列表块**:用户明确要求 ordered list / 有序列表块时,必须用 JSONML `p` 节点的 `list.isOrdered=true`(同一 `listId`;仅首项设 `start:1`)或等价原生 orderedList element;带 `1.` 前缀的普通段落、普通 Markdown 或一次 create 不满足要求。
- **简单内容追加**:用户只说追加纯文本且不强调块操作时可用 [`./doc-update.md`](./doc-update.md) `--mode append`;用户明确说 block insert / 插入段落 / 插入标题 / 插入列表块时必须走 block insert。
- **JSONML validator**(写入端默认行为):
- 裸字符串、缺 uuid 等结构错误会被 validator 抦下并返回带 path 的错误(如 `$[2][2]: paragraph child must be span wrapper, got raw string.`)。
- `--fix-jsonml` 开启 JSON 语法修复,推荐 agent 调用。
@@ -241,6 +238,12 @@ dws doc block list --node <DOC_ID> --content-format jsonml --block-id <UUID>
dws doc block insert --node <DOC_ID> --content-format jsonml --ref-block <UUID> --where after \
--element '["p",{},["span",{"data-type":"text"},["span",{"data-type":"leaf"},"新段落"]]]'
# 插入有序列表块(3 项共用 listId,仅首项有 start)
dws doc block insert --node <DOC_ID> --content-format jsonml \
--element '["p",{"uuid":"ol1","list":{"listId":"actions","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":"ol2","list":{"listId":"actions","level":0,"isOrdered":true}},["span",{"data-type":"text"},["span",{"data-type":"leaf"},"第二项"]]]'
# 插入 callout(colorBlocks)
dws doc block insert --node <DOC_ID> --content-format jsonml --ref-block <UUID> --where after \
--element '["container",{"uuid":"co1","subType":"colorBlocks","metadata":{"bgcolor":"#FDE2E0","border":"#F5C2C7"}},["p",{"uuid":"co1p1"},["span",{"data-type":"text"},["span",{"data-type":"leaf"},"高风险操作,先备份"]]]]'
@@ -1,9 +1,6 @@
# doc comment(文档评论:list / create / reply / update / delete / create-inline)
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
>
> **同任务常配合**:`dws aisearch person`(查 `--mention` 用 userId)/ `dws chat search`(查群用 openConversationId)/ [`doc-block.md`](./doc-block.md)(划词评论必须先取 blockId 与 paragraph 文本)
> 本文件自包含评论命令契约。仅在需要 mention 时查询真实 userId/openConversationId;仅在划词评论尚无 blockId 与 paragraph 文本时读取 [`doc-block.md`](./doc-block.md) 并执行 block list。
---
@@ -1,11 +1,6 @@
# doc create(创建文档)
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
> 2. [`./style/doc-create-workflow.md`](./style/doc-create-workflow.md) — 创建工作流(标题、位置、骨架、回读校验)
> 3. [`./style/doc-style-guideline.md`](./style/doc-style-guideline.md) — 排版规范(草稿元素清单、骨架样板)
> 4. [`./doc-update.md` §内容写入管道](./doc-update.md) — 长内容自动分片、`--content-file` vs `--content` 选择
> 5. [`./format/doc-jsonml-cookbook.md`](./format/doc-jsonml-cookbook.md) — 仅当使用 `--content-format jsonml` 时必读
> 本文件自包含普通 Markdown 创建契约,不要递归预读 `doc.md`、style 或 update reference。仅当用户要求复杂版式并实际选择 JSONML 时,读取 [`doc-jsonml-cookbook.md`](./format/doc-jsonml-cookbook.md);需要文档骨架建议时才读取对应 style 章节。
## 创建路由前置判断(必看)
@@ -40,7 +35,7 @@ Flags:
## 关键说明
- **`--name` 是 H1**:正文从 `##` 开始;正文内不要再写 `#` 一级标题(除非确需且已说明动机)。
- **标题优先级**:`--name` 是文档外壳标题,默认可视作 H1;但它不能替代用户显式要求的正文一级标题。用户说“正文写 `# ...`”“正文先起个一级标题”时,必须在初始内容中保留该 `#` H1;用户未要求正文 H1 时,正文默认从 `##` 开始以避免重复。
- 不传 `--folder` 和 `--workspace` 时,默认创建在「我的文档」根目录。
- `--folder` 仅接受文档文件夹 `nodeId` / `dentryUuid` / alidocs 文件夹 URL;**禁止**传入 drive `dentryId`、`parentId`、`spaceId` 这类纯数字 ID。
- 输入方式选择见 [`./doc-update.md` §内容写入管道](./doc-update.md)(与 update 共用)。短文本字面量可 `--content`,多行/表格/特殊字符必须 `--content-file` 或 `--content -`。
@@ -54,6 +49,12 @@ Flags:
| `docUrl` | 最终交付给用户的链接;缺失时用 [`./doc-info.md`](./doc-info.md) 补查 |
| `chunksWritten` | 判断是否触发自动分片;> 1 时重点检查章节顺序 |
同一请求后续出现“这篇/刚才那篇/上次那篇”时,直接续用本次 create 返回的 `nodeId`;禁止先搜索同名文档再把后续操作指向旧节点。
## 显式操作序列
用户点名 `block list`、插入、追加、更新等后续动作时,必须按原顺序逐项执行。`doc create` 只写用户指定的初始内容,不能为了减少调用把后续标题、列表或段落提前塞进 create。例:`创建 → 查看块结构 → 末尾插入段落` 必须真实执行 create、block list、block insert 三步。
## 回读验收(必读)
CLI **不会**自动回读校验。**每次创建后**都必须执行 `doc read --node <nodeId>` 校验关键标题、段落首句、表格表头是否完整。详见 [`./style/doc-create-workflow.md` «回读验收»](./style/doc-create-workflow.md)。
@@ -1,7 +1,6 @@
# doc export(在线文档导出为 docx/markdown/pdf)
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
> 本文件自包含 export 契约。已有当前请求返回的 adoc nodeId 时直接导出;目标类型未知时只执行一次 `drive info`,不要递归读取 `doc.md`。
> **路由前置判断**:用户说「下载/导出」时**必须**先用 `dws drive info --node <ID> --format json` 查 `extension`:
> - `extension` 为 `adoc`(在线文档)→ **必须用 `export`**,禁止用 `download`
@@ -51,6 +50,7 @@ Flags:
## 关键说明
- 同一请求中刚执行 create/copy/import 并紧接着说“这篇/刚才那篇/上次那篇”时,`--node` 必须使用该写操作真实返回的新 `nodeId`;不得预先搜索同名文档,也不得用搜索结果中的旧节点替换它。
- `export` 是一体化命令,一条命令自动完成提交→轮询→下载,**无需手动编排轮询**。CLI 内部使用渐进式退避轮询(最多约 5 分钟)。
- `export` 超时或中断后,CLI 会输出 `jobId`,可用 `dws doc export get --job-id <jobId>` 手动查询任务状态。
- `export` 支持钉钉在线文档(alidocs,`contentType=ALIDOC`)导出为 `docx`、`markdown` 或 `pdf`,**在线表格导出请使用其他命令**。
@@ -1,11 +1,12 @@
# doc import(本地文件导入为在线文档)
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
> 本文件自包含 import 契约。文件、目标 folder/workspace 与参数已知时直接执行;只有参数或安全语义不确定时查询精确 leaf Schema,不要递归读取 `doc.md`。
> **支持的文件格式**:docx, doc, xlsx, xls, md, txt, xmind, mark
> **文件大小限制**:20MB
> **在线编辑硬路由**:用户说“上传后在线编辑/大家直接在线改/转成钉钉文档”时必须使用 `doc import`。`drive upload` 只保留原始 `.docx/.xlsx/...` 普通文件,不能据此宣称已可在线编辑。若用户明确要同时保留原文件和在线版,才先 `drive upload`,再单独 `doc import`,并分别验证两个返回节点。
---
## doc import(一体化命令)
@@ -63,6 +64,7 @@ Flags:
## 关键说明
- `import` 是一体化命令,一条命令自动完成创建会话→上传→确认→轮询,**无需手动编排**。CLI 内部使用渐进式退避轮询(最多约 5 分钟)。
- 导入完成后必须用返回的 `documentUrl`/`nodeId` 执行 `drive info` 或 `doc info`,确认 `extension=adoc`(Word/文本)或对应在线类型,并确认目标 `folderId`;只有验证后才能说“可直接在线编辑”。
- `import` 超时或中断后,CLI 会输出 `taskId`,可用 `dws doc import get --task-id <taskId>` 手动查询任务状态。
- 支持的文件格式:docx, doc, xlsx, xls, md, txt, xmind, mark(共 8 种)。
- 文件大小限制:20MB。超过限制时 CLI 会直接报错,不会发起网络请求。
@@ -1,8 +1,6 @@
# doc info(获取文档元信息 + URL 解析)
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
> 2. [`url-patterns.md`](../../../dws-shared/references/url-patterns.md) — 仅当用户原始 `alidocs` URL 需要 probe 时
> 本文件自包含已知 nodeId/URL 的 info 契约。只有原始 alidocs URL 类型仍不明确时,才读取 [`url-patterns.md`](../../../dws-shared/references/url-patterns.md);不要递归读取 `doc.md`。
>
> **探测入口变更**:alidocs URL 的类型探测现在统一走 `dws drive info`(详见 [链接规范](../../../dws-shared/references/url-patterns.md#alidocs-url-类型探测流程))。`drive info` 检测到 `extension=adoc/axls/able` 时会自动调用 `doc info` 返回更详细的文档信息。**仅在 `drive info` 已确认是 ALIDOC 类型后**,才需要直接使用 `doc info`。
>
@@ -1,7 +1,6 @@
# doc media(附件 / 图片:download / insert)
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
> 本文件自包含 media insert/download 契约。nodeId、文件路径或 resourceId 已知时直接执行;只在需要相对块定位且 blockId 未知时读取 [`doc-block.md`](./doc-block.md)。
> ⚠️ **图片插入硬规则**:
> - 图片来源如果是钉盘/文档空间中的文件,**必须先下载到本地**(`dws drive download --node <图片nodeId> --output /tmp/xxx.png`),再执行 `media insert`
@@ -1,10 +1,6 @@
# doc read(读取文档内容)
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
> 2. [`./format/doc-jsonml-cookbook.md`](./format/doc-jsonml-cookbook.md) — 仅当使用 `--content-format jsonml` 时必读
>
> **同任务常配合**:[`doc-info.md`](doc-info.md)(先解析 URL,确认 extension=adoc)/ [`doc-update.md`](doc-update.md)(读后改写)/ [`doc-block.md`](doc-block.md)(块级精修前先读结构)
> 本文件自包含普通 read 契约。用户已给当前 adoc nodeId/URL 时直接读取;类型未知时才先执行 `drive info`。选择 JSONML 只为获取结构,不要求预读 cookbook;实际构造 JSONML 写入时再按需加载。
## 命令格式
@@ -1,12 +1,6 @@
# doc update(更新文档内容)
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
> 2. [`./style/doc-update-workflow.md`](./style/doc-update-workflow.md) — 改写流程(编辑形态优先级、分片 append、回读验收)
> 3. [`./style/doc-style-guideline.md`](./style/doc-style-guideline.md) — 排版规范
> 4. [`./format/doc-jsonml-cookbook.md`](./format/doc-jsonml-cookbook.md) — 仅当使用 `--content-format jsonml` 时必读
>
> **同任务常配合**:[`doc-read.md`](./doc-read.md)(改写前必读,jsonml 模式拿当前结构;担心被并发覆盖时再取 revision)/ [`doc-block.md`](./doc-block.md)(单 block 改写优先;本命令更适合追加 / 整篇 overwrite)
> 本文件自包含普通 append/overwrite 契约,不要递归预读路由或 style reference。纯文本 append 可直接执行;overwrite 先 read/dry-run/确认。只有保真改写或复杂 JSONML 才读取 [`doc-update-workflow.md`](./style/doc-update-workflow.md) 与 cookbook;单块修改改用 [`doc-block.md`](./doc-block.md)。
## 命令格式
@@ -4,11 +4,9 @@
> 改写已有文档见 [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 等)
普通 Markdown 创建不需要先读本文件或 style guideline。只有用户要求设计文档骨架或复杂版式时,才查看下方对应章节;实际选择 JSONML 后再读取 cookbook。需要按文档类型选骨架时,按需读取 [doc-style-guideline.md](./doc-style-guideline.md) 的对应一节,不要通读。
### 关键词速查(用户意图 → 起稿路径)
@@ -45,7 +43,7 @@
| 项目 | 要求 |
|------|------|
| 标题 | 用 `--name` 传入;正文不要再重复同名一级标题 |
| 标题 | 用 `--name` 传入;用户显式要求正文 H1 时原样保留,未要求时正文默认从 H2 开始 |
| 位置 | 默认创建到我的文档;指定目录时只接受文档文件夹 `nodeId` 或 alidocs 文件夹 URL |
| 正文 | 多行、表格、代码块、特殊字符或长度 >= 2KB 时必须写入 UTF-8 临时 `.md` 文件 |
| 格式 | 按 §JSONML 起稿判定 决定起稿路径:命中 JSONML 起稿条件时**直接用 JSONML 构造**(跳过 markdown);未命中时用 Markdown 起稿,创建后按 [doc-update-workflow.md](./doc-update-workflow.md) 精修 |
@@ -226,7 +224,7 @@ callout 可通过 `"showstk": true, "sticker": "图标名"` 配置顶部贴纸
> **脚手架策略警示**:Markdown 无法表达分栏/callout/色彩表头,拉回的 JSONML 只有纯文本骨架。精修阶段不是“在现有结构上加色”,而是“参照 RFC/Spec 重组结构”。
> **MUST READ**:动手写 JSONML 前,必须先用 Read 工具读取 [doc-jsonml-cookbook.md](../format/doc-jsonml-cookbook.md) — 其中 §决策型文档骨架范例 有可直接复制修改的完整模板。
> **JSONML 条件加载**:仅在确定使用 JSONML 后读取 [doc-jsonml-cookbook.md](../format/doc-jsonml-cookbook.md);其中“决策型文档骨架范例”可直接改写。
> 节点类型和属性的权威定义见 [doc-jsonml-schema.md](../format/doc-jsonml-schema.md)。
### ⚠️ JSONML 降级约束
@@ -274,7 +272,7 @@ callout 可通过 `"showstk": true, "sticker": "图标名"` 配置顶部贴纸
```
- 根节点固定 `"root"`(不是 `"body"`)
- `--name` 已是 H1,JSONML 从 `h2` 开始
- 用户未要求正文 H1 时,JSONML 从 `h2` 开始;用户明确要求“正文一级标题/插入一级标题”时必须构造 `h1`
- 表格结构是 `table → tr → tc`(无 `th`/`td`)
- 分栏是 `table` + `"sr": true`,`tc` 建议设 `fill` 背景色
- 有序列表:仅第一项设 `"start": 1`,后续项不设 `start`(系统自动递增)
@@ -315,7 +313,7 @@ dws doc read --node <nodeId> --content-format jsonml --output /tmp/<name>-readba
- 只使用用户已提供或对话中已确认的正文素材。
- 如果正文素材不足,先补齐文档目标、受众、章节和缺口;不要在本文中临时扩展跨产品采集流程。
- **先按 [doc-style-guideline.md §2.0 类型判断决策表](./doc-style-guideline.md) 确定文档类型,再用对应类型的骨架样板(§2.1 决策型 / §2.2 执行型 / §2.3 说明型 / §2.4 知识沉淀型)**。不要套通用三段式。
- **`--name` 已是 H1,正文从 `##` 开始**;正文内不要再写 `#` 一级标题(除非确实需要正文内再造一级 H1 并说明动机)。
- **`--name` 是外壳标题,不覆盖显式正文 H1**:用户未要求正文一级标题时从 `##` 开始;用户明确给出 `# ...` 或要求“先起一级标题”时,正文必须保留该 H1。
- 摘要、bullet、引用块、callout 等元素的使用边界以 style-guideline §3-§7 为准。
- 同类信息保持一致:风险、状态、行动项各用一种元素 + 一种视觉语义(style-guideline §1.2 / §5)。
- 临时文件必须保留真实换行,不能把换行写成字面量 `\n`。
@@ -25,7 +25,7 @@
## 一、硬规则
1. **`--name` 是 H1**:正文从 `##` 开始;正文内不写 `#`(除非确需正文内再造一级 H1 并说明动机)
1. **用户显式正文 H1 优先**:`--name` 是文档外壳标题;用户未要求正文一级标题时从 `##` 开始。用户明确说“正文写 `# ...` / 先起一级标题 / 插入一级标题”时必须保留或插入真实 H1,不得用 `--name` 代替
2. **同类信息同表达**:风险、状态、行动项、证据,每类只用一种元素 + 一种视觉语义(见 §5)
3. **Markdown 草稿阶段只用稳定元素**:标题、段落、列表、checklist、表格、代码块;callout / 分栏 / 附件 / 复杂嵌套留到创建后用 `doc block insert` / `doc media insert` 精修
4. **引用块只用于原文**:用户原话、会议摘录、外部材料原文;不许包装作者自己的结论
@@ -209,7 +209,7 @@
### 4.1 标题与段落
- 正文从 `##` 开始(H1 已被 `--name` 占用)
- 用户未要求正文 H1 时从 `##` 开始;用户明确要求正文 H1 时按原文使用 `#` 或 heading level 1
- 标题层级 ≤ 4 层(§7)
- 单段过长先拆段,再考虑换元素
@@ -217,6 +217,7 @@
- 普通列表:并列要点
- 有序列表:顺序步骤
- 用户明确要求“有序列表块”时必须使用真实列表结构;JSONML 为带 `list.isOrdered=true` 的多个 `p` 节点,不能只写带数字前缀的普通段落或以整篇 Markdown 代替显式 block insert
- checklist:待办状态(含 `- [ ]` / `- [x]`)
列表项里开始出现"负责人 / 截止时间 / 状态"这类字段时,改用表格。
+1 -1
View File
@@ -68,7 +68,7 @@ metadata:
**触发**:在知识库建文档/页面。
1. **执行(必须)**:`dws wiki node create --workspace <workspaceId> --type adoc --name "<名称>" --format json`(按需 `--parent-id <父节点>`);返回取 `nodeId`。
2. **写内容(必须)**:节点内容编辑切 `dingtalk-doc`,用 `dws doc update --node <nodeId> --mode overwrite|append --content-file <tmp.md> --yes`;写后 `doc read` 回读。
2. **写内容(必须)**:节点内容编辑切 `dingtalk-doc`。追加用 `dws doc update --node <nodeId> --mode append --content-file <tmp.md>`;只有用户确认整篇覆盖后才用 `dws doc update --node <nodeId> --mode overwrite --content-file <tmp.md> --yes`。写后都要 `doc read` 回读。
3. **验证(必须)**:`dws wiki node list --workspace <workspaceId> --format json` 复核节点已建。
**禁止**:在 wiki 内直接拼内容(应切 doc 写)、建后不回读。
+110
View File
@@ -14,6 +14,8 @@ from unittest import mock
ROOT = Path(__file__).resolve().parents[2]
SKILL_ROOT = ROOT / "skills" / "multi" / "dingtalk-doc"
SCRIPT_PATH = SKILL_ROOT / "scripts" / "doc_create_and_write.py"
MONO_DOC_ROOT = ROOT / "skills" / "mono" / "references" / "products" / "doc"
MONO_SCRIPT_PATH = ROOT / "skills" / "mono" / "scripts" / "doc_create_and_write.py"
def load_script():
@@ -69,6 +71,96 @@ class DocSkillAlignmentTest(unittest.TestCase):
self.assertNotIn("超过 200KB", combined)
self.assertNotIn("doc get", combined)
def test_common_routes_do_not_require_recursive_reference_loading(self):
skill = (SKILL_ROOT / "SKILL.md").read_text(encoding="utf-8")
branch_names = [
"doc-create.md",
"doc-block.md",
"doc-comment.md",
"doc-import.md",
"doc-export.md",
"doc-media.md",
"doc-read.md",
"doc-update.md",
"doc-info.md",
]
for name in branch_names:
text = (
SKILL_ROOT / "references" / "doc" / name
).read_text(encoding="utf-8")
self.assertNotIn("前置条件(MUST READ)", text, name)
self.assertNotIn("必须先用 Read 工具读取以下文件", text, name)
self.assertIn("不超过 5 个确定性 DWS 操作", skill)
self.assertIn("不创建 Todo", skill)
self.assertIn("按“先/再/然后”切分操作阶段", skill)
self.assertLessEqual(len(skill.encode("utf-8")), 9500)
def test_workflow_and_identity_rules_are_explicit(self):
skill = (SKILL_ROOT / "SKILL.md").read_text(encoding="utf-8")
create_refs = "\n".join(
path.read_text(encoding="utf-8")
for path in [
SKILL_ROOT / "references" / "doc" / "doc-create.md",
MONO_DOC_ROOT / "doc-create.md",
]
)
block_refs = "\n".join(
path.read_text(encoding="utf-8")
for path in [
SKILL_ROOT / "references" / "doc" / "doc-block.md",
MONO_DOC_ROOT / "doc-block.md",
]
)
import_refs = "\n".join(
path.read_text(encoding="utf-8")
for path in [
SKILL_ROOT / "references" / "doc" / "doc-import.md",
MONO_DOC_ROOT / "doc-import.md",
]
)
self.assertIn("不能覆盖用户显式要求的正文 H1", skill)
self.assertIn("禁止先搜索同名文档", create_refs)
self.assertIn("显式块操作不可折叠", block_refs)
self.assertIn("list.isOrdered=true", block_refs)
self.assertIn("在线编辑硬路由", import_refs)
def test_schema_selection_preserves_doc_drive_boundaries(self):
doc = json.loads(
(
ROOT / "internal" / "cli" / "schema_hints" / "selection" / "doc.json"
).read_text(encoding="utf-8")
)["tools"]
drive = json.loads(
(
ROOT
/ "internal"
/ "cli"
/ "schema_hints"
/ "selection"
/ "drive.json"
).read_text(encoding="utf-8")
)["tools"]
self.assertIn(
"正文 H1",
" ".join(doc["doc.create_document"]["use_when"]),
)
self.assertIn(
"list.isOrdered=true",
" ".join(doc["doc.insert_document_block"]["use_when"]),
)
self.assertIn(
"dws doc import",
" ".join(drive["drive.upload"]["avoid_when"]),
)
def test_mono_and_multi_wrappers_stay_aligned(self):
self.assertEqual(
SCRIPT_PATH.read_text(encoding="utf-8"),
MONO_SCRIPT_PATH.read_text(encoding="utf-8"),
)
class DocCreateAndWriteTest(unittest.TestCase):
def setUp(self):
@@ -144,6 +236,24 @@ class DocCreateAndWriteTest(unittest.TestCase):
self.assertEqual("doc-1", summary["nodeId"])
self.assertTrue(summary["verified"])
def test_wrapper_preserves_explicit_body_h1(self):
seen_content = []
def fake_run(args, dry_run=False):
if args[:2] == ["doc", "create"]:
content_path = Path(args[args.index("--content-file") + 1])
seen_content.append(content_path.read_text(encoding="utf-8"))
return {"success": True, "nodeId": "doc-1", "chunksWritten": 1}
if args[:2] == ["doc", "info"]:
return {"success": True, "docUrl": "https://example.test/doc-1"}
return {"success": True, "markdown": "# 周报"}
with mock.patch.object(self.module, "run_dws", side_effect=fake_run):
with contextlib.redirect_stdout(io.StringIO()):
code = self.module.run(["--name", "周报", "--content", "# 周报"])
self.assertEqual(0, code)
self.assertEqual(["# 周报"], seen_content)
def test_dry_run_shows_create_and_verification_commands(self):
stdout = io.StringIO()
with contextlib.redirect_stdout(stdout):