Skip to main content
Glama

DesktopOS — AI-to-Windows Bridge

一个跑在 Windows 上的 MCP Server,给 AI Agent 提供持久化 PowerShell 会话。

核心区别于「每次开一个新 PowerShell 进程」:cd 之后目录保持、环境变量保持、venv 状态保持。 AI 可以像人一样连续操作,而不是每条命令都从零开始。


当前状态

阶段

内容

状态

Week 1-2

Shell Worker(持久化会话)

✅ 完成

Week 3-4

双通道感知(窗口树 + 截屏 + 交叉验证)

✅ 完成

Week 5-6

内嵌 Playwright / CDP

✅ CDP 已通(Playwright 未做,见下)

Week 7-8

操作验证(自愈)

✅ 完成:执行→验证→诊断→修补→重试

Week 9

记忆层

✅ 完成(窗口级 + 元素级)

Week 10

MCP 封装 + 开源

🚧 MCP 已通并接入 DSH,开源待做

—

元素级控件树(UIA)

✅ 完成(Electron 类应用除外,见下)


Related MCP server: Windows-MCP

快速开始

要求:Windows + Node ≥ 22(CDP 用 Node 内置的全局 WebSocket,无需额外依赖)。

cd <项目目录>

npm install       # 压缩包里已带 node_modules,可跳过
npm test          # 307 项,全绿才算可用
npm start         # 以 MCP Server 运行(stdio)

npm test 会真的起 PowerShell、真的开浏览器、真的动鼠标(只读动作),不是 mock。

注册到 MCP 客户端

把这段合并进客户端配置(模板见 examples/mcp-config.json):

"desktopos": {
  "type": "stdio",
  "command": "node",
  "args": ["<项目路径>/src/mcp-server/index.js"],
  "cwd": "<项目路径>"
}
  • DSH(新版):C:\Users\<你>\.dsh\dsh-mcp-manager.json

  • DSH(旧版 / qwenworkcn):C:\Users\<你>\.qwenworkcn\mcp.json

  • Claude Desktop:%APPDATA%\Claude\claude_desktop_config.json

  • Cursor:%USERPROFILE%\.cursor\mcp.json

哪个文件已存在就改哪个。路径用正斜杠可以避开 JSON 的反斜杠转义。重启客户端后生效。

第一次上手:读 docs/QUICKSTART.md —— 五分钟一页纸, 含可复制的客户端配置、一分钟自检清单、典型用法和已知限制。

想判断它靠不靠谱:读 docs/REVIEW.md —— 评审导览, 含 5 分钟验证路径、目录导航、已知边界和安全提示。


暴露的工具

工具

作用

shell_exec

在持久化会话中执行命令

shell_cd

切工作目录(支持中文路径)

shell_cwd

查当前目录

shell_env_set / shell_env_get

环境变量读写,跨调用保持

shell_exec_verified

执行 + 独立验证结果 + 失败退避重试(自愈)

shell_reset

会话异常时重启 PowerShell

desktop_list_windows

结构通道:枚举窗口(标题/进程/矩形/聚焦/最小化)

desktop_screenshot

视觉通道:截屏,返回 PNG/JPEG(maxWidth / format 控体积)

desktop_perceive

双通道 + 交叉验证(核心能力)

desktop_cursor

读鼠标坐标(只读)

desktop_move

移动鼠标(不点击)

desktop_click

点击 / 双击 / 右键(可带 verify + verifyRegion)

desktop_type

输入文本,支持中文

desktop_key

组合键,如 ctrl+c / alt+tab

desktop_scroll

滚轮滚动

desktop_focus_window

把指定窗口切到前台

desktop_elements

元素级:枚举窗口内控件树(名称/类型/矩形/可点击)

desktop_click_element

元素级:按名称点控件(InvokePattern,失败回落点中心)

memory_recall

把指代解析成坐标("刚才那个窗口" / "钉钉",也支持控件级)

memory_timeline

最近操作时间线

memory_stats / memory_clear

索引状态 / 清空

desktop_browser_launch

启动一个带调试端口的 Edge/Chrome(独立 profile,不抢你正在用的浏览器)

desktop_cdp_targets

只读扫描:本机有哪些可连的 CDP 端点与页面

desktop_cdp_open

连上页面建立会话(可按标题/URL 选,或导航到指定 URL)

desktop_cdp_elements

列页面内控件(角色/名称/层级,已去重)

desktop_cdp_click

按名称点控件(算矩形中心 + 注入真实鼠标事件)

desktop_cdp_type

输入文本,Input.insertText 中文天然无损

desktop_cdp_eval

在页面里执行 JS 并拿回结果

desktop_cdp_screenshot

页面级截图

desktop_cdp_close

关会话 + 杀掉自己启动的浏览器

desktop_diagnose

把一条报错翻译成「为什么 + 怎么办 + 能否自动修」

典型用法:

shell_cd   → <某个含中文的目录>
shell_exec → git status          ← 直接在上面那个目录执行

