dsh-plugin-admin 0.4.2 → 0.4.3

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.
Files changed (3) hide show
  1. package/README.md +11 -11
  2. package/lib/client.js +66 -60
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,24 +1,24 @@
1
1
  # dsh-plugin-admin
2
2
 
3
- dsh web UI 插件:在浏览器设置弹窗(侧边栏底部“设置”)中新增一个**管理中心**设置页,内含**插件管理**、**会话管理**、**MCP 配置**三个 Tab,按需加载当前面板。
3
+ dsh web UI 插件:把管理能力拆进设置界面的既有结构——在官方**插件**设置页新增**扩展插件**页签,并在设置栏新增 **MCP服务器** **历史会话** 两个独立设置页,按需加载当前面板。
4
4
 
5
- - **🔌 插件管理**:
5
+ - **🔌 扩展插件**(插件设置页第三个页签,位于「插件配置」「插件列表」之后):
6
6
  - 顶部输入框支持 npm 包名(如 `dsh-xxx`)或本地绝对路径安装插件(回车或点击提交);
7
- - 列表展示当前 profile 下的全部 bundle 层(名称、版本,以及**内置 / 包安装 / 本地安装**标记;本地安装的插件额外显示其源路径,依据 profile 依赖清单中的 `link:` / `file:` / 绝对路径 spec 判定);
7
+ - 列表展示当前 profile 下的全部 bundle 层(名称、版本,以及**内置 / 包安装 / 本地安装**标记;本地安装的插件额外显示其源路径,依据 profile 依赖清单中的 `link:` / `file:` / 绝对路径 spec 判定),按 **内置 → 包安装 → 本地安装** 排序,卡片以一行两列栅格展示(窄屏回落单列);名称过长时单行省略显示,悬停可见完整名称;
8
8
  - **名称模糊搜索**:列表上方搜索框支持按插件名 / 版本号 / 本地路径模糊过滤(大小写不敏感子串匹配),可叠加「全部 / 扩展插件 / 系统内置」筛选胶囊,无结果时给出专属空状态提示;
9
9
  - 支持扩展插件一键卸载(带有优雅的行内二次确认);
10
- - **远程更新检测(自动)**:打开「插件管理」Tab 即自动按 profile 实际使用的 npm registry(`npm_config_registry` → 项目/用户 `.npmrc` → 官方源)查询每个 **registry 安装**的插件(跳过内置与本地路径安装)的 `latest` 版本,与本地版本比对——有新版时卡片显示琥珀色「⬆ 有新版本 vX.Y.Z」徽标并出现一键「⬆ 更新」按钮(`npm install <name>@latest`);工具栏「⬆ 检查更新」是唯一的手动重查入口,**强制绕过 5 分钟缓存直接重查 registry 并刷新缓存内容**(自动检查走缓存,手动检查才是真查);查询带超时与 5 分钟缓存(host 侧缓存,**缓存命中时按当前安装版本重算 `updateAvailable`**,重启 dsh 后首次打开自动重查),网络失败按条目提示而非报错;**「有新版本」提醒会保存下来(浏览器 localStorage 持久化)**——关闭再进入管理中心、甚至重启 dsh 后依然显示(含本次检查网络失败时),**只有点「更新」真正升级、或后续检查确认已是最新版本后,提醒才自动消除并同步清掉持久化记录;更新其中一个插件不会影响其余插件的提醒;升级完成后的自动复核同样**强制绕过缓存**重新查询,registry 在 TTL 窗口内刚发布更新的情况下也不会把刚升完级的插件误标回「有新版本」,检查与升级并发交错时,检查刚发现的提醒也不会被升级提交用旧快照覆盖(合并一律基于当前 state 的函数式更新,持久化由统一的镜像 effect 收口);
10
+ - **远程更新检测(自动)**:打开「扩展插件」页签即自动按 profile 实际使用的 npm registry(`npm_config_registry` → 项目/用户 `.npmrc` → 官方源)查询每个 **registry 安装**的插件(跳过内置与本地路径安装)的 `latest` 版本,与本地版本比对——有新版时卡片显示琥珀色「⬆ 有新版本 vX.Y.Z」徽标并出现一键「⬆ 更新」按钮(`npm install <name>@latest`);工具栏「⬆ 检查更新」是唯一的手动重查入口,**强制绕过 5 分钟缓存直接重查 registry 并刷新缓存内容**(自动检查走缓存,手动检查才是真查);查询带超时与 5 分钟缓存(host 侧缓存,**缓存命中时按当前安装版本重算 `updateAvailable`**,重启 dsh 后首次打开自动重查),网络失败按条目提示而非报错;**「有新版本」提醒会保存下来(浏览器 localStorage 持久化)**——关闭再进入页签、甚至重启 dsh 后依然显示(含本次检查网络失败时),**只有点「更新」真正升级、或后续检查确认已是最新版本后,提醒才自动消除并同步清掉持久化记录;更新其中一个插件不会影响其余插件的提醒;升级完成后的自动复核同样**强制绕过缓存**重新查询,registry 在 TTL 窗口内刚发布更新的情况下也不会把刚升完级的插件误标回「有新版本」,检查与升级并发交错时,检查刚发现的提醒也不会被升级提交用旧快照覆盖(合并一律基于当前 state 的函数式更新,持久化由统一的镜像 effect 收口);
11
11
  - 安装/卸载在 host 侧编排 profile 目录下的 pnpm 并自动同步 `package.json` 的 `dsh.profile.bundles` 清单(变更在重启 dsh 后生效)。
