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:
@@ -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
File diff suppressed because one or more lines are too long
@@ -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] {"data":{},"error":{"code":"-1","message":"no record","retryable":true,"type":"SYSTEM_ERROR"},"meta":{},"status":"error","success":true,"summary":"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] {"data":{},"error":{"code":"403","message":"Forbidden","retryable":false,"type":"AUTH_ERROR"},"meta":{},"status":"error","success":true,"summary":"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] {"data":{},"error":{"code":"404","message":"Not Found","retryable":true,"type":"SYSTEM_ERROR"},"meta":{},"status":"error","success":true,"summary":"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] {"data":{},"error":{"code":"INVALID_PARAMS","message":"taskId cannot be combined with scope, format, tableId or viewId","retryable":false,"type":"INPUT_ERROR"},"meta":{},"status":"error","success":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] {"data":{},"error":{"code":"-1","message":"no record","retryable":true,"type":"SYSTEM_ERROR"},"meta":{},"status":"error","success":true,"summary":"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] {"data":{},"error":{"code":"INVALID_PARAMS","message":"roleId is required","retryable":false,"type":"INPUT_ERROR"},"meta":{},"status":"error","success":true,"summary":"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] {"data":{},"error":{"code":"GET_WORKFLOW_ERROR","message":"调用远程服务业务异常","retryable":true,"type":"SYSTEM_ERROR"},"meta":{},"status":"error","success":true,"summary":"Failed to get workflow in base '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] {"data":{},"error":{"code":"LIST_WORKFLOWS_ERROR","message":"biz error","retryable":true,"type":"SYSTEM_ERROR"},"meta":{},"status":"error","success":true,"summary":"Failed to list workflows in base '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 取值 "ALL" 不合法,允许值: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] {"data":{},"error":{"code":"INVALID_BASE_ID","message":"baseId cannot be resolved to docId","retryable":false,"type":"INPUT_ERROR"},"meta":{},"status":"error","success":true,"summary":"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] {"data":{},"error":{"code":"INVALID_BASE_ID","message":"baseId cannot be resolved to docId","retryable":false,"type":"INPUT_ERROR"},"meta":{},"status":"error","success":true,"summary":"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] {"data":{},"error":{"code":"BASE_NOT_FOUND","message":"Specified base does not exist, has been deleted, or is inaccessible","retryable":false,"type":"INPUT_ERROR"},"meta":{},"status":"error","success":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] {"data":{},"error":{"code":null,"message":"Invalid source baseId: DWSREALTESTNOSUCHID0000000000000","retryable":null,"type":"USER_ERROR"},"meta":{},"status":"error","success":true,"summary":"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] {"data":{},"error":{"code":"52600003","message":"Data not found","retryable":true,"type":"SYSTEM_ERROR"},"meta":{},"status":"error","success":true,"summary":"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] {"data":{},"error":{"code":"BASE_NOT_FOUND","message":"Specified base does not exist, has been deleted, or is inaccessible","retryable":false,"type":"INPUT_ERROR"},"meta":{},"status":"error","success":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] {"data":{},"error":{"code":"INVALID_BASE_ID","message":"baseId cannot be resolved to docId","retryable":false,"type":"INPUT_ERROR"},"meta":{},"status":"error","success":true,"summary":"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] {"data":{},"error":{"code":"INVALID_BASE_ID","message":"baseId cannot be resolved to docId","retryable":false,"type":"INPUT_ERROR"},"meta":{},"status":"error","success":true,"summary":"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] {"data":{},"error":{"code":"INVALID_PARAMS","message":"config is required","retryable":false,"type":"INPUT_ERROR"},"meta":{},"status":"error","success":true,"summary":"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] {"data":{},"error":{"code":"INVALID_BASE_ID","message":"baseId cannot be resolved to docId","retryable":false,"type":"INPUT_ERROR"},"meta":{},"status":"error","success":true,"summary":"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] {"data":{},"error":{"code":"INVALID_BASE_ID","message":"baseId cannot be resolved to docId","retryable":false,"type":"INPUT_ERROR"},"meta":{},"status":"error","success":true,"summary":"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] {"data":{},"error":{"code":"INVALID_BASE_ID","message":"baseId cannot be resolved to docId","retryable":false,"type":"INPUT_ERROR"},"meta":{},"status":"error","success":true,"summary":"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] {"data":{},"error":{"code":"INVALID_BASE_ID","message":"baseId cannot be resolved to docId","retryable":false,"type":"INPUT_ERROR"},"meta":{},"status":"error","success":true,"summary":"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] {"data":{},"error":{"code":"404","message":"getDentryDTO returns null","retryable":true,"type":"SYSTEM_ERROR"},"meta":{},"status":"error","success":true,"summary":"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] {"data":{},"error":{"code":"404","message":"getDentryDTO returns null","retryable":true,"type":"SYSTEM_ERROR"},"meta":{},"status":"error","success":true,"summary":"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] {"data":{},"error":{"code":"INVALID_BASE_ID","message":"baseId cannot be resolved to docId","retryable":false,"type":"INPUT_ERROR"},"meta":{},"status":"error","success":true,"summary":"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] {"data":{},"error":{"code":"INVALID_BASE_ID","message":"baseId cannot be resolved to docId","retryable":false,"type":"INPUT_ERROR"},"meta":{},"status":"error","success":true,"summary":"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] {"data":{},"error":{"code":"INVALID_BASE_ID","message":"baseId cannot be resolved to docId","retryable":false,"type":"INPUT_ERROR"},"meta":{},"status":"error","success":true,"summary":"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] {"data":{},"error":{"code":"INVALID_BASE_ID","message":"baseId cannot be resolved to docId","retryable":false,"type":"INPUT_ERROR"},"meta":{},"status":"error","success":true,"summary":"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] {"data":{},"error":{"code":"INVALID_BASE_ID","message":"baseId cannot be resolved to docId","retryable":false,"type":"INPUT_ERROR"},"meta":{},"status":"error","success":true,"summary":"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] {"data":{},"error":{"code":"INVALID_IMPORT_ID","message":"importId not found: either invalid or expired","retryable":false,"type":"INPUT_ERROR"},"meta":{},"status":"error","success":true,"summary":"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] {"data":{},"error":{"code":"INVALID_BASE_ID","message":"无法解析 baseId","retryable":false,"type":"INPUT_ERROR"},"meta":{},"status":"error","success":true,"summary":"无效的 baseId","trace_id":"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] {"data":{},"error":{"code":"404","message":"getDentryDTO returns null","retryable":true,"type":"SYSTEM_ERROR"},"meta":{},"status":"error","success":true,"summary":"Failed to delete records","trace_id":"…</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] {"data":{},"error":{"code":"RESOLVE_DOC_ID_ERROR","message":"Failed to resolve docId from baseId","retryable":true,"type":"SYSTEM_ERROR"},"meta":{},"status":"error","success":true,"summary":"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] {"data":{},"error":{"code":"INVALID_RECORDS","message":"records must contain at least one writable record","retryable":false,"type":"INPUT_ERROR"},"meta":{},"status":"error","success":true,"summary":"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] {"data":{},"error":{"code":"INVALID_PARAMETER","message":"records is required and must not be empty","retryable":false,"type":"INPUT_ERROR"},"meta":{},"status":"error","success":true,"summary":"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] {"data":{},"error":{"code":"INVALID_BASE_ID","message":"baseId cannot be resolved to docId","retryable":false,"type":"INPUT_ERROR"},"meta":{},"status":"error","success":true,"summary":"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] {"data":{},"error":{"code":"INVALID_PARAMS","message":"roleId is required","retryable":false,"type":"INPUT_ERROR"},"meta":{},"status":"error","success":true,"summary":"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] {"data":{},"error":{"code":"INVALID_PARAMS","message":"roleId is required","retryable":false,"type":"INPUT_ERROR"},"meta":{},"status":"error","success":true,"summary":"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] {"data":{},"error":{"code":"INVALID_BASE_ID","message":"baseId cannot be resolved to docId","retryable":false,"type":"INPUT_ERROR"},"meta":{},"status":"error","success":true,"summary":"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] {"data":{},"error":{"code":"INVALID_BASE_ID","message":"baseId cannot be resolved to docId","retryable":false,"type":"INPUT_ERROR"},"meta":{},"status":"error","success":true,"summary":"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] {"data":{},"error":{"code":"INVALID_BASE_ID","message":"baseId cannot be resolved to docId","retryable":false,"type":"INPUT_ERROR"},"meta":{},"status":"error","success":true,"summary":"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] {"data":{},"error":{"code":"INVALID_BASE_ID","message":"baseId cannot be resolved to docId","retryable":false,"type":"INPUT_ERROR"},"meta":{},"status":"error","success":true,"summary":"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] {"data":{},"error":{"code":"INVALID_BASE_ID","message":"baseId cannot be resolved to docId","retryable":false,"type":"INPUT_ERROR"},"meta":{},"status":"error","success":true,"summary":"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] {"data":{},"error":{"code":"BASE_NOT_FOUND","message":"Specified base does not exist, has been deleted, or is inaccessible","retryable":false,"type":"INPUT_ERROR"},"meta":{},"status":"error","success":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] {"data":{},"error":{"code":"404","message":"getDentryDTO returns null","retryable":true,"type":"SYSTEM_ERROR"},"meta":{},"status":"error","success":true,"summary":"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] {"data":{},"error":{"code":"INVALID_BASE_ID","message":"baseId cannot be resolved to docId","retryable":false,"type":"INPUT_ERROR"},"meta":{},"status":"error","success":true,"summary":"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] {"data":{},"error":{"code":"INVALID_BASE_ID","message":"baseId cannot be resolved to docId","retryable":false,"type":"INPUT_ERROR"},"meta":{},"status":"error","success":true,"summary":"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] {"data":{},"error":{"code":"INVALID_BASE_ID","message":"baseId cannot be resolved to docId","retryable":false,"type":"INPUT_ERROR"},"meta":{},"status":"error","success":true,"summary":"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] {"data":{},"error":{"code":"INVALID_PARAMS","message":"conditionalFormats is required","retryable":false,"type":"INPUT_ERROR"},"meta":{},"status":"error","success":true,"summary":"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] {"data":{},"error":{"code":"INVALID_BASE_ID","message":"baseId cannot be resolved to docId","retryable":false,"type":"INPUT_ERROR"},"meta":{},"status":"error","success":true,"summary":"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] {"data":{},"error":{"code":"INVALID_BASE_ID","message":"baseId cannot be resolved to docId","retryable":false,"type":"INPUT_ERROR"},"meta":{},"status":"error","success":true,"summary":"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] {"data":{},"error":{"code":"INVALID_BASE_ID","message":"baseId cannot be resolved to docId","retryable":false,"type":"INPUT_ERROR"},"meta":{},"status":"error","success":true,"summary":"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] {"data":{},"error":{"code":"BASE_NOT_FOUND","message":"Cannot resolve base 'DWSREALTESTNOSUCHID0000000000000', please check if the baseId is valid","retryable":false,"type":"INPUT_ERROR"},"meta":{},"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] {"data":{},"error":{"code":"BASE_NOT_FOUND","message":"Cannot resolve base 'DWSREALTESTNOSUCHID0000000000000', please check if the baseId is valid","retryable":false,"type":"INPUT_ERROR"},"meta":{},"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">没找到待审批单据:待我处理的审批里没有标题/单号包含 "__DWS_SHORTCUT_REAL_TEST_NO_SUCH_APPROVAL_20260715-151724__" 的单据。</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">没找到名为 "__DWS_SHORTCUT_REAL_TEST_NO_SUCH_SPACE__" 的知识库;换个更完整/精确的空间名再试。</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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", "", "审批说明(可选)")
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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"),
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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...)
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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),
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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())
|
||||
@@ -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><xxx></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 <svc> <verb></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__":
|
||||
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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
@@ -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 信息。
|
||||
|
||||
|
||||
## 产品总览
|
||||
|
||||
|
||||
@@ -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)。
|
||||
|
||||
|
||||
@@ -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)。
|
||||
|
||||
@@ -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)。
|
||||
|
||||
@@ -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)。
|
||||
|
||||
@@ -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)。
|
||||
|
||||
@@ -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)。
|
||||
|
||||
@@ -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)。
|
||||
|
||||
@@ -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)。
|
||||
|
||||
@@ -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)。
|
||||
|
||||
@@ -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)。
|
||||
|
||||
@@ -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)。
|
||||
|
||||
@@ -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)。
|
||||
|
||||
@@ -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)。
|
||||
|
||||
@@ -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)。
|
||||
|
||||
@@ -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)。
|
||||
|
||||
@@ -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)。
|
||||
|
||||
@@ -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)。
|
||||
|
||||
@@ -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)。
|
||||
|
||||
@@ -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)。
|
||||
|
||||
@@ -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)。
|
||||
|
||||
@@ -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 自动注入)。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user