dsh-expert-group 0.1.4 → 0.1.5

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
@@ -24,33 +24,27 @@
24
24
  - **无内置默认模型**:新安装后所有角色均为空,需在“dsh-expert-group”设置页从 DSH
25
25
  已配置的模型中自行选择并保存;未配置模型的角色在调用时会给出明确提示。
26
26
  - 模型与思考深度通过 `llm.listProviders/listModels` + `resolveCallConfig` 校验,只接受设置中已添加的模型。
27
- - **动态 settings 保存能力**:插件不在模块加载期 import `@deepseek-ai/dsh-settings`,package 也不声明该 peer,因此默认安装不会额外下载 settings 包。目标运行时为 `0.2.0-rc.2`;插件通过宿主 settings 服务的 describe/update/mutate 面读写 profile 条目,并由 DEG 自有的 settings-commit 端点完成语义校验与 revision 冲突检测(宿主不再提供 owner validate 钩子)。
27
+ - **动态 settings 保存能力**:插件不在模块加载期 import `@deepseek-ai/dsh-settings`(package 声明的是 `>=0.2.1-rc.1` peer,宿主应提供兼容版本,插件自身不额外加载该包)。目标运行时为 `0.2.1-rc.1` / `0.2.1-rc.2`(即 `>0.2.1-rc.1` 的 RC2 契约宿主线);插件通过宿主 settings 服务的 describe/update/mutate 面读写 profile 条目,并由 DEG 自有的 settings-commit 端点完成语义校验与 revision 冲突检测(宿主不再提供 owner validate 钩子)。
28
28
  - 子代理的 `reasoningEffort` 通过子代理 `agent/request` waterfall 注入,绕开 `dsh-tool-subagent`
29
29
  `AgentOptions` 尚未公开该字段的限制。
30
30
 
31
31
  ## 安装
32
32
 
33
- 本插件发布到 npm 公共源,用 DSH 官方 `dsh plugin` CLI 按包名安装(`--profile` 为必填参数,一般填 `web`):
34
-
35
- ```powershell
36
- dsh plugin --profile web add dsh-expert-group
37
- ```
38
-
39
- 也可以直接用 npm 安装:
33
+ 本插件发布到 npm 公共源,可直接用 npm 安装:
40
34
 
41
35
  ```bash
42
36
  npm i dsh-expert-group
43
37
  ```
44
38
 
45
- 前置条件:DSH Web 可正常运行(`dsh web`)、Node.js ≥ 22,且 PATH 上有 `pnpm`。安装后重启 `dsh web` 使插件生效。
39
+ 也可以在 DSH 的「添加插件」页面按包名安装:在输入框中填入 `dsh-expert-group`,按需在右侧切换安装源,然后点击「安装」。
46
40
 
47
- 桌面版(Electron 客户端)的 `desktop` profile 由客户端独占管理,PATH 上的 `dsh` CLI 改不动它:先打开 Desktop 完成初始化再完全退出,用安装目录自带的 CLI 执行同一命令。
41
+ ![DSH「添加插件」对话框截图:输入框中填写的是 dsh-expert-group,右侧可切换安装源](docs/assets/dsh-plugin-add-dialog.png)
48
42
 
49
- ```powershell
50
- & '<DSH 安装目录>\resources\runtime\cli\bin\dsh.cmd' plugin --profile desktop add dsh-expert-group
51
- ```
43
+ > 该截图存放在仓库的 `docs/assets/` 下;npm 安装包按 `package.json` 的 `files` 白名单发布、**不包含 `docs/`**,因此从 npm 安装的副本里这张图不会显示,在仓库中阅读一切正常。
44
+
45
+ 前置条件:DSH 版本 ≥ `0.2.1-rc.1`、DSH Web 可正常运行(`dsh web`)、Node.js ≥ 22,且 PATH 上有 `pnpm`。安装后重启 `dsh web` 使插件生效。
52
46
 
53
- 安装后重新打开 Desktop 使插件生效。本插件不内置角色默认模型:先在 DSH「设置 → 模型」配置模型,再到「设置 → dsh-expert-group」为各角色选择模型并保存。
47
+ 本插件不内置角色默认模型:先在 DSH「设置 → 模型」配置模型,再到「设置 → dsh-expert-group」为各角色选择模型并保存。
54
48
 
55
49
  ## 界面总览
56
50
 
package/client-bundle.js CHANGED
@@ -4432,7 +4432,13 @@
4432
4432
  // 关闭态:只调用记录里的精确 tabId。绝不根据 active() 当前是谁来决定关哪个 tab。
