Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e924d11db6 | ||
|
|
7c1edadd4d | ||
|
|
2699d2cfbe | ||
|
|
798def7337 | ||
|
|
5af3f0a8ba | ||
|
|
36803ac9bb |
@@ -82,74 +82,6 @@ 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,61 +621,6 @@ 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 {
|
||||
@@ -758,9 +703,6 @@ 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,8 +4713,7 @@
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"仅在用户没有指定块操作、只要整篇追加 Markdown 时优先 doc update --mode append;显式 insert/标题块/列表块不能折叠",
|
||||
"插入本地文件附件优先 doc media insert;不要为普通附件手写 attachment JSON",
|
||||
"整段 Markdown 或长内容优先 doc update --mode append,不要拆成大量 block insert",
|
||||
"插入本地文件附件优先 doc media insert",
|
||||
"删块用 block delete;改已有块用 block update"
|
||||
],
|
||||
"confirmation": "not_required",
|
||||
@@ -4766,8 +4765,7 @@
|
||||
"avoid_when": {
|
||||
"value": [
|
||||
"仅在用户没有指定块操作、只要整篇追加 Markdown 时优先 doc update --mode append;显式 insert/标题块/列表块不能折叠",
|
||||
"插入本地文件附件优先 doc media insert;不要为普通附件手写 attachment JSON",
|
||||
"整段 Markdown 或长内容优先 doc update --mode append,不要拆成大量 block insert",
|
||||
"插入本地文件附件优先 doc media insert",
|
||||
"删块用 block delete;改已有块用 block update"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
@@ -4778,8 +4776,7 @@
|
||||
{
|
||||
"value": [
|
||||
"仅在用户没有指定块操作、只要整篇追加 Markdown 时优先 doc update --mode append;显式 insert/标题块/列表块不能折叠",
|
||||
"插入本地文件附件优先 doc media insert;不要为普通附件手写 attachment JSON",
|
||||
"整段 Markdown 或长内容优先 doc update --mode append,不要拆成大量 block insert",
|
||||
"插入本地文件附件优先 doc media insert",
|
||||
"删块用 block delete;改已有块用 block update"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
@@ -4929,8 +4926,7 @@
|
||||
"use_when": {
|
||||
"value": [
|
||||
"用户明确要求在文档中插入新块(段落/标题/列表等)时;简单段落/标题用 --text/--heading,复杂块用 --element JSON",
|
||||
"用户要求有序列表块时使用 JSONML p.list.isOrdered=true(同一 listId)或等价原生 orderedList 结构,不能用普通 Markdown 代替",
|
||||
"相对插入时先 block list 获取真实 blockId,再同时传 --ref-block 与 --where before|after"
|
||||
"用户要求有序列表块时使用 JSONML p.list.isOrdered=true(同一 listId)或等价原生 orderedList 结构,不能用普通 Markdown 代替"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -4940,8 +4936,7 @@
|
||||
{
|
||||
"value": [
|
||||
"用户明确要求在文档中插入新块(段落/标题/列表等)时;简单段落/标题用 --text/--heading,复杂块用 --element JSON",
|
||||
"用户要求有序列表块时使用 JSONML p.list.isOrdered=true(同一 listId)或等价原生 orderedList 结构,不能用普通 Markdown 代替",
|
||||
"相对插入时先 block list 获取真实 blockId,再同时传 --ref-block 与 --where before|after"
|
||||
"用户要求有序列表块时使用 JSONML p.list.isOrdered=true(同一 listId)或等价原生 orderedList 结构,不能用普通 Markdown 代替"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -4976,8 +4971,7 @@
|
||||
],
|
||||
"use_when": [
|
||||
"用户明确要求在文档中插入新块(段落/标题/列表等)时;简单段落/标题用 --text/--heading,复杂块用 --element JSON",
|
||||
"用户要求有序列表块时使用 JSONML p.list.isOrdered=true(同一 listId)或等价原生 orderedList 结构,不能用普通 Markdown 代替",
|
||||
"相对插入时先 block list 获取真实 blockId,再同时传 --ref-block 与 --where before|after"
|
||||
"用户要求有序列表块时使用 JSONML p.list.isOrdered=true(同一 listId)或等价原生 orderedList 结构,不能用普通 Markdown 代替"
|
||||
]
|
||||
},
|
||||
"doc block list": {
|
||||
@@ -5257,8 +5251,7 @@
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"插入新块用 block insert;删除用 block delete",
|
||||
"改文档显示名用 rename;整篇覆盖用 update overwrite",
|
||||
"不要跨文档复用 blockId,也不要把 nodeId/commentKey 当作 blockId"
|
||||
"改文档显示名用 rename;整篇覆盖用 update overwrite"
|
||||
],
|
||||
"confirmation": "not_required",
|
||||
"effect": "write",
|
||||
@@ -5308,8 +5301,7 @@
|
||||
"avoid_when": {
|
||||
"value": [
|
||||
"插入新块用 block insert;删除用 block delete",
|
||||
"改文档显示名用 rename;整篇覆盖用 update overwrite",
|
||||
"不要跨文档复用 blockId,也不要把 nodeId/commentKey 当作 blockId"
|
||||
"改文档显示名用 rename;整篇覆盖用 update overwrite"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -5319,8 +5311,7 @@
|
||||
{
|
||||
"value": [
|
||||
"插入新块用 block insert;删除用 block delete",
|
||||
"改文档显示名用 rename;整篇覆盖用 update overwrite",
|
||||
"不要跨文档复用 blockId,也不要把 nodeId/commentKey 当作 blockId"
|
||||
"改文档显示名用 rename;整篇覆盖用 update overwrite"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -5466,7 +5457,7 @@
|
||||
},
|
||||
"use_when": {
|
||||
"value": [
|
||||
"修改已有块的文本/标题/样式时;blockId 必须来自目标文档本次 block list 结果"
|
||||
"修改已有块的文本/标题/样式(已知 blockId)时"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -5475,7 +5466,7 @@
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"修改已有块的文本/标题/样式时;blockId 必须来自目标文档本次 block list 结果"
|
||||
"修改已有块的文本/标题/样式(已知 blockId)时"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -5509,7 +5500,7 @@
|
||||
"structured-hint:internal/cli/schema_hints/selection-review.json#doc.update_document_block"
|
||||
],
|
||||
"use_when": [
|
||||
"修改已有块的文本/标题/样式时;blockId 必须来自目标文档本次 block list 结果"
|
||||
"修改已有块的文本/标题/样式(已知 blockId)时"
|
||||
]
|
||||
},
|
||||
"doc comment create": {
|
||||
@@ -6656,8 +6647,7 @@
|
||||
"agent_summary_source": "dws-agent-selection/doc",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"新建评论用 create/create-inline;删评论用 delete",
|
||||
"不要把 commentId、blockId 或示例占位符传给 --comment-key"
|
||||
"新建评论用 create/create-inline;删评论用 delete"
|
||||
],
|
||||
"confirmation": "not_required",
|
||||
"effect": "write",
|
||||
@@ -6707,8 +6697,7 @@
|
||||
},
|
||||
"avoid_when": {
|
||||
"value": [
|
||||
"新建评论用 create/create-inline;删评论用 delete",
|
||||
"不要把 commentId、blockId 或示例占位符传给 --comment-key"
|
||||
"新建评论用 create/create-inline;删评论用 delete"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -6717,8 +6706,7 @@
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"新建评论用 create/create-inline;删评论用 delete",
|
||||
"不要把 commentId、blockId 或示例占位符传给 --comment-key"
|
||||
"新建评论用 create/create-inline;删评论用 delete"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -6912,7 +6900,7 @@
|
||||
},
|
||||
"use_when": {
|
||||
"value": [
|
||||
"回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 必须来自本次 list/create 返回"
|
||||
"回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 来自 list/create"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -6921,7 +6909,7 @@
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 必须来自本次 list/create 返回"
|
||||
"回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 来自 list/create"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -6956,7 +6944,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": {
|
||||
@@ -6964,7 +6952,8 @@
|
||||
"agent_summary_source": "dws-agent-selection/doc",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"删除评论用 delete;回复用 reply"
|
||||
"删除评论用 delete;回复用 reply",
|
||||
"响应为 null/空对象或回查仍是旧正文时不能判成功,必须报告更新未生效或部分完成"
|
||||
],
|
||||
"confirmation": "not_required",
|
||||
"effect": "write",
|
||||
@@ -6979,14 +6968,14 @@
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
|
||||
"review_reason": "人工对齐评论更新的业务结果与 comment list 回查语义;null/空响应不能单独证明成功,不改变命令身份、参数契约或接口绑定。",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "更新指定文档评论的文字内容和可选 @用户/@群。",
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。"
|
||||
"review_reason": "人工对齐评论更新的业务结果与 comment list 回查语义;null/空响应不能单独证明成功,不改变命令身份、参数契约或接口绑定。"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -7008,21 +6997,23 @@
|
||||
},
|
||||
"avoid_when": {
|
||||
"value": [
|
||||
"删除评论用 delete;回复用 reply"
|
||||
"删除评论用 delete;回复用 reply",
|
||||
"响应为 null/空对象或回查仍是旧正文时不能判成功,必须报告更新未生效或部分完成"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
|
||||
"review_reason": "人工对齐评论更新的业务结果与 comment list 回查语义;null/空响应不能单独证明成功,不改变命令身份、参数契约或接口绑定。",
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"删除评论用 delete;回复用 reply"
|
||||
"删除评论用 delete;回复用 reply",
|
||||
"响应为 null/空对象或回查仍是旧正文时不能判成功,必须报告更新未生效或部分完成"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。"
|
||||
"review_reason": "人工对齐评论更新的业务结果与 comment list 回查语义;null/空响应不能单独证明成功,不改变命令身份、参数契约或接口绑定。"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -7078,7 +7069,7 @@
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
|
||||
"review_reason": "人工对齐评论更新的业务结果与 comment list 回查语义;null/空响应不能单独证明成功,不改变命令身份、参数契约或接口绑定。",
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
@@ -7088,7 +7079,7 @@
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。"
|
||||
"review_reason": "人工对齐评论更新的业务结果与 comment list 回查语义;null/空响应不能单独证明成功,不改变命令身份、参数契约或接口绑定。"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -7181,7 +7172,7 @@
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": false,
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。"
|
||||
"review_reason": "人工对齐评论更新的业务结果与 comment list 回查语义;null/空响应不能单独证明成功,不改变命令身份、参数契约或接口绑定。"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -7209,21 +7200,21 @@
|
||||
},
|
||||
"use_when": {
|
||||
"value": [
|
||||
"修改已有评论正文;可选更新 --mention 或 --mentioned-open-conversation-id"
|
||||
"修改已有评论正文;可选更新 --mention 或 --mentioned-open-conversation-id;执行后必须用 comment list 回查同一 commentKey 的目标字段"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
|
||||
"review_reason": "人工对齐评论更新的业务结果与 comment list 回查语义;null/空响应不能单独证明成功,不改变命令身份、参数契约或接口绑定。",
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"修改已有评论正文;可选更新 --mention 或 --mentioned-open-conversation-id"
|
||||
"修改已有评论正文;可选更新 --mention 或 --mentioned-open-conversation-id;执行后必须用 comment list 回查同一 commentKey 的目标字段"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。"
|
||||
"review_reason": "人工对齐评论更新的业务结果与 comment list 回查语义;null/空响应不能单独证明成功,不改变命令身份、参数契约或接口绑定。"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -7244,7 +7235,7 @@
|
||||
"structured-hint:internal/cli/schema_hints/products/doc.json"
|
||||
],
|
||||
"use_when": [
|
||||
"修改已有评论正文;可选更新 --mention 或 --mentioned-open-conversation-id"
|
||||
"修改已有评论正文;可选更新 --mention 或 --mentioned-open-conversation-id;执行后必须用 comment list 回查同一 commentKey 的目标字段"
|
||||
]
|
||||
},
|
||||
"doc copy": {
|
||||
@@ -7515,8 +7506,8 @@
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"创建表格/脑图/白板/多维表/演示改用 dws wiki node create --type \u003ctype\u003e(勿用 doc create)",
|
||||
"在知识库建空节点实体也可用 wiki node create;本命令侧重可写初始内容的 adoc",
|
||||
"导入本地 Word/Markdown 为在线文档用 dws doc import,并显式提供 --folder 或 --workspace;不要用已迁移的 doc upload --convert"
|
||||
"只管理知识库空节点或层级时用 wiki node create;本命令侧重创建并写入 adoc",
|
||||
"导入本地 Word/Markdown 并保留服务端转换语义时用 dws doc import;不要用自写分片脚本替代原生 create"
|
||||
],
|
||||
"confirmation": "not_required",
|
||||
"effect": "write",
|
||||
@@ -7567,8 +7558,8 @@
|
||||
"avoid_when": {
|
||||
"value": [
|
||||
"创建表格/脑图/白板/多维表/演示改用 dws wiki node create --type \u003ctype\u003e(勿用 doc create)",
|
||||
"在知识库建空节点实体也可用 wiki node create;本命令侧重可写初始内容的 adoc",
|
||||
"导入本地 Word/Markdown 为在线文档用 dws doc import,并显式提供 --folder 或 --workspace;不要用已迁移的 doc upload --convert"
|
||||
"只管理知识库空节点或层级时用 wiki node create;本命令侧重创建并写入 adoc",
|
||||
"导入本地 Word/Markdown 并保留服务端转换语义时用 dws doc import;不要用自写分片脚本替代原生 create"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -7578,8 +7569,8 @@
|
||||
{
|
||||
"value": [
|
||||
"创建表格/脑图/白板/多维表/演示改用 dws wiki node create --type \u003ctype\u003e(勿用 doc create)",
|
||||
"在知识库建空节点实体也可用 wiki node create;本命令侧重可写初始内容的 adoc",
|
||||
"导入本地 Word/Markdown 为在线文档用 dws doc import,并显式提供 --folder 或 --workspace;不要用已迁移的 doc upload --convert"
|
||||
"只管理知识库空节点或层级时用 wiki node create;本命令侧重创建并写入 adoc",
|
||||
"导入本地 Word/Markdown 并保留服务端转换语义时用 dws doc import;不要用自写分片脚本替代原生 create"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -7790,7 +7781,7 @@
|
||||
"agent_summary_source": "dws-agent-selection/doc",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"常规文件删除使用 dws drive delete;不要继续选择已弃用的 doc delete 兼容入口",
|
||||
"常规文件删除优先 dws drive delete;本入口仅为兼容",
|
||||
"用户未确认或目标不清时不要删",
|
||||
"删块用 doc block delete;删评论用 doc comment delete"
|
||||
],
|
||||
@@ -7841,7 +7832,7 @@
|
||||
},
|
||||
"avoid_when": {
|
||||
"value": [
|
||||
"常规文件删除使用 dws drive delete;不要继续选择已弃用的 doc delete 兼容入口",
|
||||
"常规文件删除优先 dws drive delete;本入口仅为兼容",
|
||||
"用户未确认或目标不清时不要删",
|
||||
"删块用 doc block delete;删评论用 doc comment delete"
|
||||
],
|
||||
@@ -7852,7 +7843,7 @@
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"常规文件删除使用 dws drive delete;不要继续选择已弃用的 doc delete 兼容入口",
|
||||
"常规文件删除优先 dws drive delete;本入口仅为兼容",
|
||||
"用户未确认或目标不清时不要删",
|
||||
"删块用 doc block delete;删评论用 doc comment delete"
|
||||
],
|
||||
@@ -9110,7 +9101,7 @@
|
||||
"agent_summary_source": "dws-agent-selection/doc",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"发起导入用 doc import;不要先调用 import get,也不要使用示例占位 taskId"
|
||||
"发起导入用 doc import(若入口可用);不要用本命令代替导入"
|
||||
],
|
||||
"confirmation": "not_required",
|
||||
"effect": "write",
|
||||
@@ -9153,7 +9144,7 @@
|
||||
},
|
||||
"avoid_when": {
|
||||
"value": [
|
||||
"发起导入用 doc import;不要先调用 import get,也不要使用示例占位 taskId"
|
||||
"发起导入用 doc import(若入口可用);不要用本命令代替导入"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -9162,7 +9153,7 @@
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"发起导入用 doc import;不要先调用 import get,也不要使用示例占位 taskId"
|
||||
"发起导入用 doc import(若入口可用);不要用本命令代替导入"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -9352,7 +9343,7 @@
|
||||
},
|
||||
"use_when": {
|
||||
"value": [
|
||||
"仅在 doc import 已返回真实 taskId 且自动轮询超时/中断后,查询导入任务结果时"
|
||||
"查询文档导入任务结果(已有 taskId,导入超时/中断后兜底)时"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -9361,7 +9352,7 @@
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"仅在 doc import 已返回真实 taskId 且自动轮询超时/中断后,查询导入任务结果时"
|
||||
"查询文档导入任务结果(已有 taskId,导入超时/中断后兜底)时"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -9388,7 +9379,7 @@
|
||||
"structured-hint:internal/cli/schema_hints/products/doc.json"
|
||||
],
|
||||
"use_when": [
|
||||
"仅在 doc import 已返回真实 taskId 且自动轮询超时/中断后,查询导入任务结果时"
|
||||
"查询文档导入任务结果(已有 taskId,导入超时/中断后兜底)时"
|
||||
]
|
||||
},
|
||||
"doc info": {
|
||||
@@ -9937,8 +9928,7 @@
|
||||
"agent_summary_source": "dws-agent-selection/doc",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"下载钉盘普通文件用 drive download;导出在线文档用 doc export",
|
||||
"不要把 blockId/nodeId 当作 resourceId,也不要编造 --output:本命令返回临时 downloadUrl"
|
||||
"下载钉盘普通文件用 drive download;导出在线文档用 doc export"
|
||||
],
|
||||
"confirmation": "not_required",
|
||||
"effect": "read",
|
||||
@@ -9987,8 +9977,7 @@
|
||||
},
|
||||
"avoid_when": {
|
||||
"value": [
|
||||
"下载钉盘普通文件用 drive download;导出在线文档用 doc export",
|
||||
"不要把 blockId/nodeId 当作 resourceId,也不要编造 --output:本命令返回临时 downloadUrl"
|
||||
"下载钉盘普通文件用 drive download;导出在线文档用 doc export"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -9997,8 +9986,7 @@
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"下载钉盘普通文件用 drive download;导出在线文档用 doc export",
|
||||
"不要把 blockId/nodeId 当作 resourceId,也不要编造 --output:本命令返回临时 downloadUrl"
|
||||
"下载钉盘普通文件用 drive download;导出在线文档用 doc export"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -10144,7 +10132,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",
|
||||
@@ -10153,7 +10141,7 @@
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"获取文档正文中附件的临时下载 URL 时;resourceId 必须来自本次 block list 返回的 attachment 块"
|
||||
"获取文档正文中附件的临时下载 URL(resourceId 来自 block list attachment)时"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -10185,7 +10173,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": {
|
||||
@@ -14690,10 +14678,10 @@
|
||||
"agent_summary_source": "dws-agent-selection/doc",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"目标不是 adoc 或只要改单个块时改用 doc block update",
|
||||
"覆盖模式用户未确认前不要执行;先 --dry-run 预览,确认后再加 --yes",
|
||||
"--content 与 --content-file 二选一;--index 仅用于 mode=append",
|
||||
"创建新文档用 doc create,不要用 update 冒充创建"
|
||||
"只在末尾补一小段纯文本优先 +doc-append;只改一个块用 doc block update",
|
||||
"目标不是 adoc 时按 drive info 的 extension 切对应产品",
|
||||
"覆盖模式用户未确认前不要执行;可先 --dry-run 预览",
|
||||
"创建新文档用 doc create;不要预先手工分片或循环重试覆盖"
|
||||
],
|
||||
"confirmation": "not_required",
|
||||
"effect": "write",
|
||||
@@ -14743,10 +14731,10 @@
|
||||
},
|
||||
"avoid_when": {
|
||||
"value": [
|
||||
"目标不是 adoc 或只要改单个块时改用 doc block update",
|
||||
"覆盖模式用户未确认前不要执行;先 --dry-run 预览,确认后再加 --yes",
|
||||
"--content 与 --content-file 二选一;--index 仅用于 mode=append",
|
||||
"创建新文档用 doc create,不要用 update 冒充创建"
|
||||
"只在末尾补一小段纯文本优先 +doc-append;只改一个块用 doc block update",
|
||||
"目标不是 adoc 时按 drive info 的 extension 切对应产品",
|
||||
"覆盖模式用户未确认前不要执行;可先 --dry-run 预览",
|
||||
"创建新文档用 doc create;不要预先手工分片或循环重试覆盖"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -14755,10 +14743,10 @@
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"目标不是 adoc 或只要改单个块时改用 doc block update",
|
||||
"覆盖模式用户未确认前不要执行;先 --dry-run 预览,确认后再加 --yes",
|
||||
"--content 与 --content-file 二选一;--index 仅用于 mode=append",
|
||||
"创建新文档用 doc create,不要用 update 冒充创建"
|
||||
"只在末尾补一小段纯文本优先 +doc-append;只改一个块用 doc block update",
|
||||
"目标不是 adoc 时按 drive info 的 extension 切对应产品",
|
||||
"覆盖模式用户未确认前不要执行;可先 --dry-run 预览",
|
||||
"创建新文档用 doc create;不要预先手工分片或循环重试覆盖"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -14938,8 +14926,7 @@
|
||||
"value": [
|
||||
"用户要向已有 adoc 追加长、多行或文件内容时用 --mode append + --content-file;CLI 自动分片",
|
||||
"用户明确要求整篇覆盖替换时用 --mode overwrite(破坏性)",
|
||||
"append 且要插到第 N 个 block 前时加 --index N(先 block list)",
|
||||
"长文本、多行内容或表格优先写入 UTF-8 文件并使用 --content-file,避免 shell 转义损坏"
|
||||
"append 且要插到第 N 个 block 前时加 --index N(先 block list)"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -14950,8 +14937,7 @@
|
||||
"value": [
|
||||
"用户要向已有 adoc 追加长、多行或文件内容时用 --mode append + --content-file;CLI 自动分片",
|
||||
"用户明确要求整篇覆盖替换时用 --mode overwrite(破坏性)",
|
||||
"append 且要插到第 N 个 block 前时加 --index N(先 block list)",
|
||||
"长文本、多行内容或表格优先写入 UTF-8 文件并使用 --content-file,避免 shell 转义损坏"
|
||||
"append 且要插到第 N 个 block 前时加 --index N(先 block list)"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -14990,8 +14976,7 @@
|
||||
"use_when": [
|
||||
"用户要向已有 adoc 追加长、多行或文件内容时用 --mode append + --content-file;CLI 自动分片",
|
||||
"用户明确要求整篇覆盖替换时用 --mode overwrite(破坏性)",
|
||||
"append 且要插到第 N 个 block 前时加 --index N(先 block list)",
|
||||
"长文本、多行内容或表格优先写入 UTF-8 文件并使用 --content-file,避免 shell 转义损坏"
|
||||
"append 且要插到第 N 个 block 前时加 --index N(先 block list)"
|
||||
]
|
||||
},
|
||||
"doc upload": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"version": 1,
|
||||
"source_hash": "sha256:35727a6c324fbffde5271e96d972843abdf780391151ba858f6d04384268cd51",
|
||||
"source_hash": "sha256:f239237a9b87fa5a95a0520b2e2f2a112e117b273de8418763ba0ecd8652b317",
|
||||
"surface_hash": "sha256:4897d582a472d9d3596f88fb4546e7795d6b9bc334d020712f5d895ce894f7a7",
|
||||
"coverage": {
|
||||
"surface_products": 26,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"version": 1,
|
||||
"source_hash": "sha256:35727a6c324fbffde5271e96d972843abdf780391151ba858f6d04384268cd51",
|
||||
"source_hash": "sha256:f239237a9b87fa5a95a0520b2e2f2a112e117b273de8418763ba0ecd8652b317",
|
||||
"surface_hash": "sha256:4897d582a472d9d3596f88fb4546e7795d6b9bc334d020712f5d895ce894f7a7",
|
||||
"source_files": 160,
|
||||
"hint_files": 54,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"version": 1,
|
||||
"surface_hash": "sha256:4897d582a472d9d3596f88fb4546e7795d6b9bc334d020712f5d895ce894f7a7",
|
||||
"source_hash": "sha256:807d11cf6c62a0d4aaca4b5a6b438e6654b2792d3a32b4e271032202d45ff8fa",
|
||||
"source_hash": "sha256:eddcc39cfdf906e0b47c896abc081fd2b5fc81150023f99938deb9def8b07d9e",
|
||||
"catalog": {
|
||||
"agent_metadata": {
|
||||
"products_with_metadata": 26,
|
||||
"source": "embedded-skill-metadata",
|
||||
"source_hash": "sha256:35727a6c324fbffde5271e96d972843abdf780391151ba858f6d04384268cd51",
|
||||
"source_hash": "sha256:f239237a9b87fa5a95a0520b2e2f2a112e117b273de8418763ba0ecd8652b317",
|
||||
"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,并显式提供 --folder 或 --workspace;不要用已迁移的 doc upload --convert"
|
||||
"只管理知识库空节点或层级时用 wiki node create;本命令侧重创建并写入 adoc",
|
||||
"导入本地 Word/Markdown 并保留服务端转换语义时用 dws doc import;不要用自写分片脚本替代原生 create"
|
||||
],
|
||||
"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;不要继续选择已弃用的 doc delete 兼容入口",
|
||||
"常规文件删除优先 dws drive delete;本入口仅为兼容",
|
||||
"用户未确认或目标不清时不要删",
|
||||
"删块用 doc block delete;删评论用 doc comment delete"
|
||||
],
|
||||
@@ -15601,8 +15601,7 @@
|
||||
"agent_summary_source": "dws-agent-selection/doc",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"下载钉盘普通文件用 drive download;导出在线文档用 doc export",
|
||||
"不要把 blockId/nodeId 当作 resourceId,也不要编造 --output:本命令返回临时 downloadUrl"
|
||||
"下载钉盘普通文件用 drive download;导出在线文档用 doc export"
|
||||
],
|
||||
"canonical_path": "doc.download_doc_attachment",
|
||||
"cli_name": "download",
|
||||
@@ -15624,7 +15623,7 @@
|
||||
"risk": "low",
|
||||
"title": "下载文档附件",
|
||||
"use_when": [
|
||||
"获取文档正文中附件的临时下载 URL 时;resourceId 必须来自本次 block list 返回的 attachment 块"
|
||||
"获取文档正文中附件的临时下载 URL(resourceId 来自 block list attachment)时"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -15762,7 +15761,7 @@
|
||||
"agent_summary_source": "dws-agent-selection/doc",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"发起导入用 doc import;不要先调用 import get,也不要使用示例占位 taskId"
|
||||
"发起导入用 doc import(若入口可用);不要用本命令代替导入"
|
||||
],
|
||||
"canonical_path": "doc.import_get",
|
||||
"cli_name": "get",
|
||||
@@ -15783,7 +15782,7 @@
|
||||
"risk": "medium",
|
||||
"title": "查询导入任务结果(手动兜底)",
|
||||
"use_when": [
|
||||
"仅在 doc import 已返回真实 taskId 且自动轮询超时/中断后,查询导入任务结果时"
|
||||
"查询文档导入任务结果(已有 taskId,导入超时/中断后兜底)时"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -15793,8 +15792,7 @@
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"仅在用户没有指定块操作、只要整篇追加 Markdown 时优先 doc update --mode append;显式 insert/标题块/列表块不能折叠",
|
||||
"插入本地文件附件优先 doc media insert;不要为普通附件手写 attachment JSON",
|
||||
"整段 Markdown 或长内容优先 doc update --mode append,不要拆成大量 block insert",
|
||||
"插入本地文件附件优先 doc media insert",
|
||||
"删块用 block delete;改已有块用 block update"
|
||||
],
|
||||
"canonical_path": "doc.insert_document_block",
|
||||
@@ -15818,8 +15816,7 @@
|
||||
"title": "插入块元素",
|
||||
"use_when": [
|
||||
"用户明确要求在文档中插入新块(段落/标题/列表等)时;简单段落/标题用 --text/--heading,复杂块用 --element JSON",
|
||||
"用户要求有序列表块时使用 JSONML p.list.isOrdered=true(同一 listId)或等价原生 orderedList 结构,不能用普通 Markdown 代替",
|
||||
"相对插入时先 block list 获取真实 blockId,再同时传 --ref-block 与 --where before|after"
|
||||
"用户要求有序列表块时使用 JSONML p.list.isOrdered=true(同一 listId)或等价原生 orderedList 结构,不能用普通 Markdown 代替"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -16111,8 +16108,7 @@
|
||||
"agent_summary_source": "dws-agent-selection/doc",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"新建评论用 create/create-inline;删评论用 delete",
|
||||
"不要把 commentId、blockId 或示例占位符传给 --comment-key"
|
||||
"新建评论用 create/create-inline;删评论用 delete"
|
||||
],
|
||||
"canonical_path": "doc.reply_comment",
|
||||
"cli_name": "reply",
|
||||
@@ -16134,7 +16130,7 @@
|
||||
"risk": "medium",
|
||||
"title": "回复评论",
|
||||
"use_when": [
|
||||
"回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 必须来自本次 list/create 返回"
|
||||
"回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 来自 list/create"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -16804,7 +16800,8 @@
|
||||
"agent_summary_source": "dws-agent-selection/doc",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"删除评论用 delete;回复用 reply"
|
||||
"删除评论用 delete;回复用 reply",
|
||||
"响应为 null/空对象或回查仍是旧正文时不能判成功,必须报告更新未生效或部分完成"
|
||||
],
|
||||
"canonical_path": "doc.update_comment",
|
||||
"cli_name": "update",
|
||||
@@ -16822,7 +16819,7 @@
|
||||
"risk": "medium",
|
||||
"title": "更新文档评论",
|
||||
"use_when": [
|
||||
"修改已有评论正文;可选更新 --mention 或 --mentioned-open-conversation-id"
|
||||
"修改已有评论正文;可选更新 --mention 或 --mentioned-open-conversation-id;执行后必须用 comment list 回查同一 commentKey 的目标字段"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -16831,10 +16828,10 @@
|
||||
"agent_summary_source": "dws-agent-selection/doc",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"目标不是 adoc 或只要改单个块时改用 doc block update",
|
||||
"覆盖模式用户未确认前不要执行;先 --dry-run 预览,确认后再加 --yes",
|
||||
"--content 与 --content-file 二选一;--index 仅用于 mode=append",
|
||||
"创建新文档用 doc create,不要用 update 冒充创建"
|
||||
"只在末尾补一小段纯文本优先 +doc-append;只改一个块用 doc block update",
|
||||
"目标不是 adoc 时按 drive info 的 extension 切对应产品",
|
||||
"覆盖模式用户未确认前不要执行;可先 --dry-run 预览",
|
||||
"创建新文档用 doc create;不要预先手工分片或循环重试覆盖"
|
||||
],
|
||||
"canonical_path": "doc.update_document",
|
||||
"cli_name": "update",
|
||||
@@ -16857,8 +16854,7 @@
|
||||
"use_when": [
|
||||
"用户要向已有 adoc 追加长、多行或文件内容时用 --mode append + --content-file;CLI 自动分片",
|
||||
"用户明确要求整篇覆盖替换时用 --mode overwrite(破坏性)",
|
||||
"append 且要插到第 N 个 block 前时加 --index N(先 block list)",
|
||||
"长文本、多行内容或表格优先写入 UTF-8 文件并使用 --content-file,避免 shell 转义损坏"
|
||||
"append 且要插到第 N 个 block 前时加 --index N(先 block list)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -16868,8 +16864,7 @@
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"插入新块用 block insert;删除用 block delete",
|
||||
"改文档显示名用 rename;整篇覆盖用 update overwrite",
|
||||
"不要跨文档复用 blockId,也不要把 nodeId/commentKey 当作 blockId"
|
||||
"改文档显示名用 rename;整篇覆盖用 update overwrite"
|
||||
],
|
||||
"canonical_path": "doc.update_document_block",
|
||||
"cli_name": "update",
|
||||
@@ -16891,7 +16886,7 @@
|
||||
"risk": "medium",
|
||||
"title": "更新块元素",
|
||||
"use_when": [
|
||||
"修改已有块的文本/标题/样式时;blockId 必须来自目标文档本次 block list 结果"
|
||||
"修改已有块的文本/标题/样式(已知 blockId)时"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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,并显式提供 --folder 或 --workspace;不要用已迁移的 doc upload --convert"
|
||||
"只管理知识库空节点或层级时用 wiki node create;本命令侧重创建并写入 adoc",
|
||||
"导入本地 Word/Markdown 并保留服务端转换语义时用 dws doc import;不要用自写分片脚本替代原生 create"
|
||||
],
|
||||
"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,并显式提供 --folder 或 --workspace;不要用已迁移的 doc upload --convert"
|
||||
"只管理知识库空节点或层级时用 wiki node create;本命令侧重创建并写入 adoc",
|
||||
"导入本地 Word/Markdown 并保留服务端转换语义时用 dws doc import;不要用自写分片脚本替代原生 create"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -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,并显式提供 --folder 或 --workspace;不要用已迁移的 doc upload --convert"
|
||||
"只管理知识库空节点或层级时用 wiki node create;本命令侧重创建并写入 adoc",
|
||||
"导入本地 Word/Markdown 并保留服务端转换语义时用 dws doc import;不要用自写分片脚本替代原生 create"
|
||||
]
|
||||
},
|
||||
"canonical_path": {
|
||||
@@ -6450,7 +6450,7 @@
|
||||
"agent_summary_source": "dws-agent-selection/doc",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"常规文件删除使用 dws drive delete;不要继续选择已弃用的 doc delete 兼容入口",
|
||||
"常规文件删除优先 dws drive 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;不要继续选择已弃用的 doc delete 兼容入口",
|
||||
"常规文件删除优先 dws drive 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;不要继续选择已弃用的 doc delete 兼容入口",
|
||||
"常规文件删除优先 dws drive delete;本入口仅为兼容",
|
||||
"用户未确认或目标不清时不要删",
|
||||
"删块用 doc block delete;删评论用 doc comment delete"
|
||||
]
|
||||
@@ -7557,8 +7557,7 @@
|
||||
"agent_summary_source": "dws-agent-selection/doc",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"下载钉盘普通文件用 drive download;导出在线文档用 doc export",
|
||||
"不要把 blockId/nodeId 当作 resourceId,也不要编造 --output:本命令返回临时 downloadUrl"
|
||||
"下载钉盘普通文件用 drive download;导出在线文档用 doc export"
|
||||
],
|
||||
"canonical_path": "doc.download_doc_attachment",
|
||||
"cli_name": "download",
|
||||
@@ -7618,8 +7617,7 @@
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"value": [
|
||||
"下载钉盘普通文件用 drive download;导出在线文档用 doc export",
|
||||
"不要把 blockId/nodeId 当作 resourceId,也不要编造 --output:本命令返回临时 downloadUrl"
|
||||
"下载钉盘普通文件用 drive download;导出在线文档用 doc export"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -7628,8 +7626,7 @@
|
||||
"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",
|
||||
"不要把 blockId/nodeId 当作 resourceId,也不要编造 --output:本命令返回临时 downloadUrl"
|
||||
"下载钉盘普通文件用 drive download;导出在线文档用 doc export"
|
||||
]
|
||||
},
|
||||
"canonical_path": {
|
||||
@@ -7851,7 +7848,7 @@
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"value": [
|
||||
"获取文档正文中附件的临时下载 URL 时;resourceId 必须来自本次 block list 返回的 attachment 块"
|
||||
"获取文档正文中附件的临时下载 URL(resourceId 来自 block list attachment)时"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -7860,7 +7857,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)时"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -8098,7 +8095,7 @@
|
||||
"source": "reviewed_command_registry",
|
||||
"title": "下载文档附件",
|
||||
"use_when": [
|
||||
"获取文档正文中附件的临时下载 URL 时;resourceId 必须来自本次 block list 返回的 attachment 块"
|
||||
"获取文档正文中附件的临时下载 URL(resourceId 来自 block list attachment)时"
|
||||
]
|
||||
},
|
||||
"doc.download_file": {
|
||||
@@ -10945,7 +10942,7 @@
|
||||
"agent_summary_source": "dws-agent-selection/doc",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"发起导入用 doc import;不要先调用 import get,也不要使用示例占位 taskId"
|
||||
"发起导入用 doc import(若入口可用);不要用本命令代替导入"
|
||||
],
|
||||
"canonical_path": "doc.import_get",
|
||||
"cli_name": "get",
|
||||
@@ -11002,7 +10999,7 @@
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"value": [
|
||||
"发起导入用 doc import;不要先调用 import get,也不要使用示例占位 taskId"
|
||||
"发起导入用 doc import(若入口可用);不要用本命令代替导入"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -11011,7 +11008,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;不要先调用 import get,也不要使用示例占位 taskId"
|
||||
"发起导入用 doc import(若入口可用);不要用本命令代替导入"
|
||||
]
|
||||
},
|
||||
"canonical_path": {
|
||||
@@ -11267,7 +11264,7 @@
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"value": [
|
||||
"仅在 doc import 已返回真实 taskId 且自动轮询超时/中断后,查询导入任务结果时"
|
||||
"查询文档导入任务结果(已有 taskId,导入超时/中断后兜底)时"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -11276,7 +11273,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 已返回真实 taskId 且自动轮询超时/中断后,查询导入任务结果时"
|
||||
"查询文档导入任务结果(已有 taskId,导入超时/中断后兜底)时"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -11395,7 +11392,7 @@
|
||||
"source": "reviewed_command_registry",
|
||||
"title": "查询导入任务结果(手动兜底)",
|
||||
"use_when": [
|
||||
"仅在 doc import 已返回真实 taskId 且自动轮询超时/中断后,查询导入任务结果时"
|
||||
"查询文档导入任务结果(已有 taskId,导入超时/中断后兜底)时"
|
||||
]
|
||||
},
|
||||
"doc.insert_document_block": {
|
||||
@@ -11420,8 +11417,7 @@
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"仅在用户没有指定块操作、只要整篇追加 Markdown 时优先 doc update --mode append;显式 insert/标题块/列表块不能折叠",
|
||||
"插入本地文件附件优先 doc media insert;不要为普通附件手写 attachment JSON",
|
||||
"整段 Markdown 或长内容优先 doc update --mode append,不要拆成大量 block insert",
|
||||
"插入本地文件附件优先 doc media insert",
|
||||
"删块用 block delete;改已有块用 block update"
|
||||
],
|
||||
"canonical_path": "doc.insert_document_block",
|
||||
@@ -11493,8 +11489,7 @@
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"value": [
|
||||
"仅在用户没有指定块操作、只要整篇追加 Markdown 时优先 doc update --mode append;显式 insert/标题块/列表块不能折叠",
|
||||
"插入本地文件附件优先 doc media insert;不要为普通附件手写 attachment JSON",
|
||||
"整段 Markdown 或长内容优先 doc update --mode append,不要拆成大量 block insert",
|
||||
"插入本地文件附件优先 doc media insert",
|
||||
"删块用 block delete;改已有块用 block update"
|
||||
]
|
||||
}
|
||||
@@ -11505,8 +11500,7 @@
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"value": [
|
||||
"仅在用户没有指定块操作、只要整篇追加 Markdown 时优先 doc update --mode append;显式 insert/标题块/列表块不能折叠",
|
||||
"插入本地文件附件优先 doc media insert;不要为普通附件手写 attachment JSON",
|
||||
"整段 Markdown 或长内容优先 doc update --mode append,不要拆成大量 block insert",
|
||||
"插入本地文件附件优先 doc media insert",
|
||||
"删块用 block delete;改已有块用 block update"
|
||||
]
|
||||
},
|
||||
@@ -11732,8 +11726,7 @@
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"value": [
|
||||
"用户明确要求在文档中插入新块(段落/标题/列表等)时;简单段落/标题用 --text/--heading,复杂块用 --element JSON",
|
||||
"用户要求有序列表块时使用 JSONML p.list.isOrdered=true(同一 listId)或等价原生 orderedList 结构,不能用普通 Markdown 代替",
|
||||
"相对插入时先 block list 获取真实 blockId,再同时传 --ref-block 与 --where before|after"
|
||||
"用户要求有序列表块时使用 JSONML p.list.isOrdered=true(同一 listId)或等价原生 orderedList 结构,不能用普通 Markdown 代替"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -11743,8 +11736,7 @@
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"value": [
|
||||
"用户明确要求在文档中插入新块(段落/标题/列表等)时;简单段落/标题用 --text/--heading,复杂块用 --element JSON",
|
||||
"用户要求有序列表块时使用 JSONML p.list.isOrdered=true(同一 listId)或等价原生 orderedList 结构,不能用普通 Markdown 代替",
|
||||
"相对插入时先 block list 获取真实 blockId,再同时传 --ref-block 与 --where before|after"
|
||||
"用户要求有序列表块时使用 JSONML p.list.isOrdered=true(同一 listId)或等价原生 orderedList 结构,不能用普通 Markdown 代替"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -12887,8 +12879,7 @@
|
||||
"title": "插入块元素",
|
||||
"use_when": [
|
||||
"用户明确要求在文档中插入新块(段落/标题/列表等)时;简单段落/标题用 --text/--heading,复杂块用 --element JSON",
|
||||
"用户要求有序列表块时使用 JSONML p.list.isOrdered=true(同一 listId)或等价原生 orderedList 结构,不能用普通 Markdown 代替",
|
||||
"相对插入时先 block list 获取真实 blockId,再同时传 --ref-block 与 --where before|after"
|
||||
"用户要求有序列表块时使用 JSONML p.list.isOrdered=true(同一 listId)或等价原生 orderedList 结构,不能用普通 Markdown 代替"
|
||||
]
|
||||
},
|
||||
"doc.list_comments": {
|
||||
@@ -19717,8 +19708,7 @@
|
||||
"agent_summary_source": "dws-agent-selection/doc",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"新建评论用 create/create-inline;删评论用 delete",
|
||||
"不要把 commentId、blockId 或示例占位符传给 --comment-key"
|
||||
"新建评论用 create/create-inline;删评论用 delete"
|
||||
],
|
||||
"canonical_path": "doc.reply_comment",
|
||||
"cli_name": "reply",
|
||||
@@ -19779,8 +19769,7 @@
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"value": [
|
||||
"新建评论用 create/create-inline;删评论用 delete",
|
||||
"不要把 commentId、blockId 或示例占位符传给 --comment-key"
|
||||
"新建评论用 create/create-inline;删评论用 delete"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -19789,8 +19778,7 @@
|
||||
"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",
|
||||
"不要把 commentId、blockId 或示例占位符传给 --comment-key"
|
||||
"新建评论用 create/create-inline;删评论用 delete"
|
||||
]
|
||||
},
|
||||
"canonical_path": {
|
||||
@@ -20068,7 +20056,7 @@
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"value": [
|
||||
"回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 必须来自本次 list/create 返回"
|
||||
"回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 来自 list/create"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -20077,7 +20065,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"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -20738,7 +20726,7 @@
|
||||
"source": "reviewed_command_registry",
|
||||
"title": "回复评论",
|
||||
"use_when": [
|
||||
"回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 必须来自本次 list/create 返回"
|
||||
"回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 来自 list/create"
|
||||
]
|
||||
},
|
||||
"doc.search_documents": {
|
||||
@@ -37251,7 +37239,8 @@
|
||||
"agent_summary_source": "dws-agent-selection/doc",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"删除评论用 delete;回复用 reply"
|
||||
"删除评论用 delete;回复用 reply",
|
||||
"响应为 null/空对象或回查仍是旧正文时不能判成功,必须报告更新未生效或部分完成"
|
||||
],
|
||||
"canonical_path": "doc.update_comment",
|
||||
"cli_name": "update",
|
||||
@@ -37270,7 +37259,7 @@
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
|
||||
"review_reason": "人工对齐评论更新的业务结果与 comment list 回查语义;null/空响应不能单独证明成功,不改变命令身份、参数契约或接口绑定。",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"value": "更新指定文档评论的文字内容和可选 @用户/@群。"
|
||||
@@ -37278,7 +37267,7 @@
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
|
||||
"review_reason": "人工对齐评论更新的业务结果与 comment list 回查语义;null/空响应不能单独证明成功,不改变命令身份、参数契约或接口绑定。",
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"value": "更新指定文档评论的文字内容和可选 @用户/@群。"
|
||||
},
|
||||
@@ -37302,20 +37291,22 @@
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
|
||||
"review_reason": "人工对齐评论更新的业务结果与 comment list 回查语义;null/空响应不能单独证明成功,不改变命令身份、参数契约或接口绑定。",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"value": [
|
||||
"删除评论用 delete;回复用 reply"
|
||||
"删除评论用 delete;回复用 reply",
|
||||
"响应为 null/空对象或回查仍是旧正文时不能判成功,必须报告更新未生效或部分完成"
|
||||
]
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
|
||||
"review_reason": "人工对齐评论更新的业务结果与 comment list 回查语义;null/空响应不能单独证明成功,不改变命令身份、参数契约或接口绑定。",
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"value": [
|
||||
"删除评论用 delete;回复用 reply"
|
||||
"删除评论用 delete;回复用 reply",
|
||||
"响应为 null/空对象或回查仍是旧正文时不能判成功,必须报告更新未生效或部分完成"
|
||||
]
|
||||
},
|
||||
"canonical_path": {
|
||||
@@ -37404,7 +37395,7 @@
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
|
||||
"review_reason": "人工对齐评论更新的业务结果与 comment list 回查语义;null/空响应不能单独证明成功,不改变命令身份、参数契约或接口绑定。",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"value": [
|
||||
@@ -37415,7 +37406,7 @@
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
|
||||
"review_reason": "人工对齐评论更新的业务结果与 comment list 回查语义;null/空响应不能单独证明成功,不改变命令身份、参数契约或接口绑定。",
|
||||
"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",
|
||||
@@ -37503,7 +37494,7 @@
|
||||
},
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
|
||||
"review_reason": "人工对齐评论更新的业务结果与 comment list 回查语义;null/空响应不能单独证明成功,不改变命令身份、参数契约或接口绑定。",
|
||||
"selected": false,
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"value": true
|
||||
@@ -37555,20 +37546,20 @@
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "reviewed_explicit",
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
|
||||
"review_reason": "人工对齐评论更新的业务结果与 comment list 回查语义;null/空响应不能单独证明成功,不改变命令身份、参数契约或接口绑定。",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"value": [
|
||||
"修改已有评论正文;可选更新 --mention 或 --mentioned-open-conversation-id"
|
||||
"修改已有评论正文;可选更新 --mention 或 --mentioned-open-conversation-id;执行后必须用 comment list 回查同一 commentKey 的目标字段"
|
||||
]
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
|
||||
"review_reason": "人工对齐评论更新的业务结果与 comment list 回查语义;null/空响应不能单独证明成功,不改变命令身份、参数契约或接口绑定。",
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"value": [
|
||||
"修改已有评论正文;可选更新 --mention 或 --mentioned-open-conversation-id"
|
||||
"修改已有评论正文;可选更新 --mention 或 --mentioned-open-conversation-id;执行后必须用 comment list 回查同一 commentKey 的目标字段"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -38093,7 +38084,7 @@
|
||||
"source": "reviewed_command_registry",
|
||||
"title": "更新文档评论",
|
||||
"use_when": [
|
||||
"修改已有评论正文;可选更新 --mention 或 --mentioned-open-conversation-id"
|
||||
"修改已有评论正文;可选更新 --mention 或 --mentioned-open-conversation-id;执行后必须用 comment list 回查同一 commentKey 的目标字段"
|
||||
]
|
||||
},
|
||||
"doc.update_document": {
|
||||
@@ -38120,10 +38111,10 @@
|
||||
"agent_summary_source": "dws-agent-selection/doc",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"目标不是 adoc 或只要改单个块时改用 doc block update",
|
||||
"覆盖模式用户未确认前不要执行;先 --dry-run 预览,确认后再加 --yes",
|
||||
"--content 与 --content-file 二选一;--index 仅用于 mode=append",
|
||||
"创建新文档用 doc create,不要用 update 冒充创建"
|
||||
"只在末尾补一小段纯文本优先 +doc-append;只改一个块用 doc block update",
|
||||
"目标不是 adoc 时按 drive info 的 extension 切对应产品",
|
||||
"覆盖模式用户未确认前不要执行;可先 --dry-run 预览",
|
||||
"创建新文档用 doc create;不要预先手工分片或循环重试覆盖"
|
||||
],
|
||||
"canonical_path": "doc.update_document",
|
||||
"cli_name": "update",
|
||||
@@ -38198,10 +38189,10 @@
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"value": [
|
||||
"目标不是 adoc 或只要改单个块时改用 doc block update",
|
||||
"覆盖模式用户未确认前不要执行;先 --dry-run 预览,确认后再加 --yes",
|
||||
"--content 与 --content-file 二选一;--index 仅用于 mode=append",
|
||||
"创建新文档用 doc create,不要用 update 冒充创建"
|
||||
"只在末尾补一小段纯文本优先 +doc-append;只改一个块用 doc block update",
|
||||
"目标不是 adoc 时按 drive info 的 extension 切对应产品",
|
||||
"覆盖模式用户未确认前不要执行;可先 --dry-run 预览",
|
||||
"创建新文档用 doc create;不要预先手工分片或循环重试覆盖"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -38210,10 +38201,10 @@
|
||||
"review_reason": "人工对齐 Runtime 的 10000 字符自动分片、append/overwrite 动态门禁与根 Skill 的写后回读流程;不改变参数和安全事实。",
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"value": [
|
||||
"目标不是 adoc 或只要改单个块时改用 doc block update",
|
||||
"覆盖模式用户未确认前不要执行;先 --dry-run 预览,确认后再加 --yes",
|
||||
"--content 与 --content-file 二选一;--index 仅用于 mode=append",
|
||||
"创建新文档用 doc create,不要用 update 冒充创建"
|
||||
"只在末尾补一小段纯文本优先 +doc-append;只改一个块用 doc block update",
|
||||
"目标不是 adoc 时按 drive info 的 extension 切对应产品",
|
||||
"覆盖模式用户未确认前不要执行;可先 --dry-run 预览",
|
||||
"创建新文档用 doc create;不要预先手工分片或循环重试覆盖"
|
||||
]
|
||||
},
|
||||
"canonical_path": {
|
||||
@@ -38469,8 +38460,7 @@
|
||||
"value": [
|
||||
"用户要向已有 adoc 追加长、多行或文件内容时用 --mode append + --content-file;CLI 自动分片",
|
||||
"用户明确要求整篇覆盖替换时用 --mode overwrite(破坏性)",
|
||||
"append 且要插到第 N 个 block 前时加 --index N(先 block list)",
|
||||
"长文本、多行内容或表格优先写入 UTF-8 文件并使用 --content-file,避免 shell 转义损坏"
|
||||
"append 且要插到第 N 个 block 前时加 --index N(先 block list)"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -38481,8 +38471,7 @@
|
||||
"value": [
|
||||
"用户要向已有 adoc 追加长、多行或文件内容时用 --mode append + --content-file;CLI 自动分片",
|
||||
"用户明确要求整篇覆盖替换时用 --mode overwrite(破坏性)",
|
||||
"append 且要插到第 N 个 block 前时加 --index N(先 block list)",
|
||||
"长文本、多行内容或表格优先写入 UTF-8 文件并使用 --content-file,避免 shell 转义损坏"
|
||||
"append 且要插到第 N 个 block 前时加 --index N(先 block list)"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -39097,8 +39086,23 @@
|
||||
"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": [
|
||||
{
|
||||
@@ -39142,8 +39146,14 @@
|
||||
"required": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "inference",
|
||||
"precedence": "cobra_contract",
|
||||
"selected": true,
|
||||
"source": "cobra_hard_required",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"precedence": "inference",
|
||||
"selected": false,
|
||||
"source": "usage_required_inference",
|
||||
"value": true
|
||||
},
|
||||
@@ -39154,9 +39164,9 @@
|
||||
"value": false
|
||||
}
|
||||
],
|
||||
"precedence": "inference",
|
||||
"precedence": "cobra_contract",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "usage_required_inference",
|
||||
"source": "cobra_hard_required",
|
||||
"value": true
|
||||
},
|
||||
"required_when": {
|
||||
@@ -39522,8 +39532,7 @@
|
||||
"use_when": [
|
||||
"用户要向已有 adoc 追加长、多行或文件内容时用 --mode append + --content-file;CLI 自动分片",
|
||||
"用户明确要求整篇覆盖替换时用 --mode overwrite(破坏性)",
|
||||
"append 且要插到第 N 个 block 前时加 --index N(先 block list)",
|
||||
"长文本、多行内容或表格优先写入 UTF-8 文件并使用 --content-file,避免 shell 转义损坏"
|
||||
"append 且要插到第 N 个 block 前时加 --index N(先 block list)"
|
||||
]
|
||||
},
|
||||
"doc.update_document_block": {
|
||||
@@ -39548,8 +39557,7 @@
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"插入新块用 block insert;删除用 block delete",
|
||||
"改文档显示名用 rename;整篇覆盖用 update overwrite",
|
||||
"不要跨文档复用 blockId,也不要把 nodeId/commentKey 当作 blockId"
|
||||
"改文档显示名用 rename;整篇覆盖用 update overwrite"
|
||||
],
|
||||
"canonical_path": "doc.update_document_block",
|
||||
"cli_name": "update",
|
||||
@@ -39619,8 +39627,7 @@
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"value": [
|
||||
"插入新块用 block insert;删除用 block delete",
|
||||
"改文档显示名用 rename;整篇覆盖用 update overwrite",
|
||||
"不要跨文档复用 blockId,也不要把 nodeId/commentKey 当作 blockId"
|
||||
"改文档显示名用 rename;整篇覆盖用 update overwrite"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -39630,8 +39637,7 @@
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"value": [
|
||||
"插入新块用 block insert;删除用 block delete",
|
||||
"改文档显示名用 rename;整篇覆盖用 update overwrite",
|
||||
"不要跨文档复用 blockId,也不要把 nodeId/commentKey 当作 blockId"
|
||||
"改文档显示名用 rename;整篇覆盖用 update overwrite"
|
||||
]
|
||||
},
|
||||
"canonical_path": {
|
||||
@@ -39853,7 +39859,7 @@
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"value": [
|
||||
"修改已有块的文本/标题/样式时;blockId 必须来自目标文档本次 block list 结果"
|
||||
"修改已有块的文本/标题/样式(已知 blockId)时"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -39862,7 +39868,7 @@
|
||||
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
|
||||
"source": "internal/cli/schema_hints/selection/doc.json",
|
||||
"value": [
|
||||
"修改已有块的文本/标题/样式时;blockId 必须来自目标文档本次 block list 结果"
|
||||
"修改已有块的文本/标题/样式(已知 blockId)时"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -40704,7 +40710,7 @@
|
||||
"source": "reviewed_command_registry",
|
||||
"title": "更新块元素",
|
||||
"use_when": [
|
||||
"修改已有块的文本/标题/样式时;blockId 必须来自目标文档本次 block list 结果"
|
||||
"修改已有块的文本/标题/样式(已知 blockId)时"
|
||||
]
|
||||
},
|
||||
"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,并显式提供 --folder 或 --workspace;不要用已迁移的 doc upload --convert"
|
||||
"只管理知识库空节点或层级时用 wiki node create;本命令侧重创建并写入 adoc",
|
||||
"导入本地 Word/Markdown 并保留服务端转换语义时用 dws doc import;不要用自写分片脚本替代原生 create"
|
||||
],
|
||||
"examples": [
|
||||
"dws doc create --name \"项目周报\" --format json",
|
||||
@@ -205,7 +205,7 @@
|
||||
"用户明确要求用 doc delete 兼容入口将文档/文件移入回收站,且已确认目标时"
|
||||
],
|
||||
"avoid_when": [
|
||||
"常规文件删除使用 dws drive delete;不要继续选择已弃用的 doc delete 兼容入口",
|
||||
"常规文件删除优先 dws drive delete;本入口仅为兼容",
|
||||
"用户未确认或目标不清时不要删",
|
||||
"删块用 doc block delete;删评论用 doc comment delete"
|
||||
],
|
||||
@@ -250,11 +250,10 @@
|
||||
"doc.download_doc_attachment": {
|
||||
"agent_summary": "获取文档附件的临时下载链接",
|
||||
"use_when": [
|
||||
"获取文档正文中附件的临时下载 URL 时;resourceId 必须来自本次 block list 返回的 attachment 块"
|
||||
"获取文档正文中附件的临时下载 URL(resourceId 来自 block list attachment)时"
|
||||
],
|
||||
"avoid_when": [
|
||||
"下载钉盘普通文件用 drive download;导出在线文档用 doc export",
|
||||
"不要把 blockId/nodeId 当作 resourceId,也不要编造 --output:本命令返回临时 downloadUrl"
|
||||
"下载钉盘普通文件用 drive download;导出在线文档用 doc export"
|
||||
],
|
||||
"examples": [
|
||||
"dws doc media download --node <DOC_ID> --resource-id <RESOURCE_ID> --format json"
|
||||
@@ -352,10 +351,10 @@
|
||||
"doc.import_get": {
|
||||
"agent_summary": "根据 taskId 查询文档导入任务的执行结果",
|
||||
"use_when": [
|
||||
"仅在 doc import 已返回真实 taskId 且自动轮询超时/中断后,查询导入任务结果时"
|
||||
"查询文档导入任务结果(已有 taskId,导入超时/中断后兜底)时"
|
||||
],
|
||||
"avoid_when": [
|
||||
"发起导入用 doc import;不要先调用 import get,也不要使用示例占位 taskId"
|
||||
"发起导入用 doc import(若入口可用);不要用本命令代替导入"
|
||||
],
|
||||
"examples": [
|
||||
"dws doc import get --task-id <TASK_ID> --format json"
|
||||
@@ -374,13 +373,11 @@
|
||||
"agent_summary": "向文档插入块元素",
|
||||
"use_when": [
|
||||
"用户明确要求在文档中插入新块(段落/标题/列表等)时;简单段落/标题用 --text/--heading,复杂块用 --element JSON",
|
||||
"用户要求有序列表块时使用 JSONML p.list.isOrdered=true(同一 listId)或等价原生 orderedList 结构,不能用普通 Markdown 代替",
|
||||
"相对插入时先 block list 获取真实 blockId,再同时传 --ref-block 与 --where before|after"
|
||||
"用户要求有序列表块时使用 JSONML p.list.isOrdered=true(同一 listId)或等价原生 orderedList 结构,不能用普通 Markdown 代替"
|
||||
],
|
||||
"avoid_when": [
|
||||
"仅在用户没有指定块操作、只要整篇追加 Markdown 时优先 doc update --mode append;显式 insert/标题块/列表块不能折叠",
|
||||
"插入本地文件附件优先 doc media insert;不要为普通附件手写 attachment JSON",
|
||||
"整段 Markdown 或长内容优先 doc update --mode append,不要拆成大量 block insert",
|
||||
"插入本地文件附件优先 doc media insert",
|
||||
"删块用 block delete;改已有块用 block update"
|
||||
],
|
||||
"examples": [
|
||||
@@ -611,11 +608,10 @@
|
||||
"doc.reply_comment": {
|
||||
"agent_summary": "回复文档评论",
|
||||
"use_when": [
|
||||
"回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 必须来自本次 list/create 返回"
|
||||
"回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 来自 list/create"
|
||||
],
|
||||
"avoid_when": [
|
||||
"新建评论用 create/create-inline;删评论用 delete",
|
||||
"不要把 commentId、blockId 或示例占位符传给 --comment-key"
|
||||
"新建评论用 create/create-inline;删评论用 delete"
|
||||
],
|
||||
"examples": [
|
||||
"dws doc comment reply --node <DOC_ID> --comment-key <COMMENT_KEY> --content \"同意\" --mentioned-open-conversation-id <openConversationId> --format json",
|
||||
@@ -725,17 +721,18 @@
|
||||
"doc.update_comment": {
|
||||
"agent_summary": "更新指定文档评论的文字内容和可选 @用户/@群。",
|
||||
"use_when": [
|
||||
"修改已有评论正文;可选更新 --mention 或 --mentioned-open-conversation-id"
|
||||
"修改已有评论正文;可选更新 --mention 或 --mentioned-open-conversation-id;执行后必须用 comment list 回查同一 commentKey 的目标字段"
|
||||
],
|
||||
"avoid_when": [
|
||||
"删除评论用 delete;回复用 reply"
|
||||
"删除评论用 delete;回复用 reply",
|
||||
"响应为 null/空对象或回查仍是旧正文时不能判成功,必须报告更新未生效或部分完成"
|
||||
],
|
||||
"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": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
|
||||
"review_reason": "人工对齐评论更新的业务结果与 comment list 回查语义;null/空响应不能单独证明成功,不改变命令身份、参数契约或接口绑定。",
|
||||
"source_refs": [
|
||||
"internal/cli/schema_command_registry.json#doc.update_comment",
|
||||
"cobra-help:dws doc comment update --help",
|
||||
@@ -749,14 +746,13 @@
|
||||
"use_when": [
|
||||
"用户要向已有 adoc 追加长、多行或文件内容时用 --mode append + --content-file;CLI 自动分片",
|
||||
"用户明确要求整篇覆盖替换时用 --mode overwrite(破坏性)",
|
||||
"append 且要插到第 N 个 block 前时加 --index N(先 block list)",
|
||||
"长文本、多行内容或表格优先写入 UTF-8 文件并使用 --content-file,避免 shell 转义损坏"
|
||||
"append 且要插到第 N 个 block 前时加 --index N(先 block list)"
|
||||
],
|
||||
"avoid_when": [
|
||||
"目标不是 adoc 或只要改单个块时改用 doc block update",
|
||||
"覆盖模式用户未确认前不要执行;先 --dry-run 预览,确认后再加 --yes",
|
||||
"--content 与 --content-file 二选一;--index 仅用于 mode=append",
|
||||
"创建新文档用 doc create,不要用 update 冒充创建"
|
||||
"只在末尾补一小段纯文本优先 +doc-append;只改一个块用 doc block update",
|
||||
"目标不是 adoc 时按 drive info 的 extension 切对应产品",
|
||||
"覆盖模式用户未确认前不要执行;可先 --dry-run 预览",
|
||||
"创建新文档用 doc create;不要预先手工分片或循环重试覆盖"
|
||||
],
|
||||
"examples": [
|
||||
"dws doc update --node <DOC_ID> --content \"# 追加内容\" --mode append --format json",
|
||||
@@ -777,12 +773,11 @@
|
||||
"doc.update_document_block": {
|
||||
"agent_summary": "更新文档中的指定块",
|
||||
"use_when": [
|
||||
"修改已有块的文本/标题/样式时;blockId 必须来自目标文档本次 block list 结果"
|
||||
"修改已有块的文本/标题/样式(已知 blockId)时"
|
||||
],
|
||||
"avoid_when": [
|
||||
"插入新块用 block insert;删除用 block delete",
|
||||
"改文档显示名用 rename;整篇覆盖用 update overwrite",
|
||||
"不要跨文档复用 blockId,也不要把 nodeId/commentKey 当作 blockId"
|
||||
"改文档显示名用 rename;整篇覆盖用 update overwrite"
|
||||
],
|
||||
"examples": [
|
||||
"dws doc block update --node <DOC_ID> --block-id <BLOCK_ID> --text \"新内容\" --format json"
|
||||
|
||||
@@ -363,24 +363,6 @@ 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,22 +340,6 @@ 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"):
|
||||
|
||||
+2
-177
@@ -9,7 +9,6 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -133,8 +132,6 @@ func recordQueryFetchAll(toolArgs map[string]any, pageLimit int) error {
|
||||
var allRecords []any
|
||||
page := 0
|
||||
lastCursor := ""
|
||||
seenCursors := map[string]struct{}{}
|
||||
stopReason := ""
|
||||
|
||||
for {
|
||||
page++
|
||||
@@ -204,19 +201,6 @@ 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 {
|
||||
@@ -239,10 +223,6 @@ 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
|
||||
}
|
||||
@@ -801,20 +781,6 @@ 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",
|
||||
@@ -848,154 +814,17 @@ func isAitableRetryableError(err error) bool {
|
||||
func parseFieldsJSON(raw string) ([]any, error) {
|
||||
var fields []any
|
||||
if err := json.Unmarshal([]byte(raw), &fields); err == nil {
|
||||
return validateAitableCreateFields(fields)
|
||||
return fields, nil
|
||||
}
|
||||
var wrapper map[string]any
|
||||
if err := json.Unmarshal([]byte(raw), &wrapper); err == nil {
|
||||
if arr, ok := wrapper["fields"].([]any); ok {
|
||||
return validateAitableCreateFields(arr)
|
||||
return arr, nil
|
||||
}
|
||||
}
|
||||
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;两者都未传返回 ""。
|
||||
// 抽出独立函数便于单测覆盖。
|
||||
@@ -1541,10 +1370,6 @@ 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,87 +88,6 @@ 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,9 +180,7 @@ 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},
|
||||
{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}} {
|
||||
}{{nil, false}, {errors.New("timeout"), true}, {errors.New("SYSTEM_ERROR"), true}, {errors.New("retryable: true"), true}, {errors.New("bad request"), false}} {
|
||||
if got := isAitableRetryableError(tc.err); got != tc.want {
|
||||
t.Errorf("isAitableRetryableError(%v) = %v", tc.err, got)
|
||||
}
|
||||
@@ -250,26 +248,15 @@ func TestCrossPlatformCoverageAitableViewConfigAndHelpers(t *testing.T) {
|
||||
t.Fatalf("typed-only update = %#v, %v", merged, err)
|
||||
}
|
||||
|
||||
for _, raw := range []string{`[{"fieldName":"N","type":"text"}]`, `{"fields":[{"fieldName":"N","type":"text"}]}`, `{}`, `{`} {
|
||||
for _, raw := range []string{`[1]`, `{"fields":[1]}`, `{}`, `{`} {
|
||||
fields, err := parseFieldsJSON(raw)
|
||||
if (strings.HasPrefix(raw, `[{`) || strings.Contains(raw, "fields")) && (err != nil || len(fields) != 1) {
|
||||
if (raw == `[1]` || 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) {
|
||||
@@ -325,12 +312,4 @@ 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 err := ValidateChatMediaID(iconMediaID); err != nil {
|
||||
return fmt.Errorf("invalid --icon-media-id: %w", err)
|
||||
if iconMediaID == "" {
|
||||
return fmt.Errorf("invalid --icon-media-id: mediaId 不能为空\n hint: 请使用上游媒体上传能力返回的有效 mediaId;DWS CLI 不提供本地文件到 mediaId 的上传命令")
|
||||
}
|
||||
return callMCPToolOnServer("im", "update_group_icon", map[string]any{
|
||||
"openConversationId": mustGetFlag(cmd, "group"),
|
||||
|
||||
@@ -85,22 +85,6 @@ 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,12 +4,7 @@
|
||||
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
import "context"
|
||||
|
||||
// ConversationLocalFileMeta exposes the already-reviewed native chat upload
|
||||
// metadata to built-in semantic Shortcuts. It remains an alias so the native
|
||||
@@ -46,21 +41,3 @@ 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
|
||||
}
|
||||
|
||||
+2
-25
@@ -33,27 +33,6 @@ 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,
|
||||
// 传大值会直接报错 (与悟空实现一致: 默认分页大小 + 游标翻页)。
|
||||
@@ -1670,8 +1649,7 @@ WARNING: --mode overwrite 为破坏性写入,会清空原文档全部内容。
|
||||
updateCmd.Flags().String("markdown", "", "已弃用,请使用 --content 代替")
|
||||
_ = updateCmd.Flags().MarkHidden("markdown")
|
||||
updateCmd.Flags().String("mode", "", "更新模式: overwrite=覆盖, append=追加 (必填)")
|
||||
// Kept out of Cobra's generic required-flag validator so doc local
|
||||
// preflight can return a structured, actionable error before MCP dispatch.
|
||||
_ = updateCmd.MarkFlagRequired("mode")
|
||||
updateCmd.Flags().Int("index", -1, "插入位置(从 0 开始),仅在 mode=append 时生效。指定将内容插入到文档第几个 block 之前。不传时追加到末尾")
|
||||
updateCmd.Flags().Bool("yes", false, "确认执行破坏性写入 (仅 --mode overwrite 需要)")
|
||||
updateCmd.Flags().Bool("dry-run", false, "预览覆盖写入差异,不调用远端 update")
|
||||
@@ -2065,7 +2043,7 @@ commentKey可从 dws doc comment create 或 dws doc comment list 返回结果中
|
||||
if err := appendCommentGroupMentions(cmd, toolArgs); err != nil {
|
||||
return err
|
||||
}
|
||||
return callDocCommentUpdate(toolArgs)
|
||||
return callMCPToolOnServer("doc-comment", "update_comment", toolArgs)
|
||||
},
|
||||
}
|
||||
commentUpdateCmd.Flags().String("node", "", "目标文档的标识,支持传入 URL 或 ID (必填)")
|
||||
@@ -2921,7 +2899,6 @@ 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,7 +18,6 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
|
||||
@@ -31,29 +30,12 @@ type docCommentMutationCall struct {
|
||||
}
|
||||
|
||||
type docCommentMutationCaller struct {
|
||||
calls []docCommentMutationCall
|
||||
response string
|
||||
calls []docCommentMutationCall
|
||||
}
|
||||
|
||||
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})
|
||||
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))
|
||||
}
|
||||
return &edition.ToolResult{Content: []edition.ContentBlock{{Type: "text", Text: `{}`}}}, nil
|
||||
}
|
||||
|
||||
func (*docCommentMutationCaller) Format() string { return "json" }
|
||||
|
||||
@@ -1,446 +0,0 @@
|
||||
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)) }
|
||||
@@ -1,137 +0,0 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
// 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,7 +326,6 @@ 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 || payload["status"] != "completed" {
|
||||
if payload["nodeId"] != "node-1" || payload["success"] != true {
|
||||
t.Fatalf("output missing success contract: %#v", payload)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,6 @@ 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"
|
||||
)
|
||||
@@ -84,18 +82,6 @@ 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{
|
||||
@@ -200,12 +186,6 @@ 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"),
|
||||
@@ -227,8 +207,7 @@ 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`},
|
||||
Validate: validateChatUpdateSettings,
|
||||
Tips: []string{`dws chat +chat-update-settings --group <openConversationId> --setting-key searchable --status 1`},
|
||||
Execute: func(rt *shortcut.RuntimeContext) error {
|
||||
return rt.CallMCP("update_group_settings", map[string]any{
|
||||
"openConversationId": rt.Str("group"),
|
||||
@@ -238,27 +217,6 @@ 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,15 +54,6 @@ 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 {
|
||||
@@ -164,9 +155,6 @@ 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" {
|
||||
@@ -189,32 +177,6 @@ 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,7 +420,8 @@ func executeMessagesSendUserFile(
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
targetArgs := messagesSendUploadTarget(group, openID)
|
||||
targetArgs := map[string]any{}
|
||||
addMessagesSendUserTarget(targetArgs, group, openID)
|
||||
idempotencyKey := messagesSendIdempotencyKey(rt)
|
||||
if rt.DryRun() {
|
||||
return rt.Output(map[string]any{
|
||||
@@ -489,13 +490,6 @@ 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,11 +2,12 @@
|
||||
|
||||
> 通用规范见 [_common/conventions.md](_common/conventions.md)。
|
||||
|
||||
## 显式工作流
|
||||
## 显式工作流与事实保真
|
||||
|
||||
- 用户点名的 `create → list → insert/append/update` 是可观察命令链,必须保持顺序逐项执行;create 只承载明确的初始正文。有序列表块必须验证回读结构中的 `list.isOrdered=true`。
|
||||
- `--name` 不替代用户显式要求的正文 H1;新建资源返回 ID 后,同一请求的指代绑定该新资源,禁止搜索同名旧资源替换。
|
||||
- Word/Excel 需要“在线编辑/直接在线改”时使用 `doc import`,普通 `drive upload` 只保留原文件。
|
||||
- 汇总时保留证据强度:验证数量不等于通过数量,整理问题不等于根因分析。任一步骤返回 `null`/空结果或回查不一致时只能报告部分完成。
|
||||
|
||||
| Recipe | 行动指南(固定路线) |
|
||||
|--------|-------------------|
|
||||
|
||||
@@ -1060,6 +1060,8 @@ 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,6 +128,7 @@ 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,6 +60,8 @@ 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}")
|
||||
@@ -145,12 +147,14 @@ def run(argv: Optional[Sequence[str]] = None) -> int:
|
||||
["doc", "info", "--node", node_id, "--format", "json"],
|
||||
dry_run=args.dry_run,
|
||||
)
|
||||
run_dws(
|
||||
readback = 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,24 @@ metadata:
|
||||
|
||||
# 钉钉 AI 表格 Skill
|
||||
|
||||
## 执行入口
|
||||
## 前置条件 — 执行操作前必读
|
||||
|
||||
执行任何 `dws` 操作前,完整读取 [`dws-shared`](../dws-shared/SKILL.md),但不要预加载其 references。高频意图直接使用本文件骨架;仅特殊参数、复杂数据形态或边界不明时读取一个 branch reference。
|
||||
> **CRITICAL — 执行任何 `dws` 操作前,MUST 先用 Read 工具完整读取 [`dws-shared`](../dws-shared/SKILL.md)。**该轻量文件包含全局执行契约、安全底线及 shared references 的按需加载导航;不要预加载其全部 references。
|
||||
|
||||
## 加载与路由顺序
|
||||
|
||||
1. 命中下方高频意图时直接使用精确骨架,不先查 Help 或产品级 Schema。
|
||||
2. 路由优先级固定为:精确骨架 / recipe > 匹配的公开 Shortcut > 原子命令。脚本只用于 Runtime 尚未覆盖的批量、文件传输或异步编排,不与普通原子命令竞争默认入口。
|
||||
2. 路由优先级固定为:精确 recipe / 可运行脚本 > 匹配的公开 Shortcut > 原子命令。命令已确定且参数清楚时直接执行。
|
||||
3. 参数、约束或安全语义不确定时只读 leaf Schema:`dws schema --cli-path "aitable <leaf>" --format json`;只有当前 Cobra flag 不确定时才读对应 `--help`。
|
||||
4. 复杂字段、筛选、导入导出、视图、权限或工作流任务,按“低频能力与 Reference”只加载相关文件,不预读整个 `references/aitable/`。
|
||||
5. 现有骨架和 reference 都无法定位能力时,才用 Runtime Shortcut Catalog 做最后发现;不得猜 `cli_path` 或 flag。
|
||||
6. Schema、Help、reference 与实际返回冲突时采用更安全的解释并报告契约漂移;`confirmation=user_required` 时先确认,再添加 `--yes`。
|
||||
7. 用户已给足目标、字段和数据时,按依赖链连续执行;中间结果只用于提取真实 ID 和判断停止条件,全部完成后统一回读并答复。
|
||||
7. 用户已给足名称、字段、数据和目标时,直接按依赖链完成全部步骤;不要调用 todo 工具、分步汇报或追问已明确的信息。中间返回只用于提取下一步 ID 和判断失败,完成所有请求后再统一回读并答复。
|
||||
|
||||
<!-- VISIBLE_SHORTCUTS_START -->
|
||||
## Shortcut 发现(按需)
|
||||
|
||||
`aitable` 当前有 29 条公开 shortcut,完整清单保留在 Runtime Shortcut Catalog,根 Skill 不重复展开。
|
||||
`aitable` 当前有 29 条公开 shortcut。完整清单保留在 Runtime Shortcut Catalog;已完成 Schema curation 的子集可通过 leaf Schema 查询。高频产品根 Skill 不重复展开完整清单。已知意图直接使用下方的优先路由、意图表或任务 reference;命令已选中时直接执行,只在参数/安全语义不确定时读取 leaf Schema,在当前 Cobra flags 不确定时读取 leaf Help。
|
||||
|
||||
仅当现有路由和 reference 都无法定位低频能力时,才执行 `dws shortcut list --service aitable --compact --format json` 做最后回退;不要为已知高频意图加载完整 Shortcut Catalog 或产品级 Schema。
|
||||
<!-- VISIBLE_SHORTCUTS_END -->
|
||||
@@ -44,9 +44,7 @@ metadata:
|
||||
| View / Dashboard / Chart | `viewId` / `dashboardId` / `chartId` 各自绑定当前 Base/Table,不跨对象复用 |
|
||||
| 异步任务 | `taskId` / `importId` 只用于对应导出或导入任务,不能替代业务对象 ID |
|
||||
|
||||
所有下游 ID 都从当前链路的结构化返回中提取;同名多候选必须让用户消歧,不默认取第一项。Base → Table → Field/Record/View 的容器关系必须保持一致,不跨 Base 或 Table 复用子对象 ID。
|
||||
|
||||
创建、复制、导入或新建字段/记录返回 ID 后,立即绑定同一请求中的“这个”“刚才新建的”等指代;除非用户明确转向历史资源,否则不得再按名称搜索并替换为旧对象。
|
||||
所有下游 ID 都从当前链路的结构化返回中提取;同名多候选必须让用户消歧,不默认取第一项,也不复用未经本轮校验的旧 ID。
|
||||
|
||||
## 核心意图与执行骨架
|
||||
|
||||
@@ -54,7 +52,7 @@ metadata:
|
||||
|---|---|---|
|
||||
| 按名称找 Base | `dws aitable +resolve-base --name "<名称>" --format json` | 唯一命中才继续;多候选停止并消歧 |
|
||||
| 浏览最近访问 | `dws aitable +base-list --format json` | 只代表最近访问,不得宣称全量 |
|
||||
| 搜索模板 | `dws aitable template search --query "<关键词>" --format json` | 只返回真实候选,不擅自套用模板或创建 Base |
|
||||
| 搜索模板 | `dws aitable +template-search --query "<关键词>" --format json` | 关键词参数是 `--query`,只返回真实候选,不擅自创建 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;按需展开以控制返回体 |
|
||||
@@ -63,7 +61,8 @@ metadata:
|
||||
| 更新记录 | `dws aitable record update --base-id <baseId> --table-id <tableId> --records '[{"recordId":"<id>","cells":{"<fieldId>":<值>}}]' --format json` | 先 query 拿 recordId;只传需改字段;取 `data.recordIds[]` 后回读 |
|
||||
| 删除记录 | 先 `dws aitable +record-query ...` 定位,再 `dws aitable record delete --base-id <baseId> --table-id <tableId> --record-ids <ids>` | 展示目标与影响,得到明确确认后才加 `--yes` |
|
||||
| 创建 Base / Table | `dws aitable base create --name "<名>"` / `dws aitable table create --base-id <id> --name "<名>" --fields '[...]'` | 使用创建返回的真实 ID;系统改名/加后缀时不得继续猜原名 |
|
||||
| 复制视图 | `dws aitable view duplicate --base-id <baseId> --table-id <tableId> --view-id <viewId> --new-name "<名>"` | `viewId` 必须属于当前表;不能用复制 Table 或新建 Dashboard 替代 |
|
||||
| 创建仪表盘 / 常用图表 | `python3 <本 Skill 绝对目录>/scripts/create_dashboard_chart.py <baseId> "<仪表盘名>" [--chart-specs <workspace内JSON>]` | 这是 dashboard/chart 创建的唯一首选 recipe;脚本创建、串联真实 ID、最终回读并输出可评分 ledger。图表 JSON 参数见对应 reference |
|
||||
| 复制视图 | `dws aitable view duplicate --base-id <baseId> --table-id <tableId> --view-id <源viewId> --new-name "<新名称>" --format json` | 源 viewId 来自当前表的真实返回;不要复制数据表或创建仪表盘替代 |
|
||||
| 批量追加 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;逐项检查成功/失败结果 |
|
||||
@@ -78,22 +77,6 @@ metadata:
|
||||
- 创建、更新、导入、批量建字段等写操作必须检查业务 `status`、逐项结果与返回 ID;普通写入按用户明确要求执行后回读,不能只凭退出码宣称成功。
|
||||
- 长 JSON 使用 `--records-file` / 任务文件;不得为绕过字段错误而静默丢列、改类型或删除失败项。
|
||||
|
||||
## 写入计划与验证
|
||||
|
||||
- 用户明确列出的“先创建、再加字段、然后写记录/建视图”等阶段是可观察的验收步骤,必须逐项真实执行;不能为了得到相似终态而折叠、重排或省略。
|
||||
- 删除 Base/Table/Field/Record、关闭高级权限、删除角色和其他高风险动作,先固化目标 ID、所属容器、影响数量、副作用与可恢复性;确认前写调用为零。
|
||||
- 批量导入、建字段和记录写入在第一笔写入前完成全部字段类型、只读性、关联表和文件边界校验;部分失败保留输入顺序与逐项 ledger。
|
||||
|
||||
| 写入对象 | 成功后必须验证 |
|
||||
|---|---|
|
||||
| Base / Table | 使用创建返回 ID 查询对象及所属关系 |
|
||||
| Field | `+field-get` 核对 fieldId、type、config 与只读性 |
|
||||
| Record | 使用返回 recordId 按 ID 回读目标 cells |
|
||||
| View / Dashboard / Chart | 重新读取当前 Base/Table 下的对象配置 |
|
||||
| 导入 / 导出任务 | 核对 task 状态、结果对象或输出文件完整性 |
|
||||
|
||||
退出码 0、`status=success`、空对象或仅有 taskId 都不能单独证明业务完成。字段未生效、回读不一致、分页不完整或异步任务未完成时,报告失败、部分完成或进行中,不得声称“全部完成”。
|
||||
|
||||
## 低频能力与 Reference
|
||||
|
||||
| 场景 | 按需读取 |
|
||||
@@ -111,7 +94,7 @@ metadata:
|
||||
- 路径或 flag 错误:按既定的 leaf Schema → leaf Help 顺序校正一次;仍失败则停止,不连续尝试猜测别名。
|
||||
- 命令非零、输出非 JSON、业务 `status != success`、必需 ID 缺失、批处理部分失败均视为失败;保留成功项与 ledger,禁止吞错。
|
||||
- 同名歧义、权限不足、资源不存在、字段类型漂移、分页无法推进或 Schema/Help 冲突时停止并报告。具体恢复动作按需读 [aitable-error-recovery.md](references/aitable/aitable-error-recovery.md)。
|
||||
- 已确认远端未写入且契约声明可重试时,才从最新实际输出重新提取 ID 后重试;写入状态未知、删除和其他 `confirmation=user_required` 操作不得自动重试或静默确认。
|
||||
- 每次重试都从最新实际输出重新提取下游 ID;删除和其他 `confirmation=user_required` 操作不得自动重试或静默确认。
|
||||
|
||||
## 跨产品协作
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# 数据分析
|
||||
|
||||
> 定位:仅在用户要求跨记录统计、聚合或业务结论时加载。普通查找、筛选和分页先按根 Skill 的 `+record-query` 骨架执行;完整分析契约以 [aitable-data-analysis-sop.md](aitable/aitable-data-analysis-sop.md) 为准,本页只保留轻量入口。
|
||||
|
||||
> 本场景所有 recipe 均为 full。
|
||||
|
||||
| Recipe | 行动指南(固定路线) |
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# 业务域通用规范
|
||||
|
||||
> 定位:供 AITable 主题 Reference 复用的批量、并行采集和 ID 传递约定,不是用户意图路由入口。根 Skill 已明确的高频任务不需要单独加载本页。
|
||||
|
||||
> 仅服务本 skill 已迁入的行动指南。安全门控、危险操作确认、`--format json` 等已在本 skill 的 `SKILL.md` 中定义,此处不重复。
|
||||
|
||||
## 批量查询规范
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# 记录操作详细指南
|
||||
|
||||
> 定位:兼容性的记录操作总览。新任务优先按需读取 `aitable/aitable-record-{query,create,update,delete,upsert}.md` 中唯一对应的叶级 Reference;不要同时加载本页和全部叶级文件。本页不得作为另一套命令发现源。
|
||||
|
||||
## 查询记录
|
||||
|
||||
```bash
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
# AI表格 (aitable) 命令参考
|
||||
|
||||
> 定位:低频 atomic 能力的一级索引,不是每次调用必读的产品手册。已知高频意图直接回到 [`SKILL.md`](../SKILL.md);参数和安全语义查 leaf Schema,真实 flags 查 leaf Help;本页只在根路由无法定位能力时用于选择一个主题 Reference,禁止递归加载全部链接。
|
||||
> **渐进式文档**:本文件为路由层(索引 + 意图判断),各命令的详细参数、示例和踩坑说明在 [aitable/](./aitable/) 目录下按需加载。
|
||||
|
||||
## 使用本索引
|
||||
|
||||
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/) 目录下按需加载。
|
||||
已知高频意图优先使用根 Skill 的精确 Shortcut/脚本骨架;本文件只在需要完整一级命令索引、对象 URL 或低频分支导航时加载。参数与安全不确定时读 leaf Schema,Cobra flag 不确定时才读 leaf Help,不要把本文件当作参数事实源。
|
||||
|
||||
## 文档地址 (URI)
|
||||
|
||||
@@ -338,3 +331,162 @@ 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,7 +1,5 @@
|
||||
# advperm — 高级权限管理
|
||||
|
||||
> 加载边界:仅在开启/关闭高级权限或管理自定义角色时读取。普通协作者、文件权限或成员管理不走本页。`baseId` 和 `roleId` 必须来自当前链路;disable、role-delete 等需确认的动作先读现状、固化影响,再执行并回读。
|
||||
|
||||
控制 Base 的高级权限总开关,并管理自定义角色(增删改查 + 子角色权限规则)。
|
||||
适用场景:"如何控制谁能看/改 Base 数据"、"开启/关闭高级权限"、"新建/修改/删除角色"、"按字段或行配置权限"。
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# 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,7 +1,5 @@
|
||||
# AI 表格最佳实践
|
||||
|
||||
> 定位:跨主题的不变量摘要,用于复杂任务复核,不作为命令索引。若根 Skill 已给出明确骨架,不要为了普通读写预加载本页;具体字段值、过滤或错误恢复分别读取对应专题。
|
||||
|
||||
## 1. 字段可写性分类
|
||||
|
||||
| 字段类型 | 可写 | 正确方式 |
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# 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,20 +1,43 @@
|
||||
# dashboard & chart — 仪表盘与图表
|
||||
|
||||
> 加载边界:仅在用户明确操作仪表盘或图表时读取。先用当前 Base 的 dashboard list/get 获取 `dashboardId`,再获取其 `chartId`;不要用 View 或 Table 命令替代,也不要跨 Dashboard 复用 chartId。
|
||||
|
||||
## 建议操作顺序
|
||||
## 创建首选流程
|
||||
|
||||
```bash
|
||||
# 1) 先看配置模板(JSONC)
|
||||
dws aitable dashboard config-example --format json
|
||||
dws aitable chart widgets-example --format json
|
||||
# 仅建仪表盘
|
||||
python3 <本 Skill 绝对目录>/scripts/create_dashboard_chart.py <BASE_ID> "<仪表盘名>"
|
||||
|
||||
# 2) 先拿 dashboard,再拿 chart 详情
|
||||
dws aitable dashboard get --base-id <BASE_ID> --dashboard-id <DASHBOARD_ID> --format json
|
||||
dws aitable chart get --base-id <BASE_ID> --dashboard-id <DASHBOARD_ID> --chart-id <CHART_ID> --format json
|
||||
# 建仪表盘和常用图表
|
||||
python3 <本 Skill 绝对目录>/scripts/create_dashboard_chart.py <BASE_ID> "<仪表盘名>" \
|
||||
--chart-specs <workspace内/charts.json>
|
||||
```
|
||||
|
||||
## 要点
|
||||
脚本是 `dashboard create → chart create(可选)→ dashboard get` 的唯一首选
|
||||
recipe,并输出 `dws-skill-script-ledger/v1`。不要在脚本前调用 config-example 或
|
||||
widgets-example,也不要在成功后重复创建或回读。
|
||||
|
||||
`charts.json` 是 1–6 项数组,每项参数:
|
||||
|
||||
| 参数 | 要求 |
|
||||
|---|---|
|
||||
| `name` | 必填,图表名 |
|
||||
| `chart_type` | 必填:`AREA`、`BAR`、`HISTOGRAM`、`LINE`、`PIE`、`STATISTICS` |
|
||||
| `table_id` | 必填,当前链路的真实 tableId |
|
||||
| `measure_type` | `record-count`(默认)或 `field` |
|
||||
| `measure_field_id` | `measure_type=field` 时必填 |
|
||||
| `dimension_field_id` | 分组、分类或时间维度需要时填写 |
|
||||
| `aggregation` | 可选:`sum`、`count`、`count_distinct`、`average`、`min`、`max` |
|
||||
| `view_id` | 可选;不用视图时省略 |
|
||||
|
||||
例如按状态统计记录数:
|
||||
|
||||
```json
|
||||
[{"name":"跟进状态记录数","chart_type":"HISTOGRAM","table_id":"<tableId>","measure_type":"record-count","dimension_field_id":"<状态fieldId>"}]
|
||||
```
|
||||
|
||||
脚本不支持的图表类型或完整高级配置才走下方原子命令;这种例外至多读取一次
|
||||
`chart widgets-example`,再按真实 tableId/fieldId 构造配置。
|
||||
|
||||
## 查询与管理要点
|
||||
|
||||
- `dashboard get` 返回的 `charts[].chartId` 可直接给 `chart get` 使用
|
||||
- `dashboard share get` 可能返回 `404`(资源不存在或未开通),需按可重试错误处理,不要误判为参数拼错
|
||||
@@ -28,7 +51,7 @@ dws aitable chart get --base-id <BASE_ID> --dashboard-id <DASHBOARD_ID> --chart-
|
||||
| `dashboard create` | 创建仪表盘 | `--base-id` + (`--config` 或 `--name`) | `--name` 简化版创建空看板;`--config` 传完整 JSON |
|
||||
| `dashboard update` | 更新仪表盘 | `--base-id` `--dashboard-id` + (`--config` 或 `--name`) | `--name` 仅改名;`--config` 更新完整配置 |
|
||||
| `dashboard delete` | 删除仪表盘 | `--base-id` `--dashboard-id` `--yes` | — |
|
||||
| `dashboard config-example` | 查看仪表盘配置模板 | 无 | 创建前先调此命令了解 config 结构 |
|
||||
| `dashboard config-example` | 查看仪表盘配置模板 | 无 | 仅脚本不支持的高级配置按需读取一次 |
|
||||
| `dashboard arrange` | 自动重排图表布局 | `--base-id` `--dashboard-id` | 把图表按行铺满网格,避免某行只占半幅、留下大片空白;返回 `{totalColumns, layout, alignedChartCount}` |
|
||||
|
||||
## chart 子命令
|
||||
@@ -36,11 +59,10 @@ dws aitable chart get --base-id <BASE_ID> --dashboard-id <DASHBOARD_ID> --chart-
|
||||
| 命令 | 用途 | 必填参数 |
|
||||
|------|------|----------|
|
||||
| `chart get` | 获取图表详情 | `--base-id` `--dashboard-id` `--chart-id` |
|
||||
| `chart create` | 创建图表 | `--base-id` `--dashboard-id` `--config` |
|
||||
| `chart create` | 创建图表 | `--base-id` `--dashboard-id` `--config` `--layout` |
|
||||
| `chart update` | 更新图表配置 | `--base-id` `--dashboard-id` `--chart-id` `--config` |
|
||||
| `chart delete` | 删除图表 | `--base-id` `--dashboard-id` `--chart-id` `--yes` |
|
||||
| `chart widgets-example` | 查看图表 widgets 配置模板 | 无 |
|
||||
| `chart widgets-example` | 查看图表 widgets 配置模板 | 无;返回很大 | 仅脚本不支持的高级图表按需读取一次 |
|
||||
|
||||
## 配置获取流程
|
||||
|
||||
创建图表前,必须先调用 `chart widgets-example` 查看配置模板,了解每种图表类型需要的字段结构,然后根据实际 tableId 和 fieldId 填充配置。
|
||||
原子 `chart create` 必须同时传 `--layout`。返回 chartId 后用 `chart get`,或最后
|
||||
一次 `dashboard get` 核对;回读成功即停止。脚本已自动完成这些动作,不再重复执行。
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# AI 表格数据分析 SOP
|
||||
|
||||
> 加载边界:用于需要全表统计、分组、排名或业务结论的任务。普通记录定位只读 record-query 专题。任何“全部/最高/总数”结论都必须携带分页完整性;未拉全时只能报告当前范围和 continuation。
|
||||
|
||||
> 当用户诉求涉及查询、筛选、排序、统计、Top/Bottom N、分组聚合、判断全局结论时,必须先读本文档再执行。
|
||||
|
||||
## 1. 查询决策树
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# AI 表格错误恢复指南
|
||||
|
||||
> 加载边界:仅在真实命令或业务返回失败后读取,不做预防性全量加载。先保留原错误、阶段、对象 ID 和已成功 ledger;只有能证明未写入且契约允许时才重试,状态未知和需确认写操作禁止自动重放。
|
||||
|
||||
> 当 CLI 命令返回错误时,按本文档的映射表判断恢复动作。
|
||||
|
||||
## 1. 错误响应结构
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# export & import — 导入导出
|
||||
|
||||
> 加载边界:仅在文件与 AITable 之间迁移数据时读取。“追加到已有 Table”与“导入成新 Table”必须先分流。`taskId/importId` 只用于对应任务状态,完成后必须验证新 tableId 或本地输出文件;任务已创建不等于业务完成。
|
||||
|
||||
## 导出数据(两阶段轮询)
|
||||
|
||||
`export data` 为异步任务:首次调用可能只返回 `taskId`,需要继续轮询。
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# 字段类型 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,7 +1,5 @@
|
||||
# field — 字段管理
|
||||
|
||||
> 加载边界:仅在字段目录不足、需要完整配置或执行字段 CRUD 时读取。`baseId/tableId` 先由根 Skill 解析;写前校验类型、config、关联表和只读性,写后用 field get 回读。删除字段先固化影响并确认。
|
||||
|
||||
## field get — 获取字段详情
|
||||
|
||||
```
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# 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,7 +1,5 @@
|
||||
# form — 表单管理
|
||||
|
||||
> 加载边界:仅在用户明确创建、配置或分享表单视图时读取。先绑定当前 Base/Table,再从创建/list 返回取得 viewId 和 fieldId;表单分享不等于记录分享,也不等于开放 Base 权限。
|
||||
|
||||
## 命令一览
|
||||
|
||||
| 命令 | 用途 |
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# AI 表格公式字段指南
|
||||
|
||||
> 加载边界:仅在创建或修改 formula 字段时读取。公式字段是只读派生字段,不能通过 record create/update 写值;字段引用必须基于当前 Table 的精确名称/ID,跨表取值应先分流到 lookup/filterUp。
|
||||
|
||||
> 当用户要创建 formula 类型字段、编写表内计算公式、做派生指标时,必须先读本文档。
|
||||
|
||||
## 1. 何时使用 formula 字段
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# 主键文档管理
|
||||
|
||||
> 加载边界:仅在记录的 primaryDoc 字段需要查询或创建关联文档时读取。先取得真实 baseId/tableId/recordId/fieldId;创建返回 nodeId 后,文档正文交给 Doc Skill,并持续使用该 nodeId,不再按标题搜索。
|
||||
|
||||
## 适用场景
|
||||
|
||||
当需要为 AI 表格中的记录创建或查询关联的主键文档时使用。主键文档是 primaryDoc 类型字段对应的钉钉在线文档,可通过 `dws doc` 进行内容读写。
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# record create — 新增记录
|
||||
|
||||
> 加载边界:仅在目标 Base/Table 已唯一确定且字段目录已取得后读取。第一笔写入前校验全部 cells;创建返回 `newRecordIds[]` 后按 ID 回读。不得因部分字段失败而静默丢列或改类型。
|
||||
|
||||
## 命令格式
|
||||
|
||||
```
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# record delete — 删除记录
|
||||
|
||||
> 加载边界:仅在用户明确要求删除记录时读取。先 query 并展示真实 recordId、关键字段和数量,确认前零删除调用;删除后重新按 ID 查询验证不存在,状态未知时禁止自动重试。
|
||||
|
||||
## 命令格式
|
||||
|
||||
```
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# 行记录变更历史(record history-list)
|
||||
|
||||
> 加载边界:仅在审计某条真实 recordId 的历史变更时读取。历史分页与当前记录查询是不同契约;未遍历完成不得声称“全部历史”,历史事件也不能直接当作当前 cells 状态。
|
||||
|
||||
按 recordId 查询单条记录的全部变更历史,用于审计、回溯字段变更、定位操作人。
|
||||
|
||||
## 命令
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# 行命名规则枚举键(recordNameKey)映射
|
||||
|
||||
> 加载边界:仅在配置 Table 的行称谓时读取。自然语言名称必须映射为已审阅枚举键;本页不用于记录查找、字段名解析或写入 cells。
|
||||
|
||||
`dws aitable table update --record-name-key <枚举键>` 用于设置数据表的"行命名规则"——卡片/详情页里"行"的展示别名。**取值是固定枚举,不是字段 ID**;传非法值服务端返回 `INVALID_RECORD_NAME_KEY`。
|
||||
|
||||
## 中文 → 枚举键(按 UI 下拉顺序)
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# record query — 查询记录
|
||||
|
||||
> 加载边界:用于目标 Base/Table 已确定后的记录查询、筛选、排序和分页。按 ID、关键词和 filters 三种模式先分流;全量结论必须检查 `--all`、page-limit、hasMore/nextCursor 和 stop reason,默认一页只能代表当前页。
|
||||
|
||||
## 命令格式
|
||||
|
||||
```
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# 行记录分享链接(record share-url)
|
||||
|
||||
> 加载边界:仅在用户需要某条真实记录的分享链接时读取。recordId 必须属于当前 Base/Table;可选 viewId 也必须同表。生成链接不等于修改记录权限或把链接发送给他人。
|
||||
|
||||
按 recordId 批量获取记录的分享链接,把某行单独发给同事查看。
|
||||
|
||||
## 命令
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# record update — 更新记录
|
||||
|
||||
> 加载边界:仅在 recordId 和待修改 fieldId 已确定后读取。只提交用户要求变更的 cells;写前读取字段类型/只读性,写后使用返回 recordId 回读。未返回 ID、字段未变化或状态未知都不能声称成功。
|
||||
|
||||
## 命令格式
|
||||
|
||||
```
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# 行记录 Upsert(record upsert)
|
||||
|
||||
> 加载边界:仅在同一批次明确混合“带 recordId 更新”和“不带 recordId 创建”时读取。若目标匹配逻辑仍依赖名称或业务键,先 query 并消歧;upsert 不负责猜测 recordId,部分成功必须返回逐项 ledger。
|
||||
|
||||
按 `recordId` 是否存在,自动把入参拆分到 update 链路或 create 链路:批次混合"已存在改 + 新出现建"时用,省掉客户端按 ID 分批的逻辑。
|
||||
|
||||
## 命令
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# 视图配置(view get/update <attr>)
|
||||
|
||||
> 加载边界:仅在创建视图或读取/修改视图配置时加载。先确认 viewType 与属性支持矩阵,并绑定当前 Base/Table/viewId;写后读取同一属性验证。复制、锁定、行高和高亮规则改读 [aitable-view-extras.md](aitable-view-extras.md)。
|
||||
|
||||
按属性局部读/写视图配置。每个属性独立子命令,typed flag 友好,agent 不必拼 JSON。
|
||||
向后兼容:`view update --config '{...}'` 一次多属性入口仍可用。
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# 视图扩展操作(lock / frozen-cols / row-height / fill-color-rule / duplicate)
|
||||
|
||||
> 加载边界:仅在用户明确操作锁定、冻结列、行高、高亮规则或复制视图时读取。所有动作复用当前 Base/Table/viewId;viewType 不支持时本地停止,写后用对应 get 或新 viewId 回读。
|
||||
|
||||
本文档讲 5 项视图操作命令:
|
||||
|
||||
- 锁定 / 解锁视图:`view lock` / `view get lock`
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# workflow — 自动化工作流管理
|
||||
|
||||
> 加载边界:仅在创建、全量更新、启停或检查 AITable 自动化工作流时读取。create/update 使用完整 DSL,先校验并保留现状;disable 等需确认动作确认前零写入,返回 flowId 后用 get/list 验证 `valid/issues/status`。
|
||||
|
||||
创建 / 更新 / 启停 / 查看 / 列出 Base 下的自动化工作流("当 X 时自动 Y" 流程)。
|
||||
适用场景:用户要求创建自动化、修改流程、停掉流程、查询已有流程或恢复运行。
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# 易混淆操作与字段规则
|
||||
|
||||
> 定位:字段操作的轻量防错入口,仅在建表/建字段前快速核对主字段、只读字段和附件边界。完整 config 读取 `aitable/aitable-field-properties.md`,cellValue 读取 `aitable/aitable-cell-value.md`;不要与两份专题同时全量加载。
|
||||
|
||||
## 易混淆操作 (高风险场景必读)
|
||||
|
||||
| 用户说的 | 正确命令 | 不是这个 |
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# aitable 局部意图消歧
|
||||
|
||||
> 定位:只处理 AITable 与 Sheet、Doc、Drive、Minutes 等 sibling 产品的边界;产品内 Base/Table/Field/Record 路由由根 Skill 负责。用户意图已经明确属于 AITable 时无需加载本页。
|
||||
|
||||
本文件从单 Skill `intent-guide.md` 拆分而来,仅保留与本产品相关的跨产品消歧规则。
|
||||
|
||||
| 用户说... | 真实意图 | 应该用 | 不要用 | 理由 |
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# URL 格式与处理规范
|
||||
|
||||
> 加载边界:仅在用户给出类型不明的 alidocs URL 且意图不足以直接分流时读取。明确的 AITable 操作优先按意图执行;探测只负责确定资源类型,不应成为每次调用前置步骤,也不能把短链或裸 key 猜成 baseId。
|
||||
|
||||
## 路由第 0 步:意图直达(优先级高于 URL 探测)
|
||||
|
||||
用户已经明确表达某产品的内容意图时,直接进入对应产品场域,不要先做 URL 类型
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Create an AI Table dashboard and optional common charts deterministically.
|
||||
|
||||
Examples:
|
||||
python3 create_dashboard_chart.py BASE_ID "状态分析仪表盘"
|
||||
python3 create_dashboard_chart.py BASE_ID "状态分析仪表盘" --chart-specs charts.json
|
||||
|
||||
charts.json is a JSON array. Each item accepts:
|
||||
name, chart_type, table_id, measure_type, measure_field_id,
|
||||
dimension_field_id, aggregation, view_id.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
LEDGER_SCHEMA_VERSION = "dws-skill-script-ledger/v1"
|
||||
SCRIPT_NAME = "create_dashboard_chart.py"
|
||||
RESOURCE_ID_PATTERN = re.compile(r"^[A-Za-z0-9_-]{3,128}$")
|
||||
SUPPORTED_CHART_TYPES = {"AREA", "BAR", "HISTOGRAM", "LINE", "PIE", "STATISTICS"}
|
||||
SUPPORTED_AGGREGATIONS = {"sum", "count", "count_distinct", "average", "avg", "min", "max"}
|
||||
MAX_CHARTS = 6
|
||||
|
||||
|
||||
def run_dws(dws_bin: str, args: list[str]) -> tuple[Optional[dict[str, Any]], str]:
|
||||
try:
|
||||
completed = subprocess.run(
|
||||
[dws_bin, *args], capture_output=True, text=True, timeout=120
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
return None, "dws command timeout after 120 seconds"
|
||||
except FileNotFoundError:
|
||||
return None, f"dws binary not found: {dws_bin}"
|
||||
if completed.returncode != 0:
|
||||
return None, (completed.stderr or completed.stdout).strip()[:800]
|
||||
try:
|
||||
payload = json.loads(completed.stdout)
|
||||
except json.JSONDecodeError as exc:
|
||||
return None, f"dws returned non-JSON output: {exc}"
|
||||
if not isinstance(payload, dict) or payload.get("status") != "success":
|
||||
return None, f"dws business failure: {payload}"
|
||||
return payload, ""
|
||||
|
||||
|
||||
def safe_json_file(value: str) -> Any:
|
||||
root = Path(os.environ.get("OPENCLAW_WORKSPACE", os.getcwd())).resolve()
|
||||
source = Path(value).expanduser()
|
||||
source = source.resolve() if source.is_absolute() else (Path.cwd() / source).resolve()
|
||||
try:
|
||||
source.relative_to(root)
|
||||
except ValueError as exc:
|
||||
raise ValueError(f"chart specs must be inside the workspace: {source}") from exc
|
||||
if not source.is_file() or source.stat().st_size > 1024 * 1024:
|
||||
raise ValueError("chart specs must be a readable JSON file no larger than 1 MiB")
|
||||
with source.open("r", encoding="utf-8") as stream:
|
||||
return json.load(stream)
|
||||
|
||||
|
||||
def validate_specs(value: Any) -> list[dict[str, Any]]:
|
||||
if not isinstance(value, list) or not value or len(value) > MAX_CHARTS:
|
||||
raise ValueError(f"chart specs must contain 1-{MAX_CHARTS} items")
|
||||
specs: list[dict[str, Any]] = []
|
||||
for index, item in enumerate(value, start=1):
|
||||
if not isinstance(item, dict):
|
||||
raise ValueError(f"chart spec #{index} must be an object")
|
||||
name = str(item.get("name") or "").strip()
|
||||
chart_type = str(item.get("chart_type") or "").strip().upper()
|
||||
table_id = str(item.get("table_id") or "").strip()
|
||||
measure_type = str(item.get("measure_type") or "record-count").strip()
|
||||
measure_field_id = str(item.get("measure_field_id") or "").strip()
|
||||
dimension_field_id = str(item.get("dimension_field_id") or "").strip()
|
||||
aggregation = str(item.get("aggregation") or "sum").strip().lower()
|
||||
view_id = str(item.get("view_id") or "").strip()
|
||||
if not name or len(name) > 80:
|
||||
raise ValueError(f"chart spec #{index} needs a 1-80 character name")
|
||||
if chart_type not in SUPPORTED_CHART_TYPES:
|
||||
raise ValueError(f"chart spec #{index} has unsupported chart_type: {chart_type}")
|
||||
if not RESOURCE_ID_PATTERN.fullmatch(table_id):
|
||||
raise ValueError(f"chart spec #{index} has invalid table_id")
|
||||
if measure_type not in {"record-count", "field"}:
|
||||
raise ValueError(f"chart spec #{index} has invalid measure_type")
|
||||
if measure_type == "field" and not RESOURCE_ID_PATTERN.fullmatch(measure_field_id):
|
||||
raise ValueError(f"chart spec #{index} needs measure_field_id")
|
||||
if aggregation not in SUPPORTED_AGGREGATIONS:
|
||||
raise ValueError(f"chart spec #{index} has unsupported aggregation")
|
||||
for label, resource_id in (
|
||||
("dimension_field_id", dimension_field_id), ("view_id", view_id)
|
||||
):
|
||||
if resource_id and not RESOURCE_ID_PATTERN.fullmatch(resource_id):
|
||||
raise ValueError(f"chart spec #{index} has invalid {label}")
|
||||
specs.append(
|
||||
{
|
||||
"name": name,
|
||||
"chart_type": chart_type,
|
||||
"table_id": table_id,
|
||||
"measure_type": measure_type,
|
||||
"measure_field_id": measure_field_id,
|
||||
"dimension_field_id": dimension_field_id,
|
||||
"aggregation": "average" if aggregation == "avg" else aggregation,
|
||||
"view_id": view_id,
|
||||
}
|
||||
)
|
||||
return specs
|
||||
|
||||
|
||||
def chart_config(spec: dict[str, Any]) -> dict[str, Any]:
|
||||
config: dict[str, Any] = {
|
||||
"chartType": spec["chart_type"],
|
||||
"name": spec["name"],
|
||||
"sheet": spec["table_id"],
|
||||
"view": spec["view_id"] or None,
|
||||
"measureType": spec["measure_type"],
|
||||
"measure": [],
|
||||
"filter": [],
|
||||
}
|
||||
if spec["measure_type"] == "field":
|
||||
config["measure"] = [
|
||||
{
|
||||
"value": spec["measure_field_id"],
|
||||
"externalValue": [{"type": "formula", "value": spec["aggregation"]}],
|
||||
}
|
||||
]
|
||||
if spec["dimension_field_id"]:
|
||||
config["dimension"] = [
|
||||
{"value": spec["dimension_field_id"], "externalValue": []}
|
||||
]
|
||||
if spec["chart_type"] in {"AREA", "BAR", "HISTOGRAM", "LINE", "PIE"}:
|
||||
config.update({"colors": "COLOR_PALETTE_1", "legend": "top", "label": True})
|
||||
if spec["chart_type"] in {"AREA", "BAR", "HISTOGRAM", "LINE"}:
|
||||
config.update({"xAxisShow": True, "yAxisShow": True})
|
||||
if spec["chart_type"] == "PIE":
|
||||
config.update({"innerRadius": 0, "outerRadius": 60})
|
||||
return config
|
||||
|
||||
|
||||
def layout(index: int, total: int) -> dict[str, int]:
|
||||
width = 12 if total == 1 else 6 if total in {2, 3, 4} else 4
|
||||
per_row = 12 // width
|
||||
return {"x": (index % per_row) * width, "y": (index // per_row) * 5, "w": width, "h": 5}
|
||||
|
||||
|
||||
def extract_id(payload: dict[str, Any], key: str) -> str:
|
||||
data = payload.get("data") if isinstance(payload.get("data"), dict) else {}
|
||||
value = data.get(key)
|
||||
return str(value or "").strip()
|
||||
|
||||
|
||||
def dashboard_chart_ids(payload: dict[str, Any]) -> set[str]:
|
||||
data = payload.get("data") if isinstance(payload.get("data"), dict) else {}
|
||||
charts = data.get("charts") if isinstance(data.get("charts"), list) else []
|
||||
return {
|
||||
str(item.get("chartId"))
|
||||
for item in charts
|
||||
if isinstance(item, dict) and item.get("chartId")
|
||||
}
|
||||
|
||||
|
||||
def ledger_step(
|
||||
cli_path: str, status: str, params: dict[str, Any], output_ids: Optional[dict[str, str]] = None,
|
||||
error: str = "",
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"cli_path": cli_path,
|
||||
"status": status,
|
||||
"params": params,
|
||||
"output_ids": output_ids or {},
|
||||
"error": error,
|
||||
}
|
||||
|
||||
|
||||
def emit(status: str, ledger: list[dict[str, Any]], **result: Any) -> None:
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"schema_version": LEDGER_SCHEMA_VERSION,
|
||||
"script": SCRIPT_NAME,
|
||||
"status": status,
|
||||
"result": result,
|
||||
"ledger": ledger,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("base_id", help="Target AI Table baseId")
|
||||
parser.add_argument("dashboard_name", help="Dashboard name")
|
||||
parser.add_argument("--chart-specs", help="Workspace-local JSON chart spec file")
|
||||
parser.add_argument("--dws", default="dws", help="dws executable")
|
||||
args = parser.parse_args()
|
||||
|
||||
base_id = args.base_id.strip()
|
||||
dashboard_name = args.dashboard_name.strip()
|
||||
if not RESOURCE_ID_PATTERN.fullmatch(base_id) or not dashboard_name:
|
||||
parser.error("base_id and dashboard_name are required and must be valid")
|
||||
try:
|
||||
specs = validate_specs(safe_json_file(args.chart_specs)) if args.chart_specs else []
|
||||
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
||||
parser.error(str(exc))
|
||||
|
||||
ledger: list[dict[str, Any]] = []
|
||||
dashboard_params = {"base-id": base_id, "name": dashboard_name, "format": "json"}
|
||||
dashboard, error = run_dws(
|
||||
args.dws,
|
||||
["aitable", "dashboard", "create", "--base-id", base_id, "--name", dashboard_name, "--format", "json"],
|
||||
)
|
||||
if not dashboard:
|
||||
ledger.append(ledger_step("aitable dashboard create", "failed", dashboard_params, error=error))
|
||||
emit("failed", ledger, error=error)
|
||||
return 1
|
||||
dashboard_id = extract_id(dashboard, "dashboardId")
|
||||
if not dashboard_id:
|
||||
error = "dashboard create returned no dashboardId"
|
||||
ledger.append(ledger_step("aitable dashboard create", "failed", dashboard_params, error=error))
|
||||
emit("failed", ledger, error=error)
|
||||
return 1
|
||||
ledger.append(
|
||||
ledger_step(
|
||||
"aitable dashboard create", "success", dashboard_params,
|
||||
{"dashboardId": dashboard_id},
|
||||
)
|
||||
)
|
||||
|
||||
chart_ids: list[str] = []
|
||||
for index, spec in enumerate(specs):
|
||||
config = chart_config(spec)
|
||||
chart_layout = layout(index, len(specs))
|
||||
params = {
|
||||
"base-id": base_id,
|
||||
"dashboard-id": dashboard_id,
|
||||
"config": config,
|
||||
"layout": chart_layout,
|
||||
"format": "json",
|
||||
}
|
||||
chart, error = run_dws(
|
||||
args.dws,
|
||||
[
|
||||
"aitable", "chart", "create", "--base-id", base_id,
|
||||
"--dashboard-id", dashboard_id,
|
||||
"--config", json.dumps(config, ensure_ascii=False, separators=(",", ":")),
|
||||
"--layout", json.dumps(chart_layout, separators=(",", ":")),
|
||||
"--format", "json",
|
||||
],
|
||||
)
|
||||
if not chart:
|
||||
ledger.append(ledger_step("aitable chart create", "failed", params, error=error))
|
||||
emit("failed", ledger, dashboardId=dashboard_id, chartIds=chart_ids, error=error)
|
||||
return 1
|
||||
chart_id = extract_id(chart, "chartId")
|
||||
if not chart_id:
|
||||
error = "chart create returned no chartId"
|
||||
ledger.append(ledger_step("aitable chart create", "failed", params, error=error))
|
||||
emit("failed", ledger, dashboardId=dashboard_id, chartIds=chart_ids, error=error)
|
||||
return 1
|
||||
chart_ids.append(chart_id)
|
||||
ledger.append(
|
||||
ledger_step("aitable chart create", "success", params, {"chartId": chart_id})
|
||||
)
|
||||
|
||||
get_params = {"base-id": base_id, "dashboard-id": dashboard_id, "format": "json"}
|
||||
verified, error = run_dws(
|
||||
args.dws,
|
||||
["aitable", "dashboard", "get", "--base-id", base_id, "--dashboard-id", dashboard_id, "--format", "json"],
|
||||
)
|
||||
if not verified:
|
||||
ledger.append(ledger_step("aitable dashboard get", "failed", get_params, error=error))
|
||||
emit("failed", ledger, dashboardId=dashboard_id, chartIds=chart_ids, error=error)
|
||||
return 1
|
||||
missing_chart_ids = set(chart_ids) - dashboard_chart_ids(verified)
|
||||
if missing_chart_ids:
|
||||
error = "dashboard verification missing chartIds: " + ",".join(sorted(missing_chart_ids))
|
||||
ledger.append(ledger_step("aitable dashboard get", "failed", get_params, error=error))
|
||||
emit("failed", ledger, dashboardId=dashboard_id, chartIds=chart_ids, error=error)
|
||||
return 1
|
||||
ledger.append(
|
||||
ledger_step("aitable dashboard get", "success", get_params, {"dashboardId": dashboard_id})
|
||||
)
|
||||
emit("success", ledger, dashboardId=dashboard_id, chartIds=chart_ids)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -72,6 +72,8 @@ 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,7 +58,9 @@ dws doc read --node <nodeId> # 校验关键标题、段落首句、表格、@
|
||||
|
||||
**禁止**在未回读的情况下向用户报告「已完成」。
|
||||
|
||||
## 显式工作流
|
||||
## 显式工作流与事实保真
|
||||
|
||||
- 用户点名的 `create → list → insert/append/update` 是可观察命令链,必须保持顺序逐项执行;create 只承载明确的初始正文。有序列表块必须验证回读结构中的 `list.isOrdered=true`。
|
||||
- 新建资源返回 ID 后,同一请求的指代默认绑定该新资源;禁止搜索同名旧资源替换绑定。
|
||||
- 汇总用户材料时保留证据强度:验证数量不等于通过数量,计划整理问题不等于承诺根因分析。不得为“更专业”而补造结论、状态或任务。
|
||||
- 任一步骤返回 `null`/空结果或回查不一致时,该步骤未完成;最终按步骤报告成功与失败,不能用其他成功步骤把整体描述成“全部完成”。
|
||||
|
||||
@@ -132,6 +132,7 @@ 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,6 +60,8 @@ 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}")
|
||||
@@ -145,12 +147,14 @@ def run(argv: Optional[Sequence[str]] = None) -> int:
|
||||
["doc", "info", "--node", node_id, "--format", "json"],
|
||||
dry_run=args.dry_run,
|
||||
)
|
||||
run_dws(
|
||||
readback = 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 append --content-file <tmp.md>`;只有用户确认整篇覆盖后才用 `dws doc update --node <nodeId> --mode overwrite --content-file <tmp.md> --yes`。写后都要 `doc read` 回读。
|
||||
2. **写内容(必须)**:节点内容编辑切 `dingtalk-doc`,用 `dws doc update --node <nodeId> --mode overwrite|append --content-file <tmp.md> --yes`;写后 `doc read` 回读。
|
||||
3. **验证(必须)**:`dws wiki node list --workspace <workspaceId> --format json` 复核节点已建。
|
||||
|
||||
**禁止**:在 wiki 内直接拼内容(应切 doc 写)、建后不回读。
|
||||
|
||||
@@ -96,7 +96,7 @@ class DocSkillAlignmentTest(unittest.TestCase):
|
||||
self.assertIn("按“先/再/然后”切分操作阶段", skill)
|
||||
self.assertLessEqual(len(skill.encode("utf-8")), 9500)
|
||||
|
||||
def test_workflow_and_identity_rules_are_explicit(self):
|
||||
def test_workflow_identity_and_fidelity_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,11 +119,22 @@ 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(
|
||||
@@ -150,6 +161,10 @@ 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"]),
|
||||
@@ -213,6 +228,16 @@ 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 = []
|
||||
|
||||
@@ -254,6 +279,19 @@ 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