Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71b45b1eab | ||
|
|
5b686efc23 | ||
|
|
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,23 +11,24 @@ metadata:
|
||||
|
||||
# 钉钉 AI 表格 Skill
|
||||
|
||||
## 前置条件 — 执行操作前必读
|
||||
## 执行入口
|
||||
|
||||
> **CRITICAL — 执行任何 `dws` 操作前,MUST 先用 Read 工具完整读取 [`dws-shared`](../dws-shared/SKILL.md)。**该轻量文件包含全局执行契约、安全底线及 shared references 的按需加载导航;不要预加载其全部 references。
|
||||
执行任何 `dws` 操作前,完整读取 [`dws-shared`](../dws-shared/SKILL.md),但不要预加载其 references。高频意图直接使用本文件骨架;仅特殊参数、复杂数据形态或边界不明时读取一个 branch reference。
|
||||
|
||||
## 加载与路由顺序
|
||||
|
||||
1. 命中下方高频意图时直接使用精确骨架,不先查 Help 或产品级 Schema。
|
||||
2. 路由优先级固定为:精确 recipe / 可运行脚本 > 匹配的公开 Shortcut > 原子命令。命令已确定且参数清楚时直接执行。
|
||||
2. 路由优先级固定为:精确骨架 / recipe > 匹配的公开 Shortcut > 原子命令。脚本只用于 Runtime 尚未覆盖的批量、文件传输或异步编排,不与普通原子命令竞争默认入口。
|
||||
3. 参数、约束或安全语义不确定时只读 leaf Schema:`dws schema --cli-path "aitable <leaf>" --format json`;只有当前 Cobra flag 不确定时才读对应 `--help`。
|
||||
4. 复杂字段、筛选、导入导出、视图、权限或工作流任务,按“低频能力与 Reference”只加载相关文件,不预读整个 `references/aitable/`。
|
||||
5. 现有骨架和 reference 都无法定位能力时,才用 Runtime Shortcut Catalog 做最后发现;不得猜 `cli_path` 或 flag。
|
||||
6. Schema、Help、reference 与实际返回冲突时采用更安全的解释并报告契约漂移;`confirmation=user_required` 时先确认,再添加 `--yes`。
|
||||
7. 用户已给足目标、字段和数据时,按依赖链连续执行;中间结果只用于提取真实 ID 和判断停止条件,全部完成后统一回读并答复。
|
||||
|
||||
<!-- VISIBLE_SHORTCUTS_START -->
|
||||
## Shortcut 发现(按需)
|
||||
|
||||
`aitable` 当前有 29 条公开 shortcut。完整清单保留在 Runtime Shortcut Catalog;已完成 Schema curation 的子集可通过 leaf Schema 查询。高频产品根 Skill 不重复展开完整清单。已知意图直接使用下方的优先路由、意图表或任务 reference;命令已选中时直接执行,只在参数/安全语义不确定时读取 leaf Schema,在当前 Cobra flags 不确定时读取 leaf Help。
|
||||
`aitable` 当前有 29 条公开 shortcut,完整清单保留在 Runtime Shortcut Catalog,根 Skill 不重复展开。
|
||||
|
||||
仅当现有路由和 reference 都无法定位低频能力时,才执行 `dws shortcut list --service aitable --compact --format json` 做最后回退;不要为已知高频意图加载完整 Shortcut Catalog 或产品级 Schema。
|
||||
<!-- VISIBLE_SHORTCUTS_END -->
|
||||
@@ -43,7 +44,9 @@ metadata:
|
||||
| View / Dashboard / Chart | `viewId` / `dashboardId` / `chartId` 各自绑定当前 Base/Table,不跨对象复用 |
|
||||
| 异步任务 | `taskId` / `importId` 只用于对应导出或导入任务,不能替代业务对象 ID |
|
||||
|
||||
所有下游 ID 都从当前链路的结构化返回中提取;同名多候选必须让用户消歧,不默认取第一项,也不复用未经本轮校验的旧 ID。
|
||||
所有下游 ID 都从当前链路的结构化返回中提取;同名多候选必须让用户消歧,不默认取第一项。Base → Table → Field/Record/View 的容器关系必须保持一致,不跨 Base 或 Table 复用子对象 ID。
|
||||
|
||||
创建、复制、导入或新建字段/记录返回 ID 后,立即绑定同一请求中的“这个”“刚才新建的”等指代;除非用户明确转向历史资源,否则不得再按名称搜索并替换为旧对象。
|
||||
|
||||
## 核心意图与执行骨架
|
||||
|
||||
@@ -51,14 +54,17 @@ metadata:
|
||||
|---|---|---|
|
||||
| 按名称找 Base | `dws aitable +resolve-base --name "<名称>" --format json` | 唯一命中才继续;多候选停止并消歧 |
|
||||
| 浏览最近访问 | `dws aitable +base-list --format json` | 只代表最近访问,不得宣称全量 |
|
||||
| 搜索模板 | `dws aitable template search --query "<关键词>" --format json` | 只返回真实候选,不擅自套用模板或创建 Base |
|
||||
| 按名称找 Table | `dws aitable +resolve-table --base <baseId> --name "<表名>" --format json` | `baseId` 必须来自上一步真实返回 |
|
||||
| 取表、字段与视图目录 | `dws aitable +table-get --base-id <baseId> [--table-ids <tableId>] --format json` | `tables[].fields[]` 是字段目录;完整类型/config 再用 `+field-get` |
|
||||
| 取字段完整配置 | `dws aitable +field-get --base-id <baseId> --table-id <tableId> [--field-ids <ids>] --format json` | 写入前核对类型、只读性和 select options;按需展开以控制返回体 |
|
||||
| 查/搜/筛记录 | `dws aitable +record-query --base-id <baseId> --table-id <tableId> [--query <词>\|--filters '<JSON>'\|--record-ids <ids>] --format json` | ID 模式忽略 filter/sort;全量结论必须完整分页 |
|
||||
| 查/搜/筛记录 | `dws aitable +record-query --base-id <baseId> --table-id <tableId> [--query <词>\|--filters '<JSON>'\|--record-ids <ids>] --format json` | ID 模式忽略 filter/sort;不要猜 `--page-limit`,分页使用返回的 cursor 与 leaf Help 中的真实分页 flag |
|
||||
| 新增记录 | `dws aitable record create --base-id <baseId> --table-id <tableId> --records '[{"cells":{"<fieldId>":<值>}}]' --format json` | 单次最多 100;取 `data.newRecordIds[]` 后立即按 ID 回读 |
|
||||
| 更新记录 | `dws aitable record update --base-id <baseId> --table-id <tableId> --records '[{"recordId":"<id>","cells":{"<fieldId>":<值>}}]' --format json` | 先 query 拿 recordId;只传需改字段;取 `data.recordIds[]` 后回读 |
|
||||
| 删除记录 | 先 `dws aitable +record-query ...` 定位,再 `dws aitable record delete --base-id <baseId> --table-id <tableId> --record-ids <ids>` | 展示目标与影响,得到明确确认后才加 `--yes` |
|
||||
| 创建 Base / Table | `dws aitable base create --name "<名>"` / `dws aitable table create --base-id <id> --name "<名>" --fields '[...]'` | 使用创建返回的真实 ID;系统改名/加后缀时不得继续猜原名 |
|
||||
| 复制视图 | `dws aitable view duplicate --base-id <baseId> --table-id <tableId> --view-id <viewId> --new-name "<名>"` | `viewId` 必须属于当前表;不能用复制 Table 或新建 Dashboard 替代 |
|
||||
| 创建图表 | 先读 `dws aitable chart config-example --format json`,再执行 `chart create ... --config '<JSON>' --layout '<JSON>'` | `--layout` 是必填项;不能只传 config 后依据退出码声称图表已创建 |
|
||||
| 批量追加 CSV / JSON 到已有表 | `python3 scripts/import_records.py <baseId> <tableId> <file> [batch_size]` | CSV 表头必须是 fieldId;脚本返回不完整 ledger 时不得宣称全成功 |
|
||||
| 文件导入为新数据表 | `python3 scripts/aitable_import_via_task.py <baseId> <file>` | 与“追加已有 table”不同;走 prepare → PUT → import task |
|
||||
| 批量创建字段 | `python3 scripts/bulk_add_fields.py <baseId> <tableId> fields.json` | 单次最多 15;逐项检查成功/失败结果 |
|
||||
@@ -69,9 +75,26 @@ metadata:
|
||||
|
||||
- `record create/update` 前必须获取目标字段的 `fieldId`、`type` 与 `config`;`filterUp`、`lookup` 等只读字段不可写。完整格式只在需要时读 [aitable-cell-value.md](references/aitable/aitable-cell-value.md)。
|
||||
- 筛选和排序字段使用 `fieldId`;`--filters` 最外层是 `and|or + operands`,`--sort` 使用 `direction: asc|desc`。日期和跨表字段规则按需读 [aitable-filter-sort.md](references/aitable/aitable-filter-sort.md)。
|
||||
- `record query --all` 仍受 `--page-limit` 约束;分页中断或局部富化失败时保留已有结果,输出 completeness 与逐项失败 ledger,不把部分结果描述为全量。
|
||||
- 记录分页不能凭经验拼 `--page-limit`。先用当前 leaf Help 确认 page-size/cursor 的真实名称;每页读取返回 cursor,直到明确终止。分页中断或局部富化失败时保留已有结果,输出 completeness 与逐项失败 ledger,不把部分结果描述为全量。
|
||||
- 创建、更新、导入、批量建字段等写操作必须检查业务 `status`、逐项结果与返回 ID;普通写入按用户明确要求执行后回读,不能只凭退出码宣称成功。
|
||||
- 长 JSON 使用 `--records-file` / 任务文件;不得为绕过字段错误而静默丢列、改类型或删除失败项。
|
||||
- `table create --fields` 的键固定为 `fieldName` / `type` / `config`,不能写成 `name`;单选/多选类型固定为 `singleSelect` / `multipleSelect`,不能写 `select`。number formatter 不确定时先读 leaf Help,禁止猜 `INTEGER` 等值。
|
||||
|
||||
## 写入计划与验证
|
||||
|
||||
- 用户明确列出的“先创建、再加字段、然后写记录/建视图”等阶段是可观察的验收步骤,必须逐项真实执行;不能为了得到相似终态而折叠、重排或省略。
|
||||
- 删除 Base/Table/Field/Record、关闭高级权限、删除角色和其他高风险动作,先固化目标 ID、所属容器、影响数量、副作用与可恢复性;确认前写调用为零。
|
||||
- 批量导入、建字段和记录写入在第一笔写入前完成全部字段类型、只读性、关联表和文件边界校验;部分失败保留输入顺序与逐项 ledger。
|
||||
|
||||
| 写入对象 | 成功后必须验证 |
|
||||
|---|---|
|
||||
| Base / Table | 使用创建返回 ID 查询对象及所属关系 |
|
||||
| Field | `+field-get` 核对 fieldId、type、config 与只读性 |
|
||||
| Record | 使用返回 recordId 按 ID 回读目标 cells |
|
||||
| View / Dashboard / Chart | 重新读取当前 Base/Table 下的对象配置 |
|
||||
| 导入 / 导出任务 | 核对 task 状态、结果对象或输出文件完整性 |
|
||||
|
||||
退出码 0、`status=success`、空对象或仅有 taskId 都不能单独证明业务完成。字段未生效、回读不一致、分页不完整或异步任务未完成时,报告失败、部分完成或进行中,不得声称“全部完成”。
|
||||
|
||||
## 低频能力与 Reference
|
||||
|
||||
@@ -90,7 +113,8 @@ metadata:
|
||||
- 路径或 flag 错误:按既定的 leaf Schema → leaf Help 顺序校正一次;仍失败则停止,不连续尝试猜测别名。
|
||||
- 命令非零、输出非 JSON、业务 `status != success`、必需 ID 缺失、批处理部分失败均视为失败;保留成功项与 ledger,禁止吞错。
|
||||
- 同名歧义、权限不足、资源不存在、字段类型漂移、分页无法推进或 Schema/Help 冲突时停止并报告。具体恢复动作按需读 [aitable-error-recovery.md](references/aitable/aitable-error-recovery.md)。
|
||||
- 每次重试都从最新实际输出重新提取下游 ID;删除和其他 `confirmation=user_required` 操作不得自动重试或静默确认。
|
||||
- 已确认远端未写入且契约声明可重试时,才从最新实际输出重新提取 ID 后重试;写入状态未知、删除和其他 `confirmation=user_required` 操作不得自动重试或静默确认。
|
||||
- 参数校验错误必须先吸收错误中的精确 hint,再最多纠正一次;不要重复猜同一 flag、formatter、folderId 或图表配置。目标能力已有原生命令时,不要以手工重建相似对象掩盖原命令失败。
|
||||
|
||||
## 跨产品协作
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# 数据分析
|
||||
|
||||
> 定位:仅在用户要求跨记录统计、聚合或业务结论时加载。普通查找、筛选和分页先按根 Skill 的 `+record-query` 骨架执行;完整分析契约以 [aitable-data-analysis-sop.md](aitable/aitable-data-analysis-sop.md) 为准,本页只保留轻量入口。
|
||||
|
||||
> 本场景所有 recipe 均为 full。
|
||||
|
||||
| Recipe | 行动指南(固定路线) |
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# 业务域通用规范
|
||||
|
||||
> 定位:供 AITable 主题 Reference 复用的批量、并行采集和 ID 传递约定,不是用户意图路由入口。根 Skill 已明确的高频任务不需要单独加载本页。
|
||||
|
||||
> 仅服务本 skill 已迁入的行动指南。安全门控、危险操作确认、`--format json` 等已在本 skill 的 `SKILL.md` 中定义,此处不重复。
|
||||
|
||||
## 批量查询规范
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# 记录操作详细指南
|
||||
|
||||
> 定位:兼容性的记录操作总览。新任务优先按需读取 `aitable/aitable-record-{query,create,update,delete,upsert}.md` 中唯一对应的叶级 Reference;不要同时加载本页和全部叶级文件。本页不得作为另一套命令发现源。
|
||||
|
||||
## 查询记录
|
||||
|
||||
```bash
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
# AI表格 (aitable) 命令参考
|
||||
|
||||
> **渐进式文档**:本文件为路由层(索引 + 意图判断),各命令的详细参数、示例和踩坑说明在 [aitable/](./aitable/) 目录下按需加载。
|
||||
> 定位:低频 atomic 能力的一级索引,不是每次调用必读的产品手册。已知高频意图直接回到 [`SKILL.md`](../SKILL.md);参数和安全语义查 leaf Schema,真实 flags 查 leaf Help;本页只在根路由无法定位能力时用于选择一个主题 Reference,禁止递归加载全部链接。
|
||||
|
||||
已知高频意图优先使用根 Skill 的精确 Shortcut/脚本骨架;本文件只在需要完整一级命令索引、对象 URL 或低频分支导航时加载。参数与安全不确定时读 leaf Schema,Cobra flag 不确定时才读 leaf Help,不要把本文件当作参数事实源。
|
||||
## 使用本索引
|
||||
|
||||
1. 先按对象选择一个分支:Base/Table、Field、Record、View/Form、Dashboard/Chart、Workflow、Advperm、Import/Export 或 Section。
|
||||
2. 只打开该分支链接的一个主题文件;命令已确定后停止发现。
|
||||
3. 所有子对象 ID 必须来自当前 Base/Table 链路;写操作完成后按对象回读。
|
||||
4. 本页示例是导航证据,不覆盖 Runtime/Cobra、leaf Schema 或根 Skill 的 Golden Route。
|
||||
|
||||
各命令的详细参数、示例和边界在 [aitable/](./aitable/) 目录下按需加载。
|
||||
|
||||
## 文档地址 (URI)
|
||||
|
||||
@@ -331,162 +338,3 @@ dws aitable export data --base-id <BASE_ID> --task-id <TASK_ID> --timeout-ms 300
|
||||
- `scope=all`:只需 `base-id`
|
||||
- `scope=table`:必须 `table-id`
|
||||
- `scope=view`:必须同时 `table-id + view-id`
|
||||
|
||||
## 意图判断
|
||||
|
||||
用户说"表格/多维表/AI表格":
|
||||
- 查看/查找/列表 → `base search`(优先)或 `base list`(仅浏览最近访问)
|
||||
- 详情 → `base get`
|
||||
- 创建 → `base create`
|
||||
- 修改 → `base update`
|
||||
- 删除 → `base delete`
|
||||
|
||||
用户说"数据表/子表/table":
|
||||
- 查看 → `table get`
|
||||
- 创建 → `table create`
|
||||
- 重命名 / 改备注 / 改行命名规则 → `table update`(三选一:`--name` / `--description` / `--record-name-key`)
|
||||
- 用户说"行命名规则/记录别名/卡片显示成 task/project/event 这种" → `table update --record-name-key <枚举键>`,**中文 → 枚举键**对照见 [aitable-record-name-key.md](./aitable/aitable-record-name-key.md)
|
||||
- 删除 → `table delete`
|
||||
|
||||
用户说"字段/列/column":
|
||||
- 查看 → `field get`
|
||||
- 添加 → `field create`(读 [aitable-field.md](./aitable/aitable-field.md))
|
||||
- 修改 → `field update`
|
||||
- 删除 → `field delete`
|
||||
|
||||
用户说"记录/行/数据/row":
|
||||
- 查看/搜索 → `record query`(读 [aitable-record-query.md](./aitable/aitable-record-query.md))
|
||||
- 找空行 / 没填东西的行 → `record query-empty`(读 [aitable-record-query.md](./aitable/aitable-record-query.md))
|
||||
- 已知 recordId 反查字段值 → `record get`(按 ID 取专用,等价 `record query --record-ids`)
|
||||
- 添加/写入 → `record create`(读 [aitable-record-create.md](./aitable/aitable-record-create.md))
|
||||
- 修改/更新(每条独立 cells) → `record update`(读 [aitable-record-update.md](./aitable/aitable-record-update.md))
|
||||
- **批量更新同一字段值**(统一标记/统一改值) → `record batch-update --record-ids ... --cells '{...}'`
|
||||
- 删除 → `record delete`
|
||||
- **查记录的字段变更历史 / 操作审计** → `record history-list`(读 [aitable-record-history.md](./aitable/aitable-record-history.md))
|
||||
- **取记录分享链接 / 把这行发给同事** → `record share-url`(读 [aitable-record-share.md](./aitable/aitable-record-share.md))
|
||||
- **不知道有没有 → 有就改、没有就建** → `record upsert`(读 [aitable-record-upsert.md](./aitable/aitable-record-upsert.md))
|
||||
|
||||
用户说"视图/view":
|
||||
- 列出/查看全部视图 → `view list`(或 `view get` 不传 --view-ids,二者等价)
|
||||
- 看某个视图详情 → `view get --view-ids <ID>`
|
||||
- 创建 → `view create`
|
||||
- 修改(含"调整字段顺序/隐藏字段") → `view update --config '{"visibleFieldIds":[...]}'`
|
||||
- 修改某一项配置(filter/sort/group/card/timebar/aggregate 等)→ `view update <attr>`(读 [aitable-view-config.md](./aitable/aitable-view-config.md))
|
||||
- 锁定 / 冻结列 / 行高 / 数据高亮规则 / 复制视图 → 读 [aitable-view-extras.md](./aitable/aitable-view-extras.md)
|
||||
- 删除 → `view delete`
|
||||
|
||||
用户说"锁定视图/解锁视图/lock view" → `view lock` / `view lock --off`,详见 [aitable-view-extras.md](./aitable/aitable-view-extras.md)
|
||||
|
||||
用户说"冻结列/冻结首列/frozen columns" → `view update frozen-cols --count N`,详见 [aitable-view-extras.md](./aitable/aitable-view-extras.md)
|
||||
|
||||
用户说"行高/单元格高度/紧凑模式/cell height" → `view update row-height --cell-height N`(合法档位 32/56/88/128),详见 [aitable-view-extras.md](./aitable/aitable-view-extras.md)
|
||||
|
||||
用户说"数据高亮/条件格式/单元格上色/fill color rule" → `view update fill-color-rule --json '[...]'`,详见 [aitable-view-extras.md](./aitable/aitable-view-extras.md)
|
||||
|
||||
用户说"复制视图/duplicate view" → `view duplicate --view-id ... [--new-name ...]`,详见 [aitable-view-extras.md](./aitable/aitable-view-extras.md)
|
||||
|
||||
用户说"筛选/过滤/filter" → 读 [aitable-filter-sort.md](./aitable/aitable-filter-sort.md)
|
||||
|
||||
用户说"统计/分析/聚合/TOP N/全量" → 读 [aitable-data-analysis-sop.md](./aitable/aitable-data-analysis-sop.md)
|
||||
|
||||
用户说"公式/formula/计算字段/派生指标" → 读 [aitable-formula-guide.md](./aitable/aitable-formula-guide.md)
|
||||
|
||||
用户说"查找引用/lookup/filterUp/跨表" → 读 [aitable-formula-guide.md](./aitable/aitable-formula-guide.md)(§5.4 跨表引用)
|
||||
|
||||
用户说"表单/form/收集表/问卷/催办填写" → 读 [aitable-form.md](./aitable/aitable-form.md)
|
||||
|
||||
用户说"自动化/工作流/流程/触发/automation/workflow" → 读 [aitable-workflow.md](./aitable/aitable-workflow.md)
|
||||
- 新建自动化 → 按子文档的最小 Demo 组装完整 DSL,再 `workflow create --dsl @file`
|
||||
- 修改自动化 → `workflow get` 留底,按最新 DSL 文档生成完整目标 DSL,再 `workflow update --dsl @file`
|
||||
- 看 Base 里有哪些流程 / 哪些在跑 → `workflow list`(看 `recordCount` / `runningCount`)
|
||||
- 看某个流程具体配置(触发条件、动作步骤) → `workflow get`
|
||||
- 启用流程 → `workflow enable`
|
||||
- 临时停掉流程(调试 / 数据迁移)→ `workflow disable --yes`
|
||||
- 删除流程:当前不支持,引导用户到 AI 表格 Web 端 → 数据表 → 自动化 面板手动完成
|
||||
|
||||
用户说"仪表盘/图表/chart" → 读 [aitable-dashboard-chart.md](./aitable/aitable-dashboard-chart.md)
|
||||
|
||||
用户说"仪表盘排版乱了/图表对不齐/重新排布/自动布局/美化仪表盘" → `dashboard arrange`(读 [aitable-dashboard-chart.md](./aitable/aitable-dashboard-chart.md))
|
||||
|
||||
用户说"附件/上传文件" → 读 [aitable-attachment.md](./aitable/aitable-attachment.md)
|
||||
|
||||
用户说"导入/导出/import/export" → 读 [aitable-export-import.md](./aitable/aitable-export-import.md)
|
||||
|
||||
用户说"模板" → `template search`
|
||||
|
||||
用户说"高级权限/角色/权限控制/谁能看/谁能改" → 读 [aitable-advperm.md](./aitable/aitable-advperm.md)
|
||||
- 开/关高级权限 → `advperm enable` / `advperm disable --yes`
|
||||
- 看角色配置 → `advperm role-list` 或 `advperm role-get`
|
||||
- 建角色(可同时指定子角色权限) → `advperm role-create --name ... --sub-roles '[...]'`
|
||||
- 改角色名 / 改子角色权限(PATCH 语义,未传字段不变) → `advperm role-update --role-id ... [--name ...] [--sub-roles '[...]']`
|
||||
- 删角色 → `advperm role-delete --yes`
|
||||
- **角色 ↔ 成员绑定**:当前 CLI 不支持,仍需在 AI 表格 Web 端面板手动完成
|
||||
|
||||
命令报错/操作失败 → 读 [aitable-error-recovery.md](./aitable/aitable-error-recovery.md)
|
||||
|
||||
**关键区分**: base=表格文件, table=数据表, field=列, record=行
|
||||
|
||||
## 核心工作流
|
||||
|
||||
```bash
|
||||
# 1. 按名称解析唯一 Base — 提取 baseId;多候选必须消歧
|
||||
dws aitable +resolve-base --name "项目" --format json
|
||||
|
||||
# 2. 按名称解析唯一 Table — 提取 tableId
|
||||
dws aitable +resolve-table --base <BASE_ID> --name "任务" --format json
|
||||
|
||||
# 3. 获取字段目录;需要完整类型 config 时再调用 field get
|
||||
dws aitable +table-get --base-id <BASE_ID> --table-ids <TABLE_ID> --format json
|
||||
dws aitable +field-get --base-id <BASE_ID> --table-id <TABLE_ID> --format json
|
||||
|
||||
# 4. 查询记录
|
||||
dws aitable +record-query --base-id <BASE_ID> --table-id <TABLE_ID> --format json
|
||||
|
||||
# 5. 新增记录 (cells 用 fieldId 作 key)
|
||||
dws aitable record create --base-id <BASE_ID> --table-id <TABLE_ID> \
|
||||
--records '[{"cells":{"fldXXX":"值"}}]' --format json
|
||||
```
|
||||
|
||||
## 上下文传递表
|
||||
|
||||
| 操作 | 从返回中提取 | 用于 |
|
||||
|------|-------------|------|
|
||||
| `base list/search` | `baseId` | 所有后续命令的 --base-id,拼接文档 URI |
|
||||
| `base create` | `baseId` | 后续命令 + 文档 URI |
|
||||
| `base get` | `tables[].tableId` | --table-id,拼接指定数据表 URI |
|
||||
| `table create` | `tableId` | 后续命令 + 拼接指定数据表 URI |
|
||||
| `table get` | `tables[].tableId`、精简 `fields[]`、`views[]` | 定位数据表、字段目录和视图;完整字段 config 再用 `field get` |
|
||||
| `field get` | `fields[].fieldId/type/config` | record 操作的 cells key、类型校验、field update/delete |
|
||||
| `record query` | `recordId` | record update/delete;按 ID 反查字段值用 `record get` |
|
||||
| `template search` | `templateId` | base create --template-id,拼接模板预览 URI |
|
||||
|
||||
## URL → baseId 提取
|
||||
|
||||
用户提供 `https://alidocs.dingtalk.com/i/nodes/{baseId}` 链接时:
|
||||
1. 提取 `/nodes/` 后的路径段作为 `baseId`
|
||||
2. 去掉尾部的查询参数(`?` 及其后内容)
|
||||
3. 传入 `--base-id` 参数
|
||||
|
||||
> 如果该 URL 来自 `dws aitable` 返回或已在当前链路 probe 过,可直接复用;
|
||||
> 如果是用户直接提供的原始 `alidocs` URL,则先按 [链接规范](url-patterns.md#alidocs-url-类型探测流程) probe,确认 `extension=able` 后再继续。
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 所有操作使用 ID(baseId/tableId/fieldId/recordId),不使用名称
|
||||
- records 的 cells key 是 fieldId,不是字段名称
|
||||
- cells 写入/读取格式见 [aitable-cell-value.md](./aitable/aitable-cell-value.md)
|
||||
- 最佳实践见 [aitable-best-practices.md](./aitable/aitable-best-practices.md)
|
||||
|
||||
## 自动化脚本
|
||||
|
||||
| 脚本 | 场景 |
|
||||
|------|------|
|
||||
| [bulk_add_fields.py](../scripts/bulk_add_fields.py) | 批量添加字段 |
|
||||
| [import_records.py](../scripts/import_records.py) | 从 JSON/CSV 批量导入记录 |
|
||||
| [aitable_import_via_task.py](../scripts/aitable_import_via_task.py) | 导入 CSV/XLS/XLSX 并新建数据表(prepare + PUT + import) |
|
||||
| [aitable_export_via_task.py](../scripts/aitable_export_via_task.py) | 文件导出(export_data 轮询 + 下载) |
|
||||
| [upload_attachment.py](../scripts/upload_attachment.py) | 上传附件到 AI 表格记录 |
|
||||
|
||||
## 相关产品
|
||||
|
||||
- [doc](../../dingtalk-doc/references/doc.md) — 富文本文档编辑,不是结构化数据表格
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# advperm — 高级权限管理
|
||||
|
||||
> 加载边界:仅在开启/关闭高级权限或管理自定义角色时读取。普通协作者、文件权限或成员管理不走本页。`baseId` 和 `roleId` 必须来自当前链路;disable、role-delete 等需确认的动作先读现状、固化影响,再执行并回读。
|
||||
|
||||
控制 Base 的高级权限总开关,并管理自定义角色(增删改查 + 子角色权限规则)。
|
||||
适用场景:"如何控制谁能看/改 Base 数据"、"开启/关闭高级权限"、"新建/修改/删除角色"、"按字段或行配置权限"。
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# attachment — 附件上传
|
||||
|
||||
> 加载边界:仅在文件要写入 AITable 的 attachment 字段时读取;普通钉盘上传走 Drive。流程必须完成“申请凭证 → HTTPS PUT → 用 fileToken 写记录 → 按 recordId 回读”,拿到 fileToken 不等于附件已进入记录。
|
||||
|
||||
> **STOP — 不要使用钉盘 (drive) 上传!** 钉盘 fileId 无法写入 attachment 字段。必须使用以下流程。
|
||||
>
|
||||
> **STOP — 严禁在 record create/update 的 cells 里直接传图片 URL!** 直传 `{"url":"https://..."}` 会导致服务端同步下载图片,批量写入时触发 TIMEOUT_ERROR。正确做法:先 `attachment upload` 获取 `fileToken`,再用 `{"fileToken":"ft_xxx"}` 写入。
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# AI 表格最佳实践
|
||||
|
||||
> 定位:跨主题的不变量摘要,用于复杂任务复核,不作为命令索引。若根 Skill 已给出明确骨架,不要为了普通读写预加载本页;具体字段值、过滤或错误恢复分别读取对应专题。
|
||||
|
||||
## 1. 字段可写性分类
|
||||
|
||||
| 字段类型 | 可写 | 正确方式 |
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# cells 写入/读取格式规范(cellValue 数据结构)
|
||||
|
||||
> 加载边界:仅在已经选定 record create/update/upsert 且需要构造某种字段值时读取。先用 field get 确认 `fieldId`、`type`、`config` 和可写性;本页不负责找 Base/Table/Record,也不能把字段名当 fieldId。
|
||||
|
||||
> 适用命令:`dws aitable record create --records`、`dws aitable record update --records`、`dws aitable record query` 返回
|
||||
>
|
||||
> 本文件是 DWS AI 表格 cellValue 的 **source of truth**。写入记录时,必须严格按此格式构造 cells 对象。
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# dashboard & chart — 仪表盘与图表
|
||||
|
||||
> 加载边界:仅在用户明确操作仪表盘或图表时读取。先用当前 Base 的 dashboard list/get 获取 `dashboardId`,再获取其 `chartId`;不要用 View 或 Table 命令替代,也不要跨 Dashboard 复用 chartId。
|
||||
|
||||
## 建议操作顺序
|
||||
|
||||
```bash
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# AI 表格数据分析 SOP
|
||||
|
||||
> 加载边界:用于需要全表统计、分组、排名或业务结论的任务。普通记录定位只读 record-query 专题。任何“全部/最高/总数”结论都必须携带分页完整性;未拉全时只能报告当前范围和 continuation。
|
||||
|
||||
> 当用户诉求涉及查询、筛选、排序、统计、Top/Bottom N、分组聚合、判断全局结论时,必须先读本文档再执行。
|
||||
|
||||
## 1. 查询决策树
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# AI 表格错误恢复指南
|
||||
|
||||
> 加载边界:仅在真实命令或业务返回失败后读取,不做预防性全量加载。先保留原错误、阶段、对象 ID 和已成功 ledger;只有能证明未写入且契约允许时才重试,状态未知和需确认写操作禁止自动重放。
|
||||
|
||||
> 当 CLI 命令返回错误时,按本文档的映射表判断恢复动作。
|
||||
|
||||
## 1. 错误响应结构
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# export & import — 导入导出
|
||||
|
||||
> 加载边界:仅在文件与 AITable 之间迁移数据时读取。“追加到已有 Table”与“导入成新 Table”必须先分流。`taskId/importId` 只用于对应任务状态,完成后必须验证新 tableId 或本地输出文件;任务已创建不等于业务完成。
|
||||
|
||||
## 导出数据(两阶段轮询)
|
||||
|
||||
`export data` 为异步任务:首次调用可能只返回 `taskId`,需要继续轮询。
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# 字段类型 config 规范(field create / table create / field update)
|
||||
|
||||
> 加载边界:仅在创建/更新字段或建表时构造字段 `config`。先确认字段类型及当前 Table;记录 cellValue 结构读取 [aitable-cell-value.md](aitable-cell-value.md),公式正文读取 [aitable-formula-guide.md](aitable-formula-guide.md),不要在本页寻找记录写入路由。
|
||||
|
||||
> 适用命令:`dws aitable field create`、`dws aitable table create --fields`、`dws aitable field update --config`
|
||||
>
|
||||
> 本文件是 DWS AI 表格字段 config 的 **source of truth**。创建/更新字段时,必须严格按此规范构造 JSON。
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# field — 字段管理
|
||||
|
||||
> 加载边界:仅在字段目录不足、需要完整配置或执行字段 CRUD 时读取。`baseId/tableId` 先由根 Skill 解析;写前校验类型、config、关联表和只读性,写后用 field get 回读。删除字段先固化影响并确认。
|
||||
|
||||
## field get — 获取字段详情
|
||||
|
||||
```
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# filters & sort — 筛选排序语法参考
|
||||
|
||||
> 加载边界:仅在 record query 或 view 配置需要构造 filter/sort JSON 时读取。先用 field get 取得真实 fieldId 和类型;服务端可过滤时不先拉全量,本页语法不能替代分页完整性判断。
|
||||
|
||||
> 视图(view)配置的 filter/sort/group **整体写入**请优先用 `view update filter` / `view update sort` / `view update group` 子命令,详见 [aitable-view-config.md](./aitable-view-config.md)。本文件聚焦于 `record query --filters` 与 view config filter 的语法和差异。
|
||||
|
||||
## filters 结构规范
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# form — 表单管理
|
||||
|
||||
> 加载边界:仅在用户明确创建、配置或分享表单视图时读取。先绑定当前 Base/Table,再从创建/list 返回取得 viewId 和 fieldId;表单分享不等于记录分享,也不等于开放 Base 权限。
|
||||
|
||||
## 命令一览
|
||||
|
||||
| 命令 | 用途 |
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# AI 表格公式字段指南
|
||||
|
||||
> 加载边界:仅在创建或修改 formula 字段时读取。公式字段是只读派生字段,不能通过 record create/update 写值;字段引用必须基于当前 Table 的精确名称/ID,跨表取值应先分流到 lookup/filterUp。
|
||||
|
||||
> 当用户要创建 formula 类型字段、编写表内计算公式、做派生指标时,必须先读本文档。
|
||||
|
||||
## 1. 何时使用 formula 字段
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# 主键文档管理
|
||||
|
||||
> 加载边界:仅在记录的 primaryDoc 字段需要查询或创建关联文档时读取。先取得真实 baseId/tableId/recordId/fieldId;创建返回 nodeId 后,文档正文交给 Doc Skill,并持续使用该 nodeId,不再按标题搜索。
|
||||
|
||||
## 适用场景
|
||||
|
||||
当需要为 AI 表格中的记录创建或查询关联的主键文档时使用。主键文档是 primaryDoc 类型字段对应的钉钉在线文档,可通过 `dws doc` 进行内容读写。
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# record create — 新增记录
|
||||
|
||||
> 加载边界:仅在目标 Base/Table 已唯一确定且字段目录已取得后读取。第一笔写入前校验全部 cells;创建返回 `newRecordIds[]` 后按 ID 回读。不得因部分字段失败而静默丢列或改类型。
|
||||
|
||||
## 命令格式
|
||||
|
||||
```
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# record delete — 删除记录
|
||||
|
||||
> 加载边界:仅在用户明确要求删除记录时读取。先 query 并展示真实 recordId、关键字段和数量,确认前零删除调用;删除后重新按 ID 查询验证不存在,状态未知时禁止自动重试。
|
||||
|
||||
## 命令格式
|
||||
|
||||
```
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# 行记录变更历史(record history-list)
|
||||
|
||||
> 加载边界:仅在审计某条真实 recordId 的历史变更时读取。历史分页与当前记录查询是不同契约;未遍历完成不得声称“全部历史”,历史事件也不能直接当作当前 cells 状态。
|
||||
|
||||
按 recordId 查询单条记录的全部变更历史,用于审计、回溯字段变更、定位操作人。
|
||||
|
||||
## 命令
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# 行命名规则枚举键(recordNameKey)映射
|
||||
|
||||
> 加载边界:仅在配置 Table 的行称谓时读取。自然语言名称必须映射为已审阅枚举键;本页不用于记录查找、字段名解析或写入 cells。
|
||||
|
||||
`dws aitable table update --record-name-key <枚举键>` 用于设置数据表的"行命名规则"——卡片/详情页里"行"的展示别名。**取值是固定枚举,不是字段 ID**;传非法值服务端返回 `INVALID_RECORD_NAME_KEY`。
|
||||
|
||||
## 中文 → 枚举键(按 UI 下拉顺序)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# record query — 查询记录
|
||||
|
||||
> 加载边界:用于目标 Base/Table 已确定后的记录查询、筛选、排序和分页。按 ID、关键词和 filters 三种模式先分流;全量结论必须检查 `--all`、page-limit、hasMore/nextCursor 和 stop reason,默认一页只能代表当前页。
|
||||
|
||||
## 命令格式
|
||||
|
||||
```
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# 行记录分享链接(record share-url)
|
||||
|
||||
> 加载边界:仅在用户需要某条真实记录的分享链接时读取。recordId 必须属于当前 Base/Table;可选 viewId 也必须同表。生成链接不等于修改记录权限或把链接发送给他人。
|
||||
|
||||
按 recordId 批量获取记录的分享链接,把某行单独发给同事查看。
|
||||
|
||||
## 命令
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# record update — 更新记录
|
||||
|
||||
> 加载边界:仅在 recordId 和待修改 fieldId 已确定后读取。只提交用户要求变更的 cells;写前读取字段类型/只读性,写后使用返回 recordId 回读。未返回 ID、字段未变化或状态未知都不能声称成功。
|
||||
|
||||
## 命令格式
|
||||
|
||||
```
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# 行记录 Upsert(record upsert)
|
||||
|
||||
> 加载边界:仅在同一批次明确混合“带 recordId 更新”和“不带 recordId 创建”时读取。若目标匹配逻辑仍依赖名称或业务键,先 query 并消歧;upsert 不负责猜测 recordId,部分成功必须返回逐项 ledger。
|
||||
|
||||
按 `recordId` 是否存在,自动把入参拆分到 update 链路或 create 链路:批次混合"已存在改 + 新出现建"时用,省掉客户端按 ID 分批的逻辑。
|
||||
|
||||
## 命令
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# 视图配置(view get/update <attr>)
|
||||
|
||||
> 加载边界:仅在创建视图或读取/修改视图配置时加载。先确认 viewType 与属性支持矩阵,并绑定当前 Base/Table/viewId;写后读取同一属性验证。复制、锁定、行高和高亮规则改读 [aitable-view-extras.md](aitable-view-extras.md)。
|
||||
|
||||
按属性局部读/写视图配置。每个属性独立子命令,typed flag 友好,agent 不必拼 JSON。
|
||||
向后兼容:`view update --config '{...}'` 一次多属性入口仍可用。
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# 视图扩展操作(lock / frozen-cols / row-height / fill-color-rule / duplicate)
|
||||
|
||||
> 加载边界:仅在用户明确操作锁定、冻结列、行高、高亮规则或复制视图时读取。所有动作复用当前 Base/Table/viewId;viewType 不支持时本地停止,写后用对应 get 或新 viewId 回读。
|
||||
|
||||
本文档讲 5 项视图操作命令:
|
||||
|
||||
- 锁定 / 解锁视图:`view lock` / `view get lock`
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# workflow — 自动化工作流管理
|
||||
|
||||
> 加载边界:仅在创建、全量更新、启停或检查 AITable 自动化工作流时读取。create/update 使用完整 DSL,先校验并保留现状;disable 等需确认动作确认前零写入,返回 flowId 后用 get/list 验证 `valid/issues/status`。
|
||||
|
||||
创建 / 更新 / 启停 / 查看 / 列出 Base 下的自动化工作流("当 X 时自动 Y" 流程)。
|
||||
适用场景:用户要求创建自动化、修改流程、停掉流程、查询已有流程或恢复运行。
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# 易混淆操作与字段规则
|
||||
|
||||
> 定位:字段操作的轻量防错入口,仅在建表/建字段前快速核对主字段、只读字段和附件边界。完整 config 读取 `aitable/aitable-field-properties.md`,cellValue 读取 `aitable/aitable-cell-value.md`;不要与两份专题同时全量加载。
|
||||
|
||||
## 易混淆操作 (高风险场景必读)
|
||||
|
||||
| 用户说的 | 正确命令 | 不是这个 |
|
||||
@@ -36,6 +38,13 @@
|
||||
|
||||
## 字段创建时设置 config(重要)
|
||||
|
||||
字段 JSON 的命名和枚举是严格契约:
|
||||
|
||||
- 批量 `--fields` 每项使用 `fieldName`,不是 `name`。
|
||||
- 单选是 `singleSelect`,多选是 `multipleSelect`;不存在通用的 `select` 类型。
|
||||
- number/date/currency formatter 只使用当前 `table create --help` 或 leaf Schema 列出的枚举;不要从别的产品或旧样例猜 `INTEGER`。
|
||||
- 第一次返回明确 hint 后只按 hint 修正一次;第二次仍失败就停止并保留部分成功 ledger。
|
||||
|
||||
创建 singleSelect/multipleSelect 字段时,**必须设置选项 (options)**:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# aitable 局部意图消歧
|
||||
|
||||
> 定位:只处理 AITable 与 Sheet、Doc、Drive、Minutes 等 sibling 产品的边界;产品内 Base/Table/Field/Record 路由由根 Skill 负责。用户意图已经明确属于 AITable 时无需加载本页。
|
||||
|
||||
本文件从单 Skill `intent-guide.md` 拆分而来,仅保留与本产品相关的跨产品消歧规则。
|
||||
|
||||
| 用户说... | 真实意图 | 应该用 | 不要用 | 理由 |
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# URL 格式与处理规范
|
||||
|
||||
> 加载边界:仅在用户给出类型不明的 alidocs URL 且意图不足以直接分流时读取。明确的 AITable 操作优先按意图执行;探测只负责确定资源类型,不应成为每次调用前置步骤,也不能把短链或裸 key 猜成 baseId。
|
||||
|
||||
## 路由第 0 步:意图直达(优先级高于 URL 探测)
|
||||
|
||||
用户已经明确表达某产品的内容意图时,直接进入对应产品场域,不要先做 URL 类型
|
||||
|
||||
@@ -17,6 +17,7 @@ from __future__ import annotations
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import ssl
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
@@ -26,7 +27,7 @@ from urllib.error import HTTPError, URLError
|
||||
from urllib.parse import urlparse
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
RESOURCE_ID_PATTERN = re.compile(r"^[A-Za-z0-9_-]{8,128}$")
|
||||
RESOURCE_ID_PATTERN = re.compile(r"^[A-Za-z0-9_-]{6,128}$")
|
||||
ALLOWED_FORMATS = {"excel", "attachment", "excel_and_attachment", "excel_with_inline_images"}
|
||||
|
||||
|
||||
@@ -64,7 +65,12 @@ def normalize_download_url(url: str) -> str:
|
||||
def download_file(url: str, output_path: Path) -> Tuple[bool, str]:
|
||||
req = Request(url, method="GET")
|
||||
try:
|
||||
with urlopen(req, timeout=180) as resp:
|
||||
try:
|
||||
import certifi
|
||||
context = ssl.create_default_context(cafile=certifi.where())
|
||||
except ImportError:
|
||||
context = ssl.create_default_context()
|
||||
with urlopen(req, timeout=180, context=context) as resp:
|
||||
redirected = urlparse(resp.geturl())
|
||||
if redirected.scheme != "https" or not redirected.hostname:
|
||||
return False, "download redirect is not HTTPS"
|
||||
|
||||
@@ -17,6 +17,7 @@ from __future__ import annotations
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import ssl
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
@@ -25,7 +26,7 @@ from urllib.error import HTTPError, URLError
|
||||
from urllib.parse import urlparse
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
RESOURCE_ID_PATTERN = re.compile(r"^[A-Za-z0-9_-]{8,128}$")
|
||||
RESOURCE_ID_PATTERN = re.compile(r"^[A-Za-z0-9_-]{6,128}$")
|
||||
ALLOWED_EXTENSIONS = {".csv", ".xlsx", ".xls"}
|
||||
|
||||
|
||||
@@ -33,6 +34,24 @@ def validate_resource_id(resource_id: str) -> bool:
|
||||
return bool(resource_id and RESOURCE_ID_PATTERN.match(resource_id.strip()))
|
||||
|
||||
|
||||
def normalize_oss_upload_url(upload_url: str) -> str:
|
||||
"""Accept the signed OSS URL returned by Runtime and upgrade it to HTTPS."""
|
||||
parsed = urlparse(upload_url)
|
||||
if parsed.scheme not in {"http", "https"} or not parsed.hostname:
|
||||
raise ValueError("uploadUrl must be a valid HTTP(S) URL")
|
||||
if not parsed.hostname.endswith(".oss.aliyuncs.com"):
|
||||
raise ValueError("uploadUrl host is not an Aliyun OSS endpoint")
|
||||
return parsed._replace(scheme="https").geturl()
|
||||
|
||||
|
||||
def tls_context() -> ssl.SSLContext:
|
||||
try:
|
||||
import certifi
|
||||
return ssl.create_default_context(cafile=certifi.where())
|
||||
except ImportError:
|
||||
return ssl.create_default_context()
|
||||
|
||||
|
||||
def run_dws(dws_bin: str, args: list[str], timeout_sec: int = 120) -> Tuple[int, str, str]:
|
||||
cmd = [dws_bin] + args
|
||||
try:
|
||||
@@ -53,15 +72,16 @@ def parse_json_output(raw: str) -> Optional[Dict[str, Any]]:
|
||||
|
||||
|
||||
def put_file(upload_url: str, file_path: Path) -> Tuple[bool, str]:
|
||||
parsed = urlparse(upload_url)
|
||||
if parsed.scheme != "https" or not parsed.hostname:
|
||||
return False, "uploadUrl must be a valid HTTPS URL"
|
||||
try:
|
||||
upload_url = normalize_oss_upload_url(upload_url)
|
||||
except ValueError as exc:
|
||||
return False, str(exc)
|
||||
payload = file_path.read_bytes()
|
||||
req = Request(upload_url, data=payload, method="PUT")
|
||||
# 关键:清空 Content-Type,避免 SignatureDoesNotMatch。
|
||||
req.add_header("Content-Type", "")
|
||||
try:
|
||||
with urlopen(req, timeout=180) as resp:
|
||||
with urlopen(req, timeout=180, context=tls_context()) as resp:
|
||||
if resp.status == 200:
|
||||
return True, ""
|
||||
return False, f"unexpected HTTP status: {resp.status}"
|
||||
|
||||
@@ -25,13 +25,14 @@ import subprocess
|
||||
import os
|
||||
import mimetypes
|
||||
import re
|
||||
import ssl
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any
|
||||
from urllib.request import Request, urlopen
|
||||
from urllib.error import HTTPError, URLError
|
||||
from urllib.parse import urlparse
|
||||
|
||||
RESOURCE_ID_PATTERN = re.compile(r'^[A-Za-z0-9_-]{8,128}$')
|
||||
RESOURCE_ID_PATTERN = re.compile(r'^[A-Za-z0-9_-]{6,128}$')
|
||||
MAX_FILE_SIZE = 100 * 1024 * 1024 # 100MB
|
||||
|
||||
|
||||
@@ -77,15 +78,24 @@ def run_dws(args: list, dws_bin: str = 'dws') -> Optional[Dict[str, Any]]:
|
||||
def upload_to_oss(upload_url: str, file_path: Path, mime_type: str) -> bool:
|
||||
"""通过 HTTP PUT 上传文件到 OSS。"""
|
||||
parsed = urlparse(upload_url)
|
||||
if parsed.scheme != 'https' or not parsed.hostname:
|
||||
print('错误:uploadUrl 必须是有效的 HTTPS URL', file=sys.stderr)
|
||||
if parsed.scheme not in {'http', 'https'} or not parsed.hostname:
|
||||
print('错误:uploadUrl 必须是有效的 HTTP(S) URL', file=sys.stderr)
|
||||
return False
|
||||
if not parsed.hostname.endswith('.oss.aliyuncs.com'):
|
||||
print('错误:uploadUrl 不是阿里云 OSS 地址', file=sys.stderr)
|
||||
return False
|
||||
upload_url = parsed._replace(scheme='https').geturl()
|
||||
file_data = file_path.read_bytes()
|
||||
req = Request(upload_url, data=file_data, method='PUT')
|
||||
req.add_header('Content-Type', mime_type)
|
||||
|
||||
try:
|
||||
with urlopen(req, timeout=120) as resp:
|
||||
try:
|
||||
import certifi
|
||||
context = ssl.create_default_context(cafile=certifi.where())
|
||||
except ImportError:
|
||||
context = ssl.create_default_context()
|
||||
with urlopen(req, timeout=120, context=context) as resp:
|
||||
if resp.status == 200:
|
||||
return True
|
||||
print(f"错误:OSS 上传失败,HTTP {resp.status}", file=sys.stderr)
|
||||
|
||||
@@ -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