@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.
Files changed (77) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/README.en.md +181 -14
  3. package/README.md +201 -16
  4. package/docs/channels/wecom.md +137 -1
  5. package/openclaw.plugin.json +688 -6
  6. package/package.json +204 -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 +619 -30
  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 +24 -0
  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 +24 -0
  40. package/src/wecom/bot-inbound-guards.js +31 -1
  41. package/src/wecom/channel-config-schema.js +132 -0
  42. package/src/wecom/channel-plugin.js +348 -7
  43. package/src/wecom/command-handlers.js +102 -11
  44. package/src/wecom/command-status-text.js +206 -0
  45. package/src/wecom/doc-client.js +7 -1
  46. package/src/wecom/inbound-content-handler-file-video-link.js +4 -0
  47. package/src/wecom/inbound-content-handler-image-voice.js +6 -0
  48. package/src/wecom/inbound-content.js +5 -0
  49. package/src/wecom/installer-api.js +910 -0
  50. package/src/wecom/media-download.js +2 -2
  51. package/src/wecom/migration-diagnostics.js +816 -0
  52. package/src/wecom/network-config.js +91 -0
  53. package/src/wecom/observability-metrics.js +9 -3
  54. package/src/wecom/outbound-agent-delivery.js +313 -0
  55. package/src/wecom/outbound-agent-media-sender.js +37 -7
  56. package/src/wecom/outbound-agent-push.js +1 -0
  57. package/src/wecom/outbound-delivery.js +129 -12
  58. package/src/wecom/outbound-stream-msg-item.js +25 -2
  59. package/src/wecom/outbound-webhook-delivery.js +19 -0
  60. package/src/wecom/outbound-webhook-media.js +30 -6
  61. package/src/wecom/pending-reply-manager.js +143 -0
  62. package/src/wecom/plugin-account-policy-services.js +26 -0
  63. package/src/wecom/plugin-base-services.js +58 -0
  64. package/src/wecom/plugin-constants.js +1 -1
  65. package/src/wecom/plugin-delivery-inbound-services.js +25 -0
  66. package/src/wecom/plugin-processing-deps.js +7 -0
  67. package/src/wecom/plugin-route-runtime-deps.js +1 -0
  68. package/src/wecom/plugin-services.js +87 -0
  69. package/src/wecom/policy-resolvers.js +93 -20
  70. package/src/wecom/quickstart-metadata.js +1247 -0
  71. package/src/wecom/reasoning-visibility.js +104 -0
  72. package/src/wecom/register-runtime.js +10 -0
  73. package/src/wecom/reliable-delivery-persistence.js +138 -0
  74. package/src/wecom/reliable-delivery.js +642 -0
  75. package/src/wecom/reply-output-policy.js +171 -0
  76. package/src/wecom/text-inbound-scheduler.js +6 -1
  77. package/src/wecom/workspace-auto-sender.js +2 -0
@@ -1,4 +1,5 @@
1
1
  import { buildDefaultAgentWebhookPath } from "./account-paths.js";
2
+ import { resolveWecomApiBaseUrl } from "./network-config.js";
2
3
 