自愈用法(不轻信命令返回值,执行后真去验证):

shell_exec_verified
  command: New-Item -ItemType Directory -Path 'D:\新目录' -Force | Out-Null
  expect : { "isDir": "D:\\新目录" }
  retries: 2

expect 支持:pathExists / pathNotExists / isDir / isFile / fileContains:[路径,内容] / cwd。 验证走 Node 的 fs(原生 UTF-8 路径),不经过 shell,中文路径同样可靠。


按需裁剪工具面(多服务器共存时用)

问题不在工具多,在于重叠没人裁决。

主流 MCP 客户端(DSH / Claude Desktop / Cursor)只支持按服务器启停, 没有工具级开关。于是机器上同时装了另一个桌面自动化、一个浏览器自动化之后, 这个服务器暴露的 33 个工具里有相当一部分是在问同一个问题: 「点这个控件」有两条路,「截个图」有三条路。模型每次调用都得猜, 而猜错的概率随重叠面积上升 —— 这是装多了之后真实会出现的退化。

客户端修不了,就在服务器侧修:启动时把重叠的那部分裁掉,决策点直接消失。

# 按分组
DESKTOPOS_TOOLS=shell,diagnose node src/mcp-server/index.js

# 按工具名,可混写
DESKTOPOS_TOOLS=shell_exec,shell_cd,desktop_perceive node src/mcp-server/index.js

# 或者排除法(exclude 优先)
DESKTOPOS_TOOLS=shell,desktop DESKTOPOS_TOOLS_EXCLUDE=desktop_screenshot

# 命令行等价写法
node src/mcp-server/index.js --only=shell,diagnose

分组:

分组

工具

什么时候需要裁

shell

shell_exec / _verified / _cd / _cwd / _env_get / _env_set / _reset

几乎总是要留 —— 这套最不可替代

desktop

list_windows / perceive / screenshot / cursor / move / click / type / key / scroll / focus_window

已有别的桌面自动化时可裁

elements

elements / click_element

需要元素级点击时留

cdp

browser_launch / cdp_*(9 个)

已有 Playwright 就整组裁掉

memory

memory_*

不需要桌面状态记忆时裁

diagnose

desktop_diagnose

纯知识,建议留

三条行为保证(test/toolfilter.test.js 覆盖):

  1. 不设过滤 = 暴露全部,OSS 用户的默认体验不受影响;

  2. 裁剪后 tools/list 只剩选中的;

  3. 执行期同样拦截 —— 只裁 tools/list 是不够的:客户端的工具表可能是缓存的, 模型也可能记得旧名字,那时调用会绕过裁剪照常执行,配置看着生效其实没有。

配套:重叠审计

裁剪是手工判断,所以给了一个把「重叠」量化成数字的脚本:

npm run audit:overlap

它会连上配置里每个 MCP 服务器、拉一遍 tools/list,然后按名字与描述的相似度 输出跨服务器的近重复对。用法是当成候选清单看,不是判决:

  • 名字撞但语义不同的很常见(windows-mcp/Click 点的是操作系统窗口, playwright/browser_click 点的是页面 DOM 元素 —— 这两条本来就不该合并);

  • 中英文描述混在一起时,描述相似度会失灵,只能靠名字相似 + 人工判断;

  • 它只看 MCP 服务器,看不到客户端的原生工具(如内置的 shell / 文件读写), 所以真实重叠量会比报告数字更大。

每装一个新服务器就跑一次,别让重叠悄悄累积 —— 这是这个脚本存在的唯一理由。


自愈:诊断器

之前的自愈只有半截 —— 执行 → 验证 → 重试。但「重试」对大多数失败毫无意义: 命令写错了,再跑十次还是错。缺的是中间那步:搞清为什么失败。

现在失败会先过一遍诊断器,产出五样东西:

category    失败类别(access-denied / cmd-not-found / missing-parent / ...)
reason      人话解释
hint        可行动的提示
suggestion  具体下一步(命令或工具调用)
retryable   原样重试到底有没有意义
fix         能自动修的话,给出修补命令

retryable 是这里最关键的判断。 大多数 AI 桌面工具失败就无脑重试, 而「权限不足」「命令不存在」「参数写错」这类失败重试一万次也是浪费时间 —— 诊断器会直接说「重试无意义」,并指向真正该做的事。

自动修补有边界,不是什么都敢修

只有纯加法的修补才默认执行(目前一类:父目录缺失时补建)。 其余必须显式开 autoFix。并且有两条硬约束:

  1. 父目录真的不存在才补建 —— 否则就是「目标本身不存在」,补建毫无意义。

  2. 命令本身是创建类才补建 —— 对 Get-ChildItem 这种读取命令去建目录, 等于凭空造出一个假路径。那不是自愈,是胡来。

真实例子(测试里跑通的):

