@dingxiang-me/openclaw-wechat 1.7.2 → 2.0.1

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 (65) hide show
  1. package/CHANGELOG.md +160 -0
  2. package/README.en.md +379 -11
  3. package/README.md +620 -12
  4. package/docs/channels/wecom.md +181 -3
  5. package/openclaw.plugin.json +148 -5
  6. package/package.json +9 -5
  7. package/src/core/delivery-router.js +2 -0
  8. package/src/core/stream-manager.js +13 -2
  9. package/src/core.js +96 -6
  10. package/src/wecom/account-config-core.js +2 -0
  11. package/src/wecom/account-config.js +12 -3
  12. package/src/wecom/agent-context.js +7 -1
  13. package/src/wecom/agent-dispatch-executor.js +13 -1
  14. package/src/wecom/agent-dispatch-fallback.js +23 -0
  15. package/src/wecom/agent-inbound-dispatch.js +1 -1
  16. package/src/wecom/agent-inbound-processor.js +33 -2
  17. package/src/wecom/agent-late-reply-runtime.js +31 -1
  18. package/src/wecom/agent-runtime-context.js +3 -0
  19. package/src/wecom/agent-webhook-handler.js +5 -0
  20. package/src/wecom/api-client-core.js +1 -1
  21. package/src/wecom/api-client-send-text.js +43 -20
  22. package/src/wecom/bot-context.js +7 -1
  23. package/src/wecom/bot-dispatch-fallback.js +34 -3
  24. package/src/wecom/bot-dispatch-handlers.js +47 -4
  25. package/src/wecom/bot-inbound-content.js +14 -6
  26. package/src/wecom/bot-inbound-dispatch-runtime.js +10 -0
  27. package/src/wecom/bot-inbound-executor-helpers.js +44 -11
  28. package/src/wecom/bot-inbound-executor.js +40 -0
  29. package/src/wecom/bot-long-connection-manager.js +983 -0
  30. package/src/wecom/bot-reply-runtime.js +36 -6
  31. package/src/wecom/bot-runtime-context.js +3 -0
  32. package/src/wecom/bot-state-store.js +4 -5
  33. package/src/wecom/bot-webhook-dispatch.js +7 -0
  34. package/src/wecom/bot-webhook-handler.js +5 -0
  35. package/src/wecom/callback-health-diagnostics.js +86 -0
  36. package/src/wecom/channel-config-schema.js +242 -0
  37. package/src/wecom/channel-plugin.js +162 -4
  38. package/src/wecom/channel-status-state.js +150 -0
  39. package/src/wecom/command-handlers.js +6 -0
  40. package/src/wecom/command-status-text.js +32 -3
  41. package/src/wecom/doc-client.js +537 -0
  42. package/src/wecom/doc-schema.js +380 -0
  43. package/src/wecom/doc-tool.js +833 -0
  44. package/src/wecom/outbound-active-stream.js +17 -10
  45. package/src/wecom/outbound-delivery.js +46 -0
  46. package/src/wecom/outbound-webhook-sender.js +39 -16
  47. package/src/wecom/plugin-account-policy-services.js +4 -1
  48. package/src/wecom/plugin-composition.js +2 -0
  49. package/src/wecom/plugin-constants.js +1 -1
  50. package/src/wecom/plugin-delivery-inbound-services.js +4 -0
  51. package/src/wecom/plugin-processing-deps.js +5 -0
  52. package/src/wecom/plugin-route-runtime-deps.js +2 -0
  53. package/src/wecom/plugin-services.js +37 -0
  54. package/src/wecom/register-runtime.js +20 -1
  55. package/src/wecom/request-parsers.js +1 -0
  56. package/src/wecom/route-registration.js +4 -1
  57. package/src/wecom/session-reset.js +168 -0
  58. package/src/wecom/target-utils.js +41 -5
  59. package/src/wecom/text-format.js +22 -5
  60. package/src/wecom/text-inbound-scheduler.js +1 -1
  61. package/src/wecom/thinking-parser.js +74 -0
  62. package/src/wecom/voice-transcription-process.js +145 -11
  63. package/src/wecom/voice-transcription.js +14 -2
  64. package/src/wecom/webhook-adapter-normalize.js +29 -0
  65. package/src/wecom/webhook-adapter.js +294 -59
