From 1beea4b87357e6b11786a67cc1c7c83559da0bdb Mon Sep 17 00:00:00 2001 From: lsr911 Date: Thu, 9 Jul 2026 18:20:38 +0800 Subject: [PATCH] fix(qqbot): use truncateUtf16Safe for debug log URL/key truncation (#102572) * fix(qqbot): use truncateUtf16Safe for debug log URL/key truncation Replace naive .slice(0, N) with truncateUtf16Safe() in: - image-size.ts: URL preview in debug logs (2 sites) - upload-cache.ts: cache key preview in debug logs (2 sites) Co-Authored-By: Claude * test(qqbot): cover UTF-16-safe debug previews --------- Co-authored-by: Claude Co-authored-by: Peter Steinberger --- .../qqbot/src/engine/utils/image-size.test.ts | 27 +++++++++++++++++++ .../qqbot/src/engine/utils/image-size.ts | 7 +++-- .../src/engine/utils/upload-cache.test.ts | 27 +++++++++++++++++++ .../qqbot/src/engine/utils/upload-cache.ts | 7 +++-- 4 files changed, 64 insertions(+), 4 deletions(-) diff --git a/extensions/qqbot/src/engine/utils/image-size.test.ts b/extensions/qqbot/src/engine/utils/image-size.test.ts index 072cc7e584a0..fc191e03d286 100644 --- a/extensions/qqbot/src/engine/utils/image-size.test.ts +++ b/extensions/qqbot/src/engine/utils/image-size.test.ts @@ -4,6 +4,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; const adapterMocks = vi.hoisted(() => ({ fetchMedia: vi.fn(), + debugLog: vi.fn(), })); vi.mock("../adapter/index.js", () => ({ @@ -12,6 +13,10 @@ vi.mock("../adapter/index.js", () => ({ }), })); +vi.mock("./log.js", () => ({ + debugLog: (...args: unknown[]) => adapterMocks.debugLog(...args), +})); + import { getImageSizeFromUrl, parseImageSize } from "./image-size.js"; /** Build a minimal valid PNG header with the given dimensions. */ @@ -39,6 +44,7 @@ function buildPngHeader(width: number, height: number): Buffer { describe("getImageSizeFromUrl", () => { beforeEach(() => { adapterMocks.fetchMedia.mockReset(); + adapterMocks.debugLog.mockReset(); }); describe("fetchMedia options contract", () => { @@ -140,6 +146,27 @@ describe("getImageSizeFromUrl", () => { expect(size).toBeNull(); }); + + it("logs fetched URLs without splitting surrogate pairs", async () => { + adapterMocks.fetchMedia.mockResolvedValueOnce({ + buffer: buildPngHeader(800, 600), + contentType: "image/png", + }); + const base = "https://cdn.example.com/"; + const urlPrefix = `${base}${"x".repeat(59 - base.length)}`; + + await getImageSizeFromUrl(`${urlPrefix}😀.png`); + + expect(adapterMocks.debugLog).toHaveBeenCalledWith( + `[image-size] Got size from URL: 800x600 - ${urlPrefix}...`, + ); + + adapterMocks.fetchMedia.mockRejectedValueOnce(new Error("probe failed")); + await getImageSizeFromUrl(`${urlPrefix}😀.png`); + expect(adapterMocks.debugLog).toHaveBeenLastCalledWith( + `[image-size] Error fetching ${urlPrefix}...: probe failed`, + ); + }); }); }); diff --git a/extensions/qqbot/src/engine/utils/image-size.ts b/extensions/qqbot/src/engine/utils/image-size.ts index 76eaf559b7ee..6fd4d5e9998c 100644 --- a/extensions/qqbot/src/engine/utils/image-size.ts +++ b/extensions/qqbot/src/engine/utils/image-size.ts @@ -5,6 +5,7 @@ */ import { Buffer } from "node:buffer"; +import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime"; import { getPlatformAdapter } from "../adapter/index.js"; import type { SsrfPolicyConfig } from "../adapter/types.js"; import { formatErrorMessage } from "./format.js"; @@ -185,7 +186,7 @@ export async function getImageSizeFromUrl( const size = parseImageSize(buffer); if (size) { debugLog( - `[image-size] Got size from URL: ${size.width}x${size.height} - ${url.slice(0, 60)}...`, + `[image-size] Got size from URL: ${size.width}x${size.height} - ${truncateUtf16Safe(url, 60)}...`, ); } return size; @@ -193,7 +194,9 @@ export async function getImageSizeFromUrl( clearTimeout(timeoutId); } } catch (err) { - debugLog(`[image-size] Error fetching ${url.slice(0, 60)}...: ${formatErrorMessage(err)}`); + debugLog( + `[image-size] Error fetching ${truncateUtf16Safe(url, 60)}...: ${formatErrorMessage(err)}`, + ); return null; } } diff --git a/extensions/qqbot/src/engine/utils/upload-cache.test.ts b/extensions/qqbot/src/engine/utils/upload-cache.test.ts index 7dc951942e73..b989596869b9 100644 --- a/extensions/qqbot/src/engine/utils/upload-cache.test.ts +++ b/extensions/qqbot/src/engine/utils/upload-cache.test.ts @@ -1,11 +1,21 @@ // Qqbot tests cover upload cache plugin behavior. import { afterEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + debugLog: vi.fn(), +})); + +vi.mock("./log.js", () => ({ + debugLog: (...args: unknown[]) => mocks.debugLog(...args), +})); + import { computeFileHash, getCachedFileInfo, setCachedFileInfo } from "./upload-cache.js"; describe("qqbot upload-cache", () => { afterEach(() => { vi.useRealTimers(); vi.restoreAllMocks(); + mocks.debugLog.mockReset(); }); it("reuses cached file info before expiry", () => { @@ -32,4 +42,21 @@ describe("qqbot upload-cache", () => { expect(getCachedFileInfo(hash, "group", "target-overflow", 1)).toBeNull(); }); + + it("logs cache keys without splitting surrogate pairs", () => { + const hash = computeFileHash("qqbot-surrogate-key"); + const keyPrefix = `${hash}:group:`; + + setCachedFileInfo(hash, "group", "😀target", 1, "file-info", "uuid-safe", 3600); + expect(getCachedFileInfo(hash, "group", "😀target", 1)).toBe("file-info"); + + expect(mocks.debugLog).toHaveBeenNthCalledWith( + 1, + `[upload-cache] Cache SET: key=${keyPrefix}..., ttl=3540s, uuid=uuid-safe`, + ); + expect(mocks.debugLog).toHaveBeenNthCalledWith( + 2, + `[upload-cache] Cache HIT: key=${keyPrefix}..., fileUuid=uuid-safe`, + ); + }); }); diff --git a/extensions/qqbot/src/engine/utils/upload-cache.ts b/extensions/qqbot/src/engine/utils/upload-cache.ts index c00a326a4de7..4981beebab10 100644 --- a/extensions/qqbot/src/engine/utils/upload-cache.ts +++ b/extensions/qqbot/src/engine/utils/upload-cache.ts @@ -8,6 +8,7 @@ import { isFutureDateTimestampMs, resolveExpiresAtMsFromDurationSeconds, } from "openclaw/plugin-sdk/number-runtime"; +import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime"; import type { ChatScope } from "../types.js"; import { debugLog } from "./log.js"; @@ -55,7 +56,9 @@ export function getCachedFileInfo( return null; } - debugLog(`[upload-cache] Cache HIT: key=${key.slice(0, 40)}..., fileUuid=${entry.fileUuid}`); + debugLog( + `[upload-cache] Cache HIT: key=${truncateUtf16Safe(key, 40)}..., fileUuid=${entry.fileUuid}`, + ); return entry.fileInfo; } @@ -100,6 +103,6 @@ export function setCachedFileInfo( }); debugLog( - `[upload-cache] Cache SET: key=${key.slice(0, 40)}..., ttl=${effectiveTtl}s, uuid=${fileUuid}`, + `[upload-cache] Cache SET: key=${truncateUtf16Safe(key, 40)}..., ttl=${effectiveTtl}s, uuid=${fileUuid}`, ); }