New-Item -ItemType File -Path '...\a\b\x.txt'      ← 父目录 a\b 不存在
  ↓ 诊断: missing-parent(原样重试有意义)
  ↓ 自动修补: New-Item -ItemType Directory -Path '...\a\b' -Force
  ↓ 重跑,验证通过,文件真的存在

规则刻意不接 LLM

纯字符串匹配,毫秒级返回,可测试可回归。代价是只能覆盖已知类别 —— 所以未命中时会老实说「未能归类」,并把报错原文回传,方便扩充规则表。

已覆盖:编码损坏 / 文件占用 / 权限不足 / 命令不存在 / 参数错误 / 类型转换 / 语法错误 / 超时 / 目标已存在 / 路径不存在 / 父目录缺失 / 控件找不到 / 控件歧义 / 窗口最小化 / 句柄失效 / UIA 不可用 / CDP 不可用。

任何工具报错都会自动附带诊断,不只是 shell —— 兜在 MCP 的 catch 里。


架构

Node (MCP Server)
   ├─ HTTP 127.0.0.1:<随机端口> ──▶ PowerShell 5.1 (worker.ps1)
   │     header: X-DOS-Token: <每进程随机>     同一 runspace 长期存活 ──▶ Win32 / UIA
   │     body:   { command: base64(UTF-8) }
   └─ WebSocket (CDP) ──▶ Edge / Chrome / Electron 的调试端口 ──▶ DOM + 无障碍树

为什么本地端口也必须鉴权

「只绑 127.0.0.1」不等于私有。攻击路径很具体:

  1. 恶意网页用 fetch('http://127.0.0.1:<port>/exec', { method:'POST', body: '<json>' });

  2. Content-Type 默认 text/plain,而它属于 CORS 安全列表值 —— 于是这是一个「简单请求」, 不触发预检,浏览器直接把请求发出去;

  3. 响应读不到(CORS 会拦),但副作用已经发生;端口虽然随机,也只有约 16k 个候选,页面盲扫几秒就够。

结论:不加鉴权的话,用户访问任意网页就等于把 PowerShell、鼠标和键盘交出去。

现在的做法:

  • Node 为每个 worker 生成 32 字节随机 token,经 argv 传给 PowerShell;

  • 每个请求必须带 X-DOS-Token,不匹配一律 403;

  • 带 Origin 头的请求一律 403(我们自己的 Node 客户端从不发 Origin)。

关键点在于自定义请求头:它不属于安全列表值,跨站请求因此必须走预检,浏览器会直接拦掉。 回归测试在 test/resilience.test.js,覆盖「跨站 Origin + text/plain」「无 token」「错误 token」「正确 token」四种情况。

token 走 argv,本机其他进程读得到它 —— 但那种攻击者本来就能直接执行代码,不在威胁模型内。

为什么不是 stdin 管道 + marker

最初的实现是 powershell.exe -Command -,往 stdin 写命令、靠一个 marker 字符串判断命令结束。 这条路有四个绕不开的坑(都已用实测确认):

  1. 编码:powershell.exe 用 [Console]::InputEncoding 解码管道 stdin,中文机器上是 CP936/GB2312。 Node 写的 UTF-8 中文被解成乱码 → 中文路径 100% 失效。 实测:InputEncoding : gb2312 (codepage 936)。

  2. 流交错:stdout 与 stderr 混在一起,marker 会被命令输出淹没或被残留 buffer 误判。

  3. 进程退出:终止性错误会让 PowerShell 直接 exit(1),后续 marker 永远等不到。

  4. 错误丢失:Invoke-Expression 不把内部非终止性错误可靠并入调用方错误流。

对应解法:

  • 换 HTTP 传输层:命令 base64 走 body,响应显式写 UTF-8 字节,全程不碰控制台编码。

  • 用 scriptblock 点源执行 + 重定向(. $sb 2>&1):既捕获非终止性错误,又保证变量/目录留在同一 runspace。

worker.ps1 刻意保持纯 ASCII,所有非 ASCII 数据一律 base64 传输, 连 PowerShell 默认按 ANSI 读 .ps1 这个坑也一并绕开。

踩坑记录:改这些 .ps1 时小心 AMSI。 往 perception.ps1 里加 JPEG 质量参数(ImageCodecInfo::GetImageEncoders() 配 EncoderParameters)之后,整份脚本被 Windows Defender 判成 「contains malicious content」直接拒绝执行 —— 单独跑那一小段没事, 和「截屏 + base64 + HttpListener」放在一起就触发了组合启发式。 换成朴素的 ImageFormat::Jpeg 重载即可。加任何"截屏后编码"的代码前先测一遍。


卡死之后会自己站起来

PowerShell 5.1 的 runspace 是单线程的:一条卡住的命令(等输入的交互程序、 忘了 -y 的 npm install、一直不返回的网络请求)会让 GetContext() 永远回不去,之后所有请求全部超时。

而且早期的 stop() 是 await 一个没有超时的 /shutdown —— 对端已经卡死, 于是逃生门自己也挂住,shell_reset 一样不返回,只能重启 MCP 客户端。

