dsh-plugin-admin 0.5.0 → 0.5.1

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
@@ -7,7 +7,7 @@ dsh web UI 插件:把管理能力拆进设置界面的既有结构——在官
7
7
  - 列表展示当前 profile 下的全部 bundle 层(名称、版本,以及**内置 / 包安装 / 本地安装**标记;本地安装的插件额外显示其源路径,依据 profile 依赖清单中的 `link:` / `file:` / 绝对路径 spec 判定),按 **内置 → 包安装 → 本地安装** 排序,卡片以一行两列栅格展示(窄屏回落单列);名称过长时单行省略显示,悬停可见完整名称;
8
8
  - **名称模糊搜索**:列表上方搜索框支持按插件名 / 版本号 / 本地路径模糊过滤(大小写不敏感子串匹配),可叠加「全部 / 扩展插件 / 系统内置」筛选胶囊,无结果时给出专属空状态提示;
9
9
  - 支持扩展插件一键卸载(带有优雅的行内二次确认);
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 收口);
10
+ - **远程更新检测(自动)**:打开「扩展插件」页签即自动按 profile 实际使用的 npm registry(`npm_config_registry` → 项目/用户 `.npmrc` → 官方源)查询每个 **registry 安装**的插件(跳过内置与本地路径安装)的 `latest` 版本,与本地版本比对——有新版时卡片显示琥珀色「⬆ 有新版本 vX.Y.Z」徽标并出现一键「⬆ 更新」按钮(升级时优先用 checkUpdates 已探测到的精确版本号 `npm install <name>@<version>`;host 端对 `@latest` 会先查 registry 解析为精确版本再安装,避免 pnpm 在已有范围约束下误判 "Already up to date" 而静默不更新);工具栏「⬆ 检查更新」是唯一的手动重查入口,**强制绕过 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` 或首条用户提问)并渲染首条消息的文本摘要气泡预览(带折行省略与多行保护);
@@ -33,7 +33,7 @@ dsh web UI 插件:把管理能力拆进设置界面的既有结构——在官
33
33
  - **Host 端(`lib/index.js`,零 dsh 依赖)**:
34
34
  - 注入 `['typert', 'workspaceRegistry', 'sessionPersistence', 'tools', 'subagents']`;
35
35
  - 提供并注册五个 RPC 命名空间:
36
- - `pluginAdmin`(`list` / `install` / `remove` / `checkUpdates`):异步 `spawn` pnpm(Windows 走 shell 解析 .cmd shim,5 分钟超时且**进程树强杀**(`taskkill /T /F`,避免超时后残留 pnpm/node 子进程继续写盘),Promise 尾链串行化防并发),镜像 CLI `reconcileBundles` 同步清单,清单写回为**原子写**(临时文件 + rename,崩溃不截断 profile 的 package.json);安装/卸载参数经**字符白名单**校验(`assertPnpmOperand`),从根上排除 `&` `|` `>` `<` `%` 引号等 cmd 元字符注入向量;`install` / `remove` 返回真实 pnpm 输出尾部(`output` 字段),前端作为提示气泡的悬浮诊断信息展示;`checkUpdates` 对 registry 安装的 bundle 并发查询 npm registry 的 `latest`(registry 解析:`npm_config_registry` env → 项目/用户 `.npmrc` → 官方源;有界并发 4 路、8s 超时、5 分钟缓存;`force` 参数为 true 时**绕过缓存强制重查并刷新缓存内容**),返回 `updateAvailable` / `latest` / `error`,本地路径与内置插件跳过;
36
+ - `pluginAdmin`(`list` / `install` / `remove` / `checkUpdates`):异步 `spawn` pnpm(Windows 走 shell 解析 .cmd shim,5 分钟超时且**进程树强杀**(`taskkill /T /F`,避免超时后残留 pnpm/node 子进程继续写盘),Promise 尾链串行化防并发),镜像 CLI `reconcileBundles` 同步清单,清单写回为**原子写**(临时文件 + rename,崩溃不截断 profile 的 package.json);安装/卸载参数经**字符白名单**校验(`assertPnpmOperand`),从根上排除 `&` `|` `>` `<` `%` 引号等 cmd 元字符注入向量;`install` 收到以 `@latest` 结尾的 spec 时,先查 registry 解析为**精确版本**再 `pnpm add`(避免 pnpm 在 manifest 已有满足版本的范围约束时误判 "Already up to date" 而静默不更新——这正是旧版「点更新无反应」的根因),registry 不可达时退回原 spec 让 pnpm 显式报错;`install` / `remove` 返回真实 pnpm 输出尾部(`output` 字段),前端作为提示气泡的悬浮诊断信息展示;`checkUpdates` 对 registry 安装的 bundle 并发查询 npm registry 的 `latest`(registry 解析:`npm_config_registry` env → 项目/用户 `.npmrc` → 官方源;有界并发 4 路、8s 超时、5 分钟缓存;`force` 参数为 true 时**绕过缓存强制重查并刷新缓存内容**),返回 `updateAvailable` / `latest` / `error`,本地路径与内置插件跳过;
37
37
  - `sessionAdmin` (`list` / `archive` / `unarchive` / `deleteSession` / `closeSession`):直接对接 `workspaceRegistry` 与 `sessionPersistence`,提供安全幂等的持久化日志清理与归档状态流转;`list` 采用**修订号驱动的摘要缓存**(`listSnapshots` 的 revision token,未变化会话不重读事件日志)与**有界并发**(最多 4 路并行 inspect),并设单会话事件扫描上限兜底;`archive` 校验会话真实存在,拒绝向归档集写入垃圾 id;对 registry 软私有写路径(`requireState` / `setState` / `enqueueOperation`)在 `apply()` 挂载时即做**兼容性探测**,dsh 版本变更会明确报出缺失成员,而不是首次归档时才静默失败。`closeSession` 使**在线会话免重启删除**成为可能:`installAgentHandleCapture` 在挂载时透明包装公开的 `ctx.agents.create` / `resume`(原样调用并返回,仅把返回的 `AgentHandle` 按 session id 存入插件私有 Map),删除在线会话时先 `handle.dispose()` 走 dsh 官方 teardown 链(停止 loop → 等待静止 → 注销 agent → 从 SessionStore 移除 → 触发 `session/disposed` → 持久化层 `retire()` flush 缓冲事件并释放写路径),再删日志——日志不会被下次 flush 复活;未被捕获 handle 的在线会话(如插件挂载前已创建)会明确报错并引导重启,绝不误删。
38
38
  - `fsAdmin` (`reveal`):跨平台在系统文件管理器中定位一个绝对路径(Windows `explorer /select`、macOS `open -R`、Linux `xdg-open`),供工作区右键菜单「在资源管理器打开」使用;
39
39
  - `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),返回服务器标识 / 工具数量 / 耗时,或失败原因(命令不存在、连接拒绝、超时等),全程不抛异常。
