fix: 修复 Windows trans 编码与参数传递

This commit is contained in:
Codex
2026-07-10 13:31:26 +02:00
parent ad93490f4a
commit b6ac6f71f6
6 changed files with 108 additions and 4 deletions
+1
View File
@@ -5,6 +5,7 @@ if ($operation -in @('head', 'tail')) {
if ($arg -in @('-n', '--lines')) { $i += 1; if ($i -ge $toolArgs.Count) { Fail ($arg + ' requires a value') 2 }; $linesWanted = Parse-NonNegativeInt $arg $toolArgs[$i] 10000; continue }
if ($arg.StartsWith('-n') -and $arg.Length -gt 2) { $linesWanted = Parse-NonNegativeInt '-n' $arg.Substring(2) 10000; continue }
if ($arg.StartsWith('--lines=')) { $linesWanted = Parse-NonNegativeInt '--lines' $arg.Substring(8) 10000; continue }
if ($arg -match '^-(\d+)$') { $linesWanted = Parse-NonNegativeInt ('-' + $Matches[1]) $Matches[1] 10000; continue }
if ($arg.StartsWith('-') -and $arg -ne '-') { Fail ('unsupported ' + $operation + ' option on Windows route: ' + $arg) 2 }
$paths.Add($arg)
}
+20
View File
@@ -46,6 +46,7 @@ describe("ssh windows PowerShell safety prelude", () => {
expect(prelude).toContain("function ConvertTo-Json");
expect(prelude).toContain("'PSPath','PSParentPath','PSChildName','PSDrive','PSProvider','ReadCount'");
expect(prelude).toContain("Microsoft.PowerShell.Utility\\ConvertTo-Json");
expect(prelude).toContain("$PSDefaultParameterValues['Get-Content:Encoding'] = 'utf8'");
expect(prelude).toContain("Set-Location -LiteralPath 'C:\\test'");
});
});
@@ -114,6 +115,25 @@ describe("ssh windows fs read-only operations", () => {
expect(decodedWindowsPowerShellCommand(cmd.parsed.remoteCommand)).toContain('python tool.py "71-FILTER 高频输出精度与频率跳变改进"');
});
test("supports GNU head/tail counts and preserves PowerShell native argv", () => {
const headScript = windowsFsReadOnlyScript("F:\\Work\\demo", "head", ["-35", "docs/read me.md"]);
const tailScript = windowsFsReadOnlyScript("F:\\Work\\demo", "tail", ["-20", "docs/read me.md"]);
const pythonCode = 'import pathlib; print({"utf8": pathlib.Path(r"docs/报告.md").read_text(encoding="utf-8") != ""})';
const python = parseSshInvocation("D601:win/F/Work/demo", ["ps", "python", "-c", pythonCode]);
const launcher = decodedWindowsPowerShellCommand(python.parsed.remoteCommand);
const encodedPayload = launcher.match(/\$payloadJsonB64 = ''([A-Za-z0-9+/=]+)'';/u)?.[1];
expect(headScript).toContain("$arg -match '^-(\\d+)$'");
expect(tailScript).toContain("$arg -match '^-(\\d+)$'");
expect(python.parsed.invocationKind).toBe("argv");
expect(encodedPayload).toBeDefined();
expect(JSON.parse(Buffer.from(encodedPayload ?? "", "base64").toString("utf8"))).toEqual({
command: "python",
args: ["-c", pythonCode],
nativeArguments: `"-c" "${pythonCode.replaceAll('"', '\\"')}"`,
});
});
test("rejects unsupported Windows read operations instead of treating them as POSIX", () => {
expect(() => parseSshInvocation("D601:win/c/test", ["sed", "-n", "1p", "hello.md"])).toThrow("unsupported ssh win operation: sed");
});
+62 -4
View File
@@ -501,11 +501,11 @@ function parseWinRouteArgs(route: ParsedSshRoute, args: string[]): ParsedSshArgs
if (operation === "ps" || operation === "powershell" || operation === "powershell.exe") {
const commandArgs = args[1] === "--" ? args.slice(2) : args.slice(1);
if (commandArgs.length >= 2 && (commandArgs[0] === "-File" || commandArgs[0] === "-file")) {
const fileArgs = commandArgs.slice(1).map((a: string) => a.replace(/\\/g, "/")).join(" ");
const fileArgs = [commandArgs[1]?.replace(/\\/g, "/") ?? "", ...commandArgs.slice(2)];
return {
remoteCommand: buildWindowsPowerShellInvocation(buildWindowsPowerShellInlineLauncherScript("& " + fileArgs, route.workspace)),
remoteCommand: buildWindowsPowerShellInvocation(buildWindowsPowerShellArgvLauncherScript(fileArgs, route.workspace)),
requiresStdin: false,
invocationKind: "helper",
invocationKind: "argv",
};
}
if (commandArgs.length === 0) {
@@ -515,8 +515,15 @@ function parseWinRouteArgs(route: ParsedSshRoute, args: string[]): ParsedSshArgs
invocationKind: "helper",
};
}
if (commandArgs.length > 1) {
return {
remoteCommand: buildWindowsPowerShellInvocation(buildWindowsPowerShellArgvLauncherScript(commandArgs, route.workspace)),
requiresStdin: false,
invocationKind: "argv",
};
}
return {
remoteCommand: buildWindowsPowerShellInvocation(buildWindowsPowerShellInlineLauncherScript(commandArgs.join(" "), route.workspace)),
remoteCommand: buildWindowsPowerShellInvocation(buildWindowsPowerShellInlineLauncherScript(commandArgs[0] ?? "", route.workspace)),
requiresStdin: false,
invocationKind: "helper",
};
@@ -810,6 +817,7 @@ export function windowsPowerShellScriptPrelude(cwd: string | null): string {
"[Console]::InputEncoding = [System.Text.UTF8Encoding]::new()",
"[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new()",
"$OutputEncoding = [System.Text.UTF8Encoding]::new()",
"$PSDefaultParameterValues['Get-Content:Encoding'] = 'utf8'",
"$env:PYTHONUTF8 = '1'",
"$env:PYTHONIOENCODING = 'utf-8'",
...windowsPowerShellJsonSafetyPreludeLines,
@@ -822,6 +830,56 @@ function buildWindowsPowerShellInlineLauncherScript(command: string, cwd: string
return buildWindowsPowerShellScriptRunner(powerShellSingleQuote(command), cwd);
}
function buildWindowsPowerShellArgvLauncherScript(commandArgs: string[], cwd: string | null): string {
const command = commandArgs[0] ?? "";
if (command.trim().length === 0) throw new Error("ssh win ps requires a command before argv arguments");
const args = commandArgs.slice(1);
const payloadJsonB64 = Buffer.from(JSON.stringify({
command,
args,
nativeArguments: args.map(windowsNativeProcessArgument).join(" "),
}), "utf8").toString("base64");
const script = [
`$payloadJsonB64 = ${powerShellSingleQuote(payloadJsonB64)};`,
"$payload = ConvertFrom-Json -InputObject ([System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($payloadJsonB64)));",
"$command = [string]$payload.command;",
"$commandArgs = @();",
"if ($null -ne $payload.args) { foreach ($item in @($payload.args)) { $commandArgs += [string]$item } }",
"$resolvedCommand = Get-Command -Name $command -ErrorAction Stop;",
"if ($resolvedCommand.CommandType -eq 'Application') {",
" $startInfo = [Diagnostics.ProcessStartInfo]::new();",
" $startInfo.FileName = $resolvedCommand.Source;",
" $startInfo.Arguments = [string]$payload.nativeArguments;",
" $startInfo.WorkingDirectory = (Get-Location).ProviderPath;",
" $startInfo.UseShellExecute = $false;",
" $process = [Diagnostics.Process]::new(); $process.StartInfo = $startInfo;",
" if (-not $process.Start()) { throw ('failed to start native command: ' + $command) }",
" $process.WaitForExit(); $global:LASTEXITCODE = $process.ExitCode;",
"} else { & $command @commandArgs; }",
].join(" ");
return buildWindowsPowerShellScriptRunner(powerShellSingleQuote(script), cwd);
}
function windowsNativeProcessArgument(value: string): string {
if (value.length === 0) return '""';
let quoted = '"';
let backslashes = 0;
for (const character of value) {
if (character === "\\") {
backslashes += 1;
continue;
}
if (character === '"') {
quoted += "\\".repeat(backslashes * 2 + 1) + '"';
backslashes = 0;
continue;
}
quoted += "\\".repeat(backslashes) + character;
backslashes = 0;
}
return quoted + "\\".repeat(backslashes * 2) + '"';
}
function buildWindowsPowerShellStdinLauncherScript(cwd: string | null): string {
return buildWindowsPowerShellScriptRunner("[Console]::In.ReadToEnd()", cwd);
}