3
4
  export function asNumber(v, fallback = null) {
4
5
  if (v == null) return fallback;
@@ -31,6 +32,7 @@ export function normalizeAccountConfig({ raw, accountId, normalizeWecomWebhookTa
31
32
  const normalizedId = normalizeAccountId(accountId);
32
33
  if (!raw || typeof raw !== "object") return null;
33
34
  if (typeof normalizeWecomWebhookTargetMap !== "function") return null;
35
+ const networkConfig = raw.network && typeof raw.network === "object" ? raw.network : {};
34
36
 
35
37
  const legacyAgent = raw.agent && typeof raw.agent === "object" ? raw.agent : {};
36
38
  const hasLegacyAgentBlock = Object.keys(legacyAgent).length > 0;
@@ -53,7 +55,19 @@ export function normalizeAccountConfig({ raw, accountId, normalizeWecomWebhookTa
53
55
  const defaultWebhookPath = buildDefaultAgentWebhookPath(normalizedId);
54
56
  const webhookPath = String(raw.webhookPath ?? legacyAgent.webhookPath ?? defaultWebhookPath).trim() || defaultWebhookPath;
55
57
  const name = pickFirstNonEmptyString(raw.name, normalizedId);
56
- const outboundProxy = String(raw.outboundProxy ?? raw.proxyUrl ?? raw.proxy ?? "").trim();
58
+ const outboundProxy = String(
59
+ raw.outboundProxy ??
60
+ raw.proxyUrl ??
61
+ raw.proxy ??
62
+ networkConfig.egressProxyUrl ??
63
+ networkConfig.proxyUrl ??
64
+ networkConfig.proxy ??
65
+ "",
66
+ ).trim();
67
+ const apiBaseUrl = resolveWecomApiBaseUrl({
68
+ accountConfig: raw,
69
+ accountId: normalizedId,
70
+ });
57
71
  const webhooks = normalizeWecomWebhookTargetMap(raw.webhooks);
58
72
  const allowFrom = raw.allowFrom ?? raw.dm?.allowFrom;
59
73
  const allowFromRejectMessage = String(raw.allowFromRejectMessage ?? raw.rejectUnauthorizedMessage ?? "").trim();
@@ -74,6 +88,7 @@ export function normalizeAccountConfig({ raw, accountId, normalizeWecomWebhookTa
74
88
  webhookPath,
75
89
  name,
76
90
  outboundProxy: outboundProxy || undefined,
91
+ apiBaseUrl: apiBaseUrl || undefined,
77
92
  webhooks: Object.keys(webhooks).length > 0 ? webhooks : undefined,
78
93
  allowFrom,
79
94
  allowFromRejectMessage: allowFromRejectMessage || undefined,
@@ -108,11 +123,21 @@ export function readAccountConfigFromEnv({
108
123
  const defaultWebhookPath = buildDefaultAgentWebhookPath(normalizedId);
109
124
  const webhookPath = String(readVar("WEBHOOK_PATH") ?? defaultWebhookPath).trim() || defaultWebhookPath;
110
125
  const outboundProxyRaw =
126
+ readVar("EGRESS_PROXY_URL") ??
111
127
  readVar("PROXY") ??
112
128
  (normalizedId === "default"
113
129
  ? requireEnv("HTTPS_PROXY")
114
130
  : envVars?.WECOM_PROXY ?? requireEnv("WECOM_PROXY") ?? requireEnv("HTTPS_PROXY"));
115
131
  const outboundProxy = String(outboundProxyRaw ?? "").trim();
132
+ const apiBaseUrl = resolveWecomApiBaseUrl({
133
+ envVars: {
134
+ ...envVars,
135
+ [`${prefix}_API_BASE_URL`]: readVar("API_BASE_URL"),
136
+ ...(normalizedId === "default" ? { WECOM_API_BASE_URL: readVar("API_BASE_URL") } : {}),
137
+ },
138
+ processEnv: {},
139
+ accountId: normalizedId,
140
+ });
116
141
  const webhooks = normalizeWecomWebhookTargetMap(readVar("WEBHOOK_TARGETS"), readVar("WEBHOOKS"));
117
142
  const allowFrom = readVar("ALLOW_FROM");
118
143
  const allowFromRejectMessage = String(readVar("ALLOW_FROM_REJECT_MESSAGE") ?? "").trim();
@@ -130,6 +155,7 @@ export function readAccountConfigFromEnv({
130
155
  callbackAesKey,
131
156
  webhookPath,
132
157
  outboundProxy: outboundProxy || undefined,
158
+ apiBaseUrl: apiBaseUrl || undefined,
133
159
  webhooks: Object.keys(webhooks).length > 0 ? webhooks : undefined,
134
160
  allowFrom,
135
161
  allowFromRejectMessage: allowFromRejectMessage || undefined,
@@ -6,10 +6,14 @@ import {
6
6
  readAccountConfigFromEnv,
7
7
  } from "./account-config-core.js";
8
8
  import { normalizePluginHttpPath } from "./http-path.js";
9
+ import { resolveWecomApiBaseUrl } from "./network-config.js";
9
10
 
10
- const LEGACY_INLINE_ACCOUNT_RESERVED_KEYS = new Set([
11
+ export const LEGACY_INLINE_ACCOUNT_RESERVED_KEYS = new Set([
11
12
  "name",
12
13
  "enabled",
14
+ "botId",
15
+ "botid",
16
+ "secret",
13
17
  "corpId",
14
18
  "corpSecret",
15
19
  "agentId",
@@ -21,9 +25,14 @@ const LEGACY_INLINE_ACCOUNT_RESERVED_KEYS = new Set([
21
25
  "outboundProxy",
22
26
  "proxyUrl",
23
27
  "proxy",
28
+ "network",
29
+ "apiBaseUrl",
24
30
  "webhooks",
25
31
  "allowFrom",
26
32
  "allowFromRejectMessage",
33
+ "groupPolicy",
34
+ "groupAllowFrom",
35
+ "groupAllowFromRejectMessage",
27
36
  "rejectUnauthorizedMessage",
28
37
  "adminUsers",
29
38
  "commandAllowlist",
@@ -31,6 +40,7 @@ const LEGACY_INLINE_ACCOUNT_RESERVED_KEYS = new Set([
31
40
  "commands",
32
41
  "workspaceTemplate",
33
42
  "groupChat",
43
+ "groups",
34
44
  "dynamicAgent",
35
45
  "dynamicAgents",
36
46
  "dm",
@@ -48,7 +58,7 @@ const LEGACY_INLINE_ACCOUNT_RESERVED_KEYS = new Set([
48
58
  "agent",
49
59
  ]);
50
60
 
51
- function listLegacyInlineAccountEntries(channelConfig) {
61
+ export function listLegacyInlineAccountEntries(channelConfig) {
52
62
  if (!channelConfig || typeof channelConfig !== "object") return [];
53
63
  const entries = [];
54
64
  for (const [rawKey, value] of Object.entries(channelConfig)) {
@@ -137,6 +147,13 @@ export function createWecomAccountRegistry({
137
147
  processEnv,
138
148
  accountId,
139
149
  });
150
+ config.apiBaseUrl = resolveWecomApiBaseUrl({
151
+ channelConfig,
152
+ accountConfig: config,
153
+ envVars,
154
+ processEnv,
155
+ accountId,
156
+ });
140
157
  }
141
158
 
142
159
  wecomAccounts.clear();
@@ -29,6 +29,7 @@ export async function executeWecomAgentDispatchFlow({
29
29
  corpSecret,
30
30
  agentId,
31
31
  proxyUrl = "",
32
+ apiBaseUrl = "",
32
33
  tempPathsToCleanup = [],
33
34
  resolveWecomReplyStreamingPolicy,
34
35
  asNumber,
@@ -37,6 +38,7 @@ export async function executeWecomAgentDispatchFlow({
37
38
  markdownToWecomText,
38
39
  autoSendWorkspaceFilesFromReplyText,
39
40
  sendWecomOutboundMediaBatch,
41
+ resolveWecomReasoningPolicy,
40
42
  withTimeout,
41
43
  isDispatchTimeoutError,
42
44
  isAgentFailureText,
@@ -44,6 +46,7 @@ export async function executeWecomAgentDispatchFlow({
44
46
  ensureLateReplyWatcherRunner,
45
47
  ACTIVE_LATE_REPLY_WATCHERS,
46
48
  sendTextToUser,
49
+ deliverAgentReply,
47
50
  clearSessionStoreEntry,
48
51
  } = {}) {
49
52
  assertFunction("resolveWecomReplyStreamingPolicy", resolveWecomReplyStreamingPolicy);
@@ -92,9 +95,12 @@ export async function executeWecomAgentDispatchFlow({
92
95
  lateReplyWatchMs,
93
96
  lateReplyPollMs,
94
97
  sendTextToUser,
98
+ deliverAgentReply,
95
99
  ensureLateReplyWatcherRunner,
96
100
  activeWatchers: ACTIVE_LATE_REPLY_WATCHERS,
97
101
  clearSessionStoreEntry,
102
+ api,
103
+ fromUser,
98
104
  logger: api?.logger,
99
105
  });
100
106
  const sendProgressNotice = lateReplyRuntime.sendProgressNotice;
@@ -115,21 +121,26 @@ export async function executeWecomAgentDispatchFlow({
115
121
  api,
116
122
  state: dispatchState,
117
123
  streamingEnabled,
124
+ sessionId,
125
+ runtimeAccountId,
118
126
  fromUser,
119
127
  routedAgentId,
120
128
  corpId,
121
129
  corpSecret,
122
130
  agentId,
123
131
  proxyUrl,
132
+ apiBaseUrl,
124
133
  flushStreamingBuffer,
125
134
  sendFailureFallback,
126
135
  sendTextToUser,
136
+ deliverAgentReply,
127
137
  markdownToWecomText,
128
138
  isAgentFailureText,
129
139
  computeStreamingTailText,
130
140
  autoSendWorkspaceFilesFromReplyText,
131
141
  buildWorkspaceAutoSendHints,
132
142
  sendWecomOutboundMediaBatch,
143
+ reasoningPolicy: resolveWecomReasoningPolicy?.(api) ?? {},
133
144
  });
134
145
  const dispatchResult = await withTimeout(
135
146
  runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
@@ -1,3 +1,7 @@
1
+ import { applyWecomReasoningPolicy } from "./reasoning-visibility.js";
2
+ import { extractWecomReplyDirectives } from "./reply-output-policy.js";
3
+ import { parseThinkingContent } from "./thinking-parser.js";
4
+
1
5
  function assertFunction(name, value) {
2
6
  if (typeof value !== "function") {
3
7
  throw new Error(`createWecomAgentDispatchHandlers: ${name} is required`);
@@ -16,21 +20,26 @@ export function createWecomAgentDispatchHandlers({
16
20
  api,
17
21
  state,
18
22
  streamingEnabled = false,
23
+ sessionId = "",
24
+ runtimeAccountId = "default",
19
25
  fromUser,
20
26
  routedAgentId = "",
21
27
  corpId = "",
22
28
  corpSecret = "",
23
29
  agentId = "",
24
30
  proxyUrl = "",
31
+ apiBaseUrl = "",
25
32
  flushStreamingBuffer,
26
33
  sendFailureFallback,
27
34
  sendTextToUser,
35
+ deliverAgentReply = null,
28
36
  markdownToWecomText,
29
37
  isAgentFailureText,
30
38
  computeStreamingTailText,
31
39
  autoSendWorkspaceFilesFromReplyText,
32
40
  buildWorkspaceAutoSendHints,
33
41
  sendWecomOutboundMediaBatch,
42
+ reasoningPolicy = {},
34
43
  } = {}) {
35
44
  if (!state || typeof state !== "object") {
36
45
  throw new Error("createWecomAgentDispatchHandlers: state is required");
@@ -67,9 +76,13 @@ export function createWecomAgentDispatchHandlers({
67
76
  }
68
77
  if (info.kind === "block") {
69
78
  if (payload.text) {
70
- state.blockTextFallback = appendWecomAgentBlockFallback(state.blockTextFallback, payload.text);
79
+ const blockParsed = parseThinkingContent(payload.text);
80
+ const blockVisibleText = extractWecomReplyDirectives(markdownToWecomText(blockParsed.visibleContent).trim()).text;
81
+ if (blockVisibleText) {
82
+ state.blockTextFallback = appendWecomAgentBlockFallback(state.blockTextFallback, blockVisibleText);
83
+ }
71
84
  if (streamingEnabled) {
72
- state.streamChunkBuffer += payload.text;
85
+ state.streamChunkBuffer += blockVisibleText || "";
73
86
  await flushStreamingBuffer({ force: false, reason: "block" });
74
87
  }
75
88
  }
@@ -85,12 +98,31 @@ export function createWecomAgentDispatchHandlers({
85
98
  return;
86
99
  }
87
100
 
101
+ const parsedFinal = parseThinkingContent(payload.text);
102
+ const parsedRawFinal = parseThinkingContent(String(payload.rawText ?? payload.text ?? ""));
103
+ const reasoningPayload = applyWecomReasoningPolicy({
104
+ text: markdownToWecomText(parsedFinal.visibleContent).trim(),
105
+ thinkingContent: markdownToWecomText(parsedFinal.thinkingContent).trim(),
106
+ policy: reasoningPolicy,
107
+ transport: "agent",
108
+ phase: "final",
109
+ });
110
+ const rawReasoningPayload = applyWecomReasoningPolicy({
111
+ text: String(parsedRawFinal.visibleContent ?? "").trim(),
112
+ thinkingContent: String(payload.rawThinkingContent ?? parsedRawFinal.thinkingContent ?? "").trim(),
113
+ policy: reasoningPolicy,
114
+ transport: "agent",
115
+ phase: "final",
116
+ });
117
+ const effectiveFinalText = extractWecomReplyDirectives(String(reasoningPayload.text ?? "").trim()).text;
118
+ const richFinalText = extractWecomReplyDirectives(String(rawReasoningPayload.text ?? "").trim()).text;
119
+
88
120
  logger?.info?.(`wecom: delivering ${info.kind} reply, length=${payload.text.length}`);
89
121
  if (streamingEnabled) {
90
122
  await flushStreamingBuffer({ force: true, reason: "final" });
91
123
  await state.streamChunkSendChain;
92
124
  if (state.streamChunkSentCount > 0) {
93
- const finalText = markdownToWecomText(payload.text).trim();
125
+ const finalText = effectiveFinalText;
94
126
  const streamedText = markdownToWecomText(state.blockTextFallback).trim();
95
127
  const tailText = computeStreamingTailText({ finalText, streamedText });
96
128
  if (tailText) {
@@ -105,9 +137,9 @@ export function createWecomAgentDispatchHandlers({
105
137
  }
106
138
 
107
139
  if (!deliveredFinalText) {
108
- const formattedReply = markdownToWecomText(payload.text);
140
+ const formattedReply = effectiveFinalText;
109
141
  const workspaceAutoMedia = await autoSendWorkspaceFilesFromReplyText({
110
- text: formattedReply,
142
+ text: String(payload.rawText ?? payload.text ?? formattedReply),
111
143
  routeAgentId: routedAgentId,
112
144
  corpId,
113
145
  corpSecret,
@@ -115,12 +147,32 @@ export function createWecomAgentDispatchHandlers({
115
147
  toUser: fromUser,
116
148
  logger,
117
149
  proxyUrl,
150
+ apiBaseUrl,
118
151
  });
119
152
  const workspaceHints = buildWorkspaceAutoSendHints(workspaceAutoMedia);
120
153
  const finalReplyText = [formattedReply, ...workspaceHints].filter(Boolean).join("\n\n");
121
- await sendTextToUser(finalReplyText);
122
- state.hasDeliveredReply = true;
123
- deliveredFinalText = true;
154
+ if (typeof deliverAgentReply === "function") {
155
+ const result = await deliverAgentReply({
156
+ api,
157
+ fromUser,
158
+ accountId: runtimeAccountId,
159
+ sessionId,
160
+ text: finalReplyText,
161
+ rawText: [richFinalText, ...workspaceHints].filter(Boolean).join("\n\n"),
162
+ rawThinkingContent: String(rawReasoningPayload.thinkingContent ?? "").trim(),
163
+ routeAgentId: routedAgentId,
164
+ reason: "final-reply",
165
+ });
166
+ state.hasDeliveredReply = true;
167
+ deliveredFinalText = result?.ok === true;
168
+ if (!result?.ok) {
169
+ logger?.warn?.(`wecom: final agent reply deferred to pending delivery session=${sessionId || "n/a"}`);
170
+ }
171
+ } else {
172
+ await sendTextToUser(finalReplyText);
173
+ state.hasDeliveredReply = true;
174
+ deliveredFinalText = true;
175
+ }
124
176
  logger?.info?.(`wecom: sent AI reply to ${fromUser}: ${finalReplyText.slice(0, 50)}...`);
125
177
  }
126
178
  }
@@ -136,6 +188,7 @@ export function createWecomAgentDispatchHandlers({
136
188
  mediaType: payload.mediaType,
137
189
  logger,
138
190
  proxyUrl,
191
+ apiBaseUrl,
139
192
  });
140
193
  if (mediaResult.sentCount > 0) {
141
194
  state.hasDeliveredReply = true;
@@ -70,6 +70,30 @@ export async function applyWecomAgentInboundGuards({
70
70
  const dmPolicy = resolveWecomDmPolicy(api, resolvedAccountId, config);
71
71
  const allowFromPolicy = resolveWecomAllowFromPolicy(api, resolvedAccountId, config);
72
72
 
73
+ if (isGroupChat) {
74
+ const groupPolicyMode = String(groupChatPolicy?.policyMode ?? (groupChatPolicy?.enabled === false ? "deny" : "open"))
75
+ .trim()
76
+ .toLowerCase();
77
+ const groupSenderAllowed =
78
+ isAdminUser ||
79
+ groupPolicyMode === "open" ||
80
+ (groupPolicyMode !== "deny" &&
81
+ (groupChatPolicy.allowFrom.includes("*") ||
82
+ isWecomSenderAllowed({
83
+ senderId: normalizedFromUser,
84
+ allowFrom: groupChatPolicy.allowFrom,
85
+ })));
86
+ if (!groupSenderAllowed) {
87
+ api?.logger?.warn?.(
88
+ `wecom: sender blocked by group policy account=${resolvedAccountId} chatId=${chatId || "unknown"} user=${normalizedFromUser} mode=${groupPolicyMode || "open"}`,
89
+ );
90
+ if (groupChatPolicy?.rejectMessage) {
91
+ await sendTextToUser(groupChatPolicy.rejectMessage);
92
+ }
93
+ return { ok: false, commandBody: nextCommandBody, isAdminUser };
94
+ }
95
+ }
96
+
73
97
  if (!isGroupChat) {
74
98
  const dmAccess = await resolveWecomDirectMessageAccess({
75
99
  api,
@@ -31,12 +31,14 @@ export function createWecomAgentInboundProcessor(deps = {}) {
31
31
  resolveWecomAgentRoute,
32
32
  seedDynamicAgentWorkspace,
33
33
  resolveWecomReplyStreamingPolicy,
34
+ resolveWecomReasoningPolicy,
34
35
  asNumber,
35
36
  requireEnv,
36
37
  getByteLength,
37
38
  markdownToWecomText,
38
39
  autoSendWorkspaceFilesFromReplyText,
39
40
  sendWecomOutboundMediaBatch,
41
+ deliverAgentReply,
40
42
  sleep,
41
43
  resolveSessionTranscriptFilePath,
42
44
  readTranscriptAppendedChunk,
@@ -50,6 +52,8 @@ export function createWecomAgentInboundProcessor(deps = {}) {
50
52
  ACTIVE_LATE_REPLY_WATCHERS,
51
53
  resetWecomConversationSession,
52
54
  clearSessionStoreEntry,
55
+ markWecomReliableInboundActivity = () => null,
56
+ flushWecomSessionPendingReplies = async () => {},
53
57
  } = deps;
54
58
 
55
59
  let lateReplyWatcherRunner = null;
@@ -94,7 +98,7 @@ export function createWecomAgentInboundProcessor(deps = {}) {
94
98
 
95
99
  const cfg = api.config;
96
100
  const runtime = api.runtime;
97
- const { corpId, corpSecret, agentId, outboundProxy: proxyUrl } = config;
101
+ const { corpId, corpSecret, agentId, outboundProxy: proxyUrl, apiBaseUrl } = config;
98
102
  const sendTextToUser = createWecomAgentTextSender({
99
103
  sendWecomText,
100
104
  corpId,
@@ -103,17 +107,29 @@ export function createWecomAgentInboundProcessor(deps = {}) {
103
107
  toUser: fromUser,
104
108
  logger: api.logger,
105
109
  proxyUrl,
110
+ apiBaseUrl,
106
111
  });
107
112
 
108
113
  try {
109
114
  const baseSessionId = buildWecomSessionId(fromUser, config.accountId || accountId || "default");
115
+ markWecomReliableInboundActivity({
116
+ mode: "agent",
117
+ accountId: config.accountId || accountId || "default",
118
+ sessionId: baseSessionId,
119
+ fromUser,
120
+ });
110
121
  let sessionId = baseSessionId;
111
122
  let routedAgentId = "";
112
123
  const normalizedFromUser = String(fromUser ?? "").trim().toLowerCase();
113
124
  const fromAddress = `wecom:${normalizedFromUser}`;
114
125
  const originalContent = content || "";
115
126
  let commandBody = originalContent;
116
- const groupChatPolicy = resolveWecomGroupChatPolicy(api);
127
+ const groupChatPolicy = resolveWecomGroupChatPolicy(
128
+ api,
129
+ config.accountId || accountId || "default",
130
+ config,
131
+ isGroupChat ? chatId : "",
132
+ );
117
133
  const dynamicAgentPolicy = resolveWecomDynamicAgentPolicy(api);
118
134
  api.logger.info?.(`wecom: processing ${msgType} message for session ${sessionId}${isGroupChat ? " (group)" : ""}`);
119
135
 
@@ -146,6 +162,7 @@ export function createWecomAgentInboundProcessor(deps = {}) {
146
162
  agentId,
147
163
  accountId: config.accountId || "default",
148
164
  proxyUrl,
165
+ apiBaseUrl,
149
166
  chatId,
150
167
  isGroupChat,
151
168
  },
@@ -206,6 +223,7 @@ export function createWecomAgentInboundProcessor(deps = {}) {
206
223
  corpSecret,
207
224
  agentId,
208
225
  proxyUrl,
226
+ apiBaseUrl,
209
227
  fromUser,
210
228
  msgType,
211
229
  baseText: msgType === "text" ? commandBody : originalContent,
@@ -255,6 +273,17 @@ export function createWecomAgentInboundProcessor(deps = {}) {
255
273
  const storePath = runtimeContext.storePath;
256
274
  const ctxPayload = runtimeContext.ctxPayload;
257
275
  const runtimeAccountId = runtimeContext.accountId;
276
+ markWecomReliableInboundActivity({
277
+ mode: "agent",
278
+ accountId: runtimeAccountId,
279
+ sessionId,
280
+ fromUser,
281
+ });
282
+ await flushWecomSessionPendingReplies({
283
+ mode: "agent",
284
+ accountId: runtimeAccountId,
285
+ sessionId,
286
+ });
258
287
 
259
288
  api.logger.info?.(`wecom: dispatching message via agent runtime for session ${sessionId}`);
260
289
  await executeWecomAgentDispatchFlow({
@@ -272,14 +301,17 @@ export function createWecomAgentInboundProcessor(deps = {}) {
272
301
  corpSecret,
273
302
  agentId,
274
303
  proxyUrl,
304
+ apiBaseUrl,
275
305
  tempPathsToCleanup,
276
306
  resolveWecomReplyStreamingPolicy,
307
+ resolveWecomReasoningPolicy,
277
308
  asNumber,
278
309
  requireEnv,
279
310
  getByteLength,
280
311
  markdownToWecomText,
281
312
  autoSendWorkspaceFilesFromReplyText,
282
313
  sendWecomOutboundMediaBatch,
314
+ deliverAgentReply,
283
315
  withTimeout,
284
316
  isDispatchTimeoutError,
285
317
  isAgentFailureText,
@@ -13,6 +13,8 @@ function isTimeoutLikeReason(reason) {
13
13
 
14
14
  export function createWecomAgentLateReplyRuntime({
15
15
  dispatchState,
16
+ api,
17
+ fromUser,
16
18
  sessionId,
17
19
  msgId = "",
18
20
  transcriptSessionId = "",
@@ -21,6 +23,7 @@ export function createWecomAgentLateReplyRuntime({
21
23
  lateReplyWatchMs,
22
24
  lateReplyPollMs,
23
25
  sendTextToUser,
26
+ deliverAgentReply = null,
24
27
  ensureLateReplyWatcherRunner,
25
28
  activeWatchers,
26
29
  clearSessionStoreEntry = null,
@@ -72,7 +75,19 @@ export function createWecomAgentLateReplyRuntime({
72
75
  dispatchState.hasDeliveredReply = true;
73
76
  const reasonText = String(reason ?? "unknown").slice(0, 160);
74
77
  try {
75
- await sendTextToUser(`抱歉,当前模型请求超时或网络不稳定,请稍后重试。\n故障信息: ${reasonText}`);
78
+ const fallbackText = `抱歉,当前模型请求超时或网络不稳定,请稍后重试。\n故障信息: ${reasonText}`;
79
+ if (typeof deliverAgentReply === "function") {
80
+ await deliverAgentReply({
81
+ api,
82
+ fromUser,
83
+ accountId,
84
+ sessionId,
85
+ text: fallbackText,
86
+ reason: "late-timeout-fallback",
87
+ });
88
+ } else {
89
+ await sendTextToUser(fallbackText);
90
+ }
76
91
  } finally {
77
92
  await autoResetTimedOutSession(reasonText);
78
93
  }
@@ -100,7 +115,20 @@ export function createWecomAgentLateReplyRuntime({
100
115
  markDelivered: () => {
101
116
  dispatchState.hasDeliveredReply = true;
102
117
  },
103
- sendText: async (text) => sendTextToUser(text),
118
+ sendText: async (text) => {
119
+ if (typeof deliverAgentReply === "function") {
120
+ await deliverAgentReply({
121
+ api,
122
+ fromUser,
123
+ accountId,
124
+ sessionId,
125
+ text,
126
+ reason: "late-reply",
127
+ });
128
+ return;
129
+ }
130
+ await sendTextToUser(text);
131
+ },
104
132
  onFailureFallback: async (err) => sendFailureFallback(err),
105
133
  }).finally(() => {
106
134
  lateReplyWatcherPromise = null;
@@ -12,6 +12,7 @@ export function createWecomAgentTextSender({
12
12
  toUser,
13
13
  logger,
14
14
  proxyUrl,
15
+ apiBaseUrl,
15
16
  } = {}) {
16
17
  assertFunction("sendWecomText", sendWecomText);
17
18
 
@@ -24,6 +25,7 @@ export function createWecomAgentTextSender({
24
25
  text,
25
26
  logger,
26
27
  proxyUrl,
28
+ apiBaseUrl,
27
29
  });
28
30
  };
29
31
  }
@@ -1,3 +1,5 @@
1
+ import { buildWecomApiUrl, isWecomApiUrl, normalizeWecomApiBaseUrl } from "./network-config.js";
2
+
1
3
  export function createWecomApiClientCore({
2
4
  fetchImpl = fetch,
3
5
  proxyAgentCtor,
@@ -11,17 +13,6 @@ export function createWecomApiClientCore({
11
13
  const proxyDispatcherCache = new Map();
12
14
  const invalidProxyCache = new Set();
13
15
 
14
- function isWecomApiUrl(url) {
15
- const raw = typeof url === "string" ? url : String(url ?? "");
16
- if (!raw) return false;
17
- try {
18
- const parsed = new URL(raw);
19
- return parsed.hostname === "qyapi.weixin.qq.com";
20
- } catch {
21
- return raw.includes("qyapi.weixin.qq.com");
22
- }
23
- }
24
-
25
16
  function isLikelyHttpProxyUrl(proxyUrl) {
26
17
  return /^https?:\/\/\S+$/i.test(proxyUrl);
27
18
  }
@@ -73,11 +64,11 @@ export function createWecomApiClientCore({
73
64
 
74
65
  function attachWecomProxyDispatcher(url, options = {}, { proxyUrl, logger } = {}) {
75
66
  const shouldForceProxy = options?.forceProxy === true;
76
- if (!isWecomApiUrl(url) && !shouldForceProxy) return options;
67
+ if (!isWecomApiUrl(url, { apiBaseUrl: options?.apiBaseUrl }) && !shouldForceProxy) return options;
77
68
  if (options?.dispatcher) return options;
78
69
  const dispatcher = resolveWecomProxyDispatcher(proxyUrl, logger);
79
70
  if (!dispatcher) return options;
80
- const { forceProxy, ...restOptions } = options || {};
71
+ const { forceProxy, apiBaseUrl, ...restOptions } = options || {};
81
72
  return {
82
73
  ...restOptions,
83
74
  dispatcher,
@@ -120,8 +111,9 @@ export function createWecomApiClientCore({
120
111
  throw lastError || new Error(`Fetch failed after ${maxRetries} retries`);
121
112
  }
122
113
 
123
- async function getWecomAccessToken({ corpId, corpSecret, proxyUrl, logger }) {
124
- const cacheKey = `${corpId}:${corpSecret}`;
114
+ async function getWecomAccessToken({ corpId, corpSecret, proxyUrl, logger, apiBaseUrl }) {
115
+ const normalizedApiBaseUrl = normalizeWecomApiBaseUrl(apiBaseUrl);
116
+ const cacheKey = `${corpId}:${corpSecret}:${normalizedApiBaseUrl}`;
125
117
  let cache = accessTokenCaches.get(cacheKey);
126
118
 
127
119
  if (!cache) {
@@ -140,8 +132,17 @@ export function createWecomApiClientCore({
140
132
 
141
133
  cache.refreshPromise = (async () => {
142
134
  try {
143
- const tokenUrl = `https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=${encodeURIComponent(corpId)}&corpsecret=${encodeURIComponent(corpSecret)}`;
144
- const tokenRes = await fetchWithRetry(tokenUrl, {}, 3, 1000, { proxyUrl, logger });
135
+ const tokenUrl = buildWecomApiUrl(
136
+ `/cgi-bin/gettoken?corpid=${encodeURIComponent(corpId)}&corpsecret=${encodeURIComponent(corpSecret)}`,
137
+ { apiBaseUrl: normalizedApiBaseUrl },
138
+ );
139
+ const tokenRes = await fetchWithRetry(
140
+ tokenUrl,
141
+ { apiBaseUrl: normalizedApiBaseUrl },
142
+ 3,
143
+ 1000,
144
+ { proxyUrl, logger },
145
+ );
145
146
  const tokenJson = await tokenRes.json();
146
147
  if (!tokenJson?.access_token) {
147
148
  throw new Error(`WeCom gettoken failed: ${JSON.stringify(tokenJson)}`);
@@ -166,6 +167,7 @@ export function createWecomApiClientCore({
166
167
  chatId,
167
168
  msgType,
168
169
  payload,
170
+ apiBaseUrl,
169
171
  }) {
170
172
  const isAppChat = Boolean(chatId);
171
173
  if (!isAppChat && !toUser && !toParty && !toTag) {
@@ -173,7 +175,10 @@ export function createWecomApiClientCore({
173
175
  }
174
176
  if (isAppChat) {
175
177
  return {
176
- sendUrl: `https://qyapi.weixin.qq.com/cgi-bin/appchat/send?access_token=${encodeURIComponent(accessToken)}`,
178
+ sendUrl: buildWecomApiUrl(
179
+ `/cgi-bin/appchat/send?access_token=${encodeURIComponent(accessToken)}`,
180
+ { apiBaseUrl },
181
+ ),
177
182
  body: {
178
183
  chatid: chatId,
179
184
  msgtype: msgType,
@@ -184,7 +189,10 @@ export function createWecomApiClientCore({
184
189
  };
185
190
  }
186
191
  return {
187
- sendUrl: `https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${encodeURIComponent(accessToken)}`,
192
+ sendUrl: buildWecomApiUrl(
193
+ `/cgi-bin/message/send?access_token=${encodeURIComponent(accessToken)}`,
194
+ { apiBaseUrl },
195
+ ),
188
196
  body: {
189
197
  touser: toUser,
190
198
  toparty: toParty,