dsh-win-mgr —— 窗口管理器
window manager for plugin page injections
像窗口管理器管窗口一样,管插件对页面的注入。
win = window(窗口)+ win(赢)的双关——管的是插件的「窗户」,不是 Windows 系统。
DeepSeek Harness(DSH)的 client 插件(网页端改页面的插件)在浏览器里全权限运行,
官方没有运行时 UI 沙箱。dsh-win-mgr 给「插件对页面的注入」加三级权限控制。
三级能力
| 级别 | 能力 | 技术落点 | 状态 |
|---|
| L1 全局开关 | 允许/拒绝插件修改页面(全局) | 加载层硬禁:host 侧 ctx.clientModules 装配图裁决,拒绝 = 不 serve bundle、不进 window.__DSH_BOOT__。管的是「改页面」而非插件本身——host 侧工具/服务照常运行,只禁 client UI 一半 | ✅ 已实现(本仓库当前阶段) |
| L2 按页面细分 | 允许哪些页面 / 禁止哪些页面 | Slot 层:拦截 ctx.slots 注册/挂载,按当前路由/页面 allow/deny | ⏳ 计划 |
| L3 统一插件页 | 被拒插件 → 重定向到隔离「插件页」 | 跨源 sandbox iframe + postMessage 桥(<iframe sandbox="allow-scripts">,不带 allow-same-origin) | ⏳ 计划 |
L1 用法
安装
dsh plugin --profile web add dsh-win-mgr # 或本地路径:dsh plugin --profile web add /path/to/dsh-win-mgr
配置
推荐方式(热生效,无需重启):浏览器「设置 → 窗口管理器」设置页——左栏按
分类(默认自带 / 自己安装)列出所有有 client 脚本的插件(含搜索框),右栏查看
插件信息并点「允许 / 阻止」。改动写入 $DSH_HOME/settings.yaml,host 侧即时
重新裁决,刷新页面后生效。
配置文件方式(作为默认值 / 无设置页时兜底):在 profile 的 cordis.patch.yml
或 --patch overlay 里按行 id 覆盖(整行 config 整体替换):
- id: dsh-win-mgr
config:
# A 硬禁(默认):名单里的插件,client 半不 serve bundle、不进 boot 图,
# 浏览器端代码完全不到达;其 host 半(工具/服务)照常运行。
blocked:
- dsh-phone-ui
- dsh-pocket
# 总闸:true = 除本插件自身与 allow 名单外,所有 client 插件(含官方 UI 插件)
# 的 client 半一律不注入页面。注意:这会连官方 UI 一起关掉(页面变裸),
# 这是总闸语义,默认 false。
globalOff: false
# 总闸下的白名单(优先于 blocked / globalOff)。
allow:
- '@deepseek-ai/dsh-client-ui-sidebar'
注:settings 命名空间(设置页写入)的 user 层优先于 entry config(patch 层)。
设置页点过「允许/阻止」后,以 settings.yaml 为准;想回到 patch 配置请清掉
settings.yaml 里的 dsh-win-mgr: 段。
示例 overlay 见 test/overlays/。
设置页数据面
浏览器侧读写配置不走官方 client settings wire——官方
dsh-host-apiproxy 对配置客户端暴露的命名空间是硬编码白名单(第三方命名空间
默认不可读/写,官方标注为 deferred work),因此 dsh-win-mgr 走自己注册的同源
路由(线格式见 src/types.ts):
GET /wmgr/plugins → { plugins: [...], policy: {blocked, allow, globalOff} }
POST /wmgr/policy → 写入 {blocked?, allow?, globalOff?}(host 进程内
settings.update,持久化到 settings.yaml 并触发全表重新裁决)
机制(L1 硬禁)
host 侧 ClientModuleRegistry(ctx.clientModules)是装配喉点:它扫描 loader 条目里声明
dsh.client 的包,把每个 bundle 的哈希写进 window.__DSH_BOOT__(boot 图),并在
/plugins/<id>/client.js 提供文件。谁进这张表,谁才能到浏览器。
dsh-win-mgr 的 host 半(src/host/registry.ts)包一层 clientModules.processOne
(每条目的装配裁决):
- 拒绝 →
cm.table.delete(entryName):不进 boot 图、serveBundle 404
- 立即把
clientModules 构造时已入表的被禁条目驱逐,走框架自身 flush 路径重算 boot 图
(recompose + notifyGraphChanged,HMR 侧同步收敛)
- 裁决规则(
src/host/policy.ts):自己永不禁 → allow 优先放行 → blocked 显式拒绝
→ globalOff 拒绝其余一切
⚠ 依赖 clientModules 内部实现(table / dirty / processOne / flush,对齐
0.1.0-rc.6)。DSH 升级若改动其结构,本插件会检测到并降级告警,不会破坏启动。
开发与构建
代码在 src/,lib/ 是构建产物(勿手改,勿提交)。构建管线学自官方
deepseek-harness 与社区
deepseek-harness-plugin-manager:
src/ ──(tsc)──▶ lib/types/ ──(tsdown)──▶ lib/
├── index.js host(node ESM)
├── types.js ./types 子路径
└── client.js 浏览器半(ModuleLoader 工厂壳)
- host 半:tsc 产物经 tsdown 打成单文件 ESM(
export { apply, inject, name })。
- client 半:tsc 产物经 tsdown 打成 CJS,banner/intro/footer 拼出
window.__ModuleLoader__.load({ id, factory }) 工厂壳;react 系列走页面模块表
(external),*.module.css 由 lightningcss 编译成 CSS Modules(哈希类名 + 自动
注入 <style data-plugin="dsh-win-mgr">),sourcemap 输出 lib/client.js.map
(DSH 原生服务 /plugins/<id>/client.js.map)。
pnpm install # 首次(含 esbuild/rolldown 原生二进制;被拦截时 pnpm approve-builds)
pnpm run typecheck # tsc 双半类型检查
pnpm test # vitest 单元测试(policy / inventory / short-name / 组件)
pnpm run build # clean + tsc + tsdown(产出 lib/)
pnpm run pack:check # pnpm pack + 产物体检(scripts/verify-package.mjs)
bash test/verify.sh # 三模式 16 项集成回归(见下)
⚠ prepare 钩子会自动 build——pnpm install 后 lib/ 即是最新产物。
目录结构
src/
├── index.ts # host 入口({ name, inject, apply } + 类型再导出)
├── types.ts # 跨半线格式(Policy / PluginInfo / PluginsSnapshot …)
├── shared/constants.ts # 跨半常量(插件 id / 命名空间 / 路由前缀)
├── host/
│ ├── context.ts # HostContext 最小结构类型(cordis 子集)
│ ├── policy.ts # PolicyModel:裁决语义(纯逻辑,可单测)
│ ├── registry.ts # clientModules 装配喉点适配(processOne 包装 + reflush)
│ ├── inventory.ts # 插件清单收集(有 client 脚本的条目)
│ ├── routes.ts # /wmgr/plugins + /wmgr/policy 数据面路由
│ ├── settings.ts # settings 命名空间注册 + 热更新
│ └── service.ts # WinMgrService 编排器
└── client/
├── index.ts # 浏览器半入口(settings.section 注册 + i18n + 样式清理)
├── types.ts # ClientContext 最小结构类型
├── api.ts # 数据面 fetch 封装
├── locales.ts # zh/en 文案字典(i18n)
├── short-name.ts # 官方同款短名算法
├── state.ts # LoadState / GroupKey 判别联合
├── cx.ts # className 拼接
├── styles.module.css # 设置页样式(CSS Modules + 官方主题变量)
└── components/
├── WinMgrSection.tsx # 页面壳(两栏 / 窄屏两级)
├── PluginList.tsx # 左栏:搜索 + 折叠分组列表
├── PluginDetail.tsx # 右栏:详情 + 允许/阻止开关
├── ToggleSwitch.tsx # 开关(受控)
└── WinIcon.tsx # 设置页图标
tests/ # vitest 单元测试
scripts/ # clean.mjs / verify-package.mjs
test/ # 集成回归(verify.sh + overlays + 夹具配置)
test-fixture/ # 验收夹具(故意手写 bundle,保持零依赖)
验收测试
仓库自带一个测试夹具 dsh-win-mgr-test-fixture(test-fixture/)——一个故意同时走
正路(shell.overlay slot)与野路子(直接往 body 插固定定位元素)改页面的 client 插件,
是 dsh-win-mgr 要管住的那类插件的放大版。仅用于测试,正常 profile 不要装。
(夹具刻意保持手写 bundle:它是测试双而非产品代码,零依赖便于任何环境复现。)
服务端断言(可无头回归)
# 1) 初始化测试 profile(工作区内的独立 DSH_HOME,不碰真实环境)
DSH_HOME=/mnt/data/Project/dsh-win-mgr/.dsh-test dsh plugin --profile web add /path/to/dsh-win-mgr
DSH_HOME=/mnt/data/Project/dsh-win-mgr/.dsh-test dsh plugin --profile web add /path/to/dsh-win-mgr/test-fixture
# 2) 跑三模式断言(基线 / blocked 名单 / globalOff 总闸,共 16 项)
cd /path/to/dsh-win-mgr && bash test/verify.sh
浏览器端断言(playwright-cli)
# 基线:fixture 的 client 代码应在浏览器里运行
DSH_HOME=/mnt/data/Project/dsh-win-mgr/.dsh-test dsh --profile web --port 3111 &
# 期望:window.__WMGR_FIXTURE_MATERIALIZED__ / __WMGR_FIXTURE_APPLIED__ 为 true,
# body 里存在 [data-testid=wmgr-fixture-wild](野路子 DOM)与
# [data-testid=wmgr-fixture-overlay](正路 slot)
# 拦截:--patch ./test/overlays/block-fixture.yml
# 期望:上述标记全部 undefined、无 fixture DOM、页面正常渲染、无 console 报错;
# /wmgr-fixture/ping 仍 200(host 半照常)
阶段状态
- ✅ Phase 1:项目骨架 + L1 全局硬禁(
blocked 名单 / globalOff 总闸 / allow 白名单)
src/host/:装配图裁决(clientModules.processOne 包装)+ 数据面路由
test-fixture/:验收夹具(正路 slot + 野路子 DOM)
test/verify.sh + test/overlays/:三模式 16 项服务端断言
- ✅ Phase 3(部分):配置 UI —— 浏览器「设置 → 窗口管理器」设置页
src/client/:settings.section 注册(左栏分类 + 搜索 + 插件列表,右栏详情 +
允许/阻止开关,assets/win.svg 图标),zh/en i18n
- settings 命名空间热配置(settings.yaml 持久化,host 即时重新裁决,刷新页面生效;
阻止/允许可热切换,无需重启)
- 已验证:设置页阻止 fixture → settings.yaml 落盘 → boot 图即时剔除 → 刷新后
插件代码完全不达浏览器;允许恢复同理
- ✅ 工程化重构(2026-08):手写 bundle →
src/ 模块化 TS + tsc/tsdown 双半构建;
vitest 单元测试 + 产物体检脚本 + GitHub Actions CI(见 .github/workflows/ci.yml)
- ⏳ Phase 2:L2 Slot 层裁决(按插件 × 页面 allow/deny)
- ⏳ Phase 3 余下:全局开关/allow 名单的设置页控件(目前
globalOff/allow 仅配置文件可设)
- ⏳ Phase 4:L3 统一插件页(跨源 sandbox iframe PoC,以 dsh-phone-ui 为第一个隔离对象)
License
MIT