@tencent-weixin/openclaw-weixin 2.4.1 → 2.4.3

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 (46) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/CHANGELOG.zh_CN.md +45 -0
  3. package/dist/index.js +0 -4
  4. package/dist/index.js.map +1 -1
  5. package/dist/src/api/api.js +41 -7
  6. package/dist/src/api/api.js.map +1 -1
  7. package/dist/src/auth/accounts.js.map +1 -1
  8. package/dist/src/auth/login-qr.js +1 -0
  9. package/dist/src/auth/login-qr.js.map +1 -1
  10. package/dist/src/channel.js +19 -0
  11. package/dist/src/channel.js.map +1 -1
  12. package/dist/src/media/voice-outbound.js +177 -0
  13. package/dist/src/media/voice-outbound.js.map +1 -0
  14. package/dist/src/messaging/abort-fence.js +70 -0
  15. package/dist/src/messaging/abort-fence.js.map +1 -0
  16. package/dist/src/messaging/buttons.js +117 -0
  17. package/dist/src/messaging/buttons.js.map +1 -0
  18. package/dist/src/messaging/lane-key.js +66 -0
  19. package/dist/src/messaging/lane-key.js.map +1 -0
  20. package/dist/src/messaging/merged-record.js +149 -0
  21. package/dist/src/messaging/merged-record.js.map +1 -0
  22. package/dist/src/messaging/model-buttons.js +182 -0
  23. package/dist/src/messaging/model-buttons.js.map +1 -0
  24. package/dist/src/messaging/model-callback-handler.js +133 -0
  25. package/dist/src/messaging/model-callback-handler.js.map +1 -0
  26. package/dist/src/monitor/lane-scheduler.js +46 -0
  27. package/dist/src/monitor/lane-scheduler.js.map +1 -0
  28. package/dist/src/monitor/monitor.js +5 -12
  29. package/dist/src/monitor/monitor.js.map +1 -1
  30. package/dist/src/streaming/stream-pipeline.js +431 -0
  31. package/dist/src/streaming/stream-pipeline.js.map +1 -0
  32. package/dist/src/streaming/stream-session.js +260 -0
  33. package/dist/src/streaming/stream-session.js.map +1 -0
  34. package/dist/src/streaming/stream.js +239 -0
  35. package/dist/src/streaming/stream.js.map +1 -0
  36. package/dist/src/util/markdown-fences.js +54 -0
  37. package/dist/src/util/markdown-fences.js.map +1 -0
  38. package/index.ts +0 -5
  39. package/openclaw.plugin.json +1 -1
  40. package/package.json +1 -1
  41. package/src/api/api.ts +42 -8
  42. package/src/auth/accounts.ts +0 -1
  43. package/src/auth/login-qr.ts +8 -0
  44. package/src/channel.ts +22 -1
  45. package/src/monitor/monitor.ts +11 -10
  46. package/src/runtime.ts +0 -70
