mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-09 15:59:30 +00:00
fix(text): keep reachable truncation boundaries UTF-16 safe
Some checks failed
CI / runner-admission (push) Waiting to run
CI / preflight (push) Blocked by required conditions
CI / security-fast (push) Blocked by required conditions
CI / pnpm-store-warmup (push) Blocked by required conditions
CI / build-artifacts (push) Blocked by required conditions
CI / native-i18n (push) Blocked by required conditions
CI / (push) Blocked by required conditions
CI / QA Smoke CI (matrix) (push) Blocked by required conditions
CI / QA Smoke CI (telegram 1/2) (push) Blocked by required conditions
CI / QA Smoke CI (telegram 2/2) (push) Blocked by required conditions
CI / QA Smoke CI (push) Blocked by required conditions
CI / -1 (push) Blocked by required conditions
CI / -2 (push) Blocked by required conditions
CI / checks-node-compat-node22 (push) Blocked by required conditions
CI / -3 (push) Blocked by required conditions
CI / check-bundled-channel-config-metadata (push) Blocked by required conditions
CI / check-dependencies (push) Blocked by required conditions
CI / check-guards (push) Blocked by required conditions
CI / check-lint (push) Blocked by required conditions
CI / check-prod-types (push) Blocked by required conditions
CI / check-shrinkwrap (push) Blocked by required conditions
CI / check-test-types (push) Blocked by required conditions
CI / check-additional-boundaries-a (push) Blocked by required conditions
CI / check-additional-boundaries-bcd (push) Blocked by required conditions
CI / check-additional-extension-package-boundary (push) Blocked by required conditions
CI / check-additional-runtime-topology-architecture (push) Blocked by required conditions
CI / check-session-accessor-boundary (push) Blocked by required conditions
CI / check-session-transcript-reader-boundary (push) Blocked by required conditions
CI / check-docs (push) Blocked by required conditions
CI / skills-python (push) Blocked by required conditions
CI / -4 (push) Blocked by required conditions
CI / -5 (push) Blocked by required conditions
CI / macos-swift (push) Blocked by required conditions
CI / ios-build (push) Blocked by required conditions
CI / -6 (push) Blocked by required conditions
CI / ci-timings-summary (push) Blocked by required conditions
ClawSweeper Dispatch / dispatch (push) Waiting to run
CodeQL / Security High (actions) (push) Waiting to run
CodeQL / Security High (channel-runtime-boundary) (push) Waiting to run
CodeQL / Security High (core-auth-secrets) (push) Waiting to run
CodeQL / Security High (mcp-process-tool-boundary) (push) Waiting to run
CodeQL / Security High (network-ssrf-boundary) (push) Waiting to run
CodeQL / Security High (plugin-trust-boundary) (push) Waiting to run
CodeQL / Security High (process-exec-boundary) (push) Waiting to run
Control UI Locale Refresh / plan (push) Waiting to run
Control UI Locale Refresh / Refresh (push) Blocked by required conditions
Control UI Locale Refresh / Commit control UI locale refresh (push) Blocked by required conditions
Docs Sync Publish Repo / sync-publish-repo (push) Waiting to run
Docs / docs (push) Waiting to run
Native App Locale Refresh / Refresh native ar (push) Waiting to run
Native App Locale Refresh / Refresh native de (push) Waiting to run
Native App Locale Refresh / Refresh native es (push) Waiting to run
CI / check-additional-extension-bundled (push) Blocked by required conditions
CI / check-additional-extension-channels (push) Blocked by required conditions
Native App Locale Refresh / Refresh native fa (push) Waiting to run
Native App Locale Refresh / Refresh native fr (push) Waiting to run
Native App Locale Refresh / Refresh native hi (push) Waiting to run
Native App Locale Refresh / Refresh native id (push) Waiting to run
Native App Locale Refresh / Refresh native it (push) Waiting to run
Native App Locale Refresh / Refresh native ja-JP (push) Waiting to run
Native App Locale Refresh / Refresh native ko (push) Waiting to run
Native App Locale Refresh / Refresh native nl (push) Waiting to run
Native App Locale Refresh / Refresh native pl (push) Waiting to run
Native App Locale Refresh / Refresh native pt-BR (push) Waiting to run
Native App Locale Refresh / Refresh native ru (push) Waiting to run
Native App Locale Refresh / Refresh native sv (push) Waiting to run
Native App Locale Refresh / Refresh native th (push) Waiting to run
Native App Locale Refresh / Refresh native tr (push) Waiting to run
Native App Locale Refresh / Refresh native uk (push) Waiting to run
Native App Locale Refresh / Refresh native vi (push) Waiting to run
Native App Locale Refresh / Refresh native zh-CN (push) Waiting to run
Native App Locale Refresh / Refresh native zh-TW (push) Waiting to run
Native App Locale Refresh / Commit native locale refresh (push) Blocked by required conditions
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Waiting to run
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Blocked by required conditions
Plugin Init Scaffold Validation / Validate provider scaffold (push) Waiting to run
Plugin NPM Release / preview_plugins_npm (push) Waiting to run
Plugin NPM Release / Validate release publish approval (push) Blocked by required conditions
Plugin NPM Release / preview_plugin_pack (push) Blocked by required conditions
Plugin NPM Release / publish_plugins_npm (push) Blocked by required conditions
Plugin NPM Release / verify_plugins_npm (push) Blocked by required conditions
Workflow Sanity / no-tabs (push) Waiting to run
Workflow Sanity / actionlint (push) Waiting to run
Workflow Sanity / generated-doc-baselines (push) Waiting to run
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Some checks failed
CI / runner-admission (push) Waiting to run
CI / preflight (push) Blocked by required conditions
CI / security-fast (push) Blocked by required conditions
CI / pnpm-store-warmup (push) Blocked by required conditions
CI / build-artifacts (push) Blocked by required conditions
CI / native-i18n (push) Blocked by required conditions
CI / (push) Blocked by required conditions
CI / QA Smoke CI (matrix) (push) Blocked by required conditions
CI / QA Smoke CI (telegram 1/2) (push) Blocked by required conditions
CI / QA Smoke CI (telegram 2/2) (push) Blocked by required conditions
CI / QA Smoke CI (push) Blocked by required conditions
CI / -1 (push) Blocked by required conditions
CI / -2 (push) Blocked by required conditions
CI / checks-node-compat-node22 (push) Blocked by required conditions
CI / -3 (push) Blocked by required conditions
CI / check-bundled-channel-config-metadata (push) Blocked by required conditions
CI / check-dependencies (push) Blocked by required conditions
CI / check-guards (push) Blocked by required conditions
CI / check-lint (push) Blocked by required conditions
CI / check-prod-types (push) Blocked by required conditions
CI / check-shrinkwrap (push) Blocked by required conditions
CI / check-test-types (push) Blocked by required conditions
CI / check-additional-boundaries-a (push) Blocked by required conditions
CI / check-additional-boundaries-bcd (push) Blocked by required conditions
CI / check-additional-extension-package-boundary (push) Blocked by required conditions
CI / check-additional-runtime-topology-architecture (push) Blocked by required conditions
CI / check-session-accessor-boundary (push) Blocked by required conditions
CI / check-session-transcript-reader-boundary (push) Blocked by required conditions
CI / check-docs (push) Blocked by required conditions
CI / skills-python (push) Blocked by required conditions
CI / -4 (push) Blocked by required conditions
CI / -5 (push) Blocked by required conditions
CI / macos-swift (push) Blocked by required conditions
CI / ios-build (push) Blocked by required conditions
CI / -6 (push) Blocked by required conditions
CI / ci-timings-summary (push) Blocked by required conditions
ClawSweeper Dispatch / dispatch (push) Waiting to run
CodeQL / Security High (actions) (push) Waiting to run
CodeQL / Security High (channel-runtime-boundary) (push) Waiting to run
CodeQL / Security High (core-auth-secrets) (push) Waiting to run
CodeQL / Security High (mcp-process-tool-boundary) (push) Waiting to run
CodeQL / Security High (network-ssrf-boundary) (push) Waiting to run
CodeQL / Security High (plugin-trust-boundary) (push) Waiting to run
CodeQL / Security High (process-exec-boundary) (push) Waiting to run
Control UI Locale Refresh / plan (push) Waiting to run
Control UI Locale Refresh / Refresh (push) Blocked by required conditions
Control UI Locale Refresh / Commit control UI locale refresh (push) Blocked by required conditions
Docs Sync Publish Repo / sync-publish-repo (push) Waiting to run
Docs / docs (push) Waiting to run
Native App Locale Refresh / Refresh native ar (push) Waiting to run
Native App Locale Refresh / Refresh native de (push) Waiting to run
Native App Locale Refresh / Refresh native es (push) Waiting to run
CI / check-additional-extension-bundled (push) Blocked by required conditions
CI / check-additional-extension-channels (push) Blocked by required conditions
Native App Locale Refresh / Refresh native fa (push) Waiting to run
Native App Locale Refresh / Refresh native fr (push) Waiting to run
Native App Locale Refresh / Refresh native hi (push) Waiting to run
Native App Locale Refresh / Refresh native id (push) Waiting to run
Native App Locale Refresh / Refresh native it (push) Waiting to run
Native App Locale Refresh / Refresh native ja-JP (push) Waiting to run
Native App Locale Refresh / Refresh native ko (push) Waiting to run
Native App Locale Refresh / Refresh native nl (push) Waiting to run
Native App Locale Refresh / Refresh native pl (push) Waiting to run
Native App Locale Refresh / Refresh native pt-BR (push) Waiting to run
Native App Locale Refresh / Refresh native ru (push) Waiting to run
Native App Locale Refresh / Refresh native sv (push) Waiting to run
Native App Locale Refresh / Refresh native th (push) Waiting to run
Native App Locale Refresh / Refresh native tr (push) Waiting to run
Native App Locale Refresh / Refresh native uk (push) Waiting to run
Native App Locale Refresh / Refresh native vi (push) Waiting to run
Native App Locale Refresh / Refresh native zh-CN (push) Waiting to run
Native App Locale Refresh / Refresh native zh-TW (push) Waiting to run
Native App Locale Refresh / Commit native locale refresh (push) Blocked by required conditions
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Waiting to run
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Blocked by required conditions
Plugin Init Scaffold Validation / Validate provider scaffold (push) Waiting to run
Plugin NPM Release / preview_plugins_npm (push) Waiting to run
Plugin NPM Release / Validate release publish approval (push) Blocked by required conditions
Plugin NPM Release / preview_plugin_pack (push) Blocked by required conditions
Plugin NPM Release / publish_plugins_npm (push) Blocked by required conditions
Plugin NPM Release / verify_plugins_npm (push) Blocked by required conditions
Workflow Sanity / no-tabs (push) Waiting to run
Workflow Sanity / actionlint (push) Waiting to run
Workflow Sanity / generated-doc-baselines (push) Waiting to run
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Consolidates #102007, #101818, and #101782. Co-authored-by: 杨浩宇0668001029 <yang.haoyu@xydigit.com> Co-authored-by: NIO <nocodet@mail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
fc4626c122
commit
7e0324263b
8 changed files with 88 additions and 9 deletions
|
|
@ -244,4 +244,11 @@ describe("formatSlackBotStarterThreadLabel", () => {
|
|||
}),
|
||||
).toBe("Slack thread DM (assistant root): Line one Line two");
|
||||
});
|
||||
|
||||
it("drops a surrogate-pair emoji whole when it straddles the snippet limit", () => {
|
||||
const starterText = `${"a".repeat(79)}🐱tail`;
|
||||
expect(formatSlackBotStarterThreadLabel({ roomLabel: "DM", starterText })).toBe(
|
||||
`Slack thread DM (assistant root): ${"a".repeat(79)}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
// Slack plugin module implements prepare thread context root behavior.
|
||||
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
||||
|
||||
type SlackBotAuthorIdentity = {
|
||||
botUserId?: string;
|
||||
botId?: string;
|
||||
|
|
@ -107,9 +109,13 @@ export function formatSlackBotStarterThreadLabel(params: {
|
|||
if (!params.starterText) {
|
||||
return base;
|
||||
}
|
||||
const snippet = params.starterText.replace(/\s+/g, " ").slice(0, 80).trim();
|
||||
const snippet = formatSlackThreadLabelSnippet(params.starterText).trim();
|
||||
if (!snippet) {
|
||||
return base;
|
||||
}
|
||||
return `${base} (assistant root): ${snippet}`;
|
||||
}
|
||||
|
||||
export function formatSlackThreadLabelSnippet(text: string): string {
|
||||
return truncateUtf16Safe(text.replace(/\s+/g, " "), 80);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -225,6 +225,22 @@ describe("resolveSlackThreadContextData", () => {
|
|||
expect(result.threadHistoryBody).not.toContain("blocked follow-up");
|
||||
});
|
||||
|
||||
it("keeps a user-started thread label UTF-16 safe at the snippet limit", async () => {
|
||||
const starterText = `${"a".repeat(79)}🐱tail`;
|
||||
const { result } = await resolveAllowlistedThreadContext({
|
||||
repliesMessages: [],
|
||||
threadStarter: {
|
||||
text: starterText,
|
||||
userId: "U1",
|
||||
ts: "100.000",
|
||||
},
|
||||
allowFromLower: ["u1"],
|
||||
allowNameMatching: false,
|
||||
});
|
||||
|
||||
expect(result.threadLabel).toBe(`Slack thread #general: ${"a".repeat(79)}`);
|
||||
});
|
||||
|
||||
it("includes bot-authored starter as assistant root context for a new thread session (default)", async () => {
|
||||
const { result } = await resolveAllowlistedThreadContext({
|
||||
repliesMessages: [
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import {
|
|||
applySlackThreadHistoryFilterPolicy,
|
||||
ensureSlackThreadHistoryHasBotRoot,
|
||||
formatSlackBotStarterThreadLabel,
|
||||
formatSlackThreadLabelSnippet,
|
||||
isSlackThreadAuthorCurrentBot,
|
||||
resolveSlackThreadHistoryFilterPolicy,
|
||||
shouldIncludeBotThreadStarterContext,
|
||||
|
|
@ -224,7 +225,7 @@ export async function resolveSlackThreadContextData(params: {
|
|||
|
||||
if (starter?.text && includeStarterContext) {
|
||||
threadStarterBody = starter.text;
|
||||
const snippet = starter.text.replace(/\s+/g, " ").slice(0, 80);
|
||||
const snippet = formatSlackThreadLabelSnippet(starter.text);
|
||||
threadLabel = `Slack thread ${params.roomLabel}${snippet ? `: ${snippet}` : ""}`;
|
||||
// Root media seeds a new thread session once. Rehydrating it later makes
|
||||
// old files look like current-turn uploads and repeats media processing.
|
||||
|
|
|
|||
|
|
@ -119,6 +119,29 @@ describe("zalo send", () => {
|
|||
expect(sendPhotoMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps outbound text and photo captions UTF-16 safe at the 2000-char limit", async () => {
|
||||
sendMessageMock.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
result: { message_id: "z-msg-surrogate" },
|
||||
});
|
||||
sendPhotoMock.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
result: { message_id: "z-photo-surrogate" },
|
||||
});
|
||||
const boundaryText = `${"a".repeat(1999)}🐱`;
|
||||
|
||||
await sendMessageZalo("dm-chat-surrogate-text", boundaryText, {
|
||||
token: "zalo-token",
|
||||
});
|
||||
await sendPhotoZalo("dm-chat-surrogate-caption", "https://example.com/photo.jpg", {
|
||||
token: "zalo-token",
|
||||
caption: boundaryText,
|
||||
});
|
||||
|
||||
expect(sendMessageMock.mock.calls[0]?.[1]?.text).toBe("a".repeat(1999));
|
||||
expect(sendPhotoMock.mock.calls[0]?.[1]?.caption).toBe("a".repeat(1999));
|
||||
});
|
||||
|
||||
it("sends cfg-backed media directly without hosted-media rewrites", async () => {
|
||||
sendPhotoMock.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
} from "openclaw/plugin-sdk/channel-outbound";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
||||
import { resolveZaloAccount } from "./accounts.js";
|
||||
import type { ZaloFetch } from "./api.js";
|
||||
import { sendMessage, sendPhoto } from "./api.js";
|
||||
|
|
@ -167,7 +168,7 @@ export async function sendMessageZalo(
|
|||
context.token,
|
||||
{
|
||||
chat_id: context.chatId,
|
||||
text: text.slice(0, 2000),
|
||||
text: truncateUtf16Safe(text, 2000),
|
||||
},
|
||||
context.fetcher,
|
||||
),
|
||||
|
|
@ -200,7 +201,8 @@ export async function sendPhotoZalo(
|
|||
{
|
||||
chat_id: context.chatId,
|
||||
photo: photoUrl.trim(),
|
||||
caption: options.caption?.slice(0, 2000),
|
||||
caption:
|
||||
options.caption !== undefined ? truncateUtf16Safe(options.caption, 2000) : undefined,
|
||||
},
|
||||
context.fetcher,
|
||||
))(),
|
||||
|
|
|
|||
|
|
@ -121,6 +121,25 @@ describe("execCommand", () => {
|
|||
expect(result.stdoutTruncatedChars).toBe(3);
|
||||
});
|
||||
|
||||
it("keeps caller-capped retained output UTF-16 safe", async () => {
|
||||
const child = createStubChild();
|
||||
const wait = createDeferred<number | null>();
|
||||
spawnMock.mockReturnValue(child);
|
||||
waitForChildProcessMock.mockReturnValue(wait.promise);
|
||||
const { execCommand } = await import("./exec.js");
|
||||
|
||||
const resultPromise = execCommand("cmd", [], "/tmp", { maxOutputChars: 2 });
|
||||
child.stdout.emit("data", Buffer.from("A😀B"));
|
||||
child.stderr.emit("data", Buffer.from("C😀D"));
|
||||
wait.resolve(0);
|
||||
|
||||
const result = await resultPromise;
|
||||
expect(result.stdout).toBe("B");
|
||||
expect(result.stderr).toBe("D");
|
||||
expect(result.stdoutTruncatedChars).toBe(3);
|
||||
expect(result.stderrTruncatedChars).toBe(3);
|
||||
});
|
||||
|
||||
it("fails instead of silently truncating default exec output", async () => {
|
||||
const child = createStubChild();
|
||||
const wait = createDeferred<number | null>();
|
||||
|
|
@ -129,7 +148,7 @@ describe("execCommand", () => {
|
|||
const { execCommand } = await import("./exec.js");
|
||||
|
||||
const resultPromise = execCommand("cmd", [], "/tmp");
|
||||
child.stdout.emit("data", Buffer.from("x".repeat(16 * 1024 * 1024 + 1)));
|
||||
child.stdout.emit("data", Buffer.from(`${"x".repeat(16 * 1024 * 1024 - 1)}😀`));
|
||||
wait.resolve(0);
|
||||
|
||||
const result = await resultPromise;
|
||||
|
|
@ -141,8 +160,9 @@ describe("execCommand", () => {
|
|||
expect(result.code).toBe(1);
|
||||
expect(result.killed).toBe(true);
|
||||
expect(result.outputLimitExceeded).toBe("stdout");
|
||||
expect(result.stdout.length).toBe(16 * 1024 * 1024);
|
||||
expect(result.stdoutTruncatedChars).toBe(1);
|
||||
expect(result.stdout.length).toBe(16 * 1024 * 1024 - 1);
|
||||
expect(result.stdout.endsWith("x")).toBe(true);
|
||||
expect(result.stdoutTruncatedChars).toBe(2);
|
||||
expect(result.stderr).toContain("exec stdout exceeded output limit");
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
*/
|
||||
|
||||
import { spawn } from "node:child_process";
|
||||
import { sliceUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
|
||||
import { killProcessTree } from "../../process/kill-tree.js";
|
||||
import { waitForChildProcess } from "../utils/child-process.js";
|
||||
|
||||
|
|
@ -63,9 +64,12 @@ function appendCapturedOutput(
|
|||
truncatedChars: current.truncatedChars,
|
||||
};
|
||||
}
|
||||
const nextText = truncateTail
|
||||
? sliceUtf16Safe(combined, overflowChars)
|
||||
: sliceUtf16Safe(combined, 0, maxOutputChars);
|
||||
return {
|
||||
text: truncateTail ? combined.slice(overflowChars) : combined.slice(0, maxOutputChars),
|
||||
truncatedChars: current.truncatedChars + overflowChars,
|
||||
text: nextText,
|
||||
truncatedChars: current.truncatedChars + combined.length - nextText.length,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue