@yanhaidao/wecom 2.3.4 → 2.3.9
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 +213 -339
- package/assets/03.bot.page.png +0 -0
- package/changelog/v2.3.9.md +22 -0
- package/compat-single-account.md +32 -2
- package/index.ts +5 -5
- package/package.json +8 -7
- package/src/agent/api-client.upload.test.ts +1 -2
- package/src/agent/handler.ts +82 -9
- package/src/agent/index.ts +1 -1
- package/src/app/account-runtime.ts +245 -0
- package/src/app/bootstrap.ts +29 -0
- package/src/app/index.ts +31 -0
- package/src/capability/agent/delivery-service.ts +79 -0
- package/src/capability/agent/fallback-policy.ts +13 -0
- package/src/capability/agent/index.ts +3 -0
- package/src/capability/agent/ingress-service.ts +38 -0
- package/src/capability/bot/dispatch-config.ts +47 -0
- package/src/capability/bot/fallback-delivery.ts +178 -0
- package/src/capability/bot/index.ts +1 -0
- package/src/capability/bot/local-path-delivery.ts +215 -0
- package/src/capability/bot/service.ts +56 -0
- package/src/capability/bot/stream-delivery.ts +379 -0
- package/src/capability/bot/stream-finalizer.ts +120 -0
- package/src/capability/bot/stream-orchestrator.ts +352 -0
- package/src/capability/bot/types.ts +8 -0
- package/src/capability/index.ts +2 -0
- package/src/channel.lifecycle.test.ts +9 -6
- package/src/channel.meta.test.ts +12 -0
- package/src/channel.ts +48 -21
- package/src/config/accounts.ts +223 -283
- package/src/config/derived-paths.test.ts +111 -0
- package/src/config/derived-paths.ts +41 -0
- package/src/config/index.ts +10 -12
- package/src/config/runtime-config.ts +46 -0
- package/src/config/schema.ts +59 -102
- package/src/domain/models.ts +7 -0
- package/src/domain/policies.ts +36 -0
- package/src/dynamic-agent.ts +6 -0
- package/src/gateway-monitor.ts +43 -93
- package/src/http.ts +23 -2
- package/src/monitor/limits.ts +7 -0
- package/src/monitor/state.ts +28 -508
- package/src/monitor.active.test.ts +3 -3
- package/src/monitor.integration.test.ts +0 -1
- package/src/monitor.ts +64 -2603
- package/src/monitor.webhook.test.ts +127 -42
- package/src/observability/audit-log.ts +48 -0
- package/src/observability/legacy-operational-event-store.ts +36 -0
- package/src/observability/raw-envelope-log.ts +28 -0
- package/src/observability/status-registry.ts +13 -0
- package/src/observability/transport-session-view.ts +14 -0
- package/src/onboarding.test.ts +219 -0
- package/src/onboarding.ts +88 -71
- package/src/outbound.test.ts +5 -5
- package/src/outbound.ts +18 -66
- package/src/runtime/dispatcher.ts +52 -0
- package/src/runtime/index.ts +4 -0
- package/src/runtime/outbound-intent.ts +4 -0
- package/src/runtime/reply-orchestrator.test.ts +38 -0
- package/src/runtime/reply-orchestrator.ts +55 -0
- package/src/runtime/routing-bridge.ts +19 -0
- package/src/runtime/session-manager.ts +76 -0
- package/src/runtime.ts +7 -14
- package/src/shared/command-auth.ts +1 -17
- package/src/shared/media-service.ts +36 -0
- package/src/shared/media-types.ts +5 -0
- package/src/store/active-reply-store.ts +42 -0
- package/src/store/interfaces.ts +11 -0
- package/src/store/memory-store.ts +43 -0
- package/src/store/stream-batch-store.ts +350 -0
- package/src/target.ts +28 -0
- package/src/transport/agent-api/client.ts +44 -0
- package/src/transport/agent-api/core.ts +367 -0
- package/src/transport/agent-api/delivery.ts +41 -0
- package/src/transport/agent-api/media-upload.ts +11 -0
- package/src/transport/agent-api/reply.ts +39 -0
- package/src/transport/agent-callback/http-handler.ts +47 -0
- package/src/transport/agent-callback/inbound.ts +5 -0
- package/src/transport/agent-callback/reply.ts +13 -0
- package/src/transport/agent-callback/request-handler.ts +244 -0
- package/src/transport/agent-callback/session.ts +23 -0
- package/src/transport/bot-webhook/active-reply.ts +36 -0
- package/src/transport/bot-webhook/http-handler.ts +48 -0
- package/src/transport/bot-webhook/inbound-normalizer.ts +371 -0
- package/src/transport/bot-webhook/inbound.ts +5 -0
- package/src/transport/bot-webhook/message-shape.ts +89 -0
- package/src/transport/bot-webhook/protocol.ts +148 -0
- package/src/transport/bot-webhook/reply.ts +15 -0
- package/src/transport/bot-webhook/request-handler.ts +394 -0
- package/src/transport/bot-webhook/session.ts +23 -0
- package/src/transport/bot-ws/inbound.ts +109 -0
- package/src/transport/bot-ws/reply.ts +48 -0
- package/src/transport/bot-ws/sdk-adapter.ts +180 -0
- package/src/transport/bot-ws/session.ts +28 -0
- package/src/transport/http/common.ts +109 -0
- package/src/transport/http/registry.ts +92 -0
- package/src/transport/http/request-handler.ts +84 -0
- package/src/transport/index.ts +14 -0
- package/src/types/account.ts +56 -91
- package/src/types/config.ts +59 -112
- package/src/types/constants.ts +20 -35
- package/src/types/events.ts +21 -0
- package/src/types/index.ts +14 -38
- package/src/types/legacy-stream.ts +50 -0
- package/src/types/runtime-context.ts +28 -0
- package/src/types/runtime.ts +161 -0
- package/src/agent/api-client.ts +0 -383
- package/src/monitor/types.ts +0 -136
package/assets/03.bot.page.png
CHANGED
|
Binary file
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# OpenClaw WeCom 插件 v2.3.9 变更简报
|
|
2
|
+
|
|
3
|
+
> [!TIP]
|
|
4
|
+
> **易用性与可观测性版本**:`v2.3.9` 重点优化默认 Bot WebSocket 接入体验、中文 onboarding、Bot/Agent 回调稳定性与排障日志。
|
|
5
|
+
|
|
6
|
+
## 2026-03-09(v2.3.9)
|
|
7
|
+
- 【默认接入】🚀 Bot onboarding 默认收敛为 `WebSocket` 模式,直接引导填写 `bot.ws.botId` 与 `bot.ws.secret`,新用户无需域名即可接入。
|
|
8
|
+
- 【主动消息】📣 在引导文案中明确 Bot WS 支持主动发消息,适合定时任务、异常提醒、工作流通知等企业场景。
|
|
9
|
+
- 【中文体验】🈶 企业微信插件自管账号创建与私聊策略配置流程,减少重复英文提示与术语负担。
|
|
10
|
+
- 【流式修复】🌊 修复 Bot WS 仅输出最终结果的问题,恢复块级流式交付能力。
|
|
11
|
+
- 【路由稳定】🧭 统一 HTTP 分发链路,并补齐默认账号的显式 Bot/Agent 回调路径别名,降低回调命中偏差。
|
|
12
|
+
- 【排障增强】🔎 增加 Agent callback 原始请求、解密摘要、发送请求与响应日志,便于定位“能接收但不能回复”的问题。
|
|
13
|
+
- 【配置清理】🧹 移除 `bot.enabled` / `agent.enabled` 冗余字段,README 与配置模型保持一致。
|
|
14
|
+
|
|
15
|
+
## 验证结果
|
|
16
|
+
- `bunx vitest run extensions/wecom/src/onboarding.test.ts extensions/wecom/src/channel.meta.test.ts`
|
|
17
|
+
- `pnpm build`
|
|
18
|
+
|
|
19
|
+
## 升级提示
|
|
20
|
+
- 默认推荐继续使用 `Bot WebSocket` 模式;如需公网回调,可手动补充 `bot.webhook`。
|
|
21
|
+
- 推荐回调路径使用 `/plugins/wecom/bot/{accountId}` 与 `/plugins/wecom/agent/{accountId}`。
|
|
22
|
+
- `package.json` 与插件元数据已同步更新为 `WeCom (企业微信)`,便于在 OpenClaw 渠道列表中直接识别。
|
package/compat-single-account.md
CHANGED
|
@@ -18,6 +18,7 @@ openclaw config set channels.wecom.enabled true
|
|
|
18
18
|
openclaw config set channels.wecom.bot.token "YOUR_BOT_TOKEN"
|
|
19
19
|
openclaw config set channels.wecom.bot.encodingAESKey "YOUR_BOT_AES_KEY"
|
|
20
20
|
openclaw config set channels.wecom.bot.receiveId ""
|
|
21
|
+
openclaw config set channels.wecom.bot.primaryTransport "webhook"
|
|
21
22
|
openclaw config set channels.wecom.bot.streamPlaceholderContent "正在思考..."
|
|
22
23
|
openclaw config set channels.wecom.bot.welcomeText "你好!我是 AI 助手"
|
|
23
24
|
|
|
@@ -108,8 +109,37 @@ openclaw channels status
|
|
|
108
109
|
|
|
109
110
|
## A.4 Webhook 路径
|
|
110
111
|
|
|
111
|
-
- Bot: `/wecom
|
|
112
|
-
- Agent: `/wecom/agent`
|
|
112
|
+
- Bot Webhook: `/plugins/wecom/bot`(推荐),兼容 `/wecom/bot`、`/wecom`
|
|
113
|
+
- Agent Callback: `/plugins/wecom/agent`(推荐),兼容 `/wecom/agent`
|
|
114
|
+
|
|
115
|
+
说明:
|
|
116
|
+
|
|
117
|
+
- 路径由系统派生,不建议额外维护自定义 path。
|
|
118
|
+
- 如果 Bot 主 transport 改成 `ws`,则 Bot 不再依赖 HTTP callback,但 Agent Callback 仍可保留。
|
|
119
|
+
|
|
120
|
+
## A.4.1 Bot WS 单账号示例
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
openclaw config set channels.wecom.bot.primaryTransport "ws"
|
|
124
|
+
openclaw config set channels.wecom.bot.ws.botId "YOUR_BOT_ID"
|
|
125
|
+
openclaw config set channels.wecom.bot.ws.secret "YOUR_BOT_SECRET"
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
运维检查:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
openclaw channels status --deep
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
重点看:
|
|
135
|
+
|
|
136
|
+
- `primaryTransport`
|
|
137
|
+
- `transport`
|
|
138
|
+
- `health`
|
|
139
|
+
- `ownerId`
|
|
140
|
+
- `lastError`
|
|
141
|
+
- `lastInboundAt`
|
|
142
|
+
- `lastOutboundAt`
|
|
113
143
|
|
|
114
144
|
## A.5 迁移建议
|
|
115
145
|
|
package/index.ts
CHANGED
|
@@ -10,16 +10,16 @@ import { wecomPlugin } from "./src/channel.js";
|
|
|
10
10
|
|
|
11
11
|
const plugin = {
|
|
12
12
|
id: "wecom",
|
|
13
|
-
name: "WeCom",
|
|
14
|
-
description: "
|
|
13
|
+
name: "WeCom (企业微信)",
|
|
14
|
+
description: "企业微信官方推荐三方插件,默认 Bot WS,支持主动发消息与统一运行时能力",
|
|
15
15
|
configSchema: emptyPluginConfigSchema(),
|
|
16
16
|
/**
|
|
17
17
|
* **register (注册插件)**
|
|
18
18
|
*
|
|
19
19
|
* OpenClaw 插件入口点。
|
|
20
|
-
* 1.
|
|
21
|
-
* 2. 注册 WeCom
|
|
22
|
-
* 3.
|
|
20
|
+
* 1. 注入统一 runtime compatibility layer。
|
|
21
|
+
* 2. 注册 capability-first WeCom 渠道插件。
|
|
22
|
+
* 3. 注册统一 HTTP 入口(所有 webhook 请求都走共享路由器)。
|
|
23
23
|
*/
|
|
24
24
|
register(api: OpenClawPluginApi) {
|
|
25
25
|
setWecomRuntime(api.runtime);
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yanhaidao/wecom",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.9",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "OpenClaw WeCom
|
|
5
|
+
"description": "OpenClaw 企业微信(WeCom)插件,默认 Bot WebSocket,支持 Agent 主动发消息与多账号接入",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "git+https://github.com/YanHaidao/wecom.git"
|
|
@@ -18,12 +18,12 @@
|
|
|
18
18
|
],
|
|
19
19
|
"channel": {
|
|
20
20
|
"id": "wecom",
|
|
21
|
-
"label": "WeCom",
|
|
22
|
-
"selectionLabel": "WeCom (
|
|
23
|
-
"detailLabel": "WeCom
|
|
21
|
+
"label": "WeCom (企业微信)",
|
|
22
|
+
"selectionLabel": "WeCom (企业微信)",
|
|
23
|
+
"detailLabel": "WeCom (企业微信)",
|
|
24
24
|
"docsPath": "/channels/wecom",
|
|
25
|
-
"docsLabel": "
|
|
26
|
-
"blurb": "
|
|
25
|
+
"docsLabel": "企业微信",
|
|
26
|
+
"blurb": "企业微信官方推荐三方插件,默认 Bot WS 配置简单,支持主动发消息与 Agent 全能力。",
|
|
27
27
|
"aliases": [
|
|
28
28
|
"wechatwork",
|
|
29
29
|
"wework",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
}
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
+
"@wecom/aibot-node-sdk": "^1.0.0",
|
|
43
44
|
"fast-xml-parser": "5.3.4",
|
|
44
45
|
"undici": "^7.20.0",
|
|
45
46
|
"zod": "^4.3.6"
|
|
@@ -15,12 +15,11 @@ vi.mock("../config/index.js", () => ({
|
|
|
15
15
|
resolveWecomEgressProxyUrlFromNetwork: resolveProxyMock,
|
|
16
16
|
}));
|
|
17
17
|
|
|
18
|
-
import { uploadMedia } from "
|
|
18
|
+
import { uploadMedia } from "../transport/agent-api/core.js";
|
|
19
19
|
|
|
20
20
|
function createAgent(agentId: number): ResolvedAgentAccount {
|
|
21
21
|
return {
|
|
22
22
|
accountId: `acct-${agentId}`,
|
|
23
|
-
enabled: true,
|
|
24
23
|
configured: true,
|
|
25
24
|
corpId: "corp",
|
|
26
25
|
corpSecret: "secret",
|
package/src/agent/handler.ts
CHANGED
|
@@ -18,12 +18,14 @@ import {
|
|
|
18
18
|
extractFileName,
|
|
19
19
|
extractAgentId,
|
|
20
20
|
} from "../shared/xml-parser.js";
|
|
21
|
-
import {
|
|
21
|
+
import { downloadAgentApiMedia, sendAgentApiText } from "../transport/agent-api/client.js";
|
|
22
22
|
import { getWecomRuntime } from "../runtime.js";
|
|
23
23
|
import type { WecomAgentInboundMessage } from "../types/index.js";
|
|
24
|
+
import type { TransportSessionPatch } from "../types/index.js";
|
|
24
25
|
import { buildWecomUnauthorizedCommandPrompt, resolveWecomCommandAuthorization } from "../shared/command-auth.js";
|
|
25
26
|
import { resolveWecomMediaMaxBytes, shouldRejectWecomDefaultRoute } from "../config/index.js";
|
|
26
|
-
import { generateAgentId, shouldUseDynamicAgent, ensureDynamicAgentListed } from "../dynamic-agent.js";
|
|
27
|
+
import { buildAgentSessionTarget, generateAgentId, shouldUseDynamicAgent, ensureDynamicAgentListed } from "../dynamic-agent.js";
|
|
28
|
+
import type { WecomRuntimeAuditEvent } from "../types/runtime-context.js";
|
|
27
29
|
|
|
28
30
|
/** 错误提示信息 */
|
|
29
31
|
const ERROR_HELP = "\n\n遇到问题?联系作者: YanHaidao (微信: YanHaidao)";
|
|
@@ -122,6 +124,8 @@ export type AgentWebhookParams = {
|
|
|
122
124
|
core: PluginRuntime;
|
|
123
125
|
log?: (msg: string) => void;
|
|
124
126
|
error?: (msg: string) => void;
|
|
127
|
+
auditSink?: (event: WecomRuntimeAuditEvent) => void;
|
|
128
|
+
touchTransportSession?: (patch: TransportSessionPatch) => void;
|
|
125
129
|
};
|
|
126
130
|
|
|
127
131
|
export type AgentInboundProcessDecision = {
|
|
@@ -194,7 +198,7 @@ function resolveQueryParams(req: IncomingMessage): URLSearchParams {
|
|
|
194
198
|
* 处理消息回调 (POST)
|
|
195
199
|
*/
|
|
196
200
|
async function handleMessageCallback(params: AgentWebhookParams): Promise<boolean> {
|
|
197
|
-
const { req, res, verifiedPost, agent, config, core, log, error } = params;
|
|
201
|
+
const { req, res, verifiedPost, agent, config, core, log, error, auditSink } = params;
|
|
198
202
|
|
|
199
203
|
try {
|
|
200
204
|
if (!verifiedPost) {
|
|
@@ -241,6 +245,17 @@ async function handleMessageCallback(params: AgentWebhookParams): Promise<boolea
|
|
|
241
245
|
const ok = rememberAgentMsgId(msgId);
|
|
242
246
|
if (!ok) {
|
|
243
247
|
log?.(`[wecom-agent] duplicate msgId=${msgId} from=${fromUser} chatId=${chatId ?? "N/A"} type=${msgType}; skipped`);
|
|
248
|
+
auditSink?.({
|
|
249
|
+
transport: "agent-callback",
|
|
250
|
+
category: "duplicate-reply",
|
|
251
|
+
messageId: msgId,
|
|
252
|
+
summary: `duplicate agent callback from=${fromUser} chatId=${chatId ?? "N/A"} type=${msgType}`,
|
|
253
|
+
raw: {
|
|
254
|
+
transport: "agent-callback",
|
|
255
|
+
envelopeType: "xml",
|
|
256
|
+
body: msg,
|
|
257
|
+
},
|
|
258
|
+
});
|
|
244
259
|
res.statusCode = 200;
|
|
245
260
|
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
246
261
|
res.end("success");
|
|
@@ -281,6 +296,8 @@ async function handleMessageCallback(params: AgentWebhookParams): Promise<boolea
|
|
|
281
296
|
msg,
|
|
282
297
|
log,
|
|
283
298
|
error,
|
|
299
|
+
auditSink,
|
|
300
|
+
touchTransportSession: params.touchTransportSession,
|
|
284
301
|
}).catch((err) => {
|
|
285
302
|
error?.(`[wecom-agent] process failed: ${String(err)}`);
|
|
286
303
|
});
|
|
@@ -317,8 +334,10 @@ async function processAgentMessage(params: {
|
|
|
317
334
|
msg: WecomAgentInboundMessage;
|
|
318
335
|
log?: (msg: string) => void;
|
|
319
336
|
error?: (msg: string) => void;
|
|
337
|
+
auditSink?: (event: WecomRuntimeAuditEvent) => void;
|
|
338
|
+
touchTransportSession?: (patch: TransportSessionPatch) => void;
|
|
320
339
|
}): Promise<void> {
|
|
321
|
-
const { agent, config, core, fromUser, chatId, content, msg, msgType, log, error } = params;
|
|
340
|
+
const { agent, config, core, fromUser, chatId, content, msg, msgType, log, error, auditSink, touchTransportSession } = params;
|
|
322
341
|
|
|
323
342
|
const isGroup = Boolean(chatId);
|
|
324
343
|
const peerId = isGroup ? chatId! : fromUser;
|
|
@@ -335,7 +354,7 @@ async function processAgentMessage(params: {
|
|
|
335
354
|
if (mediaId) {
|
|
336
355
|
try {
|
|
337
356
|
log?.(`[wecom-agent] downloading media: ${mediaId} (${msgType})`);
|
|
338
|
-
const { buffer, contentType, filename: headerFileName } = await
|
|
357
|
+
const { buffer, contentType, filename: headerFileName } = await downloadAgentApiMedia({ agent, mediaId, maxBytes: mediaMaxBytes });
|
|
339
358
|
const xmlFileName = extractFileName(msg);
|
|
340
359
|
const originalFileName = (xmlFileName || headerFileName || `${mediaId}.bin`).trim();
|
|
341
360
|
const heuristic = analyzeTextHeuristic(buffer);
|
|
@@ -413,6 +432,18 @@ async function processAgentMessage(params: {
|
|
|
413
432
|
log?.(`[wecom-agent] file preview: enabled=${looksText} finalContentLen=${finalContent.length} attachments=${attachments.length}`);
|
|
414
433
|
} catch (err) {
|
|
415
434
|
error?.(`[wecom-agent] media processing failed: ${String(err)}`);
|
|
435
|
+
auditSink?.({
|
|
436
|
+
transport: "agent-callback",
|
|
437
|
+
category: "runtime-error",
|
|
438
|
+
messageId: extractMsgId(msg) ?? undefined,
|
|
439
|
+
summary: `agent media processing failed mediaId=${mediaId}`,
|
|
440
|
+
raw: {
|
|
441
|
+
transport: "agent-callback",
|
|
442
|
+
envelopeType: "xml",
|
|
443
|
+
body: msg,
|
|
444
|
+
},
|
|
445
|
+
error: err instanceof Error ? err.message : String(err),
|
|
446
|
+
});
|
|
416
447
|
finalContent = [
|
|
417
448
|
content,
|
|
418
449
|
"",
|
|
@@ -450,10 +481,22 @@ async function processAgentMessage(params: {
|
|
|
450
481
|
`[wecom-agent] routing guard: blocked default fallback accountId=${agent.accountId} matchedBy=${route.matchedBy} from=${fromUser}`,
|
|
451
482
|
);
|
|
452
483
|
try {
|
|
453
|
-
await
|
|
484
|
+
await sendAgentApiText({ agent, toUser: fromUser, chatId: undefined, text: prompt });
|
|
485
|
+
touchTransportSession?.({ lastOutboundAt: Date.now(), running: true });
|
|
454
486
|
log?.(`[wecom-agent] routing guard prompt delivered to ${fromUser}`);
|
|
455
487
|
} catch (err: unknown) {
|
|
456
488
|
error?.(`[wecom-agent] routing guard prompt failed: ${String(err)}`);
|
|
489
|
+
auditSink?.({
|
|
490
|
+
transport: "agent-callback",
|
|
491
|
+
category: "fallback-delivery-failed",
|
|
492
|
+
summary: `routing guard prompt failed user=${fromUser}`,
|
|
493
|
+
raw: {
|
|
494
|
+
transport: "agent-callback",
|
|
495
|
+
envelopeType: "xml",
|
|
496
|
+
body: msg,
|
|
497
|
+
},
|
|
498
|
+
error: err instanceof Error ? err.message : String(err),
|
|
499
|
+
});
|
|
457
500
|
}
|
|
458
501
|
return;
|
|
459
502
|
}
|
|
@@ -504,10 +547,22 @@ async function processAgentMessage(params: {
|
|
|
504
547
|
if (authz.shouldComputeAuth && authz.commandAuthorized !== true) {
|
|
505
548
|
const prompt = buildWecomUnauthorizedCommandPrompt({ senderUserId: fromUser, dmPolicy: authz.dmPolicy, scope: "agent" });
|
|
506
549
|
try {
|
|
507
|
-
await
|
|
550
|
+
await sendAgentApiText({ agent, toUser: fromUser, chatId: undefined, text: prompt });
|
|
551
|
+
touchTransportSession?.({ lastOutboundAt: Date.now(), running: true });
|
|
508
552
|
log?.(`[wecom-agent] unauthorized command: replied via DM to ${fromUser}`);
|
|
509
553
|
} catch (err: unknown) {
|
|
510
554
|
error?.(`[wecom-agent] unauthorized command reply failed: ${String(err)}`);
|
|
555
|
+
auditSink?.({
|
|
556
|
+
transport: "agent-callback",
|
|
557
|
+
category: "fallback-delivery-failed",
|
|
558
|
+
summary: `unauthorized prompt failed user=${fromUser}`,
|
|
559
|
+
raw: {
|
|
560
|
+
transport: "agent-callback",
|
|
561
|
+
envelopeType: "xml",
|
|
562
|
+
body: msg,
|
|
563
|
+
},
|
|
564
|
+
error: err instanceof Error ? err.message : String(err),
|
|
565
|
+
});
|
|
511
566
|
}
|
|
512
567
|
return;
|
|
513
568
|
}
|
|
@@ -531,7 +586,7 @@ async function processAgentMessage(params: {
|
|
|
531
586
|
// 标记为 Agent 会话的回复路由目标,避免与 Bot 会话混淆:
|
|
532
587
|
// - 用于让 /new /reset 这类命令回执不被 Bot 侧策略拦截
|
|
533
588
|
// - 群聊场景也统一路由为私信触发者(与 deliver 策略一致)
|
|
534
|
-
OriginatingTo:
|
|
589
|
+
OriginatingTo: buildAgentSessionTarget(fromUser, agent.accountId),
|
|
535
590
|
CommandAuthorized: authz.commandAuthorized ?? true,
|
|
536
591
|
MediaPath: mediaPath,
|
|
537
592
|
MediaType: mediaType,
|
|
@@ -552,18 +607,36 @@ async function processAgentMessage(params: {
|
|
|
552
607
|
await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
553
608
|
ctx: ctxPayload,
|
|
554
609
|
cfg: config,
|
|
610
|
+
replyOptions: {
|
|
611
|
+
disableBlockStreaming: true,
|
|
612
|
+
},
|
|
555
613
|
dispatcherOptions: {
|
|
556
614
|
deliver: async (payload: { text?: string }, info: { kind: string }) => {
|
|
615
|
+
if (info.kind !== "final") {
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
557
618
|
const text = payload.text ?? "";
|
|
558
619
|
if (!text) return;
|
|
559
620
|
|
|
560
621
|
try {
|
|
561
622
|
// 统一策略:Agent 模式在群聊场景默认只私信触发者(避免 wr/wc chatId 86008)
|
|
562
|
-
await
|
|
623
|
+
await sendAgentApiText({ agent, toUser: fromUser, chatId: undefined, text });
|
|
624
|
+
touchTransportSession?.({ lastOutboundAt: Date.now(), running: true });
|
|
563
625
|
log?.(`[wecom-agent] reply delivered (${info.kind}) to ${fromUser}`);
|
|
564
626
|
} catch (err: unknown) {
|
|
565
627
|
const message = err instanceof Error ? `${err.message}${err.cause ? ` (cause: ${String(err.cause)})` : ""}` : String(err);
|
|
566
628
|
error?.(`[wecom-agent] reply failed: ${message}`);
|
|
629
|
+
auditSink?.({
|
|
630
|
+
transport: "agent-callback",
|
|
631
|
+
category: "fallback-delivery-failed",
|
|
632
|
+
summary: `agent callback reply failed user=${fromUser} kind=${info.kind}`,
|
|
633
|
+
raw: {
|
|
634
|
+
transport: "agent-callback",
|
|
635
|
+
envelopeType: "xml",
|
|
636
|
+
body: msg,
|
|
637
|
+
},
|
|
638
|
+
error: message,
|
|
639
|
+
});
|
|
567
640
|
} },
|
|
568
641
|
onError: (err: unknown, info: { kind: string }) => {
|
|
569
642
|
error?.(`[wecom-agent] ${info.kind} reply error: ${String(err)}`);
|
package/src/agent/index.ts
CHANGED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
|
|
2
|
+
|
|
3
|
+
import { InMemoryRuntimeStore } from "../store/memory-store.js";
|
|
4
|
+
import { WecomMediaService } from "../shared/media-service.js";
|
|
5
|
+
import { summarizeTransportSessions } from "../observability/transport-session-view.js";
|
|
6
|
+
import type {
|
|
7
|
+
AccountRuntimeStatusSnapshot,
|
|
8
|
+
ReplyHandle,
|
|
9
|
+
ReplyPayload,
|
|
10
|
+
TransportSessionPatch,
|
|
11
|
+
TransportSessionSnapshot,
|
|
12
|
+
UnifiedInboundEvent,
|
|
13
|
+
WecomAuditCategory,
|
|
14
|
+
WecomRuntimeHealth,
|
|
15
|
+
WecomTransportKind,
|
|
16
|
+
} from "../types/index.js";
|
|
17
|
+
import type { ResolvedRuntimeAccount } from "../config/runtime-config.js";
|
|
18
|
+
import { dispatchInboundEvent } from "../runtime/dispatcher.js";
|
|
19
|
+
import { WecomAuditLog } from "../observability/audit-log.js";
|
|
20
|
+
import { WecomStatusRegistry } from "../observability/status-registry.js";
|
|
21
|
+
|
|
22
|
+
export class WecomAccountRuntime {
|
|
23
|
+
readonly store = new InMemoryRuntimeStore();
|
|
24
|
+
readonly mediaService: WecomMediaService;
|
|
25
|
+
readonly auditLog = new WecomAuditLog();
|
|
26
|
+
readonly statusRegistry = new WecomStatusRegistry();
|
|
27
|
+
private readonly runtimeStatus: AccountRuntimeStatusSnapshot;
|
|
28
|
+
|
|
29
|
+
constructor(
|
|
30
|
+
readonly core: PluginRuntime,
|
|
31
|
+
readonly cfg: OpenClawConfig,
|
|
32
|
+
readonly resolved: ResolvedRuntimeAccount,
|
|
33
|
+
private readonly log: {
|
|
34
|
+
info?: (message: string) => void;
|
|
35
|
+
warn?: (message: string) => void;
|
|
36
|
+
error?: (message: string) => void;
|
|
37
|
+
} = {},
|
|
38
|
+
private readonly statusSink?: (snapshot: Record<string, unknown>) => void,
|
|
39
|
+
) {
|
|
40
|
+
this.mediaService = new WecomMediaService(core);
|
|
41
|
+
this.runtimeStatus = {
|
|
42
|
+
accountId: resolved.account.accountId,
|
|
43
|
+
health: "idle",
|
|
44
|
+
ownerId: null,
|
|
45
|
+
ownerDriftAt: null,
|
|
46
|
+
lastError: null,
|
|
47
|
+
lastErrorAt: null,
|
|
48
|
+
lastInboundAt: null,
|
|
49
|
+
lastOutboundAt: null,
|
|
50
|
+
recentInboundSummary: null,
|
|
51
|
+
recentOutboundSummary: null,
|
|
52
|
+
recentIssueCategory: null,
|
|
53
|
+
recentIssueSummary: null,
|
|
54
|
+
transportSessions: [],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
get account() {
|
|
59
|
+
return this.resolved.account;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async handleEvent(event: UnifiedInboundEvent, replyHandle: ReplyHandle): Promise<void> {
|
|
63
|
+
this.runtimeStatus.lastInboundAt = Date.now();
|
|
64
|
+
this.runtimeStatus.recentInboundSummary = `${event.transport} ${event.inboundKind} ${event.messageId}`;
|
|
65
|
+
this.log.info?.(
|
|
66
|
+
`[wecom-runtime] inbound account=${event.accountId} transport=${event.transport} kind=${event.inboundKind} messageId=${event.messageId} peer=${event.conversation.peerKind}:${event.conversation.peerId}`,
|
|
67
|
+
);
|
|
68
|
+
this.emitStatus();
|
|
69
|
+
|
|
70
|
+
const trackedReplyHandle: ReplyHandle = {
|
|
71
|
+
context: replyHandle.context,
|
|
72
|
+
deliver: async (payload: ReplyPayload, info) => {
|
|
73
|
+
await replyHandle.deliver(payload, info);
|
|
74
|
+
this.runtimeStatus.lastOutboundAt = Date.now();
|
|
75
|
+
const outboundSummary = payload.text?.trim() || payload.mediaUrl || payload.mediaUrls?.[0] || info.kind;
|
|
76
|
+
this.runtimeStatus.recentOutboundSummary = `${replyHandle.context.transport} ${outboundSummary.slice(0, 120)}`;
|
|
77
|
+
this.log.info?.(
|
|
78
|
+
`[wecom-runtime] outbound account=${event.accountId} transport=${replyHandle.context.transport} kind=${info.kind} messageId=${event.messageId} summary=${JSON.stringify(this.runtimeStatus.recentOutboundSummary)}`,
|
|
79
|
+
);
|
|
80
|
+
this.emitStatus();
|
|
81
|
+
},
|
|
82
|
+
fail: async (error: unknown) => {
|
|
83
|
+
this.recordOperationalIssue({
|
|
84
|
+
transport: replyHandle.context.transport,
|
|
85
|
+
category: "runtime-error",
|
|
86
|
+
messageId: event.messageId,
|
|
87
|
+
raw: replyHandle.context.raw,
|
|
88
|
+
summary: `reply-fail ${String(error)}`,
|
|
89
|
+
error: error instanceof Error ? error.message : String(error),
|
|
90
|
+
});
|
|
91
|
+
this.log.error?.(
|
|
92
|
+
`[wecom-runtime] reply-fail account=${event.accountId} transport=${replyHandle.context.transport} messageId=${event.messageId} error=${String(error)}`,
|
|
93
|
+
);
|
|
94
|
+
await replyHandle.fail?.(error);
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
await dispatchInboundEvent({
|
|
99
|
+
core: this.core,
|
|
100
|
+
cfg: this.cfg,
|
|
101
|
+
store: this.store,
|
|
102
|
+
auditLog: this.auditLog,
|
|
103
|
+
mediaService: this.mediaService,
|
|
104
|
+
event,
|
|
105
|
+
replyHandle: trackedReplyHandle,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
updateTransportSession(snapshot: TransportSessionSnapshot): void {
|
|
110
|
+
const previous = this.store.readTransportSession(snapshot.accountId, snapshot.transport);
|
|
111
|
+
this.store.writeTransportSession(snapshot);
|
|
112
|
+
this.statusRegistry.write(snapshot);
|
|
113
|
+
this.log.info?.(
|
|
114
|
+
`[wecom-runtime] session account=${snapshot.accountId} transport=${snapshot.transport} running=${snapshot.running} owner=${snapshot.ownerId ?? "none"} connected=${String(snapshot.connected ?? false)} authenticated=${String(snapshot.authenticated ?? false)} error=${snapshot.lastError ?? "none"}`,
|
|
115
|
+
);
|
|
116
|
+
if (
|
|
117
|
+
previous?.ownerId &&
|
|
118
|
+
snapshot.ownerId &&
|
|
119
|
+
previous.ownerId !== snapshot.ownerId &&
|
|
120
|
+
previous.running
|
|
121
|
+
) {
|
|
122
|
+
this.recordOperationalIssue({
|
|
123
|
+
transport: snapshot.transport,
|
|
124
|
+
category: "owner-drift",
|
|
125
|
+
summary: `owner drift ${previous.ownerId} -> ${snapshot.ownerId}`,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
if (snapshot.lastError) {
|
|
129
|
+
this.runtimeStatus.lastError = snapshot.lastError;
|
|
130
|
+
this.runtimeStatus.lastErrorAt = Date.now();
|
|
131
|
+
} else if (snapshot.running) {
|
|
132
|
+
this.runtimeStatus.lastError = null;
|
|
133
|
+
}
|
|
134
|
+
this.emitStatus();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
touchTransportSession(transport: WecomTransportKind, patch: TransportSessionPatch): void {
|
|
138
|
+
const current = this.store.readTransportSession(this.account.accountId, transport);
|
|
139
|
+
const next: TransportSessionSnapshot = {
|
|
140
|
+
accountId: this.account.accountId,
|
|
141
|
+
transport,
|
|
142
|
+
running: patch.running ?? current?.running ?? true,
|
|
143
|
+
ownerId: patch.ownerId ?? current?.ownerId,
|
|
144
|
+
connected: patch.connected ?? current?.connected,
|
|
145
|
+
authenticated: patch.authenticated ?? current?.authenticated,
|
|
146
|
+
lastConnectedAt: patch.lastConnectedAt ?? current?.lastConnectedAt,
|
|
147
|
+
lastDisconnectedAt: patch.lastDisconnectedAt ?? current?.lastDisconnectedAt,
|
|
148
|
+
lastInboundAt: patch.lastInboundAt ?? current?.lastInboundAt,
|
|
149
|
+
lastOutboundAt: patch.lastOutboundAt ?? current?.lastOutboundAt,
|
|
150
|
+
lastError: "lastError" in patch ? patch.lastError ?? undefined : current?.lastError,
|
|
151
|
+
};
|
|
152
|
+
this.updateTransportSession(next);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
listTransportSessions() {
|
|
156
|
+
return this.statusRegistry.read(this.account.accountId);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
listAuditEntries() {
|
|
160
|
+
return this.auditLog.list();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
buildRuntimeStatus(): AccountRuntimeStatusSnapshot {
|
|
164
|
+
const sessions = this.listTransportSessions();
|
|
165
|
+
const primarySession = this.resolvePrimarySession(sessions);
|
|
166
|
+
return {
|
|
167
|
+
...this.runtimeStatus,
|
|
168
|
+
health: this.computeHealth(sessions),
|
|
169
|
+
transport: primarySession?.transport,
|
|
170
|
+
ownerId: primarySession?.ownerId ?? this.runtimeStatus.ownerId ?? null,
|
|
171
|
+
connected: primarySession?.connected,
|
|
172
|
+
authenticated: primarySession?.authenticated,
|
|
173
|
+
lastError:
|
|
174
|
+
primarySession?.lastError ??
|
|
175
|
+
(primarySession?.running ? null : this.runtimeStatus.lastError ?? null),
|
|
176
|
+
transportSessions: summarizeTransportSessions(sessions),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
recordOperationalIssue(params: {
|
|
181
|
+
transport: WecomTransportKind;
|
|
182
|
+
category: WecomAuditCategory;
|
|
183
|
+
summary: string;
|
|
184
|
+
messageId?: string;
|
|
185
|
+
raw?: ReplyHandle["context"]["raw"];
|
|
186
|
+
error?: string;
|
|
187
|
+
}): void {
|
|
188
|
+
this.auditLog.appendOperational({
|
|
189
|
+
accountId: this.account.accountId,
|
|
190
|
+
transport: params.transport,
|
|
191
|
+
category: params.category,
|
|
192
|
+
messageId: params.messageId,
|
|
193
|
+
summary: params.summary,
|
|
194
|
+
raw: params.raw,
|
|
195
|
+
error: params.error,
|
|
196
|
+
});
|
|
197
|
+
if (params.category === "owner-drift" || params.category === "ws-kicked") {
|
|
198
|
+
this.runtimeStatus.ownerDriftAt = Date.now();
|
|
199
|
+
}
|
|
200
|
+
this.runtimeStatus.lastError = params.error ?? params.summary;
|
|
201
|
+
this.runtimeStatus.lastErrorAt = Date.now();
|
|
202
|
+
this.runtimeStatus.recentIssueCategory = params.category;
|
|
203
|
+
this.runtimeStatus.recentIssueSummary = params.summary;
|
|
204
|
+
const sink = params.category === "runtime-error" || params.category === "fallback-delivery-failed" ? this.log.error : this.log.warn;
|
|
205
|
+
sink?.(
|
|
206
|
+
`[wecom-runtime] issue account=${this.account.accountId} transport=${params.transport} category=${params.category} messageId=${params.messageId ?? "n/a"} summary=${params.summary}`,
|
|
207
|
+
);
|
|
208
|
+
this.emitStatus();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private emitStatus(): void {
|
|
212
|
+
this.statusSink?.(this.buildRuntimeStatus() as unknown as Record<string, unknown>);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private resolvePrimarySession(
|
|
216
|
+
sessions: TransportSessionSnapshot[],
|
|
217
|
+
): TransportSessionSnapshot | undefined {
|
|
218
|
+
const primaryTransport = this.account.bot?.configured
|
|
219
|
+
? this.account.bot.primaryTransport === "ws"
|
|
220
|
+
? "bot-ws"
|
|
221
|
+
: "bot-webhook"
|
|
222
|
+
: this.account.agent?.callbackConfigured
|
|
223
|
+
? "agent-callback"
|
|
224
|
+
: undefined;
|
|
225
|
+
if (!primaryTransport) {
|
|
226
|
+
return sessions[0];
|
|
227
|
+
}
|
|
228
|
+
return sessions.find((session) => session.transport === primaryTransport) ?? sessions[0];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private computeHealth(sessions: TransportSessionSnapshot[]): WecomRuntimeHealth {
|
|
232
|
+
if (sessions.length === 0) {
|
|
233
|
+
return this.runtimeStatus.lastError ? "down" : "idle";
|
|
234
|
+
}
|
|
235
|
+
const hasRunning = sessions.some((session) => session.running);
|
|
236
|
+
const hasError = sessions.some((session) => Boolean(session.lastError));
|
|
237
|
+
if (hasRunning && !hasError) {
|
|
238
|
+
return "healthy";
|
|
239
|
+
}
|
|
240
|
+
if (hasRunning) {
|
|
241
|
+
return "degraded";
|
|
242
|
+
}
|
|
243
|
+
return hasError ? "down" : "idle";
|
|
244
|
+
}
|
|
245
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ChannelGatewayContext } from "openclaw/plugin-sdk";
|
|
2
|
+
|
|
3
|
+
import { resolveWecomRuntimeAccount } from "../config/runtime-config.js";
|
|
4
|
+
import type { ResolvedWecomAccount } from "../types/index.js";
|
|
5
|
+
import { WecomAccountRuntime } from "./account-runtime.js";
|
|
6
|
+
import { getWecomRuntime } from "./index.js";
|
|
7
|
+
|
|
8
|
+
export function createAccountRuntime(ctx: ChannelGatewayContext<ResolvedWecomAccount>): WecomAccountRuntime {
|
|
9
|
+
const resolved = resolveWecomRuntimeAccount({
|
|
10
|
+
cfg: ctx.cfg,
|
|
11
|
+
accountId: ctx.accountId,
|
|
12
|
+
});
|
|
13
|
+
return new WecomAccountRuntime(
|
|
14
|
+
getWecomRuntime(),
|
|
15
|
+
ctx.cfg,
|
|
16
|
+
resolved,
|
|
17
|
+
{
|
|
18
|
+
info: (message) => ctx.log?.info(message),
|
|
19
|
+
warn: (message) => ctx.log?.warn(message),
|
|
20
|
+
error: (message) => ctx.log?.error(message),
|
|
21
|
+
},
|
|
22
|
+
(snapshot) => {
|
|
23
|
+
ctx.setStatus({
|
|
24
|
+
accountId: ctx.accountId,
|
|
25
|
+
...snapshot,
|
|
26
|
+
});
|
|
27
|
+
},
|
|
28
|
+
);
|
|
29
|
+
}
|
package/src/app/index.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { PluginRuntime } from "openclaw/plugin-sdk";
|
|
2
|
+
|
|
3
|
+
import { WecomAccountRuntime } from "./account-runtime.js";
|
|
4
|
+
|
|
5
|
+
let runtime: PluginRuntime | null = null;
|
|
6
|
+
const runtimes = new Map<string, WecomAccountRuntime>();
|
|
7
|
+
|
|
8
|
+
export function setWecomRuntime(next: PluginRuntime): void {
|
|
9
|
+
runtime = next;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getWecomRuntime(): PluginRuntime {
|
|
13
|
+
if (!runtime) {
|
|
14
|
+
throw new Error("WeCom runtime not initialized");
|
|
15
|
+
}
|
|
16
|
+
return runtime;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function registerAccountRuntime(accountRuntime: WecomAccountRuntime): void {
|
|
20
|
+
runtimes.set(accountRuntime.account.accountId, accountRuntime);
|
|
21
|
+
console.log(`[wecom-runtime] register account=${accountRuntime.account.accountId}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getAccountRuntimeSnapshot(accountId: string) {
|
|
25
|
+
return runtimes.get(accountId)?.buildRuntimeStatus();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function unregisterAccountRuntime(accountId: string): void {
|
|
29
|
+
runtimes.delete(accountId);
|
|
30
|
+
console.log(`[wecom-runtime] unregister account=${accountId}`);
|
|
31
|
+
}
|