From 31cec9efa71be8d72fbb5035d7271a1c1c2dd057 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=91=9E=E8=BE=BE?= Date: Wed, 5 Aug 2026 15:47:47 +0800 Subject: [PATCH] feat(aitable): align skill with progressive schema loading --- .../cli/schema_agent_metadata/aitable.json | 186 +++---- internal/cli/schema_agent_metadata/index.json | 23 +- internal/cli/schema_agent_metadata_audit.json | 2 +- internal/cli/schema_catalog/catalog.json | 79 +-- .../cli/schema_catalog/tools/aitable.json | 186 +++---- .../cli/schema_hints/selection/aitable.json | 83 +-- scripts/gen_skill_shortcut_sections.py | 2 +- scripts/policy/check-skill-context-budget.sh | 25 +- skills/multi/dingtalk-aitable/SKILL.md | 190 +++---- .../dingtalk-aitable/references/aitable.md | 24 +- .../references/aitable/aitable-attachment.md | 1 + .../aitable/aitable-error-recovery.md | 18 +- .../aitable/aitable-export-import.md | 4 + .../references/aitable/aitable-field.md | 2 +- .../aitable/aitable-record-create.md | 2 + .../aitable/aitable-record-query.md | 2 +- .../aitable/aitable-record-update.md | 12 +- .../scripts/aitable_export_via_task.py | 41 +- .../scripts/aitable_import_via_task.py | 4 + .../scripts/bulk_add_fields.py | 437 ++++++++-------- .../scripts/import_records.py | 481 ++++++++---------- .../scripts/upload_attachment.py | 51 +- test/scripts/aitable_skill_scripts_test.py | 208 ++++++++ test/unit/aitable_skill_scripts_test.go | 30 ++ 24 files changed, 1148 insertions(+), 945 deletions(-) create mode 100644 test/scripts/aitable_skill_scripts_test.py create mode 100644 test/unit/aitable_skill_scripts_test.go diff --git a/internal/cli/schema_agent_metadata/aitable.json b/internal/cli/schema_agent_metadata/aitable.json index cfc2f451..1c1edb48 100644 --- a/internal/cli/schema_agent_metadata/aitable.json +++ b/internal/cli/schema_agent_metadata/aitable.json @@ -264,7 +264,7 @@ "agent_summary_source": "dws-agent-selection/aitable", "availability": "available", "avoid_when": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "知道名称并要解析唯一 baseId 用 +resolve-base;按关键词要完整候选用 +base-search;不要把最近访问列表描述成全部 Base" ], "confirmation": "not_required", "effect": "read", @@ -308,7 +308,7 @@ }, "avoid_when": { "value": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "知道名称并要解析唯一 baseId 用 +resolve-base;按关键词要完整候选用 +base-search;不要把最近访问列表描述成全部 Base" ], "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", @@ -317,7 +317,7 @@ "candidates": [ { "value": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "知道名称并要解析唯一 baseId 用 +resolve-base;按关键词要完整候选用 +base-search;不要把最近访问列表描述成全部 Base" ], "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", @@ -525,7 +525,7 @@ "agent_summary_source": "dws-agent-selection/aitable", "availability": "available", "avoid_when": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "只需唯一 baseId 用 +resolve-base;浏览最近访问用 +base-list;需要未公开底层参数或原始响应才用 base search" ], "confirmation": "not_required", "effect": "read", @@ -568,7 +568,7 @@ }, "avoid_when": { "value": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "只需唯一 baseId 用 +resolve-base;浏览最近访问用 +base-list;需要未公开底层参数或原始响应才用 base search" ], "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", @@ -577,7 +577,7 @@ "candidates": [ { "value": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "只需唯一 baseId 用 +resolve-base;浏览最近访问用 +base-list;需要未公开底层参数或原始响应才用 base search" ], "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", @@ -1815,7 +1815,7 @@ "agent_summary_source": "dws-agent-selection/aitable", "availability": "available", "avoid_when": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "只需精简 fieldId/type 目录用 +table-get;创建或修改字段用 field create/update;需要未公开底层语义才用 field get" ], "confirmation": "not_required", "effect": "read", @@ -1858,7 +1858,7 @@ }, "avoid_when": { "value": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "只需精简 fieldId/type 目录用 +table-get;创建或修改字段用 field create/update;需要未公开底层语义才用 field get" ], "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", @@ -1867,7 +1867,7 @@ "candidates": [ { "value": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "只需精简 fieldId/type 目录用 +table-get;创建或修改字段用 field create/update;需要未公开底层语义才用 field get" ], "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", @@ -3624,7 +3624,7 @@ "agent_summary_source": "dws-agent-selection/aitable", "availability": "available", "avoid_when": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "完全空行扫描用 +record-query-empty;变更历史用 +record-history-list;需要 --all/--page-limit 或未公开底层语义才用 record query" ], "confirmation": "not_required", "effect": "read", @@ -3667,7 +3667,7 @@ }, "avoid_when": { "value": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "完全空行扫描用 +record-query-empty;变更历史用 +record-history-list;需要 --all/--page-limit 或未公开底层语义才用 record query" ], "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", @@ -3676,7 +3676,7 @@ "candidates": [ { "value": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "完全空行扫描用 +record-query-empty;变更历史用 +record-history-list;需要 --all/--page-limit 或未公开底层语义才用 record query" ], "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", @@ -4659,7 +4659,7 @@ "agent_summary_source": "dws-agent-selection/aitable", "availability": "available", "avoid_when": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "多候选时必须让用户消歧;只要完整候选列表用 +base-search;浏览最近访问用 +base-list;已知 baseId 不再重复搜索" ], "confirmation": "not_required", "effect": "read", @@ -4702,7 +4702,7 @@ }, "avoid_when": { "value": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "多候选时必须让用户消歧;只要完整候选列表用 +base-search;浏览最近访问用 +base-list;已知 baseId 不再重复搜索" ], "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", @@ -4711,7 +4711,7 @@ "candidates": [ { "value": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "多候选时必须让用户消歧;只要完整候选列表用 +base-search;浏览最近访问用 +base-list;已知 baseId 不再重复搜索" ], "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", @@ -4877,7 +4877,7 @@ }, "use_when": { "value": [ - "当你只知道某个多维表 Base 的名称(或名称里的关键词)、想把它解析成可直接用于后续工具的 baseId 时使用;内部按 --name 关键词调用 search_bases 搜索 Base,再在本地投影出每个候选的 baseId 和 name。如果只命中一个 Base 就直接返回它的 baseId;如果命中多个则列出全部候选让你消歧,绝不替你瞎猜;如果一个都没命中则提示未找到。这是纯只读操作,只做搜索与本地投影,不会修改任何 Base。" + "只知道 Base 名称或关键词,需要解析成唯一 baseId 供后续 Table/Record 操作时" ], "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", @@ -4886,7 +4886,7 @@ "candidates": [ { "value": [ - "当你只知道某个多维表 Base 的名称(或名称里的关键词)、想把它解析成可直接用于后续工具的 baseId 时使用;内部按 --name 关键词调用 search_bases 搜索 Base,再在本地投影出每个候选的 baseId 和 name。如果只命中一个 Base 就直接返回它的 baseId;如果命中多个则列出全部候选让你消歧,绝不替你瞎猜;如果一个都没命中则提示未找到。这是纯只读操作,只做搜索与本地投影,不会修改任何 Base。" + "只知道 Base 名称或关键词,需要解析成唯一 baseId 供后续 Table/Record 操作时" ], "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", @@ -4909,7 +4909,7 @@ "internal/cli/schema_hints/selection/aitable.json" ], "use_when": [ - "当你只知道某个多维表 Base 的名称(或名称里的关键词)、想把它解析成可直接用于后续工具的 baseId 时使用;内部按 --name 关键词调用 search_bases 搜索 Base,再在本地投影出每个候选的 baseId 和 name。如果只命中一个 Base 就直接返回它的 baseId;如果命中多个则列出全部候选让你消歧,绝不替你瞎猜;如果一个都没命中则提示未找到。这是纯只读操作,只做搜索与本地投影,不会修改任何 Base。" + "只知道 Base 名称或关键词,需要解析成唯一 baseId 供后续 Table/Record 操作时" ] }, "aitable +resolve-table": { @@ -4917,7 +4917,7 @@ "agent_summary_source": "dws-agent-selection/aitable", "availability": "available", "avoid_when": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "多候选时必须让用户消歧;需要所有表及字段/视图目录用 +table-get;已知 tableId 不再重复解析" ], "confirmation": "not_required", "effect": "read", @@ -4960,7 +4960,7 @@ }, "avoid_when": { "value": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "多候选时必须让用户消歧;需要所有表及字段/视图目录用 +table-get;已知 tableId 不再重复解析" ], "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", @@ -4969,7 +4969,7 @@ "candidates": [ { "value": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "多候选时必须让用户消歧;需要所有表及字段/视图目录用 +table-get;已知 tableId 不再重复解析" ], "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", @@ -5135,7 +5135,7 @@ }, "use_when": { "value": [ - "当你已经知道某个多维表 Base 的 baseId、又只记得里面某张数据表(table)的名称或名称关键词、想把它解析成可直接用于后续工具的 tableId 时使用;内部先用 get_tables(只传 baseId)列出该 Base 下的全部数据表,再在本地把每张表投影成 tableId、name,并按 --name 关键词做大小写不敏感的包含匹配来筛选候选。如果只命中一张表就直接返回它的 tableId;如果命中多张则列出全部候选让你消歧,绝不替你瞎猜;如果一张都没命中则提示未找到。这是纯只读操作,只做列举、本地匹配与投影,不会创建、修改或删除任何数据表。" + "已有真实 baseId、只知道数据表名称或关键词,需要解析成唯一 tableId 时" ], "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", @@ -5144,7 +5144,7 @@ "candidates": [ { "value": [ - "当你已经知道某个多维表 Base 的 baseId、又只记得里面某张数据表(table)的名称或名称关键词、想把它解析成可直接用于后续工具的 tableId 时使用;内部先用 get_tables(只传 baseId)列出该 Base 下的全部数据表,再在本地把每张表投影成 tableId、name,并按 --name 关键词做大小写不敏感的包含匹配来筛选候选。如果只命中一张表就直接返回它的 tableId;如果命中多张则列出全部候选让你消歧,绝不替你瞎猜;如果一张都没命中则提示未找到。这是纯只读操作,只做列举、本地匹配与投影,不会创建、修改或删除任何数据表。" + "已有真实 baseId、只知道数据表名称或关键词,需要解析成唯一 tableId 时" ], "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", @@ -5167,7 +5167,7 @@ "internal/cli/schema_hints/selection/aitable.json" ], "use_when": [ - "当你已经知道某个多维表 Base 的 baseId、又只记得里面某张数据表(table)的名称或名称关键词、想把它解析成可直接用于后续工具的 tableId 时使用;内部先用 get_tables(只传 baseId)列出该 Base 下的全部数据表,再在本地把每张表投影成 tableId、name,并按 --name 关键词做大小写不敏感的包含匹配来筛选候选。如果只命中一张表就直接返回它的 tableId;如果命中多张则列出全部候选让你消歧,绝不替你瞎猜;如果一张都没命中则提示未找到。这是纯只读操作,只做列举、本地匹配与投影,不会创建、修改或删除任何数据表。" + "已有真实 baseId、只知道数据表名称或关键词,需要解析成唯一 tableId 时" ] }, "aitable +role-list": { @@ -5949,7 +5949,7 @@ "agent_summary_source": "dws-agent-selection/aitable", "availability": "available", "avoid_when": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "只知道表名并要解析唯一 tableId 用 +resolve-table;需要字段完整 config 用 +field-get;需要未公开底层语义才用 table get" ], "confirmation": "not_required", "effect": "read", @@ -5993,7 +5993,7 @@ }, "avoid_when": { "value": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "只知道表名并要解析唯一 tableId 用 +resolve-table;需要字段完整 config 用 +field-get;需要未公开底层语义才用 table get" ], "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", @@ -6002,7 +6002,7 @@ "candidates": [ { "value": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "只知道表名并要解析唯一 tableId 用 +resolve-table;需要字段完整 config 用 +field-get;需要未公开底层语义才用 table get" ], "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", @@ -11191,11 +11191,11 @@ ] }, "aitable base search": { - "agent_summary": "按名称搜索 AI 表格 Base(优先于仅最近访问的 list)。", + "agent_summary": "底层按名称搜索 AI 表格 Base,返回原始候选列表。", "agent_summary_source": "dws-agent-selection/aitable", "availability": "available", "avoid_when": [ - "只要最近访问列表用 base list;已知 baseId 取详情用 base get;电子表格 axls 用 sheet" + "普通按名称解析唯一 baseId 优先用 +resolve-base;只要候选列表用 +base-search;最近访问用 +base-list;电子表格 axls 用 sheet" ], "confirmation": "not_required", "effect": "read", @@ -11205,14 +11205,14 @@ ], "field_provenance": { "agent_summary": { - "value": "按名称搜索 AI 表格 Base(优先于仅最近访问的 list)。", + "value": "底层按名称搜索 AI 表格 Base,返回原始候选列表。", "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", "review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。", "candidates": [ { - "value": "按名称搜索 AI 表格 Base(优先于仅最近访问的 list)。", + "value": "底层按名称搜索 AI 表格 Base,返回原始候选列表。", "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", "selected": true, @@ -11244,7 +11244,7 @@ }, "avoid_when": { "value": [ - "只要最近访问列表用 base list;已知 baseId 取详情用 base get;电子表格 axls 用 sheet" + "普通按名称解析唯一 baseId 优先用 +resolve-base;只要候选列表用 +base-search;最近访问用 +base-list;电子表格 axls 用 sheet" ], "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", @@ -11253,7 +11253,7 @@ "candidates": [ { "value": [ - "只要最近访问列表用 base list;已知 baseId 取详情用 base get;电子表格 axls 用 sheet" + "普通按名称解析唯一 baseId 优先用 +resolve-base;只要候选列表用 +base-search;最近访问用 +base-list;电子表格 axls 用 sheet" ], "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", @@ -11407,7 +11407,7 @@ }, "use_when": { "value": [ - "用户要找某个 AI 表格/多维表,按名称检索时优先使用" + "需要 Shortcut 未公开的底层参数、原始 search_bases 响应或自定义候选处理时" ], "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", @@ -11416,7 +11416,7 @@ "candidates": [ { "value": [ - "用户要找某个 AI 表格/多维表,按名称检索时优先使用" + "需要 Shortcut 未公开的底层参数、原始 search_bases 响应或自定义候选处理时" ], "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", @@ -11441,10 +11441,12 @@ "internal/cli/schema_hints/metadata/aitable.json", "internal/cli/schema_hints/selection/aitable.json", "internal/cli/schema_mcp_metadata.json#tools.aitable.base_search", - "skills/mono/references/products/aitable.md" + "skills/mono/references/products/aitable.md", + "skills/multi/dingtalk-aitable/SKILL.md", + "skills/multi/dingtalk-aitable/references/aitable.md" ], "use_when": [ - "用户要找某个 AI 表格/多维表,按名称检索时优先使用" + "需要 Shortcut 未公开的底层参数、原始 search_bases 响应或自定义候选处理时" ] }, "aitable base update": { @@ -16699,11 +16701,11 @@ ] }, "aitable field get": { - "agent_summary": "获取字段完整配置。", + "agent_summary": "底层获取字段完整类型与 config。", "agent_summary_source": "dws-agent-selection/aitable", "availability": "available", "avoid_when": [ - "表级字段目录也可用 table get;创建字段用 field create;不可用本命令改类型" + "正常按字段 ID 展开类型/config 优先用 +field-get;精简字段目录用 +table-get;创建字段用 field create" ], "confirmation": "not_required", "effect": "read", @@ -16713,14 +16715,14 @@ ], "field_provenance": { "agent_summary": { - "value": "获取字段完整配置。", + "value": "底层获取字段完整类型与 config。", "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", "review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。", "candidates": [ { - "value": "获取字段完整配置。", + "value": "底层获取字段完整类型与 config。", "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", "selected": true, @@ -16752,7 +16754,7 @@ }, "avoid_when": { "value": [ - "表级字段目录也可用 table get;创建字段用 field create;不可用本命令改类型" + "正常按字段 ID 展开类型/config 优先用 +field-get;精简字段目录用 +table-get;创建字段用 field create" ], "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", @@ -16761,7 +16763,7 @@ "candidates": [ { "value": [ - "表级字段目录也可用 table get;创建字段用 field create;不可用本命令改类型" + "正常按字段 ID 展开类型/config 优先用 +field-get;精简字段目录用 +table-get;创建字段用 field create" ], "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", @@ -16913,7 +16915,7 @@ }, "use_when": { "value": [ - "需要字段类型/config(选项、公式等)详情时" + "需要 +field-get 未公开的底层参数、原始响应或不同执行语义时" ], "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", @@ -16922,7 +16924,7 @@ "candidates": [ { "value": [ - "需要字段类型/config(选项、公式等)详情时" + "需要 +field-get 未公开的底层参数、原始响应或不同执行语义时" ], "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", @@ -16950,10 +16952,12 @@ "internal/cli/schema_hints/selection/aitable.json", "internal/cli/schema_mcp_metadata.json#tools.aitable.field_get", "skills/mono/references/products/aitable.md", - "skills/mono/references/products/aitable/aitable-field.md" + "skills/mono/references/products/aitable/aitable-field.md", + "skills/multi/dingtalk-aitable/SKILL.md", + "skills/multi/dingtalk-aitable/references/aitable/aitable-field.md" ], "use_when": [ - "需要字段类型/config(选项、公式等)详情时" + "需要 +field-get 未公开的底层参数、原始响应或不同执行语义时" ] }, "aitable field list": { @@ -22339,11 +22343,11 @@ ] }, "aitable record create": { - "agent_summary": "新增记录(cells 的 key 必须是 fieldId)。", + "agent_summary": "新增记录;使用 fieldId 写入并从 newRecordIds 回读验证。", "agent_summary_source": "dws-agent-selection/aitable", "availability": "available", "avoid_when": [ - "更新已有行用 record update;有则更无则增用 upsert;批量同 patch 用 batch-update" + "未核对字段类型时先 field get;更新已有行用 record update;有则更无则增用 upsert;本地 CSV/JSON 批量追加可用已评审脚本" ], "confirmation": "not_required", "effect": "write", @@ -22353,14 +22357,14 @@ ], "field_provenance": { "agent_summary": { - "value": "新增记录(cells 的 key 必须是 fieldId)。", + "value": "新增记录;使用 fieldId 写入并从 newRecordIds 回读验证。", "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", "review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。", "candidates": [ { - "value": "新增记录(cells 的 key 必须是 fieldId)。", + "value": "新增记录;使用 fieldId 写入并从 newRecordIds 回读验证。", "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", "selected": true, @@ -22392,7 +22396,7 @@ }, "avoid_when": { "value": [ - "更新已有行用 record update;有则更无则增用 upsert;批量同 patch 用 batch-update" + "未核对字段类型时先 field get;更新已有行用 record update;有则更无则增用 upsert;本地 CSV/JSON 批量追加可用已评审脚本" ], "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", @@ -22401,7 +22405,7 @@ "candidates": [ { "value": [ - "更新已有行用 record update;有则更无则增用 upsert;批量同 patch 用 batch-update" + "未核对字段类型时先 field get;更新已有行用 record update;有则更无则增用 upsert;本地 CSV/JSON 批量追加可用已评审脚本" ], "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", @@ -22553,7 +22557,7 @@ }, "use_when": { "value": [ - "需要插入新行数据时" + "已取得 baseId/tableId 与字段完整配置,需要插入一条或多条新记录并回读时" ], "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", @@ -22562,7 +22566,7 @@ "candidates": [ { "value": [ - "需要插入新行数据时" + "已取得 baseId/tableId 与字段完整配置,需要插入一条或多条新记录并回读时" ], "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", @@ -22592,10 +22596,12 @@ "skills/mono/references/products/aitable-record-ops.md", "skills/mono/references/products/aitable.md", "skills/mono/references/products/aitable/aitable-attachment.md", - "skills/mono/references/products/aitable/aitable-record-create.md" + "skills/mono/references/products/aitable/aitable-record-create.md", + "skills/multi/dingtalk-aitable/SKILL.md", + "skills/multi/dingtalk-aitable/references/aitable/aitable-record-create.md" ], "use_when": [ - "需要插入新行数据时" + "已取得 baseId/tableId 与字段完整配置,需要插入一条或多条新记录并回读时" ] }, "aitable record delete": { @@ -23929,11 +23935,11 @@ ] }, "aitable record query": { - "agent_summary": "查询/搜索记录(filters/sort/分页/--all;cells 键为 fieldId)。", + "agent_summary": "底层查询/搜索记录,额外支持原子命令的完整分页控制。", "agent_summary_source": "dws-agent-selection/aitable", "availability": "available", "avoid_when": [ - "已知 recordId 窄查可用 record get;空行用 query-empty;写入用 create/update;电子表格单元格用 sheet" + "普通按 ID、关键词、filters、sort 或 cursor 查询优先用 +record-query;空行用 +record-query-empty;写入用 create/update;电子表格单元格用 sheet" ], "confirmation": "not_required", "effect": "read", @@ -23943,14 +23949,14 @@ ], "field_provenance": { "agent_summary": { - "value": "查询/搜索记录(filters/sort/分页/--all;cells 键为 fieldId)。", + "value": "底层查询/搜索记录,额外支持原子命令的完整分页控制。", "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", "review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。", "candidates": [ { - "value": "查询/搜索记录(filters/sort/分页/--all;cells 键为 fieldId)。", + "value": "底层查询/搜索记录,额外支持原子命令的完整分页控制。", "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", "selected": true, @@ -23982,7 +23988,7 @@ }, "avoid_when": { "value": [ - "已知 recordId 窄查可用 record get;空行用 query-empty;写入用 create/update;电子表格单元格用 sheet" + "普通按 ID、关键词、filters、sort 或 cursor 查询优先用 +record-query;空行用 +record-query-empty;写入用 create/update;电子表格单元格用 sheet" ], "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", @@ -23991,7 +23997,7 @@ "candidates": [ { "value": [ - "已知 recordId 窄查可用 record get;空行用 query-empty;写入用 create/update;电子表格单元格用 sheet" + "普通按 ID、关键词、filters、sort 或 cursor 查询优先用 +record-query;空行用 +record-query-empty;写入用 create/update;电子表格单元格用 sheet" ], "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", @@ -24151,7 +24157,7 @@ }, "use_when": { "value": [ - "查看、筛选、全文搜索或遍历记录时的主入口" + "需要 +record-query 未公开的 --all/--page-limit、底层原始响应或不同执行语义时" ], "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", @@ -24160,7 +24166,7 @@ "candidates": [ { "value": [ - "查看、筛选、全文搜索或遍历记录时的主入口" + "需要 +record-query 未公开的 --all/--page-limit、底层原始响应或不同执行语义时" ], "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", @@ -24189,10 +24195,12 @@ "internal/cli/schema_mcp_metadata.json#tools.aitable.query_records", "skills/mono/references/products/aitable-record-ops.md", "skills/mono/references/products/aitable.md", - "skills/mono/references/products/aitable/aitable-record-query.md" + "skills/mono/references/products/aitable/aitable-record-query.md", + "skills/multi/dingtalk-aitable/SKILL.md", + "skills/multi/dingtalk-aitable/references/aitable/aitable-record-query.md" ], "use_when": [ - "查看、筛选、全文搜索或遍历记录时的主入口" + "需要 +record-query 未公开的 --all/--page-limit、底层原始响应或不同执行语义时" ] }, "aitable record query-empty": { @@ -24711,11 +24719,11 @@ ] }, "aitable record update": { - "agent_summary": "更新已有记录字段(先 query 拿 recordId;只传需改字段)。", + "agent_summary": "更新已有记录字段;先 query 拿 recordId,使用 fieldId 写入并回读。", "agent_summary_source": "dws-agent-selection/aitable", "availability": "available", "avoid_when": [ - "新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert" + "未定位 recordId 或字段配置时先 query/field get;新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert" ], "confirmation": "not_required", "effect": "write", @@ -24725,14 +24733,14 @@ ], "field_provenance": { "agent_summary": { - "value": "更新已有记录字段(先 query 拿 recordId;只传需改字段)。", + "value": "更新已有记录字段;先 query 拿 recordId,使用 fieldId 写入并回读。", "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", "review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。", "candidates": [ { - "value": "更新已有记录字段(先 query 拿 recordId;只传需改字段)。", + "value": "更新已有记录字段;先 query 拿 recordId,使用 fieldId 写入并回读。", "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", "selected": true, @@ -24764,7 +24772,7 @@ }, "avoid_when": { "value": [ - "新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert" + "未定位 recordId 或字段配置时先 query/field get;新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert" ], "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", @@ -24773,7 +24781,7 @@ "candidates": [ { "value": [ - "新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert" + "未定位 recordId 或字段配置时先 query/field get;新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert" ], "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", @@ -24925,7 +24933,7 @@ }, "use_when": { "value": [ - "需要修改已有记录若干字段时" + "已查询得到真实 recordId 并核对字段类型,需要修改每条记录各自的 cells 时" ], "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", @@ -24934,7 +24942,7 @@ "candidates": [ { "value": [ - "需要修改已有记录若干字段时" + "已查询得到真实 recordId 并核对字段类型,需要修改每条记录各自的 cells 时" ], "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", @@ -24963,10 +24971,12 @@ "internal/cli/schema_mcp_metadata.json#tools.aitable.record_update", "skills/mono/references/products/aitable-record-ops.md", "skills/mono/references/products/aitable.md", - "skills/mono/references/products/aitable/aitable-attachment.md" + "skills/mono/references/products/aitable/aitable-attachment.md", + "skills/multi/dingtalk-aitable/SKILL.md", + "skills/multi/dingtalk-aitable/references/aitable/aitable-record-update.md" ], "use_when": [ - "需要修改已有记录若干字段时" + "已查询得到真实 recordId 并核对字段类型,需要修改每条记录各自的 cells 时" ] }, "aitable record upsert": { @@ -27862,11 +27872,11 @@ ] }, "aitable table get": { - "agent_summary": "获取数据表结构(字段+视图目录)。", + "agent_summary": "底层获取数据表结构、精简字段目录与视图目录。", "agent_summary_source": "dws-agent-selection/aitable", "availability": "available", "avoid_when": [ - "只关心 Base 级 tables 列表可先 base get;字段完整配置也可用 field get" + "正常获取表、精简字段和视图目录优先用 +table-get;按表名解析 ID 用 +resolve-table;完整字段 config 用 +field-get" ], "confirmation": "not_required", "effect": "read", @@ -27876,14 +27886,14 @@ ], "field_provenance": { "agent_summary": { - "value": "获取数据表结构(字段+视图目录)。", + "value": "底层获取数据表结构、精简字段目录与视图目录。", "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", "review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。", "candidates": [ { - "value": "获取数据表结构(字段+视图目录)。", + "value": "底层获取数据表结构、精简字段目录与视图目录。", "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", "selected": true, @@ -27915,7 +27925,7 @@ }, "avoid_when": { "value": [ - "只关心 Base 级 tables 列表可先 base get;字段完整配置也可用 field get" + "正常获取表、精简字段和视图目录优先用 +table-get;按表名解析 ID 用 +resolve-table;完整字段 config 用 +field-get" ], "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", @@ -27924,7 +27934,7 @@ "candidates": [ { "value": [ - "只关心 Base 级 tables 列表可先 base get;字段完整配置也可用 field get" + "正常获取表、精简字段和视图目录优先用 +table-get;按表名解析 ID 用 +resolve-table;完整字段 config 用 +field-get" ], "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", @@ -28076,7 +28086,7 @@ }, "use_when": { "value": [ - "需要字段 fieldId 或视图目录以继续 record/field 操作时优先 table get" + "需要 +table-get 未公开的底层参数、原始 get_tables 响应或不同执行语义时" ], "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", @@ -28085,7 +28095,7 @@ "candidates": [ { "value": [ - "需要字段 fieldId 或视图目录以继续 record/field 操作时优先 table get" + "需要 +table-get 未公开的底层参数、原始 get_tables 响应或不同执行语义时" ], "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", @@ -28115,10 +28125,12 @@ "skills/mono/references/products/aitable.md", "skills/mono/references/products/aitable/aitable-advperm.md", "skills/mono/references/products/aitable/aitable-primary-doc.md", - "skills/mono/references/products/aitable/aitable-record-create.md" + "skills/mono/references/products/aitable/aitable-record-create.md", + "skills/multi/dingtalk-aitable/SKILL.md", + "skills/multi/dingtalk-aitable/references/aitable.md" ], "use_when": [ - "需要字段 fieldId 或视图目录以继续 record/field 操作时优先 table get" + "需要 +table-get 未公开的底层参数、原始 get_tables 响应或不同执行语义时" ] }, "aitable table list": { diff --git a/internal/cli/schema_agent_metadata/index.json b/internal/cli/schema_agent_metadata/index.json index e5c898ce..9cc51b94 100644 --- a/internal/cli/schema_agent_metadata/index.json +++ b/internal/cli/schema_agent_metadata/index.json @@ -1,6 +1,6 @@ { "version": 1, - "source_hash": "sha256:709741e6cd06537bcf0c4bfb6a8930a14f9a43108128e1064b712bd9e2fbaa42", + "source_hash": "sha256:b3c33641f91b69b73f3e319e0927e8654b6ba1e24f99071e22ee813a4e9f861d", "surface_hash": "sha256:4897d582a472d9d3596f88fb4546e7795d6b9bc334d020712f5d895ce894f7a7", "coverage": { "surface_products": 26, @@ -88,20 +88,20 @@ ] }, "aitable": { - "agent_summary": "管理 AI 表格 Base、数据表、字段、记录、视图、表单、仪表盘、权限、导入导出与自动化工作流。", + "agent_summary": "管理 AI 表格的 Base、Table、Field、Record、视图表单、仪表盘、权限、导入导出与自动化工作流。", "agent_summary_source": "dws-agent-selection/aitable", "avoid_when": [ - "目标是在线电子表格单元格读写时用 sheet;普通文档用 doc" + "在线电子表格工作表、单元格或公式用 sheet;普通文档用 doc;钉盘普通文件用 drive;类型不明的 alidocs URL 先做类型预检" ], "field_provenance": { "agent_summary": { - "value": "管理 AI 表格 Base、数据表、字段、记录、视图、表单、仪表盘、权限、导入导出与自动化工作流。", + "value": "管理 AI 表格的 Base、Table、Field、Record、视图表单、仪表盘、权限、导入导出与自动化工作流。", "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", "candidates": [ { - "value": "管理 AI 表格 Base、数据表、字段、记录、视图、表单、仪表盘、权限、导入导出与自动化工作流。", + "value": "管理 AI 表格的 Base、Table、Field、Record、视图表单、仪表盘、权限、导入导出与自动化工作流。", "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", "selected": true @@ -110,7 +110,7 @@ }, "avoid_when": { "value": [ - "目标是在线电子表格单元格读写时用 sheet;普通文档用 doc" + "在线电子表格工作表、单元格或公式用 sheet;普通文档用 doc;钉盘普通文件用 drive;类型不明的 alidocs URL 先做类型预检" ], "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", @@ -118,7 +118,7 @@ "candidates": [ { "value": [ - "目标是在线电子表格单元格读写时用 sheet;普通文档用 doc" + "在线电子表格工作表、单元格或公式用 sheet;普通文档用 doc;钉盘普通文件用 drive;类型不明的 alidocs URL 先做类型预检" ], "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", @@ -128,7 +128,7 @@ }, "use_when": { "value": [ - "需要读取或管理 AI 表格中的结构、数据、视图、权限、导入导出或工作流时" + "目标是 AI 表格/多维表中的结构化 Base、数据表、字段、记录、视图、权限、文件导入导出或自动化工作流时" ], "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", @@ -136,7 +136,7 @@ "candidates": [ { "value": [ - "需要读取或管理 AI 表格中的结构、数据、视图、权限、导入导出或工作流时" + "目标是 AI 表格/多维表中的结构化 Base、数据表、字段、记录、视图、权限、文件导入导出或自动化工作流时" ], "source": "internal/cli/schema_hints/selection/aitable.json", "precedence": "reviewed_explicit", @@ -153,10 +153,11 @@ "internal/cli/schema_hints/selection/aitable.json", "skills/mono/SKILL.md", "skills/mono/references/intent-guide.md", - "skills/mono/references/products/aitable.md" + "skills/multi/dingtalk-aitable/SKILL.md", + "skills/multi/dingtalk-aitable/references/aitable.md" ], "use_when": [ - "需要读取或管理 AI 表格中的结构、数据、视图、权限、导入导出或工作流时" + "目标是 AI 表格/多维表中的结构化 Base、数据表、字段、记录、视图、权限、文件导入导出或自动化工作流时" ] }, "attendance": { diff --git a/internal/cli/schema_agent_metadata_audit.json b/internal/cli/schema_agent_metadata_audit.json index 8baac4bc..8526d83f 100644 --- a/internal/cli/schema_agent_metadata_audit.json +++ b/internal/cli/schema_agent_metadata_audit.json @@ -1,6 +1,6 @@ { "version": 1, - "source_hash": "sha256:709741e6cd06537bcf0c4bfb6a8930a14f9a43108128e1064b712bd9e2fbaa42", + "source_hash": "sha256:b3c33641f91b69b73f3e319e0927e8654b6ba1e24f99071e22ee813a4e9f861d", "surface_hash": "sha256:4897d582a472d9d3596f88fb4546e7795d6b9bc334d020712f5d895ce894f7a7", "source_files": 160, "hint_files": 54, diff --git a/internal/cli/schema_catalog/catalog.json b/internal/cli/schema_catalog/catalog.json index 9a0d8aea..c4df502f 100644 --- a/internal/cli/schema_catalog/catalog.json +++ b/internal/cli/schema_catalog/catalog.json @@ -1,12 +1,12 @@ { "version": 1, "surface_hash": "sha256:4897d582a472d9d3596f88fb4546e7795d6b9bc334d020712f5d895ce894f7a7", - "source_hash": "sha256:399dcd69d9f98e98ef9668edb6d965b59f5f61f9964f9546fb5796fee9b08865", + "source_hash": "sha256:3246b6b86cec02dd90b7cd093310667d926d62d3237d3408cfccc13ad3751b45", "catalog": { "agent_metadata": { "products_with_metadata": 26, "source": "embedded-skill-metadata", - "source_hash": "sha256:709741e6cd06537bcf0c4bfb6a8930a14f9a43108128e1064b712bd9e2fbaa42", + "source_hash": "sha256:b3c33641f91b69b73f3e319e0927e8654b6ba1e24f99071e22ee813a4e9f861d", "surface_hash": "sha256:4897d582a472d9d3596f88fb4546e7795d6b9bc334d020712f5d895ce894f7a7", "surface_products": 26, "surface_tools": 875, @@ -225,12 +225,13 @@ "internal/cli/schema_hints/selection/aitable.json", "skills/mono/SKILL.md", "skills/mono/references/intent-guide.md", - "skills/mono/references/products/aitable.md" + "skills/multi/dingtalk-aitable/SKILL.md", + "skills/multi/dingtalk-aitable/references/aitable.md" ], - "agent_summary": "管理 AI 表格 Base、数据表、字段、记录、视图、表单、仪表盘、权限、导入导出与自动化工作流。", + "agent_summary": "管理 AI 表格的 Base、Table、Field、Record、视图表单、仪表盘、权限、导入导出与自动化工作流。", "agent_summary_source": "dws-agent-selection/aitable", "avoid_when": [ - "目标是在线电子表格单元格读写时用 sheet;普通文档用 doc" + "在线电子表格工作表、单元格或公式用 sheet;普通文档用 doc;钉盘普通文件用 drive;类型不明的 alidocs URL 先做类型预检" ], "description": "AI 表格操作", "field_provenance": { @@ -240,13 +241,13 @@ "precedence": "reviewed_explicit", "selected": true, "source": "internal/cli/schema_hints/selection/aitable.json", - "value": "管理 AI 表格 Base、数据表、字段、记录、视图、表单、仪表盘、权限、导入导出与自动化工作流。" + "value": "管理 AI 表格的 Base、Table、Field、Record、视图表单、仪表盘、权限、导入导出与自动化工作流。" } ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", "source": "internal/cli/schema_hints/selection/aitable.json", - "value": "管理 AI 表格 Base、数据表、字段、记录、视图、表单、仪表盘、权限、导入导出与自动化工作流。" + "value": "管理 AI 表格的 Base、Table、Field、Record、视图表单、仪表盘、权限、导入导出与自动化工作流。" }, "avoid_when": { "candidates": [ @@ -255,7 +256,7 @@ "selected": true, "source": "internal/cli/schema_hints/selection/aitable.json", "value": [ - "目标是在线电子表格单元格读写时用 sheet;普通文档用 doc" + "在线电子表格工作表、单元格或公式用 sheet;普通文档用 doc;钉盘普通文件用 drive;类型不明的 alidocs URL 先做类型预检" ] } ], @@ -263,7 +264,7 @@ "resolution": "highest_precedence", "source": "internal/cli/schema_hints/selection/aitable.json", "value": [ - "目标是在线电子表格单元格读写时用 sheet;普通文档用 doc" + "在线电子表格工作表、单元格或公式用 sheet;普通文档用 doc;钉盘普通文件用 drive;类型不明的 alidocs URL 先做类型预检" ] }, "use_when": { @@ -273,7 +274,7 @@ "selected": true, "source": "internal/cli/schema_hints/selection/aitable.json", "value": [ - "需要读取或管理 AI 表格中的结构、数据、视图、权限、导入导出或工作流时" + "目标是 AI 表格/多维表中的结构化 Base、数据表、字段、记录、视图、权限、文件导入导出或自动化工作流时" ] } ], @@ -281,7 +282,7 @@ "resolution": "highest_precedence", "source": "internal/cli/schema_hints/selection/aitable.json", "value": [ - "需要读取或管理 AI 表格中的结构、数据、视图、权限、导入导出或工作流时" + "目标是 AI 表格/多维表中的结构化 Base、数据表、字段、记录、视图、权限、文件导入导出或自动化工作流时" ] } }, @@ -726,11 +727,11 @@ }, { "agent_metadata_source": "embedded-skill-metadata", - "agent_summary": "按名称搜索 AI 表格 Base(优先于仅最近访问的 list)。", + "agent_summary": "底层按名称搜索 AI 表格 Base,返回原始候选列表。", "agent_summary_source": "dws-agent-selection/aitable", "availability": "available", "avoid_when": [ - "只要最近访问列表用 base list;已知 baseId 取详情用 base get;电子表格 axls 用 sheet" + "普通按名称解析唯一 baseId 优先用 +resolve-base;只要候选列表用 +base-search;最近访问用 +base-list;电子表格 axls 用 sheet" ], "canonical_path": "aitable.base_search", "cli_name": "search", @@ -752,7 +753,7 @@ "risk": "low", "title": "搜索 AI 表格", "use_when": [ - "用户要找某个 AI 表格/多维表,按名称检索时优先使用" + "需要 Shortcut 未公开的底层参数、原始 search_bases 响应或自定义候选处理时" ] }, { @@ -1375,11 +1376,11 @@ }, { "agent_metadata_source": "embedded-skill-metadata", - "agent_summary": "获取字段完整配置。", + "agent_summary": "底层获取字段完整类型与 config。", "agent_summary_source": "dws-agent-selection/aitable", "availability": "available", "avoid_when": [ - "表级字段目录也可用 table get;创建字段用 field create;不可用本命令改类型" + "正常按字段 ID 展开类型/config 优先用 +field-get;精简字段目录用 +table-get;创建字段用 field create" ], "canonical_path": "aitable.field_get", "cli_name": "get", @@ -1401,7 +1402,7 @@ "risk": "low", "title": "获取字段详情", "use_when": [ - "需要字段类型/config(选项、公式等)详情时" + "需要 +field-get 未公开的底层参数、原始响应或不同执行语义时" ] }, { @@ -1948,14 +1949,14 @@ }, { "agent_metadata_source": "embedded-skill-metadata", - "agent_summary": "查询/搜索记录(filters/sort/分页/--all;cells 键为 fieldId)。", + "agent_summary": "底层查询/搜索记录,额外支持原子命令的完整分页控制。", "agent_summary_source": "dws-agent-selection/aitable", "aliases": [ "aitable record list" ], "availability": "available", "avoid_when": [ - "已知 recordId 窄查可用 record get;空行用 query-empty;写入用 create/update;电子表格单元格用 sheet" + "普通按 ID、关键词、filters、sort 或 cursor 查询优先用 +record-query;空行用 +record-query-empty;写入用 create/update;电子表格单元格用 sheet" ], "canonical_path": "aitable.query_records", "cli_name": "query", @@ -1977,7 +1978,7 @@ "risk": "low", "title": "获取行记录", "use_when": [ - "查看、筛选、全文搜索或遍历记录时的主入口" + "需要 +record-query 未公开的 --all/--page-limit、底层原始响应或不同执行语义时" ] }, { @@ -2009,11 +2010,11 @@ }, { "agent_metadata_source": "embedded-skill-metadata", - "agent_summary": "新增记录(cells 的 key 必须是 fieldId)。", + "agent_summary": "新增记录;使用 fieldId 写入并从 newRecordIds 回读验证。", "agent_summary_source": "dws-agent-selection/aitable", "availability": "available", "avoid_when": [ - "更新已有行用 record update;有则更无则增用 upsert;批量同 patch 用 batch-update" + "未核对字段类型时先 field get;更新已有行用 record update;有则更无则增用 upsert;本地 CSV/JSON 批量追加可用已评审脚本" ], "canonical_path": "aitable.record_create", "cli_name": "create", @@ -2035,7 +2036,7 @@ "risk": "medium", "title": "新增记录", "use_when": [ - "需要插入新行数据时" + "已取得 baseId/tableId 与字段完整配置,需要插入一条或多条新记录并回读时" ] }, { @@ -2257,11 +2258,11 @@ }, { "agent_metadata_source": "embedded-skill-metadata", - "agent_summary": "更新已有记录字段(先 query 拿 recordId;只传需改字段)。", + "agent_summary": "更新已有记录字段;先 query 拿 recordId,使用 fieldId 写入并回读。", "agent_summary_source": "dws-agent-selection/aitable", "availability": "available", "avoid_when": [ - "新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert" + "未定位 recordId 或字段配置时先 query/field get;新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert" ], "canonical_path": "aitable.record_update", "cli_name": "update", @@ -2283,7 +2284,7 @@ "risk": "medium", "title": "更新记录", "use_when": [ - "需要修改已有记录若干字段时" + "已查询得到真实 recordId 并核对字段类型,需要修改每条记录各自的 cells 时" ] }, { @@ -2596,7 +2597,7 @@ "agent_summary_source": "dws-agent-selection/aitable", "availability": "available", "avoid_when": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "知道名称并要解析唯一 baseId 用 +resolve-base;按关键词要完整候选用 +base-search;不要把最近访问列表描述成全部 Base" ], "canonical_path": "aitable.shortcut_base_list", "cli_name": "+base-list", @@ -2622,7 +2623,7 @@ "agent_summary_source": "dws-agent-selection/aitable", "availability": "available", "avoid_when": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "只需唯一 baseId 用 +resolve-base;浏览最近访问用 +base-list;需要未公开底层参数或原始响应才用 base search" ], "canonical_path": "aitable.shortcut_base_search", "cli_name": "+base-search", @@ -2752,7 +2753,7 @@ "agent_summary_source": "dws-agent-selection/aitable", "availability": "available", "avoid_when": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "只需精简 fieldId/type 目录用 +table-get;创建或修改字段用 field create/update;需要未公开底层语义才用 field get" ], "canonical_path": "aitable.shortcut_field_get", "cli_name": "+field-get", @@ -2934,7 +2935,7 @@ "agent_summary_source": "dws-agent-selection/aitable", "availability": "available", "avoid_when": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "完全空行扫描用 +record-query-empty;变更历史用 +record-history-list;需要 --all/--page-limit 或未公开底层语义才用 record query" ], "canonical_path": "aitable.shortcut_record_query", "cli_name": "+record-query", @@ -3038,7 +3039,7 @@ "agent_summary_source": "dws-agent-selection/aitable", "availability": "available", "avoid_when": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "多候选时必须让用户消歧;只要完整候选列表用 +base-search;浏览最近访问用 +base-list;已知 baseId 不再重复搜索" ], "canonical_path": "aitable.shortcut_resolve_base", "cli_name": "+resolve-base", @@ -3055,7 +3056,7 @@ "risk": "low", "title": "按名称搜索多维表 Base 并解析出唯一 baseId(只读)", "use_when": [ - "当你只知道某个多维表 Base 的名称(或名称里的关键词)、想把它解析成可直接用于后续工具的 baseId 时使用;内部按 --name 关键词调用 search_bases 搜索 Base,再在本地投影出每个候选的 baseId 和 name。如果只命中一个 Base 就直接返回它的 baseId;如果命中多个则列出全部候选让你消歧,绝不替你瞎猜;如果一个都没命中则提示未找到。这是纯只读操作,只做搜索与本地投影,不会修改任何 Base。" + "只知道 Base 名称或关键词,需要解析成唯一 baseId 供后续 Table/Record 操作时" ] }, { @@ -3064,7 +3065,7 @@ "agent_summary_source": "dws-agent-selection/aitable", "availability": "available", "avoid_when": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "多候选时必须让用户消歧;需要所有表及字段/视图目录用 +table-get;已知 tableId 不再重复解析" ], "canonical_path": "aitable.shortcut_resolve_table", "cli_name": "+resolve-table", @@ -3081,7 +3082,7 @@ "risk": "low", "title": "在某个多维表 Base 内按名称解析出唯一的数据表 tableId(只读)", "use_when": [ - "当你已经知道某个多维表 Base 的 baseId、又只记得里面某张数据表(table)的名称或名称关键词、想把它解析成可直接用于后续工具的 tableId 时使用;内部先用 get_tables(只传 baseId)列出该 Base 下的全部数据表,再在本地把每张表投影成 tableId、name,并按 --name 关键词做大小写不敏感的包含匹配来筛选候选。如果只命中一张表就直接返回它的 tableId;如果命中多张则列出全部候选让你消歧,绝不替你瞎猜;如果一张都没命中则提示未找到。这是纯只读操作,只做列举、本地匹配与投影,不会创建、修改或删除任何数据表。" + "已有真实 baseId、只知道数据表名称或关键词,需要解析成唯一 tableId 时" ] }, { @@ -3168,7 +3169,7 @@ "agent_summary_source": "dws-agent-selection/aitable", "availability": "available", "avoid_when": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "只知道表名并要解析唯一 tableId 用 +resolve-table;需要字段完整 config 用 +field-get;需要未公开底层语义才用 table get" ], "canonical_path": "aitable.shortcut_table_get", "cli_name": "+table-get", @@ -3382,11 +3383,11 @@ }, { "agent_metadata_source": "embedded-skill-metadata", - "agent_summary": "获取数据表结构(字段+视图目录)。", + "agent_summary": "底层获取数据表结构、精简字段目录与视图目录。", "agent_summary_source": "dws-agent-selection/aitable", "availability": "available", "avoid_when": [ - "只关心 Base 级 tables 列表可先 base get;字段完整配置也可用 field get" + "正常获取表、精简字段和视图目录优先用 +table-get;按表名解析 ID 用 +resolve-table;完整字段 config 用 +field-get" ], "canonical_path": "aitable.table_get", "cli_name": "get", @@ -3408,7 +3409,7 @@ "risk": "low", "title": "获取数据表", "use_when": [ - "需要字段 fieldId 或视图目录以继续 record/field 操作时优先 table get" + "需要 +table-get 未公开的底层参数、原始 get_tables 响应或不同执行语义时" ] }, { @@ -4571,7 +4572,7 @@ } ], "use_when": [ - "需要读取或管理 AI 表格中的结构、数据、视图、权限、导入导出或工作流时" + "目标是 AI 表格/多维表中的结构化 Base、数据表、字段、记录、视图、权限、文件导入导出或自动化工作流时" ] }, { diff --git a/internal/cli/schema_catalog/tools/aitable.json b/internal/cli/schema_catalog/tools/aitable.json index 3f8cf870..69b8bd45 100644 --- a/internal/cli/schema_catalog/tools/aitable.json +++ b/internal/cli/schema_catalog/tools/aitable.json @@ -8704,13 +8704,15 @@ "internal/cli/schema_hints/metadata/aitable.json", "internal/cli/schema_hints/selection/aitable.json", "internal/cli/schema_mcp_metadata.json#tools.aitable.base_search", - "skills/mono/references/products/aitable.md" + "skills/mono/references/products/aitable.md", + "skills/multi/dingtalk-aitable/SKILL.md", + "skills/multi/dingtalk-aitable/references/aitable.md" ], - "agent_summary": "按名称搜索 AI 表格 Base(优先于仅最近访问的 list)。", + "agent_summary": "底层按名称搜索 AI 表格 Base,返回原始候选列表。", "agent_summary_source": "dws-agent-selection/aitable", "availability": "available", "avoid_when": [ - "只要最近访问列表用 base list;已知 baseId 取详情用 base get;电子表格 axls 用 sheet" + "普通按名称解析唯一 baseId 优先用 +resolve-base;只要候选列表用 +base-search;最近访问用 +base-list;电子表格 axls 用 sheet" ], "canonical_path": "aitable.base_search", "cli_name": "search", @@ -8731,14 +8733,14 @@ "review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。", "selected": true, "source": "internal/cli/schema_hints/selection/aitable.json", - "value": "按名称搜索 AI 表格 Base(优先于仅最近访问的 list)。" + "value": "底层按名称搜索 AI 表格 Base,返回原始候选列表。" } ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", "review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。", "source": "internal/cli/schema_hints/selection/aitable.json", - "value": "按名称搜索 AI 表格 Base(优先于仅最近访问的 list)。" + "value": "底层按名称搜索 AI 表格 Base,返回原始候选列表。" }, "availability": { "candidates": [ @@ -8770,7 +8772,7 @@ "selected": true, "source": "internal/cli/schema_hints/selection/aitable.json", "value": [ - "只要最近访问列表用 base list;已知 baseId 取详情用 base get;电子表格 axls 用 sheet" + "普通按名称解析唯一 baseId 优先用 +resolve-base;只要候选列表用 +base-search;最近访问用 +base-list;电子表格 axls 用 sheet" ] } ], @@ -8779,7 +8781,7 @@ "review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。", "source": "internal/cli/schema_hints/selection/aitable.json", "value": [ - "只要最近访问列表用 base list;已知 baseId 取详情用 base get;电子表格 axls 用 sheet" + "普通按名称解析唯一 baseId 优先用 +resolve-base;只要候选列表用 +base-search;最近访问用 +base-list;电子表格 axls 用 sheet" ] }, "canonical_path": { @@ -9009,7 +9011,7 @@ "selected": true, "source": "internal/cli/schema_hints/selection/aitable.json", "value": [ - "用户要找某个 AI 表格/多维表,按名称检索时优先使用" + "需要 Shortcut 未公开的底层参数、原始 search_bases 响应或自定义候选处理时" ] } ], @@ -9018,7 +9020,7 @@ "review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。", "source": "internal/cli/schema_hints/selection/aitable.json", "value": [ - "用户要找某个 AI 表格/多维表,按名称检索时优先使用" + "需要 Shortcut 未公开的底层参数、原始 search_bases 响应或自定义候选处理时" ] } }, @@ -9237,7 +9239,7 @@ "source": "reviewed_command_registry", "title": "搜索 AI 表格", "use_when": [ - "用户要找某个 AI 表格/多维表,按名称检索时优先使用" + "需要 Shortcut 未公开的底层参数、原始 search_bases 响应或自定义候选处理时" ] }, "aitable.base_update": { @@ -23545,13 +23547,15 @@ "internal/cli/schema_hints/selection/aitable.json", "internal/cli/schema_mcp_metadata.json#tools.aitable.field_get", "skills/mono/references/products/aitable.md", - "skills/mono/references/products/aitable/aitable-field.md" + "skills/mono/references/products/aitable/aitable-field.md", + "skills/multi/dingtalk-aitable/SKILL.md", + "skills/multi/dingtalk-aitable/references/aitable/aitable-field.md" ], - "agent_summary": "获取字段完整配置。", + "agent_summary": "底层获取字段完整类型与 config。", "agent_summary_source": "dws-agent-selection/aitable", "availability": "available", "avoid_when": [ - "表级字段目录也可用 table get;创建字段用 field create;不可用本命令改类型" + "正常按字段 ID 展开类型/config 优先用 +field-get;精简字段目录用 +table-get;创建字段用 field create" ], "canonical_path": "aitable.field_get", "cli_name": "get", @@ -23572,14 +23576,14 @@ "review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。", "selected": true, "source": "internal/cli/schema_hints/selection/aitable.json", - "value": "获取字段完整配置。" + "value": "底层获取字段完整类型与 config。" } ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", "review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。", "source": "internal/cli/schema_hints/selection/aitable.json", - "value": "获取字段完整配置。" + "value": "底层获取字段完整类型与 config。" }, "availability": { "candidates": [ @@ -23611,7 +23615,7 @@ "selected": true, "source": "internal/cli/schema_hints/selection/aitable.json", "value": [ - "表级字段目录也可用 table get;创建字段用 field create;不可用本命令改类型" + "正常按字段 ID 展开类型/config 优先用 +field-get;精简字段目录用 +table-get;创建字段用 field create" ] } ], @@ -23620,7 +23624,7 @@ "review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。", "source": "internal/cli/schema_hints/selection/aitable.json", "value": [ - "表级字段目录也可用 table get;创建字段用 field create;不可用本命令改类型" + "正常按字段 ID 展开类型/config 优先用 +field-get;精简字段目录用 +table-get;创建字段用 field create" ] }, "canonical_path": { @@ -23848,7 +23852,7 @@ "selected": true, "source": "internal/cli/schema_hints/selection/aitable.json", "value": [ - "需要字段类型/config(选项、公式等)详情时" + "需要 +field-get 未公开的底层参数、原始响应或不同执行语义时" ] } ], @@ -23857,7 +23861,7 @@ "review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。", "source": "internal/cli/schema_hints/selection/aitable.json", "value": [ - "需要字段类型/config(选项、公式等)详情时" + "需要 +field-get 未公开的底层参数、原始响应或不同执行语义时" ] } }, @@ -24201,7 +24205,7 @@ "source": "reviewed_command_registry", "title": "获取字段详情", "use_when": [ - "需要字段类型/config(选项、公式等)详情时" + "需要 +field-get 未公开的底层参数、原始响应或不同执行语义时" ] }, "aitable.field_list": { @@ -38119,16 +38123,18 @@ "internal/cli/schema_mcp_metadata.json#tools.aitable.query_records", "skills/mono/references/products/aitable-record-ops.md", "skills/mono/references/products/aitable.md", - "skills/mono/references/products/aitable/aitable-record-query.md" + "skills/mono/references/products/aitable/aitable-record-query.md", + "skills/multi/dingtalk-aitable/SKILL.md", + "skills/multi/dingtalk-aitable/references/aitable/aitable-record-query.md" ], - "agent_summary": "查询/搜索记录(filters/sort/分页/--all;cells 键为 fieldId)。", + "agent_summary": "底层查询/搜索记录,额外支持原子命令的完整分页控制。", "agent_summary_source": "dws-agent-selection/aitable", "aliases": [ "aitable record list" ], "availability": "available", "avoid_when": [ - "已知 recordId 窄查可用 record get;空行用 query-empty;写入用 create/update;电子表格单元格用 sheet" + "普通按 ID、关键词、filters、sort 或 cursor 查询优先用 +record-query;空行用 +record-query-empty;写入用 create/update;电子表格单元格用 sheet" ], "canonical_path": "aitable.query_records", "cli_name": "query", @@ -38149,14 +38155,14 @@ "review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。", "selected": true, "source": "internal/cli/schema_hints/selection/aitable.json", - "value": "查询/搜索记录(filters/sort/分页/--all;cells 键为 fieldId)。" + "value": "底层查询/搜索记录,额外支持原子命令的完整分页控制。" } ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", "review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。", "source": "internal/cli/schema_hints/selection/aitable.json", - "value": "查询/搜索记录(filters/sort/分页/--all;cells 键为 fieldId)。" + "value": "底层查询/搜索记录,额外支持原子命令的完整分页控制。" }, "availability": { "candidates": [ @@ -38188,7 +38194,7 @@ "selected": true, "source": "internal/cli/schema_hints/selection/aitable.json", "value": [ - "已知 recordId 窄查可用 record get;空行用 query-empty;写入用 create/update;电子表格单元格用 sheet" + "普通按 ID、关键词、filters、sort 或 cursor 查询优先用 +record-query;空行用 +record-query-empty;写入用 create/update;电子表格单元格用 sheet" ] } ], @@ -38197,7 +38203,7 @@ "review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。", "source": "internal/cli/schema_hints/selection/aitable.json", "value": [ - "已知 recordId 窄查可用 record get;空行用 query-empty;写入用 create/update;电子表格单元格用 sheet" + "普通按 ID、关键词、filters、sort 或 cursor 查询优先用 +record-query;空行用 +record-query-empty;写入用 create/update;电子表格单元格用 sheet" ] }, "canonical_path": { @@ -38447,7 +38453,7 @@ "selected": true, "source": "internal/cli/schema_hints/selection/aitable.json", "value": [ - "查看、筛选、全文搜索或遍历记录时的主入口" + "需要 +record-query 未公开的 --all/--page-limit、底层原始响应或不同执行语义时" ] } ], @@ -38456,7 +38462,7 @@ "review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。", "source": "internal/cli/schema_hints/selection/aitable.json", "value": [ - "查看、筛选、全文搜索或遍历记录时的主入口" + "需要 +record-query 未公开的 --all/--page-limit、底层原始响应或不同执行语义时" ] } }, @@ -39699,7 +39705,7 @@ "source": "reviewed_command_registry", "title": "获取行记录", "use_when": [ - "查看、筛选、全文搜索或遍历记录时的主入口" + "需要 +record-query 未公开的 --all/--page-limit、底层原始响应或不同执行语义时" ] }, "aitable.record_batch_update": { @@ -40397,13 +40403,15 @@ "skills/mono/references/products/aitable-record-ops.md", "skills/mono/references/products/aitable.md", "skills/mono/references/products/aitable/aitable-attachment.md", - "skills/mono/references/products/aitable/aitable-record-create.md" + "skills/mono/references/products/aitable/aitable-record-create.md", + "skills/multi/dingtalk-aitable/SKILL.md", + "skills/multi/dingtalk-aitable/references/aitable/aitable-record-create.md" ], - "agent_summary": "新增记录(cells 的 key 必须是 fieldId)。", + "agent_summary": "新增记录;使用 fieldId 写入并从 newRecordIds 回读验证。", "agent_summary_source": "dws-agent-selection/aitable", "availability": "available", "avoid_when": [ - "更新已有行用 record update;有则更无则增用 upsert;批量同 patch 用 batch-update" + "未核对字段类型时先 field get;更新已有行用 record update;有则更无则增用 upsert;本地 CSV/JSON 批量追加可用已评审脚本" ], "canonical_path": "aitable.record_create", "cli_name": "create", @@ -40424,14 +40432,14 @@ "review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。", "selected": true, "source": "internal/cli/schema_hints/selection/aitable.json", - "value": "新增记录(cells 的 key 必须是 fieldId)。" + "value": "新增记录;使用 fieldId 写入并从 newRecordIds 回读验证。" } ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", "review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。", "source": "internal/cli/schema_hints/selection/aitable.json", - "value": "新增记录(cells 的 key 必须是 fieldId)。" + "value": "新增记录;使用 fieldId 写入并从 newRecordIds 回读验证。" }, "availability": { "candidates": [ @@ -40463,7 +40471,7 @@ "selected": true, "source": "internal/cli/schema_hints/selection/aitable.json", "value": [ - "更新已有行用 record update;有则更无则增用 upsert;批量同 patch 用 batch-update" + "未核对字段类型时先 field get;更新已有行用 record update;有则更无则增用 upsert;本地 CSV/JSON 批量追加可用已评审脚本" ] } ], @@ -40472,7 +40480,7 @@ "review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。", "source": "internal/cli/schema_hints/selection/aitable.json", "value": [ - "更新已有行用 record update;有则更无则增用 upsert;批量同 patch 用 batch-update" + "未核对字段类型时先 field get;更新已有行用 record update;有则更无则增用 upsert;本地 CSV/JSON 批量追加可用已评审脚本" ] }, "canonical_path": { @@ -40700,7 +40708,7 @@ "selected": true, "source": "internal/cli/schema_hints/selection/aitable.json", "value": [ - "需要插入新行数据时" + "已取得 baseId/tableId 与字段完整配置,需要插入一条或多条新记录并回读时" ] } ], @@ -40709,7 +40717,7 @@ "review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。", "source": "internal/cli/schema_hints/selection/aitable.json", "value": [ - "需要插入新行数据时" + "已取得 baseId/tableId 与字段完整配置,需要插入一条或多条新记录并回读时" ] } }, @@ -41156,7 +41164,7 @@ "source": "reviewed_command_registry", "title": "新增记录", "use_when": [ - "需要插入新行数据时" + "已取得 baseId/tableId 与字段完整配置,需要插入一条或多条新记录并回读时" ] }, "aitable.record_delete": { @@ -46549,13 +46557,15 @@ "internal/cli/schema_mcp_metadata.json#tools.aitable.record_update", "skills/mono/references/products/aitable-record-ops.md", "skills/mono/references/products/aitable.md", - "skills/mono/references/products/aitable/aitable-attachment.md" + "skills/mono/references/products/aitable/aitable-attachment.md", + "skills/multi/dingtalk-aitable/SKILL.md", + "skills/multi/dingtalk-aitable/references/aitable/aitable-record-update.md" ], - "agent_summary": "更新已有记录字段(先 query 拿 recordId;只传需改字段)。", + "agent_summary": "更新已有记录字段;先 query 拿 recordId,使用 fieldId 写入并回读。", "agent_summary_source": "dws-agent-selection/aitable", "availability": "available", "avoid_when": [ - "新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert" + "未定位 recordId 或字段配置时先 query/field get;新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert" ], "canonical_path": "aitable.record_update", "cli_name": "update", @@ -46576,14 +46586,14 @@ "review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。", "selected": true, "source": "internal/cli/schema_hints/selection/aitable.json", - "value": "更新已有记录字段(先 query 拿 recordId;只传需改字段)。" + "value": "更新已有记录字段;先 query 拿 recordId,使用 fieldId 写入并回读。" } ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", "review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。", "source": "internal/cli/schema_hints/selection/aitable.json", - "value": "更新已有记录字段(先 query 拿 recordId;只传需改字段)。" + "value": "更新已有记录字段;先 query 拿 recordId,使用 fieldId 写入并回读。" }, "availability": { "candidates": [ @@ -46615,7 +46625,7 @@ "selected": true, "source": "internal/cli/schema_hints/selection/aitable.json", "value": [ - "新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert" + "未定位 recordId 或字段配置时先 query/field get;新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert" ] } ], @@ -46624,7 +46634,7 @@ "review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。", "source": "internal/cli/schema_hints/selection/aitable.json", "value": [ - "新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert" + "未定位 recordId 或字段配置时先 query/field get;新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert" ] }, "canonical_path": { @@ -46852,7 +46862,7 @@ "selected": true, "source": "internal/cli/schema_hints/selection/aitable.json", "value": [ - "需要修改已有记录若干字段时" + "已查询得到真实 recordId 并核对字段类型,需要修改每条记录各自的 cells 时" ] } ], @@ -46861,7 +46871,7 @@ "review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。", "source": "internal/cli/schema_hints/selection/aitable.json", "value": [ - "需要修改已有记录若干字段时" + "已查询得到真实 recordId 并核对字段类型,需要修改每条记录各自的 cells 时" ] } }, @@ -47308,7 +47318,7 @@ "source": "reviewed_command_registry", "title": "更新记录", "use_when": [ - "需要修改已有记录若干字段时" + "已查询得到真实 recordId 并核对字段类型,需要修改每条记录各自的 cells 时" ] }, "aitable.record_upsert": { @@ -53447,7 +53457,7 @@ "agent_summary_source": "dws-agent-selection/aitable", "availability": "available", "avoid_when": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "知道名称并要解析唯一 baseId 用 +resolve-base;按关键词要完整候选用 +base-search;不要把最近访问列表描述成全部 Base" ], "canonical_path": "aitable.shortcut_base_list", "cli_name": "+base-list", @@ -53502,7 +53512,7 @@ "selected": true, "source": "internal/cli/schema_hints/selection/aitable.json", "value": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "知道名称并要解析唯一 baseId 用 +resolve-base;按关键词要完整候选用 +base-search;不要把最近访问列表描述成全部 Base" ] } ], @@ -53511,7 +53521,7 @@ "review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.", "source": "internal/cli/schema_hints/selection/aitable.json", "value": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "知道名称并要解析唯一 baseId 用 +resolve-base;按关键词要完整候选用 +base-search;不要把最近访问列表描述成全部 Base" ] }, "canonical_path": { @@ -53943,7 +53953,7 @@ "agent_summary_source": "dws-agent-selection/aitable", "availability": "available", "avoid_when": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "只需唯一 baseId 用 +resolve-base;浏览最近访问用 +base-list;需要未公开底层参数或原始响应才用 base search" ], "canonical_path": "aitable.shortcut_base_search", "cli_name": "+base-search", @@ -53997,7 +54007,7 @@ "selected": true, "source": "internal/cli/schema_hints/selection/aitable.json", "value": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "只需唯一 baseId 用 +resolve-base;浏览最近访问用 +base-list;需要未公开底层参数或原始响应才用 base search" ] } ], @@ -54006,7 +54016,7 @@ "review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.", "source": "internal/cli/schema_hints/selection/aitable.json", "value": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "只需唯一 baseId 用 +resolve-base;浏览最近访问用 +base-list;需要未公开底层参数或原始响应才用 base search" ] }, "canonical_path": { @@ -56196,7 +56206,7 @@ "agent_summary_source": "dws-agent-selection/aitable", "availability": "available", "avoid_when": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "只需精简 fieldId/type 目录用 +table-get;创建或修改字段用 field create/update;需要未公开底层语义才用 field get" ], "canonical_path": "aitable.shortcut_field_get", "cli_name": "+field-get", @@ -56250,7 +56260,7 @@ "selected": true, "source": "internal/cli/schema_hints/selection/aitable.json", "value": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "只需精简 fieldId/type 目录用 +table-get;创建或修改字段用 field create/update;需要未公开底层语义才用 field get" ] } ], @@ -56259,7 +56269,7 @@ "review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.", "source": "internal/cli/schema_hints/selection/aitable.json", "value": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "只需精简 fieldId/type 目录用 +table-get;创建或修改字段用 field create/update;需要未公开底层语义才用 field get" ] }, "canonical_path": { @@ -60316,7 +60326,7 @@ "agent_summary_source": "dws-agent-selection/aitable", "availability": "available", "avoid_when": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "完全空行扫描用 +record-query-empty;变更历史用 +record-history-list;需要 --all/--page-limit 或未公开底层语义才用 record query" ], "canonical_path": "aitable.shortcut_record_query", "cli_name": "+record-query", @@ -60370,7 +60380,7 @@ "selected": true, "source": "internal/cli/schema_hints/selection/aitable.json", "value": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "完全空行扫描用 +record-query-empty;变更历史用 +record-history-list;需要 --all/--page-limit 或未公开底层语义才用 record query" ] } ], @@ -60379,7 +60389,7 @@ "review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.", "source": "internal/cli/schema_hints/selection/aitable.json", "value": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "完全空行扫描用 +record-query-empty;变更历史用 +record-history-list;需要 --all/--page-limit 或未公开底层语义才用 record query" ] }, "canonical_path": { @@ -63485,7 +63495,7 @@ "agent_summary_source": "dws-agent-selection/aitable", "availability": "available", "avoid_when": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "多候选时必须让用户消歧;只要完整候选列表用 +base-search;浏览最近访问用 +base-list;已知 baseId 不再重复搜索" ], "canonical_path": "aitable.shortcut_resolve_base", "cli_name": "+resolve-base", @@ -63539,7 +63549,7 @@ "selected": true, "source": "internal/cli/schema_hints/selection/aitable.json", "value": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "多候选时必须让用户消歧;只要完整候选列表用 +base-search;浏览最近访问用 +base-list;已知 baseId 不再重复搜索" ] } ], @@ -63548,7 +63558,7 @@ "review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.", "source": "internal/cli/schema_hints/selection/aitable.json", "value": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "多候选时必须让用户消歧;只要完整候选列表用 +base-search;浏览最近访问用 +base-list;已知 baseId 不再重复搜索" ] }, "canonical_path": { @@ -63758,7 +63768,7 @@ "selected": true, "source": "internal/cli/schema_hints/selection/aitable.json", "value": [ - "当你只知道某个多维表 Base 的名称(或名称里的关键词)、想把它解析成可直接用于后续工具的 baseId 时使用;内部按 --name 关键词调用 search_bases 搜索 Base,再在本地投影出每个候选的 baseId 和 name。如果只命中一个 Base 就直接返回它的 baseId;如果命中多个则列出全部候选让你消歧,绝不替你瞎猜;如果一个都没命中则提示未找到。这是纯只读操作,只做搜索与本地投影,不会修改任何 Base。" + "只知道 Base 名称或关键词,需要解析成唯一 baseId 供后续 Table/Record 操作时" ] } ], @@ -63767,7 +63777,7 @@ "review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.", "source": "internal/cli/schema_hints/selection/aitable.json", "value": [ - "当你只知道某个多维表 Base 的名称(或名称里的关键词)、想把它解析成可直接用于后续工具的 baseId 时使用;内部按 --name 关键词调用 search_bases 搜索 Base,再在本地投影出每个候选的 baseId 和 name。如果只命中一个 Base 就直接返回它的 baseId;如果命中多个则列出全部候选让你消歧,绝不替你瞎猜;如果一个都没命中则提示未找到。这是纯只读操作,只做搜索与本地投影,不会修改任何 Base。" + "只知道 Base 名称或关键词,需要解析成唯一 baseId 供后续 Table/Record 操作时" ] } }, @@ -63884,7 +63894,7 @@ "source": "reviewed_command_registry", "title": "按名称搜索多维表 Base 并解析出唯一 baseId(只读)", "use_when": [ - "当你只知道某个多维表 Base 的名称(或名称里的关键词)、想把它解析成可直接用于后续工具的 baseId 时使用;内部按 --name 关键词调用 search_bases 搜索 Base,再在本地投影出每个候选的 baseId 和 name。如果只命中一个 Base 就直接返回它的 baseId;如果命中多个则列出全部候选让你消歧,绝不替你瞎猜;如果一个都没命中则提示未找到。这是纯只读操作,只做搜索与本地投影,不会修改任何 Base。" + "只知道 Base 名称或关键词,需要解析成唯一 baseId 供后续 Table/Record 操作时" ] }, "aitable.shortcut_resolve_table": { @@ -63900,7 +63910,7 @@ "agent_summary_source": "dws-agent-selection/aitable", "availability": "available", "avoid_when": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "多候选时必须让用户消歧;需要所有表及字段/视图目录用 +table-get;已知 tableId 不再重复解析" ], "canonical_path": "aitable.shortcut_resolve_table", "cli_name": "+resolve-table", @@ -63954,7 +63964,7 @@ "selected": true, "source": "internal/cli/schema_hints/selection/aitable.json", "value": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "多候选时必须让用户消歧;需要所有表及字段/视图目录用 +table-get;已知 tableId 不再重复解析" ] } ], @@ -63963,7 +63973,7 @@ "review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.", "source": "internal/cli/schema_hints/selection/aitable.json", "value": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "多候选时必须让用户消歧;需要所有表及字段/视图目录用 +table-get;已知 tableId 不再重复解析" ] }, "canonical_path": { @@ -64173,7 +64183,7 @@ "selected": true, "source": "internal/cli/schema_hints/selection/aitable.json", "value": [ - "当你已经知道某个多维表 Base 的 baseId、又只记得里面某张数据表(table)的名称或名称关键词、想把它解析成可直接用于后续工具的 tableId 时使用;内部先用 get_tables(只传 baseId)列出该 Base 下的全部数据表,再在本地把每张表投影成 tableId、name,并按 --name 关键词做大小写不敏感的包含匹配来筛选候选。如果只命中一张表就直接返回它的 tableId;如果命中多张则列出全部候选让你消歧,绝不替你瞎猜;如果一张都没命中则提示未找到。这是纯只读操作,只做列举、本地匹配与投影,不会创建、修改或删除任何数据表。" + "已有真实 baseId、只知道数据表名称或关键词,需要解析成唯一 tableId 时" ] } ], @@ -64182,7 +64192,7 @@ "review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.", "source": "internal/cli/schema_hints/selection/aitable.json", "value": [ - "当你已经知道某个多维表 Base 的 baseId、又只记得里面某张数据表(table)的名称或名称关键词、想把它解析成可直接用于后续工具的 tableId 时使用;内部先用 get_tables(只传 baseId)列出该 Base 下的全部数据表,再在本地把每张表投影成 tableId、name,并按 --name 关键词做大小写不敏感的包含匹配来筛选候选。如果只命中一张表就直接返回它的 tableId;如果命中多张则列出全部候选让你消歧,绝不替你瞎猜;如果一张都没命中则提示未找到。这是纯只读操作,只做列举、本地匹配与投影,不会创建、修改或删除任何数据表。" + "已有真实 baseId、只知道数据表名称或关键词,需要解析成唯一 tableId 时" ] } }, @@ -64395,7 +64405,7 @@ "source": "reviewed_command_registry", "title": "在某个多维表 Base 内按名称解析出唯一的数据表 tableId(只读)", "use_when": [ - "当你已经知道某个多维表 Base 的 baseId、又只记得里面某张数据表(table)的名称或名称关键词、想把它解析成可直接用于后续工具的 tableId 时使用;内部先用 get_tables(只传 baseId)列出该 Base 下的全部数据表,再在本地把每张表投影成 tableId、name,并按 --name 关键词做大小写不敏感的包含匹配来筛选候选。如果只命中一张表就直接返回它的 tableId;如果命中多张则列出全部候选让你消歧,绝不替你瞎猜;如果一张都没命中则提示未找到。这是纯只读操作,只做列举、本地匹配与投影,不会创建、修改或删除任何数据表。" + "已有真实 baseId、只知道数据表名称或关键词,需要解析成唯一 tableId 时" ] }, "aitable.shortcut_role_list": { @@ -65656,7 +65666,7 @@ "agent_summary_source": "dws-agent-selection/aitable", "availability": "available", "avoid_when": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "只知道表名并要解析唯一 tableId 用 +resolve-table;需要字段完整 config 用 +field-get;需要未公开底层语义才用 table get" ], "canonical_path": "aitable.shortcut_table_get", "cli_name": "+table-get", @@ -65711,7 +65721,7 @@ "selected": true, "source": "internal/cli/schema_hints/selection/aitable.json", "value": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "只知道表名并要解析唯一 tableId 用 +resolve-table;需要字段完整 config 用 +field-get;需要未公开底层语义才用 table get" ] } ], @@ -65720,7 +65730,7 @@ "review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.", "source": "internal/cli/schema_hints/selection/aitable.json", "value": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "只知道表名并要解析唯一 tableId 用 +resolve-table;需要字段完整 config 用 +field-get;需要未公开底层语义才用 table get" ] }, "canonical_path": { @@ -70522,13 +70532,15 @@ "skills/mono/references/products/aitable.md", "skills/mono/references/products/aitable/aitable-advperm.md", "skills/mono/references/products/aitable/aitable-primary-doc.md", - "skills/mono/references/products/aitable/aitable-record-create.md" + "skills/mono/references/products/aitable/aitable-record-create.md", + "skills/multi/dingtalk-aitable/SKILL.md", + "skills/multi/dingtalk-aitable/references/aitable.md" ], - "agent_summary": "获取数据表结构(字段+视图目录)。", + "agent_summary": "底层获取数据表结构、精简字段目录与视图目录。", "agent_summary_source": "dws-agent-selection/aitable", "availability": "available", "avoid_when": [ - "只关心 Base 级 tables 列表可先 base get;字段完整配置也可用 field get" + "正常获取表、精简字段和视图目录优先用 +table-get;按表名解析 ID 用 +resolve-table;完整字段 config 用 +field-get" ], "canonical_path": "aitable.table_get", "cli_name": "get", @@ -70549,14 +70561,14 @@ "review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。", "selected": true, "source": "internal/cli/schema_hints/selection/aitable.json", - "value": "获取数据表结构(字段+视图目录)。" + "value": "底层获取数据表结构、精简字段目录与视图目录。" } ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", "review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。", "source": "internal/cli/schema_hints/selection/aitable.json", - "value": "获取数据表结构(字段+视图目录)。" + "value": "底层获取数据表结构、精简字段目录与视图目录。" }, "availability": { "candidates": [ @@ -70588,7 +70600,7 @@ "selected": true, "source": "internal/cli/schema_hints/selection/aitable.json", "value": [ - "只关心 Base 级 tables 列表可先 base get;字段完整配置也可用 field get" + "正常获取表、精简字段和视图目录优先用 +table-get;按表名解析 ID 用 +resolve-table;完整字段 config 用 +field-get" ] } ], @@ -70597,7 +70609,7 @@ "review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。", "source": "internal/cli/schema_hints/selection/aitable.json", "value": [ - "只关心 Base 级 tables 列表可先 base get;字段完整配置也可用 field get" + "正常获取表、精简字段和视图目录优先用 +table-get;按表名解析 ID 用 +resolve-table;完整字段 config 用 +field-get" ] }, "canonical_path": { @@ -70825,7 +70837,7 @@ "selected": true, "source": "internal/cli/schema_hints/selection/aitable.json", "value": [ - "需要字段 fieldId 或视图目录以继续 record/field 操作时优先 table get" + "需要 +table-get 未公开的底层参数、原始 get_tables 响应或不同执行语义时" ] } ], @@ -70834,7 +70846,7 @@ "review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。", "source": "internal/cli/schema_hints/selection/aitable.json", "value": [ - "需要字段 fieldId 或视图目录以继续 record/field 操作时优先 table get" + "需要 +table-get 未公开的底层参数、原始 get_tables 响应或不同执行语义时" ] } }, @@ -71081,7 +71093,7 @@ "source": "reviewed_command_registry", "title": "获取数据表", "use_when": [ - "需要字段 fieldId 或视图目录以继续 record/field 操作时优先 table get" + "需要 +table-get 未公开的底层参数、原始 get_tables 响应或不同执行语义时" ] }, "aitable.table_list": { diff --git a/internal/cli/schema_hints/selection/aitable.json b/internal/cli/schema_hints/selection/aitable.json index ef1f0ae8..b1b8a177 100644 --- a/internal/cli/schema_hints/selection/aitable.json +++ b/internal/cli/schema_hints/selection/aitable.json @@ -289,12 +289,12 @@ ] }, "aitable.base_search": { - "agent_summary": "按名称搜索 AI 表格 Base(优先于仅最近访问的 list)。", + "agent_summary": "底层按名称搜索 AI 表格 Base,返回原始候选列表。", "use_when": [ - "用户要找某个 AI 表格/多维表,按名称检索时优先使用" + "需要 Shortcut 未公开的底层参数、原始 search_bases 响应或自定义候选处理时" ], "avoid_when": [ - "只要最近访问列表用 base list;已知 baseId 取详情用 base get;电子表格 axls 用 sheet" + "普通按名称解析唯一 baseId 优先用 +resolve-base;只要候选列表用 +base-search;最近访问用 +base-list;电子表格 axls 用 sheet" ], "examples": [ "dws aitable base search --query \"项目\"" @@ -304,7 +304,8 @@ "source_refs": [ "internal/cli/schema_command_registry.json", "cobra-help:dws aitable base search --help", - "skills/mono/references/products/aitable.md", + "skills/multi/dingtalk-aitable/SKILL.md", + "skills/multi/dingtalk-aitable/references/aitable.md", "dws-schema-live:aitable.search_bases" ] }, @@ -710,12 +711,12 @@ ] }, "aitable.field_get": { - "agent_summary": "获取字段完整配置。", + "agent_summary": "底层获取字段完整类型与 config。", "use_when": [ - "需要字段类型/config(选项、公式等)详情时" + "需要 +field-get 未公开的底层参数、原始响应或不同执行语义时" ], "avoid_when": [ - "表级字段目录也可用 table get;创建字段用 field create;不可用本命令改类型" + "正常按字段 ID 展开类型/config 优先用 +field-get;精简字段目录用 +table-get;创建字段用 field create" ], "examples": [ "dws aitable field get --base-id --table-id " @@ -725,7 +726,8 @@ "source_refs": [ "internal/cli/schema_command_registry.json", "cobra-help:dws aitable field get --help", - "skills/mono/references/products/aitable.md", + "skills/multi/dingtalk-aitable/SKILL.md", + "skills/multi/dingtalk-aitable/references/aitable/aitable-field.md", "dws-schema-live:aitable.get_fields" ] }, @@ -1110,12 +1112,12 @@ ] }, "aitable.query_records": { - "agent_summary": "查询/搜索记录(filters/sort/分页/--all;cells 键为 fieldId)。", + "agent_summary": "底层查询/搜索记录,额外支持原子命令的完整分页控制。", "use_when": [ - "查看、筛选、全文搜索或遍历记录时的主入口" + "需要 +record-query 未公开的 --all/--page-limit、底层原始响应或不同执行语义时" ], "avoid_when": [ - "已知 recordId 窄查可用 record get;空行用 query-empty;写入用 create/update;电子表格单元格用 sheet" + "普通按 ID、关键词、filters、sort 或 cursor 查询优先用 +record-query;空行用 +record-query-empty;写入用 create/update;电子表格单元格用 sheet" ], "examples": [ "dws aitable record query --base-id --table-id " @@ -1125,7 +1127,8 @@ "source_refs": [ "internal/cli/schema_command_registry.json", "cobra-help:dws aitable record query --help", - "skills/mono/references/products/aitable.md", + "skills/multi/dingtalk-aitable/SKILL.md", + "skills/multi/dingtalk-aitable/references/aitable/aitable-record-query.md", "dws-schema-live:aitable.query_records" ] }, @@ -1150,12 +1153,12 @@ ] }, "aitable.record_create": { - "agent_summary": "新增记录(cells 的 key 必须是 fieldId)。", + "agent_summary": "新增记录;使用 fieldId 写入并从 newRecordIds 回读验证。", "use_when": [ - "需要插入新行数据时" + "已取得 baseId/tableId 与字段完整配置,需要插入一条或多条新记录并回读时" ], "avoid_when": [ - "更新已有行用 record update;有则更无则增用 upsert;批量同 patch 用 batch-update" + "未核对字段类型时先 field get;更新已有行用 record update;有则更无则增用 upsert;本地 CSV/JSON 批量追加可用已评审脚本" ], "examples": [ "dws aitable record create --base-id --table-id --records '[{\"cells\":{\"fldXXX\":\"值\"}}]'" @@ -1165,7 +1168,8 @@ "source_refs": [ "internal/cli/schema_command_registry.json", "cobra-help:dws aitable record create --help", - "skills/mono/references/products/aitable.md", + "skills/multi/dingtalk-aitable/SKILL.md", + "skills/multi/dingtalk-aitable/references/aitable/aitable-record-create.md", "dws-schema-live:aitable.create_records" ] }, @@ -1310,12 +1314,12 @@ ] }, "aitable.record_update": { - "agent_summary": "更新已有记录字段(先 query 拿 recordId;只传需改字段)。", + "agent_summary": "更新已有记录字段;先 query 拿 recordId,使用 fieldId 写入并回读。", "use_when": [ - "需要修改已有记录若干字段时" + "已查询得到真实 recordId 并核对字段类型,需要修改每条记录各自的 cells 时" ], "avoid_when": [ - "新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert" + "未定位 recordId 或字段配置时先 query/field get;新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert" ], "examples": [ "dws aitable record update --base-id --table-id --records '[{\"recordId\":\"recXXX\",\"cells\":{\"fldYYY\":\"新值\"}}]'" @@ -1325,7 +1329,8 @@ "source_refs": [ "internal/cli/schema_command_registry.json", "cobra-help:dws aitable record update --help", - "skills/mono/references/products/aitable.md", + "skills/multi/dingtalk-aitable/SKILL.md", + "skills/multi/dingtalk-aitable/references/aitable/aitable-record-update.md", "dws-schema-live:aitable.update_records" ] }, @@ -1550,12 +1555,12 @@ ] }, "aitable.table_get": { - "agent_summary": "获取数据表结构(字段+视图目录)。", + "agent_summary": "底层获取数据表结构、精简字段目录与视图目录。", "use_when": [ - "需要字段 fieldId 或视图目录以继续 record/field 操作时优先 table get" + "需要 +table-get 未公开的底层参数、原始 get_tables 响应或不同执行语义时" ], "avoid_when": [ - "只关心 Base 级 tables 列表可先 base get;字段完整配置也可用 field get" + "正常获取表、精简字段和视图目录优先用 +table-get;按表名解析 ID 用 +resolve-table;完整字段 config 用 +field-get" ], "examples": [ "dws aitable table get --base-id BASE_ID" @@ -1565,7 +1570,8 @@ "source_refs": [ "internal/cli/schema_command_registry.json", "cobra-help:dws aitable table get --help", - "skills/mono/references/products/aitable.md", + "skills/multi/dingtalk-aitable/SKILL.md", + "skills/multi/dingtalk-aitable/references/aitable.md", "dws-schema-live:aitable.get_tables" ] }, @@ -2335,7 +2341,7 @@ "当你不知道具体 baseId、想先浏览自己最近用过或可访问的 AI 表格清单以便定位目标时使用;支持游标分页,返回 Base 列表及其 baseId。" ], "avoid_when": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "知道名称并要解析唯一 baseId 用 +resolve-base;按关键词要完整候选用 +base-search;不要把最近访问列表描述成全部 Base" ], "examples": [ "dws aitable +base-list", @@ -2355,7 +2361,7 @@ "当你知道某个 AI 表格的名字或部分关键词、想直接定位到它并拿到 baseId 时使用;输入名称关键词,返回匹配的 Base 列表。" ], "avoid_when": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "只需唯一 baseId 用 +resolve-base;浏览最近访问用 +base-list;需要未公开底层参数或原始响应才用 base search" ], "examples": [ "dws aitable +base-search --query \"项目管理\"" @@ -2393,7 +2399,7 @@ "当你已进入某个 Base、需要了解其中某些数据表有哪些字段(拿 fieldId)、有哪些视图(拿 viewId)以便读写数据时使用;批量返回表信息、字段目录和视图目录。" ], "avoid_when": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "只知道表名并要解析唯一 tableId 用 +resolve-table;需要字段完整 config 用 +field-get;需要未公开底层语义才用 table get" ], "examples": [ "dws aitable +table-get --base-id BASE_ID", @@ -2413,7 +2419,7 @@ "当你需要查看字段的完整类型配置(如单选选项、关联表设置、AI 配置)以便正确写入数据或改配置时使用;批量返回字段详情。" ], "avoid_when": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "只需精简 fieldId/type 目录用 +table-get;创建或修改字段用 field create/update;需要未公开底层语义才用 field get" ], "examples": [ "dws aitable +field-get --base-id B --table-id T" @@ -2432,7 +2438,7 @@ "当你要读取表格里的行数据——按 recordId 精确取、按结构化条件筛选、按关键词全文搜索或分页遍历时使用;返回匹配记录及其单元格值。" ], "avoid_when": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "完全空行扫描用 +record-query-empty;变更历史用 +record-history-list;需要 --all/--page-limit 或未公开底层语义才用 record query" ], "examples": [ "dws aitable +record-query --base-id B --table-id T --query \"关键词\" --limit 50" @@ -2849,10 +2855,10 @@ "aitable.shortcut_resolve_base": { "agent_summary": "按名称搜索多维表 Base 并解析出唯一 baseId(只读)", "use_when": [ - "当你只知道某个多维表 Base 的名称(或名称里的关键词)、想把它解析成可直接用于后续工具的 baseId 时使用;内部按 --name 关键词调用 search_bases 搜索 Base,再在本地投影出每个候选的 baseId 和 name。如果只命中一个 Base 就直接返回它的 baseId;如果命中多个则列出全部候选让你消歧,绝不替你瞎猜;如果一个都没命中则提示未找到。这是纯只读操作,只做搜索与本地投影,不会修改任何 Base。" + "只知道 Base 名称或关键词,需要解析成唯一 baseId 供后续 Table/Record 操作时" ], "avoid_when": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "多候选时必须让用户消歧;只要完整候选列表用 +base-search;浏览最近访问用 +base-list;已知 baseId 不再重复搜索" ], "examples": [ "dws aitable +resolve-base --name 项目管理" @@ -2868,10 +2874,10 @@ "aitable.shortcut_resolve_table": { "agent_summary": "在某个多维表 Base 内按名称解析出唯一的数据表 tableId(只读)", "use_when": [ - "当你已经知道某个多维表 Base 的 baseId、又只记得里面某张数据表(table)的名称或名称关键词、想把它解析成可直接用于后续工具的 tableId 时使用;内部先用 get_tables(只传 baseId)列出该 Base 下的全部数据表,再在本地把每张表投影成 tableId、name,并按 --name 关键词做大小写不敏感的包含匹配来筛选候选。如果只命中一张表就直接返回它的 tableId;如果命中多张则列出全部候选让你消歧,绝不替你瞎猜;如果一张都没命中则提示未找到。这是纯只读操作,只做列举、本地匹配与投影,不会创建、修改或删除任何数据表。" + "已有真实 baseId、只知道数据表名称或关键词,需要解析成唯一 tableId 时" ], "avoid_when": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "多候选时必须让用户消歧;需要所有表及字段/视图目录用 +table-get;已知 tableId 不再重复解析" ], "examples": [ "dws aitable +resolve-table --base B --name 任务" @@ -2887,18 +2893,19 @@ }, "products": { "aitable": { - "agent_summary": "管理 AI 表格 Base、数据表、字段、记录、视图、表单、仪表盘、权限、导入导出与自动化工作流。", + "agent_summary": "管理 AI 表格的 Base、Table、Field、Record、视图表单、仪表盘、权限、导入导出与自动化工作流。", "use_when": [ - "需要读取或管理 AI 表格中的结构、数据、视图、权限、导入导出或工作流时" + "目标是 AI 表格/多维表中的结构化 Base、数据表、字段、记录、视图、权限、文件导入导出或自动化工作流时" ], "avoid_when": [ - "目标是在线电子表格单元格读写时用 sheet;普通文档用 doc" + "在线电子表格工作表、单元格或公式用 sheet;普通文档用 doc;钉盘普通文件用 drive;类型不明的 alidocs URL 先做类型预检" ], "reviewed": true, - "review_reason": "人工结合实时 MCP/Skill/Cobra 审阅产品级选择边界。", + "review_reason": "人工结合多产品边界、根 Skill 的渐进加载顺序、真实 Cobra/Shortcut 路径与 MCP 语义审阅产品级选择边界。", "source_refs": [ "internal/cli/schema_command_registry.json", - "skills/mono/references/products/aitable.md", + "skills/multi/dingtalk-aitable/SKILL.md", + "skills/multi/dingtalk-aitable/references/aitable.md", "dws-schema-live:product-index" ] } diff --git a/scripts/gen_skill_shortcut_sections.py b/scripts/gen_skill_shortcut_sections.py index 9df3e9e9..8fe9b519 100644 --- a/scripts/gen_skill_shortcut_sections.py +++ b/scripts/gen_skill_shortcut_sections.py @@ -55,7 +55,7 @@ PRODUCT_END = "" # only after verifying that the product skill has its own reviewed routing # section and intent table; compacting a sparse skill without an alternative # route would make its shortcuts harder to discover. -COMPACT_PRODUCT_SERVICES = {"chat", "doc"} +COMPACT_PRODUCT_SERVICES = {"aitable", "chat", "doc"} # These compact products have every public Shortcut curated into Runtime # Schema. Keep this separate from COMPACT_PRODUCT_SERVICES because Chat still diff --git a/scripts/policy/check-skill-context-budget.sh b/scripts/policy/check-skill-context-budget.sh index dfb1cc10..a21a366d 100755 --- a/scripts/policy/check-skill-context-budget.sh +++ b/scripts/policy/check-skill-context-budget.sh @@ -7,9 +7,11 @@ cd "$ROOT" python3 scripts/gen_skill_shortcut_sections.py --check chat_skill="skills/multi/dingtalk-chat/SKILL.md" +aitable_skill="skills/multi/dingtalk-aitable/SKILL.md" doc_skill="skills/multi/dingtalk-doc/SKILL.md" mono_skill="skills/mono/SKILL.md" chat_max_bytes=14000 +aitable_max_bytes=11000 doc_max_bytes=9500 chat_bytes="$(wc -c < "$chat_skill" | tr -d ' ')" @@ -19,6 +21,13 @@ if [ "$chat_bytes" -gt "$chat_max_bytes" ]; then exit 1 fi +aitable_bytes="$(wc -c < "$aitable_skill" | tr -d ' ')" +if [ "$aitable_bytes" -gt "$aitable_max_bytes" ]; then + printf '%s\n' \ + "skill context budget exceeded: $aitable_skill is ${aitable_bytes} bytes (max ${aitable_max_bytes})" >&2 + exit 1 +fi + doc_bytes="$(wc -c < "$doc_skill" | tr -d ' ')" if [ "$doc_bytes" -gt "$doc_max_bytes" ]; then printf '%s\n' \ @@ -40,6 +49,20 @@ if [ "$shortcut_rows" -ne 0 ]; then exit 1 fi +aitable_shortcut_rows="$( + awk ' + // { in_block = 1; next } + // { in_block = 0 } + in_block && /^\|[[:space:]]*`/ { count++ } + END { print count + 0 } + ' "$aitable_skill" +)" +if [ "$aitable_shortcut_rows" -ne 0 ]; then + printf '%s\n' \ + "skill context budget exceeded: $aitable_skill re-expanded $aitable_shortcut_rows shortcut rows" >&2 + exit 1 +fi + doc_shortcut_rows="$( awk ' // { in_block = 1; next } @@ -61,4 +84,4 @@ if grep -Fq "充分阅读产品参考文件" "$mono_skill"; then fi printf '%s\n' \ - "skill context budget: ok (chat_bytes=$chat_bytes max=$chat_max_bytes shortcut_rows=$shortcut_rows; doc_bytes=$doc_bytes max=$doc_max_bytes shortcut_rows=$doc_shortcut_rows)" + "skill context budget: ok (chat_bytes=$chat_bytes max=$chat_max_bytes shortcut_rows=$shortcut_rows; aitable_bytes=$aitable_bytes max=$aitable_max_bytes shortcut_rows=$aitable_shortcut_rows; doc_bytes=$doc_bytes max=$doc_max_bytes shortcut_rows=$doc_shortcut_rows)" diff --git a/skills/multi/dingtalk-aitable/SKILL.md b/skills/multi/dingtalk-aitable/SKILL.md index 3a7bb5e2..65e3b293 100644 --- a/skills/multi/dingtalk-aitable/SKILL.md +++ b/skills/multi/dingtalk-aitable/SKILL.md @@ -1,6 +1,6 @@ --- name: dingtalk-aitable -description: 钉钉 AI 表格(多维表)。Use when 用户说 AI表格/多维表/数据表/base/table/建表/查记录/写数据/字段/记录增删改查/筛选/排序/公式/模板搜索/批量导入CSV或JSON/导出/仪表盘/图表/上传附件到表格/按字段类型建表。不做电子表格单元格读写(走 dingtalk-misc)、文档编辑(走 dingtalk-doc);听记待办入表先用 dingtalk-minutes 提取,再由本 skill 写入。命令前缀:dws aitable。 +description: 钉钉 AI 表格(多维表)。Use when 用户说 AI表格/多维表/Base/数据表/字段/记录增删改查/筛选排序/公式与跨表引用/视图表单/仪表盘图表/高级权限/自动化工作流/模板/CSV或JSON批量导入/Excel导入导出/记录附件。不做电子表格单元格读写与工作表公式(走 dingtalk-sheet)、普通文档编辑(走 dingtalk-doc)或钉盘文件管理(走 dingtalk-drive);听记待办入表先用 dingtalk-minutes 提取,再由本 skill 写入。命令前缀:dws aitable。 metadata: cli_version: ">=0.2.14" category: product @@ -15,146 +15,86 @@ metadata: > **CRITICAL — 执行任何 `dws` 操作前,MUST 先用 Read 工具完整读取 [`dws-shared`](../dws-shared/SKILL.md)。**该轻量文件包含全局执行契约、安全底线及 shared references 的按需加载导航;不要预加载其全部 references。 -> 命令参考:[aitable.md](references/aitable.md);复杂命令按需加载 `references/aitable/*.md`;剧本:[06-data-analytics.md](references/06-data-analytics.md)。 +## 加载与路由顺序 + +1. 命中下方高频意图时直接使用精确骨架,不先查 Help 或产品级 Schema。 +2. 路由优先级固定为:精确 recipe / 可运行脚本 > 匹配的公开 Shortcut > 原子命令。命令已确定且参数清楚时直接执行。 +3. 参数、约束或安全语义不确定时只读 leaf Schema:`dws schema --cli-path "aitable " --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`。 -## Shortcuts(无专用脚本/recipe 时优先) +## Shortcut 发现(按需) -以下 shortcut 同时进入公开 catalog 与 Runtime Schema。先按本 skill 的意图表、脚本和 recipe 路由:存在精确覆盖该场景的专用脚本/recipe 时按其执行;否则用户意图命中时,shortcut 优先于手写原子命令。命令已选中时直接执行;只在参数或安全语义不确定时读取 leaf Schema(例如 `dws schema --cli-path "aitable +" --format json`),在当前 Cobra flags 不确定时读取 `dws aitable --help`。仅当现有路由和 reference 都无法定位低频能力时,才用 `dws shortcut list --service aitable --format json` 批量发现。 +`aitable` 当前有 29 条公开 shortcut。完整清单保留在 Runtime Shortcut Catalog;已完成 Schema curation 的子集可通过 leaf Schema 查询。高频产品根 Skill 不重复展开完整清单。已知意图直接使用下方的优先路由、意图表或任务 reference;命令已选中时直接执行,只在参数/安全语义不确定时读取 leaf Schema,在当前 Cobra flags 不确定时读取 leaf Help。 -| Shortcut | 风险 | 适用场景 | -|---|---|---| -| `dws aitable +base-get` | read | 获取指定 Base 的目录信息(tables / dashboards summary) | -| `dws aitable +base-list` | read | 获取当前用户可访问的 AI 表格 Base 列表(最近访问,支持游标分页) | -| `dws aitable +base-search` | read | 按名称关键词搜索 AI 表格 Base | -| `dws aitable +chart-get` | read | 获取指定 chart 的详细信息 | -| `dws aitable +chart-widgets-example` | read | 获取所有图表类型的 widget config 示例 | -| `dws aitable +dashboard-config-example` | read | 获取 dashboard config 的结构示例 | -| `dws aitable +dashboard-get` | read | 获取指定 dashboard 的详细信息(含 charts summary) | -| `dws aitable +field-get` | read | 批量获取字段详情(含类型相关完整配置) | -| `dws aitable +find-record` | read | 在指定多维表里按关键词查记录(只读) | -| `dws aitable +form-field-list` | read | 列出表单视图当前可见的字段及其配置 | -| `dws aitable +form-list` | read | 列出指定数据表下的所有表单视图 | -| `dws aitable +form-share-get` | read | 读取视图当前的分享表单配置 | -| `dws aitable +list-tables` | read | 列出某个多维表(base)里的所有数据表(只读,投影 tableId/tableName) | -| `dws aitable +record-history-list` | read | 按 recordId 查询单条记录的变更历史 | -| `dws aitable +record-query` | read | 查询表格记录(按 ID 取 / 条件筛选 / 关键词 / 分页) | -| `dws aitable +record-query-empty` | read | 扫描并过滤出完全没填用户字段的空行 | -| `dws aitable +record-share-links` | read | 批量(可 >20 条)获取多维表记录分享链接:去重+分片+合并 | -| `dws aitable +record-share-url` | read | 按 recordId 批量获取记录分享链接,单次最多 20 条 | -| `dws aitable +resolve-base` | read | 按名称搜索多维表 Base 并解析出唯一 baseId(只读) | -| `dws aitable +resolve-table` | read | 在某个多维表 Base 内按名称解析出唯一的数据表 tableId(只读) | -| `dws aitable +role-list` | read | 列出指定 Base 下的全部角色 | -| `dws aitable +section-list-empty` | read | 列出指定 Base 下所有没有子节点的空文件夹 | -| `dws aitable +section-list-nodes` | read | 列出指定 Base 当前版本下的全部 nsheet 节点 | -| `dws aitable +table-get` | read | 批量获取指定数据表的表级信息、字段目录与视图目录 | -| `dws aitable +template-search` | read | 按名称关键词搜索 AI 表格模板 | -| `dws aitable +view-get` | read | 获取视图完整信息(列顺序、筛选、排序、分组等) | -| `dws aitable +view-get-frozen-cols` | read | 获取视图当前冻结的左侧列数 | -| `dws aitable +view-get-lock` | read | 获取视图锁定状态 | -| `dws aitable +view-get-row-height` | read | 获取视图单元格行高(像素) | +仅当现有路由和 reference 都无法定位低频能力时,才执行 `dws shortcut list --service aitable --compact --format json` 做最后回退;不要为已知高频意图加载完整 Shortcut Catalog 或产品级 Schema。 -## 意图表 +## 核心对象与 ID -| 用户说 | 命令 | -|--------|------| -| "搜表格 / 找一个 base" | `dws aitable base search --query "<名>"` | -| "创建 AI 表格 / 多维表" | `dws aitable base create --name "<名称>" [--template-id ]` | -| "查数据表 / 建数据表" | `dws aitable table get --base-id ` / `dws aitable table create --base-id --name "<表名>" --fields '[...]'` | -| "查字段 / 字段类型" | `dws aitable field get --base-id --table-id ` | -| "查记录 / 搜索记录" | `dws aitable record query --base-id --table-id [--filters '...']` | -| "写记录 / 更新记录 / 删除记录" | `dws aitable record create/update/delete --base-id --table-id ...` | -| "筛选 / 排序 / 公式 / 跨表引用" | 先读 `references/aitable/aitable-filter-sort.md` / `aitable-formula-guide.md` | -| "批量导入 JSON / CSV" | `python scripts/import_records.py data.csv\|data.json` | -| "批量加字段" | `python scripts/bulk_add_fields.py --base-id --table-id --fields fields.json` | -| "导入 / 导出表格" | 先读 `references/aitable/aitable-export-import.md`;导出优先 `python scripts/aitable_export_via_task.py --scope table --table-id ` | -| "仪表盘 / 图表" | 先读 `references/aitable/aitable-dashboard-chart.md` | -| "上传附件到记录" | 先读 `references/aitable/aitable-attachment.md`;可用 `python scripts/upload_attachment.py --base-id --file ` | +| 对象 | 标识与执行边界 | +|---|---| +| Base | `baseId` 标识一个 AI 表格文件;名称只用于搜索或消歧,不能当 ID | +| Table | `tableId` 标识 Base 内的数据表;必须来自 `+resolve-table` / `+table-get` / 创建返回 | +| Field | `fieldId` 标识列;写入、筛选、排序和字段变更优先使用真实 `fieldId` | +| Record | `recordId` 标识行;更新、删除和分享前必须先查询得到真实 ID | +| View / Dashboard / Chart | `viewId` / `dashboardId` / `chartId` 各自绑定当前 Base/Table,不跨对象复用 | +| 异步任务 | `taskId` / `importId` 只用于对应导出或导入任务,不能替代业务对象 ID | -## 标准 SOP(必遵流程) +所有下游 ID 都从当前链路的结构化返回中提取;同名多候选必须让用户消歧,不默认取第一项,也不复用未经本轮校验的旧 ID。 -> 命中以下意图**必须**按对应 SOP 顺序执行;**禁止**跳步、替换命令、编造 flag/ID。每条命令必须带 `--format json`,执行后必须按"解析"步取真实字段,不得凭返回结构猜测。`baseId`/`tableId`/`fieldId`/`recordId` 一律先查后用,**禁止默认/编造**。 +## 核心意图与执行骨架 -### SOP-1 定位 Base 与 Table(list / search → table get) +| 用户意图 | 首选骨架 | 必须保留的执行边界 | +|---|---|---| +| 按名称找 Base | `dws aitable +resolve-base --name "<名称>" --format json` | 唯一命中才继续;多候选停止并消歧 | +| 浏览最近访问 | `dws aitable +base-list --format json` | 只代表最近访问,不得宣称全量 | +| 按名称找 Table | `dws aitable +resolve-table --base --name "<表名>" --format json` | `baseId` 必须来自上一步真实返回 | +| 取表、字段与视图目录 | `dws aitable +table-get --base-id [--table-ids ] --format json` | `tables[].fields[]` 是字段目录;完整类型/config 再用 `+field-get` | +| 取字段完整配置 | `dws aitable +field-get --base-id --table-id [--field-ids ] --format json` | 写入前核对类型、只读性和 select options;按需展开以控制返回体 | +| 查/搜/筛记录 | `dws aitable +record-query --base-id --table-id [--query <词>\|--filters ''\|--record-ids ] --format json` | ID 模式忽略 filter/sort;全量结论必须完整分页 | +| 新增记录 | `dws aitable record create --base-id --table-id --records '[{"cells":{"":<值>}}]' --format json` | 单次最多 100;取 `data.newRecordIds[]` 后立即按 ID 回读 | +| 更新记录 | `dws aitable record update --base-id --table-id --records '[{"recordId":"","cells":{"":<值>}}]' --format json` | 先 query 拿 recordId;只传需改字段;取 `data.recordIds[]` 后回读 | +| 删除记录 | 先 `dws aitable +record-query ...` 定位,再 `dws aitable record delete --base-id --table-id --record-ids ` | 展示目标与影响,得到明确确认后才加 `--yes` | +| 创建 Base / Table | `dws aitable base create --name "<名>"` / `dws aitable table create --base-id --name "<名>" --fields '[...]'` | 使用创建返回的真实 ID;系统改名/加后缀时不得继续猜原名 | +| 批量追加 CSV / JSON 到已有表 | `python3 scripts/import_records.py [batch_size]` | CSV 表头必须是 fieldId;脚本返回不完整 ledger 时不得宣称全成功 | +| 文件导入为新数据表 | `python3 scripts/aitable_import_via_task.py ` | 与“追加已有 table”不同;走 prepare → PUT → import task | +| 批量创建字段 | `python3 scripts/bulk_add_fields.py fields.json` | 单次最多 15;逐项检查成功/失败结果 | +| 导出 Base / Table / View | `python3 scripts/aitable_export_via_task.py --scope all\|table\|view [...]` | 保存路径、覆盖与异步未完成状态必须显式处理 | +| 上传记录附件 | `python3 scripts/upload_attachment.py ` | 返回 `fileToken` 后仍需按字段格式写入记录并回读 | -**触发**:找/打开某张 AI 表格、不知 baseId 或 tableId。 +## 记录读写不变量 -1. **选源(必须)**:有名称/关键词 → `dws aitable base search --query "<名称>"`;列最近访问 → `dws aitable base list`。`base list` 仅返回最近访问,不是全部,**禁止**当作全量清单。 -2. **执行(必须)**:`dws aitable base search --query "<完整名>" --format json`(或 `dws aitable base list --format json`)。 -3. **解析(必须)**:从 JSON 取真实 `baseId`;**多候选必须输出让用户选,禁止默认取第一个**。 -4. **取 tableId(必须)**:`dws aitable table get --base-id --format json` → 从 `data.tables[].tableId` 取目标表 ID,并记录 `views[]`。枚举模式不返回 `fields[]`;需要字段目录时必须继续执行 SOP-2 的 `field get`。若只核对某张表,可显式加 `--table-ids ` 控制返回体。 -5. **失败(必须)**:`base list` 为空或不命中 → 换 `base search --query` 关键词重试一次;仍无果**必须如实告知**,禁止臆造 baseId/tableId。 +- `record create/update` 前必须获取目标字段的 `fieldId`、`type` 与 `config`;`filterUp`、`lookup` 等只读字段不可写。完整格式只在需要时读 [aitable-cell-value.md](references/aitable/aitable-cell-value.md)。 +- 筛选和排序字段使用 `fieldId`;`--filters` 最外层是 `and|or + operands`,`--sort` 使用 `direction: asc|desc`。日期和跨表字段规则按需读 [aitable-filter-sort.md](references/aitable/aitable-filter-sort.md)。 +- `record query --all` 仍受 `--page-limit` 约束;分页中断或局部富化失败时保留已有结果,输出 completeness 与逐项失败 ledger,不把部分结果描述为全量。 +- 创建、更新、导入、批量建字段等写操作必须检查业务 `status`、逐项结果与返回 ID;普通写入按用户明确要求执行后回读,不能只凭退出码宣称成功。 +- 长 JSON 使用 `--records-file` / 任务文件;不得为绕过字段错误而静默丢列、改类型或删除失败项。 -**禁止**:跳过 `table get` 直接用字段名写记录、用模糊名匹配当 baseId、用旧会话里的 ID 不再校验。 +## 低频能力与 Reference -### SOP-2 拿字段定义(field get,写记录/改字段前置) +| 场景 | 按需读取 | +|---|---| +| 完整命令索引、对象 URL 与一级路由 | [aitable.md](references/aitable.md) | +| 记录 query/create/update/delete/upsert/history/share | 对应 `references/aitable/aitable-record-*.md` | +| 字段创建、字段 config、cellValue、公式与跨表引用 | [aitable-field.md](references/aitable/aitable-field.md)、[aitable-field-properties.md](references/aitable/aitable-field-properties.md)、[aitable-cell-value.md](references/aitable/aitable-cell-value.md)、[aitable-formula-guide.md](references/aitable/aitable-formula-guide.md) | +| 筛选、排序、统计、全量分析 | [aitable-filter-sort.md](references/aitable/aitable-filter-sort.md)、[aitable-data-analysis-sop.md](references/aitable/aitable-data-analysis-sop.md) | +| 导入导出、附件 | [aitable-export-import.md](references/aitable/aitable-export-import.md)、[aitable-attachment.md](references/aitable/aitable-attachment.md) | +| 视图、表单、仪表盘与图表 | [aitable-view-config.md](references/aitable/aitable-view-config.md)、[aitable-view-extras.md](references/aitable/aitable-view-extras.md)、[aitable-form.md](references/aitable/aitable-form.md)、[aitable-dashboard-chart.md](references/aitable/aitable-dashboard-chart.md) | +| 高级权限、自动化工作流、导航节点 | [aitable-advperm.md](references/aitable/aitable-advperm.md)、[aitable-workflow.md](references/aitable/aitable-workflow.md)、[aitable.md](references/aitable.md) 的 section 路由 | -**触发**:建/改/写记录、改字段名或 options、按字段类型拼写入参前。 +## 错误恢复 -1. **前置(必须)**:先按 SOP-1 拿到 `baseId` + `tableId`。 -2. **执行(必须)**:`dws aitable field get --base-id --table-id --format json`(仅展开需要的字段时加 `--field-ids fld1,fld2`,单次最多 10 个)。 -3. **解析(必须)**:取每个目标字段的 `fieldId`、`type`、`config`(如 singleSelect/multipleSelect 的 `options[].id|name`);写入 cells 的 key **必须用 `fieldId`**,不是字段中文名;select 字段过滤/写入传**选项名称字面量**,不传 option ID。 -4. **衔接(必须)**:拿到字段定义 → 进入 SOP-3 写记录、或 `dws aitable field update --field-id --name <新名>|--config --format json` 改字段。 -5. **失败(必须)**:字段不存在或类型不符 → 重新 `field get` 核对,**禁止**凭旧名称/旧类型继续写入。 - -**禁止**:用字段中文名当 cells key、跳过 `field get` 直接 `record create/update`、对 select 字段传 option ID 当写入值。 - -### SOP-3 写/批量写记录(record create) - -**触发**:新增记录、批量加数据、CSV/JSON 入表。 - -1. **前置(必须)**:SOP-1 取 `baseId`/`tableId` + SOP-2 取 `fieldId`/类型。 -2. **执行(必须)**:`dws aitable record create --base-id --table-id --records '[{"cells":{"":<值>}}]' --format json`;单次最多 100 条,超长用 `--records-file ./data.json`。 -3. **写入格式(必须)**:按 `record create --help` 类型表严格传值(text→字符串、number→数值、singleSelect→"选项名"、date→RFC3339、url→`{"text","link"}`、group→`{"cid"}` 等);`filterUp`/`lookup` 字段只读不可写。 -4. **解析与验证(必须)**:从返回 `data.newRecordIds[]` 取全部新记录 ID;不要读取不存在的标量 `recordId`。立即执行 `dws aitable record query --base-id --table-id --record-ids --format json` 回读写入值。 -5. **失败(必须)**:类型/格式错误按返回报错修正后重试,**禁止**降级丢弃字段;不确定格式先 `field get` 复核 config。 - -**禁止**:编造 fieldId/recordId、跳过 `field get` 凭中文名写、把 URL 字符串直接塞给 url 字段。 - -### SOP-4 查/筛/排记录(record query) - -**触发**:查记录、按条件筛选、排序、取关联记录、定位待改/待删的 recordId。 - -1. **前置(必须)**:SOP-1 拿 `baseId`/`tableId`。 -2. **执行(必须)**:`dws aitable record query --base-id --table-id --format json`;已知 ID 直取加 `--record-ids rec1,rec2`(忽略 filters/sort,单次≤100)。 -3. **筛选/排序(必须)**:`--filters` 最外层必须 `{"operator":"and|or","operands":[...]}`,select 字段值传**选项名字面量**;日期只能用 `date_eq/before/after/not_before/not_after`,范围用 `not_before`+`not_after` 组合,**禁止** `eq`/区间/相对时间。`--sort` 用 `[{"fieldId":"..","direction":"asc|desc"}]`(**必须用 `direction`**)。公式/引用/关联字段默认不返回,需显式 `--field-ids` 指定。 -4. **解析(必须)**:取真实 `recordId` 与字段值;分页用 `--cursor`,全表用 `--all --page-limit N`。 -5. **衔接(必须)**:拿到 recordId → SOP-5 更新、`record delete --record-ids --yes` 删除(删前确认)。 - -**禁止**:用字段名做 filter/sort key、对日期用 `eq`、漏掉 `direction` 用旧 `order` 字段、用本地过滤替代服务端 filter。 - -### SOP-5 更新记录(record update) - -**触发**:改记录字段值、批量更新状态、单字段重命名需求之外的记录改动。 - -1. **前置(必须)**:SOP-1 拿 `baseId`/`tableId`;SOP-2 拿字段类型;SOP-4 拿目标 `recordId`。 -2. **执行(必须)**:`dws aitable record update --base-id --table-id --records '[{"recordId":"recXXX","cells":{"":<新值>}}]' --format json`(每条必含 `recordId`+`cells`,单次≤100;超长用 `--records-file`);只传需改字段,未传保持原值。 -3. **解析与验证(必须)**:写入格式同 SOP-3;从返回 `data.recordIds[]` 取实际更新的记录 ID。更新响应不返回“受影响字段”,必须立即用 `record query --record-ids --format json` 回读目标字段确认。 -4. **失败(必须)**:recordId 不存在或类型不符 → 回 SOP-4 重新定位,**禁止**编造 ID 强写。 - -**禁止**:省略 `recordId`、用字段中文名当 cells key、凭空猜测 recordId 直接 update。 - -## 危险操作 - -`base delete` / `table delete` / `field delete` / `record delete` 不可逆,必须先向用户确认再加 `--yes`。 - -## 高频硬约束 - -- 创建/改字段/写记录是多轮连续任务时,不能在"让我执行/先获取 ID"后停下;必须实际调用对应 `dws aitable` 命令并验证结果。 -- 字段重命名使用 `dws aitable field update --base-id --table-id --field-id --name "<新名称>" --format json`;先 `field get` 找真实 `fieldId`,不要猜字段名能直接更新。 -- 写记录前必须 `field get` 获取 `fieldId` 与类型;`record create/update` 的 `cells` key 用 `fieldId`,不是字段中文名。长 JSON 使用 `--records-file`。 -- 表或字段创建返回名称被系统自动加后缀时,后续必须使用返回的真实 `tableId`/`fieldId`,不要继续按原名称猜。 -- `record update/delete` 先 `record query/list` 定位 `recordId`;删除必须确认,普通新增/更新按用户明确要求可直接执行后读回验证。 -- `record query/create/update/delete`、`field create`、导入导出、图表和附件场景必须先读对应 `references/aitable/*.md`,不要凭旧单文件参数猜 flag。 - -## 字段类型规则 - -详见本 skill 的 [field-rules.md](references/field-rules.md)。 +- 路径或 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` 操作不得自动重试或静默确认。 ## 跨产品协作 -- 单元格 / 工作表 / 公式 → 切到 `dingtalk-misc`(`references/sheet.md`,命令前缀:`dws sheet`) -## 局部意图 - -- [局部意图消歧](references/intent-guide.md)。 +- 电子表格工作表、单元格与公式 → `dingtalk-sheet`;结构化 Base/Table/Field/Record 才走本 skill。 +- 普通文档内容 → `dingtalk-doc`;钉盘普通文件与文件夹 → `dingtalk-drive`;记录附件上传仍走本 skill。 +- 用户直接提供类型不明的 alidocs URL 时,按 `dws-shared` 的 URL 预检导航确认 `extension=able` 后再执行。 +- 听记内容入表:先用 `dingtalk-minutes` 提取结构化结果,再按本 skill 的字段与记录规则写入。 diff --git a/skills/multi/dingtalk-aitable/references/aitable.md b/skills/multi/dingtalk-aitable/references/aitable.md index 890bea3e..b4e772f6 100644 --- a/skills/multi/dingtalk-aitable/references/aitable.md +++ b/skills/multi/dingtalk-aitable/references/aitable.md @@ -2,6 +2,8 @@ > **渐进式文档**:本文件为路由层(索引 + 意图判断),各命令的详细参数、示例和踩坑说明在 [aitable/](./aitable/) 目录下按需加载。 +已知高频意图优先使用根 Skill 的精确 Shortcut/脚本骨架;本文件只在需要完整一级命令索引、对象 URL 或低频分支导航时加载。参数与安全不确定时读 leaf Schema,Cobra flag 不确定时才读 leaf Help,不要把本文件当作参数事实源。 + ## 文档地址 (URI) | 资源 | URI 格式 | @@ -35,7 +37,7 @@ | 命令 | 用途 | 必填参数 | 路由提醒 | |------|------|----------|----------| -| `table get` | 获取数据表/视图目录 | `--base-id` | 不传 `--table-ids` 枚举全部表,但不返回字段;字段目录使用 `field get` | +| `table get` | 获取表级信息、字段目录与视图目录 | `--base-id` | 不传 `--table-ids` 枚举全部表并返回精简 `fields[]`/`views[]`;字段完整 config 使用 `field get` | | `table create` | 创建数据表 | `--base-id` `--name` `--fields` | fields 为 JSON 数组,至少 1 个 | | `table update` | 修改表名 / 备注 / 行命名规则 | `--base-id` `--table-id` + 三选一(`--name` / `--description` / `--record-name-key`) | `--record-name-key` 是固定枚举(如 task/project/event/customer/ji_lu 等),非字段 ID | | `table delete` | 删除表 | `--base-id` `--table-id` | 不可逆 | @@ -427,17 +429,18 @@ dws aitable export data --base-id --task-id --timeout-ms 300 ## 核心工作流 ```bash -# 1. 搜索/列出 Base — 提取 baseId -dws aitable base search --query "项目" --format json +# 1. 按名称解析唯一 Base — 提取 baseId;多候选必须消歧 +dws aitable +resolve-base --name "项目" --format json -# 2. 获取 Base 信息 — 提取 tableId -dws aitable base get --base-id --format json +# 2. 按名称解析唯一 Table — 提取 tableId +dws aitable +resolve-table --base --name "任务" --format json -# 3. 获取字段目录 — 提取 fieldId -dws aitable field get --base-id --table-id --format json +# 3. 获取字段目录;需要完整类型 config 时再调用 field get +dws aitable +table-get --base-id --table-ids --format json +dws aitable +field-get --base-id --table-id --format json # 4. 查询记录 -dws aitable record query --base-id --table-id --format json +dws aitable +record-query --base-id --table-id --format json # 5. 新增记录 (cells 用 fieldId 作 key) dws aitable record create --base-id --table-id \ @@ -452,8 +455,8 @@ dws aitable record create --base-id --table-id \ | `base create` | `baseId` | 后续命令 + 文档 URI | | `base get` | `tables[].tableId` | --table-id,拼接指定数据表 URI | | `table create` | `tableId` | 后续命令 + 拼接指定数据表 URI | -| `table get` | `tables[].tableId`、视图目录 | 定位数据表和视图;字段需继续调用 `field get` | -| `field get` | `fields[].fieldId` | record 操作的 cells key, field update/delete | +| `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 | @@ -480,6 +483,7 @@ dws aitable record create --base-id --table-id \ |------|------| | [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 表格记录 | diff --git a/skills/multi/dingtalk-aitable/references/aitable/aitable-attachment.md b/skills/multi/dingtalk-aitable/references/aitable/aitable-attachment.md index 45e9c2ae..46e0e148 100644 --- a/skills/multi/dingtalk-aitable/references/aitable/aitable-attachment.md +++ b/skills/multi/dingtalk-aitable/references/aitable/aitable-attachment.md @@ -32,6 +32,7 @@ dws aitable record create --base-id --table-id \ ``` > `uploadUrl` 有时效性(`expiresAt`),脚本会自动在获取后立即上传。 +> 脚本只接受成功的业务响应和 HTTPS `uploadUrl`;它返回 `fileToken` 不等于记录已经写入,后续仍需执行 record create/update 并按 recordId 回读。 ## 手动流程(不使用脚本) diff --git a/skills/multi/dingtalk-aitable/references/aitable/aitable-error-recovery.md b/skills/multi/dingtalk-aitable/references/aitable/aitable-error-recovery.md index 88c3aa9c..0e66ef10 100644 --- a/skills/multi/dingtalk-aitable/references/aitable/aitable-error-recovery.md +++ b/skills/multi/dingtalk-aitable/references/aitable/aitable-error-recovery.md @@ -15,6 +15,7 @@ - `status: "error"` 表示操作失败 - `summary` 包含错误摘要信息 - `trace_id` 用于问题追踪 +- 命令退出码为 0 但 `status != "success"`、必需 ID 缺失或逐项结果含失败,也属于业务失败 ## 2. 常见错误与恢复动作 @@ -42,7 +43,7 @@ |-------------------|------|---------| | `base not found` | base-id 错误或无权限 | 确认 base-id 正确;尝试 `base list` 或 `base search` 重新定位 | | `table not found` | table-id 错误 | 用 `table get --base-id ` 不带 table-ids 查看所有表 | -| 表名重复 | 同 Base 下已存在同名表 | 系统会自动续号(如"原名 1"),无需额外处理 | +| 表名重复 | 同 Base 下已存在同名表 | 使用创建返回的真实 `tableId/tableName`;禁止继续按原名猜测目标 | ### 2.4 视图操作错误 @@ -57,7 +58,7 @@ |-------------------|------|---------| | filters 无效被忽略 | 根节点不是 and/or,或 operands 格式错误 | 确保 filters 根节点是 `{"operator":"and"/"or", "operands":[...]}` 结构 | | sort 无效 | fieldId 不存在 | 先 `field get` 确认字段 ID | -| 筛选结果为空 | 条件过严或字段值不匹配 | 放宽条件验证;注意 singleSelect 筛选值用 option name 或 id | +| 筛选结果为空 | 条件过严或字段值不匹配 | 放宽条件验证;singleSelect/multipleSelect 筛选值使用 option name 字面量 | ### 2.6 导入导出错误 @@ -65,6 +66,7 @@ |-------------------|------|---------| | 导出任务超时 | 数据量大,异步任务未完成 | 用 `export data --task-id ` 轮询直到完成 | | 导入文件格式错误 | 不支持的文件格式或文件损坏 | 确认文件为 .xlsx 格式且未加密 | +| 批处理只成功一部分 | 某批或某个字段返回失败 | 保留成功 ID,输出失败 batch/item ledger,以非零状态结束;不要整批重放 | ## 3. 重试策略 @@ -106,18 +108,6 @@ dws aitable record create \ `--verbose` 会输出请求/响应的详细信息,帮助定位问题。 -### 4.2 使用 --dry-run 预览 - -```bash -dws aitable record create \ - --base-id \ - --table-id \ - --records '[...]' \ - --dry-run --format json -``` - -`--dry-run` 只预览不执行,适合在不确定参数是否正确时先验证。 - ## 5. 错误预防最佳实践 1. **写记录前先读字段结构** — `field get` 确认字段类型和 ID diff --git a/skills/multi/dingtalk-aitable/references/aitable/aitable-export-import.md b/skills/multi/dingtalk-aitable/references/aitable/aitable-export-import.md index fc755451..0030f982 100644 --- a/skills/multi/dingtalk-aitable/references/aitable/aitable-export-import.md +++ b/skills/multi/dingtalk-aitable/references/aitable/aitable-export-import.md @@ -4,6 +4,8 @@ `export data` 为异步任务:首次调用可能只返回 `taskId`,需要继续轮询。 +优先使用 `python3 scripts/aitable_export_via_task.py --scope all|table|view [...]`:它检查业务状态、持续轮询、要求 HTTPS 下载地址,并在本地文件已存在时停止而不是静默覆盖。只有需要控制底层轮询参数时才走下面的原子命令。 + > ⚠️ **`--format` 冲突警告**:`export data` 的 `--format` 是**导出格式**(excel/attachment 等),不是全局输出格式。**此命令禁止追加全局 `--format json`**,否则会覆盖导出格式导致 `INVALID_EXPORT_FORMAT` 错误。输出默认就是 JSON,无需额外指定。 ```bash @@ -28,6 +30,8 @@ dws aitable export data --base-id --task-id --timeout-ms 300 > **无需手动解析 CSV/Excel 再逐条 record create**,效率极低且容易出错。 +新建数据表导入优先使用 `python3 scripts/aitable_import_via_task.py `,脚本封装 prepare → PUT → import 并检查每一步业务状态。追加到已有表且需要字段级类型控制时,使用 `python3 scripts/import_records.py [batch_size]`;二者语义不同,不要自动互换。 + ```bash # 第 1 步:申请上传凭证 dws aitable import upload --base-id \ diff --git a/skills/multi/dingtalk-aitable/references/aitable/aitable-field.md b/skills/multi/dingtalk-aitable/references/aitable/aitable-field.md index 303cf474..3cd623af 100644 --- a/skills/multi/dingtalk-aitable/references/aitable/aitable-field.md +++ b/skills/multi/dingtalk-aitable/references/aitable/aitable-field.md @@ -14,7 +14,7 @@ Flags: --table-id string Table ID (必填) ``` -返回字段的完整配置(含 options 等)。不要假设未指定 `--table-ids` 的 `table get` 枚举结果含字段;字段目录和配置以 `field get` 返回为准。 +返回字段的完整配置(含 options 等)。`table get` 会返回精简字段目录,但写入、筛选或修改前需要类型相关完整 `config` 时,以 `field get` 返回为准。 ## field create — 创建字段 diff --git a/skills/multi/dingtalk-aitable/references/aitable/aitable-record-create.md b/skills/multi/dingtalk-aitable/references/aitable/aitable-record-create.md index ec597412..ebc00e8d 100644 --- a/skills/multi/dingtalk-aitable/references/aitable/aitable-record-create.md +++ b/skills/multi/dingtalk-aitable/references/aitable/aitable-record-create.md @@ -47,6 +47,8 @@ dws aitable record query --base-id --table-id \ 创建成功以 `data.newRecordIds[]` 为 ID 来源;不要把整个 `data` 当作单个 recordId,也不要只以退出码作为写入成功证据。 +批量追加本地 JSON/CSV 到已有表时可使用 `python3 scripts/import_records.py [batch_size]`。脚本会逐批检查业务状态、收集 `newRecordIds` 并按 ID 回读;任何批次失败或回读不完整都会输出 ledger 并以非零状态结束,已有成功批次仍会保留在结果中。 + ## cells 写入格式 各字段类型的写入格式见 [aitable-cell-value.md](./aitable-cell-value.md)。 diff --git a/skills/multi/dingtalk-aitable/references/aitable/aitable-record-query.md b/skills/multi/dingtalk-aitable/references/aitable/aitable-record-query.md index 20ed92da..c8133354 100644 --- a/skills/multi/dingtalk-aitable/references/aitable/aitable-record-query.md +++ b/skills/multi/dingtalk-aitable/references/aitable/aitable-record-query.md @@ -60,7 +60,7 @@ dws aitable record query --base-id X --table-id Y --all --cursor "上次返回 {"operator":"and","operands":[{"operator":"eq","operands":["",""]}]} ``` -> **singleSelect/multipleSelect 过滤**:filters 中可传 option id 或 option name,但建议优先用 **option id**(通过 `field get` 获取),更可靠。 +> **singleSelect/multipleSelect 过滤**:filters 中传选项名称字面量,不传 option id。选项名称先通过 `field get` 的 `config.options[]` 核对。 ## 减少响应体积 diff --git a/skills/multi/dingtalk-aitable/references/aitable/aitable-record-update.md b/skills/multi/dingtalk-aitable/references/aitable/aitable-record-update.md index eb47db53..a1e133c1 100644 --- a/skills/multi/dingtalk-aitable/references/aitable/aitable-record-update.md +++ b/skills/multi/dingtalk-aitable/references/aitable/aitable-record-update.md @@ -17,23 +17,17 @@ Flags: 只需传入需修改的字段,未传入的保持原值。每条记录必须含 recordId 和 cells。 -## cells key:优先使用 fieldId,也支持唯一字段名 +## cells key:自动化与 Agent 路径使用 fieldId -`cells` 的 key 有两种写法: +公开稳定的 Agent/自动化写法是 `fieldId`:它不受字段重命名或重名影响,并可通过 `field get` 获取。 -- fieldId(推荐):不受字段重命名或重名影响,通过 `field get` 获取。 -- 当前表内唯一的字段名:按名称精确匹配;如果存在同名字段,必须改用 fieldId。 - -同一字段同时通过 fieldId 和字段名传入时,fieldId 对应的值优先。 +Runtime 仍兼容当前表内唯一字段名,但这属于便捷兼容路径:重名或重命名会改变解析结果,不能用于脚本、跨步骤调用或从旧上下文重放。字段名与 fieldId 同时出现时也不要依赖覆盖顺序。 ```bash # 推荐:fieldId dws aitable record update --base-id --table-id \ --records '[{"recordId":"recXXX","cells":{"fldStatusId":"已完成"}}]' --format json -# 便捷写法:当前表内唯一字段名 -dws aitable record update --base-id --table-id \ - --records '[{"recordId":"recXXX","cells":{"状态":"已完成"}}]' --format json ``` ## 推荐参数形式 diff --git a/skills/multi/dingtalk-aitable/scripts/aitable_export_via_task.py b/skills/multi/dingtalk-aitable/scripts/aitable_export_via_task.py index caa66f80..6921fc24 100644 --- a/skills/multi/dingtalk-aitable/scripts/aitable_export_via_task.py +++ b/skills/multi/dingtalk-aitable/scripts/aitable_export_via_task.py @@ -54,15 +54,20 @@ def parse_json_output(raw: str) -> Optional[Dict[str, Any]]: def normalize_download_url(url: str) -> str: - if url.startswith("http://") or url.startswith("https://"): - return url - return f"https://{url}" + normalized = url if "://" in url else f"https://{url}" + parsed = urlparse(normalized) + if parsed.scheme != "https" or not parsed.hostname: + raise ValueError("downloadUrl 必须是有效的 HTTPS URL") + return normalized def download_file(url: str, output_path: Path) -> Tuple[bool, str]: req = Request(url, method="GET") try: with urlopen(req, timeout=180) as resp: + redirected = urlparse(resp.geturl()) + if redirected.scheme != "https" or not redirected.hostname: + return False, "download redirect is not HTTPS" if resp.status != 200: return False, f"download http status: {resp.status}" output_path.write_bytes(resp.read()) @@ -79,6 +84,20 @@ def fail(msg: str, code: int = 1) -> None: sys.exit(code) +def resolve_output_path(value: Optional[str], file_name: str, overwrite: bool) -> Path: + root = Path.cwd().resolve() + candidate = Path(value) if value else Path(file_name).name + output_path = candidate.resolve() if candidate.is_absolute() else (root / candidate).resolve() + try: + output_path.relative_to(root) + except ValueError as exc: + raise ValueError("输出路径必须位于当前工作目录内") from exc + if output_path.exists() and not overwrite: + raise ValueError(f"输出文件已存在:{output_path};如需覆盖请显式传 --overwrite") + output_path.parent.mkdir(parents=True, exist_ok=True) + return output_path + + def build_start_args(args: argparse.Namespace) -> list[str]: cmd = [ "aitable", @@ -113,6 +132,7 @@ def main() -> None: parser.add_argument("--output", help="本地保存路径(不传则按 fileName 保存到当前目录)") parser.add_argument("--dws", default="dws", help="dws 可执行文件路径,默认 dws") parser.add_argument("--no-download", action="store_true", help="仅返回 downloadUrl,不下载文件") + parser.add_argument("--overwrite", action="store_true", help="允许覆盖当前工作目录内的已有输出文件") args = parser.parse_args() if not validate_resource_id(args.base_id): @@ -121,6 +141,10 @@ def main() -> None: fail("scope=table/view 时必须传 --table-id") if args.scope == "view" and not args.view_id: fail("scope=view 时必须传 --view-id") + if args.table_id and not validate_resource_id(args.table_id): + fail("无效的 tableId 格式") + if args.view_id and not validate_resource_id(args.view_id): + fail("无效的 viewId 格式") print("[1/2] start export task", file=sys.stderr) rc, out, err = run_dws(args.dws, build_start_args(args), timeout_sec=120) @@ -132,7 +156,7 @@ def main() -> None: data = obj.get("data", {}) or {} status = obj.get("status") - if status == "error": + if status != "success": fail(f"export_data 返回失败: {json.dumps(obj, ensure_ascii=False)}") download_url = data.get("downloadUrl") @@ -163,7 +187,7 @@ def main() -> None: obj2 = parse_json_output(out2) if not obj2: fail(f"export_data 轮询返回非 JSON: {out2[:300]}") - if obj2.get("status") == "error": + if obj2.get("status") != "success": fail(f"export_data 轮询返回失败: {json.dumps(obj2, ensure_ascii=False)}") d2 = obj2.get("data", {}) or {} download_url = d2.get("downloadUrl") or download_url @@ -194,8 +218,11 @@ def main() -> None: print(json.dumps(result, ensure_ascii=False, indent=2)) return - norm_url = normalize_download_url(download_url) - output_path = Path(args.output).expanduser().resolve() if args.output else Path.cwd() / file_name + try: + norm_url = normalize_download_url(download_url) + output_path = resolve_output_path(args.output, file_name, args.overwrite) + except ValueError as exc: + fail(str(exc)) ok, dl_err = download_file(norm_url, output_path) if not ok: fail(f"downloadUrl 下载失败: {dl_err}") diff --git a/skills/multi/dingtalk-aitable/scripts/aitable_import_via_task.py b/skills/multi/dingtalk-aitable/scripts/aitable_import_via_task.py index fea1ad5e..6abb1f45 100644 --- a/skills/multi/dingtalk-aitable/scripts/aitable_import_via_task.py +++ b/skills/multi/dingtalk-aitable/scripts/aitable_import_via_task.py @@ -22,6 +22,7 @@ import sys from pathlib import Path from typing import Any, Dict, Optional, Tuple from urllib.error import HTTPError, URLError +from urllib.parse import urlparse from urllib.request import Request, urlopen RESOURCE_ID_PATTERN = re.compile(r"^[A-Za-z0-9_-]{8,128}$") @@ -52,6 +53,9 @@ def parse_json_output(raw: str) -> Optional[Dict[str, Any]]: def put_file(upload_url: str, file_path: Path) -> Tuple[bool, str]: + parsed = urlparse(upload_url) + if parsed.scheme != "https" or not parsed.hostname: + return False, "uploadUrl must be a valid HTTPS URL" payload = file_path.read_bytes() req = Request(upload_url, data=payload, method="PUT") # 关键:清空 Content-Type,避免 SignatureDoesNotMatch。 diff --git a/skills/multi/dingtalk-aitable/scripts/bulk_add_fields.py b/skills/multi/dingtalk-aitable/scripts/bulk_add_fields.py index 3ba88313..a03d00c0 100644 --- a/skills/multi/dingtalk-aitable/scripts/bulk_add_fields.py +++ b/skills/multi/dingtalk-aitable/scripts/bulk_add_fields.py @@ -1,273 +1,276 @@ #!/usr/bin/env python3 -""" -批量添加字段到钉钉 AI 表格数据表(新版 schema) +"""批量添加字段到钉钉 AI 表格数据表。 用法: - python bulk_add_fields.py fields.json + python3 bulk_add_fields.py fields.json -fields.json 格式: - [ - {"fieldName": "字段 1", "type": "text"}, - {"fieldName": "字段 2", "type": "number", "config": {"formatter": "INT"}}, - {"fieldName": "字段 3", "type": "singleSelect", "config": {"options": [{"name": "高"}]}} - ] - -兼容写法: -- name 会自动映射为 fieldName -- phone 会自动映射为 telephone +脚本检查业务状态和逐项结果,并回读成功字段。部分成功会输出 ledger,但整体以非零 +状态结束。fields.json 单次最多 15 个字段;name 会映射为 fieldName,phone 会映射为 +telephone。 """ -import sys +from __future__ import annotations + +import argparse import json -import subprocess import os import re +import subprocess +import sys from pathlib import Path -from typing import Union, List, Dict, Any, Optional, Tuple +from typing import Any, Dict, List, Optional, Tuple, Union JsonData = Union[List[Any], Dict[str, Any]] - MAX_FILE_SIZE = 10 * 1024 * 1024 -ALLOWED_FILE_EXTENSIONS = ['.json'] -RESOURCE_ID_PATTERN = re.compile(r'^[A-Za-z0-9_-]{8,128}$') +RESOURCE_ID_PATTERN = re.compile(r"^[A-Za-z0-9_-]{8,128}$") ALLOWED_FIELD_TYPES = { - 'text', 'number', 'singleSelect', 'multipleSelect', 'date', 'currency', - 'user', 'department', 'group', 'progress', 'rating', 'checkbox', - 'attachment', 'url', 'richText', 'telephone', 'email', 'idCard', - 'barcode', 'geolocation', 'address', 'primaryDoc', 'formula', - 'unidirectionalLink', 'bidirectionalLink', 'lookup', 'filterUp', - 'creator', 'lastModifier', 'createdTime', 'lastModifiedTime', -} -FIELD_TYPE_ALIASES = { - 'phone': 'telephone', + "text", "number", "singleSelect", "multipleSelect", "date", "currency", + "user", "department", "group", "progress", "rating", "checkbox", + "attachment", "url", "richText", "telephone", "email", "idCard", + "barcode", "geolocation", "address", "primaryDoc", "formula", + "unidirectionalLink", "bidirectionalLink", "lookup", "filterUp", + "creator", "lastModifier", "createdTime", "lastModifiedTime", } def resolve_safe_path(path: str, allowed_root: Optional[str] = None) -> Path: - if allowed_root is None: - allowed_root = os.environ.get('OPENCLAW_WORKSPACE', os.getcwd()) - - allowed_root = Path(allowed_root).resolve() - target_path = ( - Path(path).resolve() - if Path(path).is_absolute() - else (Path.cwd() / path).resolve() - ) - + root = Path(allowed_root or os.environ.get("OPENCLAW_WORKSPACE", os.getcwd())).resolve() + candidate = Path(path) + target = candidate.resolve() if candidate.is_absolute() else (Path.cwd() / candidate).resolve() try: - target_path.relative_to(allowed_root) - return target_path - except ValueError: - raise ValueError( - f"路径超出允许范围:{path}\n" - f"目标路径:{target_path}\n" - f"允许根目录:{allowed_root}\n" - f"提示:设置 OPENCLAW_WORKSPACE 环境变量或确保文件在工作目录内" - ) + target.relative_to(root) + except ValueError as exc: + raise ValueError(f"路径超出允许范围:{path}(允许根目录:{root})") from exc + return target def validate_resource_id(resource_id: str) -> bool: - return bool(resource_id and RESOURCE_ID_PATTERN.match(resource_id.strip())) + return bool(resource_id and RESOURCE_ID_PATTERN.fullmatch(resource_id.strip())) -def validate_file_extension(filename: str, allowed_extensions: list) -> bool: - return any(filename.lower().endswith(ext) for ext in allowed_extensions) - - -def safe_json_load(file_path: Path, max_size: int = MAX_FILE_SIZE) -> JsonData: - file_size = file_path.stat().st_size - if file_size > max_size: - raise ValueError( - f"文件过大:{file_size:,} 字节 (限制:{max_size:,} 字节)" - ) - with open(file_path, 'r', encoding='utf-8') as f: - return json.load(f) +def safe_json_load(file_path: Path) -> JsonData: + if file_path.stat().st_size > MAX_FILE_SIZE: + raise ValueError(f"文件过大(限制 {MAX_FILE_SIZE:,} 字节)") + with file_path.open("r", encoding="utf-8") as stream: + return json.load(stream) def normalize_field_config(field: Dict[str, Any]) -> Dict[str, Any]: normalized = dict(field) - if 'fieldName' not in normalized and 'name' in normalized: - normalized['fieldName'] = normalized.pop('name') - normalized['type'] = FIELD_TYPE_ALIASES.get( - normalized.get('type', 'text'), normalized.get('type', 'text') - ) + if "fieldName" not in normalized and "name" in normalized: + normalized["fieldName"] = normalized.pop("name") + if normalized.get("type") == "phone": + normalized["type"] = "telephone" return normalized -def validate_field_config(field: Dict[str, Any]) -> Tuple[bool, str]: +def validate_field_config(field: Any) -> Tuple[bool, str]: if not isinstance(field, dict): - return False, '字段配置必须是对象' - - field = normalize_field_config(field) - - if 'fieldName' not in field: - return False, '缺少必需字段:fieldName' - if not isinstance(field['fieldName'], str) or not field['fieldName'].strip(): - return False, 'fieldName 必须是非空字符串' - - field_type = field.get('type', 'text') + return False, "字段配置必须是对象" + normalized = normalize_field_config(field) + name = normalized.get("fieldName") + if not isinstance(name, str) or not name.strip(): + return False, "fieldName 必须是非空字符串" + field_type = normalized.get("type", "text") if field_type not in ALLOWED_FIELD_TYPES: return False, f"不支持的字段类型:{field_type}" - - config = field.get('config') + config = normalized.get("config") if config is not None and not isinstance(config, dict): - return False, 'config 必须是对象' - - if field_type in {'singleSelect', 'multipleSelect'}: - options = (config or {}).get('options') - if not options or not isinstance(options, list): - return False, ( - 'singleSelect / multipleSelect 必须提供 config.options 数组' - ) - - if field_type in {'unidirectionalLink', 'bidirectionalLink'}: - linked_table_id = (config or {}).get('linkedTableId') - if not linked_table_id or not validate_resource_id(linked_table_id): - return False, ( - '关联字段必须提供合法的 config.linkedTableId(目标 Table ID)' - ) - - if field_type == 'lookup': - cfg = config or {} - if not cfg.get('associateField'): - return False, 'lookup 必须提供 config.associateField(本表关联字段的 fieldId)' - if not cfg.get('valuesField'): - return False, 'lookup 必须提供 config.valuesField(关联目标表中要取值的字段 fieldId)' - if not cfg.get('aggregator'): - return False, 'lookup 必须提供 config.aggregator(SUM/AVERAGE/COUNT/MAX/MIN/CONCATENATE)' - - if field_type == 'filterUp': - cfg = config or {} - if not cfg.get('targetSheet'): - return False, 'filterUp 必须提供 config.targetSheet(目标 Table ID)' - filters = cfg.get('filters') - if not filters or not isinstance(filters, list): - return False, 'filterUp 必须提供 config.filters(至少一条筛选规则)' - if not cfg.get('valuesField'): - return False, 'filterUp 必须提供 config.valuesField(目标表中要取值的字段 fieldId)' - if not cfg.get('aggregator'): - return False, 'filterUp 必须提供 config.aggregator(SUM/AVERAGE/COUNT/MAX/MIN/CONCATENATE)' - - return True, '' + return False, "config 必须是对象" + ai_config = normalized.get("aiConfig") + if ai_config is not None and not isinstance(ai_config, dict): + return False, "aiConfig 必须是对象" + if field_type in {"singleSelect", "multipleSelect"}: + options = (config or {}).get("options") + if not isinstance(options, list) or not options: + return False, "singleSelect / multipleSelect 必须提供 config.options 数组" + if field_type in {"unidirectionalLink", "bidirectionalLink"}: + linked_table_id = (config or {}).get("linkedTableId") + if not validate_resource_id(str(linked_table_id or "")): + return False, "关联字段必须提供合法的 config.linkedTableId" + if field_type == "lookup": + required = ("associateField", "valuesField", "aggregator") + missing = [name for name in required if not (config or {}).get(name)] + if missing: + return False, f"lookup 缺少 config.{missing[0]}" + if field_type == "filterUp": + required = ("targetSheet", "filters", "valuesField", "aggregator") + missing = [name for name in required if not (config or {}).get(name)] + if missing: + return False, f"filterUp 缺少 config.{missing[0]}" + if not isinstance((config or {}).get("filters"), list): + return False, "filterUp config.filters 必须是数组" + return True, "" -def build_fields_json(fields: List[Dict[str, Any]]) -> str: - """构建 --fields 参数的 JSON 字符串。""" - payload_fields = [] +def build_fields_payload(fields: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + payload: List[Dict[str, Any]] = [] for field in fields: normalized = normalize_field_config(field) item: Dict[str, Any] = { - 'fieldName': normalized['fieldName'].strip(), - 'type': normalized.get('type', 'text'), + "fieldName": normalized["fieldName"].strip(), + "type": normalized.get("type", "text"), } - if 'config' in normalized and normalized['config'] is not None: - item['config'] = normalized['config'] - payload_fields.append(item) - return json.dumps(payload_fields, ensure_ascii=False) + for key in ("config", "aiConfig"): + if normalized.get(key) is not None: + item[key] = normalized[key] + payload.append(item) + return payload -def run_dws(args: List[str]) -> Optional[Dict[str, Any]]: - if not args: - print('错误:空命令') - return None - - cmd = ['dws'] + args +def run_dws(dws_bin: str, args: List[str], timeout_sec: int = 120) -> Tuple[Optional[Dict[str, Any]], str]: try: result = subprocess.run( - cmd, capture_output=True, text=True, timeout=60 + [dws_bin] + args, + capture_output=True, + text=True, + timeout=timeout_sec, ) - if result.returncode != 0: - print(f"错误:{result.stderr.strip()}") - return None - try: - return json.loads(result.stdout) - except json.JSONDecodeError as e: - print(f"无法解析响应:{result.stdout[:200]}...") - print(f"JSON 解析错误:{e}") - return None except subprocess.TimeoutExpired: - print('错误:命令执行超时(60 秒)') - return None + return None, f"dws 命令超时({timeout_sec} 秒)" except FileNotFoundError: - print('错误:未找到 dws 命令,请确认已安装') - return None + return None, f"未找到 dws 命令:{dws_bin}" + if result.returncode != 0: + return None, (result.stderr or result.stdout).strip() or f"dws 退出码 {result.returncode}" + try: + payload = json.loads(result.stdout) + except json.JSONDecodeError as exc: + return None, f"dws 返回非 JSON:{exc}" + if not isinstance(payload, dict): + return None, "dws 返回的 JSON 不是对象" + if payload.get("status") != "success": + detail = payload.get("summary") or payload.get("error") or payload + return None, f"业务失败:{detail}" + return payload, "" + + +def normalize_result_item(index: int, item: Any) -> Dict[str, Any]: + if not isinstance(item, dict): + return {"index": index, "status": "failed", "error": "逐项结果不是对象"} + field_id = item.get("fieldId") or (item.get("data") or {}).get("fieldId") + succeeded = item.get("success") is True or item.get("status") == "success" + if "success" not in item and "status" not in item: + succeeded = bool(field_id) + result: Dict[str, Any] = { + "index": index, + "status": "success" if succeeded and field_id else "failed", + } + if field_id: + result["fieldId"] = str(field_id) + if result["status"] != "success": + result["error"] = item.get("reason") or item.get("error") or "字段创建未返回 fieldId" + return result def bulk_add_fields( - base_id: str, table_id: str, fields_file: str -) -> bool: + base_id: str, + table_id: str, + fields: List[Dict[str, Any]], + dws_bin: str = "dws", +) -> Dict[str, Any]: + created, error = run_dws( + dws_bin, + [ + "aitable", "field", "create", + "--base-id", base_id, + "--table-id", table_id, + "--fields", json.dumps(build_fields_payload(fields), ensure_ascii=False), + "--format", "json", + ], + ) + if created is None: + return { + "status": "failed", + "complete": False, + "requestedCount": len(fields), + "verifiedCount": 0, + "ledger": [{"status": "failed", "error": error}], + } + + data = created.get("data") if isinstance(created.get("data"), dict) else {} + raw_results = data.get("results") + if not isinstance(raw_results, list) or len(raw_results) != len(fields): + return { + "status": "failed", + "complete": False, + "requestedCount": len(fields), + "verifiedCount": 0, + "ledger": [{"status": "failed", "error": "响应缺少与输入数量一致的 data.results[]"}], + } + ledger = [normalize_result_item(index, item) for index, item in enumerate(raw_results)] + field_ids = [item["fieldId"] for item in ledger if item["status"] == "success"] + verified_ids: List[str] = [] + if field_ids: + queried, query_error = run_dws( + dws_bin, + [ + "aitable", "field", "get", + "--base-id", base_id, + "--table-id", table_id, + "--field-ids", ",".join(field_ids), + "--format", "json", + ], + ) + if queried is None: + for item in ledger: + if item["status"] == "success": + item["status"] = "verify_failed" + item["error"] = query_error + else: + query_data = queried.get("data") if isinstance(queried.get("data"), dict) else {} + raw_fields = query_data.get("fields") or query_data.get("items") or [] + found = { + str(item.get("fieldId")) + for item in raw_fields + if isinstance(item, dict) and item.get("fieldId") + } + verified_ids = [field_id for field_id in field_ids if field_id in found] + for item in ledger: + if item.get("fieldId") in field_ids and item.get("fieldId") not in found: + item["status"] = "verify_failed" + item["error"] = "回读未返回该字段" + complete = len(verified_ids) == len(fields) and all(item["status"] == "success" for item in ledger) + return { + "status": "success" if complete else "partial", + "complete": complete, + "requestedCount": len(fields), + "verifiedCount": len(verified_ids), + "fieldIds": verified_ids, + "ledger": ledger, + } + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("base_id") + parser.add_argument("table_id") + parser.add_argument("fields_file") + parser.add_argument("--dws", default="dws", help="dws 可执行文件路径") + args = parser.parse_args() + if not validate_resource_id(args.base_id): + parser.error("无效的 baseId 格式") + if not validate_resource_id(args.table_id): + parser.error("无效的 tableId 格式") try: - safe_path = resolve_safe_path(fields_file) - except ValueError as e: - print(f"路径验证失败:{e}") - return False - - if not validate_file_extension(fields_file, ALLOWED_FILE_EXTENSIONS): - print(f"错误:只允许 {', '.join(ALLOWED_FILE_EXTENSIONS)} 文件") - return False - if not safe_path.exists(): - print(f"错误:文件不存在:{safe_path}") - return False - - try: - fields = safe_json_load(safe_path) - except ValueError as e: - print(f"错误:{e}") - return False - except json.JSONDecodeError as e: - print(f"错误:JSON 格式无效:{e}") - return False - - if not isinstance(fields, list) or not fields: - print('错误:fields.json 必须是非空 JSON 数组') - return False - if len(fields) > 15: - print('错误:单次最多创建 15 个字段,请拆分后重试') - return False - - for i, field in enumerate(fields): - valid, error = validate_field_config(field) - if not valid: - print(f"错误:字段 #{i+1} 配置无效:{error}") - return False - - fields_json = build_fields_json(fields) - result = run_dws([ - 'aitable', 'field', 'create', - '--base-id', base_id, - '--table-id', table_id, - '--fields', fields_json, - '--format', 'json', - ]) - - if not result: - return False - + path = resolve_safe_path(args.fields_file) + if path.suffix.lower() != ".json" or not path.exists() or not path.is_file(): + raise ValueError("fields_file 必须是工作区内存在的 .json 文件") + fields = safe_json_load(path) + if not isinstance(fields, list) or not fields: + raise ValueError("fields.json 必须是非空 JSON 数组") + if len(fields) > 15: + raise ValueError("单次最多创建 15 个字段,请拆分后重试") + for index, field in enumerate(fields, start=1): + valid, error = validate_field_config(field) + if not valid: + raise ValueError(f"字段 #{index} 配置无效:{error}") + result = bulk_add_fields(args.base_id, args.table_id, fields, args.dws) + except (ValueError, OSError, json.JSONDecodeError) as exc: + print(f"错误:{exc}", file=sys.stderr) + sys.exit(1) print(json.dumps(result, ensure_ascii=False, indent=2)) - return True + sys.exit(0 if result["complete"] else 2) -def main(): - if len(sys.argv) != 4: - print(__doc__) - print('用法示例:') - print(' python bulk_add_fields.py basexxx tablexxx fields.json') - sys.exit(1) - - base_id = sys.argv[1] - table_id = sys.argv[2] - fields_file = sys.argv[3] - - if not validate_resource_id(base_id): - print('错误:无效的 baseId 格式') - sys.exit(1) - if not validate_resource_id(table_id): - print('错误:无效的 tableId 格式') - sys.exit(1) - - success = bulk_add_fields(base_id, table_id, fields_file) - sys.exit(0 if success else 1) - - -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/skills/multi/dingtalk-aitable/scripts/import_records.py b/skills/multi/dingtalk-aitable/scripts/import_records.py index a275806e..a192f63e 100644 --- a/skills/multi/dingtalk-aitable/scripts/import_records.py +++ b/skills/multi/dingtalk-aitable/scripts/import_records.py @@ -1,333 +1,264 @@ #!/usr/bin/env python3 -""" -从 CSV / JSON 批量导入记录到钉钉 AI 表格(新版 schema) +"""批量追加 CSV / JSON 记录到已有钉钉 AI 表格数据表。 用法: - python import_records.py data.csv [batch_size] - python import_records.py data.json [batch_size] + python3 import_records.py data.csv [batch_size] + python3 import_records.py data.json [batch_size] -说明: -- CSV 表头默认视为 fieldId -- JSON 支持两种格式: - 1. [{"cells": {"fldxxx": "value"}}, ...] - 2. [{"fldxxx": "value"}, ...] # 会自动包装成 cells - -⚠️ CSV 自动类型转换风险: - CSV 读入的所有 cell 都是 string,本脚本会尝试自动识别 'true'/'false'/数字 - 并转成对应类型(避免 text 字段塞入纯文本数字)。但当 fieldId 对应的字段是 - text / telephone / idCard / barcode 这类"字符串形数字"字段时,自动转 int / float - 会让 server 拒绝(字段类型不匹配)。这种情况建议改用 JSON 格式(自己显式控制类型), - 或在 CSV 写入前给字段值前缀加引号 / 改为非纯数字。 +CSV 表头必须是 fieldId。CSV 值保持字符串;需要布尔、数组、对象等精确类型时使用 +JSON。JSON 支持 [{"cells": {...}}] 或 [{"fld...": value}] 两种格式。 +脚本逐批检查业务状态、提取 newRecordIds 并回读验证;部分成功会保留 ledger, +但整体以非零状态结束。 """ -import sys +from __future__ import annotations + +import argparse import csv import json -import subprocess import os import re +import subprocess +import sys from pathlib import Path -from typing import Union, List, Dict, Any, Optional, Tuple +from typing import Any, Dict, List, Optional, Tuple, Union JsonData = Union[List[Any], Dict[str, Any]] -RecordDict = Dict[str, str] - MAX_FILE_SIZE = 50 * 1024 * 1024 -ALLOWED_CSV_EXTENSIONS = ['.csv'] -ALLOWED_JSON_EXTENSIONS = ['.json'] -RESOURCE_ID_PATTERN = re.compile(r'^[A-Za-z0-9_-]{8,128}$') +RESOURCE_ID_PATTERN = re.compile(r"^[A-Za-z0-9_-]{8,128}$") MAX_RECORDS_PER_BATCH = 100 DEFAULT_BATCH_SIZE = 50 -def resolve_safe_path( - path: str, allowed_root: Optional[str] = None -) -> Path: - if allowed_root is None: - allowed_root = os.environ.get('OPENCLAW_WORKSPACE', os.getcwd()) - allowed_root = Path(allowed_root).resolve() - target_path = ( - Path(path).resolve() - if Path(path).is_absolute() - else (Path.cwd() / path).resolve() - ) +def resolve_safe_path(path: str, allowed_root: Optional[str] = None) -> Path: + root = Path(allowed_root or os.environ.get("OPENCLAW_WORKSPACE", os.getcwd())).resolve() + candidate = Path(path) + target = candidate.resolve() if candidate.is_absolute() else (Path.cwd() / candidate).resolve() try: - target_path.relative_to(allowed_root) - return target_path - except ValueError: - raise ValueError( - f"路径超出允许范围:{path}\n" - f"目标路径:{target_path}\n" - f"允许根目录:{allowed_root}\n" - f"提示:设置 OPENCLAW_WORKSPACE 环境变量或确保文件在工作目录内" - ) + target.relative_to(root) + except ValueError as exc: + raise ValueError(f"路径超出允许范围:{path}(允许根目录:{root})") from exc + return target def validate_resource_id(resource_id: str) -> bool: - return bool( - resource_id and RESOURCE_ID_PATTERN.match(resource_id.strip()) - ) + return bool(resource_id and RESOURCE_ID_PATTERN.fullmatch(resource_id.strip())) -def validate_file_extension( - filename: str, allowed_extensions: list -) -> bool: - return any(filename.lower().endswith(ext) for ext in allowed_extensions) +def safe_csv_load(file_path: Path) -> List[Dict[str, str]]: + if file_path.stat().st_size > MAX_FILE_SIZE: + raise ValueError(f"文件过大(限制 {MAX_FILE_SIZE:,} 字节)") + with file_path.open("r", encoding="utf-8-sig", newline="") as stream: + reader = csv.DictReader(stream) + if not reader.fieldnames or any(not str(name).strip() for name in reader.fieldnames): + raise ValueError("CSV 必须包含非空 fieldId 表头") + return list(reader) -def safe_csv_load( - file_path: Path, max_size: int = MAX_FILE_SIZE -) -> List[RecordDict]: - file_size = file_path.stat().st_size - if file_size > max_size: - raise ValueError( - f"文件过大:{file_size:,} 字节 (限制:{max_size:,} 字节)" - ) - with open(file_path, 'r', encoding='utf-8', newline='') as f: - return list(csv.DictReader(f)) - - -def safe_json_load( - file_path: Path, max_size: int = MAX_FILE_SIZE -) -> JsonData: - file_size = file_path.stat().st_size - if file_size > max_size: - raise ValueError( - f"文件过大:{file_size:,} 字节 (限制:{max_size:,} 字节)" - ) - with open(file_path, 'r', encoding='utf-8') as f: - return json.load(f) - - -def sanitize_record_value( - value: Any, -) -> Optional[Union[str, int, float, bool, list, dict]]: - if value is None: - return None - if isinstance(value, (bool, int, float, list, dict)): - return value - if not isinstance(value, str): - return value - if not value.strip(): - return None - - value = value.strip() - if value.lower() == 'true': - return True - if value.lower() == 'false': - return False - - try: - if '.' in value: - return float(value) - return int(value) - except ValueError: - return value +def safe_json_load(file_path: Path) -> JsonData: + if file_path.stat().st_size > MAX_FILE_SIZE: + raise ValueError(f"文件过大(限制 {MAX_FILE_SIZE:,} 字节)") + with file_path.open("r", encoding="utf-8") as stream: + return json.load(stream) def normalize_record(record: Dict[str, Any]) -> Dict[str, Any]: - if 'cells' in record and isinstance(record['cells'], dict): - cells = record['cells'] - else: - cells = record - normalized = {} + cells = record.get("cells") if isinstance(record.get("cells"), dict) else record + normalized: Dict[str, Any] = {} for key, value in cells.items(): - sanitized = sanitize_record_value(value) - if sanitized is not None: - normalized[key] = sanitized - return {'cells': normalized} + if not isinstance(key, str) or not key.strip(): + continue + if value is None: + continue + if isinstance(value, str): + value = value.strip() + if not value: + continue + normalized[key.strip()] = value + return {"cells": normalized} -def validate_record(record: Dict[str, Any]) -> Tuple[bool, str]: +def validate_record(record: Any) -> Tuple[bool, str]: if not isinstance(record, dict): - return False, '记录必须是对象' - normalized = normalize_record(record) - cells = normalized.get('cells', {}) - if not cells or not isinstance(cells, dict): - return False, '记录必须包含非空 cells 对象' - return True, '' + return False, "记录必须是对象" + cells = normalize_record(record).get("cells") + if not isinstance(cells, dict) or not cells: + return False, "记录必须包含非空 cells 对象" + return True, "" -def run_dws(args: List[str]) -> Optional[Dict[str, Any]]: - if not args: - print('错误:空命令') - return None - cmd = ['dws'] + args +def run_dws(dws_bin: str, args: List[str], timeout_sec: int = 120) -> Tuple[Optional[Dict[str, Any]], str]: try: result = subprocess.run( - cmd, capture_output=True, text=True, timeout=120 + [dws_bin] + args, + capture_output=True, + text=True, + timeout=timeout_sec, ) - if result.returncode != 0: - print(f"错误:{result.stderr.strip()}") - return None - try: - return json.loads(result.stdout) - except json.JSONDecodeError as e: - print(f"无法解析响应:{result.stdout[:200]}...") - print(f"JSON 解析错误:{e}") - return None except subprocess.TimeoutExpired: - print('错误:命令执行超时(120 秒)') - return None + return None, f"dws 命令超时({timeout_sec} 秒)" except FileNotFoundError: - print('错误:未找到 dws 命令,请确认已安装') - return None - - -def import_from_csv( - base_id: str, table_id: str, csv_file: str, - batch_size: int = DEFAULT_BATCH_SIZE, -) -> bool: + return None, f"未找到 dws 命令:{dws_bin}" + if result.returncode != 0: + return None, (result.stderr or result.stdout).strip() or f"dws 退出码 {result.returncode}" try: - safe_path = resolve_safe_path(csv_file) - except ValueError as e: - print(f"路径验证失败:{e}") - return False + payload = json.loads(result.stdout) + except json.JSONDecodeError as exc: + return None, f"dws 返回非 JSON:{exc}" + if not isinstance(payload, dict): + return None, "dws 返回的 JSON 不是对象" + if payload.get("status") != "success": + detail = payload.get("summary") or payload.get("error") or payload + return None, f"业务失败:{detail}" + return payload, "" - if not validate_file_extension(csv_file, ALLOWED_CSV_EXTENSIONS): - print(f"错误:只允许 {', '.join(ALLOWED_CSV_EXTENSIONS)} 文件") - return False - if not safe_path.exists(): - print(f"错误:文件不存在:{safe_path}") - return False - try: - rows = safe_csv_load(safe_path) - except ValueError as e: - print(f"错误:{e}") - return False - except csv.Error as e: - print(f"错误:CSV 格式无效:{e}") - return False - - if not rows: - print('错误:CSV 文件为空或没有有效数据行') - return False - - records = [ - normalize_record(row) - for row in rows - if normalize_record(row)['cells'] +def extract_query_record_ids(payload: Dict[str, Any]) -> List[str]: + data = payload.get("data") if isinstance(payload.get("data"), dict) else {} + records = data.get("records") or data.get("items") or [] + if not isinstance(records, list): + return [] + return [ + str(item.get("recordId")) + for item in records + if isinstance(item, dict) and item.get("recordId") ] - return import_records(base_id, table_id, records, batch_size) - - -def import_from_json( - base_id: str, table_id: str, json_file: str, - batch_size: int = DEFAULT_BATCH_SIZE, -) -> bool: - try: - safe_path = resolve_safe_path(json_file) - except ValueError as e: - print(f"路径验证失败:{e}") - return False - - if not validate_file_extension(json_file, ALLOWED_JSON_EXTENSIONS): - print(f"错误:只允许 {', '.join(ALLOWED_JSON_EXTENSIONS)} 文件") - return False - if not safe_path.exists(): - print(f"错误:文件不存在:{safe_path}") - return False - - try: - records = safe_json_load(safe_path) - except ValueError as e: - print(f"错误:{e}") - return False - except json.JSONDecodeError as e: - print(f"错误:JSON 格式无效:{e}") - return False - - if not isinstance(records, list) or not records: - print('错误:JSON 文件必须是非空数组') - return False - - for i, record in enumerate(records): - valid, error = validate_record(record) - if not valid: - print(f"错误:记录 #{i+1} 格式无效:{error}") - return False - - return import_records( - base_id, table_id, - [normalize_record(r) for r in records], batch_size, - ) def import_records( - base_id: str, table_id: str, - records: List[Dict[str, Any]], batch_size: int, -) -> bool: - if batch_size <= 0: - print('错误:batch_size 必须大于 0') - return False - if batch_size > MAX_RECORDS_PER_BATCH: - batch_size = MAX_RECORDS_PER_BATCH + base_id: str, + table_id: str, + records: List[Dict[str, Any]], + batch_size: int, + dws_bin: str = "dws", +) -> Dict[str, Any]: + if batch_size <= 0 or batch_size > MAX_RECORDS_PER_BATCH: + raise ValueError(f"batch_size 必须在 1..{MAX_RECORDS_PER_BATCH} 之间") + ledger: List[Dict[str, Any]] = [] + verified_ids: List[str] = [] total_batches = (len(records) + batch_size - 1) // batch_size - success = True - - for i in range(0, len(records), batch_size): - batch = records[i:i + batch_size] - batch_num = (i // batch_size) + 1 - records_json = json.dumps(batch, ensure_ascii=False) - result = run_dws([ - 'aitable', 'record', 'create', - '--base-id', base_id, - '--table-id', table_id, - '--records', records_json, - '--format', 'json', - ]) - if result: - print( - f"[{batch_num}/{total_batches}] " - f"✓ 已提交 {len(batch)} 条记录" - ) - else: - print(f"[{batch_num}/{total_batches}] ✗ 导入失败") - success = False - - return success - - -def main(): - if len(sys.argv) < 4 or len(sys.argv) > 5: - print(__doc__) - print('用法示例:') - print( - ' python import_records.py basexxx tablexxx data.csv 50' + for start in range(0, len(records), batch_size): + batch = records[start : start + batch_size] + batch_number = start // batch_size + 1 + print(f"[{batch_number}/{total_batches}] 创建 {len(batch)} 条记录", file=sys.stderr) + created, error = run_dws( + dws_bin, + [ + "aitable", "record", "create", + "--base-id", base_id, + "--table-id", table_id, + "--records", json.dumps(batch, ensure_ascii=False), + "--format", "json", + ], ) - sys.exit(1) + entry: Dict[str, Any] = { + "batch": batch_number, + "inputCount": len(batch), + "status": "failed", + "recordIds": [], + } + if created is None: + entry["error"] = error + ledger.append(entry) + continue - base_id = sys.argv[1] - table_id = sys.argv[2] - input_file = sys.argv[3] - batch_size = ( - int(sys.argv[4]) if len(sys.argv) == 5 - else DEFAULT_BATCH_SIZE - ) + data = created.get("data") if isinstance(created.get("data"), dict) else {} + record_ids = data.get("newRecordIds") + if not isinstance(record_ids, list) or len(record_ids) != len(batch) or not all(record_ids): + entry["error"] = "创建响应缺少与输入数量一致的 data.newRecordIds[]" + ledger.append(entry) + continue + record_ids = [str(value) for value in record_ids] + entry["recordIds"] = record_ids - if not validate_resource_id(base_id): - print('错误:无效的 baseId 格式') - sys.exit(1) - if not validate_resource_id(table_id): - print('错误:无效的 tableId 格式') - sys.exit(1) - - if input_file.lower().endswith('.csv'): - success = import_from_csv( - base_id, table_id, input_file, batch_size - ) - elif input_file.lower().endswith('.json'): - success = import_from_json( - base_id, table_id, input_file, batch_size + queried, query_error = run_dws( + dws_bin, + [ + "aitable", "record", "query", + "--base-id", base_id, + "--table-id", table_id, + "--record-ids", ",".join(record_ids), + "--format", "json", + ], ) + if queried is None: + entry["status"] = "verify_failed" + entry["error"] = query_error + ledger.append(entry) + continue + found = set(extract_query_record_ids(queried)) + missing = [record_id for record_id in record_ids if record_id not in found] + if missing: + entry["status"] = "verify_failed" + entry["missingRecordIds"] = missing + entry["error"] = "回读未返回全部新记录" + ledger.append(entry) + continue + entry["status"] = "success" + verified_ids.extend(record_ids) + ledger.append(entry) + + complete = all(item["status"] == "success" for item in ledger) + return { + "status": "success" if complete else "partial", + "complete": complete, + "requestedCount": len(records), + "verifiedCount": len(verified_ids), + "recordIds": verified_ids, + "ledger": ledger, + } + + +def load_records(input_file: str) -> List[Dict[str, Any]]: + path = resolve_safe_path(input_file) + if not path.exists() or not path.is_file(): + raise ValueError(f"文件不存在或不可读:{path}") + suffix = path.suffix.lower() + if suffix == ".csv": + raw: Any = safe_csv_load(path) + elif suffix == ".json": + raw = safe_json_load(path) else: - print('错误:仅支持 .csv 或 .json 文件') + raise ValueError("仅支持 .csv 或 .json 文件") + if not isinstance(raw, list) or not raw: + raise ValueError("输入文件必须包含至少一条记录") + normalized: List[Dict[str, Any]] = [] + for index, record in enumerate(raw, start=1): + valid, error = validate_record(record) + if not valid: + raise ValueError(f"记录 #{index} 格式无效:{error}") + normalized.append(normalize_record(record)) + return normalized + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("base_id") + parser.add_argument("table_id") + parser.add_argument("input_file") + parser.add_argument("batch_size", nargs="?", type=int, default=DEFAULT_BATCH_SIZE) + parser.add_argument("--dws", default="dws", help="dws 可执行文件路径") + args = parser.parse_args() + if not validate_resource_id(args.base_id): + parser.error("无效的 baseId 格式") + if not validate_resource_id(args.table_id): + parser.error("无效的 tableId 格式") + try: + records = load_records(args.input_file) + result = import_records( + args.base_id, + args.table_id, + records, + args.batch_size, + args.dws, + ) + except (ValueError, OSError, csv.Error, json.JSONDecodeError) as exc: + print(f"错误:{exc}", file=sys.stderr) sys.exit(1) - - sys.exit(0 if success else 1) + print(json.dumps(result, ensure_ascii=False, indent=2)) + sys.exit(0 if result["complete"] else 2) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/skills/multi/dingtalk-aitable/scripts/upload_attachment.py b/skills/multi/dingtalk-aitable/scripts/upload_attachment.py index 5b816a7e..63abbfd1 100644 --- a/skills/multi/dingtalk-aitable/scripts/upload_attachment.py +++ b/skills/multi/dingtalk-aitable/scripts/upload_attachment.py @@ -18,6 +18,7 @@ --records '[{"cells":{"fldAttachId":[{"fileToken":"ft_xxx"}]}}]' --format json """ +import argparse import sys import json import subprocess @@ -28,6 +29,7 @@ from pathlib import Path from typing import Optional, Dict, Any from urllib.request import Request, urlopen from urllib.error import HTTPError, URLError +from urllib.parse import urlparse RESOURCE_ID_PATTERN = re.compile(r'^[A-Za-z0-9_-]{8,128}$') MAX_FILE_SIZE = 100 * 1024 * 1024 # 100MB @@ -43,16 +45,24 @@ def detect_mime_type(file_path: Path) -> str: return mime_type or 'application/octet-stream' -def run_dws(args: list) -> Optional[Dict[str, Any]]: +def run_dws(args: list, dws_bin: str = 'dws') -> Optional[Dict[str, Any]]: """调用 dws 命令并返回解析后的 JSON 结果。""" - cmd = ['dws'] + args + cmd = [dws_bin] + args try: result = subprocess.run(cmd, capture_output=True, text=True, timeout=60) if result.returncode != 0: print(f"错误:dws 命令失败: {result.stderr.strip()}", file=sys.stderr) return None try: - return json.loads(result.stdout) + payload = json.loads(result.stdout) + if not isinstance(payload, dict): + print('错误:dws 响应 JSON 不是对象', file=sys.stderr) + return None + if payload.get('status') != 'success': + detail = payload.get('summary') or payload.get('error') or payload + print(f"错误:dws 业务失败: {detail}", file=sys.stderr) + return None + return payload except json.JSONDecodeError: print(f"错误:无法解析 dws 响应: {result.stdout[:300]}", file=sys.stderr) return None @@ -66,6 +76,10 @@ def run_dws(args: list) -> Optional[Dict[str, Any]]: def upload_to_oss(upload_url: str, file_path: Path, mime_type: str) -> bool: """通过 HTTP PUT 上传文件到 OSS。""" + parsed = urlparse(upload_url) + if parsed.scheme != 'https' or not parsed.hostname: + print('错误:uploadUrl 必须是有效的 HTTPS URL', file=sys.stderr) + return False file_data = file_path.read_bytes() req = Request(upload_url, data=file_data, method='PUT') req.add_header('Content-Type', mime_type) @@ -84,7 +98,9 @@ def upload_to_oss(upload_url: str, file_path: Path, mime_type: str) -> bool: return False -def upload_attachment(base_id: str, file_path_str: str) -> Optional[Dict[str, Any]]: +def upload_attachment( + base_id: str, file_path_str: str, dws_bin: str = 'dws' +) -> Optional[Dict[str, Any]]: """ 执行完整的附件上传流程: 1. prepare_attachment_upload → uploadUrl + fileToken @@ -121,7 +137,7 @@ def upload_attachment(base_id: str, file_path_str: str) -> Optional[Dict[str, An '--mime-type', mime_type, '--format', 'json', ] - result = run_dws(dws_args) + result = run_dws(dws_args, dws_bin) if not result: return None @@ -157,27 +173,20 @@ def upload_attachment(base_id: str, file_path_str: str) -> Optional[Dict[str, An def main(): - if len(sys.argv) != 3: - print(__doc__) - print('用法:') - print(' python upload_attachment.py ') - print() - print('示例:') - print(' python upload_attachment.py G1DKw2zgV2bEk6PMSBooNxlEVB5r9YAn ./report.pdf') - print() - print('然后在 record create 中使用返回的 fileToken:') - print(' dws aitable record create --base-id --table-id \\') - print(' --records \'[{"cells":{"fldAttachId":[{"fileToken":"ft_xxx"}]}}]\' --format json') - sys.exit(1) + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('base_id') + parser.add_argument('file_path') + parser.add_argument('--dws', default='dws', help='dws 可执行文件路径') + args = parser.parse_args() - base_id = sys.argv[1] - file_path = sys.argv[2] + base_id = args.base_id + file_path = args.file_path if not validate_resource_id(base_id): print('错误:无效的 baseId 格式', file=sys.stderr) - sys.exit(1) + parser.error('无效的 baseId 格式') - result = upload_attachment(base_id, file_path) + result = upload_attachment(base_id, file_path, args.dws) if result is None: sys.exit(1) diff --git a/test/scripts/aitable_skill_scripts_test.py b/test/scripts/aitable_skill_scripts_test.py new file mode 100644 index 00000000..eab3936f --- /dev/null +++ b/test/scripts/aitable_skill_scripts_test.py @@ -0,0 +1,208 @@ +import importlib.util +import json +import os +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] +SCRIPTS = ROOT / "skills" / "multi" / "dingtalk-aitable" / "scripts" + + +def load_module(name: str, filename: str): + spec = importlib.util.spec_from_file_location(name, SCRIPTS / filename) + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + spec.loader.exec_module(module) + return module + + +IMPORT_RECORDS = load_module("aitable_import_records", "import_records.py") +BULK_FIELDS = load_module("aitable_bulk_fields", "bulk_add_fields.py") +UPLOAD_ATTACHMENT = load_module("aitable_upload_attachment", "upload_attachment.py") +EXPORT_TASK = load_module("aitable_export_task", "aitable_export_via_task.py") +IMPORT_TASK = load_module("aitable_import_task", "aitable_import_via_task.py") + + +class AITableSkillScriptsTest(unittest.TestCase): + def make_fake_dws(self, directory: Path, body: str) -> Path: + script = directory / "fake-dws" + script.write_text("#!/usr/bin/env python3\n" + body, encoding="utf-8") + script.chmod(0o755) + return script + + def run_script(self, script: str, args, cwd: Path): + env = os.environ.copy() + env["OPENCLAW_WORKSPACE"] = str(cwd) + return subprocess.run( + [sys.executable, str(SCRIPTS / script), *map(str, args)], + cwd=cwd, + env=env, + text=True, + capture_output=True, + ) + + def test_csv_values_remain_strings(self): + with tempfile.TemporaryDirectory() as raw: + root = Path(raw) + source = root / "records.csv" + source.write_text("fldPhone01,fldBool01\n00123,true\n", encoding="utf-8") + previous = os.getcwd() + os.chdir(root) + try: + records = IMPORT_RECORDS.load_records(str(source)) + finally: + os.chdir(previous) + self.assertEqual(records[0]["cells"]["fldPhone01"], "00123") + self.assertEqual(records[0]["cells"]["fldBool01"], "true") + + def test_import_records_checks_ids_and_readback(self): + with tempfile.TemporaryDirectory() as raw: + root = Path(raw) + source = root / "records.json" + source.write_text('[{"cells":{"fldText01":"值"}}]', encoding="utf-8") + fake = self.make_fake_dws( + root, + """import json, sys +args = sys.argv[1:] +if args[:3] == ['aitable', 'record', 'create']: + print(json.dumps({'status':'success','data':{'newRecordIds':['rec12345678']}})) +elif args[:3] == ['aitable', 'record', 'query']: + print(json.dumps({'status':'success','data':{'records':[{'recordId':'rec12345678'}]}})) +else: + print(json.dumps({'status':'error','summary':'unexpected command'})) +""", + ) + result = self.run_script( + "import_records.py", + ["base12345678", "table1234567", source, "--dws", fake], + root, + ) + self.assertEqual(result.returncode, 0, result.stderr) + payload = json.loads(result.stdout) + self.assertTrue(payload["complete"]) + self.assertEqual(payload["recordIds"], ["rec12345678"]) + self.assertEqual(payload["ledger"][0]["status"], "success") + + def test_import_records_treats_exit_zero_business_error_as_failure(self): + with tempfile.TemporaryDirectory() as raw: + root = Path(raw) + source = root / "records.json" + source.write_text('[{"cells":{"fldText01":"值"}}]', encoding="utf-8") + fake = self.make_fake_dws( + root, + "import json; print(json.dumps({'status':'error','summary':'denied'}))\n", + ) + result = self.run_script( + "import_records.py", + ["base12345678", "table1234567", source, "--dws", fake], + root, + ) + self.assertEqual(result.returncode, 2) + payload = json.loads(result.stdout) + self.assertFalse(payload["complete"]) + self.assertIn("业务失败", payload["ledger"][0]["error"]) + + def test_bulk_fields_preserves_ai_config_and_verifies(self): + with tempfile.TemporaryDirectory() as raw: + root = Path(raw) + source = root / "fields.json" + source.write_text( + json.dumps([ + { + "fieldName": "AI 摘要", + "type": "text", + "aiConfig": {"outputType": "text", "prompt": [{"type": "text", "value": "摘要"}]}, + } + ], ensure_ascii=False), + encoding="utf-8", + ) + fake = self.make_fake_dws( + root, + """import json, sys +args = sys.argv[1:] +if args[:3] == ['aitable', 'field', 'create']: + fields = json.loads(args[args.index('--fields') + 1]) + if 'aiConfig' not in fields[0]: + print(json.dumps({'status':'error','summary':'aiConfig lost'})) + else: + print(json.dumps({'status':'success','data':{'results':[{'success':True,'fieldId':'field123456'}]}})) +elif args[:3] == ['aitable', 'field', 'get']: + print(json.dumps({'status':'success','data':{'fields':[{'fieldId':'field123456'}]}})) +else: + print(json.dumps({'status':'error','summary':'unexpected command'})) +""", + ) + result = self.run_script( + "bulk_add_fields.py", + ["base12345678", "table1234567", source, "--dws", fake], + root, + ) + self.assertEqual(result.returncode, 0, result.stderr) + payload = json.loads(result.stdout) + self.assertTrue(payload["complete"]) + self.assertEqual(payload["fieldIds"], ["field123456"]) + + def test_bulk_fields_reports_partial_result(self): + with tempfile.TemporaryDirectory() as raw: + root = Path(raw) + source = root / "fields.json" + source.write_text( + '[{"fieldName":"A","type":"text"},{"fieldName":"B","type":"text"}]', + encoding="utf-8", + ) + fake = self.make_fake_dws( + root, + """import json, sys +args = sys.argv[1:] +if args[:3] == ['aitable', 'field', 'create']: + print(json.dumps({'status':'success','data':{'results':[{'success':True,'fieldId':'field123456'},{'success':False,'reason':'duplicate'}]}})) +elif args[:3] == ['aitable', 'field', 'get']: + print(json.dumps({'status':'success','data':{'fields':[{'fieldId':'field123456'}]}})) +""", + ) + result = self.run_script( + "bulk_add_fields.py", + ["base12345678", "table1234567", source, "--dws", fake], + root, + ) + self.assertEqual(result.returncode, 2) + payload = json.loads(result.stdout) + self.assertFalse(payload["complete"]) + self.assertEqual(payload["verifiedCount"], 1) + self.assertEqual(payload["ledger"][1]["error"], "duplicate") + + def test_upload_helpers_reject_plain_http_urls(self): + with tempfile.TemporaryDirectory() as raw: + file_path = Path(raw) / "file.bin" + file_path.write_bytes(b"x") + self.assertFalse( + UPLOAD_ATTACHMENT.upload_to_oss( + "http://example.com/upload", file_path, "application/octet-stream" + ) + ) + ok, error = IMPORT_TASK.put_file("http://example.com/upload", file_path) + self.assertFalse(ok) + self.assertIn("HTTPS", error) + + def test_export_helpers_reject_http_and_existing_output(self): + with self.assertRaises(ValueError): + EXPORT_TASK.normalize_download_url("http://example.com/file.xlsx") + with tempfile.TemporaryDirectory() as raw: + root = Path(raw) + output = root / "existing.xlsx" + output.write_bytes(b"existing") + previous = os.getcwd() + os.chdir(root) + try: + with self.assertRaises(ValueError): + EXPORT_TASK.resolve_output_path("existing.xlsx", "ignored.xlsx", False) + finally: + os.chdir(previous) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/unit/aitable_skill_scripts_test.go b/test/unit/aitable_skill_scripts_test.go new file mode 100644 index 00000000..5d9ce2d3 --- /dev/null +++ b/test/unit/aitable_skill_scripts_test.go @@ -0,0 +1,30 @@ +package unit_test + +import ( + "os/exec" + "path/filepath" + "runtime" + "testing" +) + +func TestAITableSkillPythonScripts(t *testing.T) { + python, err := exec.LookPath("python3") + if err != nil { + t.Skip("python3 is not installed") + } + + _, filename, _, ok := runtime.Caller(0) + if !ok { + t.Fatal("runtime.Caller(0) failed") + } + root := filepath.Clean(filepath.Join(filepath.Dir(filename), "..", "..")) + cmd := exec.Command( + python, + "test/scripts/aitable_skill_scripts_test.py", + ) + cmd.Dir = root + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("AI Table skill Python tests failed: %v\n%s", err, output) + } +}