现在的分层:

层

机制

命令

超时即抛出;自动硬重启 worker,并把记住的 cwd 与环境变量重放回去

请求

_request 自带超时(命令超时 +5s),任何一层都不能把上一层永远挂住

停止

先短超时(1.5s)尝试优雅退出,然后无条件 kill 并等待真实退出

启动

缓存的是启动 Promise,并发调用共用同一次 spawn(_spawnOnce 最多重试 3 次)

所以卡一次命令的代价是「重启 + 重放状态」,不是「这一整个会话废了」。 代价要说清楚:进程内没被 shell_env_set 记录过的变量、以及非 cwd 的其它 runspace 状态(函数定义、模块导入)确实会丢。test/resilience.test.js 验的就是 cwd 和 通过 shell_env_set 写过的变量能活下来。


双通道感知

所有 AI 桌面工具都在做「截图 → OCR → 猜」,这是错误架构。这里两条通道同时开:

通道

实现

产出

结构

Win32 P/Invoke:EnumWindows / GetWindowRect / GetWindowText / GetForegroundWindow

句柄、标题、进程、PID、矩形、聚焦、最小化

视觉

System.Drawing 全屏截屏,按窗口矩形采样像素算标准差

该区域是否真有内容渲染

交叉验证:结构通道说「可见」的窗口,视觉通道必须证明它 (1) 在屏幕内 (2) 真有内容。否则标为不一致,原因分三类:

minimized  最小化(Windows 把它们停放到 -32000/-21333 这类坐标)
offscreen  在屏幕外
blank      区域无内容(如 1x1 的隐藏窗口)

实测本机 17 个窗口:一致 5 个,不一致 12 个(minimized 8 / offscreen 2 / blank 2)。 关键是 IsWindowVisible 对最小化窗口也返回 true —— 只看窗口列表会误判,必须双通道核对。


执行层(UI 执行器)

真实输入注入:focus / move / click / scroll / type / key。

中文输入是个坑:SendKeys 不支持 Unicode,中文会被打成乱码。 所以输入走剪贴板(SetText,完整 Unicode)+ Ctrl+V,中文完好无损—— 和整个项目一致的思路:非 ASCII 一律不裸奔过 shell/控制台编码。

内置验证:任何动作都可以带 verify,前后各截一次屏,采样比对后返回 diffRatio。 「操作 → 验证」因此是基础设施,不用每个调用方自己拼。

验证可以限定区域:verifyRegion: { x, y, w, h }(通常直接传目标窗口的 rect)。 不传就比对全屏 —— 而全屏比对回答的是「屏幕上有没有东西变」, 不是「我这个动作有没有让目标变」;一个跳秒的时钟或闪烁的光标都会被算进去。 返回里带 scope: "region" | "screen",一眼能看出这次结论的力度。

典型闭环:

desktop_perceive                       → 拿到目标窗口 handle 与 rect
desktop_click {x,y,verify,verifyRegion → 点它,只在那个窗口范围内比对
              :<rect>}
desktop_screenshot                     → 肉眼确认

⚠️ 点击和输入会真实抢走你的鼠标与键盘焦点。 自动化测试只覆盖只读与无副作用动作(cursor / move), 真实 click / type / key 请用 test/_dbg-actor.js 手动触发,或经 MCP 显式调用。

node test/_dbg-actor.js cursor
node test/_dbg-actor.js click 500 300        # 会真点,工具内置 5 秒缓冲
node test/_dbg-actor.js type "中文输入测试"

记忆层

解决 AI 操作电脑最痛的一点:不记得之前做过什么、看过什么。 有了它,「点击刚才那个按钮」这类指代才成立。

三块内容:

组成

实现

操作日志

每次 perceive / act 自动记一条,带时间戳

特征索引

窗口的 8×8 dHash 差分哈希(真正的「截图 → 向量」),可按汉明距离再认出来

上下文管理器

聚焦窗口历史 / 导航栈

持久化在 ~/.desktopos/memory.json,跨进程、跨重启保留。

实测效果:

"刚才那个窗口"  -> 微信 [WeChat]        坐标(853,509)   按"最近"定位
"explorer"      -> Program Manager      坐标(854,534)   按关键词定位
"Excel"         -> 存在但已最小化,请先用 desktop_focus_window 还原(句柄 68036)

最后一条是关键设计:区分「真没有」和「有但最小化了」。 说成「不存在」会把 Agent 带偏,所以给了可行动的提示 + 句柄。

感知哈希是 8×8 灰度差分哈希(dHash),不需要任何 ML 模型 —— 窗口移动、缩放后仍能再认。

为什么不是 aHash:aHash 只记「这一格比平均值亮还是暗」,梯度方向与颜色全丢。 实测在本机 33 个窗口里,aHash 把「文件资源管理器」和一个毫不相关的网页算成 完全相同的 64 位指纹(距离 0);而 findByHash 的用途正是把「刚才那个窗口」 解析成坐标,指错了下一步就是真点击。换成 dHash 之后重新抓帧比对 105 对窗口, 只有 2 对距离为 0(都是同类的扁平小窗),98 对距离 >12,分离度足够。

