@dingxiang-me/openclaw-wechat 1.4.1 → 1.7.2
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 +119 -0
- package/README.en.md +89 -12
- package/README.md +103 -15
- package/docs/channels/wecom.md +28 -3
- package/openclaw.plugin.json +467 -10
- package/package.json +13 -2
- package/src/core/agent-routing.js +6 -0
- package/src/core.js +564 -35
- package/src/wecom/account-config-core.js +28 -8
- package/src/wecom/account-config.js +55 -0
- package/src/wecom/account-diagnostics.js +121 -0
- package/src/wecom/account-paths.js +39 -0
- package/src/wecom/agent-inbound-dispatch.js +9 -0
- package/src/wecom/agent-inbound-guards.js +24 -4
- package/src/wecom/agent-inbound-processor.js +27 -0
- package/src/wecom/agent-webhook-handler.js +11 -0
- package/src/wecom/bot-context.js +2 -1
- package/src/wecom/bot-dispatch-fallback.js +2 -1
- package/src/wecom/bot-inbound-content.js +73 -3
- package/src/wecom/bot-inbound-dispatch-runtime.js +2 -1
- package/src/wecom/bot-inbound-executor-helpers.js +56 -5
- package/src/wecom/bot-inbound-executor.js +19 -0
- package/src/wecom/bot-inbound-guards.js +36 -4
- package/src/wecom/bot-runtime-context.js +5 -3
- package/src/wecom/bot-webhook-dispatch.js +45 -12
- package/src/wecom/bot-webhook-handler.js +45 -13
- package/src/wecom/command-handlers.js +26 -0
- package/src/wecom/command-status-text.js +76 -7
- package/src/wecom/observability-metrics.js +133 -0
- package/src/wecom/outbound-agent-push.js +2 -1
- package/src/wecom/outbound-bot-card.js +103 -0
- package/src/wecom/outbound-delivery.js +92 -7
- package/src/wecom/outbound-response-delivery.js +10 -6
- package/src/wecom/outbound-webhook-delivery.js +42 -1
- package/src/wecom/plugin-account-policy-services.js +19 -0
- package/src/wecom/plugin-base-services.js +13 -0
- package/src/wecom/plugin-constants.js +1 -1
- package/src/wecom/plugin-delivery-inbound-services.js +8 -0
- package/src/wecom/plugin-processing-deps.js +4 -0
- package/src/wecom/plugin-route-runtime-deps.js +5 -0
- package/src/wecom/plugin-services.js +7 -0
- package/src/wecom/policy-resolvers.js +82 -5
- package/src/wecom/register-runtime.js +31 -2
- package/src/wecom/route-registration.js +173 -41
- package/src/wecom/runtime-utils.js +7 -2
- package/src/wecom/webhook-adapter.js +61 -0
- package/src/wecom/webhook-bot.js +26 -0
|
@@ -4,6 +4,45 @@ function assertFunction(name, value) {
|
|
|
4
4
|
}
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
const UNSUPPORTED_BOT_GROUP_TRIGGER_WARNED = new Set();
|
|
8
|
+
|
|
9
|
+
function warnUnsupportedBotGroupTriggerOnce(triggerMode, logger) {
|
|
10
|
+
const mode = String(triggerMode ?? "").trim().toLowerCase();
|
|
11
|
+
if (!mode || UNSUPPORTED_BOT_GROUP_TRIGGER_WARNED.has(mode)) return;
|
|
12
|
+
UNSUPPORTED_BOT_GROUP_TRIGGER_WARNED.add(mode);
|
|
13
|
+
logger?.warn?.(
|
|
14
|
+
`wecom(bot): groupChat.triggerMode=${mode} is not supported by WeCom Bot group callbacks; forcing mention mode (@).`,
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function normalizeWecomBotGroupChatPolicy(groupChatPolicy = {}, logger) {
|
|
19
|
+
const policy = groupChatPolicy && typeof groupChatPolicy === "object" ? groupChatPolicy : {};
|
|
20
|
+
const enabled = policy.enabled !== false;
|
|
21
|
+
const mentionPatterns =
|
|
22
|
+
Array.isArray(policy.mentionPatterns) && policy.mentionPatterns.length > 0 ? policy.mentionPatterns : ["@"];
|
|
23
|
+
|
|
24
|
+
if (!enabled) {
|
|
25
|
+
return {
|
|
26
|
+
...policy,
|
|
27
|
+
enabled: false,
|
|
28
|
+
mentionPatterns,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const triggerMode = String(policy.triggerMode ?? "").trim().toLowerCase();
|
|
33
|
+
if (triggerMode && triggerMode !== "mention") {
|
|
34
|
+
warnUnsupportedBotGroupTriggerOnce(triggerMode, logger);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
...policy,
|
|
39
|
+
enabled: true,
|
|
40
|
+
triggerMode: "mention",
|
|
41
|
+
requireMention: true,
|
|
42
|
+
mentionPatterns,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
7
46
|
export function assertWecomBotInboundFlowDeps({ api, ...deps } = {}) {
|
|
8
47
|
if (!api || typeof api !== "object") {
|
|
9
48
|
throw new Error("executeWecomBotInboundFlow: api is required");
|
|
@@ -23,6 +62,7 @@ export function assertWecomBotInboundFlowDeps({ api, ...deps } = {}) {
|
|
|
23
62
|
"stripWecomGroupMentions",
|
|
24
63
|
"resolveWecomCommandPolicy",
|
|
25
64
|
"resolveWecomAllowFromPolicy",
|
|
65
|
+
"resolveWecomDmPolicy",
|
|
26
66
|
"isWecomSenderAllowed",
|
|
27
67
|
"extractLeadingSlashCommand",
|
|
28
68
|
"buildWecomBotHelpText",
|
|
@@ -48,11 +88,15 @@ export function assertWecomBotInboundFlowDeps({ api, ...deps } = {}) {
|
|
|
48
88
|
|
|
49
89
|
export function createWecomBotInboundFlowState({
|
|
50
90
|
api,
|
|
91
|
+
accountId = "default",
|
|
51
92
|
fromUser,
|
|
52
93
|
content,
|
|
53
94
|
imageUrls,
|
|
54
95
|
fileUrl,
|
|
55
96
|
fileName,
|
|
97
|
+
voiceUrl,
|
|
98
|
+
voiceMediaId,
|
|
99
|
+
voiceContentType,
|
|
56
100
|
quote,
|
|
57
101
|
buildWecomBotSessionId,
|
|
58
102
|
resolveWecomBotConfig,
|
|
@@ -62,23 +106,29 @@ export function createWecomBotInboundFlowState({
|
|
|
62
106
|
} = {}) {
|
|
63
107
|
const runtime = api.runtime;
|
|
64
108
|
const cfg = api.config;
|
|
65
|
-
const
|
|
109
|
+
const normalizedAccountId = String(accountId ?? "default").trim().toLowerCase() || "default";
|
|
110
|
+
const baseSessionId = buildWecomBotSessionId(fromUser, normalizedAccountId);
|
|
66
111
|
const state = {
|
|
67
112
|
runtime,
|
|
68
113
|
cfg,
|
|
114
|
+
accountId: normalizedAccountId,
|
|
69
115
|
baseSessionId,
|
|
70
116
|
sessionId: baseSessionId,
|
|
71
117
|
routedAgentId: "",
|
|
72
|
-
fromAddress:
|
|
118
|
+
fromAddress:
|
|
119
|
+
normalizedAccountId === "default" ? `wecom-bot:${fromUser}` : `wecom-bot:${normalizedAccountId}:${fromUser}`,
|
|
73
120
|
normalizedFromUser: String(fromUser ?? "").trim().toLowerCase(),
|
|
74
121
|
originalContent: String(content ?? ""),
|
|
75
122
|
commandBody: String(content ?? ""),
|
|
76
123
|
dispatchStartedAt: Date.now(),
|
|
77
124
|
tempPathsToCleanup: [],
|
|
78
|
-
botModeConfig: resolveWecomBotConfig(api),
|
|
79
|
-
botProxyUrl: resolveWecomBotProxyConfig(api),
|
|
125
|
+
botModeConfig: resolveWecomBotConfig(api, normalizedAccountId),
|
|
126
|
+
botProxyUrl: resolveWecomBotProxyConfig(api, normalizedAccountId),
|
|
80
127
|
normalizedFileUrl: String(fileUrl ?? "").trim(),
|
|
81
128
|
normalizedFileName: String(fileName ?? "").trim(),
|
|
129
|
+
normalizedVoiceUrl: String(voiceUrl ?? "").trim(),
|
|
130
|
+
normalizedVoiceMediaId: String(voiceMediaId ?? "").trim(),
|
|
131
|
+
normalizedVoiceContentType: String(voiceContentType ?? "").trim(),
|
|
82
132
|
normalizedQuote:
|
|
83
133
|
quote && typeof quote === "object"
|
|
84
134
|
? {
|
|
@@ -93,7 +143,7 @@ export function createWecomBotInboundFlowState({
|
|
|
93
143
|
.filter(Boolean),
|
|
94
144
|
),
|
|
95
145
|
),
|
|
96
|
-
groupChatPolicy: resolveWecomGroupChatPolicy(api),
|
|
146
|
+
groupChatPolicy: normalizeWecomBotGroupChatPolicy(resolveWecomGroupChatPolicy(api), api?.logger),
|
|
97
147
|
dynamicAgentPolicy: resolveWecomDynamicAgentPolicy(api),
|
|
98
148
|
isAdminUser: false,
|
|
99
149
|
};
|
|
@@ -129,6 +179,7 @@ export function createWecomBotSafeReplyHelpers({
|
|
|
129
179
|
const result = await deliverBotReplyText({
|
|
130
180
|
api,
|
|
131
181
|
fromUser,
|
|
182
|
+
accountId: state.accountId,
|
|
132
183
|
sessionId: state.sessionId,
|
|
133
184
|
streamId,
|
|
134
185
|
responseUrl,
|
|
@@ -13,6 +13,7 @@ export async function executeWecomBotInboundFlow(payload = {}) {
|
|
|
13
13
|
const {
|
|
14
14
|
api,
|
|
15
15
|
streamId,
|
|
16
|
+
accountId = "default",
|
|
16
17
|
fromUser,
|
|
17
18
|
content,
|
|
18
19
|
msgType = "text",
|
|
@@ -22,6 +23,9 @@ export async function executeWecomBotInboundFlow(payload = {}) {
|
|
|
22
23
|
imageUrls = [],
|
|
23
24
|
fileUrl = "",
|
|
24
25
|
fileName = "",
|
|
26
|
+
voiceUrl = "",
|
|
27
|
+
voiceMediaId = "",
|
|
28
|
+
voiceContentType = "",
|
|
25
29
|
quote = null,
|
|
26
30
|
responseUrl = "",
|
|
27
31
|
buildWecomBotSessionId,
|
|
@@ -38,6 +42,7 @@ export async function executeWecomBotInboundFlow(payload = {}) {
|
|
|
38
42
|
stripWecomGroupMentions,
|
|
39
43
|
resolveWecomCommandPolicy,
|
|
40
44
|
resolveWecomAllowFromPolicy,
|
|
45
|
+
resolveWecomDmPolicy,
|
|
41
46
|
isWecomSenderAllowed,
|
|
42
47
|
extractLeadingSlashCommand,
|
|
43
48
|
buildWecomBotHelpText,
|
|
@@ -65,11 +70,15 @@ export async function executeWecomBotInboundFlow(payload = {}) {
|
|
|
65
70
|
|
|
66
71
|
const state = createWecomBotInboundFlowState({
|
|
67
72
|
api,
|
|
73
|
+
accountId,
|
|
68
74
|
fromUser,
|
|
69
75
|
content,
|
|
70
76
|
imageUrls,
|
|
71
77
|
fileUrl,
|
|
72
78
|
fileName,
|
|
79
|
+
voiceUrl,
|
|
80
|
+
voiceMediaId,
|
|
81
|
+
voiceContentType,
|
|
73
82
|
quote,
|
|
74
83
|
buildWecomBotSessionId,
|
|
75
84
|
resolveWecomBotConfig,
|
|
@@ -111,12 +120,15 @@ export async function executeWecomBotInboundFlow(payload = {}) {
|
|
|
111
120
|
|
|
112
121
|
const commandGuardResult = applyWecomBotCommandAndSenderGuard({
|
|
113
122
|
api,
|
|
123
|
+
accountId: state.accountId,
|
|
114
124
|
fromUser,
|
|
125
|
+
isGroupChat,
|
|
115
126
|
msgType,
|
|
116
127
|
commandBody: state.commandBody,
|
|
117
128
|
normalizedFromUser: state.normalizedFromUser,
|
|
118
129
|
resolveWecomCommandPolicy,
|
|
119
130
|
resolveWecomAllowFromPolicy,
|
|
131
|
+
resolveWecomDmPolicy,
|
|
120
132
|
isWecomSenderAllowed,
|
|
121
133
|
extractLeadingSlashCommand,
|
|
122
134
|
buildWecomBotHelpText,
|
|
@@ -138,6 +150,10 @@ export async function executeWecomBotInboundFlow(payload = {}) {
|
|
|
138
150
|
normalizedImageUrls: state.normalizedImageUrls,
|
|
139
151
|
normalizedFileUrl: state.normalizedFileUrl,
|
|
140
152
|
normalizedFileName: state.normalizedFileName,
|
|
153
|
+
normalizedVoiceUrl: state.normalizedVoiceUrl,
|
|
154
|
+
normalizedVoiceMediaId: state.normalizedVoiceMediaId,
|
|
155
|
+
normalizedVoiceContentType: state.normalizedVoiceContentType,
|
|
156
|
+
voiceInputMessageId: msgId,
|
|
141
157
|
normalizedQuote: state.normalizedQuote,
|
|
142
158
|
});
|
|
143
159
|
if (Array.isArray(inboundContentResult.tempPathsToCleanup)) {
|
|
@@ -166,6 +182,7 @@ export async function executeWecomBotInboundFlow(payload = {}) {
|
|
|
166
182
|
commandBody: state.commandBody,
|
|
167
183
|
originalContent: state.originalContent,
|
|
168
184
|
fromAddress: state.fromAddress,
|
|
185
|
+
accountId: state.accountId,
|
|
169
186
|
groupChatPolicy: state.groupChatPolicy,
|
|
170
187
|
dynamicAgentPolicy: state.dynamicAgentPolicy,
|
|
171
188
|
isAdminUser: state.isAdminUser,
|
|
@@ -186,6 +203,7 @@ export async function executeWecomBotInboundFlow(payload = {}) {
|
|
|
186
203
|
cfg,
|
|
187
204
|
ctxPayload,
|
|
188
205
|
streamId,
|
|
206
|
+
accountId: state.accountId,
|
|
189
207
|
sessionId: state.sessionId,
|
|
190
208
|
routedAgentId: state.routedAgentId,
|
|
191
209
|
storePath,
|
|
@@ -225,6 +243,7 @@ export async function executeWecomBotInboundFlow(payload = {}) {
|
|
|
225
243
|
readTranscriptFallbackResult,
|
|
226
244
|
safeDeliverReply,
|
|
227
245
|
markTranscriptReplyDelivered,
|
|
246
|
+
accountId: state.accountId,
|
|
228
247
|
});
|
|
229
248
|
if (shouldReturnFromError) return;
|
|
230
249
|
} finally {
|
|
@@ -52,12 +52,15 @@ export function applyWecomBotGroupChatGuard({
|
|
|
52
52
|
|
|
53
53
|
export function applyWecomBotCommandAndSenderGuard({
|
|
54
54
|
api,
|
|
55
|
+
accountId = "default",
|
|
55
56
|
fromUser,
|
|
57
|
+
isGroupChat = false,
|
|
56
58
|
msgType = "text",
|
|
57
59
|
commandBody = "",
|
|
58
60
|
normalizedFromUser = "",
|
|
59
61
|
resolveWecomCommandPolicy,
|
|
60
62
|
resolveWecomAllowFromPolicy,
|
|
63
|
+
resolveWecomDmPolicy,
|
|
61
64
|
isWecomSenderAllowed,
|
|
62
65
|
extractLeadingSlashCommand,
|
|
63
66
|
buildWecomBotHelpText,
|
|
@@ -65,6 +68,7 @@ export function applyWecomBotCommandAndSenderGuard({
|
|
|
65
68
|
} = {}) {
|
|
66
69
|
assertFunction("resolveWecomCommandPolicy", resolveWecomCommandPolicy);
|
|
67
70
|
assertFunction("resolveWecomAllowFromPolicy", resolveWecomAllowFromPolicy);
|
|
71
|
+
assertFunction("resolveWecomDmPolicy", resolveWecomDmPolicy);
|
|
68
72
|
assertFunction("isWecomSenderAllowed", isWecomSenderAllowed);
|
|
69
73
|
assertFunction("extractLeadingSlashCommand", extractLeadingSlashCommand);
|
|
70
74
|
assertFunction("buildWecomBotHelpText", buildWecomBotHelpText);
|
|
@@ -72,7 +76,34 @@ export function applyWecomBotCommandAndSenderGuard({
|
|
|
72
76
|
|
|
73
77
|
const commandPolicy = resolveWecomCommandPolicy(api);
|
|
74
78
|
const isAdminUser = commandPolicy.adminUsers.includes(String(normalizedFromUser ?? "").trim().toLowerCase());
|
|
75
|
-
const
|
|
79
|
+
const dmPolicy = resolveWecomDmPolicy(api, accountId, {});
|
|
80
|
+
if (!isGroupChat) {
|
|
81
|
+
if (dmPolicy.mode === "deny") {
|
|
82
|
+
return {
|
|
83
|
+
ok: false,
|
|
84
|
+
finishText: dmPolicy.rejectMessage || "当前渠道私聊已关闭,请联系管理员。",
|
|
85
|
+
commandBody: String(commandBody ?? ""),
|
|
86
|
+
isAdminUser,
|
|
87
|
+
commandPolicy,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (dmPolicy.mode === "allowlist") {
|
|
91
|
+
const dmSenderAllowed = isAdminUser || isWecomSenderAllowed({
|
|
92
|
+
senderId: normalizedFromUser,
|
|
93
|
+
allowFrom: dmPolicy.allowFrom,
|
|
94
|
+
});
|
|
95
|
+
if (!dmSenderAllowed) {
|
|
96
|
+
return {
|
|
97
|
+
ok: false,
|
|
98
|
+
finishText: dmPolicy.rejectMessage || "当前私聊账号未授权,请联系管理员。",
|
|
99
|
+
commandBody: String(commandBody ?? ""),
|
|
100
|
+
isAdminUser,
|
|
101
|
+
commandPolicy,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const allowFromPolicy = resolveWecomAllowFromPolicy(api, accountId, {});
|
|
76
107
|
const senderAllowed = isAdminUser || isWecomSenderAllowed({
|
|
77
108
|
senderId: normalizedFromUser,
|
|
78
109
|
allowFrom: allowFromPolicy.allowFrom,
|
|
@@ -90,14 +121,15 @@ export function applyWecomBotCommandAndSenderGuard({
|
|
|
90
121
|
let nextCommandBody = String(commandBody ?? "");
|
|
91
122
|
if (msgType === "text") {
|
|
92
123
|
let commandKey = extractLeadingSlashCommand(nextCommandBody);
|
|
93
|
-
if (commandKey === "/clear") {
|
|
94
|
-
nextCommandBody = nextCommandBody.replace(/^\/clear\b/i, "/reset");
|
|
124
|
+
if (commandKey === "/clear" || commandKey === "/new") {
|
|
125
|
+
nextCommandBody = nextCommandBody.replace(/^\/(?:clear|new)\b/i, "/reset");
|
|
95
126
|
commandKey = "/reset";
|
|
96
127
|
}
|
|
97
128
|
if (commandKey) {
|
|
98
129
|
const commandAllowed =
|
|
99
130
|
commandPolicy.allowlist.includes(commandKey) ||
|
|
100
|
-
(commandKey === "/reset" &&
|
|
131
|
+
(commandKey === "/reset" &&
|
|
132
|
+
(commandPolicy.allowlist.includes("/clear") || commandPolicy.allowlist.includes("/new")));
|
|
101
133
|
if (commandPolicy.enabled && !isAdminUser && !commandAllowed) {
|
|
102
134
|
return {
|
|
103
135
|
ok: false,
|
|
@@ -8,6 +8,7 @@ export async function prepareWecomBotRuntimeContext({
|
|
|
8
8
|
api,
|
|
9
9
|
runtime,
|
|
10
10
|
cfg,
|
|
11
|
+
accountId = "default",
|
|
11
12
|
baseSessionId,
|
|
12
13
|
fromUser,
|
|
13
14
|
chatId,
|
|
@@ -34,7 +35,7 @@ export async function prepareWecomBotRuntimeContext({
|
|
|
34
35
|
runtime,
|
|
35
36
|
cfg,
|
|
36
37
|
channel: "wecom",
|
|
37
|
-
accountId
|
|
38
|
+
accountId,
|
|
38
39
|
sessionKey: baseSessionId,
|
|
39
40
|
fromUser,
|
|
40
41
|
chatId,
|
|
@@ -83,6 +84,7 @@ export async function prepareWecomBotRuntimeContext({
|
|
|
83
84
|
commandBody,
|
|
84
85
|
fromAddress,
|
|
85
86
|
sessionId,
|
|
87
|
+
accountId,
|
|
86
88
|
isGroupChat,
|
|
87
89
|
chatId,
|
|
88
90
|
fromUser,
|
|
@@ -100,7 +102,7 @@ export async function prepareWecomBotRuntimeContext({
|
|
|
100
102
|
sessionKey: sessionId,
|
|
101
103
|
channel: "wecom",
|
|
102
104
|
to: fromUser,
|
|
103
|
-
accountId
|
|
105
|
+
accountId,
|
|
104
106
|
},
|
|
105
107
|
onRecordError: (err) => {
|
|
106
108
|
api?.logger?.warn?.(`wecom(bot): failed to record session: ${err}`);
|
|
@@ -109,7 +111,7 @@ export async function prepareWecomBotRuntimeContext({
|
|
|
109
111
|
|
|
110
112
|
runtime.channel.activity.record({
|
|
111
113
|
channel: "wecom",
|
|
112
|
-
accountId
|
|
114
|
+
accountId,
|
|
113
115
|
direction: "inbound",
|
|
114
116
|
});
|
|
115
117
|
|
|
@@ -52,7 +52,6 @@ function buildEncryptedStreamPayload({
|
|
|
52
52
|
|
|
53
53
|
export function createWecomBotParsedDispatcher({
|
|
54
54
|
api,
|
|
55
|
-
botConfig,
|
|
56
55
|
cleanupExpiredBotStreams,
|
|
57
56
|
getBotStream,
|
|
58
57
|
buildWecomBotEncryptedResponse,
|
|
@@ -65,6 +64,8 @@ export function createWecomBotParsedDispatcher({
|
|
|
65
64
|
processBotInboundMessage,
|
|
66
65
|
deliverBotReplyText,
|
|
67
66
|
finishBotStream,
|
|
67
|
+
recordInboundMetric = () => {},
|
|
68
|
+
recordRuntimeErrorMetric = () => {},
|
|
68
69
|
randomUuid = () => "",
|
|
69
70
|
} = {}) {
|
|
70
71
|
assertFunction("cleanupExpiredBotStreams", cleanupExpiredBotStreams);
|
|
@@ -83,20 +84,29 @@ export function createWecomBotParsedDispatcher({
|
|
|
83
84
|
assertFunction("finishBotStream", finishBotStream);
|
|
84
85
|
assertFunction("randomUuid", randomUuid);
|
|
85
86
|
|
|
86
|
-
function buildStreamId() {
|
|
87
|
+
function buildStreamId(accountId = "default") {
|
|
87
88
|
const normalized = String(randomUuid() || "").trim();
|
|
89
|
+
const accountSlug = String(accountId ?? "default")
|
|
90
|
+
.trim()
|
|
91
|
+
.toLowerCase()
|
|
92
|
+
.replace(/[^a-z0-9_-]/g, "_") || "default";
|
|
88
93
|
if (normalized) return `stream_${normalized}`;
|
|
89
|
-
return `stream_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
94
|
+
return `stream_${accountSlug}_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
90
95
|
}
|
|
91
96
|
|
|
92
97
|
function respondStreamRefresh({ parsed, res, timestamp, nonce }) {
|
|
93
|
-
|
|
98
|
+
const activeBotConfig = parsed?._botConfig;
|
|
99
|
+
if (!activeBotConfig?.token || !activeBotConfig?.encodingAesKey) {
|
|
100
|
+
sendPlainText(res, 401, "Invalid bot account config");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
cleanupExpiredBotStreams(activeBotConfig.streamExpireMs);
|
|
94
104
|
const streamId = parsed.streamId || `stream-${Date.now()}`;
|
|
95
105
|
const stream = getBotStream(streamId);
|
|
96
106
|
const feedbackId = String(parsed.feedbackId || stream?.feedbackId || "").trim();
|
|
97
107
|
const encryptedResponse = buildEncryptedStreamPayload({
|
|
98
108
|
buildWecomBotEncryptedResponse,
|
|
99
|
-
botConfig,
|
|
109
|
+
botConfig: activeBotConfig,
|
|
100
110
|
timestamp,
|
|
101
111
|
nonce,
|
|
102
112
|
streamId,
|
|
@@ -130,17 +140,27 @@ export function createWecomBotParsedDispatcher({
|
|
|
130
140
|
fileName: parsed.fileName,
|
|
131
141
|
quote: parsed.quote,
|
|
132
142
|
responseUrl: parsed.responseUrl,
|
|
143
|
+
accountId: parsed.accountId,
|
|
144
|
+
voiceUrl: parsed.voiceUrl,
|
|
145
|
+
voiceMediaId: parsed.voiceMediaId,
|
|
146
|
+
voiceContentType: parsed.voiceContentType,
|
|
133
147
|
}),
|
|
134
148
|
}),
|
|
135
149
|
)
|
|
136
150
|
.catch((err) => {
|
|
137
151
|
api.logger.error?.(`wecom(bot): async message processing failed: ${String(err?.message || err)}`);
|
|
152
|
+
recordRuntimeErrorMetric({
|
|
153
|
+
scope: "bot-dispatch",
|
|
154
|
+
reason: String(err?.message || err),
|
|
155
|
+
accountId: parsed.accountId,
|
|
156
|
+
});
|
|
138
157
|
deliverBotReplyText({
|
|
139
158
|
api,
|
|
140
159
|
fromUser: parsed.fromUser,
|
|
141
160
|
sessionId: botSessionId,
|
|
142
161
|
streamId,
|
|
143
162
|
responseUrl: parsed.responseUrl,
|
|
163
|
+
accountId: parsed.accountId,
|
|
144
164
|
text: `抱歉,当前模型请求失败,请稍后重试。\n故障信息: ${String(err?.message || err).slice(0, 160)}`,
|
|
145
165
|
reason: "bot-async-processing-error",
|
|
146
166
|
}).catch((deliveryErr) => {
|
|
@@ -154,6 +174,17 @@ export function createWecomBotParsedDispatcher({
|
|
|
154
174
|
}
|
|
155
175
|
|
|
156
176
|
function respondMessage({ parsed, res, timestamp, nonce }) {
|
|
177
|
+
const activeBotConfig = parsed?._botConfig;
|
|
178
|
+
if (!activeBotConfig?.token || !activeBotConfig?.encodingAesKey) {
|
|
179
|
+
sendPlainText(res, 401, "Invalid bot account config");
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const accountId = String(parsed?.accountId ?? "default").trim().toLowerCase() || "default";
|
|
183
|
+
recordInboundMetric({
|
|
184
|
+
mode: "bot",
|
|
185
|
+
msgType: parsed.msgType || parsed.kind || "unknown",
|
|
186
|
+
accountId,
|
|
187
|
+
});
|
|
157
188
|
const dedupeStub = {
|
|
158
189
|
MsgId: parsed.msgId,
|
|
159
190
|
FromUserName: parsed.fromUser,
|
|
@@ -161,17 +192,18 @@ export function createWecomBotParsedDispatcher({
|
|
|
161
192
|
Content: parsed.content,
|
|
162
193
|
CreateTime: String(Math.floor(Date.now() / 1000)),
|
|
163
194
|
};
|
|
164
|
-
if (!markInboundMessageSeen(dedupeStub,
|
|
195
|
+
if (!markInboundMessageSeen(dedupeStub, `bot:${accountId}`)) {
|
|
165
196
|
sendPlainText(res, 200, "success");
|
|
166
197
|
return;
|
|
167
198
|
}
|
|
168
199
|
|
|
169
|
-
const botSessionId = buildWecomBotSessionId(parsed.fromUser);
|
|
170
|
-
const streamId = buildStreamId();
|
|
200
|
+
const botSessionId = buildWecomBotSessionId(parsed.fromUser, accountId);
|
|
201
|
+
const streamId = buildStreamId(accountId);
|
|
171
202
|
const feedbackId = String(parsed.feedbackId ?? "").trim();
|
|
172
|
-
createBotStream(streamId,
|
|
203
|
+
createBotStream(streamId, activeBotConfig.placeholderText, {
|
|
173
204
|
feedbackId,
|
|
174
205
|
sessionId: botSessionId,
|
|
206
|
+
accountId,
|
|
175
207
|
});
|
|
176
208
|
if (parsed.responseUrl) {
|
|
177
209
|
upsertBotResponseUrlCache({
|
|
@@ -181,11 +213,11 @@ export function createWecomBotParsedDispatcher({
|
|
|
181
213
|
}
|
|
182
214
|
const encryptedResponse = buildEncryptedStreamPayload({
|
|
183
215
|
buildWecomBotEncryptedResponse,
|
|
184
|
-
botConfig,
|
|
216
|
+
botConfig: activeBotConfig,
|
|
185
217
|
timestamp,
|
|
186
218
|
nonce,
|
|
187
219
|
streamId,
|
|
188
|
-
content:
|
|
220
|
+
content: activeBotConfig.placeholderText,
|
|
189
221
|
finish: false,
|
|
190
222
|
feedbackId,
|
|
191
223
|
});
|
|
@@ -197,11 +229,12 @@ export function createWecomBotParsedDispatcher({
|
|
|
197
229
|
});
|
|
198
230
|
}
|
|
199
231
|
|
|
200
|
-
return async function dispatchParsed({ parsed, res, timestamp, nonce } = {}) {
|
|
232
|
+
return async function dispatchParsed({ parsed, res, timestamp, nonce, botConfig } = {}) {
|
|
201
233
|
if (!parsed || typeof parsed !== "object") {
|
|
202
234
|
sendPlainText(res, 200, "success");
|
|
203
235
|
return true;
|
|
204
236
|
}
|
|
237
|
+
parsed._botConfig = botConfig;
|
|
205
238
|
if (parsed.kind === "stream-refresh") {
|
|
206
239
|
respondStreamRefresh({ parsed, res, timestamp, nonce });
|
|
207
240
|
return true;
|
|
@@ -4,6 +4,7 @@ import { createWecomBotParsedDispatcher } from "./bot-webhook-dispatch.js";
|
|
|
4
4
|
export function createWecomBotWebhookHandler({
|
|
5
5
|
api,
|
|
6
6
|
botConfig,
|
|
7
|
+
botConfigs,
|
|
7
8
|
normalizedPath,
|
|
8
9
|
readRequestBody,
|
|
9
10
|
parseIncomingJson,
|
|
@@ -23,10 +24,27 @@ export function createWecomBotWebhookHandler({
|
|
|
23
24
|
processBotInboundMessage,
|
|
24
25
|
deliverBotReplyText,
|
|
25
26
|
finishBotStream,
|
|
27
|
+
recordInboundMetric = () => {},
|
|
28
|
+
recordRuntimeErrorMetric = () => {},
|
|
26
29
|
} = {}) {
|
|
30
|
+
const configuredBotConfigs = Array.isArray(botConfigs) && botConfigs.length > 0 ? botConfigs : [botConfig];
|
|
31
|
+
const signedBotConfigs = configuredBotConfigs.filter((item) => item?.token && item?.encodingAesKey);
|
|
32
|
+
function pickBotConfigBySignature({ msgSignature, timestamp, nonce, encrypt }) {
|
|
33
|
+
if (!msgSignature || !encrypt) return null;
|
|
34
|
+
for (const cfg of signedBotConfigs) {
|
|
35
|
+
const expected = computeMsgSignature({
|
|
36
|
+
token: cfg.token,
|
|
37
|
+
timestamp,
|
|
38
|
+
nonce,
|
|
39
|
+
encrypt,
|
|
40
|
+
});
|
|
41
|
+
if (expected === msgSignature) return cfg;
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
27
46
|
const dispatchParsed = createWecomBotParsedDispatcher({
|
|
28
47
|
api,
|
|
29
|
-
botConfig,
|
|
30
48
|
cleanupExpiredBotStreams,
|
|
31
49
|
getBotStream,
|
|
32
50
|
buildWecomBotEncryptedResponse,
|
|
@@ -39,6 +57,8 @@ export function createWecomBotWebhookHandler({
|
|
|
39
57
|
processBotInboundMessage,
|
|
40
58
|
deliverBotReplyText,
|
|
41
59
|
finishBotStream,
|
|
60
|
+
recordInboundMetric,
|
|
61
|
+
recordRuntimeErrorMetric,
|
|
42
62
|
randomUuid: () => crypto.randomUUID?.(),
|
|
43
63
|
});
|
|
44
64
|
|
|
@@ -51,9 +71,9 @@ export function createWecomBotWebhookHandler({
|
|
|
51
71
|
const echostr = url.searchParams.get("echostr") ?? "";
|
|
52
72
|
|
|
53
73
|
if (req.method === "GET" && !echostr) {
|
|
54
|
-
res.statusCode = 200;
|
|
74
|
+
res.statusCode = signedBotConfigs.length > 0 ? 200 : 500;
|
|
55
75
|
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
56
|
-
res.end("wecom bot webhook ok");
|
|
76
|
+
res.end(signedBotConfigs.length > 0 ? "wecom bot webhook ok" : "wecom bot webhook not configured");
|
|
57
77
|
return;
|
|
58
78
|
}
|
|
59
79
|
|
|
@@ -64,26 +84,28 @@ export function createWecomBotWebhookHandler({
|
|
|
64
84
|
res.end("Missing query params");
|
|
65
85
|
return;
|
|
66
86
|
}
|
|
67
|
-
const
|
|
68
|
-
|
|
87
|
+
const matchedBotConfig = pickBotConfigBySignature({
|
|
88
|
+
msgSignature: msg_signature,
|
|
69
89
|
timestamp,
|
|
70
90
|
nonce,
|
|
71
91
|
encrypt: echostr,
|
|
72
92
|
});
|
|
73
|
-
if (
|
|
93
|
+
if (!matchedBotConfig) {
|
|
74
94
|
res.statusCode = 401;
|
|
75
95
|
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
76
96
|
res.end("Invalid signature");
|
|
77
97
|
return;
|
|
78
98
|
}
|
|
79
99
|
const { msg: plainEchostr } = decryptWecom({
|
|
80
|
-
aesKey:
|
|
100
|
+
aesKey: matchedBotConfig.encodingAesKey,
|
|
81
101
|
cipherTextBase64: echostr,
|
|
82
102
|
});
|
|
83
103
|
res.statusCode = 200;
|
|
84
104
|
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
85
105
|
res.end(plainEchostr);
|
|
86
|
-
api.logger.info?.(
|
|
106
|
+
api.logger.info?.(
|
|
107
|
+
`wecom(bot): verified callback URL at ${normalizedPath} (account=${matchedBotConfig.accountId || "default"})`,
|
|
108
|
+
);
|
|
87
109
|
return;
|
|
88
110
|
}
|
|
89
111
|
|
|
@@ -114,13 +136,13 @@ export function createWecomBotWebhookHandler({
|
|
|
114
136
|
return;
|
|
115
137
|
}
|
|
116
138
|
|
|
117
|
-
const
|
|
118
|
-
|
|
139
|
+
const matchedBotConfig = pickBotConfigBySignature({
|
|
140
|
+
msgSignature: msg_signature,
|
|
119
141
|
timestamp,
|
|
120
142
|
nonce,
|
|
121
143
|
encrypt: encryptedBody,
|
|
122
144
|
});
|
|
123
|
-
if (
|
|
145
|
+
if (!matchedBotConfig) {
|
|
124
146
|
res.statusCode = 401;
|
|
125
147
|
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
126
148
|
res.end("Invalid signature");
|
|
@@ -130,7 +152,7 @@ export function createWecomBotWebhookHandler({
|
|
|
130
152
|
let incomingPayload = null;
|
|
131
153
|
try {
|
|
132
154
|
const { msg: decryptedPayload } = decryptWecom({
|
|
133
|
-
aesKey:
|
|
155
|
+
aesKey: matchedBotConfig.encodingAesKey,
|
|
134
156
|
cipherTextBase64: encryptedBody,
|
|
135
157
|
});
|
|
136
158
|
incomingPayload = parseIncomingJson(decryptedPayload);
|
|
@@ -143,12 +165,18 @@ export function createWecomBotWebhookHandler({
|
|
|
143
165
|
}
|
|
144
166
|
|
|
145
167
|
const parsed = parseWecomBotInboundMessage(incomingPayload);
|
|
146
|
-
|
|
168
|
+
if (parsed && typeof parsed === "object") {
|
|
169
|
+
parsed.accountId = String(matchedBotConfig.accountId ?? "default").trim().toLowerCase() || "default";
|
|
170
|
+
}
|
|
171
|
+
api.logger.info?.(
|
|
172
|
+
`wecom(bot): inbound ${describeWecomBotParsedMessage(parsed)} account=${matchedBotConfig.accountId || "default"}`,
|
|
173
|
+
);
|
|
147
174
|
const handled = await dispatchParsed({
|
|
148
175
|
parsed,
|
|
149
176
|
res,
|
|
150
177
|
timestamp,
|
|
151
178
|
nonce,
|
|
179
|
+
botConfig: matchedBotConfig,
|
|
152
180
|
});
|
|
153
181
|
if (handled) {
|
|
154
182
|
return;
|
|
@@ -159,6 +187,10 @@ export function createWecomBotWebhookHandler({
|
|
|
159
187
|
res.end("success");
|
|
160
188
|
} catch (err) {
|
|
161
189
|
api.logger.error?.(`wecom(bot): webhook handler failed: ${String(err?.message || err)}`);
|
|
190
|
+
recordRuntimeErrorMetric({
|
|
191
|
+
scope: "bot-webhook",
|
|
192
|
+
reason: String(err?.message || err),
|
|
193
|
+
});
|
|
162
194
|
if (!res.writableEnded) {
|
|
163
195
|
res.statusCode = 500;
|
|
164
196
|
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|