@ljwei-stak/dsh-model-router 0.13.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.
Files changed (37) hide show
  1. package/.dsh-plugin/client.js +3092 -0
  2. package/.dsh-plugin/index.mjs +1651 -0
  3. package/.dsh-plugin/official-tools-remote-service.mjs +104 -0
  4. package/.dsh-plugin/shared/harness-plan.mjs +179 -0
  5. package/.dsh-plugin/shared/livebench.mjs +264 -0
  6. package/.dsh-plugin/shared/model-profiles.mjs +142 -0
  7. package/.dsh-plugin/shared/official-team-runtime.mjs +411 -0
  8. package/.dsh-plugin/shared/official-tool-executor.mjs +801 -0
  9. package/.dsh-plugin/shared/official-tool-registry.mjs +138 -0
  10. package/.dsh-plugin/shared/official-tools-remote.mjs +173 -0
  11. package/.dsh-plugin/shared/official-tools-runtime.mjs +642 -0
  12. package/.dsh-plugin/shared/router-state.mjs +207 -0
  13. package/.dsh-plugin/shared/router.mjs +1134 -0
  14. package/.dsh-plugin/shared/routing-presets.mjs +49 -0
  15. package/.dsh-plugin/shared/run-ledger.mjs +348 -0
  16. package/.dsh-plugin/shared/security-boundaries.mjs +54 -0
  17. package/.dsh-plugin/shared/subscription-billing.mjs +340 -0
  18. package/.dsh-plugin/shared/task-executors.mjs +1154 -0
  19. package/.dsh-plugin/shared/tool-health.mjs +311 -0
  20. package/.dsh-plugin/shared/vendor-mimo-grok-adapter.mjs +308 -0
  21. package/.dsh-plugin/shared/vendor-minimax-adapter.mjs +247 -0
  22. package/.dsh-plugin/shared/zcode-bundle.mjs +208 -0
  23. package/.dsh-plugin/shared/zcode-installer.mjs +247 -0
  24. package/CHANGELOG.md +36 -0
  25. package/INSTALLATION_GUIDE.zh.md +134 -0
  26. package/LICENSE +21 -0
  27. package/MIGRATION.md +53 -0
  28. package/README.i18n.yaml +3 -0
  29. package/README.md +424 -0
  30. package/README.zh.md +413 -0
  31. package/cordis.patch.yml +12 -0
  32. package/docs/assets/candidate-pruning.svg +80 -0
  33. package/docs/assets/desktop-official-tools-0.9.0.png +0 -0
  34. package/docs/assets/router-only-0.12.0.png +0 -0
  35. package/docs/assets/routing-workflow.svg +96 -0
  36. package/docs/assets/workbench-usage.svg +119 -0
  37. package/package.json +161 -0
