dsh-agentone 0.5.33 → 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 +63 -8
- package/lib/instance.js +123 -0
- package/lib/model.js +111 -20
- package/lib/page.js +27 -12
- 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';
|
|
@@ -84,18 +85,23 @@ async function readJsonBody(request, limitBytes = 8192) {
|
|
|
84
85
|
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
85
86
|
}
|
|
86
87
|
|
|
87
|
-
/** 应用平台模型配置并把结果落盘(成功清 model_error,失败记录原因与错误码)。
|
|
88
|
+
/** 应用平台模型配置并把结果落盘(成功清 model_error,失败记录原因与错误码)。
|
|
89
|
+
* model_apply 记录本次是否写进了活配置层('live' 即时生效 / 'next-restart')。 */
|
|
88
90
|
async function syncPlatformModel(config, log) {
|
|
89
91
|
try {
|
|
90
|
-
const result = await applyPlatformModel(config);
|
|
92
|
+
const result = await applyPlatformModel(config, settingsService);
|
|
91
93
|
await updateCredentials(config, {
|
|
92
94
|
models_count: result.models.length,
|
|
93
95
|
default_model: result.defaultModel,
|
|
94
96
|
model_synced_at: new Date().toISOString(),
|
|
97
|
+
model_apply: result.apply,
|
|
95
98
|
model_error: null,
|
|
96
99
|
model_error_code: null,
|
|
97
100
|
});
|
|
98
|
-
log.info(
|
|
101
|
+
log.info(
|
|
102
|
+
`[dsh-agentone] platform model applied: ${result.defaultModel}(共 ${result.models.length} 个可用模型,`
|
|
103
|
+
+ `${result.apply === 'live' ? '已即时生效' : '重启 dsh 后生效'})`,
|
|
104
|
+
);
|
|
99
105
|
return result;
|
|
100
106
|
} catch (error) {
|
|
101
107
|
const message = error?.message || String(error);
|
|
@@ -190,6 +196,12 @@ async function ensureFreshToken(config, log) {
|
|
|
190
196
|
let PLUGIN_PROFILE = 'web';
|
|
191
197
|
/** desktop 受管插件操作通道(desktopPnpm.runPlugin 包装),非 desktop 为 null。 */
|
|
192
198
|
let desktopPluginCli = null;
|
|
199
|
+
/**
|
|
200
|
+
* dsh ≥ 0.1.7 的官方 settings 服务(apply 时可选获取):用来把平台模型写进
|
|
201
|
+
* profile patch 活配置层,同步即时生效。旧宿主没有该服务,取到 null 即回退
|
|
202
|
+
* 为「只写 settings.yaml + 下次启动导入」。
|
|
203
|
+
*/
|
|
204
|
+
let settingsService = null;
|
|
193
205
|
|
|
194
206
|
function pluginProfileDir(config) {
|
|
195
207
|
return join(resolveDshHome(config), 'profiles', PLUGIN_PROFILE);
|
|
@@ -724,6 +736,7 @@ button{font:inherit;padding:9px 22px;border-radius:10px;border:none;cursor:point
|
|
|
724
736
|
const expiresIn = Number(result.expires_in) || 0;
|
|
725
737
|
await saveCredentials(config, {
|
|
726
738
|
schema_version: 2,
|
|
739
|
+
source: 'user',
|
|
727
740
|
access_token: result.access_token,
|
|
728
741
|
refresh_token: result.refresh_token || null,
|
|
729
742
|
platform_url: config.platformUrl,
|
|
@@ -767,11 +780,22 @@ function isDesktopHost() {
|
|
|
767
780
|
|
|
768
781
|
export function apply(ctx, config) {
|
|
769
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
|
+
}
|
|
770
790
|
// Desktop 环境探测(官方契约,见 harness-one DESKTOP.md):desktopProfiles
|
|
771
791
|
// 只在 DSH Desktop 存在。据此把插件管理切到当前 profile,并用受管
|
|
772
792
|
// desktopPnpm 做装/卸/升级(desktop 里 dsh CLI 拒绝操作 desktop profile)。
|
|
773
793
|
const desktopProfiles = ctx.get?.('desktopProfiles');
|
|
774
794
|
const isDesktopRuntime = Boolean(desktopProfiles?.current?.name) || isDesktopHost();
|
|
795
|
+
// 官方 settings 服务(可选依赖):dsh ≥ 0.1.7 用它把平台模型写进 profile
|
|
796
|
+
// patch 活配置层;取不到(旧宿主)就回退「只写 settings.yaml + 下次启动导入」。
|
|
797
|
+
// 不能写进 inject:那会让插件在旧宿主上一直 PENDING。
|
|
798
|
+
settingsService = ctx.get?.('settings') ?? null;
|
|
775
799
|
if (desktopProfiles?.current?.name) {
|
|
776
800
|
PLUGIN_PROFILE = desktopProfiles.current.name;
|
|
777
801
|
ctx.inject(['desktopPnpm'], (desktopCtx) => {
|
|
@@ -820,10 +844,18 @@ export function apply(ctx, config) {
|
|
|
820
844
|
let skills = null;
|
|
821
845
|
let displayNames = {};
|
|
822
846
|
if (credentials) {
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
847
|
+
// 身份来源分两种:设备登录走 /api/me(用户令牌);托管实例的实例级
|
|
848
|
+
// 凭据按权限边界**不能**调 /api/me(它没有 sub,普通 API 一律 401),
|
|
849
|
+
// 身份改由插件能力面带回(capabilities.email)。两者都拿不到才算未登录。
|
|
850
|
+
if (isInstanceManaged()) {
|
|
826
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
|
+
}
|
|
827
859
|
}
|
|
828
860
|
// 失败自愈:模型同步残留错误时由轮询驱动后台重试(成功即清,
|
|
829
861
|
// 下次轮询状态页恢复);错误已被登录/手动同步清除时取消待定
|
|
@@ -852,6 +884,10 @@ export function apply(ctx, config) {
|
|
|
852
884
|
platform_url: config.platformUrl,
|
|
853
885
|
logged_in: Boolean(userEmail),
|
|
854
886
|
user_email: userEmail,
|
|
887
|
+
// 托管实例:凭据由平台注入,页面据此显示「平台托管」并隐藏登录/登出
|
|
888
|
+
managed: isInstanceManaged(),
|
|
889
|
+
source: credentials?.source ?? null,
|
|
890
|
+
instance_id: credentials?.instance_id ?? null,
|
|
855
891
|
token_expires_at: credentials?.expires_at ?? null,
|
|
856
892
|
plugin_version: PLUGIN_VERSION,
|
|
857
893
|
capabilities,
|
|
@@ -864,6 +900,8 @@ export function apply(ctx, config) {
|
|
|
864
900
|
default_model: credentials.default_model ?? null,
|
|
865
901
|
models_count: credentials.models_count ?? 0,
|
|
866
902
|
synced_at: credentials.model_synced_at ?? null,
|
|
903
|
+
// 'live' 已写活配置层即时生效;'next-restart' 需重启 dsh 才导入
|
|
904
|
+
apply: credentials.model_apply ?? null,
|
|
867
905
|
error: credentials.model_error ?? null,
|
|
868
906
|
error_code: credentials.model_error_code ?? null,
|
|
869
907
|
}
|
|
@@ -976,6 +1014,7 @@ export function apply(ctx, config) {
|
|
|
976
1014
|
ok: true,
|
|
977
1015
|
default_model: result.defaultModel,
|
|
978
1016
|
models_count: result.models.length,
|
|
1017
|
+
apply: result.apply,
|
|
979
1018
|
});
|
|
980
1019
|
} catch (error) {
|
|
981
1020
|
sendJson(response, 502, { error: error?.message || '同步平台模型失败' });
|
|
@@ -1482,6 +1521,14 @@ export function apply(ctx, config) {
|
|
|
1482
1521
|
response.end();
|
|
1483
1522
|
return;
|
|
1484
1523
|
}
|
|
1524
|
+
// 托管实例的授权来自容器 env(Hub 每次启动重签):在这里登出既清不掉
|
|
1525
|
+
// 根因,重启后又会「自动登录」,只会让用户困惑——明确说明正确做法。
|
|
1526
|
+
if (isInstanceManaged()) {
|
|
1527
|
+
sendJson(response, 400, {
|
|
1528
|
+
error: '当前实例由 AgentOne 平台托管,授权随实例生命周期自动维护;如需停止使用请删除该实例',
|
|
1529
|
+
});
|
|
1530
|
+
return;
|
|
1531
|
+
}
|
|
1485
1532
|
try {
|
|
1486
1533
|
// 先吊销平台侧 refresh token(尽力而为),再清本地;凭据读取失败
|
|
1487
1534
|
// (文件损坏等)也要走到清凭据,不能把用户卡在登录态。
|
|
@@ -1490,7 +1537,7 @@ export function apply(ctx, config) {
|
|
|
1490
1537
|
await revokePlatformToken(config.platformUrl, credentials.refresh_token);
|
|
1491
1538
|
}
|
|
1492
1539
|
await clearCredentials(config);
|
|
1493
|
-
await removePlatformModel(config).catch((error) => {
|
|
1540
|
+
await removePlatformModel(config, settingsService).catch((error) => {
|
|
1494
1541
|
log.warn(`[dsh-agentone] remove platform model failed: ${error?.message || error}`);
|
|
1495
1542
|
});
|
|
1496
1543
|
await removePlatformSkillhub(config, log).catch((error) => {
|
|
@@ -1513,10 +1560,18 @@ export function apply(ctx, config) {
|
|
|
1513
1560
|
log.warn(`[dsh-agentone] language default check failed: ${error?.message || error}`);
|
|
1514
1561
|
});
|
|
1515
1562
|
(async () => {
|
|
1563
|
+
// 托管实例先把 Hub 注入的凭据落盘,再走既有的「已登录」启动流程。
|
|
1564
|
+
await bootstrapInstanceCredentials(config, log).catch((error) => {
|
|
1565
|
+
log.warn(`[dsh-agentone] instance credential bootstrap failed: ${error?.message || error}`);
|
|
1566
|
+
});
|
|
1516
1567
|
const credentials = await ensureFreshToken(config, log);
|
|
1517
1568
|
if (!credentials) return;
|
|
1518
1569
|
await syncPlatformModel(config, log).catch(() => {});
|
|
1519
|
-
|
|
1570
|
+
// SkillHub 连接在托管容器里由 Hub 直接注入 env(CLAWHUB_* / SKILLHUB_*),
|
|
1571
|
+
// 插件不再重复写 skillhub.env / clawhub login,避免两个写入方互相盖。
|
|
1572
|
+
if (!isInstanceManaged()) {
|
|
1573
|
+
await syncPlatformSkillhub(config, log, credentials).catch(() => {});
|
|
1574
|
+
}
|
|
1520
1575
|
await syncPlanSkills(config, log, credentials).catch(() => {});
|
|
1521
1576
|
})();
|
|
1522
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/model.js
CHANGED
|
@@ -79,20 +79,16 @@ function parseYamlObject(text) {
|
|
|
79
79
|
return {};
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
/**
|
|
83
|
-
*
|
|
82
|
+
/** 平台 provider 配置片段。settings.yaml 与 dsh「活配置层」(profile patch)
|
|
83
|
+
* 必须写出同一形状,两处共用本构造器,避免两份定义漂移。
|
|
84
|
+
*
|
|
85
|
+
* direct(直连 Token Hub)时带 compat.supportsDeveloperRole=false:pi-ai 对
|
|
84
86
|
* reasoning 模型默认把系统提示发成 developer 角色(自定义域名不在它的
|
|
85
87
|
* 非标准名单里),GLM 上游只认 system 会 400;false 保持 system。该配置键
|
|
86
88
|
* 需要 dsh-llm-pi-ai ≥ 0.1.0-rc.8,代理模式不写(网关侧有同样的归一化)。
|
|
87
89
|
*/
|
|
88
|
-
export function
|
|
89
|
-
|
|
90
|
-
const existing = settings['llm-pi-ai'];
|
|
91
|
-
const llmPiAi =
|
|
92
|
-
existing && typeof existing === 'object' && existing.providers && typeof existing.providers === 'object'
|
|
93
|
-
? existing
|
|
94
|
-
: { providers: {} };
|
|
95
|
-
llmPiAi.providers[PROVIDER_KEY] = {
|
|
90
|
+
export function buildPlatformProvider(baseUrl, models, direct = false) {
|
|
91
|
+
return {
|
|
96
92
|
displayName: '平台模型',
|
|
97
93
|
apiKeyEnv: CREDENTIAL_REF,
|
|
98
94
|
api: 'openai-completions',
|
|
@@ -103,12 +99,24 @@ export function buildSettingsYaml(existingText, { baseUrl, models, defaultModel,
|
|
|
103
99
|
.map((item) => platformModelEntry(item))
|
|
104
100
|
.filter((entry) => entry !== null),
|
|
105
101
|
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** 平台默认模型配置片段(settings.yaml 与活层共用)。 */
|
|
105
|
+
export function buildDefaultModel(defaultModel) {
|
|
106
|
+
return { provider: PROVIDER_KEY, model: defaultModel, reasoningEffort: 'high' };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** 合并出 settings.yaml 文本:覆盖 platform provider 与 agent-default-model。 */
|
|
110
|
+
export function buildSettingsYaml(existingText, { baseUrl, models, defaultModel, direct = false }) {
|
|
111
|
+
const settings = parseYamlObject(existingText);
|
|
112
|
+
const existing = settings['llm-pi-ai'];
|
|
113
|
+
const llmPiAi =
|
|
114
|
+
existing && typeof existing === 'object' && existing.providers && typeof existing.providers === 'object'
|
|
115
|
+
? existing
|
|
116
|
+
: { providers: {} };
|
|
117
|
+
llmPiAi.providers[PROVIDER_KEY] = buildPlatformProvider(baseUrl, models, direct);
|
|
106
118
|
settings['llm-pi-ai'] = llmPiAi;
|
|
107
|
-
settings['agent-default-model'] =
|
|
108
|
-
provider: PROVIDER_KEY,
|
|
109
|
-
model: defaultModel,
|
|
110
|
-
reasoningEffort: 'high',
|
|
111
|
-
};
|
|
119
|
+
settings['agent-default-model'] = buildDefaultModel(defaultModel);
|
|
112
120
|
return stringify(settings);
|
|
113
121
|
}
|
|
114
122
|
|
|
@@ -177,11 +185,85 @@ function tmpName(path) {
|
|
|
177
185
|
}
|
|
178
186
|
|
|
179
187
|
/**
|
|
180
|
-
*
|
|
188
|
+
* 在 dsh「活配置层」(profile patch)里写一个 entry 的 volatile 字段。
|
|
189
|
+
*
|
|
190
|
+
* 活层写操作都要带 revision:冲突只重试一次(重新读 revision),绝不盲循环。
|
|
191
|
+
*/
|
|
192
|
+
async function withLiveEntry(settings, ns, run) {
|
|
193
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
194
|
+
const descriptor = settings.describe().find((row) => row?.ns === ns);
|
|
195
|
+
if (!descriptor) throw new Error(`配置项 ${ns} 不可用`);
|
|
196
|
+
try {
|
|
197
|
+
await run(descriptor.revision);
|
|
198
|
+
return;
|
|
199
|
+
} catch (error) {
|
|
200
|
+
if (error?.code !== 'SETTINGS_CONFLICT' || attempt >= 1) throw error;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* 把平台 provider 写进 dsh 活配置层,返回是否即时生效。
|
|
207
|
+
*
|
|
208
|
+
* dsh ≥ 0.1.7 起用户配置真源迁到 profile patch 层:settings.yaml 只在 host
|
|
209
|
+
* 启动时被导入一次(dsh-settings 的 importLegacyDocument,导入后改名
|
|
210
|
+
* .imported),此后 patch 层才是活配置。所以插件只写 settings.yaml 的话,
|
|
211
|
+
* 面板点「重新同步模型」要等重启才生效,界面还会长期显示上一轮导入的旧值。
|
|
212
|
+
*
|
|
213
|
+
* 有官方 settings 服务(ctx.get('settings'))时把同一份 provider 写进活层:
|
|
214
|
+
* - `llm-pi-ai` 的 providers 与 `agent-default-model` 的字段在目标版本都是
|
|
215
|
+
* volatile,可经 settings.update 写入;
|
|
216
|
+
* - 合并语义是对象深合并、数组整体替换,所以只覆盖 platform,不碰用户的
|
|
217
|
+
* 其它 provider;
|
|
218
|
+
* - llm-pi-ai 监听 loader/volatile-update 重新注册路由 → 改完即时生效。
|
|
219
|
+
*
|
|
220
|
+
* @returns 'live' 已写活层;'next-restart' 没有该服务或写入被拒(旧宿主行为)
|
|
221
|
+
*/
|
|
222
|
+
export async function applyPlatformProviderLive(settings, { baseUrl, models, defaultModel, direct = false }) {
|
|
223
|
+
if (!settings || typeof settings.describe !== 'function' || typeof settings.update !== 'function') {
|
|
224
|
+
return 'next-restart';
|
|
225
|
+
}
|
|
226
|
+
try {
|
|
227
|
+
const provider = buildPlatformProvider(baseUrl, models, direct);
|
|
228
|
+
await withLiveEntry(settings, 'llm-pi-ai', (revision) =>
|
|
229
|
+
settings.update('llm-pi-ai', { providers: { [PROVIDER_KEY]: provider } }, revision),
|
|
230
|
+
);
|
|
231
|
+
} catch {
|
|
232
|
+
// 活层写不进去不阻断:settings.yaml 已写成功,下次启动仍会导入(旧行为)
|
|
233
|
+
return 'next-restart';
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
await withLiveEntry(settings, 'agent-default-model', (revision) =>
|
|
237
|
+
settings.update('agent-default-model', buildDefaultModel(defaultModel), revision),
|
|
238
|
+
);
|
|
239
|
+
} catch {
|
|
240
|
+
// 默认模型写不进去不影响模型可用(provider 已生效),保留用户当前默认
|
|
241
|
+
}
|
|
242
|
+
return 'live';
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* 登出时清掉活层的平台 provider。
|
|
247
|
+
*
|
|
248
|
+
* 用 mutate 的 unset 语义(applyPathOp 对对象键走 Reflect.deleteProperty);
|
|
249
|
+
* 刻意不动 `agent-default-model`:它的 provider/model 是 schema required 字段,
|
|
250
|
+
* 置空会把 entry 弄成半残,而删掉 provider 已经足够让平台模型不可选。
|
|
251
|
+
*/
|
|
252
|
+
export async function removePlatformProviderLive(settings) {
|
|
253
|
+
if (!settings || typeof settings.describe !== 'function' || typeof settings.mutate !== 'function') return;
|
|
254
|
+
await withLiveEntry(settings, 'llm-pi-ai', (revision) =>
|
|
255
|
+
settings.mutate('llm-pi-ai', [{ op: 'unset', path: ['providers', PROVIDER_KEY] }], revision),
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* 拉取平台模型并写入本机 dsh 配置。返回 { models, defaultModel, apply }。
|
|
261
|
+
* apply='live' 表示已写进活配置层即时生效;'next-restart' 表示只能等下次
|
|
262
|
+
* 启动导入 settings.yaml。
|
|
181
263
|
* 失败抛 PlatformError(中文 message + 平台错误码),由调用方记录/展示
|
|
182
264
|
* (不影响已生效的旧配置)。
|
|
183
265
|
*/
|
|
184
|
-
export async function applyPlatformModel(config) {
|
|
266
|
+
export async function applyPlatformModel(config, settings) {
|
|
185
267
|
const credentials = await loadCredentials(config);
|
|
186
268
|
if (!credentials) throw new Error('尚未登录平台');
|
|
187
269
|
const { models, defaultModel, direct } = await fetchPlatformModels(
|
|
@@ -205,11 +287,19 @@ export async function applyPlatformModel(config) {
|
|
|
205
287
|
settingsPath,
|
|
206
288
|
buildSettingsYaml(existingSettings, { baseUrl, models, defaultModel, direct: direct !== null }),
|
|
207
289
|
);
|
|
208
|
-
|
|
290
|
+
// settings.yaml 与活层双写:前者兼容旧宿主,也让下次启动的导入是幂等的
|
|
291
|
+
// (只写活层的话,settings.yaml 里的旧值会在下次启动把活层盖回去)。
|
|
292
|
+
const apply = await applyPlatformProviderLive(settings, {
|
|
293
|
+
baseUrl,
|
|
294
|
+
models,
|
|
295
|
+
defaultModel,
|
|
296
|
+
direct: direct !== null,
|
|
297
|
+
});
|
|
298
|
+
return { models, defaultModel, apply };
|
|
209
299
|
}
|
|
210
300
|
|
|
211
|
-
/** 登出时清理本机配置:删除凭据引用与 platform provider
|
|
212
|
-
export async function removePlatformModel(config) {
|
|
301
|
+
/** 登出时清理本机配置:删除凭据引用与 platform provider(含活层)。 */
|
|
302
|
+
export async function removePlatformModel(config, settings) {
|
|
213
303
|
const home = resolveDshHome(config);
|
|
214
304
|
const settingsPath = join(home, 'settings.yaml');
|
|
215
305
|
const credentialsPath = join(home, '.credentials.yaml');
|
|
@@ -223,4 +313,5 @@ export async function removePlatformModel(config) {
|
|
|
223
313
|
if (existingSettings) {
|
|
224
314
|
await writeFileAtomic(settingsPath, buildLogoutSettingsYaml(existingSettings));
|
|
225
315
|
}
|
|
316
|
+
await removePlatformProviderLive(settings).catch(() => {});
|
|
226
317
|
}
|
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>',
|
|
@@ -1012,6 +1013,21 @@ const searchCatalog = async () => {
|
|
|
1012
1013
|
btn.innerHTML = '搜索';
|
|
1013
1014
|
}
|
|
1014
1015
|
};
|
|
1016
|
+
// 手动同步平台模型(模型卡与套餐阻断兜底两处共用)。成功提示必须区分是否
|
|
1017
|
+
// 已写进活配置层:dsh ≥0.1.7 走 settings 服务即时生效;旧宿主只落盘
|
|
1018
|
+
// settings.yaml,要重启才被导入——不区分的话用户会以为同步没生效。
|
|
1019
|
+
const syncModelNow = (btn) => withLoading(
|
|
1020
|
+
btn,
|
|
1021
|
+
async () => {
|
|
1022
|
+
const resp = await fetch('api/model/sync', { method: 'POST' });
|
|
1023
|
+
const data = await resp.clone().json().catch(() => ({}));
|
|
1024
|
+
if (resp.ok) {
|
|
1025
|
+
notify(data.apply === 'live' ? '平台模型已同步,已即时生效' : '平台模型已同步,重启 DSH 后生效');
|
|
1026
|
+
}
|
|
1027
|
+
return resp;
|
|
1028
|
+
},
|
|
1029
|
+
null,
|
|
1030
|
+
);
|
|
1015
1031
|
const refresh = async () => {
|
|
1016
1032
|
let s;
|
|
1017
1033
|
try {
|
|
@@ -1038,12 +1054,15 @@ const refresh = async () => {
|
|
|
1038
1054
|
// 瞬时失败)就触发一次同步;节流 + 单飞,装完列表变 installed 自然停
|
|
1039
1055
|
if (s.logged_in && skillhubEnabled) maybeAutoSyncPlanSkills(false);
|
|
1040
1056
|
statePill.className = 'pill ' + (s.logged_in ? 'ok' : 'warn');
|
|
1041
|
-
|
|
1057
|
+
// 托管实例(平台注入实例凭据)显示「平台托管」:用户没有做过登录动作,
|
|
1058
|
+
// 写「已登录」会让人以为是自己登的,还会去找登出按钮。
|
|
1059
|
+
statePill.textContent = s.logged_in ? (s.managed ? '平台托管' : '已登录') : '未登录';
|
|
1042
1060
|
// 首个状态到达:骨架占位让位给登录卡/主内容
|
|
1043
1061
|
bootCard.hidden = true;
|
|
1044
1062
|
loginCard.hidden = s.logged_in;
|
|
1045
1063
|
mainCards.hidden = !s.logged_in;
|
|
1046
|
-
|
|
1064
|
+
// 托管态不给登出按钮:授权来自容器 env,登出既清不掉根因、重启又会自动登录
|
|
1065
|
+
logoutBtn.hidden = !s.logged_in || Boolean(s.managed);
|
|
1047
1066
|
if (s.logged_in && s.model) {
|
|
1048
1067
|
const m = s.model;
|
|
1049
1068
|
const lines = [];
|
|
@@ -1064,6 +1083,10 @@ const refresh = async () => {
|
|
|
1064
1083
|
lines.push('<div class="kv"><span class="k">默认模型</span><span class="v">' + esc(m.default_model || '未同步') + '</span></div>');
|
|
1065
1084
|
lines.push('<div class="kv"><span class="k">可用模型</span><span class="v">' + esc(m.models_count) + ' 个</span></div>');
|
|
1066
1085
|
if (m.synced_at) lines.push('<div class="kv"><span class="k">上次同步</span><span class="v">' + new Date(m.synced_at).toLocaleString() + '</span></div>');
|
|
1086
|
+
// 旧宿主(无 settings 服务)才提示:同步内容要等下次启动导入才生效
|
|
1087
|
+
if (m.apply === 'next-restart') {
|
|
1088
|
+
lines.push('<div class="kv"><span class="k">生效方式</span><span class="v muted">重启 DSH 后生效</span></div>');
|
|
1089
|
+
}
|
|
1067
1090
|
}
|
|
1068
1091
|
if (!planBlocked && m.error) lines.push('<div class="kv"><span class="k">同步状态</span><span class="v" style="color:var(--err)">' + esc(m.error) + '</span></div>');
|
|
1069
1092
|
if (s.token_expires_at) {
|
|
@@ -1081,11 +1104,7 @@ const refresh = async () => {
|
|
|
1081
1104
|
// planBlocked 兜底手动出口:旧错误残留时(套餐已批但本地还挂着
|
|
1082
1105
|
// plan_required)可立即重同步,不等后端自愈的退避周期
|
|
1083
1106
|
const retryModel = $('retry-model-btn');
|
|
1084
|
-
if (retryModel) retryModel.onclick = () =>
|
|
1085
|
-
retryModel,
|
|
1086
|
-
() => fetch('api/model/sync', { method: 'POST' }),
|
|
1087
|
-
'平台模型已同步',
|
|
1088
|
-
);
|
|
1107
|
+
if (retryModel) retryModel.onclick = () => syncModelNow(retryModel);
|
|
1089
1108
|
}
|
|
1090
1109
|
if (s.logged_in && s.skillhub) {
|
|
1091
1110
|
const sh = s.skillhub;
|
|
@@ -1125,11 +1144,7 @@ $('login-btn').onclick = async () => {
|
|
|
1125
1144
|
// 按钮保持可点:授权窗被用户关掉后可立即重新发起,无需等 2 分钟
|
|
1126
1145
|
btn.textContent = '已打开授权窗口,完成后自动继续(可重开)';
|
|
1127
1146
|
};
|
|
1128
|
-
$('sync-btn').onclick = () =>
|
|
1129
|
-
$('sync-btn'),
|
|
1130
|
-
() => fetch('api/model/sync', { method: 'POST' }),
|
|
1131
|
-
'平台模型已同步',
|
|
1132
|
-
);
|
|
1147
|
+
$('sync-btn').onclick = () => syncModelNow($('sync-btn'));
|
|
1133
1148
|
$('skillhub-sync-btn').onclick = () => withLoading(
|
|
1134
1149
|
$('skillhub-sync-btn'),
|
|
1135
1150
|
() => fetch('api/skillhub/sync', { method: 'POST' }),
|
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"
|