@wecom/wecom-openclaw-plugin 2026.8.17 → 2026.9.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ English | [中文版](./README.zh.md)
2
+
1
3
  > 💡 **快速上手指引 & 交流群**
2
4
  >
3
5
  > 📖 [点击查看完整接入指引文档](https://doc.weixin.qq.com/doc/w3_AFYA1wY6ACoCNRxfnyGRJQaSa6jjJ?scode=AJEAIQdfAAo0RJmzxLAFYA1wY6ACo) — 包含配置步骤、产品介绍、常见问题解答等。
package/README.zh.md ADDED
@@ -0,0 +1,627 @@
1
+ [English](./README.md) | 中文版
2
+
3
+ > 💡 **快速上手指引 & 交流群**
4
+ >
5
+ > 📖 [点击查看完整接入指引文档](https://doc.weixin.qq.com/doc/w3_AFYA1wY6ACoCNRxfnyGRJQaSa6jjJ?scode=AJEAIQdfAAo0RJmzxLAFYA1wY6ACo) — 包含配置步骤、产品介绍、常见问题解答等。
6
+ >
7
+ > 💬 扫码加入企业微信交流群:
8
+ >
9
+ > <img src="https://wwcdn.weixin.qq.com/node/wework/images/202603241759.3fb01c32cc.png" alt="扫码入群交流" width="200" />
10
+
11
+ # 特别说明
12
+ > ****2026.3.22 版本 OpenClaw 兼容说明****
13
+ >
14
+ > 如果你的 OpenClaw 是 2026.3.22 及以上的版本,请升级插件到 2026.3.24 及以上版本。
15
+ >
16
+ > 如果你的 OpenClaw 是 2026.3.22 以下的版本,请保持插件版本在 2026.3.20 版本。
17
+ >
18
+ > 你可以使用以下命令快速安装: `npx -y @wecom/wecom-openclaw-cli install --force`
19
+
20
+ # 🤖 WeCom OpenClaw 插件
21
+
22
+ **面向 [OpenClaw](https://github.com/openclaw) 的企业微信 Channel 与业务能力插件** — 由腾讯企业微信团队开发。
23
+
24
+ > 一个由企业微信提供支持、兼具 Channel 与业务能力的插件。支持 **Bot 模式**(WebSocket 长轮询,或 JSON 回调的 HTTP webhook)与 **Agent 模式**(XML 加密回调的 HTTP webhook),以及单聊、群聊、流式回复、主动消息推送,并内置 `wecom-cli` 业务操作能力。
25
+
26
+ ---
27
+
28
+ 📖 [企业微信 AI Bot 官方文档](https://open.work.weixin.qq.com/help?doc_id=21657)
29
+
30
+
31
+ ## ✨ 特性
32
+
33
+ - 🔗 **双模式**:Bot(WebSocket / Webhook)与 Agent(HTTP webhook)可独立或同时运行
34
+ - 💬 同时支持单聊(DM)与群聊
35
+ - 📤 向指定用户、群组、部门或标签主动推送消息
36
+ - 🖼️ 接收并处理图片、语音、视频、文件及**图文混排**消息,并自动下载
37
+ - 🗣️ 语音转文字:自动提取语音消息中的转写文本
38
+ - 💬 引用消息支持:处理被引用的文本、图片、语音与文件消息
39
+ - ⏳ 流式回复,支持"思考中"占位消息(Bot 模式)
40
+ - 🔐 Agent 模式:AES-256-CBC 加密 XML 回调 + SHA1 签名校验
41
+ - 📝 回复支持 Markdown 格式
42
+ - 🃏 模板卡片消息(text_notice、news_notice、button_interaction、vote_interaction、multiple_interaction),支持**事件回调处理**
43
+ - 🔒 内置访问控制:DM 策略(pairing / open / allowlist / disabled)与群组策略(open / allowlist / disabled)
44
+ - 🔑 命令授权:按账号进行命令权限控制,支持访问组
45
+ - 👥 多账号支持:可运行多个企业微信账号,各自独立的 bot/agent 配置
46
+ - 🧰 内置业务 Skills,由插件提供的 `wecom-cli` 工具驱动
47
+ - 🔀 动态 Agent 路由:按用户/群组自动创建隔离的 Agent
48
+ - 📁 本地文件发送,支持可配置的媒体路径白名单(`mediaLocalRoots`)
49
+ - 📊 智能媒体大小限制与自动降级(图片 10MB → 文件,视频 10MB → 文件,语音 2MB/仅 AMR → 文件,最大 20MB)
50
+ - 🔄 **Bot 优先、Agent 兜底**的出站投递:Bot WS 不可用时自动回退到 Agent HTTP API
51
+ - ⚡ 自动心跳保活与重连(最多 10 次重连,5 次鉴权失败重试)
52
+ - 🛡️ 防踢保护:抑制服务端断连导致的自动重启,避免互相踢下线
53
+ - 🧙 交互式 CLI 安装向导
54
+
55
+ ### 企业微信业务能力
56
+
57
+ 插件内置 `wecom-cli` 工具及对应的 Skills,覆盖以下企业微信业务品类:
58
+
59
+ | 品类 | 能力 |
60
+ |---|---|
61
+ | 💬 消息 | 向机器人最近对话过的单聊/群聊主动推送消息,支持 Markdown/图片/文件/语音/视频消息 |
62
+ | 📧 邮件 | 邮件发送/回复/转发,邮件搜索,获取邮件内容详情 |
63
+ | 📄 文档 | 在线文档的新建、导入、读取、追加与覆盖写入 |
64
+ | 🗂️ 文档管理 | 多种文档类型的搜索,在线文档/在线表格/智能表格/智能文档的重命名、成员权限与加入规则管理 |
65
+ | 📊 在线表格 | 在线表格新建、CSV/Excel 导入、内容读改、追加行、子表管理 |
66
+ | 🧮 智能表格 | 智能表格创建,子表/字段/记录/视图/图表管理,行列样式修改 |
67
+ | 📰 智能文档 | 智能文档创建、获取页面内容、编辑文档内容、内置数据表信息获取 |
68
+ | ✅ 待办 | 创建/读取/更新/删除待办,分派参与人与完成待办等 |
69
+ | 📅 日程 | 日程增删改查、参与人管理、多成员闲忙查询、会议室查询预订等 |
70
+ | 🎥 会议 | 创建预约会议、取消会议、更新参会人、查询列表与详情、读取会议纪要与转写原文 |
71
+ | 💾 微盘 | 微盘文件的搜索、基础信息读取、上传、下载 |
72
+ | 👤 通讯录 | 按姓名/拼音/别名搜索成员,获取成员基本信息,以用于会议、日程等多人场景 |
73
+
74
+ ---
75
+
76
+ ## 🚀 快速开始
77
+
78
+ ### 环境要求
79
+
80
+ - OpenClaw `>= 2026.3.28`
81
+
82
+ ### 快速安装
83
+
84
+ 使用 CLI 工具一键完成插件安装与机器人配置:
85
+
86
+ ```shell
87
+ # 自动安装 Channel 插件并快速完成配置,同时适用于升级
88
+ npx -y @wecom/wecom-openclaw-cli install
89
+ ```
90
+
91
+ 更多选项
92
+ ```shell
93
+ # 如果安装失败,尝试强制安装
94
+ npx -y @wecom/wecom-openclaw-cli install --force
95
+
96
+ # 使用 --help 了解该工具的更多用法
97
+ npx -y @wecom/wecom-openclaw-cli --help
98
+ ```
99
+
100
+ ### 手动安装
101
+
102
+ ```shell
103
+ openclaw plugins install @wecom/wecom-openclaw-plugin
104
+ ```
105
+
106
+ ### 配置
107
+
108
+ #### 方式一:交互式配置
109
+
110
+ ```shell
111
+ openclaw channels add
112
+ ```
113
+
114
+ 按提示输入机器人的 **Bot ID** 与 **Secret**。
115
+
116
+ #### 方式二:CLI 快速配置
117
+
118
+ ```shell
119
+ openclaw config set channels.wecom.botId <YOUR_BOT_ID>
120
+ openclaw config set channels.wecom.secret <YOUR_BOT_SECRET>
121
+ openclaw config set channels.wecom.enabled true
122
+ openclaw gateway restart
123
+ ```
124
+
125
+ ### 启用业务工具
126
+
127
+ 以上企业微信业务能力通过插件提供的 `wecom-cli` 工具暴露。除非 `tools.profile` 设为 `full`,否则需要放行插件并重启 Gateway:
128
+
129
+ ```shell
130
+ openclaw config set tools.alsoAllow '["wecom-openclaw-plugin"]'
131
+ openclaw gateway restart
132
+ ```
133
+
134
+ > 按插件 ID 放行会启用该插件当前及未来注册的全部工具。若 `tools.alsoAllow` 中已有其他条目,请将 `wecom-openclaw-plugin` 合并进现有数组,而不要覆盖。
135
+
136
+ ### 模式概览
137
+
138
+ 插件支持两种连接模式,可独立或同时使用:
139
+
140
+ | 模式 | 连接方式 | 消息格式 | 适用场景 |
141
+ |------|-----------|---------------|----------|
142
+ | **Bot**(智能体) | WebSocket(默认)或 HTTP webhook | JSON | 快速接入、流式回复 |
143
+ | **Agent**(自建应用) | HTTP webhook 回调 | XML | 企业应用、API 驱动的消息 |
144
+
145
+ > **说明**:Bot 模式通过 `connectionMode` 支持两种连接方式:
146
+ > - `websocket`(默认)— WebSocket 长轮询,需要 `botId` + `secret`
147
+ > - `webhook` — HTTP 回调,需要 `token` + `encodingAESKey`
148
+
149
+ ### Bot 模式配置
150
+
151
+ #### 核心设置
152
+
153
+ | 配置路径 | 说明 | 可选值 | 默认值 |
154
+ |---|---|---|---|
155
+ | `channels.wecom.enabled` | 启用该 Channel | `true` / `false` | `false` |
156
+ | `channels.wecom.connectionMode` | Bot 连接方式 | `websocket` / `webhook` | `websocket` |
157
+ | `channels.wecom.name` | 账号显示名称 | — | `企业微信` |
158
+
159
+ #### WebSocket 模式(默认)
160
+
161
+ | 配置路径 | 说明 | 可选值 | 默认值 |
162
+ |---|---|---|---|
163
+ | `channels.wecom.botId` | 企业微信机器人 ID | — | — |
164
+ | `channels.wecom.secret` | 企业微信机器人 secret | — | — |
165
+ | `channels.wecom.websocketUrl` | WebSocket 端点 | — | `wss://openws.work.weixin.qq.com` |
166
+ | `channels.wecom.sendThinkingMessage` | 发送"思考中"占位消息 | `true` / `false` | `true` |
167
+
168
+ #### Webhook 模式(`connectionMode: "webhook"`)
169
+
170
+ | 配置路径 | 说明 | 可选值 | 默认值 |
171
+ |---|---|---|---|
172
+ | `channels.wecom.token` | Webhook 校验 token | — | — |
173
+ | `channels.wecom.encodingAESKey` | AES 加密密钥(43 字符 Base64) | — | — |
174
+ | `channels.wecom.receiveId` | 接收方 ID(用于解密校验) | — | — |
175
+ | `channels.wecom.welcomeText` | enter_chat 事件欢迎语 | — | — |
176
+ | `channels.wecom.streamPlaceholderContent` | 流式占位内容 | — | — |
177
+
178
+ #### 访问控制
179
+
180
+ | 配置路径 | 说明 | 可选值 | 默认值 |
181
+ |---|---|---|---|
182
+ | `channels.wecom.dmPolicy` | 单聊访问策略 | `pairing` / `open` / `allowlist` / `disabled` | `open` |
183
+ | `channels.wecom.allowFrom` | 单聊白名单(用户 ID) | — | `[]` |
184
+ | `channels.wecom.groupPolicy` | 群聊访问策略 | `open` / `allowlist` / `disabled` | `open` |
185
+ | `channels.wecom.groupAllowFrom` | 群聊白名单(群 ID) | — | `[]` |
186
+ | `channels.wecom.groups` | 按群配置(如发送者白名单) | — | `{}` |
187
+
188
+ #### 媒体设置
189
+
190
+ | 配置路径 | 说明 | 默认值 |
191
+ |---|---|---|
192
+ | `channels.wecom.mediaLocalRoots` | 允许发送媒体的额外本地路径(支持 `~`) | `[]` |
193
+ | `channels.wecom.media.maxBytes` | 媒体文件最大字节数 | `20971520`(20MB) |
194
+ | `channels.wecom.media.tempDir` | 媒体处理临时目录 | — |
195
+ | `channels.wecom.media.retentionHours` | 媒体文件保留时长(小时) | — |
196
+ | `channels.wecom.media.cleanupOnStart` | 启动时清理临时媒体 | — |
197
+
198
+ **媒体大小限制与自动降级:**
199
+
200
+ | 媒体类型 | 最大大小 | 降级行为 |
201
+ |---|---|---|
202
+ | 图片 | 10 MB | 超出 → 以文件发送 |
203
+ | 视频 | 10 MB | 超出 → 以文件发送 |
204
+ | 语音 | 2 MB(仅 AMR) | 非 AMR 格式或超出 → 以文件发送 |
205
+ | 文件 | 20 MB | 超出 → 拒绝(无法发送) |
206
+
207
+ #### 网络设置
208
+
209
+ | 配置路径 | 说明 | 默认值 |
210
+ |---|---|---|
211
+ | `channels.wecom.network.timeoutMs` | HTTP 请求超时(毫秒) | — |
212
+ | `channels.wecom.network.retries` | 重试次数 | — |
213
+ | `channels.wecom.network.retryDelayMs` | 重试间隔(毫秒) | — |
214
+ | `channels.wecom.network.egressProxyUrl` | 可信 IP 场景的出站代理 URL | — |
215
+
216
+ > **出站代理优先级**:`channels.wecom.network.egressProxyUrl` > `OPENCLAW_WECOM_EGRESS_PROXY_URL` > `WECOM_EGRESS_PROXY_URL` > `HTTPS_PROXY` > `ALL_PROXY` > `HTTP_PROXY`
217
+
218
+ ### Agent 模式配置
219
+
220
+ Agent 模式使用 HTTP webhook 回调,消息为 XML 加密格式。需要在企业微信管理后台的「API 接收」设置中配置回调 URL。
221
+
222
+ #### 前置条件
223
+
224
+ 1. 在 [企业微信管理后台](https://work.weixin.qq.com/wework_admin/frame#apps) 创建自建应用
225
+ 2. 记录 **CorpID**、**CorpSecret**(来自应用设置)与 **AgentId**
226
+ 3. 在应用设置的「API 接收」中:
227
+ - 记录 **Token** 与 **EncodingAESKey**(自动生成或自定义)
228
+ - **先不要点击保存** — 点击保存时企业微信会立即校验回调 URL
229
+
230
+ #### 设置步骤
231
+
232
+ > **重要**:必须先配置 Gateway,再在企业微信管理后台保存回调 URL。点击保存时企业微信会立即发送校验请求(带 `echostr` 的 GET 请求),Gateway 需要 `token` 和 `encodingAESKey` 才能正确解密并响应。
233
+
234
+ **步骤 1:配置 Gateway**
235
+
236
+ ```shell
237
+ openclaw config set channels.wecom.agent.corpId <YOUR_CORP_ID>
238
+ openclaw config set channels.wecom.agent.corpSecret <YOUR_CORP_SECRET>
239
+ openclaw config set channels.wecom.agent.agentId <YOUR_AGENT_ID>
240
+ openclaw config set channels.wecom.agent.token <YOUR_CALLBACK_TOKEN>
241
+ openclaw config set channels.wecom.agent.encodingAESKey <YOUR_ENCODING_AES_KEY>
242
+ openclaw config set channels.wecom.enabled true
243
+ openclaw gateway restart
244
+ ```
245
+
246
+ **步骤 2:在企业微信管理后台保存回调 URL**
247
+
248
+ 回到「API 接收」设置,填入回调 URL:
249
+ - **URL**:`https://<your-gateway-host>/plugins/wecom/agent/<accountId>`(例如 `/plugins/wecom/agent/default`);单账号模式也可使用 `/plugins/wecom/agent`
250
+
251
+ 点击保存,校验应通过。
252
+
253
+ #### JSON 配置
254
+
255
+ ```json
256
+ {
257
+ "channels": {
258
+ "wecom": {
259
+ "enabled": true,
260
+ "agent": {
261
+ "corpId": "ww1234567890abcdef",
262
+ "corpSecret": "your-corp-secret",
263
+ "agentId": 1000002,
264
+ "token": "your-callback-token",
265
+ "encodingAESKey": "your-encoding-aes-key-43-chars"
266
+ }
267
+ }
268
+ }
269
+ }
270
+ ```
271
+
272
+ #### Agent 配置参考
273
+
274
+ | 配置路径 | 说明 | 是否必填 |
275
+ |---|---|---|
276
+ | `channels.wecom.agent.corpId` | 企业 Corp ID | 是 |
277
+ | `channels.wecom.agent.corpSecret` | 应用 secret | 是 |
278
+ | `channels.wecom.agent.agentId` | 应用 Agent ID | 否(主动推送消息时需要) |
279
+ | `channels.wecom.agent.token` | 回调校验 token | 是 |
280
+ | `channels.wecom.agent.encodingAESKey` | 回调加密密钥(43 字符) | 是 |
281
+ | `channels.wecom.agent.welcomeText` | 欢迎语 | 否 |
282
+ | `channels.wecom.agent.dmPolicy` | 单聊访问策略(覆盖顶层) | 否 |
283
+ | `channels.wecom.agent.allowFrom` | 单聊白名单(覆盖顶层) | 否 |
284
+
285
+ #### Webhook 路径
286
+
287
+ **Agent 模式:**
288
+
289
+ | 路径 | 说明 |
290
+ |---|---|
291
+ | `/plugins/wecom/agent/<accountId>` | 推荐路径(例如 `/plugins/wecom/agent/default`) |
292
+ | `/plugins/wecom/agent/default` | 多账号模式下自动路由到默认账号(即使默认账号 ID 不是 `default`) |
293
+ | `/plugins/wecom/agent` | 兼容路径(单账号 / 多账号签名匹配) |
294
+ | `/wecom/agent` | 旧版兼容路径 |
295
+
296
+ **Bot Webhook 模式**(`connectionMode: "webhook"`):
297
+
298
+ | 路径 | 说明 |
299
+ |---|---|
300
+ | `/plugins/wecom/bot` | 推荐路径(单账号) |
301
+ | `/plugins/wecom/bot/<accountId>` | 多账号路径 |
302
+ | `/wecom/bot` | 旧版兼容路径 |
303
+ | `/wecom` | 旧版兼容路径 |
304
+
305
+ ### 出站投递(Bot WS → Agent HTTP 兜底)
306
+
307
+ 插件采用 **Bot 优先、Agent 兜底** 的出站消息投递策略:
308
+
309
+ 1. **Bot WebSocket 可用** → 通过 WS 发送(支持 markdown、流式)
310
+ 2. **Bot WS 不可用** → 自动回退到 **Agent HTTP API**(`cgi-bin/message/send`)
311
+
312
+ 这意味着:
313
+ - **仅 Agent 的账号**(未配置 Bot)仍可发送主动消息、Cron 投递与广播
314
+ - **目标格式**如 `party:1`、`tag:Ops`、`user:zhangsan` 在两条路径中都完全支持
315
+ - **媒体兜底**:当 Bot WS 不可用时,媒体文件先下载、通过 Agent API 上传到企业微信再发送;若上传失败则回退为文本 + URL
316
+ - 无需手动切换 — 插件自动处理兜底
317
+
318
+ ### 双模式并用
319
+
320
+ Bot 与 Agent 可在同一账号上同时运行。Bot 负责 WebSocket 流式;Agent 负责 HTTP webhook 回调与 API 驱动的回复。
321
+
322
+ ```json
323
+ {
324
+ "channels": {
325
+ "wecom": {
326
+ "enabled": true,
327
+ "botId": "your-bot-id",
328
+ "secret": "your-bot-secret",
329
+ "agent": {
330
+ "corpId": "ww1234567890abcdef",
331
+ "corpSecret": "your-corp-secret",
332
+ "agentId": 1000002,
333
+ "token": "your-callback-token",
334
+ "encodingAESKey": "your-encoding-aes-key-43-chars"
335
+ }
336
+ }
337
+ }
338
+ }
339
+ ```
340
+
341
+ ### 多账号配置
342
+
343
+ 使用 `accounts` 配置多个企业微信账号,每个账号可带可选的 bot 和/或 agent 子配置。账号级字段覆盖同名的顶层字段。
344
+
345
+ ```json
346
+ {
347
+ "channels": {
348
+ "wecom": {
349
+ "enabled": true,
350
+ "defaultAccount": "main",
351
+ "dmPolicy": "open",
352
+ "accounts": {
353
+ "main": {
354
+ "botId": "bot-id-1",
355
+ "secret": "secret-1",
356
+ "agent": {
357
+ "corpId": "ww1234567890abcdef",
358
+ "corpSecret": "secret-a",
359
+ "agentId": 1000002,
360
+ "token": "token-a",
361
+ "encodingAESKey": "aes-key-a"
362
+ }
363
+ },
364
+ "support": {
365
+ "dmPolicy": "allowlist",
366
+ "allowFrom": ["admin1"],
367
+ "agent": {
368
+ "corpId": "ww1234567890abcdef",
369
+ "corpSecret": "secret-b",
370
+ "agentId": 1000003,
371
+ "token": "token-b",
372
+ "encodingAESKey": "aes-key-b"
373
+ }
374
+ }
375
+ }
376
+ }
377
+ }
378
+ }
379
+ ```
380
+
381
+ > **说明**:多账号模式下,没有显式 `bindings` 的账号不会回退到默认 agent。请为每个账号配置 bindings:
382
+ > ```json
383
+ > {
384
+ > "bindings": [
385
+ > { "agentId": "your-agent", "match": { "channel": "wecom", "accountId": "main" } }
386
+ > ]
387
+ > }
388
+ > ```
389
+
390
+ ### 动态 Agent 配置
391
+
392
+ 动态 Agent 路由按用户或群组自动创建隔离的 Agent,实现会话隔离。
393
+
394
+ ```json
395
+ {
396
+ "channels": {
397
+ "wecom": {
398
+ "dynamicAgents": {
399
+ "enabled": true,
400
+ "dmCreateAgent": true,
401
+ "groupEnabled": true,
402
+ "adminUsers": ["admin_user_id"]
403
+ }
404
+ }
405
+ }
406
+ }
407
+ ```
408
+
409
+ | 配置路径 | 说明 | 默认值 |
410
+ |---|---|---|
411
+ | `channels.wecom.dynamicAgents.enabled` | 启用动态 Agent 路由 | `false` |
412
+ | `channels.wecom.dynamicAgents.dmCreateAgent` | 为每个单聊用户创建隔离 Agent | `true` |
413
+ | `channels.wecom.dynamicAgents.groupEnabled` | 为群聊启用动态 Agent | `true` |
414
+ | `channels.wecom.dynamicAgents.adminUsers` | 管理员用户(绕过动态路由,使用主 Agent) | `[]` |
415
+
416
+ ---
417
+
418
+ ## 🔒 访问控制
419
+
420
+ ### 单聊(Direct Message)访问
421
+
422
+ **默认**:`dmPolicy: "open"` — 所有用户无需审批即可发送单聊消息。
423
+
424
+ #### 审批配对
425
+
426
+ ```shell
427
+ openclaw pairing list wecom # 查看待处理的配对请求
428
+ openclaw pairing approve wecom <CODE> # 审批配对请求
429
+ ```
430
+
431
+ #### 白名单模式
432
+
433
+ 通过 `channels.wecom.allowFrom` 配置允许的用户 ID:
434
+
435
+ ```json
436
+ {
437
+ "channels": {
438
+ "wecom": {
439
+ "dmPolicy": "allowlist",
440
+ "allowFrom": ["user_id_1", "user_id_2"]
441
+ }
442
+ }
443
+ }
444
+ ```
445
+
446
+ #### 开放模式
447
+
448
+ 设置 `dmPolicy: "open"` 允许所有用户无需审批即可发送单聊消息。
449
+
450
+ #### 禁用模式
451
+
452
+ 设置 `dmPolicy: "disabled"` 完全屏蔽所有单聊消息。
453
+
454
+ ### 群聊访问
455
+
456
+ #### 群组策略(`channels.wecom.groupPolicy`)
457
+
458
+ - `"open"` — 允许所有群聊消息(默认)
459
+ - `"allowlist"` — 仅允许 `groupAllowFrom` 中列出的群
460
+ - `"disabled"` — 禁用所有群聊消息
461
+
462
+ ### 群组配置示例
463
+
464
+ #### 允许所有群(默认行为)
465
+
466
+ ```json
467
+ {
468
+ "channels": {
469
+ "wecom": {
470
+ "groupPolicy": "open"
471
+ }
472
+ }
473
+ }
474
+ ```
475
+
476
+ #### 仅允许指定群
477
+
478
+ ```json
479
+ {
480
+ "channels": {
481
+ "wecom": {
482
+ "groupPolicy": "allowlist",
483
+ "groupAllowFrom": ["group_id_1", "group_id_2"]
484
+ }
485
+ }
486
+ }
487
+ ```
488
+
489
+ #### 仅允许群内指定发送者(发送者白名单)
490
+
491
+ 除群白名单外,还可限制群内哪些成员能与机器人交互。仅处理 `groups.<chatId>.allowFrom` 中列出的用户消息,其他成员的消息会被静默忽略。这是发送者级白名单,适用于**所有消息**。
492
+
493
+ ```json
494
+ {
495
+ "channels": {
496
+ "wecom": {
497
+ "groupPolicy": "allowlist",
498
+ "groupAllowFrom": ["group_id_1"],
499
+ "groups": {
500
+ "group_id_1": {
501
+ "allowFrom": ["user_id_1", "user_id_2"]
502
+ }
503
+ }
504
+ }
505
+ }
506
+ }
507
+ ```
508
+
509
+ ---
510
+
511
+ ## ⏰ Cronjob(定时任务)
512
+
513
+ 插件通过 OpenClaw 内置的 Cron 服务支持定时消息投递。Cron 任务走 **Agent 出站**通道,因此必须配置 Agent 模式。
514
+
515
+ ### 目标格式
516
+
517
+ `delivery.to` 字段支持以下目标格式:
518
+
519
+ | 格式 | 目标 | 示例 |
520
+ |--------|--------|--------|
521
+ | `party:<id>` | 部门(所有成员) | `party:1`(根部门 = 全体员工) |
522
+ | `dept:<id>` | 部门(party 的别名) | `dept:5` |
523
+ | `tag:<id>` | 标签组 | `tag:Ops` |
524
+ | `user:<id>` | 指定用户 | `user:zhangsan` |
525
+ | `group:<id>` | 外部群聊 | `group:wr123abc` |
526
+ | `chat:<id>` | 群聊(group 的别名) | `chat:wc456def` |
527
+ | 纯数字 | 自动识别为部门 | `1` → `party:1` |
528
+ | `wr...` / `wc...` | 自动识别为群聊 | `wr123` → `chatid` |
529
+ | 其他字符串 | 自动识别为用户 | `zhangsan` → `touser` |
530
+
531
+ > **命名空间前缀**(`wecom:`、`qywx:`、`wework:`、`wechatwork:`、`wecom-agent:`)在解析前会自动去除。
532
+
533
+ ### 方式一:CLI(推荐 — 立即生效)
534
+
535
+ ```shell
536
+ openclaw cron add \
537
+ --name "daily-report" \
538
+ --agent main \
539
+ --cron "0 9 * * 1-5" \
540
+ --tz "Asia/Shanghai" \
541
+ --message "Good morning! Here is your daily briefing." \
542
+ --announce \
543
+ --channel wecom \
544
+ --to "party:1"
545
+ ```
546
+
547
+ > **说明**:`--announce` 启用投递模式(将 AI 回复广播到目标会话)。使用 `--no-deliver` 保持内部输出。已废弃的 `--deliver` 标志是 `--announce` 的别名。
548
+
549
+ 常用 CLI 命令:
550
+
551
+ ```shell
552
+ openclaw cron list # 列出所有 cron 任务
553
+ openclaw cron show <id> # 查看任务详情
554
+ openclaw cron enable <id> # 启用任务
555
+ openclaw cron disable <id> # 禁用任务
556
+ openclaw cron remove <id> # 删除任务
557
+ openclaw cron run <id> # 手动触发任务
558
+ openclaw cron runs --id <id> # 查看运行历史
559
+ openclaw cron edit <id> --message "New prompt" # 编辑任务
560
+ ```
561
+
562
+ ### 方式二:编辑 `jobs.json`(需重启 gateway)
563
+
564
+ 文件路径:`~/.openclaw/cron/jobs.json`
565
+
566
+ ```json
567
+ {
568
+ "version": 1,
569
+ "jobs": [
570
+ {
571
+ "id": "daily-report",
572
+ "name": "Daily Report",
573
+ "agentId": "main",
574
+ "enabled": true,
575
+ "schedule": { "kind": "cron", "expr": "0 9 * * 1-5", "tz": "Asia/Shanghai" },
576
+ "sessionTarget": "isolated",
577
+ "wakeMode": "now",
578
+ "payload": {
579
+ "kind": "agentTurn",
580
+ "message": "Generate today's briefing and send it."
581
+ },
582
+ "delivery": {
583
+ "mode": "announce",
584
+ "channel": "wecom",
585
+ "to": "party:1",
586
+ "accountId": "main"
587
+ },
588
+ "state": {}
589
+ }
590
+ ]
591
+ }
592
+ ```
593
+
594
+ 编辑后重启 gateway:
595
+
596
+ ```shell
597
+ openclaw gateway restart
598
+ ```
599
+
600
+ ### 方式三:通过对话创建(立即生效)
601
+
602
+ 可以直接在企业微信会话中向 AI 智能体提问:
603
+
604
+ > "创建一个定时任务:每个工作日早上 9 点向全公司发送每日简报"
605
+
606
+ 智能体会调用 Cron API 创建任务 — 无需重启。
607
+
608
+ ### 注意事项
609
+
610
+ - Cron 任务使用 **Agent 出站**路径 — 必须配置 Agent 模式(`corpId` / `corpSecret` / `agentId`)。
611
+ - 服务器 IP 必须在企业微信可信 IP 白名单内,或配置 `egressProxyUrl` 使用固定出站代理。
612
+ - 通过 CLI 或对话 API 创建的任务立即生效。手动编辑 `jobs.json` 需要执行 `openclaw gateway restart`。
613
+ - 多账号场景下,将 `delivery.accountId` 设为目标账号(如 `"main"`、`"support"`)。
614
+
615
+ ---
616
+
617
+ ## 📦 更新
618
+
619
+ ```shell
620
+ openclaw plugins update wecom-openclaw-plugin
621
+ ```
622
+
623
+ ---
624
+
625
+ ## 📄 许可证
626
+
627
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wecom/wecom-openclaw-plugin",
3
- "version": "2026.8.17",
3
+ "version": "2026.9.15",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -35,8 +35,8 @@ description: 企微 doc 内容操作技能,包含新建在线文档、导入
35
35
 
36
36
  | 用户意图 | 参考位置 |
37
37
  |---|---------------------------------------------------------------|
38
- | 新建doc文档(在线) | 见下方「新建doc文档」 |
39
- | 导入本地文件为企微doc文档 | 见下方「导入doc文档」 |
38
+ | 从零开始新建doc文档 | 见下方「新建doc文档」 |
39
+ | 用户已有本地文件,要求写入 / 创建 / 新建企微doc文档 | 见下方「导入doc文档」 |
40
40
  | 读取doc文档内容 | 见下方「读取doc文档内容」 |
41
41
  | 追加文本到doc文档末尾 | [+contents-append](references/doc-contents-append.md) |
42
42
  | 全量覆盖doc文档内容 | [+contents-overwrite](references/doc-contents-overwrite.md) |
@@ -50,10 +50,10 @@ description: 企微 doc 内容操作技能,包含新建在线文档、导入
50
50
 
51
51
  ### 新建doc文档
52
52
 
53
- 新建企微doc文档统一走「**生成 `.docx` → 导入**」两步流程:
53
+ 仅适用于用户**没有本地文件,需要从零开始创建doc文档**的场景,统一走「生成 `.docx` → 导入」两步流程:
54
54
 
55
55
  1. 生成 `.docx` 文件:按 [+doc-create](references/doc-create.md) 生成 `.docx` 文件。
56
- 2. 导入为企微doc文档:使用下方「导入doc文档」接口将生成的 `.docx` 文件导入为企微doc文档。注意import导入的时候 `file_name` 应和文档标题保持一致。
56
+ 2. 导入为企微doc文档:使用下方「导入doc文档」接口将生成的 `.docx` 文件导入为企微doc文档。
57
57
 
58
58
  ### 导入doc文档
59
59
 
@@ -138,3 +138,8 @@ wecom-cli doc contents get --json '<JSON 参数>'
138
138
  1. 从文档链接提取(优先):用户提供了企微文档 URL 时,直接从 URL 中解析。URL 格式为 `https://doc.weixin.qq.com/<type>/<docid>?scode=...`,取 `/<type>/` 后、`?` 前的部分即为 docid。
139
139
  2. 通过文档搜索获取(备选):用户仅提供文档名称或关键词、未给链接时,先调用 `wecomcli-doc-manage` 搜索文档,从返回结果中取 `docid`。
140
140
  3. 用户直接提供:用户明确给出了完整 `docid`,可直接使用,无需再提取或搜索。
141
+
142
+
143
+ ## 安装依赖
144
+
145
+ - `python-docx`:用于从零开始新建/生成一个docx文件。如果当前环境无此包,需要先安装。
@@ -1,6 +1,6 @@
1
1
  # 创建 docx 文件
2
2
 
3
- 模型只需写一份 JSONL 描述文件(**不需要写 Python 脚本**),由分发器 `build_docx.py` 把每条命令派发到对应函数完成 `.docx` 生成
3
+ 模型只需写一份 JSONL 描述文件(**不需要写 Python 脚本**),由分发器 `build_docx.py` 把每条命令派发到对应函数完成 `.docx` 生成。该脚本依赖 `python-docx`,需提前检查安装。
4
4
 
5
5
  # 整体工作流
6
6
 
@@ -9,7 +9,8 @@
9
9
  | 1. 写 jsonl | `Write` 工具 | 输出一个 `*.jsonl` 文件 |
10
10
  | 2. 生成 docx | `python build_docx.py <*.jsonl>` | 自动应用默认样式 → 按 `action` 派发 → 输出 `.docx` |
11
11
 
12
- > **`build_docx.py` 位置**:(即与 当前 `references/` 同级的 `scripts/` 目录下)
12
+ `build_docx.py` 位置:位于 `wecom-unified` 技能目录的scripts子目录下 `/wecom-unified/scripts/`。
13
+ 最后生成的docx文件默认位于 JSONL 所在目录的 `docx/` 子目录。
13
14
 
14
15
  # JSONL 书写规范
15
16
 
@@ -10,9 +10,8 @@ Workflow::
10
10
  (action 取值 / params 字段名 / 类型 / 取值范围)。
11
11
  任何偏差立即抛 ``TypeError``("类型错误,无法执行")。
12
12
  2. 校验通过后,再创建 docx 并按 action 派发到 ``DocxBuilder``。
13
- 3. 通过本地文件系统写出 ``.docx``,输出路径自动选取于
14
- ``WECOMAGENT_WRITABLE_DIRS`` 的第一个目录;同名文件会追加
15
- ``_1`` / ``_2`` … 后缀避免覆盖。
13
+ 3. 通过本地文件系统写出 ``.docx``,默认输出到 JSONL 文件所在目录的
14
+ ``docx/`` 子目录;同名文件会追加时间戳与进程号后缀避免覆盖。
16
15
 
17
16
  Usage::
18
17
 
@@ -22,8 +21,6 @@ Usage::
22
21
  from __future__ import annotations
23
22
 
24
23
  import argparse
25
- import base64
26
- import functools
27
24
  import io
28
25
  import json
29
26
  import os
@@ -462,97 +459,22 @@ def _spec_validate_command(cmd: Any, line_no: int) -> tuple[str, dict]:
462
459
 
463
460
 
464
461
  # ===========================================================================
465
- # Sandboxed local IO helpers
462
+ # Local IO helpers
466
463
  # ===========================================================================
467
- #
468
- # 所有 fs 读写都限制在
469
- # ``WECOMAGENT_READABLE_DIRS`` / ``WECOMAGENT_WRITABLE_DIRS`` 限定
470
- # (JSON 数组:``[{"path": "/abs/dir", "label": "..."}]``)。
471
-
472
- ENV_READABLE = "WECOMAGENT_READABLE_DIRS"
473
- ENV_WRITABLE = "WECOMAGENT_WRITABLE_DIRS"
474
464
 
475
465
  # 读入 / 写出文件的大小硬上限:30 MiB。
476
466
  # - 读入:避免一次性把巨型 JSONL 拉进内存撑爆进程;
477
- # - 写出:避免生成过大的 .docx 写入磁盘(base64 后体积更大)。
467
+ # - 写出:避免生成过大的 .docx 写入磁盘。
478
468
  MAX_FILE_SIZE_BYTES = 30 * 1024 * 1024
479
469
 
480
470
 
481
- @functools.lru_cache(maxsize=None)
482
- def _parse_roots(env_name: str) -> tuple[str, ...]:
483
- """Parse a JSON-array env var into a tuple of realpath roots (cached)."""
484
- raw = os.environ.get(env_name, "")
485
- if not raw:
486
- raise RuntimeError(f"环境变量 {env_name} 未设置或为空")
487
- parsed = json.loads(raw)
488
- if not isinstance(parsed, list):
489
- raise RuntimeError(
490
- f"{env_name} 必须是 JSON 数组,实际为 {type(parsed).__name__}"
491
- )
492
- roots: list[str] = []
493
- for it in parsed:
494
- if isinstance(it, str):
495
- it = json.loads(it)
496
- if not isinstance(it, dict):
497
- raise RuntimeError(
498
- f"{env_name} 元素必须是 dict 或 dict 的 JSON 字符串,"
499
- f"实际为 {type(it).__name__}"
500
- )
501
- p = it.get("path")
502
- if not isinstance(p, str) or not p.strip():
503
- raise RuntimeError(f"{env_name} 元素缺少有效的 path 字段: {it!r}")
504
- roots.append(os.path.realpath(p.strip()))
505
- if not roots:
506
- raise RuntimeError(f"环境变量 {env_name} 解析后为空")
507
- return tuple(roots)
508
-
509
-
510
- def _reject_relative_segments(path: str) -> None:
511
- """Reject path strings that include ``.`` or ``..`` segments such as
512
- ``./foo``, ``../bar`` or ``/abs/path/../x``.
513
-
514
- Although ``os.path.realpath`` would silently normalize these away,
515
- accepting them would bypass the contract that callers must hand in
516
- explicit, fully-qualified paths inside the sandboxed roots — and
517
- could be abused to escape the intended directory in edge cases where
518
- symlinks are present.
519
- """
520
- if not path:
521
- return
522
- for seg in path.replace("\\", "/").split("/"):
523
- if seg in (".", ".."):
524
- raise ValueError(
525
- f"路径不允许包含 './' 或 '../' 这类相对路径片段: {path!r}"
526
- )
527
-
528
-
529
- def _ensure_within(path: str, env_name: str) -> str:
530
- """Realpath ``path`` and ensure it lies within one of ``env_name``'s roots."""
531
- if not path:
532
- raise ValueError("path 不能为空")
533
- _reject_relative_segments(path)
534
- real = os.path.realpath(path)
535
- roots = _parse_roots(env_name)
536
- for root in roots:
537
- try:
538
- common = os.path.commonpath([real, root])
539
- except ValueError:
540
- continue
541
- if common == root:
542
- return real
543
- raise PermissionError(
544
- f"路径越权: {real} 不在 {env_name} 范围 {roots} 之内"
545
- )
546
-
547
-
548
471
  def _read_text(path: str) -> str:
549
- """Read a UTF-8 text file from an allowed readable directory.
472
+ """Read a UTF-8 text file without applying directory restrictions.
550
473
 
551
474
  最多读取 ``MAX_FILE_SIZE_BYTES + 1`` 字节,以便在不把超大文件
552
475
  整体载入内存的前提下判断是否超限。
553
476
  """
554
- real = _ensure_within(path, ENV_READABLE)
555
- with open(real, "rb") as f:
477
+ with open(path, "rb") as f:
556
478
  data = f.read(MAX_FILE_SIZE_BYTES + 1)
557
479
  if len(data) > MAX_FILE_SIZE_BYTES:
558
480
  raise ValueError(
@@ -562,38 +484,17 @@ def _read_text(path: str) -> str:
562
484
  return data.decode("utf-8")
563
485
 
564
486
 
565
- def _write_b64(path: str, data_b64: str, overwrite: bool = False) -> None:
566
- """Write a base64-encoded binary blob to an allowed writable directory.
567
-
568
- 这里在写入前对路径做 ``os.path.islink`` 检查并显式拒绝:
569
- - 检查 ``path``(原始入参):拦截 "目标位置本身就是软链" 的常见情况;
570
- - 检查 ``real``(realpath 结果):作为防御纵深,覆盖悬挂软链 /
571
- 竞态等 realpath 仍可能返回软链的边缘情况。
572
- """
573
- real = _ensure_within(path, ENV_WRITABLE)
574
- if os.path.islink(path) or os.path.islink(real):
575
- raise PermissionError(
576
- f"拒绝写入符号链接以避免跨目录覆盖: {path!r}"
577
- )
578
- data = base64.b64decode(data_b64, validate=True)
487
+ def _write_bytes(path: str, data: bytes) -> None:
488
+ """Write binary data without applying directory restrictions."""
579
489
  if len(data) > MAX_FILE_SIZE_BYTES:
580
490
  raise ValueError(
581
- f"输出文件过大:解码后 {len(data)} 字节,"
491
+ f"输出文件过大:{len(data)} 字节,"
582
492
  f"超过上限 {MAX_FILE_SIZE_BYTES} 字节(30 MiB)"
583
493
  )
584
494
 
585
- parent = os.path.dirname(real)
495
+ parent = os.path.dirname(os.path.abspath(path))
586
496
  os.makedirs(parent, exist_ok=True)
587
-
588
- # 创建父目录后再次解析路径,防止目录在检查和写入之间变为软链。
589
- real = _ensure_within(path, ENV_WRITABLE)
590
- if os.path.islink(path) or os.path.islink(real):
591
- raise PermissionError(
592
- f"拒绝写入符号链接以避免跨目录覆盖: {path!r}"
593
- )
594
-
595
- mode = "wb" if overwrite else "xb"
596
- with open(real, mode) as f:
497
+ with open(path, "xb") as f:
597
498
  f.write(data)
598
499
 
599
500
 
@@ -736,10 +637,10 @@ class DocxBuilder:
736
637
  def save(self, path: str) -> None:
737
638
  """Persist the document to the local filesystem.
738
639
 
739
- ``overwrite=False`` enforces the "never clobber an existing .docx"
640
+ Exclusive-create mode enforces the "never clobber an existing .docx"
740
641
  guarantee that ``_pick_output_path`` makes when picking the filename.
741
642
 
742
- 在编码 / 写入之前校验序列化后的 docx 体积不得超过
643
+ 在写入之前校验序列化后的 docx 体积不得超过
743
644
  ``MAX_FILE_SIZE_BYTES``;超限直接抛 ``ValueError`` 中止保存。
744
645
  """
745
646
  buf = io.BytesIO()
@@ -750,8 +651,7 @@ class DocxBuilder:
750
651
  f"输出文件过大:序列化后 {size} 字节,"
751
652
  f"超过上限 {MAX_FILE_SIZE_BYTES} 字节(30 MiB)"
752
653
  )
753
- data_b64 = base64.b64encode(buf.getvalue()).decode("ascii")
754
- _write_b64(path, data_b64, overwrite=False)
654
+ _write_bytes(path, buf.getvalue())
755
655
 
756
656
  # -- 2. Page setup ----------------------------------------------------
757
657
 
@@ -1317,14 +1217,16 @@ def run_jsonl(spec_path: str, output: str) -> str:
1317
1217
 
1318
1218
 
1319
1219
  def _pick_output_path(spec_path: str) -> str:
1320
- """Pick a non-conflicting ``.docx`` path under the writable root.
1220
+ """Pick a non-conflicting path beside the JSONL file.
1321
1221
 
1322
- Filename derives from the spec's stem (``report.jsonl`` → ``report.docx``);
1323
- on conflict a timestamp suffix is appended to avoid overwriting.
1222
+ The document is written to a ``docx/`` subdirectory of the JSONL file's
1223
+ directory. Its filename derives from the spec's stem
1224
+ (``report.jsonl`` → ``report.docx``); on conflict a timestamp suffix is
1225
+ appended to avoid overwriting.
1324
1226
  """
1325
- target_dir = os.path.join(_parse_roots(ENV_WRITABLE)[0], "docx")
1326
- target_dir = _ensure_within(target_dir, ENV_WRITABLE)
1327
- stem = Path(spec_path).stem or "document"
1227
+ spec = Path(spec_path).expanduser().resolve()
1228
+ target_dir = str(spec.parent / "docx")
1229
+ stem = spec.stem or "document"
1328
1230
  if not re.fullmatch(r"[A-Za-z0-9_.\-]{1,128}", stem):
1329
1231
  stem = "document"
1330
1232
 
@@ -1350,7 +1252,7 @@ def main() -> None:
1350
1252
 
1351
1253
  try:
1352
1254
  output = _pick_output_path(args.spec)
1353
- except Exception as e:
1255
+ except Exception:
1354
1256
  print("Error: failed to pick output path")
1355
1257
  sys.exit(2)
1356
1258
 
@@ -1361,9 +1263,6 @@ def main() -> None:
1361
1263
  # 转译成 "类型错误,无法执行" 提示。
1362
1264
  print(f"Error: 类型错误,无法执行: {e}", file=sys.stderr)
1363
1265
  sys.exit(2)
1364
- except PermissionError:
1365
- print("Error: 路径不在允许范围内", file=sys.stderr)
1366
- sys.exit(2)
1367
1266
  except Exception:
1368
1267
  print("Error: 执行失败,请检查输入文件格式或稍后重试", file=sys.stderr)
1369
1268
  sys.exit(2)
@@ -45,53 +45,18 @@ description: 企业微信在线表格文档管理:新建在线表格、导入
45
45
 
46
46
  ### 新建在线表格
47
47
 
48
- 从零新建一篇企微在线表格:空白,或带初始数据(二维表格数据)。**本接口不接受任何文件路径参数**——"用本地文件建/导入"走「导入在线表格」。
48
+ 从零新建企微在线表格时,先按用户需求创建一个本地 `.xlsx`,完成质量校验后,再通过 `wecom-cli sheet import` 导入为企微在线表格。
49
49
 
50
- #### 调用方式
51
-
52
- 使用 wecom-cli 工具按照下面格式调用
53
-
54
- ```
55
- wecom-cli sheet create --json '<JSON 参数>'
56
- ```
50
+ #### 标准流程
57
51
 
58
- #### 参数
59
-
60
- | 字段 | 类型 | 必填 | 默认值 | 语义 |
61
- |---|---|---|---|---|
62
- | `doc_name` | string | 是 | — | 表格标题 |
63
- | `grid_data` | object | 否 | — | 默认子表初始化数据;子结构见下方 |
64
-
65
- `grid_data` 对象结构:
66
-
67
- | 子字段 | 类型 | 必填 | 默认值 | 语义 |
68
- |---|---|---|---|---|
69
- | `start_row` / `start_column` | int | 否 | `0` | 起始行 / 列号,从 0 起 |
70
- | `rows` | array | 否 | — | 各行数据;每项 `values` 为单元格数组 |
71
- | `rows[].values[].cell_value` | object | 否 | — | 单元格值,见下方「cell_value 类型选择」 |
72
- | `rows[].values[].data_type` | string | 否 | — | 枚举:`TEXT` / `NUMBER` / `LINK` / `FORMULA` |
73
-
74
- ##### cell_value 类型选择
75
-
76
- > 硬规则:选择 `cell_value` 形态后,必须同时把同级的 `data_type` 设置为下表对应值。
77
-
78
- | 形态 | 对应 `data_type` | 结构 | 适用场景 |
79
- |---|---|---|---|
80
- | `text` | `TEXT` | `{"text": "<纯文本>"}` | 纯文本内容(如姓名、说明、标签、编号字符串等) |
81
- | `number` | `NUMBER` | `{"number": 123.45}` | 数值,用于金额、数量、比率等需要参与公式计算或聚合的数据;值为 JSON 数字类型,不加引号 |
82
- | `formula` | `FORMULA` | `{"formula": "=SUM(A1,A2)"}` | 任何以 `=` 开头的公式,包括 `=SUM(...)`、`=A1+B1`、`=IF(...)`、`=VLOOKUP(...)` 等 |
83
- | `link` | `LINK` | `{"link": {"url": "<URL>", "text": "<显示文本>"}}` | 超链接 |
84
-
85
- #### 返回
86
-
87
- | 字段 | 类型 | 说明 |
88
- |---|---|---|
89
- | `docid` | string | 新建表格 ID |
90
- | `url` | string | 表格访问链接 |
52
+ 1. 使用 `openpyxl` 创建本地 `.xlsx`
53
+ 2. 导入前校验本地 `.xlsx`:文件存在且非空、可正常打开、工作表名称与数量正确、关键数据/公式/格式符合用户要求。校验不通过时先修复本地文件,不得导入未验证产物。
54
+ 3. 调用下方「导入在线表格」的 `wecom-cli sheet import`,把已校验的 `.xlsx` 导入为企微在线表格。
55
+ 4. 确认导入结果的 `task_status` 为成功状态。向用户返回企微在线表格 URL。除非用户明确要求保留或接收本地文件,否则不要把本地 `.xlsx` 当作最终交付物。
91
56
 
92
57
  #### 使用规则
93
58
 
94
- - **何时走 import 而非本接口**:用户提到具体文件路径、或明确说"导入 / 用这个文件建",一律走「导入在线表格」。
59
+ - 不要混淆编辑场景:上述流程只适用于从零新建。修改已有企微在线表格时,继续使用读取、修改、追加、子表管理等在线接口,不得重新生成 XLSX 后覆盖导入
95
60
 
96
61
  ### 导入在线表格
97
62
 
@@ -159,7 +124,7 @@ wecom-cli sheet get --json '<JSON 参数>'
159
124
 
160
125
  > 必填参数缺失 / `docid` 多候选 / 新建 vs 导入等歧义场景,用简洁自然语言仅追问缺失或有歧义的信息;有候选项时在文字中列出供用户选择,不得自行猜测。
161
126
 
162
- #### `docid` 使用规则
127
+ ## `docid` 使用规则
163
128
 
164
129
  `docid`仅cli使用。
165
130
  最终展示用户时,不应展示 `docid`,而是使用文档 URL:
@@ -175,3 +140,7 @@ wecom-cli sheet get --json '<JSON 参数>'
175
140
  1. 从文档链接提取(优先):用户提供了企微文档 URL 时,直接从 URL 中解析。URL 格式为 `https://doc.weixin.qq.com/<type>/<docid>?scode=...`,取 `/<type>/` 后、`?` 前的部分即为 docid。
176
141
  2. 通过文档搜索获取(备选):用户仅提供文档名称或关键词、未给链接时,先调用 `wecomcli-doc-manage` 搜索文档,从返回结果中取 `docid`。
177
142
  3. 用户直接提供:用户明确给出了完整 `docid`,可直接使用,无需再提取或搜索。
143
+
144
+ ## 安装依赖
145
+
146
+ - `openpyxl`: 用于创建本地xlsx文件 / 在线表格。如果环境没有此包,必须先安装。