fix(migrate-claude): use truncateUtf16Safe for skill description truncation (#102616)

* fix(migrate-claude): use truncateUtf16Safe for skill description truncation

One .slice(0, 180) truncation site in the migrate-claude extension
may cut UTF-16 surrogate pairs in half when the skill description
contains multi-byte characters such as emoji. Replace it with
truncateUtf16Safe to keep the truncated output valid Unicode.

* test(migrate-claude): cover UTF-16 skill description

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
lzyyzznl 2026-07-09 19:38:20 +08:00 committed by GitHub
parent 0aae1ea816
commit d750477d43
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 14 additions and 5 deletions

View file

@ -133,7 +133,11 @@ describe("Claude migration provider", () => {
},
}),
);
await writeFile(path.join(source, ".claude", "commands", "ship.md"), "Ship $ARGUMENTS\n");
const commandDescriptionPrefix = "a".repeat(179);
await writeFile(
path.join(source, ".claude", "commands", "ship.md"),
`${commandDescriptionPrefix}😀tail\n`,
);
await writeFile(path.join(source, ".claude", "skills", "Review", "SKILL.md"), "# Review\n");
const config = {
@ -169,9 +173,13 @@ describe("Claude migration provider", () => {
expect(await fs.readFile(path.join(workspaceDir, "AGENTS.md"), "utf8")).toContain(
"Imported from Claude: project CLAUDE.md",
);
await expect(
fs.access(path.join(workspaceDir, "skills", "claude-command-ship", "SKILL.md")),
).resolves.toBeUndefined();
const generatedSkill = await fs.readFile(
path.join(workspaceDir, "skills", "claude-command-ship", "SKILL.md"),
"utf8",
);
expect(generatedSkill.split("\n").find((line) => line.startsWith("description: "))).toBe(
`description: ${JSON.stringify(commandDescriptionPrefix)}`,
);
await expect(
fs.access(path.join(workspaceDir, "skills", "review", "SKILL.md")),
).resolves.toBeUndefined();

View file

@ -9,6 +9,7 @@ import {
MIGRATION_REASON_TARGET_EXISTS,
} from "openclaw/plugin-sdk/migration";
import type { MigrationItem } from "openclaw/plugin-sdk/plugin-entry";
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
import { exists, readText, sanitizeName } from "./helpers.js";
import type { ClaudeSource } from "./source.js";
import type { PlannedTargets } from "./targets.js";
@ -153,7 +154,7 @@ function generatedCommandSkillContent(params: {
return [
"---",
`name: ${params.skillName}`,
`description: ${JSON.stringify(description.slice(0, 180))}`,
`description: ${JSON.stringify(truncateUtf16Safe(description, 180))}`,
"disable-model-invocation: true",
"---",
"",