dsh-web-plugin-manager 功能参考(工作区文档)
August 27, 2026 · View on GitHub
README 只保留功能速览;本文件存放功能与限制的细致说明,供开发与排障查阅。
查看
- 四源合并:include 树稳定行(
EntryOptions.id,官方语义稳定,Loader 挂载 id 每次随机,patch 定位必须用它)+dsh.profile.bundles层栈 +package.json依赖 +cordis.patch.ymlinsert 行 - 非运行 profile 显示离线合成条目(官方 in-box 包 base/web-app/headless 不标记为已安装)
- 已安装但未挂载的依赖:官方 CLI/pnpm 手动安装的非 bundle 插件只写依赖、不写挂载行(从未被加载)——list() 合成为
unmounted条目,目录页显示「未挂载」标签与「挂载」按钮,一键补写 managed insert 行(运行中 profile 实时生效)
实时启停
- 受控编辑 profile 的
cordis.patch.yml:# dsh-plugin-manager:managed:start/end标记块,行级操作、原子写入(tmp + rename),保留用户内容,可逆可审阅 - 变更先经 loader include 条目直接应用(
entry.update,与平台 watchUserPatches 同通道)再写文件——绕开平台 watcher 死锁(HMR 卸载等待自身 disposables 循环等待),实时生效、零重启 - 插件自有的 patch 文件 watcher:手动编辑持续实时生效,不依赖 HMR 生命周期
- 5s 超时兜底;跨 profile 隔离(仅作用于运行中宿主 profile)
- 卸载(删除插件)的实时卸载失败/超时不再静默:
liveUnmountPackage把applyLiveOps的{ok, message}透传给删除结果——失败时输出附「live unmount did not complete,请重启后完全生效」,且结果live: false。半挂的 loader 树(Web 服务已关但进程不退)会让自动重启链路(sessions.flush永不 settle →scheduleExit不执行 → 进程退出兜底不触发)卡死,此时需手动拉起进程;根治在宿主侧 flush 超时兜底(issue #10) - 随机行(无显式 id 的挂载行)不可经此启停——id 每次挂载变化
安装
- 调用官方
dsh pluginCLI(pnpm reconcile),保护 in-box bundles(base/web-app/headless 从dsh.profile.bundles恢复) - 非 bundle 插件自动写 managed insert 行并实时挂载;bundle 插件进入层栈,重启后加载
- git 源自动 clone 进
$DSH_HOME/plugin-manager-src缓存;已发布 npm 的包优先走 npm 安装(npm-first) - 安装即质量门(见下),任何问题自动回滚,profile 保持可启动
- 行 id 冲突预检:slugify 后的行 id 与既有行(insert 行同名不同包、用户顶层行)冲突时拒绝安装并回滚——避免 loader 拒绝整棵树
质量门(安装时)
- 扫描整条加载链(入口 + 相对 import 可达文件,BFS 有界)的 import,对照声明依赖 + 平台白名单
- 白名单:
@deepseek-ai/dsh-client-*/cordis-plugin-*前缀族 + cordis / react 等 loader 提供项 - 覆盖副作用导入、re-export、动态 import、minified 形态;
import type不误报 - 声明了但未安装的依赖同样拦截;bundle 插件
cordis.patch.yml行名逐一校验 - 官方包只能 peerDependencies:普通
dependencies命中安装兜底闭包的@deepseek-ai/*一律拦截回滚——普通依赖会在 profile 内装出第二份官方拷贝,loader 最近优先解析劫持官方行,模块身份(unique symbol/class)分裂导致Cannot read properties of undefined (reading 'prepare')类运行时故障;写peerDependencies即可(autoInstallPeers: false下经共享兜底目录解析,全 profile 共享一份)。豁免名单OFFICIAL_DEP_ALLOWED(analyze.ts):@deepseek-ai/schemastery——官方 cookbook 允许的运行时 validator,ValidationError用Symbol.for全局符号 duck-typed、schema 为纯函数闭包,双拷贝不分裂身份(官方 @deepseek-ai/dsh-llm-deepseek 即按 dependencies 声明)
更新
- npm 包对比 registry dist-tag
latest;git 缓存源(link: 路径是 git 仓库)git fetch对比远端 HEAD(@{u} 回退 FETCH_HEAD);git URL 源ls-remote对比安装 commit(gitHead) - 可更新卡片淡绿边框,更新按钮在删除左侧(仅检测到更新可点),详情显示当前/最新版本
- 更新动作:npm
@latest重装 / git 缓存 fetch+reset 后重跑官方 add / git URL 源重解析,均带质量门与回滚 - 边界:本地目录安装(非 git)无上游可比,报告"不可检测";git URL 源需要 manifest 记录安装 commit(gitHead)
健康检查(src/analyze.ts,离线引擎)
- 依赖图:全包 entry imports → providers(含 pnpm symlink、exports 子路径)→ 包间依赖边;拓扑排序输出(加载顺序提示,cordis 激活本身是服务可用性驱动)
- 缺失依赖(imports 无提供者且未声明)、被禁用依赖、循环依赖(DFS)、重复 patch 行 id
- 同名注册冲突(确定性 fail-loud 故障):服务名(new Service/ctx.provide)、工具名(ctx.tools.register)、prompt section 名(ctx.systemPrompt.section)、web 路由路径(ctx.webServer.register)——源码正则扫描,动态注册(字符串拼接的名字)检测不到
- peerDependencies 版本满足性(简化 semver:
^/~/>=/<=/>/</精确/星号),含经共享 fallback 解析的官方核心包 - 官方包重复安装(official-duplicate):
@deepseek-ai/*同时存在于 profile node_modules 与安装兜底目录 → 报肇事插件(哪个依赖引入的)与修复指引 - 运行时诊断(宿主):pending 注入根因(静态 inject 声明 vs 活跃服务表)、fiber 加载失败原因(
_error) - 作用效果冲突(语义冲突)不在静态检测范围:两个插件做相反的事(互相矛盾的 prompt 指导、竞争同一事件流)没有同名可查,只能靠运行时观察尽力而为
分级自动修复(管理页健康检查区)
- A 级(安全默认,一键自动修 / 单条「修复」):
duplicate-row-id(保留第一个删其余重复顶层行)、disabled-dependency(恢复启用被依赖条目)、official-duplicate(删除 profile node_modules 里的重复官方包拷贝 + 清理 manifest 声明) - B 级(建议 + 行内确认,不弹窗):
service/tool/section/route-conflict——建议"禁用后注册者",按钮两态(执行建议 → 确认执行?) - C 级(只输出):
missing-import/circular-dependency/peer-mismatch/ 运行时诊断 - 交互:行内按钮 + 状态反馈(已修复 ✓),执行细节进命令输出区;
fixAll批量跑 A 级;修复后自动重新分析 - 服务端:
fixIssue(profile, action, target)/fixAll(profile)(mutation mutex 串行,A 级动作执行前不二次确认)
环境管理
- 设置 → 插件 → 环境:启动/停止(终端或后台)、复制/转移插件(按记录源重装)、创建/重命名/删除 profile
- 官方 profile(web/headless)只读,不可管理
市场
- 静态索引为主:DSH-Plugins-Marketplace 的 CI 索引
registry.json.gz(topic:dsh-plugin 全量 ~3100 仓库,含星数/更新时间/pkg_name/版本/分类,每 2 小时更新)——多源兜底链:api.github.com contents(有GH_TOKEN/GITHUB_TOKEN带认证)→ jsDelivr CDN(generated_at新鲜度 6h 校验)→raw.githubusercontent→ 本地磁盘缓存(上次成功完整索引)→ GitHub 搜索 API(topic:dsh-plugin,残缺应急,不落盘);索引条目自带星数,不再逐个打 GitHub API - 精选覆盖层:awesome-dsh-plugins 结构化 catalog(catalog/plugins/*.json + tombstones)+ PLUGINS.md 双源合并,叠加在索引之上(状态徽标/精选描述/包名/分类),精选独有条目追加;24h 缓存,空结果不写正缓存
- dsh.so 验证/安全叠加:dsh.so 独立索引(1630 条,全部带 verification L1–L5 + 自动化安全扫描)按仓库名叠加 verification/security 徽标到卡片(TAG 行与 awesome 状态徽标同行,不新增条目、不作安装源);dsh.so 索引独立磁盘缓存 24h,失败降级不阻塞;注意扫描为静态启发式,
high风险可能是误报(如本管理器自身也被标 high)——徽标是"装前留意"信号,最终判定仍是安装质量门 - 已安装判定在服务端(每请求按目标 profile 计算,12 并发池标注):① npm 包名(registry pkg_name / 仓库名)② manifest
repository双向匹配(同名不同仓库不误判)③ git 缓存源 owner-repo 身份 ④~/.dsh/skills|.agent-presets目录探测;返回installed/installedVersion/latestVersion(索引版本字段)/updateAvailable(仅严格更高才提示,回滚不误报) - 同名包冲突消解:同一 pkg_name 只保留一条(已安装优先、否则星数高者),
dropped计数透传前端提示「N 个同名包已隐藏」 - 卡片动作:未安装 → 安装;已装无新版 → 绿色「已安装 vX」;已装有新版 → 橙色「更新」(npm 包走受保护 update 链路重写 specifier + 质量门 + 回滚,git-only 源重装);星数排序时已安装置顶
- 卡片布局:标题省略号不挤占按钮区;短 meta 行(星数缩写 2.5K / 来源 npm包|git仓库 / 类型占位);TAG 行(审核状态 + 功能分类本地化);单/双列切换(localStorage 记忆);增量渲染——首屏 120 条 + 触底加载更多 +
content-visibility: auto(~3000 条列表不卡顿,无需服务端分页) - 缓存:进程内存镜像(listing 与 profile 无关,切 profile 只重算已安装标记,零磁盘 IO)+ 磁盘 24h 缓存 + 失败负缓存 5min + registry 原始索引缓存;
refresh=1是唯一强制网络路径 - 网络健壮性:每请求 15s 超时(AbortSignal.timeout);支持
HTTP_PROXY/HTTPS_PROXY/NO_PROXY(undici ProxyAgent——Node 全局 fetch 会丢弃 dispatcher 选项,市场请求必须走 undici 自身 fetch);失败原因负缓存 5 分钟(marketplace-failure.json,避免每次进页重跑全量 GitHub 往返);索引全挂且无缓存时空列表直接显示失败原因 - GitHub API 未认证限流 60/h:仅 catalog 独有条目的星数富化会打 API(量小);403/429 停止富化(星数降级用上次快照元数据),列表本身不受影响;raw 兜底源可达时列表保持非空
- 系统代理/规则模式加速器对 Node 进程无效(undici 不读系统代理)——市场为空且此类加速器用户,把代理地址写进环境变量,或改 TUN/全局模式
agent 工具与安装守卫
plugin_status/plugin_install/plugin_uninstall/plugin_toggle(目标 profile 由配置profile指定,默认web);依赖注入避免循环导入plugin_search:自然语言检索市场(本地索引,name 3 分 / topics 2 分 / 描述 1 分加权,空查询按星数;中英文分词),结果带已安装标记与仓库链接,并提示"安装前先浏览仓库"(索引元数据无法判断质量,安装本身走质量门)- 安装守卫(
src/guard.ts):tools.guard拒绝 bash/run_code 中裸dsh plugin add/remove/update与指向 profile 目录的pnpm add/remove(只读 verb list/status/dump-config/help 放行),拒绝原因直接给模型指路plugin_*工具与dshpm;systemPrompt.section(order 300)常驻提示同一规则 - 守卫只拦 agent 工具调用,拦不住用户在终端手工执行裸命令
多类型安装(skill / agent 预设)
- 类型检测分层:根/嵌套
agent.cordis.yml→ 预设(官方单文件判定,preset.yml 仅显示元数据);根 package.json 声明 DSH 能力 → cordis 插件;根SKILL.md→ skill;嵌套预设/插件/技能根;其余(含含 install.sh 的仓库——永不自动执行第三方脚本)→ 非三类,拒绝安装并加入市场屏蔽名单 - skill 安装到官方根
<dshHome>/skills(frontmatter name 优先,技能集合仓库逐个装,跳过点目录/node_modules/vendored);chokidar watch 默认开启 → 安装/删除即热加载;预设安装到<dshHome>/.agent-presets(目录名即 preset id,官方每次会话发现重读);预设的管理(复制/删除/默认)由官方设置页完成 - 安装记录
installed-kinds.json(市场安装的来源/类型/位置/时间,串行队列读写);市场已安装判定与卡片类型徽标(skill/预设/插件)来自记录 - 市场卸载:管理页「技能与预设」区块(记录列表 + 卸载 + git 源重新拉取);
dshpm uninstall-kind <owner/repo>;skill/预设删目录含路径越界防护,cordis 按记录逐个走受保护 remove - 屏蔽名单:安装时检测为非三类的仓库写入
blocked-repos.json,市场所有列表路径(缓存/新拉/兜底)统一过滤,响应带blocked/blockedRepos;市场页提供「解除屏蔽」
插件拥有的 Agent 预设(归属管理)
-
背景:预设目录名是 preset id,不是所属插件——插件(如 dsh-agent-rp)自带的伴随预设是插件自己复制进
<dshHome>/.agent-presets的,卸载插件后预设成为孤儿(选择器残留、选中报错)。归属标记回答「这个目录是谁的、还是原版吗」。 -
标准标记(本管理器安装预设时写入):
.dsh-preset-owner.json={format: 0, owners: [包名], digest: sha256(agent.cordis.yml[+preset.yml])},不覆盖已有标记。 -
兼容读取(生态既有惯例,只读不写):gamelike-plugin-manage 的
.plugin-manage-owner.json(owners 数组)、dsh-agent-rp 的.dsh-agent-rp-owner.json(owner 字符串 + format + digest,严格校验 format)。标准标记损坏 → 视为无归属(不删);兼容标记损坏/format 不符 → 单个跳过。 -
卸载清理(remove / uninstall-kind / plugin_uninstall):只删「唯一 owner == 被卸载插件 且 digest 匹配(用户未修改)」的预设;用户改过的跳过并报告;多 owner 预设(归属重叠)不处理;system 信任预设绝不碰;删除优先走宿主
agentPresets.remove()(处理 settings.default 与 standing 会话),宿主不可用(CLI)降级直删(rmRetry + 路径守卫)。预设是全局的:其他 profile 仍安装该插件时不清理。 -
禁用归档 / 启用恢复(setEnabled):禁用插件把其拥有的预设目录移出 user root(
<dshHome>/plugin-manager-cache/preset-archive/,含用户修改版,零数据损失,选择器即时消失);重新启用自动移回(同名冲突保留新预设并提示)。禁用从不删除——宿主 broken 判定只查 YAML 语法、不反映「引用的插件被禁用」,且临时禁用不应毁数据。 -
已知限制:CLI 直删路径不清宿主 settings.default(无 ctx);skill 无此问题(SKILL.md 不引用插件代码,残留无害)。
-
类型检测分层:根/嵌套
agent.cordis.yml→ 预设(官方单文件判定,preset.yml 仅显示元数据);根 package.json 声明 DSH 能力 → cordis 插件;根SKILL.md→ skill;嵌套预设/插件/技能根;其余(含含 install.sh 的仓库——永不自动执行第三方脚本)→ 非三类,拒绝安装并加入市场屏蔽名单 -
skill 安装到官方根
<dshHome>/skills(frontmatter name 优先,技能集合仓库逐个装,跳过点目录/node_modules/vendored);chokidar watch 默认开启 → 安装/删除即热加载;预设安装到<dshHome>/.agent-presets(目录名即 preset id,官方每次会话发现重读);预设的管理(复制/删除/默认)由官方设置页完成 -
安装记录
installed-kinds.json(市场安装的来源/类型/位置/时间,串行队列读写);市场已安装判定与卡片类型徽标(skill/预设/插件)来自记录 -
市场卸载:管理页「技能与预设」区块(记录列表 + 卸载 + git 源重新拉取);
dshpm uninstall-kind <owner/repo>;skill/预设删目录含路径越界防护,cordis 按记录逐个走受保护 remove -
屏蔽名单:安装时检测为非三类的仓库写入
blocked-repos.json,市场所有列表路径(缓存/新拉/兜底)统一过滤,响应带blocked/blockedRepos;市场页提供「解除屏蔽」
CLI(dshpm)
- bin 随插件安装进入 profile 的 node_modules;也可
node <profile>/node_modules/dsh-web-plugin-manager/dist/cli.js直接调用;--home指定 DSH_HOME - 所有变更走与 Web UI 完全相同的受保护链路:pnpm add/remove(官方 CLI)→ 质量门 → 自动回滚 → 安装后分析 → insert 行维护;ctx 可空:无宿主进程时跳过 live 应用,文件级操作与 Web UI 一致
- 命令:
dshpm install <source>:npm 名 / github:user/repo / git URL / tarball / 本地路径dshpm remove <name>:insert 行 + 包依赖一并清理(含 node_modules)dshpm mount <name>:补挂载官方 CLI 手动安装的未挂载依赖dshpm list:bundle 层栈 / 已装包 / insert 行dshpm analyze:健康检查全文输出,有问题退出码 1
架构模块
- Host:
src/index.ts——PluginManagerService(ctx.pluginManager)+/api2/plugin-manager/*REST(webServer.register) - 实时应用:
src/live.ts;分析引擎:src/analyze.ts(与质量门共享扫描器,永不漂移);Patch 编辑:src/patch.ts(YAML 陷阱:@包名引号、空数组文档[]、纯注释文件恢复模板);网络助手:src/net.ts(超时 + 代理);REST 原语:src/rest.ts(信任围栏 + 请求体读取,纯函数可单测);Agent 工具:src/tools.ts;守卫与提示:src/guard.ts;CLI:src/cli.ts - Client:
src/client/——settings.plugins.tab(all 遮蔽官方只读列表 + manager + environments)+settings.section(marketplace);同源 fetch 调 REST(不走 Typert Remote)
已知限制明细
- 禁用被依赖的条目可能导致 profile 启动失败(官方 fail-loud 设计);恢复:手动编辑该 profile 的
cordis.patch.yml删除 managed 块 - 安装来自 git 的 bundle 需要用户在终端放行
pnpm allowBuilds(命令输出会回显) - git 子包安装:多包仓库用
#路径:<dir>约定指定子目录(#ref是 git ref) - 质量门可能误伤:未声明运行时依赖的插件会被拦截回滚(保守策略);若插件确实由 Loader/host 提供该模块,需在 manifest 声明或加入白名单;Node 内置模块说明符(裸名与
node:前缀等价)已由isBuiltin统一豁免——运行时无条件提供,不构成缺失依赖(issue #9) - 市场条目来源于静态索引 + awesome 目录,个别仓库可能已删除/私有(安装时报
Repository not found);索引项目本身是第三方维护(社区项目),其数据问题(打错 tag、非插件仓库)由精选覆盖层与安装质量门兜底 - nvm 用户注意:子进程命令(dsh/npm/pnpm/git)解析按「运行中 node 目录 → PATH → $NVM_DIR」兜底,并把命中的工具目录注入子进程与终端窗口的 PATH——宿主进程不在 nvm 激活的 shell 中启动(桌面启动器/服务/nohup)也能工作;仅当 dsh 完全未安装时才需要从 nvm 激活的终端启动