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/lib/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// dsh-agentone 主入口:挂 /agentone/* 路由(dsh webServer 原生 Node handler)。
|
|
2
2
|
//
|
|
3
|
-
// M1
|
|
3
|
+
// M1(登录,IDOne 优先、飞书兜底)页面与 API:
|
|
4
4
|
// GET /agentone/ 状态页(登录/登出/模型同步状态)
|
|
5
|
-
// GET /agentone/api/status {platform_url, logged_in, user_email, model}
|
|
6
|
-
// POST /agentone/api/login {callback_url} → {login_url}(浏览器打开)
|
|
5
|
+
// GET /agentone/api/status {platform_url, logged_in, user_email, model, login_provider}
|
|
6
|
+
// POST /agentone/api/login {callback_url} → {login_url, provider}(浏览器打开)
|
|
7
7
|
// GET /agentone/callback?code&state 平台 landing 跳回:换令牌后自动配置模型
|
|
8
8
|
// POST /agentone/api/logout 清除本地凭据并移除平台模型配置
|
|
9
9
|
// POST /agentone/api/model/sync 重新拉取平台模型并写入 settings.yaml
|
|
@@ -21,24 +21,29 @@ import {
|
|
|
21
21
|
applyPlatformPlan,
|
|
22
22
|
buildLoginUrl,
|
|
23
23
|
exchangeCode,
|
|
24
|
+
fetchAuthCapabilities,
|
|
24
25
|
fetchPlatformCapabilities,
|
|
25
26
|
fetchPlatformPlan,
|
|
26
27
|
fetchPlatformProfile,
|
|
27
28
|
newLoginState,
|
|
29
|
+
pickLoginProvider,
|
|
28
30
|
refreshPlatformToken,
|
|
29
31
|
revokePlatformToken,
|
|
30
32
|
} from './auth.js';
|
|
31
33
|
import { applyPlatformModel, removePlatformModel } from './model.js';
|
|
32
34
|
import { installPlatformSkill, listPlatformSkills, removePlatformSkill } from './skill.js';
|
|
33
35
|
import { applyPlatformSkillhub, removePlatformSkillhub } from './skillhub.js';
|
|
36
|
+
import { createProfilePlugins, validatePackage, PROTECTED_PACKAGES } from './profile-plugins.js';
|
|
37
|
+
import { createDefaultsSync } from './defaults-sync.js';
|
|
38
|
+
import { canonicalSkillSlug, localSkillState } from './skill-state.js';
|
|
34
39
|
import { ensureLanguageDefault } from './language.js';
|
|
35
40
|
import { bootstrapInstanceCredentials, INSTANCE_SOURCE, isInstanceManaged, resolvePlatformUrl } from './instance.js';
|
|
36
41
|
import { beginCliAuth, cliAuthQrcode, completeCliAuth, parseCliAuthBeginOutput } from './cliauth.mjs';
|
|
37
42
|
import { platformFetch } from './http.js';
|
|
38
43
|
import { donePageHtml, statusPageHtml } from './page.js';
|
|
39
|
-
import { compareSemver,
|
|
44
|
+
import { compareSemver, firstErrorLines, hasChineseText, installCliTool, isTerminalCliAuthFailure, isUnexpectedStoreError, larkCliInitActiveUrl, larkCliInitStatus, probeCliDetail, probePythonEnv, pnpmViewLatest, runCliTool, startLarkCliInit, stopLarkCliInit, unexpectedStoreGuidance, upgradeCliTool } from './proc.js';
|
|
40
45
|
|
|
41
|
-
import { clearCredentials, loadCredentials,
|
|
46
|
+
import { clearCredentials, loadCredentials, saveCredentials, updateCredentials } from './store.js';
|
|
42
47
|
|
|
43
48
|
const require = createRequire(import.meta.url);
|
|
44
49
|
|
|
@@ -56,11 +61,35 @@ export const Config = z.object({
|
|
|
56
61
|
.default('https://ccpg.one.agentone.work')
|
|
57
62
|
.description('AgentOne 平台地址(本地开发在 profile cordis.patch.yml 覆盖为 http://127.0.0.1:8558)'),
|
|
58
63
|
storeDir: z.string().default('').description('凭据存储目录,空则用 $DSH_HOME/agentone'),
|
|
64
|
+
profileName: z.string().default('').description('当前 Web profile 名称;Desktop 自动读取当前 profile,旧版 Web 宿主请明确填写'),
|
|
59
65
|
});
|
|
60
66
|
|
|
61
67
|
/** 本次进程内的登录 CSRF state(/callback 比对后即失效)。 */
|
|
62
68
|
const pendingStates = new Set();
|
|
63
69
|
|
|
70
|
+
// 登录入口判定缓存:key = platformUrl(避免切换平台后沿用旧判定)。
|
|
71
|
+
// 成功缓存 5 分钟(平台配置热重载后慢收敛可接受);查询失败只缓存 30 秒
|
|
72
|
+
// (避免平台瞬时抖动把入口钉死在飞书,也不让未登录状态轮询反复吃超时)。
|
|
73
|
+
const PROVIDER_CACHE_OK_MS = 5 * 60 * 1000;
|
|
74
|
+
const PROVIDER_CACHE_FAIL_MS = 30 * 1000;
|
|
75
|
+
const loginProviderCache = new Map();
|
|
76
|
+
|
|
77
|
+
/** 查平台能力面决定登录入口('idone' | 'feishu'),永抛错、永阻断登录。 */
|
|
78
|
+
async function resolveLoginProvider(platformUrl, log) {
|
|
79
|
+
const key = String(platformUrl || '').replace(/\/+$/, '');
|
|
80
|
+
const cached = loginProviderCache.get(key);
|
|
81
|
+
if (cached && Date.now() - cached.at < cached.ttl) return cached.provider;
|
|
82
|
+
const caps = await fetchAuthCapabilities(key);
|
|
83
|
+
const provider = pickLoginProvider(caps);
|
|
84
|
+
loginProviderCache.set(key, {
|
|
85
|
+
provider,
|
|
86
|
+
at: Date.now(),
|
|
87
|
+
ttl: caps ? PROVIDER_CACHE_OK_MS : PROVIDER_CACHE_FAIL_MS,
|
|
88
|
+
});
|
|
89
|
+
if (!caps) log?.warn?.(`[dsh-agentone] capabilities unavailable, login falls back to feishu: ${key}`);
|
|
90
|
+
return provider;
|
|
91
|
+
}
|
|
92
|
+
|
|
64
93
|
function sendJson(response, status, data) {
|
|
65
94
|
response.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
66
95
|
response.end(JSON.stringify(data));
|
|
@@ -189,13 +218,6 @@ async function ensureFreshToken(config, log) {
|
|
|
189
218
|
return tokenRefreshInFlight;
|
|
190
219
|
}
|
|
191
220
|
|
|
192
|
-
/**
|
|
193
|
-
* 插件管理目标 profile:普通 dsh 固定 web(控制台只跑 web profile);
|
|
194
|
-
* DSH Desktop 用 desktopProfiles.current(apply 时探测填充)。
|
|
195
|
-
*/
|
|
196
|
-
let PLUGIN_PROFILE = 'web';
|
|
197
|
-
/** desktop 受管插件操作通道(desktopPnpm.runPlugin 包装),非 desktop 为 null。 */
|
|
198
|
-
let desktopPluginCli = null;
|
|
199
221
|
/**
|
|
200
222
|
* dsh ≥ 0.1.7 的官方 settings 服务(apply 时可选获取):用来把平台模型写进
|
|
201
223
|
* profile patch 活配置层,同步即时生效。旧宿主没有该服务,取到 null 即回退
|
|
@@ -203,10 +225,6 @@ let desktopPluginCli = null;
|
|
|
203
225
|
*/
|
|
204
226
|
let settingsService = null;
|
|
205
227
|
|
|
206
|
-
function pluginProfileDir(config) {
|
|
207
|
-
return join(resolveDshHome(config), 'profiles', PLUGIN_PROFILE);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
228
|
/** 模型同步失败自愈重试调度(模块级单例:apply 重入先清旧定时器)。
|
|
211
229
|
* 平台侧瞬时故障(如 AirRouter 短暂不可达)会让启动同步失败,错误一直
|
|
212
230
|
* 挂在状态页直到下次 dsh 重启——这里由 5s 状态轮询驱动:发现 model_error
|
|
@@ -242,14 +260,6 @@ function scheduleModelRetry(config, log) {
|
|
|
242
260
|
modelRetryTimer.unref?.();
|
|
243
261
|
}
|
|
244
262
|
|
|
245
|
-
/** 转发 dsh plugin 子命令:desktop 走受管 pnpm,普通 dsh 走官方 CLI。 */
|
|
246
|
-
async function runPluginCli(args) {
|
|
247
|
-
if (desktopPluginCli) {
|
|
248
|
-
return desktopPluginCli(args);
|
|
249
|
-
}
|
|
250
|
-
return dshPluginCli(args, PLUGIN_PROFILE);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
263
|
/** 宿主 DesktopPnpmService 的并发锁占用报错(同一时刻只允许一个 pnpm
|
|
254
264
|
* 操作——设计如此,不是故障)。识别后应排队等待而非把错误抛给用户。 */
|
|
255
265
|
const DESKTOP_PNPM_BUSY_RE = /another desktop pnpm operation is already running/i;
|
|
@@ -275,13 +285,14 @@ const sleep = (ms) => new Promise((resolve) => { setTimeout(resolve, ms); });
|
|
|
275
285
|
*/
|
|
276
286
|
export function desktopRunPlugin(service, invokingDir) {
|
|
277
287
|
let queue = Promise.resolve();
|
|
278
|
-
const run = async (args) => {
|
|
288
|
+
const run = async (args, options = {}) => {
|
|
279
289
|
// 宿主 lock 是同步抛错(runPlugin 调用即抛),先探再跑 + 失败退避都做
|
|
280
290
|
let attempt = 0;
|
|
281
291
|
for (;;) {
|
|
292
|
+
options.signal?.throwIfAborted();
|
|
282
293
|
let handle;
|
|
283
294
|
try {
|
|
284
|
-
handle = service.runPlugin([...args], invokingDir);
|
|
295
|
+
handle = service.runPlugin([...args], invokingDir, options.signal);
|
|
285
296
|
} catch (error) {
|
|
286
297
|
const message = error?.message || String(error);
|
|
287
298
|
if (DESKTOP_PNPM_BUSY_RE.test(message) && attempt < 5) {
|
|
@@ -291,6 +302,9 @@ export function desktopRunPlugin(service, invokingDir) {
|
|
|
291
302
|
}
|
|
292
303
|
throw error;
|
|
293
304
|
}
|
|
305
|
+
const cancel = () => handle.cancel();
|
|
306
|
+
options.signal?.addEventListener('abort', cancel, { once: true });
|
|
307
|
+
if (options.signal?.aborted) cancel();
|
|
294
308
|
let stdout = '';
|
|
295
309
|
let stderr = '';
|
|
296
310
|
const append = (acc, chunk) => (acc + String(chunk)).slice(-64 * 1024);
|
|
@@ -304,6 +318,7 @@ export function desktopRunPlugin(service, invokingDir) {
|
|
|
304
318
|
}, 300_000);
|
|
305
319
|
try {
|
|
306
320
|
const outcome = await handle.done;
|
|
321
|
+
options.signal?.throwIfAborted();
|
|
307
322
|
if (timedOut) throw new Error('命令执行超时,请稍后重试');
|
|
308
323
|
if (outcome.exitCode !== 0 || outcome.signal != null) {
|
|
309
324
|
const message = firstErrorLines(`${stderr}\n${stdout}`);
|
|
@@ -325,35 +340,18 @@ export function desktopRunPlugin(service, invokingDir) {
|
|
|
325
340
|
return { stdout, stderr };
|
|
326
341
|
} finally {
|
|
327
342
|
clearTimeout(timer);
|
|
343
|
+
options.signal?.removeEventListener('abort', cancel);
|
|
328
344
|
}
|
|
329
345
|
}
|
|
330
346
|
};
|
|
331
|
-
return (args) => {
|
|
332
|
-
const next = queue.then(() => run(args));
|
|
347
|
+
return (args, options = {}) => {
|
|
348
|
+
const next = queue.then(() => run(args, options));
|
|
333
349
|
// 队列尾吞错,避免一次失败卡死后续排队操作
|
|
334
350
|
queue = next.catch(() => undefined);
|
|
335
351
|
return next;
|
|
336
352
|
};
|
|
337
353
|
}
|
|
338
354
|
|
|
339
|
-
/**
|
|
340
|
-
* npm 包名白名单(scoped 与非 scoped)。用户输入会进入 CLI 参数,必须先
|
|
341
|
-
* 收敛到 npm 官方命名规则,杜绝以 `-` 开头的值被 dsh/pnpm 当成 flag。
|
|
342
|
-
*/
|
|
343
|
-
const NPM_NAME_RE = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
|
|
344
|
-
/** 版本白名单:语义化版本、^ ~ 前缀与 latest 标签。 */
|
|
345
|
-
const VERSION_RE = /^(?:\^|~)?\d+(?:\.\d+){0,3}(?:[-+][a-z0-9._-]+)?$|^latest$/;
|
|
346
|
-
|
|
347
|
-
/** 校验用户输入的包名/版本;非法值直接拒绝(中文报错)。 */
|
|
348
|
-
function assertValidPackage(name, version) {
|
|
349
|
-
if (!NPM_NAME_RE.test(name)) {
|
|
350
|
-
throw new Error('插件名不合法:仅允许小写字母、数字和 . _ -(scoped 名如 @scope/pkg)');
|
|
351
|
-
}
|
|
352
|
-
if (version !== undefined && version !== '' && !VERSION_RE.test(version)) {
|
|
353
|
-
throw new Error('版本号不合法:仅允许语义化版本(如 1.2.3、^1.2.0)或 latest');
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
355
|
async function readJsonFile(path) {
|
|
358
356
|
try {
|
|
359
357
|
return JSON.parse(await readFile(path, 'utf8'));
|
|
@@ -368,19 +366,6 @@ async function installedVersion(profile, name) {
|
|
|
368
366
|
return typeof manifest?.version === 'string' ? manifest.version : null;
|
|
369
367
|
}
|
|
370
368
|
|
|
371
|
-
/** 包是否带 dsh.bundle 声明(决定它在层列表里是否被激活)。 */
|
|
372
|
-
async function isBundlePackage(profile, name) {
|
|
373
|
-
const manifest = await readJsonFile(join(profile, 'node_modules', name, 'package.json'));
|
|
374
|
-
return Boolean(manifest?.dsh?.bundle);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
/** link:/file: 等本地路径依赖展示短标签(没有版本号可读)。 */
|
|
378
|
-
function specLabel(spec) {
|
|
379
|
-
if (typeof spec !== 'string') return '';
|
|
380
|
-
if (spec.startsWith('link:') || spec.startsWith('file:')) return '本地链接';
|
|
381
|
-
return '';
|
|
382
|
-
}
|
|
383
|
-
|
|
384
369
|
/** dsh 插件与 CLI 操作白名单:用户输入不得进入 execFile 的 tool 位。 */
|
|
385
370
|
const CLI_TOOLS = new Set(['lark-cli', 'ccpg-cli']);
|
|
386
371
|
/** 除 CLI 外可通过 /agentone/api/cli/install 自助安装的工具(python 无
|
|
@@ -423,7 +408,7 @@ const INBOX_BUNDLES = new Set([
|
|
|
423
408
|
* 装入 /opt/prebuilt/dsh-home,init.sh 首启物化到持久卷)。用户可以卸载,
|
|
424
409
|
* 但它们承载控制台核心体验(侧栏宿主/Workflow One/插件市场/IM 桥),所以
|
|
425
410
|
* 每次 dsh 启动时检测:缺失则静默重装(常驻保证),已装则不碰(尊重用户
|
|
426
|
-
*
|
|
411
|
+
* 的版本选择)。安装走 profile-plugins 受管通道,与平台默认能力同一队列。
|
|
427
412
|
*/
|
|
428
413
|
const BUILTIN_PLUGINS = [
|
|
429
414
|
'dsh-better-sidebar',
|
|
@@ -451,206 +436,18 @@ const BUILTIN_PLUGIN_DESCRIPTIONS = {
|
|
|
451
436
|
'dsh-im':
|
|
452
437
|
'IM 桥核心:让 dsh 接入飞书等聊天工具——群内 @机器人 干活、收通知与审批卡片',
|
|
453
438
|
[PLUGIN_PACKAGE_NAME]:
|
|
454
|
-
'AgentOne
|
|
439
|
+
'AgentOne 平台集成:IDOne 登录、平台模型、SkillHub 技能与 CLI 授权(本插件)',
|
|
455
440
|
};
|
|
456
441
|
|
|
457
|
-
/**
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
const manifest = await readJsonFile(join(dir, 'package.json'));
|
|
461
|
-
const dependencies = manifest?.dependencies ?? {};
|
|
462
|
-
const bundles = Array.isArray(manifest?.dsh?.profile?.bundles)
|
|
463
|
-
? manifest.dsh.profile.bundles
|
|
464
|
-
: [];
|
|
465
|
-
const plugins = [];
|
|
466
|
-
const seen = new Set();
|
|
467
|
-
for (const [name, spec] of Object.entries(dependencies)) {
|
|
468
|
-
if (INBOX_BUNDLES.has(name)) continue;
|
|
469
|
-
seen.add(name);
|
|
470
|
-
const version = await installedVersion(dir, name);
|
|
471
|
-
const bundle = version !== null && (await isBundlePackage(dir, name));
|
|
472
|
-
plugins.push({
|
|
473
|
-
name,
|
|
474
|
-
spec,
|
|
475
|
-
version,
|
|
476
|
-
source: specLabel(spec) || 'npm',
|
|
477
|
-
bundled: bundle,
|
|
478
|
-
// 激活 = 已装 + 带 bundle 声明 + 出现在 dsh.profile.bundles
|
|
479
|
-
active: bundle && bundles.includes(name),
|
|
480
|
-
builtin: BUILTIN_PLUGINS.includes(name),
|
|
481
|
-
// 自身随平台镜像升级:前端据此隐藏卸载/升级按钮(与后端拒绝逻辑同源)
|
|
482
|
-
self_managed: name === PLUGIN_PACKAGE_NAME && isSelfManaged(),
|
|
483
|
-
description: BUILTIN_PLUGIN_DESCRIPTIONS[name] || null,
|
|
484
|
-
});
|
|
485
|
-
}
|
|
486
|
-
// 内置插件被卸载后不在 dependencies 里,但仍要出现在列表中
|
|
487
|
-
// (状态「未安装」+「安装」按钮),否则用户无从感知与恢复。
|
|
488
|
-
for (const name of BUILTIN_PLUGINS) {
|
|
489
|
-
if (seen.has(name)) continue;
|
|
490
|
-
plugins.push({
|
|
491
|
-
name,
|
|
492
|
-
spec: null,
|
|
493
|
-
version: null,
|
|
494
|
-
source: 'npm',
|
|
495
|
-
bundled: false,
|
|
496
|
-
active: false,
|
|
497
|
-
builtin: true,
|
|
498
|
-
self_managed: false,
|
|
499
|
-
description: BUILTIN_PLUGIN_DESCRIPTIONS[name] || null,
|
|
500
|
-
});
|
|
501
|
-
}
|
|
502
|
-
return {
|
|
503
|
-
profile: PLUGIN_PROFILE,
|
|
504
|
-
bundles: bundles.filter((name) => !INBOX_BUNDLES.has(name)),
|
|
505
|
-
plugins,
|
|
506
|
-
};
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
/** dsh plugin add 的构建脚本放行参数(与镜像 Dockerfile 预装命令同源)。
|
|
510
|
-
* pnpm 11 默认拦截依赖构建脚本,node-pty(sidebar/harness-one 的终端依赖)
|
|
511
|
-
* 被拦会导致 add 非零退出、bundles reconcile 不执行——包装上了却永远
|
|
512
|
-
* 「未激活」。用 = 连接形式,避免空格分隔被 pnpm 拆成两个参数。
|
|
513
|
-
*/
|
|
442
|
+
/** 内置插件安装期构建授权(与镜像 Dockerfile 预装命令同源):pnpm 默认
|
|
443
|
+
* 拦截依赖构建脚本,node-pty(侧栏/工作台的终端依赖)被拦会导致插件
|
|
444
|
+
* 装上却无法激活。平台默认能力清单未声明时按此兜底合并。 */
|
|
514
445
|
const BUILTIN_PLUGIN_ALLOW_BUILD = {
|
|
515
|
-
'dsh-better-sidebar': ['
|
|
516
|
-
'dsh-harness-one': ['
|
|
517
|
-
dshmarket: ['
|
|
518
|
-
'dsh-im': [],
|
|
519
|
-
[PLUGIN_PACKAGE_NAME]: [],
|
|
446
|
+
'dsh-better-sidebar': ['node-pty'],
|
|
447
|
+
'dsh-harness-one': ['node-pty'],
|
|
448
|
+
dshmarket: ['node-pty'],
|
|
520
449
|
};
|
|
521
450
|
|
|
522
|
-
/** 安装:白名单校验后转发(desktop 受管 pnpm / 官方 CLI;参数数组,无 shell)。
|
|
523
|
-
* pnpm 11 在 profile 存在被忽略的构建脚本时 add 以 ERR_PNPM_IGNORED_BUILDS
|
|
524
|
-
* 非零退出,但包本身已装上(先装后报)——以落盘为准,装上即成功。 */
|
|
525
|
-
async function installProfilePlugin(config, name, version) {
|
|
526
|
-
assertValidPackage(name, version);
|
|
527
|
-
const target = version ? `${name}@${version}` : name;
|
|
528
|
-
try {
|
|
529
|
-
await runPluginCli(['add', target, ...(BUILTIN_PLUGIN_ALLOW_BUILD[name] || [])]);
|
|
530
|
-
} catch (error) {
|
|
531
|
-
if ((await installedVersion(pluginProfileDir(config), name)) === null) throw error;
|
|
532
|
-
}
|
|
533
|
-
return listProfilePlugins(config);
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
/** 卸载:自身与内置插件拒绝(无卸载按钮 + 后端兜底);白名单校验后转发官方 CLI。 */
|
|
537
|
-
async function removeProfilePlugin(config, name) {
|
|
538
|
-
assertValidPackage(name);
|
|
539
|
-
if (INBOX_BUNDLES.has(name)) {
|
|
540
|
-
throw new Error('dsh 内置组件不可卸载');
|
|
541
|
-
}
|
|
542
|
-
if (BUILTIN_PLUGINS.includes(name)) {
|
|
543
|
-
throw new Error('内置插件承载 dsh 核心体验,不可卸载');
|
|
544
|
-
}
|
|
545
|
-
if (name === PLUGIN_PACKAGE_NAME) {
|
|
546
|
-
throw new Error('AgentOne 插件是登录与模型配置的载体,不能卸载自己');
|
|
547
|
-
}
|
|
548
|
-
await runPluginCli(['remove', name]);
|
|
549
|
-
return listProfilePlugins(config);
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
/** 升级到 registry 最新版:view 查实时 latest → 显式版本号 add(会重写
|
|
553
|
-
* package.json spec)→ 校验落盘版本。不能用 `dsh plugin update`(即
|
|
554
|
-
* `pnpm update`):它只在 spec range 内升级,`dsh plugin add` 写入的精确
|
|
555
|
-
* pin(如 "0.19.1")下永远是 no-op,却返回成功——历史上「点升级提示已
|
|
556
|
-
* 升级、版本永不变」的根因。 */
|
|
557
|
-
async function upgradeProfilePlugin(config, name) {
|
|
558
|
-
assertValidPackage(name);
|
|
559
|
-
if (INBOX_BUNDLES.has(name)) {
|
|
560
|
-
throw new Error('dsh 内置组件随 dsh 一同升级,请等待平台更新 dsh');
|
|
561
|
-
}
|
|
562
|
-
if (name === PLUGIN_PACKAGE_NAME && isSelfManaged()) {
|
|
563
|
-
throw new Error('AgentOne 插件随平台镜像一同升级,无需手动操作');
|
|
564
|
-
}
|
|
565
|
-
const dir = pluginProfileDir(config);
|
|
566
|
-
const [latest, current] = await Promise.all([
|
|
567
|
-
pnpmViewLatest(name, dir).catch(() => null),
|
|
568
|
-
installedVersion(dir, name),
|
|
569
|
-
]);
|
|
570
|
-
if (!latest) {
|
|
571
|
-
throw new Error('查询插件最新版本失败,请稍后重试');
|
|
572
|
-
}
|
|
573
|
-
// latest dist-tag 可能指向旧版(作者误指/回退),不比已装高就没必要升
|
|
574
|
-
if (latest === current || (current && compareSemver(latest, current) <= 0)) {
|
|
575
|
-
throw new Error(`已是最新版本(v${current ?? latest})`);
|
|
576
|
-
}
|
|
577
|
-
try {
|
|
578
|
-
await runPluginCli(['add', `${name}@${latest}`, ...(BUILTIN_PLUGIN_ALLOW_BUILD[name] || [])]);
|
|
579
|
-
} catch (error) {
|
|
580
|
-
// 同 install:ERR_PNPM_IGNORED_BUILDS 非零退出但包已装上,不阻断升级
|
|
581
|
-
if ((await installedVersion(dir, name)) !== latest) throw error;
|
|
582
|
-
}
|
|
583
|
-
const after = await installedVersion(dir, name);
|
|
584
|
-
if (after !== latest) {
|
|
585
|
-
// 兑现「插件已升级」提示的前置校验:没升上去就明说,不再假报成功
|
|
586
|
-
throw new Error(`升级未生效(当前 v${after ?? '未安装'},目标 v${latest}),请稍后重试`);
|
|
587
|
-
}
|
|
588
|
-
return listProfilePlugins(config);
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
/**
|
|
592
|
-
* 内置插件常驻检测:镜像预装插件缺失时逐个静默重装。单个失败不阻断
|
|
593
|
-
* (网络抖动等),下一个启动周期再试;全部已装时零开销。
|
|
594
|
-
*/
|
|
595
|
-
async function ensureBuiltinPlugins(config, log) {
|
|
596
|
-
const state = await listProfilePlugins(config);
|
|
597
|
-
const installed = new Set(state.plugins.map((plugin) => plugin.name));
|
|
598
|
-
const missing = BUILTIN_PLUGINS.filter((name) => !installed.has(name));
|
|
599
|
-
if (!missing.length) return [];
|
|
600
|
-
const reinstalled = [];
|
|
601
|
-
for (const name of missing) {
|
|
602
|
-
try {
|
|
603
|
-
await installProfilePlugin(config, name);
|
|
604
|
-
reinstalled.push(name);
|
|
605
|
-
log.info(`[dsh-agentone] builtin plugin reinstalled: ${name}`);
|
|
606
|
-
} catch (error) {
|
|
607
|
-
log.warn(`[dsh-agentone] builtin plugin reinstall failed (${name}): ${error?.message || error}`);
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
return reinstalled;
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
/**
|
|
614
|
-
* 套餐技能自动同步:平台上 source=plan 且未安装的技能静默装齐(与平台托管
|
|
615
|
-
* 实例的套餐技能预装语义对齐);单个失败不阻断其余,结果记 credentials。
|
|
616
|
-
*/
|
|
617
|
-
async function syncPlanSkills(config, log, credentials) {
|
|
618
|
-
const token = credentials?.access_token;
|
|
619
|
-
if (!token) return null;
|
|
620
|
-
let listing;
|
|
621
|
-
try {
|
|
622
|
-
listing = await listPlatformSkills(config.platformUrl, token);
|
|
623
|
-
} catch (error) {
|
|
624
|
-
log.warn(`[dsh-agentone] skills list failed: ${error?.message || error}`);
|
|
625
|
-
await updateCredentials(config, { skills_error: String(error?.message || error) }).catch(
|
|
626
|
-
() => {},
|
|
627
|
-
);
|
|
628
|
-
return null;
|
|
629
|
-
}
|
|
630
|
-
const pending = (listing.skills || []).filter((skill) => skill.source === 'plan' && !skill.installed);
|
|
631
|
-
let installed = 0;
|
|
632
|
-
const failures = [];
|
|
633
|
-
for (const skill of pending) {
|
|
634
|
-
try {
|
|
635
|
-
await installPlatformSkill(config.platformUrl, token, skill.canonical_slug, {
|
|
636
|
-
config,
|
|
637
|
-
source: 'plan',
|
|
638
|
-
});
|
|
639
|
-
installed += 1;
|
|
640
|
-
log.info(`[dsh-agentone] plan skill installed: ${skill.canonical_slug}`);
|
|
641
|
-
} catch (error) {
|
|
642
|
-
failures.push(`${skill.canonical_slug}: ${error?.message || error}`);
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
await updateCredentials(config, {
|
|
646
|
-
skills_synced_at: new Date().toISOString(),
|
|
647
|
-
skills_error: failures.length ? failures.join(';') : null,
|
|
648
|
-
}).catch(() => {});
|
|
649
|
-
if (failures.length) log.warn(`[dsh-agentone] plan skill failures: ${failures.join(';')}`);
|
|
650
|
-
return { installed, failed: failures.length };
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
|
|
654
451
|
/**
|
|
655
452
|
* status 内缓存:skills/catalog 内容几乎不变,却被 5s 轮询每次全量重拉。
|
|
656
453
|
* 缓存 45s;模型同步结果本就落盘在 credentials,不受缓存影响。
|
|
@@ -702,7 +499,7 @@ async function cachedPlatformStatus(config, credentials) {
|
|
|
702
499
|
* token + state 比对),收完第一个请求即关停;5 分钟无回调自动超时关闭。
|
|
703
500
|
* 返回外部浏览器可达的回调地址。
|
|
704
501
|
*/
|
|
705
|
-
function openCallbackServer(config, log) {
|
|
502
|
+
function openCallbackServer(config, log, onLogin = () => {}, isCurrent = () => true) {
|
|
706
503
|
return new Promise((resolve, reject) => {
|
|
707
504
|
const server = createServer(async (req, res) => {
|
|
708
505
|
const close = () => server.close();
|
|
@@ -733,6 +530,7 @@ button{font:inherit;padding:9px 22px;border-radius:10px;border:none;cursor:point
|
|
|
733
530
|
finish(false);
|
|
734
531
|
return;
|
|
735
532
|
}
|
|
533
|
+
if (!isCurrent()) { finish(false); return; }
|
|
736
534
|
const expiresIn = Number(result.expires_in) || 0;
|
|
737
535
|
await saveCredentials(config, {
|
|
738
536
|
schema_version: 2,
|
|
@@ -749,10 +547,8 @@ button{font:inherit;padding:9px 22px;border-radius:10px;border:none;cursor:point
|
|
|
749
547
|
...(await loadCredentials(config)),
|
|
750
548
|
access_token: result.access_token,
|
|
751
549
|
}).catch(() => {});
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
access_token: result.access_token,
|
|
755
|
-
}).catch(() => {});
|
|
550
|
+
if (!isCurrent()) { finish(false); return; }
|
|
551
|
+
onLogin();
|
|
756
552
|
finish(true);
|
|
757
553
|
} catch (error) {
|
|
758
554
|
log.warn(`[dsh-agentone] desktop exchange failed: ${error?.message || error}`);
|
|
@@ -796,14 +592,96 @@ export function apply(ctx, config) {
|
|
|
796
592
|
// patch 活配置层;取不到(旧宿主)就回退「只写 settings.yaml + 下次启动导入」。
|
|
797
593
|
// 不能写进 inject:那会让插件在旧宿主上一直 PENDING。
|
|
798
594
|
settingsService = ctx.get?.('settings') ?? null;
|
|
799
|
-
if (desktopProfiles?.current?.name) {
|
|
800
|
-
PLUGIN_PROFILE = desktopProfiles.current.name;
|
|
801
|
-
ctx.inject(['desktopPnpm'], (desktopCtx) => {
|
|
802
|
-
desktopPluginCli = desktopRunPlugin(desktopCtx.desktopPnpm, pluginProfileDir(config));
|
|
803
|
-
log.info(`[dsh-agentone] desktop profile: ${PLUGIN_PROFILE}`);
|
|
804
|
-
});
|
|
805
|
-
}
|
|
806
595
|
ctx.effect(() => {
|
|
596
|
+
let disposed = false;
|
|
597
|
+
const profiles = createProfilePlugins({ ctx, config, desktopRun: desktopRunPlugin });
|
|
598
|
+
const defaults = createDefaultsSync({ config, profiles,
|
|
599
|
+
credentials: () => ensureFreshToken(config, log), log });
|
|
600
|
+
const listProfilePlugins = async () => {
|
|
601
|
+
const state = await profiles.list();
|
|
602
|
+
const seen = new Set();
|
|
603
|
+
state.plugins.forEach((plugin) => {
|
|
604
|
+
seen.add(plugin.name);
|
|
605
|
+
plugin.self_managed = plugin.name === PLUGIN_PACKAGE_NAME && isSelfManaged();
|
|
606
|
+
plugin.builtin = plugin.builtin || BUILTIN_PLUGINS.includes(plugin.name);
|
|
607
|
+
plugin.description = plugin.description || BUILTIN_PLUGIN_DESCRIPTIONS[plugin.name] || null;
|
|
608
|
+
});
|
|
609
|
+
// 内置插件被卸载后不在 dependencies 里,仍以「未安装」占位出现在列表,
|
|
610
|
+
// 否则用户无从感知与恢复(常驻检测会在下次启动时自动重装)。
|
|
611
|
+
for (const name of BUILTIN_PLUGINS) {
|
|
612
|
+
if (seen.has(name)) continue;
|
|
613
|
+
state.plugins.push({ name, spec: null, version: null, bundled: false, registered: false,
|
|
614
|
+
active: false, source: 'npm', builtin: true,
|
|
615
|
+
self_managed: name === PLUGIN_PACKAGE_NAME && isSelfManaged(),
|
|
616
|
+
description: BUILTIN_PLUGIN_DESCRIPTIONS[name] || null, target_version: null,
|
|
617
|
+
state: 'missing', loaded: null });
|
|
618
|
+
}
|
|
619
|
+
return state;
|
|
620
|
+
};
|
|
621
|
+
const installProfilePlugin = async (_config, name, version = 'latest') => {
|
|
622
|
+
validatePackage(name, version);
|
|
623
|
+
// 内置插件(镜像控制台四件套/自身)是本机概念,不依赖平台登录:
|
|
624
|
+
// 直接走受管安装队列,构建授权用镜像预装命令同源的 node-pty 兜底。
|
|
625
|
+
if (BUILTIN_PLUGINS.includes(name)) {
|
|
626
|
+
await profiles.install(name, 'latest', { allowBuild: BUILTIN_PLUGIN_ALLOW_BUILD[name] || [] });
|
|
627
|
+
return listProfilePlugins();
|
|
628
|
+
}
|
|
629
|
+
await defaults.manualOperation(async (_owner, options) => {
|
|
630
|
+
const policy = options.document.plugins.find((item) => item.name === name);
|
|
631
|
+
// 平台清单声明的构建授权与内置插件已知的 node-pty 兜底合并
|
|
632
|
+
const allowBuild = [...new Set([...(policy?.allow_build || []), ...(BUILTIN_PLUGIN_ALLOW_BUILD[name] || [])])];
|
|
633
|
+
return profiles.install(name, 'latest', { ...options, allowBuild });
|
|
634
|
+
});
|
|
635
|
+
return listProfilePlugins();
|
|
636
|
+
};
|
|
637
|
+
const removeProfilePlugin = async (_config, name) => {
|
|
638
|
+
validatePackage(name);
|
|
639
|
+
if (PROTECTED_PACKAGES.has(name)) throw new Error('AgentOne 自身和 DSH 基础组件不可卸载');
|
|
640
|
+
if (BUILTIN_PLUGINS.includes(name)) throw new Error('内置插件承载 dsh 控制台核心体验,不可卸载');
|
|
641
|
+
return defaults.manualOperation((_owner, options) => profiles.remove(name, options));
|
|
642
|
+
};
|
|
643
|
+
const upgradeProfilePlugin = async (_config, name) => {
|
|
644
|
+
if (INBOX_BUNDLES.has(name) || (name === PLUGIN_PACKAGE_NAME && isSelfManaged())) {
|
|
645
|
+
throw new Error('此组件随宿主或平台更新,请等待平台更新');
|
|
646
|
+
}
|
|
647
|
+
await defaults.manualOperation(async (_owner, options) => {
|
|
648
|
+
const managed = options.document.plugins.find((item) => item.name === name);
|
|
649
|
+
if (managed) return profiles.install(name, managed.version, { ...options, allowBuild: managed.allow_build });
|
|
650
|
+
const latest = await pnpmViewLatest(name, profiles.target().dir).catch(() => null);
|
|
651
|
+
if (!latest) throw new Error('查询插件最新版本失败,请检查 npm 连接后重试');
|
|
652
|
+
const installed = (await profiles.list()).plugins.find((item) => item.name === name);
|
|
653
|
+
if (installed?.version && compareSemver(latest, installed.version) <= 0) {
|
|
654
|
+
throw new Error(`已是最新版本(v${installed.version})`);
|
|
655
|
+
}
|
|
656
|
+
return profiles.install(name, latest, options);
|
|
657
|
+
});
|
|
658
|
+
return listProfilePlugins();
|
|
659
|
+
};
|
|
660
|
+
const syncPlanSkills = async () => {
|
|
661
|
+
const result = await defaults.sync({ manual: true });
|
|
662
|
+
if (result?.error) throw new Error(result.error);
|
|
663
|
+
invalidateStatusCache();
|
|
664
|
+
return { installed: result?.skills.filter((item) => item.state === 'current').length || 0,
|
|
665
|
+
failed: result?.skills.filter((item) => ['failed', 'conflict', 'report_pending'].includes(item.state)).length || 0 };
|
|
666
|
+
};
|
|
667
|
+
const onLogin = () => {
|
|
668
|
+
defaults.reset();
|
|
669
|
+
void defaults.sync().then(invalidateStatusCache).catch(() => {});
|
|
670
|
+
};
|
|
671
|
+
// 内置插件常驻检测:缺失时静默重装(不依赖登录态);单个失败不阻断,
|
|
672
|
+
// 下个启动周期再试。已装不碰——版本升级交给平台默认能力同步。
|
|
673
|
+
const ensureBuiltinPlugins = async () => {
|
|
674
|
+
const state = await profiles.list();
|
|
675
|
+
const installed = new Set(state.plugins.filter((plugin) => plugin.version).map((plugin) => plugin.name));
|
|
676
|
+
for (const name of BUILTIN_PLUGINS.filter((entry) => !installed.has(entry))) {
|
|
677
|
+
try {
|
|
678
|
+
await profiles.install(name, 'latest', { allowBuild: BUILTIN_PLUGIN_ALLOW_BUILD[name] || [] });
|
|
679
|
+
log.info(`[dsh-agentone] builtin plugin reinstalled: ${name}`);
|
|
680
|
+
} catch (error) {
|
|
681
|
+
log.warn(`[dsh-agentone] builtin plugin reinstall failed (${name}): ${error?.message || error}`);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
};
|
|
807
685
|
// 模块级重试定时器跨 apply 存活:重入先清,避免旧定时器引用旧 config
|
|
808
686
|
clearModelRetry();
|
|
809
687
|
modelRetryAttempts = 0;
|
|
@@ -880,16 +758,32 @@ export function apply(ctx, config) {
|
|
|
880
758
|
modelRetryAttempts = 0;
|
|
881
759
|
}
|
|
882
760
|
}
|
|
761
|
+
if (skills?.skills && defaults.snapshot().user_id) {
|
|
762
|
+
const identity = defaults.snapshot();
|
|
763
|
+
skills = { ...skills, skills: await Promise.all(skills.skills.map(async (item) => {
|
|
764
|
+
const local = await localSkillState(config.platformUrl, item.canonical_slug, {
|
|
765
|
+
config: { ...config, dshHome: identity.home }, userId: identity.user_id, registry: identity.registry,
|
|
766
|
+
});
|
|
767
|
+
return { ...item, installed: ['current', 'report_pending'].includes(local.state),
|
|
768
|
+
local_state: local.state, installed_version: local.record?.version || '',
|
|
769
|
+
upgrade_available: local.state === 'current' && Boolean(item.latest_version && item.latest_fingerprint)
|
|
770
|
+
&& (local.record.version !== item.latest_version || local.record.fingerprint !== item.latest_fingerprint) };
|
|
771
|
+
})) };
|
|
772
|
+
}
|
|
773
|
+
// 登录入口判定:UI 登录卡文案需要;托管实例不显示登录入口,跳过查询
|
|
774
|
+
const loginProvider = isInstanceManaged() ? 'feishu' : await resolveLoginProvider(config.platformUrl, log);
|
|
883
775
|
sendJson(response, 200, {
|
|
884
776
|
platform_url: config.platformUrl,
|
|
885
777
|
logged_in: Boolean(userEmail),
|
|
886
778
|
user_email: userEmail,
|
|
779
|
+
login_provider: loginProvider,
|
|
887
780
|
// 托管实例:凭据由平台注入,页面据此显示「平台托管」并隐藏登录/登出
|
|
888
781
|
managed: isInstanceManaged(),
|
|
889
782
|
source: credentials?.source ?? null,
|
|
890
783
|
instance_id: credentials?.instance_id ?? null,
|
|
891
784
|
token_expires_at: credentials?.expires_at ?? null,
|
|
892
785
|
plugin_version: PLUGIN_VERSION,
|
|
786
|
+
defaults: defaults.snapshot(),
|
|
893
787
|
capabilities,
|
|
894
788
|
skills,
|
|
895
789
|
skill_display_names: displayNames,
|
|
@@ -932,7 +826,7 @@ export function apply(ctx, config) {
|
|
|
932
826
|
// /agentone/callback 会 403。desktop 环境(服务端自检 execPath 或页面
|
|
933
827
|
// 传 desktop 标记)改由插件在本进程起一次性本地服务收回调。
|
|
934
828
|
const callbackUrl = body.desktop || isDesktopRuntime
|
|
935
|
-
? await openCallbackServer(config, log)
|
|
829
|
+
? await openCallbackServer(config, log, onLogin, () => !disposed)
|
|
936
830
|
: String(body.callback_url || '');
|
|
937
831
|
if (!/^https?:\/\/|^\/portal\//.test(callbackUrl)) {
|
|
938
832
|
sendJson(response, 400, { error: '回调地址不合法' });
|
|
@@ -940,7 +834,12 @@ export function apply(ctx, config) {
|
|
|
940
834
|
}
|
|
941
835
|
const state = newLoginState();
|
|
942
836
|
pendingStates.add(state);
|
|
943
|
-
|
|
837
|
+
// 入口按平台能力面路由:IDOne(锚点模式)优先,飞书兜底
|
|
838
|
+
const provider = await resolveLoginProvider(config.platformUrl, log);
|
|
839
|
+
sendJson(response, 200, {
|
|
840
|
+
login_url: buildLoginUrl(config.platformUrl, callbackUrl, state, provider),
|
|
841
|
+
provider,
|
|
842
|
+
});
|
|
944
843
|
} catch (error) {
|
|
945
844
|
log.warn(`[dsh-agentone] login failed: ${error?.message || error}`);
|
|
946
845
|
sendJson(response, 500, { error: '发起登录失败' });
|
|
@@ -975,6 +874,7 @@ export function apply(ctx, config) {
|
|
|
975
874
|
redirect(false);
|
|
976
875
|
return;
|
|
977
876
|
}
|
|
877
|
+
if (disposed) { redirect(false); return; }
|
|
978
878
|
const expiresIn = Number(result.expires_in) || 0;
|
|
979
879
|
await saveCredentials(config, {
|
|
980
880
|
schema_version: 2,
|
|
@@ -991,10 +891,8 @@ export function apply(ctx, config) {
|
|
|
991
891
|
...(await loadCredentials(config)),
|
|
992
892
|
access_token: result.access_token,
|
|
993
893
|
}).catch(() => {});
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
access_token: result.access_token,
|
|
997
|
-
}).catch(() => {});
|
|
894
|
+
if (disposed) { redirect(false); return; }
|
|
895
|
+
onLogin();
|
|
998
896
|
redirect(true);
|
|
999
897
|
} catch (error) {
|
|
1000
898
|
log.warn(`[dsh-agentone] exchange failed: ${error?.message || error}`);
|
|
@@ -1002,6 +900,33 @@ export function apply(ctx, config) {
|
|
|
1002
900
|
}
|
|
1003
901
|
});
|
|
1004
902
|
|
|
903
|
+
register('/agentone/api/defaults/status', (request, response) => {
|
|
904
|
+
if (request.method !== 'GET') { sendJson(response, 405, { error: '请求方法不正确' }); return; }
|
|
905
|
+
sendJson(response, 200, defaults.snapshot());
|
|
906
|
+
});
|
|
907
|
+
for (const action of ['sync', 'retry', 'apply']) {
|
|
908
|
+
register(`/agentone/api/defaults/${action}`, async (request, response) => {
|
|
909
|
+
if (request.method !== 'POST') { sendJson(response, 405, { error: '请求方法不正确' }); return; }
|
|
910
|
+
try {
|
|
911
|
+
const body = await readJsonBody(request);
|
|
912
|
+
const credentials = await ensureFreshToken(config, log);
|
|
913
|
+
if (!credentials) { sendJson(response, 401, { error: '请先登录 AgentOne 平台' }); return; }
|
|
914
|
+
let item = null;
|
|
915
|
+
if (action !== 'sync') {
|
|
916
|
+
if (!['skill', 'plugin'].includes(body.kind) || typeof body.name !== 'string') {
|
|
917
|
+
sendJson(response, 400, { error: '请选择有效的技能或插件' }); return;
|
|
918
|
+
}
|
|
919
|
+
if (action === 'apply' && body.confirm !== true) {
|
|
920
|
+
sendJson(response, 400, { error: '应用差异将替换当前版本,请确认后继续' }); return;
|
|
921
|
+
}
|
|
922
|
+
item = { kind: body.kind, name: body.name, apply: action === 'apply' };
|
|
923
|
+
}
|
|
924
|
+
void defaults.sync({ manual: true, item }).then(invalidateStatusCache).catch(() => {});
|
|
925
|
+
sendJson(response, 202, { ok: true, running: true });
|
|
926
|
+
} catch (error) { sendJson(response, 400, { error: error?.message || '默认能力同步失败,请重试' }); }
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
|
|
1005
930
|
register('/agentone/api/model/sync', async (request, response) => {
|
|
1006
931
|
if (request.method !== 'POST') {
|
|
1007
932
|
response.writeHead(405, { allow: 'POST' });
|
|
@@ -1030,7 +955,7 @@ export function apply(ctx, config) {
|
|
|
1030
955
|
try {
|
|
1031
956
|
const credentials = await ensureFreshToken(config, log);
|
|
1032
957
|
if (!credentials) {
|
|
1033
|
-
sendJson(response, 401, { error: '
|
|
958
|
+
sendJson(response, 401, { error: '请先完成 AgentOne 登录' });
|
|
1034
959
|
return;
|
|
1035
960
|
}
|
|
1036
961
|
const result = await syncPlatformSkillhub(config, log, credentials);
|
|
@@ -1049,21 +974,16 @@ export function apply(ctx, config) {
|
|
|
1049
974
|
try {
|
|
1050
975
|
const credentials = await ensureFreshToken(config, log);
|
|
1051
976
|
if (!credentials) {
|
|
1052
|
-
sendJson(response, 401, { error: '
|
|
977
|
+
sendJson(response, 401, { error: '请先完成 AgentOne 登录' });
|
|
1053
978
|
return;
|
|
1054
979
|
}
|
|
1055
980
|
const body = await readJsonBody(request);
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
config.platformUrl,
|
|
1063
|
-
credentials.access_token,
|
|
1064
|
-
slug,
|
|
1065
|
-
{ config },
|
|
1066
|
-
);
|
|
981
|
+
let slug;
|
|
982
|
+
try { slug = canonicalSkillSlug(body.slug); }
|
|
983
|
+
catch (error) { sendJson(response, 400, { error: error.message }); return; }
|
|
984
|
+
const result = await defaults.manualOperation((owner, options) => installPlatformSkill(
|
|
985
|
+
owner.platform, owner.token, slug, options,
|
|
986
|
+
));
|
|
1067
987
|
invalidateStatusCache();
|
|
1068
988
|
sendJson(response, 200, { ok: true, ...result });
|
|
1069
989
|
} catch (error) {
|
|
@@ -1082,7 +1002,7 @@ export function apply(ctx, config) {
|
|
|
1082
1002
|
try {
|
|
1083
1003
|
const credentials = await ensureFreshToken(config, log);
|
|
1084
1004
|
if (!credentials) {
|
|
1085
|
-
sendJson(response, 401, { error: '
|
|
1005
|
+
sendJson(response, 401, { error: '请先完成 AgentOne 登录' });
|
|
1086
1006
|
return;
|
|
1087
1007
|
}
|
|
1088
1008
|
const result = await syncPlanSkills(config, log, credentials);
|
|
@@ -1102,7 +1022,7 @@ export function apply(ctx, config) {
|
|
|
1102
1022
|
try {
|
|
1103
1023
|
const credentials = await ensureFreshToken(config, log);
|
|
1104
1024
|
if (!credentials) {
|
|
1105
|
-
sendJson(response, 401, { error: '
|
|
1025
|
+
sendJson(response, 401, { error: '请先完成 AgentOne 登录' });
|
|
1106
1026
|
return;
|
|
1107
1027
|
}
|
|
1108
1028
|
const query = new URL(request.url, 'http://localhost').searchParams.get('q') || '';
|
|
@@ -1120,7 +1040,15 @@ export function apply(ctx, config) {
|
|
|
1120
1040
|
config.platformUrl,
|
|
1121
1041
|
credentials.access_token,
|
|
1122
1042
|
).catch(() => null);
|
|
1123
|
-
const
|
|
1043
|
+
const identity = defaults.snapshot();
|
|
1044
|
+
const installed = [];
|
|
1045
|
+
if (identity.user_id) {
|
|
1046
|
+
for (const skill of listing?.skills || []) {
|
|
1047
|
+
const local = await localSkillState(config.platformUrl, skill.canonical_slug, {
|
|
1048
|
+
config: { ...config, dshHome: identity.home }, userId: identity.user_id, registry: identity.registry });
|
|
1049
|
+
if (['current', 'report_pending'].includes(local.state)) installed.push({ ...skill, installed: true, installed_version: local.record.version });
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1124
1052
|
sendJson(response, 200, { ...data, installed });
|
|
1125
1053
|
} catch (error) {
|
|
1126
1054
|
sendJson(response, 502, { error: '技能市场暂时不可用,请稍后再试' });
|
|
@@ -1136,16 +1064,14 @@ export function apply(ctx, config) {
|
|
|
1136
1064
|
try {
|
|
1137
1065
|
const credentials = await loadCredentials(config);
|
|
1138
1066
|
if (!credentials) {
|
|
1139
|
-
sendJson(response, 401, { error: '
|
|
1067
|
+
sendJson(response, 401, { error: '请先完成 AgentOne 登录' });
|
|
1140
1068
|
return;
|
|
1141
1069
|
}
|
|
1142
1070
|
const body = await readJsonBody(request);
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
}
|
|
1148
|
-
await removePlatformSkill(config.platformUrl, credentials.access_token, slug, { config });
|
|
1071
|
+
let slug;
|
|
1072
|
+
try { slug = canonicalSkillSlug(body.slug); }
|
|
1073
|
+
catch (error) { sendJson(response, 400, { error: error.message }); return; }
|
|
1074
|
+
await defaults.manualOperation((owner, options) => removePlatformSkill(owner.platform, owner.token, slug, options));
|
|
1149
1075
|
invalidateStatusCache();
|
|
1150
1076
|
sendJson(response, 200, { ok: true, canonical_slug: slug });
|
|
1151
1077
|
} catch (error) {
|
|
@@ -1165,7 +1091,7 @@ export function apply(ctx, config) {
|
|
|
1165
1091
|
// 不用 pnpm outdated——它受元数据缓存与 minimumReleaseAge 冷却期
|
|
1166
1092
|
// 影响,新发布的版本一段时间内查不到,升级按钮追着发版延迟出现。
|
|
1167
1093
|
// 只查已装且来自 npm 的包;未安装/本地链接没有版本可查。
|
|
1168
|
-
const dir =
|
|
1094
|
+
const dir = profiles.target().dir;
|
|
1169
1095
|
await Promise.all(
|
|
1170
1096
|
state.plugins
|
|
1171
1097
|
.filter((item) => item.version !== null && item.source === 'npm')
|
|
@@ -1240,7 +1166,7 @@ export function apply(ctx, config) {
|
|
|
1240
1166
|
// 中版本。升级成功后落盘先变、运行代码要重启宿主才换——两者差值即
|
|
1241
1167
|
// 前端「已安装待重启」提示的依据。
|
|
1242
1168
|
try {
|
|
1243
|
-
const dir =
|
|
1169
|
+
const dir = profiles.target().dir;
|
|
1244
1170
|
const [version, latest] = await Promise.all([
|
|
1245
1171
|
installedVersion(dir, PLUGIN_PACKAGE_NAME),
|
|
1246
1172
|
pnpmViewLatest(PLUGIN_PACKAGE_NAME, dir).catch(() => null),
|
|
@@ -1583,7 +1509,7 @@ export function apply(ctx, config) {
|
|
|
1583
1509
|
try {
|
|
1584
1510
|
const credentials = await loadCredentials(config);
|
|
1585
1511
|
if (!credentials) {
|
|
1586
|
-
sendJson(response, 401, { error: '
|
|
1512
|
+
sendJson(response, 401, { error: '请先完成 AgentOne 登录' });
|
|
1587
1513
|
return;
|
|
1588
1514
|
}
|
|
1589
1515
|
const plan = await fetchPlatformPlan(config.platformUrl, credentials.access_token);
|
|
@@ -1602,7 +1528,7 @@ export function apply(ctx, config) {
|
|
|
1602
1528
|
try {
|
|
1603
1529
|
const credentials = await loadCredentials(config);
|
|
1604
1530
|
if (!credentials) {
|
|
1605
|
-
sendJson(response, 401, { error: '
|
|
1531
|
+
sendJson(response, 401, { error: '请先完成 AgentOne 登录' });
|
|
1606
1532
|
return;
|
|
1607
1533
|
}
|
|
1608
1534
|
const body = await readJsonBody(request);
|
|
@@ -1641,6 +1567,7 @@ export function apply(ctx, config) {
|
|
|
1641
1567
|
try {
|
|
1642
1568
|
// 先吊销平台侧 refresh token(尽力而为),再清本地;凭据读取失败
|
|
1643
1569
|
// (文件损坏等)也要走到清凭据,不能把用户卡在登录态。
|
|
1570
|
+
defaults.reset();
|
|
1644
1571
|
const credentials = await loadCredentials(config);
|
|
1645
1572
|
if (credentials?.refresh_token) {
|
|
1646
1573
|
await revokePlatformToken(config.platformUrl, credentials.refresh_token);
|
|
@@ -1660,9 +1587,9 @@ export function apply(ctx, config) {
|
|
|
1660
1587
|
}
|
|
1661
1588
|
});
|
|
1662
1589
|
|
|
1663
|
-
//
|
|
1664
|
-
//
|
|
1665
|
-
ensureBuiltinPlugins(
|
|
1590
|
+
// 默认能力属于已登录平台;无凭据不安装兼容默认。协调器独立于页面存活。
|
|
1591
|
+
// 内置插件常驻检测与登录无关,独立先行。
|
|
1592
|
+
ensureBuiltinPlugins().catch((error) => {
|
|
1666
1593
|
log.warn(`[dsh-agentone] builtin plugin check failed: ${error?.message || error}`);
|
|
1667
1594
|
});
|
|
1668
1595
|
ensureLanguageDefault(config, log).catch((error) => {
|
|
@@ -1674,17 +1601,20 @@ export function apply(ctx, config) {
|
|
|
1674
1601
|
log.warn(`[dsh-agentone] instance credential bootstrap failed: ${error?.message || error}`);
|
|
1675
1602
|
});
|
|
1676
1603
|
const credentials = await ensureFreshToken(config, log);
|
|
1677
|
-
if (!credentials) return;
|
|
1604
|
+
if (!credentials || disposed) return;
|
|
1605
|
+
void defaults.sync().then(invalidateStatusCache).catch(() => {});
|
|
1678
1606
|
await syncPlatformModel(config, log).catch(() => {});
|
|
1679
1607
|
// SkillHub 连接在托管容器里由 Hub 直接注入 env(CLAWHUB_* / SKILLHUB_*),
|
|
1680
1608
|
// 插件不再重复写 skillhub.env / clawhub login,避免两个写入方互相盖。
|
|
1681
1609
|
if (!isInstanceManaged()) {
|
|
1682
1610
|
await syncPlatformSkillhub(config, log, credentials).catch(() => {});
|
|
1683
1611
|
}
|
|
1684
|
-
await syncPlanSkills(config, log, credentials).catch(() => {});
|
|
1685
1612
|
})();
|
|
1686
1613
|
|
|
1687
|
-
return () => {
|
|
1614
|
+
return async () => {
|
|
1615
|
+
disposed = true;
|
|
1616
|
+
await defaults.dispose();
|
|
1617
|
+
await profiles.dispose();
|
|
1688
1618
|
pendingStates.clear();
|
|
1689
1619
|
pendingCliAuth.clear();
|
|
1690
1620
|
clearModelRetry();
|