@luchenglong/cli-bridge 0.2.9 → 0.2.11

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.
@@ -2,6 +2,7 @@
2
2
  //
3
3
  // 构建(node scripts/build.mjs --only=mimo)把 core/bridge-kit.mjs 与
4
4
  // providers/mimo.mjs 分别内联进下方两个占位标记行(各占一行、行内仅此标记),
5
+ // 第三个占位标记行负责内联 providers/mimo-*.mjs 片文件(纯解析/结果构造),
5
6
  // 生成 dynamic/mimo.host.js。注意:标记字面量绝不能出现在本注释里 —— 构建做全局
6
7
  // 替换,注释中出现同样也会被替换成正文代码而炸掉语法。
7
8
  // 沙箱纪律(旧仓 dynamic/host.js 头注 verbatim 精神):
@@ -565,520 +566,725 @@ function createRunner(o) {
565
566
  return { coreExecute, runSync, askFallback }
566
567
  }
567
568
 
568
- // providers/mimo.mjs — mimo provider descriptor(单后端:本地 mimo CLI / MiMoCode)。
569
- //
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)。
574
- //
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
- // ── 单后端归一(旧 mimo-core 双后端 verbatim 的收缩形态)───────────────────────
587
- // mimo-desktop 取消后不再有第二后端:backends() 恒 null;任何输入(含历史别名
588
- // mimocode / mimo-cli / desktop / 空串 / 非法值)一律归一为 null。会话路由由
589
- // engine.resolveTarget(cwd + sessionId)完成,backend 恒 null。
590
- const DEFAULT_TIMEOUT_SEC = 600
591
-
592
- // 旧 LIMIT_RE(比 kit 版多 free-API/登录/第三方等 MiMo 文案;provider.isLimited 覆盖)。
593
- // 命名 MIMO_ 前缀:dynamic 内联时 kit 也声明顶层 LIMIT_RE,同名会在同一脚本作用域
594
- // 触发「Identifier already declared」构建错误(scripts/build.mjs 文本拼接的固有约束)。
595
- 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
596
-
597
- // ── Node 内建探测(沙箱纪律:getBuiltinModule + typeof 守卫)───────────────────
598
- // 名字带 mimo 前缀:本文件会与 bridge-kit / core/scheduling.mjs【文本内联进同一个动态
599
- // 沙箱脚本作用域】,撞名即「Identifier already declared」构建错误。
600
- function mimoBuiltin(name) {
601
- try {
602
- const proc = globalThis.process
603
- return (proc && typeof proc.getBuiltinModule === 'function') ? proc.getBuiltinModule(name) : null
604
- } catch (e) { return null }
605
- }
606
-
607
- // ── 可执行文件解析(env MIMO_BIN/MIMO_CLI > 已知安装位置 > PATH 名字)───────────
608
- // MiMoCode CLI 官方便携安装目录:<USERPROFILE|HOME>/.mimocode/bin/mimo[.exe]。
609
- // 沙箱内可能没有 fs —— 全部读取走 typeof 守卫,读不到就返回空数组(退回 PATH 查找)。
610
- const MIMO_EXE_DIR = ['.mimocode', 'bin']
611
-
612
- /** 探测本机 mimo 可执行文件候选路径(官方便携安装位置);无 fs 能力时返回空数组。 */
613
- function mimoCandidateExePaths(env) {
614
- const e = env || {}
615
- const out = []
616
- const fs = mimoBuiltin('node:fs')
617
- if (!fs || typeof fs.existsSync !== 'function') return out
618
- const path = mimoBuiltin('node:path')
619
- const join = (path && typeof path.join === 'function') ? ((a, b) => path.join(a, b)) : ((a, b) => a + '/' + b)
620
- for (const k of ['USERPROFILE', 'HOME']) {
621
- if (!e[k]) continue
622
- const dir = join(join(String(e[k]), MIMO_EXE_DIR[0]), MIMO_EXE_DIR[1])
623
- for (const base of ['mimo.exe', 'mimo']) {
624
- const exe = join(dir, base)
625
- try { if (fs.existsSync(exe) && out.indexOf(exe) < 0) out.push(exe) } catch (err) { /* 探测失败静默跳过 */ }
626
- }
627
- }
628
- return out
629
- }
630
-
631
- /**
632
- * 解析本次调用使用的 exe 前缀(数组或字符串)。
633
- * 优先级:binPath(显式路径)> env MIMO_BIN/MIMO_CLI > 已知安装位置 > PATH 名字。
634
- */
635
- function resolveMimoExePrefix(env, binPath) {
636
- const e = env || {}
637
- const bin = binPath || e.MIMO_BIN || e.MIMO_CLI || ''
638
- if (bin) {
639
- if (/\.(mjs|cjs|js)$/i.test(bin)) {
640
- const proc = globalThis.process
641
- return [(proc && proc.execPath) ? proc.execPath : 'node', bin]
642
- }
643
- return [String(bin)]
644
- }
645
- const found = mimoCandidateExePaths(e)
646
- return found.length ? [found[0]] : ['mimo']
647
- }
648
-
649
- // ── 模型列表(`mimo models`;设置面板下拉用)───────────────────────────────────
650
- // 实测输出形如每行 `provider/model — window 1M, compacts at 900K`(分隔符是中文破折号
651
- // U+2014)。模型名自身含连字符(xiaomi/mimo-v2.6-pro-ultraspeed),所以只认「行首的
652
- // provider/model」,其后一律当描述文本;表头/说明/空行一律丢弃,绝不抛。
653
- const MIMO_MODEL_ID_RE = /^([A-Za-z0-9_.-]+\/[A-Za-z0-9_.@#+-]+)(?=$|[\s—–:|,])/
654
-
655
- /** 解析 `mimo models` 文本输出为 [{id, provider, name, note}](去重、保序)。 */
656
- function parseMimoModels(text) {
657
- const out = []
658
- const seen = Object.create(null)
659
- for (const raw of String(text || '').split(/\r?\n/)) {
660
- // 去掉列表记号(• * - 1. 等)与行首空白,避免把记号当 provider 名。
661
- const ln = raw.replace(/^[^A-Za-z0-9]+/, '').trim()
662
- if (!ln) continue
663
- const m = ln.match(MIMO_MODEL_ID_RE)
664
- if (!m) continue
665
- const id = m[1]
666
- if (seen[id]) continue
667
- seen[id] = true
668
- const slash = id.indexOf('/')
669
- out.push({ id: id, provider: id.slice(0, slash), name: id.slice(slash + 1), note: ln.slice(id.length).replace(/^[\s—–:|,-]+/, '').trim() })
670
- }
671
- return out
672
- }
673
-
674
- /**
675
- * 同步抓取模型列表(供设置面板下拉)。失败返回空数组 —— 下拉退化为自由输入,
676
- * 绝不因探测失败影响面板渲染。仅 spawnSync 可用时生效(宿主形态)。
677
- */
678
- function fetchMimoModelsSync(env, timeoutMs) {
679
- try {
680
- const child = mimoBuiltin('node:child_process')
681
- if (!child || typeof child.spawnSync !== 'function') return []
682
- const prefix = resolveMimoExePrefix(env)
683
- 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'] })
684
- if (!r || r.error || r.status !== 0) return []
685
- return parseMimoModels(String(r.stdout || ''))
686
- } catch (e) { return [] }
687
- }
688
-
689
- // ── 三工具 schema(旧 preset verbatim;desktop 专属入参已随之后端取消而删除)────
690
- const MODEL_DESC = 'provider/model 格式,如 xiaomi/mimo-v2.6-flash;留空用设置里的默认模型'
691
- const RUN_PARAMS = {
692
- type: 'object', additionalProperties: true, required: ['prompt'],
693
- properties: {
694
- 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).' },
695
- 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.' },
696
- model: { type: 'string', description: MODEL_DESC },
697
- effort: { type: 'string', enum: ['low', 'medium', 'high'], description: 'Optional reasoning effort, mapped to mimo --variant (low→minimal, high→high).' },
698
- cwd: { type: 'string', description: 'Working directory for mimo (--dir).' },
699
- timeoutSec: { type: 'integer', description: 'Hard timeout seconds (10-3600, default 600). Grace +60s on top.' },
700
- 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.' },
701
- sessionId: { type: 'string', description: 'Optional mimo session id to resume (--session). Follow-ups on the same topic normally go through mimo_continue.' },
702
- title: { type: 'string', description: 'Optional session title.' }
703
- }
704
- }
705
- const CONTINUE_PARAMS = {
706
- type: 'object', additionalProperties: true, required: ['prompt'],
707
- properties: {
708
- prompt: { type: 'string', description: 'Follow-up instruction for the ongoing mimo session. State only what is new; mimo preserves its conversation context.' },
709
- sessionId: { type: 'string', description: 'mimo session id to resume (--session).' },
710
- latest: { type: 'boolean', description: 'Continue the most recent mimo session (--continue).' },
711
- mode: { type: 'string', enum: ['auto', 'plan', 'build', 'compose', 'accept-edits'], description: 'Execution mode.' },
712
- model: { type: 'string', description: MODEL_DESC },
713
- effort: { type: 'string', enum: ['low', 'medium', 'high'], description: 'Optional reasoning effort.' },
714
- cwd: { type: 'string', description: 'Working directory for mimo.' },
715
- timeoutSec: { type: 'integer', description: 'Hard timeout seconds (10-3600, default 600).' },
716
- background: { type: 'boolean', description: 'Defaults to true (background job).' }
717
- }
718
- }
719
- const STATUS_PARAMS = { type: 'object', additionalProperties: true, required: [], properties: { cwd: { type: 'string', description: 'Optional: filter the snapshot to a single project (working directory).' } } }
720
- const SETTINGS_PARAMS = { type: 'object', additionalProperties: true, required: [], properties: { model: { type: 'string', description: MODEL_DESC } } }
721
- 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).'
722
-
723
- function renderSettings(value) {
724
- const s = value || {}
725
- const lines = [
726
- 'mimo settings',
727
- ' settings file : ' + (s.settingsPath || '(unknown)'),
728
- ' default model : ' + (s.defaultModel || '(unset — 不传 --model)'),
729
- ' effective : ' + (s.effective || '(unknown)'),
730
- ' exe : ' + (s.exe || '(unknown)'),
731
- ' saved : ' + (s.saved ? 'yes' : 'no')
732
- ]
733
- if (s.saveError) lines.push(' save error : ' + s.saveError)
734
- if (s.note) lines.push(' note : ' + s.note)
735
- return [{ type: 'text', text: lines.join('\n') }]
736
- }
737
- // ── 可纯函数化的 mimo-core 语义(descriptor 方法 + 独立导出双份)───────────────
738
-
739
- function defineProvider (deps = {}) {
740
- const env = deps.env || {}
741
- const nodeFs = deps.nodeFs || null
742
- const nodePath = deps.nodePath || null
743
- let host = null // attach 后保存 hostOpts(preset/dynamic 形态提供 getSettings)
744
- let settingsRef = { defaultModel: '' }
745
-
746
- /** 取当前生效的偏好设置。三条来源按优先级合并,每次 buildArgv 都重新读 ——
747
- * 面板改默认模型后无需重启即生效:
748
- * 1) `host.getSettings('mimo')` —— preset/dynamic 形态(attach 时由宿主注入);
749
- * 2) `deps.readSettings()` —— MCP 形态(真 Node 进程,每次现读设置文件);
750
- * 3) `settingsRef` —— attach 时固化的一次性快照 / 缺省值。
751
- * 三条都拿不到 → 缺省 `{defaultModel:''}`(= 不传 --model,交给 mimo CLI 自决)。
752
- *
753
- * 历史坑:MCP 形态不 attach(自建 engine),只认 host.getSettings 会让面板设置的
754
- * defaultModel 在 MCP 面对 mimo_run 完全失效 —— 只有 args.model 才落地。
755
- * 三个形态行为不一致是缺陷,故此处统一把 readSettings 纳入。 */
756
- function currentSettings() {
757
- const out = Object.assign({}, settingsRef)
758
- try {
759
- if (typeof deps.readSettings === 'function') {
760
- const r = deps.readSettings()
761
- if (r && typeof r === 'object') {
762
- // 宿主可返回 { partition, path, home } 或直接返回分区对象(宽松兼容)。
763
- const part = ('partition' in r || 'path' in r || 'home' in r) ? r.partition : r
764
- if (part && typeof part === 'object') Object.assign(out, part)
765
- }
766
- }
767
- } catch (e) { }
768
- try {
769
- if (host && typeof host.getSettings === 'function') {
770
- const s = host.getSettings('mimo')
771
- if (s && typeof s === 'object') Object.assign(out, s)
772
- }
773
- } catch (e) { }
774
- return out
775
- }
776
-
777
- // 模型优先级:调用参数 > 宿主注入设置 > runner opts 透传 > 空(不传 --model)。
778
- // 用户要求「不设默认 → 由 mimo CLI 自决」,故空值绝不落地 --model。
779
- function effectiveModel(args, opts) {
780
- const o = opts || {}
781
- const fromArg = (args && args.model && String(args.model).trim()) || ''
782
- if (fromArg) return fromArg
783
- const fromSettings = (currentSettings().defaultModel || '').trim()
784
- if (fromSettings) return fromSettings
785
- const fromOpts = (o.settings && o.settings.defaultModel) || o.defaultModel
786
- return fromOpts ? String(fromOpts).trim() : ''
787
- }
788
-
789
- // ── 统一设置分区读取(keystone 落盘;descriptor 只读)。宿主注入 deps.readSettings
790
- // 动态读取(preset/MCP 每次取最新文件值),或静态 deps.settings(测试/dynamic)。
791
- function liveSettings() {
792
- if (typeof deps.readSettings === 'function') {
793
- try {
794
- const r = deps.readSettings()
795
- if (r && typeof r === 'object') {
796
- // 宿主可返回 { partition, path, home } 或直接返回分区对象(宽松兼容)。
797
- if ('partition' in r || 'path' in r || 'home' in r) {
798
- return { partition: r.partition || null, path: r.path || null, home: r.home || null }
799
- }
800
- return { partition: r, path: deps.settingsPath || null, home: deps.dshHome || null }
801
- }
802
- } catch (e) { }
803
- }
804
- return { partition: (deps.settings && typeof deps.settings === 'object') ? deps.settings : null, path: deps.settingsPath || null, home: deps.dshHome || null }
805
- }
806
-
807
- const descriptor = {
808
- id: 'mimo', label: 'MIMO', presetId: 'cordis-mimo', defaultBackend: null, defaultMode: 'auto',
809
- exeName: 'mimo', binEnvVars: ['MIMO_BIN', 'MIMO_CLI'], unavailableStatus: 'MIMO_UNAVAILABLE',
810
-
811
- // 单后端:backends() 恒 null;历史别名(mimocode/mimo-cli/desktop)与非法值一律
812
- // 归一为 null(契约 §1:非法输入返回 null)。
813
- backends() { return null },
814
- normalizeBackend() { return null },
815
- resolveBackend(args, target, preferred) { return null },
816
-
817
- // 旧 preset buildArgv(exe, args, planActive) verbatim 语义(execPrefix 可为
818
- // [nodeExe, binPath] 数组,runner 已归一)。
819
- buildArgv(exePrefix, args, opts) {
820
- const o = opts || {}
821
- const timeoutSec = clampInt(args.timeoutSec, DEFAULT_TIMEOUT_SEC, 10, 3600)
822
- const argv = Array.isArray(exePrefix) ? exePrefix.slice() : [String(exePrefix)]
823
- argv.push('run')
824
- if (args.prompt != null && String(args.prompt).length) argv.push(String(args.prompt))
825
- argv.push('--format', 'json', '--dangerously-skip-permissions')
826
- let mode = args.mode || o.defaultMode || 'auto'
827
- if (mode === 'auto') mode = o.planActive ? 'plan' : 'build'
828
- if (mode === 'accept-edits') mode = 'build'
829
- // mimo --agent: build | plan | compose
830
- const agent = args.agent || mode
831
- if (agent === 'plan' || agent === 'build' || agent === 'compose') argv.push('--agent', agent)
832
- // 模型:调用参数 > 设置 defaultModel > 不传(mimo CLI 自身默认)。
833
- const model = effectiveModel(args, o)
834
- if (model) argv.push('--model', String(model))
835
- // effort (low/medium/high) maps to mimo --variant; pass-through if already a variant name
836
- let variant = args.variant || args.effort
837
- if (variant === 'low') variant = 'minimal'
838
- else if (variant === 'medium') variant = undefined
839
- else if (variant === 'high') variant = 'high'
840
- if (variant) argv.push('--variant', String(variant))
841
- if (args.cwd) argv.push('--dir', String(args.cwd))
842
- if (args.sessionId) argv.push('--session', String(args.sessionId))
843
- else if (args.continueLatest) argv.push('--continue')
844
- if (args.title) argv.push('--title', String(args.title))
845
- return { argv, timeoutSec, mode: agent }
846
- },
847
-
848
- parseJson: parseMimoJson,
849
- buildResult,
850
-
851
- // 旧 foldStepUpdate 语义 verbatim(识别 tool/text/error 事件),trail 条目同时带
852
- // kit 展示层字段(stepIndex/tool/state/args)与旧字段(kind/label),这样
853
- // renderStatus 与 MCP 快照映射(buildMcpBridgePayload)都能直接渲染。
854
- foldStreamEvent(ev, p, summarizeArgsFn, maxArgLen, maxTrail) {
855
- if (!ev || typeof ev !== 'object') return
856
- const t = ev.type
857
- const pr = ev.properties || ev
858
- const cap = maxTrail || 12
859
- const idx = p.trail ? p.trail.length : 0
860
- let entry = null
861
- if (t === 'tool' || t === 'tool-call' || t === 'tool_use' || t === 'step_update') {
862
- const name = pr.tool_name || pr.tool || pr.name
863
- if (name) {
864
- const args = (summarizeArgsFn || summarizeArgs)(pr.arguments || pr.args || pr.input) || null
865
- const state = pr.state || pr.status || 'ACTIVE'
866
- entry = {
867
- at: Date.now(), kind: 'tool', stepIndex: idx, tool: String(name), args, state,
868
- label: shortLabel(name + ' ' + JSON.stringify(args || {}))
869
- }
870
- if (state === 'ACTIVE') p.current = { tool: String(name), args, stepIndex: idx, since: entry.at }
871
- else if (p.current && p.current.tool === String(name) && p.current.stepIndex === idx) p.current = null
872
- }
873
- } else if (t === 'text' || t === 'message') {
874
- const chunk = pr.text || pr.content || ''
875
- if (typeof chunk === 'string' && chunk) {
876
- const capLen = maxArgLen || 120
877
- entry = { at: Date.now(), kind: 'text', stepIndex: idx, tool: 'agent_response', args: { text: chunk.slice(0, capLen) }, state: 'ACTIVE', label: shortLabel(chunk) }
878
- p.current = { tool: 'agent_response', args: entry.args, stepIndex: idx, since: entry.at }
879
- }
880
- } else if (t === 'error') {
881
- const err = pr.error || pr
882
- const msg = (err.data && err.data.message) || err.message || 'error'
883
- entry = { at: Date.now(), kind: 'error', stepIndex: idx, tool: 'error', args: { message: String(msg).slice(0, maxArgLen || 120) }, state: 'FAILED', label: shortLabel(msg) }
884
- }
885
- if (entry && p.trail) {
886
- p.trail.push(entry)
887
- while (p.trail.length > cap) p.trail.shift()
888
- }
889
- },
890
-
891
- // 旧 LIMIT_RE 覆盖(kit 版不含 free API/登录/第三方等 MiMo 文案)。
892
- isLimited(res) {
893
- if (!res || res.ok) return false
894
- if (res.status === 'SPAWN_ERROR' || res.status === 'MIMO_UNAVAILABLE' || res.status === 'HUNG_TIMEOUT') return true
895
- const hay = String(res.stderr || '') + ' ' + String(res.response || '') + ' ' + String(res.status || '') + ' ' + String(res.errorMessage || '')
896
- return MIMO_LIMIT_RE.test(hay)
897
- },
898
-
899
- failureHint(res) {
900
- if (!res || res.ok) return ''
901
- const st = String(res.status || '')
902
- if (st === 'MIMO_UNAVAILABLE') return '[诊断] 未找到 mimo 可执行文件:请安装 MiMoCode CLI(mimo,官方便携安装位置 ~/.mimocode/bin/mimo.exe),或设置 MIMO_BIN 指向其绝对路径。'
903
- const hay = String(res.stderr || '') + ' ' + String(res.errorMessage || '') + ' ' + String(res.response || '')
904
- if (/free API service has ended|sign in|configure a third-party|登录|第三方|免费/i.test(hay)) {
905
- return '[诊断] MiMo 免费 API 已终止,请在 mimo CLI 用 `mimo auth login` 登录或配置第三方 API,并在 CLI Bridge 设置里指定模型。'
906
- }
907
- if (/model not found|unknown model|no models available|invalid model|unknown provider/i.test(hay)) {
908
- return '[诊断] mimo 模型不可用或模型名无效:先在本机运行 `mimo models` 列出可用模型,再把 provider/model 填进 CLI Bridge 的「默认模型」设置(或本次调用的 model 参数)。'
909
- }
910
- return ''
911
- },
912
-
913
- // ACCEPTED 计入成功态(指示灯不红;契约 §1);desktop 取消后不再产生 ACCEPTED,
914
- // 但成功态集合逐字保持不变(兼容旧会话/测试语义)。
915
- isSuccessStatus(st) {
916
- if (!st) return false
917
- return st === 'SUCCESS' || st === 'ACCEPTED'
918
- },
919
-
920
- tools: {
921
- 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 },
922
- 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 },
923
- 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 }
924
- },
925
-
926
- // mimo_settings:入参键保持旧 prompt 连续性(backend/desktopModel → model),落统一
927
- // 分区 defaultModel(迁移映射 keystone 已做)。写盘仅在宿主注入 nodeFs + nodePath 时
928
- // 可用;否则回显(dynamic 沙箱旧 echo 语义)。
929
- extraTools: [{
930
- name: 'mimo_settings',
931
- description: SETTINGS_DESCRIPTION,
932
- inputSchema: SETTINGS_PARAMS,
933
- render: renderSettings,
934
- async logic(args) {
935
- const a = args || {}
936
- const live = liveSettings()
937
- const cur = Object.assign({ defaultModel: '' }, live.partition || {})
938
- let settingsPath = live.path || null
939
- let saved = false, saveError = null
940
- const home = live.home || deps.dshHome || null
941
- const patch = (typeof a.model === 'string' && a.model.trim()) ? { defaultModel: a.model.trim() } : null
942
- if (patch && nodeFs && nodePath && (settingsPath || home)) {
943
- try { settingsPath = await writePartition(nodeFs, nodePath, settingsPath, home, env, cur, patch); saved = true } catch (e) { saveError = String(e && e.message || e) }
944
- }
945
- const merged = Object.assign({}, cur)
946
- if (patch) merged.defaultModel = patch.defaultModel
947
- return {
948
- ok: !saveError,
949
- settingsPath: settingsPath || '(unified; not writable in this host)',
950
- defaultModel: merged.defaultModel || '',
951
- effective: merged.defaultModel ? ('settings:' + merged.defaultModel) : '(unset — 不传 --model,mimo CLI 自决)',
952
- exe: resolveMimoExePrefix(env).join(' '),
953
- saved, saveError,
954
- note: '默认模型在设置面板或本工具里指定(provider/model);mimo_run/mimo_continue 的 model 参数可单次覆盖。可用模型:在本机运行 `mimo models` 查看。'
955
- }
956
- }
957
- }],
958
-
959
- // 设置分区(契约 §3):keystone 读写/GUI;本处纯逻辑。diagnose/meta 必须同步。
960
- settings: {
961
- keys: ['defaultModel'],
962
- defaults: () => ({ defaultModel: '' }),
963
- normalize(raw) {
964
- const src = raw && typeof raw === 'object' ? raw : {}
965
- return { defaultModel: (typeof src.defaultModel === 'string' && src.defaultModel.trim()) ? src.defaultModel.trim() : '' }
966
- },
967
- meta(envArg) {
968
- const e = envArg && typeof envArg === 'object' ? envArg : env
969
- // 下拉选项:实时 `mimo models`(失败则退化为空 → 面板仍可自由输入)。
970
- let options = []
971
- try {
972
- options = fetchMimoModelsSync(e, 8000).map((m) => ({ value: m.id, label: m.id }))
973
- } catch (err) { options = [] }
974
- return {
975
- titleSuffix: 'MiMo',
976
- fields: [{
977
- key: 'defaultModel', label: '默认模型', type: 'select', allowCustom: true,
978
- options: options,
979
- placeholder: '留空 = 不传 --model(由 mimo CLI 自身默认决定)',
980
- note: options.length
981
- ? '共 ' + options.length + ' 个可用模型(来自 `mimo models`)。mimo_run/mimo_continue 的 model 参数可单次覆盖。'
982
- : '未能读取模型列表(mimo CLI 未安装/未登录/超时)—— 仍可手动输入 provider/model,如 xiaomi/mimo-v2.6-flash。'
983
- }]
984
- }
985
- },
986
- // 同步一行诊断:当前模型 + exe 来源(env > 已知安装位置 > PATH)。
987
- diagnose(settings, envArg) {
988
- const s = settings && typeof settings === 'object' ? settings : {}
989
- const e = envArg && typeof envArg === 'object' ? envArg : env
990
- const model = s.defaultModel ? 'model=' + s.defaultModel : 'model=(未设置 → 不传 --model)'
991
- if (e.MIMO_BIN || e.MIMO_CLI) return model + ' · bin=env:' + (e.MIMO_BIN || e.MIMO_CLI)
992
- const found = mimoCandidateExePaths(e)
993
- return model + ' · ' + (found.length ? 'bin=' + found[0] : 'bin=PATH:mimo')
994
- }
995
- },
996
-
997
- // MCP 形态扩展:**不含 version**。历史:四处 `mcp: { version: '0.2.2' }` 与
998
- // mcp/version.mjs 的 packageVersion() 双源,漏改一处就出现「统一 server 报 0.2.3、
999
- // provider 面仍报 0.2.2」的自相矛盾输出;且 mcp/version.mjs 依赖 import.meta + node:fs,
1000
- // 其函数体无法随本文件内联进 dynamic 沙箱(build 文本拼接会丢实现/炸语法)。
1001
- // 定案:版本号单一来源 = mcp/version.mjs 的 packageVersion(),由 MCP 面(mcp/providers/
1002
- // mimo.mjs 的 meta.version)读取;描述符不复制 version 字面量,宿主如需注入可经 deps.mcp。
1003
- mcp: (deps.mcp && typeof deps.mcp === 'object') ? Object.assign({}, deps.mcp) : {}
1004
- }
1005
-
1006
- // 统一分区写入(tmp→rename 原子写;与 keystone lib/settings.mjs 同文件同分区规则,
1007
- // 本工具自实现落盘 —— 不 import home-plugin;宿主给出 settingsPath 或 dshHome)。
1008
- async function writePartition(fsMod, pathMod, knownPath, home, environment, cur, patch) {
1009
- const file = knownPath || environment.CLI_BRIDGE_SETTINGS_FILE || (home ? pathMod.join(home, 'cli-bridge-settings.json') : null)
1010
- if (!file) throw new Error('dsh-home not resolved (set DSH_HOME or CLI_BRIDGE_SETTINGS_FILE)')
1011
- let all = {}
1012
- try { all = JSON.parse(fsMod.readFileSync(file, 'utf8')) } catch (e) { all = {} }
1013
- if (!all || typeof all !== 'object') all = {}
1014
- const next = Object.assign({}, all)
1015
- const part = Object.assign({}, cur, next.mimo || {})
1016
- if (patch && patch.defaultModel) part.defaultModel = patch.defaultModel
1017
- next.mimo = descriptor.settings.normalize(part)
1018
- next.updatedAt = Date.now()
1019
- try { fsMod.mkdirSync(pathMod.dirname(file), { recursive: true }) } catch (e) { }
1020
- const tmp = file + '.tmp-' + ((typeof process !== 'undefined' && process.pid) || 'mimo')
1021
- fsMod.writeFileSync(tmp, JSON.stringify(next, null, 2), 'utf8')
1022
- fsMod.renameSync(tmp, file)
1023
- return file
1024
- }
1025
-
1026
- // 三形态共用的工具逻辑。签名保持不变(keystone 与队友按此接线)。
1027
- // 旧 mimo preset 语义:background 默认 true(仅显式 false 才前台阻塞)。
1028
- descriptor.runToolLogic = async function (args, exec) {
1029
- const a = args || {}
1030
- const normalized = a.background === undefined ? Object.assign({}, a, { background: true }) : a
1031
- return this.runner.coreExecute(normalized, exec)
1032
- }
1033
- descriptor.continueToolLogic = async function (args, exec) {
1034
- const a = args || {}
1035
- 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 }
1036
- if (a.sessionId) mapped.sessionId = a.sessionId
1037
- else if (a.latest) mapped.continueLatest = true
1038
- return this.runToolLogic(mapped, exec)
1039
- }
1040
- // 旧 statusTool.execute verbatim:cwd 过滤到单项目(Object.assign 形式与 opencode 一致)。
1041
- descriptor.statusToolLogic = async function (args) {
1042
- const a = args || {}
1043
- const snap = this.engine.statusSnapshot()
1044
- if (a.cwd) {
1045
- const key = String(a.cwd)
1046
- snap.projects = (snap.projects || []).filter((p) => p.cwd === key)
1047
- const g = snap.projects[0]
1048
- if (g) {
1049
- Object.assign(snap, {
1050
- state: g.state, running: g.running, current: g.current, trail: g.trail,
1051
- lastStatus: g.lastStatus, lastAt: g.lastAt, lastSessionId: g.lastSessionId,
1052
- lastOk: g.lastOk, lastFailed: g.lastFailed, fallbackActive: g.fallbackActive,
1053
- runs: g.runs, totalTokens: g.totalTokens, updatedAt: g.updatedAt
1054
- })
1055
- }
1056
- }
1057
- return snap
1058
- }
1059
- // 工厂装配(provider 无关骨架 + 描述符);宿主注入 getSettings 时把面板偏好带进 buildArgv。
1060
- descriptor.attach = function (hostOpts) {
1061
- host = hostOpts || {}
1062
- try {
1063
- if (typeof host.getSettings === 'function') {
1064
- const s = host.getSettings('mimo')
1065
- if (s && typeof s === 'object') settingsRef = descriptor.settings.normalize(s)
1066
- }
1067
- } catch (e) { }
1068
- this.engine = createStatusEngine({ provider: this, publish: host.publish })
1069
- const runner = createRunner(Object.assign({}, host, { provider: this, engine: this.engine }))
1070
- this.runner = runner
1071
- return runner
1072
- }
1073
- // 测试/宿主可见的纯函数面(不进协议字段)。currentSettings/liveSettings/writePartition 是
1074
- // 闭包函数,不依赖 attach(未 attach 时 host 为 null → 走 deps 注入或默认空值),测试可
1075
- // 直接调用;writePartition 需要调用方自行传入可用的 fs/path 模块。
1076
- descriptor._internals = { MIMO_LIMIT_RE, parseMimoJson, buildResult,
1077
- mimoCandidateExePaths, resolveMimoExePrefix, parseMimoModels, fetchMimoModelsSync,
1078
- currentSettings, liveSettings, writePartition
1079
- }
1080
- return descriptor
1081
- }
569
+ // providers/mimo.mjs — mimo provider descriptor(单后端:本地 mimo CLI / MiMoCode)。
570
+ //
571
+ // 移植基线:mimo-first-bridge v1.x 仓 core/mimo-core.mjs + core/runtime-settings.mjs +
572
+ // preset/mimo-first/mimo-first-bridge.mjs 的内联逻辑(buildArgv / parseMimoJson /
573
+ // buildResult / 三工具 schema / mimo_settings),语义逐字保持;与 provider 无关的骨架
574
+ // (状态引擎/行流/回退弹窗/展示层/超时护栏)在 core/bridge-kit.mjs(契约 §1)。
575
+ //
576
+ // 2026-10 去 desktop 化(用户定案:MiMo 免费 API 服务已被官方终止 + 本机 0 凭据):
577
+ // 1. 取消 mimo-desktop 后端 —— 只走 `mimo run`(MiMoCode CLI,官方便携安装位置
578
+ // ~/.mimocode/bin/mimo.exe);登录由用户在 CLI 侧 `mimo auth login` 自理。
579
+ // 2. 模型由用户在设置里手动指定(settings.defaultModel,不写死默认值);留空 =
580
+ // 不传 --model,交给 mimo CLI 自身默认。可用模型以 `mimo models` 实测为准
581
+ // (xiaomi/mimo-v2.6-flash / -pro / -pro-ultraspeed 等)。
582
+ // 契约 §3.2:面板**不自动探测**,meta() 只读上次手动拉取的缓存;用户点
583
+ // 「拉取可用模型」才经 settings.refreshModels 跑一次 `mimo models`。
584
+ // 3. 是否启用 mimo 由调度(_scheduling.enabled)控制;本描述符不含任何开关。
585
+ //
586
+ // 红线(PROVIDER-CONTRACT.md §8 + Lead 定案):本文件会被 build 内联进 dynamic 沙箱
587
+ // (new Function)—— 绝不出现 import.meta 字面量;process/fs/child_process 只经
588
+ // globalThis.process.getBuiltinModule + typeof 守卫读取;只允许 import bridge-kit
589
+ // 与同目录的 mimo-pure.mjs 片文件(build 会把 providers/mimo-*.mjs 一并内联进同一沙箱作用域)。
590
+ // import { createStatusEngine, createRunner, clampInt, shortLabel, summarizeArgs } from '../core/bridge-kit.mjs'
591
+ // import { parseMimoJson, buildResult, mimoCandidateExePaths, resolveMimoExePrefix, parseMimoModels, fetchMimoModelsDetailed, fetchMimoModelsSync, cachedMimoModels, rememberMimoModels } from './mimo-pure.mjs'
592
+ // 模型清单探测链路搬到片文件后仍保留旧 import 入口(ESM 具名导出;dynamic 内联时本行被 build 剥掉)。
593
+
594
+ // ── 单后端归一(旧 mimo-core 双后端 verbatim 的收缩形态)───────────────────────
595
+ // mimo-desktop 取消后不再有第二后端:backends() 恒 null;任何输入(含历史别名
596
+ // mimocode / mimo-cli / desktop / 空串 / 非法值)一律归一为 null。会话路由由
597
+ // engine.resolveTarget(cwd + sessionId)完成,backend 恒 null。
598
+ const DEFAULT_TIMEOUT_SEC = 600
599
+
600
+ // 旧 LIMIT_RE(比 kit 版多 free-API/登录/第三方等 MiMo 文案;provider.isLimited 覆盖)。
601
+ // 命名 MIMO_ 前缀:dynamic 内联时 kit 也声明顶层 LIMIT_RE,同名会在同一脚本作用域
602
+ // 触发「Identifier already declared」构建错误(scripts/build.mjs 文本拼接的固有约束)。
603
+ 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
604
+
605
+ // ── 模型清单探测(可执行路径 / `mimo models` 解析 / 手动拉取缓存)──────────────
606
+ // 全部在 providers/mimo-pure.mjs(本文件只消费):契约 §3.2 要求面板渲染不 spawn,
607
+ // 只有 settings.refreshModels 才会真正跑一次 `mimo models`,结果进内存缓存供 meta() 读。
608
+
609
+
610
+ // ── 三工具 schema(旧 preset verbatim;desktop 专属入参已随之后端取消而删除)────
611
+ const MODEL_DESC = 'provider/model 格式,如 xiaomi/mimo-v2.6-flash;留空用设置里的默认模型'
612
+ const RUN_PARAMS = {
613
+ type: 'object', additionalProperties: true, required: ['prompt'],
614
+ properties: {
615
+ 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).' },
616
+ 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.' },
617
+ model: { type: 'string', description: MODEL_DESC },
618
+ effort: { type: 'string', enum: ['low', 'medium', 'high'], description: 'Optional reasoning effort, mapped to mimo --variant (low→minimal, high→high).' },
619
+ cwd: { type: 'string', description: 'Working directory for mimo (--dir).' },
620
+ timeoutSec: { type: 'integer', description: 'Hard timeout seconds (10-3600, default 600). Grace +60s on top.' },
621
+ 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.' },
622
+ sessionId: { type: 'string', description: 'Optional mimo session id to resume (--session). Follow-ups on the same topic normally go through mimo_continue.' },
623
+ title: { type: 'string', description: 'Optional session title.' }
624
+ }
625
+ }
626
+ const CONTINUE_PARAMS = {
627
+ type: 'object', additionalProperties: true, required: ['prompt'],
628
+ properties: {
629
+ prompt: { type: 'string', description: 'Follow-up instruction for the ongoing mimo session. State only what is new; mimo preserves its conversation context.' },
630
+ sessionId: { type: 'string', description: 'mimo session id to resume (--session).' },
631
+ latest: { type: 'boolean', description: 'Continue the most recent mimo session (--continue).' },
632
+ mode: { type: 'string', enum: ['auto', 'plan', 'build', 'compose', 'accept-edits'], description: 'Execution mode.' },
633
+ model: { type: 'string', description: MODEL_DESC },
634
+ effort: { type: 'string', enum: ['low', 'medium', 'high'], description: 'Optional reasoning effort.' },
635
+ cwd: { type: 'string', description: 'Working directory for mimo.' },
636
+ timeoutSec: { type: 'integer', description: 'Hard timeout seconds (10-3600, default 600).' },
637
+ background: { type: 'boolean', description: 'Defaults to true (background job).' }
638
+ }
639
+ }
640
+ const STATUS_PARAMS = { type: 'object', additionalProperties: true, required: [], properties: { cwd: { type: 'string', description: 'Optional: filter the snapshot to a single project (working directory).' } } }
641
+ const SETTINGS_PARAMS = { type: 'object', additionalProperties: true, required: [], properties: { model: { type: 'string', description: MODEL_DESC } } }
642
+ 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).'
643
+
644
+ function renderSettings(value) {
645
+ const s = value || {}
646
+ const lines = [
647
+ 'mimo settings',
648
+ ' settings file : ' + (s.settingsPath || '(unknown)'),
649
+ ' default model : ' + (s.defaultModel || '(unset — 不传 --model)'),
650
+ ' effective : ' + (s.effective || '(unknown)'),
651
+ ' exe : ' + (s.exe || '(unknown)'),
652
+ ' saved : ' + (s.saved ? 'yes' : 'no')
653
+ ]
654
+ if (s.saveError) lines.push(' save error : ' + s.saveError)
655
+ if (s.note) lines.push(' note : ' + s.note)
656
+ return [{ type: 'text', text: lines.join('\n') }]
657
+ }
658
+ // ── 可纯函数化的 mimo-core 语义(descriptor 方法 + 独立导出双份)───────────────
659
+
660
+ function defineProvider (deps = {}) {
661
+ const env = deps.env || {}
662
+ const nodeFs = deps.nodeFs || null
663
+ const nodePath = deps.nodePath || null
664
+ let host = null // attach 后保存 hostOpts(preset/dynamic 形态提供 getSettings)
665
+ let settingsRef = { defaultModel: '' }
666
+
667
+ /** 取当前生效的偏好设置。三条来源按优先级合并,每次 buildArgv 都重新读 ——
668
+ * 面板改默认模型后无需重启即生效:
669
+ * 1) `host.getSettings('mimo')` —— preset/dynamic 形态(attach 时由宿主注入);
670
+ * 2) `deps.readSettings()` —— MCP 形态(真 Node 进程,每次现读设置文件);
671
+ * 3) `settingsRef` —— attach 时固化的一次性快照 / 缺省值。
672
+ * 三条都拿不到 → 缺省 `{defaultModel:''}`(= 不传 --model,交给 mimo CLI 自决)。
673
+ *
674
+ * 历史坑:MCP 形态不 attach(自建 engine),只认 host.getSettings 会让面板设置的
675
+ * defaultModel 在 MCP 面对 mimo_run 完全失效 —— 只有 args.model 才落地。
676
+ * 三个形态行为不一致是缺陷,故此处统一把 readSettings 纳入。 */
677
+ function currentSettings() {
678
+ const out = Object.assign({}, settingsRef)
679
+ try {
680
+ if (typeof deps.readSettings === 'function') {
681
+ const r = deps.readSettings()
682
+ if (r && typeof r === 'object') {
683
+ // 宿主可返回 { partition, path, home } 或直接返回分区对象(宽松兼容)。
684
+ const part = ('partition' in r || 'path' in r || 'home' in r) ? r.partition : r
685
+ if (part && typeof part === 'object') Object.assign(out, part)
686
+ }
687
+ }
688
+ } catch (e) { }
689
+ try {
690
+ if (host && typeof host.getSettings === 'function') {
691
+ const s = host.getSettings('mimo')
692
+ if (s && typeof s === 'object') Object.assign(out, s)
693
+ }
694
+ } catch (e) { }
695
+ return out
696
+ }
697
+
698
+ // 模型优先级:调用参数 > 宿主注入设置 > runner opts 透传 > 空(不传 --model)。
699
+ // 用户要求「不设默认 → 由 mimo CLI 自决」,故空值绝不落地 --model。
700
+ function effectiveModel(args, opts) {
701
+ const o = opts || {}
702
+ const fromArg = (args && args.model && String(args.model).trim()) || ''
703
+ if (fromArg) return fromArg
704
+ const fromSettings = (currentSettings().defaultModel || '').trim()
705
+ if (fromSettings) return fromSettings
706
+ const fromOpts = (o.settings && o.settings.defaultModel) || o.defaultModel
707
+ return fromOpts ? String(fromOpts).trim() : ''
708
+ }
709
+
710
+ // ── 统一设置分区读取(keystone 落盘;descriptor 只读)。宿主注入 deps.readSettings
711
+ // 动态读取(preset/MCP 每次取最新文件值),或静态 deps.settings(测试/dynamic)。
712
+ function liveSettings() {
713
+ if (typeof deps.readSettings === 'function') {
714
+ try {
715
+ const r = deps.readSettings()
716
+ if (r && typeof r === 'object') {
717
+ // 宿主可返回 { partition, path, home } 或直接返回分区对象(宽松兼容)。
718
+ if ('partition' in r || 'path' in r || 'home' in r) {
719
+ return { partition: r.partition || null, path: r.path || null, home: r.home || null }
720
+ }
721
+ return { partition: r, path: deps.settingsPath || null, home: deps.dshHome || null }
722
+ }
723
+ } catch (e) { }
724
+ }
725
+ return { partition: (deps.settings && typeof deps.settings === 'object') ? deps.settings : null, path: deps.settingsPath || null, home: deps.dshHome || null }
726
+ }
727
+
728
+ const descriptor = {
729
+ id: 'mimo', label: 'MIMO', presetId: 'cordis-mimo', defaultBackend: null, defaultMode: 'auto',
730
+ exeName: 'mimo', binEnvVars: ['MIMO_BIN', 'MIMO_CLI'], unavailableStatus: 'MIMO_UNAVAILABLE',
731
+
732
+ // 单后端:backends() 恒 null;历史别名(mimocode/mimo-cli/desktop)与非法值一律
733
+ // 归一为 null(契约 §1:非法输入返回 null)。
734
+ backends() { return null },
735
+ normalizeBackend() { return null },
736
+ resolveBackend(args, target, preferred) { return null },
737
+
738
+ // 旧 preset buildArgv(exe, args, planActive) verbatim 语义(execPrefix 可为
739
+ // [nodeExe, binPath] 数组,runner 已归一)。
740
+ buildArgv(exePrefix, args, opts) {
741
+ const o = opts || {}
742
+ const timeoutSec = clampInt(args.timeoutSec, DEFAULT_TIMEOUT_SEC, 10, 3600)
743
+ const argv = Array.isArray(exePrefix) ? exePrefix.slice() : [String(exePrefix)]
744
+ argv.push('run')
745
+ if (args.prompt != null && String(args.prompt).length) argv.push(String(args.prompt))
746
+ argv.push('--format', 'json', '--dangerously-skip-permissions')
747
+ let mode = args.mode || o.defaultMode || 'auto'
748
+ if (mode === 'auto') mode = o.planActive ? 'plan' : 'build'
749
+ if (mode === 'accept-edits') mode = 'build'
750
+ // mimo --agent: build | plan | compose
751
+ const agent = args.agent || mode
752
+ if (agent === 'plan' || agent === 'build' || agent === 'compose') argv.push('--agent', agent)
753
+ // 模型:调用参数 > 设置 defaultModel > 不传(mimo CLI 自身默认)。
754
+ const model = effectiveModel(args, o)
755
+ if (model) argv.push('--model', String(model))
756
+ // effort (low/medium/high) maps to mimo --variant; pass-through if already a variant name
757
+ let variant = args.variant || args.effort
758
+ if (variant === 'low') variant = 'minimal'
759
+ else if (variant === 'medium') variant = undefined
760
+ else if (variant === 'high') variant = 'high'
761
+ if (variant) argv.push('--variant', String(variant))
762
+ if (args.cwd) argv.push('--dir', String(args.cwd))
763
+ if (args.sessionId) argv.push('--session', String(args.sessionId))
764
+ else if (args.continueLatest) argv.push('--continue')
765
+ if (args.title) argv.push('--title', String(args.title))
766
+ return { argv, timeoutSec, mode: agent }
767
+ },
768
+
769
+ parseJson: parseMimoJson,
770
+ buildResult,
771
+
772
+ // 旧 foldStepUpdate 语义 verbatim(识别 tool/text/error 事件),trail 条目同时带
773
+ // kit 展示层字段(stepIndex/tool/state/args)与旧字段(kind/label),这样
774
+ // renderStatus 与 MCP 快照映射(buildMcpBridgePayload)都能直接渲染。
775
+ foldStreamEvent(ev, p, summarizeArgsFn, maxArgLen, maxTrail) {
776
+ if (!ev || typeof ev !== 'object') return
777
+ const t = ev.type
778
+ const pr = ev.properties || ev
779
+ const cap = maxTrail || 12
780
+ const idx = p.trail ? p.trail.length : 0
781
+ let entry = null
782
+ if (t === 'tool' || t === 'tool-call' || t === 'tool_use' || t === 'step_update') {
783
+ const name = pr.tool_name || pr.tool || pr.name
784
+ if (name) {
785
+ const args = (summarizeArgsFn || summarizeArgs)(pr.arguments || pr.args || pr.input) || null
786
+ const state = pr.state || pr.status || 'ACTIVE'
787
+ entry = {
788
+ at: Date.now(), kind: 'tool', stepIndex: idx, tool: String(name), args, state,
789
+ label: shortLabel(name + ' ' + JSON.stringify(args || {}))
790
+ }
791
+ if (state === 'ACTIVE') p.current = { tool: String(name), args, stepIndex: idx, since: entry.at }
792
+ else if (p.current && p.current.tool === String(name) && p.current.stepIndex === idx) p.current = null
793
+ }
794
+ } else if (t === 'text' || t === 'message') {
795
+ const chunk = pr.text || pr.content || ''
796
+ if (typeof chunk === 'string' && chunk) {
797
+ const capLen = maxArgLen || 120
798
+ entry = { at: Date.now(), kind: 'text', stepIndex: idx, tool: 'agent_response', args: { text: chunk.slice(0, capLen) }, state: 'ACTIVE', label: shortLabel(chunk) }
799
+ p.current = { tool: 'agent_response', args: entry.args, stepIndex: idx, since: entry.at }
800
+ }
801
+ } else if (t === 'error') {
802
+ const err = pr.error || pr
803
+ const msg = (err.data && err.data.message) || err.message || 'error'
804
+ entry = { at: Date.now(), kind: 'error', stepIndex: idx, tool: 'error', args: { message: String(msg).slice(0, maxArgLen || 120) }, state: 'FAILED', label: shortLabel(msg) }
805
+ }
806
+ if (entry && p.trail) {
807
+ p.trail.push(entry)
808
+ while (p.trail.length > cap) p.trail.shift()
809
+ }
810
+ },
811
+
812
+ // 旧 LIMIT_RE 覆盖(kit 版不含 free API/登录/第三方等 MiMo 文案)。
813
+ isLimited(res) {
814
+ if (!res || res.ok) return false
815
+ if (res.status === 'SPAWN_ERROR' || res.status === 'MIMO_UNAVAILABLE' || res.status === 'HUNG_TIMEOUT') return true
816
+ const hay = String(res.stderr || '') + ' ' + String(res.response || '') + ' ' + String(res.status || '') + ' ' + String(res.errorMessage || '')
817
+ return MIMO_LIMIT_RE.test(hay)
818
+ },
819
+
820
+ failureHint(res) {
821
+ if (!res || res.ok) return ''
822
+ const st = String(res.status || '')
823
+ if (st === 'MIMO_UNAVAILABLE') return '[诊断] 未找到 mimo 可执行文件:请安装 MiMoCode CLI(mimo,官方便携安装位置 ~/.mimocode/bin/mimo.exe),或设置 MIMO_BIN 指向其绝对路径。'
824
+ const hay = String(res.stderr || '') + ' ' + String(res.errorMessage || '') + ' ' + String(res.response || '')
825
+ if (/free API service has ended|sign in|configure a third-party|登录|第三方|免费/i.test(hay)) {
826
+ return '[诊断] MiMo 免费 API 已终止,请在 mimo CLI 用 `mimo auth login` 登录或配置第三方 API,并在 CLI Bridge 设置里指定模型。'
827
+ }
828
+ if (/model not found|unknown model|no models available|invalid model|unknown provider/i.test(hay)) {
829
+ return '[诊断] mimo 模型不可用或模型名无效:先在本机运行 `mimo models` 列出可用模型,再把 provider/model 填进 CLI Bridge 的「默认模型」设置(或本次调用的 model 参数)。'
830
+ }
831
+ return ''
832
+ },
833
+
834
+ // ACCEPTED 计入成功态(指示灯不红;契约 §1);desktop 取消后不再产生 ACCEPTED,
835
+ // 但成功态集合逐字保持不变(兼容旧会话/测试语义)。
836
+ isSuccessStatus(st) {
837
+ if (!st) return false
838
+ return st === 'SUCCESS' || st === 'ACCEPTED'
839
+ },
840
+
841
+ tools: {
842
+ 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 },
843
+ 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 },
844
+ 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 }
845
+ },
846
+
847
+ // mimo_settings:入参键保持旧 prompt 连续性(backend/desktopModel → model),落统一
848
+ // 分区 defaultModel(迁移映射 keystone 已做)。写盘仅在宿主注入 nodeFs + nodePath 时
849
+ // 可用;否则回显(dynamic 沙箱旧 echo 语义)。
850
+ extraTools: [{
851
+ name: 'mimo_settings',
852
+ description: SETTINGS_DESCRIPTION,
853
+ inputSchema: SETTINGS_PARAMS,
854
+ render: renderSettings,
855
+ async logic(args) {
856
+ const a = args || {}
857
+ const live = liveSettings()
858
+ const cur = Object.assign({ defaultModel: '' }, live.partition || {})
859
+ let settingsPath = live.path || null
860
+ let saved = false, saveError = null
861
+ const home = live.home || deps.dshHome || null
862
+ const patch = (typeof a.model === 'string' && a.model.trim()) ? { defaultModel: a.model.trim() } : null
863
+ if (patch && nodeFs && nodePath && (settingsPath || home)) {
864
+ try { settingsPath = await writePartition(nodeFs, nodePath, settingsPath, home, env, cur, patch); saved = true } catch (e) { saveError = String(e && e.message || e) }
865
+ }
866
+ const merged = Object.assign({}, cur)
867
+ if (patch) merged.defaultModel = patch.defaultModel
868
+ return {
869
+ ok: !saveError,
870
+ settingsPath: settingsPath || '(unified; not writable in this host)',
871
+ defaultModel: merged.defaultModel || '',
872
+ effective: merged.defaultModel ? ('settings:' + merged.defaultModel) : '(unset — 不传 --model,mimo CLI 自决)',
873
+ exe: resolveMimoExePrefix(env).join(' '),
874
+ saved, saveError,
875
+ note: '默认模型在设置面板或本工具里指定(provider/model);mimo_run/mimo_continue 的 model 参数可单次覆盖。可用模型:在本机运行 `mimo models` 查看。'
876
+ }
877
+ }
878
+ }],
879
+
880
+ // 设置分区(契约 §3):keystone 读写/GUI;本处纯逻辑。diagnose/meta 必须同步。
881
+ settings: {
882
+ keys: ['defaultModel'],
883
+ defaults: () => ({ defaultModel: '' }),
884
+ normalize(raw) {
885
+ const src = raw && typeof raw === 'object' ? raw : {}
886
+ return { defaultModel: (typeof src.defaultModel === 'string' && src.defaultModel.trim()) ? src.defaultModel.trim() : '' }
887
+ },
888
+ meta(envArg) {
889
+ const e = envArg && typeof envArg === 'object' ? envArg : env
890
+ // 契约 §3.2:面板渲染**只读缓存**(命中就填 options,没命中留空),绝不 spawn。
891
+ // 探测改由用户点「拉取可用模型」显式触发(settings.refreshModels)。
892
+ const options = cachedMimoModels().map((m) => ({ value: m.id, label: m.id }))
893
+ return {
894
+ titleSuffix: 'MiMo',
895
+ fields: [{
896
+ key: 'defaultModel', label: '默认模型', type: 'select', allowCustom: true,
897
+ options: options,
898
+ refresh: { label: '拉取可用模型' },
899
+ placeholder: '留空 = 不传 --model(由 mimo CLI 自身默认决定)',
900
+ note: options.length
901
+ ? '共 ' + options.length + ' 个可用模型(上次手动拉取的结果,来自 `mimo models`)。点「拉取可用模型」可重新拉取;mimo_run/mimo_continue 的 model 参数可单次覆盖。'
902
+ : '还没拉取过可用模型清单 —— 点右侧「拉取可用模型」按钮拉取(面板不再自动探测,避免拖慢首屏)。拉取失败会显示具体原因(mimo CLI 未安装 / 未登录 / 免费 API 已终止)。仍可手动输入 provider/model,如 xiaomi/mimo-v2.6-flash。'
903
+ }]
904
+ }
905
+ },
906
+ /**
907
+ * 契约 §3.2:手动拉取可用模型(面板按钮 → POST /cli-bridge/settings/models)。
908
+ * 复用 fetchMimoModelsDetailed(同步探测包一层 async);成功写回缓存让 meta() 命中;
909
+ * 失败**必须** ok:false + 可操作原因,绝不返回 { ok:true, ids:[] } 或硬编码名称。
910
+ */
911
+ async refreshModels(settings, envArg) {
912
+ const e = envArg && typeof envArg === 'object' ? envArg : env
913
+ const r = fetchMimoModelsDetailed(e, 15000)
914
+ const source = 'mimo-models'
915
+ if (!r.ok) return { ok: false, ids: [], source: source, error: r.error }
916
+ rememberMimoModels(r.models)
917
+ const labels = Object.create(null)
918
+ for (const m of r.models) labels[m.id] = m.id
919
+ return { ok: true, ids: r.models.map((m) => String(m.id)), labels: labels, source: source }
920
+ },
921
+ // 同步一行诊断:当前模型 + exe 来源(env > 已知安装位置 > PATH)。
922
+ diagnose(settings, envArg) {
923
+ const s = settings && typeof settings === 'object' ? settings : {}
924
+ const e = envArg && typeof envArg === 'object' ? envArg : env
925
+ const model = s.defaultModel ? 'model=' + s.defaultModel : 'model=(未设置 → 不传 --model)'
926
+ if (e.MIMO_BIN || e.MIMO_CLI) return model + ' · bin=env:' + (e.MIMO_BIN || e.MIMO_CLI)
927
+ const found = mimoCandidateExePaths(e)
928
+ return model + ' · ' + (found.length ? 'bin=' + found[0] : 'bin=PATH:mimo')
929
+ }
930
+ },
931
+
932
+ // MCP 形态扩展:**不含 version**。历史:四处 `mcp: { version: '0.2.2' }` 与
933
+ // mcp/version.mjs 的 packageVersion() 双源,漏改一处就出现「统一 server 报 0.2.3、
934
+ // provider 面仍报 0.2.2」的自相矛盾输出;且 mcp/version.mjs 依赖 import.meta + node:fs,
935
+ // 其函数体无法随本文件内联进 dynamic 沙箱(build 文本拼接会丢实现/炸语法)。
936
+ // 定案:版本号单一来源 = mcp/version.mjs 的 packageVersion(),由 MCP 面(mcp/providers/
937
+ // mimo.mjs 的 meta.version)读取;描述符不复制 version 字面量,宿主如需注入可经 deps.mcp。
938
+ mcp: (deps.mcp && typeof deps.mcp === 'object') ? Object.assign({}, deps.mcp) : {}
939
+ }
940
+
941
+ // 统一分区写入(tmp→rename 原子写;与 keystone lib/settings.mjs 同文件同分区规则,
942
+ // 本工具自实现落盘 —— 不 import home-plugin;宿主给出 settingsPath 或 dshHome)。
943
+ async function writePartition(fsMod, pathMod, knownPath, home, environment, cur, patch) {
944
+ const file = knownPath || environment.CLI_BRIDGE_SETTINGS_FILE || (home ? pathMod.join(home, 'cli-bridge-settings.json') : null)
945
+ if (!file) throw new Error('dsh-home not resolved (set DSH_HOME or CLI_BRIDGE_SETTINGS_FILE)')
946
+ let all = {}
947
+ try { all = JSON.parse(fsMod.readFileSync(file, 'utf8')) } catch (e) { all = {} }
948
+ if (!all || typeof all !== 'object') all = {}
949
+ const next = Object.assign({}, all)
950
+ const part = Object.assign({}, cur, next.mimo || {})
951
+ if (patch && patch.defaultModel) part.defaultModel = patch.defaultModel
952
+ next.mimo = descriptor.settings.normalize(part)
953
+ next.updatedAt = Date.now()
954
+ try { fsMod.mkdirSync(pathMod.dirname(file), { recursive: true }) } catch (e) { }
955
+ const tmp = file + '.tmp-' + ((typeof process !== 'undefined' && process.pid) || 'mimo')
956
+ fsMod.writeFileSync(tmp, JSON.stringify(next, null, 2), 'utf8')
957
+ fsMod.renameSync(tmp, file)
958
+ return file
959
+ }
960
+
961
+ // 三形态共用的工具逻辑。签名保持不变(keystone 与队友按此接线)。
962
+ // 旧 mimo preset 语义:background 默认 true(仅显式 false 才前台阻塞)。
963
+ descriptor.runToolLogic = async function (args, exec) {
964
+ const a = args || {}
965
+ const normalized = a.background === undefined ? Object.assign({}, a, { background: true }) : a
966
+ return this.runner.coreExecute(normalized, exec)
967
+ }
968
+ descriptor.continueToolLogic = async function (args, exec) {
969
+ const a = args || {}
970
+ 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 }
971
+ if (a.sessionId) mapped.sessionId = a.sessionId
972
+ else if (a.latest) mapped.continueLatest = true
973
+ return this.runToolLogic(mapped, exec)
974
+ }
975
+ // 旧 statusTool.execute verbatim:cwd 过滤到单项目(Object.assign 形式与 opencode 一致)。
976
+ descriptor.statusToolLogic = async function (args) {
977
+ const a = args || {}
978
+ const snap = this.engine.statusSnapshot()
979
+ if (a.cwd) {
980
+ const key = String(a.cwd)
981
+ snap.projects = (snap.projects || []).filter((p) => p.cwd === key)
982
+ const g = snap.projects[0]
983
+ if (g) {
984
+ Object.assign(snap, {
985
+ state: g.state, running: g.running, current: g.current, trail: g.trail,
986
+ lastStatus: g.lastStatus, lastAt: g.lastAt, lastSessionId: g.lastSessionId,
987
+ lastOk: g.lastOk, lastFailed: g.lastFailed, fallbackActive: g.fallbackActive,
988
+ runs: g.runs, totalTokens: g.totalTokens, updatedAt: g.updatedAt
989
+ })
990
+ }
991
+ }
992
+ return snap
993
+ }
994
+ // 工厂装配(provider 无关骨架 + 描述符);宿主注入 getSettings 时把面板偏好带进 buildArgv。
995
+ descriptor.attach = function (hostOpts) {
996
+ host = hostOpts || {}
997
+ try {
998
+ if (typeof host.getSettings === 'function') {
999
+ const s = host.getSettings('mimo')
1000
+ if (s && typeof s === 'object') settingsRef = descriptor.settings.normalize(s)
1001
+ }
1002
+ } catch (e) { }
1003
+ this.engine = createStatusEngine({ provider: this, publish: host.publish })
1004
+ const runner = createRunner(Object.assign({}, host, { provider: this, engine: this.engine }))
1005
+ this.runner = runner
1006
+ return runner
1007
+ }
1008
+ // 测试/宿主可见的纯函数面(不进协议字段)。currentSettings/liveSettings/writePartition 是
1009
+ // 闭包函数,不依赖 attach(未 attach 时 host 为 null → 走 deps 注入或默认空值),测试可
1010
+ // 直接调用;writePartition 需要调用方自行传入可用的 fs/path 模块。
1011
+ descriptor._internals = { MIMO_LIMIT_RE, parseMimoJson, buildResult,
1012
+ mimoCandidateExePaths, resolveMimoExePrefix, parseMimoModels, fetchMimoModelsSync,
1013
+ fetchMimoModelsDetailed, cachedMimoModels, rememberMimoModels,
1014
+ currentSettings, liveSettings, writePartition
1015
+ }
1016
+ return descriptor
1017
+ }
1018
+
1019
+ // ── inlined from providers/mimo-pure.mjs ──
1020
+ // providers/mimo-pure.mjs — mimo 的纯逻辑片文件(无宿主依赖):事件解析 / 结果构造 +
1021
+ // 「怎么问本机 mimo 要模型清单」链路。
1022
+ //
1023
+ // 从 providers/mimo.mjs 拆出,让两个文件都留在 600 行红线内(规则 §26)。
1024
+ // build 会把 providers/mimo-*.mjs 全部内联进 dynamic 沙箱同一作用域,故主文件
1025
+ // 仍可直接调用这些函数;ESM 形态走本文件的 import。
1026
+ // 沙箱纪律:禁用 import.meta 字面量;fs/path/child_process 一律经
1027
+ // globalThis.process.getBuiltinModule + typeof 守卫读取(沙箱可能没有 node 能力)。
1028
+ // 注意:注释里绝不写出口令标记字面量(build 全局替换会把注释劈开)。
1029
+ // import { shortLabel, summarizeArgs, clampInt } from '../core/bridge-kit.mjs'
1030
+ // Node 内建探测(沙箱纪律:getBuiltinModule + typeof 守卫)。
1031
+ // 名字带 mimo 前缀:本文件会与 bridge-kit / core/scheduling.mjs【文本内联进同一个动态
1032
+ // 沙箱脚本作用域】,撞名即「Identifier already declared」构建错误。
1033
+ function mimoBuiltin(name) {
1034
+ try {
1035
+ const proc = globalThis.process
1036
+ return (proc && typeof proc.getBuiltinModule === 'function') ? proc.getBuiltinModule(name) : null
1037
+ } catch (e) { return null }
1038
+ }
1039
+
1040
+ // ── 可执行文件解析(env MIMO_BIN/MIMO_CLI > 已知安装位置 > PATH 名字)───────────
1041
+ // MiMoCode CLI 官方便携安装目录:<USERPROFILE|HOME>/.mimocode/bin/mimo[.exe]。
1042
+ // 沙箱内可能没有 fs —— 全部读取走 typeof 守卫,读不到就返回空数组(退回 PATH 查找)。
1043
+ const MIMO_EXE_DIR = ['.mimocode', 'bin']
1044
+
1045
+ /** 探测本机 mimo 可执行文件候选路径(官方便携安装位置);无 fs 能力时返回空数组。 */
1046
+ function mimoCandidateExePaths(env) {
1047
+ const e = env || {}
1048
+ const out = []
1049
+ const fs = mimoBuiltin('node:fs')
1050
+ if (!fs || typeof fs.existsSync !== 'function') return out
1051
+ const path = mimoBuiltin('node:path')
1052
+ const join = (path && typeof path.join === 'function') ? ((a, b) => path.join(a, b)) : ((a, b) => a + '/' + b)
1053
+ for (const k of ['USERPROFILE', 'HOME']) {
1054
+ if (!e[k]) continue
1055
+ const dir = join(join(String(e[k]), MIMO_EXE_DIR[0]), MIMO_EXE_DIR[1])
1056
+ for (const base of ['mimo.exe', 'mimo']) {
1057
+ const exe = join(dir, base)
1058
+ try { if (fs.existsSync(exe) && out.indexOf(exe) < 0) out.push(exe) } catch (err) { /* 探测失败静默跳过 */ }
1059
+ }
1060
+ }
1061
+ return out
1062
+ }
1063
+
1064
+ /**
1065
+ * 解析本次调用使用的 exe 前缀(数组或字符串)。
1066
+ * 优先级:binPath(显式路径)> env MIMO_BIN/MIMO_CLI > 已知安装位置 > PATH 名字。
1067
+ */
1068
+ function resolveMimoExePrefix(env, binPath) {
1069
+ const e = env || {}
1070
+ const bin = binPath || e.MIMO_BIN || e.MIMO_CLI || ''
1071
+ if (bin) {
1072
+ if (/\.(mjs|cjs|js)$/i.test(bin)) {
1073
+ const proc = globalThis.process
1074
+ return [(proc && proc.execPath) ? proc.execPath : 'node', bin]
1075
+ }
1076
+ return [String(bin)]
1077
+ }
1078
+ const found = mimoCandidateExePaths(e)
1079
+ return found.length ? [found[0]] : ['mimo']
1080
+ }
1081
+
1082
+ // ── 模型列表(`mimo models`;设置面板下拉用)───────────────────────────────────
1083
+ // 实测输出形如每行 `provider/model — window 1M, compacts at 900K`(分隔符是中文破折号
1084
+ // U+2014)。模型名自身含连字符(xiaomi/mimo-v2.6-pro-ultraspeed),所以只认「行首的
1085
+ // provider/model」,其后一律当描述文本;表头/说明/空行一律丢弃,绝不抛。
1086
+ const MIMO_MODEL_ID_RE = /^([A-Za-z0-9_.-]+\/[A-Za-z0-9_.@#+-]+)(?=$|[\s—–:|,])/
1087
+
1088
+ /** 解析 `mimo models` 文本输出为 [{id, provider, name, note}](去重、保序)。 */
1089
+ function parseMimoModels(text) {
1090
+ const out = []
1091
+ const seen = Object.create(null)
1092
+ for (const raw of String(text || '').split(/\r?\n/)) {
1093
+ // 去掉列表记号(• * - 1. 等)与行首空白,避免把记号当 provider 名。
1094
+ const ln = raw.replace(/^[^A-Za-z0-9]+/, '').trim()
1095
+ if (!ln) continue
1096
+ const m = ln.match(MIMO_MODEL_ID_RE)
1097
+ if (!m) continue
1098
+ const id = m[1]
1099
+ if (seen[id]) continue
1100
+ seen[id] = true
1101
+ const slash = id.indexOf('/')
1102
+ out.push({ id: id, provider: id.slice(0, slash), name: id.slice(slash + 1), note: ln.slice(id.length).replace(/^[\s—–:|,-]+/, '').trim() })
1103
+ }
1104
+ return out
1105
+ }
1106
+
1107
+ // 诊断文本压成一行(CLI 的 stderr/stdout 常带换行与表格)。
1108
+ function mimoOneLine(text, max) {
1109
+ const s = String(text == null ? '' : text).replace(/\s+/g, ' ').trim()
1110
+ const cap = max || 200
1111
+ return s.length > cap ? s.slice(0, cap) + '…' : s
1112
+ }
1113
+
1114
+ /**
1115
+ * 真正跑一次 `mimo models`,**带可操作失败原因**(契约 §3.2:失败必须 ok:false)。
1116
+ *
1117
+ * 为什么不沿用「失败返回空数组」:空数组区分不了「没装 CLI」「没登录 / 免费 API 已终止」
1118
+ * 「超时」「输出格式变了」,而面板与用户下一步动作完全不同。
1119
+ * @returns {{ok:boolean, models:Array, exe:string, error?:string}}
1120
+ */
1121
+ function fetchMimoModelsDetailed(env, timeoutMs) {
1122
+ const prefix = resolveMimoExePrefix(env)
1123
+ const exe = prefix.join(' ')
1124
+ const child = mimoBuiltin('node:child_process')
1125
+ if (!child || typeof child.spawnSync !== 'function') {
1126
+ return { ok: false, models: [], exe: exe, error: '当前宿主没有子进程能力(spawnSync 不可用)—— 请在有 Node 子进程能力的形态(DSH 主进程 / MCP 进程)里点「拉取可用模型」' }
1127
+ }
1128
+ let r = null
1129
+ try {
1130
+ r = child.spawnSync(prefix[0], prefix.slice(1).concat(['models']), {
1131
+ encoding: 'utf8', timeout: clampInt(timeoutMs, 20000, 2000, 60000),
1132
+ windowsHide: true, stdio: ['ignore', 'pipe', 'pipe']
1133
+ })
1134
+ } catch (e) {
1135
+ return { ok: false, models: [], exe: exe, error: '启动 `' + exe + ' models` 失败:' + mimoOneLine(e && e.message || e) }
1136
+ }
1137
+ if (!r) return { ok: false, models: [], exe: exe, error: '`' + exe + ' models` 没有任何返回(spawnSync 返回 null),请确认可执行文件路径' }
1138
+ if (r.error) {
1139
+ const code = String(r.error.code || '')
1140
+ if (code === 'ENOENT') {
1141
+ return { ok: false, models: [], exe: exe, error: '未找到 mimo 可执行文件(' + exe + ')—— 请安装 MiMoCode CLI(官方便携安装位置 ~/.mimocode/bin/mimo.exe),或设置 MIMO_BIN 指向其绝对路径' }
1142
+ }
1143
+ if (code === 'ETIMEDOUT' || code === 'ABORT_ERR') {
1144
+ return { ok: false, models: [], exe: exe, error: '`' + exe + ' models` 超时 —— 网络或鉴权可能卡住:请先在终端手动跑一次 `mimo models` 确认,必要时执行 `mimo auth login`' }
1145
+ }
1146
+ return { ok: false, models: [], exe: exe, error: '`' + exe + ' models` 启动失败(' + (code || 'unknown') + '):' + mimoOneLine(r.error.message || r.error) }
1147
+ }
1148
+ const stdout = String(r.stdout || '')
1149
+ const stderr = String(r.stderr || '')
1150
+ const models = parseMimoModels(stdout)
1151
+ if (models.length) return { ok: true, models: models, exe: exe }
1152
+ if (r.status !== 0) {
1153
+ return { ok: false, models: [], exe: exe, error: '`mimo models` 退出码 ' + r.status + ':' + mimoOneLine(stderr || stdout) + ' —— 若提示未登录 / 免费 API 已终止,请先在终端执行 `mimo auth login`' }
1154
+ }
1155
+ return { ok: false, models: [], exe: exe, error: '`mimo models` 成功退出但没有解析到任何 provider/model 行' + (stdout ? '(输出开头:' + mimoOneLine(stdout, 160) + ')' : '(输出为空)') + ' —— 请先在终端手动跑一次 `mimo models` 确认输出格式与登录状态' }
1156
+ }
1157
+
1158
+ /** 兼容薄封装:失败返回空数组(旧语义,供不关心原因的调用方)。 */
1159
+ function fetchMimoModelsSync(env, timeoutMs) {
1160
+ const d = fetchMimoModelsDetailed(env, timeoutMs)
1161
+ return d.ok ? d.models : []
1162
+ }
1163
+
1164
+ // ── 手动拉取缓存(契约 §3.2)─────────────────────────────────────────────────
1165
+ // `meta()` 每次面板渲染都会被调用:**只读本缓存,绝不 spawn**。缓存只存在进程内存里
1166
+ // (宿主进程重启即失效,不会把过时清单落盘成"配置");用户点按钮才刷新它。
1167
+ const MIMO_MODEL_CACHE_TTL_MS = 6 * 60 * 60 * 1000
1168
+ const MIMO_MODEL_CACHE = { at: 0, models: [] }
1169
+
1170
+ /** 读缓存(未拉过 / 已过期 → 空数组,面板据此提示「点按钮拉取」)。 */
1171
+ function cachedMimoModels(nowMs) {
1172
+ const at = Number(nowMs) || Date.now()
1173
+ if (!MIMO_MODEL_CACHE.models.length) return []
1174
+ if (at - MIMO_MODEL_CACHE.at >= MIMO_MODEL_CACHE_TTL_MS) return []
1175
+ return MIMO_MODEL_CACHE.models.slice()
1176
+ }
1177
+
1178
+ /** 手动拉取成功后回填缓存;空清单绝不入缓存(否则下次渲染"有缓存但没内容")。 */
1179
+ function rememberMimoModels(models, nowMs) {
1180
+ if (!Array.isArray(models) || !models.length) return false
1181
+ MIMO_MODEL_CACHE.models = models.slice()
1182
+ MIMO_MODEL_CACHE.at = Number(nowMs) || Date.now()
1183
+ return true
1184
+ }
1185
+
1186
+ function parseMimoJson(stdoutText) {
1187
+ const trimmed = String(stdoutText || '').trim()
1188
+ if (!trimmed) return null
1189
+ const events = []
1190
+ const lines = trimmed.split(/\r?\n/)
1191
+ for (const raw of lines) {
1192
+ const ln = raw.trim()
1193
+ if (!ln.startsWith('{')) continue
1194
+ try {
1195
+ const obj = JSON.parse(ln)
1196
+ if (obj && typeof obj === 'object') events.push(obj)
1197
+ } catch (e) { /* tolerate log lines */ }
1198
+ }
1199
+ if (!events.length) {
1200
+ try {
1201
+ const obj = JSON.parse(trimmed)
1202
+ if (obj && typeof obj === 'object') events.push(obj)
1203
+ } catch (e) {
1204
+ return { events: [], text: trimmed, sessionId: null, error: null, steps: [] }
1205
+ }
1206
+ }
1207
+ let sessionId = null
1208
+ let text = ''
1209
+ let errorMessage = null
1210
+ let errorName = null
1211
+ let numTurns = 0
1212
+ let totalTokens = null
1213
+ const steps = []
1214
+ const textParts = []
1215
+ for (const ev of events) {
1216
+ if (ev.sessionID && typeof ev.sessionID === 'string') sessionId = ev.sessionID
1217
+ if (ev.sessionId && typeof ev.sessionId === 'string') sessionId = ev.sessionId
1218
+ if (ev.session && typeof ev.session === 'object' && ev.session.id) sessionId = ev.session.id
1219
+ if (ev.type === 'session' && ev.properties && ev.properties.id) sessionId = ev.properties.id
1220
+ const t = ev.type
1221
+ const p = ev.properties || ev
1222
+ if (t === 'error') {
1223
+ const err = ev.error || p
1224
+ errorMessage = (err.data && err.data.message) || err.message || err.name || 'error'
1225
+ errorName = err.name || 'Error'
1226
+ } else if (t === 'text' || t === 'message' || t === 'assistant' || t === 'output') {
1227
+ const chunk = p.text || p.content || p.message || p.output || (typeof ev.text === 'string' ? ev.text : '')
1228
+ if (typeof chunk === 'string' && chunk) {
1229
+ textParts.push(chunk)
1230
+ steps.push({ at: Date.now(), kind: 'text', label: shortLabel(chunk) })
1231
+ }
1232
+ } else if (t === 'step-start' || t === 'step-finish' || t === 'step_update') {
1233
+ numTurns += 1
1234
+ if (p.tokens && typeof p.tokens === 'object') {
1235
+ const tt = p.tokens.total ?? p.tokens.input ?? p.tokens.output
1236
+ if (typeof tt === 'number') totalTokens = (totalTokens || 0) + tt
1237
+ }
1238
+ if (t === 'step_update') {
1239
+ const name = p.tool_name || p.tool || p.name
1240
+ if (name) steps.push({ at: Date.now(), kind: 'tool', label: shortLabel(name + ' ' + JSON.stringify(summarizeArgs(p.arguments || p.args || p.input) || {})) })
1241
+ }
1242
+ } else if (t === 'tool' || t === 'tool-call' || t === 'tool_use') {
1243
+ const name = p.tool || p.name || p.tool_name || 'tool'
1244
+ steps.push({ at: Date.now(), kind: 'tool', label: shortLabel(name + ' ' + JSON.stringify(summarizeArgs(p.arguments || p.args || p.input) || {})) })
1245
+ }
1246
+ }
1247
+ if (textParts.length) text = textParts.join('\n')
1248
+ else {
1249
+ const last = events[events.length - 1] || {}
1250
+ const r = last.result || last
1251
+ if (typeof r.response === 'string') text = r.response
1252
+ else if (typeof r.text === 'string') text = r.text
1253
+ }
1254
+ return { events, text, sessionId, error: errorMessage, errorName, numTurns, totalTokens, steps }
1255
+ }
1256
+
1257
+ function buildResult(parsed, outcome, mode, stderrText, stdoutText, backend) {
1258
+ const MAX_TRAIL = 12
1259
+ const exitCode = outcome ? outcome.exitCode : null
1260
+ const errText = (parsed && parsed.error) ? String(parsed.error) : ''
1261
+ const stderr = (stderrText ? String(stderrText).slice(-2000) : '') + (errText ? (stderrText ? ' ' : '') + errText : '')
1262
+ const text = parsed && typeof parsed.text === 'string' ? parsed.text : ''
1263
+ const failed = exitCode !== 0 || !!(parsed && parsed.error)
1264
+ const be = (parsed && parsed.backend) || (outcome && outcome.backend) || backend || null
1265
+ if (parsed) {
1266
+ return {
1267
+ ok: !failed && (!!text || parsed.accepted === true),
1268
+ status: failed ? (parsed.errorName || 'ERROR') : (parsed.accepted && !text ? 'ACCEPTED' : (text ? 'SUCCESS' : 'EMPTY')),
1269
+ response: text,
1270
+ sessionId: parsed.sessionId || null,
1271
+ durationSeconds: null,
1272
+ numTurns: parsed.numTurns || null,
1273
+ totalTokens: parsed.totalTokens || null,
1274
+ exitCode, mode, backend: be,
1275
+ stderr,
1276
+ errorMessage: parsed.error || '',
1277
+ accepted: parsed.accepted === true,
1278
+ steps: Array.isArray(parsed.steps) ? parsed.steps.slice(-MAX_TRAIL) : []
1279
+ }
1280
+ }
1281
+ return {
1282
+ ok: false, status: 'PARSE_ERROR', response: '', sessionId: null,
1283
+ durationSeconds: null, numTurns: null, totalTokens: null,
1284
+ exitCode, mode, backend: be, stderr, errorMessage: '',
1285
+ rawStdout: String(stdoutText || '').slice(-2000)
1286
+ }
1287
+ }
1082
1288
 
1083
1289
  // core/scheduling.mjs — CLI 调度(启用集合 + 调用优先级)单源纯逻辑。
1084
1290
  //