Просмотр исходного кода

fix(core): force UTF-8 for Windows shells

Force cmd, PowerShell, and piped Python subprocesses to emit UTF-8 so V2 shell output is decoded without CJK mojibake.

Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Dax Raad 4 недель назад
Родитель
Сommit
34cfa8601f

+ 1 - 0
packages/core/src/shell.ts

@@ -173,6 +173,7 @@ export const layer = Layer.effect(
       const file = path.join(outputDir, `${id}.out`)
       const env = {
         ...process.env,
+        ...ShellSelect.env(),
         TERM: "xterm-256color",
         OPENCODE_TERMINAL: "1",
       } as Record<string, string>

+ 23 - 2
packages/core/src/shell/select.ts

@@ -163,12 +163,33 @@ function info(file: string): Item {
   }
 }
 
+export function env(platform = process.platform) {
+  if (platform === "win32") return { PYTHONIOENCODING: "utf-8" }
+  return {}
+}
+
 export function args(file: string, command: string) {
   const n = name(file)
   if (n === "nu" || n === "fish") return ["-c", command]
   if (n === "zsh" || n === "bash") return ["-c", command]
-  if (n === "cmd") return ["/c", command]
-  if (ps(file)) return ["-NoProfile", "-Command", command]
+  if (n === "cmd") return ["/d", "/c", `chcp 65001 >nul 2>nul & ${command}`]
+  if (ps(file)) {
+    const payload = Buffer.from(command, "utf8").toString("base64")
+    const script = [
+      "$utf8 = [System.Text.UTF8Encoding]::new($false)",
+      "[Console]::InputEncoding = $utf8",
+      "[Console]::OutputEncoding = $utf8",
+      "$OutputEncoding = $utf8",
+      `& ([scriptblock]::Create([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('${payload}'))))`,
+    ].join("; ")
+    return [
+      "-NoLogo",
+      "-NoProfile",
+      "-NonInteractive",
+      "-EncodedCommand",
+      Buffer.from(script, "utf16le").toString("base64"),
+    ]
+  }
   return ["-c", command]
 }
 

+ 1 - 1
packages/core/src/tool/shell.ts

@@ -70,7 +70,7 @@ const modelOutput = (output: Output): string | undefined => {
 // TODO: Port tree-sitter bash / PowerShell parser-based approval reduction.
 // TODO: Port BashArity reusable command-prefix approvals.
 // TODO: Replace token-based command-argument external-directory advisories with parser-based detection.
-// TODO: Restore PowerShell and cmd-specific invocation/path handling on Windows.
+// TODO: Restore Windows shell-specific path handling.
 // TODO: Add plugin shell.env environment augmentation once V2 plugin hooks exist.
 // TODO: Add durable/live progress metadata streaming for long-running commands once V2 tool invocation progress context is wired.
 // TODO: Persist job status and define restart recovery before exposing remote observation.

+ 17 - 0
packages/core/test/shell.test.ts

@@ -61,6 +61,23 @@ describe("shell", () => {
     expect(ShellSelect.args("/bin/bash", "echo hi")).toEqual(["-c", "echo hi"])
   })
 
+  test("switches cmd output to UTF-8", () => {
+    expect(ShellSelect.args("cmd", 'echo "你好"')).toEqual(["/d", "/c", 'chcp 65001 >nul 2>nul & echo "你好"'])
+  })
+
+  test("transports PowerShell commands independently of the active code page", () => {
+    const args = ShellSelect.args("pwsh", 'Write-Output "你好"')
+    expect(args.slice(0, 4)).toEqual(["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand"])
+    expect(Buffer.from(args[4], "base64").toString("utf16le")).toBe(
+      "$utf8 = [System.Text.UTF8Encoding]::new($false); [Console]::InputEncoding = $utf8; [Console]::OutputEncoding = $utf8; $OutputEncoding = $utf8; & ([scriptblock]::Create([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('V3JpdGUtT3V0cHV0ICLkvaDlpb0i'))))",
+    )
+  })
+
+  test("forces piped Python output to UTF-8 on Windows", () => {
+    expect(ShellSelect.env("win32")).toEqual({ PYTHONIOENCODING: "utf-8" })
+    expect(ShellSelect.env("linux")).toEqual({})
+  })
+
   if (process.platform === "win32") {
     test("rejects blacklisted shells case-insensitively", async () => {
       await withShell("NU.EXE", async () => {

+ 1 - 1
packages/core/test/tool-shell.test.ts

@@ -570,7 +570,7 @@ test("keeps locked deferred parity TODOs visible", async () => {
     "Port tree-sitter bash / PowerShell parser-based approval reduction.",
     "Port BashArity reusable command-prefix approvals.",
     "Replace token-based command-argument external-directory advisories with parser-based detection.",
-    "Restore PowerShell and cmd-specific invocation/path handling on Windows.",
+    "Restore Windows shell-specific path handling.",
     "Add plugin shell.env environment augmentation once V2 plugin hooks exist.",
     "Add durable/live progress metadata streaming for long-running commands once V2 tool invocation progress context is wired.",
     "Persist job status and define restart recovery before exposing remote observation.",