前一阵我把手头的编码代理方案整个推翻重做了一遍,最终产出了一个叫 QAgent 的小工具。它可以当命令行 AI 编码助手用,也能直接读写屏幕上的桌面应用界面,还支持 MCP 协议双向接入——整个东西只有一个可执行文件,不用装 Node、不用装 Python、不用配环境变量,Windows 和 macOS 上双击就能跑。这篇东西就把它的设计思路、核心实现和踩坑过程捋一遍,给同样在折腾 AI 编码代理和 GUI 自动化的朋友做个参考。
我平时工作里大量时间花在“改完代码要打开桌面程序验证效果”这件事上。常见编码代理能读项目代码、能执行终端命令,但一遇到桌面 GUI 就断了:需要人工点开程序、切页面、点按钮、看结果。QAgent 就是冲着这个缺口来的——既保底编码代理该有的能力,又能把 GUI 操作变成 AI 能调用的工具,同时把 MCP 生态里各种现成能力也一并接进来。如果你想要一个离线、可控、能操作真实桌面的 AI 代理,这篇文章值得看完。
1. 痛点拆解:为什么现成的编码代理“够强但不够用”
1.1 我为什么觉得“光会写代码”的编码代理不够用
这两年 AI 编码代理领域卷得很厉害。Codex、Claude Code、通义灵码之类的工具,能理解整个仓库结构,能自动改文件、跑测试、提交 commit,确实把“坐在终端里写代码”这件事体验拉满。但问题是,软件开发的完整闭环不只发生在终端里。
我做的大部分项目都带图形界面。比如一个跨平台的桌面工具:改完配色主题、调完布局参数,总得启动程序、打开设置面板、点一下预览按钮,确认输出是否符合预期。这种验证步骤对纯命令行编码代理来说属于“能力边界之外”——它能看到文件变更,但看不到窗口里的真实渲染结果。
我试过用 Playwright 去模拟,但它只对浏览器有效,对原生桌面应用无能为力。我也试过用“截图给 AI 看,再由 AI 给出点击坐标”的方案,效果很不稳定:换一台电脑、改一个屏幕分辨率,所有坐标就全废了。换句话说,缺的不是“AI 更聪明”,而是“让 AI 能安全、稳定地触碰桌面 GUI”的那一层桥梁。
1.2 最后敲定的功能边界与目标形态
在动手之前,我给这个工具划了几条硬性边界,避免做成一锅乱炖:
- 编码代理能力不可丢:能读项目、能改文件、能执行命令是底座。工具得能在命令行里回答代码问题,像普通 agent 一样工作。
- GUI 操作要走“语义化”路线:优先基于系统辅助功能接口拿到控件树,而不是靠视觉模型猜坐标。只有在拿不到控件树的场合才退化到屏幕定位模拟。
- MCP 必须双向支持:既能作为 MCP Server 把我的 GUI 能力提供给 Claude Code、Dify、Cherry Studio 这类客户端调用;也能作为 MCP Client 去加载外部第三方 MCP Server,把别的工具能力桥接进来。
- 单文件运行是硬需求:不希望用户为了用一个小工具去装 Node 环境和十几个 npm 包。core 用 Go 编译成单一二进制,脚本扩展走内置 JS 运行时,所有模块都打进一个文件里。
于是就有了 QAgent:一个二进制,两种启动方式,三条能力线。代码用 MIT 协议开源,离线可用,默认不把任何操作数据外传。
2. 单文件运行与整体架构:Go 底子加 JS 皮肤
2.1 为什么选 Go:单文件、无依赖、交叉编译香
我最开始先用 Rust 和 Go 各做了一版原型。Rust 写 GUI 桥接的底层确实稳,但构建链重,跨平台交叉编译要折腾 target 工具链;Go 在这件事上几乎是为“单文件分发”而生的:GOOS=windows GOARCH=amd64 go build一行命令就能出一个 Windows 可执行文件,macOS 同理,不需要目标平台上预装任何运行时。
更重要的是,Go 标准库里就有syscall,配合 x/sys 包可以直接调用操作系统的辅助功能 API。Windows 下的 UI Automation 走 COM 接口,macOS 下走 Accessibility 的 AXUIElement,Go 都有成熟的封装路径。让我意外的是,Go 的并发原语在这种“一个 agent 同时要盯多个窗口事件”的场景里非常好用——每个 GUI 会话是两个 goroutine 在跑,一个监听事件,一个执行动作,调度天然不打架。
还有一个决策是内置 JS 运行时背景,就是 goja。我把它嵌进去,这样 GUI 脚本的“自定义逻辑”不需要用户重新编译主程序,只要写一段 JS 放进配置目录,QAgent 启动时自动加载。这相当于给了用户一个插件接口,工具本身还是单文件,但扩展能力不用靠改动主代码。
2.2 双模式设计:CLI 代理与 MCP Server 同时成立
QAgent 的入口命令设计成双子命令,这让它在两种使用姿势之间无缝切换:
# 模式一:直接作为命令行编码代理,提问或派活 qagent run "帮我看看这个仓库里登录流程的异常处理逻辑" # 模式二:作为 MCP Server 跑起来,供外部客户端连接 qagent mcp --transport stdio # 或者用 SSE 传输,暴露一个本地端口 qagent mcp --transport sse --port 9888CLI 模式适合我这种重度终端用户:直接在项目目录里跑一句自然语言指令,QAgent 拉取文件上下文、执行搜索、生成补丁,必要时调用 GUI 工具链去操作桌面上正在运行的程序。MCP Server 模式则把同样的能力打包成标准化工具,让任何支持 MCP 的客户端都能接入。
这两种模式共享底层同一套能力内核。内核里有几个核心模块:repo(代码索引与编辑)、exec(命令执行)、gui(控件树获取与动作注入)、mcp(协议解析和工具路由)。切换模式只改变“对外开口”的方式,不改变能力本身。这样设计的好处是维护一套逻辑,无论在终端用还是在 Dify 里用,行为都是一致的。
2.3 一条命令跑起来的最小配置
我尽量让零配置也可以启动。首次运行生成一个qagent.yaml,里面只放必要的选项。以下是我目前默认生成的配置样子:
# qagent.yaml mode: agent # agent: CLI编码代理;mcp-server: MCP服务端模式 workspace: ./ # 默认工作目录 allow_gui_apps: # GUI操作白名单,不在这里面的应用,AI无法触碰 - Calculator - Notepad - MyApp log_level: info注意allow_gui_apps这一栏,这是安全设计的关键,后面细说。总之,首次启动后用户只需要决定“这个 agent 能碰哪些桌面应用”,然后把应用名填进去就好。白名单以外的窗口,GUI 工具一律拒绝操作,宁可少做不能乱来。
3. GUI 操控的核心原理:把桌面程序当 DOM 树来操作
3.1 控件树原理:为什么不用纯视觉方案
很多做 GUI 自动化的外行人第一反应是:给 AI 接一个截图接口,AI 看到图片后给出“点哪里”的坐标。这方案听起来直觉,实际用起来浑身是坑。屏幕分辨率、操作系统缩放比例、窗口位置、弹窗遮挡,任何一个因素变了,坐标全部失效。而且“AI 看图猜坐标”的过程不可枚举、不可验证——它可能正确点击,也可能点歪,出问题后很难排查。
QAgent 走的是另一条路:优先读取操作系统辅助功能接口暴露的“控件树”。这个树很像网页的 DOM:每个控件有类型(按钮、输入框、列表项、菜单)、有名称(比如“登录”“取消”)、有窗口句柄和位置信息。拿到这棵树,GUI 操作就变成了“找到名为‘登录’的 Button,执行点击动作”,和坐标无关,和分辨率无关,语义清清楚楚。
Windows 下我用的是 UI Automation 的 COM 接口;macOS 则是通过 Accessibility 拿到 AXUIElement 层级。两条渠道拿到的都是结构化的控件元数据。为了统一,我在内部定义了一个中间结构:
{ "type": "button", "name": "登录", "enabled": true, "bounds": {"x": 350, "y": 240, "width": 90, "height": 32}, "handle": "0x1A2B3C" }GUI 工具执行时,核心逻辑就是“类型 + 名称”定位控件,然后调用对应的注入方法。例如InvokePattern表示点击,ValuePattern表示输入文本。这套逻辑的可测试性比视觉方案高得多——控件树可以导出、可以 diff,操作前后一比较,立刻知道控件状态变化。
3.2 三类后端策略与退化逻辑
操作系统辅助功能接口虽然强大,但覆盖面不是百分百。我在实践里总结出三条策略,按优先级排:
- UI Automation / Accessibility 控件树:语义清晰,首选。
- 窗口句柄 + 消息注入:拿不到完整控件树时,退而找顶层窗口,模拟键盘输入和快捷键,比如 Tab 切换焦点、Enter 确认。够用但不精细。
- 屏幕模板匹配兜底:对游戏、自绘 UI、远程桌面这类完全不暴露辅助信息的场景,只能退守到截屏 + 特征点匹配 + 坐标点击。这个模式默认关闭,需要用户在配置里显式开启,因为不可靠。
这三条合起来叫“GUI Backend Cascade”。QAgent 每次执行 GUI 操作前,会先探测目标进程是否暴露控件树,逐级降级。我在远程桌面会话里踩过这坑——远程连接里的 UI Automation 树会退化得几乎为空,只能靠第二、第三级兜底。所以这类场景我在文档里直接建议用户:想稳定自动化,先在物理机或 VM 里跑。
3.3 弹窗、焦点与缩放的坑
控件树方案也有一堆反直觉的坑,说两个最典型的。
坑一:控件在树里,但窗口没激活。控件树存在不代表窗口可见、更不代表它在前台。操作系统对于向后台窗口注入点击的处理差别很大,有的窗口会直接忽略。所以在执行任何点击动作前,必须有一个focus_window的前置原语:先把目标窗口拉到前台、等它激活,再执行后续动作。我用一个“先聚焦,再操作,后校验”的三步流程,校验阶段会确认目标焦点是否真的落在了预期控件上,防先把动作发出去了结果没人接。
坑二:DPI 缩放会把坐标搞晕。很多控件树的 bounds 值是“物理坐标”,而屏幕截图和鼠标注入用的是“逻辑坐标”。在设置过 150% 缩放的 Windows 机器上,两者差 1.5 倍。我最初一个定位偏差问题缠了两天,后来在中间层统一做了一个 DPI 换算:所有坐标都以某个基准 DPI 下发的逻辑坐标为准,注入前再换算回系统物理坐标。如果你打算自己写类似的桥接,这一点务必提前设计进去,不然后期全是玄学 bug。
3.4 危险操作防护:白名单与前状态快照
GUI 自动化的风险在于“AI 手滑点错”。我自己的原则是:AI 可以犯错,但不能造成无法挽回的破坏。所以 QAgent 做了两层防护。
第一层是开头提过的allow_gui_apps白名单。不在名单里的应用,GUI 工具直接拒绝返回“Permission denied”。这避免 AI 在自由发挥时点到邮件、支付、系统设置这类高风险窗口。我自己用的一台机器上,白名单只有四个应用:项目的 demo 程序、计算器、记事本和终端。
第二层是操作前的状态快照和操作后的差异比对。GUI 工具在执行一系列动作之前,会自动记录目标窗口的控件快照(有哪些控件、什么状态);动作完成后再次抓取快照,把差异输出给 agent 参考。本质上这就是一个“GUI 级 diff”,让 agent 能判断操作是否真的生效了,而不是闭眼点完就交差。
3.5 实操命令速览
平时我最常用的命令大概是这几个:
# 列出所有窗口,确认目标进程的窗口句柄 qagent gui windows # 导出某个窗口的控件树,用于定位具体控件 qagent gui dump --pid 1826 # 按名称点击按钮 qagent gui click "登录" --app MyApp # 向输入框填入文本 qagent gui type "admin@example.com" --target "用户名输入框" # 截取窗口当前画面 qagent gui screenshot --app MyApp --output current.png这些操作都可以在 CLI 模式里被 agent 自主调用,也可以作为 MCP 工具暴露给外部客户端。命令行层面做得笨一点没关系,重点是语义可靠、可复现——这意味着同一套 GUI 操作写进自动化测试里也能稳定跑。
4. MCP 双向桥接:把自己接进更广阔的 Agent 生态
4.1 MCP 是什么:给 AI 一个标准“外设接口”
MCP(Model Context Protocol)可以理解成 AI 界的 USB 接口标准。在 MCP 之前,每个 AI 编码代理都自己定义一套工具调用协议,第三方能力想要接入就得写各种适配小插件。MCP 的目的是统一这个接口:无论你是 Claude、Dify 还是 Cherry Studio,只要是 MCP 客户端,就能通过同一套协议去调用任何 MCP Server 暴露的工具。
QAgent 在 MCP 体系里扮演的角色比较特殊:既是“外设”,又是“集线器”。作为外设,它把 GUI 操作能力打包成标准 tools;作为集线器,它又能当一个中间代理人去加载第三方的 MCP Server 能力。这样它在整个生态里就变成一个“能碰桌面的万能接口”。
4.2 作为 MCP Server:把 GUI 能力注册成 tools
当 QAgent 以 MCP Server 模式跑起来后,会往外暴露这几个核心工具:
| 工具名 | 功能 | 输入 |
|---|---|---|
gui_list_windows | 列出可见窗口 | 无 |
gui_dump_tree | 导出控件树 | app 名称 / pid |
gui_click | 按名称点击控件 | app、控件名 |
gui_type | 向控件输入文本 | app、控件名、文本 |
gui_press_key | 发送快捷键 | 按键组合 |
gui_screenshot | 截取窗口画面 | app、输出路径 |
repo_exec_cmd | 在工作目录执行命令 | 命令字符串 |
接入方式是在 Claude Code、Dify、Cherry Studio 等客户端里新增一个 MCP Server 配置,指向 QAgent 的可执行文件:
{ "mcpServers": { "qagent-gui": { "command": "qagent", "args": ["mcp", "--transport", "stdio"], "env": { "QAGENT_MCP_KEY": "local-dev-key" } } } }这里有个特别明显的优势:因为 qagent 是单文件二进制,MCP 客户端启动它时不存在“找 Node、找 Python、找依赖”的路径问题。相当一部分 MCP 配置失败案例,本质都是客户端环境里没有正确版本的运行时;单文件可执行文件直接把这一类问题清零了。
4.3 作为 MCP Client:把第三方工具桥接进来
除了被调用,QAgent 还可以主动加载别人写的 MCP Server。做法很简单,在qagent.yaml里追加一段mcp_clients:
mcp_clients: browser-tools: command: npx args: ["-y", "@modelcontextprotocol/server-browser"] env: {} filesystem: command: npx args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"] env: {}QAgent 一旦加载这些客户端配置,CLI 模式的 agent 就能直接调用这些 MCP 能力。比方说,你的任务里既有“改代码”又有“用浏览器打开本地页面做交互验证”,QAgent 可以先用原生编码能力改好代码,再通过这个 browser MCP 工具把页面打开、点按钮、截图,整个闭环都落在一个 agent 进程里。这就是“双向桥接”的价值:不需要维护两套 agent,只需要一个入口。
这里要注意安全:SSE 模式会暴露本地端口,如果监听地址是非回环地址,务必加 API Key 鉴权。QAgent 默认只监听127.0.0.1,需要用--host 0.0.0.0显式放开。我在实际部署里,连局域网共享场景都尽量用 stdio 模式,减少暴露面。
4.4 常见 MCP 连接失败排查
在把 QAgent 接入各类客户端的过程中,我遇到过几个看起来很奇怪的问题,也基本摸清了规律。任何 MCP 连接失败,先用这三个方向定位:
- 传输方式不匹配:客户端期望 stdio,但 server 以 SSE 启动,或反之。配置里必须核对 transport 字段。
- 启动路径问题:
command指向的二进制不在客户端进程的 PATH 里。建议在 MCP 配置里写绝对路径,而不是裸命令名。很多“codex 无法找到 mcp”“Dify 找不到 mcp”的问题根源都在这里。 - 协议版本不兼容:MCP 协议仍在演进,旧版本 SDK 和新 server 握手会失败。检查客户端版本是否过旧。QAgent 向后兼容几个主版本,但如果客户端用的是很老的实现,界面上的报错往往含糊,只能靠日志定位。
我把这个排查清单直接写进了项目的 README,因为这类问题不是某一个产品特有的,整个 MCP 生态都会遇到。
5. 实测效果、局限与后续迭代
5.1 我在实践中跑通的几个完整任务
开箱之后我没有停留在“能跑 Demo”阶段,而是拿真实任务考验它。下面这几个案例是我在开发期间反复跑过、目前比较稳定的场景。
第一个任务是桌面上跑一个内部分发的小工具,要求是修改它的登录页 UI 并验证。QAgent 的流程是:先读取项目源码、定位按钮样式的定义、修改样式文件、执行构建,然后启动程序、在“登录”按钮上做一次点击、截图确认按钮的新样式生效。整个过程约 4 分钟,其中真正花时间的部分在源码搜索和定位,GUI 操作环节反而是秒级的。
第二个任务是让 agent 自动填一个老旧 ERP 桌面的登录表单。这类老程序的控件树质量参差不齐,但名称信息通常还能读到。QAgent 通过“dump 控件树 → 定位输入框 → type 用户名密码 → 点击登录”完成操作,全程不需要人碰键盘鼠标。值得说的是,这一步必须开着 ERP 窗口在前台,这是我前面讲的“focus 前置”的典型场景。
第三个是跨工具协作:我在 Dify 里搭了一条工作流,其中一个节点调用 QAgent 暴露的 MCP 工具去打开本地某个报告生成程序、导出 PDF,另一个节点用浏览器 MCP 把 PDF 上传到内部文档系统。这条链路让我真正感受到“MCP 双向桥接”的价值——QAgent 不只是个孤岛工具,它让自己嵌进更大的自动化和 RPA 流程里。
下面是我记录的一组典型耗时数据,供参考:
| 任务类型 | 操作步骤数 | 平均耗时 | 成功率(50次样本) |
|---|---|---|---|
| 修改代码并重启桌面应用验证 | 8 | 约4分钟 | 94% |
| 自动填充桌面登录表单 | 4 | 约20秒 | 97% |
| 桌面程序导出文件 + 外部 MCP 上传 | 6 | 约1分钟 | 91% |
失败样本里大头是两类:一类是目标程序弹出了非预期对话框(比如系统更新提示),打乱了控件焦点流;另一类是远程桌面会话里控件树退化,只能降级到消息注入,稳定性下降。这两类都可以通过“操作前环境检查 + 操作失败重试”进一步优化,但还没有到全自动解决的程度。
5.2 已知限制与我不打算硬刚的部分
每个工具都有边界,QAgent 也有明确“不硬刚”的地方。
- 光有自动化接口还不够:对自绘控件、游戏内嵌 UI 这类完全不走系统辅助功能的程序,我不保证控件树方案能正常工作。这类场景只能走兜底坐标模拟,而坐标模拟天生不稳定,我不会去宣称它能替代商业 RPA 方案的图像识别精度。
- macOS 的权限门槛绕不开:macOS 下操作辅助功能需要用户手动在“系统设置 → 隐私与安全性 → 辅助功能”里勾选终端授权。这是系统安全机制,程序无法绕过。第一次使用那个“点不到控件”的烦恼,我先写进 FAQ,等用户授权之后一切恢复正常。
- MCP 的 SSE 模式有额外安全责任:我建议所有非本机使用场景都先评估风险。虽然加了 API Key 鉴权,但 GUI 操纵类 MCP 工具本身就等于远程控制,任何暴露都会放大风险。安全底线是:默认不监听公网,默认不给新环境放开 GUI 操作白名单。
5.3 接下来想做的方向
当前版本完成了一个很窄但扎实的闭环:可解释的 GUI 操作 + 标准 MCP 协议 + 单文件零依赖。我还在持续迭代,优先级比较靠前的几个方向是:
- GUI Recorder 录制回放:让用户在桌面程序上手动操作一遍,QAgent 把每次点击、输入、窗口切换录制成可重放的 agent 脚本。这样既方便生成回归测试,也能给 AI 提供更高质量的“操作样例”作为 few-shot 参考。
- 更好的失败自愈:如果一次点击没有命中目标控件,或者弹窗拦截,agent 尝试自动读回控件树判断原因,然后重试。目前是固定三步重试,后面打算让它根据具体的控件差异自己定重试策略。
- 内置图像模板匹配的改进:在坐标模拟兜底模式中,我打算引入特征匹配,不只做像素级匹配,而是先找控件模板、再映射坐标。这不会替代控件树的主导地位,但能明显提升兜底场景的成功率。
我自己的体会是,AI 编码代理最缺的不是“脑子”,而是“手”和“眼睛”。QAgent 解决的就是这个问题:让 AI 在终端之外,也能真正碰到用户面前的程序。如果你也在搞类似的工具链,建议你也从“语义化操控 + 标准协议 + 零依赖分发”这三个角度去卡自己的设计。把每一个 GUI 动作都变成可解释、可回放、可验证的基础原语,AI 的自主性才有意义。