@luchenglong/cli-bridge 0.0.0-stage → 0.2.3

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 (67) hide show
  1. package/CHANGELOG.md +191 -0
  2. package/LICENSE +21 -0
  3. package/MCP-POLICY.md +53 -0
  4. package/PROVIDER-CONTRACT.md +235 -0
  5. package/README.en.md +98 -0
  6. package/README.md +151 -2
  7. package/assets/indicator-states.svg +62 -0
  8. package/bin/agy-quota.mjs +223 -0
  9. package/core/bridge-kit.mjs +549 -0
  10. package/core/scheduling.mjs +149 -0
  11. package/docs/ARCHITECTURE.md +117 -0
  12. package/docs/FALLBACK-AND-INDICATOR.md +93 -0
  13. package/docs/HANDOVER.md +226 -0
  14. package/docs/INSTALL.md +118 -0
  15. package/docs/MCP-POLICY.zh.md +49 -0
  16. package/docs/MIGRATION.md +43 -0
  17. package/docs/RELEASE-SOP.md +80 -0
  18. package/docs/history/agy/CHANGELOG.md +326 -0
  19. package/docs/history/codebuddy/CHANGELOG.md +668 -0
  20. package/docs/history/mimo/CHANGELOG.md +346 -0
  21. package/dynamic/agy.host.js +1237 -0
  22. package/dynamic/agy.host.template.mjs +140 -0
  23. package/dynamic/codebuddy.host.js +1787 -0
  24. package/dynamic/codebuddy.host.template.mjs +162 -0
  25. package/dynamic/mimo.host.js +1732 -0
  26. package/dynamic/mimo.host.template.mjs +169 -0
  27. package/dynamic/opencode.host.js +1443 -0
  28. package/dynamic/opencode.host.template.mjs +125 -0
  29. package/home-plugin/cli-bridge-indicator/cordis.patch.yml +31 -0
  30. package/home-plugin/cli-bridge-indicator/lib/client.js +606 -0
  31. package/home-plugin/cli-bridge-indicator/lib/index.mjs +420 -0
  32. package/home-plugin/cli-bridge-indicator/lib/preset-declarations.mjs +137 -0
  33. package/home-plugin/cli-bridge-indicator/lib/settings.mjs +300 -0
  34. package/mcp/agy-mcp-server.mjs +6 -0
  35. package/mcp/cli-bridge-mcp.mjs +185 -0
  36. package/mcp/codebuddy-mcp-server.mjs +6 -0
  37. package/mcp/mimo-mcp-server.mjs +6 -0
  38. package/mcp/providers/agy.mjs +254 -0
  39. package/mcp/providers/codebuddy.mjs +397 -0
  40. package/mcp/providers/mimo.mjs +290 -0
  41. package/mcp/providers/opencode.mjs +236 -0
  42. package/package.json +89 -4
  43. package/preset/agy-first/agent.cordis.yml +141 -0
  44. package/preset/agy-first/cli-bridge.mjs +169 -0
  45. package/preset/agy-first/plugin-list.mjs +26 -0
  46. package/preset/agy-first/preset.yml +3 -0
  47. package/preset/codebuddy-first/agent.cordis.yml +141 -0
  48. package/preset/codebuddy-first/cli-bridge.mjs +188 -0
  49. package/preset/codebuddy-first/plugin-list.mjs +20 -0
  50. package/preset/codebuddy-first/preset.yml +3 -0
  51. package/preset/mimo-first/agent.cordis.yml +143 -0
  52. package/preset/mimo-first/cli-bridge.mjs +236 -0
  53. package/preset/mimo-first/plugin-list.mjs +39 -0
  54. package/preset/mimo-first/preset.yml +3 -0
  55. package/preset/opencode-first/agent.cordis.yml +141 -0
  56. package/preset/opencode-first/cli-bridge.mjs +147 -0
  57. package/preset/opencode-first/plugin-list.mjs +26 -0
  58. package/preset/opencode-first/preset.yml +3 -0
  59. package/preset/standard-base.mjs +102 -0
  60. package/providers/agy.mjs +398 -0
  61. package/providers/codebuddy.mjs +926 -0
  62. package/providers/mimo.mjs +864 -0
  63. package/providers/opencode.mjs +619 -0
  64. package/providers/registry.mjs +39 -0
  65. package/scripts/build.mjs +235 -0
  66. package/scripts/deploy.mjs +61 -0
  67. package/scripts/verify.mjs +148 -0
