zen-gitsync 2.16.41 → 2.16.43
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/package.json +1 -1
- package/src/ui/public/assets/AgentView-8-xsOmE6.css +1 -0
- package/src/ui/public/assets/AgentView-CVfH_WTL.js +4 -0
- package/src/ui/public/assets/AppVersionBadge-BKQZrA46.js +2 -0
- package/src/ui/public/assets/AppVersionBadge-CS7Trvp-.css +1 -0
- package/src/ui/public/assets/BranchSelector-B0eLkAdv.css +1 -0
- package/src/ui/public/assets/BranchSelector-BrmzYPb2.js +1 -0
- package/src/ui/public/assets/CommitForm-CSAwdqxy.js +6 -0
- package/src/ui/public/assets/CommitForm-DdK8dymc.css +1 -0
- package/src/ui/public/assets/CommonDialog-ChSKsEz2.css +1 -0
- package/src/ui/public/assets/CommonDialog-Clc3mo5h.js +1 -0
- package/src/ui/public/assets/EditorView-B05M4l8X.js +1 -0
- package/src/ui/public/assets/EditorView-DZMLvQds.css +1 -0
- package/src/ui/public/assets/FlowExecutionViewer-Du0JPzV8.js +1 -0
- package/src/ui/public/assets/FlowExecutionViewer-jNHI5zdm.css +1 -0
- package/src/ui/public/assets/FlowOrchestrationWorkspace-CYGp-PqS.css +1 -0
- package/src/ui/public/assets/FlowOrchestrationWorkspace-Kg7HaggD.js +21 -0
- package/src/ui/public/assets/LogList-BlJplIi1.css +1 -0
- package/src/ui/public/assets/LogList-DSl8gjqq.js +7 -0
- package/src/ui/public/assets/MindmapView-BQTqx842.css +1 -0
- package/src/ui/public/assets/MindmapView-KeS9uPEF.js +2 -0
- package/src/ui/public/assets/MonitorView-BYqPPmpL.js +1 -0
- package/src/ui/public/assets/MonitorView-TaS4kL0Y.css +1 -0
- package/src/ui/public/assets/ProjectStartupButton-COEXIVyh.js +1 -0
- package/src/ui/public/assets/ProjectStartupButton-QLPltCK5.css +1 -0
- package/src/ui/public/assets/RemoteRepoCard-BRGNlbeO.css +1 -0
- package/src/ui/public/assets/RemoteRepoCard-Cqyksiy4.js +5 -0
- package/src/ui/public/assets/SourceMapView-BNRjvgzH.css +1 -0
- package/src/ui/public/assets/SourceMapView-DhqMQTQD.js +3 -0
- package/src/ui/public/assets/{SvgIcon-BQ6bR7l0.css → SvgIcon-CDNsk4-Q.css} +1 -1
- package/src/ui/public/assets/SvgIcon-DLr4V32f.js +1 -0
- package/src/ui/public/assets/UserInputNode-BVgQkB2l.css +1 -0
- package/src/ui/public/assets/UserInputNode-Do__vhj9.js +1 -0
- package/src/ui/public/assets/{WorkbenchView-CcYljxjT.css → WorkbenchView-Bp2YaSGS.css} +1 -1
- package/src/ui/public/assets/WorkbenchView-Dk5OlqZz.js +28 -0
- package/src/ui/public/assets/{_plugin-vue_export-helper-CkojBFWh.js → _plugin-vue_export-helper-CMAHJXhc.js} +4 -4
- package/src/ui/public/assets/{configStore-BisOZpe4.js → configStore-D666qKi0.js} +1 -1
- package/src/ui/public/assets/element-plus-DXuL5yWp.js +39 -0
- package/src/ui/public/assets/{flow-mindmap-D4bc307Q.js → flow-mindmap-Bi227PSm.js} +12 -12
- package/src/ui/public/assets/index-BT90zGWp.js +89 -0
- package/src/ui/public/assets/index-RKGE_YSG.css +1 -0
- package/src/ui/public/assets/office-docx-ZOPof2Jf.js +32 -0
- package/src/ui/public/assets/{office-excel-CgdzbtOn.js → office-excel-DxreApmb.js} +1 -1
- package/src/ui/public/assets/office-pptx-CR0bf9zY.js +74 -0
- package/src/ui/public/assets/toolsStore-DsvD8S_8.js +1 -0
- package/src/ui/public/assets/{vendor-FHXYVNcm.js → vendor-I3yQCqJK.js} +46 -39
- package/src/ui/public/assets/vue-flow-DV5FYL9P.js +39 -0
- package/src/ui/public/index.html +13 -13
- package/src/ui/server/routes/fileOpen.js +248 -20
- package/src/ui/server/routes/fileOpen.test.js +239 -0
- package/src/ui/public/assets/AgentView-BsN9y4Kg.css +0 -1
- package/src/ui/public/assets/AgentView-DnxCoGA3.js +0 -4
- package/src/ui/public/assets/AppVersionBadge-BYHep72-.js +0 -2
- package/src/ui/public/assets/AppVersionBadge-Dj6H729N.css +0 -1
- package/src/ui/public/assets/BranchSelector-CxDWJWY9.js +0 -1
- package/src/ui/public/assets/BranchSelector-T_rJm-JY.css +0 -1
- package/src/ui/public/assets/CommitForm-CUvl0VcF.css +0 -1
- package/src/ui/public/assets/CommitForm-uOwUV97K.js +0 -6
- package/src/ui/public/assets/CommonDialog-Cyh3xu0p.css +0 -1
- package/src/ui/public/assets/CommonDialog-Zoo8fqjA.js +0 -1
- package/src/ui/public/assets/EditorView-B2lxPjuX.js +0 -1
- package/src/ui/public/assets/EditorView-BgA-_8WV.css +0 -1
- package/src/ui/public/assets/FlowExecutionViewer-B5kh7VxS.css +0 -1
- package/src/ui/public/assets/FlowExecutionViewer-BUAzBEte.js +0 -1
- package/src/ui/public/assets/FlowOrchestrationWorkspace-BAQDU9Jd.css +0 -1
- package/src/ui/public/assets/FlowOrchestrationWorkspace-Ca4ROV6g.js +0 -21
- package/src/ui/public/assets/LogList-CLcg4XNV.js +0 -7
- package/src/ui/public/assets/LogList-r8sgFBhZ.css +0 -1
- package/src/ui/public/assets/MindmapView-B-w2CfCF.js +0 -1
- package/src/ui/public/assets/MindmapView-CS2t30Ko.css +0 -1
- package/src/ui/public/assets/MonitorView-DxFogQIt.css +0 -1
- package/src/ui/public/assets/MonitorView-RhIHMXtT.js +0 -1
- package/src/ui/public/assets/ProjectStartupButton-B7zvqRj_.css +0 -1
- package/src/ui/public/assets/ProjectStartupButton-D1woOmfK.js +0 -1
- package/src/ui/public/assets/RemoteRepoCard-C00T4duQ.js +0 -1
- package/src/ui/public/assets/RemoteRepoCard-d0klonVb.css +0 -1
- package/src/ui/public/assets/SourceMapView-BlGFU0dY.js +0 -3
- package/src/ui/public/assets/SourceMapView-DG_a_076.css +0 -1
- package/src/ui/public/assets/SvgIcon-DniMQk5U.js +0 -1
- package/src/ui/public/assets/UserInputNode-D83u028f.js +0 -1
- package/src/ui/public/assets/UserInputNode-DDtvv9Iq.css +0 -1
- package/src/ui/public/assets/WorkbenchView-BhZcJVl4.js +0 -12
- package/src/ui/public/assets/element-plus-Cchqf1s7.js +0 -19
- package/src/ui/public/assets/index-CwgZxqFI.js +0 -46
- package/src/ui/public/assets/index-DLi15Rb_.css +0 -1
- package/src/ui/public/assets/office-docx-BHL8ws1l.js +0 -31
- package/src/ui/public/assets/office-pptx-DyxMcWQh.js +0 -60
- package/src/ui/public/assets/toolsStore-C0VjU2dD.js +0 -1
- package/src/ui/public/assets/vue-flow-3jJr-V2W.js +0 -39
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import fs from 'fs/promises';
|
|
16
16
|
import { asyncRoute, HttpError } from '../utils/asyncRoute.js';
|
|
17
17
|
import path from 'path';
|
|
18
|
+
import os from 'os';
|
|
18
19
|
import open from 'open';
|
|
19
20
|
import { spawn, spawnSync } from 'child_process';
|
|
20
21
|
|
|
@@ -44,6 +45,61 @@ function commandExists(command, platform = process.platform) {
|
|
|
44
45
|
return result.status === 0;
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
/**
|
|
49
|
+
* 从 `--version` 的输出里提取 semver 版本号。
|
|
50
|
+
* 各工具输出格式五花八门:
|
|
51
|
+
* dsh → "0.1.1-rc.2"
|
|
52
|
+
* claude → "1.0.60 (Claude Code)"
|
|
53
|
+
* code → "1.92.0 abc123def (commit ...)"
|
|
54
|
+
* opencode → "opencode 0.1.30"
|
|
55
|
+
* 策略:取第一个 semver 模式(含 prerelease 0.1.1-rc.2 / build 1.0.0+build.1)。
|
|
56
|
+
* 提不出来返回 null(调用方按"未知版本"处理,不影响安装判定)。
|
|
57
|
+
*/
|
|
58
|
+
export function parseVersionOutput(text) {
|
|
59
|
+
if (!text) return null;
|
|
60
|
+
const match = String(text).match(/\d+\.\d+\.\d+(?:[-+][\w.-]+)?/);
|
|
61
|
+
return match ? match[0] : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── npm registry 最新版本查询(右键更新菜单的「当前 → 最新」提示用)──────────
|
|
65
|
+
// 为什么走 HTTP 而不是 `npm view <pkg> version`:spawnSync('npm.cmd', ...) 在
|
|
66
|
+
// server 子进程里不稳(prefix -g 曾返回 "…npm:3" 废地址,见 findDshExecutable
|
|
67
|
+
// 的修复记录),HTTP 直查又快又可控。
|
|
68
|
+
// registry 优先级:~/.npmrc 的 registry 字段(用户可能配了镜像)→ npmmirror 默认。
|
|
69
|
+
|
|
70
|
+
let cachedRegistryBase = null;
|
|
71
|
+
|
|
72
|
+
export function getRegistryBase(readFile = fs.readFile) {
|
|
73
|
+
if (cachedRegistryBase) return cachedRegistryBase;
|
|
74
|
+
return readFile(path.join(os.homedir(), '.npmrc'), 'utf8').then((npmrc) => {
|
|
75
|
+
const match = String(npmrc).match(/^\s*registry\s*=\s*(\S+)/m);
|
|
76
|
+
cachedRegistryBase = match
|
|
77
|
+
? match[1].replace(/\/+$/, '')
|
|
78
|
+
: 'https://registry.npmmirror.com';
|
|
79
|
+
return cachedRegistryBase;
|
|
80
|
+
}).catch(() => (cachedRegistryBase = 'https://registry.npmmirror.com'));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function fetchLatestVersion(packageName, { timeoutMs = 8000, fetchImpl = fetch } = {}) {
|
|
84
|
+
if (!packageName || !/^[\w@/.-]+$/.test(packageName)) return null;
|
|
85
|
+
const base = await getRegistryBase();
|
|
86
|
+
const controller = new AbortController();
|
|
87
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
88
|
+
try {
|
|
89
|
+
const resp = await fetchImpl(`${base}/${packageName}/latest`, {
|
|
90
|
+
signal: controller.signal,
|
|
91
|
+
headers: { accept: 'application/json' },
|
|
92
|
+
});
|
|
93
|
+
if (!resp.ok) return null;
|
|
94
|
+
const data = await resp.json();
|
|
95
|
+
return typeof data?.version === 'string' ? data.version : null;
|
|
96
|
+
} catch {
|
|
97
|
+
return null; // 网络失败/超时:菜单显示原文案,不影响任何功能
|
|
98
|
+
} finally {
|
|
99
|
+
clearTimeout(timer);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
47
103
|
/**
|
|
48
104
|
* 返回当前平台可展示、可执行的安装方案。
|
|
49
105
|
* executable/args 只在服务端使用;前端只能提交固定 tool id,不能提交命令。
|
|
@@ -64,6 +120,12 @@ export function getToolInstallers(platform = process.platform, hasCommand = comm
|
|
|
64
120
|
: '未检测到 npm,请先安装 Node.js/npm,或按照官方文档手动安装。',
|
|
65
121
|
executable: npmExecutable,
|
|
66
122
|
args: ['install', '-g', packageName],
|
|
123
|
+
// 升级变体:@latest 强制同步到最新版。已是最新时 npm 自然跳过。
|
|
124
|
+
updateCommand: `npm install -g ${packageName}@latest`,
|
|
125
|
+
updatePackageManager: 'npm',
|
|
126
|
+
updateNote: '将在新终端中执行 `npm install -g <pkg>@latest`。已是最新时 npm 会跳过。',
|
|
127
|
+
updateExecutable: npmExecutable,
|
|
128
|
+
updateArgs: ['install', '-g', `${packageName}@latest`],
|
|
67
129
|
};
|
|
68
130
|
}
|
|
69
131
|
|
|
@@ -79,6 +141,18 @@ export function getToolInstallers(platform = process.platform, hasCommand = comm
|
|
|
79
141
|
: '未检测到 winget,请使用 VS Code 官方安装程序。',
|
|
80
142
|
executable: 'winget',
|
|
81
143
|
args: ['install', '--id', 'Microsoft.VisualStudioCode', '-e', '--accept-package-agreements', '--accept-source-agreements'],
|
|
144
|
+
// winget 的升级动词是 upgrade(不是 install)
|
|
145
|
+
updateCommand: wingetAvailable
|
|
146
|
+
? 'winget upgrade --id Microsoft.VisualStudioCode -e --accept-package-agreements --accept-source-agreements'
|
|
147
|
+
: undefined,
|
|
148
|
+
updatePackageManager: 'winget',
|
|
149
|
+
updateNote: wingetAvailable
|
|
150
|
+
? '将在新终端中通过 winget upgrade 升级 VS Code。'
|
|
151
|
+
: '未检测到 winget,请使用 VS Code 官方安装包。',
|
|
152
|
+
updateExecutable: wingetAvailable ? 'winget' : undefined,
|
|
153
|
+
updateArgs: wingetAvailable
|
|
154
|
+
? ['upgrade', '--id', 'Microsoft.VisualStudioCode', '-e', '--accept-package-agreements', '--accept-source-agreements']
|
|
155
|
+
: undefined,
|
|
82
156
|
};
|
|
83
157
|
} else if (platform === 'darwin') {
|
|
84
158
|
const brewAvailable = hasCommand('brew', platform);
|
|
@@ -92,6 +166,11 @@ export function getToolInstallers(platform = process.platform, hasCommand = comm
|
|
|
92
166
|
: '未检测到 Homebrew,请使用 VS Code 官方安装程序。',
|
|
93
167
|
executable: 'brew',
|
|
94
168
|
args: ['install', '--cask', 'visual-studio-code'],
|
|
169
|
+
updateCommand: brewAvailable ? 'brew upgrade --cask visual-studio-code' : undefined,
|
|
170
|
+
updatePackageManager: 'Homebrew',
|
|
171
|
+
updateNote: brewAvailable ? '将在新终端中通过 brew upgrade 升级 VS Code。' : '未检测到 brew。',
|
|
172
|
+
updateExecutable: brewAvailable ? 'brew' : undefined,
|
|
173
|
+
updateArgs: brewAvailable ? ['upgrade', '--cask', 'visual-studio-code'] : undefined,
|
|
95
174
|
};
|
|
96
175
|
} else {
|
|
97
176
|
const snapAvailable = hasCommand('snap', platform);
|
|
@@ -105,6 +184,12 @@ export function getToolInstallers(platform = process.platform, hasCommand = comm
|
|
|
105
184
|
: '当前 Linux 环境未检测到 snap,请按照发行版对应的官方说明安装。',
|
|
106
185
|
executable: 'sudo',
|
|
107
186
|
args: ['snap', 'install', 'code', '--classic'],
|
|
187
|
+
// snap 的升级动词是 refresh
|
|
188
|
+
updateCommand: snapAvailable ? 'sudo snap refresh code' : undefined,
|
|
189
|
+
updatePackageManager: 'snap',
|
|
190
|
+
updateNote: snapAvailable ? '将在新终端中通过 snap refresh 升级 VS Code。' : '未检测到 snap。',
|
|
191
|
+
updateExecutable: snapAvailable ? 'sudo' : undefined,
|
|
192
|
+
updateArgs: snapAvailable ? ['snap', 'refresh', 'code'] : undefined,
|
|
108
193
|
};
|
|
109
194
|
}
|
|
110
195
|
|
|
@@ -130,6 +215,26 @@ export function getToolInstallers(platform = process.platform, hasCommand = comm
|
|
|
130
215
|
'Invoke-RestMethod https://code.kimi.com/kimi-code/install.ps1 | Invoke-Expression',
|
|
131
216
|
].join('; ')
|
|
132
217
|
: undefined,
|
|
218
|
+
// 升级:kimi 官方脚本幂等,已安装时执行会自动升级到最新版,直接复用同一脚本
|
|
219
|
+
updateCommand: platform === 'win32'
|
|
220
|
+
? 'irm https://code.kimi.com/kimi-code/install.ps1 | iex'
|
|
221
|
+
: 'curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash',
|
|
222
|
+
updatePackageManager: 'Kimi Code 官方安装脚本',
|
|
223
|
+
updateNote: '官方安装脚本幂等,已安装时执行会自动升级到最新版本。',
|
|
224
|
+
updateKind: 'script',
|
|
225
|
+
updateExecutionCommand: platform === 'win32'
|
|
226
|
+
? [
|
|
227
|
+
"if (-not (Get-Command Get-FileHash -ErrorAction SilentlyContinue)) {",
|
|
228
|
+
"function global:Get-FileHash { param([string]$Path, [string]$Algorithm = 'SHA256')",
|
|
229
|
+
"$stream = [System.IO.File]::OpenRead($Path)",
|
|
230
|
+
"try { $sha = [System.Security.Cryptography.SHA256]::Create(); try { $bytes = $sha.ComputeHash($stream) } finally { $sha.Dispose() } } finally { $stream.Dispose() }",
|
|
231
|
+
"$hash = ([System.BitConverter]::ToString($bytes)).Replace('-', '')",
|
|
232
|
+
"New-Object PSObject -Property @{ Algorithm = 'SHA256'; Hash = $hash; Path = $Path }",
|
|
233
|
+
"}",
|
|
234
|
+
"}",
|
|
235
|
+
'Invoke-RestMethod https://code.kimi.com/kimi-code/install.ps1 | Invoke-Expression',
|
|
236
|
+
].join('; ')
|
|
237
|
+
: undefined,
|
|
133
238
|
};
|
|
134
239
|
installers.zcode = {
|
|
135
240
|
supported: false,
|
|
@@ -137,6 +242,10 @@ export function getToolInstallers(platform = process.platform, hasCommand = comm
|
|
|
137
242
|
packageManager: 'ZCode 官方安装包',
|
|
138
243
|
docsUrl: TOOL_DOCS_URLS.zcode,
|
|
139
244
|
note: 'ZCode 是桌面应用,官方提供 Windows、macOS 和 Linux 安装包,没有官方 npm CLI。',
|
|
245
|
+
// ZCode 是桌面应用、没有 CLI 更新通道;/api/update-tool 会对它返回 400
|
|
246
|
+
updateCommand: undefined,
|
|
247
|
+
updatePackageManager: 'ZCode 官方安装包',
|
|
248
|
+
updateNote: 'ZCode 是桌面应用,升级请到官网下载新版安装包覆盖安装。',
|
|
140
249
|
};
|
|
141
250
|
return installers;
|
|
142
251
|
}
|
|
@@ -283,17 +392,34 @@ async function launchDsh(dirPath) {
|
|
|
283
392
|
return launchInTerminal(dirPath, executable, ['web']);
|
|
284
393
|
}
|
|
285
394
|
|
|
286
|
-
|
|
395
|
+
// 在 Windows 上找 dsh 可执行文件。
|
|
396
|
+
// 优先用 where.exe dsh(覆盖 nvm4w 的 C:\nvm4w\nodejs\、默认的 %APPDATA%\npm、
|
|
397
|
+
// pnpm/yarn 自定义 prefix、以及用户手摆到 PATH 里的任意位置),
|
|
398
|
+
//再兜底 APPDATA\npm + npm prefix -g 两个写死路径(为 where.exe 漏检的边缘情况,
|
|
399
|
+
//比如 PATH 里只有 junction/symlink 时)。
|
|
400
|
+
export async function findDshExecutable() {
|
|
287
401
|
if (process.platform !== 'win32') return commandExists('dsh') ? 'dsh' : null;
|
|
288
402
|
|
|
289
|
-
const
|
|
403
|
+
const where = spawnSync('where.exe', ['dsh'], { encoding: 'utf8', windowsHide: true });
|
|
404
|
+
if (where.status === 0 && where.stdout) {
|
|
405
|
+
for (const line of where.stdout.split(/\r?\n/).map((s) => s.trim()).filter(Boolean)) {
|
|
406
|
+
// 偶尔会输出 "PATH=..." 这种变量行,跳过
|
|
407
|
+
if (/^[A-Z_]+=/i.test(line)) continue;
|
|
408
|
+
try {
|
|
409
|
+
const stat = await fs.stat(line);
|
|
410
|
+
if (stat.isFile()) return line;
|
|
411
|
+
} catch {}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const fallback = [path.join(process.env.APPDATA || '', 'npm', 'dsh.cmd')];
|
|
290
416
|
const npmPrefix = spawnSync('npm.cmd', ['prefix', '-g'], {
|
|
291
417
|
encoding: 'utf8',
|
|
292
418
|
windowsHide: true,
|
|
293
419
|
}).stdout?.trim();
|
|
294
|
-
if (npmPrefix)
|
|
420
|
+
if (npmPrefix) fallback.push(path.join(npmPrefix, 'dsh.cmd'));
|
|
295
421
|
|
|
296
|
-
for (const candidate of
|
|
422
|
+
for (const candidate of fallback.filter(Boolean)) {
|
|
297
423
|
try {
|
|
298
424
|
const stat = await fs.stat(candidate);
|
|
299
425
|
if (stat.isFile()) return candidate;
|
|
@@ -390,27 +516,35 @@ async function findZCodeExecutable() {
|
|
|
390
516
|
return null;
|
|
391
517
|
}
|
|
392
518
|
|
|
393
|
-
async function launchToolInstaller(installer, dirPath = process.cwd()) {
|
|
394
|
-
|
|
519
|
+
async function launchToolInstaller(installer, dirPath = process.cwd(), { update = false } = {}) {
|
|
520
|
+
// update=true 时改用 updateXxx 字段(升级命令与安装命令动词不同:
|
|
521
|
+
// npm @latest / winget upgrade / brew upgrade / snap refresh / kimi 脚本幂等重跑)
|
|
522
|
+
const kindField = update ? 'updateKind' : 'kind';
|
|
523
|
+
const execCmdField = update ? 'updateExecutionCommand' : 'executionCommand';
|
|
524
|
+
const cmdField = update ? 'updateCommand' : 'command';
|
|
525
|
+
const executableField = update ? 'updateExecutable' : 'executable';
|
|
526
|
+
const argsField = update ? 'updateArgs' : 'args';
|
|
527
|
+
|
|
528
|
+
if (installer[kindField] === 'script') {
|
|
395
529
|
if (process.platform === 'win32') {
|
|
396
530
|
// 显式创建新控制台;编码脚本可避免 cmd.exe 截获 PowerShell 管道符。
|
|
397
|
-
const encoded = Buffer.from(installer
|
|
531
|
+
const encoded = Buffer.from(installer[execCmdField] || installer[cmdField], 'utf16le').toString('base64');
|
|
398
532
|
return spawnDetached('cmd.exe', [
|
|
399
533
|
'/c', 'start', '""', 'powershell.exe', '-NoLogo', '-NoExit',
|
|
400
534
|
'-ExecutionPolicy', 'Bypass', '-EncodedCommand', encoded,
|
|
401
535
|
], { cwd: dirPath });
|
|
402
536
|
}
|
|
403
|
-
return launchInTerminal(dirPath, 'bash', ['-lc', installer
|
|
537
|
+
return launchInTerminal(dirPath, 'bash', ['-lc', installer[cmdField]]);
|
|
404
538
|
}
|
|
405
539
|
if (process.platform === 'win32') {
|
|
406
|
-
//
|
|
407
|
-
const commandLine = [installer
|
|
540
|
+
// 安装/更新命令完全来自服务端白名单。使用可见 cmd 窗口,让用户看到进度和错误。
|
|
541
|
+
const commandLine = [installer[executableField], ...installer[argsField]].join(' ');
|
|
408
542
|
return spawnDetached('cmd.exe', ['/c', 'start', '', 'cmd.exe', '/k', commandLine], {
|
|
409
543
|
cwd: dirPath,
|
|
410
544
|
});
|
|
411
545
|
}
|
|
412
546
|
|
|
413
|
-
return launchInTerminal(dirPath, installer
|
|
547
|
+
return launchInTerminal(dirPath, installer[executableField], installer[argsField]);
|
|
414
548
|
}
|
|
415
549
|
|
|
416
550
|
export function registerFileOpenRoutes({
|
|
@@ -752,30 +886,113 @@ export function registerFileOpenRoutes({
|
|
|
752
886
|
});
|
|
753
887
|
}));
|
|
754
888
|
|
|
755
|
-
//
|
|
756
|
-
//
|
|
889
|
+
// 更新本地工具:与 install-tool 同一套白名单,只是换用 updateXxx 命令变体
|
|
890
|
+
// (npm @latest / winget upgrade / brew upgrade / snap refresh / kimi 脚本重跑)。
|
|
891
|
+
// 没有 updateCommand 的工具(如 zcode 桌面应用)返回 400。
|
|
892
|
+
app.post('/api/update-tool', asyncRoute(async (req, res) => {
|
|
893
|
+
const { tool } = req.body || {};
|
|
894
|
+
const installers = getToolInstallers();
|
|
895
|
+
const installer = installers[tool];
|
|
896
|
+
|
|
897
|
+
if (!installer) {
|
|
898
|
+
throw new HttpError(400, '不支持的工具');
|
|
899
|
+
}
|
|
900
|
+
if (!installer.updateCommand) {
|
|
901
|
+
throw new HttpError(400, installer.updateNote || '当前工具不支持一键更新,请按官方文档手动升级');
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
await launchToolInstaller(installer, process.cwd(), { update: true });
|
|
905
|
+
res.json({
|
|
906
|
+
success: true,
|
|
907
|
+
message: '更新命令已在新终端中启动',
|
|
908
|
+
});
|
|
909
|
+
}));
|
|
910
|
+
|
|
911
|
+
// 查询 npm registry 上的最新版本,供前端更新菜单显示「当前 v1 → 最新 v2」。
|
|
912
|
+
// 只支持 TOOL_INSTALL_PACKAGES 里的 npm 工具;网络失败的工具返回 null(前端降级为原文案)。
|
|
913
|
+
// body 可选 { tools: ['claude', ...] } —— 缺省查全部 npm 工具。
|
|
914
|
+
app.post('/api/latest-tool-versions', asyncRoute(async (req, res) => {
|
|
915
|
+
const { tools } = req.body || {};
|
|
916
|
+
let requested;
|
|
917
|
+
if (tools === undefined) {
|
|
918
|
+
requested = Object.keys(TOOL_INSTALL_PACKAGES);
|
|
919
|
+
} else {
|
|
920
|
+
if (!Array.isArray(tools) || tools.some((t) => typeof t !== 'string')) {
|
|
921
|
+
throw new HttpError(400, 'tools 必须是字符串数组');
|
|
922
|
+
}
|
|
923
|
+
const unknown = tools.filter((t) => !TOOL_INSTALL_PACKAGES[t]);
|
|
924
|
+
if (unknown.length) {
|
|
925
|
+
throw new HttpError(400, `不支持的工具: ${unknown.join(', ')}`);
|
|
926
|
+
}
|
|
927
|
+
requested = tools;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
const entries = await Promise.all(
|
|
931
|
+
requested.map(async (tool) => [tool, await fetchLatestVersion(TOOL_INSTALL_PACKAGES[tool])]),
|
|
932
|
+
);
|
|
933
|
+
res.json({
|
|
934
|
+
success: true,
|
|
935
|
+
latest: Object.fromEntries(entries),
|
|
936
|
+
});
|
|
937
|
+
}));
|
|
938
|
+
|
|
939
|
+
// 检测本地工具是否已安装 + 采集版本号(供前端决定按钮行为、tooltip 显示版本)
|
|
940
|
+
// 检测方式: spawn 'tool --version',exit 0 即视为已安装;stdout/stderr 里提取 semver。
|
|
757
941
|
// 超时 15s:Claude CLI / opencode 等 Node 包装的工具首次冷启动
|
|
758
942
|
// 在 Windows 上经常 5-7s(磁盘 cache + Defender 扫描),3s 永远命中超时。
|
|
759
943
|
app.get('/api/check-tools', asyncRoute(async (req, res) => {
|
|
760
944
|
const checkCmd = (cmd) => new Promise((resolve) => {
|
|
761
945
|
const child = spawn(cmd, ['--version'], {
|
|
762
|
-
|
|
946
|
+
// 要捕获输出来解析版本号,不能再用 'ignore'
|
|
947
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
763
948
|
shell: process.platform === 'win32',
|
|
764
949
|
windowsHide: true,
|
|
765
950
|
});
|
|
951
|
+
let output = '';
|
|
766
952
|
let done = false;
|
|
767
953
|
const finish = (ok) => {
|
|
768
954
|
if (done) return;
|
|
769
955
|
done = true;
|
|
770
956
|
try { child.kill('SIGKILL'); } catch {}
|
|
771
|
-
resolve(ok);
|
|
957
|
+
resolve({ installed: ok, version: parseVersionOutput(output) });
|
|
772
958
|
};
|
|
959
|
+
// 有些 CLI 把版本号打到 stderr(如部分 Node 包装工具),两边都收
|
|
960
|
+
child.stdout.on('data', (chunk) => { output += chunk; });
|
|
961
|
+
child.stderr.on('data', (chunk) => { output += chunk; });
|
|
773
962
|
child.on('error', () => finish(false));
|
|
774
963
|
child.on('exit', (code) => finish(code === 0));
|
|
775
964
|
setTimeout(() => finish(false), 15000);
|
|
776
965
|
});
|
|
777
966
|
|
|
778
|
-
|
|
967
|
+
// dsh / kimi 走 find*Executable 拿到完整路径,再补一次 --version 采版本。
|
|
968
|
+
// zcode 是纯 GUI 桌面应用,ZCode.exe --version 行为未知(可能弹窗),不采集。
|
|
969
|
+
const versionOfExecutable = (executable) => {
|
|
970
|
+
if (!executable) return Promise.resolve(null);
|
|
971
|
+
return new Promise((resolve) => {
|
|
972
|
+
const child = spawn(executable, ['--version'], {
|
|
973
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
974
|
+
shell: process.platform === 'win32',
|
|
975
|
+
windowsHide: true,
|
|
976
|
+
});
|
|
977
|
+
let output = '';
|
|
978
|
+
let done = false;
|
|
979
|
+
const finish = () => {
|
|
980
|
+
if (done) return;
|
|
981
|
+
done = true;
|
|
982
|
+
try { child.kill('SIGKILL'); } catch {}
|
|
983
|
+
resolve(parseVersionOutput(output));
|
|
984
|
+
};
|
|
985
|
+
child.stdout.on('data', (chunk) => { output += chunk; });
|
|
986
|
+
child.stderr.on('data', (chunk) => { output += chunk; });
|
|
987
|
+
child.on('error', () => finish());
|
|
988
|
+
child.on('exit', () => finish());
|
|
989
|
+
setTimeout(() => finish(), 15000);
|
|
990
|
+
});
|
|
991
|
+
};
|
|
992
|
+
|
|
993
|
+
// dsh / kimi 的版本采集依赖 find 结果,用 then 链接保持全并行
|
|
994
|
+
// (find 会跑两次——一次拿路径一次链版本——都是纯 fs/注册表查询,开销可忽略)
|
|
995
|
+
const [vscodeRes, claudeRes, codexRes, opencodeRes, kimiExecutable, zcodeExecutable, dshExecutable, kimiVersion, dshVersion] = await Promise.all([
|
|
779
996
|
checkCmd('code'),
|
|
780
997
|
checkCmd('claude'),
|
|
781
998
|
checkCmd('codex'),
|
|
@@ -783,6 +1000,8 @@ export function registerFileOpenRoutes({
|
|
|
783
1000
|
findKimiExecutable(),
|
|
784
1001
|
findZCodeExecutable(),
|
|
785
1002
|
findDshExecutable(),
|
|
1003
|
+
findKimiExecutable().then(versionOfExecutable),
|
|
1004
|
+
findDshExecutable().then(versionOfExecutable),
|
|
786
1005
|
]);
|
|
787
1006
|
|
|
788
1007
|
const installers = getToolInstallers();
|
|
@@ -790,13 +1009,22 @@ export function registerFileOpenRoutes({
|
|
|
790
1009
|
success: true,
|
|
791
1010
|
platform: process.platform,
|
|
792
1011
|
installers: publicInstallerInfo(installers),
|
|
793
|
-
vscode,
|
|
794
|
-
claude,
|
|
795
|
-
codex,
|
|
796
|
-
opencode,
|
|
1012
|
+
vscode: vscodeRes.installed,
|
|
1013
|
+
claude: claudeRes.installed,
|
|
1014
|
+
codex: codexRes.installed,
|
|
1015
|
+
opencode: opencodeRes.installed,
|
|
797
1016
|
kimi: !!kimiExecutable,
|
|
798
1017
|
zcode: !!zcodeExecutable,
|
|
799
1018
|
dsh: !!dshExecutable,
|
|
1019
|
+
versions: {
|
|
1020
|
+
vscode: vscodeRes.version,
|
|
1021
|
+
claude: claudeRes.version,
|
|
1022
|
+
codex: codexRes.version,
|
|
1023
|
+
opencode: opencodeRes.version,
|
|
1024
|
+
kimi: kimiExecutable ? kimiVersion : null,
|
|
1025
|
+
zcode: null, // 桌面应用,无 CLI 版本通道
|
|
1026
|
+
dsh: dshExecutable ? dshVersion : null,
|
|
1027
|
+
},
|
|
800
1028
|
});
|
|
801
1029
|
}));
|
|
802
1030
|
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
// Copyright 2026 xz333221
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
//
|
|
15
|
+
// findDshExecutable 回归测试(2026-09-05)
|
|
16
|
+
// 旧实现只查 %APPDATA%\npm + npm prefix -g,在 nvm4w 用户(全局包在
|
|
17
|
+
// C:\nvm4w\nodejs\) 上"未安装"误报。改用 where.exe dsh 优先后,这里守住
|
|
18
|
+
// "返回的路径必须真实存在"这条契约。
|
|
19
|
+
import { test } from 'node:test'
|
|
20
|
+
import assert from 'node:assert/strict'
|
|
21
|
+
import fs from 'node:fs/promises'
|
|
22
|
+
import { fetchLatestVersion, findDshExecutable, getToolInstallers, parseVersionOutput, registerFileOpenRoutes } from './fileOpen.js'
|
|
23
|
+
|
|
24
|
+
// ── parseVersionOutput(版本号采集,2026-09-05 check-tools 增强)────────────
|
|
25
|
+
|
|
26
|
+
test('parseVersionOutput: 各工具的 --version 输出都能提取 semver', () => {
|
|
27
|
+
assert.equal(parseVersionOutput('0.1.1-rc.2\n'), '0.1.1-rc.2') // dsh(带 prerelease)
|
|
28
|
+
assert.equal(parseVersionOutput('1.0.60 (Claude Code)'), '1.0.60') // claude
|
|
29
|
+
assert.equal(parseVersionOutput('1.92.0 abc123def (commit)\n'), '1.92.0') // vscode
|
|
30
|
+
assert.equal(parseVersionOutput('opencode 0.1.30\n'), '0.1.30') // opencode 前缀
|
|
31
|
+
assert.equal(parseVersionOutput('codex-cli 0.9.2+build.1'), '0.9.2+build.1') // build 元数据
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('parseVersionOutput: 边界输入返回 null', () => {
|
|
35
|
+
assert.equal(parseVersionOutput(''), null)
|
|
36
|
+
assert.equal(parseVersionOutput(null), null)
|
|
37
|
+
assert.equal(parseVersionOutput(undefined), null)
|
|
38
|
+
assert.equal(parseVersionOutput('no version here'), null)
|
|
39
|
+
// 多段数字但不是 semver(如日期 2026.09.05 会被提取——可接受,总比没有强)
|
|
40
|
+
assert.ok(parseVersionOutput('version: 2.16.42\nsomething 1.0.0').startsWith('2.16.42'))
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('findDshExecutable: Windows 上返回 string 或 null,且非 null 时路径必须存在', async () => {
|
|
44
|
+
const result = await findDshExecutable()
|
|
45
|
+
if (process.platform !== 'win32') {
|
|
46
|
+
// 非 win32 平台走 commandExists('dsh') 路径:要么是 'dsh'(which 命中),
|
|
47
|
+
// 要么是 null。不强求特定值,只断言类型。
|
|
48
|
+
assert.ok(result === null || typeof result === 'string')
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
if (result !== null) {
|
|
52
|
+
assert.equal(typeof result, 'string')
|
|
53
|
+
// 核心契约:返回的路径必须真实存在且是文件。
|
|
54
|
+
// 这正是这次修复要解决的:不允许再出现"返回看似合法但 fs.stat 不到的路径"
|
|
55
|
+
// (旧实现里 APPDATA/npm/dsh.cmd + npm prefix -g 拼出的废地址都是 fs.stat 不到的)。
|
|
56
|
+
const stat = await fs.stat(result)
|
|
57
|
+
assert.equal(stat.isFile(), true, `${result} 不是文件`)
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
// ── 工具升级(updateCommand)──────────────────────────────────────────
|
|
62
|
+
// 2026-09-05 新增:右键工具 → 更新。install 与 update 命令动词不同
|
|
63
|
+
// (npm @latest / winget upgrade / brew upgrade / snap refresh / kimi 脚本重跑)。
|
|
64
|
+
|
|
65
|
+
test('getToolInstallers: npm 包工具的升级变体带 @latest', () => {
|
|
66
|
+
const installers = getToolInstallers('win32', () => true)
|
|
67
|
+
for (const tool of ['claude', 'codex', 'opencode', 'dsh']) {
|
|
68
|
+
const inst = installers[tool]
|
|
69
|
+
assert.match(inst.updateCommand, /@latest$/, `${tool}.updateCommand 应带 @latest`)
|
|
70
|
+
assert.ok(inst.updateExecutable, `${tool}.updateExecutable 缺失`)
|
|
71
|
+
assert.ok(
|
|
72
|
+
inst.updateArgs.at(-1).endsWith('@latest'),
|
|
73
|
+
`${tool}.updateArgs 最后一项应带 @latest`,
|
|
74
|
+
)
|
|
75
|
+
// 安装命令不被升级字段污染
|
|
76
|
+
assert.doesNotMatch(inst.command, /@latest/, `${tool}.command 不应带 @latest`)
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('getToolInstallers: 各平台 vscode 升级用各自的 upgrade 动词', () => {
|
|
81
|
+
const win = getToolInstallers('win32', () => true)
|
|
82
|
+
assert.match(win.vscode.updateCommand, /^winget upgrade/)
|
|
83
|
+
assert.deepEqual(win.vscode.updateArgs.slice(0, 2), ['upgrade', '--id'])
|
|
84
|
+
|
|
85
|
+
const mac = getToolInstallers('darwin', () => true)
|
|
86
|
+
assert.match(mac.vscode.updateCommand, /^brew upgrade/)
|
|
87
|
+
|
|
88
|
+
const linux = getToolInstallers('linux', () => true)
|
|
89
|
+
assert.match(linux.vscode.updateCommand, /^sudo snap refresh/)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('getToolInstallers: kimi 升级复用幂等脚本,zcode 不支持一键升级', () => {
|
|
93
|
+
const installers = getToolInstallers('win32', () => true)
|
|
94
|
+
assert.equal(installers.kimi.updateKind, 'script')
|
|
95
|
+
assert.match(installers.kimi.updateCommand, /install\.ps1/)
|
|
96
|
+
// zcode 是桌面应用,updateCommand 必须为空 → /api/update-tool 对它返回 400
|
|
97
|
+
assert.equal(installers.zcode.updateCommand, undefined)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
test('update-tool 路由: zcode(无 updateCommand)返回 400 且不启动任何进程', async () => {
|
|
101
|
+
// zcode 的 400 在 launchToolInstaller 之前抛出,本测试不会真的打开终端窗口。
|
|
102
|
+
const routes = new Map()
|
|
103
|
+
const app = {
|
|
104
|
+
get(path, handler) { routes.set(`GET ${path}`, handler) },
|
|
105
|
+
post(path, handler) { routes.set(`POST ${path}`, handler) },
|
|
106
|
+
}
|
|
107
|
+
registerFileOpenRoutes({ app })
|
|
108
|
+
const handler = routes.get('POST /api/update-tool')
|
|
109
|
+
assert.ok(handler, 'POST /api/update-tool 路由未注册')
|
|
110
|
+
|
|
111
|
+
let statusCode = 200
|
|
112
|
+
let payload = null
|
|
113
|
+
const res = {
|
|
114
|
+
status(code) { statusCode = code; return this },
|
|
115
|
+
json(value) { payload = value; return this },
|
|
116
|
+
}
|
|
117
|
+
await handler({ method: 'POST', path: '/api/update-tool', body: { tool: 'zcode' } }, res, () => {})
|
|
118
|
+
|
|
119
|
+
assert.equal(statusCode, 400)
|
|
120
|
+
assert.equal(payload.success, false)
|
|
121
|
+
assert.match(payload.error, /不支持一键更新|桌面应用/)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test('update-tool 路由: 未知工具 id 返回 400', async () => {
|
|
125
|
+
const routes = new Map()
|
|
126
|
+
const app = {
|
|
127
|
+
get(path, handler) { routes.set(`GET ${path}`, handler) },
|
|
128
|
+
post(path, handler) { routes.set(`POST ${path}`, handler) },
|
|
129
|
+
}
|
|
130
|
+
registerFileOpenRoutes({ app })
|
|
131
|
+
const handler = routes.get('POST /api/update-tool')
|
|
132
|
+
|
|
133
|
+
let statusCode = 200
|
|
134
|
+
let payload = null
|
|
135
|
+
const res = {
|
|
136
|
+
status(code) { statusCode = code; return this },
|
|
137
|
+
json(value) { payload = value; return this },
|
|
138
|
+
}
|
|
139
|
+
await handler({ method: 'POST', path: '/api/update-tool', body: { tool: 'not-a-tool' } }, res, () => {})
|
|
140
|
+
|
|
141
|
+
assert.equal(statusCode, 400)
|
|
142
|
+
assert.equal(payload.success, false)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
// ── fetchLatestVersion / latest-tool-versions(2026-09-05 更新菜单「当前→最新」)──
|
|
146
|
+
|
|
147
|
+
test('fetchLatestVersion: 非法包名直接返回 null,不发请求', async () => {
|
|
148
|
+
let fetchCalls = 0
|
|
149
|
+
const fakeFetch = async () => { fetchCalls++; return { ok: true, json: async () => ({ version: '1.0.0' }) } }
|
|
150
|
+
assert.equal(await fetchLatestVersion(''), null)
|
|
151
|
+
assert.equal(await fetchLatestVersion(null), null)
|
|
152
|
+
assert.equal(await fetchLatestVersion('bad pkg name with spaces'), null)
|
|
153
|
+
assert.equal(await fetchLatestVersion('../etc/passwd'), null)
|
|
154
|
+
assert.equal(fetchCalls, 0, '非法包名不应触发任何网络请求')
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
test('fetchLatestVersion: 命中 registry 返回 version;非 200 / 抛异常 / 字段缺失都降级为 null', async () => {
|
|
158
|
+
const okFetch = async () => ({ ok: true, json: async () => ({ version: '2.3.4' }) })
|
|
159
|
+
assert.equal(await fetchLatestVersion('opencode-ai', { fetchImpl: okFetch }), '2.3.4')
|
|
160
|
+
|
|
161
|
+
const notOkFetch = async () => ({ ok: false, json: async () => ({}) })
|
|
162
|
+
assert.equal(await fetchLatestVersion('opencode-ai', { fetchImpl: notOkFetch }), null)
|
|
163
|
+
|
|
164
|
+
const throwFetch = async () => { throw new Error('network down') }
|
|
165
|
+
assert.equal(await fetchLatestVersion('opencode-ai', { fetchImpl: throwFetch }), null)
|
|
166
|
+
|
|
167
|
+
const missingFieldFetch = async () => ({ ok: true, json: async () => ({ name: 'opencode-ai' }) })
|
|
168
|
+
assert.equal(await fetchLatestVersion('opencode-ai', { fetchImpl: missingFieldFetch }), null)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
test('latest-tool-versions 路由: tools 非数组 / 含未知工具 → 400,且不发请求', async () => {
|
|
172
|
+
const routes = new Map()
|
|
173
|
+
const app = {
|
|
174
|
+
get(path, handler) { routes.set(`GET ${path}`, handler) },
|
|
175
|
+
post(path, handler) { routes.set(`POST ${path}`, handler) },
|
|
176
|
+
}
|
|
177
|
+
registerFileOpenRoutes({ app })
|
|
178
|
+
const handler = routes.get('POST /api/latest-tool-versions')
|
|
179
|
+
assert.ok(handler, 'POST /api/latest-tool-versions 路由未注册')
|
|
180
|
+
|
|
181
|
+
const makeRes = () => {
|
|
182
|
+
let statusCode = 200
|
|
183
|
+
let payload = null
|
|
184
|
+
return {
|
|
185
|
+
res: { status(code) { statusCode = code; return this }, json(value) { payload = value; return this } },
|
|
186
|
+
get status() { return statusCode },
|
|
187
|
+
get payload() { return payload },
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
let ctx = makeRes()
|
|
192
|
+
await handler({ method: 'POST', body: { tools: 'claude' } }, ctx.res, () => {})
|
|
193
|
+
assert.equal(ctx.status, 400)
|
|
194
|
+
|
|
195
|
+
ctx = makeRes()
|
|
196
|
+
await handler({ method: 'POST', body: { tools: [123] } }, ctx.res, () => {})
|
|
197
|
+
assert.equal(ctx.status, 400)
|
|
198
|
+
|
|
199
|
+
ctx = makeRes()
|
|
200
|
+
await handler({ method: 'POST', body: { tools: ['claude', 'zcode'] } }, ctx.res, () => {})
|
|
201
|
+
// zcode 不在 TOOL_INSTALL_PACKAGES(npm 工具白名单)里 → 400
|
|
202
|
+
assert.equal(ctx.status, 400)
|
|
203
|
+
assert.match(ctx.payload.error, /不支持的工具/)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
test('latest-tool-versions 路由: 正常路径调 registry 并把结果按 tool id 归还', async () => {
|
|
207
|
+
// 用全局 fetch mock 验证「路由 → fetchLatestVersion → registry URL」整条接线,
|
|
208
|
+
// 测试完必须恢复,不然会污染其它测试。
|
|
209
|
+
const requestedUrls = []
|
|
210
|
+
const originalFetch = globalThis.fetch
|
|
211
|
+
globalThis.fetch = async (url) => {
|
|
212
|
+
requestedUrls.push(String(url))
|
|
213
|
+
return { ok: true, json: async () => ({ version: '9.9.9' }) }
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
const routes = new Map()
|
|
217
|
+
const app = {
|
|
218
|
+
get(path, handler) { routes.set(`GET ${path}`, handler) },
|
|
219
|
+
post(path, handler) { routes.set(`POST ${path}`, handler) },
|
|
220
|
+
}
|
|
221
|
+
registerFileOpenRoutes({ app })
|
|
222
|
+
const handler = routes.get('POST /api/latest-tool-versions')
|
|
223
|
+
|
|
224
|
+
let payload = null
|
|
225
|
+
const res = {
|
|
226
|
+
status() { return this },
|
|
227
|
+
json(value) { payload = value; return this },
|
|
228
|
+
}
|
|
229
|
+
await handler({ method: 'POST', body: { tools: ['claude', 'dsh'] } }, res, () => {})
|
|
230
|
+
|
|
231
|
+
assert.equal(payload.success, true)
|
|
232
|
+
assert.deepEqual(payload.latest, { claude: '9.9.9', dsh: '9.9.9' })
|
|
233
|
+
assert.equal(requestedUrls.length, 2)
|
|
234
|
+
assert.ok(requestedUrls[0].includes('/@anthropic-ai/claude-code/latest'), `URL 不对: ${requestedUrls[0]}`)
|
|
235
|
+
assert.ok(requestedUrls[1].includes('/@deepseek-ai/dsh/latest'), `URL 不对: ${requestedUrls[1]}`)
|
|
236
|
+
} finally {
|
|
237
|
+
globalThis.fetch = originalFetch
|
|
238
|
+
}
|
|
239
|
+
})
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
.agent-view[data-v-773baa74]{background:var(--bg-container);height:100%;min-height:0;display:flex}.agent-sidebar[data-v-773baa74]{background:var(--bg-panel);border-right:1px solid var(--border-color);flex-direction:column;flex-shrink:0;display:flex;overflow:hidden}.sidebar-header[data-v-773baa74]{flex-shrink:0;padding:10px 12px}.new-session-btn[data-v-773baa74]{border:1px dashed var(--border-color);border-radius:var(--radius-md);width:100%;color:var(--text-secondary);cursor:pointer;background:0 0;justify-content:center;align-items:center;gap:6px;padding:8px 12px;font-family:inherit;font-size:13px;transition:all .15s;display:flex}.new-session-btn[data-v-773baa74]:hover{border-color:var(--color-primary);color:var(--color-primary);background:color-mix(in srgb, var(--color-primary) 6%, transparent)}.sidebar-search[data-v-773baa74]{flex-shrink:0;align-items:center;gap:6px;padding:0 12px 8px;display:flex}.sidebar-search .search-icon[data-v-773baa74]{color:var(--text-tertiary);flex-shrink:0}.sidebar-search .search-input[data-v-773baa74]{color:var(--text-primary);background:0 0;border:none;outline:none;flex:1;padding:4px 0;font-family:inherit;font-size:13px}.sidebar-search .search-input[data-v-773baa74]::placeholder{color:var(--text-tertiary)}.session-list[data-v-773baa74]{flex:1;min-height:0;padding:0 6px 8px;overflow-y:auto}.empty-state[data-v-773baa74]{text-align:center;color:var(--text-tertiary);flex-direction:column;justify-content:center;align-items:center;padding:48px 16px;display:flex}.empty-state .empty-text[data-v-773baa74]{color:var(--text-secondary);margin-top:10px;font-size:14px}.empty-state .empty-hint[data-v-773baa74]{margin-top:4px;font-size:12px}.session-item[data-v-773baa74]{border-radius:var(--radius-sm);cursor:pointer;align-items:flex-start;gap:4px;padding:8px 10px;transition:background .12s;display:flex;position:relative}.session-item[data-v-773baa74]:hover{background:var(--bg-hover)}.session-item:hover .session-item-actions[data-v-773baa74]{opacity:1}.session-item.active[data-v-773baa74]{background:color-mix(in srgb, var(--color-primary) 10%, transparent)}.session-item.active .session-item-title[data-v-773baa74]{color:var(--color-primary)}.session-item.active[data-v-773baa74]:before{content:"";background:var(--color-primary);border-radius:0 2px 2px 0;width:3px;height:20px;position:absolute;top:50%;left:0;transform:translateY(-50%)}.session-item-main[data-v-773baa74]{flex:1;min-width:0}.session-item-title[data-v-773baa74]{color:var(--text-primary);white-space:nowrap;text-overflow:ellipsis;margin-bottom:2px;font-size:13px;font-weight:500;overflow:hidden}.session-item-meta[data-v-773baa74]{color:var(--text-tertiary);align-items:center;gap:4px;font-size:11px;display:flex}.session-item-meta .meta-dot[data-v-773baa74]{opacity:.5}.session-item-meta .meta-source[data-v-773baa74]{border-radius:3px;padding:0 4px;font-size:10px;font-weight:600}.session-item-meta .meta-source.cli[data-v-773baa74]{background:color-mix(in srgb, var(--color-warning) 20%, transparent);color:var(--color-warning)}.session-item-actions[data-v-773baa74]{opacity:0;flex-shrink:0;gap:2px;transition:opacity .12s;display:flex}.item-action-btn[data-v-773baa74]{width:24px;height:24px;color:var(--text-tertiary);cursor:pointer;border-radius:var(--radius-xs);background:0 0;border:none;justify-content:center;align-items:center;padding:0;transition:all .12s;display:flex}.item-action-btn[data-v-773baa74]:hover{background:var(--bg-hover);color:var(--text-secondary)}.item-action-btn.danger[data-v-773baa74]:hover{color:var(--color-danger);background:color-mix(in srgb, var(--color-danger) 10%, transparent)}.sidebar-resizer[data-v-773baa74]{cursor:col-resize;background:0 0;flex-shrink:0;width:4px;transition:background .15s}.sidebar-resizer[data-v-773baa74]:hover,.sidebar-resizer.active[data-v-773baa74]{background:var(--color-primary);opacity:.3}.agent-chat-area[data-v-773baa74]{flex-direction:column;flex:1;min-width:0;display:flex;position:relative;overflow:hidden}.chat-loading[data-v-773baa74]{color:var(--text-tertiary);flex-direction:column;flex:1;justify-content:center;align-items:center;gap:12px;font-size:14px;display:flex}.stop-button-bar[data-v-773baa74]{z-index:10;position:absolute;bottom:80px;left:50%;transform:translate(-50%)}.stop-button[data-v-773baa74]{border:1px solid var(--color-danger);background:var(--bg-container);color:var(--color-danger);cursor:pointer;border-radius:20px;align-items:center;gap:6px;padding:6px 16px;font-family:inherit;font-size:13px;transition:all .15s;display:flex;box-shadow:0 2px 8px #0000001f}.stop-button[data-v-773baa74]:hover{background:var(--color-danger);color:#fff}.fade-enter-active[data-v-773baa74],.fade-leave-active[data-v-773baa74]{transition:opacity .2s}.fade-enter-from[data-v-773baa74],.fade-leave-to[data-v-773baa74]{opacity:0}[data-theme=dark]{background:var(--bg-panel)}
|