@dingxiang-me/openclaw-wechat 2.0.1 → 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.
Files changed (79) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/README.en.md +204 -32
  3. package/README.md +234 -63
  4. package/docs/channels/wecom.md +137 -1
  5. package/openclaw.plugin.json +694 -10
  6. package/package.json +207 -4
  7. package/scripts/wecom-agent-selfcheck.mjs +775 -0
  8. package/scripts/wecom-bot-longconn-probe.mjs +582 -0
  9. package/scripts/wecom-bot-selfcheck.mjs +952 -0
  10. package/scripts/wecom-callback-matrix.mjs +224 -0
  11. package/scripts/wecom-doctor.mjs +1407 -0
  12. package/scripts/wecom-e2e-scenario.mjs +333 -0
  13. package/scripts/wecom-migrate.mjs +261 -0
  14. package/scripts/wecom-quickstart.mjs +1824 -0
  15. package/scripts/wecom-release-check.mjs +232 -0
  16. package/scripts/wecom-remote-e2e.mjs +310 -0
  17. package/scripts/wecom-selfcheck.mjs +1255 -0
  18. package/scripts/wecom-smoke.sh +74 -0
  19. package/src/core/delivery-router.js +21 -0
  20. package/src/core.js +631 -34
  21. package/src/wecom/account-config-core.js +27 -1
  22. package/src/wecom/account-config.js +19 -2
  23. package/src/wecom/agent-dispatch-executor.js +11 -0
  24. package/src/wecom/agent-dispatch-handlers.js +61 -8
  25. package/src/wecom/agent-inbound-guards.js +63 -16
  26. package/src/wecom/agent-inbound-processor.js +34 -2
  27. package/src/wecom/agent-late-reply-runtime.js +30 -2
  28. package/src/wecom/agent-text-sender.js +2 -0
  29. package/src/wecom/api-client-core.js +27 -19
  30. package/src/wecom/api-client-media.js +16 -7
  31. package/src/wecom/api-client-send-text.js +4 -0
  32. package/src/wecom/api-client-send-typed.js +4 -1
  33. package/src/wecom/api-client-senders.js +41 -3
  34. package/src/wecom/api-client.js +1 -0
  35. package/src/wecom/bot-dispatch-fallback.js +18 -3
  36. package/src/wecom/bot-dispatch-handlers.js +47 -10
  37. package/src/wecom/bot-inbound-dispatch-runtime.js +3 -0
  38. package/src/wecom/bot-inbound-executor-helpers.js +11 -1
  39. package/src/wecom/bot-inbound-executor.js +25 -1
  40. package/src/wecom/bot-inbound-guards.js +78 -23
  41. package/src/wecom/bot-long-connection-manager.js +4 -4
  42. package/src/wecom/channel-config-schema.js +132 -0
  43. package/src/wecom/channel-plugin.js +370 -7
  44. package/src/wecom/command-handlers.js +107 -10
  45. package/src/wecom/command-status-text.js +275 -1
  46. package/src/wecom/doc-client.js +7 -1
  47. package/src/wecom/inbound-content-handler-file-video-link.js +4 -0
  48. package/src/wecom/inbound-content-handler-image-voice.js +6 -0
  49. package/src/wecom/inbound-content.js +5 -0
  50. package/src/wecom/installer-api.js +910 -0
  51. package/src/wecom/media-download.js +2 -2
  52. package/src/wecom/migration-diagnostics.js +816 -0
  53. package/src/wecom/network-config.js +91 -0
  54. package/src/wecom/observability-metrics.js +9 -3
  55. package/src/wecom/outbound-agent-delivery.js +313 -0
  56. package/src/wecom/outbound-agent-media-sender.js +37 -7
  57. package/src/wecom/outbound-agent-push.js +1 -0
  58. package/src/wecom/outbound-delivery.js +129 -12
  59. package/src/wecom/outbound-stream-msg-item.js +25 -2
  60. package/src/wecom/outbound-webhook-delivery.js +19 -0
  61. package/src/wecom/outbound-webhook-media.js +30 -6
  62. package/src/wecom/pairing.js +188 -0
  63. package/src/wecom/pending-reply-manager.js +143 -0
  64. package/src/wecom/plugin-account-policy-services.js +26 -0
  65. package/src/wecom/plugin-base-services.js +58 -0
  66. package/src/wecom/plugin-constants.js +1 -1
  67. package/src/wecom/plugin-delivery-inbound-services.js +25 -0
  68. package/src/wecom/plugin-processing-deps.js +7 -0
  69. package/src/wecom/plugin-route-runtime-deps.js +1 -0
  70. package/src/wecom/plugin-services.js +87 -0
  71. package/src/wecom/policy-resolvers.js +93 -20
  72. package/src/wecom/quickstart-metadata.js +1247 -0
  73. package/src/wecom/reasoning-visibility.js +104 -0
  74. package/src/wecom/register-runtime.js +10 -0
  75. package/src/wecom/reliable-delivery-persistence.js +138 -0
  76. package/src/wecom/reliable-delivery.js +642 -0
  77. package/src/wecom/reply-output-policy.js +171 -0
  78. package/src/wecom/text-inbound-scheduler.js +6 -1
  79. package/src/wecom/workspace-auto-sender.js +2 -0