12
- - **💬 会话管理**:
12
+ - **💬 历史会话**(设置栏独立页):
13
13
  - **标题与内容摘要**:自动解析会话标题(`session/title` 或首条用户提问)并渲染首条消息的文本摘要气泡预览(带折行省略与多行保护);
14
14
  - **元信息直观展示**:显示对话消息计数(如 `5 条消息`)、工作目录路径(`📁 项目名 (完整路径)`)、创建时间及短 Session ID;
15
15
  - **状态呼吸灯与徽标**:绿色呼吸光晕(**会话在线**,悬停提示说明"在线 = 仍挂载于 dsh host 内存,非正在运行")、琥珀色标签(**已归档**)、灰色默认点(**已结束**);
16
16
  - **多维快捷搜索与筛选**:输入框支持同时模糊匹配标题、对话摘要、工作目录或 Session ID;支持按状态胶囊筛选(全部、在线、已归档、已结束);
17
17
  - **会话清理与恢复**:支持会话永久物理删除(行内二次防误触确认,彻底清理磁盘日志、工作区记账与归档集合);**在线会话不再需要重启 dsh**——「关停并删除」通过插件在 host 侧透明捕获的 `AgentHandle` 走 dsh 官方 dispose 链(停止 agent 运行、等待静止、注销 agent、从内存 SessionStore 移除并触发 `session/disposed`,持久化层随即 flush 缓冲事件并释放写路径),然后才删除日志,因此日志不会在下次 flush 复活;支持已归档会话一键取消归档,侧边栏即时联动刷新。
18
- - **🔌 MCP 配置**:管理中心中的独立 Tab——列出 profile 的 `cordis.patch.yml` 中所有 `@deepseek-ai/dsh-mcp-client` 实例,支持添加(stdio 子进程 / streamable-http)、编辑与移除,配置写回后重启 dsh 生效;新增**连通性检测**(🔌 测试)——host 侧按条目配置发起一次真实的 MCP 握手(`initialize` → `notifications/initialized` → `tools/list`),stdio 走新行分隔 JSON-RPC 子进程(Windows 下与 real 插件同样经 `cmd.exe` 解析 `.cmd` shim,超时强杀进程树、捕获 stderr 尾部便于诊断),streamable-http 走 `initialize` POST(兼容 SSE / 纯 JSON 响应);成功后行内显示服务器标识、**工具数量与工具名列表**,失败给出可诊断错误(命令不存在 / 连接被拒 / 超时等);若 `command` 写成整行调用(如 `npx -y fetcher-mcp`),探测会自动拆分执行并给出**警告**提示需拆分为 `command` + `args`(否则 dsh 启动该 MCP 服务器会失败);结果经 typert 边界 JSON 安全清洗,绝无 `undefined` 字段;
18
+ - **🔌 MCP服务器**(设置栏独立页):列出 profile 的 `cordis.patch.yml` 中所有 `@deepseek-ai/dsh-mcp-client` 实例,支持添加(stdio 子进程 / streamable-http)、编辑与移除,配置写回后重启 dsh 生效;新增**连通性检测**(🔌 测试)——host 侧按条目配置发起一次真实的 MCP 握手(`initialize` → `notifications/initialized` → `tools/list`),stdio 走新行分隔 JSON-RPC 子进程(Windows 下与 real 插件同样经 `cmd.exe` 解析 `.cmd` shim,超时强杀进程树、捕获 stderr 尾部便于诊断),streamable-http 走 `initialize` POST(兼容 SSE / 纯 JSON 响应);成功后行内显示服务器标识、**工具数量与工具名列表**,失败给出可诊断错误(命令不存在 / 连接被拒 / 超时等);若 `command` 写成整行调用(如 `npx -y fetcher-mcp`),探测会自动拆分执行并给出**警告**提示需拆分为 `command` + `args`(否则 dsh 启动该 MCP 服务器会失败);结果经 typert 边界 JSON 安全清洗,绝无 `undefined` 字段;
19
19
  - **测试结果的缓存边界**:只有**成功**的探测结果持久化到 localStorage(重开面板/重启后恢复上次状态并标注「缓存于」时刻);**失败(❌ 不通)只在当前会话内显示、不落盘**——瞬时故障不会变成跨会话的过期 ❌,旧版本遗留的失败记录会在载入时自动清除;新增表单中手输与既有条目相同的 id 会被**直接拒绝并提示换名**——host 的 upsert 按 id 原位覆盖,不拦截就是静默毁掉该条目的既有配置。
