feat: gate unverified shortcuts for release

Hide shortcuts that did not pass real testing from public help/list discovery while keeping commands available for internal retest.

Record real shortcut inputs/outputs, backend issue summaries, next-release hidden list, and refresh skill guidance to mirror the lark-cli shortcut integration pattern.
This commit is contained in:
Dennis
2026-07-15 16:45:51 +08:00
parent fde008a25d
commit 8687d68567
58 changed files with 35819 additions and 320 deletions
+174
View File
@@ -0,0 +1,174 @@
# Shortcut 真实测试:后端 / MCP 问题整理
这份报告只汇总 `failure_category = backend-or-mcp-error` 的 case,已尽量排除权限、缺真实资源、当前账号无数据等噪音。
## 总览
- Backend/MCP case 总数:33
- 聚合问题数:8
- 复现口径:真实 dws CLI;无 mock;无 dry-run;命令输入和 trace_id 均来自真实测试结果。
## 建议优先看
1. [P1] Chat/IM 会话 ID 字段在 MCP/后端映射中疑似丢失(15 case)
2. [P1] Chat card 发送 receiverUid 疑似未从 receiver 透传(1 case)
3. [P1] Chat 入群审批 applicantUid/inviterUid 疑似未透传(1 case)
4. [P1] AI 表格 MCP 错误 envelope 语义不一致:success=true 但 error 非空/status=error(5 case)
5. [P1] AI 表格 Workflow 查询在真实 Base 下返回系统级错误(2 case)
6. [P1] AI 表格 roleId 参数疑似未被 MCP 正确读取(3 case)
7. [P2] AI 表格记录主文档查询在真实 record 下返回 no record/SYSTEM_ERROR(2 case)
8. [P2] AI 表格无效 Base/Table/Field/Record 被包装成 SYSTEM_ERROR(4 case)
## Chat/IM 会话 ID 字段在 MCP/后端映射中疑似丢失
- 优先级:P1
- 建议 owner:IM MCP / IM 后端字段映射
- 现象:CLI 已传 group/conversation-id/open-conversation-id(部分 case 使用真实 cid),后端仍报 openCid/openConversationId/cid required。
- 期望:MCP schema/网关应接受并透传 openConversationId/openCid/cid 中的兼容字段;如果资源无效,应返回“无效会话”,而不是 required。
- 涉及 case:15
| 套件 | shortcut | operation | trace_id | 证据 |
|---|---|---|---|---|
| `read` | `chat +chat-members-get` | `tools/call` | `2127d89817840997754345760e07bd` | [UNCLASSIFIED] openCid or cid is required (operation: im/list_group_member_by_ids) hint: Use --verbose for detailed error logs |
| | input | | | `/private/tmp/dws-real-test chat +chat-members-get --id DWSREALREADNOSUCHID0000000000000 --users '冬翔' --yes --format json` |
| `read` | `chat +chat-messages` | `tools/call` | `2104a64c17840997767792656e085e` | [UNCLASSIFIED] openCid or cid is required hint: Use --verbose for detailed error logs |
| | input | | | `/private/tmp/dws-real-test chat +chat-messages --group cid3Jijzhe2aqs9ysOXjhi05g== --time '2026-07-15 10:00:00' --limit 10 --direction older --yes --format json` |
| `read` | `chat +messages-list` | `tools/call` | `2127d89817840997797873841e0757` | [UNCLASSIFIED] openCid or cid is required hint: Use --verbose for detailed error logs |
| | input | | | `/private/tmp/dws-real-test chat +messages-list --group cid3Jijzhe2aqs9ysOXjhi05g== --time '2026-07-15 10:00:00' --forward --limit 10 --yes --format json` |
| `write` | `chat +chat-mute-member` | `tools/call` | `2104a64c17840999166036583e08a3` | [UNCLASSIFIED] openConversationId or cid is required (operation: im/set_group_member_mute_list) hint: Use --verbose for detailed error logs |
| | input | | | `/private/tmp/dws-real-test chat +chat-mute-member --group cidDWSREALTESTNOSUCHCONV --users __DWS_SHORTCUT_REAL_TEST_NO_SUCH_USER__ --mute-time 1 --off --yes --format json` |
| `write` | `chat +chat-transfer-owner` | `tools/call` | `0b5deb3217840999222318863e087a` | [UNCLASSIFIED] openConversationId is required (operation: im/transfer_group_owner) hint: Use --verbose for detailed error logs |
| | input | | | `/private/tmp/dws-real-test chat +chat-transfer-owner --group cidDWSREALTESTNOSUCHCONV --new-owner __DWS_SHORTCUT_REAL_TEST_NO_SUCH_USER__ --yes --format json` |
| `write` | `chat +conversation-clear-messages` | `tools/call` | `2127d89817840999254816721e079b` | [UNCLASSIFIED] openConversationId or cid is required (operation: im/clear_conversation_messages) hint: Use --verbose for detailed error logs |
| | input | | | `/private/tmp/dws-real-test chat +conversation-clear-messages --conversation-id cidDWSREALTESTNOSUCHCONV --yes --format json` |
| `write` | `chat +conversation-clear-red-point` | `tools/call` | `2104a64c17840999265511295e085f` | [UNCLASSIFIED] openConversationId or cid is required (operation: im/clear_conversation_red_point) hint: Use --verbose for detailed error logs |
| | input | | | `/private/tmp/dws-real-test chat +conversation-clear-red-point --conversation-id cidDWSREALTESTNOSUCHCONV --yes --format json` |
| `write` | `chat +conversation-hide` | `tools/call` | `2127d89817840999276117140e079b` | [UNCLASSIFIED] openConversationId or cid is required (operation: im/hide_conversation) hint: Use --verbose for detailed error logs |
| | input | | | `/private/tmp/dws-real-test chat +conversation-hide --conversation-id cidDWSREALTESTNOSUCHCONV --yes --format json` |
| `write` | `chat +conversation-mark-unread` | `tools/call` | `0bb7c36217840999298744910e0758` | [UNCLASSIFIED] openConversationId or cid is required (operation: im/mark_conversation_unread) hint: Use --verbose for detailed error logs |
| | input | | | `/private/tmp/dws-real-test chat +conversation-mark-unread --conversation-id cidDWSREALTESTNOSUCHCONV --yes --format json` |
| `write` | `chat +conversation-mute` | `tools/call` | `2104a64c17840999309241865e085f` | [UNCLASSIFIED] openConversationId is required (operation: im/update_notification_off) hint: Use --verbose for detailed error logs |
| | input | | | `/private/tmp/dws-real-test chat +conversation-mute --conversation-id cidDWSREALTESTNOSUCHCONV --off --yes --format json` |
| `write` | `chat +conversation-mute-at-all` | `tools/call` | `2127d89817840999320028068e07dd` | [UNCLASSIFIED] openConversationId is required (operation: im/update_at_all_notification_off) hint: Use --verbose for detailed error logs |
| | input | | | `/private/tmp/dws-real-test chat +conversation-mute-at-all --conversation-id cidDWSREALTESTNOSUCHCONV --off --yes --format json` |
| `write` | `chat +conversation-mute-red-envelope` | `tools/call` | `0bb7c36217840999330228283e07fe` | [UNCLASSIFIED] openConversationId is required (operation: im/update_red_env_notification_off) hint: Use --verbose for detailed error logs |
| | input | | | `/private/tmp/dws-real-test chat +conversation-mute-red-envelope --conversation-id cidDWSREALTESTNOSUCHCONV --off --yes --format json` |
| `write` | `chat +conversation-set-top` | `tools/call` | `2127d89817840999341302961e07fe` | [UNCLASSIFIED] openConversationId or cid is required (operation: im/set_top_conversation) hint: Use --verbose for detailed error logs |
| | input | | | `/private/tmp/dws-real-test chat +conversation-set-top --conversation-id cidDWSREALTESTNOSUCHCONV --off --yes --format json` |
| `write` | `chat +messages-set-pin` | `tools/call` | `0bb7c36217840999521117991e0758` | [UNCLASSIFIED] openConversationId or cid is required (operation: im/set_pin_message) hint: Use --verbose for detailed error logs |
| | input | | | `/private/tmp/dws-real-test chat +messages-set-pin --open-conversation-id cidDWSREALTESTNOSUCHCONV --msg-id DWSREALTESTNOSUCHID0000000000000 --yes --format json` |
| `write` | `chat +messages-unset-pin` | `tools/call` | `2104a64c17840999544428103e08ee` | [UNCLASSIFIED] openConversationId or cid is required (operation: im/unset_pin_message) hint: Use --verbose for detailed error logs |
| | input | | | `/private/tmp/dws-real-test chat +messages-unset-pin --open-conversation-id cidDWSREALTESTNOSUCHCONV --msg-id DWSREALTESTNOSUCHID0000000000000 --yes --format json` |
## Chat card 发送 receiverUid 疑似未从 receiver 透传
- 优先级:P1
- 建议 owner:IM MCP / card 发送参数映射
- 现象:CLI 传入 receiver=103262,后端仍报 receiverUid 和 openConversationId 不能同时为空。
- 期望:receiver 应映射为 receiverUid,或 schema 明确要求 receiverUid;真实入参不应在 MCP 层丢失。
- 涉及 case:1
| 套件 | shortcut | operation | trace_id | 证据 |
|---|---|---|---|---|
| `write` | `chat +messages-send-card` | `tools/call` | `2104a64c17840999509255753e081a` | [UNCLASSIFIED] receiverUid和openConversationId不能同时为空 (operation: im/create_and_send_card) hint: Use --verbose for detailed error logs |
| | input | | | `/private/tmp/dws-real-test chat +messages-send-card --receiver 103262 --yes --format json` |
## Chat 入群审批 applicantUid/inviterUid 疑似未透传
- 优先级:P1
- 建议 owner:IM MCP / 入群审批参数映射
- 现象:CLI 传入 applicant=103262、inviter=519019,后端仍报 applicantUid required。
- 期望:applicant/inviter 应映射为 applicantUid/inviterUid;如果 recordId/group 无效,应返回对应资源错误而不是 applicantUid 缺失。
- 涉及 case:1
| 套件 | shortcut | operation | trace_id | 证据 |
|---|---|---|---|---|
| `write` | `chat +chat-audit-join` | `tools/call` | `0bb7c36217840999154832733e0758` | [UNCLASSIFIED] applicantUid is required (operation: im/audit_join_group) hint: Use --verbose for detailed error logs |
| | input | | | `/private/tmp/dws-real-test chat +chat-audit-join --group cidDWSREALTESTNOSUCHCONV --record-id 999999999999 --applicant 103262 --inviter 519019 --status AuditApprove --description 'DWS shortcut 真实测试描述,可删除' --yes --format json` |
## AI 表格 MCP 错误 envelope 语义不一致:success=true 但 error 非空/status=error
- 优先级:P1
- 建议 owner:AI 表格 MCP wrapper
- 现象:多条 AI 表格命令返回 MCP_TOOL_ERROR,内部 JSON 同时出现 success=true、status=error、error 非空。
- 期望:只要 error 非空或 status=error,success 应为 false,外层也应按业务错误返回稳定错误码/trace。
- 涉及 case:5
| 套件 | shortcut | operation | trace_id | 证据 |
|---|---|---|---|---|
| `read` | `aitable +export-data` | `-` | `2104a64c17840997514714448e0817` | [MCP_TOOL_ERROR] {"data":{},"error":{"code":"INVALID_PARAMS","message":"taskId cannot be combined with scope, format, tableId or viewId","retryable":false,"type":"INPUT_ERROR"},"m… |
| | input | | | `/private/tmp/dws-real-test aitable +export-data --base-id gpG2NdyVXQyZ0OmoSbd1vbA6JMwvDqPk --task-id DWSREALREADNOSUCHID0000000000000 --scope all --format excel --table-id hERWDMS --view-id qvGDAH2 --timeout-ms 1 --yes` |
| `write` | `aitable +chart-update` | `-` | `2106d98117840998553244877e08df` | [MCP_TOOL_ERROR] {"data":{},"error":{"code":"INVALID_PARAMS","message":"config is required","retryable":false,"type":"INPUT_ERROR"},"meta":{},"status":"error","success":true,"summ… |
| | input | | | `/private/tmp/dws-real-test aitable +chart-update --base-id DWSREALTESTNOSUCHID0000000000000 --dashboard-id DWSREALTESTNOSUCHID0000000000000 --chart-id DWSREALTESTNOSUCHID0000000000000 --config '{}' --layout '{}' --yes --format json` |
| `write` | `aitable +record-update` | `-` | `0bab027317840998747383236e090b` | [MCP_TOOL_ERROR] {"data":{},"error":{"code":"INVALID_RECORDS","message":"records must contain at least one writable record","retryable":false,"type":"INPUT_ERROR"},"meta":{},"stat… |
| | input | | | `/private/tmp/dws-real-test aitable +record-update --base-id DWSREALTESTNOSUCHID0000000000000 --table-id DWSREALTESTNOSUCHID0000000000000 --records '[{"recordId":"recDWSREALTEST","cells":{}}]' --yes --format json` |
| `write` | `aitable +record-upsert` | `-` | `2106d98117840998759832182e087b` | [MCP_TOOL_ERROR] {"data":{},"error":{"code":"INVALID_PARAMETER","message":"records is required and must not be empty","retryable":false,"type":"INPUT_ERROR"},"meta":{},"status":"e… |
| | input | | | `/private/tmp/dws-real-test aitable +record-upsert --base-id DWSREALTESTNOSUCHID0000000000000 --table-id DWSREALTESTNOSUCHID0000000000000 --records '[]' --yes --format json` |
| `write` | `aitable +view-set-fill-color-rule` | `-` | `2106d98117840998924181709e08df` | [MCP_TOOL_ERROR] {"data":{},"error":{"code":"INVALID_PARAMS","message":"conditionalFormats is required","retryable":false,"type":"INPUT_ERROR"},"meta":{},"status":"error","success… |
| | input | | | `/private/tmp/dws-real-test aitable +view-set-fill-color-rule --base-id DWSREALTESTNOSUCHID0000000000000 --table-id DWSREALTESTNOSUCHID0000000000000 --view-id DWSREALTESTNOSUCHID0000000000000 --json '{}' --yes --format json` |
## AI 表格 Workflow 查询在真实 Base 下返回系统级错误
- 优先级:P1
- 建议 owner:AI 表格 Workflow MCP / 后端
- 现象:使用真实可访问 Base 查询 workflow list/get,返回 LIST_WORKFLOWS_ERROR/GET_WORKFLOW_ERROR。
- 期望:无 workflow 时应返回空列表或 WORKFLOW_NOT_FOUND;有后端异常时需提供稳定错误码和可排查 trace。
- 涉及 case:2
| 套件 | shortcut | operation | trace_id | 证据 |
|---|---|---|---|---|
| `read` | `aitable +workflow-get` | `-` | `2104a64c17840997556363676e08ee` | [MCP_TOOL_ERROR] {"data":{},"error":{"code":"GET_WORKFLOW_ERROR","message":"调用远程服务业务异常","retryable":true,"type":"SYSTEM_ERROR"},"meta":{},"status":"error","success":true,"summary"… |
| | input | | | `/private/tmp/dws-real-test aitable +workflow-get --base-id gpG2NdyVXQyZ0OmoSbd1vbA6JMwvDqPk --workflow-id DWSREALREADNOSUCHID0000000000000 --yes --format json` |
| `read` | `aitable +workflow-list` | `-` | `2127d89817840997572427879e075d` | [MCP_TOOL_ERROR] {"data":{},"error":{"code":"LIST_WORKFLOWS_ERROR","message":"biz error","retryable":true,"type":"SYSTEM_ERROR"},"meta":{},"status":"error","success":true,"summary… |
| | input | | | `/private/tmp/dws-real-test aitable +workflow-list --base-id gpG2NdyVXQyZ0OmoSbd1vbA6JMwvDqPk --limit 10 --offset 1 --yes --format json` |
## AI 表格 roleId 参数疑似未被 MCP 正确读取
- 优先级:P1
- 建议 owner:AI 表格 MCP role 接口
- 现象:CLI 已传 --role-id,但 MCP 返回 roleId is required。
- 期望:role-id/roleId 字段应被正确映射;如果 role 不存在,返回 ROLE_NOT_FOUND,而不是 required。
- 涉及 case:3
| 套件 | shortcut | operation | trace_id | 证据 |
|---|---|---|---|---|
| `read` | `aitable +role-get` | `-` | `2104a64c17840997542904838e0817` | [MCP_TOOL_ERROR] {"data":{},"error":{"code":"INVALID_PARAMS","message":"roleId is required","retryable":false,"type":"INPUT_ERROR"},"meta":{},"status":"error","success":true,"summ… |
| | input | | | `/private/tmp/dws-real-test aitable +role-get --base-id gpG2NdyVXQyZ0OmoSbd1vbA6JMwvDqPk --role-id x --yes --format json` |
| `write` | `aitable +role-delete` | `-` | `2106d98117840998782482701e089c` | [MCP_TOOL_ERROR] {"data":{},"error":{"code":"INVALID_PARAMS","message":"roleId is required","retryable":false,"type":"INPUT_ERROR"},"meta":{},"status":"error","success":true,"summ… |
| | input | | | `/private/tmp/dws-real-test aitable +role-delete --base-id DWSREALTESTNOSUCHID0000000000000 --role-id DWSREALTESTNOSUCHID0000000000000 --yes --format json` |
| `write` | `aitable +role-update` | `-` | `2132f5ca17840998794483634e08d8` | [MCP_TOOL_ERROR] {"data":{},"error":{"code":"INVALID_PARAMS","message":"roleId is required","retryable":false,"type":"INPUT_ERROR"},"meta":{},"status":"error","success":true,"summ… |
| | input | | | `/private/tmp/dws-real-test aitable +role-update --base-id DWSREALTESTNOSUCHID0000000000000 --role-id DWSREALTESTNOSUCHID0000000000000 --name 'DWS shortcut 真实测试 20260715-151724' --role-type x --flow-type x --sub-roles '[]' --yes --format json` |
## AI 表格记录主文档查询在真实 record 下返回 no record/SYSTEM_ERROR
- 优先级:P2
- 建议 owner:AI 表格 primary doc MCP / 后端
- 现象:record-query 已能查到真实 recordId,但 primary-doc 查询返回 no record、type=SYSTEM_ERROR。
- 期望:若该记录无主文档,应返回空/未创建;若 recordId 语义不匹配,应返回明确参数错误,不应是系统错误。
- 涉及 case:2
| 套件 | shortcut | operation | trace_id | 证据 |
|---|---|---|---|---|
| `read` | `aitable +base-get-primary-doc-id` | `-` | `0b5deb3217840997466255627e08ee` | [MCP_TOOL_ERROR] {"data":{},"error":{"code":"-1","message":"no record","retryable":true,"type":"SYSTEM_ERROR"},"meta":{},"status":"error","success":true,"summary":"Failed to query… |
| | input | | | `/private/tmp/dws-real-test aitable +base-get-primary-doc-id --base-id gpG2NdyVXQyZ0OmoSbd1vbA6JMwvDqPk --table-id hERWDMS --record-id 1015oH3OXy --yes --format json` |
| `read` | `aitable +record-primary-doc-get` | `-` | `2127d89817840997528393730e079c` | [MCP_TOOL_ERROR] {"data":{},"error":{"code":"-1","message":"no record","retryable":true,"type":"SYSTEM_ERROR"},"meta":{},"status":"error","success":true,"summary":"Failed to query… |
| | input | | | `/private/tmp/dws-real-test aitable +record-primary-doc-get --base-id gpG2NdyVXQyZ0OmoSbd1vbA6JMwvDqPk --table-id hERWDMS --record-id 1015oH3OXy --yes --format json` |
## AI 表格无效 Base/Table/Field/Record 被包装成 SYSTEM_ERROR
- 优先级:P2
- 建议 owner:AI 表格 MCP wrapper / 后端错误码
- 现象:安全负向 ID 下,部分写接口返回 getDentryDTO returns null、type=SYSTEM_ERROR、retryable=true。
- 期望:资源不存在应返回 INPUT_ERROR/NOT_FOUND 且 retryable=false,避免误导调用方重试。
- 涉及 case:4
| 套件 | shortcut | operation | trace_id | 证据 |
|---|---|---|---|---|
| `write` | `aitable +field-delete` | `-` | `0bab027317840998611338768e08c8` | [MCP_TOOL_ERROR] {"data":{},"error":{"code":"404","message":"getDentryDTO returns null","retryable":true,"type":"SYSTEM_ERROR"},"meta":{},"status":"error","success":true,"summary"… |
| | input | | | `/private/tmp/dws-real-test aitable +field-delete --base-id DWSREALTESTNOSUCHID0000000000000 --table-id DWSREALTESTNOSUCHID0000000000000 --field-id DWSREALTESTNOSUCHID0000000000000 --yes --format json` |
| `write` | `aitable +field-update` | `-` | `213ee25c17840998623207342e08e2` | [MCP_TOOL_ERROR] {"data":{},"error":{"code":"404","message":"getDentryDTO returns null","retryable":true,"type":"SYSTEM_ERROR"},"meta":{},"status":"error","success":true,"summary"… |
| | input | | | `/private/tmp/dws-real-test aitable +field-update --base-id DWSREALTESTNOSUCHID0000000000000 --table-id DWSREALTESTNOSUCHID0000000000000 --field-id DWSREALTESTNOSUCHID0000000000000 --name 'DWS shortcut 真实测试 20260715-151724' --config '{}' --ai-config '{}' --yes --format json` |
| `write` | `aitable +record-delete` | `-` | `2132f5ca17840998724753149e0853` | [MCP_TOOL_ERROR] {"data":{},"error":{"code":"404","message":"getDentryDTO returns null","retryable":true,"type":"SYSTEM_ERROR"},"meta":{},"status":"error","success":true,"summary"… |
| | input | | | `/private/tmp/dws-real-test aitable +record-delete --base-id DWSREALTESTNOSUCHID0000000000000 --table-id DWSREALTESTNOSUCHID0000000000000 --record-ids DWSREALTESTNOSUCHID0000000000000 --yes --format json` |
| `write` | `aitable +table-update` | `-` | `213ee25c17840998877246229e087f` | [MCP_TOOL_ERROR] {"data":{},"error":{"code":"404","message":"getDentryDTO returns null","retryable":true,"type":"SYSTEM_ERROR"},"meta":{},"status":"error","success":true,"summary"… |
| | input | | | `/private/tmp/dws-real-test aitable +table-update --base-id DWSREALTESTNOSUCHID0000000000000 --table-id DWSREALTESTNOSUCHID0000000000000 --name 'DWS shortcut 真实测试 20260715-151724' --description 'DWS shortcut 真实测试描述,可删除' --record-name-key task --yes --format json` |
+20601 -186
View File
File diff suppressed because one or more lines are too long
+279
View File
@@ -0,0 +1,279 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Shortcut 真实测试失败逐项 review</title>
<style>
:root{--bg:#0f1420;--card:#151d2b;--line:#263246;--text:#dce7f7;--muted:#91a0b5;--blue:#8fd3ff;--green:#66d38a;--yellow:#e2b23c;--red:#f27272;--purple:#d3a7ff}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--text);font:13px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"PingFang SC","Microsoft YaHei",sans-serif}
header{padding:28px 32px 14px;border-bottom:1px solid var(--line);background:linear-gradient(180deg,#172033,#0f1420)}
h1{margin:0 0 8px;font-size:26px}
h2{margin:28px 0 10px;font-size:18px}
.sub,.note,.count{color:var(--muted)}
.wrap{padding:18px 32px 40px;max-width:1800px;margin:0 auto}
.note{background:var(--card);border:1px solid var(--line);border-radius:10px;padding:12px 14px;margin:10px 0 18px}
.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin:18px 0}
.stat{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:14px}
.stat .n{font-size:24px;color:var(--blue);font-weight:700}
.stat .l{color:var(--muted);font-size:12px}
.summary-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:18px;margin:16px 0 22px}
table{width:100%;border-collapse:collapse;background:var(--card);border:1px solid var(--line);border-radius:10px;overflow:hidden}
th,td{padding:8px 10px;border-bottom:1px solid var(--line);vertical-align:top;text-align:left}
th{background:#1b2536;color:var(--muted);font-size:12px;font-weight:600;position:sticky;top:0;z-index:1}
tr:last-child td{border-bottom:none}
code{font-family:"SF Mono",Menlo,Consolas,monospace;color:#c7cfdb;font-size:12px}
.review{table-layout:fixed}
.review th:nth-child(1),.review td:nth-child(1){width:44px}
.review th:nth-child(2),.review td:nth-child(2){width:165px}
.review th:nth-child(3),.review td:nth-child(3){width:70px}
.review th:nth-child(4),.review td:nth-child(4){width:120px}
.review th:nth-child(5),.review td:nth-child(5){width:130px}
.review th:nth-child(8),.review td:nth-child(8){width:130px}
.review th:nth-child(9),.review td:nth-child(9){width:180px}
.review td{word-break:break-word}
.num{color:var(--muted);text-align:right}
.risk{color:var(--green)}
.cat{color:var(--yellow)}
.owner{color:var(--purple)}
.evidence{color:#c7cfdb;font-size:12px}
a{color:var(--blue)}
</style>
</head>
<body>
<header>
<h1>Shortcut 真实测试失败逐项 review</h1>
<div class="sub">由 <code>scripts/gen_shortcut_error_review.py</code> 从真实测试结果生成;目标是把每个失败项落到“应该改哪里”。</div>
</header>
<main class="wrap">
<div class="note">
Read:204 条,成功 162,失败 41,超时 0。
Write:162 条,成功 48,失败 114,超时 0。
判定口径:如果 fake MCP 已看到字段但真实后端仍报 required,按后端/MCP schema 映射处理;如果真实后端报资源无效/不存在,按 fixture 处理;权限类不在 CLI 中绕过。
</div>
<div class="stats"><div class="stat"><div class="n">41</div><div class="l">Read 失败</div></div>
<div class="stat"><div class="n">114</div><div class="l">Write 失败</div></div>
<div class="stat"><div class="n">156</div><div class="l">逐项 review</div></div>
<div class="stat"><div class="n">72</div><div class="l">测试数据/fixture</div></div>
<div class="stat"><div class="n">22</div><div class="l">权限/应用配置</div></div>
<div class="stat"><div class="n">33</div><div class="l">后端/MCP schema</div></div></div>
<div class="summary-grid">
<section><h2>按错误类型</h2><table><thead><tr><th>类型</th><th>数量</th></tr></thead><tbody><tr><td>输入/业务校验</td><td>36</td></tr>
<tr><td>后端/MCP</td><td>33</td></tr>
<tr><td>缺 AI 表格 fixture</td><td>31</td></tr>
<tr><td>缺真实资源</td><td>30</td></tr>
<tr><td>鉴权/权限</td><td>22</td></tr>
<tr><td>缺妙记 fixture</td><td>3</td></tr>
<tr><td>敏感/高风险暂缓</td><td>1</td></tr></tbody></table></section>
<section><h2>按要改哪里</h2><table><thead><tr><th>要改哪里</th><th>数量</th></tr></thead><tbody><tr><td>测试数据</td><td>72</td></tr>
<tr><td>后端/MCP schema</td><td>33</td></tr>
<tr><td>权限/应用配置</td><td>22</td></tr>
<tr><td>测试输入/业务校验</td><td>13</td></tr>
<tr><td>后端业务/测试 fixture</td><td>11</td></tr>
<tr><td>测试输入</td><td>2</td></tr>
<tr><td>人工安全确认</td><td>1</td></tr>
<tr><td>测试输入/shortcut 枚举</td><td>1</td></tr>
<tr><td>测试输入/业务规则</td><td>1</td></tr></tbody></table></section>
</div>
<h2>缺真实资源复盘 <span class="count">· 可自造/可查资源处理结果</span></h2>
<table>
<thead><tr><th>命令/范围</th><th>处理状态</th><th>本次实际排查/造数结果</th><th>后续建议</th></tr></thead>
<tbody><tr><td><code>chat +group-members</code></td><td><span class="cat">已补齐</span></td><td>查到真实群名 `浅曦-kida,Dennis,秋画`,runner 已改为用群名而不是 openConversationId;真实回归成功。</td><td>无需后续动作。</td></tr>
<tr><td><code>chat +messages-mget</code></td><td><span class="cat">已补齐</span></td><td>复用真实单聊消息 `msgEuOor1PmFBNlx9M06N9z1Q==`;真实回归成功。</td><td>无需后续动作。</td></tr>
<tr><td><code>chat +messages-read-status</code></td><td><span class="cat">已补齐</span></td><td>复用真实单聊会话 `cidie1367hAfBxqipzE59k5sknHLrHmvYkw98NADhfnjPI=` 与同一 openMessageId;真实回归成功。</td><td>无需后续动作。</td></tr>
<tr><td><code>chat +messages-query-send-status</code></td><td><span class="cat">已补齐</span></td><td>复用真实发送返回的 openTaskId;真实回归成功。</td><td>无需后续动作。</td></tr>
<tr><td><code>ding +receiver-status</code></td><td><span class="cat">已补齐</span></td><td>先只读 `ding +list` 找到已有 openDingId,再查询 receiver status;没有新发 DING,真实回归成功。</td><td>无需后续动作。</td></tr>
<tr><td><code>sheet +list-sheets</code></td><td><span class="cat">已自造</span></td><td>创建临时在线表格 `DWS shortcut 真实测试表格 20260715`,nodeId=`mweZ92PV6O36dZbnsMZx70ylJxEKBD6p`;真实回归成功。</td><td>后续可保留为稳定 fixture,或测试结束后人工清理。</td></tr>
<tr><td><code>todo +todo-done</code></td><td><span class="cat">已自造并修复 CLI</span></td><td>runner 会先创建当前账号自己的临时待办,再执行 `todo +todo-done`;同时修复了 todo 列表 pageSize=50 返回空、响应多层 result unwrap 不稳的问题;真实回归成功。</td><td>无需后续动作;代码已有单测覆盖 nested result。</td></tr>
<tr><td><code>contact +by-mobile</code></td><td><span class="cat">已按用户授权补齐</span></td><td>使用用户指定手机号 `13161187007` 作为真实 fixture;runner 只在该命令上替换 mobile,不扩散到其它服务。</td><td>真实回归成功后该项将从失败列表移除;若后续要脱敏公开报告,可再加展示层脱敏。</td></tr>
<tr><td><code>attendance +get-class / +get-group / +get-group-filtered</code></td><td><span class="cat">不建议自造</span></td><td>`attendance +search-class` 与 `+search-group --type FIXED` 均返回空;创建班次/考勤组会改组织考勤配置,属于高影响业务数据。</td><td>需要考勤后端/业务同学提供可读测试班次与考勤组 ID。</td></tr>
<tr><td><code>chat +chat-get-by-id</code></td><td><span class="cat">暂未找到</span></td><td>该 shortcut 只接受数字 groupId;真实群列表只返回 openConversationId,没有数字群号字段。</td><td>需要 IM 后端提供可用数字 groupId,或评估是否新增 openConversationId 形态的 shortcut。</td></tr>
<tr><td><code>chat +messages-resource-url</code></td><td><span class="cat">暂未自造</span></td><td>需要真实含 mediaId 的图片/文件/视频消息;当前文本消息无法产生 resource-id。</td><td>可在测试群发一条图片/文件消息并提取 mediaId 后补 fixture;注意会产生群消息。</td></tr>
<tr><td><code>chat +thread-replies</code></td><td><span class="cat">暂未自造</span></td><td>需要真实话题消息 topicId;普通群消息不能替代。</td><td>需要话题群 fixture,或由 IM 同学提供当前账号可访问 topicId。</td></tr>
<tr><td><code>aitable +dashboard-share-get</code></td><td><span class="cat">真实资源仍失败</span></td><td>已有真实 base/dashboard,但 share-get 返回 404 `Failed to get dashboard share config`;更像分享配置未开启或后端接口行为问题。</td><td>需要 AI 表格/后端确认如何创建/开启 dashboard share fixture,或修正 404 语义。</td></tr>
<tr><td><code>write 类 delete/recall/approve/wiki move/copy 等</code></td><td><span class="cat">不自动自造</span></td><td>这些命令即便能造资源,也会涉及删除、撤回、审批通过、知识库移动/复制等高影响动作。</td><td>需要逐项授权和专门测试空间/机器人/审批单据,不建议混在批量回归里自动跑。</td></tr></tbody>
</table>
<h2>Read 失败逐项 <span class="count">· 42 条</span></h2>
<table class="review">
<thead><tr>
<th>#</th><th>命令</th><th>风险</th><th>类型</th><th>要改哪里</th><th>具体改法</th><th>验证方式</th><th>operation</th><th>trace_id</th><th>证据</th>
</tr></thead>
<tbody>
<tr><td class="num">1</td><td><code>aitable +base-get-primary-doc-id</code></td><td><span class="risk">read</span></td><td><span class="cat">后端/MCP</span></td><td><span class="owner">后端/MCP schema</span></td><td>修 aitable MCP wrapper 的参数校验和错误语义:不要返回 success=true+error;对 required 字段给出 CLI 可识别的参数名,系统错误要带 retryable/trace。</td><td>MCP 修完后重跑该 aitable 命令,并确认 stdout JSON 不再出现 success=true 但 error 非空。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:&quot;-1&quot;,&quot;message&quot;:&quot;no record&quot;,&quot;retryable&quot;:true,&quot;type&quot;:&quot;SYSTEM_ERROR&quot;},&quot;meta&quot;:{},&quot;status&quot;:&quot;error&quot;,&quot;success&quot;:true,&quot;summary&quot;:&quot;Failed to query cell doc for record 1015oH3OXy in table…</td></tr>
<tr><td class="num">2</td><td><code>aitable +chart-share-get</code></td><td><span class="risk">read</span></td><td><span class="cat">鉴权/权限</span></td><td><span class="owner">权限/应用配置</span></td><td>给当前登录账号、DWS 应用或对应资源补齐权限/scope;本仓库 shortcut 不应绕过权限。拿 trace_id 给服务端/开放平台排查具体 scope。</td><td>补权限后重跑 `aitable +chart-share-get`;若仍是 permission,再看 trace_id。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:&quot;403&quot;,&quot;message&quot;:&quot;Forbidden&quot;,&quot;retryable&quot;:false,&quot;type&quot;:&quot;AUTH_ERROR&quot;},&quot;meta&quot;:{},&quot;status&quot;:&quot;error&quot;,&quot;success&quot;:true,&quot;summary&quot;:&quot;Failed to get chart share config for chart widget-dlxFo…</td></tr>
<tr><td class="num">3</td><td><code>aitable +dashboard-share-get</code></td><td><span class="risk">read</span></td><td><span class="cat">缺真实资源</span></td><td><span class="owner">测试数据</span></td><td>把 runner 里的安全负向 ID 换成真实资源 ID;当前错误说明调用已进后端,但资源不存在。</td><td>准备 fixture 后重跑 `aitable +dashboard-share-get`。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:&quot;404&quot;,&quot;message&quot;:&quot;Not Found&quot;,&quot;retryable&quot;:true,&quot;type&quot;:&quot;SYSTEM_ERROR&quot;},&quot;meta&quot;:{},&quot;status&quot;:&quot;error&quot;,&quot;success&quot;:true,&quot;summary&quot;:&quot;Failed to get dashboard share config for dashboard KY9…</td></tr>
<tr><td class="num">4</td><td><code>aitable +export-data</code></td><td><span class="risk">read</span></td><td><span class="cat">后端/MCP</span></td><td><span class="owner">后端/MCP schema</span></td><td>修 aitable MCP wrapper 的参数校验和错误语义:不要返回 success=true+error;对 required 字段给出 CLI 可识别的参数名,系统错误要带 retryable/trace。</td><td>MCP 修完后重跑该 aitable 命令,并确认 stdout JSON 不再出现 success=true 但 error 非空。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:&quot;INVALID_PARAMS&quot;,&quot;message&quot;:&quot;taskId cannot be combined with scope, format, tableId or viewId&quot;,&quot;retryable&quot;:false,&quot;type&quot;:&quot;INPUT_ERROR&quot;},&quot;meta&quot;:{},&quot;status&quot;:&quot;error&quot;,&quot;success&quot;:true,…</td></tr>
<tr><td class="num">5</td><td><code>aitable +record-primary-doc-get</code></td><td><span class="risk">read</span></td><td><span class="cat">后端/MCP</span></td><td><span class="owner">后端/MCP schema</span></td><td>修 aitable MCP wrapper 的参数校验和错误语义:不要返回 success=true+error;对 required 字段给出 CLI 可识别的参数名,系统错误要带 retryable/trace。</td><td>MCP 修完后重跑该 aitable 命令,并确认 stdout JSON 不再出现 success=true 但 error 非空。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:&quot;-1&quot;,&quot;message&quot;:&quot;no record&quot;,&quot;retryable&quot;:true,&quot;type&quot;:&quot;SYSTEM_ERROR&quot;},&quot;meta&quot;:{},&quot;status&quot;:&quot;error&quot;,&quot;success&quot;:true,&quot;summary&quot;:&quot;Failed to query cell doc for record 1015oH3OXy in table…</td></tr>
<tr><td class="num">6</td><td><code>aitable +role-get</code></td><td><span class="risk">read</span></td><td><span class="cat">后端/MCP</span></td><td><span class="owner">后端/MCP schema</span></td><td>修 aitable MCP wrapper 的参数校验和错误语义:不要返回 success=true+error;对 required 字段给出 CLI 可识别的参数名,系统错误要带 retryable/trace。</td><td>MCP 修完后重跑该 aitable 命令,并确认 stdout JSON 不再出现 success=true 但 error 非空。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:&quot;INVALID_PARAMS&quot;,&quot;message&quot;:&quot;roleId is required&quot;,&quot;retryable&quot;:false,&quot;type&quot;:&quot;INPUT_ERROR&quot;},&quot;meta&quot;:{},&quot;status&quot;:&quot;error&quot;,&quot;success&quot;:true,&quot;summary&quot;:&quot;Failed to get role because roleId …</td></tr>
<tr><td class="num">7</td><td><code>aitable +workflow-get</code></td><td><span class="risk">read</span></td><td><span class="cat">后端/MCP</span></td><td><span class="owner">后端/MCP schema</span></td><td>修 aitable MCP wrapper 的参数校验和错误语义:不要返回 success=true+error;对 required 字段给出 CLI 可识别的参数名,系统错误要带 retryable/trace。</td><td>MCP 修完后重跑该 aitable 命令,并确认 stdout JSON 不再出现 success=true 但 error 非空。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:&quot;GET_WORKFLOW_ERROR&quot;,&quot;message&quot;:&quot;调用远程服务业务异常&quot;,&quot;retryable&quot;:true,&quot;type&quot;:&quot;SYSTEM_ERROR&quot;},&quot;meta&quot;:{},&quot;status&quot;:&quot;error&quot;,&quot;success&quot;:true,&quot;summary&quot;:&quot;Failed to get workflow in base &#x27;gpG2Nd…</td></tr>
<tr><td class="num">8</td><td><code>aitable +workflow-list</code></td><td><span class="risk">read</span></td><td><span class="cat">后端/MCP</span></td><td><span class="owner">后端/MCP schema</span></td><td>修 aitable MCP wrapper 的参数校验和错误语义:不要返回 success=true+error;对 required 字段给出 CLI 可识别的参数名,系统错误要带 retryable/trace。</td><td>MCP 修完后重跑该 aitable 命令,并确认 stdout JSON 不再出现 success=true 但 error 非空。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:&quot;LIST_WORKFLOWS_ERROR&quot;,&quot;message&quot;:&quot;biz error&quot;,&quot;retryable&quot;:true,&quot;type&quot;:&quot;SYSTEM_ERROR&quot;},&quot;meta&quot;:{},&quot;status&quot;:&quot;error&quot;,&quot;success&quot;:true,&quot;summary&quot;:&quot;Failed to list workflows in base &#x27;gpG…</td></tr>
<tr><td class="num">9</td><td><code>attendance +get-class</code></td><td><span class="risk">read</span></td><td><span class="cat">缺真实资源</span></td><td><span class="owner">测试数据</span></td><td>替换为真实考勤班次/考勤组/员工/假期等资源 ID;当前 no-such ID 只能验证负向路径。</td><td>先用考勤列表/管理后台拿真实 ID,再重跑该 attendance 命令。</td><td><code>tools/call</code></td><td><code>2127d89817840997588238831e0757</code></td><td class="evidence">[UNCLASSIFIED] business error: success=false (operation: attendance-wukong/get_class_detail) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">10</td><td><code>attendance +get-global-setting</code></td><td><span class="risk">read</span></td><td><span class="cat">鉴权/权限</span></td><td><span class="owner">权限/应用配置</span></td><td>给当前登录账号、DWS 应用或对应资源补齐权限/scope;本仓库 shortcut 不应绕过权限。拿 trace_id 给服务端/开放平台排查具体 scope。</td><td>补权限后重跑 `attendance +get-global-setting`;若仍是 permission,再看 trace_id。</td><td><code>tools/call</code></td><td><code>2104a64c17840997604718062e085e</code></td><td class="evidence">[UNCLASSIFIED] business error: success=false (operation: attendance-wukong/query_global_setting) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">11</td><td><code>attendance +get-group</code></td><td><span class="risk">read</span></td><td><span class="cat">缺真实资源</span></td><td><span class="owner">测试数据</span></td><td>替换为真实考勤班次/考勤组/员工/假期等资源 ID;当前 no-such ID 只能验证负向路径。</td><td>先用考勤列表/管理后台拿真实 ID,再重跑该 attendance 命令。</td><td><code>tools/call</code></td><td><code>0b5deb3217840997620512305e08ef</code></td><td class="evidence">[UNCLASSIFIED] business error: success=false (operation: attendance-wukong/get_group_detail) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">12</td><td><code>attendance +get-group-filtered</code></td><td><span class="risk">read</span></td><td><span class="cat">缺真实资源</span></td><td><span class="owner">测试数据</span></td><td>替换为真实考勤班次/考勤组/员工/假期等资源 ID;当前 no-such ID 只能验证负向路径。</td><td>先用考勤列表/管理后台拿真实 ID,再重跑该 attendance 命令。</td><td><code>tools/call</code></td><td><code>2104a64c17840997638062468e08c7</code></td><td class="evidence">[UNCLASSIFIED] business error: success=false (operation: attendance-wukong/get_group_filtered_detail) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">13</td><td><code>attendance +get-leave-balance</code></td><td><span class="risk">read</span></td><td><span class="cat">输入/业务校验</span></td><td><span class="owner">测试输入/业务校验</span></td><td>当前命令已进入后端业务校验;先把测试输入换成真实合法 fixture,再判断是否需要改 shortcut。</td><td>重跑 `attendance +get-leave-balance` 并比较 stdout/stderr。</td><td><code>tools/call</code></td><td><code>2104a64c17840997652901307e081a</code></td><td class="evidence">[UNCLASSIFIED] 亲,假期类型没有余额 (operation: attendance-wukong/get_leave_balance_quota) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">14</td><td><code>attendance +list-report-columns</code></td><td><span class="risk">read</span></td><td><span class="cat">鉴权/权限</span></td><td><span class="owner">权限/应用配置</span></td><td>给当前登录账号、DWS 应用或对应资源补齐权限/scope;本仓库 shortcut 不应绕过权限。拿 trace_id 给服务端/开放平台排查具体 scope。</td><td>补权限后重跑 `attendance +list-report-columns`;若仍是 permission,再看 trace_id。</td><td><code>tools/call</code></td><td><code>2127d89817840997666188472e0756</code></td><td class="evidence">[UNCLASSIFIED] business error: success=false (operation: attendance-wukong/get_report_columns) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">15</td><td><code>attendance +query-report-leave</code></td><td><span class="risk">read</span></td><td><span class="cat">鉴权/权限</span></td><td><span class="owner">权限/应用配置</span></td><td>给当前登录账号、DWS 应用或对应资源补齐权限/scope;本仓库 shortcut 不应绕过权限。拿 trace_id 给服务端/开放平台排查具体 scope。</td><td>补权限后重跑 `attendance +query-report-leave`;若仍是 permission,再看 trace_id。</td><td><code>tools/call</code></td><td><code>2104a64c17840997679973065e085f</code></td><td class="evidence">[UNCLASSIFIED] business error: success=false (operation: attendance-wukong/get_leave_time_by_leave_names) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">16</td><td><code>calendar +find-room</code></td><td><span class="risk">read</span></td><td><span class="cat">输入/业务校验</span></td><td><span class="owner">测试输入</span></td><td>会议室类命令需要真实 roomId 或更小会议室分组;修改 runner 先定位会议室/分组,再喂给查询命令。</td><td>用真实 roomId/分组重跑 calendar room/freebusy 命令。</td><td><code>tools/call</code></td><td><code>0bb7c36217840997694975303e0758</code></td><td class="evidence">[UNCLASSIFIED] 查询范围内的会议室数量,超过上限100,请选择更小范围的分组进行查询。 hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">17</td><td><code>calendar +room-find</code></td><td><span class="risk">read</span></td><td><span class="cat">输入/业务校验</span></td><td><span class="owner">测试输入</span></td><td>会议室类命令需要真实 roomId 或更小会议室分组;修改 runner 先定位会议室/分组,再喂给查询命令。</td><td>用真实 roomId/分组重跑 calendar room/freebusy 命令。</td><td><code>tools/call</code></td><td><code>2104a64c17840997709913005e0819</code></td><td class="evidence">[UNCLASSIFIED] 查询范围内的会议室数量,超过上限100,请选择更小范围的分组进行查询。 (operation: calendar/query_available_meeting_room) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">18</td><td><code>chat +category-list-conversations</code></td><td><span class="risk">read</span></td><td><span class="cat">输入/业务校验</span></td><td><span class="owner">测试输入/业务校验</span></td><td>当前命令已进入后端业务校验;先把测试输入换成真实合法 fixture,再判断是否需要改 shortcut。</td><td>重跑 `chat +category-list-conversations` 并比较 stdout/stderr。</td><td><code>tools/call</code></td><td><code>0bb7c36217840997724375834e0758</code></td><td class="evidence">[UNCLASSIFIED] listConversationsByCategoryV2 error hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">19</td><td><code>chat +chat-get-by-id</code></td><td><span class="risk">read</span></td><td><span class="cat">缺真实资源</span></td><td><span class="owner">测试数据</span></td><td>把 runner 里的安全负向 ID 换成真实资源 ID;当前错误说明调用已进后端,但资源不存在。</td><td>准备 fixture 后重跑 `chat +chat-get-by-id`。</td><td><code>tools/call</code></td><td><code>2127d89817840997739165535e07bd</code></td><td class="evidence">[UNCLASSIFIED] verifyGroupId error: The group id does not exit (operation: im/get_conv_info_by_group_id) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">20</td><td><code>chat +chat-members-get</code></td><td><span class="risk">read</span></td><td><span class="cat">后端/MCP</span></td><td><span class="owner">后端/MCP schema</span></td><td>CLI fake MCP 已证明字段已装配;需要修 MCP tool schema 或网关字段映射,确认 openConversationId/openCid/cid、receiverUid、applicantUid 等字段没有在 schema 校验/转发时被丢弃。</td><td>修 MCP 后不改 shortcut,直接重跑真实命令;预期错误从 required 变为资源无效或成功。</td><td><code>tools/call</code></td><td><code>2127d89817840997754345760e07bd</code></td><td class="evidence">[UNCLASSIFIED] openCid or cid is required (operation: im/list_group_member_by_ids) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">21</td><td><code>chat +chat-messages</code></td><td><span class="risk">read</span></td><td><span class="cat">后端/MCP</span></td><td><span class="owner">后端/MCP schema</span></td><td>CLI fake MCP 已证明字段已装配;需要修 MCP tool schema 或网关字段映射,确认 openConversationId/openCid/cid、receiverUid、applicantUid 等字段没有在 schema 校验/转发时被丢弃。</td><td>修 MCP 后不改 shortcut,直接重跑真实命令;预期错误从 required 变为资源无效或成功。</td><td><code>tools/call</code></td><td><code>2104a64c17840997767792656e085e</code></td><td class="evidence">[UNCLASSIFIED] openCid or cid is required hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">22</td><td><code>chat +messages-list</code></td><td><span class="risk">read</span></td><td><span class="cat">后端/MCP</span></td><td><span class="owner">后端/MCP schema</span></td><td>CLI fake MCP 已证明字段已装配;需要修 MCP tool schema 或网关字段映射,确认 openConversationId/openCid/cid、receiverUid、applicantUid 等字段没有在 schema 校验/转发时被丢弃。</td><td>修 MCP 后不改 shortcut,直接重跑真实命令;预期错误从 required 变为资源无效或成功。</td><td><code>tools/call</code></td><td><code>2127d89817840997797873841e0757</code></td><td class="evidence">[UNCLASSIFIED] openCid or cid is required hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">23</td><td><code>chat +messages-resource-url</code></td><td><span class="risk">read</span></td><td><span class="cat">缺真实资源</span></td><td><span class="owner">测试数据</span></td><td>把 runner 里的安全负向 ID 换成真实资源 ID;当前错误说明调用已进后端,但资源不存在。</td><td>准备 fixture 后重跑 `chat +messages-resource-url`。</td><td><code>tools/call</code></td><td><code>2127d89817840997855423145e07dd</code></td><td class="evidence">[UNCLASSIFIED] failed to get download url for resourceId: x (operation: im/get_resource_download_url) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">24</td><td><code>chat +search-msg</code></td><td><span class="risk">read</span></td><td><span class="cat">输入/业务校验</span></td><td><span class="owner">测试数据</span></td><td>准备能命中的真实数据,或把 runner 查询词改成当前账号一定存在的对象;shortcut 本身不需要改。</td><td>造数后重跑,预期从 validation empty result 变为成功。</td><td><code>tools/call</code></td><td><code>0b5deb3217840997866824643e0853</code></td><td class="evidence">[UNCLASSIFIED] 当前用户暂无消息搜索权益,无法执行本次搜索。请提示用户开通消息搜索权益后重试。 hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">25</td><td><code>chat +thread-replies</code></td><td><span class="risk">read</span></td><td><span class="cat">缺真实资源</span></td><td><span class="owner">测试数据</span></td><td>把 runner 里的安全负向 ID 换成真实资源 ID;当前错误说明调用已进后端,但资源不存在。</td><td>准备 fixture 后重跑 `chat +thread-replies`。</td><td><code>tools/call</code></td><td><code>0b5deb3217840997883504915e0853</code></td><td class="evidence">[UNCLASSIFIED] failed to decrypt openConvThreadId hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">26</td><td><code>contact +get-roster</code></td><td><span class="risk">read</span></td><td><span class="cat">鉴权/权限</span></td><td><span class="owner">权限/应用配置</span></td><td>给当前登录账号、DWS 应用或对应资源补齐权限/scope;本仓库 shortcut 不应绕过权限。拿 trace_id 给服务端/开放平台排查具体 scope。</td><td>补权限后重跑 `contact +get-roster`;若仍是 permission,再看 trace_id。</td><td><code>tools/call</code></td><td><code>2106d98117840997920166915e087b</code></td><td class="evidence">[UNCLASSIFIED] 操作人无花名册管理权限 (operation: hrmregister/get_authorized_emp_rosterInfo) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">27</td><td><code>contact +list-roster-fields</code></td><td><span class="risk">read</span></td><td><span class="cat">鉴权/权限</span></td><td><span class="owner">权限/应用配置</span></td><td>给当前登录账号、DWS 应用或对应资源补齐权限/scope;本仓库 shortcut 不应绕过权限。拿 trace_id 给服务端/开放平台排查具体 scope。</td><td>补权限后重跑 `contact +list-roster-fields`;若仍是 permission,再看 trace_id。</td><td><code>tools/call</code></td><td><code>213ee25c17840997934295673e08e2</code></td><td class="evidence">[UNCLASSIFIED] 操作人无花名册管理权限 (operation: hrmregister/list_authorized_roster_fields) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">28</td><td><code>devapp +credentials-get</code></td><td><span class="risk">read</span></td><td><span class="cat">敏感/高风险暂缓</span></td><td><span class="owner">人工安全确认</span></td><td>该项涉及敏感读取或无安全负向目标,不适合自动用真实资源跑;需要在安全环境逐项人工确认。</td><td>人工确认后单独重跑 `devapp +credentials-get`,并避免在报告中泄露密钥/凭证。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">该命令会读取真实应用凭证/密钥;不能用真实 app 自动执行。当前仅用占位 ID 验证负向路径,真实成功需人工在安全环境单独确认。</td></tr>
<tr><td class="num">29</td><td><code>drive +download</code></td><td><span class="risk">read</span></td><td><span class="cat">输入/业务校验</span></td><td><span class="owner">测试输入/业务校验</span></td><td>当前命令已进入后端业务校验;先把测试输入换成真实合法 fixture,再判断是否需要改 shortcut。</td><td>重跑 `drive +download` 并比较 stdout/stderr。</td><td><code>tools/call</code></td><td><code>0bab027317840997956923965e08c9</code></td><td class="evidence">[UNCLASSIFIED] 该文件类型不支持通过 download_file 下载。download_file 仅支持普通文件(如 PDF、Word、Excel 等),不支持钉钉在线文档/表格/脑图等在线编辑类型。如需导出在线文档内容,请使用钉钉文档导出相关接口。 (operation: drive/download_file) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">30</td><td><code>drive +list</code></td><td><span class="risk">read</span></td><td><span class="cat">输入/业务校验</span></td><td><span class="owner">测试输入/业务校验</span></td><td>当前命令已进入后端业务校验;先把测试输入换成真实合法 fixture,再判断是否需要改 shortcut。</td><td>重跑 `drive +list` 并比较 stdout/stderr。</td><td><code>tools/call</code></td><td><code>2106d98117840997968627612e087b</code></td><td class="evidence">[UNCLASSIFIED] parentId 不属于指定的 spaceId,请确认 parentId 和 spaceId 属于同一个钉盘空间。 hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">31</td><td><code>minutes +action-items</code></td><td><span class="risk">read</span></td><td><span class="cat">缺妙记 fixture</span></td><td><span class="owner">测试数据</span></td><td>准备当前账号可见的真实妙记/听记/录制会话,或把 runner 的搜索关键词改成必然能命中的会议产物。</td><td>用真实 taskUuid/note/minutes 资源重跑 minutes 命令。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">暂无妙记</td></tr>
<tr><td class="num">32</td><td><code>minutes +latest-minutes</code></td><td><span class="risk">read</span></td><td><span class="cat">缺妙记 fixture</span></td><td><span class="owner">测试数据</span></td><td>准备当前账号可见的真实妙记/听记/录制会话,或把 runner 的搜索关键词改成必然能命中的会议产物。</td><td>用真实 taskUuid/note/minutes 资源重跑 minutes 命令。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">暂无妙记</td></tr>
<tr><td class="num">33</td><td><code>minutes +minutes-search</code></td><td><span class="risk">read</span></td><td><span class="cat">输入/业务校验</span></td><td><span class="owner">测试数据</span></td><td>准备能命中的真实数据,或把 runner 查询词改成当前账号一定存在的对象;shortcut 本身不需要改。</td><td>造数后重跑,预期从 validation empty result 变为成功。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">没搜到妙记</td></tr>
<tr><td class="num">34</td><td><code>minutes +transcript</code></td><td><span class="risk">read</span></td><td><span class="cat">缺妙记 fixture</span></td><td><span class="owner">测试数据</span></td><td>准备当前账号可见的真实妙记/听记/录制会话,或把 runner 的搜索关键词改成必然能命中的会议产物。</td><td>用真实 taskUuid/note/minutes 资源重跑 minutes 命令。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">暂无妙记</td></tr>
<tr><td class="num">35</td><td><code>oa +done-approvals</code></td><td><span class="risk">read</span></td><td><span class="cat">输入/业务校验</span></td><td><span class="owner">测试数据</span></td><td>准备能命中的真实数据,或把 runner 查询词改成当前账号一定存在的对象;shortcut 本身不需要改。</td><td>造数后重跑,预期从 validation empty result 变为成功。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">没有已处理的审批记录</td></tr>
<tr><td class="num">36</td><td><code>oa +pending</code></td><td><span class="risk">read</span></td><td><span class="cat">输入/业务校验</span></td><td><span class="owner">测试数据</span></td><td>准备能命中的真实数据,或把 runner 查询词改成当前账号一定存在的对象;shortcut 本身不需要改。</td><td>造数后重跑,预期从 validation empty result 变为成功。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">当前没有待我审批的任务</td></tr>
<tr><td class="num">37</td><td><code>report +report-latest</code></td><td><span class="risk">read</span></td><td><span class="cat">输入/业务校验</span></td><td><span class="owner">测试数据</span></td><td>准备能命中的真实数据,或把 runner 查询词改成当前账号一定存在的对象;shortcut 本身不需要改。</td><td>造数后重跑,预期从 validation empty result 变为成功。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">暂无日志</td></tr>
<tr><td class="num">38</td><td><code>todo +due-today</code></td><td><span class="risk">read</span></td><td><span class="cat">输入/业务校验</span></td><td><span class="owner">测试数据</span></td><td>准备能命中的真实数据,或把 runner 查询词改成当前账号一定存在的对象;shortcut 本身不需要改。</td><td>造数后重跑,预期从 validation empty result 变为成功。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">今天没有到期的待办</td></tr>
<tr><td class="num">39</td><td><code>todo +related-tasks</code></td><td><span class="risk">read</span></td><td><span class="cat">输入/业务校验</span></td><td><span class="owner">测试数据</span></td><td>准备能命中的真实数据,或把 runner 查询词改成当前账号一定存在的对象;shortcut 本身不需要改。</td><td>造数后重跑,预期从 validation empty result 变为成功。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">没有与你相关的待办(creator/executor/participant 三种角色下均为空)</td></tr>
<tr><td class="num">40</td><td><code>wiki +node-list</code></td><td><span class="risk">read</span></td><td><span class="cat">输入/业务校验</span></td><td><span class="owner">后端业务/测试 fixture</span></td><td>后端只返回 success=false,信息不足;先准备真实合法 fixture,若仍无细节,需要后端补充错误码/错误信息。</td><td>用真实资源重跑;若仍 success=false,把 operation+trace_id 给后端。</td><td><code>tools/call</code></td><td><code>2132f5ca17840998189126304e08d9</code></td><td class="evidence">[UNCLASSIFIED] business error: success=false hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">41</td><td><code>wiki +resolve-space</code></td><td><span class="risk">read</span></td><td><span class="cat">输入/业务校验</span></td><td><span class="owner">测试数据</span></td><td>准备能命中的真实数据,或把 runner 查询词改成当前账号一定存在的对象;shortcut 本身不需要改。</td><td>造数后重跑,预期从 validation empty result 变为成功。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">没有找到名称包含 DWS shortcut 真实测试 的知识空间</td></tr>
<tr><td class="num">42</td><td><code>wiki +space-list</code></td><td><span class="risk">read</span></td><td><span class="cat">输入/业务校验</span></td><td><span class="owner">测试输入/业务校验</span></td><td>当前命令已进入后端业务校验;先把测试输入换成真实合法 fixture,再判断是否需要改 shortcut。</td><td>重跑 `wiki +space-list` 并比较 stdout/stderr。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">参数 --type 取值 &quot;ALL&quot; 不合法,允许值:orgWikiSpace, myWikiSpace</td></tr>
</tbody>
</table>
<h2>Write 失败逐项 <span class="count">· 114 条</span></h2>
<table class="review">
<thead><tr>
<th>#</th><th>命令</th><th>风险</th><th>类型</th><th>要改哪里</th><th>具体改法</th><th>验证方式</th><th>operation</th><th>trace_id</th><th>证据</th>
</tr></thead>
<tbody>
<tr><td class="num">1</td><td><code>aitable +advperm-disable</code></td><td><span class="risk">high-risk-write</span></td><td><span class="cat">鉴权/权限</span></td><td><span class="owner">权限/应用配置</span></td><td>给当前登录账号、DWS 应用或对应资源补齐权限/scope;本仓库 shortcut 不应绕过权限。拿 trace_id 给服务端/开放平台排查具体 scope。</td><td>补权限后重跑 `aitable +advperm-disable`;若仍是 permission,再看 trace_id。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:&quot;INVALID_BASE_ID&quot;,&quot;message&quot;:&quot;baseId cannot be resolved to docId&quot;,&quot;retryable&quot;:false,&quot;type&quot;:&quot;INPUT_ERROR&quot;},&quot;meta&quot;:{},&quot;status&quot;:&quot;error&quot;,&quot;success&quot;:true,&quot;summary&quot;:&quot;Failed to set adv…</td></tr>
<tr><td class="num">2</td><td><code>aitable +advperm-enable</code></td><td><span class="risk">write</span></td><td><span class="cat">鉴权/权限</span></td><td><span class="owner">权限/应用配置</span></td><td>给当前登录账号、DWS 应用或对应资源补齐权限/scope;本仓库 shortcut 不应绕过权限。拿 trace_id 给服务端/开放平台排查具体 scope。</td><td>补权限后重跑 `aitable +advperm-enable`;若仍是 permission,再看 trace_id。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:&quot;INVALID_BASE_ID&quot;,&quot;message&quot;:&quot;baseId cannot be resolved to docId&quot;,&quot;retryable&quot;:false,&quot;type&quot;:&quot;INPUT_ERROR&quot;},&quot;meta&quot;:{},&quot;status&quot;:&quot;error&quot;,&quot;success&quot;:true,&quot;summary&quot;:&quot;Failed to set adv…</td></tr>
<tr><td class="num">3</td><td><code>aitable +attachment-upload</code></td><td><span class="risk">write</span></td><td><span class="cat">缺 AI 表格 fixture</span></td><td><span class="owner">测试数据</span></td><td>准备真实 Base/Table/View/Field/Record/Role/Chart/Dashboard 等 fixture,并把真实 ID 写入真实测试 runner;当前安全负向 ID 只能证明调用链,不可能成功。</td><td>fixture 准备好后重跑对应 aitable 命令;预期从 not_found/invalid_base_id 变为成功或更具体业务错误。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:&quot;BASE_NOT_FOUND&quot;,&quot;message&quot;:&quot;Specified base does not exist, has been deleted, or is inaccessible&quot;,&quot;retryable&quot;:false,&quot;type&quot;:&quot;INPUT_ERROR&quot;},&quot;meta&quot;:{},&quot;status&quot;:&quot;error&quot;,&quot;success&quot;:t…</td></tr>
<tr><td class="num">4</td><td><code>aitable +base-copy</code></td><td><span class="risk">write</span></td><td><span class="cat">缺 AI 表格 fixture</span></td><td><span class="owner">测试数据</span></td><td>准备真实 Base/Table/View/Field/Record/Role/Chart/Dashboard 等 fixture,并把真实 ID 写入真实测试 runner;当前安全负向 ID 只能证明调用链,不可能成功。</td><td>fixture 准备好后重跑对应 aitable 命令;预期从 not_found/invalid_base_id 变为成功或更具体业务错误。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:null,&quot;message&quot;:&quot;Invalid source baseId: DWSREALTESTNOSUCHID0000000000000&quot;,&quot;retryable&quot;:null,&quot;type&quot;:&quot;USER_ERROR&quot;},&quot;meta&quot;:{},&quot;status&quot;:&quot;error&quot;,&quot;success&quot;:true,&quot;summary&quot;:&quot;Invalid sou…</td></tr>
<tr><td class="num">5</td><td><code>aitable +base-delete</code></td><td><span class="risk">high-risk-write</span></td><td><span class="cat">缺真实资源</span></td><td><span class="owner">测试数据</span></td><td>把 runner 里的安全负向 ID 换成真实资源 ID;当前错误说明调用已进后端,但资源不存在。</td><td>准备 fixture 后重跑 `aitable +base-delete`。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:&quot;52600003&quot;,&quot;message&quot;:&quot;Data not found&quot;,&quot;retryable&quot;:true,&quot;type&quot;:&quot;SYSTEM_ERROR&quot;},&quot;meta&quot;:{},&quot;status&quot;:&quot;error&quot;,&quot;success&quot;:true,&quot;summary&quot;:&quot;Failed to delete base DWSREALTESTNOSUCHID000…</td></tr>
<tr><td class="num">6</td><td><code>aitable +base-update</code></td><td><span class="risk">write</span></td><td><span class="cat">缺 AI 表格 fixture</span></td><td><span class="owner">测试数据</span></td><td>准备真实 Base/Table/View/Field/Record/Role/Chart/Dashboard 等 fixture,并把真实 ID 写入真实测试 runner;当前安全负向 ID 只能证明调用链,不可能成功。</td><td>fixture 准备好后重跑对应 aitable 命令;预期从 not_found/invalid_base_id 变为成功或更具体业务错误。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:&quot;BASE_NOT_FOUND&quot;,&quot;message&quot;:&quot;Specified base does not exist, has been deleted, or is inaccessible&quot;,&quot;retryable&quot;:false,&quot;type&quot;:&quot;INPUT_ERROR&quot;},&quot;meta&quot;:{},&quot;status&quot;:&quot;error&quot;,&quot;success&quot;:t…</td></tr>
<tr><td class="num">7</td><td><code>aitable +chart-delete</code></td><td><span class="risk">high-risk-write</span></td><td><span class="cat">缺 AI 表格 fixture</span></td><td><span class="owner">测试数据</span></td><td>准备真实 Base/Table/View/Field/Record/Role/Chart/Dashboard 等 fixture,并把真实 ID 写入真实测试 runner;当前安全负向 ID 只能证明调用链,不可能成功。</td><td>fixture 准备好后重跑对应 aitable 命令;预期从 not_found/invalid_base_id 变为成功或更具体业务错误。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:&quot;INVALID_BASE_ID&quot;,&quot;message&quot;:&quot;baseId cannot be resolved to docId&quot;,&quot;retryable&quot;:false,&quot;type&quot;:&quot;INPUT_ERROR&quot;},&quot;meta&quot;:{},&quot;status&quot;:&quot;error&quot;,&quot;success&quot;:true,&quot;summary&quot;:&quot;Failed to delete …</td></tr>
<tr><td class="num">8</td><td><code>aitable +chart-share-update</code></td><td><span class="risk">write</span></td><td><span class="cat">缺 AI 表格 fixture</span></td><td><span class="owner">测试数据</span></td><td>准备真实 Base/Table/View/Field/Record/Role/Chart/Dashboard 等 fixture,并把真实 ID 写入真实测试 runner;当前安全负向 ID 只能证明调用链,不可能成功。</td><td>fixture 准备好后重跑对应 aitable 命令;预期从 not_found/invalid_base_id 变为成功或更具体业务错误。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:&quot;INVALID_BASE_ID&quot;,&quot;message&quot;:&quot;baseId cannot be resolved to docId&quot;,&quot;retryable&quot;:false,&quot;type&quot;:&quot;INPUT_ERROR&quot;},&quot;meta&quot;:{},&quot;status&quot;:&quot;error&quot;,&quot;success&quot;:true,&quot;summary&quot;:&quot;Failed to update …</td></tr>
<tr><td class="num">9</td><td><code>aitable +chart-update</code></td><td><span class="risk">write</span></td><td><span class="cat">后端/MCP</span></td><td><span class="owner">后端/MCP schema</span></td><td>修 aitable MCP wrapper 的参数校验和错误语义:不要返回 success=true+error;对 required 字段给出 CLI 可识别的参数名,系统错误要带 retryable/trace。</td><td>MCP 修完后重跑该 aitable 命令,并确认 stdout JSON 不再出现 success=true 但 error 非空。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:&quot;INVALID_PARAMS&quot;,&quot;message&quot;:&quot;config is required&quot;,&quot;retryable&quot;:false,&quot;type&quot;:&quot;INPUT_ERROR&quot;},&quot;meta&quot;:{},&quot;status&quot;:&quot;error&quot;,&quot;success&quot;:true,&quot;summary&quot;:&quot;Failed to update chart because con…</td></tr>
<tr><td class="num">10</td><td><code>aitable +dashboard-arrange</code></td><td><span class="risk">write</span></td><td><span class="cat">缺 AI 表格 fixture</span></td><td><span class="owner">测试数据</span></td><td>准备真实 Base/Table/View/Field/Record/Role/Chart/Dashboard 等 fixture,并把真实 ID 写入真实测试 runner;当前安全负向 ID 只能证明调用链,不可能成功。</td><td>fixture 准备好后重跑对应 aitable 命令;预期从 not_found/invalid_base_id 变为成功或更具体业务错误。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:&quot;INVALID_BASE_ID&quot;,&quot;message&quot;:&quot;baseId cannot be resolved to docId&quot;,&quot;retryable&quot;:false,&quot;type&quot;:&quot;INPUT_ERROR&quot;},&quot;meta&quot;:{},&quot;status&quot;:&quot;error&quot;,&quot;success&quot;:true,&quot;summary&quot;:&quot;Failed to align d…</td></tr>
<tr><td class="num">11</td><td><code>aitable +dashboard-delete</code></td><td><span class="risk">high-risk-write</span></td><td><span class="cat">缺 AI 表格 fixture</span></td><td><span class="owner">测试数据</span></td><td>准备真实 Base/Table/View/Field/Record/Role/Chart/Dashboard 等 fixture,并把真实 ID 写入真实测试 runner;当前安全负向 ID 只能证明调用链,不可能成功。</td><td>fixture 准备好后重跑对应 aitable 命令;预期从 not_found/invalid_base_id 变为成功或更具体业务错误。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:&quot;INVALID_BASE_ID&quot;,&quot;message&quot;:&quot;baseId cannot be resolved to docId&quot;,&quot;retryable&quot;:false,&quot;type&quot;:&quot;INPUT_ERROR&quot;},&quot;meta&quot;:{},&quot;status&quot;:&quot;error&quot;,&quot;success&quot;:true,&quot;summary&quot;:&quot;Failed to delete …</td></tr>
<tr><td class="num">12</td><td><code>aitable +dashboard-share-update</code></td><td><span class="risk">write</span></td><td><span class="cat">缺 AI 表格 fixture</span></td><td><span class="owner">测试数据</span></td><td>准备真实 Base/Table/View/Field/Record/Role/Chart/Dashboard 等 fixture,并把真实 ID 写入真实测试 runner;当前安全负向 ID 只能证明调用链,不可能成功。</td><td>fixture 准备好后重跑对应 aitable 命令;预期从 not_found/invalid_base_id 变为成功或更具体业务错误。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:&quot;INVALID_BASE_ID&quot;,&quot;message&quot;:&quot;baseId cannot be resolved to docId&quot;,&quot;retryable&quot;:false,&quot;type&quot;:&quot;INPUT_ERROR&quot;},&quot;meta&quot;:{},&quot;status&quot;:&quot;error&quot;,&quot;success&quot;:true,&quot;summary&quot;:&quot;Failed to update …</td></tr>
<tr><td class="num">13</td><td><code>aitable +dashboard-update</code></td><td><span class="risk">write</span></td><td><span class="cat">缺 AI 表格 fixture</span></td><td><span class="owner">测试数据</span></td><td>准备真实 Base/Table/View/Field/Record/Role/Chart/Dashboard 等 fixture,并把真实 ID 写入真实测试 runner;当前安全负向 ID 只能证明调用链,不可能成功。</td><td>fixture 准备好后重跑对应 aitable 命令;预期从 not_found/invalid_base_id 变为成功或更具体业务错误。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:&quot;INVALID_BASE_ID&quot;,&quot;message&quot;:&quot;baseId cannot be resolved to docId&quot;,&quot;retryable&quot;:false,&quot;type&quot;:&quot;INPUT_ERROR&quot;},&quot;meta&quot;:{},&quot;status&quot;:&quot;error&quot;,&quot;success&quot;:true,&quot;summary&quot;:&quot;Failed to update …</td></tr>
<tr><td class="num">14</td><td><code>aitable +field-delete</code></td><td><span class="risk">high-risk-write</span></td><td><span class="cat">后端/MCP</span></td><td><span class="owner">后端/MCP schema</span></td><td>修 aitable MCP wrapper 的参数校验和错误语义:不要返回 success=true+error;对 required 字段给出 CLI 可识别的参数名,系统错误要带 retryable/trace。</td><td>MCP 修完后重跑该 aitable 命令,并确认 stdout JSON 不再出现 success=true 但 error 非空。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:&quot;404&quot;,&quot;message&quot;:&quot;getDentryDTO returns null&quot;,&quot;retryable&quot;:true,&quot;type&quot;:&quot;SYSTEM_ERROR&quot;},&quot;meta&quot;:{},&quot;status&quot;:&quot;error&quot;,&quot;success&quot;:true,&quot;summary&quot;:&quot;Failed to get current field info befor…</td></tr>
<tr><td class="num">15</td><td><code>aitable +field-update</code></td><td><span class="risk">write</span></td><td><span class="cat">后端/MCP</span></td><td><span class="owner">后端/MCP schema</span></td><td>修 aitable MCP wrapper 的参数校验和错误语义:不要返回 success=true+error;对 required 字段给出 CLI 可识别的参数名,系统错误要带 retryable/trace。</td><td>MCP 修完后重跑该 aitable 命令,并确认 stdout JSON 不再出现 success=true 但 error 非空。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:&quot;404&quot;,&quot;message&quot;:&quot;getDentryDTO returns null&quot;,&quot;retryable&quot;:true,&quot;type&quot;:&quot;SYSTEM_ERROR&quot;},&quot;meta&quot;:{},&quot;status&quot;:&quot;error&quot;,&quot;success&quot;:true,&quot;summary&quot;:&quot;Failed to get current field info for u…</td></tr>
<tr><td class="num">16</td><td><code>aitable +form-delete</code></td><td><span class="risk">high-risk-write</span></td><td><span class="cat">缺 AI 表格 fixture</span></td><td><span class="owner">测试数据</span></td><td>准备真实 Base/Table/View/Field/Record/Role/Chart/Dashboard 等 fixture,并把真实 ID 写入真实测试 runner;当前安全负向 ID 只能证明调用链,不可能成功。</td><td>fixture 准备好后重跑对应 aitable 命令;预期从 not_found/invalid_base_id 变为成功或更具体业务错误。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:&quot;INVALID_BASE_ID&quot;,&quot;message&quot;:&quot;baseId cannot be resolved to docId&quot;,&quot;retryable&quot;:false,&quot;type&quot;:&quot;INPUT_ERROR&quot;},&quot;meta&quot;:{},&quot;status&quot;:&quot;error&quot;,&quot;success&quot;:true,&quot;summary&quot;:&quot;Failed to delete …</td></tr>
<tr><td class="num">17</td><td><code>aitable +form-field-hide</code></td><td><span class="risk">write</span></td><td><span class="cat">缺 AI 表格 fixture</span></td><td><span class="owner">测试数据</span></td><td>准备真实 Base/Table/View/Field/Record/Role/Chart/Dashboard 等 fixture,并把真实 ID 写入真实测试 runner;当前安全负向 ID 只能证明调用链,不可能成功。</td><td>fixture 准备好后重跑对应 aitable 命令;预期从 not_found/invalid_base_id 变为成功或更具体业务错误。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:&quot;INVALID_BASE_ID&quot;,&quot;message&quot;:&quot;baseId cannot be resolved to docId&quot;,&quot;retryable&quot;:false,&quot;type&quot;:&quot;INPUT_ERROR&quot;},&quot;meta&quot;:{},&quot;status&quot;:&quot;error&quot;,&quot;success&quot;:true,&quot;summary&quot;:&quot;Failed to update …</td></tr>
<tr><td class="num">18</td><td><code>aitable +form-field-update</code></td><td><span class="risk">write</span></td><td><span class="cat">缺 AI 表格 fixture</span></td><td><span class="owner">测试数据</span></td><td>准备真实 Base/Table/View/Field/Record/Role/Chart/Dashboard 等 fixture,并把真实 ID 写入真实测试 runner;当前安全负向 ID 只能证明调用链,不可能成功。</td><td>fixture 准备好后重跑对应 aitable 命令;预期从 not_found/invalid_base_id 变为成功或更具体业务错误。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:&quot;INVALID_BASE_ID&quot;,&quot;message&quot;:&quot;baseId cannot be resolved to docId&quot;,&quot;retryable&quot;:false,&quot;type&quot;:&quot;INPUT_ERROR&quot;},&quot;meta&quot;:{},&quot;status&quot;:&quot;error&quot;,&quot;success&quot;:true,&quot;summary&quot;:&quot;Failed to update …</td></tr>
<tr><td class="num">19</td><td><code>aitable +form-share-update</code></td><td><span class="risk">write</span></td><td><span class="cat">缺 AI 表格 fixture</span></td><td><span class="owner">测试数据</span></td><td>准备真实 Base/Table/View/Field/Record/Role/Chart/Dashboard 等 fixture,并把真实 ID 写入真实测试 runner;当前安全负向 ID 只能证明调用链,不可能成功。</td><td>fixture 准备好后重跑对应 aitable 命令;预期从 not_found/invalid_base_id 变为成功或更具体业务错误。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:&quot;INVALID_BASE_ID&quot;,&quot;message&quot;:&quot;baseId cannot be resolved to docId&quot;,&quot;retryable&quot;:false,&quot;type&quot;:&quot;INPUT_ERROR&quot;},&quot;meta&quot;:{},&quot;status&quot;:&quot;error&quot;,&quot;success&quot;:true,&quot;summary&quot;:&quot;Failed to update …</td></tr>
<tr><td class="num">20</td><td><code>aitable +form-update</code></td><td><span class="risk">write</span></td><td><span class="cat">缺 AI 表格 fixture</span></td><td><span class="owner">测试数据</span></td><td>准备真实 Base/Table/View/Field/Record/Role/Chart/Dashboard 等 fixture,并把真实 ID 写入真实测试 runner;当前安全负向 ID 只能证明调用链,不可能成功。</td><td>fixture 准备好后重跑对应 aitable 命令;预期从 not_found/invalid_base_id 变为成功或更具体业务错误。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:&quot;INVALID_BASE_ID&quot;,&quot;message&quot;:&quot;baseId cannot be resolved to docId&quot;,&quot;retryable&quot;:false,&quot;type&quot;:&quot;INPUT_ERROR&quot;},&quot;meta&quot;:{},&quot;status&quot;:&quot;error&quot;,&quot;success&quot;:true,&quot;summary&quot;:&quot;Failed to update …</td></tr>
<tr><td class="num">21</td><td><code>aitable +import-data</code></td><td><span class="risk">write</span></td><td><span class="cat">缺真实资源</span></td><td><span class="owner">测试数据</span></td><td>把 runner 里的安全负向 ID 换成真实资源 ID;当前错误说明调用已进后端,但资源不存在。</td><td>准备 fixture 后重跑 `aitable +import-data`。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:&quot;INVALID_IMPORT_ID&quot;,&quot;message&quot;:&quot;importId not found: either invalid or expired&quot;,&quot;retryable&quot;:false,&quot;type&quot;:&quot;INPUT_ERROR&quot;},&quot;meta&quot;:{},&quot;status&quot;:&quot;error&quot;,&quot;success&quot;:true,&quot;summary&quot;:&quot;impo…</td></tr>
<tr><td class="num">22</td><td><code>aitable +import-upload</code></td><td><span class="risk">write</span></td><td><span class="cat">缺 AI 表格 fixture</span></td><td><span class="owner">测试数据</span></td><td>准备真实 Base/Table/View/Field/Record/Role/Chart/Dashboard 等 fixture,并把真实 ID 写入真实测试 runner;当前安全负向 ID 只能证明调用链,不可能成功。</td><td>fixture 准备好后重跑对应 aitable 命令;预期从 not_found/invalid_base_id 变为成功或更具体业务错误。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:&quot;INVALID_BASE_ID&quot;,&quot;message&quot;:&quot;无法解析 baseId&quot;,&quot;retryable&quot;:false,&quot;type&quot;:&quot;INPUT_ERROR&quot;},&quot;meta&quot;:{},&quot;status&quot;:&quot;error&quot;,&quot;success&quot;:true,&quot;summary&quot;:&quot;无效的 baseId&quot;,&quot;trace_id&quot;:&quot;0bab027317840998…</td></tr>
<tr><td class="num">23</td><td><code>aitable +record-delete</code></td><td><span class="risk">high-risk-write</span></td><td><span class="cat">后端/MCP</span></td><td><span class="owner">后端/MCP schema</span></td><td>修 aitable MCP wrapper 的参数校验和错误语义:不要返回 success=true+error;对 required 字段给出 CLI 可识别的参数名,系统错误要带 retryable/trace。</td><td>MCP 修完后重跑该 aitable 命令,并确认 stdout JSON 不再出现 success=true 但 error 非空。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:&quot;404&quot;,&quot;message&quot;:&quot;getDentryDTO returns null&quot;,&quot;retryable&quot;:true,&quot;type&quot;:&quot;SYSTEM_ERROR&quot;},&quot;meta&quot;:{},&quot;status&quot;:&quot;error&quot;,&quot;success&quot;:true,&quot;summary&quot;:&quot;Failed to delete records&quot;,&quot;trace_id&quot;:&quot;…</td></tr>
<tr><td class="num">24</td><td><code>aitable +record-primary-doc-create</code></td><td><span class="risk">write</span></td><td><span class="cat">缺 AI 表格 fixture</span></td><td><span class="owner">测试数据</span></td><td>准备真实 Base/Table/View/Field/Record/Role/Chart/Dashboard 等 fixture,并把真实 ID 写入真实测试 runner;当前安全负向 ID 只能证明调用链,不可能成功。</td><td>fixture 准备好后重跑对应 aitable 命令;预期从 not_found/invalid_base_id 变为成功或更具体业务错误。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:&quot;RESOLVE_DOC_ID_ERROR&quot;,&quot;message&quot;:&quot;Failed to resolve docId from baseId&quot;,&quot;retryable&quot;:true,&quot;type&quot;:&quot;SYSTEM_ERROR&quot;},&quot;meta&quot;:{},&quot;status&quot;:&quot;error&quot;,&quot;success&quot;:true,&quot;summary&quot;:&quot;Failed to c…</td></tr>
<tr><td class="num">25</td><td><code>aitable +record-update</code></td><td><span class="risk">write</span></td><td><span class="cat">后端/MCP</span></td><td><span class="owner">后端/MCP schema</span></td><td>修 aitable MCP wrapper 的参数校验和错误语义:不要返回 success=true+error;对 required 字段给出 CLI 可识别的参数名,系统错误要带 retryable/trace。</td><td>MCP 修完后重跑该 aitable 命令,并确认 stdout JSON 不再出现 success=true 但 error 非空。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:&quot;INVALID_RECORDS&quot;,&quot;message&quot;:&quot;records must contain at least one writable record&quot;,&quot;retryable&quot;:false,&quot;type&quot;:&quot;INPUT_ERROR&quot;},&quot;meta&quot;:{},&quot;status&quot;:&quot;error&quot;,&quot;success&quot;:true,&quot;summary&quot;:&quot;Fa…</td></tr>
<tr><td class="num">26</td><td><code>aitable +record-upsert</code></td><td><span class="risk">write</span></td><td><span class="cat">后端/MCP</span></td><td><span class="owner">后端/MCP schema</span></td><td>修 aitable MCP wrapper 的参数校验和错误语义:不要返回 success=true+error;对 required 字段给出 CLI 可识别的参数名,系统错误要带 retryable/trace。</td><td>MCP 修完后重跑该 aitable 命令,并确认 stdout JSON 不再出现 success=true 但 error 非空。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:&quot;INVALID_PARAMETER&quot;,&quot;message&quot;:&quot;records is required and must not be empty&quot;,&quot;retryable&quot;:false,&quot;type&quot;:&quot;INPUT_ERROR&quot;},&quot;meta&quot;:{},&quot;status&quot;:&quot;error&quot;,&quot;success&quot;:true,&quot;summary&quot;:&quot;records …</td></tr>
<tr><td class="num">27</td><td><code>aitable +role-create</code></td><td><span class="risk">write</span></td><td><span class="cat">缺 AI 表格 fixture</span></td><td><span class="owner">测试数据</span></td><td>准备真实 Base/Table/View/Field/Record/Role/Chart/Dashboard 等 fixture,并把真实 ID 写入真实测试 runner;当前安全负向 ID 只能证明调用链,不可能成功。</td><td>fixture 准备好后重跑对应 aitable 命令;预期从 not_found/invalid_base_id 变为成功或更具体业务错误。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:&quot;INVALID_BASE_ID&quot;,&quot;message&quot;:&quot;baseId cannot be resolved to docId&quot;,&quot;retryable&quot;:false,&quot;type&quot;:&quot;INPUT_ERROR&quot;},&quot;meta&quot;:{},&quot;status&quot;:&quot;error&quot;,&quot;success&quot;:true,&quot;summary&quot;:&quot;Failed to create …</td></tr>
<tr><td class="num">28</td><td><code>aitable +role-delete</code></td><td><span class="risk">high-risk-write</span></td><td><span class="cat">后端/MCP</span></td><td><span class="owner">后端/MCP schema</span></td><td>修 aitable MCP wrapper 的参数校验和错误语义:不要返回 success=true+error;对 required 字段给出 CLI 可识别的参数名,系统错误要带 retryable/trace。</td><td>MCP 修完后重跑该 aitable 命令,并确认 stdout JSON 不再出现 success=true 但 error 非空。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:&quot;INVALID_PARAMS&quot;,&quot;message&quot;:&quot;roleId is required&quot;,&quot;retryable&quot;:false,&quot;type&quot;:&quot;INPUT_ERROR&quot;},&quot;meta&quot;:{},&quot;status&quot;:&quot;error&quot;,&quot;success&quot;:true,&quot;summary&quot;:&quot;Failed to delete role because role…</td></tr>
<tr><td class="num">29</td><td><code>aitable +role-update</code></td><td><span class="risk">write</span></td><td><span class="cat">后端/MCP</span></td><td><span class="owner">后端/MCP schema</span></td><td>修 aitable MCP wrapper 的参数校验和错误语义:不要返回 success=true+error;对 required 字段给出 CLI 可识别的参数名,系统错误要带 retryable/trace。</td><td>MCP 修完后重跑该 aitable 命令,并确认 stdout JSON 不再出现 success=true 但 error 非空。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:&quot;INVALID_PARAMS&quot;,&quot;message&quot;:&quot;roleId is required&quot;,&quot;retryable&quot;:false,&quot;type&quot;:&quot;INPUT_ERROR&quot;},&quot;meta&quot;:{},&quot;status&quot;:&quot;error&quot;,&quot;success&quot;:true,&quot;summary&quot;:&quot;Failed to patch role because roleI…</td></tr>
<tr><td class="num">30</td><td><code>aitable +section-create</code></td><td><span class="risk">write</span></td><td><span class="cat">缺 AI 表格 fixture</span></td><td><span class="owner">测试数据</span></td><td>准备真实 Base/Table/View/Field/Record/Role/Chart/Dashboard 等 fixture,并把真实 ID 写入真实测试 runner;当前安全负向 ID 只能证明调用链,不可能成功。</td><td>fixture 准备好后重跑对应 aitable 命令;预期从 not_found/invalid_base_id 变为成功或更具体业务错误。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:&quot;INVALID_BASE_ID&quot;,&quot;message&quot;:&quot;baseId cannot be resolved to docId&quot;,&quot;retryable&quot;:false,&quot;type&quot;:&quot;INPUT_ERROR&quot;},&quot;meta&quot;:{},&quot;status&quot;:&quot;error&quot;,&quot;success&quot;:true,&quot;summary&quot;:&quot;Failed to create …</td></tr>
<tr><td class="num">31</td><td><code>aitable +section-delete</code></td><td><span class="risk">high-risk-write</span></td><td><span class="cat">缺 AI 表格 fixture</span></td><td><span class="owner">测试数据</span></td><td>准备真实 Base/Table/View/Field/Record/Role/Chart/Dashboard 等 fixture,并把真实 ID 写入真实测试 runner;当前安全负向 ID 只能证明调用链,不可能成功。</td><td>fixture 准备好后重跑对应 aitable 命令;预期从 not_found/invalid_base_id 变为成功或更具体业务错误。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:&quot;INVALID_BASE_ID&quot;,&quot;message&quot;:&quot;baseId cannot be resolved to docId&quot;,&quot;retryable&quot;:false,&quot;type&quot;:&quot;INPUT_ERROR&quot;},&quot;meta&quot;:{},&quot;status&quot;:&quot;error&quot;,&quot;success&quot;:true,&quot;summary&quot;:&quot;Failed to delete …</td></tr>
<tr><td class="num">32</td><td><code>aitable +section-move-node</code></td><td><span class="risk">write</span></td><td><span class="cat">缺 AI 表格 fixture</span></td><td><span class="owner">测试数据</span></td><td>准备真实 Base/Table/View/Field/Record/Role/Chart/Dashboard 等 fixture,并把真实 ID 写入真实测试 runner;当前安全负向 ID 只能证明调用链,不可能成功。</td><td>fixture 准备好后重跑对应 aitable 命令;预期从 not_found/invalid_base_id 变为成功或更具体业务错误。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:&quot;INVALID_BASE_ID&quot;,&quot;message&quot;:&quot;baseId cannot be resolved to docId&quot;,&quot;retryable&quot;:false,&quot;type&quot;:&quot;INPUT_ERROR&quot;},&quot;meta&quot;:{},&quot;status&quot;:&quot;error&quot;,&quot;success&quot;:true,&quot;summary&quot;:&quot;Failed to move no…</td></tr>
<tr><td class="num">33</td><td><code>aitable +section-rename</code></td><td><span class="risk">write</span></td><td><span class="cat">缺 AI 表格 fixture</span></td><td><span class="owner">测试数据</span></td><td>准备真实 Base/Table/View/Field/Record/Role/Chart/Dashboard 等 fixture,并把真实 ID 写入真实测试 runner;当前安全负向 ID 只能证明调用链,不可能成功。</td><td>fixture 准备好后重跑对应 aitable 命令;预期从 not_found/invalid_base_id 变为成功或更具体业务错误。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:&quot;INVALID_BASE_ID&quot;,&quot;message&quot;:&quot;baseId cannot be resolved to docId&quot;,&quot;retryable&quot;:false,&quot;type&quot;:&quot;INPUT_ERROR&quot;},&quot;meta&quot;:{},&quot;status&quot;:&quot;error&quot;,&quot;success&quot;:true,&quot;summary&quot;:&quot;Failed to rename …</td></tr>
<tr><td class="num">34</td><td><code>aitable +section-reorder</code></td><td><span class="risk">write</span></td><td><span class="cat">缺 AI 表格 fixture</span></td><td><span class="owner">测试数据</span></td><td>准备真实 Base/Table/View/Field/Record/Role/Chart/Dashboard 等 fixture,并把真实 ID 写入真实测试 runner;当前安全负向 ID 只能证明调用链,不可能成功。</td><td>fixture 准备好后重跑对应 aitable 命令;预期从 not_found/invalid_base_id 变为成功或更具体业务错误。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:&quot;INVALID_BASE_ID&quot;,&quot;message&quot;:&quot;baseId cannot be resolved to docId&quot;,&quot;retryable&quot;:false,&quot;type&quot;:&quot;INPUT_ERROR&quot;},&quot;meta&quot;:{},&quot;status&quot;:&quot;error&quot;,&quot;success&quot;:true,&quot;summary&quot;:&quot;Failed to reorder…</td></tr>
<tr><td class="num">35</td><td><code>aitable +table-delete</code></td><td><span class="risk">high-risk-write</span></td><td><span class="cat">缺 AI 表格 fixture</span></td><td><span class="owner">测试数据</span></td><td>准备真实 Base/Table/View/Field/Record/Role/Chart/Dashboard 等 fixture,并把真实 ID 写入真实测试 runner;当前安全负向 ID 只能证明调用链,不可能成功。</td><td>fixture 准备好后重跑对应 aitable 命令;预期从 not_found/invalid_base_id 变为成功或更具体业务错误。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:&quot;BASE_NOT_FOUND&quot;,&quot;message&quot;:&quot;Specified base does not exist, has been deleted, or is inaccessible&quot;,&quot;retryable&quot;:false,&quot;type&quot;:&quot;INPUT_ERROR&quot;},&quot;meta&quot;:{},&quot;status&quot;:&quot;error&quot;,&quot;success&quot;:t…</td></tr>
<tr><td class="num">36</td><td><code>aitable +table-update</code></td><td><span class="risk">write</span></td><td><span class="cat">后端/MCP</span></td><td><span class="owner">后端/MCP schema</span></td><td>修 aitable MCP wrapper 的参数校验和错误语义:不要返回 success=true+error;对 required 字段给出 CLI 可识别的参数名,系统错误要带 retryable/trace。</td><td>MCP 修完后重跑该 aitable 命令,并确认 stdout JSON 不再出现 success=true 但 error 非空。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:&quot;404&quot;,&quot;message&quot;:&quot;getDentryDTO returns null&quot;,&quot;retryable&quot;:true,&quot;type&quot;:&quot;SYSTEM_ERROR&quot;},&quot;meta&quot;:{},&quot;status&quot;:&quot;error&quot;,&quot;success&quot;:true,&quot;summary&quot;:&quot;Failed to update table DWSREALTESTNOSU…</td></tr>
<tr><td class="num">37</td><td><code>aitable +view-delete</code></td><td><span class="risk">high-risk-write</span></td><td><span class="cat">缺 AI 表格 fixture</span></td><td><span class="owner">测试数据</span></td><td>准备真实 Base/Table/View/Field/Record/Role/Chart/Dashboard 等 fixture,并把真实 ID 写入真实测试 runner;当前安全负向 ID 只能证明调用链,不可能成功。</td><td>fixture 准备好后重跑对应 aitable 命令;预期从 not_found/invalid_base_id 变为成功或更具体业务错误。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:&quot;INVALID_BASE_ID&quot;,&quot;message&quot;:&quot;baseId cannot be resolved to docId&quot;,&quot;retryable&quot;:false,&quot;type&quot;:&quot;INPUT_ERROR&quot;},&quot;meta&quot;:{},&quot;status&quot;:&quot;error&quot;,&quot;success&quot;:true,&quot;summary&quot;:&quot;Failed to delete …</td></tr>
<tr><td class="num">38</td><td><code>aitable +view-duplicate</code></td><td><span class="risk">write</span></td><td><span class="cat">缺 AI 表格 fixture</span></td><td><span class="owner">测试数据</span></td><td>准备真实 Base/Table/View/Field/Record/Role/Chart/Dashboard 等 fixture,并把真实 ID 写入真实测试 runner;当前安全负向 ID 只能证明调用链,不可能成功。</td><td>fixture 准备好后重跑对应 aitable 命令;预期从 not_found/invalid_base_id 变为成功或更具体业务错误。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:&quot;INVALID_BASE_ID&quot;,&quot;message&quot;:&quot;baseId cannot be resolved to docId&quot;,&quot;retryable&quot;:false,&quot;type&quot;:&quot;INPUT_ERROR&quot;},&quot;meta&quot;:{},&quot;status&quot;:&quot;error&quot;,&quot;success&quot;:true,&quot;summary&quot;:&quot;Failed to duplica…</td></tr>
<tr><td class="num">39</td><td><code>aitable +view-lock</code></td><td><span class="risk">write</span></td><td><span class="cat">缺 AI 表格 fixture</span></td><td><span class="owner">测试数据</span></td><td>准备真实 Base/Table/View/Field/Record/Role/Chart/Dashboard 等 fixture,并把真实 ID 写入真实测试 runner;当前安全负向 ID 只能证明调用链,不可能成功。</td><td>fixture 准备好后重跑对应 aitable 命令;预期从 not_found/invalid_base_id 变为成功或更具体业务错误。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:&quot;INVALID_BASE_ID&quot;,&quot;message&quot;:&quot;baseId cannot be resolved to docId&quot;,&quot;retryable&quot;:false,&quot;type&quot;:&quot;INPUT_ERROR&quot;},&quot;meta&quot;:{},&quot;status&quot;:&quot;error&quot;,&quot;success&quot;:true,&quot;summary&quot;:&quot;Failed to lock_or…</td></tr>
<tr><td class="num">40</td><td><code>aitable +view-set-fill-color-rule</code></td><td><span class="risk">write</span></td><td><span class="cat">后端/MCP</span></td><td><span class="owner">后端/MCP schema</span></td><td>修 aitable MCP wrapper 的参数校验和错误语义:不要返回 success=true+error;对 required 字段给出 CLI 可识别的参数名,系统错误要带 retryable/trace。</td><td>MCP 修完后重跑该 aitable 命令,并确认 stdout JSON 不再出现 success=true 但 error 非空。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:&quot;INVALID_PARAMS&quot;,&quot;message&quot;:&quot;conditionalFormats is required&quot;,&quot;retryable&quot;:false,&quot;type&quot;:&quot;INPUT_ERROR&quot;},&quot;meta&quot;:{},&quot;status&quot;:&quot;error&quot;,&quot;success&quot;:true,&quot;summary&quot;:&quot;Failed to set fill col…</td></tr>
<tr><td class="num">41</td><td><code>aitable +view-set-frozen-cols</code></td><td><span class="risk">write</span></td><td><span class="cat">缺 AI 表格 fixture</span></td><td><span class="owner">测试数据</span></td><td>准备真实 Base/Table/View/Field/Record/Role/Chart/Dashboard 等 fixture,并把真实 ID 写入真实测试 runner;当前安全负向 ID 只能证明调用链,不可能成功。</td><td>fixture 准备好后重跑对应 aitable 命令;预期从 not_found/invalid_base_id 变为成功或更具体业务错误。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:&quot;INVALID_BASE_ID&quot;,&quot;message&quot;:&quot;baseId cannot be resolved to docId&quot;,&quot;retryable&quot;:false,&quot;type&quot;:&quot;INPUT_ERROR&quot;},&quot;meta&quot;:{},&quot;status&quot;:&quot;error&quot;,&quot;success&quot;:true,&quot;summary&quot;:&quot;Failed to set fro…</td></tr>
<tr><td class="num">42</td><td><code>aitable +view-set-row-height</code></td><td><span class="risk">write</span></td><td><span class="cat">缺 AI 表格 fixture</span></td><td><span class="owner">测试数据</span></td><td>准备真实 Base/Table/View/Field/Record/Role/Chart/Dashboard 等 fixture,并把真实 ID 写入真实测试 runner;当前安全负向 ID 只能证明调用链,不可能成功。</td><td>fixture 准备好后重跑对应 aitable 命令;预期从 not_found/invalid_base_id 变为成功或更具体业务错误。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:&quot;INVALID_BASE_ID&quot;,&quot;message&quot;:&quot;baseId cannot be resolved to docId&quot;,&quot;retryable&quot;:false,&quot;type&quot;:&quot;INPUT_ERROR&quot;},&quot;meta&quot;:{},&quot;status&quot;:&quot;error&quot;,&quot;success&quot;:true,&quot;summary&quot;:&quot;Failed to set cel…</td></tr>
<tr><td class="num">43</td><td><code>aitable +view-update</code></td><td><span class="risk">write</span></td><td><span class="cat">缺 AI 表格 fixture</span></td><td><span class="owner">测试数据</span></td><td>准备真实 Base/Table/View/Field/Record/Role/Chart/Dashboard 等 fixture,并把真实 ID 写入真实测试 runner;当前安全负向 ID 只能证明调用链,不可能成功。</td><td>fixture 准备好后重跑对应 aitable 命令;预期从 not_found/invalid_base_id 变为成功或更具体业务错误。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:&quot;INVALID_BASE_ID&quot;,&quot;message&quot;:&quot;baseId cannot be resolved to docId&quot;,&quot;retryable&quot;:false,&quot;type&quot;:&quot;INPUT_ERROR&quot;},&quot;meta&quot;:{},&quot;status&quot;:&quot;error&quot;,&quot;success&quot;:true,&quot;summary&quot;:&quot;Failed to update …</td></tr>
<tr><td class="num">44</td><td><code>aitable +workflow-disable</code></td><td><span class="risk">high-risk-write</span></td><td><span class="cat">缺 AI 表格 fixture</span></td><td><span class="owner">测试数据</span></td><td>准备真实 Base/Table/View/Field/Record/Role/Chart/Dashboard 等 fixture,并把真实 ID 写入真实测试 runner;当前安全负向 ID 只能证明调用链,不可能成功。</td><td>fixture 准备好后重跑对应 aitable 命令;预期从 not_found/invalid_base_id 变为成功或更具体业务错误。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:&quot;BASE_NOT_FOUND&quot;,&quot;message&quot;:&quot;Cannot resolve base &#x27;DWSREALTESTNOSUCHID0000000000000&#x27;, please check if the baseId is valid&quot;,&quot;retryable&quot;:false,&quot;type&quot;:&quot;INPUT_ERROR&quot;},&quot;meta&quot;:{},&quot;sta…</td></tr>
<tr><td class="num">45</td><td><code>aitable +workflow-enable</code></td><td><span class="risk">write</span></td><td><span class="cat">缺 AI 表格 fixture</span></td><td><span class="owner">测试数据</span></td><td>准备真实 Base/Table/View/Field/Record/Role/Chart/Dashboard 等 fixture,并把真实 ID 写入真实测试 runner;当前安全负向 ID 只能证明调用链,不可能成功。</td><td>fixture 准备好后重跑对应 aitable 命令;预期从 not_found/invalid_base_id 变为成功或更具体业务错误。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">[MCP_TOOL_ERROR] {&quot;data&quot;:{},&quot;error&quot;:{&quot;code&quot;:&quot;BASE_NOT_FOUND&quot;,&quot;message&quot;:&quot;Cannot resolve base &#x27;DWSREALTESTNOSUCHID0000000000000&#x27;, please check if the baseId is valid&quot;,&quot;retryable&quot;:false,&quot;type&quot;:&quot;INPUT_ERROR&quot;},&quot;meta&quot;:{},&quot;sta…</td></tr>
<tr><td class="num">46</td><td><code>attendance +boss-check</code></td><td><span class="risk">write</span></td><td><span class="cat">输入/业务校验</span></td><td><span class="owner">后端业务/测试 fixture</span></td><td>后端只返回 success=false,信息不足;先准备真实合法 fixture,若仍无细节,需要后端补充错误码/错误信息。</td><td>用真实资源重跑;若仍 success=false,把 operation+trace_id 给后端。</td><td><code>tools/call</code></td><td><code>213ee25c17840998998942184e087d</code></td><td class="evidence">[UNCLASSIFIED] business error: success=false (operation: attendance-wukong/boss_check) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">47</td><td><code>attendance +create-class</code></td><td><span class="risk">write</span></td><td><span class="cat">输入/业务校验</span></td><td><span class="owner">后端业务/测试 fixture</span></td><td>后端只返回 success=false,信息不足;先准备真实合法 fixture,若仍无细节,需要后端补充错误码/错误信息。</td><td>用真实资源重跑;若仍 success=false,把 operation+trace_id 给后端。</td><td><code>tools/call</code></td><td><code>0bab027317840999009926838e090b</code></td><td class="evidence">[UNCLASSIFIED] business error: success=false (operation: attendance-wukong/create_class_setting) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">48</td><td><code>attendance +create-group</code></td><td><span class="risk">write</span></td><td><span class="cat">输入/业务校验</span></td><td><span class="owner">后端业务/测试 fixture</span></td><td>后端只返回 success=false,信息不足;先准备真实合法 fixture,若仍无细节,需要后端补充错误码/错误信息。</td><td>用真实资源重跑;若仍 success=false,把 operation+trace_id 给后端。</td><td><code>tools/call</code></td><td><code>2106d98117840999021121258e08b8</code></td><td class="evidence">[UNCLASSIFIED] business error: success=false (operation: attendance-wukong/create_group_setting) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">49</td><td><code>attendance +import-schedule</code></td><td><span class="risk">write</span></td><td><span class="cat">输入/业务校验</span></td><td><span class="owner">后端业务/测试 fixture</span></td><td>后端只返回 success=false,信息不足;先准备真实合法 fixture,若仍无细节,需要后端补充错误码/错误信息。</td><td>用真实资源重跑;若仍 success=false,把 operation+trace_id 给后端。</td><td><code>tools/call</code></td><td><code>2104a64c17840999034845189e085e</code></td><td class="evidence">[UNCLASSIFIED] business error: success=false (operation: attendance-wukong/generateTurnSchedule) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">50</td><td><code>attendance +save-leave-balance</code></td><td><span class="risk">write</span></td><td><span class="cat">鉴权/权限</span></td><td><span class="owner">权限/应用配置</span></td><td>给当前登录账号、DWS 应用或对应资源补齐权限/scope;本仓库 shortcut 不应绕过权限。拿 trace_id 给服务端/开放平台排查具体 scope。</td><td>补权限后重跑 `attendance +save-leave-balance`;若仍是 permission,再看 trace_id。</td><td><code>tools/call</code></td><td><code>2127d89817840999045751134e079c</code></td><td class="evidence">[UNCLASSIFIED] 无权更新指定员工的假期余额 (operation: attendance-wukong/update_leave_balance) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">51</td><td><code>attendance +update-class</code></td><td><span class="risk">write</span></td><td><span class="cat">输入/业务校验</span></td><td><span class="owner">后端业务/测试 fixture</span></td><td>后端只返回 success=false,信息不足;先准备真实合法 fixture,若仍无细节,需要后端补充错误码/错误信息。</td><td>用真实资源重跑;若仍 success=false,把 operation+trace_id 给后端。</td><td><code>tools/call</code></td><td><code>0b5deb3217840999058236471e08b5</code></td><td class="evidence">[UNCLASSIFIED] business error: success=false (operation: attendance-wukong/update_class_setting) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">52</td><td><code>attendance +update-group</code></td><td><span class="risk">write</span></td><td><span class="cat">输入/业务校验</span></td><td><span class="owner">后端业务/测试 fixture</span></td><td>后端只返回 success=false,信息不足;先准备真实合法 fixture,若仍无细节,需要后端补充错误码/错误信息。</td><td>用真实资源重跑;若仍 success=false,把 operation+trace_id 给后端。</td><td><code>tools/call</code></td><td><code>0b5deb3217840999068826691e087a</code></td><td class="evidence">[UNCLASSIFIED] business error: success=false (operation: attendance-wukong/update_group_setting) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">53</td><td><code>attendance +update-group-members</code></td><td><span class="risk">write</span></td><td><span class="cat">输入/业务校验</span></td><td><span class="owner">后端业务/测试 fixture</span></td><td>后端只返回 success=false,信息不足;先准备真实合法 fixture,若仍无细节,需要后端补充错误码/错误信息。</td><td>用真实资源重跑;若仍 success=false,把 operation+trace_id 给后端。</td><td><code>tools/call</code></td><td><code>2127d89817840999081094644e07dd</code></td><td class="evidence">[UNCLASSIFIED] business error: success=false (operation: attendance-wukong/update_group_member) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">54</td><td><code>attendance +update-leave-type</code></td><td><span class="risk">write</span></td><td><span class="cat">鉴权/权限</span></td><td><span class="owner">权限/应用配置</span></td><td>给当前登录账号、DWS 应用或对应资源补齐权限/scope;本仓库 shortcut 不应绕过权限。拿 trace_id 给服务端/开放平台排查具体 scope。</td><td>补权限后重跑 `attendance +update-leave-type`;若仍是 permission,再看 trace_id。</td><td><code>tools/call</code></td><td><code>0bb7c36217840999093924897e07fe</code></td><td class="evidence">[RESOURCE_NOT_FOUND] Requested resource not found (operation: attendance-wukong/save_leave_type) hint: Check if the resource exists or if your account has permission</td></tr>
<tr><td class="num">55</td><td><code>calendar +respond-event</code></td><td><span class="risk">write</span></td><td><span class="cat">缺真实资源</span></td><td><span class="owner">测试数据</span></td><td>把 runner 里的安全负向 ID 换成真实资源 ID;当前错误说明调用已进后端,但资源不存在。</td><td>准备 fixture 后重跑 `calendar +respond-event`。</td><td><code>tools/call</code></td><td><code>0bb7c36217840999105062121e07db</code></td><td class="evidence">[UNCLASSIFIED] code: 300000, developerMessage: Event does not exist. (operation: calendar/respond) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">56</td><td><code>chat +category-add-conversation</code></td><td><span class="risk">write</span></td><td><span class="cat">鉴权/权限</span></td><td><span class="owner">权限/应用配置</span></td><td>给当前登录账号、DWS 应用或对应资源补齐权限/scope;本仓库 shortcut 不应绕过权限。拿 trace_id 给服务端/开放平台排查具体 scope。</td><td>补权限后重跑 `chat +category-add-conversation`;若仍是 permission,再看 trace_id。</td><td><code>tools/call</code></td><td><code>0b5deb3217840999117776203e08f9</code></td><td class="evidence">[RESOURCE_NOT_FOUND] Requested resource not found (operation: im/add_conv_to_categories) hint: Check if the resource exists or if your account has permission</td></tr>
<tr><td class="num">57</td><td><code>chat +category-remove-conversation</code></td><td><span class="risk">write</span></td><td><span class="cat">鉴权/权限</span></td><td><span class="owner">权限/应用配置</span></td><td>给当前登录账号、DWS 应用或对应资源补齐权限/scope;本仓库 shortcut 不应绕过权限。拿 trace_id 给服务端/开放平台排查具体 scope。</td><td>补权限后重跑 `chat +category-remove-conversation`;若仍是 permission,再看 trace_id。</td><td><code>tools/call</code></td><td><code>2104a64c17840999130466520e085e</code></td><td class="evidence">[RESOURCE_NOT_FOUND] Requested resource not found (operation: im/remove_conv_from_categories) hint: Check if the resource exists or if your account has permission</td></tr>
<tr><td class="num">58</td><td><code>chat +chat-add-bot</code></td><td><span class="risk">write</span></td><td><span class="cat">鉴权/权限</span></td><td><span class="owner">权限/应用配置</span></td><td>给当前登录账号、DWS 应用或对应资源补齐权限/scope;本仓库 shortcut 不应绕过权限。拿 trace_id 给服务端/开放平台排查具体 scope。</td><td>补权限后重跑 `chat +chat-add-bot`;若仍是 permission,再看 trace_id。</td><td><code>tools/call</code></td><td><code>2104a64c17840999144193460e08c7</code></td><td class="evidence">[RESOURCE_NOT_FOUND] Requested resource not found (operation: bot/add_robot_to_group) hint: Check if the resource exists or if your account has permission</td></tr>
<tr><td class="num">59</td><td><code>chat +chat-audit-join</code></td><td><span class="risk">write</span></td><td><span class="cat">后端/MCP</span></td><td><span class="owner">后端/MCP schema</span></td><td>CLI fake MCP 已证明字段已装配;需要修 MCP tool schema 或网关字段映射,确认 openConversationId/openCid/cid、receiverUid、applicantUid 等字段没有在 schema 校验/转发时被丢弃。</td><td>修 MCP 后不改 shortcut,直接重跑真实命令;预期错误从 required 变为资源无效或成功。</td><td><code>tools/call</code></td><td><code>0bb7c36217840999154832733e0758</code></td><td class="evidence">[UNCLASSIFIED] applicantUid is required (operation: im/audit_join_group) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">60</td><td><code>chat +chat-mute-member</code></td><td><span class="risk">write</span></td><td><span class="cat">后端/MCP</span></td><td><span class="owner">后端/MCP schema</span></td><td>CLI fake MCP 已证明字段已装配;需要修 MCP tool schema 或网关字段映射,确认 openConversationId/openCid/cid、receiverUid、applicantUid 等字段没有在 schema 校验/转发时被丢弃。</td><td>修 MCP 后不改 shortcut,直接重跑真实命令;预期错误从 required 变为资源无效或成功。</td><td><code>tools/call</code></td><td><code>2104a64c17840999166036583e08a3</code></td><td class="evidence">[UNCLASSIFIED] openConversationId or cid is required (operation: im/set_group_member_mute_list) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">61</td><td><code>chat +chat-quit</code></td><td><span class="risk">write</span></td><td><span class="cat">输入/业务校验</span></td><td><span class="owner">测试输入/业务校验</span></td><td>当前命令已进入后端业务校验;先把测试输入换成真实合法 fixture,再判断是否需要改 shortcut。</td><td>重跑 `chat +chat-quit` 并比较 stdout/stderr。</td><td><code>tools/call</code></td><td><code>0bb7c36217840999176728426e0779</code></td><td class="evidence">[UNCLASSIFIED] listBaseConversationByIds error (operation: im/quit_group) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">62</td><td><code>chat +chat-remove-bot</code></td><td><span class="risk">high-risk-write</span></td><td><span class="cat">缺真实资源</span></td><td><span class="owner">测试数据</span></td><td>把 runner 里的安全负向 ID 换成真实资源 ID;当前错误说明调用已进后端,但资源不存在。</td><td>准备 fixture 后重跑 `chat +chat-remove-bot`。</td><td><code>tools/call</code></td><td><code>0b5deb3217840999189888305e08b5</code></td><td class="evidence">[UNCLASSIFIED] 无效的会话 (operation: bot/remove_robot_in_group) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">63</td><td><code>chat +chat-role-remove</code></td><td><span class="risk">high-risk-write</span></td><td><span class="cat">输入/业务校验</span></td><td><span class="owner">测试输入/业务校验</span></td><td>当前命令已进入后端业务校验;先把测试输入换成真实合法 fixture,再判断是否需要改 shortcut。</td><td>重跑 `chat +chat-role-remove` 并比较 stdout/stderr。</td><td><code>tools/call</code></td><td><code>2127d89817840999200483204e079c</code></td><td class="evidence">[UNCLASSIFIED] listBaseConversationByIds error (operation: im/remove_custom_group_role) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">64</td><td><code>chat +chat-role-remove-user</code></td><td><span class="risk">write</span></td><td><span class="cat">输入/业务校验</span></td><td><span class="owner">测试输入/业务校验</span></td><td>当前命令已进入后端业务校验;先把测试输入换成真实合法 fixture,再判断是否需要改 shortcut。</td><td>重跑 `chat +chat-role-remove-user` 并比较 stdout/stderr。</td><td><code>tools/call</code></td><td><code>2127d89817840999211317952e0757</code></td><td class="evidence">[UNCLASSIFIED] listBaseConversationByIds error (operation: im/remove_custom_user_roles) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">65</td><td><code>chat +chat-transfer-owner</code></td><td><span class="risk">write</span></td><td><span class="cat">后端/MCP</span></td><td><span class="owner">后端/MCP schema</span></td><td>CLI fake MCP 已证明字段已装配;需要修 MCP tool schema 或网关字段映射,确认 openConversationId/openCid/cid、receiverUid、applicantUid 等字段没有在 schema 校验/转发时被丢弃。</td><td>修 MCP 后不改 shortcut,直接重跑真实命令;预期错误从 required 变为资源无效或成功。</td><td><code>tools/call</code></td><td><code>0b5deb3217840999222318863e087a</code></td><td class="evidence">[UNCLASSIFIED] openConversationId is required (operation: im/transfer_group_owner) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">66</td><td><code>chat +chat-update-icon</code></td><td><span class="risk">write</span></td><td><span class="cat">输入/业务校验</span></td><td><span class="owner">测试输入/业务校验</span></td><td>当前命令已进入后端业务校验;先把测试输入换成真实合法 fixture,再判断是否需要改 shortcut。</td><td>重跑 `chat +chat-update-icon` 并比较 stdout/stderr。</td><td><code>tools/call</code></td><td><code>2104a64c17840999232478654e0819</code></td><td class="evidence">[UNCLASSIFIED] listBaseConversationByIds error (operation: im/update_group_icon) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">67</td><td><code>chat +chat-update-settings</code></td><td><span class="risk">write</span></td><td><span class="cat">输入/业务校验</span></td><td><span class="owner">测试输入/shortcut 枚举</span></td><td>把测试输入的 setting-key 从 x 改为后端支持的 key;同时可在 shortcut flag 上补 enum,避免用户传非法 key。</td><td>改 runner 后重跑;如果补 enum,跑 shortcut 单测确认校验文案。</td><td><code>tools/call</code></td><td><code>2127d89817840999244326971e07dd</code></td><td class="evidence">[UNCLASSIFIED] unsupported setting key: x (operation: im/update_group_settings) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">68</td><td><code>chat +conversation-clear-messages</code></td><td><span class="risk">high-risk-write</span></td><td><span class="cat">后端/MCP</span></td><td><span class="owner">后端/MCP schema</span></td><td>CLI fake MCP 已证明字段已装配;需要修 MCP tool schema 或网关字段映射,确认 openConversationId/openCid/cid、receiverUid、applicantUid 等字段没有在 schema 校验/转发时被丢弃。</td><td>修 MCP 后不改 shortcut,直接重跑真实命令;预期错误从 required 变为资源无效或成功。</td><td><code>tools/call</code></td><td><code>2127d89817840999254816721e079b</code></td><td class="evidence">[UNCLASSIFIED] openConversationId or cid is required (operation: im/clear_conversation_messages) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">69</td><td><code>chat +conversation-clear-red-point</code></td><td><span class="risk">write</span></td><td><span class="cat">后端/MCP</span></td><td><span class="owner">后端/MCP schema</span></td><td>CLI fake MCP 已证明字段已装配;需要修 MCP tool schema 或网关字段映射,确认 openConversationId/openCid/cid、receiverUid、applicantUid 等字段没有在 schema 校验/转发时被丢弃。</td><td>修 MCP 后不改 shortcut,直接重跑真实命令;预期错误从 required 变为资源无效或成功。</td><td><code>tools/call</code></td><td><code>2104a64c17840999265511295e085f</code></td><td class="evidence">[UNCLASSIFIED] openConversationId or cid is required (operation: im/clear_conversation_red_point) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">70</td><td><code>chat +conversation-hide</code></td><td><span class="risk">write</span></td><td><span class="cat">后端/MCP</span></td><td><span class="owner">后端/MCP schema</span></td><td>CLI fake MCP 已证明字段已装配;需要修 MCP tool schema 或网关字段映射,确认 openConversationId/openCid/cid、receiverUid、applicantUid 等字段没有在 schema 校验/转发时被丢弃。</td><td>修 MCP 后不改 shortcut,直接重跑真实命令;预期错误从 required 变为资源无效或成功。</td><td><code>tools/call</code></td><td><code>2127d89817840999276117140e079b</code></td><td class="evidence">[UNCLASSIFIED] openConversationId or cid is required (operation: im/hide_conversation) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">71</td><td><code>chat +conversation-mark-read</code></td><td><span class="risk">write</span></td><td><span class="cat">缺真实资源</span></td><td><span class="owner">测试数据</span></td><td>替换为当前账号真实可访问的群/会话 openConversationId;如果用真实群仍报“无效”,再查 IM 后端解析。</td><td>先用 `chat +my-groups` 或群搜索拿真实会话 ID,再重跑。</td><td><code>tools/call</code></td><td><code>2127d89817840999287654280e07bd</code></td><td class="evidence">[UNCLASSIFIED] openConversationId无效,无法解析为cid (operation: im/mark_message_read) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">72</td><td><code>chat +conversation-mark-unread</code></td><td><span class="risk">write</span></td><td><span class="cat">后端/MCP</span></td><td><span class="owner">后端/MCP schema</span></td><td>CLI fake MCP 已证明字段已装配;需要修 MCP tool schema 或网关字段映射,确认 openConversationId/openCid/cid、receiverUid、applicantUid 等字段没有在 schema 校验/转发时被丢弃。</td><td>修 MCP 后不改 shortcut,直接重跑真实命令;预期错误从 required 变为资源无效或成功。</td><td><code>tools/call</code></td><td><code>0bb7c36217840999298744910e0758</code></td><td class="evidence">[UNCLASSIFIED] openConversationId or cid is required (operation: im/mark_conversation_unread) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">73</td><td><code>chat +conversation-mute</code></td><td><span class="risk">write</span></td><td><span class="cat">后端/MCP</span></td><td><span class="owner">后端/MCP schema</span></td><td>CLI fake MCP 已证明字段已装配;需要修 MCP tool schema 或网关字段映射,确认 openConversationId/openCid/cid、receiverUid、applicantUid 等字段没有在 schema 校验/转发时被丢弃。</td><td>修 MCP 后不改 shortcut,直接重跑真实命令;预期错误从 required 变为资源无效或成功。</td><td><code>tools/call</code></td><td><code>2104a64c17840999309241865e085f</code></td><td class="evidence">[UNCLASSIFIED] openConversationId is required (operation: im/update_notification_off) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">74</td><td><code>chat +conversation-mute-at-all</code></td><td><span class="risk">write</span></td><td><span class="cat">后端/MCP</span></td><td><span class="owner">后端/MCP schema</span></td><td>CLI fake MCP 已证明字段已装配;需要修 MCP tool schema 或网关字段映射,确认 openConversationId/openCid/cid、receiverUid、applicantUid 等字段没有在 schema 校验/转发时被丢弃。</td><td>修 MCP 后不改 shortcut,直接重跑真实命令;预期错误从 required 变为资源无效或成功。</td><td><code>tools/call</code></td><td><code>2127d89817840999320028068e07dd</code></td><td class="evidence">[UNCLASSIFIED] openConversationId is required (operation: im/update_at_all_notification_off) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">75</td><td><code>chat +conversation-mute-red-envelope</code></td><td><span class="risk">write</span></td><td><span class="cat">后端/MCP</span></td><td><span class="owner">后端/MCP schema</span></td><td>CLI fake MCP 已证明字段已装配;需要修 MCP tool schema 或网关字段映射,确认 openConversationId/openCid/cid、receiverUid、applicantUid 等字段没有在 schema 校验/转发时被丢弃。</td><td>修 MCP 后不改 shortcut,直接重跑真实命令;预期错误从 required 变为资源无效或成功。</td><td><code>tools/call</code></td><td><code>0bb7c36217840999330228283e07fe</code></td><td class="evidence">[UNCLASSIFIED] openConversationId is required (operation: im/update_red_env_notification_off) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">76</td><td><code>chat +conversation-set-top</code></td><td><span class="risk">write</span></td><td><span class="cat">后端/MCP</span></td><td><span class="owner">后端/MCP schema</span></td><td>CLI fake MCP 已证明字段已装配;需要修 MCP tool schema 或网关字段映射,确认 openConversationId/openCid/cid、receiverUid、applicantUid 等字段没有在 schema 校验/转发时被丢弃。</td><td>修 MCP 后不改 shortcut,直接重跑真实命令;预期错误从 required 变为资源无效或成功。</td><td><code>tools/call</code></td><td><code>2127d89817840999341302961e07fe</code></td><td class="evidence">[UNCLASSIFIED] openConversationId or cid is required (operation: im/set_top_conversation) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">77</td><td><code>chat +messages-add-emoji</code></td><td><span class="risk">write</span></td><td><span class="cat">缺真实资源</span></td><td><span class="owner">测试数据</span></td><td>替换为真实 openMessageId,并保证消息属于当前账号可访问会话;当前 no-such ID 只能验证负向路径。</td><td>先用消息列表拿 messageId,再重跑消息详情/状态/撤回类命令。</td><td><code>tools/call</code></td><td><code>2127d89817840999352941910e0756</code></td><td class="evidence">[UNCLASSIFIED] invalid openMsgId: DWSREALTESTNOSUCHID0000000000000 (operation: im/add_emoji_reaction) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">78</td><td><code>chat +messages-add-text-emotion</code></td><td><span class="risk">write</span></td><td><span class="cat">缺真实资源</span></td><td><span class="owner">测试数据</span></td><td>替换为真实 openMessageId,并保证消息属于当前账号可访问会话;当前 no-such ID 只能验证负向路径。</td><td>先用消息列表拿 messageId,再重跑消息详情/状态/撤回类命令。</td><td><code>tools/call</code></td><td><code>0b5deb3217840999362862711e08b5</code></td><td class="evidence">[UNCLASSIFIED] invalid openMsgId: DWSREALTESTNOSUCHID0000000000000 (operation: im/add_text_emotion) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">79</td><td><code>chat +messages-batch-recall-by-bot</code></td><td><span class="risk">write</span></td><td><span class="cat">缺真实资源</span></td><td><span class="owner">测试数据</span></td><td>把 runner 里的安全负向 ID 换成真实资源 ID;当前错误说明调用已进后端,但资源不存在。</td><td>准备 fixture 后重跑 `chat +messages-batch-recall-by-bot`。</td><td><code>tools/call</code></td><td><code>2104a64c17840999373456305e0817</code></td><td class="evidence">[UNCLASSIFIED] business error: success=false (operation: bot/batch_recall_robot_users_msg) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">80</td><td><code>chat +messages-batch-send-by-bot</code></td><td><span class="risk">write</span></td><td><span class="cat">缺真实资源</span></td><td><span class="owner">测试数据</span></td><td>把 runner 里的安全负向 ID 换成真实资源 ID;当前错误说明调用已进后端,但资源不存在。</td><td>准备 fixture 后重跑 `chat +messages-batch-send-by-bot`。</td><td><code>tools/call</code></td><td><code>0bb7c36217840999385748776e0757</code></td><td class="evidence">[UNCLASSIFIED] business error: success=false (operation: bot/batch_send_robot_msg_to_users) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">81</td><td><code>chat +messages-combine-forward</code></td><td><span class="risk">write</span></td><td><span class="cat">缺真实资源</span></td><td><span class="owner">测试数据</span></td><td>替换为当前账号真实可访问的群/会话 openConversationId;如果用真实群仍报“无效”,再查 IM 后端解析。</td><td>先用 `chat +my-groups` 或群搜索拿真实会话 ID,再重跑。</td><td><code>tools/call</code></td><td><code>2104a64c17840999396596163e08ee</code></td><td class="evidence">[UNCLASSIFIED] srcOpenCid无效,无法解析为cid (operation: im/combine_forward_messages) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">82</td><td><code>chat +messages-create-text-emotion</code></td><td><span class="risk">write</span></td><td><span class="cat">输入/业务校验</span></td><td><span class="owner">测试输入/业务规则</span></td><td>换成后端支持的文字表情组合,或把该命令保留为业务负向;CLI 不应绕过后端限制。</td><td>用一个真实可保存的表情模板重跑。</td><td><code>tools/call</code></td><td><code>0b5deb3217840999407422891e08cd</code></td><td class="evidence">[UNCLASSIFIED] 暂不支持保存该文字表情 (operation: im/create_text_emotion) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">83</td><td><code>chat +messages-forward</code></td><td><span class="risk">write</span></td><td><span class="cat">缺真实资源</span></td><td><span class="owner">测试数据</span></td><td>替换为真实 openMessageId,并保证消息属于当前账号可访问会话;当前 no-such ID 只能验证负向路径。</td><td>先用消息列表拿 messageId,再重跑消息详情/状态/撤回类命令。</td><td><code>tools/call</code></td><td><code>2104a64c17840999417966407e08ee</code></td><td class="evidence">[UNCLASSIFIED] openMessageId解密失败 (operation: im/forward_message) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">84</td><td><code>chat +messages-forward-topic</code></td><td><span class="risk">write</span></td><td><span class="cat">缺真实资源</span></td><td><span class="owner">测试数据</span></td><td>替换为真实 openMessageId,并保证消息属于当前账号可访问会话;当前 no-such ID 只能验证负向路径。</td><td>先用消息列表拿 messageId,再重跑消息详情/状态/撤回类命令。</td><td><code>tools/call</code></td><td><code>2127d89817840999428541474e07dd</code></td><td class="evidence">[UNCLASSIFIED] openMessageId解密失败 (operation: im/forward_topic) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">85</td><td><code>chat +messages-recall</code></td><td><span class="risk">write</span></td><td><span class="cat">缺真实资源</span></td><td><span class="owner">测试数据</span></td><td>替换为当前账号真实可访问的群/会话 openConversationId;如果用真实群仍报“无效”,再查 IM 后端解析。</td><td>先用 `chat +my-groups` 或群搜索拿真实会话 ID,再重跑。</td><td><code>tools/call</code></td><td><code>2104a64c17840999441138103e08c7</code></td><td class="evidence">[UNCLASSIFIED] openConversationId无效,无法解析为cid (operation: im/recall_message) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">86</td><td><code>chat +messages-recall-by-bot</code></td><td><span class="risk">write</span></td><td><span class="cat">输入/业务校验</span></td><td><span class="owner">后端业务/测试 fixture</span></td><td>后端只返回 success=false,信息不足;先准备真实合法 fixture,若仍无细节,需要后端补充错误码/错误信息。</td><td>用真实资源重跑;若仍 success=false,把 operation+trace_id 给后端。</td><td><code>tools/call</code></td><td><code>2104a64c17840999454382709e08a3</code></td><td class="evidence">[UNCLASSIFIED] business error: success=false (operation: bot/recall_robot_group_message) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">87</td><td><code>chat +messages-remove-emoji</code></td><td><span class="risk">write</span></td><td><span class="cat">缺真实资源</span></td><td><span class="owner">测试数据</span></td><td>替换为真实 openMessageId,并保证消息属于当前账号可访问会话;当前 no-such ID 只能验证负向路径。</td><td>先用消息列表拿 messageId,再重跑消息详情/状态/撤回类命令。</td><td><code>tools/call</code></td><td><code>0b5deb3217840999467733059e08f9</code></td><td class="evidence">[UNCLASSIFIED] invalid openMsgId: DWSREALTESTNOSUCHID0000000000000 (operation: im/remove_emoji_reaction) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">88</td><td><code>chat +messages-remove-text-emotion</code></td><td><span class="risk">write</span></td><td><span class="cat">缺真实资源</span></td><td><span class="owner">测试数据</span></td><td>替换为真实 openMessageId,并保证消息属于当前账号可访问会话;当前 no-such ID 只能验证负向路径。</td><td>先用消息列表拿 messageId,再重跑消息详情/状态/撤回类命令。</td><td><code>tools/call</code></td><td><code>0b5deb3217840999478923406e087f</code></td><td class="evidence">[UNCLASSIFIED] invalid openMsgId: DWSREALTESTNOSUCHID0000000000000 (operation: im/remove_text_emotion) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">89</td><td><code>chat +messages-send-by-bot</code></td><td><span class="risk">write</span></td><td><span class="cat">缺真实资源</span></td><td><span class="owner">测试数据</span></td><td>把 runner 里的安全负向 ID 换成真实资源 ID;当前错误说明调用已进后端,但资源不存在。</td><td>准备 fixture 后重跑 `chat +messages-send-by-bot`。</td><td><code>tools/call</code></td><td><code>0b5deb3217840999494005921e08ef</code></td><td class="evidence">[UNCLASSIFIED] business error: success=false (operation: bot/send_robot_group_message) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">90</td><td><code>chat +messages-send-card</code></td><td><span class="risk">write</span></td><td><span class="cat">后端/MCP</span></td><td><span class="owner">后端/MCP schema</span></td><td>CLI fake MCP 已证明字段已装配;需要修 MCP tool schema 或网关字段映射,确认 openConversationId/openCid/cid、receiverUid、applicantUid 等字段没有在 schema 校验/转发时被丢弃。</td><td>修 MCP 后不改 shortcut,直接重跑真实命令;预期错误从 required 变为资源无效或成功。</td><td><code>tools/call</code></td><td><code>2104a64c17840999509255753e081a</code></td><td class="evidence">[UNCLASSIFIED] receiverUid和openConversationId不能同时为空 (operation: im/create_and_send_card) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">91</td><td><code>chat +messages-set-pin</code></td><td><span class="risk">write</span></td><td><span class="cat">后端/MCP</span></td><td><span class="owner">后端/MCP schema</span></td><td>CLI fake MCP 已证明字段已装配;需要修 MCP tool schema 或网关字段映射,确认 openConversationId/openCid/cid、receiverUid、applicantUid 等字段没有在 schema 校验/转发时被丢弃。</td><td>修 MCP 后不改 shortcut,直接重跑真实命令;预期错误从 required 变为资源无效或成功。</td><td><code>tools/call</code></td><td><code>0bb7c36217840999521117991e0758</code></td><td class="evidence">[UNCLASSIFIED] openConversationId or cid is required (operation: im/set_pin_message) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">92</td><td><code>chat +messages-set-top</code></td><td><span class="risk">write</span></td><td><span class="cat">缺真实资源</span></td><td><span class="owner">测试数据</span></td><td>替换为真实 openMessageId,并保证消息属于当前账号可访问会话;当前 no-such ID 只能验证负向路径。</td><td>先用消息列表拿 messageId,再重跑消息详情/状态/撤回类命令。</td><td><code>tools/call</code></td><td><code>2104a64c17840999532438476e0817</code></td><td class="evidence">[UNCLASSIFIED] openMessageId解密失败 (operation: im/set_top_message) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">93</td><td><code>chat +messages-unset-pin</code></td><td><span class="risk">write</span></td><td><span class="cat">后端/MCP</span></td><td><span class="owner">后端/MCP schema</span></td><td>CLI fake MCP 已证明字段已装配;需要修 MCP tool schema 或网关字段映射,确认 openConversationId/openCid/cid、receiverUid、applicantUid 等字段没有在 schema 校验/转发时被丢弃。</td><td>修 MCP 后不改 shortcut,直接重跑真实命令;预期错误从 required 变为资源无效或成功。</td><td><code>tools/call</code></td><td><code>2104a64c17840999544428103e08ee</code></td><td class="evidence">[UNCLASSIFIED] openConversationId or cid is required (operation: im/unset_pin_message) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">94</td><td><code>chat +messages-unset-top</code></td><td><span class="risk">write</span></td><td><span class="cat">缺真实资源</span></td><td><span class="owner">测试数据</span></td><td>替换为真实 openMessageId,并保证消息属于当前账号可访问会话;当前 no-such ID 只能验证负向路径。</td><td>先用消息列表拿 messageId,再重跑消息详情/状态/撤回类命令。</td><td><code>tools/call</code></td><td><code>0b5deb3217840999554154221e08f9</code></td><td class="evidence">[UNCLASSIFIED] openMessageId解密失败 (operation: im/unset_top_message) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">95</td><td><code>devapp +event-subscribe</code></td><td><span class="risk">write</span></td><td><span class="cat">鉴权/权限</span></td><td><span class="owner">权限/应用配置</span></td><td>给当前登录账号、DWS 应用或对应资源补齐权限/scope;本仓库 shortcut 不应绕过权限。拿 trace_id 给服务端/开放平台排查具体 scope。</td><td>补权限后重跑 `devapp +event-subscribe`;若仍是 permission,再看 trace_id。</td><td><code>tools/call</code></td><td><code>2104a64c17840999564022020e08c7</code></td><td class="evidence">[UNCLASSIFIED] 当前用户没有应用事件订阅权限 (operation: devapp/subscribe_dev_app_events) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">96</td><td><code>devapp +event-unsubscribe</code></td><td><span class="risk">high-risk-write</span></td><td><span class="cat">鉴权/权限</span></td><td><span class="owner">权限/应用配置</span></td><td>给当前登录账号、DWS 应用或对应资源补齐权限/scope;本仓库 shortcut 不应绕过权限。拿 trace_id 给服务端/开放平台排查具体 scope。</td><td>补权限后重跑 `devapp +event-unsubscribe`;若仍是 permission,再看 trace_id。</td><td><code>tools/call</code></td><td><code>0bb7c36217840999575698500e07db</code></td><td class="evidence">[UNCLASSIFIED] business error: success=false (operation: devapp/unsubscribe_dev_app_events) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">97</td><td><code>devapp +permission-add</code></td><td><span class="risk">write</span></td><td><span class="cat">鉴权/权限</span></td><td><span class="owner">权限/应用配置</span></td><td>给当前登录账号、DWS 应用或对应资源补齐权限/scope;本仓库 shortcut 不应绕过权限。拿 trace_id 给服务端/开放平台排查具体 scope。</td><td>补权限后重跑 `devapp +permission-add`;若仍是 permission,再看 trace_id。</td><td><code>tools/call</code></td><td><code>2127d89817840999587758457e079c</code></td><td class="evidence">[UNCLASSIFIED] 当前用户没有开发者身份 (operation: devapp/apply_dev_app_permissions) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">98</td><td><code>devapp +permission-remove</code></td><td><span class="risk">high-risk-write</span></td><td><span class="cat">鉴权/权限</span></td><td><span class="owner">权限/应用配置</span></td><td>给当前登录账号、DWS 应用或对应资源补齐权限/scope;本仓库 shortcut 不应绕过权限。拿 trace_id 给服务端/开放平台排查具体 scope。</td><td>补权限后重跑 `devapp +permission-remove`;若仍是 permission,再看 trace_id。</td><td><code>tools/call</code></td><td><code>2104a64c17840999598445247e085e</code></td><td class="evidence">[UNCLASSIFIED] 当前用户没有开发者身份 (operation: devapp/remove_dev_app_permissions) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">99</td><td><code>devapp +robot-config</code></td><td><span class="risk">write</span></td><td><span class="cat">鉴权/权限</span></td><td><span class="owner">权限/应用配置</span></td><td>给当前登录账号、DWS 应用或对应资源补齐权限/scope;本仓库 shortcut 不应绕过权限。拿 trace_id 给服务端/开放平台排查具体 scope。</td><td>补权限后重跑 `devapp +robot-config`;若仍是 permission,再看 trace_id。</td><td><code>tools/call</code></td><td><code>0bb7c36217840999609828961e07db</code></td><td class="evidence">[UNCLASSIFIED] business error: success=false (operation: devapp/set_extension_robot_config) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">100</td><td><code>devapp +robot-disable</code></td><td><span class="risk">high-risk-write</span></td><td><span class="cat">鉴权/权限</span></td><td><span class="owner">权限/应用配置</span></td><td>给当前登录账号、DWS 应用或对应资源补齐权限/scope;本仓库 shortcut 不应绕过权限。拿 trace_id 给服务端/开放平台排查具体 scope。</td><td>补权限后重跑 `devapp +robot-disable`;若仍是 permission,再看 trace_id。</td><td><code>tools/call</code></td><td><code>2104a64c17840999620461113e08ee</code></td><td class="evidence">[UNCLASSIFIED] business error: success=false (operation: devapp/disable_dev_app_robot) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">101</td><td><code>devapp +robot-enable</code></td><td><span class="risk">write</span></td><td><span class="cat">鉴权/权限</span></td><td><span class="owner">权限/应用配置</span></td><td>给当前登录账号、DWS 应用或对应资源补齐权限/scope;本仓库 shortcut 不应绕过权限。拿 trace_id 给服务端/开放平台排查具体 scope。</td><td>补权限后重跑 `devapp +robot-enable`;若仍是 permission,再看 trace_id。</td><td><code>tools/call</code></td><td><code>0bb7c36217840999632641525e0758</code></td><td class="evidence">[UNCLASSIFIED] business error: success=false (operation: devapp/enable_dev_app_robot) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">102</td><td><code>devapp +security-config</code></td><td><span class="risk">write</span></td><td><span class="cat">鉴权/权限</span></td><td><span class="owner">权限/应用配置</span></td><td>给当前登录账号、DWS 应用或对应资源补齐权限/scope;本仓库 shortcut 不应绕过权限。拿 trace_id 给服务端/开放平台排查具体 scope。</td><td>补权限后重跑 `devapp +security-config`;若仍是 permission,再看 trace_id。</td><td><code>tools/call</code></td><td><code>2104a64c17840999645522046e0817</code></td><td class="evidence">[UNCLASSIFIED] 当前用户没有开发者身份 (operation: devapp/update_dev_app_security_config) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">103</td><td><code>devapp +version-create</code></td><td><span class="risk">write</span></td><td><span class="cat">输入/业务校验</span></td><td><span class="owner">后端业务/测试 fixture</span></td><td>后端只返回 success=false,信息不足;先准备真实合法 fixture,若仍无细节,需要后端补充错误码/错误信息。</td><td>用真实资源重跑;若仍 success=false,把 operation+trace_id 给后端。</td><td><code>tools/call</code></td><td><code>0b5deb3217840999656705920e0853</code></td><td class="evidence">[UNCLASSIFIED] business error: success=false (operation: devapp/create_dev_app_version) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">104</td><td><code>devapp +version-publish</code></td><td><span class="risk">write</span></td><td><span class="cat">输入/业务校验</span></td><td><span class="owner">后端业务/测试 fixture</span></td><td>后端只返回 success=false,信息不足;先准备真实合法 fixture,若仍无细节,需要后端补充错误码/错误信息。</td><td>用真实资源重跑;若仍 success=false,把 operation+trace_id 给后端。</td><td><code>tools/call</code></td><td><code>2127d89817840999667906017e075d</code></td><td class="evidence">[UNCLASSIFIED] business error: success=false (operation: devapp/publish_dev_app_version) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">105</td><td><code>ding +send-by-message</code></td><td><span class="risk">write</span></td><td><span class="cat">输入/业务校验</span></td><td><span class="owner">测试输入/业务校验</span></td><td>当前命令已进入后端业务校验;先把测试输入换成真实合法 fixture,再判断是否需要改 shortcut。</td><td>重跑 `ding +send-by-message` 并比较 stdout/stderr。</td><td><code>tools/call</code></td><td><code>0b5deb3217840999678466213e0853</code></td><td class="evidence">[UNCLASSIFIED] remindType非法,合法值:APP/SMS/PHONE (operation: im/send_ding_by_message) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">106</td><td><code>doc +comment-create-inline</code></td><td><span class="risk">write</span></td><td><span class="cat">缺真实资源</span></td><td><span class="owner">测试数据</span></td><td>替换为真实文档/节点 ID;文档评论、分享、版本等命令需要资源存在且账号可访问。</td><td>用真实 doc/node 重跑;若仍失败再看 doc/doc-comment 工具字段。</td><td><code>tools/call</code></td><td><code>2127d89817840999689931893e079c</code></td><td class="evidence">[TABLE_NOT_FOUND] Requested resource not found (operation: doc-comment/create_inline_comment) hint: Document may have been deleted or moved</td></tr>
<tr><td class="num">107</td><td><code>doc +template-apply</code></td><td><span class="risk">write</span></td><td><span class="cat">鉴权/权限</span></td><td><span class="owner">权限/应用配置</span></td><td>给当前登录账号、DWS 应用或对应资源补齐权限/scope;本仓库 shortcut 不应绕过权限。拿 trace_id 给服务端/开放平台排查具体 scope。</td><td>补权限后重跑 `doc +template-apply`;若仍是 permission,再看 trace_id。</td><td><code>tools/call</code></td><td><code>2127d89817840999701186954e0757</code></td><td class="evidence">[RESOURCE_NOT_FOUND] Requested resource not found (operation: doc/apply_doc_template) hint: Check if the resource exists or if your account has permission</td></tr>
<tr><td class="num">108</td><td><code>minutes +record-pause</code></td><td><span class="risk">write</span></td><td><span class="cat">输入/业务校验</span></td><td><span class="owner">测试输入/业务校验</span></td><td>当前命令已进入后端业务校验;先把测试输入换成真实合法 fixture,再判断是否需要改 shortcut。</td><td>重跑 `minutes +record-pause` 并比较 stdout/stderr。</td><td><code>tools/call</code></td><td><code>0bb7c36217840999713124987e0757</code></td><td class="evidence">[UNCLASSIFIED] aiAgentTestRunCmdUnknownError (operation: minutes/执行听记指令-发起AI听记录音) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">109</td><td><code>minutes +record-resume</code></td><td><span class="risk">write</span></td><td><span class="cat">输入/业务校验</span></td><td><span class="owner">测试输入/业务校验</span></td><td>当前命令已进入后端业务校验;先把测试输入换成真实合法 fixture,再判断是否需要改 shortcut。</td><td>重跑 `minutes +record-resume` 并比较 stdout/stderr。</td><td><code>tools/call</code></td><td><code>0bb7c36217840999724452848e0758</code></td><td class="evidence">[UNCLASSIFIED] aiAgentTestRunCmdUnknownError (operation: minutes/执行听记指令-发起AI听记录音) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">110</td><td><code>minutes +record-stop</code></td><td><span class="risk">write</span></td><td><span class="cat">输入/业务校验</span></td><td><span class="owner">测试输入/业务校验</span></td><td>当前命令已进入后端业务校验;先把测试输入换成真实合法 fixture,再判断是否需要改 shortcut。</td><td>重跑 `minutes +record-stop` 并比较 stdout/stderr。</td><td><code>tools/call</code></td><td><code>2127d89817840999735397508e0757</code></td><td class="evidence">[UNCLASSIFIED] aiAgentTestRunCmdUnknownError (operation: minutes/执行听记指令-发起AI听记录音) hint: Use --verbose for detailed error logs</td></tr>
<tr><td class="num">111</td><td><code>oa +approve-by</code></td><td><span class="risk">write</span></td><td><span class="cat">缺真实资源</span></td><td><span class="owner">测试数据</span></td><td>把 runner 里的安全负向 ID 换成真实资源 ID;当前错误说明调用已进后端,但资源不存在。</td><td>准备 fixture 后重跑 `oa +approve-by`。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">没找到待审批单据:待我处理的审批里没有标题/单号包含 &quot;__DWS_SHORTCUT_REAL_TEST_NO_SUCH_APPROVAL_20260715-151724__&quot; 的单据。</td></tr>
<tr><td class="num">112</td><td><code>wiki +node-copy</code></td><td><span class="risk">write</span></td><td><span class="cat">缺真实资源</span></td><td><span class="owner">测试数据</span></td><td>替换为真实文档/节点 ID;文档评论、分享、版本等命令需要资源存在且账号可访问。</td><td>用真实 doc/node 重跑;若仍失败再看 doc/doc-comment 工具字段。</td><td><code>tools/call</code></td><td><code>0bb7c36217840999769818495e0779</code></td><td class="evidence">[TABLE_NOT_FOUND] Requested resource not found (operation: doc/copy_document) hint: Document may have been deleted or moved</td></tr>
<tr><td class="num">113</td><td><code>wiki +node-move</code></td><td><span class="risk">write</span></td><td><span class="cat">缺真实资源</span></td><td><span class="owner">测试数据</span></td><td>替换为真实文档/节点 ID;文档评论、分享、版本等命令需要资源存在且账号可访问。</td><td>用真实 doc/node 重跑;若仍失败再看 doc/doc-comment 工具字段。</td><td><code>tools/call</code></td><td><code>0bb7c36217840999780286509e07fe</code></td><td class="evidence">[TABLE_NOT_FOUND] Requested resource not found (operation: doc/move_document) hint: Document may have been deleted or moved</td></tr>
<tr><td class="num">114</td><td><code>wiki +wiki-new-doc</code></td><td><span class="risk">write</span></td><td><span class="cat">缺真实资源</span></td><td><span class="owner">测试数据</span></td><td>把 runner 里的安全负向 ID 换成真实资源 ID;当前错误说明调用已进后端,但资源不存在。</td><td>准备 fixture 后重跑 `wiki +wiki-new-doc`。</td><td><code>-</code></td><td><code>-</code></td><td class="evidence">没找到名为 &quot;__DWS_SHORTCUT_REAL_TEST_NO_SUCH_SPACE__&quot; 的知识库;换个更完整/精确的空间名再试。</td></tr>
</tbody>
</table>
</main>
</body>
</html>
File diff suppressed because it is too large Load Diff
+168
View File
@@ -0,0 +1,168 @@
# 本期隐藏、下期修复的 shortcut 清单
生成时间:`2026-07-15T16:44:43`
来源:`docs/shortcut-real-read-results.json` 与 `docs/shortcut-real-write-results.json`。
口径:最新真实后端测试结果不是 `real-ok` 的 shortcut,本期不对外 release;代码保留,并通过 `internal/shortcut/release_hidden_generated.go` 标记为 Hidden,后续补 fixture / 修后端 / 修 CLI 后再放开。
总计:156 条。
| # | suite | shortcut | risk | status | category | fixability | 下期处理依据 |
|---:|---|---|---|---|---|---|---|
| 1 | read | `aitable +base-get-primary-doc-id` | read | real-error | backend-or-mcp-error | not-cli-fixable-first | 后端/MCP 服务返回内部错误;CLI 无法直接修复,但报告保留 trace/stdout 供服务端排查。 |
| 2 | read | `aitable +chart-share-get` | read | real-error | auth-or-permission | not-cli-fixable | 真实账号、应用 scope 或资源权限不足;CLI 只能如实暴露,不能在本仓库内修复权限。 |
| 3 | read | `aitable +dashboard-share-get` | read | real-error | missing-real-resource | not-cli-fixable-without-fixture | 真实测试使用的资源/单据/消息/群/文档不存在;需要准备对应 fixture 后才能期望成功,不属于 shortcut 参数投影错误。 |
| 4 | read | `aitable +export-data` | read | real-error | backend-or-mcp-error | not-cli-fixable-first | 后端/MCP 服务返回内部错误;CLI 无法直接修复,但报告保留 trace/stdout 供服务端排查。 |
| 5 | read | `aitable +record-primary-doc-get` | read | real-error | backend-or-mcp-error | not-cli-fixable-first | 后端/MCP 服务返回内部错误;CLI 无法直接修复,但报告保留 trace/stdout 供服务端排查。 |
| 6 | read | `aitable +role-get` | read | real-error | backend-or-mcp-error | not-cli-fixable-first | 后端/MCP 服务返回内部错误;CLI 无法直接修复,但报告保留 trace/stdout 供服务端排查。 |
| 7 | read | `aitable +workflow-get` | read | real-error | backend-or-mcp-error | not-cli-fixable-first | 后端/MCP 服务返回内部错误;CLI 无法直接修复,但报告保留 trace/stdout 供服务端排查。 |
| 8 | read | `aitable +workflow-list` | read | real-error | backend-or-mcp-error | not-cli-fixable-first | 后端/MCP 服务返回内部错误;CLI 无法直接修复,但报告保留 trace/stdout 供服务端排查。 |
| 9 | read | `attendance +get-class` | read | real-error | missing-real-resource | not-cli-fixable-without-fixture | 真实测试使用的资源/单据/消息/群/文档不存在;需要准备对应 fixture 后才能期望成功,不属于 shortcut 参数投影错误。 |
| 10 | read | `attendance +get-global-setting` | read | real-error | auth-or-permission | not-cli-fixable | 真实账号、应用 scope 或资源权限不足;CLI 只能如实暴露,不能在本仓库内修复权限。 |
| 11 | read | `attendance +get-group` | read | real-error | missing-real-resource | not-cli-fixable-without-fixture | 真实测试使用的资源/单据/消息/群/文档不存在;需要准备对应 fixture 后才能期望成功,不属于 shortcut 参数投影错误。 |
| 12 | read | `attendance +get-group-filtered` | read | real-error | missing-real-resource | not-cli-fixable-without-fixture | 真实测试使用的资源/单据/消息/群/文档不存在;需要准备对应 fixture 后才能期望成功,不属于 shortcut 参数投影错误。 |
| 13 | read | `attendance +get-leave-balance` | read | real-error | input-or-business-validation | test-input-or-backend-rule | 命令已真实进入本地/后端校验;若该项仍使用安全负向输入,则失败符合预期;若使用真实 fixture 仍失败,再作为 CLI bug 处理。 |
| 14 | read | `attendance +list-report-columns` | read | real-error | auth-or-permission | not-cli-fixable | 真实账号、应用 scope 或资源权限不足;CLI 只能如实暴露,不能在本仓库内修复权限。 |
| 15 | read | `attendance +query-report-leave` | read | real-error | auth-or-permission | not-cli-fixable | 真实账号、应用 scope 或资源权限不足;CLI 只能如实暴露,不能在本仓库内修复权限。 |
| 16 | read | `calendar +find-room` | read | real-error | input-or-business-validation | test-input-or-backend-rule | 命令已真实进入本地/后端校验;若该项仍使用安全负向输入,则失败符合预期;若使用真实 fixture 仍失败,再作为 CLI bug 处理。 |
| 17 | read | `calendar +room-find` | read | real-error | input-or-business-validation | test-input-or-backend-rule | 命令已真实进入本地/后端校验;若该项仍使用安全负向输入,则失败符合预期;若使用真实 fixture 仍失败,再作为 CLI bug 处理。 |
| 18 | read | `chat +category-list-conversations` | read | real-error | input-or-business-validation | test-input-or-backend-rule | 命令已真实进入本地/后端校验;若该项仍使用安全负向输入,则失败符合预期;若使用真实 fixture 仍失败,再作为 CLI bug 处理。 |
| 19 | read | `chat +chat-get-by-id` | read | real-error | missing-real-resource | not-cli-fixable-without-fixture | 真实测试使用的资源/单据/消息/群/文档不存在;需要准备对应 fixture 后才能期望成功,不属于 shortcut 参数投影错误。 |
| 20 | read | `chat +chat-members-get` | read | real-error | backend-or-mcp-error | not-cli-fixable-first | fake MCP 已证明 CLI 已装配会话 ID 字段;真实后端仍报 openConversationId/openCid/cid 缺失,优先按 MCP schema/服务端字段映射问题处理。 |
| 21 | read | `chat +chat-messages` | read | real-error | backend-or-mcp-error | not-cli-fixable-first | fake MCP 已证明 CLI 已装配会话 ID 字段;真实后端仍报 openConversationId/openCid/cid 缺失,优先按 MCP schema/服务端字段映射问题处理。 |
| 22 | read | `chat +messages-list` | read | real-error | backend-or-mcp-error | not-cli-fixable-first | fake MCP 已证明 CLI 已装配会话 ID 字段;真实后端仍报 openConversationId/openCid/cid 缺失,优先按 MCP schema/服务端字段映射问题处理。 |
| 23 | read | `chat +messages-resource-url` | read | real-error | missing-real-resource | not-cli-fixable-without-fixture | 真实测试使用的资源/单据/消息/群/文档不存在;需要准备对应 fixture 后才能期望成功,不属于 shortcut 参数投影错误。 |
| 24 | read | `chat +search-msg` | read | real-error | input-or-business-validation | test-input-or-backend-rule | 命令已真实进入本地/后端校验;若该项仍使用安全负向输入,则失败符合预期;若使用真实 fixture 仍失败,再作为 CLI bug 处理。 |
| 25 | read | `chat +thread-replies` | read | real-error | missing-real-resource | not-cli-fixable-without-fixture | 真实测试使用的资源/单据/消息/群/文档不存在;需要准备对应 fixture 后才能期望成功,不属于 shortcut 参数投影错误。 |
| 26 | read | `contact +get-roster` | read | real-error | auth-or-permission | not-cli-fixable | 真实账号、应用 scope 或资源权限不足;CLI 只能如实暴露,不能在本仓库内修复权限。 |
| 27 | read | `contact +list-roster-fields` | read | real-error | auth-or-permission | not-cli-fixable | 真实账号、应用 scope 或资源权限不足;CLI 只能如实暴露,不能在本仓库内修复权限。 |
| 28 | read | `devapp +credentials-get` | read | held | held | manual-approval | 高风险或无安全目标,需人工逐项授权后执行。 |
| 29 | read | `drive +download` | read | real-error | input-or-business-validation | test-input-or-backend-rule | 命令已真实进入本地/后端校验;若该项仍使用安全负向输入,则失败符合预期;若使用真实 fixture 仍失败,再作为 CLI bug 处理。 |
| 30 | read | `drive +list` | read | real-error | input-or-business-validation | test-input-or-backend-rule | 命令已真实进入本地/后端校验;若该项仍使用安全负向输入,则失败符合预期;若使用真实 fixture 仍失败,再作为 CLI bug 处理。 |
| 31 | read | `minutes +action-items` | read | real-error | missing-real-minutes-fixture | not-cli-fixable-without-fixture | 当前账号没有满足条件的妙记/听记或录制会话;需准备真实会议产物后复测。 |
| 32 | read | `minutes +latest-minutes` | read | real-error | missing-real-minutes-fixture | not-cli-fixable-without-fixture | 当前账号没有满足条件的妙记/听记或录制会话;需准备真实会议产物后复测。 |
| 33 | read | `minutes +minutes-search` | read | real-error | input-or-business-validation | test-input-or-backend-rule | 命令已真实进入本地/后端校验;若该项仍使用安全负向输入,则失败符合预期;若使用真实 fixture 仍失败,再作为 CLI bug 处理。 |
| 34 | read | `minutes +transcript` | read | real-error | missing-real-minutes-fixture | not-cli-fixable-without-fixture | 当前账号没有满足条件的妙记/听记或录制会话;需准备真实会议产物后复测。 |
| 35 | read | `oa +done-approvals` | read | real-error | input-or-business-validation | test-input-or-backend-rule | 命令已真实进入本地/后端校验;若该项仍使用安全负向输入,则失败符合预期;若使用真实 fixture 仍失败,再作为 CLI bug 处理。 |
| 36 | read | `oa +pending` | read | real-error | input-or-business-validation | test-input-or-backend-rule | 命令已真实进入本地/后端校验;若该项仍使用安全负向输入,则失败符合预期;若使用真实 fixture 仍失败,再作为 CLI bug 处理。 |
| 37 | read | `report +report-latest` | read | real-error | input-or-business-validation | test-input-or-backend-rule | 命令已真实进入本地/后端校验;若该项仍使用安全负向输入,则失败符合预期;若使用真实 fixture 仍失败,再作为 CLI bug 处理。 |
| 38 | read | `todo +due-today` | read | real-error | input-or-business-validation | test-input-or-backend-rule | 命令已真实进入本地/后端校验;若该项仍使用安全负向输入,则失败符合预期;若使用真实 fixture 仍失败,再作为 CLI bug 处理。 |
| 39 | read | `todo +related-tasks` | read | real-error | input-or-business-validation | test-input-or-backend-rule | 命令已真实进入本地/后端校验;若该项仍使用安全负向输入,则失败符合预期;若使用真实 fixture 仍失败,再作为 CLI bug 处理。 |
| 40 | read | `wiki +node-list` | read | real-error | input-or-business-validation | test-input-or-backend-rule | 命令已真实进入本地/后端校验;若该项仍使用安全负向输入,则失败符合预期;若使用真实 fixture 仍失败,再作为 CLI bug 处理。 |
| 41 | read | `wiki +resolve-space` | read | real-error | input-or-business-validation | test-input-or-backend-rule | 命令已真实进入本地/后端校验;若该项仍使用安全负向输入,则失败符合预期;若使用真实 fixture 仍失败,再作为 CLI bug 处理。 |
| 42 | read | `wiki +space-list` | read | real-error | input-or-business-validation | test-input-or-backend-rule | 命令已真实进入本地/后端校验;若该项仍使用安全负向输入,则失败符合预期;若使用真实 fixture 仍失败,再作为 CLI bug 处理。 |
| 43 | write | `aitable +advperm-disable` | high-risk-write | real-error | auth-or-permission | not-cli-fixable | 真实账号、应用 scope 或资源权限不足;CLI 只能如实暴露,不能在本仓库内修复权限。 |
| 44 | write | `aitable +advperm-enable` | write | real-error | auth-or-permission | not-cli-fixable | 真实账号、应用 scope 或资源权限不足;CLI 只能如实暴露,不能在本仓库内修复权限。 |
| 45 | write | `aitable +attachment-upload` | write | real-error | missing-real-aitable-fixture | not-cli-fixable-without-fixture | AI 表格命令需要真实 Base/Table/View/Record 等资源;安全负向 ID 只能验证调用链,不能让后端成功。 |
| 46 | write | `aitable +base-copy` | write | real-error | missing-real-aitable-fixture | not-cli-fixable-without-fixture | AI 表格命令需要真实 Base/Table/View/Record 等资源;安全负向 ID 只能验证调用链,不能让后端成功。 |
| 47 | write | `aitable +base-delete` | high-risk-write | real-error | missing-real-resource | not-cli-fixable-without-fixture | 真实测试使用的资源/单据/消息/群/文档不存在;需要准备对应 fixture 后才能期望成功,不属于 shortcut 参数投影错误。 |
| 48 | write | `aitable +base-update` | write | real-error | missing-real-aitable-fixture | not-cli-fixable-without-fixture | AI 表格命令需要真实 Base/Table/View/Record 等资源;安全负向 ID 只能验证调用链,不能让后端成功。 |
| 49 | write | `aitable +chart-delete` | high-risk-write | real-error | missing-real-aitable-fixture | not-cli-fixable-without-fixture | AI 表格命令需要真实 Base/Table/View/Record 等资源;安全负向 ID 只能验证调用链,不能让后端成功。 |
| 50 | write | `aitable +chart-share-update` | write | real-error | missing-real-aitable-fixture | not-cli-fixable-without-fixture | AI 表格命令需要真实 Base/Table/View/Record 等资源;安全负向 ID 只能验证调用链,不能让后端成功。 |
| 51 | write | `aitable +chart-update` | write | real-error | backend-or-mcp-error | not-cli-fixable-first | 后端/MCP 服务返回内部错误;CLI 无法直接修复,但报告保留 trace/stdout 供服务端排查。 |
| 52 | write | `aitable +dashboard-arrange` | write | real-error | missing-real-aitable-fixture | not-cli-fixable-without-fixture | AI 表格命令需要真实 Base/Table/View/Record 等资源;安全负向 ID 只能验证调用链,不能让后端成功。 |
| 53 | write | `aitable +dashboard-delete` | high-risk-write | real-error | missing-real-aitable-fixture | not-cli-fixable-without-fixture | AI 表格命令需要真实 Base/Table/View/Record 等资源;安全负向 ID 只能验证调用链,不能让后端成功。 |
| 54 | write | `aitable +dashboard-share-update` | write | real-error | missing-real-aitable-fixture | not-cli-fixable-without-fixture | AI 表格命令需要真实 Base/Table/View/Record 等资源;安全负向 ID 只能验证调用链,不能让后端成功。 |
| 55 | write | `aitable +dashboard-update` | write | real-error | missing-real-aitable-fixture | not-cli-fixable-without-fixture | AI 表格命令需要真实 Base/Table/View/Record 等资源;安全负向 ID 只能验证调用链,不能让后端成功。 |
| 56 | write | `aitable +field-delete` | high-risk-write | real-error | backend-or-mcp-error | not-cli-fixable-first | 后端/MCP 服务返回内部错误;CLI 无法直接修复,但报告保留 trace/stdout 供服务端排查。 |
| 57 | write | `aitable +field-update` | write | real-error | backend-or-mcp-error | not-cli-fixable-first | 后端/MCP 服务返回内部错误;CLI 无法直接修复,但报告保留 trace/stdout 供服务端排查。 |
| 58 | write | `aitable +form-delete` | high-risk-write | real-error | missing-real-aitable-fixture | not-cli-fixable-without-fixture | AI 表格命令需要真实 Base/Table/View/Record 等资源;安全负向 ID 只能验证调用链,不能让后端成功。 |
| 59 | write | `aitable +form-field-hide` | write | real-error | missing-real-aitable-fixture | not-cli-fixable-without-fixture | AI 表格命令需要真实 Base/Table/View/Record 等资源;安全负向 ID 只能验证调用链,不能让后端成功。 |
| 60 | write | `aitable +form-field-update` | write | real-error | missing-real-aitable-fixture | not-cli-fixable-without-fixture | AI 表格命令需要真实 Base/Table/View/Record 等资源;安全负向 ID 只能验证调用链,不能让后端成功。 |
| 61 | write | `aitable +form-share-update` | write | real-error | missing-real-aitable-fixture | not-cli-fixable-without-fixture | AI 表格命令需要真实 Base/Table/View/Record 等资源;安全负向 ID 只能验证调用链,不能让后端成功。 |
| 62 | write | `aitable +form-update` | write | real-error | missing-real-aitable-fixture | not-cli-fixable-without-fixture | AI 表格命令需要真实 Base/Table/View/Record 等资源;安全负向 ID 只能验证调用链,不能让后端成功。 |
| 63 | write | `aitable +import-data` | write | real-error | missing-real-resource | not-cli-fixable-without-fixture | 真实测试使用的资源/单据/消息/群/文档不存在;需要准备对应 fixture 后才能期望成功,不属于 shortcut 参数投影错误。 |
| 64 | write | `aitable +import-upload` | write | real-error | missing-real-aitable-fixture | not-cli-fixable-without-fixture | AI 表格命令需要真实 Base/Table/View/Record 等资源;安全负向 ID 只能验证调用链,不能让后端成功。 |
| 65 | write | `aitable +record-delete` | high-risk-write | real-error | backend-or-mcp-error | not-cli-fixable-first | 后端/MCP 服务返回内部错误;CLI 无法直接修复,但报告保留 trace/stdout 供服务端排查。 |
| 66 | write | `aitable +record-primary-doc-create` | write | real-error | missing-real-aitable-fixture | not-cli-fixable-without-fixture | AI 表格命令需要真实 Base/Table/View/Record 等资源;安全负向 ID 只能验证调用链,不能让后端成功。 |
| 67 | write | `aitable +record-update` | write | real-error | backend-or-mcp-error | not-cli-fixable-first | 后端/MCP 服务返回内部错误;CLI 无法直接修复,但报告保留 trace/stdout 供服务端排查。 |
| 68 | write | `aitable +record-upsert` | write | real-error | backend-or-mcp-error | not-cli-fixable-first | 后端/MCP 服务返回内部错误;CLI 无法直接修复,但报告保留 trace/stdout 供服务端排查。 |
| 69 | write | `aitable +role-create` | write | real-error | missing-real-aitable-fixture | not-cli-fixable-without-fixture | AI 表格命令需要真实 Base/Table/View/Record 等资源;安全负向 ID 只能验证调用链,不能让后端成功。 |
| 70 | write | `aitable +role-delete` | high-risk-write | real-error | backend-or-mcp-error | not-cli-fixable-first | 后端/MCP 服务返回内部错误;CLI 无法直接修复,但报告保留 trace/stdout 供服务端排查。 |
| 71 | write | `aitable +role-update` | write | real-error | backend-or-mcp-error | not-cli-fixable-first | 后端/MCP 服务返回内部错误;CLI 无法直接修复,但报告保留 trace/stdout 供服务端排查。 |
| 72 | write | `aitable +section-create` | write | real-error | missing-real-aitable-fixture | not-cli-fixable-without-fixture | AI 表格命令需要真实 Base/Table/View/Record 等资源;安全负向 ID 只能验证调用链,不能让后端成功。 |
| 73 | write | `aitable +section-delete` | high-risk-write | real-error | missing-real-aitable-fixture | not-cli-fixable-without-fixture | AI 表格命令需要真实 Base/Table/View/Record 等资源;安全负向 ID 只能验证调用链,不能让后端成功。 |
| 74 | write | `aitable +section-move-node` | write | real-error | missing-real-aitable-fixture | not-cli-fixable-without-fixture | AI 表格命令需要真实 Base/Table/View/Record 等资源;安全负向 ID 只能验证调用链,不能让后端成功。 |
| 75 | write | `aitable +section-rename` | write | real-error | missing-real-aitable-fixture | not-cli-fixable-without-fixture | AI 表格命令需要真实 Base/Table/View/Record 等资源;安全负向 ID 只能验证调用链,不能让后端成功。 |
| 76 | write | `aitable +section-reorder` | write | real-error | missing-real-aitable-fixture | not-cli-fixable-without-fixture | AI 表格命令需要真实 Base/Table/View/Record 等资源;安全负向 ID 只能验证调用链,不能让后端成功。 |
| 77 | write | `aitable +table-delete` | high-risk-write | real-error | missing-real-aitable-fixture | not-cli-fixable-without-fixture | AI 表格命令需要真实 Base/Table/View/Record 等资源;安全负向 ID 只能验证调用链,不能让后端成功。 |
| 78 | write | `aitable +table-update` | write | real-error | backend-or-mcp-error | not-cli-fixable-first | 后端/MCP 服务返回内部错误;CLI 无法直接修复,但报告保留 trace/stdout 供服务端排查。 |
| 79 | write | `aitable +view-delete` | high-risk-write | real-error | missing-real-aitable-fixture | not-cli-fixable-without-fixture | AI 表格命令需要真实 Base/Table/View/Record 等资源;安全负向 ID 只能验证调用链,不能让后端成功。 |
| 80 | write | `aitable +view-duplicate` | write | real-error | missing-real-aitable-fixture | not-cli-fixable-without-fixture | AI 表格命令需要真实 Base/Table/View/Record 等资源;安全负向 ID 只能验证调用链,不能让后端成功。 |
| 81 | write | `aitable +view-lock` | write | real-error | missing-real-aitable-fixture | not-cli-fixable-without-fixture | AI 表格命令需要真实 Base/Table/View/Record 等资源;安全负向 ID 只能验证调用链,不能让后端成功。 |
| 82 | write | `aitable +view-set-fill-color-rule` | write | real-error | backend-or-mcp-error | not-cli-fixable-first | 后端/MCP 服务返回内部错误;CLI 无法直接修复,但报告保留 trace/stdout 供服务端排查。 |
| 83 | write | `aitable +view-set-frozen-cols` | write | real-error | missing-real-aitable-fixture | not-cli-fixable-without-fixture | AI 表格命令需要真实 Base/Table/View/Record 等资源;安全负向 ID 只能验证调用链,不能让后端成功。 |
| 84 | write | `aitable +view-set-row-height` | write | real-error | missing-real-aitable-fixture | not-cli-fixable-without-fixture | AI 表格命令需要真实 Base/Table/View/Record 等资源;安全负向 ID 只能验证调用链,不能让后端成功。 |
| 85 | write | `aitable +view-update` | write | real-error | missing-real-aitable-fixture | not-cli-fixable-without-fixture | AI 表格命令需要真实 Base/Table/View/Record 等资源;安全负向 ID 只能验证调用链,不能让后端成功。 |
| 86 | write | `aitable +workflow-disable` | high-risk-write | real-error | missing-real-aitable-fixture | not-cli-fixable-without-fixture | AI 表格命令需要真实 Base/Table/View/Record 等资源;安全负向 ID 只能验证调用链,不能让后端成功。 |
| 87 | write | `aitable +workflow-enable` | write | real-error | missing-real-aitable-fixture | not-cli-fixable-without-fixture | AI 表格命令需要真实 Base/Table/View/Record 等资源;安全负向 ID 只能验证调用链,不能让后端成功。 |
| 88 | write | `attendance +boss-check` | write | real-error | input-or-business-validation | test-input-or-backend-rule | 命令已真实进入本地/后端校验;若该项仍使用安全负向输入,则失败符合预期;若使用真实 fixture 仍失败,再作为 CLI bug 处理。 |
| 89 | write | `attendance +create-class` | write | real-error | input-or-business-validation | test-input-or-backend-rule | 命令已真实进入本地/后端校验;若该项仍使用安全负向输入,则失败符合预期;若使用真实 fixture 仍失败,再作为 CLI bug 处理。 |
| 90 | write | `attendance +create-group` | write | real-error | input-or-business-validation | test-input-or-backend-rule | 命令已真实进入本地/后端校验;若该项仍使用安全负向输入,则失败符合预期;若使用真实 fixture 仍失败,再作为 CLI bug 处理。 |
| 91 | write | `attendance +import-schedule` | write | real-error | input-or-business-validation | test-input-or-backend-rule | 命令已真实进入本地/后端校验;若该项仍使用安全负向输入,则失败符合预期;若使用真实 fixture 仍失败,再作为 CLI bug 处理。 |
| 92 | write | `attendance +save-leave-balance` | write | real-error | auth-or-permission | not-cli-fixable | 真实账号、应用 scope 或资源权限不足;CLI 只能如实暴露,不能在本仓库内修复权限。 |
| 93 | write | `attendance +update-class` | write | real-error | input-or-business-validation | test-input-or-backend-rule | 命令已真实进入本地/后端校验;若该项仍使用安全负向输入,则失败符合预期;若使用真实 fixture 仍失败,再作为 CLI bug 处理。 |
| 94 | write | `attendance +update-group` | write | real-error | input-or-business-validation | test-input-or-backend-rule | 命令已真实进入本地/后端校验;若该项仍使用安全负向输入,则失败符合预期;若使用真实 fixture 仍失败,再作为 CLI bug 处理。 |
| 95 | write | `attendance +update-group-members` | write | real-error | input-or-business-validation | test-input-or-backend-rule | 命令已真实进入本地/后端校验;若该项仍使用安全负向输入,则失败符合预期;若使用真实 fixture 仍失败,再作为 CLI bug 处理。 |
| 96 | write | `attendance +update-leave-type` | write | real-error | auth-or-permission | not-cli-fixable | 真实账号、应用 scope 或资源权限不足;CLI 只能如实暴露,不能在本仓库内修复权限。 |
| 97 | write | `calendar +respond-event` | write | real-error | missing-real-resource | not-cli-fixable-without-fixture | 真实测试使用的资源/单据/消息/群/文档不存在;需要准备对应 fixture 后才能期望成功,不属于 shortcut 参数投影错误。 |
| 98 | write | `chat +category-add-conversation` | write | real-error | auth-or-permission | not-cli-fixable | 真实账号、应用 scope 或资源权限不足;CLI 只能如实暴露,不能在本仓库内修复权限。 |
| 99 | write | `chat +category-remove-conversation` | write | real-error | auth-or-permission | not-cli-fixable | 真实账号、应用 scope 或资源权限不足;CLI 只能如实暴露,不能在本仓库内修复权限。 |
| 100 | write | `chat +chat-add-bot` | write | real-error | auth-or-permission | not-cli-fixable | 真实账号、应用 scope 或资源权限不足;CLI 只能如实暴露,不能在本仓库内修复权限。 |
| 101 | write | `chat +chat-audit-join` | write | real-error | backend-or-mcp-error | not-cli-fixable-first | dry-run 已证明 CLI 装配了 applicantUid/inviterUid;真实后端仍报 applicantUid 缺失,优先按 MCP schema/服务端字段映射问题处理。 |
| 102 | write | `chat +chat-mute-member` | write | real-error | backend-or-mcp-error | not-cli-fixable-first | fake MCP 已证明 CLI 已装配会话 ID 字段;真实后端仍报 openConversationId/openCid/cid 缺失,优先按 MCP schema/服务端字段映射问题处理。 |
| 103 | write | `chat +chat-quit` | write | real-error | input-or-business-validation | test-input-or-backend-rule | 命令已真实进入本地/后端校验;若该项仍使用安全负向输入,则失败符合预期;若使用真实 fixture 仍失败,再作为 CLI bug 处理。 |
| 104 | write | `chat +chat-remove-bot` | high-risk-write | real-error | missing-real-resource | not-cli-fixable-without-fixture | 真实测试使用的资源/单据/消息/群/文档不存在;需要准备对应 fixture 后才能期望成功,不属于 shortcut 参数投影错误。 |
| 105 | write | `chat +chat-role-remove` | high-risk-write | real-error | input-or-business-validation | test-input-or-backend-rule | 命令已真实进入本地/后端校验;若该项仍使用安全负向输入,则失败符合预期;若使用真实 fixture 仍失败,再作为 CLI bug 处理。 |
| 106 | write | `chat +chat-role-remove-user` | write | real-error | input-or-business-validation | test-input-or-backend-rule | 命令已真实进入本地/后端校验;若该项仍使用安全负向输入,则失败符合预期;若使用真实 fixture 仍失败,再作为 CLI bug 处理。 |
| 107 | write | `chat +chat-transfer-owner` | write | real-error | backend-or-mcp-error | not-cli-fixable-first | fake MCP 已证明 CLI 已装配会话 ID 字段;真实后端仍报 openConversationId/openCid/cid 缺失,优先按 MCP schema/服务端字段映射问题处理。 |
| 108 | write | `chat +chat-update-icon` | write | real-error | input-or-business-validation | test-input-or-backend-rule | 命令已真实进入本地/后端校验;若该项仍使用安全负向输入,则失败符合预期;若使用真实 fixture 仍失败,再作为 CLI bug 处理。 |
| 109 | write | `chat +chat-update-settings` | write | real-error | input-or-business-validation | test-input-or-backend-rule | 命令已真实进入本地/后端校验;若该项仍使用安全负向输入,则失败符合预期;若使用真实 fixture 仍失败,再作为 CLI bug 处理。 |
| 110 | write | `chat +conversation-clear-messages` | high-risk-write | real-error | backend-or-mcp-error | not-cli-fixable-first | fake MCP 已证明 CLI 已装配会话 ID 字段;真实后端仍报 openConversationId/openCid/cid 缺失,优先按 MCP schema/服务端字段映射问题处理。 |
| 111 | write | `chat +conversation-clear-red-point` | write | real-error | backend-or-mcp-error | not-cli-fixable-first | fake MCP 已证明 CLI 已装配会话 ID 字段;真实后端仍报 openConversationId/openCid/cid 缺失,优先按 MCP schema/服务端字段映射问题处理。 |
| 112 | write | `chat +conversation-hide` | write | real-error | backend-or-mcp-error | not-cli-fixable-first | fake MCP 已证明 CLI 已装配会话 ID 字段;真实后端仍报 openConversationId/openCid/cid 缺失,优先按 MCP schema/服务端字段映射问题处理。 |
| 113 | write | `chat +conversation-mark-read` | write | real-error | missing-real-resource | not-cli-fixable-without-fixture | 真实测试使用的资源/单据/消息/群/文档不存在;需要准备对应 fixture 后才能期望成功,不属于 shortcut 参数投影错误。 |
| 114 | write | `chat +conversation-mark-unread` | write | real-error | backend-or-mcp-error | not-cli-fixable-first | fake MCP 已证明 CLI 已装配会话 ID 字段;真实后端仍报 openConversationId/openCid/cid 缺失,优先按 MCP schema/服务端字段映射问题处理。 |
| 115 | write | `chat +conversation-mute` | write | real-error | backend-or-mcp-error | not-cli-fixable-first | fake MCP 已证明 CLI 已装配会话 ID 字段;真实后端仍报 openConversationId/openCid/cid 缺失,优先按 MCP schema/服务端字段映射问题处理。 |
| 116 | write | `chat +conversation-mute-at-all` | write | real-error | backend-or-mcp-error | not-cli-fixable-first | fake MCP 已证明 CLI 已装配会话 ID 字段;真实后端仍报 openConversationId/openCid/cid 缺失,优先按 MCP schema/服务端字段映射问题处理。 |
| 117 | write | `chat +conversation-mute-red-envelope` | write | real-error | backend-or-mcp-error | not-cli-fixable-first | fake MCP 已证明 CLI 已装配会话 ID 字段;真实后端仍报 openConversationId/openCid/cid 缺失,优先按 MCP schema/服务端字段映射问题处理。 |
| 118 | write | `chat +conversation-set-top` | write | real-error | backend-or-mcp-error | not-cli-fixable-first | fake MCP 已证明 CLI 已装配会话 ID 字段;真实后端仍报 openConversationId/openCid/cid 缺失,优先按 MCP schema/服务端字段映射问题处理。 |
| 119 | write | `chat +messages-add-emoji` | write | real-error | missing-real-resource | not-cli-fixable-without-fixture | 真实测试使用的资源/单据/消息/群/文档不存在;需要准备对应 fixture 后才能期望成功,不属于 shortcut 参数投影错误。 |
| 120 | write | `chat +messages-add-text-emotion` | write | real-error | missing-real-resource | not-cli-fixable-without-fixture | 真实测试使用的资源/单据/消息/群/文档不存在;需要准备对应 fixture 后才能期望成功,不属于 shortcut 参数投影错误。 |
| 121 | write | `chat +messages-batch-recall-by-bot` | write | real-error | missing-real-resource | not-cli-fixable-without-fixture | 真实测试使用的资源/单据/消息/群/文档不存在;需要准备对应 fixture 后才能期望成功,不属于 shortcut 参数投影错误。 |
| 122 | write | `chat +messages-batch-send-by-bot` | write | real-error | missing-real-resource | not-cli-fixable-without-fixture | 真实测试使用的资源/单据/消息/群/文档不存在;需要准备对应 fixture 后才能期望成功,不属于 shortcut 参数投影错误。 |
| 123 | write | `chat +messages-combine-forward` | write | real-error | missing-real-resource | not-cli-fixable-without-fixture | 真实测试使用的资源/单据/消息/群/文档不存在;需要准备对应 fixture 后才能期望成功,不属于 shortcut 参数投影错误。 |
| 124 | write | `chat +messages-create-text-emotion` | write | real-error | input-or-business-validation | test-input-or-backend-rule | 命令已真实进入本地/后端校验;若该项仍使用安全负向输入,则失败符合预期;若使用真实 fixture 仍失败,再作为 CLI bug 处理。 |
| 125 | write | `chat +messages-forward` | write | real-error | missing-real-resource | not-cli-fixable-without-fixture | 真实测试使用的资源/单据/消息/群/文档不存在;需要准备对应 fixture 后才能期望成功,不属于 shortcut 参数投影错误。 |
| 126 | write | `chat +messages-forward-topic` | write | real-error | missing-real-resource | not-cli-fixable-without-fixture | 真实测试使用的资源/单据/消息/群/文档不存在;需要准备对应 fixture 后才能期望成功,不属于 shortcut 参数投影错误。 |
| 127 | write | `chat +messages-recall` | write | real-error | missing-real-resource | not-cli-fixable-without-fixture | 真实测试使用的资源/单据/消息/群/文档不存在;需要准备对应 fixture 后才能期望成功,不属于 shortcut 参数投影错误。 |
| 128 | write | `chat +messages-recall-by-bot` | write | real-error | input-or-business-validation | test-input-or-backend-rule | 命令已真实进入本地/后端校验;若该项仍使用安全负向输入,则失败符合预期;若使用真实 fixture 仍失败,再作为 CLI bug 处理。 |
| 129 | write | `chat +messages-remove-emoji` | write | real-error | missing-real-resource | not-cli-fixable-without-fixture | 真实测试使用的资源/单据/消息/群/文档不存在;需要准备对应 fixture 后才能期望成功,不属于 shortcut 参数投影错误。 |
| 130 | write | `chat +messages-remove-text-emotion` | write | real-error | missing-real-resource | not-cli-fixable-without-fixture | 真实测试使用的资源/单据/消息/群/文档不存在;需要准备对应 fixture 后才能期望成功,不属于 shortcut 参数投影错误。 |
| 131 | write | `chat +messages-send-by-bot` | write | real-error | missing-real-resource | not-cli-fixable-without-fixture | 真实测试使用的资源/单据/消息/群/文档不存在;需要准备对应 fixture 后才能期望成功,不属于 shortcut 参数投影错误。 |
| 132 | write | `chat +messages-send-card` | write | real-error | backend-or-mcp-error | not-cli-fixable-first | dry-run 已证明 CLI 装配了 receiverUid;真实后端仍报 receiverUid/openConversationId 为空,优先按 MCP schema/服务端字段映射问题处理。 |
| 133 | write | `chat +messages-set-pin` | write | real-error | backend-or-mcp-error | not-cli-fixable-first | fake MCP 已证明 CLI 已装配会话 ID 字段;真实后端仍报 openConversationId/openCid/cid 缺失,优先按 MCP schema/服务端字段映射问题处理。 |
| 134 | write | `chat +messages-set-top` | write | real-error | missing-real-resource | not-cli-fixable-without-fixture | 真实测试使用的资源/单据/消息/群/文档不存在;需要准备对应 fixture 后才能期望成功,不属于 shortcut 参数投影错误。 |
| 135 | write | `chat +messages-unset-pin` | write | real-error | backend-or-mcp-error | not-cli-fixable-first | fake MCP 已证明 CLI 已装配会话 ID 字段;真实后端仍报 openConversationId/openCid/cid 缺失,优先按 MCP schema/服务端字段映射问题处理。 |
| 136 | write | `chat +messages-unset-top` | write | real-error | missing-real-resource | not-cli-fixable-without-fixture | 真实测试使用的资源/单据/消息/群/文档不存在;需要准备对应 fixture 后才能期望成功,不属于 shortcut 参数投影错误。 |
| 137 | write | `devapp +event-subscribe` | write | real-error | auth-or-permission | not-cli-fixable | 真实账号、应用 scope 或资源权限不足;CLI 只能如实暴露,不能在本仓库内修复权限。 |
| 138 | write | `devapp +event-unsubscribe` | high-risk-write | real-error | auth-or-permission | not-cli-fixable | 真实账号、应用 scope 或资源权限不足;CLI 只能如实暴露,不能在本仓库内修复权限。 |
| 139 | write | `devapp +permission-add` | write | real-error | auth-or-permission | not-cli-fixable | 真实账号、应用 scope 或资源权限不足;CLI 只能如实暴露,不能在本仓库内修复权限。 |
| 140 | write | `devapp +permission-remove` | high-risk-write | real-error | auth-or-permission | not-cli-fixable | 真实账号、应用 scope 或资源权限不足;CLI 只能如实暴露,不能在本仓库内修复权限。 |
| 141 | write | `devapp +robot-config` | write | real-error | auth-or-permission | not-cli-fixable | 真实账号、应用 scope 或资源权限不足;CLI 只能如实暴露,不能在本仓库内修复权限。 |
| 142 | write | `devapp +robot-disable` | high-risk-write | real-error | auth-or-permission | not-cli-fixable | 真实账号、应用 scope 或资源权限不足;CLI 只能如实暴露,不能在本仓库内修复权限。 |
| 143 | write | `devapp +robot-enable` | write | real-error | auth-or-permission | not-cli-fixable | 真实账号、应用 scope 或资源权限不足;CLI 只能如实暴露,不能在本仓库内修复权限。 |
| 144 | write | `devapp +security-config` | write | real-error | auth-or-permission | not-cli-fixable | 真实账号、应用 scope 或资源权限不足;CLI 只能如实暴露,不能在本仓库内修复权限。 |
| 145 | write | `devapp +version-create` | write | real-error | input-or-business-validation | test-input-or-backend-rule | 命令已真实进入本地/后端校验;若该项仍使用安全负向输入,则失败符合预期;若使用真实 fixture 仍失败,再作为 CLI bug 处理。 |
| 146 | write | `devapp +version-publish` | write | real-error | input-or-business-validation | test-input-or-backend-rule | 命令已真实进入本地/后端校验;若该项仍使用安全负向输入,则失败符合预期;若使用真实 fixture 仍失败,再作为 CLI bug 处理。 |
| 147 | write | `ding +send-by-message` | write | real-error | input-or-business-validation | test-input-or-backend-rule | 命令已真实进入本地/后端校验;若该项仍使用安全负向输入,则失败符合预期;若使用真实 fixture 仍失败,再作为 CLI bug 处理。 |
| 148 | write | `doc +comment-create-inline` | write | real-error | missing-real-resource | not-cli-fixable-without-fixture | 真实测试使用的资源/单据/消息/群/文档不存在;需要准备对应 fixture 后才能期望成功,不属于 shortcut 参数投影错误。 |
| 149 | write | `doc +template-apply` | write | real-error | auth-or-permission | not-cli-fixable | 真实账号、应用 scope 或资源权限不足;CLI 只能如实暴露,不能在本仓库内修复权限。 |
| 150 | write | `minutes +record-pause` | write | real-error | input-or-business-validation | test-input-or-backend-rule | 命令已真实进入本地/后端校验;若该项仍使用安全负向输入,则失败符合预期;若使用真实 fixture 仍失败,再作为 CLI bug 处理。 |
| 151 | write | `minutes +record-resume` | write | real-error | input-or-business-validation | test-input-or-backend-rule | 命令已真实进入本地/后端校验;若该项仍使用安全负向输入,则失败符合预期;若使用真实 fixture 仍失败,再作为 CLI bug 处理。 |
| 152 | write | `minutes +record-stop` | write | real-error | input-or-business-validation | test-input-or-backend-rule | 命令已真实进入本地/后端校验;若该项仍使用安全负向输入,则失败符合预期;若使用真实 fixture 仍失败,再作为 CLI bug 处理。 |
| 153 | write | `oa +approve-by` | write | real-error | missing-real-resource | not-cli-fixable-without-fixture | 真实测试使用的资源/单据/消息/群/文档不存在;需要准备对应 fixture 后才能期望成功,不属于 shortcut 参数投影错误。 |
| 154 | write | `wiki +node-copy` | write | real-error | missing-real-resource | not-cli-fixable-without-fixture | 真实测试使用的资源/单据/消息/群/文档不存在;需要准备对应 fixture 后才能期望成功,不属于 shortcut 参数投影错误。 |
| 155 | write | `wiki +node-move` | write | real-error | missing-real-resource | not-cli-fixable-without-fixture | 真实测试使用的资源/单据/消息/群/文档不存在;需要准备对应 fixture 后才能期望成功,不属于 shortcut 参数投影错误。 |
| 156 | write | `wiki +wiki-new-doc` | write | real-error | missing-real-resource | not-cli-fixable-without-fixture | 真实测试使用的资源/单据/消息/群/文档不存在;需要准备对应 fixture 后才能期望成功,不属于 shortcut 参数投影错误。 |
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
+22 -22
View File
@@ -1372,9 +1372,10 @@ func newChatCommand() *cobra.Command {
timeVal := mustGetFlag(cmd, "time")
if groupID != "" {
toolArgs := map[string]any{
"openconversation_id": groupID,
"time": timeVal,
"forward": forward,
"openCid": groupID,
"cid": groupID,
"time": timeVal,
"forward": forward,
}
if v := chatIntFlagOrFallback(cmd, "limit", "size"); v > 0 {
toolArgs["limit"] = v
@@ -1500,14 +1501,13 @@ func newChatCommand() *cobra.Command {
openDingTalkID = userID
userID = ""
}
// 数字 userId 尝试 lookup 转换为 openDingTalkId,让所有消息类型(文本/媒体)都走 openDingTalkId 路径
// lookup 失败时降级保留 userID,由下游用 receiverUserId 发送
// 数字 userId 尝试 lookup 转换为 openDingTalkId,让所有消息类型(文本/媒体)都走 openDingTalkId 路径。
// 真实后端的 send_personal_message 单聊路径稳定接受 receiverOpenDingTalkId;
// userId/uid 直传会被服务端判定为空,因此解析失败时直接返回明确错误。
if userID != "" {
resolved, err := resolveOpenDingTalkID(cmd.Context(), userID)
if err != nil {
if commandBoolFlag(cmd, "debug") || commandBoolFlag(cmd, "verbose") {
fmt.Fprintf(os.Stderr, "[debug] resolveOpenDingTalkID(%q) failed: %v, falling back to receiverUserId\n", userID, err)
}
return fmt.Errorf("cannot resolve --user %q to openDingTalkId: %w; pass --open-dingtalk-id instead", userID, err)
} else {
if commandBoolFlag(cmd, "debug") || commandBoolFlag(cmd, "verbose") {
fmt.Fprintf(os.Stderr, "[debug] resolved userID=%q to openDingTalkId=%q\n", userID, resolved)
@@ -1611,7 +1611,7 @@ func newChatCommand() *cobra.Command {
} else if openDingTalkID != "" {
params["receiverOpenDingTalkId"] = openDingTalkID
} else if userID != "" {
params["receiverUserId"] = userID
params["receiverUid"] = userID
} else {
return fmt.Errorf("--group, --user or --open-dingtalk-id is required for media messages")
}
@@ -1667,10 +1667,10 @@ func newChatCommand() *cobra.Command {
if userID != "" {
directContentJSON, _ := marshalJSONRaw(map[string]string{"title": title, "text": text})
directMsgParams := map[string]any{
"receiverUserId": userID,
"msgType": "markdown",
"content": string(directContentJSON),
"clawType": clawType,
"receiverUid": userID,
"msgType": "markdown",
"content": string(directContentJSON),
"clawType": clawType,
}
if msgUuid != "" {
directMsgParams["uuid"] = msgUuid
@@ -4334,8 +4334,8 @@ status 可选值:
AuditIgnore — 忽略(服务端拒绝,不可用)
AuditRefuse — 拒绝(服务端拒绝,不可用)
AuditBlock — 拒绝且不再接受该用户的申请(服务端拒绝,不可用)`,
Example: ` dws chat group audit-join-validation --group <openConversationId> --record-id 123456 --applicant <openDingTalkId> --inviter <openDingTalkId> --status AuditApprove
dws chat group audit-join-validation --group <openConversationId> --record-id 123456 --applicant <openDingTalkId> --inviter <openDingTalkId> --status AuditDelete --description "不符合入群条件"
Example: ` dws chat group audit-join-validation --group <openConversationId> --record-id 123456 --applicant <userId> --inviter <userId> --status AuditApprove
dws chat group audit-join-validation --group <openConversationId> --record-id 123456 --applicant <userId> --inviter <userId> --status AuditDelete --description "不符合入群条件"
# 查询入群验证记录: dws chat group list-join-validations`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "group", "record-id", "applicant", "inviter", "status"); err != nil {
@@ -4346,11 +4346,11 @@ status 可选值:
return fmt.Errorf("--record-id must be a valid integer: %w", err)
}
toolArgs := map[string]any{
"openConversationId": mustGetFlag(cmd, "group"),
"applyRecordId": recordID,
"applicantOpenDingTalkId": mustGetFlag(cmd, "applicant"),
"inviterOpenDingTalkId": mustGetFlag(cmd, "inviter"),
"status": mustGetFlag(cmd, "status"),
"openConversationId": mustGetFlag(cmd, "group"),
"applyRecordId": recordID,
"applicantUid": mustGetFlag(cmd, "applicant"),
"inviterUid": mustGetFlag(cmd, "inviter"),
"status": mustGetFlag(cmd, "status"),
}
if v, _ := cmd.Flags().GetString("description"); v != "" {
toolArgs["auditDescription"] = v
@@ -4364,9 +4364,9 @@ status 可选值:
_ = chatGroupAuditJoinValidationCmd.MarkFlagRequired("record-id")
chatGroupAuditJoinValidationCmd.Flags().String("status", "", "审批动作,真机仅 AuditApprove/AuditDelete 可用;AuditIgnore/AuditRefuse/AuditBlock 服务端拒绝 (必填)")
_ = chatGroupAuditJoinValidationCmd.MarkFlagRequired("status")
chatGroupAuditJoinValidationCmd.Flags().String("applicant", "", "申请人 openDingTalkId (必填)")
chatGroupAuditJoinValidationCmd.Flags().String("applicant", "", "申请人 userId (必填)")
_ = chatGroupAuditJoinValidationCmd.MarkFlagRequired("applicant")
chatGroupAuditJoinValidationCmd.Flags().String("inviter", "", "邀请人 openDingTalkId (必填)")
chatGroupAuditJoinValidationCmd.Flags().String("inviter", "", "邀请人 userId (必填)")
_ = chatGroupAuditJoinValidationCmd.MarkFlagRequired("inviter")
chatGroupAuditJoinValidationCmd.Flags().String("description", "", "审批说明(可选)")
+52 -1
View File
@@ -459,9 +459,34 @@ func buildMinimalPATJSON(code string) string {
// isBusinessError checks if a parsed JSON body represents a business-level error.
func isBusinessError(body map[string]any) bool {
if _, ok := body["error"].(string); ok {
if v, ok := body["error"]; ok {
switch t := v.(type) {
case string:
if strings.TrimSpace(t) != "" {
return true
}
case map[string]any:
if len(t) > 0 {
return true
}
case []any:
if len(t) > 0 {
return true
}
default:
if t != nil {
return true
}
}
}
if v, ok := body["status"].(string); ok && strings.EqualFold(strings.TrimSpace(v), "error") {
return true
}
for _, key := range []string{"errorCode", "error_code", "code"} {
if isErrorCodeValue(body[key]) {
return true
}
}
if v, ok := body["success"].(bool); ok && !v {
return true
}
@@ -471,6 +496,32 @@ func isBusinessError(body map[string]any) bool {
return false
}
func isErrorCodeValue(v any) bool {
switch t := v.(type) {
case string:
code := strings.TrimSpace(t)
if code == "" {
return false
}
switch strings.ToLower(code) {
case "0", "ok", "success", "succeed":
return false
default:
return true
}
case float64:
return t != 0
case int:
return t != 0
case int64:
return t != 0
case json.Number:
return strings.TrimSpace(t.String()) != "" && t.String() != "0"
default:
return false
}
}
// isNotLoggedInError checks if the error body indicates missing authentication.
func isNotLoggedInError(body map[string]any) bool {
if errMsg, ok := body["error"].(string); ok {
+41
View File
@@ -0,0 +1,41 @@
package helpers
import "testing"
func TestIsBusinessErrorRecognizesRealErrorEnvelopes(t *testing.T) {
cases := []map[string]any{
{"status": "error", "success": true, "error": map[string]any{"code": "INVALID_BASE_ID"}},
{"success": true, "errorCode": "1001"},
{"success": true, "error": []any{"failed"}},
{"success": false},
}
for _, tc := range cases {
if !isBusinessError(tc) {
t.Fatalf("isBusinessError(%v) = false, want true", tc)
}
}
}
func TestIsBusinessErrorAllowsSuccessEnvelope(t *testing.T) {
body := map[string]any{
"success": true,
"errorCode": nil,
"errorMsg": nil,
"result": map[string]any{"ok": true},
}
if isBusinessError(body) {
t.Fatalf("isBusinessError(%v) = true, want false", body)
}
}
func TestIsBusinessErrorAllowsCodeZeroSuccessEnvelope(t *testing.T) {
body := map[string]any{
"success": true,
"code": "0",
"message": "success",
"result": []any{},
}
if isBusinessError(body) {
t.Fatalf("isBusinessError(%v) = true, want false", body)
}
}
+20 -4
View File
@@ -63,6 +63,7 @@ var ConversationSetTop = shortcut.Shortcut{
Execute: func(rt *shortcut.RuntimeContext) error {
return rt.CallMCP("set_top_conversation", map[string]any{
"openConversationId": rt.Str("conversation-id"),
"cid": rt.Str("conversation-id"),
"top": !rt.Bool("off"),
})
},
@@ -84,6 +85,7 @@ var ConversationMute = shortcut.Shortcut{
Execute: func(rt *shortcut.RuntimeContext) error {
return rt.CallMCP("update_notification_off", map[string]any{
"openConversationId": rt.Str("conversation-id"),
"cid": rt.Str("conversation-id"),
"mute": !rt.Bool("off"),
})
},
@@ -105,6 +107,7 @@ var ConversationMuteAtAll = shortcut.Shortcut{
Execute: func(rt *shortcut.RuntimeContext) error {
return rt.CallMCP("update_at_all_notification_off", map[string]any{
"openConversationId": rt.Str("conversation-id"),
"cid": rt.Str("conversation-id"),
"mute": !rt.Bool("off"),
})
},
@@ -126,6 +129,7 @@ var ConversationMuteRedEnvelope = shortcut.Shortcut{
Execute: func(rt *shortcut.RuntimeContext) error {
return rt.CallMCP("update_red_env_notification_off", map[string]any{
"openConversationId": rt.Str("conversation-id"),
"cid": rt.Str("conversation-id"),
"mute": !rt.Bool("off"),
})
},
@@ -144,7 +148,10 @@ var ConversationMarkUnread = shortcut.Shortcut{
},
Tips: []string{`dws chat +conversation-mark-unread --conversation-id <openConversationId>`},
Execute: func(rt *shortcut.RuntimeContext) error {
return rt.CallMCP("mark_conversation_unread", map[string]any{"openConversationId": rt.Str("conversation-id")})
return rt.CallMCP("mark_conversation_unread", map[string]any{
"openConversationId": rt.Str("conversation-id"),
"cid": rt.Str("conversation-id"),
})
},
}
@@ -161,7 +168,10 @@ var ConversationClearRedPoint = shortcut.Shortcut{
},
Tips: []string{`dws chat +conversation-clear-red-point --conversation-id <openConversationId>`},
Execute: func(rt *shortcut.RuntimeContext) error {
return rt.CallMCP("clear_conversation_red_point", map[string]any{"openConversationId": rt.Str("conversation-id")})
return rt.CallMCP("clear_conversation_red_point", map[string]any{
"openConversationId": rt.Str("conversation-id"),
"cid": rt.Str("conversation-id"),
})
},
}
@@ -399,7 +409,10 @@ var ConversationClearMessages = shortcut.Shortcut{
},
Tips: []string{`dws chat +conversation-clear-messages --conversation-id <openConversationId>`},
Execute: func(rt *shortcut.RuntimeContext) error {
return rt.CallMCP("clear_conversation_messages", map[string]any{"openConversationId": rt.Str("conversation-id")})
return rt.CallMCP("clear_conversation_messages", map[string]any{
"openConversationId": rt.Str("conversation-id"),
"cid": rt.Str("conversation-id"),
})
},
}
@@ -437,7 +450,10 @@ var ConversationHide = shortcut.Shortcut{
},
Tips: []string{`dws chat +conversation-hide --conversation-id <openConversationId>`},
Execute: func(rt *shortcut.RuntimeContext) error {
return rt.CallMCP("hide_conversation", map[string]any{"openConversationId": rt.Str("conversation-id")})
return rt.CallMCP("hide_conversation", map[string]any{
"openConversationId": rt.Str("conversation-id"),
"cid": rt.Str("conversation-id"),
})
},
}
+19 -11
View File
@@ -64,6 +64,7 @@ var ChatMembersGet = shortcut.Shortcut{
Execute: func(rt *shortcut.RuntimeContext) error {
return rt.CallMCP("list_group_member_by_ids", map[string]any{
"openConversationId": rt.Str("id"),
"cid": rt.Str("id"),
"memberOpenDingTalkIds": rt.StrSlice("users"),
})
},
@@ -87,7 +88,10 @@ var ChatTransferOwner = shortcut.Shortcut{
Tips: []string{`dws chat +chat-transfer-owner --group <openConversationId> --new-owner <openDingTalkId>`},
Execute: func(rt *shortcut.RuntimeContext) error {
newOwner := rt.Str("new-owner")
params := map[string]any{"openConversationId": rt.Str("group")}
params := map[string]any{
"openConversationId": rt.Str("group"),
"cid": rt.Str("group"),
}
if isOpenID(newOwner) {
params["newOwnerOpenDingTalkId"] = newOwner
} else {
@@ -111,7 +115,10 @@ var ChatInviteURL = shortcut.Shortcut{
},
Tips: []string{`dws chat +chat-invite-url --group <openConversationId>`},
Execute: func(rt *shortcut.RuntimeContext) error {
params := map[string]any{"openConversationId": rt.Str("group")}
params := map[string]any{
"openConversationId": rt.Str("group"),
"cid": rt.Str("group"),
}
if rt.Changed("expires-seconds") {
params["expiresSeconds"] = rt.Int("expires-seconds")
}
@@ -457,24 +464,24 @@ var ChatAuditJoin = shortcut.Shortcut{
Command: "+chat-audit-join",
Product: "im",
Description: "审批入群验证(通过/拒绝/删除/忽略/拉黑)",
Intent: "当你要处理某条入群申请时使用;会实际执行通过/拒绝/删除/忽略/拉黑动作,需传群 openConversationId、recordId、申请人与邀请人 openDingTalkId 及 status。",
Intent: "当你要处理某条入群申请时使用;会实际执行通过/拒绝/删除/忽略/拉黑动作,需传群 openConversationId、recordId、申请人与邀请人 userId 及 status。",
Risk: shortcut.RiskWrite,
Flags: []shortcut.Flag{
{Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId", Required: true},
{Name: "record-id", Type: shortcut.FlagInt, Desc: "申请记录 ID", Required: true},
{Name: "applicant", Type: shortcut.FlagString, Desc: "申请人 openDingTalkId", Required: true},
{Name: "inviter", Type: shortcut.FlagString, Desc: "邀请人 openDingTalkId", Required: true},
{Name: "applicant", Type: shortcut.FlagString, Desc: "申请人 userId", Required: true},
{Name: "inviter", Type: shortcut.FlagString, Desc: "邀请人 userId", Required: true},
{Name: "status", Type: shortcut.FlagString, Desc: "审批动作", Required: true, Enum: []string{"AuditApprove", "AuditDelete", "AuditIgnore", "AuditRefuse", "AuditBlock"}},
{Name: "description", Type: shortcut.FlagString, Desc: "审批说明"},
},
Tips: []string{`dws chat +chat-audit-join --group <openConversationId> --record-id 123 --applicant <odid> --inviter <odid> --status AuditApprove`},
Tips: []string{`dws chat +chat-audit-join --group <openConversationId> --record-id 123 --applicant <userId> --inviter <userId> --status AuditApprove`},
Execute: func(rt *shortcut.RuntimeContext) error {
params := map[string]any{
"openConversationId": rt.Str("group"),
"applyRecordId": rt.Int("record-id"),
"applicantOpenDingTalkId": rt.Str("applicant"),
"inviterOpenDingTalkId": rt.Str("inviter"),
"status": rt.Str("status"),
"openConversationId": rt.Str("group"),
"applyRecordId": rt.Int("record-id"),
"applicantUid": rt.Str("applicant"),
"inviterUid": rt.Str("inviter"),
"status": rt.Str("status"),
}
if rt.Changed("description") {
params["auditDescription"] = rt.Str("description")
@@ -661,6 +668,7 @@ var ChatMuteMember = shortcut.Shortcut{
off := rt.Bool("off")
params := map[string]any{
"openConversationId": rt.Str("group"),
"cid": rt.Str("group"),
"mute": !off,
}
if len(userIDs) > 0 {
+13 -7
View File
@@ -214,9 +214,10 @@ var MessagesList = shortcut.Shortcut{
Tips: []string{`dws chat +messages-list --group <openConversationId> --time "2025-03-01 00:00:00"`},
Execute: func(rt *shortcut.RuntimeContext) error {
params := map[string]any{
"openconversation_id": rt.Str("group"),
"time": rt.Str("time"),
"forward": rt.Bool("forward"),
"openCid": rt.Str("group"),
"cid": rt.Str("group"),
"time": rt.Str("time"),
"forward": rt.Bool("forward"),
}
if rt.Int("limit") > 0 {
params["limit"] = rt.Int("limit")
@@ -638,11 +639,11 @@ var MessagesSendCard = shortcut.Shortcut{
Command: "+messages-send-card",
Product: "im",
Description: "创建并推送流式卡片(需配合 messages-update-card)",
Intent: "当你要发送一张可后续流式更新的卡片消息(如 AI 逐字输出)时使用;会实际推送卡片并返回 bizId,群 openConversationId 或单聊接收者 openDingTalkId 二选一,配合 messages-update-card 更新。",
Intent: "当你要发送一张可后续流式更新的卡片消息(如 AI 逐字输出)时使用;会实际推送卡片并返回 bizId,群 openConversationId 或单聊接收者 userId 二选一,配合 messages-update-card 更新。",
Risk: shortcut.RiskWrite,
Flags: []shortcut.Flag{
{Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId(与 --receiver 互斥)"},
{Name: "receiver", Type: shortcut.FlagString, Desc: "单聊接收者 openDingTalkId(与 --group 互斥)"},
{Name: "receiver", Type: shortcut.FlagString, Desc: "单聊接收者 userId(与 --group 互斥)"},
},
Tips: []string{`dws chat +messages-send-card --group <openConversationId>`},
Execute: func(rt *shortcut.RuntimeContext) error {
@@ -658,7 +659,7 @@ var MessagesSendCard = shortcut.Shortcut{
if group != "" {
params["openConversationId"] = group
} else {
params["receiverOpenDingTalkId"] = receiver
params["receiverUid"] = receiver
}
return rt.CallMCP("create_and_send_card", params)
},
@@ -809,6 +810,7 @@ var MessagesSetPin = shortcut.Shortcut{
Execute: func(rt *shortcut.RuntimeContext) error {
return rt.CallMCP("set_pin_message", map[string]any{
"openConversationId": rt.Str("open-conversation-id"),
"cid": rt.Str("open-conversation-id"),
"openMessageId": rt.Str("msg-id"),
})
},
@@ -830,6 +832,7 @@ var MessagesUnsetPin = shortcut.Shortcut{
Execute: func(rt *shortcut.RuntimeContext) error {
return rt.CallMCP("unset_pin_message", map[string]any{
"openConversationId": rt.Str("open-conversation-id"),
"cid": rt.Str("open-conversation-id"),
"openMessageId": rt.Str("msg-id"),
})
},
@@ -850,7 +853,10 @@ var MessagesListPin = shortcut.Shortcut{
},
Tips: []string{`dws chat +messages-list-pin --open-conversation-id <openConversationId>`},
Execute: func(rt *shortcut.RuntimeContext) error {
params := map[string]any{"openConversationId": rt.Str("open-conversation-id")}
params := map[string]any{
"openConversationId": rt.Str("open-conversation-id"),
"cid": rt.Str("open-conversation-id"),
}
if rt.Changed("cursor") {
params["cursor"] = rt.Str("cursor")
}
+3
View File
@@ -28,6 +28,9 @@ var allShortcuts []Shortcut
// Register adds one or more shortcuts to the built-in registry. Call from a
// service package's init().
func Register(shortcuts ...Shortcut) {
for i := range shortcuts {
shortcuts[i] = applyReleaseGate(shortcuts[i])
}
allShortcuts = append(allShortcuts, shortcuts...)
}
+23
View File
@@ -0,0 +1,23 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0
package shortcut
func releaseHiddenKey(service, command string) string {
return service + "\x00" + command
}
func applyReleaseGate(s Shortcut) Shortcut {
if _, ok := ReleaseHiddenReason(s.Service, s.Command); ok {
s.Hidden = true
}
return s
}
// ReleaseHiddenReason reports why a shortcut is hidden from public discovery in
// this release. Hidden shortcuts remain invocable by exact command path so the
// implementation can be kept and repaired in the next iteration.
func ReleaseHiddenReason(service, command string) (string, bool) {
reason, ok := releaseHiddenShortcuts[releaseHiddenKey(service, command)]
return reason, ok
}
@@ -0,0 +1,166 @@
// Code generated by scripts/gen_shortcut_release_hidden.py; DO NOT EDIT.
package shortcut
// releaseHiddenShortcuts contains shortcuts intentionally hidden from
// public discovery in this release because their latest real-backend
// test did not pass. The command implementations remain registered and
// invocable by exact path for internal follow-up testing.
var releaseHiddenShortcuts = map[string]string{
"aitable\u0000+base-get-primary-doc-id": "read real test not passed; status=real-error; category=backend-or-mcp-error; fixability=not-cli-fixable-first",
"aitable\u0000+chart-share-get": "read real test not passed; status=real-error; category=auth-or-permission; fixability=not-cli-fixable",
"aitable\u0000+dashboard-share-get": "read real test not passed; status=real-error; category=missing-real-resource; fixability=not-cli-fixable-without-fixture",
"aitable\u0000+export-data": "read real test not passed; status=real-error; category=backend-or-mcp-error; fixability=not-cli-fixable-first",
"aitable\u0000+record-primary-doc-get": "read real test not passed; status=real-error; category=backend-or-mcp-error; fixability=not-cli-fixable-first",
"aitable\u0000+role-get": "read real test not passed; status=real-error; category=backend-or-mcp-error; fixability=not-cli-fixable-first",
"aitable\u0000+workflow-get": "read real test not passed; status=real-error; category=backend-or-mcp-error; fixability=not-cli-fixable-first",
"aitable\u0000+workflow-list": "read real test not passed; status=real-error; category=backend-or-mcp-error; fixability=not-cli-fixable-first",
"attendance\u0000+get-class": "read real test not passed; status=real-error; category=missing-real-resource; fixability=not-cli-fixable-without-fixture",
"attendance\u0000+get-global-setting": "read real test not passed; status=real-error; category=auth-or-permission; fixability=not-cli-fixable",
"attendance\u0000+get-group": "read real test not passed; status=real-error; category=missing-real-resource; fixability=not-cli-fixable-without-fixture",
"attendance\u0000+get-group-filtered": "read real test not passed; status=real-error; category=missing-real-resource; fixability=not-cli-fixable-without-fixture",
"attendance\u0000+get-leave-balance": "read real test not passed; status=real-error; category=input-or-business-validation; fixability=test-input-or-backend-rule",
"attendance\u0000+list-report-columns": "read real test not passed; status=real-error; category=auth-or-permission; fixability=not-cli-fixable",
"attendance\u0000+query-report-leave": "read real test not passed; status=real-error; category=auth-or-permission; fixability=not-cli-fixable",
"calendar\u0000+find-room": "read real test not passed; status=real-error; category=input-or-business-validation; fixability=test-input-or-backend-rule",
"calendar\u0000+room-find": "read real test not passed; status=real-error; category=input-or-business-validation; fixability=test-input-or-backend-rule",
"chat\u0000+category-list-conversations": "read real test not passed; status=real-error; category=input-or-business-validation; fixability=test-input-or-backend-rule",
"chat\u0000+chat-get-by-id": "read real test not passed; status=real-error; category=missing-real-resource; fixability=not-cli-fixable-without-fixture",
"chat\u0000+chat-members-get": "read real test not passed; status=real-error; category=backend-or-mcp-error; fixability=not-cli-fixable-first",
"chat\u0000+chat-messages": "read real test not passed; status=real-error; category=backend-or-mcp-error; fixability=not-cli-fixable-first",
"chat\u0000+messages-list": "read real test not passed; status=real-error; category=backend-or-mcp-error; fixability=not-cli-fixable-first",
"chat\u0000+messages-resource-url": "read real test not passed; status=real-error; category=missing-real-resource; fixability=not-cli-fixable-without-fixture",
"chat\u0000+search-msg": "read real test not passed; status=real-error; category=input-or-business-validation; fixability=test-input-or-backend-rule",
"chat\u0000+thread-replies": "read real test not passed; status=real-error; category=missing-real-resource; fixability=not-cli-fixable-without-fixture",
"contact\u0000+get-roster": "read real test not passed; status=real-error; category=auth-or-permission; fixability=not-cli-fixable",
"contact\u0000+list-roster-fields": "read real test not passed; status=real-error; category=auth-or-permission; fixability=not-cli-fixable",
"devapp\u0000+credentials-get": "read real test not passed; status=held; category=held; fixability=manual-approval",
"drive\u0000+download": "read real test not passed; status=real-error; category=input-or-business-validation; fixability=test-input-or-backend-rule",
"drive\u0000+list": "read real test not passed; status=real-error; category=input-or-business-validation; fixability=test-input-or-backend-rule",
"minutes\u0000+action-items": "read real test not passed; status=real-error; category=missing-real-minutes-fixture; fixability=not-cli-fixable-without-fixture",
"minutes\u0000+latest-minutes": "read real test not passed; status=real-error; category=missing-real-minutes-fixture; fixability=not-cli-fixable-without-fixture",
"minutes\u0000+minutes-search": "read real test not passed; status=real-error; category=input-or-business-validation; fixability=test-input-or-backend-rule",
"minutes\u0000+transcript": "read real test not passed; status=real-error; category=missing-real-minutes-fixture; fixability=not-cli-fixable-without-fixture",
"oa\u0000+done-approvals": "read real test not passed; status=real-error; category=input-or-business-validation; fixability=test-input-or-backend-rule",
"oa\u0000+pending": "read real test not passed; status=real-error; category=input-or-business-validation; fixability=test-input-or-backend-rule",
"report\u0000+report-latest": "read real test not passed; status=real-error; category=input-or-business-validation; fixability=test-input-or-backend-rule",
"todo\u0000+due-today": "read real test not passed; status=real-error; category=input-or-business-validation; fixability=test-input-or-backend-rule",
"todo\u0000+related-tasks": "read real test not passed; status=real-error; category=input-or-business-validation; fixability=test-input-or-backend-rule",
"wiki\u0000+node-list": "read real test not passed; status=real-error; category=input-or-business-validation; fixability=test-input-or-backend-rule",
"wiki\u0000+resolve-space": "read real test not passed; status=real-error; category=input-or-business-validation; fixability=test-input-or-backend-rule",
"wiki\u0000+space-list": "read real test not passed; status=real-error; category=input-or-business-validation; fixability=test-input-or-backend-rule",
"aitable\u0000+advperm-disable": "write real test not passed; status=real-error; category=auth-or-permission; fixability=not-cli-fixable",
"aitable\u0000+advperm-enable": "write real test not passed; status=real-error; category=auth-or-permission; fixability=not-cli-fixable",
"aitable\u0000+attachment-upload": "write real test not passed; status=real-error; category=missing-real-aitable-fixture; fixability=not-cli-fixable-without-fixture",
"aitable\u0000+base-copy": "write real test not passed; status=real-error; category=missing-real-aitable-fixture; fixability=not-cli-fixable-without-fixture",
"aitable\u0000+base-delete": "write real test not passed; status=real-error; category=missing-real-resource; fixability=not-cli-fixable-without-fixture",
"aitable\u0000+base-update": "write real test not passed; status=real-error; category=missing-real-aitable-fixture; fixability=not-cli-fixable-without-fixture",
"aitable\u0000+chart-delete": "write real test not passed; status=real-error; category=missing-real-aitable-fixture; fixability=not-cli-fixable-without-fixture",
"aitable\u0000+chart-share-update": "write real test not passed; status=real-error; category=missing-real-aitable-fixture; fixability=not-cli-fixable-without-fixture",
"aitable\u0000+chart-update": "write real test not passed; status=real-error; category=backend-or-mcp-error; fixability=not-cli-fixable-first",
"aitable\u0000+dashboard-arrange": "write real test not passed; status=real-error; category=missing-real-aitable-fixture; fixability=not-cli-fixable-without-fixture",
"aitable\u0000+dashboard-delete": "write real test not passed; status=real-error; category=missing-real-aitable-fixture; fixability=not-cli-fixable-without-fixture",
"aitable\u0000+dashboard-share-update": "write real test not passed; status=real-error; category=missing-real-aitable-fixture; fixability=not-cli-fixable-without-fixture",
"aitable\u0000+dashboard-update": "write real test not passed; status=real-error; category=missing-real-aitable-fixture; fixability=not-cli-fixable-without-fixture",
"aitable\u0000+field-delete": "write real test not passed; status=real-error; category=backend-or-mcp-error; fixability=not-cli-fixable-first",
"aitable\u0000+field-update": "write real test not passed; status=real-error; category=backend-or-mcp-error; fixability=not-cli-fixable-first",
"aitable\u0000+form-delete": "write real test not passed; status=real-error; category=missing-real-aitable-fixture; fixability=not-cli-fixable-without-fixture",
"aitable\u0000+form-field-hide": "write real test not passed; status=real-error; category=missing-real-aitable-fixture; fixability=not-cli-fixable-without-fixture",
"aitable\u0000+form-field-update": "write real test not passed; status=real-error; category=missing-real-aitable-fixture; fixability=not-cli-fixable-without-fixture",
"aitable\u0000+form-share-update": "write real test not passed; status=real-error; category=missing-real-aitable-fixture; fixability=not-cli-fixable-without-fixture",
"aitable\u0000+form-update": "write real test not passed; status=real-error; category=missing-real-aitable-fixture; fixability=not-cli-fixable-without-fixture",
"aitable\u0000+import-data": "write real test not passed; status=real-error; category=missing-real-resource; fixability=not-cli-fixable-without-fixture",
"aitable\u0000+import-upload": "write real test not passed; status=real-error; category=missing-real-aitable-fixture; fixability=not-cli-fixable-without-fixture",
"aitable\u0000+record-delete": "write real test not passed; status=real-error; category=backend-or-mcp-error; fixability=not-cli-fixable-first",
"aitable\u0000+record-primary-doc-create": "write real test not passed; status=real-error; category=missing-real-aitable-fixture; fixability=not-cli-fixable-without-fixture",
"aitable\u0000+record-update": "write real test not passed; status=real-error; category=backend-or-mcp-error; fixability=not-cli-fixable-first",
"aitable\u0000+record-upsert": "write real test not passed; status=real-error; category=backend-or-mcp-error; fixability=not-cli-fixable-first",
"aitable\u0000+role-create": "write real test not passed; status=real-error; category=missing-real-aitable-fixture; fixability=not-cli-fixable-without-fixture",
"aitable\u0000+role-delete": "write real test not passed; status=real-error; category=backend-or-mcp-error; fixability=not-cli-fixable-first",
"aitable\u0000+role-update": "write real test not passed; status=real-error; category=backend-or-mcp-error; fixability=not-cli-fixable-first",
"aitable\u0000+section-create": "write real test not passed; status=real-error; category=missing-real-aitable-fixture; fixability=not-cli-fixable-without-fixture",
"aitable\u0000+section-delete": "write real test not passed; status=real-error; category=missing-real-aitable-fixture; fixability=not-cli-fixable-without-fixture",
"aitable\u0000+section-move-node": "write real test not passed; status=real-error; category=missing-real-aitable-fixture; fixability=not-cli-fixable-without-fixture",
"aitable\u0000+section-rename": "write real test not passed; status=real-error; category=missing-real-aitable-fixture; fixability=not-cli-fixable-without-fixture",
"aitable\u0000+section-reorder": "write real test not passed; status=real-error; category=missing-real-aitable-fixture; fixability=not-cli-fixable-without-fixture",
"aitable\u0000+table-delete": "write real test not passed; status=real-error; category=missing-real-aitable-fixture; fixability=not-cli-fixable-without-fixture",
"aitable\u0000+table-update": "write real test not passed; status=real-error; category=backend-or-mcp-error; fixability=not-cli-fixable-first",
"aitable\u0000+view-delete": "write real test not passed; status=real-error; category=missing-real-aitable-fixture; fixability=not-cli-fixable-without-fixture",
"aitable\u0000+view-duplicate": "write real test not passed; status=real-error; category=missing-real-aitable-fixture; fixability=not-cli-fixable-without-fixture",
"aitable\u0000+view-lock": "write real test not passed; status=real-error; category=missing-real-aitable-fixture; fixability=not-cli-fixable-without-fixture",
"aitable\u0000+view-set-fill-color-rule": "write real test not passed; status=real-error; category=backend-or-mcp-error; fixability=not-cli-fixable-first",
"aitable\u0000+view-set-frozen-cols": "write real test not passed; status=real-error; category=missing-real-aitable-fixture; fixability=not-cli-fixable-without-fixture",
"aitable\u0000+view-set-row-height": "write real test not passed; status=real-error; category=missing-real-aitable-fixture; fixability=not-cli-fixable-without-fixture",
"aitable\u0000+view-update": "write real test not passed; status=real-error; category=missing-real-aitable-fixture; fixability=not-cli-fixable-without-fixture",
"aitable\u0000+workflow-disable": "write real test not passed; status=real-error; category=missing-real-aitable-fixture; fixability=not-cli-fixable-without-fixture",
"aitable\u0000+workflow-enable": "write real test not passed; status=real-error; category=missing-real-aitable-fixture; fixability=not-cli-fixable-without-fixture",
"attendance\u0000+boss-check": "write real test not passed; status=real-error; category=input-or-business-validation; fixability=test-input-or-backend-rule",
"attendance\u0000+create-class": "write real test not passed; status=real-error; category=input-or-business-validation; fixability=test-input-or-backend-rule",
"attendance\u0000+create-group": "write real test not passed; status=real-error; category=input-or-business-validation; fixability=test-input-or-backend-rule",
"attendance\u0000+import-schedule": "write real test not passed; status=real-error; category=input-or-business-validation; fixability=test-input-or-backend-rule",
"attendance\u0000+save-leave-balance": "write real test not passed; status=real-error; category=auth-or-permission; fixability=not-cli-fixable",
"attendance\u0000+update-class": "write real test not passed; status=real-error; category=input-or-business-validation; fixability=test-input-or-backend-rule",
"attendance\u0000+update-group": "write real test not passed; status=real-error; category=input-or-business-validation; fixability=test-input-or-backend-rule",
"attendance\u0000+update-group-members": "write real test not passed; status=real-error; category=input-or-business-validation; fixability=test-input-or-backend-rule",
"attendance\u0000+update-leave-type": "write real test not passed; status=real-error; category=auth-or-permission; fixability=not-cli-fixable",
"calendar\u0000+respond-event": "write real test not passed; status=real-error; category=missing-real-resource; fixability=not-cli-fixable-without-fixture",
"chat\u0000+category-add-conversation": "write real test not passed; status=real-error; category=auth-or-permission; fixability=not-cli-fixable",
"chat\u0000+category-remove-conversation": "write real test not passed; status=real-error; category=auth-or-permission; fixability=not-cli-fixable",
"chat\u0000+chat-add-bot": "write real test not passed; status=real-error; category=auth-or-permission; fixability=not-cli-fixable",
"chat\u0000+chat-audit-join": "write real test not passed; status=real-error; category=backend-or-mcp-error; fixability=not-cli-fixable-first",
"chat\u0000+chat-mute-member": "write real test not passed; status=real-error; category=backend-or-mcp-error; fixability=not-cli-fixable-first",
"chat\u0000+chat-quit": "write real test not passed; status=real-error; category=input-or-business-validation; fixability=test-input-or-backend-rule",
"chat\u0000+chat-remove-bot": "write real test not passed; status=real-error; category=missing-real-resource; fixability=not-cli-fixable-without-fixture",
"chat\u0000+chat-role-remove": "write real test not passed; status=real-error; category=input-or-business-validation; fixability=test-input-or-backend-rule",
"chat\u0000+chat-role-remove-user": "write real test not passed; status=real-error; category=input-or-business-validation; fixability=test-input-or-backend-rule",
"chat\u0000+chat-transfer-owner": "write real test not passed; status=real-error; category=backend-or-mcp-error; fixability=not-cli-fixable-first",
"chat\u0000+chat-update-icon": "write real test not passed; status=real-error; category=input-or-business-validation; fixability=test-input-or-backend-rule",
"chat\u0000+chat-update-settings": "write real test not passed; status=real-error; category=input-or-business-validation; fixability=test-input-or-backend-rule",
"chat\u0000+conversation-clear-messages": "write real test not passed; status=real-error; category=backend-or-mcp-error; fixability=not-cli-fixable-first",
"chat\u0000+conversation-clear-red-point": "write real test not passed; status=real-error; category=backend-or-mcp-error; fixability=not-cli-fixable-first",
"chat\u0000+conversation-hide": "write real test not passed; status=real-error; category=backend-or-mcp-error; fixability=not-cli-fixable-first",
"chat\u0000+conversation-mark-read": "write real test not passed; status=real-error; category=missing-real-resource; fixability=not-cli-fixable-without-fixture",
"chat\u0000+conversation-mark-unread": "write real test not passed; status=real-error; category=backend-or-mcp-error; fixability=not-cli-fixable-first",
"chat\u0000+conversation-mute": "write real test not passed; status=real-error; category=backend-or-mcp-error; fixability=not-cli-fixable-first",
"chat\u0000+conversation-mute-at-all": "write real test not passed; status=real-error; category=backend-or-mcp-error; fixability=not-cli-fixable-first",
"chat\u0000+conversation-mute-red-envelope": "write real test not passed; status=real-error; category=backend-or-mcp-error; fixability=not-cli-fixable-first",
"chat\u0000+conversation-set-top": "write real test not passed; status=real-error; category=backend-or-mcp-error; fixability=not-cli-fixable-first",
"chat\u0000+messages-add-emoji": "write real test not passed; status=real-error; category=missing-real-resource; fixability=not-cli-fixable-without-fixture",
"chat\u0000+messages-add-text-emotion": "write real test not passed; status=real-error; category=missing-real-resource; fixability=not-cli-fixable-without-fixture",
"chat\u0000+messages-batch-recall-by-bot": "write real test not passed; status=real-error; category=missing-real-resource; fixability=not-cli-fixable-without-fixture",
"chat\u0000+messages-batch-send-by-bot": "write real test not passed; status=real-error; category=missing-real-resource; fixability=not-cli-fixable-without-fixture",
"chat\u0000+messages-combine-forward": "write real test not passed; status=real-error; category=missing-real-resource; fixability=not-cli-fixable-without-fixture",
"chat\u0000+messages-create-text-emotion": "write real test not passed; status=real-error; category=input-or-business-validation; fixability=test-input-or-backend-rule",
"chat\u0000+messages-forward": "write real test not passed; status=real-error; category=missing-real-resource; fixability=not-cli-fixable-without-fixture",
"chat\u0000+messages-forward-topic": "write real test not passed; status=real-error; category=missing-real-resource; fixability=not-cli-fixable-without-fixture",
"chat\u0000+messages-recall": "write real test not passed; status=real-error; category=missing-real-resource; fixability=not-cli-fixable-without-fixture",
"chat\u0000+messages-recall-by-bot": "write real test not passed; status=real-error; category=input-or-business-validation; fixability=test-input-or-backend-rule",
"chat\u0000+messages-remove-emoji": "write real test not passed; status=real-error; category=missing-real-resource; fixability=not-cli-fixable-without-fixture",
"chat\u0000+messages-remove-text-emotion": "write real test not passed; status=real-error; category=missing-real-resource; fixability=not-cli-fixable-without-fixture",
"chat\u0000+messages-send-by-bot": "write real test not passed; status=real-error; category=missing-real-resource; fixability=not-cli-fixable-without-fixture",
"chat\u0000+messages-send-card": "write real test not passed; status=real-error; category=backend-or-mcp-error; fixability=not-cli-fixable-first",
"chat\u0000+messages-set-pin": "write real test not passed; status=real-error; category=backend-or-mcp-error; fixability=not-cli-fixable-first",
"chat\u0000+messages-set-top": "write real test not passed; status=real-error; category=missing-real-resource; fixability=not-cli-fixable-without-fixture",
"chat\u0000+messages-unset-pin": "write real test not passed; status=real-error; category=backend-or-mcp-error; fixability=not-cli-fixable-first",
"chat\u0000+messages-unset-top": "write real test not passed; status=real-error; category=missing-real-resource; fixability=not-cli-fixable-without-fixture",
"devapp\u0000+event-subscribe": "write real test not passed; status=real-error; category=auth-or-permission; fixability=not-cli-fixable",
"devapp\u0000+event-unsubscribe": "write real test not passed; status=real-error; category=auth-or-permission; fixability=not-cli-fixable",
"devapp\u0000+permission-add": "write real test not passed; status=real-error; category=auth-or-permission; fixability=not-cli-fixable",
"devapp\u0000+permission-remove": "write real test not passed; status=real-error; category=auth-or-permission; fixability=not-cli-fixable",
"devapp\u0000+robot-config": "write real test not passed; status=real-error; category=auth-or-permission; fixability=not-cli-fixable",
"devapp\u0000+robot-disable": "write real test not passed; status=real-error; category=auth-or-permission; fixability=not-cli-fixable",
"devapp\u0000+robot-enable": "write real test not passed; status=real-error; category=auth-or-permission; fixability=not-cli-fixable",
"devapp\u0000+security-config": "write real test not passed; status=real-error; category=auth-or-permission; fixability=not-cli-fixable",
"devapp\u0000+version-create": "write real test not passed; status=real-error; category=input-or-business-validation; fixability=test-input-or-backend-rule",
"devapp\u0000+version-publish": "write real test not passed; status=real-error; category=input-or-business-validation; fixability=test-input-or-backend-rule",
"ding\u0000+send-by-message": "write real test not passed; status=real-error; category=input-or-business-validation; fixability=test-input-or-backend-rule",
"doc\u0000+comment-create-inline": "write real test not passed; status=real-error; category=missing-real-resource; fixability=not-cli-fixable-without-fixture",
"doc\u0000+template-apply": "write real test not passed; status=real-error; category=auth-or-permission; fixability=not-cli-fixable",
"minutes\u0000+record-pause": "write real test not passed; status=real-error; category=input-or-business-validation; fixability=test-input-or-backend-rule",
"minutes\u0000+record-resume": "write real test not passed; status=real-error; category=input-or-business-validation; fixability=test-input-or-backend-rule",
"minutes\u0000+record-stop": "write real test not passed; status=real-error; category=input-or-business-validation; fixability=test-input-or-backend-rule",
"oa\u0000+approve-by": "write real test not passed; status=real-error; category=missing-real-resource; fixability=not-cli-fixable-without-fixture",
"wiki\u0000+node-copy": "write real test not passed; status=real-error; category=missing-real-resource; fixability=not-cli-fixable-without-fixture",
"wiki\u0000+node-move": "write real test not passed; status=real-error; category=missing-real-resource; fixability=not-cli-fixable-without-fixture",
"wiki\u0000+wiki-new-doc": "write real test not passed; status=real-error; category=missing-real-resource; fixability=not-cli-fixable-without-fixture",
}
+1 -1
View File
@@ -153,7 +153,7 @@ func TestCallMCPDataRejectsLikelyWriteUnderDryRun(t *testing.T) {
}
rt := &RuntimeContext{cmd: cmd, shortcut: Shortcut{Service: "chat"}}
_, err := rt.CallMCPData("chat", "send_personal_message", map[string]any{"receiverUserId": "u"})
_, err := rt.CallMCPData("chat", "send_personal_message", map[string]any{"receiverUid": "u"})
if err == nil {
t.Fatal("expected likely write guard error")
}
+10 -6
View File
@@ -24,8 +24,8 @@ import (
// Broadcast: send the SAME single-chat message to several people by NAME.
//
// Steps: split the --to name CSV → resolve each name to a unique userId →
// send an individual single-chat message to each resolved user. Names that
// Steps: split the --to name CSV → resolve each name to a unique user →
// send an individual single-chat message to each user's openDingTalkId. Names that
// fail to resolve (unknown / ambiguous) are collected and reported at the end
// without aborting delivery to the others. Replaces manually running
// `chat +dm` once per recipient.
@@ -37,7 +37,7 @@ var Broadcast = shortcut.Shortcut{
Product: "chat",
Description: "按姓名逐一给多个人群发同一条单聊消息(自动解析 userId、逐个发送)",
Intent: "当你想把同一条通知一次性单聊发给多位同事、但只知道他们的姓名不想逐个查 userId 时使用;" +
"内部把姓名列表逐个解析成唯一 userId 后,对每个人单独发一条单聊消息,并汇总成功/失败人数。" +
"内部把姓名列表逐个解析成唯一用户后,用 openDingTalkId 对每个人单独发一条单聊消息,并汇总成功/失败人数。" +
"某个姓名匹配不到人或匹配到多人时,会跳过该人并在结尾报出,不影响其他人收到消息。会真实发出多条消息。",
Risk: shortcut.RiskWrite,
Flags: []shortcut.Flag{
@@ -71,6 +71,10 @@ var Broadcast = shortcut.Shortcut{
failed = append(failed, fmt.Sprintf("%s(%s)", name, err.Error()))
continue
}
if user.openDingTalkID == "" {
failed = append(failed, fmt.Sprintf("%s(通讯录结果缺少 openDingTalkId)", name))
continue
}
// Step 2 — send the single-chat message to this recipient. Under
// --dry-run we still resolve names (a read) but never send: record the
@@ -80,9 +84,9 @@ var Broadcast = shortcut.Shortcut{
continue
}
if _, err := rt.CallMCPWriteData("chat", "send_personal_message", map[string]any{
"receiverUserId": user.userID,
"msgType": "markdown",
"content": string(content),
"receiverOpenDingTalkId": user.openDingTalkID,
"msgType": "markdown",
"content": string(content),
}); err != nil {
failed = append(failed, fmt.Sprintf("%s(发送失败:%s)", name, err.Error()))
continue
+5 -4
View File
@@ -25,7 +25,7 @@ import (
//
// Steps:
// 1. depending on whether --group or --user is given, call either
// list_conversation_message_v2 (group; param openconversation_id) or
// list_conversation_message_v2 (group; param openCid) or
// list_individual_chat_message (single chat; param userId) on the chat
// server — tool names and param keys copied verbatim from chat.go's
// `dws chat message list` call sites;
@@ -68,8 +68,8 @@ var ChatMessages = shortcut.Shortcut{
},
Execute: func(rt *shortcut.RuntimeContext) error {
// Step 1 — build params and pick the right tool. Param keys
// (openconversation_id / userId / time / forward / limit) are copied
// verbatim from chat.go's `dws chat message list` call sites.
// (openCid/cid / userId / time / forward / limit) match the MCP server
// schema for group and direct message listing.
var tool string
params := map[string]any{}
@@ -93,7 +93,8 @@ var ChatMessages = shortcut.Shortcut{
if rt.Str("group") != "" {
tool = "list_conversation_message_v2"
params["openconversation_id"] = rt.Str("group")
params["openCid"] = rt.Str("group")
params["cid"] = rt.Str("group")
} else {
tool = "list_individual_chat_message"
params["userId"] = rt.Str("user")
+11 -7
View File
@@ -20,14 +20,15 @@ package smart
import (
"encoding/json"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
)
// DM: message a person by NAME, no ID juggling.
//
// Steps: resolve name → single userId (disambiguate on multiple matches) → send
// a single-chat message. Replaces `contact +search-user` (copy userId) →
// `chat +messages-send --user <id>`.
// Steps: resolve name → single user (disambiguate on multiple matches) → send
// a single-chat message via openDingTalkId. Replaces `contact +search-user`
// (copy openDingTalkId) → `chat +messages-send --open-dingtalk-id <id>`.
//
// dws chat +dm --to 张三 --text "周报发我一下"
var DM = shortcut.Shortcut{
@@ -36,7 +37,7 @@ var DM = shortcut.Shortcut{
Product: "chat",
Description: "按姓名直接给某人发单聊消息(自动解析 userId)",
Intent: "当你只知道对方姓名、想直接发一条单聊消息而不想先查 userId 时使用;" +
"内部先按姓名搜通讯录解析出唯一 userId 再发送,姓名匹配到多人时会列出候选让你区分。会真实发出消息。",
"内部先按姓名搜通讯录解析出唯一用户,并用其 openDingTalkId 发送,姓名匹配到多人时会列出候选让你区分。会真实发出消息。",
Risk: shortcut.RiskWrite,
Flags: []shortcut.Flag{
{Name: "to", Type: shortcut.FlagString, Desc: "收件人姓名/花名", Required: true},
@@ -51,13 +52,16 @@ var DM = shortcut.Shortcut{
if err != nil {
return err
}
if user.openDingTalkID == "" {
return apperrors.NewValidation("通讯录结果缺少 openDingTalkId,无法发送单聊消息;请改用 chat +messages-send --open-dingtalk-id")
}
// Step 2 — send the single-chat message.
content, _ := json.Marshal(map[string]string{"title": text, "text": text})
return rt.CallMCP("send_personal_message", map[string]any{
"receiverUserId": user.userID,
"msgType": "markdown",
"content": string(content),
"receiverOpenDingTalkId": user.openDingTalkID,
"msgType": "markdown",
"content": string(content),
})
},
}
+14 -6
View File
@@ -22,8 +22,8 @@ import (
)
// Remind: create a personal todo for YOURSELF with an optional due/reminder time,
// in one command. No name resolution and no executorIds — a personal todo without
// executorIds defaults to the current user.
// in one command. It resolves the current user and explicitly passes executorIds;
// the real todo backend does not reliably default a missing executor to "me".
//
// Steps: take --task as the subject, and (optionally) parse --at into epoch
// milliseconds (mirroring the todo helper's parseISOTimeToMillis, which stores
@@ -37,7 +37,7 @@ var Remind = shortcut.Shortcut{
Product: "todo",
Description: "给自己创建一条带截止/提醒时间的待办",
Intent: "当你想给自己记一件事、并(可选)设一个截止/提醒时间,又不想先查自己的 userId 时使用;" +
"内部直接创建个人待办,不传 executorIds 时默认执行人就是你自己,--at 会按 ISO8601 解析为截止时间。会真实创建待办。",
"内部先解析当前登录用户的 userId,再显式设置 executorIds,--at 会按 ISO8601 解析为截止时间。会真实创建待办。",
Risk: shortcut.RiskWrite,
Flags: []shortcut.Flag{
{Name: "task", Type: shortcut.FlagString, Desc: "待办标题/内容", Required: true},
@@ -45,10 +45,18 @@ var Remind = shortcut.Shortcut{
},
Tips: []string{`dws todo +remind --task "交周报" --at 2026-03-10T18:00:00+08:00`},
Execute: func(rt *shortcut.RuntimeContext) error {
// subject is the only required field; a personal todo with no executorIds
// is created for the current user.
profile, err := rt.CallMCPData("contact", "get_current_user_profile", nil)
if err != nil {
return err
}
userID := myAttendanceCurrentUserID(profile)
if userID == "" {
return apperrors.NewValidation("无法解析当前登录用户的 userId,无法给自己创建待办")
}
vo := map[string]any{
"subject": rt.Str("task"),
"subject": rt.Str("task"),
"executorIds": []string{userID},
}
// Optional due/reminder time. The todo helper feeds --due through
+10 -5
View File
@@ -24,8 +24,9 @@ import (
// contactUser is the minimal identity a smart shortcut needs after resolving a
// name.
type contactUser struct {
userID string
name string
userID string
openDingTalkID string
name string
}
// resolveUser turns a human name into a single unique contact, the "name → ID"
@@ -53,8 +54,8 @@ func resolveUser(rt *shortcut.RuntimeContext, name string) (contactUser, error)
return users[0], nil
}
// extractUsers pulls {userId, name} out of a search_contact_by_key_word response
// ({"result": [ {userId, name, ...} ]}).
// extractUsers pulls {userId, openDingTalkId, name} out of a
// search_contact_by_key_word response ({"result": [ {userId, name, ...} ]}).
func extractUsers(data map[string]any) []contactUser {
raw, ok := data["result"].([]any)
if !ok {
@@ -71,7 +72,11 @@ func extractUsers(data map[string]any) []contactUser {
continue
}
nm, _ := m["name"].(string)
out = append(out, contactUser{userID: id, name: nm})
openID, _ := m["openDingTalkId"].(string)
if openID == "" {
openID, _ = m["openDingtalkId"].(string)
}
out = append(out, contactUser{userID: id, openDingTalkID: openID, name: nm})
}
return out
}
+13 -8
View File
@@ -17,15 +17,17 @@ import (
"encoding/json"
"strings"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
)
// ShareDoc: send a document link to a person by NAME, no ID juggling.
//
// Steps: resolve name → single userId (disambiguate on multiple matches) →
// build a Markdown message that links to the document → send it as a single-chat
// message. This is a pure "share a link" flow: it does not touch any doc tool,
// it just delivers the URL you already have straight to the recipient's inbox.
// Steps: resolve name → single user (disambiguate on multiple matches) →
// build a Markdown message that links to the document → send it to the user's
// openDingTalkId as a single-chat message. This is a pure "share a link" flow:
// it does not touch any doc tool, it just delivers the URL you already have
// straight to the recipient's inbox.
//
// dws doc +share-doc --to 张三 --url https://docs.dingtalk.com/xxx --note "帮忙过一下"
var ShareDoc = shortcut.Shortcut{
@@ -34,7 +36,7 @@ var ShareDoc = shortcut.Shortcut{
Product: "chat",
Description: "按姓名把文档链接私信发给某人(自动解析 userId)",
Intent: "当你手上已经有一个文档链接、想直接私信发给某个人而不必先查 userId 时使用;" +
"内部先按姓名搜通讯录解析出唯一 userId,再把链接拼成一条 Markdown 消息发出去," +
"内部先按姓名搜通讯录解析出唯一用户,再用 openDingTalkId 把链接拼成一条 Markdown 消息发出去," +
"姓名匹配到多人时会列出候选让你区分。只发链接、不读取或改动文档本身,会真实发出消息。",
Risk: shortcut.RiskWrite,
Flags: []shortcut.Flag{
@@ -52,6 +54,9 @@ var ShareDoc = shortcut.Shortcut{
if err != nil {
return err
}
if user.openDingTalkID == "" {
return apperrors.NewValidation("通讯录结果缺少 openDingTalkId,无法发送文档分享消息;请改用 chat +messages-send --open-dingtalk-id")
}
// Step 2 — build a Markdown message linking to the document.
text := shareDocBuildText(url, note)
@@ -60,9 +65,9 @@ var ShareDoc = shortcut.Shortcut{
// Step 3 — deliver it as a single-chat message.
return rt.CallMCP("send_personal_message", map[string]any{
"receiverUserId": user.userID,
"msgType": "markdown",
"content": string(content),
"receiverOpenDingTalkId": user.openDingTalkID,
"msgType": "markdown",
"content": string(content),
})
},
}
+22 -9
View File
@@ -113,21 +113,34 @@ func shortcutTodoMatch(cards []map[string]any, keyword string) []shortcutTodoCar
return out
}
// shortcutTodoCards pulls the todoCards list out of the response, probing the
// common container shape {"result": {"todoCards": [...]}} first, then a bare
// top-level todoCards as a fallback.
// shortcutTodoCards pulls the todoCards list out of the response, probing
// wrapper layers defensively. Real MCP responses may arrive as either
// {"result":{"todoCards":[...]}} or {"result":{"result":{"todoCards":[...]}}}
// depending on the call path, so do not assume a single fixed wrapper.
func shortcutTodoCards(data map[string]any) []map[string]any {
if r, ok := data["result"].(map[string]any); ok {
if arr, ok := r["todoCards"].([]any); ok {
return shortcutTodoToMaps(arr)
}
}
if arr, ok := data["todoCards"].([]any); ok {
if arr := shortcutTodoFindCards(data, 0); arr != nil {
return shortcutTodoToMaps(arr)
}
return nil
}
func shortcutTodoFindCards(m map[string]any, depth int) []any {
if depth > 4 {
return nil
}
if arr, ok := m["todoCards"].([]any); ok {
return arr
}
for _, k := range []string{"result", "data"} {
if child, ok := m[k].(map[string]any); ok {
if arr := shortcutTodoFindCards(child, depth+1); arr != nil {
return arr
}
}
}
return nil
}
func shortcutTodoToMaps(arr []any) []map[string]any {
out := make([]map[string]any, 0, len(arr))
for _, it := range arr {
+32
View File
@@ -0,0 +1,32 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0
package smart
import "testing"
func TestShortcutTodoCardsUnwrapsNestedResult(t *testing.T) {
data := map[string]any{
"result": map[string]any{
"result": map[string]any{
"todoCards": []any{
map[string]any{
"subject": "DWS shortcut 真实测试待办",
"taskId": "55155814691",
},
},
},
},
}
cards := shortcutTodoCards(data)
if len(cards) != 1 {
t.Fatalf("len(cards)=%d, want 1", len(cards))
}
if got := cards[0]["subject"]; got != "DWS shortcut 真实测试待办" {
t.Fatalf("subject=%v", got)
}
if got := cards[0]["taskId"]; got != "55155814691" {
t.Fatalf("taskId=%v", got)
}
}
+6 -4
View File
@@ -19,12 +19,14 @@ import (
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
)
// todoPageSize is the per-page fetch size for get_user_todos_in_current_org,
// matching helpers.todo list. todoMaxPages caps total pages so a runaway list
// can never loop unbounded (40 * 50 = 2000 todos, well past any realistic
// todoPageSize is the per-page fetch size for get_user_todos_in_current_org.
// The real backend currently returns an empty page for pageSize=50 while
// pageSize=20 works, so keep this aligned with the public +get-my-tasks default
// instead of pushing the maximum. todoMaxPages caps total pages so a runaway
// list can never loop unbounded (40 * 20 = 800 todos, well past any realistic
// backlog a single user filters/matches against).
const (
todoPageSize = 50
todoPageSize = 20
todoMaxPages = 40
)
+27 -7
View File
@@ -48,19 +48,26 @@ func newListCommand() *cobra.Command {
Short: "列出内建 shortcut",
RunE: func(cmd *cobra.Command, _ []string) error {
svc, _ := cmd.Flags().GetString("service")
includeHidden, _ := cmd.Flags().GetBool("include-hidden")
type row struct {
Service string `json:"service"`
Command string `json:"command"`
Product string `json:"product"`
Risk string `json:"risk"`
Description string `json:"description"`
Intent string `json:"intent,omitempty"`
Service string `json:"service"`
Command string `json:"command"`
Product string `json:"product"`
Risk string `json:"risk"`
Description string `json:"description"`
Intent string `json:"intent,omitempty"`
Hidden bool `json:"hidden,omitempty"`
HiddenReason string `json:"hiddenReason,omitempty"`
}
var rows []row
for _, s := range shortcut.All() {
if svc != "" && s.Service != svc {
continue
}
hiddenReason, hiddenByRelease := shortcut.ReleaseHiddenReason(s.Service, s.Command)
if s.Hidden && !includeHidden {
continue
}
product := s.Product
if product == "" {
product = s.Service
@@ -69,7 +76,19 @@ func newListCommand() *cobra.Command {
if risk == "" {
risk = string(shortcut.RiskRead)
}
rows = append(rows, row{s.Service, s.Command, product, risk, s.Description, s.Intent})
item := row{
Service: s.Service,
Command: s.Command,
Product: product,
Risk: risk,
Description: s.Description,
Intent: s.Intent,
Hidden: s.Hidden,
}
if hiddenByRelease {
item.HiddenReason = hiddenReason
}
rows = append(rows, item)
}
return output.WriteCommandPayload(cmd, map[string]any{
"count": len(rows),
@@ -78,6 +97,7 @@ func newListCommand() *cobra.Command {
},
}
cmd.Flags().String("service", "", "只列出指定服务的 shortcut")
cmd.Flags().Bool("include-hidden", false, "包含本期隐藏、仅内部复测用的 shortcut")
return cmd
}
+237
View File
@@ -0,0 +1,237 @@
#!/usr/bin/env python3
"""Generate a backend/MCP issue handoff report from real shortcut results."""
from __future__ import annotations
import json
import re
from collections import defaultdict
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[1]
READ_PATH = ROOT / "docs" / "shortcut-real-read-results.json"
WRITE_PATH = ROOT / "docs" / "shortcut-real-write-results.json"
OUT_PATH = ROOT / "docs" / "shortcut-backend-mcp-issues.md"
def load(path: Path) -> dict[str, Any]:
return json.loads(path.read_text(encoding="utf-8"))
def squish(text: str, limit: int = 300) -> str:
text = re.sub(r"\s+", " ", text or "").strip()
if len(text) <= limit:
return text
return text[: limit - 1] + "…"
def parse_payload(row: dict[str, Any]) -> dict[str, Any]:
for key in ("stderr", "stdout"):
raw = (row.get(key) or "").strip()
if not raw:
continue
try:
value = json.loads(raw)
except json.JSONDecodeError:
continue
if isinstance(value, dict):
err = value.get("error")
if isinstance(err, dict):
return err
return value
return {}
def message(row: dict[str, Any]) -> str:
payload = parse_payload(row)
msg = payload.get("message")
if isinstance(msg, str) and msg:
return msg
return (row.get("stderr") or row.get("stdout") or "").strip()
def operation(row: dict[str, Any]) -> str:
payload = parse_payload(row)
op = payload.get("operation")
if isinstance(op, str) and op:
return op
msg = message(row)
match = re.search(r"\(operation:\s*([^)]+)\)", msg)
return match.group(1) if match else "-"
def trace_id(row: dict[str, Any]) -> str:
payload = parse_payload(row)
value = payload.get("trace_id") or payload.get("traceId")
if value:
return str(value)
msg = message(row)
match = re.search(r'"trace_id":"([^"]+)"', msg)
return match.group(1) if match else "-"
def issue_key(row: dict[str, Any]) -> str:
service = row.get("service")
command = row.get("command")
msg = message(row).lower()
op = operation(row)
if service == "chat" and "receiveruid" in msg:
return "chat-receiveruid-mapping"
if service == "chat" and "applicantuid" in msg:
return "chat-applicantuid-mapping"
if service == "chat" and ("opencid" in msg or "openconversationid" in msg):
return "chat-conversation-id-mapping"
if service == "aitable" and "success\":true" in msg and "error" in msg:
if "roleid is required" in msg:
return "aitable-role-id-mapping"
if "getdentrydto returns null" in msg:
return "aitable-notfound-as-system-error"
if "workflow" in msg or "workflow" in op:
return "aitable-workflow-system-error"
if command in {"+base-get-primary-doc-id", "+record-primary-doc-get"}:
return "aitable-primary-doc-system-error"
return "aitable-success-true-error-envelope"
return f"{service}-other-backend-mcp"
ISSUE_META = {
"chat-conversation-id-mapping": {
"title": "Chat/IM 会话 ID 字段在 MCP/后端映射中疑似丢失",
"owner": "IM MCP / IM 后端字段映射",
"priority": "P1",
"summary": "CLI 已传 group/conversation-id/open-conversation-id(部分 case 使用真实 cid),后端仍报 openCid/openConversationId/cid required。",
"expect": "MCP schema/网关应接受并透传 openConversationId/openCid/cid 中的兼容字段;如果资源无效,应返回“无效会话”,而不是 required。",
},
"chat-receiveruid-mapping": {
"title": "Chat card 发送 receiverUid 疑似未从 receiver 透传",
"owner": "IM MCP / card 发送参数映射",
"priority": "P1",
"summary": "CLI 传入 receiver=103262,后端仍报 receiverUid 和 openConversationId 不能同时为空。",
"expect": "receiver 应映射为 receiverUid,或 schema 明确要求 receiverUid;真实入参不应在 MCP 层丢失。",
},
"chat-applicantuid-mapping": {
"title": "Chat 入群审批 applicantUid/inviterUid 疑似未透传",
"owner": "IM MCP / 入群审批参数映射",
"priority": "P1",
"summary": "CLI 传入 applicant=103262、inviter=519019,后端仍报 applicantUid required。",
"expect": "applicant/inviter 应映射为 applicantUid/inviterUid;如果 recordId/group 无效,应返回对应资源错误而不是 applicantUid 缺失。",
},
"aitable-success-true-error-envelope": {
"title": "AI 表格 MCP 错误 envelope 语义不一致:success=true 但 error 非空/status=error",
"owner": "AI 表格 MCP wrapper",
"priority": "P1",
"summary": "多条 AI 表格命令返回 MCP_TOOL_ERROR,内部 JSON 同时出现 success=true、status=error、error 非空。",
"expect": "只要 error 非空或 status=error,success 应为 false,外层也应按业务错误返回稳定错误码/trace。",
},
"aitable-role-id-mapping": {
"title": "AI 表格 roleId 参数疑似未被 MCP 正确读取",
"owner": "AI 表格 MCP role 接口",
"priority": "P1",
"summary": "CLI 已传 --role-id,但 MCP 返回 roleId is required。",
"expect": "role-id/roleId 字段应被正确映射;如果 role 不存在,返回 ROLE_NOT_FOUND,而不是 required。",
},
"aitable-notfound-as-system-error": {
"title": "AI 表格无效 Base/Table/Field/Record 被包装成 SYSTEM_ERROR",
"owner": "AI 表格 MCP wrapper / 后端错误码",
"priority": "P2",
"summary": "安全负向 ID 下,部分写接口返回 getDentryDTO returns null、type=SYSTEM_ERROR、retryable=true。",
"expect": "资源不存在应返回 INPUT_ERROR/NOT_FOUND 且 retryable=false,避免误导调用方重试。",
},
"aitable-workflow-system-error": {
"title": "AI 表格 Workflow 查询在真实 Base 下返回系统级错误",
"owner": "AI 表格 Workflow MCP / 后端",
"priority": "P1",
"summary": "使用真实可访问 Base 查询 workflow list/get,返回 LIST_WORKFLOWS_ERROR/GET_WORKFLOW_ERROR。",
"expect": "无 workflow 时应返回空列表或 WORKFLOW_NOT_FOUND;有后端异常时需提供稳定错误码和可排查 trace。",
},
"aitable-primary-doc-system-error": {
"title": "AI 表格记录主文档查询在真实 record 下返回 no record/SYSTEM_ERROR",
"owner": "AI 表格 primary doc MCP / 后端",
"priority": "P2",
"summary": "record-query 已能查到真实 recordId,但 primary-doc 查询返回 no record、type=SYSTEM_ERROR。",
"expect": "若该记录无主文档,应返回空/未创建;若 recordId 语义不匹配,应返回明确参数错误,不应是系统错误。",
},
}
def collect() -> list[dict[str, Any]]:
rows: list[dict[str, Any]] = []
for suite, path in (("read", READ_PATH), ("write", WRITE_PATH)):
data = load(path)
for row in data.get("results", []):
if row.get("failure_category") == "backend-or-mcp-error":
item = dict(row)
item["suite"] = suite
rows.append(item)
return rows
def render_case(row: dict[str, Any]) -> str:
return (
f"| `{row['suite']}` | `{row.get('service')} {row.get('command')}` | "
f"`{operation(row)}` | `{trace_id(row)}` | {squish(message(row), 180)} |\n"
f"| | input | | | `{row.get('input') or ''}` |\n"
)
def main() -> int:
rows = collect()
grouped: dict[str, list[dict[str, Any]]] = defaultdict(list)
for row in rows:
grouped[issue_key(row)].append(row)
lines: list[str] = []
lines.append("# Shortcut 真实测试:后端 / MCP 问题整理\n")
lines.append("这份报告只汇总 `failure_category = backend-or-mcp-error` 的 case,已尽量排除权限、缺真实资源、当前账号无数据等噪音。\n")
lines.append("## 总览\n")
lines.append(f"- Backend/MCP case 总数:{len(rows)}")
lines.append(f"- 聚合问题数:{len(grouped)}")
lines.append("- 复现口径:真实 dws CLI;无 mock;无 dry-run;命令输入和 trace_id 均来自真实测试结果。\n")
lines.append("## 建议优先看\n")
priority_order = [
"chat-conversation-id-mapping",
"chat-receiveruid-mapping",
"chat-applicantuid-mapping",
"aitable-success-true-error-envelope",
"aitable-workflow-system-error",
"aitable-role-id-mapping",
"aitable-primary-doc-system-error",
"aitable-notfound-as-system-error",
]
for idx, key in enumerate([k for k in priority_order if k in grouped], 1):
meta = ISSUE_META.get(key, {})
lines.append(f"{idx}. [{meta.get('priority', 'P2')}] {meta.get('title', key)}({len(grouped[key])} case)")
lines.append("")
for key in [k for k in priority_order if k in grouped] + [k for k in grouped if k not in priority_order]:
cases = grouped[key]
meta = ISSUE_META.get(key, {
"title": key,
"owner": "待确认",
"priority": "P2",
"summary": "未归入已知聚合类型,需要按 case 单独排查。",
"expect": "请根据 operation/trace_id 判断是 schema、字段映射还是后端错误语义问题。",
})
lines.append(f"## {meta['title']}\n")
lines.append(f"- 优先级:{meta['priority']}")
lines.append(f"- 建议 owner:{meta['owner']}")
lines.append(f"- 现象:{meta['summary']}")
lines.append(f"- 期望:{meta['expect']}")
lines.append(f"- 涉及 case:{len(cases)}\n")
lines.append("| 套件 | shortcut | operation | trace_id | 证据 |")
lines.append("|---|---|---|---|---|")
for row in cases:
lines.append(render_case(row).rstrip())
lines.append("")
OUT_PATH.write_text("\n".join(lines), encoding="utf-8")
print(f"written: {OUT_PATH}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+362 -11
View File
@@ -3,7 +3,7 @@
# shortcut — the dws `+command`, the equivalent lark-cli command, and the raw
# dws native `dws mcp ...` combination it replaces. Data is extracted directly
# from source so it stays truthful.
import os, re, glob, html, json
import os, re, glob, html, json, subprocess
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SC_DIR = os.path.join(ROOT, "internal", "shortcut")
@@ -26,12 +26,26 @@ LARK_MAP = {
"aisearch": None, "live": None, "devdoc": None,
}
# lark-cli services that are intentionally not mapped to DWS shortcuts today.
# These are platform/product-model gaps, not just missing wrapper work.
LARK_PLATFORM_ONLY = {
"okr": "飞书 OKR 是独立目标管理产品;当前 DWS / 钉钉 MCP 没有等价的 OKR 周期、目标、指标和进展对象,不能只靠 wrapper 复刻。",
"vc": "飞书 VC 暴露会议加入/离开、会中事件、消息、录制等对象;当前 DWS 没有一套干净的钉钉会议 MCP tool 可等价承接,已有事件能力也不是同一套会议生命周期语义。",
"slides": "飞书 Slides 是原生演示文稿对象,支持页面替换、截图、XML 和媒体上传;当前 DWS 没有钉钉幻灯片对象模型与页面级 MCP 能力。",
"markdown": "lark-cli 的 markdown shortcut 绑定飞书云文档/云盘里的 Markdown 工作流;当前 DWS 没有等价的钉钉 Markdown 云文档资源与 patch/diff/overwrite API。",
"whiteboard": "飞书 Whiteboard 有独立画板节点、查询和结构化更新能力;当前 DWS 没有钉钉画板节点 schema 或对应 MCP tool。",
"note": "飞书 Note 是会议纪要直查对象,可按 note_id 取详情和逐字稿;DWS 的 minutes 能力属于钉钉妙记/听记,资源 ID、权限和数据模型不同,不能保证等价。",
"event": "飞书 event shortcut 面向 lark-cli 的事件订阅模型;DWS 的钉钉事件/机器人连接模型不同,目前没有沉淀成同参数、同语义的 shortcut。",
}
def load_lark():
out = {}
for svc in set(v for v in LARK_MAP.values() if v):
services = set(v for v in LARK_MAP.values() if v) | set(LARK_PLATFORM_ONLY)
for svc in services:
cmds = set()
for f in glob.glob(os.path.join(LARK, svc, "*.go")):
if f.endswith("_test.go"):
base = os.path.basename(f)
if f.endswith("_test.go") or base.startswith("register"):
continue
try:
txt = open(f, encoding="utf-8").read()
@@ -94,7 +108,8 @@ def parse_block(block, pkg, consts=None):
if k not in seen:
seen.add(k); keys.append(k)
return dict(service=svc, command=cmd, product=product, desc=desc, risk=risk,
tool=tool, flags=flags, keys=keys)
tool=tool, flags=flags, keys=keys,
layer="smart" if pkg == "smart" else "atomic")
def collect():
items = []
@@ -120,6 +135,9 @@ def shortcut_cmd(it):
return s
def raw_cmd(it):
if it["layer"] == "smart":
primary = f";入口工具 {it['tool']}" if it["tool"] else ""
return f"复合编排:多次 MCP 调用 / 分页、消歧、聚合或回滚{primary}"
if not it["tool"]:
return f"dws mcp {it['product']} <tool>"
if not it["keys"]:
@@ -140,7 +158,187 @@ def lark_cmd(it, lark):
RISK_CLS = {"read": "rk-r", "write": "rk-w", "high-risk-write": "rk-h"}
def gen_html(items, lark):
def load_test_matrix():
env = os.environ.copy()
env.setdefault("GOCACHE", "/private/tmp/dws_gocache")
cmd = ["go", "run", "./scripts/gen_shortcut_test_matrix.go"]
raw = subprocess.check_output(cmd, cwd=ROOT, env=env, text=True)
data = json.loads(raw)
by_cmd = {
(r["service"], r["command"]): r
for r in data.get("results", [])
}
return data, by_cmd
def load_real_matrix(filename):
path = os.path.join(ROOT, "docs", filename)
if not os.path.exists(path):
return None, {}
with open(path, encoding="utf-8") as f:
data = json.load(f)
by_cmd = {
(r["service"], r["command"]): r
for r in data.get("results", [])
}
return data, by_cmd
def load_real_read_matrix():
return load_real_matrix("shortcut-real-read-results.json")
def load_real_write_matrix():
return load_real_matrix("shortcut-real-write-results.json")
def matrix_summary(data):
if not data:
return {}
if isinstance(data.get("summary"), dict):
return data["summary"]
return data
FAILURE_CATEGORY_LABELS = {
"auth-or-permission": "权限 / 鉴权不足",
"backend-business-rule": "后端业务规则限制",
"backend-or-mcp-error": "后端 / MCP 内部错误",
"cli-error-envelope": "CLI 错误 envelope 未转非零退出",
"held": "人工授权后执行",
"input-or-business-validation": "输入或业务校验失败",
"missing-real-aitable-fixture": "缺 AI 表格真实 fixture",
"missing-real-minutes-fixture": "缺妙记/听记真实 fixture",
"missing-real-resource": "缺真实资源 fixture",
"timeout": "真实后端超时",
"unclassified-real-error": "未分类真实错误",
}
FIXABILITY_LABELS = {
"cli-wrapper-fix-needed": "需要 CLI / helper 修",
"fixed": "已成功",
"manual-approval": "需人工授权",
"needs-rerun": "需单独复测",
"needs-triage": "需继续排查",
"not-cli-fixable": "非 CLI 可修",
"not-cli-fixable-first": "优先后端 / MCP 排查",
"not-cli-fixable-without-fixture": "需真实 fixture,非 CLI 可直接修",
"test-input-or-backend-rule": "需区分测试输入或后端规则",
}
def failure_category_table(data):
cats = (data or {}).get("failure_categories") or {}
if not cats:
return '<p class="section-lead">暂无失败归因。</p>'
rows = ['<table class="why"><colgroup><col style="width:34%"><col style="width:16%"><col style="width:50%"></colgroup>',
'<thead><tr><th>失败归因</th><th>数量</th><th>说明</th></tr></thead><tbody>']
notes = {
"auth-or-permission": "账号、应用 scope 或资源权限不足;需要授权/换账号/配置应用权限。",
"backend-business-rule": "后端业务规则限制,CLI 只能避免误导或提示用户换场景。",
"backend-or-mcp-error": "服务端/MCP 返回内部错误,报告保留原始输出供后端排查。",
"cli-error-envelope": "这是 CLI 层可继续修的类型:业务错误不应 exit 0。",
"held": "无安全负向目标或高风险动作,需人工逐项确认。",
"input-or-business-validation": "命令真实进入校验;可能是安全负向输入触发,也可能需要更完整 fixture。",
"missing-real-aitable-fixture": "AI 表格命令依赖真实 Base/Table/View/Record 等资源。",
"missing-real-minutes-fixture": "妙记/听记命令依赖当前账号可见的真实会议产物。",
"missing-real-resource": "资源、消息、群、文档、单据等真实对象不存在。",
"timeout": "需要单独扩大超时或排查网络/后端响应。",
"unclassified-real-error": "暂未自动归因,需要人工继续看 stdout/stderr。",
}
for key, count in cats.items():
rows.append(f'<tr><td>{html.escape(FAILURE_CATEGORY_LABELS.get(key, key))}</td><td>{count}</td><td>{html.escape(notes.get(key, ""))}</td></tr>')
rows.append('</tbody></table>')
return "\n".join(rows)
TEST_CLS = {
"assembled": "test-ok",
"validation-blocked": "test-warn",
"failed": "test-bad",
}
def real_html(it, real_tests):
r = real_tests.get((it["service"], it["command"]))
if r:
cls = "test-ok" if r.get("status") == "real-ok" else ("test-bad" if r.get("status") == "timeout" else "test-warn")
label = {"real-ok": "真实后端成功", "real-error": "真实后端返回错误", "timeout": "真实后端超时", "held": "待真实执行"}.get(r.get("status"), r.get("status", "unknown"))
stdout = r.get("stdout") or ""
stderr = r.get("stderr") or ""
output = stdout or stderr or "(空)"
method = r.get("method") or "real-backend; no --mock; no --dry-run"
category = r.get("failure_category") or ""
fixability = r.get("fixability") or ""
diagnosis = r.get("diagnosis") or ""
setup = r.get("setup") if isinstance(r.get("setup"), dict) else None
setup_html = ""
if setup:
setup_html = (
f'<div class="io-label">前置真实资源准备</div>'
f'<pre>{html.escape(setup.get("purpose") or "—")}\n'
f'input: {html.escape(setup.get("input") or "")}\n'
f'exit={html.escape(str(setup.get("exit_code")))};duration={html.escape(str(setup.get("duration_ms")))}ms\n'
f'stdout:\n{html.escape(setup.get("stdout") or "(空)")}\n'
f'stderr:\n{html.escape(setup.get("stderr") or "(空)")}</pre>'
)
diagnosis_html = ""
if category or fixability or diagnosis:
diagnosis_html = (
f'<div class="io-label">失败归因 / 可修复性</div>'
f'<pre>{html.escape(FAILURE_CATEGORY_LABELS.get(category, category) or "—")} / '
f'{html.escape(FIXABILITY_LABELS.get(fixability, fixability) or "—")}\n'
f'{html.escape(diagnosis or "—")}</pre>'
)
return (
'<details class="io"><summary>真实后端测试输入 / 输出</summary>'
f'<span class="{cls}">{html.escape(label)}</span>'
f'<div class="desc">exit={r.get("exit_code")};duration={r.get("duration_ms")}ms;{html.escape(method)}</div>'
f'{diagnosis_html}'
f'{setup_html}'
f'<div class="io-label">真实输入</div><pre>{html.escape(r.get("input", ""))}</pre>'
f'<div class="io-label">真实 stdout</div><pre>{html.escape(stdout or "(空)")}</pre>'
f'<div class="io-label">真实 stderr / error</div><pre>{html.escape(stderr or "(空)")}</pre>'
f'<div class="io-label">真实返回</div><pre>{html.escape(output)}</pre>'
'</details>'
)
if it["risk"] == "read":
return '<details class="io"><summary>真实后端测试输入 / 输出</summary><span class="test-bad">未覆盖</span><div class="desc">此 read shortcut 未出现在真实只读测试结果中。</div></details>'
return (
'<details class="io"><summary>真实后端测试输入 / 输出</summary>'
'<span class="test-warn">待分批真实执行写入</span>'
'<div class="desc">该命令是 write / high-risk-write。真实执行会修改线上数据;本轮采用临时测试资源和测试人员分批推进,未覆盖到的命令会继续补测。</div>'
'</details>'
)
def test_html(it, tests, real_tests):
r = tests.get((it["service"], it["command"]))
if not r:
return '<span class="test-bad">未测试</span><div class="desc">测试矩阵没有覆盖到此命令。</div>'
cls = TEST_CLS.get(r.get("status"), "test-bad")
status_label = {
"assembled": "已实测装配",
"validation-blocked": "实测校验拦截",
"failed": "失败",
}.get(r.get("status"), r.get("status", "unknown"))
tools = r.get("tools") or []
tool_txt = "、".join(tools) if tools else "无 MCP 调用(校验前拦截)"
verified = "tool 已校验" if r.get("tool_verified") else "tool 未通过校验"
method = "cobra 实际执行 + fake MCP 拦截,零网络/零副作用"
err = r.get("error", "")
err_html = f'<span class="test-err">;{html.escape(err)}</span>' if err else ""
stdout = r.get("stdout") or ""
stderr = r.get("stderr") or ""
output = stdout or stderr or err or "(无 stdout/stderr;命令在校验前停止或无内容输出)"
io_block = (
'<details class="io"><summary>输入 / CLI 输出</summary>'
f'<div class="io-label">输入</div><pre>{html.escape(r.get("input", ""))}</pre>'
f'<div class="io-label">stdout</div><pre>{html.escape(stdout or "(空)")}</pre>'
f'<div class="io-label">stderr / error</div><pre>{html.escape((stderr or err) or "(空)")}</pre>'
f'<div class="io-label">最终用于判定的返回</div><pre>{html.escape(output)}</pre>'
'</details>'
)
return (
f'<span class="{cls}">{html.escape(status_label)}</span>'
f'<div class="desc">{html.escape(method)};调用数 {r.get("call_count", 0)};'
f'{html.escape(verified)};<code>{html.escape(tool_txt)}</code>{err_html}</div>'
f'{io_block}'
f'{real_html(it, real_tests)}'
)
def gen_html(items, lark, matrix, tests, real_read_matrix, real_write_matrix, real_tests):
by_svc = {}
for it in items:
by_svc.setdefault(it["service"], []).append(it)
@@ -148,6 +346,33 @@ def gen_html(items, lark):
hit = sum(1 for it in items if lark_cmd(it, lark)[0] == "hit")
dss = sum(1 for it in items if LARK_MAP.get(it["service"]) is None)
smart = [it for it in items if it["layer"] == "smart"]
atomic_count = len(items) - len(smart)
smart_by_svc = {}
for it in smart:
smart_by_svc.setdefault(it["service"], []).append(it)
lark_platform_only = {
svc: sorted(lark.get(svc, set()))
for svc in sorted(LARK_PLATFORM_ONLY)
if lark.get(svc)
}
lark_platform_only_count = sum(len(cmds) for cmds in lark_platform_only.values())
test_total = matrix.get("total", 0)
test_assembled = matrix.get("assembled", 0)
test_validation = matrix.get("validation_blocked", 0)
test_failed = matrix.get("failed", 0)
test_tool_bad = matrix.get("tool_verification_bad", 0)
real_read_summary = matrix_summary(real_read_matrix)
real_write_summary = matrix_summary(real_write_matrix)
real_total = real_read_summary.get("total", 0)
real_ok = real_read_summary.get("ok", 0)
real_error = real_read_summary.get("error", 0)
real_timeout = real_read_summary.get("timeout", 0)
real_write_total = real_write_summary.get("total", 0)
real_write_ok = real_write_summary.get("ok", 0)
real_write_error = real_write_summary.get("error", 0)
real_write_timeout = real_write_summary.get("timeout", 0)
real_write_held = real_write_summary.get("held", 0)
P = []
P.append(f'''<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8">
@@ -165,6 +390,15 @@ h1{{margin:0 0 6px;font-size:25px}} .sub{{color:var(--muted);font-size:13px}}
.note{{background:var(--card);border:1px solid var(--line);border-left:3px solid var(--accent);border-radius:8px;padding:12px 16px;margin:22px 0;color:var(--muted);font-size:13px}}
.note b{{color:var(--ink)}}
h2{{font-size:18px;margin:34px 0 4px;padding-top:10px}} h2 .c{{color:var(--muted);font-size:14px;font-weight:400}}
.section-lead{{color:var(--muted);max-width:980px;margin:7px 0 14px}}
.card-grid{{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:12px;margin:12px 0 24px}}
.card{{background:var(--card);border:1px solid var(--line);border-radius:10px;padding:13px 15px}}
.card h3{{margin:0 0 8px;font-size:14px;color:var(--accent)}}
.smart-list{{list-style:none;padding:0;margin:0;display:grid;gap:7px}}
.smart-list li{{border-top:1px solid var(--line);padding-top:7px}}
.smart-list li:first-child{{border-top:0;padding-top:0}}
.smart-list code{{color:#d3a7ff}} .smart-desc{{display:block;color:var(--muted);font-size:11.5px;margin-top:1px}}
.why td:first-child{{color:#9aa8bb}} .why td:nth-child(2){{color:#dce7f7}}
table{{width:100%;border-collapse:collapse;margin:10px 0 6px;background:var(--card);border:1px solid var(--line);border-radius:10px;overflow:hidden;table-layout:fixed}}
th,td{{padding:9px 11px;text-align:left;border-bottom:1px solid var(--line);vertical-align:top;word-break:break-all}}
th{{background:#1b2536;color:var(--muted);font-size:12px;font-weight:600}}
@@ -175,6 +409,13 @@ code{{font-family:"SF Mono",Menlo,Consolas,monospace;font-size:12.5px}}
.desc{{color:var(--muted);font-size:11.5px;margin-top:3px}}
.rk{{display:inline-block;font-size:10px;padding:0 6px;border-radius:10px;margin-left:6px;vertical-align:1px}}
.rk-r{{background:#12321d;color:#66d38a}} .rk-w{{background:#3a2f10;color:#e2b23c}} .rk-h{{background:#3a1414;color:#f27272}}
.smart-badge{{display:inline-block;font-size:10px;padding:0 6px;border-radius:10px;margin-left:6px;vertical-align:1px;background:#35204d;color:#d3a7ff}}
.test-ok,.test-warn,.test-bad{{display:inline-block;font-size:10px;padding:0 6px;border-radius:10px;vertical-align:1px}}
.test-ok{{background:#12321d;color:#66d38a}} .test-warn{{background:#3a2f10;color:#e2b23c}} .test-bad{{background:#3a1414;color:#f27272}}
.test-err{{color:#d5a429}}
details.io{{margin-top:6px}} details.io summary{{cursor:pointer;color:#8fd3ff;font-size:11.5px}}
.io-label{{color:#9aa8bb;font-size:11px;margin-top:6px}}
details.io pre{{white-space:pre-wrap;word-break:break-word;background:#101827;border:1px solid var(--line);border-radius:6px;padding:7px;margin:3px 0 6px;color:#c7cfdb;max-height:260px;overflow:auto;font-size:11px}}
.lk-none{{color:#6b7890}} .lk-miss{{color:var(--yellow)}} .lk-hit{{color:var(--green)}}
footer{{color:var(--muted);text-align:center;font-size:12px;margin-top:36px}}
</style></head><body>
@@ -182,31 +423,126 @@ footer{{color:var(--muted);text-align:center;font-size:12px;margin-top:36px}}
<div class="sub">每个 shortcut:<b style="color:#8fd3ff">dws +命令</b> vs <b style="color:#84e39a">lark-cli</b> vs <b style="color:#c7cfdb">dws 原生 MCP 组合</b> · 源码自动提取</div>
<div class="stats">
<div class="stat"><div class="n">{len(items)}</div><div class="l">shortcut 总数</div></div>
<div class="stat"><div class="n">{len(smart)}</div><div class="l">复合 / 智能型</div></div>
<div class="stat"><div class="n">{atomic_count}</div><div class="l">原子型</div></div>
<div class="stat"><div class="n">{len(order)}</div><div class="l">服务</div></div>
<div class="stat"><div class="n">{hit}</div><div class="l">lark 同名可对齐</div></div>
<div class="stat"><div class="n">{dss}</div><div class="l">钉钉特有(无 lark)</div></div>
<div class="stat"><div class="n">{lark_platform_only_count}</div><div class="l">lark 平台特有</div></div>
<div class="stat"><div class="n">{test_total}</div><div class="l">逐条实测覆盖</div></div>
<div class="stat"><div class="n">{real_total}</div><div class="l">真实只读后端测试</div></div>
<div class="stat"><div class="n">{real_write_total}</div><div class="l">真实写入后端测试</div></div>
</div></header>
<div class="wrap">
<div class="note"><b>读表说明</b>:① <b style="color:#8fd3ff">dws shortcut</b> = 新增的精选命令(<code>&lt;xxx&gt;</code> 为参数占位)。
② <b style="color:#c7cfdb">dws 原生 MCP 组合</b> = 该 shortcut 底层等价的裸命令,需手拼 JSON —— 直观体现 shortcut 的收敛价值。
③ <b style="color:#84e39a">lark-cli</b> = 飞书对应命令:<span class="lk-hit">绿色</span>=同名可对齐;<span class="lk-miss">黄色</span>=lark 有该服务但命名/能力不同;<span class="lk-none">灰色</span>=钉钉特有服务,lark 无对应。因两边 API 不同,仅同名项做逐一对齐。</div>
② <b style="color:#c7cfdb">原生 MCP / 编排说明</b>:原子型展示等价裸命令;复合型说明其分页、消歧、多步调用、聚合或回滚职责。
③ <b style="color:#84e39a">lark-cli</b> = 飞书对应命令:<span class="lk-hit">绿色</span>=同名可对齐;<span class="lk-miss">黄色</span>=lark 有该服务但命名/能力不同;<span class="lk-none">灰色</span>=钉钉特有服务,lark 无对应。因两边 API 不同,仅同名项做逐一对齐。
④ <span class="smart-badge">复合/智能型</span> = 不只是参数改名,内部包含多次调用、跨服务 ID 解析、分页、消歧、聚合、投影、批处理或失败回滚。
⑤ <b>lark 平台特有</b> = 飞书有独立产品对象或事件模型,但当前 DWS / 钉钉 MCP 没有等价资源,不能通过简单封装实现。
⑥ <b>实际测试情况</b> = 每条 dws shortcut 都通过真实 cobra 命令树执行;fake MCP 拦截所有后端调用,因此不发网络、不写线上数据。绿色=已装配到 MCP 调用;黄色=synthetic 参数被命令自身校验正确拦截;红色=失败。每行都可展开查看本次测试的 CLI 输入、stdout、stderr/error。
⑦ <b>真实后端测试</b> = 用真实 dws 二进制、真实登录态、真实后端执行(无 --mock、无 --dry-run),逐条记录真实输入与真实 stdout/stderr。read 已全量覆盖;write / high-risk-write 按临时测试资源和测试人员分批推进,未覆盖项会继续补测。
⑧ <b>逐项失败 review</b> = <a href="./shortcut-error-review.html" style="color:#8fd3ff">shortcut-error-review.html</a> 已把每个真实失败逐条标出“要改哪里 / 具体改法 / 验证方式”。</div>
''')
P.append(f'<h2 id="shortcut-test-matrix">逐条实际测试矩阵 <span class="c">· {test_total} / {len(items)} 覆盖</span></h2>')
P.append(f'<p class="section-lead">本页每一条 dws shortcut 都有测试记录。测试由 <code>scripts/gen_shortcut_test_matrix.go</code> 生成:逐条喂入合成参数,实际执行 cobra 命令树,所有 MCP 调用由 fake caller 拦截并记录 product/tool/args,同时捕获每条命令的 CLI 输入、stdout、stderr/error,确保零网络、零副作用。结果:{test_assembled} 条已装配到真实 MCP tool,{test_validation} 条被自身校验按预期拦截,失败 {test_failed} 条,tool ground truth 校验失败 {test_tool_bad} 条。</p>')
P.append('''<table class="why"><colgroup><col style="width:24%"><col style="width:25%"><col style="width:25%"><col style="width:26%"></colgroup>
<thead><tr><th>测试类型</th><th>数量</th><th>含义</th><th>副作用边界</th></tr></thead><tbody>''')
P.append(f'<tr><td><span class="test-ok">已实测装配</span></td><td>{test_assembled}</td><td>命令实际执行到 MCP 调用组装路径,并记录 product/tool/args</td><td>fake MCP 拦截,不访问后端,不写数据</td></tr>')
P.append(f'<tr><td><span class="test-warn">实测校验拦截</span></td><td>{test_validation}</td><td>命令实际执行到自身校验链路,synthetic 参数被合法拒绝</td><td>校验前停止,无 MCP 调用</td></tr>')
P.append(f'<tr><td><span class="test-bad">失败</span></td><td>{test_failed}</td><td>panic、无调用无错误、或 tool ground truth 未通过</td><td>必须为 0 才允许生成报告</td></tr>')
P.append('</tbody></table>')
P.append(f'<h2 id="real-read-test-matrix">真实后端只读测试 <span class="c">· {real_total} / 204 read 覆盖</span></h2>')
P.append(f'<p class="section-lead">这一层不使用 mock,也不使用 dry-run:用临时构建的真实 dws 二进制和当前 profile 登录态执行 read shortcut,并捕获真实 stdout/stderr。结果:成功 {real_ok} 条,真实后端/参数/鉴权错误 {real_error} 条,超时 {real_timeout} 条。大量错误并不代表命令未执行,而是后端真实返回了 not_authenticated、参数校验、资源不存在或权限/业务错误;这些原始输出已逐条写在主表展开项中。</p>')
P.append('''<table class="why"><colgroup><col style="width:24%"><col style="width:25%"><col style="width:25%"><col style="width:26%"></colgroup>
<thead><tr><th>真实测试类型</th><th>数量</th><th>含义</th><th>副作用边界</th></tr></thead><tbody>''')
P.append(f'<tr><td><span class="test-ok">真实后端成功</span></td><td>{real_ok}</td><td>命令真实访问后端并 exit 0</td><td>仅 read 命令</td></tr>')
P.append(f'<tr><td><span class="test-warn">真实后端返回错误</span></td><td>{real_error}</td><td>命令真实访问后端或真实执行本地校验后返回错误;stdout/stderr 已逐条记录</td><td>仅 read 命令</td></tr>')
P.append(f'<tr><td><span class="test-bad">真实后端超时</span></td><td>{real_timeout}</td><td>超过测试超时时间</td><td>仅 read 命令</td></tr>')
P.append('</tbody></table>')
P.append('<h3>只读真实失败归因</h3>')
P.append(failure_category_table(real_read_matrix))
P.append(f'<h2 id="real-write-test-matrix">真实后端写入测试 <span class="c">· {real_write_total} / 162 write 覆盖中</span></h2>')
P.append(f'<p class="section-lead">这一层同样不使用 mock,也不使用 dry-run:用真实 dws 二进制、当前登录态、临时测试资源以及测试人员(冬翔、克谨、怒龙)分批执行 write / high-risk-write shortcut。当前已记录 {real_write_total} 条:成功 {real_write_ok} 条,真实后端/参数/业务错误 {real_write_error} 条,超时 {real_write_timeout} 条,待执行 {real_write_held} 条。写入类的失败同样保留真实 stdout/stderr,便于定位是 shortcut 参数投影问题、后端业务约束还是权限限制。</p>')
P.append('''<table class="why"><colgroup><col style="width:24%"><col style="width:25%"><col style="width:25%"><col style="width:26%"></colgroup>
<thead><tr><th>真实测试类型</th><th>数量</th><th>含义</th><th>副作用边界</th></tr></thead><tbody>''')
P.append(f'<tr><td><span class="test-ok">真实后端成功</span></td><td>{real_write_ok}</td><td>命令真实访问后端并 exit 0</td><td>临时资源 / 测试人员</td></tr>')
P.append(f'<tr><td><span class="test-warn">真实后端返回错误</span></td><td>{real_write_error}</td><td>命令真实访问后端或真实执行本地校验后返回错误;stdout/stderr 已逐条记录</td><td>临时资源 / 测试人员</td></tr>')
P.append(f'<tr><td><span class="test-bad">真实后端超时</span></td><td>{real_write_timeout}</td><td>超过测试超时时间</td><td>需单独复测</td></tr>')
P.append('</tbody></table>')
P.append('<h3>写入真实失败归因</h3>')
P.append(failure_category_table(real_write_matrix))
P.append(f'<h2 id="why-atomic-shortcuts">非复合 / 智能型 shortcut 为什么也要写? <span class="c">· {atomic_count} 个</span></h2>')
P.append('<p class="section-lead">原子型 shortcut 不是在宣称“新增了底层能力”,而是在 MCP tool 之上提供一层稳定、可发现、可测试的用户入口。它的价值边界要讲清楚:有增量就保留,纯重复就删除。</p>')
P.append('''<table class="why"><colgroup><col style="width:27%"><col style="width:36.5%"><col style="width:36.5%"></colgroup>
<thead><tr><th>必要性</th><th>没有原子型 shortcut 时</th><th>保留原子型 shortcut 的价值</th></tr></thead><tbody>
<tr><td>补齐命令面空白</td><td>有些 MCP tool 没有对应的 <code>dws &lt;svc&gt; &lt;verb&gt;</code> helper,只能走裸 <code>dws mcp ... --json</code></td><td>把真实可用的底层能力挂到产品命令树里,用户和 Agent 不必手写 JSON</td></tr>
<tr><td>参数收敛</td><td>调用者要记服务端字段名、嵌套 JSON、枚举和必填关系</td><td>收敛成命名 flag、默认值和校验,降低误填与 prompt 猜参概率</td></tr>
<tr><td>Intent / Help / 可发现性</td><td>MCP schema 偏接口视角,Agent 需要先理解 tool 再判断何时使用</td><td>每条 shortcut 都带任务化描述,可被 <code>--help</code>、<code>dws shortcut list</code> 和 Agent 规划直接利用</td></tr>
<tr><td>统一安全与输出入口</td><td>读写风险、dry-run、format / jq / fields 等能力分散到调用者侧</td><td>复用 shortcut 框架的 risk 分级、dry-run 通道和统一输出契约</td></tr>
<tr><td>稳定投影</td><td>底层响应 envelope 冗长且字段不稳定,常把噪声暴露给用户</td><td>对一部分读类命令保留干净投影,只输出用户真正要看的字段</td></tr>
<tr><td>对齐与治理</td><td>无法判断与 lark-cli、helper、MCP tool 的覆盖差异,也难以做后续去重</td><td>形成可枚举的命令面,便于生成报告、做 GSB 对比和持续 prune</td></tr>
</tbody></table>''')
P.append('<div class="note"><b>保留标准</b>:原子型 shortcut 必须至少满足“补 helper 空白”或“提供投影/校验/Intent 等体验增量”。本轮已删除 213 条仅换名字、无投影增量、且 helper 已覆盖的纯重复 wrapper;所以这里的原子型不是为了凑数量,而是为了把仍有价值的底层能力变成稳定入口。</div>')
P.append(f'<h2 id="smart-shortcuts">复合 / 智能型 shortcut <span class="c">· {len(smart)} 个</span></h2>')
P.append('<p class="section-lead">这组命令面向完整用户意图,例如“按姓名发消息”“找共同空闲时间”“找到最新妙记并取逐字稿”。其中一部分是真正的多 MCP / 跨服务编排,另一部分是单服务内的分页、过滤、批处理和稳定输出;两者都不是普通 API 别名。</p>')
P.append('<div class="card-grid">')
for svc in sorted(smart_by_svc, key=lambda s: (-len(smart_by_svc[s]), s)):
rows = sorted(smart_by_svc[svc], key=lambda x: x["command"])
P.append(f'<section class="card"><h3>{html.escape(svc)} · {len(rows)}</h3><ul class="smart-list">')
for it in rows:
P.append(f'<li><code>dws {html.escape(it["service"])} {html.escape(it["command"])}</code><span class="smart-desc">{html.escape(it["desc"])}</span></li>')
P.append('</ul></section>')
P.append('</div>')
P.append('<h2 id="why-shortcuts">为什么不只提供 API 形态?</h2>')
P.append('<p class="section-lead">API/MCP 形态以服务端资源和接口为中心;shortcut 以用户任务为中心。单独封装的价值不是少写几个字符,而是把稳定的客户端语义、保护措施和多步流程变成可测试契约。</p>')
P.append('''<table class="why"><colgroup><col style="width:27%"><col style="width:36.5%"><col style="width:36.5%"></colgroup>
<thead><tr><th>维度</th><th>直接 API / MCP</th><th>单独 shortcut 封装</th></tr></thead><tbody>
<tr><td>输入语义</td><td>要求调用者理解服务端字段名、类型、枚举和 JSON 结构</td><td>提供面向任务的 flags、默认值、枚举、时间解析和跨字段校验</td></tr>
<tr><td>ID 与上下文</td><td>通常先查 userId、deptId、eventId、baseId,再复制到下一次调用</td><td>可按姓名/标题解析、唯一性消歧,并自动串联后续操作</td></tr>
<tr><td>多步一致性</td><td>调用者自行处理部分成功、重试、回滚与中间结果</td><td>集中实现 fan-out、分页、去重、partial failure 和必要回滚</td></tr>
<tr><td>安全</td><td>写接口裸露,dry-run 和确认逻辑由每个调用者重复实现</td><td>统一 risk 分级、确认提示、dry-run 计划和高风险保护</td></tr>
<tr><td>输出契约</td><td>响应 envelope 和字段随底层工具而异,常含大量噪声</td><td>稳定投影关键字段,并统一支持 format / jq / fields</td></tr>
<tr><td>Agent 可发现性</td><td>模型需先理解 API schema,再自己规划完整调用链</td><td>Intent、示例和窄参数面直接表达“什么时候用、会发生什么”</td></tr>
<tr><td>演进隔离</td><td>上游参数或响应变化会扩散到所有脚本和 Agent prompt</td><td>在一个经过测试的适配层吸收兼容变化</td></tr>
</tbody></table>''')
P.append('<div class="note"><b>它不是 API 的替代品。</b> 对一次性、低频、需要完整底层字段或刚上线尚未沉淀的能力,仍应直接调用 API/MCP。只有当封装提供明确增量——复合编排、校验、安全、稳定投影或高频意图——才值得保留;纯粹改名且没有增量的 wrapper 应删除或不新增。</div>')
P.append(f'<h2 id="lark-platform-only">lark 平台特有 shortcut <span class="c">· {lark_platform_only_count} 个</span></h2>')
P.append('<p class="section-lead">这批不是“DWS 少写了几个 shortcut”,而是飞书平台有对应的一等产品对象或事件模型,当前钉钉 MCP/DWS 没有同语义承接点。后续只有在钉钉开放对应 API、或 DWS 接入等价 MCP tool 后,才适合再做 shortcut。</p>')
P.append('<div class="card-grid">')
for svc, cmds in lark_platform_only.items():
reason = LARK_PLATFORM_ONLY[svc]
P.append(f'<section class="card"><h3>lark {html.escape(svc)} · {len(cmds)}</h3>')
P.append(f'<div class="smart-desc">{html.escape(reason)}</div>')
P.append('<ul class="smart-list">')
for cmd in cmds:
P.append(f'<li><code>lark-cli {html.escape(svc)} {html.escape(cmd)}</code></li>')
P.append('</ul></section>')
P.append('</div>')
for svc in order:
rows = by_svc[svc]
lsvc = LARK_MAP.get(svc)
lbl = f"↔ lark {lsvc}" if lsvc else "钉钉特有 · lark 无对应"
P.append(f'<h2>{html.escape(svc)} <span class="c">· {len(rows)} 个 · {lbl}</span></h2>')
P.append('<table><colgroup><col class="c1"><col class="c2"><col class="c3"></colgroup>')
P.append('<thead><tr><th>dws shortcut(新增)</th><th>dws 原生 MCP 组合(等价裸命令)</th><th>lark-cli 对应</th></tr></thead><tbody>')
P.append('<table><colgroup><col style="width:27%"><col style="width:28%"><col style="width:22%"><col style="width:23%"></colgroup>')
P.append('<thead><tr><th>dws shortcut(新增)</th><th>原生 MCP / 编排说明</th><th>lark-cli 对应</th><th>实际测试情况</th></tr></thead><tbody>')
for it in sorted(rows, key=lambda x: x["command"]):
rk = RISK_CLS.get(it["risk"], "rk-r")
lk_cls, lk_txt = lark_cmd(it, lark)
lk_html = f'<span class="lk-{lk_cls}"><code>{html.escape(lk_txt)}</code></span>' if lk_cls == "hit" else f'<span class="lk-{lk_cls}">{html.escape(lk_txt)}</span>'
smart_badge = '<span class="smart-badge">复合/智能型</span>' if it["layer"] == "smart" else ""
P.append("<tr>")
P.append(f'<td class="sc"><code>{html.escape(shortcut_cmd(it))}</code><span class="rk {rk}">{it["risk"]}</span><div class="desc">{html.escape(it["desc"])}</div></td>')
P.append(f'<td class="sc"><code>{html.escape(shortcut_cmd(it))}</code><span class="rk {rk}">{it["risk"]}</span>{smart_badge}<div class="desc">{html.escape(it["desc"])}</div></td>')
P.append(f'<td class="raw"><code>{html.escape(raw_cmd(it))}</code></td>')
P.append(f'<td>{lk_html}</td>')
P.append(f'<td>{test_html(it, tests, real_tests)}</td>')
P.append("</tr>")
P.append("</tbody></table>")
@@ -216,13 +552,28 @@ footer{{color:var(--muted);text-align:center;font-size:12px;margin-top:36px}}
def main():
lark = load_lark()
items = collect()
out = gen_html(items, lark)
matrix, tests = load_test_matrix()
real_read_matrix, real_read_tests = load_real_read_matrix()
real_write_matrix, real_write_tests = load_real_write_matrix()
real_tests = {}
real_tests.update(real_read_tests)
real_tests.update(real_write_tests)
if matrix.get("failed", 0) or matrix.get("tool_verification_bad", 0):
raise SystemExit(f"shortcut test matrix failed: failed={matrix.get('failed')} tool_bad={matrix.get('tool_verification_bad')}")
out = gen_html(items, lark, matrix, tests, real_read_matrix, real_write_matrix, real_tests)
dst = os.path.join(ROOT, "docs", "shortcut-comparison.html")
open(dst, "w", encoding="utf-8").write(out)
hit = sum(1 for it in items if lark_cmd(it, lark)[0] == "hit")
miss = sum(1 for it in items if lark_cmd(it, lark)[0] == "miss")
none = sum(1 for it in items if lark_cmd(it, lark)[0] == "none")
print(f"shortcuts={len(items)} lark_hit={hit} lark_miss={miss} dingtalk_specific={none}")
print(f"tests={matrix.get('total')} assembled={matrix.get('assembled')} validation_blocked={matrix.get('validation_blocked')} failed={matrix.get('failed')} tool_bad={matrix.get('tool_verification_bad')}")
if real_read_matrix:
s = matrix_summary(real_read_matrix)
print(f"real_read_tests={s.get('total')} ok={s.get('ok')} error={s.get('error')} timeout={s.get('timeout')}")
if real_write_matrix:
s = matrix_summary(real_write_matrix)
print(f"real_write_tests={s.get('total')} ok={s.get('ok')} error={s.get('error')} timeout={s.get('timeout')} held={s.get('held')}")
print("written:", dst)
if __name__ == "__main__":
+449
View File
@@ -0,0 +1,449 @@
#!/usr/bin/env python3
"""Generate a per-error review report from real shortcut test results."""
from __future__ import annotations
import json
import re
from collections import Counter
import html
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[1]
READ_PATH = ROOT / "docs" / "shortcut-real-read-results.json"
WRITE_PATH = ROOT / "docs" / "shortcut-real-write-results.json"
OUT_PATH = ROOT / "docs" / "shortcut-error-review.html"
CATEGORY_LABELS = {
"auth-or-permission": "鉴权/权限",
"backend-business-rule": "后端业务规则",
"backend-or-mcp-error": "后端/MCP",
"input-or-business-validation": "输入/业务校验",
"missing-real-aitable-fixture": "缺 AI 表格 fixture",
"missing-real-minutes-fixture": "缺妙记 fixture",
"missing-real-resource": "缺真实资源",
"held": "敏感/高风险暂缓",
}
RESOURCE_AUDIT = [
("chat +group-members", "已补齐", "查到真实群名 `浅曦-kida,Dennis,秋画`,runner 已改为用群名而不是 openConversationId;真实回归成功。", "无需后续动作。"),
("chat +messages-mget", "已补齐", "复用真实单聊消息 `msgEuOor1PmFBNlx9M06N9z1Q==`;真实回归成功。", "无需后续动作。"),
("chat +messages-read-status", "已补齐", "复用真实单聊会话 `cidie1367hAfBxqipzE59k5sknHLrHmvYkw98NADhfnjPI=` 与同一 openMessageId;真实回归成功。", "无需后续动作。"),
("chat +messages-query-send-status", "已补齐", "复用真实发送返回的 openTaskId;真实回归成功。", "无需后续动作。"),
("ding +receiver-status", "已补齐", "先只读 `ding +list` 找到已有 openDingId,再查询 receiver status;没有新发 DING,真实回归成功。", "无需后续动作。"),
("sheet +list-sheets", "已自造", "创建临时在线表格 `DWS shortcut 真实测试表格 20260715`,nodeId=`mweZ92PV6O36dZbnsMZx70ylJxEKBD6p`;真实回归成功。", "后续可保留为稳定 fixture,或测试结束后人工清理。"),
("todo +todo-done", "已自造并修复 CLI", "runner 会先创建当前账号自己的临时待办,再执行 `todo +todo-done`;同时修复了 todo 列表 pageSize=50 返回空、响应多层 result unwrap 不稳的问题;真实回归成功。", "无需后续动作;代码已有单测覆盖 nested result。"),
("contact +by-mobile", "已按用户授权补齐", "使用用户指定手机号 `13161187007` 作为真实 fixture;runner 只在该命令上替换 mobile,不扩散到其它服务。", "真实回归成功后该项将从失败列表移除;若后续要脱敏公开报告,可再加展示层脱敏。"),
("attendance +get-class / +get-group / +get-group-filtered", "不建议自造", "`attendance +search-class` 与 `+search-group --type FIXED` 均返回空;创建班次/考勤组会改组织考勤配置,属于高影响业务数据。", "需要考勤后端/业务同学提供可读测试班次与考勤组 ID。"),
("chat +chat-get-by-id", "暂未找到", "该 shortcut 只接受数字 groupId;真实群列表只返回 openConversationId,没有数字群号字段。", "需要 IM 后端提供可用数字 groupId,或评估是否新增 openConversationId 形态的 shortcut。"),
("chat +messages-resource-url", "暂未自造", "需要真实含 mediaId 的图片/文件/视频消息;当前文本消息无法产生 resource-id。", "可在测试群发一条图片/文件消息并提取 mediaId 后补 fixture;注意会产生群消息。"),
("chat +thread-replies", "暂未自造", "需要真实话题消息 topicId;普通群消息不能替代。", "需要话题群 fixture,或由 IM 同学提供当前账号可访问 topicId。"),
("aitable +dashboard-share-get", "真实资源仍失败", "已有真实 base/dashboard,但 share-get 返回 404 `Failed to get dashboard share config`;更像分享配置未开启或后端接口行为问题。", "需要 AI 表格/后端确认如何创建/开启 dashboard share fixture,或修正 404 语义。"),
("write 类 delete/recall/approve/wiki move/copy 等", "不自动自造", "这些命令即便能造资源,也会涉及删除、撤回、审批通过、知识库移动/复制等高影响动作。", "需要逐项授权和专门测试空间/机器人/审批单据,不建议混在批量回归里自动跑。"),
]
def load(path: Path) -> dict[str, Any]:
return json.loads(path.read_text(encoding="utf-8"))
def squish(text: str, limit: int = 180) -> str:
text = re.sub(r"\s+", " ", text or "").strip()
if len(text) <= limit:
return text
return text[: limit - 1] + "…"
def error_text(row: dict[str, Any]) -> str:
return f"{row.get('stderr') or ''}\n{row.get('stdout') or ''}"
def parse_error_payload(row: dict[str, Any]) -> dict[str, Any]:
for key in ("stderr", "stdout"):
raw = (row.get(key) or "").strip()
if not raw:
continue
try:
value = json.loads(raw)
except json.JSONDecodeError:
continue
if isinstance(value, dict):
err = value.get("error")
if isinstance(err, dict):
return err
return value
return {}
def operation(row: dict[str, Any]) -> str:
payload = parse_error_payload(row)
op = payload.get("operation")
if isinstance(op, str) and op:
return op
msg = payload.get("message")
if isinstance(msg, str):
m = re.search(r"\(operation:\s*([^)]+)\)", msg)
if m:
return m.group(1)
return "-"
def trace_id(row: dict[str, Any]) -> str:
payload = parse_error_payload(row)
tid = payload.get("trace_id") or payload.get("traceId")
return str(tid) if tid else "-"
def evidence(row: dict[str, Any]) -> str:
payload = parse_error_payload(row)
msg = payload.get("message")
if isinstance(msg, str) and msg.strip():
return squish(msg, 220)
return squish(error_text(row), 220)
def action_for(row: dict[str, Any], suite: str) -> tuple[str, str, str]:
"""Return (owner, action, verification) for one failing row."""
category = row.get("failure_category") or "unclassified"
service = row.get("service") or ""
command = row.get("command") or ""
text = error_text(row).lower()
msg = evidence(row)
if category == "auth-or-permission":
return (
"权限/应用配置",
"给当前登录账号、DWS 应用或对应资源补齐权限/scope;本仓库 shortcut 不应绕过权限。拿 trace_id 给服务端/开放平台排查具体 scope。",
f"补权限后重跑 `{service} {command}`;若仍是 permission,再看 trace_id。",
)
if category == "missing-real-aitable-fixture":
return (
"测试数据",
"准备真实 Base/Table/View/Field/Record/Role/Chart/Dashboard 等 fixture,并把真实 ID 写入真实测试 runner;当前安全负向 ID 只能证明调用链,不可能成功。",
"fixture 准备好后重跑对应 aitable 命令;预期从 not_found/invalid_base_id 变为成功或更具体业务错误。",
)
if category == "missing-real-minutes-fixture":
return (
"测试数据",
"准备当前账号可见的真实妙记/听记/录制会话,或把 runner 的搜索关键词改成必然能命中的会议产物。",
"用真实 taskUuid/note/minutes 资源重跑 minutes 命令。",
)
if category == "missing-real-resource":
if "opencid" in text or "openconversationid" in text:
return (
"测试数据",
"替换为当前账号真实可访问的群/会话 openConversationId;如果用真实群仍报“无效”,再查 IM 后端解析。",
"先用 `chat +my-groups` 或群搜索拿真实会话 ID,再重跑。",
)
if "openmessageid" in text or "openmsgid" in text or "invalid openmsgid" in text:
return (
"测试数据",
"替换为真实 openMessageId,并保证消息属于当前账号可访问会话;当前 no-such ID 只能验证负向路径。",
"先用消息列表拿 messageId,再重跑消息详情/状态/撤回类命令。",
)
if service == "attendance":
return (
"测试数据",
"替换为真实考勤班次/考勤组/员工/假期等资源 ID;当前 no-such ID 只能验证负向路径。",
"先用考勤列表/管理后台拿真实 ID,再重跑该 attendance 命令。",
)
if service == "contact":
return (
"测试数据",
"替换为当前组织通讯录中真实存在的人、手机号、部门或花名册对象;当前占位查询不会命中。",
"用真实姓名/手机号/部门重跑 contact 命令。",
)
if "table_not_found" in text or "node" in text or "doc" in text:
return (
"测试数据",
"替换为真实文档/节点 ID;文档评论、分享、版本等命令需要资源存在且账号可访问。",
"用真实 doc/node 重跑;若仍失败再看 doc/doc-comment 工具字段。",
)
return (
"测试数据",
"把 runner 里的安全负向 ID 换成真实资源 ID;当前错误说明调用已进后端,但资源不存在。",
f"准备 fixture 后重跑 `{service} {command}`。",
)
if category == "backend-or-mcp-error":
if service == "chat" and (
"opencid" in text
or "openconversationid" in text
or "receiveruid" in text
or "applicantuid" in text
):
return (
"后端/MCP schema",
"CLI fake MCP 已证明字段已装配;需要修 MCP tool schema 或网关字段映射,确认 openConversationId/openCid/cid、receiverUid、applicantUid 等字段没有在 schema 校验/转发时被丢弃。",
"修 MCP 后不改 shortcut,直接重跑真实命令;预期错误从 required 变为资源无效或成功。",
)
if service == "aitable":
return (
"后端/MCP schema",
"修 aitable MCP wrapper 的参数校验和错误语义:不要返回 success=true+error;对 required 字段给出 CLI 可识别的参数名,系统错误要带 retryable/trace。",
"MCP 修完后重跑该 aitable 命令,并确认 stdout JSON 不再出现 success=true 但 error 非空。",
)
return (
"后端/MCP 服务",
"优先修服务端/MCP 内部错误或 schema 映射;CLI 当前只能如实暴露错误,不应吞掉或伪造成功。",
"服务端修复后按 trace_id/operation 重跑同一命令。",
)
if category == "backend-business-rule":
return (
"测试账号/业务规则",
"按后端业务规则换账号或换场景;例如组织者不能响应自己的日程邀请,这不是 CLI bug。",
"用满足业务角色的账号重跑。",
)
if category == "held":
return (
"人工安全确认",
"该项涉及敏感读取或无安全负向目标,不适合自动用真实资源跑;需要在安全环境逐项人工确认。",
f"人工确认后单独重跑 `{service} {command}`,并避免在报告中泄露密钥/凭证。",
)
if category == "input-or-business-validation":
if "没有找到名称包含" in msg or "没搜到" in msg or "暂无" in msg or "没有已处理" in msg or "当前没有" in msg or "今天没有" in msg or "没有与你相关" in msg:
return (
"测试数据",
"准备能命中的真实数据,或把 runner 查询词改成当前账号一定存在的对象;shortcut 本身不需要改。",
"造数后重跑,预期从 validation empty result 变为成功。",
)
if "roomid invalid" in text or "会议室数量,超过上限100" in msg:
return (
"测试输入",
"会议室类命令需要真实 roomId 或更小会议室分组;修改 runner 先定位会议室/分组,再喂给查询命令。",
"用真实 roomId/分组重跑 calendar room/freebusy 命令。",
)
if "token decode create_at fail" in text:
return (
"测试输入",
"calendarId 不能用占位/primary;runner 应先通过日历列表拿真实 calendarId,再用于 agenda。",
"用真实 calendarId 重跑 `calendar +agenda`。",
)
if "unsupported setting key" in text:
return (
"测试输入/shortcut 枚举",
"把测试输入的 setting-key 从 x 改为后端支持的 key;同时可在 shortcut flag 上补 enum,避免用户传非法 key。",
"改 runner 后重跑;如果补 enum,跑 shortcut 单测确认校验文案。",
)
if "暂不支持保存该文字表情" in msg:
return (
"测试输入/业务规则",
"换成后端支持的文字表情组合,或把该命令保留为业务负向;CLI 不应绕过后端限制。",
"用一个真实可保存的表情模板重跑。",
)
if "opentaskid解密失败" in text:
return (
"测试数据",
"消息发送状态查询需要真实 openTaskId;先真实发送/获取任务 ID,再作为 fixture 输入。",
"用真实 openTaskId 重跑。",
)
if "taskid is null" in text:
return (
"后端/MCP schema",
"CLI 已传 task-id 但后端看到 null,优先查 todo MCP 参数映射;若 schema 只认嵌套 request,需要同步修改 shortcut 参数投影。",
"用 dry-run/fake MCP 对比真实 trace,确认 taskId 是否在 MCP 层丢失。",
)
if "system error" in text or "mcp_server_error" in text or "系统繁忙" in msg:
return (
"后端服务",
"服务端返回系统繁忙/内部错误;CLI 侧可增加重试提示,但根因需要服务端修。",
"稍后重跑并带 trace_id 给服务端。",
)
if "business error: success=false" in text or "business error: code" in text:
return (
"后端业务/测试 fixture",
"后端只返回 success=false,信息不足;先准备真实合法 fixture,若仍无细节,需要后端补充错误码/错误信息。",
"用真实资源重跑;若仍 success=false,把 operation+trace_id 给后端。",
)
return (
"测试输入/业务校验",
"当前命令已进入后端业务校验;先把测试输入换成真实合法 fixture,再判断是否需要改 shortcut。",
f"重跑 `{service} {command}` 并比较 stdout/stderr。",
)
return (
"待人工 triage",
"当前分类不足,需要结合 trace_id、operation 和完整 stdout/stderr 单独查。",
"先复现单条命令,再决定改 CLI、runner 还是后端。",
)
def esc(value: Any) -> str:
return html.escape(str(value), quote=True)
def table_rows(rows: list[dict[str, Any]], suite: str) -> list[str]:
out: list[str] = []
for idx, row in enumerate(rows, 1):
owner, action, verify = action_for(row, suite)
category = CATEGORY_LABELS.get(row.get("failure_category"), row.get("failure_category", "-"))
cmd = f"{row.get('service')} {row.get('command')}"
out.append(
"<tr>"
f"<td class=\"num\">{idx}</td>"
f"<td><code>{esc(cmd)}</code></td>"
f"<td><span class=\"risk\">{esc(row.get('risk') or '-')}</span></td>"
f"<td><span class=\"cat\">{esc(category)}</span></td>"
f"<td><span class=\"owner\">{esc(owner)}</span></td>"
f"<td>{esc(action)}</td>"
f"<td>{esc(verify)}</td>"
f"<td><code>{esc(operation(row))}</code></td>"
f"<td><code>{esc(trace_id(row))}</code></td>"
f"<td class=\"evidence\">{esc(evidence(row))}</td>"
"</tr>"
)
return out
def stat_cards(read: dict[str, Any], write: dict[str, Any], read_errors: list[dict[str, Any]], write_errors: list[dict[str, Any]]) -> str:
all_errors = read_errors + write_errors
owner_counts = Counter(action_for(row, "all")[0] for row in all_errors)
category_counts = Counter(CATEGORY_LABELS.get(row.get("failure_category"), row.get("failure_category", "-")) for row in all_errors)
cards = [
("Read 失败", read["summary"]["error"]),
("Write 失败", write["summary"]["error"]),
("逐项 review", len(all_errors)),
("测试数据/fixture", owner_counts.get("测试数据", 0)),
("权限/应用配置", owner_counts.get("权限/应用配置", 0)),
("后端/MCP schema", owner_counts.get("后端/MCP schema", 0)),
]
card_html = "\n".join(f'<div class="stat"><div class="n">{esc(v)}</div><div class="l">{esc(k)}</div></div>' for k, v in cards)
category_rows = "\n".join(
f"<tr><td>{esc(k)}</td><td>{esc(v)}</td></tr>"
for k, v in category_counts.most_common()
)
owner_rows = "\n".join(
f"<tr><td>{esc(k)}</td><td>{esc(v)}</td></tr>"
for k, v in owner_counts.most_common()
)
return f"""
<div class="stats">{card_html}</div>
<div class="summary-grid">
<section><h2>按错误类型</h2><table><thead><tr><th>类型</th><th>数量</th></tr></thead><tbody>{category_rows}</tbody></table></section>
<section><h2>按要改哪里</h2><table><thead><tr><th>要改哪里</th><th>数量</th></tr></thead><tbody>{owner_rows}</tbody></table></section>
</div>
"""
def render_table(title: str, rows: list[dict[str, Any]], suite: str) -> str:
body = "\n".join(table_rows(rows, suite))
return f"""
<h2>{esc(title)} <span class="count">· {len(rows)} 条</span></h2>
<table class="review">
<thead><tr>
<th>#</th><th>命令</th><th>风险</th><th>类型</th><th>要改哪里</th><th>具体改法</th><th>验证方式</th><th>operation</th><th>trace_id</th><th>证据</th>
</tr></thead>
<tbody>
{body}
</tbody>
</table>
"""
def render_resource_audit() -> str:
rows = "\n".join(
"<tr>"
f"<td><code>{esc(cmd)}</code></td>"
f"<td><span class=\"cat\">{esc(status)}</span></td>"
f"<td>{esc(finding)}</td>"
f"<td>{esc(next_step)}</td>"
"</tr>"
for cmd, status, finding, next_step in RESOURCE_AUDIT
)
return f"""
<h2>缺真实资源复盘 <span class="count">· 可自造/可查资源处理结果</span></h2>
<table>
<thead><tr><th>命令/范围</th><th>处理状态</th><th>本次实际排查/造数结果</th><th>后续建议</th></tr></thead>
<tbody>{rows}</tbody>
</table>
"""
def render() -> str:
read = load(READ_PATH)
write = load(WRITE_PATH)
read_errors = [r for r in read["results"] if r.get("status") != "real-ok"]
write_errors = [r for r in write["results"] if r.get("status") != "real-ok"]
return f"""<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Shortcut 真实测试失败逐项 review</title>
<style>
:root{{--bg:#0f1420;--card:#151d2b;--line:#263246;--text:#dce7f7;--muted:#91a0b5;--blue:#8fd3ff;--green:#66d38a;--yellow:#e2b23c;--red:#f27272;--purple:#d3a7ff}}
*{{box-sizing:border-box}}
body{{margin:0;background:var(--bg);color:var(--text);font:13px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"PingFang SC","Microsoft YaHei",sans-serif}}
header{{padding:28px 32px 14px;border-bottom:1px solid var(--line);background:linear-gradient(180deg,#172033,#0f1420)}}
h1{{margin:0 0 8px;font-size:26px}}
h2{{margin:28px 0 10px;font-size:18px}}
.sub,.note,.count{{color:var(--muted)}}
.wrap{{padding:18px 32px 40px;max-width:1800px;margin:0 auto}}
.note{{background:var(--card);border:1px solid var(--line);border-radius:10px;padding:12px 14px;margin:10px 0 18px}}
.stats{{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin:18px 0}}
.stat{{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:14px}}
.stat .n{{font-size:24px;color:var(--blue);font-weight:700}}
.stat .l{{color:var(--muted);font-size:12px}}
.summary-grid{{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:18px;margin:16px 0 22px}}
table{{width:100%;border-collapse:collapse;background:var(--card);border:1px solid var(--line);border-radius:10px;overflow:hidden}}
th,td{{padding:8px 10px;border-bottom:1px solid var(--line);vertical-align:top;text-align:left}}
th{{background:#1b2536;color:var(--muted);font-size:12px;font-weight:600;position:sticky;top:0;z-index:1}}
tr:last-child td{{border-bottom:none}}
code{{font-family:"SF Mono",Menlo,Consolas,monospace;color:#c7cfdb;font-size:12px}}
.review{{table-layout:fixed}}
.review th:nth-child(1),.review td:nth-child(1){{width:44px}}
.review th:nth-child(2),.review td:nth-child(2){{width:165px}}
.review th:nth-child(3),.review td:nth-child(3){{width:70px}}
.review th:nth-child(4),.review td:nth-child(4){{width:120px}}
.review th:nth-child(5),.review td:nth-child(5){{width:130px}}
.review th:nth-child(8),.review td:nth-child(8){{width:130px}}
.review th:nth-child(9),.review td:nth-child(9){{width:180px}}
.review td{{word-break:break-word}}
.num{{color:var(--muted);text-align:right}}
.risk{{color:var(--green)}}
.cat{{color:var(--yellow)}}
.owner{{color:var(--purple)}}
.evidence{{color:#c7cfdb;font-size:12px}}
a{{color:var(--blue)}}
</style>
</head>
<body>
<header>
<h1>Shortcut 真实测试失败逐项 review</h1>
<div class="sub">由 <code>scripts/gen_shortcut_error_review.py</code> 从真实测试结果生成;目标是把每个失败项落到“应该改哪里”。</div>
</header>
<main class="wrap">
<div class="note">
Read:{esc(read['summary']['total'])} 条,成功 {esc(read['summary']['ok'])},失败 {esc(read['summary']['error'])},超时 {esc(read['summary']['timeout'])}。
Write:{esc(write['summary']['total'])} 条,成功 {esc(write['summary']['ok'])},失败 {esc(write['summary']['error'])},超时 {esc(write['summary']['timeout'])}。
判定口径:如果 fake MCP 已看到字段但真实后端仍报 required,按后端/MCP schema 映射处理;如果真实后端报资源无效/不存在,按 fixture 处理;权限类不在 CLI 中绕过。
</div>
{stat_cards(read, write, read_errors, write_errors)}
{render_resource_audit()}
{render_table("Read 失败逐项", read_errors, "read")}
{render_table("Write 失败逐项", write_errors, "write")}
</main>
</body>
</html>
"""
def main() -> int:
OUT_PATH.write_text(render(), encoding="utf-8")
print(f"written: {OUT_PATH}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+172
View File
@@ -0,0 +1,172 @@
#!/usr/bin/env python3
"""Generate the release-hidden shortcut list from real test results.
Shortcuts whose latest real-backend result is not `real-ok` are kept in source
code but hidden from public discovery for this release. The generated Go file
is a release gate; the Markdown/JSON files are the handoff list for the next
iteration.
"""
from __future__ import annotations
import datetime as dt
import json
import re
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[1]
READ_PATH = ROOT / "docs" / "shortcut-real-read-results.json"
WRITE_PATH = ROOT / "docs" / "shortcut-real-write-results.json"
GO_PATH = ROOT / "internal" / "shortcut" / "release_hidden_generated.go"
MD_PATH = ROOT / "docs" / "shortcut-next-release-hidden.md"
JSON_PATH = ROOT / "docs" / "shortcut-next-release-hidden.json"
def load(path: Path) -> dict[str, Any]:
return json.loads(path.read_text(encoding="utf-8"))
def squish(text: str, limit: int = 180) -> str:
text = re.sub(r"\s+", " ", text or "").strip()
if len(text) <= limit:
return text
return text[: limit - 1] + "…"
def error_text(row: dict[str, Any]) -> str:
return f"{row.get('stderr') or ''}\n{row.get('stdout') or ''}"
def parse_error_payload(row: dict[str, Any]) -> dict[str, Any]:
for key in ("stderr", "stdout"):
raw = (row.get(key) or "").strip()
if not raw:
continue
try:
value = json.loads(raw)
except json.JSONDecodeError:
continue
if isinstance(value, dict):
err = value.get("error")
if isinstance(err, dict):
return err
return value
return {}
def evidence(row: dict[str, Any]) -> str:
payload = parse_error_payload(row)
msg = payload.get("message")
if isinstance(msg, str) and msg.strip():
return squish(msg, 220)
return squish(error_text(row), 220)
def collect() -> list[dict[str, Any]]:
out: list[dict[str, Any]] = []
for suite, path in (("read", READ_PATH), ("write", WRITE_PATH)):
data = load(path)
for row in data.get("results", []):
if row.get("status") == "real-ok":
continue
out.append({
"suite": suite,
"service": row.get("service") or "",
"command": row.get("command") or "",
"risk": row.get("risk") or "",
"status": row.get("status") or "",
"failure_category": row.get("failure_category") or "unclassified",
"fixability": row.get("fixability") or "",
"diagnosis": row.get("diagnosis") or "",
"evidence": evidence(row),
})
out.sort(key=lambda r: (r["suite"], r["service"], r["command"]))
return out
def go_quote(s: str) -> str:
return json.dumps(s, ensure_ascii=False)
def write_go(rows: list[dict[str, Any]]) -> None:
lines = [
"// Code generated by scripts/gen_shortcut_release_hidden.py; DO NOT EDIT.",
"",
"package shortcut",
"",
"// releaseHiddenShortcuts contains shortcuts intentionally hidden from",
"// public discovery in this release because their latest real-backend",
"// test did not pass. The command implementations remain registered and",
"// invocable by exact path for internal follow-up testing.",
"var releaseHiddenShortcuts = map[string]string{",
]
for row in rows:
key = f"{row['service']}\x00{row['command']}"
reason = (
f"{row['suite']} real test not passed; "
f"status={row['status']}; category={row['failure_category']}; "
f"fixability={row['fixability'] or '-'}"
)
lines.append(f"\t{go_quote(key)}: {go_quote(reason)},")
lines.extend(["}", ""])
GO_PATH.write_text("\n".join(lines), encoding="utf-8")
def md_escape(s: str) -> str:
return str(s).replace("|", "\\|").replace("\n", "<br>")
def write_md(rows: list[dict[str, Any]]) -> None:
generated = dt.datetime.now().isoformat(timespec="seconds")
lines = [
"# 本期隐藏、下期修复的 shortcut 清单",
"",
f"生成时间:`{generated}`",
"",
"来源:`docs/shortcut-real-read-results.json` 与 `docs/shortcut-real-write-results.json`。",
"",
"口径:最新真实后端测试结果不是 `real-ok` 的 shortcut,本期不对外 release;代码保留,并通过 `internal/shortcut/release_hidden_generated.go` 标记为 Hidden,后续补 fixture / 修后端 / 修 CLI 后再放开。",
"",
f"总计:{len(rows)} 条。",
"",
"| # | suite | shortcut | risk | status | category | fixability | 下期处理依据 |",
"|---:|---|---|---|---|---|---|---|",
]
for idx, row in enumerate(rows, 1):
shortcut = f"{row['service']} {row['command']}"
basis = row["diagnosis"] or row["evidence"]
lines.append(
f"| {idx} | {md_escape(row['suite'])} | `{md_escape(shortcut)}` | "
f"{md_escape(row['risk'])} | {md_escape(row['status'])} | "
f"{md_escape(row['failure_category'])} | {md_escape(row['fixability'])} | "
f"{md_escape(squish(basis, 160))} |"
)
lines.append("")
MD_PATH.write_text("\n".join(lines), encoding="utf-8")
def write_json(rows: list[dict[str, Any]]) -> None:
payload = {
"generated_at": dt.datetime.now().isoformat(),
"count": len(rows),
"results": rows,
}
JSON_PATH.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
def main() -> int:
rows = collect()
write_go(rows)
write_md(rows)
write_json(rows)
print(f"hidden={len(rows)}")
print(f"written: {GO_PATH}")
print(f"written: {MD_PATH}")
print(f"written: {JSON_PATH}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+314
View File
@@ -0,0 +1,314 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0.
// gen_shortcut_test_matrix executes every registered built-in shortcut through
// the real cobra command tree with a fake MCP caller. It records whether each
// command safely assembles an MCP call (zero network / zero side effect) or is
// intentionally stopped by its own validation. The JSON output is consumed by
// scripts/gen_shortcut_comparison.py so the HTML report can show per-command
// test status instead of only aggregate coverage.
package main
import (
"context"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/helpers"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/builtin"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
"github.com/spf13/cobra"
)
type mcpCall struct {
Product string `json:"product"`
Tool string `json:"tool"`
Args map[string]any `json:"args,omitempty"`
}
type fakeCaller struct {
calls []mcpCall
}
func (f *fakeCaller) reset() { f.calls = nil }
func (f *fakeCaller) CallTool(_ context.Context, product, tool string, args map[string]any) (*edition.ToolResult, error) {
f.calls = append(f.calls, mcpCall{Product: product, Tool: tool, Args: args})
payload := map[string]any{
"_fake": true,
"product": product,
"tool": tool,
"args": args,
}
b, _ := json.Marshal(payload)
return &edition.ToolResult{Content: []edition.ContentBlock{{Type: "text", Text: string(b)}}}, nil
}
func (f *fakeCaller) Format() string { return "json" }
func (f *fakeCaller) DryRun() bool { return false }
func (f *fakeCaller) Fields() string { return "" }
func (f *fakeCaller) JQ() string { return "" }
type commandResult struct {
Service string `json:"service"`
Command string `json:"command"`
Risk string `json:"risk"`
Method string `json:"method"`
Status string `json:"status"`
Input string `json:"input"`
Args []string `json:"args"`
Stdout string `json:"stdout,omitempty"`
Stderr string `json:"stderr,omitempty"`
Calls []mcpCall `json:"calls,omitempty"`
CallCount int `json:"call_count"`
Tools []string `json:"tools,omitempty"`
ToolVerified bool `json:"tool_verified"`
Error string `json:"error,omitempty"`
Note string `json:"note"`
}
type matrix struct {
Total int `json:"total"`
Assembled int `json:"assembled"`
ValidationBlocked int `json:"validation_blocked"`
Failed int `json:"failed"`
ToolVerificationBad int `json:"tool_verification_bad"`
Results []commandResult `json:"results"`
}
func realToolSet() map[string]bool {
files, _ := filepath.Glob("internal/helpers/*.go")
wordRe := regexp.MustCompile(`[A-Za-z][A-Za-z0-9_]{2,}`)
cjkRe := regexp.MustCompile(`"([^"]*\p{Han}[^"]*)"`)
set := map[string]bool{}
for _, f := range files {
data, err := os.ReadFile(f)
if err != nil {
continue
}
s := string(data)
for _, m := range wordRe.FindAllString(s, -1) {
set[m] = true
}
for _, m := range cjkRe.FindAllStringSubmatch(s, -1) {
set[m[1]] = true
}
}
return set
}
func synthArgs(s shortcut.Shortcut) []string {
args := []string{s.Service, s.Command}
for _, f := range s.Flags {
switch {
case len(f.Enum) > 0:
args = append(args, "--"+f.Name, f.Enum[0])
case f.Type == shortcut.FlagBool:
args = append(args, "--"+f.Name)
case f.Type == shortcut.FlagInt:
args = append(args, "--"+f.Name, "1")
default:
args = append(args, "--"+f.Name, "x")
}
}
return append(args, "--yes")
}
func newRoot() *cobra.Command {
root := &cobra.Command{Use: "dws", SilenceUsage: true, SilenceErrors: true}
root.SetOut(io.Discard)
root.SetErr(io.Discard)
root.PersistentFlags().Bool("yes", false, "")
root.PersistentFlags().Bool("dry-run", false, "")
root.PersistentFlags().String("format", "json", "")
root.AddCommand(builtin.Commands()...)
return root
}
func uniqueTools(calls []mcpCall) []string {
seen := map[string]bool{}
var out []string
for _, c := range calls {
name := c.Product + "/" + c.Tool
if !seen[name] {
seen[name] = true
out = append(out, name)
}
}
sort.Strings(out)
return out
}
func riskString(s shortcut.Shortcut) string {
if s.Risk == "" {
return string(shortcut.RiskRead)
}
return string(s.Risk)
}
func setCommandOutput(cmd *cobra.Command, out, errOut io.Writer) {
cmd.SetOut(out)
cmd.SetErr(errOut)
for _, child := range cmd.Commands() {
setCommandOutput(child, out, errOut)
}
}
func runCaptured(fake *fakeCaller, root *cobra.Command, args []string) (stdout, stderr string, err error, panicValue any) {
stdoutFile, err := os.CreateTemp("", "dws-shortcut-stdout-*.txt")
if err != nil {
return "", "", err, nil
}
defer os.Remove(stdoutFile.Name())
stderrFile, err := os.CreateTemp("", "dws-shortcut-stderr-*.txt")
if err != nil {
stdoutFile.Close()
return "", "", err, nil
}
defer os.Remove(stderrFile.Name())
oldStdout := os.Stdout
oldStderr := os.Stderr
os.Stdout = stdoutFile
os.Stderr = stderrFile
defer func() {
os.Stdout = oldStdout
os.Stderr = oldStderr
}()
helpers.InitDeps(fake)
setCommandOutput(root, stdoutFile, stderrFile)
root.SetArgs(args)
err = func() (e error) {
defer func() {
if r := recover(); r != nil {
panicValue = r
}
}()
return root.Execute()
}()
stdoutFile.Sync()
stderrFile.Sync()
outBytes, _ := os.ReadFile(stdoutFile.Name())
errBytes, _ := os.ReadFile(stderrFile.Name())
stdoutFile.Close()
stderrFile.Close()
return string(outBytes), string(errBytes), err, panicValue
}
func shellQuote(arg string) string {
if arg == "" {
return "''"
}
if strings.ContainsAny(arg, " \t\n'\"\\$`") {
return "'" + strings.ReplaceAll(arg, "'", "'\\''") + "'"
}
return arg
}
func commandLine(args []string) string {
parts := make([]string, 0, len(args)+1)
parts = append(parts, "dws")
for _, arg := range args {
parts = append(parts, shellQuote(arg))
}
return strings.Join(parts, " ")
}
func main() {
fake := &fakeCaller{}
helpers.InitDeps(fake)
root := newRoot()
real := realToolSet()
all := shortcut.All()
results := make([]commandResult, 0, len(all))
out := matrix{Total: len(all)}
for _, s := range all {
args := synthArgs(s)
fake.reset()
stdout, stderr, err, panicValue := runCaptured(fake, root, args)
res := commandResult{
Service: s.Service,
Command: s.Command,
Risk: riskString(s),
Method: "cobra+fake-mcp-zero-side-effect",
Input: commandLine(args),
Args: append([]string(nil), args...),
Stdout: strings.TrimSpace(stdout),
Stderr: strings.TrimSpace(stderr),
Calls: append([]mcpCall(nil), fake.calls...),
}
res.CallCount = len(res.Calls)
res.Tools = uniqueTools(res.Calls)
res.ToolVerified = true
for _, call := range res.Calls {
if call.Tool == "" || !real[call.Tool] {
res.ToolVerified = false
}
}
switch {
case panicValue != nil:
res.Status = "failed"
res.Error = fmt.Sprintf("panic: %v", panicValue)
res.Note = "测试失败:执行时发生 panic。"
out.Failed++
case len(fake.calls) > 0:
res.Status = "assembled"
if err != nil {
res.Error = err.Error()
res.Note = "已实际执行到 MCP 组装/调用路径;fake MCP 已拦截,无网络、无副作用;后续因合成响应不足产生错误,不影响装配验证。"
} else {
res.Note = "已实际执行到 MCP 组装/调用路径;fake MCP 已拦截,无网络、无副作用。"
}
out.Assembled++
case err != nil:
res.Status = "validation-blocked"
res.Error = err.Error()
res.Note = "已实际执行到命令校验链路;synthetic 参数被该命令自身校验拦截,属于预期的零副作用验证结果。"
out.ValidationBlocked++
default:
res.Status = "failed"
res.Note = "测试失败:命令无错误返回但未组装 MCP 调用。"
out.Failed++
}
if !res.ToolVerified {
out.ToolVerificationBad++
if res.Error != "" {
res.Error += "; "
}
res.Error += "tool literal not found in helper ground truth"
}
res.Error = strings.TrimSpace(res.Error)
results = append(results, res)
}
sort.Slice(results, func(i, j int) bool {
if results[i].Service == results[j].Service {
return results[i].Command < results[j].Command
}
return results[i].Service < results[j].Service
})
out.Results = results
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
if err := enc.Encode(out); err != nil {
fmt.Fprintf(os.Stderr, "encode matrix: %v\n", err)
os.Exit(1)
}
if out.Failed > 0 || out.ToolVerificationBad > 0 {
os.Exit(1)
}
}
+104
View File
@@ -0,0 +1,104 @@
#!/usr/bin/env python3
"""Run one shortcut command and append its real CLI result to a JSON report."""
from __future__ import annotations
import argparse
import datetime as dt
import json
import shlex
import subprocess
import time
from pathlib import Path
from shortcut_real_result import classify_failure, summarize_failure_categories, classify_real_status, summarize_results
ROOT = Path(__file__).resolve().parents[1]
OUT_PATH = ROOT / "docs" / "shortcut-real-write-results.json"
def load_report() -> dict:
if not OUT_PATH.exists():
return {
"generated_at": dt.datetime.now().isoformat(),
"summary": {"total": 0, "ok": 0, "error": 0, "timeout": 0, "held": 0},
"results": [],
}
return json.loads(OUT_PATH.read_text(encoding="utf-8"))
def shell_join(cmd: list[str]) -> str:
return " ".join(shlex.quote(x) for x in cmd)
def main() -> int:
p = argparse.ArgumentParser()
p.add_argument("--service", required=True)
p.add_argument("--command", required=True)
p.add_argument("--risk", default="write")
p.add_argument("--method", default="real-backend-manual-write; no --mock; no --dry-run")
p.add_argument("--timeout", type=int, default=35)
p.add_argument("argv", nargs=argparse.REMAINDER)
ns = p.parse_args()
argv = ns.argv
if argv and argv[0] == "--":
argv = argv[1:]
if not argv:
raise SystemExit("missing command argv")
started = time.time()
status = "real-error"
stdout = ""
stderr = ""
exit_code = None
try:
proc = subprocess.run(argv, text=True, capture_output=True, timeout=ns.timeout)
stdout = proc.stdout.strip()
stderr = proc.stderr.strip()
exit_code = proc.returncode
status = classify_real_status(exit_code, stdout)
except subprocess.TimeoutExpired as e:
status = "timeout"
if isinstance(e.stdout, str):
stdout = e.stdout.strip()
if isinstance(e.stderr, str):
stderr = e.stderr.strip()
duration_ms = int((time.time() - started) * 1000)
report = load_report()
# Replace previous result for the same shortcut only when this manual run is
# more specific than a broad held/negative placeholder.
result = {
"service": ns.service,
"command": ns.command,
"risk": ns.risk,
"method": ns.method,
"status": status,
"input": shell_join(argv),
"args": argv[1:],
"stdout": stdout,
"stderr": stderr,
"exit_code": exit_code,
"duration_ms": duration_ms,
"recorded_at": dt.datetime.now().isoformat(),
}
category, fixability, note = classify_failure(result)
if category != "passed":
result["failure_category"] = category
result["fixability"] = fixability
result["diagnosis"] = note
old = report.get("results", [])
old = [r for r in old if not (r.get("service") == ns.service and r.get("command") == ns.command)]
old.append(result)
report["results"] = old
report["summary"] = summarize_results(old, include_held=True)
report["failure_categories"] = summarize_failure_categories(old)
report["generated_at"] = dt.datetime.now().isoformat()
OUT_PATH.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
print(json.dumps(result, ensure_ascii=False, indent=2))
return 0 if status == "real-ok" else 2
if __name__ == "__main__":
raise SystemExit(main())
+479
View File
@@ -0,0 +1,479 @@
#!/usr/bin/env python3
"""Run every read shortcut against the real DWS backend.
This launches the built CLI once per read shortcut. It does not use --mock and
does not use --dry-run. Inputs are synthetic but realistic where a shortcut
expects a name, date, or query; resource identifiers remain test placeholders
when no resource has been created for that command.
"""
from __future__ import annotations
import datetime as dt
import argparse
import json
import os
import shlex
import subprocess
import time
from pathlib import Path
from shortcut_real_result import (
classify_failure,
sanitize_result,
summarize_failure_categories,
classify_real_status,
summarize_results,
)
ROOT = Path(__file__).resolve().parents[1]
MATRIX_PATH = Path("/private/tmp/dws-shortcut-matrix.json")
OUT_PATH = ROOT / "docs" / "shortcut-real-read-results.json"
BIN = os.environ.get("DWS_REAL_TEST_BIN", "/private/tmp/dws-real-test")
DEVAPP_FIXTURE_ID = "678f27ec-4339-49d8-9c49-371b284bf552"
DEVAPP_FIXTURE_VERSION_ID = "4743accb-45e2-4bc9-8c96-74fc62ace2e8"
CHAT_FIXTURE_OPEN_CONVERSATION_ID = "cid3Jijzhe2aqs9ysOXjhi05g=="
CHAT_FIXTURE_GROUP_NAME = "浅曦-kida,Dennis,秋画"
CHAT_FIXTURE_DM_OPEN_CONVERSATION_ID = "cidie1367hAfBxqipzE59k5sknHLrHmvYkw98NADhfnjPI="
CHAT_FIXTURE_OPEN_MESSAGE_ID = "msgEuOor1PmFBNlx9M06N9z1Q=="
CHAT_FIXTURE_OPEN_TASK_ID = "y/wM6Lo+9GbIqtILYPv1BZDcMW+2FgnqskgcpdOiMdM="
CALENDAR_FIXTURE_EVENT_ID = "THN4YUtOTlplYU9sZzd2czE4YURLQT09_1784078100000"
DOC_FIXTURE_NODE_ID = "P0MALyR8knpgo9GycY7ZlMxlJ3bzYmDO"
DOC_FIXTURE_FOLDER_ID = "Amq4vjg89ZOAdqyaSMKpApXdW3kdP0wQ"
DOC_FIXTURE_EXPORT_JOB_ID = "29346731713"
SHEET_FIXTURE_NODE_ID = "mweZ92PV6O36dZbnsMZx70ylJxEKBD6p"
DING_FIXTURE_OPEN_DING_ID = "5D73E3AC29C780072D1CD56C6874ACC2"
CONTACT_FIXTURE_MOBILE = "13161187007"
AITABLE_FIXTURE_BASE_ID = "gpG2NdyVXQyZ0OmoSbd1vbA6JMwvDqPk"
AITABLE_FIXTURE_TABLE_ID = "hERWDMS"
AITABLE_FIXTURE_VIEW_ID = "qvGDAH2"
AITABLE_FIXTURE_FORM_VIEW_ID = "lmeV1cb"
AITABLE_FIXTURE_FIELD_ID = "01ZM8y7"
AITABLE_FIXTURE_RECORD_ID = "1015oH3OXy"
AITABLE_FIXTURE_DASHBOARD_ID = "KY9tlWg5NEHgfs8WT6IO2"
AITABLE_FIXTURE_CHART_ID = "widget-dlxFo0tNSImp4ITpDn5fQ"
HELD_CASES = {
("devapp", "+credentials-get"):
"该命令会读取真实应用凭证/密钥;不能用真实 app 自动执行。当前仅用占位 ID 验证负向路径,真实成功需人工在安全环境单独确认。",
}
def ensure_matrix() -> dict:
env = os.environ.copy()
env.setdefault("GOCACHE", "/private/tmp/dws_gocache")
raw = subprocess.check_output(
["go", "run", "./scripts/gen_shortcut_test_matrix.go"],
cwd=ROOT,
env=env,
text=True,
)
MATRIX_PATH.write_text(raw, encoding="utf-8")
return json.loads(raw)
def replace_flag_values(args: list[str], service: str, command: str) -> list[str]:
today = dt.date.today()
start = (dt.datetime.now() + dt.timedelta(days=1)).replace(hour=10, minute=0, second=0, microsecond=0).isoformat() + "+08:00"
end = (dt.datetime.now() + dt.timedelta(days=1)).replace(hour=11, minute=0, second=0, microsecond=0).isoformat() + "+08:00"
no_id = "DWSREALREADNOSUCHID0000000000000"
no_conv = "cidDWSREALREADNOSUCHCONV"
day = str(today)
yesterday = str(today - dt.timedelta(days=1))
datetime_start = dt.datetime.now().replace(hour=10, minute=0, second=0, microsecond=0).strftime("%Y-%m-%d %H:%M:%S")
datetime_end = (dt.datetime.now() + dt.timedelta(hours=1)).replace(minute=0, second=0, microsecond=0).strftime("%Y-%m-%d %H:%M:%S")
replacements = {
"name": "DWS shortcut 真实测试",
"query": "测试",
"keyword": "测试",
"q": "测试",
"text": "测试",
"title": "测试",
"phone": "13000000000",
"mobile": "13000000000",
"user": "冬翔",
"users": "冬翔",
"to": "冬翔",
"with": "冬翔",
"who": "冬翔",
"dept": "模型算法",
"dept-id": "842379556",
"department-id": "842379556",
"start": start,
"end": end,
"from": str(today - dt.timedelta(days=7)),
"to-date": str(today),
"date": str(today),
"time": datetime_start,
"days": "7",
"types": "leave",
"columns": "1001",
"limit": "10",
"page": "1",
"page-size": "10",
"cursor": "0",
"calendar-id": "primary",
"event": no_id,
"type": "ALL",
"types": "leave",
"columns": "1001",
"role-types": "executor",
"status": "false",
"artifacts": "basic",
"direction": "older",
"file-types": "alidoc",
"order-by": "name",
"order": "asc",
"space-id": "1",
"space": "测试",
"base": no_id,
"base-id": no_id,
"table": no_id,
"table-id": no_id,
"view-id": no_id,
"record-id": no_id,
"record-ids": no_id,
"field-id": no_id,
"dashboard-id": no_id,
"chart-id": no_id,
"workflow-id": no_id,
"node": no_id,
"doc": no_id,
"folder": no_id,
"workspace": no_id,
"group": no_conv,
"conversation-id": no_conv,
"open-conversation-id": no_conv,
"message-id": no_id,
"msg-id": no_id,
"id": no_id,
"session-id": no_id,
"process-instance-id": no_id,
"task-id": no_id,
"template-id": no_id,
"mail-id": no_id,
"filters": "{}",
"sort": "[]",
}
if service == "calendar" and command == "+free-slots":
replacements["from"] = "9"
replacements["to"] = "18"
if service == "calendar":
replacements["event"] = CALENDAR_FIXTURE_EVENT_ID
replacements["cursor"] = ""
if command == "+freebusy":
replacements["users"] = "103262"
if service == "doc" and command == "+comment-list":
replacements["type"] = "global"
if service == "doc":
replacements["node"] = DOC_FIXTURE_NODE_ID
replacements["doc"] = DOC_FIXTURE_NODE_ID
replacements["folder"] = DOC_FIXTURE_FOLDER_ID
replacements["job-id"] = DOC_FIXTURE_EXPORT_JOB_ID
if service == "drive":
replacements["node"] = DOC_FIXTURE_NODE_ID
replacements["folder"] = DOC_FIXTURE_FOLDER_ID
if service == "todo":
replacements["task-id"] = "55119034912"
replacements["size"] = "10"
if service == "aitable":
replacements["name"] = "Real共创版设备去向登记"
replacements["base"] = AITABLE_FIXTURE_BASE_ID
replacements["base-id"] = AITABLE_FIXTURE_BASE_ID
replacements["table"] = AITABLE_FIXTURE_TABLE_ID
replacements["table-id"] = AITABLE_FIXTURE_TABLE_ID
replacements["view-id"] = AITABLE_FIXTURE_VIEW_ID
replacements["view-ids"] = AITABLE_FIXTURE_VIEW_ID
replacements["field-id"] = AITABLE_FIXTURE_FIELD_ID
replacements["field-ids"] = AITABLE_FIXTURE_FIELD_ID
replacements["record-id"] = AITABLE_FIXTURE_RECORD_ID
replacements["record-ids"] = AITABLE_FIXTURE_RECORD_ID
replacements["dashboard-id"] = AITABLE_FIXTURE_DASHBOARD_ID
replacements["chart-id"] = AITABLE_FIXTURE_CHART_ID
if command.startswith("+form-"):
replacements["view-id"] = AITABLE_FIXTURE_FORM_VIEW_ID
if command == "+resolve-table":
replacements["name"] = "Mac Mini"
if service == "ding" and command == "+list":
replacements["type"] = "ALL"
if service == "ding" and command == "+receiver-status":
replacements["ding-id"] = DING_FIXTURE_OPEN_DING_ID
if service == "contact" and command == "+list-sub-depts":
replacements["dept"] = "842379556"
if service == "contact":
replacements["name"] = "冬翔"
if command == "+by-mobile":
replacements["mobile"] = CONTACT_FIXTURE_MOBILE
if command == "+resolve-dept":
replacements["name"] = "模型算法"
if service == "oa":
now_ms = int(dt.datetime.now().timestamp() * 1000)
replacements["start"] = str(now_ms - 7 * 24 * 60 * 60 * 1000)
replacements["end"] = str(now_ms)
replacements["page"] = "1"
replacements["limit"] = "10"
if service == "report":
replacements["start"] = (dt.datetime.now() - dt.timedelta(days=7)).replace(microsecond=0).isoformat() + "+08:00"
replacements["end"] = dt.datetime.now().replace(microsecond=0).isoformat() + "+08:00"
replacements["modified-start"] = replacements["start"]
replacements["modified-end"] = replacements["end"]
if service == "attendance":
replacements.update({
"user": "202397",
"users": "202397",
"staff-ids": "202397",
"operator-staff-id": "202397",
"leave-code": "731ed089-62ff-4734-a6c7-3c8fcc8294fc",
"leave-names": "年假",
"start": yesterday,
"end": day,
"from": yesterday,
"to-date": day,
"date": day,
})
if command in {"+get-checkin-record", "+query-report-data", "+query-report-leave"}:
replacements["start"] = datetime_start
replacements["end"] = datetime_end
if command == "+get-approve-template":
replacements["type"] = "leave"
if command == "+search-group":
replacements["type"] = "FIXED"
if service == "devapp":
replacements["unified-app-id"] = DEVAPP_FIXTURE_ID
replacements["version-id"] = DEVAPP_FIXTURE_VERSION_ID
replacements["cursor"] = ""
if service == "mail":
replacements["email"] = "xinyang.dxy@alibaba-inc.com"
replacements["folder"] = "2"
replacements["query"] = "subject:测试"
replacements["keyword"] = "董鑫阳"
replacements["employee-no"] = "202397"
replacements["size"] = "10"
replacements["cursor"] = ""
if command == "+find-mail-user":
replacements["query"] = "董鑫阳"
if service == "chat":
replacements["group"] = CHAT_FIXTURE_OPEN_CONVERSATION_ID
replacements["conversation-id"] = CHAT_FIXTURE_OPEN_CONVERSATION_ID
replacements["open-conversation-id"] = CHAT_FIXTURE_OPEN_CONVERSATION_ID
replacements["msg-ids"] = CHAT_FIXTURE_OPEN_MESSAGE_ID
replacements["message-id"] = CHAT_FIXTURE_OPEN_MESSAGE_ID
replacements["msg-id"] = CHAT_FIXTURE_OPEN_MESSAGE_ID
replacements["open-task-id"] = CHAT_FIXTURE_OPEN_TASK_ID
if command == "+group-members":
replacements["group"] = CHAT_FIXTURE_GROUP_NAME
if command == "+messages-read-status":
replacements["conversation-id"] = CHAT_FIXTURE_DM_OPEN_CONVERSATION_ID
replacements["users"] = "冬翔"
if command == "+messages-resource-url":
replacements["type"] = "mediaId"
if command == "+bot-find":
replacements["cursor"] = ""
if service == "sheet" and command == "+list-sheets":
replacements["node"] = SHEET_FIXTURE_NODE_ID
out = list(args)
if "--format" not in out:
out.extend(["--format", "json"])
i = 0
while i < len(out) - 1:
if out[i].startswith("--"):
key = out[i][2:]
if key in replacements and not out[i + 1].startswith("--"):
out[i + 1] = replacements[key]
i += 2
continue
i += 1
return out
def shell_join(cmd: list[str]) -> str:
return " ".join(shlex.quote(x) for x in cmd)
def drop_flag(args: list[str], flag: str) -> list[str]:
out: list[str] = []
i = 0
while i < len(args):
if args[i] == flag:
i += 2 if i + 1 < len(args) and not args[i + 1].startswith("--") else 1
continue
out.append(args[i])
i += 1
return out
def adjust_command_args(args: list[str], service: str, command: str) -> list[str]:
if service == "chat" and command == "+chat-messages":
return drop_flag(args, "--user")
if service == "chat" and command == "+messages-list-direct":
out = drop_flag(args, "--open-dingtalk-id")
for i in range(len(out) - 1):
if out[i] == "--user":
out[i + 1] = "103262"
return out
if service == "calendar" and command == "+freebusy":
return drop_flag(args, "--rooms")
if service == "devapp" and command == "+permission-list":
out = drop_flag(args, "--scope-value")
out = drop_flag(out, "--scope-type")
out = drop_flag(out, "--api-status")
for i in range(len(out) - 1):
if out[i] == "--auth-status":
out[i + 1] = "ALL"
return out
if service == "doc" and command == "+search":
out = drop_flag(args, "--extensions")
out = drop_flag(out, "--created-from")
out = drop_flag(out, "--created-to")
out = drop_flag(out, "--visited-from")
out = drop_flag(out, "--visited-to")
out = drop_flag(out, "--creator-uids")
out = drop_flag(out, "--editor-uids")
out = drop_flag(out, "--mentioned-uids")
out = drop_flag(out, "--workspace-ids")
return out
if service == "doc" and command == "+comment-list":
out = drop_flag(args, "--cursor")
out = drop_flag(out, "--resolve-status")
return out
if service == "doc" and command == "+list":
out = drop_flag(args, "--workspace")
out = drop_flag(out, "--cursor")
return out
if service == "report" and command == "+outbox-list":
return drop_flag(args, "--template-name")
return args
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--service", action="append", help="Only run shortcuts from this service; may repeat")
parser.add_argument("--command", action="append", help="Only run this shortcut command; may repeat")
parser.add_argument("--failed-only", action="store_true", help="Only rerun shortcuts currently marked non-success in the output report")
ns = parser.parse_args()
services = set(ns.service or [])
commands = set(ns.command or [])
matrix = ensure_matrix()
rows = [r for r in matrix["results"] if r.get("risk") == "read"]
if services:
rows = [r for r in rows if r["service"] in services]
if commands:
rows = [r for r in rows if r["command"] in commands]
if ns.failed_only and OUT_PATH.exists():
existing = json.loads(OUT_PATH.read_text(encoding="utf-8"))
failed = {
(r.get("service"), r.get("command"))
for r in existing.get("results", [])
if r.get("status") != "real-ok"
}
rows = [r for r in rows if (r["service"], r["command"]) in failed]
results = []
summary = {"total": len(rows), "ok": 0, "error": 0, "timeout": 0, "held": 0}
for idx, r in enumerate(rows, 1):
key = (r["service"], r["command"])
if key in HELD_CASES:
summary["held"] += 1
item = {
"service": r["service"],
"command": r["command"],
"risk": r["risk"],
"method": "held; sensitive credential read",
"status": "held",
"input": "",
"args": [],
"stdout": "",
"stderr": HELD_CASES[key],
"exit_code": None,
"duration_ms": 0,
}
item = sanitize_result(item)
category, fixability, note = classify_failure(item)
item["failure_category"] = category
item["fixability"] = fixability
item["diagnosis"] = note
results.append(item)
continue
args = adjust_command_args(replace_flag_values(r["args"], r["service"], r["command"]), r["service"], r["command"])
cmd = [BIN] + args
started = time.time()
status = "real-error"
stdout = ""
stderr = ""
exit_code = None
try:
p = subprocess.run(cmd, text=True, capture_output=True, timeout=30)
stdout = p.stdout.strip()
stderr = p.stderr.strip()
exit_code = p.returncode
status = classify_real_status(exit_code, stdout)
if status == "real-ok":
status = "real-ok"
summary["ok"] += 1
else:
summary["error"] += 1
except subprocess.TimeoutExpired as e:
status = "timeout"
summary["timeout"] += 1
if isinstance(e.stdout, str):
stdout = e.stdout.strip()
if isinstance(e.stderr, str):
stderr = e.stderr.strip()
duration_ms = int((time.time() - started) * 1000)
item = {
"service": r["service"],
"command": r["command"],
"risk": r["risk"],
"method": "real-backend-read; no --mock; no --dry-run",
"status": status,
"input": shell_join(cmd),
"args": args,
"stdout": stdout,
"stderr": stderr,
"exit_code": exit_code,
"duration_ms": duration_ms,
}
item = sanitize_result(item)
category, fixability, note = classify_failure(item)
if category != "passed":
item["failure_category"] = category
item["fixability"] = fixability
item["diagnosis"] = note
results.append(item)
if idx % 25 == 0 or idx == len(rows):
print(
f"progress {idx}/{len(rows)} ok={summary['ok']} "
f"error={summary['error']} timeout={summary['timeout']} held={summary['held']}",
flush=True,
)
if not services and not commands and not ns.failed_only:
OUT_PATH.write_text(json.dumps({
"generated_at": dt.datetime.now().isoformat(),
"summary": summary,
"failure_categories": summarize_failure_categories(results),
"results": results,
}, ensure_ascii=False, indent=2), encoding="utf-8")
if OUT_PATH.exists() and (services or commands or ns.failed_only):
existing = json.loads(OUT_PATH.read_text(encoding="utf-8"))
replace_keys = {(r["service"], r["command"]) for r in results}
merged = [
r for r in existing.get("results", [])
if (r.get("service"), r.get("command")) not in replace_keys
]
merged.extend(results)
else:
merged = results
summary = summarize_results(merged, include_held=True)
OUT_PATH.write_text(json.dumps({
"generated_at": dt.datetime.now().isoformat(),
"summary": summary,
"failure_categories": summarize_failure_categories(merged),
"results": merged,
}, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"saved {OUT_PATH} batch={summarize_results(results, include_held=True)} merged={summary}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+455
View File
@@ -0,0 +1,455 @@
#!/usr/bin/env python3
"""Run write/high-risk shortcuts against the real CLI with safe negative inputs.
This is intentionally not a mock and not a dry-run. It launches the built CLI
for every write/high-risk shortcut and records the real stdout/stderr/exit code.
To avoid broad irreversible side effects, target identifiers and people are
rewritten to obviously-nonexistent test values. Commands that have no
resource/user target and would necessarily mutate the current account state are
held and recorded rather than executed.
"""
from __future__ import annotations
import datetime as dt
import argparse
import json
import os
import shlex
import subprocess
import time
from pathlib import Path
from shortcut_real_result import (
classify_failure,
sanitize_result,
summarize_failure_categories,
classify_real_status,
summarize_results,
)
ROOT = Path(__file__).resolve().parents[1]
MATRIX_PATH = Path("/private/tmp/dws-shortcut-matrix.json")
OUT_PATH = ROOT / "docs" / "shortcut-real-write-results.json"
BIN = os.environ.get("DWS_REAL_TEST_BIN", "/private/tmp/dws-real-test")
NO_SAFE_NEGATIVE_TARGET = {
("chat", "+conversation-clear-all-red-point"):
"该命令没有目标 ID,会直接修改当前账号所有会话红点状态;需要逐项授权后单独跑。",
("chat", "+category-create"):
"该命令只需要标题,真实执行会在当前账号创建会话分类;需要逐项授权后单独跑。",
}
def ensure_matrix() -> dict:
if not MATRIX_PATH.exists():
env = os.environ.copy()
env.setdefault("GOCACHE", "/private/tmp/dws_gocache")
raw = subprocess.check_output(
["go", "run", "./scripts/gen_shortcut_test_matrix.go"],
cwd=ROOT,
env=env,
text=True,
)
MATRIX_PATH.write_text(raw, encoding="utf-8")
return json.loads(MATRIX_PATH.read_text(encoding="utf-8"))
def replace_flag_values(args: list[str], now: str, service: str = "", command: str = "") -> list[str]:
future_start = (
dt.datetime.now() + dt.timedelta(days=7)
).replace(hour=10, minute=0, second=0, microsecond=0).isoformat() + "+08:00"
future_end = (
dt.datetime.now() + dt.timedelta(days=7)
).replace(hour=10, minute=15, second=0, microsecond=0).isoformat() + "+08:00"
no_user = "__DWS_SHORTCUT_REAL_TEST_NO_SUCH_USER__"
no_id = "DWSREALTESTNOSUCHID0000000000000"
no_conv = "cidDWSREALTESTNOSUCHCONV"
huge_num = "999999999999"
text = f"DWS shortcut 真实测试 {now},安全负向输入,请忽略。"
replacements = {
# People / users.
"to": no_user,
"with": no_user,
"owner": no_user,
"target": no_user,
"user": no_user,
"users": no_user,
"user-ids": no_user,
"new-owner": no_user,
"applicant": no_user,
"inviter": no_user,
"approver-user-id": no_user,
"add-users": no_user,
"remove-users": "",
"add-extra-users": "",
"remove-extra-users": "",
"at-user-ids": no_user,
"at-users": no_user,
"at-open-dingtalk-ids": "",
"open-dingtalk-ids": "",
# Conversation / message / Ding targets.
"group": no_conv,
"id": no_id,
"conversation-id": no_conv,
"open-conversation-id": no_conv,
"src-conversation-id": no_conv,
"dest-conversation-id": no_conv,
"message-id": no_id,
"msg-id": no_id,
"msg-ids": no_id,
"src-msg-id": no_id,
"src-thread-id": no_id,
"keys": no_id,
"biz-id": no_id,
"category-ids": huge_num,
"receiver": no_user,
"robot-code": "__DWS_SHORTCUT_REAL_TEST_NO_SUCH_ROBOT__",
"bot-id": no_id,
"token": "__DWS_SHORTCUT_REAL_TEST_NO_SUCH_WEBHOOK_TOKEN__",
# DingTalk document / app / aitable / wiki ids.
"base-id": no_id,
"base": no_id,
"table-id": no_id,
"table": no_id,
"field-id": no_id,
"record-id": huge_num,
"record-ids": no_id,
"view-id": no_id,
"dashboard-id": no_id,
"chart-id": no_id,
"workflow-id": no_id,
"role-id": no_id,
"role-ids": no_id,
"section-id": no_id,
"parent-section-id": no_id,
"new-parent-section-id": no_id,
"node-id": no_id,
"node": no_id,
"doc": no_id,
"folder": no_id,
"workspace": no_id,
"template-id": no_id,
"comment-key": no_id,
"block-id": no_id,
"unified-app-id": no_id,
"icon-media-id": no_id,
"media-id": no_id,
"session-id": no_id,
"import-id": no_id,
"space": "__DWS_SHORTCUT_REAL_TEST_NO_SUCH_SPACE__",
# Numeric identifiers.
"category-id": huge_num,
"record-id-num": huge_num,
"record-id-int": huge_num,
"class-id": huge_num,
"group-id": huge_num,
"plan-id": huge_num,
"result-id": huge_num,
# Text-like values.
"title": f"DWS shortcut 真实测试 {now}",
"name": f"DWS shortcut 真实测试 {now}",
"new-name": f"DWS shortcut 真实测试 {now}",
"alias-title": f"DWS shortcut 真实测试 {now}",
"nick": f"DWS测试{now}",
"desc": "DWS shortcut 真实测试描述,可删除",
"description": "DWS shortcut 真实测试描述,可删除",
"field-description": "DWS shortcut 真实测试字段说明",
"text": text,
"content": text,
"comment": text,
"note": text,
"remark": text,
"selected-text": "DWS测试",
"task": f"DWS shortcut 真实测试待办 {now}",
"keyword": f"__DWS_SHORTCUT_REAL_TEST_NO_SUCH_APPROVAL_{now}__",
"reason": "DWS shortcut 真实测试安全负向清理",
"uuid": f"dws-shortcut-real-negative-{now}",
# Times.
"due": future_end,
"at": future_start,
"start": future_start,
"end": future_end,
"time": future_start,
# Structured values.
"config": "{}",
"layout": "{}",
"json": "{}",
"records": "[]",
"ai-config": "{}",
"field-mapping": "{}",
"class-vo": '{"sections":[{"times":[{"checkType":"OnDuty","checkTime":"09:00","across":0},{"checkType":"OffDuty","checkTime":"18:00","across":0}]}]}',
"group-vo": '{"defaultClassId":999999999999,"workDayClassList":[999999999999,999999999999,999999999999,999999999999,999999999999,0,0]}',
"visibility-rules": '[{"type":"dept","visible":["-1"]}]',
"sub-roles": "[]",
"skills": "[]",
"scope-values": "[]",
"event-codes": "[]",
"schedules": '[{"userId":"202397","workDate":"2026-07-16 09:00:00","classId":999999999999,"isRest":"N"}]',
"class-ids": "[999999999999]",
"add-depts": "",
"remove-depts": "",
# URLs / app config.
"homepage-url": "https://example.invalid/dws-shortcut-real-test",
"pc-homepage-url": "https://example.invalid/dws-shortcut-real-test",
"omp-url": "https://example.invalid/dws-shortcut-real-test",
"outgoing-url": "https://example.invalid/dws-shortcut-real-test",
"event-callback-url": "https://example.invalid/dws-shortcut-real-test",
"redirect-urls": "https://example.invalid/dws-shortcut-real-test",
"sso-urls": "https://example.invalid/dws-shortcut-real-test",
"ip-whitelist": "127.0.0.1",
"url": "https://example.invalid/dws-shortcut-real-test",
# Misc.
"type": "FIXED",
"record-name-key": "task",
"file-name": f"dws-shortcut-real-{now}.txt",
"mime-type": "text/plain",
"version": "0.0.1-dws-real-test",
"version-id": no_id,
"member-type": "USER",
"emotion-id": no_id,
"emotion-name": "DWS测试表情",
"background-id": no_id,
"emoji": "👍",
"pair": "测试=>测试",
"leave-code": no_id,
"num": "1",
}
if service == "chat" and command == "+messages-send-card":
replacements["receiver"] = "103262"
if service == "chat" and command == "+chat-audit-join":
replacements["applicant"] = "103262"
replacements["inviter"] = "519019"
if service == "doc" and command == "+comment-create-inline":
replacements["start"] = "0"
replacements["end"] = "1"
if service == "attendance" and command == "+save-leave-balance":
replacements["start"] = (dt.date.today() + dt.timedelta(days=1)).isoformat()
replacements["end"] = (dt.date.today() + dt.timedelta(days=30)).isoformat()
if service == "aitable" and command == "+record-update":
replacements["records"] = '[{"recordId":"recDWSREALTEST","cells":{}}]'
if service == "aitable" and command == "+view-update":
replacements["desc"] = "{}"
out = list(args)
if "--format" not in out:
out.extend(["--format", "json"])
i = 0
while i < len(out) - 1:
if out[i].startswith("--"):
key = out[i][2:]
if key in replacements and not out[i + 1].startswith("--"):
out[i + 1] = replacements[key]
i += 2
continue
i += 1
return out
def shell_join(cmd: list[str]) -> str:
return " ".join(shlex.quote(x) for x in cmd)
def drop_flag(args: list[str], flag: str) -> list[str]:
out: list[str] = []
i = 0
while i < len(args):
if args[i] == flag:
i += 2 if i + 1 < len(args) and not args[i + 1].startswith("--") else 1
continue
out.append(args[i])
i += 1
return out
def adjust_command_args(args: list[str], service: str, command: str) -> list[str]:
if service == "chat" and command == "+messages-send-card":
# --group and --receiver are mutually exclusive; exercise the direct-message path.
return drop_flag(args, "--group")
return args
def setup_prerequisite(service: str, command: str, now: str) -> dict | None:
if service == "todo" and command == "+todo-done":
title = f"DWS shortcut 真实测试待办 {now}"
due = (
dt.datetime.now() + dt.timedelta(days=7)
).replace(hour=10, minute=15, second=0, microsecond=0).isoformat() + "+08:00"
cmd = [
BIN,
"todo",
"+remind",
"--task",
title,
"--at",
due,
"--yes",
"--format",
"json",
]
started = time.time()
p = subprocess.run(cmd, text=True, capture_output=True, timeout=25)
return {
"purpose": "为 todo +todo-done 创建当前账号自己的临时待办 fixture",
"input": shell_join(cmd),
"stdout": p.stdout.strip(),
"stderr": p.stderr.strip(),
"exit_code": p.returncode,
"duration_ms": int((time.time() - started) * 1000),
}
return None
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--service", action="append", help="Only run shortcuts from this service; may repeat")
parser.add_argument("--command", action="append", help="Only run this shortcut command; may repeat")
parser.add_argument("--missing-only", action="store_true", help="Skip shortcuts already present in the output report")
parser.add_argument("--failed-only", action="store_true", help="Only rerun shortcuts currently marked non-success in the output report")
ns = parser.parse_args()
services = set(ns.service or [])
commands = set(ns.command or [])
matrix = ensure_matrix()
rows = [r for r in matrix["results"] if r.get("risk") != "read"]
if services:
rows = [r for r in rows if r["service"] in services]
if commands:
rows = [r for r in rows if r["command"] in commands]
if ns.failed_only and OUT_PATH.exists():
existing = json.loads(OUT_PATH.read_text(encoding="utf-8"))
failed = {
(r.get("service"), r.get("command"))
for r in existing.get("results", [])
if r.get("status") != "real-ok"
}
rows = [r for r in rows if (r["service"], r["command"]) in failed]
if ns.missing_only and OUT_PATH.exists():
existing = json.loads(OUT_PATH.read_text(encoding="utf-8"))
done = {(r.get("service"), r.get("command")) for r in existing.get("results", [])}
rows = [r for r in rows if (r["service"], r["command"]) not in done]
now = dt.datetime.now().strftime("%Y%m%d-%H%M%S")
results = []
batch_summary = {"total": len(rows), "ok": 0, "error": 0, "timeout": 0, "held": 0}
for idx, r in enumerate(rows, 1):
key = (r["service"], r["command"])
if key in NO_SAFE_NEGATIVE_TARGET:
batch_summary["held"] += 1
item = {
"service": r["service"],
"command": r["command"],
"risk": r["risk"],
"method": "held; no safe negative target",
"status": "held",
"input": "",
"args": [],
"stdout": "",
"stderr": NO_SAFE_NEGATIVE_TARGET[key],
"exit_code": None,
"duration_ms": 0,
}
item = sanitize_result(item)
category, fixability, note = classify_failure(item)
item["failure_category"] = category
item["fixability"] = fixability
item["diagnosis"] = note
results.append(item)
continue
setup = None
try:
setup = setup_prerequisite(r["service"], r["command"], now)
except subprocess.TimeoutExpired as e:
setup = {
"purpose": "创建前置真实资源超时",
"input": "",
"stdout": e.stdout.strip() if isinstance(e.stdout, str) else "",
"stderr": e.stderr.strip() if isinstance(e.stderr, str) else "",
"exit_code": None,
"duration_ms": 25000,
}
args = adjust_command_args(replace_flag_values(r["args"], now, r["service"], r["command"]), r["service"], r["command"])
cmd = [BIN] + args
started = time.time()
status = "real-error"
stdout = ""
stderr = ""
exit_code = None
try:
p = subprocess.run(cmd, text=True, capture_output=True, timeout=25)
stdout = p.stdout.strip()
stderr = p.stderr.strip()
exit_code = p.returncode
status = classify_real_status(exit_code, stdout)
if status == "real-ok":
status = "real-ok"
batch_summary["ok"] += 1
else:
batch_summary["error"] += 1
except subprocess.TimeoutExpired as e:
status = "timeout"
batch_summary["timeout"] += 1
if isinstance(e.stdout, str):
stdout = e.stdout.strip()
if isinstance(e.stderr, str):
stderr = e.stderr.strip()
duration_ms = int((time.time() - started) * 1000)
item = {
"service": r["service"],
"command": r["command"],
"risk": r["risk"],
"method": "real-backend-safe-negative-write; no --mock; no --dry-run",
"status": status,
"input": shell_join(cmd),
"args": args,
"stdout": stdout,
"stderr": stderr,
"exit_code": exit_code,
"duration_ms": duration_ms,
}
if setup is not None:
item["setup"] = setup
item = sanitize_result(item)
category, fixability, note = classify_failure(item)
if category != "passed":
item["failure_category"] = category
item["fixability"] = fixability
item["diagnosis"] = note
results.append(item)
if idx % 10 == 0 or idx == len(rows):
print(
f"progress {idx}/{len(rows)} ok={batch_summary['ok']} "
f"error={batch_summary['error']} timeout={batch_summary['timeout']} held={batch_summary['held']}",
flush=True,
)
if OUT_PATH.exists():
existing = json.loads(OUT_PATH.read_text(encoding="utf-8"))
else:
existing = {"results": []}
replace_keys = {(r["service"], r["command"]) for r in results}
merged = [
r for r in existing.get("results", [])
if (r.get("service"), r.get("command")) not in replace_keys
]
merged.extend(results)
summary = summarize_results(merged, include_held=True)
failure_categories = summarize_failure_categories(merged)
OUT_PATH.write_text(json.dumps({
"generated_at": dt.datetime.now().isoformat(),
"summary": summary,
"failure_categories": failure_categories,
"results": merged,
}, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"saved {OUT_PATH} batch={batch_summary} merged={summary}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+325
View File
@@ -0,0 +1,325 @@
#!/usr/bin/env python3
"""Helpers for classifying real shortcut CLI executions.
Some DWS helper paths can exit 0 while returning a JSON error envelope on
stdout. Real-test reports should reflect the backend payload, not just the
process exit code.
"""
from __future__ import annotations
import json
import re
from typing import Any
def _json_values(text: str) -> list[Any]:
"""Parse one or more adjacent JSON values from stdout."""
values: list[Any] = []
text = (text or "").strip()
if not text:
return values
decoder = json.JSONDecoder()
idx = 0
length = len(text)
while idx < length:
while idx < length and text[idx].isspace():
idx += 1
if idx >= length:
break
try:
value, next_idx = decoder.raw_decode(text, idx)
except json.JSONDecodeError:
return values
values.append(value)
idx = next_idx
return values
def _truthy_error(value: Any) -> bool:
if value is None or value is False:
return False
if isinstance(value, str):
return value.strip() != ""
if isinstance(value, (list, dict)):
return len(value) > 0
return True
def _truthy_error_code(value: Any) -> bool:
if value is None or value is False:
return False
if isinstance(value, str):
code = value.strip()
if not code:
return False
return code.lower() not in {"0", "ok", "success", "succeed"}
if isinstance(value, (int, float)):
return value != 0
return True
def payload_indicates_error(stdout: str) -> bool:
for value in _json_values(stdout):
if not isinstance(value, dict):
continue
status = value.get("status")
if isinstance(status, str) and status.lower() == "error":
return True
if value.get("success") is False:
return True
if _truthy_error(value.get("error")):
return True
if _truthy_error_code(value.get("errorCode")):
return True
if _truthy_error_code(value.get("error_code")):
return True
if _truthy_error_code(value.get("code")):
return True
return False
def classify_real_status(exit_code: int | None, stdout: str, current_status: str | None = None) -> str:
if current_status in {"timeout", "held"}:
return current_status
if exit_code != 0:
return "real-error"
if payload_indicates_error(stdout):
return "real-error"
return "real-ok"
def _haystack(result: dict[str, Any]) -> str:
return f"{result.get('stdout') or ''}\n{result.get('stderr') or ''}".lower()
def classify_failure(result: dict[str, Any]) -> tuple[str, str, str]:
"""Return (category, fixability, note) for a real-test result."""
status = result.get("status")
if status == "real-ok":
return ("passed", "fixed", "真实后端执行成功。")
if status == "timeout":
return ("timeout", "needs-rerun", "命令超过测试超时时间;需单独复测或扩大超时。")
if status == "held":
return ("held", "manual-approval", "高风险或无安全目标,需人工逐项授权后执行。")
text = _haystack(result)
service = result.get("service")
command = result.get("command")
method = result.get("method") or ""
if "payload-classified-error" in method or (result.get("exit_code") == 0 and payload_indicates_error(result.get("stdout") or "")):
return (
"cli-error-envelope",
"cli-wrapper-fix-needed",
"进程退出码为 0,但 stdout JSON 含 error/status:error/errorCode;需要底层 helper 或 MCP 调用层把业务错误转成非零退出。",
)
if (
"not_authenticated" in text
or "auth_permission" in text
or "permission" in text
or "forbidden" in text
or "auth_error" in text
or "权限" in text
or "没有开发者身份" in text
or "无权限" in text
or "当前登录用户无权限" in text
):
return (
"auth-or-permission",
"not-cli-fixable",
"真实账号、应用 scope 或资源权限不足;CLI 只能如实暴露,不能在本仓库内修复权限。",
)
if "invalid_base_id" in text or "base_not_found" in text or "invalid source baseid" in text or "failed to resolve docid from baseid" in text:
return (
"missing-real-aitable-fixture",
"not-cli-fixable-without-fixture",
"AI 表格命令需要真实 Base/Table/View/Record 等资源;安全负向 ID 只能验证调用链,不能让后端成功。",
)
if (
"resource_not_found" in text
or "not found" in text
or "does not exist" in text
or "does not exit" in text
or "not exist" in text
or "不存在" in text
or "没找到" in text
or "no such" in text
or "not_found" in text
or "无效的会话" in text
or "opencid无效" in text
or "openconversationid无效" in text
or "openmessageid解密失败" in text
or "failed to decrypt" in text
or "invalid openmsgid" in text
or "opendingid 无效" in text
or "event does not exist" in text
or "nodeid 格式不合法" in text
):
return (
"missing-real-resource",
"not-cli-fixable-without-fixture",
"真实测试使用的资源/单据/消息/群/文档不存在;需要准备对应 fixture 后才能期望成功,不属于 shortcut 参数投影错误。",
)
if service == "calendar" and command == "+respond-event" and "organizer" in text:
return (
"backend-business-rule",
"not-cli-fixable",
"后端业务规则:日程组织者不能修改自己的参会响应;需要用非组织者账号复测。",
)
if service == "minutes" and ("ai_minutes" in text or "暂无妙记" in text):
return (
"missing-real-minutes-fixture",
"not-cli-fixable-without-fixture",
"当前账号没有满足条件的妙记/听记或录制会话;需准备真实会议产物后复测。",
)
if service == "chat" and command == "+messages-send-card" and "receiveruid" in text:
return (
"backend-or-mcp-error",
"not-cli-fixable-first",
"dry-run 已证明 CLI 装配了 receiverUid;真实后端仍报 receiverUid/openConversationId 为空,优先按 MCP schema/服务端字段映射问题处理。",
)
if service == "chat" and command == "+chat-audit-join" and "applicantuid" in text:
return (
"backend-or-mcp-error",
"not-cli-fixable-first",
"dry-run 已证明 CLI 装配了 applicantUid/inviterUid;真实后端仍报 applicantUid 缺失,优先按 MCP schema/服务端字段映射问题处理。",
)
if (
service == "chat"
and (
"opencid or cid is required" in text
or "openconversationid or cid is required" in text
or "openconversationid is required" in text
)
):
return (
"backend-or-mcp-error",
"not-cli-fixable-first",
"fake MCP 已证明 CLI 已装配会话 ID 字段;真实后端仍报 openConversationId/openCid/cid 缺失,优先按 MCP schema/服务端字段映射问题处理。",
)
if (
"system_error" in text
or "system error" in text
or "mcp_server_error" in text
or "nullpointer" in text
or "mcp_tool_error" in text
):
return (
"backend-or-mcp-error",
"not-cli-fixable-first",
"后端/MCP 服务返回内部错误;CLI 无法直接修复,但报告保留 trace/stdout 供服务端排查。",
)
if (
"参数" in text
or "param" in text
or "required" in text
or "不能为空" in text
or "invalid argument" in text
or "invalid " in text
or "validation" in text
or "json 解析失败" in text
or "格式错误" in text
):
return (
"input-or-business-validation",
"test-input-or-backend-rule",
"命令已真实进入本地/后端校验;若该项仍使用安全负向输入,则失败符合预期;若使用真实 fixture 仍失败,再作为 CLI bug 处理。",
)
return (
"unclassified-real-error",
"needs-triage",
"真实后端返回错误,当前无法自动判断归因;需结合 stdout/stderr 单独排查。",
)
def summarize_results(results: list[dict[str, Any]], include_held: bool = True) -> dict[str, int]:
summary = {"total": len(results), "ok": 0, "error": 0, "timeout": 0}
if include_held:
summary["held"] = 0
for r in results:
status = r.get("status")
if status == "real-ok":
summary["ok"] += 1
elif status == "timeout":
summary["timeout"] += 1
elif status == "held" and include_held:
summary["held"] += 1
else:
summary["error"] += 1
return summary
def summarize_failure_categories(results: list[dict[str, Any]]) -> dict[str, int]:
out: dict[str, int] = {}
for r in results:
category = r.get("failure_category")
if not category:
category, _, _ = classify_failure(r)
if category == "passed":
continue
out[category] = out.get(category, 0) + 1
return dict(sorted(out.items(), key=lambda kv: (-kv[1], kv[0])))
_OSS_URL_RE = re.compile(r"https?://[^\s\"'<>]*oss[^\s\"'<>]*", re.IGNORECASE)
_ALIBABA_ACCESS_KEY_ID_RE = re.compile(r"\bLTAI[A-Za-z0-9]{12,}\b")
_OSS_QUERY_KEY_RE = re.compile(
r"(?i)\b(OSSAccessKeyId|AccessKeyId|accessKeyId|access_key_id)=([^&\s\"'<>]+)"
)
_OSS_SIGNATURE_RE = re.compile(r"(?i)\b(Signature|security-token|x-oss-security-token)=([^&\s\"'<>]+)")
_SECRET_ASSIGN_RE = re.compile(
r'(?i)("?(?:accessKeySecret|access_key_secret|clientSecret|client_secret|appSecret|app_secret|secret)"?\s*[:=]\s*")([^"]+)(")'
)
def sanitize_text(text: str) -> str:
"""Redact secrets from real CLI outputs before writing reports.
Real backend read commands may return signed OSS URLs or app credential
fields. Reports need the shape of stdout/stderr for debugging, not live
credentials or presigned download URLs.
"""
if not text:
return text
text = _OSS_URL_RE.sub("[REDACTED_OSS_URL]", text)
text = _ALIBABA_ACCESS_KEY_ID_RE.sub("[REDACTED_ALIBABA_ACCESS_KEY_ID]", text)
text = _OSS_QUERY_KEY_RE.sub(lambda m: f"{m.group(1)}=[REDACTED_ALIBABA_ACCESS_KEY_ID]", text)
text = _OSS_SIGNATURE_RE.sub(lambda m: f"{m.group(1)}=[REDACTED_SIGNATURE]", text)
text = _SECRET_ASSIGN_RE.sub(lambda m: f"{m.group(1)}[REDACTED_SECRET]{m.group(3)}", text)
return text
def sanitize_value(value: Any) -> Any:
if isinstance(value, str):
return sanitize_text(value)
if isinstance(value, list):
return [sanitize_value(v) for v in value]
if isinstance(value, dict):
out: dict[str, Any] = {}
for key, item in value.items():
key_l = str(key).lower()
if key_l in {
"accesskeysecret",
"access_key_secret",
"clientsecret",
"client_secret",
"appsecret",
"app_secret",
"secret",
"signature",
}:
out[key] = "[REDACTED_SECRET]"
elif key_l in {"accesskeyid", "access_key_id", "ossaccesskeyid"}:
out[key] = "[REDACTED_ALIBABA_ACCESS_KEY_ID]"
elif key_l in {"resourceurl", "url", "downloadurl"} and isinstance(item, str) and "oss" in item.lower():
out[key] = "[REDACTED_OSS_URL]"
else:
out[key] = sanitize_value(item)
return out
return value
def sanitize_result(result: dict[str, Any]) -> dict[str, Any]:
return sanitize_value(result)
+11 -1
View File
@@ -9,7 +9,7 @@ cli_version: ">=1.0.15"
通过 `dws` 命令管理钉钉产品能力。
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。本期真实测试未通过的 shortcut 会从 `--help` 和 `dws shortcut list` 默认视图隐藏;Agent 不应主动推荐隐藏命令,内部排查 / 下期修复才使用 `dws shortcut list --include-hidden`。
## 严格禁止 (NEVER DO)
- 不要使用 dws 命令以外的方式操作(禁止 curl、HTTP API、浏览器)
@@ -24,6 +24,16 @@ cli_version: ">=1.0.15"
- **脚本优先**:[scripts/](./scripts/) 下的 `python scripts/<name>.py` 已封装翻页/轮询/批量逻辑,遇到对应场景(如 AI 表格批量导入导出、AI 应用创建轮询、文档创建后写内容、钉盘目录树等)**优先调用脚本**而非手写多步命令。脚本均支持 `--dry-run` 预览、`--format json` 输出,失败时回退到手动步骤
- **实时个人消息事件例外**:用户要监听消息、订阅事件、自动回复消息或事件驱动 Agent 时,必须走 `dws event consume` 长连接,不要写脚本轮询消息历史
## Shortcut 与原子命令的使用原则
`shortcut` 是对常用操作的高层封装,适合优先承担用户意图;产品参考文档和本 skill 负责判断意图、风险、跨产品流程和复杂参数,CLI 帮助负责声明当前版本真正可调用的命令。
- 用户意图可由可见 shortcut 满足时,优先使用 `dws <service> +<verb> ... --format json`。
- 调用前用 `dws <service> --help` 或 `dws shortcut list --service <service> --format json` 确认可见命令;不要凭文档或记忆猜测 shortcut。
- 如果 shortcut 没出现在默认 help / list 中,视为本期未对外 release:不要主动推荐或调用隐藏 shortcut,改用已验证的原子命令、脚本、参考流程,或说明该能力本期未开放。
- 只有在内部 CR、真实测试复盘、下期修复时,才使用 `dws shortcut list --include-hidden --format json` 查看隐藏项及 `hiddenReason`。
- shortcut 失败时按“错误处理”流程先加 `--verbose` 复查;若仍失败,不要自行换成隐藏 shortcut 兜底,应记录具体输入、输出、trace / endpoint / tool 信息。
## 产品总览
+1 -1
View File
@@ -18,7 +18,7 @@ metadata:
<!-- SAFETY_PREAMBLE_INJECT -->
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。本期真实测试未通过的 shortcut 会从 `--help` 和 `dws shortcut list` 默认视图隐藏;Agent 不应主动推荐隐藏命令,内部排查 / 下期修复才使用 `dws shortcut list --include-hidden`。
> 命令参考:[agoal.md](references/agoal.md)。
+1 -1
View File
@@ -18,7 +18,7 @@ metadata:
<!-- SAFETY_PREAMBLE_INJECT -->
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。本期真实测试未通过的 shortcut 会从 `--help` 和 `dws shortcut list` 默认视图隐藏;Agent 不应主动推荐隐藏命令,内部排查 / 下期修复才使用 `dws shortcut list --include-hidden`。
> 命令参考:[aisearch.md](references/aisearch.md)。
+1 -1
View File
@@ -18,7 +18,7 @@ metadata:
<!-- SAFETY_PREAMBLE_INJECT -->
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。本期真实测试未通过的 shortcut 会从 `--help` 和 `dws shortcut list` 默认视图隐藏;Agent 不应主动推荐隐藏命令,内部排查 / 下期修复才使用 `dws shortcut list --include-hidden`。
> 命令参考:[aitable.md](references/aitable.md);复杂命令按需加载 `references/aitable/*.md`;剧本:[06-data-analytics.md](references/06-data-analytics.md)。
+1 -1
View File
@@ -18,7 +18,7 @@ metadata:
<!-- SAFETY_PREAMBLE_INJECT -->
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。本期真实测试未通过的 shortcut 会从 `--help` 和 `dws shortcut list` 默认视图隐藏;Agent 不应主动推荐隐藏命令,内部排查 / 下期修复才使用 `dws shortcut list --include-hidden`。
> 命令参考:[attendance.md](references/attendance.md)。
+1 -1
View File
@@ -18,7 +18,7 @@ metadata:
<!-- SAFETY_PREAMBLE_INJECT -->
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。本期真实测试未通过的 shortcut 会从 `--help` 和 `dws shortcut list` 默认视图隐藏;Agent 不应主动推荐隐藏命令,内部排查 / 下期修复才使用 `dws shortcut list --include-hidden`。
> 命令参考:[calendar.md](references/calendar.md);剧本:[03-meeting.md](references/03-meeting.md)。
+1 -1
View File
@@ -18,7 +18,7 @@ metadata:
<!-- SAFETY_PREAMBLE_INJECT -->
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。本期真实测试未通过的 shortcut 会从 `--help` 和 `dws shortcut list` 默认视图隐藏;Agent 不应主动推荐隐藏命令,内部排查 / 下期修复才使用 `dws shortcut list --include-hidden`。
> 命令参考:[chat.md](references/chat.md);表情:[chat-emoji-list.md](references/chat-emoji-list.md);剧本:[01-messaging.md](references/01-messaging.md)。
+1 -1
View File
@@ -18,7 +18,7 @@ metadata:
<!-- SAFETY_PREAMBLE_INJECT -->
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。本期真实测试未通过的 shortcut 会从 `--help` 和 `dws shortcut list` 默认视图隐藏;Agent 不应主动推荐隐藏命令,内部排查 / 下期修复才使用 `dws shortcut list --include-hidden`。
> 命令参考:[contact.md](references/contact.md);剧本:[08-directory.md](references/08-directory.md)。
+1 -1
View File
@@ -18,7 +18,7 @@ metadata:
<!-- SAFETY_PREAMBLE_INJECT -->
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。本期真实测试未通过的 shortcut 会从 `--help` 和 `dws shortcut list` 默认视图隐藏;Agent 不应主动推荐隐藏命令,内部排查 / 下期修复才使用 `dws shortcut list --include-hidden`。
> 命令参考:[devdoc.md](references/devdoc.md)。
+1 -1
View File
@@ -18,7 +18,7 @@ metadata:
<!-- SAFETY_PREAMBLE_INJECT -->
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。本期真实测试未通过的 shortcut 会从 `--help` 和 `dws shortcut list` 默认视图隐藏;Agent 不应主动推荐隐藏命令,内部排查 / 下期修复才使用 `dws shortcut list --include-hidden`。
> 命令参考:[ding.md](references/ding.md)。
+1 -1
View File
@@ -18,7 +18,7 @@ metadata:
<!-- SAFETY_PREAMBLE_INJECT -->
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。本期真实测试未通过的 shortcut 会从 `--help` 和 `dws shortcut list` 默认视图隐藏;Agent 不应主动推荐隐藏命令,内部排查 / 下期修复才使用 `dws shortcut list --include-hidden`。
> 命令参考:[doc.md](references/doc.md);剧本:[04-document.md](references/04-document.md);URL 识别与类型探测:[url-patterns.md](references/url-patterns.md)。
+1 -1
View File
@@ -18,7 +18,7 @@ metadata:
<!-- SAFETY_PREAMBLE_INJECT -->
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。本期真实测试未通过的 shortcut 会从 `--help` 和 `dws shortcut list` 默认视图隐藏;Agent 不应主动推荐隐藏命令,内部排查 / 下期修复才使用 `dws shortcut list --include-hidden`。
> 命令参考:[drive.md](references/drive.md)。
+1 -1
View File
@@ -18,7 +18,7 @@ metadata:
<!-- SAFETY_PREAMBLE_INJECT -->
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。本期真实测试未通过的 shortcut 会从 `--help` 和 `dws shortcut list` 默认视图隐藏;Agent 不应主动推荐隐藏命令,内部排查 / 下期修复才使用 `dws shortcut list --include-hidden`。
> 命令参考:[live.md](references/live.md)。
+1 -1
View File
@@ -18,7 +18,7 @@ metadata:
<!-- SAFETY_PREAMBLE_INJECT -->
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。本期真实测试未通过的 shortcut 会从 `--help` 和 `dws shortcut list` 默认视图隐藏;Agent 不应主动推荐隐藏命令,内部排查 / 下期修复才使用 `dws shortcut list --include-hidden`。
> 命令参考:[mail.md](references/mail.md)。复杂搜索、附件、批量处理、草稿等多步邮件场景参考:[09-mail.md](references/09-mail.md)。
+1 -1
View File
@@ -18,7 +18,7 @@ metadata:
<!-- SAFETY_PREAMBLE_INJECT -->
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。本期真实测试未通过的 shortcut 会从 `--help` 和 `dws shortcut list` 默认视图隐藏;Agent 不应主动推荐隐藏命令,内部排查 / 下期修复才使用 `dws shortcut list --include-hidden`。
> 命令参考:[minutes.md](references/minutes.md);剧本:[07-minutes.md](references/07-minutes.md)。
+1 -1
View File
@@ -18,7 +18,7 @@ metadata:
<!-- SAFETY_PREAMBLE_INJECT -->
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。本期真实测试未通过的 shortcut 会从 `--help` 和 `dws shortcut list` 默认视图隐藏;Agent 不应主动推荐隐藏命令,内部排查 / 下期修复才使用 `dws shortcut list --include-hidden`。
> 命令参考:[oa.md](references/oa.md)。
+1 -1
View File
@@ -18,7 +18,7 @@ metadata:
<!-- SAFETY_PREAMBLE_INJECT -->
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。本期真实测试未通过的 shortcut 会从 `--help` 和 `dws shortcut list` 默认视图隐藏;Agent 不应主动推荐隐藏命令,内部排查 / 下期修复才使用 `dws shortcut list --include-hidden`。
> 命令参考:[report.md](references/report.md);剧本:[05-reporting.md](references/05-reporting.md)。
+1 -1
View File
@@ -18,7 +18,7 @@ metadata:
<!-- SAFETY_PREAMBLE_INJECT -->
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。本期真实测试未通过的 shortcut 会从 `--help` 和 `dws shortcut list` 默认视图隐藏;Agent 不应主动推荐隐藏命令,内部排查 / 下期修复才使用 `dws shortcut list --include-hidden`。
> 命令参考:[sheet.md](references/sheet.md);URL 识别与类型探测:[url-patterns.md](references/url-patterns.md)。
+1 -1
View File
@@ -18,7 +18,7 @@ metadata:
<!-- SAFETY_PREAMBLE_INJECT -->
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。本期真实测试未通过的 shortcut 会从 `--help` 和 `dws shortcut list` 默认视图隐藏;Agent 不应主动推荐隐藏命令,内部排查 / 下期修复才使用 `dws shortcut list --include-hidden`。
> 命令参考:[skill.md](references/skill.md)。
+1 -1
View File
@@ -18,7 +18,7 @@ metadata:
<!-- SAFETY_PREAMBLE_INJECT -->
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。本期真实测试未通过的 shortcut 会从 `--help` 和 `dws shortcut list` 默认视图隐藏;Agent 不应主动推荐隐藏命令,内部排查 / 下期修复才使用 `dws shortcut list --include-hidden`。
> 命令参考:[todo.md](references/todo.md);剧本:[02-task.md](references/02-task.md)。
+1 -1
View File
@@ -18,7 +18,7 @@ metadata:
<!-- SAFETY_PREAMBLE_INJECT -->
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。本期真实测试未通过的 shortcut 会从 `--help` 和 `dws shortcut list` 默认视图隐藏;Agent 不应主动推荐隐藏命令,内部排查 / 下期修复才使用 `dws shortcut list --include-hidden`。
> 命令参考:[wiki.md](references/wiki.md)。
+7
View File
@@ -25,6 +25,13 @@ metadata:
- 全局 `--profile <名称|corpId>`:单次指定本命令用哪个组织,一次性、不改默认组织
- 危险 / 写 / 删操作执行前先向用户确认
## Shortcut 可用性与隐藏规则
- `shortcut` 是对常用操作的高层封装,子 skill 负责意图、风险、跨产品流程和复杂参数;当前 `dws` 二进制的 `--help` / `dws shortcut list` 才是可用命令事实源。
- 用户意图可由默认可见 shortcut 满足时,优先使用 `dws <service> +<verb> ... --format json`。
- 调用前用 `dws <service> --help` 或 `dws shortcut list --service <service> --format json` 确认可见命令;不要凭 skill 文档或记忆猜测 shortcut。
- 本期真实测试未通过的 shortcut 会从默认 help / list 隐藏。缺失的 shortcut 视为未对外 release,改用已验证原子命令、脚本或参考流程;不要主动推荐隐藏命令。
- 只有内部 CR、真实测试复盘、下期修复时,才用 `dws shortcut list --include-hidden --format json` 查看隐藏项和 `hiddenReason`。
## 多组织 / --profile(关键规则)
dws 可同时登录多个钉钉组织,一个 profile = 一个已登录组织(corp)。当前 profile 决定本次命令用哪个组织的身份(corpId / userId 自动注入)。