mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-10 17:46:08 +00:00
fix(qqbot): use UTF-16-safe truncation for messaging reply and approval previews (#101421)
* fix(qqbot): use UTF-16-safe truncation for messaging reply and approval previews * test(qqbot): focus UTF-16 approval regression --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
committed by
GitHub
parent
08abf83a66
commit
048fe0850e
@@ -1,6 +1,6 @@
|
||||
// Qqbot tests cover index plugin behavior.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildApprovalKeyboard } from "./index.js";
|
||||
import { buildApprovalKeyboard, buildExecApprovalText } from "./index.js";
|
||||
|
||||
describe("buildApprovalKeyboard", () => {
|
||||
it("omits allow-always when the decision is unavailable", () => {
|
||||
@@ -21,3 +21,18 @@ describe("buildApprovalKeyboard", () => {
|
||||
expect(buttons.map((button) => button.id)).toEqual(["allow", "always", "deny"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildExecApprovalText", () => {
|
||||
it("truncates the command preview on a UTF-16 boundary without splitting surrogate pairs", () => {
|
||||
const safePrefix = "x".repeat(299);
|
||||
const text = buildExecApprovalText({
|
||||
id: "approval-1",
|
||||
expiresAtMs: Date.now() + 60_000,
|
||||
request: {
|
||||
commandPreview: `${safePrefix}🎉 trailing text`,
|
||||
},
|
||||
});
|
||||
const codeFence = text.split("```\n")[1]?.split("\n```")[0] ?? "";
|
||||
expect(codeFence).toBe(safePrefix);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
* - Parse INTERACTION_CREATE button data
|
||||
*/
|
||||
|
||||
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
||||
import type { ChatScope, InlineKeyboard, KeyboardButton } from "../types.js";
|
||||
|
||||
// ============ Types ============
|
||||
@@ -61,7 +62,7 @@ export function buildExecApprovalText(request: ExecApprovalRequest): string {
|
||||
const lines: string[] = ["\u{1f510} \u547d\u4ee4\u6267\u884c\u5ba1\u6279", ""];
|
||||
const cmd = request.request.commandPreview ?? request.request.command ?? "";
|
||||
if (cmd) {
|
||||
lines.push(`\`\`\`\n${cmd.slice(0, 300)}\n\`\`\``);
|
||||
lines.push(`\`\`\`\n${truncateUtf16Safe(cmd, 300)}\n\`\`\``);
|
||||
}
|
||||
if (request.request.cwd) {
|
||||
lines.push(`\u{1f4c1} \u76ee\u5f55: ${request.request.cwd}`);
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import crypto from "node:crypto";
|
||||
import path from "node:path";
|
||||
import { resolveLocalPathFromRootsSync } from "openclaw/plugin-sdk/security-runtime";
|
||||
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
||||
import { MediaFileType, type GatewayAccount } from "../types.js";
|
||||
import { formatFileSize, getImageMimeType, getMaxUploadSize } from "../utils/file-utils.js";
|
||||
import { formatErrorMessage } from "../utils/format.js";
|
||||
@@ -281,10 +282,7 @@ function resolveStructuredPayloadPath(
|
||||
}
|
||||
|
||||
function sanitizeForLog(value: string, maxLen = 200): string {
|
||||
return value
|
||||
.replace(/[\r\n\t]/g, " ")
|
||||
.replaceAll("\0", " ")
|
||||
.slice(0, maxLen);
|
||||
return truncateUtf16Safe(value.replace(/[\r\n\t]/g, " ").replaceAll("\0", " "), maxLen);
|
||||
}
|
||||
|
||||
function describeMediaTargetForLog(pathValue: string, isHttpUrl: boolean): string {
|
||||
@@ -513,7 +511,7 @@ export async function sendTextAsVoiceReply(
|
||||
return false;
|
||||
}
|
||||
|
||||
log?.debug?.(`TTS: "${ttsText.slice(0, 50)}..."`);
|
||||
log?.debug?.(`TTS: "${truncateUtf16Safe(ttsText, 50)}..."`);
|
||||
const ttsResult = await deps.tts.textToSpeech({
|
||||
text: ttsText,
|
||||
cfg,
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
*/
|
||||
|
||||
import os from "node:os";
|
||||
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
||||
import { ApiClient } from "../api/api-client.js";
|
||||
import { ChunkedMediaApi as ChunkedMediaApiClass } from "../api/media-chunked.js";
|
||||
import { downloadDirectUploadUrl, MediaApi as MediaApiClass } from "../api/media.js";
|
||||
@@ -349,7 +350,7 @@ export async function withTokenRetry<T>(
|
||||
if (looksLike401) {
|
||||
log?.warn?.(
|
||||
`Token retry triggered by string heuristic (err is not ApiError). ` +
|
||||
`Consider propagating ApiError end-to-end. msg=${errMsg.slice(0, 120)}`,
|
||||
`Consider propagating ApiError end-to-end. msg=${truncateUtf16Safe(errMsg, 120)}`,
|
||||
);
|
||||
clearTokenCache(creds.appId);
|
||||
const newToken = await getAccessToken(creds.appId, creds.clientSecret);
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
* it is treated as a new message.
|
||||
*/
|
||||
|
||||
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
||||
import { getNextMsgSeq } from "../api/routes.js";
|
||||
import type { GatewayAccount } from "../types.js";
|
||||
import {
|
||||
@@ -543,7 +544,7 @@ export class StreamingController {
|
||||
*/
|
||||
async onDeliver(payload: { text?: string }): Promise<void> {
|
||||
const rawLen = payload.text?.length ?? 0;
|
||||
const preview = (payload.text ?? "").slice(0, 60).replace(/\n/g, "\\n");
|
||||
const preview = truncateUtf16Safe(payload.text ?? "", 60).replace(/\n/g, "\\n");
|
||||
this.logDebug(
|
||||
`onDeliver: rawLen=${rawLen}, phase=${this.phase}, streamMsgId=${this.streamMsgId}, sentIndex=${this.sentIndex}, sentChunks=${this.sentStreamChunkCount}, firstCB=${this.firstCallbackSource}, preview="${preview}"`,
|
||||
);
|
||||
@@ -790,7 +791,7 @@ export class StreamingController {
|
||||
}
|
||||
|
||||
this.logInfo(
|
||||
`processMediaTags: found <${found.tagName}> at offset ${this.sentIndex}, textBefore="${found.textBefore.slice(0, 40)}"`,
|
||||
`processMediaTags: found <${found.tagName}> at offset ${this.sentIndex}, textBefore="${truncateUtf16Safe(found.textBefore, 40)}"`,
|
||||
);
|
||||
|
||||
// ---- 1.1 终结当前流式会话(如果有的话) ----
|
||||
@@ -813,7 +814,7 @@ export class StreamingController {
|
||||
if (found.mediaPath && this.deps.mediaContext) {
|
||||
const item: SendQueueItem = { type: found.itemType, content: found.mediaPath };
|
||||
this.logDebug(
|
||||
`processMediaTags: sending ${found.itemType}: ${found.mediaPath.slice(0, 80)}`,
|
||||
`processMediaTags: sending ${found.itemType}: ${truncateUtf16Safe(found.mediaPath, 80)}`,
|
||||
);
|
||||
await sendMediaQueue([item], this.deps.mediaContext);
|
||||
this.sentMediaCount++;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
* 拆分、路径编码修复,以及统一的发送队列执行器。
|
||||
*/
|
||||
|
||||
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
||||
import type { GatewayAccount } from "../types.js";
|
||||
import { normalizePath } from "../utils/platform.js";
|
||||
import type { OutboundMediaAccessContext } from "./outbound-types.js";
|
||||
@@ -295,7 +296,7 @@ export async function executeSendQueue(
|
||||
}
|
||||
|
||||
log?.info(
|
||||
`${prefix} executeSendQueue: sending ${item.type}: ${item.content.slice(0, 80)}...`,
|
||||
`${prefix} executeSendQueue: sending ${item.type}: ${truncateUtf16Safe(item.content, 80)}...`,
|
||||
);
|
||||
|
||||
if (item.type === "image") {
|
||||
|
||||
Reference in New Issue
Block a user