dsh-agentone 0.5.37 → 0.5.39
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 +25 -16
- package/lib/auth.js +35 -6
- package/lib/client.js +9 -5
- package/lib/defaults-sync.js +257 -0
- package/lib/http.js +6 -3
- package/lib/index.js +236 -306
- package/lib/page.js +86 -11
- package/lib/platform-config.js +56 -0
- package/lib/proc.js +6 -4
- package/lib/profile-plugins.js +143 -0
- package/lib/skill-state.js +102 -0
- package/lib/skill.js +182 -104
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
# dsh-agentone
|
|
2
2
|
|
|
3
3
|
AgentOne 平台集成插件(dsh web 版 / 桌面版通用)。安装后让独立 dsh 接入公司
|
|
4
|
-
AgentOne
|
|
4
|
+
AgentOne 平台:IDOne 登录、平台模型、SkillHub 技能、套餐申请、dsh 插件管理、
|
|
5
5
|
lark-cli / ccpg-cli 状态探测。
|
|
6
6
|
|
|
7
7
|
## 能力总览(M1–M4 已全部交付)
|
|
8
8
|
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
- **平台登录(M1)**:设备登录流(一次性授权码),入口按平台能力面自动
|
|
10
|
+
路由——平台开启邮箱锚点模式(IDOne 统一登录)时走 IDOne,否则回退
|
|
11
|
+
飞书;凭据保存在本机 `$DSH_HOME/agentone/credentials.json`(0600)。
|
|
12
|
+
access 令牌临期自动轮换(single-flight),refresh 失效自动回到未登录态。
|
|
12
13
|
- **平台模型(M2)**:登录后自动把 dsh 的 llm provider 指向 AgentOne
|
|
13
14
|
OpenAI 兼容代理(`/api/plugin/v1`),模型密钥不下发到本机;对话经平台
|
|
14
15
|
走 TokenHub 按用户计量。平台 403 的 `plan_required` 错误码原样透传,
|
|
15
16
|
前端据此引导申请套餐(不依赖中文文案匹配)。
|
|
16
|
-
- **SkillHub 技能(M3
|
|
17
|
-
|
|
17
|
+
- **SkillHub 技能(M3)**:套餐技能与管理员默认技能在启动/登录和常驻后台同步,
|
|
18
|
+
支持 `611/c611-ops-workbench`、`611--c611-ops-workbench` 与全局技能名;
|
|
18
19
|
技能市场搜索、手动安装/卸载,装/卸载后技能列表即时刷新(失效 45s
|
|
19
20
|
status 缓存);已装技能平台侧对比 latest 版本/指纹,有更新时展示
|
|
20
21
|
「v旧 → v新」并支持一键升级(覆盖式重装,原子替换旧目录);技能包
|
|
@@ -26,11 +27,17 @@ lark-cli / ccpg-cli 状态探测。
|
|
|
26
27
|
登出时全部清理。会话内使用:`source $DSH_HOME/agentone/skillhub.env`。
|
|
27
28
|
- **套餐申请(M3+)**:状态页「套餐」tab 展示当前套餐/审批中/可申请列表,
|
|
28
29
|
一键提交申请;审批通过后前端轮询检测到 active 自动同步模型。
|
|
29
|
-
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
- **平台默认能力(#479)**:管理员在「集成开关 → dsh 插件」单独保存默认
|
|
31
|
+
技能、npm 插件、用途说明和安装期构建授权。技能和插件始终安装最新发布版本,
|
|
32
|
+
受管内容有新版时自动升级;客户端启动、登录及约每 5 分钟后台检查。
|
|
33
|
+
本机状态由目录、版本和归属记录共同确认,用户级台账作为参考。
|
|
34
|
+
目录已有用户文件、账号/registry 不同时等待用户确认应用;停用只停止管理,
|
|
35
|
+
不删除本机内容。平台暂时不可用时缓存仅展示,不用于安装。
|
|
36
|
+
- **dsh 插件管理(M3+)**:列出当前 profile 的包、版本、bundle 注册状态;
|
|
37
|
+
安装成功需物理版本、插件声明和 profile 注册同时成立。注册不证明运行时加载,
|
|
38
|
+
页面提示重启生效。Web 必须配置实际 `profileName`,Desktop 使用宿主当前 profile
|
|
39
|
+
和受管 pnpm 通道;安装/卸载串行排队、支持生命周期取消。自身与 DSH 基础组件
|
|
40
|
+
不属于平台默认清单,自身只在独立安装模式允许手动升级。
|
|
34
41
|
- **CLI 探测、安装与授权(M4)**:`lark-cli` / `ccpg-cli` 安装与授权态三态探测;
|
|
35
42
|
未安装时「安装」一键自助安装(lark 走 `npm install -g @larksuite/cli@latest`,
|
|
36
43
|
ccpg 走公司 `auto-install.sh`,口径与平台 cli-deployd 一致);
|
|
@@ -46,7 +53,7 @@ lark-cli / ccpg-cli 状态探测。
|
|
|
46
53
|
每个会话基线加载,project 级指令优先);幂等不重复注入,用户已有语言
|
|
47
54
|
偏好或删除标记块后不再介入(本机只做一次决策,记录于
|
|
48
55
|
`$DSH_HOME/agentone/language-default.json`)。
|
|
49
|
-
- **首启引导**:dsh
|
|
56
|
+
- **首启引导**:dsh 首次启动弹出平台登录引导(order -50,早于官方
|
|
50
57
|
「添加 API Key」),登录一次即跳过官方密钥引导;暗色主题适配。
|
|
51
58
|
|
|
52
59
|
所有平台请求统一走 `lib/http.js`(默认 10s 超时、技能包下载 60s),
|
|
@@ -63,8 +70,9 @@ dsh plugin --profile web add dsh-agentone
|
|
|
63
70
|
|
|
64
71
|
## 使用
|
|
65
72
|
|
|
66
|
-
打开 dsh web 的 `/agentone/` 页面(或 dsh 设置 → AgentOne
|
|
67
|
-
|
|
73
|
+
打开 dsh web 的 `/agentone/` 页面(或 dsh 设置 → AgentOne)→「IDOne 登录」
|
|
74
|
+
(平台未开启统一登录时为「飞书登录」)→ 浏览器完成平台 OAuth → 自动跳回
|
|
75
|
+
并显示已登录账号。授权完成页会等待
|
|
68
76
|
模型配置就绪后自动关闭;配置失败时展示原因并提供「重试同步」。
|
|
69
77
|
|
|
70
78
|
登录成功后插件自动完成模型配置(也可点「重新同步模型」手动触发):
|
|
@@ -88,8 +96,8 @@ dsh plugin --profile web add dsh-agentone
|
|
|
88
96
|
|
|
89
97
|
```
|
|
90
98
|
插件页 ──POST /agentone/api/login──▶ 本插件后端
|
|
91
|
-
◀─ {login_url} ──
|
|
92
|
-
浏览器 ──login_url──▶
|
|
99
|
+
◀─ {login_url, provider} ──
|
|
100
|
+
浏览器 ──login_url──▶ 平台 OAuth(IDOne 优先,飞书兜底)──▶ /api/auth/plugin/landing
|
|
93
101
|
landing(JS, Bearer) ──POST /api/auth/plugin/grant──▶ {redirect_url(一次性 code)}
|
|
94
102
|
浏览器 ──redirect_url──▶ 本插件 /agentone/callback?code
|
|
95
103
|
插件后端 ──POST /api/auth/plugin/exchange──▶ {access_token}
|
|
@@ -105,6 +113,7 @@ dsh plugin --profile web add dsh-agentone
|
|
|
105
113
|
| --- | --- | --- |
|
|
106
114
|
| `platformUrl` | `https://ccpg.one.agentone.work` | AgentOne 平台地址(本地开发用 profile `cordis.patch.yml` 覆盖为 `http://127.0.0.1:8558`) |
|
|
107
115
|
| `storeDir` | `$DSH_HOME/agentone` | 凭据目录 |
|
|
116
|
+
| `profileName` | 空 | Web 模式填写实际启动 profile(例如 web 或自定义名);无法确定时暂停插件写入。Desktop 自动读取当前 profile |
|
|
108
117
|
|
|
109
118
|
密钥类值一律不进 settings,仅落 `credentials.json`(0600)。
|
|
110
119
|
|
package/lib/auth.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// AgentOne 设备登录流(一次性授权码)。
|
|
2
2
|
//
|
|
3
3
|
// 流程(与平台 app/api/plugin_auth.py 契约对应):
|
|
4
|
-
// 1. buildLoginUrl
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
4
|
+
// 1. buildLoginUrl:拼平台 OAuth 入口(IDOne 优先、飞书兜底,见
|
|
5
|
+
// pickLoginProvider),redirect 指向平台 landing 页并携带
|
|
6
|
+
// callback_url/state(经 OAuth state 往返透传);
|
|
7
|
+
// 2. 浏览器完成登录 → 平台回调页写 hermeshub_token → landing 页用平台
|
|
8
|
+
// 令牌换一次性 code → 跳转 callback_url(即本插件的 /agentone/callback
|
|
9
|
+
// 路由);
|
|
8
10
|
// 3. exchangeCode:code 换新签发的平台访问令牌。
|
|
9
11
|
import { randomBytes } from 'node:crypto';
|
|
10
12
|
|
|
@@ -14,10 +16,37 @@ export function newLoginState() {
|
|
|
14
16
|
return randomBytes(16).toString('hex');
|
|
15
17
|
}
|
|
16
18
|
|
|
17
|
-
export function buildLoginUrl(platformUrl, callbackUrl, state) {
|
|
19
|
+
export function buildLoginUrl(platformUrl, callbackUrl, state, provider = 'feishu') {
|
|
18
20
|
const landing = `/api/auth/plugin/landing?callback_url=${encodeURIComponent(callbackUrl)}&state=${encodeURIComponent(state)}`;
|
|
19
21
|
const redirect = encodeURIComponent(landing);
|
|
20
|
-
|
|
22
|
+
const entry = provider === 'idone' ? '/api/auth/casdoor/login' : '/api/auth/feishu/login';
|
|
23
|
+
return `${platformUrl.replace(/\/+$/, '')}${entry}?redirect=${redirect}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 匿名拉取平台能力面(登录入口开关)。登录前无令牌,走公开的
|
|
28
|
+
* /api/auth/capabilities(门户登录页同源依赖)。任何失败(超时/非 2xx/
|
|
29
|
+
* 解析错)返回 null,调用方按能力未知处理。
|
|
30
|
+
*/
|
|
31
|
+
export async function fetchAuthCapabilities(platformUrl) {
|
|
32
|
+
try {
|
|
33
|
+
const response = await platformFetch(`${platformUrl.replace(/\/+$/, '')}/api/auth/capabilities`, {}, 3000);
|
|
34
|
+
if (!response.ok) return null;
|
|
35
|
+
return await response.json();
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 按 capabilities 选登录入口:仅当 IDOne 已启用且邮箱锚点模式开启
|
|
43
|
+
* (平台统一登录语义,未绑定用户由后端按邮箱自动认人)才走 IDOne;
|
|
44
|
+
* 其余情况(未启用 / 非锚点 / 查询失败)一律回退飞书。
|
|
45
|
+
*/
|
|
46
|
+
export function pickLoginProvider(caps) {
|
|
47
|
+
return caps?.casdoor_oauth_enabled === true && caps?.casdoor_email_anchor_enabled === true
|
|
48
|
+
? 'idone'
|
|
49
|
+
: 'feishu';
|
|
21
50
|
}
|
|
22
51
|
|
|
23
52
|
export async function exchangeCode(platformUrl, code) {
|
package/lib/client.js
CHANGED
|
@@ -176,6 +176,8 @@ window.__ModuleLoader__.load({
|
|
|
176
176
|
const { complete } = props;
|
|
177
177
|
const [phase, setPhase] = React.useState('checking'); // checking | idle | waiting
|
|
178
178
|
const [error, setError] = React.useState('');
|
|
179
|
+
// 登录入口名(服务端按平台能力面路由:IDOne 优先、飞书兜底)
|
|
180
|
+
const [providerName, setProviderName] = React.useState('飞书');
|
|
179
181
|
|
|
180
182
|
React.useEffect(() => {
|
|
181
183
|
let cancelled = false;
|
|
@@ -183,8 +185,10 @@ window.__ModuleLoader__.load({
|
|
|
183
185
|
try {
|
|
184
186
|
const response = await fetch('/agentone/api/status');
|
|
185
187
|
const status = await response.json();
|
|
186
|
-
if (
|
|
187
|
-
|
|
188
|
+
if (cancelled) return;
|
|
189
|
+
if (status?.login_provider === 'idone') setProviderName('IDOne');
|
|
190
|
+
if (status?.logged_in) complete();
|
|
191
|
+
else setPhase('idle');
|
|
188
192
|
} catch {
|
|
189
193
|
if (!cancelled) setPhase('idle');
|
|
190
194
|
}
|
|
@@ -245,8 +249,8 @@ window.__ModuleLoader__.load({
|
|
|
245
249
|
'p',
|
|
246
250
|
{ style: bodyStyle },
|
|
247
251
|
phase === 'waiting'
|
|
248
|
-
?
|
|
249
|
-
:
|
|
252
|
+
? `已打开授权窗口,完成${providerName}授权后这里会自动继续…`
|
|
253
|
+
: `使用公司${providerName}账号登录,公司托管的模型与技能会自动配置,无需任何 API Key。`,
|
|
250
254
|
),
|
|
251
255
|
error
|
|
252
256
|
? React.createElement('p', { style: { ...bodyStyle, color: '#c0392b' } }, error)
|
|
@@ -261,7 +265,7 @@ window.__ModuleLoader__.load({
|
|
|
261
265
|
onClick: startLogin,
|
|
262
266
|
disabled: phase === 'waiting',
|
|
263
267
|
},
|
|
264
|
-
phase === 'waiting' ? '等待授权…' :
|
|
268
|
+
phase === 'waiting' ? '等待授权…' : `${providerName}登录`,
|
|
265
269
|
),
|
|
266
270
|
React.createElement(
|
|
267
271
|
'button',
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
// apply 范围内的默认能力协调器:后台检查、single-flight、身份隔离和可取消生命周期。
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { join, resolve } from 'node:path';
|
|
5
|
+
import { platformError, platformFetch } from './http.js';
|
|
6
|
+
import { fetchPlatformConfig, validatePlatformConfig } from './platform-config.js';
|
|
7
|
+
import { installPlatformSkill, listPlatformSkills } from './skill.js';
|
|
8
|
+
import { localSkillState } from './skill-state.js';
|
|
9
|
+
import { resolveDshHome } from './store.js';
|
|
10
|
+
|
|
11
|
+
export function createDefaultsSync({ config, profiles, credentials, log, intervalMs = 300_000,
|
|
12
|
+
readConfig = fetchPlatformConfig, readSkills = listPlatformSkills, installSkill = installPlatformSkill,
|
|
13
|
+
now = () => new Date().toISOString(), setTimer = setTimeout, clearTimer = clearTimeout }) {
|
|
14
|
+
let disposed = false;
|
|
15
|
+
let generation = 0;
|
|
16
|
+
let inFlight = null;
|
|
17
|
+
let flightGeneration = null;
|
|
18
|
+
let timer = null;
|
|
19
|
+
let abort = null;
|
|
20
|
+
const manualOperations = new Map();
|
|
21
|
+
let state = { source: null, revision: null, checked_at: null, user_id: null, error: null,
|
|
22
|
+
running: false, skills: [], plugins: [] };
|
|
23
|
+
const active = (value) => !disposed && generation === value;
|
|
24
|
+
function stop() {
|
|
25
|
+
generation += 1; abort?.abort(); abort = null;
|
|
26
|
+
for (const controller of manualOperations.values()) controller.abort();
|
|
27
|
+
if (timer !== null) clearTimer(timer);
|
|
28
|
+
timer = null;
|
|
29
|
+
}
|
|
30
|
+
function schedule() {
|
|
31
|
+
if (disposed || timer !== null) return;
|
|
32
|
+
timer = setTimer(() => { timer = null; void sync().catch(() => {}); }, intervalMs + Math.floor(Math.random() * intervalMs * 0.1));
|
|
33
|
+
timer?.unref?.();
|
|
34
|
+
}
|
|
35
|
+
async function context(controller) {
|
|
36
|
+
const current = await credentials();
|
|
37
|
+
if (!current?.access_token) throw new Error('请先登录 AgentOne 平台');
|
|
38
|
+
const platform = (current.platform_url || config.platformUrl).replace(/\/+$/, '');
|
|
39
|
+
// 实例凭据仅能访问 plugin API;旧后端缺少身份字段时暂停新同步。
|
|
40
|
+
const instance = current.source === 'instance';
|
|
41
|
+
const response = await platformFetch(`${platform}${instance ? '/api/plugin/v1/config' : '/api/me'}`, {
|
|
42
|
+
signal: controller.signal, headers: { Authorization: `Bearer ${current.access_token}` },
|
|
43
|
+
});
|
|
44
|
+
if (!response.ok) throw await platformError(response, '登录身份暂时无法确认,请重新登录');
|
|
45
|
+
const identity = await response.json();
|
|
46
|
+
const userId = instance ? identity.user_id : identity.id;
|
|
47
|
+
if (typeof userId !== 'string' || !userId) throw new Error('平台未返回有效身份,请检查平台地址');
|
|
48
|
+
const verifiedConfig = instance ? { document: validatePlatformConfig(identity, userId), source: 'platform' } : null;
|
|
49
|
+
let target = null;
|
|
50
|
+
let profileError = null;
|
|
51
|
+
try { target = profiles.target(); } catch (error) { profileError = error.message; }
|
|
52
|
+
const home = resolve(target?.home || config.dshHome || resolveDshHome());
|
|
53
|
+
return { platform, token: current.access_token, userId, verifiedConfig, home,
|
|
54
|
+
profile: target?.name || 'unknown', profileDir: target?.dir || null, profileError };
|
|
55
|
+
}
|
|
56
|
+
function cachePath(owner) {
|
|
57
|
+
const hash = createHash('sha256').update(JSON.stringify([owner.platform, owner.userId, owner.home, owner.profile])).digest('hex');
|
|
58
|
+
return join(owner.home, 'agentone', 'defaults-state', `${hash}.json`);
|
|
59
|
+
}
|
|
60
|
+
async function writeCache(owner, document, source) {
|
|
61
|
+
const file = cachePath(owner); await mkdir(join(file, '..'), { recursive: true, mode: 0o700 });
|
|
62
|
+
const temp = `${file}.${randomUUID()}.tmp`;
|
|
63
|
+
await writeFile(temp, JSON.stringify({ document, source, checked_at: state.checked_at }) + '\n', { mode: 0o600 });
|
|
64
|
+
await rename(temp, file);
|
|
65
|
+
}
|
|
66
|
+
async function readCache(owner) {
|
|
67
|
+
try {
|
|
68
|
+
const cached = JSON.parse(await readFile(cachePath(owner), 'utf8'));
|
|
69
|
+
return { ...cached, document: validatePlatformConfig(cached.document, owner.userId) };
|
|
70
|
+
} catch { return null; }
|
|
71
|
+
}
|
|
72
|
+
async function run(manual, selected) {
|
|
73
|
+
const currentGeneration = generation;
|
|
74
|
+
const controller = new AbortController(); abort = controller;
|
|
75
|
+
state = { ...state, running: true, error: null };
|
|
76
|
+
let owner;
|
|
77
|
+
try {
|
|
78
|
+
owner = await context(controller);
|
|
79
|
+
if (!active(currentGeneration)) return;
|
|
80
|
+
if (state.user_id !== owner.userId || state.platform !== owner.platform || state.profile !== owner.profile) {
|
|
81
|
+
profiles.setPolicy([]);
|
|
82
|
+
state = { source: null, revision: null, checked_at: null,
|
|
83
|
+
user_id: owner.userId, platform: owner.platform, profile: owner.profile,
|
|
84
|
+
error: null, running: true, skills: [], plugins: [] };
|
|
85
|
+
}
|
|
86
|
+
let result;
|
|
87
|
+
try { result = owner.verifiedConfig || await readConfig(owner.platform, owner.token, owner.userId, controller.signal); }
|
|
88
|
+
catch (error) {
|
|
89
|
+
if (error.status !== 401 && error.status !== 403) {
|
|
90
|
+
const cached = await readCache(owner);
|
|
91
|
+
if (cached && active(currentGeneration)) {
|
|
92
|
+
const local = owner.profileError ? { plugins: [] } : await profiles.list();
|
|
93
|
+
state = { ...state, source: 'cache', revision: cached.document.revision,
|
|
94
|
+
checked_at: cached.checked_at, user_id: owner.userId, home: owner.home,
|
|
95
|
+
registry: cached.document.skill_registry, auto_sync: cached.document.auto_sync,
|
|
96
|
+
plugins: cached.document.plugins.map((item) => ({ kind: 'plugin', name: item.name,
|
|
97
|
+
description: item.description, target_version: item.version,
|
|
98
|
+
version: local.plugins.find((entry) => entry.name === item.name)?.version || null,
|
|
99
|
+
state: 'pending', error: '平台暂时不可用,此项为缓存参考' })),
|
|
100
|
+
skills: await Promise.all(cached.document.skills.map(async (item) => {
|
|
101
|
+
const local = await localSkillState(owner.platform, item.canonical_slug, {
|
|
102
|
+
config: { ...config, dshHome: owner.home }, userId: owner.userId, registry: cached.document.skill_registry });
|
|
103
|
+
return { kind: 'skill', name: item.canonical_slug, description: item.description,
|
|
104
|
+
state: local.state, version: local.record?.version || null, sources: ['builtin'] };
|
|
105
|
+
})) };
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
profiles.setPolicy([]);
|
|
109
|
+
state = { source: null, revision: null, checked_at: null, user_id: null,
|
|
110
|
+
error: null, running: true, skills: [], plugins: [] };
|
|
111
|
+
}
|
|
112
|
+
// 缓存只展示;本轮失败不进行任何新安装。
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
if (!active(currentGeneration)) return;
|
|
116
|
+
const document = result.document;
|
|
117
|
+
profiles.setPolicy(document.plugins);
|
|
118
|
+
state = { ...state, source: result.source, revision: document.revision, user_id: owner.userId,
|
|
119
|
+
checked_at: now(), profile: owner.profile, home: owner.home, registry: document.skill_registry,
|
|
120
|
+
auto_sync: document.auto_sync, profile_error: owner.profileError, skills: [], plugins: [] };
|
|
121
|
+
await writeCache(owner, document, result.source);
|
|
122
|
+
const beforeCommit = async () => {
|
|
123
|
+
const identity = await context(controller);
|
|
124
|
+
if (!active(currentGeneration) || identity.userId !== owner.userId || identity.platform !== owner.platform
|
|
125
|
+
|| identity.home !== owner.home || identity.profile !== owner.profile) throw new Error('登录身份或当前 profile 已改变,请重新同步');
|
|
126
|
+
const fresh = identity.verifiedConfig || await readConfig(owner.platform, owner.token, owner.userId, controller.signal);
|
|
127
|
+
if (JSON.stringify(fresh.document) !== JSON.stringify(document)) throw new Error('平台清单已更新,请重新检查后应用');
|
|
128
|
+
};
|
|
129
|
+
const options = { config: { ...config, dshHome: owner.home }, userId: owner.userId,
|
|
130
|
+
registry: document.skill_registry, signal: controller.signal, beforeCommit,
|
|
131
|
+
isCurrent: () => active(currentGeneration), missingOnly: !(selected && selected.apply) };
|
|
132
|
+
const enabled = manual || document.auto_sync;
|
|
133
|
+
if (!owner.profileError) {
|
|
134
|
+
const local = await profiles.list();
|
|
135
|
+
for (const item of document.plugins) {
|
|
136
|
+
if (!active(currentGeneration)) return;
|
|
137
|
+
const selectedItem = !selected || (selected.kind === 'plugin' && selected.name === item.name);
|
|
138
|
+
const installed = local.plugins.find((entry) => entry.name === item.name);
|
|
139
|
+
let entry = { kind: 'plugin', name: item.name, description: item.description,
|
|
140
|
+
target_version: item.version, version: installed?.version || null, loaded: null,
|
|
141
|
+
state: installed?.registered ? (item.version !== 'latest' && installed.version !== item.version ? 'pending' : 'current') : 'pending' };
|
|
142
|
+
state.plugins.push(entry);
|
|
143
|
+
if (!enabled && !installed?.registered) continue;
|
|
144
|
+
try {
|
|
145
|
+
const exact = await profiles.resolveVersion?.(item.name, 'latest') || 'latest';
|
|
146
|
+
entry.target_version = exact;
|
|
147
|
+
if (installed?.registered && installed.version !== exact && (!enabled || !selectedItem)) {
|
|
148
|
+
entry.state = 'pending';
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (!enabled || !selectedItem) continue;
|
|
152
|
+
entry.state = 'running';
|
|
153
|
+
const after = await profiles.install(item.name, exact, { ...options, updateLatest: true, allowBuild: item.allow_build });
|
|
154
|
+
if (!active(currentGeneration)) return;
|
|
155
|
+
Object.assign(entry, { version: after.version, state: after.state, registered: after.registered, loaded: null });
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if (!active(currentGeneration)) return;
|
|
158
|
+
Object.assign(entry, { state: /本地链接/.test(error.message) ? 'conflict' : 'failed', error: error.message });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// 套餐语义独立于默认能力 auto_sync;sources 为空的纯台账项不补装。
|
|
163
|
+
const listing = await readSkills(owner.platform, owner.token, { signal: controller.signal });
|
|
164
|
+
if (!active(currentGeneration)) return;
|
|
165
|
+
if (listing.enabled === false || document.skills_enabled === false) return;
|
|
166
|
+
if (!Array.isArray(listing.skills)) throw new Error('平台技能清单格式异常');
|
|
167
|
+
for (const item of listing.skills) {
|
|
168
|
+
if (!active(currentGeneration)) return;
|
|
169
|
+
const sources = Array.isArray(item.sources) ? item.sources
|
|
170
|
+
: item.source === 'plan' ? ['plan'] : [];
|
|
171
|
+
if (!sources.includes('plan') && !sources.includes('builtin')) continue;
|
|
172
|
+
const selectedItem = !selected || (selected.kind === 'skill' && selected.name === item.canonical_slug);
|
|
173
|
+
let entry = { kind: 'skill', name: item.canonical_slug, sources,
|
|
174
|
+
description: document.skills.find((skill) => skill.canonical_slug === item.canonical_slug)?.description || '', state: 'pending' };
|
|
175
|
+
state.skills.push(entry);
|
|
176
|
+
try {
|
|
177
|
+
const local = await localSkillState(owner.platform, item.canonical_slug, options);
|
|
178
|
+
if (!active(currentGeneration)) return;
|
|
179
|
+
Object.assign(entry, { state: local.state, version: local.record?.version || null });
|
|
180
|
+
if (local.state === 'conflict' && (options.missingOnly || !selectedItem)) { entry.error = '本机技能已有用户内容或来源不一致,请确认后手动应用'; continue; }
|
|
181
|
+
if (local.state === 'current' && (!item.latest_version || !item.latest_fingerprint) && options.missingOnly) {
|
|
182
|
+
entry.state = 'failed'; entry.error = '暂时无法确认技能最新版本,请稍后重试'; continue;
|
|
183
|
+
}
|
|
184
|
+
if (local.record && ['current', 'report_pending'].includes(local.state) && (options.missingOnly || !selectedItem)
|
|
185
|
+
&& item.latest_version && item.latest_fingerprint
|
|
186
|
+
&& (local.record.fingerprint !== item.latest_fingerprint || local.record.version !== item.latest_version)) {
|
|
187
|
+
entry.target_version = item.latest_version;
|
|
188
|
+
if ((!enabled && !sources.includes('plan')) || !selectedItem) { entry.state = 'pending'; continue; }
|
|
189
|
+
}
|
|
190
|
+
if ((!enabled && !sources.includes('plan')) || !selectedItem) continue;
|
|
191
|
+
entry.state = 'running';
|
|
192
|
+
const installed = await installSkill(owner.platform, owner.token, item.canonical_slug,
|
|
193
|
+
{ ...options, updateLatest: true, latestVersion: item.latest_version, latestFingerprint: item.latest_fingerprint,
|
|
194
|
+
source: sources.includes('plan') ? 'plan' : 'builtin' });
|
|
195
|
+
if (!active(currentGeneration)) return;
|
|
196
|
+
Object.assign(entry, installed);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
if (!active(currentGeneration)) return;
|
|
199
|
+
Object.assign(entry, { state: 'failed', error: error.message });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
} catch (error) {
|
|
203
|
+
if (active(currentGeneration)) {
|
|
204
|
+
if (error.status === 401 || error.status === 403) {
|
|
205
|
+
profiles.setPolicy([]);
|
|
206
|
+
state = { source: null, revision: null, checked_at: null, user_id: null,
|
|
207
|
+
skills: [], plugins: [], running: true };
|
|
208
|
+
}
|
|
209
|
+
state.error = error.message; log?.warn(`[dsh-agentone] defaults sync: ${error.message}`);
|
|
210
|
+
}
|
|
211
|
+
} finally {
|
|
212
|
+
if (active(currentGeneration)) { state.running = false; abort = null; schedule(); }
|
|
213
|
+
}
|
|
214
|
+
return structuredClone(state);
|
|
215
|
+
}
|
|
216
|
+
function sync({ manual = false, item = null } = {}) {
|
|
217
|
+
if (disposed) return Promise.resolve(structuredClone(state));
|
|
218
|
+
if (inFlight) {
|
|
219
|
+
if (flightGeneration === generation && !item) return inFlight;
|
|
220
|
+
const queuedGeneration = generation;
|
|
221
|
+
return inFlight.then(() => active(queuedGeneration) ? sync({ manual, item }) : structuredClone(state));
|
|
222
|
+
}
|
|
223
|
+
const promise = run(manual, item);
|
|
224
|
+
flightGeneration = generation;
|
|
225
|
+
inFlight = promise;
|
|
226
|
+
promise.finally(() => { if (inFlight === promise) inFlight = null; }).catch(() => {});
|
|
227
|
+
return promise;
|
|
228
|
+
}
|
|
229
|
+
function manualOperation(operation) {
|
|
230
|
+
const controller = new AbortController();
|
|
231
|
+
const currentGeneration = generation;
|
|
232
|
+
const promise = (async () => {
|
|
233
|
+
const owner = await context(controller);
|
|
234
|
+
const result = owner.verifiedConfig || await readConfig(owner.platform, owner.token, owner.userId, controller.signal);
|
|
235
|
+
const beforeCommit = async () => {
|
|
236
|
+
const freshOwner = await context(controller);
|
|
237
|
+
if (!active(currentGeneration) || freshOwner.userId !== owner.userId || freshOwner.platform !== owner.platform
|
|
238
|
+
|| freshOwner.home !== owner.home || freshOwner.profile !== owner.profile) throw new Error('登录身份或当前 profile 已改变,请重新操作');
|
|
239
|
+
const fresh = freshOwner.verifiedConfig || await readConfig(owner.platform, freshOwner.token, freshOwner.userId, controller.signal);
|
|
240
|
+
if (JSON.stringify(fresh.document) !== JSON.stringify(result.document)) throw new Error('平台清单已更新,请重新检查后操作');
|
|
241
|
+
};
|
|
242
|
+
await beforeCommit();
|
|
243
|
+
profiles.setPolicy(result.document.plugins);
|
|
244
|
+
return operation(owner, { document: result.document, config: { ...config, dshHome: owner.home }, userId: owner.userId,
|
|
245
|
+
registry: result.document.skill_registry, signal: controller.signal, beforeCommit,
|
|
246
|
+
isCurrent: () => active(currentGeneration) });
|
|
247
|
+
})();
|
|
248
|
+
manualOperations.set(promise, controller);
|
|
249
|
+
promise.finally(() => manualOperations.delete(promise)).catch(() => {});
|
|
250
|
+
return promise;
|
|
251
|
+
}
|
|
252
|
+
return { sync, manualOperation, snapshot: () => structuredClone(state),
|
|
253
|
+
reset() { stop(); profiles.setPolicy([]); state = { source: null, revision: null,
|
|
254
|
+
checked_at: null, user_id: null, error: null, running: false, skills: [], plugins: [] }; },
|
|
255
|
+
dispose() { disposed = true; stop(); return Promise.allSettled([inFlight, ...manualOperations.keys()]); },
|
|
256
|
+
};
|
|
257
|
+
}
|
package/lib/http.js
CHANGED
|
@@ -20,7 +20,8 @@ export class PlatformError extends Error {
|
|
|
20
20
|
*/
|
|
21
21
|
export async function platformError(response, fallbackMessage) {
|
|
22
22
|
const data = await response.json().catch(() => ({}));
|
|
23
|
-
|
|
23
|
+
const message = data?.detail || data?.error?.message;
|
|
24
|
+
return new PlatformError(typeof message === 'string' && /[\u3400-\u9fff]/.test(message) ? message : fallbackMessage, {
|
|
24
25
|
status: response.status,
|
|
25
26
|
code: data?.error?.code || data?.code || null,
|
|
26
27
|
});
|
|
@@ -30,12 +31,14 @@ export async function platformError(response, fallbackMessage) {
|
|
|
30
31
|
export async function platformFetch(url, options = {}, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
31
32
|
let response;
|
|
32
33
|
try {
|
|
33
|
-
|
|
34
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
35
|
+
const signal = options.signal ? AbortSignal.any([options.signal, timeout]) : timeout;
|
|
36
|
+
response = await fetch(url, { ...options, signal });
|
|
34
37
|
} catch (error) {
|
|
35
38
|
if (error?.name === 'TimeoutError' || error?.name === 'AbortError') {
|
|
36
39
|
throw new PlatformError('连接 AgentOne 平台超时,请稍后重试');
|
|
37
40
|
}
|
|
38
|
-
throw
|
|
41
|
+
throw new PlatformError('无法连接 AgentOne 平台,请检查平台地址和网络后重试');
|
|
39
42
|
}
|
|
40
43
|
return response;
|
|
41
44
|
}
|