即便如此,哈希也只是弱辅助信号:findByHash(hash, { expect: { title, process } }) 建议带上标题/进程做交叉校验,只有对上才算同一个目标;默认阈值也从 10 收紧到 6。 纯色 / 空白区域哈希相同仍属正常退化现象。

索引已经不止窗口级:desktop_elements / desktop_perceive 会自动把控件也写进索引, 「刚才那个按钮」这类指代因此可以命中元素而不是窗口。


元素级感知(UIA)

从「点窗口中心」进化到「点那个按钮」,靠 System.Windows.Automation(UIA)枚举控件树:

desktop_list_windows      → handle=854182(现在会带句柄输出)
desktop_elements          → { handle: 854182 }
desktop_click_element     → { handle: 854182, name: "小组件" }

desktop_elements 返回每个控件的 名称 / 控件类型 / 矩形 / 是否支持 Invoke。 desktop_click_element 优先走 InvokePattern.Invoke()(不需要坐标,最小化也能触发), 不支持时才回落到「算矩形中心 → 真实点击」。

实测控件名中文往返无损:

Button       小组件 22°C 大部晴朗
Group        22°C 大部晴朗

已知边界:Electron / Chromium 是瞎的

应用

枚举到的控件数

explorer

28

msedge

25

WorkBuddy(Electron)

0

UIA 看不见 Chromium 渲染树内部 —— 这类应用整个窗口只暴露一两个 pane, 再往下就是一片黑。返回 0 时工具会给出解释而不是崩溃: 「该应用不暴露 UI Automation 控件树(Electron/Chromium 常见)」。

这正是 Week 5-6 要接 Playwright / CDP 的直接原因: 对 Chromium 系应用,UIA 这条路走不通,必须走浏览器调试协议。


CDP 通道(看见 Chromium 内部)

UIA 看不进去的地方,用 Chrome DevTools Protocol 直接问浏览器。零新增依赖 —— Node 22 起 WebSocket 已是全局对象。

desktop_browser_launch { headless: false, url: "https://..." }   -> 返回端口
desktop_cdp_open       { port, url, navigate: true }             -> 会话 s1
desktop_cdp_elements   { session: "s1" }                         -> [button] 搜索 / [textbox] 请输入
desktop_cdp_type       { text: "中文关键词", target: "请输入" }
desktop_cdp_click      { name: "搜索" }
desktop_cdp_eval       { expression: "document.title" }
desktop_cdp_close      {}

语义刻意和元素级 UIA 对齐(elements / click / type),上层不用分两套心智模型。

三个关键设计

1. 必须先开调试端口。 浏览器默认不暴露 CDP。所以给了一个 launcher: 带 --remote-debugging-port + 独立 --user-data-dir,不会和你正在用的浏览器抢 profile。 端口只监听 127.0.0.1(CDP 默认行为)。

2. Input.insertText 而不是键盘事件。 中文走 UIA 那套要剪贴板 + Ctrl+V;CDP 有 Input.insertText,直接写入、不经过键盘布局, 中文和 emoji 都无损 —— 比前两条路都干净。

3. AX 树必须去重。 getFullAXTree 会给一个按钮同时产出 button / StaticText / InlineTextBox 三个同名节点。 那是同一个控件的三种投影,不是三个控件 —— 不去重的话「点搜索」会被判成歧义而失败。 elements() 默认丢掉与交互式节点同名的文本投影;find() 再按「交互式角色优先 + 层级更浅优先」排序。 (不修这个,实测枚举出来是 13 个控件、点击直接报错;修完是 5 个、点击正常。)

⚠️ headless: false 时浏览器窗口会真的弹出来并抢焦点,自动化场景用 headless: true。 用完记得 desktop_cdp_close,否则无头浏览器会留在后台。

为什么没上 Playwright

Playwright 的价值是等待/定位/自动重试,代价是几十 MB 依赖 + 浏览器下载。 CDP 直连已经拿到了 Playwright 的底层能力(DOM、AX 树、输入注入), 而「等待」可以用 waitReady() + desktop_cdp_eval 自己表达。 等真出现「选择器 + 自动等待」的强需求再引不迟。

已知边界

  • 需要浏览器带调试端口启动,无法凭空虚附到已在运行的实例上(这是浏览器侧限制)。

  • 页面级坐标系,不是屏幕坐标 —— 所以 CDP 的点击和 desktop_click 的屏幕坐标不是一个东西。


测试覆盖

npm test = 6 编码守卫 + 13 Shell Worker + 15 韧性/安全 + 13 工具过滤 + 20 感知

  • 11 执行 + 123 诊断 + 38 记忆 + 81 MCP 端到端,共 320 项。

