Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
468a418d3a | ||
|
|
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{
|
||||
|
||||
@@ -4713,7 +4713,8 @@
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"仅在用户没有指定块操作、只要整篇追加 Markdown 时优先 doc update --mode append;显式 insert/标题块/列表块不能折叠",
|
||||
"插入本地文件附件优先 doc media insert",
|
||||
"插入本地文件附件优先 doc media insert;不要为普通附件手写 attachment JSON",
|
||||
"整段 Markdown 或长内容优先 doc update --mode append,不要拆成大量 block insert",
|
||||
"删块用 block delete;改已有块用 block update"
|
||||
],
|
||||
"confirmation": "not_required",
|
||||
@@ -4765,7 +4766,8 @@
|
||||
"avoid_when": {
|
||||
"value": [
|
||||
"仅在用户没有指定块操作、只要整篇追加 Markdown 时优先 doc update --mode append;显式 insert/标题块/列表块不能折叠",
|
||||
"插入本地文件附件优先 doc media 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",
|
||||
@@ -4776,7 +4778,8 @@
|
||||
{
|
||||
"value": [
|
||||
"仅在用户没有指定块操作、只要整篇追加 Markdown 时优先 doc update --mode append;显式 insert/标题块/列表块不能折叠",
|
||||
"插入本地文件附件优先 doc media 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",
|
||||
@@ -4926,7 +4929,8 @@
|
||||
"use_when": {
|
||||
"value": [
|
||||
"用户明确要求在文档中插入新块(段落/标题/列表等)时;简单段落/标题用 --text/--heading,复杂块用 --element JSON",
|
||||
"用户要求有序列表块时使用 JSONML p.list.isOrdered=true(同一 listId)或等价原生 orderedList 结构,不能用普通 Markdown 代替"
|
||||
"用户要求有序列表块时使用 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",
|
||||
@@ -4936,7 +4940,8 @@
|
||||
{
|
||||
"value": [
|
||||
"用户明确要求在文档中插入新块(段落/标题/列表等)时;简单段落/标题用 --text/--heading,复杂块用 --element JSON",
|
||||
"用户要求有序列表块时使用 JSONML p.list.isOrdered=true(同一 listId)或等价原生 orderedList 结构,不能用普通 Markdown 代替"
|
||||
"用户要求有序列表块时使用 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",
|
||||
@@ -4971,7 +4976,8 @@
|
||||
],
|
||||
"use_when": [
|
||||
"用户明确要求在文档中插入新块(段落/标题/列表等)时;简单段落/标题用 --text/--heading,复杂块用 --element JSON",
|
||||
"用户要求有序列表块时使用 JSONML p.list.isOrdered=true(同一 listId)或等价原生 orderedList 结构,不能用普通 Markdown 代替"
|
||||
"用户要求有序列表块时使用 JSONML p.list.isOrdered=true(同一 listId)或等价原生 orderedList 结构,不能用普通 Markdown 代替",
|
||||
"相对插入时先 block list 获取真实 blockId,再同时传 --ref-block 与 --where before|after"
|
||||
]
|
||||
},
|
||||
"doc block list": {
|
||||
@@ -5251,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",
|
||||
@@ -5301,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",
|
||||
@@ -5311,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",
|
||||
@@ -5457,7 +5466,7 @@
|
||||
},
|
||||
"use_when": {
|
||||
"value": [
|
||||
"修改已有块的文本/标题/样式(已知 blockId)时"
|
||||
"修改已有块的文本/标题/样式时;blockId 必须来自目标文档本次 block list 结果"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -5466,7 +5475,7 @@
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"修改已有块的文本/标题/样式(已知 blockId)时"
|
||||
"修改已有块的文本/标题/样式时;blockId 必须来自目标文档本次 block list 结果"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -5500,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": {
|
||||
@@ -6647,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",
|
||||
@@ -6697,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",
|
||||
@@ -6706,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",
|
||||
@@ -6900,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",
|
||||
@@ -6909,7 +6921,7 @@
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 来自 list/create"
|
||||
"回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 必须来自本次 list/create 返回"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -6944,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": {
|
||||
@@ -6952,8 +6964,7 @@
|
||||
"agent_summary_source": "dws-agent-selection/doc",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"删除评论用 delete;回复用 reply",
|
||||
"响应为 null/空对象或回查仍是旧正文时不能判成功,必须报告更新未生效或部分完成"
|
||||
"删除评论用 delete;回复用 reply"
|
||||
],
|
||||
"confirmation": "not_required",
|
||||
"effect": "write",
|
||||
@@ -6968,14 +6979,14 @@
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工对齐评论更新的业务结果与 comment list 回查语义;null/空响应不能单独证明成功,不改变命令身份、参数契约或接口绑定。",
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "更新指定文档评论的文字内容和可选 @用户/@群。",
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "人工对齐评论更新的业务结果与 comment list 回查语义;null/空响应不能单独证明成功,不改变命令身份、参数契约或接口绑定。"
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -6997,23 +7008,21 @@
|
||||
},
|
||||
"avoid_when": {
|
||||
"value": [
|
||||
"删除评论用 delete;回复用 reply",
|
||||
"响应为 null/空对象或回查仍是旧正文时不能判成功,必须报告更新未生效或部分完成"
|
||||
"删除评论用 delete;回复用 reply"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工对齐评论更新的业务结果与 comment list 回查语义;null/空响应不能单独证明成功,不改变命令身份、参数契约或接口绑定。",
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"删除评论用 delete;回复用 reply",
|
||||
"响应为 null/空对象或回查仍是旧正文时不能判成功,必须报告更新未生效或部分完成"
|
||||
"删除评论用 delete;回复用 reply"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "人工对齐评论更新的业务结果与 comment list 回查语义;null/空响应不能单独证明成功,不改变命令身份、参数契约或接口绑定。"
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -7069,7 +7078,7 @@
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工对齐评论更新的业务结果与 comment list 回查语义;null/空响应不能单独证明成功,不改变命令身份、参数契约或接口绑定。",
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
@@ -7079,7 +7088,7 @@
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "人工对齐评论更新的业务结果与 comment list 回查语义;null/空响应不能单独证明成功,不改变命令身份、参数契约或接口绑定。"
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -7172,7 +7181,7 @@
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": false,
|
||||
"review_reason": "人工对齐评论更新的业务结果与 comment list 回查语义;null/空响应不能单独证明成功,不改变命令身份、参数契约或接口绑定。"
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -7200,21 +7209,21 @@
|
||||
},
|
||||
"use_when": {
|
||||
"value": [
|
||||
"修改已有评论正文;可选更新 --mention 或 --mentioned-open-conversation-id;执行后必须用 comment list 回查同一 commentKey 的目标字段"
|
||||
"修改已有评论正文;可选更新 --mention 或 --mentioned-open-conversation-id"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工对齐评论更新的业务结果与 comment list 回查语义;null/空响应不能单独证明成功,不改变命令身份、参数契约或接口绑定。",
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"修改已有评论正文;可选更新 --mention 或 --mentioned-open-conversation-id;执行后必须用 comment list 回查同一 commentKey 的目标字段"
|
||||
"修改已有评论正文;可选更新 --mention 或 --mentioned-open-conversation-id"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "人工对齐评论更新的业务结果与 comment list 回查语义;null/空响应不能单独证明成功,不改变命令身份、参数契约或接口绑定。"
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -7235,7 +7244,7 @@
|
||||
"structured-hint:internal/cli/schema_hints/products/doc.json"
|
||||
],
|
||||
"use_when": [
|
||||
"修改已有评论正文;可选更新 --mention 或 --mentioned-open-conversation-id;执行后必须用 comment list 回查同一 commentKey 的目标字段"
|
||||
"修改已有评论正文;可选更新 --mention 或 --mentioned-open-conversation-id"
|
||||
]
|
||||
},
|
||||
"doc copy": {
|
||||
@@ -7506,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",
|
||||
@@ -7558,8 +7567,8 @@
|
||||
"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",
|
||||
@@ -7569,8 +7578,8 @@
|
||||
{
|
||||
"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",
|
||||
@@ -7781,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"
|
||||
],
|
||||
@@ -7832,7 +7841,7 @@
|
||||
},
|
||||
"avoid_when": {
|
||||
"value": [
|
||||
"常规文件删除优先 dws drive delete;本入口仅为兼容",
|
||||
"常规文件删除使用 dws drive delete;不要继续选择已弃用的 doc delete 兼容入口",
|
||||
"用户未确认或目标不清时不要删",
|
||||
"删块用 doc block delete;删评论用 doc comment delete"
|
||||
],
|
||||
@@ -7843,7 +7852,7 @@
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"常规文件删除优先 dws drive delete;本入口仅为兼容",
|
||||
"常规文件删除使用 dws drive delete;不要继续选择已弃用的 doc delete 兼容入口",
|
||||
"用户未确认或目标不清时不要删",
|
||||
"删块用 doc block delete;删评论用 doc comment delete"
|
||||
],
|
||||
@@ -9101,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",
|
||||
@@ -9144,7 +9153,7 @@
|
||||
},
|
||||
"avoid_when": {
|
||||
"value": [
|
||||
"发起导入用 doc import(若入口可用);不要用本命令代替导入"
|
||||
"发起导入用 doc import;不要先调用 import get,也不要使用示例占位 taskId"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -9153,7 +9162,7 @@
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"发起导入用 doc import(若入口可用);不要用本命令代替导入"
|
||||
"发起导入用 doc import;不要先调用 import get,也不要使用示例占位 taskId"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -9343,7 +9352,7 @@
|
||||
},
|
||||
"use_when": {
|
||||
"value": [
|
||||
"查询文档导入任务结果(已有 taskId,导入超时/中断后兜底)时"
|
||||
"仅在 doc import 已返回真实 taskId 且自动轮询超时/中断后,查询导入任务结果时"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -9352,7 +9361,7 @@
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"查询文档导入任务结果(已有 taskId,导入超时/中断后兜底)时"
|
||||
"仅在 doc import 已返回真实 taskId 且自动轮询超时/中断后,查询导入任务结果时"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -9379,7 +9388,7 @@
|
||||
"structured-hint:internal/cli/schema_hints/products/doc.json"
|
||||
],
|
||||
"use_when": [
|
||||
"查询文档导入任务结果(已有 taskId,导入超时/中断后兜底)时"
|
||||
"仅在 doc import 已返回真实 taskId 且自动轮询超时/中断后,查询导入任务结果时"
|
||||
]
|
||||
},
|
||||
"doc info": {
|
||||
@@ -9928,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",
|
||||
@@ -9977,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",
|
||||
@@ -9986,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",
|
||||
@@ -10132,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",
|
||||
@@ -10141,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",
|
||||
@@ -10173,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": {
|
||||
@@ -14678,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",
|
||||
@@ -14731,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",
|
||||
@@ -14743,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",
|
||||
@@ -14926,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",
|
||||
@@ -14937,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",
|
||||
@@ -14976,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": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"version": 1,
|
||||
"source_hash": "sha256:f239237a9b87fa5a95a0520b2e2f2a112e117b273de8418763ba0ecd8652b317",
|
||||
"source_hash": "sha256:35727a6c324fbffde5271e96d972843abdf780391151ba858f6d04384268cd51",
|
||||
"surface_hash": "sha256:4897d582a472d9d3596f88fb4546e7795d6b9bc334d020712f5d895ce894f7a7",
|
||||
"coverage": {
|
||||
"surface_products": 26,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"version": 1,
|
||||
"source_hash": "sha256:f239237a9b87fa5a95a0520b2e2f2a112e117b273de8418763ba0ecd8652b317",
|
||||
"source_hash": "sha256:35727a6c324fbffde5271e96d972843abdf780391151ba858f6d04384268cd51",
|
||||
"surface_hash": "sha256:4897d582a472d9d3596f88fb4546e7795d6b9bc334d020712f5d895ce894f7a7",
|
||||
"source_files": 160,
|
||||
"hint_files": 54,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"version": 1,
|
||||
"surface_hash": "sha256:4897d582a472d9d3596f88fb4546e7795d6b9bc334d020712f5d895ce894f7a7",
|
||||
"source_hash": "sha256:eddcc39cfdf906e0b47c896abc081fd2b5fc81150023f99938deb9def8b07d9e",
|
||||
"source_hash": "sha256:807d11cf6c62a0d4aaca4b5a6b438e6654b2792d3a32b4e271032202d45ff8fa",
|
||||
"catalog": {
|
||||
"agent_metadata": {
|
||||
"products_with_metadata": 26,
|
||||
"source": "embedded-skill-metadata",
|
||||
"source_hash": "sha256:f239237a9b87fa5a95a0520b2e2f2a112e117b273de8418763ba0ecd8652b317",
|
||||
"source_hash": "sha256:35727a6c324fbffde5271e96d972843abdf780391151ba858f6d04384268cd51",
|
||||
"surface_hash": "sha256:4897d582a472d9d3596f88fb4546e7795d6b9bc334d020712f5d895ce894f7a7",
|
||||
"surface_products": 26,
|
||||
"surface_tools": 875,
|
||||
@@ -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",
|
||||
@@ -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 且自动轮询超时/中断后,查询导入任务结果时"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -15792,7 +15793,8 @@
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"仅在用户没有指定块操作、只要整篇追加 Markdown 时优先 doc update --mode append;显式 insert/标题块/列表块不能折叠",
|
||||
"插入本地文件附件优先 doc media insert",
|
||||
"插入本地文件附件优先 doc media insert;不要为普通附件手写 attachment JSON",
|
||||
"整段 Markdown 或长内容优先 doc update --mode append,不要拆成大量 block insert",
|
||||
"删块用 block delete;改已有块用 block update"
|
||||
],
|
||||
"canonical_path": "doc.insert_document_block",
|
||||
@@ -15816,7 +15818,8 @@
|
||||
"title": "插入块元素",
|
||||
"use_when": [
|
||||
"用户明确要求在文档中插入新块(段落/标题/列表等)时;简单段落/标题用 --text/--heading,复杂块用 --element JSON",
|
||||
"用户要求有序列表块时使用 JSONML p.list.isOrdered=true(同一 listId)或等价原生 orderedList 结构,不能用普通 Markdown 代替"
|
||||
"用户要求有序列表块时使用 JSONML p.list.isOrdered=true(同一 listId)或等价原生 orderedList 结构,不能用普通 Markdown 代替",
|
||||
"相对插入时先 block list 获取真实 blockId,再同时传 --ref-block 与 --where before|after"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -16108,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",
|
||||
@@ -16130,7 +16134,7 @@
|
||||
"risk": "medium",
|
||||
"title": "回复评论",
|
||||
"use_when": [
|
||||
"回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 来自 list/create"
|
||||
"回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 必须来自本次 list/create 返回"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -16800,8 +16804,7 @@
|
||||
"agent_summary_source": "dws-agent-selection/doc",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"删除评论用 delete;回复用 reply",
|
||||
"响应为 null/空对象或回查仍是旧正文时不能判成功,必须报告更新未生效或部分完成"
|
||||
"删除评论用 delete;回复用 reply"
|
||||
],
|
||||
"canonical_path": "doc.update_comment",
|
||||
"cli_name": "update",
|
||||
@@ -16819,7 +16822,7 @@
|
||||
"risk": "medium",
|
||||
"title": "更新文档评论",
|
||||
"use_when": [
|
||||
"修改已有评论正文;可选更新 --mention 或 --mentioned-open-conversation-id;执行后必须用 comment list 回查同一 commentKey 的目标字段"
|
||||
"修改已有评论正文;可选更新 --mention 或 --mentioned-open-conversation-id"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -16828,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",
|
||||
@@ -16854,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 转义损坏"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -16864,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",
|
||||
@@ -16886,7 +16891,7 @@
|
||||
"risk": "medium",
|
||||
"title": "更新块元素",
|
||||
"use_when": [
|
||||
"修改已有块的文本/标题/样式(已知 blockId)时"
|
||||
"修改已有块的文本/标题/样式时;blockId 必须来自目标文档本次 block list 结果"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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",
|
||||
@@ -2384,8 +2384,8 @@
|
||||
"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"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -2395,8 +2395,8 @@
|
||||
"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": {
|
||||
@@ -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": {
|
||||
@@ -11417,7 +11420,8 @@
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"仅在用户没有指定块操作、只要整篇追加 Markdown 时优先 doc update --mode append;显式 insert/标题块/列表块不能折叠",
|
||||
"插入本地文件附件优先 doc media insert",
|
||||
"插入本地文件附件优先 doc media insert;不要为普通附件手写 attachment JSON",
|
||||
"整段 Markdown 或长内容优先 doc update --mode append,不要拆成大量 block insert",
|
||||
"删块用 block delete;改已有块用 block update"
|
||||
],
|
||||
"canonical_path": "doc.insert_document_block",
|
||||
@@ -11489,7 +11493,8 @@
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"value": [
|
||||
"仅在用户没有指定块操作、只要整篇追加 Markdown 时优先 doc update --mode append;显式 insert/标题块/列表块不能折叠",
|
||||
"插入本地文件附件优先 doc media insert",
|
||||
"插入本地文件附件优先 doc media insert;不要为普通附件手写 attachment JSON",
|
||||
"整段 Markdown 或长内容优先 doc update --mode append,不要拆成大量 block insert",
|
||||
"删块用 block delete;改已有块用 block update"
|
||||
]
|
||||
}
|
||||
@@ -11500,7 +11505,8 @@
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"value": [
|
||||
"仅在用户没有指定块操作、只要整篇追加 Markdown 时优先 doc update --mode append;显式 insert/标题块/列表块不能折叠",
|
||||
"插入本地文件附件优先 doc media insert",
|
||||
"插入本地文件附件优先 doc media insert;不要为普通附件手写 attachment JSON",
|
||||
"整段 Markdown 或长内容优先 doc update --mode append,不要拆成大量 block insert",
|
||||
"删块用 block delete;改已有块用 block update"
|
||||
]
|
||||
},
|
||||
@@ -11726,7 +11732,8 @@
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"value": [
|
||||
"用户明确要求在文档中插入新块(段落/标题/列表等)时;简单段落/标题用 --text/--heading,复杂块用 --element JSON",
|
||||
"用户要求有序列表块时使用 JSONML p.list.isOrdered=true(同一 listId)或等价原生 orderedList 结构,不能用普通 Markdown 代替"
|
||||
"用户要求有序列表块时使用 JSONML p.list.isOrdered=true(同一 listId)或等价原生 orderedList 结构,不能用普通 Markdown 代替",
|
||||
"相对插入时先 block list 获取真实 blockId,再同时传 --ref-block 与 --where before|after"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -11736,7 +11743,8 @@
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"value": [
|
||||
"用户明确要求在文档中插入新块(段落/标题/列表等)时;简单段落/标题用 --text/--heading,复杂块用 --element JSON",
|
||||
"用户要求有序列表块时使用 JSONML p.list.isOrdered=true(同一 listId)或等价原生 orderedList 结构,不能用普通 Markdown 代替"
|
||||
"用户要求有序列表块时使用 JSONML p.list.isOrdered=true(同一 listId)或等价原生 orderedList 结构,不能用普通 Markdown 代替",
|
||||
"相对插入时先 block list 获取真实 blockId,再同时传 --ref-block 与 --where before|after"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -12879,7 +12887,8 @@
|
||||
"title": "插入块元素",
|
||||
"use_when": [
|
||||
"用户明确要求在文档中插入新块(段落/标题/列表等)时;简单段落/标题用 --text/--heading,复杂块用 --element JSON",
|
||||
"用户要求有序列表块时使用 JSONML p.list.isOrdered=true(同一 listId)或等价原生 orderedList 结构,不能用普通 Markdown 代替"
|
||||
"用户要求有序列表块时使用 JSONML p.list.isOrdered=true(同一 listId)或等价原生 orderedList 结构,不能用普通 Markdown 代替",
|
||||
"相对插入时先 block list 获取真实 blockId,再同时传 --ref-block 与 --where before|after"
|
||||
]
|
||||
},
|
||||
"doc.list_comments": {
|
||||
@@ -19708,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",
|
||||
@@ -19769,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"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -19778,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": {
|
||||
@@ -20056,7 +20068,7 @@
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"value": [
|
||||
"回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 来自 list/create"
|
||||
"回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 必须来自本次 list/create 返回"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -20065,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 返回"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -20726,7 +20738,7 @@
|
||||
"source": "reviewed_command_registry",
|
||||
"title": "回复评论",
|
||||
"use_when": [
|
||||
"回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 来自 list/create"
|
||||
"回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 必须来自本次 list/create 返回"
|
||||
]
|
||||
},
|
||||
"doc.search_documents": {
|
||||
@@ -37239,8 +37251,7 @@
|
||||
"agent_summary_source": "dws-agent-selection/doc",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"删除评论用 delete;回复用 reply",
|
||||
"响应为 null/空对象或回查仍是旧正文时不能判成功,必须报告更新未生效或部分完成"
|
||||
"删除评论用 delete;回复用 reply"
|
||||
],
|
||||
"canonical_path": "doc.update_comment",
|
||||
"cli_name": "update",
|
||||
@@ -37259,7 +37270,7 @@
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "人工对齐评论更新的业务结果与 comment list 回查语义;null/空响应不能单独证明成功,不改变命令身份、参数契约或接口绑定。",
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"value": "更新指定文档评论的文字内容和可选 @用户/@群。"
|
||||
@@ -37267,7 +37278,7 @@
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工对齐评论更新的业务结果与 comment list 回查语义;null/空响应不能单独证明成功,不改变命令身份、参数契约或接口绑定。",
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"value": "更新指定文档评论的文字内容和可选 @用户/@群。"
|
||||
},
|
||||
@@ -37291,22 +37302,20 @@
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "人工对齐评论更新的业务结果与 comment list 回查语义;null/空响应不能单独证明成功,不改变命令身份、参数契约或接口绑定。",
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"value": [
|
||||
"删除评论用 delete;回复用 reply",
|
||||
"响应为 null/空对象或回查仍是旧正文时不能判成功,必须报告更新未生效或部分完成"
|
||||
"删除评论用 delete;回复用 reply"
|
||||
]
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工对齐评论更新的业务结果与 comment list 回查语义;null/空响应不能单独证明成功,不改变命令身份、参数契约或接口绑定。",
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"value": [
|
||||
"删除评论用 delete;回复用 reply",
|
||||
"响应为 null/空对象或回查仍是旧正文时不能判成功,必须报告更新未生效或部分完成"
|
||||
"删除评论用 delete;回复用 reply"
|
||||
]
|
||||
},
|
||||
"canonical_path": {
|
||||
@@ -37395,7 +37404,7 @@
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "人工对齐评论更新的业务结果与 comment list 回查语义;null/空响应不能单独证明成功,不改变命令身份、参数契约或接口绑定。",
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"value": [
|
||||
@@ -37406,7 +37415,7 @@
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工对齐评论更新的业务结果与 comment list 回查语义;null/空响应不能单独证明成功,不改变命令身份、参数契约或接口绑定。",
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"value": [
|
||||
"dws doc comment update --node \u003cDOC_ID\u003e --comment-key \u003cCOMMENT_KEY\u003e --content \"已按最新数据修正\" --format json",
|
||||
@@ -37494,7 +37503,7 @@
|
||||
},
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "人工对齐评论更新的业务结果与 comment list 回查语义;null/空响应不能单独证明成功,不改变命令身份、参数契约或接口绑定。",
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
|
||||
"selected": false,
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"value": true
|
||||
@@ -37546,20 +37555,20 @@
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "人工对齐评论更新的业务结果与 comment list 回查语义;null/空响应不能单独证明成功,不改变命令身份、参数契约或接口绑定。",
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"value": [
|
||||
"修改已有评论正文;可选更新 --mention 或 --mentioned-open-conversation-id;执行后必须用 comment list 回查同一 commentKey 的目标字段"
|
||||
"修改已有评论正文;可选更新 --mention 或 --mentioned-open-conversation-id"
|
||||
]
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工对齐评论更新的业务结果与 comment list 回查语义;null/空响应不能单独证明成功,不改变命令身份、参数契约或接口绑定。",
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"value": [
|
||||
"修改已有评论正文;可选更新 --mention 或 --mentioned-open-conversation-id;执行后必须用 comment list 回查同一 commentKey 的目标字段"
|
||||
"修改已有评论正文;可选更新 --mention 或 --mentioned-open-conversation-id"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -38084,7 +38093,7 @@
|
||||
"source": "reviewed_command_registry",
|
||||
"title": "更新文档评论",
|
||||
"use_when": [
|
||||
"修改已有评论正文;可选更新 --mention 或 --mentioned-open-conversation-id;执行后必须用 comment list 回查同一 commentKey 的目标字段"
|
||||
"修改已有评论正文;可选更新 --mention 或 --mentioned-open-conversation-id"
|
||||
]
|
||||
},
|
||||
"doc.update_document": {
|
||||
@@ -38111,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",
|
||||
@@ -38189,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 冒充创建"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -38201,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": {
|
||||
@@ -38460,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 转义损坏"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -38471,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 转义损坏"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -39086,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": [
|
||||
{
|
||||
@@ -39145,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
|
||||
},
|
||||
@@ -39164,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": {
|
||||
@@ -39532,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": {
|
||||
@@ -39557,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",
|
||||
@@ -39627,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"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -39637,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": {
|
||||
@@ -39859,7 +39853,7 @@
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"value": [
|
||||
"修改已有块的文本/标题/样式(已知 blockId)时"
|
||||
"修改已有块的文本/标题/样式时;blockId 必须来自目标文档本次 block list 结果"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -39868,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 结果"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -40710,7 +40704,7 @@
|
||||
"source": "reviewed_command_registry",
|
||||
"title": "更新块元素",
|
||||
"use_when": [
|
||||
"修改已有块的文本/标题/样式(已知 blockId)时"
|
||||
"修改已有块的文本/标题/样式时;blockId 必须来自目标文档本次 block list 结果"
|
||||
]
|
||||
},
|
||||
"doc.update_permission": {
|
||||
|
||||
@@ -89,8 +89,8 @@
|
||||
],
|
||||
"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",
|
||||
@@ -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"
|
||||
@@ -373,11 +374,13 @@
|
||||
"agent_summary": "向文档插入块元素",
|
||||
"use_when": [
|
||||
"用户明确要求在文档中插入新块(段落/标题/列表等)时;简单段落/标题用 --text/--heading,复杂块用 --element JSON",
|
||||
"用户要求有序列表块时使用 JSONML p.list.isOrdered=true(同一 listId)或等价原生 orderedList 结构,不能用普通 Markdown 代替"
|
||||
"用户要求有序列表块时使用 JSONML p.list.isOrdered=true(同一 listId)或等价原生 orderedList 结构,不能用普通 Markdown 代替",
|
||||
"相对插入时先 block list 获取真实 blockId,再同时传 --ref-block 与 --where before|after"
|
||||
],
|
||||
"avoid_when": [
|
||||
"仅在用户没有指定块操作、只要整篇追加 Markdown 时优先 doc update --mode append;显式 insert/标题块/列表块不能折叠",
|
||||
"插入本地文件附件优先 doc media insert",
|
||||
"插入本地文件附件优先 doc media insert;不要为普通附件手写 attachment JSON",
|
||||
"整段 Markdown 或长内容优先 doc update --mode append,不要拆成大量 block insert",
|
||||
"删块用 block delete;改已有块用 block update"
|
||||
],
|
||||
"examples": [
|
||||
@@ -608,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",
|
||||
@@ -721,18 +725,17 @@
|
||||
"doc.update_comment": {
|
||||
"agent_summary": "更新指定文档评论的文字内容和可选 @用户/@群。",
|
||||
"use_when": [
|
||||
"修改已有评论正文;可选更新 --mention 或 --mentioned-open-conversation-id;执行后必须用 comment list 回查同一 commentKey 的目标字段"
|
||||
"修改已有评论正文;可选更新 --mention 或 --mentioned-open-conversation-id"
|
||||
],
|
||||
"avoid_when": [
|
||||
"删除评论用 delete;回复用 reply",
|
||||
"响应为 null/空对象或回查仍是旧正文时不能判成功,必须报告更新未生效或部分完成"
|
||||
"删除评论用 delete;回复用 reply"
|
||||
],
|
||||
"examples": [
|
||||
"dws doc comment update --node <DOC_ID> --comment-key <COMMENT_KEY> --content \"已按最新数据修正\" --format json",
|
||||
"dws doc comment update --node <DOC_ID> --comment-key <COMMENT_KEY> --content \"请群内确认\" --mentioned-open-conversation-id <openConversationId>"
|
||||
],
|
||||
"reviewed": true,
|
||||
"review_reason": "人工对齐评论更新的业务结果与 comment list 回查语义;null/空响应不能单独证明成功,不改变命令身份、参数契约或接口绑定。",
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
|
||||
"source_refs": [
|
||||
"internal/cli/schema_command_registry.json#doc.update_comment",
|
||||
"cobra-help:dws doc comment update --help",
|
||||
@@ -746,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",
|
||||
@@ -773,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"
|
||||
|
||||
@@ -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
-2
@@ -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,
|
||||
// 传大值会直接报错 (与悟空实现一致: 默认分页大小 + 游标翻页)。
|
||||
@@ -1649,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")
|
||||
@@ -2043,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 (必填)")
|
||||
@@ -2899,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
|
||||
}
|
||||
|
||||
@@ -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,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,12 +2,11 @@
|
||||
|
||||
> 通用规范见 [_common/conventions.md](_common/conventions.md)。
|
||||
|
||||
## 显式工作流与事实保真
|
||||
## 显式工作流
|
||||
|
||||
- 用户点名的 `create → list → insert/append/update` 是可观察命令链,必须保持顺序逐项执行;create 只承载明确的初始正文。有序列表块必须验证回读结构中的 `list.isOrdered=true`。
|
||||
- `--name` 不替代用户显式要求的正文 H1;新建资源返回 ID 后,同一请求的指代绑定该新资源,禁止搜索同名旧资源替换。
|
||||
- Word/Excel 需要“在线编辑/直接在线改”时使用 `doc import`,普通 `drive upload` 只保留原文件。
|
||||
- 汇总时保留证据强度:验证数量不等于通过数量,整理问题不等于根因分析。任一步骤返回 `null`/空结果或回查不一致时只能报告部分完成。
|
||||
|
||||
| Recipe | 行动指南(固定路线) |
|
||||
|--------|-------------------|
|
||||
|
||||
@@ -1060,8 +1060,6 @@ EOF
|
||||
- `drive upload` / `doc upload` 是普通文件存储路径;用户要求 Word/Excel “在线编辑/直接在线改”时硬路由到 `doc import`,并验证导入后的在线类型和文件夹。只有用户明确同时要原文件与在线版时才分别 upload + import
|
||||
- 同一请求中新建、复制或导入返回的 `nodeId` 必须绑定后续“这篇/刚才那篇/上次那篇”;禁止搜索同名旧资源覆盖绑定
|
||||
- `--name` 只是文档外壳标题,不能替代用户显式要求的正文 H1;用户说“正文先起一级标题”时必须写入或插入真实 H1
|
||||
- 汇总只能保持用户事实强度:“验证 12 条”不等于“12 条全部通过”,“整理问题清单”不等于“输出根因分析”
|
||||
- 写操作响应为 `null`/空对象或回查未变化时,该步骤失败;必须报告部分完成,禁止用其他成功步骤把整体说成“全部完成”
|
||||
- `upload` 是三步自动完成的流程 (获取凭证 → OSS 上传 → 提交入库),无需手动分步操作
|
||||
- `download` 是两步自动完成的流程 (获取下载链接 → HTTP GET 下载),支持自动推断文件名;`--output` 可指定文件路径或目录
|
||||
- `media insert` 是三步自动完成的流程 (获取附件上传凭证 → OSS 上传 → 插入附件块到文档),无需手动分步操作
|
||||
|
||||
@@ -128,7 +128,6 @@ Flags:
|
||||
- 划词评论的 `--start` / `--end` 是块内文本字符偏移量,从 0 开始;通过 [`./doc-block.md`](./doc-block.md) `block list` 取 `paragraph.text` 后人工或脚本计算。
|
||||
- `reply` 加 `--emoji` 时 `--content` 填表情名称(如 `比心`、`赞`),不是文字内容。
|
||||
- `reply --emoji` 不能同时 @群。
|
||||
- `comment create/reply/update/delete` 的退出码 0 不等于业务成功。响应为 `null`、空对象或缺少可核验字段时,立即执行 `comment list` 回查目标 `commentKey`。若 update 后正文仍是旧值,必须判定“更新未生效”;即使其他步骤成功或评论随后被删除,也只能报告部分完成,禁止写“全部完成”。
|
||||
|
||||
## 上下文传递
|
||||
|
||||
|
||||
@@ -60,8 +60,6 @@ def run_dws(args: Sequence[str], dry_run: bool = False) -> Any:
|
||||
f"dws 命令失败:{detail or f'退出码 {result.returncode}'}"
|
||||
)
|
||||
data = decode_json_output(result.stdout)
|
||||
if data is None or data == {}:
|
||||
raise ScriptError("dws 返回空业务结果,无法确认操作成功")
|
||||
if isinstance(data, dict) and data.get("success") is False:
|
||||
detail = data.get("errorMsg") or data.get("message") or "未知错误"
|
||||
raise ScriptError(f"dws 业务调用失败:{detail}")
|
||||
@@ -147,14 +145,12 @@ def run(argv: Optional[Sequence[str]] = None) -> int:
|
||||
["doc", "info", "--node", node_id, "--format", "json"],
|
||||
dry_run=args.dry_run,
|
||||
)
|
||||
readback = run_dws(
|
||||
run_dws(
|
||||
["doc", "read", "--node", node_id, "--format", "json"],
|
||||
dry_run=args.dry_run,
|
||||
)
|
||||
if args.dry_run:
|
||||
return 0
|
||||
if not first_value(readback, ("markdown", "jsonml", "content")):
|
||||
raise ScriptError("文档回读未返回正文,无法确认写入成功")
|
||||
|
||||
summary = {
|
||||
"success": True,
|
||||
|
||||
@@ -11,24 +11,28 @@ 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 > 原子命令。命令已确定且参数清楚时直接执行。
|
||||
1. 先定义目标对象、父容器、用户要求的阶段和完成证据;命中下方高频意图时,把精确
|
||||
骨架作为首选起点,不先查 Help 或产品级 Schema。
|
||||
2. 首选顺序为:精确骨架 / recipe > 匹配的公开 Shortcut > 原子命令。它用于降低
|
||||
首调用成本,不是禁止依据 Runtime 新证据调整路径。脚本只用于 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. 用户已给足名称、字段、数据和目标时,直接按依赖链完成全部步骤;不要调用 todo 工具、分步汇报或追问已明确的信息。中间返回只用于提取下一步 ID 和判断失败,完成所有请求后再统一回读并答复。
|
||||
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 -->
|
||||
@@ -44,7 +48,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 后,立即绑定同一请求中的“这个”“刚才新建的”等指代;除非用户明确转向历史资源,否则不得再按名称搜索并替换为旧对象。
|
||||
|
||||
## 核心意图与执行骨架
|
||||
|
||||
@@ -52,16 +58,17 @@ metadata:
|
||||
|---|---|---|
|
||||
| 按名称找 Base | `dws aitable +resolve-base --name "<名称>" --format json` | 唯一命中才继续;多候选停止并消歧 |
|
||||
| 浏览最近访问 | `dws aitable +base-list --format json` | 只代表最近访问,不得宣称全量 |
|
||||
| 搜索模板 | `dws aitable +template-search --query "<关键词>" --format json` | 关键词参数是 `--query`,只返回真实候选,不擅自创建 Base |
|
||||
| 搜索模板 | `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 "<新名称>" --format json` | 源 viewId 来自当前表的真实返回;不要复制数据表或创建仪表盘替代 |
|
||||
| 复制视图 | `dws aitable view duplicate --base-id <baseId> --table-id <tableId> --view-id <viewId> --new-name "<名>"` | `viewId` 必须属于当前表;不能用复制 Table 或新建 Dashboard 替代 |
|
||||
| 创建图表 | 先读 `dws aitable chart widgets-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;逐项检查成功/失败结果 |
|
||||
@@ -72,9 +79,31 @@ 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 都不能单独证明业务完成。字段未生效、回读不一致、分页不完整或异步任务未完成时,报告失败、部分完成或进行中,不得声称“全部完成”。
|
||||
|
||||
验证必须与用户点名的操作语义一致:Base 搜索只能由搜索结果中的目标命中关闭,最近
|
||||
访问列表或创建回读只能证明对象存在;复制必须有复制操作产生的新对象证据;移动必须
|
||||
回读新的父级关系;删除必须核对精确目标并确认其已不存在。邻近证据可用于诊断,不能
|
||||
替代目标步骤的完成证据。
|
||||
|
||||
## 低频能力与 Reference
|
||||
|
||||
@@ -90,10 +119,18 @@ metadata:
|
||||
|
||||
## 错误恢复
|
||||
|
||||
- 路径或 flag 错误:按既定的 leaf Schema → leaf Help 顺序校正一次;仍失败则停止,不连续尝试猜测别名。
|
||||
- 不使用全局“只准纠正一次”的硬限制。恢复预算由已观察状态和副作用风险决定,只在
|
||||
新错误信息、leaf Schema/Help 或回读产生新证据时继续;重复同一假设或不再增加证据
|
||||
时停止。
|
||||
- 路径、flag 或 payload 在写入前被校验拒绝:吸收精确 hint,保持目标和容器不变后
|
||||
修正。只读调用可做有界替代探测,但不得用相邻能力冒充目标能力。
|
||||
- 命令非零、输出非 JSON、业务 `status != success`、必需 ID 缺失、批处理部分失败均视为失败;保留成功项与 ledger,禁止吞错。
|
||||
- 同名歧义、权限不足、资源不存在、字段类型漂移、分页无法推进或 Schema/Help 冲突时停止并报告。具体恢复动作按需读 [aitable-error-recovery.md](references/aitable/aitable-error-recovery.md)。
|
||||
- 每次重试都从最新实际输出重新提取下游 ID;删除和其他 `confirmation=user_required` 操作不得自动重试或静默确认。
|
||||
- 写入已返回 ID 时保留成功状态,只补字段、记录、视图、图表或验证缺口。写入超时或
|
||||
状态未知时先回读当前 Base/Table;只有明确未写入且契约允许时才重试。删除和其他
|
||||
`confirmation=user_required` 操作不得自动重试或静默确认。
|
||||
- 不要重复猜同一 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,11 +1,14 @@
|
||||
# dashboard & chart — 仪表盘与图表
|
||||
|
||||
> 加载边界:仅在用户明确操作仪表盘或图表时读取。先用当前 Base 的 dashboard list/get 获取 `dashboardId`,再获取其 `chartId`;不要用 View 或 Table 命令替代,也不要跨 Dashboard 复用 chartId。
|
||||
|
||||
## 建议操作顺序
|
||||
|
||||
```bash
|
||||
# 1) 先看配置模板(JSONC)
|
||||
# 1) 先看图表配置模板和当前 leaf 契约
|
||||
dws aitable dashboard config-example --format json
|
||||
dws aitable chart widgets-example --format json
|
||||
dws aitable chart create --help
|
||||
|
||||
# 2) 先拿 dashboard,再拿 chart 详情
|
||||
dws aitable dashboard get --base-id <BASE_ID> --dashboard-id <DASHBOARD_ID> --format json
|
||||
@@ -41,4 +44,8 @@ dws aitable chart get --base-id <BASE_ID> --dashboard-id <DASHBOARD_ID> --chart-
|
||||
|
||||
## 配置获取流程
|
||||
|
||||
创建图表前至多调用一次 `chart widgets-example`,不要为解析其 JSONC 重复调用或编写临时解析脚本;根据真实 tableId 和 fieldId 填充 `--config`。`chart create` 必须同时传 `--layout '{"x":0,"y":0,"w":6,"h":4}'`,多图表使用互不重叠的位置。用户未指定数量时只创建足以表达目标的 1–2 个图表。取返回 chartId 后立即 `chart get`,或最后 `dashboard get` 核对 charts;回读成功即答复,缺失时不得声称成功或重复提交同一条缺参命令。
|
||||
创建图表前,先调用当前 Runtime 的 `chart widgets-example`(如未来命令发生变化,
|
||||
以 leaf Schema/Help 的真实命令为准),再根据实际 tableId 和 fieldId 填充配置,并
|
||||
在第一次创建时同时传入 `--config` 与 `--layout`。创建返回
|
||||
`chartId` 后用 `dashboard get`/`chart get` 回读;只有 Dashboard 存在但图表缺失时,
|
||||
图表阶段仍未完成。
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# AI 表格数据分析 SOP
|
||||
|
||||
> 加载边界:用于需要全表统计、分组、排名或业务结论的任务。普通记录定位只读 record-query 专题。任何“全部/最高/总数”结论都必须携带分页完整性;未拉全时只能报告当前范围和 continuation。
|
||||
|
||||
> 当用户诉求涉及查询、筛选、排序、统计、Top/Bottom N、分组聚合、判断全局结论时,必须先读本文档再执行。
|
||||
|
||||
## 1. 查询决策树
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# AI 表格错误恢复指南
|
||||
|
||||
> 加载边界:仅在真实命令或业务返回失败后读取,不做预防性全量加载。先保留原错误、阶段、对象 ID 和已成功 ledger;只有能证明未写入且契约允许时才重试,状态未知和需确认写操作禁止自动重放。
|
||||
|
||||
> 当 CLI 命令返回错误时,按本文档的映射表判断恢复动作。
|
||||
|
||||
## 1. 错误响应结构
|
||||
@@ -68,22 +70,27 @@
|
||||
| 导入文件格式错误 | 不支持的文件格式或文件损坏 | 确认文件为 .xlsx 格式且未加密 |
|
||||
| 批处理只成功一部分 | 某批或某个字段返回失败 | 保留成功 ID,输出失败 batch/item ledger,以非零状态结束;不要整批重放 |
|
||||
|
||||
## 3. 重试策略
|
||||
## 3. 状态感知恢复策略
|
||||
|
||||
### 3.1 可重试的错误
|
||||
不要用一个全局次数限制处理所有错误。先判断上一次调用是否可能产生副作用,再决定
|
||||
探测、继续或停止。
|
||||
|
||||
| 错误类型 | 重试方式 | 最大重试次数 |
|
||||
|---------|---------|------------|
|
||||
| 网络超时 / 5xx | 等待 2s 后原样重试 | 2 |
|
||||
| 导出任务未完成 | 轮询 task-id | 5(间隔 3s) |
|
||||
| 并发写入冲突 | 串行重试 | 1 |
|
||||
| 已观察状态 | 恢复动作 |
|
||||
|---|---|
|
||||
| 只读调用失败 | 根据精确错误或 leaf 契约做有界探测;目标和父容器保持不变 |
|
||||
| 写入前参数校验失败 | 确认无副作用后修正 payload/flag;不原样重试 |
|
||||
| 网络超时 / 5xx,写入状态未知 | 先回读目标 Base/Table,确定未生效后才考虑重试 |
|
||||
| 已返回新对象或记录 ID | 绑定该 ID,只继续尚未完成的步骤 |
|
||||
| 异步任务未完成 | 继续查询同一个 taskId/importId,直到契约规定的终态或总超时 |
|
||||
| 并发冲突且明确未写入 | 降低并发或串行重试;再次冲突时停止并报告 |
|
||||
| 删除或高影响操作状态未知 | 停止并回读,不自动重放 |
|
||||
|
||||
### 3.2 不可重试的错误(立即停止)
|
||||
|
||||
| 错误类型 | 原因 | 处理方式 |
|
||||
|---------|------|---------|
|
||||
| 权限不足 / 403 | 用户对该 Base 无权限 | 停止操作,提示用户确认权限 |
|
||||
| 参数格式错误 | 请求结构不合法 | 修正参数后重试,不要原样重试 |
|
||||
| 参数格式错误 | 请求结构不合法 | 只在校验明确未写入且有新契约证据时修正,不要原样重试 |
|
||||
| 资源不存在 / 404 | ID 错误或资源已删除 | 重新查询定位资源 |
|
||||
| 配额超限 / 429 | API 调用频率过高 | 等待后重试,并降低并发 |
|
||||
|
||||
@@ -92,7 +99,12 @@
|
||||
在重试前,先确认:
|
||||
1. ❓ 错误是暂时性的还是永久性的?
|
||||
2. ❓ 参数有没有明显错误需要修正?
|
||||
3. ❓ 是否需要先查询最新状态再重试?
|
||||
3. ❓ 上次调用是否可能已经写入?
|
||||
4. ❓ 是否已经获得对象 ID,应该从后续缺口继续?
|
||||
5. ❓ 新尝试是否基于新证据,而不是重复同一猜测?
|
||||
|
||||
当没有新证据、开始重复同一错误、目标发生变化或副作用无法判断时停止。Base 搜索
|
||||
失败时,`base list` 只能帮助确认对象存在,不能作为搜索完成证据。
|
||||
|
||||
## 4. 调试技巧
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# export & import — 导入导出
|
||||
|
||||
> 加载边界:仅在文件与 AITable 之间迁移数据时读取。“追加到已有 Table”与“导入成新 Table”必须先分流。`taskId/importId` 只用于对应任务状态,完成后必须验证新 tableId 或本地输出文件;任务已创建不等于业务完成。
|
||||
|
||||
## 导出数据(两阶段轮询)
|
||||
|
||||
`export data` 为异步任务:首次调用可能只返回 `taskId`,需要继续轮询。
|
||||
@@ -38,8 +40,8 @@ dws aitable import upload --base-id <BASE_ID> \
|
||||
--file-name data.xlsx --file-size <字节数> --format json
|
||||
# → 返回 uploadUrl 和 importId
|
||||
|
||||
# 第 2 步:上传文件到 OSS(注意:Content-Type 必须设为空)
|
||||
curl -X PUT "<uploadUrl>" -H "Content-Type:" --data-binary @data.xlsx
|
||||
# 第 2 步:由受测 helper 上传到 OSS(Content-Type 为空)
|
||||
# 正常 Agent 执行使用 aitable_import_via_task.py,不手工拆出 curl fallback。
|
||||
|
||||
# 第 3 步:触发导入(新建表模式)
|
||||
dws aitable import data --import-id <importId> --format json
|
||||
@@ -55,7 +57,7 @@ dws aitable import data --import-id <importId> --table-id <TABLE_ID> --format js
|
||||
| 步骤 | 命令 | 说明 |
|
||||
|------|------|------|
|
||||
| 申请上传凭证 | `import upload --base-id <ID> --file-name <名称> --file-size <字节>` | `--file-size` 必须与实际文件大小一致 |
|
||||
| 上传文件 | HTTP PUT(curl 等) | **必须** 带 `-H "Content-Type:"` 将 Content-Type 设为空,否则 OSS 返回 403 |
|
||||
| 上传文件 | 受测 helper 的 HTTP PUT | Content-Type 必须为空,否则 OSS 返回 403;helper 需校验 OSS host,并将受信 HTTP signed URL 升级为 HTTPS |
|
||||
| 触发导入 | `import data --import-id <ID> [--table-id <TABLE_ID>]` | 同步等待,大多一次调用即返回结果;超时可用相同 importId 重试 |
|
||||
|
||||
### import data 参数
|
||||
@@ -102,7 +104,9 @@ dws aitable import data --import-id <importId> --table-id <TABLE_ID> --format js
|
||||
> **解决方案**:
|
||||
> 1. **首选**:创建目标表时,字段名与 Excel 表头列名**保持完全一致**
|
||||
> 2. **备选**:传 `--field-mapping '{"目标字段名":"Excel列名"}'` 手动指定映射
|
||||
> 3. **兜底**:如果 import data 多次失败,改用 `record create` 逐条写入
|
||||
> 3. 如果 import data 失败,先按错误和任务状态判断是否已写入;只有用户目标允许
|
||||
> “追加已有表”、文件内容可安全解析且新建表导入明确未生效时,才提出改用
|
||||
> `record create`。逐条写入不等价于文件级导入,不能静默替代并宣称导入成功。
|
||||
|
||||
### 适用场景
|
||||
|
||||
|
||||
@@ -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,15 @@
|
||||
|
||||
## 字段创建时设置 config(重要)
|
||||
|
||||
字段 JSON 的命名和枚举是严格契约:
|
||||
|
||||
- 批量 `--fields` 每项使用 `fieldName`,不是 `name`。
|
||||
- 单选是 `singleSelect`,多选是 `multipleSelect`;不存在通用的 `select` 类型。
|
||||
- number/date/currency formatter 只使用当前 `table create --help` 或 leaf Schema 列出的枚举;不要从别的产品或旧样例猜 `INTEGER`。
|
||||
- 返回明确 hint 时,只修改 hint 指向的字段契约并保持目标不变。只要写入前校验明确
|
||||
未产生副作用且新的 Schema/Help 证据仍在收敛,就可继续修正;重复同一假设或不再
|
||||
获得新证据时停止并保留部分成功 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)
|
||||
|
||||
@@ -72,8 +72,6 @@ metadata:
|
||||
- 已有 callout、分栏、样式、@人、图片或附件时,先读 JSONML;局部改动优先 `block update`,不要用 Markdown 整篇重写。
|
||||
- `block insert` 默认追加;只有明确相对位置时才传真实 `--ref-block` / `--parent-block`。`block delete` 和评论删除必须确认。
|
||||
- 写后按对象验证:正文用 `doc read`,块/附件用 `doc block list`,元信息/链接用 `doc info`,版本用 `version list`。
|
||||
- 工具退出码 0、`success=true`、空对象或 `null` 都不能单独证明成功。每个写步骤必须同时有非空业务结果和针对目标字段的回查;例如 comment update 返回 `null` 且 list 仍是旧内容时,该步骤失败,最终必须报告“部分完成/更新未生效”,不得以“全部完成”开头。
|
||||
- 汇总和改写只能重组用户给出的事实,不得增强确定性或新增任务:“验证 12 条用例”不能写成“12 条全部通过”,“整理问题清单”不能扩写成“输出根因分析”。数字、状态、结论和承诺逐项保持原义。
|
||||
|
||||
## 低频 Reference
|
||||
|
||||
|
||||
@@ -58,9 +58,7 @@ dws doc read --node <nodeId> # 校验关键标题、段落首句、表格、@
|
||||
|
||||
**禁止**在未回读的情况下向用户报告「已完成」。
|
||||
|
||||
## 显式工作流与事实保真
|
||||
## 显式工作流
|
||||
|
||||
- 用户点名的 `create → list → insert/append/update` 是可观察命令链,必须保持顺序逐项执行;create 只承载明确的初始正文。有序列表块必须验证回读结构中的 `list.isOrdered=true`。
|
||||
- 新建资源返回 ID 后,同一请求的指代默认绑定该新资源;禁止搜索同名旧资源替换绑定。
|
||||
- 汇总用户材料时保留证据强度:验证数量不等于通过数量,计划整理问题不等于承诺根因分析。不得为“更专业”而补造结论、状态或任务。
|
||||
- 任一步骤返回 `null`/空结果或回查不一致时,该步骤未完成;最终按步骤报告成功与失败,不能用其他成功步骤把整体描述成“全部完成”。
|
||||
|
||||
@@ -132,7 +132,6 @@ Flags:
|
||||
- `reply` 加 `--emoji` 时 `--content` 填表情名称(如 `比心`、`赞`),不是文字内容。
|
||||
- `reply --emoji` 与群 mention 冲突;CLI 会在调用服务端前报错,不会静默忽略。
|
||||
- `delete` 是不可逆操作;AI Agent 必须先让用户确认,再追加 `--yes`,避免 CLI 进入交互等待。
|
||||
- `comment create/reply/update/delete` 的退出码 0 不等于业务成功。响应为 `null`、空对象或缺少可核验字段时,立即执行 `comment list` 回查目标 `commentKey`。若 update 后正文仍是旧值,必须判定“更新未生效”;即使其他步骤成功或评论随后被删除,也只能报告部分完成,禁止写“全部完成”。
|
||||
|
||||
## 上下文传递
|
||||
|
||||
|
||||
@@ -60,8 +60,6 @@ def run_dws(args: Sequence[str], dry_run: bool = False) -> Any:
|
||||
f"dws 命令失败:{detail or f'退出码 {result.returncode}'}"
|
||||
)
|
||||
data = decode_json_output(result.stdout)
|
||||
if data is None or data == {}:
|
||||
raise ScriptError("dws 返回空业务结果,无法确认操作成功")
|
||||
if isinstance(data, dict) and data.get("success") is False:
|
||||
detail = data.get("errorMsg") or data.get("message") or "未知错误"
|
||||
raise ScriptError(f"dws 业务调用失败:{detail}")
|
||||
@@ -147,14 +145,12 @@ def run(argv: Optional[Sequence[str]] = None) -> int:
|
||||
["doc", "info", "--node", node_id, "--format", "json"],
|
||||
dry_run=args.dry_run,
|
||||
)
|
||||
readback = run_dws(
|
||||
run_dws(
|
||||
["doc", "read", "--node", node_id, "--format", "json"],
|
||||
dry_run=args.dry_run,
|
||||
)
|
||||
if args.dry_run:
|
||||
return 0
|
||||
if not first_value(readback, ("markdown", "jsonml", "content")):
|
||||
raise ScriptError("文档回读未返回正文,无法确认写入成功")
|
||||
|
||||
summary = {
|
||||
"success": True,
|
||||
|
||||
@@ -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 写)、建后不回读。
|
||||
|
||||
@@ -39,6 +39,16 @@ skill 作为入口进行二级路由。
|
||||
- 创建、更新、发送等写操作后,按产品 skill 的要求回读或查询状态,不得仅凭退出码
|
||||
宣称成功。
|
||||
|
||||
## 先定义结果,再选择命令
|
||||
|
||||
执行前把请求压缩成一个结果契约:目标状态、资源类型与父容器、用户明确要求的阶段、
|
||||
副作用边界,以及每个关键结论需要的验证证据。命令只是达到结果的候选路径,不得用
|
||||
相似终态替代用户点名的操作:新建相似对象不等于复制成功,最近访问列表不等于搜索
|
||||
命中,发送请求返回成功也不等于对方已收到。
|
||||
|
||||
执行过程中维护轻量状态账本:已确认成功的步骤和 ID、未完成缺口、写入状态是否确定、
|
||||
下一步安全探测及最终验证。恢复时从缺口继续,不重放已经成功的写操作。
|
||||
|
||||
## 渐进加载
|
||||
|
||||
只读取当前任务需要的文件,不要一次性加载全部 shared references:
|
||||
@@ -82,8 +92,12 @@ skill 作为入口进行二级路由。
|
||||
|
||||
## 错误最短路径
|
||||
|
||||
1. `unknown command` / `unknown flag`:运行对应层级 `--help`,修正后最多重试一次。
|
||||
1. `unknown command` / `unknown flag`:读取错误中的 hint、leaf Schema 或对应层级
|
||||
`--help`,只在获得新证据时修正;不要重复尝试同一假设。
|
||||
2. 认证或权限错误:读取 `global-reference.md` 与 `error-codes.md` 对应章节。
|
||||
3. 其他错误:加 `--verbose` 重试一次;仍失败则停止并报告真实错误,不连续尝试替代
|
||||
命令。
|
||||
4. 明确不支持的能力:说明边界,不通过其他接口绕过。
|
||||
3. 只读调用或写入前参数校验失败:保持目标不变,依据新证据做有界恢复。
|
||||
4. 写入已返回 ID:保留成功状态,只补后续缺口;写入超时、返回不明或可能已生效时,
|
||||
先回读目标容器,禁止盲目重放。删除及高影响操作状态不明时停止并报告。
|
||||
5. 当后续尝试不再产生新证据、开始重复同一错误、改变目标或副作用变得不确定时停止;
|
||||
不用不同产品或相似对象掩盖原操作失败。
|
||||
6. 明确不支持的能力:说明边界,不通过其他接口绕过。
|
||||
|
||||
@@ -96,7 +96,7 @@ class DocSkillAlignmentTest(unittest.TestCase):
|
||||
self.assertIn("按“先/再/然后”切分操作阶段", skill)
|
||||
self.assertLessEqual(len(skill.encode("utf-8")), 9500)
|
||||
|
||||
def test_workflow_identity_and_fidelity_rules_are_explicit(self):
|
||||
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")
|
||||
@@ -119,22 +119,11 @@ class DocSkillAlignmentTest(unittest.TestCase):
|
||||
MONO_DOC_ROOT / "doc-import.md",
|
||||
]
|
||||
)
|
||||
comment_refs = "\n".join(
|
||||
path.read_text(encoding="utf-8")
|
||||
for path in [
|
||||
SKILL_ROOT / "references" / "doc" / "doc-comment.md",
|
||||
MONO_DOC_ROOT / "doc-comment.md",
|
||||
]
|
||||
)
|
||||
|
||||
self.assertIn("不能覆盖用户显式要求的正文 H1", skill)
|
||||
self.assertIn("禁止先搜索同名文档", create_refs)
|
||||
self.assertIn("显式块操作不可折叠", block_refs)
|
||||
self.assertIn("list.isOrdered=true", block_refs)
|
||||
self.assertIn("在线编辑硬路由", import_refs)
|
||||
self.assertIn("update 后正文仍是旧值", comment_refs)
|
||||
self.assertIn("验证 12 条用例", skill)
|
||||
self.assertIn("部分完成/更新未生效", skill)
|
||||
|
||||
def test_schema_selection_preserves_doc_drive_boundaries(self):
|
||||
doc = json.loads(
|
||||
@@ -161,10 +150,6 @@ class DocSkillAlignmentTest(unittest.TestCase):
|
||||
"list.isOrdered=true",
|
||||
" ".join(doc["doc.insert_document_block"]["use_when"]),
|
||||
)
|
||||
self.assertIn(
|
||||
"null/空对象",
|
||||
" ".join(doc["doc.update_comment"]["avoid_when"]),
|
||||
)
|
||||
self.assertIn(
|
||||
"dws doc import",
|
||||
" ".join(drive["drive.upload"]["avoid_when"]),
|
||||
@@ -228,16 +213,6 @@ class DocCreateAndWriteTest(unittest.TestCase):
|
||||
with self.assertRaisesRegex(self.module.ScriptError, "denied"):
|
||||
self.module.run_dws(["doc", "create"])
|
||||
|
||||
with mock.patch.object(
|
||||
self.module.subprocess,
|
||||
"run",
|
||||
return_value=subprocess.CompletedProcess(
|
||||
["dws"], 0, stdout="null", stderr=""
|
||||
),
|
||||
):
|
||||
with self.assertRaisesRegex(self.module.ScriptError, "空业务结果"):
|
||||
self.module.run_dws(["doc", "create"])
|
||||
|
||||
def test_wrapper_uses_create_then_info_and_read_without_manual_update(self):
|
||||
calls = []
|
||||
|
||||
@@ -279,19 +254,6 @@ class DocCreateAndWriteTest(unittest.TestCase):
|
||||
|
||||
self.assertEqual(0, code)
|
||||
self.assertEqual(["# 周报"], seen_content)
|
||||
|
||||
def test_wrapper_rejects_empty_readback(self):
|
||||
def fake_run(args, dry_run=False):
|
||||
if args[:2] == ["doc", "create"]:
|
||||
return {"success": True, "nodeId": "doc-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 self.assertRaisesRegex(self.module.ScriptError, "回读未返回正文"):
|
||||
self.module.run(["--name", "周报", "--content", "hello"])
|
||||
|
||||
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