dsh-agentone 0.5.39 → 0.5.41

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 CHANGED
@@ -35,9 +35,17 @@ lark-cli / ccpg-cli 状态探测。
35
35
  不删除本机内容。平台暂时不可用时缓存仅展示,不用于安装。
36
36
  - **dsh 插件管理(M3+)**:列出当前 profile 的包、版本、bundle 注册状态;
37
37
  安装成功需物理版本、插件声明和 profile 注册同时成立。注册不证明运行时加载,
38
- 页面提示重启生效。Web 必须配置实际 `profileName`,Desktop 使用宿主当前 profile
39
- 和受管 pnpm 通道;安装/卸载串行排队、支持生命周期取消。自身与 DSH 基础组件
40
- 不属于平台默认清单,自身只在独立安装模式允许手动升级。
38
+ 页面提示重启生效。Web 必须配置实际 `profileName`;桌面壳按宿主契约自动取
39
+ 当前 profile——第三方壳(desktopProfiles + 受管 desktopPnpm 通道)与官方
40
+ DeepSeek Harness 应用(launcher 的 profileContext + 宿主 pluginManager,
41
+ 与应用内「设置 → 插件」同一管线,不接受 pnpm 选项)分别适配;安装/卸载
42
+ 串行排队、支持生命周期取消。自身与 DSH 基础组件不属于平台默认清单,
43
+ 自身只在独立安装模式允许手动升级。
44
+ - **旧版技能记录迁移(0.5.41)**:0.5.39 起本机安装真源是
45
+ `$DSH_HOME/agentone/skill-state/<slug>.json`;≤0.5.38 只写平台台账,升级后
46
+ 这些技能会「有目录、无记录」。这类条目标注「旧版安装」,插件页提供
47
+ 「重新应用旧版安装的技能」批量入口(按平台版本原子覆盖目录并重建记录);
48
+ 有本机记录但内容被改过的真冲突仍逐项人工确认,不自动覆盖。
41
49
  - **CLI 探测、安装与授权(M4)**:`lark-cli` / `ccpg-cli` 安装与授权态三态探测;
42
50
  未安装时「安装」一键自助安装(lark 走 `npm install -g @larksuite/cli@latest`,
43
51
  ccpg 走公司 `auto-install.sh`,口径与平台 cli-deployd 一致);
