Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@
会生成下一周期的独立任务,并按客户端时区保留本地时间。
- 新增 MiniMax Code 后台支持:通过官方 `mcode acp` 原生 ACP 接入并支持一键安装;
登录、模型与 Provider 配置仍由 MiniMax Code 自己管理。
- 修复 Windows 上 Node.js 位于含空格目录(如默认的 `C:\Program Files\nodejs`)时,
一键安装后台的 npm 步骤被 cmd.exe 截断命令路径而失败的问题;安装后也会正确读取
npm 全局前缀。
- 后台 Session 模型覆盖统一使用 ACP `configOptions` 与
`session/set_config_option`,不再调用私有模型接口或生成后台配置文件;未声明标准
模型选项的 Agent 将沿用自身配置。OpenCode/OpenClaw 一键托管初始化保持不变。
Expand Down
47 changes: 47 additions & 0 deletions server/test/backend-install.test.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import assert from 'node:assert/strict'
import { EventEmitter } from 'node:events'
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import test from 'node:test'
import { backendNames } from '../../shared/backend/catalog.mjs'
import {
Expand Down Expand Up @@ -485,6 +488,50 @@ test('resolves npm.cmd on Windows', async () => {
assert.equal(found[0], 'npm.cmd')
})

test('runs npm.cmd from a Windows directory containing spaces', {
skip: process.platform !== 'win32',
}, async () => {
const root = mkdtempSync(join(tmpdir(), 'qwen-audio-install-'))
try {
const directory = join(root, 'Program Files', 'nodejs')
mkdirSync(directory, { recursive: true })
const npm = join(directory, 'npm.cmd')
writeFileSync(npm, [
'@echo off',
'if "%1"=="config" (',
' echo %~dp0prefix',
' exit /b 0',
')',
'echo %*> "%~dp0install-args.txt"',
'exit /b 0',
'',
].join('\r\n'))
const inspectedPaths = []
const result = await installBackend('opencode', {
env: { ...process.env },
platform: 'win32',
find: () => npm,
inspect: async options => {
inspectedPaths.push(options.env.PATH)
return inspectedPaths.length > 1
? readyReport('opencode')
: { backends: [{ id: 'opencode', ready: false }] }
},
})
assert.equal(result.ok, true, JSON.stringify(result.error))
assert.match(
readFileSync(join(directory, 'install-args.txt'), 'utf8'),
/^install -g opencode-ai@/,
)
assert.ok(
inspectedPaths[1].toLowerCase().includes(`${directory}\\prefix`.toLowerCase()),
'安装后应使用 npm 报告的全局前缀更新 PATH',
)
} finally {
rmSync(root, { recursive: true, force: true })
}
})

test('normalizes a Windows Path key without borrowing the host PATH', async () => {
const observed = []
let inspection = 0
Expand Down
26 changes: 21 additions & 5 deletions shared/backend/install.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ function environmentPath(env) {
return key ? String(env[key] || '') : ''
}

// Windows 上 npm.cmd 必须经 cmd.exe 执行;cmd.exe 会在第一个空格处截断未加
// 引号的命令路径(如 C:\Program Files\nodejs\npm.cmd)。
function windowsShellCommand(command, platform) {
const value = String(command || '')
return platform === 'win32' && /\s/.test(value) && !/^".*"$/.test(value)
? `"${value}"`
: value
}

function specSteps(id, platform) {
const onboarding = backendOnboardingAdapter(id, { platform })
const spec = onboarding.installation
Expand Down Expand Up @@ -254,7 +263,7 @@ function runStep(command, args, {
let child
let output = ''
try {
child = spawnImpl(command, args, {
child = spawnImpl(windowsShellCommand(command, platform), args, {
env: { ...env },
windowsHide: true,
shell: platform === 'win32',
Expand Down Expand Up @@ -629,10 +638,17 @@ export async function installBackend(id, {
if (step.kind === 'npm' && step.scope !== 'backend' && npmCommand) {
// npm install -g 成功后,把全局 bin 目录加入 resolvedEnv.PATH,
// 否则后续验证步骤找不到刚安装的二进制。
const prefixResult = spawnSync(npmCommand, ['config', 'get', 'prefix'], {
env: npmRunEnv(resolvedEnv, npmCommand, platform),
encoding: 'utf8',
})
const prefixResult = spawnSync(
windowsShellCommand(npmCommand, platform),
['config', 'get', 'prefix'],
{
env: npmRunEnv(resolvedEnv, npmCommand, platform),
encoding: 'utf8',
// 与安装步骤一致:Node 拒绝不经 shell 直接启动 .cmd(EINVAL)。
shell: platform === 'win32',
windowsHide: true,
},
)
const rawPrefix = (prefixResult.stdout || '').trim()
const npmGlobalBin = rawPrefix
? platform === 'win32'
Expand Down