Sync third-party and MCP marketplace plugins
Constraint: Public skills are published only by explicit administrator action unless they are tracked third-party market sources. Confidence: high Scope-risk: narrow Directive: Keep private/internal skills out of the public marketplace and preserve normal incremental market Git history. Tested: Marketplace validation passed.
This commit is contained in:
1 parent
79c20679c5
commit
ed33e12b7d
40 files changed
+946
-277
No files matched your search
@@ -42,8 +42,8 @@
|
|||||||
"repo": "https://github.com/vercel-labs/skills.git",
|
"repo": "https://github.com/vercel-labs/skills.git",
|
||||||
"ref": "main",
|
"ref": "main",
|
||||||
"adapter": "claude-skill",
|
"adapter": "claude-skill",
|
||||||
"commit": "d8eca76d5dbe47df2d146d036e32534b74ece57a",
|
"commit": "699d35b21bd3c97dbc0edeae062b2698898bb2e5",
|
||||||
"syncedAt": "2026-10-28T01:57:49Z"
|
"syncedAt": "2026-10-28T15:59:59Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "generate-agents-md",
|
"id": "generate-agents-md",
|
||||||
@@ -94,10 +94,10 @@
|
|||||||
"id": "dingtalk-workspace",
|
"id": "dingtalk-workspace",
|
||||||
"pluginName": "dingtalk-workspace",
|
"pluginName": "dingtalk-workspace",
|
||||||
"repo": "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli.git",
|
"repo": "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli.git",
|
||||||
"ref": "v1.0.65",
|
"ref": "v1.0.66",
|
||||||
"adapter": "skill-collection",
|
"adapter": "skill-collection",
|
||||||
"commit": "92c4f8c355c749ec470c68f998d862a00d7d1709",
|
"commit": "f215043e676d6889e1ddce3464828d8b1ec40c31",
|
||||||
"syncedAt": "2026-10-21T06:48:34Z"
|
"syncedAt": "2026-10-28T15:59:59Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ui-ux-pro-max",
|
"id": "ui-ux-pro-max",
|
||||||
@@ -177,8 +177,8 @@
|
|||||||
"repo": "https://github.com/hugohe3/ppt-master.git",
|
"repo": "https://github.com/hugohe3/ppt-master.git",
|
||||||
"ref": "main",
|
"ref": "main",
|
||||||
"adapter": "claude-skill",
|
"adapter": "claude-skill",
|
||||||
"commit": "f3660bfcd20bc07c29260b7e330261a01a2b5a6d",
|
"commit": "6fbac39e1d06a66ce3a8724b9366cb878b793f99",
|
||||||
"syncedAt": "2026-10-28T01:57:49Z"
|
"syncedAt": "2026-10-28T15:59:59Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "grill-me",
|
"id": "grill-me",
|
||||||
@@ -258,8 +258,8 @@
|
|||||||
"repo": "https://github.com/vercel/next.js.git",
|
"repo": "https://github.com/vercel/next.js.git",
|
||||||
"ref": "canary",
|
"ref": "canary",
|
||||||
"adapter": "skill-collection",
|
"adapter": "skill-collection",
|
||||||
"commit": "7356b0bcb5f22371c15dcb413afe5fb28412f52d",
|
"commit": "f922205f9e860de24673269f81dea51e9e33b0cb",
|
||||||
"syncedAt": "2026-10-28T01:57:49Z"
|
"syncedAt": "2026-10-28T15:59:59Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "shuorenhua",
|
"id": "shuorenhua",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dingtalk-workspace",
|
"name": "dingtalk-workspace",
|
||||||
"version": "1.0.65",
|
"version": "1.0.66",
|
||||||
"description": "通过 dws CLI 操作钉钉消息、文档、表格、日历、待办和知识库的官方 Skill 集合。需要另行安装匹配版本的 dws、登录并取得企业授权;辅助脚本需要 Python 3。",
|
"description": "通过 dws CLI 操作钉钉消息、文档、表格、日历、待办和知识库的官方 Skill 集合。需要另行安装匹配版本的 dws、登录并取得企业授权;辅助脚本需要 Python 3。",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "DingTalk",
|
"name": "DingTalk",
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"sourceId": "dingtalk-workspace",
|
"sourceId": "dingtalk-workspace",
|
||||||
"repo": "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli.git",
|
"repo": "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli.git",
|
||||||
"ref": "v1.0.65",
|
"ref": "v1.0.66",
|
||||||
"commit": "92c4f8c355c749ec470c68f998d862a00d7d1709",
|
"commit": "f215043e676d6889e1ddce3464828d8b1ec40c31",
|
||||||
"adapter": "skill-collection",
|
"adapter": "skill-collection",
|
||||||
"sourcePath": "skills/multi",
|
"sourcePath": "skills/multi",
|
||||||
"syncedAt": "2026-10-21T06:48:34Z"
|
"syncedAt": "2026-10-28T15:59:59Z"
|
||||||
}
|
}
|
||||||
@@ -114,7 +114,7 @@ Help/Schema 是离线契约查询,不调用线上业务,也不读写用户
|
|||||||
<!-- VISIBLE_SHORTCUTS_START -->
|
<!-- VISIBLE_SHORTCUTS_START -->
|
||||||
## Shortcut 发现(按需)
|
## Shortcut 发现(按需)
|
||||||
|
|
||||||
`aitable` 当前有 127 条公开 shortcut,完整清单保留在 Runtime Catalog 与 Schema,不在高频产品根 Skill 中重复展开。已知 leaf 直接执行。参数只查 `dws schema --cli-path "aitable <leaf>" --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 <leaf>" --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。
|
仅当根路由、精确 task reference 和 `references/aitable.md` 的低频原子索引都无法定位能力时,才执行 `dws shortcut list --service aitable --format json` 做最终回退;不要为已知意图加载完整 Shortcut Catalog 或产品级 Schema。
|
||||||
<!-- VISIBLE_SHORTCUTS_END -->
|
<!-- VISIBLE_SHORTCUTS_END -->
|
||||||
@@ -178,6 +178,8 @@ Help/Schema 是离线契约查询,不调用线上业务,也不读写用户
|
|||||||
- 已有 ID 直接使用;URL 只解析一次;“唯一定位并操作”用 `+resolve-base` / `+resolve-table`,“搜索候选/存在性检查”直接用 `+base-search`。人员、部门或群组只有显示名称时用 `entity search` 取得稳定身份;这些路径不要串行探测。filters/sort 缺 fieldId 时才读取字段目录。
|
- 已有 ID 直接使用;URL 只解析一次;“唯一定位并操作”用 `+resolve-base` / `+resolve-table`,“搜索候选/存在性检查”直接用 `+base-search`。人员、部门或群组只有显示名称时用 `entity search` 取得稳定身份;这些路径不要串行探测。filters/sort 缺 fieldId 时才读取字段目录。
|
||||||
- Golden Route 已给出准确命令和参数时直接执行;不预读或默认读取通用 `references/aitable.md`。只有操作参数、JSON 结构或恢复语义确实缺失时,才读取下方一个精确操作 Reference。
|
- Golden Route 已给出准确命令和参数时直接执行;不预读或默认读取通用 `references/aitable.md`。只有操作参数、JSON 结构或恢复语义确实缺失时,才读取下方一个精确操作 Reference。
|
||||||
- Shortcut 已含分片或验证时不重复拆步;已有 Base 新建完整表结构直接用 `+table-bootstrap`。
|
- Shortcut 已含分片或验证时不重复拆步;已有 Base 新建完整表结构直接用 `+table-bootstrap`。
|
||||||
|
- 明确仪表盘设计、大小混排或纵向叠放时保留显式 layout,创建后不默认 arrange。多图集中编排用 `+charts-create`;需要多步依赖和中断接续时读 dashboard-chart 专项,使用 `+build-plan` / `+build-plan-verify`。
|
||||||
|
- AI submitted 只表示有真实 taskId 的受理;新状态入口已部署且有固定目标记录时才使用 `+field-wait-ai`。本轮终态与结果写回可读分别判断,缺少版本证据时保持 pending。
|
||||||
- 单产品线性任务直接执行,不创建 TodoWrite;只有跨产品或多个独立分支的长任务才建计划,并且只在阶段切换时更新,不在每条 CLI 后刷新状态。
|
- 单产品线性任务直接执行,不创建 TodoWrite;只有跨产品或多个独立分支的长任务才建计划,并且只在阶段切换时更新,不在每条 CLI 后刷新状态。
|
||||||
- 用户要求资源名带当前时间戳时只取一次并在 Base、Table、Dashboard 等名称中复用同一值;不要为每个资源分别取时间。
|
- 用户要求资源名带当前时间戳时只取一次并在 Base、Table、Dashboard 等名称中复用同一值;不要为每个资源分别取时间。
|
||||||
- JSON 已返回所需字段时立即复用;不得为寻找同一字段改用 `--verbose`、`raw`、`pretty` 重复请求。
|
- JSON 已返回所需字段时立即复用;不得为寻找同一字段改用 `--verbose`、`raw`、`pretty` 重复请求。
|
||||||
|
|||||||
+10
-6
@@ -153,7 +153,7 @@ Base、Table、应用模式 App/Page/Widget、普通 Field、普通 Record、Vie
|
|||||||
|------|------|
|
|------|------|
|
||||||
| `dashboard get/create/update/delete` | 仪表盘管理 |
|
| `dashboard get/create/update/delete` | 仪表盘管理 |
|
||||||
| `dashboard config-example` | 查看仪表盘配置模板 |
|
| `dashboard config-example` | 查看仪表盘配置模板 |
|
||||||
| `dashboard arrange` | 自动重排仪表盘图表布局(智能填满网格,避免空缺) |
|
| `dashboard arrange` | 明确接受统一按行重排时使用;改变位置、宽高和叠放关系 |
|
||||||
| `chart get/create/update/delete` | 图表管理 |
|
| `chart get/create/update/delete` | 图表管理 |
|
||||||
| `chart widgets-example` | 查看图表 widgets 配置模板 |
|
| `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-update` | 增量更新自定义角色(PATCH) | `--base-id` `--role-id` | 未传字段不变;`--sub-roles` 按 (targetId,targetType) 合并 |
|
||||||
| `advperm role-delete` | 删除自定义角色 | `--base-id` `--role-id` `--yes` | 不可逆;系统角色禁删;**调用者必须是该 AI 表格的管理员/Owner**,非管理员会得到 401 AUTH_ERROR |
|
| `advperm role-delete` | 删除自定义角色 | `--base-id` `--role-id` `--yes` | 不可逆;系统角色禁删;**调用者必须是该 AI 表格的管理员/Owner**,非管理员会得到 401 AUTH_ERROR |
|
||||||
|
|
||||||
> **角色 CRUD 已全支持**:create/get/list/update/delete 都可走 CLI。
|
> **高级权限角色与成员已支持**:角色 create/get/list/update/delete、成员追加/完整替换、角色复制和按用户查角色均可走 CLI。
|
||||||
> 所有写命令(enable/disable/role-create/role-update/role-delete)需要 Base 管理员权限;非管理员只能调 `role-list` / `role-get`(只读)。
|
> 所有写命令(enable/disable/role-create/role-update/role-delete/role-members-add/role-members-update/role-copy)需要 Base 管理员权限;非管理员只读使用 `role-list`、`role-get`、`role-query-user`。
|
||||||
> "角色 ↔ 成员"绑定当前 CLI 不支持,仍需在 AI 表格 Web 端 → Base 设置 → 高级权限面板手动完成。
|
> 成员全量替换前必须回读 `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 (文件夹与节点管理)
|
### section (文件夹与节点管理)
|
||||||
|
|
||||||
@@ -410,7 +411,7 @@ dws aitable export data --base-id <BASE_ID> --task-id <TASK_ID> --timeout-ms 300
|
|||||||
|
|
||||||
用户说"仪表盘/图表/chart" → 读 [aitable-dashboard-chart.md](./aitable/aitable-dashboard-chart.md)
|
用户说"仪表盘/图表/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)
|
用户说"附件/上传文件" → 读 [aitable-attachment.md](./aitable/aitable-attachment.md)
|
||||||
|
|
||||||
@@ -424,7 +425,10 @@ dws aitable export data --base-id <BASE_ID> --task-id <TASK_ID> --timeout-ms 300
|
|||||||
- 建角色(可同时指定子角色权限) → `advperm role-create --name ... --sub-roles '[...]'`
|
- 建角色(可同时指定子角色权限) → `advperm role-create --name ... --sub-roles '[...]'`
|
||||||
- 改角色名 / 改子角色权限(PATCH 语义,未传字段不变) → `advperm role-update --role-id ... [--name ...] [--sub-roles '[...]']`
|
- 改角色名 / 改子角色权限(PATCH 语义,未传字段不变) → `advperm role-update --role-id ... [--name ...] [--sub-roles '[...]']`
|
||||||
- 删角色 → `advperm role-delete --yes`
|
- 删角色 → `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)
|
命令报错/操作失败 → 读 [aitable-error-recovery.md](./aitable/aitable-error-recovery.md)
|
||||||
|
|
||||||
|
|||||||
+70
-3
@@ -14,8 +14,18 @@
|
|||||||
| `advperm role-create` | 创建自定义角色 |
|
| `advperm role-create` | 创建自定义角色 |
|
||||||
| `advperm role-update` | 增量更新自定义角色(PATCH 语义) |
|
| `advperm role-update` | 增量更新自定义角色(PATCH 语义) |
|
||||||
| `advperm role-delete` | 删除自定义角色(不可逆) |
|
| `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)。
|
- `roleType`:`custom`(自定义) / `system_editor` / `system_reader` / `5000`(owner) / `4000`(manager)。
|
||||||
- `system`:boolean,true 表示系统角色(不可删)。
|
- `system`:boolean,true 表示系统角色(不可删)。
|
||||||
- `subRoles[].display.*`:服务端返回的人类可读标签,可直接拼接给用户阅读,无需自行映射枚举。
|
- `subRoles[].display.*`:服务端返回的人类可读标签,可直接拼接给用户阅读,无需自行映射枚举。
|
||||||
- 不返回角色成员列表;如需"成员-角色"映射请去 AI 表格 Web 端。
|
- `roles[].members` 返回该角色当前成员;配置回读成功不等于成员实际权限已验证,仍需对应身份执行真实访问测试。
|
||||||
- 新建 Base 默认 `enabled=false`,开启后只有 `owner` / `manager` 两个 meta 角色;`system_editor` / `system_reader` 需要在 Web UI 给成员授权"可编辑/可查看"后才会被服务端自动生成。
|
- 新建 Base 默认 `enabled=false`,开启后只有 `owner` / `manager` 两个 meta 角色;`system_editor` / `system_reader` 需要在 Web UI 给成员授权"可编辑/可查看"后才会被服务端自动生成。
|
||||||
|
|
||||||
`role-list` / `role-get` 不需要管理员权限,普通成员也可读。
|
`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-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":"<UID>","memberType":"2"}]'
|
||||||
|
|
||||||
|
# 全量替换成员(跨组织示例,123456 需替换为成员所属组织 ID)
|
||||||
|
# 必须带完整目标列表和刚回读的默认角色;[] 表示明确清空
|
||||||
|
dws aitable +role-members-update --base-id BASE_ID --role-id ROLE_ID \
|
||||||
|
--members '[{"id":"<UID>","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 语义,不清空未传字段) | ✅ 需管理员 |
|
| 增量修改角色(PATCH 语义,不清空未传字段) | ✅ 需管理员 |
|
||||||
| 删除自定义角色 | ✅ 需管理员 |
|
| 删除自定义角色 | ✅ 需管理员 |
|
||||||
| 修改/删除系统角色 | ❌ 服务端禁止;只能在 AI 表格 Web 端操作 |
|
| 修改/删除系统角色 | ❌ 服务端禁止;只能在 AI 表格 Web 端操作 |
|
||||||
| 角色 ↔ 成员绑定 | ❌ CLI 暂不支持,需在 AI 表格 Web 端 → Base 设置 → 高级权限 → 角色管理面板手动完成 |
|
| 向角色追加成员 | ✅ `+role-members-add`,需管理员;写后回读 |
|
||||||
|
| 完整替换角色成员 | ✅ `+role-members-update`,需管理员;必须显式保留 defaultRole |
|
||||||
|
| 复制自定义角色 | ✅ `+role-copy`,不复制成员;系统角色禁止 |
|
||||||
|
| 查询用户命中角色 | ✅ `+role-query-user`,只读 |
|
||||||
|
|
||||||
## 错误码速查
|
## 错误码速查
|
||||||
|
|
||||||
|
|||||||
+23
-4
@@ -22,9 +22,9 @@ dws aitable chart get --base-id <BASE_ID> --dashboard-id <DASHBOARD_ID> --chart-
|
|||||||
|
|
||||||
- `dashboard get` 返回的 `charts[].chartId` 可直接给 `chart get` 使用
|
- `dashboard get` 返回的 `charts[].chartId` 可直接给 `chart get` 使用
|
||||||
- `chart create` 以及携带 `--layout` 的 `chart update` 会在写入前自动调用 `get_dashboard` 并强制校验根网格。只有 `schemaVersionTypeVerified=true` 时才使用原始类型判据:JSON number `2` 为 48 列,其他已验证值为 12 列;证据缺失时命令停止且不调用 Chart 写工具
|
- `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 以保留原值
|
- 全局 12/48 列只适用于根布局。存在非根 `parentId` 时使用容器自己的坐标系;拿不到容器真实列数时,创建操作停止,更新操作省略 layout 以保留原值
|
||||||
- `schemaVersion` 与 `isAppMode` 都是只读字段,不能写入或透传到 Dashboard config
|
- `schemaVersion` 与 `isAppMode` 都是只读字段,不能写入 Dashboard config
|
||||||
- 删除 dashboard 会级联删除其全部 chart;确认前必须说明该影响
|
- 删除 dashboard 会级联删除其全部 chart;确认前必须说明该影响
|
||||||
- `dashboard share get` 可能返回 `404`(资源不存在或未开通),需按可重试错误处理,不要误判为参数拼错
|
- `dashboard share get` 可能返回 `404`(资源不存在或未开通),需按可重试错误处理,不要误判为参数拼错
|
||||||
- `chart share get` 可正常返回 `enabled/shareUrl`,用于分享状态判断
|
- `chart share get` 可正常返回 `enabled/shareUrl`,用于分享状态判断
|
||||||
@@ -35,7 +35,7 @@ dws aitable chart get --base-id <BASE_ID> --dashboard-id <DASHBOARD_ID> --chart-
|
|||||||
| 命令 | 用途 | 必填参数 | 说明 |
|
| 命令 | 用途 | 必填参数 | 说明 |
|
||||||
|------|------|----------|------|
|
|------|------|----------|------|
|
||||||
| `dashboard config-example` | 查看完整仪表盘配置模板 | 无 | 仅在完整 config 结构缺失时调用一次 |
|
| `dashboard config-example` | 查看完整仪表盘配置模板 | 无 | 仅在完整 config 结构缺失时调用一次 |
|
||||||
| `dashboard arrange` | 自动重排图表布局 | `--base-id` `--dashboard-id` | 把图表按行铺满网格,避免某行只占半幅、留下大片空白;返回 `{totalColumns, layout, alignedChartCount}` |
|
| `dashboard arrange` | 自动重排图表布局 | `--base-id` `--dashboard-id` | 仅用户明确接受统一按行重排时使用;改变位置、宽高和叠放关系;返回 `{totalColumns, layout, alignedChartCount}` |
|
||||||
|
|
||||||
## chart 子命令
|
## chart 子命令
|
||||||
|
|
||||||
@@ -49,4 +49,23 @@ dws aitable chart get --base-id <BASE_ID> --dashboard-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 <JSON数组>` 集中编排,写前只读确认能力后优先服务端批建,缺能力才串行写;`--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 <B> --table-id <T> --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` 强制旧路径。写入开始后任何超时或未知结果都不切换路径重放。
|
||||||
+24
@@ -74,6 +74,14 @@ dws aitable field create --base-id <BASE_ID> --table-id <TABLE_ID> \
|
|||||||
|
|
||||||
### AI 字段创建示例
|
### AI 字段创建示例
|
||||||
|
|
||||||
|
内置 AI 插件先用 `field ai-plugins --base-id <BASE_ID> --table-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":["<FIELD_ID>"],"autoRecompute":true}`,无需通用 AI 的 `prompt` / `outputType`。由 MCP 校验与转换,CLI 不自行构造 `decoratorConfigs`。回执 `verificationStatus=pending` 时继续 `field get` 只读核对,不能视为已生效或再次写入。
|
||||||
|
|
||||||
|
快递/OCR 等已支持的内置模板可能要求基础字段类型 `object`,按 `ai-plugins` 返回的 `fieldTypes` 选择;这不表示可以创建任意普通 object 字段。
|
||||||
|
|
||||||
|
下例是通用 AI 配置:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dws aitable field create --base-id <BASE_ID> --table-id <TABLE_ID> \
|
dws aitable field create --base-id <BASE_ID> --table-id <TABLE_ID> \
|
||||||
--name "AI摘要" --type text \
|
--name "AI摘要" --type text \
|
||||||
@@ -95,6 +103,22 @@ dws aitable field create --base-id <BASE_ID> --table-id <TABLE_ID> \
|
|||||||
- `autoRecompute=true` 表示引用字段变化后自动重算
|
- `autoRecompute=true` 表示引用字段变化后自动重算
|
||||||
- **AI 字段的 prompt 必须至少包含一个 `fieldRef` 引用**,纯文本 prompt 会被后端拒绝
|
- **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 格式有严格要求:
|
创建 `lookup`(关联引用)和 `filterUp`(查找引用)字段时,config 格式有严格要求:
|
||||||
|
|||||||
+15
-3
@@ -13,6 +13,7 @@
|
|||||||
| `form field update` | 更新字段必填/描述 |
|
| `form field update` | 更新字段必填/描述 |
|
||||||
| `form field hide` | 在表单中隐藏/显示字段(不影响底层数据表字段) |
|
| `form field hide` | 在表单中隐藏/显示字段(不影响底层数据表字段) |
|
||||||
| `form share get` | 获取分享配置 |
|
| `form share get` | 获取分享配置 |
|
||||||
|
| `form share url` | 获取标准填写链接,可用 `--qr-file form.png` 生成本地二维码 |
|
||||||
| `form share update` | 部分更新分享开关、授权、有效期和通知等配置 |
|
| `form share update` | 部分更新分享开关、授权、有效期和通知等配置 |
|
||||||
| `form questions create` | 添加题目(等价于 `field create`,命令位置上的别名) |
|
| `form questions create` | 添加题目(等价于 `field create`,命令位置上的别名) |
|
||||||
| `form questions delete` | 删除题目(等价于 `field delete`,命令位置上的别名) |
|
| `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
|
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 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 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 get` 的 `data` 是客户端按 `viewId` 精确筛出的单个表单对象,不是 `formViews` 数组。Agent 直接读取 `data.viewId` / `data.name`;不存在的 `viewId` 会明确失败。
|
||||||
- `form field list` 仅返回**未隐藏**的字段;`hidden=true` 的字段不在此返回,如需查看全部字段请用 `field get`。
|
- `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 field hide` 当前每次只接收一个 `fieldId`。多字段必须在同一 Base 写队列中逐个串行设置,全部完成后统一回读一次;不传数组,不并发写。
|
||||||
- 新建表单首次开启分享时,复用 `form create`/`form update` 中已知的标题,通过 `--form-name` 与 `--enabled true` 同时传入;不要为取标题额外调用 `form get`。已有分享仅调整其他配置时,不覆盖原名称。
|
- 新建表单首次开启分享时,复用 `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` 的成功结果已经来自服务端回读并完成 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`。
|
- DWS 不自行调用第二个 View 更新命令补偿 CP,也不拼装封面 URL。旧服务端发布窗口内 `formCover` 可能为空,应如实说明;稍后诊断当前配置时可用 `form share get`。
|
||||||
- “已开启分享”不等于“已允许匿名/免登录/组织外提交”;需按用户意图显式传入 `--anonymous-submit` 和 `--auth-type-code/--auth-data`。写超时时先查询真实状态,不原样重放 mutation。
|
- “已开启分享”不等于“已允许匿名/免登录/组织外提交”;需按用户意图显式传入 `--anonymous-submit` 和 `--auth-type-code/--auth-data`。写超时时先查询真实状态,不原样重放 mutation。
|
||||||
+1
-1
@@ -12,7 +12,7 @@ dws doc +create --name "<文档名>" --content @body.json --doc-format jsonml --
|
|||||||
|
|
||||||
- 统一输入协议:已有或临时文件先暂存到当前工作目录后传 `@相对文件`;单次生成文本可用 `--content -` 从 stdin 读取。
|
- 统一输入协议:已有或临时文件先暂存到当前工作目录后传 `@相对文件`;单次生成文本可用 `--content -` 从 stdin 读取。
|
||||||
- `@file` 禁止绝对路径和 `..` 逃逸;不要直接引用宿主临时目录。
|
- `@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。
|
- JSONML 顶层必须是数组。用户已经明确要求 callout、代码块、表格等具体富结构时,直接构造一份完整 JSONML 并只调用一次 `+create`;这不是开放式风格设计,不读取 `style/`。字段不确定时只读 [JSONML cookbook](format/doc-jsonml-cookbook.md),不再连读 create/update/style。
|
||||||
|
|
||||||
`+create` 负责创建、长 Markdown 分片和最终回读验证。正常成功结果至少包含:
|
`+create` 负责创建、长 Markdown 分片和最终回读验证。正常成功结果至少包含:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: dingtalk-minutes
|
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:
|
metadata:
|
||||||
cli_version: ">=0.2.14"
|
cli_version: ">=0.2.14"
|
||||||
category: product
|
category: product
|
||||||
@@ -52,7 +52,7 @@ metadata:
|
|||||||
| 上传音视频生成听记 | `dws minutes +upload --file <相对路径>` | 真实执行会上传文件并创建远端听记,必须按 Runtime confirmation;用户明确要闪记卡片时改用 `+upload-and-notify`,需要上传后等待分析产物时用 `+upload-and-analyze` |
|
| 上传音视频生成听记 | `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` |
|
| 真实开始、暂停、继续或停止录音 | `+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 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 <taskUuid>` | 创建后有界轮询;超时或未知状态保留恢复信息,用 `--resume` 继续,不重复创建 |
|
| 生成或继续查询听记内置思维导图 | `dws minutes +mindmap --id <taskUuid>` | 创建后有界轮询;超时或未知状态保留恢复信息,用 `--resume` 继续,不重复创建 |
|
||||||
| 生成或继续发言人洞察 | `dws minutes +speaker-insights --id <taskUuid>` | 只有 `state=ready/complete=true` 才完成;pending 保留 `taskId` 并同账号 `--resume [--task-id <ID>]`,不重复创建;用户给定等待时长传 `--timeout` |
|
| 生成或继续发言人洞察 | `dws minutes +speaker-insights --id <taskUuid>` | 只有 `state=ready/complete=true` 才完成;pending 保留 `taskId` 并同账号 `--resume [--task-id <ID>]`,不重复创建;用户给定等待时长传 `--timeout` |
|
||||||
| 当前用户申请查看/下载/编辑权限 | `dws minutes +apply-permission --id <taskUuid> --permission view|download|edit` | 这是“我申请访问”,不是所有者给别人授权;按 Runtime confirmation 执行 |
|
| 当前用户申请查看/下载/编辑权限 | `dws minutes +apply-permission --id <taskUuid> --permission view|download|edit` | 这是“我申请访问”,不是所有者给别人授权;按 Runtime confirmation 执行 |
|
||||||
| 所有者给成员授权或撤权 | `dws minutes +share ...` / `dws minutes +unshare ...` | 先用通讯录把姓名解析为同组织稳定 UID;撤权是破坏性操作。批量结果必须保留逐成员 ledger 和失败项 |
|
| 所有者给成员授权或撤权 | `dws minutes +share ...` / `dws minutes +unshare ...` | 先用通讯录把姓名解析为同组织稳定 UID;撤权是破坏性操作。批量结果必须保留逐成员 ledger 和失败项 |
|
||||||
@@ -95,6 +95,10 @@ dws minutes +list-all --page-all --format json
|
|||||||
- 用户明确要求“仅预览/不实际写入”时,任务在真实现状读取、dry-run 计划和差异交付后结束;不得继续请求写入确认,也不得为了验证预览而执行真实写入后再还原。
|
- 用户明确要求“仅预览/不实际写入”时,任务在真实现状读取、dry-run 计划和差异交付后结束;不得继续请求写入确认,也不得为了验证预览而执行真实写入后再还原。
|
||||||
- 分享/撤权使用稳定成员 UID,不能把姓名、手机号或跨组织 ID 直接当 UID;同一目标解析、读取、写入和验证必须使用同一 profile。
|
- 分享/撤权使用稳定成员 UID,不能把姓名、手机号或跨组织 ID 直接当 UID;同一目标解析、读取、写入和验证必须使用同一 profile。
|
||||||
|
|
||||||
|
## 脑图目标边界
|
||||||
|
|
||||||
|
`minutes +mindmap` 的目标是听记内置思维导图,结果展示在听记详情中,不创建可独立编辑的 .amind 文件。用户明确要求独立脑图或编辑已有脑图主题时,转 [独立脑图](../dingtalk-misc/references/mind.md)。已有听记 URL 不覆盖明确的独立文件意图;只有目标未明时才消歧。若用户明确另存独立脑图,可先读取获授权的听记内容,再按 mind reference 构造 OpenNodes;不套用内置生成的“禁止先读取内容”规则。
|
||||||
|
|
||||||
## 按需加载
|
## 按需加载
|
||||||
|
|
||||||
Golden Route 参数足够时直接执行,不预读 Reference。每个 Case 最多先读取一个最精确的文件;参数事实优先读取 compact leaf Schema,不读取产品级全量 Catalog。
|
Golden Route 参数足够时直接执行,不预读 Reference。每个 Case 最多先读取一个最精确的文件;参数事实优先读取 compact leaf Schema,不读取产品级全量 Catalog。
|
||||||
|
|||||||
@@ -28,6 +28,8 @@
|
|||||||
|
|
||||||
摘要脚本的 `--dry-run` 只打印计划且不得调用 DWS;DWS 失败、非法 JSON 与合法空结果必须分开。摘要脚本没有覆盖完整分页、目标消歧与跨 scope 聚合,因此不能用它的输出声称“全部听记”。时间转换脚本始终纯本地,不提供或需要 `--dry-run`。行动项读取由 `+action-items`(单条)或 `+detail --ids ... --artifacts todos`(多条)承接,不再发布重复脚本。
|
摘要脚本的 `--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:
|
命令前缀统一为 `dws minutes`。结构化读取加 `--format json`;参数不确定时查询精确 leaf:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: dingtalk-misc
|
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:
|
metadata:
|
||||||
cli_version: ">=1.0.61"
|
cli_version: ">=1.0.61"
|
||||||
category: product
|
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。
|
同一请求同时出现日报、周报或简报名称与审批人、抄送人、审批路径、审批单等审批意图时,审批意图优先,统一路由 OA 并读取 `oa.md`;本次任务不得执行 `dws report`。OA 中没有同名模板也不能降级提交 Report,只能交付 OA 搜索结果或请求消歧。只有用户明确要求提交钉钉日志,且没有审批流语义时,才路由 Report。
|
||||||
|
|
||||||
Attendance 任务直接按产品索引读取一份最匹配的 `attendance*.md`,不要重复预读 `dingtalk-shared`。只有出现跨产品编排、profile/认证问题、未知全局错误或 Reference 明确指向 shared 时,才按需读取 shared 对应内容。
|
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。缺少任一身份时停止并补齐,禁止转成独立白板。
|
用户明确要求创建或操作白板文件、独立白板时,直接读取 [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 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) |
|
| 直播 / 我的直播 / 直播列表 | 直播列表与直播记录查询 | `dws live` | [live.md](references/live.md) |
|
||||||
| DING / 紧急通知 / 电话DING / 短信DING / 必达消息 | DING 紧急消息(应用内/短信/电话),个人DING | `dws ding` | [ding.md](references/ding.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) |
|
| DEAP 数字员工 / 数字员工 / 人设 / 群感知 / DSH 接入 | 兼容路由;完整自然语言编排只由独立 `dingtalk-tag` skill 维护 | `dws dingtalk-tag` | 切换 [`dingtalk-tag`](../dingtalk-tag/SKILL.md) |
|
||||||
| 目标管理 / 战略解码 / 经营合约字段 / 计分卡 / OKR / 目标规则周期 / 个人目标 / 目标模板 / 周月报规则统计 / 按时迟交未提交 / 跟催 | Agoal 目标管理与经营目标跟进 | `dws agoal` | [agoal.md](references/agoal.md) |
|
| 目标管理 / 战略解码 / 经营合约字段 / 计分卡 / OKR / 目标规则周期 / 个人目标 / 目标模板 / 周月报规则统计 / 按时迟交未提交 / 跟催 | Agoal 目标管理与经营目标跟进 | `dws agoal` | [agoal.md](references/agoal.md) |
|
||||||
| 日报 / 周报 / 月报 / 写日志 / 收件箱日志 / 发件箱日志 | 日志(日报/周报/月报)查询与按模版提交 | `dws report`(别名 `dws log`) | [report.md](references/report.md) |
|
| 日报 / 周报 / 月报 / 写日志 / 收件箱日志 / 发件箱日志 | 日志(日报/周报/月报)查询与按模版提交 | `dws report`(别名 `dws log`) | [report.md](references/report.md) |
|
||||||
| 电子表格 / 工作表 / 单元格读写 / 公式 / 超链接 / 浮动图片 | 电子表格创建/读写/公式/超链接/浮动图片/导出 | `dws sheet` | [sheet.md](references/sheet.md) |
|
| 电子表格 / 工作表 / 单元格读写 / 公式 / 超链接 / 浮动图片 | 电子表格创建/读写/公式/超链接/浮动图片/导出 | `dws sheet` | [sheet.md](references/sheet.md) |
|
||||||
| 开放平台文档 / API文档 / 接口文档 / 接口报错 | 开放平台开发文档搜索 | `dws devdoc` | [devdoc.md](references/devdoc.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) |
|
| 未封装 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) |
|
| 已有承载文档 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 mono 或 multi skill | DWS 技能市场搜索、下载、安装与内置技能部署 | `dws skill` | [skill.md](references/skill.md) |
|
||||||
| 人才池 / 储备干部池 / 员工档案 / 职业历程 / 绩效记录 / 员工标签 / 组织大脑 / 人才搜索 | 组织大脑:人才池、员工档案专项模块与结构化人才搜索 | `dws hrbrain` | [hrbrain.md](references/hrbrain.md) |
|
| 人才池 / 储备干部池 / 员工档案 / 职业历程 / 绩效记录 / 员工标签 / 组织大脑 / 人才搜索 | 组织大脑:人才池、员工档案专项模块与结构化人才搜索 | `dws hrbrain` | [hrbrain.md](references/hrbrain.md) |
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ dws dev app get --unified-app-id <id> --format json
|
|||||||
dws dev app get --app-key <appKey> --format json
|
dws dev app get --app-key <appKey> --format json
|
||||||
|
|
||||||
# 写操作:同一条命令先预检,再正式执行
|
# 写操作:同一条命令先预检,再正式执行
|
||||||
|
# create 的 --desc 服务端必填(缺省返回 67010 DESC_INVALID),名称仅限中英文/数字/指定标点
|
||||||
dws dev app create --name <名称> --desc <描述> --dry-run --format json
|
dws dev app create --name <名称> --desc <描述> --dry-run --format json
|
||||||
dws dev app update --unified-app-id <id> --desc <描述> --dry-run --format json
|
dws dev app update --unified-app-id <id> --desc <描述> --dry-run --format json
|
||||||
dws dev app disable --unified-app-id <id> --dry-run --format json
|
dws dev app disable --unified-app-id <id> --dry-run --format json
|
||||||
|
|||||||
+2
@@ -31,4 +31,6 @@ dws dev connect --daemon --unified-app-id <id> --channel <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` 报告,未经用户要求不扩展成系统安装排障。
|
前台 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。
|
同一 restart/connect 错误且记录、参数、状态没变化时停止,不重复 help、环境探测或重启。已知路径直接执行;仅当前 leaf flag 确有疑问时查一次 compact Schema,Schema 不可用才查一次精确 leaf help。
|
||||||
+1
@@ -8,6 +8,7 @@
|
|||||||
| AppKey、AppSecret、clientId、agentId | `dws dev app credentials get` | [`credentials.md`](./credentials.md) |
|
| AppKey、AppSecret、clientId、agentId | `dws dev app credentials get` | [`credentials.md`](./credentials.md) |
|
||||||
| 开放平台应用的管理成员、成员角色与角色汇总 | `dws dev app member ...` | [`member.md`](./member.md) |
|
| 开放平台应用的管理成员、成员角色与角色汇总 | `dws dev app member ...` | [`member.md`](./member.md) |
|
||||||
| 应用权限 | `dws dev app permission ...` | [`permission.md`](./permission.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) |
|
| IP 白名单、回调或 SSO 地址 | `dws dev app security config` | [`security.md`](./security.md) |
|
||||||
| H5、PC 主页与 OMP 地址 | `dws dev app webapp ...` | [`webapp.md`](./webapp.md) |
|
| H5、PC 主页与 OMP 地址 | `dws dev app webapp ...` | [`webapp.md`](./webapp.md) |
|
||||||
| 机器人配置、启停、提交审核 | `dws dev app robot ...` | [`robot.md`](./robot.md) |
|
| 机器人配置、启停、提交审核 | `dws dev app robot ...` | [`robot.md`](./robot.md) |
|
||||||
|
|||||||
+10
-2
@@ -3,7 +3,7 @@
|
|||||||
命令分为两层:
|
命令分为两层:
|
||||||
|
|
||||||
- `dws dev mcp ...`:开发端,管理服务、工具、鉴权、凭证、协作者和 HSF 方法。
|
- `dws dev mcp ...`:开发端,管理服务、工具、鉴权、凭证、协作者和 HSF 方法。
|
||||||
- `dws mcp published ...`:消费端,按 `mcpId` 查看或调用已发布工具。
|
- `dws mcp market search` / `dws mcp published ...`:消费端。只知道名称时先按关键词搜索 MCP 市场拿到 `mcpId`,再按 `mcpId` 查看或调用已发布工具。
|
||||||
|
|
||||||
远端 `serverName` 和工具名不会注册成新的顶层命令,也不写入跨身份缓存。消费端每次按当前登录 profile、用户和组织身份解析服务地址;输出只展示脱敏地址。
|
远端 `serverName` 和工具名不会注册成新的顶层命令,也不写入跨身份缓存。消费端每次按当前登录 profile、用户和组织身份解析服务地址;输出只展示脱敏地址。
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
1. 下列已知 leaf 直接执行,不先读分组 help。只有创建/更新等复杂字段或确认语义确有不确定性时,读取一次 `dws schema --cli-path "dev mcp <leaf>" --compact --format json`;Schema 与 Cobra 不一致时才读取同一 leaf 的一次 help。
|
1. 下列已知 leaf 直接执行,不先读分组 help。只有创建/更新等复杂字段或确认语义确有不确定性时,读取一次 `dws schema --cli-path "dev mcp <leaf>" --compact --format json`;Schema 与 Cobra 不一致时才读取同一 leaf 的一次 help。
|
||||||
2. 开发写操作的最初请求只允许 `--dry-run --format json`。展示 invocation 中的准确对象、动作、业务参数和影响,等用户对该预览明确确认后,才把同一命令仅由 `--dry-run` 换成 `--yes`;目标或业务参数变化就重新预检并确认,确认前不得真实写入。
|
2. 开发写操作的最初请求只允许 `--dry-run --format json`。展示 invocation 中的准确对象、动作、业务参数和影响,等用户对该预览明确确认后,才把同一命令仅由 `--dry-run` 换成 `--yes`;目标或业务参数变化就重新预检并确认,确认前不得真实写入。
|
||||||
3. 发布前执行 `tool get` 和 `tool debug`,核对输入、输出与映射。
|
3. 发布前执行 `tool get` 和 `tool debug`,核对输入、输出与映射。
|
||||||
4. 调用已发布工具前先执行 `mcp published tools <mcpId>`,按返回的 `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 <mcpId>`,按返回的 `inputSchema` 构造 `--params`;真实 invoke 还会在本次调用中重新发现工具并按实时 Schema 校验。
|
||||||
5. `mcp published invoke` 无法静态判断远端工具副作用,真实调用一律需要 `--yes`;未明确影响时只做 dry-run。
|
5. `mcp published invoke` 无法静态判断远端工具副作用,真实调用一律需要 `--yes`;未明确影响时只做 dry-run。
|
||||||
6. MCP URL、凭证内容、token 和密钥不得写入回答、日志、文档或代码仓库。
|
6. MCP URL、凭证内容、token 和密钥不得写入回答、日志、文档或代码仓库。
|
||||||
7. 若真实命令返回 `endpoint_not_resolved`,表示当前 CLI 的 `mcpdev` 运行端点不可用,不是 flag 错误;立即报告并停止,不用 help、改参数、Raw API 或 dry-run 冒充业务完成。
|
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。
|
确认帮助中包含 `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
|
```bash
|
||||||
dws mcp published tools <mcpId> --format json
|
dws mcp published tools <mcpId> --format json
|
||||||
dws mcp published invoke <mcpId> <toolName> \
|
dws mcp published invoke <mcpId> <toolName> \
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# 应用可见范围(开放平台应用)
|
||||||
|
|
||||||
|
面向开放平台应用(企业内部应用/三方应用)的可见人员范围,不是钉盘、文档或知识库的分享范围。
|
||||||
|
|
||||||
|
## 查询
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dws dev app scope get --unified-app-id <unifiedAppId> --format json
|
||||||
|
```
|
||||||
|
|
||||||
|
- 只读;返回部门/角色/人员等可见范围配置。
|
||||||
|
|
||||||
|
## 设置(全量替换语义)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 第 1 步(可选):本地调用预览,--dry-run 不发起任何远端调用,不是服务端预检
|
||||||
|
dws dev app scope set --unified-app-id <unifiedAppId> \
|
||||||
|
--user-visible-scopes <scopeValue1,scopeValue2> \
|
||||||
|
--precheck-only --dry-run --format json
|
||||||
|
|
||||||
|
# 第 2 步:服务端预检——真实调用,只生成计划不写入;经用户确认后执行
|
||||||
|
dws dev app scope set --unified-app-id <unifiedAppId> \
|
||||||
|
--user-visible-scopes <scopeValue1,scopeValue2> \
|
||||||
|
--precheck-only --yes --format json
|
||||||
|
|
||||||
|
# 第 3 步:核对计划(尤其将被移除的范围)并确认后,正式执行
|
||||||
|
dws dev app scope set --unified-app-id <unifiedAppId> \
|
||||||
|
--user-visible-scopes <scopeValue1,scopeValue2> \
|
||||||
|
--precheck-only=false --yes --format json
|
||||||
|
```
|
||||||
|
|
||||||
|
- `--dry-run` 是本地调用预览(不访问服务端);服务端预检必须用 `--precheck-only --yes` 真实调用,返回计划与替换影响,不会写入。
|
||||||
|
- `scope set` 是**全量替换**:本次未传入的既有范围会被移除;增量添加前先 `scope get` 确认现状并在同一次调用带上完整集合。
|
||||||
|
- 范围列表支持 CSV 或 JSON 数组;`[]` 或 `,` 转换后为空会在本地直接拒绝。
|
||||||
|
- `--is-hidden` 控制应用可见性;显式传 `--is-hidden=false` 会被保留传递,不会丢失。
|
||||||
|
- 正式执行前必须完成服务端预检并取得用户对计划(含将被移除范围)的确认;参数变化需重新预检。
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
# 钉钉独立脑图
|
||||||
|
|
||||||
|
本页用于独立 `.amind` 脑图文件的带内容创建、主题查询和节点增删改。
|
||||||
|
|
||||||
|
| 目标 | 入口 |
|
||||||
|
|---|---|
|
||||||
|
| 创建带页面、主题和层级的独立脑图,或修改其中主题 | `dws mind` |
|
||||||
|
| 只在知识库创建空白脑图文件,不初始化主题内容 | `dws wiki node create --type amind` |
|
||||||
|
| 生成听记内置思维导图(结果在听记详情中) | `dws minutes +mindmap --id <taskUuid>`;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 不覆盖错误说明。
|
||||||
+66
@@ -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 等临时值编辑文件。
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
# 钉钉白板内容操作
|
# 钉钉白板内容操作
|
||||||
|
|
||||||
|
OpenNodes 不是白板专属名称:独立脑图的 mind-v1 页面/主题树使用 [mind.md](./mind.md),不能传给白板;仅提到 OpenNodes 而目标未明时先消歧。
|
||||||
|
|
||||||
本页覆盖两个相互独立的目标,不能互相推断、转换或回退:
|
本页覆盖两个相互独立的目标,不能互相推断、转换或回退:
|
||||||
|
|
||||||
- 独立白板是 `.adraw` 白板文件,使用白板自身 `nodeId`,不存在文档 `partId`。
|
- 独立白板是 `.adraw` 白板文件,使用白板自身 `nodeId`,不存在文档 `partId`。
|
||||||
|
|||||||
@@ -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`。更新时不传就不更新该字段,显式值保持不变;最终有效配置由服务端按当前草稿校验。
|
- 创建时未提供 `--response-mode` 或值为空,CLI 默认发送 `mention_only`,包括 `local_agent`。更新时不传就不更新该字段,显式值保持不变;最终有效配置由服务端按当前草稿校验。
|
||||||
- 创建要求名称、描述和主程序类型;部门可不传,由服务端补操作人的主任职部门。不要向用户索要部门名称、岗位、工号或内部 uid。
|
- 创建要求名称、描述和主程序类型;部门可不传,由服务端补操作人的主任职部门。不要向用户索要部门名称、岗位、工号或内部 uid。
|
||||||
- `save-draft` 只更新用户明确修改的基础草稿字段,不接受 Skill/MCP 完整数组。Skill/MCP 生命周期统一使用 `capability skill|mcp create|update|delete|list|query`;不设计 attach/detach,`check_mcp` 只在 create 和带配置的 update 内部调用。
|
- `save-draft` 只更新用户明确修改的基础草稿字段,不接受 Skill/MCP 完整数组。Skill/MCP 生命周期统一使用 `capability skill|mcp create|update|delete|list|query`;不设计 attach/detach,`check_mcp` 只在 create 和带配置的 update 内部调用。
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ dws dingtalk-tag capability skill query --agent-uuid <agentUuid> --skill-id <ski
|
|||||||
|
|
||||||
Skill ZIP 最大 50 MiB,必须包含 `SKILL.md`。create 和带 `--file` 的 update 会先校验本地 ZIP,再通过 OpenAPI multipart 上传;临时上传地址不会输出或落盘。update 必须二选一且互斥:切换启停用 `--enabled=true|false`、替换包用 `--file`,二者不能同时提供(对应服务端 update_skill 的 fileUrl 不能与 enabled/attributes 并存,同时传会被 CLI 本地拦截);未提供的字段保持原值。create/update/delete 均为 `confirmation=user_required`,必须先预览并取得用户确认。
|
Skill ZIP 最大 50 MiB,必须包含 `SKILL.md`。create 和带 `--file` 的 update 会先校验本地 ZIP,再通过 OpenAPI multipart 上传;临时上传地址不会输出或落盘。update 必须二选一且互斥:切换启停用 `--enabled=true|false`、替换包用 `--file`,二者不能同时提供(对应服务端 update_skill 的 fileUrl 不能与 enabled/attributes 并存,同时传会被 CLI 本地拦截);未提供的字段保持原值。create/update/delete 均为 `confirmation=user_required`,必须先预览并取得用户确认。
|
||||||
|
|
||||||
|
权限失败以服务端具体文案为准。Skill 上传阶段保留公开权限管理页链接,不因包含该链接而清空整段引导;凭证、签名地址及其他 URL 仍脱敏。Skill 更新仅收到裸 `NO_PERMISSION` 时,CLI 补充条件式说明:未获管理权限可联系企业主管理员在管理页授权;已是共同管理员、部门管理员或企业主管理员则联系平台核查,不要求重复申请或扩大权限。保留原错误码和可用 Trace ID,不自动重试;获取上传凭证被拒不代表已经执行 ZIP 替换。
|
||||||
|
|
||||||
## MCP
|
## MCP
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
|||||||
+4
-2
@@ -30,7 +30,9 @@ dws dingtalk-tag connect restart --agent-uuid <agentUuid> --format json
|
|||||||
|
|
||||||
连接使用数字员工 Profile 启动 Event Consumer,调用与 dev connect 共用的 Agent 协议,以员工身份按回复模式更新 A2UI 卡片,或引用回复最终文本。后台结果只有在 ready 后才返回运行成功;不支持的 Agent 或缺失的依赖必须明确报错。
|
连接使用数字员工 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,群消息仍需满足用户白名单。不要向用户索要其它内部人员标识。
|
默认仅主管可用;`--allowed-users` 接收精确 userId 并在员工上下文解析,`--allowed-groups` 接收该上下文的群会话 ID,群消息仍需满足用户白名单。不要向用户索要其它内部人员标识。
|
||||||
|
|
||||||
@@ -50,7 +52,7 @@ dws dingtalk-tag manage create \
|
|||||||
--dry-run --format json
|
--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 恢复。
|
||||||
|
|
||||||
## 修改:按字段更新
|
## 修改:按字段更新
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
```
|
```
|
||||||
Usage:
|
Usage:
|
||||||
dws dingtalk-tag manage create --name <名称> --description <职责描述> --type <open_code|local_agent> [flags]
|
dws dingtalk-tag manage create --name <名称> --description <职责描述> --type <qwenwork|local_agent> [flags]
|
||||||
Flags:
|
Flags:
|
||||||
--name 必填,同组织内唯一(≤30 Unicode 码点)
|
--name 必填,同组织内唯一(≤30 Unicode 码点)
|
||||||
--description 必填,职责描述(≤300 码点)
|
--description 必填,职责描述(≤300 码点)
|
||||||
@@ -14,30 +14,30 @@ Flags:
|
|||||||
--dept-id 归属部门 ID;可选,省略时服务端补操作人主任职部门
|
--dept-id 归属部门 ID;可选,省略时服务端补操作人主任职部门
|
||||||
--avatar-url 公网 HTTP(S) 头像地址,或本地图片路径(≤10 MiB)
|
--avatar-url 公网 HTTP(S) 头像地址,或本地图片路径(≤10 MiB)
|
||||||
--supervisor-user-id 直属上级 userId
|
--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
|
--response-mode mention_only | targeted_proactive | mention_only,targeted_proactive;未提供或空值时默认 mention_only
|
||||||
Example:
|
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` 当前为 `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 — 查询
|
## detail / list — 查询
|
||||||
|
|
||||||
```
|
```
|
||||||
Usage:
|
Usage:
|
||||||
dws dingtalk-tag manage detail --agent-uuid <agentUuid> [--snapshot draft|published]
|
dws dingtalk-tag manage detail --agent-uuid <agentUuid> [--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:
|
Example:
|
||||||
dws dingtalk-tag manage detail --agent-uuid <agentUuid> --format json
|
dws dingtalk-tag manage detail --agent-uuid <agentUuid> --format json
|
||||||
dws dingtalk-tag manage list --keyword "周报" --type local_agent --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`。
|
`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 <agentUuid> --visibility PAR
|
|||||||
```
|
```
|
||||||
|
|
||||||
- `--agent-uuid`、`--visibility` 必填;`ALL` 表示本企业全员,`PARTIAL` 表示指定成员或部门。
|
- `--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` 无需成员/部门列表。
|
- 这是**全量替换草稿可见范围**,不会自动发布。未传某一维度会清空该维度,不能把本次输入当作增量追加;`ALL` 无需成员/部门列表。
|
||||||
- `--user-ids` 接收当前组织内的 userId,MCP 请求字段仍是 `staffIds`。
|
- `--user-ids` 接收当前组织内的 userId,MCP 请求字段仍是 `staffIds`。
|
||||||
- 先预览并让用户确认替换范围,再执行带 `--yes` 的命令。其他草稿字段保持不变。
|
- 先预览并让用户确认替换范围,再执行带 `--yes` 的命令。其他草稿字段保持不变。
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ Example:
|
|||||||
dws dingtalk-tag run run-status --agent-uuid <id> --source-id <openMessageId> --source-type im_message --format json
|
dws dingtalk-tag run run-status --agent-uuid <id> --source-id <openMessageId> --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` 返回 Langfuse 原始 trace JSON,**含完整对话与模型输入输出**;服务端先做管理者 / 触发人两级授权,无权返回 `NO_PERMISSION`
|
||||||
- `trace` 输出为 `null`(无 data)**不是报错**,而是该来源暂无可用 trace:可能 trace 尚未就绪(异步写入有延迟)、来源 ID/类型不匹配,或该来源无对应执行;可稍后重试,或先用 `run-status` 确认执行是否存在
|
- `trace` 输出为 `null`(无 data)**不是报错**,而是该来源暂无可用 trace:可能 trace 尚未就绪(异步写入有延迟)、来源 ID/类型不匹配,或该来源无对应执行;可稍后重试,或先用 `run-status` 确认执行是否存在
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ metadata:
|
|||||||
|
|
||||||
# 钉钉知识库 Skill
|
# 钉钉知识库 Skill
|
||||||
|
|
||||||
|
|
||||||
|
脑图创建边界:`wiki node create --type amind` 只创建空白文件,不携带初始化主题内容。用户要求按已有内容或层级创建脑图时,转 [独立脑图](../dingtalk-misc/references/mind.md) 的 `dws mind create`;不向 wiki 命令猜传 source。
|
||||||
|
|
||||||
<!-- DWS_RUNTIME_CONTRACT_START -->
|
<!-- DWS_RUNTIME_CONTRACT_START -->
|
||||||
## 最小 DWS 执行契约
|
## 最小 DWS 执行契约
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
"sourceId": "find-skills",
|
"sourceId": "find-skills",
|
||||||
"repo": "https://github.com/vercel-labs/skills.git",
|
"repo": "https://github.com/vercel-labs/skills.git",
|
||||||
"ref": "main",
|
"ref": "main",
|
||||||
"commit": "d8eca76d5dbe47df2d146d036e32534b74ece57a",
|
"commit": "699d35b21bd3c97dbc0edeae062b2698898bb2e5",
|
||||||
"adapter": "claude-skill",
|
"adapter": "claude-skill",
|
||||||
"sourcePath": "skills/find-skills",
|
"sourcePath": "skills/find-skills",
|
||||||
"syncedAt": "2026-10-28T01:57:49Z"
|
"syncedAt": "2026-10-28T15:59:59Z"
|
||||||
}
|
}
|
||||||
@@ -3,5 +3,5 @@
|
|||||||
"name": "playwright浏览器自动化操作",
|
"name": "playwright浏览器自动化操作",
|
||||||
"version": "20260605",
|
"version": "20260605",
|
||||||
"keySource": "none",
|
"keySource": "none",
|
||||||
"syncedAt": "2026-10-28T02:01:13Z"
|
"syncedAt": "2026-10-28T16:03:24Z"
|
||||||
}
|
}
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
"sourceId": "next-skills",
|
"sourceId": "next-skills",
|
||||||
"repo": "https://github.com/vercel/next.js.git",
|
"repo": "https://github.com/vercel/next.js.git",
|
||||||
"ref": "canary",
|
"ref": "canary",
|
||||||
"commit": "7356b0bcb5f22371c15dcb413afe5fb28412f52d",
|
"commit": "f922205f9e860de24673269f81dea51e9e33b0cb",
|
||||||
"adapter": "skill-collection",
|
"adapter": "skill-collection",
|
||||||
"sourcePath": "skills",
|
"sourcePath": "skills",
|
||||||
"syncedAt": "2026-10-28T01:57:49Z"
|
"syncedAt": "2026-10-28T15:59:59Z"
|
||||||
}
|
}
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
"sourceId": "ppt-master",
|
"sourceId": "ppt-master",
|
||||||
"repo": "https://github.com/hugohe3/ppt-master.git",
|
"repo": "https://github.com/hugohe3/ppt-master.git",
|
||||||
"ref": "main",
|
"ref": "main",
|
||||||
"commit": "f3660bfcd20bc07c29260b7e330261a01a2b5a6d",
|
"commit": "6fbac39e1d06a66ce3a8724b9366cb878b793f99",
|
||||||
"adapter": "claude-skill",
|
"adapter": "claude-skill",
|
||||||
"sourcePath": "skills/ppt-master",
|
"sourcePath": "skills/ppt-master",
|
||||||
"syncedAt": "2026-10-28T01:57:49Z"
|
"syncedAt": "2026-10-28T15:59:59Z"
|
||||||
}
|
}
|
||||||
@@ -85,6 +85,8 @@ Sparse sidecar (unlisted slides inherit resolved defaults):
|
|||||||
| `--animation none` | Disables all per-element animation |
|
| `--animation none` | Disables all per-element animation |
|
||||||
| `--no-animations` | Also removes transitions |
|
| `--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 <effect>` prints each effect's exact options.
|
The complete field grammar, inheritance, and validation are [`pptx-animations.md`](../scripts/docs/pptx-animations.md) §8; `pptx_animations.py --describe <effect>` prints each effect's exact options.
|
||||||
|
|
||||||
### 2.1 Deterministic Morph Object Pairing
|
### 2.1 Deterministic Morph Object Pairing
|
||||||
|
|||||||
@@ -290,12 +290,13 @@ animation-to-video contract.
|
|||||||
| `morph.pairs.<key>.from` / `.to` | Unique source/destination direct-root group ids receiving the shared PowerPoint name `!!<key>`; Morph by object only |
|
| `morph.pairs.<key>.from` / `.to` | Unique source/destination direct-root group ids receiving the shared PowerPoint name `!!<key>`; Morph by object only |
|
||||||
| `defaults.animation` / `slides.<slide>.animation` | Deck-wide or slide-specific default object-animation behavior |
|
| `defaults.animation` / `slides.<slide>.animation` | Deck-wide or slide-specific default object-animation behavior |
|
||||||
| `animation.effect` | Default object effect: one canonical key, `auto`, `mixed`, `random`, or `none` |
|
| `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.<id>.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.<id>.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.<id>.effect` | Backward-compatible single-row form; old short names are read-only compatibility inputs |
|
| `groups.<id>.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` |
|
| `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 |
|
| `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 |
|
| `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 <effect>`; `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 |
|
| `effect_options` | Effect-specific parameters (`direction`, `amount`, `color`, `font_name`, `relative`, `size`) limited to what the selected effect supports (`pptx_animations.py --describe <effect>`; `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` |
|
| `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` |
|
| `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 |
|
| `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.
|
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.
|
||||||
@@ -1020,11 +1020,10 @@ def _resolve_animation_groups(
|
|||||||
timing_options,
|
timing_options,
|
||||||
label=f'canonical animation for "{effect_path}"',
|
label=f'canonical animation for "{effect_path}"',
|
||||||
)
|
)
|
||||||
default_delay = (
|
if effect_trigger == "after-previous" and sequence_index > 0:
|
||||||
settings.stagger_ms / 1000
|
default_delay = settings.stagger_ms / 1000
|
||||||
if effect_trigger == "after-previous" and sequence_index > 0
|
else:
|
||||||
else 0
|
default_delay = 0
|
||||||
)
|
|
||||||
original_delay_ms = animation_seconds_to_milliseconds(
|
original_delay_ms = animation_seconds_to_milliseconds(
|
||||||
effect_cfg.get("delay", default_delay),
|
effect_cfg.get("delay", default_delay),
|
||||||
f'canonical animation delay for "{effect_path}"',
|
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'
|
f'Derived animation slide "{slide_name}" groups must be an object'
|
||||||
)
|
)
|
||||||
|
|
||||||
# Start modes are row-relative; slide completion spans overlapping rows.
|
# With-previous delays share a beat; after-previous waits for all rows.
|
||||||
previous_start_ms = 0
|
beat_start_ms = 0
|
||||||
previous_row_end_ms = 0
|
|
||||||
timeline_end_ms = 0
|
timeline_end_ms = 0
|
||||||
has_previous_row = False
|
|
||||||
referenced_cues: set[int] = set()
|
referenced_cues: set[int] = set()
|
||||||
seen_groups: set[str] = set()
|
seen_groups: set[str] = set()
|
||||||
for state in states:
|
for state in states:
|
||||||
@@ -1260,12 +1257,9 @@ def rebuild_animations(
|
|||||||
if first_group_effect
|
if first_group_effect
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
if not has_previous_row:
|
if state.trigger == "after-previous":
|
||||||
sequence_base_ms = 0
|
beat_start_ms = timeline_end_ms
|
||||||
elif state.trigger == "with-previous":
|
sequence_base_ms = beat_start_ms
|
||||||
sequence_base_ms = previous_start_ms
|
|
||||||
else:
|
|
||||||
sequence_base_ms = previous_row_end_ms
|
|
||||||
if cue_number is None:
|
if cue_number is None:
|
||||||
if first_group_effect:
|
if first_group_effect:
|
||||||
fallback_count += 1
|
fallback_count += 1
|
||||||
@@ -1310,10 +1304,7 @@ def rebuild_animations(
|
|||||||
effect_value["order"] = state.order
|
effect_value["order"] = state.order
|
||||||
effect_value["delay"] = _seconds_from_ms(delay_ms)
|
effect_value["delay"] = _seconds_from_ms(delay_ms)
|
||||||
effect_value["trigger"] = state.trigger
|
effect_value["trigger"] = state.trigger
|
||||||
previous_start_ms = actual_start_ms
|
timeline_end_ms = max(timeline_end_ms, actual_start_ms + state.duration_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
|
|
||||||
|
|
||||||
ignored_cue_count += len(cues) - len(referenced_cues)
|
ignored_cue_count += len(cues) - len(referenced_cues)
|
||||||
|
|
||||||
|
|||||||
@@ -1761,31 +1761,23 @@ def _build_animation_row_xml(
|
|||||||
def _main_target_offsets(targets: Sequence[AnimationTarget]) -> list[int]:
|
def _main_target_offsets(targets: Sequence[AnimationTarget]) -> list[int]:
|
||||||
"""Return each regular row's start offset within its click group."""
|
"""Return each regular row's start offset within its click group."""
|
||||||
offsets: list[int] = []
|
offsets: list[int] = []
|
||||||
previous_start_ms = 0
|
beat_start_ms = 0
|
||||||
previous_duration_ms = 0
|
timeline_end_ms = 0
|
||||||
has_previous = False
|
|
||||||
for target in targets:
|
for target in targets:
|
||||||
if target.trigger == 'on-click':
|
if target.trigger == 'on-click':
|
||||||
start_ms = target.delay_ms
|
beat_start_ms = 0
|
||||||
elif target.trigger == 'with-previous':
|
timeline_end_ms = 0
|
||||||
start_ms = (
|
elif target.trigger == 'after-previous':
|
||||||
previous_start_ms if has_previous else 0
|
beat_start_ms = timeline_end_ms
|
||||||
) + target.delay_ms
|
# With-previous rows share the beat's trigger, before any row delay.
|
||||||
else:
|
start_ms = beat_start_ms + target.delay_ms
|
||||||
start_ms = (
|
|
||||||
previous_start_ms + previous_duration_ms
|
|
||||||
if has_previous
|
|
||||||
else 0
|
|
||||||
) + target.delay_ms
|
|
||||||
if start_ms > MAX_OOXML_MILLISECONDS:
|
if start_ms > MAX_OOXML_MILLISECONDS:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
'animation sequence offset exceeds the OOXML millisecond '
|
'animation sequence offset exceeds the OOXML millisecond '
|
||||||
f'limit at target {len(offsets) + 1}: {start_ms}'
|
f'limit at target {len(offsets) + 1}: {start_ms}'
|
||||||
)
|
)
|
||||||
offsets.append(start_ms)
|
offsets.append(start_ms)
|
||||||
previous_start_ms = start_ms
|
timeline_end_ms = max(timeline_end_ms, start_ms + target.playback_duration_ms)
|
||||||
previous_duration_ms = target.playback_duration_ms
|
|
||||||
has_previous = True
|
|
||||||
return offsets
|
return offsets
|
||||||
|
|
||||||
|
|
||||||
@@ -1793,7 +1785,7 @@ def _build_mixed_main_steps(
|
|||||||
targets: Sequence[AnimationTarget],
|
targets: Sequence[AnimationTarget],
|
||||||
next_id: int,
|
next_id: int,
|
||||||
) -> tuple[str, 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)
|
offsets = _main_target_offsets(targets)
|
||||||
groups: list[list[tuple[AnimationTarget, int]]] = []
|
groups: list[list[tuple[AnimationTarget, int]]] = []
|
||||||
for target, offset_ms in zip(targets, offsets):
|
for target, offset_ms in zip(targets, offsets):
|
||||||
@@ -1813,29 +1805,42 @@ def _build_mixed_main_steps(
|
|||||||
'<p:cond delay="indefinite"/>'
|
'<p:cond delay="indefinite"/>'
|
||||||
'<p:cond evt="onBegin" delay="0"><p:tn val="2"/></p:cond>'
|
'<p:cond evt="onBegin" delay="0"><p:tn val="2"/></p:cond>'
|
||||||
)
|
)
|
||||||
rendered_rows: list[str] = []
|
beats: list[list[tuple[AnimationTarget, int]]] = []
|
||||||
for target, offset_ms in group:
|
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
|
wrapper_id = next_id
|
||||||
row_id = next_id + 1
|
next_id += 1
|
||||||
row_xml, next_id = _build_animation_row_xml(
|
leader, offset_ms = beat[0]
|
||||||
target,
|
wrapper_offset_ms = offset_ms - leader.delay_ms
|
||||||
target.trigger,
|
rendered_rows: list[str] = []
|
||||||
row_id,
|
for target, _ in beat:
|
||||||
next_id + 2,
|
row_xml, next_id = _build_animation_row_xml(
|
||||||
)
|
target,
|
||||||
wrapper_offset_ms = offset_ms - target.delay_ms
|
target.trigger,
|
||||||
rendered_rows.append(f'''<p:par>
|
next_id,
|
||||||
|
next_id + 1,
|
||||||
|
)
|
||||||
|
rendered_rows.append(f'<p:par>{row_xml}</p:par>')
|
||||||
|
rows_xml = '\n '.join(rendered_rows)
|
||||||
|
rendered_beats.append(f'''<p:par>
|
||||||
<p:cTn id="{wrapper_id}" fill="hold">
|
<p:cTn id="{wrapper_id}" fill="hold">
|
||||||
<p:stCondLst><p:cond delay="{wrapper_offset_ms}"/></p:stCondLst>
|
<p:stCondLst><p:cond delay="{wrapper_offset_ms}"/></p:stCondLst>
|
||||||
<p:childTnLst><p:par>{row_xml}</p:par></p:childTnLst>
|
<p:childTnLst>
|
||||||
|
{rows_xml}
|
||||||
|
</p:childTnLst>
|
||||||
</p:cTn>
|
</p:cTn>
|
||||||
</p:par>''')
|
</p:par>''')
|
||||||
rows_xml = '\n '.join(rendered_rows)
|
beats_xml = '\n '.join(rendered_beats)
|
||||||
rendered_groups.append(f'''<p:par>
|
rendered_groups.append(f'''<p:par>
|
||||||
<p:cTn id="{group_id}" fill="hold">
|
<p:cTn id="{group_id}" fill="hold">
|
||||||
<p:stCondLst>{group_conditions}</p:stCondLst>
|
<p:stCondLst>{group_conditions}</p:stCondLst>
|
||||||
<p:childTnLst>
|
<p:childTnLst>
|
||||||
{rows_xml}
|
{beats_xml}
|
||||||
</p:childTnLst>
|
</p:childTnLst>
|
||||||
</p:cTn>
|
</p:cTn>
|
||||||
</p:par>''')
|
</p:par>''')
|
||||||
@@ -1852,8 +1857,8 @@ def create_sequence_timing_xml(
|
|||||||
Args:
|
Args:
|
||||||
targets: list of (shape_id, delay_ms, animation_name) or
|
targets: list of (shape_id, delay_ms, animation_name) or
|
||||||
(shape_id, delay_ms, animation_name, duration_seconds) tuples, in
|
(shape_id, delay_ms, animation_name, duration_seconds) tuples, in
|
||||||
the order they should play. ``delay_ms`` is the gap before
|
animation-pane order. ``delay_ms`` is measured from the beat
|
||||||
this element starts relative to its Start mode. Mapping targets
|
start, before its leader's own delay. Mapping targets
|
||||||
may set an independent ``trigger``; otherwise they inherit the
|
may set an independent ``trigger``; otherwise they inherit the
|
||||||
function-level ``trigger``. A target with ``trigger_shape_id``
|
function-level ``trigger``. A target with ``trigger_shape_id``
|
||||||
must use ``on-click`` and runs in an interactive sequence.
|
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.
|
native presets retain their PowerPoint-authored duration.
|
||||||
trigger: PowerPoint-standard Start mode for each element.
|
trigger: PowerPoint-standard Start mode for each element.
|
||||||
``'after-previous'`` — first element fires on slide entry,
|
``'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).
|
(default).
|
||||||
``'on-click'`` — one presenter click per element.
|
``'on-click'`` — one presenter click per element.
|
||||||
``'with-previous'`` — all elements start together on slide
|
``'with-previous'`` — all elements share the slide-entry beat,
|
||||||
entry.
|
each with its own ``delay_ms``.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A ``<p:timing>`` element string. Returns an empty string when
|
A ``<p:timing>`` element string. Returns an empty string when
|
||||||
@@ -1897,139 +1903,7 @@ def create_sequence_timing_xml(
|
|||||||
if target.trigger_shape_id is not None
|
if target.trigger_shape_id is not None
|
||||||
]
|
]
|
||||||
|
|
||||||
main_triggers = {target.trigger for target in main_targets}
|
all_steps, next_id = _build_mixed_main_steps(main_targets, next_id)
|
||||||
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'''<p:par>
|
|
||||||
<p:cTn id="{wrapper_id}" fill="hold">
|
|
||||||
<p:stCondLst><p:cond delay="indefinite"/></p:stCondLst>
|
|
||||||
<p:childTnLst>
|
|
||||||
<p:par>
|
|
||||||
<p:cTn id="{inner_id}" fill="hold">
|
|
||||||
<p:stCondLst><p:cond delay="0"/></p:stCondLst>
|
|
||||||
<p:childTnLst>
|
|
||||||
<p:par>
|
|
||||||
{row_xml}
|
|
||||||
</p:par>
|
|
||||||
</p:childTnLst>
|
|
||||||
</p:cTn>
|
|
||||||
</p:par>
|
|
||||||
</p:childTnLst>
|
|
||||||
</p:cTn>
|
|
||||||
</p:par>''')
|
|
||||||
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'''<p:par>
|
|
||||||
{row_xml}
|
|
||||||
</p:par>''')
|
|
||||||
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'''<p:par>
|
|
||||||
<p:cTn id="{wrapper_id}" fill="hold">
|
|
||||||
<p:stCondLst><p:cond delay="{elapsed_ms}"/></p:stCondLst>
|
|
||||||
<p:childTnLst>
|
|
||||||
<p:par>
|
|
||||||
{row_xml}
|
|
||||||
</p:par>
|
|
||||||
</p:childTnLst>
|
|
||||||
</p:cTn>
|
|
||||||
</p:par>''')
|
|
||||||
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'''<p:par>
|
|
||||||
<p:cTn id="{with_wrapper_id}" fill="hold">
|
|
||||||
<p:stCondLst><p:cond delay="0"/></p:stCondLst>
|
|
||||||
<p:childTnLst>
|
|
||||||
{inner_xml}
|
|
||||||
</p:childTnLst>
|
|
||||||
</p:cTn>
|
|
||||||
</p:par>'''
|
|
||||||
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 = (
|
|
||||||
'<p:cond delay="indefinite"/>'
|
|
||||||
'<p:cond evt="onBegin" delay="0"><p:tn val="2"/></p:cond>'
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
outer_start_conditions = '<p:cond delay="0"/>'
|
|
||||||
all_steps = f'''<p:par>
|
|
||||||
<p:cTn id="{outer_id}" fill="hold">
|
|
||||||
<p:stCondLst>{outer_start_conditions}</p:stCondLst>
|
|
||||||
<p:childTnLst>
|
|
||||||
{inner_xml}
|
|
||||||
</p:childTnLst>
|
|
||||||
</p:cTn>
|
|
||||||
</p:par>'''
|
|
||||||
|
|
||||||
if main_targets:
|
if main_targets:
|
||||||
main_sequence_xml = f'''<p:seq concurrent="1" nextAc="seek">
|
main_sequence_xml = f'''<p:seq concurrent="1" nextAc="seek">
|
||||||
|
|||||||
+14
-18
@@ -240,28 +240,26 @@ def _resolve_group_id(
|
|||||||
def _relative_delays(rows: tuple[AnimationRowSummary, ...]) -> tuple[int, ...]:
|
def _relative_delays(rows: tuple[AnimationRowSummary, ...]) -> tuple[int, ...]:
|
||||||
"""Invert the native writer's absolute main-sequence offsets."""
|
"""Invert the native writer's absolute main-sequence offsets."""
|
||||||
delays: list[int] = []
|
delays: list[int] = []
|
||||||
previous_start_ms = 0
|
beat_start_ms = 0
|
||||||
previous_duration_ms = 0
|
timeline_end_ms = 0
|
||||||
has_previous = False
|
|
||||||
for index, row in enumerate(rows, 1):
|
for index, row in enumerate(rows, 1):
|
||||||
if row.playback_duration_ms is None:
|
if row.playback_duration_ms is None:
|
||||||
raise AnimationImportError(
|
raise AnimationImportError(
|
||||||
f"animation row {index} has no exact playback duration"
|
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
|
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:
|
else:
|
||||||
raise AnimationImportError(
|
if row.trigger == "on-click":
|
||||||
f"animation row {index} has unsupported trigger {row.trigger!r}"
|
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
|
delay_ms = row.offset_ms - base_ms
|
||||||
if delay_ms < 0:
|
if delay_ms < 0:
|
||||||
raise AnimationImportError(
|
raise AnimationImportError(
|
||||||
@@ -269,9 +267,7 @@ def _relative_delays(rows: tuple[AnimationRowSummary, ...]) -> tuple[int, ...]:
|
|||||||
)
|
)
|
||||||
delays.append(delay_ms)
|
delays.append(delay_ms)
|
||||||
if row.trigger_shape_id is None:
|
if row.trigger_shape_id is None:
|
||||||
previous_start_ms = row.offset_ms
|
timeline_end_ms = max(timeline_end_ms, row.offset_ms + row.playback_duration_ms)
|
||||||
previous_duration_ms = row.playback_duration_ms
|
|
||||||
has_previous = True
|
|
||||||
return tuple(delays)
|
return tuple(delays)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -1030,12 +1030,12 @@
|
|||||||
"max_tokens": 3250
|
"max_tokens": 3250
|
||||||
},
|
},
|
||||||
"stage.shared.conversion-reference": {
|
"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",
|
"scope": "incremental",
|
||||||
"files": [
|
"files": [
|
||||||
"skills/ppt-master/scripts/docs/conversion.md"
|
"skills/ppt-master/scripts/docs/conversion.md"
|
||||||
],
|
],
|
||||||
"max_tokens": 8750
|
"max_tokens": 11000
|
||||||
},
|
},
|
||||||
"stage.shared.troubleshooting": {
|
"stage.shared.troubleshooting": {
|
||||||
"description": "Conditional troubleshooting reference for generation failures.",
|
"description": "Conditional troubleshooting reference for generation failures.",
|
||||||
|
|||||||
+6
-9
@@ -5997,15 +5997,12 @@ def _build_sequence_targets(
|
|||||||
'trigger "on-click" when trigger is explicit'
|
'trigger "on-click" when trigger is explicit'
|
||||||
)
|
)
|
||||||
resolved_trigger = 'on-click'
|
resolved_trigger = 'on-click'
|
||||||
default_delay = (
|
if trigger_shape is not None:
|
||||||
stagger
|
default_delay = 0
|
||||||
if (
|
elif resolved_trigger == 'after-previous' and main_sequence_count > 0:
|
||||||
trigger_shape is None
|
default_delay = stagger
|
||||||
and resolved_trigger == 'after-previous'
|
else:
|
||||||
and main_sequence_count > 0
|
default_delay = 0
|
||||||
)
|
|
||||||
else 0
|
|
||||||
)
|
|
||||||
delay_seconds = validate_seconds(
|
delay_seconds = validate_seconds(
|
||||||
group_cfg.get('delay', default_delay),
|
group_cfg.get('delay', default_delay),
|
||||||
f'animations.json {effect_path}.delay',
|
f'animations.json {effect_path}.delay',
|
||||||
|
|||||||
+188
@@ -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 = ('<svg xmlns="http://www.w3.org/2000/svg"><g id="content" '
|
||||||
|
'data-pptx-shape-scope="slide" data-pptx-shape-id="2"/></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'<p:sld xmlns:p="{PML_NS}"><p:cSld><p:spTree>'
|
||||||
|
'<p:sp><p:nvSpPr><p:cNvPr id="2" name="content"/></p:nvSpPr></p:sp>'
|
||||||
|
'<p:sp><p:nvSpPr><p:cNvPr id="3" name="button"/></p:nvSpPr></p:sp>'
|
||||||
|
f'</p:spTree></p:cSld>{timing}</p:sld>')
|
||||||
|
|
||||||
|
|
||||||
|
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'''<p:par><p:cTn id="{wrapper_id}" fill="hold">
|
||||||
|
<p:stCondLst><p:cond delay="{offset_ms}"/></p:stCondLst><p:childTnLst><p:par>
|
||||||
|
<p:cTn id="{wrapper_id + 1}" presetID="10" presetClass="entr"
|
||||||
|
presetSubtype="0" fill="hold" grpId="0" nodeType="{node_type}">
|
||||||
|
<p:stCondLst><p:cond delay="{delay_ms}"/></p:stCondLst><p:childTnLst>
|
||||||
|
<p:set><p:cBhvr><p:cTn id="{wrapper_id + 2}" dur="1" fill="hold">
|
||||||
|
<p:stCondLst><p:cond delay="0"/></p:stCondLst></p:cTn>
|
||||||
|
<p:tgtEl><p:spTgt spid="2"/></p:tgtEl>
|
||||||
|
<p:attrNameLst><p:attrName>style.visibility</p:attrName></p:attrNameLst>
|
||||||
|
</p:cBhvr><p:to><p:strVal val="visible"/></p:to></p:set>
|
||||||
|
<p:animEffect transition="in" filter="fade"><p:cBhvr>
|
||||||
|
<p:cTn id="{wrapper_id + 3}" dur="500"/>
|
||||||
|
<p:tgtEl><p:spTgt spid="2"/></p:tgtEl>
|
||||||
|
</p:cBhvr></p:animEffect>
|
||||||
|
</p:childTnLst>
|
||||||
|
</p:cTn>
|
||||||
|
</p:par></p:childTnLst>
|
||||||
|
</p:cTn></p:par>''')
|
||||||
|
return _slide(f'''<p:timing><p:tnLst><p:par>
|
||||||
|
<p:cTn id="1" dur="indefinite" restart="never" nodeType="tmRoot"><p:childTnLst>
|
||||||
|
<p:seq concurrent="1" nextAc="seek">
|
||||||
|
<p:cTn id="2" dur="indefinite" nodeType="mainSeq"><p:childTnLst><p:par>
|
||||||
|
<p:cTn id="3" fill="hold"><p:stCondLst><p:cond delay="indefinite"/>
|
||||||
|
<p:cond evt="onBegin" delay="0"><p:tn val="2"/></p:cond>
|
||||||
|
</p:stCondLst><p:childTnLst>{''.join(wrappers)}</p:childTnLst></p:cTn>
|
||||||
|
</p:par></p:childTnLst></p:cTn>
|
||||||
|
<p:prevCondLst><p:cond evt="onPrev" delay="0"><p:tgtEl><p:sldTgt/></p:tgtEl></p:cond></p:prevCondLst>
|
||||||
|
<p:nextCondLst><p:cond evt="onNext" delay="0"><p:tgtEl><p:sldTgt/></p:tgtEl></p:cond></p:nextCondLst>
|
||||||
|
</p:seq>
|
||||||
|
</p:childTnLst></p:cTn>
|
||||||
|
</p:par></p:tnLst></p:timing>''')
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
+272
@@ -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'<p:sp><p:nvSpPr><p:cNvPr id="{shape_id}" name="item"/>'
|
||||||
|
'</p:nvSpPr></p:sp>' for shape_id in sorted(shape_ids))
|
||||||
|
return (f'<p:sld xmlns:p="{PML_NS}"><p:cSld><p:spTree>{shapes}'
|
||||||
|
f'</p:spTree></p:cSld>{timing}</p:sld>')
|
||||||
|
|
||||||
|
|
||||||
|
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 = ('<svg xmlns="http://www.w3.org/2000/svg"><g id="content" '
|
||||||
|
'data-pptx-shape-scope="slide" data-pptx-shape-id="2"/></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'<g id="{group_id}" data-pptx-shape-scope="slide" '
|
||||||
|
f'data-pptx-shape-id="{index + 2}"/>'
|
||||||
|
for index, group_id in enumerate(groups)
|
||||||
|
)
|
||||||
|
imported = read_animation_config(
|
||||||
|
slide, f'<svg xmlns="http://www.w3.org/2000/svg">{svg_groups}</svg>',
|
||||||
|
)
|
||||||
|
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)
|
||||||
Reference in new issue
Block a user