@dingxiang-me/openclaw-wechat 2.1.0 → 2.3.0
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/CHANGELOG.md +72 -0
- package/README.en.md +181 -14
- package/README.md +201 -16
- package/docs/channels/wecom.md +137 -1
- package/openclaw.plugin.json +688 -6
- package/package.json +204 -4
- package/scripts/wecom-agent-selfcheck.mjs +775 -0
- package/scripts/wecom-bot-longconn-probe.mjs +582 -0
- package/scripts/wecom-bot-selfcheck.mjs +952 -0
- package/scripts/wecom-callback-matrix.mjs +224 -0
- package/scripts/wecom-doctor.mjs +1407 -0
- package/scripts/wecom-e2e-scenario.mjs +333 -0
- package/scripts/wecom-migrate.mjs +261 -0
- package/scripts/wecom-quickstart.mjs +1824 -0
- package/scripts/wecom-release-check.mjs +232 -0
- package/scripts/wecom-remote-e2e.mjs +310 -0
- package/scripts/wecom-selfcheck.mjs +1255 -0
- package/scripts/wecom-smoke.sh +74 -0
- package/src/core/delivery-router.js +21 -0
- package/src/core.js +619 -30
- package/src/wecom/account-config-core.js +27 -1
- package/src/wecom/account-config.js +19 -2
- package/src/wecom/agent-dispatch-executor.js +11 -0
- package/src/wecom/agent-dispatch-handlers.js +61 -8
- package/src/wecom/agent-inbound-guards.js +24 -0
- package/src/wecom/agent-inbound-processor.js +34 -2
- package/src/wecom/agent-late-reply-runtime.js +30 -2
- package/src/wecom/agent-text-sender.js +2 -0
- package/src/wecom/api-client-core.js +27 -19
- package/src/wecom/api-client-media.js +16 -7
- package/src/wecom/api-client-send-text.js +4 -0
- package/src/wecom/api-client-send-typed.js +4 -1
- package/src/wecom/api-client-senders.js +41 -3
- package/src/wecom/api-client.js +1 -0
- package/src/wecom/bot-dispatch-fallback.js +18 -3
- package/src/wecom/bot-dispatch-handlers.js +47 -10
- package/src/wecom/bot-inbound-dispatch-runtime.js +3 -0
- package/src/wecom/bot-inbound-executor-helpers.js +11 -1
- package/src/wecom/bot-inbound-executor.js +24 -0
- package/src/wecom/bot-inbound-guards.js +31 -1
- package/src/wecom/channel-config-schema.js +132 -0
- package/src/wecom/channel-plugin.js +348 -7
- package/src/wecom/command-handlers.js +102 -11
- package/src/wecom/command-status-text.js +206 -0
- package/src/wecom/doc-client.js +7 -1
- package/src/wecom/inbound-content-handler-file-video-link.js +4 -0
- package/src/wecom/inbound-content-handler-image-voice.js +6 -0
- package/src/wecom/inbound-content.js +5 -0
- package/src/wecom/installer-api.js +910 -0
- package/src/wecom/media-download.js +2 -2
- package/src/wecom/migration-diagnostics.js +816 -0
- package/src/wecom/network-config.js +91 -0
- package/src/wecom/observability-metrics.js +9 -3
- package/src/wecom/outbound-agent-delivery.js +313 -0
- package/src/wecom/outbound-agent-media-sender.js +37 -7
- package/src/wecom/outbound-agent-push.js +1 -0
- package/src/wecom/outbound-delivery.js +129 -12
- package/src/wecom/outbound-stream-msg-item.js +25 -2
- package/src/wecom/outbound-webhook-delivery.js +19 -0
- package/src/wecom/outbound-webhook-media.js +30 -6
- package/src/wecom/pending-reply-manager.js +143 -0
- package/src/wecom/plugin-account-policy-services.js +26 -0
- package/src/wecom/plugin-base-services.js +58 -0
- package/src/wecom/plugin-constants.js +1 -1
- package/src/wecom/plugin-delivery-inbound-services.js +25 -0
- package/src/wecom/plugin-processing-deps.js +7 -0
- package/src/wecom/plugin-route-runtime-deps.js +1 -0
- package/src/wecom/plugin-services.js +87 -0
- package/src/wecom/policy-resolvers.js +93 -20
- package/src/wecom/quickstart-metadata.js +1247 -0
- package/src/wecom/reasoning-visibility.js +104 -0
- package/src/wecom/register-runtime.js +10 -0
- package/src/wecom/reliable-delivery-persistence.js +138 -0
- package/src/wecom/reliable-delivery.js +642 -0
- package/src/wecom/reply-output-policy.js +171 -0
- package/src/wecom/text-inbound-scheduler.js +6 -1
- package/src/wecom/workspace-auto-sender.js +2 -0
|
@@ -18,6 +18,14 @@ import {
|
|
|
18
18
|
webhookSendText,
|
|
19
19
|
} from "./webhook-bot.js";
|
|
20
20
|
import { stat } from "node:fs/promises";
|
|
21
|
+
import { inferWecomDeliveryStatus } from "./reliable-delivery.js";
|
|
22
|
+
import { applyWecomReasoningPolicy } from "./reasoning-visibility.js";
|
|
23
|
+
import {
|
|
24
|
+
extractWecomReplyDirectives,
|
|
25
|
+
mergeWecomReplyMediaItems,
|
|
26
|
+
resolveWecomReplyDirectiveMediaItems,
|
|
27
|
+
} from "./reply-output-policy.js";
|
|
28
|
+
import { parseThinkingContent } from "./thinking-parser.js";
|
|
21
29
|
|
|
22
30
|
function assertFunction(name, fn) {
|
|
23
31
|
if (typeof fn !== "function") {
|
|
@@ -30,6 +38,16 @@ export function createWecomBotReplyDeliverer({
|
|
|
30
38
|
resolveWecomDeliveryFallbackPolicy,
|
|
31
39
|
resolveWecomWebhookBotDeliveryPolicy,
|
|
32
40
|
resolveWecomObservabilityPolicy,
|
|
41
|
+
resolveWecomReasoningPolicy = () => ({
|
|
42
|
+
mode: "separate",
|
|
43
|
+
sendThinkingMessage: true,
|
|
44
|
+
includeInFinalAnswer: false,
|
|
45
|
+
title: "思考过程",
|
|
46
|
+
maxChars: 1200,
|
|
47
|
+
}),
|
|
48
|
+
resolveWecomReplyFormatPolicy = () => ({
|
|
49
|
+
mode: "auto",
|
|
50
|
+
}),
|
|
33
51
|
resolveWecomBotProxyConfig,
|
|
34
52
|
resolveWecomBotConfig,
|
|
35
53
|
resolveWecomBotLongConnectionReplyContext,
|
|
@@ -53,6 +71,8 @@ export function createWecomBotReplyDeliverer({
|
|
|
53
71
|
extractWorkspacePathsFromText = () => [],
|
|
54
72
|
resolveWorkspacePathToHost = () => "",
|
|
55
73
|
recordDeliveryMetric = () => {},
|
|
74
|
+
recordReliableDeliveryOutcome = () => {},
|
|
75
|
+
enqueuePendingReply = () => null,
|
|
56
76
|
statImpl = stat,
|
|
57
77
|
fetchImpl = fetch,
|
|
58
78
|
} = {}) {
|
|
@@ -60,6 +80,8 @@ export function createWecomBotReplyDeliverer({
|
|
|
60
80
|
assertFunction("resolveWecomDeliveryFallbackPolicy", resolveWecomDeliveryFallbackPolicy);
|
|
61
81
|
assertFunction("resolveWecomWebhookBotDeliveryPolicy", resolveWecomWebhookBotDeliveryPolicy);
|
|
62
82
|
assertFunction("resolveWecomObservabilityPolicy", resolveWecomObservabilityPolicy);
|
|
83
|
+
assertFunction("resolveWecomReasoningPolicy", resolveWecomReasoningPolicy);
|
|
84
|
+
assertFunction("resolveWecomReplyFormatPolicy", resolveWecomReplyFormatPolicy);
|
|
63
85
|
assertFunction("resolveWecomBotProxyConfig", resolveWecomBotProxyConfig);
|
|
64
86
|
assertFunction("resolveWecomBotConfig", resolveWecomBotConfig);
|
|
65
87
|
assertFunction("resolveWecomBotLongConnectionReplyContext", resolveWecomBotLongConnectionReplyContext);
|
|
@@ -83,6 +105,8 @@ export function createWecomBotReplyDeliverer({
|
|
|
83
105
|
assertFunction("extractWorkspacePathsFromText", extractWorkspacePathsFromText);
|
|
84
106
|
assertFunction("resolveWorkspacePathToHost", resolveWorkspacePathToHost);
|
|
85
107
|
assertFunction("recordDeliveryMetric", recordDeliveryMetric);
|
|
108
|
+
assertFunction("recordReliableDeliveryOutcome", recordReliableDeliveryOutcome);
|
|
109
|
+
assertFunction("enqueuePendingReply", enqueuePendingReply);
|
|
86
110
|
assertFunction("statImpl", statImpl);
|
|
87
111
|
|
|
88
112
|
const inlineImageExts = new Set([".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".heic", ".heif"]);
|
|
@@ -167,38 +191,92 @@ export function createWecomBotReplyDeliverer({
|
|
|
167
191
|
streamId,
|
|
168
192
|
responseUrl,
|
|
169
193
|
text,
|
|
194
|
+
rawText = "",
|
|
170
195
|
thinkingContent = "",
|
|
196
|
+
rawThinkingContent = "",
|
|
171
197
|
routeAgentId = "",
|
|
172
198
|
mediaUrl,
|
|
173
199
|
mediaUrls,
|
|
200
|
+
mediaItems,
|
|
174
201
|
mediaType,
|
|
175
202
|
reason = "reply",
|
|
203
|
+
allowPendingEnqueue = true,
|
|
176
204
|
} = {}) {
|
|
177
205
|
const normalizedAccountId = String(accountId ?? "default").trim().toLowerCase() || "default";
|
|
178
206
|
const fallbackPolicy = resolveWecomDeliveryFallbackPolicy(api);
|
|
179
207
|
const webhookBotPolicy = resolveWecomWebhookBotDeliveryPolicy(api);
|
|
180
208
|
const observabilityPolicy = resolveWecomObservabilityPolicy(api);
|
|
209
|
+
const reasoningPolicy = resolveWecomReasoningPolicy(api);
|
|
210
|
+
const replyFormatPolicy = resolveWecomReplyFormatPolicy(api);
|
|
181
211
|
const botProxyUrl = resolveWecomBotProxyConfig(api, normalizedAccountId);
|
|
182
212
|
const botModeConfig = resolveWecomBotConfig(api, normalizedAccountId);
|
|
183
213
|
const normalizedText = String(text ?? "").trim();
|
|
214
|
+
const normalizedRawText = String(rawText ?? normalizedText).trim();
|
|
215
|
+
const parsedText =
|
|
216
|
+
String(thinkingContent ?? "").trim().length > 0
|
|
217
|
+
? {
|
|
218
|
+
visibleContent: normalizedText,
|
|
219
|
+
thinkingContent: String(thinkingContent ?? "").trim(),
|
|
220
|
+
}
|
|
221
|
+
: parseThinkingContent(normalizedText);
|
|
222
|
+
const parsedRawText =
|
|
223
|
+
String(rawThinkingContent ?? "").trim().length > 0
|
|
224
|
+
? {
|
|
225
|
+
visibleContent: normalizedRawText,
|
|
226
|
+
thinkingContent: String(rawThinkingContent ?? "").trim(),
|
|
227
|
+
}
|
|
228
|
+
: parseThinkingContent(normalizedRawText);
|
|
229
|
+
const reasoningPayload = applyWecomReasoningPolicy({
|
|
230
|
+
text: parsedText.visibleContent,
|
|
231
|
+
thinkingContent: parsedText.thinkingContent,
|
|
232
|
+
policy: reasoningPolicy,
|
|
233
|
+
transport: "bot",
|
|
234
|
+
phase: "final",
|
|
235
|
+
});
|
|
236
|
+
const richReasoningPayload = applyWecomReasoningPolicy({
|
|
237
|
+
text: parsedRawText.visibleContent,
|
|
238
|
+
thinkingContent: parsedRawText.thinkingContent,
|
|
239
|
+
policy: reasoningPolicy,
|
|
240
|
+
transport: "bot",
|
|
241
|
+
phase: "final",
|
|
242
|
+
});
|
|
243
|
+
const plainDirectivePayload = extractWecomReplyDirectives(reasoningPayload.text);
|
|
244
|
+
const richDirectivePayload = extractWecomReplyDirectives(richReasoningPayload.text);
|
|
245
|
+
const effectiveText = String(plainDirectivePayload.text ?? "").trim();
|
|
246
|
+
const richEffectiveText = String(richDirectivePayload.text ?? "").trim();
|
|
247
|
+
const effectiveThinkingContent = String(reasoningPayload.thinkingContent ?? "").trim();
|
|
248
|
+
const directiveMediaItems = resolveWecomReplyDirectiveMediaItems({
|
|
249
|
+
mediaItems: richDirectivePayload.mediaItems,
|
|
250
|
+
routeAgentId,
|
|
251
|
+
resolveWorkspacePathToHost,
|
|
252
|
+
});
|
|
184
253
|
const inlineWorkspaceMediaUrls = await collectInlineWorkspaceImageMediaUrls({
|
|
185
|
-
text:
|
|
254
|
+
text: richEffectiveText || effectiveText,
|
|
186
255
|
routeAgentId,
|
|
187
256
|
});
|
|
188
|
-
const
|
|
257
|
+
const normalizedMediaItems = mergeWecomReplyMediaItems({
|
|
189
258
|
mediaUrl,
|
|
190
259
|
mediaUrls: [...(Array.isArray(mediaUrls) ? mediaUrls : []), ...inlineWorkspaceMediaUrls],
|
|
260
|
+
mediaItems,
|
|
261
|
+
mediaType,
|
|
262
|
+
extraMediaItems: directiveMediaItems,
|
|
263
|
+
});
|
|
264
|
+
const normalizedMediaUrls = normalizeWecomBotOutboundMediaUrls({
|
|
265
|
+
mediaUrls: normalizedMediaItems.map((item) => item.url),
|
|
191
266
|
});
|
|
192
267
|
const mixedPayload =
|
|
193
268
|
normalizedMediaUrls.length > 0
|
|
194
269
|
? buildWecomBotMixedPayload({
|
|
195
|
-
text:
|
|
270
|
+
text: effectiveText,
|
|
196
271
|
mediaUrls: normalizedMediaUrls,
|
|
197
272
|
})
|
|
198
273
|
: null;
|
|
199
|
-
const fallbackText =
|
|
274
|
+
const fallbackText = effectiveText || "已收到模型返回的媒体结果,请查看以下链接。";
|
|
200
275
|
const cardPayload = buildWecomBotCardPayload({
|
|
201
|
-
text:
|
|
276
|
+
text:
|
|
277
|
+
String(replyFormatPolicy?.mode ?? "").trim().toLowerCase() === "markdown" && richEffectiveText
|
|
278
|
+
? richEffectiveText
|
|
279
|
+
: effectiveText || fallbackText,
|
|
202
280
|
cardPolicy: botModeConfig?.card,
|
|
203
281
|
hasMedia: normalizedMediaUrls.length > 0,
|
|
204
282
|
});
|
|
@@ -231,6 +309,7 @@ export function createWecomBotReplyDeliverer({
|
|
|
231
309
|
if (normalizedMediaUrls.length > 0) {
|
|
232
310
|
const processed = await buildActiveStreamMsgItems({
|
|
233
311
|
mediaUrls: normalizedMediaUrls,
|
|
312
|
+
mediaItems: normalizedMediaItems,
|
|
234
313
|
mediaType,
|
|
235
314
|
fetchMediaFromUrl,
|
|
236
315
|
proxyUrl: botProxyUrl,
|
|
@@ -246,7 +325,7 @@ export function createWecomBotReplyDeliverer({
|
|
|
246
325
|
if (fallbackMediaUrls.length > 0) {
|
|
247
326
|
streamContent = `${streamContent}\n\n媒体链接:\n${fallbackMediaUrls.join("\n")}`.trim();
|
|
248
327
|
}
|
|
249
|
-
if (!streamContent && !streamMsgItem.length && !
|
|
328
|
+
if (!streamContent && !streamMsgItem.length && !effectiveThinkingContent) {
|
|
250
329
|
streamContent = fallbackText;
|
|
251
330
|
}
|
|
252
331
|
return pushWecomBotLongConnectionStreamUpdate({
|
|
@@ -256,7 +335,7 @@ export function createWecomBotReplyDeliverer({
|
|
|
256
335
|
content: streamContent,
|
|
257
336
|
finish: true,
|
|
258
337
|
msgItem: streamMsgItem,
|
|
259
|
-
thinkingContent,
|
|
338
|
+
thinkingContent: effectiveThinkingContent,
|
|
260
339
|
});
|
|
261
340
|
},
|
|
262
341
|
active_stream: async ({ text: content }) => {
|
|
@@ -264,10 +343,10 @@ export function createWecomBotReplyDeliverer({
|
|
|
264
343
|
streamId,
|
|
265
344
|
sessionId: normalizedSessionId,
|
|
266
345
|
content,
|
|
267
|
-
thinkingContent,
|
|
346
|
+
thinkingContent: effectiveThinkingContent,
|
|
268
347
|
normalizedMediaUrls,
|
|
269
348
|
mediaType,
|
|
270
|
-
normalizedText,
|
|
349
|
+
normalizedText: effectiveText,
|
|
271
350
|
fallbackText,
|
|
272
351
|
botProxyUrl,
|
|
273
352
|
logger: api.logger,
|
|
@@ -296,9 +375,13 @@ export function createWecomBotReplyDeliverer({
|
|
|
296
375
|
webhookBotPolicy,
|
|
297
376
|
botProxyUrl,
|
|
298
377
|
content,
|
|
378
|
+
richContent:
|
|
379
|
+
String(replyFormatPolicy?.mode ?? "").trim().toLowerCase() === "markdown" ? richEffectiveText : "",
|
|
380
|
+
replyFormatMode: replyFormatPolicy?.mode || "auto",
|
|
299
381
|
fallbackText,
|
|
300
|
-
normalizedText,
|
|
382
|
+
normalizedText: effectiveText,
|
|
301
383
|
normalizedMediaUrls,
|
|
384
|
+
normalizedMediaItems,
|
|
302
385
|
mediaType,
|
|
303
386
|
cardPayload,
|
|
304
387
|
cardPolicy: botModeConfig?.card ?? {},
|
|
@@ -318,7 +401,7 @@ export function createWecomBotReplyDeliverer({
|
|
|
318
401
|
});
|
|
319
402
|
|
|
320
403
|
const deliveryResult = await router.deliverText({
|
|
321
|
-
text:
|
|
404
|
+
text: effectiveText || fallbackText,
|
|
322
405
|
traceId,
|
|
323
406
|
meta: {
|
|
324
407
|
reason,
|
|
@@ -328,7 +411,7 @@ export function createWecomBotReplyDeliverer({
|
|
|
328
411
|
streamId: streamId || "",
|
|
329
412
|
hasResponseUrl: Boolean(inlineResponseUrl || cachedResponseUrl?.url),
|
|
330
413
|
mediaCount: normalizedMediaUrls.length,
|
|
331
|
-
hasThinkingContent: Boolean(
|
|
414
|
+
hasThinkingContent: Boolean(effectiveThinkingContent),
|
|
332
415
|
botCardMode: botModeConfig?.card?.enabled ? botModeConfig.card.mode : "off",
|
|
333
416
|
},
|
|
334
417
|
});
|
|
@@ -336,9 +419,43 @@ export function createWecomBotReplyDeliverer({
|
|
|
336
419
|
layer: deliveryResult?.layer || "",
|
|
337
420
|
ok: deliveryResult?.ok === true,
|
|
338
421
|
finalStatus: deliveryResult?.finalStatus || "",
|
|
422
|
+
deliveryStatus: deliveryResult?.deliveryStatus || "",
|
|
339
423
|
accountId: normalizedAccountId,
|
|
340
424
|
attempts: deliveryResult?.attempts,
|
|
341
425
|
});
|
|
426
|
+
recordReliableDeliveryOutcome({
|
|
427
|
+
mode: "bot",
|
|
428
|
+
accountId: normalizedAccountId,
|
|
429
|
+
sessionId: normalizedSessionId,
|
|
430
|
+
fromUser,
|
|
431
|
+
deliveryStatus:
|
|
432
|
+
deliveryResult?.deliveryStatus ||
|
|
433
|
+
(deliveryResult?.ok === true
|
|
434
|
+
? "delivered"
|
|
435
|
+
: inferWecomDeliveryStatus({
|
|
436
|
+
reason: deliveryResult?.error || deliveryResult?.attempts?.slice?.(-1)?.[0]?.reason || "delivery-failed",
|
|
437
|
+
layer: deliveryResult?.layer || deliveryResult?.attempts?.slice?.(-1)?.[0]?.layer || "",
|
|
438
|
+
})),
|
|
439
|
+
layer: deliveryResult?.layer || deliveryResult?.attempts?.slice?.(-1)?.[0]?.layer || "",
|
|
440
|
+
reason: deliveryResult?.error || deliveryResult?.attempts?.slice?.(-1)?.[0]?.reason || reason,
|
|
441
|
+
});
|
|
442
|
+
if (deliveryResult?.ok !== true && allowPendingEnqueue) {
|
|
443
|
+
enqueuePendingReply(api, {
|
|
444
|
+
mode: "bot",
|
|
445
|
+
accountId: normalizedAccountId,
|
|
446
|
+
sessionId: normalizedSessionId,
|
|
447
|
+
fromUser,
|
|
448
|
+
payload: {
|
|
449
|
+
text: effectiveText || fallbackText,
|
|
450
|
+
thinkingContent: effectiveThinkingContent,
|
|
451
|
+
mediaUrls: normalizedMediaUrls,
|
|
452
|
+
mediaItems: normalizedMediaItems,
|
|
453
|
+
mediaType,
|
|
454
|
+
},
|
|
455
|
+
reason: deliveryResult?.error || deliveryResult?.attempts?.slice?.(-1)?.[0]?.reason || reason,
|
|
456
|
+
deliveryStatus: deliveryResult?.deliveryStatus || "rejected_unknown",
|
|
457
|
+
});
|
|
458
|
+
}
|
|
342
459
|
return deliveryResult;
|
|
343
460
|
}
|
|
344
461
|
|
|
@@ -16,6 +16,7 @@ function isSupportedActiveStreamMsgItemImage(buffer) {
|
|
|
16
16
|
|
|
17
17
|
export async function buildActiveStreamMsgItems({
|
|
18
18
|
mediaUrls,
|
|
19
|
+
mediaItems,
|
|
19
20
|
mediaType,
|
|
20
21
|
fetchMediaFromUrl,
|
|
21
22
|
proxyUrl,
|
|
@@ -23,13 +24,35 @@ export async function buildActiveStreamMsgItems({
|
|
|
23
24
|
}) {
|
|
24
25
|
const msgItem = [];
|
|
25
26
|
const fallbackUrls = [];
|
|
27
|
+
const rawItems = [
|
|
28
|
+
...(Array.isArray(mediaUrls) ? mediaUrls : []).map((url) => ({
|
|
29
|
+
url,
|
|
30
|
+
mediaType,
|
|
31
|
+
})),
|
|
32
|
+
...(Array.isArray(mediaItems) ? mediaItems : []),
|
|
33
|
+
];
|
|
34
|
+
const dedupe = new Set();
|
|
35
|
+
const candidates = [];
|
|
36
|
+
for (const item of rawItems) {
|
|
37
|
+
const normalizedUrl = String(item?.url ?? "").trim();
|
|
38
|
+
const normalizedType = String(item?.mediaType ?? "").trim().toLowerCase() || undefined;
|
|
39
|
+
if (!normalizedUrl) continue;
|
|
40
|
+
const dedupeKey = `${normalizedType || ""}:${normalizedUrl}`;
|
|
41
|
+
if (dedupe.has(dedupeKey)) continue;
|
|
42
|
+
dedupe.add(dedupeKey);
|
|
43
|
+
candidates.push({
|
|
44
|
+
url: normalizedUrl,
|
|
45
|
+
mediaType: normalizedType,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
26
48
|
|
|
27
|
-
for (const
|
|
49
|
+
for (const mediaItem of candidates) {
|
|
50
|
+
const mediaUrl = mediaItem.url;
|
|
28
51
|
if (msgItem.length >= ACTIVE_STREAM_MSG_ITEM_LIMIT) {
|
|
29
52
|
fallbackUrls.push(mediaUrl);
|
|
30
53
|
continue;
|
|
31
54
|
}
|
|
32
|
-
const target = resolveWecomOutboundMediaTarget({ mediaUrl, mediaType });
|
|
55
|
+
const target = resolveWecomOutboundMediaTarget({ mediaUrl, mediaType: mediaItem.mediaType ?? mediaType });
|
|
33
56
|
if (target.type !== "image") {
|
|
34
57
|
fallbackUrls.push(mediaUrl);
|
|
35
58
|
continue;
|
|
@@ -26,9 +26,12 @@ export function createWecomWebhookBotDeliverer({
|
|
|
26
26
|
webhookBotPolicy,
|
|
27
27
|
botProxyUrl = "",
|
|
28
28
|
content = "",
|
|
29
|
+
richContent = "",
|
|
30
|
+
replyFormatMode = "auto",
|
|
29
31
|
fallbackText = "",
|
|
30
32
|
normalizedText = "",
|
|
31
33
|
normalizedMediaUrls = [],
|
|
34
|
+
normalizedMediaItems = [],
|
|
32
35
|
mediaType,
|
|
33
36
|
cardPayload = null,
|
|
34
37
|
cardPolicy = {},
|
|
@@ -46,6 +49,7 @@ export function createWecomWebhookBotDeliverer({
|
|
|
46
49
|
|
|
47
50
|
const dispatcher = attachWecomProxyDispatcher(sendUrl, {}, { proxyUrl: botProxyUrl, logger: api?.logger })?.dispatcher;
|
|
48
51
|
const textPayload = `${content || fallbackText}`.trim();
|
|
52
|
+
const markdownPayload = String(richContent || content || fallbackText).trim();
|
|
49
53
|
let sentAny = false;
|
|
50
54
|
let usedCardMode = "";
|
|
51
55
|
|
|
@@ -82,6 +86,20 @@ export function createWecomWebhookBotDeliverer({
|
|
|
82
86
|
}
|
|
83
87
|
}
|
|
84
88
|
|
|
89
|
+
const preferMarkdown = String(replyFormatMode ?? "").trim().toLowerCase() === "markdown";
|
|
90
|
+
if (!sentAny && preferMarkdown && markdownPayload && normalizedMediaUrls.length === 0) {
|
|
91
|
+
await webhookSendMarkdown({
|
|
92
|
+
url: webhookBotPolicy?.url,
|
|
93
|
+
key: webhookBotPolicy?.key,
|
|
94
|
+
content: markdownPayload,
|
|
95
|
+
timeoutMs: webhookBotPolicy?.timeoutMs,
|
|
96
|
+
dispatcher,
|
|
97
|
+
fetchImpl,
|
|
98
|
+
});
|
|
99
|
+
sentAny = true;
|
|
100
|
+
usedCardMode = "markdown";
|
|
101
|
+
}
|
|
102
|
+
|
|
85
103
|
if (!sentAny && textPayload && (normalizedText || normalizedMediaUrls.length === 0)) {
|
|
86
104
|
await webhookSendText({
|
|
87
105
|
url: webhookBotPolicy?.url,
|
|
@@ -101,6 +119,7 @@ export function createWecomWebhookBotDeliverer({
|
|
|
101
119
|
webhookBotPolicy,
|
|
102
120
|
proxyUrl: botProxyUrl,
|
|
103
121
|
mediaUrls: normalizedMediaUrls,
|
|
122
|
+
mediaItems: normalizedMediaItems,
|
|
104
123
|
mediaType,
|
|
105
124
|
});
|
|
106
125
|
sentAny = sentAny || mediaMeta.sentCount > 0;
|
|
@@ -32,6 +32,7 @@ export function createWecomWebhookBotMediaSender({
|
|
|
32
32
|
webhookBotPolicy,
|
|
33
33
|
proxyUrl,
|
|
34
34
|
mediaUrls,
|
|
35
|
+
mediaItems,
|
|
35
36
|
mediaType,
|
|
36
37
|
}) {
|
|
37
38
|
const sendUrl = resolveWebhookBotSendUrl({
|
|
@@ -51,12 +52,35 @@ export function createWecomWebhookBotMediaSender({
|
|
|
51
52
|
const dispatcher = resolveWebhookDispatcher(attachWecomProxyDispatcher, sendUrl, proxyUrl, logger);
|
|
52
53
|
let sentCount = 0;
|
|
53
54
|
const failedUrls = [];
|
|
54
|
-
const
|
|
55
|
+
const rawItems = [
|
|
56
|
+
...(Array.isArray(mediaUrls) ? mediaUrls : []).map((url) => ({
|
|
57
|
+
url,
|
|
58
|
+
mediaType,
|
|
59
|
+
})),
|
|
60
|
+
...(Array.isArray(mediaItems) ? mediaItems : []),
|
|
61
|
+
];
|
|
62
|
+
const dedupe = new Set();
|
|
63
|
+
const candidateMediaItems = [];
|
|
64
|
+
for (const item of rawItems) {
|
|
65
|
+
const normalizedUrl = String(item?.url ?? "").trim();
|
|
66
|
+
const normalizedType = String(item?.mediaType ?? "").trim().toLowerCase() || undefined;
|
|
67
|
+
if (!normalizedUrl) continue;
|
|
68
|
+
const dedupeKey = `${normalizedType || ""}:${normalizedUrl}`;
|
|
69
|
+
if (dedupe.has(dedupeKey)) continue;
|
|
70
|
+
dedupe.add(dedupeKey);
|
|
71
|
+
candidateMediaItems.push({
|
|
72
|
+
url: normalizedUrl,
|
|
73
|
+
mediaType: normalizedType,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
55
76
|
|
|
56
|
-
for (const
|
|
57
|
-
const target = resolveWecomOutboundMediaTarget({
|
|
77
|
+
for (const mediaItem of candidateMediaItems) {
|
|
78
|
+
const target = resolveWecomOutboundMediaTarget({
|
|
79
|
+
mediaUrl: mediaItem.url,
|
|
80
|
+
mediaType: mediaItem.mediaType ?? mediaType,
|
|
81
|
+
});
|
|
58
82
|
try {
|
|
59
|
-
const { buffer } = await fetchMediaFromUrl(
|
|
83
|
+
const { buffer } = await fetchMediaFromUrl(mediaItem.url, {
|
|
60
84
|
proxyUrl,
|
|
61
85
|
logger,
|
|
62
86
|
forceProxy: Boolean(proxyUrl),
|
|
@@ -91,9 +115,9 @@ export function createWecomWebhookBotMediaSender({
|
|
|
91
115
|
}
|
|
92
116
|
sentCount += 1;
|
|
93
117
|
} catch (err) {
|
|
94
|
-
failedUrls.push(
|
|
118
|
+
failedUrls.push(mediaItem.url);
|
|
95
119
|
logger?.warn?.(
|
|
96
|
-
`wecom(bot): webhook media send failed target=${
|
|
120
|
+
`wecom(bot): webhook media send failed target=${mediaItem.url} type=${target.type} reason=${String(err?.message || err)}`,
|
|
97
121
|
);
|
|
98
122
|
}
|
|
99
123
|
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
function assertFunction(name, value) {
|
|
2
|
+
if (typeof value !== "function") {
|
|
3
|
+
throw new Error(`createWecomPendingReplyManager: ${name} is required`);
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function createWecomPendingReplyManager({
|
|
8
|
+
reliableDeliveryStore,
|
|
9
|
+
resolveWecomPendingReplyPolicy,
|
|
10
|
+
deliverPendingReply,
|
|
11
|
+
ensurePersistenceLoaded = async () => true,
|
|
12
|
+
schedulePersistenceFlush = () => {},
|
|
13
|
+
logger,
|
|
14
|
+
now = () => Date.now(),
|
|
15
|
+
sweepIntervalMs = 15000,
|
|
16
|
+
} = {}) {
|
|
17
|
+
if (!reliableDeliveryStore || typeof reliableDeliveryStore !== "object") {
|
|
18
|
+
throw new Error("createWecomPendingReplyManager: reliableDeliveryStore is required");
|
|
19
|
+
}
|
|
20
|
+
assertFunction("resolveWecomPendingReplyPolicy", resolveWecomPendingReplyPolicy);
|
|
21
|
+
assertFunction("deliverPendingReply", deliverPendingReply);
|
|
22
|
+
assertFunction("ensurePersistenceLoaded", ensurePersistenceLoaded);
|
|
23
|
+
assertFunction("schedulePersistenceFlush", schedulePersistenceFlush);
|
|
24
|
+
assertFunction("now", now);
|
|
25
|
+
|
|
26
|
+
let sweepTimer = null;
|
|
27
|
+
let sweepPromise = Promise.resolve();
|
|
28
|
+
|
|
29
|
+
function ensureSweepTimer() {
|
|
30
|
+
if (sweepTimer) return;
|
|
31
|
+
sweepTimer = setInterval(() => {
|
|
32
|
+
void flushDuePendingReplies("timer");
|
|
33
|
+
}, Math.max(5000, Number(sweepIntervalMs) || 15000));
|
|
34
|
+
sweepTimer.unref?.();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function initialize(api) {
|
|
38
|
+
await ensurePersistenceLoaded(api);
|
|
39
|
+
if (reliableDeliveryStore.countPendingReplies() > 0) {
|
|
40
|
+
ensureSweepTimer();
|
|
41
|
+
}
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function enqueuePendingReply(api, payload = {}) {
|
|
46
|
+
const policy = resolveWecomPendingReplyPolicy(api);
|
|
47
|
+
if (policy?.enabled !== true) return null;
|
|
48
|
+
const entry = reliableDeliveryStore.enqueuePendingReply({
|
|
49
|
+
...payload,
|
|
50
|
+
maxRetries: policy.maxRetries,
|
|
51
|
+
retryBackoffMs: policy.retryBackoffMs,
|
|
52
|
+
expireMs: policy.expireMs,
|
|
53
|
+
});
|
|
54
|
+
if (entry) {
|
|
55
|
+
ensureSweepTimer();
|
|
56
|
+
schedulePersistenceFlush("pending-enqueue", api);
|
|
57
|
+
}
|
|
58
|
+
return entry;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function attemptPendingEntry(entry, trigger = "timer") {
|
|
62
|
+
try {
|
|
63
|
+
const result = await deliverPendingReply(entry, trigger);
|
|
64
|
+
if (result?.ok === true) {
|
|
65
|
+
reliableDeliveryStore.markPendingDelivered({ id: entry.id, at: now() });
|
|
66
|
+
schedulePersistenceFlush("pending-delivered");
|
|
67
|
+
return { delivered: true, result };
|
|
68
|
+
}
|
|
69
|
+
const reason = String(result?.deliveryStatus || result?.finalStatus || result?.error || "pending-retry-failed");
|
|
70
|
+
return {
|
|
71
|
+
delivered: false,
|
|
72
|
+
result,
|
|
73
|
+
rescheduled: reliableDeliveryStore.reschedulePendingReply({
|
|
74
|
+
id: entry.id,
|
|
75
|
+
reason,
|
|
76
|
+
at: now(),
|
|
77
|
+
}),
|
|
78
|
+
};
|
|
79
|
+
} catch (err) {
|
|
80
|
+
const reason = String(err?.message || err || "pending-retry-failed");
|
|
81
|
+
logger?.warn?.(`wecom: pending reply retry failed id=${entry.id} trigger=${trigger} reason=${reason}`);
|
|
82
|
+
return {
|
|
83
|
+
delivered: false,
|
|
84
|
+
error: reason,
|
|
85
|
+
rescheduled: reliableDeliveryStore.reschedulePendingReply({
|
|
86
|
+
id: entry.id,
|
|
87
|
+
reason,
|
|
88
|
+
at: now(),
|
|
89
|
+
}),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function flushEntries(entries = [], trigger = "timer") {
|
|
95
|
+
const list = Array.isArray(entries) ? entries : [];
|
|
96
|
+
for (const entry of list) {
|
|
97
|
+
// Keep retries ordered and deterministic.
|
|
98
|
+
// eslint-disable-next-line no-await-in-loop
|
|
99
|
+
await attemptPendingEntry(entry, trigger);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function flushDuePendingReplies(trigger = "timer") {
|
|
104
|
+
sweepPromise = sweepPromise
|
|
105
|
+
.then(async () => {
|
|
106
|
+
await initialize();
|
|
107
|
+
reliableDeliveryStore.dropExpiredPendingReplies({ at: now() });
|
|
108
|
+
const entries = reliableDeliveryStore.listDuePendingReplies({ at: now(), limit: 20 });
|
|
109
|
+
await flushEntries(entries, trigger);
|
|
110
|
+
schedulePersistenceFlush(`pending-flush:${trigger}`);
|
|
111
|
+
})
|
|
112
|
+
.catch((err) => {
|
|
113
|
+
logger?.warn?.(`wecom: pending reply sweep failed: ${String(err?.message || err)}`);
|
|
114
|
+
});
|
|
115
|
+
return sweepPromise;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function flushSessionPendingReplies({ mode = "agent", accountId = "default", sessionId = "" } = {}) {
|
|
119
|
+
return ensurePersistenceLoaded()
|
|
120
|
+
.then(() =>
|
|
121
|
+
reliableDeliveryStore.listPendingRepliesForSession({
|
|
122
|
+
mode,
|
|
123
|
+
accountId,
|
|
124
|
+
sessionId,
|
|
125
|
+
}),
|
|
126
|
+
)
|
|
127
|
+
.then((entries) => {
|
|
128
|
+
if (entries.length === 0) return null;
|
|
129
|
+
return flushEntries(entries, "session-inbound");
|
|
130
|
+
})
|
|
131
|
+
.then(() => {
|
|
132
|
+
schedulePersistenceFlush("pending-session-flush");
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
initialize,
|
|
138
|
+
enqueuePendingReply,
|
|
139
|
+
flushDuePendingReplies,
|
|
140
|
+
flushSessionPendingReplies,
|
|
141
|
+
ensureSweepTimer,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
@@ -6,6 +6,7 @@ import { createWecomCommandHandlers } from "./command-handlers.js";
|
|
|
6
6
|
import { createWecomPolicyResolvers } from "./policy-resolvers.js";
|
|
7
7
|
import { createWecomVoiceTranscriber } from "./voice-transcription.js";
|
|
8
8
|
import {
|
|
9
|
+
buildWecomSessionId,
|
|
9
10
|
isLocalVoiceInputTypeDirectlySupported,
|
|
10
11
|
normalizeAudioContentType,
|
|
11
12
|
normalizeWecomWebhookTargetMap,
|
|
@@ -19,6 +20,10 @@ import {
|
|
|
19
20
|
resolveWecomDmPolicyConfig,
|
|
20
21
|
resolveWecomEventPolicyConfig,
|
|
21
22
|
resolveWecomDeliveryFallbackConfig,
|
|
23
|
+
resolveWecomPendingReplyConfig,
|
|
24
|
+
resolveWecomQuotaTrackingConfig,
|
|
25
|
+
resolveWecomReasoningConfig,
|
|
26
|
+
resolveWecomReplyFormatConfig,
|
|
22
27
|
resolveWecomDynamicAgentConfig,
|
|
23
28
|
resolveWecomGroupChatConfig,
|
|
24
29
|
resolveWecomObservabilityConfig,
|
|
@@ -27,11 +32,13 @@ import {
|
|
|
27
32
|
resolveWecomStreamingConfig,
|
|
28
33
|
resolveWecomWebhookBotDeliveryConfig,
|
|
29
34
|
} from "../core.js";
|
|
35
|
+
import { buildWecomBotSessionId } from "./runtime-utils.js";
|
|
30
36
|
|
|
31
37
|
export function createWecomPluginAccountPolicyServices({
|
|
32
38
|
processEnv = process.env,
|
|
33
39
|
getGatewayRuntime,
|
|
34
40
|
getWecomObservabilityMetrics = () => ({}),
|
|
41
|
+
getWecomReliableDeliverySnapshot = () => null,
|
|
35
42
|
normalizeWecomResolvedTarget,
|
|
36
43
|
formatWecomTargetForLog,
|
|
37
44
|
sendWecomWebhookText,
|
|
@@ -82,6 +89,10 @@ export function createWecomPluginAccountPolicyServices({
|
|
|
82
89
|
resolveWecomTextDebouncePolicy,
|
|
83
90
|
resolveWecomReplyStreamingPolicy,
|
|
84
91
|
resolveWecomDeliveryFallbackPolicy,
|
|
92
|
+
resolveWecomPendingReplyPolicy,
|
|
93
|
+
resolveWecomQuotaTrackingPolicy,
|
|
94
|
+
resolveWecomReasoningPolicy,
|
|
95
|
+
resolveWecomReplyFormatPolicy,
|
|
85
96
|
resolveWecomWebhookBotDeliveryPolicy,
|
|
86
97
|
resolveWecomStreamManagerPolicy,
|
|
87
98
|
resolveWecomObservabilityPolicy,
|
|
@@ -100,6 +111,10 @@ export function createWecomPluginAccountPolicyServices({
|
|
|
100
111
|
resolveWecomDebounceConfig,
|
|
101
112
|
resolveWecomStreamingConfig,
|
|
102
113
|
resolveWecomDeliveryFallbackConfig,
|
|
114
|
+
resolveWecomPendingReplyConfig,
|
|
115
|
+
resolveWecomQuotaTrackingConfig,
|
|
116
|
+
resolveWecomReasoningConfig,
|
|
117
|
+
resolveWecomReplyFormatConfig,
|
|
103
118
|
resolveWecomWebhookBotDeliveryConfig,
|
|
104
119
|
resolveWecomStreamManagerConfig,
|
|
105
120
|
resolveWecomObservabilityConfig,
|
|
@@ -134,10 +149,17 @@ export function createWecomPluginAccountPolicyServices({
|
|
|
134
149
|
resolveWecomTextDebouncePolicy,
|
|
135
150
|
resolveWecomReplyStreamingPolicy,
|
|
136
151
|
resolveWecomDeliveryFallbackPolicy,
|
|
152
|
+
resolveWecomPendingReplyPolicy,
|
|
153
|
+
resolveWecomQuotaTrackingPolicy,
|
|
154
|
+
resolveWecomReasoningPolicy,
|
|
155
|
+
resolveWecomReplyFormatPolicy,
|
|
137
156
|
resolveWecomStreamManagerPolicy,
|
|
138
157
|
resolveWecomWebhookBotDeliveryPolicy,
|
|
139
158
|
resolveWecomDynamicAgentPolicy,
|
|
140
159
|
resolveWecomBotConfig,
|
|
160
|
+
buildWecomSessionId,
|
|
161
|
+
buildWecomBotSessionId,
|
|
162
|
+
getWecomReliableDeliverySnapshot,
|
|
141
163
|
getWecomObservabilityMetrics,
|
|
142
164
|
pluginVersion: PLUGIN_VERSION,
|
|
143
165
|
});
|
|
@@ -162,6 +184,10 @@ export function createWecomPluginAccountPolicyServices({
|
|
|
162
184
|
resolveWecomTextDebouncePolicy,
|
|
163
185
|
resolveWecomReplyStreamingPolicy,
|
|
164
186
|
resolveWecomDeliveryFallbackPolicy,
|
|
187
|
+
resolveWecomPendingReplyPolicy,
|
|
188
|
+
resolveWecomQuotaTrackingPolicy,
|
|
189
|
+
resolveWecomReasoningPolicy,
|
|
190
|
+
resolveWecomReplyFormatPolicy,
|
|
165
191
|
resolveWecomWebhookBotDeliveryPolicy,
|
|
166
192
|
resolveWecomStreamManagerPolicy,
|
|
167
193
|
resolveWecomObservabilityPolicy,
|