dsh-agentone 0.5.34 → 0.5.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -32,6 +32,7 @@ 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';
@@ -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,
@@ -1500,6 +1521,14 @@ export function apply(ctx, config) {
1500
1521
  response.end();
1501
1522
  return;
1502
1523
  }
1524
+ // 托管实例的授权来自容器 env(Hub 每次启动重签):在这里登出既清不掉
1525
+ // 根因,重启后又会「自动登录」,只会让用户困惑——明确说明正确做法。
1526
+ if (isInstanceManaged()) {
1527
+ sendJson(response, 400, {
1528
+ error: '当前实例由 AgentOne 平台托管,授权随实例生命周期自动维护;如需停止使用请删除该实例',
1529
+ });
1530
+ return;
1531
+ }
1503
1532
  try {
1504
1533
  // 先吊销平台侧 refresh token(尽力而为),再清本地;凭据读取失败
1505
1534
  // (文件损坏等)也要走到清凭据,不能把用户卡在登录态。
@@ -1531,10 +1560,18 @@ export function apply(ctx, config) {
1531
1560
  log.warn(`[dsh-agentone] language default check failed: ${error?.message || error}`);
1532
1561
  });
1533
1562
  (async () => {
1563
+ // 托管实例先把 Hub 注入的凭据落盘,再走既有的「已登录」启动流程。
1564
+ await bootstrapInstanceCredentials(config, log).catch((error) => {
1565
+ log.warn(`[dsh-agentone] instance credential bootstrap failed: ${error?.message || error}`);
1566
+ });
1534
1567
  const credentials = await ensureFreshToken(config, log);
1535
1568
  if (!credentials) return;
1536
1569
  await syncPlatformModel(config, log).catch(() => {});
1537
- await syncPlatformSkillhub(config, log, credentials).catch(() => {});
1570
+ // SkillHub 连接在托管容器里由 Hub 直接注入 env(CLAWHUB_* / SKILLHUB_*),
1571
+ // 插件不再重复写 skillhub.env / clawhub login,避免两个写入方互相盖。
1572
+ if (!isInstanceManaged()) {
1573
+ await syncPlatformSkillhub(config, log, credentials).catch(() => {});
1574
+ }
1538
1575
  await syncPlanSkills(config, log, credentials).catch(() => {});
1539
1576
  })();
1540
1577
 
@@ -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
@@ -970,6 +970,7 @@ const renderAbout = (s) => {
970
970
  const lines = [
971
971
  aboutVersionRow(),
972
972
  '<div class="kv"><span class="k">登录账号</span><span class="v">' + esc(s.user_email || '未登录') + '</span></div>',
973
+ '<div class="kv"><span class="k">授权方式</span><span class="v muted">' + (s.managed ? '平台托管(实例内自动授权,无需登录)' : '飞书登录') + '</span></div>',
973
974
  '<div class="kv"><span class="k">所属套餐</span><span class="v">' + esc(s.capabilities?.plan_name || '—') + '</span></div>',
974
975
  '<div class="kv"><span class="k">平台地址</span><span class="v muted">' + esc(s.platform_url || '—') + '</span></div>',
975
976
  '<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 +1054,15 @@ const refresh = async () => {
1053
1054
  // 瞬时失败)就触发一次同步;节流 + 单飞,装完列表变 installed 自然停
1054
1055
  if (s.logged_in && skillhubEnabled) maybeAutoSyncPlanSkills(false);
1055
1056
  statePill.className = 'pill ' + (s.logged_in ? 'ok' : 'warn');
1056
- statePill.textContent = s.logged_in ? '已登录' : '未登录';
1057
+ // 托管实例(平台注入实例凭据)显示「平台托管」:用户没有做过登录动作,
1058
+ // 写「已登录」会让人以为是自己登的,还会去找登出按钮。
1059
+ statePill.textContent = s.logged_in ? (s.managed ? '平台托管' : '已登录') : '未登录';
1057
1060
  // 首个状态到达:骨架占位让位给登录卡/主内容
1058
1061
  bootCard.hidden = true;
1059
1062
  loginCard.hidden = s.logged_in;
1060
1063
  mainCards.hidden = !s.logged_in;
1061
- logoutBtn.hidden = !s.logged_in;
1064
+ // 托管态不给登出按钮:授权来自容器 env,登出既清不掉根因、重启又会自动登录
1065
+ logoutBtn.hidden = !s.logged_in || Boolean(s.managed);
1062
1066
  if (s.logged_in && s.model) {
1063
1067
  const m = s.model;
1064
1068
  const lines = [];
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.35",
4
+ "description": "AgentOne 平台集成:平台模型、SkillHub 技能、套餐与 CLI;平台托管实例免登录自动授权",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "smoke": "node scripts/env-matrix-smoke.mjs"