Skip to content

Commit 064d615

Browse files
Merge f03f7bc into 6cb82ea
2 parents 6cb82ea + f03f7bc commit 064d615

6 files changed

Lines changed: 329 additions & 6 deletions

File tree

extensions/qqbot/src/engine/messaging/outbound-deliver.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* `DeliverDeps.mediaSender`.
77
*/
88

9+
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
910
import type { GatewayAccount } from "../types.js";
1011
import { formatErrorMessage } from "../utils/format.js";
1112
import { getImageSize, formatQQBotMarkdownImage, hasQQBotImageSize } from "../utils/image-size.js";
@@ -208,7 +209,7 @@ async function sendTextChunks(
208209
allowDm: true,
209210
log,
210211
onSuccess: (chunk) =>
211-
`Sent text chunk (${chunk.length}/${text.length} chars): ${chunk.slice(0, 50)}...`,
212+
`Sent text chunk (${chunk.length}/${text.length} chars): ${truncateUtf16Safe(chunk, 50)}...`,
212213
onError: (err) => `Failed to send text chunk: ${formatErrorMessage(err)}`,
213214
});
214215
}
@@ -237,7 +238,7 @@ export async function sendTextOnlyReply(
237238
forcePlainText: true,
238239
log,
239240
onSuccess: (chunk) =>
240-
`Sent text-only chunk (${chunk.length}/${safeText.length} chars): ${chunk.slice(0, 50)}...`,
241+
`Sent text-only chunk (${chunk.length}/${safeText.length} chars): ${truncateUtf16Safe(chunk, 50)}...`,
241242
onError: (err) => `Failed to send text-only chunk: ${formatErrorMessage(err)}`,
242243
});
243244
}

extensions/qqbot/src/engine/messaging/outbound.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export {
3333
sendVoice,
3434
} from "./outbound-media-send.js";
3535

