dsh-agentone 0.5.34 → 0.5.36

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 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 级指令优先);幂等不重复注入,用户已有语言
package/lib/index.js CHANGED
@@ -32,10 +32,11 @@ import { applyPlatformModel, removePlatformModel } from './model.js';
32
32
  import { installPlatformSkill, listPlatformSkills, removePlatformSkill } from './skill.js';
33
33
  import { applyPlatformSkillhub, removePlatformSkillhub } from './skillhub.js';
34
34
  import { ensureLanguageDefault } from './language.js';
35
+ import { bootstrapInstanceCredentials, INSTANCE_SOURCE, isInstanceManaged, resolvePlatformUrl } from './instance.js';
35
36
  import { beginCliAuth, cliAuthQrcode, completeCliAuth, parseCliAuthBeginOutput } from './cliauth.mjs';
36
37
  import { platformFetch } from './http.js';
37
38
  import { donePageHtml, statusPageHtml } from './page.js';
38
- 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';
39
40
 
40
41
  import { clearCredentials, loadCredentials, resolveDshHome, saveCredentials, updateCredentials } from './store.js';
41
42
 
@@ -735,6 +736,7 @@ button{font:inherit;padding:9px 22px;border-radius:10px;border:none;cursor:point
735
736
  const expiresIn = Number(result.expires_in) || 0;
736
737
  await saveCredentials(config, {
737
738
  schema_version: 2,
739
+ source: 'user',
738
740
  access_token: result.access_token,
739
741
  refresh_token: result.refresh_token || null,
740
742
  platform_url: config.platformUrl,
@@ -778,6 +780,13 @@ function isDesktopHost() {
778
780
 
779
781
  export function apply(ctx, config) {
780
782
  const log = ctx.logger || console;
783
+ // 托管实例:注入的平台地址优先。容器内只有这个地址可达(本地部署是
784
+ // host.docker.internal),插件配置里的默认公网地址在容器里打不通。
785
+ const injectedPlatformUrl = resolvePlatformUrl(config, null);
786
+ if (isInstanceManaged() && injectedPlatformUrl !== config.platformUrl) {
787
+ log.info(`[dsh-agentone] platform url from instance env: ${injectedPlatformUrl}`);
788
+ config.platformUrl = injectedPlatformUrl;
789
+ }
781
790
  // Desktop 环境探测(官方契约,见 harness-one DESKTOP.md):desktopProfiles
782
791
  // 只在 DSH Desktop 存在。据此把插件管理切到当前 profile,并用受管
783
792
  // desktopPnpm 做装/卸/升级(desktop 里 dsh CLI 拒绝操作 desktop profile)。
@@ -835,10 +844,18 @@ export function apply(ctx, config) {
835
844
  let skills = null;
836
845
  let displayNames = {};
837
846
  if (credentials) {
838
- const profile = await fetchPlatformProfile(config.platformUrl, credentials.access_token);
839
- userEmail = profile?.email ?? null;
840
- if (userEmail) {
847
+ // 身份来源分两种:设备登录走 /api/me(用户令牌);托管实例的实例级
848
+ // 凭据按权限边界**不能**调 /api/me(它没有 sub,普通 API 一律 401),
849
+ // 身份改由插件能力面带回(capabilities.email)。两者都拿不到才算未登录。
850
+ if (isInstanceManaged()) {
841
851
  ({ capabilities, skills, displayNames } = await cachedPlatformStatus(config, credentials));
852
+ userEmail = capabilities?.email ?? null;
853
+ } else {
854
+ const profile = await fetchPlatformProfile(config.platformUrl, credentials.access_token);
855
+ userEmail = profile?.email ?? null;
856
+ if (userEmail) {
857
+ ({ capabilities, skills, displayNames } = await cachedPlatformStatus(config, credentials));
858
+ }
842
859
  }
843
860
  // 失败自愈:模型同步残留错误时由轮询驱动后台重试(成功即清,
844
861
  // 下次轮询状态页恢复);错误已被登录/手动同步清除时取消待定
@@ -867,6 +884,10 @@ export function apply(ctx, config) {
867
884
  platform_url: config.platformUrl,
868
885
  logged_in: Boolean(userEmail),
869
886
  user_email: userEmail,
887
+ // 托管实例:凭据由平台注入,页面据此显示「平台托管」并隐藏登录/登出
888
+ managed: isInstanceManaged(),
889
+ source: credentials?.source ?? null,
890
+ instance_id: credentials?.instance_id ?? null,
870
891
  token_expires_at: credentials?.expires_at ?? null,
871
892
  plugin_version: PLUGIN_VERSION,
872
893
  capabilities,
@@ -1308,6 +1329,17 @@ export function apply(ctx, config) {
1308
1329
  // 与 pendingStates 同生命周期语义——插件重启后用户重新发起即可。
1309
1330
  const pendingCliAuth = new Map();
1310
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
+
1311
1343
  register('/agentone/api/cli/auth/begin', async (request, response) => {
1312
1344
  if (request.method !== 'POST') {
1313
1345
  response.writeHead(405, { allow: 'POST' });
@@ -1325,12 +1357,16 @@ export function apply(ctx, config) {
1325
1357
  const detail = await probeCliDetail(tool, { withLatest: false })
1326
1358
  .then(withCliUpgradeGuard).catch(() => null);
1327
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) {
1328
1366
  sendJson(response, 400, {
1329
- error: detail.reason === 'broken_install'
1330
- ? 'CLI 安装不完整,请先点击「修复安装」后再授权'
1331
- : detail.reason === 'unavailable'
1332
- ? 'CLI 暂时无法访问(可能被安全软件占用或响应超时),请稍后重试;若持续失败请重新安装'
1333
- : 'CLI 未安装,请先安装或联系管理员',
1367
+ error: isInstanceManaged()
1368
+ ? 'lark-cli 应用配置由平台统一下发,当前实例缺少配置,请重启实例或联系管理员'
1369
+ : 'lark-cli 尚未完成应用初始化,请先点击「初始化应用」完成飞书应用创建',
1334
1370
  });
1335
1371
  return;
1336
1372
  }
@@ -1430,7 +1466,7 @@ export function apply(ctx, config) {
1430
1466
  // CLI(lark-cli / ccpg-cli)的进行中授权会话均可——二维码由内置
1431
1467
  // 编码器生成(lib/qrcode.mjs),不依赖 lark-cli 本机存在。
1432
1468
  const queryUrl = new URL(request.url, 'http://localhost').searchParams.get('url') || '';
1433
- if (!queryUrl || pendingCliAuth.size === 0) {
1469
+ if (!queryUrl || (pendingCliAuth.size === 0 && !larkCliInitActiveUrl())) {
1434
1470
  sendJson(response, 400, { error: '没有进行中的授权会话' });
1435
1471
  return;
1436
1472
  }
@@ -1444,6 +1480,100 @@ export function apply(ctx, config) {
1444
1480
  }
1445
1481
  });
1446
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
+
1447
1577
  register('/agentone/api/plan', async (request, response) => {
1448
1578
  if (request.method !== 'GET') {
1449
1579
  response.writeHead(405, { allow: 'GET' });
@@ -1500,6 +1630,14 @@ export function apply(ctx, config) {
1500
1630
  response.end();
1501
1631
  return;
1502
1632
  }
1633
+ // 托管实例的授权来自容器 env(Hub 每次启动重签):在这里登出既清不掉
1634
+ // 根因,重启后又会「自动登录」,只会让用户困惑——明确说明正确做法。
1635
+ if (isInstanceManaged()) {
1636
+ sendJson(response, 400, {
1637
+ error: '当前实例由 AgentOne 平台托管,授权随实例生命周期自动维护;如需停止使用请删除该实例',
1638
+ });
1639
+ return;
1640
+ }
1503
1641
  try {
1504
1642
  // 先吊销平台侧 refresh token(尽力而为),再清本地;凭据读取失败
1505
1643
  // (文件损坏等)也要走到清凭据,不能把用户卡在登录态。
@@ -1531,10 +1669,18 @@ export function apply(ctx, config) {
1531
1669
  log.warn(`[dsh-agentone] language default check failed: ${error?.message || error}`);
1532
1670
  });
1533
1671
  (async () => {
1672
+ // 托管实例先把 Hub 注入的凭据落盘,再走既有的「已登录」启动流程。
1673
+ await bootstrapInstanceCredentials(config, log).catch((error) => {
1674
+ log.warn(`[dsh-agentone] instance credential bootstrap failed: ${error?.message || error}`);
1675
+ });
1534
1676
  const credentials = await ensureFreshToken(config, log);
1535
1677
  if (!credentials) return;
1536
1678
  await syncPlatformModel(config, log).catch(() => {});
1537
- await syncPlatformSkillhub(config, log, credentials).catch(() => {});
1679
+ // SkillHub 连接在托管容器里由 Hub 直接注入 env(CLAWHUB_* / SKILLHUB_*),
1680
+ // 插件不再重复写 skillhub.env / clawhub login,避免两个写入方互相盖。
1681
+ if (!isInstanceManaged()) {
1682
+ await syncPlatformSkillhub(config, log, credentials).catch(() => {});
1683
+ }
1538
1684
  await syncPlanSkills(config, log, credentials).catch(() => {});
1539
1685
  })();
1540
1686
 
@@ -0,0 +1,123 @@
1
+ // 平台托管实例的自动授权(instance bootstrap)。
2
+ //
3
+ // 平台托管的 dsh 容器里没有人在浏览器里点飞书登录,所以 Hub 在容器启动时
4
+ // 注入实例级凭据(app/services/plugin_instance_auth_service.py 签发):
5
+ //
6
+ // AGENTONE_PLATFORM_URL 容器内可达的平台地址(本地部署是 host.docker.internal)
7
+ // AGENTONE_PLUGIN_TOKEN 实例级凭据,只够调 /api/plugin/v1/*
8
+ // AGENTONE_INSTANCE_ID 实例 id(Hub 对全部镜像统一注入,用于展示与自检)
9
+ //
10
+ // 插件启动读到 token 就把它落成与设备登录同形状的 credentials.json
11
+ // (source: "instance"),后续所有逻辑(模型同步、技能、套餐)无需区分来源。
12
+ // 容器每次 start 都会重建,env 随之重签,因此这里每次都对账:
13
+ //
14
+ // - env token 与本地已存的不同(新容器 / 属主变更 / TTL 轮换)→ 覆盖;
15
+ // - 相同 → 不写盘,避免无谓的原子写与 watcher 抖动;
16
+ // - 没有 env token(独立版/桌面版 dsh)→ 完全不动本地凭据。
17
+ //
18
+ // 凭据对容器内智能体可见(同为 10000:10000),所以它必须是这条最小面凭据——
19
+ // 绝不能是用户主账号令牌(那样 agent 就拿到了 portal 全权)。
20
+ import { loadCredentials, saveCredentials, updateCredentials } from './store.js';
21
+
22
+ export const PLATFORM_URL_ENV = 'AGENTONE_PLATFORM_URL';
23
+ export const PLUGIN_TOKEN_ENV = 'AGENTONE_PLUGIN_TOKEN';
24
+ export const INSTANCE_ID_ENV = 'AGENTONE_INSTANCE_ID';
25
+
26
+ /** 托管凭据的来源标记:设备登录写 'user',Hub 注入写 'instance'。 */
27
+ export const INSTANCE_SOURCE = 'instance';
28
+ export const USER_SOURCE = 'user';
29
+
30
+ /** 读取注入的实例授权环境变量(未注入时 token 为空)。 */
31
+ export function readInstanceEnv(env = process.env) {
32
+ return {
33
+ token: String(env[PLUGIN_TOKEN_ENV] || '').trim(),
34
+ platformUrl: String(env[PLATFORM_URL_ENV] || '').trim().replace(/\/+$/, ''),
35
+ instanceId: String(env[INSTANCE_ID_ENV] || '').trim(),
36
+ };
37
+ }
38
+
39
+ /** 是否运行在平台托管实例内(判据是注入了实例凭据,而非镜像特征)。 */
40
+ export function isInstanceManaged(env = process.env) {
41
+ return Boolean(readInstanceEnv(env).token);
42
+ }
43
+
44
+ /**
45
+ * 解析 JWT 的 exp(秒)为 ISO 字符串;解不出来返回 null。
46
+ * 只是为了状态页能显示有效期,失败不影响功能(凭据有效性由平台判定)。
47
+ */
48
+ export function tokenExpiresAt(token) {
49
+ const parts = String(token || '').split('.');
50
+ if (parts.length < 2) return null;
51
+ try {
52
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
53
+ const exp = Number(payload?.exp);
54
+ if (!Number.isFinite(exp) || exp <= 0) return null;
55
+ return new Date(exp * 1000).toISOString();
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+
61
+ /**
62
+ * 把 Hub 注入的实例凭据写入本地凭据库(幂等)。
63
+ *
64
+ * @returns {Promise<{applied: boolean, instanceId: string, platformUrl: string}|null>}
65
+ * null 表示当前不是托管实例(无 env token),未做任何改动。
66
+ */
67
+ export async function bootstrapInstanceCredentials(config, log, env = process.env) {
68
+ const { token, platformUrl, instanceId } = readInstanceEnv(env);
69
+ if (!token) return null;
70
+
71
+ const platform = platformUrl || config.platformUrl;
72
+ const existing = await loadCredentials(config);
73
+ if (
74
+ existing
75
+ && existing.source === INSTANCE_SOURCE
76
+ && existing.access_token === token
77
+ && existing.platform_url === platform
78
+ ) {
79
+ // 容器重建后 env 变了才会走到覆盖分支;这里只补 instance_id/expires 缺失的旧记录
80
+ if (existing.instance_id === instanceId && existing.expires_at) {
81
+ return { applied: false, instanceId, platformUrl: platform };
82
+ }
83
+ }
84
+
85
+ const expiresAt = tokenExpiresAt(token);
86
+ await saveCredentials(config, {
87
+ schema_version: 2,
88
+ source: INSTANCE_SOURCE,
89
+ access_token: token,
90
+ // 实例凭据不带 refresh:容器每次 start 由 Hub 重签(重启即续期),
91
+ // 也让「登出即失效」这类语义留在平台侧。
92
+ refresh_token: null,
93
+ platform_url: platform,
94
+ instance_id: instanceId || null,
95
+ saved_at: new Date().toISOString(),
96
+ expires_at: expiresAt,
97
+ });
98
+ // 上一轮设备登录留下的同步结果/错误不再适用于新凭据
99
+ await updateCredentials(config, {
100
+ model_error: null,
101
+ model_error_code: null,
102
+ skillhub_error: null,
103
+ }).catch(() => {});
104
+ if (log) {
105
+ log.info(
106
+ `[dsh-agentone] instance credential applied (instance=${instanceId || 'unknown'}, platform=${platform})`,
107
+ );
108
+ }
109
+ return { applied: true, instanceId, platformUrl: platform };
110
+ }
111
+
112
+ /**
113
+ * 平台地址优先级:注入的实例地址 > 本地凭据里记录的平台 > 插件配置默认值。
114
+ * 托管实例里注入值必须胜出——那是容器内唯一可达的地址。
115
+ */
116
+ export function resolvePlatformUrl(config, credentials, env = process.env) {
117
+ const injected = readInstanceEnv(env).platformUrl;
118
+ return (
119
+ injected
120
+ || credentials?.platform_url
121
+ || config.platformUrl
122
+ ).replace(/\/+$/, '');
123
+ }
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
- : state === 'unauthorized'
542
- ? '<span class="pill warn" style="font-size:11px">未授权</span>'
543
- : info.reason === 'broken_install'
544
- ? '<span class="pill warn" style="font-size:11px">安装损坏</span>'
545
- : '<span class="pill warn" style="font-size:11px">未安装</span>';
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
- state === 'unauthorized'
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 同样支持);已安装未授权时点「去授权」用飞书扫码完成即可。</div>';
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
  * 详情,直接合进缓存刷新界面。 */
@@ -970,6 +1115,7 @@ const renderAbout = (s) => {
970
1115
  const lines = [
971
1116
  aboutVersionRow(),
972
1117
  '<div class="kv"><span class="k">登录账号</span><span class="v">' + esc(s.user_email || '未登录') + '</span></div>',
1118
+ '<div class="kv"><span class="k">授权方式</span><span class="v muted">' + (s.managed ? '平台托管(实例内自动授权,无需登录)' : '飞书登录') + '</span></div>',
973
1119
  '<div class="kv"><span class="k">所属套餐</span><span class="v">' + esc(s.capabilities?.plan_name || '—') + '</span></div>',
974
1120
  '<div class="kv"><span class="k">平台地址</span><span class="v muted">' + esc(s.platform_url || '—') + '</span></div>',
975
1121
  '<div class="kv"><span class="k">开发者</span><span class="v"><a href="mailto:chumingjun@cc-pg.cn">chumingjun@cc-pg.cn</a></span></div>',
@@ -1053,12 +1199,15 @@ const refresh = async () => {
1053
1199
  // 瞬时失败)就触发一次同步;节流 + 单飞,装完列表变 installed 自然停
1054
1200
  if (s.logged_in && skillhubEnabled) maybeAutoSyncPlanSkills(false);
1055
1201
  statePill.className = 'pill ' + (s.logged_in ? 'ok' : 'warn');
1056
- statePill.textContent = s.logged_in ? '已登录' : '未登录';
1202
+ // 托管实例(平台注入实例凭据)显示「平台托管」:用户没有做过登录动作,
1203
+ // 写「已登录」会让人以为是自己登的,还会去找登出按钮。
1204
+ statePill.textContent = s.logged_in ? (s.managed ? '平台托管' : '已登录') : '未登录';
1057
1205
  // 首个状态到达:骨架占位让位给登录卡/主内容
1058
1206
  bootCard.hidden = true;
1059
1207
  loginCard.hidden = s.logged_in;
1060
1208
  mainCards.hidden = !s.logged_in;
1061
- logoutBtn.hidden = !s.logged_in;
1209
+ // 托管态不给登出按钮:授权来自容器 env,登出既清不掉根因、重启又会自动登录
1210
+ logoutBtn.hidden = !s.logged_in || Boolean(s.managed);
1062
1211
  if (s.logged_in && s.model) {
1063
1212
  const m = s.model;
1064
1213
  const lines = [];
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
- function execResolvedPath(resolved, args, { timeoutMs, cwd, env } = {}) {
300
- const childEnv = { ...process.env, ...desktopShimEnvDefaults(), ...(env || {}) };
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.34",
4
- "description": "AgentOne 平台集成插件:飞书登录、平台模型(直连 Token Hub 不过境平台、同步失败自愈重试)、SkillHub 技能(升级检测一键升级、装后即时刷新、套餐技能自动同步)、套餐申请、插件管理与自助升级、lark-cli / ccpg-cli 探测与一键安装(请求超时与错误码透传、令牌轮换 single-flight、凭据损坏容错与并发安全写入、状态页 UX 增强、全局中文回复默认(AGENTS.md 受标记管理、永不覆盖用户设置)、桌面版设置页白屏修复(iframe 子框架导航无桌面令牌改走主框架 fetch + srcdoc 渲染)、「关于」页升级中态跨状态轮询保留(点升级不再像没反应、不再并发重复升级)与 toast 常驻定位修复、平台模型同步写进 dsh 活配置层(DSH ≥0.1.7 即时生效不再要重启)与测试污染真实 home 修复)",
3
+ "version": "0.5.36",
4
+ "description": "AgentOne 平台集成:平台模型、SkillHub 技能、套餐与 CLI;lark-cli 首装一键初始化飞书应用;平台托管实例免登录自动授权",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "smoke": "node scripts/env-matrix-smoke.mjs"