package/lib/client.js CHANGED
@@ -201,10 +201,14 @@ window.__ModuleLoader__.load({
201
201
  const startLogin = async () => {
202
202
  setError('');
203
203
  try {
204
+ // 本组件只跑在桌面壳(DSH Desktop / DSH NEXT "DeepSeek HARNESS")里,
205
+ // 页面经 srcdoc/沙箱 iframe 加载:location.origin 是 "null",拼
206
+ // callback_url 会被插件 400「回调地址不合法」。与 page.js 的桌面
207
+ // 路径一致发 desktop 标记,让插件起一次性 127.0.0.1 本地回调服务。
204
208
  const response = await fetch('/agentone/api/login', {
205
209
  method: 'POST',
206
210
  headers: { 'Content-Type': 'application/json' },
207
- body: JSON.stringify({ callback_url: `${location.origin}/agentone/callback` }),
211
+ body: JSON.stringify({ desktop: true }),
208
212
  });
209
213
  const data = await response.json();
210
214
  if (!response.ok) {
@@ -176,8 +176,18 @@ export function createDefaultsSync({ config, profiles, credentials, log, interva
176
176
  try {
177
177
  const local = await localSkillState(owner.platform, item.canonical_slug, options);
178
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; }
179
+ // 0.5.39 起「本机记录文件」才是安装真源;更早版本(≤0.5.38)只写
180
+ // 平台台账,升级后这些目录必然「有目录、无记录」——和「用户改过
181
+ // 内容」撞在同一个 conflict 状态里,必须区分开,否则升级当天全部
182
+ // 技能被误报成来源不一致/未安装(实测 11/11 命中)。
183
+ const legacy = local.state === 'conflict' && !local.record && Boolean(item.installed_fingerprint);
184
+ Object.assign(entry, { state: local.state, version: local.record?.version || null, legacy });
185
+ if (local.state === 'conflict' && (options.missingOnly || !selectedItem)) {
186
+ entry.error = legacy
187
+ ? '旧版本安装的技能缺少本机记录,请点「重新应用」重建(按平台版本覆盖技能目录)'
188
+ : '本机技能已有用户内容或来源不一致,请确认后手动应用';
189
+ continue;
190
+ }
181
191
  if (local.state === 'current' && (!item.latest_version || !item.latest_fingerprint) && options.missingOnly) {
182
192
  entry.state = 'failed'; entry.error = '暂时无法确认技能最新版本,请稍后重试'; continue;
183
193
  }
package/lib/desktop.js ADDED
@@ -0,0 +1,42 @@
1
+ // 桌面壳探测与 profile 解析:判定只允许一份实现。
2
+ //
3
+ // 两种桌面契约(实测当前官方应用只满足后者):
4
+ // 1. 第三方壳(dsh-plugin-desktop):发布 `desktopProfiles.current`
5
+ // (name + dir),装包走 `desktopPnpm`;
6
+ // 2. 官方 DeepSeek Harness Electron 应用:**不发布** desktopProfiles /
7
+ // desktopPnpm(app.asar 里两个名字均 0 命中),由 launcher 发布
8
+ // `profileContext`(name/dir/packageManager),装包走 `pluginManager`。
9
+ // 该壳里 dsh CLI 明确拒绝 `--profile desktop`,profileContext 是唯一答案。
10
+ import { join } from 'node:path';
11
+
12
+ /** 桌面壳品牌:DSH Desktop(第三方壳)/ DSH NEXT、DeepSeek HARNESS
13
+ * (NEXT 及其公开发布名)。宿主进程是 Electron 时 execPath 指向壳可执行文件。 */
14
+ export function isDesktopHost() {
15
+ return /(?:DSH Desktop|DSH NEXT|DeepSeek HARNESS)\.(?:app|exe)/i.test(process.execPath);
16
+ }
17
+
18
+ export function validProfileName(name) {
19
+ return typeof name === 'string' && /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,119}$/.test(name) && name !== 'node_modules';
20
+ }
21
+
22
+ /**
23
+ * 解析当前 profile。桌面宿主以壳/launcher 的答案为准(桌面壳里 dsh CLI 拒绝
24
+ * 操作 desktop profile,猜出来的目录一定对不上);普通 Web/CLI 宿主仍以插件
25
+ * 配置的 profileName 优先,其次 launcher 的 profileContext。
26
+ * 目录优先取 launcher 自己给的 dir——profile 落在哪里由 launcher 决定。
27
+ * @returns {{home: string, name: string, dir: string, desktop: boolean}|null}
28
+ */
29
+ export function resolveProfileTarget({ ctx, config = {}, home, desktop }) {
30
+ const shell = ctx?.get?.('desktopProfiles');
31
+ const launched = ctx?.get?.('profileContext');
32
+ const candidates = desktop
33
+ ? [shell?.current?.name, launched?.name, config.profileName]
34
+ : [config.profileName, launched?.name, shell?.current?.name];
35
+ const name = candidates.find(validProfileName);
36
+ if (!name) return null;
37
+ const owned = [
38
+ [shell?.current?.name, shell?.current?.dir],
39
+ [launched?.name, launched?.dir],
40
+ ].find(([owner, dir]) => owner === name && typeof dir === 'string' && dir.trim() !== '');
41
+ return { home, name, dir: owned ? owned[1].trim() : join(home, 'profiles', name), desktop };
42
+ }
package/lib/index.js CHANGED
@@ -34,6 +34,7 @@ import { applyPlatformModel, removePlatformModel } from './model.js';
34
34
  import { installPlatformSkill, listPlatformSkills, removePlatformSkill } from './skill.js';
35
35
  import { applyPlatformSkillhub, removePlatformSkillhub } from './skillhub.js';
36
36
  import { createProfilePlugins, validatePackage, PROTECTED_PACKAGES } from './profile-plugins.js';
37
+ import { isDesktopHost } from './desktop.js';
37
38
  import { createDefaultsSync } from './defaults-sync.js';
38
39
  import { canonicalSkillSlug, localSkillState } from './skill-state.js';
39
40
  import { ensureLanguageDefault } from './language.js';
@@ -565,15 +566,6 @@ button{font:inherit;padding:9px 22px;border-radius:10px;border:none;cursor:point
565
566
  });
566
567
  }
567
568
 
568
- /**
569
- * 是否运行在 DSH Desktop 内:宿主进程是 Electron(execPath 指向 DSH Desktop
570
- * 可执行文件)。与页面侧标记(page.js 传 desktop:true)互为兜底——iframe 内
571
- * location.search 不含宿主 query,页面自检在设置 iframe 场景会漏。
572
- */
573
- function isDesktopHost() {
574
- return /DSH Desktop\.(?:app|exe)/i.test(process.execPath);
575
- }
576
-
577
569
  export function apply(ctx, config) {
578
570
  const log = ctx.logger || console;
579
571
  // 托管实例:注入的平台地址优先。容器内只有这个地址可达(本地部署是
@@ -583,9 +575,11 @@ export function apply(ctx, config) {
583
575
  log.info(`[dsh-agentone] platform url from instance env: ${injectedPlatformUrl}`);
584
576
  config.platformUrl = injectedPlatformUrl;
585
577
  }
586
- // Desktop 环境探测(官方契约,见 harness-one DESKTOP.md):desktopProfiles
587
- // 只在 DSH Desktop 存在。据此把插件管理切到当前 profile,并用受管
588
- // desktopPnpm 做装/卸/升级(desktop 里 dsh CLI 拒绝操作 desktop profile)。
578
+ // Desktop 环境探测:第三方壳发布 desktopProfiles + desktopPnpm,官方
579
+ // Electron 应用只发布 profileContext + pluginManager,判定统一在 desktop.js
580
+ // (品牌名与 profile 解析只有一份实现)。与页面侧标记(page.js 传
581
+ // desktop:true)互为兜底——iframe 内 location.search 不含宿主 query,
582
+ // 页面自检在设置 iframe 场景会漏。
589
583
  const desktopProfiles = ctx.get?.('desktopProfiles');
590
584
  const isDesktopRuntime = Boolean(desktopProfiles?.current?.name) || isDesktopHost();
591
585
  // 官方 settings 服务(可选依赖):dsh ≥ 0.1.7 用它把平台模型写进 profile
@@ -991,6 +985,50 @@ export function apply(ctx, config) {
991
985
  }
992
986
  });
993
987
 
988
+ // 旧版本(≤0.5.38)装的技能没有本机记录:目录和平台台账都在,但本机
989
+ // 状态判定只会得到 conflict。按平台台账逐个重新应用(原子覆盖目录 +
990
+ // 重建记录 + 回报平台),一次点击修完一台机器。只处理「无本机记录」
991
+ // 的旧版安装;真有记录却内容不一致(用户改过)的仍走人工确认。
992
+ register('/agentone/api/skill/repair', async (request, response) => {
993
+ if (request.method !== 'POST') {
994
+ response.writeHead(405, { allow: 'POST' });
995
+ response.end();
996
+ return;
997
+ }
998
+ try {
999
+ const credentials = await ensureFreshToken(config, log);
1000
+ if (!credentials) {
1001
+ sendJson(response, 401, { error: '请先完成 AgentOne 登录' });
1002
+ return;
1003
+ }
1004
+ const result = await defaults.manualOperation(async (owner, options) => {
1005
+ const listing = await listPlatformSkills(owner.platform, owner.token, { signal: options.signal });
1006
+ const repaired = [];
1007
+ const failed = [];
1008
+ for (const item of listing?.skills || []) {
1009
+ if (!item.installed_fingerprint) continue;
1010
+ const local = await localSkillState(owner.platform, item.canonical_slug, options);
1011
+ if (!(local.state === 'conflict' && !local.record)) continue;
1012
+ try {
1013
+ await installPlatformSkill(owner.platform, owner.token, item.canonical_slug, {
1014
+ ...options,
1015
+ source: ['plan', 'builtin'].includes(item.source) ? item.source : 'manual',
1016
+ });
1017
+ repaired.push(item.canonical_slug);
1018
+ } catch (error) {
1019
+ failed.push({ name: item.canonical_slug, error: error?.message || '重新应用失败' });
1020
+ }
1021
+ }
1022
+ return { repaired: repaired.length, failed: failed.length, names: repaired, errors: failed };
1023
+ });
1024
+ invalidateStatusCache();
1025
+ void defaults.sync({ manual: true }).catch(() => {});
1026
+ sendJson(response, 200, { ok: true, ...result });
1027
+ } catch (error) {
1028
+ sendJson(response, 502, { error: error?.message || '重新应用旧版技能失败' });
1029
+ }
1030
+ });
1031
+
994
1032
  // 套餐技能同步:把平台上 source=plan 且未安装的技能装齐。前端在套餐
995
1033
  // 生效联动与自动补装触发器里调,也可手动触发;同步结果失效技能缓存。
996
1034
  register('/agentone/api/skill/sync', async (request, response) => {
package/lib/page.js CHANGED
@@ -263,7 +263,10 @@ const maybeAutoSyncPlanSkills = (forced) => {
263
263
  const skills = lastStatus?.skills?.skills;
264
264
  if (!forced) {
265
265
  if (!Array.isArray(skills)) return;
266
- if (!skills.some((skill) => skill.source === 'plan' && !skill.installed)) return;
266
+ // 只有本机真的缺目录(missing)才值得补装。conflict 是「目录在、记录/
267
+ // 来源对不上」,missingOnly 的同步会跳过它——旧版技能升级后这里每 60s
268
+ // 空转一次,用户只会看到重复提示却什么都不变。
269
+ if (!skills.some((skill) => skill.source === 'plan' && !skill.installed && skill.local_state === 'missing')) return;
267
270
  }
268
271
  planSkillSyncAt = now;
269
272
  planSkillSyncing = true;
@@ -399,12 +402,18 @@ const renderDefaults = (s) => {
399
402
  failed: '失败', conflict: '本机冲突', report_pending: '已安装,待回报', restart_required: '已安装,重启后生效' };
400
403
  const sources = { platform: '平台清单', compatibility: '旧平台兼容默认', cache: '本机缓存' };
401
404
  if (!config || !s.logged_in) { body.innerHTML = '<div class="muted">登录后检查平台默认能力</div>'; return; }
405
+ const entries = [...(config.skills || []), ...(config.plugins || [])];
406
+ // legacy:0.5.39 之前装的技能只有平台台账、没有本机记录(见 defaults-sync)。
407
+ // 这类条目单独给一个批量入口,不然升级后每台机器都要点 N 次「确认应用」。
408
+ const legacyCount = entries.filter((item) => item.legacy).length;
402
409
  const summary = '<div class="skill-sub">' + esc(sources[config.source] || '尚未检查')
403
410
  + (config.revision !== null ? ' · 版本 ' + esc(config.revision) : '')
404
411
  + (config.checked_at ? ' · ' + esc(new Date(config.checked_at).toLocaleString()) : '')
405
412
  + (config.running ? ' · 正在检查' : '') + '</div>'
406
- + (config.error || config.profile_error ? '<div class="skill-sub" style="color:var(--err)">' + esc(config.error || config.profile_error) + '</div>' : '');
407
- const entries = [...(config.skills || []), ...(config.plugins || [])];
413
+ + (config.error || config.profile_error ? '<div class="skill-sub" style="color:var(--err)">' + esc(config.error || config.profile_error) + '</div>' : '')
414
+ + (legacyCount ? '<div class="skill-sub"><button id="defaults-repair" class="btn small ghost"'
415
+ + (config.running ? ' disabled' : '') + '>重新应用旧版安装的技能(' + legacyCount + ')</button>'
416
+ + ' 技能目录会按平台当前版本覆盖</div>' : '');
408
417
  const html = summary + entries.map((item) => {
409
418
  const action = ['failed', 'report_pending', 'missing'].includes(item.state) ? 'retry'
410
419
  : ['conflict', 'pending'].includes(item.state) ? 'apply' : null;
@@ -414,6 +423,7 @@ const renderDefaults = (s) => {
414
423
  return '<div class="skill-item"><div class="skill-info"><div class="skill-name">'
415
424
  + esc(item.description || item.name) + ' <span class="tag">' + (item.kind === 'skill' ? '技能' : '插件')
416
425
  + '</span></div><div class="skill-sub">' + esc(item.name) + ' · ' + esc(labels[item.state] || item.state)
426
+ + (item.legacy ? '(旧版安装)' : '')
417
427
  + (item.version ? ' · ' + esc(item.version) : '')
418
428
  + (item.target_version && item.target_version !== item.version ? ' → ' + esc(item.target_version) : '')
419
429
  + (item.error ? '<br><span style="color:var(--err)">' + esc(item.error) + '</span>' : '')
@@ -429,6 +439,37 @@ const renderDefaults = (s) => {
429
439
  ? confirmThen(btn, '替换当前版本?', request, '已开始应用,请查看同步状态')
430
440
  : withLoading(btn, request, '已开始重试,请查看同步状态');
431
441
  });
442
+ const repair = body.querySelector('#defaults-repair');
443
+ if (repair) repair.onclick = async () => {
444
+ if (repair.dataset.confirming !== '1') {
445
+ repair.dataset.confirming = '1';
446
+ repair.dataset.originalLabel = repair.textContent;
447
+ repair.textContent = '确认按平台版本覆盖?';
448
+ repair.dataset.confirmTimer = String(setTimeout(() => {
449
+ repair.textContent = repair.dataset.originalLabel || '重新应用';
450
+ delete repair.dataset.confirming;
451
+ }, 3000));
452
+ return;
453
+ }
454
+ delete repair.dataset.confirming;
455
+ const original = repair.innerHTML;
456
+ repair.disabled = true;
457
+ repair.innerHTML = '<span class="spin"></span>重新应用中…';
458
+ try {
459
+ const resp = await fetch('api/skill/repair', { method: 'POST' });
460
+ const data = await resp.json().catch(() => ({}));
461
+ if (!resp.ok) throw new Error(data.error || '重新应用失败');
462
+ notify(data.failed
463
+ ? '已重新应用 ' + data.repaired + ' 个技能,' + data.failed + ' 个失败'
464
+ : '已重新应用 ' + data.repaired + ' 个旧版技能');
465
+ } catch (error) {
466
+ notify(error.message || '重新应用失败', 'err');
467
+ } finally {
468
+ repair.disabled = false;
469
+ repair.innerHTML = original;
470
+ await refresh();
471
+ }
472
+ };
432
473
  }
433
474
  $('defaults-sync-btn').disabled = config.running;
434
475
  };
@@ -461,14 +502,19 @@ const renderSkills = (s) => {
461
502
  // 可升级:平台对比 latest 指纹/版本后给的结论,提示「v旧 → v新」;
462
503
  // latest 查询失败时平台降级 false,这里不额外猜测
463
504
  const upgradable = skill.installed && skill.upgrade_available;
505
+ // 目录在、记录不符:不是「没装」,重新应用会按平台版本覆盖目录
506
+ const stranded = !skill.installed && skill.local_state === 'conflict';
464
507
  const meta = !skill.installed
465
- ? '<span class="muted">未安装</span>'
508
+ ? (stranded
509
+ ? '<span class="muted">本机有目录但记录不符,需重新应用</span>'
510
+ : '<span class="muted">未安装</span>')
466
511
  : upgradable
467
512
  ? '已装 ' + esc(skill.installed_version || '')
468
513
  + ' → <b>v' + esc(skill.latest_version || '新版本') + '</b>'
469
514
  : '已装 ' + esc(skill.installed_version || '');
470
515
  const action = !skill.installed
471
- ? '<button class="btn small primary" data-install="' + esc(skill.canonical_slug) + '">安装</button>'
516
+ ? '<button class="btn small primary" data-install="' + esc(skill.canonical_slug) + '">'
517
+ + (stranded ? '重新应用' : '安装') + '</button>'
472
518
  : upgradable
473
519
  ? '<button class="btn small primary" data-upgrade="' + esc(skill.canonical_slug) + '">升级</button>'
474
520
  + '<button class="btn small danger" data-uninstall="' + esc(skill.canonical_slug) + '">卸载</button>'
@@ -487,7 +533,7 @@ const renderSkills = (s) => {
487
533
  // 次确认
488
534
  const fingerprint = list.map((skill) =>
489
535
  skill.canonical_slug + ':' + (skill.installed ? 1 : 0) + ':' + (skill.installed_version || '')
490
- + ':' + (skill.upgrade_available ? 1 : 0),
536
+ + ':' + (skill.upgrade_available ? 1 : 0) + ':' + (skill.local_state || ''),
491
537
  ).join('|') + '#' + metaHtml;
492
538
  if ($('skills-list').dataset.fingerprint !== fingerprint) {
493
539
  $('skills-list').dataset.fingerprint = fingerprint;
@@ -1,6 +1,8 @@
1
1
  // 当前 profile 的插件操作;读取真实包和注册状态,写操作共享实例队列。
2
+ import { randomUUID } from 'node:crypto';
2
3
  import { readFile } from 'node:fs/promises';
3
4
  import { isAbsolute, join, relative, resolve } from 'node:path';
5
+ import { isDesktopHost, resolveProfileTarget } from './desktop.js';
4
6
  import { dshPluginCli, pnpmViewLatest } from './proc.js';
5
7
  import { resolveDshHome } from './store.js';
6
8
 
@@ -20,22 +22,81 @@ async function readJson(path) {
20
22
  try { return JSON.parse(await readFile(path, 'utf8')); }
21
23
  catch (error) { if (error.code === 'ENOENT') return null; throw new Error('插件配置文件无法读取,请检查本机 profile'); }
22
24
  }
23
- function validProfile(name) {
24
- return typeof name === 'string' && /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,119}$/.test(name) && name !== 'node_modules';
25
+
26
+ /** npm spec 白名单:`name` 或 `name@version`(`TARGET_RE` 同时用于官方
27
+ * 桌面端 pluginManager 通道,那个通道只接受包名/版本,不接受 pnpm 选项)。 */
28
+ export const TARGET_RE = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*(?:@[^\s@]+)?$/i;
29
+
30
+ /** 官方桌面端 pluginManager 的返回:application 为终态枚举,packageResult
31
+ * 是 pnpm 段的输出。`overridden` 是「改动保留了,只是被别的层覆盖显示」,
32
+ * 不是失败(市场侧踩过这个坑)。 */
33
+ const MANAGER_APPLIED = new Set(['applied', 'restart-required', 'overridden']);
34
+
35
+ function failureDetail(error) {
36
+ if (typeof error === 'string') return error;
37
+ if (error instanceof Error) return error.message;
38
+ if (error === undefined) return '';
39
+ try { return JSON.stringify(error); } catch { return String(error); }
25
40
  }
26
- export function createProfilePlugins({ ctx, config = {}, desktopRun, runCli = dshPluginCli, latest = pnpmViewLatest }) {
41
+
42
+ /**
43
+ * 官方 Electron 桌面端通道:这个 profile 完全由宿主 in-process 的
44
+ * pluginManager 管(安装/卸载只会走它自己的「设置 → 插件」管线)。只接受
45
+ * `add <spec>` / `remove <name>`——官方管线自己处理构建脚本审批,不接受
46
+ * pnpm 选项,传进去会让整次操作被拒(`--allow-build` 因此不转发)。
47
+ * 返回契约与 dshPluginCli 一致:成功 {stdout, stderr},失败抛 Error。
48
+ */
49
+ export function desktopManagerRun(manager) {
50
+ let queue = Promise.resolve();
51
+ const settle = (result) => {
52
+ const stdout = String(result?.packageResult?.output || '');
53
+ if (MANAGER_APPLIED.has(result?.application)) return { stdout, stderr: '' };
54
+ throw new Error(failureDetail(result?.error) || '插件操作未完成,请在「设置 → 插件」里重试');
55
+ };
56
+ const run = async (args, options = {}) => {
57
+ options.signal?.throwIfAborted();
58
+ const [command, target, ...extra] = args;
59
+ // 官方管线自己处理构建脚本审批(与应用内「设置 → 插件」同一管线),
60
+ // 不接受 pnpm 选项:除了 dsh-agentone 自己生成的 --allow-build,其余
61
+ // 参数一律拒绝,避免「静默丢参数改变用户意图」。
62
+ const rejected = extra.filter((argument) => !/^--allow-build=[^@\s]+$/.test(argument));
63
+ if (rejected.length) throw new Error('官方桌面端的插件操作由应用自己执行,不接受附加参数');
64
+ if (typeof target !== 'string' || !TARGET_RE.test(target)) throw new Error('插件名不合法,请填写完整的 npm 包名');
65
+ if (command === 'add') {
66
+ const requestId = randomUUID();
67
+ const cancel = () => manager.cancelInstall?.(requestId);
68
+ options.signal?.addEventListener('abort', cancel, { once: true });
69
+ try {
70
+ const result = await manager.installBundle(target, { requestId });
71
+ options.signal?.throwIfAborted();
72
+ return settle(result);
73
+ } finally {
74
+ options.signal?.removeEventListener('abort', cancel);
75
+ }
76
+ }
77
+ if (command === 'remove') return settle(await manager.removeBundle(target));
78
+ throw new Error(`官方桌面端不支持这个插件操作(${command}),请在「设置 → 插件」里完成`);
79
+ };
80
+ return (args, options = {}) => {
81
+ const next = queue.then(() => run(args, options));
82
+ queue = next.catch(() => undefined);
83
+ return next;
84
+ };
85
+ }
86
+
87
+ export function createProfilePlugins({ ctx, config = {}, desktopRun, runCli = dshPluginCli, latest = pnpmViewLatest,
88
+ isDesktop = () => Boolean(ctx.get?.('desktopProfiles')) || isDesktopHost(),
89
+ managerRun = desktopManagerRun }) {
27
90
  let queue = Promise.resolve();
28
91
  let closed = false;
29
92
  let policy = [];
30
93
  function target() {
31
94
  if (closed) throw new Error('插件已停止,请重新打开后再试');
32
- const desktop = ctx.get?.('desktopProfiles');
33
- const isDesktop = Boolean(desktop) || /DSH Desktop\.(?:app|exe)/i.test(process.execPath);
34
- const name = desktop?.current?.name || (!isDesktop ? config.profileName : null);
35
- if (!validProfile(name)) throw new Error('无法确定当前 DSH profile,请在 AgentOne 插件配置中填写 profileName 后重启');
36
95
  const homeService = ctx.get?.('dshHomePath');
37
96
  const home = resolve(config.dshHome || (typeof homeService === 'function' ? homeService() : homeService) || resolveDshHome());
38
- return { home, name, dir: join(home, 'profiles', name), desktop: isDesktop };
97
+ const resolved = resolveProfileTarget({ ctx, config, home, desktop: isDesktop() });
98
+ if (!resolved) throw new Error('无法确定当前 DSH profile,请在 AgentOne 插件配置中填写 profileName 后重启');
99
+ return resolved;
39
100
  }
40
101
  function enqueue(operation) {
41
102
  let expected;
@@ -91,9 +152,12 @@ export function createProfilePlugins({ ctx, config = {}, desktopRun, runCli = ds
91
152
  await options.beforeCommit?.();
92
153
  check(current, options);
93
154
  if (current.desktop) {
155
+ // 第三方壳:受管 desktopPnpm;官方 Electron 应用:pluginManager。
94
156
  const service = ctx.get?.('desktopPnpm');
95
- if (!service || !desktopRun) throw new Error('桌面插件安装通道尚未就绪,请稍后重试');
96
- return desktopRun(service, current.dir)(args, options);
157
+ if (service && desktopRun) return desktopRun(service, current.dir)(args, options);
158
+ const manager = ctx.get?.('pluginManager');
159
+ if (manager) return managerRun(manager)(args, options);
160
+ throw new Error('桌面插件安装通道尚未就绪,请稍后重试');
97
161
  }
98
162
  return runCli(args, current.name, { env: { DSH_HOME: current.home }, signal: options.signal });
99
163
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-agentone",
3
- "version": "0.5.39",
4
- "description": "AgentOne 平台集成:IDOne 登录(飞书兜底)、平台模型、SkillHub 技能、套餐与 CLI;平台默认能力(内置技能与默认插件)自动安装并始终保持最新;lark-cli 首装一键初始化飞书应用;平台托管实例免登录自动授权",
3
+ "version": "0.5.41",
4
+ "description": "AgentOne 平台集成:平台模型、SkillHub 技能、套餐与 CLI;lark-cli 首装一键初始化飞书应用;修复官方桌面壳 profile 解析失效并支持旧版技能记录一键重建",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "smoke": "node scripts/env-matrix-smoke.mjs"