其中 CDP 那 16 项是真端到端:真的起一个无头 Edge、真的导航、真的注入鼠标事件、 断言页面 DOM 真的变了。本机没装 Edge/Chrome 时自动 SKIP,不算失败。

**韧性/安全(resilience.test.js,新增)**覆盖三类曾经真实出过的问题:

  • 鉴权:跨站 Origin + text/plain、无 token、错误 token 一律 403;正确 token 才执行

  • 卡死恢复:命令超时后会话自动恢复;cwd 与 shell_env_set 写过的变量被重放回来; 卡死状态下 stop() 仍在有限时间内返回

  • 并发冷启动:5 个并发 start() 只 spawn 一个进程

  • 请求体上限:5 MB body 返回 413,而不是被 ReadToEnd() 全读进内存

编码守卫(ascii-guard):强制所有 .ps1 纯 ASCII。 PowerShell 5.1 无 BOM 时按 ANSI/CP936 读 .ps1,混进中文注释会被解成乱码、 并悄悄破坏紧随其后的代码行(曾导致一行 $min = if (...) {...} 整体失效, 输出的 JSON 变成 "minimized":,)。这条测试就是这条规则的执行者。

重点回归点:

  • cd 到含中文的路径(...\代码\desktopos)—— 旧方案在此必挂

  • 中文窗口标题经感知层往返无损(无 U+FFFD)

  • 目录/环境变量跨调用持久化

  • 非终止性错误、终止性错误、原生命令 stderr 都能捕获

  • 出错后会话仍存活(不挂死)

  • 交叉验证自洽:一致数 + 不一致数 = 总数,且与布尔字段一致

  • 元素枚举:至少一个真实窗口能枚举出控件(正向),且控件名无 U+FFFD

  • 点不存在的控件必须返回 isError,不能误触任何东西

  • 诊断器判定 retryable: false 时真的不再重试(不是只写在提示里)

  • 路径里出现 C:\Users\Administrator\... 不能被误判成权限不足

  • dispatcher 里能处理的工具必须都在 tools/list 登记过(否则 Agent 根本看不到它)

调试工具(非测试)

  • test/_dbg-exec.js —— 逐个查看命令的 output/error/exitCode,排查错误捕获

  • test/_dbg-perception.js —— 打印双通道明细与耗时,排查感知问题

  • test/_dbg-actor.js —— 手动触发真实 click / type / key(自动化测试不碰)

  • test/_dbg-elements.js —— 打印指定窗口的控件树,排查 UIA 枚举

  • test/_dbg-cdp.js —— CDP 调试:survey 扫描端点 / launch 起实例 / smoke 全自动冒烟


文件结构

src/
├── mcp-server/index.js      MCP Server(stdio transport,33 个工具)
├── http-worker/index.js     三个 worker 共享的传输层:token 鉴权 / 分层超时 / 端口重试 / 无条件 kill
├── verifier/index.js        执行→验证→诊断→修补→重试(自愈闭环,retryable 已接线)
├── diagnoser/index.js       错误诊断器:18 类失败规则,产出原因/建议/修补
├── cdp/
│   ├── index.js             CDP 高层:Browser / Session(elements/click/type/eval/screenshot)
│   ├── client.js            极简 CDP 客户端(原生 WebSocket,零依赖)
│   ├── discover.js          扫描 127.0.0.1 上的调试端点 /json/version、/json/list
│   └── launch.js            带调试端口 + 一次性 profile 启动 Edge/Chrome
├── sensor/
│   ├── index.js             Node 端:感知 worker 封装(listWindows/screenshot/perceive/elements)
│   └── perception.ps1       PowerShell 端:结构通道 + 视觉通道 + 交叉验证 + UIA + dHash
├── actor/
│   ├── index.js             Node 端:执行 worker 封装(focus/click/type/key/scroll/element)
│   └── actor.ps1            PowerShell 端:Win32 输入注入 + 区域化截图比对 + UIA Invoke
├── memory/index.js          记忆层:操作日志 / 特征索引 / 上下文栈 / 元素索引
└── shell-worker/
    ├── index.js             Node 端:会话封装 + 卡死后自动重启与状态重放
    └── worker.ps1           PowerShell 端:HttpListener + token 校验 + 执行引擎
test/
├── ascii-guard.test.js      .ps1 纯 ASCII 守卫(防编码回归)
├── shell-worker.test.js     Shell Worker 单元/集成
├── resilience.test.js       鉴权 / 卡死恢复 / 并发单例 / 请求体上限
├── perception.test.js       双通道感知 + 元素级枚举
├── actor.test.js            UI 执行层(安全子集)
├── diagnoser.test.js        诊断器:分类 + 真自愈集成(123 项)
├── memory.test.js           记忆层(用临时存储,不污染真实记忆)
├── mcp-e2e.test.js          MCP 协议端到端
├── _dbg-*.js                手动调试工具(不参与 npm test)
└── _probe-*.js              评审期复现脚本(不参与 npm test)

Available Tools

