@tencent-weixin/openclaw-weixin 2.4.3 → 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 (74) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/CHANGELOG.zh_CN.md +34 -0
  3. package/dist/src/api/api.js +79 -4
  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 +2 -0
  8. package/dist/src/api/types.js.map +1 -1
  9. package/dist/src/cdn/cdn-upload.js +3 -2
  10. package/dist/src/cdn/cdn-upload.js.map +1 -1
  11. package/dist/src/cdn/upload.js +12 -3
  12. package/dist/src/cdn/upload.js.map +1 -1
  13. package/dist/src/channel.js +7 -1
  14. package/dist/src/channel.js.map +1 -1
  15. package/dist/src/config/config-schema.js +1 -0
  16. package/dist/src/config/config-schema.js.map +1 -1
  17. package/dist/src/config/reply-progress.js +5 -0
  18. package/dist/src/config/reply-progress.js.map +1 -0
  19. package/dist/src/messaging/error-notice.js +1 -0
  20. package/dist/src/messaging/error-notice.js.map +1 -1
  21. package/dist/src/messaging/outbound-hooks.js +2 -0
  22. package/dist/src/messaging/outbound-hooks.js.map +1 -1
  23. package/dist/src/messaging/process-message.js +32 -7
  24. package/dist/src/messaging/process-message.js.map +1 -1
  25. package/dist/src/messaging/reply-progress-sender.js +93 -0
  26. package/dist/src/messaging/reply-progress-sender.js.map +1 -0
  27. package/dist/src/messaging/send.js +40 -2
  28. package/dist/src/messaging/send.js.map +1 -1
  29. package/dist/src/monitor/monitor.js +11 -8
  30. package/dist/src/monitor/monitor.js.map +1 -1
  31. package/npm-shrinkwrap.json +21848 -0
  32. package/openclaw.plugin.json +1 -1
  33. package/package.json +7 -5
  34. package/src/api/api.ts +102 -4
  35. package/src/api/session-guard.ts +3 -3
  36. package/src/api/types.ts +18 -1
  37. package/src/cdn/cdn-upload.ts +8 -2
  38. package/src/cdn/upload.ts +14 -3
  39. package/src/channel.ts +7 -1
  40. package/src/config/config-schema.ts +1 -0
  41. package/src/config/reply-progress.ts +10 -0
  42. package/src/messaging/error-notice.ts +2 -0
  43. package/src/messaging/outbound-hooks.ts +4 -0
  44. package/src/messaging/process-message.ts +32 -7
  45. package/src/messaging/reply-progress-sender.ts +122 -0
  46. package/src/messaging/send-media.ts +1 -1
  47. package/src/messaging/send.ts +60 -7
  48. package/src/monitor/monitor.ts +12 -11
  49. package/dist/src/media/voice-outbound.js +0 -177
  50. package/dist/src/media/voice-outbound.js.map +0 -1
  51. package/dist/src/messaging/abort-fence.js +0 -70
  52. package/dist/src/messaging/abort-fence.js.map +0 -1
  53. package/dist/src/messaging/buttons.js +0 -117
  54. package/dist/src/messaging/buttons.js.map +0 -1
  55. package/dist/src/messaging/lane-key.js +0 -66
  56. package/dist/src/messaging/lane-key.js.map +0 -1
  57. package/dist/src/messaging/merged-record.js +0 -149
  58. package/dist/src/messaging/merged-record.js.map +0 -1
  59. package/dist/src/messaging/model-buttons.js +0 -182
  60. package/dist/src/messaging/model-buttons.js.map +0 -1
  61. package/dist/src/messaging/model-callback-handler.js +0 -133
  62. package/dist/src/messaging/model-callback-handler.js.map +0 -1
  63. package/dist/src/monitor/lane-scheduler.js +0 -46
  64. package/dist/src/monitor/lane-scheduler.js.map +0 -1
  65. package/dist/src/runtime.js +0 -54
  66. package/dist/src/runtime.js.map +0 -1
  67. package/dist/src/streaming/stream-pipeline.js +0 -431
  68. package/dist/src/streaming/stream-pipeline.js.map +0 -1
  69. package/dist/src/streaming/stream-session.js +0 -260
  70. package/dist/src/streaming/stream-session.js.map +0 -1
  71. package/dist/src/streaming/stream.js +0 -239
  72. package/dist/src/streaming/stream.js.map +0 -1
  73. package/dist/src/util/markdown-fences.js +0 -54
  74. package/dist/src/util/markdown-fences.js.map +0 -1
