[codex] 修复 Windows Coding Agents 安装状态检测 (#1126)

* fix windows coding agent status detection

* fix windows coding agents detection and terminal launch

- Fix Claude Code status detection on Windows by prioritizing .cmd files over unix-style scripts when using 'where' command
- Fix command execution logic for .cmd/.bat files to use proper cmd.exe quoting instead of complex cmdQuote function
- Fix native terminal launch on Windows by properly escaping shellCommand in PowerShell Start-Process instead of using empty $args[0]

These changes resolve issues where Claude Code was incorrectly detected as uninstalled on Windows and native terminal launch failed with PowerShell argument errors.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix claude code custom model launch

* fix windows filename sanitization for coding agent config paths

- Replace invalid filename characters (< > : " / \ | ? *) with underscores in provider/profile names
- Prevents ENOENT errors when provider names contain Windows-invalid characters like colons
- Fixes issue where 'custom:glm-coding-plan' provider would fail to create config directory on Windows

This change ensures that coding agent configuration paths are valid on all platforms while preserving the semantic meaning of provider names.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* remove stale planning docs

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ekko
2026-05-29 20:26:02 +08:00
committed by GitHub
parent 9838173365
commit e3359c671c
4 changed files with 68 additions and 673 deletions
+57 -17
View File
@@ -2,7 +2,7 @@ import { execFile } from 'child_process'
import { existsSync, realpathSync } from 'fs'
import { mkdir, readFile, stat, writeFile } from 'fs/promises'
import { homedir } from 'os'
import { delimiter, dirname, join } from 'path'
import { delimiter, dirname, extname, join } from 'path'
import { promisify } from 'util'
import { getWebUiHome } from '../config'
import { registerClaudeCodeProxyTarget, type ApiMode } from './claude-code-proxy'
@@ -12,9 +12,6 @@ import { getModelContextLength } from './hermes/model-context'
const execFileAsync = promisify(execFile)
const LAUNCH_API_MODES = new Set<ApiMode>(['chat_completions', 'codex_responses', 'anthropic_messages'])
const CLAUDE_PROXY_HAIKU_MODEL = 'claude-haiku-4-5'
const CLAUDE_PROXY_SONNET_MODEL = 'claude-sonnet-4-6'
const CLAUDE_PROXY_OPUS_MODEL = 'claude-opus-4-7'
const CODING_AGENT_HOME_DIR = 'coding-agent'
const CODEX_MODEL_CATALOG_FILE = 'codex-model-catalog.json'
const CODEX_CATALOG_BASE_INSTRUCTIONS = 'You are Codex, a coding agent. Be precise, safe, and helpful.'
@@ -167,12 +164,15 @@ function getNpmBin() {
}
function normalizeScopeSegment(value: string | undefined, fallback: string, label: string): string {
const segment = String(value || '').trim() || fallback
// Replace invalid filename characters with underscores
// Windows invalid chars: < > : " / \ | ? *
// Additional problematic chars: control characters
const sanitizedValue = String(value || '').trim().replace(/[<>:"/\\|?*\x00-\x1f]/g, '_')
const segment = sanitizedValue || fallback
if (
segment === '.' ||
segment === '..' ||
segment.includes('/') ||
segment.includes('\\') ||
segment.includes('\0')
) {
const err = new Error(`Invalid ${label}`)
@@ -309,6 +309,10 @@ function powerShellQuote(value: string): string {
return `'${value.replace(/'/g, "''")}'`
}
function cmdQuote(value: string): string {
return `"${value.replace(/"/g, '""')}"`
}
function buildLaunchShellCommand(input: {
workspaceDir: string
env: Record<string, string>
@@ -357,11 +361,11 @@ function isDockerRuntime(): boolean {
async function openNativeTerminal(shellCommand: string): Promise<string> {
if (process.platform === 'win32') {
const escapedCommand = shellCommand.replace(/"/g, '""').replace(/\$/g, '`$')
await execFileAsync('powershell.exe', [
'-NoProfile',
'-Command',
"Start-Process -FilePath powershell.exe -ArgumentList @('-NoExit', '-Command', $args[0])",
shellCommand,
`Start-Process -FilePath powershell.exe -ArgumentList @('-NoExit', '-Command', "${escapedCommand}")`,
], {
encoding: 'utf-8',
timeout: 8000,
@@ -508,6 +512,34 @@ async function findCommandPaths(command: string, env: NodeJS.ProcessEnv): Promis
}
}
function windowsCommandNeedsShell(command: string): boolean {
const extension = extname(command).toLowerCase()
return extension === '.cmd' || extension === '.bat'
}
async function resolveCommandForExecution(command: string, env: NodeJS.ProcessEnv): Promise<string> {
if (process.platform !== 'win32') return command
const paths = await findCommandPaths(command, env)
// On Windows, prioritize paths with .cmd or .bat extensions since where may return
// both the unix-style script (without extension) and the Windows shim (.cmd)
const windowsPath = paths.find(path => windowsCommandNeedsShell(path))
return windowsPath || paths[0] || command
}
function commandExecution(command: string, args: string[]): { command: string; args: string[] } {
if (process.platform === 'win32' && windowsCommandNeedsShell(command)) {
// For CMD /C, the command and args need to be passed as a single string
// The command path should be quoted if it contains spaces, but args are joined directly
const commandArg = / /.test(command) ? `"${command}"` : command
const argsString = args.map(arg => / /.test(arg) ? `"${arg}"` : arg).join(' ')
return {
command: 'cmd.exe',
args: ['/d', '/s', '/c', `${commandArg} ${argsString}`],
}
}
return { command, args }
}
function packageParts(packageName: string): string[] {
return packageName.split('/').filter(Boolean)
}
@@ -601,11 +633,14 @@ export function getCodingAgentConfigFileDefinitions(id: string): CodingAgentConf
export async function getCodingAgentStatus(definition: CodingAgentDefinition): Promise<CodingAgentToolStatus> {
try {
const { stdout, stderr } = await execFileAsync(definition.command, ['--version'], {
const env = await commandEnv()
const resolvedCommand = await resolveCommandForExecution(definition.command, env)
const execution = commandExecution(resolvedCommand, ['--version'])
const { stdout, stderr } = await execFileAsync(execution.command, execution.args, {
encoding: 'utf-8',
timeout: 8000,
windowsHide: true,
env: await commandEnv(),
env,
})
const rawVersion = `${stdout || ''}${stderr || ''}`.trim()
return {
@@ -864,16 +899,21 @@ export async function prepareCodingAgentLaunch(id: string, input: CodingAgentLau
: null
const claudeBaseUrl = proxyTarget?.baseUrl || baseUrl
const claudeApiKey = proxyTarget?.token || apiKey
const modelName = displayNameForModel(model)
const settings = {
model,
env: {
...(claudeApiKey ? { ANTHROPIC_API_KEY: claudeApiKey } : {}),
...(claudeBaseUrl ? { ANTHROPIC_BASE_URL: claudeBaseUrl } : {}),
ANTHROPIC_DEFAULT_HAIKU_MODEL: CLAUDE_PROXY_HAIKU_MODEL,
ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME: model,
ANTHROPIC_DEFAULT_SONNET_MODEL: CLAUDE_PROXY_SONNET_MODEL,
ANTHROPIC_DEFAULT_SONNET_MODEL_NAME: model,
ANTHROPIC_DEFAULT_OPUS_MODEL: CLAUDE_PROXY_OPUS_MODEL,
ANTHROPIC_DEFAULT_OPUS_MODEL_NAME: model,
ANTHROPIC_MODEL: model,
ANTHROPIC_CUSTOM_MODEL_OPTION: model,
ANTHROPIC_CUSTOM_MODEL_OPTION_NAME: modelName,
ANTHROPIC_DEFAULT_HAIKU_MODEL: model,
ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME: modelName,
ANTHROPIC_DEFAULT_SONNET_MODEL: model,
ANTHROPIC_DEFAULT_SONNET_MODEL_NAME: modelName,
ANTHROPIC_DEFAULT_OPUS_MODEL: model,
ANTHROPIC_DEFAULT_OPUS_MODEL_NAME: modelName,
},
}
await writeScopedFile('settings', `${JSON.stringify(settings, null, 2)}\n`)