dsh-agentone 0.5.21 → 0.5.23
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 +56 -16
- package/lib/page.js +48 -10
- package/lib/proc.js +445 -61
- 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, probeCliDetail, pnpmViewLatest, 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
|
|
|
@@ -300,6 +300,11 @@ export function desktopRunPlugin(service, invokingDir) {
|
|
|
300
300
|
await sleep(2000 * attempt);
|
|
301
301
|
continue;
|
|
302
302
|
}
|
|
303
|
+
// pnpm store 主版本冲突(Windows 桌面版高发:用户全局 pnpm 与
|
|
304
|
+
// 受管 pnpm 版本不同):给可操作指引,不裸抛英文堆栈
|
|
305
|
+
if (isUnexpectedStoreError(`${stderr}\n${stdout}`)) {
|
|
306
|
+
throw new Error(unexpectedStoreGuidance());
|
|
307
|
+
}
|
|
303
308
|
// ERR_PNPM_IGNORED_BUILDS 等非零退出在这里抛出,由调用侧
|
|
304
309
|
// install/upgrade 的「以落盘版本为准」兜底接管
|
|
305
310
|
throw new Error(message || `命令执行失败(退出码 ${outcome.exitCode ?? '未知'})`);
|
|
@@ -365,6 +370,21 @@ function specLabel(spec) {
|
|
|
365
370
|
|
|
366
371
|
/** dsh 插件与 CLI 操作白名单:用户输入不得进入 execFile 的 tool 位。 */
|
|
367
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
|
+
}
|
|
368
388
|
|
|
369
389
|
/**
|
|
370
390
|
* 插件自身是否由平台托管镜像管理:镜像 Dockerfile 固有 ENV
|
|
@@ -807,8 +827,22 @@ export function apply(ctx, config) {
|
|
|
807
827
|
// 失败自愈:模型同步残留错误时由轮询驱动后台重试(成功即清,
|
|
808
828
|
// 下次轮询状态页恢复);错误已被登录/手动同步清除时取消待定
|
|
809
829
|
// 重试并复位退避计数。
|
|
810
|
-
if (credentials.model_error)
|
|
811
|
-
|
|
830
|
+
if (credentials.model_error) {
|
|
831
|
+
// 套餐已生效(active)却残留 model_error:多半是审批前同步失败
|
|
832
|
+
// 的旧错误(plan_required),此时重试必然成功——立即同步一次,
|
|
833
|
+
// 不走首轮 60s 的指数退避。常量幂等:同步成功清掉错误后不再进。
|
|
834
|
+
const planActive = capabilities?.plan_state === 'active';
|
|
835
|
+
if (planActive) {
|
|
836
|
+
syncPlatformModel(config, log)
|
|
837
|
+
.then(() => {
|
|
838
|
+
clearModelRetry();
|
|
839
|
+
modelRetryAttempts = 0;
|
|
840
|
+
})
|
|
841
|
+
.catch(() => scheduleModelRetry(config, log));
|
|
842
|
+
} else {
|
|
843
|
+
scheduleModelRetry(config, log);
|
|
844
|
+
}
|
|
845
|
+
} else {
|
|
812
846
|
clearModelRetry();
|
|
813
847
|
modelRetryAttempts = 0;
|
|
814
848
|
}
|
|
@@ -1191,12 +1225,15 @@ export function apply(ctx, config) {
|
|
|
1191
1225
|
}
|
|
1192
1226
|
// 真实探测本机 CLI:三态与平台 cli-deployd 判定口径一致,附带版本
|
|
1193
1227
|
// 与最新版(查询失败降级 null,不阻塞三态)。并发探测,单个失败不影
|
|
1194
|
-
//
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1228
|
+
// 响另一个。附带 Python 环境探测(技能脚本标准依赖,Windows GUI 启动
|
|
1229
|
+
// 时 PATH 常缺 python 或前段是 Store 假别名,probePythonEnv 逐个候选
|
|
1230
|
+
// 穿透重试),缺失时前端给安装按钮/指引。
|
|
1231
|
+
const [lark, ccpg, python] = await Promise.all([
|
|
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 })),
|
|
1198
1235
|
]);
|
|
1199
|
-
sendJson(response, 200, { tools: { 'lark-cli': lark, 'ccpg-cli': ccpg } });
|
|
1236
|
+
sendJson(response, 200, { tools: { 'lark-cli': lark, 'ccpg-cli': ccpg }, python });
|
|
1200
1237
|
});
|
|
1201
1238
|
|
|
1202
1239
|
register('/agentone/api/cli/install', async (request, response) => {
|
|
@@ -1208,15 +1245,18 @@ export function apply(ctx, config) {
|
|
|
1208
1245
|
try {
|
|
1209
1246
|
const body = await readJsonBody(request);
|
|
1210
1247
|
const tool = String(body.tool || '');
|
|
1211
|
-
if (!CLI_TOOLS.has(tool)) {
|
|
1248
|
+
if (!CLI_TOOLS.has(tool) && !CLI_INSTALL_TOOLS.has(tool)) {
|
|
1212
1249
|
sendJson(response, 400, { error: '不支持的 CLI 工具' });
|
|
1213
1250
|
return;
|
|
1214
1251
|
}
|
|
1215
|
-
// 自助安装(lark 走 npm、ccpg
|
|
1216
|
-
//
|
|
1252
|
+
// 自助安装(lark 走 npm、ccpg 走官方安装器、python 走 python.org
|
|
1253
|
+
// 官方安装器静默装),与升级一样分钟级耗时:输出尾部透传给前端展示
|
|
1217
1254
|
const output = await installCliTool(tool);
|
|
1218
|
-
|
|
1219
|
-
|
|
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 });
|
|
1220
1260
|
} catch (error) {
|
|
1221
1261
|
sendJson(response, 502, { error: error?.message || 'CLI 安装失败,请稍后重试' });
|
|
1222
1262
|
}
|
|
@@ -1238,7 +1278,7 @@ export function apply(ctx, config) {
|
|
|
1238
1278
|
// 官方自升级(ccpg 下载二进制可达分钟级),输出尾部透传给前端
|
|
1239
1279
|
// (lark-cli 手动安装时含 GitHub 下载地址)
|
|
1240
1280
|
const output = await upgradeCliTool(tool);
|
|
1241
|
-
const detail = await probeCliDetail(tool).catch(() => null);
|
|
1281
|
+
const detail = await probeCliDetail(tool).then(withCliUpgradeGuard).catch(() => null);
|
|
1242
1282
|
sendJson(response, 200, { ok: true, output, tool: detail });
|
|
1243
1283
|
} catch (error) {
|
|
1244
1284
|
sendJson(response, 502, { error: error?.message || 'CLI 升级失败,请稍后重试' });
|
|
@@ -1262,7 +1302,7 @@ export function apply(ctx, config) {
|
|
|
1262
1302
|
sendJson(response, 400, { error: '不支持的 CLI 工具' });
|
|
1263
1303
|
return;
|
|
1264
1304
|
}
|
|
1265
|
-
const detail = await probeCliDetail(tool).catch(() => null);
|
|
1305
|
+
const detail = await probeCliDetail(tool).then(withCliUpgradeGuard).catch(() => null);
|
|
1266
1306
|
if (detail?.state === 'missing') {
|
|
1267
1307
|
sendJson(response, 400, { error: 'CLI 未安装,请先安装或联系管理员' });
|
|
1268
1308
|
return;
|
|
@@ -1312,7 +1352,7 @@ export function apply(ctx, config) {
|
|
|
1312
1352
|
// lark-cli 同时轮同一码。这里 25s 一掐,前端串行重试(见 page.js)。
|
|
1313
1353
|
await completeCliAuth(tool, deviceCode, { timeoutMs: 25_000 });
|
|
1314
1354
|
pendingCliAuth.delete(tool);
|
|
1315
|
-
const detail = await probeCliDetail(tool).catch(() => null);
|
|
1355
|
+
const detail = await probeCliDetail(tool).then(withCliUpgradeGuard).catch(() => null);
|
|
1316
1356
|
sendJson(response, 200, { ok: true, tool: detail });
|
|
1317
1357
|
} catch (error) {
|
|
1318
1358
|
// 超时=我们主动掐断轮询(用户还没在授权页同意):保留会话,202
|
package/lib/page.js
CHANGED
|
@@ -465,22 +465,26 @@ const renderCli = async (s) => {
|
|
|
465
465
|
// 真实探测本机安装与授权态(数秒),期间先展示读取中
|
|
466
466
|
body.innerHTML = '<div class="skill-sub">检测中…</div>';
|
|
467
467
|
let tools;
|
|
468
|
+
let python;
|
|
468
469
|
try {
|
|
469
470
|
const resp = await fetch('api/cli/status');
|
|
470
471
|
const data = await resp.json().catch(() => ({}));
|
|
471
472
|
if (!resp.ok) throw new Error(data.error || '检测失败');
|
|
472
473
|
tools = data.tools || {};
|
|
474
|
+
python = data.python || null;
|
|
473
475
|
} catch {
|
|
474
476
|
body.innerHTML = '<div class="skill-sub" style="color:var(--err)">CLI 状态检测失败,请稍后重试。</div>';
|
|
475
477
|
return;
|
|
476
478
|
}
|
|
477
|
-
renderCliData(s, tools);
|
|
479
|
+
renderCliData(s, tools, python);
|
|
478
480
|
};
|
|
479
|
-
// 纯渲染:tools 形如 { 'lark-cli': {state,version,latest}, ... }
|
|
481
|
+
// 纯渲染:tools 形如 { 'lark-cli': {state,version,latest}, ... },
|
|
482
|
+
// python 形如 { available, command, version }(null 表示未探测到数据)。
|
|
480
483
|
// 升级成功后端会带回升级后立即探测的单工具详情,直接合进缓存同步刷新,
|
|
481
484
|
// 不再整页二次探测(否则要再等一轮「检测中…」,用户以为没生效)。
|
|
482
|
-
const renderCliData = (s, tools) => {
|
|
485
|
+
const renderCliData = (s, tools, python) => {
|
|
483
486
|
renderCli._tools = { ...(renderCli._tools || {}), ...tools };
|
|
487
|
+
if (python !== undefined && python !== null) renderCli._python = python;
|
|
484
488
|
const allTools = renderCli._tools;
|
|
485
489
|
const cliEnabled = s.capabilities?.cli_deployd_enabled !== false;
|
|
486
490
|
const body = $('cli-body');
|
|
@@ -500,7 +504,6 @@ const renderCliData = (s, tools) => {
|
|
|
500
504
|
: state === 'unauthorized'
|
|
501
505
|
? '<span class="pill warn" style="font-size:11px">未授权</span>'
|
|
502
506
|
: '<span class="pill warn" style="font-size:11px">未安装</span>';
|
|
503
|
-
// 版本展示:当前版 · 有新版时「v旧 → v新」+ 升级按钮(查询失败静默退化)
|
|
504
507
|
const versionLine = meta.desc + ' · ' + meta.label
|
|
505
508
|
+ (info.version ? ' · v' + esc(info.version) : '')
|
|
506
509
|
+ (info.latest && info.latest !== info.version
|
|
@@ -523,9 +526,30 @@ const renderCliData = (s, tools) => {
|
|
|
523
526
|
+ esc(name) + status + '</div><div class="skill-sub">'
|
|
524
527
|
+ versionLine + '</div></div><div style="display:flex;gap:6px">' + actions + '</div></div>';
|
|
525
528
|
});
|
|
526
|
-
|
|
529
|
+
// Python 环境行:技能脚本的标准依赖。Windows 未装时可一键安装
|
|
530
|
+
// (python.org 官方安装器静默装用户目录),其他平台给包管理器指引。
|
|
531
|
+
const py = renderCli._python;
|
|
532
|
+
const isWin = navigator.platform && /win/i.test(navigator.platform);
|
|
533
|
+
const pythonRow = py
|
|
534
|
+
? '<div class="skill-item"><div class="skill-info"><div class="skill-name">Python '
|
|
535
|
+
+ (py.available
|
|
536
|
+
? '<span class="pill ok" style="font-size:11px">可用</span>'
|
|
537
|
+
: '<span class="pill warn" style="font-size:11px">未安装</span>')
|
|
538
|
+
+ '</div><div class="skill-sub">'
|
|
539
|
+
+ (py.available
|
|
540
|
+
? '技能脚本运行环境 · ' + esc(py.command) + ' · v' + esc(py.version)
|
|
541
|
+
: isWin
|
|
542
|
+
? '技能脚本运行环境 · 未检测到 Python(技能脚本需要 Python 3),点右侧「安装」自动装官方版本(python.org)'
|
|
543
|
+
: '技能脚本运行环境 · 未检测到 Python,可用系统包管理器安装(如 brew install python)后重启 dsh')
|
|
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>'
|
|
549
|
+
: '';
|
|
550
|
+
body.innerHTML = rows.join('') + pythonRow
|
|
527
551
|
+ '<div class="hint">在对话中直接使用(如 /lark 帮助),由 dsh 自动调用。</div>'
|
|
528
|
-
+ '<div class="hint"
|
|
552
|
+
+ '<div class="hint">未安装时点「安装」自助安装(Python 同样支持);已安装未授权时点「去授权」用飞书扫码完成即可。</div>';
|
|
529
553
|
body.querySelectorAll('[data-cli-upgrade]').forEach((btn) => {
|
|
530
554
|
btn.onclick = () => cliUpgrade(btn.dataset.cliUpgrade, btn);
|
|
531
555
|
});
|
|
@@ -648,8 +672,8 @@ const cliAuthDialog = async (tool) => {
|
|
|
648
672
|
}
|
|
649
673
|
};
|
|
650
674
|
|
|
651
|
-
/** CLI 一键安装:lark 走 npm 全局安装、ccpg
|
|
652
|
-
*
|
|
675
|
+
/** CLI 一键安装:lark 走 npm 全局安装、ccpg/python 走官方安装器,耗时
|
|
676
|
+
* 可达分钟级。响应自带安装后立即探测的详情,直接合进缓存刷新界面。 */
|
|
653
677
|
const cliInstall = async (tool, btn) => {
|
|
654
678
|
btn.disabled = true;
|
|
655
679
|
btn.textContent = '安装中,可能需要 1–3 分钟…';
|
|
@@ -662,7 +686,11 @@ const cliInstall = async (tool, btn) => {
|
|
|
662
686
|
const data = await resp.json().catch(() => ({}));
|
|
663
687
|
if (!resp.ok) throw new Error(data.error || 'CLI 安装失败');
|
|
664
688
|
renderCli._probedAt = 0;
|
|
665
|
-
if (
|
|
689
|
+
if (tool === 'python') {
|
|
690
|
+
// python 无三态:回传 {available,command,version},重绘 Python 行
|
|
691
|
+
if (data.python) renderCliData(lastStatus, {}, data.python);
|
|
692
|
+
else renderCli(lastStatus);
|
|
693
|
+
} else if (data.tool) {
|
|
666
694
|
renderCliData(lastStatus, { [tool]: data.tool });
|
|
667
695
|
} else {
|
|
668
696
|
renderCli(lastStatus);
|
|
@@ -957,11 +985,21 @@ const refresh = async () => {
|
|
|
957
985
|
}
|
|
958
986
|
$('model-body').innerHTML = lines.join('')
|
|
959
987
|
+ (planBlocked
|
|
960
|
-
? '<div class="row" style="margin-top:10px"
|
|
988
|
+
? '<div class="row" style="margin-top:10px">'
|
|
989
|
+
+ '<button id="retry-model-btn" class="btn ghost small" data-loading="同步中…">重新同步模型</button>'
|
|
990
|
+
+ '<button id="goto-plan-btn" class="btn primary small">去申请套餐</button></div>'
|
|
961
991
|
: '');
|
|
962
992
|
$('sync-btn').hidden = !modelEnabled || planBlocked;
|
|
963
993
|
const gotoPlan = $('goto-plan-btn');
|
|
964
994
|
if (gotoPlan) gotoPlan.onclick = () => activateTab('plan');
|
|
995
|
+
// planBlocked 兜底手动出口:旧错误残留时(套餐已批但本地还挂着
|
|
996
|
+
// plan_required)可立即重同步,不等后端自愈的退避周期
|
|
997
|
+
const retryModel = $('retry-model-btn');
|
|
998
|
+
if (retryModel) retryModel.onclick = () => withLoading(
|
|
999
|
+
retryModel,
|
|
1000
|
+
() => fetch('api/model/sync', { method: 'POST' }),
|
|
1001
|
+
'平台模型已同步',
|
|
1002
|
+
);
|
|
965
1003
|
}
|
|
966
1004
|
if (s.logged_in && s.skillhub) {
|
|
967
1005
|
const sh = s.skillhub;
|
package/lib/proc.js
CHANGED
|
@@ -2,62 +2,212 @@
|
|
|
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 } 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);
|
|
14
15
|
|
|
15
16
|
/** 常见安装目录兑底(环境 PATH 之外的补充):GUI 启动的 DSH Desktop 继承
|
|
16
|
-
* launchd 的 PATH(/usr/bin:/bin
|
|
17
|
+
* launchd 的 PATH(/usr/bin:/bin/…),不含 nvm/homebrew/.local —— 实测
|
|
17
18
|
* 从 Dock 启动时 pnpm/lark-cli 全部 ENOENT,插件页 latest 查不到、CLI
|
|
18
|
-
* 全显示未安装。
|
|
19
|
+
* 全显示未安装。Windows 同理:GUI 启动继承系统 PATH,常缺 %APPDATA%\npm
|
|
20
|
+
* (npm 全局 bin)与 %LOCALAPPDATA%\pnpm(pnpm 默认 home)。nvm 取最高
|
|
21
|
+
* node 版本的 bin;结果进程内缓存。 */
|
|
19
22
|
let extraBinDirsCache = null;
|
|
20
23
|
function extraBinDirs() {
|
|
21
24
|
if (extraBinDirsCache !== null) return extraBinDirsCache;
|
|
22
25
|
const dirs = [];
|
|
23
26
|
const home = homedir();
|
|
27
|
+
if (process.platform === 'win32') {
|
|
28
|
+
const appData = process.env.APPDATA || join(home, 'AppData', 'Roaming');
|
|
29
|
+
const localAppData = process.env.LOCALAPPDATA || join(home, 'AppData', 'Local');
|
|
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
|
+
dirs.push(join(programFiles, 'nodejs'), join(programFilesX86, 'nodejs'));
|
|
45
|
+
// npm -g 默认 prefix、pnpm 默认 PNPM_HOME,以及 ccpg-cli Windows 官方
|
|
46
|
+
// 安装器的落点 %USERPROFILE%\bin(本机实测 C:\Users\<user>\bin\
|
|
47
|
+
// ccpg-cli.exe;装完不重启也能立即探测/自升级)。nodejs.org 安装器的
|
|
48
|
+
// 系统级安装不在这些目录,但那种环境 PATH 里有 node,不需要兑底。
|
|
49
|
+
dirs.push(join(appData, 'npm'), join(localAppData, 'pnpm'), join(home, 'bin'));
|
|
50
|
+
// Python 常见安装位(python 探测兑底):python.org 每用户安装位
|
|
51
|
+
// %LOCALAPPDATA%\Programs\Python\Python3xx、机器级 Program Files\
|
|
52
|
+
// Python3xx、Miniconda/Anaconda 默认位。自定义安装位(如
|
|
53
|
+
// E:\VScode\miniconda3)在 PATH 里,不受影响。
|
|
54
|
+
try {
|
|
55
|
+
const pyRoot = join(localAppData, 'Programs', 'Python');
|
|
56
|
+
for (const entry of readdirSync(pyRoot)) {
|
|
57
|
+
if (existsSync(join(pyRoot, entry, 'python.exe'))) dirs.push(join(pyRoot, entry));
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
// 未装 python.org 版:跳过
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
for (const entry of readdirSync(programFiles)) {
|
|
64
|
+
if (/^Python\d+/.test(entry) && existsSync(join(programFiles, entry, 'python.exe'))) {
|
|
65
|
+
dirs.push(join(programFiles, entry));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
} catch {
|
|
69
|
+
// 未装机器级 python.org 版:跳过
|
|
70
|
+
}
|
|
71
|
+
// Miniconda/Anaconda 常见默认位:用户目录与 ProgramData(机器级共享)
|
|
72
|
+
dirs.push(
|
|
73
|
+
join(home, 'miniconda3'), join(home, 'anaconda3'),
|
|
74
|
+
join('C:\\', 'ProgramData', 'miniconda3'), join('C:\\', 'ProgramData', 'Anaconda3'),
|
|
75
|
+
);
|
|
76
|
+
} else {
|
|
77
|
+
try {
|
|
78
|
+
// nvm 目录名形如 v24.18.0,用 compareSemver 取最高版(compareSemver
|
|
79
|
+
// 在本文件后文定义,函数提升可用)
|
|
80
|
+
const versions = readdirSync(join(home, '.nvm/versions/node'))
|
|
81
|
+
.filter((v) => /^v?\d/.test(v))
|
|
82
|
+
.sort((a, b) => compareSemver(a.replace(/^v/, ''), b.replace(/^v/, '')));
|
|
83
|
+
if (versions.length) dirs.push(join(home, '.nvm/versions/node', versions[versions.length - 1], 'bin'));
|
|
84
|
+
} catch {
|
|
85
|
+
// 无 nvm:跳过,走后面的通用目录
|
|
86
|
+
}
|
|
87
|
+
dirs.push(join(home, '.local/bin'), '/opt/homebrew/bin', '/usr/local/bin');
|
|
88
|
+
}
|
|
89
|
+
// DSH Desktop 自带的 node/pnpm/dsh shim(Electron 当 node 用):宿主只把
|
|
90
|
+
// 这目录注入桌面终端的 PATH,插件子进程看不到——不兑底的话 dsh 自身有
|
|
91
|
+
// node 环境但 lark-cli 安装仍报「未找到 npm」。目录名是 profile 的 hash,
|
|
92
|
+
// 扫一层取含 bin 的子目录。Windows 桌面版把 shim 放在
|
|
93
|
+
// %LOCALAPPDATA%\\DSH Desktop\\shims(按官方约定),一并覆盖。
|
|
24
94
|
try {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (versions.length) dirs.push(join(home, '.nvm/versions/node', versions[versions.length - 1], 'bin'));
|
|
95
|
+
const cliRoot = join(home, 'Library', 'Application Support', 'DSH Desktop', 'cli');
|
|
96
|
+
for (const entry of readdirSync(cliRoot)) {
|
|
97
|
+
const shimBin = join(cliRoot, entry, 'bin');
|
|
98
|
+
if (existsSync(join(shimBin, 'node'))) dirs.push(shimBin);
|
|
99
|
+
}
|
|
31
100
|
} catch {
|
|
32
|
-
//
|
|
101
|
+
// 非 macOS / 未生成终端 shim:跳过
|
|
102
|
+
}
|
|
103
|
+
if (process.platform === 'win32') {
|
|
104
|
+
try {
|
|
105
|
+
const shimDir = join(
|
|
106
|
+
process.env.LOCALAPPDATA || join(home, 'AppData', 'Local'),
|
|
107
|
+
'DSH Desktop', 'shims',
|
|
108
|
+
);
|
|
109
|
+
if (existsSync(join(shimDir, 'node.exe')) || existsSync(join(shimDir, 'node.cmd'))) dirs.push(shimDir);
|
|
110
|
+
} catch {
|
|
111
|
+
// 目录不存在:跳过
|
|
112
|
+
}
|
|
33
113
|
}
|
|
34
|
-
dirs.push(join(home, '.local/bin'), '/opt/homebrew/bin', '/usr/local/bin');
|
|
35
114
|
extraBinDirsCache = dirs;
|
|
36
115
|
return dirs;
|
|
37
116
|
}
|
|
38
117
|
|
|
39
|
-
/**
|
|
40
|
-
*
|
|
41
|
-
|
|
118
|
+
/** 安装完成后清空兑底目录缓存:一键安装 python/ccpg 后新目录要立即可
|
|
119
|
+
* 探测(python.org 装到 %LOCALAPPDATA%\Programs\Python\Python3xx),不
|
|
120
|
+
* 能等 DSH Desktop 重启。 */
|
|
121
|
+
export function resetExtraBinDirsCache() {
|
|
122
|
+
extraBinDirsCache = null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** 候选扩展名顺序(纯函数,供测试):win32 下 CreateProcess 只认可执行
|
|
126
|
+
* 映像,npm/pnpm/lark-cli 的安装目录同时存在同名的无扩展名 sh 脚本
|
|
127
|
+
* (给 Git Bash 用)与 .cmd shim——无扩展名必须排最后,否则解析命中 sh
|
|
128
|
+
* 脚本,execFile 直接失败。 */
|
|
129
|
+
export function candidateSuffixes(platform) {
|
|
130
|
+
return platform === 'win32' ? ['.exe', '.cmd', ''] : [''];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** win32 下 .cmd/.bat shim 的 execFile 等价调用形态(纯函数,供测试):
|
|
134
|
+
* Node ≥ 18.20/20.12(CVE-2024-27980 修复,DSH Desktop 内置 Node 同样
|
|
135
|
+
* 受影响)起 execFile 直接执行 .cmd/.bat 会同步抛 spawn EINVAL,必须经
|
|
136
|
+
* cmd.exe /d /s /c 解释。参数仍走数组、由 Node 按 Windows 规则引号转义,
|
|
137
|
+
* 不拼接用户输入。非 win32 或非 shim 返回 null。 */
|
|
138
|
+
export function cmdShimInvocation(platform, resolved, args) {
|
|
139
|
+
if (platform !== 'win32') return null;
|
|
140
|
+
if (!/\.(cmd|bat)$/i.test(resolved)) return null;
|
|
141
|
+
return { file: 'cmd.exe', args: ['/d', '/s', '/c', resolved, ...args] };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** PATH 环境变量键名(纯函数,供测试):win32 下实际键通常是 Path,注入
|
|
145
|
+
* 时必须复用已有键,否则 env 里出现 Path+PATH 两个重复键,子进程读到
|
|
146
|
+
* 哪个取决于实现。 */
|
|
147
|
+
export function pathEnvKey(env, platform) {
|
|
148
|
+
if (platform !== 'win32') return 'PATH';
|
|
149
|
+
return Object.keys(env).find((key) => /^path$/i.test(key)) || 'PATH';
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** 命令解析:PATH 里找不到时从兑底目录解析绝对路径(找不到返回 null)。
|
|
153
|
+
* 所有 execFile 调用统一过这里;返回 null 时调用方走「未找到命令」的
|
|
154
|
+
* 中文指引分支。绝对路径原样返回(ccpg 安装器等)。 */
|
|
155
|
+
export function resolveCommandOrNull(tool) {
|
|
156
|
+
if (isAbsolute(tool)) return existsSync(tool) ? tool : null;
|
|
42
157
|
const pathDirs = String(process.env.PATH || '').split(pathDelimiter).filter(Boolean);
|
|
43
|
-
const
|
|
158
|
+
const suffixes = candidateSuffixes(process.platform);
|
|
44
159
|
for (const dir of [...pathDirs, ...extraBinDirs()]) {
|
|
45
|
-
|
|
46
|
-
|
|
160
|
+
for (const suffix of suffixes) {
|
|
161
|
+
const candidate = join(dir, tool + suffix);
|
|
162
|
+
if (existsSync(candidate)) return candidate;
|
|
163
|
+
}
|
|
47
164
|
}
|
|
48
|
-
return
|
|
165
|
+
return null;
|
|
49
166
|
}
|
|
50
167
|
|
|
51
|
-
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
168
|
+
/** 与 resolveCommandOrNull 同口径,但返回全部按序去重候选。python 这类
|
|
169
|
+
* 探测必须逐个候选试:Windows PATH 前段常是 Microsoft Store 的假别名
|
|
170
|
+
* (0 字节重解析点,执行退出码 9009),真正的解释器(miniconda /
|
|
171
|
+
* python.org)排在更后面——只试首个命中会误报未安装。 */
|
|
172
|
+
export function resolveCommandCandidates(tool) {
|
|
173
|
+
if (isAbsolute(tool)) return existsSync(tool) ? [tool] : [];
|
|
174
|
+
const seen = new Set();
|
|
175
|
+
const out = [];
|
|
176
|
+
const pathDirs = String(process.env.PATH || '').split(pathDelimiter).filter(Boolean);
|
|
177
|
+
const suffixes = candidateSuffixes(process.platform);
|
|
178
|
+
for (const dir of [...pathDirs, ...extraBinDirs()]) {
|
|
179
|
+
for (const suffix of suffixes) {
|
|
180
|
+
const candidate = join(dir, tool + suffix);
|
|
181
|
+
if (existsSync(candidate) && !seen.has(candidate)) {
|
|
182
|
+
seen.add(candidate);
|
|
183
|
+
out.push(candidate);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return out;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** 兼容旧签名:找不到时返回原名(后续 ENOENT 走既有 missing/降级语义)。 */
|
|
191
|
+
export function resolveCommand(tool) {
|
|
192
|
+
return resolveCommandOrNull(tool) || tool;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** 统一的子进程执行:解析命令路径,并把**所有兑底目录**前置进子进程
|
|
196
|
+
* PATH——win32 下 npm/lark-cli 是 .cmd shim(`%~dp0\node.exe` 找 node),
|
|
197
|
+
* node 可能在另一个兑底目录(nodejs.org 系统安装位);.cmd 又经
|
|
198
|
+
* cmd.exe /d /s /c 路由,cmd.exe 在第三个兑底目录(System32)。只前置
|
|
199
|
+
* 解析到的那个目录会让 shim 内部找不到依赖,必须整组前置。opts.env 只
|
|
200
|
+
* 传增量变量。 */
|
|
54
201
|
function execResolved(tool, args, { timeoutMs, cwd, env } = {}) {
|
|
55
202
|
const resolved = resolveCommand(tool);
|
|
56
203
|
const childEnv = { ...process.env, ...(env || {}) };
|
|
57
|
-
|
|
58
|
-
|
|
204
|
+
const key = pathEnvKey(childEnv, process.platform);
|
|
205
|
+
const extras = extraBinDirs().filter((dir) => existsSync(dir));
|
|
206
|
+
if (extras.length) {
|
|
207
|
+
childEnv[key] = [...extras, childEnv[key] || ''].filter(Boolean).join(pathDelimiter);
|
|
59
208
|
}
|
|
60
|
-
|
|
209
|
+
const shim = cmdShimInvocation(process.platform, resolved, args);
|
|
210
|
+
return execFileAsync(shim ? shim.file : resolved, shim ? shim.args : args, {
|
|
61
211
|
timeout: timeoutMs,
|
|
62
212
|
killSignal: 'SIGKILL',
|
|
63
213
|
cwd,
|
|
@@ -76,6 +226,21 @@ export function firstErrorLines(raw) {
|
|
|
76
226
|
return picked.slice(0, 300);
|
|
77
227
|
}
|
|
78
228
|
|
|
229
|
+
/** pnpm store 版本冲突(受管 pnpm 与用户全局 pnpm 主版本不同,store 目录
|
|
230
|
+
* 格式不兼容)。Windows 桌面版高发:用户装了独立 pnpm 且主版本与 DSH
|
|
231
|
+
* Desktop 内置的不同。识别后给可操作指引而非裸抛英文堆栈。 */
|
|
232
|
+
export function isUnexpectedStoreError(message) {
|
|
233
|
+
return /ERR_PNPM_UNEXPECTED_STORE/i.test(String(message || ''));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** 翻译 pnpm store 冲突为人话 + 自愈指引。 */
|
|
237
|
+
export function unexpectedStoreGuidance() {
|
|
238
|
+
return '检测到 pnpm 存储版本冲突(DSH Desktop 内置的 pnpm 与你电脑上安装的 pnpm 主版本不一致)。'
|
|
239
|
+
+ '请在 DSH Desktop 外自行执行一次插件安装即可自动重建存储:关闭本窗口后运行 '
|
|
240
|
+
+ (process.platform === 'win32' ? 'dsh plugin --profile desktop add dsh-agentone' : 'dsh plugin --profile web add dsh-agentone')
|
|
241
|
+
+ ',或删除 profile 目录下的 node_modules 后重试。';
|
|
242
|
+
}
|
|
243
|
+
|
|
79
244
|
/**
|
|
80
245
|
* 运行一次性命令。webServer 注册的处理器是同步 Node handler 场景下的
|
|
81
246
|
* async 函数;execFile 不经过 shell,参数数组中的每个元素都是独立参数,
|
|
@@ -91,16 +256,41 @@ export async function runCliTool(tool, args, { timeoutMs = 300000, cwd, env } =
|
|
|
91
256
|
return { stdout, stderr };
|
|
92
257
|
} catch (error) {
|
|
93
258
|
if (error.code === 'ENOENT') {
|
|
94
|
-
throw new Error(
|
|
259
|
+
throw new Error(missingCommandGuidance(tool));
|
|
95
260
|
}
|
|
96
261
|
if (error.killed) {
|
|
97
262
|
throw new Error('命令执行超时,请稍后重试');
|
|
98
263
|
}
|
|
99
|
-
const
|
|
264
|
+
const raw = `${error.stderr || ''}\n${error.stdout || ''}`;
|
|
265
|
+
if (isUnexpectedStoreError(raw)) {
|
|
266
|
+
throw new Error(unexpectedStoreGuidance());
|
|
267
|
+
}
|
|
268
|
+
const message = firstErrorLines(raw);
|
|
100
269
|
throw new Error(message || `命令执行失败(退出码 ${error.code ?? '未知'})`);
|
|
101
270
|
}
|
|
102
271
|
}
|
|
103
272
|
|
|
273
|
+
/** 工具缺失时的中文指引:不同工具有不同的安装出路。 */
|
|
274
|
+
function missingCommandGuidance(tool) {
|
|
275
|
+
if (tool === 'npm' || tool === 'npx') {
|
|
276
|
+
return process.platform === 'win32'
|
|
277
|
+
? '未找到 npm/npx 命令:lark-cli 依赖 Node.js 环境,请先从 https://nodejs.org 下载安装 Node.js(LTS 版,安装时保持默认选项),安装完成后重启 DSH Desktop 再点「安装」'
|
|
278
|
+
: '未找到 npm/npx 命令:lark-cli 依赖 Node.js 环境,请先安装 Node.js(推荐 nvm:nvm install --lts),安装完成后重启 DSH Desktop 再点「安装」';
|
|
279
|
+
}
|
|
280
|
+
if (tool === 'pnpm') {
|
|
281
|
+
return '未找到 pnpm 命令,无法查询插件最新版本。可安装 pnpm(npm install -g pnpm)后重启 DSH Desktop;不影响插件正常使用,仅影响升级提示。';
|
|
282
|
+
}
|
|
283
|
+
if (tool === 'dsh') {
|
|
284
|
+
return '未找到 dsh 命令,请确认已安装 dsh 并在终端可用';
|
|
285
|
+
}
|
|
286
|
+
if (tool === 'python' || tool === 'python3' || tool === 'py') {
|
|
287
|
+
return process.platform === 'win32'
|
|
288
|
+
? '未检测到 Python:技能脚本需要 Python 3 运行环境,点「安装」自动装官方版本(python.org),或自行从 https://www.python.org 下载安装(勾选 Add to PATH)后重启 DSH Desktop'
|
|
289
|
+
: '未检测到 Python:技能脚本需要 Python 3 运行环境,请用系统包管理器安装(如 brew install python / apt install python3)后重启 dsh';
|
|
290
|
+
}
|
|
291
|
+
return `未找到 ${tool} 命令,请确认已安装`;
|
|
292
|
+
}
|
|
293
|
+
|
|
104
294
|
/** dsh plugin 子命令:转发给官方 CLI(profile 由调用方给出)。 */
|
|
105
295
|
export function dshPluginCli(args, profile = 'web') {
|
|
106
296
|
return runCliTool('dsh', ['plugin', '--profile', profile, ...args]);
|
|
@@ -157,16 +347,25 @@ export function compareSemver(a, b) {
|
|
|
157
347
|
}
|
|
158
348
|
|
|
159
349
|
/**
|
|
160
|
-
* 查询包在 registry
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
350
|
+
* 查询包在 registry 的最新版本。优先 `pnpm view`;pnpm 不可用(Windows
|
|
351
|
+
* 桌面版常见——受管 pnpm 不在 PATH)时退化 `npm view`(输出同为版本行,
|
|
352
|
+
* parseLatestVersionLine 通吃)。为什么不用 `pnpm outdated` /
|
|
353
|
+
* `update -L` / `add pkg@latest`:实测 pnpm 11 里 outdated 受 dist-tag
|
|
354
|
+
* 元数据缓存与 minimumReleaseAge 冷却期影响(新版发布后一段时间查不到),
|
|
355
|
+
* update -L 对精确 pin 的依赖是 no-op,add@latest 会解析到缓存旧版;只有
|
|
356
|
+
* 显式版本号可用。view 单字段查询每次真实打 registry,是这里唯一的实时
|
|
357
|
+
* 数据源。查询失败返回 null(前端按钮不出现)。
|
|
166
358
|
*/
|
|
167
359
|
export async function pnpmViewLatest(name, cwd) {
|
|
168
|
-
|
|
169
|
-
|
|
360
|
+
try {
|
|
361
|
+
const { stdout } = await pnpmCli(['view', `${name}@latest`, 'version'], 30000, cwd);
|
|
362
|
+
return parseLatestVersionLine(stdout);
|
|
363
|
+
} catch (error) {
|
|
364
|
+
// pnpm 缺失(含 store 冲突指引类可操作错误)不影响升级检测本身:
|
|
365
|
+
// npm view 同样实时打 registry,版本号全局一致
|
|
366
|
+
const { stdout } = await runCliTool('npm', ['view', `${name}@latest`, 'version'], { timeoutMs: 30_000, cwd });
|
|
367
|
+
return parseLatestVersionLine(stdout);
|
|
368
|
+
}
|
|
170
369
|
}
|
|
171
370
|
|
|
172
371
|
/**
|
|
@@ -233,12 +432,39 @@ async function cliLatestVersion(tool, timeoutMs) {
|
|
|
233
432
|
return typeof data?.latest_version === 'string' ? data.latest_version : null;
|
|
234
433
|
}
|
|
235
434
|
if (tool === 'lark-cli') {
|
|
236
|
-
|
|
435
|
+
// runCliTool 而非裸 execResolved:走 Windows 兑底目录解析 npm,
|
|
436
|
+
// ENOENT 时抛中文指引由上层 catch 降级(按钮不出现)而非静默挂死
|
|
437
|
+
const { stdout } = await runCliTool('npm', ['view', '@larksuite/cli', 'version'], { timeoutMs });
|
|
237
438
|
return parseCliVersion(stdout);
|
|
238
439
|
}
|
|
239
440
|
return null;
|
|
240
441
|
}
|
|
241
442
|
|
|
443
|
+
/**
|
|
444
|
+
* Python 环境探测(技能脚本的标准依赖)。win32 优先官方 py 启动器,再
|
|
445
|
+
* python / python3;每个名字按 resolveCommandCandidates 逐个候选试
|
|
446
|
+
* `--version`(穿透 Store 假别名)。Python 2.x 不算可用——技能脚本需要
|
|
447
|
+
* 3.x,继续找下一个候选。与平台 instance_service 的口径一致:探测失败
|
|
448
|
+
* 不抛错,返回 available:false 由前端给安装指引/按钮。
|
|
449
|
+
*/
|
|
450
|
+
export async function probePythonEnv({ timeoutMs = 15_000 } = {}) {
|
|
451
|
+
const names = process.platform === 'win32' ? ['py', 'python', 'python3'] : ['python3', 'python'];
|
|
452
|
+
for (const name of names) {
|
|
453
|
+
for (const candidate of resolveCommandCandidates(name)) {
|
|
454
|
+
try {
|
|
455
|
+
const { stdout, stderr } = await runCliTool(candidate, ['--version'], { timeoutMs });
|
|
456
|
+
const match = /(\d+)\.(\d+)(?:\.(\d+))?/.exec(`${stdout || ''}${stderr || ''}`);
|
|
457
|
+
if (match && Number(match[1]) >= 3) {
|
|
458
|
+
return { available: true, command: name, version: match[0] };
|
|
459
|
+
}
|
|
460
|
+
} catch {
|
|
461
|
+
// Store 假别名(9009)/ 不可执行 / 超时:试下一个候选
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return { available: false, command: null, version: null };
|
|
466
|
+
}
|
|
467
|
+
|
|
242
468
|
/** ccpg-cli 官方安装脚本地址(与平台侧 cli-deployd-go InstallCCPGCLI 同源,
|
|
243
469
|
* 保持两端口径一致;auto-install.sh 自带二进制下载与 PATH 写入)。 */
|
|
244
470
|
const CCPG_CLI_INSTALL_URL_PREFIX = 'http://bms.einwin.com/media/ccpg-cli/202605091513';
|
|
@@ -273,29 +499,51 @@ async function installLarkSkills({ timeoutMs = 180_000 } = {}) {
|
|
|
273
499
|
* cli-deployd-go 的 install 口径一致。
|
|
274
500
|
* 返回输出尾部给前端展示。只接受 CLI_TOOLS 白名单内的 tool。
|
|
275
501
|
*/
|
|
276
|
-
export async function installCliTool(tool, { timeoutMs =
|
|
502
|
+
export async function installCliTool(tool, { timeoutMs = 600_000 } = {}) {
|
|
277
503
|
let stdout;
|
|
278
504
|
if (tool === 'lark-cli') {
|
|
279
|
-
|
|
505
|
+
// 预检+降级链:优先独立 npm;没有 npm 但有 pnpm(含 DSH Desktop 自带
|
|
506
|
+
// shim)时用 pnpm 全局装——lark-cli 官方支持 pnpm 安装方式,装出来的
|
|
507
|
+
// 二进制与 npm 路径一致,后续 lark-cli update 也能自升级。
|
|
508
|
+
// 两者都没有才报「装 Node.js」指引。
|
|
509
|
+
if (resolveCommandOrNull('npm')) {
|
|
510
|
+
({ stdout } = await runCliTool('npm', ['install', '-g', '@larksuite/cli@latest'], { timeoutMs }));
|
|
511
|
+
} else if (resolveCommandOrNull('pnpm')) {
|
|
512
|
+
({ stdout } = await runCliTool('pnpm', ['add', '-g', '@larksuite/cli@latest'], { timeoutMs }));
|
|
513
|
+
} else {
|
|
514
|
+
throw new Error(missingCommandGuidance('npm'));
|
|
515
|
+
}
|
|
280
516
|
const skillsNote = await installLarkSkills().catch((error) => {
|
|
281
517
|
// 软失败:技能装不上不影响 CLI 本体已装成功(如 GitHub 不可达)
|
|
282
518
|
return `lark-cli skills install soft-failed: ${error?.message || error}`;
|
|
283
519
|
});
|
|
284
520
|
stdout = `${stdout}\n${skillsNote}`;
|
|
285
521
|
} else if (tool === 'ccpg-cli') {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
timeoutMs
|
|
297
|
-
|
|
298
|
-
|
|
522
|
+
if (process.platform === 'win32') {
|
|
523
|
+
({ stdout } = await installCcpgCliWindows(timeoutMs));
|
|
524
|
+
} else {
|
|
525
|
+
// 下载 auto-install.sh(curl 失败退化 wget)后交 bash 执行;脚本文本
|
|
526
|
+
// 来自固定公司 URL(非用户输入),作为 bash -c 的单一参数,与平台
|
|
527
|
+
// 侧 `| bash` 等价。
|
|
528
|
+
const scriptUrl = `${CCPG_CLI_INSTALL_URL_PREFIX}/auto-install.sh`;
|
|
529
|
+
const fetchScript = () => runCliTool('curl', [
|
|
530
|
+
'-fsSL', '--retry', '3', '--retry-delay', '5', '--max-time', '120',
|
|
531
|
+
scriptUrl,
|
|
532
|
+
], { timeoutMs: 120_000 })
|
|
533
|
+
.catch(() => runCliTool('wget', ['-qO-', scriptUrl], { timeoutMs: 120_000 }));
|
|
534
|
+
const fetched = (await fetchScript()).stdout.trim();
|
|
535
|
+
if (!fetched) throw new Error('安装脚本下载失败,请检查网络后重试');
|
|
536
|
+
({ stdout } = await runCliTool('bash', ['-c', fetched], {
|
|
537
|
+
timeoutMs,
|
|
538
|
+
env: { CCPG_CLI_URL_PREFIX: CCPG_CLI_INSTALL_URL_PREFIX },
|
|
539
|
+
}));
|
|
540
|
+
}
|
|
541
|
+
resetExtraBinDirsCache();
|
|
542
|
+
} else if (tool === 'python') {
|
|
543
|
+
if (process.platform !== 'win32') {
|
|
544
|
+
throw new Error('请用系统包管理器安装 Python(如 brew install python / apt install python3)后重启 dsh');
|
|
545
|
+
}
|
|
546
|
+
({ stdout } = await installPythonWindows(timeoutMs));
|
|
299
547
|
} else {
|
|
300
548
|
throw new Error('不支持的 CLI 工具');
|
|
301
549
|
}
|
|
@@ -303,18 +551,154 @@ export async function installCliTool(tool, { timeoutMs = 300_000 } = {}) {
|
|
|
303
551
|
return lines.slice(-6).join('\n');
|
|
304
552
|
}
|
|
305
553
|
|
|
554
|
+
/** ccpg-cli Windows 安装(与 mac/Linux auto-install.sh 同构:裸二进制落地):
|
|
555
|
+
* 1) 动态查询官方更新接口拿当前二进制地址——媒体目录按日期滚动,pin 死
|
|
556
|
+
* 的旧地址会被服务器清理(实测教程里的 202607151817 已 404);接口与
|
|
557
|
+
* ccpg-cli 自身 updater 同源,永远指向当前有效版本。接口返回的 URL
|
|
558
|
+
* 必须落在 bms.einwin.com/media/ccpg-cli/ 前缀下才接受(防接口被篡
|
|
559
|
+
* 改指向恶意地址),接口异常时兜底最近一次验证过的 pin 值。
|
|
560
|
+
* 注意:接口给的是**裸二进制本体**(实测与 update 落地文件逐字节一
|
|
561
|
+
* 致),不是安装器——落地即用,无 /S 静默安装一说。
|
|
562
|
+
* 2) curl 下载到 %USERPROFILE%\bin\ccpg-cli.exe(先下暂存再改名,原子)
|
|
563
|
+
* 3) 校验 --version 可执行(PE 完整、架构匹配)
|
|
564
|
+
* 4) %USERPROFILE%\bin 写入用户 PATH(注册表 User 作用域,免管理员)
|
|
565
|
+
* 5) 教程必做步骤 ccpg-cli update(刚下的就是 latest,通常 no-op,软失败)
|
|
566
|
+
* 全程 execFile 参数数组,不经 shell。 */
|
|
567
|
+
const CCPG_CLI_UPDATE_ENDPOINT = 'https://bms.einwin.com/mdm-third-party/ccpg-cli/latest?os=windows&arch=amd64';
|
|
568
|
+
const CCPG_CLI_WINDOWS_URL_PREFIX = 'https://bms.einwin.com/media/ccpg-cli/';
|
|
569
|
+
const CCPG_CLI_WINDOWS_FALLBACK_URL = `${CCPG_CLI_WINDOWS_URL_PREFIX}202608271055/ccpg-cli-windows-amd64.exe`;
|
|
570
|
+
|
|
571
|
+
async function resolveCcpgWindowsDownloadUrl() {
|
|
572
|
+
try {
|
|
573
|
+
const { stdout } = await runCliTool('curl', [
|
|
574
|
+
'-fsSL', '--max-time', '30', CCPG_CLI_UPDATE_ENDPOINT,
|
|
575
|
+
], { timeoutMs: 40_000 });
|
|
576
|
+
const url = JSON.parse(String(stdout).trim())?.value?.url;
|
|
577
|
+
if (typeof url === 'string' && url.startsWith(CCPG_CLI_WINDOWS_URL_PREFIX)) return url;
|
|
578
|
+
} catch {
|
|
579
|
+
// 更新接口异常:走兜底 pin 值
|
|
580
|
+
}
|
|
581
|
+
return CCPG_CLI_WINDOWS_FALLBACK_URL;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/** 把目录追加进用户 PATH(注册表 User 作用域,新开终端生效)。已存在则
|
|
585
|
+
* 跳过。用 powershell [Environment] API——setx 会静默截断超过 1024 字符
|
|
586
|
+
* 的 PATH,不用。 */
|
|
587
|
+
async function ensureUserPathContains(dir) {
|
|
588
|
+
const safeDir = dir.replace(/'/g, "''");
|
|
589
|
+
const script = [
|
|
590
|
+
"$p=[Environment]::GetEnvironmentVariable('Path','User')",
|
|
591
|
+
`if(($p -split ';') -notcontains '${safeDir}'){[Environment]::SetEnvironmentVariable('Path',($p.TrimEnd(';')+';${safeDir}'),'User');'added'}else{'exists'}`,
|
|
592
|
+
].join(';');
|
|
593
|
+
const { stdout } = await runCliTool('powershell', ['-NoProfile', '-Command', script], { timeoutMs: 30_000 });
|
|
594
|
+
return String(stdout).includes('added')
|
|
595
|
+
? '已把 %USERPROFILE%\\bin 写入用户 PATH(新开的终端生效)'
|
|
596
|
+
: '%USERPROFILE%\\bin 已在用户 PATH 中';
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
async function installCcpgCliWindows(timeoutMs) {
|
|
600
|
+
const binDir = join(homedir(), 'bin');
|
|
601
|
+
const target = join(binDir, 'ccpg-cli.exe');
|
|
602
|
+
mkdirSync(binDir, { recursive: true });
|
|
603
|
+
const url = await resolveCcpgWindowsDownloadUrl();
|
|
604
|
+
const staging = join(binDir, 'ccpg-cli.exe.download');
|
|
605
|
+
await runCliTool('curl', [
|
|
606
|
+
'-fsSL', '--retry', '3', '--retry-delay', '5', '--max-time', '600',
|
|
607
|
+
'--speed-limit', '65536', '--speed-time', '30',
|
|
608
|
+
'-o', staging, url,
|
|
609
|
+
], { timeoutMs });
|
|
610
|
+
renameSync(staging, target);
|
|
611
|
+
// 校验二进制可执行(PE 完整、架构匹配)
|
|
612
|
+
const { stdout: versionOut } = await runCliTool(target, ['--version'], { timeoutMs: 30_000 });
|
|
613
|
+
resetExtraBinDirsCache();
|
|
614
|
+
const pathNote = await ensureUserPathContains(binDir)
|
|
615
|
+
.catch(() => 'PATH 写入失败,请手动把 %USERPROFILE%\\bin 加入用户 PATH');
|
|
616
|
+
// 官方教程的必做步骤:装完立即自升级(刚下的就是 latest,通常 no-op;
|
|
617
|
+
// 软失败不阻断)
|
|
618
|
+
const updateNote = await runCliTool('ccpg-cli', ['update'], {
|
|
619
|
+
timeoutMs: 120_000,
|
|
620
|
+
env: { CCPG_FULL_CLI: '1' },
|
|
621
|
+
})
|
|
622
|
+
.then((r) => String(r.stdout || '').trim().split('\n').slice(-2).join('\n'))
|
|
623
|
+
.catch(() => 'ccpg-cli update 未完成,安装后请手动执行 ccpg-cli update');
|
|
624
|
+
return {
|
|
625
|
+
stdout: `ccpg-cli 安装完成(${String(versionOut).trim()})\n${updateNote}\n${pathNote}\n如状态页仍显示未安装,请重启 DSH Desktop 后点「重新检测」`,
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/** Python Windows 一键安装(python.org 官方安装器口径):
|
|
630
|
+
* 1) Windows 10 1803+ 自带 curl.exe 下载官方安装器——**国内镜像优先**
|
|
631
|
+
* (python.org 官网源在国内动辄超时,实测华为云镜像 2MB/s、官网源
|
|
632
|
+
* 300s 拉不完 45MB),镜像目录结构与官网一致、按版本号直链;
|
|
633
|
+
* 镜像不可用时退化官网源(海外用户/镜像异常)
|
|
634
|
+
* 2) 静默每用户安装:InstallAllUsers=0(无需 UAC/管理员)、
|
|
635
|
+
* PrependPath=1(安装器直接写入用户 PATH——不勾选的话 GUI 宿主
|
|
636
|
+
* 要等重启才能看到 python.exe)
|
|
637
|
+
* 3) 装完清兑底目录缓存,本进程内即可探测到新解释器
|
|
638
|
+
* 全程 execFile 参数数组,不经 shell。 */
|
|
639
|
+
const PYTHON_WINDOWS_INSTALLER_VERSION = '3.13.6';
|
|
640
|
+
const PYTHON_WINDOWS_INSTALLER_URLS = [
|
|
641
|
+
`https://mirrors.huaweicloud.com/python/${PYTHON_WINDOWS_INSTALLER_VERSION}/python-${PYTHON_WINDOWS_INSTALLER_VERSION}-amd64.exe`,
|
|
642
|
+
`https://www.python.org/ftp/python/${PYTHON_WINDOWS_INSTALLER_VERSION}/python-${PYTHON_WINDOWS_INSTALLER_VERSION}-amd64.exe`,
|
|
643
|
+
];
|
|
644
|
+
|
|
645
|
+
async function installPythonWindows(timeoutMs) {
|
|
646
|
+
const installer = join(tmpdir(), 'python-setup.exe');
|
|
647
|
+
// 逐个源下载:curl 失败(非 2xx/超时/低速假死)换下一个源。
|
|
648
|
+
// --speed-limit/--speed-time:30 秒内速度低于 64KB/s 视为假死换源
|
|
649
|
+
// (官网源在国内常见形态不是断连而是 kb 级慢速拖死)。
|
|
650
|
+
let downloaded = false;
|
|
651
|
+
let lastError = null;
|
|
652
|
+
for (const url of PYTHON_WINDOWS_INSTALLER_URLS) {
|
|
653
|
+
try {
|
|
654
|
+
await runCliTool('curl', [
|
|
655
|
+
'-fsSL', '--retry', '2', '--retry-delay', '3', '--max-time', '600',
|
|
656
|
+
'--speed-limit', '65536', '--speed-time', '30',
|
|
657
|
+
'-o', installer, url,
|
|
658
|
+
], { timeoutMs });
|
|
659
|
+
downloaded = true;
|
|
660
|
+
break;
|
|
661
|
+
} catch (error) {
|
|
662
|
+
lastError = error;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
if (!downloaded) throw lastError || new Error('安装包下载失败,请检查网络后重试');
|
|
666
|
+
// python.org 安装器静默模式:/quiet + 每用户安装 + 写入用户 PATH
|
|
667
|
+
await runCliTool(installer, [
|
|
668
|
+
'/quiet', 'InstallAllUsers=0', 'PrependPath=1', 'Include_test=0',
|
|
669
|
+
], { timeoutMs });
|
|
670
|
+
resetExtraBinDirsCache();
|
|
671
|
+
// 立即复探(本进程内兑底目录已含 python.org 每用户安装位)
|
|
672
|
+
const probe = await probePythonEnv().catch(() => ({ available: false }));
|
|
673
|
+
return {
|
|
674
|
+
stdout: probe.available
|
|
675
|
+
? `Python ${probe.version} 安装完成(${probe.command})。如技能页仍提示未安装,请重启 DSH Desktop 后点「重新检测」`
|
|
676
|
+
: `Python ${PYTHON_WINDOWS_INSTALLER_VERSION} 安装完成。安装器已写入用户 PATH,请重启 DSH Desktop 后点「重新检测」`,
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
|
|
306
680
|
/**
|
|
307
681
|
* 触发 CLI 官方自升级(`tool update`):ccpg-cli 从更新服务器下载二进制
|
|
308
|
-
* (~17MB
|
|
682
|
+
* (~17MB),新版 lark-cli 自动探测 npm/pnpm/手动安装方式(手动时只打印
|
|
309
683
|
* GitHub 下载地址)。返回输出尾部给前端展示(含手动安装的下载链接)。
|
|
684
|
+
* 兜底:老版本 lark-cli 没有 update 子命令(实测 1.0.0 报 unknown
|
|
685
|
+
* command),此时「重装即升级」——走 installCliTool 的 npm/pnpm 全局装
|
|
686
|
+
* 最新版链路,覆盖旧版。
|
|
310
687
|
*/
|
|
311
688
|
export async function upgradeCliTool(tool, { timeoutMs = 300_000 } = {}) {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
689
|
+
try {
|
|
690
|
+
const { stdout } = await runCliTool(tool, ['update'], {
|
|
691
|
+
timeoutMs,
|
|
692
|
+
env: tool === 'ccpg-cli' ? { CCPG_FULL_CLI: '1' } : undefined,
|
|
693
|
+
});
|
|
694
|
+
const lines = String(stdout).split('\n').map((l) => l.trim()).filter(Boolean);
|
|
695
|
+
return lines.slice(-6).join('\n');
|
|
696
|
+
} catch (error) {
|
|
697
|
+
if (tool === 'lark-cli' && /unknown command/i.test(String(error?.message || ''))) {
|
|
698
|
+
return installCliTool('lark-cli', { timeoutMs });
|
|
699
|
+
}
|
|
700
|
+
throw error;
|
|
701
|
+
}
|
|
318
702
|
}
|
|
319
703
|
|
|
320
704
|
/** 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.23",
|
|
4
4
|
"description": "AgentOne 平台集成插件:飞书登录、平台模型(同步失败自愈重试)、SkillHub 技能(升级检测一键升级、装后即时刷新、套餐技能自动同步)、套餐申请、插件管理与自助升级、lark-cli / ccpg-cli 探测与一键安装(请求超时与错误码透传、令牌轮换 single-flight、凭据损坏容错与并发安全写入、状态页 UX 增强)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|