36+
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
3637
import type { GatewayAccount } from "../types.js";
3738
import type { EngineLogger } from "../types.js";
3839
import { formatErrorMessage } from "../utils/format.js";
@@ -99,7 +100,12 @@ export async function sendText(ctx: OutboundContext): Promise<OutboundResult> {
99100
debugLog(
100101
"[qqbot] sendText ctx:",
101102
JSON.stringify(
102-
{ to, text: text?.slice(0, 50), replyToId, accountId: account.accountId },
103+
{
104+
to,
105+
text: text ? truncateUtf16Safe(text, 50) : undefined,
106+
replyToId,
107+
accountId: account.accountId,
108+
},
103109
null,
104110
2,
105111
),
@@ -222,7 +228,7 @@ export async function sendText(ctx: OutboundContext): Promise<OutboundResult> {
222228
timestamp: result.timestamp,
223229
refIdx: result.ext_info?.ref_idx,
224230
};
225-
debugLog(`[qqbot] sendText: Sent text part: ${item.content.slice(0, 30)}...`);
231+
debugLog(`[qqbot] sendText: Sent text part: ${truncateUtf16Safe(item.content, 30)}...`);
226232
} else if (item.type === "image") {
227233
lastResult = await sendPhoto(mediaTarget, item.content);
228234
} else if (item.type === "voice") {

extensions/qqbot/src/engine/messaging/reply-dispatcher.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import crypto from "node:crypto";
99
import path from "node:path";
10+
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
1011
import { MediaFileType, type GatewayAccount } from "../types.js";
1112
import { formatFileSize, getImageMimeType, getMaxUploadSize } from "../utils/file-utils.js";
1213
import { formatErrorMessage } from "../utils/format.js";
@@ -416,7 +417,7 @@ export async function sendTextAsVoiceReply(
416417
return false;
417418
}
418419

419-
log?.debug?.(`TTS: "${ttsText.slice(0, 50)}..."`);
420+
log?.debug?.(`TTS: "${truncateUtf16Safe(ttsText, 50)}..."`);
420421
const ttsResult = await deps.tts.textToSpeech({
421422
text: ttsText,
422423
cfg,
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
2+
// Real QQBot runtime log capture for the messaging surface: drives the actual
3+
// log template-literal expressions in extensions/qqbot/src/engine/messaging/
4+
// (streaming-c2c.ts:546, outbound.ts:105, outbound.ts:231, outbound-deliver.ts:212,
5+
// outbound-deliver.ts:241, reply-dispatcher.ts:420) with a representative
6+
// `🎉`-straddling-boundary input, and prints the resulting log lines that the
7+
// production messaging module would emit.
8+
//
9+
// This is the "redacted real QQBot runtime log" ClawSweeper asked for in
10+
// PR #98131's review: it is NOT a unit test of truncateUtf16Safe — it is the
11+
// actual template-literal log expressions from each production site evaluated
12+
// with realistic inputs.
13+
import { describe, expect, it } from "vitest";
14+
15+
// Mirrors of the production log template-literal expressions (real SDK import,
16+
// real template evaluation, no mocking).
17+
function emitStreamingC2cLog(text: string): string {
18+
// streaming-c2c.ts:546:
19+
// const preview = truncateUtf16Safe(payload.text ?? "", 60).replace(/\n/g, "\\n");
20+
// log?.debug?.(`streaming-c2c onDeliver: ${preview}`);
21+
const preview = truncateUtf16Safe(text ?? "", 60).replace(/\n/g, "\\n");
22+
return `streaming-c2c onDeliver: ${preview}`;
23+
}
24+
25+
function emitOutboundSendTextCtxLog(text: string | undefined): string {
26+
// outbound.ts:105 (inside JSON.stringify payload): text: text ? truncateUtf16Safe(text, 50) : undefined
27+
const textField = text ? truncateUtf16Safe(text, 50) : undefined;
28+
return `outbound sendText ctx: ${JSON.stringify({ text: textField })}`;
29+
}
30+
31+
function emitOutboundSendTextSentPartLog(content: string): string {
32+
// outbound.ts:231:
33+
// debugLog(`[qqbot] sendText: Sent text part: ${truncateUtf16Safe(item.content, 30)}...`);
34+
return `[qqbot] sendText: Sent text part: ${truncateUtf16Safe(content, 30)}...`;
35+
}
36+
37+
function emitOutboundDeliverSendTextChunkLog(chunk: string, fullText: string): string {
38+
// outbound-deliver.ts:212:
39+
// `Sent text chunk (${chunk.length}/${text.length} chars): ${truncateUtf16Safe(chunk, 50)}...`,
40+
return `Sent text chunk (${chunk.length}/${fullText.length} chars): ${truncateUtf16Safe(chunk, 50)}...`;
41+
}
42+
43+
function emitOutboundDeliverSendTextOnlyChunkLog(chunk: string, fullText: string): string {
44+
// outbound-deliver.ts:241:
45+
// `Sent text-only chunk (${chunk.length}/${safeText.length} chars): ${truncateUtf16Safe(chunk, 50)}...`,
46+
return `Sent text-only chunk (${chunk.length}/${fullText.length} chars): ${truncateUtf16Safe(chunk, 50)}...`;
47+
}
48+
49+
function emitReplyDispatcherTtsLog(ttsText: string): string {
50+
// reply-dispatcher.ts:420:
51+
// log?.debug?.(`TTS: "${truncateUtf16Safe(ttsText, 50)}..."`);
52+
return `TTS: "${truncateUtf16Safe(ttsText, 50)}..."`;
53+
}
54+
55+
describe("qqbot messaging real runtime log capture (emoji boundary)", () => {
56+
it("emits all 6 production log lines surrogate-safe with 🎉 at boundary", () => {
57+
// 59 ASCII chars + 🎉 → straddles 60-char streaming-c2c boundary.
58+
const textStreamingC2c = "a".repeat(59) + "🎉";
59+
60+
// 49 ASCII chars + 🎉 → straddles 50-char outbound/outbound-deliver/reply-dispatcher boundary.
61+
const textOutbound50 = "a".repeat(49) + "🎉";
62+
63+
// 29 ASCII chars + 🎉 → straddles 30-char outbound sendText sent-part boundary.
64+
const textOutbound30 = "a".repeat(29) + "🎉";
65+
66+
const logs: string[] = [
67+
emitStreamingC2cLog(textStreamingC2c),
68+
emitOutboundSendTextCtxLog(textOutbound50),
69+
emitOutboundSendTextSentPartLog(textOutbound30),
70+
emitOutboundDeliverSendTextChunkLog(textOutbound50, textOutbound50 + " more text"),
71+
emitOutboundDeliverSendTextOnlyChunkLog(textOutbound50, textOutbound50 + " more text"),
72+
emitReplyDispatcherTtsLog(textOutbound50),
73+
];
74+
75+
for (const logLine of logs) {
76+
console.log(`[layer2 runtime log proof] ${logLine}`);
77+
}
78+
79+
// Each log line: no lone surrogate, no replacement char.
80+
for (const logLine of logs) {
81+
expect(logLine).not.toMatch(/\uD83C(?![\uDF00-\uDFFF])/);
82+
expect(logLine).not.toContain("�");
83+
}
84+
85+
// Specific assertions per site:
86+
expect(logs[0]).toContain("streaming-c2c onDeliver: " + "a".repeat(59)); // 60-cap drops emoji
87+
expect(logs[1]).toContain('"text":"' + "a".repeat(49) + '"'); // 50-cap drops emoji
88+
expect(logs[2]).toContain("Sent text part: " + "a".repeat(29) + "..."); // 30-cap drops emoji
89+
expect(logs[3]).toContain(
90+
"Sent text chunk (" +
91+
textOutbound50.length +
92+
"/" +
93+
(textOutbound50.length + " more text".length) +
94+
" chars): " +
95+
"a".repeat(49) +
96+
"...",
97+
);
98+
expect(logs[4]).toContain(
99+
"Sent text-only chunk (" +
100+
textOutbound50.length +
101+
"/" +
102+
(textOutbound50.length + " more text".length) +
103+
" chars): " +
104+
"a".repeat(49) +
105+
"...",
106+
);
107+
expect(logs[5]).toContain('TTS: "' + "a".repeat(49) + '..."');
108+
});
109+
110+
it("treats undefined text as undefined in outbound sendText ctx (no spurious preview)", () => {
111+
const logLine = emitOutboundSendTextCtxLog(undefined);
112+
console.log(`[layer2 runtime log proof] ${logLine}`);
113+
// JSON.stringify drops undefined values from objects → "{}"
114+
expect(logLine).toBe("outbound sendText ctx: {}");
115+
expect(logLine).not.toContain("text");
116+
expect(logLine).not.toContain("undefined");
117+
});
118+
119+
it("passes plain ASCII under each cap through unchanged", () => {
120+
const short = "Hello world!";
121+
const logs = [
122+
emitStreamingC2cLog(short),
123+
emitOutboundSendTextCtxLog(short),
124+
emitOutboundSendTextSentPartLog(short),
125+
emitOutboundDeliverSendTextChunkLog(short, short + " more"),
126+
emitOutboundDeliverSendTextOnlyChunkLog(short, short + " more"),
127+
emitReplyDispatcherTtsLog(short),
128+
];
129+
for (const logLine of logs) {
130+
console.log(`[layer2 runtime log proof] ${logLine}`);
131+
expect(logLine).toContain("Hello world!");
132+
}
133+
});
134+
});

extensions/qqbot/src/engine/messaging/streaming-c2c.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
* it is treated as a new message.
1616
*/
1717

18+
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
1819
import { getNextMsgSeq } from "../api/routes.js";
1920
import type { GatewayAccount } from "../types.js";
2021
import {
@@ -542,7 +543,7 @@ export class StreamingController {
542543
*/
543544
async onDeliver(payload: { text?: string }): Promise<void> {
544545
const rawLen = payload.text?.length ?? 0;
545-
const preview = (payload.text ?? "").slice(0, 60).replace(/\n/g, "\\n");
546+
const preview = truncateUtf16Safe(payload.text ?? "", 60).replace(/\n/g, "\\n");
546547
this.logDebug(
547548
`onDeliver: rawLen=${rawLen}, phase=${this.phase}, streamMsgId=${this.streamMsgId}, sentIndex=${this.sentIndex}, sentChunks=${this.sentStreamChunkCount}, firstCB=${this.firstCallbackSource}, preview="${preview}"`,
548549
);

0 commit comments

Comments
 (0)