我第一次接到让Agent自动操作桌面端Electron应用这个需求时,脑子里冒出来的第一套方案,还是RPA那套老玩法:截屏、图像识别、模拟鼠标键盘、坐标点击。等真的把界面跑起来才发现,这套东西在Electron面前脆弱得让人绝望——界面稍微改个布局,图像模板就失效;系统里突然弹出个窗口,坐标全部错位;文本框输入中文还要处理输入法和剪贴板的异步问题。后来我换了一个完全不同的思路:既然Electron应用本质上是Chromium的一部分,为什么不直接通过CDP协议(Chrome DevTools Protocol)去接管它的渲染层和控制层?这个转变,让我把所有基于坐标和图像的脆弱逻辑全部扔掉了。
这篇文章想把这套完整实践记录下来,适合做桌面智能体、RPA工具、Electron应用自动化测试的朋友参考。我会从CDP的核心原理讲起,然后给出一条可直接落地的最小Agent操作链路,再展开多窗口管理、弹窗处理、长任务稳定性这些实操中的硬骨头,最后分享我实测下来的优化经验和安全边界。整个方案面向的是"Agent像人一样操作界面完成任务"这一类需求,但它比人更稳、更快、更可回溯。
1. 为什么是CDP:桌面自动化中的"正统解法"迟早要交到Chromium手里
1.1 传统UI自动化的三座大山
接到自动化操作某Electron桌面应用的需求时,很多人的第一反应是标准的RPA套路:截图、找图、模拟点击、键盘输入。这套方案也不是不能用,但一旦投入生产环境,就会撞上三个绕不开的坎。
第一是坐标不稳定。界面分辨率不同、屏幕缩放比例不同、窗口位置变化,都会导致坐标偏移。更麻烦的是,Electron应用里大量使用动态布局,按钮在窗口拉伸后可能跑到另一个位置。基于图像模板匹配的方式,也要面对同一套界面在不同系统下渲染差异的问题。这些问题在开发环境里往往不会暴露,一旦部署到用户机器上,立刻变成一片沼泽。
第二是输入不可控。模拟键盘输入英文数字还行,一旦涉及中文、特殊符号、快捷键组合,就会出现输入法状态窗口抢焦点、剪贴板数据被覆盖、组合键被系统吃掉等各种意外。一次完整的表单录入,往往要处理几十个边界情况。我当时花了一周时间调输入法兼容性,最后还是没有完全解决。
第三是状态不可见。传统RPA只能从图像判断"看起来对不对",但Agent真正需要的是"这个按钮现在是否可点击、这个输入框是否已经填上值、这个列表里到底加载了几条数据"。如果只靠截屏,Agent对界面状态的理解是非常表象的,后续决策自然也就只能靠猜。
这三点叠在一起,导致用传统方案做复杂界面自动化时,代码维护成本高得吓人,每换一个版本就要重新校准一遍。这也是我想去找CDP这条路的直接原因。
1.2 Electron应用与CDP协议的天然吻合
Electron应用虽然是一个桌面程序,但它的UI渲染层完全跑在Chromium引擎内部。换句话说,Electron应用内部就是一套完整的浏览器环境,窗口里的页面、DOM、JavaScript、网络请求,全部由Chromium那一套组件来管理。
CDP协议本来就是Chromium暴露给开发者工具用的协议,它能直接操作页面DOM、模拟鼠标键盘事件、执行任意JavaScript、抓取网络请求、控制页面生命周期。既然Electron应用内部是一个Chromium页面,那CDP协议所能做的一切,理论上都能作用到Electron应用上。
这就带来一个很大的优势:自动化操作不需要依赖屏幕坐标和图像识别,而是直接基于DOM结构、元素属性、内部状态来完成。Agent可以拿到界面的真实结构,而不是一张需要猜的截图。这也是为什么我把核心方案定为CDP协议,而不是继续在坐标和图像里打转。
2. 先拆清楚CDP的几个核心概念:端口、Target、Session、消息格式
2.1 让Electron把内部调试端口打开
要用CDP协议连接Electron应用,第一步是让它把调试端口暴露出来。Electron基于Chromium,因此可以直接复用Chromium的远程调试开关。
有两种方式可以打开端口。第一种是在应用代码里主动设置开关,这一段要放在app的ready事件之前:
const { app } = require('electron'); app.commandLine.appendSwitch('remote-debugging-port', '9222'); app.commandLine.appendSwitch('remote-debugging-address', '127.0.0.1'); app.whenReady().then(() => { // 创建BrowserWindow等业务代码 });第二种方式是启动时通过命令行参数传入,适用于不改应用代码、只改启动方式的情况:
/path/to/your-electron-app --remote-debugging-port=9222这里有一点需要注意:如果应用是打包后的二进制,命令行参数是否会被透传,取决于Electron版本和启动器的实现。我在实践中发现,最稳定的做法还是应用内主动设置commandLine.appendSwitch,因为它不依赖外部传参的行为。
端口打开后,访问http://127.0.0.1:9222/json/list就能看到一个JSON数组,数组里每个元素对应一个可以控制的页面目标,里面包含这个页面专属的WebSocket地址。这个接口是整个自动化链路的入口,后面连接WebSocket的时候还会用到它。
2.2 Target体系:一个窗口就是一个可控制目标
CDP协议把"可以被调试和控制的对象"统称为Target。在Electron应用里,每个BrowserWindow通常都会对应一个页面类型的Target;如果应用里有多个窗口,/json/list接口就会返回多个条目。
每个Target有三个字段需要重点看:id、webSocketDebuggerUrl、title。
| 字段 | 特点 | 使用建议 |
|---|---|---|
id | Target唯一标识,稳定不随标题变化 | 长期自动化任务中优先用它绑定窗口 |
webSocketDebuggerUrl | 建立WebSocket连接的地址 | 连接前从这里取地址 |
title | 窗口标题 | 仅作参考,勿当唯一依据 |
title可能被业务代码动态修改,也可能多个窗口有相同标题。在Agent链路中,我们可能会同时连接多个Target,给每个窗口维护独立的CDP会话。这个多窗口管理是后文会重点展开的硬骨头,这里先把概念理清。
2.3 消息协议:请求、响应、事件
CDP的消息格式是JSON,底层通过WebSocket传输。客户端发请求时要带一个自增的id,方法名放在method字段里,参数放在params字段里。服务端响应时会带上同样的id,这样客户端就能把响应和请求对应起来。
事件消息则不带id,而是带method字段来表示事件名称,比如Page.loadEventFired、Runtime.consoleAPICalled。在实际的Agent链路里,事件机制非常重要,因为很多操作需要"等一个异步事件触发后再继续",比如等待页面加载完成、等待某个请求返回、等待DOM节点出现。
响应消息里还有result字段存放返回值,错误则通过error字段返回。这里有个容易踩的坑:CDP的响应是异步到达的,WebSocket连接上可能同时有多个请求在途。客户端必须自己维护一张"请求id到Promise"的映射表,不能假设服务端会按请求顺序返回响应。我习惯封装一个简单的send(method, params)方法,内部自动分配id并把Promise存起来,响应回来时再resolve。
const WebSocket = require('ws'); class CdpClient { constructor(wsUrl) { this.ws = new WebSocket(wsUrl); this.id = 0; this.pending = new Map(); } async connect() { this.ws.on('message', (data) => { const msg = JSON.parse(data.toString()); if (msg.id && this.pending.has(msg.id)) { const { resolve, reject } = this.pending.get(msg.id); this.pending.delete(msg.id); if (msg.error) reject(new Error(msg.error.message)); else resolve(msg.result); } }); await new Promise((resolve, reject) => { this.ws.on('open', resolve); this.ws.on('error', reject); }); } send(method, params = {}) { return new Promise((resolve, reject) => { const msgId = ++this.id; this.pending.set(msgId, { resolve, reject }); this.ws.send(JSON.stringify({ id: msgId, method, params })); }); } }这段代码虽然短,但已经是一个最小可用的CDP客户端骨架。后续所有自动化操作都是建立在它之上。
2.4 我在实际选型时用的核心能力域
CDP协议有几十个能力域,但Agent操作Electron应用时最常用的是下面这几个:
| 能力域 | 用途 | 关键方法 |
|---|---|---|
| Runtime | 在页面里执行JavaScript取数据 | Runtime.evaluate |
| DOM | 获取DOM节点信息 | DOM.getDocument、DOM.querySelector |
| Input | 模拟真实鼠标键盘事件 | Input.dispatchMouseEvent、Input.dispatchKeyEvent、Input.insertText |
| Page | 页面控制与截图 | Page.captureScreenshot、Page.reload |
| Accessibility | 获取可访问性树,转成Agent友好文本 | Accessibility.getFullAXTree |
这里我重点说两个容易被忽略的点:Input.insertText在输入中文时非常好用,它直接把一段文本塞进当前焦点元素,不经过剪贴板,也不会触发输入法状态栏干扰,这比模拟键盘敲击键码要稳定得多。
Accessibility.getFullAXTree则非常适合生成Agent可读的界面快照。可访问性树里包含了每个控件的角色、名称、状态、值,正好是Agent做语义决策需要的结构化信息。
3. 搭一条最小可用的Agent操作链路:从快照到动作执行
3.1 阶段一:拿一份Agent能读懂的"界面清单"
Agent要操作应用,第一步永远是感知当前界面。直接给大模型看整棵DOM是不现实的,节点太多、噪声太大。我实践下来比较有效的做法,是只提取当前屏幕范围内的可交互元素,转成一份紧凑的JSON清单。
下面这段脚本通过Runtime.evaluate在页面内部执行,筛选出按钮、输入框、链接、可点击的标签页等可交互元素,并标注它们在屏幕上的坐标和尺寸:
const getSnapshot = await cdp.send('Runtime.evaluate', { expression: ` (function() { const selector = [ 'button', 'input', 'select', 'textarea', 'a[href]', '[role="button"]', '[role="tab"]', '[role="checkbox"]', '[role="radio"]' ].join(','); const nodes = Array.from(document.querySelectorAll(selector)); return nodes.map((el, index) => { const rect = el.getBoundingClientRect(); const visible = rect.width > 0 && rect.height > 0 && rect.bottom > 0 && rect.top < window.innerHeight && rect.right > 0 && rect.left < window.innerWidth; if (!visible) return null; const label = (el.getAttribute('aria-label') || el.textContent || el.getAttribute('placeholder') || '').trim().replace(/\\s+/g, ' ').slice(0, 60); return { ref: 'id_' + index, tag: el.tagName.toLowerCase(), type: el.getAttribute('type') || '', label: label, x: Math.round(rect.x + rect.width / 2), y: Math.round(rect.y + rect.height / 2), w: Math.round(rect.width), h: Math.round(rect.height), disabled: el.disabled || false }; }).filter(Boolean); })() `, returnByValue: true });这段脚本会返回一个数组,每个元素都有ref、tag、label、坐标和尺寸。Agent不需要看整个页面,只需要看这份清单就能决定下一步操作。
有人可能会问:为什么不直接截图给多模态模型看?我的经验是,截图对多模态模型确实有效,但截图有一个致命问题——无法准确映射回去。模型看到一张图里的某个按钮,它不知道这个按钮的精确坐标和DOM信息。而结构化快照天然包含精确位置,模型只要输出"点击ref为id_12的元素",我就能立刻执行。如果要结合截图,也应该把结构化快照作为主要信源,截图只作为辅助的视觉上下文。
3.2 阶段二:把Agent的决策翻译成CDP动作
拿到结构化快照后,Agent需要做出决策。无论你用的是大模型、规则引擎还是混合策略,最终输出的动作一般就是几类:点击、输入、滚动、等待、按键、切换标签页、完成。
现在要解决的问题是:给定一个ref和动作类型,怎么执行CDP操作。
如果Agent选择的是点击某个元素,最贴近真实用户行为的做法是用Input.dispatchMouseEvent在元素中心坐标处发送鼠标按下和抬起事件:
async function clickAt(cdp, x, y) { await cdp.send('Input.dispatchMouseEvent', { type: 'mousePressed', x: x, y: y, button: 'left', clickCount: 1 }); await cdp.send('Input.dispatchMouseEvent', { type: 'mouseReleased', x: x, y: y, button: 'left', clickCount: 1 }); }可能有人会想,直接在Runtime.evaluate里调用element.click()不是更简单?确实简单,但element.click()不会触发某些依赖真实鼠标位置或事件顺序的业务逻辑,比如悬停效果、拖拽准备、右键菜单等。在Agent自动化场景里,尽量模拟真实输入事件,能减少很多"明明调用了却没反应"的奇怪问题。
输入文本则分两步走:先把焦点移到目标输入框,再用Input.insertText塞入文本:
async function fillInput(cdp, elementSelector, text) { await cdp.send('Runtime.evaluate', { expression: `(() => { const el = document.querySelector(${JSON.stringify(elementSelector)}); if (!el) return { ok: false, reason: 'element-not-found' }; el.focus(); return { ok: true }; })()` }); await cdp.send('Input.insertText', { text: text }); }Input.insertText这种方式对中文、英文、数字、特殊符号都是安全的。不用处理剪贴板,不用模拟组合键,也避免了输入法状态栏干扰。
滚动操作则直接通过Runtime.evaluate控制页面滚动位置,或者用Input.dispatchMouseEvent的type: 'mouseWheel'模拟滚轮事件。对于普通列表滚动,前者足够且更快。
3.3 阶段三:动作后的结果校验
Agent操作不能只执行动作不管结果。一个健壮的链路里,每次执行动作后都要校验"预期是否达成"。
这个校验可以用快照对比来做:动作执行前拍一份快照,执行后再拍一份快照,比对关键元素的状态变化。比如点击一个"展开"按钮后,校验目标区域内是否出现了新的可交互节点;填入文本后,校验输入框的value是否等于预期文本。
async function verifyValue(cdp, selector, expectedText) { const result = await cdp.send('Runtime.evaluate', { expression: `(() => { const el = document.querySelector(${JSON.stringify(selector)}); if (!el) return { ok: false, reason: 'element-not-found' }; return { ok: true, value: el.value || el.textContent }; })()` }); return result.result.value === expectedText || result.result.value.includes(expectedText); }这里要提一个关键经验:校验时不要只看DOM值,还要考虑元素的disabled状态、可见性、是否出现加载动画。Electron应用经常在点击按钮后出现短暂的loading状态,如果Agent在loading期间就拍快照,很可能会误判"操作失败"。
所以我的链路里一般会加一个"等待稳定"步骤:在动作执行后,每隔100~200毫秒连续拍两次快照,如果关键区域的节点集合没有变化,就认为是稳定状态,再进行校验。这个策略虽然简单,但能过滤掉绝大多数由异步渲染引起的假阴性。
4. 多窗口、弹窗和长任务:Agent操作Electron时的三个硬骨头
4.1 多窗口间的Target漂移与正确绑定方法
很多Electron应用不止一个窗口。主窗口、设置窗口、预览窗口、可能还有弹出来的独立对话框,每一个都会在/json/list里对应一个Target。
如果Agent在任务过程中要跨窗口操作,最稳妥的绑定方式是:在连接时记录每个Target的id和webSocketDebuggerUrl,并按需为每个Target建立独立的CDP会话。不要用title来挑选窗口,因为title可能在业务运行中被修改,尤其在已登录状态下标题栏经常会带上用户信息。
这里有一个在Electron场景下特有的坑:当窗口重新加载或者应用从深层路由恢复到某个页面时,Target的id可能会变化。一旦Target id变了,旧的WebSocket连接就失效了。所以Agent链路里必须有"重新发现Target并重建会话"的机制,不能假设连接是一劳永逸的。
我的做法是封装一个简单的窗口管理器,每次任务循环开始时,先拉取一次/json/list,和上一轮的Target id做对比,发现新的就建立连接,发现消失的就清理资源。这样即使窗口发生重载,Agent也能在下一次决策前自动找到正确的目标。
async function refreshTargets(baseUrl = 'http://127.0.0.1:9222') { const res = await fetch(`${baseUrl}/json/list`); return await res.json(); }这个函数虽然简单,但配合一个按id维护的连接池,就能应对绝大多数窗口创建和销毁场景。
4.2 弹窗和模态框的识别与处理
Electron应用里的弹窗大致分两类:一类是原生对话框,比如dialog.showMessageBox产生的模态框;一类是应用内自绘的DOM弹窗。后者很好处理,它在DOM中就是普通节点,快照和点击逻辑完全适用。真正有坑的是前者。
原生对话框并不对应一个页面Target。当原生对话框弹出时,页面会被阻塞,CDP请求也会一直挂起。我遇到过的最典型的问题是:Agent往输入框发了Input.insertText之后,应用弹出了一个原生保存对话框,然后整个链路里所有后续CDP调用全部超时,因为页面主线程被信号量卡住了。
遇到这种情况,绕过比硬抗更现实。我的经验是,如果Agent链路要处理保存、确认这类场景,优先不要让应用弹出原生对话框。Electron应用可以通过设置浏览器的文件下载路径、或者在应用内注册自定义事件来避免原生弹窗。如果你能拿到Electron应用的代码,那在调用主进程API之前就拦截掉对话框逻辑是干净利落的处理方法。
如果确实必须和无UI弹窗交互,那么只能在应用侧通过修改Electron主进程的开关来禁用或替换掉原生对话框。对Agent方案来说,在应用内做一层"自动化模式开关"往往是性价比最高的做法,下面还会讲到。
4.3 长任务链路的超时与断线恢复
Agent在操作Electron应用时,一个任务往往要执行几十个动作,耗时可能从几十秒到几十分钟。这种长任务链路里,WebSocket连接因为网络抖动、应用卡顿、窗口重载而断开,是概率不小的事件。
我踩过一次很深的坑:Agent执行到第十五个步骤时,Electron应用因为某个无关的渲染异常导致整个Target被销毁重建,WebSocket连接立刻断开,而我的Agent循环里没有处理断线,结果所有的后续步骤全部失败,需要人工介入。
从那以后,我在所有Agent循环里都加了重连模块。重连逻辑并不复杂:捕获WebSocket错误后,重新请求/json/list,找到相同id的Target或者根据业务规则找到替代Target,建一个新的CdpClient,然后从当前步骤继续执行。
这里有一个和业务相关的取舍:有些任务要求断线后必须从断点恢复,有些任务则可以接受从头重试。从断点恢复需要对动作序列做持久化,至少要把"已完成步骤列表"和"当前步骤索引"存在内存之外的地方。我在实际项目里的做法是把每一步动作的入参和CDP响应都记录到一个日志队列里,重连后根据日志判断哪些步骤已经执行成功,哪些步骤的结果待确认。
如果结果无法确认,比如"点击了提交按钮但不知道是否成功",那宁可对该步骤做一次幂等重试,也不要贸然跳过。Electron应用里的提交类操作,很多本身不具备幂等性,重复点击可能产生重复数据。这种时候我会通过新的快照去校验当前界面状态,再决定是重试还是继续。
5. 效率优化与稳定性调参:这组实测数据值得保留
5.1 快照瘦身:不要让Agent看整棵DOM树
我发现很多人在做Agent自动化时会犯一个错误:把整棵DOM树的outerHTML全量丢给大模型。这个做法在页面上有几个按钮时尚且能跑,一旦界面复杂起来,输入token爆炸,模型响应变慢,而且还会被大量无意义的div、span干扰。
我建议把快照控制在"足够Agent决策的最少信息"级别。上面那段Runtime.evaluate脚本就是一个很好的基线:只取可交互元素,只保留label、坐标、尺寸和状态字段,通常一次快照的JSON大小可以控制在几千字节以内。即使加上每个元素的祖先路径信息,也不会超过几十KB。
实测下来,一份中等复杂度的Electron界面,全量DOM的JSON可能有1到2MB,而瘦身后的交互节点快照通常只有5到15KB。在Agent链路里,这个差别意味着模型推理速度可能快一个数量级,同时显著降低上下文窗口被撑爆的风险。如果你的Agent还要做多轮历史记忆,快照体积直接决定能记住多少轮。
5.2 坐标与DOM引用的取舍
操作元素时到底用坐标还是用DOM引用,是我纠结过很久的问题。纯坐标点击的问题是元素位置可能动态变化,纯DOM引用的方式又无法处理canvas等非DOM渲染区域。
我的结论是:在Agent语义决策层面,全部用ref来标记元素;在执行层面,优先在最新快照中重新查找坐标,再用坐标执行鼠标事件。也就是说,Agent只认"ref",执行器负责把ref翻译成当时的坐标。
这样做的原因是:Agent决策时和动作执行时之间有时间差,界面可能已经发生了变化。如果Agent直接说"点击(100, 200)",而这个坐标对应的元素已经滚走了,那这次点击大概率会点错地方。反过来,我们用ref去找当时页面上仍然存在的那个按钮,重新获取它的坐标,就能消除这个时间差带来的漂移。
真实场景里我是这么做的:快照为每个元素生成一个稳定定位器,比如[data-testid="submit-btn"]、input[placeholder="请输入账号"]这类兜底选择器,执行点击前先用当前页面重新匹配一次,如果匹配到了就取最新坐标,匹配不到再降级到老坐标尝试,并记录一条"元素漂移"日志。
5.3 连接保活与WS重建机制
CDP的WebSocket连接没有内置心跳,连接断了不会立刻自动重连。在一个需要长时间挂机的Agent任务里,最好自己实现一个保活机制。
保活的方式很简单:每30秒发送一个轻量级请求,比如Page.getLayoutMetrics或者Runtime.evaluate执行一个1+1的表达式,只要能收到响应就说明连接还活着。连续三次没有响应,就主动关闭旧连接并重建。
这里有一个经验值可以参考:CDP请求默认没有统一的超时时间,很多库会把超时设成30秒或60秒。对于Agent自动化场景,我建议把单个动作的CDP请求超时控制在10秒以内,而把"等待某个DOM状态出现"这类需要更长时间的操作单独放到一个轮询函数里,轮询的间隔在100到200毫秒之间。这样既能快速发现异常,又能覆盖异步渲染等需要耐心等待的场景。
另外,Electron应用在窗口最小化或被系统遮挡时,Page.captureScreenshot偶尔会返回空白或者不全的图片,这在某些自动化校验任务里会导致误判。如果你的Agent链路里大量依赖截图,那么在执行截图前最好先把窗口恢复到前台。如果必须最小化运行,可以考虑用Page.startScreencast这一类的替代方案,但我个人觉得大多数场景里,保证窗口不为最小化状态更简单可靠。
6. 安全边界与写在最后的一些提醒
6.1 远程调试端口的安全影响
打开remote-debugging-port意味着任何能访问这个端口的本地进程,都可以完全控制你的Electron应用:读取页面数据、执行任意JavaScript、模拟用户操作、抓取网络请求。这不是一个可以随便暴露的功能。
我一般会做三层限制。第一,只在自动化测试环境或专用Agent环境里开启调试端口,生产环境默认关闭。第二,把监听地址绑定到127.0.0.1,不要对外开放。第三,如果Agent和Electron应用运行在同一台机器上,还可以在应用内增加一个自动化模式开关,只有显式开启时才注入调试开关,避免端口长期开放。
另外要提醒一点:Electron应用开启远程调试后,它内部所有JavaScript执行能力都会暴露给连接方。Agent链路本质上是在目标应用内执行任意代码,这必须建立在"你对这个应用有合法操作权限"的前提下。把这套能力用在自己的应用、自己拥有的测试环境或者取得明确授权的系统里,是完全没问题的,但绝不是用来绕开任何访问控制的工具。
6.2 这个方案不适用的情况
CDP方案也不是万能的。虽然Electron应用本质是Chromium,但仍有一些场景不太适合直接套用。
最典型的是Canvas或WebGL渲染的复杂界面。如果界面的核心内容都在Canvas里绘制,比如绘图工具、图形编辑器,那基于DOM元素的快照就无法反映真实状态,坐标点击也往往偏离实际绘制内容。这种情况应该考虑结合图像识别或直接调用应用的内部API。
再看音视频播放器这一类场景,播放控制往往走HTML5媒体API,CDP能够通过Runtime.evaluate操作video元素,但如果业务逻辑大量存在于WebAssembly或原生插件中,DOM层面的操作就只是表面功夫。
还有一类是主进程逻辑占大头的Electron应用。CDP控制的主要是渲染层,主进程里的文件系统操作、系统托盘、原生菜单,CDP并不能直接接管。需要操作这些能力时,还是得在应用侧留出专门的接口让Agent调用,而不是硬用CDP去模拟。
我之前接手过一个混合架构的桌面工具,界面部分用Electron,但核心能力封装在系统原生模块里。Agent操作界面做得再好,最终触发功能都必须经过原生模块的接口。后来我在Electron应用里加了一个自动化服务端口,Agent通过一个受控的本地机制去调用原生模块能力,同时继续用CDP操作界面,两者配合才把整条链路跑通。
这个经验也说明:CDP是一种很优秀的界面层自动化通道,但它解决的问题边界是"操控Chromium渲染中的UI"。一个完整稳健的Agent方案,往往是CDP加应用级接口的组合,而不是单靠某一个通道包打天下。
如果你正在做Agent自动操作Electron应用这件事,我建议你先把CDP这一条链路搭起来,体验一下"从DOM层面直接操控应用"的爽快感,然后再根据你具体应用的架构去补上应用级接口那一环。等到整个系统稳定跑起来之后,你会和我一样发现,最值钱的不是一次次手动写自动化脚本,而是那套能让Agent持续感知界面、自主决策、可靠执行、并从容面对各种异常状态的任务循环。
我在真实项目里把这套方案跑到了上万次的自动化操作,长期稳定性比最开始用坐标方案时的体感好了太多。后续你还可能遇到消息序错乱、特定控件点击无效、Electron版本升级导致CDP行为变化这些问题,解决思路都是同一个:回到CDP的消息模型和Chromium的渲染逻辑里去推演,而不是绕着界面打转。这个方向,值得每一位做桌面智能体、RPA、自动化测试的同学深入研究。