ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

能碰 GUI 的 AI 编码代理:基于 MCP 与单文件架构的实战解析

能碰 GUI 的 AI 编码代理:基于 MCP 与单文件架构的实战解析 1. 为什么我要做一个能碰 GUI的 AI 编码代理1.1 编码代理的天花板不在模型而在手伸不到的地方先说说我为什么会做这个项目。之前很长一段时间我都在用各种 AI 编码代理写代码。它们确实能帮忙改文件、跑命令、搜文档但越用越觉得别扭——这些工具本质上只活在终端 文件系统这个二维世界里。你让它打开设置面板把主题换成深色在这个软件里导入 Excel 并生成图表把某张截图里的内容填进表单它立刻傻眼。因为它看不见图标、点不了按钮、拖不动窗口甚至不知道屏幕上正在发生什么。但现实里的编程任务恰恰有大量绕不开 GUI 的部分你写了一个桌面工具需要验证安装向导每一步的点击效果你要给别人做的软件写自动化验收用例但被测程序只有图形界面你想让代理帮你配置一个 IDE 插件配置入口在层层叠叠的设置页里甚至就是最简单的工作——打开浏览器登录一下这个系统把验证码填进去现有终端型代理也搞不定。我当时的想法很简单能不能做一个编码代理它既具备普通 AI Agent 读写文件、执行命令的能力又能像人一样看屏幕、动鼠标、敲键盘也就是标题里说的操控 GUI。MCP 的支持则是另一个刚需——现在的工具生态里越来越多的服务通过 MCP 暴露能力数据库、浏览器、设计稿、CI 系统都有 MCP server如果代理不支持 MCP就等于拒绝接入整个生态。1.2 为什么选择MCP GUI 单文件这套组合先说 MCPModel Context Protocol。很多人刚听到 MCP 会以为是什么硬件协议其实它就是个软件层的东西定义了大模型应用和外部工具之间怎么互相发现、怎么调用、怎么传数据。你可以把 MCP server 想象成一个USB 设备把支持 MCP 的代理想象成USB 接口。任何厂商只要按协议做好一个 server任何兼容 MCP 的客户端都能直接插上用不需要每家都做一遍适配。所以我的代理必须做 MCP 客户端这是标准问题不是特色问题。再说单文件运行。市面上很多编码代理挺重的要装 Python 环境、要 npm install 半天、要拉模型、要配数据库。我见过太多人连第一步环境依赖都没跑过去就放弃了。我自己也烦这种体验。所以我给自己定了个硬性目标最终产物就是一个可执行文件双击就能跑顶多再加一个配置文件。Windows 上是一个 .exemacOS 上是一个二进制扔到哪都能运行。这是这个项目从第一天起就确定的三个支柱会操作图形界面、兼容 MCP 生态、单文件免安装。我不指望它能替代 Cursor 或者 Cline 这类成熟产品但如果你恰好需要的是一个不挑环境、能跑通完整看屏-决策-操作-验证闭环的代理这篇文章里的设计思路和踩坑记录应该能帮你省下不少时间。2. 单文件的身体里其实藏着一个五脏俱全的微内核2.1 单文件不是一个乱糟糟的大脚本而是分层架构很多人一看单文件运行就以为代码是乱七八糟的大杂烩。恰恰相反正因为要打包成单文件我对模块边界的划分反而更严格。我内部的结构大概是这样的模块职责对外暴露能力Agent 核心维护上下文、规划步骤、调度工具调用接收用户自然语言任务终端/文件工具集执行命令、读写文件、搜索shell、fs APIGUI 控制层截屏、坐标定位、鼠标键盘操作、UI 元素树提取click(type, x, y)、type_text、scroll、wait_forMCP 网关加载 MCP server 配置、发现 tool、调用 toollist_mcp_tools、call_mcp_tool安全网关权限审批、危险操作拦截、GUI 点击确认黑白名单提示词组装把环境状态转换成模型可读的上下文system prompt、observation 文本Agent 核心是最典型的一层Mark你之前可能听过 ReAct 模式Reason Act我就是这么做的模型根据当前观察到的屏幕状态和任务目标输出下一步动作动作由对应模块执行执行结果再回传给模型循环往复直到任务完成。2.2 为什么不直接套一个 Cline 或 OpenHands 的架构我认真评估过改造成本。Cline、OpenHands 这类项目对 MCP 的支持已经很成熟了但它们有一个共同的问题它们的内核是为代码仓库任务设计的工具集里默认优先的是文件操作和命令执行GUI 控制最多算一个锦上添花的插件而不是一等公民。如果我在它们的架构里塞 GUI 控制会遇到两个麻烦工具命名空间与提示词膨胀本来已经很长的 system prompt 里再加上一堆 GUI 动作描述模型更容易决策混乱状态同步困难它们的上下文模型围绕文件 diff命令输出构建GUI 的屏幕状态是高度动态的每轮都要重新截图重新理解这跟文件变更的上下文形态完全不同。与其强行改造不如从零写一个以屏幕操作为核心输入的心。开发成本确实更高但换来的是决策链路的干净模型每次规划时看到的输入就是一段紧凑的可读状态当前窗口标题、UI 元素树摘要、鼠标位置加一张按需截取的屏幕图没有多余噪音。MCP 工具和内置工具在 Agent 眼里是同一套调用协议只是来源不同。2.3 运行时选型为什么是 Deno 而不是 Python 或 Go既然要单文件首先得选一个能产出单一可执行文件的运行时。三个候选我都试过Python PyInstaller生态最强但要处理依赖泥潭产物体积轻松上百 MB而且不同系统上打包配置差异大Go打包最干净单体二进制非常小但写 GUI 自动化相关库时生态明显比 Python/Node 弱OCR、剪贴板、窗口树这类能力能找到的库有限DenoTypeScript既有 Node 的生态调用能力通过 npm 兼容又内置了权限沙箱最关键是deno compile一条命令就能产出原生可执行文件。我最后选了 Deno TypeScript。原因有三权限模型天然适配我要控制 GUI这件事Deno 允许我在启动时声明需要哪些系统权限网络、文件、运行子进程、读取环境变量这个权限声明在单文件运行场景下特别有用让代理在用户机器上明确知道自己能用什么打包成本低deno compile --target x86_64-unknown-linux-gnu --output agent main.ts这种写法交叉编译也方便TypeScript 写工具类项目太舒服了MCP 协议本身是基于 JSON-RPC 的TypeScript 的类型定义能帮我减少很多低级错误。3. GUI 操控的核心实现从截图到看得懂、点得准3.1 屏幕感知截图只是起点还得有结构第一步自然是截屏。但如果只把原始图片丢给多模态模型有两个问题一是贵、二是慢。每一轮推理都送一张 4K 截图Token 消耗会迅速失控。所以我的方案是结构优先、图像兜底能拿到 UI 元素树就用元素树在 Windows 上通过系统无障碍接口读取窗口树macOS 上有对应的 UI 元素 APILinux 上可以依赖 AT-SPI。元素树本身是带层级和属性的文本结构比如按钮[显示文本保存]模型不需要看图就知道屏幕上有哪些可操作元素拿不到元素树时退化为视觉方案截图然后用内置 OCR 或视觉模型提取文本 坐标清单两种信息都失败时才让多模态模型直接看整张截图只有最终兜底。这个过程很像我们人点外卖时的操作顺序先看菜单列表找红烧肉结构树菜单不显示文字就靠照片认菜OCR实在不行才放大图片反复确认视觉模型。有了这个分层大多数常规 GUI 操作都能在较低成本下完成。3.2 坐标、缩放和多显示器GUI 自动化的三个隐形杀手哪怕拿到了屏幕上某个按钮在 (1200, 800)这样的信息真正去点击时也有一堆坑。我踩得最深的三个系统缩放率DPI ScallingWindows 上常见的 125%、150% 缩放会让逻辑坐标和物理像素坐标不一致。你在普通截图里算出按钮在 (100, 100)实际点击时系统会把它当 (125, 125) 处理直接点偏。解决办法是截屏时记录当前缩放因子点击前把相对坐标换算成物理坐标。多显示器负坐标副屏在主屏左边时它的 X 坐标是负值比如 -1920 到 0。很多 GUI 自动化库只处理主屏原点的坐标你的代理一操作副屏就点在空气中。我在坐标归一化时就明确不把原点当左上角而是让所有坐标基于虚拟桌面的绝对坐标系。窗口移动了/动画还没结束元素树里拿到一个按钮位置但用户正好拖动了窗口等代理真正去点击时按钮已经跑了。所以 GUI 操作循环里必须有一个动作后校验步骤点击后立刻重新获取窗口状态确认预期变化是否发生没发生就回滚或重试。3.3 实操示例让代理自己完成一次另存为对话框我平时测试项目时最常用的一种方式是给代理一句话打开记事本输入Hello GUI Agent然后按下 CtrlS在弹出的保存对话框里把文件名改成 demo.txt保存到桌面。整个执行链路大概是这样的规划Agent 判定需要依次调用启动应用、激活窗口、输入文本、触发快捷键、等待对话框、在 GUI 上定位元素、点击、输入、确认等动作启动与输入调用终端工具notepad.exe然后用 GUI 层找到一个编辑区域拿元素树调用 type_text 写入内容触发保存通过 GUI 层的热键能力发送 CtrlS等待对话框出现轮询窗口树直到出现另存为窗口这一步不是固定 sleep而是条件等待对话框 1 秒弹出或 10 秒弹出都能兼容定位目标元素从窗口树里定位文件名输入框和保存按钮必要时切换到截图 OCR 辅助执行与校验先点击输入框全选输入新文件名再点击保存。校验点窗口是否关闭、桌面是否出现 demo.txt。这个过程看起来不复杂但每一步背后都有失败处理。比如 OCR 把另存为识别成了另存为零宽字符或者在对话框打开瞬间点击被系统拦截。我在调试版本里专门加了一个轨迹录制功能把每次截图、每次坐标点击、每次 UI 树快照保存下来失败时回放能看出是哪一步判断错了。这个能力强烈建议你自己做项目时也加上没有轨迹记录的 GUI Agent 等于盲人走夜路出了问题根本没法排查。4. MCP 支持把代理变成一个能插所有工具的设备插座4.1 MCP 不是硬件协议它解决的是工具调用的即插即用有个热词搜索里有人在问MCP 到底属于软件协议还是硬件协议答案很清楚它是纯软件协议跟 USB、PCIe 这些没关系。MCP 定义的是大模型应用和外部数据/工具之间的交互方式底层走 JSON-RPC 2.0。现在 MCP 最主流的传输方式有两种stdio标准输入输出客户端启动一个子进程通过 stdin/stdout 和它对话。适合本地工具比如某个文件处理脚本。HTTP/SSE通过网络连接远端 MCP server适合数据库、云端服务这类需要共享访问的工具。我实现的 MCP 客户端同时支持这两种。你要插一个本地写的小工具配置文件里 type 写stdio要连远程服务type 写http。4.2 让 Agent 动态发现 MCP 工具而不需要硬编码关于 MCP很多人第一次接触时最大的困惑是我该怎么告诉Agent有哪些工具可用传统做法是在 system prompt 里写死工具列表。但这有个问题MCP server 是动态加载的今天用户配置了一个数据库 server明天可能换成一个浏览器自动化 server工具列表一变又要改提示词。我的处理方式是走标准的 MCP 握手流程启动时读取用户配置文件mcp.json对每个 server 发起 initialize 请求建立会话调用tools/list拿到该 server 支持的全部工具清单把工具清单和描述动态拼接进当前任务的工具上下文里。也就是说用户不需要在 Agent 代码里为每个工具写适配层只要 MCP server 本身实现规范代理这边就能自动识别并调用。这一点是 MCP 协议最有价值的地方。下面是一段简化的内部逻辑for (const [name, config] of Object.entries(mcpServers)) { let client: MCPClient; if (config.type stdio) { client await MCPClient.connect_stdio(config.command, config.args, config.env); } else { client await MCPClient.connect_http(config.url, config.headers); } const { tools } await client.listTools(); agent.registerTools(mcp__${name}__${tool.name}, tool.description, async (args) { const result await client.callTool(tool.name, args); return result; }); }有了这层注册逻辑Agent 规划时看到的工具列表里就既有内置工具执行命令、读写文件、截屏点击也有外部 MCP 工具。它自己会判断该用谁。4.3 用户侧的 MCP 配置应该长什么样项目分发出去后用户最关心的就是我怎么接我自己的工具。我给项目配了一个示例mcp.json放在可执行文件同目录下。它长这样{ mcpServers: { filesystem: { type: stdio, command: npx, args: [-y, modelcontextprotocol/server-filesystem, /workspace], env: {} }, remote-db: { type: http, url: http://127.0.0.1:8000/mcp, headers: { Authorization: Bearer YOUR_TOKEN } } } }注意几个容易出错的地方stdio 的 command 字段必须写能在系统 PATH 里找到的命令否则子进程启动直接失败env 里不要写死敏感凭据我建议支持类似${env:DATABASE_URL}这种从系统环境变量展开的写法避免密钥被写进配置文件HTTP server 的 URL 需要后端实现 MCP Streamable HTTP 规范不是随便一个 HTTP 接口都能叫 MCP。有一位朋友拿来接自家的 Oracle 数据库查询用的就是某个 IDE 插件里类似的 MCP 配置方式——只要你理解服务端通过工具协议暴露 SQL 执行能力代理侧就能调用迁移到哪个环境都不费劲。MCP 的好处就是同一个概念在 IDE 插件里成立在我这个单文件 GUI Agent 里也成立。5. 单文件运行这件事难点根本不在打包5.1 打包本身很简单一行命令的事情只要选对了技术栈打包真的不难。我用了 Deno 的 compile 能力deno compile \ --allow-run --allow-read --allow-write --allow-env --allow-net \ --target x86_64-pc-windows-msvc \ --output gui-agent.exe \ src/main.ts产物是一个带图标我自己画了个丑图标的可执行文件把它复制到任何一台 Windows 机器上就能跑。macOS 和 Linux 同理只是 target 参数不同。但这只是表面上的单文件。5.2 真正的麻烦外部可执行文件、OCR 模型和系统权限单文件只是你提供的二进制只有一个不代表运行时不需要外部依赖。我踩过的几个具体问题OCR 引擎我希望内置文字识别能力但如果把本地 OCR 模型打进去体积立刻膨胀到 900MB 以上这违背了轻量初衷。妥协方案是优先调用系统级 OCRWindows 自带、macOS 有 Vision 框架实在没有才提示用户当前系统缺少 OCR 依赖。MCP 工具子进程很多 MCP server 本身依赖 Node.js 环境。用户机器上没有 Nodenpx命令就跑不起来。这不是我能打包解决的只能在文档里写明使用该 server 前请安装对应运行时。GUI 结构 API 的系统权限Windows 上读取窗口树比较容易macOS 必须要给终端或应用授予辅助功能/屏幕录制权限否则元素树拿不到、截屏是黑的。Linux 上依赖 AT-SPI 服务某些精简发行版默认没装 D-Bus 接口就得让用户手工装包。所以我把单文件定位成对外分发形态是单文件运行时的系统集成能力按需发现。启动时代理会自己检查当前系统有哪些可用能力缺了就给明确提示而不是一崩溃了事。这样既保证了轻量又不至于让用户陷入软件打不开也不知道为什么的困境。5.3 跨平台差异如何收敛我测试最多的三个平台是 Windows 11、macOSApple Silicon和 Ubuntu 24.04。GUI 控制的 API 差异极大这里有一张我项目里内部的兼容性表格能力WindowsmacOSLinux截屏系统 API稳定需要屏幕录制权限X11/Wayland 差异大UI 元素树无障碍接口成熟可用需辅助功能权限AT-SPI依赖桌面环境模拟点击Win32 API区分逻辑/物理坐标CGEvent 全局事件XTest 或 uinput多显示器支持良好支持良好X11 还行Wayland 受限我给 GUI 控制层定义了一个统一接口每个平台自己实现。这样 Agent 核心层的代码不需要关心底层的坐标换算和权限问题只要调用click(x, y)、get_ui_tree()、type_text()就够了。6. 实测体验能跑通但第一次就成功的概率没那么高6.1 第一次跑通一个完整任务的全过程我拿最经典的测试任务来演示让代理打开系统计算器计算 7 乘以 8并把结果读出来。第一次跑通的日志大概是Agent 调用内置终端工具执行calcWindows 下这是计算器命令GUI 控制层轮询窗口树确认计算器窗口出现并拿到窗口边界坐标用元素树查找数字键7的坐标调用 click 点击点击乘号点击数字8此时计算器上没有直接的等号按钮其实有但传统布局里等号按钮在窗口右侧点击等号后用 OCR 读取结果区域文本得到56回传给用户7 * 8 56。整个过程大约 20 秒。真正让我觉得可行的是它并不是靠写死的步骤跑通的而是模型实时看着元素树自己决策的。如果计算器按键布局换了它也能重新看结构再决定点哪里。这就是GUI Agent和写死脚本的 RPA最本质的区别。6.2 踩坑一截屏全黑但没有任何报错我第一次在 macOS 上测试时代理一直在说我没看到任何窗口内容。排查链路先确认不是代码问题手动调用截屏函数返回的图片是纯黑色再确认是不是权限问题打开系统设置发现我的终端没有屏幕录制权限验证是整个进程的问题还是单次调用的问题重启终端后重新截图正常。结论macOS 的权限提示有时不会弹窗只在系统设置里默默拒绝。后来我在程序启动时主动检测权限状态如果截屏 API 返回全黑或空内容就直接提示请到系统设置中授予屏幕录制权限而不是让模型自己猜为什么看不到屏幕。6.3 踩坑二MCP stdio 工具一直报连接已关闭另一个频发的坑来自 MCP 配置。用户配置了一个 stdio server运行时告诉我连接已关闭。排查过程先检查配置文件里的 command 是否在 PATH 中终端里手动执行npx -y some/server --version发现命令是存在的再看是不是协议版本问题用对照工具手动发起 initialize 请求发现 server 返回正常最后才发现程序的工作目录和 MCP server 要求的当前目录不一致。我在启动子进程时没有把 cwd 设置为 server 配置里指定的目录server 一启动就崩了。修复很简单stdio 配置里增加cwd字段启动子进程时传进去。但这个 bug 我查了整整半天。所以如果你的 MCP server 总是莫名断开优先检查子进程的工作目录、环境变量、PATH 路径这三样别一开始就去翻协议实现。6.4 安全边界为什么我加了危险操作确认开关让一个 AI 代理能控制鼠标和键盘安全性怎么强调都不过分。我在设计之初就把安全网关放在最高优先级默认只读模式GUI 操作里滚动、读取窗口、截屏可以直接执行点击、拖拽、输入这些有副作用的动作如果用户开启了严格模式需要用户按一次回车或说一句继续才会执行敏感命令拦截终端工具内置一份危险命令黑名单格式化磁盘、删除系统目录、下载并执行远程脚本等命中就拒绝除非用户在配置里显式允许操作可观测性每次 GUI 动作都会打印一个结构化日志包括坐标、动作类型、涉及的元素文本用户随时能停止任务。我做这个项目最深的体会是GUI Agent 的能力越强越需要给用户反悔和看着它干的能力。如果一个代理能替你在屏幕上做任何事那你至少应该能随时知道它在做什么。7. 下一步这个项目后续还能怎么长7.1 让 GUI 操作变成可回放、可验证的脚本资产现在代理虽然能操作 GUI但每轮任务都是即兴表演没有沉淀。我希望后续把每次成功的操作轨迹保存成一种 DSL窗口条件、点击位置、输入内容、校验断言全部结构化。这样下次跑同样的流程时可以直接以低倍速回放脚本 关键点校验的方式执行不用再让模型重新看图猜位置。这本质上是把 Agent 的灵活性跟 RPA 的稳定性结合——平时用 RPA 脚本跑遇到异常再叫 Agent 来兜底。7.2 让 MCP 工具支持热插拔现在的 MCP server 必须在启动时加载。我计划做成可热插拔的用户运行过程中编辑 mcp.json代理监听文件变化自动对新增 server 发起握手和工具发现不需要重启。这在调试复杂流程时非常有用不用为改一个工具配置就把整个会话推倒重来。7.3 GUI 状态快照与时间回溯调试 GUI Agent 最大的痛苦是状态不可复现。我会给每次交互打一个状态快照截图 UI 树 坐标存成带时间戳的序列。下次复现 bug 时可以直接跳到任意一步从这里重新开始推理和操作。这有点像视频编辑里的时间线既能前进也能回退对排查复杂 GUI 自动化问题非常有用。7.4 跑一个轻量本地模型做离线降级最后我还想尝试把大模型换成端侧小模型做低功耗模式。日常的简单 GUI 操作点击固定按钮、填写表单、读取固定区域文本其实用不到复杂推理一个 1B 到 3B 的本地模型配合规则引擎就能完成。只有在遇到突发情况时才唤醒云端大模型做复杂决策。这样用户在没有外网的环境里也能跑一些基础自动化任务功耗和延迟都会好看很多。最后分享一点个人体会。这个项目最让我意外的不是技术多难而是人的预期和实际行为的差距。很多人第一次看到代理操控 GUI 时会期待它像科幻电影里的 AI 一样无所不能实际用起来却发现它连一个滚动条都可能抓不准。但如果把预期调整为它像一个刚开始学用电脑的实习生能看懂界面结构、会按步骤操作、出错了能自己重试你会发现它其实已经能帮忙干不少事了。单文件也好、MCP 也好、GUI 控制也好说到底都是为了让 AI 离真实世界更近一步。我还会继续在这个方向打磨下去也希望看到更多人用开源方式把同类工具推到更完善的状态。
返回列表