@@ -0,0 +1,122 @@
1
+ import type { WeixinApiOptions } from "../api/api.js";
2
+ import type { MessageItem } from "../api/types.js";
3
+ import { MessageItemType } from "../api/types.js";
4
+ import { logger } from "../util/logger.js";
5
+
6
+ import { sendMessageItemWeixin } from "./send.js";
7
+
8
+ export type WeixinReplyProgressSenderDeps = {
9
+ runId: string;
10
+ to: string;
11
+ accountId: string;
12
+ opts: WeixinApiOptions & {
13
+ contextToken?: string;
14
+ runId?: string;
15
+ };
16
+ };
17
+
18
+ type ToolItemEventPayload = {
19
+ itemId?: string;
20
+ kind?: string;
21
+ title?: string;
22
+ name?: string;
23
+ phase?: string;
24
+ status?: string;
25
+ };
26
+
27
+ function normalizeToolStatus(status?: string): string {
28
+ if (status === "completed") return "completed";
29
+ if (status === "failed") return "failed";
30
+ if (status === "blocked") return "blocked";
31
+ return "unknown";
32
+ }
33
+
34
+ export class WeixinReplyProgressSender {
35
+ readonly runId: string;
36
+
37
+ private readonly to: string;
38
+ private readonly accountId: string;
39
+ private readonly opts: WeixinReplyProgressSenderDeps["opts"];
40
+ private finalized = false;
41
+ private sendChain: Promise<void> = Promise.resolve();
42
+
43
+ constructor(deps: WeixinReplyProgressSenderDeps) {
44
+ this.runId = deps.runId;
45
+ this.to = deps.to;
46
+ this.accountId = deps.accountId;
47
+ this.opts = { ...deps.opts, runId: deps.runId };
48
+ }
49
+
50
+ get replyOptions() {
51
+ return {
52
+ runId: this.runId,
53
+ onItemEvent: (payload: ToolItemEventPayload) => this.handleToolItemEvent(payload),
54
+ };
55
+ }
56
+
57
+ private enqueueMessage(item: MessageItem, label: string): void {
58
+ if (this.finalized) return;
59
+ this.sendChain = this.sendChain
60
+ .then(async () => {
61
+ await sendMessageItemWeixin({
62
+ to: this.to,
63
+ item,
64
+ opts: this.opts,
65
+ label,
66
+ });
67
+ })
68
+ .catch((err) => {
69
+ logger.warn(`${label}: failed to=${this.to} accountId=${this.accountId} runId=${this.runId} err=${String(err)}`);
70
+ });
71
+ }
72
+
73
+ private handleToolItemEvent(payload: ToolItemEventPayload): void {
74
+ if (this.finalized) return;
75
+ if (payload.kind !== "tool") return;
76
+ if (payload.phase !== "start" && payload.phase !== "end") return;
77
+
78
+ const now = Date.now();
79
+ const toolName = payload.name?.trim() || payload.title?.trim() || "tool";
80
+ const toolCallId = payload.itemId?.trim() || undefined;
81
+
82
+ if (payload.phase === "start") {
83
+ this.enqueueMessage(
84
+ {
85
+ type: MessageItemType.TOOL_CALL_START,
86
+ create_time_ms: now,
87
+ is_completed: false,
88
+ tool_call_start_item: {
89
+ tool_name: toolName,
90
+ tool_call_id: toolCallId,
91
+ },
92
+ },
93
+ "sendToolCallStartMessage",
94
+ );
95
+ return;
96
+ }
97
+
98
+ this.enqueueMessage(
99
+ {
100
+ type: MessageItemType.TOOL_CALL_RESULT,
101
+ create_time_ms: now,
102
+ is_completed: true,
103
+ tool_call_result_item: {
104
+ tool_name: toolName,
105
+ tool_call_id: toolCallId,
106
+ status: normalizeToolStatus(payload.status),
107
+ },
108
+ },
109
+ "sendToolCallResultMessage",
110
+ );
111
+ }
112
+
113
+ async finalize(): Promise<void> {
114
+ if (this.finalized) return;
115
+ this.finalized = true;
116
+ try {
117
+ await this.sendChain;
118
+ } catch (err) {
119
+ logger.warn(`WeixinReplyProgressSender.finalize: send drain failed runId=${this.runId} err=${String(err)}`);
120
+ }
121
+ }
122
+ }
@@ -18,7 +18,7 @@ export async function sendWeixinMediaFile(params: {
18
18
  filePath: string;
19
19
  to: string;
20
20
  text: string;
21
- opts: WeixinApiOptions & { contextToken?: string };
21
+ opts: WeixinApiOptions & { contextToken?: string; runId?: string };
22
22
  cdnBaseUrl: string;
23
23
  }): Promise<{ messageId: string }> {
24
24
  const { filePath, to, text, opts, cdnBaseUrl } = params;
@@ -10,6 +10,11 @@ import type { UploadedFileInfo } from "../cdn/upload.js";
10
10
 
11
11
  export { StreamingMarkdownFilter } from "./markdown-filter.js";
12
12
 
13
+ type WeixinMessageSendOptions = WeixinApiOptions & {
14
+ contextToken?: string;
15
+ runId?: string;
16
+ };
17
+
13
18
  function generateClientId(): string {
14
19
  return generateId("openclaw-weixin");
15
20
  }
@@ -19,9 +24,10 @@ function buildTextMessageReq(params: {
19
24
  to: string;
20
25
  text: string;
21
26
  contextToken?: string;
27
+ runId?: string;
22
28
  clientId: string;
23
29
  }): SendMessageReq {
24
- const { to, text, contextToken, clientId } = params;
30
+ const { to, text, contextToken, runId, clientId } = params;
25
31
  const item_list: MessageItem[] = text
26
32
  ? [{ type: MessageItemType.TEXT, text_item: { text } }]
27
33
  : [];
@@ -34,6 +40,7 @@ function buildTextMessageReq(params: {
34
40
  message_state: MessageState.FINISH,
35
41
  item_list: item_list.length ? item_list : undefined,
36
42
  context_token: contextToken ?? undefined,
43
+ run_id: runId ?? undefined,
37
44
  },
38
45
  };
39
46
  }
@@ -42,14 +49,16 @@ function buildTextMessageReq(params: {
42
49
  function buildSendMessageReq(params: {
43
50
  to: string;
44
51
  contextToken?: string;
52
+ runId?: string;
45
53
  payload: ReplyPayload;
46
54
  clientId: string;
47
55
  }): SendMessageReq {
48
- const { to, contextToken, payload, clientId } = params;
56
+ const { to, contextToken, runId, payload, clientId } = params;
49
57
  return buildTextMessageReq({
50
58
  to,
51
59
  text: payload.text ?? "",
52
60
  contextToken,
61
+ runId,
53
62
  clientId,
54
63
  });
55
64
  }
@@ -60,7 +69,7 @@ function buildSendMessageReq(params: {
60
69
  export async function sendMessageWeixin(params: {
61
70
  to: string;
62
71
  text: string;
63
- opts: WeixinApiOptions & { contextToken?: string };
72
+ opts: WeixinMessageSendOptions;
64
73
  }): Promise<{ messageId: string }> {
65
74
  const { to, text, opts } = params;
66
75
  if (!opts.contextToken) {
@@ -70,6 +79,7 @@ export async function sendMessageWeixin(params: {
70
79
  const req = buildSendMessageReq({
71
80
  to,
72
81
  contextToken: opts.contextToken,
82
+ runId: opts.runId,
73
83
  payload: { text },
74
84
  clientId,
75
85
  });
@@ -87,6 +97,47 @@ export async function sendMessageWeixin(params: {
87
97
  return { messageId: clientId };
88
98
  }
89
99
 
100
+ /** Send a single structured MessageItem downstream. */
101
+ export async function sendMessageItemWeixin(params: {
102
+ to: string;
103
+ item: MessageItem;
104
+ opts: WeixinMessageSendOptions;
105
+ clientId?: string;
106
+ label?: string;
107
+ }): Promise<{ messageId: string }> {
108
+ const { to, item, opts } = params;
109
+ if (!opts.contextToken) {
110
+ logger.warn(`sendMessageItemWeixin: contextToken missing for to=${to}, sending without context`);
111
+ }
112
+ const clientId = params.clientId ?? generateClientId();
113
+ const req: SendMessageReq = {
114
+ msg: {
115
+ from_user_id: "",
116
+ to_user_id: to,
117
+ client_id: clientId,
118
+ message_type: MessageType.BOT,
119
+ message_state: MessageState.FINISH,
120
+ item_list: [item],
121
+ context_token: opts.contextToken ?? undefined,
122
+ run_id: opts.runId,
123
+ },
124
+ };
125
+ try {
126
+ await sendMessageApi({
127
+ baseUrl: opts.baseUrl,
128
+ token: opts.token,
129
+ timeoutMs: opts.timeoutMs,
130
+ body: req,
131
+ });
132
+ } catch (err) {
133
+ logger.error(
134
+ `${params.label ?? "sendMessageItemWeixin"}: failed to=${to} clientId=${clientId} err=${String(err)}`,
135
+ );
136
+ throw err;
137
+ }
138
+ return { messageId: clientId };
139
+ }
140
+
90
141
  /**
91
142
  * Send one or more MessageItems (optionally preceded by a text caption) downstream.
92
143
  * Each item is sent as its own request so that item_list always has exactly one entry.
@@ -95,10 +146,11 @@ async function sendMediaItems(params: {
95
146
  to: string;
96
147
  text: string;
97
148
  mediaItem: MessageItem;
98
- opts: WeixinApiOptions & { contextToken?: string };
149
+ opts: WeixinMessageSendOptions;
99
150
  label: string;
100
151
  }): Promise<{ messageId: string }> {
101
152
  const { to, text, mediaItem, opts, label } = params;
153
+ const runId = opts.runId;
102
154
 
103
155
  const items: MessageItem[] = [];
104
156
  if (text) {
@@ -118,6 +170,7 @@ async function sendMediaItems(params: {
118
170
  message_state: MessageState.FINISH,
119
171
  item_list: [item],
120
172
  context_token: opts.contextToken ?? undefined,
173
+ run_id: runId,
121
174
  },
122
175
  };
123
176
  try {
@@ -152,7 +205,7 @@ export async function sendImageMessageWeixin(params: {
152
205
  to: string;
153
206
  text: string;
154
207
  uploaded: UploadedFileInfo;
155
- opts: WeixinApiOptions & { contextToken?: string };
208
+ opts: WeixinMessageSendOptions;
156
209
  }): Promise<{ messageId: string }> {
157
210
  const { to, text, uploaded, opts } = params;
158
211
  if (!opts.contextToken) {
@@ -186,7 +239,7 @@ export async function sendVideoMessageWeixin(params: {
186
239
  to: string;
187
240
  text: string;
188
241
  uploaded: UploadedFileInfo;
189
- opts: WeixinApiOptions & { contextToken?: string };
242
+ opts: WeixinMessageSendOptions;
190
243
  }): Promise<{ messageId: string }> {
191
244
  const { to, text, uploaded, opts } = params;
192
245
  if (!opts.contextToken) {
@@ -218,7 +271,7 @@ export async function sendFileMessageWeixin(params: {
218
271
  text: string;
219
272
  fileName: string;
220
273
  uploaded: UploadedFileInfo;
221
- opts: WeixinApiOptions & { contextToken?: string };
274
+ opts: WeixinMessageSendOptions;
222
275
  }): Promise<{ messageId: string }> {
223
276
  const { to, text, fileName, uploaded, opts } = params;
224
277
  if (!opts.contextToken) {
@@ -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";
@@ -96,6 +96,9 @@ export async function monitorWeixinProvider(opts: MonitorWeixinOpts): Promise<vo
96
96
  token,
97
97
  get_updates_buf: getUpdatesBuf,
98
98
  timeoutMs: nextTimeoutMs,
99
+ // Stop/hot-reload should cancel the in-flight long-poll immediately
100
+ // instead of waiting for the server-side long-poll timeout.
101
+ abortSignal,
99
102
  });
100
103
  aLog.debug(
101
104
  `getUpdates response: ret=${resp.ret}, msgs=${resp.msgs?.length ?? 0}, get_updates_buf_length=${resp.get_updates_buf?.length ?? 0}`,
@@ -109,17 +112,14 @@ export async function monitorWeixinProvider(opts: MonitorWeixinOpts): Promise<vo
109
112
  (resp.ret !== undefined && resp.ret !== 0) ||
110
113
  (resp.errcode !== undefined && resp.errcode !== 0);
111
114
  if (isApiError) {
112
- const isSessionExpired =
113
- resp.errcode === SESSION_EXPIRED_ERRCODE || resp.ret === SESSION_EXPIRED_ERRCODE;
115
+ const isStaleToken =
116
+ resp.errcode === STALE_TOKEN_ERRCODE || resp.ret === STALE_TOKEN_ERRCODE;
114
117
 
115
- if (isSessionExpired) {
118
+ if (isStaleToken) {
116
119
  pauseSession(accountId);
117
120
  const pauseMs = getRemainingPauseMs(accountId);
118
- errLog(
119
- `weixin getUpdates: session expired (errcode ${SESSION_EXPIRED_ERRCODE}), pausing bot for ${Math.ceil(pauseMs / 60_000)} min`,
120
- );
121
121
  aLog.error(
122
- `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`,
123
123
  );
124
124
  consecutiveFailures = 0;
125
125
  await sleep(pauseMs, abortSignal);
@@ -187,10 +187,11 @@ export async function monitorWeixinProvider(opts: MonitorWeixinOpts): Promise<vo
187
187
  return;
188
188
  }
189
189
  consecutiveFailures += 1;
190
+ const classified = classifyFetchError(err);
190
191
  errLog(
191
- `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}` : ""}`,
192
193
  );
193
- 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}`);
194
195
  if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
195
196
  errLog(
196
197
  `weixin getUpdates: ${MAX_CONSECUTIVE_FAILURES} consecutive failures, backing off 30s`,
@@ -1,177 +0,0 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
- import { logger } from "../util/logger.js";
4
- const OGG_CAPTURE = Buffer.from("OggS");
5
- const OPUS_HEAD_MAGIC = Buffer.from("OpusHead");
6
- const GP_UNKNOWN = 0xffffffffffffffffn;
7
- /** Weixin `VoiceItem.encode_type`: 6=silk (README: inbound SILK). */
8
- export const VOICE_ENCODE_SILK = 6;
9
- /** Ogg container (used for Opus-in-Ogg outbound). */
10
- export const VOICE_ENCODE_OGG_SPEEX = 8;
11
- /**
12
- * Extensions always treated as outbound voice (upload + VOICE item).
13
- * `.ogg` is handled separately: only when the file is Ogg Opus (see {@link isWeixinVoiceOutboundPath}).
14
- */
15
- const VOICE_EXTENSIONS = new Set([".opus", ".silk", ".slk"]);
16
- function readUint32LE(buf, o) {
17
- return buf.readUInt32LE(o);
18
- }
19
- /**
20
- * Parse one Ogg page starting at `start`. Returns next byte offset after this page, or null if invalid.
21
- */
22
- function skipOggPage(buf, start) {
23
- if (start + 27 > buf.length)
24
- return null;
25
- if (!buf.subarray(start, start + 4).equals(OGG_CAPTURE))
26
- return null;
27
- const nsegs = buf[start + 26];
28
- if (start + 27 + nsegs > buf.length)
29
- return null;
30
- let bodySize = 0;
31
- for (let i = 0; i < nsegs; i++) {
32
- bodySize += buf[start + 27 + i];
33
- }
34
- const end = start + 27 + nsegs + bodySize;
35
- if (end > buf.length)
36
- return null;
37
- return end;
38
- }
39
- /**
40
- * First Ogg page whose first packet payload starts with OpusHead defines the Opus logical stream serial.
41
- */
42
- function findOpusStreamSerial(buf) {
43
- let off = 0;
44
- while (off < buf.length) {
45
- const idx = buf.indexOf(OGG_CAPTURE, off);
46
- if (idx < 0)
47
- return null;
48
- const end = skipOggPage(buf, idx);
49
- if (end === null) {
50
- off = idx + 1;
51
- continue;
52
- }
53
- const nsegs = buf[idx + 26];
54
- const bodyStart = idx + 27 + nsegs;
55
- const firstSegLen = nsegs > 0 ? buf[idx + 27] : 0;
56
- if (firstSegLen >= OPUS_HEAD_MAGIC.length &&
57
- bodyStart + firstSegLen <= buf.length) {
58
- const payload = buf.subarray(bodyStart, bodyStart + firstSegLen);
59
- if (payload.subarray(0, OPUS_HEAD_MAGIC.length).equals(OPUS_HEAD_MAGIC)) {
60
- return readUint32LE(buf, idx + 14);
61
- }
62
- }
63
- off = end;
64
- }
65
- return null;
66
- }
67
- function maxGranuleForSerial(buf, targetSerial) {
68
- let off = 0;
69
- let maxGp = 0n;
70
- while (off < buf.length) {
71
- const idx = buf.indexOf(OGG_CAPTURE, off);
72
- if (idx < 0)
73
- break;
74
- const end = skipOggPage(buf, idx);
75
- if (end === null) {
76
- off = idx + 1;
77
- continue;
78
- }
79
- const serial = readUint32LE(buf, idx + 14);
80
- if (serial === targetSerial) {
81
- const gp = buf.readBigUInt64LE(idx + 6);
82
- if (gp !== GP_UNKNOWN && gp > maxGp)
83
- maxGp = gp;
84
- }
85
- off = end;
86
- }
87
- return maxGp;
88
- }
89
- /**
90
- * Opus granule position counts 48 kHz PCM samples (RFC 7845). Duration in ms = gp * 1000 / 48000.
91
- */
92
- export function playtimeMsFromOpusGranule(maxGp) {
93
- if (maxGp <= 0n)
94
- return 0;
95
- const ms = (maxGp * 1000n) / 48000n;
96
- return Number(ms);
97
- }
98
- export function parseOggOpusPlaytimeMs(buf) {
99
- const serial = findOpusStreamSerial(buf);
100
- if (serial === null)
101
- return null;
102
- const maxGp = maxGranuleForSerial(buf, serial);
103
- if (maxGp <= 0n)
104
- return null;
105
- const ms = playtimeMsFromOpusGranule(maxGp);
106
- return ms > 0 ? ms : null;
107
- }
108
- export function bufferLooksLikeOggOpus(buf) {
109
- return findOpusStreamSerial(buf) !== null;
110
- }
111
- async function silkPlaytimeMsWithOptionalWasm(buf) {
112
- try {
113
- const { decode } = await import("silk-wasm");
114
- const result = await decode(buf, 24_000);
115
- const d = result.duration;
116
- if (typeof d !== "number" || !Number.isFinite(d) || d <= 0)
117
- return null;
118
- return Math.round(d);
119
- }
120
- catch (err) {
121
- logger.debug(`voice-outbound: silk-wasm decode unavailable or failed err=${String(err)}`);
122
- return null;
123
- }
124
- }
125
- export async function isWeixinVoiceOutboundPath(filePath) {
126
- const ext = path.extname(filePath).toLowerCase();
127
- if (VOICE_EXTENSIONS.has(ext))
128
- return true;
129
- if (ext !== ".ogg")
130
- return false;
131
- const head = Buffer.allocUnsafe(65536);
132
- const fh = await fs.open(filePath, "r");
133
- try {
134
- const { bytesRead } = await fh.read(head, 0, head.length, 0);
135
- if (bytesRead <= 0)
136
- return false;
137
- return bufferLooksLikeOggOpus(head.subarray(0, bytesRead));
138
- }
139
- finally {
140
- await fh.close();
141
- }
142
- }
143
- /**
144
- * Compute voice playtime (ms) and encoding hints for Weixin VOICE item.
145
- * Returns null if duration cannot be determined (caller should send as file).
146
- */
147
- export async function resolveWeixinOutboundVoiceMeta(filePath) {
148
- const ext = path.extname(filePath).toLowerCase();
149
- const buf = await fs.readFile(filePath);
150
- if (ext === ".opus" || ext === ".ogg") {
151
- const ms = parseOggOpusPlaytimeMs(buf);
152
- if (ms == null || ms <= 0) {
153
- logger.warn(`voice-outbound: cannot resolve playtime for ${ext} file — parseOggOpusPlaytimeMs returned ${ms} (not a valid Ogg Opus stream?) filePath=${filePath} size=${buf.length}`);
154
- return null;
155
- }
156
- return {
157
- playtimeMs: ms,
158
- encode_type: VOICE_ENCODE_OGG_SPEEX,
159
- sample_rate: 48_000,
160
- };
161
- }
162
- if (ext === ".silk" || ext === ".slk") {
163
- const ms = await silkPlaytimeMsWithOptionalWasm(buf);
164
- if (ms == null || ms <= 0) {
165
- 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}`);
166
- return null;
167
- }
168
- return {
169
- playtimeMs: ms,
170
- encode_type: VOICE_ENCODE_SILK,
171
- sample_rate: 24_000,
172
- };
173
- }
174
- logger.warn(`voice-outbound: unsupported voice extension '${ext}' for filePath=${filePath}`);
175
- return null;
176
- }
177
- //# sourceMappingURL=voice-outbound.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"voice-outbound.js","sourceRoot":"","sources":["../../../src/media/voice-outbound.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAClC,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAE3C,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AACxC,MAAM,eAAe,GAAG,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;AAChD,MAAM,UAAU,GAAG,mBAAsB,CAAC;AAE1C,qEAAqE;AACrE,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC;AACnC,qDAAqD;AACrD,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC;AAExC;;;GAGG;AACH,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC;AAE7D,SAAS,YAAY,CAAC,GAAW,EAAE,CAAS;IAC1C,OAAO,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;AAC7B,CAAC;AAED;;GAEG;AACH,SAAS,WAAW,CAAC,GAAW,EAAE,KAAa;IAC7C,IAAI,KAAK,GAAG,EAAE,GAAG,GAAG,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IACzC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC;QAAE,OAAO,IAAI,CAAC;IACrE,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,GAAG,EAAE,CAAC,CAAC;IAC9B,IAAI,KAAK,GAAG,EAAE,GAAG,KAAK,GAAG,GAAG,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IACjD,IAAI,QAAQ,GAAG,CAAC,CAAC;IACjB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC;QAC/B,QAAQ,IAAI,GAAG,CAAC,KAAK,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;IAClC,CAAC;IACD,MAAM,GAAG,GAAG,KAAK,GAAG,EAAE,GAAG,KAAK,GAAG,QAAQ,CAAC;IAC1C,IAAI,GAAG,GAAG,GAAG,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IAClC,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;GAEG;AACH,SAAS,oBAAoB,CAAC,GAAW;IACvC,IAAI,GAAG,GAAG,CAAC,CAAC;IACZ,OAAO,GAAG,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC;QACxB,MAAM,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC;QAC1C,IAAI,GAAG,GAAG,CAAC;YAAE,OAAO,IAAI,CAAC;QACzB,MAAM,GAAG,GAAG,WAAW,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QAClC,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;YACjB,GAAG,GAAG,GAAG,GAAG,CAAC,CAAC;YACd,SAAS;QACX,CAAC;QACD,MAAM,KAAK,GAAG,GAAG,CAAC,GAAG,GAAG,EAAE,CAAC,CAAC;QAC5B,MAAM,SAAS,GAAG,GAAG,GAAG,EAAE,GAAG,KAAK,CAAC;QACnC,MAAM,WAAW,GAAG,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAClD,IACE,WAAW,IAAI,eAAe,CAAC,MAAM;YACrC,SAAS,GAAG,WAAW,IAAI,GAAG,CAAC,MAAM,EACrC,CAAC;YACD,MAAM,OAAO,GAAG,GAAG,CAAC,QAAQ,CAAC,SAAS,EAAE,SAAS,GAAG,WAAW,CAAC,CAAC;YACjE,IAAI,OAAO,CAAC,QAAQ,CAAC,CAAC,EAAE,eAAe,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,eAAe,CAAC,EAAE,CAAC;gBACxE,OAAO,YAAY,CAAC,GAAG,EAAE,GAAG,GAAG,EAAE,CAAC,CAAC;YACrC,CAAC;QACH,CAAC;QACD,GAAG,GAAG,GAAG,CAAC;IACZ,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,mBAAmB,CAAC,GAAW,EAAE,YAAoB;IAC5D,IAAI,GAAG,GAAG,CAAC,CAAC;IACZ,IAAI,KAAK,GAAG,EAAE,CAAC;IACf,OAAO,GAAG,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC;QACxB,MAAM,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC;QAC1C,IAAI,GAAG,GAAG,CAAC;YAAE,MAAM;QACnB,MAAM,GAAG,GAAG,WAAW,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QAClC,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;YACjB,GAAG,GAAG,GAAG,GAAG,CAAC,CAAC;YACd,SAAS;QACX,CAAC;QACD,MAAM,MAAM,GAAG,YAAY,CAAC,GAAG,EAAE,GAAG,GAAG,EAAE,CAAC,CAAC;QAC3C,IAAI,MAAM,KAAK,YAAY,EAAE,CAAC;YAC5B,MAAM,EAAE,GAAG,GAAG,CAAC,eAAe,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC;YACxC,IAAI,EAAE,KAAK,UAAU,IAAI,EAAE,GAAG,KAAK;gBAAE,KAAK,GAAG,EAAE,CAAC;QAClD,CAAC;QACD,GAAG,GAAG,GAAG,CAAC;IACZ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,yBAAyB,CAAC,KAAa;IACrD,IAAI,KAAK,IAAI,EAAE;QAAE,OAAO,CAAC,CAAC;IAC1B,MAAM,EAAE,GAAG,CAAC,KAAK,GAAG,KAAK,CAAC,GAAG,MAAM,CAAC;IACpC,OAAO,MAAM,CAAC,EAAE,CAAC,CAAC;AACpB,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,GAAW;IAChD,MAAM,MAAM,GAAG,oBAAoB,CAAC,GAAG,CAAC,CAAC;IACzC,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACjC,MAAM,KAAK,GAAG,mBAAmB,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IAC/C,IAAI,KAAK,IAAI,EAAE;QAAE,OAAO,IAAI,CAAC;IAC7B,MAAM,EAAE,GAAG,yBAAyB,CAAC,KAAK,CAAC,CAAC;IAC5C,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AAC5B,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,GAAW;IAChD,OAAO,oBAAoB,CAAC,GAAG,CAAC,KAAK,IAAI,CAAC;AAC5C,CAAC;AAED,KAAK,UAAU,8BAA8B,CAAC,GAAW;IACvD,IAAI,CAAC;QACH,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC,CAAC;QAC7C,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QACzC,MAAM,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC;QAC1B,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC;QACxE,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACvB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,CAAC,KAAK,CAAC,8DAA8D,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC1F,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAAC,QAAgB;IAC9D,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,WAAW,EAAE,CAAC;IACjD,IAAI,gBAAgB,CAAC,GAAG,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAC3C,IAAI,GAAG,KAAK,MAAM;QAAE,OAAO,KAAK,CAAC;IAEjC,MAAM,IAAI,GAAG,MAAM,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;IACvC,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;IACxC,IAAI,CAAC;QACH,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;QAC7D,IAAI,SAAS,IAAI,CAAC;YAAE,OAAO,KAAK,CAAC;QACjC,OAAO,sBAAsB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC;IAC7D,CAAC;YAAS,CAAC;QACT,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC;IACnB,CAAC;AACH,CAAC;AAQD;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,8BAA8B,CAAC,QAAgB;IACnE,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,WAAW,EAAE,CAAC;IACjD,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAExC,IAAI,GAAG,KAAK,OAAO,IAAI,GAAG,KAAK,MAAM,EAAE,CAAC;QACtC,MAAM,EAAE,GAAG,sBAAsB,CAAC,GAAG,CAAC,CAAC;QACvC,IAAI,EAAE,IAAI,IAAI,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC;YAC1B,MAAM,CAAC,IAAI,CACT,+CAA+C,GAAG,2CAA2C,EAAE,4CAA4C,QAAQ,SAAS,GAAG,CAAC,MAAM,EAAE,CACzK,CAAC;YACF,OAAO,IAAI,CAAC;QACd,CAAC;QACD,OAAO;YACL,UAAU,EAAE,EAAE;YACd,WAAW,EAAE,sBAAsB;YACnC,WAAW,EAAE,MAAM;SACpB,CAAC;IACJ,CAAC;IAED,IAAI,GAAG,KAAK,OAAO,IAAI,GAAG,KAAK,MAAM,EAAE,CAAC;QACtC,MAAM,EAAE,GAAG,MAAM,8BAA8B,CAAC,GAAG,CAAC,CAAC;QACrD,IAAI,EAAE,IAAI,IAAI,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC;YAC1B,MAAM,CAAC,IAAI,CACT,+CAA+C,GAAG,qCAAqC,EAAE,wDAAwD,QAAQ,SAAS,GAAG,CAAC,MAAM,EAAE,CAC/K,CAAC;YACF,OAAO,IAAI,CAAC;QACd,CAAC;QACD,OAAO;YACL,UAAU,EAAE,EAAE;YACd,WAAW,EAAE,iBAAiB;YAC9B,WAAW,EAAE,MAAM;SACpB,CAAC;IACJ,CAAC;IAED,MAAM,CAAC,IAAI,CACT,gDAAgD,GAAG,kBAAkB,QAAQ,EAAE,CAChF,CAAC;IACF,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -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"}