@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.
- package/CHANGELOG.md +85 -0
- package/README.en.md +204 -32
- package/README.md +234 -63
- package/docs/channels/wecom.md +137 -1
- package/openclaw.plugin.json +694 -10
- package/package.json +207 -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 +631 -34
- 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 +63 -16
- 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 +25 -1
- package/src/wecom/bot-inbound-guards.js +78 -23
- package/src/wecom/bot-long-connection-manager.js +4 -4
- package/src/wecom/channel-config-schema.js +132 -0
- package/src/wecom/channel-plugin.js +370 -7
- package/src/wecom/command-handlers.js +107 -10
- package/src/wecom/command-status-text.js +275 -1
- 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/pairing.js +188 -0
- 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
|
@@ -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 =
|
|
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?.
|
|
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
|
|
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
|
|