@tencent-weixin/openclaw-weixin 2.4.4 → 2.4.5

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 (58) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/CHANGELOG.zh_CN.md +34 -0
  3. package/dist/src/api/api.js +35 -1
  4. package/dist/src/api/api.js.map +1 -1
  5. package/dist/src/api/session-guard.js +3 -3
  6. package/dist/src/api/session-guard.js.map +1 -1
  7. package/dist/src/api/types.js.map +1 -1
  8. package/dist/src/cdn/cdn-upload.js +3 -2
  9. package/dist/src/cdn/cdn-upload.js.map +1 -1
  10. package/dist/src/cdn/upload.js +12 -3
  11. package/dist/src/cdn/upload.js.map +1 -1
  12. package/dist/src/monitor/monitor.js +8 -8
  13. package/dist/src/monitor/monitor.js.map +1 -1
  14. package/npm-shrinkwrap.json +21848 -0
  15. package/openclaw.plugin.json +1 -1
  16. package/package.json +7 -5
  17. package/src/api/api.ts +50 -1
  18. package/src/api/session-guard.ts +3 -3
  19. package/src/api/types.ts +2 -1
  20. package/src/cdn/cdn-upload.ts +8 -2
  21. package/src/cdn/upload.ts +14 -3
  22. package/src/monitor/monitor.ts +9 -11
  23. package/dist/src/media/voice-outbound.js +0 -274
  24. package/dist/src/media/voice-outbound.js.map +0 -1
  25. package/dist/src/messaging/abort-fence.js +0 -70
  26. package/dist/src/messaging/abort-fence.js.map +0 -1
  27. package/dist/src/messaging/batch-session.js +0 -271
  28. package/dist/src/messaging/batch-session.js.map +0 -1
  29. package/dist/src/messaging/buttons.js +0 -117
  30. package/dist/src/messaging/buttons.js.map +0 -1
  31. package/dist/src/messaging/lane-key.js +0 -66
  32. package/dist/src/messaging/lane-key.js.map +0 -1
  33. package/dist/src/messaging/merged-record.js +0 -149
  34. package/dist/src/messaging/merged-record.js.map +0 -1
  35. package/dist/src/messaging/model-buttons.js +0 -182
  36. package/dist/src/messaging/model-buttons.js.map +0 -1
  37. package/dist/src/messaging/model-callback-handler.js +0 -133
  38. package/dist/src/messaging/model-callback-handler.js.map +0 -1
  39. package/dist/src/messaging/run-context.js +0 -9
  40. package/dist/src/messaging/run-context.js.map +0 -1
  41. package/dist/src/messaging/run-report-session.js +0 -123
  42. package/dist/src/messaging/run-report-session.js.map +0 -1
  43. package/dist/src/monitor/lane-scheduler.js +0 -46
  44. package/dist/src/monitor/lane-scheduler.js.map +0 -1
  45. package/dist/src/runtime.js +0 -54
  46. package/dist/src/runtime.js.map +0 -1
  47. package/dist/src/streaming/batch-session.js +0 -271
  48. package/dist/src/streaming/batch-session.js.map +0 -1
  49. package/dist/src/streaming/stream-piece.js +0 -54
  50. package/dist/src/streaming/stream-piece.js.map +0 -1
  51. package/dist/src/streaming/stream-pipeline.js +0 -431
  52. package/dist/src/streaming/stream-pipeline.js.map +0 -1
  53. package/dist/src/streaming/stream-session.js +0 -260
  54. package/dist/src/streaming/stream-session.js.map +0 -1
  55. package/dist/src/streaming/stream.js +0 -239
  56. package/dist/src/streaming/stream.js.map +0 -1
  57. package/dist/src/util/markdown-fences.js +0 -54
  58. package/dist/src/util/markdown-fences.js.map +0 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "openclaw-weixin",