@@ -0,0 +1,311 @@
1
+ /**
2
+ * Onboarding health check for the fixed official tool registry.
3
+ *
4
+ * For every registry tool: installed?, version at least the pinned registry
5
+ * version?, and logged in? Login checks use only fixed, read-only status
6
+ * commands (`claude auth status --json`, `codex login status`) or the
7
+ * presence of a credential file / environment variable name (Gemini, Kimi,
8
+ * MiMo, Grok). MiniMax is read from the `status` field of its non-secret
9
+ * auth-state.json (the CLI refuses to persist OAuth secrets there). ZCode exposes
10
+ * nothing and stays 'unknown'. No credential value is read or returned. Results are cached so routing can skip a tool
11
+ * that is known to be logged out instead of waiting for its CLI to fail.
12
+ */
13
+ import { OFFICIAL_TOOLS, getOfficialTool } from './official-tool-registry.mjs'
14
+
15
+ export const LOGIN_CHECK_TIMEOUT_MS = 5_000
16
+ export const HEALTH_CACHE_MS = 10 * 60_000
17
+
18
+ /** How to log in. Commands are display-only; the plugin never starts an interactive login. */
19
+ export const LOGIN_GUIDES = Object.freeze({
20
+ 'claude-code': Object.freeze({ command: 'claude auth login', steps: '在终端运行 claude auth login(或启动 claude 后输入 /login),按浏览器提示登录 Anthropic 账号。也可以在 Harness 模型页为 Anthropic 配置 API Key,插件只把它传给该 CLI 进程。' }),
21
+ codex: Object.freeze({ command: 'codex login', steps: '在终端运行 codex login,按浏览器提示登录 ChatGPT 账号;或设置 OPENAI_API_KEY 后运行 codex login --with-api-key。' }),
22
+ gemini: Object.freeze({ command: 'gemini', steps: '在终端运行 gemini,选择 “Login with Google” 完成登录;或在环境变量中设置 GEMINI_API_KEY。' }),
23
+ 'kimi-code': Object.freeze({ command: 'kimi login', steps: '在终端运行 kimi login(设备码登录,可加 --region mainland-cn 或 --region global),按提示在浏览器中授权 Kimi 账号;也可以设置 KIMI_API_KEY 使用 API 计费。' }),
24
+ 'minimax-code': Object.freeze({ command: 'mcode login', steps: '在终端运行 mcode login(可加 --region cn 或 --region global),在浏览器中登录 MiniMax 账号;或运行 mcode set-minimax-key / 设置 MINIMAX_API_KEY 使用 API Key。插件读取 MiniMax Code 的登录状态文件 auth-state.json(不含密钥)判断是否已登录;用 API Key 时状态显示为未知。' }),
25
+ 'mimo-code': Object.freeze({ command: 'mimo auth login', steps: '在终端运行 mimo auth login,选择 Xiaomi MiMo Platform 登录小米账号或填入 API Key;mimo auth list 可查看已保存的凭据。未登录时 MiMo Auto 免费匿名通道可能仍可用。' }),
26
+ 'grok-build': Object.freeze({ command: 'grok login', steps: '在终端运行 grok login,按浏览器提示登录 xAI 账号;无浏览器环境用 grok login --device-auth,或设置 XAI_API_KEY 使用 API 计费。' }),
27
+ zcode: Object.freeze({ command: null, steps: '打开 ZCode 桌面版,在欢迎页选择“连接 BigModel 继续使用”或“连接 Z.ai 继续使用”完成授权;已订阅 GLM Coding Plan 时在“模型设置 → BigModel”右上角选择“编程套餐”绑定。改用 API Key 时 OpenAI 地址须填 Coding 专用端点 https://open.bigmodel.cn/api/coding/paas/v4。ZCode 没有可调用的登录状态命令。' }),
28
+ })
29
+
30
+ /** Environment variable names whose presence means a CLI can authenticate without a session. */
31
+ const KEY_ENV = Object.freeze({
32
+ 'claude-code': ['ANTHROPIC_API_KEY', 'CLAUDE_CODE_OAUTH_TOKEN'],
33
+ codex: ['OPENAI_API_KEY', 'CODEX_API_KEY'],
34
+ gemini: ['GEMINI_API_KEY', 'GOOGLE_API_KEY'],
35
+ 'kimi-code': ['KIMI_API_KEY', 'MOONSHOT_API_KEY'],
36
+ 'minimax-code': ['MINIMAX_API_KEY'],
37
+ 'mimo-code': ['MIMO_API_KEY'],
38
+ 'grok-build': ['XAI_API_KEY'],
39
+ })
40
+
41
+ /**
42
+ * Credential files the official CLIs write after an account login. Verified
43
+ * against the published packages (kimi-code 2.1.1 FileTokenStorage,
44
+ * mimocode 0.1.15 `mimo auth list`, grok 1.0.41 `~/.grok/auth.json`). Only
45
+ * existence is checked; the file is never opened.
46
+ */
47
+ export function credentialFileFor(toolId, { home = '', env = process.env } = {}) {
48
+ const value = name => (typeof env?.[name] === 'string' && env[name].trim() ? env[name].trim().replace(/[\\/]+$/u, '') : '')
49
+ const base = home ? String(home).replace(/[\\/]+$/u, '') : ''
50
+ if (toolId === 'kimi-code') { const root = value('KIMI_CODE_HOME') || (base && `${base}/.kimi-code`); return root ? `${root}/credentials/kimi-code.json` : '' }
51
+ if (toolId === 'mimo-code') { const root = value('XDG_DATA_HOME') || (base && `${base}/.local/share`); return root ? `${root}/mimocode/auth.json` : '' }
52
+ if (toolId === 'grok-build') { const root = value('GROK_HOME') || (base && `${base}/.grok`); return root ? `${root}/auth.json` : '' }
53
+ if (toolId === 'gemini') return base ? `${base}/.gemini/oauth_creds.json` : ''
54
+ return ''
55
+ }
56
+
57
+ const FILE_LOGIN = Object.freeze({
58
+ 'kimi-code': { found: path => `检测到 Kimi Code 登录凭据文件 ${path}(未验证是否过期)。`, missing: path => `未找到 Kimi Code 登录凭据文件 ${path}(也可能通过 config.toml 中的自定义供应商认证),登录状态未知;如未登录请运行 kimi login。`, missingState: 'unknown' },
59
+ 'mimo-code': { found: path => `检测到 MiMo Code 凭据文件 ${path}(未验证是否有效,也可能只含 API Key)。`, missing: path => `未找到 MiMo Code 凭据文件 ${path};MiMo Auto 免费匿名通道可能仍可用,订阅/账号状态未知。`, missingState: 'unknown' },
60
+ 'grok-build': { found: path => `检测到 Grok 登录凭据文件 ${path}(未验证是否过期)。`, missing: path => `未找到 Grok 登录凭据文件 ${path},登录状态未知;如未登录请运行 grok login。`, missingState: 'unknown' },
61
+ })
62
+
63
+ /**
64
+ * MiniMax Code 0.5.5 keeps OAuth state per region in
65
+ * `<MINIMAX_DATA_DIR or ~/.minimax>/auth/prod/<cn|global>/mcode-public/auth-state.json`.
66
+ * That file is the CLI's non-secret state record (`status`: anonymous /
67
+ * authorizing / refreshing / authenticated / logging_out); the token itself
68
+ * lives in auth.json next to it or in the OS keyring and is never opened.
69
+ */
70
+ export function minimaxAuthStateFiles({ home = '', env = process.env } = {}) {
71
+ const configured = typeof env?.MINIMAX_DATA_DIR === 'string' ? env.MINIMAX_DATA_DIR.trim().replace(/[\\/]+$/u, '') : ''
72
+ const base = configured || (home ? `${String(home).replace(/[\\/]+$/u, '')}/.minimax` : '')
73
+ if (!base) return []
74
+ return ['cn', 'global'].map(region => ({ region, path: `${base}/auth/prod/${region}/mcode-public/auth-state.json` }))
75
+ }
76
+
77
+ /** Grok runs launched by the plugin may use a relocated GROK_HOME (see ensureNpmPrefixOnPath). */
78
+ function grokHomeHint(env) {
79
+ const configured = typeof env?.GROK_HOME === 'string' ? env.GROK_HOME.trim() : ''
80
+ return configured ? `插件启动 Grok 时使用 GROK_HOME=${configured};请在设置了同样 GROK_HOME 的终端中运行 grok login(PowerShell:$env:GROK_HOME='${configured}'; grok login),否则插件里的 Grok 读不到登录。` : ''
81
+ }
82
+
83
+ /** Variables that make the CLI bill an API account rather than a subscription login. */
84
+ const API_KEY_ENV = Object.freeze({
85
+ 'claude-code': ['ANTHROPIC_API_KEY'],
86
+ codex: ['OPENAI_API_KEY', 'CODEX_API_KEY'],
87
+ gemini: ['GEMINI_API_KEY', 'GOOGLE_API_KEY'],
88
+ 'kimi-code': ['KIMI_API_KEY', 'MOONSHOT_API_KEY'],
89
+ 'minimax-code': ['MINIMAX_API_KEY'],
90
+ 'mimo-code': ['MIMO_API_KEY'],
91
+ 'grok-build': ['XAI_API_KEY'],
92
+ zcode: ['ZAI_API_KEY'],
93
+ })
94
+
95
+ /** Copy of `env` without this CLI's API-key variables, so a status probe reports the account login itself. */
96
+ function sessionEnvironment(toolId, env) {
97
+ const names = API_KEY_ENV[toolId] ?? []
98
+ if (!env || names.length === 0) return undefined
99
+ return Object.fromEntries(Object.entries(env).filter(([name]) => !names.includes(name)))
100
+ }
101
+
102
+ /** True when an API-key variable for this CLI is set in `env` (names only, values are never read out). */
103
+ export function apiKeyEnvPresent(toolId, env = process.env) {
104
+ return (API_KEY_ENV[toolId] ?? []).some(name => typeof env?.[name] === 'string' && env[name].trim() !== '')
105
+ }
106
+
107
+ export function compareVersions(left, right) {
108
+ const parts = value => String(value ?? '').split(/[-+]/u)[0].split('.').map(item => Number.parseInt(item, 10) || 0)
109
+ const a = parts(left)
110
+ const b = parts(right)
111
+ for (let index = 0; index < Math.max(a.length, b.length); index += 1) {
112
+ const diff = (a[index] ?? 0) - (b[index] ?? 0)
113
+ if (diff !== 0) return diff > 0 ? 1 : -1
114
+ }
115
+ return 0
116
+ }
117
+
118
+ export function versionStatus(tool, version) {
119
+ if (!version || !tool?.version) return 'unknown'
120
+ return compareVersions(version, tool.version) >= 0 ? 'ok' : 'older'
121
+ }
122
+
123
+ function guide(toolId) {
124
+ return LOGIN_GUIDES[toolId] ?? { command: null, steps: '请按厂商官方文档完成登录。' }
125
+ }
126
+
127
+ /**
128
+ * Login state for one installed tool: 'logged-in', 'logged-out', or 'unknown'.
129
+ * `runner(executable, args, { timeoutMs })` resolves { ok, code, stdout, stderr, timedOut }.
130
+ */
131
+ export async function checkLogin(toolId, { runner, env = process.env, exists = async () => false, home = '', readAuthState = async () => null } = {}) {
132
+ const keyNames = (KEY_ENV[toolId] ?? []).filter(name => typeof env?.[name] === 'string' && env[name].trim())
133
+ const viaKey = keyNames.length > 0 ? { state: 'logged-in', detail: `检测到环境变量 ${keyNames.join('、')}(未验证有效性)。`, source: 'environment',
134
+ billing: apiKeyEnvPresent(toolId, env) ? 'api-key' : 'subscription',
135
+ accountLogin: keyNames.some(name => !(API_KEY_ENV[toolId] ?? []).includes(name)) } : null
136
+ const keyBilling = apiKeyEnvPresent(toolId, env) ? 'api-key' : null
137
+ if (toolId === 'claude-code' && typeof runner === 'function') {
138
+ const outcome = await runner('claude', ['auth', 'status', '--json'], { timeoutMs: LOGIN_CHECK_TIMEOUT_MS, env: sessionEnvironment(toolId, env) })
139
+ if (outcome.timedOut) return viaKey ?? { state: 'unknown', detail: '登录状态检测超时。' }
140
+ try {
141
+ const status = JSON.parse(outcome.stdout)
142
+ if (status?.loggedIn === true) return { state: 'logged-in', detail: `已登录(${String(status.authMethod ?? 'account')})。`, source: 'cli',
143
+ billing: keyBilling ?? (/api[_-]?key/i.test(String(status.authMethod ?? '')) ? 'api-key' : 'subscription'),
144
+ accountLogin: !/api[_-]?key/i.test(String(status.authMethod ?? '')) }
145
+ if (status?.loggedIn === false) return viaKey ?? { state: 'logged-out', detail: 'claude auth status 报告未登录。', source: 'cli' }
146
+ } catch { /* older CLI without JSON status */ }
147
+ return viaKey ?? { state: 'unknown', detail: '无法解析 claude auth status 输出。' }
148
+ }
149
+ if (toolId === 'codex' && typeof runner === 'function') {
150
+ const outcome = await runner('codex', ['login', 'status'], { timeoutMs: LOGIN_CHECK_TIMEOUT_MS, env: sessionEnvironment(toolId, env) })
151
+ if (outcome.timedOut) return viaKey ?? { state: 'unknown', detail: '登录状态检测超时。' }
152
+ const output = `${outcome.stdout}\n${outcome.stderr}`
153
+ if (/not logged in/i.test(output)) return viaKey ?? { state: 'logged-out', detail: 'codex login status 报告未登录。', source: 'cli' }
154
+ if (outcome.ok) return { state: 'logged-in', detail: output.split(/\r?\n/u).map(line => line.trim()).find(Boolean)?.slice(0, 120) || '已登录。', source: 'cli',
155
+ billing: keyBilling ?? (/api key/i.test(output) ? 'api-key' : /chatgpt/i.test(output) ? 'subscription' : 'unknown'),
156
+ accountLogin: /chatgpt/i.test(output) ? true : /api key/i.test(output) ? false : null }
157
+ return viaKey ?? { state: 'unknown', detail: 'codex login status 未给出明确结果。' }
158
+ }
159
+ if (toolId === 'gemini') {
160
+ const credentialFile = credentialFileFor('gemini', { home, env })
161
+ const hasFile = Boolean(credentialFile) && await exists(credentialFile)
162
+ if (viaKey) return { ...viaKey, accountLogin: hasFile }
163
+ if (hasFile) return { state: 'logged-in', detail: '检测到 Gemini CLI 登录凭据文件(未验证是否过期)。', source: 'file', billing: 'subscription', accountLogin: true }
164
+ return { state: 'unknown', detail: 'Gemini CLI 没有登录状态命令;首次运行时会提示登录。' }
165
+ }
166
+ if (FILE_LOGIN[toolId]) {
167
+ const text = FILE_LOGIN[toolId]
168
+ const credentialFile = credentialFileFor(toolId, { home, env })
169
+ const hasFile = Boolean(credentialFile) && await exists(credentialFile)
170
+ if (viaKey) return { ...viaKey, accountLogin: hasFile }
171
+ if (hasFile) return { state: 'logged-in', detail: text.found(credentialFile), source: 'file', billing: toolId === 'mimo-code' ? 'unknown' : 'subscription', accountLogin: toolId === 'mimo-code' ? null : true }
172
+ if (!credentialFile) return { state: 'unknown', detail: '无法确定用户目录,登录状态未知。' }
173
+ const hint = toolId === 'grok-build' ? grokHomeHint(env) : ''
174
+ return { state: text.missingState, detail: hint ? `${text.missing(credentialFile)}${hint}` : text.missing(credentialFile), source: 'file' }
175
+ }
176
+ if (toolId === 'minimax-code') {
177
+ const states = []
178
+ for (const { region, path } of minimaxAuthStateFiles({ home, env })) {
179
+ let record = null
180
+ try { record = await readAuthState(path) } catch { record = null }
181
+ const status = typeof record?.status === 'string' ? record.status : null
182
+ if (status) states.push({ region, status })
183
+ }
184
+ const signedIn = states.find(item => item.status === 'authenticated' || item.status === 'refreshing')
185
+ if (signedIn) return { state: 'logged-in', detail: `MiniMax Code 已登录(${signedIn.region === 'cn' ? '国内站' : '国际站'},状态文件 auth-state.json 为 ${signedIn.status},未验证令牌是否过期)。`, source: 'file',
186
+ billing: viaKey?.billing === 'api-key' ? 'api-key' : 'subscription', accountLogin: true }
187
+ if (viaKey) return viaKey
188
+ if (states.length) return { state: 'unknown', source: 'file', detail: `MiniMax Code 账号未登录(auth-state.json 为 ${states.map(item => `${item.region}: ${item.status}`).join('、')});如果用 mcode set-minimax-key 保存了 API Key 仍可使用,否则请运行 mcode login。` }
189
+ return { state: 'unknown', detail: '未找到 MiniMax Code 登录状态文件(~/.minimax/auth/prod/<cn|global>/mcode-public/auth-state.json),登录状态未知;如未登录请运行 mcode login。' }
190
+ }
191
+ if (toolId === 'zcode') return viaKey ?? { state: 'unknown', detail: 'ZCode 是桌面应用,没有可调用的登录状态命令;登录与 GLM Coding Plan 状态请在 ZCode 内查看。' }
192
+ return viaKey ?? { state: 'unknown', detail: '该 CLI 没有可安全调用的登录状态命令;首次运行时以实际结果为准。' }
193
+ }
194
+
195
+ /**
196
+ * Whether a CLI can run on its subscription login (with API-key variables
197
+ * removed): 'subscription', 'api-key' (only an API key authenticates it),
198
+ * 'logged-out', or 'unknown'.
199
+ */
200
+ export function subscriptionLoginOf(login) {
201
+ if (!login) return 'unknown'
202
+ if (login.state === 'logged-out') return 'logged-out'
203
+ if (login.accountLogin === true) return 'subscription'
204
+ if (login.accountLogin === false) return 'api-key'
205
+ if (login.billing === 'subscription') return 'subscription'
206
+ return 'unknown'
207
+ }
208
+
209
+ /** Combine one probe with its login state. */
210
+ export async function checkToolHealth(probe, options = {}) {
211
+ const tool = getOfficialTool(probe?.id)
212
+ if (!tool) throw new TypeError('unknown official tool')
213
+ const installed = probe.installed === true
214
+ const login = installed ? await checkLogin(tool.id, options) : { state: 'unknown', detail: probe.status === 'not-installed' && /检测到已安装/.test(String(probe.detail ?? '')) ? String(probe.detail) : '尚未安装。' }
215
+ const { command, steps } = guide(tool.id)
216
+ return {
217
+ id: tool.id,
218
+ label: tool.label,
219
+ installed,
220
+ installStatus: probe.status ?? (installed ? 'installed' : 'not-installed'),
221
+ version: probe.version ?? null,
222
+ pinnedVersion: tool.version ?? null,
223
+ versionStatus: installed ? versionStatus(tool, probe.version) : 'unknown',
224
+ login: { ...login, command, steps },
225
+ ready: installed && login.state !== 'logged-out',
226
+ }
227
+ }
228
+
229
+ /** Run the whole health check; individual failures become 'unknown' rather than errors. */
230
+ export async function runHealthCheck(probes, options = {}) {
231
+ const now = options.now ?? Date.now
232
+ const byId = new Map((Array.isArray(probes) ? probes : []).map(probe => [probe.id, probe]))
233
+ const tools = await Promise.all(OFFICIAL_TOOLS.map(async tool => {
234
+ const probe = byId.get(tool.id) ?? { id: tool.id, installed: false, status: 'not-installed' }
235
+ try { return await checkToolHealth(probe, options) }
236
+ catch (error) {
237
+ return { id: tool.id, label: tool.label, installed: probe.installed === true, version: probe.version ?? null,
238
+ pinnedVersion: tool.version ?? null, versionStatus: 'unknown', ready: probe.installed === true,
239
+ login: { state: 'unknown', detail: String(error?.message ?? error).slice(0, 200), ...guide(tool.id) } }
240
+ }
241
+ }))
242
+ return { checkedAt: now(), tools }
243
+ }
244
+
245
+ /** In-process readiness cache shared by planning and execution. */
246
+ export function createHealthCache({ ttlMs = HEALTH_CACHE_MS, now = Date.now } = {}) {
247
+ let report = null
248
+ const overrides = new Map()
249
+ const seenAuth = new Map()
250
+ return {
251
+ remember(next) {
252
+ if (next && Array.isArray(next.tools)) {
253
+ report = next
254
+ // A runtime authentication failure stays known while the new check still says
255
+ // logged out, so the router keeps asking instead of silently using the API key.
256
+ for (const [toolId, override] of [...overrides]) {
257
+ const entry = next.tools.find(item => item.id === toolId)
258
+ if (override.source !== 'runtime-auth' || entry?.login?.state !== 'logged-out') overrides.delete(toolId)
259
+ }
260
+ }
261
+ return report
262
+ },
263
+ report() { return report },
264
+ /** Mark one tool logged out after its CLI reported an authentication failure. */
265
+ markLoggedOut(toolId, detail, at = now()) {
266
+ overrides.set(toolId, { at, state: 'logged-out', detail: String(detail ?? '').slice(0, 200), source: 'runtime-auth' })
267
+ },
268
+ /** The CLI ran on its subscription again: forget the authentication failure. */
269
+ clearLoggedOut(toolId) { seenAuth.delete(toolId); return overrides.delete(toolId) },
270
+ /**
271
+ * Adopt persisted state written by other processes: a newer health report,
272
+ * new authentication failures, and failures another process cleared.
273
+ */
274
+ sync(saved) {
275
+ let changed = 0
276
+ if (saved?.health && Array.isArray(saved.health.tools) && Number.isFinite(saved.health.checkedAt)
277
+ && (!report || saved.health.checkedAt > report.checkedAt)) { this.remember(saved.health); changed += 1 }
278
+ const disk = saved?.authFailures && typeof saved.authFailures === 'object' ? saved.authFailures : {}
279
+ for (const [toolId, entry] of Object.entries(disk)) {
280
+ const at = Number.isFinite(entry?.at) ? entry.at : 0
281
+ const mine = overrides.get(toolId)
282
+ if (!mine || (mine.source === 'runtime-auth' && at > mine.at)) { this.markLoggedOut(toolId, entry?.detail, at); changed += 1 }
283
+ seenAuth.set(toolId, at)
284
+ }
285
+ for (const [toolId, mine] of [...overrides]) {
286
+ if (toolId in disk || mine.source !== 'runtime-auth') continue
287
+ if (seenAuth.has(toolId) && mine.at <= seenAuth.get(toolId)) { overrides.delete(toolId); changed += 1 }
288
+ seenAuth.delete(toolId)
289
+ }
290
+ return changed
291
+ },
292
+ loginState(toolId) {
293
+ const override = overrides.get(toolId)
294
+ if (override && (now() - override.at < ttlMs || report?.tools?.find(item => item.id === toolId)?.login?.state === 'logged-out')) return override
295
+ if (!report || now() - report.checkedAt >= ttlMs) return null
296
+ const entry = report.tools.find(item => item.id === toolId)
297
+ return entry?.installed ? { state: entry.login?.state ?? 'unknown', detail: entry.login?.detail ?? '', billing: entry.login?.billing ?? null,
298
+ accountLogin: typeof entry.login?.accountLogin === 'boolean' ? entry.login.accountLogin : null } : null
299
+ },
300
+ /** Reason to skip the CLI, or null. A configured API key still lets the CLI authenticate. */
301
+ skipReason({ toolId, hasApiKey = false }) {
302
+ if (hasApiKey) return null
303
+ const state = this.loginState(toolId)
304
+ if (state?.state !== 'logged-out') return null
305
+ const label = getOfficialTool(toolId)?.label ?? toolId
306
+ return `${label} 未登录(开箱体检结果),已直接使用模型目录 API。登录后在工作台点“重新体检”即可恢复官方 CLI。`
307
+ },
308
+ }
309
+ }
310
+
311
+ export const healthCache = createHealthCache()
@@ -0,0 +1,308 @@
1
+ /**
2
+ * Fixed Windows launch adapters for official MiMo Code 0.1.15 and Grok 1.0.41.
3
+ *
4
+ * MiMo source: https://github.com/XiaomiMiMo/MiMo-Code/blob/v0.1.15/packages/opencode/src/cli/cmd/run.ts
5
+ * Grok source: https://github.com/xai-org/grok-build/blob/main/crates/codegen/xai-grok-shell/README.md
6
+ * Grok npm bootstrap: https://unpkg.com/@xai-official/grok@1.0.41/bin/grok-bootstrap.js
7
+ *
8
+ * We bypass npm's mutable .cmd/JS launcher. Package names, versions, platform
9
+ * binaries, and file digests are fixed here; only model IDs and task text are
10
+ * supplied by the caller. The Host must still approve write mode and confine
11
+ * the process to its isolated workspace.
12
+ */
13
+ import { createReadStream } from 'node:fs'
14
+ import { createHash } from 'node:crypto'
15
+ import { readFile, realpath, stat } from 'node:fs/promises'
16
+ import { createBrotliDecompress } from 'node:zlib'
17
+ import { homedir } from 'node:os'
18
+ import { basename, delimiter, isAbsolute, join, relative, resolve, sep } from 'node:path'
19
+ import { ensureNpmPrefixOnPath } from './official-tools-runtime.mjs'
20
+ import { usageFromEvents } from './task-executors.mjs'
21
+
22
+ const VERSIONS = Object.freeze({ 'mimo-code': '0.1.15', 'grok-build': '1.0.41' })
23
+ const PLATFORMS = Object.freeze({
24
+ 'mimo-code': Object.freeze({
25
+ x64: Object.freeze({ package: '@mimo-ai/mimocode-windows-x64', integrity: 'sha256-6nBnONfwCs5+Y+pLp+Vcc4cwuWDDMP0hLBdlQTHpmkw=', size: 135419392 }),
26
+ arm64: Object.freeze({ package: '@mimo-ai/mimocode-windows-arm64', integrity: 'sha256-5w/7e0cy90Smv2sBaIlrdLliel4i2VjEKcWm1I2J6eo=', size: 131460608 }),
27
+ }),
28
+ 'grok-build': Object.freeze({
29
+ x64: Object.freeze({ package: '@xai-official/grok-win32-x64', integrity: 'sha256-ZK5odjiiROsE+OE72BdLTcbZgxKRoSnVFUUyDufuaS8=', size: 46488279 }),
30
+ arm64: Object.freeze({ package: '@xai-official/grok-win32-arm64', integrity: 'sha256-oJkk4XYpNvNQBk5jbFReagr8WnbCTWRpXPir0Oa7YWg=', size: 41589854 }),
31
+ }),
32
+ })
33
+
34
+ const WRAPPERS = Object.freeze({
35
+ 'mimo-code': Object.freeze({ name: '@mimo-ai/cli', command: 'mimo', bin: 'bin/mimo' }),
36
+ 'grok-build': Object.freeze({ name: '@xai-official/grok', command: 'grok', bin: 'bin/grok' }),
37
+ })
38
+ const FINAL_TEXT_LIMIT = 32_000
39
+ const WINDOWS_PROMPT_LIMIT = 12_000
40
+
41
+ function inside(parent, child) {
42
+ const part = relative(parent, child)
43
+ return part !== '' && part !== '..' && !part.startsWith(`..${sep}`) && !isAbsolute(part)
44
+ }
45
+
46
+ function packagePath(modules, name) { return join(modules, ...name.split('/')) }
47
+
48
+ function moduleRoots() {
49
+ const path = process.env.PATH ?? process.env.Path ?? ''
50
+ const roots = new Set()
51
+ for (const raw of path.split(delimiter)) {
52
+ const directory = raw.trim().replace(/^"|"$/g, '')
53
+ if (!directory) continue
54
+ roots.add(join(directory, 'node_modules'))
55
+ if (basename(directory).toLowerCase() === '.bin') roots.add(resolve(directory, '..'))
56
+ }
57
+ return [...roots]
58
+ }
59
+
60
+ async function manifestAt(directory, name, version) {
61
+ try {
62
+ const root = await realpath(directory)
63
+ const manifest = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'))
64
+ if (manifest.name !== name || manifest.version !== version) return null
65
+ return { root, manifest }
66
+ } catch { return null }
67
+ }
68
+
69
+ async function hashFile(filename, decompress = false) {
70
+ const hash = createHash('sha256')
71
+ const stream = decompress
72
+ ? createReadStream(filename).pipe(createBrotliDecompress())
73
+ : createReadStream(filename)
74
+ for await (const chunk of stream) hash.update(chunk)
75
+ return `sha256-${hash.digest('base64')}`
76
+ }
77
+
78
+ async function realFileWithin(root, candidate, workspace) {
79
+ try {
80
+ const file = await realpath(candidate)
81
+ if (!inside(root, file) || file === workspace || inside(workspace, file)) return null
82
+ if (!(await stat(file)).isFile()) return null
83
+ return file
84
+ } catch { return null }
85
+ }
86
+
87
+ async function optionalPackage(wrapper, toolId, platform, modules, workspace) {
88
+ if (wrapper.manifest.optionalDependencies?.[platform.package] !== VERSIONS[toolId]) return null
89
+ const candidates = [
90
+ packagePath(modules, platform.package),
91
+ packagePath(join(wrapper.root, 'node_modules'), platform.package),
92
+ ]
93
+ for (const candidate of candidates) {
94
+ const optional = await manifestAt(candidate, platform.package, VERSIONS[toolId])
95
+ if (!optional || optional.root === workspace || inside(workspace, optional.root)) continue
96
+ return optional
97
+ }
98
+ return null
99
+ }
100
+
101
+ async function findMiMoExecutable(workspace, platform) {
102
+ const wrapperInfo = WRAPPERS['mimo-code']
103
+ for (const modules of moduleRoots()) {
104
+ const wrapper = await manifestAt(packagePath(modules, wrapperInfo.name), wrapperInfo.name, VERSIONS['mimo-code'])
105
+ if (!wrapper || String(wrapper.manifest.bin?.[wrapperInfo.command]).replace(/^\.\//, '') !== wrapperInfo.bin) continue
106
+ const optional = await optionalPackage(wrapper, 'mimo-code', platform, modules, workspace)
107
+ if (!optional) continue
108
+ const file = await realFileWithin(optional.root, join(optional.root, 'bin', 'mimo.exe'), workspace)
109
+ if (!file || (await stat(file)).size !== platform.size) continue
110
+ if (await hashFile(file) !== platform.integrity) continue
111
+ return { file, source: 'official-npm-native-sha256' }
112
+ }
113
+ return null
114
+ }
115
+
116
+ async function grokHome() {
117
+ const configured = process.env.GROK_HOME
118
+ if (configured) return isAbsolute(configured) ? resolve(configured) : null
119
+ try { return join(await realpath(homedir()), '.grok') }
120
+ catch { return join(homedir(), '.grok') }
121
+ }
122
+
123
+ async function findGrokExecutable(workspace, platform) {
124
+ const wrapperInfo = WRAPPERS['grok-build']
125
+ for (const modules of moduleRoots()) {
126
+ const wrapper = await manifestAt(packagePath(modules, wrapperInfo.name), wrapperInfo.name, VERSIONS['grok-build'])
127
+ if (!wrapper || String(wrapper.manifest.bin?.[wrapperInfo.command]).replace(/^\.\//, '') !== wrapperInfo.bin) continue
128
+ const optional = await optionalPackage(wrapper, 'grok-build', platform, modules, workspace)
129
+ if (!optional) continue
130
+ const compressed = await realFileWithin(optional.root, join(optional.root, 'bin', 'grok.exe.br'), workspace)
131
+ if (!compressed || (await stat(compressed)).size !== platform.size) continue
132
+ if (await hashFile(compressed) !== platform.integrity) continue
133
+ const uncompressedIntegrity = await hashFile(compressed, true)
134
+ const home = await grokHome()
135
+ const candidates = [
136
+ ...(home ? [join(home, 'bin', `grok-${VERSIONS['grok-build']}.exe`), join(home, 'bin', 'grok.exe')] : []),
137
+ join(optional.root, 'bin', 'grok.exe'),
138
+ ]
139
+ for (const candidate of candidates) {
140
+ try {
141
+ const file = await realpath(candidate)
142
+ if (file === workspace || inside(workspace, file) || !(await stat(file)).isFile()) continue
143
+ if (await hashFile(file) === uncompressedIntegrity) {
144
+ return { file, source: 'official-npm-native-sha256' }
145
+ }
146
+ } catch { /* try next installed binary */ }
147
+ }
148
+ }
149
+ return null
150
+ }
151
+
152
+ /**
153
+ * Return a fixed executable and arguments for the Host's existing sandboxed
154
+ * process runner, or an unsupported reason. The Host retains its own task,
155
+ * workspace, model, approval, timeout, and cancellation validation.
156
+ */
157
+ export async function resolveMiMoGrokLaunch({ toolId, workspace, mode, modelId, task }) {
158
+ if (!(toolId in VERSIONS)) return { unsupported: '未知的 MiMo/Grok 官方工具 ID。' }
159
+ if (process.platform !== 'win32') return { unsupported: 'MiMo/Grok 原生入口目前仅完成 Windows 适配。' }
160
+ if (!isAbsolute(workspace) || !['read-only', 'workspace-write'].includes(mode)) {
161
+ return { unsupported: '缺少有效的绝对工作区或执行模式。' }
162
+ }
163
+ if (modelId != null && !/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,119}$/.test(modelId)) {
164
+ return { unsupported: '模型 ID 格式无效。' }
165
+ }
166
+ if (toolId === 'mimo-code' && modelId && !modelId.includes('/')) {
167
+ return { unsupported: 'MiMo CLI 的模型参数需要 provider/model;请使用 MiMo 中已配置的模型名称,或省略 modelId 使用默认模型。' }
168
+ }
169
+ const platform = PLATFORMS[toolId][process.arch]
170
+ if (!platform) return { unsupported: '当前 Windows CPU 架构没有核验的官方原生程序。' }
171
+ await ensureNpmPrefixOnPath()
172
+ const directory = await realpath(workspace)
173
+ const found = toolId === 'mimo-code'
174
+ ? await findMiMoExecutable(directory, platform)
175
+ : await findGrokExecutable(directory, platform)
176
+ if (!found) return { unsupported: `未找到与官方 npm ${VERSIONS[toolId]} 发行文件哈希一致的 Windows 原生程序。` }
177
+
178
+ if (toolId === 'mimo-code') {
179
+ // MiMo's v0.1.15 run command reads all piped stdin and emits JSONL. Its
180
+ // default headless permission requests are auto-rejected; isolated write
181
+ // mode can auto-approve only after the Host has approved/constrained it.
182
+ return {
183
+ ...found,
184
+ args: [
185
+ 'run', '--format', 'json', '--dir', directory,
186
+ ...(modelId ? ['--model', modelId] : []),
187
+ ...(mode === 'workspace-write' ? ['--dangerously-skip-permissions'] : []),
188
+ ],
189
+ format: 'mimo-jsonl',
190
+ stdinTask: true,
191
+ }
192
+ }
193
+
194
+ // xAI's headless contract takes the prompt in -p. Piped stdin is not the
195
+ // documented task channel, so do not launch with a fixed stub prompt.
196
+ if (typeof task !== 'string' || !task.trim() || task.length > WINDOWS_PROMPT_LIMIT) {
197
+ return { unsupported: `Grok 任务必须是非空文本且不超过 ${WINDOWS_PROMPT_LIMIT} 个 UTF-16 字符,以适应 Windows 命令行长度。` }
198
+ }
199
+ return {
200
+ ...found,
201
+ args: [
202
+ '--no-auto-update', '-p', task, '--cwd', directory,
203
+ '--output-format', 'streaming-json',
204
+ ...(modelId ? ['--model', modelId] : []),
205
+ ...(mode === 'read-only'
206
+ ? ['--tools', 'read_file,grep,list_dir,web_search,web_fetch']
207
+ : ['--always-approve']),
208
+ ],
209
+ format: 'grok-jsonl',
210
+ stdinTask: false,
211
+ }
212
+ }
213
+
214
+ /** Parse vendor JSONL without treating a clean process exit as proof of work. */
215
+ export function createMiMoGrokParser(toolId) {
216
+ if (!(toolId in VERSIONS)) throw new TypeError('unsupported vendor parser')
217
+ let protocolError = null
218
+ let vendorError = null
219
+ let finalText = ''
220
+ let terminal = null
221
+ let sessionId = null
222
+ let requestId = null
223
+ let toolErrors = 0
224
+ let events = 0
225
+ const mimoSteps = []
226
+ let grokSpend = null
227
+
228
+ return {
229
+ push(line) {
230
+ if (!String(line).trim()) return
231
+ let event
232
+ try { event = JSON.parse(line) }
233
+ catch { protocolError = '官方 CLI 输出了无效 JSONL。'; return }
234
+ if (!event || typeof event !== 'object' || Array.isArray(event) || typeof event.type !== 'string') {
235
+ protocolError = '官方 CLI 输出了无效事件。'
236
+ return
237
+ }
238
+ events += 1
239
+ if (toolId === 'mimo-code') {
240
+ if (typeof event.sessionID !== 'string' || !event.sessionID) {
241
+ protocolError = 'MiMo 事件缺少会话 ID。'
242
+ return
243
+ }
244
+ if (sessionId && sessionId !== event.sessionID) {
245
+ protocolError = 'MiMo 混入了其他会话的事件。'
246
+ return
247
+ }
248
+ sessionId = event.sessionID
249
+ if (event.type === 'error') {
250
+ vendorError = 'MiMo 返回了会话错误。'
251
+ terminal = 'failed'
252
+ } else if (event.type === 'step_start') {
253
+ // A previous completed step is not the terminal state of a new step.
254
+ terminal = null
255
+ } else if (event.type === 'step_finish') {
256
+ if (event.part?.type !== 'step-finish' || typeof event.part.reason !== 'string') {
257
+ protocolError = 'MiMo 步骤终态格式无效。'
258
+ } else {
259
+ terminal = event.part.reason === 'stop' ? 'completed' : 'failed'
260
+ if (event.part.tokens && typeof event.part.tokens === 'object') mimoSteps.push({ tokens: event.part.tokens, cost: event.part.cost })
261
+ }
262
+ } else if (event.type === 'text' && event.part?.type === 'text'
263
+ && typeof event.part.text === 'string' && event.part.time?.end) {
264
+ finalText = event.part.text.slice(-FINAL_TEXT_LIMIT)
265
+ } else if (event.type === 'tool_use' && event.part?.state?.status === 'error') {
266
+ toolErrors += 1
267
+ }
268
+ } else {
269
+ if (event.type === 'error') {
270
+ vendorError = 'Grok 返回了错误事件。'
271
+ terminal = 'failed'
272
+ } else if (event.type === 'text') {
273
+ if (typeof event.data !== 'string') protocolError = 'Grok 文本事件格式无效。'
274
+ else finalText = (finalText + event.data).slice(-FINAL_TEXT_LIMIT)
275
+ } else if (event.type === 'end') {
276
+ if (terminal !== null) protocolError = 'Grok 返回了重复终态。'
277
+ else {
278
+ const reason = String(event.stopReason ?? '').toLowerCase().replaceAll('_', '')
279
+ terminal = reason === 'endturn' ? 'completed' : 'failed'
280
+ if (typeof event.sessionId === 'string') sessionId = event.sessionId
281
+ if (typeof event.requestId === 'string') requestId = event.requestId
282
+ if (event.usage && typeof event.usage === 'object') grokSpend = event
283
+ }
284
+ }
285
+ }
286
+ },
287
+ finish(exitCode) {
288
+ function spend() {
289
+ const reported = toolId === 'mimo-code' ? usageFromEvents('mimo-steps', mimoSteps) : usageFromEvents('grok-end', grokSpend)
290
+ return reported ? { usage: reported.usage, ...(reported.reportedCostUsd ? { reportedCostUsd: reported.reportedCostUsd } : {}) } : {}
291
+ }
292
+ const good = exitCode === 0 && !protocolError && !vendorError
293
+ && terminal === 'completed' && finalText.trim().length > 0
294
+ && (toolId !== 'grok-build' || Boolean(sessionId))
295
+ return {
296
+ status: good ? 'succeeded' : 'failed',
297
+ terminalEvent: terminal,
298
+ finalText,
299
+ sessionId,
300
+ requestId,
301
+ toolErrors,
302
+ events,
303
+ ...spend(),
304
+ ...(!good ? { error: protocolError ?? vendorError ?? `${toolId} 未返回完整的成功终态和回答。` } : {}),
305
+ }
306
+ },
307
+ }
308
+ }