@wecom/wecom-openclaw-plugin 2026.3.31-beta.4 → 2026.3.31-beta.5
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 +54 -28
- package/dist/index.js +0 -56
- package/dist/src/agent/handler.js +3 -3
- package/dist/src/channel.js +7 -1
- package/dist/src/group-policy.js +5 -15
- package/dist/src/monitor.js +26 -62
- package/dist/src/webhook/gateway.js +9 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,17 +31,24 @@
|
|
|
31
31
|
- 🔗 **Dual-mode**: Bot (WebSocket / Webhook) and Agent (HTTP webhook) can run independently or together
|
|
32
32
|
- 💬 Supports both direct messages (DM) and group chat
|
|
33
33
|
- 📤 Proactive messaging to specific users, groups, departments, or tags
|
|
34
|
-
- 🖼️ Receives and processes image, voice, video, and
|
|
34
|
+
- 🖼️ Receives and processes image, voice, video, file, and **mixed (图文混排)** messages with automatic downloading
|
|
35
|
+
- 🗣️ Voice-to-text: automatically extracts transcribed text from voice messages
|
|
36
|
+
- 💬 Quote message support: processes quoted text, image, voice, and file messages
|
|
35
37
|
- ⏳ Streaming replies with "thinking" placeholder messages (Bot mode)
|
|
36
38
|
- 🔐 Agent mode: AES-256-CBC encrypted XML callbacks with SHA1 signature verification
|
|
37
39
|
- 📝 Markdown formatting support for replies
|
|
38
|
-
- 🃏 Template card messages (text_notice, news_notice, button_interaction, vote_interaction, multiple_interaction)
|
|
40
|
+
- 🃏 Template card messages (text_notice, news_notice, button_interaction, vote_interaction, multiple_interaction) with **event callback handling**
|
|
39
41
|
- 🔒 Built-in access control: DM Policy (pairing / open / allowlist / disabled) and Group Policy (open / allowlist / disabled)
|
|
42
|
+
- 🔑 Command authorization: per-account command permission control with access group support
|
|
40
43
|
- 👥 Multi-account support: run multiple WeCom accounts with independent bot/agent configs
|
|
41
|
-
- 🧩 MCP tool integration (`wecom_mcp`)
|
|
44
|
+
- 🧩 MCP tool integration (`wecom_mcp`) with interceptor pipeline (biz-error, media, smartpage-create, smartpage-export)
|
|
45
|
+
- 🎯 **15 built-in Skills**: contact lookup, doc management, todo, meeting, schedule, messaging, smartsheet, template cards, and more
|
|
42
46
|
- 🔀 Dynamic Agent routing: auto-create isolated agents per user/group
|
|
43
47
|
- 📁 Local file sending with configurable media path allowlist (`mediaLocalRoots`)
|
|
44
|
-
-
|
|
48
|
+
- 📊 Smart media size limits with auto-downgrade (image 10MB → file, video 10MB → file, voice 2MB/AMR-only → file, max 20MB)
|
|
49
|
+
- 🔄 **Bot-first, Agent-fallback** outbound delivery: auto fallback to Agent HTTP API when Bot WS is unavailable
|
|
50
|
+
- ⚡ Auto heartbeat keep-alive and reconnection (up to 10 reconnect attempts, 5 auth failure retries)
|
|
51
|
+
- 🛡️ Anti-kick protection: suppresses auto-restart on server-side disconnection to prevent mutual kicking loops
|
|
45
52
|
- 🧙 Interactive CLI setup wizard
|
|
46
53
|
|
|
47
54
|
---
|
|
@@ -152,11 +159,20 @@ The plugin supports two connection modes that can be used independently or toget
|
|
|
152
159
|
| Config Path | Description | Default |
|
|
153
160
|
|---|---|---|
|
|
154
161
|
| `channels.wecom.mediaLocalRoots` | Extra local paths allowed for media sending (supports `~`) | `[]` |
|
|
155
|
-
| `channels.wecom.media.maxBytes` | Max media file size in bytes | `
|
|
162
|
+
| `channels.wecom.media.maxBytes` | Max media file size in bytes | `20971520` (20MB) |
|
|
156
163
|
| `channels.wecom.media.tempDir` | Temp directory for media processing | — |
|
|
157
164
|
| `channels.wecom.media.retentionHours` | Media file retention hours | — |
|
|
158
165
|
| `channels.wecom.media.cleanupOnStart` | Clean temp media on startup | — |
|
|
159
166
|
|
|
167
|
+
**Media Size Limits & Auto-Downgrade:**
|
|
168
|
+
|
|
169
|
+
| Media Type | Max Size | Downgrade Behavior |
|
|
170
|
+
|---|---|---|
|
|
171
|
+
| Image | 10 MB | Exceeds → sent as file |
|
|
172
|
+
| Video | 10 MB | Exceeds → sent as file |
|
|
173
|
+
| Voice | 2 MB (AMR only) | Non-AMR format or exceeds → sent as file |
|
|
174
|
+
| File | 20 MB | Exceeds → rejected (cannot send) |
|
|
175
|
+
|
|
160
176
|
#### Network Settings
|
|
161
177
|
|
|
162
178
|
| Config Path | Description | Default |
|
|
@@ -175,13 +191,16 @@ Agent mode uses HTTP webhook callbacks with XML encrypted messages. You need to
|
|
|
175
191
|
#### Prerequisites
|
|
176
192
|
|
|
177
193
|
1. Create a self-built app in [WeCom Admin Console](https://work.weixin.qq.com/wework_admin/frame#apps)
|
|
178
|
-
2.
|
|
179
|
-
|
|
180
|
-
- **Token
|
|
181
|
-
- **
|
|
182
|
-
|
|
194
|
+
2. Note down the **CorpID**, **CorpSecret** (from app settings), and **AgentId**
|
|
195
|
+
3. In the app settings, go to "API Receive" (API接收):
|
|
196
|
+
- Note down the **Token** and **EncodingAESKey** (auto-generated or custom)
|
|
197
|
+
- **Do NOT click save yet** — WeCom will verify the callback URL immediately when you save
|
|
198
|
+
|
|
199
|
+
#### Setup Steps
|
|
183
200
|
|
|
184
|
-
|
|
201
|
+
> **Important**: You must configure the Gateway **before** saving the callback URL in WeCom admin console. WeCom sends a verification request (GET with `echostr`) immediately when you save, and the Gateway needs the `token` and `encodingAESKey` to decrypt and respond correctly.
|
|
202
|
+
|
|
203
|
+
**Step 1: Configure Gateway**
|
|
185
204
|
|
|
186
205
|
```shell
|
|
187
206
|
openclaw config set channels.wecom.agent.corpId <YOUR_CORP_ID>
|
|
@@ -193,6 +212,13 @@ openclaw config set channels.wecom.enabled true
|
|
|
193
212
|
openclaw gateway restart
|
|
194
213
|
```
|
|
195
214
|
|
|
215
|
+
**Step 2: Save callback URL in WeCom admin console**
|
|
216
|
+
|
|
217
|
+
Go back to the "API Receive" settings and enter the callback URL:
|
|
218
|
+
- **URL**: `https://<your-gateway-host>/plugins/wecom/agent/<accountId>` (e.g. `/plugins/wecom/agent/default`); single-account mode can also use `/plugins/wecom/agent`
|
|
219
|
+
|
|
220
|
+
Now click save — the verification should pass.
|
|
221
|
+
|
|
196
222
|
#### JSON Configuration
|
|
197
223
|
|
|
198
224
|
```json
|
|
@@ -255,6 +281,7 @@ The plugin uses a **Bot-first, Agent-fallback** strategy for outbound message de
|
|
|
255
281
|
This means:
|
|
256
282
|
- **Agent-only accounts** (no Bot configured) can still send proactive messages, cron deliveries, and broadcasts
|
|
257
283
|
- **Target formats** like `party:1`, `tag:Ops`, `user:zhangsan` are fully supported in both paths
|
|
284
|
+
- **Media fallback**: when Bot WS is unavailable, media files are downloaded, uploaded to WeCom via Agent API, then sent; if upload fails, falls back to text + URL
|
|
258
285
|
- No manual switching needed — the plugin handles fallback transparently
|
|
259
286
|
|
|
260
287
|
### Using Both Modes Together
|
|
@@ -320,7 +347,7 @@ Use `accounts` to configure multiple WeCom accounts, each with optional bot and/
|
|
|
320
347
|
}
|
|
321
348
|
```
|
|
322
349
|
|
|
323
|
-
> **Note**: In multi-account mode,
|
|
350
|
+
> **Note**: In multi-account mode, accounts without explicit `bindings` will not fall back to the default agent. Configure bindings for each account:
|
|
324
351
|
> ```json
|
|
325
352
|
> {
|
|
326
353
|
> "bindings": [
|
|
@@ -355,12 +382,6 @@ Dynamic Agent routing automatically creates isolated agents per user or group, e
|
|
|
355
382
|
| `channels.wecom.dynamicAgents.groupEnabled` | Enable dynamic agent for group chats | `true` |
|
|
356
383
|
| `channels.wecom.dynamicAgents.adminUsers` | Admin users (bypass dynamic routing, use main agent) | `[]` |
|
|
357
384
|
|
|
358
|
-
### Routing Configuration
|
|
359
|
-
|
|
360
|
-
| Config Path | Description | Default |
|
|
361
|
-
|---|---|---|
|
|
362
|
-
| `channels.wecom.routing.failClosedOnDefaultRoute` | Reject unbound accounts from falling back to default agent | `true` (multi-account) / `false` (single account) |
|
|
363
|
-
|
|
364
385
|
---
|
|
365
386
|
|
|
366
387
|
## 🔒 Access Control
|
|
@@ -465,17 +486,19 @@ The plugin supports scheduled message delivery via OpenClaw's built-in Cron serv
|
|
|
465
486
|
The `delivery.to` field supports the following target formats:
|
|
466
487
|
|
|
467
488
|
| Format | Target | Example |
|
|
468
|
-
|
|
489
|
+
|--------|--------|--------|
|
|
469
490
|
| `party:<id>` | Department (all members) | `party:1` (root dept = all employees) |
|
|
470
|
-
| `dept:<id>` | Department (alias) | `dept:5` |
|
|
491
|
+
| `dept:<id>` | Department (alias for party) | `dept:5` |
|
|
471
492
|
| `tag:<id>` | Tag group | `tag:Ops` |
|
|
472
493
|
| `user:<id>` | Specific user | `user:zhangsan` |
|
|
473
494
|
| `group:<id>` | External group chat | `group:wr123abc` |
|
|
474
|
-
| `chat:<id>` | Group chat (alias) | `chat:wc456def` |
|
|
495
|
+
| `chat:<id>` | Group chat (alias for group) | `chat:wc456def` |
|
|
475
496
|
| Pure number | Auto-detected as department | `1` → `party:1` |
|
|
476
497
|
| `wr...` / `wc...` | Auto-detected as group chat | `wr123` → `chatid` |
|
|
477
498
|
| Other string | Auto-detected as user | `zhangsan` → `touser` |
|
|
478
499
|
|
|
500
|
+
> **Namespace prefixes** (`wecom:`, `qywx:`, `wework:`, `wechatwork:`, `wecom-agent:`) are automatically stripped before parsing.
|
|
501
|
+
|
|
479
502
|
### Method 1: CLI (Recommended — takes effect immediately)
|
|
480
503
|
|
|
481
504
|
```shell
|
|
@@ -483,20 +506,26 @@ openclaw cron add \
|
|
|
483
506
|
--name "daily-report" \
|
|
484
507
|
--agent main \
|
|
485
508
|
--cron "0 9 * * 1-5" \
|
|
509
|
+
--tz "Asia/Shanghai" \
|
|
486
510
|
--message "Good morning! Here is your daily briefing." \
|
|
487
|
-
--
|
|
488
|
-
--
|
|
489
|
-
--
|
|
511
|
+
--announce \
|
|
512
|
+
--channel wecom \
|
|
513
|
+
--to "party:1"
|
|
490
514
|
```
|
|
491
515
|
|
|
516
|
+
> **Note**: `--announce` enables delivery mode (broadcasts the AI response to the target chat). Use `--no-deliver` to keep output internal. The deprecated `--deliver` flag is an alias for `--announce`.
|
|
517
|
+
|
|
492
518
|
Common CLI commands:
|
|
493
519
|
|
|
494
520
|
```shell
|
|
495
521
|
openclaw cron list # List all cron jobs
|
|
522
|
+
openclaw cron show <id> # Show job details
|
|
496
523
|
openclaw cron enable <id> # Enable a job
|
|
497
524
|
openclaw cron disable <id> # Disable a job
|
|
498
525
|
openclaw cron remove <id> # Remove a job
|
|
499
526
|
openclaw cron run <id> # Manually trigger a job
|
|
527
|
+
openclaw cron runs --id <id> # View run history
|
|
528
|
+
openclaw cron edit <id> --message "New prompt" # Edit a job
|
|
500
529
|
```
|
|
501
530
|
|
|
502
531
|
### Method 2: Edit `jobs.json` (requires gateway restart)
|
|
@@ -517,10 +546,7 @@ File path: `~/.openclaw/cron/jobs.json`
|
|
|
517
546
|
"wakeMode": "now",
|
|
518
547
|
"payload": {
|
|
519
548
|
"kind": "agentTurn",
|
|
520
|
-
"message": "Generate today's briefing and send it."
|
|
521
|
-
"deliver": true,
|
|
522
|
-
"channel": "wecom",
|
|
523
|
-
"to": "party:1"
|
|
549
|
+
"message": "Generate today's briefing and send it."
|
|
524
550
|
},
|
|
525
551
|
"delivery": {
|
|
526
552
|
"mode": "announce",
|
package/dist/index.js
CHANGED
|
@@ -5,57 +5,6 @@ import { setWeComRuntime } from "./src/runtime.js";
|
|
|
5
5
|
import { CHANNEL_ID, WEBHOOK_PATHS } from "./src/const.js";
|
|
6
6
|
import { createWecomAgentWebhookHandler } from "./src/agent/webhook.js";
|
|
7
7
|
import { handleWecomWebhookRequest } from "./src/webhook/index.js";
|
|
8
|
-
// ============================================================================
|
|
9
|
-
// 需要自动注入 tools.alsoAllow 的工具名列表
|
|
10
|
-
// ============================================================================
|
|
11
|
-
const REQUIRED_ALSO_ALLOW_TOOLS = ["wecom_mcp"];
|
|
12
|
-
/**
|
|
13
|
-
* 确保 tools.alsoAllow 中包含插件所需的工具名。
|
|
14
|
-
*
|
|
15
|
-
* 逻辑:
|
|
16
|
-
* 1. 读取当前配置
|
|
17
|
-
* 2. 检查 tools.alsoAllow 中是否已包含所需工具
|
|
18
|
-
* 3. 如有缺失,合并写入配置文件
|
|
19
|
-
*
|
|
20
|
-
* 幂等操作——重复调用不会产生副作用。
|
|
21
|
-
* 若 tools.allow 已显式设置(与 alsoAllow 互斥),则跳过注入并打印提示。
|
|
22
|
-
*/
|
|
23
|
-
async function ensureToolsAlsoAllow(api) {
|
|
24
|
-
try {
|
|
25
|
-
const cfg = api.runtime.config.loadConfig();
|
|
26
|
-
const tools = cfg.tools ?? {};
|
|
27
|
-
// 如果用户显式配置了 tools.allow(全量白名单),则 alsoAllow 与之互斥,
|
|
28
|
-
// 此时不应自动追加 alsoAllow,避免产生校验冲突
|
|
29
|
-
if (tools.allow && tools.allow.length > 0) {
|
|
30
|
-
const missing = REQUIRED_ALSO_ALLOW_TOOLS.filter((t) => !tools.allow.includes(t));
|
|
31
|
-
if (missing.length > 0) {
|
|
32
|
-
console.warn(`[wecom] tools.allow 已显式设置,无法自动注入 alsoAllow。` +
|
|
33
|
-
`请手动将 ${JSON.stringify(missing)} 加入 tools.allow。`);
|
|
34
|
-
}
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
const existing = tools.alsoAllow ?? [];
|
|
38
|
-
const missing = REQUIRED_ALSO_ALLOW_TOOLS.filter((t) => !existing.includes(t));
|
|
39
|
-
if (missing.length === 0) {
|
|
40
|
-
// 所有工具已在白名单中,无需操作
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
const merged = [...existing, ...missing];
|
|
44
|
-
const nextConfig = {
|
|
45
|
-
...cfg,
|
|
46
|
-
tools: {
|
|
47
|
-
...tools,
|
|
48
|
-
alsoAllow: merged,
|
|
49
|
-
},
|
|
50
|
-
};
|
|
51
|
-
await api.runtime.config.writeConfigFile(nextConfig);
|
|
52
|
-
console.log(`[wecom] 已自动将 ${JSON.stringify(missing)} 加入 tools.alsoAllow`);
|
|
53
|
-
}
|
|
54
|
-
catch (err) {
|
|
55
|
-
// 配置写入失败不应阻断 Gateway 启动
|
|
56
|
-
console.error(`[wecom] 自动注入 tools.alsoAllow 失败:`, err instanceof Error ? err.message : String(err));
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
8
|
const plugin = {
|
|
60
9
|
id: "wecom-openclaw-plugin",
|
|
61
10
|
name: "企业微信",
|
|
@@ -91,11 +40,6 @@ const plugin = {
|
|
|
91
40
|
match: "prefix",
|
|
92
41
|
});
|
|
93
42
|
}
|
|
94
|
-
// ── Gateway 启动时自动确保 tools.alsoAllow 包含 wecom_mcp ──────────
|
|
95
|
-
// 在 gateway_start 阶段检测并写入,保证插件安装/更新后首次启动即生效
|
|
96
|
-
// api.on("gateway_start", async () => {
|
|
97
|
-
// await ensureToolsAlsoAllow(api);
|
|
98
|
-
// });
|
|
99
43
|
// 注入媒体发送指令和文件大小限制提示词(仅对企业微信 channel 生效)
|
|
100
44
|
api.on("before_prompt_build", (_event, ctx) => {
|
|
101
45
|
// 只在企业微信 channel 的会话中注入,避免影响其他 channel 插件
|
|
@@ -9,7 +9,7 @@ import { extractMsgType, extractFromUser, extractContent, extractChatId, extract
|
|
|
9
9
|
import { sendText, downloadMedia, uploadMedia, sendMedia as sendAgentMedia } from "./api-client.js";
|
|
10
10
|
import { buildWecomUnauthorizedCommandPrompt, resolveWecomCommandAuthorization } from "../shared/command-auth.js";
|
|
11
11
|
import { processDynamicRouting } from "../dynamic-routing.js";
|
|
12
|
-
import { DEFAULT_MEDIA_MAX_MB } from "../const.js";
|
|
12
|
+
import { CHANNEL_ID, DEFAULT_MEDIA_MAX_MB } from "../const.js";
|
|
13
13
|
function resolveWecomMediaMaxBytes(config) {
|
|
14
14
|
return config.channels?.wecom?.media?.maxBytes ?? DEFAULT_MEDIA_MAX_MB * 1024 * 1024;
|
|
15
15
|
}
|
|
@@ -404,9 +404,9 @@ async function processAgentMessage(params) {
|
|
|
404
404
|
ConversationLabel: fromLabel,
|
|
405
405
|
SenderName: fromUser,
|
|
406
406
|
SenderId: fromUser,
|
|
407
|
-
Provider:
|
|
407
|
+
Provider: CHANNEL_ID,
|
|
408
408
|
Surface: "webchat",
|
|
409
|
-
OriginatingChannel:
|
|
409
|
+
OriginatingChannel: CHANNEL_ID,
|
|
410
410
|
// 标记为 Agent 会话的回复路由目标,避免与 Bot 会话混淆:
|
|
411
411
|
// - 用于让 /new /reset 这类命令回执不被 Bot 侧策略拦截
|
|
412
412
|
// - 群聊场景也统一路由为私信触发者(与 deliver 策略一致)
|
package/dist/src/channel.js
CHANGED
|
@@ -432,7 +432,13 @@ export const wecomPlugin = {
|
|
|
432
432
|
WEBHOOK_PATHS.AGENT_PLUGIN,
|
|
433
433
|
WEBHOOK_PATHS.AGENT,
|
|
434
434
|
]
|
|
435
|
-
: [
|
|
435
|
+
: [
|
|
436
|
+
// 单账号模式:同时注册 /default 路径以支持显式指定
|
|
437
|
+
WEBHOOK_PATHS.AGENT_PLUGIN,
|
|
438
|
+
WEBHOOK_PATHS.AGENT,
|
|
439
|
+
`${WEBHOOK_PATHS.AGENT_PLUGIN}/${DEFAULT_ACCOUNT_ID}`,
|
|
440
|
+
`${WEBHOOK_PATHS.AGENT}/${DEFAULT_ACCOUNT_ID}`,
|
|
441
|
+
];
|
|
436
442
|
for (const p of paths) {
|
|
437
443
|
registerAgentWebhookTarget({
|
|
438
444
|
agent,
|
package/dist/src/group-policy.js
CHANGED
|
@@ -76,21 +76,11 @@ function isGroupSenderAllowed(params) {
|
|
|
76
76
|
* @returns 检查结果,包含是否允许继续处理
|
|
77
77
|
*/
|
|
78
78
|
export function checkGroupPolicy(params) {
|
|
79
|
-
const { chatId, senderId, account,
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
// providerConfigPresent: config.channels?.[CHANNEL_ID] !== undefined,
|
|
85
|
-
// groupPolicy: wecomConfig.groupPolicy,
|
|
86
|
-
// defaultGroupPolicy,
|
|
87
|
-
// });
|
|
88
|
-
// warnMissingProviderGroupPolicyFallbackOnce({
|
|
89
|
-
// providerMissingFallbackApplied,
|
|
90
|
-
// providerKey: CHANNEL_ID,
|
|
91
|
-
// accountId: account.accountId,
|
|
92
|
-
// log: (msg) => runtime.log?.(msg),
|
|
93
|
-
// });
|
|
79
|
+
const { chatId, senderId, account, runtime } = params;
|
|
80
|
+
// 使用 account.config(已经过多账号合并),而非顶层 config.channels.wecom
|
|
81
|
+
// 避免多账户模式下 groupAllowFrom / groups 等字段取不到账号级配置
|
|
82
|
+
const wecomConfig = account.config;
|
|
83
|
+
const groupPolicy = wecomConfig.groupPolicy ?? "open";
|
|
94
84
|
const groupAllowFrom = wecomConfig.groupAllowFrom ?? [];
|
|
95
85
|
const groupAllowed = isWeComGroupAllowed({
|
|
96
86
|
groupPolicy,
|
package/dist/src/monitor.js
CHANGED
|
@@ -22,7 +22,7 @@ import { getWeComRuntime } from "./runtime.js";
|
|
|
22
22
|
import { getDefaultMediaLocalRoots, resolveStateDir } from "./openclaw-compat.js";
|
|
23
23
|
import { CHANNEL_ID, THINKING_MESSAGE, MEDIA_IMAGE_PLACEHOLDER, MEDIA_DOCUMENT_PLACEHOLDER, WS_HEARTBEAT_INTERVAL_MS, WS_MAX_RECONNECT_ATTEMPTS, WS_MAX_AUTH_FAILURE_ATTEMPTS, EVENT_ENTER_CHECK_UPDATE, CMD_ENTER_EVENT_REPLY, SCENE_WECOM_OPENCLAW, } from "./const.js";
|
|
24
24
|
import { parseMessageContent } from "./message-parser.js";
|
|
25
|
-
import { sendWeComReply,
|
|
25
|
+
import { sendWeComReply, StreamExpiredError } from "./message-sender.js";
|
|
26
26
|
import { downloadAndSaveImages, downloadAndSaveFiles } from "./media-handler.js";
|
|
27
27
|
import { uploadAndSendMedia } from "./media-uploader.js";
|
|
28
28
|
import { maskTemplateCardBlocks } from "./template-card-parser.js";
|
|
@@ -32,14 +32,6 @@ import { checkDmPolicy } from "./dm-policy.js";
|
|
|
32
32
|
import { setWeComWebSocket, setMessageState, deleteMessageState, setReqIdForChat, warmupReqIdStore, startMessageStateCleanup, stopMessageStateCleanup, cleanupAccount, } from "./state-manager.js";
|
|
33
33
|
import { PLUGIN_VERSION } from "./version.js";
|
|
34
34
|
import { processDynamicRouting } from "./dynamic-routing.js";
|
|
35
|
-
/**
|
|
36
|
-
* 去除文本中的 `<think>...</think>` 标签(支持跨行),返回剩余可见文本。
|
|
37
|
-
* 用于判断大模型回复中是否包含实际用户可见内容(而非仅有 thinking 推理过程)。
|
|
38
|
-
*/
|
|
39
|
-
function stripThinkTags(text) {
|
|
40
|
-
return text;
|
|
41
|
-
// return text.replace(/<think>[\s\S]*?<\/think>/g, "").trim();
|
|
42
|
-
}
|
|
43
35
|
// ============================================================================
|
|
44
36
|
// 媒体本地路径白名单扩展
|
|
45
37
|
// ============================================================================
|
|
@@ -270,7 +262,7 @@ async function finishThinkingStream(ctx) {
|
|
|
270
262
|
const { wsClient, frame, state, runtime } = ctx;
|
|
271
263
|
const body = frame.body;
|
|
272
264
|
const chatId = body.chatid || body.from.userid;
|
|
273
|
-
const visibleText =
|
|
265
|
+
const visibleText = state.accumulatedText;
|
|
274
266
|
let finishText = state.accumulatedText;
|
|
275
267
|
if (visibleText) {
|
|
276
268
|
finishText = state.accumulatedText;
|
|
@@ -348,20 +340,6 @@ async function routeAndDispatchMessage(params) {
|
|
|
348
340
|
await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
349
341
|
ctx: ctxPayload,
|
|
350
342
|
cfg: config,
|
|
351
|
-
// replyResolver: async (ctx, opts) => {
|
|
352
|
-
// const startTime = Date.now();
|
|
353
|
-
// const TEN_MINUTES = 7 * 60 * 1000;
|
|
354
|
-
//
|
|
355
|
-
// console.log('开始输出内容');
|
|
356
|
-
//
|
|
357
|
-
// while (Date.now() - startTime < TEN_MINUTES) {
|
|
358
|
-
// // 每隔一段时间发送一个 block reply
|
|
359
|
-
// await opts?.onBlockReply?.({ text: `输出内容 ${new Date().toISOString()}` });
|
|
360
|
-
// await new Promise(resolve => setTimeout(resolve, 30 * 1000)); // 每5秒输出一次
|
|
361
|
-
// }
|
|
362
|
-
//
|
|
363
|
-
// return { text: "10分钟输出完成" }; // 最终回复
|
|
364
|
-
// },
|
|
365
343
|
dispatcherOptions: {
|
|
366
344
|
onReplyStart: async () => {
|
|
367
345
|
if (!isShowThink && state.streamId && !state.accumulatedText) {
|
|
@@ -375,10 +353,10 @@ async function routeAndDispatchMessage(params) {
|
|
|
375
353
|
}
|
|
376
354
|
},
|
|
377
355
|
deliver: async (payload, info) => {
|
|
378
|
-
|
|
379
|
-
//
|
|
356
|
+
runtime.log?.(`[openclaw -> plugin] kind=${info.kind}, text=${payload.text ?? ''}, mediaUrl=${payload.mediaUrl ?? ''}, mediaUrls=${JSON.stringify(payload.mediaUrls ?? [])}`);
|
|
357
|
+
// 累积文本
|
|
380
358
|
if (payload.text) {
|
|
381
|
-
state.accumulatedText
|
|
359
|
+
state.accumulatedText += `${payload.text || ''}`;
|
|
382
360
|
}
|
|
383
361
|
// 发送媒体(统一走主动发送)
|
|
384
362
|
const mediaUrls = payload.mediaUrls?.length ? payload.mediaUrls : payload.mediaUrl ? [payload.mediaUrl] : [];
|
|
@@ -398,41 +376,30 @@ async function routeAndDispatchMessage(params) {
|
|
|
398
376
|
runtime.error?.(`[wecom] sendMediaBatch threw: ${errMsg}`);
|
|
399
377
|
}
|
|
400
378
|
}
|
|
401
|
-
//
|
|
402
|
-
// deliver 只负责文本累积和媒体发送
|
|
403
|
-
},
|
|
404
|
-
onError: (err, info) => {
|
|
405
|
-
runtime.error?.(`[wecom] ${info.kind} reply failed: ${String(err)}`);
|
|
406
|
-
},
|
|
407
|
-
},
|
|
408
|
-
// onPartialReply:token 级别的流式 UI 刷新
|
|
409
|
-
// 使用 SDK 的 replyStreamNonBlocking:上一条同 reqId 的消息未收到 ack 时跳过本次发送,
|
|
410
|
-
// 避免流式中间帧排队积压导致延迟。丢帧在流式场景下可接受(text 是累积文本,最终由 finishThinkingStream 兜底)。
|
|
411
|
-
replyOptions: {
|
|
412
|
-
onPartialReply: (data) => {
|
|
413
|
-
// 累积文本
|
|
414
|
-
if (data.text) {
|
|
415
|
-
state.accumulatedText = (data.text || '');
|
|
416
|
-
}
|
|
417
|
-
if (!state.streamId || !data.text || state.streamExpired)
|
|
418
|
-
return;
|
|
379
|
+
// 中间帧:有可见文本时流式更新(流式过期后跳过,等 deliver 完成后主动发送)
|
|
419
380
|
// 使用 maskTemplateCardBlocks 遮罩正在构建中的模板卡片代码块,
|
|
420
381
|
// 避免 JSON 源码在流式输出过程中暴露给终端用户
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
streamId: state.streamId,
|
|
429
|
-
}).catch((err) => {
|
|
430
|
-
if (err instanceof StreamExpiredError) {
|
|
431
|
-
state.streamExpired = true;
|
|
432
|
-
runtime.log?.(`[wecom] Stream expired during partial reply, will fallback to proactive send`);
|
|
382
|
+
if (state.accumulatedText && !state.streamExpired) {
|
|
383
|
+
try {
|
|
384
|
+
const displayText = maskTemplateCardBlocks(state.accumulatedText, (...args) => runtime.log?.(...args));
|
|
385
|
+
// if (displayText !== state.accumulatedText) {
|
|
386
|
+
// runtime.log?.(`[wecom][template-card] Mid-frame masked: original=${state.accumulatedText.length}chars, masked=${displayText.length}chars`);
|
|
387
|
+
// }
|
|
388
|
+
await sendWeComReply({ wsClient, frame, text: displayText, runtime, finish: false, streamId: state.streamId });
|
|
433
389
|
}
|
|
434
|
-
|
|
435
|
-
|
|
390
|
+
catch (err) {
|
|
391
|
+
if (err instanceof StreamExpiredError) {
|
|
392
|
+
state.streamExpired = true;
|
|
393
|
+
runtime.log?.(`[wecom] Stream expired during intermediate reply, will fallback to proactive send`);
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
throw err;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
},
|
|
401
|
+
onError: (err, info) => {
|
|
402
|
+
runtime.error?.(`[wecom] ${info.kind} reply failed: ${String(err)}`);
|
|
436
403
|
},
|
|
437
404
|
},
|
|
438
405
|
});
|
|
@@ -628,7 +595,6 @@ function createSdkLogger(runtime, accountId) {
|
|
|
628
595
|
export async function monitorWeComProvider(options) {
|
|
629
596
|
const { account, config, runtime, abortSignal, setStatus } = options;
|
|
630
597
|
runtime.log?.(`[${account.accountId}] [${PLUGIN_VERSION}] Initializing WSClient with SDK...`);
|
|
631
|
-
runtime.error?.(`[${account.accountId}] [diag] monitor boot marker: build=20260325-event-debug-1`);
|
|
632
598
|
// 启动消息状态定期清理
|
|
633
599
|
startMessageStateCleanup();
|
|
634
600
|
return new Promise((resolve, reject) => {
|
|
@@ -802,7 +768,6 @@ export async function monitorWeComProvider(options) {
|
|
|
802
768
|
const eventBody = frame.body;
|
|
803
769
|
const eventType = eventBody.event?.eventtype;
|
|
804
770
|
runtime.log?.(`[${account.accountId}] Received event callback: eventtype=${eventType ?? ""}, msgid=${eventBody.msgid ?? ""}`);
|
|
805
|
-
runtime.error?.(`[${account.accountId}] [diag] event-listener fired: eventtype=${eventType ?? ""}, msgid=${eventBody.msgid ?? ""}`);
|
|
806
771
|
if (eventType !== "template_card_event") {
|
|
807
772
|
return;
|
|
808
773
|
}
|
|
@@ -835,7 +800,6 @@ export async function monitorWeComProvider(options) {
|
|
|
835
800
|
}
|
|
836
801
|
});
|
|
837
802
|
runtime.log?.(`[${account.accountId}] Event listeners attached: message + event(template_card_event)`);
|
|
838
|
-
runtime.error?.(`[${account.accountId}] [diag] listeners-ready marker`);
|
|
839
803
|
// 启动前预热 reqId 缓存,确保完成后再建立连接,避免 getSync 在预热完成前返回 undefined
|
|
840
804
|
warmupReqIdStore(account.accountId, (...args) => runtime.log?.(...args))
|
|
841
805
|
.then((count) => {
|
|
@@ -15,6 +15,7 @@ import { monitorState } from "./state.js";
|
|
|
15
15
|
import { registerWecomWebhookTarget, hasActiveTargets } from "./target.js";
|
|
16
16
|
import { startAgentForStream } from "./monitor.js";
|
|
17
17
|
import { hasMultiAccounts } from "../accounts.js";
|
|
18
|
+
import { DEFAULT_ACCOUNT_ID } from "../openclaw-compat.js";
|
|
18
19
|
import { getWeComRuntime } from "../runtime.js";
|
|
19
20
|
// ============================================================================
|
|
20
21
|
// 全局状态
|
|
@@ -51,7 +52,14 @@ function resolveBotRegistrationPaths(params) {
|
|
|
51
52
|
WEBHOOK_PATHS.BOT_ALT,
|
|
52
53
|
]);
|
|
53
54
|
}
|
|
54
|
-
|
|
55
|
+
// 单账号模式:同时注册 /default 路径以支持显式指定
|
|
56
|
+
return uniquePaths([
|
|
57
|
+
WEBHOOK_PATHS.BOT_PLUGIN,
|
|
58
|
+
WEBHOOK_PATHS.BOT,
|
|
59
|
+
WEBHOOK_PATHS.BOT_ALT,
|
|
60
|
+
`${WEBHOOK_PATHS.BOT_PLUGIN}/${DEFAULT_ACCOUNT_ID}`,
|
|
61
|
+
`${WEBHOOK_PATHS.BOT_ALT}/${DEFAULT_ACCOUNT_ID}`,
|
|
62
|
+
]);
|
|
55
63
|
}
|
|
56
64
|
// ============================================================================
|
|
57
65
|
// 公共 API
|