@luchenglong/cli-bridge 0.2.3 → 0.2.5

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.
@@ -1,73 +1,150 @@
1
- // providers/mimo.mjs — mimo provider descriptor(真移植,替换 Lead 冒烟占位)。
1
+ // providers/mimo.mjs — mimo provider descriptor(单后端:本地 mimo CLI / MiMoCode)。
2
2
  //
3
- // 移植基线:mimo-first-bridge v1.x Xiaomi MiMo-bridge 仓 core/mimo-core.mjs +
4
- // core/runtime-settings.mjs + preset/mimo-first/mimo-first-bridge.mjs 的内联逻辑
5
- // (双后端 mimocode/mimo-desktop、buildArgv/parseMimoJson/buildResult/桌面回合编排/
6
- // 三工具 schema/mimo_settings),语义逐字保持;与 provider 无关的骨架(状态引擎/行流/
7
- // 回退弹窗/展示层/超时护栏)在 core/bridge-kit.mjs(PROVIDER-CONTRACT.md §1)。
3
+ // 移植基线:mimo-first-bridge v1.x 仓 core/mimo-core.mjs + core/runtime-settings.mjs +
4
+ // preset/mimo-first/mimo-first-bridge.mjs 的内联逻辑(buildArgv / parseMimoJson /
5
+ // buildResult / 三工具 schema / mimo_settings),语义逐字保持;与 provider 无关的骨架
6
+ // (状态引擎/行流/回退弹窗/展示层/超时护栏)在 core/bridge-kit.mjs(契约 §1)。
8
7
  //
9
- // 红线(PROVIDER-CONTRACT.md §8 + Lead 定案):
10
- // - 本文件会被 build 内联进 dynamic 沙箱(new Function):绝不出现 import.meta 字面量;
11
- // process/fs/child_process 只经 deps 注入或 typeof/globalThis 守卫读取;原生定时器
12
- // (setTimeout/AbortController)只在 typeof 守卫下使用(沙箱缺省时自动降级)。
13
- // - 只允许 import '../core/bridge-kit.mjs'。
14
- // - mimo-desktop 走本地 HTTP(非 spawn):delegates(backend) 只认 desktop,execute 自己
15
- // 复刻旧回合编排;mimocode 不写 execute 分支 → kit 通用 pipeline 带 jobs/重试/弹窗。
8
+ // 2026-10 去 desktop 化(用户定案:MiMo 免费 API 服务已被官方终止 + 本机 0 凭据):
9
+ // 1. 取消 mimo-desktop 后端 —— 只走 `mimo run`(MiMoCode CLI,官方便携安装位置
10
+ // ~/.mimocode/bin/mimo.exe);登录由用户在 CLI 侧 `mimo auth login` 自理。
11
+ // 2. 模型由用户在设置里手动指定(settings.defaultModel,不写死默认值);留空 =
12
+ // 不传 --model,交给 mimo CLI 自身默认。可用模型以 `mimo models` 实测为准
13
+ // (xiaomi/mimo-v2.6-flash / -pro / -pro-ultraspeed 等)。
14
+ // 3. 是否启用 mimo 由调度(_scheduling.enabled)控制;本描述符不含任何开关。
15
+ //
16
+ // 红线(PROVIDER-CONTRACT.md §8 + Lead 定案):本文件会被 build 内联进 dynamic 沙箱
17
+ // (new Function)—— 绝不出现 import.meta 字面量;process/fs/child_process 只经
18
+ // globalThis.process.getBuiltinModule + typeof 守卫读取;只允许 import bridge-kit。
16
19
  import { createStatusEngine, createRunner, clampInt, shortLabel, summarizeArgs } from '../core/bridge-kit.mjs'
17
20
 
18
- // ── 后端常量与别名归一(旧 mimo-core verbatim)────────────────────────────────
19
- const BACKEND_MIMOCODE = 'mimocode'
20
- const BACKEND_DESKTOP = 'mimo-desktop'
21
- const BACKENDS = [BACKEND_MIMOCODE, BACKEND_DESKTOP]
22
- const DEFAULT_BACKEND = BACKEND_MIMOCODE
21
+ // ── 单后端归一(旧 mimo-core 双后端 verbatim 的收缩形态)───────────────────────
22
+ // mimo-desktop 取消后不再有第二后端:backends() 恒 null;任何输入(含历史别名
23
+ // mimocode / mimo-cli / desktop / 空串 / 非法值)一律归一为 null。会话路由由
24
+ // engine.resolveTarget(cwd + sessionId)完成,backend 恒 null。
25
+ const DEFAULT_TIMEOUT_SEC = 600
23
26
 
24
27
  // 旧 LIMIT_RE(比 kit 版多 free-API/登录/第三方等 MiMo 文案;provider.isLimited 覆盖)。
25
28
  // 命名 MIMO_ 前缀:dynamic 内联时 kit 也声明顶层 LIMIT_RE,同名会在同一脚本作用域
26
29
  // 触发「Identifier already declared」构建错误(scripts/build.mjs 文本拼接的固有约束)。
27
30
  const MIMO_LIMIT_RE = /rate.?limit|ratelimit|429|too many|quota|insufficient|credit|balance|exhausted|exceed|network|offline|ENETUNREACH|ECONNREFUSED|ECONNRESET|ETIMEDOUT|EAI_AGAIN|ENOTFOUND|timeout|timed out|unavailable|503|502|500|401|403|unauthorized|invalid api|api key|connection|proxy|socket|tls|ssl|dns|free API service has ended|sign in|configure a third-party|网络|超时|限流|流量|受限|配额|金额|余额|额度|认证|连接|断开|登录|第三方/i
28
31
 
29
- const DEFAULT_TIMEOUT_SEC = 600
32
+ // ── Node 内建探测(沙箱纪律:getBuiltinModule + typeof 守卫)───────────────────
33
+ // 名字带 mimo 前缀:本文件会与 bridge-kit / core/scheduling.mjs【文本内联进同一个动态
34
+ // 沙箱脚本作用域】,撞名即「Identifier already declared」构建错误。
35
+ function mimoBuiltin(name) {
36
+ try {
37
+ const proc = globalThis.process
38
+ return (proc && typeof proc.getBuiltinModule === 'function') ? proc.getBuiltinModule(name) : null
39
+ } catch (e) { return null }
40
+ }
41
+
42
+ // ── 可执行文件解析(env MIMO_BIN/MIMO_CLI > 已知安装位置 > PATH 名字)───────────
43
+ // MiMoCode CLI 官方便携安装目录:<USERPROFILE|HOME>/.mimocode/bin/mimo[.exe]。
44
+ // 沙箱内可能没有 fs —— 全部读取走 typeof 守卫,读不到就返回空数组(退回 PATH 查找)。
45
+ const MIMO_EXE_DIR = ['.mimocode', 'bin']
46
+
47
+ /** 探测本机 mimo 可执行文件候选路径(官方便携安装位置);无 fs 能力时返回空数组。 */
48
+ export function mimoCandidateExePaths(env) {
49
+ const e = env || {}
50
+ const out = []
51
+ const fs = mimoBuiltin('node:fs')
52
+ if (!fs || typeof fs.existsSync !== 'function') return out
53
+ const path = mimoBuiltin('node:path')
54
+ const join = (path && typeof path.join === 'function') ? ((a, b) => path.join(a, b)) : ((a, b) => a + '/' + b)
55
+ for (const k of ['USERPROFILE', 'HOME']) {
56
+ if (!e[k]) continue
57
+ const dir = join(join(String(e[k]), MIMO_EXE_DIR[0]), MIMO_EXE_DIR[1])
58
+ for (const base of ['mimo.exe', 'mimo']) {
59
+ const exe = join(dir, base)
60
+ try { if (fs.existsSync(exe) && out.indexOf(exe) < 0) out.push(exe) } catch (err) { /* 探测失败静默跳过 */ }
61
+ }
62
+ }
63
+ return out
64
+ }
65
+
66
+ /**
67
+ * 解析本次调用使用的 exe 前缀(数组或字符串)。
68
+ * 优先级:binPath(显式路径)> env MIMO_BIN/MIMO_CLI > 已知安装位置 > PATH 名字。
69
+ */
70
+ export function resolveMimoExePrefix(env, binPath) {
71
+ const e = env || {}
72
+ const bin = binPath || e.MIMO_BIN || e.MIMO_CLI || ''
73
+ if (bin) {
74
+ if (/\.(mjs|cjs|js)$/i.test(bin)) {
75
+ const proc = globalThis.process
76
+ return [(proc && proc.execPath) ? proc.execPath : 'node', bin]
77
+ }
78
+ return [String(bin)]
79
+ }
80
+ const found = mimoCandidateExePaths(e)
81
+ return found.length ? [found[0]] : ['mimo']
82
+ }
30
83
 
