@ljwei-stak/dsh-model-router 0.18.2 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dsh-plugin/client.js +257 -57
- package/.dsh-plugin/index.mjs +233 -81
- package/.dsh-plugin/official-tools-remote-service.mjs +7 -3
- package/.dsh-plugin/shared/cli-terminal-protocol.mjs +15 -5
- package/.dsh-plugin/shared/cli-terminal.mjs +126 -18
- package/.dsh-plugin/shared/official-team-runtime.mjs +189 -49
- package/.dsh-plugin/shared/official-tool-executor.mjs +343 -70
- package/.dsh-plugin/shared/official-tool-registry.mjs +2 -2
- package/.dsh-plugin/shared/official-tools-remote.mjs +12 -0
- package/.dsh-plugin/shared/official-tools-runtime.mjs +55 -14
- package/.dsh-plugin/shared/run-ledger.mjs +51 -2
- package/.dsh-plugin/shared/task-executors.mjs +62 -18
- package/.dsh-plugin/shared/tool-health.mjs +24 -6
- package/.dsh-plugin/shared/tool-install-preferences.mjs +22 -0
- package/.dsh-plugin/shared/zcode-bundle.mjs +29 -8
- package/.dsh-plugin/shared/zcode-installer.mjs +2 -2
- package/CHANGELOG.md +29 -0
- package/README.md +22 -14
- package/README.zh.md +21 -13
- package/docs/INSTALLATION_GUIDE.zh.md +6 -6
- package/docs/WORKBENCH_USER_GUIDE.zh.md +36 -8
- package/docs/releases/0.20.0-validation.md +26 -0
- package/package.json +4 -3
|
@@ -9,10 +9,12 @@
|
|
|
9
9
|
* into that interactive process, exactly as in their own terminal, so a
|
|
10
10
|
* session starts only after an explicit confirmation.
|
|
11
11
|
*/
|
|
12
|
-
import { getOfficialTool } from './official-tool-registry.mjs'
|
|
12
|
+
import { getOfficialTool, OFFICIAL_TOOLS } from './official-tool-registry.mjs'
|
|
13
13
|
|
|
14
|
-
/**
|
|
15
|
-
export const TERMINAL_TOOL_IDS = Object.freeze(
|
|
14
|
+
/** Follow the tool registry, including the TUI bundled with ZCode desktop. */
|
|
15
|
+
export const TERMINAL_TOOL_IDS = Object.freeze(OFFICIAL_TOOLS
|
|
16
|
+
.filter(tool => !tool.unsupported && (tool.probeExecutables?.length || tool.manager === 'signed-windows-installer'))
|
|
17
|
+
.map(tool => tool.id))
|
|
16
18
|
export const TERMINAL_SHELL_ID = 'shell'
|
|
17
19
|
export const TERMINAL_TARGETS = Object.freeze([TERMINAL_SHELL_ID, ...TERMINAL_TOOL_IDS])
|
|
18
20
|
export const TERMINAL_MODES = Object.freeze(['interactive', 'login'])
|
|
@@ -46,8 +48,16 @@ export const TERMINAL_LOGIN_ARGS = Object.freeze({
|
|
|
46
48
|
'mimo-code': Object.freeze(['auth', 'login']),
|
|
47
49
|
'grok-build': Object.freeze(['login']),
|
|
48
50
|
gemini: Object.freeze([]),
|
|
51
|
+
opencode: Object.freeze(['auth', 'login']),
|
|
52
|
+
stepcode: Object.freeze(['login']),
|
|
53
|
+
zcode: Object.freeze([]),
|
|
49
54
|
})
|
|
50
55
|
|
|
56
|
+
/** Tools without a separate login command authenticate from their TUI. */
|
|
57
|
+
export function terminalCanLogin(target) {
|
|
58
|
+
return Boolean(TERMINAL_LOGIN_ARGS[target]?.length)
|
|
59
|
+
}
|
|
60
|
+
|
|
51
61
|
const SESSION_ID = /^term-[a-z0-9]{6,40}$/
|
|
52
62
|
|
|
53
63
|
function plainObject(value, subject) {
|
|
@@ -82,7 +92,7 @@ export function terminalTargetLabel(target) {
|
|
|
82
92
|
export function terminalCommandPreview(target, mode = 'interactive', platform = 'win32') {
|
|
83
93
|
if (target === TERMINAL_SHELL_ID) return platform === 'win32' ? 'powershell.exe -NoLogo' : '$SHELL -l'
|
|
84
94
|
const tool = getOfficialTool(target)
|
|
85
|
-
const executable = tool?.probeExecutables?.[0] ?? String(target)
|
|
95
|
+
const executable = target === 'zcode' ? 'ZCode 桌面版内置 TUI' : tool?.probeExecutables?.[0] ?? String(target)
|
|
86
96
|
const args = mode === 'login' ? TERMINAL_LOGIN_ARGS[target] ?? [] : []
|
|
87
97
|
return [executable, ...args].join(' ')
|
|
88
98
|
}
|
|
@@ -92,7 +102,7 @@ export function parseTerminalStart(value) {
|
|
|
92
102
|
if (!TERMINAL_TARGETS.includes(request.target)) throw new TypeError('target must be the shell or a fixed official CLI')
|
|
93
103
|
const mode = request.mode === undefined ? 'interactive' : request.mode
|
|
94
104
|
if (!TERMINAL_MODES.includes(mode)) throw new TypeError('mode must be interactive or login')
|
|
95
|
-
if (mode === 'login' && request.target
|
|
105
|
+
if (mode === 'login' && !terminalCanLogin(request.target)) throw new TypeError('this target has no login mode')
|
|
96
106
|
if (!isAbsoluteDirectoryText(request.cwd)) throw new TypeError('cwd must be an absolute directory path')
|
|
97
107
|
if (request.confirmed !== true) throw new TypeError('starting a terminal requires the user\'s confirmation')
|
|
98
108
|
return {
|
|
@@ -13,11 +13,14 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { spawn } from 'node:child_process'
|
|
15
15
|
import { randomBytes } from 'node:crypto'
|
|
16
|
-
import { stat } from 'node:fs/promises'
|
|
16
|
+
import { readFile, readdir, realpath, stat } from 'node:fs/promises'
|
|
17
17
|
import { createRequire } from 'node:module'
|
|
18
18
|
import { StringDecoder } from 'node:string_decoder'
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
19
|
+
import { homedir } from 'node:os'
|
|
20
|
+
import { extname, isAbsolute, join, relative, sep } from 'node:path'
|
|
21
|
+
import { getOfficialTool, installMethodFor } from './official-tool-registry.mjs'
|
|
22
|
+
import { resolveInstallPlan } from './tool-install-preferences.mjs'
|
|
23
|
+
import { discoverZCodeBundle } from './zcode-bundle.mjs'
|
|
21
24
|
import {
|
|
22
25
|
TERMINAL_LIMITS,
|
|
23
26
|
TERMINAL_LOGIN_ARGS,
|
|
@@ -84,6 +87,75 @@ export function pickWindowsExecutable(candidates) {
|
|
|
84
87
|
|
|
85
88
|
const quoteWindows = path => `"${String(path).replace(/"/g, '')}"`
|
|
86
89
|
|
|
90
|
+
function within(root, child) {
|
|
91
|
+
const part = relative(root, child)
|
|
92
|
+
return Boolean(part) && part !== '..' && !part.startsWith(`..${sep}`) && !isAbsolute(part)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Bind a configured npm/pnpm tool to its declared package entry, not a shared shim. */
|
|
96
|
+
export async function findTerminalPackageEntry(tool, plan, { platform = process.platform } = {}) {
|
|
97
|
+
let roots = plan.packageModuleRoots
|
|
98
|
+
let boundary
|
|
99
|
+
if (!Array.isArray(roots)) {
|
|
100
|
+
if (!plan.installDir) return null
|
|
101
|
+
if (plan.manager === 'pnpm') {
|
|
102
|
+
const global = join(plan.installDir, '.pnpm', 'global')
|
|
103
|
+
boundary = await realpath(global).catch(() => null)
|
|
104
|
+
const versions = await readdir(global, { withFileTypes: true }).catch(() => [])
|
|
105
|
+
roots = versions.filter(item => item.isDirectory() && /^\d+$/.test(item.name))
|
|
106
|
+
.sort((left, right) => Number(left.name) - Number(right.name))
|
|
107
|
+
.map(item => join(global, item.name, 'node_modules'))
|
|
108
|
+
} else {
|
|
109
|
+
const modules = platform === 'win32' ? join(plan.installDir, 'node_modules') : join(plan.installDir, 'lib', 'node_modules')
|
|
110
|
+
boundary = await realpath(modules).catch(() => null)
|
|
111
|
+
roots = [modules]
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
boundary = await realpath(plan.globalDir || plan.installDir || roots[0]).catch(() => null)
|
|
115
|
+
}
|
|
116
|
+
if (!boundary) return null
|
|
117
|
+
const entries = []
|
|
118
|
+
for (const modules of roots) {
|
|
119
|
+
try {
|
|
120
|
+
const packageRoot = await realpath(join(modules, ...tool.package.split('/')))
|
|
121
|
+
if (!within(boundary, packageRoot)) continue
|
|
122
|
+
const manifest = JSON.parse(await readFile(join(packageRoot, 'package.json'), 'utf8'))
|
|
123
|
+
if (manifest.name !== tool.package) continue
|
|
124
|
+
for (const executable of tool.probeExecutables) {
|
|
125
|
+
const raw = typeof manifest.bin === 'string' ? manifest.bin : manifest.bin?.[executable]
|
|
126
|
+
const bin = typeof raw === 'string' ? raw.replace(/\\/g, '/').replace(/^\.\//, '') : ''
|
|
127
|
+
if (!bin || bin.split('/').some(part => !part || part === '..') || isAbsolute(bin)) continue
|
|
128
|
+
const entry = await realpath(join(packageRoot, ...bin.split('/')))
|
|
129
|
+
if (!within(packageRoot, entry) || !(await stat(entry)).isFile()) continue
|
|
130
|
+
entries.push({ entry, packageRoot, version: String(manifest.version ?? '') })
|
|
131
|
+
}
|
|
132
|
+
} catch { /* not the selected tool's usable package */ }
|
|
133
|
+
}
|
|
134
|
+
// pnpm normally has one active global tree. Prefer the highest numbered
|
|
135
|
+
// layout when multiple layouts survive an upgrade, following its directories.
|
|
136
|
+
return entries.at(-1) ?? null
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function terminalNode(workspace, minimum, acceptedVersion) {
|
|
140
|
+
const { findNodeExecutable } = await import('./official-tool-executor.mjs')
|
|
141
|
+
return findNodeExecutable(workspace, minimum, acceptedVersion)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function executableLaunch(file, args, display, { platform, env }) {
|
|
145
|
+
if (platform !== 'win32') return { file, args, verbatim: false, display }
|
|
146
|
+
const extension = extname(file).toLowerCase()
|
|
147
|
+
if (extension === '.cmd' || extension === '.bat') {
|
|
148
|
+
const comspec = env.ComSpec || env.COMSPEC || 'cmd.exe'
|
|
149
|
+
const tail = `/d /s /c "${[quoteWindows(file), ...args].join(' ')}"`
|
|
150
|
+
return { file: comspec, args: tail, verbatim: true, display }
|
|
151
|
+
}
|
|
152
|
+
if (extension === '.ps1') {
|
|
153
|
+
const root = env.SystemRoot || env.SYSTEMROOT || 'C:\\Windows'
|
|
154
|
+
return { file: `${root}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`, args: ['-NoLogo', '-NoProfile', '-File', file, ...args], verbatim: false, display }
|
|
155
|
+
}
|
|
156
|
+
return { file, args, verbatim: false, display }
|
|
157
|
+
}
|
|
158
|
+
|
|
87
159
|
/**
|
|
88
160
|
* Build the fixed launch for one target. `locate(executable)` returns the
|
|
89
161
|
* candidate paths `where`/`which` printed. Returns
|
|
@@ -92,6 +164,9 @@ const quoteWindows = path => `"${String(path).replace(/"/g, '')}"`
|
|
|
92
164
|
*/
|
|
93
165
|
export async function resolveTerminalLaunch(target, mode = 'interactive', {
|
|
94
166
|
platform = process.platform, env = process.env, locate = defaultLocate,
|
|
167
|
+
preferences = {}, home = homedir(), cwd = process.cwd(),
|
|
168
|
+
packageEntry = findTerminalPackageEntry, fileExists = async path => (await stat(path).catch(() => null))?.isFile() === true,
|
|
169
|
+
discoverBundle = discoverZCodeBundle, findNode = terminalNode,
|
|
95
170
|
} = {}) {
|
|
96
171
|
if (target === TERMINAL_SHELL_ID) {
|
|
97
172
|
if (platform === 'win32') {
|
|
@@ -104,9 +179,47 @@ export async function resolveTerminalLaunch(target, mode = 'interactive', {
|
|
|
104
179
|
}
|
|
105
180
|
if (!TERMINAL_TOOL_IDS.includes(target)) throw new Error('只能启动系统终端或固定注册表中的官方 CLI。')
|
|
106
181
|
const tool = getOfficialTool(target)
|
|
182
|
+
if (target === 'zcode') {
|
|
183
|
+
if (platform !== 'win32') throw new Error('当前仅支持 Windows ZCode 桌面版内置 TUI。')
|
|
184
|
+
const bundle = await discoverBundle({ installDir: preferences.zcodeInstallDir, env })
|
|
185
|
+
if (!bundle) throw new Error('未找到通过智谱签名与组件核验的 ZCode 桌面版;请先配置 ZCode 独立安装目录并重新体检。')
|
|
186
|
+
const node = await findNode(cwd, [24, 14, 0], version => Number(String(version).split('.')[0]) === 24)
|
|
187
|
+
// The verified desktop's electron-node entry is usable independently of
|
|
188
|
+
// its GUI. It supplies its own runtime when external Node 24 is absent.
|
|
189
|
+
return { file: node || bundle.executable, args: [bundle.cliEntry], verbatim: false,
|
|
190
|
+
display: `ZCode TUI · ${bundle.cliEntry}`, ...(!node ? { env: { ELECTRON_RUN_AS_NODE: '1' } } : {}) }
|
|
191
|
+
}
|
|
107
192
|
const executable = tool.probeExecutables[0]
|
|
108
193
|
const args = mode === 'login' ? [...(TERMINAL_LOGIN_ARGS[target] ?? [])] : []
|
|
109
194
|
const display = [executable, ...args].join(' ')
|
|
195
|
+
const selectedScript = installMethodFor(tool, preferences.methods?.[target])?.kind === 'script'
|
|
196
|
+
const selectedPreferences = selectedScript ? { ...preferences, allowScriptInstall: true } : preferences
|
|
197
|
+
const plan = resolveInstallPlan(tool, undefined, selectedPreferences, { platform, env, home })
|
|
198
|
+
const configured = Boolean(preferences.installDir || preferences.methods?.[target])
|
|
199
|
+
if (plan.kind === 'package-manager' && configured) {
|
|
200
|
+
const boundPlan = plan.installDir ? plan : await (await import('./official-tools-runtime.mjs')).resolvePackageJobPlan(tool, plan, selectedPreferences, { options: { platform, env, home } })
|
|
201
|
+
const found = await packageEntry(tool, boundPlan, { platform })
|
|
202
|
+
if (!found) throw new Error(`未在所选 ${boundPlan.manager} 安装目录中找到 ${tool.label} 的真实命令入口;请重新体检或修复。`)
|
|
203
|
+
const extension = extname(found.entry).toLowerCase()
|
|
204
|
+
if (['.js', '.mjs', '.cjs'].includes(extension) || (platform === 'win32' && !extension)) {
|
|
205
|
+
const node = await findNode(cwd)
|
|
206
|
+
if (!node) throw new Error(`未找到可核验的 Node.js 运行时,无法启动 ${tool.label}。`)
|
|
207
|
+
return { file: node, args: [found.entry, ...args], verbatim: false, display }
|
|
208
|
+
}
|
|
209
|
+
return executableLaunch(found.entry, args, display, { platform, env })
|
|
210
|
+
}
|
|
211
|
+
if (plan.kind === 'script' && plan.binDir) {
|
|
212
|
+
for (const name of tool.probeExecutables) {
|
|
213
|
+
const filenames = platform === 'win32' ? WINDOWS_PREFERENCE.map(extension => `${name}${extension}`) : [name]
|
|
214
|
+
for (const filename of filenames) {
|
|
215
|
+
const file = join(plan.binDir, filename)
|
|
216
|
+
if (await fileExists(file)) return executableLaunch(file, args, display, { platform, env })
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// A saved method or directory is authoritative: an older PATH copy cannot
|
|
220
|
+
// satisfy a missing install in the user's selected tool directory.
|
|
221
|
+
if (configured || tool.manager === 'script-installer') throw new Error(`未在所选安装目录 ${plan.binDir} 中找到 ${tool.label};请重新体检或安装。`)
|
|
222
|
+
}
|
|
110
223
|
const candidates = await locate(executable, { platform, env })
|
|
111
224
|
if (platform !== 'win32') {
|
|
112
225
|
const file = candidates.map(item => String(item).trim()).find(item => item.startsWith('/'))
|
|
@@ -115,18 +228,7 @@ export async function resolveTerminalLaunch(target, mode = 'interactive', {
|
|
|
115
228
|
}
|
|
116
229
|
const file = pickWindowsExecutable(candidates)
|
|
117
230
|
if (!file) throw new Error(`未在 PATH 中找到 ${executable};请先在“官方工具 · 体检”中安装 ${tool.label}。`)
|
|
118
|
-
|
|
119
|
-
if (extension === '.cmd' || extension === '.bat') {
|
|
120
|
-
// npm shims only run under cmd.exe. Every token is fixed by the registry.
|
|
121
|
-
const comspec = env.ComSpec || env.COMSPEC || 'cmd.exe'
|
|
122
|
-
const tail = `/d /s /c "${[quoteWindows(file), ...args].join(' ')}"`
|
|
123
|
-
return { file: comspec, args: tail, verbatim: true, display }
|
|
124
|
-
}
|
|
125
|
-
if (extension === '.ps1') {
|
|
126
|
-
const root = env.SystemRoot || env.SYSTEMROOT || 'C:\\Windows'
|
|
127
|
-
return { file: `${root}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`, args: ['-NoLogo', '-NoProfile', '-File', file, ...args], verbatim: false, display }
|
|
128
|
-
}
|
|
129
|
-
return { file, args, verbatim: false, display }
|
|
231
|
+
return executableLaunch(file, args, display, { platform, env })
|
|
130
232
|
}
|
|
131
233
|
|
|
132
234
|
/** Locate an executable with where.exe / which (fixed registry names only). */
|
|
@@ -161,7 +263,10 @@ function spawnPtyProcess(pty, launch, { cwd, cols, rows, env }) {
|
|
|
161
263
|
|
|
162
264
|
/** Pipe fallback: no TTY; the Host echoes input and maps Enter/Ctrl+C. */
|
|
163
265
|
export function spawnPipeProcess(launch, { cwd, env, platform = process.platform, spawnImpl = spawn }) {
|
|
164
|
-
|
|
266
|
+
// node-pty accepts a Windows command-line string; node:child_process needs
|
|
267
|
+
// an array even with windowsVerbatimArguments. Keep the prepared tail intact.
|
|
268
|
+
const args = typeof launch.args === 'string' ? [launch.args] : launch.args
|
|
269
|
+
const child = spawnImpl(launch.file, args, {
|
|
165
270
|
cwd, env, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true, shell: false,
|
|
166
271
|
windowsVerbatimArguments: launch.verbatim === true,
|
|
167
272
|
})
|
|
@@ -220,6 +325,8 @@ export function createTerminalManager({
|
|
|
220
325
|
statPath = stat,
|
|
221
326
|
prepare = async () => {},
|
|
222
327
|
environment = () => terminalEnvironment(process.env),
|
|
328
|
+
preferences = () => ({}),
|
|
329
|
+
launchOptions = () => ({}),
|
|
223
330
|
onSessionEnd = () => {},
|
|
224
331
|
now = Date.now,
|
|
225
332
|
platform = process.platform,
|
|
@@ -329,10 +436,11 @@ export function createTerminalManager({
|
|
|
329
436
|
try { info = await statPath(request.cwd) } catch { info = null }
|
|
330
437
|
if (!info?.isDirectory?.()) throw new Error(`工作目录不存在或不是目录:${request.cwd}`)
|
|
331
438
|
await prepare()
|
|
332
|
-
const launch = await resolveLaunch(request.target, request.mode, { platform })
|
|
333
439
|
const env = environment()
|
|
440
|
+
const launch = await resolveLaunch(request.target, request.mode, { ...launchOptions(), platform, env,
|
|
441
|
+
preferences: preferences(), cwd: request.cwd })
|
|
334
442
|
const backend = backendInfo()
|
|
335
|
-
const options = { cwd: request.cwd, cols: request.cols, rows: request.rows, env, platform }
|
|
443
|
+
const options = { cwd: request.cwd, cols: request.cols, rows: request.rows, env: { ...env, ...launch.env }, platform }
|
|
336
444
|
let proc
|
|
337
445
|
try {
|
|
338
446
|
proc = backend.backend === 'pty'
|
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
* worktree for review. No package supplies an executable or shell fragment.
|
|
6
6
|
*/
|
|
7
7
|
import { spawn } from 'node:child_process'
|
|
8
|
-
import { appendFile, mkdir, realpath, stat } from 'node:fs/promises'
|
|
9
|
-
import { randomUUID } from 'node:crypto'
|
|
8
|
+
import { appendFile, lstat, mkdir, realpath, stat } from 'node:fs/promises'
|
|
9
|
+
import { createHash, randomUUID } from 'node:crypto'
|
|
10
|
+
import { tmpdir } from 'node:os'
|
|
10
11
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
|
|
11
12
|
import { runOfficialTool, officialToolExecutionCapabilities, officialToolReadiness } from './official-tool-executor.mjs'
|
|
12
13
|
import { toolForProvider } from './official-tool-registry.mjs'
|
|
14
|
+
import { redactDiagnostic, verifiedDiagnostic } from './task-executors.mjs'
|
|
13
15
|
|
|
14
16
|
const MAX_GIT_OUTPUT = 64_000_000
|
|
15
17
|
const GIT_TIMEOUT_MS = 120_000
|
|
@@ -26,6 +28,61 @@ async function canonicalDirectory(path) {
|
|
|
26
28
|
return canonical
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
function pathIdentity(path) {
|
|
32
|
+
return process.platform === 'win32' ? resolve(path).toLowerCase() : resolve(path)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function approvedSource(workspace, allowedRoot) {
|
|
36
|
+
const source = await canonicalDirectory(workspace)
|
|
37
|
+
const root = await canonicalDirectory(allowedRoot ?? source)
|
|
38
|
+
if (!within(root, source)) throw new Error('原工作区不在当前 Harness 会话批准的工作区范围内。')
|
|
39
|
+
return source
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function sourceStorageKey(source) {
|
|
43
|
+
return createHash('sha256').update(pathIdentity(source)).digest('hex')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function requireDisjointStorage(source, storage) {
|
|
47
|
+
if (within(source, storage) || within(storage, source)) {
|
|
48
|
+
throw new Error('Host 独立工作区存储必须与原工作区互不包含。')
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Reject junctions before creating deeper directories in Host-managed storage. */
|
|
53
|
+
async function storageDirectory(parent, name) {
|
|
54
|
+
const target = join(parent, name)
|
|
55
|
+
try { await mkdir(target) }
|
|
56
|
+
catch (error) { if (error?.code !== 'EEXIST') throw error }
|
|
57
|
+
const entry = await lstat(target)
|
|
58
|
+
const canonical = await canonicalDirectory(target)
|
|
59
|
+
if (!entry.isDirectory() || entry.isSymbolicLink() || pathIdentity(canonical) !== pathIdentity(target)) {
|
|
60
|
+
throw new Error('Host 独立工作区存储不能经过符号链接或目录联接。')
|
|
61
|
+
}
|
|
62
|
+
return canonical
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Windows ACL grants need WRITE_OWNER as well as ordinary file modification.
|
|
67
|
+
* Use the user's existing private temp storage rather than changing a source
|
|
68
|
+
* drive's ACL. Only the individual run receives the sandbox write capability;
|
|
69
|
+
* the SDK's separate random private temp remains disjoint from that run.
|
|
70
|
+
*/
|
|
71
|
+
async function windowsStorageParent(source, create = false) {
|
|
72
|
+
const temporary = await canonicalDirectory(tmpdir())
|
|
73
|
+
const rootPath = join(temporary, '.model-router-workspaces')
|
|
74
|
+
requireDisjointStorage(source, rootPath)
|
|
75
|
+
const root = create ? await storageDirectory(temporary, '.model-router-workspaces')
|
|
76
|
+
: await canonicalDirectory(rootPath)
|
|
77
|
+
if (pathIdentity(root) !== pathIdentity(rootPath)) throw new Error('Host 独立工作区存储路径校验失败。')
|
|
78
|
+
const key = sourceStorageKey(source)
|
|
79
|
+
const parentPath = join(root, key)
|
|
80
|
+
const parent = create ? await storageDirectory(root, key) : await canonicalDirectory(parentPath)
|
|
81
|
+
if (pathIdentity(parent) !== pathIdentity(parentPath)) throw new Error('Host 独立工作区存储路径校验失败。')
|
|
82
|
+
requireDisjointStorage(source, parent)
|
|
83
|
+
return parent
|
|
84
|
+
}
|
|
85
|
+
|
|
29
86
|
/** The session owner, never model arguments, selects the filesystem scope. */
|
|
30
87
|
export async function sessionWorkspace(ctx, exec) {
|
|
31
88
|
const session = exec?.agent?.session
|
|
@@ -97,6 +154,13 @@ async function cleanRepoRoot(workspace, signal) {
|
|
|
97
154
|
return repo
|
|
98
155
|
}
|
|
99
156
|
|
|
157
|
+
/** Preview-only validation; never creates a worktree or changes ignore rules. */
|
|
158
|
+
export async function preflightOfficialWorkspace(workspace, mode = 'read-only', signal) {
|
|
159
|
+
const source = await canonicalDirectory(workspace)
|
|
160
|
+
if (mode === 'workspace-write') await cleanRepoRoot(source, signal)
|
|
161
|
+
return source
|
|
162
|
+
}
|
|
163
|
+
|
|
100
164
|
/** Keep nested worktrees out of ordinary git status without changing tracked files. */
|
|
101
165
|
async function excludeNestedWorktrees(workspace, signal) {
|
|
102
166
|
const ignored = await git(['check-ignore', '-q', '--', '.model-router-workspaces/'], { cwd: workspace, signal })
|
|
@@ -114,25 +178,31 @@ async function excludeNestedWorktrees(workspace, signal) {
|
|
|
114
178
|
if (!confirmed.ok) throw gitError('确认独立工作区忽略规则', confirmed)
|
|
115
179
|
}
|
|
116
180
|
|
|
117
|
-
/**
|
|
181
|
+
/** Source authorization is independent from the Host's isolated output storage. */
|
|
118
182
|
async function isolatedWorktree(workspace, signal, allowedRoot) {
|
|
183
|
+
workspace = await approvedSource(workspace, allowedRoot)
|
|
119
184
|
await cleanRepoRoot(workspace, signal)
|
|
120
185
|
const base = await git(['rev-parse', 'HEAD'], { cwd: workspace, signal })
|
|
121
186
|
if (!base.ok || !/^[0-9a-f]{40,64}$/i.test(base.output.toString('utf8').trim())) {
|
|
122
187
|
throw gitError('记录原仓库基线', base)
|
|
123
188
|
}
|
|
124
189
|
const baseCommit = base.output.toString('utf8').trim()
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
190
|
+
let isolatedRoot
|
|
191
|
+
if (process.platform === 'win32') isolatedRoot = await windowsStorageParent(workspace, true)
|
|
192
|
+
else {
|
|
193
|
+
const policyRoot = await canonicalDirectory(allowedRoot ?? workspace)
|
|
194
|
+
const sibling = join(dirname(workspace), '.model-router-workspaces', basename(workspace))
|
|
195
|
+
const parent = within(policyRoot, sibling) ? sibling : join(workspace, '.model-router-workspaces')
|
|
196
|
+
if (parent === join(workspace, '.model-router-workspaces')) await excludeNestedWorktrees(workspace, signal)
|
|
197
|
+
await mkdir(parent, { recursive: true })
|
|
198
|
+
isolatedRoot = await canonicalDirectory(parent)
|
|
199
|
+
}
|
|
131
200
|
const target = join(isolatedRoot, `run-${randomUUID()}`)
|
|
132
201
|
const added = await git(['worktree', 'add', '--detach', target, 'HEAD'], { cwd: workspace, signal })
|
|
133
202
|
if (!added.ok) throw gitError('创建独立 Git 工作区', added)
|
|
134
203
|
const runPath = await canonicalDirectory(target)
|
|
135
|
-
if (
|
|
204
|
+
if (pathIdentity(dirname(runPath)) !== pathIdentity(isolatedRoot)
|
|
205
|
+
|| pathIdentity(runPath) !== pathIdentity(target)) throw new Error('独立工作区路径校验失败')
|
|
136
206
|
return { workspace: runPath, isolatedRoot, baseCommit, source: workspace }
|
|
137
207
|
}
|
|
138
208
|
|
|
@@ -198,13 +268,11 @@ function checkedTeamCliModels(cliModels, assigned) {
|
|
|
198
268
|
|
|
199
269
|
function teamCliModel(item, cliModels) {
|
|
200
270
|
// Harness directory IDs and vendor CLI aliases are independent namespaces.
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
// user supplies a verified CLI mapping through that vendor's own settings.
|
|
271
|
+
// Every vendor uses its own configured default unless a package or tool
|
|
272
|
+
// explicitly binds the model name accepted by that CLI.
|
|
204
273
|
if (cliModels[item.id]) return cliModels[item.id]
|
|
205
274
|
if (cliModels[item.toolId]) return cliModels[item.toolId]
|
|
206
|
-
return
|
|
207
|
-
? item.recommendedModel : null
|
|
275
|
+
return null
|
|
208
276
|
}
|
|
209
277
|
|
|
210
278
|
function reportedCliModel(toolId, value) {
|
|
@@ -241,8 +309,8 @@ function packagePrompt(task, item, completed) {
|
|
|
241
309
|
}
|
|
242
310
|
|
|
243
311
|
/** Preflight every assignment before any model request or workspace write. */
|
|
244
|
-
export function executableTeamPackages(plan, installedIds = [], mode = 'read-only') {
|
|
245
|
-
const capabilities = new Map(officialToolExecutionCapabilities().map(item => [item.id, item]))
|
|
312
|
+
export function executableTeamPackages(plan, installedIds = [], mode = 'read-only', executionEnvironment = 'harness') {
|
|
313
|
+
const capabilities = new Map(officialToolExecutionCapabilities({ executionEnvironment }).map(item => [item.id, item]))
|
|
246
314
|
const packages = Array.isArray(plan?.team?.workPackages) ? plan.team.workPackages : []
|
|
247
315
|
const blocking = []
|
|
248
316
|
const assigned = packages.map(item => {
|
|
@@ -266,6 +334,34 @@ async function removeWorktree(source, target) {
|
|
|
266
334
|
try { await git(['worktree', 'remove', '--force', target], { cwd: source }) } catch { /* left for manual cleanup */ }
|
|
267
335
|
}
|
|
268
336
|
|
|
337
|
+
function executionDiagnostic(value) {
|
|
338
|
+
let detail = String(value ?? '')
|
|
339
|
+
for (const key of ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN', 'CLAUDE_CODE_OAUTH_TOKEN',
|
|
340
|
+
'OPENAI_API_KEY', 'CODEX_API_KEY', 'KIMI_API_KEY', 'MOONSHOT_API_KEY', 'MINIMAX_API_KEY',
|
|
341
|
+
'MIMO_API_KEY', 'XAI_API_KEY', 'ZAI_API_KEY', 'GEMINI_API_KEY', 'GOOGLE_API_KEY']) {
|
|
342
|
+
const secret = process.env[key]
|
|
343
|
+
if (typeof secret === 'string' && secret) detail = detail.split(secret).join('[redacted]')
|
|
344
|
+
}
|
|
345
|
+
return redactDiagnostic(detail, '', 2_000)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function executionException(error, signal) {
|
|
349
|
+
const detail = executionDiagnostic(error?.message ?? error)
|
|
350
|
+
const cancelled = signal?.aborted === true || error?.name === 'AbortError'
|
|
351
|
+
return { status: cancelled ? 'cancelled' : 'failed', exitCode: null, finalText: '',
|
|
352
|
+
error: `${cancelled ? '官方 CLI 执行已取消' : '官方 CLI 执行器异常'}${detail ? `:${detail}` : '。'}`,
|
|
353
|
+
detail }
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** The ledger accepts this bounded, scrubbed field instead of raw CLI streams. */
|
|
357
|
+
function withFailureDiagnostic(result) {
|
|
358
|
+
if (result.status === 'succeeded') return result
|
|
359
|
+
const diagnostic = executionDiagnostic(result.diagnostic ?? verifiedDiagnostic(result))
|
|
360
|
+
return { ...result, ...(result.error ? { error: executionDiagnostic(result.error) } : {}),
|
|
361
|
+
...(result.reason ? { reason: executionDiagnostic(result.reason) } : {}),
|
|
362
|
+
...(diagnostic ? { diagnostic } : {}) }
|
|
363
|
+
}
|
|
364
|
+
|
|
269
365
|
/**
|
|
270
366
|
* Copy the changes of an earlier, never integrated worktree into a fresh one.
|
|
271
367
|
* Refuses when the original checkout moved since that run (the patch would be
|
|
@@ -279,11 +375,25 @@ async function seedWorktree(isolated, seedFrom, signal) {
|
|
|
279
375
|
let prior
|
|
280
376
|
try { prior = await canonicalDirectory(seedFrom.workspace) }
|
|
281
377
|
catch { throw new Error('原运行的独立工作区已不存在,无法套用之前的改动;请重新执行整个团队任务。') }
|
|
282
|
-
//
|
|
283
|
-
|
|
284
|
-
const
|
|
378
|
+
// A basename is not ownership evidence: require this repository's membership
|
|
379
|
+
// and an exact canonical parent derived from this source, including old runs.
|
|
380
|
+
const listed = await git(['worktree', 'list', '--porcelain', '-z'], { cwd: isolated.source, signal })
|
|
381
|
+
const worktrees = listed.ok ? listed.output.toString('utf8').split('\0').filter(line => line.startsWith('worktree ')).map(line => line.slice(9)) : []
|
|
285
382
|
const known = await Promise.all(worktrees.map(path => canonicalDirectory(path).catch(() => null)))
|
|
286
|
-
|
|
383
|
+
const expectedParents = [join(isolated.source, '.model-router-workspaces'),
|
|
384
|
+
join(dirname(isolated.source), '.model-router-workspaces', basename(isolated.source))]
|
|
385
|
+
if (process.platform === 'win32') {
|
|
386
|
+
try { expectedParents.push(await windowsStorageParent(isolated.source)) } catch { /* a removed/redirected Host parent is not trusted */ }
|
|
387
|
+
}
|
|
388
|
+
const parents = await Promise.all(expectedParents.map(async path => {
|
|
389
|
+
try {
|
|
390
|
+
const canonical = await canonicalDirectory(path)
|
|
391
|
+
return pathIdentity(canonical) === pathIdentity(path) ? canonical : null
|
|
392
|
+
} catch { return null }
|
|
393
|
+
}))
|
|
394
|
+
if (!known.some(path => path && pathIdentity(path) === pathIdentity(prior))
|
|
395
|
+
|| !/^run-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(basename(prior))
|
|
396
|
+
|| !parents.some(path => path && pathIdentity(path) === pathIdentity(dirname(prior)))) {
|
|
287
397
|
throw new Error('原独立工作区不是本仓库由插件创建的 Git 工作树,拒绝读取。')
|
|
288
398
|
}
|
|
289
399
|
const staged = await git(['add', '-A'], { cwd: prior, signal })
|
|
@@ -301,7 +411,9 @@ async function seedWorktree(isolated, seedFrom, signal) {
|
|
|
301
411
|
}
|
|
302
412
|
|
|
303
413
|
/** Sequential execution preserves DAG dependencies and avoids edit conflicts. */
|
|
304
|
-
export async function runOfficialTeam({ plan, task, workspace, allowedRoot, mode = 'read-only', installedIds = [], cliModels, signal, sandbox, runtime, previous = [], onlyIds = null, seedFrom = null }) {
|
|
414
|
+
export async function runOfficialTeam({ plan, task, workspace, allowedRoot, mode = 'read-only', executionEnvironment = 'harness', installedIds = [], cliModels, signal, sandbox, runtime, previous = [], onlyIds = null, seedFrom = null }) {
|
|
415
|
+
if (!['harness', 'cli-native'].includes(executionEnvironment)
|
|
416
|
+
|| (executionEnvironment === 'cli-native' && mode !== 'workspace-write')) throw new TypeError('invalid team CLI execution environment')
|
|
305
417
|
// A retry runs only `onlyIds`; earlier results feed dependency context. An
|
|
306
418
|
// editable retry needs `seedFrom` (the earlier, never integrated worktree and
|
|
307
419
|
// its base commit): a fresh worktree gets that worktree's changes first, so
|
|
@@ -310,22 +422,23 @@ export async function runOfficialTeam({ plan, task, workspace, allowedRoot, mode
|
|
|
310
422
|
if (retry && mode !== 'read-only' && !(seedFrom && typeof seedFrom.workspace === 'string' && typeof seedFrom.baseCommit === 'string')) {
|
|
311
423
|
throw new TypeError('an editable team retry needs the earlier isolated worktree (seedFrom)')
|
|
312
424
|
}
|
|
313
|
-
const selected = executableTeamPackages(plan, installedIds, mode)
|
|
425
|
+
const selected = executableTeamPackages(plan, installedIds, mode, executionEnvironment)
|
|
314
426
|
const assigned = retry ? selected.assigned.filter(item => onlyIds.includes(item.id)) : selected.assigned
|
|
315
427
|
const blocking = retry ? selected.blocking.filter(item => onlyIds.includes(item.id)) : selected.blocking
|
|
316
428
|
if (assigned.length === 0) return { status: 'blocked', blocking: [{ reason: '计划没有工作包。' }], results: [] }
|
|
317
429
|
if (blocking.length > 0) return { status: 'blocked', blocking, results: [] }
|
|
318
430
|
if (mode !== 'read-only' && mode !== 'workspace-write') throw new TypeError('team mode must be read-only or workspace-write')
|
|
319
431
|
const configuredCliModels = checkedTeamCliModels(cliModels, assigned)
|
|
320
|
-
const source = await
|
|
321
|
-
if (typeof sandbox?.confine !== 'function') return { status: 'blocked', blocking: [{ reason: '官方 Harness 进程沙箱不可用。' }], results: [] }
|
|
432
|
+
const source = await approvedSource(workspace, allowedRoot)
|
|
433
|
+
if (executionEnvironment === 'harness' && typeof sandbox?.confine !== 'function') return { status: 'blocked', blocking: [{ reason: '官方 Harness 进程沙箱不可用。' }], results: [] }
|
|
322
434
|
// The Host passes no runtime override. Tests can inject inert CLI adapters to
|
|
323
435
|
// exercise assignment, stopping, and Git integration without provider accounts.
|
|
324
436
|
const readinessCheck = runtime?.readiness ?? officialToolReadiness
|
|
325
437
|
const runTool = runtime?.runTool ?? runOfficialTool
|
|
326
|
-
const readiness = await Promise.all([...new Set(assigned.map(item => item.toolId))].map(id => readinessCheck(id, source)))
|
|
327
|
-
const unready = readiness.filter(item => !item.ready)
|
|
328
|
-
if (unready.length > 0) return { status: 'blocked', blocking: unready.map(item => ({ toolId: item.id,
|
|
438
|
+
const readiness = await Promise.all([...new Set(assigned.map(item => item.toolId))].map(id => readinessCheck(id, source, { executionEnvironment })))
|
|
439
|
+
const unready = readiness.filter(item => !item.ready || !(item.readyModes ?? item.modes ?? [mode]).includes(mode))
|
|
440
|
+
if (unready.length > 0) return { status: 'blocked', blocking: unready.map(item => ({ toolId: item.id,
|
|
441
|
+
reason: item.reason ?? `官方 CLI 尚未就绪,或不支持 ${mode} 模式。` })), results: [] }
|
|
329
442
|
const isolated = mode === 'workspace-write' ? await isolatedWorktree(source, signal, allowedRoot) : null
|
|
330
443
|
const runWorkspace = isolated?.workspace ?? source
|
|
331
444
|
let seeded = null
|
|
@@ -339,18 +452,28 @@ export async function runOfficialTeam({ plan, task, workspace, allowedRoot, mode
|
|
|
339
452
|
const completed = []
|
|
340
453
|
const context = retry ? (Array.isArray(previous) ? previous : []).filter(item => item?.id && typeof item.finalText === 'string') : []
|
|
341
454
|
for (const item of assigned) {
|
|
342
|
-
if (signal?.aborted) return { status: 'cancelled', workspace: runWorkspace, results: completed
|
|
455
|
+
if (signal?.aborted) return { status: 'cancelled', workspace: runWorkspace, results: completed,
|
|
456
|
+
...(isolated ? { baseCommit: isolated.baseCommit } : {}), ...(seeded ? { seeded } : {}) }
|
|
343
457
|
const cliModel = teamCliModel(item, configuredCliModels)
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
458
|
+
let result
|
|
459
|
+
try {
|
|
460
|
+
result = await runTool({
|
|
461
|
+
toolId: item.toolId,
|
|
462
|
+
modelId: cliModel,
|
|
463
|
+
task: packagePrompt(task, item, [...context, ...completed]),
|
|
464
|
+
workspace: runWorkspace,
|
|
465
|
+
mode,
|
|
466
|
+
isolatedRoot: isolated?.isolatedRoot,
|
|
467
|
+
sandbox,
|
|
468
|
+
signal,
|
|
469
|
+
executionEnvironment,
|
|
470
|
+
})
|
|
471
|
+
if (!result || typeof result.status !== 'string') throw new TypeError('官方 CLI 执行器没有返回有效状态。')
|
|
472
|
+
} catch (error) {
|
|
473
|
+
// Keep earlier paid results and the isolated worktree available for the
|
|
474
|
+
// ledger and retry even when a later adapter rejects before returning.
|
|
475
|
+
result = executionException(error, signal)
|
|
476
|
+
}
|
|
354
477
|
const reportedModel = reportedCliModel(item.toolId, result.reportedModel)
|
|
355
478
|
const modelMismatch = result.status === 'succeeded' && cliModel && reportedModel
|
|
356
479
|
&& cliModel !== reportedModel
|
|
@@ -358,19 +481,26 @@ export async function runOfficialTeam({ plan, task, workspace, allowedRoot, mode
|
|
|
358
481
|
completed.push({ id: item.id, name: item.name,
|
|
359
482
|
...(item.objective ? { objective: item.objective } : {}), provider: item.recommendedProvider,
|
|
360
483
|
recommendedModel: item.recommendedModel, toolId: item.toolId,
|
|
361
|
-
requestedCliModel: result.requestedModel
|
|
484
|
+
requestedCliModel: Object.hasOwn(result, 'requestedModel') ? result.requestedModel : cliModel,
|
|
362
485
|
actualModel: reportedModel,
|
|
486
|
+
...(result.reportedModel !== undefined ? { reportedModel: result.reportedModel } : {}),
|
|
487
|
+
...(result.usageIncomplete === true ? { usageIncomplete: true } : {}),
|
|
488
|
+
...(typeof result.source === 'string' ? { source: result.source } : {}),
|
|
489
|
+
...(typeof result.sandboxEnforcement === 'string' ? { sandboxEnforcement: result.sandboxEnforcement } : {}),
|
|
490
|
+
executionEnvironment,
|
|
363
491
|
modelNotice: modelMismatch
|
|
364
492
|
? `官方 CLI 回报模型 ${reportedModel},与本包指定的 ${cliModel} 不一致;后续工作包已停止。`
|
|
365
493
|
: result.modelNotice ?? (!cliModel
|
|
366
494
|
? 'Harness 推荐模型 ID 未经此厂商 CLI 验证;本包使用厂商 CLI 已配置的默认模型。'
|
|
367
495
|
: reportedModel ? null : '该 CLI 未返回可核验的实际模型 ID;请以厂商运行记录核对。'),
|
|
368
496
|
status: packageStatus, finalText: result.finalText ?? '',
|
|
497
|
+
exitCode: result.exitCode ?? null,
|
|
369
498
|
...(result.usage ? { usage: result.usage } : {}),
|
|
370
499
|
...(Number.isFinite(result.reportedCostUsd) ? { reportedCostUsd: result.reportedCostUsd } : {}),
|
|
371
|
-
error: modelMismatch ? '官方 CLI 实际模型与指定模型不一致。'
|
|
372
|
-
|
|
373
|
-
|
|
500
|
+
error: modelMismatch ? '官方 CLI 实际模型与指定模型不一致。'
|
|
501
|
+
: (result.error || result.reason) ? executionDiagnostic(result.error || result.reason) : null,
|
|
502
|
+
outputTail: result.status === 'succeeded' ? null : executionDiagnostic(verifiedDiagnostic(result)) || null })
|
|
503
|
+
if (packageStatus !== 'succeeded') return { status: packageStatus === 'cancelled' ? 'cancelled' : 'incomplete', workspace: runWorkspace, results: completed,
|
|
374
504
|
...(isolated ? { baseCommit: isolated.baseCommit } : {}), ...(seeded ? { seeded } : {}) }
|
|
375
505
|
}
|
|
376
506
|
if (!isolated) return { status: 'cli-completed', workspace: runWorkspace, results: completed,
|
|
@@ -390,14 +520,24 @@ export async function runOfficialTeam({ plan, task, workspace, allowedRoot, mode
|
|
|
390
520
|
}
|
|
391
521
|
|
|
392
522
|
/** One editable CLI task also uses the same isolated workspace and merge gate. */
|
|
393
|
-
export async function runOfficialTask({ toolId, task, workspace, allowedRoot, modelId, mode = 'read-only', signal, sandbox }) {
|
|
394
|
-
const source = await
|
|
523
|
+
export async function runOfficialTask({ toolId, task, workspace, allowedRoot, modelId, mode = 'read-only', signal, sandbox, runtime }) {
|
|
524
|
+
const source = await approvedSource(workspace, allowedRoot)
|
|
395
525
|
if (typeof sandbox?.confine !== 'function') return { toolId, status: 'unsupported', reason: '官方 Harness 进程沙箱不可用。' }
|
|
396
|
-
|
|
397
|
-
|
|
526
|
+
// The Host supplies no runtime override; offline tests use inert adapters.
|
|
527
|
+
const readiness = await (runtime?.readiness ?? officialToolReadiness)(toolId, source)
|
|
528
|
+
const readyModes = readiness.readyModes ?? readiness.modes ?? []
|
|
529
|
+
if (!readiness.ready || !readyModes.includes(mode)) return { toolId, status: 'unsupported',
|
|
530
|
+
reason: readiness.reason ?? readiness.modeReasons?.[mode] ?? `官方 CLI 尚未就绪,或不支持 ${mode} 模式。` }
|
|
398
531
|
const isolated = mode === 'workspace-write' ? await isolatedWorktree(source, signal, allowedRoot) : null
|
|
399
|
-
|
|
400
|
-
|
|
532
|
+
let result
|
|
533
|
+
try {
|
|
534
|
+
result = await (runtime?.runTool ?? runOfficialTool)({ toolId, task, modelId, workspace: isolated?.workspace ?? source,
|
|
535
|
+
mode, isolatedRoot: isolated?.isolatedRoot, signal, sandbox })
|
|
536
|
+
if (!result || typeof result.status !== 'string') throw new TypeError('官方 CLI 执行器没有返回有效状态。')
|
|
537
|
+
} catch (error) {
|
|
538
|
+
result = executionException(error, signal)
|
|
539
|
+
}
|
|
540
|
+
result = withFailureDiagnostic(result)
|
|
401
541
|
if (!isolated || result.status !== 'succeeded') return { ...result, ...(isolated ? { isolatedWorkspace: isolated.workspace, baseCommit: isolated.baseCommit } : {}) }
|
|
402
542
|
try {
|
|
403
543
|
const integration = await integrateWorktree(source, isolated.workspace, isolated.baseCommit, signal)
|