20
20
  - **侧边栏右键菜单(会话 & 工作区)**:
21
- - **会话行右键**:在原生菜单末尾追加**复制会话 ID**与**删除会话**(危险操作);删除按标题**精确匹配**解析目标,存在同名会话时拒绝执行并引导到管理中心按 ID 删除,且菜单项需**4 秒内两次点击**确认;删除复用 `sessionAdmin` 的安全语义——在线会话走 `closeSession`(先 dispose 捕获的 agent handle 再删日志,不再要求重启),非在线会话走 `deleteSession`,均定向 detach。
21
+ - **会话行右键**:在原生菜单末尾追加**复制会话 ID**与**删除会话**(危险操作);删除按标题**精确匹配**解析目标,存在同名会话时拒绝执行并引导到历史会话按 ID 删除,且菜单项需**4 秒内两次点击**确认;删除复用 `sessionAdmin` 的安全语义——在线会话走 `closeSession`(先 dispose 捕获的 agent handle 再删日志,不再要求重启),非在线会话走 `deleteSession`,均定向 detach。
22
22
  - **工作区行右键**:新增**在资源管理器打开**——调用 `fsAdmin.reveal` 在系统文件管理器中定位该工作区目录(Windows `explorer /select`、macOS `open -R`、Linux `xdg-open`)。
23
23
 
24
24
  ---
@@ -33,7 +33,7 @@ dsh web UI 插件:在浏览器设置弹窗(侧边栏底部“设置”)中
33
33
  - `fsAdmin` (`reveal`):跨平台在系统文件管理器中定位一个绝对路径(Windows `explorer /select`、macOS `open -R`、Linux `xdg-open`),供工作区右键菜单「在资源管理器打开」使用;
34
34
  - `mcpAdmin` (`list` / `upsert` / `remove` / `test`):管理 profile 的 `cordis.patch.yml` 中的 MCP 客户端实例(`@deepseek-ai/dsh-mcp-client`)。基于行级 YAML 块编辑(零依赖):识别顶层 `- id:` 块并仅改动 `name` 为 MCP 客户端插件的条目,`upsert` 按 id 原位替换或追加,`remove` 整块删除,写回走**原子写**;校验 id / serverName / transport / command / url,拒绝畸形输入;文件变更与插件安装共用同一**串行操作队列**(读-改-写不交错)。`test` 按条目 id 发起**连通性探测**:stdio 子进程(newline JSON-RPC,`initialize` → `initialized` → `tools/list`,超时 `taskkill /T /F` 强杀进程树,捕获 stderr 尾部)或 streamable-http(`fetch` POST `initialize`,兼容 SSE 与纯 JSON,超时 AbortController),返回服务器标识 / 工具数量 / 耗时,或失败原因(命令不存在、连接拒绝、超时等),全程不抛异常。
35
35
  - **浏览器前端(`lib/client.js`)**:
36
- - 注入 `slots` + `connection`,向设置弹窗注册一个 `settings.section`:`plugin-admin`(管理中心, order 25);页内 Tab 仅挂载当前面板;
36
+ - 注入 `slots` + `connection`,注册三个界面贡献:向官方插件设置页的 `settings.plugins.tab` 注册「扩展插件」页签(id `extensions`,order 20,排在「插件配置」0 与「插件列表」10 之后);向设置弹窗注册两个 `settings.section`:`mcp-servers`(MCP服务器, order 25,紧跟「Agent 预设」)与 `session-history`(历史会话, order 100);面板随官方页签/分区按需挂载;
37
37
  - 采用模块表平台词 `require('react')` 与 Shell 共享同一 React 实例;
38
38
  - 深度利用 `--dsw-*` design tokens,自适应浅色/深色主题,支持卡片式布局、状态呼吸灯、加载动效(Spinner)、危险操作防误触确认条与空状态提示;
39
39
  - 经 `/api` RPC Gateway 调用 Host 对应服务的 Remote 方法;
@@ -80,11 +80,11 @@ node scripts/host-check.mjs
80
80
 