package/lib/client.js CHANGED
@@ -650,7 +650,16 @@ function PluginsSection(props) {
650
650
  /** Upgrade one plugin to its latest version (registry install by name). */
651
651
  function upgradePlugin(name) {
652
652
  if (pView.busy) return
653
- var spec = name + '@latest'
653
+ // Prefer the exact version already discovered by checkUpdates(): passing
654
+ // `name@latest` to pnpm is unreliable when the manifest already carries a
655
+ // range constraint (e.g. "^0.4.2") — pnpm can resolve @latest against the
656
+ // satisfied range and report "Already up to date" WITHOUT fetching the new
657
+ // version (exit 0, nothing changed). Pinning the exact latest version makes
658
+ // pnpm bump the constraint and actually download the new package. The host
659
+ // install() also falls back to resolving @latest server-side, so a direct
660
+ // `name@latest` still works as a second guard.
661
+ var known = pView.updates && pView.updates[name]
662
+ var spec = (known && known.latest) ? (name + '@' + known.latest) : (name + '@latest')
654
663
  patchPlugin({ busy: true, error: '', confirming: null, note: '' })
655
664
  callRemote('pluginAdmin/install', { spec: spec }).then(function (result) {
656
665
  if (!alive.current) return
@@ -1878,7 +1887,7 @@ function renderPluginCard(plugin, view, remove, patch, upgrade) {
1878
1887
  key: 'btn-upgrade',
1879
1888
  className: 'btn sm',
1880
1889
  disabled: view.busy,
1881
- title: '升级到 v' + updateInfo.latest + '(npm install ' + plugin.name + '@latest)',
1890
+ title: '升级到 v' + updateInfo.latest + '(npm install ' + plugin.name + '@' + updateInfo.latest + ')',
1882
1891
  onClick: function () { upgrade(plugin.name) },
1883
1892
  }, '⬆ 更新'))
1884
1893
  }
package/lib/index.js CHANGED
@@ -192,6 +192,27 @@ function resolveNpmRegistry(profileDir) {
192
192
  return NPM_REGISTRY_DEFAULT
193
193
  }
194
194
 
195
+ /**
196
+ * Resolve a `name@latest` install spec into a pinned `name@<version>` spec by
197
+ * querying the registry for the current `latest` dist-tag. Returns the original
198
+ * `name@latest` unchanged when the registry is unreachable, so an offline
199
+ * upgrade attempt still fails loudly (pnpm will error on the unknown tag)
200
+ * rather than silently no-op'ing. Scoped names (`@scope/name`) are supported.
201
+ * @param profileDir - profile directory (used to locate a local .npmrc).
202
+ * @param name - package name without any version/range suffix.
203
+ * @returns the pinned spec string (e.g. "dsh-plugin-admin@0.5.0").
204
+ */
205
+ async function resolveLatestSpec(profileDir, name) {
206
+ const registry = resolveNpmRegistry(profileDir)
207
+ const latest = await fetchLatestVersion(registry, name)
208
+ if (latest === null) {
209
+ // Registry unreachable: keep the literal @latest so pnpm surfaces the
210
+ // failure (no silent "up to date").
211
+ return name + '@latest'
212
+ }
213
+ return name + '@' + latest
214
+ }
215
+
195
216
  /**
196
217
  * Query the npm registry for the `latest` dist-tag version of one package.
197
218
  * Strict timeout; returns null on any failure so a dead registry never
@@ -1190,7 +1211,19 @@ export function apply(ctx) {
1190
1211
  if (typeof spec !== 'string' || spec.trim() === '') {
1191
1212
  throw new Error('plugin-admin: install requires a spec string')
1192
1213
  }
1193
- const operand = assertPnpmOperand('install spec', spec)
1214
+ const rawOperand = assertPnpmOperand('install spec', spec)
1215
+ // A bare `@latest` dist-tag is ambiguous for pnpm when the manifest
1216
+ // already pins a satisified range (e.g. "^0.4.2"): pnpm resolves
1217
+ // @latest against the range, concludes "Already up to date", and exits
1218
+ // 0 WITHOUT fetching the newer published version — the upgrade silently
1219
+ // no-ops. Resolve the real latest version from the registry first and
1220
+ // pin it exactly so pnpm is forced to bump the constraint and download.
1221
+ // The client already sends an exact `name@<version>` when it knows the
1222
+ // latest (from checkUpdates); this is the server-side safety net for
1223
+ // any caller that still passes `@latest`.
1224
+ const operand = rawOperand.endsWith('@latest')
1225
+ ? await resolveLatestSpec(profileDir, rawOperand.slice(0, -'@latest'.length))
1226
+ : rawOperand
1194
1227
  return enqueue(async () => {
1195
1228
  const output = await runPnpm(profileDir, ['add', operand])
1196
1229
  reconcileBundles(profileDir)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-admin",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "type": "module",
5
5
  "description": "dsh web UI admin plugin: extension-plugin marketplace tab, MCP server & session-history settings pages, and subagent management (named delegation-tool instances with persona/tool-filter/model config, plus external CLI backend mounting) — all persisted as profile cordis.patch.yml rows that Cordis HMR hot-reloads",
6
6
  "main": "lib/index.js",