@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
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
const DEFAULT_ACCOUNT_ID = "default";
|
|
2
|
+
const CHANNEL_CONNECTED_TTL_MS = 10 * 60 * 1000;
|
|
3
|
+
|
|
4
|
+
const accountInboundState = new Map();
|
|
5
|
+
const accountConnectionState = new Map();
|
|
6
|
+
|
|
7
|
+
function readString(value) {
|
|
8
|
+
const trimmed = String(value ?? "").trim();
|
|
9
|
+
return trimmed || "";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function normalizeAccountId(accountId) {
|
|
13
|
+
return readString(accountId).toLowerCase() || DEFAULT_ACCOUNT_ID;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function normalizeInboundTimestamp(value) {
|
|
17
|
+
if (value == null || value === "") return Date.now();
|
|
18
|
+
const raw = Number(value);
|
|
19
|
+
if (!Number.isFinite(raw) || raw <= 0) return Date.now();
|
|
20
|
+
if (raw < 1e12) return Math.floor(raw * 1000);
|
|
21
|
+
return Math.floor(raw);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function formatIso(ms) {
|
|
25
|
+
try {
|
|
26
|
+
return new Date(ms).toISOString();
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function toConnectedFlag(ms) {
|
|
33
|
+
if (!Number.isFinite(ms) || ms <= 0) return false;
|
|
34
|
+
return Date.now() - ms <= CHANNEL_CONNECTED_TTL_MS;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function buildMergedConnectionState(accountId, latestInboundMs = 0) {
|
|
38
|
+
const connection = accountConnectionState.get(normalizeAccountId(accountId));
|
|
39
|
+
const inboundConnected = toConnectedFlag(latestInboundMs);
|
|
40
|
+
if (!connection) {
|
|
41
|
+
return {
|
|
42
|
+
connected: inboundConnected,
|
|
43
|
+
transport: null,
|
|
44
|
+
connectedAt: null,
|
|
45
|
+
connectedAtMs: null,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
connected: connection.connected === true || inboundConnected,
|
|
50
|
+
transport: connection.transport || null,
|
|
51
|
+
connectedAt: connection.connectedAt ?? null,
|
|
52
|
+
connectedAtMs: Number(connection.connectedAtMs) || null,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function markWecomInboundActivity({ accountId, timestamp } = {}) {
|
|
57
|
+
const normalizedAccountId = normalizeAccountId(accountId);
|
|
58
|
+
const inboundAtMs = normalizeInboundTimestamp(timestamp);
|
|
59
|
+
const existing = accountInboundState.get(normalizedAccountId);
|
|
60
|
+
if (existing && Number(existing.lastInboundAtMs) > inboundAtMs) {
|
|
61
|
+
return existing;
|
|
62
|
+
}
|
|
63
|
+
const next = {
|
|
64
|
+
accountId: normalizedAccountId,
|
|
65
|
+
lastInboundAtMs: inboundAtMs,
|
|
66
|
+
lastInboundAt: formatIso(inboundAtMs),
|
|
67
|
+
};
|
|
68
|
+
accountInboundState.set(normalizedAccountId, next);
|
|
69
|
+
return next;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function getWecomInboundActivity(accountId) {
|
|
73
|
+
const entry = accountInboundState.get(normalizeAccountId(accountId));
|
|
74
|
+
if (!entry) {
|
|
75
|
+
const connectionState = buildMergedConnectionState(accountId, 0);
|
|
76
|
+
if (!connectionState.connected && !connectionState.transport) return null;
|
|
77
|
+
return {
|
|
78
|
+
accountId: normalizeAccountId(accountId),
|
|
79
|
+
lastInboundAtMs: null,
|
|
80
|
+
lastInboundAt: null,
|
|
81
|
+
...connectionState,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const latestMs = Number(entry.lastInboundAtMs ?? 0);
|
|
85
|
+
return {
|
|
86
|
+
...entry,
|
|
87
|
+
...buildMergedConnectionState(accountId, latestMs),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function getWecomChannelInboundActivity(accountIds = []) {
|
|
92
|
+
const normalizedIds = Array.isArray(accountIds)
|
|
93
|
+
? accountIds.map((item) => normalizeAccountId(item))
|
|
94
|
+
: [];
|
|
95
|
+
const targetEntries =
|
|
96
|
+
normalizedIds.length > 0
|
|
97
|
+
? normalizedIds.map((id) => accountInboundState.get(id)).filter(Boolean)
|
|
98
|
+
: Array.from(accountInboundState.values());
|
|
99
|
+
if (targetEntries.length === 0) {
|
|
100
|
+
return {
|
|
101
|
+
connected: false,
|
|
102
|
+
lastInboundAt: null,
|
|
103
|
+
lastInboundAtMs: null,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let latest = targetEntries[0];
|
|
108
|
+
for (const entry of targetEntries) {
|
|
109
|
+
if ((entry?.lastInboundAtMs ?? 0) > (latest?.lastInboundAtMs ?? 0)) {
|
|
110
|
+
latest = entry;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const latestMs = Number(latest?.lastInboundAtMs ?? 0);
|
|
115
|
+
const connectionEntries =
|
|
116
|
+
normalizedIds.length > 0
|
|
117
|
+
? normalizedIds.map((id) => accountConnectionState.get(id)).filter(Boolean)
|
|
118
|
+
: Array.from(accountConnectionState.values());
|
|
119
|
+
const anyConnected = connectionEntries.some((entry) => entry?.connected === true);
|
|
120
|
+
return {
|
|
121
|
+
connected: anyConnected || toConnectedFlag(latestMs),
|
|
122
|
+
lastInboundAt: latest?.lastInboundAt ?? null,
|
|
123
|
+
lastInboundAtMs: Number.isFinite(latestMs) ? latestMs : null,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function setWecomConnectionState({ accountId, connected, transport = "" } = {}) {
|
|
128
|
+
const normalizedAccountId = normalizeAccountId(accountId);
|
|
129
|
+
const nextConnected = connected === true;
|
|
130
|
+
const existing = accountConnectionState.get(normalizedAccountId);
|
|
131
|
+
const connectedAtMs = nextConnected
|
|
132
|
+
? Number(existing?.connectedAtMs) > 0
|
|
133
|
+
? Number(existing.connectedAtMs)
|
|
134
|
+
: Date.now()
|
|
135
|
+
: null;
|
|
136
|
+
const next = {
|
|
137
|
+
accountId: normalizedAccountId,
|
|
138
|
+
connected: nextConnected,
|
|
139
|
+
transport: String(transport ?? "").trim() || existing?.transport || null,
|
|
140
|
+
connectedAtMs,
|
|
141
|
+
connectedAt: connectedAtMs ? formatIso(connectedAtMs) : null,
|
|
142
|
+
};
|
|
143
|
+
accountConnectionState.set(normalizedAccountId, next);
|
|
144
|
+
return next;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function __resetWecomInboundActivityForTests() {
|
|
148
|
+
accountInboundState.clear();
|
|
149
|
+
accountConnectionState.clear();
|
|
150
|
+
}
|
|
@@ -7,6 +7,7 @@ export function createWecomCommandHandlers({
|
|
|
7
7
|
listWebhookTargetAliases,
|
|
8
8
|
listAllWebhookTargetAliases,
|
|
9
9
|
resolveWecomVoiceTranscriptionConfig,
|
|
10
|
+
inspectWecomVoiceTranscriptionRuntime = async () => null,
|
|
10
11
|
resolveWecomCommandPolicy,
|
|
11
12
|
resolveWecomAllowFromPolicy,
|
|
12
13
|
resolveWecomDmPolicy,
|
|
@@ -38,6 +39,9 @@ export function createWecomCommandHandlers({
|
|
|
38
39
|
if (typeof resolveWecomVoiceTranscriptionConfig !== "function") {
|
|
39
40
|
throw new Error("createWecomCommandHandlers: resolveWecomVoiceTranscriptionConfig is required");
|
|
40
41
|
}
|
|
42
|
+
if (typeof inspectWecomVoiceTranscriptionRuntime !== "function") {
|
|
43
|
+
throw new Error("createWecomCommandHandlers: inspectWecomVoiceTranscriptionRuntime is required");
|
|
44
|
+
}
|
|
41
45
|
if (typeof resolveWecomCommandPolicy !== "function") {
|
|
42
46
|
throw new Error("createWecomCommandHandlers: resolveWecomCommandPolicy is required");
|
|
43
47
|
}
|
|
@@ -96,6 +100,7 @@ export function createWecomCommandHandlers({
|
|
|
96
100
|
const accountIds = listWecomAccountIds(api);
|
|
97
101
|
const webhookTargetAliases = listWebhookTargetAliases(config);
|
|
98
102
|
const voiceConfig = resolveWecomVoiceTranscriptionConfig(api);
|
|
103
|
+
const voiceRuntimeInfo = await inspectWecomVoiceTranscriptionRuntime({ api, voiceConfig });
|
|
99
104
|
const commandPolicy = resolveWecomCommandPolicy(api);
|
|
100
105
|
const allowFromPolicy = resolveWecomAllowFromPolicy(api, config?.accountId, config);
|
|
101
106
|
const dmPolicy = resolveWecomDmPolicy(api, config?.accountId, config);
|
|
@@ -116,6 +121,7 @@ export function createWecomCommandHandlers({
|
|
|
116
121
|
webhookTargetAliases,
|
|
117
122
|
pluginVersion,
|
|
118
123
|
voiceConfig,
|
|
124
|
+
voiceRuntimeInfo,
|
|
119
125
|
commandPolicy,
|
|
120
126
|
allowFromPolicy,
|
|
121
127
|
dmPolicy,
|
|
@@ -54,6 +54,32 @@ function buildObservabilityStatusLines(observabilityMetrics = {}) {
|
|
|
54
54
|
};
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
function buildVoiceStatusLine(voiceConfig = {}, voiceRuntimeInfo = null) {
|
|
58
|
+
if (!voiceConfig?.enabled) {
|
|
59
|
+
return "⚠️ 语音消息转写回退未启用(仅使用企业微信 Recognition)";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const modelLabel = voiceConfig.modelPath || voiceConfig.model || "未配置";
|
|
63
|
+
const baseLine = `✅ 语音消息转写(本地 ${voiceConfig.provider},模型: ${modelLabel})`;
|
|
64
|
+
if (!voiceRuntimeInfo || typeof voiceRuntimeInfo !== "object") {
|
|
65
|
+
return baseLine;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const commandState = voiceRuntimeInfo.resolvedCommand
|
|
69
|
+
? `命令 ${voiceRuntimeInfo.resolvedCommand}`
|
|
70
|
+
: `命令缺失(检查 ${voiceRuntimeInfo.commandCandidates?.join(" / ") || "未配置"})`;
|
|
71
|
+
const ffmpegState = voiceRuntimeInfo.ffmpegEnabled
|
|
72
|
+
? voiceRuntimeInfo.ffmpegAvailable
|
|
73
|
+
? "ffmpeg 已安装"
|
|
74
|
+
: "ffmpeg 缺失"
|
|
75
|
+
: "ffmpeg 未启用";
|
|
76
|
+
const issueSuffix =
|
|
77
|
+
Array.isArray(voiceRuntimeInfo.issues) && voiceRuntimeInfo.issues.length > 0
|
|
78
|
+
? `;问题:${voiceRuntimeInfo.issues.join(";")}`
|
|
79
|
+
: "";
|
|
80
|
+
return `${baseLine}(${commandState},${ffmpegState})${issueSuffix}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
57
83
|
export function buildAgentStatusText({
|
|
58
84
|
fromUser,
|
|
59
85
|
config,
|
|
@@ -61,6 +87,7 @@ export function buildAgentStatusText({
|
|
|
61
87
|
webhookTargetAliases,
|
|
62
88
|
pluginVersion,
|
|
63
89
|
voiceConfig,
|
|
90
|
+
voiceRuntimeInfo,
|
|
64
91
|
commandPolicy,
|
|
65
92
|
allowFromPolicy,
|
|
66
93
|
dmPolicy,
|
|
@@ -75,9 +102,7 @@ export function buildAgentStatusText({
|
|
|
75
102
|
observabilityMetrics,
|
|
76
103
|
} = {}) {
|
|
77
104
|
const proxyEnabled = Boolean(config?.outboundProxy);
|
|
78
|
-
const voiceStatusLine = voiceConfig
|
|
79
|
-
? `✅ 语音消息转写(本地 ${voiceConfig.provider},模型: ${voiceConfig.modelPath || voiceConfig.model})`
|
|
80
|
-
: "⚠️ 语音消息转写回退未启用(仅使用企业微信 Recognition)";
|
|
105
|
+
const voiceStatusLine = buildVoiceStatusLine(voiceConfig, voiceRuntimeInfo);
|
|
81
106
|
const commandPolicyLine = commandPolicy.enabled
|
|
82
107
|
? `✅ 指令白名单已启用(${commandPolicy.allowlist.length} 条,管理员 ${commandPolicy.adminUsers.length} 人)`
|
|
83
108
|
: "ℹ️ 指令白名单未启用";
|
|
@@ -191,6 +216,10 @@ export function buildBotStatusText({
|
|
|
191
216
|
const webhookBotPolicyLine = webhookBotPolicy.enabled
|
|
192
217
|
? "✅ Webhook Bot 回包已启用"
|
|
193
218
|
: "ℹ️ Webhook Bot 回包未启用";
|
|
219
|
+
const longConnectionLine =
|
|
220
|
+
botConfig?.longConnection?.enabled === true
|
|
221
|
+
? `✅ Bot 长连接已启用(BotID=${String(botConfig?.longConnection?.botId ?? "").slice(0, 8) || "n/a"}...)`
|
|
222
|
+
: "ℹ️ Bot 长连接未启用";
|
|
194
223
|
const webhookTargetsLine = buildWebhookTargetStatusLine({
|
|
195
224
|
aliases: allWebhookTargetAliases,
|
|
196
225
|
scope: "全部账户",
|