@@ -0,0 +1,864 @@
1
+ // providers/mimo.mjs — mimo provider descriptor(真移植,替换 Lead 冒烟占位)。
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)。
8
+ //
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/重试/弹窗。
16
+ import { createStatusEngine, createRunner, clampInt, shortLabel, summarizeArgs } from '../core/bridge-kit.mjs'
17
+
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
23
+
24
+ // 旧 LIMIT_RE(比 kit 版多 free-API/登录/第三方等 MiMo 文案;provider.isLimited 覆盖)。
25
+ // 命名 MIMO_ 前缀:dynamic 内联时 kit 也声明顶层 LIMIT_RE,同名会在同一脚本作用域
26
+ // 触发「Identifier already declared」构建错误(scripts/build.mjs 文本拼接的固有约束)。
27
+ 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
+
29
+ const DEFAULT_TIMEOUT_SEC = 600
30
+
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))) : '')
37
+ }
38
+
39
+ // 工具三件套 schema(旧 preset ctx.tools.register parameters verbatim,转 JSON Schema
40
+ // 形态对齐 agy 描述符约定;required 进顶层数组,其余字段语义逐字保持)。
41
+ const RUN_PARAMS = {
42
+ type: 'object',
43
+ additionalProperties: true,
44
+ required: ['prompt'],
45
+ properties: {
46
+ 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
+ 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).' },
52
+ timeoutSec: { type: 'integer', description: 'Hard timeout seconds (10-3600, default 600). Grace +60s on top.' },
53
+ 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.' },
57
+ title: { type: 'string', description: 'Optional session title.' }
58
+ }
59
+ }
60
+ const CONTINUE_PARAMS = {
61
+ type: 'object',
62
+ additionalProperties: true,
63
+ required: ['prompt'],
64
+ properties: {
65
+ prompt: { type: 'string', description: 'Follow-up instruction for the ongoing mimo session. State only what is new; mimo preserves its conversation context.' },
66
+ sessionId: { type: 'string', description: 'mimo session id to resume (--session).' },
67
+ 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
+ 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.' },
71
+ effort: { type: 'string', enum: ['low', 'medium', 'high'], description: 'Optional reasoning effort.' },
72
+ cwd: { type: 'string', description: 'Working directory for mimo.' },
73
+ timeoutSec: { type: 'integer', description: 'Hard timeout seconds (10-3600, default 600).' },
74
+ background: { type: 'boolean', description: 'Defaults to true (background job).' }
75
+ }
76
+ }
77
+ 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.'
88
+
89
+ function renderSettings(value) {
90
+ const s = value || {}
91
+ const lines = [
92
+ 'mimo settings',
93
+ ' 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(' | ')))
103
+ ]
104
+ if (s.note) lines.push(' note : ' + s.note)
105
+ return [{ type: 'text', text: lines.join('\n') }]
106
+ }
107
+
108
+ // ── 可纯函数化的 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
+ function parseMimoJson(stdoutText) {
118
+ const trimmed = String(stdoutText || '').trim()
119
+ if (!trimmed) return null
120
+ const MAX_ARG_LEN = 120
121
+ const MAX_TRAIL = 12
122
+ const events = []
123
+ const lines = trimmed.split(/\r?\n/)
124
+ for (const raw of lines) {
125
+ const ln = raw.trim()
126
+ if (!ln.startsWith('{')) continue
127
+ try {
128
+ const obj = JSON.parse(ln)
129
+ if (obj && typeof obj === 'object') events.push(obj)
130
+ } catch (e) { /* tolerate log lines */ }
131
+ }
132
+ if (!events.length) {
133
+ try {
134
+ const obj = JSON.parse(trimmed)
135
+ if (obj && typeof obj === 'object') events.push(obj)
136
+ } catch (e) {
137
+ return { events: [], text: trimmed, sessionId: null, error: null, steps: [] }
138
+ }
139
+ }
140
+ let sessionId = null
141
+ let text = ''
142
+ let errorMessage = null
143
+ let errorName = null
144
+ let numTurns = 0
145
+ let totalTokens = null
146
+ const steps = []
147
+ const textParts = []
148
+ for (const ev of events) {
149
+ if (ev.sessionID && typeof ev.sessionID === 'string') sessionId = ev.sessionID
150
+ if (ev.sessionId && typeof ev.sessionId === 'string') sessionId = ev.sessionId
151
+ if (ev.session && typeof ev.session === 'object' && ev.session.id) sessionId = ev.session.id
152
+ if (ev.type === 'session' && ev.properties && ev.properties.id) sessionId = ev.properties.id
153
+ const t = ev.type
154
+ if (t === 'error') {
155
+ const err = ev.error || ev.properties || {}
156
+ errorMessage = (err.data && err.data.message) || err.message || err.name || 'error'
157
+ errorName = err.name || 'Error'
158
+ } else if (t === 'text' || t === 'message' || t === 'assistant' || t === 'output') {
159
+ const p = ev.properties || ev
160
+ const chunk = p.text || p.content || p.message || p.output || (typeof ev.text === 'string' ? ev.text : '')
161
+ if (typeof chunk === 'string' && chunk) {
162
+ textParts.push(chunk)
163
+ steps.push({ at: Date.now(), kind: 'text', label: shortLabel(chunk) })
164
+ }
165
+ } else if (t === 'step-start' || t === 'step-finish' || t === 'step_update') {
166
+ numTurns += 1
167
+ const p = ev.properties || ev
168
+ if (p.tokens && typeof p.tokens === 'object') {
169
+ const tt = p.tokens.total ?? p.tokens.input ?? p.tokens.output
170
+ if (typeof tt === 'number') totalTokens = (totalTokens || 0) + tt
171
+ }
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
+ })
178
+ }
179
+ } else if (t === 'tool' || t === 'tool-call' || t === 'tool_use') {
180
+ const p = ev.properties || ev
181
+ 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
+ })
187
+ }
188
+ }
189
+ if (textParts.length) text = textParts.join('\n')
190
+ else {
191
+ const last = events[events.length - 1] || {}
192
+ const r = last.result || last
193
+ if (typeof r.response === 'string') text = r.response
194
+ else if (typeof r.text === 'string') text = r.text
195
+ }
196
+ return { events, text, sessionId, error: errorMessage, errorName, numTurns, totalTokens, steps }
197
+ }
198
+
199
+ function buildResult(parsed, outcome, mode, stderrText, stdoutText, backend) {
200
+ const MAX_TRAIL = 12
201
+ const exitCode = outcome ? outcome.exitCode : null
202
+ const errText = (parsed && parsed.error) ? String(parsed.error) : ''
203
+ const stderr = (stderrText ? String(stderrText).slice(-2000) : '') + (errText ? (stderrText ? ' ' : '') + errText : '')
204
+ const text = parsed && typeof parsed.text === 'string' ? parsed.text : ''
205
+ const failed = exitCode !== 0 || !!(parsed && parsed.error)
206
+ const be = (parsed && parsed.backend) || (outcome && outcome.backend) || backend || null
207
+ if (parsed) {
208
+ return {
209
+ ok: !failed && (!!text || parsed.accepted === true),
210
+ status: failed ? (parsed.errorName || 'ERROR') : (parsed.accepted && !text ? 'ACCEPTED' : (text ? 'SUCCESS' : 'EMPTY')),
211
+ response: text,
212
+ sessionId: parsed.sessionId || null,
213
+ durationSeconds: null,
214
+ numTurns: parsed.numTurns || null,
215
+ totalTokens: parsed.totalTokens || null,
216
+ exitCode, mode, backend: be,
217
+ stderr,
218
+ errorMessage: parsed.error || '',
219
+ accepted: parsed.accepted === true,
220
+ steps: Array.isArray(parsed.steps) ? parsed.steps.slice(-MAX_TRAIL) : []
221
+ }
222
+ }
223
+ return {
224
+ ok: false, status: 'PARSE_ERROR', response: '', sessionId: null,
225
+ durationSeconds: null, numTurns: null, totalTokens: null,
226
+ exitCode, mode, backend: be, stderr, errorMessage: '',
227
+ rawStdout: String(stdoutText || '').slice(-2000)
228
+ }
229
+ }
230
+
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
+ export default function defineProvider(deps = {}) {
312
+ const env = deps.env || {}
313
+ const nodeFs = deps.nodeFs || null
314
+ 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 }
341
+ }
342
+ } catch (e) {
343
+ d.error = String(e && e.message || e)
344
+ }
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() {
391
+ 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
+ }
428
+ }
429
+
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
+ }
437
+ }
438
+
439
+ // 每次调用前刷一次发现结果(Desktop 可能中途启动/退出;env-only 形态零开销)。
440
+ let desktop = discoverDesktop()
441
+
442
+ // ── 统一设置分区读取(keystone 落盘;descriptor 只读)。宿主注入 deps.readSettings
443
+ // 动态读取(preset/MCP 每次取最新文件值),或静态 deps.settings(测试/dynamic)。
444
+ function liveSettings() {
445
+ if (typeof deps.readSettings === 'function') {
446
+ try {
447
+ const r = deps.readSettings()
448
+ if (r && typeof r === 'object') {
449
+ // 宿主可返回 { partition, path, home } 或直接返回分区对象(宽松兼容)。
450
+ if ('partition' in r || 'path' in r || 'home' in r) {
451
+ return { partition: r.partition || null, path: r.path || null, home: r.home || null }
452
+ }
453
+ return { partition: r, path: deps.settingsPath || null, home: deps.dshHome || null }
454
+ }
455
+ } catch (e) { }
456
+ }
457
+ return { partition: (deps.settings && typeof deps.settings === 'object') ? deps.settings : null, path: deps.settingsPath || null, home: deps.dshHome || null }
458
+ }
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
+
472
+ const descriptor = {
473
+ id: 'mimo', label: 'MIMO', presetId: 'cordis-mimo', defaultBackend: DEFAULT_BACKEND, defaultMode: 'auto',
474
+ exeName: 'mimo', binEnvVars: ['MIMO_BIN', 'MIMO_CLI'], unavailableStatus: 'MIMO_UNAVAILABLE',
475
+
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 },
489
+
490
+ buildArgv(exePrefix, args, opts) {
491
+ const o = opts || {}
492
+ 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
+ const argv = Array.isArray(exePrefix) ? exePrefix.slice() : [String(exePrefix)]
498
+ argv.push('run')
499
+ if (args.prompt != null && String(args.prompt).length) argv.push(String(args.prompt))
500
+ argv.push('--format', 'json', '--dangerously-skip-permissions')
501
+ let mode = args.mode || o.defaultMode || 'auto'
502
+ if (mode === 'auto') mode = o.planActive ? 'plan' : 'build'
503
+ if (mode === 'accept-edits') mode = 'build'
504
+ // mimo --agent: build | plan | compose
505
+ const agent = args.agent || mode
506
+ 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))
509
+ // effort (low/medium/high) maps to mimo --variant; pass-through if already a variant name
510
+ let variant = args.variant || args.effort
511
+ if (variant === 'low') variant = 'minimal'
512
+ else if (variant === 'medium') variant = undefined
513
+ else if (variant === 'high') variant = 'high'
514
+ if (variant) argv.push('--variant', String(variant))
515
+ if (args.cwd) argv.push('--dir', String(args.cwd))
516
+ if (args.sessionId) argv.push('--session', String(args.sessionId))
517
+ else if (args.continueLatest) argv.push('--continue')
518
+ if (args.title) argv.push('--title', String(args.title))
519
+ return { argv, timeoutSec, mode: agent }
520
+ },
521
+
522
+ parseJson: parseMimoJson,
523
+ buildResult,
524
+
525
+ // 旧 foldStepUpdate 语义 verbatim(识别 tool/text/error 事件),但 trail 条目同时
526
+ // 带 kit 展示层字段(stepIndex/tool/state/args)与旧字段(kind/label),
527
+ // 这样 renderStatus 与 MCP 快照映射(buildMcpBridgePayload)都能直接渲染。
528
+ foldStreamEvent(ev, p, summarizeArgsFn, maxArgLen, maxTrail) {
529
+ if (!ev || typeof ev !== 'object') return
530
+ const t = ev.type
531
+ const pr = ev.properties || ev
532
+ const cap = maxTrail || 12
533
+ const idx = p.trail ? p.trail.length : 0
534
+ let entry = null
535
+ if (t === 'tool' || t === 'tool-call' || t === 'tool_use' || t === 'step_update') {
536
+ const name = pr.tool_name || pr.tool || pr.name
537
+ if (name) {
538
+ const args = (summarizeArgsFn || summarizeArgs)(pr.arguments || pr.args || pr.input) || null
539
+ const state = pr.state || pr.status || 'ACTIVE'
540
+ entry = {
541
+ at: Date.now(), kind: 'tool', stepIndex: idx, tool: String(name), args, state,
542
+ label: shortLabel(name + ' ' + JSON.stringify(args || {}))
543
+ }
544
+ if (state === 'ACTIVE') p.current = { tool: String(name), args, stepIndex: idx, since: entry.at }
545
+ else if (p.current && p.current.tool === String(name) && p.current.stepIndex === idx) p.current = null
546
+ }
547
+ } else if (t === 'text' || t === 'message') {
548
+ const chunk = pr.text || pr.content || ''
549
+ if (typeof chunk === 'string' && chunk) {
550
+ const capLen = maxArgLen || 120
551
+ entry = { at: Date.now(), kind: 'text', stepIndex: idx, tool: 'agent_response', args: { text: chunk.slice(0, capLen) }, state: 'ACTIVE', label: shortLabel(chunk) }
552
+ p.current = { tool: 'agent_response', args: entry.args, stepIndex: idx, since: entry.at }
553
+ }
554
+ } else if (t === 'error') {
555
+ const err = pr.error || pr
556
+ const msg = (err.data && err.data.message) || err.message || 'error'
557
+ entry = { at: Date.now(), kind: 'error', stepIndex: idx, tool: 'error', args: { message: String(msg).slice(0, maxArgLen || 120) }, state: 'FAILED', label: shortLabel(msg) }
558
+ }
559
+ if (entry && p.trail) {
560
+ p.trail.push(entry)
561
+ while (p.trail.length > cap) p.trail.shift()
562
+ }
563
+ },
564
+
565
+ // 旧 LIMIT_RE 覆盖(kit 版不含 free API/登录/第三方等 MiMo 文案)。
566
+ isLimited(res) {
567
+ if (!res || res.ok) return false
568
+ if (res.status === 'SPAWN_ERROR' || res.status === 'MIMO_UNAVAILABLE' || res.status === 'HUNG_TIMEOUT') return true
569
+ const hay = String(res.stderr || '') + ' ' + String(res.response || '') + ' ' + String(res.status || '') + ' ' + String(res.errorMessage || '')
570
+ return MIMO_LIMIT_RE.test(hay)
571
+ },
572
+
573
+ failureHint(res) {
574
+ if (!res || res.ok) return ''
575
+ 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 登录态。'
581
+ }
582
+ return ''
583
+ },
584
+
585
+ // ACCEPTED 计入成功态(指示灯不红;契约 §1)。
586
+ isSuccessStatus(st) {
587
+ if (!st) return false
588
+ return st === 'SUCCESS' || st === 'ACCEPTED'
589
+ },
590
+
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
+ 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 },
676
+ 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
+ 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
+ },
679
+
680
+ // mimo_settings:入参键保持旧 prompt 连续性(backend/desktopModel),落统一分区
681
+ // preferredBackend/desktopModel(迁移映射 keystone 已做)。写盘仅在宿主注入
682
+ // nodeFs + nodePath + settingsWrite 时可用;否则回显(dynamic 沙箱旧 echo 语义)。
683
+ extraTools: [{
684
+ name: 'mimo_settings',
685
+ description: SETTINGS_DESCRIPTION,
686
+ inputSchema: SETTINGS_PARAMS,
687
+ render: renderSettings,
688
+ async logic(args, exec) {
689
+ const a = args || {}
690
+ const live = liveSettings()
691
+ const cur = Object.assign({ preferredBackend: DEFAULT_BACKEND, desktopModel: 'mimo-auto' }, live.partition || {})
692
+ let settingsPath = live.path || null
693
+ let saved = false
694
+ let saveError = null
695
+ const fsMod = nodeFs
696
+ const pathMod = nodePath
697
+ 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) }
703
+ }
704
+ 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()
708
+ return {
709
+ ok: !saveError,
710
+ 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.'
723
+ }
724
+ }
725
+ }],
726
+
727
+ // 设置分区(契约 §3):keystone 读写/GUI;本处纯逻辑。diagnose/meta 必须同步。
728
+ settings: {
729
+ keys: ['preferredBackend', 'desktopModel'],
730
+ defaults: () => ({ preferredBackend: DEFAULT_BACKEND, desktopModel: 'mimo-auto' }),
731
+ normalize(raw) {
732
+ 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
738
+ },
739
+ meta() {
740
+ return {
741
+ 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
+ ]
746
+ }
747
+ },
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
767
+ }
768
+ },
769
+
770
+ mcp: { version: '0.2.2' }
771
+ }
772
+
773
+ let desktopHealthCache = { ok: null, at: 0 }
774
+
775
+ // 统一分区写入(tmp→rename 原子写;与 keystone lib/settings.mjs 同文件同分区规则,
776
+ // 本工具自实现落盘 —— 不 import home-plugin;宿主给出 settingsPath 或 dshHome)。
777
+ 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)
780
+ if (!file) throw new Error('dsh-home not resolved (set DSH_HOME or CLI_BRIDGE_SETTINGS_FILE)')
781
+ let all = {}
782
+ try { all = JSON.parse(fsMod.readFileSync(file, 'utf8')) } catch (e) { all = {} }
783
+ if (!all || typeof all !== 'object') all = {}
784
+ const next = Object.assign({}, all)
785
+ 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
788
+ next.mimo = descriptor.settings.normalize(part)
789
+ next.updatedAt = Date.now()
790
+ try { fsMod.mkdirSync(pathMod.dirname(file), { recursive: true }) } catch (e) { }
791
+ const tmp = file + '.tmp-' + ((typeof process !== 'undefined' && process.pid) || 'mimo')
792
+ fsMod.writeFileSync(tmp, JSON.stringify(next, null, 2), 'utf8')
793
+ fsMod.renameSync(tmp, file)
794
+ return file
795
+ }
796
+
797
+ // 三形态共用的工具逻辑。签名保持不变(keystone 与队友按此接线)。
798
+ descriptor.runToolLogic = async function (args, exec) {
799
+ const a = args || {}
800
+ // 旧 mimo preset 语义:background 默认 true(仅显式 false 才前台阻塞)。
801
+ const normalized = a.background === undefined ? Object.assign({}, a, { background: true }) : a
802
+ return this.runner.coreExecute(normalized, exec)
803
+ }
804
+ descriptor.continueToolLogic = async function (args, exec) {
805
+ 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
+ }
819
+ if (a.sessionId) mapped.sessionId = a.sessionId
820
+ else if (a.latest) mapped.continueLatest = true
821
+ return this.runToolLogic(mapped, exec)
822
+ }
823
+ descriptor.statusToolLogic = async function (args) {
824
+ const a = args || {}
825
+ const snap = this.engine.statusSnapshot()
826
+ if (a.cwd) {
827
+ const key = String(a.cwd)
828
+ snap.projects = (snap.projects || []).filter((p) => p.cwd === key)
829
+ const g = snap.projects[0]
830
+ 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
842
+ }
843
+ }
844
+ return snap
845
+ }
846
+
847
+ // 工厂装配(provider 无关骨架 + 描述符):宿主形态通过 deps 注入 runner 依赖。
848
+ 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 }))
851
+ this.runner = runner
852
+ return runner
853
+ }
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
861
+ }
862
+
863
+ return descriptor
864
+ }