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,175 @@
---
name: wecom-contact
description: >-
按姓名、姓名拼音、英文名或别名搜索企业微信通讯录里的人,拿到对方的姓名、英文名、职务、
部门路径与邮箱,同时在内部解析出后续接口需要的 userid。用户说"张三是谁""找一下李四"
"王五在哪个部门""公司有几个叫张伟的""他的邮箱是多少"时用它;
凡是要给某人发消息、拉某人进日程/会议、把待办分派给某人、给某人开文档权限,
也都必须先用它把人名解析成 userid。本技能只做人员查询不做部门树遍历、不按部门列员工、
不查组织架构图,也不发送任何消息(发消息找 wecom-message
version: 1.0.0
type: procedural
risk_level: low
status: enabled
tags:
- wecom
- contact
---
# 企业微信通讯录搜索
只有一个方法,但它是整个企微技能集的**枢纽**:企业微信的所有写操作认的是 `userid`
而用户嘴里说的永远是人名。**人名 → `userid` 的唯一合法转换入口就是这里。**
> **前置**:执行任何 `wecom-cli` 命令前,必须先完成 `wecom-shared` 的前置检查。
## 能力清单
| 能力 | 命令 | 风险 |
|---|---|---|
| 按关键词搜索通讯录成员 | `wecom-cli contact users search` | read隐私敏感返回邮箱/部门/职务) |
> 这是 read 方法,无副作用,但返回人员邮箱、部门与职务,属于**隐私敏感的读**。
> 用户只是想找人时直接查即可;用户在批量搜集人员信息时,先说明将要查什么再执行。
## 它在依赖链里的位置
几乎所有需要指定"人"的接口都要 `userid`,而 `userid` 只能从这里来:
| 目标操作 | 需要的字段 | 来源 |
|---|---|---|
| 创建/更新日程、会议,指定参与人 | `attendees` / `add_attendees` / `remove_attendees` | 本技能的 `users[].userid` |
| 创建/更新待办,指定参与人 | `follower_ids` / `followers` | 同上 |
| 会议指定主持人 | `organizer`**单值字符串**,不是对象数组) | 同上 |
| 文档加成员、改权限 | 成员 `userid` | 同上 |
| 微盘按创建人筛文件 | `creator_userids` | 同上 |
| 发邮件按人(而非邮箱地址)指定收件人 | `to.userids` / `cc.userids` / `bcc.userids` | 同上 |
格式约定:绝大多数接口要求**对象数组** `[{"userid":"woxxx"}]``organizer` 是例外,传单个字符串。
`userid` 通常以 `wo` 开头。`open_vid``userid` 等价,可互换传入。
**绝对禁止**:把姓名当 `userid` 直接拼进参数、凭记忆编造 `userid`
复用历史上下文里的 `userid` 而不重新解析(人可能已离职或改名)。
## 场景:找一个人
用户说「张三是谁」「帮我找一下李四」「王五在哪个部门」。
```bash
wecom-cli contact users search --keywords '张三'
```
关键词可以是**姓名、姓名拼音、英文名、别名**中的任意一种——不限于中文名。
「zhangsan」「Tony」「老张如果配了别名」都能命中。
多个关键词一次查(**最多 10 个,之间是 OR 关系**)。重复 flag 与空格分隔两种写法都可以,
生成的请求体完全一致(已用 `--dry-run` 实测):
```bash
# 写法一:重复 flag
wecom-cli contact users search --keywords '张三' --keywords '李四' --keywords '王五'
# 写法二:一个 flag 跟多个值
wecom-cli contact users search --keywords '张三' '李四' '王五'
```
拿到结果后:
- 唯一命中 → 直接用可读信息作答(姓名 / 英文名 / 职务 / 部门),`userid` 留在内部。
- 多个候选 → 见下一节。
- 零命中 → 如实告知没找到,并建议换个写法(换成拼音、英文名、或只给姓)。**不要编一个人出来。**
## 场景:同名消歧(多个候选)
用户说「给张伟发个消息」,而公司里有三个张伟。
1. 按接口返回的 `users` **原始顺序**展示候选,**用序号 + 可读信息**(姓名 / 英文名 / 职务 / 部门路径):
```
找到 3 位「张伟」,请问是哪一位?
1. 张伟Tony· 研发中心/平台组 · 负责人
2. 张伟 · 市场部/品牌组
3. 张伟David· 财务部
```
2. **候选超过 5 位时只展示前 5 位**,并告知「若目标不在其中可要求『查看更多』」,
仅在用户明确要求时再展开下一批。
3. **禁止用 `userid` 让用户辨认**,也禁止自行重排、随机排序或按你觉得"更相关"的顺序打乱。
4. 用户选定后,从对应那一项内部取出 `userid` 继续后续操作。
## 场景:要完整名单(清点/穷举)
用户说「一共有几个张三」「所有叫李四的人」「列出全部同名人员」这类**清点、穷举**意图时,
才显式传 `search_mode=list`
```bash
wecom-cli contact users search --keywords '张三' --search-mode list
```
- 默认(**不传** `search_mode`):按热度 top3 截断 + 数量截断,返回最相关的候选。
**绝大多数场景走这个分支**,日常找人不要传 `list`。
- 传 `list`:全量列表模式(按热度 + 部门距离排序,仍有数量截断)。
此时不受上面「只展示前 5 位」的约束,可以完整列出。
## 返回字段
| 字段 | 说明 | 能不能对用户展示 |
|---|---|---|
| `users[].name` | 中文姓名 | ✅ |
| `users[].alias` | 英文名 / 别名(可能为空) | ✅ |
| `users[].position` | **职务**(如「负责人」),注意不是「职位」(可能为空) | ✅ |
| `users[].departments` | 部门路径列表,从大到小,**主部门靠前** | ✅ |
| `users[].email` | 邮箱(可能为空) | ✅(用户问才给) |
| `users[].matched_keywords` | 本条命中了请求里的哪些关键词 | ✅(多关键词时用来说清哪条对应哪个) |
| `users[].userid` | 用户唯一标识 | ❌ **内部流转,绝不外露** |
| `users_count` | `users` 数组元素数量 | ✅ |
| `hint` | 结果受限提示(可能为空) | ✅ 见下 |
**`hint` 非空时必须处理**:告知用户「当前返回内容有限,仅返回了部分结果」,
并结合 `hint` 内容说明受限原因。**不要静默忽略它**——用户会以为看到的是全部。
## ⚠️ 它不是「全量通讯录导出」接口
这一点最容易误判,直接决定回答的口径:
- 返回结果**受当前授权身份的权限边界约束**。机器人以授权真人的身份工作,
`identity whoami` 返回的 `extra_identity_context` 里明确包含「权限边界说明」——
搜到的是**当前用户有权限看到的人**,不是企业全体成员。
- 即便在权限范围内,结果**仍然会被截断**:默认模式是"热度 top3 + 数量截断"
`list` 模式是"热度 + 部门距离排序 + 数量截断"。**两种模式都会截断。**
- 因此,**没搜到 ≠ 这个人不存在**。回答要说「在你的通讯录可见范围内没有找到」,
而不是「公司里没有这个人」。同理,`users_count` 不能当作「公司里有 N 个张三」的结论,
尤其在 `hint` 非空时。
- 本接口**做不到**:遍历部门树、按部门列出全部员工、拉组织架构图、导出全量花名册。
用户要这些时如实说明不支持,不要用多次搜索去拼凑。
## 参数速查
| 参数 | 类型 | 必填 | 说明 |
|---|---|:--:|---|
| `--keywords` | `[<str>...]` | **实际必填**(见易错点) | 搜索关键词列表1~10 个,可重复传;多个之间是 OR 关系 |
| `--search-mode` | `<str>` | 否 | 只有 `list` 一个有意义的取值;不传 = 默认模式 |
完整 schema 用 `wecom-cli contact users search --help` / `--doc` / `--schema` 自查。
## 易错点
- **`--keywords` 的 `--help` 不标 `[必填]`,但不传就会失败**。schema 里它不在 `required` 数组,
却带 `minItems: 1` ——这是和 `todo.*` 的 `items` 同一类隐蔽坑。
没有关键词时**向用户追问**,不要传空、也不要拿空请求去"试试看"。
(该结论来自 schema 推断,尚未实测确认失败信息的具体形态。)
- **一次最多 10 个关键词**,超了会失败,要分批。
- **`position` 是「职务」不是「职位」**:它表达的是「负责人」这类管理身份,
不要当成 job title 去说「张三的职位是负责人」。
- **展示顺序必须保持接口原始顺序**,不得重排或随机化——顺序本身携带相关性信息。
- **`userid` 是本技能唯一的产出物,也是最容易漏掉的禁露字段**。
用户问「他的 ID 是多少」时,说明该标识属于内部字段不便提供,改用可读信息或直接帮他把事办了。
- **别把 `userid` 缓存过夜再用**。需要指定人的操作,当次流程内重新解析一遍最稳。
- 参数缺失且上下文推不出来时,用简洁的自然语言追问,**不得猜测默认值**。
---
## 来源
本技能改写自 [wecom-cli](https://github.com/WecomTeam/wecom-cli) 官方 Skill
MIT License© WecomTeam针对 DesireCore 的风险治理与交互约定做了适配。
上游对应技能:`wecomcli-contact`。