dsh-agentone 0.5.38 → 0.5.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -33,14 +33,17 @@ import {
33
33
  import { applyPlatformModel, removePlatformModel } from './model.js';
34
34
  import { installPlatformSkill, listPlatformSkills, removePlatformSkill } from './skill.js';
35
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';
36
39
  import { ensureLanguageDefault } from './language.js';
37
40
  import { bootstrapInstanceCredentials, INSTANCE_SOURCE, isInstanceManaged, resolvePlatformUrl } from './instance.js';
38
41
  import { beginCliAuth, cliAuthQrcode, completeCliAuth, parseCliAuthBeginOutput } from './cliauth.mjs';
39
42
  import { platformFetch } from './http.js';
40
43
  import { donePageHtml, statusPageHtml } from './page.js';
41
- import { compareSemver, dshPluginCli, firstErrorLines, hasChineseText, installCliTool, isTerminalCliAuthFailure, isUnexpectedStoreError, larkCliInitActiveUrl, larkCliInitStatus, probeCliDetail, probePythonEnv, pnpmViewLatest, runCliTool, startLarkCliInit, stopLarkCliInit, unexpectedStoreGuidance, upgradeCliTool } from './proc.js';
44
+ import { compareSemver, firstErrorLines, hasChineseText, installCliTool, isTerminalCliAuthFailure, isUnexpectedStoreError, larkCliInitActiveUrl, larkCliInitStatus, probeCliDetail, probePythonEnv, pnpmViewLatest, runCliTool, startLarkCliInit, stopLarkCliInit, unexpectedStoreGuidance, upgradeCliTool } from './proc.js';
42
45
 
43
- import { clearCredentials, loadCredentials, resolveDshHome, saveCredentials, updateCredentials } from './store.js';
46
+ import { clearCredentials, loadCredentials, saveCredentials, updateCredentials } from './store.js';
44
47
 
45
48
  const require = createRequire(import.meta.url);
46
49
 
@@ -58,6 +61,7 @@ export const Config = z.object({
58
61
  .default('https://ccpg.one.agentone.work')
59
62
  .description('AgentOne 平台地址(本地开发在 profile cordis.patch.yml 覆盖为 http://127.0.0.1:8558)'),
60
63
  storeDir: z.string().default('').description('凭据存储目录,空则用 $DSH_HOME/agentone'),
64
+ profileName: z.string().default('').description('当前 Web profile 名称;Desktop 自动读取当前 profile,旧版 Web 宿主请明确填写'),
61
65
  });
62
66
 
63
67
  /** 本次进程内的登录 CSRF state(/callback 比对后即失效)。 */
@@ -214,13 +218,6 @@ async function ensureFreshToken(config, log) {
214
218
  return tokenRefreshInFlight;
215
219
  }
216
220
 
217
- /**
218
- * 插件管理目标 profile:普通 dsh 固定 web(控制台只跑 web profile);
219
- * DSH Desktop 用 desktopProfiles.current(apply 时探测填充)。
220
- */
221
- let PLUGIN_PROFILE = 'web';
222
- /** desktop 受管插件操作通道(desktopPnpm.runPlugin 包装),非 desktop 为 null。 */
223
- let desktopPluginCli = null;
224
221
  /**
225
222
  * dsh ≥ 0.1.7 的官方 settings 服务(apply 时可选获取):用来把平台模型写进
226
223
  * profile patch 活配置层,同步即时生效。旧宿主没有该服务,取到 null 即回退
@@ -228,10 +225,6 @@ let desktopPluginCli = null;
228
225
  */
229
226
  let settingsService = null;
230
227
 
231
- function pluginProfileDir(config) {
232
- return join(resolveDshHome(config), 'profiles', PLUGIN_PROFILE);
233
- }
234
-
235
228
  /** 模型同步失败自愈重试调度(模块级单例:apply 重入先清旧定时器)。
236
229
  * 平台侧瞬时故障(如 AirRouter 短暂不可达)会让启动同步失败,错误一直
237
230
  * 挂在状态页直到下次 dsh 重启——这里由 5s 状态轮询驱动:发现 model_error
@@ -267,14 +260,6 @@ function scheduleModelRetry(config, log) {
267
260
  modelRetryTimer.unref?.();
268
261
  }
269
262
 
270
- /** 转发 dsh plugin 子命令:desktop 走受管 pnpm,普通 dsh 走官方 CLI。 */
271
- async function runPluginCli(args) {
272
- if (desktopPluginCli) {
273
- return desktopPluginCli(args);
274
- }
275
- return dshPluginCli(args, PLUGIN_PROFILE);
276
- }
277
-
278
263
  /** 宿主 DesktopPnpmService 的并发锁占用报错(同一时刻只允许一个 pnpm
279
264
  * 操作——设计如此,不是故障)。识别后应排队等待而非把错误抛给用户。 */
280
265
  const DESKTOP_PNPM_BUSY_RE = /another desktop pnpm operation is already running/i;
@@ -300,13 +285,14 @@ const sleep = (ms) => new Promise((resolve) => { setTimeout(resolve, ms); });
300
285
  */
301
286
  export function desktopRunPlugin(service, invokingDir) {
302
287
  let queue = Promise.resolve();
303
- const run = async (args) => {
288
+ const run = async (args, options = {}) => {
304
289
  // 宿主 lock 是同步抛错(runPlugin 调用即抛),先探再跑 + 失败退避都做
305
290
  let attempt = 0;
306
291
  for (;;) {
292
+ options.signal?.throwIfAborted();
307
293
  let handle;
308
294
  try {
309
- handle = service.runPlugin([...args], invokingDir);
295
+ handle = service.runPlugin([...args], invokingDir, options.signal);
310
296
  } catch (error) {
311
297
  const message = error?.message || String(error);
312
298
  if (DESKTOP_PNPM_BUSY_RE.test(message) && attempt < 5) {
@@ -316,6 +302,9 @@ export function desktopRunPlugin(service, invokingDir) {
316
302
  }
317
303
  throw error;
318
304
  }
305
+ const cancel = () => handle.cancel();
306
+ options.signal?.addEventListener('abort', cancel, { once: true });
307
+ if (options.signal?.aborted) cancel();
319
308
  let stdout = '';
320
309
  let stderr = '';
321
310
  const append = (acc, chunk) => (acc + String(chunk)).slice(-64 * 1024);
@@ -329,6 +318,7 @@ export function desktopRunPlugin(service, invokingDir) {
329
318
  }, 300_000);
330
319
  try {
331
320
  const outcome = await handle.done;
321
+ options.signal?.throwIfAborted();
332
322
  if (timedOut) throw new Error('命令执行超时,请稍后重试');
333
323
  if (outcome.exitCode !== 0 || outcome.signal != null) {
334
324
  const message = firstErrorLines(`${stderr}\n${stdout}`);
@@ -350,35 +340,18 @@ export function desktopRunPlugin(service, invokingDir) {
350
340
  return { stdout, stderr };
351
341
  } finally {
352
342
  clearTimeout(timer);
343
+ options.signal?.removeEventListener('abort', cancel);
353
344
  }
354
345
  }
355
346
  };
356
- return (args) => {
357
- const next = queue.then(() => run(args));
347
+ return (args, options = {}) => {
348
+ const next = queue.then(() => run(args, options));
358
349
  // 队列尾吞错,避免一次失败卡死后续排队操作
359
350
  queue = next.catch(() => undefined);
360
351
  return next;
361
352
  };
362
353
  }
363
354
 
364
- /**
365
- * npm 包名白名单(scoped 与非 scoped)。用户输入会进入 CLI 参数,必须先
366
- * 收敛到 npm 官方命名规则,杜绝以 `-` 开头的值被 dsh/pnpm 当成 flag。
367
- */
368
- const NPM_NAME_RE = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
369
- /** 版本白名单:语义化版本、^ ~ 前缀与 latest 标签。 */
370
- const VERSION_RE = /^(?:\^|~)?\d+(?:\.\d+){0,3}(?:[-+][a-z0-9._-]+)?$|^latest$/;
371
-
372
- /** 校验用户输入的包名/版本;非法值直接拒绝(中文报错)。 */
373
- function assertValidPackage(name, version) {
374
- if (!NPM_NAME_RE.test(name)) {
375
- throw new Error('插件名不合法:仅允许小写字母、数字和 . _ -(scoped 名如 @scope/pkg)');
376
- }
377
- if (version !== undefined && version !== '' && !VERSION_RE.test(version)) {
378
- throw new Error('版本号不合法:仅允许语义化版本(如 1.2.3、^1.2.0)或 latest');
379
- }
380
- }
381
-
382
355
  async function readJsonFile(path) {
383
356
  try {
384
357
  return JSON.parse(await readFile(path, 'utf8'));
@@ -393,19 +366,6 @@ async function installedVersion(profile, name) {
393
366
  return typeof manifest?.version === 'string' ? manifest.version : null;
394
367
  }
395
368
 
396
- /** 包是否带 dsh.bundle 声明(决定它在层列表里是否被激活)。 */
397
- async function isBundlePackage(profile, name) {
398
- const manifest = await readJsonFile(join(profile, 'node_modules', name, 'package.json'));
399
- return Boolean(manifest?.dsh?.bundle);
400
- }
401
-
402
- /** link:/file: 等本地路径依赖展示短标签(没有版本号可读)。 */
403
- function specLabel(spec) {
404
- if (typeof spec !== 'string') return '';
405
- if (spec.startsWith('link:') || spec.startsWith('file:')) return '本地链接';
406
- return '';
407
- }
408
-
409
369
  /** dsh 插件与 CLI 操作白名单:用户输入不得进入 execFile 的 tool 位。 */
410
370
  const CLI_TOOLS = new Set(['lark-cli', 'ccpg-cli']);
411
371
  /** 除 CLI 外可通过 /agentone/api/cli/install 自助安装的工具(python 无
@@ -448,7 +408,7 @@ const INBOX_BUNDLES = new Set([
448
408
  * 装入 /opt/prebuilt/dsh-home,init.sh 首启物化到持久卷)。用户可以卸载,
449
409
  * 但它们承载控制台核心体验(侧栏宿主/Workflow One/插件市场/IM 桥),所以
450
410
  * 每次 dsh 启动时检测:缺失则静默重装(常驻保证),已装则不碰(尊重用户
451
- * 的版本选择)。
411
+ * 的版本选择)。安装走 profile-plugins 受管通道,与平台默认能力同一队列。
452
412
  */
453
413
  const BUILTIN_PLUGINS = [
454
414
  'dsh-better-sidebar',
@@ -479,203 +439,15 @@ const BUILTIN_PLUGIN_DESCRIPTIONS = {
479
439
  'AgentOne 平台集成:IDOne 登录、平台模型、SkillHub 技能与 CLI 授权(本插件)',
480
440
  };
481
441
 
482
- /** 汇总 profile 插件状态:dependencies(去掉 in-box)+ 已装版本 + 激活态。 */
483
- async function listProfilePlugins(config) {
484
- const dir = pluginProfileDir(config);
485
- const manifest = await readJsonFile(join(dir, 'package.json'));
486
- const dependencies = manifest?.dependencies ?? {};
487
- const bundles = Array.isArray(manifest?.dsh?.profile?.bundles)
488
- ? manifest.dsh.profile.bundles
489
- : [];
490
- const plugins = [];
491
- const seen = new Set();
492
- for (const [name, spec] of Object.entries(dependencies)) {
493
- if (INBOX_BUNDLES.has(name)) continue;
494
- seen.add(name);
495
- const version = await installedVersion(dir, name);
496
- const bundle = version !== null && (await isBundlePackage(dir, name));
497
- plugins.push({
498
- name,
499
- spec,
500
- version,
501
- source: specLabel(spec) || 'npm',
502
- bundled: bundle,
503
- // 激活 = 已装 + 带 bundle 声明 + 出现在 dsh.profile.bundles
504
- active: bundle && bundles.includes(name),
505
- builtin: BUILTIN_PLUGINS.includes(name),
506
- // 自身随平台镜像升级:前端据此隐藏卸载/升级按钮(与后端拒绝逻辑同源)
507
- self_managed: name === PLUGIN_PACKAGE_NAME && isSelfManaged(),
508
- description: BUILTIN_PLUGIN_DESCRIPTIONS[name] || null,
509
- });
510
- }
511
- // 内置插件被卸载后不在 dependencies 里,但仍要出现在列表中
512
- // (状态「未安装」+「安装」按钮),否则用户无从感知与恢复。
513
- for (const name of BUILTIN_PLUGINS) {
514
- if (seen.has(name)) continue;
515
- plugins.push({
516
- name,
517
- spec: null,
518
- version: null,
519
- source: 'npm',
520
- bundled: false,
521
- active: false,
522
- builtin: true,
523
- self_managed: false,
524
- description: BUILTIN_PLUGIN_DESCRIPTIONS[name] || null,
525
- });
526
- }
527
- return {
528
- profile: PLUGIN_PROFILE,
529
- bundles: bundles.filter((name) => !INBOX_BUNDLES.has(name)),
530
- plugins,
531
- };
532
- }
533
-
534
- /** dsh plugin add 的构建脚本放行参数(与镜像 Dockerfile 预装命令同源)。
535
- * pnpm 11 默认拦截依赖构建脚本,node-pty(sidebar/harness-one 的终端依赖)
536
- * 被拦会导致 add 非零退出、bundles reconcile 不执行——包装上了却永远
537
- * 「未激活」。用 = 连接形式,避免空格分隔被 pnpm 拆成两个参数。
538
- */
442
+ /** 内置插件安装期构建授权(与镜像 Dockerfile 预装命令同源):pnpm 默认
443
+ * 拦截依赖构建脚本,node-pty(侧栏/工作台的终端依赖)被拦会导致插件
444
+ * 装上却无法激活。平台默认能力清单未声明时按此兜底合并。 */
539
445
  const BUILTIN_PLUGIN_ALLOW_BUILD = {
540
- 'dsh-better-sidebar': ['--allow-build=node-pty'],
541
- 'dsh-harness-one': ['--allow-build=node-pty'],
542
- dshmarket: ['--allow-build=node-pty'],
543
- 'dsh-im': [],
544
- [PLUGIN_PACKAGE_NAME]: [],
446
+ 'dsh-better-sidebar': ['node-pty'],
447
+ 'dsh-harness-one': ['node-pty'],
448
+ dshmarket: ['node-pty'],
545
449
  };
546
450
 
547
- /** 安装:白名单校验后转发(desktop 受管 pnpm / 官方 CLI;参数数组,无 shell)。
548
- * pnpm 11 在 profile 存在被忽略的构建脚本时 add 以 ERR_PNPM_IGNORED_BUILDS
549
- * 非零退出,但包本身已装上(先装后报)——以落盘为准,装上即成功。 */
550
- async function installProfilePlugin(config, name, version) {
551
- assertValidPackage(name, version);
552
- const target = version ? `${name}@${version}` : name;
553
- try {
554
- await runPluginCli(['add', target, ...(BUILTIN_PLUGIN_ALLOW_BUILD[name] || [])]);
555
- } catch (error) {
556
- if ((await installedVersion(pluginProfileDir(config), name)) === null) throw error;
557
- }
558
- return listProfilePlugins(config);
559
- }
560
-
561
- /** 卸载:自身与内置插件拒绝(无卸载按钮 + 后端兜底);白名单校验后转发官方 CLI。 */
562
- async function removeProfilePlugin(config, name) {
563
- assertValidPackage(name);
564
- if (INBOX_BUNDLES.has(name)) {
565
- throw new Error('dsh 内置组件不可卸载');
566
- }
567
- if (BUILTIN_PLUGINS.includes(name)) {
568
- throw new Error('内置插件承载 dsh 核心体验,不可卸载');
569
- }
570
- if (name === PLUGIN_PACKAGE_NAME) {
571
- throw new Error('AgentOne 插件是登录与模型配置的载体,不能卸载自己');
572
- }
573
- await runPluginCli(['remove', name]);
574
- return listProfilePlugins(config);
575
- }
576
-
577
- /** 升级到 registry 最新版:view 查实时 latest → 显式版本号 add(会重写
578
- * package.json spec)→ 校验落盘版本。不能用 `dsh plugin update`(即
579
- * `pnpm update`):它只在 spec range 内升级,`dsh plugin add` 写入的精确
580
- * pin(如 "0.19.1")下永远是 no-op,却返回成功——历史上「点升级提示已
581
- * 升级、版本永不变」的根因。 */
582
- async function upgradeProfilePlugin(config, name) {
583
- assertValidPackage(name);
584
- if (INBOX_BUNDLES.has(name)) {
585
- throw new Error('dsh 内置组件随 dsh 一同升级,请等待平台更新 dsh');
586
- }
587
- if (name === PLUGIN_PACKAGE_NAME && isSelfManaged()) {
588
- throw new Error('AgentOne 插件随平台镜像一同升级,无需手动操作');
589
- }
590
- const dir = pluginProfileDir(config);
591
- const [latest, current] = await Promise.all([
592
- pnpmViewLatest(name, dir).catch(() => null),
593
- installedVersion(dir, name),
594
- ]);
595
- if (!latest) {
596
- throw new Error('查询插件最新版本失败,请稍后重试');
597
- }
598
- // latest dist-tag 可能指向旧版(作者误指/回退),不比已装高就没必要升
599
- if (latest === current || (current && compareSemver(latest, current) <= 0)) {
600
- throw new Error(`已是最新版本(v${current ?? latest})`);
601
- }
602
- try {
603
- await runPluginCli(['add', `${name}@${latest}`, ...(BUILTIN_PLUGIN_ALLOW_BUILD[name] || [])]);
604
- } catch (error) {
605
- // 同 install:ERR_PNPM_IGNORED_BUILDS 非零退出但包已装上,不阻断升级
606
- if ((await installedVersion(dir, name)) !== latest) throw error;
607
- }
608
- const after = await installedVersion(dir, name);
609
- if (after !== latest) {
610
- // 兑现「插件已升级」提示的前置校验:没升上去就明说,不再假报成功
611
- throw new Error(`升级未生效(当前 v${after ?? '未安装'},目标 v${latest}),请稍后重试`);
612
- }
613
- return listProfilePlugins(config);
614
- }
615
-
616
- /**
617
- * 内置插件常驻检测:镜像预装插件缺失时逐个静默重装。单个失败不阻断
618
- * (网络抖动等),下一个启动周期再试;全部已装时零开销。
619
- */
620
- async function ensureBuiltinPlugins(config, log) {
621
- const state = await listProfilePlugins(config);
622
- const installed = new Set(state.plugins.map((plugin) => plugin.name));
623
- const missing = BUILTIN_PLUGINS.filter((name) => !installed.has(name));
624
- if (!missing.length) return [];
625
- const reinstalled = [];
626
- for (const name of missing) {
627
- try {
628
- await installProfilePlugin(config, name);
629
- reinstalled.push(name);
630
- log.info(`[dsh-agentone] builtin plugin reinstalled: ${name}`);
631
- } catch (error) {
632
- log.warn(`[dsh-agentone] builtin plugin reinstall failed (${name}): ${error?.message || error}`);
633
- }
634
- }
635
- return reinstalled;
636
- }
637
-
638
- /**
639
- * 套餐技能自动同步:平台上 source=plan 且未安装的技能静默装齐(与平台托管
640
- * 实例的套餐技能预装语义对齐);单个失败不阻断其余,结果记 credentials。
641
- */
642
- async function syncPlanSkills(config, log, credentials) {
643
- const token = credentials?.access_token;
644
- if (!token) return null;
645
- let listing;
646
- try {
647
- listing = await listPlatformSkills(config.platformUrl, token);
648
- } catch (error) {
649
- log.warn(`[dsh-agentone] skills list failed: ${error?.message || error}`);
650
- await updateCredentials(config, { skills_error: String(error?.message || error) }).catch(
651
- () => {},
652
- );
653
- return null;
654
- }
655
- const pending = (listing.skills || []).filter((skill) => skill.source === 'plan' && !skill.installed);
656
- let installed = 0;
657
- const failures = [];
658
- for (const skill of pending) {
659
- try {
660
- await installPlatformSkill(config.platformUrl, token, skill.canonical_slug, {
661
- config,
662
- source: 'plan',
663
- });
664
- installed += 1;
665
- log.info(`[dsh-agentone] plan skill installed: ${skill.canonical_slug}`);
666
- } catch (error) {
667
- failures.push(`${skill.canonical_slug}: ${error?.message || error}`);
668
- }
669
- }
670
- await updateCredentials(config, {
671
- skills_synced_at: new Date().toISOString(),
672
- skills_error: failures.length ? failures.join(';') : null,
673
- }).catch(() => {});
674
- if (failures.length) log.warn(`[dsh-agentone] plan skill failures: ${failures.join(';')}`);
675
- return { installed, failed: failures.length };
676
- }
677
-
678
-
679
451
  /**
680
452
  * status 内缓存:skills/catalog 内容几乎不变,却被 5s 轮询每次全量重拉。
681
453
  * 缓存 45s;模型同步结果本就落盘在 credentials,不受缓存影响。
@@ -727,7 +499,7 @@ async function cachedPlatformStatus(config, credentials) {
727
499
  * token + state 比对),收完第一个请求即关停;5 分钟无回调自动超时关闭。
728
500
  * 返回外部浏览器可达的回调地址。
729
501
  */
730
- function openCallbackServer(config, log) {
502
+ function openCallbackServer(config, log, onLogin = () => {}, isCurrent = () => true) {
731
503
  return new Promise((resolve, reject) => {
732
504
  const server = createServer(async (req, res) => {
733
505
  const close = () => server.close();
@@ -758,6 +530,7 @@ button{font:inherit;padding:9px 22px;border-radius:10px;border:none;cursor:point
758
530
  finish(false);
759
531
  return;
760
532
  }
533
+ if (!isCurrent()) { finish(false); return; }
761
534
  const expiresIn = Number(result.expires_in) || 0;
762
535
  await saveCredentials(config, {
763
536
  schema_version: 2,
@@ -774,10 +547,8 @@ button{font:inherit;padding:9px 22px;border-radius:10px;border:none;cursor:point
774
547
  ...(await loadCredentials(config)),
775
548
  access_token: result.access_token,
776
549
  }).catch(() => {});
777
- await syncPlanSkills(config, log, {
778
- ...(await loadCredentials(config)),
779
- access_token: result.access_token,
780
- }).catch(() => {});
550
+ if (!isCurrent()) { finish(false); return; }
551
+ onLogin();
781
552
  finish(true);
782
553
  } catch (error) {
783
554
  log.warn(`[dsh-agentone] desktop exchange failed: ${error?.message || error}`);
@@ -795,12 +566,13 @@ button{font:inherit;padding:9px 22px;border-radius:10px;border:none;cursor:point
795
566
  }
796
567
 
797
568
  /**
798
- * 是否运行在 DSH Desktop 内:宿主进程是 Electron(execPath 指向 DSH Desktop
799
- * 可执行文件)。与页面侧标记(page.js 传 desktop:true)互为兜底——iframe 内
569
+ * 是否运行在桌面壳内:宿主进程是 Electron(execPath 指向桌面壳可执行文件)。
570
+ * 壳品牌:DSH Desktop / DSH NEXT / DeepSeek HARNESS(NEXT 的公开发布名)。
571
+ * 与页面侧标记(page.js 传 desktop:true)互为兜底——iframe 内
800
572
  * location.search 不含宿主 query,页面自检在设置 iframe 场景会漏。
801
573
  */
802
574
  function isDesktopHost() {
803
- return /DSH Desktop\.(?:app|exe)/i.test(process.execPath);
575
+ return /(?:DSH Desktop|DSH NEXT|DeepSeek HARNESS)\.(?:app|exe)/i.test(process.execPath);
804
576
  }
805
577
 
806
578
  export function apply(ctx, config) {
@@ -821,14 +593,96 @@ export function apply(ctx, config) {
821
593
  // patch 活配置层;取不到(旧宿主)就回退「只写 settings.yaml + 下次启动导入」。
822
594
  // 不能写进 inject:那会让插件在旧宿主上一直 PENDING。
823
595
  settingsService = ctx.get?.('settings') ?? null;
824
- if (desktopProfiles?.current?.name) {
825
- PLUGIN_PROFILE = desktopProfiles.current.name;
826
- ctx.inject(['desktopPnpm'], (desktopCtx) => {
827
- desktopPluginCli = desktopRunPlugin(desktopCtx.desktopPnpm, pluginProfileDir(config));
828
- log.info(`[dsh-agentone] desktop profile: ${PLUGIN_PROFILE}`);
829
- });
830
- }
831
596
  ctx.effect(() => {
597
+ let disposed = false;
598
+ const profiles = createProfilePlugins({ ctx, config, desktopRun: desktopRunPlugin });
599
+ const defaults = createDefaultsSync({ config, profiles,
600
+ credentials: () => ensureFreshToken(config, log), log });
601
+ const listProfilePlugins = async () => {
602
+ const state = await profiles.list();
603
+ const seen = new Set();
604
+ state.plugins.forEach((plugin) => {
605
+ seen.add(plugin.name);
606
+ plugin.self_managed = plugin.name === PLUGIN_PACKAGE_NAME && isSelfManaged();
607
+ plugin.builtin = plugin.builtin || BUILTIN_PLUGINS.includes(plugin.name);
608
+ plugin.description = plugin.description || BUILTIN_PLUGIN_DESCRIPTIONS[plugin.name] || null;
609
+ });
610
+ // 内置插件被卸载后不在 dependencies 里,仍以「未安装」占位出现在列表,
611
+ // 否则用户无从感知与恢复(常驻检测会在下次启动时自动重装)。
612
+ for (const name of BUILTIN_PLUGINS) {
613
+ if (seen.has(name)) continue;
614
+ state.plugins.push({ name, spec: null, version: null, bundled: false, registered: false,
615
+ active: false, source: 'npm', builtin: true,
616
+ self_managed: name === PLUGIN_PACKAGE_NAME && isSelfManaged(),
617
+ description: BUILTIN_PLUGIN_DESCRIPTIONS[name] || null, target_version: null,
618
+ state: 'missing', loaded: null });
619
+ }
620
+ return state;
621
+ };
622
+ const installProfilePlugin = async (_config, name, version = 'latest') => {
623
+ validatePackage(name, version);
624
+ // 内置插件(镜像控制台四件套/自身)是本机概念,不依赖平台登录:
625
+ // 直接走受管安装队列,构建授权用镜像预装命令同源的 node-pty 兜底。
626
+ if (BUILTIN_PLUGINS.includes(name)) {
627
+ await profiles.install(name, 'latest', { allowBuild: BUILTIN_PLUGIN_ALLOW_BUILD[name] || [] });
628
+ return listProfilePlugins();
629
+ }
630
+ await defaults.manualOperation(async (_owner, options) => {
631
+ const policy = options.document.plugins.find((item) => item.name === name);
632
+ // 平台清单声明的构建授权与内置插件已知的 node-pty 兜底合并
633
+ const allowBuild = [...new Set([...(policy?.allow_build || []), ...(BUILTIN_PLUGIN_ALLOW_BUILD[name] || [])])];
634
+ return profiles.install(name, 'latest', { ...options, allowBuild });
635
+ });
636
+ return listProfilePlugins();
637
+ };
638
+ const removeProfilePlugin = async (_config, name) => {
639
+ validatePackage(name);
640
+ if (PROTECTED_PACKAGES.has(name)) throw new Error('AgentOne 自身和 DSH 基础组件不可卸载');
641
+ if (BUILTIN_PLUGINS.includes(name)) throw new Error('内置插件承载 dsh 控制台核心体验,不可卸载');
642
+ return defaults.manualOperation((_owner, options) => profiles.remove(name, options));
643
+ };
644
+ const upgradeProfilePlugin = async (_config, name) => {
645
+ if (INBOX_BUNDLES.has(name) || (name === PLUGIN_PACKAGE_NAME && isSelfManaged())) {
646
+ throw new Error('此组件随宿主或平台更新,请等待平台更新');
647
+ }
648
+ await defaults.manualOperation(async (_owner, options) => {
649
+ const managed = options.document.plugins.find((item) => item.name === name);
650
+ if (managed) return profiles.install(name, managed.version, { ...options, allowBuild: managed.allow_build });
651
+ const latest = await pnpmViewLatest(name, profiles.target().dir).catch(() => null);
652
+ if (!latest) throw new Error('查询插件最新版本失败,请检查 npm 连接后重试');
653
+ const installed = (await profiles.list()).plugins.find((item) => item.name === name);
654
+ if (installed?.version && compareSemver(latest, installed.version) <= 0) {
655
+ throw new Error(`已是最新版本(v${installed.version})`);
656
+ }
657
+ return profiles.install(name, latest, options);
658
+ });
659
+ return listProfilePlugins();
660
+ };
661
+ const syncPlanSkills = async () => {
662
+ const result = await defaults.sync({ manual: true });
663
+ if (result?.error) throw new Error(result.error);
664
+ invalidateStatusCache();
665
+ return { installed: result?.skills.filter((item) => item.state === 'current').length || 0,
666
+ failed: result?.skills.filter((item) => ['failed', 'conflict', 'report_pending'].includes(item.state)).length || 0 };
667
+ };
668
+ const onLogin = () => {
669
+ defaults.reset();
670
+ void defaults.sync().then(invalidateStatusCache).catch(() => {});
671
+ };
672
+ // 内置插件常驻检测:缺失时静默重装(不依赖登录态);单个失败不阻断,
673
+ // 下个启动周期再试。已装不碰——版本升级交给平台默认能力同步。
674
+ const ensureBuiltinPlugins = async () => {
675
+ const state = await profiles.list();
676
+ const installed = new Set(state.plugins.filter((plugin) => plugin.version).map((plugin) => plugin.name));
677
+ for (const name of BUILTIN_PLUGINS.filter((entry) => !installed.has(entry))) {
678
+ try {
679
+ await profiles.install(name, 'latest', { allowBuild: BUILTIN_PLUGIN_ALLOW_BUILD[name] || [] });
680
+ log.info(`[dsh-agentone] builtin plugin reinstalled: ${name}`);
681
+ } catch (error) {
682
+ log.warn(`[dsh-agentone] builtin plugin reinstall failed (${name}): ${error?.message || error}`);
683
+ }
684
+ }
685
+ };
832
686
  // 模块级重试定时器跨 apply 存活:重入先清,避免旧定时器引用旧 config
833
687
  clearModelRetry();
834
688
  modelRetryAttempts = 0;
@@ -905,6 +759,18 @@ export function apply(ctx, config) {
905
759
  modelRetryAttempts = 0;
906
760
  }
907
761
  }
762
+ if (skills?.skills && defaults.snapshot().user_id) {
763
+ const identity = defaults.snapshot();
764
+ skills = { ...skills, skills: await Promise.all(skills.skills.map(async (item) => {
765
+ const local = await localSkillState(config.platformUrl, item.canonical_slug, {
766
+ config: { ...config, dshHome: identity.home }, userId: identity.user_id, registry: identity.registry,
767
+ });
768
+ return { ...item, installed: ['current', 'report_pending'].includes(local.state),
769
+ local_state: local.state, installed_version: local.record?.version || '',
770
+ upgrade_available: local.state === 'current' && Boolean(item.latest_version && item.latest_fingerprint)
771
+ && (local.record.version !== item.latest_version || local.record.fingerprint !== item.latest_fingerprint) };
772
+ })) };
773
+ }
908
774
  // 登录入口判定:UI 登录卡文案需要;托管实例不显示登录入口,跳过查询
909
775
  const loginProvider = isInstanceManaged() ? 'feishu' : await resolveLoginProvider(config.platformUrl, log);
910
776
  sendJson(response, 200, {
@@ -918,6 +784,7 @@ export function apply(ctx, config) {
918
784
  instance_id: credentials?.instance_id ?? null,
919
785
  token_expires_at: credentials?.expires_at ?? null,
920
786
  plugin_version: PLUGIN_VERSION,
787
+ defaults: defaults.snapshot(),
921
788
  capabilities,
922
789
  skills,
923
790
  skill_display_names: displayNames,
@@ -960,7 +827,7 @@ export function apply(ctx, config) {
960
827
  // /agentone/callback 会 403。desktop 环境(服务端自检 execPath 或页面
961
828
  // 传 desktop 标记)改由插件在本进程起一次性本地服务收回调。
962
829
  const callbackUrl = body.desktop || isDesktopRuntime
963
- ? await openCallbackServer(config, log)
830
+ ? await openCallbackServer(config, log, onLogin, () => !disposed)
964
831
  : String(body.callback_url || '');
965
832
  if (!/^https?:\/\/|^\/portal\//.test(callbackUrl)) {
966
833
  sendJson(response, 400, { error: '回调地址不合法' });
@@ -1008,6 +875,7 @@ export function apply(ctx, config) {
1008
875
  redirect(false);
1009
876
  return;
1010
877
  }
878
+ if (disposed) { redirect(false); return; }
1011
879
  const expiresIn = Number(result.expires_in) || 0;
1012
880
  await saveCredentials(config, {
1013
881
  schema_version: 2,
@@ -1024,10 +892,8 @@ export function apply(ctx, config) {
1024
892
  ...(await loadCredentials(config)),
1025
893
  access_token: result.access_token,
1026
894
  }).catch(() => {});
1027
- await syncPlanSkills(config, log, {
1028
- ...(await loadCredentials(config)),
1029
- access_token: result.access_token,
1030
- }).catch(() => {});
895
+ if (disposed) { redirect(false); return; }
896
+ onLogin();
1031
897
  redirect(true);
1032
898
  } catch (error) {
1033
899
  log.warn(`[dsh-agentone] exchange failed: ${error?.message || error}`);
@@ -1035,6 +901,33 @@ export function apply(ctx, config) {
1035
901
  }
1036
902
  });
1037
903
 
904
+ register('/agentone/api/defaults/status', (request, response) => {
905
+ if (request.method !== 'GET') { sendJson(response, 405, { error: '请求方法不正确' }); return; }
906
+ sendJson(response, 200, defaults.snapshot());
907
+ });
908
+ for (const action of ['sync', 'retry', 'apply']) {
909
+ register(`/agentone/api/defaults/${action}`, async (request, response) => {
910
+ if (request.method !== 'POST') { sendJson(response, 405, { error: '请求方法不正确' }); return; }
911
+ try {
912
+ const body = await readJsonBody(request);
913
+ const credentials = await ensureFreshToken(config, log);
914
+ if (!credentials) { sendJson(response, 401, { error: '请先登录 AgentOne 平台' }); return; }
915
+ let item = null;
916
+ if (action !== 'sync') {
917
+ if (!['skill', 'plugin'].includes(body.kind) || typeof body.name !== 'string') {
918
+ sendJson(response, 400, { error: '请选择有效的技能或插件' }); return;
919
+ }
920
+ if (action === 'apply' && body.confirm !== true) {
921
+ sendJson(response, 400, { error: '应用差异将替换当前版本,请确认后继续' }); return;
922
+ }
923
+ item = { kind: body.kind, name: body.name, apply: action === 'apply' };
924
+ }
925
+ void defaults.sync({ manual: true, item }).then(invalidateStatusCache).catch(() => {});
926
+ sendJson(response, 202, { ok: true, running: true });
927
+ } catch (error) { sendJson(response, 400, { error: error?.message || '默认能力同步失败,请重试' }); }
928
+ });
929
+ }
930
+
1038
931
  register('/agentone/api/model/sync', async (request, response) => {
1039
932
  if (request.method !== 'POST') {
1040
933
  response.writeHead(405, { allow: 'POST' });
@@ -1086,17 +979,12 @@ export function apply(ctx, config) {
1086
979
  return;
1087
980
  }
1088
981
  const body = await readJsonBody(request);
1089
- const slug = String(body.slug || '');
1090
- if (!/^[a-z0-9][a-z0-9-]*(--[a-z0-9][a-z0-9-]*)?$/i.test(slug)) {
1091
- sendJson(response, 400, { error: '技能标识格式不正确' });
1092
- return;
1093
- }
1094
- const result = await installPlatformSkill(
1095
- config.platformUrl,
1096
- credentials.access_token,
1097
- slug,
1098
- { config },
1099
- );
982
+ let slug;
983
+ try { slug = canonicalSkillSlug(body.slug); }
984
+ catch (error) { sendJson(response, 400, { error: error.message }); return; }
985
+ const result = await defaults.manualOperation((owner, options) => installPlatformSkill(
986
+ owner.platform, owner.token, slug, options,
987
+ ));
1100
988
  invalidateStatusCache();
1101
989
  sendJson(response, 200, { ok: true, ...result });
1102
990
  } catch (error) {
@@ -1153,7 +1041,15 @@ export function apply(ctx, config) {
1153
1041
  config.platformUrl,
1154
1042
  credentials.access_token,
1155
1043
  ).catch(() => null);
1156
- const installed = (listing?.skills || []).filter((skill) => skill.installed);
1044
+ const identity = defaults.snapshot();
1045
+ const installed = [];
1046
+ if (identity.user_id) {
1047
+ for (const skill of listing?.skills || []) {
1048
+ const local = await localSkillState(config.platformUrl, skill.canonical_slug, {
1049
+ config: { ...config, dshHome: identity.home }, userId: identity.user_id, registry: identity.registry });
1050
+ if (['current', 'report_pending'].includes(local.state)) installed.push({ ...skill, installed: true, installed_version: local.record.version });
1051
+ }
1052
+ }
1157
1053
  sendJson(response, 200, { ...data, installed });
1158
1054
  } catch (error) {
1159
1055
  sendJson(response, 502, { error: '技能市场暂时不可用,请稍后再试' });
@@ -1173,12 +1069,10 @@ export function apply(ctx, config) {
1173
1069
  return;
1174
1070
  }
1175
1071
  const body = await readJsonBody(request);
1176
- const slug = String(body.slug || '');
1177
- if (!/^[a-z0-9][a-z0-9-]*(--[a-z0-9][a-z0-9-]*)?$/i.test(slug)) {
1178
- sendJson(response, 400, { error: '技能标识格式不正确' });
1179
- return;
1180
- }
1181
- await removePlatformSkill(config.platformUrl, credentials.access_token, slug, { config });
1072
+ let slug;
1073
+ try { slug = canonicalSkillSlug(body.slug); }
1074
+ catch (error) { sendJson(response, 400, { error: error.message }); return; }
1075
+ await defaults.manualOperation((owner, options) => removePlatformSkill(owner.platform, owner.token, slug, options));
1182
1076
  invalidateStatusCache();
1183
1077
  sendJson(response, 200, { ok: true, canonical_slug: slug });
1184
1078
  } catch (error) {
@@ -1198,7 +1092,7 @@ export function apply(ctx, config) {
1198
1092
  // 不用 pnpm outdated——它受元数据缓存与 minimumReleaseAge 冷却期
1199
1093
  // 影响,新发布的版本一段时间内查不到,升级按钮追着发版延迟出现。
1200
1094
  // 只查已装且来自 npm 的包;未安装/本地链接没有版本可查。
1201
- const dir = pluginProfileDir(config);
1095
+ const dir = profiles.target().dir;
1202
1096
  await Promise.all(
1203
1097
  state.plugins
1204
1098
  .filter((item) => item.version !== null && item.source === 'npm')
@@ -1273,7 +1167,7 @@ export function apply(ctx, config) {
1273
1167
  // 中版本。升级成功后落盘先变、运行代码要重启宿主才换——两者差值即
1274
1168
  // 前端「已安装待重启」提示的依据。
1275
1169
  try {
1276
- const dir = pluginProfileDir(config);
1170
+ const dir = profiles.target().dir;
1277
1171
  const [version, latest] = await Promise.all([
1278
1172
  installedVersion(dir, PLUGIN_PACKAGE_NAME),
1279
1173
  pnpmViewLatest(PLUGIN_PACKAGE_NAME, dir).catch(() => null),
@@ -1674,6 +1568,7 @@ export function apply(ctx, config) {
1674
1568
  try {
1675
1569
  // 先吊销平台侧 refresh token(尽力而为),再清本地;凭据读取失败
1676
1570
  // (文件损坏等)也要走到清凭据,不能把用户卡在登录态。
1571
+ defaults.reset();
1677
1572
  const credentials = await loadCredentials(config);
1678
1573
  if (credentials?.refresh_token) {
1679
1574
  await revokePlatformToken(config.platformUrl, credentials.refresh_token);
@@ -1693,9 +1588,9 @@ export function apply(ctx, config) {
1693
1588
  }
1694
1589
  });
1695
1590
 
1696
- // 启动时若已有登录态:先保鲜令牌(临期轮换),再后台刷新模型配置、
1697
- // SkillHub 配置与套餐技能。内置插件常驻检测与登录无关,独立先行。
1698
- ensureBuiltinPlugins(config, log).catch((error) => {
1591
+ // 默认能力属于已登录平台;无凭据不安装兼容默认。协调器独立于页面存活。
1592
+ // 内置插件常驻检测与登录无关,独立先行。
1593
+ ensureBuiltinPlugins().catch((error) => {
1699
1594
  log.warn(`[dsh-agentone] builtin plugin check failed: ${error?.message || error}`);
1700
1595
  });
1701
1596
  ensureLanguageDefault(config, log).catch((error) => {
@@ -1707,17 +1602,20 @@ export function apply(ctx, config) {
1707
1602
  log.warn(`[dsh-agentone] instance credential bootstrap failed: ${error?.message || error}`);
1708
1603
  });
1709
1604
  const credentials = await ensureFreshToken(config, log);
1710
- if (!credentials) return;
1605
+ if (!credentials || disposed) return;
1606
+ void defaults.sync().then(invalidateStatusCache).catch(() => {});
1711
1607
  await syncPlatformModel(config, log).catch(() => {});
1712
1608
  // SkillHub 连接在托管容器里由 Hub 直接注入 env(CLAWHUB_* / SKILLHUB_*),
1713
1609
  // 插件不再重复写 skillhub.env / clawhub login,避免两个写入方互相盖。
1714
1610
  if (!isInstanceManaged()) {
1715
1611
  await syncPlatformSkillhub(config, log, credentials).catch(() => {});
1716
1612
  }
1717
- await syncPlanSkills(config, log, credentials).catch(() => {});
1718
1613
  })();
1719
1614
 
1720
- return () => {
1615
+ return async () => {
1616
+ disposed = true;
1617
+ await defaults.dispose();
1618
+ await profiles.dispose();
1721
1619
  pendingStates.clear();
1722
1620
  pendingCliAuth.clear();
1723
1621
  clearModelRetry();