@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.
- package/.dsh-plugin/client.js +920 -102
- package/.dsh-plugin/index.mjs +133 -27
- package/.dsh-plugin/official-tools-remote-service.mjs +82 -10
- package/.dsh-plugin/shared/latest-versions.mjs +19 -0
- package/.dsh-plugin/shared/official-tool-executor.mjs +8 -0
- package/.dsh-plugin/shared/official-tool-registry.mjs +393 -12
- package/.dsh-plugin/shared/official-tools-remote.mjs +38 -2
- package/.dsh-plugin/shared/official-tools-runtime.mjs +599 -68
- package/.dsh-plugin/shared/tool-health.mjs +7 -0
- package/.dsh-plugin/shared/tool-install-preferences.mjs +346 -0
- package/CHANGELOG.md +26 -1
- package/README.md +13 -3
- package/README.zh.md +13 -3
- package/{INSTALLATION_GUIDE.zh.md → docs/INSTALLATION_GUIDE.zh.md} +7 -7
- package/docs/WORKBENCH_USER_GUIDE.zh.md +36 -5
- package/{MIGRATION.md → docs/archive/MIGRATION.md} +1 -1
- package/docs/archive/README.md +21 -0
- package/package.json +5 -4
package/.dsh-plugin/index.mjs
CHANGED
|
@@ -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
|
-
:
|
|
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
|
-
:
|
|
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
|
-
:
|
|
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
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
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 (
|
|
1538
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
1558
|
-
|
|
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
|
-
|
|
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}
|
|
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:
|
|
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,
|
|
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
|
-
|
|
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 {
|
|
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(
|
|
97
|
-
|
|
151
|
+
async installTool(request) {
|
|
152
|
+
return settled(async () => {
|
|
98
153
|
await ensureNpmPrefixOnPath()
|
|
99
|
-
return { accepted: true, job: startInstall(
|
|
100
|
-
}
|
|
101
|
-
|
|
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
|
|
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. */
|