dsh-unified-desktop
by nalikip
README.md
# dsh-unified-desktop
[](https://github.com/nalikip/dsh-unified-desktop/actions/workflows/ci.yml)
[](LICENSE)
[](package.json)
[](package.json)
**两个 Windows 桌面自动化 MCP,44 个工具,其中 15 个是重复或危险的。**
这个插件把它们合成一个:**29 个工具,一个命名空间,内建危险调用拦截。**
不是简单的代理转发。它知道 `click` 该用哪个后端(wincu 走 UIA Invoke,不抢焦点),
`ocr` 只有一边有,`read_webpage` 的 DOM 模式只有另一边有,
而 `PowerShell` / `Registry` / `FileSystem` / `Process` 两边都不该暴露。
> 合并的是 [dsh-computer-use-win](https://github.com/Yu-tao-Li/dsh-computer-use-win)(UI Automation + OCR + 元素级语义操作)
> 与 [Windows-MCP](https://github.com/CursorTouch/Windows-MCP)(浏览器 DOM + 区域截图 + 应用启动)。
> 每个取舍的理由见 **[能力归属矩阵](docs/capability-matrix.md)**。
---
## 它解决的问题
单独装这两个插件,模型会看到 44 个工具、两份互相打架的说明:
| 问题 | 具体表现 |
|---|---|
| **能力重复** | `click` / `Click`、`type_text` / `Type`、`scroll` / `Scroll`… 共 10 组,模型不知道该选哪个 |
| **上下文吃紧** | 两套 schema 合计 61,599 tokens(实测),光工具定义就吃掉大半上下文 |
| **危险工具裸露** | windows-mcp 默认暴露 `PowerShell`(任意命令)、`Registry`(改注册表)、`FileSystem`、`Process`(kill) |
| **无统一入口** | 两个 MCP server 各自启动,工具命名空间分裂成两个 |
## 合并后的变化
| | 分开装 | 本插件 |
|---|---|---|
| MCP server 数 | 2 | **1** |
| 工具数 | 44 | **29** |
| 重复能力 | 10 组 | **已去重** |
| 危险工具 | 全部裸露 | **内建守卫拦截** |
| 工具命名空间 | 分裂成两个 | **统一 `mcp__desk__*`** |
工具数与去重结果由 `lib/catalog.mjs` 的 `CATALOG` / `EXCLUDED` 定义,
`node test/catalog-test.mjs` 会校验一致性(43 项断言)。
**关于上下文占用的说明**:44 个工具的实测基线是 61,599 tokens,
数据在 [docs/tool-footprint.json](docs/tool-footprint.json)(2026-10-22 从两个后端实时拉 `tools/list` 后统计)。
合并后只保留 29 个工具的 schema,但**具体数值随上游 schema 体积变化,本项目未持续测量**——
本插件只做去重与重命名,不裁剪单个工具的 schema。
想在自己的环境里量,可以跑 `node test/aggregate-e2e.mjs`,它会打印当前工具面的字符数与估算 token。
## 和通用 MCP 网关有什么不同
GitHub 上已有成熟的多 MCP 聚合方案([1MCP](https://github.com/1mcp-app/agent) 525★、
[IBM mcp-context-forge](https://github.com/IBM/mcp-context-forge) 4.7k★、
[MCP-Gateway](https://github.com/aiguicai/MCP-Gateway) 155★)。它们是**横向**的:接任意 N 个
server,靠配置里的 filter 规则裁剪工具面。
本项目是**纵向**的:只为两个特定后端服务,靠一张**能力归属表**做语义级去重——
它知道 `Snapshot` 该拆成「树模式用 wincu、DOM 模式并入 read_webpage」,
而通用网关只能按名字过滤,不知道这两个工具其实是同一件事的不同实现。
**如果你要接的是别的 MCP server**,用上面那些通用网关更合适。
**如果你就在 Windows 上用这两个后端**,这张归属表是为你写的。

> 这张图由 `node scripts/gen-dedup-svg.mjs` 从 `lib/catalog.mjs` 生成,
> 数字与代码永远一致,不会随文档改动而失同步。
## 架构
```
DSH ──stdio──> [aggregate.mjs] ──stdio──> wincu server.mjs
└─stdio──> windows-mcp.exe
```
聚合层负责三件事:
1. **合并工具面** —— 从两个后端拉 `tools/list`,按目录去重合并
2. **路由调用** —— `tools/call` 按目录映射到对应后端
3. **内建守卫** —— 危险参数在转发前拦截
## 能力分工
一句话概括:**交互类全给 wincu,系统与浏览器类全给 windows-mcp。**
**选 wincu 的(它更强)**:
| 能力 | 为什么用 wincu |
|---|---|
| `snapshot` / `read_tree` / `find` | UIA 树 + RuntimeId 精确元素定位 |
| `ocr` | **只有 wincu 有**,Windows.Media.Ocr 字级坐标 |
| `click` / `double_click` | 优先走 UIA Invoke 模式,不抢焦点 |
| `invoke` / `set_value` | **只有 wincu 有**,UIA 语义操作 |
| `type_text` | 剪贴板路径支持中文,且恢复原剪贴板 |
| 窗口管理 | activate / close / move |
**选 windows-mcp 的(wincu 没有)**:
| 能力 | 用途 |
|---|---|
| `read_webpage` | **浏览器 DOM 模式**,读当前标签页,绕开反爬 |
| `screenshot` | 区域截图 `region` + 多屏 `display` |
| `launch_app` | 按开始菜单名启动应用 |
| `notify` | Windows toast 通知 |
| `clipboard` | 读写剪贴板 |
| `display_info` / `control_status` | 显示器信息、桌面控制权 |
## 不暴露的工具
**风险类**(内建守卫,且 DSH 原生工具已覆盖):
| 工具 | 排除原因 |
|---|---|
| `PowerShell` | 任意命令执行;DSH 原生 `pwsh` 已覆盖且受策略管辖 |
| `Registry` | 直接改注册表,风险过高 |
| `FileSystem` | DSH 原生 `read`/`write`/`edit`/`glob`/`grep` 已覆盖 |
| `Process` | `kill` 风险高 |
**重复类**(已由 wincu 的更强实现替代):
`Click`、`Type`、`Move`、`Scroll`、`Shortcut`、`Wait`、`WaitFor`、`Snapshot`、`MultiSelect`、`MultiEdit`
完整清单见 [lib/catalog.mjs](lib/catalog.mjs) 的 `EXCLUDED`。
> 每一项「为什么留这个、为什么砍那个」的完整推导,见
> **[docs/capability-matrix.md](docs/capability-matrix.md)**。
## 工具清单(29 个)
**观察**
`snapshot` `screenshot` `read_tree` `element_info` `find` `ocr` `list_windows` `read_webpage` `display_info` `control_status` `health`
**鼠标键盘**
`click` `double_click` `type_text` `keypress` `scroll` `move_mouse` `drag` `set_value` `invoke` `focus`
**窗口**
`activate_window` `close_window` `move_window` `launch_app`
**等待**
`wait` `wait_for`
**系统**
`notify` `clipboard`
## 安装
**前置**:
```powershell
# 1. wincu(提供 UIA + OCR)
dsh plugin --profile web add dsh-computer-use-win
# 2. windows-mcp(提供 DOM + 应用启动)
uv tool install windows-mcp --python 3.14
```
**安装本插件**:
```powershell
dsh plugin --profile web add <本目录路径>
```
**打后端补丁(重要)**:
```powershell
node scripts/patch-backend.mjs
```
不打补丁的话,windows-mcp 会要求你**连续空闲 10 秒**才允许操作,
否则 `screenshot` / `read_webpage` / `launch_app` 等全部返回 `USER_CONTROL`。
详见 [docs/backend-patch.md](docs/backend-patch.md)。
> ⚠️ **这个补丁会直接修改你本机的 windows-mcp 安装**
>
> `scripts/patch-backend.mjs` 不是插件,它改写 `uv tool` 安装目录下的
> `windows_mcp/desktop/control.py` 与 `control_hooks.py` 源文件。执行前请知悉:
>
> - **先备份或确认可回滚**。撤销用 `node scripts/patch-backend.mjs --revert`;
> 彻底还原也可以 `uv tool install --force windows-mcp --python 3.14` 重装。
> - **上游不认可这个修改**。它是本地规避 [#498](https://github.com/CursorTouch/Windows-MCP/issues/498) 的权宜之计。
> 该缺陷已在 [PR #508](https://github.com/CursorTouch/Windows-MCP/pull/508) 修复并合并进上游 `main`,
> 但**尚未进入 PyPI 发布版**(最新仍是 0.8.8)。上游发新版本后应移除本补丁。
> - **升级 windows-mcp 会让补丁失效**,需要重新执行。
> - 补丁状态检查:`node scripts/patch-backend.mjs --check`
**重要**:本插件已聚合 wincu 的能力,需**禁用独立挂载的 wincu**避免工具重复。
在 profile 的 `cordis.patch.yml` 加:
```yaml
- id: mcp-dsh-computer-use-win
disabled: true
```
重启 DSH,工具以 `mcp__desk__*` 出现。
## 设置项
| 项 | 默认 | 说明 |
|---|---|---|
| `enabledTools` | 全部 29 个 | 暴露哪些工具 |
| `guardEnabled` | `true` | 策略守卫开关 |
| `auditLog` | `true` | 写审计日志 |
| `idleThresholdSeconds` | `0.5` | windows-mcp 桌面控制门的空闲阈值(秒) |
| `wcuServerPath` | 自动探测 | wincu server.mjs 路径 |
| `wmExePath` | 自动探测 | windows-mcp.exe 路径 |
| `toolCallTimeoutMs` | `180000` | 工具超时 |
## 一个关键实现细节
**wincu 必须用 `.generations/live/` 下的真实路径启动,不能用 `profiles/web/node_modules` 的软链接。**
因为它的入口判断是:
```js
import.meta.url === pathToFileURL(process.argv[1]).href
```
软链接会让 `import.meta.url` 解析成真实路径,而 `argv[1]` 是链接路径,两者不等,
server 会**静默退出(exit 0,无任何输出)**。
本插件的 `resolveWcuServer()` 会自动优先选 `.generations` 路径。
## 测试
```powershell
npm test # 全部 6 个套件
```
单独跑:
```powershell
node test/catalog-test.mjs # 目录一致性、去重、危险工具排除(43 断言)
node test/config-test.mjs # 配置校验与路径解析(21 断言)
node test/backend-patch-test.mjs # 后端补丁状态与行为(14 断言,无后端时跳过)
node test/aggregate-e2e.mjs # 工具面合并、双后端路由(13 断言)
node test/concurrency-test.mjs # 并发不串号(6 断言)
node test/electron-verify.mjs # DSH 真实 Electron 运行时(7 断言)
```
> `catalog-test`、`config-test`、`backend-patch-test` 不需要运行中的后端,任何环境都能跑
> (CI 默认跑这三个)。其余三个需要本机已安装 wincu 与 windows-mcp。
## 诊断
遇到 wm 工具被拒(`USER_CONTROL`)时:
```powershell
node scripts/diagnose-gate.mjs
```
输出补丁状态、5 秒内的钩子触发统计、以及 MCP 层实测结果。
完整排查过程见 [docs/diagnosis-user-control.md](docs/diagnosis-user-control.md)。
## 排错
**工具没出现(`mcp__desk__*` 不存在)**
1. 看日志 `%APPDATA%\dsh-desktop\logs\harness.log`,搜 `[aggregate]`
2. 正常情况下应有:
```
[aggregate] unified-desktop 0.1.0 ready (wcu=true wm=true guard=true)
[wcu] windows-computer-use MCP server 0.5.0 ready
[wm] Starting MCP server 'windows-mcp'
```
3. 如果 `wcu=false`:检查 wincu 是否安装。路径必须是
`profiles\.generations\live\dsh-computer-use-win+*\node_modules\dsh-computer-use-win\mcp\server.mjs`
4. 如果 `wm=false`:`uv tool install windows-mcp --python 3.14`
**loader 一直显示 `still waiting for 1 plugin entries`**
这是正常的。MCP 初始化约需 30-40 秒(两个后端依次启动),loader 只是计时提示,
插件就绪后不再刷新该行。
**工具重复出现(`mcp__wincu__*` 和 `mcp__desk__*` 都有)**
说明独立的 wincu 没有被禁用。在 profile 的 `cordis.patch.yml` 加:
```yaml
- id: mcp-dsh-computer-use-win
disabled: true
```
**调用返回 `USER_CONTROL`**
windows-mcp 的桌面控制门。两种情况:
1. **没打后端补丁** → 执行 `node scripts/patch-backend.mjs` 后重启
2. **打了补丁但你正在操作** → 停下鼠标键盘约 `idleThresholdSeconds` 秒再试
用 `control_status` 查看当前状态:`state: "ready"` 表示可操作,`"user"` 表示正在等你空闲。
如果一直卡在 `user` 且 `wait_seconds` 固定在某个值附近不动,说明有设备在高频上报
无位移的鼠标移动(常见于无线接收器)。补丁 2 已处理这种情况,确认它是否应用:
```powershell
node scripts/patch-backend.mjs --check
```
**DOM 模式读不到网页**
`read_webpage` 的 `use_dom=true` 需要浏览器(Chrome/Edge/Firefox)有活动标签页,
且该页面已加载完成。
## 已知限制
1. **不是沙箱**。守卫是正则黑名单,挡得住直白危险命令,挡不住刻意构造的绕过。
2. **wincu 升级可能破坏路径**。`resolveWcuServer` 按目录名前缀匹配,升级后版本号变化会自动重新探测。
3. **两个后端独立启动**。一个挂了另一个仍可用,但相应工具会缺失。
4. **DOM 模式依赖浏览器**。`read_webpage` 的 `use_dom` 需要 Chrome/Edge/Firefox 活动标签页。
5. **参数未做归一化**。两个后端的参数名保持原样(如 wincu 用 `elementId`,wm 用 `loc`/`label`),
模型需要按各工具自己的 schema 传参。归一化在 `Unreleased` 计划中。
6. **依赖本地后端补丁**。windows-mcp 0.8.8 的桌面控制门在部分硬件上不可用,
本项目通过 `scripts/patch-backend.mjs` 修复。已向上游报告
([CursorTouch/Windows-MCP#498](https://github.com/CursorTouch/Windows-MCP/issues/498));
若上游合并,可移除本地补丁。详见 [docs/upstream-tracking.md](docs/upstream-tracking.md)。
## 设计取舍
**为什么用「聚合」而不是「改源码合并」**
两个后端分别是 Node(wincu)和 Python(windows-mcp)。真合并意味着用同一种语言重写另一方的核心能力:
- 把 wincu 的 OCR + UIA RuntimeId 机制移植到 Python —— 工作量大且易出错
- 把 windows-mcp 的 DOM 提取移植到 Node —— 需要重写 CDP/IAccessible2 逻辑
聚合层用 MCP 协议做边界,两个后端各自演进,互不干扰。代价是多一个进程,收益是
**升级任一方都不需要改聚合层**(除非工具名变了,那会在 `buildToolList` 里报 `missing backend tool`)。
**为什么排除 PowerShell / Registry / FileSystem / Process**
不是能力不够,而是**重复且危险**:
- DSH 原生已有 `pwsh`、`read`/`write`/`edit`/`glob`/`grep`,且**受 DSH 沙箱与审批管辖**
- MCP server 是独立进程,DSH 的沙箱管不到它(`dsh-fs-sandbox` 的策略只作用于 DSH 自己的工具)
- 保留它们等于开一条绕过沙箱的通道,收益为零
## 贡献
```powershell
git clone <repo>
cd dsh-unified-desktop
npm test # 需要本机已装 wincu 与 windows-mcp
```
**添加新工具**:
1. 在 `lib/catalog.mjs` 的 `CATALOG` 加条目(`name` / `backend` / `target` / `description`)
2. 跑 `node test/catalog-test.mjs` —— 它会校验命名规范、去重、能力覆盖
3. 跑 `node test/aggregate-e2e.mjs` —— 确认后端真的提供了该工具
**修改守卫规则**:编辑 `lib/guard.mjs` 的 `PS_RULES` / `REG_DENY_PREFIX`,然后跑 `npm test`。
## License
MIT。聚合层原创;能力来自两个上游项目(均为 MIT)。
上游的版权声明见 [THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md)。
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues