Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f5923736e | ||
|
|
dbe4b5f388 | ||
|
|
405a027002 | ||
|
|
09643fc6a1 | ||
|
|
a3e5145322 | ||
|
|
0e4ec9af34 | ||
|
|
9b62989ddb | ||
|
|
e81e040579 | ||
|
|
4f4fd4eb66 | ||
|
|
8510cb8b91 |
@@ -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 入口。
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;该模式会覆盖远端内容并要求确认"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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;该模式会覆盖远端内容并要求确认"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 "..."` |
|
||||
|
||||
@@ -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]`)
|
||||
|
||||
列表项里开始出现"负责人 / 截止时间 / 状态"这类字段时,改用表格。
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 后,同一请求的指代默认绑定该新资源;禁止搜索同名旧资源替换绑定。
|
||||
|
||||
@@ -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]`)
|
||||
|
||||
列表项里开始出现"负责人 / 截止时间 / 状态"这类字段时,改用表格。
|
||||
|
||||
@@ -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 写)、建后不回读。
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user