@ljwei-stak/dsh-model-router 0.16.4 → 0.17.1

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.
@@ -35,8 +35,12 @@ import {
35
35
  OFFICIAL_TOOLS,
36
36
  getOfficialTool,
37
37
  installCommandLine,
38
+ installCommandLineFor,
39
+ installMethodFor,
40
+ installMethodsFor,
38
41
  toolForProvider,
39
42
  } from './shared/official-tool-registry.mjs'
43
+ import { parseInstallPreferences, resolveInstallPlan } from './shared/tool-install-preferences.mjs'
40
44
  import { officialToolExecutionCapabilities, officialToolReadiness, runOfficialTool } from './shared/official-tool-executor.mjs'
41
45
  import { runOfficialTask, runOfficialTeam, sessionWorkspace } from './shared/official-team-runtime.mjs'
42
46
  import { downstreamPackageIds, executeAssignmentPlan, rerunAssignmentPackage, taskTextProblem } from './shared/task-executors.mjs'
@@ -44,11 +48,17 @@ import {
44
48
  probeAllTools,
45
49
  probeToolWith,
46
50
  startInstall,
51
+ startRepair,
52
+ startUninstall,
47
53
  cancelInstall,
54
+ currentInstallOptions,
48
55
  installStatus,
49
56
  installedToolIds,
50
57
  defaultRunner,
51
58
  ensureNpmPrefixOnPath,
59
+ installPreferences,
60
+ planCommandLine,
61
+ setInstallPreferences,
52
62
  } from './shared/official-tools-runtime.mjs'
53
63
 
54
64
  // Cordis plugin name, matching the profile entry id; unchanged by the 0.13.0 package rename.
@@ -79,6 +89,12 @@ export const Config = z.object({
79
89
  subscriptionCooldownMinutes: z.number().step(1).min(1).max(10_080).default(DEFAULT_COOLDOWN_MINUTES).volatile(),
80
90
  quotaPatternsJson: z.string().max(8_000).default('{}').volatile(),
81
91
  onSubscriptionFailure: z.union(['ask', 'api', 'fail']).default('ask').volatile(),
92
+ // 官方工具安装设置:统一安装目录 + 下载源。两者都只在本机生效,不会随计划上传。
93
+ toolInstallDir: z.string().max(4_096).default('').volatile(),
94
+ toolNpmRegistry: z.string().max(2_048).default('https://registry.npmjs.org/').volatile(),
95
+ toolInstallMethodsJson: z.string().max(8_000).default('{}').volatile(),
96
+ toolScriptUrlsJson: z.string().max(8_000).default('{}').volatile(),
97
+ toolAllowScriptInstall: z.boolean().default(true).volatile(),
82
98
  })
83
99
 