@@ -0,0 +1,91 @@
1
+ export const DEFAULT_WECOM_API_BASE_URL = "https://qyapi.weixin.qq.com";
2
+
3
+ function pickFirstNonEmptyString(...values) {
4
+ for (const value of values) {
5
+ const trimmed = String(value ?? "").trim();
6
+ if (trimmed) return trimmed;
7
+ }
8
+ return "";
9
+ }
10
+
11
+ function normalizeAccountId(accountId) {
12
+ const normalized = String(accountId ?? "default").trim().toLowerCase();
13
+ return normalized || "default";
14
+ }
15
+
16
+ function normalizeHttpBaseUrl(value, fallback = DEFAULT_WECOM_API_BASE_URL) {
17
+ const raw = String(value ?? "").trim() || String(fallback ?? "").trim() || DEFAULT_WECOM_API_BASE_URL;
18
+ const withProtocol = /^https?:\/\//i.test(raw) ? raw : `https://${raw}`;
19
+ const parsed = new URL(withProtocol);
20
+ parsed.search = "";
21
+ parsed.hash = "";
22
+ if (!parsed.pathname || parsed.pathname === "/") {
23
+ parsed.pathname = "/";
24
+ } else if (parsed.pathname.endsWith("/")) {
25
+ parsed.pathname = parsed.pathname.replace(/\/+$/g, "/");
26
+ }
27
+ return parsed.toString().replace(/\/$/, "");
28
+ }
29
+
30
+ function readScopedEnvValue({ envVars = {}, processEnv = process.env, accountId = "default", suffix = "" } = {}) {
31
+ const normalizedAccountId = normalizeAccountId(accountId);
32
+ const scopedKey = normalizedAccountId === "default" ? null : `WECOM_${normalizedAccountId.toUpperCase()}_${suffix}`;
33
+ return pickFirstNonEmptyString(
34
+ scopedKey ? envVars?.[scopedKey] : undefined,
35
+ scopedKey ? processEnv?.[scopedKey] : undefined,
36
+ envVars?.[`WECOM_${suffix}`],
37
+ processEnv?.[`WECOM_${suffix}`],
38
+ );
39
+ }
40
+
41
+ export function normalizeWecomApiBaseUrl(value, fallback = DEFAULT_WECOM_API_BASE_URL) {
42
+ return normalizeHttpBaseUrl(value, fallback);
43
+ }
44
+
45
+ export function buildWecomApiUrl(path, { apiBaseUrl = DEFAULT_WECOM_API_BASE_URL } = {}) {
46
+ const normalizedBaseUrl = normalizeWecomApiBaseUrl(apiBaseUrl);
47
+ const normalizedPath = String(path ?? "").trim();
48
+ if (!normalizedPath) return normalizedBaseUrl;
49
+ return new URL(normalizedPath.replace(/^\/+/, ""), `${normalizedBaseUrl}/`).toString();
50
+ }
51
+
52
+ export function isWecomApiUrl(url, { apiBaseUrl = DEFAULT_WECOM_API_BASE_URL } = {}) {
53
+ const raw = String(url ?? "").trim();
54
+ if (!raw) return false;
55
+ const candidates = [DEFAULT_WECOM_API_BASE_URL, apiBaseUrl]
56
+ .map((value) => {
57
+ try {
58
+ return new URL(`${normalizeWecomApiBaseUrl(value)}/`);
59
+ } catch {
60
+ return null;
61
+ }
62
+ })
63
+ .filter(Boolean);
64
+
65
+ try {
66
+ const parsed = new URL(raw);
67
+ return candidates.some(
68
+ (baseUrl) => parsed.origin === baseUrl.origin && parsed.pathname.startsWith(baseUrl.pathname),
69
+ );
70
+ } catch {
71
+ return candidates.some((baseUrl) => raw.includes(baseUrl.origin));
72
+ }
73
+ }
74
+
75
+ export function resolveWecomApiBaseUrl({
76
+ channelConfig = {},
77
+ accountConfig = {},
78
+ envVars = {},
79
+ processEnv = process.env,
80
+ accountId = "default",
81
+ } = {}) {
82
+ const fromAccount = pickFirstNonEmptyString(accountConfig?.apiBaseUrl, accountConfig?.network?.apiBaseUrl);
83
+ const fromChannel = pickFirstNonEmptyString(channelConfig?.apiBaseUrl, channelConfig?.network?.apiBaseUrl);
84
+ const fromEnv = readScopedEnvValue({
85
+ envVars,
86
+ processEnv,
87
+ accountId,
88
+ suffix: "API_BASE_URL",
89
+ });
90
+ return normalizeWecomApiBaseUrl(pickFirstNonEmptyString(fromAccount, fromChannel, fromEnv));
91
+ }
@@ -62,11 +62,15 @@ export function createWecomObservabilityMetricsStore({
62
62
  layer = "",
63
63
  ok = false,
64
64
  finalStatus = "",
65
+ deliveryStatus = "",
65
66
  accountId = "default",
66
67
  attempts = [],
67
68
  } = {}) {
68
69
  const normalizedLayer = String(layer ?? "").trim().toLowerCase() || "unknown";
69
- const normalizedStatus = String(finalStatus ?? "").trim().toLowerCase() || (ok ? "ok" : "failed");
70
+ const normalizedStatus =
71
+ String(deliveryStatus ?? "").trim().toLowerCase() ||
72
+ String(finalStatus ?? "").trim().toLowerCase() ||
73
+ (ok ? "ok" : "failed");
70
74
  const normalizedAccountId = String(accountId ?? "default").trim().toLowerCase() || "default";
71
75
  state.deliveryTotal += 1;
72
76
  if (ok) state.deliverySuccess += 1;
@@ -77,10 +81,12 @@ export function createWecomObservabilityMetricsStore({
77
81
 
78
82
  const normalizedAttempts = Array.isArray(attempts) ? attempts : [];
79
83
  for (const attempt of normalizedAttempts) {
80
- if (attempt?.status === "error") {
84
+ if (attempt?.status === "error" || attempt?.status === "miss") {
81
85
  pushRecentFailure({
82
86
  scope: "delivery",
83
- reason: String(attempt?.reason ?? "unknown"),
87
+ reason: `${String(attempt?.deliveryStatus ?? attempt?.status ?? "unknown")} ${String(
88
+ attempt?.reason ?? "unknown",
89
+ )}`.trim(),
84
90
  accountId: normalizedAccountId,
85
91
  layer: String(attempt?.layer ?? ""),
86
92
  });
@@ -0,0 +1,313 @@
1
+ import { inferWecomDeliveryStatus } from "./reliable-delivery.js";
2
+ import { applyWecomReasoningPolicy } from "./reasoning-visibility.js";
3
+ import {
4
+ extractWecomReplyDirectives,
5
+ mergeWecomReplyMediaItems,
6
+ resolveWecomReplyDirectiveMediaItems,
7
+ selectWecomReplyTextVariant,
8
+ } from "./reply-output-policy.js";
9
+ import { parseThinkingContent } from "./thinking-parser.js";
10
+
11
+ function assertFunction(name, value) {
12
+ if (typeof value !== "function") {
13
+ throw new Error(`createWecomAgentReplyDeliverer: ${name} is required`);
14
+ }
15
+ }
16
+
17
+ export function createWecomAgentReplyDeliverer({
18
+ getWecomConfig,
19
+ sendWecomText,
20
+ sendWecomMarkdown = null,
21
+ sendWecomOutboundMediaBatch,
22
+ resolveWecomReasoningPolicy = () => ({
23
+ mode: "separate",
24
+ sendThinkingMessage: true,
25
+ includeInFinalAnswer: false,
26
+ title: "思考过程",
27
+ maxChars: 1200,
28
+ }),
29
+ resolveWecomReplyFormatPolicy = () => ({
30
+ mode: "auto",
31
+ }),
32
+ resolveWorkspacePathToHost = () => "",
33
+ createDeliveryTraceId,
34
+ recordDeliveryMetric = () => {},
35
+ recordReliableDeliveryOutcome = () => {},
36
+ enqueuePendingReply = () => null,
37
+ } = {}) {
38
+ assertFunction("getWecomConfig", getWecomConfig);
39
+ assertFunction("sendWecomText", sendWecomText);
40
+ assertFunction("sendWecomOutboundMediaBatch", sendWecomOutboundMediaBatch);
41
+ assertFunction("resolveWecomReasoningPolicy", resolveWecomReasoningPolicy);
42
+ assertFunction("resolveWecomReplyFormatPolicy", resolveWecomReplyFormatPolicy);
43
+ assertFunction("resolveWorkspacePathToHost", resolveWorkspacePathToHost);
44
+ assertFunction("createDeliveryTraceId", createDeliveryTraceId);
45
+ assertFunction("recordDeliveryMetric", recordDeliveryMetric);
46
+ assertFunction("recordReliableDeliveryOutcome", recordReliableDeliveryOutcome);
47
+ assertFunction("enqueuePendingReply", enqueuePendingReply);
48
+
49
+ return async function deliverAgentReply({
50
+ api,
51
+ fromUser,
52
+ accountId = "default",
53
+ sessionId = "",
54
+ text = "",
55
+ rawText = "",
56
+ thinkingContent = "",
57
+ rawThinkingContent = "",
58
+ routeAgentId = "",
59
+ mediaUrl,
60
+ mediaUrls,
61
+ mediaItems,
62
+ mediaType,
63
+ reason = "reply",
64
+ allowPendingEnqueue = true,
65
+ } = {}) {
66
+ const normalizedText = String(text ?? "").trim();
67
+ const normalizedRawText = String(rawText ?? normalizedText).trim();
68
+ const normalizedAccountId = String(accountId ?? "default").trim().toLowerCase() || "default";
69
+ const normalizedSessionId = String(sessionId ?? "").trim();
70
+ const traceId = createDeliveryTraceId("wecom-agent");
71
+ const attempts = [];
72
+
73
+ const parsedPlainReply =
74
+ String(thinkingContent ?? "").trim().length > 0
75
+ ? {
76
+ visibleContent: normalizedText,
77
+ thinkingContent: String(thinkingContent ?? "").trim(),
78
+ }
79
+ : parseThinkingContent(normalizedText);
80
+ const parsedRawReply =
81
+ String(rawThinkingContent ?? "").trim().length > 0
82
+ ? {
83
+ visibleContent: normalizedRawText,
84
+ thinkingContent: String(rawThinkingContent ?? "").trim(),
85
+ }
86
+ : parseThinkingContent(normalizedRawText);
87
+
88
+ const reasoningPolicy = resolveWecomReasoningPolicy(api);
89
+ const plainReasoningPayload = applyWecomReasoningPolicy({
90
+ text: parsedPlainReply.visibleContent,
91
+ thinkingContent: parsedPlainReply.thinkingContent,
92
+ policy: reasoningPolicy,
93
+ transport: "agent",
94
+ phase: "final",
95
+ });
96
+ const richReasoningPayload = applyWecomReasoningPolicy({
97
+ text: parsedRawReply.visibleContent,
98
+ thinkingContent: parsedRawReply.thinkingContent,
99
+ policy: reasoningPolicy,
100
+ transport: "agent",
101
+ phase: "final",
102
+ });
103
+
104
+ const plainDirectivePayload = extractWecomReplyDirectives(plainReasoningPayload.text);
105
+ const richDirectivePayload = extractWecomReplyDirectives(richReasoningPayload.text);
106
+ const directiveMediaItems = resolveWecomReplyDirectiveMediaItems({
107
+ mediaItems: richDirectivePayload.mediaItems,
108
+ routeAgentId,
109
+ resolveWorkspacePathToHost,
110
+ });
111
+ const normalizedMediaItems = mergeWecomReplyMediaItems({
112
+ mediaUrl,
113
+ mediaUrls,
114
+ mediaItems,
115
+ mediaType,
116
+ extraMediaItems: directiveMediaItems,
117
+ });
118
+ const pendingMediaUrls = normalizedMediaItems.map((item) => item.url);
119
+ const selectedReplyText = selectWecomReplyTextVariant({
120
+ plainText: plainDirectivePayload.text,
121
+ richText: richDirectivePayload.text,
122
+ policy: resolveWecomReplyFormatPolicy(api),
123
+ supportsMarkdown: typeof sendWecomMarkdown === "function",
124
+ });
125
+ const effectiveText = String(plainDirectivePayload.text ?? "").trim();
126
+
127
+ const account = getWecomConfig(api, normalizedAccountId) ?? getWecomConfig(api, "default") ?? getWecomConfig(api);
128
+ if (!account?.corpId || !account?.corpSecret || !account?.agentId) {
129
+ const failed = {
130
+ ok: false,
131
+ layer: "agent_push",
132
+ finalStatus: "failed",
133
+ deliveryStatus: "rejected_target",
134
+ attempts: [
135
+ {
136
+ layer: "agent_push",
137
+ ok: false,
138
+ status: "miss",
139
+ deliveryStatus: "rejected_target",
140
+ reason: "agent-config-missing",
141
+ },
142
+ ],
143
+ error: "agent-config-missing",
144
+ };
145
+ recordDeliveryMetric({
146
+ layer: failed.layer,
147
+ ok: false,
148
+ finalStatus: failed.finalStatus,
149
+ deliveryStatus: failed.deliveryStatus,
150
+ accountId: normalizedAccountId,
151
+ attempts: failed.attempts,
152
+ });
153
+ recordReliableDeliveryOutcome({
154
+ mode: "agent",
155
+ accountId: normalizedAccountId,
156
+ sessionId: normalizedSessionId,
157
+ fromUser,
158
+ deliveryStatus: failed.deliveryStatus,
159
+ layer: failed.layer,
160
+ reason: failed.error,
161
+ });
162
+ if (allowPendingEnqueue) {
163
+ enqueuePendingReply(api, {
164
+ mode: "agent",
165
+ accountId: normalizedAccountId,
166
+ sessionId: normalizedSessionId,
167
+ fromUser,
168
+ payload: {
169
+ text: effectiveText,
170
+ mediaUrls: pendingMediaUrls,
171
+ mediaType,
172
+ },
173
+ reason,
174
+ deliveryStatus: failed.deliveryStatus,
175
+ });
176
+ }
177
+ return failed;
178
+ }
179
+
180
+ try {
181
+ const sendTarget = {
182
+ corpId: account.corpId,
183
+ corpSecret: account.corpSecret,
184
+ agentId: account.agentId,
185
+ toUser: fromUser,
186
+ logger: api?.logger,
187
+ proxyUrl: account.outboundProxy,
188
+ apiBaseUrl: account.apiBaseUrl,
189
+ };
190
+ if (selectedReplyText.text) {
191
+ if (selectedReplyText.format === "markdown" && typeof sendWecomMarkdown === "function") {
192
+ await sendWecomMarkdown({
193
+ ...sendTarget,
194
+ content: selectedReplyText.text,
195
+ });
196
+ } else {
197
+ await sendWecomText({
198
+ ...sendTarget,
199
+ text: selectedReplyText.text,
200
+ });
201
+ }
202
+ }
203
+
204
+ let mediaResult = { sentCount: 0, failed: [] };
205
+ if (normalizedMediaItems.length > 0) {
206
+ mediaResult = await sendWecomOutboundMediaBatch({
207
+ corpId: account.corpId,
208
+ corpSecret: account.corpSecret,
209
+ agentId: account.agentId,
210
+ toUser: fromUser,
211
+ mediaItems: normalizedMediaItems,
212
+ logger: api?.logger,
213
+ proxyUrl: account.outboundProxy,
214
+ apiBaseUrl: account.apiBaseUrl,
215
+ });
216
+ }
217
+
218
+ attempts.push({
219
+ layer: "agent_push",
220
+ ok: true,
221
+ status: "ok",
222
+ deliveryStatus: "delivered",
223
+ reason: "",
224
+ });
225
+ const success = {
226
+ ok: true,
227
+ layer: "agent_push",
228
+ finalStatus: "ok",
229
+ deliveryStatus: "delivered",
230
+ attempts,
231
+ traceId,
232
+ meta: {
233
+ accountId: account.accountId || normalizedAccountId,
234
+ mediaSent: Number(mediaResult.sentCount || 0),
235
+ mediaFailed: Array.isArray(mediaResult.failed) ? mediaResult.failed.length : 0,
236
+ replyFormat: selectedReplyText.format,
237
+ },
238
+ };
239
+ recordDeliveryMetric({
240
+ layer: success.layer,
241
+ ok: true,
242
+ finalStatus: success.finalStatus,
243
+ deliveryStatus: success.deliveryStatus,
244
+ accountId: normalizedAccountId,
245
+ attempts: success.attempts,
246
+ });
247
+ recordReliableDeliveryOutcome({
248
+ mode: "agent",
249
+ accountId: normalizedAccountId,
250
+ sessionId: normalizedSessionId,
251
+ fromUser,
252
+ deliveryStatus: success.deliveryStatus,
253
+ layer: success.layer,
254
+ reason,
255
+ });
256
+ return success;
257
+ } catch (err) {
258
+ const deliveryStatus = inferWecomDeliveryStatus({
259
+ reason: String(err?.message || err),
260
+ layer: "agent_push",
261
+ });
262
+ attempts.push({
263
+ layer: "agent_push",
264
+ ok: false,
265
+ status: "error",
266
+ deliveryStatus,
267
+ reason: String(err?.message || err),
268
+ });
269
+ const failed = {
270
+ ok: false,
271
+ layer: "agent_push",
272
+ finalStatus: "failed",
273
+ deliveryStatus,
274
+ attempts,
275
+ error: String(err?.message || err),
276
+ traceId,
277
+ };
278
+ recordDeliveryMetric({
279
+ layer: failed.layer,
280
+ ok: false,
281
+ finalStatus: failed.finalStatus,
282
+ deliveryStatus: failed.deliveryStatus,
283
+ accountId: normalizedAccountId,
284
+ attempts: failed.attempts,
285
+ });
286
+ recordReliableDeliveryOutcome({
287
+ mode: "agent",
288
+ accountId: normalizedAccountId,
289
+ sessionId: normalizedSessionId,
290
+ fromUser,
291
+ deliveryStatus: failed.deliveryStatus,
292
+ layer: failed.layer,
293
+ reason: failed.error,
294
+ });
295
+ if (allowPendingEnqueue) {
296
+ enqueuePendingReply(api, {
297
+ mode: "agent",
298
+ accountId: normalizedAccountId,
299
+ sessionId: normalizedSessionId,
300
+ fromUser,
301
+ payload: {
302
+ text: effectiveText,
303
+ mediaUrls: pendingMediaUrls,
304
+ mediaType,
305
+ },
306
+ reason: failed.error,
307
+ deliveryStatus: failed.deliveryStatus,
308
+ });
309
+ }
310
+ return failed;
311
+ }
312
+ };
313
+ }
@@ -38,12 +38,36 @@ export function createWecomAgentMediaSender({
38
38
  chatId,
39
39
  mediaUrl,
40
40
  mediaUrls,
41
+ mediaItems,
41
42
  mediaType,
42
43
  logger,
43
44
  proxyUrl,
45
+ apiBaseUrl,
44
46
  maxBytes = 20 * 1024 * 1024,
45
47
  } = {}) {
46
- const candidates = normalizeOutboundMediaUrls({ mediaUrl, mediaUrls });
48
+ const rawItems = [
49
+ ...[mediaUrl, ...(Array.isArray(mediaUrls) ? mediaUrls : [])]
50
+ .map((url) => ({
51
+ url,
52
+ mediaType,
53
+ }))
54
+ .filter((item) => String(item?.url ?? "").trim()),
55
+ ...(Array.isArray(mediaItems) ? mediaItems : []),
56
+ ];
57
+ const dedupe = new Set();
58
+ const candidates = [];
59
+ for (const item of rawItems) {
60
+ const normalizedUrl = String(item?.url ?? "").trim();
61
+ const normalizedType = String(item?.mediaType ?? "").trim().toLowerCase() || undefined;
62
+ if (!normalizedUrl) continue;
63
+ const dedupeKey = `${normalizedType || ""}:${normalizedUrl}`;
64
+ if (dedupe.has(dedupeKey)) continue;
65
+ dedupe.add(dedupeKey);
66
+ candidates.push({
67
+ url: normalizedUrl,
68
+ mediaType: normalizedType,
69
+ });
70
+ }
47
71
  if (candidates.length === 0) {
48
72
  return { total: 0, sentCount: 0, failed: [] };
49
73
  }
@@ -54,10 +78,10 @@ export function createWecomAgentMediaSender({
54
78
  for (const candidate of candidates) {
55
79
  try {
56
80
  const target = resolveWecomOutboundMediaTarget({
57
- mediaUrl: candidate,
58
- mediaType: candidates.length === 1 ? mediaType : undefined,
81
+ mediaUrl: candidate.url,
82
+ mediaType: candidate.mediaType ?? (candidates.length === 1 ? mediaType : undefined),
59
83
  });
60
- const { buffer } = await fetchMediaFromUrl(candidate, {
84
+ const { buffer } = await fetchMediaFromUrl(candidate.url, {
61
85
  proxyUrl,
62
86
  logger,
63
87
  forceProxy: Boolean(proxyUrl),
@@ -79,9 +103,10 @@ export function createWecomAgentMediaSender({
79
103
  text: fallbackText,
80
104
  logger,
81
105
  proxyUrl,
106
+ apiBaseUrl,
82
107
  });
83
108
  logger?.info?.(
84
- `wecom: tiny file fallback as text (${buffer.length} bytes) target=${candidate.slice(0, 120)}`,
109
+ `wecom: tiny file fallback as text (${buffer.length} bytes) target=${candidate.url.slice(0, 120)}`,
85
110
  );
86
111
  sentCount += 1;
87
112
  continue;
@@ -94,6 +119,7 @@ export function createWecomAgentMediaSender({
94
119
  filename: target.filename,
95
120
  logger,
96
121
  proxyUrl,
122
+ apiBaseUrl,
97
123
  });
98
124
  if (target.type === "image") {
99
125
  await sendWecomImage({
@@ -107,6 +133,7 @@ export function createWecomAgentMediaSender({
107
133
  mediaId,
108
134
  logger,
109
135
  proxyUrl,
136
+ apiBaseUrl,
110
137
  });
111
138
  } else if (target.type === "video") {
112
139
  await sendWecomVideo({
@@ -120,6 +147,7 @@ export function createWecomAgentMediaSender({
120
147
  mediaId,
121
148
  logger,
122
149
  proxyUrl,
150
+ apiBaseUrl,
123
151
  });
124
152
  } else if (target.type === "voice") {
125
153
  await sendWecomVoice({
@@ -133,6 +161,7 @@ export function createWecomAgentMediaSender({
133
161
  mediaId,
134
162
  logger,
135
163
  proxyUrl,
164
+ apiBaseUrl,
136
165
  });
137
166
  } else {
138
167
  await sendWecomFile({
@@ -146,15 +175,16 @@ export function createWecomAgentMediaSender({
146
175
  mediaId,
147
176
  logger,
148
177
  proxyUrl,
178
+ apiBaseUrl,
149
179
  });
150
180
  }
151
181
  sentCount += 1;
152
182
  } catch (err) {
153
183
  failed.push({
154
- url: candidate,
184
+ url: candidate.url,
155
185
  reason: String(err?.message || err),
156
186
  });
157
- logger?.warn?.(`wecom: failed to send outbound media ${candidate}: ${String(err?.message || err)}`);
187
+ logger?.warn?.(`wecom: failed to send outbound media ${candidate.url}: ${String(err?.message || err)}`);
158
188
  }
159
189
  }
160
190
 
@@ -31,6 +31,7 @@ export function createWecomAgentPushDeliverer({
31
31
  text: `${content || fallbackText}${mediaFallbackSuffix}`.trim(),
32
32
  logger: api?.logger,
33
33
  proxyUrl: account.outboundProxy,
34
+ apiBaseUrl: account.apiBaseUrl,
34
35
  });
35
36
  return {
36
37
  ok: true,