@luchenglong/cli-bridge 0.2.4 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -0
- package/docs/HANDOVER.md +201 -51
- package/dynamic/agy.host.js +1 -1
- package/dynamic/codebuddy.host.js +1063 -925
- package/dynamic/codebuddy.host.template.mjs +1 -0
- package/dynamic/mimo.host.js +314 -548
- package/dynamic/mimo.host.template.mjs +43 -30
- package/dynamic/opencode.host.js +32 -22
- package/home-plugin/cli-bridge-indicator/lib/preset-declarations.mjs +11 -14
- package/mcp/cli-bridge-mcp.mjs +1 -1
- package/mcp/providers/codebuddy.mjs +427 -398
- package/mcp/providers/mimo.mjs +37 -68
- package/mcp/providers/opencode.mjs +28 -1
- package/package.json +2 -1
- package/preset/codebuddy-first/cli-bridge.mjs +12 -0
- package/preset/mimo-first/cli-bridge.mjs +32 -50
- package/providers/codebuddy-catalog.mjs +432 -0
- package/providers/codebuddy.mjs +641 -926
- package/providers/mimo.mjs +271 -517
- package/providers/opencode.mjs +31 -21
- package/scripts/build.mjs +23 -5
package/dynamic/mimo.host.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// 构建(node scripts/build.mjs --only=mimo)把 core/bridge-kit.mjs 与
|
|
4
4
|
// providers/mimo.mjs 分别内联进下方两个占位标记行(各占一行、行内仅此标记),
|
|
5
5
|
// 生成 dynamic/mimo.host.js。注意:标记字面量绝不能出现在本注释里 —— 构建做全局
|
|
6
|
-
//
|
|
6
|
+
// 替换,注释中出现同样也会被替换成正文代码而炸掉语法。
|
|
7
7
|
// 沙箱纪律(旧仓 dynamic/host.js 头注 verbatim 精神):
|
|
8
8
|
// - 禁 import/require、无 TypeScript;无 AbortController/process 裸引用/Buffer/原生
|
|
9
9
|
// 定时器 —— process 只经 typeof 守卫探测;宿主能力全部来自注入的 harness.*/ctx。
|
|
@@ -12,14 +12,9 @@
|
|
|
12
12
|
// - 无 ctx.emit:状态经 ctx.get('cliBridgeCollector').mergeSnapshot('mimo', snap)
|
|
13
13
|
// 推给家级灯(旧 mimoCollector 服务名在 keystone 里保留别名);动态形态不宣告
|
|
14
14
|
// mode(旧语义:普通模式灯仅在调用 mimo 时临时显示)。
|
|
15
|
-
// - mimo-desktop 在沙箱的形态(契约 §9):无 fs 发现 desktop-api.json,且多数沙箱
|
|
16
|
-
// 无 fetch —— 描述符 preflight 直接 abort 为 DESKTOP_UNREACHABLE 并提示改用
|
|
17
|
-
// backend=mimocode(旧仓返回 DESKTOP_UNSUPPORTED 的替代;语义升级,Lead 定案)。
|
|
18
|
-
// 宿主若注入了 globalThis.fetch 且给出 MIMO_DESKTOP_PORT/MIMO_DESKTOP_TOKEN env,
|
|
19
|
-
// 委托 execute 仍可走通(fetch 路径全部 typeof 守卫)。
|
|
20
15
|
// - mimo_settings 在沙箱不持久化(无 nodeFs → 描述符回显语义;旧 dynamic echo 形态)。
|
|
21
|
-
// -
|
|
22
|
-
//
|
|
16
|
+
// - 单后端(本地 mimo CLI / MiMoCode):exe 解析走 agy 模板同款链路 ——
|
|
17
|
+
// MIMO_BIN/MIMO_CLI env > 已知安装位置 > subprocess.resolveExecutable(PATH)。
|
|
23
18
|
// core/bridge-kit.mjs — CLI Bridge 单一事实来源:通用执行/状态骨架。
|
|
24
19
|
//
|
|
25
20
|
// 深度重构核心(合并 agy-first-bridge / codebuddy-first-bridge / mimo-first-bridge):
|
|
@@ -570,75 +565,152 @@ function createRunner(o) {
|
|
|
570
565
|
return { coreExecute, runSync, askFallback }
|
|
571
566
|
}
|
|
572
567
|
|
|
573
|
-
// providers/mimo.mjs — mimo provider descriptor
|
|
568
|
+
// providers/mimo.mjs — mimo provider descriptor(单后端:本地 mimo CLI / MiMoCode)。
|
|
574
569
|
//
|
|
575
|
-
// 移植基线:mimo-first-bridge v1.x
|
|
576
|
-
//
|
|
577
|
-
//
|
|
578
|
-
//
|
|
579
|
-
// 回退弹窗/展示层/超时护栏)在 core/bridge-kit.mjs(PROVIDER-CONTRACT.md §1)。
|
|
570
|
+
// 移植基线:mimo-first-bridge v1.x 仓 core/mimo-core.mjs + core/runtime-settings.mjs +
|
|
571
|
+
// preset/mimo-first/mimo-first-bridge.mjs 的内联逻辑(buildArgv / parseMimoJson /
|
|
572
|
+
// buildResult / 三工具 schema / mimo_settings),语义逐字保持;与 provider 无关的骨架
|
|
573
|
+
// (状态引擎/行流/回退弹窗/展示层/超时护栏)在 core/bridge-kit.mjs(契约 §1)。
|
|
580
574
|
//
|
|
581
|
-
//
|
|
582
|
-
//
|
|
583
|
-
//
|
|
584
|
-
//
|
|
585
|
-
//
|
|
586
|
-
//
|
|
587
|
-
//
|
|
588
|
-
|
|
589
|
-
//
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
575
|
+
// 2026-10 去 desktop 化(用户定案:MiMo 免费 API 服务已被官方终止 + 本机 0 凭据):
|
|
576
|
+
// 1. 取消 mimo-desktop 后端 —— 只走 `mimo run`(MiMoCode CLI,官方便携安装位置
|
|
577
|
+
// ~/.mimocode/bin/mimo.exe);登录由用户在 CLI 侧 `mimo auth login` 自理。
|
|
578
|
+
// 2. 模型由用户在设置里手动指定(settings.defaultModel,不写死默认值);留空 =
|
|
579
|
+
// 不传 --model,交给 mimo CLI 自身默认。可用模型以 `mimo models` 实测为准
|
|
580
|
+
// (xiaomi/mimo-v2.6-flash / -pro / -pro-ultraspeed 等)。
|
|
581
|
+
// 3. 是否启用 mimo 由调度(_scheduling.enabled)控制;本描述符不含任何开关。
|
|
582
|
+
//
|
|
583
|
+
// 红线(PROVIDER-CONTRACT.md §8 + Lead 定案):本文件会被 build 内联进 dynamic 沙箱
|
|
584
|
+
// (new Function)—— 绝不出现 import.meta 字面量;process/fs/child_process 只经
|
|
585
|
+
// globalThis.process.getBuiltinModule + typeof 守卫读取;只允许 import bridge-kit。
|
|
586
|
+
// import { createStatusEngine, createRunner, clampInt, shortLabel, summarizeArgs } from '../core/bridge-kit.mjs'
|
|
587
|
+
// ── 单后端归一(旧 mimo-core 双后端 verbatim 的收缩形态)───────────────────────
|
|
588
|
+
// mimo-desktop 取消后不再有第二后端:backends() 恒 null;任何输入(含历史别名
|
|
589
|
+
// mimocode / mimo-cli / desktop / 空串 / 非法值)一律归一为 null。会话路由由
|
|
590
|
+
// engine.resolveTarget(cwd + sessionId)完成,backend 恒 null。
|
|
591
|
+
const DEFAULT_TIMEOUT_SEC = 600
|
|
594
592
|
|
|
595
593
|
// 旧 LIMIT_RE(比 kit 版多 free-API/登录/第三方等 MiMo 文案;provider.isLimited 覆盖)。
|
|
596
594
|
// 命名 MIMO_ 前缀:dynamic 内联时 kit 也声明顶层 LIMIT_RE,同名会在同一脚本作用域
|
|
597
595
|
// 触发「Identifier already declared」构建错误(scripts/build.mjs 文本拼接的固有约束)。
|
|
598
596
|
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
|
|
599
597
|
|
|
600
|
-
|
|
598
|
+
// ── Node 内建探测(沙箱纪律:getBuiltinModule + typeof 守卫)───────────────────
|
|
599
|
+
// 名字带 mimo 前缀:本文件会与 bridge-kit / core/scheduling.mjs【文本内联进同一个动态
|
|
600
|
+
// 沙箱脚本作用域】,撞名即「Identifier already declared」构建错误。
|
|
601
|
+
function mimoBuiltin(name) {
|
|
602
|
+
try {
|
|
603
|
+
const proc = globalThis.process
|
|
604
|
+
return (proc && typeof proc.getBuiltinModule === 'function') ? proc.getBuiltinModule(name) : null
|
|
605
|
+
} catch (e) { return null }
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// ── 可执行文件解析(env MIMO_BIN/MIMO_CLI > 已知安装位置 > PATH 名字)───────────
|
|
609
|
+
// MiMoCode CLI 官方便携安装目录:<USERPROFILE|HOME>/.mimocode/bin/mimo[.exe]。
|
|
610
|
+
// 沙箱内可能没有 fs —— 全部读取走 typeof 守卫,读不到就返回空数组(退回 PATH 查找)。
|
|
611
|
+
const MIMO_EXE_DIR = ['.mimocode', 'bin']
|
|
612
|
+
|
|
613
|
+
/** 探测本机 mimo 可执行文件候选路径(官方便携安装位置);无 fs 能力时返回空数组。 */
|
|
614
|
+
function mimoCandidateExePaths(env) {
|
|
615
|
+
const e = env || {}
|
|
616
|
+
const out = []
|
|
617
|
+
const fs = mimoBuiltin('node:fs')
|
|
618
|
+
if (!fs || typeof fs.existsSync !== 'function') return out
|
|
619
|
+
const path = mimoBuiltin('node:path')
|
|
620
|
+
const join = (path && typeof path.join === 'function') ? ((a, b) => path.join(a, b)) : ((a, b) => a + '/' + b)
|
|
621
|
+
for (const k of ['USERPROFILE', 'HOME']) {
|
|
622
|
+
if (!e[k]) continue
|
|
623
|
+
const dir = join(join(String(e[k]), MIMO_EXE_DIR[0]), MIMO_EXE_DIR[1])
|
|
624
|
+
for (const base of ['mimo.exe', 'mimo']) {
|
|
625
|
+
const exe = join(dir, base)
|
|
626
|
+
try { if (fs.existsSync(exe) && out.indexOf(exe) < 0) out.push(exe) } catch (err) { /* 探测失败静默跳过 */ }
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
return out
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* 解析本次调用使用的 exe 前缀(数组或字符串)。
|
|
634
|
+
* 优先级:binPath(显式路径)> env MIMO_BIN/MIMO_CLI > 已知安装位置 > PATH 名字。
|
|
635
|
+
*/
|
|
636
|
+
function resolveMimoExePrefix(env, binPath) {
|
|
637
|
+
const e = env || {}
|
|
638
|
+
const bin = binPath || e.MIMO_BIN || e.MIMO_CLI || ''
|
|
639
|
+
if (bin) {
|
|
640
|
+
if (/\.(mjs|cjs|js)$/i.test(bin)) {
|
|
641
|
+
const proc = globalThis.process
|
|
642
|
+
return [(proc && proc.execPath) ? proc.execPath : 'node', bin]
|
|
643
|
+
}
|
|
644
|
+
return [String(bin)]
|
|
645
|
+
}
|
|
646
|
+
const found = mimoCandidateExePaths(e)
|
|
647
|
+
return found.length ? [found[0]] : ['mimo']
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// ── 模型列表(`mimo models`;设置面板下拉用)───────────────────────────────────
|
|
651
|
+
// 实测输出形如每行 `provider/model — window 1M, compacts at 900K`(分隔符是中文破折号
|
|
652
|
+
// U+2014)。模型名自身含连字符(xiaomi/mimo-v2.6-pro-ultraspeed),所以只认「行首的
|
|
653
|
+
// provider/model」,其后一律当描述文本;表头/说明/空行一律丢弃,绝不抛。
|
|
654
|
+
const MIMO_MODEL_ID_RE = /^([A-Za-z0-9_.-]+\/[A-Za-z0-9_.@#+-]+)(?=$|[\s—–:|,])/
|
|
655
|
+
|
|
656
|
+
/** 解析 `mimo models` 文本输出为 [{id, provider, name, note}](去重、保序)。 */
|
|
657
|
+
function parseMimoModels(text) {
|
|
658
|
+
const out = []
|
|
659
|
+
const seen = Object.create(null)
|
|
660
|
+
for (const raw of String(text || '').split(/\r?\n/)) {
|
|
661
|
+
// 去掉列表记号(• * - 1. 等)与行首空白,避免把记号当 provider 名。
|
|
662
|
+
const ln = raw.replace(/^[^A-Za-z0-9]+/, '').trim()
|
|
663
|
+
if (!ln) continue
|
|
664
|
+
const m = ln.match(MIMO_MODEL_ID_RE)
|
|
665
|
+
if (!m) continue
|
|
666
|
+
const id = m[1]
|
|
667
|
+
if (seen[id]) continue
|
|
668
|
+
seen[id] = true
|
|
669
|
+
const slash = id.indexOf('/')
|
|
670
|
+
out.push({ id: id, provider: id.slice(0, slash), name: id.slice(slash + 1), note: ln.slice(id.length).replace(/^[\s—–:|,-]+/, '').trim() })
|
|
671
|
+
}
|
|
672
|
+
return out
|
|
673
|
+
}
|
|
601
674
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
675
|
+
/**
|
|
676
|
+
* 同步抓取模型列表(供设置面板下拉)。失败返回空数组 —— 下拉退化为自由输入,
|
|
677
|
+
* 绝不因探测失败影响面板渲染。仅 spawnSync 可用时生效(宿主形态)。
|
|
678
|
+
*/
|
|
679
|
+
function fetchMimoModelsSync(env, timeoutMs) {
|
|
680
|
+
try {
|
|
681
|
+
const child = mimoBuiltin('node:child_process')
|
|
682
|
+
if (!child || typeof child.spawnSync !== 'function') return []
|
|
683
|
+
const prefix = resolveMimoExePrefix(env)
|
|
684
|
+
const r = child.spawnSync(prefix[0], prefix.slice(1).concat(['models']), { encoding: 'utf8', timeout: clampInt(timeoutMs, 8000, 1000, 30000), windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
685
|
+
if (!r || r.error || r.status !== 0) return []
|
|
686
|
+
return parseMimoModels(String(r.stdout || ''))
|
|
687
|
+
} catch (e) { return [] }
|
|
608
688
|
}
|
|
609
689
|
|
|
610
|
-
//
|
|
611
|
-
|
|
690
|
+
// ── 三工具 schema(旧 preset verbatim;desktop 专属入参已随之后端取消而删除)────
|
|
691
|
+
const MODEL_DESC = 'provider/model 格式,如 xiaomi/mimo-v2.6-flash;留空用设置里的默认模型'
|
|
612
692
|
const RUN_PARAMS = {
|
|
613
|
-
type: 'object',
|
|
614
|
-
additionalProperties: true,
|
|
615
|
-
required: ['prompt'],
|
|
693
|
+
type: 'object', additionalProperties: true, required: ['prompt'],
|
|
616
694
|
properties: {
|
|
617
695
|
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).' },
|
|
618
696
|
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.' },
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
cwd: { type: 'string', description: 'Working directory for mimo (--dir / desktop directory).' },
|
|
697
|
+
model: { type: 'string', description: MODEL_DESC },
|
|
698
|
+
effort: { type: 'string', enum: ['low', 'medium', 'high'], description: 'Optional reasoning effort, mapped to mimo --variant (low→minimal, high→high).' },
|
|
699
|
+
cwd: { type: 'string', description: 'Working directory for mimo (--dir).' },
|
|
623
700
|
timeoutSec: { type: 'integer', description: 'Hard timeout seconds (10-3600, default 600). Grace +60s on top.' },
|
|
624
701
|
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.' },
|
|
625
|
-
|
|
626
|
-
// 显式指定一个**已存在**的 Desktop 会话 id,否则每次都生成自造 id → 静默空操作。
|
|
627
|
-
sessionId: { type: 'string', description: 'mimo-desktop only: id of an ALREADY EXISTING MiMo Desktop session (see the Desktop app). Omit for mimocode.' },
|
|
702
|
+
sessionId: { type: 'string', description: 'Optional mimo session id to resume (--session). Follow-ups on the same topic normally go through mimo_continue.' },
|
|
628
703
|
title: { type: 'string', description: 'Optional session title.' }
|
|
629
704
|
}
|
|
630
705
|
}
|
|
631
706
|
const CONTINUE_PARAMS = {
|
|
632
|
-
type: 'object',
|
|
633
|
-
additionalProperties: true,
|
|
634
|
-
required: ['prompt'],
|
|
707
|
+
type: 'object', additionalProperties: true, required: ['prompt'],
|
|
635
708
|
properties: {
|
|
636
709
|
prompt: { type: 'string', description: 'Follow-up instruction for the ongoing mimo session. State only what is new; mimo preserves its conversation context.' },
|
|
637
710
|
sessionId: { type: 'string', description: 'mimo session id to resume (--session).' },
|
|
638
711
|
latest: { type: 'boolean', description: 'Continue the most recent mimo session (--continue).' },
|
|
639
|
-
backend: { type: 'string', enum: BACKENDS, description: 'Dispatch backend for THIS call only (see mimo_run.backend). Prefer the preselected default.' },
|
|
640
712
|
mode: { type: 'string', enum: ['auto', 'plan', 'build', 'compose', 'accept-edits'], description: 'Execution mode.' },
|
|
641
|
-
model: { type: 'string', description:
|
|
713
|
+
model: { type: 'string', description: MODEL_DESC },
|
|
642
714
|
effort: { type: 'string', enum: ['low', 'medium', 'high'], description: 'Optional reasoning effort.' },
|
|
643
715
|
cwd: { type: 'string', description: 'Working directory for mimo.' },
|
|
644
716
|
timeoutSec: { type: 'integer', description: 'Hard timeout seconds (10-3600, default 600).' },
|
|
@@ -646,50 +718,27 @@ const CONTINUE_PARAMS = {
|
|
|
646
718
|
}
|
|
647
719
|
}
|
|
648
720
|
const STATUS_PARAMS = { type: 'object', additionalProperties: true, required: [], properties: { cwd: { type: 'string', description: 'Optional: filter the snapshot to a single project (working directory).' } } }
|
|
649
|
-
const SETTINGS_PARAMS = {
|
|
650
|
-
|
|
651
|
-
additionalProperties: true,
|
|
652
|
-
required: [],
|
|
653
|
-
properties: {
|
|
654
|
-
backend: { type: 'string', enum: BACKENDS, description: 'If set, save this as the default backend.' },
|
|
655
|
-
desktopModel: { type: 'string', description: 'Optional desktop model id (mimo-desktop backend only), e.g. mimo-auto.' }
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
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.'
|
|
721
|
+
const SETTINGS_PARAMS = { type: 'object', additionalProperties: true, required: [], properties: { model: { type: 'string', description: MODEL_DESC } } }
|
|
722
|
+
const SETTINGS_DESCRIPTION = 'Read or change the default mimo model (settings). Call with model=<provider/model> to save it as the default for subsequent mimo_run/mimo_continue; call with no args to read current settings. Persisted to the settings file; a per-call `model` argument on mimo_run/mimo_continue overrides it for that call only. Available models are listed by `mimo models` on this machine (mimo CLI must be installed and logged in).'
|
|
659
723
|
|
|
660
724
|
function renderSettings(value) {
|
|
661
725
|
const s = value || {}
|
|
662
726
|
const lines = [
|
|
663
727
|
'mimo settings',
|
|
664
728
|
' settings file : ' + (s.settingsPath || '(unknown)'),
|
|
665
|
-
'
|
|
666
|
-
'
|
|
667
|
-
'
|
|
668
|
-
'
|
|
669
|
-
' desktop model : ' + (s.desktopModel || '(default)'),
|
|
670
|
-
' desktop api : ' + (s.desktopAvailable
|
|
671
|
-
? 'available' + (s.desktopApiFile ? ' (' + s.desktopApiFile + ')' : '')
|
|
672
|
-
: 'unavailable' + (s.desktopError ? ' — ' + s.desktopError : '')),
|
|
673
|
-
' backends : ' + ((Array.isArray(s.backends) && s.backends.length ? s.backends.join(' | ') : BACKENDS.join(' | ')))
|
|
729
|
+
' default model : ' + (s.defaultModel || '(unset — 不传 --model)'),
|
|
730
|
+
' effective : ' + (s.effective || '(unknown)'),
|
|
731
|
+
' exe : ' + (s.exe || '(unknown)'),
|
|
732
|
+
' saved : ' + (s.saved ? 'yes' : 'no')
|
|
674
733
|
]
|
|
734
|
+
if (s.saveError) lines.push(' save error : ' + s.saveError)
|
|
675
735
|
if (s.note) lines.push(' note : ' + s.note)
|
|
676
736
|
return [{ type: 'text', text: lines.join('\n') }]
|
|
677
737
|
}
|
|
678
|
-
|
|
679
738
|
// ── 可纯函数化的 mimo-core 语义(descriptor 方法 + 独立导出双份)───────────────
|
|
680
|
-
function normalizeBackend(v) {
|
|
681
|
-
if (v == null || v === '') return null
|
|
682
|
-
const s = String(v).trim().toLowerCase()
|
|
683
|
-
if (s === 'mimocode' || s === 'mimo-code' || s === 'cli' || s === 'mimo-cli') return BACKEND_MIMOCODE
|
|
684
|
-
if (s === 'mimo-desktop' || s === 'desktop' || s === 'mimo_desktop' || s === 'xiaomi-mimo' || s === 'xiaomi-mimo-desktop') return BACKEND_DESKTOP
|
|
685
|
-
return null
|
|
686
|
-
}
|
|
687
|
-
|
|
688
739
|
function parseMimoJson(stdoutText) {
|
|
689
740
|
const trimmed = String(stdoutText || '').trim()
|
|
690
741
|
if (!trimmed) return null
|
|
691
|
-
const MAX_ARG_LEN = 120
|
|
692
|
-
const MAX_TRAIL = 12
|
|
693
742
|
const events = []
|
|
694
743
|
const lines = trimmed.split(/\r?\n/)
|
|
695
744
|
for (const raw of lines) {
|
|
@@ -722,12 +771,12 @@ function parseMimoJson(stdoutText) {
|
|
|
722
771
|
if (ev.session && typeof ev.session === 'object' && ev.session.id) sessionId = ev.session.id
|
|
723
772
|
if (ev.type === 'session' && ev.properties && ev.properties.id) sessionId = ev.properties.id
|
|
724
773
|
const t = ev.type
|
|
774
|
+
const p = ev.properties || ev
|
|
725
775
|
if (t === 'error') {
|
|
726
|
-
const err = ev.error ||
|
|
776
|
+
const err = ev.error || p
|
|
727
777
|
errorMessage = (err.data && err.data.message) || err.message || err.name || 'error'
|
|
728
778
|
errorName = err.name || 'Error'
|
|
729
779
|
} else if (t === 'text' || t === 'message' || t === 'assistant' || t === 'output') {
|
|
730
|
-
const p = ev.properties || ev
|
|
731
780
|
const chunk = p.text || p.content || p.message || p.output || (typeof ev.text === 'string' ? ev.text : '')
|
|
732
781
|
if (typeof chunk === 'string' && chunk) {
|
|
733
782
|
textParts.push(chunk)
|
|
@@ -735,26 +784,17 @@ function parseMimoJson(stdoutText) {
|
|
|
735
784
|
}
|
|
736
785
|
} else if (t === 'step-start' || t === 'step-finish' || t === 'step_update') {
|
|
737
786
|
numTurns += 1
|
|
738
|
-
const p = ev.properties || ev
|
|
739
787
|
if (p.tokens && typeof p.tokens === 'object') {
|
|
740
788
|
const tt = p.tokens.total ?? p.tokens.input ?? p.tokens.output
|
|
741
789
|
if (typeof tt === 'number') totalTokens = (totalTokens || 0) + tt
|
|
742
790
|
}
|
|
743
|
-
if (t === 'step_update'
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
kind: 'tool',
|
|
747
|
-
label: shortLabel((p.tool_name || p.tool || p.name) + ' ' + JSON.stringify(summarizeArgs(p.arguments || p.args || p.input) || {}))
|
|
748
|
-
})
|
|
791
|
+
if (t === 'step_update') {
|
|
792
|
+
const name = p.tool_name || p.tool || p.name
|
|
793
|
+
if (name) steps.push({ at: Date.now(), kind: 'tool', label: shortLabel(name + ' ' + JSON.stringify(summarizeArgs(p.arguments || p.args || p.input) || {})) })
|
|
749
794
|
}
|
|
750
795
|
} else if (t === 'tool' || t === 'tool-call' || t === 'tool_use') {
|
|
751
|
-
const p = ev.properties || ev
|
|
752
796
|
const name = p.tool || p.name || p.tool_name || 'tool'
|
|
753
|
-
steps.push({
|
|
754
|
-
at: Date.now(),
|
|
755
|
-
kind: 'tool',
|
|
756
|
-
label: shortLabel(name + ' ' + JSON.stringify(summarizeArgs(p.arguments || p.args || p.input) || {}))
|
|
757
|
-
})
|
|
797
|
+
steps.push({ at: Date.now(), kind: 'tool', label: shortLabel(name + ' ' + JSON.stringify(summarizeArgs(p.arguments || p.args || p.input) || {})) })
|
|
758
798
|
}
|
|
759
799
|
}
|
|
760
800
|
if (textParts.length) text = textParts.join('\n')
|
|
@@ -799,217 +839,56 @@ function buildResult(parsed, outcome, mode, stderrText, stdoutText, backend) {
|
|
|
799
839
|
}
|
|
800
840
|
}
|
|
801
841
|
|
|
802
|
-
function desktopResult(ok, opts) {
|
|
803
|
-
const o = opts || {}
|
|
804
|
-
return {
|
|
805
|
-
ok: !!ok,
|
|
806
|
-
fallback: false,
|
|
807
|
-
status: o.status || (ok ? (o.accepted ? 'ACCEPTED' : 'SUCCESS') : 'ERROR'),
|
|
808
|
-
response: o.response || '',
|
|
809
|
-
sessionId: o.sessionId || null,
|
|
810
|
-
durationSeconds: null,
|
|
811
|
-
numTurns: null,
|
|
812
|
-
totalTokens: null,
|
|
813
|
-
exitCode: o.exitCode != null ? o.exitCode : (ok ? 0 : 1),
|
|
814
|
-
mode: o.mode || 'auto',
|
|
815
|
-
backend: BACKEND_DESKTOP,
|
|
816
|
-
stderr: o.stderr || '',
|
|
817
|
-
errorMessage: o.errorMessage || '',
|
|
818
|
-
accepted: o.accepted === true,
|
|
819
|
-
steps: []
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
function buildDesktopTurnBody(args) {
|
|
824
|
-
const a = args || {}
|
|
825
|
-
const message = String(a.prompt != null ? a.prompt : (a.message != null ? a.message : ''))
|
|
826
|
-
const model = String(a.model || a.desktopModel || 'mimo-auto')
|
|
827
|
-
const body = { message, model }
|
|
828
|
-
const directory = a.directory || a.cwd
|
|
829
|
-
if (directory) body.directory = String(directory)
|
|
830
|
-
return body
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
function newDesktopSessionId() {
|
|
834
|
-
// ses_ + 18-ish random chars, same shape as Desktop API ids
|
|
835
|
-
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'
|
|
836
|
-
let s = ''
|
|
837
|
-
for (let i = 0; i < 18; i++) s += chars[Math.floor(Math.random() * chars.length)]
|
|
838
|
-
return 'ses_' + s
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
function extractDesktopText(j) {
|
|
842
|
-
if (!j || typeof j !== 'object') return { text: '', sessionId: null, error: null }
|
|
843
|
-
const sessionId = j.sessionID || j.sessionId || (j.session && j.session.id) || null
|
|
844
|
-
if (j.type === 'error' || j.error) {
|
|
845
|
-
const err = j.error || j
|
|
846
|
-
const msg = (err.data && err.data.message) || err.message || err.name || 'error'
|
|
847
|
-
return { text: '', sessionId, error: String(msg) }
|
|
848
|
-
}
|
|
849
|
-
let text = ''
|
|
850
|
-
if (typeof j.text === 'string') text = j.text
|
|
851
|
-
else if (typeof j.message === 'string') text = j.message
|
|
852
|
-
else if (typeof j.content === 'string') text = j.content
|
|
853
|
-
else if (typeof j.response === 'string') text = j.response
|
|
854
|
-
else if (Array.isArray(j.parts)) {
|
|
855
|
-
for (const p of j.parts) {
|
|
856
|
-
if (p && typeof p.text === 'string') text += (text ? '\n' : '') + p.text
|
|
857
|
-
}
|
|
858
|
-
} else if (j.data && typeof j.data.text === 'string') text = j.data.text
|
|
859
|
-
return { text, sessionId, error: null }
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
function parseDesktopEvents(stdoutOrJson) {
|
|
863
|
-
const raw = String(stdoutOrJson || '').trim()
|
|
864
|
-
if (!raw) return { text: '', sessionId: null, error: null }
|
|
865
|
-
try {
|
|
866
|
-
const j = JSON.parse(raw)
|
|
867
|
-
return extractDesktopText(j)
|
|
868
|
-
} catch (e) { }
|
|
869
|
-
const texts = []
|
|
870
|
-
for (const line of raw.split(/\r?\n/)) {
|
|
871
|
-
const ln = line.trim()
|
|
872
|
-
if (!ln.startsWith('{')) continue
|
|
873
|
-
try {
|
|
874
|
-
const r = extractDesktopText(JSON.parse(ln))
|
|
875
|
-
if (r.text) texts.push(r.text)
|
|
876
|
-
if (r.error) return { text: texts.join('\n'), sessionId: r.sessionId, error: r.error }
|
|
877
|
-
} catch (e) { }
|
|
878
|
-
}
|
|
879
|
-
return { text: texts.join('\n'), sessionId: null, error: null }
|
|
880
|
-
}
|
|
881
|
-
|
|
882
842
|
function defineProvider (deps = {}) {
|
|
883
843
|
const env = deps.env || {}
|
|
884
844
|
const nodeFs = deps.nodeFs || null
|
|
885
845
|
const nodePath = deps.nodePath || null
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
if (!p || !nodeFs.existsSync || !nodeFs.existsSync(p)) continue
|
|
909
|
-
const j = JSON.parse(nodeFs.readFileSync(p, 'utf8'))
|
|
910
|
-
if (j && j.port && j.token) {
|
|
911
|
-
return { available: true, baseUrl: 'http://127.0.0.1:' + j.port, token: String(j.token), apiFile: p, error: null }
|
|
846
|
+
let host = null // attach 后保存 hostOpts(preset/dynamic 形态提供 getSettings)
|
|
847
|
+
let settingsRef = { defaultModel: '' }
|
|
848
|
+
|
|
849
|
+
/** 取当前生效的偏好设置。三条来源按优先级合并,每次 buildArgv 都重新读 ——
|
|
850
|
+
* 面板改默认模型后无需重启即生效:
|
|
851
|
+
* 1) `host.getSettings('mimo')` —— preset/dynamic 形态(attach 时由宿主注入);
|
|
852
|
+
* 2) `deps.readSettings()` —— MCP 形态(真 Node 进程,每次现读设置文件);
|
|
853
|
+
* 3) `settingsRef` —— attach 时固化的一次性快照 / 缺省值。
|
|
854
|
+
* 三条都拿不到 → 缺省 `{defaultModel:''}`(= 不传 --model,交给 mimo CLI 自决)。
|
|
855
|
+
*
|
|
856
|
+
* 历史坑:MCP 形态不 attach(自建 engine),只认 host.getSettings 会让面板设置的
|
|
857
|
+
* defaultModel 在 MCP 面对 mimo_run 完全失效 —— 只有 args.model 才落地。
|
|
858
|
+
* 三个形态行为不一致是缺陷,故此处统一把 readSettings 纳入。 */
|
|
859
|
+
function currentSettings() {
|
|
860
|
+
const out = Object.assign({}, settingsRef)
|
|
861
|
+
try {
|
|
862
|
+
if (typeof deps.readSettings === 'function') {
|
|
863
|
+
const r = deps.readSettings()
|
|
864
|
+
if (r && typeof r === 'object') {
|
|
865
|
+
// 宿主可返回 { partition, path, home } 或直接返回分区对象(宽松兼容)。
|
|
866
|
+
const part = ('partition' in r || 'path' in r || 'home' in r) ? r.partition : r
|
|
867
|
+
if (part && typeof part === 'object') Object.assign(out, part)
|
|
912
868
|
}
|
|
913
|
-
} catch (e) {
|
|
914
|
-
d.error = String(e && e.message || e)
|
|
915
869
|
}
|
|
916
|
-
}
|
|
917
|
-
if (!d.error) d.error = 'desktop-api.json not found (start Xiaomi MiMo Desktop)'
|
|
918
|
-
return d
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
function fetchFn() {
|
|
922
|
-
return (typeof globalThis !== 'undefined' && typeof globalThis.fetch === 'function') ? globalThis.fetch.bind(globalThis) : null
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
// desktopFetch(旧 runtime-settings verbatim;AbortController/setTimeout 守卫,
|
|
926
|
-
// 沙箱缺省时退化为无超时 fetch —— 仍可用)。
|
|
927
|
-
async function desktopFetch(method, path, body, timeoutMs) {
|
|
928
|
-
const fetch = fetchFn()
|
|
929
|
-
if (!fetch) { const e = new Error('fetch not available in this host'); e.code = 'DESKTOP_UNAVAILABLE'; throw e }
|
|
930
|
-
const d = desktop
|
|
931
|
-
if (!d || !d.available || !d.baseUrl) {
|
|
932
|
-
const e = new Error('MiMo Desktop API unavailable: ' + (d && d.error || 'not configured'))
|
|
933
|
-
e.code = 'DESKTOP_UNAVAILABLE'
|
|
934
|
-
throw e
|
|
935
|
-
}
|
|
936
|
-
const url = d.baseUrl + path
|
|
937
|
-
const ms = timeoutMs == null ? 8000 : timeoutMs
|
|
938
|
-
let ctrl = null
|
|
939
|
-
let timer = null
|
|
940
|
-
const init = {
|
|
941
|
-
method: method || 'GET',
|
|
942
|
-
headers: { Authorization: 'Bearer ' + d.token, 'Content-Type': 'application/json' }
|
|
943
|
-
}
|
|
944
|
-
if (body != null) init.body = typeof body === 'string' ? body : JSON.stringify(body)
|
|
945
|
-
if (typeof AbortController === 'function' && typeof setTimeout === 'function' && typeof clearTimeout === 'function') {
|
|
946
|
-
ctrl = new AbortController()
|
|
947
|
-
init.signal = ctrl.signal
|
|
948
|
-
timer = setTimeout(() => { try { ctrl.abort() } catch (e) {} }, ms)
|
|
949
|
-
}
|
|
950
|
-
try {
|
|
951
|
-
const res = await fetch(url, init)
|
|
952
|
-
const text = await res.text()
|
|
953
|
-
let json = null
|
|
954
|
-
try { json = text ? JSON.parse(text) : null } catch (e) { }
|
|
955
|
-
return { status: res.status, ok: res.ok, text, json }
|
|
956
|
-
} finally {
|
|
957
|
-
if (timer != null) clearTimeout(timer)
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
async function desktopHealth() {
|
|
870
|
+
} catch (e) { }
|
|
962
871
|
try {
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
async function desktopListSessions(limit) {
|
|
971
|
-
const r = await desktopFetch('GET', DESKTOP_API.sessionsPath + '?limit=' + (limit || 10))
|
|
972
|
-
return r.json
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
async function desktopSendTurn(sessionId, args) {
|
|
976
|
-
const id = sessionId || newDesktopSessionId()
|
|
977
|
-
const body = buildDesktopTurnBody(args)
|
|
978
|
-
const r = await desktopFetch('POST', DESKTOP_API.turnsPath(id), body, 15000)
|
|
979
|
-
// 【实测事故】Desktop API 对**任意** session id 都无条件返回 202 {"ok":true} ——
|
|
980
|
-
// 它不校验目标会话是否存在。实测:向自造 id POST 得到 202,但
|
|
981
|
-
// GET /v1/sessions 计数不变(19→19),会话从未创建,任务从未执行。
|
|
982
|
-
// 因此 202 不能作为成功判据,必须回读会话列表确认目标 id 确实存在,
|
|
983
|
-
// 否则桥接层会把"静默丢弃"报告成 ACCEPTED,用户在 Desktop UI 里什么都看不到。
|
|
984
|
-
const accepted = r.status === 202 || (r.json && r.json.ok === true)
|
|
985
|
-
let verified = null
|
|
986
|
-
if (accepted) {
|
|
987
|
-
try {
|
|
988
|
-
const list = await desktopListSessions(200)
|
|
989
|
-
if (Array.isArray(list)) verified = list.some((s) => s && String(s.id) === String(id))
|
|
990
|
-
else verified = null
|
|
991
|
-
} catch (e) { verified = null }
|
|
992
|
-
}
|
|
993
|
-
return {
|
|
994
|
-
id, status: r.status, ok: r.ok, body: r.json || r.text,
|
|
995
|
-
accepted, verified,
|
|
996
|
-
// 仅当回读确认存在(或回读不可用)才算真正投递成功
|
|
997
|
-
delivered: accepted && verified !== false
|
|
998
|
-
}
|
|
872
|
+
if (host && typeof host.getSettings === 'function') {
|
|
873
|
+
const s = host.getSettings('mimo')
|
|
874
|
+
if (s && typeof s === 'object') Object.assign(out, s)
|
|
875
|
+
}
|
|
876
|
+
} catch (e) { }
|
|
877
|
+
return out
|
|
999
878
|
}
|
|
1000
879
|
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
880
|
+
// 模型优先级:调用参数 > 宿主注入设置 > runner opts 透传 > 空(不传 --model)。
|
|
881
|
+
// 用户要求「不设默认 → 由 mimo CLI 自决」,故空值绝不落地 --model。
|
|
882
|
+
function effectiveModel(args, opts) {
|
|
883
|
+
const o = opts || {}
|
|
884
|
+
const fromArg = (args && args.model && String(args.model).trim()) || ''
|
|
885
|
+
if (fromArg) return fromArg
|
|
886
|
+
const fromSettings = (currentSettings().defaultModel || '').trim()
|
|
887
|
+
if (fromSettings) return fromSettings
|
|
888
|
+
const fromOpts = (o.settings && o.settings.defaultModel) || o.defaultModel
|
|
889
|
+
return fromOpts ? String(fromOpts).trim() : ''
|
|
1008
890
|
}
|
|
1009
891
|
|
|
1010
|
-
// 每次调用前刷一次发现结果(Desktop 可能中途启动/退出;env-only 形态零开销)。
|
|
1011
|
-
let desktop = discoverDesktop()
|
|
1012
|
-
|
|
1013
892
|
// ── 统一设置分区读取(keystone 落盘;descriptor 只读)。宿主注入 deps.readSettings
|
|
1014
893
|
// 动态读取(preset/MCP 每次取最新文件值),或静态 deps.settings(测试/dynamic)。
|
|
1015
894
|
function liveSettings() {
|
|
@@ -1027,44 +906,22 @@ function defineProvider (deps = {}) {
|
|
|
1027
906
|
}
|
|
1028
907
|
return { partition: (deps.settings && typeof deps.settings === 'object') ? deps.settings : null, path: deps.settingsPath || null, home: deps.dshHome || null }
|
|
1029
908
|
}
|
|
1030
|
-
function partitionSettings() {
|
|
1031
|
-
return liveSettings().partition || null
|
|
1032
|
-
}
|
|
1033
|
-
function fileBackend() {
|
|
1034
|
-
const s = partitionSettings()
|
|
1035
|
-
if (s && s.preferredBackend) return s.preferredBackend
|
|
1036
|
-
return null
|
|
1037
|
-
}
|
|
1038
|
-
function desktopModelChain(cfg) {
|
|
1039
|
-
const s = partitionSettings()
|
|
1040
|
-
return (cfg && cfg.desktopModel) || (s && s.desktopModel) || env.MIMO_DESKTOP_MODEL || 'mimo-auto'
|
|
1041
|
-
}
|
|
1042
909
|
|
|
1043
910
|
const descriptor = {
|
|
1044
|
-
id: 'mimo', label: 'MIMO', presetId: 'cordis-mimo', defaultBackend:
|
|
911
|
+
id: 'mimo', label: 'MIMO', presetId: 'cordis-mimo', defaultBackend: null, defaultMode: 'auto',
|
|
1045
912
|
exeName: 'mimo', binEnvVars: ['MIMO_BIN', 'MIMO_CLI'], unavailableStatus: 'MIMO_UNAVAILABLE',
|
|
1046
913
|
|
|
1047
|
-
backends()
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
resolveBackend(args, target, preferred) {
|
|
1053
|
-
const b = normalizeBackend(args && args.backend)
|
|
1054
|
-
if (b) return b
|
|
1055
|
-
return normalizeBackend(preferred) || descriptor.defaultBackend
|
|
1056
|
-
},
|
|
1057
|
-
|
|
1058
|
-
// 委托规则(Lead 定案):只有 desktop 走 provider.execute;mimocode 落 kit 通用管线。
|
|
1059
|
-
delegates(backend) { return backend === BACKEND_DESKTOP },
|
|
914
|
+
// 单后端:backends() 恒 null;历史别名(mimocode/mimo-cli/desktop)与非法值一律
|
|
915
|
+
// 归一为 null(契约 §1:非法输入返回 null)。
|
|
916
|
+
backends() { return null },
|
|
917
|
+
normalizeBackend() { return null },
|
|
918
|
+
resolveBackend(args, target, preferred) { return null },
|
|
1060
919
|
|
|
920
|
+
// 旧 preset buildArgv(exe, args, planActive) verbatim 语义(execPrefix 可为
|
|
921
|
+
// [nodeExe, binPath] 数组,runner 已归一)。
|
|
1061
922
|
buildArgv(exePrefix, args, opts) {
|
|
1062
923
|
const o = opts || {}
|
|
1063
924
|
const timeoutSec = clampInt(args.timeoutSec, DEFAULT_TIMEOUT_SEC, 10, 3600)
|
|
1064
|
-
// mimo-desktop:非 spawn 路径,空壳交给 execute(kit 委托前会调本函数)。
|
|
1065
|
-
if (o.backend === BACKEND_DESKTOP) {
|
|
1066
|
-
return { argv: [], mode: args.mode || 'auto', timeoutSec }
|
|
1067
|
-
}
|
|
1068
925
|
const argv = Array.isArray(exePrefix) ? exePrefix.slice() : [String(exePrefix)]
|
|
1069
926
|
argv.push('run')
|
|
1070
927
|
if (args.prompt != null && String(args.prompt).length) argv.push(String(args.prompt))
|
|
@@ -1075,8 +932,9 @@ function defineProvider (deps = {}) {
|
|
|
1075
932
|
// mimo --agent: build | plan | compose
|
|
1076
933
|
const agent = args.agent || mode
|
|
1077
934
|
if (agent === 'plan' || agent === 'build' || agent === 'compose') argv.push('--agent', agent)
|
|
1078
|
-
//
|
|
1079
|
-
|
|
935
|
+
// 模型:调用参数 > 设置 defaultModel > 不传(mimo CLI 自身默认)。
|
|
936
|
+
const model = effectiveModel(args, o)
|
|
937
|
+
if (model) argv.push('--model', String(model))
|
|
1080
938
|
// effort (low/medium/high) maps to mimo --variant; pass-through if already a variant name
|
|
1081
939
|
let variant = args.variant || args.effort
|
|
1082
940
|
if (variant === 'low') variant = 'minimal'
|
|
@@ -1093,9 +951,9 @@ function defineProvider (deps = {}) {
|
|
|
1093
951
|
parseJson: parseMimoJson,
|
|
1094
952
|
buildResult,
|
|
1095
953
|
|
|
1096
|
-
// 旧 foldStepUpdate 语义 verbatim(识别 tool/text/error
|
|
1097
|
-
//
|
|
1098
|
-
//
|
|
954
|
+
// 旧 foldStepUpdate 语义 verbatim(识别 tool/text/error 事件),trail 条目同时带
|
|
955
|
+
// kit 展示层字段(stepIndex/tool/state/args)与旧字段(kind/label),这样
|
|
956
|
+
// renderStatus 与 MCP 快照映射(buildMcpBridgePayload)都能直接渲染。
|
|
1099
957
|
foldStreamEvent(ev, p, summarizeArgsFn, maxArgLen, maxTrail) {
|
|
1100
958
|
if (!ev || typeof ev !== 'object') return
|
|
1101
959
|
const t = ev.type
|
|
@@ -1144,218 +1002,121 @@ function defineProvider (deps = {}) {
|
|
|
1144
1002
|
failureHint(res) {
|
|
1145
1003
|
if (!res || res.ok) return ''
|
|
1146
1004
|
const st = String(res.status || '')
|
|
1147
|
-
if (st === 'MIMO_UNAVAILABLE') return '[诊断] 未找到 mimo 可执行文件:请安装 MiMoCode CLI(mimo),或设置 MIMO_BIN
|
|
1148
|
-
|
|
1149
|
-
if (
|
|
1150
|
-
|
|
1151
|
-
|
|
1005
|
+
if (st === 'MIMO_UNAVAILABLE') return '[诊断] 未找到 mimo 可执行文件:请安装 MiMoCode CLI(mimo,官方便携安装位置 ~/.mimocode/bin/mimo.exe),或设置 MIMO_BIN 指向其绝对路径。'
|
|
1006
|
+
const hay = String(res.stderr || '') + ' ' + String(res.errorMessage || '') + ' ' + String(res.response || '')
|
|
1007
|
+
if (/free API service has ended|sign in|configure a third-party|登录|第三方|免费/i.test(hay)) {
|
|
1008
|
+
return '[诊断] MiMo 免费 API 已终止,请在 mimo CLI 用 `mimo auth login` 登录或配置第三方 API,并在 CLI Bridge 设置里指定模型。'
|
|
1009
|
+
}
|
|
1010
|
+
if (/model not found|unknown model|no models available|invalid model|unknown provider/i.test(hay)) {
|
|
1011
|
+
return '[诊断] mimo 模型不可用或模型名无效:先在本机运行 `mimo models` 列出可用模型,再把 provider/model 填进 CLI Bridge 的「默认模型」设置(或本次调用的 model 参数)。'
|
|
1152
1012
|
}
|
|
1153
1013
|
return ''
|
|
1154
1014
|
},
|
|
1155
1015
|
|
|
1156
|
-
// ACCEPTED 计入成功态(指示灯不红;契约 §1
|
|
1016
|
+
// ACCEPTED 计入成功态(指示灯不红;契约 §1);desktop 取消后不再产生 ACCEPTED,
|
|
1017
|
+
// 但成功态集合逐字保持不变(兼容旧会话/测试语义)。
|
|
1157
1018
|
isSuccessStatus(st) {
|
|
1158
1019
|
if (!st) return false
|
|
1159
1020
|
return st === 'SUCCESS' || st === 'ACCEPTED'
|
|
1160
1021
|
},
|
|
1161
1022
|
|
|
1162
|
-
// 桌面可达性前置检查(契约 §9):dynamic 无 fetch / 无发现 → abort 提示 mimocode。
|
|
1163
|
-
// 有 fs 时做一次健康探测缓存(异步进行中先放行 —— preflight 是同步钩子)。
|
|
1164
|
-
preflight({ backend }) {
|
|
1165
|
-
if (backend !== BACKEND_DESKTOP) return null
|
|
1166
|
-
desktop = discoverDesktop()
|
|
1167
|
-
if (!fetchFn()) {
|
|
1168
|
-
return {
|
|
1169
|
-
abort: desktopResult(false, {
|
|
1170
|
-
status: 'DESKTOP_UNREACHABLE',
|
|
1171
|
-
mode: 'auto',
|
|
1172
|
-
errorMessage: '当前宿主无 HTTP fetch 能力(dynamic 沙箱),mimo-desktop 不可用。请改用 backend=mimocode,或在 DSH 正式宿主(preset/MCP 形态)中使用桌面后端。'
|
|
1173
|
-
})
|
|
1174
|
-
}
|
|
1175
|
-
}
|
|
1176
|
-
if (!desktop.available) {
|
|
1177
|
-
return {
|
|
1178
|
-
abort: desktopResult(false, {
|
|
1179
|
-
status: 'DESKTOP_UNREACHABLE',
|
|
1180
|
-
mode: 'auto',
|
|
1181
|
-
stderr: String(desktop.error || 'not configured'),
|
|
1182
|
-
errorMessage: 'MiMo Desktop client unavailable. Start Xiaomi MiMo Desktop, or use backend=mimocode.'
|
|
1183
|
-
})
|
|
1184
|
-
}
|
|
1185
|
-
}
|
|
1186
|
-
return null
|
|
1187
|
-
},
|
|
1188
|
-
|
|
1189
|
-
// mimo-desktop 委托执行(旧 runDesktop verbatim 编排;engine.begin/end 由 kit 做)。
|
|
1190
|
-
async execute({ args, cwd, built }) {
|
|
1191
|
-
desktop = discoverDesktop()
|
|
1192
|
-
const a = args || {}
|
|
1193
|
-
const sessionId = a.sessionId || newDesktopSessionId()
|
|
1194
|
-
const mode = built && built.mode ? built.mode : (a.mode || 'auto')
|
|
1195
|
-
try {
|
|
1196
|
-
const health = await desktopHealth()
|
|
1197
|
-
if (!health.ok) {
|
|
1198
|
-
return desktopResult(false, {
|
|
1199
|
-
status: 'DESKTOP_UNAVAILABLE',
|
|
1200
|
-
sessionId,
|
|
1201
|
-
mode,
|
|
1202
|
-
stderr: String(health.error || health.body || 'desktop api not ready'),
|
|
1203
|
-
errorMessage: 'MiMo Desktop client unavailable. Start Xiaomi MiMo Desktop and retry, or set backend=mimocode.'
|
|
1204
|
-
})
|
|
1205
|
-
}
|
|
1206
|
-
const s = partitionSettings()
|
|
1207
|
-
const turn = await desktopSendTurn(sessionId, {
|
|
1208
|
-
prompt: a.prompt,
|
|
1209
|
-
model: a.model || desktopModelChain(s),
|
|
1210
|
-
directory: cwd
|
|
1211
|
-
})
|
|
1212
|
-
const okTurn = turn.delivered === true
|
|
1213
|
-
let text = ''
|
|
1214
|
-
if (okTurn) {
|
|
1215
|
-
try {
|
|
1216
|
-
const ev = await desktopReadEvents(sessionId, cwd, 2500)
|
|
1217
|
-
text = ev.text || ''
|
|
1218
|
-
} catch (e) { }
|
|
1219
|
-
}
|
|
1220
|
-
return desktopResult(okTurn, {
|
|
1221
|
-
status: okTurn ? (text ? 'SUCCESS' : 'ACCEPTED') : 'DESKTOP_SESSION_NOT_CREATED',
|
|
1222
|
-
response: text || (okTurn
|
|
1223
|
-
? '(Desktop turn accepted; watch Desktop UI or call mimo_continue for the reply)'
|
|
1224
|
-
: ''),
|
|
1225
|
-
sessionId,
|
|
1226
|
-
accepted: okTurn,
|
|
1227
|
-
mode,
|
|
1228
|
-
stderr: okTurn ? '' : 'Desktop API returned ' + turn.status + ' but session ' + turn.id + ' does not exist',
|
|
1229
|
-
errorMessage: okTurn ? '' : ('MiMo Desktop 未创建会话(API 返回 ' + turn.status +
|
|
1230
|
-
' 但会话列表中不存在 ' + turn.id + ')。任务未执行。' +
|
|
1231
|
-
'MiMo Desktop API 无法创建会话,请在 Xiaomi MiMo Desktop 应用中新建(或选用现有)' +
|
|
1232
|
-
'会话,再把它的 id 传给 mimo_run 的 sessionId 参数。')
|
|
1233
|
-
})
|
|
1234
|
-
} catch (e) {
|
|
1235
|
-
return desktopResult(false, {
|
|
1236
|
-
status: 'DESKTOP_ERROR',
|
|
1237
|
-
sessionId,
|
|
1238
|
-
mode,
|
|
1239
|
-
stderr: String(e && e.message || e),
|
|
1240
|
-
errorMessage: String(e && e.message || e)
|
|
1241
|
-
})
|
|
1242
|
-
}
|
|
1243
|
-
},
|
|
1244
|
-
|
|
1245
1023
|
tools: {
|
|
1246
|
-
run: { name: 'mimo_run', description: 'Dispatch a coding/build/debug/investigation task to the local mimo agent CLI (Xiaomi
|
|
1024
|
+
run: { name: 'mimo_run', description: 'Dispatch a coding/build/debug/investigation task to the local mimo agent CLI (Xiaomi MiMoCode) and return its final answer. DSH fully controls mimo (--dangerously-skip-permissions; mimo never prompts). On rate-limit/network failure DSH pops a fallback dialog; fallback=true means finish with native tools. Runs in the background by default (returns a jobId immediately; collect with job_output); set background=false to block for the final answer. While it runs, call mimo_status to watch what mimo is doing live.', inputSchema: RUN_PARAMS },
|
|
1247
1025
|
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 },
|
|
1248
1026
|
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 }
|
|
1249
1027
|
},
|
|
1250
1028
|
|
|
1251
|
-
// mimo_settings:入参键保持旧 prompt 连续性(backend/desktopModel
|
|
1252
|
-
//
|
|
1253
|
-
//
|
|
1029
|
+
// mimo_settings:入参键保持旧 prompt 连续性(backend/desktopModel → model),落统一
|
|
1030
|
+
// 分区 defaultModel(迁移映射 keystone 已做)。写盘仅在宿主注入 nodeFs + nodePath 时
|
|
1031
|
+
// 可用;否则回显(dynamic 沙箱旧 echo 语义)。
|
|
1254
1032
|
extraTools: [{
|
|
1255
1033
|
name: 'mimo_settings',
|
|
1256
1034
|
description: SETTINGS_DESCRIPTION,
|
|
1257
1035
|
inputSchema: SETTINGS_PARAMS,
|
|
1258
1036
|
render: renderSettings,
|
|
1259
|
-
async logic(args
|
|
1037
|
+
async logic(args) {
|
|
1260
1038
|
const a = args || {}
|
|
1261
1039
|
const live = liveSettings()
|
|
1262
|
-
const cur = Object.assign({
|
|
1040
|
+
const cur = Object.assign({ defaultModel: '' }, live.partition || {})
|
|
1263
1041
|
let settingsPath = live.path || null
|
|
1264
|
-
let saved = false
|
|
1265
|
-
let saveError = null
|
|
1266
|
-
const fsMod = nodeFs
|
|
1267
|
-
const pathMod = nodePath
|
|
1042
|
+
let saved = false, saveError = null
|
|
1268
1043
|
const home = live.home || deps.dshHome || null
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
saved = true
|
|
1273
|
-
} catch (e) { saveError = String(e && e.message || e) }
|
|
1044
|
+
const patch = (typeof a.model === 'string' && a.model.trim()) ? { defaultModel: a.model.trim() } : null
|
|
1045
|
+
if (patch && nodeFs && nodePath && (settingsPath || home)) {
|
|
1046
|
+
try { settingsPath = await writePartition(nodeFs, nodePath, settingsPath, home, env, cur, patch); saved = true } catch (e) { saveError = String(e && e.message || e) }
|
|
1274
1047
|
}
|
|
1275
1048
|
const merged = Object.assign({}, cur)
|
|
1276
|
-
if (
|
|
1277
|
-
if (a.desktopModel) merged.desktopModel = a.desktopModel
|
|
1278
|
-
const rt = discoverDesktop()
|
|
1049
|
+
if (patch) merged.defaultModel = patch.defaultModel
|
|
1279
1050
|
return {
|
|
1280
1051
|
ok: !saveError,
|
|
1281
1052
|
settingsPath: settingsPath || '(unified; not writable in this host)',
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
desktopAvailable: !!rt.available,
|
|
1288
|
-
desktopError: rt.error || null,
|
|
1289
|
-
desktopApiFile: rt.apiFile || null,
|
|
1290
|
-
saved,
|
|
1291
|
-
saveError,
|
|
1292
|
-
backends: BACKENDS,
|
|
1293
|
-
note: 'Preselect via settings file / plugin config.backend / MIMO_BRIDGE_BACKEND. Per-call `backend` overrides only that call.'
|
|
1053
|
+
defaultModel: merged.defaultModel || '',
|
|
1054
|
+
effective: merged.defaultModel ? ('settings:' + merged.defaultModel) : '(unset — 不传 --model,mimo CLI 自决)',
|
|
1055
|
+
exe: resolveMimoExePrefix(env).join(' '),
|
|
1056
|
+
saved, saveError,
|
|
1057
|
+
note: '默认模型在设置面板或本工具里指定(provider/model);mimo_run/mimo_continue 的 model 参数可单次覆盖。可用模型:在本机运行 `mimo models` 查看。'
|
|
1294
1058
|
}
|
|
1295
1059
|
}
|
|
1296
1060
|
}],
|
|
1297
1061
|
|
|
1298
1062
|
// 设置分区(契约 §3):keystone 读写/GUI;本处纯逻辑。diagnose/meta 必须同步。
|
|
1299
1063
|
settings: {
|
|
1300
|
-
keys: ['
|
|
1301
|
-
defaults: () => ({
|
|
1064
|
+
keys: ['defaultModel'],
|
|
1065
|
+
defaults: () => ({ defaultModel: '' }),
|
|
1302
1066
|
normalize(raw) {
|
|
1303
1067
|
const src = raw && typeof raw === 'object' ? raw : {}
|
|
1304
|
-
|
|
1305
|
-
const b = normalizeBackend(src.preferredBackend)
|
|
1306
|
-
if (b) out.preferredBackend = b
|
|
1307
|
-
if (typeof src.desktopModel === 'string' && src.desktopModel.trim()) out.desktopModel = src.desktopModel.trim()
|
|
1308
|
-
return out
|
|
1068
|
+
return { defaultModel: (typeof src.defaultModel === 'string' && src.defaultModel.trim()) ? src.defaultModel.trim() : '' }
|
|
1309
1069
|
},
|
|
1310
|
-
meta() {
|
|
1070
|
+
meta(envArg) {
|
|
1071
|
+
const e = envArg && typeof envArg === 'object' ? envArg : env
|
|
1072
|
+
// 下拉选项:实时 `mimo models`(失败则退化为空 → 面板仍可自由输入)。
|
|
1073
|
+
let options = []
|
|
1074
|
+
try {
|
|
1075
|
+
options = fetchMimoModelsSync(e, 8000).map((m) => ({ value: m.id, label: m.id }))
|
|
1076
|
+
} catch (err) { options = [] }
|
|
1311
1077
|
return {
|
|
1312
1078
|
titleSuffix: 'MiMo',
|
|
1313
|
-
fields: [
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1079
|
+
fields: [{
|
|
1080
|
+
key: 'defaultModel', label: '默认模型', type: 'select', allowCustom: true,
|
|
1081
|
+
options: options,
|
|
1082
|
+
placeholder: '留空 = 不传 --model(由 mimo CLI 自身默认决定)',
|
|
1083
|
+
note: options.length
|
|
1084
|
+
? '共 ' + options.length + ' 个可用模型(来自 `mimo models`)。mimo_run/mimo_continue 的 model 参数可单次覆盖。'
|
|
1085
|
+
: '未能读取模型列表(mimo CLI 未安装/未登录/超时)—— 仍可手动输入 provider/model,如 xiaomi/mimo-v2.6-flash。'
|
|
1086
|
+
}]
|
|
1317
1087
|
}
|
|
1318
1088
|
},
|
|
1319
|
-
//
|
|
1320
|
-
diagnose(settings) {
|
|
1321
|
-
const
|
|
1322
|
-
const
|
|
1323
|
-
|
|
1324
|
-
if (
|
|
1325
|
-
const
|
|
1326
|
-
|
|
1327
|
-
f(rt.baseUrl + DESKTOP_API.healthPath, { headers: { Authorization: 'Bearer ' + rt.token } })
|
|
1328
|
-
.then((r) => { desktopHealthCache = { ok: r.ok, at: Date.now() } })
|
|
1329
|
-
.catch(() => { desktopHealthCache = { ok: false, at: Date.now() } })
|
|
1330
|
-
} catch (e) { desktopHealthCache = { ok: false, at: Date.now() } }
|
|
1331
|
-
if (desktopHealthCache.at && Date.now() - desktopHealthCache.at < 60000) {
|
|
1332
|
-
return desktopHealthCache.ok
|
|
1333
|
-
? ('桌面 API 在线(' + rt.baseUrl + ');当前预选 ' + s)
|
|
1334
|
-
: ('桌面 API 不可达(检查 MiMo Desktop 是否在运行);当前预选 ' + s + ',可改用 mimocode')
|
|
1335
|
-
}
|
|
1336
|
-
desktopHealthCache = { ok: null, at: Date.now() }
|
|
1337
|
-
return '桌面 API 已发现(' + rt.baseUrl + (rt.apiFile ? '' : ',env 配置') + ');健康检测中…;当前预选 ' + s
|
|
1089
|
+
// 同步一行诊断:当前模型 + exe 来源(env > 已知安装位置 > PATH)。
|
|
1090
|
+
diagnose(settings, envArg) {
|
|
1091
|
+
const s = settings && typeof settings === 'object' ? settings : {}
|
|
1092
|
+
const e = envArg && typeof envArg === 'object' ? envArg : env
|
|
1093
|
+
const model = s.defaultModel ? 'model=' + s.defaultModel : 'model=(未设置 → 不传 --model)'
|
|
1094
|
+
if (e.MIMO_BIN || e.MIMO_CLI) return model + ' · bin=env:' + (e.MIMO_BIN || e.MIMO_CLI)
|
|
1095
|
+
const found = mimoCandidateExePaths(e)
|
|
1096
|
+
return model + ' · ' + (found.length ? 'bin=' + found[0] : 'bin=PATH:mimo')
|
|
1338
1097
|
}
|
|
1339
1098
|
},
|
|
1340
1099
|
|
|
1341
|
-
mcp: { version: '0.2.2' }
|
|
1100
|
+
// MCP 形态扩展:**不含 version**。历史:四处 `mcp: { version: '0.2.2' }` 与
|
|
1101
|
+
// mcp/version.mjs 的 packageVersion() 双源,漏改一处就出现「统一 server 报 0.2.3、
|
|
1102
|
+
// provider 面仍报 0.2.2」的自相矛盾输出;且 mcp/version.mjs 依赖 import.meta + node:fs,
|
|
1103
|
+
// 其函数体无法随本文件内联进 dynamic 沙箱(build 文本拼接会丢实现/炸语法)。
|
|
1104
|
+
// 定案:版本号单一来源 = mcp/version.mjs 的 packageVersion(),由 MCP 面(mcp/providers/
|
|
1105
|
+
// mimo.mjs 的 meta.version)读取;描述符不复制 version 字面量,宿主如需注入可经 deps.mcp。
|
|
1106
|
+
mcp: (deps.mcp && typeof deps.mcp === 'object') ? Object.assign({}, deps.mcp) : {}
|
|
1342
1107
|
}
|
|
1343
1108
|
|
|
1344
|
-
let desktopHealthCache = { ok: null, at: 0 }
|
|
1345
|
-
|
|
1346
1109
|
// 统一分区写入(tmp→rename 原子写;与 keystone lib/settings.mjs 同文件同分区规则,
|
|
1347
1110
|
// 本工具自实现落盘 —— 不 import home-plugin;宿主给出 settingsPath 或 dshHome)。
|
|
1348
1111
|
async function writePartition(fsMod, pathMod, knownPath, home, environment, cur, patch) {
|
|
1349
|
-
const
|
|
1350
|
-
const file = knownPath || environment.CLI_BRIDGE_SETTINGS_FILE || (home ? pathMod.join(home, SETTINGS_FILE_NAME) : null)
|
|
1112
|
+
const file = knownPath || environment.CLI_BRIDGE_SETTINGS_FILE || (home ? pathMod.join(home, 'cli-bridge-settings.json') : null)
|
|
1351
1113
|
if (!file) throw new Error('dsh-home not resolved (set DSH_HOME or CLI_BRIDGE_SETTINGS_FILE)')
|
|
1352
1114
|
let all = {}
|
|
1353
1115
|
try { all = JSON.parse(fsMod.readFileSync(file, 'utf8')) } catch (e) { all = {} }
|
|
1354
1116
|
if (!all || typeof all !== 'object') all = {}
|
|
1355
1117
|
const next = Object.assign({}, all)
|
|
1356
1118
|
const part = Object.assign({}, cur, next.mimo || {})
|
|
1357
|
-
if (patch.
|
|
1358
|
-
if (patch.desktopModel) part.desktopModel = patch.desktopModel
|
|
1119
|
+
if (patch && patch.defaultModel) part.defaultModel = patch.defaultModel
|
|
1359
1120
|
next.mimo = descriptor.settings.normalize(part)
|
|
1360
1121
|
next.updatedAt = Date.now()
|
|
1361
1122
|
try { fsMod.mkdirSync(pathMod.dirname(file), { recursive: true }) } catch (e) { }
|
|
@@ -1366,31 +1127,20 @@ function defineProvider (deps = {}) {
|
|
|
1366
1127
|
}
|
|
1367
1128
|
|
|
1368
1129
|
// 三形态共用的工具逻辑。签名保持不变(keystone 与队友按此接线)。
|
|
1130
|
+
// 旧 mimo preset 语义:background 默认 true(仅显式 false 才前台阻塞)。
|
|
1369
1131
|
descriptor.runToolLogic = async function (args, exec) {
|
|
1370
1132
|
const a = args || {}
|
|
1371
|
-
// 旧 mimo preset 语义:background 默认 true(仅显式 false 才前台阻塞)。
|
|
1372
1133
|
const normalized = a.background === undefined ? Object.assign({}, a, { background: true }) : a
|
|
1373
1134
|
return this.runner.coreExecute(normalized, exec)
|
|
1374
1135
|
}
|
|
1375
1136
|
descriptor.continueToolLogic = async function (args, exec) {
|
|
1376
1137
|
const a = args || {}
|
|
1377
|
-
const mapped = {
|
|
1378
|
-
prompt: a.prompt,
|
|
1379
|
-
mode: a.mode,
|
|
1380
|
-
model: a.model,
|
|
1381
|
-
effort: a.effort,
|
|
1382
|
-
cwd: a.cwd,
|
|
1383
|
-
timeoutSec: a.timeoutSec,
|
|
1384
|
-
background: a.background,
|
|
1385
|
-
backend: a.backend,
|
|
1386
|
-
// 【关键】desktop 后端的 sessionId 必须转发(白名单转发漏了它会导致
|
|
1387
|
-
// mimo_run 传 sessionId 被静默丢弃 → 永远造新 id → desktop 后端不可用)。
|
|
1388
|
-
sessionId: a.sessionId
|
|
1389
|
-
}
|
|
1138
|
+
const mapped = { prompt: a.prompt, mode: a.mode, model: a.model, effort: a.effort, cwd: a.cwd, timeoutSec: a.timeoutSec, background: a.background, sessionId: a.sessionId }
|
|
1390
1139
|
if (a.sessionId) mapped.sessionId = a.sessionId
|
|
1391
1140
|
else if (a.latest) mapped.continueLatest = true
|
|
1392
1141
|
return this.runToolLogic(mapped, exec)
|
|
1393
1142
|
}
|
|
1143
|
+
// 旧 statusTool.execute verbatim:cwd 过滤到单项目(Object.assign 形式与 opencode 一致)。
|
|
1394
1144
|
descriptor.statusToolLogic = async function (args) {
|
|
1395
1145
|
const a = args || {}
|
|
1396
1146
|
const snap = this.engine.statusSnapshot()
|
|
@@ -1399,41 +1149,39 @@ function defineProvider (deps = {}) {
|
|
|
1399
1149
|
snap.projects = (snap.projects || []).filter((p) => p.cwd === key)
|
|
1400
1150
|
const g = snap.projects[0]
|
|
1401
1151
|
if (g) {
|
|
1402
|
-
snap
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
snap.lastSessionId = g.lastSessionId
|
|
1409
|
-
snap.fallbackActive = g.fallbackActive
|
|
1410
|
-
snap.runs = g.runs
|
|
1411
|
-
snap.totalTokens = g.totalTokens
|
|
1412
|
-
snap.updatedAt = g.updatedAt
|
|
1152
|
+
Object.assign(snap, {
|
|
1153
|
+
state: g.state, running: g.running, current: g.current, trail: g.trail,
|
|
1154
|
+
lastStatus: g.lastStatus, lastAt: g.lastAt, lastSessionId: g.lastSessionId,
|
|
1155
|
+
lastOk: g.lastOk, lastFailed: g.lastFailed, fallbackActive: g.fallbackActive,
|
|
1156
|
+
runs: g.runs, totalTokens: g.totalTokens, updatedAt: g.updatedAt
|
|
1157
|
+
})
|
|
1413
1158
|
}
|
|
1414
1159
|
}
|
|
1415
1160
|
return snap
|
|
1416
1161
|
}
|
|
1417
|
-
|
|
1418
|
-
// 工厂装配(provider 无关骨架 + 描述符):宿主形态通过 deps 注入 runner 依赖。
|
|
1162
|
+
// 工厂装配(provider 无关骨架 + 描述符);宿主注入 getSettings 时把面板偏好带进 buildArgv。
|
|
1419
1163
|
descriptor.attach = function (hostOpts) {
|
|
1420
|
-
|
|
1421
|
-
|
|
1164
|
+
host = hostOpts || {}
|
|
1165
|
+
try {
|
|
1166
|
+
if (typeof host.getSettings === 'function') {
|
|
1167
|
+
const s = host.getSettings('mimo')
|
|
1168
|
+
if (s && typeof s === 'object') settingsRef = descriptor.settings.normalize(s)
|
|
1169
|
+
}
|
|
1170
|
+
} catch (e) { }
|
|
1171
|
+
this.engine = createStatusEngine({ provider: this, publish: host.publish })
|
|
1172
|
+
const runner = createRunner(Object.assign({}, host, { provider: this, engine: this.engine }))
|
|
1422
1173
|
this.runner = runner
|
|
1423
1174
|
return runner
|
|
1424
1175
|
}
|
|
1425
|
-
|
|
1426
|
-
//
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
desktopFetch, desktopHealth, desktopSendTurn, desktopReadEvents
|
|
1176
|
+
// 测试/宿主可见的纯函数面(不进协议字段)。currentSettings/liveSettings/writePartition 是
|
|
1177
|
+
// 闭包函数,不依赖 attach(未 attach 时 host 为 null → 走 deps 注入或默认空值),测试可
|
|
1178
|
+
// 直接调用;writePartition 需要调用方自行传入可用的 fs/path 模块。
|
|
1179
|
+
descriptor._internals = { MIMO_LIMIT_RE, parseMimoJson, buildResult,
|
|
1180
|
+
mimoCandidateExePaths, resolveMimoExePrefix, parseMimoModels, fetchMimoModelsSync,
|
|
1181
|
+
currentSettings, liveSettings, writePartition
|
|
1432
1182
|
}
|
|
1433
|
-
|
|
1434
1183
|
return descriptor
|
|
1435
1184
|
}
|
|
1436
|
-
|
|
1437
1185
|
// core/scheduling.mjs — CLI 调度(启用集合 + 调用优先级)单源纯逻辑。
|
|
1438
1186
|
//
|
|
1439
1187
|
// 语义(用户可见的两件事):
|
|
@@ -1611,14 +1359,16 @@ return {
|
|
|
1611
1359
|
} catch (e) { /* 灯数据尽力而为 */ }
|
|
1612
1360
|
}
|
|
1613
1361
|
|
|
1614
|
-
// exe:MIMO_BIN/MIMO_CLI env > PATH 查找 mimo
|
|
1615
|
-
//
|
|
1362
|
+
// exe:MIMO_BIN/MIMO_CLI env > 已知安装位置 > PATH 查找 mimo
|
|
1363
|
+
// (subprocess.resolveExecutable)。与 descriptor 的 resolveMimoExePrefix 同一套
|
|
1364
|
+
// 优先级;.js/.mjs 目标经 process.execPath 包装(供测试 fixture 使用)。
|
|
1616
1365
|
async function resolveExe(backend, signal) {
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
if (
|
|
1621
|
-
return
|
|
1366
|
+
const prefix = resolveMimoExePrefix(env)
|
|
1367
|
+
if (prefix.length && prefix[0]) {
|
|
1368
|
+
const head = String(prefix[0])
|
|
1369
|
+
if (prefix.length > 1) return prefix
|
|
1370
|
+
if (/\.(mjs|cjs|js)$/i.test(head) && typeof process !== 'undefined' && process.execPath) return [process.execPath, head]
|
|
1371
|
+
return head
|
|
1622
1372
|
}
|
|
1623
1373
|
return await subprocess.resolveExecutable('mimo', undefined, signal)
|
|
1624
1374
|
}
|
|
@@ -1633,9 +1383,22 @@ return {
|
|
|
1633
1383
|
return '.'
|
|
1634
1384
|
}
|
|
1635
1385
|
|
|
1636
|
-
//
|
|
1637
|
-
|
|
1638
|
-
|
|
1386
|
+
// 设置分区(descriptor 的 host.getSettings('mimo') 契约:直接返回分区对象
|
|
1387
|
+
// { defaultModel } 或空对象)。每次调用重读统一设置文件 —— 面板改默认模型后
|
|
1388
|
+
// 无需重启即生效;读不到(沙箱无 fs / 文件缺失)回落 {} = 不传 --model。
|
|
1389
|
+
function getSettings() {
|
|
1390
|
+
try {
|
|
1391
|
+
const proc = (typeof process !== 'undefined') ? process : null
|
|
1392
|
+
const nodeFs = (proc && typeof proc.getBuiltinModule === 'function') ? proc.getBuiltinModule('node:fs') : null
|
|
1393
|
+
if (!nodeFs || typeof nodeFs.readFileSync !== 'function') return {}
|
|
1394
|
+
const nodePath = (proc && typeof proc.getBuiltinModule === 'function') ? proc.getBuiltinModule('node:path') : null
|
|
1395
|
+
const join = nodePath ? nodePath.join.bind(nodePath) : ((...p) => p.join('/'))
|
|
1396
|
+
const file = env.CLI_BRIDGE_SETTINGS_FILE || (env.DSH_HOME ? join(env.DSH_HOME, 'cli-bridge-settings.json') : null)
|
|
1397
|
+
if (!file) return {}
|
|
1398
|
+
const all = JSON.parse(nodeFs.readFileSync(file, 'utf8'))
|
|
1399
|
+
if (all && typeof all === 'object' && all.mimo && typeof all.mimo === 'object') return all.mimo
|
|
1400
|
+
} catch (e) { /* 读不到:回落空分区(默认模型为空 → 不传 --model) */ }
|
|
1401
|
+
return {}
|
|
1639
1402
|
}
|
|
1640
1403
|
|
|
1641
1404
|
function planActiveFor(exec) {
|
|
@@ -1648,7 +1411,7 @@ return {
|
|
|
1648
1411
|
return false
|
|
1649
1412
|
}
|
|
1650
1413
|
|
|
1651
|
-
provider.attach({ ctx, subprocess, resolveExe, getCwdFallback,
|
|
1414
|
+
provider.attach({ ctx, subprocess, resolveExe, getCwdFallback, getSettings, planActiveFor, publish })
|
|
1652
1415
|
|
|
1653
1416
|
const OUTPUT_SCHEMA = { type: 'object', additionalProperties: true }
|
|
1654
1417
|
|
|
@@ -1689,13 +1452,22 @@ return {
|
|
|
1689
1452
|
// 浏览器指示灯轮询的 RPC 面(旧 dynamic/handle('mimo_status') 语义)。
|
|
1690
1453
|
harness.handle('mimo_status', function () { return provider.statusToolLogic({}) })
|
|
1691
1454
|
|
|
1692
|
-
//
|
|
1455
|
+
// 提示词连续性(契约 §8):决策表 / 上下文协议 / 回退协议三段保留旧语义;后端段与
|
|
1456
|
+
// 模型段随「去 desktop 化 + 模型手动指定」重写(免费 API 已终止 + mimo auth login)。
|
|
1457
|
+
// 与 preset/mimo-first/cli-bridge.mjs 的 policy 段保持同文。
|
|
1693
1458
|
const policyText = [
|
|
1694
|
-
'mimo-first execution policy (Xiaomi MiMo: MiMoCode CLI
|
|
1459
|
+
'mimo-first execution policy (Xiaomi MiMo: MiMoCode CLI).',
|
|
1460
|
+
'',
|
|
1461
|
+
'Local `mimo` CLI (MiMoCode) handles coding tasks under full DSH control (--dangerously-skip-permissions, never prompts).',
|
|
1695
1462
|
'',
|
|
1696
|
-
'
|
|
1697
|
-
'-
|
|
1698
|
-
'-
|
|
1463
|
+
'Model:',
|
|
1464
|
+
'- The model is chosen in the CLI Bridge settings panel (provider/model form, e.g. xiaomi/mimo-v2.6-flash) — this bridge does not pick one.',
|
|
1465
|
+
'- Leave the setting empty to pass no --model, letting the mimo CLI use its own default. A per-call `model` argument overrides the setting for that call only.',
|
|
1466
|
+
'- List available models with `mimo models` on this machine.',
|
|
1467
|
+
'',
|
|
1468
|
+
'Credentials:',
|
|
1469
|
+
"- Xiaomi MiMo's free API service has ENDED. The user must run `mimo auth login` (or configure a third-party API) on this machine before dispatch works.",
|
|
1470
|
+
'- If a run fails with sign-in / free-API-ended errors, tell the user to log in. Do not retry in a loop.',
|
|
1699
1471
|
'',
|
|
1700
1472
|
'By default, dispatch runs in the background and returns a jobId immediately; collect via job_output and watch live progress via mimo_status.',
|
|
1701
1473
|
'',
|
|
@@ -1705,12 +1477,6 @@ return {
|
|
|
1705
1477
|
'- Follow-ups on the same topic -> mimo_continue (with latest=true or sessionId; context is preserved).',
|
|
1706
1478
|
'- Final build/test verification of mimo output -> Native tools.',
|
|
1707
1479
|
'- Plan / read-only analysis -> mimo_run with mode=plan.',
|
|
1708
|
-
'- Desktop-native work (Desktop session UI, shared Desktop login/model) -> prefer backend=mimo-desktop (or preselect it in settings).',
|
|
1709
|
-
'',
|
|
1710
|
-
'Backend preselection (settings):',
|
|
1711
|
-
'- Default backend is configured OUT-OF-BAND (plugin config.backend / env MIMO_BRIDGE_BACKEND / settings file).',
|
|
1712
|
-
'- Per-call `backend` argument overrides the default only when necessary.',
|
|
1713
|
-
'- Do NOT invent a backend every call — use the preselected default.',
|
|
1714
1480
|
'',
|
|
1715
1481
|
'Context protocol (one topic = one mimo session):',
|
|
1716
1482
|
'- First dispatch on a topic: compact CONTEXT preamble (point to paths, do not paste file contents).',
|