@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.
Files changed (65) hide show
  1. package/CHANGELOG.md +160 -0
  2. package/README.en.md +379 -11
  3. package/README.md +620 -12
  4. package/docs/channels/wecom.md +181 -3
  5. package/openclaw.plugin.json +148 -5
  6. package/package.json +9 -5
  7. package/src/core/delivery-router.js +2 -0
  8. package/src/core/stream-manager.js +13 -2
  9. package/src/core.js +96 -6
  10. package/src/wecom/account-config-core.js +2 -0
  11. package/src/wecom/account-config.js +12 -3
  12. package/src/wecom/agent-context.js +7 -1
  13. package/src/wecom/agent-dispatch-executor.js +13 -1
  14. package/src/wecom/agent-dispatch-fallback.js +23 -0
  15. package/src/wecom/agent-inbound-dispatch.js +1 -1
  16. package/src/wecom/agent-inbound-processor.js +33 -2
  17. package/src/wecom/agent-late-reply-runtime.js +31 -1
  18. package/src/wecom/agent-runtime-context.js +3 -0
  19. package/src/wecom/agent-webhook-handler.js +5 -0
  20. package/src/wecom/api-client-core.js +1 -1
  21. package/src/wecom/api-client-send-text.js +43 -20
  22. package/src/wecom/bot-context.js +7 -1
  23. package/src/wecom/bot-dispatch-fallback.js +34 -3
  24. package/src/wecom/bot-dispatch-handlers.js +47 -4
  25. package/src/wecom/bot-inbound-content.js +14 -6
  26. package/src/wecom/bot-inbound-dispatch-runtime.js +10 -0
  27. package/src/wecom/bot-inbound-executor-helpers.js +44 -11
  28. package/src/wecom/bot-inbound-executor.js +40 -0
  29. package/src/wecom/bot-long-connection-manager.js +983 -0
  30. package/src/wecom/bot-reply-runtime.js +36 -6
  31. package/src/wecom/bot-runtime-context.js +3 -0
  32. package/src/wecom/bot-state-store.js +4 -5
  33. package/src/wecom/bot-webhook-dispatch.js +7 -0
  34. package/src/wecom/bot-webhook-handler.js +5 -0
  35. package/src/wecom/callback-health-diagnostics.js +86 -0
  36. package/src/wecom/channel-config-schema.js +242 -0
  37. package/src/wecom/channel-plugin.js +162 -4
  38. package/src/wecom/channel-status-state.js +150 -0
  39. package/src/wecom/command-handlers.js +6 -0
  40. package/src/wecom/command-status-text.js +32 -3
  41. package/src/wecom/doc-client.js +537 -0
  42. package/src/wecom/doc-schema.js +380 -0
  43. package/src/wecom/doc-tool.js +833 -0
  44. package/src/wecom/outbound-active-stream.js +17 -10
  45. package/src/wecom/outbound-delivery.js +46 -0
  46. package/src/wecom/outbound-webhook-sender.js +39 -16
  47. package/src/wecom/plugin-account-policy-services.js +4 -1
  48. package/src/wecom/plugin-composition.js +2 -0
  49. package/src/wecom/plugin-constants.js +1 -1
  50. package/src/wecom/plugin-delivery-inbound-services.js +4 -0
  51. package/src/wecom/plugin-processing-deps.js +5 -0
  52. package/src/wecom/plugin-route-runtime-deps.js +2 -0
  53. package/src/wecom/plugin-services.js +37 -0
  54. package/src/wecom/register-runtime.js +20 -1
  55. package/src/wecom/request-parsers.js +1 -0
  56. package/src/wecom/route-registration.js +4 -1
  57. package/src/wecom/session-reset.js +168 -0
  58. package/src/wecom/target-utils.js +41 -5
  59. package/src/wecom/text-format.js +22 -5
  60. package/src/wecom/text-inbound-scheduler.js +1 -1
  61. package/src/wecom/thinking-parser.js +74 -0
  62. package/src/wecom/voice-transcription-process.js +145 -11
  63. package/src/wecom/voice-transcription.js +14 -2
  64. package/src/wecom/webhook-adapter-normalize.js +29 -0
  65. 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.enabled
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: "全部账户",