@yanhaidao/wecom 2.3.160 → 2.3.190
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 +294 -379
- package/SKILLS_CAL.md +895 -0
- package/SKILLS_DOC.md +2288 -0
- package/changelog/v2.3.18.md +22 -0
- package/changelog/v2.3.19.md +73 -0
- package/index.ts +39 -3
- package/package.json +2 -3
- package/src/agent/handler.event-filter.test.ts +11 -0
- package/src/agent/handler.ts +732 -643
- package/src/app/account-runtime.ts +46 -20
- package/src/app/index.ts +20 -1
- package/src/capability/bot/stream-orchestrator.ts +1 -1
- package/src/capability/calendar/SKILLS_CHECKLIST.md +251 -0
- package/src/capability/calendar/client.ts +815 -0
- package/src/capability/calendar/index.ts +3 -0
- package/src/capability/calendar/schema.ts +417 -0
- package/src/capability/calendar/tool.ts +417 -0
- package/src/capability/calendar/types.ts +309 -0
- package/src/capability/doc/client.ts +788 -64
- package/src/capability/doc/schema.ts +419 -318
- package/src/capability/doc/tool.ts +1517 -1178
- package/src/capability/doc/types.ts +130 -14
- package/src/capability/mcp/index.ts +10 -0
- package/src/capability/mcp/schema.ts +107 -0
- package/src/capability/mcp/tool.ts +170 -0
- package/src/capability/mcp/transport.ts +394 -0
- package/src/channel.ts +70 -28
- package/src/config/index.ts +7 -1
- package/src/config/media.test.ts +113 -0
- package/src/config/media.ts +133 -6
- package/src/config/schema.ts +74 -102
- package/src/outbound.test.ts +250 -15
- package/src/outbound.ts +155 -30
- package/src/runtime/reply-orchestrator.test.ts +35 -2
- package/src/runtime/reply-orchestrator.ts +14 -2
- package/src/runtime/routing-bridge.test.ts +115 -0
- package/src/runtime/routing-bridge.ts +26 -1
- package/src/runtime/session-manager.ts +20 -6
- package/src/runtime/source-registry.ts +165 -0
- package/src/transport/bot-webhook/inbound-normalizer.ts +4 -4
- package/src/transport/bot-ws/media.test.ts +44 -0
- package/src/transport/bot-ws/media.ts +272 -0
- package/src/transport/bot-ws/reply.test.ts +216 -18
- package/src/transport/bot-ws/reply.ts +116 -21
- package/src/transport/bot-ws/sdk-adapter.test.ts +64 -1
- package/src/transport/bot-ws/sdk-adapter.ts +89 -12
- package/src/types/config.ts +3 -0
- package/.claude/settings.local.json +0 -11
- package/docs/update-content-fix.md +0 -135
package/src/outbound.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import type { ChannelOutboundAdapter, ChannelOutboundContext } from "openclaw/plugin-sdk";
|
|
2
|
-
|
|
3
|
-
import { resolveWecomAccount, resolveWecomAccountConflict, resolveWecomAccounts } from "./config/index.js";
|
|
4
2
|
import { WecomAgentDeliveryService } from "./capability/agent/index.js";
|
|
3
|
+
import {
|
|
4
|
+
resolveWecomMergedMediaLocalRoots,
|
|
5
|
+
resolveWecomMediaMaxBytes,
|
|
6
|
+
resolveWecomAccount,
|
|
7
|
+
resolveWecomAccountConflict,
|
|
8
|
+
resolveWecomAccounts,
|
|
9
|
+
} from "./config/index.js";
|
|
5
10
|
import { getAccountRuntime, getBotWsPushHandle, getWecomRuntime } from "./runtime.js";
|
|
6
11
|
import { resolveScopedWecomTarget } from "./target.js";
|
|
7
12
|
|
|
@@ -49,7 +54,9 @@ function resolveAgentConfigOrThrow(params: {
|
|
|
49
54
|
);
|
|
50
55
|
}
|
|
51
56
|
// 注意:不要在日志里输出 corpSecret 等敏感信息
|
|
52
|
-
getAccountRuntime(account.accountId)?.log.info?.(
|
|
57
|
+
getAccountRuntime(account.accountId)?.log.info?.(
|
|
58
|
+
`[wecom-outbound] Using agent config: accountId=${account.accountId}, corpId=${account.corpId}, agentId=${account.agentId}`,
|
|
59
|
+
);
|
|
53
60
|
return account;
|
|
54
61
|
}
|
|
55
62
|
|
|
@@ -89,7 +96,13 @@ function shouldPreferBotWsOutbound(params: {
|
|
|
89
96
|
accountId: params.accountId,
|
|
90
97
|
});
|
|
91
98
|
return {
|
|
92
|
-
preferred:
|
|
99
|
+
preferred:
|
|
100
|
+
!isExplicitAgentTarget(params.to) &&
|
|
101
|
+
Boolean(
|
|
102
|
+
account.bot?.configured &&
|
|
103
|
+
account.bot.primaryTransport === "ws" &&
|
|
104
|
+
account.bot.wsConfigured,
|
|
105
|
+
),
|
|
93
106
|
accountId: account.accountId,
|
|
94
107
|
};
|
|
95
108
|
}
|
|
@@ -122,12 +135,71 @@ async function sendTextViaBotWs(params: {
|
|
|
122
135
|
`WeCom outbound account=${accountId} is configured for Bot WS active push, but the WS transport is not connected.`,
|
|
123
136
|
);
|
|
124
137
|
}
|
|
125
|
-
console.log(
|
|
138
|
+
console.log(
|
|
139
|
+
`[wecom-outbound] Sending Bot WS active message to target=${String(params.to ?? "")} chatId=${chatId} (len=${params.text.length})`,
|
|
140
|
+
);
|
|
126
141
|
await handle.sendMarkdown(chatId, params.text);
|
|
127
142
|
console.log(`[wecom-outbound] Successfully sent Bot WS active message to ${chatId}`);
|
|
128
143
|
return true;
|
|
129
144
|
}
|
|
130
145
|
|
|
146
|
+
async function sendMediaViaBotWs(params: {
|
|
147
|
+
cfg: ChannelOutboundContext["cfg"];
|
|
148
|
+
accountId?: string | null;
|
|
149
|
+
to: string | undefined;
|
|
150
|
+
mediaUrl: string;
|
|
151
|
+
text?: string;
|
|
152
|
+
mediaLocalRoots?: readonly string[];
|
|
153
|
+
}): Promise<{
|
|
154
|
+
attempted: boolean;
|
|
155
|
+
sent: boolean;
|
|
156
|
+
reason?: string;
|
|
157
|
+
}> {
|
|
158
|
+
const { preferred, accountId } = shouldPreferBotWsOutbound(params);
|
|
159
|
+
if (!preferred) {
|
|
160
|
+
return { attempted: false, sent: false };
|
|
161
|
+
}
|
|
162
|
+
const chatId = resolveBotWsChatTarget({
|
|
163
|
+
to: params.to,
|
|
164
|
+
accountId,
|
|
165
|
+
});
|
|
166
|
+
if (!chatId) {
|
|
167
|
+
return { attempted: false, sent: false };
|
|
168
|
+
}
|
|
169
|
+
const handle = getBotWsPushHandle(accountId);
|
|
170
|
+
if (!handle) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
`WeCom outbound account=${accountId} is configured for Bot WS active push, but no live WS runtime is registered.`,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
if (!handle.isConnected()) {
|
|
176
|
+
throw new Error(
|
|
177
|
+
`WeCom outbound account=${accountId} is configured for Bot WS active push, but the WS transport is not connected.`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
console.log(
|
|
181
|
+
`[wecom-outbound] Sending Bot WS media to target=${String(params.to ?? "")} chatId=${chatId} media=${params.mediaUrl}`,
|
|
182
|
+
);
|
|
183
|
+
const effectiveMediaLocalRoots = resolveWecomMergedMediaLocalRoots({
|
|
184
|
+
cfg: params.cfg,
|
|
185
|
+
baseRoots: params.mediaLocalRoots,
|
|
186
|
+
});
|
|
187
|
+
const result = await handle.sendMedia({
|
|
188
|
+
chatId,
|
|
189
|
+
mediaUrl: params.mediaUrl,
|
|
190
|
+
text: params.text,
|
|
191
|
+
mediaLocalRoots: effectiveMediaLocalRoots,
|
|
192
|
+
maxBytes: resolveWecomMediaMaxBytes(params.cfg, accountId),
|
|
193
|
+
});
|
|
194
|
+
if (result.ok) {
|
|
195
|
+
console.log(`[wecom-outbound] Successfully sent Bot WS media to ${chatId}`);
|
|
196
|
+
return { attempted: true, sent: true };
|
|
197
|
+
}
|
|
198
|
+
const reason = result.rejectReason || result.error || "unknown";
|
|
199
|
+
console.warn(`[wecom-outbound] Bot WS media failed for ${chatId}: ${reason}`);
|
|
200
|
+
return { attempted: true, sent: false, reason };
|
|
201
|
+
}
|
|
202
|
+
|
|
131
203
|
export const wecomOutbound: ChannelOutboundAdapter = {
|
|
132
204
|
deliveryMode: "direct",
|
|
133
205
|
chunkerMode: "text",
|
|
@@ -155,8 +227,7 @@ export const wecomOutbound: ChannelOutboundAdapter = {
|
|
|
155
227
|
const trimmed = String(outgoingText ?? "").trim();
|
|
156
228
|
const rawTo = typeof to === "string" ? to.trim().toLowerCase() : "";
|
|
157
229
|
const isAgentSessionTarget = rawTo.startsWith("wecom-agent:");
|
|
158
|
-
const looksLikeNewSessionAck =
|
|
159
|
-
/new session started/i.test(trimmed) && /model:/i.test(trimmed);
|
|
230
|
+
const looksLikeNewSessionAck = /new session started/i.test(trimmed) && /model:/i.test(trimmed);
|
|
160
231
|
|
|
161
232
|
if (looksLikeNewSessionAck) {
|
|
162
233
|
if (!isAgentSessionTarget) {
|
|
@@ -174,7 +245,7 @@ export const wecomOutbound: ChannelOutboundAdapter = {
|
|
|
174
245
|
|
|
175
246
|
let sentViaBotWs = false;
|
|
176
247
|
let agent: any = null;
|
|
177
|
-
|
|
248
|
+
|
|
178
249
|
try {
|
|
179
250
|
sentViaBotWs = await sendTextViaBotWs({
|
|
180
251
|
cfg,
|
|
@@ -185,7 +256,9 @@ export const wecomOutbound: ChannelOutboundAdapter = {
|
|
|
185
256
|
if (!sentViaBotWs) {
|
|
186
257
|
// Defer Agent resolution until needed for fallback
|
|
187
258
|
agent = resolveAgentConfigOrThrow({ cfg, accountId });
|
|
188
|
-
getAccountRuntime(agent.accountId)?.log.info?.(
|
|
259
|
+
getAccountRuntime(agent.accountId)?.log.info?.(
|
|
260
|
+
`[wecom-outbound] Sending text to target=${String(to ?? "")} (len=${outgoingText.length})`,
|
|
261
|
+
);
|
|
189
262
|
const deliveryService = new WecomAgentDeliveryService(agent);
|
|
190
263
|
await deliveryService.sendText({
|
|
191
264
|
to,
|
|
@@ -195,7 +268,9 @@ export const wecomOutbound: ChannelOutboundAdapter = {
|
|
|
195
268
|
}
|
|
196
269
|
} catch (err) {
|
|
197
270
|
if (agent) {
|
|
198
|
-
getAccountRuntime(agent.accountId)?.log.error?.(
|
|
271
|
+
getAccountRuntime(agent.accountId)?.log.error?.(
|
|
272
|
+
`[wecom-outbound] Failed to send text to ${String(to ?? "")}: ${err instanceof Error ? err.message : String(err)}`,
|
|
273
|
+
);
|
|
199
274
|
}
|
|
200
275
|
throw err;
|
|
201
276
|
}
|
|
@@ -206,18 +281,42 @@ export const wecomOutbound: ChannelOutboundAdapter = {
|
|
|
206
281
|
timestamp: Date.now(),
|
|
207
282
|
};
|
|
208
283
|
},
|
|
209
|
-
sendMedia: async ({
|
|
284
|
+
sendMedia: async ({
|
|
285
|
+
cfg,
|
|
286
|
+
to,
|
|
287
|
+
text,
|
|
288
|
+
mediaUrl,
|
|
289
|
+
accountId,
|
|
290
|
+
mediaLocalRoots,
|
|
291
|
+
}: ChannelOutboundContext) => {
|
|
210
292
|
// signal removed - not supported in current SDK
|
|
293
|
+
if (!mediaUrl) {
|
|
294
|
+
throw new Error("WeCom outbound requires mediaUrl.");
|
|
295
|
+
}
|
|
211
296
|
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
297
|
+
const botWs = await sendMediaViaBotWs({
|
|
298
|
+
cfg,
|
|
299
|
+
accountId,
|
|
300
|
+
to,
|
|
301
|
+
text,
|
|
302
|
+
mediaUrl,
|
|
303
|
+
mediaLocalRoots,
|
|
304
|
+
});
|
|
305
|
+
if (botWs.sent) {
|
|
306
|
+
return {
|
|
307
|
+
channel: "wecom",
|
|
308
|
+
messageId: `bot-ws-media-${Date.now()}`,
|
|
309
|
+
timestamp: Date.now(),
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
if (botWs.attempted) {
|
|
313
|
+
throw new Error(
|
|
314
|
+
`WeCom Bot WS media delivery failed for ${String(to ?? "")}: ${botWs.reason ?? "unknown"}`,
|
|
315
|
+
);
|
|
215
316
|
}
|
|
317
|
+
|
|
216
318
|
const agent = resolveAgentConfigOrThrow({ cfg, accountId });
|
|
217
319
|
const deliveryService = new WecomAgentDeliveryService(agent);
|
|
218
|
-
if (!mediaUrl) {
|
|
219
|
-
throw new Error("WeCom outbound requires mediaUrl.");
|
|
220
|
-
}
|
|
221
320
|
|
|
222
321
|
let buffer: Buffer;
|
|
223
322
|
let contentType: string;
|
|
@@ -246,23 +345,49 @@ export const wecomOutbound: ChannelOutboundAdapter = {
|
|
|
246
345
|
// 根据扩展名推断 content-type
|
|
247
346
|
const ext = path.extname(mediaUrl).slice(1).toLowerCase();
|
|
248
347
|
const mimeTypes: Record<string, string> = {
|
|
249
|
-
jpg: "image/jpeg",
|
|
250
|
-
|
|
251
|
-
|
|
348
|
+
jpg: "image/jpeg",
|
|
349
|
+
jpeg: "image/jpeg",
|
|
350
|
+
png: "image/png",
|
|
351
|
+
gif: "image/gif",
|
|
352
|
+
webp: "image/webp",
|
|
353
|
+
bmp: "image/bmp",
|
|
354
|
+
mp3: "audio/mpeg",
|
|
355
|
+
wav: "audio/wav",
|
|
356
|
+
amr: "audio/amr",
|
|
357
|
+
mp4: "video/mp4",
|
|
358
|
+
pdf: "application/pdf",
|
|
359
|
+
doc: "application/msword",
|
|
252
360
|
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
253
|
-
xls: "application/vnd.ms-excel",
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
361
|
+
xls: "application/vnd.ms-excel",
|
|
362
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
363
|
+
ppt: "application/vnd.ms-powerpoint",
|
|
364
|
+
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
365
|
+
txt: "text/plain",
|
|
366
|
+
csv: "text/csv",
|
|
367
|
+
tsv: "text/tab-separated-values",
|
|
368
|
+
md: "text/markdown",
|
|
369
|
+
json: "application/json",
|
|
370
|
+
xml: "application/xml",
|
|
371
|
+
yaml: "application/yaml",
|
|
372
|
+
yml: "application/yaml",
|
|
373
|
+
zip: "application/zip",
|
|
374
|
+
rar: "application/vnd.rar",
|
|
375
|
+
"7z": "application/x-7z-compressed",
|
|
376
|
+
tar: "application/x-tar",
|
|
377
|
+
gz: "application/gzip",
|
|
378
|
+
tgz: "application/gzip",
|
|
379
|
+
rtf: "application/rtf",
|
|
380
|
+
odt: "application/vnd.oasis.opendocument.text",
|
|
260
381
|
};
|
|
261
382
|
contentType = mimeTypes[ext] || "application/octet-stream";
|
|
262
|
-
console.log(
|
|
383
|
+
console.log(
|
|
384
|
+
`[wecom-outbound] Reading local file: ${mediaUrl}, ext=${ext}, contentType=${contentType}`,
|
|
385
|
+
);
|
|
263
386
|
}
|
|
264
387
|
|
|
265
|
-
console.log(
|
|
388
|
+
console.log(
|
|
389
|
+
`[wecom-outbound] Sending media to ${String(to ?? "")} (filename=${filename}, contentType=${contentType})`,
|
|
390
|
+
);
|
|
266
391
|
|
|
267
392
|
try {
|
|
268
393
|
await deliveryService.sendMedia({
|
|
@@ -280,7 +405,7 @@ export const wecomOutbound: ChannelOutboundAdapter = {
|
|
|
280
405
|
|
|
281
406
|
return {
|
|
282
407
|
channel: "wecom",
|
|
283
|
-
messageId:
|
|
408
|
+
messageId: `${botWs.attempted ? "agent-fallback-media" : "agent-media"}-${Date.now()}`,
|
|
284
409
|
timestamp: Date.now(),
|
|
285
410
|
};
|
|
286
411
|
},
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from "vitest";
|
|
2
|
-
|
|
3
2
|
import { dispatchRuntimeReply } from "./reply-orchestrator.js";
|
|
4
3
|
|
|
5
4
|
describe("dispatchRuntimeReply", () => {
|
|
6
5
|
it("enables block streaming for bot-ws replies", async () => {
|
|
7
|
-
const dispatchReplyWithBufferedBlockDispatcher = vi
|
|
6
|
+
const dispatchReplyWithBufferedBlockDispatcher = vi
|
|
7
|
+
.fn()
|
|
8
|
+
.mockResolvedValue({ queuedFinal: false, counts: { block: 0, final: 0, tool: 0 } });
|
|
8
9
|
const core = {
|
|
9
10
|
channel: {
|
|
10
11
|
reply: {
|
|
@@ -35,4 +36,36 @@ describe("dispatchRuntimeReply", () => {
|
|
|
35
36
|
}),
|
|
36
37
|
);
|
|
37
38
|
});
|
|
39
|
+
|
|
40
|
+
it("synthesizes a final close for bot-ws when only block replies were queued", async () => {
|
|
41
|
+
const dispatchReplyWithBufferedBlockDispatcher = vi.fn().mockResolvedValue({
|
|
42
|
+
queuedFinal: false,
|
|
43
|
+
counts: { block: 1, final: 0, tool: 0 },
|
|
44
|
+
});
|
|
45
|
+
const deliver = vi.fn().mockResolvedValue(undefined);
|
|
46
|
+
const core = {
|
|
47
|
+
channel: {
|
|
48
|
+
reply: {
|
|
49
|
+
dispatchReplyWithBufferedBlockDispatcher,
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
} as any;
|
|
53
|
+
|
|
54
|
+
await dispatchRuntimeReply({
|
|
55
|
+
core,
|
|
56
|
+
cfg: {} as any,
|
|
57
|
+
session: { ctx: { SessionKey: "session-a" } } as any,
|
|
58
|
+
replyHandle: {
|
|
59
|
+
context: {
|
|
60
|
+
transport: "bot-ws",
|
|
61
|
+
accountId: "default",
|
|
62
|
+
raw: { transport: "bot-ws", envelopeType: "ws", body: {} },
|
|
63
|
+
},
|
|
64
|
+
deliver,
|
|
65
|
+
} as any,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
expect(deliver).toHaveBeenCalledTimes(1);
|
|
69
|
+
expect(deliver).toHaveBeenCalledWith({ text: "" }, { kind: "final" });
|
|
70
|
+
});
|
|
38
71
|
});
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
|
|
2
|
-
|
|
3
2
|
import type { ReplyHandle } from "../types/index.js";
|
|
4
3
|
import type { PreparedSession } from "./session-manager.js";
|
|
5
4
|
|
|
@@ -29,7 +28,7 @@ export async function dispatchRuntimeReply(params: {
|
|
|
29
28
|
replyHandle: ReplyHandle;
|
|
30
29
|
}): Promise<void> {
|
|
31
30
|
const { core, cfg, session, replyHandle } = params;
|
|
32
|
-
await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
31
|
+
const result = await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
33
32
|
ctx: session.ctx,
|
|
34
33
|
cfg,
|
|
35
34
|
replyOptions:
|
|
@@ -52,4 +51,17 @@ export async function dispatchRuntimeReply(params: {
|
|
|
52
51
|
},
|
|
53
52
|
},
|
|
54
53
|
});
|
|
54
|
+
|
|
55
|
+
if (
|
|
56
|
+
replyHandle.context.transport === "bot-ws" &&
|
|
57
|
+
result &&
|
|
58
|
+
result.queuedFinal !== true &&
|
|
59
|
+
(result.counts?.block ?? 0) > 0
|
|
60
|
+
) {
|
|
61
|
+
await dispatchReplyPayload({
|
|
62
|
+
replyHandle,
|
|
63
|
+
payload: { text: "" },
|
|
64
|
+
kind: "final",
|
|
65
|
+
});
|
|
66
|
+
}
|
|
55
67
|
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { resolveRuntimeRoute } from "./routing-bridge.js";
|
|
5
|
+
import { ensureDynamicAgentListed } from "../dynamic-agent.js";
|
|
6
|
+
import type { UnifiedInboundEvent } from "../types/index.js";
|
|
7
|
+
|
|
8
|
+
vi.mock("../dynamic-agent.js", async () => {
|
|
9
|
+
const actual = await vi.importActual<typeof import("../dynamic-agent.js")>(
|
|
10
|
+
"../dynamic-agent.js",
|
|
11
|
+
);
|
|
12
|
+
return {
|
|
13
|
+
...actual,
|
|
14
|
+
ensureDynamicAgentListed: vi.fn().mockResolvedValue(undefined),
|
|
15
|
+
};
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("resolveRuntimeRoute", () => {
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
vi.mocked(ensureDynamicAgentListed).mockClear();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("overrides WS runtime routes with dynamic agent routing for direct chats", () => {
|
|
24
|
+
const baseRoute = {
|
|
25
|
+
agentId: "main",
|
|
26
|
+
channel: "wecom",
|
|
27
|
+
accountId: "acct-ws",
|
|
28
|
+
sessionKey: "agent:main",
|
|
29
|
+
mainSessionKey: "agent:main:main",
|
|
30
|
+
lastRoutePolicy: "session" as const,
|
|
31
|
+
matchedBy: "default" as const,
|
|
32
|
+
};
|
|
33
|
+
const resolveAgentRoute = vi.fn().mockReturnValue({ ...baseRoute });
|
|
34
|
+
const core = {
|
|
35
|
+
channel: {
|
|
36
|
+
routing: {
|
|
37
|
+
resolveAgentRoute,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
} as unknown as PluginRuntime;
|
|
41
|
+
const cfg = {
|
|
42
|
+
channels: {
|
|
43
|
+
wecom: {
|
|
44
|
+
dynamicAgents: {
|
|
45
|
+
enabled: true,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
} as OpenClawConfig;
|
|
50
|
+
const event = {
|
|
51
|
+
accountId: "acct-ws",
|
|
52
|
+
conversation: {
|
|
53
|
+
accountId: "acct-ws",
|
|
54
|
+
peerKind: "direct",
|
|
55
|
+
peerId: "HiDaoMax",
|
|
56
|
+
senderId: "HiDaoMax",
|
|
57
|
+
},
|
|
58
|
+
} as UnifiedInboundEvent;
|
|
59
|
+
|
|
60
|
+
const route = resolveRuntimeRoute({ core, cfg, event });
|
|
61
|
+
|
|
62
|
+
expect(resolveAgentRoute).toHaveBeenCalledOnce();
|
|
63
|
+
expect(route.agentId).toBe("wecom-acct-ws-dm-hidaomax");
|
|
64
|
+
expect(route.sessionKey).toBe(
|
|
65
|
+
"agent:wecom-acct-ws-dm-hidaomax:wecom:acct-ws:dm:HiDaoMax",
|
|
66
|
+
);
|
|
67
|
+
expect(vi.mocked(ensureDynamicAgentListed)).toHaveBeenCalledWith(
|
|
68
|
+
"wecom-acct-ws-dm-hidaomax",
|
|
69
|
+
core,
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("keeps the resolved core route when dynamic agent routing is disabled for the sender", () => {
|
|
74
|
+
const baseRoute = {
|
|
75
|
+
agentId: "main",
|
|
76
|
+
channel: "wecom",
|
|
77
|
+
accountId: "acct-ws",
|
|
78
|
+
sessionKey: "agent:main",
|
|
79
|
+
mainSessionKey: "agent:main:main",
|
|
80
|
+
lastRoutePolicy: "session" as const,
|
|
81
|
+
matchedBy: "binding.account" as const,
|
|
82
|
+
};
|
|
83
|
+
const core = {
|
|
84
|
+
channel: {
|
|
85
|
+
routing: {
|
|
86
|
+
resolveAgentRoute: vi.fn().mockReturnValue({ ...baseRoute }),
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
} as unknown as PluginRuntime;
|
|
90
|
+
const cfg = {
|
|
91
|
+
channels: {
|
|
92
|
+
wecom: {
|
|
93
|
+
dynamicAgents: {
|
|
94
|
+
enabled: true,
|
|
95
|
+
adminUsers: ["HiDaoMax"],
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
} as OpenClawConfig;
|
|
100
|
+
const event = {
|
|
101
|
+
accountId: "acct-ws",
|
|
102
|
+
conversation: {
|
|
103
|
+
accountId: "acct-ws",
|
|
104
|
+
peerKind: "direct",
|
|
105
|
+
peerId: "HiDaoMax",
|
|
106
|
+
senderId: "HiDaoMax",
|
|
107
|
+
},
|
|
108
|
+
} as UnifiedInboundEvent;
|
|
109
|
+
|
|
110
|
+
const route = resolveRuntimeRoute({ core, cfg, event });
|
|
111
|
+
|
|
112
|
+
expect(route).toEqual(baseRoute);
|
|
113
|
+
expect(vi.mocked(ensureDynamicAgentListed)).not.toHaveBeenCalled();
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
|
|
2
2
|
|
|
3
|
+
import {
|
|
4
|
+
ensureDynamicAgentListed,
|
|
5
|
+
generateAgentId,
|
|
6
|
+
shouldUseDynamicAgent,
|
|
7
|
+
} from "../dynamic-agent.js";
|
|
3
8
|
import type { UnifiedInboundEvent } from "../types/index.js";
|
|
4
9
|
|
|
5
10
|
export function resolveRuntimeRoute(params: {
|
|
@@ -7,7 +12,7 @@ export function resolveRuntimeRoute(params: {
|
|
|
7
12
|
cfg: OpenClawConfig;
|
|
8
13
|
event: UnifiedInboundEvent;
|
|
9
14
|
}) {
|
|
10
|
-
|
|
15
|
+
const route = params.core.channel.routing.resolveAgentRoute({
|
|
11
16
|
cfg: params.cfg,
|
|
12
17
|
channel: "wecom",
|
|
13
18
|
accountId: params.event.accountId,
|
|
@@ -16,4 +21,24 @@ export function resolveRuntimeRoute(params: {
|
|
|
16
21
|
id: params.event.conversation.peerId,
|
|
17
22
|
},
|
|
18
23
|
});
|
|
24
|
+
|
|
25
|
+
const chatType = params.event.conversation.peerKind === "group" ? "group" : "dm";
|
|
26
|
+
const useDynamicAgent = shouldUseDynamicAgent({
|
|
27
|
+
chatType,
|
|
28
|
+
senderId: params.event.conversation.senderId,
|
|
29
|
+
config: params.cfg,
|
|
30
|
+
});
|
|
31
|
+
if (!useDynamicAgent) {
|
|
32
|
+
return route;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const targetAgentId = generateAgentId(
|
|
36
|
+
chatType,
|
|
37
|
+
params.event.conversation.peerId,
|
|
38
|
+
params.event.accountId,
|
|
39
|
+
);
|
|
40
|
+
route.agentId = targetAgentId;
|
|
41
|
+
route.sessionKey = `agent:${targetAgentId}:wecom:${params.event.accountId}:${chatType}:${params.event.conversation.peerId}`;
|
|
42
|
+
ensureDynamicAgentListed(targetAgentId, params.core).catch(() => {});
|
|
43
|
+
return route;
|
|
19
44
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
|
|
2
|
-
|
|
3
|
-
import { resolveRuntimeRoute } from "./routing-bridge.js";
|
|
4
|
-
import type { UnifiedInboundEvent } from "../types/index.js";
|
|
5
2
|
import type { WecomMediaService } from "../shared/media-service.js";
|
|
3
|
+
import type { UnifiedInboundEvent } from "../types/index.js";
|
|
4
|
+
import { resolveRuntimeRoute } from "./routing-bridge.js";
|
|
5
|
+
import { registerWecomSourceSnapshot } from "./source-registry.js";
|
|
6
6
|
|
|
7
7
|
export type PreparedSession = {
|
|
8
8
|
route: ReturnType<typeof resolveRuntimeRoute>;
|
|
@@ -18,6 +18,14 @@ export async function prepareInboundSession(params: {
|
|
|
18
18
|
}): Promise<PreparedSession> {
|
|
19
19
|
const { core, cfg, event, mediaService } = params;
|
|
20
20
|
const route = resolveRuntimeRoute({ core, cfg, event });
|
|
21
|
+
if (event.transport === "bot-ws") {
|
|
22
|
+
registerWecomSourceSnapshot({
|
|
23
|
+
accountId: event.accountId,
|
|
24
|
+
source: "bot-ws",
|
|
25
|
+
messageId: event.messageId,
|
|
26
|
+
sessionKey: route.sessionKey,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
21
29
|
const storePath = core.channel.session.resolveStorePath(cfg.session?.store, {
|
|
22
30
|
agentId: route.agentId,
|
|
23
31
|
});
|
|
@@ -47,7 +55,10 @@ export async function prepareInboundSession(params: {
|
|
|
47
55
|
event.conversation.peerKind === "group"
|
|
48
56
|
? `wecom:group:${event.conversation.peerId}`
|
|
49
57
|
: `wecom:user:${event.conversation.senderId}`,
|
|
50
|
-
To:
|
|
58
|
+
To:
|
|
59
|
+
event.conversation.peerKind === "group"
|
|
60
|
+
? `wecom:group:${event.conversation.peerId}`
|
|
61
|
+
: `wecom:user:${event.conversation.peerId}`,
|
|
51
62
|
SessionKey: route.sessionKey,
|
|
52
63
|
AccountId: route.accountId,
|
|
53
64
|
ChatType: event.conversation.peerKind,
|
|
@@ -57,7 +68,10 @@ export async function prepareInboundSession(params: {
|
|
|
57
68
|
Provider: "wecom",
|
|
58
69
|
Surface: "wecom",
|
|
59
70
|
OriginatingChannel: "wecom",
|
|
60
|
-
OriginatingTo:
|
|
71
|
+
OriginatingTo:
|
|
72
|
+
event.conversation.peerKind === "group"
|
|
73
|
+
? `wecom:group:${event.conversation.peerId}`
|
|
74
|
+
: `wecom:user:${event.conversation.peerId}`,
|
|
61
75
|
MessageSid: event.messageId,
|
|
62
76
|
CommandAuthorized: true,
|
|
63
77
|
MediaPath: mediaPath,
|
|
@@ -69,7 +83,7 @@ export async function prepareInboundSession(params: {
|
|
|
69
83
|
storePath,
|
|
70
84
|
sessionKey: ctx.SessionKey ?? route.sessionKey,
|
|
71
85
|
ctx,
|
|
72
|
-
onRecordError: () => {
|
|
86
|
+
onRecordError: () => {},
|
|
73
87
|
});
|
|
74
88
|
|
|
75
89
|
return { route, ctx, storePath };
|