@wecom/wecom-openclaw-plugin 2026.8.17 → 2026.9.15-beta.1
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
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
|
@@ -35,8 +35,8 @@ description: 企微 doc 内容操作技能,包含新建在线文档、导入
|
|
|
35
35
|
|
|
36
36
|
| 用户意图 | 参考位置 |
|
|
37
37
|
|---|---------------------------------------------------------------|
|
|
38
|
-
|
|
|
39
|
-
|
|
|
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
|
-
|
|
53
|
+
仅适用于用户**没有本地文件,需要从零开始创建doc文档**的场景,统一走「生成 `.docx` → 导入」两步流程:
|
|
54
54
|
|
|
55
55
|
1. 生成 `.docx` 文件:按 [+doc-create](references/doc-create.md) 生成 `.docx` 文件。
|
|
56
|
-
2. 导入为企微doc文档:使用下方「导入doc文档」接口将生成的 `.docx` 文件导入为企微doc
|
|
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
|
-
|
|
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
|
-
``
|
|
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
|
-
#
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
566
|
-
"""Write
|
|
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"
|
|
491
|
+
f"输出文件过大:{len(data)} 字节,"
|
|
582
492
|
f"超过上限 {MAX_FILE_SIZE_BYTES} 字节(30 MiB)"
|
|
583
493
|
)
|
|
584
494
|
|
|
585
|
-
parent = os.path.dirname(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1220
|
+
"""Pick a non-conflicting path beside the JSONL file.
|
|
1321
1221
|
|
|
1322
|
-
|
|
1323
|
-
|
|
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
|
-
|
|
1326
|
-
target_dir =
|
|
1327
|
-
stem =
|
|
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
|
|
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
|
-
-
|
|
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
|
-
|
|
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文件 / 在线表格。如果环境没有此包,必须先安装。
|