Imported from zhu1090093659/dsh-web (
packages/dsh-plugin-manager/AGENTS.md). Install upstream withnpx skills add zhu1090093659/dsh-web --skill dsh-plugin-manager. Copyright stays with the author.
AGENTS.md — dsh-plugin-manager
DSH web GUI plugin dsh-plugin-manager. 包级规则:只写本包特有约定,不重复根 AGENTS.md 与 packages/AGENTS.md 的全局/包级规则。
本包要点
- 本包向官方「插件」面板(
main面板 idplugins)声明的plugins.detail.section席位贡献 一个「检查更新」区块(idfamily-update-check,order 30),只渲染在已安装组合包的页面上; 它不注册settings.plugins.tab,也不拥有自己的设置分区——安装/卸载/启停由官方页面负责, 本包只补官方页面没有的 registry 更新检查与 DSH 运行时兼容门禁。 该席位是官方 list 槽,类型由官方页面运行时声明;本包在PluginUpdatePatch.tsx内以同形declare module重新声明(跨包禁止 value import,与家族卡片复用plugins.bundle.config的做法一致)。宿主交付的PluginPageSubject(bundle/row/item)是契约观察,不是 import。 - 行 id 不与官方行重叠,
name保持恰好等于包名:本包独立安装的行 id 是ui-plugin-manager-update-check;官方@deepseek-ai/dsh-web-app的ui-plugin-manager归官方插件页(唯一注册sidebar.panellist的半区),两行必须并存——loader 同 id 后来者胜 且不报错,重叠时侧栏「插件」入口与整个插件管理页静默消失(#1794)。行的name必须恰好是@linxin666/dsh-client-ui-plugin-manager:dsh-client-modules只把浏览器半区挂在说明符等于 包名的那一行上,改成家族子路径只挂 host 半区。host 半区的 cordis 插件名(src/index.ts)与 行 id 同名,LOCKED_ENTRY_IDS同时收录该独立行 id 与家族行web-ui-plugin-manager;本包 不变量由tests/bundle-row-id.spec.ts固定。聚合侧的家族行 id 与子路径plugin-manager由聚合 清单的familyIds:映射钉住,不随本包独立行改名(见 dsh-web-all/AGENTS.md)。 - 列表级工具条只能插进官方页面的 chrome:官方「插件」页的扩展点包括
plugins.detail.*、plugins.item、plugins.bundle.config、plugins.row.config、plugins.bundle.activation,以及 0.2.1 起承载整页内容的plugins.tab;后者会把批量更新藏到 另一个 tab,不能替代「已安装」标题旁的即时检查/pending 计数。因此工具条仍由plugin-toolbar-mount.tsx直接插进[data-plugin-panel] [data-plugin-group="bundles"]的标题元素,并用共享的body-mutations.ts(sync-shared 副本,勿手改)自查位; 容器自持 React root,不做 DOM 改写。页面重绘/切走时容器自动挪位或摘除,重复 apply 通过TOOLBAR_MOUNT_SELECTOR去重。 - 批量更新策略放 core,不放 host:
src/core/updates.ts是唯一策略源——只更新第三方 (非@deepseek-ai/)且compatible !== false的行;官方包随 DSH 本体升级,绝不批量改写。 单页区块(plugins.detail.section)仍可对官方包做单包更新,那是用户逐条确认的动作。 - 重启三态,禁止臆造命令行:
src/host/restart.ts判定并回传实际模式——relaunch(终端启动:detached helper 等旧进程退出后重放本进程自身的 execPath + argv,剔除--inspect*)、shell(打包桌面:只退出,由桌面外壳自己的恢复对话框重启)、manual(无终端/无法判定:不退出,界面提示手动重启)。判定是纯函数planRestart,执行是performRestart(helper 起不来时降级为manual而不是让进程白白退出)。新增重启路径前 先确认宿主真的能重启,不要把「重启」做成静默失败。 - 重启路由必须按方法区分:
GET /api/plugin-manager/restart只读方案(无副作用),POST才执行,其他方法 405。缺了这道守卫时,一个普通 GET(浏览器预取、直接输入 URL、 误探测)就能停掉正在运行的宿主并弹出桌面外壳的崩溃恢复对话框(2026-09-30 实证)。 界面必须先读方案再确认:确认面板写的后果必须与POST实际产生的后果一致。 - 桌面端的重启只能借道外壳,且必须在确认前写清后果:打包桌面没有给 Web GUI 任何重启
通道(preload 只暴露
dshDesktop.updates.status/open/subscribe,app.relaunch()只能由 外壳自己的恢复对话框触发),所以宿主退出必然产生一个错误样式的对话框并写崩溃报告。 桌面端(shell)的确认面板里,主按钮仍是「确认重启」,但面板必须先写明:会出现官方的 「应用无法启动或已意外停止」对话框、需在对话框里点「重启」、并会写一份崩溃报告;同时 给出手动替代(退出应用后重新打开)。用户明确要求过「点确认就直接走官方对话框」, 不要退回成「只有手动说明」。 - 双通道纪律:运行时探测官方
/plugin-installer通道,存在(DSHCode / 1.0.4 checkout web)则全部走官方 RPC(单一写入器 = 官方安装器);不存在(npm 发布的官方 web)则走本包 host 半区的 loopback HTTP 网关——安装/卸载 spawn 官方dsh pluginCLI(仍是唯一写入器),启停写 profile patch 的disabled覆盖行。 - 应用自有 profile 的更新走官方进程内管理器:打包桌面启动(
facts.desktop)里 CLI 拒绝写该 profile(dsh plugin --profile desktop …直接报错),更新必须经宿主挂载的 官方pluginManager服务(ctx.get('pluginManager'),契约观察不 import);其余运行时 CLI 仍是唯一写入器,两条路径共用同一任务表、状态轮询与版本核对。 - 官方管理器拒绝原地更新(
already-installed):0.2.1-rc.2 起installBundle在hmr挂载且该包 已是 profile 依赖时直接抛already-installed(守卫在 pnpm 之前),而 dsh-base 只在profileContext存在时挂hmr行——即每个 profile 宿主,官方插件页自己的更新按钮走同一条路。 因此应用自有 profile 的更新按两阶段执行:先removeBundle再installBundle;第二阶段失败时 装回运行前 profile 携带的 spec,并在错误里写明是「已装回」还是「需手动重装」。单次installBundle仍排在最前,上游哪天接受原地更新就自动回到单次路径。见 记录。 - 官方管理器 resolve 不等于成功:它的
change()包装器把失败折叠进返回值 (application: 'failed'/'cancelled',附error.code/error.diagnostic),而不是 抛错。原生安装/更新/卸载必须先经nativeManagerFailure()读该判定再重读 profile, 否则会把 pnpm 已经拒绝的运行报成「官方插件管理器报告成功,但…未生效」,把真实原因 (例如ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION)藏起来。判定形状见 更新路径记录。 - CLI 是 Node 脚本时不能用 shebang 直接 spawn:GUI 启动的宿主(桌面应用)PATH 里没有
node,#!/usr/bin/env node会在 CLI 启动前以env: node: No such file or directory(退出码 127)失败。用 CLI 旁的node(npm-global/homebrew 布局),否则用process.execPath执行脚本,并把 CLI 所在目录放到子进程 PATH 首位——dsh plugin转发的 pnpm 就在那里。 - 网关安全:所有
/api/plugin-manager/*路由必须经isLoopbackRequest门禁; set-enabled 写文件走备份 + tmp + rename;不改其它写入器的行(insert 格式行内层disabled除外,见src/host/rows.ts)。 - 目录分区:
src/index.tshost 半区(网关挂载);src/host/网关实现(profile 解析、行编辑、CLI 作业、路由);src/client/browser 半区(更新区块 UI、槽位注册、 双通道封装);src/core/两侧共享纯逻辑(wire 解析、层 diff、冲突行形状)。 - 启动 profile 解析:桌面端启动器通过
runProfile({ profile: 'desktop' })启动, 既不传--profile也不导出DSH_PROFILE,因此resolveProfile()除 argv 与环境外 还必须读宿主发布的profileContext(可选ctx.get)以及启动器 argv 里位置参数携带的 profile 目录(Electron 会剥掉 exec 开关,故桌面宿主 argv 形如[execPath, dsh-desktop-host/lib/index.js, <dsh>, <profileDir>, …])。优先级为显式--profile、已发布 profile、argv 启动目录、DSH_PROFILE、web子命令、桌面端 持久化选择,别无来源时保持休眠(不猜测 profile)。已发布事实按外来输入处理:名称做 穿越校验,dir必须为绝对且 无穿越的路径,patchPath只在等于该 profile 自己的cordis.patch.yml时接受 (它是写入目标);desktop只由启动器事实决定,不由 profile 名称推断。详见 Agent Note。 - wire 形状镜像官方
ui-settings-plugin-installer的协议(DSH 源码 checkout),是 契约观察而非 import;形状变化时更新src/core/protocol.ts与测试。 - 已随「插件管理」Tab 移除的能力(
src/core/repair.ts、修复会话、安装冲突 UI、 安全模式横幅、只读清单与子插件展开)不要再以 client UI 形式加回:host 侧的冲突 / notice 台账(GatewayJob.conflicts/notices)保留为可观测事实,但没有渲染方。 - 共享件副本:
src/mount-once.ts、src/host/loopback.ts、src/host/dsh-home.ts、src/client/body-mutations.ts由scripts/sync-shared.mjs生成,禁止手改。
提交前检查
pnpm --filter @linxin666/dsh-client-ui-plugin-manager typecheck
pnpm --filter @linxin666/dsh-client-ui-plugin-manager test
pnpm --filter @linxin666/dsh-client-ui-plugin-manager build