diff --git a/config/external-sources.lock.json b/config/external-sources.lock.json index 3457b5d1..54b323b5 100644 --- a/config/external-sources.lock.json +++ b/config/external-sources.lock.json @@ -42,8 +42,8 @@ "repo": "https://github.com/vercel-labs/skills.git", "ref": "main", "adapter": "claude-skill", - "commit": "d8eca76d5dbe47df2d146d036e32534b74ece57a", - "syncedAt": "2026-10-28T01:57:49Z" + "commit": "699d35b21bd3c97dbc0edeae062b2698898bb2e5", + "syncedAt": "2026-10-28T15:59:59Z" }, { "id": "generate-agents-md", @@ -94,10 +94,10 @@ "id": "dingtalk-workspace", "pluginName": "dingtalk-workspace", "repo": "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli.git", - "ref": "v1.0.65", + "ref": "v1.0.66", "adapter": "skill-collection", - "commit": "92c4f8c355c749ec470c68f998d862a00d7d1709", - "syncedAt": "2026-10-21T06:48:34Z" + "commit": "f215043e676d6889e1ddce3464828d8b1ec40c31", + "syncedAt": "2026-10-28T15:59:59Z" }, { "id": "ui-ux-pro-max", @@ -177,8 +177,8 @@ "repo": "https://github.com/hugohe3/ppt-master.git", "ref": "main", "adapter": "claude-skill", - "commit": "f3660bfcd20bc07c29260b7e330261a01a2b5a6d", - "syncedAt": "2026-10-28T01:57:49Z" + "commit": "6fbac39e1d06a66ce3a8724b9366cb878b793f99", + "syncedAt": "2026-10-28T15:59:59Z" }, { "id": "grill-me", @@ -258,8 +258,8 @@ "repo": "https://github.com/vercel/next.js.git", "ref": "canary", "adapter": "skill-collection", - "commit": "7356b0bcb5f22371c15dcb413afe5fb28412f52d", - "syncedAt": "2026-10-28T01:57:49Z" + "commit": "f922205f9e860de24673269f81dea51e9e33b0cb", + "syncedAt": "2026-10-28T15:59:59Z" }, { "id": "shuorenhua", diff --git a/plugins/codex/plugins/dingtalk-workspace/.codex-plugin/plugin.json b/plugins/codex/plugins/dingtalk-workspace/.codex-plugin/plugin.json index a5741c18..cbda77a0 100644 --- a/plugins/codex/plugins/dingtalk-workspace/.codex-plugin/plugin.json +++ b/plugins/codex/plugins/dingtalk-workspace/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "dingtalk-workspace", - "version": "1.0.65", + "version": "1.0.66", "description": "通过 dws CLI 操作钉钉消息、文档、表格、日历、待办和知识库的官方 Skill 集合。需要另行安装匹配版本的 dws、登录并取得企业授权;辅助脚本需要 Python 3。", "author": { "name": "DingTalk", diff --git a/plugins/codex/plugins/dingtalk-workspace/THIRD_PARTY_SOURCE.json b/plugins/codex/plugins/dingtalk-workspace/THIRD_PARTY_SOURCE.json index 3a93ab83..e41b3981 100644 --- a/plugins/codex/plugins/dingtalk-workspace/THIRD_PARTY_SOURCE.json +++ b/plugins/codex/plugins/dingtalk-workspace/THIRD_PARTY_SOURCE.json @@ -1,9 +1,9 @@ { "sourceId": "dingtalk-workspace", "repo": "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli.git", - "ref": "v1.0.65", - "commit": "92c4f8c355c749ec470c68f998d862a00d7d1709", + "ref": "v1.0.66", + "commit": "f215043e676d6889e1ddce3464828d8b1ec40c31", "adapter": "skill-collection", "sourcePath": "skills/multi", - "syncedAt": "2026-10-21T06:48:34Z" + "syncedAt": "2026-10-28T15:59:59Z" } diff --git a/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-aitable/SKILL.md b/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-aitable/SKILL.md index 0add8008..b02a2bfd 100644 --- a/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-aitable/SKILL.md +++ b/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-aitable/SKILL.md @@ -114,7 +114,7 @@ Help/Schema 是离线契约查询,不调用线上业务,也不读写用户 ## Shortcut 发现(按需) -`aitable` 当前有 127 条公开 shortcut,完整清单保留在 Runtime Catalog 与 Schema,不在高频产品根 Skill 中重复展开。已知 leaf 直接执行。参数只查 `dws schema --cli-path "aitable " --compact --jq '{cli_path,parameters,constraints,confirmation}' -f json`;仅需且已发布 `result` 时查 outcomes/pagination,字段级再查 `data_schema`;缺失不以 Help/样例推断。Schema 不可用才读一次已知 leaf Help;`unknown flag` 用同 leaf Help 修正一次。`unknown command` 禁 Help:错误 suggestion → 已加载 Skill/reference 明确入口;均无则报漂移。禁全 Catalog/root/parent/product Help;低频 reference 不默认 Help;同一任务只读一个 Reference。 +`aitable` 当前有 140 条公开 shortcut,完整清单保留在 Runtime Catalog 与 Schema,不在高频产品根 Skill 中重复展开。已知 leaf 直接执行。参数只查 `dws schema --cli-path "aitable " --compact --jq '{cli_path,parameters,constraints,confirmation}' -f json`;仅需且已发布 `result` 时查 outcomes/pagination,字段级再查 `data_schema`;缺失不以 Help/样例推断。Schema 不可用才读一次已知 leaf Help;`unknown flag` 用同 leaf Help 修正一次。`unknown command` 禁 Help:错误 suggestion → 已加载 Skill/reference 明确入口;均无则报漂移。禁全 Catalog/root/parent/product Help;低频 reference 不默认 Help;同一任务只读一个 Reference。 仅当根路由、精确 task reference 和 `references/aitable.md` 的低频原子索引都无法定位能力时,才执行 `dws shortcut list --service aitable --format json` 做最终回退;不要为已知意图加载完整 Shortcut Catalog 或产品级 Schema。 @@ -178,6 +178,8 @@ Help/Schema 是离线契约查询,不调用线上业务,也不读写用户 - 已有 ID 直接使用;URL 只解析一次;“唯一定位并操作”用 `+resolve-base` / `+resolve-table`,“搜索候选/存在性检查”直接用 `+base-search`。人员、部门或群组只有显示名称时用 `entity search` 取得稳定身份;这些路径不要串行探测。filters/sort 缺 fieldId 时才读取字段目录。 - Golden Route 已给出准确命令和参数时直接执行;不预读或默认读取通用 `references/aitable.md`。只有操作参数、JSON 结构或恢复语义确实缺失时,才读取下方一个精确操作 Reference。 - Shortcut 已含分片或验证时不重复拆步;已有 Base 新建完整表结构直接用 `+table-bootstrap`。 +- 明确仪表盘设计、大小混排或纵向叠放时保留显式 layout,创建后不默认 arrange。多图集中编排用 `+charts-create`;需要多步依赖和中断接续时读 dashboard-chart 专项,使用 `+build-plan` / `+build-plan-verify`。 +- AI submitted 只表示有真实 taskId 的受理;新状态入口已部署且有固定目标记录时才使用 `+field-wait-ai`。本轮终态与结果写回可读分别判断,缺少版本证据时保持 pending。 - 单产品线性任务直接执行,不创建 TodoWrite;只有跨产品或多个独立分支的长任务才建计划,并且只在阶段切换时更新,不在每条 CLI 后刷新状态。 - 用户要求资源名带当前时间戳时只取一次并在 Base、Table、Dashboard 等名称中复用同一值;不要为每个资源分别取时间。 - JSON 已返回所需字段时立即复用;不得为寻找同一字段改用 `--verbose`、`raw`、`pretty` 重复请求。 diff --git a/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-aitable/references/aitable.md b/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-aitable/references/aitable.md index ccff42ef..3005155c 100644 --- a/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-aitable/references/aitable.md +++ b/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-aitable/references/aitable.md @@ -153,7 +153,7 @@ Base、Table、应用模式 App/Page/Widget、普通 Field、普通 Record、Vie |------|------| | `dashboard get/create/update/delete` | 仪表盘管理 | | `dashboard config-example` | 查看仪表盘配置模板 | -| `dashboard arrange` | 自动重排仪表盘图表布局(智能填满网格,避免空缺) | +| `dashboard arrange` | 明确接受统一按行重排时使用;改变位置、宽高和叠放关系 | | `chart get/create/update/delete` | 图表管理 | | `chart widgets-example` | 查看图表 widgets 配置模板 | @@ -189,9 +189,10 @@ Base、Table、应用模式 App/Page/Widget、普通 Field、普通 Record、Vie | `advperm role-update` | 增量更新自定义角色(PATCH) | `--base-id` `--role-id` | 未传字段不变;`--sub-roles` 按 (targetId,targetType) 合并 | | `advperm role-delete` | 删除自定义角色 | `--base-id` `--role-id` `--yes` | 不可逆;系统角色禁删;**调用者必须是该 AI 表格的管理员/Owner**,非管理员会得到 401 AUTH_ERROR | -> **角色 CRUD 已全支持**:create/get/list/update/delete 都可走 CLI。 -> 所有写命令(enable/disable/role-create/role-update/role-delete)需要 Base 管理员权限;非管理员只能调 `role-list` / `role-get`(只读)。 -> "角色 ↔ 成员"绑定当前 CLI 不支持,仍需在 AI 表格 Web 端 → Base 设置 → 高级权限面板手动完成。 +> **高级权限角色与成员已支持**:角色 create/get/list/update/delete、成员追加/完整替换、角色复制和按用户查角色均可走 CLI。 +> 所有写命令(enable/disable/role-create/role-update/role-delete/role-members-add/role-members-update/role-copy)需要 Base 管理员权限;非管理员只读使用 `role-list`、`role-get`、`role-query-user`。 +> 成员全量替换前必须回读 `defaultRole`,DWS 强制完整 `--members`(`[]` 清空,不接受 null)及 `--default-role-mode`(0/1/2,1 还需 `--default-role-id`)。成员最多 200 项,id/memberType 均为 String,用户填 uid(不是 userId)及 `"2"`,不能用 user/conv 或数字 2;完整编码和可选 number `memberIdBelongOrgId` 见高级权限 reference 类型表。 +> 复制沿用 roleType/flowType/subRoles,不复制成员,name 省略或空字符串使用「源角色名 副本」;查询返回 enabled 和 roles 配置(不含成员),高级权限关闭时 roles 通常为空,targetUid 不替换调用身份。 ### section (文件夹与节点管理) @@ -410,7 +411,7 @@ dws aitable export data --base-id --task-id --timeout-ms 300 用户说"仪表盘/图表/chart" → 读 [aitable-dashboard-chart.md](./aitable/aitable-dashboard-chart.md) -用户说"仪表盘排版乱了/图表对不齐/重新排布/自动布局/美化仪表盘" → `dashboard arrange`(读 [aitable-dashboard-chart.md](./aitable/aitable-dashboard-chart.md)) +用户明确接受改变位置和尺寸、统一按行重排 → `dashboard arrange`;已有设计、大小混排或纵向叠放保留显式布局(读 [aitable-dashboard-chart.md](./aitable/aitable-dashboard-chart.md)) 用户说"附件/上传文件" → 读 [aitable-attachment.md](./aitable/aitable-attachment.md) @@ -424,7 +425,10 @@ dws aitable export data --base-id --task-id --timeout-ms 300 - 建角色(可同时指定子角色权限) → `advperm role-create --name ... --sub-roles '[...]'` - 改角色名 / 改子角色权限(PATCH 语义,未传字段不变) → `advperm role-update --role-id ... [--name ...] [--sub-roles '[...]']` - 删角色 → `advperm role-delete --yes` -- **角色 ↔ 成员绑定**:当前 CLI 不支持,仍需在 AI 表格 Web 端面板手动完成 +- 向角色追加成员 → `+role-members-add --role-id ... --members '[...]' --yes` +- 完整替换或清空成员 → `+role-members-update --role-id ... --members '[...]' --default-role-mode 0 --yes`;mode=1 时必须同时传 `--default-role-id` +- 复制角色配置(不复制成员)→ `+role-copy --source-role-id ... --yes` +- 查询当前或指定用户命中的角色 → `+role-query-user [--target-uid ...]` 命令报错/操作失败 → 读 [aitable-error-recovery.md](./aitable/aitable-error-recovery.md) diff --git a/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-aitable/references/aitable/aitable-advperm.md b/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-aitable/references/aitable/aitable-advperm.md index 84d9dfe6..2eac48bc 100644 --- a/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-aitable/references/aitable/aitable-advperm.md +++ b/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-aitable/references/aitable/aitable-advperm.md @@ -14,8 +14,18 @@ | `advperm role-create` | 创建自定义角色 | | `advperm role-update` | 增量更新自定义角色(PATCH 语义) | | `advperm role-delete` | 删除自定义角色(不可逆) | +| `+role-members-add` | 向自定义角色增量追加成员 | +| `+role-members-update` | 完整替换角色成员并显式保留默认角色 | +| `+role-copy` | 复制自定义角色配置(不复制成员) | +| `+role-query-user` | 查询用户实际命中的高级权限角色 | -> 所有子命令的 `--base-id` 必填,可用隐藏别名 `--base`。 +> `--base-id` 是 Agent 与 Schema 发布的唯一公开参数;`--base` 仅是历史兼容别名,Skill 和示例不得使用。 + +## 运行时配置 + +- 不在命令或 Skill 中硬编码 HSF 地址、版本、uid、orgId、userId、corpId 或 operatorUid;这些可信上下文由 MCP 运行时注入。 +- 本文档 4 个角色成员命令(role-members-add/update、role-copy、role-query-user 的原子命令与 Shortcut)直连 `aitable` 主服务 MCP,不经 `aitable-helper` 兼容路由、不触发能力发现;MCP 端点由 `~/.dws/mcp_url`(或 `DWS_CONFIG_DIR/mcp_url`)首行决定。只需完成 `dws auth login` 并使用当前环境的端点配置。 +- 新增工具对应 MCP 工具名为 `add_role_members`、`update_role_members`、`copy_role`、`query_user_roles`;CLI 已封装为下文命令,不需要手填 HSF interfaceName、version 或 DTO 映射。 ## 命令详情 @@ -82,7 +92,7 @@ dws aitable advperm role-list --base-id BASE_ID --format json - `roleType`:`custom`(自定义) / `system_editor` / `system_reader` / `5000`(owner) / `4000`(manager)。 - `system`:boolean,true 表示系统角色(不可删)。 - `subRoles[].display.*`:服务端返回的人类可读标签,可直接拼接给用户阅读,无需自行映射枚举。 -- 不返回角色成员列表;如需"成员-角色"映射请去 AI 表格 Web 端。 +- `roles[].members` 返回该角色当前成员;配置回读成功不等于成员实际权限已验证,仍需对应身份执行真实访问测试。 - 新建 Base 默认 `enabled=false`,开启后只有 `owner` / `manager` 两个 meta 角色;`system_editor` / `system_reader` 需要在 Web UI 给成员授权"可编辑/可查看"后才会被服务端自动生成。 `role-list` / `role-get` 不需要管理员权限,普通成员也可读。 @@ -162,6 +172,60 @@ dws aitable advperm role-delete --base-id BASE_ID --role-id ROLE_ID --yes --form 不可逆,删前先 `role-get` 留底。 +### 角色成员与复制 + +先执行 `+role-list` 读取 `enabled`、`defaultRole`、角色及其 `members`;涉及单个角色时再执行 `+role-get`。成员写入及复制只能操作自定义角色,需 Base 管理员权限。成员 ID 必须来自可信解析结果或用户提供的可信 ID,并核对下表类型;用户必须填 uid,不可把 userId 当 uid,禁止猜测或轮换 ID 试错。复制配置不要求取得成员 ID。 + +以下规则同样适用于原子命令 `advperm role-members-add`、`advperm role-members-update`、`advperm role-copy`、`advperm role-query-user`。 + +| 参数/字段 | JSON 类型 | 契约 | +|---|---|---| +| `baseId` | String | Base 唯一标识 | +| `roleId` / `sourceRoleId` / `defaultRoleId` / `targetUid` | String | long 形态字符串,不是 JSON number;`targetUid` 为用户 uid | +| `members` | array | 最多 200 个对象,每项必须含非空 String `id` 和 `memberType` | +| `members[].id` | String | 随成员类型填写,见下表;用户填 uid(不是 userId) | +| `members[].memberType` | String | 使用下表字符串编码,例如 `"2"`,不是数字 `2`,也不是 `user` / `conv` | +| `members[].memberIdBelongOrgId` | number | 可选,Java Long 整数,不加引号;同文档组织可省略,服务端回填 docOrgId;跨组织必须填成员所属组织 ID | + +| memberType 字符串编码 | 成员类型 | id 含义(String) | +|---|---|---| +| `"1"` | 群 | 群 id | +| `"2"` | 用户 | uid,不是 userId | +| `"3"` | 组织 | orgid | +| `"4"` | 部门 | 部门 id | +| `"5"` | 虚拟组 | 虚拟组 id | +| `"6"` | 角色组 | 角色组 id | +| `"8"` | 动态组 | 动态组 id | + +```bash +# 增量追加同组织用户:不会移除现有成员 + dws aitable +role-members-add --base-id BASE_ID --role-id ROLE_ID \ + --members '[{"id":"","memberType":"2"}]' + +# 全量替换成员(跨组织示例,123456 需替换为成员所属组织 ID) +# 必须带完整目标列表和刚回读的默认角色;[] 表示明确清空 + dws aitable +role-members-update --base-id BASE_ID --role-id ROLE_ID \ + --members '[{"id":"","memberType":"2","memberIdBelongOrgId":123456}]' \ + --default-role-mode 0 + +# 仅当当前 defaultRole.mode=1 时同时传当前默认角色 ID + dws aitable +role-members-update --base-id BASE_ID --role-id ROLE_ID --members '[]' \ + --default-role-mode 1 --default-role-id CURRENT_DEFAULT_ROLE_ID + +# 复制配置(不复制成员)与查询用户实际命中的角色 + dws aitable +role-copy --base-id BASE_ID --source-role-id ROLE_ID --name "市场可读副本" + dws aitable +role-query-user --base-id BASE_ID --target-uid TARGET_UID +``` + +成员配置规则: + +- `+role-members-add` 是增量追加;`+role-members-update` 是完整覆盖,绝不能用后者仅传一个待追加成员。二者均需管理员,返回写后最新成员列表 `{roleId, members}`。 +- DWS 的 `+role-members-update` 强制 `--members`:非空全替换,`[]` 明确清空,不接受 `null`;同时强制 `--default-role-mode` 为 `0`(无兜底)、`1`(指定自定义角色)或 `2`(继承文档权限)。mode 为 `1` 时必须带非空 long 形态字符串 `--default-role-id`;按刚回读值传入,防止误改默认角色。 +- MCP `update_role_members` 的 members 可省略(不改),传入时须为 array:`[]` 清空,非空全替换;defaultRoleMode/defaultRoleId 都省略时不改默认角色;**这不放宽上述 DWS 必填约束**。 +- `+role-copy` 沿用源角色的 `roleType`、`flowType`、`subRoles`,不复制成员,系统角色不可复制;`name` 省略或空字符串使用「源角色名 副本」,返回新角色完整配置。 +- `+role-query-user` 的 `--target-uid` 可选,省略或空字符串查询当前调用人,仅指定查询对象,绝不覆盖调用身份。返回 `{enabled, roles}`,roles 仅含角色配置,不含成员(members 恒为空);高级权限关闭时 roles 通常为空,不能把此接口当成员列表。 +- 写操作只调用一次;超时、8001 或结果不确定时先用 `+role-list`/`+role-get` 回读,未确认幂等前不得自动重试。 + ## 能力边界 | 能力 | 状态 | @@ -172,7 +236,10 @@ dws aitable advperm role-delete --base-id BASE_ID --role-id ROLE_ID --yes --form | 增量修改角色(PATCH 语义,不清空未传字段) | ✅ 需管理员 | | 删除自定义角色 | ✅ 需管理员 | | 修改/删除系统角色 | ❌ 服务端禁止;只能在 AI 表格 Web 端操作 | -| 角色 ↔ 成员绑定 | ❌ CLI 暂不支持,需在 AI 表格 Web 端 → Base 设置 → 高级权限 → 角色管理面板手动完成 | +| 向角色追加成员 | ✅ `+role-members-add`,需管理员;写后回读 | +| 完整替换角色成员 | ✅ `+role-members-update`,需管理员;必须显式保留 defaultRole | +| 复制自定义角色 | ✅ `+role-copy`,不复制成员;系统角色禁止 | +| 查询用户命中角色 | ✅ `+role-query-user`,只读 | ## 错误码速查 diff --git a/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-aitable/references/aitable/aitable-dashboard-chart.md b/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-aitable/references/aitable/aitable-dashboard-chart.md index 6c973170..9a5ac274 100644 --- a/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-aitable/references/aitable/aitable-dashboard-chart.md +++ b/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-aitable/references/aitable/aitable-dashboard-chart.md @@ -22,9 +22,9 @@ dws aitable chart get --base-id --dashboard-id --chart- - `dashboard get` 返回的 `charts[].chartId` 可直接给 `chart get` 使用 - `chart create` 以及携带 `--layout` 的 `chart update` 会在写入前自动调用 `get_dashboard` 并强制校验根网格。只有 `schemaVersionTypeVerified=true` 时才使用原始类型判据:JSON number `2` 为 48 列,其他已验证值为 12 列;证据缺失时命令停止且不调用 Chart 写工具 -- 任务上下文已确认应用模式时,Chart 写命令显式传 `--is-app-mode=true`,DWS 按只读上下文强制使用 48 列;该 flag 不会进入 MCP payload。未确认应用模式时省略,不能为了绕过 schemaVersion 证据传 true +- 任务上下文已确认应用模式时,Chart 写命令显式传 `--is-app-mode=true`,DWS 按只读上下文强制使用 48 列;该上下文作为顶层 isAppMode 传给 MCP 作写前校验,不写入 config。未确认应用模式时省略,不能为了绕过 schemaVersion 证据传 true - 全局 12/48 列只适用于根布局。存在非根 `parentId` 时使用容器自己的坐标系;拿不到容器真实列数时,创建操作停止,更新操作省略 layout 以保留原值 -- `schemaVersion` 与 `isAppMode` 都是只读字段,不能写入或透传到 Dashboard config +- `schemaVersion` 与 `isAppMode` 都是只读字段,不能写入 Dashboard config - 删除 dashboard 会级联删除其全部 chart;确认前必须说明该影响 - `dashboard share get` 可能返回 `404`(资源不存在或未开通),需按可重试错误处理,不要误判为参数拼错 - `chart share get` 可正常返回 `enabled/shareUrl`,用于分享状态判断 @@ -35,7 +35,7 @@ dws aitable chart get --base-id --dashboard-id --chart- | 命令 | 用途 | 必填参数 | 说明 | |------|------|----------|------| | `dashboard config-example` | 查看完整仪表盘配置模板 | 无 | 仅在完整 config 结构缺失时调用一次 | -| `dashboard arrange` | 自动重排图表布局 | `--base-id` `--dashboard-id` | 把图表按行铺满网格,避免某行只占半幅、留下大片空白;返回 `{totalColumns, layout, alignedChartCount}` | +| `dashboard arrange` | 自动重排图表布局 | `--base-id` `--dashboard-id` | 仅用户明确接受统一按行重排时使用;改变位置、宽高和叠放关系;返回 `{totalColumns, layout, alignedChartCount}` | ## chart 子命令 @@ -49,4 +49,23 @@ dws aitable chart get --base-id --dashboard-id --chart- ## 配置获取流程 -已有符合当前 leaf Schema 的合法 config 时直接创建/更新,不读取模板。只有 Chart config 缺少结构时调用一次 `+chart-widgets-example`;该命令当前返回所有图表类型示例,随后只使用目标类型,并按真实 tableId/fieldId 填充后执行。Dashboard 完整 config 缺少结构时只调用一次 `dashboard config-example`,不要同时读取两个模板。 +已有符合当前 leaf Schema 的合法 config 时直接创建/更新,不读取模板。只有 Chart config 缺少结构时调用一次 `+chart-widgets-example`;旧入口返回全量 JSONC;优先用 `+chart-templates --chart-types LINE,PIE` 正确解析并只投影目标类型。MCP 新筛选入口部署确认后才追加 `--server-filter`。Dashboard 完整 config 缺少结构时只调用一次 `dashboard config-example`,不要同时读取两个模板。 + +## 保留设计、批量和接续 + +- 已有明确布局、大小卡混排或纵向叠放时,按声明的 `layout` 创建;创建后不默认追加 `arrange`。等高铺满只属于明确选择的按行重排策略。 +- 同一 Dashboard 的 1~20 张图表使用 `+charts-create --charts ` 集中编排,写前只读确认能力后优先服务端批建,缺能力才串行写;`--server-batch=false` 强制串行。逐图独立读取完整配置及布局,摘要和受理数量不等于 verified。 +- `+dashboard-diagnose` 只读检查根几何和已声明 minW/minH;页面标题、数字及图例仍需固定视口验收,不能从无重叠推导可读。 +- 新协议部署后,`+dashboard-preview` 返回真实前后布局及 source 摘要;原子 `dashboard arrange --expected-layout-fingerprint <真实摘要>` 在源布局已变化时停止。摘要和提交前检查不等价于原子 CAS。 +- 多步构建使用 `+build-plan --plan plan.json --checkpoint task.json`。版本 1 计划固定同一 Base,声明顺序/dependsOn、完整业务参数及逐步回读断言;模板和结构化 `$ref` 由程序展开并绑定真实 ID。单表大结构优先 bootstrap。 +- 中断后先 `+build-plan-verify --plan plan.json --checkpoint task.json`;接续写入用 `+build-plan ... --resume`。检查点绑定计划摘要、实际账号组织及 Base,已完成步骤重新核验,不重做;未知创建无可靠 ID 时停止并对账,不能按旧值或名称自动重建。 +- AI 状态新入口部署后,使用 `+field-wait-ai --base-id --table-id --tasks <本轮taskId/fieldId/recordIds数组>`。指定记录可按本轮行任务终态结束等待;现有兼容接口不提供写回版本,因此结果可读性仍为未确认,标准输出保持 pending。固定等待、运行数量为 0、旧单元格非空均不能证明本轮完成。 + +### 设计诊断、等待恢复和离线统计 + +- `+dashboard-diagnose` 可传 `--expected-layout` 完整根设计及 `--regions` 指定区域;设计差异、区域并集覆盖和几何分别判断,不证明页面可读。子容器不套用根坐标。 +- `+field-wait-ai --checkpoint <私有任务文件>` 保存全部原任务和逐行证据;恢复携带相同 `--tasks`、`--checkpoint` 和 `--resume`,只读续查,不重触发 AI。`+build-plan` 的固定记录 AI 步骤也持久保留等待证据;本轮结果未核实可读时保持 pending,阻断后续写入。 +- `+task-report --checkpoint <文件>` 离线汇总统计,支持 `--baseline` 及显式客户端模型指标。缺失指标保持未知;不同输入/身份/模型/环境、未核验结果或不完整统计不产生提速结论。 +- 新 DWS 在 Chart 布局写前核验后传递明确的应用模式布尔值。旧请求省略时 MCP 兼容只检查 48 列绝对上界,不能把它当作具体 12/48 列网格已验证。 + +默认 `+charts-create` / `+chart-templates` 在写前通过只读能力发现优先选择服务端批建 / 按类型模板;明确缺能力时使用旧串行 / 全量模板。发现失败先排查,不发送写入;可用 `--server-batch=false` / `--server-filter=false` 强制旧路径。写入开始后任何超时或未知结果都不切换路径重放。 diff --git a/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-aitable/references/aitable/aitable-field.md b/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-aitable/references/aitable/aitable-field.md index da4f7901..fd6477a1 100644 --- a/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-aitable/references/aitable/aitable-field.md +++ b/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-aitable/references/aitable/aitable-field.md @@ -74,6 +74,14 @@ dws aitable field create --base-id --table-id \ ### AI 字段创建示例 +内置 AI 插件先用 `field ai-plugins --base-id --table-id ` 只读查询真实 `pluginId`、`fieldTypes`、`configSchema` 与 `configurationMode/reason`,不猜插件 ID。工作流凭据需要现有 UI 绑定时,按返回限制处理,不把凭据传入 CLI。 + +配置复用 `field create --ai-config`、`field update --ai-config` 与 `+field-update --ai-config`。配置使用 `{"type":"builtin","pluginId":"<已确认插件ID>","formData":{},"referFieldIds":[""],"autoRecompute":true}`,无需通用 AI 的 `prompt` / `outputType`。由 MCP 校验与转换,CLI 不自行构造 `decoratorConfigs`。回执 `verificationStatus=pending` 时继续 `field get` 只读核对,不能视为已生效或再次写入。 + +快递/OCR 等已支持的内置模板可能要求基础字段类型 `object`,按 `ai-plugins` 返回的 `fieldTypes` 选择;这不表示可以创建任意普通 object 字段。 + +下例是通用 AI 配置: + ```bash dws aitable field create --base-id --table-id \ --name "AI摘要" --type text \ @@ -95,6 +103,22 @@ dws aitable field create --base-id --table-id \ - `autoRecompute=true` 表示引用字段变化后自动重算 - **AI 字段的 prompt 必须至少包含一个 `fieldRef` 引用**,纯文本 prompt 会被后端拒绝 +### 字段编组 + +字段编组作用于列,不是按记录值分组。入口须已在目标 MCP 部署: + +| 入口 | 参数与行为 | +|---|---| +| `field-group list` | `--base-id`、`--table-id`;查询组名、说明和组内字段 ID | +| `field-group create` | 加 `--name`、`--field-ids F1,F2`(1~100 项),可选 `--description`;清空旧组时须 `--confirm-empty-group=true` | +| `field-group update` | 加 `--group-id` 和 `--name` / `--description` 至少一项;显式空说明清除,未传配置保留 | +| `field-group ungroup` | 加 `--group-id`;取消编组,保留字段与数据 | +| `field-group move-fields` | 加 `--field-ids`;传 `--target-group-id` 移入目标组,省略则移出成为普通列 | + +移空源组时必须显式 `--confirm-empty-group=true`,沿用页面自动取消空组的处理;`--yes` 只确认写操作,不替代这个业务确认。取消或移出后的普通列继续遵循当前视图列顺序与隐藏规则,不额外创建“未分组”组。变更 `verificationStatus=pending` 时保留回执,按 `meta.operation.next_command` 只读核对,不能重放写入。 + +`field-group list` 与 `field ai-plugins` 的读取失败在 `error.upstream_code` 保留 MCP 错误码:`InvalidArgument`、`InvalidRequest.ResourceNotFound`、`InvalidRequest.Forbidden` 须先修正请求、资源标识或访问条件;`InvalidRequest.DataNotReady`、`500` 在 `error.retryable=true` 时可等待后重新读取。重试标记仅为 true 时出现在 CLI 回执中,不能从错误文案推断。CLI 不自动重试这两个入口,也不将读取重试提示用于重放写操作。 + ### 关联字段与跨表引用字段 创建 `lookup`(关联引用)和 `filterUp`(查找引用)字段时,config 格式有严格要求: diff --git a/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-aitable/references/aitable/aitable-form.md b/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-aitable/references/aitable/aitable-form.md index fe078c49..af82eeab 100644 --- a/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-aitable/references/aitable/aitable-form.md +++ b/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-aitable/references/aitable/aitable-form.md @@ -13,6 +13,7 @@ | `form field update` | 更新字段必填/描述 | | `form field hide` | 在表单中隐藏/显示字段(不影响底层数据表字段) | | `form share get` | 获取分享配置 | +| `form share url` | 获取标准填写链接,可用 `--qr-file form.png` 生成本地二维码 | | `form share update` | 部分更新分享开关、授权、有效期和通知等配置 | | `form questions create` | 添加题目(等价于 `field create`,命令位置上的别名) | | `form questions delete` | 删除题目(等价于 `field delete`,命令位置上的别名) | @@ -91,7 +92,7 @@ dws aitable +form-share-update --base-id BASE_ID --table-id TABLE_ID --view-id V dws aitable +form-share-get --base-id BASE_ID --table-id TABLE_ID --view-id VIEW_ID --format json ``` -从最后一次返回直接读取 `data.shareFormUuid`,分享地址为 `https://alidocs.dingtalk.com/i/form/{shareFormUuid}`。字段已存在时不要再调用 Help、Catalog、`form get`,也不要换 `--verbose`、`raw`、`pretty` 重复请求。用户还要求发送时,把该完整 URL 交给 Chat 的发送命令并检查真实发送回执。 +分享地址统一为 `https://alidocs.dingtalk.com/notable/share/form/{shareFormUuid}`。执行 `form share url --base-id BASE_ID --table-id TABLE_ID --view-id VIEW_ID --qr-file form.png` 可交付 `shareUrl/copyableUrl/qrFile`;二维码路径必须位于工作目录内,不覆盖已有文件。未开启分享或受处罚时失败,dry-run 不读远端、不生成文件。发送时把真实 `shareUrl` 交给 Chat 的发送命令并检查发送回执。 ## 要点 @@ -139,6 +140,7 @@ dws aitable +form-share-get --base-id BASE_ID --table-id TABLE_ID --view-id VIEW |------|------|----------|------| | `form share get` | 获取分享配置 | `--base-id` `--table-id` `--view-id` | 返回服务端真实的 enabled/status/shareFormUuid/formCover;只读,不修复 CP | | `form share update` | 部分更新分享配置 | `--base-id` `--table-id` `--view-id` + 至少一个配置参数 | 成功返回真实 shareFormUuid/status/formCover/cpSynced;只有 cpSynced=true 才完成闭环;未传字段保持原值 | +| `form share url` | 获取可复制的标准分享链接 | `--base-id` `--table-id` `--view-id` | 读取 `shareFormUuid` 后返回 `shareUrl`/`copyableUrl`;未开启分享时失败 | ## 完整工作流示例 @@ -175,7 +177,7 @@ dws aitable form share update --base-id BASE_ID --table-id TABLE_ID --view-id VI ## 返回结构补充 -- `form list` 返回 `data.formViews[]`,**每条含** `viewId/name`(以及服务端当前可用的 `title/createdAt`);`shareFormUuid` 不在此返回,请用 `form share get` 单独获取。 +- `form list` 返回 `data.formViews[]`,每条含 `viewId/name` 及服务端可用的 `title/createdAt/shareFormUuid/shareFormUrl`。列表中的链接仅为候选地址,不证明当前仍可填写。发送前用 `form share url` 验证当前是否已开启、未受处罚,不能仅凭历史 UUID 判断可填写。 - `form get` 的 `data` 是客户端按 `viewId` 精确筛出的单个表单对象,不是 `formViews` 数组。Agent 直接读取 `data.viewId` / `data.name`;不存在的 `viewId` 会明确失败。 - `form field list` 仅返回**未隐藏**的字段;`hidden=true` 的字段不在此返回,如需查看全部字段请用 `field get`。 @@ -183,7 +185,17 @@ dws aitable form share update --base-id BASE_ID --table-id TABLE_ID --view-id VI - `form field hide` 当前每次只接收一个 `fieldId`。多字段必须在同一 Base 写队列中逐个串行设置,全部完成后统一回读一次;不传数组,不并发写。 - 新建表单首次开启分享时,复用 `form create`/`form update` 中已知的标题,通过 `--form-name` 与 `--enabled true` 同时传入;不要为取标题额外调用 `form get`。已有分享仅调整其他配置时,不覆盖原名称。 +- 时间限制可使用 `--form-start-time/--form-end-time` 的毫秒时间戳,也可使用带明确时区的 `--form-start-at/--form-end-at`(RFC3339);传 `0` 清除对应一端,未传字段保持原值。 +- `--load-last-submit true` 开启提交后查看/修改;`--submit-record-view-mode` 取 `0`(最近一次)或 `1`(全部历史),不会修改历史记录数据。 +- 本地封面用 `--form-cover-file ./cover.png`,支持 PNG/JPEG/GIF、最大 10 MiB。CLI 上传图片,MCP 将资源转为公开封面,再同步分享配置与页面主题并回读;与 `--form-cover`、`--form-cover-resource-id` 互斥。上传后配置未确认时保留回执和 resourceId,先只读检查,不重复上传或原样重放。 +- `--form-cover` 仅接受 HTTPS,主机名精确限于 `img.alicdn.com`、`static.dingtalk.com`、`down-cdn.dingtalk.com`、`alidocs.oss-cn-zhangjiakou.aliyuncs.com`、`alidocs2.oss-cn-zhangjiakou.aliyuncs.com`、`alidocs.oss-cn-hangzhou.aliyuncs.com`,不含子域名,仅默认端口或 443;其他图片先上传到当前 Base。不支持清除封面:省略保持原值,显式空串/全空白报错。URL 不得含空白、未编码的引号/括号、鉴权信息、查询参数(包括空 `?`)、片段(包括空 `#`)或 `/core/api/resources` 内部资源路径(含编码/点路径变体);文件名中的引号/括号须百分号编码。URL 只做结构校验,不验证可访问性、图片内容或文件大小。 +- `--form-cover-resource-id` 必须是当前 Base 已上传的非空图片资源 ID,不能含首尾空白;资源服务按当前操作者身份校验,转存 URL 同样受上述域名、端口和格式限制,拒绝或公开失败时不能继续写配置。资源公开无法随后续配置失败回滚;先回读配置,已有符合限制的目标公开 URL 时人工核验后使用 `--form-cover`,不重复公开或上传。内部鉴权 URL 不能代替资源 ID。 +- `--auth-type-code`:`0` 企业内、`1` 需登录的钉钉用户、`2` 指定成员、`6` 免登录。指定成员用 `--auth-data USER_ID_1,USER_ID_2`,当前组织内 1-100 人,由 MCP 转换内部身份;回读为含 userId/corpId 的成员 JSON。非指定成员模式允许显式 `--auth-data ""` 清空残留授权;未传保持原值,空字符串回读表示已清空,`null` 不表示清空。 +- 匿名 `--anonymous-submit` 不等于免登录。开启免登录时沿用页面规则,自动开启匿名、关闭提交后修改和单人次数限制;显式传入矛盾配置会失败。 +- 简单填写通知用 `--reply-notice true`,接收人用 `--share-uid-list USER_ID_1,USER_ID_2`(历史参数名,实际接受当前组织 userId,最多 100 人,空字符串清除)。这与 `form share notify` 催填是两种不同操作。 +- `form share notify --recipients USER_ID_1,USER_ID_2` 接受当前组织的 userId,1-100 人,由 MCP 转换并按实际成员去重;回执 `recipients` 为 userId。该命令会真实触达用户,需确认;超时或结果不明时不自动重发。 - `form share update` 的成功结果已经来自服务端回读并完成 CP 投影校验;必须检查 `shareFormUuid/status/formCover/cpSynced`,只有 `cpSynced=true` 才能确认闭环完成。部分失败或 `cpSynced=false` 不得描述为成功。 -- `form share update` 的成功结果还与本次请求逐项比对:响应 `baseId/tableId/viewId` 必须与请求一致,本次显式请求的 `enabled/formName/formDesc` 必须与响应回读值一致,不一致即返回 `partial_failure`。响应不回读的显式请求字段(如 `anonymousSubmit`、`authData`)列在 `data.unverified` 且 `verified=false`;这些字段只能报告“已提交但未验证”,不得声称已生效。 +- `form share update` 的成功结果还与本次请求逐项比对:响应 `baseId/tableId/viewId` 必须与请求一致,已回读的权限、匿名、查看范围、通知、时间和封面等请求配置按类型及语义比对,不一致返回 `partial_failure`(退出码 7)。数字按整数值比较;通知接收人和指定成员按外部 userId 集合比较,`authData` 回读的成员 JSON 不能直接写回;负数总次数上限归零,正数上限允许服务端归一为不超过请求值的非负值。旧服务端缺少回读、身份不可用或 `formCoverResourceId` 无资源对应证明时,列入 `data.unverified` 且 `verified=false`,只能报告“已提交但未验证”。 +- 历史成员无法转换时,`authData/shareUidList=null` 表示身份不可用,不能当作成员已清空。MCP 的安全提示保留在成功结果的 `data.warnings`;终态部分失败时保留在 `data.failed[0].error.details.warnings`。只保留提示的 `action/reason/priority`,不会自动执行建议或重放写入。 - DWS 不自行调用第二个 View 更新命令补偿 CP,也不拼装封面 URL。旧服务端发布窗口内 `formCover` 可能为空,应如实说明;稍后诊断当前配置时可用 `form share get`。 - “已开启分享”不等于“已允许匿名/免登录/组织外提交”;需按用户意图显式传入 `--anonymous-submit` 和 `--auth-type-code/--auth-data`。写超时时先查询真实状态,不原样重放 mutation。 diff --git a/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-doc/references/doc/doc-create.md b/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-doc/references/doc/doc-create.md index c688e533..ccb328a6 100644 --- a/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-doc/references/doc/doc-create.md +++ b/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-doc/references/doc/doc-create.md @@ -12,7 +12,7 @@ dws doc +create --name "<文档名>" --content @body.json --doc-format jsonml -- - 统一输入协议:已有或临时文件先暂存到当前工作目录后传 `@相对文件`;单次生成文本可用 `--content -` 从 stdin 读取。 - `@file` 禁止绝对路径和 `..` 逃逸;不要直接引用宿主临时目录。 -- `--name` 是文档名称;默认不要再在正文开头重复同名 H1,只有用户明确要求正文一级标题时才保留。 +- `--name` 是文档名称;省略时从 Markdown 首个一级标题提取,否则使用未命名文档。Markdown 开头与文档名相同的 H1 会自动移除,标题不同或属于缩进代码块则保留;JSONML 不做标题去重,确需保留正文同名 H1 时使用 JSONML。 - JSONML 顶层必须是数组。用户已经明确要求 callout、代码块、表格等具体富结构时,直接构造一份完整 JSONML 并只调用一次 `+create`;这不是开放式风格设计,不读取 `style/`。字段不确定时只读 [JSONML cookbook](format/doc-jsonml-cookbook.md),不再连读 create/update/style。 `+create` 负责创建、长 Markdown 分片和最终回读验证。正常成功结果至少包含: diff --git a/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-minutes/SKILL.md b/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-minutes/SKILL.md index e93ed126..c6dc6db6 100644 --- a/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-minutes/SKILL.md +++ b/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-minutes/SKILL.md @@ -1,6 +1,6 @@ --- name: dingtalk-minutes -description: 钉钉 AI 听记。Use when 查询或修改听记摘要、完整逐字稿、关键词、标签、行动项、录音、上传、思维导图、发言人洞察、ASR 热词/识别词配置或分享权限。写文档走 dingtalk-doc;建待办走 dingtalk-todo;日程走 dingtalk-calendar。命令前缀:dws minutes。 +description: 钉钉 AI 听记。Use when 查询或修改听记摘要、完整逐字稿、关键词、标签、行动项、录音、上传、听记内置思维导图、发言人洞察、ASR 热词/识别词配置或分享权限。独立 .amind 脑图文件的创建和主题读写走 dingtalk-misc 的 mind reference;写文档走 dingtalk-doc;建待办走 dingtalk-todo;日程走 dingtalk-calendar。命令前缀:dws minutes。 metadata: cli_version: ">=0.2.14" category: product @@ -52,7 +52,7 @@ metadata: | 上传音视频生成听记 | `dws minutes +upload --file <相对路径>` | 真实执行会上传文件并创建远端听记,必须按 Runtime confirmation;用户明确要闪记卡片时改用 `+upload-and-notify`,需要上传后等待分析产物时用 `+upload-and-analyze` | | 真实开始、暂停、继续或停止录音 | `+record-start` / `+record-pause` / `+record-resume` / `+record-stop` | 这组入口会真实执行。start 返回 `accepted=true, bound=false` 或 `controlReady=false` 时,报告“已受理但未绑定”并停止:不得重试 start,也不得用 `+latest`、列表第一条或时间最近项猜 ID。结束并等待产物用 `+record-wrap-up` | | 只预览录音请求,不实际执行 | `dws minutes record start --dry-run --format json` | 使用对应的原子 `minutes record start|pause|resume|stop` leaf;start 的 `--session-id` 可选,pause/resume/stop 必须传真实 `--id`。不要把被拒绝的 Shortcut dry-run 描述成预览成功 | -| 生成或继续思维导图 | `dws minutes +mindmap --id ` | 创建后有界轮询;超时或未知状态保留恢复信息,用 `--resume` 继续,不重复创建 | +| 生成或继续查询听记内置思维导图 | `dws minutes +mindmap --id ` | 创建后有界轮询;超时或未知状态保留恢复信息,用 `--resume` 继续,不重复创建 | | 生成或继续发言人洞察 | `dws minutes +speaker-insights --id ` | 只有 `state=ready/complete=true` 才完成;pending 保留 `taskId` 并同账号 `--resume [--task-id ]`,不重复创建;用户给定等待时长传 `--timeout` | | 当前用户申请查看/下载/编辑权限 | `dws minutes +apply-permission --id --permission view|download|edit` | 这是“我申请访问”,不是所有者给别人授权;按 Runtime confirmation 执行 | | 所有者给成员授权或撤权 | `dws minutes +share ...` / `dws minutes +unshare ...` | 先用通讯录把姓名解析为同组织稳定 UID;撤权是破坏性操作。批量结果必须保留逐成员 ledger 和失败项 | @@ -95,6 +95,10 @@ dws minutes +list-all --page-all --format json - 用户明确要求“仅预览/不实际写入”时,任务在真实现状读取、dry-run 计划和差异交付后结束;不得继续请求写入确认,也不得为了验证预览而执行真实写入后再还原。 - 分享/撤权使用稳定成员 UID,不能把姓名、手机号或跨组织 ID 直接当 UID;同一目标解析、读取、写入和验证必须使用同一 profile。 +## 脑图目标边界 + +`minutes +mindmap` 的目标是听记内置思维导图,结果展示在听记详情中,不创建可独立编辑的 .amind 文件。用户明确要求独立脑图或编辑已有脑图主题时,转 [独立脑图](../dingtalk-misc/references/mind.md)。已有听记 URL 不覆盖明确的独立文件意图;只有目标未明时才消歧。若用户明确另存独立脑图,可先读取获授权的听记内容,再按 mind reference 构造 OpenNodes;不套用内置生成的“禁止先读取内容”规则。 + ## 按需加载 Golden Route 参数足够时直接执行,不预读 Reference。每个 Case 最多先读取一个最精确的文件;参数事实优先读取 compact leaf Schema,不读取产品级全量 Catalog。 diff --git a/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-minutes/references/minutes.md b/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-minutes/references/minutes.md index 91f68a07..dbe48132 100644 --- a/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-minutes/references/minutes.md +++ b/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-minutes/references/minutes.md @@ -28,6 +28,8 @@ 摘要脚本的 `--dry-run` 只打印计划且不得调用 DWS;DWS 失败、非法 JSON 与合法空结果必须分开。摘要脚本没有覆盖完整分页、目标消歧与跨 scope 聚合,因此不能用它的输出声称“全部听记”。时间转换脚本始终纯本地,不提供或需要 `--dry-run`。行动项读取由 `+action-items`(单条)或 `+detail --ids ... --artifacts todos`(多条)承接,不再发布重复脚本。 +本页的思维导图命令仅生成听记内置产物;独立 .amind 文件创建和主题编辑走 dingtalk-misc 的 mind reference,不因提供听记 URL 而改变明确的独立文件目标。 + 命令前缀统一为 `dws minutes`。结构化读取加 `--format json`;参数不确定时查询精确 leaf: ```text diff --git a/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-misc/SKILL.md b/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-misc/SKILL.md index 15c752ac..93c3aaf4 100644 --- a/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-misc/SKILL.md +++ b/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-misc/SKILL.md @@ -1,6 +1,6 @@ --- name: dingtalk-misc -description: 钉钉独立白板(白板文件)的创建、绘图、保存和修改;文档内嵌白板仅在已有承载文档 nodeId 和 partId 时提供图形内容读写(dws whiteboard)。两类目标相互独立,不互相推断或切换;文档内白板卡片容器不属于本 skill。长尾产品集合技能,覆盖低频钉钉产品:OA审批查询与处理/法务智能合同/考勤/直播/DING紧急消息/开放平台应用与MCP开发/Agoal目标管理/日志日报周报/电子表格/开放平台文档搜索与OpenAPI逃生舱/DWS技能市场安装/组织大脑Hrbrain/原生Markdown/原生HTML/PAT行为授权/多组织profile。Use when 用户提到上述任一产品,尤其是 Agoal/目标管理/战略解码/经营合约及字段配置/计分卡/OKR/目标规则与周期/个人目标/目标模板/周月报规则提交统计/按时/迟交/未提交/跟催,或查询/创建合同台账、批量导入合同、按听记起草合同、归档合同、管理合同项目/相对方/账款,或查待审批/同意拒绝转交撤销审批/打卡/排班/日报周报内容填报/独立白板带内容创建/独立白板节点读写/已有 nodeId 和 partId 的文档内嵌白板图形内容读写/单元格读写/搜索安装技能/开发者后台应用/企业内部应用/查应用状态或能否删除/应用管理成员及角色汇总/应用版本历史/企业内部应用机器人配置、消息模式或在线状态/本机机器人连接器/Stream连接/MCP服务或工具/发布或调试MCP/获取或调用已发布MCP/把OpenAPI、Swagger、Postman、curl或HTTP接口做成MCP或给Agent使用/未封装OpenAPI/llms.txt/dws api/人才池/员工档案/职业历程/绩效/原生.md文件/Markdown版本比较/本地草稿diff/Markdown评论/原生.html文件/新建HTML页面/PAT授权/切换组织/跨组织/profile 等相关操作。周报/月报内容填报属于 report;规则级按时、迟交、未提交统计与人员跟催属于 Agoal;带审批人、抄送人或审批流的日报、周报、简报提交属于 OA 审批。未来审批任务或实例变化的实时监听不属于本 skill,应使用 dingtalk-event。命中后由本 skill 的「产品索引表」定位具体子产品和命令前缀,再按对应子产品说明执行。 +description: 独立脑图的带内容创建与主题读写使用 dws mind;空白脑图走 wiki,听记内置思维导图走 minutes。 钉钉独立白板(白板文件)的创建、绘图、保存和修改;文档内嵌白板仅在已有承载文档 nodeId 和 partId 时提供图形内容读写(dws whiteboard)。两类目标相互独立,不互相推断或切换;文档内白板卡片容器不属于本 skill。长尾产品集合技能,覆盖低频钉钉产品:OA审批查询与处理/法务智能合同/考勤/直播/DING紧急消息/开放平台应用与MCP开发/Agoal目标管理/日志日报周报/电子表格/开放平台文档搜索与OpenAPI逃生舱/DWS技能市场安装/组织大脑Hrbrain/原生Markdown/原生HTML/PAT行为授权/多组织profile。Use when 用户提到上述任一产品,尤其是 Agoal/目标管理/战略解码/经营合约及字段配置/计分卡/OKR/目标规则与周期/个人目标/目标模板/周月报规则提交统计/按时/迟交/未提交/跟催,或查询/创建合同台账、批量导入合同、按听记起草合同、归档合同、管理合同项目/相对方/账款,或查待审批/同意拒绝转交撤销审批/打卡/排班/日报周报内容填报/独立白板带内容创建/独立白板节点读写/已有 nodeId 和 partId 的文档内嵌白板图形内容读写/单元格读写/搜索安装技能/开发者后台应用/企业内部应用/查应用状态或能否删除/应用管理成员及角色汇总/应用版本历史/企业内部应用机器人配置、消息模式或在线状态/本机机器人连接器/Stream连接/MCP服务或工具/发布或调试MCP/搜索MCP市场/获取或调用已发布MCP/把OpenAPI、Swagger、Postman、curl或HTTP接口做成MCP或给Agent使用/未封装OpenAPI/llms.txt/dws api/人才池/员工档案/职业历程/绩效/原生.md文件/Markdown版本比较/本地草稿diff/Markdown评论/原生.html文件/新建HTML页面/PAT授权/切换组织/跨组织/profile 等相关操作。周报/月报内容填报属于 report;规则级按时、迟交、未提交统计与人员跟催属于 Agoal;带审批人、抄送人或审批流的日报、周报、简报提交属于 OA 审批。未来审批任务或实例变化的实时监听不属于本 skill,应使用 dingtalk-event。命中后由本 skill 的「产品索引表」定位具体子产品和命令前缀,再按对应子产品说明执行。 metadata: cli_version: ">=1.0.61" category: product @@ -13,12 +13,14 @@ metadata: ## 执行前路由 -本文件只负责产品路由。先由下表确定唯一产品:单一、清晰的 Whiteboard、Attendance、Report、Sheet 或 Dev 任务直接读取对应 reference(内含该任务所需的最小执行契约);其它产品先读取 [`dingtalk-shared`](../dingtalk-shared/SKILL.md),再读取唯一产品 reference。仅在实际触发认证、profile、确认或错误恢复时补读一份精确 shared reference,不做冷启动预读。 +本文件只负责产品路由。先由下表确定唯一产品:单一、清晰的 Mind、Whiteboard、Attendance、Report、Sheet 或 Dev 任务直接读取对应 reference(内含该任务所需的最小执行契约);其它产品先读取 [`dingtalk-shared`](../dingtalk-shared/SKILL.md),再读取唯一产品 reference。仅在实际触发认证、profile、确认或错误恢复时补读一份精确 shared reference,不做冷启动预读。 同一请求同时出现日报、周报或简报名称与审批人、抄送人、审批路径、审批单等审批意图时,审批意图优先,统一路由 OA 并读取 `oa.md`;本次任务不得执行 `dws report`。OA 中没有同名模板也不能降级提交 Report,只能交付 OA 搜索结果或请求消歧。只有用户明确要求提交钉钉日志,且没有审批流语义时,才路由 Report。 Attendance 任务直接按产品索引读取一份最匹配的 `attendance*.md`,不要重复预读 `dingtalk-shared`。只有出现跨产品编排、profile/认证问题、未知全局错误或 Reference 明确指向 shared 时,才按需读取 shared 对应内容。 +用户要求独立脑图文件的带内容创建、主题查询或增删改时,直接读取 [mind.md](references/mind.md)。只创建空白脑图文件走 `wiki node create --type amind`;听记内置思维导图走 `dingtalk-minutes`。仅提到“脑图/思维导图”且目标不明时先消歧;明确独立文件需求不得路由听记。OpenNodes 不是白板专属触发词,应按目标产品和 catalogVersion 区分,不把 mind-v1 传给白板。 + 用户明确要求创建或操作白板文件、独立白板时,直接读取 [whiteboard.md](references/whiteboard.md),无需先读 Doc 或 shared 路由。课表、日历、流程图等是内容形态,不能据此把文档内嵌目标改成独立白板。文档内嵌白板的卡片容器操作走 `dingtalk-doc`;只有已有承载文档 `nodeId` 和 `partId` 的图形内容操作才读取 whiteboard reference。缺少任一身份时停止并补齐,禁止转成独立白板。 ## 产品索引表 @@ -30,14 +32,15 @@ Attendance 任务直接按产品索引读取一份最匹配的 `attendance*.md` | 考勤 / 打卡 / 班次 / 考勤组 / 排班 / 考勤报表 / 假期余额 | 考勤记录、规则与配置、排班、报表、假期 | `dws attendance` | 日常查询/规则/设置:[attendance.md](references/attendance.md);排班导入或排班表导出:[attendance-schedule.md](references/attendance-schedule.md);考勤 Excel/报表导出:[attendance-report.md](references/attendance-report.md);假期/余额:[attendance-vacation.md](references/attendance-vacation.md) | | 直播 / 我的直播 / 直播列表 | 直播列表与直播记录查询 | `dws live` | [live.md](references/live.md) | | DING / 紧急通知 / 电话DING / 短信DING / 必达消息 | DING 紧急消息(应用内/短信/电话),个人DING | `dws ding` | [ding.md](references/ding.md) | -| 开放平台应用 / 企业内部应用 / 应用状态或删除条件 / 应用管理成员 / 应用权限 / 应用版本或版本历史 / agentId / clientId / 应用机器人配置 / 机器人消息模式或在线状态 / 本机机器人连接器 / Stream连接 / 版本发布 / connect / MCP 服务开发 / MCP 工具发布 / MCP 凭证 / OpenAPI或Swagger或Postman或curl转MCP / 接口给Agent用 / 已发布MCP调用 | 开放平台企业内部应用,以及 MCP 服务、工具、鉴权、凭证、协作者和发布后动态调用 | `dws dev` / `dws dev mcp` / `dws mcp published` | 按下方「Dev 直达规则」读取一个 `references/dev/*.md`;意图不明时才读 [dev-index.md](references/dev/dev-index.md) | +| 开放平台应用 / 企业内部应用 / 应用状态或删除条件 / 应用管理成员 / 应用权限 / 应用版本或版本历史 / agentId / clientId / 应用机器人配置 / 机器人消息模式或在线状态 / 本机机器人连接器 / Stream连接 / 版本发布 / connect / MCP 服务开发 / MCP 工具发布 / MCP 凭证 / OpenAPI或Swagger或Postman或curl转MCP / 接口给Agent用 / 已发布MCP调用 / MCP市场搜索 | 开放平台企业内部应用,以及 MCP 服务、工具、鉴权、凭证、协作者、发布后动态调用和市场搜索 | `dws dev` / `dws dev mcp` / `dws mcp published` / `dws mcp market search` | 按下方「Dev 直达规则」读取一个 `references/dev/*.md`;意图不明时才读 [dev-index.md](references/dev/dev-index.md) | | DEAP 数字员工 / 数字员工 / 人设 / 群感知 / DSH 接入 | 兼容路由;完整自然语言编排只由独立 `dingtalk-tag` skill 维护 | `dws dingtalk-tag` | 切换 [`dingtalk-tag`](../dingtalk-tag/SKILL.md) | | 目标管理 / 战略解码 / 经营合约字段 / 计分卡 / OKR / 目标规则周期 / 个人目标 / 目标模板 / 周月报规则统计 / 按时迟交未提交 / 跟催 | Agoal 目标管理与经营目标跟进 | `dws agoal` | [agoal.md](references/agoal.md) | | 日报 / 周报 / 月报 / 写日志 / 收件箱日志 / 发件箱日志 | 日志(日报/周报/月报)查询与按模版提交 | `dws report`(别名 `dws log`) | [report.md](references/report.md) | | 电子表格 / 工作表 / 单元格读写 / 公式 / 超链接 / 浮动图片 | 电子表格创建/读写/公式/超链接/浮动图片/导出 | `dws sheet` | [sheet.md](references/sheet.md) | | 开放平台文档 / API文档 / 接口文档 / 接口报错 | 开放平台开发文档搜索 | `dws devdoc` | [devdoc.md](references/devdoc.md) | | 未封装 OpenAPI / llms.txt / dws api / Raw API / API 逃生舱 | 官方 llms.txt 分层发现,仅对企业内部应用 App Token 服务端 API 生成并确认 Raw 调用 | `dws api` | [openapi-explorer.md](references/openapi-explorer.md) | -| 白板文件 / 独立白板 / 保存到白板 / 画布 / OpenNodes / 白板节点 / SVG 预渲染 | 独立白板的带内容创建、本地 SVG 预渲染、读取、写前 diff 预览和更新 | `dws whiteboard` | [whiteboard.md](references/whiteboard.md) | +| 独立脑图 / 带内容创建脑图 / 脑图主题查询和增删改 | 独立 .amind 文件的主题树;不包含听记内置思维导图生成或空白文件创建 | `dws mind` | [mind.md](references/mind.md) | +| 白板文件 / 独立白板 / 保存到白板 / 画布 / 白板 OpenNodes / 白板节点 / SVG 预渲染 | 独立白板的带内容创建、本地 SVG 预渲染、读取、写前 diff 预览和更新 | `dws whiteboard` | [whiteboard.md](references/whiteboard.md) | | 已有承载文档 nodeId 和 partId 的文档内嵌白板图形内容 | 只读写该 part 内的图形内容;卡片容器插入、定位、删除走 `dingtalk-doc`;身份缺失时停止,禁止改成独立白板 | `dws whiteboard --node ... --part-id ...` | [whiteboard.md](references/whiteboard.md) | | 搜索技能 / 找技能 / 安装技能 / 技能市场 / 安装 DWS mono 或 multi skill | DWS 技能市场搜索、下载、安装与内置技能部署 | `dws skill` | [skill.md](references/skill.md) | | 人才池 / 储备干部池 / 员工档案 / 职业历程 / 绩效记录 / 员工标签 / 组织大脑 / 人才搜索 | 组织大脑:人才池、员工档案专项模块与结构化人才搜索 | `dws hrbrain` | [hrbrain.md](references/hrbrain.md) | diff --git a/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-misc/references/dev/app.md b/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-misc/references/dev/app.md index 17bcd96e..8bd0bb10 100644 --- a/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-misc/references/dev/app.md +++ b/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-misc/references/dev/app.md @@ -13,6 +13,7 @@ dws dev app get --unified-app-id --format json dws dev app get --app-key --format json # 写操作:同一条命令先预检,再正式执行 +# create 的 --desc 服务端必填(缺省返回 67010 DESC_INVALID),名称仅限中英文/数字/指定标点 dws dev app create --name <名称> --desc <描述> --dry-run --format json dws dev app update --unified-app-id --desc <描述> --dry-run --format json dws dev app disable --unified-app-id --dry-run --format json diff --git a/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-misc/references/dev/connect.md b/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-misc/references/dev/connect.md index f75f271c..6e9c178b 100644 --- a/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-misc/references/dev/connect.md +++ b/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-misc/references/dev/connect.md @@ -31,4 +31,6 @@ dws dev connect --daemon --unified-app-id --channel --format json 前台 connect 是长驻进程,不要在评测/对话 shell 里阻塞运行;需要可管理实例时用 daemon。daemon 启动后重新执行 `list --json`,用新记录的 `clientId` 做 status/restart/stop 定位。dry-run 的 `completionState=LOCAL_DEBUG_ONLY`、`doesNotPublish=true` 表示只完成本地调试准备。凭证优先由 `unifiedAppId` 自动取得,密钥不落盘、不回显;缺本地渠道 CLI 时按 dry-run 的 `installed/autoInstall/installHint` 报告,未经用户要求不扩展成系统安装排障。 +Pi 使用 `--channel pi`,复用 Pi 自身的登录和模型配置;`--agent-model` 可指定 `provider/model`。按会话隔离记忆,`/new`、`/clear` 重置会话映射;需要禁用工具时显式使用 `--agent-permission-mode ask`,或在未配置权限模式及其环境变量时使用 `--agent-approval-mode ask`。不存在 `--ask` 参数,`--yolo=false` 也不会关闭默认高权限模式。原生 Pi 通道不要同时设置 `--agent-cmd`,也不要用 `DWS_AGENT_CMD` 覆盖命令。 + 同一 restart/connect 错误且记录、参数、状态没变化时停止,不重复 help、环境探测或重启。已知路径直接执行;仅当前 leaf flag 确有疑问时查一次 compact Schema,Schema 不可用才查一次精确 leaf help。 diff --git a/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-misc/references/dev/dev-index.md b/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-misc/references/dev/dev-index.md index adf17c3c..f5439f39 100644 --- a/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-misc/references/dev/dev-index.md +++ b/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-misc/references/dev/dev-index.md @@ -8,6 +8,7 @@ | AppKey、AppSecret、clientId、agentId | `dws dev app credentials get` | [`credentials.md`](./credentials.md) | | 开放平台应用的管理成员、成员角色与角色汇总 | `dws dev app member ...` | [`member.md`](./member.md) | | 应用权限 | `dws dev app permission ...` | [`permission.md`](./permission.md) | +| 开发者后台应用的可见人员范围(部门/角色/人员,查询或全量设置) | `dws dev app scope get` / `dws dev app scope set` | [`scope.md`](./scope.md) | | IP 白名单、回调或 SSO 地址 | `dws dev app security config` | [`security.md`](./security.md) | | H5、PC 主页与 OMP 地址 | `dws dev app webapp ...` | [`webapp.md`](./webapp.md) | | 机器人配置、启停、提交审核 | `dws dev app robot ...` | [`robot.md`](./robot.md) | diff --git a/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-misc/references/dev/mcp.md b/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-misc/references/dev/mcp.md index d13a8f11..fb4273fc 100644 --- a/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-misc/references/dev/mcp.md +++ b/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-misc/references/dev/mcp.md @@ -3,7 +3,7 @@ 命令分为两层: - `dws dev mcp ...`:开发端,管理服务、工具、鉴权、凭证、协作者和 HSF 方法。 -- `dws mcp published ...`:消费端,按 `mcpId` 查看或调用已发布工具。 +- `dws mcp market search` / `dws mcp published ...`:消费端。只知道名称时先按关键词搜索 MCP 市场拿到 `mcpId`,再按 `mcpId` 查看或调用已发布工具。 远端 `serverName` 和工具名不会注册成新的顶层命令,也不写入跨身份缓存。消费端每次按当前登录 profile、用户和组织身份解析服务地址;输出只展示脱敏地址。 @@ -26,7 +26,7 @@ 1. 下列已知 leaf 直接执行,不先读分组 help。只有创建/更新等复杂字段或确认语义确有不确定性时,读取一次 `dws schema --cli-path "dev mcp " --compact --format json`;Schema 与 Cobra 不一致时才读取同一 leaf 的一次 help。 2. 开发写操作的最初请求只允许 `--dry-run --format json`。展示 invocation 中的准确对象、动作、业务参数和影响,等用户对该预览明确确认后,才把同一命令仅由 `--dry-run` 换成 `--yes`;目标或业务参数变化就重新预检并确认,确认前不得真实写入。 3. 发布前执行 `tool get` 和 `tool debug`,核对输入、输出与映射。 -4. 调用已发布工具前先执行 `mcp published tools `,按返回的 `inputSchema` 构造 `--params`;真实 invoke 还会在本次调用中重新发现工具并按实时 Schema 校验。 +4. 想获取新的 MCP,或想知道市场里有哪些 MCP 但不知道 mcpId 时,先执行 `dws mcp market search --keyword <关键词>` 从 MCP 市场取 `mcpId`,不要猜测或编造。该命令只覆盖已上架市场(MARKET)的 MCP;企业内发布、未上架市场(PUBLISHED)的 MCP 不在结果里,有开发权限的可用 `dws dev mcp service list` 查。调用已发布工具前先执行 `mcp published tools `,按返回的 `inputSchema` 构造 `--params`;真实 invoke 还会在本次调用中重新发现工具并按实时 Schema 校验。 5. `mcp published invoke` 无法静态判断远端工具副作用,真实调用一律需要 `--yes`;未明确影响时只做 dry-run。 6. MCP URL、凭证内容、token 和密钥不得写入回答、日志、文档或代码仓库。 7. 若真实命令返回 `endpoint_not_resolved`,表示当前 CLI 的 `mcpdev` 运行端点不可用,不是 flag 错误;立即报告并停止,不用 help、改参数、Raw API 或 dry-run 冒充业务完成。 @@ -122,6 +122,14 @@ dws mcp published invoke --help 确认帮助中包含 `inputSchemaDigest (fresh_core_subset_snapshot)`。若没有,先运行 `dws upgrade` 升级到最新**稳定版**并重新检测。不要安装/建议 beta;能力仍缺失时停止真实调用,只可展示本地 dry-run。 +只知道名称时先搜市场(只读,无需确认): + +```bash +dws mcp market search --keyword <关键词> --format json +``` + +`mcpId` 在 `data.mcps[].mcpId`。搜索是模糊匹配,带 `mcp` 这类通用词会命中大量结果,要按 `name`、`corpName`、`official` 甄别后再用。续页把 `meta.pagination.next_token` 原样传给 `--cursor`。 + ```bash dws mcp published tools --format json dws mcp published invoke \ diff --git a/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-misc/references/dev/scope.md b/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-misc/references/dev/scope.md new file mode 100644 index 00000000..699eda6d --- /dev/null +++ b/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-misc/references/dev/scope.md @@ -0,0 +1,36 @@ +# 应用可见范围(开放平台应用) + +面向开放平台应用(企业内部应用/三方应用)的可见人员范围,不是钉盘、文档或知识库的分享范围。 + +## 查询 + +```bash +dws dev app scope get --unified-app-id --format json +``` + +- 只读;返回部门/角色/人员等可见范围配置。 + +## 设置(全量替换语义) + +```bash +# 第 1 步(可选):本地调用预览,--dry-run 不发起任何远端调用,不是服务端预检 +dws dev app scope set --unified-app-id \ + --user-visible-scopes \ + --precheck-only --dry-run --format json + +# 第 2 步:服务端预检——真实调用,只生成计划不写入;经用户确认后执行 +dws dev app scope set --unified-app-id \ + --user-visible-scopes \ + --precheck-only --yes --format json + +# 第 3 步:核对计划(尤其将被移除的范围)并确认后,正式执行 +dws dev app scope set --unified-app-id \ + --user-visible-scopes \ + --precheck-only=false --yes --format json +``` + +- `--dry-run` 是本地调用预览(不访问服务端);服务端预检必须用 `--precheck-only --yes` 真实调用,返回计划与替换影响,不会写入。 +- `scope set` 是**全量替换**:本次未传入的既有范围会被移除;增量添加前先 `scope get` 确认现状并在同一次调用带上完整集合。 +- 范围列表支持 CSV 或 JSON 数组;`[]` 或 `,` 转换后为空会在本地直接拒绝。 +- `--is-hidden` 控制应用可见性;显式传 `--is-hidden=false` 会被保留传递,不会丢失。 +- 正式执行前必须完成服务端预检并取得用户对计划(含将被移除范围)的确认;参数变化需重新预检。 diff --git a/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-misc/references/mind.md b/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-misc/references/mind.md new file mode 100644 index 00000000..97cdccb3 --- /dev/null +++ b/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-misc/references/mind.md @@ -0,0 +1,77 @@ +# 钉钉独立脑图 + +本页用于独立 `.amind` 脑图文件的带内容创建、主题查询和节点增删改。 + +| 目标 | 入口 | +|---|---| +| 创建带页面、主题和层级的独立脑图,或修改其中主题 | `dws mind` | +| 只在知识库创建空白脑图文件,不初始化主题内容 | `dws wiki node create --type amind` | +| 生成听记内置思维导图(结果在听记详情中) | `dws minutes +mindmap --id `;multi 使用 dingtalk-minutes | +| 操作白板形状、坐标、连接线和样式 | `dws whiteboard` | + +不能仅凭“OpenNodes”选择白板:脑图使用 `catalogVersion=mind-v1` 的页面/主题树,白板是不同协议。用户提供听记内容但明确要求另存独立脑图时,读取获授权的内容后走 `mind create`;不能用听记内置思维导图生成代替独立文件。若目标在听记内还是独立文件不明确,先消歧。 + +## 按需执行 + +只用 `dws`,结构化输出加 `--format json`;身份由登录/profile 和网关提供,不拼 HSF、HTTP 或 uid/组织参数。读写使用同一明确 profile,文件、页面、主题 ID 从真实返回取得。 + +已知命令直接使用。需要构造或理解 OpenNodes 时,读取 [脑图 OpenNodes 协议与十节点示例](./mind/open-nodes-v1.md);普通查询不预读协议。参数或安全契约不明时查一次精确叶子 Schema,例如 `dws schema --cli-path 'mind node update' --compact --format json`。命令/参数漂移按公共 DWS 规则处理,不枚举全 Catalog。 + +写入按 Runtime/Schema 的确认要求执行;用户已确认同一目标、内容和影响时不重复索要确认,确认后才添加 `--yes`。下面示例不预置确认。不要求脑图执行白板的 render、+diff 或 sourceDigest 流程;脑图没有这些命令。可用 `--dry-run` 校验和预览请求摘要,`executed=false` 不代表远端已创建。 + +## 五个命令 + +| 命令 | 用途与关键输入 | +|---|---| +| `mind create` | `--name`、`--source`、`--request-id`;可选 `--workspace` 或 `--folder` | +| `mind node list` | `--node-id`;可选 `--mode`、`--page-id`、`--topic-id`、`--depth`、`--max-results`、`--next-token` | +| `mind node create` | 文件/页面/真实父主题 ID、`--topic` JSON 或文件、`--request-id`;可选 `--before-topic-id`、`--expected-version` | +| `mind node update` | 文件/页面/主题 ID、`--text`、`--request-id`;可选 `--expected-version` | +| `mind node delete` | 文件/页面/主题 ID、`--request-id`;可选 `--expected-version`;删除主题及整个子树 | + +### 携带内容创建 + +先准备合法 source;名称最多 128 个 UTF-16 单元,不截断。示例中的文件须由本次任务准备: + +```bash +dws mind create --workspace WORKSPACE_ID --name '项目计划' --source ./mind.json --request-id mind-create-001 --format json +``` + +`--source` 接受内联 JSON 或文件路径;CLI 校验后以 JSON 字符串发送,不手动再套一层 JSON 字符串。folder 与 workspace 同传时 folder 优先;它们不是同一种 ID。不传位置会创建到“我的文档”,创建成功不豁免后续组织和资源权限校验。`data.message` 是使用提示,不是错误或授权证明。 + +创建以 CLI `outcome=success`、`data.status=READY`、`data.requestedContentApplied=true` 确认。记录 `data.nodeId`、`data.pageIdMap`、`data.nodeIdMap`;临时 ID 不得当成后续真实 ID。需要验收初始化内容时按下面查询方式读回目标页。 + +### 查询、分页及写前定位 + +```bash +dws mind node list --node-id FILE_ID --format json +dws mind node list --node-id FILE_ID --page-id PAGE_ID --topic-id ROOT_TOPIC_ID --mode SUBTREE --depth 8 --max-results 200 --format json +``` + +- `--node-id` 是脑图文件 ID;`--page-id` 是真实页面 ID;`--topic-id` 是该页面中的真实主题 ID,三者不可互换。 +- 默认 ROOT 发现各页及根主题;必须省略 topic-id,next-token 可省略或为空。NODE 读指定主题,CHILDREN 读直接子主题,SUBTREE 读起点及限定深度的后代;后三者均要求 page-id/topic-id。 +- CLI 返回 `data.items[]` 页面及 `items[].nodes[]` 主题;从中取得真实 ID。MCP 的原生 pages/nodes 数组已由 CLI 按 pageId 归组,不需要对结果再做 JSON 字符串解码。 +- depth 为 1..8,起点计第 1 层;max-results 为 1..200。一次命令只读一页,下一页使用 `meta.pagination.next_token`,保持身份、文件、页面、主题、模式、depth 和 max-results 不变。 +- `endpoint_exhausted=true` 仅说明本次查询没有下一页;`data.truncated=true` 仍可能表示深度截断,不能宣称完整读取整棵树。非空 token 为 `mindc1_` 加 43 个 Base64URL 字符,不自行构造或修改。 +- 写入前用已有的有效查询结果定位目标、确认 `writeSupport` 和版本;缺少时查询所需范围,不重复读取整个脑图。根主题可作为新增子主题的父节点,不能通过工具修改或删除。受保护、不支持写入的主题不强行覆盖。 + +### 新增、编辑与删除 + +```bash +dws mind node create --node-id FILE_ID --page-id PAGE_ID --parent-topic-id ROOT_TOPIC_ID --topic '{"id":"task-1","type":"topic","text":"任务"}' --request-id mind-add-001 --expected-version 4 --format json +dws mind node update --node-id FILE_ID --page-id PAGE_ID --topic-id TOPIC_ID --text '' --request-id mind-clear-001 --expected-version 5 --format json +dws mind node delete --node-id FILE_ID --page-id PAGE_ID --topic-id TOPIC_ID --request-id mind-delete-001 --expected-version 6 --format json +``` + +示例 request-id 仅展示格式;每个新操作使用独立的稳定标识,恢复沿用原值和全部参数。版本示例仅展示格式;执行时使用实际查询返回的 `data.readVersion`,不得照抄示例数字。新增返回的 nodeIdMap 将临时主题 ID 映射成真实 topicId;插入位置 before-topic-id 必须属于同一父主题。`--text` 必须显式提供,空字符串表示清空;不把缺失或 null 当清空。删除覆盖整个子树,先明确后代影响。 + +CLI 会把 topic 编码为 MCP 的 node JSON 字符串,把 text 编成 patch JSON 字符串;调用者不传 checkpoint、内部模型树或 HSF 请求类包装。 + +## 完成与恢复 + +- 节点写入以 `outcome=success`、`status=APPLIED`、`applied=true` 确认;编辑的 NO_CHANGE 表示服务端确认文本已一致,无须再写。删除数量仅在 deletedCountKnown=true 时可信。 +- INITIALIZING / COMMITTED_UNVERIFIED 可返回 `outcome=pending`,不是完成。使用返回的恢复信息及 `meta.operation.next_command`,保留原 request-id 和全部业务参数;恢复命令会携带可解析的原 profile,多行内容使用 Bash/zsh 引用并保留尾部换行。其他 shell 请按原参数恢复并明确指定同一 profile;不自动循环重放,也不附加未经确认的 `--yes`。 +- UNKNOWN、网络超时或效果未明时,不换 request-id 重新提交;原请求有界恢复后仍未知则停止并保留回执/trace。明确 CONFLICT 后先重新查询,再决定是否以新 request-id 发起新操作,不能把未知提交当冲突重做。 +- 原始 MCP `success=true` 不等于写入已确认;以 CLI outcome、业务状态和确认字段联合判断。需要确认实际内容或排查未决状态时,执行同一文件的有界只读核对。 +- 失败保留稳定错误码、阶段和 trace;公共 runner 提取的错误类别位于 `error.details.error_category`。TIMEOUT / VALIDATION / DOWNSTREAM 不授予自动重试权限;DOWNSTREAM 可表示权限、资源、版本、保护或功能未启用,不应统一解释成入参错误。 +- mind.feature.disabled 时报告能力尚未启用或未放量,不修改开关、不换组织或调用入口绕过;权限拒绝也不切换目标或账号。message 不覆盖错误说明。 diff --git a/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-misc/references/mind/open-nodes-v1.md b/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-misc/references/mind/open-nodes-v1.md new file mode 100644 index 00000000..6ed2d659 --- /dev/null +++ b/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-misc/references/mind/open-nodes-v1.md @@ -0,0 +1,66 @@ +# 脑图 OpenNodes mind-v1 + +[返回脑图操作入口](../mind.md)。仅在携带内容创建或需要理解主题结构时读取本页。 + +## 开放结构与传输 + +创建 source 包含 `schemaVersion="1.0"`、`catalogVersion="mind-v1"` 和 pages 数组。每页包含 id、title、nodes;每个主题包含 id、type="topic"、text,可选 parentId。这是脑图协议,不能使用白板的 shape、坐标、连接线、样式或其 OpenNodes 示例。 + +| 字段/边界 | 约束 | +|---|---| +| source | UTF-8,最多 2 MiB,1..20 页,总计最多 10000 个主题 | +| 页面 id、主题 id | 客户端临时 ID,匹配 `[A-Za-z0-9][A-Za-z0-9._:-]{0,127}`;页面 ID 不重复,主题 ID 在全部页面中不重复 | +| 页面 title | 1..255 个 Unicode 字符;与创建显示名称的 128 个 UTF-16 单元限制不同 | +| 主题 text | 必须显式提供字符串,最多 10000 个 Unicode 字符;允许空字符串,空格和换行原样保留 | +| parentId | 根主题必须省略,不能用 null 或空字符串代替;其他主题引用同页父主题 | +| 树结构 | 每页恰好一个根,无环、无孤儿、无跨页父引用,最多 64 层(根为第 1 层);父主题可出现在数组后面,同父兄弟顺序按输入相对顺序 | +| 字段 | 拒绝未知字段、重复 JSON key、错误类型或缺失必填字段,不传内部 checkpoint/模型协议 | + +CLI 的 `--source` 接受文件或内联 JSON,`--topic` 接受仅含 id/type/text 的 JSON 对象或文件(最多 128 KiB),`--text` 接受原始文本。CLI 前置校验发生在远端调用前,dry-run 也校验;非法输入不会提交给 MCP。 + +MCP 将嵌套内容定义为 JSON string,CLI 负责一次编码: + +| CLI 输入 | MCP 业务参数 | +|---|---| +| `--source ./mind.json` | source:文件内容对应的 JSON 字符串 | +| `--topic '{"id":"temp","type":"topic","text":"任务"}'` | node:JSON 字符串,不是嵌套对象 | +| `--text ''` | patch:字符串 `{"text":""}`,不是 null 或缺失 text | + +不要把整个 source 再编码成带外层引号的 JSON 字符串文件,也不要手写 MindCreateRequest、uid 或组织包装。回参的数组和 ID 映射仍为原生数组/对象,不转字符串。 + +## 一页十节点示例 + +下面内容保存为 `mind.json`:一个根主题加九个子主题,总计十个;n9 特意保留空文本。 + +```json +{ + "schemaVersion": "1.0", + "catalogVersion": "mind-v1", + "pages": [ + { + "id": "p1", + "title": "项目计划", + "nodes": [ + {"id": "root", "type": "topic", "text": "项目计划"}, + {"id": "n1", "type": "topic", "text": "目标", "parentId": "root"}, + {"id": "n2", "type": "topic", "text": "需求", "parentId": "root"}, + {"id": "n3", "type": "topic", "text": "方案", "parentId": "root"}, + {"id": "n4", "type": "topic", "text": "开发", "parentId": "root"}, + {"id": "n5", "type": "topic", "text": "测试", "parentId": "root"}, + {"id": "n6", "type": "topic", "text": "发布", "parentId": "root"}, + {"id": "n7", "type": "topic", "text": "风险", "parentId": "root"}, + {"id": "n8", "type": "topic", "text": "跟进", "parentId": "root"}, + {"id": "n9", "type": "topic", "text": "", "parentId": "root"} + ] + } + ] +} +``` + +本地预览: + +```bash +dws mind create --name '项目计划' --source ./mind.json --request-id mind-create-001 --dry-run --format json +``` + +预览应返回 pageCount=1、topicCount=10、executed=false;这不是远端创建证明。正式创建位置、确认、返回 ID 与回读方式见操作入口。创建后使用 pageIdMap/nodeIdMap 或查询结果中的真实 ID,不能直接用 p1、root、n1 等临时值编辑文件。 diff --git a/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-misc/references/whiteboard.md b/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-misc/references/whiteboard.md index 82bda159..d263f294 100644 --- a/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-misc/references/whiteboard.md +++ b/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-misc/references/whiteboard.md @@ -1,5 +1,7 @@ # 钉钉白板内容操作 +OpenNodes 不是白板专属名称:独立脑图的 mind-v1 页面/主题树使用 [mind.md](./mind.md),不能传给白板;仅提到 OpenNodes 而目标未明时先消歧。 + 本页覆盖两个相互独立的目标,不能互相推断、转换或回退: - 独立白板是 `.adraw` 白板文件,使用白板自身 `nodeId`,不存在文档 `partId`。 diff --git a/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-tag/SKILL.md b/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-tag/SKILL.md index 0ebf839d..2f37cdb4 100644 --- a/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-tag/SKILL.md +++ b/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-tag/SKILL.md @@ -33,7 +33,7 @@ metadata: ## 自然语言编排硬约束 -- 创建/更新的 MCP 主程序类型字段为 `digitalTagEmployeeProfile.type`,列表过滤使用顶层 `type`,仅支持 `open_code`、`local_agent`,`a2a` 暂不支持。CLI 参数为 `--type`;创建时必须显式传 `--type open_code|local_agent`,缺失或空值由 DWS 本地拦截;应告知调用方选择主程序类型,不能省略或发送空值。更新时未传则保持原值;明确接入本地 Agent/DSH 时传 `--type local_agent`。注意 `create`/`list`/`save-draft` 的 `--type` 是主程序类型,而 `detail` 使用 `--snapshot draft|published` 选择配置来源,不要混淆。 +- 创建/更新的 MCP 主程序类型字段为 `digitalTagEmployeeProfile.type`,列表过滤使用顶层 `type`,仅支持 `qwenwork`、`local_agent`,`a2a` 暂不支持。CLI 参数为 `--type`;创建时必须显式传 `--type qwenwork|local_agent`,缺失或空值由 DWS 本地拦截;应告知调用方选择主程序类型,不能省略或发送空值。更新时未传则保持原值;明确接入本地 Agent/DSH 时传 `--type local_agent`。注意 `create`/`list`/`save-draft` 的 `--type` 是主程序类型,而 `detail` 使用 `--snapshot draft|published` 选择配置来源,不要混淆。 - 创建时未提供 `--response-mode` 或值为空,CLI 默认发送 `mention_only`,包括 `local_agent`。更新时不传就不更新该字段,显式值保持不变;最终有效配置由服务端按当前草稿校验。 - 创建要求名称、描述和主程序类型;部门可不传,由服务端补操作人的主任职部门。不要向用户索要部门名称、岗位、工号或内部 uid。 - `save-draft` 只更新用户明确修改的基础草稿字段,不接受 Skill/MCP 完整数组。Skill/MCP 生命周期统一使用 `capability skill|mcp create|update|delete|list|query`;不设计 attach/detach,`check_mcp` 只在 create 和带配置的 update 内部调用。 diff --git a/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-tag/references/capability.md b/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-tag/references/capability.md index 02c40eaf..1e0eb5e3 100644 --- a/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-tag/references/capability.md +++ b/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-tag/references/capability.md @@ -17,6 +17,8 @@ dws dingtalk-tag capability skill query --agent-uuid --skill-id --format json 连接使用数字员工 Profile 启动 Event Consumer,调用与 dev connect 共用的 Agent 协议,以员工身份按回复模式更新 A2UI 卡片,或引用回复最终文本。后台结果只有在 ready 后才返回运行成功;不支持的 Agent 或缺失的依赖必须明确报错。 -支持 `qoder/qoderwork/workbuddy/claudecode/codebuddy/codex/gemini/opencode/custom`;custom 使用 `--agent-cmd`,问题作为最后一个参数、stdout 作为答案。模型、工作目录、会话和权限参数沿用 dev connect,模型推理是否远端执行由 Agent 自身决定。 +支持 `qoder/qoderwork/workbuddy/claudecode/codebuddy/codex/gemini/opencode/pi/custom`;custom 使用 `--agent-cmd`,问题作为最后一个参数、stdout 作为答案。模型、工作目录、会话和权限参数沿用 dev connect,模型推理是否远端执行由 Agent 自身决定。 + +Pi 使用 `--channel pi`,按员工与会话隔离记忆;需要禁用工具时显式使用 `--agent-permission-mode ask`,或在未配置权限模式及其环境变量时使用 `--agent-approval-mode ask`。不存在 `--ask` 参数,`--yolo=false` 也不会关闭默认高权限模式。Pi 尚未适配公开进度接口,数字员工按既有回退路径发送最终文本;不提供 A2UI 工具进度。Pi 直接音频、视频输入会被明确拒绝。 默认仅主管可用;`--allowed-users` 接收精确 userId 并在员工上下文解析,`--allowed-groups` 接收该上下文的群会话 ID,群消息仍需满足用户白名单。不要向用户索要其它内部人员标识。 @@ -50,7 +52,7 @@ dws dingtalk-tag manage create \ --dry-run --format json ``` -create 只创建草稿并返回 `agentUuid`。名称、描述和主程序类型必填;必须显式传 `--type open_code|local_agent`,不可为空;部门可不传,由服务端补操作人的主任职部门。create 支持可选 --prompt;local_agent 创建时省略则由 DWS 自动保存默认人设,发布时不会再写入人设。open_code 省略时仅提醒发布前补齐,不阻断草稿创建。显式空字符串或纯空白无效。其他发布所需配置由服务端校验。创建成功后必须立即保存 `agentUuid`;后续失败只从 detail/save-draft/publish 恢复。 +create 只创建草稿并返回 `agentUuid`。名称、描述和主程序类型必填;必须显式传 `--type qwenwork|local_agent`,不可为空;部门可不传,由服务端补操作人的主任职部门。create 支持可选 --prompt;local_agent 创建时省略则由 DWS 自动保存默认人设,发布时不会再写入人设。qwenwork 省略时仅提醒发布前补齐,不阻断草稿创建。显式空字符串或纯空白无效。其他发布所需配置由服务端校验。创建成功后必须立即保存 `agentUuid`;后续失败只从 detail/save-draft/publish 恢复。 ## 修改:按字段更新 diff --git a/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-tag/references/manage.md b/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-tag/references/manage.md index 2d93317a..86c647d0 100644 --- a/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-tag/references/manage.md +++ b/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-tag/references/manage.md @@ -6,7 +6,7 @@ ``` Usage: - dws dingtalk-tag manage create --name <名称> --description <职责描述> --type [flags] + dws dingtalk-tag manage create --name <名称> --description <职责描述> --type [flags] Flags: --name 必填,同组织内唯一(≤30 Unicode 码点) --description 必填,职责描述(≤300 码点) @@ -14,30 +14,30 @@ Flags: --dept-id 归属部门 ID;可选,省略时服务端补操作人主任职部门 --avatar-url 公网 HTTP(S) 头像地址,或本地图片路径(≤10 MiB) --supervisor-user-id 直属上级 userId - --type 必填:open_code | local_agent;必须显式填写且不能为空 + --type 必填:qwenwork | local_agent;必须显式填写且不能为空 --response-mode mention_only | targeted_proactive | mention_only,targeted_proactive;未提供或空值时默认 mention_only Example: - dws dingtalk-tag manage create --name "周报助手" --description "汇总并推送团队周报" --type open_code --prompt "你是周报助手,负责汇总团队进展。" --avatar-url ./avatar.png --dry-run --format json + dws dingtalk-tag manage create --name "周报助手" --description "汇总并推送团队周报" --type qwenwork --prompt "你是周报助手,负责汇总团队进展。" --avatar-url ./avatar.png --dry-run --format json ``` -只建草稿,不会上线。`--prompt` 对所有当前支持的类型均可选;显式空字符串或纯空白会被拒绝。`local_agent` 省略时自动保存默认人设;其他类型(当前为 `open_code`)省略时仅提醒发布前补齐,不阻断创建,也不套用本地 Agent 的默认人设。DWS 在同一次命令中先创建草稿,再保存人设及本地头像;后续保存失败时保留 `agentUuid`,使用 detail / save-draft 恢复,禁止重复创建。`--dry-run` 会展示待保存的人设且不调用远端。不传 `--dept-id` 时,OpenAPI 查询操作人主任职部门并补齐;CLI 不接收部门名称。`--avatar-url` 传 HTTP(S) 时直接使用,传本地 jpg/jpeg/png/gif/webp 时复用 Skill 本地文件上传封装,组合执行“先创建草稿 → 上传头像 → 回写草稿”;后两步失败时保留已创建的 `agentUuid`,禁止重复 create。用户只感知 `avatarUrl`,不需要手动调用上传接口。 +只建草稿,不会上线。`--prompt` 对所有当前支持的类型均可选;显式空字符串或纯空白会被拒绝。`local_agent` 省略时自动保存默认人设;其他类型(当前为 `qwenwork`)省略时仅提醒发布前补齐,不阻断创建,也不套用本地 Agent 的默认人设。DWS 在同一次命令中先创建草稿,再保存人设及本地头像;后续保存失败时保留 `agentUuid`,使用 detail / save-draft 恢复,禁止重复创建。`--dry-run` 会展示待保存的人设且不调用远端。不传 `--dept-id` 时,OpenAPI 查询操作人主任职部门并补齐;CLI 不接收部门名称。`--avatar-url` 传 HTTP(S) 时直接使用,传本地 jpg/jpeg/png/gif/webp 时复用 Skill 本地文件上传封装,组合执行“先创建草稿 → 上传头像 → 回写草稿”;后两步失败时保留已创建的 `agentUuid`,禁止重复 create。用户只感知 `avatarUrl`,不需要手动调用上传接口。 `create` 当前为 `confirmation=not_required`:先用 `--dry-run` 核对,确认参数无误后移除 `--dry-run` 执行即可,不要额外猜测或重复创建。 -`create` / `save-draft` 的 MCP 主程序类型字段为 `digitalTagEmployeeProfile.type`,仅支持 `open_code`、`local_agent`,`a2a` 暂不支持;CLI 对应参数为 `--type`。创建时必须显式传 `--type open_code|local_agent`,缺失或空值由 DWS 本地拦截,不自动选择类型;未提供 `--response-mode` 或值为空时,CLI 默认发送 `mention_only`(包括 `local_agent`)。更新时未传主程序类型或响应模式则不更新对应字段。注意:`create` / `list` / `save-draft` 的 `--type` 表示主程序类型,而 `detail` 使用 `--snapshot draft|published` 选择配置来源,分属不同子命令,不要混淆。主管参数和返回都使用字段名 `supervisorUserId`,字段值为当前组织内的 `userId`,不对用户暴露 uid/robotUid 概念。工号由平台管理,本命令不提供 `employee-no`。 +`create` / `save-draft` 的 MCP 主程序类型字段为 `digitalTagEmployeeProfile.type`,仅支持 `qwenwork`、`local_agent`,`a2a` 暂不支持;CLI 对应参数为 `--type`。创建时必须显式传 `--type qwenwork|local_agent`,缺失或空值由 DWS 本地拦截,不自动选择类型;未提供 `--response-mode` 或值为空时,CLI 默认发送 `mention_only`(包括 `local_agent`)。更新时未传主程序类型或响应模式则不更新对应字段。注意:`create` / `list` / `save-draft` 的 `--type` 表示主程序类型,而 `detail` 使用 `--snapshot draft|published` 选择配置来源,分属不同子命令,不要混淆。主管参数和返回都使用字段名 `supervisorUserId`,字段值为当前组织内的 `userId`,不对用户暴露 uid/robotUid 概念。工号由平台管理,本命令不提供 `employee-no`。 ## detail / list — 查询 ``` Usage: dws dingtalk-tag manage detail --agent-uuid [--snapshot draft|published] - dws dingtalk-tag manage list [--keyword <关键词>] [--type open_code|local_agent] [--page 1] [--page-size 20] + dws dingtalk-tag manage list [--keyword <关键词>] [--type qwenwork|local_agent] [--page 1] [--page-size 20] Example: dws dingtalk-tag manage detail --agent-uuid --format json dws dingtalk-tag manage list --keyword "周报" --type local_agent --format json ``` -`--keyword` 按名称或职责等可见基础信息模糊匹配,不对外提供工号搜索语义。`--type` 会映射到 MCP 的 `type`,仅支持 `open_code`、`local_agent`,不传表示不过滤。`--page` / `--page-size` 均不得小于 1。 +`--keyword` 按名称或职责等可见基础信息模糊匹配,不对外提供工号搜索语义。`--type` 会映射到 MCP 的 `type`,仅支持 `qwenwork`、`local_agent`,不传表示不过滤。`--page` / `--page-size` 均不得小于 1。 `detail` 的 `--snapshot` 默认为 `draft`;需要核对已发布配置时显式传 `--snapshot published`。该参数映射到 MCP 的 `snapshot` 字段。响应中的 `snapshot` 明确本次配置来源(draft/published),与生命周期 `status` 独立;未成功发布的员工查询 published 返回 `NOT_FOUND`,不回退到 dev 对象。曾发布后下架的员工仍可读取保留的 published 配置,`status=offline` 不代表本地 Agent 正在运行。Skill/MCP 资源也使用 snapshot。人员标识统一为 `userId`,直属上级的输入与输出字段统一为 `supervisorUserId`,数字员工 ID 统一为 `agentUuid`。 @@ -90,7 +90,8 @@ dws dingtalk-tag manage set-visibility --agent-uuid --visibility PAR ``` - `--agent-uuid`、`--visibility` 必填;`ALL` 表示本企业全员,`PARTIAL` 表示指定成员或部门。 -- `PARTIAL` 的 `--user-ids`、`--dept-ids` 至少一项非空;两项均支持英文逗号分隔或重复传入。 +- `PARTIAL` 的 `--user-ids`、`--dept-ids` 至少一项包含非空 ID;空白不算有效 ID。两项均支持英文逗号分隔或重复传入。 +- `--visibility` 只接受 `ALL` / `PARTIAL`;`ALL` 不接受非空成员或部门列表,避免误把限定范围设置成全员可见。上述约束在 dry-run 和真实执行时都会先由 CLI 本地校验,无效请求不调用 MCP。 - 这是**全量替换草稿可见范围**,不会自动发布。未传某一维度会清空该维度,不能把本次输入当作增量追加;`ALL` 无需成员/部门列表。 - `--user-ids` 接收当前组织内的 userId,MCP 请求字段仍是 `staffIds`。 - 先预览并让用户确认替换范围,再执行带 `--yes` 的命令。其他草稿字段保持不变。 diff --git a/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-tag/references/run.md b/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-tag/references/run.md index 5c0314bd..e940b367 100644 --- a/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-tag/references/run.md +++ b/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-tag/references/run.md @@ -16,7 +16,7 @@ Example: dws dingtalk-tag run run-status --agent-uuid --source-id --source-type im_message --format json ``` -- `run-status` 返回 `result`(1 成功 / -1 失败 / 0 运行中 / 2 中止)、`execStatus`、`runId`、`messageId` +- `run-status` 返回 `result`(人类可读的状态说明,字符串,例如“成功”,不是数字状态码)、`execStatus`、`runId`、`messageId`。程序判断执行状态应使用 `execStatus`,如 `RUNNING` / `SUCCESS`;不要把 `result` 按 1/-1/0/2 解析,也不要将未知或空状态视为成功。 - `trace` 返回 Langfuse 原始 trace JSON,**含完整对话与模型输入输出**;服务端先做管理者 / 触发人两级授权,无权返回 `NO_PERMISSION` - `trace` 输出为 `null`(无 data)**不是报错**,而是该来源暂无可用 trace:可能 trace 尚未就绪(异步写入有延迟)、来源 ID/类型不匹配,或该来源无对应执行;可稍后重试,或先用 `run-status` 确认执行是否存在 diff --git a/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-wiki/SKILL.md b/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-wiki/SKILL.md index da3e1fd2..4c36e013 100644 --- a/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-wiki/SKILL.md +++ b/plugins/codex/plugins/dingtalk-workspace/skills/dingtalk-wiki/SKILL.md @@ -11,6 +11,9 @@ metadata: # 钉钉知识库 Skill + +脑图创建边界:`wiki node create --type amind` 只创建空白文件,不携带初始化主题内容。用户要求按已有内容或层级创建脑图时,转 [独立脑图](../dingtalk-misc/references/mind.md) 的 `dws mind create`;不向 wiki 命令猜传 source。 + ## 最小 DWS 执行契约 diff --git a/plugins/codex/plugins/find-skills/THIRD_PARTY_SOURCE.json b/plugins/codex/plugins/find-skills/THIRD_PARTY_SOURCE.json index e38d54bd..36c71bcf 100644 --- a/plugins/codex/plugins/find-skills/THIRD_PARTY_SOURCE.json +++ b/plugins/codex/plugins/find-skills/THIRD_PARTY_SOURCE.json @@ -2,8 +2,8 @@ "sourceId": "find-skills", "repo": "https://github.com/vercel-labs/skills.git", "ref": "main", - "commit": "d8eca76d5dbe47df2d146d036e32534b74ece57a", + "commit": "699d35b21bd3c97dbc0edeae062b2698898bb2e5", "adapter": "claude-skill", "sourcePath": "skills/find-skills", - "syncedAt": "2026-10-28T01:57:49Z" + "syncedAt": "2026-10-28T15:59:59Z" } diff --git a/plugins/codex/plugins/mcp-playwright/MCP_SOURCE.json b/plugins/codex/plugins/mcp-playwright/MCP_SOURCE.json index 42f4f50f..6524dfc3 100644 --- a/plugins/codex/plugins/mcp-playwright/MCP_SOURCE.json +++ b/plugins/codex/plugins/mcp-playwright/MCP_SOURCE.json @@ -3,5 +3,5 @@ "name": "playwright浏览器自动化操作", "version": "20260605", "keySource": "none", - "syncedAt": "2026-10-28T02:01:13Z" + "syncedAt": "2026-10-28T16:03:24Z" } diff --git a/plugins/codex/plugins/next-skills/THIRD_PARTY_SOURCE.json b/plugins/codex/plugins/next-skills/THIRD_PARTY_SOURCE.json index 587ed2e9..22a0e94f 100644 --- a/plugins/codex/plugins/next-skills/THIRD_PARTY_SOURCE.json +++ b/plugins/codex/plugins/next-skills/THIRD_PARTY_SOURCE.json @@ -2,8 +2,8 @@ "sourceId": "next-skills", "repo": "https://github.com/vercel/next.js.git", "ref": "canary", - "commit": "7356b0bcb5f22371c15dcb413afe5fb28412f52d", + "commit": "f922205f9e860de24673269f81dea51e9e33b0cb", "adapter": "skill-collection", "sourcePath": "skills", - "syncedAt": "2026-10-28T01:57:49Z" + "syncedAt": "2026-10-28T15:59:59Z" } diff --git a/plugins/codex/plugins/ppt-master/THIRD_PARTY_SOURCE.json b/plugins/codex/plugins/ppt-master/THIRD_PARTY_SOURCE.json index 476d42f2..23f86e20 100644 --- a/plugins/codex/plugins/ppt-master/THIRD_PARTY_SOURCE.json +++ b/plugins/codex/plugins/ppt-master/THIRD_PARTY_SOURCE.json @@ -2,8 +2,8 @@ "sourceId": "ppt-master", "repo": "https://github.com/hugohe3/ppt-master.git", "ref": "main", - "commit": "f3660bfcd20bc07c29260b7e330261a01a2b5a6d", + "commit": "6fbac39e1d06a66ce3a8724b9366cb878b793f99", "adapter": "claude-skill", "sourcePath": "skills/ppt-master", - "syncedAt": "2026-10-28T01:57:49Z" + "syncedAt": "2026-10-28T15:59:59Z" } diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/animations.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/animations.md index 3fdee500..8e6d0a74 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/animations.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/animations.md @@ -85,6 +85,8 @@ Sparse sidecar (unlisted slides inherit resolved defaults): | `--animation none` | Disables all per-element animation | | `--no-animations` | Also removes transitions | +**Timing contract**: A `with-previous` row measures `delay` from its beat's start, before the leader's own delay. Repeat the leader's delay to start together; use `leader delay + Δ` to trail by Δ. `after-previous` waits for every earlier row in the click group to finish. + The complete field grammar, inheritance, and validation are [`pptx-animations.md`](../scripts/docs/pptx-animations.md) §8; `pptx_animations.py --describe ` prints each effect's exact options. ### 2.1 Deterministic Morph Object Pairing diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/pptx-animations.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/pptx-animations.md index 3160d710..409bbea9 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/pptx-animations.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/pptx-animations.md @@ -290,12 +290,13 @@ animation-to-video contract. | `morph.pairs..from` / `.to` | Unique source/destination direct-root group ids receiving the shared PowerPoint name `!!`; Morph by object only | | `defaults.animation` / `slides..animation` | Deck-wide or slide-specific default object-animation behavior | | `animation.effect` | Default object effect: one canonical key, `auto`, `mixed`, `random`, or `none` | -| `animation.duration` / `animation.stagger` / `animation.trigger` | Default schedule duration, delay between rows, and Start mode (`after-previous`, `with-previous`, `on-click`) | +| `animation.duration` / `animation.trigger` | Default schedule duration and Start mode (`after-previous`, `with-previous`, `on-click`) | +| `animation.stagger` | Default `delay` for an `after-previous` row with an earlier main-sequence row; finite non-negative seconds, default `0.5`. An explicit row `delay`, including `0`, replaces it. It does not affect the first main-sequence row, `with-previous`, `on-click`, or shape-triggered rows | | `groups..effects[]` | Non-empty ordered array for a multi-duty lifecycle; every row names `effect`; cannot coexist with legacy single-effect fields in the same group | | `groups..effect` | Backward-compatible single-row form; old short names are read-only compatibility inputs | | `effects[].trigger` / legacy `trigger` | Row-specific Start mode; omitted values inherit `animation.trigger` | | `order` | Page-wide order for ordinary rows; ties keep SVG group order, then `effects[]` index; an unlisted animated group inherits the order of the nearest listed group above it in SVG order (0 before the first); `trigger_shape` rows keep relative order in separate interactive sequences; SVG layer order never changes | -| `delay` | Row-specific seconds added to the resolved Start or shape trigger | +| `delay` | Row-specific finite non-negative seconds after the beat start or shape trigger. Omission means `0` except where `animation.stagger` applies; omitted `with-previous` and explicit `0` are identical. Main-sequence beat and click-group timing are defined below | | `duration` | Per-row schedule duration; scalable native trees keep internal ratios, while `entrance_appear` and instantaneous presets keep their authored duration and use the value for `after-previous` spacing | | `effect_options` | Effect-specific parameters (`direction`, `amount`, `color`, `font_name`, `relative`, `size`) limited to what the selected effect supports (`pptx_animations.py --describe `; `direction` is the way the motion travels — PowerPoint names the origin edge, so `right` is its "From Left"); requires an explicit canonical `effect` in the same block or row; `font_name` is one target-installed face | | `trigger_shape` | Different top-level group id for native **On Click of**; row-only, not inherited; implies `on-click` and accepts an explicit row `trigger` only when it is also `on-click` | @@ -306,4 +307,14 @@ animation-to-video contract. | `after_effect` | `none`, `dim` with `color`, `hide`, or `hide-on-next-click` | | `sound` | Object-animation cue: project-relative or absolute `.m4a` / `.mp3` / `.wav` on low-level inputs; bundled selections use the synced project-relative `.wav` path | +Main-sequence timing starts at slide entry until an `on-click` row opens a new +click group with its own time origin. Each `after-previous` row opens a new +beat when every earlier row in that click group has finished, using their full +playback spans, including repeats and auto-reverse. Consecutive `with-previous` +rows share the current beat's start before its leader's delay; each row adds +only its own `delay`. Thus equal delays start together, and a later row with a +smaller delay can start earlier. A leading `with-previous` row uses slide entry +as its beat start. Shape-triggered rows use their own shape click and do not +participate in main-sequence timing. + An unlisted SVG inherits the resolved deck-wide settings; a listed slide may contain only the `transition`, `animation`, `groups`, or `morph` fields it overrides. The chrome-name heuristic matches an id equal to, or holding a `-`/`_`-separated token from, `bg`, `background`, `header`, `footer`, `decor`, `decoration`, `decorations`, `chrome`, `nav`, `watermark`, `logo`, `pagenumber`, `pagenum`, `slidenumber`, `slidenum`, `rule`; `list-groups` names the excluded ids at the end of each slide line. A marker-free `*-header` / `*-footer` group whose text reaches the page's median font size is read as the title block and stays an ordinary target. For chrome defaults, explicit sidecar overrides, and structural exclusions, see [`animations.md`](../../references/animations.md) §5. diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/narration_sync.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/narration_sync.py index 7b38fcf8..43976750 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/narration_sync.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/narration_sync.py @@ -1020,11 +1020,10 @@ def _resolve_animation_groups( timing_options, label=f'canonical animation for "{effect_path}"', ) - default_delay = ( - settings.stagger_ms / 1000 - if effect_trigger == "after-previous" and sequence_index > 0 - else 0 - ) + if effect_trigger == "after-previous" and sequence_index > 0: + default_delay = settings.stagger_ms / 1000 + else: + default_delay = 0 original_delay_ms = animation_seconds_to_milliseconds( effect_cfg.get("delay", default_delay), f'canonical animation delay for "{effect_path}"', @@ -1245,11 +1244,9 @@ def rebuild_animations( f'Derived animation slide "{slide_name}" groups must be an object' ) - # Start modes are row-relative; slide completion spans overlapping rows. - previous_start_ms = 0 - previous_row_end_ms = 0 + # With-previous delays share a beat; after-previous waits for all rows. + beat_start_ms = 0 timeline_end_ms = 0 - has_previous_row = False referenced_cues: set[int] = set() seen_groups: set[str] = set() for state in states: @@ -1260,12 +1257,9 @@ def rebuild_animations( if first_group_effect else None ) - if not has_previous_row: - sequence_base_ms = 0 - elif state.trigger == "with-previous": - sequence_base_ms = previous_start_ms - else: - sequence_base_ms = previous_row_end_ms + if state.trigger == "after-previous": + beat_start_ms = timeline_end_ms + sequence_base_ms = beat_start_ms if cue_number is None: if first_group_effect: fallback_count += 1 @@ -1310,10 +1304,7 @@ def rebuild_animations( effect_value["order"] = state.order effect_value["delay"] = _seconds_from_ms(delay_ms) effect_value["trigger"] = state.trigger - previous_start_ms = actual_start_ms - previous_row_end_ms = actual_start_ms + state.duration_ms - timeline_end_ms = max(timeline_end_ms, previous_row_end_ms) - has_previous_row = True + timeline_end_ms = max(timeline_end_ms, actual_start_ms + state.duration_ms) ignored_cue_count += len(cues) - len(referenced_cues) diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_animations.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_animations.py index 31c88e0f..a2648537 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_animations.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_animations.py @@ -1761,31 +1761,23 @@ def _build_animation_row_xml( def _main_target_offsets(targets: Sequence[AnimationTarget]) -> list[int]: """Return each regular row's start offset within its click group.""" offsets: list[int] = [] - previous_start_ms = 0 - previous_duration_ms = 0 - has_previous = False + beat_start_ms = 0 + timeline_end_ms = 0 for target in targets: if target.trigger == 'on-click': - start_ms = target.delay_ms - elif target.trigger == 'with-previous': - start_ms = ( - previous_start_ms if has_previous else 0 - ) + target.delay_ms - else: - start_ms = ( - previous_start_ms + previous_duration_ms - if has_previous - else 0 - ) + target.delay_ms + beat_start_ms = 0 + timeline_end_ms = 0 + elif target.trigger == 'after-previous': + beat_start_ms = timeline_end_ms + # With-previous rows share the beat's trigger, before any row delay. + start_ms = beat_start_ms + target.delay_ms if start_ms > MAX_OOXML_MILLISECONDS: raise ValueError( 'animation sequence offset exceeds the OOXML millisecond ' f'limit at target {len(offsets) + 1}: {start_ms}' ) offsets.append(start_ms) - previous_start_ms = start_ms - previous_duration_ms = target.playback_duration_ms - has_previous = True + timeline_end_ms = max(timeline_end_ms, start_ms + target.playback_duration_ms) return offsets @@ -1793,7 +1785,7 @@ def _build_mixed_main_steps( targets: Sequence[AnimationTarget], next_id: int, ) -> tuple[str, int]: - """Build one mainSeq containing mixed per-row PowerPoint Start modes.""" + """Build click groups with one shared wrapper per semantic beat.""" offsets = _main_target_offsets(targets) groups: list[list[tuple[AnimationTarget, int]]] = [] for target, offset_ms in zip(targets, offsets): @@ -1813,29 +1805,42 @@ def _build_mixed_main_steps( '' '' ) - rendered_rows: list[str] = [] + beats: list[list[tuple[AnimationTarget, int]]] = [] for target, offset_ms in group: + if not beats or target.trigger != 'with-previous': + beats.append([]) + beats[-1].append((target, offset_ms)) + + rendered_beats: list[str] = [] + for beat in beats: wrapper_id = next_id - row_id = next_id + 1 - row_xml, next_id = _build_animation_row_xml( - target, - target.trigger, - row_id, - next_id + 2, - ) - wrapper_offset_ms = offset_ms - target.delay_ms - rendered_rows.append(f''' + next_id += 1 + leader, offset_ms = beat[0] + wrapper_offset_ms = offset_ms - leader.delay_ms + rendered_rows: list[str] = [] + for target, _ in beat: + row_xml, next_id = _build_animation_row_xml( + target, + target.trigger, + next_id, + next_id + 1, + ) + rendered_rows.append(f'{row_xml}') + rows_xml = '\n '.join(rendered_rows) + rendered_beats.append(f''' - {row_xml} + + {rows_xml} + ''') - rows_xml = '\n '.join(rendered_rows) + beats_xml = '\n '.join(rendered_beats) rendered_groups.append(f''' {group_conditions} - {rows_xml} + {beats_xml} ''') @@ -1852,8 +1857,8 @@ def create_sequence_timing_xml( Args: targets: list of (shape_id, delay_ms, animation_name) or (shape_id, delay_ms, animation_name, duration_seconds) tuples, in - the order they should play. ``delay_ms`` is the gap before - this element starts relative to its Start mode. Mapping targets + animation-pane order. ``delay_ms`` is measured from the beat + start, before its leader's own delay. Mapping targets may set an independent ``trigger``; otherwise they inherit the function-level ``trigger``. A target with ``trigger_shape_id`` must use ``on-click`` and runs in an interactive sequence. @@ -1861,11 +1866,12 @@ def create_sequence_timing_xml( native presets retain their PowerPoint-authored duration. trigger: PowerPoint-standard Start mode for each element. ``'after-previous'`` — first element fires on slide entry, - rest chain after the previous one with ``delay_ms`` spacing + rest wait for all earlier rows in the click group to finish, + then apply ``delay_ms`` spacing (default). ``'on-click'`` — one presenter click per element. - ``'with-previous'`` — all elements start together on slide - entry. + ``'with-previous'`` — all elements share the slide-entry beat, + each with its own ``delay_ms``. Returns: A ```` element string. Returns an empty string when @@ -1897,139 +1903,7 @@ def create_sequence_timing_xml( if target.trigger_shape_id is not None ] - main_triggers = {target.trigger for target in main_targets} - main_trigger = ( - next(iter(main_triggers)) - if len(main_triggers) == 1 - else None - ) - needs_per_target_layout = ( - main_trigger is None - or ( - main_trigger == 'with-previous' - and any(target.delay_ms for target in main_targets) - ) - ) - - if needs_per_target_layout and main_targets: - all_steps, next_id = _build_mixed_main_steps(main_targets, next_id) - elif main_trigger == 'on-click': - # Each element is an independent click-driven par directly under - # mainSeq. Three-level nesting per element: outer cTn holds for - # the click via delay="indefinite", innermost cTn owns the - # clickEffect + animation children. Each click advances the seq. - steps = [] - for target in main_targets: - wrapper_id = next_id - inner_id = next_id + 1 - leaf_id = next_id + 2 - row_xml, next_id = _build_animation_row_xml( - target, - main_trigger, - leaf_id, - next_id + 3, - ) - steps.append(f''' - - - - - - - - - {row_xml} - - - - - - -''') - all_steps = '\n '.join(steps) - else: - # with-previous / after-previous: wrap the entire cascade in ONE - # par so the sequence has a real trigger anchor under mainSeq. - # - # Native PowerPoint after-previous export uses two timing layers: - # each row owns its TriggerDelayTime, while its wrapper owns the - # previous row's absolute end. Their sum is the absolute start offset. - outer_id = next_id - next_id += 1 - inner_steps = [] - with_wrapper_id = None - if main_trigger == 'with-previous': - with_wrapper_id = next_id - next_id += 1 - elapsed_ms = 0 - for target_index, target in enumerate(main_targets, 1): - if main_trigger == 'with-previous': - leaf_id = next_id - row_xml, next_id = _build_animation_row_xml( - target, - main_trigger, - leaf_id, - next_id + 1, - ) - inner_steps.append(f''' - {row_xml} - ''') - else: - if elapsed_ms > MAX_OOXML_MILLISECONDS: - raise ValueError( - 'animation sequence offset exceeds the OOXML ' - f'millisecond limit at target {target_index}: {elapsed_ms}' - ) - wrapper_id = next_id - leaf_id = next_id + 1 - row_xml, next_id = _build_animation_row_xml( - target, - main_trigger, - leaf_id, - next_id + 2, - ) - inner_steps.append(f''' - - - - - {row_xml} - - - - ''') - elapsed_ms += target.delay_ms + target.playback_duration_ms - - inner_xml = '\n '.join(inner_steps) - if main_trigger == 'with-previous': - # Match PowerPoint's native "Start: With Previous" export: - # one delay=0 wrapper begins on slide entry, and all withEffect - # rows live under that wrapper so they truly start in parallel. - inner_xml = f''' - - - - {inner_xml} - - - ''' - if main_trigger in ('with-previous', 'after-previous'): - # Match PowerPoint's native slide-entry export: the wrapper waits - # for mainSeq to begin, then child nodes resolve their Start modes. - outer_start_conditions = ( - '' - '' - ) - else: - outer_start_conditions = '' - all_steps = f''' - - {outer_start_conditions} - - {inner_xml} - - - ''' + all_steps, next_id = _build_mixed_main_steps(main_targets, next_id) if main_targets: main_sequence_xml = f''' diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/animation_import.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/animation_import.py index 31bc43de..bc17eafd 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/animation_import.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/animation_import.py @@ -240,28 +240,26 @@ def _resolve_group_id( def _relative_delays(rows: tuple[AnimationRowSummary, ...]) -> tuple[int, ...]: """Invert the native writer's absolute main-sequence offsets.""" delays: list[int] = [] - previous_start_ms = 0 - previous_duration_ms = 0 - has_previous = False + beat_start_ms = 0 + timeline_end_ms = 0 for index, row in enumerate(rows, 1): if row.playback_duration_ms is None: raise AnimationImportError( f"animation row {index} has no exact playback duration" ) - if row.trigger_shape_id is not None or row.trigger == "on-click": + if row.trigger_shape_id is not None: base_ms = 0 - elif row.trigger == "with-previous": - base_ms = previous_start_ms if has_previous else 0 - elif row.trigger == "after-previous": - base_ms = ( - previous_start_ms + previous_duration_ms - if has_previous - else 0 - ) else: - raise AnimationImportError( - f"animation row {index} has unsupported trigger {row.trigger!r}" - ) + if row.trigger == "on-click": + beat_start_ms = 0 + timeline_end_ms = 0 + elif row.trigger == "after-previous": + beat_start_ms = timeline_end_ms + elif row.trigger != "with-previous": + raise AnimationImportError( + f"animation row {index} has unsupported trigger {row.trigger!r}" + ) + base_ms = beat_start_ms delay_ms = row.offset_ms - base_ms if delay_ms < 0: raise AnimationImportError( @@ -269,9 +267,7 @@ def _relative_delays(rows: tuple[AnimationRowSummary, ...]) -> tuple[int, ...]: ) delays.append(delay_ms) if row.trigger_shape_id is None: - previous_start_ms = row.offset_ms - previous_duration_ms = row.playback_duration_ms - has_previous = True + timeline_end_ms = max(timeline_end_ms, row.offset_ms + row.playback_duration_ms) return tuple(delays) diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/prompt_audit_manifest.json b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/prompt_audit_manifest.json index 260503ec..e8922a0c 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/prompt_audit_manifest.json +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/prompt_audit_manifest.json @@ -1030,12 +1030,12 @@ "max_tokens": 3250 }, "stage.shared.conversion-reference": { - "description": "Conditional source-conversion and import compatibility reference. Ceiling raised after BUDGET_LOAD_SET reported 7935 tokens.", + "description": "Conditional source-conversion and import compatibility reference. Ceiling raised after BUDGET_LOAD_SET reported 9144 tokens.", "scope": "incremental", "files": [ "skills/ppt-master/scripts/docs/conversion.md" ], - "max_tokens": 8750 + "max_tokens": 11000 }, "stage.shared.troubleshooting": { "description": "Conditional troubleshooting reference for generation failures.", diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/pptx_package/builder.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/pptx_package/builder.py index 81b8bd85..8bfa9299 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/pptx_package/builder.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/pptx_package/builder.py @@ -5997,15 +5997,12 @@ def _build_sequence_targets( 'trigger "on-click" when trigger is explicit' ) resolved_trigger = 'on-click' - default_delay = ( - stagger - if ( - trigger_shape is None - and resolved_trigger == 'after-previous' - and main_sequence_count > 0 - ) - else 0 - ) + if trigger_shape is not None: + default_delay = 0 + elif resolved_trigger == 'after-previous' and main_sequence_count > 0: + default_delay = stagger + else: + default_delay = 0 delay_seconds = validate_seconds( group_cfg.get('delay', default_delay), f'animations.json {effect_path}.delay', diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/tests/test_animation_beat_grouping.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/tests/test_animation_beat_grouping.py new file mode 100644 index 00000000..19772aa2 --- /dev/null +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/tests/test_animation_beat_grouping.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +"""Regress shared native beat wrappers and legacy timing-tree imports. + +Usage: + python3 -m unittest tests.test_animation_beat_grouping + +Dependencies: + The animation scripts' normal dependencies. +""" + +from __future__ import annotations + +import sys +import unittest +from pathlib import Path +from xml.etree import ElementTree as ET + +SCRIPTS_DIR = Path(__file__).resolve().parents[1] +if str(SCRIPTS_DIR) not in sys.path: + sys.path.insert(0, str(SCRIPTS_DIR)) + +from pptx_animations import ( # noqa: E402 + create_sequence_timing_xml, + read_slide_animation_sequence, + validate_generated_animation_xml, +) +from pptx_to_svg.animation_import import read_animation_config # noqa: E402 + +PML_NS = "http://schemas.openxmlformats.org/presentationml/2006/main" +NS = {"p": PML_NS} +SVG = ('') + + +def _target(trigger: str, delay_ms: int = 0, duration: float = 0.2, **options: object) -> dict: + return {"shape_id": 2, "effect": "entrance_fade", "duration": duration, + "trigger": trigger, "delay_ms": delay_ms, **options} + + +def _slide(timing: str) -> str: + return (f'' + '' + '' + f'{timing}') + + +def _legacy_slide(rows: list[tuple[str, int, int]]) -> str: + """Build the former per-row-wrapper fixture without invoking the writer. + + Each tuple supplies the native node type, wrapper offset, and leaf delay. + The fade behavior is frozen from the pre-grouping output. + """ + wrappers = [] + for index, (node_type, offset_ms, delay_ms) in enumerate(rows): + wrapper_id = 4 + index * 4 + wrappers.append(f''' + + + + + + + style.visibility + + + + + + + + + ''') + return _slide(f''' + + + + + + {''.join(wrappers)} + + + + + + ''') + + +class AnimationBeatGroupingTests(unittest.TestCase): + def _assert_layout(self, targets: list[dict], offsets: list[int], + layout: list[list[list[int]]]) -> str: + slide = _slide(create_sequence_timing_xml(targets)) + summary = read_slide_animation_sequence(slide, require_supported_effects=True) + self.assertEqual([row.offset_ms for row in summary.rows], offsets) + validate_generated_animation_xml(slide, targets) + root = ET.fromstring(slide) + main = root.find('.//p:cTn[@nodeType="mainSeq"]', NS) + rows = [row for row in main.iter(f"{{{PML_NS}}}cTn") if row.get("presetClass")] + row_indices = {row: index for index, row in enumerate(rows)} + actual = [] + for group in main.findall("p:childTnLst/p:par/p:cTn", NS): + beats = [] + for beat in group.findall("p:childTnLst/p:par/p:cTn", NS): + beats.append([row_indices[row] for row in beat.findall("p:childTnLst/p:par/p:cTn", NS)]) + actual.append(beats) + self.assertEqual(actual, layout) + return slide + + def test_issue_331_keeps_offsets_and_shares_zoom_beat(self) -> None: + self._assert_layout([ + _target("after-previous", duration=0.75), + _target("after-previous", duration=0.28, effect="entrance_zoom", + effect_options={"direction": "out"}, accelerate=1), + _target("with-previous", duration=0.12), + _target("after-previous", duration=0.09, effect="emphasis_grow_shrink", + effect_options={"size": 110}, auto_reverse=True), + ], [0, 750, 750, 1030], [[[0], [1, 2], [3]]]) + + def test_mixed_click_groups_and_out_of_order_delays(self) -> None: + self._assert_layout([ + _target("after-previous", duration=2), + _target("with-previous", 2000, 0.1), + _target("after-previous", 100), + _target("with-previous", 50, 0.1), + _target("on-click", 400, 0.3), + _target("with-previous", 100), + _target("after-previous", 20), + _target("with-previous", 0, 0.1), + ], [0, 2000, 2200, 2150, 400, 100, 720, 700], + [[[0, 1], [2, 3]], [[4, 5], [6, 7]]]) + + def test_equal_offsets_do_not_merge_separate_click_beats(self) -> None: + self._assert_layout([ + _target("with-previous"), _target("with-previous"), + _target("on-click"), _target("with-previous"), + _target("on-click"), _target("with-previous"), + ], [0] * 6, [[[0, 1]], [[2, 3]], [[4, 5]]]) + + def test_uniform_modes_use_the_same_beat_structure(self) -> None: + cases = [ + ("with-previous", [0, 0, 0], [0, 0, 0], [[[0, 1, 2]]]), + ("with-previous", [450, 450, 450], [450, 450, 450], [[[0, 1, 2]]]), + ("with-previous", [100, 200, 50], [100, 200, 50], [[[0, 1, 2]]]), + ("after-previous", [100, 200, 50], [100, 500, 750], [[[0], [1], [2]]]), + ("on-click", [100, 200, 50], [100, 200, 50], [[[0]], [[1]], [[2]]]), + ] + for trigger, delays, offsets, layout in cases: + with self.subTest(trigger=trigger, delays=delays): + self._assert_layout([_target(trigger, delay) for delay in delays], offsets, layout) + + def test_interactive_rows_stay_outside_shared_main_beats(self) -> None: + slide = self._assert_layout([ + _target("on-click", 400, 5, trigger_shape_id=3), + _target("after-previous", 100), _target("with-previous", 50), + _target("after-previous", 20), + ], [100, 50, 320, 400], [[[0, 1], [2]]]) + root = ET.fromstring(slide) + interactive = root.findall('.//p:cTn[@nodeType="interactiveSeq"]', NS) + self.assertEqual(len(interactive), 1) + self.assertEqual(sum(row.get("presetClass") is not None + for row in interactive[0].iter(f"{{{PML_NS}}}cTn")), 1) + + def test_legacy_per_row_wrappers_still_read_import_and_reexport(self) -> None: + cases = [ + ([('afterEffect', 0, 0), ('afterEffect', 500, 500), + ('withEffect', 500, 200), ('withEffect', 500, 0), ('afterEffect', 1500, 500)], + [0, 1000, 700, 500, 2000], [[[0], [1, 2, 3], [4]]]), + ([('withEffect', 0, 450), ('withEffect', 0, 450), ('withEffect', 0, 100)], + [450, 450, 100], [[[0, 1, 2]]]), + ] + for rows, offsets, layout in cases: + with self.subTest(rows=rows): + legacy = _legacy_slide(rows) + summary = read_slide_animation_sequence(legacy, require_supported_effects=True) + self.assertEqual([row.offset_ms for row in summary.rows], offsets) + effects = read_animation_config(legacy, SVG).groups["content"]["effects"] + self.assertEqual([round(row["delay"] * 1000) for row in effects], + [delay for _, _, delay in rows]) + targets = [ + {key: value for key, value in row.items() if key not in {"delay", "order"}} + | {"shape_id": 2, "delay_ms": round(row["delay"] * 1000)} + for row in effects + ] + validate_generated_animation_xml(legacy, targets) + rebuilt = self._assert_layout(targets, offsets, layout) + self.assertEqual(read_slide_animation_sequence(rebuilt), summary) + self.assertEqual(read_animation_config(rebuilt, SVG).groups, + read_animation_config(legacy, SVG).groups) diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/tests/test_animation_beat_timing.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/tests/test_animation_beat_timing.py new file mode 100644 index 00000000..359fe6ac --- /dev/null +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/tests/test_animation_beat_timing.py @@ -0,0 +1,272 @@ +#!/usr/bin/env python3 +"""Regress mixed PowerPoint beats through timing, import, and narration. + +Usage: + python3 -m unittest tests.test_animation_beat_timing + +Dependencies: + The animation and narration scripts' normal dependencies. +""" + +from __future__ import annotations + +import io +import json +import random +import sys +import tempfile +import unittest +from contextlib import redirect_stderr +from pathlib import Path +from unittest.mock import patch +from xml.etree import ElementTree as ET + +SCRIPTS_DIR = Path(__file__).resolve().parents[1] +if str(SCRIPTS_DIR) not in sys.path: + sys.path.insert(0, str(SCRIPTS_DIR)) + +from narration_sync import rebuild_animations # noqa: E402 +from pptx_animations import ( # noqa: E402 + create_sequence_timing_xml, + read_slide_animation_sequence, + validate_generated_animation_xml, +) +from pptx_to_svg.animation_import import read_animation_config # noqa: E402 +from svg_to_pptx.pptx_package.builder import _build_sequence_targets # noqa: E402 + +PML_NS = "http://schemas.openxmlformats.org/presentationml/2006/main" +NS = {"p": PML_NS} + + +def _target(duration: float, trigger: str = "after-previous", delay_ms: int = 0, + **options: object) -> dict: + return {"shape_id": 2, "effect": "entrance_fade", "duration": duration, + "trigger": trigger, "delay_ms": delay_ms, **options} + + +def _issue_targets() -> list[dict]: + return [ + _target(0.75), + _target(0.28, effect="entrance_zoom", effect_options={"direction": "out"}, accelerate=1), + _target(0.12, "with-previous"), + _target(0.09, effect="emphasis_grow_shrink", effect_options={"size": 110}, auto_reverse=True), + ] + + +def _slide(targets: list[dict]) -> str: + timing = create_sequence_timing_xml(targets) + shape_ids = {target["shape_id"] for target in targets} + shape_ids.update(target["trigger_shape_id"] for target in targets if "trigger_shape_id" in target) + shapes = "".join(f'' + '' for shape_id in sorted(shape_ids)) + return (f'{shapes}' + f'{timing}') + + +class AnimationBeatTimingTests(unittest.TestCase): + def _assert_offsets(self, targets: list[dict], expected: list[int]) -> str: + slide = _slide(targets) + summary = read_slide_animation_sequence(slide, require_supported_effects=True) + self.assertEqual([row.offset_ms for row in summary.rows], expected) + validate_generated_animation_xml(slide, targets) + return slide + + def test_after_previous_waits_for_issue_331_zoom(self) -> None: + slide = self._assert_offsets(_issue_targets(), [0, 750, 750, 1030]) + rows = read_slide_animation_sequence(slide).rows + self.assertEqual(max(row.offset_ms + row.playback_duration_ms for row in rows), 1210) + + def test_each_with_previous_delay_uses_beat_start(self) -> None: + self._assert_offsets([ + _target(0.2, delay_ms=100), + _target(0.1, "with-previous", 200), + _target(0.1, "with-previous", 50), + _target(0.1, delay_ms=20), + ], [100, 200, 50, 320]) + + def test_long_parallel_playback_includes_repeat_and_reverse(self) -> None: + for modifiers, end_ms in (({"repeat_count": 3, "auto_reverse": True}, 1200), + ({"repeat_duration": 1.4, "auto_reverse": True}, 1400)): + with self.subTest(modifiers=modifiers): + self._assert_offsets([ + _target(0.1), _target(0.2, "with-previous", **modifiers), + _target(0.05, "with-previous"), _target(0.1, delay_ms=30), + ], [0, 0, 0, end_ms + 30]) + + def test_click_resets_beat_and_interactive_rows_do_not_extend_it(self) -> None: + self._assert_offsets([ + _target(2), _target(0.2, "on-click", 100), + _target(0.1, "with-previous", 50), _target(0.1), + _target(5, "on-click", 70, trigger_shape_id=3), + ], [0, 100, 50, 300, 70]) + + def test_uniform_start_modes_remain_consistent(self) -> None: + for trigger, expected in (("after-previous", [100, 500, 750]), + ("with-previous", [100, 200, 50])): + with self.subTest(trigger=trigger): + self._assert_offsets([_target(0.2, trigger, delay) for delay in (100, 200, 50)], expected) + + def test_verifier_rejects_old_early_after_previous_offset(self) -> None: + targets = _issue_targets() + root = ET.fromstring(_slide(targets)) + row = root.find('.//p:cTn[@presetClass="emph"]', NS) + parent = {child: node for node in root.iter() for child in node} + wrapper = parent[parent[parent[row]]] + wrapper.find("p:stCondLst/p:cond", NS).set("delay", "870") + with self.assertRaisesRegex(ValueError, "offset"): + validate_generated_animation_xml(ET.tostring(root), targets) + + def test_import_preserves_parallel_delays_and_reexports_offsets(self) -> None: + targets = [ + _target(0.75), _target(0.28, delay_ms=100), + _target(0.12, "with-previous", 200), + _target(0.05, "with-previous", 50), _target(0.09), + ] + svg = ('') + imported = read_animation_config(_slide(targets), svg) + effects = imported.groups["content"]["effects"] + self.assertEqual([row["delay"] for row in effects], [0, 0.1, 0.2, 0.05, 0]) + rebuilt = [ + {key: value for key, value in row.items() if key not in {"delay", "order"}} + | {"shape_id": 2, "delay_ms": round(row["delay"] * 1000)} for row in effects + ] + self._assert_offsets(rebuilt, [0, 850, 950, 800, 1130]) + + def _narration(self, targets: list[dict], audio_duration: float, + *, following_cue: bool = False, cue_start: str = "00:00:00,000") -> dict: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + effects = [{key: value for key, value in target.items() if key not in {"shape_id", "delay_ms"}} + | {"delay": target["delay_ms"] / 1000, "order": 1} for target in targets] + groups = {"content": {"effects": effects}} + if following_cue: + groups["next"] = {"effect": "entrance_fade", "duration": 0.1, "order": 2} + canonical = {"defaults": {"animation": {"effect": "none", "stagger": 0}, + "transition": {"effect": "none"}}, + "slides": {"slide": {"groups": groups}}} + source = root / "animations.json" + source.write_text(json.dumps(canonical), encoding="utf-8") + subtitles = f"1\n{cue_start} --> 00:00:00,200\nFirst\n\n" + if following_cue: + subtitles += "2\n00:00:01,500 --> 00:00:01,700\nNext\n" + (root / "slide.srt").write_text(subtitles, encoding="utf-8") + (root / "slide.wav").write_bytes(b"") + output = root / "derived.json" + with patch("narration_sync.probe_audio_duration", return_value=audio_duration), \ + redirect_stderr(io.StringIO()): + rebuild_animations(root, canonical_path=source, plan_path=root / "missing-plan.json", + subtitle_dir=root, audio_dir=root, output_path=output, + narration_padding=0, narration_start_floor=0, force=False) + self.assertEqual(json.loads(source.read_text(encoding="utf-8")), canonical) + return json.loads(output.read_text(encoding="utf-8"))["slides"]["slide"]["groups"] + + def test_narration_cue_delay_uses_complete_preceding_beat(self) -> None: + groups = self._narration(_issue_targets(), 2, following_cue=True) + self.assertEqual(groups["next"]["delay"], 0.29) + + def test_narration_rejects_advance_before_complete_beat(self) -> None: + with self.assertRaisesRegex(ValueError, "end at 1.210s.*advance at 1.100s"): + self._narration(_issue_targets(), 1.1) + + def test_narration_nonzero_with_delays_do_not_accumulate(self) -> None: + groups = self._narration([ + _target(0.3), _target(0.1, "with-previous", 200), + _target(0.1, "with-previous", 50), _target(0.1), + ], 0.52, cue_start="00:00:00,100") + self.assertEqual([row["delay"] for row in groups["content"]["effects"]], [0.1, 0.2, 0.05, 0]) + + def _sidecar_targets(self, groups: dict) -> list[dict]: + anim_targets = [(index + 2, group_id) for index, group_id in enumerate(groups)] + targets, _ = _build_sequence_targets( + anim_targets, "slide", {"groups": groups}, None, + {"trigger": "after-previous"}, 0.5, 0.5, 0, random.Random(0), + ) + return targets + + def _staggered_groups(self) -> dict: + return { + "images": {"effect": "entrance_fade", "order": 1}, + "bracket": {"effect": "entrance_wipe", "order": 2}, + "legend": {"effect": "entrance_fade", "order": 2, "trigger": "with-previous"}, + "note": {"effect": "entrance_fade", "order": 3, "trigger": "with-previous", "delay": 0.2}, + } + + def test_builder_omitted_with_previous_delay_equals_explicit_zero(self) -> None: + groups = self._staggered_groups() + omitted = self._sidecar_targets(groups) + groups["legend"]["delay"] = 0 + explicit_zero = self._sidecar_targets(groups) + self.assertEqual(omitted, explicit_zero) + self.assertEqual([target["delay_ms"] for target in omitted], [0, 500, 0, 200]) + self._assert_offsets(omitted, [0, 1000, 500, 700]) + + def test_companion_repeating_leader_delay_starts_together(self) -> None: + groups = self._staggered_groups() + groups["legend"]["delay"] = 0.5 + targets = self._sidecar_targets(groups) + self._assert_offsets(targets, [0, 1000, 1000, 700]) + + def test_later_row_with_smaller_delay_starts_before_previous_row(self) -> None: + groups = self._staggered_groups() + groups["legend"]["delay"] = 0.2 + groups["note"]["delay"] = 0.05 + slide = self._assert_offsets(self._sidecar_targets(groups), [0, 1000, 700, 550]) + rows = read_slide_animation_sequence(slide).rows + self.assertEqual([row.shape_id for row in rows], [2, 3, 4, 5]) + self.assertLess(rows[-1].offset_ms, rows[-2].offset_ms) + + def test_builder_delays_roundtrip_through_animation_import(self) -> None: + for companion_delay in (None, 0, 0.5, 0.7): + with self.subTest(companion_delay=companion_delay): + groups = self._staggered_groups() + if companion_delay is not None: + groups["legend"]["delay"] = companion_delay + targets = self._sidecar_targets(groups) + expected = [0, 1000, 500 + round((companion_delay or 0) * 1000), 700] + slide = self._assert_offsets(targets, expected) + svg_groups = "".join( + f'' + for index, group_id in enumerate(groups) + ) + imported = read_animation_config( + slide, f'{svg_groups}', + ) + self.assertEqual(imported.groups["legend"]["delay"], companion_delay or 0) + self._assert_offsets(self._sidecar_targets(imported.groups), expected) + + def test_narration_beat_delays_agree_with_exporter(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + canonical = {"defaults": {"animation": {"effect": "none", "stagger": 0.5}, + "transition": {"effect": "none"}}, + "slides": {"slide": {"groups": {"content": {"effects": [ + {"effect": "entrance_fade", "duration": 0.5}, + {"effect": "entrance_wipe", "duration": 0.5}, + {"effect": "entrance_fade", "duration": 0.5, "trigger": "with-previous"}, + {"effect": "entrance_fade", "duration": 0.5, + "trigger": "with-previous", "delay": 0}, + {"effect": "entrance_fade", "duration": 0.5, + "trigger": "with-previous", "delay": 0.5}, + {"effect": "entrance_fade", "duration": 0.5, + "trigger": "with-previous", "delay": 0.2}, + ]}}}}} + source = root / "animations.json" + source.write_text(json.dumps(canonical), encoding="utf-8") + (root / "slide.srt").write_text("1\n00:00:00,000 --> 00:00:00,200\nFirst\n", + encoding="utf-8") + (root / "slide.wav").write_bytes(b"") + output = root / "derived.json" + with patch("narration_sync.probe_audio_duration", return_value=3), \ + redirect_stderr(io.StringIO()): + rebuild_animations(root, canonical_path=source, plan_path=root / "missing-plan.json", + subtitle_dir=root, audio_dir=root, output_path=output, + narration_padding=0, narration_start_floor=0, force=False) + derived_groups = json.loads(output.read_text(encoding="utf-8"))["slides"]["slide"]["groups"] + self.assertEqual(json.loads(source.read_text(encoding="utf-8")), canonical) + effects = derived_groups["content"]["effects"] + self.assertEqual([row["delay"] for row in effects], [0, 0.5, 0, 0, 0.5, 0.2]) + expected = [0, 1000, 500, 500, 1000, 700] + self._assert_offsets(self._sidecar_targets(canonical["slides"]["slide"]["groups"]), expected) + self._assert_offsets(self._sidecar_targets(derived_groups), expected)