@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.
- package/CHANGELOG.md +160 -0
- package/README.en.md +379 -11
- package/README.md +620 -12
- package/docs/channels/wecom.md +181 -3
- package/openclaw.plugin.json +148 -5
- package/package.json +9 -5
- package/src/core/delivery-router.js +2 -0
- package/src/core/stream-manager.js +13 -2
- package/src/core.js +96 -6
- package/src/wecom/account-config-core.js +2 -0
- package/src/wecom/account-config.js +12 -3
- package/src/wecom/agent-context.js +7 -1
- package/src/wecom/agent-dispatch-executor.js +13 -1
- package/src/wecom/agent-dispatch-fallback.js +23 -0
- package/src/wecom/agent-inbound-dispatch.js +1 -1
- package/src/wecom/agent-inbound-processor.js +33 -2
- package/src/wecom/agent-late-reply-runtime.js +31 -1
- package/src/wecom/agent-runtime-context.js +3 -0
- package/src/wecom/agent-webhook-handler.js +5 -0
- package/src/wecom/api-client-core.js +1 -1
- package/src/wecom/api-client-send-text.js +43 -20
- package/src/wecom/bot-context.js +7 -1
- package/src/wecom/bot-dispatch-fallback.js +34 -3
- package/src/wecom/bot-dispatch-handlers.js +47 -4
- package/src/wecom/bot-inbound-content.js +14 -6
- package/src/wecom/bot-inbound-dispatch-runtime.js +10 -0
- package/src/wecom/bot-inbound-executor-helpers.js +44 -11
- package/src/wecom/bot-inbound-executor.js +40 -0
- package/src/wecom/bot-long-connection-manager.js +983 -0
- package/src/wecom/bot-reply-runtime.js +36 -6
- package/src/wecom/bot-runtime-context.js +3 -0
- package/src/wecom/bot-state-store.js +4 -5
- package/src/wecom/bot-webhook-dispatch.js +7 -0
- package/src/wecom/bot-webhook-handler.js +5 -0
- package/src/wecom/callback-health-diagnostics.js +86 -0
- package/src/wecom/channel-config-schema.js +242 -0
- package/src/wecom/channel-plugin.js +162 -4
- package/src/wecom/channel-status-state.js +150 -0
- package/src/wecom/command-handlers.js +6 -0
- package/src/wecom/command-status-text.js +32 -3
- package/src/wecom/doc-client.js +537 -0
- package/src/wecom/doc-schema.js +380 -0
- package/src/wecom/doc-tool.js +833 -0
- package/src/wecom/outbound-active-stream.js +17 -10
- package/src/wecom/outbound-delivery.js +46 -0
- package/src/wecom/outbound-webhook-sender.js +39 -16
- package/src/wecom/plugin-account-policy-services.js +4 -1
- package/src/wecom/plugin-composition.js +2 -0
- package/src/wecom/plugin-constants.js +1 -1
- package/src/wecom/plugin-delivery-inbound-services.js +4 -0
- package/src/wecom/plugin-processing-deps.js +5 -0
- package/src/wecom/plugin-route-runtime-deps.js +2 -0
- package/src/wecom/plugin-services.js +37 -0
- package/src/wecom/register-runtime.js +20 -1
- package/src/wecom/request-parsers.js +1 -0
- package/src/wecom/route-registration.js +4 -1
- package/src/wecom/session-reset.js +168 -0
- package/src/wecom/target-utils.js +41 -5
- package/src/wecom/text-format.js +22 -5
- package/src/wecom/text-inbound-scheduler.js +1 -1
- package/src/wecom/thinking-parser.js +74 -0
- package/src/wecom/voice-transcription-process.js +145 -11
- package/src/wecom/voice-transcription.js +14 -2
- package/src/wecom/webhook-adapter-normalize.js +29 -0
- 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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
101
|
-
|
|
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
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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 } =
|
|
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 = "
|
|
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
|
}
|
|
@@ -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
|
-
|
|
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
|
+
}
|