@dingxiang-me/openclaw-wechat 1.7.1 → 2.0.0

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 (59) hide show
  1. package/CHANGELOG.md +162 -0
  2. package/README.en.md +379 -11
  3. package/README.md +626 -15
  4. package/docs/channels/wecom.md +186 -6
  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/bot-context.js +7 -1
  22. package/src/wecom/bot-dispatch-fallback.js +34 -3
  23. package/src/wecom/bot-dispatch-handlers.js +47 -4
  24. package/src/wecom/bot-inbound-dispatch-runtime.js +10 -0
  25. package/src/wecom/bot-inbound-executor-helpers.js +51 -5
  26. package/src/wecom/bot-inbound-executor.js +34 -0
  27. package/src/wecom/bot-long-connection-manager.js +971 -0
  28. package/src/wecom/bot-reply-runtime.js +36 -6
  29. package/src/wecom/bot-runtime-context.js +3 -0
  30. package/src/wecom/bot-state-store.js +4 -5
  31. package/src/wecom/bot-webhook-dispatch.js +5 -0
  32. package/src/wecom/bot-webhook-handler.js +5 -0
  33. package/src/wecom/callback-health-diagnostics.js +86 -0
  34. package/src/wecom/channel-config-schema.js +242 -0
  35. package/src/wecom/channel-plugin.js +162 -4
  36. package/src/wecom/channel-status-state.js +150 -0
  37. package/src/wecom/command-handlers.js +6 -0
  38. package/src/wecom/command-status-text.js +35 -8
  39. package/src/wecom/doc-client.js +537 -0
  40. package/src/wecom/doc-schema.js +380 -0
  41. package/src/wecom/doc-tool.js +833 -0
  42. package/src/wecom/outbound-active-stream.js +17 -10
  43. package/src/wecom/outbound-delivery.js +49 -0
  44. package/src/wecom/plugin-account-policy-services.js +4 -1
  45. package/src/wecom/plugin-composition.js +2 -0
  46. package/src/wecom/plugin-constants.js +1 -1
  47. package/src/wecom/plugin-delivery-inbound-services.js +4 -0
  48. package/src/wecom/plugin-processing-deps.js +5 -0
  49. package/src/wecom/plugin-route-runtime-deps.js +2 -0
  50. package/src/wecom/plugin-services.js +37 -0
  51. package/src/wecom/register-runtime.js +20 -1
  52. package/src/wecom/request-parsers.js +1 -0
  53. package/src/wecom/route-registration.js +4 -1
  54. package/src/wecom/session-reset.js +168 -0
  55. package/src/wecom/text-format.js +22 -5
  56. package/src/wecom/text-inbound-scheduler.js +1 -1
  57. package/src/wecom/thinking-parser.js +74 -0
  58. package/src/wecom/voice-transcription-process.js +80 -8
  59. package/src/wecom/voice-transcription.js +11 -0
