mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-28 15:01:14 +00:00
fix(export): prevent broken emoji in HTML tool call previews (#104054)
This commit is contained in:
@@ -640,6 +640,25 @@
|
||||
return p;
|
||||
}
|
||||
|
||||
function truncateUtf16Safe(s, maxLen) {
|
||||
const limit = Math.max(0, Math.floor(maxLen));
|
||||
if (s.length <= limit) {
|
||||
return s;
|
||||
}
|
||||
|
||||
let end = limit;
|
||||
if (end > 0) {
|
||||
const lastCodeUnit = s.charCodeAt(end - 1);
|
||||
const nextCodeUnit = s.charCodeAt(end);
|
||||
const endsWithHighSurrogate = lastCodeUnit >= 0xd800 && lastCodeUnit <= 0xdbff;
|
||||
const continuesWithLowSurrogate = nextCodeUnit >= 0xdc00 && nextCodeUnit <= 0xdfff;
|
||||
if (endsWithHighSurrogate && continuesWithLowSurrogate) {
|
||||
end -= 1;
|
||||
}
|
||||
}
|
||||
return s.slice(0, end);
|
||||
}
|
||||
|
||||
function formatToolCall(name, args) {
|
||||
switch (name) {
|
||||
case "read": {
|
||||
@@ -660,11 +679,8 @@
|
||||
return `[edit: ${shortenPath(String(args.path || args.file_path || ""))}]`;
|
||||
case "bash": {
|
||||
const rawCmd = String(args.command || "");
|
||||
const cmd = rawCmd
|
||||
.replace(/[\n\t]/g, " ")
|
||||
.trim()
|
||||
.slice(0, 50);
|
||||
return `[bash: ${cmd}${rawCmd.length > 50 ? "..." : ""}]`;
|
||||
const cmd = rawCmd.replace(/[\n\t]/g, " ").trim();
|
||||
return `[bash: ${truncateUtf16Safe(cmd, 50)}${rawCmd.length > 50 ? "..." : ""}]`;
|
||||
}
|
||||
case "grep":
|
||||
return `[grep: /${args.pattern || ""}/ in ${shortenPath(String(args.path || "."))}]`;
|
||||
@@ -673,8 +689,9 @@
|
||||
case "ls":
|
||||
return `[ls: ${shortenPath(String(args.path || "."))}]`;
|
||||
default: {
|
||||
const argsStr = JSON.stringify(args).slice(0, 40);
|
||||
return `[${name}: ${argsStr}${JSON.stringify(args).length > 40 ? "..." : ""}]`;
|
||||
const argsJson = JSON.stringify(args);
|
||||
const argsStr = truncateUtf16Safe(argsJson, 40);
|
||||
return `[${name}: ${argsStr}${argsJson.length > 40 ? "..." : ""}]`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -726,19 +743,7 @@
|
||||
if (s.length <= maxLen) {
|
||||
return s;
|
||||
}
|
||||
let endOffset = maxLen;
|
||||
const beforeBoundary = s.charCodeAt(endOffset - 1);
|
||||
const afterBoundary = s.charCodeAt(endOffset);
|
||||
// Keep the existing UTF-16 unit ceiling, but retreat if it splits a surrogate pair.
|
||||
if (
|
||||
beforeBoundary >= 0xd800 &&
|
||||
beforeBoundary <= 0xdbff &&
|
||||
afterBoundary >= 0xdc00 &&
|
||||
afterBoundary <= 0xdfff
|
||||
) {
|
||||
endOffset -= 1;
|
||||
}
|
||||
return s.slice(0, endOffset) + "...";
|
||||
return truncateUtf16Safe(s, maxLen) + "...";
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -792,3 +792,95 @@ describe("export html security hardening", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("export html tool call previews", () => {
|
||||
it("truncates tool previews without splitting emoji", async () => {
|
||||
const bashPrefix = "a".repeat(49);
|
||||
const genericPrefix = "b".repeat(29);
|
||||
const bashExecutionPrefix = "c".repeat(99);
|
||||
const session: SessionData = {
|
||||
header: { id: "session-tool-preview-emoji", timestamp: now() },
|
||||
entries: [
|
||||
{
|
||||
id: "1",
|
||||
parentId: null,
|
||||
timestamp: now(),
|
||||
type: "message",
|
||||
message: { role: "user", content: "run tools" },
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
parentId: "1",
|
||||
timestamp: now(),
|
||||
type: "message",
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "toolCall",
|
||||
id: "call-bash",
|
||||
name: "bash",
|
||||
arguments: { command: `${bashPrefix}😀tail` },
|
||||
},
|
||||
{
|
||||
type: "toolCall",
|
||||
id: "call-custom",
|
||||
name: "custom",
|
||||
arguments: { value: `${genericPrefix}😀tail` },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
parentId: "2",
|
||||
timestamp: now(),
|
||||
type: "message",
|
||||
message: {
|
||||
role: "toolResult",
|
||||
toolCallId: "call-bash",
|
||||
content: "bash output",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
parentId: "3",
|
||||
timestamp: now(),
|
||||
type: "message",
|
||||
message: {
|
||||
role: "toolResult",
|
||||
toolCallId: "call-custom",
|
||||
content: "custom output",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
parentId: "4",
|
||||
timestamp: now(),
|
||||
type: "message",
|
||||
message: {
|
||||
role: "bashExecution",
|
||||
command: `${bashExecutionPrefix}😀tail`,
|
||||
},
|
||||
},
|
||||
],
|
||||
leafId: "5",
|
||||
systemPrompt: "",
|
||||
tools: [],
|
||||
};
|
||||
|
||||
const { document } = await renderTemplate(session);
|
||||
const previews = ["3", "4", "5"].map((id) =>
|
||||
requireElement(
|
||||
document.querySelector(`.tree-node[data-id="${id}"] .tree-content`),
|
||||
`tool preview ${id} missing`,
|
||||
).textContent,
|
||||
);
|
||||
|
||||
expect(previews).toEqual([
|
||||
`[bash: ${bashPrefix}...]`,
|
||||
`[custom: {"value":"${genericPrefix}...]`,
|
||||
`[bash]: ${bashExecutionPrefix}...`,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user