dsh-agentone 0.5.22 → 0.5.24
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/lib/index.js +32 -28
- package/lib/page.js +18 -8
- package/lib/proc.js +419 -64
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -34,7 +34,7 @@ import { applyPlatformSkillhub, removePlatformSkillhub } from './skillhub.js';
|
|
|
34
34
|
import { beginCliAuth, cliAuthQrcode, completeCliAuth, parseCliAuthBeginOutput } from './cliauth.mjs';
|
|
35
35
|
import { platformFetch } from './http.js';
|
|
36
36
|
import { donePageHtml, statusPageHtml } from './page.js';
|
|
37
|
-
import { compareSemver, dshPluginCli, firstErrorLines, installCliTool, isUnexpectedStoreError, probeCliDetail, pnpmViewLatest, runCliTool, unexpectedStoreGuidance, upgradeCliTool } from './proc.js';
|
|
37
|
+
import { compareSemver, dshPluginCli, firstErrorLines, installCliTool, isUnexpectedStoreError, probeCliDetail, probePythonEnv, pnpmViewLatest, runCliTool, unexpectedStoreGuidance, upgradeCliTool } from './proc.js';
|
|
38
38
|
|
|
39
39
|
import { clearCredentials, loadCredentials, resolveDshHome, saveCredentials, updateCredentials } from './store.js';
|
|
40
40
|
|
|
@@ -370,6 +370,21 @@ function specLabel(spec) {
|
|
|
370
370
|
|
|
371
371
|
/** dsh 插件与 CLI 操作白名单:用户输入不得进入 execFile 的 tool 位。 */
|
|
372
372
|
const CLI_TOOLS = new Set(['lark-cli', 'ccpg-cli']);
|
|
373
|
+
/** 除 CLI 外可通过 /agentone/api/cli/install 自助安装的工具(python 无
|
|
374
|
+
* 授权/升级概念,仅安装)。 */
|
|
375
|
+
const CLI_INSTALL_TOOLS = new Set(['python']);
|
|
376
|
+
|
|
377
|
+
/** CLI 详情防降级:latest 不高于已装版本时置 null,前端不出「升级」
|
|
378
|
+
* 按钮(ccpg 媒体通道可能比 update 通道发得更新,实测装到 1.2.16 时
|
|
379
|
+
* update --check 的 latest 还是 1.2.15;与插件列表 1123/1200 行同口径)。 */
|
|
380
|
+
function withCliUpgradeGuard(detail) {
|
|
381
|
+
if (!detail || typeof detail !== 'object') return detail;
|
|
382
|
+
const { version, latest } = detail;
|
|
383
|
+
return {
|
|
384
|
+
...detail,
|
|
385
|
+
latest: latest && version && compareSemver(latest, version) > 0 ? latest : null,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
373
388
|
|
|
374
389
|
/**
|
|
375
390
|
* 插件自身是否由平台托管镜像管理:镜像 Dockerfile 固有 ENV
|
|
@@ -1211,26 +1226,12 @@ export function apply(ctx, config) {
|
|
|
1211
1226
|
// 真实探测本机 CLI:三态与平台 cli-deployd 判定口径一致,附带版本
|
|
1212
1227
|
// 与最新版(查询失败降级 null,不阻塞三态)。并发探测,单个失败不影
|
|
1213
1228
|
// 响另一个。附带 Python 环境探测(技能脚本标准依赖,Windows GUI 启动
|
|
1214
|
-
// 时 PATH 常缺 python
|
|
1215
|
-
|
|
1216
|
-
? ['python', 'python3', 'py']
|
|
1217
|
-
: ['python3', 'python'];
|
|
1218
|
-
const probePython = async () => {
|
|
1219
|
-
for (const name of pythonCandidates) {
|
|
1220
|
-
try {
|
|
1221
|
-
const result = await runCliTool(name, ['--version'], { timeoutMs: 10_000 });
|
|
1222
|
-
const match = /(\d+\.\d+(?:\.\d+)?)/.exec(`${result.stdout || ''}${result.stderr || ''}`);
|
|
1223
|
-
if (match) return { available: true, command: name, version: match[1] };
|
|
1224
|
-
} catch {
|
|
1225
|
-
// 未安装或 Store 假别名:试下一个候选
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
|
-
return { available: false, command: null, version: null };
|
|
1229
|
-
};
|
|
1229
|
+
// 时 PATH 常缺 python 或前段是 Store 假别名,probePythonEnv 逐个候选
|
|
1230
|
+
// 穿透重试),缺失时前端给安装按钮/指引。
|
|
1230
1231
|
const [lark, ccpg, python] = await Promise.all([
|
|
1231
|
-
probeCliDetail('lark-cli').catch(() => ({ state: 'missing', version: null, latest: null })),
|
|
1232
|
-
probeCliDetail('ccpg-cli').catch(() => ({ state: 'missing', version: null, latest: null })),
|
|
1233
|
-
|
|
1232
|
+
probeCliDetail('lark-cli').then(withCliUpgradeGuard).catch(() => ({ state: 'missing', version: null, latest: null })),
|
|
1233
|
+
probeCliDetail('ccpg-cli').then(withCliUpgradeGuard).catch(() => ({ state: 'missing', version: null, latest: null })),
|
|
1234
|
+
probePythonEnv().catch(() => ({ available: false, command: null, version: null })),
|
|
1234
1235
|
]);
|
|
1235
1236
|
sendJson(response, 200, { tools: { 'lark-cli': lark, 'ccpg-cli': ccpg }, python });
|
|
1236
1237
|
});
|
|
@@ -1244,15 +1245,18 @@ export function apply(ctx, config) {
|
|
|
1244
1245
|
try {
|
|
1245
1246
|
const body = await readJsonBody(request);
|
|
1246
1247
|
const tool = String(body.tool || '');
|
|
1247
|
-
if (!CLI_TOOLS.has(tool)) {
|
|
1248
|
+
if (!CLI_TOOLS.has(tool) && !CLI_INSTALL_TOOLS.has(tool)) {
|
|
1248
1249
|
sendJson(response, 400, { error: '不支持的 CLI 工具' });
|
|
1249
1250
|
return;
|
|
1250
1251
|
}
|
|
1251
|
-
// 自助安装(lark 走 npm、ccpg
|
|
1252
|
-
//
|
|
1252
|
+
// 自助安装(lark 走 npm、ccpg 走官方安装器、python 走 python.org
|
|
1253
|
+
// 官方安装器静默装),与升级一样分钟级耗时:输出尾部透传给前端展示
|
|
1253
1254
|
const output = await installCliTool(tool);
|
|
1254
|
-
|
|
1255
|
-
|
|
1255
|
+
// python 无三态概念,回传探测详情供前端就地刷新
|
|
1256
|
+
const detail = tool === 'python'
|
|
1257
|
+
? await probePythonEnv().catch(() => null)
|
|
1258
|
+
: await probeCliDetail(tool).then(withCliUpgradeGuard).catch(() => null);
|
|
1259
|
+
sendJson(response, 200, { ok: true, output, tool: detail, python: tool === 'python' ? detail : undefined });
|
|
1256
1260
|
} catch (error) {
|
|
1257
1261
|
sendJson(response, 502, { error: error?.message || 'CLI 安装失败,请稍后重试' });
|
|
1258
1262
|
}
|
|
@@ -1274,7 +1278,7 @@ export function apply(ctx, config) {
|
|
|
1274
1278
|
// 官方自升级(ccpg 下载二进制可达分钟级),输出尾部透传给前端
|
|
1275
1279
|
// (lark-cli 手动安装时含 GitHub 下载地址)
|
|
1276
1280
|
const output = await upgradeCliTool(tool);
|
|
1277
|
-
const detail = await probeCliDetail(tool).catch(() => null);
|
|
1281
|
+
const detail = await probeCliDetail(tool).then(withCliUpgradeGuard).catch(() => null);
|
|
1278
1282
|
sendJson(response, 200, { ok: true, output, tool: detail });
|
|
1279
1283
|
} catch (error) {
|
|
1280
1284
|
sendJson(response, 502, { error: error?.message || 'CLI 升级失败,请稍后重试' });
|
|
@@ -1298,7 +1302,7 @@ export function apply(ctx, config) {
|
|
|
1298
1302
|
sendJson(response, 400, { error: '不支持的 CLI 工具' });
|
|
1299
1303
|
return;
|
|
1300
1304
|
}
|
|
1301
|
-
const detail = await probeCliDetail(tool).catch(() => null);
|
|
1305
|
+
const detail = await probeCliDetail(tool).then(withCliUpgradeGuard).catch(() => null);
|
|
1302
1306
|
if (detail?.state === 'missing') {
|
|
1303
1307
|
sendJson(response, 400, { error: 'CLI 未安装,请先安装或联系管理员' });
|
|
1304
1308
|
return;
|
|
@@ -1348,7 +1352,7 @@ export function apply(ctx, config) {
|
|
|
1348
1352
|
// lark-cli 同时轮同一码。这里 25s 一掐,前端串行重试(见 page.js)。
|
|
1349
1353
|
await completeCliAuth(tool, deviceCode, { timeoutMs: 25_000 });
|
|
1350
1354
|
pendingCliAuth.delete(tool);
|
|
1351
|
-
const detail = await probeCliDetail(tool).catch(() => null);
|
|
1355
|
+
const detail = await probeCliDetail(tool).then(withCliUpgradeGuard).catch(() => null);
|
|
1352
1356
|
sendJson(response, 200, { ok: true, tool: detail });
|
|
1353
1357
|
} catch (error) {
|
|
1354
1358
|
// 超时=我们主动掐断轮询(用户还没在授权页同意):保留会话,202
|
package/lib/page.js
CHANGED
|
@@ -526,7 +526,8 @@ const renderCliData = (s, tools, python) => {
|
|
|
526
526
|
+ esc(name) + status + '</div><div class="skill-sub">'
|
|
527
527
|
+ versionLine + '</div></div><div style="display:flex;gap:6px">' + actions + '</div></div>';
|
|
528
528
|
});
|
|
529
|
-
// Python
|
|
529
|
+
// Python 环境行:技能脚本的标准依赖。Windows 未装时可一键安装
|
|
530
|
+
// (python.org 官方安装器静默装用户目录),其他平台给包管理器指引。
|
|
530
531
|
const py = renderCli._python;
|
|
531
532
|
const isWin = navigator.platform && /win/i.test(navigator.platform);
|
|
532
533
|
const pythonRow = py
|
|
@@ -538,13 +539,17 @@ const renderCliData = (s, tools, python) => {
|
|
|
538
539
|
+ (py.available
|
|
539
540
|
? '技能脚本运行环境 · ' + esc(py.command) + ' · v' + esc(py.version)
|
|
540
541
|
: isWin
|
|
541
|
-
? '技能脚本运行环境 · 未检测到 Python
|
|
542
|
+
? '技能脚本运行环境 · 未检测到 Python(技能脚本需要 Python 3),点右侧「安装」自动装官方版本(python.org)'
|
|
542
543
|
: '技能脚本运行环境 · 未检测到 Python,可用系统包管理器安装(如 brew install python)后重启 dsh')
|
|
543
|
-
+ '</div></div
|
|
544
|
+
+ '</div></div>'
|
|
545
|
+
+ (!py.available && isWin
|
|
546
|
+
? '<div style="display:flex;gap:6px"><button class="btn small primary" data-cli-install="python">安装</button></div>'
|
|
547
|
+
: '')
|
|
548
|
+
+ '</div>'
|
|
544
549
|
: '';
|
|
545
550
|
body.innerHTML = rows.join('') + pythonRow
|
|
546
551
|
+ '<div class="hint">在对话中直接使用(如 /lark 帮助),由 dsh 自动调用。</div>'
|
|
547
|
-
+ '<div class="hint"
|
|
552
|
+
+ '<div class="hint">未安装时点「安装」自助安装(Python 同样支持);已安装未授权时点「去授权」用飞书扫码完成即可。</div>';
|
|
548
553
|
body.querySelectorAll('[data-cli-upgrade]').forEach((btn) => {
|
|
549
554
|
btn.onclick = () => cliUpgrade(btn.dataset.cliUpgrade, btn);
|
|
550
555
|
});
|
|
@@ -667,11 +672,12 @@ const cliAuthDialog = async (tool) => {
|
|
|
667
672
|
}
|
|
668
673
|
};
|
|
669
674
|
|
|
670
|
-
/** CLI 一键安装:lark 走 npm
|
|
671
|
-
*
|
|
675
|
+
/** CLI 一键安装:lark 走 npm 全局安装(机器无 Node.js 时先自动补装)、
|
|
676
|
+
* ccpg/python 走官方安装器,耗时可达数分钟。响应自带安装后立即探测的
|
|
677
|
+
* 详情,直接合进缓存刷新界面。 */
|
|
672
678
|
const cliInstall = async (tool, btn) => {
|
|
673
679
|
btn.disabled = true;
|
|
674
|
-
btn.textContent = '安装中,可能需要
|
|
680
|
+
btn.textContent = '安装中,可能需要 3–5 分钟…';
|
|
675
681
|
try {
|
|
676
682
|
const resp = await fetch('api/cli/install', {
|
|
677
683
|
method: 'POST',
|
|
@@ -681,7 +687,11 @@ const cliInstall = async (tool, btn) => {
|
|
|
681
687
|
const data = await resp.json().catch(() => ({}));
|
|
682
688
|
if (!resp.ok) throw new Error(data.error || 'CLI 安装失败');
|
|
683
689
|
renderCli._probedAt = 0;
|
|
684
|
-
if (
|
|
690
|
+
if (tool === 'python') {
|
|
691
|
+
// python 无三态:回传 {available,command,version},重绘 Python 行
|
|
692
|
+
if (data.python) renderCliData(lastStatus, {}, data.python);
|
|
693
|
+
else renderCli(lastStatus);
|
|
694
|
+
} else if (data.tool) {
|
|
685
695
|
renderCliData(lastStatus, { [tool]: data.tool });
|
|
686
696
|
} else {
|
|
687
697
|
renderCli(lastStatus);
|
package/lib/proc.js
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
//
|
|
3
3
|
// 官方 `dsh plugin --profile web <args>` 内部就是 pnpm 转发 + profile
|
|
4
4
|
// 初始化 + dsh.profile.bundles reconcile,全部复用。这里只封装进程执行
|
|
5
|
-
// (execFile
|
|
6
|
-
//
|
|
5
|
+
// (execFile 参数数组,不经 shell 拼接;win32 的 .cmd/.bat shim 是唯一
|
|
6
|
+
// 例外——按 CVE-2024-27980 后的 Node 约定经 cmd.exe /d /s /c 执行,
|
|
7
|
+
// 参数仍是数组形态)与错误翻译;包名/版本白名单在 index.js 的调用侧执行。
|
|
7
8
|
import { execFile } from 'node:child_process';
|
|
8
|
-
import { existsSync, readdirSync } from 'node:fs';
|
|
9
|
-
import { homedir } from 'node:os';
|
|
10
|
-
import { join,
|
|
9
|
+
import { existsSync, mkdirSync, readdirSync, renameSync, rmSync } from 'node:fs';
|
|
10
|
+
import { homedir, tmpdir } from 'node:os';
|
|
11
|
+
import { join, isAbsolute, delimiter as pathDelimiter } from 'node:path';
|
|
11
12
|
import { promisify } from 'node:util';
|
|
12
13
|
|
|
13
14
|
const execFileAsync = promisify(execFile);
|
|
@@ -26,9 +27,57 @@ function extraBinDirs() {
|
|
|
26
27
|
if (process.platform === 'win32') {
|
|
27
28
|
const appData = process.env.APPDATA || join(home, 'AppData', 'Roaming');
|
|
28
29
|
const localAppData = process.env.LOCALAPPDATA || join(home, 'AppData', 'Local');
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
const systemRoot = process.env.SystemRoot || 'C:\\Windows';
|
|
31
|
+
const programFiles = process.env.ProgramFiles || 'C:\\Program Files';
|
|
32
|
+
const programFilesX86 = process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)';
|
|
33
|
+
// Windows 系统目录:cmd.exe / curl.exe / powershell 等系统自带命令的
|
|
34
|
+
// 落地位——GUI 宿主 PATH 贫瘠时连 cmd.exe 都 resolve 不到,.cmd shim
|
|
35
|
+
// 的路由与 curl 下载全部瘫痪,必须兑底。
|
|
36
|
+
dirs.push(
|
|
37
|
+
join(systemRoot, 'System32'),
|
|
38
|
+
systemRoot,
|
|
39
|
+
join(systemRoot, 'System32', 'WindowsPowerShell', 'v1.0'),
|
|
40
|
+
);
|
|
41
|
+
// nodejs.org 官方安装器默认系统级安装位(机器级 Program Files;
|
|
42
|
+
// 机器上没有 node.exe 时不会注入 PATH,但兑底目录里如果找到了,
|
|
43
|
+
// 就能解析出 node/npm,不用再要求用户装 Node)+ 本插件一键补装的
|
|
44
|
+
// 用户级安装位(%LOCALAPPDATA%\Programs\nodejs,免管理员)。
|
|
45
|
+
dirs.push(
|
|
46
|
+
join(programFiles, 'nodejs'),
|
|
47
|
+
join(programFilesX86, 'nodejs'),
|
|
48
|
+
join(localAppData, 'Programs', 'nodejs'),
|
|
49
|
+
);
|
|
50
|
+
// npm -g 默认 prefix、pnpm 默认 PNPM_HOME,以及 ccpg-cli Windows 官方
|
|
51
|
+
// 安装器的落点 %USERPROFILE%\bin(本机实测 C:\Users\<user>\bin\
|
|
52
|
+
// ccpg-cli.exe;装完不重启也能立即探测/自升级)。nodejs.org 安装器的
|
|
53
|
+
// 系统级安装不在这些目录,但那种环境 PATH 里有 node,不需要兑底。
|
|
54
|
+
dirs.push(join(appData, 'npm'), join(localAppData, 'pnpm'), join(home, 'bin'));
|
|
55
|
+
// Python 常见安装位(python 探测兑底):python.org 每用户安装位
|
|
56
|
+
// %LOCALAPPDATA%\Programs\Python\Python3xx、机器级 Program Files\
|
|
57
|
+
// Python3xx、Miniconda/Anaconda 默认位。自定义安装位(如
|
|
58
|
+
// E:\VScode\miniconda3)在 PATH 里,不受影响。
|
|
59
|
+
try {
|
|
60
|
+
const pyRoot = join(localAppData, 'Programs', 'Python');
|
|
61
|
+
for (const entry of readdirSync(pyRoot)) {
|
|
62
|
+
if (existsSync(join(pyRoot, entry, 'python.exe'))) dirs.push(join(pyRoot, entry));
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
// 未装 python.org 版:跳过
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
for (const entry of readdirSync(programFiles)) {
|
|
69
|
+
if (/^Python\d+/.test(entry) && existsSync(join(programFiles, entry, 'python.exe'))) {
|
|
70
|
+
dirs.push(join(programFiles, entry));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
// 未装机器级 python.org 版:跳过
|
|
75
|
+
}
|
|
76
|
+
// Miniconda/Anaconda 常见默认位:用户目录与 ProgramData(机器级共享)
|
|
77
|
+
dirs.push(
|
|
78
|
+
join(home, 'miniconda3'), join(home, 'anaconda3'),
|
|
79
|
+
join('C:\\', 'ProgramData', 'miniconda3'), join('C:\\', 'ProgramData', 'Anaconda3'),
|
|
80
|
+
);
|
|
32
81
|
} else {
|
|
33
82
|
try {
|
|
34
83
|
// nvm 目录名形如 v24.18.0,用 compareSemver 取最高版(compareSemver
|
|
@@ -44,25 +93,29 @@ function extraBinDirs() {
|
|
|
44
93
|
}
|
|
45
94
|
// DSH Desktop 自带的 node/pnpm/dsh shim(Electron 当 node 用):宿主只把
|
|
46
95
|
// 这目录注入桌面终端的 PATH,插件子进程看不到——不兑底的话 dsh 自身有
|
|
47
|
-
// node 环境但 lark-cli 安装仍报「未找到 npm
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (process.platform === 'win32') {
|
|
96
|
+
// node 环境但 lark-cli 安装仍报「未找到 npm」。macOS 位
|
|
97
|
+
// ~/Library/Application Support/DSH Desktop/cli/<profile hash>/bin;
|
|
98
|
+
// Windows 实测在 Roaming 下(真实用户机报错的命令路径前缀就是
|
|
99
|
+
// C:\Users\<user>\AppData\Roaming\DSH…),Local 也一并扫。两种布局都
|
|
100
|
+
// 兼容:shims/ 直挂、cli/<hash>/bin。
|
|
101
|
+
const desktopRoots = process.platform === 'win32'
|
|
102
|
+
? [
|
|
103
|
+
join(process.env.APPDATA || join(home, 'AppData', 'Roaming'), 'DSH Desktop'),
|
|
104
|
+
join(process.env.LOCALAPPDATA || join(home, 'AppData', 'Local'), 'DSH Desktop'),
|
|
105
|
+
]
|
|
106
|
+
: [join(home, 'Library', 'Application Support', 'DSH Desktop')];
|
|
107
|
+
for (const root of desktopRoots) {
|
|
60
108
|
try {
|
|
61
|
-
const shimDir = join(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
109
|
+
const shimDir = join(root, 'shims');
|
|
110
|
+
if (existsSync(join(shimDir, 'node')) || existsSync(join(shimDir, 'node.exe')) || existsSync(join(shimDir, 'node.cmd'))) {
|
|
111
|
+
dirs.push(shimDir);
|
|
112
|
+
}
|
|
113
|
+
for (const entry of readdirSync(join(root, 'cli'))) {
|
|
114
|
+
const shimBin = join(root, 'cli', entry, 'bin');
|
|
115
|
+
if (existsSync(join(shimBin, 'node')) || existsSync(join(shimBin, 'node.exe')) || existsSync(join(shimBin, 'node.cmd'))) {
|
|
116
|
+
dirs.push(shimBin);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
66
119
|
} catch {
|
|
67
120
|
// 目录不存在:跳过
|
|
68
121
|
}
|
|
@@ -71,17 +124,82 @@ function extraBinDirs() {
|
|
|
71
124
|
return dirs;
|
|
72
125
|
}
|
|
73
126
|
|
|
127
|
+
/** 安装完成后清空兑底目录缓存:一键安装 python/ccpg 后新目录要立即可
|
|
128
|
+
* 探测(python.org 装到 %LOCALAPPDATA%\Programs\Python\Python3xx),不
|
|
129
|
+
* 能等 DSH Desktop 重启。 */
|
|
130
|
+
export function resetExtraBinDirsCache() {
|
|
131
|
+
extraBinDirsCache = null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** 候选扩展名顺序(纯函数,供测试):win32 下 CreateProcess 只认可执行
|
|
135
|
+
* 映像,npm/pnpm/lark-cli 的安装目录同时存在同名的无扩展名 sh 脚本
|
|
136
|
+
* (给 Git Bash 用)与 .cmd shim——无扩展名必须排最后,否则解析命中 sh
|
|
137
|
+
* 脚本,execFile 直接失败。 */
|
|
138
|
+
export function candidateSuffixes(platform) {
|
|
139
|
+
return platform === 'win32' ? ['.exe', '.cmd', ''] : [''];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** win32 下 .cmd/.bat shim 的 execFile 等价调用形态(纯函数,供测试):
|
|
143
|
+
* Node ≥ 18.20/20.12(CVE-2024-27980 修复,DSH Desktop 内置 Node 同样
|
|
144
|
+
* 受影响)起 execFile 直接执行 .cmd/.bat 会同步抛 spawn EINVAL,必须经
|
|
145
|
+
* cmd.exe /d /s /c 解释。
|
|
146
|
+
* 形态必须整行单参数 + 外层引号 + windowsVerbatimArguments(与 Node
|
|
147
|
+
* shell:true 的内部构造一致):逐参数传数组时,含空格路径(如
|
|
148
|
+
* "C:\Users\x\AppData\Roaming\DSH Desktop\shims\npm.cmd")会被 cmd /s 的
|
|
149
|
+
* 引号剥离规则在空格处拆断,报「不是内部或外部命令」(真实用户机实测)。
|
|
150
|
+
* 参数不拼接用户输入(白名单 tool + 固定 flag + 校验过的设备码/URL)。 */
|
|
151
|
+
export function cmdShimInvocation(platform, resolved, args) {
|
|
152
|
+
if (platform !== 'win32') return null;
|
|
153
|
+
if (!/\.(cmd|bat)$/i.test(resolved)) return null;
|
|
154
|
+
const quote = (s) => (/[\s"]/.test(s) ? `"${String(s).replace(/"/g, '""')}"` : String(s));
|
|
155
|
+
const line = [resolved, ...args].map(quote).join(' ');
|
|
156
|
+
return { file: 'cmd.exe', args: ['/d', '/s', '/c', `"${line}"`], verbatim: true };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** PATH 环境变量键名(纯函数,供测试):win32 下实际键通常是 Path,注入
|
|
160
|
+
* 时必须复用已有键,否则 env 里出现 Path+PATH 两个重复键,子进程读到
|
|
161
|
+
* 哪个取决于实现。 */
|
|
162
|
+
export function pathEnvKey(env, platform) {
|
|
163
|
+
if (platform !== 'win32') return 'PATH';
|
|
164
|
+
return Object.keys(env).find((key) => /^path$/i.test(key)) || 'PATH';
|
|
165
|
+
}
|
|
166
|
+
|
|
74
167
|
/** 命令解析:PATH 里找不到时从兑底目录解析绝对路径(找不到返回 null)。
|
|
75
168
|
* 所有 execFile 调用统一过这里;返回 null 时调用方走「未找到命令」的
|
|
76
|
-
*
|
|
169
|
+
* 中文指引分支。绝对路径原样返回(ccpg 安装器等)。 */
|
|
77
170
|
export function resolveCommandOrNull(tool) {
|
|
171
|
+
if (isAbsolute(tool)) return existsSync(tool) ? tool : null;
|
|
172
|
+
const pathDirs = String(process.env.PATH || '').split(pathDelimiter).filter(Boolean);
|
|
173
|
+
const suffixes = candidateSuffixes(process.platform);
|
|
174
|
+
for (const dir of [...pathDirs, ...extraBinDirs()]) {
|
|
175
|
+
for (const suffix of suffixes) {
|
|
176
|
+
const candidate = join(dir, tool + suffix);
|
|
177
|
+
if (existsSync(candidate)) return candidate;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** 与 resolveCommandOrNull 同口径,但返回全部按序去重候选。python 这类
|
|
184
|
+
* 探测必须逐个候选试:Windows PATH 前段常是 Microsoft Store 的假别名
|
|
185
|
+
* (0 字节重解析点,执行退出码 9009),真正的解释器(miniconda /
|
|
186
|
+
* python.org)排在更后面——只试首个命中会误报未安装。 */
|
|
187
|
+
export function resolveCommandCandidates(tool) {
|
|
188
|
+
if (isAbsolute(tool)) return existsSync(tool) ? [tool] : [];
|
|
189
|
+
const seen = new Set();
|
|
190
|
+
const out = [];
|
|
78
191
|
const pathDirs = String(process.env.PATH || '').split(pathDelimiter).filter(Boolean);
|
|
79
|
-
const
|
|
192
|
+
const suffixes = candidateSuffixes(process.platform);
|
|
80
193
|
for (const dir of [...pathDirs, ...extraBinDirs()]) {
|
|
81
|
-
|
|
82
|
-
|
|
194
|
+
for (const suffix of suffixes) {
|
|
195
|
+
const candidate = join(dir, tool + suffix);
|
|
196
|
+
if (existsSync(candidate) && !seen.has(candidate)) {
|
|
197
|
+
seen.add(candidate);
|
|
198
|
+
out.push(candidate);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
83
201
|
}
|
|
84
|
-
return
|
|
202
|
+
return out;
|
|
85
203
|
}
|
|
86
204
|
|
|
87
205
|
/** 兼容旧签名:找不到时返回原名(后续 ENOENT 走既有 missing/降级语义)。 */
|
|
@@ -89,21 +207,55 @@ export function resolveCommand(tool) {
|
|
|
89
207
|
return resolveCommandOrNull(tool) || tool;
|
|
90
208
|
}
|
|
91
209
|
|
|
92
|
-
/**
|
|
93
|
-
*
|
|
94
|
-
*
|
|
210
|
+
/** 子进程输出解码(纯函数,供测试):Windows 中文系统的 cmd/批处理内部
|
|
211
|
+
* 错误是 GBK 编码(如「'xx' 不是内部或外部命令」),而 npm/node 自身输出
|
|
212
|
+
* 是 UTF-8——先按 UTF-8 严格解码,失败退化 GBK,乱码(♦/锟斤拷)不进
|
|
213
|
+
* 用户可见的错误提示。 */
|
|
214
|
+
export function decodeOutput(buf) {
|
|
215
|
+
const bytes = Buffer.isBuffer(buf) ? buf : Buffer.from(String(buf ?? ''));
|
|
216
|
+
try {
|
|
217
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
218
|
+
} catch {
|
|
219
|
+
try {
|
|
220
|
+
return new TextDecoder('gbk').decode(bytes);
|
|
221
|
+
} catch {
|
|
222
|
+
return bytes.toString('utf-8');
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** 统一的子进程执行:解析命令路径,并把**所有兑底目录**前置进子进程
|
|
228
|
+
* PATH——win32 下 npm/lark-cli 是 .cmd shim(`%~dp0\node.exe` 找 node),
|
|
229
|
+
* node 可能在另一个兑底目录(nodejs.org 系统安装位);.cmd 又经
|
|
230
|
+
* cmd.exe /d /s /c 路由,cmd.exe 在第三个兑底目录(System32)。只前置
|
|
231
|
+
* 解析到的那个目录会让 shim 内部找不到依赖,必须整组前置。输出按
|
|
232
|
+
* encoding:buffer 收回后 decodeOutput(GBK 容错)。opts.env 只传增量变量。 */
|
|
95
233
|
function execResolved(tool, args, { timeoutMs, cwd, env } = {}) {
|
|
96
234
|
const resolved = resolveCommand(tool);
|
|
97
235
|
const childEnv = { ...process.env, ...(env || {}) };
|
|
98
|
-
|
|
99
|
-
|
|
236
|
+
const key = pathEnvKey(childEnv, process.platform);
|
|
237
|
+
const extras = extraBinDirs().filter((dir) => existsSync(dir));
|
|
238
|
+
if (extras.length) {
|
|
239
|
+
childEnv[key] = [...extras, childEnv[key] || ''].filter(Boolean).join(pathDelimiter);
|
|
100
240
|
}
|
|
101
|
-
|
|
241
|
+
const shim = cmdShimInvocation(process.platform, resolved, args);
|
|
242
|
+
return execFileAsync(shim ? shim.file : resolved, shim ? shim.args : args, {
|
|
102
243
|
timeout: timeoutMs,
|
|
103
244
|
killSignal: 'SIGKILL',
|
|
104
245
|
cwd,
|
|
105
246
|
env: childEnv,
|
|
106
|
-
|
|
247
|
+
encoding: 'buffer',
|
|
248
|
+
windowsVerbatimArguments: shim?.verbatim === true,
|
|
249
|
+
}).then(
|
|
250
|
+
({ stdout, stderr }) => ({ stdout: decodeOutput(stdout), stderr: decodeOutput(stderr) }),
|
|
251
|
+
(error) => {
|
|
252
|
+
if (error && (error.stdout !== undefined || error.stderr !== undefined)) {
|
|
253
|
+
error.stdout = decodeOutput(error.stdout);
|
|
254
|
+
error.stderr = decodeOutput(error.stderr);
|
|
255
|
+
}
|
|
256
|
+
throw error;
|
|
257
|
+
},
|
|
258
|
+
);
|
|
107
259
|
}
|
|
108
260
|
|
|
109
261
|
/** 从 pnpm 冗长输出里提取对用户有意义的错误行。 */
|
|
@@ -174,6 +326,11 @@ function missingCommandGuidance(tool) {
|
|
|
174
326
|
if (tool === 'dsh') {
|
|
175
327
|
return '未找到 dsh 命令,请确认已安装 dsh 并在终端可用';
|
|
176
328
|
}
|
|
329
|
+
if (tool === 'python' || tool === 'python3' || tool === 'py') {
|
|
330
|
+
return process.platform === 'win32'
|
|
331
|
+
? '未检测到 Python:技能脚本需要 Python 3 运行环境,点「安装」自动装官方版本(python.org),或自行从 https://www.python.org 下载安装(勾选 Add to PATH)后重启 DSH Desktop'
|
|
332
|
+
: '未检测到 Python:技能脚本需要 Python 3 运行环境,请用系统包管理器安装(如 brew install python / apt install python3)后重启 dsh';
|
|
333
|
+
}
|
|
177
334
|
return `未找到 ${tool} 命令,请确认已安装`;
|
|
178
335
|
}
|
|
179
336
|
|
|
@@ -326,6 +483,31 @@ async function cliLatestVersion(tool, timeoutMs) {
|
|
|
326
483
|
return null;
|
|
327
484
|
}
|
|
328
485
|
|
|
486
|
+
/**
|
|
487
|
+
* Python 环境探测(技能脚本的标准依赖)。win32 优先官方 py 启动器,再
|
|
488
|
+
* python / python3;每个名字按 resolveCommandCandidates 逐个候选试
|
|
489
|
+
* `--version`(穿透 Store 假别名)。Python 2.x 不算可用——技能脚本需要
|
|
490
|
+
* 3.x,继续找下一个候选。与平台 instance_service 的口径一致:探测失败
|
|
491
|
+
* 不抛错,返回 available:false 由前端给安装指引/按钮。
|
|
492
|
+
*/
|
|
493
|
+
export async function probePythonEnv({ timeoutMs = 15_000 } = {}) {
|
|
494
|
+
const names = process.platform === 'win32' ? ['py', 'python', 'python3'] : ['python3', 'python'];
|
|
495
|
+
for (const name of names) {
|
|
496
|
+
for (const candidate of resolveCommandCandidates(name)) {
|
|
497
|
+
try {
|
|
498
|
+
const { stdout, stderr } = await runCliTool(candidate, ['--version'], { timeoutMs });
|
|
499
|
+
const match = /(\d+)\.(\d+)(?:\.(\d+))?/.exec(`${stdout || ''}${stderr || ''}`);
|
|
500
|
+
if (match && Number(match[1]) >= 3) {
|
|
501
|
+
return { available: true, command: name, version: match[0] };
|
|
502
|
+
}
|
|
503
|
+
} catch {
|
|
504
|
+
// Store 假别名(9009)/ 不可执行 / 超时:试下一个候选
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
return { available: false, command: null, version: null };
|
|
509
|
+
}
|
|
510
|
+
|
|
329
511
|
/** ccpg-cli 官方安装脚本地址(与平台侧 cli-deployd-go InstallCCPGCLI 同源,
|
|
330
512
|
* 保持两端口径一致;auto-install.sh 自带二进制下载与 PATH 写入)。 */
|
|
331
513
|
const CCPG_CLI_INSTALL_URL_PREFIX = 'http://bms.einwin.com/media/ccpg-cli/202605091513';
|
|
@@ -360,17 +542,22 @@ async function installLarkSkills({ timeoutMs = 180_000 } = {}) {
|
|
|
360
542
|
* cli-deployd-go 的 install 口径一致。
|
|
361
543
|
* 返回输出尾部给前端展示。只接受 CLI_TOOLS 白名单内的 tool。
|
|
362
544
|
*/
|
|
363
|
-
export async function installCliTool(tool, { timeoutMs =
|
|
545
|
+
export async function installCliTool(tool, { timeoutMs = 600_000 } = {}) {
|
|
364
546
|
let stdout;
|
|
365
547
|
if (tool === 'lark-cli') {
|
|
366
548
|
// 预检+降级链:优先独立 npm;没有 npm 但有 pnpm(含 DSH Desktop 自带
|
|
367
|
-
// shim)时用 pnpm
|
|
368
|
-
//
|
|
369
|
-
//
|
|
549
|
+
// shim)时用 pnpm 全局装;两者都没有时 Windows 先一键补装 Node.js
|
|
550
|
+
// (用户目录免管理员,npmmirror 镜像),再用其自带 npm 装 lark-cli。
|
|
551
|
+
// lark-cli 官方支持 pnpm 安装方式,装出来的二进制与 npm 路径一致,
|
|
552
|
+
// 后续 lark-cli update 也能自升级。
|
|
370
553
|
if (resolveCommandOrNull('npm')) {
|
|
371
554
|
({ stdout } = await runCliTool('npm', ['install', '-g', '@larksuite/cli@latest'], { timeoutMs }));
|
|
372
555
|
} else if (resolveCommandOrNull('pnpm')) {
|
|
373
556
|
({ stdout } = await runCliTool('pnpm', ['add', '-g', '@larksuite/cli@latest'], { timeoutMs }));
|
|
557
|
+
} else if (process.platform === 'win32') {
|
|
558
|
+
const nodeNote = await installNodeWindows(timeoutMs);
|
|
559
|
+
({ stdout } = await runCliTool('npm', ['install', '-g', '@larksuite/cli@latest'], { timeoutMs }));
|
|
560
|
+
stdout = `${nodeNote}\n${stdout}`;
|
|
374
561
|
} else {
|
|
375
562
|
throw new Error(missingCommandGuidance('npm'));
|
|
376
563
|
}
|
|
@@ -399,6 +586,12 @@ export async function installCliTool(tool, { timeoutMs = 300_000 } = {}) {
|
|
|
399
586
|
env: { CCPG_CLI_URL_PREFIX: CCPG_CLI_INSTALL_URL_PREFIX },
|
|
400
587
|
}));
|
|
401
588
|
}
|
|
589
|
+
resetExtraBinDirsCache();
|
|
590
|
+
} else if (tool === 'python') {
|
|
591
|
+
if (process.platform !== 'win32') {
|
|
592
|
+
throw new Error('请用系统包管理器安装 Python(如 brew install python / apt install python3)后重启 dsh');
|
|
593
|
+
}
|
|
594
|
+
({ stdout } = await installPythonWindows(timeoutMs));
|
|
402
595
|
} else {
|
|
403
596
|
throw new Error('不支持的 CLI 工具');
|
|
404
597
|
}
|
|
@@ -406,44 +599,206 @@ export async function installCliTool(tool, { timeoutMs = 300_000 } = {}) {
|
|
|
406
599
|
return lines.slice(-6).join('\n');
|
|
407
600
|
}
|
|
408
601
|
|
|
409
|
-
/** ccpg-cli Windows
|
|
410
|
-
* 1)
|
|
411
|
-
*
|
|
412
|
-
*
|
|
413
|
-
*
|
|
602
|
+
/** ccpg-cli Windows 安装(与 mac/Linux auto-install.sh 同构:裸二进制落地):
|
|
603
|
+
* 1) 动态查询官方更新接口拿当前二进制地址——媒体目录按日期滚动,pin 死
|
|
604
|
+
* 的旧地址会被服务器清理(实测教程里的 202607151817 已 404);接口与
|
|
605
|
+
* ccpg-cli 自身 updater 同源,永远指向当前有效版本。接口返回的 URL
|
|
606
|
+
* 必须落在 bms.einwin.com/media/ccpg-cli/ 前缀下才接受(防接口被篡
|
|
607
|
+
* 改指向恶意地址),接口异常时兜底最近一次验证过的 pin 值。
|
|
608
|
+
* 注意:接口给的是**裸二进制本体**(实测与 update 落地文件逐字节一
|
|
609
|
+
* 致),不是安装器——落地即用,无 /S 静默安装一说。
|
|
610
|
+
* 2) curl 下载到 %USERPROFILE%\bin\ccpg-cli.exe(先下暂存再改名,原子)
|
|
611
|
+
* 3) 校验 --version 可执行(PE 完整、架构匹配)
|
|
612
|
+
* 4) %USERPROFILE%\bin 写入用户 PATH(注册表 User 作用域,免管理员)
|
|
613
|
+
* 5) 教程必做步骤 ccpg-cli update(刚下的就是 latest,通常 no-op,软失败)
|
|
414
614
|
* 全程 execFile 参数数组,不经 shell。 */
|
|
415
|
-
const
|
|
615
|
+
const CCPG_CLI_UPDATE_ENDPOINT = 'https://bms.einwin.com/mdm-third-party/ccpg-cli/latest?os=windows&arch=amd64';
|
|
616
|
+
const CCPG_CLI_WINDOWS_URL_PREFIX = 'https://bms.einwin.com/media/ccpg-cli/';
|
|
617
|
+
const CCPG_CLI_WINDOWS_FALLBACK_URL = `${CCPG_CLI_WINDOWS_URL_PREFIX}202608271055/ccpg-cli-windows-amd64.exe`;
|
|
618
|
+
|
|
619
|
+
async function resolveCcpgWindowsDownloadUrl() {
|
|
620
|
+
try {
|
|
621
|
+
const { stdout } = await runCliTool('curl', [
|
|
622
|
+
'-fsSL', '--max-time', '30', CCPG_CLI_UPDATE_ENDPOINT,
|
|
623
|
+
], { timeoutMs: 40_000 });
|
|
624
|
+
const url = JSON.parse(String(stdout).trim())?.value?.url;
|
|
625
|
+
if (typeof url === 'string' && url.startsWith(CCPG_CLI_WINDOWS_URL_PREFIX)) return url;
|
|
626
|
+
} catch {
|
|
627
|
+
// 更新接口异常:走兜底 pin 值
|
|
628
|
+
}
|
|
629
|
+
return CCPG_CLI_WINDOWS_FALLBACK_URL;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/** 把目录追加进用户 PATH(注册表 User 作用域,新开终端生效)。已存在则
|
|
633
|
+
* 跳过。用 powershell [Environment] API——setx 会静默截断超过 1024 字符
|
|
634
|
+
* 的 PATH,不用。 */
|
|
635
|
+
async function ensureUserPathContains(dir) {
|
|
636
|
+
const safeDir = dir.replace(/'/g, "''");
|
|
637
|
+
const script = [
|
|
638
|
+
"$p=[Environment]::GetEnvironmentVariable('Path','User')",
|
|
639
|
+
`if(($p -split ';') -notcontains '${safeDir}'){[Environment]::SetEnvironmentVariable('Path',($p.TrimEnd(';')+';${safeDir}'),'User');'added'}else{'exists'}`,
|
|
640
|
+
].join(';');
|
|
641
|
+
const { stdout } = await runCliTool('powershell', ['-NoProfile', '-Command', script], { timeoutMs: 30_000 });
|
|
642
|
+
return String(stdout).includes('added')
|
|
643
|
+
? '已把 %USERPROFILE%\\bin 写入用户 PATH(新开的终端生效)'
|
|
644
|
+
: '%USERPROFILE%\\bin 已在用户 PATH 中';
|
|
645
|
+
}
|
|
416
646
|
|
|
417
647
|
async function installCcpgCliWindows(timeoutMs) {
|
|
418
|
-
const
|
|
419
|
-
const
|
|
648
|
+
const binDir = join(homedir(), 'bin');
|
|
649
|
+
const target = join(binDir, 'ccpg-cli.exe');
|
|
650
|
+
mkdirSync(binDir, { recursive: true });
|
|
651
|
+
const url = await resolveCcpgWindowsDownloadUrl();
|
|
652
|
+
const staging = join(binDir, 'ccpg-cli.exe.download');
|
|
420
653
|
await runCliTool('curl', [
|
|
421
|
-
'-fsSL', '--retry', '3', '--retry-delay', '5', '--max-time', '
|
|
422
|
-
'-
|
|
654
|
+
'-fsSL', '--retry', '3', '--retry-delay', '5', '--max-time', '600',
|
|
655
|
+
'--speed-limit', '65536', '--speed-time', '30',
|
|
656
|
+
'-o', staging, url,
|
|
423
657
|
], { timeoutMs });
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
658
|
+
renameSync(staging, target);
|
|
659
|
+
// 校验二进制可执行(PE 完整、架构匹配)
|
|
660
|
+
const { stdout: versionOut } = await runCliTool(target, ['--version'], { timeoutMs: 30_000 });
|
|
661
|
+
resetExtraBinDirsCache();
|
|
662
|
+
const pathNote = await ensureUserPathContains(binDir)
|
|
663
|
+
.catch(() => 'PATH 写入失败,请手动把 %USERPROFILE%\\bin 加入用户 PATH');
|
|
664
|
+
// 官方教程的必做步骤:装完立即自升级(刚下的就是 latest,通常 no-op;
|
|
665
|
+
// 软失败不阻断)
|
|
666
|
+
const updateNote = await runCliTool('ccpg-cli', ['update'], {
|
|
667
|
+
timeoutMs: 120_000,
|
|
668
|
+
env: { CCPG_FULL_CLI: '1' },
|
|
669
|
+
})
|
|
428
670
|
.then((r) => String(r.stdout || '').trim().split('\n').slice(-2).join('\n'))
|
|
429
671
|
.catch(() => 'ccpg-cli update 未完成,安装后请手动执行 ccpg-cli update');
|
|
430
672
|
return {
|
|
431
|
-
stdout: `ccpg-cli
|
|
673
|
+
stdout: `ccpg-cli 安装完成(${String(versionOut).trim()})\n${updateNote}\n${pathNote}\n如状态页仍显示未安装,请重启 DSH Desktop 后点「重新检测」`,
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/** Python Windows 一键安装(python.org 官方安装器口径):
|
|
678
|
+
* 1) Windows 10 1803+ 自带 curl.exe 下载官方安装器——**国内镜像优先**
|
|
679
|
+
* (python.org 官网源在国内动辄超时,实测华为云镜像 2MB/s、官网源
|
|
680
|
+
* 300s 拉不完 45MB),镜像目录结构与官网一致、按版本号直链;
|
|
681
|
+
* 镜像不可用时退化官网源(海外用户/镜像异常)
|
|
682
|
+
* 2) 静默每用户安装:InstallAllUsers=0(无需 UAC/管理员)、
|
|
683
|
+
* PrependPath=1(安装器直接写入用户 PATH——不勾选的话 GUI 宿主
|
|
684
|
+
* 要等重启才能看到 python.exe)
|
|
685
|
+
* 3) 装完清兑底目录缓存,本进程内即可探测到新解释器
|
|
686
|
+
* 全程 execFile 参数数组,不经 shell。 */
|
|
687
|
+
const PYTHON_WINDOWS_INSTALLER_VERSION = '3.13.6';
|
|
688
|
+
const PYTHON_WINDOWS_INSTALLER_URLS = [
|
|
689
|
+
`https://mirrors.huaweicloud.com/python/${PYTHON_WINDOWS_INSTALLER_VERSION}/python-${PYTHON_WINDOWS_INSTALLER_VERSION}-amd64.exe`,
|
|
690
|
+
`https://www.python.org/ftp/python/${PYTHON_WINDOWS_INSTALLER_VERSION}/python-${PYTHON_WINDOWS_INSTALLER_VERSION}-amd64.exe`,
|
|
691
|
+
];
|
|
692
|
+
|
|
693
|
+
async function installPythonWindows(timeoutMs) {
|
|
694
|
+
const installer = join(tmpdir(), 'python-setup.exe');
|
|
695
|
+
// 逐个源下载:curl 失败(非 2xx/超时/低速假死)换下一个源。
|
|
696
|
+
// --speed-limit/--speed-time:30 秒内速度低于 64KB/s 视为假死换源
|
|
697
|
+
// (官网源在国内常见形态不是断连而是 kb 级慢速拖死)。
|
|
698
|
+
let downloaded = false;
|
|
699
|
+
let lastError = null;
|
|
700
|
+
for (const url of PYTHON_WINDOWS_INSTALLER_URLS) {
|
|
701
|
+
try {
|
|
702
|
+
await runCliTool('curl', [
|
|
703
|
+
'-fsSL', '--retry', '2', '--retry-delay', '3', '--max-time', '600',
|
|
704
|
+
'--speed-limit', '65536', '--speed-time', '30',
|
|
705
|
+
'-o', installer, url,
|
|
706
|
+
], { timeoutMs });
|
|
707
|
+
downloaded = true;
|
|
708
|
+
break;
|
|
709
|
+
} catch (error) {
|
|
710
|
+
lastError = error;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
if (!downloaded) throw lastError || new Error('安装包下载失败,请检查网络后重试');
|
|
714
|
+
// python.org 安装器静默模式:/quiet + 每用户安装 + 写入用户 PATH
|
|
715
|
+
await runCliTool(installer, [
|
|
716
|
+
'/quiet', 'InstallAllUsers=0', 'PrependPath=1', 'Include_test=0',
|
|
717
|
+
], { timeoutMs });
|
|
718
|
+
resetExtraBinDirsCache();
|
|
719
|
+
// 立即复探(本进程内兑底目录已含 python.org 每用户安装位)
|
|
720
|
+
const probe = await probePythonEnv().catch(() => ({ available: false }));
|
|
721
|
+
return {
|
|
722
|
+
stdout: probe.available
|
|
723
|
+
? `Python ${probe.version} 安装完成(${probe.command})。如技能页仍提示未安装,请重启 DSH Desktop 后点「重新检测」`
|
|
724
|
+
: `Python ${PYTHON_WINDOWS_INSTALLER_VERSION} 安装完成。安装器已写入用户 PATH,请重启 DSH Desktop 后点「重新检测」`,
|
|
432
725
|
};
|
|
433
726
|
}
|
|
434
727
|
|
|
728
|
+
/** Node.js Windows 一键补装(lark-cli 的运行时依赖:机器无 node/npm/pnpm
|
|
729
|
+
* 时自动补,用户不再被要求「先自行安装 Node.js」):
|
|
730
|
+
* 1) npmmirror 二进制镜像优先(官网 nodejs.org 国内慢/不稳),失败退官网;
|
|
731
|
+
* zip 包绿色解压,不走 MSI(免管理员/UAC)
|
|
732
|
+
* 2) 解压到 %LOCALAPPDATA%\Programs\nodejs(与 python.org 每用户安装同
|
|
733
|
+
* 级的标准位置),写用户 PATH(注册表 User 作用域)
|
|
734
|
+
* 3) npm 全局 prefix 即该目录,后续装的 lark-cli 也落这里——兑底目录已
|
|
735
|
+
* 覆盖,装完本进程内即可探测
|
|
736
|
+
* 全程 execFile 参数数组,不经 shell。 */
|
|
737
|
+
const NODE_WINDOWS_VERSION = 'v22.17.0';
|
|
738
|
+
const NODE_WINDOWS_ZIP_URLS = [
|
|
739
|
+
`https://registry.npmmirror.com/-/binary/node/${NODE_WINDOWS_VERSION}/node-${NODE_WINDOWS_VERSION}-win-x64.zip`,
|
|
740
|
+
`https://nodejs.org/dist/${NODE_WINDOWS_VERSION}/node-${NODE_WINDOWS_VERSION}-win-x64.zip`,
|
|
741
|
+
];
|
|
742
|
+
|
|
743
|
+
async function installNodeWindows(timeoutMs) {
|
|
744
|
+
const localAppData = process.env.LOCALAPPDATA || join(homedir(), 'AppData', 'Local');
|
|
745
|
+
const programsDir = join(localAppData, 'Programs');
|
|
746
|
+
const nodeHome = join(programsDir, 'nodejs');
|
|
747
|
+
const zip = join(tmpdir(), 'node-setup.zip');
|
|
748
|
+
let downloaded = false;
|
|
749
|
+
let lastError = null;
|
|
750
|
+
for (const url of NODE_WINDOWS_ZIP_URLS) {
|
|
751
|
+
try {
|
|
752
|
+
await runCliTool('curl', [
|
|
753
|
+
'-fsSL', '--retry', '2', '--retry-delay', '3', '--max-time', '600',
|
|
754
|
+
'--speed-limit', '65536', '--speed-time', '30',
|
|
755
|
+
'-o', zip, url,
|
|
756
|
+
], { timeoutMs });
|
|
757
|
+
downloaded = true;
|
|
758
|
+
break;
|
|
759
|
+
} catch (error) {
|
|
760
|
+
lastError = error;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
if (!downloaded) throw lastError || new Error('Node.js 安装包下载失败,请检查网络后重试');
|
|
764
|
+
mkdirSync(programsDir, { recursive: true });
|
|
765
|
+
// PowerShell Expand-Archive(Win10+ 自带)。路径单引号内嵌转义防空格/引号
|
|
766
|
+
const psQuote = (p) => `'${p.replace(/'/g, "''")}'`;
|
|
767
|
+
await runCliTool('powershell', [
|
|
768
|
+
'-NoProfile', '-Command',
|
|
769
|
+
`Expand-Archive -LiteralPath ${psQuote(zip)} -DestinationPath ${psQuote(programsDir)} -Force`,
|
|
770
|
+
], { timeoutMs });
|
|
771
|
+
// zip 内层目录名 node-vX.Y.Z-win-x64 → 平铺为 nodejs(重装时先清旧目录)
|
|
772
|
+
rmSync(nodeHome, { recursive: true, force: true });
|
|
773
|
+
renameSync(join(programsDir, `node-${NODE_WINDOWS_VERSION}-win-x64`), nodeHome);
|
|
774
|
+
rmSync(zip, { force: true });
|
|
775
|
+
await ensureUserPathContains(nodeHome).catch(() => {});
|
|
776
|
+
resetExtraBinDirsCache();
|
|
777
|
+
return `Node.js ${NODE_WINDOWS_VERSION} 安装完成(${nodeHome})`;
|
|
778
|
+
}
|
|
779
|
+
|
|
435
780
|
/**
|
|
436
781
|
* 触发 CLI 官方自升级(`tool update`):ccpg-cli 从更新服务器下载二进制
|
|
437
|
-
* (~17MB
|
|
782
|
+
* (~17MB),新版 lark-cli 自动探测 npm/pnpm/手动安装方式(手动时只打印
|
|
438
783
|
* GitHub 下载地址)。返回输出尾部给前端展示(含手动安装的下载链接)。
|
|
784
|
+
* 兜底:老版本 lark-cli 没有 update 子命令(实测 1.0.0 报 unknown
|
|
785
|
+
* command),此时「重装即升级」——走 installCliTool 的 npm/pnpm 全局装
|
|
786
|
+
* 最新版链路,覆盖旧版。
|
|
439
787
|
*/
|
|
440
788
|
export async function upgradeCliTool(tool, { timeoutMs = 300_000 } = {}) {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
789
|
+
try {
|
|
790
|
+
const { stdout } = await runCliTool(tool, ['update'], {
|
|
791
|
+
timeoutMs,
|
|
792
|
+
env: tool === 'ccpg-cli' ? { CCPG_FULL_CLI: '1' } : undefined,
|
|
793
|
+
});
|
|
794
|
+
const lines = String(stdout).split('\n').map((l) => l.trim()).filter(Boolean);
|
|
795
|
+
return lines.slice(-6).join('\n');
|
|
796
|
+
} catch (error) {
|
|
797
|
+
if (tool === 'lark-cli' && /unknown command/i.test(String(error?.message || ''))) {
|
|
798
|
+
return installCliTool('lark-cli', { timeoutMs });
|
|
799
|
+
}
|
|
800
|
+
throw error;
|
|
801
|
+
}
|
|
447
802
|
}
|
|
448
803
|
|
|
449
804
|
/** lark-cli 形态:identities 下的 tokenStatus 或新版 status/available 组合。 */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-agentone",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.24",
|
|
4
4
|
"description": "AgentOne 平台集成插件:飞书登录、平台模型(同步失败自愈重试)、SkillHub 技能(升级检测一键升级、装后即时刷新、套餐技能自动同步)、套餐申请、插件管理与自助升级、lark-cli / ccpg-cli 探测与一键安装(请求超时与错误码透传、令牌轮换 single-flight、凭据损坏容错与并发安全写入、状态页 UX 增强)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|