33 tools
desktop_browser_launchA

启动一个"可被 CDP 调试"的浏览器实例(Edge / Chrome)。 带 --remote-debugging-port 和一次性的 --user-data-dir(不传时每个实例独立目录, 关闭时自动删除),不会抢你正在用的浏览器 profile。 端口只监听 127.0.0.1。返回端口号,之后用 desktop_cdp_open 连它。 headless=true 时不弹窗口,适合自动化;headless=false 时窗口会真的跳出来。

Inputs: url, waitMs, headless, browserPath, +1 more.

desktop_cdp_clickA

按名称点击页面里的控件(走 CDP 注入真实鼠标事件)。 先定位控件矩形中心再点,所以不需要你猜坐标。同名多个时会报错并列出候选。

Inputs: name (required), role, session.

desktop_cdp_closeA

关闭 CDP 会话,并杀掉由 desktop_browser_launch 启动的浏览器进程。 用完记得调用,否则无头浏览器会留在后台。

Inputs: session, killBrowser.

desktop_cdp_elementsB

列出当前页面内的控件(CDP 无障碍树)。 返回角色(button/link/textbox/...)、名称、层级深度。中文名称完整保留。 已自动去重:AX 树给一个按钮同时产出 button/StaticText/InlineTextBox,这里只留能操作的那个。

Inputs: role, limit, session.

desktop_cdp_evalA

在当前页面执行 JavaScript 并返回结果(JSON 化)。 这是 CDP 相对 UIA 的杀手锏:拿不到的信息可以直接问页面要。

Inputs: expression (required), session, awaitPromise.

desktop_cdp_openA

连上一个浏览器的页面,建立 CDP 会话(后续 cdp_* 工具都作用于它)。 可按 title / url 子串或下标选页面;带 url 时会先导航过去。 这是操作 Chromium 系应用(Chrome / Edge / Electron)的入口 —— UIA 看不见它们的内部,CDP 能。

Inputs: url, port, index, title, +1 more.

desktop_cdp_screenshotA

截取当前页面(不是整屏)。返回 PNG。 页面级截图比全屏截图更适合判断"浏览器里现在是什么状态"。

Inputs: session.

desktop_cdp_targetsA

只读:扫描本机所有开放的 CDP 调试端点及其页面,不做任何修改。 用来判断"现在能不能连浏览器"。 如果为空,说明没有浏览器开调试端口 —— 用 desktop_browser_launch 启动一个。

Inputs: no named parameters.

desktop_cdp_typeB

向页面输入文本。走 CDP 的 Input.insertText, 不经过键盘布局,中文 / emoji 天然无损(比 SendKeys 和剪贴板都干净)。 可用 target 先聚焦到某个输入框。

Inputs: text (required), clear, target, session.

desktop_clickA

在指定坐标点击鼠标。会真实操作你的桌面 —— 调用前请确认目标位置。 坐标可从 desktop_perceive 得到的窗口矩形推算(例如取其中心)。

Inputs: x (required), y (required), count, button, +2 more.

desktop_click_elementB

元素级执行:按可见文本定位控件并触发它。 优先用 UI Automation 的 InvokePattern(对按钮最可靠),不支持时退回点击其中心。 会真实操作界面,调用前请确认控件名称。

Inputs: name (required), handle (required), verify, verifyRegion.

desktop_cursorA

读取当前鼠标光标坐标(只读,安全)。

Inputs: no named parameters.

desktop_diagnoseA

把一条报错文本翻译成"为什么 + 怎么办"。 任何工具返回错误时都会自动附带诊断;这个工具用于手动诊断任意一段报错。 规则匹配、毫秒级返回,不调 LLM。

Inputs: error (required), output, command.

desktop_elementsA

元素级结构通道:枚举一个窗口内的 UI 控件(UI Automation)。 返回控件名称、类型(Button/Edit/...)、AutomationId、坐标矩形、是否可 Invoke。 适合定位"确定按钮"这类具体控件,比只按坐标猜可靠得多。 注意:Electron / Chromium 类应用默认不暴露控件树,会返回很少或 0 个 (这类需要用 CDP,即路线图 Week 5-6)。原生 Windows 应用正常。

Inputs: handle (required), maxElements.

desktop_focus_windowA

把指定句柄的窗口切到前台(最小化时会先还原)。句柄来自 desktop_list_windows / desktop_perceive。

Inputs: handle (required), verify.

desktop_keyB

发送组合键,如 ctrl+c / ctrl+v / alt+tab / enter / tab / f5。会作用于当前焦点窗口。

Inputs: combo (required), verify, verifyRegion.

desktop_list_windowsA

结构通道:枚举当前所有可见窗口。 返回句柄、标题、进程名、PID、屏幕矩形、是否聚焦、是否最小化。 用它可以知道"屏幕上有什么",以及目标窗口的位置。

Inputs: no named parameters.

desktop_moveA

移动鼠标到指定坐标(不点击)。副作用小,可用于定位或配合 verify 观察界面反应。

