first commit
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
---
|
||||
name: dingtalk-minutes
|
||||
description: 钉钉 AI 听记。Use when 查询或修改听记摘要、完整逐字稿、关键词、行动项、录音、上传、思维导图、发言人洞察或分享权限。写文档走 dingtalk-doc;建待办走 dingtalk-todo;日程走 dingtalk-calendar。命令前缀:dws minutes。
|
||||
metadata:
|
||||
cli_version: ">=0.2.14"
|
||||
category: product
|
||||
requires:
|
||||
bins:
|
||||
- dws
|
||||
---
|
||||
|
||||
# 钉钉 AI 听记 Skill
|
||||
|
||||
<!-- DWS_RUNTIME_CONTRACT_START -->
|
||||
## 最小 DWS 执行契约
|
||||
|
||||
- 只通过 `dws` CLI 操作钉钉;结构化读取使用 `--format json`,按真实返回判断结果。
|
||||
- 已知命令直接执行。只有 leaf 参数或安全语义不确定时读取精确 Schema,只有 Cobra flag 不确定时读取精确 leaf Help;不要加载产品级 Catalog 代替选路。
|
||||
- 不猜命令、flag、字段、ID、账号或时间。后续 ID 必须来自真实返回;零命中、多候选或类型不明时停止并消歧。
|
||||
- 解析目标、读取上下文和最终执行必须使用同一 profile;不得跨组织复用 userId、openDingTalkId 或 openConversationId。多账号组织只使用明确的 `isOrgCurrent=true` 默认账号;没有默认账号时要求用户指定,禁止选择第一项、最近登录或最近使用账号。
|
||||
- 不输出或记录 token、refresh token、appSecret、webhook token 等凭据;宿主已注入认证时不要索要凭据。
|
||||
- 写操作必须符合用户明确意图。是否需要确认以最终 Runtime gate 和 Schema 为准;需要确认时先说明对象、动作与影响,再追加 `--yes`。
|
||||
- 写后按任务结果契约验证;不能仅凭退出码宣称成功。部分结果、未知投递状态和失败项必须如实保留。
|
||||
- 时间戳面向用户展示时转换为带时区的可读时间;默认使用当前会话时区,必要时同时保留原值。
|
||||
- 遇到认证、权限、profile、confirmation 或未知错误时,只加载 `dingtalk-shared` 中对应 reference;不要连续猜测替代命令。
|
||||
<!-- DWS_RUNTIME_CONTRACT_END -->
|
||||
|
||||
<!-- VISIBLE_SHORTCUTS_START -->
|
||||
## Shortcut 发现(按需)
|
||||
|
||||
`minutes` 当前有 29 条公开 shortcut,完整清单保留在 Runtime Catalog 与 Schema,不在高频产品根 Skill 中重复展开。已知意图直接使用下方的优先路由、意图表或任务 reference;命令已选中时直接执行,只在参数/安全语义不确定时读取 leaf Schema,在当前 Cobra flags 不确定时读取 leaf Help。
|
||||
|
||||
仅当现有路由和 reference 都无法定位低频能力时,才执行 `dws shortcut list --service minutes --format json` 做最后回退;不要为已知高频意图加载完整 Shortcut Catalog 或产品级 Schema。
|
||||
<!-- VISIBLE_SHORTCUTS_END -->
|
||||
|
||||
## Golden Route
|
||||
|
||||
以下是当前 Minutes Case 支持的核心路径,用于减少 Agent 选路分叉;它不等同于生产使用频率统计。已有 `taskUuid` 直接使用;完整 `shanji.dingtalk.com` URL 先提取其中的真实 ID。只有标题或时间线索时先搜索,零命中停止,多候选或候选差异较大时让用户消歧,不默认取第一条。
|
||||
|
||||
| 用户意图 | 唯一推荐入口 | 关键边界 |
|
||||
|---|---|---|
|
||||
| 按标题或时间找听记 | `dws minutes +search --scope all --query "<关键词>" --page-all` | `scope` 可选 `mine/shared/all`;至少提供 query/start/end 之一。`all --page-all` 分别追完 mine/shared 后去重,不把单个 noLimit 端点当完整全集 |
|
||||
| 浏览我创建、共享给我或全部可访问听记 | `+list-mine` / `+list-shared` / `+list-all` | 默认是可续拉预览;要声称完整必须加 `--page-all` 并检查 `complete=true`。用户只说“我的听记”不等于明确 `mine`,范围不清时用 `all` |
|
||||
| 看我最新创建的一条 | `dws minutes +latest [--keyword <关键词>]` | 只在用户明确说“最新”时用;不能用它替代具名目标搜索,也不能在录音 start 后拿 latest 猜新录音 ID |
|
||||
| 读取基础信息、摘要或关键词 | `dws minutes +detail --id <taskUuid> --artifacts basic,summary,keywords` | 已有 taskUuid 且只读取现有产物时直接使用,不要进入上传 workflow;任一产物失败都按 partial/非零处理,不把缺失项说成空内容 |
|
||||
| 读取逐字稿 | `dws minutes +transcript --id <taskUuid> [--direction 1] [--single-page]` | 已有 taskUuid 且只读取逐字稿时直接使用,不要借 `+upload-and-analyze --resume-id` 代读。默认正序并追完分页;倒序必须传 `--direction 1`,用户明确只要第一页时传 `--single-page`,不得随后自动续页。交付前检查 `data.direction/data.complete/data.pages` 与 `meta.pagination` |
|
||||
| 读取行动项 | `dws minutes +action-items --id <taskUuid>` | 只有受支持字段明确返回空数组才能说“没有待办”;`unsupported_shape`、字段解析失败或工具失败都不是空结果。需要创建钉钉待办时再切 `dingtalk-todo` |
|
||||
| 把摘要、关键词、完整逐字稿和行动项归档到本地 | `dws minutes +export-pack --id <taskUuid> --output <新相对目录>` | 要带媒体时加 `--include-media`;必须由 `published/path/manifest/files` 证明落盘,只有建目录、计划或文件名不能称已生成 |
|
||||
| 修改或预览标题 | `dws minutes +update --id <taskUuid> --title "<新标题>"` | 真实修改按 Runtime confirmation 执行并读回;用户只要预览时先读 basic,再加 `--dry-run`,展示“当前值 → 目标值”后停止,不追加 `--yes` |
|
||||
| 覆盖纪要正文 | `dws minutes +summary --id <taskUuid> --content @<相对文件>` | `content` 是完整目标正文,不是局部 patch;按 Runtime confirmation 执行,并保护图片引用、读回全文 |
|
||||
| 上传音视频生成听记 | `dws minutes +upload --file <相对路径>` | 真实执行会上传文件并创建远端听记,必须按 Runtime confirmation;用户明确要闪记卡片时改用 `+upload-and-notify`,需要上传后等待分析产物时用 `+upload-and-analyze` |
|
||||
| 真实开始、暂停、继续或停止录音 | `+record-start` / `+record-pause` / `+record-resume` / `+record-stop` | 这组入口会真实执行。start 返回 `accepted=true, bound=false` 或 `controlReady=false` 时,报告“已受理但未绑定”并停止:不得重试 start,也不得用 `+latest`、列表第一条或时间最近项猜 ID。结束并等待产物用 `+record-wrap-up` |
|
||||
| 只预览录音请求,不实际执行 | `dws minutes record start --dry-run --format json` | 使用对应的原子 `minutes record start|pause|resume|stop` leaf;start 的 `--session-id` 可选,pause/resume/stop 必须传真实 `--id`。不要把被拒绝的 Shortcut dry-run 描述成预览成功 |
|
||||
| 生成或继续思维导图 | `dws minutes +mindmap --id <taskUuid>` | 创建后有界轮询;超时或未知状态保留恢复信息,用 `--resume` 继续,不重复创建 |
|
||||
| 生成或继续发言人洞察 | `dws minutes +speaker-insights --id <taskUuid>` | 有界轮询;保留 `taskId`,恢复时用 `--resume [--task-id <ID>]`,不重复创建 |
|
||||
| 当前用户申请查看/下载/编辑权限 | `dws minutes +apply-permission --id <taskUuid> --permission view|download|edit` | 这是“我申请访问”,不是所有者给别人授权;按 Runtime confirmation 执行 |
|
||||
| 所有者给成员授权或撤权 | `dws minutes +share ...` / `dws minutes +unshare ...` | 先用通讯录把姓名解析为同组织稳定 UID;撤权是破坏性操作。批量结果必须保留逐成员 ledger 和失败项 |
|
||||
|
||||
### 搜索与列表执行胶囊
|
||||
|
||||
用户要求“全部、所有、完整、汇总整个范围”时,首轮直接使用 `--page-all`:
|
||||
|
||||
```text
|
||||
dws minutes +search --query "<关键词>" --scope all --page-all --format json
|
||||
dws minutes +search --start "<RFC3339>" --end "<RFC3339>" --scope mine --page-all --format json
|
||||
dws minutes +list-mine --page-all --format json
|
||||
dws minutes +list-shared --page-all --format json
|
||||
dws minutes +list-all --page-all --format json
|
||||
```
|
||||
|
||||
只有用户明确要第一页、预览或样本时才省略 `--page-all`,并如实保留 `data.complete=false` 与 `meta.pagination.next_token`。有时间窗必须使用 `+search`,因为 `+list-*` 不接受 `--start/--end`。
|
||||
|
||||
## 目标与完整性
|
||||
|
||||
- 目标锁定优先级:用户给出的 `taskUuid`/URL > 精确标题 > 标题包含或语义相关候选。相似候选可展示,但候选差异明显或多个候选都合理时必须停下来消歧;分页未完成本身不是目标歧义。
|
||||
- 用户说“先确认/核对目标”默认要求用真实 basic 字段完成证据核对,不自动变成等待用户回复的会话门禁。目标唯一、分页完整且后续均为只读时,展示标题、时间、归属等核对证据后在同一轮继续;只有用户明确要求“等我确认后再继续”或仍有多个合理候选时才暂停。
|
||||
- 纯能力、规则或错误说明且没有唯一真实目标时直接解释,不猜对象;用户明确要求核对且目标唯一时必须真实读取,不能只展示命令。
|
||||
- `mine` 仅表示我创建的,`shared` 仅表示共享给我的;`all` 表示 accessible 聚合目标。不得声称后端单个 noLimit 端点天然等于 `mine + shared`。
|
||||
- 列表或逐字稿只有 `data.complete=true` 才能称为“全部/完整”。全量请求遇到 `meta.pagination.next_token` 时继续;只有 token 缺失、cursor 停滞/循环、达到 `page-limit` 或后页失败时才停止,并保留失败信封或不完整证据。
|
||||
- 用户要求核对、汇总“这些/每条/全部”命中项时,必须覆盖完整命中集合;可用 `dws minutes +detail --ids <uuid1,uuid2> --artifacts basic --format json` 批量核对,并逐项保留失败。只检查第一条不能代表全体;响应没有逐条归属或组织字段时如实说明不可得,不能用当前 profile 的组织名代替每条听记的归属。
|
||||
- 多听记、多来源或跨产品汇总按每个 `taskUuid`/来源 ID 保留 `requested/resolved/missing/artifacts/status`;缺输入或必需产物时整体按 partial,不用已找到项代表全部。
|
||||
- 内容归纳必须来自每条真实 `summary/transcript/keywords`;只有 `title/basic` 时只列元数据,不生成摘要、关键词或分类。
|
||||
- `partial_success`、异步 `pending`、超时和未知写入结果不是成功。按结果中的恢复句柄继续,不能重放已成功步骤。
|
||||
|
||||
## 安全边界
|
||||
|
||||
- 是否确认以 leaf Schema 与 Runtime gate 为准,不根据“看起来像写操作”自行推断。推荐 Golden Route 中的 `+update`、`+summary`、`+record-*`、`+share/+unshare/+apply-permission`、`+speaker-replace`、`+replace-batch` 等当前要求确认。
|
||||
- 为兼容既有公开 Contract,对应的底层原子命令保留历史 `not_required`;它们只用于 Shortcut 无法表达的明确底层控制,不得为了绕过 Golden Route 的确认门禁而降级调用。
|
||||
- `+upload` 与 `+upload-and-analyze` 即使不发送消息,仍会上传本地媒体并创建远端听记,真实执行必须按 Runtime confirmation;`--dry-run` 仍可在零远端调用下预览。上传并发送闪记卡片、精确同步并删除热词、撤权等副作用更大的入口继续单独处理。
|
||||
- `+mindmap`、`+speaker-insights` 和 `+prepare-asr` 当前要求确认;`--resume` 仍沿用所在 Shortcut 的命令级门禁。旧 `+upload --enable-message-card` 与 `+upload-and-analyze --enable-message-card` 继续作为可执行兼容入口,并遵循各自 Shortcut 的确认门禁;新调用仍推荐 `+upload-and-notify`。原子 `minutes upload create --enable-message-card` 与旧 `--sync` 只保留为公开迁移提示,不得当作可执行 Golden Route。
|
||||
- `--dry-run` 必须返回明确的 dry-run/request 证据且不调用远端;录音预览按上方原子入口执行。任何入口若拒绝 dry-run,必须报告“不支持预览”,不得把拦截或普通执行称为预演成功。
|
||||
- 用户明确要求“仅预览/不实际写入”时,任务在真实现状读取、dry-run 计划和差异交付后结束;不得继续请求写入确认,也不得为了验证预览而执行真实写入后再还原。
|
||||
- 分享/撤权使用稳定成员 UID,不能把姓名、手机号或跨组织 ID 直接当 UID;同一目标解析、读取、写入和验证必须使用同一 profile。
|
||||
|
||||
## 按需加载
|
||||
|
||||
Golden Route 参数足够时直接执行,不预读 Reference。每个 Case 最多先读取一个最精确的文件;参数事实优先读取 compact leaf Schema,不读取产品级全量 Catalog。
|
||||
|
||||
| 触发条件 | Reference |
|
||||
|---|---|
|
||||
| ASR 热词、上传会话恢复、复杂异步轮询、批量权限 workflow | [复杂流程](references/07-minutes.md) |
|
||||
| 发言人替换、批量文本替换、下载媒体、离线导出等低频意图 | [局部意图](references/intent-guide.md) |
|
||||
| 必须落到原子命令,或需要 URL/参数/确认事实 | [原子命令](references/minutes.md) |
|
||||
|
||||
## 错误最短路径
|
||||
|
||||
1. 零命中、多候选或 ID 类型不明:停止并返回候选证据。全量请求分页未完成但有有效 continuation 时继续;只有 token 缺失/停滞/循环、达到页数上限或后页失败时停止并返回 `data.complete`、`meta.pagination` 或失败信封等证据。
|
||||
2. 认证、权限、profile 或 confirmation 错误:按 `dingtalk-shared` 对应错误 Reference 处理;不更换 scope、账号或写命令碰运气。
|
||||
3. 异步超时或部分成功:保留 `taskUuid/taskId/sessionId/checkpoint`,只恢复未完成阶段;未知写入先读回,不能自动重试。
|
||||
|
||||
## 跨产品边界
|
||||
|
||||
- 把听记摘要或逐字稿写成文档 → 读取真实内容后切 `dingtalk-doc`。
|
||||
- 把听记行动项创建为任务 → 读取行动项后切 `dingtalk-todo`,并按其身份解析规则处理执行人。
|
||||
- 把摘要发给同事 → 切 `dingtalk-chat`;`+share` 只管理听记权限,不发送摘要消息。
|
||||
- 创建或修改日程、会议室 → `dingtalk-calendar`;Minutes 只处理听记产物和录音控制。
|
||||
@@ -0,0 +1,149 @@
|
||||
# Minutes 复杂流程
|
||||
|
||||
> 返回入口:[DingTalk Minutes Skill](../SKILL.md) · [Reference 与脚本索引](minutes.md)
|
||||
|
||||
只在根 Skill 已确定属于 ASR、上传恢复、异步生成或批量权限 workflow 时读取本文件。普通搜索、详情、逐字稿、标题或摘要修改直接按根 Skill Golden Route 执行。
|
||||
|
||||
## 1. ASR 热词
|
||||
|
||||
| 用户意图 | 推荐入口 | 是否确认 | 关键语义 |
|
||||
|---|---|---|---|
|
||||
| “录音前加上这些专有词”“补充热词” | `dws minutes +prepare-asr --words "DWS,听记"` | 需要 | 只新增缺失热词,不删除现有项;读回验证 |
|
||||
| “让热词最终只保留这组”“精确同步热词” | `dws minutes +sync-asr --words "DWS,听记"` | 需要,且先说明会删除目标集合外热词 | 新增缺失项并删除多余项;属于 destructive/high |
|
||||
| 只查看当前 ASR 热词 / “识别词配置” | `dws minutes hot-word list --format json` | 不需要 | 原子只读;返回当前账号的识别词配置,不是某个音频的转写结果 |
|
||||
| 删除一个或多个已知热词 | `dws minutes hot-word delete --words "<热词1,热词2>"` | 原子入口历史 `not_required` | 仅作兼容底层入口;推荐需要确认且能读回验证的 `+sync-asr`,不模糊删除 |
|
||||
|
||||
`+prepare-asr --sync` 作为已发布参数保持公开可见,但只返回迁移提示且在任何 MCP 调用前停止。需要删除时必须显式改用 `+sync-asr`,不能把“准备热词”解释成“覆盖整个词表”。两个 Shortcut 的 `--dry-run` 都只输出本地计划,不读取或写入远端;要比较真实差异时先读取词表,再单独执行目标入口。
|
||||
|
||||
用户说“先核对识别词/词表”时,默认指当前 ASR 热词配置,必须实际执行 `hot-word list`,不能只展示命令。如果用户明确要核对某个音频最终识别出的文字,则必须真实上传并等待转写;upload dry-run 做不到这一点。用户同时要求“不实际创建听记”时,以不写入为最高边界,如实说明两项要求不能同时满足,不能通过真实 create 后 cancel 来伪造预览。
|
||||
|
||||
## 2. 上传、通知与恢复
|
||||
|
||||
### 2.1 直接上传
|
||||
|
||||
| 目标 | 推荐入口 | 结果边界 |
|
||||
|---|---|---|
|
||||
| 上传并创建听记,不发送额外消息 | `dws minutes +upload --file <相对路径> [--title <标题>]` | 真实执行需要确认;完成 create、文件 PUT、complete 和详情读回,失败时取消可取消的 session |
|
||||
| 上传并额外发送闪记卡片 | `dws minutes +upload-and-notify --file <相对路径> [--title <标题>]` | 推荐新入口;旧 `+upload --enable-message-card` 仍可执行并遵循 `+upload` 的确认门禁 |
|
||||
| 上传并等待摘要/逐字稿等分析产物 | `dws minutes +upload-and-analyze --file <相对路径> --artifacts summary,transcript` | 真实执行需要确认;有界等待,可加 `--mindmap` / `--speaker-insights`,不要把 pending/timeout 说成完成 |
|
||||
|
||||
用户要求预览上传,并明确要求核对热词配置或比较听记列表是否变化时,执行以下可验证流程;没有这些额外要求时不必增加读操作:
|
||||
|
||||
```text
|
||||
dws minutes hot-word list --format json
|
||||
dws minutes +list-mine --page-all --format json
|
||||
dws minutes +upload --file <相对路径> --title "<标题>" --input-language zh --template-id <templateId> --dry-run --format json
|
||||
dws minutes +list-mine --page-all --format json
|
||||
```
|
||||
|
||||
- 热词查询、上传计划和前后列表是三份不同证据;命令示例不能代替真实查询结果。
|
||||
- dry-run 只证明请求计划与 `executed=false`,不会创建 session、听记或 ASR 结果。前后列表按 `taskUuid` 集合比较;不能只比较数量或第一页。
|
||||
- 没有真实文件、文件字节数或 sessionId 时,停止在相应前置门禁;不得调用 create/complete。用户要求确认“没有生成新听记”时,仍需用真实列表证据回答,不能仅由“我没有调用上传”推断列表事实。
|
||||
|
||||
如果已有 `taskUuid`,并且只需要读取当前已经生成的摘要与逐字稿,直接使用只读入口:
|
||||
|
||||
```text
|
||||
dws minutes +detail --id <taskUuid> --artifacts basic,summary,keywords --format json
|
||||
dws minutes +transcript --id <taskUuid> --format json
|
||||
```
|
||||
|
||||
不要仅因资源最初来自上传就再次进入上传 workflow。只有产物尚未就绪、确实需要有界轮询时,才使用:
|
||||
|
||||
```text
|
||||
dws minutes +upload-and-analyze --resume-id <taskUuid> --artifacts summary,transcript
|
||||
```
|
||||
|
||||
`--resume-id` 分支不重复上传或再次通知,但 `+upload-and-analyze` 是同时包含新上传分支的混合入口,因此仍按该命令的 Runtime confirmation 执行。旧 `+upload-and-analyze --enable-message-card` 继续执行原有通知语义并遵循同一确认门禁;新调用需要通知时优先使用 `+upload-and-notify`,再按需读取或恢复分析。
|
||||
|
||||
### 2.2 原子 upload session
|
||||
|
||||
只在 Shortcut 返回了可恢复 session、需要诊断某一阶段,或调用方自己负责文件 PUT 时使用原子命令:
|
||||
|
||||
| 阶段 | 原子命令 | 关键句柄 |
|
||||
|---|---|---|
|
||||
| 创建普通 session | `dws minutes upload create ...` | 保存 session/upload URL 等真实返回;旧 `--enable-message-card` 只返回迁移提示 |
|
||||
| 创建并通知 | `dws minutes upload create-and-notify ...` | 需要确认;不要用普通 create 模拟通知 |
|
||||
| 完成 session | `dws minutes upload complete ...` | 只对已知 session 执行,保留最终 taskUuid |
|
||||
| 取消 session | `dws minutes upload cancel ...` | 取消失败或状态未知时停止,不能谎报已清理 |
|
||||
|
||||
上传状态未知时先根据真实 session/taskUuid 读回;不能重新 create 来“试一次”。预签名 URL 属于敏感临时数据,不写入日志、报告或长期 manifest。
|
||||
|
||||
## 3. 异步生成与录音收尾
|
||||
|
||||
### 3.1 思维导图
|
||||
|
||||
```text
|
||||
dws minutes +mindmap --id <taskUuid>
|
||||
```
|
||||
|
||||
- 首次执行负责 create + 有界轮询。
|
||||
- 真实执行遵循 `user_required`;`--resume` 沿用同一命令级门禁。
|
||||
- 返回 pending/timeout 时保留 taskUuid;继续检查用 `--resume`,不重复 create。
|
||||
- 只有明确终态成功才声称已生成;失败和无法解析的状态返回非零。
|
||||
|
||||
### 3.2 发言人洞察
|
||||
|
||||
```text
|
||||
dws minutes +speaker-insights --id <taskUuid>
|
||||
```
|
||||
|
||||
- 首次执行保存 create 返回的异步 `taskId`。
|
||||
- 真实执行遵循 `user_required`;`--resume` 沿用同一命令级门禁。
|
||||
- 超时后使用 `--resume [--task-id <taskId>]` 继续轮询。
|
||||
- `taskId` 缺失、状态未知或结果不可解析时保留恢复信息并停止,不再次创建任务。
|
||||
|
||||
### 3.3 结束录音并等待产物
|
||||
|
||||
```text
|
||||
dws minutes +record-wrap-up --id <taskUuid> --artifacts summary,transcript
|
||||
```
|
||||
|
||||
该入口先停止指定录音,再有界等待产物。它只接受已绑定的真实 `taskUuid`;如果 `+record-start` 返回 `controlReady=false`,不能通过 `+latest` 或列表第一项猜录音目标。停止已成功但等待超时时,保留 taskUuid 和未完成产物,后续只恢复读取,不再次 stop。
|
||||
|
||||
## 4. 权限 workflow
|
||||
|
||||
先区分三种身份语义:
|
||||
|
||||
| 用户意图 | 推荐入口 | 目标身份 |
|
||||
|---|---|---|
|
||||
| “我打不开,帮我申请查看/下载/编辑” | `+apply-permission --id <taskUuid> --permission view|download|edit` | 当前登录用户 |
|
||||
| “把这条听记分享给张三” | `+share --id <taskUuid> --member-uids <UID> --permission view|download|edit` | 所有者给指定成员授权 |
|
||||
| “撤销张三对这条听记的权限” | `+unshare --id <taskUuid> --member-uids <UID>` | 所有者移除指定成员权限 |
|
||||
|
||||
### 4.1 成员解析
|
||||
|
||||
1. 用户已给稳定成员 UID:直接复用,但必须保持同一 profile/组织。
|
||||
2. 只有姓名、手机号或部门线索:切 `dingtalk-contact`/`dingtalk-aisearch` 解析;零或多候选时停止。
|
||||
3. 不把姓名、手机号、userId、openId 或跨组织 UID 互相猜测转换。
|
||||
|
||||
### 4.2 批量执行
|
||||
|
||||
`+share` 的精确业务参数是 `--id|--ids`、`--member-uids`、必填的 `--permission view|download|edit`,以及可选的 `--cover`、`--sub-resources OrigContent|Summary|Analysis|Note`、`--failure-policy stop|continue`:
|
||||
|
||||
```text
|
||||
dws minutes +share --id <taskUuid> --member-uids <uid1,uid2> --permission view --failure-policy stop --format json
|
||||
dws minutes +share --ids <uuid1,uuid2> --member-uids <uid> --permission edit --cover --sub-resources OrigContent,Summary --failure-policy continue --format json
|
||||
```
|
||||
|
||||
`+unshare` 的精确业务参数是 `--id|--ids`、`--member-uids` 和可选的 `--failure-policy stop|continue`;它没有 `--permission`、`--cover` 或 `--sub-resources`:
|
||||
|
||||
```text
|
||||
dws minutes +unshare --id <taskUuid> --member-uids <uid1,uid2> --failure-policy stop --format json
|
||||
dws minutes +unshare --ids <uuid1,uuid2> --member-uids <uid> --failure-policy continue --format json
|
||||
```
|
||||
|
||||
- `--id` 与 `--ids` 必须且只能选一个;听记 taskUuid 和成员 UID 去重后各为 `1..50` 个。
|
||||
- `+share --permission` 必填、没有默认值:`edit=policy 2`、`download=policy 3`、`view=policy 4`。管理员 `0`、所有者 `1` 只能走 `minutes permission add --policy 0|1`。
|
||||
- `--failure-policy` 默认 `stop`,首个成员失败后停止;显式 `continue` 才继续其他成员。任何失败都必须作为 partial/非零交付,并保留失败与未执行成员。
|
||||
- `+unshare` 是 write/medium、`user_required`;执行前明确听记、成员和撤权影响。`+share`、`+apply-permission` 同样执行 Runtime 的 `user_required` confirmation。对应原子 permission 命令只保留历史兼容 Contract,不作为绕过确认的推荐路径。
|
||||
- `+apply-permission` 只接受单个 `--id` 和必填的 `--permission view|download|edit`,目标固定为当前登录用户,不接受 `--member-uids`。
|
||||
|
||||
### 4.3 验证边界
|
||||
|
||||
当前没有公开的 `minutes permission list/get/inspect` 命令。真实执行后,`+share/+unshare` 可输出逐成员成功、失败和未执行 ledger,但成功项只表示写接口已确认接收;即使 `+unshare` 执行前读取了听记基本信息,也没有读到成员的最终权限。
|
||||
|
||||
因此权限结果必须按 `verification.mode=write_ack_only`、`verified=false` 交付。不要把 ledger 中的 `complete=true`、听记基本信息、dry-run 计划或退出码解释为“已读回验证权限生效”;也不要为了验证而重放授权或撤权请求。
|
||||
|
||||
### 4.4 dry-run
|
||||
|
||||
这些权限 Shortcut 的 dry-run 只展示目标组合与将执行的动作,不调用远端,也不证明听记、成员或当前权限状态。真实执行后也只能声明“写调用已确认接收”,不能声明“已读回最终权限”。
|
||||
@@ -0,0 +1,32 @@
|
||||
# 听记发言人内容匹配
|
||||
|
||||
> 返回入口:[DingTalk Minutes Skill](../SKILL.md) · [Reference 与脚本索引](minutes.md) · [确认后替换发言人](11-minutes-speaker-correct.md)
|
||||
|
||||
只在用户要求“总结某人在这场会议里说了什么”,但逐字稿中的发言人标注仍需核对时读取本文件。只要用户要的是整场会议摘要,直接使用根 Skill 的 `+detail`;不要进入本流程。
|
||||
|
||||
## 稳定流程
|
||||
|
||||
1. **锁定听记**:已有 `taskUuid` 或 URL 时直接解析;只有标题/时间时使用 `+search --scope all --page-all`。零命中、多候选或分页不完整时停止,不默认第一条。
|
||||
2. **读取完整逐字稿**:使用 `dws minutes +transcript --id <taskUuid>`,只有 `complete=true` 才进入人物归集;后页失败时不能用前几页代表整场发言。
|
||||
3. **检查现有标注**:逐字稿已经稳定标注目标姓名时,按真实 speaker/segment 归集;不要仅在正文中搜索姓名,因为“别人提到某人”不等于“此人发言”。
|
||||
4. **处理匿名发言人**:把匿名发言人的代表性原话和时间段作为候选证据。只有用户明确要求协助识别时,才可按会议时间查询日程参会人,并用通讯录补充候选的组织信息。
|
||||
5. **要求用户消歧**:没有唯一、可验证的身份映射时展示候选并让用户选择。职位、部门、语言风格、发言顺序或某一条聊天/文档都不能单独证明真实身份。
|
||||
6. **输出总结**:只总结已确认人物的真实发言,区分核心观点、问题、承诺/行动项和明确立场;源数据没有的责任人、数字或结论不得补写。
|
||||
|
||||
## 身份与隐私边界
|
||||
|
||||
- 日程参与人只是候选集合,不等于实际发言人,也不等于发言顺序。
|
||||
- 不为识别发言人而默认检索其私人聊天或个人文档;用户明确要求跨产品核对时,才分别加载对应产品 Skill,并保留来源边界。
|
||||
- 不暴露内部 speaker ID 作为人物身份结论。展示候选时使用逐字稿中可见的昵称或“匿名发言人 1/2”。
|
||||
- 置信度分数不是身份凭证。即使模型判断“很像”,在没有稳定证据或用户确认时也只能给候选,不能写回。
|
||||
|
||||
## 后续替换
|
||||
|
||||
总结任务本身是只读。只有用户明确要求修改标注,并确认“当前昵称 → 目标姓名”的对应关系后,才读取 [11-minutes-speaker-correct.md](11-minutes-speaker-correct.md);不能把“帮我总结某人说了什么”自动扩大为替换发言人。
|
||||
|
||||
## 失败与不完整
|
||||
|
||||
- 逐字稿不完整:返回 `data.complete/data.pages`、`meta.pagination` 或失败信封等证据,停止人物级完整总结。
|
||||
- 目标姓名未出现在稳定标注中:说明无法直接确认,展示匿名候选或询问用户。
|
||||
- 同一姓名对应多人或跨组织结果:要求用户选择,保持当前 profile,不跨组织复用 UID。
|
||||
- 没有足够发言内容:如实说明样本不足,不用其他人的发言补齐。
|
||||
@@ -0,0 +1,40 @@
|
||||
# 听记发言人标注与替换
|
||||
|
||||
> 返回入口:[DingTalk Minutes Skill](../SKILL.md) · [Reference 与脚本索引](minutes.md) · [先识别和总结发言人](10-minutes-speaker-match.md)
|
||||
|
||||
只在用户已经提供或确认“当前发言人昵称 → 目标姓名”的对应关系,并明确要求修改听记时读取本文件。本流程不自动判断谁是谁,也不把候选推断当成写入授权。
|
||||
|
||||
## 写入前计划
|
||||
|
||||
1. 使用真实 `taskUuid` 锁定听记;只有标题时先按根 Skill 搜索并消歧。
|
||||
2. 使用 `+transcript` 拉取完整逐字稿,确认每个 `--from` 昵称真实存在,并记录涉及的段落数量。
|
||||
3. 将用户给出的每一组映射整理成 `from/to` 计划;同一个源昵称不得映射到多个目标姓名。
|
||||
4. 用户要求绑定组织成员身份时,用 `dingtalk-aisearch` 唯一解析人员,再由 `dingtalk-contact` 核对同组织稳定 UID。零命中或多候选时停止该项;不能把姓名、手机号或跨组织 ID 直接当 UID。
|
||||
5. 先执行 `+speaker-replace --dry-run` 查看计划。dry-run 不调用远端,也不能被解释为用户已经同意真实替换。
|
||||
|
||||
## 执行与验证
|
||||
|
||||
推荐入口:
|
||||
|
||||
```text
|
||||
dws minutes +speaker-replace --id <taskUuid> --from <当前昵称> --to <目标姓名> [--target-uid <UID>]
|
||||
```
|
||||
|
||||
- 这是写操作,按 leaf Schema 与 Runtime confirmation 执行;确认内容必须包含听记、源昵称、目标姓名和可选 UID。
|
||||
- 多组映射逐项保留 ledger。某项失败后不能丢掉已成功项,也不能重放整个批次。
|
||||
- 每次写入后重新读取逐字稿,验证旧昵称的目标段落已经更新,且没有修改其他发言人。
|
||||
- 验证失败或结果未知时停止,报告已执行动作和当前证据;禁止盲目重试。
|
||||
|
||||
## 听音识别边界
|
||||
|
||||
当前没有发布“自动选择音频片段、调用本地 ffmpeg、发送片段并自动绑定身份”的稳定 Minutes Golden Route。需要用户听音识别时,可以先用 `+download` 安全下载用户明确指定听记的媒体,再让用户提供对应关系;不要静默安装工具、手写媒体请求或自行把音色推断成真实人员。
|
||||
|
||||
## 失败处理
|
||||
|
||||
| 情况 | 处理 |
|
||||
|---|---|
|
||||
| 源昵称不存在 | 停止该项并展示逐字稿中的真实昵称候选 |
|
||||
| 人员解析多候选 | 展示姓名、部门等必要信息,让用户选择 |
|
||||
| 找不到组织成员 | 询问是否仅修改显示名;未获授权时不执行 |
|
||||
| 权限不足 | 保留原目标和计划,提示需要听记所有者/协作者权限 |
|
||||
| 写入或读回状态未知 | 不自动重试;先按真实 taskUuid 重新读取 |
|
||||
@@ -0,0 +1,79 @@
|
||||
# Minutes 低频意图与产品边界
|
||||
|
||||
> 返回入口:[DingTalk Minutes Skill](../SKILL.md) · [Reference 与脚本索引](minutes.md)
|
||||
|
||||
本文件只承接不在根 Skill Golden Route 展开的低频能力。命令参数或 Safety 不确定时读取对应 compact leaf Schema;不要因此加载 Minutes 全量 Catalog。
|
||||
|
||||
## 低频能力路由
|
||||
|
||||
| 用户意图 | 推荐入口 | 关键边界 |
|
||||
|---|---|---|
|
||||
| “把发言人1改成张三” | `dws minutes +speaker-replace --id <taskUuid> --from "发言人1" --to "张三" [--target-uid <UID>]` | 这是逐字稿里的昵称替换,不是 speaker_id 与用户身份的系统级重绑;先完整预检源昵称,按 Runtime confirmation 执行并读回验证 |
|
||||
| “把这篇听记里的 A/B/C 批量替换” | `dws minutes +replace-batch --id <taskUuid> --pair "旧词=>新词" --failure-policy stop --page-limit 100 --dry-run --format json` | dry-run 只预览本地规则,不检查远端命中;真实执行按 Runtime confirmation,逐项验证并保留失败项 |
|
||||
| “下载这条听记的音频/视频” | `dws minutes +download --id <taskUuid> --output <相对路径>` | 媒体 URL 是短期签名地址;默认直接安全下载。只有用户明确只要链接时使用 `--url-only` |
|
||||
| “把多条听记媒体下载到目录” | `dws minutes +download --ids <uuid1,uuid2> --output-dir <相对目录>` | 最多 50 个,逐项保留成功与失败;禁止目录穿越和静默覆盖 |
|
||||
| “把摘要、关键词、完整逐字稿、待办归档成一包” | `dws minutes +export-pack --id <taskUuid> --output <新目录>` | 逐字稿必须完整;所有必需产物验证后才原子发布目录;目标目录已存在时拒绝覆盖 |
|
||||
| “归档时也带媒体” | `dws minutes +export-pack --id <taskUuid> --output <新相对目录> --include-media --format json` | manifest 不保存签名 URL;媒体未就绪导致归档不完整时必须明确失败 |
|
||||
| “按标签找听记” | `dws minutes tag list --format json`,再用真实 `tagId` 执行 `dws minutes tag query --tag-id <tagId> --limit 10 --format json` | 不按标签名猜 ID;空标签直接结束,有 `nextToken` 时续拉或明确结果不完整 |
|
||||
| “查语音备忘” | `dws minutes audio-memo list --format json` | 属于独立原子查询;需要时间范围或分页参数时读取该 compact leaf Schema |
|
||||
|
||||
需要根据逐字稿总结指定发言人的内容时读取 [发言人匹配流程](10-minutes-speaker-match.md);用户已经确认“匿名发言人 → 姓名”的对应关系、准备执行标注替换时读取 [发言人纠正流程](11-minutes-speaker-correct.md)。两者都不能凭职位、语言风格或列表顺序自动认定身份。
|
||||
|
||||
## 批量文本替换
|
||||
|
||||
```text
|
||||
dws minutes +replace-batch --id <taskUuid> --pair "旧词1=>新词1" --pair "旧词2=>新词2" --failure-policy stop --page-limit 100 --dry-run --format json
|
||||
```
|
||||
|
||||
- `--pair` 可以重复;也可用 `--json '<数组>'`、`--json @<相对文件>` 或 `--json -`,二者至少提供一种。每组格式固定为 `原文=>替换`,原文不能为空或重复。
|
||||
- 用户没有提供完整的“原文=>替换”映射时,不查询 Help、不编造替换词,也不执行没有业务意义的空 dry-run;先按已有线索列出待补齐的 `原文=><目标词>` 模板并只询问一次,拿到完整映射后再预演。
|
||||
- `--failure-policy continue` 只表示失败后继续尝试剩余规则;只要有一项失败,整体仍是 partial/非零。
|
||||
- dry-run 是 `remoteReads=false` 的本地计划;返回的 `total` 是替换规则数,不是逐字稿中的命中次数。要声称命中必须有完整逐字稿或真实执行前预检证据。
|
||||
- 真实执行按 Runtime confirmation;完成后以逐项 ledger 和完整逐字稿读回为准,不能让最终答复与工具结果矛盾。
|
||||
|
||||
## 标签查询
|
||||
|
||||
```text
|
||||
dws minutes tag list --format json
|
||||
dws minutes tag query --tag-id <tag-list真实返回的tagId> --limit 10 --format json
|
||||
dws minutes tag query --tag-id <tagId> --limit 10 --cursor <nextToken> --format json
|
||||
```
|
||||
|
||||
- 不按标签名称猜 `tagId`;`tag list` 明确返回空数组时直接交付“当前无标签”,不再查 Help 或拿其他分组补位。
|
||||
- `tag query` 是单页原子查询;返回真实 `nextToken` 时继续续拉,或明确说明当前结果不完整。
|
||||
|
||||
## 本地归档
|
||||
|
||||
```text
|
||||
dws minutes +export-pack --id <taskUuid> --output ./minutes-export --format json
|
||||
dws minutes +export-pack --id <taskUuid> --output ./minutes-export --include-media --format json
|
||||
```
|
||||
|
||||
- `--output` 必须是工作目录内尚不存在的安全相对目录;命令拒绝目录穿越和静默覆盖。
|
||||
- 默认归档 `basic,summary,keywords,transcript,todos`;如用 `--artifacts` 缩小集合,只能声称已交付实际选择并验证通过的产物。
|
||||
- `--include-media` 只决定是否附带媒体,不降低逐字稿完整性要求;manifest 不保存短期签名 URL。
|
||||
- 只有响应中的 `published=true`、真实 `path/manifest/files` 和所选产物均完整,才能称归档已生成;任一产物 unknown/pending/failed 时不得宣称成功。
|
||||
|
||||
## 目标匹配
|
||||
|
||||
- 用户给了 `taskUuid` 或听记 URL:直接解析和使用真实 ID,不再按标题搜索。
|
||||
- 用户给了标题:优先精确标题;没有精确命中时可以返回标题包含或语义相关候选。候选足够接近且唯一时可继续;差异明显或多个候选都合理时让用户选择。全量搜索尚未完成但有有效 `nextToken` 时继续,只有 continuation 缺失/停滞/循环、达到页数上限或后页失败时停止并说明不完整。
|
||||
- 不要求用户口述标题必须与服务端字符逐字相同,但也不能把“语义相关”当成“已确认目标”。任何写操作前都要确保目标唯一。
|
||||
- 用户明确说“最新一条”才使用 `+latest`;它不是通用消歧器,也不能用于录音绑定。
|
||||
|
||||
## 内容形态边界
|
||||
|
||||
| 需要的结果 | 使用 |
|
||||
|---|---|
|
||||
| 只在对话里查看摘要/逐字稿/待办 | Minutes 读取命令 |
|
||||
| 形成可持续编辑的钉钉文档 | 先用 Minutes 读取真实内容,再切 `dingtalk-doc` 创建或编辑 |
|
||||
| 把行动项变成可分派任务 | 先用 `+action-items`,再切 `dingtalk-todo` |
|
||||
| 给别人发摘要文本 | 切 `dingtalk-chat`;不要把授予听记权限误当成发送消息 |
|
||||
| 管理会议时间或会议室 | `dingtalk-calendar` |
|
||||
| 管理普通云盘文件 | `dingtalk-drive`;听记媒体下载和听记上传仍由 Minutes 负责 |
|
||||
|
||||
## 写入与确认
|
||||
|
||||
- 发言人替换、批量文本替换都改变现有听记内容,按 Runtime confirmation 执行;dry-run 只显示计划,不写远端。
|
||||
- 下载和导出写入本地工作目录,不改变远端听记,但必须使用安全相对路径、no-clobber 和原子发布语义。
|
||||
- 任一批量流程只要存在失败项,就按 partial/非零交付完整 ledger;不能只汇报成功项。
|
||||
@@ -0,0 +1,25 @@
|
||||
# Minutes 兼容 Recipe 索引
|
||||
|
||||
> 返回入口:[DingTalk Minutes Skill](../SKILL.md) · [Reference 与脚本索引](minutes.md) · [Recipe 通用约束](recipes/conventions.md)
|
||||
|
||||
本文件保留旧版 Recipe 名称到当前 Golden Route 的映射,供兼容调用方定位;它不是另一套命令权威。当前路由、安全、Result 和 Pagination 以根 Skill、精确 Reference、leaf Schema 与 Runtime 为准。
|
||||
|
||||
| 旧 Recipe 意图 | 当前推荐入口 | 继续阅读 |
|
||||
|---|---|---|
|
||||
| `minutes-query`:查询、列表、详情 | `+search`、`+list-*`、`+latest`、`+detail` | [原子与完整性边界](minutes.md) |
|
||||
| 完整逐字稿 | `+transcript` | [读取内容](minutes.md#3-读取内容) |
|
||||
| 行动项 | `+action-items` | [读取内容](minutes.md#3-读取内容) |
|
||||
| `minutes-edit`:标题、纪要 | `+update`、`+summary` | [更新与录音控制](minutes.md#4-更新与录音控制) |
|
||||
| 发言人总结/替换 | `+transcript` → 用户确认 → `+speaker-replace` | [发言人内容匹配](10-minutes-speaker-match.md) / [标注与替换](11-minutes-speaker-correct.md) |
|
||||
| `minutes-tag`:标签与语音备忘 | 原子 `tag` / `audio-memo` 精确 leaf | [标签与语音备忘](minutes.md#9-标签与语音备忘) |
|
||||
| `minutes-permission` | `+apply-permission`、`+share`、`+unshare` | [复杂流程](07-minutes.md#4-权限-workflow) |
|
||||
| `minutes-upload` | `+upload`、`+upload-and-notify`、`+upload-and-analyze` | [上传、通知与恢复](07-minutes.md#2-上传通知与恢复) |
|
||||
| ASR 热词 | `+prepare-asr` 或明确 destructive 的 `+sync-asr` | [ASR 热词](07-minutes.md#1-asr-热词) |
|
||||
|
||||
## 兼容边界
|
||||
|
||||
- “我的听记”不自动等于 `mine`;只有用户明确说“我创建/发起的”才缩窄。完整 accessible 读取走当前有界聚合,并检查 `complete=true`。
|
||||
- 原子 `get transcription` 是单页接口;完整逐字稿只推荐 `+transcript`。
|
||||
- 旧 flag、alias 或脚本只有在当前 leaf Help/脚本说明仍存在时才可使用;不能根据旧 Recipe 猜参数。
|
||||
- 仓库辅助脚本的定位和限制见 [Reference 与脚本索引](minutes.md#辅助脚本)。它们不覆盖完整分页、跨 scope 聚合或通用目标消歧。
|
||||
- 任何写操作的确认以当前 leaf Schema 和 Runtime gate 为准;旧 Recipe 中出现过的示例不能构成执行授权。
|
||||
@@ -0,0 +1,210 @@
|
||||
# Minutes 原子命令参考
|
||||
|
||||
> 返回入口:[DingTalk Minutes Skill](../SKILL.md)
|
||||
|
||||
本文件只在必须使用原子命令、需要确认 URL/参数边界,或 Shortcut 无法表达底层控制时读取。Golden Route 已能完成任务时不要降级为手写多步原子调用。
|
||||
|
||||
## Reference 与脚本索引
|
||||
|
||||
本页同时是 Minutes 的二级导航页。根 Skill 只链接任务级入口;需要继续下钻时,从这里进入一个最精确的 Reference,不要一次预读多个文件。
|
||||
|
||||
| 任务 | Reference |
|
||||
|---|---|
|
||||
| ASR、上传恢复、异步生成、录音收尾、批量权限 | [07-minutes.md](07-minutes.md) |
|
||||
| 发言人/文本替换、媒体下载、离线导出、标签等低频意图 | [intent-guide.md](intent-guide.md) |
|
||||
| 总结指定发言人的内容并处理匿名发言人候选 | [10-minutes-speaker-match.md](10-minutes-speaker-match.md) |
|
||||
| 用户确认对应关系后的发言人标注与替换 | [11-minutes-speaker-correct.md](11-minutes-speaker-correct.md) |
|
||||
| 兼容旧调用方式的轻量 Recipe 索引 | [lite-recipes.md](lite-recipes.md) |
|
||||
| 旧 Recipe 仍需遵守的分页、ID 与批量约束 | [recipes/conventions.md](recipes/conventions.md) |
|
||||
|
||||
### 辅助脚本
|
||||
|
||||
以下文件仍随 Skill 交付,所以必须可发现;但它不是 Golden Route。当前 Runtime 有对应 Shortcut 时优先 Shortcut,只有用户明确要求生成本地 Markdown 汇总文件、使用仓库脚本或兼容旧调用方时才运行脚本。
|
||||
|
||||
| 文件 | 定位 | 当前边界 |
|
||||
|---|---|---|
|
||||
| [minutes_recent_summary.py](../scripts/minutes_recent_summary.py) | 汇总最近若干条“我创建的”听记摘要 | 可直接运行;固定使用 `mine`,不能代替 `+search --scope all` 或具名目标定位 |
|
||||
|
||||
脚本的 `--dry-run` 只打印计划且不得调用 DWS;DWS 失败、非法 JSON 与合法空结果必须分开。脚本没有覆盖完整分页、目标消歧与跨 scope 聚合,因此不能用脚本输出声称“全部听记”。行动项读取由 `+action-items`(单条)或 `+detail --ids ... --artifacts todos`(多条)承接,不再发布重复脚本。
|
||||
|
||||
命令前缀统一为 `dws minutes`。结构化读取加 `--format json`;参数不确定时查询精确 leaf:
|
||||
|
||||
```text
|
||||
dws schema --cli-path "minutes <group> <leaf>" --compact --format json
|
||||
dws minutes <group> <leaf> --help
|
||||
```
|
||||
|
||||
只在 Schema 语义/安全不确定时读第一条,只在当前 Cobra flag 不确定时读第二条;不要加载产品级全量 Schema。
|
||||
|
||||
## 1. ID 与 URL
|
||||
|
||||
`--id` 接受纯 `taskUuid`,不接受完整 URL。用户给听记链接时由 Agent 自动解析,不要求用户手动抠 ID。
|
||||
|
||||
| URL 形式 | 提取规则 |
|
||||
|---|---|
|
||||
| `https://shanji.dingtalk.com/app/transcribes/<taskUuid>` | 取 `/transcribes/` 后至 `?` 或路径结束的值 |
|
||||
| `https://shanji.dingtalk.com/meeting/minutes?taskUuid=<taskUuid>` | 读取 `taskUuid` query 参数 |
|
||||
| 其他包含 `minutesId` 的可信听记链接 | 读取 `minutesId` query 参数,并按 leaf 要求作为 taskUuid 使用 |
|
||||
| 纯 taskUuid | 直接使用 |
|
||||
|
||||
解析失败时停止并说明不识别该链接格式;不把整条 URL 传给 `--id`,不把 URL 当标题关键词搜索。多个 URL 逐一解析,后续始终保持 ID 与标题/组织/时间对应关系。
|
||||
|
||||
## 2. 列表与定位
|
||||
|
||||
| 原子命令 | 范围 | 分页事实 |
|
||||
|---|---|---|
|
||||
| `minutes list mine` | 我创建的 | 单页接口;继续读取必须透传真实 cursor/nextToken |
|
||||
| `minutes list shared` | 共享给我的 | 单页接口;继续读取必须透传真实 cursor/nextToken |
|
||||
| `minutes list all` | 后端 noLimit 视图 | 不能仅凭该端点宣称等于完整 `mine + shared` |
|
||||
|
||||
完整检索优先用 `+search --page-all` 或 `+list-* --page-all`。这些 Shortcut 使用统一结果信封:业务集合与范围完整性位于 `data.scope/count/minutes/pages/complete`,端点耗尽与续页信息位于 `meta.pagination.endpoint_exhausted/next_token`。原子列表只有一页,必须解析真实 `itemList`,不能把未知响应形态当空数组。
|
||||
|
||||
定位规则:
|
||||
|
||||
1. 用户给真实 taskUuid/URL:直接使用。
|
||||
2. 用户给标题/关键词/时间:先搜索。服务端过滤与 Agent 复核应使用同一时间范围和 profile。
|
||||
3. 精确标题优先;标题包含或语义相关结果可作为候选。零命中停止,多候选、差异较大或分页未完成时消歧。
|
||||
4. 锁定后所有 get/update 操作复用同一 taskUuid;某项内容为空或失败不能偷偷换对象。
|
||||
|
||||
## 3. 读取内容
|
||||
|
||||
| 原子命令 | 返回内容 | 重要边界 |
|
||||
|---|---|---|
|
||||
| `minutes get info --id <taskUuid>` | 标题、创建/时间、组织、链接等基础信息 | 结果必须能归属于请求 ID |
|
||||
| `minutes get summary --id <taskUuid>` | AI 摘要/纪要 | 合法空摘要与调用失败分开 |
|
||||
| `minutes get keywords --id <taskUuid>` | 关键词 | 不从空/未知字段编造关键词 |
|
||||
| `minutes get transcription --id <taskUuid>` | 单页逐字稿 | 这是单页原子入口;存在下一页时继续传 cursor。需要完整结果优先 `+transcript` |
|
||||
| `minutes get todos --id <taskUuid>` | 行动项 | 当前响应可能使用 `actions` 或 `dingtalkTodoList`;失败不能伪装成“暂无待办” |
|
||||
| `minutes get audio --id <taskUuid>` | 临时媒体 URL | URL 敏感且会过期,不长期记录 |
|
||||
| `minutes get batch --ids <uuid1,uuid2>` | 多条基础详情 | 批量结果逐项对应 ID,缺项不能算全成功 |
|
||||
|
||||
完整逐字稿优先 `+transcript`;它跨页去重,业务完整性位于 `data.complete/data.pages`,续页状态位于 `meta.pagination`,分页中断返回失败信封。`+detail` 适合一次读取多种产物;任何所选产物失败都属于 partial,不把 bundle 说成完整。
|
||||
|
||||
需要核对多条命中的 basic 时,不要只抽查第一条:
|
||||
|
||||
```text
|
||||
dws minutes +detail --ids <uuid1,uuid2> --artifacts basic --format json
|
||||
```
|
||||
|
||||
结果必须逐项覆盖请求 ID;缺项或失败项如实保留。当前 basic 投影没有逐条 `orgName` 时,应说明归属字段不可得;当前 profile 的 `corpName` 只能证明执行上下文,不能证明每条听记的创建组织或归属。
|
||||
|
||||
多听记、多来源或跨产品任务先建立逐来源证据台账:`requested` 记录用户要求的输入,`resolved` 记录已锁定的 `taskUuid`/来源 ID,`missing` 记录未找到的输入,`artifacts` 记录每条实际取得的内容,`status` 记录 `succeeded/partial/failed/unknown`。任一必需来源缺失时整体不能称完整;后续跨产品 Skill 只能接收有来源 ID 的真实产物,不能把已找到的子集写成全部。
|
||||
|
||||
内容型结论必须与逐条 artifact 对齐。只有 `title/basic` 时可以列出标题、时间等元数据,不能生成摘要、关键词、主题或内容分类;只有取得该条真实 `summary/transcript/keywords` 后,才能对相应内容做归纳。
|
||||
|
||||
## 4. 更新与录音控制
|
||||
|
||||
| 原子命令 | 效果 | 当前 confirmation |
|
||||
|---|---|---|
|
||||
| `minutes update title` | 修改听记标题 | `not_required`(历史兼容原子入口) |
|
||||
| `minutes update summary` | 全量覆盖纪要正文 | `not_required`(历史兼容原子入口) |
|
||||
| `minutes record start` | 发起实时录音 | `not_required`(历史兼容原子入口) |
|
||||
| `minutes record pause` | 暂停指定 taskUuid | `not_required`(历史兼容原子入口) |
|
||||
| `minutes record resume` | 恢复指定 taskUuid | `not_required`(历史兼容原子入口) |
|
||||
| `minutes record stop` | 永久停止指定 taskUuid | `not_required`(历史兼容原子入口) |
|
||||
|
||||
标题与纪要推荐分别使用仍执行 `user_required` 门禁的 `+update`、`+summary`,因为它们包含预检/读回验证。原子入口不得作为绕过确认的降级路径。更新纪要时先读取当前完整正文,保留原有 Markdown 图片和用户未要求改变的内容,再写回完整目标内容。
|
||||
|
||||
仅预览标题变化时,先读取当前标题,再调用本地计划;`+update --dry-run` 本身不访问远端,所以 `before` 必须来自前一条真实 basic 读取:
|
||||
|
||||
```text
|
||||
dws minutes +detail --id <taskUuid> --artifacts basic --format json
|
||||
dws minutes +update --id <taskUuid> --title "<目标标题>" --dry-run --format json
|
||||
```
|
||||
|
||||
最终展示 `当前标题 → 目标标题`、`executed=false` 和同一 taskUuid 后即结束。用户说“不实际写入”时不得继续索要写入确认、追加 `--yes`、真实改名或再以还原补救。
|
||||
|
||||
录音 start 的成功回执不一定含可控制的 taskUuid。只有响应明确提供 `taskUuid`,并由 Shortcut 返回 `controlReady=true`,才能执行 pause/resume/stop;不能通过“最新听记”或列表第一条猜测绑定。
|
||||
|
||||
## 5. 思维导图与发言人
|
||||
|
||||
| 原子命令 | 效果 | 当前 confirmation |
|
||||
|---|---|---|
|
||||
| `minutes mind-graph create --id <taskUuid>` | 创建思维导图异步任务 | `not_required` |
|
||||
| `minutes mind-graph status --id <taskUuid>` | 查询任务状态 | `not_required` |
|
||||
| `minutes speaker replace ...` | 替换逐字稿发言人昵称 | `not_required`(历史兼容原子入口) |
|
||||
| `minutes speaker summary create --ids <IDs>` | 创建发言人段落总结 | `not_required` |
|
||||
| `minutes speaker summary get --ids <IDs>` | 查询发言人总结 | `not_required` |
|
||||
|
||||
异步 create 后有界轮询;pending/timeout 保留 taskUuid/taskId,恢复只做 status/get。发言人 replace 是昵称替换,不是身份系统中的 speaker_id 重绑;写前必须完整读取逐字稿,确认源昵称存在且目标唯一。
|
||||
|
||||
## 6. ASR 热词与文本替换
|
||||
|
||||
| 原子命令 | 效果 | 当前 confirmation |
|
||||
|---|---|---|
|
||||
| `minutes hot-word list` | 查看个人热词 | `not_required` |
|
||||
| `minutes hot-word add` | 新增热词 | `not_required` |
|
||||
| `minutes hot-word delete` | 删除热词 | `not_required`,write/medium(历史兼容原子入口) |
|
||||
| `minutes replace-text` | 替换一条听记中的文本 | `not_required`(历史兼容原子入口) |
|
||||
|
||||
普通“补充热词”优先需要确认的 `+prepare-asr`,它只新增缺失项。只有用户明确要求最终集合完全一致并接受删除多余项时使用 `+sync-asr`;旧 `+prepare-asr --sync` 保持公开以提供迁移提示,但不会调用 MCP。批量文本替换优先仍要求确认的 `+replace-batch`,保留逐项验证和失败 ledger;不得为了绕过确认改用原子 delete/replace。
|
||||
|
||||
## 7. Upload session
|
||||
|
||||
| 原子命令 | 效果 | 当前 confirmation |
|
||||
|---|---|---|
|
||||
| `minutes upload create` | 创建普通上传 session | `not_required` |
|
||||
| `minutes upload create-and-notify` | 创建 session,并要求上传完成后发送闪记卡片 | `user_required` |
|
||||
| `minutes upload complete` | 完成已知 session 并生成听记 | `not_required` |
|
||||
| `minutes upload cancel` | 取消已知 session | `not_required` |
|
||||
|
||||
正常本地文件上传优先 `+upload` 或 `+upload-and-notify`,由 Shortcut 管理 create、PUT、complete、取消和读回。原子流程必须保存真实 sessionId/上传地址/taskUuid:
|
||||
|
||||
`minutes upload create --enable-message-card` 当前仍是原子命令迁移提示;需要通知时使用 `minutes upload create-and-notify`。Shortcut 层的旧 `+upload --enable-message-card` 和 `+upload-and-analyze --enable-message-card` 则继续执行原有通知语义,并遵循各自通用 Runtime confirmation;新调用优先使用 `+upload-and-notify`。真实执行 `+upload` 和 `+upload-and-analyze` 必须遵循确认门禁,不能因为不发送消息就绕过创建听记的确认。
|
||||
|
||||
1. create 或 create-and-notify。
|
||||
2. 按服务端返回的预签名地址上传文件,不记录该地址。
|
||||
3. 对同一个 session 执行 complete。
|
||||
4. 上传前失败可对真实 session 执行 cancel;状态未知先读回,不重新创建。
|
||||
|
||||
## 8. 权限
|
||||
|
||||
### 8.1 当前能力边界
|
||||
|
||||
| 用户要做的事 | 当前公开入口 | 能否完成 | 边界 |
|
||||
|---|---|---:|---|
|
||||
| 当前登录用户为自己申请访问 | `minutes permission apply` / `+apply-permission` | 是 | 原子入口保留历史 `not_required`;推荐 `+apply-permission`,要求确认且只支持编辑、查看下载、仅查看 |
|
||||
| 所有者/管理员给稳定 member UID 授权 | `minutes permission add` / `+share` | 是 | 原子入口保留历史 `not_required` 且支持 policy `0..4`;推荐 `+share`,要求确认且只支持 `edit/download/view` |
|
||||
| 所有者/管理员撤销稳定 member UID 权限 | `minutes permission remove` / `+unshare` | 是 | 原子入口为历史 write/medium、`not_required`;推荐 `+unshare` 为 write/medium、`user_required` |
|
||||
| 列出、读取或检查一条听记当前的成员权限 | 无公开 `permission list/get/inspect` 命令 | 否 | 不得用基本信息、写入回执或 dry-run 冒充权限读回 |
|
||||
| 删除整条听记 | 无公开 Minutes delete 命令 | 否 | `permission remove` 只撤权;`hot-word delete` 只删除个人热词,都不能替代删除听记 |
|
||||
|
||||
### 8.2 policy 映射
|
||||
|
||||
| `--policy` | 权限 | `permission add` | `permission apply` | Shortcut 语义值 |
|
||||
|---:|---|---:|---:|---|
|
||||
| `0` | 管理员 | 支持 | 不支持 | 无;需要时使用原子命令 |
|
||||
| `1` | 所有者 | 支持 | 不支持 | 无;需要时使用原子命令 |
|
||||
| `2` | 可编辑 | 支持 | 支持 | `edit` |
|
||||
| `3` | 可查看/下载 | 支持 | 支持 | `download` |
|
||||
| `4` | 仅查看 | 支持 | 支持 | `view` |
|
||||
|
||||
`permission add` 的 `--policy` 是必填参数,没有默认值;`permission apply --policy` 也必须显式传入,只接受 `2..4`。用户未指定权限类型时先询问,不得把空值解释成 `0`,也不得擅自选择“仅查看”;即使确认选择“仅查看”,命令中仍必须显式传入 `--policy 4`。示例:
|
||||
|
||||
```text
|
||||
dws minutes permission add --ids <uuid1,uuid2> --member-uids <uid1,uid2> --policy 3
|
||||
dws minutes permission remove --ids <uuid1,uuid2> --member-uids <uid1,uid2>
|
||||
dws minutes permission apply --id <taskUuid> --policy 4
|
||||
```
|
||||
|
||||
根路径优先用 `+apply-permission`、`+share`、`+unshare` 的语义化参数;完整参数见 [07-minutes.md](07-minutes.md#4-权限-workflow)。只有姓名时先用 Contact/AI Search 解析为同组织稳定 UID,不能把姓名、手机号、userId、openId 或跨组织 UID 直接当作 member UID。
|
||||
|
||||
当前没有公开权限读取接口,因此权限写入成功最多证明服务端接受了这次写调用。交付边界按 `verification.mode=write_ack_only`、`verified=false` 表述;这两个值描述证据等级,不代表 Runtime 一定已经返回同名字段。`+share/+unshare` 的逐成员 `complete` ledger 仍只是写入回执,不能声称已读回最终权限状态。
|
||||
|
||||
## 9. 标签与语音备忘
|
||||
|
||||
| 原子命令 | 用途 |
|
||||
|---|---|
|
||||
| `minutes tag list` | 列出当前用户的听记标签,取得真实 tagId |
|
||||
| `minutes tag query --tag-id <tagId>` | 查询标签下的听记并按真实 token 翻页 |
|
||||
| `minutes audio-memo list` | 查询语音备忘;具体范围和分页参数读取 leaf Schema |
|
||||
|
||||
这些是长尾只读入口,不在根 Skill 展开。tagId 必须来自真实 `tag list`,不能按标签名猜 ID。
|
||||
|
||||
## 10. 结果与错误底线
|
||||
|
||||
- 成功必须由结构化业务结果与结果契约共同证明,不只看退出码或 `success=true`。
|
||||
- 列表、逐字稿、批量读取必须保留分页与完整性事实;页数上限、cursor 循环、缺 nextToken 或某页失败都返回不完整/非零。
|
||||
- 有公开读取接口的写操作完成后按稳定 ID 读回;权限写入当前没有公开读回入口,只能报告 `write_ack_only`、`verified=false`,未知结果不得重放整个请求。
|
||||
- 权限、上传、异步任务和批量替换必须保留部分成功 ledger 与恢复句柄。
|
||||
- Runtime confirmation 与 compact Schema 是最终权威;若本文与当前 leaf Schema 冲突,使用更安全解释并报告 contract drift。
|
||||
@@ -0,0 +1,33 @@
|
||||
# Minutes Recipe 通用约束
|
||||
|
||||
> 返回入口:[DingTalk Minutes Skill](../../SKILL.md) · [Reference 与脚本索引](../minutes.md) · [兼容 Recipe](../lite-recipes.md)
|
||||
|
||||
本页只约束仍使用旧 Recipe 名称的兼容调用方,不复制根 Skill 的完整执行契约。
|
||||
|
||||
## 目标与 ID
|
||||
|
||||
| 字段 | 真实来源 | 后续用途 |
|
||||
|---|---|---|
|
||||
| `taskUuid` | Minutes 搜索/列表/创建的真实结果,或可信听记 URL 解析 | 所有详情、内容、更新、权限、下载与异步产物命令 |
|
||||
| 成员 UID | 同 profile 下 AI Search/Contact 的唯一人员结果 | `+share/+unshare` 与需要身份绑定的发言人替换 |
|
||||
| `sessionId` | upload create 的真实结果 | upload complete/cancel 与状态恢复 |
|
||||
| `taskId` | 异步 create 的真实结果 | 继续轮询或恢复,不能重新 create 代替 |
|
||||
|
||||
标题、姓名、手机号、列表顺序和“最新一条”都不能替代稳定 ID。目标零命中、多候选、跨组织或分页未完成时停止并消歧。
|
||||
|
||||
## 分页与批量
|
||||
|
||||
- 完整列表和逐字稿必须读取到端点穷尽并检查 `complete=true`;缺 token、cursor 不前进/循环、页数上限或某页失败都属于不完整。
|
||||
- 优先使用发布的批量接口或 Shortcut;没有批量能力时采用有界执行,不把 shell `& wait` 写成通用要求。
|
||||
- 批量结果逐项记录目标、动作、成功、失败与未知状态。部分成功返回完整 ledger,不只汇报成功项。
|
||||
|
||||
## 写入与恢复
|
||||
|
||||
- Runtime 要求确认时,远端写调用数在确认前必须为 0;dry-run 也必须为 0。
|
||||
- 非幂等或状态未知的 create/update 不盲目重试。先按真实 ID 读回,再决定只恢复未完成步骤。
|
||||
- 写入成功必须有业务回执和必要读回证据;退出码 0、HTTP 200 或 `success=true` 单独都不足以证明完成。
|
||||
|
||||
## 跨产品传递
|
||||
|
||||
- Minutes 只交付真实听记内容和稳定 ID;写文档、建待办、发消息分别切换 `dingtalk-doc`、`dingtalk-todo`、`dingtalk-chat`。
|
||||
- 跨产品传递时保留来源听记的 `taskUuid/title/profile`,不得把另一个组织或另一个候选的字段拼接到当前对象。
|
||||
@@ -0,0 +1,196 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
获取最近 N 条听记的 AI 摘要并合并输出
|
||||
|
||||
用法:
|
||||
python minutes_recent_summary.py # 最近 5 条
|
||||
python minutes_recent_summary.py --max 10 # 最近 10 条
|
||||
python minutes_recent_summary.py --output summary.md
|
||||
python minutes_recent_summary.py --dry-run
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import subprocess
|
||||
import argparse
|
||||
from typing import List, Any, Optional, Tuple
|
||||
|
||||
|
||||
class DWSCommandError(RuntimeError):
|
||||
"""DWS 没有返回可用 JSON;调用方不得把它解释成空业务结果。"""
|
||||
|
||||
|
||||
def _unwrap_rows(payload: Any) -> List[Any]:
|
||||
if isinstance(payload, list):
|
||||
return payload
|
||||
if not isinstance(payload, dict):
|
||||
return []
|
||||
for key in ('result', 'data', 'list'):
|
||||
value = payload.get(key)
|
||||
if isinstance(value, list):
|
||||
return value
|
||||
if isinstance(value, dict):
|
||||
for inner_key in (
|
||||
'itemList', 'items', 'list', 'records', 'minutes',
|
||||
):
|
||||
inner = value.get(inner_key)
|
||||
if isinstance(inner, list):
|
||||
return inner
|
||||
return []
|
||||
|
||||
|
||||
def uuid_title_pairs_from_payload(
|
||||
payload: Any,
|
||||
) -> List[Tuple[str, str]]:
|
||||
"""列表项可为对象、JSON 字符串、或纯 taskUuid 字符串。"""
|
||||
out: List[Tuple[str, str]] = []
|
||||
for item in _unwrap_rows(payload):
|
||||
if isinstance(item, dict):
|
||||
uuid = (
|
||||
item.get('taskUuid')
|
||||
or item.get('id')
|
||||
or item.get('task_uuid')
|
||||
)
|
||||
if not uuid:
|
||||
continue
|
||||
title = item.get('title') or item.get('name') or '无标题'
|
||||
if not isinstance(uuid, (str, int, float, bool)):
|
||||
continue
|
||||
if not isinstance(title, (str, int, float, bool)):
|
||||
title = str(title) if isinstance(title, dict) else '无标题'
|
||||
out.append((str(uuid), str(title)))
|
||||
elif isinstance(item, str):
|
||||
text = item.strip()
|
||||
if not text:
|
||||
continue
|
||||
if text.startswith('{'):
|
||||
try:
|
||||
parsed = json.loads(text)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if not isinstance(parsed, dict):
|
||||
continue
|
||||
uuid = (
|
||||
parsed.get('taskUuid')
|
||||
or parsed.get('id')
|
||||
or parsed.get('task_uuid')
|
||||
)
|
||||
if not uuid:
|
||||
continue
|
||||
title = parsed.get('title') or parsed.get('name') or '无标题'
|
||||
out.append((str(uuid), str(title)))
|
||||
else:
|
||||
out.append((text, text))
|
||||
return out
|
||||
|
||||
|
||||
def run_dws(
|
||||
args: List[str], dry_run: bool = False,
|
||||
) -> Optional[Any]:
|
||||
cmd = ['dws'] + args
|
||||
if dry_run:
|
||||
print(f"[dry-run] {' '.join(cmd)}")
|
||||
return None
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd, capture_output=True, text=True, timeout=60
|
||||
)
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError) as exc:
|
||||
raise DWSCommandError(str(exc)) from exc
|
||||
if result.returncode != 0:
|
||||
detail = result.stderr.strip() or f"退出码 {result.returncode}"
|
||||
raise DWSCommandError(detail)
|
||||
try:
|
||||
return json.loads(result.stdout)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise DWSCommandError(f"DWS 返回的不是合法 JSON:{exc}") from exc
|
||||
|
||||
|
||||
def summary_text_from_payload(payload: Any) -> str:
|
||||
"""兼容当前 Runtime 的 result.fullSummary 与历史直接字段。"""
|
||||
if isinstance(payload, str):
|
||||
return payload
|
||||
if not isinstance(payload, dict):
|
||||
return ''
|
||||
inner = payload.get('result', payload)
|
||||
if isinstance(inner, str):
|
||||
return inner
|
||||
if not isinstance(inner, dict):
|
||||
return ''
|
||||
value = (inner.get('fullSummary') or inner.get('summary')
|
||||
or inner.get('content'))
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
if value is not None:
|
||||
return json.dumps(value, ensure_ascii=False)
|
||||
return json.dumps(inner, ensure_ascii=False)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='获取最近听记的 AI 摘要'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--max', type=int, default=5, help='获取条数 (默认 5)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--output', default='', help='输出到 Markdown 文件'
|
||||
)
|
||||
parser.add_argument('--dry-run', action='store_true')
|
||||
args = parser.parse_args()
|
||||
|
||||
print('🎙️ 获取听记列表...')
|
||||
list_data = run_dws([
|
||||
'minutes', 'list', 'mine',
|
||||
'--max', str(args.max),
|
||||
'--format', 'json',
|
||||
], dry_run=args.dry_run)
|
||||
|
||||
if args.dry_run:
|
||||
run_dws([
|
||||
'minutes', 'get', 'summary',
|
||||
'--id', '<TASK_UUID>', '--format', 'json',
|
||||
], dry_run=True)
|
||||
return
|
||||
|
||||
if not list_data:
|
||||
print('未找到听记')
|
||||
return
|
||||
|
||||
pairs = uuid_title_pairs_from_payload(list_data)
|
||||
if not pairs:
|
||||
print('暂无听记')
|
||||
return
|
||||
|
||||
output_lines = [f"# 最近 {len(pairs)} 条听记摘要\n"]
|
||||
for i, (uuid, title) in enumerate(pairs, 1):
|
||||
print(f" [{i}/{len(pairs)}] 获取摘要: {title}")
|
||||
|
||||
summary_data = run_dws([
|
||||
'minutes', 'get', 'summary',
|
||||
'--id', uuid, '--format', 'json',
|
||||
])
|
||||
summary_text = summary_text_from_payload(summary_data)
|
||||
|
||||
output_lines.append(f"## {i}. {title}\n")
|
||||
if summary_text:
|
||||
output_lines.append(f"{summary_text}\n")
|
||||
else:
|
||||
output_lines.append("(暂无摘要)\n")
|
||||
|
||||
full_output = '\n'.join(output_lines)
|
||||
|
||||
if args.output:
|
||||
with open(args.output, 'w', encoding='utf-8') as f:
|
||||
f.write(full_output)
|
||||
print(f"\n✓ 已输出到 {args.output}")
|
||||
else:
|
||||
print('\n' + full_output)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
main()
|
||||
except DWSCommandError as exc:
|
||||
print(f"错误:{exc}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,222 @@
|
||||
"""Minutes 示例脚本的离线 Contract 回归测试。"""
|
||||
|
||||
import io
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
from contextlib import redirect_stdout
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
|
||||
SCRIPTS_DIR = Path(__file__).resolve().parent
|
||||
SKILL_DIR = SCRIPTS_DIR.parent
|
||||
SKILL_FILE = SKILL_DIR / 'SKILL.md'
|
||||
REFERENCES_DIR = SKILL_DIR / 'references'
|
||||
if str(SCRIPTS_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(SCRIPTS_DIR))
|
||||
|
||||
import minutes_recent_summary
|
||||
from minutes_recent_summary import uuid_title_pairs_from_payload
|
||||
|
||||
|
||||
class MinutesScriptContractTest(unittest.TestCase):
|
||||
def test_reference_graph_has_no_dead_or_orphaned_files(self):
|
||||
markdown_files = [SKILL_FILE, *sorted(REFERENCES_DIR.rglob('*.md'))]
|
||||
markdown_link = re.compile(r'\[[^\]]+\]\(([^)]+)\)')
|
||||
graph = {path.resolve(): set() for path in markdown_files}
|
||||
linked_scripts = set()
|
||||
|
||||
for source in markdown_files:
|
||||
for raw_target in markdown_link.findall(
|
||||
source.read_text(encoding='utf-8')
|
||||
):
|
||||
target = raw_target.split('#', 1)[0].strip()
|
||||
if not target or '://' in target or target.startswith('mailto:'):
|
||||
continue
|
||||
resolved = (source.parent / target).resolve()
|
||||
self.assertTrue(
|
||||
resolved.exists(),
|
||||
f'dead link: {source.relative_to(SKILL_DIR)} -> {target}',
|
||||
)
|
||||
if resolved in graph:
|
||||
graph[source.resolve()].add(resolved)
|
||||
if resolved.parent == SCRIPTS_DIR.resolve():
|
||||
linked_scripts.add(resolved)
|
||||
|
||||
reachable = set()
|
||||
pending = [SKILL_FILE.resolve()]
|
||||
while pending:
|
||||
current = pending.pop()
|
||||
if current in reachable:
|
||||
continue
|
||||
reachable.add(current)
|
||||
pending.extend(graph.get(current, ()))
|
||||
|
||||
orphaned = sorted(
|
||||
str(path.relative_to(SKILL_DIR))
|
||||
for path in graph
|
||||
if path != SKILL_FILE.resolve() and path not in reachable
|
||||
)
|
||||
self.assertEqual(orphaned, [], f'orphan references: {orphaned}')
|
||||
|
||||
production_scripts = {
|
||||
path.resolve()
|
||||
for path in SCRIPTS_DIR.glob('*.py')
|
||||
if not path.name.startswith('test_')
|
||||
}
|
||||
missing_scripts = sorted(
|
||||
path.name for path in production_scripts - linked_scripts
|
||||
)
|
||||
self.assertEqual(
|
||||
missing_scripts, [],
|
||||
f'production scripts missing from references: {missing_scripts}',
|
||||
)
|
||||
|
||||
def test_list_parser_accepts_runtime_item_list(self):
|
||||
payload = {
|
||||
'result': {
|
||||
'itemList': [{'taskUuid': 'u1', 'title': '周会'}],
|
||||
},
|
||||
}
|
||||
self.assertEqual(
|
||||
uuid_title_pairs_from_payload(payload), [('u1', '周会')]
|
||||
)
|
||||
|
||||
def test_summary_accepts_runtime_full_summary(self):
|
||||
payload = {'result': {'fullSummary': '完整摘要'}}
|
||||
self.assertEqual(
|
||||
minutes_recent_summary.summary_text_from_payload(payload),
|
||||
'完整摘要',
|
||||
)
|
||||
|
||||
def test_dws_failure_is_not_treated_as_empty_result(self):
|
||||
failed = subprocess.CompletedProcess(
|
||||
args=['dws'], returncode=2, stdout='', stderr='boom'
|
||||
)
|
||||
with mock.patch.object(
|
||||
minutes_recent_summary.subprocess, 'run', return_value=failed
|
||||
):
|
||||
with self.assertRaises(minutes_recent_summary.DWSCommandError):
|
||||
minutes_recent_summary.run_dws(['minutes', 'list', 'mine'])
|
||||
|
||||
def test_summary_dry_run_never_starts_subprocess(self):
|
||||
argv = ['minutes_recent_summary.py', '--max', '2', '--dry-run']
|
||||
with mock.patch.object(sys, 'argv', argv):
|
||||
with mock.patch.object(
|
||||
minutes_recent_summary.subprocess,
|
||||
'run',
|
||||
side_effect=AssertionError('dry-run called subprocess'),
|
||||
):
|
||||
output = io.StringIO()
|
||||
with redirect_stdout(output):
|
||||
minutes_recent_summary.main()
|
||||
rendered = output.getvalue()
|
||||
self.assertIn('dws minutes list mine --max 2', rendered)
|
||||
self.assertIn(
|
||||
'dws minutes get summary --id <TASK_UUID>', rendered
|
||||
)
|
||||
|
||||
def test_phase2_execution_capsules_and_capability_boundaries(self):
|
||||
skill = SKILL_FILE.read_text(encoding='utf-8')
|
||||
intent_guide = (REFERENCES_DIR / 'intent-guide.md').read_text(
|
||||
encoding='utf-8'
|
||||
)
|
||||
minutes_reference = (REFERENCES_DIR / 'minutes.md').read_text(
|
||||
encoding='utf-8'
|
||||
)
|
||||
workflow_reference = (REFERENCES_DIR / '07-minutes.md').read_text(
|
||||
encoding='utf-8'
|
||||
)
|
||||
|
||||
for command in (
|
||||
'dws minutes +search --query "<关键词>" --scope all --page-all',
|
||||
'dws minutes +list-mine --page-all --format json',
|
||||
'dws minutes +list-shared --page-all --format json',
|
||||
'dws minutes +list-all --page-all --format json',
|
||||
'dws minutes +export-pack --id <taskUuid>',
|
||||
'dws minutes record start --dry-run --format json',
|
||||
):
|
||||
self.assertIn(command, skill)
|
||||
self.assertNotIn('+record-start --dry-run', skill)
|
||||
|
||||
self.assertIn('--pair "旧词1=>新词1"', intent_guide)
|
||||
self.assertIn('total` 是替换规则数', intent_guide)
|
||||
self.assertIn('minutes tag query --tag-id <tagId>', intent_guide)
|
||||
self.assertNotIn('+replace-batch ...', intent_guide)
|
||||
self.assertNotIn('minutes tag ...', intent_guide)
|
||||
|
||||
self.assertIn('无公开 `permission list/get/inspect` 命令', minutes_reference)
|
||||
self.assertIn('`0` | 管理员', minutes_reference)
|
||||
self.assertIn('`4` | 仅查看', minutes_reference)
|
||||
self.assertIn('verification.mode=write_ack_only', minutes_reference)
|
||||
self.assertIn('dws minutes +share --id <taskUuid>', workflow_reference)
|
||||
self.assertIn('dws minutes +unshare --id <taskUuid>', workflow_reference)
|
||||
|
||||
def test_phase3_bad_case_guidance_is_general_and_executable(self):
|
||||
skill = SKILL_FILE.read_text(encoding='utf-8')
|
||||
intent_guide = (REFERENCES_DIR / 'intent-guide.md').read_text(
|
||||
encoding='utf-8'
|
||||
)
|
||||
minutes_reference = (REFERENCES_DIR / 'minutes.md').read_text(
|
||||
encoding='utf-8'
|
||||
)
|
||||
workflow_reference = (REFERENCES_DIR / '07-minutes.md').read_text(
|
||||
encoding='utf-8'
|
||||
)
|
||||
|
||||
for fact in (
|
||||
'--direction 1',
|
||||
'等我确认后再继续',
|
||||
'dws minutes +detail --ids <uuid1,uuid2> --artifacts basic',
|
||||
'不能用当前 profile 的组织名代替每条听记的归属',
|
||||
'不得为了验证预览而执行真实写入后再还原',
|
||||
'纯能力、规则或错误说明',
|
||||
'requested/resolved/missing/artifacts/status',
|
||||
'只有 `title/basic` 时只列元数据',
|
||||
):
|
||||
self.assertIn(fact, skill)
|
||||
|
||||
self.assertIn('不查询 Help、不编造替换词', intent_guide)
|
||||
self.assertIn('逐来源证据台账', minutes_reference)
|
||||
self.assertIn(
|
||||
'不能生成摘要、关键词、主题或内容分类',
|
||||
minutes_reference,
|
||||
)
|
||||
self.assertIn(
|
||||
'dws minutes +update --id <taskUuid> --title "<目标标题>" --dry-run',
|
||||
minutes_reference,
|
||||
)
|
||||
self.assertIn('当前 profile 的 `corpName`', minutes_reference)
|
||||
self.assertIn(
|
||||
'dws minutes hot-word list --format json', workflow_reference
|
||||
)
|
||||
self.assertIn(
|
||||
'dws minutes +list-mine --page-all --format json',
|
||||
workflow_reference,
|
||||
)
|
||||
self.assertIn('不能通过真实 create 后 cancel 来伪造预览', workflow_reference)
|
||||
for case_id in (
|
||||
'0052',
|
||||
'0060',
|
||||
'0066',
|
||||
'0068',
|
||||
'0070',
|
||||
'0071',
|
||||
'0110',
|
||||
'0121',
|
||||
'0132',
|
||||
'0136',
|
||||
'0137',
|
||||
'0142',
|
||||
'0145',
|
||||
):
|
||||
self.assertNotIn(case_id, skill)
|
||||
self.assertNotIn(case_id, intent_guide)
|
||||
self.assertNotIn(case_id, minutes_reference)
|
||||
self.assertNotIn(case_id, workflow_reference)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user