@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
package/.dsh-plugin/index.mjs
CHANGED
|
@@ -23,6 +23,7 @@ import { routeBoundaries } from './shared/security-boundaries.mjs'
|
|
|
23
23
|
import { createPlanFromRoutes } from './shared/harness-plan.mjs'
|
|
24
24
|
import { applyModelProfiles, parseModelProfilesJson } from './shared/model-profiles.mjs'
|
|
25
25
|
import { registerOfficialToolsRemote } from './official-tools-remote-service.mjs'
|
|
26
|
+
import { parseRunRequest } from './shared/official-tools-remote.mjs'
|
|
26
27
|
import { createTerminalManager } from './shared/cli-terminal.mjs'
|
|
27
28
|
import {
|
|
28
29
|
parseTerminalRead,
|
|
@@ -42,7 +43,7 @@ import {
|
|
|
42
43
|
} from './shared/official-tool-registry.mjs'
|
|
43
44
|
import { parseInstallPreferences, resolveInstallPlan } from './shared/tool-install-preferences.mjs'
|
|
44
45
|
import { officialToolExecutionCapabilities, officialToolReadiness, runOfficialTool } from './shared/official-tool-executor.mjs'
|
|
45
|
-
import { runOfficialTask, runOfficialTeam, sessionWorkspace } from './shared/official-team-runtime.mjs'
|
|
46
|
+
import { preflightOfficialWorkspace, runOfficialTask, runOfficialTeam, sessionWorkspace } from './shared/official-team-runtime.mjs'
|
|
46
47
|
import { downstreamPackageIds, executeAssignmentPlan, rerunAssignmentPackage, taskTextProblem } from './shared/task-executors.mjs'
|
|
47
48
|
import {
|
|
48
49
|
probeAllTools,
|
|
@@ -95,6 +96,7 @@ export const Config = z.object({
|
|
|
95
96
|
toolInstallMethodsJson: z.string().max(8_000).default('{}').volatile(),
|
|
96
97
|
toolScriptUrlsJson: z.string().max(8_000).default('{}').volatile(),
|
|
97
98
|
toolAllowScriptInstall: z.boolean().default(true).volatile(),
|
|
99
|
+
zcodeInstallDir: z.string().max(4_096).default('').volatile(),
|
|
98
100
|
})
|
|
99
101
|
|
|
100
102
|
const JSON_OUTPUT = {
|
|
@@ -386,9 +388,21 @@ export function billingForResult(result) {
|
|
|
386
388
|
})
|
|
387
389
|
}
|
|
388
390
|
|
|
389
|
-
function routePricing(routes, item) {
|
|
390
|
-
const
|
|
391
|
-
|
|
391
|
+
export function routePricing(routes, item) {
|
|
392
|
+
const matches = routes.filter(route => {
|
|
393
|
+
if (route.provider !== item?.provider) return false
|
|
394
|
+
if (item?.channel !== 'official-cli') return route.model === item?.model
|
|
395
|
+
// A routing recommendation cannot establish which model the CLI billed.
|
|
396
|
+
return typeof item.actualModel === 'string' && [route.model, route.cliModel, `${route.provider}/${route.model}`].includes(item.actualModel)
|
|
397
|
+
})
|
|
398
|
+
// Different catalog routes can share a CLI alias. Conflicting prices are
|
|
399
|
+
// not evidence of what the CLI actually billed.
|
|
400
|
+
const prices = new Set(matches.map(route => JSON.stringify(route.pricing ?? null)))
|
|
401
|
+
if (prices.size > 1) return null
|
|
402
|
+
const route = matches[0]
|
|
403
|
+
return route?.pricing ? { ...route.pricing, provider: route.provider, model: route.model,
|
|
404
|
+
...(route.cliModel ? { cliModel: route.cliModel } : {}),
|
|
405
|
+
source: route.pricingSource ?? 'route', version: route.pricingVersion ?? null, asOf: route.snapshotAsOf ?? null } : null
|
|
392
406
|
}
|
|
393
407
|
|
|
394
408
|
function feedbackOptions(config) {
|
|
@@ -462,7 +476,7 @@ export async function createRoutePlan(ctx, task, config = {}, options = {}) {
|
|
|
462
476
|
const readiness = options.skipToolProbe === true ? []
|
|
463
477
|
: await Promise.all(installed.map(id => officialToolReadiness(id, options.workspace ?? process.cwd())))
|
|
464
478
|
const executable = new Set(Array.isArray(options.runnableToolIds)
|
|
465
|
-
? options.runnableToolIds : readiness.filter(item => item.ready).map(item => item.id))
|
|
479
|
+
? options.runnableToolIds : readiness.filter(item => item.ready && (item.readyModes ?? item.modes ?? []).includes('read-only')).map(item => item.id))
|
|
466
480
|
const loggedOut = options.skipToolProbe === true
|
|
467
481
|
? (Array.isArray(options.loggedOutToolIds) ? options.loggedOutToolIds : [])
|
|
468
482
|
: loggedOutIds(await currentHealth())
|
|
@@ -698,7 +712,7 @@ export async function planAssignment(ctx, task, config = {}, options = {}) {
|
|
|
698
712
|
: await Promise.all(installed.map(id => officialToolReadiness(id, options.workspace ?? process.cwd())))
|
|
699
713
|
const runnableToolIds = Array.isArray(options.runnableToolIds)
|
|
700
714
|
? options.runnableToolIds
|
|
701
|
-
: readiness.filter(item => item.ready).map(item => item.id)
|
|
715
|
+
: readiness.filter(item => item.ready && (item.readyModes ?? item.modes ?? []).includes('read-only')).map(item => item.id)
|
|
702
716
|
const loggedOut = options.skipToolProbe === true
|
|
703
717
|
? (Array.isArray(options.loggedOutToolIds) ? options.loggedOutToolIds : [])
|
|
704
718
|
: loggedOutIds(await currentHealth())
|
|
@@ -822,7 +836,7 @@ export function rerunSupport(run) {
|
|
|
822
836
|
return { ok: true, kind: run.kind === 'team' ? 'team' : 'assign', writes: false }
|
|
823
837
|
}
|
|
824
838
|
|
|
825
|
-
export async function rerunRecordedStep(ctx, config = {}, { runId, packageId, provider, model, confirmOverBudget = false, confirmWrite = false, subscriptionChoice = null, workspace, root, signal, ...options } = {}) {
|
|
839
|
+
export async function rerunRecordedStep(ctx, config = {}, { runId, packageId, provider, model, confirmOverBudget = false, confirmWrite = false, confirmUnsandboxed = false, subscriptionChoice = null, workspace, root, signal, ...options } = {}) {
|
|
826
840
|
const { run, saved } = await storedRun(runId)
|
|
827
841
|
const target = run.packages.find(item => item.id === text(packageId))
|
|
828
842
|
if (!target) throw new Error(`运行记录中没有工作包 ${text(packageId)}`)
|
|
@@ -853,9 +867,12 @@ export async function rerunRecordedStep(ctx, config = {}, { runId, packageId, pr
|
|
|
853
867
|
if (support.kind === 'team-write' && target.ok) {
|
|
854
868
|
throw new Error('可编辑团队运行只能续跑失败或未完成的步骤:已成功步骤的改动已在原独立工作区中,重跑会重复套用。如需重做请重新调用 model_router_team_execute。')
|
|
855
869
|
}
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
870
|
+
const missingConfirmations = []
|
|
871
|
+
if (support.writes && confirmWrite !== true) missingConfirmations.push('workspace-write')
|
|
872
|
+
if (run.kind === 'team' && run.executionEnvironment === 'cli-native' && confirmUnsandboxed !== true) missingConfirmations.push('unsandboxed')
|
|
873
|
+
if (missingConfirmations.length) {
|
|
874
|
+
return { paused: true, needsConfirmation: missingConfirmations, run,
|
|
875
|
+
budget: null, message: '请确认续跑所需的文件修改与执行环境权限后继续;厂商 CLI 正常环境以当前用户权限运行,独立 Git 工作树不是操作系统读写边界。' }
|
|
859
876
|
}
|
|
860
877
|
const sameRoute = !override || (override.provider === target.recommendedProvider && override.model === target.recommendedModel)
|
|
861
878
|
const budget = budgetFor(config, saved, sameRoute ? target.estimatedCost : null)
|
|
@@ -929,16 +946,21 @@ function teamPlanFromRun(run, ids, override, targetId) {
|
|
|
929
946
|
*/
|
|
930
947
|
async function rerunTeamStep(ctx, { run, target, override, routes, budget, cwd, root, signal, options, config = {} }) {
|
|
931
948
|
const mode = run.executionMode === 'workspace-write' ? 'workspace-write' : 'read-only'
|
|
949
|
+
const executionEnvironment = teamExecutionEnvironment(run.executionEnvironment, mode)
|
|
932
950
|
const downstream = downstreamPackageIds(run.packages, target.id)
|
|
933
951
|
.filter(id => !run.packages.find(item => item.id === id)?.ok)
|
|
934
952
|
const ids = [target.id, ...downstream]
|
|
935
953
|
const plan = teamPlanFromRun(run, ids, override, target.id)
|
|
936
954
|
const cliModels = Object.fromEntries(Object.entries(run.cliModels ?? {}).filter(([key]) => ids.includes(key) && !(override && key === target.id)))
|
|
955
|
+
if (override) {
|
|
956
|
+
const reassignedPlan = { team: { workPackages: plan.team.workPackages.filter(item => item.id === target.id) } }
|
|
957
|
+
Object.assign(cliModels, resolveTeamCliModelBindings(reassignedPlan, routes))
|
|
958
|
+
}
|
|
937
959
|
const previous = run.packages.filter(item => item.ok && !ids.includes(item.id))
|
|
938
960
|
.map(item => ({ id: item.id, name: item.name, finalText: item.answer }))
|
|
939
961
|
const runTeam = options.runTeam ?? runOfficialTeam
|
|
940
962
|
const installed = Array.isArray(options.installedToolIds) ? options.installedToolIds : await installedToolIds()
|
|
941
|
-
const execution = await runTeam({ plan, task: run.task, workspace: cwd, allowedRoot: root ?? cwd, mode,
|
|
963
|
+
const execution = await runTeam({ plan, task: run.task, workspace: cwd, allowedRoot: root ?? cwd, mode, executionEnvironment,
|
|
942
964
|
installedIds: installed, cliModels, signal, sandbox: ctx.sandbox, previous, onlyIds: ids,
|
|
943
965
|
...(mode === 'workspace-write' ? { seedFrom: { workspace: run.isolatedWorkspace, baseCommit: run.baseCommit } } : {}),
|
|
944
966
|
...(options.runtime ? { runtime: options.runtime } : {}) })
|
|
@@ -960,6 +982,7 @@ async function rerunTeamStep(ctx, { run, target, override, routes, budget, cwd,
|
|
|
960
982
|
if (stored) stored.billingSwitch = note
|
|
961
983
|
}
|
|
962
984
|
current.status = execution?.status ?? current.status
|
|
985
|
+
current.executionEnvironment = executionEnvironment
|
|
963
986
|
if (mode === 'workspace-write' && execution?.workspace && execution.status !== 'blocked') {
|
|
964
987
|
current.isolatedWorkspace = String(execution.workspace)
|
|
965
988
|
if (typeof execution.baseCommit === 'string') current.baseCommit = execution.baseCommit
|
|
@@ -968,6 +991,10 @@ async function rerunTeamStep(ctx, { run, target, override, routes, budget, cwd,
|
|
|
968
991
|
if (override) {
|
|
969
992
|
const stored = current.packages.find(item => item.id === target.id)
|
|
970
993
|
if (stored) { stored.recommendedProvider = override.provider; stored.recommendedModel = override.model }
|
|
994
|
+
const bindings = { ...(current.cliModels ?? {}) }
|
|
995
|
+
delete bindings[target.id]
|
|
996
|
+
if (cliModels[target.id]) bindings[target.id] = cliModels[target.id]
|
|
997
|
+
current.cliModels = bindings
|
|
971
998
|
}
|
|
972
999
|
})
|
|
973
1000
|
return { run: updated, execution: { ...converted, ranIds: ids, raw: execution }, budget }
|
|
@@ -1010,13 +1037,14 @@ async function annotateCliQuota(run, config, failures) {
|
|
|
1010
1037
|
}
|
|
1011
1038
|
}
|
|
1012
1039
|
|
|
1013
|
-
export async function recordTeamRun({ task, plan, execution, mode, workspace, routes, cliModels, budget, startedAt, preset, config = {} }) {
|
|
1040
|
+
export async function recordTeamRun({ task, plan, execution, mode, executionEnvironment = 'harness', workspace, routes, cliModels, budget, startedAt, preset, config = {} }) {
|
|
1014
1041
|
const run = buildTeamRunRecord({
|
|
1015
1042
|
id: randomUUID(), createdAt: startedAt, finishedAt: Date.now(), task, plan, execution, mode, workspace,
|
|
1016
1043
|
preset: plan?.preset ?? preset ?? DEFAULT_ROUTING_PRESET,
|
|
1017
1044
|
pricingFor: item => routePricing(routes, item), billingFor: billingForResult,
|
|
1018
1045
|
budget: budget ? { estimateUsd: budget.estimateUsd, exceeded: budget.exceeded, downgraded: false, confirmed: budget.exceeded ? true : undefined } : null,
|
|
1019
1046
|
})
|
|
1047
|
+
run.executionEnvironment = teamExecutionEnvironment(executionEnvironment, mode ?? 'read-only')
|
|
1020
1048
|
const bindings = Object.fromEntries(Object.entries(cliModels ?? {}).filter(([, value]) => typeof value === 'string' && value))
|
|
1021
1049
|
if (Object.keys(bindings).length) run.cliModels = bindings
|
|
1022
1050
|
await annotateCliQuota(run, config, (Array.isArray(execution?.results) ? execution.results : [])
|
|
@@ -1048,9 +1076,7 @@ export async function planToolRun(ctx, config, args = {}, { signal } = {}) {
|
|
|
1048
1076
|
routes = await routesWithLearning(ctx, config, signal, refreshed.runs, refreshed.dynamicData, refreshed.routingLearning)
|
|
1049
1077
|
route = routes.find(item => item.provider === text(args.provider) && item.model === text(args.model)) ?? null
|
|
1050
1078
|
if (!route) throw new Error('所选 provider/model 不在官方模型目录中')
|
|
1051
|
-
modelId = route.cliModel && toolId !== 'zcode'
|
|
1052
|
-
? route.cliModel
|
|
1053
|
-
: toolId === 'claude-code' || toolId === 'codex' ? route.model : null
|
|
1079
|
+
modelId = route.cliModel && toolId !== 'zcode' ? route.cliModel : null
|
|
1054
1080
|
}
|
|
1055
1081
|
if (text(args.cliModel)) {
|
|
1056
1082
|
if (!route) throw new Error('cliModel 需要同时提供已配置的 provider 和 model 路线')
|
|
@@ -1060,7 +1086,7 @@ export async function planToolRun(ctx, config, args = {}, { signal } = {}) {
|
|
|
1060
1086
|
const estimate = route ? createPlanFromRoutes(taskText, routes, { mode: 'direct', directProvider: route.provider, directModel: route.model }).estimatedCost : null
|
|
1061
1087
|
const budget = budgetFor(config, await savedState(), Number.isFinite(estimate) ? estimate : null)
|
|
1062
1088
|
return { toolId, tool, taskText, mode, modelId, route, routes, estimate: Number.isFinite(estimate) ? estimate : null, budget,
|
|
1063
|
-
provider: route?.provider ?? '', model: route?.model ?? '', cliModel:
|
|
1089
|
+
provider: route?.provider ?? '', model: route?.model ?? '', cliModel: modelId }
|
|
1064
1090
|
}
|
|
1065
1091
|
|
|
1066
1092
|
/** Run a planned tool call and record it; `rerunOf` links a rerun to the original run. */
|
|
@@ -1199,6 +1225,103 @@ export function resolveTeamCliModelBindings(plan, routes, requested = {}) {
|
|
|
1199
1225
|
.filter(([key]) => !Object.hasOwn(bindings, key))) }
|
|
1200
1226
|
}
|
|
1201
1227
|
|
|
1228
|
+
/** Only plan team packages on an installed CLI that is ready in this mode. */
|
|
1229
|
+
export function executableTeamRoutes(routes, installed, readiness, mode, capabilities = officialToolExecutionCapabilities()) {
|
|
1230
|
+
const available = new Set(installed)
|
|
1231
|
+
const ready = new Map(readiness.map(item => [item.id, item]))
|
|
1232
|
+
const supported = new Map(capabilities.map(item => [item.id, item]))
|
|
1233
|
+
return routes.filter(route => {
|
|
1234
|
+
const id = toolForProvider(route.provider)?.id
|
|
1235
|
+
const state = ready.get(id)
|
|
1236
|
+
const capability = supported.get(id)
|
|
1237
|
+
const readyModes = state?.readyModes ?? state?.modes ?? capability?.modes ?? []
|
|
1238
|
+
return available.has(id) && state?.ready === true && capability?.modes?.includes(mode)
|
|
1239
|
+
&& readyModes.includes(mode)
|
|
1240
|
+
})
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
function teamExecutionEnvironment(value = 'harness', mode = 'read-only') {
|
|
1244
|
+
if (!['harness', 'cli-native'].includes(value)) throw new Error('团队执行环境必须是 harness 或 cli-native')
|
|
1245
|
+
if (value === 'cli-native' && mode !== 'workspace-write') throw new Error('厂商 CLI 正常环境仅支持官方团队的 workspace-write 模式')
|
|
1246
|
+
return value
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
/** Plan a CLI-only team without starting a provider request or changing files. */
|
|
1250
|
+
export async function planOfficialTeam(ctx, config = {}, args = {}, options = {}) {
|
|
1251
|
+
const taskText = text(args.task)
|
|
1252
|
+
const problem = taskTextProblem(taskText)
|
|
1253
|
+
if (problem) throw new Error(problem)
|
|
1254
|
+
const mode = args.mode ?? args.executionMode ?? 'read-only'
|
|
1255
|
+
if (!['read-only', 'workspace-write'].includes(mode)) throw new Error('团队执行模式必须是 read-only 或 workspace-write')
|
|
1256
|
+
const executionEnvironment = teamExecutionEnvironment(args.executionEnvironment, mode)
|
|
1257
|
+
const workspace = options.workspace ?? process.cwd()
|
|
1258
|
+
throwIfAborted(options.signal)
|
|
1259
|
+
const saved = await routingSavedState(config)
|
|
1260
|
+
const [routes, installed] = await Promise.all([
|
|
1261
|
+
routesWithLearning(ctx, config, options.signal, saved.runs, saved.dynamicData, saved.routingLearning),
|
|
1262
|
+
Array.isArray(options.installedToolIds) ? options.installedToolIds : installedToolIds(),
|
|
1263
|
+
])
|
|
1264
|
+
const readiness = Array.isArray(options.readiness) ? options.readiness
|
|
1265
|
+
: await Promise.all(installed.map(id => (options.readinessFor ?? officialToolReadiness)(id, workspace, { executionEnvironment })))
|
|
1266
|
+
const capabilities = options.executionCapabilities ?? (options.capabilitiesFor ?? officialToolExecutionCapabilities)({ executionEnvironment })
|
|
1267
|
+
const executableRoutes = executableTeamRoutes(routes, installed, readiness, mode, capabilities)
|
|
1268
|
+
const base = { taskText, workspace, mode, executionEnvironment, plan: null, routes: executableRoutes, installed, readiness,
|
|
1269
|
+
executionCapabilities: capabilities, executionReadiness: readiness, cliModels: {}, budget: null }
|
|
1270
|
+
if (executionEnvironment === 'harness' && typeof ctx.sandbox?.confine !== 'function') {
|
|
1271
|
+
return { ...base, status: 'blocked', reason: '官方 Harness 进程沙箱不可用,不能执行官方 CLI 团队。' }
|
|
1272
|
+
}
|
|
1273
|
+
if (executableRoutes.length === 0) return { ...base, status: 'blocked',
|
|
1274
|
+
reason: `官方模型目录中没有同时满足已配置路线、已安装 CLI、托管执行适配器与 ${mode} 模式的供应商。请检查该模式下的 CLI 执行就绪状态。` }
|
|
1275
|
+
const supported = new Set(executableRoutes.map(route => toolForProvider(route.provider).id))
|
|
1276
|
+
const configuredBudget = valueOf(config, 'budgetUsd', DEFAULT_ROUTER_SETTINGS.budgetUsd)
|
|
1277
|
+
const budgetUsd = Math.max(0, finiteNumber(args.budgetUsd, finiteNumber(configuredBudget, 0)))
|
|
1278
|
+
const plan = createPlanFromRoutes(taskText, executableRoutes, {
|
|
1279
|
+
...planDataOptions(saved, config), mode: 'team', budgetUsd, installedToolIds: installed,
|
|
1280
|
+
runnableToolIds: installed.filter(id => supported.has(id)), preset: routingPresetOf(config, args.preset),
|
|
1281
|
+
})
|
|
1282
|
+
const bindingsText = text(args.cliModelsJson)
|
|
1283
|
+
if (bindingsText.length > 4_000) throw new Error('cliModelsJson 超过 4000 字符上限')
|
|
1284
|
+
let requestedModels = {}
|
|
1285
|
+
if (bindingsText) {
|
|
1286
|
+
try { requestedModels = JSON.parse(bindingsText) }
|
|
1287
|
+
catch { throw new Error('cliModelsJson 不是有效的 JSON 对象') }
|
|
1288
|
+
}
|
|
1289
|
+
const cliModels = resolveTeamCliModelBindings(plan, executableRoutes, requestedModels)
|
|
1290
|
+
const budget = budgetFor(config, await savedState(), plan.estimatedCost)
|
|
1291
|
+
return { ...base, status: 'planned', plan, cliModels, budget }
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
/** Execute and record a planned team through official CLIs only. */
|
|
1295
|
+
export async function executeOfficialTeamPlan(ctx, config = {}, planned, options = {}) {
|
|
1296
|
+
if (planned?.status === 'blocked') return { ...planned }
|
|
1297
|
+
if (!planned?.plan || !['read-only', 'workspace-write'].includes(planned.mode)) throw new Error('缺少有效的官方 CLI 团队计划')
|
|
1298
|
+
const executionEnvironment = teamExecutionEnvironment(planned.executionEnvironment, planned.mode)
|
|
1299
|
+
const workspace = options.workspace ?? planned.workspace
|
|
1300
|
+
const budget = budgetFor(config, await savedState(), planned.plan.estimatedCost)
|
|
1301
|
+
const common = { plan: planned.plan, workspace, mode: planned.mode, executionEnvironment, budget }
|
|
1302
|
+
if (budget.exceeded && options.confirmOverBudget !== true) return { ...common, status: 'paused-budget',
|
|
1303
|
+
notice: `${budget.message} 已暂停团队执行。请向用户确认后再执行。` }
|
|
1304
|
+
if (executionEnvironment === 'cli-native' && options.confirmUnsandboxed !== true) return { ...common,
|
|
1305
|
+
status: 'needs-confirmation', missing: ['unsandboxed'],
|
|
1306
|
+
reasons: confirmationReasons(['unsandboxed'], { executionEnvironment }),
|
|
1307
|
+
notice: '厂商 CLI 正常环境必须明确确认后才会启动。' }
|
|
1308
|
+
if (executionEnvironment === 'harness' && typeof ctx.sandbox?.confine !== 'function') return { ...common, status: 'blocked',
|
|
1309
|
+
reason: '官方 Harness 进程沙箱不可用,不能执行官方 CLI 团队。' }
|
|
1310
|
+
throwIfAborted(options.signal)
|
|
1311
|
+
const startedAt = Date.now()
|
|
1312
|
+
const execution = await (options.runTeam ?? runOfficialTeam)({ plan: planned.plan, task: planned.taskText, workspace,
|
|
1313
|
+
allowedRoot: options.root ?? workspace, mode: planned.mode, executionEnvironment, installedIds: planned.installed,
|
|
1314
|
+
cliModels: planned.cliModels, signal: options.signal, sandbox: ctx.sandbox })
|
|
1315
|
+
const run = await recordTeamRun({ task: planned.taskText, plan: planned.plan, execution, mode: planned.mode, executionEnvironment, workspace,
|
|
1316
|
+
routes: planned.routes, cliModels: planned.cliModels, budget, startedAt, config })
|
|
1317
|
+
return { ...common, status: execution.status, execution, run, runId: run?.id ?? null,
|
|
1318
|
+
rerunNotice: planned.mode === 'read-only'
|
|
1319
|
+
? '失败的工作包可用 model_router_rerun_step 单步重跑(只重跑该步及其未完成的下游)。'
|
|
1320
|
+
: '若在某个步骤失败而停止,可用 model_router_rerun_step 在新的独立工作树中套用之前的改动后续跑(需审批);已整合或待人工整合的运行不能单步重跑。',
|
|
1321
|
+
modelNotice: '已配置 cliModel 的路线按工作包传给官方 CLI;所有厂商无映射时使用 CLI 默认模型。请求模型、Harness 推荐模型与 CLI 回报的实际模型分别记录;未核验实际模型时不按推荐路线价格计算实际费用。',
|
|
1322
|
+
billingNotice: 'budgetUsd 仅影响估算与路由,无法限制官方 CLI 账号实际费用。' }
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1202
1325
|
/** What was assigned and why, per package: route, difficulty, estimated cost, channel. */
|
|
1203
1326
|
export function decisionSummary(plan) {
|
|
1204
1327
|
const packages = plan?.routingBypassed
|
|
@@ -1291,12 +1414,28 @@ export function approvalReasonCodes(name, args = {}, config = {}, { rerunRun = n
|
|
|
1291
1414
|
if (name === 'model_router_rerun_step' && args.subscriptionChoice === 'api') codes.push('subscription-api')
|
|
1292
1415
|
if (args.confirmOverBudget === true) codes.push('over-budget')
|
|
1293
1416
|
const directRun = name === 'model_router_execute' || (name === 'model_router_rerun_step' && (!rerunRun || rerunSupport(rerunRun).kind === 'assign'))
|
|
1417
|
+
if ((name === 'model_router_team_execute' && args.executionEnvironment === 'cli-native')
|
|
1418
|
+
|| (name === 'model_router_rerun_step' && rerunRun?.executionEnvironment === 'cli-native')) codes.push('unsandboxed')
|
|
1294
1419
|
if (directRun && valueOf(config, 'confirmUnsandboxedCli', true) !== false && mayLaunchDirectCli(args)) codes.push('unsandboxed')
|
|
1295
1420
|
return codes
|
|
1296
1421
|
}
|
|
1297
1422
|
|
|
1423
|
+
function executionApprovalText(code, sandboxAvailable, executionEnvironment) {
|
|
1424
|
+
if (executionEnvironment === 'cli-native' && ['workspace-write', 'unsandboxed'].includes(code)) {
|
|
1425
|
+
return { reason: 'Run verified official CLIs with the current user permissions, outside the Harness process sandbox',
|
|
1426
|
+
zh: code === 'workspace-write'
|
|
1427
|
+
? '修改文件:CLI 在独立 Git 工作树执行,成功后核对并整合补丁。工作树用于改动回收,不构成操作系统读写边界。'
|
|
1428
|
+
: '厂商 CLI 正常环境:使用原登录、配置、技能及 MCP,以当前用户权限运行,不经过 Harness 进程沙箱。工具可能访问工作树外当前用户可访问的资源;不会自动切换执行环境。',
|
|
1429
|
+
en: code === 'workspace-write'
|
|
1430
|
+
? 'Edit in an isolated Git worktree and integrate the verified patch. The worktree collects changes; it is not an OS file-access boundary.'
|
|
1431
|
+
: 'Run the verified vendor CLI with its original login, configuration, skills and MCP under the current user permissions, outside the Harness process sandbox. Tools can access resources outside the worktree. This environment is never selected automatically.' }
|
|
1432
|
+
}
|
|
1433
|
+
return APPROVAL_TEXT[code](sandboxAvailable)
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1298
1436
|
function approvalReasons(name, args, config, { sandboxAvailable = false, rerunRun = null } = {}) {
|
|
1299
|
-
return approvalReasonCodes(name, args, config, { rerunRun }).map(code => ({ code,
|
|
1437
|
+
return approvalReasonCodes(name, args, config, { rerunRun }).map(code => ({ code,
|
|
1438
|
+
...executionApprovalText(code, sandboxAvailable, rerunRun?.executionEnvironment ?? args.executionEnvironment) }))
|
|
1300
1439
|
}
|
|
1301
1440
|
|
|
1302
1441
|
/** One approval prompt listing every reason, so the user is asked once. */
|
|
@@ -1317,8 +1456,8 @@ export function combinedAsk(reasons) {
|
|
|
1317
1456
|
}
|
|
1318
1457
|
|
|
1319
1458
|
/** Reasons in the workbench's shape: code plus the Chinese and English text. */
|
|
1320
|
-
export function confirmationReasons(codes, { sandboxAvailable = false } = {}) {
|
|
1321
|
-
return codes.map(code => ({ code,
|
|
1459
|
+
export function confirmationReasons(codes, { sandboxAvailable = false, executionEnvironment = 'harness' } = {}) {
|
|
1460
|
+
return codes.map(code => ({ code, ...executionApprovalText(code, sandboxAvailable, executionEnvironment) }))
|
|
1322
1461
|
}
|
|
1323
1462
|
|
|
1324
1463
|
/**
|
|
@@ -1336,10 +1475,39 @@ export async function workbenchWorkspace(requested, runs = []) {
|
|
|
1336
1475
|
return chosen
|
|
1337
1476
|
}
|
|
1338
1477
|
|
|
1339
|
-
/**
|
|
1340
|
-
|
|
1478
|
+
/** Resolve one fresh preview and retain its exact CLI plan for the confirmed run. */
|
|
1479
|
+
async function prepareWorkbenchPreview(ctx, config, supplied = {}, options = {}) {
|
|
1480
|
+
const request = parseRunRequest(supplied)
|
|
1341
1481
|
const saved = await savedState()
|
|
1342
1482
|
const workspace = await workbenchWorkspace(request.workspace, saved.runs)
|
|
1483
|
+
if (request.executor === 'official-team') {
|
|
1484
|
+
const mode = request.executionMode
|
|
1485
|
+
let planned
|
|
1486
|
+
try {
|
|
1487
|
+
await (options.preflightWorkspace ?? preflightOfficialWorkspace)(workspace, mode, options.signal)
|
|
1488
|
+
planned = await planOfficialTeam(ctx, config, { ...request, mode }, {
|
|
1489
|
+
workspace, signal: options.signal, ...options.planOptions,
|
|
1490
|
+
})
|
|
1491
|
+
} catch (error) {
|
|
1492
|
+
return { request, preview: { executor: 'official-team', executionMode: mode, executionEnvironment: request.executionEnvironment ?? 'harness', workspace,
|
|
1493
|
+
status: 'blocked', reason: errorText(error), decision: { packages: [] }, selected: null,
|
|
1494
|
+
estimatedCost: null, budget: null, reasons: [], readOnly: mode === 'read-only' } }
|
|
1495
|
+
}
|
|
1496
|
+
const decision = decisionSummary(planned.plan)
|
|
1497
|
+
decision.packages = decision.packages.map(item => ({ ...item, channel: 'official-cli',
|
|
1498
|
+
channelDetail: `官方 CLI;${mode === 'workspace-write' ? '在隔离 Git 工作树中使用自己的工具' : '只读检查工程,工具集按各 CLI 只读适配限制;Claude 禁用 shell、技能及既有 MCP,并忽略用户设置'};未配置映射时使用 CLI 默认模型;失败停止,不自动转 API。` }))
|
|
1499
|
+
const codes = planned.status === 'blocked' ? [] : approvalReasonCodes('model_router_team_execute', {
|
|
1500
|
+
mode, executionEnvironment: planned.executionEnvironment, confirmOverBudget: Boolean(planned.budget?.exceeded),
|
|
1501
|
+
}, config)
|
|
1502
|
+
return { request, planned, preview: {
|
|
1503
|
+
executor: 'official-team', executionMode: mode, executionEnvironment: planned.executionEnvironment, workspace, status: planned.status,
|
|
1504
|
+
...(planned.reason ? { reason: planned.reason } : {}),
|
|
1505
|
+
decision, selected: planned.plan?.selected ?? null, routingBypassed: false,
|
|
1506
|
+
estimatedCost: planned.plan?.estimatedCost ?? null, budget: planned.budget,
|
|
1507
|
+
reasons: confirmationReasons(codes, { sandboxAvailable: typeof ctx.sandbox?.confine === 'function', executionEnvironment: planned.executionEnvironment }),
|
|
1508
|
+
readOnly: mode === 'read-only',
|
|
1509
|
+
} }
|
|
1510
|
+
}
|
|
1343
1511
|
const configuredBudget = valueOf(config, 'budgetUsd', DEFAULT_ROUTER_SETTINGS.budgetUsd)
|
|
1344
1512
|
const planned = await planAssignment(ctx, request.task, config, {
|
|
1345
1513
|
provider: request.provider, model: request.model, planMode: request.planMode, preset: request.preset,
|
|
@@ -1347,7 +1515,7 @@ export async function previewWorkbenchRun(ctx, config, request = {}, options = {
|
|
|
1347
1515
|
})
|
|
1348
1516
|
await hydrateHealth()
|
|
1349
1517
|
const codes = approvalReasonCodes('model_router_execute', { provider: request.provider, confirmOverBudget: Boolean(planned.budget.exceeded) }, config)
|
|
1350
|
-
return {
|
|
1518
|
+
return { request, preview: {
|
|
1351
1519
|
workspace,
|
|
1352
1520
|
decision: decisionSummary(planned.plan),
|
|
1353
1521
|
selected: planned.plan.selected ?? null,
|
|
@@ -1356,15 +1524,32 @@ export async function previewWorkbenchRun(ctx, config, request = {}, options = {
|
|
|
1356
1524
|
budget: planned.budget,
|
|
1357
1525
|
reasons: confirmationReasons(codes, { sandboxAvailable: typeof ctx.sandbox?.confine === 'function' }),
|
|
1358
1526
|
readOnly: true,
|
|
1359
|
-
}
|
|
1527
|
+
} }
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
/** Plan preview: no model calls, workspace changes or worktree creation. */
|
|
1531
|
+
export async function previewWorkbenchRun(ctx, config, request = {}, options = {}) {
|
|
1532
|
+
return (await prepareWorkbenchPreview(ctx, config, request, options)).preview
|
|
1360
1533
|
}
|
|
1361
1534
|
|
|
1362
1535
|
/** Execute from the workbench; refuses until every current reason is in confirmedReasons. */
|
|
1363
|
-
export async function startWorkbenchRun(ctx, config,
|
|
1364
|
-
const
|
|
1536
|
+
export async function startWorkbenchRun(ctx, config, supplied = {}, options = {}) {
|
|
1537
|
+
const prepared = await prepareWorkbenchPreview(ctx, config, supplied, options)
|
|
1538
|
+
const { preview, request } = prepared
|
|
1539
|
+
if (preview.status === 'blocked') return preview
|
|
1365
1540
|
const confirmed = new Set(Array.isArray(request.confirmedReasons) ? request.confirmedReasons : [])
|
|
1366
1541
|
const missing = preview.reasons.filter(item => !confirmed.has(item.code))
|
|
1367
|
-
if (missing.length) return { status: 'needs-confirmation',
|
|
1542
|
+
if (missing.length) return { ...preview, status: 'needs-confirmation', reasons: preview.reasons, missing: missing.map(item => item.code) }
|
|
1543
|
+
if (request.executor === 'official-team') {
|
|
1544
|
+
const result = await (options.executeOfficialTeam ?? executeOfficialTeamPlan)(ctx, config, prepared.planned, {
|
|
1545
|
+
workspace: preview.workspace, root: preview.workspace, signal: options.signal,
|
|
1546
|
+
confirmOverBudget: confirmed.has('over-budget'), confirmUnsandboxed: confirmed.has('unsandboxed'), ...(options.teamOptions ?? {}),
|
|
1547
|
+
})
|
|
1548
|
+
return { status: result.status, runId: result.runId ?? null, workspace: preview.workspace,
|
|
1549
|
+
executor: 'official-team', executionMode: request.executionMode, executionEnvironment: preview.executionEnvironment, decision: preview.decision,
|
|
1550
|
+
budget: result.budget ?? preview.budget, ...(result.reason ? { reason: result.reason } : {}),
|
|
1551
|
+
...(result.notice ? { notice: result.notice } : {}) }
|
|
1552
|
+
}
|
|
1368
1553
|
const configuredBudget = valueOf(config, 'budgetUsd', DEFAULT_ROUTER_SETTINGS.budgetUsd)
|
|
1369
1554
|
const result = await (options.execute ?? executeConfiguredAssignment)(ctx, request.task, config, {
|
|
1370
1555
|
provider: request.provider, model: request.model, planMode: request.planMode, preset: request.preset,
|
|
@@ -1387,6 +1572,8 @@ export async function startWorkbenchRun(ctx, config, request = {}, options = {})
|
|
|
1387
1572
|
export function createRouterTerminals(options = {}) {
|
|
1388
1573
|
return createTerminalManager({
|
|
1389
1574
|
prepare: () => ensureNpmPrefixOnPath(),
|
|
1575
|
+
preferences: () => installPreferences(),
|
|
1576
|
+
launchOptions: () => currentInstallOptions(),
|
|
1390
1577
|
onSessionEnd: record => { void routerStateStore().appendTerminalSession(record).catch(() => {}) },
|
|
1391
1578
|
...options,
|
|
1392
1579
|
})
|
|
@@ -1397,11 +1584,14 @@ async function terminalHistory(limit = 20) {
|
|
|
1397
1584
|
return (saved.terminalSessions ?? []).slice(-limit).reverse()
|
|
1398
1585
|
}
|
|
1399
1586
|
|
|
1400
|
-
function terminalServices(terminals) {
|
|
1587
|
+
function terminalServices(terminals, assertPreferences = () => {}) {
|
|
1401
1588
|
if (!terminals) return {}
|
|
1402
1589
|
return {
|
|
1403
|
-
terminalInfo: async () =>
|
|
1404
|
-
|
|
1590
|
+
terminalInfo: async () => {
|
|
1591
|
+
const tools = await probeAllTools({ fresh: true })
|
|
1592
|
+
return { ...terminals.info(), tools, toolsCheckedAt: Date.now(), history: await terminalHistory() }
|
|
1593
|
+
},
|
|
1594
|
+
terminalStart: async request => { assertPreferences(); return terminals.start(parseTerminalStart(request)) },
|
|
1405
1595
|
terminalRead: async request => terminals.read(parseTerminalRead(request)),
|
|
1406
1596
|
terminalWrite: async request => terminals.write(parseTerminalWrite(request)),
|
|
1407
1597
|
terminalResize: async request => terminals.resize(parseTerminalResize(request)),
|
|
@@ -1409,9 +1599,9 @@ function terminalServices(terminals) {
|
|
|
1409
1599
|
}
|
|
1410
1600
|
}
|
|
1411
1601
|
|
|
1412
|
-
export function routerRemoteServices(ctx, config, { terminals = null } = {}) {
|
|
1602
|
+
export function routerRemoteServices(ctx, config, { terminals = null, assertPreferences } = {}) {
|
|
1413
1603
|
return {
|
|
1414
|
-
...terminalServices(terminals),
|
|
1604
|
+
...terminalServices(terminals, assertPreferences),
|
|
1415
1605
|
health: async fresh => {
|
|
1416
1606
|
const report = await toolHealthReport({ fresh: fresh === true })
|
|
1417
1607
|
const saved = await savedState()
|
|
@@ -1444,7 +1634,7 @@ export function apply(ctx, config = {}) {
|
|
|
1444
1634
|
installPreferenceError = null
|
|
1445
1635
|
} catch (error) { installPreferenceError = errorText(error) }
|
|
1446
1636
|
}
|
|
1447
|
-
const installFields = new Set(['toolInstallDir', 'toolNpmRegistry', 'toolInstallMethodsJson', 'toolScriptUrlsJson', 'toolAllowScriptInstall'])
|
|
1637
|
+
const installFields = new Set(['toolInstallDir', 'toolNpmRegistry', 'toolInstallMethodsJson', 'toolScriptUrlsJson', 'toolAllowScriptInstall', 'zcodeInstallDir'])
|
|
1448
1638
|
syncInstallPreferences()
|
|
1449
1639
|
// Loader scopes this event to the fiber whose refs were committed.
|
|
1450
1640
|
ctx.on('loader/volatile-update', paths => {
|
|
@@ -1464,9 +1654,10 @@ export function apply(ctx, config = {}) {
|
|
|
1464
1654
|
return () => { process.off('exit', onExit); terminals.disposeAll('dispose') }
|
|
1465
1655
|
}, 'model-router-galgame: interactive terminals')
|
|
1466
1656
|
registerOfficialToolsRemote(ctx, {
|
|
1467
|
-
...routerRemoteServices(ctx, config, { terminals }),
|
|
1657
|
+
...routerRemoteServices(ctx, config, { terminals, assertPreferences: assertInstallPreferences }),
|
|
1468
1658
|
installSettingsError: () => installPreferenceError,
|
|
1469
1659
|
installRequestedDirectory: () => valueOf(config, 'toolInstallDir', ''),
|
|
1660
|
+
zcodeRequestedDirectory: () => valueOf(config, 'zcodeInstallDir', ''),
|
|
1470
1661
|
})
|
|
1471
1662
|
}
|
|
1472
1663
|
ctx.on('tools/pre-execute', async (exec, next) => {
|
|
@@ -1759,7 +1950,7 @@ function registerOfficialToolModels(ctx, config, assertInstallPreferences) {
|
|
|
1759
1950
|
}))
|
|
1760
1951
|
ctx.tools.register(defineTool({
|
|
1761
1952
|
name: 'model_router_tool_run',
|
|
1762
|
-
description: 'Run one supported official model CLI in the current Harness session workspace. Claude, Codex, MiMo and Grok support read-only; Kimi, MiniMax and ZCode require workspace-write. Write mode needs approval and a clean Git repository. Optional provider/model must match a configured Harness route and selected vendor; cliModel can specify that vendor CLI’s own configured model name.
|
|
1953
|
+
description: 'Run one supported official model CLI in the current Harness session workspace. Claude, Codex, MiMo and Grok support read-only; Kimi, MiniMax and ZCode require workspace-write. Write mode needs approval and a clean Git repository. Optional provider/model must match a configured Harness route and selected vendor; cliModel can specify that vendor CLI’s own configured model name. Every CLI uses its own default unless an explicit cliModel or saved route mapping is configured.',
|
|
1763
1954
|
parameters: {
|
|
1764
1955
|
tool: { type: 'string', required: true, description: 'Fixed registry tool id, e.g. claude-code or codex.' },
|
|
1765
1956
|
task: { type: 'string', required: true, description: 'Concrete task for the official CLI model.' },
|
|
@@ -1860,7 +2051,7 @@ function registerOfficialToolModels(ctx, config, assertInstallPreferences) {
|
|
|
1860
2051
|
// The pre-execute hook already asked the user about file edits for editable reruns.
|
|
1861
2052
|
const result = await rerunRecordedStep(ctx, config, {
|
|
1862
2053
|
runId: args.runId, packageId: args.packageId, provider: args.provider, model: args.model,
|
|
1863
|
-
confirmOverBudget: args.confirmOverBudget === true, confirmWrite: true, workspace: cwd, root, signal: exec.signal,
|
|
2054
|
+
confirmOverBudget: args.confirmOverBudget === true, confirmWrite: true, confirmUnsandboxed: stored?.executionEnvironment === 'cli-native', workspace: cwd, root, signal: exec.signal,
|
|
1864
2055
|
...(text(args.subscriptionChoice) ? { subscriptionChoice: text(args.subscriptionChoice) } : {}),
|
|
1865
2056
|
})
|
|
1866
2057
|
return jsonValue({
|
|
@@ -1887,10 +2078,11 @@ function registerOfficialToolModels(ctx, config, assertInstallPreferences) {
|
|
|
1887
2078
|
}))
|
|
1888
2079
|
ctx.tools.register(defineTool({
|
|
1889
2080
|
name: 'model_router_team_execute',
|
|
1890
|
-
description: 'Plan a complex task into dependent work packages, route among configured providers with ready official CLIs, and run each package sequentially.
|
|
2081
|
+
description: 'Plan a complex task into dependent work packages, route among configured providers with ready official CLIs, and run each package sequentially. Every CLI uses its own default unless a saved cliModel mapping or cliModelsJson supplies exact CLI names. Editable runs use one isolated Git worktree and integrate source changes after CLI success. Confirm the actual model from vendor records.',
|
|
1891
2082
|
parameters: {
|
|
1892
2083
|
task: { type: 'string', required: true, description: 'Full task to plan, distribute and execute.' },
|
|
1893
2084
|
mode: { type: 'string', enum: ['read-only', 'workspace-write'], description: 'Default read-only; workspace-write needs a clean Git repository and approval.' },
|
|
2085
|
+
executionEnvironment: { type: 'string', enum: ['harness', 'cli-native'], description: 'Default harness. cli-native requires workspace-write and separate approval: normal vendor tools run with current user permissions outside Harness sandbox; Git isolation only collects changes.' },
|
|
1894
2086
|
budgetUsd: { type: 'number', description: 'Estimated planning ceiling only, not a vendor billing limit.' },
|
|
1895
2087
|
cliModelsJson: { type: 'string', description: 'Optional JSON object mapping official tool IDs or work package IDs to exact model names configured in those CLIs. MiniMax/MiMo require provider/model; ZCode cannot switch per call.' },
|
|
1896
2088
|
confirmOverBudget: { type: 'boolean', description: 'Set only after the user agreed to exceed the daily/monthly budget. Triggers an approval prompt.' },
|
|
@@ -1900,51 +2092,11 @@ function registerOfficialToolModels(ctx, config, assertInstallPreferences) {
|
|
|
1900
2092
|
const { cwd, root, sandboxMode } = await sessionWorkspace(ctx, exec)
|
|
1901
2093
|
const mode = args.mode === 'workspace-write' ? 'workspace-write' : 'read-only'
|
|
1902
2094
|
if (mode === 'workspace-write' && sandboxMode === 'read-only') throw new Error('当前 Harness 会话为只读模式,不能请求可编辑团队执行')
|
|
1903
|
-
const
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
const executableRoutes = routes.filter(route => {
|
|
1909
|
-
const tool = toolForProvider(route.provider)
|
|
1910
|
-
return tool && installed.includes(tool.id) && supported.has(tool.id)
|
|
1911
|
-
&& capabilities.get(tool.id)?.modes?.includes(mode)
|
|
1912
|
-
})
|
|
1913
|
-
if (executableRoutes.length === 0) return jsonValue({ status: 'blocked',
|
|
1914
|
-
reason: `官方模型目录中没有同时满足已配置路线、已安装 CLI、托管执行适配器与 ${mode} 模式的供应商;Kimi、MiniMax、ZCode 仅支持经审批的 workspace-write。`,
|
|
1915
|
-
installed, executionCapabilities: officialToolExecutionCapabilities(), executionReadiness: readiness })
|
|
1916
|
-
const configuredBudget = valueOf(config, 'budgetUsd', DEFAULT_ROUTER_SETTINGS.budgetUsd)
|
|
1917
|
-
const budgetUsd = Math.max(0, finiteNumber(args.budgetUsd, finiteNumber(configuredBudget, 0)))
|
|
1918
|
-
const plan = createPlanFromRoutes(args.task, executableRoutes, {
|
|
1919
|
-
...planDataOptions(saved, config),
|
|
1920
|
-
mode: 'team', budgetUsd, installedToolIds: installed,
|
|
1921
|
-
runnableToolIds: installed.filter(id => supported.has(id)),
|
|
1922
|
-
preset: routingPresetOf(config),
|
|
1923
|
-
})
|
|
1924
|
-
const budget = budgetFor(config, await savedState(), plan.estimatedCost)
|
|
1925
|
-
if (budget.exceeded && args.confirmOverBudget !== true) {
|
|
1926
|
-
return jsonValue({ status: 'paused-budget', budget, plan,
|
|
1927
|
-
notice: `${budget.message} 已暂停团队执行。请向用户确认后以 confirmOverBudget: true 重新调用。` })
|
|
1928
|
-
}
|
|
1929
|
-
const bindingsText = text(args.cliModelsJson)
|
|
1930
|
-
if (bindingsText.length > 4_000) throw new Error('cliModelsJson 超过 4000 字符上限')
|
|
1931
|
-
let cliModels = null
|
|
1932
|
-
if (bindingsText) {
|
|
1933
|
-
try { cliModels = JSON.parse(bindingsText) }
|
|
1934
|
-
catch { throw new Error('cliModelsJson 不是有效的 JSON 对象') }
|
|
1935
|
-
}
|
|
1936
|
-
cliModels = resolveTeamCliModelBindings(plan, executableRoutes, cliModels ?? {})
|
|
1937
|
-
const startedAt = Date.now()
|
|
1938
|
-
const execution = await runOfficialTeam({ plan, task: args.task, workspace: cwd,
|
|
1939
|
-
allowedRoot: root, mode, installedIds: installed, cliModels, signal: exec.signal, sandbox: ctx.sandbox })
|
|
1940
|
-
const recorded = await recordTeamRun({ task: args.task, plan, execution, mode, workspace: cwd,
|
|
1941
|
-
routes: executableRoutes, cliModels, budget, startedAt, config })
|
|
1942
|
-
return jsonValue({ plan, execution, runId: recorded?.id ?? null,
|
|
1943
|
-
rerunNotice: mode === 'read-only'
|
|
1944
|
-
? '失败的工作包可用 model_router_rerun_step 单步重跑(只重跑该步及其未完成的下游)。'
|
|
1945
|
-
: '若在某个步骤失败而停止,可用 model_router_rerun_step 在新的独立工作树中套用之前的改动后续跑(需审批);已整合或待人工整合的运行不能单步重跑。',
|
|
1946
|
-
modelNotice: '已配置 cliModel 的路线按工作包传给官方 CLI;Claude/Codex 在未配置映射时请求 Harness 模型 ID。其他厂商无映射时使用 CLI 默认模型。多数 CLI 尚不返回可核验的实际模型 ID,须以厂商运行记录核对。',
|
|
1947
|
-
billingNotice: 'budgetUsd 仅影响估算与路由,无法限制官方 CLI 账号实际费用。' })
|
|
2095
|
+
const planned = await planOfficialTeam(ctx, config, { ...args, mode }, { workspace: cwd, signal: exec.signal })
|
|
2096
|
+
return jsonValue(await executeOfficialTeamPlan(ctx, config, planned, {
|
|
2097
|
+
workspace: cwd, root, signal: exec.signal, confirmOverBudget: args.confirmOverBudget === true,
|
|
2098
|
+
confirmUnsandboxed: args.executionEnvironment === 'cli-native',
|
|
2099
|
+
}))
|
|
1948
2100
|
},
|
|
1949
2101
|
}))
|
|
1950
2102
|
}
|
|
@@ -44,6 +44,7 @@ const unavailable = () => { throw new Error('模型路由工作台服务尚未
|
|
|
44
44
|
export function installSummary(preferences = installPreferences(), options = currentInstallOptions()) {
|
|
45
45
|
return {
|
|
46
46
|
installDir: preferences.installDir ?? '',
|
|
47
|
+
zcodeInstallDir: preferences.zcodeInstallDir ?? '',
|
|
47
48
|
registry: preferences.registry ?? '',
|
|
48
49
|
allowScriptInstall: preferences.allowScriptInstall !== false,
|
|
49
50
|
scriptUrls: { ...(preferences.scriptUrls ?? {}) },
|
|
@@ -61,12 +62,14 @@ export function installSummary(preferences = installPreferences(), options = cur
|
|
|
61
62
|
}
|
|
62
63
|
if (tool.manager === 'signed-windows-installer') {
|
|
63
64
|
entry.methodId = 'signed-windows-installer'
|
|
65
|
+
entry.installDir = preferences.zcodeInstallDir ?? ''
|
|
64
66
|
if (options.platform === 'win32') {
|
|
65
67
|
entry.command = installCommandLine(tool)
|
|
66
68
|
entry.updateCommand = entry.command
|
|
67
|
-
entry.notices = [preferences.
|
|
68
|
-
?
|
|
69
|
-
: '
|
|
69
|
+
entry.notices = [preferences.zcodeInstallDir
|
|
70
|
+
? `TUI 使用已保存的 ZCode 桌面目录 ${preferences.zcodeInstallDir}。更新时请在官方安装窗口选择同一目录,完成后重新体检。`
|
|
71
|
+
: '请在下方单独配置 ZCode 桌面目录,或留空自动检测。内置 CLI 随桌面版更新,TUI 的登录、模型和插件在终端中自行配置。']
|
|
72
|
+
entry.notices.push('ZCode 桌面目录独立于统一 npm 安装根;不会把桌面资源放进其他工具的共享目录。')
|
|
70
73
|
} else entry.installError = entry.error = 'ZCode 签名安装器仅支持 Windows。'
|
|
71
74
|
entry.uninstallError = 'ZCode 是桌面安装版,请在系统“应用”中卸载它。'
|
|
72
75
|
return entry
|
|
@@ -191,6 +194,7 @@ export class OfficialToolsRemoteService extends TypertRemoteService {
|
|
|
191
194
|
// The saved input can contain Host-only home/environment expansions. Keep
|
|
192
195
|
// it separate from the resolved directory used by the actual command.
|
|
193
196
|
install.requestedInstallDir = this.services?.installRequestedDirectory?.() ?? install.installDir
|
|
197
|
+
install.requestedZCodeInstallDir = this.services?.zcodeRequestedDirectory?.() ?? install.zcodeInstallDir
|
|
194
198
|
return { install, hostVersion: HOST_PLUGIN_VERSION }
|
|
195
199
|
}
|
|
196
200
|
|