31
- // Desktop-local API 路径构造器(旧 DESKTOP_API verbatim)。
32
- const DESKTOP_API = {
33
- healthPath: '/v1/health',
34
- sessionsPath: '/v1/sessions',
35
- turnsPath: (id) => '/v1/sessions/' + encodeURIComponent(String(id)) + '/turns',
36
- eventsPath: (id, dir) => '/v1/sessions/' + encodeURIComponent(String(id)) + '/events' + (dir ? ('?dir=' + encodeURIComponent(String(dir))) : '')
84
+ // ── 模型列表(`mimo models`;设置面板下拉用)───────────────────────────────────
85
+ // 实测输出形如每行 `provider/model — window 1M, compacts at 900K`(分隔符是中文破折号
86
+ // U+2014)。模型名自身含连字符(xiaomi/mimo-v2.6-pro-ultraspeed),所以只认「行首的
87
+ // provider/model」,其后一律当描述文本;表头/说明/空行一律丢弃,绝不抛。
88
+ const MIMO_MODEL_ID_RE = /^([A-Za-z0-9_.-]+\/[A-Za-z0-9_.@#+-]+)(?=$|[\s—–:|,])/
89
+
90
+ /** 解析 `mimo models` 文本输出为 [{id, provider, name, note}](去重、保序)。 */
91
+ export function parseMimoModels(text) {
92
+ const out = []
93
+ const seen = Object.create(null)
94
+ for (const raw of String(text || '').split(/\r?\n/)) {
95
+ // 去掉列表记号(• * - 1. 等)与行首空白,避免把记号当 provider 名。
96
+ const ln = raw.replace(/^[^A-Za-z0-9]+/, '').trim()
97
+ if (!ln) continue
98
+ const m = ln.match(MIMO_MODEL_ID_RE)
99
+ if (!m) continue
100
+ const id = m[1]
101
+ if (seen[id]) continue
102
+ seen[id] = true
103
+ const slash = id.indexOf('/')
104
+ out.push({ id: id, provider: id.slice(0, slash), name: id.slice(slash + 1), note: ln.slice(id.length).replace(/^[\s—–:|,-]+/, '').trim() })
105
+ }
106
+ return out
37
107
  }
38
108
 
39
- // 工具三件套 schema(旧 preset ctx.tools.register parameters verbatim,转 JSON Schema
40
- // 形态对齐 agy 描述符约定;required 进顶层数组,其余字段语义逐字保持)。
109
+ /**
110
+ * 同步抓取模型列表(供设置面板下拉)。失败返回空数组 —— 下拉退化为自由输入,
111
+ * 绝不因探测失败影响面板渲染。仅 spawnSync 可用时生效(宿主形态)。
112
+ */
113
+ export function fetchMimoModelsSync(env, timeoutMs) {
114
+ try {
115
+ const child = mimoBuiltin('node:child_process')
116
+ if (!child || typeof child.spawnSync !== 'function') return []
117
+ const prefix = resolveMimoExePrefix(env)
118
+ const r = child.spawnSync(prefix[0], prefix.slice(1).concat(['models']), { encoding: 'utf8', timeout: clampInt(timeoutMs, 8000, 1000, 30000), windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] })
119
+ if (!r || r.error || r.status !== 0) return []
120
+ return parseMimoModels(String(r.stdout || ''))
121
+ } catch (e) { return [] }
122
+ }
123
+
124
+ // ── 三工具 schema(旧 preset verbatim;desktop 专属入参已随之后端取消而删除)────
125
+ const MODEL_DESC = 'provider/model 格式,如 xiaomi/mimo-v2.6-flash;留空用设置里的默认模型'
41
126
  const RUN_PARAMS = {
42
- type: 'object',
43
- additionalProperties: true,
44
- required: ['prompt'],
127
+ type: 'object', additionalProperties: true, required: ['prompt'],
45
128
  properties: {
46
129
  prompt: { type: 'string', description: 'The task instruction for mimo. On first dispatch for a topic, include a compact CONTEXT preamble (goal, repo root, key paths, decisions/constraints; point at paths, do not paste file contents — mimo reads the repo itself). For follow-ups on the same topic, use mimo_continue with latest=true or sessionId (mimo keeps conversation context; state only what is new).' },
47
130
  mode: { type: 'string', enum: ['auto', 'plan', 'build', 'compose', 'accept-edits'], description: 'auto follows DSH plan state (plan vs build); plan = read-only analysis (--agent plan); build = full tools (--agent build); compose = compose workflow.' },
48
- backend: { type: 'string', enum: BACKENDS, description: 'Dispatch backend for THIS call only. Default comes from settings preselection (plugin config.backend / MIMO_BRIDGE_BACKEND / settings file): mimocode = local `mimo` CLI (MiMoCode); mimo-desktop = Xiaomi MiMo Desktop client local API. Prefer the preselected default; override only when necessary.' },
49
- model: { type: 'string', description: 'Optional mimo model id as provider/model (e.g. mimo/mimo). For mimo-desktop, desktop model id (e.g. mimo-auto).' },
50
- effort: { type: 'string', enum: ['low', 'medium', 'high'], description: 'Optional reasoning effort, mapped to mimo --variant (low→minimal, high→high). mimocode only.' },
51
- cwd: { type: 'string', description: 'Working directory for mimo (--dir / desktop directory).' },
131
+ model: { type: 'string', description: MODEL_DESC },
132
+ effort: { type: 'string', enum: ['low', 'medium', 'high'], description: 'Optional reasoning effort, mapped to mimo --variant (low→minimal, high→high).' },
133
+ cwd: { type: 'string', description: 'Working directory for mimo (--dir).' },
52
134
  timeoutSec: { type: 'integer', description: 'Hard timeout seconds (10-3600, default 600). Grace +60s on top.' },
53
135
  background: { type: 'boolean', description: 'Defaults to true: dispatch returns a jobId immediately; collect with job_output, watch progress with mimo_status. Set false to block for the final answer.' },
54
- // desktop 后端**不能创建会话**(API 对任意 id 都回 202 但不落库),所以必须能
55
- // 显式指定一个**已存在**的 Desktop 会话 id,否则每次都生成自造 id → 静默空操作。
56
- sessionId: { type: 'string', description: 'mimo-desktop only: id of an ALREADY EXISTING MiMo Desktop session (see the Desktop app). Omit for mimocode.' },
136
+ sessionId: { type: 'string', description: 'Optional mimo session id to resume (--session). Follow-ups on the same topic normally go through mimo_continue.' },
57
137
  title: { type: 'string', description: 'Optional session title.' }
58
138
  }
59
139
  }
60
140
  const CONTINUE_PARAMS = {
61
- type: 'object',
62
- additionalProperties: true,
63
- required: ['prompt'],
141
+ type: 'object', additionalProperties: true, required: ['prompt'],
64
142
  properties: {
65
143
  prompt: { type: 'string', description: 'Follow-up instruction for the ongoing mimo session. State only what is new; mimo preserves its conversation context.' },
66
144
  sessionId: { type: 'string', description: 'mimo session id to resume (--session).' },
67
145
  latest: { type: 'boolean', description: 'Continue the most recent mimo session (--continue).' },
68
- backend: { type: 'string', enum: BACKENDS, description: 'Dispatch backend for THIS call only (see mimo_run.backend). Prefer the preselected default.' },
69
146
  mode: { type: 'string', enum: ['auto', 'plan', 'build', 'compose', 'accept-edits'], description: 'Execution mode.' },
70
- model: { type: 'string', description: 'Optional mimo model id as provider/model.' },
147
+ model: { type: 'string', description: MODEL_DESC },
71
148
  effort: { type: 'string', enum: ['low', 'medium', 'high'], description: 'Optional reasoning effort.' },
72
149
  cwd: { type: 'string', description: 'Working directory for mimo.' },
73
150
  timeoutSec: { type: 'integer', description: 'Hard timeout seconds (10-3600, default 600).' },
@@ -75,50 +152,27 @@ const CONTINUE_PARAMS = {
75
152
  }
76
153
  }
77
154
  const STATUS_PARAMS = { type: 'object', additionalProperties: true, required: [], properties: { cwd: { type: 'string', description: 'Optional: filter the snapshot to a single project (working directory).' } } }
78
- const SETTINGS_PARAMS = {
79
- type: 'object',
80
- additionalProperties: true,
81
- required: [],
82
- properties: {
83
- backend: { type: 'string', enum: BACKENDS, description: 'If set, save this as the default backend.' },
84
- desktopModel: { type: 'string', description: 'Optional desktop model id (mimo-desktop backend only), e.g. mimo-auto.' }
85
- }
86
- }
87
- const SETTINGS_DESCRIPTION = 'Read or change the preselected dispatch backend (settings). backend=mimocode (local mimo CLI / MiMoCode) or mimo-desktop (Xiaomi MiMo Desktop client). Call with only backend to set the default for subsequent mimo_run/mimo_continue; call with no args to read current settings. Persisted to the settings file; plugin config and MIMO_BRIDGE_BACKEND env still override when set.'
155
+ const SETTINGS_PARAMS = { type: 'object', additionalProperties: true, required: [], properties: { model: { type: 'string', description: MODEL_DESC } } }
156
+ const SETTINGS_DESCRIPTION = 'Read or change the default mimo model (settings). Call with model=<provider/model> to save it as the default for subsequent mimo_run/mimo_continue; call with no args to read current settings. Persisted to the settings file; a per-call `model` argument on mimo_run/mimo_continue overrides it for that call only. Available models are listed by `mimo models` on this machine (mimo CLI must be installed and logged in).'
88
157
 
89
158
  function renderSettings(value) {
90
159
  const s = value || {}
91
160
  const lines = [
92
161
  'mimo settings',
93
162
  ' settings file : ' + (s.settingsPath || '(unknown)'),
94
- ' file backend : ' + (s.fileBackend || '(unset)'),
95
- ' config backend : ' + (s.configBackend || '(unset)'),
96
- ' env backend : ' + (s.envBackend || '(unset)'),
97
- ' effective : ' + (s.effectiveBackend || '(unknown)'),
98
- ' desktop model : ' + (s.desktopModel || '(default)'),
99
- ' desktop api : ' + (s.desktopAvailable
100
- ? 'available' + (s.desktopApiFile ? ' (' + s.desktopApiFile + ')' : '')
101
- : 'unavailable' + (s.desktopError ? ' — ' + s.desktopError : '')),
102
- ' backends : ' + ((Array.isArray(s.backends) && s.backends.length ? s.backends.join(' | ') : BACKENDS.join(' | ')))
163
+ ' default model : ' + (s.defaultModel || '(unset — 不传 --model)'),
164
+ ' effective : ' + (s.effective || '(unknown)'),
165
+ ' exe : ' + (s.exe || '(unknown)'),
166
+ ' saved : ' + (s.saved ? 'yes' : 'no')
103
167
  ]
168
+ if (s.saveError) lines.push(' save error : ' + s.saveError)
104
169
  if (s.note) lines.push(' note : ' + s.note)
105
170
  return [{ type: 'text', text: lines.join('\n') }]
106
171
  }
107
-
108
172
  // ── 可纯函数化的 mimo-core 语义(descriptor 方法 + 独立导出双份)───────────────
109
- function normalizeBackend(v) {
110
- if (v == null || v === '') return null
111
- const s = String(v).trim().toLowerCase()
112
- if (s === 'mimocode' || s === 'mimo-code' || s === 'cli' || s === 'mimo-cli') return BACKEND_MIMOCODE
113
- if (s === 'mimo-desktop' || s === 'desktop' || s === 'mimo_desktop' || s === 'xiaomi-mimo' || s === 'xiaomi-mimo-desktop') return BACKEND_DESKTOP
114
- return null
115
- }
116
-
117
173
  function parseMimoJson(stdoutText) {
118
174
  const trimmed = String(stdoutText || '').trim()
119
175
  if (!trimmed) return null
120
- const MAX_ARG_LEN = 120
121
- const MAX_TRAIL = 12
122
176
  const events = []
123
177
  const lines = trimmed.split(/\r?\n/)
124
178
  for (const raw of lines) {
@@ -151,12 +205,12 @@ function parseMimoJson(stdoutText) {
151
205
  if (ev.session && typeof ev.session === 'object' && ev.session.id) sessionId = ev.session.id
152
206
  if (ev.type === 'session' && ev.properties && ev.properties.id) sessionId = ev.properties.id
153
207
  const t = ev.type
208
+ const p = ev.properties || ev
154
209
  if (t === 'error') {
155
- const err = ev.error || ev.properties || {}
210
+ const err = ev.error || p
156
211
  errorMessage = (err.data && err.data.message) || err.message || err.name || 'error'
157
212
  errorName = err.name || 'Error'
158
213
  } else if (t === 'text' || t === 'message' || t === 'assistant' || t === 'output') {
159
- const p = ev.properties || ev
160
214
  const chunk = p.text || p.content || p.message || p.output || (typeof ev.text === 'string' ? ev.text : '')
161
215
  if (typeof chunk === 'string' && chunk) {
162
216
  textParts.push(chunk)
@@ -164,26 +218,17 @@ function parseMimoJson(stdoutText) {
164
218
  }
165
219
  } else if (t === 'step-start' || t === 'step-finish' || t === 'step_update') {
166
220
  numTurns += 1
167
- const p = ev.properties || ev
168
221
  if (p.tokens && typeof p.tokens === 'object') {
169
222
  const tt = p.tokens.total ?? p.tokens.input ?? p.tokens.output
170
223
  if (typeof tt === 'number') totalTokens = (totalTokens || 0) + tt
171
224
  }
172
- if (t === 'step_update' && (p.tool_name || p.tool || p.name)) {
173
- steps.push({
174
- at: Date.now(),
175
- kind: 'tool',
176
- label: shortLabel((p.tool_name || p.tool || p.name) + ' ' + JSON.stringify(summarizeArgs(p.arguments || p.args || p.input) || {}))
177
- })
225
+ if (t === 'step_update') {
226
+ const name = p.tool_name || p.tool || p.name
227
+ if (name) steps.push({ at: Date.now(), kind: 'tool', label: shortLabel(name + ' ' + JSON.stringify(summarizeArgs(p.arguments || p.args || p.input) || {})) })
178
228
  }
179
229
  } else if (t === 'tool' || t === 'tool-call' || t === 'tool_use') {
180
- const p = ev.properties || ev
181
230
  const name = p.tool || p.name || p.tool_name || 'tool'
182
- steps.push({
183
- at: Date.now(),
184
- kind: 'tool',
185
- label: shortLabel(name + ' ' + JSON.stringify(summarizeArgs(p.arguments || p.args || p.input) || {}))
186
- })
231
+ steps.push({ at: Date.now(), kind: 'tool', label: shortLabel(name + ' ' + JSON.stringify(summarizeArgs(p.arguments || p.args || p.input) || {})) })
187
232
  }
188
233
  }
189
234
  if (textParts.length) text = textParts.join('\n')
@@ -228,217 +273,56 @@ function buildResult(parsed, outcome, mode, stderrText, stdoutText, backend) {
228
273
  }
229
274
  }
230
275
 
231
- function desktopResult(ok, opts) {
232
- const o = opts || {}
233
- return {
234
- ok: !!ok,
235
- fallback: false,
236
- status: o.status || (ok ? (o.accepted ? 'ACCEPTED' : 'SUCCESS') : 'ERROR'),
237
- response: o.response || '',
238
- sessionId: o.sessionId || null,
239
- durationSeconds: null,
240
- numTurns: null,
241
- totalTokens: null,
242
- exitCode: o.exitCode != null ? o.exitCode : (ok ? 0 : 1),
243
- mode: o.mode || 'auto',
244
- backend: BACKEND_DESKTOP,
245
- stderr: o.stderr || '',
246
- errorMessage: o.errorMessage || '',
247
- accepted: o.accepted === true,
248
- steps: []
249
- }
250
- }
251
-
252
- function buildDesktopTurnBody(args) {
253
- const a = args || {}
254
- const message = String(a.prompt != null ? a.prompt : (a.message != null ? a.message : ''))
255
- const model = String(a.model || a.desktopModel || 'mimo-auto')
256
- const body = { message, model }
257
- const directory = a.directory || a.cwd
258
- if (directory) body.directory = String(directory)
259
- return body
260
- }
261
-
262
- function newDesktopSessionId() {
263
- // ses_ + 18-ish random chars, same shape as Desktop API ids
264
- const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'
265
- let s = ''
266
- for (let i = 0; i < 18; i++) s += chars[Math.floor(Math.random() * chars.length)]
267
- return 'ses_' + s
268
- }
269
-
270
- function extractDesktopText(j) {
271
- if (!j || typeof j !== 'object') return { text: '', sessionId: null, error: null }
272
- const sessionId = j.sessionID || j.sessionId || (j.session && j.session.id) || null
273
- if (j.type === 'error' || j.error) {
274
- const err = j.error || j
275
- const msg = (err.data && err.data.message) || err.message || err.name || 'error'
276
- return { text: '', sessionId, error: String(msg) }
277
- }
278
- let text = ''
279
- if (typeof j.text === 'string') text = j.text
280
- else if (typeof j.message === 'string') text = j.message
281
- else if (typeof j.content === 'string') text = j.content
282
- else if (typeof j.response === 'string') text = j.response
283
- else if (Array.isArray(j.parts)) {
284
- for (const p of j.parts) {
285
- if (p && typeof p.text === 'string') text += (text ? '\n' : '') + p.text
286
- }
287
- } else if (j.data && typeof j.data.text === 'string') text = j.data.text
288
- return { text, sessionId, error: null }
289
- }
290
-
291
- function parseDesktopEvents(stdoutOrJson) {
292
- const raw = String(stdoutOrJson || '').trim()
293
- if (!raw) return { text: '', sessionId: null, error: null }
294
- try {
295
- const j = JSON.parse(raw)
296
- return extractDesktopText(j)
297
- } catch (e) { }
298
- const texts = []
299
- for (const line of raw.split(/\r?\n/)) {
300
- const ln = line.trim()
301
- if (!ln.startsWith('{')) continue
302
- try {
303
- const r = extractDesktopText(JSON.parse(ln))
304
- if (r.text) texts.push(r.text)
305
- if (r.error) return { text: texts.join('\n'), sessionId: r.sessionId, error: r.error }
306
- } catch (e) { }
307
- }
308
- return { text: texts.join('\n'), sessionId: null, error: null }
309
- }
310
-
311
276
  export default function defineProvider(deps = {}) {
312
277
  const env = deps.env || {}
313
278
  const nodeFs = deps.nodeFs || null
314
279
  const nodePath = deps.nodePath || null
315
-
316
- // ── Desktop-local API discovery(旧 runtime-settings.loadRuntime 桌面段语义)───
317
- // fs 缺失(dynamic 沙箱)→ 只认 MIMO_DESKTOP_PORT/TOKEN env;都没有 → 不可用。
318
- function discoverDesktop() {
319
- const d = { available: false, baseUrl: null, token: null, apiFile: null, error: null }
320
- if (env.MIMO_DESKTOP_PORT && env.MIMO_DESKTOP_TOKEN) {
321
- return { available: true, baseUrl: 'http://127.0.0.1:' + env.MIMO_DESKTOP_PORT, token: String(env.MIMO_DESKTOP_TOKEN), apiFile: null, error: null }
322
- }
323
- const candidates = []
324
- if (env.MIMO_DESKTOP_API_FILE) candidates.push(env.MIMO_DESKTOP_API_FILE)
325
- const joinP = (...segs) => (nodePath ? nodePath.join(...segs) : segs.join('/'))
326
- if (env.APPDATA) candidates.push(joinP(env.APPDATA, 'Xiaomi MiMo', 'desktop-api.json'))
327
- if (env.HOME) {
328
- candidates.push(joinP(env.HOME, '.config', 'Xiaomi MiMo', 'desktop-api.json'))
329
- candidates.push(joinP(env.HOME, 'Library', 'Application Support', 'Xiaomi MiMo', 'desktop-api.json'))
330
- }
331
- if (!nodeFs) {
332
- d.error = 'desktop-api.json not readable (no fs in this host; set MIMO_DESKTOP_PORT/MIMO_DESKTOP_TOKEN)'
333
- return d
334
- }
335
- for (const p of candidates) {
336
- try {
337
- if (!p || !nodeFs.existsSync || !nodeFs.existsSync(p)) continue
338
- const j = JSON.parse(nodeFs.readFileSync(p, 'utf8'))
339
- if (j && j.port && j.token) {
340
- return { available: true, baseUrl: 'http://127.0.0.1:' + j.port, token: String(j.token), apiFile: p, error: null }
280
+ let host = null // attach 后保存 hostOpts(preset/dynamic 形态提供 getSettings)
281
+ let settingsRef = { defaultModel: '' }
282
+
283
+ /** 取当前生效的偏好设置。三条来源按优先级合并,每次 buildArgv 都重新读 ——
284
+ * 面板改默认模型后无需重启即生效:
285
+ * 1) `host.getSettings('mimo')` —— preset/dynamic 形态(attach 时由宿主注入);
286
+ * 2) `deps.readSettings()` —— MCP 形态(真 Node 进程,每次现读设置文件);
287
+ * 3) `settingsRef` —— attach 时固化的一次性快照 / 缺省值。
288
+ * 三条都拿不到 → 缺省 `{defaultModel:''}`(= 不传 --model,交给 mimo CLI 自决)。
289
+ *
290
+ * 历史坑:MCP 形态不 attach(自建 engine),只认 host.getSettings 会让面板设置的
291
+ * defaultModel 在 MCP 面对 mimo_run 完全失效 —— 只有 args.model 才落地。
292
+ * 三个形态行为不一致是缺陷,故此处统一把 readSettings 纳入。 */
293
+ function currentSettings() {
294
+ const out = Object.assign({}, settingsRef)
295
+ try {
296
+ if (typeof deps.readSettings === 'function') {
297
+ const r = deps.readSettings()
298
+ if (r && typeof r === 'object') {
299
+ // 宿主可返回 { partition, path, home } 或直接返回分区对象(宽松兼容)。
300
+ const part = ('partition' in r || 'path' in r || 'home' in r) ? r.partition : r
301
+ if (part && typeof part === 'object') Object.assign(out, part)
341
302
  }
342
- } catch (e) {
343
- d.error = String(e && e.message || e)
344
303
  }
345
- }
346
- if (!d.error) d.error = 'desktop-api.json not found (start Xiaomi MiMo Desktop)'
347
- return d
348
- }
349
-
350
- function fetchFn() {
351
- return (typeof globalThis !== 'undefined' && typeof globalThis.fetch === 'function') ? globalThis.fetch.bind(globalThis) : null
352
- }
353
-
354
- // desktopFetch(旧 runtime-settings verbatim;AbortController/setTimeout 守卫,
355
- // 沙箱缺省时退化为无超时 fetch —— 仍可用)。
356
- async function desktopFetch(method, path, body, timeoutMs) {
357
- const fetch = fetchFn()
358
- if (!fetch) { const e = new Error('fetch not available in this host'); e.code = 'DESKTOP_UNAVAILABLE'; throw e }
359
- const d = desktop
360
- if (!d || !d.available || !d.baseUrl) {
361
- const e = new Error('MiMo Desktop API unavailable: ' + (d && d.error || 'not configured'))
362
- e.code = 'DESKTOP_UNAVAILABLE'
363
- throw e
364
- }
365
- const url = d.baseUrl + path
366
- const ms = timeoutMs == null ? 8000 : timeoutMs
367
- let ctrl = null
368
- let timer = null
369
- const init = {
370
- method: method || 'GET',
371
- headers: { Authorization: 'Bearer ' + d.token, 'Content-Type': 'application/json' }
372
- }
373
- if (body != null) init.body = typeof body === 'string' ? body : JSON.stringify(body)
374
- if (typeof AbortController === 'function' && typeof setTimeout === 'function' && typeof clearTimeout === 'function') {
375
- ctrl = new AbortController()
376
- init.signal = ctrl.signal
377
- timer = setTimeout(() => { try { ctrl.abort() } catch (e) {} }, ms)
378
- }
379
- try {
380
- const res = await fetch(url, init)
381
- const text = await res.text()
382
- let json = null
383
- try { json = text ? JSON.parse(text) : null } catch (e) { }
384
- return { status: res.status, ok: res.ok, text, json }
385
- } finally {
386
- if (timer != null) clearTimeout(timer)
387
- }
388
- }
389
-
390
- async function desktopHealth() {
304
+ } catch (e) { }
391
305
  try {
392
- const r = await desktopFetch('GET', DESKTOP_API.healthPath)
393
- return { ok: r.ok, status: r.status, body: r.json || r.text }
394
- } catch (e) {
395
- return { ok: false, status: 0, error: String(e && e.message || e) }
396
- }
397
- }
398
-
399
- async function desktopListSessions(limit) {
400
- const r = await desktopFetch('GET', DESKTOP_API.sessionsPath + '?limit=' + (limit || 10))
401
- return r.json
402
- }
403
-
404
- async function desktopSendTurn(sessionId, args) {
405
- const id = sessionId || newDesktopSessionId()
406
- const body = buildDesktopTurnBody(args)
407
- const r = await desktopFetch('POST', DESKTOP_API.turnsPath(id), body, 15000)
408
- // 【实测事故】Desktop API 对**任意** session id 都无条件返回 202 {"ok":true} ——
409
- // 它不校验目标会话是否存在。实测:向自造 id POST 得到 202,但
410
- // GET /v1/sessions 计数不变(19→19),会话从未创建,任务从未执行。
411
- // 因此 202 不能作为成功判据,必须回读会话列表确认目标 id 确实存在,
412
- // 否则桥接层会把"静默丢弃"报告成 ACCEPTED,用户在 Desktop UI 里什么都看不到。
413
- const accepted = r.status === 202 || (r.json && r.json.ok === true)
414
- let verified = null
415
- if (accepted) {
416
- try {
417
- const list = await desktopListSessions(200)
418
- if (Array.isArray(list)) verified = list.some((s) => s && String(s.id) === String(id))
419
- else verified = null
420
- } catch (e) { verified = null }
421
- }
422
- return {
423
- id, status: r.status, ok: r.ok, body: r.json || r.text,
424
- accepted, verified,
425
- // 仅当回读确认存在(或回读不可用)才算真正投递成功
426
- delivered: accepted && verified !== false
427
- }
306
+ if (host && typeof host.getSettings === 'function') {
307
+ const s = host.getSettings('mimo')
308
+ if (s && typeof s === 'object') Object.assign(out, s)
309
+ }
310
+ } catch (e) { }
311
+ return out
428
312
  }
429
313
 
430
- async function desktopReadEvents(sessionId, dir, timeoutMs) {
431
- try {
432
- const r = await desktopFetch('GET', DESKTOP_API.eventsPath(sessionId, dir), null, timeoutMs || 2500)
433
- return parseDesktopEvents(r.text || (r.json ? JSON.stringify(r.json) : ''))
434
- } catch (e) {
435
- return { text: '', sessionId, error: String(e && e.message || e) }
436
- }
314
+ // 模型优先级:调用参数 > 宿主注入设置 > runner opts 透传 > 空(不传 --model)。
315
+ // 用户要求「不设默认 → 由 mimo CLI 自决」,故空值绝不落地 --model。
316
+ function effectiveModel(args, opts) {
317
+ const o = opts || {}
318
+ const fromArg = (args && args.model && String(args.model).trim()) || ''
319
+ if (fromArg) return fromArg
320
+ const fromSettings = (currentSettings().defaultModel || '').trim()
321
+ if (fromSettings) return fromSettings
322
+ const fromOpts = (o.settings && o.settings.defaultModel) || o.defaultModel
323
+ return fromOpts ? String(fromOpts).trim() : ''
437
324
  }
438
325
 
439
- // 每次调用前刷一次发现结果(Desktop 可能中途启动/退出;env-only 形态零开销)。
440
- let desktop = discoverDesktop()
441
-
442
326
  // ── 统一设置分区读取(keystone 落盘;descriptor 只读)。宿主注入 deps.readSettings
443
327
  // 动态读取(preset/MCP 每次取最新文件值),或静态 deps.settings(测试/dynamic)。
444
328
  function liveSettings() {
@@ -456,44 +340,22 @@ export default function defineProvider(deps = {}) {
456
340
  }
457
341
  return { partition: (deps.settings && typeof deps.settings === 'object') ? deps.settings : null, path: deps.settingsPath || null, home: deps.dshHome || null }
458
342
  }
459
- function partitionSettings() {
460
- return liveSettings().partition || null
461
- }
462
- function fileBackend() {
463
- const s = partitionSettings()
464
- if (s && s.preferredBackend) return s.preferredBackend
465
- return null
466
- }
467
- function desktopModelChain(cfg) {
468
- const s = partitionSettings()
469
- return (cfg && cfg.desktopModel) || (s && s.desktopModel) || env.MIMO_DESKTOP_MODEL || 'mimo-auto'
470
- }
471
343
 
472
344
  const descriptor = {
473
- id: 'mimo', label: 'MIMO', presetId: 'cordis-mimo', defaultBackend: DEFAULT_BACKEND, defaultMode: 'auto',
345
+ id: 'mimo', label: 'MIMO', presetId: 'cordis-mimo', defaultBackend: null, defaultMode: 'auto',
474
346
  exeName: 'mimo', binEnvVars: ['MIMO_BIN', 'MIMO_CLI'], unavailableStatus: 'MIMO_UNAVAILABLE',
475
347
 
476
- backends() { return BACKENDS },
477
- normalizeBackend,
478
-
479
- // 预选优先级(旧 resolveBackend 语义):工具参数 > 插件 config > env > 设置文件 > 默认。
480
- // keystone runner 已把 config/env/file 折进 getPreferredBackend(),这里只补 arg 级。
481
- resolveBackend(args, target, preferred) {
482
- const b = normalizeBackend(args && args.backend)
483
- if (b) return b
484
- return normalizeBackend(preferred) || descriptor.defaultBackend
485
- },
486
-
487
- // 委托规则(Lead 定案):只有 desktop 走 provider.execute;mimocode 落 kit 通用管线。
488
- delegates(backend) { return backend === BACKEND_DESKTOP },
348
+ // 单后端:backends() 恒 null;历史别名(mimocode/mimo-cli/desktop)与非法值一律
349
+ // 归一为 null(契约 §1:非法输入返回 null)。
350
+ backends() { return null },
351
+ normalizeBackend() { return null },
352
+ resolveBackend(args, target, preferred) { return null },
489
353
 
354
+ // 旧 preset buildArgv(exe, args, planActive) verbatim 语义(execPrefix 可为
355
+ // [nodeExe, binPath] 数组,runner 已归一)。
490
356
  buildArgv(exePrefix, args, opts) {
491
357
  const o = opts || {}
492
358
  const timeoutSec = clampInt(args.timeoutSec, DEFAULT_TIMEOUT_SEC, 10, 3600)
493
- // mimo-desktop:非 spawn 路径,空壳交给 execute(kit 委托前会调本函数)。
494
- if (o.backend === BACKEND_DESKTOP) {
495
- return { argv: [], mode: args.mode || 'auto', timeoutSec }
496
- }
497
359
  const argv = Array.isArray(exePrefix) ? exePrefix.slice() : [String(exePrefix)]
498
360
  argv.push('run')
499
361
  if (args.prompt != null && String(args.prompt).length) argv.push(String(args.prompt))
@@ -504,8 +366,9 @@ export default function defineProvider(deps = {}) {
504
366
  // mimo --agent: build | plan | compose
505
367
  const agent = args.agent || mode
506
368
  if (agent === 'plan' || agent === 'build' || agent === 'compose') argv.push('--agent', agent)
507
- // mimocode 用 --model provider/model(桌面模型链只在 desktop execute 内使用)。
508
- if (args.model) argv.push('--model', String(args.model))
369
+ // 模型:调用参数 > 设置 defaultModel > 不传(mimo CLI 自身默认)。
370
+ const model = effectiveModel(args, o)
371
+ if (model) argv.push('--model', String(model))
509
372
  // effort (low/medium/high) maps to mimo --variant; pass-through if already a variant name
510
373
  let variant = args.variant || args.effort
511
374
  if (variant === 'low') variant = 'minimal'
@@ -522,9 +385,9 @@ export default function defineProvider(deps = {}) {
522
385
  parseJson: parseMimoJson,
523
386
  buildResult,
524
387
 
525
- // 旧 foldStepUpdate 语义 verbatim(识别 tool/text/error 事件),但 trail 条目同时
526
- // 带 kit 展示层字段(stepIndex/tool/state/args)与旧字段(kind/label),
527
- // 这样 renderStatus 与 MCP 快照映射(buildMcpBridgePayload)都能直接渲染。
388
+ // 旧 foldStepUpdate 语义 verbatim(识别 tool/text/error 事件),trail 条目同时带
389
+ // kit 展示层字段(stepIndex/tool/state/args)与旧字段(kind/label),这样
390
+ // renderStatus 与 MCP 快照映射(buildMcpBridgePayload)都能直接渲染。
528
391
  foldStreamEvent(ev, p, summarizeArgsFn, maxArgLen, maxTrail) {
529
392
  if (!ev || typeof ev !== 'object') return
530
393
  const t = ev.type
@@ -573,218 +436,121 @@ export default function defineProvider(deps = {}) {
573
436
  failureHint(res) {
574
437
  if (!res || res.ok) return ''
575
438
  const st = String(res.status || '')
576
- if (st === 'MIMO_UNAVAILABLE') return '[诊断] 未找到 mimo 可执行文件:请安装 MiMoCode CLI(mimo),或设置 MIMO_BIN 指向其绝对路径;也可改用 backend=mimo-desktop 走 MiMo Desktop 客户端。'
577
- if (st === 'DESKTOP_UNREACHABLE') return '[诊断] MiMo Desktop 本地 API 不可达/本宿主不支持 HTTP:请启动 Xiaomi MiMo Desktop 后重试,或改用 backend=mimocode(本地 mimo CLI)。'
578
- if (st === 'DESKTOP_SESSION_NOT_CREATED') return '[诊断] MiMo Desktop API 无法创建会话:请在 Desktop 应用中新建或选用现有会话,把它的 id 传给 sessionId 参数。'
579
- if (/free API service has ended|sign in|configure a third-party|登录|第三方/i.test(String(res.stderr || '') + String(res.errorMessage || ''))) {
580
- return '[诊断] MiMo 免费 API 服务疑似已停止或未登录:请在 mimo CLI 完成登录/配置第三方模型(configure a third-party),或改用 backend=mimo-desktop 走 Desktop 登录态。'
439
+ if (st === 'MIMO_UNAVAILABLE') return '[诊断] 未找到 mimo 可执行文件:请安装 MiMoCode CLI(mimo,官方便携安装位置 ~/.mimocode/bin/mimo.exe),或设置 MIMO_BIN 指向其绝对路径。'
440
+ const hay = String(res.stderr || '') + ' ' + String(res.errorMessage || '') + ' ' + String(res.response || '')
441
+ if (/free API service has ended|sign in|configure a third-party|登录|第三方|免费/i.test(hay)) {
442
+ return '[诊断] MiMo 免费 API 已终止,请在 mimo CLI 用 `mimo auth login` 登录或配置第三方 API,并在 CLI Bridge 设置里指定模型。'
443
+ }
444
+ if (/model not found|unknown model|no models available|invalid model|unknown provider/i.test(hay)) {
445
+ return '[诊断] mimo 模型不可用或模型名无效:先在本机运行 `mimo models` 列出可用模型,再把 provider/model 填进 CLI Bridge 的「默认模型」设置(或本次调用的 model 参数)。'
581
446
  }
582
447
  return ''
583
448
  },
584
449
 
585
- // ACCEPTED 计入成功态(指示灯不红;契约 §1)。
450
+ // ACCEPTED 计入成功态(指示灯不红;契约 §1);desktop 取消后不再产生 ACCEPTED,
451
+ // 但成功态集合逐字保持不变(兼容旧会话/测试语义)。
586
452
  isSuccessStatus(st) {
587
453
  if (!st) return false
588
454
  return st === 'SUCCESS' || st === 'ACCEPTED'
589
455
  },
590
456
 
591
- // 桌面可达性前置检查(契约 §9):dynamic 无 fetch / 无发现 → abort 提示 mimocode。
592
- // 有 fs 时做一次健康探测缓存(异步进行中先放行 —— preflight 是同步钩子)。
593
- preflight({ backend }) {
594
- if (backend !== BACKEND_DESKTOP) return null
595
- desktop = discoverDesktop()
596
- if (!fetchFn()) {
597
- return {
598
- abort: desktopResult(false, {
599
- status: 'DESKTOP_UNREACHABLE',
600
- mode: 'auto',
601
- errorMessage: '当前宿主无 HTTP fetch 能力(dynamic 沙箱),mimo-desktop 不可用。请改用 backend=mimocode,或在 DSH 正式宿主(preset/MCP 形态)中使用桌面后端。'
602
- })
603
- }
604
- }
605
- if (!desktop.available) {
606
- return {
607
- abort: desktopResult(false, {
608
- status: 'DESKTOP_UNREACHABLE',
609
- mode: 'auto',
610
- stderr: String(desktop.error || 'not configured'),
611
- errorMessage: 'MiMo Desktop client unavailable. Start Xiaomi MiMo Desktop, or use backend=mimocode.'
612
- })
613
- }
614
- }
615
- return null
616
- },
617
-
618
- // mimo-desktop 委托执行(旧 runDesktop verbatim 编排;engine.begin/end 由 kit 做)。
619
- async execute({ args, cwd, built }) {
620
- desktop = discoverDesktop()
621
- const a = args || {}
622
- const sessionId = a.sessionId || newDesktopSessionId()
623
- const mode = built && built.mode ? built.mode : (a.mode || 'auto')
624
- try {
625
- const health = await desktopHealth()
626
- if (!health.ok) {
627
- return desktopResult(false, {
628
- status: 'DESKTOP_UNAVAILABLE',
629
- sessionId,
630
- mode,
631
- stderr: String(health.error || health.body || 'desktop api not ready'),
632
- errorMessage: 'MiMo Desktop client unavailable. Start Xiaomi MiMo Desktop and retry, or set backend=mimocode.'
633
- })
634
- }
635
- const s = partitionSettings()
636
- const turn = await desktopSendTurn(sessionId, {
637
- prompt: a.prompt,
638
- model: a.model || desktopModelChain(s),
639
- directory: cwd
640
- })
641
- const okTurn = turn.delivered === true
642
- let text = ''
643
- if (okTurn) {
644
- try {
645
- const ev = await desktopReadEvents(sessionId, cwd, 2500)
646
- text = ev.text || ''
647
- } catch (e) { }
648
- }
649
- return desktopResult(okTurn, {
650
- status: okTurn ? (text ? 'SUCCESS' : 'ACCEPTED') : 'DESKTOP_SESSION_NOT_CREATED',
651
- response: text || (okTurn
652
- ? '(Desktop turn accepted; watch Desktop UI or call mimo_continue for the reply)'
653
- : ''),
654
- sessionId,
655
- accepted: okTurn,
656
- mode,
657
- stderr: okTurn ? '' : 'Desktop API returned ' + turn.status + ' but session ' + turn.id + ' does not exist',
658
- errorMessage: okTurn ? '' : ('MiMo Desktop 未创建会话(API 返回 ' + turn.status +
659
- ' 但会话列表中不存在 ' + turn.id + ')。任务未执行。' +
660
- 'MiMo Desktop API 无法创建会话,请在 Xiaomi MiMo Desktop 应用中新建(或选用现有)' +
661
- '会话,再把它的 id 传给 mimo_run 的 sessionId 参数。')
662
- })
663
- } catch (e) {
664
- return desktopResult(false, {
665
- status: 'DESKTOP_ERROR',
666
- sessionId,
667
- mode,
668
- stderr: String(e && e.message || e),
669
- errorMessage: String(e && e.message || e)
670
- })
671
- }
672
- },
673
-
674
457
  tools: {
675
- run: { name: 'mimo_run', description: 'Dispatch a coding/build/debug/investigation task to the local mimo agent CLI (Xiaomi MiMo Desktop / MiMoCode) and return its final answer. DSH fully controls mimo (--dangerously-skip-permissions; mimo never prompts). On rate-limit/network failure DSH pops a fallback dialog; fallback=true means finish with native tools. Runs in the background by default (returns a jobId immediately; collect with job_output); set background=false to block for the final answer. While it runs, call mimo_status to watch what mimo is doing live.', inputSchema: RUN_PARAMS },
458
+ run: { name: 'mimo_run', description: 'Dispatch a coding/build/debug/investigation task to the local mimo agent CLI (Xiaomi MiMoCode) and return its final answer. DSH fully controls mimo (--dangerously-skip-permissions; mimo never prompts). On rate-limit/network failure DSH pops a fallback dialog; fallback=true means finish with native tools. Runs in the background by default (returns a jobId immediately; collect with job_output); set background=false to block for the final answer. While it runs, call mimo_status to watch what mimo is doing live.', inputSchema: RUN_PARAMS },
676
459
  continue: { name: 'mimo_continue', description: 'Continue an existing mimo session with a follow-up prompt. Pass sessionId or set latest=true. Same DSH-controlled, no-prompt execution and same fallback dialog as mimo_run.', inputSchema: CONTINUE_PARAMS },
677
460
  status: { name: 'mimo_status', description: 'Read a live snapshot of what the local mimo agent is currently doing. Returns one section per project (working directory): running count, current step, recent step trail, last completed run status + session id, per-project runs/token usage. Call this to check on an in-flight mimo_run/mimo_continue without waiting for it to finish.', inputSchema: STATUS_PARAMS }
678
461
  },
679
462
 
680
- // mimo_settings:入参键保持旧 prompt 连续性(backend/desktopModel),落统一分区
681
- // preferredBackend/desktopModel(迁移映射 keystone 已做)。写盘仅在宿主注入
682
- // nodeFs + nodePath + settingsWrite 时可用;否则回显(dynamic 沙箱旧 echo 语义)。
463
+ // mimo_settings:入参键保持旧 prompt 连续性(backend/desktopModel → model),落统一
464
+ // 分区 defaultModel(迁移映射 keystone 已做)。写盘仅在宿主注入 nodeFs + nodePath 时
465
+ // 可用;否则回显(dynamic 沙箱旧 echo 语义)。
683
466
  extraTools: [{
684
467
  name: 'mimo_settings',
685
468
  description: SETTINGS_DESCRIPTION,
686
469
  inputSchema: SETTINGS_PARAMS,
687
470
  render: renderSettings,
688
- async logic(args, exec) {
471
+ async logic(args) {
689
472
  const a = args || {}
690
473
  const live = liveSettings()
691
- const cur = Object.assign({ preferredBackend: DEFAULT_BACKEND, desktopModel: 'mimo-auto' }, live.partition || {})
474
+ const cur = Object.assign({ defaultModel: '' }, live.partition || {})
692
475
  let settingsPath = live.path || null
693
- let saved = false
694
- let saveError = null
695
- const fsMod = nodeFs
696
- const pathMod = nodePath
476
+ let saved = false, saveError = null
697
477
  const home = live.home || deps.dshHome || null
698
- if ((a.backend || a.desktopModel) && fsMod && pathMod && (settingsPath || home)) {
699
- try {
700
- settingsPath = await writePartition(fsMod, pathMod, settingsPath, home, env, cur, a)
701
- saved = true
702
- } catch (e) { saveError = String(e && e.message || e) }
478
+ const patch = (typeof a.model === 'string' && a.model.trim()) ? { defaultModel: a.model.trim() } : null
479
+ if (patch && nodeFs && nodePath && (settingsPath || home)) {
480
+ try { settingsPath = await writePartition(nodeFs, nodePath, settingsPath, home, env, cur, patch); saved = true } catch (e) { saveError = String(e && e.message || e) }
703
481
  }
704
482
  const merged = Object.assign({}, cur)
705
- if (a.backend) merged.preferredBackend = a.backend
706
- if (a.desktopModel) merged.desktopModel = a.desktopModel
707
- const rt = discoverDesktop()
483
+ if (patch) merged.defaultModel = patch.defaultModel
708
484
  return {
709
485
  ok: !saveError,
710
486
  settingsPath: settingsPath || '(unified; not writable in this host)',
711
- fileBackend: merged.preferredBackend || null,
712
- configBackend: (deps.config && deps.config.backend) || null,
713
- envBackend: env.MIMO_BRIDGE_BACKEND || env.MIMO_BACKEND || null,
714
- effectiveBackend: descriptor.resolveBackend({ backend: a.backend }, null, normalizeBackend(env.MIMO_BRIDGE_BACKEND || env.MIMO_BACKEND) || merged.preferredBackend),
715
- desktopModel: merged.desktopModel || 'mimo-auto',
716
- desktopAvailable: !!rt.available,
717
- desktopError: rt.error || null,
718
- desktopApiFile: rt.apiFile || null,
719
- saved,
720
- saveError,
721
- backends: BACKENDS,
722
- note: 'Preselect via settings file / plugin config.backend / MIMO_BRIDGE_BACKEND. Per-call `backend` overrides only that call.'
487
+ defaultModel: merged.defaultModel || '',
488
+ effective: merged.defaultModel ? ('settings:' + merged.defaultModel) : '(unset — 不传 --model,mimo CLI 自决)',
489
+ exe: resolveMimoExePrefix(env).join(' '),
490
+ saved, saveError,
491
+ note: '默认模型在设置面板或本工具里指定(provider/model);mimo_run/mimo_continue 的 model 参数可单次覆盖。可用模型:在本机运行 `mimo models` 查看。'
723
492
  }
724
493
  }
725
494
  }],
726
495
 
727
496
  // 设置分区(契约 §3):keystone 读写/GUI;本处纯逻辑。diagnose/meta 必须同步。
728
497
  settings: {
729
- keys: ['preferredBackend', 'desktopModel'],
730
- defaults: () => ({ preferredBackend: DEFAULT_BACKEND, desktopModel: 'mimo-auto' }),
498
+ keys: ['defaultModel'],
499
+ defaults: () => ({ defaultModel: '' }),
731
500
  normalize(raw) {
732
501
  const src = raw && typeof raw === 'object' ? raw : {}
733
- const out = { preferredBackend: DEFAULT_BACKEND, desktopModel: 'mimo-auto' }
734
- const b = normalizeBackend(src.preferredBackend)
735
- if (b) out.preferredBackend = b
736
- if (typeof src.desktopModel === 'string' && src.desktopModel.trim()) out.desktopModel = src.desktopModel.trim()
737
- return out
502
+ return { defaultModel: (typeof src.defaultModel === 'string' && src.defaultModel.trim()) ? src.defaultModel.trim() : '' }
738
503
  },
739
- meta() {
504
+ meta(envArg) {
505
+ const e = envArg && typeof envArg === 'object' ? envArg : env
506
+ // 下拉选项:实时 `mimo models`(失败则退化为空 → 面板仍可自由输入)。
507
+ let options = []
508
+ try {
509
+ options = fetchMimoModelsSync(e, 8000).map((m) => ({ value: m.id, label: m.id }))
510
+ } catch (err) { options = [] }
740
511
  return {
741
512
  titleSuffix: 'MiMo',
742
- fields: [
743
- { key: 'preferredBackend', label: '预选后端', type: 'select', options: [{ value: 'mimocode', label: 'MiMoCode CLI(本地 mimo 命令,DSH 完全控制)' }, { value: 'mimo-desktop', label: 'MiMo Desktop 客户端(本地 API,共享其登录/模型/会话 UI)' }], note: 'mimo_run/mimo_continue 默认走这里选的后端;单次调用可用 backend 参数覆盖。' },
744
- { key: 'desktopModel', label: '桌面模型', type: 'text', allowCustom: true, placeholder: 'mimo-auto', note: '仅 mimo-desktop 后端使用(Desktop 里的模型 id,如 mimo-auto)。' }
745
- ]
513
+ fields: [{
514
+ key: 'defaultModel', label: '默认模型', type: 'select', allowCustom: true,
515
+ options: options,
516
+ placeholder: '留空 = 不传 --model(由 mimo CLI 自身默认决定)',
517
+ note: options.length
518
+ ? '共 ' + options.length + ' 个可用模型(来自 `mimo models`)。mimo_run/mimo_continue 的 model 参数可单次覆盖。'
519
+ : '未能读取模型列表(mimo CLI 未安装/未登录/超时)—— 仍可手动输入 provider/model,如 xiaomi/mimo-v2.6-flash。'
520
+ }]
746
521
  }
747
522
  },
748
- // 同步一行诊断:探测缓存 + 惰性 health 后台刷新(结果进缓存,下次面板可见)。
749
- diagnose(settings) {
750
- const rt = discoverDesktop()
751
- const f = fetchFn()
752
- if (!f) return ''
753
- if (!rt.available) return '桌面 API 未发现(' + (rt.error || '未配置') + ');mimocode 后端不受影响'
754
- const s = settings && settings.preferredBackend || partitionSettings() && partitionSettings().preferredBackend || DEFAULT_BACKEND
755
- try {
756
- f(rt.baseUrl + DESKTOP_API.healthPath, { headers: { Authorization: 'Bearer ' + rt.token } })
757
- .then((r) => { desktopHealthCache = { ok: r.ok, at: Date.now() } })
758
- .catch(() => { desktopHealthCache = { ok: false, at: Date.now() } })
759
- } catch (e) { desktopHealthCache = { ok: false, at: Date.now() } }
760
- if (desktopHealthCache.at && Date.now() - desktopHealthCache.at < 60000) {
761
- return desktopHealthCache.ok
762
- ? ('桌面 API 在线(' + rt.baseUrl + ');当前预选 ' + s)
763
- : ('桌面 API 不可达(检查 MiMo Desktop 是否在运行);当前预选 ' + s + ',可改用 mimocode')
764
- }
765
- desktopHealthCache = { ok: null, at: Date.now() }
766
- return '桌面 API 已发现(' + rt.baseUrl + (rt.apiFile ? '' : ',env 配置') + ');健康检测中…;当前预选 ' + s
523
+ // 同步一行诊断:当前模型 + exe 来源(env > 已知安装位置 > PATH)。
524
+ diagnose(settings, envArg) {
525
+ const s = settings && typeof settings === 'object' ? settings : {}
526
+ const e = envArg && typeof envArg === 'object' ? envArg : env
527
+ const model = s.defaultModel ? 'model=' + s.defaultModel : 'model=(未设置 → 不传 --model)'
528
+ if (e.MIMO_BIN || e.MIMO_CLI) return model + ' · bin=env:' + (e.MIMO_BIN || e.MIMO_CLI)
529
+ const found = mimoCandidateExePaths(e)
530
+ return model + ' · ' + (found.length ? 'bin=' + found[0] : 'bin=PATH:mimo')
767
531
  }
768
532
  },
769
533
 
770
- mcp: { version: '0.2.2' }
534
+ // MCP 形态扩展:**不含 version**。历史:四处 `mcp: { version: '0.2.2' }` 与
535
+ // mcp/version.mjs 的 packageVersion() 双源,漏改一处就出现「统一 server 报 0.2.3、
536
+ // provider 面仍报 0.2.2」的自相矛盾输出;且 mcp/version.mjs 依赖 import.meta + node:fs,
537
+ // 其函数体无法随本文件内联进 dynamic 沙箱(build 文本拼接会丢实现/炸语法)。
538
+ // 定案:版本号单一来源 = mcp/version.mjs 的 packageVersion(),由 MCP 面(mcp/providers/
539
+ // mimo.mjs 的 meta.version)读取;描述符不复制 version 字面量,宿主如需注入可经 deps.mcp。
540
+ mcp: (deps.mcp && typeof deps.mcp === 'object') ? Object.assign({}, deps.mcp) : {}
771
541
  }
772
542
 
773
- let desktopHealthCache = { ok: null, at: 0 }
774
-
775
543
  // 统一分区写入(tmp→rename 原子写;与 keystone lib/settings.mjs 同文件同分区规则,
776
544
  // 本工具自实现落盘 —— 不 import home-plugin;宿主给出 settingsPath 或 dshHome)。
777
545
  async function writePartition(fsMod, pathMod, knownPath, home, environment, cur, patch) {
778
- const SETTINGS_FILE_NAME = 'cli-bridge-settings.json'
779
- const file = knownPath || environment.CLI_BRIDGE_SETTINGS_FILE || (home ? pathMod.join(home, SETTINGS_FILE_NAME) : null)
546
+ const file = knownPath || environment.CLI_BRIDGE_SETTINGS_FILE || (home ? pathMod.join(home, 'cli-bridge-settings.json') : null)
780
547
  if (!file) throw new Error('dsh-home not resolved (set DSH_HOME or CLI_BRIDGE_SETTINGS_FILE)')
781
548
  let all = {}
782
549
  try { all = JSON.parse(fsMod.readFileSync(file, 'utf8')) } catch (e) { all = {} }
783
550
  if (!all || typeof all !== 'object') all = {}
784
551
  const next = Object.assign({}, all)
785
552
  const part = Object.assign({}, cur, next.mimo || {})
786
- if (patch.backend) part.preferredBackend = normalizeBackend(patch.backend) || patch.backend
787
- if (patch.desktopModel) part.desktopModel = patch.desktopModel
553
+ if (patch && patch.defaultModel) part.defaultModel = patch.defaultModel
788
554
  next.mimo = descriptor.settings.normalize(part)
789
555
  next.updatedAt = Date.now()
790
556
  try { fsMod.mkdirSync(pathMod.dirname(file), { recursive: true }) } catch (e) { }
@@ -795,31 +561,20 @@ export default function defineProvider(deps = {}) {
795
561
  }
796
562
 
797
563
  // 三形态共用的工具逻辑。签名保持不变(keystone 与队友按此接线)。
564
+ // 旧 mimo preset 语义:background 默认 true(仅显式 false 才前台阻塞)。
798
565
  descriptor.runToolLogic = async function (args, exec) {
799
566
  const a = args || {}
800
- // 旧 mimo preset 语义:background 默认 true(仅显式 false 才前台阻塞)。
801
567
  const normalized = a.background === undefined ? Object.assign({}, a, { background: true }) : a
802
568
  return this.runner.coreExecute(normalized, exec)
803
569
  }
804
570
  descriptor.continueToolLogic = async function (args, exec) {
805
571
  const a = args || {}
806
- const mapped = {
807
- prompt: a.prompt,
808
- mode: a.mode,
809
- model: a.model,
810
- effort: a.effort,
811
- cwd: a.cwd,
812
- timeoutSec: a.timeoutSec,
813
- background: a.background,
814
- backend: a.backend,
815
- // 【关键】desktop 后端的 sessionId 必须转发(白名单转发漏了它会导致
816
- // mimo_run 传 sessionId 被静默丢弃 → 永远造新 id → desktop 后端不可用)。
817
- sessionId: a.sessionId
818
- }
572
+ const mapped = { prompt: a.prompt, mode: a.mode, model: a.model, effort: a.effort, cwd: a.cwd, timeoutSec: a.timeoutSec, background: a.background, sessionId: a.sessionId }
819
573
  if (a.sessionId) mapped.sessionId = a.sessionId
820
574
  else if (a.latest) mapped.continueLatest = true
821
575
  return this.runToolLogic(mapped, exec)
822
576
  }
577
+ // 旧 statusTool.execute verbatim:cwd 过滤到单项目(Object.assign 形式与 opencode 一致)。
823
578
  descriptor.statusToolLogic = async function (args) {
824
579
  const a = args || {}
825
580
  const snap = this.engine.statusSnapshot()
@@ -828,37 +583,36 @@ export default function defineProvider(deps = {}) {
828
583
  snap.projects = (snap.projects || []).filter((p) => p.cwd === key)
829
584
  const g = snap.projects[0]
830
585
  if (g) {
831
- snap.state = g.state
832
- snap.running = g.running
833
- snap.current = g.current
834
- snap.trail = g.trail
835
- snap.lastStatus = g.lastStatus
836
- snap.lastAt = g.lastAt
837
- snap.lastSessionId = g.lastSessionId
838
- snap.fallbackActive = g.fallbackActive
839
- snap.runs = g.runs
840
- snap.totalTokens = g.totalTokens
841
- snap.updatedAt = g.updatedAt
586
+ Object.assign(snap, {
587
+ state: g.state, running: g.running, current: g.current, trail: g.trail,
588
+ lastStatus: g.lastStatus, lastAt: g.lastAt, lastSessionId: g.lastSessionId,
589
+ lastOk: g.lastOk, lastFailed: g.lastFailed, fallbackActive: g.fallbackActive,
590
+ runs: g.runs, totalTokens: g.totalTokens, updatedAt: g.updatedAt
591
+ })
842
592
  }
843
593
  }
844
594
  return snap
845
595
  }
846
-
847
- // 工厂装配(provider 无关骨架 + 描述符):宿主形态通过 deps 注入 runner 依赖。
596
+ // 工厂装配(provider 无关骨架 + 描述符);宿主注入 getSettings 时把面板偏好带进 buildArgv。
848
597
  descriptor.attach = function (hostOpts) {
849
- this.engine = createStatusEngine({ provider: this, publish: hostOpts.publish })
850
- const runner = createRunner(Object.assign({}, hostOpts, { provider: this, engine: this.engine }))
598
+ host = hostOpts || {}
599
+ try {
600
+ if (typeof host.getSettings === 'function') {
601
+ const s = host.getSettings('mimo')
602
+ if (s && typeof s === 'object') settingsRef = descriptor.settings.normalize(s)
603
+ }
604
+ } catch (e) { }
605
+ this.engine = createStatusEngine({ provider: this, publish: host.publish })
606
+ const runner = createRunner(Object.assign({}, host, { provider: this, engine: this.engine }))
851
607
  this.runner = runner
852
608
  return runner
853
609
  }
854
-
855
- // 测试/宿主可见的纯函数面(不进协议字段)。
856
- descriptor._internals = {
857
- BACKENDS, BACKEND_MIMOCODE, BACKEND_DESKTOP, DEFAULT_BACKEND, MIMO_LIMIT_RE,
858
- DESKTOP_API, parseMimoJson, buildResult, desktopResult, buildDesktopTurnBody,
859
- newDesktopSessionId, parseDesktopEvents, normalizeBackend, discoverDesktop,
860
- desktopFetch, desktopHealth, desktopSendTurn, desktopReadEvents
610
+ // 测试/宿主可见的纯函数面(不进协议字段)。currentSettings/liveSettings/writePartition 是
611
+ // 闭包函数,不依赖 attach(未 attach 时 host 为 null → 走 deps 注入或默认空值),测试可
612
+ // 直接调用;writePartition 需要调用方自行传入可用的 fs/path 模块。
613
+ descriptor._internals = { MIMO_LIMIT_RE, parseMimoJson, buildResult,
614
+ mimoCandidateExePaths, resolveMimoExePrefix, parseMimoModels, fetchMimoModelsSync,
615
+ currentSettings, liveSettings, writePartition
861
616
  }
862
-
863
617
  return descriptor
864
- }
618
+ }