dsh-agentone 0.5.35 → 0.5.37
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 +9 -2
- package/lib/index.js +116 -7
- package/lib/instance.js +1 -1
- package/lib/model.js +97 -11
- package/lib/page.js +153 -8
- package/lib/proc.js +212 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -35,7 +35,12 @@ lark-cli / ccpg-cli 状态探测。
|
|
|
35
35
|
未安装时「安装」一键自助安装(lark 走 `npm install -g @larksuite/cli@latest`,
|
|
36
36
|
ccpg 走公司 `auto-install.sh`,口径与平台 cli-deployd 一致);
|
|
37
37
|
已装未授权时「去授权」发起官方 Device Flow(授权链接 + ASCII 二维码 +
|
|
38
|
-
验证码 +
|
|
38
|
+
验证码 + 自动轮询确认,设备码仅存插件进程内存);
|
|
39
|
+
lark-cli 首装未 `config init` 时探测为「未配置」,「初始化应用」一键驱动
|
|
40
|
+
官方 `config init --new`(长驻子进程 + 验证链接/二维码 + 终态轮询,成功后
|
|
41
|
+
自动衔接设备流授权;关闭弹窗即终止子进程)。平台托管实例不提供自助初始化
|
|
42
|
+
——容器内 HERMES_HOME 上下文下 config init 会拒绝执行,应用配置由 Hub 经
|
|
43
|
+
cli-deployd 统一下发。
|
|
39
44
|
- **全局中文回复默认**:插件启动时在 `$DSH_HOME/AGENTS.md` 注入受标记
|
|
40
45
|
管理的「默认始终使用中文回复」指令块(dsh 官方 agent-instructions 机制,
|
|
41
46
|
每个会话基线加载,project 级指令优先);幂等不重复注入,用户已有语言
|
|
@@ -66,7 +71,9 @@ dsh plugin --profile web add dsh-agentone
|
|
|
66
71
|
|
|
67
72
|
- `$DSH_HOME/settings.yaml` 写入 `llm-pi-ai.providers.platform`(指向平台
|
|
68
73
|
`/api/plugin/v1` 代理)与 `agent-default-model`,dsh 模型选择器出现
|
|
69
|
-
|
|
74
|
+
「平台模型」;模型列表按系列聚簇 + 系列内版本降序写入(插件侧按平台
|
|
75
|
+
`sort_models_by_version` 同一规则兜底重排,旧版平台返回的上游原序也能得到
|
|
76
|
+
稳定顺序,默认模型仍取平台下发的 `default_model`);
|
|
70
77
|
- `$DSH_HOME/.credentials.yaml` 写入 `refs.AGENTONE_API_KEY`(0600),内容
|
|
71
78
|
是 30 天插件令牌而非真实模型密钥——密钥不出平台,TokenHub 按用户计量;
|
|
72
79
|
- 只覆盖 `platform` 命名空间,用户自己配置的其它 provider 原样保留;
|
package/lib/index.js
CHANGED
|
@@ -36,7 +36,7 @@ import { bootstrapInstanceCredentials, INSTANCE_SOURCE, isInstanceManaged, resol
|
|
|
36
36
|
import { beginCliAuth, cliAuthQrcode, completeCliAuth, parseCliAuthBeginOutput } from './cliauth.mjs';
|
|
37
37
|
import { platformFetch } from './http.js';
|
|
38
38
|
import { donePageHtml, statusPageHtml } from './page.js';
|
|
39
|
-
import { compareSemver, dshPluginCli, firstErrorLines, hasChineseText, installCliTool, isTerminalCliAuthFailure, isUnexpectedStoreError, probeCliDetail, probePythonEnv, pnpmViewLatest, runCliTool, unexpectedStoreGuidance, upgradeCliTool } from './proc.js';
|
|
39
|
+
import { compareSemver, dshPluginCli, firstErrorLines, hasChineseText, installCliTool, isTerminalCliAuthFailure, isUnexpectedStoreError, larkCliInitActiveUrl, larkCliInitStatus, probeCliDetail, probePythonEnv, pnpmViewLatest, runCliTool, startLarkCliInit, stopLarkCliInit, unexpectedStoreGuidance, upgradeCliTool } from './proc.js';
|
|
40
40
|
|
|
41
41
|
import { clearCredentials, loadCredentials, resolveDshHome, saveCredentials, updateCredentials } from './store.js';
|
|
42
42
|
|
|
@@ -1329,6 +1329,17 @@ export function apply(ctx, config) {
|
|
|
1329
1329
|
// 与 pendingStates 同生命周期语义——插件重启后用户重新发起即可。
|
|
1330
1330
|
const pendingCliAuth = new Map();
|
|
1331
1331
|
|
|
1332
|
+
// 探测结果 state=missing 时的中文指引(授权与初始化两条链共用)。
|
|
1333
|
+
const cliMissingMessage = (detail) => {
|
|
1334
|
+
if (detail?.reason === 'broken_install') {
|
|
1335
|
+
return 'CLI 安装不完整,请先点击「修复安装」后再授权';
|
|
1336
|
+
}
|
|
1337
|
+
if (detail?.reason === 'unavailable') {
|
|
1338
|
+
return 'CLI 暂时无法访问(可能被安全软件占用或响应超时),请稍后重试;若持续失败请重新安装';
|
|
1339
|
+
}
|
|
1340
|
+
return 'CLI 未安装,请先安装或联系管理员';
|
|
1341
|
+
};
|
|
1342
|
+
|
|
1332
1343
|
register('/agentone/api/cli/auth/begin', async (request, response) => {
|
|
1333
1344
|
if (request.method !== 'POST') {
|
|
1334
1345
|
response.writeHead(405, { allow: 'POST' });
|
|
@@ -1346,12 +1357,16 @@ export function apply(ctx, config) {
|
|
|
1346
1357
|
const detail = await probeCliDetail(tool, { withLatest: false })
|
|
1347
1358
|
.then(withCliUpgradeGuard).catch(() => null);
|
|
1348
1359
|
if (detail?.state === 'missing') {
|
|
1360
|
+
sendJson(response, 400, { error: cliMissingMessage(detail) });
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
// lark-cli 首装未 config init 时 auth login 直接报 not_configured——
|
|
1364
|
+
// 前置拦截给明确指引,而不是让 begin 抛技术错误
|
|
1365
|
+
if (detail?.unconfigured) {
|
|
1349
1366
|
sendJson(response, 400, {
|
|
1350
|
-
error:
|
|
1351
|
-
? '
|
|
1352
|
-
:
|
|
1353
|
-
? 'CLI 暂时无法访问(可能被安全软件占用或响应超时),请稍后重试;若持续失败请重新安装'
|
|
1354
|
-
: 'CLI 未安装,请先安装或联系管理员',
|
|
1367
|
+
error: isInstanceManaged()
|
|
1368
|
+
? 'lark-cli 应用配置由平台统一下发,当前实例缺少配置,请重启实例或联系管理员'
|
|
1369
|
+
: 'lark-cli 尚未完成应用初始化,请先点击「初始化应用」完成飞书应用创建',
|
|
1355
1370
|
});
|
|
1356
1371
|
return;
|
|
1357
1372
|
}
|
|
@@ -1451,7 +1466,7 @@ export function apply(ctx, config) {
|
|
|
1451
1466
|
// CLI(lark-cli / ccpg-cli)的进行中授权会话均可——二维码由内置
|
|
1452
1467
|
// 编码器生成(lib/qrcode.mjs),不依赖 lark-cli 本机存在。
|
|
1453
1468
|
const queryUrl = new URL(request.url, 'http://localhost').searchParams.get('url') || '';
|
|
1454
|
-
if (!queryUrl || pendingCliAuth.size === 0) {
|
|
1469
|
+
if (!queryUrl || (pendingCliAuth.size === 0 && !larkCliInitActiveUrl())) {
|
|
1455
1470
|
sendJson(response, 400, { error: '没有进行中的授权会话' });
|
|
1456
1471
|
return;
|
|
1457
1472
|
}
|
|
@@ -1465,6 +1480,100 @@ export function apply(ctx, config) {
|
|
|
1465
1480
|
}
|
|
1466
1481
|
});
|
|
1467
1482
|
|
|
1483
|
+
// lark-cli 首次应用初始化(config init --new 长驻子进程)。仅独立版桌面
|
|
1484
|
+
// 提供:托管容器内 HERMES_HOME 上下文会让 config init 拒绝执行,且应用
|
|
1485
|
+
// 配置由 Hub 经 cli-deployd 统一下发(instance_service 的 CLI configure
|
|
1486
|
+
// 链路),员工不应在容器里自建飞书应用。
|
|
1487
|
+
register('/agentone/api/cli/auth/init', async (request, response) => {
|
|
1488
|
+
if (request.method !== 'POST') {
|
|
1489
|
+
response.writeHead(405, { allow: 'POST' });
|
|
1490
|
+
response.end();
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
try {
|
|
1494
|
+
const body = await readJsonBody(request);
|
|
1495
|
+
const tool = String(body.tool || '');
|
|
1496
|
+
if (tool !== 'lark-cli') {
|
|
1497
|
+
sendJson(response, 400, { error: '仅 lark-cli 支持应用初始化' });
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1500
|
+
if (isInstanceManaged()) {
|
|
1501
|
+
sendJson(response, 400, {
|
|
1502
|
+
error: '平台托管实例的 lark-cli 应用配置由平台统一下发,如缺失请重启实例或联系管理员',
|
|
1503
|
+
});
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1506
|
+
const detail = await probeCliDetail(tool, { withLatest: false })
|
|
1507
|
+
.then(withCliUpgradeGuard).catch(() => null);
|
|
1508
|
+
if (detail?.state === 'missing') {
|
|
1509
|
+
sendJson(response, 400, { error: cliMissingMessage(detail) });
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
if (detail?.state === 'authorized') {
|
|
1513
|
+
sendJson(response, 400, { error: 'lark-cli 已授权,无需初始化' });
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1516
|
+
if (detail && !detail.unconfigured) {
|
|
1517
|
+
sendJson(response, 400, { error: 'lark-cli 已有应用配置,请直接点击「去授权」' });
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1520
|
+
const parsed = await startLarkCliInit();
|
|
1521
|
+
sendJson(response, 200, {
|
|
1522
|
+
ok: true,
|
|
1523
|
+
tool,
|
|
1524
|
+
verification_url: parsed.url,
|
|
1525
|
+
user_code: parsed.userCode,
|
|
1526
|
+
reused: parsed.reused === true,
|
|
1527
|
+
});
|
|
1528
|
+
} catch (error) {
|
|
1529
|
+
const message = String(error?.message || '');
|
|
1530
|
+
sendJson(response, 502, {
|
|
1531
|
+
error: hasChineseText(message) ? message : '发起初始化失败,请稍后重试',
|
|
1532
|
+
});
|
|
1533
|
+
}
|
|
1534
|
+
});
|
|
1535
|
+
|
|
1536
|
+
register('/agentone/api/cli/auth/init/status', async (request, response) => {
|
|
1537
|
+
if (request.method !== 'POST') {
|
|
1538
|
+
response.writeHead(405, { allow: 'POST' });
|
|
1539
|
+
response.end();
|
|
1540
|
+
return;
|
|
1541
|
+
}
|
|
1542
|
+
try {
|
|
1543
|
+
const body = await readJsonBody(request);
|
|
1544
|
+
const tool = String(body.tool || '');
|
|
1545
|
+
if (tool !== 'lark-cli') {
|
|
1546
|
+
sendJson(response, 400, { error: '仅 lark-cli 支持应用初始化' });
|
|
1547
|
+
return;
|
|
1548
|
+
}
|
|
1549
|
+
const init = larkCliInitStatus();
|
|
1550
|
+
// 子进程退出(成功)后 config.json 已落盘,此时才附带一次三态探测,
|
|
1551
|
+
// 前端据此决定「已授权直接完成」还是「衔接设备流授权」;running
|
|
1552
|
+
// 期间不探测(每 3s 轮询各起一个子进程没有意义)。
|
|
1553
|
+
const detail = init.status === 'succeeded'
|
|
1554
|
+
? await probeCliDetail(tool, { withLatest: false })
|
|
1555
|
+
.then(withCliUpgradeGuard).catch(() => null)
|
|
1556
|
+
: null;
|
|
1557
|
+
sendJson(response, 200, { ok: true, init, tool: detail });
|
|
1558
|
+
} catch (error) {
|
|
1559
|
+
sendJson(response, 502, { error: error?.message || '查询初始化状态失败' });
|
|
1560
|
+
}
|
|
1561
|
+
});
|
|
1562
|
+
|
|
1563
|
+
register('/agentone/api/cli/auth/init/cancel', async (request, response) => {
|
|
1564
|
+
if (request.method !== 'POST') {
|
|
1565
|
+
response.writeHead(405, { allow: 'POST' });
|
|
1566
|
+
response.end();
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
try {
|
|
1570
|
+
const stopped = await stopLarkCliInit();
|
|
1571
|
+
sendJson(response, 200, { ok: true, stopped });
|
|
1572
|
+
} catch (error) {
|
|
1573
|
+
sendJson(response, 502, { error: error?.message || '取消初始化失败' });
|
|
1574
|
+
}
|
|
1575
|
+
});
|
|
1576
|
+
|
|
1468
1577
|
register('/agentone/api/plan', async (request, response) => {
|
|
1469
1578
|
if (request.method !== 'GET') {
|
|
1470
1579
|
response.writeHead(405, { allow: 'GET' });
|
package/lib/instance.js
CHANGED
package/lib/model.js
CHANGED
|
@@ -145,6 +145,90 @@ export function buildLogoutSettingsYaml(existingText) {
|
|
|
145
145
|
return stringify(settings);
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
// 模型选择器展示顺序:同一系列(模型 id 首段,如 deepseek / glm / kimi)聚簇,
|
|
149
|
+
// 系列内版本降序,容量降配档(kimi-k3-256k)排在本版本标准档之后。
|
|
150
|
+
//
|
|
151
|
+
// 为什么插件也要排一遍:平台 /api/plugin/v1/models 的 data 顺序在旧版平台上
|
|
152
|
+
// 直接沿用 AirRouter /models 的返回顺序(上游一变用户看到的列表就跟着变),
|
|
153
|
+
// 平台侧的排序修复要等部署才生效。插件把平台返回的列表按同一套规则重排后
|
|
154
|
+
// 再写进 dsh,旧平台也能拿到稳定顺序。规则与
|
|
155
|
+
// app/services/dsh_model_capabilities.py 的 sort_models_by_version 完全一致
|
|
156
|
+
// (系列聚簇 + 数字段补零取负降序 + 字母段字母序 + 容量段垫底),
|
|
157
|
+
// 平台已经排好时这里是幂等的,不会出现两边顺序漂移。
|
|
158
|
+
//
|
|
159
|
+
// 只改展示顺序:defaultModel 仍取平台下发的 default_model,排序不参与决策
|
|
160
|
+
//(平台未下发时回退列表首项,此时取排序后的首项,比上游原序更确定)。
|
|
161
|
+
const VERSION_SLOTS = 6;
|
|
162
|
+
const VERSION_NUMBERS = /\d+/g;
|
|
163
|
+
// 容量限定段(256k / 1m):同版本的降配档,不是更新的版本号。
|
|
164
|
+
// 必须连着字母一起匹配:VERSION_NUMBERS 切出来的只有数字部分。
|
|
165
|
+
const CAPACITY_QUALIFIER = /\d+(?:k|m|b)\b/g;
|
|
166
|
+
|
|
167
|
+
/** 模型 id → 系列键(id 首段小写;没有连字符的裸 id 整体作为分组键)。 */
|
|
168
|
+
function modelFamily(id) {
|
|
169
|
+
const lowered = String(id ?? '').trim().toLowerCase();
|
|
170
|
+
const at = lowered.indexOf('-');
|
|
171
|
+
return at > 0 ? lowered.slice(0, at) : lowered;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** 系列内的版本比较键:数字段取负(升序即版本降序)、字母段、容量降配标记。 */
|
|
175
|
+
function versionKey(id) {
|
|
176
|
+
const lowered = String(id ?? '').trim().toLowerCase();
|
|
177
|
+
const capacityHits = lowered.match(CAPACITY_QUALIFIER) ?? [];
|
|
178
|
+
const numbers = (lowered.replace(CAPACITY_QUALIFIER, ' ').match(VERSION_NUMBERS) ?? [])
|
|
179
|
+
.slice(0, VERSION_SLOTS)
|
|
180
|
+
.map((part) => -Number(part));
|
|
181
|
+
while (numbers.length < VERSION_SLOTS) numbers.push(0);
|
|
182
|
+
return {
|
|
183
|
+
numbers,
|
|
184
|
+
words: lowered.split('-').filter((part) => part && !/^\d+$/.test(part)),
|
|
185
|
+
downgraded: capacityHits.length > 0,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Python 字符串/元组比较口径(逐码点),与平台 sorted() 的结果对齐。 */
|
|
190
|
+
function compareStrings(left, right) {
|
|
191
|
+
if (left === right) return 0;
|
|
192
|
+
return left < right ? -1 : 1;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function compareVersionKeys(left, right) {
|
|
196
|
+
for (let index = 0; index < VERSION_SLOTS; index += 1) {
|
|
197
|
+
if (left.numbers[index] !== right.numbers[index]) return left.numbers[index] - right.numbers[index];
|
|
198
|
+
}
|
|
199
|
+
const shared = Math.min(left.words.length, right.words.length);
|
|
200
|
+
for (let index = 0; index < shared; index += 1) {
|
|
201
|
+
const compared = compareStrings(left.words[index], right.words[index]);
|
|
202
|
+
if (compared !== 0) return compared;
|
|
203
|
+
}
|
|
204
|
+
if (left.words.length !== right.words.length) return left.words.length - right.words.length;
|
|
205
|
+
if (left.downgraded !== right.downgraded) return left.downgraded ? 1 : -1;
|
|
206
|
+
return 0;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* 按系列聚簇、系列内版本降序排模型条目(只改展示顺序,不改集合成员)。
|
|
211
|
+
*
|
|
212
|
+
* 入参可以是平台条目对象(取 .id)或字符串 id;排序是确定性的,同样的输入
|
|
213
|
+
* 必得同样的输出,完全不依赖平台 / AirRouter 的返回顺序。
|
|
214
|
+
*/
|
|
215
|
+
export function sortPlatformModels(models) {
|
|
216
|
+
const entries = (Array.isArray(models) ? models : []).map((item, index) => ({
|
|
217
|
+
item,
|
|
218
|
+
index,
|
|
219
|
+
id: typeof item === 'string' ? item : String(item?.id ?? ''),
|
|
220
|
+
}));
|
|
221
|
+
entries.sort(
|
|
222
|
+
(left, right) =>
|
|
223
|
+
compareStrings(modelFamily(left.id), modelFamily(right.id))
|
|
224
|
+
// 原始下标兜底:与 Python sorted 的稳定性同口径,完全相同键保持原序
|
|
225
|
+
|| compareVersionKeys(versionKey(left.id), versionKey(right.id))
|
|
226
|
+
|| compareStrings(left.id.trim().toLowerCase(), right.id.trim().toLowerCase())
|
|
227
|
+
|| left.index - right.index,
|
|
228
|
+
);
|
|
229
|
+
return entries.map((entry) => entry.item);
|
|
230
|
+
}
|
|
231
|
+
|
|
148
232
|
export async function fetchPlatformModels(platformUrl, accessToken) {
|
|
149
233
|
const response = await platformFetch(`${platformUrl.replace(/\/+$/, '')}/api/plugin/v1/models`, {
|
|
150
234
|
headers: { Authorization: `Bearer ${accessToken}` },
|
|
@@ -155,17 +239,19 @@ export async function fetchPlatformModels(platformUrl, accessToken) {
|
|
|
155
239
|
throw await platformError(response, '获取平台模型列表失败');
|
|
156
240
|
}
|
|
157
241
|
const data = await response.json().catch(() => ({}));
|
|
158
|
-
const models =
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
242
|
+
const models = sortPlatformModels(
|
|
243
|
+
Array.isArray(data.data)
|
|
244
|
+
? data.data
|
|
245
|
+
.map((item) =>
|
|
246
|
+
typeof item === 'string'
|
|
247
|
+
? item
|
|
248
|
+
: item?.id
|
|
249
|
+
? { id: item.id, input: item.input, reasoning: item.reasoning, contextWindow: item.contextWindow }
|
|
250
|
+
: null,
|
|
251
|
+
)
|
|
252
|
+
.filter(Boolean)
|
|
253
|
+
: [],
|
|
254
|
+
);
|
|
169
255
|
if (models.length === 0) throw new Error('平台返回的可用模型列表为空');
|
|
170
256
|
const direct =
|
|
171
257
|
data.direct && typeof data.direct.base_url === 'string' && typeof data.direct.api_key === 'string'
|
package/lib/page.js
CHANGED
|
@@ -536,23 +536,37 @@ const renderCliData = (s, tools, python) => {
|
|
|
536
536
|
const rows = Object.entries(CLI_TOOL_META).map(([name, meta]) => {
|
|
537
537
|
const info = allTools[name] || { state: 'missing' };
|
|
538
538
|
const state = info.state || 'missing';
|
|
539
|
+
// lark-cli 首装未 config init:unauthorized + unconfigured 标志——独立版
|
|
540
|
+
// 给「初始化应用」按钮;托管实例配置由平台下发,只提示不给自助入口
|
|
541
|
+
//(容器内 HERMES_HOME 上下文下 config init 会拒绝执行)。
|
|
542
|
+
const unconfigured = state === 'unauthorized' && info.unconfigured;
|
|
539
543
|
const status = state === 'authorized'
|
|
540
544
|
? '<span class="pill ok" style="font-size:11px">已授权</span>'
|
|
541
|
-
:
|
|
542
|
-
? '<span class="pill warn" style="font-size:11px"
|
|
543
|
-
:
|
|
544
|
-
? '<span class="pill warn" style="font-size:11px"
|
|
545
|
-
:
|
|
545
|
+
: unconfigured
|
|
546
|
+
? '<span class="pill warn" style="font-size:11px">未配置</span>'
|
|
547
|
+
: state === 'unauthorized'
|
|
548
|
+
? '<span class="pill warn" style="font-size:11px">未授权</span>'
|
|
549
|
+
: info.reason === 'broken_install'
|
|
550
|
+
? '<span class="pill warn" style="font-size:11px">安装损坏</span>'
|
|
551
|
+
: '<span class="pill warn" style="font-size:11px">未安装</span>';
|
|
546
552
|
const versionLine = meta.desc + ' · ' + meta.label
|
|
547
553
|
+ (info.reason === 'broken_install' ? ' · 检测到文件不完整,请修复安装' : '')
|
|
554
|
+
+ (unconfigured
|
|
555
|
+
? (s.managed ? ' · 应用配置由平台统一下发,当前缺失请联系管理员' : ' · 首次使用需先创建飞书应用')
|
|
556
|
+
: '')
|
|
548
557
|
+ (info.version ? ' · v' + esc(info.version) : '')
|
|
549
558
|
+ (info.latest && info.latest !== info.version
|
|
550
559
|
? ' → <b>v' + esc(info.latest) + '</b>'
|
|
551
560
|
: '');
|
|
552
561
|
const upgradable = info.latest && info.latest !== info.version;
|
|
553
|
-
//
|
|
562
|
+
// 已装未配置 → 「初始化应用」;已装未授权 → 「去授权」;残缺安装 → 「修复安装」
|
|
554
563
|
const actions = [
|
|
555
|
-
|
|
564
|
+
unconfigured
|
|
565
|
+
? (s.managed
|
|
566
|
+
? ''
|
|
567
|
+
: '<button class="btn small primary" data-cli-init="' + esc(name) + '">初始化应用</button>')
|
|
568
|
+
: '',
|
|
569
|
+
state === 'unauthorized' && !unconfigured
|
|
556
570
|
? '<button class="btn small primary" data-cli-auth="' + esc(name) + '">去授权</button>'
|
|
557
571
|
: '',
|
|
558
572
|
state === 'missing'
|
|
@@ -590,7 +604,7 @@ const renderCliData = (s, tools, python) => {
|
|
|
590
604
|
: '';
|
|
591
605
|
body.innerHTML = rows.join('') + pythonRow
|
|
592
606
|
+ '<div class="hint">在对话中直接使用(如 /lark 帮助),由 dsh 自动调用。</div>'
|
|
593
|
-
+ '<div class="hint">未安装时点「安装」自助安装(Python
|
|
607
|
+
+ '<div class="hint">未安装时点「安装」自助安装(Python 同样支持);lark-cli 首次使用需先「初始化应用」创建飞书应用,再扫码完成授权。</div>';
|
|
594
608
|
body.querySelectorAll('[data-cli-upgrade]').forEach((btn) => {
|
|
595
609
|
btn.onclick = () => cliUpgrade(btn.dataset.cliUpgrade, btn);
|
|
596
610
|
});
|
|
@@ -600,6 +614,9 @@ const renderCliData = (s, tools, python) => {
|
|
|
600
614
|
body.querySelectorAll('[data-cli-auth]').forEach((btn) => {
|
|
601
615
|
btn.onclick = () => cliAuthDialog(btn.dataset.cliAuth);
|
|
602
616
|
});
|
|
617
|
+
body.querySelectorAll('[data-cli-init]').forEach((btn) => {
|
|
618
|
+
btn.onclick = () => cliInitDialog(btn.dataset.cliInit);
|
|
619
|
+
});
|
|
603
620
|
};
|
|
604
621
|
|
|
605
622
|
/**
|
|
@@ -713,6 +730,134 @@ const cliAuthDialog = async (tool) => {
|
|
|
713
730
|
}
|
|
714
731
|
};
|
|
715
732
|
|
|
733
|
+
// PAGE_JS 是模板字面量:内部注释与代码一律不用反引号(会提前终止模板)。
|
|
734
|
+
/**
|
|
735
|
+
* lark-cli 应用初始化弹窗:后端以长驻子进程驱动官方 config init --new
|
|
736
|
+
*(阻塞式、无 --no-wait),前端轮询 init/status 直到子进程退出(用户在
|
|
737
|
+
* 浏览器/扫码完成应用创建)。成功后按三态衔接:已授权直接完成;未授权
|
|
738
|
+
* 自动打开「去授权」设备流弹窗。关闭弹窗会取消并终止后端初始化进程。
|
|
739
|
+
*/
|
|
740
|
+
const cliInitDialog = async (tool) => {
|
|
741
|
+
const overlay = document.createElement('div');
|
|
742
|
+
overlay.className = 'modal-backdrop';
|
|
743
|
+
overlay.innerHTML = '<div class="card" style="max-width:420px;margin:8% auto;text-align:left">'
|
|
744
|
+
+ '<h3 style="margin:0 0 6px">初始化 ' + esc(tool) + '</h3>'
|
|
745
|
+
+ '<div id="cli-init-status" class="skill-sub">正在发起应用初始化…</div>'
|
|
746
|
+
+ '<img id="cli-init-qr" alt="配置二维码" style="display:none;width:100%;max-width:300px;'
|
|
747
|
+
+ 'background:#fff;border-radius:10px;margin:10px 0">'
|
|
748
|
+
+ '<div id="cli-init-link"></div>'
|
|
749
|
+
+ '<div class="row" style="margin-top:12px;justify-content:flex-end">'
|
|
750
|
+
+ '<button id="cli-init-open" class="btn small primary" hidden>打开配置页</button>'
|
|
751
|
+
+ '<button id="cli-init-copy" class="btn small ghost" hidden>复制验证码</button>'
|
|
752
|
+
+ '<button id="cli-init-close" class="btn small ghost">关闭</button></div></div>';
|
|
753
|
+
document.body.appendChild(overlay);
|
|
754
|
+
let pollTimer = null;
|
|
755
|
+
let closed = false;
|
|
756
|
+
const statusEl = overlay.querySelector('#cli-init-status');
|
|
757
|
+
const stopPolling = () => {
|
|
758
|
+
closed = true;
|
|
759
|
+
if (pollTimer) clearTimeout(pollTimer);
|
|
760
|
+
};
|
|
761
|
+
// 关闭即取消:终止后端初始化子进程(已终态时后端为 no-op,可安全直调)
|
|
762
|
+
const close = () => {
|
|
763
|
+
stopPolling();
|
|
764
|
+
fetch('api/cli/auth/init/cancel', {
|
|
765
|
+
method: 'POST',
|
|
766
|
+
headers: { 'Content-Type': 'application/json' },
|
|
767
|
+
body: JSON.stringify({ tool }),
|
|
768
|
+
}).catch(() => {});
|
|
769
|
+
overlay.remove();
|
|
770
|
+
};
|
|
771
|
+
overlay.querySelector('#cli-init-close').onclick = close;
|
|
772
|
+
overlay.addEventListener('click', (event) => { if (event.target === overlay) close(); });
|
|
773
|
+
try {
|
|
774
|
+
const resp = await fetch('api/cli/auth/init', {
|
|
775
|
+
method: 'POST',
|
|
776
|
+
headers: { 'Content-Type': 'application/json' },
|
|
777
|
+
body: JSON.stringify({ tool }),
|
|
778
|
+
});
|
|
779
|
+
const data = await resp.json().catch(() => ({}));
|
|
780
|
+
if (!resp.ok) throw new Error(data.error || '发起初始化失败');
|
|
781
|
+
statusEl.textContent = '用手机飞书 App 扫二维码,或打开链接完成应用创建。完成后此处会自动继续。';
|
|
782
|
+
fetch('api/cli/auth/qrcode?url=' + encodeURIComponent(data.verification_url))
|
|
783
|
+
.then((r) => (r.ok ? r.text() : ''))
|
|
784
|
+
.then((svg) => {
|
|
785
|
+
const qr = overlay.querySelector('#cli-init-qr');
|
|
786
|
+
if (svg && svg.startsWith('<svg')) {
|
|
787
|
+
qr.src = 'data:image/svg+xml;utf8,' + encodeURIComponent(svg);
|
|
788
|
+
qr.style.display = 'block';
|
|
789
|
+
}
|
|
790
|
+
})
|
|
791
|
+
.catch(() => {});
|
|
792
|
+
const openBtn = overlay.querySelector('#cli-init-open');
|
|
793
|
+
openBtn.hidden = false;
|
|
794
|
+
openBtn.onclick = () => window.open(data.verification_url, '_blank');
|
|
795
|
+
const copyBtn = overlay.querySelector('#cli-init-copy');
|
|
796
|
+
if (data.user_code) {
|
|
797
|
+
copyBtn.hidden = false;
|
|
798
|
+
copyBtn.onclick = async () => {
|
|
799
|
+
try {
|
|
800
|
+
await navigator.clipboard.writeText(data.user_code);
|
|
801
|
+
copyBtn.textContent = '已复制';
|
|
802
|
+
} catch {
|
|
803
|
+
copyBtn.textContent = data.user_code;
|
|
804
|
+
}
|
|
805
|
+
};
|
|
806
|
+
overlay.querySelector('#cli-init-link').innerHTML
|
|
807
|
+
= '<div class="skill-sub">验证码:<b>' + esc(data.user_code) + '</b></div>';
|
|
808
|
+
}
|
|
809
|
+
// 自调度串行轮询(与授权弹窗同口径):running 继续等;succeeded 按三态
|
|
810
|
+
// 衔接;failed/cancelled 展示输出尾部诊断。
|
|
811
|
+
const poll = async () => {
|
|
812
|
+
if (closed) return;
|
|
813
|
+
try {
|
|
814
|
+
const r = await fetch('api/cli/auth/init/status', {
|
|
815
|
+
method: 'POST',
|
|
816
|
+
headers: { 'Content-Type': 'application/json' },
|
|
817
|
+
body: JSON.stringify({ tool }),
|
|
818
|
+
});
|
|
819
|
+
const d = await r.json().catch(() => ({}));
|
|
820
|
+
const init = d.init || {};
|
|
821
|
+
if (init.status === 'running') {
|
|
822
|
+
statusEl.textContent = '正在等待你在配置页完成应用创建…';
|
|
823
|
+
pollTimer = setTimeout(poll, 3000);
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
if (init.status === 'succeeded') {
|
|
827
|
+
stopPolling();
|
|
828
|
+
if (d.tool && d.tool.state === 'authorized') {
|
|
829
|
+
statusEl.textContent = '✓ 初始化并授权完成';
|
|
830
|
+
renderCliData(lastStatus, { [tool]: d.tool });
|
|
831
|
+
setTimeout(() => overlay.remove(), 800);
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
// config init 只建应用,用户 token 未必随带:自动衔接设备流授权
|
|
835
|
+
statusEl.textContent = '✓ 应用创建完成,正在发起授权…';
|
|
836
|
+
overlay.remove();
|
|
837
|
+
cliAuthDialog(tool);
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
stopPolling();
|
|
841
|
+
if (init.status === 'cancelled') {
|
|
842
|
+
statusEl.textContent = '已取消初始化。';
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
// PAGE_JS 是模板字面量:换行符须写 \\n,写 \n 会被模板吃掉变成真实
|
|
846
|
+
// 换行、产出未闭合的字符串字面量炸掉整页脚本
|
|
847
|
+
const tail = String(init.output || '').split('\\n').filter(Boolean).slice(-3).join(' ');
|
|
848
|
+
statusEl.textContent = tail
|
|
849
|
+
? '初始化失败:' + tail
|
|
850
|
+
: '初始化失败,请重试;持续失败请升级 lark-cli 后再试。';
|
|
851
|
+
} catch {
|
|
852
|
+
if (!closed) pollTimer = setTimeout(poll, 3000);
|
|
853
|
+
}
|
|
854
|
+
};
|
|
855
|
+
pollTimer = setTimeout(poll, 3000);
|
|
856
|
+
} catch (error) {
|
|
857
|
+
statusEl.textContent = error.message || '发起初始化失败';
|
|
858
|
+
}
|
|
859
|
+
};
|
|
860
|
+
|
|
716
861
|
/** CLI 一键安装:lark 走 npm 全局安装(机器无 Node.js 时先自动补装)、
|
|
717
862
|
* ccpg/python 走官方安装器,耗时可达数分钟。响应自带安装后立即探测的
|
|
718
863
|
* 详情,直接合进缓存刷新界面。 */
|
package/lib/proc.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// (execFile 参数数组,不经 shell 拼接;win32 的 .cmd/.bat shim 是唯一
|
|
6
6
|
// 例外——按 CVE-2024-27980 后的 Node 约定经 cmd.exe /d /s /c 执行,
|
|
7
7
|
// 参数仍是数组形态)与错误翻译;包名/版本白名单在 index.js 的调用侧执行。
|
|
8
|
-
import { execFile } from 'node:child_process';
|
|
8
|
+
import { execFile, spawn } from 'node:child_process';
|
|
9
9
|
import { existsSync, mkdirSync, readdirSync, renameSync, rmSync } from 'node:fs';
|
|
10
10
|
import { homedir, tmpdir } from 'node:os';
|
|
11
11
|
import { join, dirname, isAbsolute, delimiter as pathDelimiter } from 'node:path';
|
|
@@ -296,13 +296,21 @@ export function decodeOutput(buf) {
|
|
|
296
296
|
* cmd.exe /d /s /c 路由,cmd.exe 在第三个兑底目录(System32)。只前置
|
|
297
297
|
* 解析到的那个目录会让 shim 内部找不到依赖,必须整组前置。输出按
|
|
298
298
|
* encoding:buffer 收回后 decodeOutput(GBK 容错)。opts.env 只传增量变量。 */
|
|
299
|
-
|
|
300
|
-
|
|
299
|
+
/** 子进程 env 组装:进程 env + 桌面 shim 变量 + 调用方增量,PATH 前置
|
|
300
|
+
* 全部兑底目录。execResolvedPath 与 config init 长驻子进程共用,保证两
|
|
301
|
+
* 条路径的子进程看到同一个世界。 */
|
|
302
|
+
function composeChildEnv(extraEnv) {
|
|
303
|
+
const childEnv = { ...process.env, ...desktopShimEnvDefaults(), ...(extraEnv || {}) };
|
|
301
304
|
const key = pathEnvKey(childEnv, process.platform);
|
|
302
305
|
const extras = extraBinDirs().filter((dir) => existsSync(dir));
|
|
303
306
|
if (extras.length) {
|
|
304
307
|
childEnv[key] = childPathWithExtras(childEnv[key], extras, desktopShimDirs());
|
|
305
308
|
}
|
|
309
|
+
return childEnv;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function execResolvedPath(resolved, args, { timeoutMs, cwd, env } = {}) {
|
|
313
|
+
const childEnv = composeChildEnv(env);
|
|
306
314
|
const shim = cmdShimInvocation(process.platform, resolved, args);
|
|
307
315
|
return execFileAsync(shim ? shim.file : resolved, shim ? shim.args : args, {
|
|
308
316
|
timeout: timeoutMs,
|
|
@@ -595,6 +603,202 @@ export function isTerminalCliAuthFailure(error) {
|
|
|
595
603
|
return /expired|invalid|denied|拒绝|失效|过期/i.test(String(error?.message || ''));
|
|
596
604
|
}
|
|
597
605
|
|
|
606
|
+
// ── lark-cli 首次应用初始化(config init --new)───────────────────────
|
|
607
|
+
//
|
|
608
|
+
// lark-cli ≥1.0.9x 首次使用必须先 `config init` 创建应用(写
|
|
609
|
+
// ~/.lark-cli/config.json),auth login 设备流才可用;未配置时 auth
|
|
610
|
+
// status / auth login 均以退出码 3 返回 not_configured。config init 是
|
|
611
|
+
// **阻塞式**命令:打印验证 URL 后原地轮询 /oauth/v1/app/registration
|
|
612
|
+
// 直到浏览器完成,没有 --no-wait/--json(实测 1.0.96 参数面)——因此
|
|
613
|
+
// 不能走 runCliTool(execFile 等退出才拿得到输出),必须长驻子进程 +
|
|
614
|
+
// 流式捕获 stdout 解析 URL,终态由状态轮询接口读取。
|
|
615
|
+
//
|
|
616
|
+
// 两处硬约束:
|
|
617
|
+
// - lark-cli 检测到 HERMES_HOME/OPENCLAW_HOME 即认为 Agent 上下文,
|
|
618
|
+
// config init 直接拒绝执行——初始化子进程必须剥离这两个变量。
|
|
619
|
+
// 注意 auth login / auth status **不剥**:托管容器里它们正是 lark-cli
|
|
620
|
+
// 读取作用域配置(/opt/data/.lark-cli/hermes)的依据。
|
|
621
|
+
// - win32 下命令是 .cmd shim(cmd.exe → node → lark-cli.exe),只杀
|
|
622
|
+
// cmd.exe 会漏掉 lark-cli.exe 继续在后台轮询(实测复现),必须
|
|
623
|
+
// taskkill /T /F 杀整棵进程树。
|
|
624
|
+
|
|
625
|
+
/** 判定输出是否为「应用未配置」(auth status/login 的 not_configured)。 */
|
|
626
|
+
export function larkNotConfigured(raw) {
|
|
627
|
+
const data = parseJsonObject(String(raw || '').trim());
|
|
628
|
+
const error = data && typeof data.error === 'object' ? data.error : null;
|
|
629
|
+
return Boolean(error && String(error.subtype || '') === 'not_configured');
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/** 从 config init --new 的流式输出解析验证链接与 user_code(纯函数)。
|
|
633
|
+
* 输出里的 ASCII 二维码块不含 http,首个 URL 即官方打印的配置链接。 */
|
|
634
|
+
export function parseLarkCliInitOutput(text) {
|
|
635
|
+
const match = /https?:\/\/[^\s'"<>]+/.exec(String(text || ''));
|
|
636
|
+
if (!match) return null;
|
|
637
|
+
const url = match[0];
|
|
638
|
+
let userCode = '';
|
|
639
|
+
try {
|
|
640
|
+
userCode = new URL(url).searchParams.get('user_code') || '';
|
|
641
|
+
} catch {
|
|
642
|
+
userCode = '';
|
|
643
|
+
}
|
|
644
|
+
return { url, userCode };
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/** 剥离 Agent 上下文变量(纯函数,不改动入参):config init 在
|
|
648
|
+
* HERMES_HOME/OPENCLAW_HOME 下会拒绝执行。 */
|
|
649
|
+
export function stripAgentHomeEnv(env) {
|
|
650
|
+
const next = { ...(env || {}) };
|
|
651
|
+
delete next.HERMES_HOME;
|
|
652
|
+
delete next.OPENCLAW_HOME;
|
|
653
|
+
return next;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const LARK_INIT_WATCHDOG_MS = 20 * 60_000;
|
|
657
|
+
const LARK_INIT_URL_TIMEOUT_MS = 30_000;
|
|
658
|
+
|
|
659
|
+
/** 当前(或上一次)初始化会话;终态结果保留到下一次 start,供状态轮询。 */
|
|
660
|
+
let larkInitSession = null;
|
|
661
|
+
|
|
662
|
+
/** 进行中初始化会话的 URL(二维码接口的门禁用;无进行中会话返回 null)。 */
|
|
663
|
+
export function larkCliInitActiveUrl() {
|
|
664
|
+
return larkInitSession && larkInitSession.status === 'running' ? larkInitSession.url : null;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/** 初始化会话快照(无会话时 status: 'idle')。output 为输出尾部(诊断用)。 */
|
|
668
|
+
export function larkCliInitStatus() {
|
|
669
|
+
if (!larkInitSession) return { status: 'idle' };
|
|
670
|
+
const { url, userCode, startedAt, status, exitCode } = larkInitSession;
|
|
671
|
+
return {
|
|
672
|
+
status,
|
|
673
|
+
url,
|
|
674
|
+
userCode,
|
|
675
|
+
startedAt,
|
|
676
|
+
exitCode,
|
|
677
|
+
output: (larkInitSession.tail || []).slice(-8).join('\n'),
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/** 杀整棵进程树(win32 的 .cmd shim 形态:cmd.exe → node → lark-cli.exe)。 */
|
|
682
|
+
function killProcessTree(child) {
|
|
683
|
+
if (!child || child.exitCode !== null || child.killed || !child.pid) return;
|
|
684
|
+
if (process.platform === 'win32') {
|
|
685
|
+
const taskkill = resolveCommandOrNull('taskkill') || 'taskkill';
|
|
686
|
+
execFileAsync(taskkill, ['/PID', String(child.pid), '/T', '/F']).catch(() => {
|
|
687
|
+
child.kill('SIGKILL');
|
|
688
|
+
});
|
|
689
|
+
} else {
|
|
690
|
+
child.kill('SIGKILL');
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* 启动(或复用进行中的)lark-cli 应用初始化:spawn `config init --new`,
|
|
696
|
+
* 流式捕获输出、解析出验证 URL 即返回;子进程继续在后台等待浏览器完成,
|
|
697
|
+
* 终态(succeeded/failed/cancelled)由 larkCliInitStatus 轮询。URL 未在
|
|
698
|
+
* urlTimeoutMs 内出现视为启动失败(老版本无该子命令/输出形态变化)。
|
|
699
|
+
*/
|
|
700
|
+
export async function startLarkCliInit({ urlTimeoutMs = LARK_INIT_URL_TIMEOUT_MS } = {}) {
|
|
701
|
+
if (larkInitSession && larkInitSession.status === 'running') {
|
|
702
|
+
const existing = await Promise.race([
|
|
703
|
+
larkInitSession.ready,
|
|
704
|
+
new Promise((_, reject) => setTimeout(
|
|
705
|
+
() => reject(cliFailure('timeout', '初始化正在启动中,请稍后重试')),
|
|
706
|
+
urlTimeoutMs,
|
|
707
|
+
)),
|
|
708
|
+
]);
|
|
709
|
+
return { url: existing.url, userCode: existing.userCode, reused: true };
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const resolved = resolveCommandOrNull('lark-cli');
|
|
713
|
+
if (!resolved) {
|
|
714
|
+
throw cliFailure('missing_command', missingCommandGuidance('lark-cli'));
|
|
715
|
+
}
|
|
716
|
+
const args = ['config', 'init', '--new'];
|
|
717
|
+
const shim = cmdShimInvocation(process.platform, resolved, args);
|
|
718
|
+
const session = {
|
|
719
|
+
child: null,
|
|
720
|
+
url: null,
|
|
721
|
+
userCode: '',
|
|
722
|
+
startedAt: Date.now(),
|
|
723
|
+
status: 'running',
|
|
724
|
+
exitCode: null,
|
|
725
|
+
tail: [],
|
|
726
|
+
chunks: [],
|
|
727
|
+
ready: null,
|
|
728
|
+
watchdog: null,
|
|
729
|
+
};
|
|
730
|
+
let resolveReady;
|
|
731
|
+
session.ready = new Promise((resolve) => {
|
|
732
|
+
resolveReady = resolve;
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
const onChunk = (buf) => {
|
|
736
|
+
session.chunks.push(buf);
|
|
737
|
+
const text = decodeOutput(Buffer.concat(session.chunks));
|
|
738
|
+
session.tail = text.split('\n').map((line) => line.trim()).filter(Boolean);
|
|
739
|
+
if (!session.url) {
|
|
740
|
+
const parsed = parseLarkCliInitOutput(text);
|
|
741
|
+
if (parsed) {
|
|
742
|
+
session.url = parsed.url;
|
|
743
|
+
session.userCode = parsed.userCode;
|
|
744
|
+
resolveReady(parsed);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
};
|
|
748
|
+
|
|
749
|
+
session.child = spawn(shim ? shim.file : resolved, shim ? shim.args : args, {
|
|
750
|
+
env: stripAgentHomeEnv(composeChildEnv()),
|
|
751
|
+
windowsVerbatimArguments: shim?.verbatim === true,
|
|
752
|
+
windowsHide: true,
|
|
753
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
754
|
+
});
|
|
755
|
+
session.child.stdout.on('data', onChunk);
|
|
756
|
+
session.child.stderr.on('data', onChunk);
|
|
757
|
+
session.child.on('error', (error) => {
|
|
758
|
+
session.tail.push(String(error?.message || error));
|
|
759
|
+
if (!session.url) resolveReady({ url: null, userCode: '' });
|
|
760
|
+
session.status = 'failed';
|
|
761
|
+
});
|
|
762
|
+
session.child.on('exit', (code) => {
|
|
763
|
+
clearTimeout(session.watchdog);
|
|
764
|
+
session.exitCode = code;
|
|
765
|
+
if (!session.url) resolveReady({ url: null, userCode: '' });
|
|
766
|
+
if (session.status !== 'cancelled') {
|
|
767
|
+
session.status = code === 0 ? 'succeeded' : 'failed';
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
session.watchdog = setTimeout(() => {
|
|
771
|
+
session.tail.push('[plugin] 初始化超时(20 分钟),已终止,请重试');
|
|
772
|
+
killProcessTree(session.child);
|
|
773
|
+
if (session.status === 'running') session.status = 'cancelled';
|
|
774
|
+
}, LARK_INIT_WATCHDOG_MS);
|
|
775
|
+
|
|
776
|
+
larkInitSession = session;
|
|
777
|
+
const parsed = await Promise.race([
|
|
778
|
+
session.ready,
|
|
779
|
+
new Promise((_, reject) => setTimeout(
|
|
780
|
+
() => reject(cliFailure('timeout', '初始化启动超时:未能从 lark-cli 输出中解析到配置链接,请重试;持续失败请升级 lark-cli 后再试')),
|
|
781
|
+
urlTimeoutMs,
|
|
782
|
+
)),
|
|
783
|
+
]);
|
|
784
|
+
if (!parsed?.url) {
|
|
785
|
+
throw cliFailure(
|
|
786
|
+
'init_failed',
|
|
787
|
+
`初始化进程异常退出:${session.tail.slice(-3).join(';')}`.slice(0, 300),
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
return { url: parsed.url, userCode: parsed.userCode, reused: false };
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/** 取消进行中的初始化(杀进程树);无可取消会话返回 false。 */
|
|
794
|
+
export async function stopLarkCliInit() {
|
|
795
|
+
if (!larkInitSession || larkInitSession.status !== 'running') return false;
|
|
796
|
+
larkInitSession.status = 'cancelled';
|
|
797
|
+
clearTimeout(larkInitSession.watchdog);
|
|
798
|
+
killProcessTree(larkInitSession.child);
|
|
799
|
+
return true;
|
|
800
|
+
}
|
|
801
|
+
|
|
598
802
|
/** 从 pnpm -g 前置校验报错里解析全局 bin 目录。pnpm 12 起输出会把
|
|
599
803
|
* "is not in PATH" 折行并加竖线装饰(单行正则取不到),且 runCliTool
|
|
600
804
|
* 的摘要只保留 ERR_ 行(路径被丢弃)——必须对 error.raw 完整输出跑。 */
|
|
@@ -659,10 +863,14 @@ export async function probeCliDetail(tool, { timeoutMs = 15000, withLatest = tru
|
|
|
659
863
|
const state = larkStyleAuthorized(stdout) || ccpgStyleAuthorized(stdout)
|
|
660
864
|
? 'authorized'
|
|
661
865
|
: 'unauthorized';
|
|
866
|
+
// lark-cli 首装机器上 auth status 退出码 3 + not_configured:仍归
|
|
867
|
+
// unauthorized(已装未授权大类不变),额外带 unconfigured 标志让前端
|
|
868
|
+
// 把「去授权」换成「初始化应用」。ccpg-cli 无此形态,恒为 false。
|
|
869
|
+
const unconfigured = tool === 'lark-cli' && state === 'unauthorized' && larkNotConfigured(stdout);
|
|
662
870
|
const latest = withLatest
|
|
663
871
|
? await cliLatestVersion(tool, timeoutMs).catch(() => null)
|
|
664
872
|
: null;
|
|
665
|
-
return { state, version: selected.version, latest };
|
|
873
|
+
return { state, unconfigured, version: selected.version, latest };
|
|
666
874
|
}
|
|
667
875
|
|
|
668
876
|
/** npm registry 的包 latest 版本(curl 直查 JSON,与机器上有无 node/
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-agentone",
|
|
3
|
-
"version": "0.5.
|
|
4
|
-
"description": "AgentOne 平台集成:平台模型、SkillHub 技能、套餐与 CLI
|
|
3
|
+
"version": "0.5.37",
|
|
4
|
+
"description": "AgentOne 平台集成:平台模型、SkillHub 技能、套餐与 CLI;lark-cli 首装一键初始化飞书应用;平台托管实例免登录自动授权",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"smoke": "node scripts/env-matrix-smoke.mjs"
|