Compare commits

...
Author SHA1 Message Date
南润 e4cae01603 fix(ci): isolate subprocess coverage output 2026-08-25 13:41:03 +08:00
南润 4ff4151287 Merge remote-tracking branch 'origin/main' into calendar_skill_opt2 2026-08-25 12:56:29 +08:00
南润 9497e40783 ci: rerun after PR merge ref refresh 2026-08-25 12:01:25 +08:00
南润 3f2d104b0e chore(calendar): drop unrelated local ignore 2026-08-25 11:56:35 +08:00
github-actions[bot] 3fd0d97a26 Merge pull request #1110 from DingTalk-Real-AI/codex/fix-command-typo-guidance
fix(cli): add bounded command typo guidance
2026-08-25 02:11:21 +00:00
南润 ca6f849ab2 Merge remote-tracking branch 'origin/main' into calendar_skill_opt2 2026-08-20 20:42:53 +08:00
南润 b0ea466dc5 docs(calendar): tighten multi-step execution 2026-08-20 20:42:38 +08:00
南润 021fd25678 docs(calendar): bound recovery and cleanup workflows 2026-08-20 14:08:53 +08:00
南润 4987aae380 Merge remote-tracking branch 'origin/main' into calendar_skill_opt1
# Conflicts:
#	skills/multi/dingtalk-calendar/SKILL.md
#	skills/multi/dingtalk-calendar/references/03-meeting.md
#	skills/multi/dingtalk-calendar/references/lite-recipes.md
2026-08-19 10:51:54 +08:00
南润 c8c997fff3 refactor(calendar): align skill with golden routes 2026-08-19 10:43:17 +08:00
6 changed files with 133 additions and 70 deletions
+7 -2
View File
@@ -1102,8 +1102,11 @@ jobs:
changed "$COVERAGE_BASE_REF" "$COVERAGE_HEAD_REF"
)"
impacted_output="$(
# Smoke has no production statements and launches a subprocess;
# focused Test owns it, while -cover only adds runtime stderr noise.
./scripts/ci/changed-test-packages.sh \
list "$COVERAGE_BASE_REF" "$COVERAGE_HEAD_REF"
list "$COVERAGE_BASE_REF" "$COVERAGE_HEAD_REF" |
awk '$0 != "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/test/smoke"'
)"
if [ -z "$changed_output" ] || [ -z "$impacted_output" ]; then
printf 'mode: atomic\n' > coverage.txt
@@ -1325,8 +1328,10 @@ jobs:
changed "$COVERAGE_BASE_REF" HEAD
)"
impacted_output="$(
# Keep the baseline plan symmetric with Coverage (current).
./scripts/ci/changed-test-packages.sh \
list "$COVERAGE_BASE_REF" HEAD
list "$COVERAGE_BASE_REF" HEAD |
awk '$0 != "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/test/smoke"'
)"
fi
base_worktree="$(mktemp -d "${RUNNER_TEMP}/dws-coverage-base.XXXXXX")"
+58 -58
View File
@@ -11,11 +11,14 @@ metadata:
# 钉钉日历 Skill
## 前置条件 — 执行操作前必读
## 执行契约
> **CRITICAL — 执行任何 `dws` 操作前,MUST 先用 Read 工具完整读取 [`dingtalk-shared`](../dingtalk-shared/SKILL.md)。**该轻量文件包含全局执行契约、安全底线及 shared references 的按需加载导航;不要预加载其全部 references。
> 命令参考:[calendar.md](references/calendar.md);剧本:[03-meeting.md](references/03-meeting.md)。
- 明确的 Calendar 请求直接按本 Skill 执行;仅在跨产品、profile、确认或错误恢复需要时读取 [`dingtalk-shared`](../dingtalk-shared/SKILL.md) 的对应章节。
- 已知意图直接走下方 Golden Route。只有 leaf 参数或安全语义不确定时读取单个 compact Schema,只有 Cobra flag 不确定时读取精确 leaf Help。
- 所有目标解析、读取、写入和验证使用同一 profile。`eventId`、`roomId`、`calendarId`、`userId` 只取真实返回;零命中或多候选时停止消歧。
- 写操作按 Runtime confirmation gate 执行:先解析并展示目标;需要确认时获得确认后才加 `--yes`。退出码为 0 不等于业务完成,必须检查结构化 outcome 和读回证据。
- 用户在原请求中明确要求创建、修改、邀请、取消、删除或恢复,即已授权对应步骤:第一次调用就带 `--yes --format json`,不要先故意触发 `confirmation_required`、不要插入 dry-run,也不要在“看过以后/办完后/最后删除”处再次停下来询问。只有目标或影响范围尚不明确时才追问。
- 默认只加载一个操作 Reference:通用原子命令读 [calendar.md](references/calendar.md);涉及会议室预订读 [03-meeting.md](references/03-meeting.md);遇到写后读回不一致、长分页或多步清理时改读更短的 [执行收束](references/execution-bounds.md),不要同时加载完整命令参考。
<!-- VISIBLE_SHORTCUTS_START -->
## Shortcuts(无专用脚本/recipe 时优先)
@@ -47,70 +50,67 @@ metadata:
| `dws calendar +week` | read | 列出我本周的日程(自动按周一为周首计算本周起止时间,无需手动填时间范围) |
<!-- VISIBLE_SHORTCUTS_END -->
## 意图表
## Golden Routes
| 用户说 | 命令 |
|--------|------|
| "今天 / 明天 / 本周日程" | `python scripts/calendar_today_agenda.py [today\|tomorrow\|week]` |
| "约会议(含参会人 + 会议室)" | `python scripts/calendar_schedule_meeting.py --title "<主题>" --start "<起>" --end "<止>" [--users <ids>] [--book-room]` |
| "多人共同空闲" | `python scripts/calendar_free_slot_finder.py --users <ids> --date <yyyy-MM-dd>` |
| "查闲忙" | `dws calendar busy search --users <userIds> --start "<ISO>" --end "<ISO>"` |
| "加参会人" / "订房" / "取消" | `dws calendar attendee add` / `room add` / `event delete` |
| 用户意图 | 首选入口 | 身份与边界 |
|---|---|---|
| 今天 / 明天 / 本周日程 | `dws calendar +today|+tomorrow|+week` | 当前 profile 的主日历;无需手算时间窗 |
| 任意时段日程 | `dws calendar +agenda --start "<ISO>" --end "<ISO>"` | 保留 `eventId` 和分页证据;`hasMore` 时继续翻页 |
| 按标题、描述或地点找日程 | `dws calendar +search-event --query "<关键词>"` | 单页零命中且 `hasMore=true` 不是全局零命中 |
| 创建个人日程或按姓名约人 | `dws calendar +book --title "<主题>" --start "<ISO>" --end "<ISO>" [--with "张三,李四"]` | 姓名必须唯一解析;写后读回;不含会议室预订 |
| 查看最近一场 | `dws calendar +next-event` | 默认未来 7 天;不要用它代替完整列表 |
| 查某人 / 会议室闲忙 | 姓名用 `+free`;ID 用 `+freebusy` | 必须有明确时段;至少指定 users/rooms 一类 |
| 推荐多人共同时间 | `dws calendar +suggest-time --with "张三,李四" --start "<ISO>" --end "<ISO>"` | 只推荐,不创建日程 |
| 找可用会议室 | `dws calendar +room-find --start "<ISO>" --end "<ISO>"` | 名称定位但不查可用性时才用 `+room-search` |
| 邀请参会人 | `dws calendar +invite --event <EVENT_ID> --with "张三,李四"` | 只修改指定已有日程 |
| 改期 | `dws calendar +reschedule --event <EVENT_ID> --start "<ISO>" --end "<ISO>"` | 只改起止时间;其他字段保持不变 |
| 取消日程 | `dws calendar +cancel-event --event <EVENT_ID>` | 高风险删除;先读目标,确认后执行并验证不存在 |
## 标准 SOP(必遵流程)
当用户需要 shortcut 未公开的字段、共享日历、循环规则、附件、ACL 或会议室绑定时,才降级到 [calendar.md](references/calendar.md) 的单个原子 leaf。
> 命中以下意图**必须**按对应 SOP 顺序执行;**禁止**跳步、替换命令、编造 userId/eventId。每条命令必须带 `--format json`,时间参数**必须**是 ISO-8601(如 `2026-07-03T14:00:00+08:00`)。
## 最短完成路径
### SOP-1 查日程(list-events)
- 按用户验收项维护最小资源台账;后续更新、恢复、读回和清理只用同一创建回执的 `eventId/roomId/aclId`,禁止从宽泛列表换用相似旧资源。
- 已知路由直接执行;仅在 `unknown flag/command` 后查一次精确 leaf Help并最多修正一次。`+book` 只覆盖标题、时间和姓名;需地点、忙闲、循环、提醒或创建时订房时直接用 `event create`。
- 原请求已明确“办完后删除/取消/恢复/撤销”即已授权收尾,到达该步通过 Runtime gate 加 `--yes`,不重复询问;任务中途失败或预算将尽也先清理本轮临时资源。
- 用户要求“把详情/名单/规则/分享信息给我”时,清理前先读回并保存这些验收字段;随后完成清理,最终答复同时呈现保存的业务信息和清理终态。不要因为资源已删除而只回答“已删除”。
- 只保留下一步所需的 ID、游标、时间、状态和验收字段,不把完整列表或原始 JSON 反复送回上下文。
**触发**:今天/明天/本周日程/我有什么会/某时段日程。
## 有限搜索与 Token 预算
1. **首选脚本(必须)**:`python scripts/calendar_today_agenda.py today|tomorrow|week`(聚合今日议程)。
2. **降级 CLI(必须)**:脚本不可用时 `dws calendar event list --start "<起始ISO>" --end "<结束ISO>" --format json`;不传 `--start/--end` 默认查今天(00:00:00~23:59:59)。`hasMore=true` 用 `--limit`/翻页。
3. **解析(必须)**:取真实 `eventId`、`attendees[]`、`start/end`;按需抽取,**禁止**把整段 JSON 原样贴出。
- 标题定位先用合理时间窗和精确关键词;普通定位限 1 个时间窗、2 个关键词、2 页,未唯一命中即收束。仅用户明确要求完整遍历时翻到 `hasMore=false`,优先最大 `--limit`;指定小页长则本地循环聚合匹配/计数/去重与最终分页证据。
- 不试探不存在的 flags,不用同义词扫描数十页,不逐间重复 busy 查询。前置资源不存在、系统日历不可修改或绑定被拒时直接报告边界;详细预算与 commit-unknown 协调见 [执行收束](references/execution-bounds.md)。
**禁止**:用 `event list` 替代闲忙查询(查闲忙走 SOP-3)、编造时间窗口、用非 ISO 时间格式。
## 资源与安全约束
### SOP-2 建日程(create-event)
- 时间必须是带时区的 ISO-8601,且 `end > start`。预约意图缺少起止时间时先追问;不要自设全天窗口。`+today/+tomorrow/+week` 和自身声明默认窗口的只读 shortcut 除外。
- 多轮任务持续复用同一 `eventId`;更新、邀请、订房和取消不得通过再次创建来替代。
- 用户点名日程但未给 `eventId` 时,用标题和合理时间窗检索;零命中停止,多候选列出标题、时间和 `eventId` 让用户选择,禁止选第一条。
- 用户姓名必须唯一解析到当前 profile 的身份;零命中、多候选或跨 profile 不复用 ID。
- 多人任务建立 `输入姓名 -> userId -> displayName` 一一映射:不同姓名必须得到不同 userId,且被邀请人不得误解析为当前组织者;发现重复、别名碰撞或返回姓名不符时停止,不得继续荐时或建会。读回参会人时按 userId 核对每一位,不能只看人数或展示名。
- `roomId` 只能来自同一 profile、同一目标时段下的 `+room-find` / `room search` 返回。会议室名、楼层编号和地点文本都不是 `roomId`;`--location` 也不等于预订。
- 用户指定时间或会议室时不得擅自换时间、换房或扩大地点范围。允许范围无空房时停止并说明,若要继续必须让用户明确放宽条件。
- 分页必须跟随 `nextCursor` 或 page 语义,检测 cursor 丢失、停滞和循环;不得把第一页当全量。
- 非事务多步写入发生 partial、pending 或 commit-unknown 时如实报告已完成与未完成步骤,先读回协调,禁止盲目重试非幂等创建。
- 创建回执失败但包含 `eventId`,或报 `readback_attendee_missing` / 读回字段缺失时,视为“可能已提交”:先对该 `eventId` 做详情与参会人协调;只有回执没有 ID 时才用精确标题+起止时间唯一定位。不得再次创建,也不得借用当天列表中的其他 eventId。
- 推荐时间必须核对返回候选的实际时长等于用户要求的分钟数;无足够时长就明确无可用候选,禁止把 30 分钟候选表述成 60 分钟。
- “找时间并创建”必须把 `--duration <分钟>` 传给荐时入口,且只能选择返回区间完整容纳该时长的候选;没有合格候选就报告前置条件不满足,不得自行缩短会议。
**触发**:建日程/约会议/加日程。
## 写后验证
1. **解析与会人(必须)**:对每个姓名 `dws aisearch person --query "<姓名>" --dimension name --format json` 取 `userId`,多人逗号拼接。
2. **执行(必须)**:`dws calendar event create --title "<主题>" --start "<ISO>" --end "<ISO>" --attendees <userId1,userId2> --format json`(按需加 `--location`/`--desc`/`--rooms`)。
3. **验证(必须)**:从返回 `result.id` 取日程 ID(下游参数语义称 `eventId`),再执行 `dws calendar event list --start "<ISO>" --end "<ISO>" --format json` 复核标题、描述和时段。
- 创建:以结构化结果中的 `eventId` 为身份,读回核对标题、起止时间和预期参会人。
- 创建命令报告读回失败但同一 `eventId` 的详情已存在时,分别记录“事件已创建”和“哪些字段尚未验证”;补做缺失字段的精确读回,不把整个创建当作可安全重放。
- 邀请 / 移除参会人:读取参会人列表核对目标;底层不提供稳定 userId 时不得伪造身份字段。
- 改期 / 更新:读回同一 `eventId`,核对实际变更字段并确认未意外覆盖其他字段。
- 订房 / 换房:读回同一日程,并用对应时段的会议室闲忙或日程详情确认绑定;仅有空响应不能证明成功。
- 取消:读回明确为不存在;权限错误、超时或缺少不存在证据时不得报告成功。
- 清理优先级高于附加汇总:任务后段出现查询失败、订房失败或 Token/时间预算将尽时,先清理本轮台账中的所有临时资源,再报告未完成项。每个删除必须使用其对应创建回执的 ID。
**禁止**:跳过与会人 userId 解析直接传姓名、编造会议室 roomId。
## 产品边界
### SOP-3 查闲忙(check-busy)
- 视频会议发起、入会链接、会中控制:当前 Calendar CLI 不支持,不能臆造 conference 命令。
- AI 听记、会后摘要与转写:转 `dingtalk-minutes`;待办任务与独立截止提醒:转 `dingtalk-todo`。
- 按姓名解析人员由 Calendar 的 `+book/+invite/+free/+suggest-time` 优先完成;只有原子命令路径才先用 `dingtalk-aisearch`。
- 日历事件是 Calendar 资源;“给自己留时间块”不是 Todo。
**触发**:某人/会议室是否有空/找空闲时段/避免冲突。
1. **解析对象(必须)**:姓名 → `dws aisearch person --query "<姓名>" --dimension name --format json` 取 `userId`;会议室用 `roomId`。
2. **收敛时段(必须)**:`--start`/`--end` **必须**由用户给出或明确收敛;时段不明确**必须先追问**,**禁止**默认全天窗口。
3. **执行(必须)**:`dws calendar busy search --users <userId1,userId2> --start "<ISO>" --end "<ISO>" --format json`(查会议室换 `--rooms <roomId...>`,可同时传)。**禁止**用 `event list` 扫日程替代闲忙查询。
4. **空闲时段(必须)**:找共同空闲用 `python scripts/calendar_free_slot_finder.py`。
**禁止**:用 `event list` 冒充 `busy search`、未确认时段就默认全天查询。
## 执行硬约束
- 多轮日程任务必须保留 `eventId`,后续加人、移人、订房、换房、改描述、删除都基于同一个 `eventId` 执行;不要重新创建重复日程。
- 用户明确说"帮我订一个空闲会议室"时,`room search` 返回可用会议室后直接选择第一个可预订且不需要自定义审批的 `roomId` 执行 `room add`;不要把选择权抛回用户导致任务停住。
- 已有日程订房:`dws calendar room search --start ... --end ... --format json` → `dws calendar room add --event <EVENT_ID> --rooms <ROOM_ID> --format json` → `event get` 或 `room/busy` 验证。
- 换会议室:先 `room delete --event <EVENT_ID> --rooms <OLD_ROOM_ID>`,再 `room add --event <EVENT_ID> --rooms <NEW_ROOM_ID>`,最后回查;不要只更新 `--location`。
- 参会人变化用 `attendee add/delete`,日程描述变化用 `event update --desc`,删除日程用 `event delete --id`。用户当前消息已明确要求删除/取消时可直接执行;否则先确认。
- 脚本失败或参数不完整时,立即降级到明确的 `dws calendar event/attendee/room` 命令,不要停在"我要查看用法"。
- 所有 dws 命令带 `--format json`;查询时间必须显式 `--start` / `--end`。
## 跨产品协作
- 视频会议发起 / 入会链接 / 邀请入会 / 会中控制 → 当前 CLI **不支持**;请在钉钉客户端完成
- 会后摘要 / 待办 → 切到 `dingtalk-minutes`
- 参会人按人名 → 先用 `dingtalk-aisearch` 解析
## 注意
`schedule-meeting` 必须读 [03-meeting.md](references/03-meeting.md) 中的「两准则」「搜房失败硬门禁」,禁止假设 `roomId`。
## 局部意图与短流程
- [局部意图消歧](references/intent-guide.md);[短流程](references/lite-recipes.md)。
涉及“创建日程并订会议室”的组合流程时读取 [03-meeting.md](references/03-meeting.md),严格执行时段、地点、`roomId` 来源与失败收束规则。
@@ -14,11 +14,11 @@
**`room search --available`**(与传入的 `--start` / `--end` 配对):返回的是在**该整段时段内**可被预订的空闲会议室(不是「有一段空就算」);脚本与用户手工选房均应沿用同一时间窗,避免误以为分段凑满即等价于整段可用。
**`dws calendar room search` 合法参数**(与 [calendar.md](./calendar.md) 一致):仅 `--start`、`--end`、`--group-id`(可选)、`--available`(可选)、`--format json` 等;**禁止使用 `--query`**,否则会报 `unknown flag: --query`。
**会议室名称参数**:shortcut 使用 `dws calendar +room-find --room-name "<核心专名>"`;原子命令使用 `dws calendar room search --room-name "<核心专名>"`。两者都不支持 `--query`。只知道名称、无需检查时段可用性时用 `+room-search --room-name`。
**地点归组早停**:若用户给的是同一地点范围(如“西溪园区 C6 楼 3-5 层”或具体楼层/楼栋),先用 `room list-groups` 找到**最相关的承载 group**(通常是该楼层;若楼层下无会议室则为直接挂会议室的上一级)。在这个最相关 group 下查不到有效 `rooms[].roomId` 或空房时,**不得**再跳去别的同级/异地 group 继续搜;同一地点的会议室不会散落在别的 group 里。只有用户明确放宽到别的楼层、楼栋或园区,才能重新解析新的 group 并继续。
**用户点名具体会议室(如「C6-4-06-N / 贡嘎山」)**:**不要**尝试 `room search --query "<名称>"`;**禁止**把用户原文(含「C6-4-06-N 贡嘎山」整句)或展示名当作 `room add --rooms` 的 `roomId`。用户输入**几乎从不会是**有效 `roomId`。须先 `dws calendar room list-groups` 定位所在楼层/分组的 `group-id`,再 `dws calendar room search --start "<ISO>" --end "<ISO>" --group-id <GROUP_ID> [--available] --format json`,在返回 `rooms[]` 中对 `roomName`、`name` 等与用户表述匹配,**仅**取 JSON 里的 `roomId`(典型为小写十六进制串,长度以返回为准),最后 `dws calendar room add --event <eventId> --rooms <roomId>`。该时段无匹配或房间忙 → 如实告知;**禁止**为通过校验而编造、拼接或猜测 `roomId`。
**用户点名具体会议室(如「C6-4-06-N / 贡嘎山」)**:**不要**尝试 `--query`;**禁止**把用户原文或展示名当作 `room add --rooms` 的 `roomId`。用 `dws calendar +room-find --room-name "<核心专名>" --start "<ISO>" --end "<ISO>" [--group-id <GROUP_ID>] --format json`,在返回会议室中按名称匹配,**仅**取真实 `roomId`,最后 `dws calendar room add --event <eventId> --rooms <roomId>`。该时段无匹配或房间忙则如实告知;禁止编造、拼接或猜测 `roomId`。
### 搜房失败硬门禁(园区/范围搜尽仍无 roomId)
@@ -52,5 +52,12 @@
| Recipe | 行动指南(固定路线) |
| ------------------ | ------------------- |
| schedule-meeting | **见上文「两准则」**、**「搜房失败硬门禁」**。**未给时段且仅说「发起/开个会」**→ 不走本 recipe;当前 CLI 不支持实时视频会议,告知用户请在钉钉客户端操作。**未给时段但有预约意图**("安排""约""定"等词):追问具体开始/结束时间。**已有时段后**,按固定顺序执行:1. `dws calendar event create` 建日程;2. 有参会人则 `dws calendar participant add`;3. 再处理会议室。**无明确会议室范围**:可直接 `python scripts/calendar_schedule_meeting.py --title "<主题>" --start "<起始>" --end "<结束>" [--users <userIds>] [--book-room] [--dry-run]`。**有明确范围(某楼/层)**:先 `dws calendar room list-groups`,锁定该地点**最相关的承载 group**;若只有一个地点,`--room-group-id` 应只传这个最相关 group,**不要**把同楼内多个楼层 group 打包传入碰运气。只有用户明确给出多个允许地点时,才把这些 `group-id` 一并传给 `python scripts/calendar_schedule_meeting.py ... --book-room --room-group-id "<id1,id2,...>"`。**用户点名具体会议室**:须手工 `dws calendar room search --start "<ISO>" --end "<ISO>" --group-id <GROUP_ID> [--available] --format json`(**无** `--query`),在 JSON 中匹配名称取 **`rooms[].roomId` 唯一真值** → `dws calendar room add --event <eventId> --rooms <roomId>`;**不得**把用户输入的会议室名当 `roomId`。**一旦连续 2 次空结果 / 任意一次 `roomId invalid`**:**必须回读本节并立即收束判断**;若整园/限定范围内搜尽仍无 roomId 或无空房 → **下一条消息必须直接向用户汇报失败结论**;否则只能向用户确认是否放宽范围/改时间。**禁止**假设 roomId、禁止无 ID 调用 `room add`、禁止用日程详情绕路、禁止继续猜测 Mock/测试环境。细则见「会议室搜索早停」。 |
| schedule-meeting | **见上文「两准则」**、**「搜房失败硬门禁」**。**未给时段且仅说「发起/开个会」**→ 当前 CLI 不支持实时视频会议,告知用户请在钉钉客户端操作。**未给时段但有预约意图**:追问具体开始/结束时间。**已有时段后**:1. 先用 `+room-find` 在用户允许范围内取得真实 `roomId`(用户没要求会议室则跳过);2. 用 `+book` 创建日程并按姓名邀请参会人;3. 有 `roomId` 时用 `room add --event <eventId> --rooms <roomId>` 绑定;4. 读回同一 `eventId` 验证。用户点名会议室时给 `+room-find` 传 `--room-name`,不得使用 `--query`。连续 2 次空结果或任意一次 `roomId invalid` 时立即收束;若限定范围内无 roomId 或无空房,直接汇报失败,除非用户明确放宽范围或改时间。 |
| reschedule-meeting | 1. `calendar event list --start "<起始ISO>" --end "<结束ISO>"` → 取 `eventId` 2. `calendar event update --id <eventId> --start "<新起始ISO>" --end "<新结束ISO>"` 更新时间 3. `chat search --query "<群名>"` → 取 `openConversationId` → `chat message send --conversation-id <openConversationId> --content "<变更通知>"` 通知变更 |
### 换期、换人、换房组合流程
1. 在写入前解析旧/新参会人的不同真实 `userId`,并分别为原时段和新时段各取得一个真实可用 `roomId`;两间房必须不同。任一前置缺失即收束,不要创建半成品。
2. 创建一次日程并记录唯一 `eventId`,加入旧参会人和原房间后读回。
3. 对同一 `eventId` 依次改期、移除旧人、加入新人、移除原房、加入新房;每步只用台账中的 ID,不重新按标题搜索。
4. 最终一次读回同时核对新时间、新参会人和新会议室,再用同一 `eventId` 删除并验证。若中途失败,保存真实 partial 状态并优先清理本轮已创建资源。
@@ -0,0 +1,47 @@
# Calendar 执行收束
仅在写后读回不一致、长分页或多步清理时加载本文件。命令参数本身不确定时改读精确 leaf Help,不加载完整 `calendar.md`。
## 写入可能已提交
`readback_attendee_missing`、`readback_*_missing`、读回超时或失败不等于创建未发生。
1. 若结构化结果/错误中有 `eventId`,只协调该 ID:`event get`,需要时再 `attendee list`。
2. 若没有 ID,用原始 `title + start + end` 在窄时间窗唯一定位一次。
3. 唯一命中后把 ID 写入本轮台账并继续;零命中报告创建未证实;多命中停止消歧。
4. 禁止重放非幂等创建,禁止从同日列表选择别的事件,禁止把部分验证写成完整成功。
## 长分页
- 普通目标定位:1 个时间窗、最多 2 个关键词、最多 2 页。
- 用户明确要求完整遍历:优先 `--limit 100`。用户指定小页长时,在本地循环中仅累计 `eventId` 去重集合、匹配项、月度/状态计数和最终 `hasMore/nextCursor`;不要把每页完整事件送回模型。
- 每次检查游标非空、前进且未重复;缺失、停滞、循环、页数或耗时上限触发时标记未完成。
- 写任务在进入分页前就记录所有临时 ID;无论正常结束、超时还是分页失败,都先执行授权过的清理并逐个验证。
- 用户强制指定小页长时,照指定页长遍历,但先预留清理调用预算;每页到达后立即用 `eventId` 集合去重并只保留目标命中与计数。接近执行上限仍未到 `hasMore=false` 时,先清理本轮临时资源,再如实报告分页未完成,禁止让清理被循环挤掉。
- 会议室可用性优先采用同一时段 `+room-find` / `room search` 的返回集合,不再逐间查询 busy;明确要求逐房间占用详情时按接口批量上限分批,失败只缩小批次一次。
## 多步资源台账
按用户验收顺序推进,始终保持这些绑定:
```text
created eventId -> title/start/end -> attendee IDs -> roomId
added aclId -> userId/privilege
uploaded fileId -> attached eventId
```
- 更新、换人、换房、恢复、删除只能引用同一行资源。
- 替换参会人:先确认旧/新 userId,再对同一事件移除旧人、添加新人、读回名单。
- 换会议室:先为新时段取得另一个真实可用 roomId,再从同一事件移除旧房、添加新房并读回;任一步失败时保留真实 partial 状态。
- 用户已在原请求明确要求最终清理时,不再二次询问;通过 Runtime confirmation gate 执行 `--yes`。
- “看过以后/确认后/办完后删除”描述的是同一请求内的顺序,不代表等待下一轮确认。先把详情、名单、实例、分享信息等验收字段保存为摘要,随后立即删除并验证;最终同时交付摘要与删除证据。
- `+suggest-time` 后核对候选实际分钟数;不足用户指定时长就报告无合格候选,不把短候选扩写成长时段。
## 立即收束条件
- `primary`/系统日历更新返回 `can't update system calendar`。
- 精确标题邀请在有限搜索后不存在。
- 用户限定范围内没有合法可用 `roomId`,或绑定返回业务拒绝。
- 必要参会人身份零命中/多候选。
触发后不要扩大范围、换无关资源或编造完成。若本轮已创建临时资源,先清理,再报告已完成、未完成与证据边界。
@@ -6,20 +6,20 @@
### list-today-meetings
**优先**:`python scripts/calendar_today_agenda.py [today|tomorrow|week]`
备选:`dws calendar event list --start "<今日起始ISO>" --end "<今日结束ISO>"`(须加 `--format json`)
**优先**:`dws calendar +today|+tomorrow|+week --format json`
任意时段:`dws calendar +agenda --start "<起始ISO>" --end "<结束ISO>" --format json`
### check-users-busy
查询多人在某时段内的闲忙(**busy**,不是用 `event list` 扫日程):
1. 解析用户:对每个姓名执行 `aisearch person --query "<姓名>" --dimension name` → `userId`;多人将 `userId` 用英文逗号拼接(无空格或按 [calendar.md](./calendar.md) `busy search` 要求)。
2. 确认时段:用户须给出或可收敛为明确的 `--start` / `--end`(ISO-8601);若未给出,**先追问**起止时间,禁止用任意默认全天窗口代替用户意图。
3. 执行:`dws calendar busy search --users <userId1,userId2,...> --start "<ISO>" --end "<ISO>" --format json`
1. 确认时段:用户须给出或可收敛为明确的 `--start` / `--end`(ISO-8601);若未给出,先追问起止时间。
2. 按姓名查一人:`dws calendar +free --who "<姓名>" --start "<ISO>" --end "<ISO>" --format json`。
3. 多人共同时间:`dws calendar +suggest-time --with "张三,李四" --start "<ISO>" --end "<ISO>" --format json`。
4. 已有 userId/roomId:`dws calendar +freebusy --users <userIds> --rooms <roomIds> --start "<ISO>" --end "<ISO>" --format json`;users/rooms 至少一类。
详见 [calendar.md](./calendar.md) 中「查询用户闲忙状态」。
### start-conference
> 当前 CLI 不提供视频会议(conference)发起/入会/会中控制能力。触发「发起会议」「开个会」「创建会议」且**没有给出具体时间**时,不要构造 `conference` 命令;直接告知用户请在钉钉客户端操作。
+5 -1
View File
@@ -467,7 +467,11 @@ func runCLIWithTimeout(t *testing.T, env []string, timeout time.Duration, args .
defer cancel()
processArgs := append([]string{"-test.run=^TestCLIHelperProcess$", "--"}, args...)
cmd := exec.CommandContext(ctx, os.Args[0], processArgs...)
cmd.Env = env
// When this package is run with -cover, the helper process is itself a
// coverage-instrumented test binary. Give it a writable output directory so
// the Go runtime does not append a GOCOVERDIR warning to the CLI's JSON
// stderr and make otherwise valid structured errors fail to decode.
cmd.Env = append(append([]string(nil), env...), "GOCOVERDIR="+t.TempDir())
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr