@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.
- package/CHANGELOG.md +191 -0
- package/LICENSE +21 -0
- package/MCP-POLICY.md +53 -0
- package/PROVIDER-CONTRACT.md +235 -0
- package/README.en.md +98 -0
- package/README.md +151 -2
- package/assets/indicator-states.svg +62 -0
- package/bin/agy-quota.mjs +223 -0
- package/core/bridge-kit.mjs +549 -0
- package/core/scheduling.mjs +149 -0
- package/docs/ARCHITECTURE.md +117 -0
- package/docs/FALLBACK-AND-INDICATOR.md +93 -0
- package/docs/HANDOVER.md +226 -0
- package/docs/INSTALL.md +118 -0
- package/docs/MCP-POLICY.zh.md +49 -0
- package/docs/MIGRATION.md +43 -0
- package/docs/RELEASE-SOP.md +80 -0
- package/docs/history/agy/CHANGELOG.md +326 -0
- package/docs/history/codebuddy/CHANGELOG.md +668 -0
- package/docs/history/mimo/CHANGELOG.md +346 -0
- package/dynamic/agy.host.js +1237 -0
- package/dynamic/agy.host.template.mjs +140 -0
- package/dynamic/codebuddy.host.js +1787 -0
- package/dynamic/codebuddy.host.template.mjs +162 -0
- package/dynamic/mimo.host.js +1732 -0
- package/dynamic/mimo.host.template.mjs +169 -0
- package/dynamic/opencode.host.js +1443 -0
- package/dynamic/opencode.host.template.mjs +125 -0
- package/home-plugin/cli-bridge-indicator/cordis.patch.yml +31 -0
- package/home-plugin/cli-bridge-indicator/lib/client.js +606 -0
- package/home-plugin/cli-bridge-indicator/lib/index.mjs +420 -0
- package/home-plugin/cli-bridge-indicator/lib/preset-declarations.mjs +137 -0
- package/home-plugin/cli-bridge-indicator/lib/settings.mjs +300 -0
- package/mcp/agy-mcp-server.mjs +6 -0
- package/mcp/cli-bridge-mcp.mjs +185 -0
- package/mcp/codebuddy-mcp-server.mjs +6 -0
- package/mcp/mimo-mcp-server.mjs +6 -0
- package/mcp/providers/agy.mjs +254 -0
- package/mcp/providers/codebuddy.mjs +397 -0
- package/mcp/providers/mimo.mjs +290 -0
- package/mcp/providers/opencode.mjs +236 -0
- package/package.json +89 -4
- package/preset/agy-first/agent.cordis.yml +141 -0
- package/preset/agy-first/cli-bridge.mjs +169 -0
- package/preset/agy-first/plugin-list.mjs +26 -0
- package/preset/agy-first/preset.yml +3 -0
- package/preset/codebuddy-first/agent.cordis.yml +141 -0
- package/preset/codebuddy-first/cli-bridge.mjs +188 -0
- package/preset/codebuddy-first/plugin-list.mjs +20 -0
- package/preset/codebuddy-first/preset.yml +3 -0
- package/preset/mimo-first/agent.cordis.yml +143 -0
- package/preset/mimo-first/cli-bridge.mjs +236 -0
- package/preset/mimo-first/plugin-list.mjs +39 -0
- package/preset/mimo-first/preset.yml +3 -0
- package/preset/opencode-first/agent.cordis.yml +141 -0
- package/preset/opencode-first/cli-bridge.mjs +147 -0
- package/preset/opencode-first/plugin-list.mjs +26 -0
- package/preset/opencode-first/preset.yml +3 -0
- package/preset/standard-base.mjs +102 -0
- package/providers/agy.mjs +398 -0
- package/providers/codebuddy.mjs +926 -0
- package/providers/mimo.mjs +864 -0
- package/providers/opencode.mjs +619 -0
- package/providers/registry.mjs +39 -0
- package/scripts/build.mjs +235 -0
- package/scripts/deploy.mjs +61 -0
- 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
|
+
}
|