84
100
  const JSON_OUTPUT = {
@@ -1417,6 +1433,12 @@ export function routerRemoteServices(ctx, config, { terminals = null } = {}) {
1417
1433
 
1418
1434
  /** Register model-facing tools and the human /router command. */
1419
1435
  export function apply(ctx, config = {}) {
1436
+ // Install addresses (directory, npm source, per-tool method and script source)
1437
+ // live in volatile config, so re-apply picks up whatever the user last saved.
1438
+ // A malformed value is reported here rather than at the first click.
1439
+ let installPreferenceError = null
1440
+ try { setInstallPreferences(parseInstallPreferences(config, { tools: OFFICIAL_TOOLS })) }
1441
+ catch (error) { installPreferenceError = errorText(error) }
1420
1442
  // The official Host injects typert; direct lightweight uses of apply may
1421
1443
  // supply only the model/command services and do not expose the Desktop RPC.
1422
1444
  if (ctx.typert) {
@@ -1436,22 +1458,35 @@ export function apply(ctx, config = {}) {
1436
1458
  return { kind: 'ask', reason: 'Confirm the user\'s subjective rating; agents must not self-rate their results',
1437
1459
  displayReason: { en: 'Save this as your rating and adjust future recommendations?', zh: '确认这是你对该结果的评价,并用于调整以后的推荐?Agent 不能代替你给自己评分。' } }
1438
1460
  }
1439
- if (exec.name === 'model_router_tool_install') {
1461
+ if (exec.name === 'model_router_tool_install' || exec.name === 'model_router_tool_uninstall' || exec.name === 'model_router_tool_repair') {
1440
1462
  const requested = getOfficialTool(text(exec.arguments?.tool))
1441
1463
  const label = requested?.label ?? '官方 CLI'
1464
+ const method = installMethodFor(requested, text(exec.arguments?.method))
1465
+ const how = method ? `(${method.label})` : ''
1442
1466
  const desktopInstaller = requested?.manager === 'signed-windows-installer'
1467
+ const uninstalling = exec.name === 'model_router_tool_uninstall'
1468
+ const repairing = exec.name === 'model_router_tool_repair'
1469
+ const verb = uninstalling ? 'Uninstall' : repairing ? 'Reinstall to repair' : 'Install'
1443
1470
  return {
1444
1471
  kind: 'ask',
1445
1472
  reason: desktopInstaller
1446
1473
  ? `Download and open the verified official ${label} desktop installer`
1447
- : `Install ${label} globally with the plugin's fixed official command`,
1474
+ : `${verb} ${label}${how} on this machine`,
1448
1475
  displayReason: {
1449
1476
  en: desktopInstaller
1450
1477
  ? `Download and open the verified ${label} installer? You can select the installation directory in its window.`
1451
- : `Install ${label} globally using the fixed official package?`,
1478
+ : uninstalling
1479
+ ? `Uninstall ${label}${how}? Its settings, login and history are kept; only the program is removed.`
1480
+ : repairing
1481
+ ? `Reinstall ${label}${how} to repair a broken install?`
1482
+ : `Install ${label}${how} using the plugin's fixed official source?`,
1452
1483
  zh: desktopInstaller
1453
1484
  ? `下载并打开已验签的 ${label} 安装器?安装窗口中可选择非 C 盘目录。`
1454
- : `使用插件固定的官方软件包,在本机全局安装 ${label}?`,
1485
+ : uninstalling
1486
+ ? `卸载 ${label}${how}?配置、登录信息和历史记录都会保留,只删除程序本身。`
1487
+ : repairing
1488
+ ? `重新安装 ${label}${how} 以修复损坏的安装?`
1489
+ : `使用插件固定的官方来源,在本机安装 ${label}${how}?`,
1455
1490
  },
1456
1491
  }
1457
1492
  }
@@ -1524,49 +1559,76 @@ export function apply(ctx, config = {}) {
1524
1559
  })
1525
1560
  ctx.commands.register({
1526
1561
  name: 'tools',
1527
- description: 'Show official model CLI install status, or install one registry tool.',
1528
- input: { hint: '留空查看状态;或输入 install/cancel <工具id>' },
1562
+ description: 'Show official model CLI install status, or install/uninstall/repair one registry tool.',
1563
+ input: { hint: '留空查看状态;或输入 install/cancel/uninstall/repair/method <工具id> [安装方式]' },
1529
1564
  async handler({ rawInput, signal }) {
1530
1565
  const input = text(rawInput)
1531
- const installMatch = input.match(/^install\s+([A-Za-z0-9_-]+)$/i)
1532
- const cancelMatch = input.match(/^cancel\s+([A-Za-z0-9_-]+)$/i)
1533
- if (cancelMatch) {
1534
- try { return { kind: 'success', text: `已请求取消 ${cancelInstall(cancelMatch[1]).tool} 的安装;请使用 /tools 查看最新状态。` } }
1566
+ const matched = input.match(/^(install|uninstall|repair|cancel|method)\s+([A-Za-z0-9_-]+)(?:\s+([A-Za-z0-9-]+))?$/i)
1567
+ if (!matched && input) {
1568
+ return { kind: 'error', text: '用法:/tools 查看状态,或 /tools install/uninstall/repair/cancel/method <工具id> [安装方式]' }
1569
+ }
1570
+ if (matched && matched[1].toLowerCase() === 'cancel') {
1571
+ try { return { kind: 'success', text: `已请求取消 ${cancelInstall(matched[2]).tool} 的安装;请使用 /tools 查看最新状态。` } }
1535
1572
  catch (error) { return { kind: 'error', text: `无法取消安装:${errorText(error)}` } }
1536
1573
  }
1537
- if (!installMatch) {
1538
- if (input) return { kind: 'error', text: '用法:/tools 查看状态,或 /tools install/cancel <工具id>' }
1574
+ if (matched && matched[1].toLowerCase() === 'method') {
1575
+ // Printing the methods is a read-only answer; persisting one is a config
1576
+ // change and stays in the settings form / workbench panel.
1577
+ const tool = getOfficialTool(matched[2])
1578
+ if (!tool) return { kind: 'error', text: `未知工具:${matched[2]}。` }
1579
+ const lines = installMethodsFor(tool).map(method => `• ${method.id}:${method.label}`)
1580
+ return {
1581
+ kind: 'success',
1582
+ text: lines.length
1583
+ ? `${tool.label} 可用的安装方式:\n${lines.join('\n')}\n在“模型路由工作台 → 官方工具”里切换,或在插件设置里固定默认方式。`
1584
+ : `${tool.label} 没有可切换的安装方式(${tool.manager === 'signed-windows-installer' ? '官方签名桌面安装器' : '不支持'})。`,
1585
+ }
1586
+ }
1587
+ if (!matched) {
1539
1588
  try {
1540
1589
  const probes = await probeAllTools({ fresh: true })
1541
1590
  const lines = probes.map(probe => {
1542
1591
  const tool = getOfficialTool(probe.id)
1543
- const command = installCommandLine(tool)
1592
+ let command = null
1593
+ try { command = planCommandLine(resolveInstallPlan(tool, undefined, installPreferences(), currentInstallOptions())) }
1594
+ catch { command = installCommandLine(tool) }
1544
1595
  const status = probe.status === 'installed'
1545
1596
  ? `已安装 ${probe.version ?? ''}`
1546
1597
  : probe.status === 'unsupported'
1547
1598
  ? '不支持一键安装'
1548
1599
  : '未安装'
1549
- return `• ${tool.label}(${probe.id}):${status}${command && probe.status === 'not-installed' ? `\n 安装:/tools install ${probe.id}(即 ${command})` : ''}`
1600
+ const methods = installMethodsFor(tool).map(method => method.id).join(' / ')
1601
+ return `• ${tool.label}(${probe.id}):${status}${methods ? `[${methods}]` : ''}`
1602
+ + `${command && probe.status === 'not-installed' ? `\n 安装:/tools install ${probe.id}(即 ${command})` : ''}`
1550
1603
  })
1551
- return { kind: 'success', text: `官方工具状态:\n${lines.join('\n')}` }
1604
+ const header = installPreferenceError ? `⚠ 官方工具安装设置无效:${installPreferenceError}\n` : ''
1605
+ return { kind: 'success', text: `${header}官方工具状态:\n${lines.join('\n')}` }
1552
1606
  } catch (error) {
1553
1607
  return { kind: 'error', text: `探测失败:${errorText(error)}` }
1554
1608
  }
1555
1609
  }
1610
+ const action = matched[1].toLowerCase()
1611
+ const toolId = matched[2]
1612
+ const method = matched[3]
1556
1613
  try {
1557
- const job = startInstall(installMatch[1])
1558
- const tool = getOfficialTool(installMatch[1])
1614
+ const job = action === 'uninstall' ? startUninstall(toolId, { method })
1615
+ : action === 'repair' ? startRepair(toolId, { method })
1616
+ : startInstall(toolId, { method })
1617
+ const tool = getOfficialTool(toolId)
1559
1618
  const settled = await waitForInstall(job.tool, signal)
1619
+ const done = { uninstall: '卸载完成', repair: '修复完成', install: '安装完成' }[action]
1560
1620
  if (settled.status === 'succeeded') {
1561
- const probe = await probeToolWith(tool, defaultRunner)
1562
- return { kind: 'success', text: `${tool.label} 安装完成${probe.version ? `,探测版本 ${probe.version}` : ''}。` }
1621
+ const probe = await probeToolWith(tool, defaultRunner, { cacheMs: 0 })
1622
+ const suffix = probe.version ? `,探测版本 ${probe.version}` : ''
1623
+ if (settled.error) return { kind: 'success', text: `${tool.label} ${done}${suffix}。${settled.error}` }
1624
+ return { kind: 'success', text: `${tool.label} ${done}${suffix}。` }
1563
1625
  }
1564
1626
  if (settled.status === 'installer-opened') {
1565
1627
  return { kind: 'success', text: `${tool.label} 官方安装器已验证并打开。请在安装窗口选择非 C 盘目录并完成安装,然后运行 /tools 重新检测;当前尚未确认安装完成。` }
1566
1628
  }
1567
- return { kind: 'error', text: `${tool.label} 安装失败:${settled.error ?? '未知原因'}\n${settled.outputTail.slice(-6).join('\n')}` }
1629
+ return { kind: 'error', text: `${tool.label} ${action === 'uninstall' ? '卸载' : action === 'repair' ? '修复' : '安装'}失败:${settled.error ?? '未知原因'}\n${settled.outputTail.slice(-6).join('\n')}` }
1568
1630
  } catch (error) {
1569
- return { kind: 'error', text: `无法开始安装:${errorText(error)}` }
1631
+ return { kind: 'error', text: `无法开始${action === 'uninstall' ? '卸载' : '安装'}:${errorText(error)}` }
1570
1632
  }
1571
1633
  },
1572
1634
  })
@@ -1590,7 +1652,7 @@ async function waitForInstall(toolId, signal) {
1590
1652
  function registerOfficialToolModels(ctx, config) {
1591
1653
  ctx.tools.register(defineTool({
1592
1654
  name: 'model_router_tools',
1593
- description: 'Probe the fixed registry of official model tools (Kimi Code, Claude Code, Codex, MiniMax Code, MiMo Code, Grok Build, ZCode, Gemini CLI) and report which are installed with their versions. Never reads credentials.',
1655
+ description: 'Probe the fixed registry of official model tools (Kimi Code, Claude Code, Codex, MiniMax Code, MiMo Code, Grok Build, Gemini CLI, OpenCode, Step Code, ZCode) and report which are installed with their versions, plus each tool\'s available install methods and the effective install directory and npm source. Never reads credentials.',
1594
1656
  parameters: {},
1595
1657
  output: JSON_OUTPUT,
1596
1658
  async execute(_args, exec) {
@@ -1602,30 +1664,74 @@ function registerOfficialToolModels(ctx, config) {
1602
1664
  executionReadiness: await Promise.all(probes.map(probe => probe.installed
1603
1665
  ? officialToolReadiness(probe.id)
1604
1666
  : Promise.resolve({ id: probe.id, ready: false, reason: 'CLI 尚未安装或版本检测失败。' }))),
1605
- installHint: '未安装的工具可由 model_router_tool_install 按注册表固定命令安装;版本与包名不接受自定义。',
1667
+ installMethods: Object.fromEntries(OFFICIAL_TOOLS.map(tool => [tool.id, installMethodsFor(tool).map(method => ({
1668
+ id: method.id, label: method.label, kind: method.kind,
1669
+ }))])),
1670
+ installHint: '未安装的工具可由 model_router_tool_install 按注册表固定命令安装,损坏的用 model_router_tool_repair 修复,多余的用 model_router_tool_uninstall 卸载;安装目录与下载源来自用户设置,版本与包名不接受自定义。',
1606
1671
  })
1607
1672
  },
1608
1673
  }))
1609
1674
  ctx.tools.register(defineTool({
1610
1675
  name: 'model_router_tool_install',
1611
- description: 'Install one official model tool by registry id from its fixed official source at the vendor latest release. ZCode opens the latest publisher-signed interactive desktop installer with a directory picker. Only registry ids are accepted; arbitrary packages or executables are refused.',
1676
+ description: 'Install one official model tool by registry id from its fixed official source at the vendor latest release, using one of that tool\'s declared install methods (npm, pnpm, or the vendor installer script). ZCode opens the latest publisher-signed interactive desktop installer with a directory picker. Only registry ids and declared method ids are accepted; arbitrary packages, URLs or executables are refused. The install directory and download sources come from the user\'s own plugin settings.',
1612
1677
  parameters: {
1613
1678
  tool: { type: 'string', required: true, description: 'Registry tool id, e.g. kimi-code.' },
1679
+ method: { type: 'string', description: 'Install method id for this tool, e.g. npm, pnpm, script-bash or script-powershell. Omit to use the user\'s configured default.' },
1614
1680
  },
1615
1681
  output: JSON_OUTPUT,
1616
1682
  async execute(args, exec) {
1617
- const job = startInstall(args.tool)
1683
+ const job = startInstall(args.tool, { method: text(args.method) || undefined })
1618
1684
  const settled = await waitForInstall(job.tool, exec.signal)
1619
1685
  const tool = getOfficialTool(args.tool)
1620
1686
  const probe = settled.status === 'succeeded'
1621
- ? await probeToolWith(tool, defaultRunner)
1687
+ ? await probeToolWith(tool, defaultRunner, { cacheMs: 0 })
1622
1688
  : null
1623
1689
  return jsonValue({
1624
1690
  ...settled,
1625
1691
  postInstallProbe: probe,
1626
1692
  notice: settled.status === 'installer-opened'
1627
1693
  ? '已打开 ZCode 官方安装窗口,请选择安装目录并完成安装,之后重新检测;此状态不代表安装完成。'
1628
- : '安装命令完全来自服务端注册表;实际版本以探测横幅为准。',
1694
+ : '安装命令完全来自服务端注册表与用户设置;实际版本以探测横幅为准。',
1695
+ })
1696
+ },
1697
+ }))
1698
+ ctx.tools.register(defineTool({
1699
+ name: 'model_router_tool_repair',
1700
+ description: 'Reinstall one official model tool by registry id, even when it already reports the latest version, to repair a broken or incomplete install. Settings and login are kept. Only registry ids and declared method ids are accepted.',
1701
+ parameters: {
1702
+ tool: { type: 'string', required: true, description: 'Registry tool id, e.g. codex.' },
1703
+ method: { type: 'string', description: 'Install method id for this tool. Omit to use the user\'s configured default.' },
1704
+ },
1705
+ output: JSON_OUTPUT,
1706
+ async execute(args, exec) {
1707
+ const job = startRepair(args.tool, { method: text(args.method) || undefined })
1708
+ const settled = await waitForInstall(job.tool, exec.signal)
1709
+ const tool = getOfficialTool(args.tool)
1710
+ const probe = settled.status === 'succeeded'
1711
+ ? await probeToolWith(tool, defaultRunner, { cacheMs: 0 })
1712
+ : null
1713
+ return jsonValue({
1714
+ ...settled,
1715
+ postInstallProbe: probe,
1716
+ notice: '修复会重新执行同一条固定官方安装命令;工具的配置与登录信息不受影响。',
1717
+ })
1718
+ },
1719
+ }))
1720
+ ctx.tools.register(defineTool({
1721
+ name: 'model_router_tool_uninstall',
1722
+ description: 'Remove one official model tool by registry id. Package-manager installs are removed with the same manager; vendor-script installs delete only the executable the vendor installer placed, plus that install\'s own PATH entry in the shell profile. Configuration, credentials and session history are always kept. Only registry ids and declared method ids are accepted.',
1723
+ parameters: {
1724
+ tool: { type: 'string', required: true, description: 'Registry tool id, e.g. opencode.' },
1725
+ method: { type: 'string', description: 'The install method it was installed with, so the matching removal runs. Omit to use the user\'s configured default.' },
1726
+ },
1727
+ output: JSON_OUTPUT,
1728
+ async execute(args, exec) {
1729
+ const job = startUninstall(args.tool, { method: text(args.method) || undefined })
1730
+ const settled = await waitForInstall(job.tool, exec.signal)
1731
+ const tool = getOfficialTool(args.tool)
1732
+ return jsonValue({
1733
+ ...settled,
1734
+ notice: '卸载只删除程序本身;工具的配置、登录信息和历史记录保留,重新安装后仍在。',
1629
1735
  })
1630
1736
  },
1631
1737
  }))
@@ -1,16 +1,22 @@
1
1
  /** Host receiver for the official desktop's typed one-click installer RPC. */
2
2
  import { readFileSync } from 'node:fs'
3
3
  import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
4
- import { getOfficialTool } from './shared/official-tool-registry.mjs'
4
+ import { getOfficialTool, installMethodBlockReason, OFFICIAL_TOOLS } from './shared/official-tool-registry.mjs'
5
+ import { resolveInstallPlan, resolveUninstallPlan } from './shared/tool-install-preferences.mjs'
5
6
  import { officialToolExecutionCapabilities, officialToolReadiness } from './shared/official-tool-executor.mjs'
6
7
  import {
7
- defaultRunner,
8
8
  cancelInstall,
9
+ currentInstallOptions,
10
+ defaultRunner,
9
11
  ensureNpmPrefixOnPath,
12
+ installPreferences,
10
13
  installStatus,
14
+ planCommandLine,
11
15
  probeAllTools,
12
16
  probeToolWith,
13
17
  startInstall,
18
+ startRepair,
19
+ startUninstall,
14
20
  } from './shared/official-tools-runtime.mjs'
15
21
  import {
16
22
  OFFICIAL_TOOLS_HOST_TYPERT,
@@ -29,6 +35,49 @@ function errorText(error) {
29
35
 
30
36
  const unavailable = () => { throw new Error('模型路由工作台服务尚未加载。') }
31
37
 
38
+ /**
39
+ * What the panel needs to render the install controls: the effective settings
40
+ * and, per tool, every method with the reason it is unavailable here plus the
41
+ * command line that would actually run. The panel shows this before the user
42
+ * clicks, so an install is never a surprise command.
43
+ */
44
+ function installSummary() {
45
+ const preferences = installPreferences()
46
+ return {
47
+ installDir: preferences.installDir ?? '',
48
+ registry: preferences.registry ?? '',
49
+ allowScriptInstall: preferences.allowScriptInstall !== false,
50
+ scriptUrls: { ...(preferences.scriptUrls ?? {}) },
51
+ methods: { ...(preferences.methods ?? {}) },
52
+ tools: OFFICIAL_TOOLS.map(tool => {
53
+ const entry = {
54
+ id: tool.id,
55
+ methods: [],
56
+ command: null,
57
+ uninstallCommand: null,
58
+ methodId: null,
59
+ // What the user pinned, which may differ from the effective method when
60
+ // the pinned one does not run on this platform.
61
+ savedMethodId: preferences.methods?.[tool.id] ?? null,
62
+ notices: [],
63
+ }
64
+ for (const method of tool.installMethods ?? []) {
65
+ entry.methods.push({ id: method.id, label: method.label, kind: method.kind, blocked: installMethodBlockReason(method) })
66
+ }
67
+ try {
68
+ const plan = resolveInstallPlan(tool, undefined, preferences, currentInstallOptions())
69
+ entry.methodId = plan.methodId
70
+ entry.command = planCommandLine(plan)
71
+ entry.notices = plan.notices ?? []
72
+ entry.uninstallCommand = planCommandLine(resolveUninstallPlan(tool, plan.methodId, preferences, currentInstallOptions()), 'uninstall')
73
+ } catch (error) {
74
+ entry.error = errorText(error).slice(0, 200)
75
+ }
76
+ return entry
77
+ }),
78
+ }
79
+ }
80
+
32
81
  /** Wrap a Host operation so the client always receives a plain object. */
33
82
  async function settled(operation) {
34
83
  try { return { ok: true, value: await operation() } }
@@ -89,20 +138,43 @@ export class OfficialToolsRemoteService extends TypertRemoteService {
89
138
  return { id: tool.id, ready: false, reason: `执行入口检测失败:${String(error?.message ?? error).slice(0, 200)}` }
90
139
  }
91
140
  }))
92
- return { tools, executionCapabilities: officialToolExecutionCapabilities(), executionReadiness, hostVersion: HOST_PLUGIN_VERSION }
141
+ return {
142
+ tools,
143
+ executionCapabilities: officialToolExecutionCapabilities(),
144
+ executionReadiness,
145
+ hostVersion: HOST_PLUGIN_VERSION,
146
+ install: installSummary(),
147
+ }
93
148
  }
94
149
 
95
150
  /** Start one serialized fixed-registry install; return immediately for UI polling. */
96
- async installTool(toolId) {
97
- try {
151
+ async installTool(request) {
152
+ return settled(async () => {
98
153
  await ensureNpmPrefixOnPath()
99
- return { accepted: true, job: startInstall(toolId) }
100
- } catch (error) {
101
- return { accepted: false, error: errorText(error) }
102
- }
154
+ return { accepted: true, job: startInstall(request.tool, { method: request.method }) }
155
+ })
156
+ }
157
+
158
+ /**
159
+ * Remove one tool. Configuration, credentials and session history are kept;
160
+ * only the program itself (and the vendor installer's own PATH block) goes.
161
+ */
162
+ async uninstallTool(request) {
163
+ return settled(async () => {
164
+ await ensureNpmPrefixOnPath()
165
+ return { accepted: true, job: startUninstall(request.tool, { method: request.method }) }
166
+ })
167
+ }
168
+
169
+ /** Reinstall the tool even when it already reports the latest version. */
170
+ async repairTool(request) {
171
+ return settled(async () => {
172
+ await ensureNpmPrefixOnPath()
173
+ return { accepted: true, job: startRepair(request.tool, { method: request.method }) }
174
+ })
103
175
  }
104
176
 
105
- /** Stop a queued or running npm install, retaining its bounded job log. */
177
+ /** Stop a queued or running install, retaining its bounded job log. */
106
178
  cancel(toolId) {
107
179
  try {
108
180
  return { accepted: true, job: cancelInstall(toolId) }
@@ -21,6 +21,12 @@ export const LATEST_FAILURE_RETRY_MS = 30 * 60_000
21
21
  export const LATEST_TIMEOUT_MS = 5_000
22
22
  export const NPM_REGISTRY = 'https://registry.npmjs.org'
23
23
  export const ZCODE_DOWNLOAD_PAGE = 'https://zcode.z.ai/en/docs/install'
24
+ /**
25
+ * Step Code has no npm package; its own installer reads this release manifest,
26
+ * so it is the same document the install will use — and the same URL its
27
+ * mirror override redirects to.
28
+ */
29
+ export const STEPCODE_RELEASE_BASE = 'https://static-openapi.stepfun.com/stepcode'
24
30
  const MAX_DOCUMENT_BYTES = 2_000_000
25
31
 
26
32
  /** True only when both versions parse and the latest is strictly newer. */
@@ -59,11 +65,20 @@ async function boundedText(fetchImpl, url, timeoutMs, accept) {
59
65
  return body
60
66
  }
61
67
 
68
+ /** The release version out of a Step Code `latest.json` manifest. */
69
+ export function parseStepCodeManifest(text) {
70
+ let body
71
+ try { body = JSON.parse(String(text ?? '')) } catch { throw new Error('阶跃发布清单不是有效 JSON') }
72
+ if (!isReleaseVersion(body?.version)) throw new Error('阶跃发布清单缺少正式版本号')
73
+ return { version: body.version, url: `${STEPCODE_RELEASE_BASE}/${body.version}/manifest.json` }
74
+ }
75
+
62
76
  /** Where to look up a tool's latest release; null when there is no cheap source. */
63
77
  export function latestSourceFor(tool) {
64
78
  if (!tool || tool.unsupported) return null
65
79
  if (tool.manager === 'npm' && typeof tool.package === 'string') return { kind: 'npm', package: tool.package }
66
80
  if (tool.id === 'zcode') return { kind: 'zcode-download-page' }
81
+ if (tool.id === 'stepcode') return { kind: 'stepcode-release-manifest' }
67
82
  return null
68
83
  }
69
84
 
@@ -78,6 +93,10 @@ async function lookupSource(source, { fetchImpl, timeoutMs }) {
78
93
  if (!found) throw new Error('官方下载页没有 Windows x64 安装包链接')
79
94
  return { version: found.version, url: found.url, source: 'zcode-download-page' }
80
95
  }
96
+ if (source.kind === 'stepcode-release-manifest') {
97
+ const found = parseStepCodeManifest(await boundedText(fetchImpl, `${STEPCODE_RELEASE_BASE}/latest.json`, timeoutMs, 'application/json'))
98
+ return { ...found, source: 'stepcode-release-manifest' }
99
+ }
81
100
  throw new Error('不支持的版本来源')
82
101
  }
83
102
 
@@ -70,6 +70,14 @@ const CAPABILITIES = Object.freeze({
70
70
  supported: false,
71
71
  reason: 'Gemini 由无界面适配器执行(gemini -p),不走签名沙箱入口;未安装或失败时回退模型目录 API。',
72
72
  }),
73
+ opencode: Object.freeze({
74
+ supported: false,
75
+ reason: 'OpenCode 尚未完成受限执行入口核验;插件负责安装、检测与登录体检,执行仍需在终端中直接使用。',
76
+ }),
77
+ stepcode: Object.freeze({
78
+ supported: false,
79
+ reason: 'Step Code 尚未完成受限执行入口核验;插件负责安装、检测与登录体检,执行仍需在终端中直接使用。',
80
+ }),
73
81
  })
74
82
 
75
83
  /** A UI/Host can show this without implying that installation means execution readiness. */