@ljwei-stak/model-router-galgame 0.8.0 → 0.9.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.
@@ -2,6 +2,7 @@ import z from '@deepseek-ai/schemastery'
2
2
  import { defineTool } from '@deepseek-ai/dsh-tools'
3
3
  import { DEFAULT_ROUTER_SETTINGS } from './shared/router.mjs'
4
4
  import { createPlanFromRoutes } from './shared/harness-plan.mjs'
5
+ import { applyModelProfiles, parseModelProfilesJson } from './shared/model-profiles.mjs'
5
6
  import { registerOfficialToolsRemote } from './official-tools-remote-service.mjs'
6
7
  import {
7
8
  getOfficialTool,
@@ -26,6 +27,7 @@ export const inject = ['commands', 'llm', 'tools', 'typert', 'sandboxPolicy', 's
26
27
  export const Config = z.object({
27
28
  budgetUsd: z.number().min(0).max(1_000_000).default(DEFAULT_ROUTER_SETTINGS.budgetUsd).volatile(),
28
29
  maxConsultOutputChars: z.number().step(1).min(500).max(50_000).default(12_000).volatile(),
30
+ modelProfilesJson: z.string().max(32_000).default('[]').volatile(),
29
31
  })
30
32
 
31
33
  const JSON_OUTPUT = {
@@ -67,6 +69,11 @@ function routeKey(route) {
67
69
  return `${route.provider}\u0000${route.model}`
68
70
  }
69
71
 
72
+ function configuredRoutesWithProfiles(routes, config) {
73
+ const profiles = parseModelProfilesJson(valueOf(config, 'modelProfilesJson', '[]'))
74
+ return applyModelProfiles(routes, profiles)
75
+ }
76
+
70
77
  function uniqueStrings(values) {
71
78
  return [...new Set(values.map(text).filter(Boolean))]
72
79
  }
@@ -118,12 +125,13 @@ export async function createRoutePlan(ctx, task, config = {}, options = {}) {
118
125
  const mode = options.mode === 'team' ? 'team' : 'single'
119
126
  const configuredBudget = valueOf(config, 'budgetUsd', DEFAULT_ROUTER_SETTINGS.budgetUsd)
120
127
  const budgetUsd = Math.max(0, finiteNumber(options.budgetUsd, finiteNumber(configuredBudget, 0)))
121
- const [availableRoutes, installed] = await Promise.all([
128
+ const [discoveredRoutes, installed] = await Promise.all([
122
129
  discoverConfiguredRoutes(ctx, options.signal),
123
130
  options.skipToolProbe === true
124
131
  ? Promise.resolve(Array.isArray(options.installedToolIds) ? options.installedToolIds : [])
125
132
  : installedToolIds(),
126
133
  ])
134
+ const availableRoutes = configuredRoutesWithProfiles(discoveredRoutes, config)
127
135
  const readiness = options.skipToolProbe === true ? []
128
136
  : await Promise.all(installed.map(id => officialToolReadiness(id, options.workspace ?? process.cwd())))
129
137
  const executable = new Set(Array.isArray(options.runnableToolIds)
@@ -228,6 +236,25 @@ function chooseConsultRoute(plan, routes, agent) {
228
236
  return routes.find(differs) ?? preferred ?? routes[0]
229
237
  }
230
238
 
239
+ /** Manual package > manual tool > saved route mapping > CLI default. */
240
+ export function resolveTeamCliModelBindings(plan, routes, requested = {}) {
241
+ if (!requested || typeof requested !== 'object' || Array.isArray(requested)) {
242
+ throw new Error('cliModelsJson 必须是以工作包或官方工具 ID 为键的 JSON 对象')
243
+ }
244
+ const byRoute = new Map(routes.map(route => [routeKey(route), route]))
245
+ const bindings = Object.create(null)
246
+ for (const item of plan.team.workPackages) {
247
+ const toolId = toolForProvider(item.recommendedProvider)?.id
248
+ const profile = byRoute.get(`${item.recommendedProvider}\u0000${item.recommendedModel}`)
249
+ if (profile?.cliModel && toolId !== 'zcode') bindings[item.id] = profile.cliModel
250
+ if (Object.hasOwn(requested, toolId)) bindings[item.id] = requested[toolId]
251
+ if (Object.hasOwn(requested, item.id)) bindings[item.id] = requested[item.id]
252
+ }
253
+ // Keep the supplied keys so the runtime can reject unknown tool/package IDs.
254
+ return { ...bindings, ...Object.fromEntries(Object.entries(requested)
255
+ .filter(([key]) => !Object.hasOwn(bindings, key))) }
256
+ }
257
+
231
258
  function commandText(plan) {
232
259
  const selected = plan.selected ? `${plan.selected.provider}/${plan.selected.model}` : '没有可用路线'
233
260
  const channel = plan.executionChannel === 'official-cli'
@@ -236,7 +263,7 @@ function commandText(plan) {
236
263
  return [
237
264
  `推荐路线:${selected}`,
238
265
  `复杂度:${plan.complexity.band};任务类型:${plan.taskType}`,
239
- `执行渠道:${channel};估算成本:$${plan.estimatedCost.toFixed(6)}(仅估算)`,
266
+ `执行渠道:${channel};估算成本:${plan.estimatedCost === null ? '价格资料不足' : `$${plan.estimatedCost.toFixed(6)}(仅估算)`}`,
240
267
  `工作包:${plan.subtasks.map(item => `${item.name} → ${item.recommendedProvider}/${item.recommended}`).join(';')}`,
241
268
  plan.team.handoff,
242
269
  ].join('\n')
@@ -295,7 +322,7 @@ export function apply(ctx, config = {}) {
295
322
  }))
296
323
  ctx.tools.register(defineTool({
297
324
  name: 'model_router_plan',
298
- description: 'Recommend configured Harness model routes for a task and optionally produce work packages for official Agent Teams. Costs are local estimates.',
325
+ description: 'Analyze task difficulty, recommend only configured Harness model routes, and optionally split compound work into dependent packages. Cost estimates require user-supplied USD prices for the exact routes.',
299
326
  parameters: {
300
327
  task: { type: 'string', required: true, description: 'Task to analyze.' },
301
328
  mode: { type: 'string', enum: ['single', 'team'], description: 'Use team to produce Agent Teams work packages.' },
@@ -469,9 +496,12 @@ function registerOfficialToolModels(ctx, config) {
469
496
  if (!text(args.provider) || !text(args.model)) throw new Error('provider 和 model 必须同时提供')
470
497
  const tool = toolForProvider(args.provider)
471
498
  if (tool?.id !== args.tool) throw new Error('所选模型供应商与官方 CLI 工具不匹配')
472
- const routes = await discoverConfiguredRoutes(ctx, exec.signal)
473
- if (!routes.some(route => route.provider === args.provider && route.model === args.model)) throw new Error('所选 provider/model 不在官方模型目录中')
474
- modelId = args.tool === 'claude-code' || args.tool === 'codex' ? args.model : null
499
+ const routes = configuredRoutesWithProfiles(await discoverConfiguredRoutes(ctx, exec.signal), config)
500
+ const route = routes.find(item => item.provider === args.provider && item.model === args.model)
501
+ if (!route) throw new Error('所选 provider/model 不在官方模型目录中')
502
+ modelId = route.cliModel && args.tool !== 'zcode'
503
+ ? route.cliModel
504
+ : args.tool === 'claude-code' || args.tool === 'codex' ? args.model : null
475
505
  }
476
506
  if (text(args.cliModel)) {
477
507
  if (!text(args.provider) || !text(args.model)) throw new Error('cliModel 需要同时提供已配置的 provider 和 model 路线')
@@ -499,7 +529,8 @@ function registerOfficialToolModels(ctx, config) {
499
529
  const { cwd, root, sandboxMode } = await sessionWorkspace(ctx, exec)
500
530
  const mode = args.mode === 'workspace-write' ? 'workspace-write' : 'read-only'
501
531
  if (mode === 'workspace-write' && sandboxMode === 'read-only') throw new Error('当前 Harness 会话为只读模式,不能请求可编辑团队执行')
502
- const [routes, installed] = await Promise.all([discoverConfiguredRoutes(ctx, exec.signal), installedToolIds()])
532
+ const [discoveredRoutes, installed] = await Promise.all([discoverConfiguredRoutes(ctx, exec.signal), installedToolIds()])
533
+ const routes = configuredRoutesWithProfiles(discoveredRoutes, config)
503
534
  const readiness = await Promise.all(installed.map(id => officialToolReadiness(id, cwd)))
504
535
  const supported = new Set(readiness.filter(item => item.ready).map(item => item.id))
505
536
  const capabilities = new Map(officialToolExecutionCapabilities().map(item => [item.id, item]))
@@ -524,10 +555,11 @@ function registerOfficialToolModels(ctx, config) {
524
555
  try { cliModels = JSON.parse(bindingsText) }
525
556
  catch { throw new Error('cliModelsJson 不是有效的 JSON 对象') }
526
557
  }
558
+ cliModels = resolveTeamCliModelBindings(plan, executableRoutes, cliModels ?? {})
527
559
  const execution = await runOfficialTeam({ plan, task: args.task, workspace: cwd,
528
560
  allowedRoot: root, mode, installedIds: installed, cliModels, signal: exec.signal, sandbox: ctx.sandbox })
529
561
  return jsonValue({ plan, execution,
530
- modelNotice: '团队向 Claude/Codex 请求 Harness 推荐模型 ID;其他厂商默认使用 CLI 已配置模型。cliModelsJson 可按工具或工作包指定准确 CLI 模型名。实际模型须以各厂商记录核对。',
562
+ modelNotice: '已配置 cliModel 的路线按工作包传给官方 CLI;Claude/Codex 在未配置映射时请求 Harness 模型 ID。其他厂商无映射时使用 CLI 默认模型。多数 CLI 尚不返回可核验的实际模型 ID,须以厂商运行记录核对。',
531
563
  billingNotice: 'budgetUsd 仅影响估算与路由,无法限制官方 CLI 账号实际费用。' })
532
564
  },
533
565
  }))
@@ -40,7 +40,7 @@ export class OfficialToolsRemoteService extends TypertRemoteService {
40
40
  }
41
41
 
42
42
  /** Start one serialized fixed-registry install; return immediately for UI polling. */
43
- async install(toolId) {
43
+ async installTool(toolId) {
44
44
  try {
45
45
  await ensureNpmPrefixOnPath()
46
46
  return { accepted: true, job: startInstall(toolId) }
@@ -44,7 +44,10 @@ function annotate(channel) {
44
44
  * by the Desktop panel. Model discovery stays on the official side of each
45
45
  * runtime and only the public provider/model directory crosses this boundary.
46
46
  */
47
- export function createPlanFromRoutes(task, availableRoutes, { mode = 'single', budgetUsd = 0, installedToolIds = [], runnableToolIds = [] } = {}) {
47
+ export function createPlanFromRoutes(task, availableRoutes, {
48
+ mode = 'single', budgetUsd = 0, installedToolIds = [], runnableToolIds = [],
49
+ pricing = {}, liveBench = null, cacheReadRatio = 0, cacheWriteRatio = 0,
50
+ } = {}) {
48
51
  const taskText = clean(task)
49
52
  if (!taskText) throw new Error('task must contain text')
50
53
  const selectedMode = mode === 'team' ? 'team' : 'single'
@@ -57,16 +60,15 @@ export function createPlanFromRoutes(task, availableRoutes, { mode = 'single', b
57
60
  return channelCache.get(key)
58
61
  }
59
62
  const needsImage = detectTaskTypes(taskText).includes('vision')
60
- const eligibleRoutes = needsImage
61
- ? routes.filter(route => !Array.isArray(route.inputModalities)
62
- || route.inputModalities.length === 0
63
- || route.inputModalities.includes('image'))
64
- : routes
65
63
  const plan = buildPlan({
66
64
  text: taskText,
67
- available: eligibleRoutes,
65
+ available: routes,
68
66
  mode: selectedMode,
69
67
  budgetUsd: Math.max(0, Number.isFinite(budgetUsd) ? budgetUsd : 0),
68
+ pricing,
69
+ liveBench,
70
+ cacheReadRatio,
71
+ cacheWriteRatio,
70
72
  })
71
73
  const selectedChannel = plan.selected ? channelOf(plan.selected.provider) : null
72
74
  return {
@@ -75,24 +77,37 @@ export function createPlanFromRoutes(task, availableRoutes, { mode = 'single', b
75
77
  availableRoutes: routes,
76
78
  ...(selectedChannel ? annotate(selectedChannel) : {}),
77
79
  availabilityNotice: '模型目录列出的路线尚未验证当前凭据和网络;实际可用性以官方适配器调用结果为准。',
78
- pricingNotice: '费用是本地估算,不是供应商账单,也不是硬性支出上限。',
79
- modalityNotice: needsImage && eligibleRoutes.length < routes.length
80
- ? '图像任务已排除明确声明不接受图像输入的模型;未声明能力的模型仍需人工验证。'
80
+ pricingNotice: plan.estimatedCost === null
81
+ ? '部分路线尚未配置该供应商的美元输入/输出单价,无法计算可靠的总费用与节省比例;请在模型价格设置中补齐。'
82
+ : '费用按已提供的美元单价和估计 token 数计算,不是供应商账单,也不是硬性支出上限。',
83
+ qualityNotice: plan.optimization.qualityEvidenceComplete
84
+ ? '模型质量使用已提供评分或基准数据估计,仍需实际任务验证。'
85
+ : '部分模型质量缺少可核验评分;目录启发式只供选择参考,质量门槛和节省比例无法保证。',
86
+ modalityNotice: needsImage
87
+ ? plan.unassignableTasks.length > 0
88
+ ? '图像工作包没有可确认支持图像输入的路线,当前计划无法完整分配;请在官方模型目录配置支持图像的模型。'
89
+ : routes.some(route => !Array.isArray(route.inputModalities) || route.inputModalities.length === 0)
90
+ ? '图像工作包只分给已声明图像能力或未声明输入能力的模型;未声明能力的模型仍需实际验证。其他文本工作包可继续使用经济型文本模型。'
91
+ : '图像工作包只分给明确支持图像输入的模型;其他文本工作包可继续使用经济型文本模型。'
81
92
  : null,
82
93
  toolNotice: '执行渠道按官方工具注册表和已核验适配器标注:official-cli 表示该厂商官方 CLI 已安装且可托管执行;harness-llm 表示通过官方模型目录调用。可在工作台查看安装与执行支持状态。',
83
94
  team: {
84
95
  requested: selectedMode === 'team',
85
96
  recommended: selectedMode === 'team' && plan.complexity.band === 'complex' && plan.subtasks.length > 1,
86
97
  handoff: '可在官方会话调用 model_router_team_execute 托管执行当前平台支持的官方 CLI 工作包;官方 Agent Teams 可协作管理任务,但成员模型由宿主配置,不能直接按本计划逐个切换。',
87
- workPackages: plan.subtasks.map(item => ({
98
+ workPackages: plan.subtasks.map((item, index) => ({
88
99
  id: item.id,
89
100
  name: item.name,
90
101
  ...(item.objective ? { objective: item.objective } : {}),
91
102
  type: item.type,
92
103
  purpose: item.purpose,
104
+ difficulty: item.difficulty,
105
+ qualitySource: item.qualitySource,
106
+ pricingSource: item.pricingSource,
93
107
  dependsOn: item.dependsOn,
94
108
  recommendedProvider: item.recommendedProvider,
95
109
  recommendedModel: item.recommended,
110
+ estimatedCost: plan.costBreakdown[index]?.estimatedCost ?? null,
96
111
  ...item.recommendedReasoningEffort ? { recommendedReasoningEffort: item.recommendedReasoningEffort } : {},
97
112
  ...annotate(channelOf(item.recommendedProvider)),
98
113
  verificationChecklist: item.purpose === 'synthesis'
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Optional, user supplied information for exact routes in the Harness model
3
+ * directory. Harness does not publish prices or comparative quality scores.
4
+ * This file never accepts credentials, endpoints, packages or shell commands.
5
+ */
6
+ import { toolForProvider } from './official-tool-registry.mjs'
7
+ const MAX_TEXT = 32_000
8
+ const MAX_PROFILES = 200
9
+ const id = value => typeof value === 'string' ? value.trim() : ''
10
+
11
+ function nonnegative(value, label) {
12
+ if (typeof value !== 'number' || !Number.isFinite(value) || value < 0 || value > 1_000_000) {
13
+ throw new Error(`${label} 必须是 0 到 1000000 之间的有限数字`)
14
+ }
15
+ return value
16
+ }
17
+
18
+ function normalizeProfile(entry, index) {
19
+ const label = `第 ${index + 1} 个模型`
20
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) throw new Error(`${label} 必须是对象`)
21
+ const allowed = new Set(['provider', 'model', 'quality', 'pricing', 'specialties', 'cliModel'])
22
+ const unknown = Object.keys(entry).find(key => !allowed.has(key))
23
+ if (unknown) throw new Error(`${label} 含不支持的字段 ${unknown};不要在这里填写密钥或命令`)
24
+ const provider = id(entry.provider)
25
+ const model = id(entry.model)
26
+ if (!provider || !model || provider.length > 160 || model.length > 240) {
27
+ throw new Error(`${label} 需要模型目录中的准确 provider 和 model`)
28
+ }
29
+ const profile = { provider, model }
30
+ if (entry.quality !== undefined) {
31
+ const value = nonnegative(entry.quality, `${label} 的 quality`)
32
+ if (value > 100) throw new Error(`${label} 的 quality 应在 0 到 100 之间`)
33
+ profile.quality = value / 100
34
+ }
35
+ if (entry.pricing !== undefined) {
36
+ const prices = entry.pricing
37
+ if (!prices || typeof prices !== 'object' || Array.isArray(prices)) throw new Error(`${label} 的 pricing 必须是对象`)
38
+ const priceFields = new Set(['input', 'output', 'cacheRead', 'cacheWrite', 'currency'])
39
+ const unknownPrice = Object.keys(prices).find(key => !priceFields.has(key))
40
+ if (unknownPrice) throw new Error(`${label} 的 pricing 含不支持的字段 ${unknownPrice}`)
41
+ if (prices.currency !== undefined && prices.currency !== 'USD') throw new Error(`${label} 的价格币种目前仅支持 USD`)
42
+ profile.pricing = {
43
+ input: nonnegative(prices.input, `${label} 的输入单价`),
44
+ output: nonnegative(prices.output, `${label} 的输出单价`),
45
+ currency: 'USD',
46
+ }
47
+ if (prices.cacheRead !== undefined) profile.pricing.cacheRead = nonnegative(prices.cacheRead, `${label} 的缓存读取单价`)
48
+ if (prices.cacheWrite !== undefined) profile.pricing.cacheWrite = nonnegative(prices.cacheWrite, `${label} 的缓存写入单价`)
49
+ }
50
+ if (entry.specialties !== undefined) {
51
+ if (!Array.isArray(entry.specialties) || entry.specialties.length > 16
52
+ || entry.specialties.some(value => !/^[a-z][a-z0-9-]{0,39}$/.test(value))) {
53
+ throw new Error(`${label} 的 specialties 必须是至多 16 个英文任务标签`)
54
+ }
55
+ profile.specialties = [...new Set(entry.specialties)]
56
+ }
57
+ if (entry.cliModel !== undefined) {
58
+ const cliModel = id(entry.cliModel)
59
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,119}$/.test(cliModel)) throw new Error(`${label} 的 cliModel 无效`)
60
+ if (toolForProvider(provider)?.id === 'zcode') throw new Error(`${label} 对应的 ZCode CLI 暂不支持逐次切换模型`)
61
+ profile.cliModel = cliModel
62
+ }
63
+ if (Object.keys(profile).length === 2) throw new Error(`${label} 至少提供 quality、pricing、specialties 或 cliModel 之一`)
64
+ return profile
65
+ }
66
+
67
+ /** Parse a settings field; an empty value intentionally means no overrides. */
68
+ export function parseModelProfilesJson(value) {
69
+ const source = value == null || value === '' ? '[]' : value
70
+ if (typeof source !== 'string' || source.length > MAX_TEXT) throw new Error('模型价格与能力配置超过 32000 字符上限')
71
+ let input
72
+ try { input = JSON.parse(source) } catch { throw new Error('模型价格与能力配置不是有效的 JSON') }
73
+ if (!Array.isArray(input) || input.length > MAX_PROFILES) throw new Error('模型价格与能力配置必须是最多 200 项的 JSON 数组')
74
+ const profiles = input.map(normalizeProfile)
75
+ const seen = new Set()
76
+ for (const profile of profiles) {
77
+ const key = `${profile.provider}\0${profile.model}`
78
+ if (seen.has(key)) throw new Error(`模型 ${profile.provider}/${profile.model} 重复配置`)
79
+ seen.add(key)
80
+ }
81
+ return profiles
82
+ }
83
+
84
+ /** Apply metadata only to exact routes actually returned by Harness. */
85
+ export function applyModelProfiles(routes, profiles) {
86
+ const input = Array.isArray(routes) ? routes : []
87
+ const values = typeof profiles === 'string' ? parseModelProfilesJson(profiles) : profiles
88
+ if (!Array.isArray(values)) throw new Error('模型配置必须是数组')
89
+ const byKey = new Map(values.map(profile => [`${profile.provider}\0${profile.model}`, profile]))
90
+ return input.map(route => {
91
+ const profile = byKey.get(`${route.provider}\0${route.model}`)
92
+ if (!profile) return route
93
+ return {
94
+ ...route,
95
+ ...(profile.quality === undefined ? {} : { quality: profile.quality, qualitySource: 'user' }),
96
+ ...(profile.pricing === undefined ? {} : { pricing: { ...profile.pricing }, pricingSource: 'user' }),
97
+ ...(profile.specialties === undefined ? {} : { specialties: [...profile.specialties] }),
98
+ ...(profile.cliModel === undefined ? {} : { cliModel: profile.cliModel }),
99
+ }
100
+ })
101
+ }
@@ -207,6 +207,18 @@ function teamCliModel(item, cliModels) {
207
207
  ? item.recommendedModel : null
208
208
  }
209
209
 
210
+ function reportedCliModel(toolId, value) {
211
+ if (typeof value === 'string' && value.trim()) return value.trim()
212
+ // MiniMax's verified exec.result.model is a structured selection, while
213
+ // its --model argument is one provider/model string.
214
+ if (toolId === 'minimax-code' && value && typeof value === 'object'
215
+ && typeof value.providerId === 'string' && value.providerId
216
+ && typeof value.modelId === 'string' && value.modelId) {
217
+ return `${value.providerId}/${value.modelId}`
218
+ }
219
+ return null
220
+ }
221
+
210
222
  function packagePrompt(task, item, completed) {
211
223
  const dependencies = item.dependsOn.map(id => completed.find(result => result.id === id)).filter(Boolean)
212
224
  const directions = {
@@ -250,7 +262,7 @@ export function executableTeamPackages(plan, installedIds = [], mode = 'read-onl
250
262
  }
251
263
 
252
264
  /** Sequential execution preserves DAG dependencies and avoids edit conflicts. */
253
- export async function runOfficialTeam({ plan, task, workspace, allowedRoot, mode = 'read-only', installedIds = [], cliModels, signal, sandbox }) {
265
+ export async function runOfficialTeam({ plan, task, workspace, allowedRoot, mode = 'read-only', installedIds = [], cliModels, signal, sandbox, runtime }) {
254
266
  const { assigned, blocking } = executableTeamPackages(plan, installedIds, mode)
255
267
  if (assigned.length === 0) return { status: 'blocked', blocking: [{ reason: '计划没有工作包。' }], results: [] }
256
268
  if (blocking.length > 0) return { status: 'blocked', blocking, results: [] }
@@ -258,7 +270,11 @@ export async function runOfficialTeam({ plan, task, workspace, allowedRoot, mode
258
270
  const configuredCliModels = checkedTeamCliModels(cliModels, assigned)
259
271
  const source = await canonicalDirectory(workspace)
260
272
  if (typeof sandbox?.confine !== 'function') return { status: 'blocked', blocking: [{ reason: '官方 Harness 进程沙箱不可用。' }], results: [] }
261
- const readiness = await Promise.all([...new Set(assigned.map(item => item.toolId))].map(id => officialToolReadiness(id, source)))
273
+ // The Host passes no runtime override. Tests can inject inert CLI adapters to
274
+ // exercise assignment, stopping, and Git integration without provider accounts.
275
+ const readinessCheck = runtime?.readiness ?? officialToolReadiness
276
+ const runTool = runtime?.runTool ?? runOfficialTool
277
+ const readiness = await Promise.all([...new Set(assigned.map(item => item.toolId))].map(id => readinessCheck(id, source)))
262
278
  const unready = readiness.filter(item => !item.ready)
263
279
  if (unready.length > 0) return { status: 'blocked', blocking: unready.map(item => ({ toolId: item.id, reason: item.reason })), results: [] }
264
280
  const isolated = mode === 'workspace-write' ? await isolatedWorktree(source, signal, allowedRoot) : null
@@ -267,7 +283,7 @@ export async function runOfficialTeam({ plan, task, workspace, allowedRoot, mode
267
283
  for (const item of assigned) {
268
284
  if (signal?.aborted) return { status: 'cancelled', workspace: runWorkspace, results: completed }
269
285
  const cliModel = teamCliModel(item, configuredCliModels)
270
- const result = await runOfficialTool({
286
+ const result = await runTool({
271
287
  toolId: item.toolId,
272
288
  modelId: cliModel,
273
289
  task: packagePrompt(task, item, completed),
@@ -277,18 +293,24 @@ export async function runOfficialTeam({ plan, task, workspace, allowedRoot, mode
277
293
  sandbox,
278
294
  signal,
279
295
  })
296
+ const reportedModel = reportedCliModel(item.toolId, result.reportedModel)
297
+ const modelMismatch = result.status === 'succeeded' && cliModel && reportedModel
298
+ && cliModel !== reportedModel
299
+ const packageStatus = modelMismatch ? 'model-mismatch' : result.status
280
300
  completed.push({ id: item.id, name: item.name,
281
301
  ...(item.objective ? { objective: item.objective } : {}), provider: item.recommendedProvider,
282
302
  recommendedModel: item.recommendedModel, toolId: item.toolId,
283
303
  requestedCliModel: result.requestedModel ?? null,
284
- actualModel: result.reportedModel ?? null,
285
- modelNotice: result.modelNotice ?? (!cliModel
304
+ actualModel: reportedModel,
305
+ modelNotice: modelMismatch
306
+ ? `官方 CLI 回报模型 ${reportedModel},与本包指定的 ${cliModel} 不一致;后续工作包已停止。`
307
+ : result.modelNotice ?? (!cliModel
286
308
  ? 'Harness 推荐模型 ID 未经此厂商 CLI 验证;本包使用厂商 CLI 已配置的默认模型。'
287
- : result.reportedModel ? null : '该 CLI 未返回可核验的实际模型 ID;请以厂商运行记录核对。'),
288
- status: result.status, finalText: result.finalText ?? '',
289
- error: result.error ?? result.reason ?? null,
309
+ : reportedModel ? null : '该 CLI 未返回可核验的实际模型 ID;请以厂商运行记录核对。'),
310
+ status: packageStatus, finalText: result.finalText ?? '',
311
+ error: modelMismatch ? '官方 CLI 实际模型与指定模型不一致。' : result.error ?? result.reason ?? null,
290
312
  outputTail: result.status === 'succeeded' ? null : result.stderrTail ?? result.stdoutTail ?? null })
291
- if (result.status !== 'succeeded') return { status: 'incomplete', workspace: runWorkspace, results: completed }
313
+ if (packageStatus !== 'succeeded') return { status: 'incomplete', workspace: runWorkspace, results: completed }
292
314
  }
293
315
  if (!isolated) return { status: 'cli-completed', workspace: runWorkspace, results: completed,
294
316
  notice: '只读模式已收集各工作包结果;模型输出仍需人工验收。' }
@@ -14,6 +14,7 @@
14
14
  * https://moonshotai.github.io/kimi-code/en/reference/kimi-command.html
15
15
  */
16
16
  import { spawn } from 'node:child_process'
17
+ import { createHash } from 'node:crypto'
17
18
  import { access, readFile, realpath, stat } from 'node:fs/promises'
18
19
  import { constants } from 'node:fs'
19
20
  import { StringDecoder } from 'node:string_decoder'
@@ -34,6 +35,9 @@ const STOP_GRACE_MS = 5_000
34
35
  const CLAUDE_MIN_RESTRICTED_VERSION = [2, 1, 259]
35
36
  const CLAUDE_SIGNER = 'Anthropic, PBC'
36
37
  const CODEX_SIGNER = 'OpenAI OpCo, LLC'
38
+ // SHA-256 of package/cli.js in the official @minimax-ai/code@0.5.5 npm tarball.
39
+ // The Windows installer-managed release carries the exact same entry file.
40
+ const MINIMAX_055_CLI_SHA256 = '8d36dec74e93beddf392459557a7afd8655bcdc874121981608d2c82658814ab'
37
41
  const WINDOWS_SYSTEM32 = join(process.env.SystemRoot ?? 'C:\\Windows', 'System32')
38
42
  const ENVIRONMENT_KEYS = Object.freeze([
39
43
  'PATH', 'Path', 'PATHEXT', 'SystemRoot', 'windir', 'ComSpec',
@@ -187,6 +191,39 @@ async function findGlobalPackageEntries(packageName, binName, expectedBin, works
187
191
  return found
188
192
  }
189
193
 
194
+ /** The official MiniMax Windows installer uses a versioned releases tree rather than npm -g. */
195
+ export async function findManagedMiniMaxEntry(workspace,
196
+ directories = pathDirectories().flatMap(directory => [directory, join(directory, '.minimax-code')]),
197
+ trustedCliSha256 = MINIMAX_055_CLI_SHA256) {
198
+ if (!IS_WINDOWS) return null
199
+ const version = getOfficialTool('minimax-code').version
200
+ for (const directory of directories) {
201
+ try {
202
+ const root = await realpath(directory)
203
+ if (root === workspace || inside(workspace, root)) continue
204
+ if (!(await stat(join(root, 'mcode.cmd'))).isFile()) continue
205
+ const current = (await readFile(join(root, 'current'), 'utf8')).trim()
206
+ if (current !== version) continue
207
+ const releaseRoot = await realpath(join(root, 'releases', version))
208
+ if (!inside(root, releaseRoot)) continue
209
+ const packageRoot = await realpath(join(releaseRoot, 'node_modules', '@minimax-ai', 'code'))
210
+ if (!inside(releaseRoot, packageRoot)) continue
211
+ const manifest = JSON.parse(await readFile(join(packageRoot, 'package.json'), 'utf8'))
212
+ if (manifest.name !== '@minimax-ai/code' || manifest.version !== version
213
+ || String(manifest.bin?.mcode).replace(/^\.\//, '') !== 'cli.js') continue
214
+ const entry = await realpath(join(packageRoot, 'cli.js'))
215
+ if (!inside(packageRoot, entry) || !(await stat(entry)).isFile()) continue
216
+ const digest = createHash('sha256').update(await readFile(entry)).digest('hex')
217
+ if (digest !== trustedCliSha256) continue
218
+ const native = await realpath(join(packageRoot, 'node_modules', 'better-sqlite3',
219
+ 'build', 'Release', 'better_sqlite3.node'))
220
+ if (!inside(packageRoot, native) || !(await stat(native)).isFile()) continue
221
+ return { entry, packageRoot, version, source: 'verified-official-windows-installer-bundle' }
222
+ } catch { /* inspect the next PATH directory */ }
223
+ }
224
+ return null
225
+ }
226
+
190
227
  async function findCodexExecutable(workspace) {
191
228
  await ensureNpmPrefixOnPath()
192
229
  const filename = IS_WINDOWS ? 'codex.exe' : 'codex'
@@ -259,7 +296,14 @@ async function findSignedPackagedCodex(packageRoot, workspace) {
259
296
 
260
297
  /** PATH or package metadata alone is insufficient: verify the Windows EXE publisher. */
261
298
  function hasOfficialWindowsSignature(entry, expectedSigner) {
262
- const script = '$s=Get-AuthenticodeSignature -LiteralPath $env:MODEL_ROUTER_VERIFY_PATH; '
299
+ // Some launch environments inherit a PowerShell module path where the
300
+ // built-in Security module does not autoload. Import its fixed system path
301
+ // explicitly, and fail closed if that import or the signature check fails.
302
+ const modulePath = join(WINDOWS_SYSTEM32, 'WindowsPowerShell', 'v1.0', 'Modules',
303
+ 'Microsoft.PowerShell.Security', 'Microsoft.PowerShell.Security.psd1')
304
+ const script = '$ErrorActionPreference="Stop"; '
305
+ + 'Import-Module -Name $env:MODEL_ROUTER_SECURITY_MODULE -ErrorAction Stop; '
306
+ + '$s=Get-AuthenticodeSignature -LiteralPath $env:MODEL_ROUTER_VERIFY_PATH; '
263
307
  + '@{status=[string]$s.Status;subject=[string]$s.SignerCertificate.Subject} | ConvertTo-Json -Compress'
264
308
  const powershell = join(WINDOWS_SYSTEM32, 'WindowsPowerShell', 'v1.0', 'powershell.exe')
265
309
  return new Promise(resolveSignature => {
@@ -269,7 +313,8 @@ function hasOfficialWindowsSignature(entry, expectedSigner) {
269
313
  shell: false,
270
314
  windowsHide: true,
271
315
  stdio: ['ignore', 'pipe', 'ignore'],
272
- env: { ...process.env, MODEL_ROUTER_VERIFY_PATH: entry },
316
+ env: { ...process.env, MODEL_ROUTER_VERIFY_PATH: entry,
317
+ MODEL_ROUTER_SECURITY_MODULE: modulePath },
273
318
  })
274
319
  } catch { resolveSignature(false); return }
275
320
  let output = ''
@@ -369,6 +414,7 @@ async function launchSpec(toolId, workspace, mode, modelId, task = 'Check readin
369
414
  const tool = getOfficialTool(toolId)
370
415
  const entries = await findGlobalPackageEntries(tool.package, 'mcode', 'cli.js', workspace)
371
416
  const found = entries.find(entry => entry.version === tool.version)
417
+ ?? await findManagedMiniMaxEntry(workspace)
372
418
  if (!found) return { unsupported: `未找到固定版本 ${tool.package}@${tool.version} 的官方入口。` }
373
419
  const nodeExecutable = await findNodeExecutable(workspace, [22, 19, 0], version => {
374
420
  const major = Number(String(version).split('.')[0])
@@ -378,10 +424,10 @@ async function launchSpec(toolId, workspace, mode, modelId, task = 'Check readin
378
424
  if (modelId && !modelId.includes('/')) {
379
425
  return { unsupported: 'MiniMax CLI 要求 provider/model;当前模型目录 ID 不能直接传入,请在 MiniMax 中配置别名。' }
380
426
  }
381
- return buildMiniMaxInvocation({
427
+ return { ...buildMiniMaxInvocation({
382
428
  nodeExecutable, cliEntry: found.entry, workspace, mode,
383
429
  modelReference: modelId, permission: mode === 'workspace-write' ? 'full' : 'smart',
384
- })
430
+ }), source: found.source }
385
431
  }
386
432
  if (toolId === 'mimo-code' || toolId === 'grok-build') {
387
433
  try {
@@ -115,7 +115,7 @@ const galRequestIdParameter = Object.freeze({
115
115
 
116
116
  export const OFFICIAL_TOOLS_REMOTE_DESCRIPTORS = Object.freeze([
117
117
  descriptor('list', [], listResultCodec),
118
- descriptor('install', [toolIdParameter], installResultCodec),
118
+ descriptor('installTool', [toolIdParameter], installResultCodec),
119
119
  descriptor('cancel', [toolIdParameter], installResultCodec),
120
120
  descriptor('status', [toolIdParameter], statusResultCodec),
121
121
  descriptor('galReply', [galRequestParameter], galResultCodec),