dsh-agentone 0.5.28 → 0.5.30
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/README.md +6 -0
- package/lib/index.js +28 -10
- package/lib/language.js +71 -0
- package/lib/page.js +15 -5
- package/lib/proc.js +269 -62
- package/lib/store.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -36,6 +36,11 @@ lark-cli / ccpg-cli 状态探测。
|
|
|
36
36
|
ccpg 走公司 `auto-install.sh`,口径与平台 cli-deployd 一致);
|
|
37
37
|
已装未授权时「去授权」发起官方 Device Flow(授权链接 + ASCII 二维码 +
|
|
38
38
|
验证码 + 自动轮询确认,设备码仅存插件进程内存)。
|
|
39
|
+
- **全局中文回复默认**:插件启动时在 `$DSH_HOME/AGENTS.md` 注入受标记
|
|
40
|
+
管理的「默认始终使用中文回复」指令块(dsh 官方 agent-instructions 机制,
|
|
41
|
+
每个会话基线加载,project 级指令优先);幂等不重复注入,用户已有语言
|
|
42
|
+
偏好或删除标记块后不再介入(本机只做一次决策,记录于
|
|
43
|
+
`$DSH_HOME/agentone/language-default.json`)。
|
|
39
44
|
- **首启引导**:dsh 首次启动弹出飞书登录引导(order -50,早于官方
|
|
40
45
|
「添加 API Key」),登录一次即跳过官方密钥引导;暗色主题适配。
|
|
41
46
|
|
|
@@ -104,6 +109,7 @@ lib/
|
|
|
104
109
|
page.js 状态页与授权完成页的 HTML/CSS/JS 模板(纯静态)
|
|
105
110
|
http.js 平台请求统一通道(超时、错误码透传)
|
|
106
111
|
auth.js 登录流、令牌轮换/吊销、能力面/套餐 API
|
|
112
|
+
language.js 全局中文回复默认($DSH_HOME/AGENTS.md 受标记管理注入)
|
|
107
113
|
model.js 平台模型 → settings.yaml / .credentials.yaml
|
|
108
114
|
skill.js 技能下载/解包/卸载(zip 穿越清洗、原子落盘)
|
|
109
115
|
store.js 凭据持久化(0600;损坏自动移开)
|
package/lib/index.js
CHANGED
|
@@ -31,10 +31,11 @@ import {
|
|
|
31
31
|
import { applyPlatformModel, removePlatformModel } from './model.js';
|
|
32
32
|
import { installPlatformSkill, listPlatformSkills, removePlatformSkill } from './skill.js';
|
|
33
33
|
import { applyPlatformSkillhub, removePlatformSkillhub } from './skillhub.js';
|
|
34
|
+
import { ensureLanguageDefault } from './language.js';
|
|
34
35
|
import { beginCliAuth, cliAuthQrcode, completeCliAuth, parseCliAuthBeginOutput } from './cliauth.mjs';
|
|
35
36
|
import { platformFetch } from './http.js';
|
|
36
37
|
import { donePageHtml, statusPageHtml } from './page.js';
|
|
37
|
-
import { compareSemver, dshPluginCli, firstErrorLines, installCliTool, isUnexpectedStoreError, probeCliDetail, probePythonEnv, pnpmViewLatest, runCliTool, unexpectedStoreGuidance, upgradeCliTool } from './proc.js';
|
|
38
|
+
import { compareSemver, dshPluginCli, firstErrorLines, hasChineseText, installCliTool, isTerminalCliAuthFailure, isUnexpectedStoreError, probeCliDetail, probePythonEnv, pnpmViewLatest, runCliTool, unexpectedStoreGuidance, upgradeCliTool } from './proc.js';
|
|
38
39
|
|
|
39
40
|
import { clearCredentials, loadCredentials, resolveDshHome, saveCredentials, updateCredentials } from './store.js';
|
|
40
41
|
|
|
@@ -1302,9 +1303,17 @@ export function apply(ctx, config) {
|
|
|
1302
1303
|
sendJson(response, 400, { error: '不支持的 CLI 工具' });
|
|
1303
1304
|
return;
|
|
1304
1305
|
}
|
|
1305
|
-
|
|
1306
|
+
// 只需要三态:跳过 latest 查询(registry 两轮 curl 最坏 ~40s)
|
|
1307
|
+
const detail = await probeCliDetail(tool, { withLatest: false })
|
|
1308
|
+
.then(withCliUpgradeGuard).catch(() => null);
|
|
1306
1309
|
if (detail?.state === 'missing') {
|
|
1307
|
-
sendJson(response, 400, {
|
|
1310
|
+
sendJson(response, 400, {
|
|
1311
|
+
error: detail.reason === 'broken_install'
|
|
1312
|
+
? 'CLI 安装不完整,请先点击「修复安装」后再授权'
|
|
1313
|
+
: detail.reason === 'unavailable'
|
|
1314
|
+
? 'CLI 暂时无法访问(可能被安全软件占用或响应超时),请稍后重试;若持续失败请重新安装'
|
|
1315
|
+
: 'CLI 未安装,请先安装或联系管理员',
|
|
1316
|
+
});
|
|
1308
1317
|
return;
|
|
1309
1318
|
}
|
|
1310
1319
|
const raw = await beginCliAuth(tool);
|
|
@@ -1322,7 +1331,10 @@ export function apply(ctx, config) {
|
|
|
1322
1331
|
expires_in: parsed.expiresIn,
|
|
1323
1332
|
});
|
|
1324
1333
|
} catch (error) {
|
|
1325
|
-
|
|
1334
|
+
const message = String(error?.message || '');
|
|
1335
|
+
sendJson(response, 502, {
|
|
1336
|
+
error: hasChineseText(message) ? message : '发起授权失败,请稍后重试',
|
|
1337
|
+
});
|
|
1326
1338
|
}
|
|
1327
1339
|
});
|
|
1328
1340
|
|
|
@@ -1354,20 +1366,23 @@ export function apply(ctx, config) {
|
|
|
1354
1366
|
await completeCliAuth(tool, deviceCode, { timeoutMs: 25_000 });
|
|
1355
1367
|
log.info(`cli auth complete ok: tool=${tool} elapsed=${Date.now() - startedAt}ms`);
|
|
1356
1368
|
pendingCliAuth.delete(tool);
|
|
1357
|
-
const detail = await probeCliDetail(tool
|
|
1369
|
+
const detail = await probeCliDetail(tool, { withLatest: false })
|
|
1370
|
+
.then(withCliUpgradeGuard).catch(() => null);
|
|
1358
1371
|
sendJson(response, 200, { ok: true, tool: detail });
|
|
1359
1372
|
} catch (error) {
|
|
1360
1373
|
// 超时=我们主动掐断轮询(用户还没在授权页同意):保留会话,202
|
|
1361
|
-
//
|
|
1362
|
-
//
|
|
1374
|
+
// 让前端继续等;终态(设备码失效/被拒、以及安装损坏/命令缺失
|
|
1375
|
+
// 这类不会因重试自愈的本地故障)才删会话——否则每 3s 重发
|
|
1376
|
+
// complete、每轮再起一个子进程,直到设备码过期。
|
|
1363
1377
|
const message = String(error?.message || '');
|
|
1364
|
-
const terminal =
|
|
1378
|
+
const terminal = isTerminalCliAuthFailure(error);
|
|
1365
1379
|
// 快速失败 ≠ 还在等:真实用户机实测,lark-cli 在部分宿主环境拒绝
|
|
1366
1380
|
// 阻塞轮询(打印 [AI agent] 提示后退出),且设备码被同意消费后
|
|
1367
1381
|
// 重放会快速报 JSON 错——这两种情况用户其实已完成授权,纯 202
|
|
1368
1382
|
// 会让弹窗永远停在「正在等待」。用 auth status 实测兜底:已授权
|
|
1369
1383
|
// 直接按成功返回,弹窗关闭、状态刷新。
|
|
1370
|
-
const detail = await probeCliDetail(tool
|
|
1384
|
+
const detail = await probeCliDetail(tool, { withLatest: false })
|
|
1385
|
+
.then(withCliUpgradeGuard).catch(() => null);
|
|
1371
1386
|
if (detail?.state === 'authorized') {
|
|
1372
1387
|
pendingCliAuth.delete(tool);
|
|
1373
1388
|
log.info(`cli auth complete ok (via status probe): tool=${tool}`);
|
|
@@ -1378,7 +1393,7 @@ export function apply(ctx, config) {
|
|
|
1378
1393
|
log.warn(`cli auth complete ${terminal ? 'terminal' : 'timeout'}: tool=${tool} message=${message.slice(0, 160)}`);
|
|
1379
1394
|
sendJson(response, terminal ? 502 : 202, {
|
|
1380
1395
|
pending: !terminal,
|
|
1381
|
-
error: message,
|
|
1396
|
+
error: hasChineseText(message) ? message : '授权确认失败,请稍后重试',
|
|
1382
1397
|
});
|
|
1383
1398
|
}
|
|
1384
1399
|
} catch (error) {
|
|
@@ -1494,6 +1509,9 @@ export function apply(ctx, config) {
|
|
|
1494
1509
|
ensureBuiltinPlugins(config, log).catch((error) => {
|
|
1495
1510
|
log.warn(`[dsh-agentone] builtin plugin check failed: ${error?.message || error}`);
|
|
1496
1511
|
});
|
|
1512
|
+
ensureLanguageDefault(config, log).catch((error) => {
|
|
1513
|
+
log.warn(`[dsh-agentone] language default check failed: ${error?.message || error}`);
|
|
1514
|
+
});
|
|
1497
1515
|
(async () => {
|
|
1498
1516
|
const credentials = await ensureFreshToken(config, log);
|
|
1499
1517
|
if (!credentials) return;
|
package/lib/language.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// 全局中文回复默认(#423):在 $DSH_HOME/AGENTS.md 注入受标记管理的指令块。
|
|
2
|
+
//
|
|
3
|
+
// dsh 没有原生「回复语言」设置(locale.preference 只是 UI 字典语言,不进
|
|
4
|
+
// 模型请求);官方全局指令机制是 agent-instructions 插件把 $DSH_HOME/AGENTS.md
|
|
5
|
+
// 作为用户全局基线注入每个会话,project 级 AGENTS.md/CLAUDE.md 更具体、优先
|
|
6
|
+
// 更高——天然满足「全局默认中文、更具体指令可覆盖」。
|
|
7
|
+
//
|
|
8
|
+
// 永不覆盖用户,本机只做一次决策(flag 落 storeDir/language-default.json):
|
|
9
|
+
// 标记块已存在 → no-op
|
|
10
|
+
// flag 已写但标记块被用户删除 → no-op(不与用户拉锯)
|
|
11
|
+
// 既有内容含语言偏好关键词 → 记 skipped,no-op
|
|
12
|
+
// 其余(含文件不存在) → 追加/创建标记块,记 applied
|
|
13
|
+
// 任何失败只记日志,绝不阻断启动。
|
|
14
|
+
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
15
|
+
import { dirname, join } from 'node:path';
|
|
16
|
+
|
|
17
|
+
import { resolveDshHome, storeDir } from './store.js';
|
|
18
|
+
|
|
19
|
+
const MARKER_BEGIN = '<!-- dsh-agentone:language-default begin -->';
|
|
20
|
+
const MARKER_END = '<!-- dsh-agentone:language-default end -->';
|
|
21
|
+
/** 用户显式语言偏好的宽松信号:命中即假定用户自管语言,插件不介入。 */
|
|
22
|
+
const LANGUAGE_HINT = /中文|汉语|英文|英语|Chinese|English|language|语言|respond in|reply in/i;
|
|
23
|
+
|
|
24
|
+
function languageBlock() {
|
|
25
|
+
return [
|
|
26
|
+
MARKER_BEGIN,
|
|
27
|
+
'- 默认始终使用中文回复(dsh-agentone 注入;删除本块并重启 dsh 即取消)。',
|
|
28
|
+
MARKER_END,
|
|
29
|
+
].join('\n');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function writeAtomic(path, text) {
|
|
33
|
+
await mkdir(dirname(path), { recursive: true });
|
|
34
|
+
const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
35
|
+
await writeFile(tmp, text, 'utf8');
|
|
36
|
+
await rename(tmp, path);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 确保全局中文回复默认就位。幂等;返回决策结果供测试与日志核对。
|
|
41
|
+
*/
|
|
42
|
+
export async function ensureLanguageDefault(config, log = console) {
|
|
43
|
+
const agentsPath = join(resolveDshHome(), 'AGENTS.md');
|
|
44
|
+
const flagPath = join(storeDir(config), 'language-default.json');
|
|
45
|
+
let content = '';
|
|
46
|
+
try {
|
|
47
|
+
content = await readFile(agentsPath, 'utf8');
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
50
|
+
}
|
|
51
|
+
if (content.includes(MARKER_BEGIN)) return 'present';
|
|
52
|
+
try {
|
|
53
|
+
await readFile(flagPath, 'utf8');
|
|
54
|
+
// 决策已做过而标记块不在了:用户主动删除,尊重之。
|
|
55
|
+
return 'user-removed';
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
58
|
+
}
|
|
59
|
+
if (content.trim() && LANGUAGE_HINT.test(content)) {
|
|
60
|
+
await writeAtomic(flagPath, `${JSON.stringify({ status: 'skipped-user-pref' }, null, 2)}\n`);
|
|
61
|
+
log.info('[dsh-agentone] AGENTS.md 已有语言偏好,跳过中文默认注入');
|
|
62
|
+
return 'skipped-user-pref';
|
|
63
|
+
}
|
|
64
|
+
const next = content.trim()
|
|
65
|
+
? `${content.replace(/\s+$/, '')}\n\n${languageBlock()}\n`
|
|
66
|
+
: `${languageBlock()}\n`;
|
|
67
|
+
await writeAtomic(agentsPath, next);
|
|
68
|
+
await writeAtomic(flagPath, `${JSON.stringify({ status: 'applied' }, null, 2)}\n`);
|
|
69
|
+
log.info('[dsh-agentone] 已注入全局中文回复默认(AGENTS.md)');
|
|
70
|
+
return 'applied';
|
|
71
|
+
}
|
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,15 +693,15 @@ 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
|
-
|
|
696
|
+
// curl 直查 registry(系统自带,零工具链依赖);失败再退 npm view。
|
|
697
|
+
// 升级检测不该依赖机器上恰好有哪套 node 工具链
|
|
698
|
+
const registry = await registryLatestVersion('@larksuite/cli', timeoutMs).catch(() => null);
|
|
699
|
+
if (registry) return registry;
|
|
542
700
|
try {
|
|
543
701
|
const { stdout } = await runCliTool('npm', ['view', '@larksuite/cli', 'version'], { timeoutMs });
|
|
544
702
|
return parseCliVersion(stdout);
|
|
545
703
|
} catch {
|
|
546
|
-
|
|
547
|
-
return version || null;
|
|
704
|
+
return null;
|
|
548
705
|
}
|
|
549
706
|
}
|
|
550
707
|
return null;
|
|
@@ -609,6 +766,66 @@ async function installLarkSkills({ timeoutMs = 180_000 } = {}) {
|
|
|
609
766
|
return 'lark-cli 已安装。默认技能包暂跳过(本机暂无 npm/npx,不影响 CLI 功能;如需技能可安装 Node.js 后重新点「安装」)';
|
|
610
767
|
}
|
|
611
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;
|
|
827
|
+
}
|
|
828
|
+
|
|
612
829
|
/**
|
|
613
830
|
* 触发 CLI 官方安装(工具缺失时用户自助安装):
|
|
614
831
|
* lark-cli npm 全局装最新版(包名 @larksuite/cli,npm/pnpm 环境均可;
|
|
@@ -621,47 +838,37 @@ async function installLarkSkills({ timeoutMs = 180_000 } = {}) {
|
|
|
621
838
|
export async function installCliTool(tool, { timeoutMs = 600_000 } = {}) {
|
|
622
839
|
let stdout;
|
|
623
840
|
if (tool === 'lark-cli') {
|
|
624
|
-
//
|
|
625
|
-
//
|
|
626
|
-
// (用户目录免管理员,npmmirror 镜像),再用其自带 npm 装 lark-cli。
|
|
627
|
-
// lark-cli 官方支持 pnpm 安装方式,装出来的二进制与 npm 路径一致,
|
|
628
|
-
// 后续 lark-cli update 也能自升级。
|
|
841
|
+
// 优先 npm;Windows 上 npm 文件被占用或安装后仍是残缺 shim 时,
|
|
842
|
+
// 仅在 pnpm 可用的情况下切换到 pnpm,避免用另一套安装器掩盖网络错误。
|
|
629
843
|
if (resolveCommandOrNull('npm')) {
|
|
630
|
-
|
|
631
|
-
} else if (resolveCommandOrNull('pnpm')) {
|
|
632
|
-
const pnpmInstall = () => runCliTool('pnpm', ['add', '-g', '@larksuite/cli@latest'], { timeoutMs });
|
|
844
|
+
let npmError = null;
|
|
633
845
|
try {
|
|
634
|
-
({ stdout } = await
|
|
846
|
+
({ stdout } = await runCliTool('npm', ['install', '-g', '@larksuite/cli@latest'], { timeoutMs }));
|
|
847
|
+
await verifyLarkCliInstall(timeoutMs);
|
|
635
848
|
} catch (error) {
|
|
636
|
-
|
|
637
|
-
// 用户机报 The configured global bin directory "C:\Users\<u>\
|
|
638
|
-
// AppData\Local\pnpm\bin" is not in PATH)。自定义安装位不在兑底
|
|
639
|
-
// 目录里时,从报错原文解析出该目录补进 PATH 重试一次
|
|
640
|
-
const dir = /global bin directory "(.+?)" is not in PATH/i.exec(String(error?.message || ''))?.[1];
|
|
641
|
-
if (!dir) throw error;
|
|
642
|
-
extraBinDirsCache = [...extraBinDirs(), dir];
|
|
643
|
-
({ stdout } = await pnpmInstall());
|
|
849
|
+
npmError = error;
|
|
644
850
|
}
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
const
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
extraBinDirsCache = [...extraBinDirs(), dir];
|
|
654
|
-
}
|
|
655
|
-
} catch {
|
|
656
|
-
// 问不到不阻断:静态兑底目录仍兜底
|
|
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);
|
|
657
859
|
}
|
|
860
|
+
} else if (resolveCommandOrNull('pnpm')) {
|
|
861
|
+
stdout = await installLarkCliWithPnpm(timeoutMs);
|
|
862
|
+
await verifyLarkCliInstall(timeoutMs);
|
|
658
863
|
} else if (process.platform === 'win32') {
|
|
659
864
|
const nodeNote = await installNodeWindows(timeoutMs);
|
|
660
865
|
({ stdout } = await runCliTool('npm', ['install', '-g', '@larksuite/cli@latest'], { timeoutMs }));
|
|
661
866
|
stdout = `${nodeNote}\n${stdout}`;
|
|
867
|
+
await verifyLarkCliInstall(timeoutMs);
|
|
662
868
|
} else {
|
|
663
869
|
throw new Error(missingCommandGuidance('npm'));
|
|
664
870
|
}
|
|
871
|
+
|
|
665
872
|
const skillsNote = await installLarkSkills().catch((error) => {
|
|
666
873
|
// 软失败:技能装不上不影响 CLI 本体已装成功(如 GitHub 不可达)
|
|
667
874
|
return `lark-cli skills install soft-failed: ${error?.message || error}`;
|
package/lib/store.js
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-agentone",
|
|
3
|
-
"version": "0.5.
|
|
4
|
-
"description": "AgentOne 平台集成插件:飞书登录、平台模型(同步失败自愈重试)、SkillHub 技能(升级检测一键升级、装后即时刷新、套餐技能自动同步)、套餐申请、插件管理与自助升级、lark-cli / ccpg-cli 探测与一键安装(请求超时与错误码透传、令牌轮换 single-flight、凭据损坏容错与并发安全写入、状态页 UX
|
|
3
|
+
"version": "0.5.30",
|
|
4
|
+
"description": "AgentOne 平台集成插件:飞书登录、平台模型(同步失败自愈重试)、SkillHub 技能(升级检测一键升级、装后即时刷新、套餐技能自动同步)、套餐申请、插件管理与自助升级、lark-cli / ccpg-cli 探测与一键安装(请求超时与错误码透传、令牌轮换 single-flight、凭据损坏容错与并发安全写入、状态页 UX 增强、全局中文回复默认(AGENTS.md 受标记管理、永不覆盖用户设置))",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"smoke": "node scripts/env-matrix-smoke.mjs"
|