Inputs: x (required), y (required), verify.

desktop_perceiveA

双通道感知 + 交叉验证(核心能力)。 结构通道枚举窗口,视觉通道对每个窗口区域采样,判断它是否真的可见、真的有内容。 返回每个窗口的一致性结论,并列出不一致原因(minimized / offscreen / blank)。 结果会自动写入记忆层,之后可用 memory_recall 按指代定位。

Inputs: format, maxWidth, includePng.

desktop_screenshotA

视觉通道:截取主屏幕。返回 PNG/JPEG(base64)与尺寸。 用于看布局、颜色、图标等结构通道拿不到的信息。 需要控制消息体积时传 maxWidth(如 1280)+ format:"jpeg"。

Inputs: format, maxWidth, includePng.

desktop_scrollC

在指定坐标滚动滚轮。delta 为负向下、正向上,通常 120 为一格。

Inputs: x (required), y (required), delta, verify, +1 more.

desktop_typeA

在当前焦点处输入文本,支持中文。 实现走剪贴板 + Ctrl+V(SendKeys 不支持 Unicode,会把中文打成乱码)。 注意:会改动你的剪贴板内容,并输入到当前焦点窗口。

Inputs: text (required), verify, verifyRegion.

memory_clearB

记忆层:清空时间线与窗口索引。

Inputs: no named parameters.

memory_recallB

记忆层:把一句指代解析成可点击的坐标。 支持 "刚才那个窗口"、"钉钉"、"WorkBuddy" 这类说法,返回窗口中心坐标, 可直接拿去 desktop_click。 支持窗口级与元素级两种命中("刚才那个按钮" 会定位到控件)。

Inputs: query (required).

memory_statsA

记忆层:查看索引规模与最后更新时间。

Inputs: no named parameters.

memory_timelineB

记忆层:查看最近的操作时间线(感知/点击/输入/命令)。

Inputs: limit.

shell_cdA

修改持久化会话的工作目录(cd),后续 shell_exec 都在新目录执行。

Inputs: path (required).

shell_cwdB

获取持久化会话的当前工作目录。

Inputs: no named parameters.

shell_env_getB

获取持久化会话中的环境变量值。

Inputs: name (required).

shell_env_setA

在持久化会话中设置环境变量。后续所有 shell_exec 都能读到。

Inputs: name (required), value (required).

shell_execA

在持久化 PowerShell 会话中执行命令。 核心创新:这是一个长期存活的 Shell 进程,不是每次开新进程。 效果:cd 之后目录保持、set 的环境变量保持、Python venv / node -e 等状态保持。 适用于:需要连续操作的场景(先 cd 再 git status、先激活 venv 再 pip install)。

Inputs: command (required), timeout.

shell_exec_verifiedA

执行命令,并用文件系统 API 独立验证结果;验证不过自动带指数退避重试(自愈雏形)。 不轻信命令自身的返回值:例如 mkdir 之后,真的去确认目录存在。 expect 支持:pathExists / pathNotExists / isDir / isFile / fileContains:[路径,内容] / cwd

Inputs: expect (required), command (required), retries, timeout.

shell_resetA

重置持久化会话(重新启动 PowerShell)。用于会话状态异常时恢复。

Inputs: no named parameters.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A3.5/5.0

    Scored across 33 tools

    Disambiguation4/5

    The tool set is large but mostly split into clearly explained channels: persistent shell, window/UIA perception, CDP browser control, and memory. There is still meaningful overlap between shell_exec/shell_exec_verified/shell_cd, desktop_elements/desktop_cdp_elements, and desktop_click/desktop_click_element/desktop_cdp_click, though descriptions give guidance on when to use each.

    Naming Consistency4/5

    Almost all tools use snake_case with subsystem prefixes (shell_, desktop_, memory_), which makes grouping predictable. Minor deviations exist: some names are verb_noun (desktop_list_windows, shell_env_get) while others are noun-only or verb-only (shell_cwd, shell_reset, memory_timeline).

    Tool Count3/5

    33 tools is heavy for one server and exceeds the comfortable range, but it is spread across several genuinely distinct subsystems (shell, UIA desktop, CDP browser, memory, diagnostics). It is borderline: more than ideal, but not arbitrary bloat.

    Completeness4/5

    Coverage is strong for a desktop automation agent: persistent shell with env/CD, verified execution, window enumeration/perception, mouse/keyboard, UIA element interaction, full CDP lifecycle, memory recall, and diagnostics. Gaps remain for window manipulation (move/resize/close), richer mouse actions (right-click/double-click/drag), and file-level operations, but core workflows are covered.

    Maintenance

    ActivityMaintained
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      Not graded
      quality
      C
      maintenance
      Enables AI agents to interact with Windows operating systems through native UI automation, file navigation, application control, and system commands. Provides seamless integration between LLMs and Windows environments for tasks like clicking, typing, launching apps, and capturing desktop state.
      MIT