3
- "version": "2.4.4",
3
+ "version": "2.4.5",
4
4
  "channels": [
5
5
  "openclaw-weixin"
6
6
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tencent-weixin/openclaw-weixin",
3
- "version": "2.4.4",
3
+ "version": "2.4.5",
4
4
  "description": "OpenClaw Weixin channel",
5
5
  "license": "MIT",
6
6
  "author": "Tencent",
@@ -10,6 +10,7 @@
10
10
  "!src/**/*.test.ts",
11
11
  "!src/**/node_modules/",
12
12
  "index.ts",
13
+ "npm-shrinkwrap.json",
13
14
  "openclaw.plugin.json",
14
15
  "dist/",
15
16
  "README.md",
@@ -20,7 +21,8 @@
20
21
  "scripts": {
21
22
  "test": "vitest run --coverage",
22
23
  "typecheck": "tsc --noEmit",
23
- "build": "tsc",
24
+ "clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
25
+ "build": "npm run clean && tsc",
24
26
  "prepublishOnly": "npm run typecheck && npm run build"
25
27
  },
26
28
  "engines": {
@@ -31,11 +33,11 @@
31
33
  "zod": "^4.3.6"
32
34
  },
33
35
  "peerDependencies": {
34
- "openclaw": ">=2026.3.22"
36
+ "openclaw": ">=2026.5.12"
35
37
  },
36
38
  "devDependencies": {
37
39
  "@vitest/coverage-v8": "^3.1.0",
38
- "openclaw": "2026.3.23",
40
+ "openclaw": "2026.5.12",
39
41
  "silk-wasm": "^3.7.1",
40
42
  "typescript": "^5.8.0",
41
43
  "vitest": "^3.1.0"
@@ -59,7 +61,7 @@
59
61
  "install": {
60
62
  "npmSpec": "@tencent-weixin/openclaw-weixin",
61
63
  "defaultChoice": "npm",
62
- "minHostVersion": ">=2026.3.22"
64
+ "minHostVersion": ">=2026.5.12"
63
65
  }
64
66
  },
65
67
  "ilink_appid": "bot"
package/src/api/api.ts CHANGED
@@ -16,6 +16,7 @@ import type {
16
16
  NotifyStopResp,
17
17
  NotifyStartResp,
18
18
  SendMessageReq,
19
+ SendMessageResp,
19
20
  SendTypingReq,
20
21
  GetConfigResp,
21
22
  } from "./types.js";
@@ -252,6 +253,40 @@ function buildHeaders(opts: { token?: string }): Record<string, string> {
252
253
  return headers;
253
254
  }
254
255
 
256
+ /**
257
+ * Classify a fetch-level error into a category for logging / diagnostics.
258
+ * This does NOT cover HTTP-level errors (4xx/5xx) — those are thrown separately.
259
+ */
260
+ export function classifyFetchError(err: unknown): {
261
+ type: "dns" | "tcp" | "tls" | "timeout" | "unknown";
262
+ description: string;
263
+ code?: string;
264
+ } {
265
+ if (err instanceof Error && err.name === "AbortError") {
266
+ return { type: "timeout", description: "request timeout" };
267
+ }
268
+
269
+ const cause = (err as NodeJS.ErrnoException)?.cause;
270
+ const causeCode = (cause as any)?.code ?? "";
271
+ const causeStr = String(cause ?? err ?? "") + " " + String(causeCode);
272
+ const matchedCode = causeCode || (typeof cause === "string" ? cause : "");
273
+
274
+ if (/ENOTFOUND|EAI_AGAIN|getaddrinfo/i.test(causeStr)) {
275
+ return { type: "dns", description: "DNS resolution failed, check DNS configuration", ...(matchedCode ? { code: matchedCode } : {}) };
276
+ }
277
+ if (/ECONNREFUSED/i.test(causeStr)) {
278
+ return { type: "tcp", description: "TCP connection refused", ...(matchedCode ? { code: matchedCode } : {}) };
279
+ }
280
+ if (/UND_ERR_CONNECT_TIMEOUT|ETIMEDOUT|ENETUNREACH|EHOSTUNREACH/i.test(causeStr)) {
281
+ return { type: "tcp", description: "TCP connection timeout or unreachable", ...(matchedCode ? { code: matchedCode } : {}) };
282
+ }
283
+ if (/UND_ERR_SOCKET|SSL|TLS|CERT|UNABLE_TO_VERIFY|DEPTH_ZERO/i.test(causeStr)) {
284
+ return { type: "tls", description: "TLS handshake error", ...(matchedCode ? { code: matchedCode } : {}) };
285
+ }
286
+
287
+ return { type: "unknown", description: "network request failed" };
288
+ }
289
+
255
290
  /**
256
291
  * GET fetch wrapper: send a GET request to a Weixin API endpoint.
257
292
  * When `timeoutMs` is set, the request is aborted after that many milliseconds.
@@ -291,6 +326,10 @@ export async function apiGetFetch(params: {
291
326
  return rawText;
292
327
  } catch (err) {
293
328
  if (t !== undefined) clearTimeout(t);
329
+ const classified = classifyFetchError(err);
330
+ logger.error(
331
+ `${params.label}: GET fetch failed url=${redactUrl(url.toString())} timeoutMs=${timeoutMs ?? "none"} type=${classified.type} description=${classified.description}${classified.code ? ` code=${classified.code}` : ""} error=${String(err)}`,
332
+ );
294
333
  throw err;
295
334
  }
296
335
  }
@@ -372,6 +411,10 @@ export async function apiPostFetch(params: {
372
411
  return rawText;
373
412
  } catch (err) {
374
413
  if (t !== undefined) clearTimeout(t);
414
+ const classified = classifyFetchError(err);
415
+ logger.error(
416
+ `${params.label}: POST fetch failed url=${redactUrl(url.toString())} timeoutMs=${params.timeoutMs ?? "none"} type=${classified.type} description=${classified.description}${classified.code ? ` code=${classified.code}` : ""} error=${String(err)}`,
417
+ );
375
418
  throw err;
376
419
  } finally {
377
420
  cleanup();
@@ -460,7 +503,7 @@ export async function getUploadUrl(
460
503
  export async function sendMessage(
461
504
  params: WeixinApiOptions & { body: SendMessageReq },
462
505
  ): Promise<void> {
463
- await apiPostFetch({
506
+ const rawText = await apiPostFetch({
464
507
  baseUrl: params.baseUrl,
465
508
  endpoint: "ilink/bot/sendmessage",
466
509
  body: JSON.stringify({ ...params.body, base_info: buildBaseInfo() }),
@@ -468,6 +511,12 @@ export async function sendMessage(
468
511
  timeoutMs: params.timeoutMs ?? DEFAULT_API_TIMEOUT_MS,
469
512
  label: "sendMessage",
470
513
  });
514
+ const resp: SendMessageResp = JSON.parse(rawText);
515
+ if (resp.ret && resp.ret !== 0) {
516
+ throw new Error(
517
+ `sendMessage ret=${resp.ret} errmsg=${resp.errmsg ?? "(none)"}`,
518
+ );
519
+ }
471
520
  }
472
521
 
473
522
  /** Fetch bot config (includes typing_ticket) for a given user. */
@@ -2,8 +2,8 @@ import { logger } from "../util/logger.js";
2
2
 
3
3
  const SESSION_PAUSE_DURATION_MS = 60 * 60 * 1000;
4
4
 
5
- /** Error code returned by the server when the bot session has expired. */
6
- export const SESSION_EXPIRED_ERRCODE = -14;
5
+ /** Error code returned by the server when the bot token is stale / expired. */
6
+ export const STALE_TOKEN_ERRCODE = -14;
7
7
 
8
8
  const pauseUntilMap = new Map<string, number>();
9
9
 
@@ -44,7 +44,7 @@ export function assertSessionActive(accountId: string): void {
44
44
  if (isSessionPaused(accountId)) {
45
45
  const remainingMin = Math.ceil(getRemainingPauseMs(accountId) / 60_000);
46
46
  throw new Error(
47
- `session paused for accountId=${accountId}, ${remainingMin} min remaining (errcode ${SESSION_EXPIRED_ERRCODE})`,
47
+ `session paused for accountId=${accountId}, ${remainingMin} min remaining (errcode ${STALE_TOKEN_ERRCODE})`,
48
48
  );
49
49
  }
50
50
  }
package/src/api/types.ts CHANGED
@@ -224,7 +224,8 @@ export interface SendMessageReq {
224
224
  }
225
225
 
226
226
  export interface SendMessageResp {
227
- // empty
227
+ ret?: number;
228
+ errmsg?: string;
228
229
  }
229
230
 
230
231
  /** Typing status: 1 = typing (default), 2 = cancel typing. */
@@ -70,10 +70,16 @@ export async function uploadBufferToCdn(params: {
70
70
  } catch (err) {
71
71
  lastError = err;
72
72
  if (err instanceof Error && err.message.includes("client error")) throw err;
73
+ const cause =
74
+ (err as NodeJS.ErrnoException).cause ?? (err as NodeJS.ErrnoException).code ?? "";
73
75
  if (attempt < UPLOAD_MAX_RETRIES) {
74
- logger.error(`${label}: attempt ${attempt} failed, retrying... err=${String(err)}`);
76
+ logger.error(
77
+ `${label}: attempt ${attempt} failed, retrying... url=${redactUrl(cdnUrl)} error=${String(err)}${cause ? ` cause=${cause}` : ""}`,
78
+ );
75
79
  } else {
76
- logger.error(`${label}: all ${UPLOAD_MAX_RETRIES} attempts failed err=${String(err)}`);
80
+ logger.error(
81
+ `${label}: all ${UPLOAD_MAX_RETRIES} attempts failed url=${redactUrl(cdnUrl)} error=${String(err)}${cause ? ` cause=${cause}` : ""}`,
82
+ );
77
83
  }
78
84
  }
79
85
  }
package/src/cdn/upload.ts CHANGED
@@ -7,6 +7,7 @@ import type { WeixinApiOptions } from "../api/api.js";
7
7
  import { aesEcbPaddedSize } from "./aes-ecb.js";
8
8
  import { uploadBufferToCdn } from "./cdn-upload.js";
9
9
  import { logger } from "../util/logger.js";
10
+ import { redactUrl } from "../util/redact.js";
10
11
  import { getExtensionFromContentTypeOrUrl } from "../media/mime.js";
11
12
  import { tempFileName } from "../util/random.js";
12
13
  import { UploadMediaType } from "../api/types.js";
@@ -28,10 +29,20 @@ export type UploadedFileInfo = {
28
29
  * Returns the local file path; extension is inferred from Content-Type / URL.
29
30
  */
30
31
  export async function downloadRemoteImageToTemp(url: string, destDir: string): Promise<string> {
31
- logger.debug(`downloadRemoteImageToTemp: fetching url=${url}`);
32
- const res = await fetch(url);
32
+ logger.debug(`downloadRemoteImageToTemp: fetching url=${redactUrl(url)}`);
33
+ let res: Response;
34
+ try {
35
+ res = await fetch(url);
36
+ } catch (err) {
37
+ const cause =
38
+ (err as NodeJS.ErrnoException).cause ?? (err as NodeJS.ErrnoException).code ?? "";
39
+ logger.error(
40
+ `downloadRemoteImageToTemp: fetch network error url=${redactUrl(url)} error=${String(err)}${cause ? ` cause=${cause}` : ""}`,
41
+ );
42
+ throw err;
43
+ }
33
44
  if (!res.ok) {
34
- const msg = `remote media download failed: ${res.status} ${res.statusText} url=${url}`;
45
+ const msg = `remote media download failed: ${res.status} ${res.statusText} url=${redactUrl(url)}`;
35
46
  logger.error(`downloadRemoteImageToTemp: ${msg}`);
36
47
  throw new Error(msg);
37
48
  }
@@ -1,9 +1,9 @@
1
1
  import type { ChannelAccountSnapshot } from "openclaw/plugin-sdk/channel-contract";
2
2
  import type { PluginRuntime } from "openclaw/plugin-sdk/core";
3
3
 
4
- import { getUpdates } from "../api/api.js";
4
+ import { getUpdates, classifyFetchError } from "../api/api.js";
5
5
  import { WeixinConfigManager } from "../api/config-cache.js";
6
- import { SESSION_EXPIRED_ERRCODE, pauseSession, getRemainingPauseMs } from "../api/session-guard.js";
6
+ import { STALE_TOKEN_ERRCODE, pauseSession, getRemainingPauseMs } from "../api/session-guard.js";
7
7
  import { processOneMessage } from "../messaging/process-message.js";
8
8
  import { getSyncBufFilePath, loadGetUpdatesBuf, saveGetUpdatesBuf } from "../storage/sync-buf.js";
9
9
  import { logger } from "../util/logger.js";
@@ -112,17 +112,14 @@ export async function monitorWeixinProvider(opts: MonitorWeixinOpts): Promise<vo
112
112
  (resp.ret !== undefined && resp.ret !== 0) ||
113
113
  (resp.errcode !== undefined && resp.errcode !== 0);
114
114
  if (isApiError) {
115
- const isSessionExpired =
116
- resp.errcode === SESSION_EXPIRED_ERRCODE || resp.ret === SESSION_EXPIRED_ERRCODE;
115
+ const isStaleToken =
116
+ resp.errcode === STALE_TOKEN_ERRCODE || resp.ret === STALE_TOKEN_ERRCODE;
117
117
 
118
- if (isSessionExpired) {
118
+ if (isStaleToken) {
119
119
  pauseSession(accountId);
120
120
  const pauseMs = getRemainingPauseMs(accountId);
121
- errLog(
122
- `weixin getUpdates: session expired (errcode ${SESSION_EXPIRED_ERRCODE}), pausing bot for ${Math.ceil(pauseMs / 60_000)} min`,
123
- );
124
121
  aLog.error(
125
- `getUpdates: session expired (errcode=${resp.errcode} ret=${resp.ret}), pausing all requests for ${Math.ceil(pauseMs / 60_000)} min`,
122
+ `getUpdates: token for ${accountId} is stale, pausing all requests for ${Math.ceil(pauseMs / 60_000)} min`,
126
123
  );
127
124
  consecutiveFailures = 0;
128
125
  await sleep(pauseMs, abortSignal);
@@ -190,10 +187,11 @@ export async function monitorWeixinProvider(opts: MonitorWeixinOpts): Promise<vo
190
187
  return;
191
188
  }
192
189
  consecutiveFailures += 1;
190
+ const classified = classifyFetchError(err);
193
191
  errLog(
194
- `weixin getUpdates error (${consecutiveFailures}/${MAX_CONSECUTIVE_FAILURES}): ${String(err)}`,
192
+ `weixin getUpdates error (${consecutiveFailures}/${MAX_CONSECUTIVE_FAILURES}): ${String(err)} type=${classified.type} description=${classified.description}${classified.code ? ` code=${classified.code}` : ""}`,
195
193
  );
196
- aLog.error(`getUpdates error: ${String(err)}, stack=${(err as Error).stack}`);
194
+ aLog.error(`getUpdates error: ${String(err)}, type=${classified.type} code=${classified.code ?? "none"}, stack=${(err as Error).stack}`);
197
195
  if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
198
196
  errLog(
199
197
  `weixin getUpdates: ${MAX_CONSECUTIVE_FAILURES} consecutive failures, backing off 30s`,
@@ -1,274 +0,0 @@
1
- import { execFile } from "node:child_process";
2
- import fs from "node:fs/promises";
3
- import os from "node:os";
4
- import path from "node:path";
5
- import { promisify } from "node:util";
6
- import { logger } from "../util/logger.js";
7
- const execFileAsync = promisify(execFile);
8
- const OGG_CAPTURE = Buffer.from("OggS");
9
- const OPUS_HEAD_MAGIC = Buffer.from("OpusHead");
10
- const GP_UNKNOWN = 0xffffffffffffffffn;
11
- /** Weixin `VoiceItem.encode_type`: 6=silk (README: inbound SILK). */
12
- export const VOICE_ENCODE_SILK = 6;
13
- /** Ogg container (used for Opus-in-Ogg outbound). */
14
- export const VOICE_ENCODE_OGG_SPEEX = 8;
15
- /**
16
- * Extensions always treated as outbound voice (upload + VOICE item).
17
- * `.ogg` is handled separately: only when the file is Ogg Opus (see {@link isWeixinVoiceOutboundPath}).
18
- */
19
- const VOICE_EXTENSIONS = new Set([".opus", ".silk", ".slk"]);
20
- const AUDIO_AS_VOICE_EXTENSIONS = new Set([
21
- ".aac",
22
- ".amr",
23
- ".m4a",
24
- ".mp3",
25
- ".oga",
26
- ".ogg",
27
- ".opus",
28
- ".silk",
29
- ".slk",
30
- ".wav",
31
- ]);
32
- function readUint32LE(buf, o) {
33
- return buf.readUInt32LE(o);
34
- }
35
- /**
36
- * Parse one Ogg page starting at `start`. Returns next byte offset after this page, or null if invalid.
37
- */
38
- function skipOggPage(buf, start) {
39
- if (start + 27 > buf.length)
40
- return null;
41
- if (!buf.subarray(start, start + 4).equals(OGG_CAPTURE))
42
- return null;
43
- const nsegs = buf[start + 26];
44
- if (start + 27 + nsegs > buf.length)
45
- return null;
46
- let bodySize = 0;
47
- for (let i = 0; i < nsegs; i++) {
48
- bodySize += buf[start + 27 + i];
49
- }
50
- const end = start + 27 + nsegs + bodySize;
51
- if (end > buf.length)
52
- return null;
53
- return end;
54
- }
55
- /**
56
- * First Ogg page whose first packet payload starts with OpusHead defines the Opus logical stream serial.
57
- */
58
- function findOpusStreamSerial(buf) {
59
- let off = 0;
60
- while (off < buf.length) {
61
- const idx = buf.indexOf(OGG_CAPTURE, off);
62
- if (idx < 0)
63
- return null;
64
- const end = skipOggPage(buf, idx);
65
- if (end === null) {
66
- off = idx + 1;
67
- continue;
68
- }
69
- const nsegs = buf[idx + 26];
70
- const bodyStart = idx + 27 + nsegs;
71
- const firstSegLen = nsegs > 0 ? buf[idx + 27] : 0;
72
- if (firstSegLen >= OPUS_HEAD_MAGIC.length &&
73
- bodyStart + firstSegLen <= buf.length) {
74
- const payload = buf.subarray(bodyStart, bodyStart + firstSegLen);
75
- if (payload.subarray(0, OPUS_HEAD_MAGIC.length).equals(OPUS_HEAD_MAGIC)) {
76
- return readUint32LE(buf, idx + 14);
77
- }
78
- }
79
- off = end;
80
- }
81
- return null;
82
- }
83
- function maxGranuleForSerial(buf, targetSerial) {
84
- let off = 0;
85
- let maxGp = 0n;
86
- while (off < buf.length) {
87
- const idx = buf.indexOf(OGG_CAPTURE, off);
88
- if (idx < 0)
89
- break;
90
- const end = skipOggPage(buf, idx);
91
- if (end === null) {
92
- off = idx + 1;
93
- continue;
94
- }
95
- const serial = readUint32LE(buf, idx + 14);
96
- if (serial === targetSerial) {
97
- const gp = buf.readBigUInt64LE(idx + 6);
98
- if (gp !== GP_UNKNOWN && gp > maxGp)
99
- maxGp = gp;
100
- }
101
- off = end;
102
- }
103
- return maxGp;
104
- }
105
- /**
106
- * Opus granule position counts 48 kHz PCM samples (RFC 7845). Duration in ms = gp * 1000 / 48000.
107
- */
108
- export function playtimeMsFromOpusGranule(maxGp) {
109
- if (maxGp <= 0n)
110
- return 0;
111
- const ms = (maxGp * 1000n) / 48000n;
112
- return Number(ms);
113
- }
114
- export function parseOggOpusPlaytimeMs(buf) {
115
- const serial = findOpusStreamSerial(buf);
116
- if (serial === null)
117
- return null;
118
- const maxGp = maxGranuleForSerial(buf, serial);
119
- if (maxGp <= 0n)
120
- return null;
121
- const ms = playtimeMsFromOpusGranule(maxGp);
122
- return ms > 0 ? ms : null;
123
- }
124
- export function bufferLooksLikeOggOpus(buf) {
125
- return findOpusStreamSerial(buf) !== null;
126
- }
127
- async function silkPlaytimeMsWithOptionalWasm(buf) {
128
- try {
129
- const { decode } = await import("silk-wasm");
130
- const result = await decode(buf, 24_000);
131
- const d = result.duration;
132
- if (typeof d !== "number" || !Number.isFinite(d) || d <= 0)
133
- return null;
134
- return Math.round(d);
135
- }
136
- catch (err) {
137
- logger.debug(`voice-outbound: silk-wasm decode unavailable or failed err=${String(err)}`);
138
- return null;
139
- }
140
- }
141
- async function ffprobePlaytimeMs(filePath) {
142
- try {
143
- const { stdout } = await execFileAsync("ffprobe", [
144
- "-v",
145
- "error",
146
- "-show_entries",
147
- "format=duration",
148
- "-of",
149
- "default=noprint_wrappers=1:nokey=1",
150
- filePath,
151
- ]);
152
- const seconds = Number.parseFloat(stdout.trim());
153
- if (!Number.isFinite(seconds) || seconds <= 0)
154
- return null;
155
- return Math.round(seconds * 1000);
156
- }
157
- catch (err) {
158
- logger.debug(`voice-outbound: ffprobe duration unavailable err=${String(err)}`);
159
- return null;
160
- }
161
- }
162
- const WEIXIN_VOICE_TRANSCODE_SAMPLE_RATE = 48_000;
163
- const WEIXIN_VOICE_TRANSCODE_BITRATE = "64k";
164
- const WEIXIN_VOICE_TRANSCODE_MAX_SECS = 60;
165
- /**
166
- * Transcode a generic audio file to Ogg/Opus for Weixin voice messages using ffmpeg.
167
- * Returns the transcoded buffer on success, or null if ffmpeg is unavailable or fails.
168
- * The caller should check the returned buffer with resolveWeixinOutboundVoiceMeta
169
- * before uploading; if that also fails, degrade to a file attachment.
170
- */
171
- export async function transcodeToWeixinVoiceOpus(filePath) {
172
- const tmpPath = path.join(os.tmpdir(), `weixin-voice-${Date.now()}-${Math.random().toString(36).slice(2)}.ogg`);
173
- try {
174
- await execFileAsync("ffmpeg", [
175
- "-hide_banner",
176
- "-loglevel",
177
- "error",
178
- "-y",
179
- "-i",
180
- filePath,
181
- "-vn",
182
- "-sn",
183
- "-dn",
184
- "-t",
185
- String(WEIXIN_VOICE_TRANSCODE_MAX_SECS),
186
- "-ar",
187
- String(WEIXIN_VOICE_TRANSCODE_SAMPLE_RATE),
188
- "-ac",
189
- "1",
190
- "-c:a",
191
- "libopus",
192
- "-b:a",
193
- WEIXIN_VOICE_TRANSCODE_BITRATE,
194
- "-f",
195
- "ogg",
196
- tmpPath,
197
- ]);
198
- return await fs.readFile(tmpPath);
199
- }
200
- catch (err) {
201
- logger.warn(`voice-outbound: transcodeToWeixinVoiceOpus failed filePath=${filePath} err=${String(err)}`);
202
- return null;
203
- }
204
- finally {
205
- await fs.unlink(tmpPath).catch(() => { });
206
- }
207
- }
208
- export function isWeixinAudioAsVoiceOutboundPath(filePath) {
209
- const ext = path.extname(filePath).toLowerCase();
210
- return AUDIO_AS_VOICE_EXTENSIONS.has(ext);
211
- }
212
- export async function isWeixinVoiceOutboundPath(filePath) {
213
- const ext = path.extname(filePath).toLowerCase();
214
- if (VOICE_EXTENSIONS.has(ext))
215
- return true;
216
- if (ext !== ".ogg")
217
- return false;
218
- const head = Buffer.allocUnsafe(65536);
219
- const fh = await fs.open(filePath, "r");
220
- try {
221
- const { bytesRead } = await fh.read(head, 0, head.length, 0);
222
- if (bytesRead <= 0)
223
- return false;
224
- return bufferLooksLikeOggOpus(head.subarray(0, bytesRead));
225
- }
226
- finally {
227
- await fh.close();
228
- }
229
- }
230
- /**
231
- * Compute voice playtime (ms) and encoding hints for Weixin VOICE item.
232
- * Returns null if duration cannot be determined (caller should send as file).
233
- */
234
- export async function resolveWeixinOutboundVoiceMeta(filePath, opts = {}) {
235
- const ext = path.extname(filePath).toLowerCase();
236
- const buf = await fs.readFile(filePath);
237
- if (ext === ".opus" || ext === ".ogg") {
238
- const ms = parseOggOpusPlaytimeMs(buf);
239
- if (ms == null || ms <= 0) {
240
- logger.warn(`voice-outbound: cannot resolve playtime for ${ext} file — parseOggOpusPlaytimeMs returned ${ms} (not a valid Ogg Opus stream?) filePath=${filePath} size=${buf.length}`);
241
- return null;
242
- }
243
- return {
244
- playtimeMs: ms,
245
- encode_type: VOICE_ENCODE_OGG_SPEEX,
246
- sample_rate: 48_000,
247
- };
248
- }
249
- if (ext === ".silk" || ext === ".slk") {
250
- const ms = await silkPlaytimeMsWithOptionalWasm(buf);
251
- if (ms == null || ms <= 0) {
252
- logger.warn(`voice-outbound: cannot resolve playtime for ${ext} file — silk-wasm decode returned ${ms} (wasm missing, or invalid SILK bitstream?) filePath=${filePath} size=${buf.length}`);
253
- return null;
254
- }
255
- return {
256
- playtimeMs: ms,
257
- encode_type: VOICE_ENCODE_SILK,
258
- sample_rate: 24_000,
259
- };
260
- }
261
- if (opts.allowGenericAudio && AUDIO_AS_VOICE_EXTENSIONS.has(ext)) {
262
- const ms = await ffprobePlaytimeMs(filePath);
263
- if (ms == null || ms <= 0) {
264
- logger.warn(`voice-outbound: cannot resolve playtime for audioAsVoice ${ext} file via ffprobe filePath=${filePath} size=${buf.length}`);
265
- return null;
266
- }
267
- return {
268
- playtimeMs: ms,
269
- };
270
- }
271
- logger.warn(`voice-outbound: unsupported voice extension '${ext}' for filePath=${filePath}`);
272
- return null;
273
- }
274
- //# sourceMappingURL=voice-outbound.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"voice-outbound.js","sourceRoot":"","sources":["../../../src/media/voice-outbound.ts"],"names":[],"mappings":"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"}
@@ -1,70 +0,0 @@
1
- /**
2
- * Abort fence (generation barrier) for Weixin message dispatch.
3
- *
4
- * When the user sends an "abort" intent (e.g. "stop", "中止", "/abort") we
5
- * cannot truly cancel the in-flight LLM/tool pipeline because the underlying
6
- * SDK does not currently propagate an AbortSignal end-to-end. Instead, we
7
- * adopt the same trick used by the Telegram channel
8
- * (extensions/telegram/src/bot-message-dispatch.ts in the openclaw main repo):
9
- *
10
- * - Each dispatch picks up a fence `generation` at entry.
11
- * - An abort message bumps the generation (`supersede=true`) and starts its
12
- * own dispatch with the new generation.
13
- * - All outbound side-effects (sendMessage, typing start/stop, debug timing,
14
- * etc.) check `isDispatchSuperseded(...)` first; if the generation no longer
15
- * matches, they become no-ops.
16
- *
17
- * Effect: the old long-running task keeps running in the background but its
18
- * output is silently dropped, so the user perceives it as cancelled and the
19
- * new turn begins immediately.
20
- *
21
- * The fence is keyed per chat (typically `accountId:userId` for Weixin DMs),
22
- * so different users do not share state.
23
- */
24
- const fenceByKey = new Map();
25
- /**
26
- * Begin a new dispatch and return its generation. When `supersede` is true
27
- * (i.e. this dispatch is itself an abort request), bump the generation first
28
- * so all already-running dispatches on this key become superseded.
29
- */
30
- export function beginAbortFence(params) {
31
- const existing = fenceByKey.get(params.key);
32
- const state = existing ?? { generation: 0, activeDispatches: 0 };
33
- if (params.supersede) {
34
- state.generation += 1;
35
- }
36
- state.activeDispatches += 1;
37
- fenceByKey.set(params.key, state);
38
- return state.generation;
39
- }
40
- /**
41
- * Returns true when the dispatch with `generation` has been superseded by a
42
- * later abort on `key`. Cheap to call; check before any user-visible side
43
- * effect.
44
- */
45
- export function isDispatchSuperseded(params) {
46
- return (fenceByKey.get(params.key)?.generation ?? 0) !== params.generation;
47
- }
48
- /**
49
- * Mark a dispatch finished. When the lane goes idle we drop the entry to
50
- * avoid unbounded growth across many distinct users.
51
- */
52
- export function endAbortFence(key) {
53
- const state = fenceByKey.get(key);
54
- if (!state) {
55
- return;
56
- }
57
- state.activeDispatches -= 1;
58
- if (state.activeDispatches <= 0) {
59
- fenceByKey.delete(key);
60
- }
61
- }
62
- /** Number of tracked fence keys. Exposed for diagnostics and tests. */
63
- export function getAbortFenceSizeForTests() {
64
- return fenceByKey.size;
65
- }
66
- /** Clear all fence state. Tests only. */
67
- export function resetAbortFenceForTests() {
68
- fenceByKey.clear();
69
- }
70
- //# sourceMappingURL=abort-fence.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"abort-fence.js","sourceRoot":"","sources":["../../../src/messaging/abort-fence.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAOH,MAAM,UAAU,GAAG,IAAI,GAAG,EAA2B,CAAC;AAEtD;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,MAA2C;IACzE,MAAM,QAAQ,GAAG,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC5C,MAAM,KAAK,GAAoB,QAAQ,IAAI,EAAE,UAAU,EAAE,CAAC,EAAE,gBAAgB,EAAE,CAAC,EAAE,CAAC;IAClF,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;QACrB,KAAK,CAAC,UAAU,IAAI,CAAC,CAAC;IACxB,CAAC;IACD,KAAK,CAAC,gBAAgB,IAAI,CAAC,CAAC;IAC5B,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IAClC,OAAO,KAAK,CAAC,UAAU,CAAC;AAC1B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,MAA2C;IAC9E,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,UAAU,IAAI,CAAC,CAAC,KAAK,MAAM,CAAC,UAAU,CAAC;AAC7E,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,GAAW;IACvC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAClC,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO;IACT,CAAC;IACD,KAAK,CAAC,gBAAgB,IAAI,CAAC,CAAC;IAC5B,IAAI,KAAK,CAAC,gBAAgB,IAAI,CAAC,EAAE,CAAC;QAChC,UAAU,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACzB,CAAC;AACH,CAAC;AAED,uEAAuE;AACvE,MAAM,UAAU,yBAAyB;IACvC,OAAO,UAAU,CAAC,IAAI,CAAC;AACzB,CAAC;AAED,yCAAyC;AACzC,MAAM,UAAU,uBAAuB;IACrC,UAAU,CAAC,KAAK,EAAE,CAAC;AACrB,CAAC"}