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 +41 -4
- package/lib/instance.js +123 -0
- package/lib/page.js +6 -2
- package/package.json +2 -2
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
|
-
|
|
839
|
-
|
|
840
|
-
|
|
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
|
-
|
|
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
|
|
package/lib/instance.js
ADDED
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
4
|
-
"description": "AgentOne
|
|
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"
|