@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.
- package/CHANGELOG.md +34 -0
- package/CHANGELOG.zh_CN.md +34 -0
- package/dist/src/api/api.js +35 -1
- package/dist/src/api/api.js.map +1 -1
- package/dist/src/api/session-guard.js +3 -3
- package/dist/src/api/session-guard.js.map +1 -1
- package/dist/src/api/types.js.map +1 -1
- package/dist/src/cdn/cdn-upload.js +3 -2
- package/dist/src/cdn/cdn-upload.js.map +1 -1
- package/dist/src/cdn/upload.js +12 -3
- package/dist/src/cdn/upload.js.map +1 -1
- package/dist/src/monitor/monitor.js +8 -8
- package/dist/src/monitor/monitor.js.map +1 -1
- package/npm-shrinkwrap.json +21848 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +7 -5
- package/src/api/api.ts +50 -1
- package/src/api/session-guard.ts +3 -3
- package/src/api/types.ts +2 -1
- package/src/cdn/cdn-upload.ts +8 -2
- package/src/cdn/upload.ts +14 -3
- package/src/monitor/monitor.ts +9 -11
- package/dist/src/media/voice-outbound.js +0 -274
- package/dist/src/media/voice-outbound.js.map +0 -1
- package/dist/src/messaging/abort-fence.js +0 -70
- package/dist/src/messaging/abort-fence.js.map +0 -1
- package/dist/src/messaging/batch-session.js +0 -271
- package/dist/src/messaging/batch-session.js.map +0 -1
- package/dist/src/messaging/buttons.js +0 -117
- package/dist/src/messaging/buttons.js.map +0 -1
- package/dist/src/messaging/lane-key.js +0 -66
- package/dist/src/messaging/lane-key.js.map +0 -1
- package/dist/src/messaging/merged-record.js +0 -149
- package/dist/src/messaging/merged-record.js.map +0 -1
- package/dist/src/messaging/model-buttons.js +0 -182
- package/dist/src/messaging/model-buttons.js.map +0 -1
- package/dist/src/messaging/model-callback-handler.js +0 -133
- package/dist/src/messaging/model-callback-handler.js.map +0 -1
- package/dist/src/messaging/run-context.js +0 -9
- package/dist/src/messaging/run-context.js.map +0 -1
- package/dist/src/messaging/run-report-session.js +0 -123
- package/dist/src/messaging/run-report-session.js.map +0 -1
- package/dist/src/monitor/lane-scheduler.js +0 -46
- package/dist/src/monitor/lane-scheduler.js.map +0 -1
- package/dist/src/runtime.js +0 -54
- package/dist/src/runtime.js.map +0 -1
- package/dist/src/streaming/batch-session.js +0 -271
- package/dist/src/streaming/batch-session.js.map +0 -1
- package/dist/src/streaming/stream-piece.js +0 -54
- package/dist/src/streaming/stream-piece.js.map +0 -1
- package/dist/src/streaming/stream-pipeline.js +0 -431
- package/dist/src/streaming/stream-pipeline.js.map +0 -1
- package/dist/src/streaming/stream-session.js +0 -260
- package/dist/src/streaming/stream-session.js.map +0 -1
- package/dist/src/streaming/stream.js +0 -239
- package/dist/src/streaming/stream.js.map +0 -1
- package/dist/src/util/markdown-fences.js +0 -54
- package/dist/src/util/markdown-fences.js.map +0 -1
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tencent-weixin/openclaw-weixin",
|
|
3
|
-
"version": "2.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
|
-
"
|
|
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.
|
|
36
|
+
"openclaw": ">=2026.5.12"
|
|
35
37
|
},
|
|
36
38
|
"devDependencies": {
|
|
37
39
|
"@vitest/coverage-v8": "^3.1.0",
|
|
38
|
-
"openclaw": "2026.
|
|
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.
|
|
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. */
|
package/src/api/session-guard.ts
CHANGED
|
@@ -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
|
|
6
|
-
export const
|
|
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 ${
|
|
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
package/src/cdn/cdn-upload.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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
|
-
|
|
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
|
}
|
package/src/monitor/monitor.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
116
|
-
resp.errcode ===
|
|
115
|
+
const isStaleToken =
|
|
116
|
+
resp.errcode === STALE_TOKEN_ERRCODE || resp.ret === STALE_TOKEN_ERRCODE;
|
|
117
117
|
|
|
118
|
-
if (
|
|
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:
|
|
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"}
|