4433
4433
  if (record !== undefined && closeSidebarResource !== null) {
4434
4434
  let closed = false;
4435
- try { closeSidebarResource.call(sidebarRight, record.tabId); closed = true; } catch { closed = false; }
4435
+ try {
4436
+ // close() 可异步失败;结算前不能删掉 Auto 来源,否则失败 tab 会被当成手动 tab 而永不重试。
4437
+ await closeSidebarResource.call(sidebarRight, record.tabId);
4438
+ closed = true;
4439
+ } catch {
4440
+ closed = false;
4441
+ }
4436
4442
  if (closed === true) {
4437
4443
  /**
4438
4444
  * 关闭成功后一起收口这个 key 上尚未结算的分栏事务。
@@ -10494,9 +10500,13 @@ window.__ModuleLoader__.load({
10494
10500
  ".deg_settings_title{font-size:20px;font-weight:600;line-height:28px;margin:0}" +
10495
10501
  ".deg_settings_hint{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);margin:0}" +
10496
10502
  ".deg_settings_row{display:grid;grid-template-columns:minmax(0,1fr) minmax(180px,280px);align-items:center;gap:20px;padding:14px 0;border-bottom:1px solid var(--dsw-alias-border-l2)}" +
10497
- ".deg_settings_management_row{grid-template-columns:minmax(0,1fr) auto minmax(180px,280px)}" +
10498
- ".deg_settings_management_row>.deg_settings_checkbox{white-space:nowrap}" +
10499
- ".deg_settings_management_row>.deg_settings_control{justify-self:end}" +
10503
+ // 管理行的四个控件按内容宽度排列,并用 space-between 吸收剩余空间,让 599px/760px 容器中的间距均匀。
10504
+ ".deg_settings_management_row{display:flex;align-items:center;justify-content:space-between;column-gap:16px;row-gap:12px;flex-wrap:nowrap;min-width:0}" +
10505
+ // 统一 29px 行高与数字输入框,保证四个控件的 bounding box 顶部对齐,同时保留可用点击高度。
10506
+ ".deg_settings_management_row>.deg_settings_checkbox{flex:0 0 auto;white-space:nowrap;height:29px;box-sizing:border-box}" +
10507
+ ".deg_settings_management_row>.deg_settings_control{flex:0 0 auto;justify-self:auto;min-width:max-content;white-space:nowrap;height:29px;box-sizing:border-box}" +
10508
+ // 520px 以下取消强制单行并左对齐,允许控件按整项换行;固定内容宽度避免标签折行、溢出或输入框被压缩。
10509
+ "@media (max-width:520px){.deg_settings_management_row{flex-wrap:wrap;justify-content:flex-start}}" +
10500
10510
  ".deg_settings_label{font-size:13px;line-height:20px;color:var(--dsw-alias-label-primary)}" +
10501
10511
  ".deg_settings_control{display:flex;align-items:center;gap:10px;min-width:0}" +
10502
10512
  ".deg_settings_control input[type=range]{width:100%;accent-color:var(--dsw-alias-interactive-bg-primary,var(--dsw-alias-label-primary))}" +
@@ -10664,7 +10674,40 @@ window.__ModuleLoader__.load({
10664
10674
  // 隐藏宿主原生 svg(不替换它的内容,React 重渲染会按 fiber 位置回写 path),
10665
10675
  // 并让 DEG 自己插入的 svg 只占图标列(不参与伸缩),label 位置因此不变。
10666
10676
  "[data-deg-settings-nav-native]{display:none}" +
10667
- "[data-deg-settings-nav-icon]{flex:none}";
10677
+ "[data-deg-settings-nav-icon]{flex:none}" +
10678
+ // ── 插件详情摘要(DSH 插件管理器的组合包页)────────────────────────────
10679
+ // 全部规则都以 deg_pd_ 前缀命名,且只用宿主已有的 CSS 变量,不依赖宿主 CSS module 的哈希类名。
10680
+ ".deg_pd{display:flex;flex-direction:column;gap:12px;width:min(100%,760px);min-width:0;box-sizing:border-box;color:var(--dsw-alias-label-primary)}" +
10681
+ ".deg_pd_title{font-size:14px;line-height:22px;font-weight:600;margin:0}" +
10682
+ ".deg_pd_roles{display:flex;flex-direction:column;gap:0;list-style:none;margin:0;padding:0}" +
10683
+ ".deg_pd_roles_header{display:grid;grid-template-columns:minmax(84px,.7fr) minmax(0,1.6fr) auto;gap:10px;padding:6px 0 2px;border-top:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary));font-size:11px;line-height:16px}" +
10684
+ ".deg_pd_roles_header span{min-width:0}" +
10685
+ ".deg_pd_roles_header span:last-child{text-align:right}" +
10686
+ ".deg_pd_role{display:grid;grid-template-columns:minmax(84px,.7fr) minmax(0,1.6fr) auto;align-items:center;gap:10px;min-height:30px;padding:5px 0;border-bottom:1px solid var(--dsw-alias-border-l2)}" +
10687
+ ".deg_pd_role:last-child{border-bottom:0}" +
10688
+ ".deg_pd_role_name{min-width:0;font-size:13px;line-height:20px;color:var(--dsw-alias-label-primary);overflow-wrap:anywhere}" +
10689
+ ".deg_pd_role_value{min-width:0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}" +
10690
+ ".deg_pd_role_value[title]{cursor:help}" +
10691
+ ".deg_pd_role_state{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary));white-space:nowrap;text-align:right}" +
10692
+ ".deg_pd_role[data-deg-role-state=configured] .deg_pd_role_state{color:var(--dsw-alias-state-success-primary,var(--dsw-alias-label-secondary))}" +
10693
+ ".deg_pd_role[data-deg-role-state=disabled] .deg_pd_role_name,.deg_pd_role[data-deg-role-state=disabled] .deg_pd_role_value{color:var(--dsw-alias-label-secondary)}" +
10694
+ // 不完整配置(provider 已选、model 未选)用警示色表达"还没配完",绝不与已配置同色。
10695
+ ".deg_pd_role[data-deg-role-state=incomplete] .deg_pd_role_state{color:var(--dsw-alias-state-warning-primary,var(--dsw-alias-label-secondary))}" +
10696
+ ".deg_pd_providers{display:flex;align-items:flex-start;gap:10px;min-width:0;flex-wrap:wrap}" +
10697
+ ".deg_pd_label{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);flex:0 0 auto}" +
10698
+ ".deg_pd_providers_list{display:flex;align-items:flex-start;gap:6px;flex:1 1 auto;flex-wrap:wrap;min-width:0;max-width:100%}" +
10699
+ ".deg_pd_tag{max-width:100%;box-sizing:border-box;font-size:11px;line-height:16px;padding:1px 6px;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;color:var(--dsw-alias-label-primary);overflow-wrap:anywhere}" +
10700
+ ".deg_pd_tag[data-deg-provider-known=false]{border-style:dashed;color:var(--dsw-alias-label-secondary)}" +
10701
+ ".deg_pd_empty{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}" +
10702
+ ".deg_pd_status{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);margin:0;overflow-wrap:anywhere}" +
10703
+ ".deg_pd_actions{display:flex;align-items:flex-start;gap:10px;flex-wrap:wrap;padding-top:2px}" +
10704
+ ".deg_pd_actions .deg_pd_open{flex:0 0 auto;min-width:auto}" +
10705
+ ".deg_pd_actions .deg_pd_open:hover:not(:disabled),.deg_pd_actions .deg_pd_open:focus-visible:not(:disabled){color:var(--dsw-alias-label-on-primary,var(--dsw-alias-bg-layer-1));background:var(--dsw-alias-interactive-bg-primary,var(--dsw-alias-label-primary));border-color:var(--dsw-alias-interactive-bg-primary,var(--dsw-alias-label-primary));filter:brightness(1.08)}" +
10706
+ ".deg_pd_actions .deg_pd_open:focus-visible{outline:2px solid var(--dsw-alias-interactive-bg-primary,var(--dsw-alias-label-primary));outline-offset:2px}" +
10707
+ ".deg_pd_actions .deg_pd_open:disabled{opacity:1;color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary));background:transparent;border-color:var(--dsw-alias-border-l2)}" +
10708
+ ".deg_pd_note{min-width:0;flex:1 1 220px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere}" +
10709
+ // 窄屏将状态移到 Provider/模型下一行,避免六角色被三列硬挤成不可读的省略号。
10710
+ "@media (max-width:480px){.deg_pd_roles_header,.deg_pd_role{grid-template-columns:minmax(72px,auto) minmax(0,1fr);column-gap:8px}.deg_pd_roles_header{padding-top:5px}.deg_pd_roles_header span:last-child{grid-column:2;text-align:left}.deg_pd_role{align-items:start;row-gap:2px;padding:7px 0}.deg_pd_role_name{grid-row:1 / span 2;align-self:start}.deg_pd_role_value{white-space:normal;overflow-wrap:anywhere;text-overflow:clip}.deg_pd_role_state{grid-column:2;justify-self:start;text-align:left}}";
10668
10711
 
10669
10712
 
10670
10713
  const cssTagId = "dsh-expert-group/settings.module.css";
@@ -17534,6 +17577,476 @@ window.__ModuleLoader__.load({
17534
17577
  };
17535
17578
  }
17536
17579
 
17580
+ // ── 插件详情(DSH 插件管理器的组合包页)上的 DEG 配置摘要 ────────────────
17581
+ /**
17582
+ * 组合包详情页的配置 slot 名与 key。
17583
+ *
17584
+ * DSH 的 ui-plugin-manager 把 `plugins.bundle.config` 声明为 keyed slot,以组合包的
17585
+ * **npm 包名**为 key,并把 `view: 'page'` 渲染在组合包页面的描述与行之间——这正是
17586
+ * 本插件详情摘要的落点。key 与 settings 分区 id 同值(profile 条目 id 就是包名),
17587
+ * 因此复用同一份字面量,不新增第二处硬编码。
17588
+ */
17589
+ const DEG_PLUGIN_DETAIL_SLOT_NAME = "plugins.bundle.config";
17590
+ const DEG_PLUGIN_DETAIL_ENTRY_KEY = DEG_SETTINGS_SECTION_ID;
17591
+ /** 设置 Shell 的侧栏席位:openSection 能力句柄的唯一来源。 */
17592
+ const DEG_SETTINGS_SIDEBAR_SLOT_NAME = "sidebar.settings";
17593
+ /** DEG 自己注册的设置分区所在的 slot。 */
17594
+ const DEG_SETTINGS_SECTION_SLOT_NAME = "settings.section";
17595
+
17596
+ /**
17597
+ * 「打开 DEG 设置」各失败原因的用户可见提示。
17598
+ *
17599
+ * 每条都表达「为什么不能跳转」,且都不包含宿主异常原文(异常只按 open-failed 归类),
17600
+ * 避免把宿主内部信息回显到界面上。
17601
+ */
17602
+ const DEG_SETTINGS_OPEN_FAILURE_TEXT = Object.freeze({
17603
+ "slots-unavailable": "宿主未提供 slot 账本读取面,暂时无法跳转到 DEG 设置",
17604
+ "sidebar-entry-missing": "未找到设置侧栏入口,暂时无法跳转到 DEG 设置",
17605
+ "sidebar-entry-ambiguous": "设置侧栏入口不唯一,已停用跳转",
17606
+ "store-missing": "设置侧栏入口缺少顶层导航 store,已停用跳转",
17607
+ "open-section-missing": "设置侧栏入口缺少 openSection 动作,已停用跳转",
17608
+ "section-missing": "DEG 设置分区尚未注册,暂时无法跳转",
17609
+ "section-ambiguous": "DEG 设置分区注册重复,已停用跳转",
17610
+ // 账本条目上的属性读取(entry/options/store/actions/create/instance.actions 的 getter)抛错:
17611
+ // 归到同一类泛化提示,既不回显宿主异常原文,也不假装能力可用。
17612
+ "ledger-unreadable": "Slot 账本读取异常,已停用跳转",
17613
+ "open-failed": "打开 DEG 设置失败,请手动打开设置页",
17614
+ });
17615
+
17616
+ /** 读一个 slot 的账本;宿主没有该读面或读面抛错时返回 null(调用方 fail-closed)。 */
17617
+ function readSlotLedger(slots, name) {
17618
+ try {
17619
+ if (slots === null || typeof slots !== "object" || typeof slots.entries !== "function") return null;
17620
+ const entries = slots.entries(name);
17621
+ return Array.isArray(entries) ? entries : [];
17622
+ } catch {
17623
+ // 宿主替身 / 代理拒答:按"读不到账本"处理,绝不让摘要或设置页因此崩溃。
17624
+ return null;
17625
+ }
17626
+ }
17627
+
17628
+ /**
17629
+ * 从设置侧栏条目的 store 句柄里解析 openSection。
17630
+ *
17631
+ * DSH 的 ui-settings-general 用 `defineStore` 造 shell store,再以
17632
+ * `{ ...handle, create: () => instance }` 的形式注册到 `sidebar.settings` 席位;DSH
17633
+ * 0.2.0-rc.2 把它投影为条目的**顶层 `store`**(不是 `entry.options.store`)。句柄本身
17634
+ * 没有 actions,实例(`create()` 的返回值,且恒为同一份壳实例)才带 `actions`。
17635
+ * 因此这里按「句柄 → 实例 → actions」解析,并兼容直接把 actions 挂在 store 上的形态。
17636
+ * 只有 `store.create()` 的**调用**在本函数内捕获(返回 null → open-section-missing);
17637
+ * 属性 getter 抛错(store.actions / store.create / instance.actions / actions.openSection)
17638
+ * 刻意向外抛出,由调用方归到 ledger-unreadable 这一类——两种失败类别不能混为一谈。
17639
+ * 任一环节缺失都不猜、不回退到别的动作。
17640
+ */
17641
+ function resolveSettingsShellOpenSection(store) {
17642
+ if (store === null || typeof store !== "object") return null;
17643
+ const direct = store.actions;
17644
+ if (direct !== null && typeof direct === "object" && typeof direct.openSection === "function") {
17645
+ return direct.openSection.bind(direct);
17646
+ }
17647
+ if (typeof store.create !== "function") return null;
17648
+ let instance = null;
17649
+ try {
17650
+ instance = store.create();
17651
+ } catch {
17652
+ return null;
17653
+ }
17654
+ const actions = instance?.actions;
17655
+ if (actions === null || typeof actions !== "object" || typeof actions.openSection !== "function") return null;
17656
+ return actions.openSection.bind(actions);
17657
+ }
17658
+
17659
+ /**
17660
+ * 「打开 DEG 设置」的严格能力适配器。
17661
+ *
17662
+ * 当前 DSH 不提供公开的设置分区跳转 API,唯一可达的链路是设置 Shell 注册在
17663
+ * `sidebar.settings` 席位上的 store 句柄(**未文档化的宿主内部句柄**,用户已确认接受
17664
+ * 该兼容风险)。本函数是本插件里**唯一**接触该句柄的地方,调用方只拿到一个已经验证过的
17665
+ * openSection 函数。
17666
+ *
17667
+ * 每次调用都重新读取账本(组件每帧渲染、每次点击都会再读一次),因此 HMR 或面板重挂载
17668
+ * 换掉 store 后不会继续调用旧句柄;任何一项校验不过都 fail-closed 并给出原因:
17669
+ * - 宿主缺少 `slots.entries` 读面 / 读面抛错 → slots-unavailable;
17670
+ * - 账本条目属性 getter 抛错(entry.options / **entry.store** / store.actions / store.create /
17671
+ * instance.actions / actions.openSection)→ ledger-unreadable(同一类泛化提示);
17672
+ * - `sidebar.settings` 没有条目 → sidebar-entry-missing;多于一条 → sidebar-entry-ambiguous;
17673
+ * - 条目**顶层**缺少 store(entry.store 非对象)→ store-missing;
17674
+ * store 解不出 openSection → open-section-missing
17675
+ * (含 `store.create()` 调用抛错这一既有覆盖);
17676
+ * - DEG 自己的 `settings.section` 注册缺失 → section-missing;重复 → section-ambiguous。
17677
+ * 最后一项保证按钮不会把用户送到一个不存在的分区。
17678
+ *
17679
+ * @param {object} ctx 客户端插件上下文。
17680
+ * @returns {{available: boolean, reason: string|null, message: string|null, openSection: (Function|null)}}
17681
+ */
17682
+ function resolveDegSettingsSectionOpener(ctx) {
17683
+ const unavailable = (reason) => ({
17684
+ available: false,
17685
+ reason,
17686
+ message: DEG_SETTINGS_OPEN_FAILURE_TEXT[reason],
17687
+ openSection: null,
17688
+ });
17689
+ let slots = null;
17690
+ try {
17691
+ slots = ctx?.slots ?? null;
17692
+ } catch {
17693
+ slots = null;
17694
+ }
17695
+ const sectionEntries = readSlotLedger(slots, DEG_SETTINGS_SECTION_SLOT_NAME);
17696
+ const sidebarEntries = readSlotLedger(slots, DEG_SETTINGS_SIDEBAR_SLOT_NAME);
17697
+ if (sectionEntries === null || sidebarEntries === null) return unavailable("slots-unavailable");
17698
+ // 账本读取整段护栏:Host 的账本条目可能是代理对象,甚至 slots.entries 可以返回
17699
+ // Array.isArray 为真的**数组代理**。因此不只是 entry.options / entry.store 的 getter,
17700
+ // 数组自身的 length 与下标读取也可能抛错——它们必须一起归到同一类 fail-closed,
17701
+ // 绝不让一次渲染或一次点击把摘要打崩。
17702
+ let ownSectionCount = 0;
17703
+ let sidebarLength = 0;
17704
+ try {
17705
+ ownSectionCount = sectionEntries.filter((entry) => entry?.options?.id === DEG_SETTINGS_SECTION_ID).length;
17706
+ sidebarLength = sidebarEntries.length;
17707
+ } catch {
17708
+ return unavailable("ledger-unreadable");
17709
+ }
17710
+ if (ownSectionCount === 0) return unavailable("section-missing");
17711
+ if (ownSectionCount > 1) return unavailable("section-ambiguous");
17712
+ if (sidebarLength === 0) return unavailable("sidebar-entry-missing");
17713
+ if (sidebarLength > 1) return unavailable("sidebar-entry-ambiguous");
17714
+ let store = null;
17715
+ let openSection = null;
17716
+ try {
17717
+ // DSH 0.2.0-rc.2 的 StoredEntry 把 store 放在**条目顶层**(entry.store);
17718
+ // entry.options 只承载 key/order/label/priority,读 options.store 只会拿到 undefined。
17719
+ store = sidebarEntries[0]?.store;
17720
+ if (store !== null && store !== undefined && typeof store === "object") {
17721
+ openSection = resolveSettingsShellOpenSection(store);
17722
+ }
17723
+ } catch {
17724
+ // entry.store / actions / create / instance.actions 的 getter 抛错:与"字段缺失"分开,
17725
+ // 统一报账本读取异常(泛化提示,不回显宿主异常原文)。
17726
+ return unavailable("ledger-unreadable");
17727
+ }
17728
+ if (store === null || store === undefined || typeof store !== "object") return unavailable("store-missing");
17729
+ if (openSection === null) return unavailable("open-section-missing");
17730
+ return { available: true, reason: null, message: null, openSection };
17731
+ }
17732
+
17733
+ /**
17734
+ * 插件详情摘要的角色状态投影(与设置页的 `useEnabledSettingsRoles` **刻意分离**)。
17735
+ *
17736
+ * 设置页在 meta 缺失时回退历史默认五角色是既有行为,摘要不能借它:meta 没读到就不知道谁在跑,
17737
+ * 猜错会把未启用角色的 provider 写进"活跃 Provider"汇总,比留白更糟。因此只在 Host meta
17738
+ * **成功返回且带有效 activeRoles/roles 数组**时才认定启用角色;Observer 仍按 enableObserver
17739
+ * 合并(与设置页同一规则)。返回的 status 只表达"读到/读取中/不可用"三种事实。
17740
+ *
17741
+ * @param {object|null} hostMeta Host meta 响应体(useHostMeta 只在 ok:true 时落值)。
17742
+ * @param {string|null} hostMetaError 读取失败原因;仅用于区分"读取中"与"不可用",不回显。
17743
+ * @returns {{status: "loading"|"ready"|"unavailable", roles: string[]|null}}
17744
+ */
17745
+ function resolvePluginDetailRoleStatus(hostMeta, hostMetaError) {
17746
+ if (hostMeta === null && hostMetaError === null) return { status: "loading", roles: null };
17747
+ const configured = hostMeta !== null && typeof hostMeta === "object"
17748
+ ? (Array.isArray(hostMeta.activeRoles) ? hostMeta.activeRoles : (Array.isArray(hostMeta.roles) ? hostMeta.roles : null))
17749
+ : null;
17750
+ if (configured === null) return { status: "unavailable", roles: null };
17751
+ const enabled = configured.filter((role) => STATUS_ROLE_IDS.includes(role));
17752
+ if (hostMeta.enableObserver === true && !enabled.includes("observer")) enabled.push("observer");
17753
+ return { status: "ready", roles: enabled.filter((role, index) => enabled.indexOf(role) === index) };
17754
+ }
17755
+
17756
+ /**
17757
+ * 插件详情摘要的纯投影(界面与测试共用同一实现)。
17758
+ *
17759
+ * 输入只有已经读到的快照,不触发任何请求:
17760
+ * @param {object} [input]
17761
+ * @param {"ready"|"loading"|"unavailable"} [input.settingsStatus] 配置表单快照状态。
17762
+ * @param {object} [input.settingsValue] 配置表单值(`defaults[role] = { provider, model }`)。
17763
+ * @param {string} [input.catalogStatus] 模型目录状态(ready 之外的取值一律按"目录不可用")。
17764
+ * @param {object[]} [input.catalogGroups] 模型目录 groups(`{ id, name }`)。
17765
+ * @param {string[]|null} [input.enabledRoles] 当前启用角色;null 表示角色表尚未读到。
17766
+ * @param {"ready"|"loading"|"unavailable"} [input.roleStatus] 角色表读取结论(见
17767
+ * resolvePluginDetailRoleStatus);缺省时按 enabledRoles 是否为数组推断。
17768
+ * @returns {{settingsStatus: string, roleStatus: string, statusText: string|null, roles: object[], activeProviders: object[], providerIds: string[], activeProviderText: string}}
17769
+ *
17770
+ * 角色状态判定顺序(前者优先):
17771
+ * 1. 配置未就绪 → loading(文案区分"配置不可用"与"读取中");
17772
+ * 2. 角色表仍在读取 → loading;
17773
+ * 3. 角色表不可用(meta 失败/缺字段)→ unknown,不猜启用状态;
17774
+ * 4. 角色未启用 → disabled;
17775
+ * 5. 没有 provider 选择 → unconfigured;
17776
+ * 6. provider 已选但 model 为空(不完整配置)→ incomplete,不算"正在使用";
17777
+ * 7. 其余(provider + model 都非空)→ configured。
17778
+ * 活跃 provider 只取「启用角色且 provider、model 都非空」的集合,按角色顺序去重;目录缺失或
17779
+ * 目录里没有该 id 时保留原始 id 并标记 `known: false`,绝不用目录里的其它 provider 冒充。
17780
+ */
17781
+ function describeDegPluginDetailSummary(input = {}) {
17782
+ const settingsStatus = typeof input.settingsStatus === "string" ? input.settingsStatus : "loading";
17783
+ const settingsValue = input.settingsValue;
17784
+ const catalogReady = input.catalogStatus === "ready";
17785
+ const catalogGroups = catalogReady && Array.isArray(input.catalogGroups) ? input.catalogGroups : [];
17786
+ const enabledRoles = Array.isArray(input.enabledRoles) ? input.enabledRoles : null;
17787
+ const requestedRoleStatus = input.roleStatus === "ready" || input.roleStatus === "unavailable" || input.roleStatus === "loading"
17788
+ ? input.roleStatus
17789
+ : (enabledRoles === null ? "loading" : "ready");
17790
+ // "已读到"却没有角色数组是矛盾输入(调用方读错了面):按不可用处理,
17791
+ // 绝不当成"全部未启用"——后者会把未确认的默认角色配置从汇总里静默剔除。
17792
+ const roleStatus = requestedRoleStatus === "ready" && enabledRoles === null ? "unavailable" : requestedRoleStatus;
17793
+ const settingsPending = settingsStatus !== "ready";
17794
+ const defaults = settingsValue !== null && typeof settingsValue === "object" ? settingsValue.defaults : null;
17795
+
17796
+ const providerInfoOf = (providerId) => {
17797
+ if (providerId.length === 0) return { label: "", known: false };
17798
+ const group = catalogGroups.find((candidate) => candidate?.id === providerId);
17799
+ const name = typeof group?.name === "string" && group.name.trim().length > 0 ? group.name : "";
17800
+ // 目录拿不到显示名(目录缺失 / 目录不含该 id / name 为空)时保留原始 id 并标注未知。
17801
+ return name.length > 0 ? { label: name, known: true } : { label: providerId, known: false };
17802
+ };
17803
+
17804
+ const roles = [];
17805
+ const activeProviders = [];
17806
+ const seenProviders = new Set();
17807
+ for (const role of STATUS_ROLE_IDS) {
17808
+ const selection = defaults !== null && typeof defaults === "object" ? defaults[role] : null;
17809
+ const provider = typeof selection?.provider === "string" && selection.provider.length > 0 ? selection.provider : "";
17810
+ const model = typeof selection?.model === "string" ? selection.model : "";
17811
+ const { label: providerLabel, known: providerKnown } = providerInfoOf(provider);
17812
+ const enabledKnown = roleStatus === "ready";
17813
+ const enabled = enabledKnown && enabledRoles.includes(role) === true;
17814
+ let state;
17815
+ if (settingsPending || roleStatus === "loading") state = "loading";
17816
+ else if (roleStatus === "unavailable") state = "unknown";
17817
+ else if (enabled !== true) state = "disabled";
17818
+ else if (provider.length === 0) state = "unconfigured";
17819
+ else if (model.length === 0) state = "incomplete";
17820
+ else state = "configured";
17821
+ let stateText;
17822
+ if (state === "loading") stateText = settingsStatus === "unavailable" ? "配置不可用" : "读取中";
17823
+ else if (state === "unknown") stateText = "角色状态未知";
17824
+ else if (state === "disabled") stateText = "未启用";
17825
+ else if (state === "unconfigured") stateText = "未配置";
17826
+ else if (state === "incomplete") stateText = "待选模型";
17827
+ else stateText = "已配置";
17828
+ if (state === "configured" && seenProviders.has(provider) !== true) {
17829
+ seenProviders.add(provider);
17830
+ activeProviders.push({ id: provider, label: providerLabel, known: providerKnown });
17831
+ }
17832
+ roles.push({
17833
+ role,
17834
+ label: STATUS_ROLE_LABELS[role] ?? role,
17835
+ enabled,
17836
+ state,
17837
+ stateText,
17838
+ provider,
17839
+ providerLabel,
17840
+ providerKnown,
17841
+ model,
17842
+ // 角色行的 provider/model 展示面:没有 provider 时留空,由状态文案表达未配置。
17843
+ valueText: provider.length === 0 ? "" : (model.length > 0 ? providerLabel + " / " + model : providerLabel),
17844
+ });
17845
+ }
17846
+ // 卡片级状态行:只表达"读到/读取中/不可用",绝不回显 Host 或 RPC 的错误原文。
17847
+ const statusText = settingsStatus === "unavailable"
17848
+ ? "配置表单不可用,无法读取角色配置。"
17849
+ : (settingsStatus !== "ready"
17850
+ ? "配置读取中…"
17851
+ : (roleStatus === "loading"
17852
+ ? "角色状态读取中…"
17853
+ : (roleStatus === "unavailable" ? "角色状态不可用,已停用活跃角色判定。" : null)));
17854
+ const providerIds = activeProviders.map((provider) => provider.id);
17855
+ return {
17856
+ settingsStatus,
17857
+ roleStatus,
17858
+ statusText,
17859
+ roles,
17860
+ activeProviders,
17861
+ providerIds,
17862
+ activeProviderText: activeProviders.length === 0
17863
+ ? "无"
17864
+ : activeProviders.map((provider) => provider.label).join("、"),
17865
+ };
17866
+ }
17867
+
17868
+ /**
17869
+ * 插件详情摘要卡片:六角色表 + 活跃 provider + 真实设置跳转入口。
17870
+ *
17871
+ * 数据面全部来自公开 seam:配置表单(只读订阅)、模型目录(显示名),以及 Host meta
17872
+ * (活跃角色,与设置页同一个只读请求)。跳转能力每帧重算(见
17873
+ * resolveDegSettingsSectionOpener),点击时再算一次;调用抛异常时立即翻转成禁用,
17874
+ * 绝不把失败显示成成功,也绝不改成打开组合包详情。
17875
+ */
17876
+ function DegPluginDetailSummary({ settingsForm, modelCatalog, ctx }) {
17877
+ const subscribeSettings = react.useMemo(
17878
+ () => (typeof settingsForm?.subscribe === "function" ? settingsForm.subscribe.bind(settingsForm) : () => () => {}),
17879
+ [settingsForm],
17880
+ );
17881
+ const readSettings = react.useMemo(
17882
+ () => (typeof settingsForm?.getSnapshot === "function" ? settingsForm.getSnapshot.bind(settingsForm) : () => void 0),
17883
+ [settingsForm],
17884
+ );
17885
+ const settingsSnapshot = react.useSyncExternalStore(subscribeSettings, readSettings);
17886
+ const subscribeCatalog = react.useMemo(
17887
+ () => (typeof modelCatalog?.subscribe === "function" ? modelCatalog.subscribe.bind(modelCatalog) : () => () => {}),
17888
+ [modelCatalog],
17889
+ );
17890
+ const readCatalog = react.useMemo(
17891
+ () => (typeof modelCatalog?.getSnapshot === "function" ? modelCatalog.getSnapshot.bind(modelCatalog) : () => void 0),
17892
+ [modelCatalog],
17893
+ );
17894
+ const catalogState = react.useSyncExternalStore(subscribeCatalog, readCatalog);
17895
+ // 只读一次 meta:活跃角色与设置页共用同一个请求,摘要不额外新增 RPC 形状。
17896
+ // 摘要刻意**不用**设置页的 useEnabledSettingsRoles:那条路径在 meta 缺失时会回退历史默认
17897
+ // 五角色(设置页既有行为,保持不变),而摘要一旦跟着回退就会把未确认启用角色的 provider
17898
+ // 写进"活跃 Provider"。这里只认 meta 的成功响应,其余情况显式标未知。
17899
+ const [hostMeta, , hostMetaError] = useHostMeta(ctx);
17900
+ const roleStatus = react.useMemo(
17901
+ () => resolvePluginDetailRoleStatus(hostMeta, hostMetaError),
17902
+ [hostMeta, hostMetaError],
17903
+ );
17904
+ const [failure, setFailure] = react.useState(null);
17905
+ react.useEffect(() => {
17906
+ // 目录调用是幂等的单飞请求,且与设置页共享同一实例:这里只为拿到 provider 显示名。
17907
+ try {
17908
+ void modelCatalog?.load?.();
17909
+ } catch {
17910
+ // 目录读取失败只影响显示名:原始 id 的回退路径照常工作。
17911
+ }
17912
+ }, [modelCatalog]);
17913
+ const model = describeDegPluginDetailSummary({
17914
+ settingsStatus: typeof settingsSnapshot?.status === "string" ? settingsSnapshot.status : "loading",
17915
+ settingsValue: settingsSnapshot?.value,
17916
+ catalogStatus: typeof catalogState?.status === "string" ? catalogState.status : "idle",
17917
+ catalogGroups: catalogState?.groups,
17918
+ roleStatus: roleStatus.status,
17919
+ enabledRoles: roleStatus.roles,
17920
+ });
17921
+ const capability = resolveDegSettingsSectionOpener(ctx);
17922
+ const noteText = failure?.message ?? (capability.available === true ? null : capability.message);
17923
+ const noteReason = failure?.reason ?? (capability.available === true ? null : capability.reason);
17924
+ /**
17925
+ * 点击处理:同步异常与异步拒绝都必须 fail-closed(禁用 + open-failed 提示)。
17926
+ *
17927
+ * 三段路径的边界:
17928
+ * 1. **同步返回**(当前 Host 的真实形态):调用后立刻清提示并返回 undefined——
17929
+ * 绝不把同步路径包进 `Promise.resolve().then(...)`,那会给非 Promise 返回值平白加一次
17930
+ * 微任务延迟,也让点击的返回类型从 undefined 变成 Promise;
17931
+ * 2. **同步抛错**:按 open-failed 处理;
17932
+ * 3. **thenable(Promise/rejected Promise)**:只有真正的 thenable 才挂结算回调,且**在同一个
17933
+ * tick 内**挂上 rejection 处理——否则未处理的 rejection 会冒到宿主页面,按钮还会错误地
17934
+ * 停在"看起来可用"的状态。成功结算不清提示:成功路径已经在第 1 步清过,而"后到的拒绝
17935
+ * 覆盖先到的成功"必须保持(fail-closed 只允许更保守,不允许被迟到的成功解锁)。
17936
+ * 任何一步失败都绝不回退到 openBundle 之类的替代动作。
17937
+ */
17938
+ const openSettings = () => {
17939
+ // 点击时重新读取:首帧渲染之后账本可能已经变化(HMR / 面板重挂载)。
17940
+ const current = resolveDegSettingsSectionOpener(ctx);
17941
+ if (current.available !== true) {
17942
+ setFailure({ reason: current.reason, message: current.message });
17943
+ return;
17944
+ }
17945
+ const failClosed = () => {
17946
+ setFailure({ reason: "open-failed", message: DEG_SETTINGS_OPEN_FAILURE_TEXT["open-failed"] });
17947
+ };
17948
+ let result;
17949
+ try {
17950
+ result = current.openSection(DEG_SETTINGS_SECTION_ID);
17951
+ } catch {
17952
+ failClosed();
17953
+ return;
17954
+ }
17955
+ setFailure(null);
17956
+ // 只对对象/函数探 then:原始值(undefined/void 返回值)连属性读取都不需要。
17957
+ if (result === null || (typeof result !== "object" && typeof result !== "function")) return;
17958
+ let then = null;
17959
+ try {
17960
+ then = result.then;
17961
+ } catch {
17962
+ // then 读取本身抛错的畸形 thenable:按调用失败处理,不当成功。
17963
+ failClosed();
17964
+ return;
17965
+ }
17966
+ if (typeof then !== "function") return;
17967
+ try {
17968
+ then.call(result, undefined, failClosed);
17969
+ } catch {
17970
+ failClosed();
17971
+ }
17972
+ };
17973
+ return react.createElement(
17974
+ "section",
17975
+ { className: "deg_pd", "data-deg-plugin-summary": "true", "aria-label": "DEG 配置摘要" },
17976
+ react.createElement("h4", { className: "deg_pd_title" }, "DEG 配置摘要"),
17977
+ react.createElement(
17978
+ "div",
17979
+ { className: "deg_pd_providers" },
17980
+ react.createElement("span", { className: "deg_pd_label" }, "活跃角色使用的 Provider"),
17981
+ react.createElement("span", {
17982
+ className: "deg_pd_providers_list",
17983
+ "data-deg-active-providers": model.providerIds.join("|"),
17984
+ }, model.activeProviders.length === 0
17985
+ ? react.createElement("span", { className: "deg_pd_empty" }, model.activeProviderText)
17986
+ : model.activeProviders.map((provider) => react.createElement("span", {
17987
+ key: provider.id,
17988
+ className: "deg_pd_tag",
17989
+ "data-deg-active-provider": provider.id,
17990
+ "data-deg-provider-known": provider.known ? "true" : "false",
17991
+ // 目录缺失或目录不含该 id 时的状态标注:保留原始 id,并说明为什么没有显示名。
17992
+ title: provider.known ? undefined : "模型目录未提供该 provider 的显示名,显示原始 id",
17993
+ "aria-label": provider.known ? undefined : provider.label + "(未知 Provider)",
17994
+ }, provider.label))),
17995
+ ),
17996
+ react.createElement("div", {
17997
+ className: "deg_pd_roles_header",
17998
+ "aria-hidden": "true",
17999
+ },
18000
+ react.createElement("span", null, "角色"),
18001
+ react.createElement("span", null, "Provider / 模型"),
18002
+ react.createElement("span", null, "状态"),
18003
+ ),
18004
+ react.createElement("ul", {
18005
+ className: "deg_pd_roles",
18006
+ "aria-label": "六个角色的 Provider、模型和配置状态",
18007
+ "data-deg-roles": model.roles.map((role) => role.role).join(","),
18008
+ }, model.roles.map((role) => react.createElement(
18009
+ "li",
18010
+ {
18011
+ key: role.role,
18012
+ className: "deg_pd_role",
18013
+ "data-deg-role": role.role,
18014
+ "data-deg-role-state": role.state,
18015
+ "data-deg-role-provider": role.provider,
18016
+ "data-deg-role-model": role.model,
18017
+ },
18018
+ react.createElement("span", { className: "deg_pd_role_name" }, role.label),
18019
+ react.createElement("span", { className: "deg_pd_role_value", title: role.valueText || undefined }, role.valueText),
18020
+ react.createElement("span", { className: "deg_pd_role_state" }, role.stateText),
18021
+ ))),
18022
+ model.statusText === null ? null : react.createElement("p", {
18023
+ className: "deg_pd_status",
18024
+ role: "status",
18025
+ // 角色状态结论(loading/ready/unavailable)的稳定标记:文案是可读面,这里给测试与自动化。
18026
+ "data-deg-role-status": model.roleStatus,
18027
+ }, model.statusText),
18028
+ react.createElement(
18029
+ "div",
18030
+ { className: "deg_pd_actions" },
18031
+ react.createElement("button", {
18032
+ type: "button",
18033
+ className: "deg_btn deg_btn_primary deg_pd_open",
18034
+ "data-deg-open-settings": "true",
18035
+ disabled: capability.available !== true || failure !== null,
18036
+ "aria-describedby": noteText === null ? undefined : "deg-pd-open-note",
18037
+ onClick: openSettings,
18038
+ }, "打开 DEG 设置"),
18039
+ noteText === null ? null : react.createElement("span", {
18040
+ id: "deg-pd-open-note",
18041
+ className: "deg_pd_note",
18042
+ role: "status",
18043
+ "data-deg-open-note": noteReason,
18044
+ }, noteText),
18045
+ ),
18046
+ );
18047
+ }
18048
+
18049
+
17537
18050
  // ── 插件主体:注册设置页与状态/元数据桥(通知由 Host 投递,客户端不挂载通知组件)。
17538
18051
  function apply(ctx) {
17539
18052
  ctx.effect(() => installImageSubmissionRoute(ctx), "dsh-expert-group: image submission route");
@@ -17604,15 +18117,21 @@ window.__ModuleLoader__.load({
17604
18117
  // describe 面唯一来源是 configForms.describe();服务缺失时传 null,该区域降级为只读提示。
17605
18118
  const modelConfigHost = createModelConfigHost(ctx, configForms);
17606
18119
  // 独立设置页由插件注册到 Host 的 settings.section ledger,导航和生命周期仍由设置 Shell 管理。
17607
- slots.inject("settings.section", () => slots.register({
17608
- name: "settings.section",
18120
+ slots.inject(DEG_SETTINGS_SECTION_SLOT_NAME, () => slots.register({
18121
+ name: DEG_SETTINGS_SECTION_SLOT_NAME,
17609
18122
  id: SETTINGS_ENTRY_ID,
17610
18123
  order: 25,
17611
18124
  label: () => SETTINGS_ENTRY_ID,
17612
18125
  inject: () => ({ settingsForm, modelCatalog, modelConfigHost, ctx }),
17613
18126
  }, DshExpertGroupSettings));
17614
18127
 
17615
-
18128
+ // 插件详情摘要挂在插件管理器的组合包页上:keyed slot,key 必须是组合包的 npm 包名。
18129
+ // 摘要只读(配置表单 + 模型目录 + meta),入口按钮走严格能力适配器真实跳转设置分区。
18130
+ slots.inject(DEG_PLUGIN_DETAIL_SLOT_NAME, () => slots.register({
18131
+ name: DEG_PLUGIN_DETAIL_SLOT_NAME,
18132
+ key: DEG_PLUGIN_DETAIL_ENTRY_KEY,
18133
+ inject: () => ({ settingsForm, modelCatalog, ctx }),
18134
+ }, DegPluginDetailSummary));
17616
18135
 
17617
18136
  // shell.overlay 只保留原生子代理行元数据;
17618
18137
  // fan-out 的可见控制面迁入 Expert Group,避免固定浮层与右侧任务树重复。
@@ -17843,6 +18362,17 @@ window.__ModuleLoader__.load({
17843
18362
  * - isDegSettingsNavRow:导航行的匹配纯逻辑(label 严格相等 + 含 svg);
17844
18363
  * - installDegSettingsNavIcon:observer 生命周期,apply 用 ctx.effect 绑定它。
17845
18364
  */
18365
+ /**
18366
+ * 插件详情摘要的公开面:
18367
+ * - describeDegPluginDetailSummary:六角色 / 活跃 provider 的纯投影(界面与测试共用同一实现);
18368
+ * - resolveDegSettingsSectionOpener:唯一一处接触 Host 内部 sidebar.settings store 句柄的
18369
+ * 严格能力适配器(未文档化方案,用户已确认接受兼容风险);
18370
+ * - DEG_PLUGIN_DETAIL_SLOT_NAME / DEG_PLUGIN_DETAIL_ENTRY_KEY:注册契约常量。
18371
+ */
18372
+ exports.describeDegPluginDetailSummary = describeDegPluginDetailSummary;
18373
+ exports.resolveDegSettingsSectionOpener = resolveDegSettingsSectionOpener;
18374
+ exports.DEG_PLUGIN_DETAIL_SLOT_NAME = DEG_PLUGIN_DETAIL_SLOT_NAME;
18375
+ exports.DEG_PLUGIN_DETAIL_ENTRY_KEY = DEG_PLUGIN_DETAIL_ENTRY_KEY;
17846
18376
  exports.DEG_SETTINGS_NAV_ICON_PATHS = DEG_SETTINGS_NAV_ICON_PATHS;
17847
18377
  exports.DEG_SETTINGS_NAV_ICON_FALLBACK_PATHS = DEG_SETTINGS_NAV_ICON_FALLBACK_PATHS;
17848
18378
  exports.isDegSettingsNavRow = isDegSettingsNavRow;