81
81
  自检套件覆盖:
82
82
  1. Bundle 工厂加载与模块导出校验;
83
- 2. 单个“管理中心” `settings.section` 声明与注册,以及插件管理 / 会话管理 / MCP 配置 Tab 的按需切换;
83
+ 2. 三个 slot 注册(`settings.plugins.tab`「扩展插件」+ `settings.section`「MCP服务器」/「历史会话」),以及扩展插件 / MCP服务器 / 历史会话面板的按需挂载;
84
84
  3. 现代化统一样式注入(`<style>` 挂载与动画定义);
85
85
  4. React 组件真实挂载(JSDOM + React 18);
86
- 5. 插件管理面板渲染、过滤统计与卸载二次确认交互;**失败反馈可见性**:安装 / 更新 / 卸载的 RPC 返回 `{ok:false}` 时,错误文案渲染后**不被同批次触发的静默列表刷新清除**(旧实现在同一微任务里先写 error 再 reload,React 18 批处理把错误吞掉,失败表现为「点击没反应」),并同步弹出顶部浮动错误 toast(双通道:面板红条持久显示细节 + toast 醒目提示;toast 为**单例**——新提示原位替换旧提示并重启倒计时,同屏至多一条,快速连续失败不会重叠遮挡);
87
- 6. 会话管理面板渲染(独立组件,无 Tab)、列表与状态渲染;
86
+ 5. 扩展插件页签渲染、过滤统计与卸载二次确认交互;**失败反馈可见性**:安装 / 更新 / 卸载的 RPC 返回 `{ok:false}` 时,错误文案渲染后**不被同批次触发的静默列表刷新清除**(旧实现在同一微任务里先写 error 再 reload,React 18 批处理把错误吞掉,失败表现为「点击没反应」),并同步弹出顶部浮动错误 toast(双通道:面板红条持久显示细节 + toast 醒目提示;toast 为**单例**——新提示原位替换旧提示并重启倒计时,同屏至多一条,快速连续失败不会重叠遮挡);
87
+ 6. 历史会话设置页渲染、列表与状态渲染;
88
88
  7. 会话删除二次确认交互;
89
89
  8. **侧边栏菜单注入**:验证会话菜单追加「复制会话 ID / 删除会话」,删除项为**两次点击确认**(首击改写标签、二击才触发 RPC)且对**同名会话拒绝删除**;工作区菜单追加「在资源管理器打开」;
90
90
  9. **MCP 配置面板渲染与保存流**:独立 MCP 设置页展示服务器列表、添加/编辑/移除入口与空状态;打开添加表单填写 id / serverName / command 后保存,断言 `mcpAdmin/upsert` 收到正确载荷、表单关闭且新服务器入列;**连通性测试按钮**(🔌 测试)触发 `mcpAdmin/test` 并在行内渲染 ✅ 连通(含服务器名与工具数量)。
package/lib/client.js CHANGED
@@ -2,14 +2,15 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-admin', factory: (require) => {
2
2
  var module = { exports: {} }; var exports = module.exports;
3
3
 
4
4
  /**
5
- * dsh-plugin-admin browser half: unified "管理中心" (插件管理 & 会话管理)
6
- * page inside the settings panel.
5
+ * dsh-plugin-admin browser half: administration surfaces parked where the
6
+ * settings shell already renders their domain.
7
7
  *
8
- * The page rides the shell's `settings.section` slot (the shell owns
9
- * the dialog chrome, navigation, and close); its component is assembled with
10
- * the platform-shared React (module-table seed word, same instance the shell
11
- * renders with), providing a tabbed modern UI to manage both profile plugin
12
- * layers and session lifecycles over the /api RPC gateway.
8
+ * Three slot contributions (all assembled with the platform-shared React,
9
+ * module-table seed word, same instance the shell renders with):
10
+ * - 扩展插件: a tab inside the shell-owned 插件 section (`settings.plugins.tab`),
11
+ * after 插件配置 and 插件列表;
12
+ * - MCP服务器 / 历史会话: standalone `settings.section` pages in the settings nav.
13
+ * Panels talk to the host over the /api RPC gateway.
13
14
  */
14
15
 
15
16
  var React = require('react')
