@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
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { buildDefaultAgentWebhookPath } from "./account-paths.js";
|
|
2
|
+
|
|
1
3
|
export function asNumber(v, fallback = null) {
|
|
2
4
|
if (v == null) return fallback;
|
|
3
5
|
const n = Number(v);
|
|
@@ -30,15 +32,30 @@ export function normalizeAccountConfig({ raw, accountId, normalizeWecomWebhookTa
|
|
|
30
32
|
if (!raw || typeof raw !== "object") return null;
|
|
31
33
|
if (typeof normalizeWecomWebhookTargetMap !== "function") return null;
|
|
32
34
|
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
const
|
|
35
|
+
const legacyAgent = raw.agent && typeof raw.agent === "object" ? raw.agent : {};
|
|
36
|
+
const hasLegacyAgentBlock = Object.keys(legacyAgent).length > 0;
|
|
37
|
+
|
|
38
|
+
const corpId = pickFirstNonEmptyString(raw.corpId, legacyAgent.corpId);
|
|
39
|
+
const corpSecret = pickFirstNonEmptyString(raw.corpSecret, legacyAgent.corpSecret);
|
|
40
|
+
const agentId = asNumber(raw.agentId ?? legacyAgent.agentId);
|
|
41
|
+
const callbackToken = pickFirstNonEmptyString(
|
|
42
|
+
raw.callbackToken,
|
|
43
|
+
legacyAgent.callbackToken,
|
|
44
|
+
legacyAgent.token,
|
|
45
|
+
hasLegacyAgentBlock ? "" : raw.token,
|
|
46
|
+
);
|
|
47
|
+
const callbackAesKey = pickFirstNonEmptyString(
|
|
48
|
+
raw.callbackAesKey,
|
|
49
|
+
legacyAgent.callbackAesKey,
|
|
50
|
+
legacyAgent.encodingAesKey,
|
|
51
|
+
hasLegacyAgentBlock ? "" : raw.encodingAesKey,
|
|
52
|
+
);
|
|
53
|
+
const defaultWebhookPath = buildDefaultAgentWebhookPath(normalizedId);
|
|
54
|
+
const webhookPath = String(raw.webhookPath ?? legacyAgent.webhookPath ?? defaultWebhookPath).trim() || defaultWebhookPath;
|
|
55
|
+
const name = pickFirstNonEmptyString(raw.name, normalizedId);
|
|
39
56
|
const outboundProxy = String(raw.outboundProxy ?? raw.proxyUrl ?? raw.proxy ?? "").trim();
|
|
40
57
|
const webhooks = normalizeWecomWebhookTargetMap(raw.webhooks);
|
|
41
|
-
const allowFrom = raw.allowFrom;
|
|
58
|
+
const allowFrom = raw.allowFrom ?? raw.dm?.allowFrom;
|
|
42
59
|
const allowFromRejectMessage = String(raw.allowFromRejectMessage ?? raw.rejectUnauthorizedMessage ?? "").trim();
|
|
43
60
|
|
|
44
61
|
if (!corpId || !corpSecret || !agentId) {
|
|
@@ -47,12 +64,14 @@ export function normalizeAccountConfig({ raw, accountId, normalizeWecomWebhookTa
|
|
|
47
64
|
|
|
48
65
|
return {
|
|
49
66
|
accountId: normalizedId,
|
|
67
|
+
name: normalizedId,
|
|
50
68
|
corpId,
|
|
51
69
|
corpSecret,
|
|
52
70
|
agentId,
|
|
53
71
|
callbackToken,
|
|
54
72
|
callbackAesKey,
|
|
55
73
|
webhookPath,
|
|
74
|
+
name,
|
|
56
75
|
outboundProxy: outboundProxy || undefined,
|
|
57
76
|
webhooks: Object.keys(webhooks).length > 0 ? webhooks : undefined,
|
|
58
77
|
allowFrom,
|
|
@@ -84,7 +103,8 @@ export function readAccountConfigFromEnv({
|
|
|
84
103
|
const agentId = asNumber(readVar("AGENT_ID"));
|
|
85
104
|
const callbackToken = pickFirstNonEmptyString(readVar("CALLBACK_TOKEN"), readVar("TOKEN"));
|
|
86
105
|
const callbackAesKey = pickFirstNonEmptyString(readVar("CALLBACK_AES_KEY"), readVar("ENCODING_AES_KEY"));
|
|
87
|
-
const
|
|
106
|
+
const defaultWebhookPath = buildDefaultAgentWebhookPath(normalizedId);
|
|
107
|
+
const webhookPath = String(readVar("WEBHOOK_PATH") ?? defaultWebhookPath).trim() || defaultWebhookPath;
|
|
88
108
|
const outboundProxyRaw =
|
|
89
109
|
readVar("PROXY") ??
|
|
90
110
|
(normalizedId === "default"
|
|
@@ -7,6 +7,58 @@ import {
|
|
|
7
7
|
} from "./account-config-core.js";
|
|
8
8
|
import { normalizePluginHttpPath } from "./http-path.js";
|
|
9
9
|
|
|
10
|
+
const LEGACY_INLINE_ACCOUNT_RESERVED_KEYS = new Set([
|
|
11
|
+
"name",
|
|
12
|
+
"enabled",
|
|
13
|
+
"corpId",
|
|
14
|
+
"corpSecret",
|
|
15
|
+
"agentId",
|
|
16
|
+
"callbackToken",
|
|
17
|
+
"token",
|
|
18
|
+
"callbackAesKey",
|
|
19
|
+
"encodingAesKey",
|
|
20
|
+
"webhookPath",
|
|
21
|
+
"outboundProxy",
|
|
22
|
+
"proxyUrl",
|
|
23
|
+
"proxy",
|
|
24
|
+
"webhooks",
|
|
25
|
+
"allowFrom",
|
|
26
|
+
"allowFromRejectMessage",
|
|
27
|
+
"rejectUnauthorizedMessage",
|
|
28
|
+
"adminUsers",
|
|
29
|
+
"commandAllowlist",
|
|
30
|
+
"commandBlockMessage",
|
|
31
|
+
"commands",
|
|
32
|
+
"workspaceTemplate",
|
|
33
|
+
"groupChat",
|
|
34
|
+
"dynamicAgent",
|
|
35
|
+
"dynamicAgents",
|
|
36
|
+
"dm",
|
|
37
|
+
"debounce",
|
|
38
|
+
"streaming",
|
|
39
|
+
"bot",
|
|
40
|
+
"delivery",
|
|
41
|
+
"webhookBot",
|
|
42
|
+
"stream",
|
|
43
|
+
"observability",
|
|
44
|
+
"voiceTranscription",
|
|
45
|
+
"accounts",
|
|
46
|
+
"agent",
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
function listLegacyInlineAccountEntries(channelConfig) {
|
|
50
|
+
if (!channelConfig || typeof channelConfig !== "object") return [];
|
|
51
|
+
const entries = [];
|
|
52
|
+
for (const [rawKey, value] of Object.entries(channelConfig)) {
|
|
53
|
+
const accountId = normalizeAccountId(rawKey);
|
|
54
|
+
if (!accountId) continue;
|
|
55
|
+
if (LEGACY_INLINE_ACCOUNT_RESERVED_KEYS.has(accountId)) continue;
|
|
56
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) continue;
|
|
57
|
+
entries.push([accountId, value]);
|
|
58
|
+
}
|
|
59
|
+
return entries;
|
|
60
|
+
}
|
|
61
|
+
|
|
10
62
|
export function createWecomAccountRegistry({
|
|
11
63
|
normalizeWecomWebhookTargetMap,
|
|
12
64
|
resolveWecomProxyConfig,
|
|
@@ -54,6 +106,9 @@ export function createWecomAccountRegistry({
|
|
|
54
106
|
upsert(accountId, accountConfig);
|
|
55
107
|
}
|
|
56
108
|
}
|
|
109
|
+
for (const [accountId, accountConfig] of listLegacyInlineAccountEntries(channelConfig)) {
|
|
110
|
+
upsert(accountId, accountConfig);
|
|
111
|
+
}
|
|
57
112
|
|
|
58
113
|
const envAccountIds = collectWecomEnvAccountIds({ envVars, processEnv });
|
|
59
114
|
for (const accountId of envAccountIds) {
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { normalizePluginHttpPath } from "./http-path.js";
|
|
2
|
+
import { buildDefaultAgentWebhookPath, buildDefaultBotWebhookPath } from "./account-paths.js";
|
|
3
|
+
|
|
4
|
+
function normalizeAccountId(accountId) {
|
|
5
|
+
const normalized = String(accountId ?? "default").trim().toLowerCase();
|
|
6
|
+
return normalized || "default";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function pushMapList(map, key, value) {
|
|
10
|
+
if (!key) return;
|
|
11
|
+
const existing = map.get(key);
|
|
12
|
+
if (existing) existing.push(value);
|
|
13
|
+
else map.set(key, [value]);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function detectDuplicateMapEntries(map, minimum = 2) {
|
|
17
|
+
const out = [];
|
|
18
|
+
for (const [key, values] of map.entries()) {
|
|
19
|
+
if (!Array.isArray(values) || values.length < minimum) continue;
|
|
20
|
+
out.push({ key, values: [...values] });
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function analyzeWecomAccountConflicts({ accounts = [], botConfigs = [] } = {}) {
|
|
26
|
+
const issues = [];
|
|
27
|
+
const enabledAccounts = (Array.isArray(accounts) ? accounts : []).filter((item) => item?.enabled !== false);
|
|
28
|
+
const enabledBotConfigs = (Array.isArray(botConfigs) ? botConfigs : []).filter((item) => item?.enabled === true);
|
|
29
|
+
|
|
30
|
+
const agentTokenToAccounts = new Map();
|
|
31
|
+
const corpAgentToAccounts = new Map();
|
|
32
|
+
const agentPathToAccounts = new Map();
|
|
33
|
+
const botTokenToAccounts = new Map();
|
|
34
|
+
const botPathToAccounts = new Map();
|
|
35
|
+
|
|
36
|
+
for (const account of enabledAccounts) {
|
|
37
|
+
const accountId = normalizeAccountId(account?.accountId);
|
|
38
|
+
const callbackToken = String(account?.callbackToken ?? "").trim();
|
|
39
|
+
const corpId = String(account?.corpId ?? "").trim().toLowerCase();
|
|
40
|
+
const agentId = String(account?.agentId ?? "").trim();
|
|
41
|
+
const normalizedPath =
|
|
42
|
+
normalizePluginHttpPath(
|
|
43
|
+
String(account?.webhookPath ?? "").trim() || buildDefaultAgentWebhookPath(accountId),
|
|
44
|
+
"/wecom/callback",
|
|
45
|
+
) ?? "/wecom/callback";
|
|
46
|
+
|
|
47
|
+
if (callbackToken) pushMapList(agentTokenToAccounts, callbackToken, accountId);
|
|
48
|
+
if (corpId && agentId) pushMapList(corpAgentToAccounts, `${corpId}:${agentId}`, accountId);
|
|
49
|
+
pushMapList(agentPathToAccounts, normalizedPath, accountId);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
for (const botConfig of enabledBotConfigs) {
|
|
53
|
+
const accountId = normalizeAccountId(botConfig?.accountId);
|
|
54
|
+
const token = String(botConfig?.token ?? "").trim();
|
|
55
|
+
const normalizedPath =
|
|
56
|
+
normalizePluginHttpPath(
|
|
57
|
+
String(botConfig?.webhookPath ?? "").trim() || buildDefaultBotWebhookPath(accountId),
|
|
58
|
+
"/wecom/bot/callback",
|
|
59
|
+
) ?? "/wecom/bot/callback";
|
|
60
|
+
if (token) pushMapList(botTokenToAccounts, token, accountId);
|
|
61
|
+
pushMapList(botPathToAccounts, normalizedPath, accountId);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for (const dup of detectDuplicateMapEntries(agentTokenToAccounts)) {
|
|
65
|
+
issues.push({
|
|
66
|
+
severity: "warn",
|
|
67
|
+
code: "agent-duplicate-callback-token",
|
|
68
|
+
message: `Agent callbackToken duplicated across accounts: ${dup.values.join(", ")}`,
|
|
69
|
+
value: dup.key,
|
|
70
|
+
accounts: dup.values,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
for (const dup of detectDuplicateMapEntries(corpAgentToAccounts)) {
|
|
74
|
+
issues.push({
|
|
75
|
+
severity: "warn",
|
|
76
|
+
code: "agent-duplicate-corp-agent",
|
|
77
|
+
message: `Agent corpId+agentId duplicated across accounts: ${dup.values.join(", ")}`,
|
|
78
|
+
value: dup.key,
|
|
79
|
+
accounts: dup.values,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
for (const dup of detectDuplicateMapEntries(botTokenToAccounts)) {
|
|
83
|
+
issues.push({
|
|
84
|
+
severity: "warn",
|
|
85
|
+
code: "bot-duplicate-token",
|
|
86
|
+
message: `Bot token duplicated across accounts: ${dup.values.join(", ")}`,
|
|
87
|
+
value: dup.key,
|
|
88
|
+
accounts: dup.values,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
for (const dup of detectDuplicateMapEntries(agentPathToAccounts)) {
|
|
92
|
+
issues.push({
|
|
93
|
+
severity: "info",
|
|
94
|
+
code: "agent-shared-webhook-path",
|
|
95
|
+
message: `Agent webhook path shared by accounts: ${dup.key} <- ${dup.values.join(", ")}`,
|
|
96
|
+
value: dup.key,
|
|
97
|
+
accounts: dup.values,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
for (const dup of detectDuplicateMapEntries(botPathToAccounts)) {
|
|
101
|
+
issues.push({
|
|
102
|
+
severity: "info",
|
|
103
|
+
code: "bot-shared-webhook-path",
|
|
104
|
+
message: `Bot webhook path shared by accounts: ${dup.key} <- ${dup.values.join(", ")}`,
|
|
105
|
+
value: dup.key,
|
|
106
|
+
accounts: dup.values,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
ok: !issues.some((item) => item.severity === "warn"),
|
|
112
|
+
issues,
|
|
113
|
+
counts: {
|
|
114
|
+
accounts: enabledAccounts.length,
|
|
115
|
+
botAccounts: enabledBotConfigs.length,
|
|
116
|
+
warnings: issues.filter((item) => item.severity === "warn").length,
|
|
117
|
+
info: issues.filter((item) => item.severity === "info").length,
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
function normalizeAccountId(accountId) {
|
|
2
|
+
const normalized = String(accountId ?? "default").trim().toLowerCase();
|
|
3
|
+
return normalized || "default";
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function buildWebhookPathAccountSlug(accountId) {
|
|
7
|
+
const normalizedId = normalizeAccountId(accountId);
|
|
8
|
+
if (normalizedId === "default") return "default";
|
|
9
|
+
const slug = normalizedId.replace(/[^a-z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
10
|
+
return slug || "default";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function buildDefaultAgentWebhookPath(accountId) {
|
|
14
|
+
const normalizedId = normalizeAccountId(accountId);
|
|
15
|
+
if (normalizedId === "default") return "/wecom/callback";
|
|
16
|
+
const slug = buildWebhookPathAccountSlug(normalizedId);
|
|
17
|
+
return `/wecom/${slug}/callback`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function buildLegacyAgentWebhookPath(accountId) {
|
|
21
|
+
const normalizedId = normalizeAccountId(accountId);
|
|
22
|
+
if (normalizedId === "default") return "/webhooks/app";
|
|
23
|
+
const slug = buildWebhookPathAccountSlug(normalizedId);
|
|
24
|
+
return `/webhooks/app/${slug}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function buildDefaultBotWebhookPath(accountId) {
|
|
28
|
+
const normalizedId = normalizeAccountId(accountId);
|
|
29
|
+
if (normalizedId === "default") return "/wecom/bot/callback";
|
|
30
|
+
const slug = buildWebhookPathAccountSlug(normalizedId);
|
|
31
|
+
return `/wecom/${slug}/bot/callback`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function buildLegacyBotWebhookPath(accountId) {
|
|
35
|
+
const normalizedId = normalizeAccountId(accountId);
|
|
36
|
+
if (normalizedId === "default") return "/webhooks/wecom";
|
|
37
|
+
const slug = buildWebhookPathAccountSlug(normalizedId);
|
|
38
|
+
return `/webhooks/wecom/${slug}`;
|
|
39
|
+
}
|
|
@@ -53,6 +53,15 @@ const ASYNC_INBOUND_HANDLERS = {
|
|
|
53
53
|
linkPicUrl: inbound.linkPicUrl,
|
|
54
54
|
}),
|
|
55
55
|
},
|
|
56
|
+
event: {
|
|
57
|
+
requiresMediaId: false,
|
|
58
|
+
errorLabel: "event",
|
|
59
|
+
buildTaskPayload: (inbound) => ({
|
|
60
|
+
msgType: "event",
|
|
61
|
+
eventType: inbound.eventType,
|
|
62
|
+
eventKey: inbound.eventKey,
|
|
63
|
+
}),
|
|
64
|
+
},
|
|
56
65
|
};
|
|
57
66
|
|
|
58
67
|
function enqueueInboundTask({
|
|
@@ -20,6 +20,7 @@ export async function applyWecomAgentInboundGuards({
|
|
|
20
20
|
stripWecomGroupMentions,
|
|
21
21
|
resolveWecomCommandPolicy,
|
|
22
22
|
resolveWecomAllowFromPolicy,
|
|
23
|
+
resolveWecomDmPolicy,
|
|
23
24
|
isWecomSenderAllowed,
|
|
24
25
|
extractLeadingSlashCommand,
|
|
25
26
|
COMMANDS,
|
|
@@ -31,6 +32,7 @@ export async function applyWecomAgentInboundGuards({
|
|
|
31
32
|
assertFunction("stripWecomGroupMentions", stripWecomGroupMentions);
|
|
32
33
|
assertFunction("resolveWecomCommandPolicy", resolveWecomCommandPolicy);
|
|
33
34
|
assertFunction("resolveWecomAllowFromPolicy", resolveWecomAllowFromPolicy);
|
|
35
|
+
assertFunction("resolveWecomDmPolicy", resolveWecomDmPolicy);
|
|
34
36
|
assertFunction("isWecomSenderAllowed", isWecomSenderAllowed);
|
|
35
37
|
assertFunction("extractLeadingSlashCommand", extractLeadingSlashCommand);
|
|
36
38
|
assertFunction("sendTextToUser", sendTextToUser);
|
|
@@ -59,6 +61,23 @@ export async function applyWecomAgentInboundGuards({
|
|
|
59
61
|
|
|
60
62
|
const commandPolicy = resolveWecomCommandPolicy(api);
|
|
61
63
|
const isAdminUser = commandPolicy.adminUsers.includes(normalizedFromUser);
|
|
64
|
+
const dmPolicy = resolveWecomDmPolicy(api, config?.accountId || accountId || "default", config);
|
|
65
|
+
if (!isGroupChat) {
|
|
66
|
+
if (dmPolicy.mode === "deny") {
|
|
67
|
+
await sendTextToUser(dmPolicy.rejectMessage || "当前渠道私聊已关闭,请联系管理员。");
|
|
68
|
+
return { ok: false, commandBody: nextCommandBody, isAdminUser };
|
|
69
|
+
}
|
|
70
|
+
if (dmPolicy.mode === "allowlist") {
|
|
71
|
+
const dmSenderAllowed = isAdminUser || isWecomSenderAllowed({
|
|
72
|
+
senderId: normalizedFromUser,
|
|
73
|
+
allowFrom: dmPolicy.allowFrom,
|
|
74
|
+
});
|
|
75
|
+
if (!dmSenderAllowed) {
|
|
76
|
+
await sendTextToUser(dmPolicy.rejectMessage || "当前私聊账号未授权,请联系管理员。");
|
|
77
|
+
return { ok: false, commandBody: nextCommandBody, isAdminUser };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
62
81
|
const allowFromPolicy = resolveWecomAllowFromPolicy(api, config?.accountId || accountId || "default", config);
|
|
63
82
|
const senderAllowed = isAdminUser || isWecomSenderAllowed({
|
|
64
83
|
senderId: normalizedFromUser,
|
|
@@ -76,15 +95,16 @@ export async function applyWecomAgentInboundGuards({
|
|
|
76
95
|
|
|
77
96
|
if (msgType === "text") {
|
|
78
97
|
let commandKey = extractLeadingSlashCommand(nextCommandBody);
|
|
79
|
-
if (commandKey === "/clear") {
|
|
80
|
-
api?.logger?.info?.(
|
|
81
|
-
nextCommandBody = nextCommandBody.replace(/^\/clear\b/i, "/reset");
|
|
98
|
+
if (commandKey === "/clear" || commandKey === "/new") {
|
|
99
|
+
api?.logger?.info?.(`wecom: translating ${commandKey} to native /reset command`);
|
|
100
|
+
nextCommandBody = nextCommandBody.replace(/^\/(?:clear|new)\b/i, "/reset");
|
|
82
101
|
commandKey = "/reset";
|
|
83
102
|
}
|
|
84
103
|
if (commandKey) {
|
|
85
104
|
const commandAllowed =
|
|
86
105
|
commandPolicy.allowlist.includes(commandKey) ||
|
|
87
|
-
(commandKey === "/reset" &&
|
|
106
|
+
(commandKey === "/reset" &&
|
|
107
|
+
(commandPolicy.allowlist.includes("/clear") || commandPolicy.allowlist.includes("/new")));
|
|
88
108
|
if (commandPolicy.enabled && !isAdminUser && !commandAllowed) {
|
|
89
109
|
api?.logger?.info?.(`wecom: command blocked by allowlist user=${fromUser} command=${commandKey}`);
|
|
90
110
|
await sendTextToUser(commandPolicy.rejectMessage);
|
|
@@ -17,6 +17,12 @@ export function createWecomAgentInboundProcessor(deps = {}) {
|
|
|
17
17
|
stripWecomGroupMentions,
|
|
18
18
|
resolveWecomCommandPolicy,
|
|
19
19
|
resolveWecomAllowFromPolicy,
|
|
20
|
+
resolveWecomDmPolicy,
|
|
21
|
+
resolveWecomEventPolicy = () => ({
|
|
22
|
+
enabled: true,
|
|
23
|
+
enterAgentWelcomeEnabled: false,
|
|
24
|
+
enterAgentWelcomeText: "",
|
|
25
|
+
}),
|
|
20
26
|
isWecomSenderAllowed,
|
|
21
27
|
sendWecomText,
|
|
22
28
|
extractLeadingSlashCommand,
|
|
@@ -65,6 +71,7 @@ export function createWecomAgentInboundProcessor(deps = {}) {
|
|
|
65
71
|
fromUser,
|
|
66
72
|
content,
|
|
67
73
|
msgType,
|
|
74
|
+
eventType,
|
|
68
75
|
mediaId,
|
|
69
76
|
picUrl,
|
|
70
77
|
recognition,
|
|
@@ -124,6 +131,7 @@ export function createWecomAgentInboundProcessor(deps = {}) {
|
|
|
124
131
|
stripWecomGroupMentions,
|
|
125
132
|
resolveWecomCommandPolicy,
|
|
126
133
|
resolveWecomAllowFromPolicy,
|
|
134
|
+
resolveWecomDmPolicy,
|
|
127
135
|
isWecomSenderAllowed,
|
|
128
136
|
extractLeadingSlashCommand,
|
|
129
137
|
COMMANDS,
|
|
@@ -140,6 +148,25 @@ export function createWecomAgentInboundProcessor(deps = {}) {
|
|
|
140
148
|
isGroupChat,
|
|
141
149
|
},
|
|
142
150
|
});
|
|
151
|
+
|
|
152
|
+
if (String(msgType ?? "").trim().toLowerCase() === "event") {
|
|
153
|
+
const normalizedEventType = String(eventType ?? "").trim().toLowerCase();
|
|
154
|
+
const eventPolicy = resolveWecomEventPolicy(api, config.accountId || accountId, config);
|
|
155
|
+
if (!eventPolicy?.enabled) {
|
|
156
|
+
api.logger.info?.(`wecom: event skipped (disabled) type=${normalizedEventType || "unknown"}`);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (normalizedEventType === "enter_agent" && eventPolicy.enterAgentWelcomeEnabled) {
|
|
160
|
+
const welcomeText = String(eventPolicy.enterAgentWelcomeText ?? "").trim();
|
|
161
|
+
if (welcomeText) {
|
|
162
|
+
await sendTextToUser(welcomeText);
|
|
163
|
+
api.logger.info?.(`wecom: enter_agent welcome sent account=${config.accountId || accountId}`);
|
|
164
|
+
}
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
api.logger.info?.(`wecom: event ignored type=${normalizedEventType || "unknown"}`);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
143
170
|
if (!guardResult.ok) return;
|
|
144
171
|
commandBody = guardResult.commandBody;
|
|
145
172
|
const isAdminUser = guardResult.isAdminUser === true;
|
|
@@ -14,6 +14,8 @@ export function createWecomAgentWebhookHandler({
|
|
|
14
14
|
messageProcessLimiter,
|
|
15
15
|
executeInboundTaskWithSessionQueue,
|
|
16
16
|
processInboundMessage,
|
|
17
|
+
recordInboundMetric = () => {},
|
|
18
|
+
recordRuntimeErrorMetric = () => {},
|
|
17
19
|
} = {}) {
|
|
18
20
|
const dispatchInbound = createWecomAgentInboundDispatcher({
|
|
19
21
|
api,
|
|
@@ -150,6 +152,11 @@ export function createWecomAgentWebhookHandler({
|
|
|
150
152
|
api.logger.info?.(
|
|
151
153
|
`wecom inbound: account=${matchedAccount.accountId} from=${fromUser} msgType=${msgType} chatId=${chatId || "N/A"} content=${(inbound?.content ?? "").slice?.(0, 80)}`,
|
|
152
154
|
);
|
|
155
|
+
recordInboundMetric({
|
|
156
|
+
mode: "agent",
|
|
157
|
+
msgType,
|
|
158
|
+
accountId: matchedAccount.accountId,
|
|
159
|
+
});
|
|
153
160
|
|
|
154
161
|
if (!fromUser) {
|
|
155
162
|
api.logger.warn?.("wecom: inbound message missing FromUserName, dropped");
|
|
@@ -173,6 +180,10 @@ export function createWecomAgentWebhookHandler({
|
|
|
173
180
|
}
|
|
174
181
|
} catch (err) {
|
|
175
182
|
api.logger.error?.(`wecom: webhook handler failed: ${String(err?.message || err)}`);
|
|
183
|
+
recordRuntimeErrorMetric({
|
|
184
|
+
scope: "agent-webhook",
|
|
185
|
+
reason: String(err?.message || err),
|
|
186
|
+
});
|
|
176
187
|
if (!res.writableEnded) {
|
|
177
188
|
res.statusCode = 500;
|
|
178
189
|
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
package/src/wecom/bot-context.js
CHANGED
|
@@ -25,6 +25,7 @@ export function buildWecomBotInboundContextPayload({
|
|
|
25
25
|
commandBody,
|
|
26
26
|
fromAddress,
|
|
27
27
|
sessionId,
|
|
28
|
+
accountId = "default",
|
|
28
29
|
isGroupChat,
|
|
29
30
|
chatId,
|
|
30
31
|
fromUser,
|
|
@@ -39,7 +40,7 @@ export function buildWecomBotInboundContextPayload({
|
|
|
39
40
|
From: fromAddress,
|
|
40
41
|
To: fromAddress,
|
|
41
42
|
SessionKey: sessionId,
|
|
42
|
-
AccountId:
|
|
43
|
+
AccountId: accountId,
|
|
43
44
|
ChatType: isGroupChat ? "group" : "direct",
|
|
44
45
|
ConversationLabel: isGroupChat && chatId ? `group:${chatId}` : fromUser,
|
|
45
46
|
SenderName: fromUser,
|
|
@@ -50,6 +50,7 @@ export async function handleWecomBotDispatchError({
|
|
|
50
50
|
startLateReplyWatcher,
|
|
51
51
|
sessionId,
|
|
52
52
|
fromUser,
|
|
53
|
+
accountId = "default",
|
|
53
54
|
buildWecomBotSessionId,
|
|
54
55
|
runtime,
|
|
55
56
|
cfg,
|
|
@@ -78,7 +79,7 @@ export async function handleWecomBotDispatchError({
|
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
try {
|
|
81
|
-
const runtimeSessionId = sessionId || buildWecomBotSessionId(fromUser);
|
|
82
|
+
const runtimeSessionId = sessionId || buildWecomBotSessionId(fromUser, accountId);
|
|
82
83
|
const runtimeStorePath = runtime.channel.session.resolveStorePath(cfg.session?.store, {
|
|
83
84
|
agentId: routedAgentId || "main",
|
|
84
85
|
});
|
|
@@ -9,6 +9,8 @@ export function createWecomBotInboundContentBuilder({
|
|
|
9
9
|
detectImageContentTypeFromBuffer,
|
|
10
10
|
decryptWecomMediaBuffer,
|
|
11
11
|
pickImageFileExtension,
|
|
12
|
+
resolveWecomVoiceTranscriptionConfig,
|
|
13
|
+
transcribeInboundVoice,
|
|
12
14
|
inferFilenameFromMediaDownload,
|
|
13
15
|
smartDecryptWecomFileBuffer,
|
|
14
16
|
basename,
|
|
@@ -22,6 +24,8 @@ export function createWecomBotInboundContentBuilder({
|
|
|
22
24
|
assertFunction("detectImageContentTypeFromBuffer", detectImageContentTypeFromBuffer);
|
|
23
25
|
assertFunction("decryptWecomMediaBuffer", decryptWecomMediaBuffer);
|
|
24
26
|
assertFunction("pickImageFileExtension", pickImageFileExtension);
|
|
27
|
+
assertFunction("resolveWecomVoiceTranscriptionConfig", resolveWecomVoiceTranscriptionConfig);
|
|
28
|
+
assertFunction("transcribeInboundVoice", transcribeInboundVoice);
|
|
25
29
|
assertFunction("inferFilenameFromMediaDownload", inferFilenameFromMediaDownload);
|
|
26
30
|
assertFunction("smartDecryptWecomFileBuffer", smartDecryptWecomFileBuffer);
|
|
27
31
|
assertFunction("basename", basename);
|
|
@@ -39,6 +43,10 @@ export function createWecomBotInboundContentBuilder({
|
|
|
39
43
|
normalizedImageUrls = [],
|
|
40
44
|
normalizedFileUrl = "",
|
|
41
45
|
normalizedFileName = "",
|
|
46
|
+
normalizedVoiceUrl = "",
|
|
47
|
+
normalizedVoiceMediaId = "",
|
|
48
|
+
normalizedVoiceContentType = "",
|
|
49
|
+
voiceInputMessageId = "",
|
|
42
50
|
normalizedQuote = null,
|
|
43
51
|
} = {}) {
|
|
44
52
|
const tempPathsToCleanup = [];
|
|
@@ -124,7 +132,8 @@ export function createWecomBotInboundContentBuilder({
|
|
|
124
132
|
}
|
|
125
133
|
}
|
|
126
134
|
|
|
127
|
-
|
|
135
|
+
const shouldHandleFile = msgType === "file" || (msgType === "mixed" && Boolean(normalizedFileUrl));
|
|
136
|
+
if (shouldHandleFile) {
|
|
128
137
|
const displayName =
|
|
129
138
|
inferFilenameFromMediaDownload({
|
|
130
139
|
explicitName: normalizedFileName,
|
|
@@ -162,22 +171,83 @@ export function createWecomBotInboundContentBuilder({
|
|
|
162
171
|
);
|
|
163
172
|
await writeFile(fileTempPath, decrypted.buffer);
|
|
164
173
|
tempPathsToCleanup.push(fileTempPath);
|
|
165
|
-
|
|
174
|
+
const fileInstruction =
|
|
166
175
|
`[用户发送了一个文件: ${safeName},已保存到: ${fileTempPath}]` +
|
|
167
176
|
"\n\n请根据文件内容回复用户;如需读取详情请使用 Read 工具。";
|
|
177
|
+
if (msgType === "mixed" && messageText) {
|
|
178
|
+
messageText = `${messageText}\n${fileInstruction}`.trim();
|
|
179
|
+
} else {
|
|
180
|
+
messageText = fileInstruction;
|
|
181
|
+
}
|
|
168
182
|
api?.logger?.info?.(
|
|
169
183
|
`wecom(bot): saved file to ${fileTempPath}, size=${decrypted.buffer.length} bytes` +
|
|
170
184
|
`, decrypted=${decrypted.decrypted ? "yes" : "no"} source=${downloaded.source || "unknown"}`,
|
|
171
185
|
);
|
|
172
186
|
} catch (fileErr) {
|
|
173
187
|
api?.logger?.warn?.(`wecom(bot): failed to fetch file url: ${String(fileErr?.message || fileErr)}`);
|
|
174
|
-
|
|
188
|
+
const failedFileHint = `[用户发送了一个文件: ${displayName},但下载失败]\n\n请提示用户重新发送文件。`;
|
|
189
|
+
if (msgType === "mixed" && messageText) {
|
|
190
|
+
messageText = `${messageText}\n${failedFileHint}`.trim();
|
|
191
|
+
} else {
|
|
192
|
+
messageText = failedFileHint;
|
|
193
|
+
}
|
|
175
194
|
}
|
|
176
195
|
} else if (!messageText) {
|
|
177
196
|
messageText = `[用户发送了一个文件: ${displayName}]`;
|
|
178
197
|
}
|
|
179
198
|
}
|
|
180
199
|
|
|
200
|
+
const shouldHandleVoice = msgType === "voice" || (msgType === "mixed" && Boolean(normalizedVoiceUrl));
|
|
201
|
+
if (shouldHandleVoice) {
|
|
202
|
+
const existingVoiceText = String(messageText ?? "").trim();
|
|
203
|
+
const voiceUrl = String(normalizedVoiceUrl ?? "").trim();
|
|
204
|
+
const voiceMediaId = String(normalizedVoiceMediaId ?? "").trim() || String(voiceInputMessageId ?? "").trim();
|
|
205
|
+
if (existingVoiceText && existingVoiceText !== "[语音]") {
|
|
206
|
+
if (msgType === "mixed") {
|
|
207
|
+
messageText = `${existingVoiceText}\n[用户发送了一条语音]`;
|
|
208
|
+
} else {
|
|
209
|
+
messageText = `[用户发送了一条语音]\n转写: ${existingVoiceText}`;
|
|
210
|
+
}
|
|
211
|
+
} else if (!voiceUrl) {
|
|
212
|
+
messageText = "语音接收成功,但未提供可下载的语音链接,请用户改发文字。";
|
|
213
|
+
} else {
|
|
214
|
+
const voiceConfig = resolveWecomVoiceTranscriptionConfig(api);
|
|
215
|
+
if (!voiceConfig.enabled) {
|
|
216
|
+
messageText = "已收到语音消息,但当前未启用语音转写,请改发文字。";
|
|
217
|
+
} else {
|
|
218
|
+
try {
|
|
219
|
+
const downloadedVoice = await fetchMediaFromUrl(voiceUrl, {
|
|
220
|
+
proxyUrl: botProxyUrl,
|
|
221
|
+
logger: api?.logger,
|
|
222
|
+
forceProxy: Boolean(botProxyUrl),
|
|
223
|
+
maxBytes: Math.max(voiceConfig.maxBytes || 0, 2 * 1024 * 1024),
|
|
224
|
+
});
|
|
225
|
+
const transcript = await transcribeInboundVoice({
|
|
226
|
+
api,
|
|
227
|
+
buffer: downloadedVoice.buffer,
|
|
228
|
+
contentType: normalizedVoiceContentType || downloadedVoice.contentType,
|
|
229
|
+
mediaId: voiceMediaId || `bot-voice-${Date.now()}`,
|
|
230
|
+
voiceConfig,
|
|
231
|
+
});
|
|
232
|
+
const voiceText = `[用户发送了一条语音]\n转写: ${String(transcript ?? "").trim()}`;
|
|
233
|
+
if (msgType === "mixed" && messageText) {
|
|
234
|
+
messageText = `${messageText}\n${voiceText}`.trim();
|
|
235
|
+
} else {
|
|
236
|
+
messageText = voiceText;
|
|
237
|
+
}
|
|
238
|
+
} catch (voiceErr) {
|
|
239
|
+
api?.logger?.warn?.(`wecom(bot): voice transcription failed: ${String(voiceErr?.message || voiceErr)}`);
|
|
240
|
+
return {
|
|
241
|
+
aborted: true,
|
|
242
|
+
abortText: "语音识别失败,请稍后重试。",
|
|
243
|
+
messageText: "",
|
|
244
|
+
tempPathsToCleanup,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
181
251
|
if (normalizedQuote?.content) {
|
|
182
252
|
const quoteLabel = normalizedQuote.msgType === "image" ? "[引用图片]" : `> ${normalizedQuote.content}`;
|
|
183
253
|
messageText = `${quoteLabel}\n\n${String(messageText ?? "").trim()}`.trim();
|
|
@@ -18,6 +18,7 @@ export async function executeWecomBotDispatchRuntime({
|
|
|
18
18
|
cfg,
|
|
19
19
|
ctxPayload,
|
|
20
20
|
streamId,
|
|
21
|
+
accountId = "default",
|
|
21
22
|
sessionId,
|
|
22
23
|
routedAgentId,
|
|
23
24
|
storePath,
|
|
@@ -96,7 +97,7 @@ export async function executeWecomBotDispatchRuntime({
|
|
|
96
97
|
? {
|
|
97
98
|
sessionKey: sessionId,
|
|
98
99
|
agentId: routedAgentId,
|
|
99
|
-
accountId
|
|
100
|
+
accountId,
|
|
100
101
|
}
|
|
101
102
|
: undefined,
|
|
102
103
|
},
|