@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
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { pathToFileURL } from "node:url";
|
|
2
|
+
|
|
3
|
+
import type { PluginRuntime } from "openclaw/plugin-sdk";
|
|
4
|
+
|
|
5
|
+
import { resolveWecomMediaMaxBytes, shouldRejectWecomDefaultRoute } from "../../config/index.js";
|
|
6
|
+
import { ensureDynamicAgentListed, generateAgentId, shouldUseDynamicAgent } from "../../dynamic-agent.js";
|
|
7
|
+
import { LIMITS, type StreamStore } from "../../monitor/state.js";
|
|
8
|
+
import { getWecomRuntime } from "../../runtime.js";
|
|
9
|
+
import { buildWecomUnauthorizedCommandPrompt, resolveWecomCommandAuthorization } from "../../shared/command-auth.js";
|
|
10
|
+
import type { PendingInbound } from "../../types/legacy-stream.js";
|
|
11
|
+
import type { WecomBotInboundMessage as WecomInboundMessage } from "../../types/index.js";
|
|
12
|
+
import type { WecomWebhookTarget } from "../../types/runtime-context.js";
|
|
13
|
+
import { looksLikeSendLocalFileIntent, processBotInboundMessage } from "../../transport/bot-webhook/inbound-normalizer.js";
|
|
14
|
+
import { resolveWecomSenderUserId } from "../../transport/bot-webhook/message-shape.js";
|
|
15
|
+
import { buildWecomBotDispatchConfig } from "./dispatch-config.js";
|
|
16
|
+
import { sendBotFallbackPromptNow } from "./fallback-delivery.js";
|
|
17
|
+
import { finalizeBotStream } from "./stream-finalizer.js";
|
|
18
|
+
import { handleDirectLocalPathIntent } from "./local-path-delivery.js";
|
|
19
|
+
import { createBotReplyDispatcher } from "./stream-delivery.js";
|
|
20
|
+
import type { BotRuntimeLogger, RecordBotOperationalEvent } from "./types.js";
|
|
21
|
+
|
|
22
|
+
export type StartBotAgentStreamParams = {
|
|
23
|
+
target: WecomWebhookTarget;
|
|
24
|
+
accountId: string;
|
|
25
|
+
msg: WecomInboundMessage;
|
|
26
|
+
streamId: string;
|
|
27
|
+
mergedContents?: string[] | string;
|
|
28
|
+
mergedMsgids?: string[];
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export function createBotStreamOrchestrator(params: {
|
|
32
|
+
streamStore: StreamStore;
|
|
33
|
+
recordBotOperationalEvent: RecordBotOperationalEvent;
|
|
34
|
+
}) {
|
|
35
|
+
const { streamStore, recordBotOperationalEvent } = params;
|
|
36
|
+
|
|
37
|
+
const logVerbose: BotRuntimeLogger = (target, message) => {
|
|
38
|
+
const should =
|
|
39
|
+
target.core.logging?.shouldLogVerbose?.() ??
|
|
40
|
+
(() => {
|
|
41
|
+
try {
|
|
42
|
+
return getWecomRuntime().logging.shouldLogVerbose();
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
})();
|
|
47
|
+
if (!should) return;
|
|
48
|
+
target.runtime.log?.(`[wecom] ${message}`);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const logInfo: BotRuntimeLogger = (target, message) => {
|
|
52
|
+
target.runtime.log?.(`[wecom] ${message}`);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const truncateUtf8Bytes = (text: string, maxBytes: number): string => {
|
|
56
|
+
const buf = Buffer.from(text, "utf8");
|
|
57
|
+
if (buf.length <= maxBytes) return text;
|
|
58
|
+
return buf.subarray(buf.length - maxBytes).toString("utf8");
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const computeTaskKey = (target: WecomWebhookTarget, msg: WecomInboundMessage): string | undefined => {
|
|
62
|
+
const msgid = msg.msgid ? String(msg.msgid) : "";
|
|
63
|
+
if (!msgid) return undefined;
|
|
64
|
+
const aibotid = String((msg as any).aibotid ?? "unknown").trim() || "unknown";
|
|
65
|
+
return `bot:${target.account.accountId}:${aibotid}:${msgid}`;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
async function flushPending(pending: PendingInbound): Promise<void> {
|
|
69
|
+
const { streamId, target, msg, contents, msgids, conversationKey, batchKey } = pending;
|
|
70
|
+
const mergedContents = contents.filter((c) => c.trim()).join("\n").trim();
|
|
71
|
+
|
|
72
|
+
let core: PluginRuntime | null = null;
|
|
73
|
+
try {
|
|
74
|
+
core = getWecomRuntime();
|
|
75
|
+
} catch (err) {
|
|
76
|
+
logVerbose(target, `flush pending: runtime not ready: ${String(err)}`);
|
|
77
|
+
streamStore.markFinished(streamId);
|
|
78
|
+
logInfo(target, `queue: runtime not ready,结束批次并推进 streamId=${streamId}`);
|
|
79
|
+
streamStore.onStreamFinished(streamId);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!core) return;
|
|
84
|
+
|
|
85
|
+
streamStore.markStarted(streamId);
|
|
86
|
+
const enrichedTarget: WecomWebhookTarget = { ...target, core };
|
|
87
|
+
logInfo(
|
|
88
|
+
target,
|
|
89
|
+
`flush pending: start batch streamId=${streamId} batchKey=${batchKey} conversationKey=${conversationKey} mergedCount=${contents.length}`,
|
|
90
|
+
);
|
|
91
|
+
logVerbose(target, `防抖结束: 开始处理聚合消息 数量=${contents.length} streamId=${streamId}`);
|
|
92
|
+
|
|
93
|
+
startAgentForStream({
|
|
94
|
+
target: enrichedTarget,
|
|
95
|
+
accountId: target.account.accountId,
|
|
96
|
+
msg,
|
|
97
|
+
streamId,
|
|
98
|
+
mergedContents: contents.length > 1 ? mergedContents : undefined,
|
|
99
|
+
mergedMsgids: msgids.length > 1 ? msgids : undefined,
|
|
100
|
+
}).catch((err) => {
|
|
101
|
+
streamStore.updateStream(streamId, (state) => {
|
|
102
|
+
state.error = err instanceof Error ? err.message : String(err);
|
|
103
|
+
state.content = state.content || `Error: ${state.error}`;
|
|
104
|
+
state.finished = true;
|
|
105
|
+
});
|
|
106
|
+
target.runtime.error?.(`[${target.account.accountId}] wecom agent failed (处理失败): ${String(err)}`);
|
|
107
|
+
streamStore.onStreamFinished(streamId);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function startAgentForStream(params: StartBotAgentStreamParams): Promise<void> {
|
|
112
|
+
const { target, msg, streamId } = params;
|
|
113
|
+
const core = target.core;
|
|
114
|
+
const config = target.config;
|
|
115
|
+
const account = target.account;
|
|
116
|
+
|
|
117
|
+
const userId = resolveWecomSenderUserId(msg) || "unknown";
|
|
118
|
+
const chatType = msg.chattype === "group" ? "group" : "direct";
|
|
119
|
+
const chatId = msg.chattype === "group" ? (msg.chatid?.trim() || "unknown") : userId;
|
|
120
|
+
const taskKey = computeTaskKey(target, msg);
|
|
121
|
+
const aibotid = String((msg as any).aibotid ?? "").trim() || undefined;
|
|
122
|
+
|
|
123
|
+
streamStore.updateStream(streamId, (s) => {
|
|
124
|
+
s.userId = userId;
|
|
125
|
+
s.chatType = chatType;
|
|
126
|
+
s.chatId = chatId;
|
|
127
|
+
s.taskKey = taskKey;
|
|
128
|
+
s.aibotid = aibotid;
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
let { body: rawBody, media } = await processBotInboundMessage({
|
|
132
|
+
target,
|
|
133
|
+
msg,
|
|
134
|
+
recordOperationalIssue: (event) => recordBotOperationalEvent(target, event),
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
if (params.mergedContents) {
|
|
138
|
+
rawBody = Array.isArray(params.mergedContents) ? params.mergedContents.join("\n") : params.mergedContents;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const handledLocalPath = await handleDirectLocalPathIntent({
|
|
142
|
+
streamStore,
|
|
143
|
+
target,
|
|
144
|
+
streamId,
|
|
145
|
+
rawBody,
|
|
146
|
+
userId,
|
|
147
|
+
chatType,
|
|
148
|
+
logVerbose,
|
|
149
|
+
looksLikeSendLocalFileIntent,
|
|
150
|
+
});
|
|
151
|
+
if (handledLocalPath) return;
|
|
152
|
+
|
|
153
|
+
let mediaPath: string | undefined;
|
|
154
|
+
let mediaType: string | undefined;
|
|
155
|
+
if (media) {
|
|
156
|
+
try {
|
|
157
|
+
const maxBytes = resolveWecomMediaMaxBytes(target.config);
|
|
158
|
+
const saved = await core.channel.media.saveMediaBuffer(media.buffer, media.contentType, "inbound", maxBytes, media.filename);
|
|
159
|
+
mediaPath = saved.path;
|
|
160
|
+
mediaType = saved.contentType;
|
|
161
|
+
logVerbose(target, `saved inbound media to ${mediaPath} (${mediaType})`);
|
|
162
|
+
} catch (err) {
|
|
163
|
+
target.runtime.error?.(`Failed to save inbound media: ${String(err)}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const route = core.channel.routing.resolveAgentRoute({
|
|
168
|
+
cfg: config,
|
|
169
|
+
channel: "wecom",
|
|
170
|
+
accountId: account.accountId,
|
|
171
|
+
peer: { kind: chatType === "group" ? "group" : "direct", id: chatId },
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const useDynamicAgent = shouldUseDynamicAgent({
|
|
175
|
+
chatType: chatType === "group" ? "group" : "dm",
|
|
176
|
+
senderId: userId,
|
|
177
|
+
config,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
if (shouldRejectWecomDefaultRoute({ cfg: config, matchedBy: route.matchedBy, useDynamicAgent })) {
|
|
181
|
+
const prompt =
|
|
182
|
+
`当前账号(${account.accountId})未绑定 OpenClaw Agent,已拒绝回退到默认主智能体。` +
|
|
183
|
+
`请在 bindings 中添加:{"agentId":"你的Agent","match":{"channel":"wecom","accountId":"${account.accountId}"}}`;
|
|
184
|
+
target.runtime.error?.(
|
|
185
|
+
`[wecom] routing guard: blocked default fallback accountId=${account.accountId} matchedBy=${route.matchedBy} streamId=${streamId}`,
|
|
186
|
+
);
|
|
187
|
+
streamStore.updateStream(streamId, (s) => {
|
|
188
|
+
s.finished = true;
|
|
189
|
+
s.content = prompt;
|
|
190
|
+
});
|
|
191
|
+
try {
|
|
192
|
+
await sendBotFallbackPromptNow({ streamId, text: prompt });
|
|
193
|
+
} catch (err) {
|
|
194
|
+
target.runtime.error?.(`routing guard prompt push failed streamId=${streamId}: ${String(err)}`);
|
|
195
|
+
}
|
|
196
|
+
streamStore.onStreamFinished(streamId);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (useDynamicAgent) {
|
|
201
|
+
const targetAgentId = generateAgentId(chatType === "group" ? "group" : "dm", chatId, account.accountId);
|
|
202
|
+
route.agentId = targetAgentId;
|
|
203
|
+
route.sessionKey = `agent:${targetAgentId}:wecom:${account.accountId}:${chatType === "group" ? "group" : "dm"}:${chatId}`;
|
|
204
|
+
ensureDynamicAgentListed(targetAgentId, core).catch(() => {});
|
|
205
|
+
logVerbose(target, `dynamic agent routing: ${targetAgentId}, sessionKey=${route.sessionKey}`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
logVerbose(target, `starting agent processing (streamId=${streamId}, agentId=${route.agentId}, peerKind=${chatType}, peerId=${chatId})`);
|
|
209
|
+
logVerbose(target, `启动 Agent 处理: streamId=${streamId} 路由=${route.agentId} 类型=${chatType} ID=${chatId}`);
|
|
210
|
+
|
|
211
|
+
const fromLabel = chatType === "group" ? `group:${chatId}` : `user:${userId}`;
|
|
212
|
+
const storePath = core.channel.session.resolveStorePath(config.session?.store, { agentId: route.agentId });
|
|
213
|
+
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(config);
|
|
214
|
+
const previousTimestamp = core.channel.session.readSessionUpdatedAt({
|
|
215
|
+
storePath,
|
|
216
|
+
sessionKey: route.sessionKey,
|
|
217
|
+
});
|
|
218
|
+
const body = core.channel.reply.formatAgentEnvelope({
|
|
219
|
+
channel: "WeCom",
|
|
220
|
+
from: fromLabel,
|
|
221
|
+
previousTimestamp,
|
|
222
|
+
envelope: envelopeOptions,
|
|
223
|
+
body: rawBody,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const authz = await resolveWecomCommandAuthorization({
|
|
227
|
+
core,
|
|
228
|
+
cfg: config,
|
|
229
|
+
accountConfig: account.config,
|
|
230
|
+
rawBody,
|
|
231
|
+
senderUserId: userId,
|
|
232
|
+
});
|
|
233
|
+
const commandAuthorized = authz.commandAuthorized;
|
|
234
|
+
logVerbose(
|
|
235
|
+
target,
|
|
236
|
+
`authz: dmPolicy=${authz.dmPolicy} shouldCompute=${authz.shouldComputeAuth} sender=${userId.toLowerCase()} senderAllowed=${authz.senderAllowed} authorizerConfigured=${authz.authorizerConfigured} commandAuthorized=${String(authz.commandAuthorized)}`,
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
if (authz.shouldComputeAuth && authz.commandAuthorized !== true) {
|
|
240
|
+
const prompt = buildWecomUnauthorizedCommandPrompt({ senderUserId: userId, dmPolicy: authz.dmPolicy, scope: "bot" });
|
|
241
|
+
streamStore.updateStream(streamId, (s) => {
|
|
242
|
+
s.finished = true;
|
|
243
|
+
s.content = prompt;
|
|
244
|
+
});
|
|
245
|
+
try {
|
|
246
|
+
await sendBotFallbackPromptNow({ streamId, text: prompt });
|
|
247
|
+
logInfo(target, `authz: 未授权命令已提示用户 streamId=${streamId}`);
|
|
248
|
+
} catch (err) {
|
|
249
|
+
target.runtime.error?.(`authz: 未授权命令提示推送失败 streamId=${streamId}: ${String(err)}`);
|
|
250
|
+
}
|
|
251
|
+
streamStore.onStreamFinished(streamId);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const attachments = mediaPath
|
|
256
|
+
? [
|
|
257
|
+
{
|
|
258
|
+
name: media?.filename || "file",
|
|
259
|
+
mimeType: mediaType,
|
|
260
|
+
url: pathToFileURL(mediaPath).href,
|
|
261
|
+
},
|
|
262
|
+
]
|
|
263
|
+
: undefined;
|
|
264
|
+
|
|
265
|
+
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
266
|
+
Body: body,
|
|
267
|
+
RawBody: rawBody,
|
|
268
|
+
CommandBody: rawBody,
|
|
269
|
+
Attachments: attachments,
|
|
270
|
+
From: chatType === "group" ? `wecom:group:${chatId}` : `wecom:${userId}`,
|
|
271
|
+
To: `wecom:${chatId}`,
|
|
272
|
+
SessionKey: route.sessionKey,
|
|
273
|
+
AccountId: route.accountId,
|
|
274
|
+
ChatType: chatType,
|
|
275
|
+
ConversationLabel: fromLabel,
|
|
276
|
+
SenderName: userId,
|
|
277
|
+
SenderId: userId,
|
|
278
|
+
Provider: "wecom",
|
|
279
|
+
Surface: "wecom",
|
|
280
|
+
MessageSid: msg.msgid,
|
|
281
|
+
CommandAuthorized: commandAuthorized,
|
|
282
|
+
OriginatingChannel: "wecom",
|
|
283
|
+
OriginatingTo: `wecom:${chatId}`,
|
|
284
|
+
MediaPath: mediaPath,
|
|
285
|
+
MediaType: mediaType,
|
|
286
|
+
MediaUrl: mediaPath,
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
await core.channel.session.recordInboundSession({
|
|
290
|
+
storePath,
|
|
291
|
+
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
|
|
292
|
+
ctx: ctxPayload,
|
|
293
|
+
onRecordError: (err) => {
|
|
294
|
+
target.runtime.error?.(`wecom: failed updating session meta: ${String(err)}`);
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
const tableMode = core.channel.text.resolveMarkdownTableMode({
|
|
299
|
+
cfg: config,
|
|
300
|
+
channel: "wecom",
|
|
301
|
+
accountId: account.accountId,
|
|
302
|
+
});
|
|
303
|
+
const cfgForDispatch = buildWecomBotDispatchConfig(config);
|
|
304
|
+
logVerbose(target, "tool-policy: WeCom Bot 会话已禁用 message 工具(tools.deny += message;并同步到 tools.sandbox.tools.deny,防止绕过 Bot 交付)");
|
|
305
|
+
|
|
306
|
+
await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
307
|
+
ctx: ctxPayload,
|
|
308
|
+
cfg: cfgForDispatch,
|
|
309
|
+
replyOptions: { disableBlockStreaming: false },
|
|
310
|
+
dispatcherOptions: createBotReplyDispatcher({
|
|
311
|
+
streamStore,
|
|
312
|
+
target,
|
|
313
|
+
accountId: account.accountId,
|
|
314
|
+
config,
|
|
315
|
+
msg,
|
|
316
|
+
streamId,
|
|
317
|
+
rawBody,
|
|
318
|
+
chatType,
|
|
319
|
+
userId,
|
|
320
|
+
core,
|
|
321
|
+
tableMode,
|
|
322
|
+
logVerbose,
|
|
323
|
+
truncateUtf8Bytes,
|
|
324
|
+
recordBotOperationalEvent,
|
|
325
|
+
}),
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
const rawBodyNormalized = rawBody.trim();
|
|
329
|
+
const isResetCommand = /^\/(new|reset)(?:\s|$)/i.test(rawBodyNormalized);
|
|
330
|
+
const resetCommandKind = isResetCommand ? (rawBodyNormalized.match(/^\/(new|reset)/i)?.[1]?.toLowerCase() ?? "new") : null;
|
|
331
|
+
|
|
332
|
+
await finalizeBotStream({
|
|
333
|
+
streamStore,
|
|
334
|
+
target,
|
|
335
|
+
streamId,
|
|
336
|
+
chatType,
|
|
337
|
+
core,
|
|
338
|
+
config,
|
|
339
|
+
accountId: account.accountId,
|
|
340
|
+
isResetCommand,
|
|
341
|
+
resetCommandKind,
|
|
342
|
+
logInfo,
|
|
343
|
+
logVerbose,
|
|
344
|
+
recordBotOperationalEvent,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
flushPending,
|
|
350
|
+
startAgentForStream,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { WecomRuntimeAuditEvent, WecomWebhookTarget } from "../../types/runtime-context.js";
|
|
2
|
+
|
|
3
|
+
export type BotRuntimeLogger = (target: WecomWebhookTarget, message: string) => void;
|
|
4
|
+
|
|
5
|
+
export type RecordBotOperationalEvent = (
|
|
6
|
+
target: Pick<WecomWebhookTarget, "account" | "auditSink">,
|
|
7
|
+
event: Omit<WecomRuntimeAuditEvent, "transport">,
|
|
8
|
+
) => void;
|
|
@@ -89,7 +89,7 @@ function createCtx(params: {
|
|
|
89
89
|
};
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
function
|
|
92
|
+
function createWebhookBotConfig(params: {
|
|
93
93
|
token: string;
|
|
94
94
|
encodingAESKey: string;
|
|
95
95
|
receiveId?: string;
|
|
@@ -99,9 +99,12 @@ function createLegacyBotConfig(params: {
|
|
|
99
99
|
wecom: {
|
|
100
100
|
enabled: true,
|
|
101
101
|
bot: {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
primaryTransport: "webhook",
|
|
103
|
+
webhook: {
|
|
104
|
+
token: params.token,
|
|
105
|
+
encodingAESKey: params.encodingAESKey,
|
|
106
|
+
receiveId: params.receiveId ?? "",
|
|
107
|
+
},
|
|
105
108
|
},
|
|
106
109
|
},
|
|
107
110
|
},
|
|
@@ -148,7 +151,7 @@ describe("wecomPlugin gateway lifecycle", () => {
|
|
|
148
151
|
it("keeps startAccount pending until abort signal", async () => {
|
|
149
152
|
const token = "token";
|
|
150
153
|
const encodingAESKey = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG";
|
|
151
|
-
const cfg =
|
|
154
|
+
const cfg = createWebhookBotConfig({ token, encodingAESKey });
|
|
152
155
|
const abortController = new AbortController();
|
|
153
156
|
const ctx = createCtx({ cfg, abortController });
|
|
154
157
|
|
|
@@ -171,7 +174,7 @@ describe("wecomPlugin gateway lifecycle", () => {
|
|
|
171
174
|
const token = "token";
|
|
172
175
|
const encodingAESKey = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG";
|
|
173
176
|
const receiveId = "";
|
|
174
|
-
const cfg =
|
|
177
|
+
const cfg = createWebhookBotConfig({ token, encodingAESKey, receiveId });
|
|
175
178
|
const abortController = new AbortController();
|
|
176
179
|
const ctx = createCtx({ cfg, abortController });
|
|
177
180
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { wecomPlugin } from "./channel.js";
|
|
3
|
+
|
|
4
|
+
describe("wecomPlugin meta", () => {
|
|
5
|
+
it("uses chinese-facing labels in channel selection", () => {
|
|
6
|
+
expect(wecomPlugin.meta.label).toBe("WeCom (企业微信)");
|
|
7
|
+
expect(wecomPlugin.meta.selectionLabel).toBe("WeCom (企业微信)");
|
|
8
|
+
expect(wecomPlugin.meta.blurb).toContain("企业微信官方推荐三方插件");
|
|
9
|
+
expect(wecomPlugin.meta.docsLabel).toBe("企业微信");
|
|
10
|
+
expect(wecomPlugin.meta.selectionDocsPrefix).toBe("文档:");
|
|
11
|
+
});
|
|
12
|
+
});
|
package/src/channel.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import {
|
|
12
12
|
DEFAULT_ACCOUNT_ID,
|
|
13
13
|
listWecomAccountIds,
|
|
14
|
+
resolveDerivedPathSummary,
|
|
14
15
|
resolveDefaultWecomAccountId,
|
|
15
16
|
resolveWecomAccount,
|
|
16
17
|
resolveWecomAccountConflict,
|
|
@@ -19,24 +20,38 @@ import type { ResolvedWecomAccount } from "./types/index.js";
|
|
|
19
20
|
import { monitorWecomProvider } from "./gateway-monitor.js";
|
|
20
21
|
import { wecomOnboardingAdapter } from "./onboarding.js";
|
|
21
22
|
import { wecomOutbound } from "./outbound.js";
|
|
22
|
-
import { WEBHOOK_PATHS } from "./types/constants.js";
|
|
23
23
|
|
|
24
24
|
const meta = {
|
|
25
25
|
id: "wecom",
|
|
26
|
-
label: "WeCom",
|
|
27
|
-
selectionLabel: "WeCom (
|
|
26
|
+
label: "WeCom (企业微信)",
|
|
27
|
+
selectionLabel: "WeCom (企业微信)",
|
|
28
28
|
docsPath: "/channels/wecom",
|
|
29
|
-
docsLabel: "
|
|
30
|
-
blurb: "
|
|
29
|
+
docsLabel: "企业微信",
|
|
30
|
+
blurb: "企业微信官方推荐三方插件,默认 Bot WS 配置简单,支持主动发消息与 Agent 全能力。",
|
|
31
|
+
selectionDocsPrefix: "文档:",
|
|
31
32
|
aliases: ["wechatwork", "wework", "qywx", "企微", "企业微信"],
|
|
32
33
|
order: 85,
|
|
33
34
|
quickstartAllowFrom: true,
|
|
34
35
|
};
|
|
35
36
|
|
|
37
|
+
function resolveAccountInboundPath(account: ResolvedWecomAccount): string | undefined {
|
|
38
|
+
const derivedPaths = resolveDerivedPathSummary(account.accountId);
|
|
39
|
+
if (account.bot?.primaryTransport === "webhook" && account.bot.webhookConfigured) {
|
|
40
|
+
return derivedPaths.botWebhook[0];
|
|
41
|
+
}
|
|
42
|
+
if (account.agent?.callbackConfigured) {
|
|
43
|
+
return derivedPaths.agentCallback[0];
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
36
48
|
function normalizeWecomMessagingTarget(raw: string): string | undefined {
|
|
37
49
|
const trimmed = raw.trim();
|
|
38
50
|
if (!trimmed) return undefined;
|
|
39
|
-
|
|
51
|
+
if (/^wecom-agent:/i.test(trimmed)) {
|
|
52
|
+
return trimmed;
|
|
53
|
+
}
|
|
54
|
+
return trimmed.replace(/^(wecom|wechatwork|wework|qywx):/i, "").trim() || undefined;
|
|
40
55
|
}
|
|
41
56
|
|
|
42
57
|
export const wecomPlugin: ChannelPlugin<ResolvedWecomAccount> = {
|
|
@@ -98,7 +113,6 @@ export const wecomPlugin: ChannelPlugin<ResolvedWecomAccount> = {
|
|
|
98
113
|
accountId: account.accountId,
|
|
99
114
|
})?.message ?? "not configured",
|
|
100
115
|
describeAccount: (account, cfg): ChannelAccountSnapshot => {
|
|
101
|
-
const matrixMode = account.accountId !== DEFAULT_ACCOUNT_ID;
|
|
102
116
|
const conflict = resolveWecomAccountConflict({
|
|
103
117
|
cfg: cfg as OpenClawConfig,
|
|
104
118
|
accountId: account.accountId,
|
|
@@ -108,11 +122,7 @@ export const wecomPlugin: ChannelPlugin<ResolvedWecomAccount> = {
|
|
|
108
122
|
name: account.name,
|
|
109
123
|
enabled: account.enabled,
|
|
110
124
|
configured: account.configured && !conflict,
|
|
111
|
-
webhookPath: account
|
|
112
|
-
? (matrixMode ? `${WEBHOOK_PATHS.BOT_PLUGIN}/${account.accountId}` : WEBHOOK_PATHS.BOT_PLUGIN)
|
|
113
|
-
: account.agent?.config
|
|
114
|
-
? (matrixMode ? `${WEBHOOK_PATHS.AGENT_PLUGIN}/${account.accountId}` : WEBHOOK_PATHS.AGENT_PLUGIN)
|
|
115
|
-
: WEBHOOK_PATHS.BOT_PLUGIN,
|
|
125
|
+
webhookPath: resolveAccountInboundPath(account),
|
|
116
126
|
};
|
|
117
127
|
},
|
|
118
128
|
resolveAllowFrom: ({ cfg, accountId }) => {
|
|
@@ -157,11 +167,23 @@ export const wecomPlugin: ChannelPlugin<ResolvedWecomAccount> = {
|
|
|
157
167
|
configured: snapshot.configured ?? false,
|
|
158
168
|
running: snapshot.running ?? false,
|
|
159
169
|
webhookPath: snapshot.webhookPath ?? null,
|
|
170
|
+
transport: (snapshot as { transport?: string }).transport ?? null,
|
|
171
|
+
ownerId: (snapshot as { ownerId?: string }).ownerId ?? null,
|
|
172
|
+
health: (snapshot as { health?: string }).health ?? "idle",
|
|
173
|
+
ownerDriftAt: (snapshot as { ownerDriftAt?: number | null }).ownerDriftAt ?? null,
|
|
174
|
+
connected: (snapshot as { connected?: boolean }).connected,
|
|
175
|
+
authenticated: (snapshot as { authenticated?: boolean }).authenticated,
|
|
160
176
|
lastStartAt: snapshot.lastStartAt ?? null,
|
|
161
177
|
lastStopAt: snapshot.lastStopAt ?? null,
|
|
162
178
|
lastError: snapshot.lastError ?? null,
|
|
179
|
+
lastErrorAt: (snapshot as { lastErrorAt?: number | null }).lastErrorAt ?? null,
|
|
163
180
|
lastInboundAt: snapshot.lastInboundAt ?? null,
|
|
164
181
|
lastOutboundAt: snapshot.lastOutboundAt ?? null,
|
|
182
|
+
recentInboundSummary: (snapshot as { recentInboundSummary?: string | null }).recentInboundSummary ?? null,
|
|
183
|
+
recentOutboundSummary: (snapshot as { recentOutboundSummary?: string | null }).recentOutboundSummary ?? null,
|
|
184
|
+
recentIssueCategory: (snapshot as { recentIssueCategory?: string | null }).recentIssueCategory ?? null,
|
|
185
|
+
recentIssueSummary: (snapshot as { recentIssueSummary?: string | null }).recentIssueSummary ?? null,
|
|
186
|
+
transportSessions: (snapshot as { transportSessions?: string[] }).transportSessions ?? [],
|
|
165
187
|
probe: snapshot.probe,
|
|
166
188
|
lastProbeAt: snapshot.lastProbeAt ?? null,
|
|
167
189
|
}),
|
|
@@ -176,21 +198,26 @@ export const wecomPlugin: ChannelPlugin<ResolvedWecomAccount> = {
|
|
|
176
198
|
name: account.name,
|
|
177
199
|
enabled: account.enabled,
|
|
178
200
|
configured: account.configured && !conflict,
|
|
179
|
-
webhookPath: account
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
: WEBHOOK_PATHS.BOT_PLUGIN,
|
|
201
|
+
webhookPath: resolveAccountInboundPath(account),
|
|
202
|
+
primaryTransport: account.bot?.primaryTransport ?? (account.agent ? "agent-callback" : null),
|
|
203
|
+
transport: (runtime as { transport?: string } | undefined)?.transport ?? null,
|
|
204
|
+
ownerId: (runtime as { ownerId?: string } | undefined)?.ownerId ?? null,
|
|
205
|
+
health: (runtime as { health?: string } | undefined)?.health ?? "idle",
|
|
206
|
+
ownerDriftAt: (runtime as { ownerDriftAt?: number | null } | undefined)?.ownerDriftAt ?? null,
|
|
207
|
+
connected: (runtime as { connected?: boolean } | undefined)?.connected,
|
|
208
|
+
authenticated: (runtime as { authenticated?: boolean } | undefined)?.authenticated,
|
|
188
209
|
running: runtime?.running ?? false,
|
|
189
210
|
lastStartAt: runtime?.lastStartAt ?? null,
|
|
190
211
|
lastStopAt: runtime?.lastStopAt ?? null,
|
|
191
212
|
lastError: runtime?.lastError ?? conflict?.message ?? null,
|
|
213
|
+
lastErrorAt: (runtime as { lastErrorAt?: number | null } | undefined)?.lastErrorAt ?? null,
|
|
192
214
|
lastInboundAt: runtime?.lastInboundAt ?? null,
|
|
193
215
|
lastOutboundAt: runtime?.lastOutboundAt ?? null,
|
|
216
|
+
recentInboundSummary: (runtime as { recentInboundSummary?: string | null } | undefined)?.recentInboundSummary ?? null,
|
|
217
|
+
recentOutboundSummary: (runtime as { recentOutboundSummary?: string | null } | undefined)?.recentOutboundSummary ?? null,
|
|
218
|
+
recentIssueCategory: (runtime as { recentIssueCategory?: string | null } | undefined)?.recentIssueCategory ?? null,
|
|
219
|
+
recentIssueSummary: (runtime as { recentIssueSummary?: string | null } | undefined)?.recentIssueSummary ?? null,
|
|
220
|
+
transportSessions: (runtime as { transportSessions?: string[] } | undefined)?.transportSessions ?? [],
|
|
194
221
|
dmPolicy: account.bot?.config.dm?.policy ?? "pairing",
|
|
195
222
|
};
|
|
196
223
|
},
|