dsh-agentone 0.5.27 → 0.5.29
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 +24 -10
- package/lib/page.js +15 -5
- package/lib/proc.js +294 -60
- package/package.json +4 -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, probePythonEnv, pnpmViewLatest, runCliTool, unexpectedStoreGuidance, upgradeCliTool } from './proc.js';
|
|
37
|
+
import { compareSemver, dshPluginCli, firstErrorLines, hasChineseText, installCliTool, isTerminalCliAuthFailure, 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
|
|
|
@@ -1302,9 +1302,17 @@ export function apply(ctx, config) {
|
|
|
1302
1302
|
sendJson(response, 400, { error: '不支持的 CLI 工具' });
|
|
1303
1303
|
return;
|
|
1304
1304
|
}
|
|
1305
|
-
|
|
1305
|
+
// 只需要三态:跳过 latest 查询(registry 两轮 curl 最坏 ~40s)
|
|
1306
|
+
const detail = await probeCliDetail(tool, { withLatest: false })
|
|
1307
|
+
.then(withCliUpgradeGuard).catch(() => null);
|
|
1306
1308
|
if (detail?.state === 'missing') {
|
|
1307
|
-
sendJson(response, 400, {
|
|
1309
|
+
sendJson(response, 400, {
|
|
1310
|
+
error: detail.reason === 'broken_install'
|
|
1311
|
+
? 'CLI 安装不完整,请先点击「修复安装」后再授权'
|
|
1312
|
+
: detail.reason === 'unavailable'
|
|
1313
|
+
? 'CLI 暂时无法访问(可能被安全软件占用或响应超时),请稍后重试;若持续失败请重新安装'
|
|
1314
|
+
: 'CLI 未安装,请先安装或联系管理员',
|
|
1315
|
+
});
|
|
1308
1316
|
return;
|
|
1309
1317
|
}
|
|
1310
1318
|
const raw = await beginCliAuth(tool);
|
|
@@ -1322,7 +1330,10 @@ export function apply(ctx, config) {
|
|
|
1322
1330
|
expires_in: parsed.expiresIn,
|
|
1323
1331
|
});
|
|
1324
1332
|
} catch (error) {
|
|
1325
|
-
|
|
1333
|
+
const message = String(error?.message || '');
|
|
1334
|
+
sendJson(response, 502, {
|
|
1335
|
+
error: hasChineseText(message) ? message : '发起授权失败,请稍后重试',
|
|
1336
|
+
});
|
|
1326
1337
|
}
|
|
1327
1338
|
});
|
|
1328
1339
|
|
|
@@ -1354,20 +1365,23 @@ export function apply(ctx, config) {
|
|
|
1354
1365
|
await completeCliAuth(tool, deviceCode, { timeoutMs: 25_000 });
|
|
1355
1366
|
log.info(`cli auth complete ok: tool=${tool} elapsed=${Date.now() - startedAt}ms`);
|
|
1356
1367
|
pendingCliAuth.delete(tool);
|
|
1357
|
-
const detail = await probeCliDetail(tool
|
|
1368
|
+
const detail = await probeCliDetail(tool, { withLatest: false })
|
|
1369
|
+
.then(withCliUpgradeGuard).catch(() => null);
|
|
1358
1370
|
sendJson(response, 200, { ok: true, tool: detail });
|
|
1359
1371
|
} catch (error) {
|
|
1360
1372
|
// 超时=我们主动掐断轮询(用户还没在授权页同意):保留会话,202
|
|
1361
|
-
//
|
|
1362
|
-
//
|
|
1373
|
+
// 让前端继续等;终态(设备码失效/被拒、以及安装损坏/命令缺失
|
|
1374
|
+
// 这类不会因重试自愈的本地故障)才删会话——否则每 3s 重发
|
|
1375
|
+
// complete、每轮再起一个子进程,直到设备码过期。
|
|
1363
1376
|
const message = String(error?.message || '');
|
|
1364
|
-
const terminal =
|
|
1377
|
+
const terminal = isTerminalCliAuthFailure(error);
|
|
1365
1378
|
// 快速失败 ≠ 还在等:真实用户机实测,lark-cli 在部分宿主环境拒绝
|
|
1366
1379
|
// 阻塞轮询(打印 [AI agent] 提示后退出),且设备码被同意消费后
|
|
1367
1380
|
// 重放会快速报 JSON 错——这两种情况用户其实已完成授权,纯 202
|
|
1368
1381
|
// 会让弹窗永远停在「正在等待」。用 auth status 实测兜底:已授权
|
|
1369
1382
|
// 直接按成功返回,弹窗关闭、状态刷新。
|
|
1370
|
-
const detail = await probeCliDetail(tool
|
|
1383
|
+
const detail = await probeCliDetail(tool, { withLatest: false })
|
|
1384
|
+
.then(withCliUpgradeGuard).catch(() => null);
|
|
1371
1385
|
if (detail?.state === 'authorized') {
|
|
1372
1386
|
pendingCliAuth.delete(tool);
|
|
1373
1387
|
log.info(`cli auth complete ok (via status probe): tool=${tool}`);
|
|
@@ -1378,7 +1392,7 @@ export function apply(ctx, config) {
|
|
|
1378
1392
|
log.warn(`cli auth complete ${terminal ? 'terminal' : 'timeout'}: tool=${tool} message=${message.slice(0, 160)}`);
|
|
1379
1393
|
sendJson(response, terminal ? 502 : 202, {
|
|
1380
1394
|
pending: !terminal,
|
|
1381
|
-
error: message,
|
|
1395
|
+
error: hasChineseText(message) ? message : '授权确认失败,请稍后重试',
|
|
1382
1396
|
});
|
|
1383
1397
|
}
|
|
1384
1398
|
} catch (error) {
|
package/lib/page.js
CHANGED
|
@@ -484,6 +484,10 @@ const renderCli = async (s) => {
|
|
|
484
484
|
body.innerHTML = '<div class="skill-sub">平台未开启 CLI 功能。</div>';
|
|
485
485
|
return;
|
|
486
486
|
}
|
|
487
|
+
// 请求代际:安装/授权成功会直接写缓存重绘,慢的旧 status 响应回来时
|
|
488
|
+
// 若已有更新的一轮在途(或已完成),丢弃——否则旧响应会把新状态覆盖回
|
|
489
|
+
// 未安装/未授权
|
|
490
|
+
const seq = (renderCli._seq = (renderCli._seq || 0) + 1);
|
|
487
491
|
// 真实探测本机安装与授权态(数秒)。有旧数据(renderCli._tools)时先用
|
|
488
492
|
// 缓存渲染再后台刷新——不清屏闪「检测中…」,内容回来后整块替换一次。
|
|
489
493
|
if (renderCli._tools && Object.keys(renderCli._tools).length) {
|
|
@@ -497,11 +501,12 @@ const renderCli = async (s) => {
|
|
|
497
501
|
const resp = await fetch('api/cli/status');
|
|
498
502
|
const data = await resp.json().catch(() => ({}));
|
|
499
503
|
if (!resp.ok) throw new Error(data.error || '检测失败');
|
|
504
|
+
if (seq !== renderCli._seq) return;
|
|
500
505
|
tools = data.tools || {};
|
|
501
506
|
python = data.python || null;
|
|
502
507
|
} catch {
|
|
503
508
|
// 探测失败:已有内容保持不动,首载才显示错误
|
|
504
|
-
if (!renderCli._tools || !Object.keys(renderCli._tools).length) {
|
|
509
|
+
if (seq === renderCli._seq && (!renderCli._tools || !Object.keys(renderCli._tools).length)) {
|
|
505
510
|
body.innerHTML = '<div class="skill-sub" style="color:var(--err)">CLI 状态检测失败,请稍后重试。</div>';
|
|
506
511
|
}
|
|
507
512
|
return;
|
|
@@ -533,20 +538,24 @@ const renderCliData = (s, tools, python) => {
|
|
|
533
538
|
? '<span class="pill ok" style="font-size:11px">已授权</span>'
|
|
534
539
|
: state === 'unauthorized'
|
|
535
540
|
? '<span class="pill warn" style="font-size:11px">未授权</span>'
|
|
536
|
-
:
|
|
541
|
+
: info.reason === 'broken_install'
|
|
542
|
+
? '<span class="pill warn" style="font-size:11px">安装损坏</span>'
|
|
543
|
+
: '<span class="pill warn" style="font-size:11px">未安装</span>';
|
|
537
544
|
const versionLine = meta.desc + ' · ' + meta.label
|
|
545
|
+
+ (info.reason === 'broken_install' ? ' · 检测到文件不完整,请修复安装' : '')
|
|
538
546
|
+ (info.version ? ' · v' + esc(info.version) : '')
|
|
539
547
|
+ (info.latest && info.latest !== info.version
|
|
540
548
|
? ' → <b>v' + esc(info.latest) + '</b>'
|
|
541
549
|
: '');
|
|
542
550
|
const upgradable = info.latest && info.latest !== info.version;
|
|
543
|
-
// 已装未授权 →
|
|
551
|
+
// 已装未授权 → 「去授权」;残缺安装 → 「修复安装」
|
|
544
552
|
const actions = [
|
|
545
553
|
state === 'unauthorized'
|
|
546
554
|
? '<button class="btn small primary" data-cli-auth="' + esc(name) + '">去授权</button>'
|
|
547
555
|
: '',
|
|
548
556
|
state === 'missing'
|
|
549
|
-
? '<button class="btn small primary" data-cli-install="' + esc(name) + '"
|
|
557
|
+
? '<button class="btn small primary" data-cli-install="' + esc(name) + '">'
|
|
558
|
+
+ (info.reason === 'broken_install' ? '修复安装' : '安装') + '</button>'
|
|
550
559
|
: '',
|
|
551
560
|
upgradable
|
|
552
561
|
? '<button class="btn small ghost" data-cli-upgrade="' + esc(name) + '">升级</button>'
|
|
@@ -706,6 +715,7 @@ const cliAuthDialog = async (tool) => {
|
|
|
706
715
|
* ccpg/python 走官方安装器,耗时可达数分钟。响应自带安装后立即探测的
|
|
707
716
|
* 详情,直接合进缓存刷新界面。 */
|
|
708
717
|
const cliInstall = async (tool, btn) => {
|
|
718
|
+
const originalText = btn.textContent;
|
|
709
719
|
btn.disabled = true;
|
|
710
720
|
btn.textContent = '安装中,可能需要 3–5 分钟…';
|
|
711
721
|
try {
|
|
@@ -730,7 +740,7 @@ const cliInstall = async (tool, btn) => {
|
|
|
730
740
|
} catch (error) {
|
|
731
741
|
alert(error.message || 'CLI 安装失败,请稍后重试');
|
|
732
742
|
btn.disabled = false;
|
|
733
|
-
btn.textContent =
|
|
743
|
+
btn.textContent = originalText;
|
|
734
744
|
}
|
|
735
745
|
};
|
|
736
746
|
|
package/lib/proc.js
CHANGED
|
@@ -12,6 +12,7 @@ import { join, dirname, isAbsolute, delimiter as pathDelimiter } from 'node:path
|
|
|
12
12
|
import { promisify } from 'node:util';
|
|
13
13
|
|
|
14
14
|
const execFileAsync = promisify(execFile);
|
|
15
|
+
const preferredCommandCache = new Map();
|
|
15
16
|
|
|
16
17
|
/** 常见安装目录兑底(环境 PATH 之外的补充):GUI 启动的 DSH Desktop 继承
|
|
17
18
|
* launchd 的 PATH(/usr/bin:/bin/…),不含 nvm/homebrew/.local —— 实测
|
|
@@ -187,6 +188,7 @@ export function childPathWithExtras(originalPath, extras, shimDirs) {
|
|
|
187
188
|
export function resetExtraBinDirsCache() {
|
|
188
189
|
extraBinDirsCache = null;
|
|
189
190
|
desktopShimDirsCache = null;
|
|
191
|
+
preferredCommandCache.clear();
|
|
190
192
|
}
|
|
191
193
|
|
|
192
194
|
/** 候选扩展名顺序(纯函数,供测试):win32 下 CreateProcess 只认可执行
|
|
@@ -265,6 +267,9 @@ export function resolveCommandCandidates(tool) {
|
|
|
265
267
|
|
|
266
268
|
/** 兼容旧签名:找不到时返回原名(后续 ENOENT 走既有 missing/降级语义)。 */
|
|
267
269
|
export function resolveCommand(tool) {
|
|
270
|
+
const preferred = preferredCommandCache.get(tool);
|
|
271
|
+
if (preferred && existsSync(preferred)) return preferred;
|
|
272
|
+
if (preferred) preferredCommandCache.delete(tool);
|
|
268
273
|
return resolveCommandOrNull(tool) || tool;
|
|
269
274
|
}
|
|
270
275
|
|
|
@@ -291,8 +296,7 @@ export function decodeOutput(buf) {
|
|
|
291
296
|
* cmd.exe /d /s /c 路由,cmd.exe 在第三个兑底目录(System32)。只前置
|
|
292
297
|
* 解析到的那个目录会让 shim 内部找不到依赖,必须整组前置。输出按
|
|
293
298
|
* encoding:buffer 收回后 decodeOutput(GBK 容错)。opts.env 只传增量变量。 */
|
|
294
|
-
function
|
|
295
|
-
const resolved = resolveCommand(tool);
|
|
299
|
+
function execResolvedPath(resolved, args, { timeoutMs, cwd, env } = {}) {
|
|
296
300
|
const childEnv = { ...process.env, ...desktopShimEnvDefaults(), ...(env || {}) };
|
|
297
301
|
const key = pathEnvKey(childEnv, process.platform);
|
|
298
302
|
const extras = extraBinDirs().filter((dir) => existsSync(dir));
|
|
@@ -319,6 +323,35 @@ function execResolved(tool, args, { timeoutMs, cwd, env } = {}) {
|
|
|
319
323
|
);
|
|
320
324
|
}
|
|
321
325
|
|
|
326
|
+
function execResolved(tool, args, options = {}) {
|
|
327
|
+
return execResolvedPath(resolveCommand(tool), args, options);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function isBrokenCliOutput(message) {
|
|
331
|
+
return /MODULE_NOT_FOUND|ERR_MODULE_NOT_FOUND|Cannot find (?:module|package)/i.test(String(message || ''));
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** 中文错误契约的兜底判定:消息含中文才直接透传给用户,否则包一层
|
|
335
|
+
* 中文说明(技术详情进日志)。 */
|
|
336
|
+
export function hasChineseText(text) {
|
|
337
|
+
return /[\u4e00-\u9fff]/.test(String(text || ''));
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export function isWindowsFileAccessError(message, platform = process.platform) {
|
|
341
|
+
if (platform !== 'win32') return false;
|
|
342
|
+
const raw = String(message || '');
|
|
343
|
+
// EBUSY:Windows「文件正被另一进程使用」(npm 安装被占用的另一种形态)
|
|
344
|
+
return /(?:EPERM|EACCES|EBUSY|errno\s*:?\s*-4048|operation not permitted)/i.test(raw)
|
|
345
|
+
&& /(?:npm|@larksuite[\\/]cli|node_modules|cleanup)/i.test(raw);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function cliFailure(kind, message, raw = '') {
|
|
349
|
+
const error = new Error(message);
|
|
350
|
+
error.kind = kind;
|
|
351
|
+
error.raw = raw;
|
|
352
|
+
return error;
|
|
353
|
+
}
|
|
354
|
+
|
|
322
355
|
/** 从 pnpm 冗长输出里提取对用户有意义的错误行。 */
|
|
323
356
|
export function firstErrorLines(raw) {
|
|
324
357
|
const lines = String(raw).split('\n').map((line) => line.trim()).filter(Boolean);
|
|
@@ -350,6 +383,11 @@ export function unexpectedStoreGuidance() {
|
|
|
350
383
|
* async 函数;execFile 不经过 shell,参数数组中的每个元素都是独立参数,
|
|
351
384
|
* 不存在拼接解释执行。
|
|
352
385
|
*/
|
|
386
|
+
function isLarkCliTool(tool) {
|
|
387
|
+
const name = String(tool || '').replace(/\\/g, '/').split('/').pop().toLowerCase();
|
|
388
|
+
return ['lark-cli', 'lark-cli.cmd', 'lark-cli.exe'].includes(name);
|
|
389
|
+
}
|
|
390
|
+
|
|
353
391
|
export async function runCliTool(tool, args, { timeoutMs = 300000, cwd, env } = {}) {
|
|
354
392
|
try {
|
|
355
393
|
const { stdout, stderr } = await execResolved(tool, args, {
|
|
@@ -360,17 +398,39 @@ export async function runCliTool(tool, args, { timeoutMs = 300000, cwd, env } =
|
|
|
360
398
|
return { stdout, stderr };
|
|
361
399
|
} catch (error) {
|
|
362
400
|
if (error.code === 'ENOENT') {
|
|
363
|
-
throw
|
|
401
|
+
throw cliFailure('missing_command', missingCommandGuidance(tool));
|
|
364
402
|
}
|
|
365
403
|
if (error.killed) {
|
|
366
|
-
throw
|
|
404
|
+
throw cliFailure('timeout', '命令执行超时,请稍后重试');
|
|
367
405
|
}
|
|
368
406
|
const raw = `${error.stderr || ''}\n${error.stdout || ''}`;
|
|
369
407
|
if (isUnexpectedStoreError(raw)) {
|
|
370
|
-
throw
|
|
408
|
+
throw cliFailure('pnpm_store_conflict', unexpectedStoreGuidance(), raw);
|
|
409
|
+
}
|
|
410
|
+
const isLarkNpmInstall = String(tool).toLowerCase().includes('npm')
|
|
411
|
+
&& args.includes('@larksuite/cli@latest');
|
|
412
|
+
// spawn 级失败(杀软拦截执行等)没有 stdout/stderr:错误码与 message
|
|
413
|
+
// 里带命令路径,单独送检
|
|
414
|
+
const spawnText = `${error.code || ''}\n${error.message || ''}`;
|
|
415
|
+
if (isLarkNpmInstall && (isWindowsFileAccessError(raw) || isWindowsFileAccessError(spawnText))) {
|
|
416
|
+
throw cliFailure(
|
|
417
|
+
'windows_file_access',
|
|
418
|
+
'Windows 无法更新 lark-cli 文件,可能有程序或安全软件正在占用安装目录。请关闭正在运行的 lark-cli 或相关终端后重试;仍失败时请重启 Windows 后再次安装。',
|
|
419
|
+
raw,
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
if (isLarkCliTool(tool) && isBrokenCliOutput(raw)) {
|
|
423
|
+
throw cliFailure('broken_install', 'lark-cli 安装不完整,请先点击「修复安装」后再授权。', raw);
|
|
371
424
|
}
|
|
372
425
|
const message = firstErrorLines(raw);
|
|
373
|
-
|
|
426
|
+
// 英文技术行不直接弹给用户:中文前缀 + 技术详情尾随(与 ccpg 下载
|
|
427
|
+
// 失败的处理同口径),完整原文保留在 error.raw 供日志诊断
|
|
428
|
+
const userMessage = hasChineseText(message)
|
|
429
|
+
? message
|
|
430
|
+
: message
|
|
431
|
+
? `命令执行失败。技术详情:${message}`
|
|
432
|
+
: `命令执行失败(退出码 ${error.code ?? '未知'})`;
|
|
433
|
+
throw cliFailure('command_failed', userMessage, raw);
|
|
374
434
|
}
|
|
375
435
|
}
|
|
376
436
|
|
|
@@ -492,37 +552,134 @@ export function parseCliVersion(stdout) {
|
|
|
492
552
|
return match ? match[1] : null;
|
|
493
553
|
}
|
|
494
554
|
|
|
555
|
+
export async function selectWorkingCliCandidate(candidates, probe) {
|
|
556
|
+
let sawBrokenInstall = false;
|
|
557
|
+
for (const candidate of candidates) {
|
|
558
|
+
try {
|
|
559
|
+
const result = await probe(candidate);
|
|
560
|
+
const output = `${result?.stdout || ''}\n${result?.stderr || ''}`;
|
|
561
|
+
// 损坏判定必须先于版本解析:cmd 包装未传播退出码时 Node 的
|
|
562
|
+
// MODULE_NOT_FOUND 堆栈会走 resolve 路径,尾行「Node.js v24.18.0」
|
|
563
|
+
// 能被 parseCliVersion 解析出版本号——先收版本会把损坏 shim 当成
|
|
564
|
+
// 可用候选缓存起来,恰好废掉「跳过损坏 shim」的核心目标
|
|
565
|
+
if (isBrokenCliOutput(output)) {
|
|
566
|
+
sawBrokenInstall = true;
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
const version = parseCliVersion(output);
|
|
570
|
+
if (version) return { command: candidate, version, reason: null };
|
|
571
|
+
} catch (error) {
|
|
572
|
+
const raw = `${error?.stderr || ''}\n${error?.stdout || ''}\n${error?.message || ''}`;
|
|
573
|
+
if (isBrokenCliOutput(raw)) sawBrokenInstall = true;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
return {
|
|
577
|
+
command: null,
|
|
578
|
+
version: null,
|
|
579
|
+
reason: sawBrokenInstall ? 'broken_install' : 'unavailable',
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
export function shouldFallbackToPnpm(error, { platform = process.platform, pnpmAvailable = false } = {}) {
|
|
584
|
+
return platform === 'win32'
|
|
585
|
+
&& pnpmAvailable
|
|
586
|
+
&& ['windows_file_access', 'broken_install'].includes(error?.kind);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/** complete 轮询的终态判定:设备码过期/被拒之外,本地确定性故障(安装
|
|
590
|
+
* 损坏、命令缺失)不会因重试自愈——按终态返回,避免前端每 3s 重发、
|
|
591
|
+
* 每轮再起一个子进程直到设备码过期。timeout 是我们主动掐断的等待,
|
|
592
|
+
* 非终态。 */
|
|
593
|
+
export function isTerminalCliAuthFailure(error) {
|
|
594
|
+
if (['broken_install', 'missing_command'].includes(error?.kind)) return true;
|
|
595
|
+
return /expired|invalid|denied|拒绝|失效|过期/i.test(String(error?.message || ''));
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/** 从 pnpm -g 前置校验报错里解析全局 bin 目录。pnpm 12 起输出会把
|
|
599
|
+
* "is not in PATH" 折行并加竖线装饰(单行正则取不到),且 runCliTool
|
|
600
|
+
* 的摘要只保留 ERR_ 行(路径被丢弃)——必须对 error.raw 完整输出跑。 */
|
|
601
|
+
export function extractPnpmGlobalBinDir(text) {
|
|
602
|
+
return /global bin directory\s+"(.+?)"\s+is not(?:\s*\n[^\n]*)?\s+in PATH/i
|
|
603
|
+
.exec(String(text || ''))?.[1] || null;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
async function resolveWorkingCliCommand(tool, timeoutMs = 10_000) {
|
|
607
|
+
const preferred = preferredCommandCache.get(tool);
|
|
608
|
+
const candidates = [preferred, ...resolveCommandCandidates(tool)]
|
|
609
|
+
.filter(Boolean)
|
|
610
|
+
.filter((candidate, index, all) => all.indexOf(candidate) === index);
|
|
611
|
+
const selected = await selectWorkingCliCandidate(candidates, (candidate) => execResolvedPath(
|
|
612
|
+
candidate,
|
|
613
|
+
['--version'],
|
|
614
|
+
{ timeoutMs },
|
|
615
|
+
));
|
|
616
|
+
if (selected.command) {
|
|
617
|
+
preferredCommandCache.set(tool, selected.command);
|
|
618
|
+
} else {
|
|
619
|
+
preferredCommandCache.delete(tool);
|
|
620
|
+
}
|
|
621
|
+
return selected;
|
|
622
|
+
}
|
|
623
|
+
|
|
495
624
|
/**
|
|
496
|
-
* 三态探测 + 版本 + 最新版(任一失败降级为 null
|
|
497
|
-
*
|
|
498
|
-
*
|
|
499
|
-
*
|
|
500
|
-
* version`(版本号全局一致,对 npm/pnpm/手动安装都成立)。
|
|
501
|
-
* npm view 需要 npm 在 PATH(node 环境下成立);查询超时/失败返回 null,
|
|
502
|
-
* 前端只显示当前版本不显示升级按钮。
|
|
625
|
+
* 三态探测 + 版本 + 最新版(任一失败降级为 null):先验证所有同名 CLI
|
|
626
|
+
* 候选的 --version,跳过 Windows 上 npm 回滚留下的损坏 shim,再读取授权状态。
|
|
627
|
+
* withLatest=false 跳过 latest 查询(两轮 registry curl 最坏 ~40s)——
|
|
628
|
+
* 授权 begin/complete 只需要三态,别让用户在弹窗里等版本号查询。
|
|
503
629
|
*/
|
|
504
|
-
export async function probeCliDetail(tool, { timeoutMs = 15000 } = {}) {
|
|
505
|
-
|
|
506
|
-
|
|
630
|
+
export async function probeCliDetail(tool, { timeoutMs = 15000, withLatest = true } = {}) {
|
|
631
|
+
const selected = await resolveWorkingCliCommand(tool, Math.min(timeoutMs, 10_000));
|
|
632
|
+
if (!selected.command) {
|
|
633
|
+
return {
|
|
634
|
+
state: 'missing',
|
|
635
|
+
reason: selected.reason,
|
|
636
|
+
version: null,
|
|
637
|
+
latest: null,
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
let stdout = '';
|
|
507
642
|
try {
|
|
508
|
-
({ stdout } = await
|
|
643
|
+
({ stdout } = await execResolvedPath(selected.command, ['auth', 'status'], {
|
|
509
644
|
timeoutMs,
|
|
510
645
|
env: tool === 'ccpg-cli' ? { CCPG_FULL_CLI: '1' } : undefined,
|
|
511
646
|
}));
|
|
512
647
|
} catch (error) {
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
648
|
+
const raw = `${error.stdout || ''}\n${error.stderr || ''}`;
|
|
649
|
+
if (isBrokenCliOutput(raw)) {
|
|
650
|
+
return {
|
|
651
|
+
state: 'missing',
|
|
652
|
+
reason: 'broken_install',
|
|
653
|
+
version: selected.version,
|
|
654
|
+
latest: null,
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
stdout = raw;
|
|
516
658
|
}
|
|
517
|
-
|
|
659
|
+
const state = larkStyleAuthorized(stdout) || ccpgStyleAuthorized(stdout)
|
|
660
|
+
? 'authorized'
|
|
661
|
+
: 'unauthorized';
|
|
662
|
+
const latest = withLatest
|
|
663
|
+
? await cliLatestVersion(tool, timeoutMs).catch(() => null)
|
|
664
|
+
: null;
|
|
665
|
+
return { state, version: selected.version, latest };
|
|
666
|
+
}
|
|
518
667
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
])
|
|
525
|
-
|
|
668
|
+
/** npm registry 的包 latest 版本(curl 直查 JSON,与机器上有无 node/
|
|
669
|
+
* npm/pnpm 完全解耦——工具链兜底链不可靠:npm 全局装的 pnpm.cmd 内部
|
|
670
|
+
* 仍要调 npm,barren 环境实测 pnpm view 报「'npm' 不是内部或外部命令」)。
|
|
671
|
+
* npmmirror 优先(国内),npmjs 兜底。 */
|
|
672
|
+
async function registryLatestVersion(name, timeoutMs = 25_000) {
|
|
673
|
+
for (const url of [`https://registry.npmmirror.com/${name}/latest`, `https://registry.npmjs.org/${name}/latest`]) {
|
|
674
|
+
try {
|
|
675
|
+
const { stdout } = await runCliTool('curl', ['-fsSL', '--max-time', '15', url], { timeoutMs });
|
|
676
|
+
const version = JSON.parse(String(stdout))?.version;
|
|
677
|
+
if (typeof version === 'string' && version) return version;
|
|
678
|
+
} catch {
|
|
679
|
+
// 换下一个源
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
return null;
|
|
526
683
|
}
|
|
527
684
|
|
|
528
685
|
/** 最新版查询:ccpg-cli 官方 check 接口;lark-cli 查 npm 包 dist-tag。 */
|
|
@@ -536,10 +693,16 @@ async function cliLatestVersion(tool, timeoutMs) {
|
|
|
536
693
|
return typeof data?.latest_version === 'string' ? data.latest_version : null;
|
|
537
694
|
}
|
|
538
695
|
if (tool === 'lark-cli') {
|
|
539
|
-
//
|
|
540
|
-
//
|
|
541
|
-
const
|
|
542
|
-
|
|
696
|
+
// curl 直查 registry(系统自带,零工具链依赖);失败再退 npm view。
|
|
697
|
+
// 升级检测不该依赖机器上恰好有哪套 node 工具链
|
|
698
|
+
const registry = await registryLatestVersion('@larksuite/cli', timeoutMs).catch(() => null);
|
|
699
|
+
if (registry) return registry;
|
|
700
|
+
try {
|
|
701
|
+
const { stdout } = await runCliTool('npm', ['view', '@larksuite/cli', 'version'], { timeoutMs });
|
|
702
|
+
return parseCliVersion(stdout);
|
|
703
|
+
} catch {
|
|
704
|
+
return null;
|
|
705
|
+
}
|
|
543
706
|
}
|
|
544
707
|
return null;
|
|
545
708
|
}
|
|
@@ -586,12 +749,81 @@ async function installLarkSkills({ timeoutMs = 180_000 } = {}) {
|
|
|
586
749
|
} catch {
|
|
587
750
|
// 目录不存在 → 未安装,继续
|
|
588
751
|
}
|
|
589
|
-
const
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
752
|
+
const args = ['skills', 'add', 'larksuite/cli', '-g', '--all', '--dangerously-accept-openclaw-risks'];
|
|
753
|
+
const tail = (out) => String(out).split('\n').map((l) => l.trim()).filter(Boolean).slice(-3).join('\n');
|
|
754
|
+
// 包执行器降级链:优先 npx(npm 机器);无 npm 的机器(仅独立 pnpm 或
|
|
755
|
+
// DSH Desktop 自带 shim)走 pnpm dlx——真实用户机 pnpm 装完 lark-cli 后
|
|
756
|
+
// 卡在 npx 缺失的引导文案上,被当成安装失败报上来;两者都缺则温和
|
|
757
|
+
// 跳过(技能是增强项,不该阻断安装成功的呈现)
|
|
758
|
+
if (resolveCommandOrNull('npx')) {
|
|
759
|
+
const { stdout } = await runCliTool('npx', ['-y', ...args], { timeoutMs });
|
|
760
|
+
return tail(stdout);
|
|
761
|
+
}
|
|
762
|
+
if (resolveCommandOrNull('pnpm')) {
|
|
763
|
+
const { stdout } = await runCliTool('pnpm', ['dlx', ...args], { timeoutMs });
|
|
764
|
+
return tail(stdout);
|
|
765
|
+
}
|
|
766
|
+
return 'lark-cli 已安装。默认技能包暂跳过(本机暂无 npm/npx,不影响 CLI 功能;如需技能可安装 Node.js 后重新点「安装」)';
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function rememberGlobalBinDir(dir) {
|
|
770
|
+
const value = String(dir || '').trim();
|
|
771
|
+
if (!value) return;
|
|
772
|
+
const existing = extraBinDirs();
|
|
773
|
+
extraBinDirsCache = [value, ...existing.filter((item) => item.toLowerCase() !== value.toLowerCase())];
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
async function installLarkCliWithPnpm(timeoutMs) {
|
|
777
|
+
const pnpmHome = process.env.PNPM_HOME
|
|
778
|
+
|| join(process.env.LOCALAPPDATA || join(homedir(), 'AppData', 'Local'), 'pnpm');
|
|
779
|
+
const pnpmEnv = { PNPM_HOME: pnpmHome };
|
|
780
|
+
const pnpmInstall = () => runCliTool(
|
|
781
|
+
'pnpm',
|
|
782
|
+
['add', '-g', '@larksuite/cli@latest'],
|
|
783
|
+
{ timeoutMs, env: pnpmEnv },
|
|
784
|
+
);
|
|
785
|
+
let stdout;
|
|
786
|
+
try {
|
|
787
|
+
({ stdout } = await pnpmInstall());
|
|
788
|
+
} catch (error) {
|
|
789
|
+
// pnpm -g 前置校验要求全局 bin 目录在 PATH 中;自定义安装位不在
|
|
790
|
+
// GUI PATH 时,从错误中取出目录并重试一次。路径只在完整输出(raw)
|
|
791
|
+
// 里且被折行——见 extractPnpmGlobalBinDir。
|
|
792
|
+
const dir = extractPnpmGlobalBinDir(`${error?.raw || ''}\n${error?.message || ''}`);
|
|
793
|
+
if (!dir) throw error;
|
|
794
|
+
rememberGlobalBinDir(dir);
|
|
795
|
+
({ stdout } = await pnpmInstall());
|
|
796
|
+
}
|
|
797
|
+
try {
|
|
798
|
+
const { stdout: binOut } = await runCliTool('pnpm', ['bin', '-g'], {
|
|
799
|
+
timeoutMs: 20_000,
|
|
800
|
+
env: pnpmEnv,
|
|
801
|
+
});
|
|
802
|
+
const dir = String(binOut || '').split('\n').map((line) => line.trim()).filter(Boolean).pop();
|
|
803
|
+
if (dir) {
|
|
804
|
+
rememberGlobalBinDir(dir);
|
|
805
|
+
const candidate = candidateSuffixes(process.platform)
|
|
806
|
+
.map((suffix) => join(dir, `lark-cli${suffix}`))
|
|
807
|
+
.find((item) => existsSync(item));
|
|
808
|
+
if (candidate) preferredCommandCache.set('lark-cli', candidate);
|
|
809
|
+
}
|
|
810
|
+
} catch {
|
|
811
|
+
// 静态兑底目录仍可供后续候选探测使用。
|
|
812
|
+
}
|
|
813
|
+
return stdout;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
async function verifyLarkCliInstall(timeoutMs) {
|
|
817
|
+
const selected = await resolveWorkingCliCommand('lark-cli', Math.min(timeoutMs, 30_000));
|
|
818
|
+
if (!selected.command) {
|
|
819
|
+
if (selected.reason === 'broken_install') {
|
|
820
|
+
throw cliFailure('broken_install', 'lark-cli 安装不完整,请点击「修复安装」重试。');
|
|
821
|
+
}
|
|
822
|
+
// 不点名按钮:unavailable 时页面按钮文案随上次状态是「安装」或
|
|
823
|
+
// 「修复安装」,点名反而对不上
|
|
824
|
+
throw cliFailure('install_verify_failed', 'lark-cli 安装未完成,请稍后重试;若持续失败请重启 DSH Desktop 后再试。');
|
|
825
|
+
}
|
|
826
|
+
return selected;
|
|
595
827
|
}
|
|
596
828
|
|
|
597
829
|
/**
|
|
@@ -606,38 +838,37 @@ async function installLarkSkills({ timeoutMs = 180_000 } = {}) {
|
|
|
606
838
|
export async function installCliTool(tool, { timeoutMs = 600_000 } = {}) {
|
|
607
839
|
let stdout;
|
|
608
840
|
if (tool === 'lark-cli') {
|
|
609
|
-
//
|
|
610
|
-
//
|
|
611
|
-
// (用户目录免管理员,npmmirror 镜像),再用其自带 npm 装 lark-cli。
|
|
612
|
-
// lark-cli 官方支持 pnpm 安装方式,装出来的二进制与 npm 路径一致,
|
|
613
|
-
// 后续 lark-cli update 也能自升级。
|
|
841
|
+
// 优先 npm;Windows 上 npm 文件被占用或安装后仍是残缺 shim 时,
|
|
842
|
+
// 仅在 pnpm 可用的情况下切换到 pnpm,避免用另一套安装器掩盖网络错误。
|
|
614
843
|
if (resolveCommandOrNull('npm')) {
|
|
615
|
-
|
|
616
|
-
} else if (resolveCommandOrNull('pnpm')) {
|
|
617
|
-
const pnpmInstall = () => runCliTool('pnpm', ['add', '-g', '@larksuite/cli@latest'], { timeoutMs });
|
|
844
|
+
let npmError = null;
|
|
618
845
|
try {
|
|
619
|
-
({ stdout } = await
|
|
846
|
+
({ stdout } = await runCliTool('npm', ['install', '-g', '@larksuite/cli@latest'], { timeoutMs }));
|
|
847
|
+
await verifyLarkCliInstall(timeoutMs);
|
|
620
848
|
} catch (error) {
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
resetExtraBinDirsCache();
|
|
632
|
-
}
|
|
849
|
+
npmError = error;
|
|
850
|
+
}
|
|
851
|
+
const canFallback = shouldFallbackToPnpm(npmError, {
|
|
852
|
+
pnpmAvailable: Boolean(resolveCommandOrNull('pnpm')),
|
|
853
|
+
});
|
|
854
|
+
if (npmError && !canFallback) throw npmError;
|
|
855
|
+
if (canFallback) {
|
|
856
|
+
const pnpmOutput = await installLarkCliWithPnpm(timeoutMs);
|
|
857
|
+
stdout = `${stdout || ''}\n${pnpmOutput || ''}`;
|
|
858
|
+
await verifyLarkCliInstall(timeoutMs);
|
|
633
859
|
}
|
|
860
|
+
} else if (resolveCommandOrNull('pnpm')) {
|
|
861
|
+
stdout = await installLarkCliWithPnpm(timeoutMs);
|
|
862
|
+
await verifyLarkCliInstall(timeoutMs);
|
|
634
863
|
} else if (process.platform === 'win32') {
|
|
635
864
|
const nodeNote = await installNodeWindows(timeoutMs);
|
|
636
865
|
({ stdout } = await runCliTool('npm', ['install', '-g', '@larksuite/cli@latest'], { timeoutMs }));
|
|
637
866
|
stdout = `${nodeNote}\n${stdout}`;
|
|
867
|
+
await verifyLarkCliInstall(timeoutMs);
|
|
638
868
|
} else {
|
|
639
869
|
throw new Error(missingCommandGuidance('npm'));
|
|
640
870
|
}
|
|
871
|
+
|
|
641
872
|
const skillsNote = await installLarkSkills().catch((error) => {
|
|
642
873
|
// 软失败:技能装不上不影响 CLI 本体已装成功(如 GitHub 不可达)
|
|
643
874
|
return `lark-cli skills install soft-failed: ${error?.message || error}`;
|
|
@@ -879,7 +1110,10 @@ export async function upgradeCliTool(tool, { timeoutMs = 300_000 } = {}) {
|
|
|
879
1110
|
const lines = String(stdout).split('\n').map((l) => l.trim()).filter(Boolean);
|
|
880
1111
|
return lines.slice(-6).join('\n');
|
|
881
1112
|
} catch (error) {
|
|
882
|
-
if (tool === 'lark-cli'
|
|
1113
|
+
if (tool === 'lark-cli') {
|
|
1114
|
+
// 老版本 update 子命令不存在(unknown command)→ 重装即升级;其余
|
|
1115
|
+
// 失败(如 pnpm 装的 lark-cli 内部走 npm 自升级而本机无 npm)也统
|
|
1116
|
+
// 一落到 installCliTool——它按机器环境选对安装器并兜底探测落点
|
|
883
1117
|
return installCliTool('lark-cli', { timeoutMs });
|
|
884
1118
|
}
|
|
885
1119
|
throw error;
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-agentone",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.29",
|
|
4
4
|
"description": "AgentOne 平台集成插件:飞书登录、平台模型(同步失败自愈重试)、SkillHub 技能(升级检测一键升级、装后即时刷新、套餐技能自动同步)、套餐申请、插件管理与自助升级、lark-cli / ccpg-cli 探测与一键安装(请求超时与错误码透传、令牌轮换 single-flight、凭据损坏容错与并发安全写入、状态页 UX 增强)",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"smoke": "node scripts/env-matrix-smoke.mjs"
|
|
8
|
+
},
|
|
6
9
|
"main": "lib/index.js",
|
|
7
10
|
"exports": {
|
|
8
11
|
".": "./lib/index.js",
|