fix(shared): use UTF-16 safe truncation in assistant error formatting… · openclaw/openclaw@e445d61
zenglingbiao
·
2026-06-29
·
via Recent Commits to openclaw:main
| Original file line number | Diff line number | Diff line change |
|---|
@@ -650,6 +650,33 @@ describe("formatRawAssistantErrorForUi", () => {
|
650 | 650 | "The provider returned an HTML error page instead of an API response. This usually means a CDN or gateway (e.g. Cloudflare) blocked the request. Retry in a moment or check provider status.", |
651 | 651 | ); |
652 | 652 | }); |
| 653 | + |
| 654 | +it("truncates fallback raw error text on UTF-16 code-point boundary without dangling surrogates", () => { |
| 655 | +// 601 UTF-16 code units: emoji (surrogate pair) straddles the 600-unit truncation boundary |
| 656 | +const prefix = "x".repeat(599); |
| 657 | +const emoji = "🎉"; // U+1F389 — high surrogate 0xD83C + low surrogate 0xDF89 |
| 658 | +const raw = prefix + emoji; // 601 code units total |
| 659 | + |
| 660 | +const result = formatRawAssistantErrorForUi(raw); |
| 661 | + |
| 662 | +// Result must end with "…" after truncation |
| 663 | +expect(result).toMatch(/…$/); |
| 664 | + |
| 665 | +// Verify the truncated portion contains no dangling surrogates |
| 666 | +for (let i = 0; i < result.length - 1; i++) { |
| 667 | +const codeUnit = result.charCodeAt(i); |
| 668 | +// High surrogate (0xD800-0xDBFF) must be followed by a low surrogate |
| 669 | +if (codeUnit >= 0xd800 && codeUnit <= 0xdbff) { |
| 670 | +const next = result.charCodeAt(i + 1); |
| 671 | +expect(next >= 0xdc00 && next <= 0xdfff).toBe(true); |
| 672 | +} |
| 673 | +// Low surrogate (0xDC00-0xDFFF) must be preceded by a high surrogate |
| 674 | +if (codeUnit >= 0xdc00 && codeUnit <= 0xdfff) { |
| 675 | +const prev = i > 0 ? result.charCodeAt(i - 1) : -1; |
| 676 | +expect(prev >= 0xd800 && prev <= 0xdbff).toBe(true); |
| 677 | +} |
| 678 | +} |
| 679 | +}); |
653 | 680 | }); |
654 | 681 | |
655 | 682 | describe("raw API error payload helpers", () => { |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -14,6 +14,7 @@ import {
|
14 | 14 | isGenericProviderInternalError, |
15 | 15 | parseApiErrorInfo, |
16 | 16 | } from "../../shared/assistant-error-format.js"; |
| 17 | +import { truncateUtf16Safe } from "../../shared/utf16-slice.js"; |
17 | 18 | export { |
18 | 19 | extractLeadingHttpStatus, |
19 | 20 | formatRawAssistantErrorForUi, |
@@ -1540,7 +1541,7 @@ export function formatAssistantErrorText(
|
1540 | 1541 | if (raw.length > 600) { |
1541 | 1542 | log.warn(`Long error truncated: ${raw.slice(0, 200)}`); |
1542 | 1543 | } |
1543 | | -return raw.length > 600 ? `${raw.slice(0, 600)}…` : raw; |
| 1544 | +return raw.length > 600 ? `${truncateUtf16Safe(raw, 600)}…` : raw; |
1544 | 1545 | } |
1545 | 1546 | |
1546 | 1547 | export function isRawAssistantErrorPassthrough(params: { |
@@ -1560,7 +1561,7 @@ export function isRawAssistantErrorPassthrough(params: {
|
1560 | 1561 | friendlyError.startsWith("HTTP "); |
1561 | 1562 | return ( |
1562 | 1563 | friendlyError === rawError || |
1563 | | -(rawError.length > 600 && friendlyError === `${rawError.slice(0, 600)}…`) || |
| 1564 | +(rawError.length > 600 && friendlyError === `${truncateUtf16Safe(rawError, 600)}…`) || |
1564 | 1565 | Boolean(parsedMessage && hasRawDerivedProviderPrefix) || |
1565 | 1566 | Boolean(leadingStatusRest && friendlyError.startsWith("HTTP ")) |
1566 | 1567 | ); |
|
| Original file line number | Diff line number | Diff line change |
|---|
|
1 | 1 | // Assistant error formatting helpers normalize assistant-visible error payloads. |
| 2 | +import { truncateUtf16Safe } from "./utf16-slice.js"; |
2 | 3 | const ERROR_PAYLOAD_PREFIX_RE = |
3 | 4 | /^(?:error|(?:[a-z][\w-]*\s+)?api\s*error|apierror|openai\s*error|anthropic\s*error|gateway\s*error|codex\s*error)(?:\s+\d{3})?[:\s-]+/i; |
4 | 5 | const HTTP_STATUS_DELIMITER_RE = /(?:\s*:\s*|\s+)/; |
@@ -246,5 +247,5 @@ export function formatRawAssistantErrorForUi(raw?: string): string {
|
246 | 247 | return `${prefix}${type}: ${info.message}`; |
247 | 248 | } |
248 | 249 | |
249 | | -return trimmed.length > 600 ? `${trimmed.slice(0, 600)}…` : trimmed; |
| 250 | +return trimmed.length > 600 ? `${truncateUtf16Safe(trimmed, 600)}…` : trimmed; |
250 | 251 | } |
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。