@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.
- package/.dsh-plugin/client.js +903 -463
- package/.dsh-plugin/index.mjs +40 -8
- package/.dsh-plugin/official-tools-remote-service.mjs +1 -1
- package/.dsh-plugin/shared/harness-plan.mjs +26 -11
- package/.dsh-plugin/shared/model-profiles.mjs +101 -0
- package/.dsh-plugin/shared/official-team-runtime.mjs +31 -9
- package/.dsh-plugin/shared/official-tool-executor.mjs +50 -4
- package/.dsh-plugin/shared/official-tools-remote.mjs +1 -1
- package/.dsh-plugin/shared/official-tools-runtime.mjs +159 -9
- package/.dsh-plugin/shared/router.mjs +196 -72
- package/.dsh-plugin/shared/vendor-mimo-grok-adapter.mjs +2 -2
- package/INSTALLATION_GUIDE.zh.md +9 -9
- package/MIGRATION.md +2 -2
- package/README.md +7 -7
- package/README.zh.md +7 -7
- package/package.json +60 -59
package/.dsh-plugin/index.mjs
CHANGED
|
@@ -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 [
|
|
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}
|
|
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: '
|
|
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
|
-
|
|
474
|
-
|
|
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 [
|
|
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: '
|
|
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
|
|
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, {
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
285
|
-
modelNotice:
|
|
304
|
+
actualModel: reportedModel,
|
|
305
|
+
modelNotice: modelMismatch
|
|
306
|
+
? `官方 CLI 回报模型 ${reportedModel},与本包指定的 ${cliModel} 不一致;后续工作包已停止。`
|
|
307
|
+
: result.modelNotice ?? (!cliModel
|
|
286
308
|
? 'Harness 推荐模型 ID 未经此厂商 CLI 验证;本包使用厂商 CLI 已配置的默认模型。'
|
|
287
|
-
:
|
|
288
|
-
status:
|
|
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 (
|
|
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
|
-
|
|
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('
|
|
118
|
+
descriptor('installTool', [toolIdParameter], installResultCodec),
|
|
119
119
|
descriptor('cancel', [toolIdParameter], installResultCodec),
|
|
120
120
|
descriptor('status', [toolIdParameter], statusResultCodec),
|
|
121
121
|
descriptor('galReply', [galRequestParameter], galResultCodec),
|