feat: 新增企业微信助手 Agent,并修正 wecom-cli 条目 ref 漂移 (#112)

## 概述 / Overview

两件事:新增「企业微信助手」Agent(自带 15 个技能),并修正 `wecom-cli` 条目钉在 6 月快照的 ref 漂移。

Two changes: adds the **WeCom Assistant** agent (bundling 15 skills),
and fixes the `wecom-cli` entry whose pinned ref was stuck on a June
snapshot.

## 1. 新增企业微信助手 Agent

覆盖企业微信 **14 类服务、95
个方法**:消息、群聊历史、通讯录、日程、会议、待办、邮件、在线文档、在线表格、智能表格、智能文档、文档管理、微盘、媒体文件。

**采用内联形态 + 自带私有技能**:Agent 安装对 `agents/<id>/` 整目录递归复制且 `skills/`
不在排除集合里,因此装 Agent 即带全部技能,用户无需再单独获取技能合集。

### 技能集(15 个,约 5000 行)

- 基于上游 [wecom-cli](https://github.com/WecomTeam/wecom-cli) 官方
Skill(MIT,© WecomTeam)改写,每个技能末尾保留归属声明
- **新增 `wecom-chat`**:补齐上游零覆盖的群聊历史读取
- 补齐上游未覆盖的 `message.send`、`doc.create`,方法覆盖达 **95/95**
- 修正上游三处文档漂移:邮件能力描述与实际相反、会议室参数名已过时、`title_highlight` 字段不存在

### 相对上游的核心增量:风险治理

- 26 个对外可见或不可逆的方法逐个写明**执行前确认要求**
- 4 个条件升级方法给出**参数级判据**,而非按方法名一刀切
- 文档权限扩散两项加重处理,涉及**企业外可见**时单独再确认一次
- 内部标识禁止外露,不因用户索要而放宽
- 拒绝导出可识别到具体自然人的隐私字段

### 三条真机实测得出、上游未覆盖的硬约束

1. 机器人**只能写入/修改自己创建的数据**,真人创建的只能读
2. 每次响应携带的 `extra_identity_context` **禁止透露给用户**
3. 权限错误(`850002`/`851008`/`853006`)**不得重试**,须将 `help_message`
**逐字原样**转给用户

## 2. 修正 wecom-cli 条目 ref 漂移

`source.ref` 原钉在 2026-06-28 的 `72e14f7`,该快照只有 7
个子技能且用已废弃的旧命名(`msg`/`schedule`)。上游 v1.2.0 已扩展到 **14 个**技能。按旧 ref
安装的用户拿到的是三个月前的快照。

- `source.ref` → `78c514b2afee7c0d3d7be715628478421f37ee63`
- `children` 由 `scripts/gen-collection-children.py` 重新生成,**7 → 14**
- sidecar 同步 `provenance.content.ref`、`childCount` 与 `children`

## 验证 / Verification

**静态**
- 215 条示例命令追加 `--dry-run` 实跑,**215/215 退出码 0**
- 未知方法 0、未知参数 0、`--json` 未知字段 0、枚举违规 0
- 15 个 `SKILL.md` 的 frontmatter 经客户端 `skillFrontmatterSchema` 校验全部通过
- `validate_catalog_metadata.py --require-complete` 与
`gen-collection-children.py`:**0 error**

**真机(在真实企业微信账号上端到端)**
- **待办域 6/6 方法全通**(含 2 个 write-high),`items` 必填的隐蔽坑实测证实
- **日程域 5 个方法全通**(含 3 个 write-high)
- 消息发送、通讯录解析、微盘列表、邮件搜索、文档搜索、会议列表、智能表格创建均已实测通过
- 测试数据已全部清理,未污染真实账号

**尚未实测**:群聊历史(机器人未开通该品类)。相关文档已明确标注验证状态,未实测的能力不写「实际效果」段落。
This commit is contained in:
2026-09-03 03:50:00 -04:00
committed by GitHub
parent c83f917901
commit aec2e7c28b
57 changed files with 16893 additions and 45 deletions

View File

@@ -0,0 +1,382 @@
---
name: wecom-calendar
description: >-
企业微信日程与会议室管理:预约/查看/搜索/改期/取消日程,查多人共同空闲时段,查办公楼与会议室可订性并预订会议室。
当用户说「约个日程 / 明天有什么安排 / 我的日历 / 项目评审是什么时候 / 挪一下时间 / 这个不开了 /
张三什么时候有空 / 大家什么时候都有空 / 订个会议室 / 1605 空不空 / 公司有哪些楼」时使用。
只负责『日程』——不含在线会议链接的安排(含纯线下面对面碰头);用户要的是含会议号/入会链接的『在线会议』时改用 wecom-meeting。
用户只说「开会/约个会/xx 会」而未说明是日程还是会议时,创建场景必须先逐字追问这一句、不得改写:
`需要创建日程还是会议?(请回复:日程 / 会议)`(禁止改成「在线会议/视频会议/线下会议/日程安排」等任何变体);
查询场景则严禁追问,日程与会议两边都查再合并。
不负责待办事项wecom-todo、姓名转 useridwecom-contact、发消息通知wecom-message
version: 1.0.0
type: procedural
risk_level: high
status: enabled
tags:
- wecom
- calendar
- schedule
- meeting-room
---
# 企业微信日程与会议室
帮用户把「什么时候、和谁、在哪儿」这件事落到企业微信日历上:约日程、看安排、找时间、订会议室、改期、取消。
> **前置**:执行任何 `wecom-cli` 命令前,必须先完成 `wecom-shared` 的前置检查
> CLI 已安装、版本达标、`auth show --status` 返回 `authorized`;具体版本门槛以 `wecom-shared` 为准)。
> 未通过前置检查时不得执行本技能任何命令。
## 能力清单
| 能力 | 命令 | 风险 |
|---|---|---|
| 查某段时间的日程列表 | `wecom-cli calendar schedules list` | read |
| 按关键词/组织人/参与人搜索日程 | `wecom-cli calendar schedules search` | read |
| 按 ID 批量取日程详情 | `wecom-cli calendar schedules get` | read |
| 查多人共同空闲时段 | `wecom-cli calendar schedules free list` | read |
| 查企业办公楼清单 | `wecom-cli meeting rooms buildings list` | read |
| 查会议室可订性 | `wecom-cli meeting rooms search` | read |
| 创建日程(可邀请参与人、可占会议室) | `wecom-cli calendar schedules create` | **write-high** |
| 更新日程(改时间/地点/人/会议室) | `wecom-cli calendar schedules update` | **write-high** |
| 取消(删除)日程 | `wecom-cli calendar schedules cancel` | **write-high** |
> 会议室与办公楼查询虽然命令前缀是 `meeting`,但**归本技能**`wecom-meeting` 要订会议室须反向调用本技能)。
### 三个高风险方法的确认要求
> ⚠️ **高风险操作**`calendar schedules create` 带 `attendees` 时会向他人发出日程邀请,对方日历上立刻出现这条安排;传 `meeting_room_id` 时会真实占用会议室。执行前必须向用户复述
> 「将创建日程「<主题>」,时间 <开始>-<结束>,邀请 <人名列表>,会议室 <会议室名>」并取得明确同意;用户未明确同意时不得执行。
> ⚠️ **高风险操作**`calendar schedules update` 改时间/地点/参与人/会议室会通知全体参与人,且被移除的人会直接失去这条日程。执行前必须向用户复述
> 「将把日程「<主题>」的 <改动项> 改为 <新值>,参与人会收到变更通知」并取得明确同意;用户未明确同意时不得执行。
> ⚠️ **高风险操作**`calendar schedules cancel` 会删除日程并通知全体参与人CLI **没有任何恢复接口**。执行前必须向用户复述
> 「将取消日程「<主题>」(<时间>),参与人会收到取消通知,且无法撤回」并取得明确同意;用户未明确同意时不得执行。
## 日程 vs 会议消歧 [CRITICAL措辞逐字固定]
企业微信里「会」有两种载体,判据只有一条:
- **含会议号(`meeting.meeting_code`/ 入会链接(`meeting.meeting_link`)的是「会议」** → 归 `wecom-meeting`
- **不含会议号与入会链接的是「日程」**(包括纯线下面对面碰头、订了会议室的线下会)→ 归本技能
`search` / `list` / `get` 返回的每条日程都带 `meeting` 字段,**直接读 `meeting.meeting_code` 是否非空即可判定,不需要额外补一次 `get`**。
### 规则一:创建场景必须逐字追问
用户只说「开会 / 约个会 / 安排个会 / xx 会 / xx 会议」等而未明确是日程还是会议时,**必须先用文字追问**,问题与选项**逐字固定、不得改写、不得增减、不得翻译**
```
需要创建日程还是会议?(请回复:日程 / 会议)
```
- 用户答「日程」→ 留在本技能,走「场景:约一个日程」。
- 用户答「会议」→ 转 `wecom-meeting` 创建会议(创建会议会自动生成对应日程,**不要**在本技能再建一条)。
- 「会议」「会」「开会」这些词**本身不构成「明确」**,禁止因 query 里出现「会议」二字就默认创建日程,也禁止反向默认成会议。
- **只给了地点或会议室号**(「在 1605 开会」「订个会议室开会」)**也不构成明确** —— 会议室里同样可能要远程接入,仍须追问。
- 只有出现「碰个面 / 创建日程 / 面对面聊」等纯线下信号时才直接留在本技能;出现「入会链接 / 会议号 / 视频会议 / 远程参会 / 外地同事接入」等信号时直接转 `wecom-meeting`,都无需追问。
### 规则二:查询场景严禁追问,两边都查再合并
查询场景**严禁**用上面那句话追问(那句话只用于创建)。按两个独立维度处理:
**维度一 —— 查哪一边**
| 用户表述 | 动作 |
|---|---|
| 明确提到「在线会议 / 视频会议 / 入会链接 / 会议号 / 腾讯会议 / 远程参会」 | 只查会议(转 `wecom-meeting` |
| 明确说「日程 / 安排 / 我的安排 / 日历 / 今天有什么安排」且无在线会议特征 | 只查日程(本技能) |
| 模糊表述:「会 / xx 会 / xx 会议 / 开会 / 最近有什么会 / 有哪些会 / 找下 xx 会议」 | **日程和会议两边都查**,再合并 |
**维度二 —— 每一边用 `search` 还是 `list`(与维度一独立,逐边各判)**
- 有**主题/名称关键词**(「项目评审是什么时候」「找下 xx 会」)→ 该边用 `search`,关键词进 `keywords`
- **只有时间/日期或泛浏览**(「今天有什么安排」「最近有什么会」)→ 该边用 `list`
**禁止把日期当 `keywords` 喂给 `search`。**
**合并展示**:两边都查时,按是否含在线会议链接分成「(会议)」与「(日程)」两部分(日程中 `meeting.meeting_code` 非空的归「(会议)」),同一场按「主题 + 时间」去重只保留一条,末尾汇总「共 N 场,其中会议 X 场、日程 Y 场」。只有一类时不分部分、不加小标题。
### 规则三:改约禁止拆成 cancel + create
「改约 / 改时间 / 挪到 / 顺延 / 重新约」等改期意图,**即使用户说「取消……再约到……」也算改期**,一律走 `update`
1.`search``list` 定位,直接读返回里的 `meeting.meeting_code`
2. `meeting_code` **为空**(纯日程)→ `calendar schedules update` 改时间。
3. `meeting_code` **非空**(会议形态日程)→ 转 `wecom-meeting`,把 `meeting.meeting_id` 传给 `meeting update`**无需再 search 一次**。
> **根因**`calendar schedules create` 只能建纯日程、**重建不出会议链接**能拆不能合。cancel + create 会让**会议链接永久丢失**,参与人拿到的旧链接全部作废。这条禁令没有例外,不得以「用户自己说要先取消」为由绕过。
## 场景:约一个日程
### 步骤
1. **消歧**(见上文规则一)。确认是「日程」后继续。
2. **补必填参数**`subject` / `begin_time` / `end_time` 缺失,或参与人无法从上下文推断时,用文字询问;其余可选参数(地点、提醒)用户没提就走默认,不专门问。
- `end_time` 用户没给 → 默认 `begin_time + 1 小时`,不追问。
- 询问时间时候选必须是**精确到分钟的具体时刻**(「明天 14:00」「周六 10:30」禁止给「上午 / 下午 / 下班前」这类模糊选项。
3. **姓名 → userid**:调 `wecom-contact` 解析,多候选时列 2~4 个(姓名 + 部门)让用户选。**禁止**把姓名当 userid 拼接,**禁止**凭记忆编造。
4. **查忙闲**(多人时必做):`calendar schedules free list`,把冲突摆给用户拍板。
5. **订会议室**(用户提到会议室时必做):见「场景:订会议室」。必须先拿到真实 `meeting_room_id` 再建日程。
6. **复述并取得同意**write-high 确认要求)。
7. **执行创建**
### 命令
```bash
# 只给自己的日程(无参与人)
wecom-cli calendar schedules create \
--subject '午餐' \
--begin-time '2026-09-01 12:00:00' \
--end-time '2026-09-01 13:00:00'
# 带参与人 —— attendees 是对象数组
wecom-cli calendar schedules create --json '{
"subject": "产品评审",
"begin_time": "2026-09-01 14:00:00",
"end_time": "2026-09-01 15:00:00",
"attendees": [{"userid": "woxxxa"}, {"userid": "woxxxb"}]
}'
# 全天日程
wecom-cli calendar schedules create --json '{
"subject": "年假",
"begin_time": "2026-09-10 00:00:00",
"end_time": "2026-09-10 23:59:59",
"is_all_day": true
}'
# 建日程 + 原子占用会议室meeting_room_id 来自 rooms search
wecom-cli calendar schedules create --json '{
"subject": "产品评审",
"begin_time": "2026-09-01 14:00:00",
"end_time": "2026-09-01 15:00:00",
"attendees": [{"userid": "woxxxa"}],
"meeting_room_id": "mrmxxxx"
}'
# 自定义提醒(提前 30 分钟;不传时默认 [-900] 即提前 15 分钟)
wecom-cli calendar schedules create --json '{
"subject": "客户拜访",
"begin_time": "2026-09-02 09:00:00",
"end_time": "2026-09-02 10:00:00",
"location": "客户现场",
"reminders": {"is_remind": true, "reminder_time": [-1800]}
}'
```
**创建返回**只有 `schedule_id` 一个字段(内部标识,禁止展示)。需要回显完整信息时用本次入参回显,或用 `schedules get` 补齐。
### 创建成功后的回复格式
只输出三行,不加寒暄、不加建议、不展示地点/提醒/`schedule_id`
```
主题:{subject}
时间:{M月D日} {HH:mm}-{HH:mm}
参与人:{人名1}、{人名2}
```
## 场景:看看我今天/这周有什么安排
只给了时间、没有主题关键词 → **走 `list`**
```bash
# 今天
wecom-cli calendar schedules list --begin-time '2026-09-01 00:00:00' --end-time '2026-09-01 23:59:59'
# 本周
wecom-cli calendar schedules list --json '{"begin_time": "2026-08-31 00:00:00", "end_time": "2026-09-06 23:59:59"}'
```
返回 `schedule_list[]`,每条已含 `subject` / `begin_time` / `end_time` / `attendees[].name` / `creator_name` / `repeat_rule` / `meeting` / `meeting_room`**不必再调 `get`**。
若用户表述模糊(「最近有什么会」),按消歧规则二**同时**转 `wecom-meeting` 用相同时间范围拉 `meeting list`,合并展示。
## 场景:项目评审是什么时候(按关键词找日程)
有主题关键词 → **走 `search`**`keywords` / `organizer` / `has_attendees` **至少传其一**,三者都不传会失败。
```bash
# 按关键词
wecom-cli calendar schedules search --keywords '项目评审'
# 关键词 + 时间范围
wecom-cli calendar schedules search --json '{
"keywords": ["周会"],
"begin_time": "2026-09-01 00:00:00",
"end_time": "2026-09-07 23:59:59"
}'
# 按组织人organizer 是单值字符串,不是数组)
wecom-cli calendar schedules search --json '{"organizer": "woxxx"}'
# 按参与人has_attendees 是对象数组)
wecom-cli calendar schedules search --json '{"has_attendees": [{"userid": "woxxx"}]}'
# 翻页
wecom-cli calendar schedules search --json '{"keywords": ["周会"], "cursor": "<next_cursor>", "limit": 50}'
```
搜索无结果时给恢复建议:换关键词 / 改按组织人搜 / 改按参与人搜,不要静默失败。
## 场景:拿到 ID 后补日程详情
`list``search` 返回已足够完整,只有在**手上只有 `schedule_id`** 时才用:
```bash
wecom-cli calendar schedules get --json '{"schedule_ids": ["<schedule_id1>", "<schedule_id2>"]}'
```
> `schedule_ids` 是**纯字符串数组**,不是对象数组 —— 与 `attendees` / `meeting_ids` 的形状不同,最容易写错。
## 场景:大家什么时候都有空
```bash
wecom-cli calendar schedules free list --json '{
"userids": [{"userid": "woxxx"}, {"userid": "woyyy"}],
"begin_time": "2026-09-01 09:00:00",
"end_time": "2026-09-01 18:00:00",
"min_duration_minutes": 60,
"limit": 5
}'
```
- `userids` **是对象数组** `[{"userid": "..."}]`,尽管字段名叫 `userids`。单人合法(退化为「某人什么时候有空」)。
- 单次窗口 **≤ 24 小时**`begin_time` 早于当前时刻的部分会被服务端**自动截断**,传纯历史窗口返回空 `slots`
- 返回 `slots[]`,每项含 `begin_time` / `end_time` / `available_users[]`(含 `name`/ `available_count` / `busy_users[]`,另有 `total_count`(入参人数)与 `extra_info`(降级提示)。
- `available_count < total_count` 说明降级了:告诉用户哪些人冲突、几人能参加,由用户决定是否按降级时段安排。
- `slots` 为空 → 引导扩大窗口或减少参与人,**不要在同一窗口反复重试**。
- 展示时只用 `available_users[].name` / `busy_users[].name`**输出正文里绝不允许出现 `wo` 前缀字符串**。
## 场景:订会议室 / 查会议室空不空 / 公司有哪些楼
完整编排、返回结构与五条硬性规则见 [`references/meeting-room.md`](references/meeting-room.md)。要点:
```bash
# 列出我可访问的办公楼(无入参)
wecom-cli meeting rooms buildings list
# 查会议室可订性begin-time / end-time 必填)
wecom-cli meeting rooms search --json '{
"begin_time": "2026-09-01 14:00:00",
"end_time": "2026-09-01 15:00:00",
"room_name": "1605",
"floor_name": "16",
"capacity_min": 4
}'
```
- **只有用户提到楼名时才调 `buildings list`**;没提楼就跳过,让 `rooms search` 用当前所在楼兜底。
- `rooms search` 返回 `target[]`(传了 `room_name` 时的命中项,每项含 `status``bookable` / `unavailable` / `not_found`+ `recommendations[]`(同楼候选)+ `inferred_building`
-`target[].room.meeting_room_id``recommendations[].meeting_room_id` 传给 `schedules create` / `schedules update` 才算真正占用。
- **会议室名绝不能只写进 `location`** —— 那样不会占用会议室。
- `meeting_room_id` 仅工具链流转,对用户只展示会议室 `name` + 楼层 + 容量。
## 场景:改期 / 改地点 / 加减人 / 换会议室
先按消歧规则三判定归属,确认是纯日程后:
```bash
# 改时间
wecom-cli calendar schedules update --json '{
"schedule_id": "<schedule_id>",
"begin_time": "2026-09-02 14:00:00",
"end_time": "2026-09-02 15:00:00"
}'
# 加人 / 减人Patch 语义,只传要改的)
wecom-cli calendar schedules update --json '{
"schedule_id": "<schedule_id>",
"add_attendees": [{"userid": "woxxxc"}],
"remove_attendees": [{"userid": "woxxxb"}]
}'
# 换会议室(新会议室须先经 rooms search 确认 status=bookable
wecom-cli calendar schedules update --json '{"schedule_id": "<schedule_id>", "meeting_room_id": "mrmyyyy"}'
# 清空地点/备注:传空字符串
wecom-cli calendar schedules update --json '{"schedule_id": "<schedule_id>", "location": "", "description": ""}'
```
- **周期日程(`repeat_rule.is_repeat = true`)不支持更新**,告知用户并引导其到企业微信客户端操作;禁止逐场 `update` 拼凑、禁止 cancel + create 重建。
- **不预先按「是不是本人创建」拦截**:直接执行,返回权限错误时再告知用户并建议联系创建人。
- 返回 `detail`(更新后的完整 `ScheduleInfo`),可据此回显。
## 场景:取消日程
1. 定位(有主题关键词走 `search`,只给时间走 `list`)。
2.`repeat_rule.is_repeat` —— **周期日程不支持取消**,引导到客户端。
3. 判断用户是真取消还是改期(带「取消」字样也可能是改期,见规则三)。
4. 复述并取得同意write-high 确认要求)。
5. 执行:
```bash
wecom-cli calendar schedules cancel --schedule-id '<schedule_id>'
```
成功返回空对象 `{}`;无权限返回错误 —— 此时告知用户并建议联系创建人。
## 参数速查
> flag 与 JSON 字段一一对应:`--begin-time` ↔ `begin_time``--meeting-room-id` ↔ `meeting_room_id`,其余同理。嵌套结构(`attendees` / `reminders` / `timezone`)建议直接用 `--json`。完整 schema 用 `wecom-cli <service> <method> --help` 或 `--doc` 查。
| 方法 | 必填 | 关键可选 |
|---|---|---|
| `calendar schedules create` | `subject``begin_time``end_time` | `attendees`(对象数组)、`location``meeting_room_id``description``is_all_day``allow_self_join`(默认 true`reminders``{is_remind, reminder_time:[秒]}`,默认 `[-900]`)、`timezone``mark_optional_attendees`**字符串数组** |
| `calendar schedules update` | `schedule_id` | `subject``begin_time``end_time``add_attendees``remove_attendees``location`(空串=清空)、`description`(空串=清空)、`meeting_room_id``is_all_day``allow_self_join` |
| `calendar schedules cancel` | `schedule_id` | — |
| `calendar schedules list` | 无 | `begin_time`(默认当前时间)、`end_time`(默认起点 +30 天) |
| `calendar schedules search` | 无(但 `keywords` / `organizer` / `has_attendees` **至少传其一** | `begin_time``end_time``limit`(默认 10最大 1000`cursor` |
| `calendar schedules get` | `schedule_ids`**字符串数组** | — |
| `calendar schedules free list` | `begin_time``end_time``userids`**对象数组**≥1 | `min_duration_minutes`(默认 30`limit`(默认 10最大 100`strategy`(仅 `max_attendees` |
| `meeting rooms search` | `begin_time``end_time` | `room_name``building_name``city_name``floor_name``capacity_min``expand_to_other_buildings``limit`(默认 20上限 100`cursor` |
| `meeting rooms buildings list` | 无入参 | — |
**时间格式**统一 `YYYY-MM-DD HH:MM:SS`,且必须先把「明天」「下周三」解析成具体时刻再传。
## 输出格式
- **姓名原样展示**:一律用接口返回的 `attendees[].name`,返回 `zhangsan(张三)` 就展示 `zhangsan(张三)`,不做加工。
- **年份**:默认只到月日;跨年时才补 `{YYYY}年M月D日`
- **相对日期**:昨天/今天/明天在月日前加相对词,如 `时间:明天 9月1日 14:00-15:00`
- **列表**:禁止 markdown 表格;按开始时间升序,每条独立条目,只含主题/时间/参与人;超过 10 条只展示前 10 条并告知「还有 N 条,需要查看更多吗?」。
- **时区标注**`timezone.timezone_offset != 28800` 时必须标注,格式 `14:00-15:00纽约时间 UTC-5``UTC±N = timezone_offset / 3600`,地区中文名由 `timezone_id` 推导,`timezone_id` 为空时只留 `UTC-5`。东八区不标注。忙闲 `slots` 不适用。
- **禁止展示**`schedule_id``userid``meeting_room_id``cal_id``cursor` / `next_cursor` 等一切内部标识。
## 不支持的事(直接告知,禁止变通绕过)
| 不支持 | 正确做法 |
|---|---|
| 创建 / 更新 / 取消**周期(重复)日程** | 告知不支持,引导到企业微信客户端;禁止用「建多条单次日程」「逐场 update」「cancel + create」变通 |
| **RSVP**(接受 / 拒绝 / 待定日程邀请) | 告知不支持,建议在客户端对该邀请操作,或私信发起人 |
| 给机器人授予某个日历本权限 | 不存在该能力 |
## 易错点
- **消歧措辞不得改写**:创建场景那句问话必须逐字是 `需要创建日程还是会议?(请回复:日程 / 会议)`,不得改成「线上还是线下」「视频会议还是普通日程」等任何变体。
- **查询场景严禁追问**,模糊表述必须日程 + 会议两边都查再合并 —— 追问本身就是错误。
- **改约禁止 cancel + create**:会议链接不可重建,一旦拆开就永久丢失。
- **三种 ID 集合形状各不相同**`schedules get``schedule_ids: ["a","b"]`(字符串数组);`attendees` / `add_attendees` / `remove_attendees` / `has_attendees` / `free list``userids``[{"userid":"wo..."}]`(对象数组);`organizer` 用单值字符串。写错会静默失败或邀请到错误的人。
- **`mark_optional_attendees` 是字符串数组**,不是对象数组 —— 与同一条命令里的 `attendees` 形状相反。
- **`schedules search` 三选一**`keywords` / `organizer` / `has_attendees` 至少传其一;只传时间范围的搜索是无效调用。
- **只给时间就用 `list`,别把日期塞进 `keywords`** 去 `search`
- **`schedules list` 窗口限当前时刻前后 30 天**,超出服务端直接不返回(不是报错)。超范围时告知用户重新给一个更短的范围,别自行截断后假装查全了。
- **`free list` 窗口 ≤ 24 小时**,且早于当前时刻的部分被自动截断 —— 查「昨天大家什么时候有空」永远返回空。
- **时间是日程时区下的墙上时间,后台不做转换**:禁止自行把用户给的时间换算成东八区再传。
- **会议室只写 `location` 等于没订**:提到会议室就必须经 `rooms search``meeting_room_id` 传入,且订房是 create 的**前置阻塞项**,不能「先建了日程回头补会议室」。
- **上游技能的会议室参数名已过时**:上游 `wecomcli-calendar` 写的是 `room_keyword` / `min_capacity` / `building_city`CLI 1.2.0 实际是 `room_name` / `capacity_min` / `city_name` / `building_name`。照抄上游会直接调用失败。
- **指定的会议室查无或被占时,必须先告知、禁止静默替换**,哪怕 `recommendations` 只有 1 个候选也要用户确认。
- **`buildings list` 没有 building_id**:下游 `rooms search``city_name` + `building_name` 引用某栋楼,且这两个值必须逐字取自 `buildings list` 返回,禁止编造。
- **判定会议形态不必补 `get`**`search` / `list` / `get` 都直接返回 `meeting` 字段。
- **`schedules create` 只返回 `schedule_id`**:想回显完整内容要么用本次入参,要么再调 `get`,别编造返回字段。
- **写操作前的复述确认不可省**:本技能三个写方法全是 write-high均对外可见或不可逆。
---
## 来源
本技能改写自 [wecom-cli](https://github.com/WecomTeam/wecom-cli) 官方 Skill
MIT License© WecomTeam针对 DesireCore 的风险治理与交互约定做了适配。
上游对应技能:`wecomcli-calendar`

View File

@@ -0,0 +1,163 @@
# 会议室与办公楼查询 — `meeting rooms buildings list` / `meeting rooms search`
两个方法都是**只读查询**risk: read只告诉你「哪间会议室这个时段能订」**不占用**。
真正的占用发生在下游:`calendar schedules create` / `calendar schedules update` /
`meeting create` / `meeting update` 传入 `meeting_room_id`
> 本文件是会议室查询的唯一信息源。`wecom-meeting` 技能创建/更新会议要订会议室时,
> **反向调用本文件**`wecom-meeting` 自身不含 `rooms.*` 方法)。
## 命令
```bash
# 列出我可访问的办公楼(无入参)
wecom-cli meeting rooms buildings list
# 查会议室可订性
wecom-cli meeting rooms search --json '{
"begin_time": "2026-09-01 14:00:00",
"end_time": "2026-09-01 15:00:00",
"room_name": "1605",
"floor_name": "16",
"capacity_min": 4
}'
# 指定办公楼查city_name 与 building_name 同传或同省略)
wecom-cli meeting rooms search --json '{
"begin_time": "2026-09-01 14:00:00",
"end_time": "2026-09-01 15:00:00",
"city_name": "北京",
"building_name": "创新大厦A座",
"capacity_min": 6
}'
# 同城跨楼推荐(仅用户明确要求才加)
wecom-cli meeting rooms search --json '{
"begin_time": "2026-09-01 14:00:00",
"end_time": "2026-09-01 15:00:00",
"expand_to_other_buildings": true
}'
```
> ⚠️ **参数名以 CLI 1.2.0 schema 为准**。上游 `wecomcli-calendar` 的同名文档写的是
> `room_keyword` / `min_capacity` / `building_city`**这三个名字已经过时**,实际是
> `room_name` / `capacity_min` / `city_name`。照抄上游会直接调用失败。
---
## `meeting rooms buildings list` — 办公楼清单
**无入参**。返回当前用户可访问的办公楼全量列表。
### 返回
| 字段 | 说明 |
|---|---|
| `total_count` | 大楼总数 |
| `buildings[].name` | 大楼名称(不含城市前缀) |
| `buildings[].city` | 城市,展示时拼 `{city} {name}` |
| `buildings[].is_current` | 是否为当前办公大楼;无法判断时全为 `false` |
**没有 `building_id`** —— 下游 `rooms search``city_name` + `building_name` 两个字符串引用某栋楼。
### 用法
- **仅当用户提到楼名时才调用**。没提楼就跳过,让 `rooms search` 用当前所在楼兜底。
- 用户口语楼名(「北京创新 A」与标准名往往写法不同简称、漏字、少写 A/B 座、带不带城市前缀),
应做**模糊匹配**,不要求逐字相同:
- 命中唯一最接近项 → 文字确认一句「你是指【{city} {name}】吗?」,确认后取该条目的 `city` + `name`
- 命中多个相近项 → 只列这几个(展示 `{city} {name}`)让用户选。
- 确实匹配不到 → 让用户补充或自由输入楼名。**禁止从全量列表里随机挑几个充数,禁止编造列表里没有的楼名。**
- 展示给用户的楼名、以及最终喂给 `rooms search``city_name` / `building_name`
**必须逐字取自 `buildings list` 的返回条目**
- `buildings` 为空数组 → 提示「暂无可预订办公地点」。
---
## `meeting rooms search` — 会议室可订性
### 参数
| 参数 | 类型 | 必填 | 默认 | 说明 |
|---|---|:--:|---|---|
| `begin_time` | string | 是 | — | `YYYY-MM-DD HH:MM:SS`,须晚于当前时刻 |
| `end_time` | string | 是 | — | 晚于 `begin_time` |
| `room_name` | string | 否 | — | 会议室名/号(如 `"1605"``"创新室"`)。传了才会有 `target` |
| `building_name` | string | 否 | 当前所在楼 | 与 `city_name` 同传或同省略 |
| `city_name` | string | 否 | 当前所在楼城市 | 同上 |
| `floor_name` | string | 否 | — | 楼层。**直接用用户原始表述**不做归一化说「16 楼」就传 `"16 楼"`说「16F」就传 `"16F"` |
| `capacity_min` | int | 否 | — | 容量下限,取 `参与人数 + 1`(含组织者) |
| `expand_to_other_buildings` | bool | 否 | `false` | 同城跨楼推荐,**仅用户明确要求才传** |
| `limit` | int | 否 | 20 | 上限 100 |
| `cursor` | string | 否 | — | 分页游标 |
`city_name` / `building_name` 均不传时用当前所在楼兜底。
> 上游文档声明了三个业务错误码(`current_building_unknown` 兜底失败 / `building_not_found` 楼名无匹配 /
> `time_in_past` 起始时间已过),但**这些错误码不在 CLI 的 JSON Schema 里**,属上游声明、未经实测。
> 遇到错误时以 CLI 实际返回的 `error.code` 与 `error.message` 为准,不要硬编码上面三个字符串做分支判断。
### 返回
| 字段 | 说明 |
|---|---|
| `inferred_building.name` / `.city` | 实际查询的办公楼,可展示给用户确认 |
| `inferred_building.source` | 来源标记(如兜底 vs 来自入参) |
| `target[]` | 传了 `room_name` 时为命中项(**可能多间**);未传时为空数组 |
| `target[].status` | `bookable` / `unavailable` / `not_found` |
| `target[].room` | 房间元数据;`not_found` 时可能为 null |
| `recommendations[]` | 同楼候选,已按「同楼层优先 → 容量恰好够用」排序 |
| `*.meeting_room_id` / `name` / `capacity` / `floor` | 房间字段。**`meeting_room_id` 仅工具链流转,禁止出现在回复正文** |
| `has_more` / `next_cursor` | 分页 |
> `meeting_rooms` / `meeting_rooms_count` 在 schema 里标注为**废弃**字段,不要使用。
### 边界
- `status = unavailable` 时**不返回占用方信息**,不要告诉用户「被谁占了」。
- 同楼无可用时 `recommendations` 为空数组,由你决定是否询问用户开 `expand_to_other_buildings`
- 抢订竞态(查到 bookable、下单时已被抢发生在 `create` 阶段,按创建返回的错误处理并重新查一轮。
---
## 编排
```
├─ 用户提了楼名 → buildings list → 模糊匹配 + 确认 → city_name + building_name
│ 用户没提楼 → 跳过rooms search 用当前所在楼兜底)
└─ rooms searchbegin/end + 可选楼 + 可选 room_name + capacity_min = 参与人数 + 1
├─ 用户指定了具体会议室(传了 room_name→ 看 target
│ ├─ target 中有 status = bookable
│ │ ├─ 仅 1 个 → 直接取其 target[].room.meeting_room_id
│ │ └─ 多个 → 用文字让用户选(禁止自动取第一个)
│ ├─ target = [](查无此名)→ 先告知「未查到你指定的『xxx』会议室」
│ │ 再让用户决定改订其他会议室或换时间(候选仅 1 个也须确认)
│ └─ target 全是 unavailable → 先告知「『xxx』该时段已被占用」再让用户选替代或换时间
├─ 用户未指定具体会议室target = []
│ ├─ recommendations 多个 → 必须让用户选
│ └─ recommendations 仅 1 个 → 可直接使用
└─ recommendations = [] → 问是否跨楼expand_to_other_buildings = true 重查)或换时间
```
`rooms search` 需要**确定的起止时间**。用户只给了「明天下午」这种范围时,
先用 `calendar schedules free list` 查共同空闲、让用户选定一个具体时段,再拿该时段查会议室。
## 五条硬性规则(下游 create / update 必须遵守)
1. **先查询、后推荐、后创建**`meeting_room_id` 必须来自本次 `rooms search` 的真实返回值。
在拿到真实结果之前,**禁止**凭记忆、上下文、历史会话罗列或推荐任何具体会议室
—— 包括回复正文里提到的会议室名 / 房间号 / 楼层 / 容量。
2. **多个候选必须让用户选**,禁止自动替用户挑。
3. **会议室禁止只写进 `location`**:那样不会真正占用会议室。只要用户提到会议室,
就必须经 `rooms search` 拿到 `meeting_room_id` 传入。
4. **先订房、后建程/建会**:会议室查询与选择是 create 的**前置阻塞项**
不得以「先把会议建起来、会议室随后补」跳过。事后要换会议室可用 `update` 传新 `meeting_room_id` 改订
(须先经 `rooms search` 确认新会议室 `status = bookable`),不必取消重建。
5. **查无 / 不可用时必须先告知、禁止静默替换**:即使 `recommendations` 只有 1 个候选也要用户确认。
「仅 1 个可直接用」只适用于用户**未指定**具体会议室的情形。
## 展示约束
- 给用户的候选 **2~4 个**,展示 `name` + 楼层 + 容量。
- **`meeting_room_id` 禁止出现在用户可见的任何文字里**,对用户只说会议室名称。