@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
|
@@ -2,9 +2,12 @@ export function createWecomPolicyResolvers({
|
|
|
2
2
|
getGatewayRuntime,
|
|
3
3
|
normalizeAccountId,
|
|
4
4
|
resolveWecomBotModeConfig,
|
|
5
|
+
resolveWecomBotModeAccountsConfig,
|
|
5
6
|
resolveWecomProxyConfig,
|
|
6
7
|
resolveWecomCommandPolicyConfig,
|
|
7
8
|
resolveWecomAllowFromPolicyConfig,
|
|
9
|
+
resolveWecomDmPolicyConfig,
|
|
10
|
+
resolveWecomEventPolicyConfig,
|
|
8
11
|
resolveWecomGroupChatConfig,
|
|
9
12
|
resolveWecomDebounceConfig,
|
|
10
13
|
resolveWecomStreamingConfig,
|
|
@@ -31,16 +34,59 @@ export function createWecomPolicyResolvers({
|
|
|
31
34
|
};
|
|
32
35
|
}
|
|
33
36
|
|
|
34
|
-
function
|
|
35
|
-
|
|
37
|
+
function resolveWecomBotConfigs(api) {
|
|
38
|
+
const inputs = resolveWecomPolicyInputs(api);
|
|
39
|
+
if (typeof resolveWecomBotModeAccountsConfig === "function") {
|
|
40
|
+
return resolveWecomBotModeAccountsConfig(inputs);
|
|
41
|
+
}
|
|
42
|
+
return [resolveWecomBotModeConfig(inputs)];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function resolveWecomBotConfig(api, accountId = "default") {
|
|
46
|
+
const normalizedAccountId = normalizeAccountId(accountId ?? "default");
|
|
47
|
+
const configs = resolveWecomBotConfigs(api);
|
|
48
|
+
const matched = configs.find((item) => normalizeAccountId(item?.accountId ?? "default") === normalizedAccountId);
|
|
49
|
+
if (matched) return matched;
|
|
50
|
+
if (normalizedAccountId !== "default") {
|
|
51
|
+
const fallback = configs.find((item) => normalizeAccountId(item?.accountId ?? "default") === "default");
|
|
52
|
+
if (fallback) return fallback;
|
|
53
|
+
}
|
|
54
|
+
return configs[0] ?? resolveWecomBotModeConfig(resolveWecomPolicyInputs(api));
|
|
36
55
|
}
|
|
37
56
|
|
|
38
|
-
function resolveWecomBotProxyConfig(api) {
|
|
57
|
+
function resolveWecomBotProxyConfig(api, accountId = "default") {
|
|
39
58
|
const inputs = resolveWecomPolicyInputs(api);
|
|
59
|
+
const normalizedAccountId = normalizeAccountId(accountId ?? "default");
|
|
60
|
+
const channelConfig = inputs.channelConfig ?? {};
|
|
61
|
+
const accountConfig =
|
|
62
|
+
normalizedAccountId === "default"
|
|
63
|
+
? channelConfig
|
|
64
|
+
: channelConfig?.accounts && typeof channelConfig.accounts === "object"
|
|
65
|
+
? channelConfig.accounts[normalizedAccountId] ?? {}
|
|
66
|
+
: {};
|
|
67
|
+
const botConfig = accountConfig?.bot && typeof accountConfig.bot === "object" ? accountConfig.bot : {};
|
|
68
|
+
const envVars = inputs.envVars ?? {};
|
|
69
|
+
const processEnvVars = inputs.processEnv ?? process.env;
|
|
70
|
+
const scopedBotProxyKey =
|
|
71
|
+
normalizedAccountId === "default" ? null : `WECOM_${normalizedAccountId.toUpperCase()}_BOT_PROXY`;
|
|
72
|
+
const scopedBotProxy = String(
|
|
73
|
+
(scopedBotProxyKey ? envVars?.[scopedBotProxyKey] ?? processEnvVars?.[scopedBotProxyKey] : undefined) ??
|
|
74
|
+
envVars?.WECOM_BOT_PROXY ??
|
|
75
|
+
processEnvVars?.WECOM_BOT_PROXY ??
|
|
76
|
+
"",
|
|
77
|
+
).trim();
|
|
78
|
+
const fromBotConfig = String(botConfig?.outboundProxy ?? botConfig?.proxyUrl ?? botConfig?.proxy ?? "").trim();
|
|
79
|
+
if (fromBotConfig) return fromBotConfig;
|
|
80
|
+
if (scopedBotProxy) return scopedBotProxy;
|
|
81
|
+
|
|
82
|
+
const proxyAccountConfig = {
|
|
83
|
+
...(accountConfig && typeof accountConfig === "object" ? accountConfig : {}),
|
|
84
|
+
...(botConfig && typeof botConfig === "object" ? botConfig : {}),
|
|
85
|
+
};
|
|
40
86
|
return resolveWecomProxyConfig({
|
|
41
87
|
...inputs,
|
|
42
|
-
accountId:
|
|
43
|
-
accountConfig:
|
|
88
|
+
accountId: normalizedAccountId,
|
|
89
|
+
accountConfig: proxyAccountConfig,
|
|
44
90
|
});
|
|
45
91
|
}
|
|
46
92
|
|
|
@@ -57,6 +103,34 @@ export function createWecomPolicyResolvers({
|
|
|
57
103
|
});
|
|
58
104
|
}
|
|
59
105
|
|
|
106
|
+
function resolveWecomDmPolicy(api, accountId, accountConfig = {}) {
|
|
107
|
+
const inputs = resolveWecomPolicyInputs(api);
|
|
108
|
+
if (typeof resolveWecomDmPolicyConfig !== "function") {
|
|
109
|
+
return { mode: "open", allowFrom: [], rejectMessage: "当前私聊账号未授权,请联系管理员。", enabled: false };
|
|
110
|
+
}
|
|
111
|
+
return resolveWecomDmPolicyConfig({
|
|
112
|
+
...inputs,
|
|
113
|
+
accountId: normalizeAccountId(accountId ?? "default"),
|
|
114
|
+
accountConfig: accountConfig ?? {},
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function resolveWecomEventPolicy(api, accountId, accountConfig = {}) {
|
|
119
|
+
const inputs = resolveWecomPolicyInputs(api);
|
|
120
|
+
if (typeof resolveWecomEventPolicyConfig !== "function") {
|
|
121
|
+
return {
|
|
122
|
+
enabled: true,
|
|
123
|
+
enterAgentWelcomeEnabled: false,
|
|
124
|
+
enterAgentWelcomeText: "你好,我是 AI 助手,直接发消息即可开始对话。",
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
return resolveWecomEventPolicyConfig({
|
|
128
|
+
...inputs,
|
|
129
|
+
accountId: normalizeAccountId(accountId ?? "default"),
|
|
130
|
+
accountConfig: accountConfig ?? {},
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
60
134
|
function resolveWecomGroupChatPolicy(api) {
|
|
61
135
|
return resolveWecomGroupChatConfig(resolveWecomPolicyInputs(api));
|
|
62
136
|
}
|
|
@@ -91,10 +165,13 @@ export function createWecomPolicyResolvers({
|
|
|
91
165
|
|
|
92
166
|
return {
|
|
93
167
|
resolveWecomPolicyInputs,
|
|
168
|
+
resolveWecomBotConfigs,
|
|
94
169
|
resolveWecomBotConfig,
|
|
95
170
|
resolveWecomBotProxyConfig,
|
|
96
171
|
resolveWecomCommandPolicy,
|
|
97
172
|
resolveWecomAllowFromPolicy,
|
|
173
|
+
resolveWecomDmPolicy,
|
|
174
|
+
resolveWecomEventPolicy,
|
|
98
175
|
resolveWecomGroupChatPolicy,
|
|
99
176
|
resolveWecomTextDebouncePolicy,
|
|
100
177
|
resolveWecomReplyStreamingPolicy,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { analyzeWecomAccountConflicts } from "./account-diagnostics.js";
|
|
2
|
+
|
|
1
3
|
export function createWecomRegisterRuntime({
|
|
2
4
|
setGatewayRuntime,
|
|
3
5
|
syncWecomSessionQueuePolicy,
|
|
@@ -6,6 +8,8 @@ export function createWecomRegisterRuntime({
|
|
|
6
8
|
resolveWecomObservabilityPolicy,
|
|
7
9
|
resolveWecomDynamicAgentPolicy,
|
|
8
10
|
resolveWecomBotConfig,
|
|
11
|
+
resolveWecomBotConfigs,
|
|
12
|
+
listEnabledWecomAccounts,
|
|
9
13
|
getWecomConfig,
|
|
10
14
|
wecomChannelPlugin,
|
|
11
15
|
wecomRouteRegistrar,
|
|
@@ -31,6 +35,12 @@ export function createWecomRegisterRuntime({
|
|
|
31
35
|
if (typeof resolveWecomBotConfig !== "function") {
|
|
32
36
|
throw new Error("createWecomRegisterRuntime: resolveWecomBotConfig is required");
|
|
33
37
|
}
|
|
38
|
+
if (resolveWecomBotConfigs != null && typeof resolveWecomBotConfigs !== "function") {
|
|
39
|
+
throw new Error("createWecomRegisterRuntime: resolveWecomBotConfigs must be a function");
|
|
40
|
+
}
|
|
41
|
+
if (listEnabledWecomAccounts != null && typeof listEnabledWecomAccounts !== "function") {
|
|
42
|
+
throw new Error("createWecomRegisterRuntime: listEnabledWecomAccounts must be a function");
|
|
43
|
+
}
|
|
34
44
|
if (typeof getWecomConfig !== "function") {
|
|
35
45
|
throw new Error("createWecomRegisterRuntime: getWecomConfig is required");
|
|
36
46
|
}
|
|
@@ -50,14 +60,22 @@ export function createWecomRegisterRuntime({
|
|
|
50
60
|
const dynamicAgentPolicy = resolveWecomDynamicAgentPolicy(api);
|
|
51
61
|
|
|
52
62
|
const botModeConfig = resolveWecomBotConfig(api);
|
|
63
|
+
const botModeConfigs =
|
|
64
|
+
typeof resolveWecomBotConfigs === "function"
|
|
65
|
+
? resolveWecomBotConfigs(api)
|
|
66
|
+
: [botModeConfig];
|
|
67
|
+
const enabledBotConfigs = (Array.isArray(botModeConfigs) ? botModeConfigs : []).filter((item) => item?.enabled === true);
|
|
53
68
|
const cfg = getWecomConfig(api);
|
|
54
69
|
if (cfg) {
|
|
55
70
|
api.logger.info?.(
|
|
56
71
|
`wecom: config loaded (corpId=${cfg.corpId?.slice(0, 8)}..., proxy=${cfg.outboundProxy ? "on" : "off"})`,
|
|
57
72
|
);
|
|
58
|
-
} else if (
|
|
73
|
+
} else if (enabledBotConfigs.length > 0) {
|
|
74
|
+
const webhookSummary = Array.from(
|
|
75
|
+
new Set(enabledBotConfigs.map((item) => String(item?.webhookPath || "/wecom/bot/callback"))),
|
|
76
|
+
).join(", ");
|
|
59
77
|
api.logger.info?.(
|
|
60
|
-
`wecom(bot): config loaded (
|
|
78
|
+
`wecom(bot): config loaded (accounts=${enabledBotConfigs.length}, webhook=${webhookSummary}, streamExpireMs=${botModeConfig.streamExpireMs})`,
|
|
61
79
|
);
|
|
62
80
|
} else {
|
|
63
81
|
api.logger.warn?.("wecom: no configuration found (check channels.wecom in openclaw.json)");
|
|
@@ -83,6 +101,17 @@ export function createWecomRegisterRuntime({
|
|
|
83
101
|
`wecom: dynamic-agent on (mode=${dynamicAgentPolicy.mode}, userMap=${Object.keys(dynamicAgentPolicy.userMap || {}).length}, groupMap=${Object.keys(dynamicAgentPolicy.groupMap || {}).length}, mentionMap=${Object.keys(dynamicAgentPolicy.mentionMap || {}).length})`,
|
|
84
102
|
);
|
|
85
103
|
}
|
|
104
|
+
if (typeof listEnabledWecomAccounts === "function") {
|
|
105
|
+
const accountDiagnostics = analyzeWecomAccountConflicts({
|
|
106
|
+
accounts: listEnabledWecomAccounts(api),
|
|
107
|
+
botConfigs: enabledBotConfigs,
|
|
108
|
+
});
|
|
109
|
+
for (const issue of accountDiagnostics.issues) {
|
|
110
|
+
const line = `wecom: account diagnosis ${issue.code} ${issue.message}`;
|
|
111
|
+
if (issue.severity === "warn") api.logger.warn?.(line);
|
|
112
|
+
else api.logger.info?.(line);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
86
115
|
|
|
87
116
|
api.registerChannel({ plugin: wecomChannelPlugin });
|
|
88
117
|
const botRouteRegistered = wecomRouteRegistrar.registerWecomBotWebhookRoute(api);
|
|
@@ -1,5 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildDefaultAgentWebhookPath,
|
|
3
|
+
buildDefaultBotWebhookPath,
|
|
4
|
+
buildLegacyAgentWebhookPath,
|
|
5
|
+
buildLegacyBotWebhookPath,
|
|
6
|
+
} from "./account-paths.js";
|
|
7
|
+
|
|
1
8
|
export function createWecomRouteRegistrar({
|
|
2
9
|
resolveWecomBotConfig,
|
|
10
|
+
resolveWecomBotConfigs,
|
|
3
11
|
normalizePluginHttpPath,
|
|
4
12
|
ensureBotStreamCleanupTimer,
|
|
5
13
|
cleanupExpiredBotStreams,
|
|
@@ -29,8 +37,13 @@ export function createWecomRouteRegistrar({
|
|
|
29
37
|
deliverBotReplyText,
|
|
30
38
|
finishBotStream,
|
|
31
39
|
groupAccountsByWebhookPath,
|
|
40
|
+
recordInboundMetric = () => {},
|
|
41
|
+
recordRuntimeErrorMetric = () => {},
|
|
32
42
|
} = {}) {
|
|
33
43
|
if (typeof resolveWecomBotConfig !== "function") throw new Error("createWecomRouteRegistrar: resolveWecomBotConfig is required");
|
|
44
|
+
if (typeof resolveWecomBotConfigs !== "function") {
|
|
45
|
+
throw new Error("createWecomRouteRegistrar: resolveWecomBotConfigs is required");
|
|
46
|
+
}
|
|
34
47
|
if (typeof normalizePluginHttpPath !== "function") {
|
|
35
48
|
throw new Error("createWecomRouteRegistrar: normalizePluginHttpPath is required");
|
|
36
49
|
}
|
|
@@ -51,56 +64,173 @@ export function createWecomRouteRegistrar({
|
|
|
51
64
|
}
|
|
52
65
|
|
|
53
66
|
function registerWecomBotWebhookRoute(api) {
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
if (
|
|
67
|
+
const botConfigs = resolveWecomBotConfigs(api);
|
|
68
|
+
const enabledBotConfigs = (Array.isArray(botConfigs) ? botConfigs : []).filter((item) => item?.enabled === true);
|
|
69
|
+
if (enabledBotConfigs.length === 0) return false;
|
|
70
|
+
|
|
71
|
+
const signedBotConfigs = enabledBotConfigs.filter((item) => item?.token && item?.encodingAesKey);
|
|
72
|
+
if (signedBotConfigs.length === 0) {
|
|
57
73
|
api.logger.warn?.("wecom(bot): enabled but missing token/encodingAesKey; route not registered");
|
|
58
74
|
return false;
|
|
59
75
|
}
|
|
60
76
|
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
botConfig
|
|
70
|
-
normalizedPath
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
77
|
+
const grouped = new Map();
|
|
78
|
+
const agentWebhookGroups = groupAccountsByWebhookPath(api);
|
|
79
|
+
const agentPathSet = new Set(
|
|
80
|
+
Array.from(agentWebhookGroups.keys()).map(
|
|
81
|
+
(path) => normalizePluginHttpPath(path ?? "/wecom/callback", "/wecom/callback") ?? "/wecom/callback",
|
|
82
|
+
),
|
|
83
|
+
);
|
|
84
|
+
for (const botConfig of signedBotConfigs) {
|
|
85
|
+
const normalizedAccountId = String(botConfig?.accountId ?? "default").trim().toLowerCase() || "default";
|
|
86
|
+
const normalizedPath =
|
|
87
|
+
normalizePluginHttpPath(botConfig.webhookPath ?? "/wecom/bot/callback", "/wecom/bot/callback") ??
|
|
88
|
+
"/wecom/bot/callback";
|
|
89
|
+
const registerGroupedPath = (candidatePath) => {
|
|
90
|
+
const existing = grouped.get(candidatePath);
|
|
91
|
+
if (existing) existing.push(botConfig);
|
|
92
|
+
else grouped.set(candidatePath, [botConfig]);
|
|
93
|
+
};
|
|
94
|
+
registerGroupedPath(normalizedPath);
|
|
95
|
+
|
|
96
|
+
const normalizedDefaultPath = normalizePluginHttpPath(
|
|
97
|
+
buildDefaultBotWebhookPath(normalizedAccountId),
|
|
98
|
+
"/wecom/bot/callback",
|
|
99
|
+
);
|
|
100
|
+
if (normalizedDefaultPath && normalizedPath === normalizedDefaultPath) {
|
|
101
|
+
const legacyAliasPath =
|
|
102
|
+
normalizePluginHttpPath(buildLegacyBotWebhookPath(normalizedAccountId), "/webhooks/wecom") ??
|
|
103
|
+
"/webhooks/wecom";
|
|
104
|
+
if (legacyAliasPath !== normalizedPath) {
|
|
105
|
+
if (agentPathSet.has(legacyAliasPath)) {
|
|
106
|
+
api.logger.warn?.(
|
|
107
|
+
`wecom(bot): skip legacy alias ${legacyAliasPath} for account=${normalizedAccountId} (conflicts with agent webhook path)`,
|
|
108
|
+
);
|
|
109
|
+
} else {
|
|
110
|
+
registerGroupedPath(legacyAliasPath);
|
|
111
|
+
api.logger.info?.(
|
|
112
|
+
`wecom(bot): registered legacy alias ${legacyAliasPath} for account=${normalizedAccountId}`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let registeredCount = 0;
|
|
120
|
+
for (const [normalizedPath, pathConfigs] of grouped.entries()) {
|
|
121
|
+
const maxStreamExpireMs = pathConfigs.reduce(
|
|
122
|
+
(acc, item) => Math.max(acc, Number(item?.streamExpireMs) || 0),
|
|
123
|
+
0,
|
|
124
|
+
);
|
|
125
|
+
ensureBotStreamCleanupTimer(maxStreamExpireMs || 600000, api.logger);
|
|
126
|
+
cleanupExpiredBotStreams(maxStreamExpireMs || 600000);
|
|
127
|
+
|
|
128
|
+
const handler = createWecomBotWebhookHandler({
|
|
129
|
+
api,
|
|
130
|
+
botConfigs: pathConfigs,
|
|
131
|
+
normalizedPath,
|
|
132
|
+
readRequestBody,
|
|
133
|
+
parseIncomingJson,
|
|
134
|
+
computeMsgSignature,
|
|
135
|
+
decryptWecom,
|
|
136
|
+
parseWecomBotInboundMessage,
|
|
137
|
+
describeWecomBotParsedMessage,
|
|
138
|
+
cleanupExpiredBotStreams,
|
|
139
|
+
getBotStream,
|
|
140
|
+
buildWecomBotEncryptedResponse,
|
|
141
|
+
markInboundMessageSeen,
|
|
142
|
+
buildWecomBotSessionId,
|
|
143
|
+
createBotStream,
|
|
144
|
+
upsertBotResponseUrlCache,
|
|
145
|
+
messageProcessLimiter,
|
|
146
|
+
executeInboundTaskWithSessionQueue,
|
|
147
|
+
processBotInboundMessage,
|
|
148
|
+
deliverBotReplyText,
|
|
149
|
+
finishBotStream,
|
|
150
|
+
recordInboundMetric,
|
|
151
|
+
recordRuntimeErrorMetric,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
api.registerHttpRoute({
|
|
155
|
+
path: normalizedPath,
|
|
156
|
+
auth: "plugin",
|
|
157
|
+
handler,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const accountIds = pathConfigs.map((item) => String(item?.accountId ?? "default")).join(", ");
|
|
161
|
+
api.logger.info?.(`wecom(bot): registered webhook at ${normalizedPath} (accounts=${accountIds})`);
|
|
162
|
+
registeredCount += 1;
|
|
163
|
+
}
|
|
164
|
+
return registeredCount > 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function buildBotWebhookPathSet(api) {
|
|
168
|
+
const botPathSet = new Set();
|
|
169
|
+
const botConfigs = resolveWecomBotConfigs(api);
|
|
170
|
+
const enabledBotConfigs = (Array.isArray(botConfigs) ? botConfigs : []).filter((item) => item?.enabled === true);
|
|
171
|
+
for (const botConfig of enabledBotConfigs) {
|
|
172
|
+
const normalizedAccountId = String(botConfig?.accountId ?? "default").trim().toLowerCase() || "default";
|
|
173
|
+
const normalizedPath =
|
|
174
|
+
normalizePluginHttpPath(botConfig.webhookPath ?? "/wecom/bot/callback", "/wecom/bot/callback") ??
|
|
175
|
+
"/wecom/bot/callback";
|
|
176
|
+
botPathSet.add(normalizedPath);
|
|
177
|
+
|
|
178
|
+
const normalizedDefaultPath = normalizePluginHttpPath(
|
|
179
|
+
buildDefaultBotWebhookPath(normalizedAccountId),
|
|
180
|
+
"/wecom/bot/callback",
|
|
181
|
+
);
|
|
182
|
+
if (normalizedDefaultPath && normalizedPath === normalizedDefaultPath) {
|
|
183
|
+
const legacyAliasPath =
|
|
184
|
+
normalizePluginHttpPath(buildLegacyBotWebhookPath(normalizedAccountId), "/webhooks/wecom") ??
|
|
185
|
+
"/webhooks/wecom";
|
|
186
|
+
botPathSet.add(legacyAliasPath);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return botPathSet;
|
|
99
190
|
}
|
|
100
191
|
|
|
101
192
|
function registerWecomAgentWebhookRoutes(api) {
|
|
102
193
|
const webhookGroups = groupAccountsByWebhookPath(api);
|
|
194
|
+
const grouped = new Map();
|
|
195
|
+
for (const [normalizedPath, accounts] of webhookGroups.entries()) {
|
|
196
|
+
grouped.set(normalizedPath, [...accounts]);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const botPathSet = buildBotWebhookPathSet(api);
|
|
103
200
|
for (const [normalizedPath, accounts] of webhookGroups.entries()) {
|
|
201
|
+
for (const account of accounts) {
|
|
202
|
+
const normalizedAccountId = String(account?.accountId ?? "default").trim().toLowerCase() || "default";
|
|
203
|
+
const normalizedDefaultPath =
|
|
204
|
+
normalizePluginHttpPath(buildDefaultAgentWebhookPath(normalizedAccountId), "/wecom/callback") ??
|
|
205
|
+
"/wecom/callback";
|
|
206
|
+
if (normalizedPath !== normalizedDefaultPath) continue;
|
|
207
|
+
|
|
208
|
+
const legacyAliasPath =
|
|
209
|
+
normalizePluginHttpPath(buildLegacyAgentWebhookPath(normalizedAccountId), "/webhooks/app") ??
|
|
210
|
+
"/webhooks/app";
|
|
211
|
+
if (!legacyAliasPath || legacyAliasPath === normalizedPath) continue;
|
|
212
|
+
if (botPathSet.has(legacyAliasPath)) {
|
|
213
|
+
api.logger.warn?.(
|
|
214
|
+
`wecom: skip legacy agent alias ${legacyAliasPath} for account=${normalizedAccountId} (conflicts with bot webhook path)`,
|
|
215
|
+
);
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const existing = grouped.get(legacyAliasPath);
|
|
220
|
+
if (existing) {
|
|
221
|
+
const duplicated = existing.some(
|
|
222
|
+
(item) =>
|
|
223
|
+
(String(item?.accountId ?? "default").trim().toLowerCase() || "default") === normalizedAccountId,
|
|
224
|
+
);
|
|
225
|
+
if (!duplicated) existing.push(account);
|
|
226
|
+
} else {
|
|
227
|
+
grouped.set(legacyAliasPath, [account]);
|
|
228
|
+
}
|
|
229
|
+
api.logger.info?.(`wecom: registered legacy agent alias ${legacyAliasPath} for account=${normalizedAccountId}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
for (const [normalizedPath, accounts] of grouped.entries()) {
|
|
104
234
|
const handler = createWecomAgentWebhookHandler({
|
|
105
235
|
api,
|
|
106
236
|
accounts,
|
|
@@ -115,6 +245,8 @@ export function createWecomRouteRegistrar({
|
|
|
115
245
|
messageProcessLimiter,
|
|
116
246
|
executeInboundTaskWithSessionQueue,
|
|
117
247
|
processInboundMessage,
|
|
248
|
+
recordInboundMetric,
|
|
249
|
+
recordRuntimeErrorMetric,
|
|
118
250
|
});
|
|
119
251
|
api.registerHttpRoute({
|
|
120
252
|
path: normalizedPath,
|
|
@@ -4,8 +4,13 @@ export function requireEnv(name, fallback, processEnv = process.env) {
|
|
|
4
4
|
return value;
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
-
export function buildWecomBotSessionId(userId) {
|
|
8
|
-
|
|
7
|
+
export function buildWecomBotSessionId(userId, accountId = "default") {
|
|
8
|
+
const normalizedUserId = String(userId ?? "").trim().toLowerCase();
|
|
9
|
+
const normalizedAccountId = String(accountId ?? "default").trim().toLowerCase() || "default";
|
|
10
|
+
if (normalizedAccountId === "default") {
|
|
11
|
+
return `wecom-bot:${normalizedUserId}`;
|
|
12
|
+
}
|
|
13
|
+
return `wecom-bot:${normalizedAccountId}:${normalizedUserId}`;
|
|
9
14
|
}
|
|
10
15
|
|
|
11
16
|
export function asNumber(value, fallback = null) {
|
|
@@ -67,11 +67,22 @@ export function parseWecomBotInboundMessage(payload) {
|
|
|
67
67
|
const imageUrls = [];
|
|
68
68
|
let fileUrl = "";
|
|
69
69
|
let fileName = "";
|
|
70
|
+
let voiceUrl = "";
|
|
71
|
+
let voiceMediaId = "";
|
|
72
|
+
let voiceContentType = "";
|
|
70
73
|
|
|
71
74
|
if (msgType === "text") {
|
|
72
75
|
content = normalizeToken(payload?.text?.content);
|
|
73
76
|
} else if (msgType === "voice") {
|
|
74
77
|
content = normalizeToken(payload?.voice?.content);
|
|
78
|
+
voiceUrl = normalizeToken(
|
|
79
|
+
payload?.voice?.url ||
|
|
80
|
+
payload?.voice?.media_url ||
|
|
81
|
+
payload?.voice?.download_url ||
|
|
82
|
+
payload?.voice?.file_url,
|
|
83
|
+
);
|
|
84
|
+
voiceMediaId = normalizeToken(payload?.voice?.media_id || payload?.voice?.mediaid || payload?.voice?.id);
|
|
85
|
+
voiceContentType = normalizeToken(payload?.voice?.content_type || payload?.voice?.mime_type || payload?.voice?.format);
|
|
75
86
|
} else if (msgType === "link") {
|
|
76
87
|
const title = normalizeToken(payload?.link?.title);
|
|
77
88
|
const description = normalizeToken(payload?.link?.description);
|
|
@@ -99,6 +110,51 @@ export function parseWecomBotInboundMessage(payload) {
|
|
|
99
110
|
imageUrls.push(...itemImageUrls);
|
|
100
111
|
parts.push("[图片]");
|
|
101
112
|
}
|
|
113
|
+
} else if (itemType === "voice") {
|
|
114
|
+
const itemVoiceUrl = normalizeToken(
|
|
115
|
+
item?.voice?.url ||
|
|
116
|
+
item?.voice?.media_url ||
|
|
117
|
+
item?.voice?.download_url ||
|
|
118
|
+
item?.voice?.file_url,
|
|
119
|
+
);
|
|
120
|
+
const itemVoiceMediaId = normalizeToken(item?.voice?.media_id || item?.voice?.mediaid || item?.voice?.id);
|
|
121
|
+
const itemVoiceContentType = normalizeToken(
|
|
122
|
+
item?.voice?.content_type || item?.voice?.mime_type || item?.voice?.format,
|
|
123
|
+
);
|
|
124
|
+
if (itemVoiceUrl) {
|
|
125
|
+
voiceUrl = voiceUrl || itemVoiceUrl;
|
|
126
|
+
voiceMediaId = voiceMediaId || itemVoiceMediaId;
|
|
127
|
+
voiceContentType = voiceContentType || itemVoiceContentType;
|
|
128
|
+
parts.push("[语音]");
|
|
129
|
+
}
|
|
130
|
+
} else if (itemType === "file") {
|
|
131
|
+
const itemFileUrl = normalizeToken(
|
|
132
|
+
item?.file?.url ||
|
|
133
|
+
item?.file?.download_url ||
|
|
134
|
+
item?.file?.media_url ||
|
|
135
|
+
item?.file?.file_url,
|
|
136
|
+
);
|
|
137
|
+
const itemFileName = normalizeToken(item?.file?.name || item?.file?.filename);
|
|
138
|
+
if (itemFileUrl || itemFileName) {
|
|
139
|
+
fileUrl = fileUrl || itemFileUrl;
|
|
140
|
+
fileName = fileName || itemFileName;
|
|
141
|
+
const displayName = itemFileName || itemFileUrl || "附件";
|
|
142
|
+
parts.push(`[文件] ${displayName}`);
|
|
143
|
+
}
|
|
144
|
+
} else if (itemType === "link") {
|
|
145
|
+
const title = normalizeToken(item?.link?.title);
|
|
146
|
+
const description = normalizeToken(item?.link?.description);
|
|
147
|
+
const url = normalizeToken(item?.link?.url);
|
|
148
|
+
const linkText = [title ? `[链接] ${title}` : "", description, url].filter(Boolean).join("\n").trim();
|
|
149
|
+
if (linkText) parts.push(linkText);
|
|
150
|
+
} else if (itemType === "location") {
|
|
151
|
+
const latitude = normalizeToken(item?.location?.latitude);
|
|
152
|
+
const longitude = normalizeToken(item?.location?.longitude);
|
|
153
|
+
const name = normalizeToken(item?.location?.name || item?.location?.label);
|
|
154
|
+
const locationText = name ? `[位置] ${name} (${latitude}, ${longitude})` : `[位置] ${latitude}, ${longitude}`;
|
|
155
|
+
if (locationText.trim() !== "[位置] ,") {
|
|
156
|
+
parts.push(locationText);
|
|
157
|
+
}
|
|
102
158
|
}
|
|
103
159
|
}
|
|
104
160
|
content = parts.join("\n").trim();
|
|
@@ -143,6 +199,9 @@ export function parseWecomBotInboundMessage(payload) {
|
|
|
143
199
|
imageUrls: dedupeUrlList(imageUrls),
|
|
144
200
|
fileUrl,
|
|
145
201
|
fileName,
|
|
202
|
+
voiceUrl,
|
|
203
|
+
voiceMediaId,
|
|
204
|
+
voiceContentType,
|
|
146
205
|
feedbackId,
|
|
147
206
|
quote,
|
|
148
207
|
isGroupChat: chatType === "group" || Boolean(chatId),
|
|
@@ -178,6 +237,8 @@ export function extractWecomXmlInboundEnvelope(msgObj) {
|
|
|
178
237
|
fromUser: normalizeToken(msgObj.FromUserName),
|
|
179
238
|
chatId: normalizeToken(msgObj.ChatId),
|
|
180
239
|
msgId: normalizeToken(msgObj.MsgId),
|
|
240
|
+
eventType: normalizeLowerToken(msgObj.Event),
|
|
241
|
+
eventKey: normalizeToken(msgObj.EventKey),
|
|
181
242
|
content: normalizeToken(msgObj.Content),
|
|
182
243
|
mediaId: normalizeToken(msgObj.MediaId),
|
|
183
244
|
picUrl: normalizeToken(msgObj.PicUrl),
|
package/src/wecom/webhook-bot.js
CHANGED
|
@@ -66,6 +66,32 @@ export async function webhookSendMarkdown({
|
|
|
66
66
|
});
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
export async function webhookSendTemplateCard({
|
|
70
|
+
url,
|
|
71
|
+
key,
|
|
72
|
+
templateCard,
|
|
73
|
+
timeoutMs = 15000,
|
|
74
|
+
dispatcher,
|
|
75
|
+
fetchImpl = fetch,
|
|
76
|
+
} = {}) {
|
|
77
|
+
const sendUrl = resolveWebhookBotSendUrl({ url, key });
|
|
78
|
+
if (!sendUrl) throw new Error("missing webhook bot url/key");
|
|
79
|
+
if (!templateCard || typeof templateCard !== "object") {
|
|
80
|
+
throw new Error("templateCard payload is required");
|
|
81
|
+
}
|
|
82
|
+
const body = {
|
|
83
|
+
msgtype: "template_card",
|
|
84
|
+
template_card: templateCard,
|
|
85
|
+
};
|
|
86
|
+
return postWebhookJson({
|
|
87
|
+
url: sendUrl,
|
|
88
|
+
body,
|
|
89
|
+
timeoutMs,
|
|
90
|
+
dispatcher,
|
|
91
|
+
fetchImpl,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
69
95
|
export async function webhookSendImage({
|
|
70
96
|
url,
|
|
71
97
|
key,
|