@@ -25,6 +25,7 @@ export function createWecomActiveStreamDeliverer({
25
25
  streamId,
26
26
  sessionId,
27
27
  content = "",
28
+ thinkingContent = "",
28
29
  normalizedMediaUrls = [],
29
30
  mediaType,
30
31
  normalizedText = "",
@@ -78,13 +79,17 @@ export function createWecomActiveStreamDeliverer({
78
79
  }
79
80
 
80
81
  let streamContent = String(content ?? "").trim();
82
+ const normalizedThinkingContent = String(thinkingContent ?? "").trim();
81
83
  if (!streamContent) {
82
- streamContent =
83
- fallbackMediaUrls.length > 0
84
- ? fallbackText
85
- : streamMsgItem.length > 0
86
- ? "已收到模型返回的媒体结果。"
87
- : "";
84
+ if (fallbackMediaUrls.length > 0) {
85
+ streamContent = fallbackText;
86
+ } else if (streamMsgItem.length > 0) {
87
+ streamContent = "已收到模型返回的媒体结果。";
88
+ } else if (normalizedThinkingContent) {
89
+ streamContent = "";
90
+ } else {
91
+ streamContent = "";
92
+ }
88
93
  }
89
94
  if (!normalizedText && streamMsgItem.length > 0 && fallbackMediaUrls.length === 0 && streamContent === fallbackText) {
90
95
  streamContent = "已收到模型返回的媒体结果。";
@@ -93,13 +98,15 @@ export function createWecomActiveStreamDeliverer({
93
98
  const suffix = `\n\n媒体链接:\n${fallbackMediaUrls.join("\n")}`;
94
99
  streamContent = `${streamContent}${suffix}`.trim();
95
100
  }
96
- if (!streamContent) {
101
+ if (!streamContent && !normalizedThinkingContent) {
97
102
  streamContent = "已收到模型返回的结果。";
98
103
  }
99
104
 
100
- finishBotStream(targetStreamId, streamContent, {
101
- msgItem: streamMsgItem,
102
- });
105
+ const finishOptions = { msgItem: streamMsgItem };
106
+ if (normalizedThinkingContent) {
107
+ finishOptions.thinkingContent = normalizedThinkingContent;
108
+ }
109
+ finishBotStream(targetStreamId, streamContent, finishOptions);
103
110
  return {
104
111
  ok: true,
105
112
  meta: {
@@ -32,6 +32,8 @@ export function createWecomBotReplyDeliverer({
32
32
  resolveWecomObservabilityPolicy,
33
33
  resolveWecomBotProxyConfig,
34
34
  resolveWecomBotConfig,
35
+ resolveWecomBotLongConnectionReplyContext,
36
+ pushWecomBotLongConnectionStreamUpdate,
35
37
  buildWecomBotSessionId,
36
38
  upsertBotResponseUrlCache,
37
39
  getBotResponseUrlCache,
@@ -60,6 +62,8 @@ export function createWecomBotReplyDeliverer({
60
62
  assertFunction("resolveWecomObservabilityPolicy", resolveWecomObservabilityPolicy);
61
63
  assertFunction("resolveWecomBotProxyConfig", resolveWecomBotProxyConfig);
62
64
  assertFunction("resolveWecomBotConfig", resolveWecomBotConfig);
65
+ assertFunction("resolveWecomBotLongConnectionReplyContext", resolveWecomBotLongConnectionReplyContext);
66
+ assertFunction("pushWecomBotLongConnectionStreamUpdate", pushWecomBotLongConnectionStreamUpdate);
63
67
  assertFunction("buildWecomBotSessionId", buildWecomBotSessionId);
64
68
  assertFunction("upsertBotResponseUrlCache", upsertBotResponseUrlCache);
65
69
  assertFunction("getBotResponseUrlCache", getBotResponseUrlCache);
@@ -163,6 +167,7 @@ export function createWecomBotReplyDeliverer({
163
167
  streamId,
164
168
  responseUrl,
165
169
  text,
170
+ thinkingContent = "",
166
171
  routeAgentId = "",
167
172
  mediaUrl,
168
173
  mediaUrls,
@@ -209,17 +214,57 @@ export function createWecomBotReplyDeliverer({
209
214
  });
210
215
  }
211
216
  const cachedResponseUrl = getBotResponseUrlCache(normalizedSessionId);
217
+ const longConnectionContext = resolveWecomBotLongConnectionReplyContext({
218
+ accountId: normalizedAccountId,
219
+ sessionId: normalizedSessionId,
220
+ streamId,
221
+ });
212
222
  const traceId = createDeliveryTraceId("wecom-bot");
213
223
  const router = createWecomDeliveryRouter({
214
224
  logger: api.logger,
215
225
  fallbackConfig: fallbackPolicy,
216
226
  observability: observabilityPolicy,
217
227
  handlers: {
228
+ long_connection: async ({ text: content }) => {
229
+ let streamMsgItem = [];
230
+ let fallbackMediaUrls = normalizedMediaUrls;
231
+ if (normalizedMediaUrls.length > 0) {
232
+ const processed = await buildActiveStreamMsgItems({
233
+ mediaUrls: normalizedMediaUrls,
234
+ mediaType,
235
+ fetchMediaFromUrl,
236
+ proxyUrl: botProxyUrl,
237
+ logger: api.logger,
238
+ });
239
+ streamMsgItem = processed.msgItem;
240
+ fallbackMediaUrls = processed.fallbackUrls;
241
+ }
242
+ let streamContent = String(content ?? "").trim();
243
+ if (!streamContent && fallbackMediaUrls.length > 0) {
244
+ streamContent = fallbackText;
245
+ }
246
+ if (fallbackMediaUrls.length > 0) {
247
+ streamContent = `${streamContent}\n\n媒体链接:\n${fallbackMediaUrls.join("\n")}`.trim();
248
+ }
249
+ if (!streamContent && !streamMsgItem.length && !String(thinkingContent ?? "").trim()) {
250
+ streamContent = fallbackText;
251
+ }
252
+ return pushWecomBotLongConnectionStreamUpdate({
253
+ accountId: normalizedAccountId,
254
+ sessionId: normalizedSessionId,
255
+ streamId,
256
+ content: streamContent,
257
+ finish: true,
258
+ msgItem: streamMsgItem,
259
+ thinkingContent,
260
+ });
261
+ },
218
262
  active_stream: async ({ text: content }) => {
219
263
  return deliverActiveStreamReply({
220
264
  streamId,
221
265
  sessionId: normalizedSessionId,
222
266
  content,
267
+ thinkingContent,
223
268
  normalizedMediaUrls,
224
269
  mediaType,
225
270
  normalizedText,
@@ -283,6 +328,7 @@ export function createWecomBotReplyDeliverer({
283
328
  streamId: streamId || "",
284
329
  hasResponseUrl: Boolean(inlineResponseUrl || cachedResponseUrl?.url),
285
330
  mediaCount: normalizedMediaUrls.length,
331
+ hasThinkingContent: Boolean(String(thinkingContent ?? "").trim()),
286
332
  botCardMode: botModeConfig?.card?.enabled ? botModeConfig.card.mode : "off",
287
333
  },
288
334
  });
@@ -32,6 +32,26 @@ export function createWecomWebhookOutboundSender({
32
32
  assertFunction("createHash", createHash);
33
33
  assertFunction("sleep", sleep);
34
34
 
35
+ const webhookSendChains = new Map();
36
+
37
+ function buildWebhookTargetKey({ target, sendUrl }) {
38
+ return [String(target?.url ?? "").trim(), String(target?.key ?? "").trim(), String(sendUrl ?? "").trim()]
39
+ .filter(Boolean)
40
+ .join("|");
41
+ }
42
+
43
+ async function enqueueWebhookSend(targetKey, task) {
44
+ const previous = webhookSendChains.get(targetKey) || Promise.resolve();
45
+ const run = previous.catch(() => {}).then(task);
46
+ const tracked = run.finally(() => {
47
+ if (webhookSendChains.get(targetKey) === tracked) {
48
+ webhookSendChains.delete(targetKey);
49
+ }
50
+ });
51
+ webhookSendChains.set(targetKey, tracked);
52
+ return run;
53
+ }
54
+
35
55
  function resolveWebhookSendContext({ webhook, webhookTargets, proxyUrl, logger }) {
36
56
  const target = resolveWecomWebhookTargetConfig(webhook, webhookTargets);
37
57
  if (!target) {
@@ -45,31 +65,34 @@ export function createWecomWebhookOutboundSender({
45
65
  throw new Error("invalid webhook target url/key");
46
66
  }
47
67
  const dispatcher = attachWecomProxyDispatcher(sendUrl, {}, { proxyUrl, logger })?.dispatcher;
48
- return { target, dispatcher };
68
+ return { target, dispatcher, sendUrl };
49
69
  }
50
70
 
51
71
  async function sendWecomWebhookText({ webhook, webhookTargets, text, logger, proxyUrl }) {
52
- const { target, dispatcher } = resolveWebhookSendContext({
72
+ const { target, dispatcher, sendUrl } = resolveWebhookSendContext({
53
73
  webhook,
54
74
  webhookTargets,
55
75
  proxyUrl,
56
76
  logger,
57
77
  });
58
- const chunks = splitWecomText(String(text ?? ""));
59
- for (let i = 0; i < chunks.length; i += 1) {
60
- await webhookSendText({
61
- url: target.url,
62
- key: target.key,
63
- content: chunks[i],
64
- timeoutMs: 15000,
65
- dispatcher,
66
- fetchImpl,
67
- });
68
- if (i < chunks.length - 1) {
69
- await sleep(200);
78
+ const targetKey = buildWebhookTargetKey({ target, sendUrl });
79
+ return enqueueWebhookSend(targetKey, async () => {
80
+ const chunks = splitWecomText(String(text ?? ""));
81
+ for (let i = 0; i < chunks.length; i += 1) {
82
+ await webhookSendText({
83
+ url: target.url,
84
+ key: target.key,
85
+ content: chunks[i],
86
+ timeoutMs: 15000,
87
+ dispatcher,
88
+ fetchImpl,
89
+ });
90
+ if (i < chunks.length - 1) {
91
+ await sleep(200);
92
+ }
70
93
  }
71
- }
72
- logger?.info?.(`wecom: webhook text sent chunks=${chunks.length}`);
94
+ logger?.info?.(`wecom: webhook text sent chunks=${chunks.length}`);
95
+ });
73
96
  }
74
97
 
75
98
  async function sendWecomWebhookMediaBatch({
@@ -107,7 +107,8 @@ export function createWecomPluginAccountPolicyServices({
107
107
  processEnv,
108
108
  });
109
109
 
110
- const { resolveWecomVoiceTranscriptionConfig, transcribeInboundVoice } = createWecomVoiceTranscriber({
110
+ const { resolveWecomVoiceTranscriptionConfig, transcribeInboundVoice, inspectWecomVoiceTranscriptionRuntime } =
111
+ createWecomVoiceTranscriber({
111
112
  tempDirName: WECOM_TEMP_DIR_NAME,
112
113
  resolveVoiceTranscriptionConfig,
113
114
  normalizeAudioContentType,
@@ -124,6 +125,7 @@ export function createWecomPluginAccountPolicyServices({
124
125
  listWebhookTargetAliases,
125
126
  listAllWebhookTargetAliases,
126
127
  resolveWecomVoiceTranscriptionConfig,
128
+ inspectWecomVoiceTranscriptionRuntime,
127
129
  resolveWecomCommandPolicy,
128
130
  resolveWecomAllowFromPolicy,
129
131
  resolveWecomDmPolicy,
@@ -165,6 +167,7 @@ export function createWecomPluginAccountPolicyServices({
165
167
  resolveWecomObservabilityPolicy,
166
168
  resolveWecomDynamicAgentPolicy,
167
169
  resolveWecomVoiceTranscriptionConfig,
170
+ inspectWecomVoiceTranscriptionRuntime,
168
171
  transcribeInboundVoice,
169
172
  COMMANDS,
170
173
  buildWecomBotHelpText,
@@ -12,6 +12,8 @@ const processingDeps = createPluginProcessingDeps({
12
12
  const { processBotInboundMessage, processInboundMessage, scheduleTextInboundProcessing } =
13
13
  createWecomPluginProcessingPipeline(processingDeps);
14
14
 
15
+ services.setWecomBotLongConnectionInboundProcessor(processBotInboundMessage);
16
+
15
17
  const routeRuntimeDeps = createPluginRouteRuntimeDeps({
16
18
  ...services,
17
19
  processBotInboundMessage,
@@ -1,5 +1,5 @@
1
1
  export const MAX_REQUEST_BODY_SIZE = 1024 * 1024;
2
- export const PLUGIN_VERSION = "1.7.1";
2
+ export const PLUGIN_VERSION = "2.0.1";
3
3
  export const WECOM_TEMP_DIR_NAME = "openclaw-wechat";
4
4
  export const WECOM_TEMP_FILE_RETENTION_MS = 30 * 60 * 1000;
5
5
  export const WECOM_MIN_FILE_SIZE = 5;
@@ -17,6 +17,8 @@ export function createWecomPluginDeliveryInboundServices({
17
17
  resolveWecomObservabilityPolicy,
18
18
  resolveWecomBotProxyConfig,
19
19
  resolveWecomBotConfig,
20
+ resolveWecomBotLongConnectionReplyContext,
21
+ pushWecomBotLongConnectionStreamUpdate,
20
22
  upsertBotResponseUrlCache,
21
23
  getBotResponseUrlCache,
22
24
  markBotResponseUrlUsed,
@@ -53,6 +55,8 @@ export function createWecomPluginDeliveryInboundServices({
53
55
  resolveWecomObservabilityPolicy,
54
56
  resolveWecomBotProxyConfig,
55
57
  resolveWecomBotConfig,
58
+ resolveWecomBotLongConnectionReplyContext,
59
+ pushWecomBotLongConnectionStreamUpdate,
56
60
  buildWecomBotSessionId,
57
61
  upsertBotResponseUrlCache,
58
62
  getBotResponseUrlCache,
@@ -42,9 +42,12 @@ export function createPluginProcessingDeps(context = {}) {
42
42
  isDispatchTimeoutError: context.isDispatchTimeoutError,
43
43
  queueBotStreamMedia: context.queueBotStreamMedia,
44
44
  updateBotStream: context.updateBotStream,
45
+ pushWecomBotLongConnectionStreamUpdate: context.pushWecomBotLongConnectionStreamUpdate,
45
46
  isAgentFailureText: context.isAgentFailureText,
46
47
  scheduleTempFileCleanup: context.scheduleTempFileCleanup,
47
48
  ACTIVE_LATE_REPLY_WATCHERS: context.ACTIVE_LATE_REPLY_WATCHERS,
49
+ resetWecomConversationSession: context.resetWecomConversationSession,
50
+ clearSessionStoreEntry: context.clearSessionStoreEntry,
48
51
  },
49
52
  agentInboundDeps: {
50
53
  getWecomConfig: context.getWecomConfig,
@@ -83,6 +86,8 @@ export function createPluginProcessingDeps(context = {}) {
83
86
  isAgentFailureText: context.isAgentFailureText,
84
87
  scheduleTempFileCleanup: context.scheduleTempFileCleanup,
85
88
  ACTIVE_LATE_REPLY_WATCHERS: context.ACTIVE_LATE_REPLY_WATCHERS,
89
+ resetWecomConversationSession: context.resetWecomConversationSession,
90
+ clearSessionStoreEntry: context.clearSessionStoreEntry,
86
91
  },
87
92
  textSchedulerDeps: {
88
93
  resolveWecomGroupChatPolicy: context.resolveWecomGroupChatPolicy,
@@ -51,9 +51,11 @@ export function createPluginRouteRuntimeDeps(context = {}) {
51
51
  resolveWecomDynamicAgentPolicy: context.resolveWecomDynamicAgentPolicy,
52
52
  resolveWecomBotConfig: context.resolveWecomBotConfig,
53
53
  resolveWecomBotConfigs: context.resolveWecomBotConfigs,
54
+ syncWecomBotLongConnections: context.syncWecomBotLongConnections,
54
55
  listEnabledWecomAccounts: context.listEnabledWecomAccounts,
55
56
  getWecomConfig: context.getWecomConfig,
56
57
  wecomChannelPlugin: context.wecomChannelPlugin,
58
+ registerWecomDocTools: context.registerWecomDocTools,
57
59
  },
58
60
  };
59
61
  }
@@ -42,6 +42,9 @@ import { createWecomPluginBaseServices } from "./plugin-base-services.js";
42
42
  import { createWecomPluginAccountPolicyServices } from "./plugin-account-policy-services.js";
43
43
  import { createWecomPluginDeliveryInboundServices } from "./plugin-delivery-inbound-services.js";
44
44
  import { createWecomBotInboundContentBuilder } from "./bot-inbound-content.js";
45
+ import { createWecomBotLongConnectionManager } from "./bot-long-connection-manager.js";
46
+ import { createWecomDocToolRegistrar } from "./doc-tool.js";
47
+ import { createWecomSessionResetter } from "./session-reset.js";
45
48
  import { markdownToWecomText } from "./text-format.js";
46
49
  import {
47
50
  buildWecomSessionId,
@@ -91,6 +94,8 @@ export function createWecomPluginServices({
91
94
  resolveWecomObservabilityPolicy: accountPolicy.resolveWecomObservabilityPolicy,
92
95
  resolveWecomBotProxyConfig: accountPolicy.resolveWecomBotProxyConfig,
93
96
  resolveWecomBotConfig: accountPolicy.resolveWecomBotConfig,
97
+ resolveWecomBotLongConnectionReplyContext: (...args) => wecomBotLongConnectionManager.resolveReplyContext(...args),
98
+ pushWecomBotLongConnectionStreamUpdate: (...args) => wecomBotLongConnectionManager.pushStreamUpdate(...args),
94
99
  upsertBotResponseUrlCache: base.upsertBotResponseUrlCache,
95
100
  getBotResponseUrlCache: base.getBotResponseUrlCache,
96
101
  markBotResponseUrlUsed: base.markBotResponseUrlUsed,
@@ -124,12 +129,44 @@ export function createWecomPluginServices({
124
129
  writeFile,
125
130
  WECOM_TEMP_DIR_NAME,
126
131
  });
132
+ const registerWecomDocTools = createWecomDocToolRegistrar({
133
+ listEnabledWecomAccounts: accountPolicy.listEnabledWecomAccounts,
134
+ normalizeAccountId: accountPolicy.normalizeAccountId,
135
+ fetchWithRetry: base.fetchWithRetry,
136
+ getWecomAccessToken: base.getWecomAccessToken,
137
+ });
138
+ const { resetWecomConversationSession, clearSessionStoreEntry } = createWecomSessionResetter();
139
+ const wecomBotLongConnectionManager = createWecomBotLongConnectionManager({
140
+ attachWecomProxyDispatcher: base.attachWecomProxyDispatcher,
141
+ resolveWecomBotConfigs: accountPolicy.resolveWecomBotConfigs,
142
+ resolveWecomBotProxyConfig: accountPolicy.resolveWecomBotProxyConfig,
143
+ parseWecomBotInboundMessage,
144
+ describeWecomBotParsedMessage,
145
+ buildWecomBotSessionId,
146
+ createBotStream: base.createBotStream,
147
+ upsertBotResponseUrlCache: base.upsertBotResponseUrlCache,
148
+ markInboundMessageSeen,
149
+ messageProcessLimiter: base.messageProcessLimiter,
150
+ executeInboundTaskWithSessionQueue: deliveryInbound.executeInboundTaskWithSessionQueue,
151
+ deliverBotReplyText: deliveryInbound.deliverBotReplyText,
152
+ recordInboundMetric: base.recordInboundMetric,
153
+ recordRuntimeErrorMetric: base.recordRuntimeErrorMetric,
154
+ });
127
155
 
128
156
  return {
129
157
  ...base,
130
158
  ...accountPolicy,
131
159
  ...deliveryInbound,
132
160
  buildBotInboundContent,
161
+ registerWecomDocTools,
162
+ resetWecomConversationSession,
163
+ clearSessionStoreEntry,
164
+ setWecomBotLongConnectionInboundProcessor: wecomBotLongConnectionManager.setProcessBotInboundHandler,
165
+ resolveWecomBotLongConnectionReplyContext: wecomBotLongConnectionManager.resolveReplyContext,
166
+ pushWecomBotLongConnectionStreamUpdate: wecomBotLongConnectionManager.pushStreamUpdate,
167
+ syncWecomBotLongConnections: wecomBotLongConnectionManager.sync,
168
+ stopAllWecomBotLongConnections: wecomBotLongConnectionManager.stopAll,
169
+ getWecomBotLongConnectionState: wecomBotLongConnectionManager.getConnectionState,
133
170
  ACTIVE_LATE_REPLY_WATCHERS,
134
171
  WECOM_TEMP_DIR_NAME,
135
172
  normalizePluginHttpPath,
@@ -9,10 +9,12 @@ export function createWecomRegisterRuntime({
9
9
  resolveWecomDynamicAgentPolicy,
10
10
  resolveWecomBotConfig,
11
11
  resolveWecomBotConfigs,
12
+ syncWecomBotLongConnections,
12
13
  listEnabledWecomAccounts,
13
14
  getWecomConfig,
14
15
  wecomChannelPlugin,
15
16
  wecomRouteRegistrar,
17
+ registerWecomDocTools,
16
18
  } = {}) {
17
19
  if (typeof setGatewayRuntime !== "function") {
18
20
  throw new Error("createWecomRegisterRuntime: setGatewayRuntime is required");
@@ -38,6 +40,9 @@ export function createWecomRegisterRuntime({
38
40
  if (resolveWecomBotConfigs != null && typeof resolveWecomBotConfigs !== "function") {
39
41
  throw new Error("createWecomRegisterRuntime: resolveWecomBotConfigs must be a function");
40
42
  }
43
+ if (syncWecomBotLongConnections != null && typeof syncWecomBotLongConnections !== "function") {
44
+ throw new Error("createWecomRegisterRuntime: syncWecomBotLongConnections must be a function");
45
+ }
41
46
  if (listEnabledWecomAccounts != null && typeof listEnabledWecomAccounts !== "function") {
42
47
  throw new Error("createWecomRegisterRuntime: listEnabledWecomAccounts must be a function");
43
48
  }
@@ -50,6 +55,9 @@ export function createWecomRegisterRuntime({
50
55
  if (!wecomRouteRegistrar || typeof wecomRouteRegistrar !== "object") {
51
56
  throw new Error("createWecomRegisterRuntime: wecomRouteRegistrar is required");
52
57
  }
58
+ if (registerWecomDocTools != null && typeof registerWecomDocTools !== "function") {
59
+ throw new Error("createWecomRegisterRuntime: registerWecomDocTools must be a function");
60
+ }
53
61
 
54
62
  function register(api) {
55
63
  setGatewayRuntime(api.runtime);
@@ -91,6 +99,14 @@ export function createWecomRegisterRuntime({
91
99
  `wecom: webhookBot fallback enabled (${webhookBotPolicy.url || webhookBotPolicy.key ? "configured" : "missing-url"})`,
92
100
  );
93
101
  }
102
+ let longConnectionStarted = 0;
103
+ if (typeof syncWecomBotLongConnections === "function") {
104
+ const longConnectionResult = syncWecomBotLongConnections(api);
105
+ longConnectionStarted = Number(longConnectionResult?.started) || 0;
106
+ if (longConnectionStarted > 0) {
107
+ api.logger.info?.(`wecom(bot-longconn): enabled accounts=${longConnectionStarted}`);
108
+ }
109
+ }
94
110
  if (observabilityPolicy.enabled) {
95
111
  api.logger.info?.(
96
112
  `wecom: observability enabled (payloadMeta=${observabilityPolicy.logPayloadMeta ? "on" : "off"})`,
@@ -114,9 +130,12 @@ export function createWecomRegisterRuntime({
114
130
  }
115
131
 
116
132
  api.registerChannel({ plugin: wecomChannelPlugin });
133
+ if (typeof registerWecomDocTools === "function") {
134
+ registerWecomDocTools(api);
135
+ }
117
136
  const botRouteRegistered = wecomRouteRegistrar.registerWecomBotWebhookRoute(api);
118
137
  const webhookGroups = wecomRouteRegistrar.registerWecomAgentWebhookRoutes(api);
119
- if (webhookGroups.size === 0 && !botRouteRegistered) {
138
+ if (webhookGroups.size === 0 && !botRouteRegistered && longConnectionStarted === 0) {
120
139
  api.logger.warn?.("wecom: no enabled account with valid config found; webhook route not registered");
121
140
  return;
122
141
  }
@@ -4,6 +4,7 @@ const DEFAULT_XML_PARSER_OPTIONS = {
4
4
  ignoreAttributes: false,
5
5
  trimValues: true,
6
6
  processEntities: false, // 防止 XXE
7
+ parseTagValue: false, // 保留前导零,避免 FromUserName/MsgId 等字段被自动转数值
7
8
  };
8
9
 
9
10
  export const DEFAULT_MAX_REQUEST_BODY_SIZE = 1024 * 1024;
@@ -70,7 +70,10 @@ export function createWecomRouteRegistrar({
70
70
 
71
71
  const signedBotConfigs = enabledBotConfigs.filter((item) => item?.token && item?.encodingAesKey);
72
72
  if (signedBotConfigs.length === 0) {
73
- api.logger.warn?.("wecom(bot): enabled but missing token/encodingAesKey; route not registered");
73
+ const longConnectionOnly = enabledBotConfigs.some((item) => item?.longConnection?.enabled === true);
74
+ if (!longConnectionOnly) {
75
+ api.logger.warn?.("wecom(bot): enabled but missing token/encodingAesKey; route not registered");
76
+ }
74
77
  return false;
75
78
  }
76
79
 
@@ -0,0 +1,168 @@
1
+ import { access, readFile, rename, writeFile } from "node:fs/promises";
2
+
3
+ function normalizeText(value) {
4
+ return String(value ?? "").trim();
5
+ }
6
+
7
+ function normalizeToken(value) {
8
+ return normalizeText(value).toLowerCase();
9
+ }
10
+
11
+ function resolveAgentIdFromSessionKey(sessionKey, fallback = "main") {
12
+ const match = /^agent:([^:]+):/i.exec(normalizeText(sessionKey));
13
+ const agentId = normalizeText(match?.[1]);
14
+ return agentId || normalizeText(fallback) || "main";
15
+ }
16
+
17
+ function resolveStoreEntryKey(store, sessionKey) {
18
+ if (!store || typeof store !== "object") return "";
19
+ const directKey = normalizeText(sessionKey);
20
+ if (directKey && Object.prototype.hasOwnProperty.call(store, directKey)) return directKey;
21
+ const normalizedKey = normalizeToken(sessionKey);
22
+ if (!normalizedKey) return "";
23
+ return Object.keys(store).find((key) => normalizeToken(key) === normalizedKey) || "";
24
+ }
25
+
26
+ async function readJsonObject(filePath) {
27
+ try {
28
+ const raw = await readFile(filePath, "utf8");
29
+ const parsed = JSON.parse(raw);
30
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
31
+ } catch (err) {
32
+ if (err?.code === "ENOENT") return {};
33
+ throw err;
34
+ }
35
+ }
36
+
37
+ async function persistJsonObject(filePath, payload) {
38
+ const serialized = `${JSON.stringify(payload, null, 2)}\n`;
39
+ await writeFile(filePath, serialized, "utf8");
40
+ }
41
+
42
+ async function archiveTranscriptFile(sessionFile, dateNow = Date.now) {
43
+ const normalizedPath = normalizeText(sessionFile);
44
+ if (!normalizedPath) return { archived: false, archivedPath: "" };
45
+ try {
46
+ await access(normalizedPath);
47
+ } catch (err) {
48
+ if (err?.code === "ENOENT") return { archived: false, archivedPath: "" };
49
+ throw err;
50
+ }
51
+ const archivedPath = `${normalizedPath}.reset-${dateNow()}`;
52
+ await rename(normalizedPath, archivedPath);
53
+ return { archived: true, archivedPath };
54
+ }
55
+
56
+ export function createWecomSessionResetter({ dateNow = Date.now } = {}) {
57
+ async function clearSessionStoreEntry({ storePath, sessionKey, logger } = {}) {
58
+ const normalizedStorePath = normalizeText(storePath);
59
+ const normalizedSessionKey = normalizeText(sessionKey);
60
+ if (!normalizedStorePath || !normalizedSessionKey) {
61
+ return { cleared: false, transcriptArchived: false, archivedTranscriptPath: "" };
62
+ }
63
+
64
+ const store = await readJsonObject(normalizedStorePath);
65
+ const entryKey = resolveStoreEntryKey(store, normalizedSessionKey);
66
+ if (!entryKey) {
67
+ return { cleared: false, transcriptArchived: false, archivedTranscriptPath: "" };
68
+ }
69
+
70
+ const entry = store?.[entryKey] && typeof store[entryKey] === "object" ? store[entryKey] : {};
71
+ delete store[entryKey];
72
+ await persistJsonObject(normalizedStorePath, store);
73
+
74
+ let archivedTranscriptPath = "";
75
+ let transcriptArchived = false;
76
+ try {
77
+ const archived = await archiveTranscriptFile(entry?.sessionFile, dateNow);
78
+ transcriptArchived = archived.archived === true;
79
+ archivedTranscriptPath = archived.archivedPath || "";
80
+ } catch (err) {
81
+ logger?.warn?.(
82
+ `wecom: failed to archive transcript during local reset session=${normalizedSessionKey}: ${String(err?.message || err)}`,
83
+ );
84
+ }
85
+
86
+ return {
87
+ cleared: true,
88
+ transcriptArchived,
89
+ archivedTranscriptPath,
90
+ };
91
+ }
92
+
93
+ async function resetWecomConversationSession({
94
+ api,
95
+ runtime,
96
+ cfg,
97
+ baseSessionId,
98
+ fromUser,
99
+ chatId = "",
100
+ isGroupChat = false,
101
+ commandBody = "/reset",
102
+ accountId = "default",
103
+ groupChatPolicy = {},
104
+ dynamicAgentPolicy = {},
105
+ isAdminUser = false,
106
+ resolveWecomAgentRoute,
107
+ activeLateReplyWatchers,
108
+ } = {}) {
109
+ if (typeof resolveWecomAgentRoute !== "function") {
110
+ throw new Error("resetWecomConversationSession: resolveWecomAgentRoute is required");
111
+ }
112
+ if (!runtime?.channel?.session?.resolveStorePath) {
113
+ throw new Error("resetWecomConversationSession: runtime.channel.session.resolveStorePath is required");
114
+ }
115
+
116
+ const normalizedAccountId = normalizeToken(accountId) || "default";
117
+ const route = resolveWecomAgentRoute({
118
+ runtime,
119
+ cfg,
120
+ channel: "wecom",
121
+ accountId: normalizedAccountId,
122
+ sessionKey: baseSessionId,
123
+ fromUser,
124
+ chatId,
125
+ isGroupChat,
126
+ content: commandBody,
127
+ mentionPatterns: groupChatPolicy?.mentionPatterns,
128
+ dynamicConfig: dynamicAgentPolicy,
129
+ isAdminUser,
130
+ logger: api?.logger,
131
+ });
132
+
133
+ const sessionKey = normalizeText(route?.sessionKey) || normalizeText(baseSessionId);
134
+ const routedAgentId =
135
+ normalizeText(route?.agentId) || resolveAgentIdFromSessionKey(sessionKey, normalizeText(cfg?.agents?.default));
136
+ const storePath = runtime.channel.session.resolveStorePath(cfg?.session?.store, {
137
+ agentId: routedAgentId,
138
+ });
139
+
140
+ const result = await clearSessionStoreEntry({
141
+ storePath,
142
+ sessionKey,
143
+ logger: api?.logger,
144
+ });
145
+
146
+ if (activeLateReplyWatchers?.delete) {
147
+ activeLateReplyWatchers.delete(sessionKey);
148
+ }
149
+
150
+ api?.logger?.info?.(
151
+ `wecom: local session reset account=${normalizedAccountId} agent=${routedAgentId || "main"} session=${sessionKey} cleared=${result.cleared ? "yes" : "no"}`,
152
+ );
153
+
154
+ return {
155
+ ...result,
156
+ accountId: normalizedAccountId,
157
+ routedAgentId,
158
+ route,
159
+ sessionKey,
160
+ storePath,
161
+ };
162
+ }
163
+
164
+ return {
165
+ clearSessionStoreEntry,
166
+ resetWecomConversationSession,
167
+ };
168
+ }