@@ -23,12 +24,6 @@ var CSS_TEXT = [
23
24
  '@keyframes dsh-admin-pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.35; transform: scale(0.85); } }',
24
25
  '[data-dsh-admin-section] { display: flex; flex-direction: column; width: 100%; gap: 14px; padding: 2px; font-size: 13px; line-height: 1.5; color: var(--dsw-alias-label-primary, #222); max-height: calc(100vh - 140px); overflow: hidden; }',
25
26
  '[data-dsh-admin-section] * { box-sizing: border-box; }',
26
- '[data-dsh-admin-section] .tabs { display: flex; padding: 3px; background: var(--dsw-alias-interactive-bg-hover, rgba(200,200,210,0.2)); border-radius: 9px; border: 1px solid var(--dsw-alias-border-subtle, rgba(200,200,210,0.3)); gap: 3px; }',
27
- '[data-dsh-admin-section] .tab { flex: 1; border: 0; background: transparent; border-radius: 7px; padding: 7px 14px; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 7px; font-size: 12px; font-weight: 500; color: var(--dsw-alias-label-secondary, #666); transition: all 0.15s ease; font-family: inherit; }',
28
- '[data-dsh-admin-section] .tab:hover:not(.active) { color: var(--dsw-alias-label-primary, #222); background: var(--dsw-alias-interactive-bg-hover, rgba(200,200,210,0.3)); }',
29
- '[data-dsh-admin-section] .tab.active { background: var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base, transparent)); color: var(--dsw-alias-label-primary, #333); font-weight: 600; box-shadow: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.04); }',
30
- '[data-dsh-admin-section] .tab-count { font-size: 10px; font-weight: 600; padding: 1px 6px; border-radius: 10px; background: var(--dsw-alias-interactive-bg-hover, rgba(200,200,210,0.4)); color: var(--dsw-alias-label-secondary, #555); }',
31
- '[data-dsh-admin-section] .tab.active .tab-count { background: var(--dsw-static-blue-500, #3b82f6); color: #fff; }',
32
27
  '[data-dsh-admin-section] .toolbar { display: flex; gap: 8px; align-items: center; padding: 10px; border: 1px solid var(--dsw-alias-border-subtle, rgba(200,200,210,0.4)); border-radius: 12px; background: var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base, transparent)); box-shadow: 0 1px 2px rgba(0,0,0,0.02); }',
33
28
  '[data-dsh-admin-section] .toolbar .input { height: 32px; padding: 0 12px; border-radius: 8px; border-color: var(--dsw-alias-border-subtle, rgba(200,200,210,0.5)); }',
34
29
  '[data-dsh-admin-section] .search-wrap { flex: 1.6; min-width: 0; position: relative; display: flex; align-items: center; height: 32px; }',
@@ -63,6 +58,10 @@ var CSS_TEXT = [
63
58
  '[data-dsh-admin-section] .btn.sm { height: 28px; padding: 0 10px; font-size: 12px; border-radius: 6px; }',
64
59
  '[data-dsh-admin-section] .spinner { width: 12px; height: 12px; border: 2px solid transparent; border-top-color: currentColor; border-radius: 50%; animation: dsh-admin-spin 0.7s linear infinite; display: inline-block; flex: none; }',
65
60
  '[data-dsh-admin-section] .list { display: flex; flex-direction: column; gap: 10px; flex: 1 1 auto; min-height: 0; max-height: 560px; overflow-y: auto; padding: 2px 4px 2px 2px; scrollbar-width: thin; scrollbar-color: var(--dsw-alias-border-subtle, rgba(200,200,210,0.4)) transparent; }',
61
+ '[data-dsh-admin-section] .list.grid2 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; align-content: start; }',
62
+ '[data-dsh-admin-section] .list.grid2 .empty { grid-column: 1 / -1; }',
63
+ '[data-dsh-admin-section] .list.grid2 .card { cursor: pointer; }',
64
+ '[data-dsh-admin-section] .list.grid2 .card:hover { transform: none; border-color: var(--dsw-alias-border-subtle, rgba(200,200,210,0.4)); background: var(--dsw-alias-interactive-bg-hover, rgba(200,200,210,0.25)); box-shadow: none; }',
66
65
  '[data-dsh-admin-section] .list::-webkit-scrollbar { width: 6px; }',
67
66
  '[data-dsh-admin-section] .list::-webkit-scrollbar-thumb { border-radius: 99px; background: var(--dsw-alias-border-subtle, rgba(200,200,210,0.4)); }',
68
67
  '[data-dsh-admin-section] .session-group { display: flex; flex-direction: column; gap: 6px; }',
@@ -121,7 +120,7 @@ var CSS_TEXT = [
121
120
  '[data-dsh-admin-section] .mcp-test-fail { color: var(--dsw-alias-state-error-primary, #ef4444); }',
122
121
  '[data-dsh-admin-section] .mcp-test-warn { color: #d97706; opacity: 0.95; }',
123
122
  '[data-dsh-admin-section] .mcp-test-cached { font-size: 10px; color: var(--dsw-alias-label-tertiary, #999); }',
124
- '@media (max-width: 520px) { [data-dsh-admin-section] { gap: 10px; } [data-dsh-admin-section] .toolbar { align-items: stretch; flex-wrap: wrap; padding: 8px; } [data-dsh-admin-section] .toolbar .input-wrap { flex-basis: 100%; } [data-dsh-admin-section] .card-header { align-items: flex-start; } [data-dsh-admin-section] .card-actions { flex-wrap: wrap; justify-content: flex-end; } [data-dsh-admin-section] .footer { gap: 6px; align-items: flex-start; flex-direction: column; } [data-dsh-admin-section] .footer .path { max-width: 100%; } }',
123
+ '@media (max-width: 520px) { [data-dsh-admin-section] { gap: 10px; } [data-dsh-admin-section] .list.grid2 { grid-template-columns: 1fr; } [data-dsh-admin-section] .toolbar { align-items: stretch; flex-wrap: wrap; padding: 8px; } [data-dsh-admin-section] .toolbar .input-wrap { flex-basis: 100%; } [data-dsh-admin-section] .card-header { align-items: flex-start; } [data-dsh-admin-section] .card-actions { flex-wrap: wrap; justify-content: flex-end; } [data-dsh-admin-section] .footer { gap: 6px; align-items: flex-start; flex-direction: column; } [data-dsh-admin-section] .footer .path { max-width: 100%; } }',
125
124
  '@keyframes dsh-toast-in { 0% { opacity: 0; transform: translateY(-12px) scale(0.96); } 100% { opacity: 1; transform: translateY(0) scale(1); } }',
126
125
  '@keyframes dsh-toast-out { 0% { opacity: 1; transform: translateY(0) scale(1); } 100% { opacity: 0; transform: translateY(-8px) scale(0.96); } }',
127
126
  '.dsh-admin-toast { position: fixed; top: 16px; left: 50%; transform: translateX(-50%); z-index: 99999; padding: 10px 20px; border-radius: 10px; font-size: 13px; font-weight: 500; line-height: 1.4; box-shadow: 0 8px 30px rgba(0,0,0,0.15), 0 2px 8px rgba(0,0,0,0.08); display: flex; align-items: center; gap: 8px; pointer-events: none; animation: dsh-toast-in 0.25s ease-out forwards; max-width: 90vw; word-break: break-word; }',
@@ -385,7 +384,7 @@ function setupMenuInjection(call, refreshSessions) {
385
384
  }
386
385
  if (exact.length === 0) { showToast('error', '❌ 未找到匹配的会话'); return }
387
386
  if (exact.length > 1) {
388
- showToast('error', '❌ 存在 ' + String(exact.length) + ' 个同名会话,无法确定要删除的目标;请在 设置 → 管理中心 → 会话管理 中按会话 ID 删除')
387
+ showToast('error', '❌ 存在 ' + String(exact.length) + ' 个同名会话,无法确定要删除的目标;请在 设置 → 历史会话 中按会话 ID 删除')
389
388
  return
390
389
  }
391
390
  cb(exact[0])
@@ -514,7 +513,8 @@ function setupMenuInjection(call, refreshSessions) {
514
513
  }
515
514
 
516
515
  /**
517
- * Plugins management settings section (standalone, no internal tabs).
516
+ * 扩展插件 tab contribution: plugin management rendered inside the shell-owned
517
+ * 插件 settings section (after 插件配置 and 插件列表).
518
518
  */
519
519
  function PluginsSection(props) {
520
520
  // Lazy initializer: the seed object (and its localStorage read) must be
@@ -751,7 +751,7 @@ function PluginsSection(props) {
751
751
  alive.current = true
752
752
  reloadPlugins()
753
753
  // Auto-check for remote updates on mount (the panel loads lazily when
754
- // the 插件管理 tab is opened, so this runs once per open). The auto path
754
+ // the 扩展插件 tab is opened, so this runs once per open). The auto path
755
755
  // is cache-friendly: the host serves its 5-minute cache, so re-opening
756
756
  // the tab shortly after does not re-hit the registry. The manual
757
757
  // ⬆ 检查更新 button is the force path — it bypasses the cache and
@@ -775,7 +775,7 @@ function PluginsSection(props) {
775
775
  }
776
776
 
777
777
  /**
778
- * Sessions management settings section (standalone, no internal tabs).
778
+ * 历史会话 settings section (standalone settings-nav page).
779
779
  */
780
780
  function SessionsSection(props) {
781
781
  var pairSessions = useState({
@@ -853,7 +853,7 @@ function SessionsSection(props) {
853
853
  }
854
854
 
855
855
  /**
856
- * MCP configuration settings section (standalone, no internal tabs).
856
+ * MCP服务器 settings section (standalone settings-nav page).
857
857
  */
858
858
 
859
859
  /* ------------------------------------------------------------------------ */
@@ -972,7 +972,7 @@ function formatTestTime(ms) {
972
972
  /* once a newer version is detected it is kept in localStorage until a */
973
973
  /* check confirms the installed version is current again (i.e. the update */
974
974
  /* actually completed). This is what keeps the badge visible after closing */
975
- /* and reopening the 管理中心, across dsh restarts, and even when a later */
975
+ /* and reopening the dialog, across dsh restarts, and even when a later */
976
976
  /* re-check hits a transient network failure — and what clears it only once */
977
977
  /* the plugin is upgraded (更新完删除提醒). */
978
978
  /* ------------------------------------------------------------------------ */
@@ -1356,35 +1356,6 @@ function McpSection(props) {
1356
1356
  renderMcpSection(mView, patchDraft, reloadMcp, openMcpEditor, closeMcpEditor, saveMcpDraft, removeMcpEntry, testMcpEntry))
1357
1357
  }
1358
1358
 
1359
- /** Unified settings page; only the selected panel mounts and loads. */
1360
- function AdminCenterSection(props) {
1361
- var pairTab = useState('plugins')
1362
- var activeTab = pairTab[0]
1363
- var setActiveTab = pairTab[1]
1364
- var tabs = [
1365
- { id: 'plugins', label: '插件管理', component: PluginsSection },
1366
- { id: 'sessions', label: '会话管理', component: SessionsSection },
1367
- { id: 'mcp', label: 'MCP 配置', component: McpSection },
1368
- ]
1369
- var selected = tabs.find(function (tab) { return tab.id === activeTab }) || tabs[0]
1370
-
1371
- return createElement('div', { 'data-dsh-admin-section': '' },
1372
- createElement('div', { className: 'tabs', role: 'tablist', 'aria-label': '管理中心功能' },
1373
- tabs.map(function (tab) {
1374
- return createElement('button', {
1375
- type: 'button',
1376
- key: tab.id,
1377
- className: 'tab' + (tab.id === selected.id ? ' active' : ''),
1378
- role: 'tab',
1379
- 'aria-selected': tab.id === selected.id,
1380
- onClick: function () { setActiveTab(tab.id) },
1381
- }, tab.label)
1382
- })
1383
- ),
1384
- createElement(selected.component, { key: selected.id, call: props.call, refreshSessions: props.refreshSessions })
1385
- )
1386
- }
1387
-
1388
1359
  /* ========================================================================== */
1389
1360
  /* Render Plugins View */
1390
1361
  /* ========================================================================== */
@@ -1392,8 +1363,9 @@ function AdminCenterSection(props) {
1392
1363
  function renderPluginsView(view, patch, install, remove, checkUpdates, upgrade) {
1393
1364
  var elements = []
1394
1365
 
1395
- // Toolbar: search (primary, wide) + install input + actions in one row.
1396
- elements.push(createElement('div', { className: 'toolbar', key: 'toolbar' },
1366
+ // Toolbar, two rows: search alone on the first (it gets the full width),
1367
+ // install input + actions on the second.
1368
+ elements.push(createElement('div', { className: 'toolbar', key: 'toolbar-search' },
1397
1369
  createElement('div', { className: 'search-wrap', key: 'search-wrap' },
1398
1370
  createElement('span', { className: 'search-icon', key: 'icon' }, '🔍'),
1399
1371
  createElement('input', {
@@ -1411,6 +1383,8 @@ function renderPluginsView(view, patch, install, remove, checkUpdates, upgrade)
1411
1383
  onClick: function () { patch({ needle: '' }) },
1412
1384
  }, '✕') : null
1413
1385
  ),
1386
+ ))
1387
+ elements.push(createElement('div', { className: 'toolbar', key: 'toolbar-install' },
1414
1388
  createElement('div', { className: 'input-wrap install-wrap', key: 'install-wrap' },
1415
1389
  createElement('input', {
1416
1390
  className: 'input',
@@ -1530,7 +1504,7 @@ function renderPluginsView(view, patch, install, remove, checkUpdates, upgrade)
1530
1504
  ))
1531
1505
  }
1532
1506
 
1533
- elements.push(createElement('div', { className: 'list', key: 'list' }, rows))
1507
+ elements.push(createElement('div', { className: 'list grid2', key: 'list' }, rows))
1534
1508
 
1535
1509
  // Footer
1536
1510
  var hintText = view.note !== '' ? view.note : '更改在重启 dsh 后生效(关闭 dsh 进程后重新运行即可)'
@@ -1859,7 +1833,7 @@ function renderPluginCard(plugin, view, remove, patch, upgrade) {
1859
1833
  var isConfirming = view.confirming === plugin.name
1860
1834
 
1861
1835
  var titleChildren = [
1862
- createElement('span', { className: 'card-title-text', key: 'name' }, plugin.name),
1836
+ createElement('span', { className: 'card-title-text', key: 'name', title: plugin.name }, plugin.name),
1863
1837
  ]
1864
1838
  if (plugin.version) {
1865
1839
  titleChildren.push(createElement('span', { className: 'tag version', key: 'version' }, 'v' + plugin.version))
@@ -1994,7 +1968,17 @@ function filterSessions(sessions, filter, needle) {
1994
1968
  }
1995
1969
 
1996
1970
  /**
1997
- * Filter plugin layers by the type pill and a fuzzy name/version/path query.
1971
+ * Display rank for the plugin list: 内置 first, then 包安装 (registry
1972
+ * install), then 本地安装 (local path). Host order is kept within a rank.
1973
+ */
1974
+ function pluginSortRank(plugin) {
1975
+ if (!plugin.removable) return 0
1976
+ return plugin.localPath ? 2 : 1
1977
+ }
1978
+
1979
+ /**
1980
+ * Filter plugin layers by the type pill and a fuzzy name/version/path query,
1981
+ * then sort 内置 → 包安装 → 本地安装 (stable: equal ranks keep host order).
1998
1982
  * The needle matches case-insensitively against the package name, the
1999
1983
  * installed version, and the local source path, so "tool" finds
2000
1984
  * dsh-custom-tool and "0.2" finds version rows.
@@ -2011,7 +1995,7 @@ function filterPlugins(plugins, filter, needle) {
2011
1995
  }
2012
1996
  result.push(p)
2013
1997
  }
2014
- return result
1998
+ return result.sort(function (a, b) { return pluginSortRank(a) - pluginSortRank(b) })
2015
1999
  }/**
2016
2000
  * Group sessions by their accounting workspace in registry order; sessions
2017
2001
  * without a workspace trail under an "未分组" bucket. Empty workspaces are
@@ -2320,16 +2304,38 @@ function apply(ctx) {
2320
2304
  } catch (e) { refreshSessions = null }
2321
2305
  var disposeSidebar = ctx.effect(function () { return setupMenuInjection(call, refreshSessions) })
2322
2306
 
2323
- // One management-center entry keeps related local administration together;
2324
- // its tab container mounts only the selected panel.
2307
+ // Three surfaces replace the former single 管理中心 page, each parked where
2308
+ // the shell already renders its domain. 插件管理 joins the shell-owned 插件
2309
+ // section as its third tab (插件配置 0, 插件列表 10, 扩展插件 20); MCP and
2310
+ // sessions become standalone settings pages (MCP服务器 takes the 25 slot
2311
+ // right after Agent 预设; 历史会话 sorts last). inject() waits on each
2312
+ // slot's declaration, so registration order never matters.
2313
+ ctx.slots.inject('settings.plugins.tab', function () {
2314
+ return ctx.slots.register({
2315
+ name: 'settings.plugins.tab',
2316
+ id: 'extensions',
2317
+ order: 20,
2318
+ label: '扩展插件',
2319
+ inject: function () { return { call: call } },
2320
+ }, PluginsSection)
2321
+ })
2325
2322
  ctx.slots.inject('settings.section', function () {
2326
2323
  return ctx.slots.register({
2327
2324
  name: 'settings.section',
2328
- id: 'plugin-admin',
2325
+ id: 'mcp-servers',
2329
2326
  order: 25,
2330
- label: '管理中心',
2327
+ label: 'MCP服务器',
2328
+ inject: function () { return { call: call } },
2329
+ }, McpSection)
2330
+ })
2331
+ ctx.slots.inject('settings.section', function () {
2332
+ return ctx.slots.register({
2333
+ name: 'settings.section',
2334
+ id: 'session-history',
2335
+ order: 100,
2336
+ label: '历史会话',
2331
2337
  inject: function () { return { call: call, refreshSessions: refreshSessions } },
2332
- }, AdminCenterSection)
2338
+ }, SessionsSection)
2333
2339
  })
2334
2340
  }
2335
2341
 
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "dsh-plugin-admin",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "type": "module",
5
- "description": "dsh web UI plugin: unified administration panelplugin and session management with modern tabbed Web UI (host remote + browser panel)",
5
+ "description": "dsh web UI plugin: administration surfaces for the settings UI 扩展插件 tab in the Plugins page, plus MCP服务器 and 历史会话 settings pages (host remote + browser panel)",
6
6
  "main": "lib/index.js",
7
7
  "license": "MIT",
8
8
  "repository": {