@@ -0,0 +1,74 @@
1
+ const QUICK_TAG_RE = /<\s*\/?\s*(?:think(?:ing)?|thought)\b/i;
2
+ const THINK_TAG_RE = /<\s*(\/?)\s*(?:think(?:ing)?|thought)\b[^<>]*>/gi;
3
+
4
+ function isInsideRegion(pos, regions) {
5
+ for (const [start, end] of regions) {
6
+ if (pos >= start && pos < end) return true;
7
+ }
8
+ return false;
9
+ }
10
+
11
+ function findCodeRegions(text) {
12
+ const regions = [];
13
+ const blockRe = /```[\s\S]*?```/g;
14
+ for (const match of text.matchAll(blockRe)) {
15
+ regions.push([match.index, match.index + match[0].length]);
16
+ }
17
+ const inlineRe = /`[^`\n]+`/g;
18
+ for (const match of text.matchAll(inlineRe)) {
19
+ if (!isInsideRegion(match.index, regions)) {
20
+ regions.push([match.index, match.index + match[0].length]);
21
+ }
22
+ }
23
+ return regions;
24
+ }
25
+
26
+ export function parseThinkingContent(text) {
27
+ if (!text) {
28
+ return { visibleContent: "", thinkingContent: "", isThinking: false };
29
+ }
30
+
31
+ const input = String(text);
32
+ if (!QUICK_TAG_RE.test(input)) {
33
+ return { visibleContent: input, thinkingContent: "", isThinking: false };
34
+ }
35
+
36
+ const codeRegions = findCodeRegions(input);
37
+ const visibleParts = [];
38
+ const thinkingParts = [];
39
+ let lastIndex = 0;
40
+ let inThinking = false;
41
+
42
+ THINK_TAG_RE.lastIndex = 0;
43
+ for (const match of input.matchAll(THINK_TAG_RE)) {
44
+ const idx = match.index;
45
+ const isClose = match[1] === "/";
46
+ if (isInsideRegion(idx, codeRegions)) continue;
47
+
48
+ const segment = input.slice(lastIndex, idx);
49
+ if (!inThinking) {
50
+ visibleParts.push(segment);
51
+ if (!isClose) {
52
+ inThinking = true;
53
+ }
54
+ } else if (isClose) {
55
+ thinkingParts.push(segment);
56
+ inThinking = false;
57
+ }
58
+
59
+ lastIndex = idx + match[0].length;
60
+ }
61
+
62
+ const remaining = input.slice(lastIndex);
63
+ if (inThinking) {
64
+ thinkingParts.push(remaining);
65
+ } else {
66
+ visibleParts.push(remaining);
67
+ }
68
+
69
+ return {
70
+ visibleContent: visibleParts.join("").replace(/\n{3,}/g, "\n\n").trim(),
71
+ thinkingContent: thinkingParts.join("\n").replace(/\n{3,}/g, "\n\n").trim(),
72
+ isThinking: inThinking,
73
+ };
74
+ }
@@ -98,7 +98,7 @@ export function createVoiceTranscriptionProcessRuntime({
98
98
  return available;
99
99
  }
100
100
 
101
- async function resolveLocalWhisperCommand({ voiceConfig, logger }) {
101
+ function listLocalWhisperCommandCandidates({ voiceConfig } = {}) {
102
102
  const provider = String(voiceConfig?.provider ?? "").trim().toLowerCase();
103
103
  const explicitCommand = String(voiceConfig?.command ?? "").trim();
104
104
  const fallbackCandidates =
@@ -110,33 +110,105 @@ export function createVoiceTranscriptionProcessRuntime({
110
110
  const candidates = explicitCommand ? [explicitCommand, ...fallbackCandidates] : fallbackCandidates;
111
111
 
112
112
  if (candidates.length === 0) {
113
- throw new Error(
114
- `unsupported voice transcription provider: ${provider || "unknown"} (supported: local-whisper-cli/local-whisper)`,
115
- );
113
+ return {
114
+ provider,
115
+ explicitCommand,
116
+ candidates: [],
117
+ error: `unsupported voice transcription provider: ${provider || "unknown"} (supported: local-whisper-cli/local-whisper)`,
118
+ };
116
119
  }
117
120
 
118
- for (const cmd of candidates) {
121
+ return {
122
+ provider,
123
+ explicitCommand,
124
+ candidates,
125
+ error: "",
126
+ };
127
+ }
128
+
129
+ async function resolveLocalWhisperCommand({ voiceConfig, logger }) {
130
+ const resolution = listLocalWhisperCommandCandidates({ voiceConfig });
131
+ if (resolution.error) {
132
+ throw new Error(resolution.error);
133
+ }
134
+
135
+ for (const cmd of resolution.candidates) {
119
136
  // eslint-disable-next-line no-await-in-loop
120
137
  if (await checkCommandAvailable(cmd)) {
121
- if (explicitCommand && cmd !== explicitCommand) {
122
- logger?.warn?.(`wecom: voice command ${explicitCommand} unavailable, fallback to ${cmd}`);
138
+ if (resolution.explicitCommand && cmd !== resolution.explicitCommand) {
139
+ logger?.warn?.(`wecom: voice command ${resolution.explicitCommand} unavailable, fallback to ${cmd}`);
123
140
  }
124
141
  return cmd;
125
142
  }
126
143
  }
127
144
 
128
- throw new Error(`local transcription command not found: ${candidates.join(" / ")}`);
145
+ const checkedList = resolution.candidates.join(" / ");
146
+ throw new Error(
147
+ `local transcription command not found: checked ${checkedList}. ` +
148
+ "Confirm the command is installed and available in PATH for the OpenClaw runtime.",
149
+ );
150
+ }
151
+
152
+ async function inspectVoiceTranscriptionRuntime({ voiceConfig, logger } = {}) {
153
+ const resolution = listLocalWhisperCommandCandidates({ voiceConfig });
154
+ const commandChecks = [];
155
+ for (const cmd of resolution.candidates) {
156
+ // eslint-disable-next-line no-await-in-loop
157
+ const available = await checkCommandAvailable(cmd);
158
+ commandChecks.push({ command: cmd, available });
159
+ }
160
+ const resolvedCommand = commandChecks.find((item) => item.available)?.command || "";
161
+ const ffmpegEnabled = voiceConfig?.ffmpegEnabled !== false;
162
+ const ffmpegAvailable = ffmpegEnabled ? await ensureFfmpegAvailable(logger) : false;
163
+ const provider = String(voiceConfig?.provider ?? "").trim().toLowerCase();
164
+ const requireModelPath = provider === "local-whisper-cli" && voiceConfig?.requireModelPath !== false;
165
+ const modelPath = String(voiceConfig?.modelPath ?? "").trim();
166
+ const issues = [];
167
+ if (!voiceConfig?.enabled) {
168
+ issues.push("voice transcription disabled");
169
+ }
170
+ if (resolution.error) {
171
+ issues.push(resolution.error);
172
+ }
173
+ if (resolution.candidates.length > 0 && !resolvedCommand) {
174
+ issues.push(`no available command in PATH: ${resolution.candidates.join(" / ")}`);
175
+ }
176
+ if (requireModelPath && !modelPath) {
177
+ issues.push("voiceTranscription.modelPath is required for local-whisper-cli");
178
+ }
179
+ if (ffmpegEnabled && !ffmpegAvailable) {
180
+ issues.push("ffmpeg not available");
181
+ }
182
+
183
+ return {
184
+ enabled: voiceConfig?.enabled === true,
185
+ provider,
186
+ explicitCommand: resolution.explicitCommand || "",
187
+ commandCandidates: resolution.candidates,
188
+ commandChecks,
189
+ resolvedCommand,
190
+ ffmpegEnabled,
191
+ ffmpegAvailable,
192
+ requireModelPath,
193
+ modelPathConfigured: Boolean(modelPath),
194
+ modelPath,
195
+ issues,
196
+ };
129
197
  }
130
198
 
131
199
  assertFunction("runProcessWithTimeout", runProcessWithTimeout);
132
200
  assertFunction("checkCommandAvailable", checkCommandAvailable);
133
201
  assertFunction("ensureFfmpegAvailable", ensureFfmpegAvailable);
202
+ assertFunction("listLocalWhisperCommandCandidates", listLocalWhisperCommandCandidates);
134
203
  assertFunction("resolveLocalWhisperCommand", resolveLocalWhisperCommand);
204
+ assertFunction("inspectVoiceTranscriptionRuntime", inspectVoiceTranscriptionRuntime);
135
205
 
136
206
  return {
137
207
  runProcessWithTimeout,
138
208
  checkCommandAvailable,
139
209
  ensureFfmpegAvailable,
210
+ listLocalWhisperCommandCandidates,
140
211
  resolveLocalWhisperCommand,
212
+ inspectVoiceTranscriptionRuntime,
141
213
  };
142
214
  }
@@ -35,6 +35,7 @@ export function createWecomVoiceTranscriber({
35
35
  checkCommandAvailable,
36
36
  ensureFfmpegAvailable,
37
37
  resolveLocalWhisperCommand,
38
+ inspectVoiceTranscriptionRuntime,
38
39
  } = processRuntime;
39
40
 
40
41
  function resolveWecomVoiceTranscriptionConfig(api) {
@@ -232,10 +233,20 @@ export function createWecomVoiceTranscriber({
232
233
  }
233
234
  }
234
235
 
236
+ async function inspectWecomVoiceTranscriptionRuntime({ api, voiceConfig } = {}) {
237
+ const resolvedConfig = voiceConfig ?? resolveWecomVoiceTranscriptionConfig(api);
238
+ return inspectVoiceTranscriptionRuntime({
239
+ voiceConfig: resolvedConfig,
240
+ logger: api?.logger,
241
+ });
242
+ }
243
+
235
244
  return {
236
245
  resolveWecomVoiceTranscriptionConfig,
237
246
  transcribeInboundVoice,
247
+ inspectWecomVoiceTranscriptionRuntime,
238
248
  __internal: {
249
+ inspectVoiceTranscriptionRuntime,
239
250
  resolveLocalWhisperCommand,
240
251
  checkCommandAvailable,
241
252
  ensureFfmpegAvailable,