@@ -0,0 +1,177 @@
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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"voice-outbound.js","sourceRoot":"","sources":["../../../src/media/voice-outbound.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,70 @@
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
@@ -0,0 +1 @@
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"}
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Shared button normalisation helpers used by both the channel action handler
3
+ * (channel.ts) and the inbound message dispatch path (process-message.ts).
4
+ */
5
+ import { logger } from "../util/logger.js";
6
+ import { findFenceSpanAt, parseFenceSpans } from "../util/markdown-fences.js";
7
+ /**
8
+ * Normalize a single button-like object to {text, callback_data, style?}.
9
+ * Handles field name variants: label→text, value/callbackData→callback_data.
10
+ */
11
+ export function normalizeButtonObject(b) {
12
+ const text = String(b.text ?? b.label ?? "");
13
+ const callbackData = String(b.callback_data ?? b.callbackData ?? b.value ?? b.action ?? b.data ?? "");
14
+ if (!text && !callbackData)
15
+ return null;
16
+ const style = typeof b.style === "string" && b.style ? b.style : undefined;
17
+ return style
18
+ ? { text, callback_data: callbackData, style }
19
+ : { text, callback_data: callbackData };
20
+ }
21
+ /**
22
+ * Normalize any button-like value to the canonical [[{text, callback_data}]] format.
23
+ * Handles all known AI malformations:
24
+ * - [[{text, callback_data}]] → already correct
25
+ * - [{text, callback_data}] → flat array, wrap into single row
26
+ * - {text, callback_data} → single button object
27
+ * - {buttons: [...]} → unwrap .buttons
28
+ * - [{type:"button", label, value}] → normalize fields
29
+ */
30
+ export function normalizeButtonsParam(raw) {
31
+ if (raw == null)
32
+ return undefined;
33
+ if (Array.isArray(raw)) {
34
+ if (raw.length === 0)
35
+ return undefined;
36
+ if (Array.isArray(raw[0])) {
37
+ return raw;
38
+ }
39
+ const flatButtons = raw.filter((b) => b != null && typeof b === "object" && !Array.isArray(b));
40
+ if (flatButtons.length === 0)
41
+ return undefined;
42
+ const row = flatButtons.map(normalizeButtonObject).filter(Boolean);
43
+ return row.length > 0 ? [row] : undefined;
44
+ }
45
+ if (typeof raw === "object") {
46
+ const obj = raw;
47
+ if (Array.isArray(obj.buttons)) {
48
+ return normalizeButtonsParam(obj.buttons);
49
+ }
50
+ const btn = normalizeButtonObject(obj);
51
+ return btn ? [[btn]] : undefined;
52
+ }
53
+ return undefined;
54
+ }
55
+ /**
56
+ * Parse and strip the inline WEIXIN_BUTTONS marker from outbound text.
57
+ *
58
+ * Agents writing plain-text responses (deliver / cron / streaming) can embed
59
+ * buttons by appending a line of the form:
60
+ *
61
+ * WEIXIN_BUTTONS:[{"label":"Yes","value":"yes"},{"label":"No","value":"no"}]
62
+ *
63
+ * Returns:
64
+ * - `cleanText`: the original text with the marker line removed and any
65
+ * resulting extra blank lines collapsed.
66
+ * - `buttons`: the normalised button array (canonical [[{text,callback_data}]]
67
+ * form) ready to pass to sendMessageWeixin / sendFinalMessageWithStreamId,
68
+ * or undefined when no valid marker is found.
69
+ */
70
+ const INLINE_BUTTONS_RE = /^WEIXIN_BUTTONS:(\[[\s\S]*?\])\s*$/gm;
71
+ export function parseInlineButtons(text) {
72
+ // Mirror openclaw core's MEDIA: handling: a marker that lives inside a
73
+ // fenced code block is treated as literal documentation, not a directive.
74
+ const fenceSpans = parseFenceSpans(text);
75
+ const re = new RegExp(INLINE_BUTTONS_RE.source, INLINE_BUTTONS_RE.flags);
76
+ let match;
77
+ while ((match = re.exec(text)) !== null) {
78
+ if (findFenceSpanAt(fenceSpans, match.index))
79
+ continue;
80
+ try {
81
+ const parsed = JSON.parse(match[1]);
82
+ const buttons = normalizeButtonsParam(parsed);
83
+ const cleanText = text
84
+ .replace(match[0], "")
85
+ .replace(/\n{3,}/g, "\n\n")
86
+ .trim();
87
+ logger.debug(`parseInlineButtons: extracted ${buttons?.length ?? 0} button rows from inline marker`);
88
+ return { cleanText, buttons };
89
+ }
90
+ catch {
91
+ logger.warn(`parseInlineButtons: JSON parse failed, keeping marker as-is`);
92
+ return { cleanText: text, buttons: undefined };
93
+ }
94
+ }
95
+ return { cleanText: text, buttons: undefined };
96
+ }
97
+ /**
98
+ * Unified outbound helper: strip any inline WEIXIN_BUTTONS marker from `text`
99
+ * and merge the resulting buttons with any explicitly-provided buttons.
100
+ *
101
+ * Explicit buttons always win over marker-parsed buttons — an agent that calls
102
+ * the `message` tool with a structured `buttons` param is making a more
103
+ * deliberate choice than one that embeds a marker in free-text.
104
+ *
105
+ * Every outbound entry point (sendText / sendMedia / sendPayload / handleAction
106
+ * / streaming deliver) should route text through this helper so the marker is
107
+ * never leaked to the wire regardless of which code path delivered it.
108
+ */
109
+ export function resolveOutboundButtons(text, explicitButtons) {
110
+ const explicitNormalised = explicitButtons != null ? normalizeButtonsParam(explicitButtons) : undefined;
111
+ const { cleanText, buttons: inlineButtons } = parseInlineButtons(text);
112
+ return {
113
+ text: cleanText,
114
+ buttons: explicitNormalised ?? inlineButtons,
115
+ };
116
+ }
117
+ //# sourceMappingURL=buttons.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"buttons.js","sourceRoot":"","sources":["../../../src/messaging/buttons.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,66 @@
1
+ import { isAbortRequestText } from "openclaw/plugin-sdk/reply-runtime";
2
+ import { MessageItemType } from "../api/types.js";
3
+ /**
4
+ * Lane key for Weixin message scheduling.
5
+ *
6
+ * Mirrors the Telegram channel's `getTelegramSequentialKey` (see
7
+ * extensions/telegram/src/sequential-key.ts in the openclaw main repo).
8
+ *
9
+ * Design:
10
+ * - Different users get different lane keys -> processed concurrently.
11
+ * - Same user, regular messages -> share `wx:<account>:<user>` lane,
12
+ * processed strictly in arrival order so AI replies, typing state and
13
+ * inbound session writes do not interleave.
14
+ * - Abort requests (e.g. "stop", "中止") are routed to a separate
15
+ * `wx:<account>:<user>:control` lane so they can run immediately even when
16
+ * the main lane is occupied by a long-running tool turn. The actual
17
+ * "stopping" is implemented via the abort-fence mechanism (see
18
+ * `abort-fence.ts`).
19
+ */
20
+ function extractFirstTextBody(itemList) {
21
+ if (!itemList?.length)
22
+ return "";
23
+ for (const item of itemList) {
24
+ if (item.type === MessageItemType.TEXT && item.text_item?.text != null) {
25
+ return String(item.text_item.text);
26
+ }
27
+ }
28
+ return "";
29
+ }
30
+ /** Extract text body suitable for abort detection from an inbound message. */
31
+ export function extractTextBodyForLane(msg) {
32
+ return extractFirstTextBody(msg.item_list);
33
+ }
34
+ /** Returns true when the message text is an abort intent (stop / 中止 / ...). */
35
+ export function isWeixinAbortMessage(msg) {
36
+ const text = extractTextBodyForLane(msg);
37
+ if (!text)
38
+ return false;
39
+ return isAbortRequestText(text);
40
+ }
41
+ /**
42
+ * Compute the lane key used by the per-key scheduler. Abort requests get the
43
+ * `:control` suffix to bypass the main lane.
44
+ *
45
+ * Note: the fallback `unknown` user happens for malformed payloads. Keep them
46
+ * on a shared lane so they cannot fan out and exhaust resources.
47
+ */
48
+ export function getWeixinLaneKey(ctx) {
49
+ const userId = ctx.msg.from_user_id?.trim() || "unknown";
50
+ const baseKey = `wx:${ctx.accountId}:${userId}`;
51
+ if (isWeixinAbortMessage(ctx.msg)) {
52
+ return `${baseKey}:control`;
53
+ }
54
+ return baseKey;
55
+ }
56
+ /**
57
+ * Per-user fence key shared by all dispatches that should be aborted as a
58
+ * group when the user sends an abort. This is intentionally the *base* key
59
+ * (without `:control`) so that an abort dispatched on the control lane bumps
60
+ * the same generation that the long-running main-lane dispatch is checking.
61
+ */
62
+ export function getWeixinFenceKey(ctx) {
63
+ const userId = ctx.msg.from_user_id?.trim() || "unknown";
64
+ return `wx:${ctx.accountId}:${userId}`;
65
+ }
66
+ //# sourceMappingURL=lane-key.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lane-key.js","sourceRoot":"","sources":["../../../src/messaging/lane-key.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,mCAAmC,CAAC;AAEvE,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAGlD;;;;;;;;;;;;;;;;GAgBG;AAEH,SAAS,oBAAoB,CAAC,QAAwB;IACpD,IAAI,CAAC,QAAQ,EAAE,MAAM;QAAE,OAAO,EAAE,CAAC;IACjC,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE,CAAC;QAC5B,IAAI,IAAI,CAAC,IAAI,KAAK,eAAe,CAAC,IAAI,IAAI,IAAI,CAAC,SAAS,EAAE,IAAI,IAAI,IAAI,EAAE,CAAC;YACvE,OAAO,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;QACrC,CAAC;IACH,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,sBAAsB,CAAC,GAAkB;IACvD,OAAO,oBAAoB,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;AAC7C,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,oBAAoB,CAAC,GAAkB;IACrD,MAAM,IAAI,GAAG,sBAAsB,CAAC,GAAG,CAAC,CAAC;IACzC,IAAI,CAAC,IAAI;QAAE,OAAO,KAAK,CAAC;IACxB,OAAO,kBAAkB,CAAC,IAAI,CAAC,CAAC;AAClC,CAAC;AAOD;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,GAAyB;IACxD,MAAM,MAAM,GAAG,GAAG,CAAC,GAAG,CAAC,YAAY,EAAE,IAAI,EAAE,IAAI,SAAS,CAAC;IACzD,MAAM,OAAO,GAAG,MAAM,GAAG,CAAC,SAAS,IAAI,MAAM,EAAE,CAAC;IAChD,IAAI,oBAAoB,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;QAClC,OAAO,GAAG,OAAO,UAAU,CAAC;IAC9B,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAAyB;IACzD,MAAM,MAAM,GAAG,GAAG,CAAC,GAAG,CAAC,YAAY,EAAE,IAAI,EAAE,IAAI,SAAS,CAAC;IACzD,OAAO,MAAM,GAAG,CAAC,SAAS,IAAI,MAAM,EAAE,CAAC;AACzC,CAAC"}
@@ -0,0 +1,149 @@
1
+ import { MessageItemType } from "../api/types.js";
2
+ import { downloadMediaFromItem } from "../media/media-download.js";
3
+ import { logger } from "../util/logger.js";
4
+ const MAX_DEPTH = 5;
5
+ function formatTimestamp(ms) {
6
+ if (!ms)
7
+ return "";
8
+ const d = new Date(ms);
9
+ const pad = (n) => String(n).padStart(2, "0");
10
+ const date = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
11
+ const time = `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
12
+ const tz = new Intl.DateTimeFormat("en", { timeZoneName: "short" })
13
+ .formatToParts(d)
14
+ .find((p) => p.type === "timeZoneName")?.value ?? "";
15
+ return `${date} ${time}${tz ? ` ${tz}` : ""}`;
16
+ }
17
+ function formatSubItemContent(item) {
18
+ switch (item.type) {
19
+ case MessageItemType.TEXT:
20
+ return item.text_item?.text ?? "";
21
+ case MessageItemType.VOICE:
22
+ if (item.voice_item?.text)
23
+ return item.voice_item.text;
24
+ return undefined;
25
+ case MessageItemType.UNSUPPORTED:
26
+ return item.unsupported_item?.text || "[不支持的消息类型]";
27
+ default:
28
+ return undefined;
29
+ }
30
+ }
31
+ async function formatMediaContent(item, deps) {
32
+ const downloaded = await downloadMediaFromItem(item, {
33
+ cdnBaseUrl: deps.cdnBaseUrl,
34
+ saveMedia: deps.saveMedia,
35
+ log: deps.log,
36
+ errLog: deps.errLog,
37
+ label: "merged-record",
38
+ });
39
+ const filePath = downloaded.decryptedPicPath ??
40
+ downloaded.decryptedVideoPath ??
41
+ downloaded.decryptedFilePath ??
42
+ downloaded.decryptedVoicePath;
43
+ let text;
44
+ let mediaType;
45
+ switch (item.type) {
46
+ case MessageItemType.IMAGE:
47
+ text = filePath ? `![图片](${filePath})` : "[图片]";
48
+ mediaType = "image/*";
49
+ break;
50
+ case MessageItemType.VIDEO:
51
+ text = filePath ? `[视频](${filePath})` : "[视频]";
52
+ mediaType = "video/mp4";
53
+ break;
54
+ case MessageItemType.FILE: {
55
+ const name = item.file_item?.file_name;
56
+ if (name && filePath)
57
+ text = `[${name}](${filePath})`;
58
+ else if (name)
59
+ text = `[文件: ${name}]`;
60
+ else if (filePath)
61
+ text = `[文件](${filePath})`;
62
+ else
63
+ text = "[文件]";
64
+ mediaType = downloaded.fileMediaType ?? "application/octet-stream";
65
+ break;
66
+ }
67
+ case MessageItemType.VOICE:
68
+ text = filePath ? `[语音](${filePath})` : "[语音]";
69
+ mediaType = downloaded.voiceMediaType ?? "audio/wav";
70
+ break;
71
+ default:
72
+ text = "[不支持的消息]";
73
+ break;
74
+ }
75
+ return { text, mediaPath: filePath, mediaType: filePath ? mediaType : undefined };
76
+ }
77
+ /**
78
+ * Format a MergedRecordItem into a readable text body with nested structure.
79
+ *
80
+ * Each nesting level is represented by `│ ` prefixes. Media sub-items are
81
+ * downloaded via CDN; the body contains text placeholders (e.g. `[图片]`)
82
+ * while actual file paths are returned in `mediaPaths` / `mediaTypes`
83
+ * for populating `ctx.MediaPaths` / `ctx.MediaTypes`.
84
+ */
85
+ export async function formatMergedRecordBody(record, deps, depth = 0) {
86
+ const title = record.title ?? "聊天记录";
87
+ const lines = [`[合并转发: ${title}]`];
88
+ const mediaPaths = [];
89
+ const mediaTypes = [];
90
+ if (depth >= MAX_DEPTH) {
91
+ lines.push("│ [嵌套层级过深,省略]");
92
+ return { body: lines.join("\n"), mediaPaths, mediaTypes };
93
+ }
94
+ for (const sub of record.sub_item_list ?? []) {
95
+ const nick = sub.from_nick_name ?? "未知";
96
+ const item = sub.item;
97
+ if (!item)
98
+ continue;
99
+ const ts = formatTimestamp(item.create_time_ms);
100
+ const header = ts ? `${nick} (${ts}):` : `${nick}:`;
101
+ if (item.type === MessageItemType.MERGED_RECORD && item.merged_record) {
102
+ lines.push(`│ ${header}`);
103
+ const nested = await formatMergedRecordBody(item.merged_record, deps, depth + 1);
104
+ for (const nestedLine of nested.body.split("\n")) {
105
+ lines.push(`│ ${nestedLine}`);
106
+ }
107
+ mediaPaths.push(...nested.mediaPaths);
108
+ mediaTypes.push(...nested.mediaTypes);
109
+ }
110
+ else {
111
+ const syncContent = formatSubItemContent(item);
112
+ if (syncContent !== undefined) {
113
+ lines.push(`│ ${header}`);
114
+ for (const contentLine of syncContent.split("\n")) {
115
+ lines.push(`│ ${contentLine}`);
116
+ }
117
+ }
118
+ else if (item.type === MessageItemType.IMAGE ||
119
+ item.type === MessageItemType.VIDEO ||
120
+ item.type === MessageItemType.FILE ||
121
+ item.type === MessageItemType.VOICE) {
122
+ try {
123
+ const media = await formatMediaContent(item, deps);
124
+ lines.push(`│ ${header}`);
125
+ lines.push(`│ ${media.text}`);
126
+ if (media.mediaPath && media.mediaType) {
127
+ mediaPaths.push(media.mediaPath);
128
+ mediaTypes.push(media.mediaType);
129
+ }
130
+ }
131
+ catch (err) {
132
+ logger.error(`merged-record: media format failed: ${String(err)}`);
133
+ lines.push(`│ ${header}`);
134
+ lines.push("│ [媒体加载失败]");
135
+ }
136
+ }
137
+ else {
138
+ lines.push(`│ ${header}`);
139
+ lines.push("│ [不支持的消息]");
140
+ }
141
+ }
142
+ lines.push("│");
143
+ }
144
+ if (lines.length > 1 && lines[lines.length - 1] === "│") {
145
+ lines.pop();
146
+ }
147
+ return { body: lines.join("\n"), mediaPaths, mediaTypes };
148
+ }
149
+ //# sourceMappingURL=merged-record.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merged-record.js","sourceRoot":"","sources":["../../../src/messaging/merged-record.ts"],"names":[],"mappings":"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"}