OpenClaw 的 chrome_cdp 工具在驱动 Chrome 跑自动化任务时Target closed 和 Session not found 大概是出现频率最高的两个报错。它们不像端口占用那样一启动就炸而是在任务跑到一半、你以为一切顺利的时候突然抛出来把整个 Agent 流程打断。更麻烦的是很多人第一反应是去翻 CDP 协议文档其实九成以上的情况跟协议无关而是页面已经关了、session 已经废了代码还在往下发命令。这类任务里常见的场景是带滑块验证的表单页拖动过程中页面重载、iframe 切换、验证模块自己刷新 DOM旧的 target 就悄悄失效了。本文按排障视角拆开讲同时把 OpenClaw 模型通道的 Key 申请和 Base URL 配置一并说清楚方便你把「模型能跑」和「CDP 能跑」两条链路都调通。1. OpenClaw chrome_cdp 报 Target closed 的现场还原1.1 报错长得什么样典型日志长这样两行经常前后脚出现Error: Target closed at CDPSession.send (openclaw/browser/cdp-session.js:118:15) at ChromeCdpTool.exec (openclaw/skills/chrome_cdp.js:64:22) Error: Session not found: 8F3A2C1B...第一种是 target 没了。CDP 里 target 可以理解成一个「页面标签」的句柄Chrome 一旦把它关掉、回收掉或者页面自己 crash 了这个句柄就变成空壳你再用它发命令Chrome 直接回一句 Target closed。第二种是 session 没了。CDP 的命令要挂在某个 sessionId 上导航跳转、新开标签、页面崩溃重建之后旧的 sessionId 就作废了继续用就是 Session not found。1.2 为什么偏偏在自动化流程里高发手动操作浏览器时你几乎遇不到这种错因为人不会在页面关掉之后还对着它点鼠标。但 Agent 是流程化的第 3 步拿到的页面句柄可能在第 5 步才用中间那一步如果页面上有验证模块触发了整页刷新句柄就过期了。我见过最多的一种写法是任务收尾时先调了一次关闭然后 finally 块里还要再读一遍页面地址做日志上报这一读就是 Target closed。所以排查顺序建议是先看代码里有没有「关掉之后还继续用」再看是不是页面被动失效。1.3 两条链路要分清这里先把边界划清楚。OpenClaw 跑起来至少涉及两条完全独立的链路一条是模型链路Agent 要做规划、决定下一步调什么工具靠的是大模型 API也就是 Key Base URL 那一套另一条是 CDP 链路chrome_cdp 工具通过 WebSocket 跟本机 Chrome 的调试端口说话。Target closed 属于第二条链路的问题。TaoToken 在本文里只负责第一条链路给 OpenClaw 的 Agent 供 Key 和 Base URL它不替代 CDP 调试也不参与 Chrome 进程管理。分清楚这一点后面排查时就不会把模型返回超时误判成 CDP 断开。2. TaoToken 给 OpenClaw 模型通道供 Key 的前置准备2.1 先注册再创建 Key打开 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 完成注册然后进控制台的 API Keys 页面创建一把新 Key。建议按用途命名比如 openclaw-cdp-debug这样后面在 OpenClaw 配置里一眼能认出来是哪把。创建完立刻复制Key 一般只完整展示一次。如果只是自己本机调试把 Key 放进环境变量比写死在配置文件里安全至少不会跟着代码一起提交到仓库。2.2 Base URL 怎么填坑都在后缀上这一步是最容易填错的地方直接说结论OpenClaw 里模型 Base URL 填https://taotoken.net/api。两个注意点。第一不要在后面加/v1OpenClaw 的 SDK 或适配层通常会自己拼路径你多写一层就变成双重路径表现是 404 而不是鉴权失败很容易误判成 Key 有问题。第二这个地址不要带任何 UTM 参数UTM 是给页面统计用的写进 API 请求地址里会污染路径匹配。注意页面上带 utm_source 的链接是给浏览器点击用的接口地址永远是干净的https://taotoken.net/api两者不要混。2.3 模型名先用一把稳的验证第一次接通不要直接上复杂模型先挑一个响应快、指令跟随稳的模型跑个 你好确认鉴权通、网络通。等 Agent 的单轮对话正常返回了再去调 Agent 的规划 prompt 和工具调用逻辑。这样出错时你能立刻判断是模型通道的问题还是 CDP 通道的问题。3. 可复制配置openclaw.config.json 与 chrome_cdp 调用3.1 模型和 Chrome 分开写配置文件按下面这样分块模型段走 TaoTokenChrome 段走本机调试端口互不干扰{ model: { provider: openai-compatible, baseUrl: https://taotoken.net/api, apiKey: ${TAOTOKEN_API_KEY}, model: 你验证通过的模型名, timeoutMs: 120000 }, chrome: { executablePath: /usr/bin/google-chrome, debuggingPort: 9222, headless: true, args: [ --headlessnew, --remote-debugging-port9222, --disable-blink-featuresAutomationControlled, --no-first-run, --no-default-browser-check ] }, browser: { pool: { maxSize: 3, idleTimeoutMs: 60000, recycleOnDisconnect: true } } }${TAOTOKEN_API_KEY}这种占位写法是为了让 OpenClaw 从环境变量读避免明文落盘。browser.pool这段是本文的重点recycleOnDisconnect: true让工具池发现连接失效时自动丢弃页面不再把坏句柄放回去复用。3.2 一个最小的 chrome_cdp 任务骨架// tasks/check-page.js const { tools } require(openclaw); async function main() { const page await tools.builtin_skills.chrome_cdp({ exec: Target.createTarget, args: { url: about:blank } }); await tools.builtin_skills.chrome_cdp({ exec: Page.navigate, args: { url: https://example.com/form } }); await tools.builtin_skills.chrome_cdp({ exec: Page.enable }); return page; } main().catch(err { console.error([task] 失败:, err.message); process.exit(1); });注意这里没有在任务末尾调用 browser.close()。很多 Target closed 是「关得太早」造成的任务级别的清理交给工具池和进程退出钩子去做业务代码里只负责用完归还。3.3 环境变量与调试开关export TAOTOKEN_API_KEYsk-你在TaoToken创建的Key export OPENCLAW_DEBUGcdp,protocol openclaw run ./tasks/check-page.js --verboseOPENCLAW_DEBUGcdp,protocol会把每一条发出的 CDP 命令和收到的响应帧打出来。不同版本的开关名可能略有差异以你本地openclaw --help里列出的为准。打开之后Target closed 出现在哪一条命令之后会非常清楚通常紧跟着的就是那条「页面已经不存在」的调用。4. 验证请求isConnected 检查与 Runtime.evaluate 实测4.1 先加前置检查再发命令最省事的改法是给所有 CDP 调用套一层 send 包装发命令之前先看连接还在不在// lib/cdp-safe.js async function safeCdpExec(page, method, params {}, retry 2) { for (let i 0; i retry; i) { try { if (typeof page.isConnected function !page.isConnected()) { throw new Error(CDP_SESSION_LOST); } return await page.send(method, params); } catch (err) { const msg String(err err.message); const recoverable /Target closed|Session not found|Session closed/.test(msg); if (!recoverable || i retry) throw err; console.warn([cdp] ${method} 失败: ${msg}第 ${i 1} 次重建上下文); await page.recover(); } } }关键点是isConnected()前置判断加page.recover()重取上下文。recover 内部要做的事是重新拉一次 target 列表、重新 attach、拿到新的 sessionId而不是拿旧句柄硬重试。4.2 用 Runtime.evaluate 确认页面还活着上面这套包装是否生效用一条最直观的命令验证const r await safeCdpExec(page, Runtime.evaluate, { expression: ({ title: document.title, url: location.href, t: Date.now() }), returnByValue: true }); console.log([verify] 页面上下文:, r.result.value);正常返回会长这样[verify] 页面上下文: { title: 表单提交, url: https://example.com/form, t: 1730000000000 }如果这条命令返回的是 Target closed说明页面确实已经不可用recover 会重建如果 recover 之后还是同样的错就不是代码写法问题而是 Chrome 进程本身出状况了去看 9222 端口还在不在监听。4.3 工具池回收失效页面// lib/pool-guard.js async function withPage(task) { const pool tools.builtin_skills.chrome_cdp.pool; let page await pool.acquire({ timeoutMs: 10000 }); try { return await task(page); } catch (err) { if (/Target closed|Session not found/.test(String(err.message))) { pool.discard(page); page null; } throw err; } finally { if (page page.isConnected()) pool.release(page); } }要点是 catch 里discard而不是release。坏掉的页面一旦被归还到池子里下一个任务拿到它第一个命令就是 Target closed这种「跨任务传染」的错最难查。归还前统一判断一次isConnected()能挡掉大部分这类问题。4.4 结合 verify 脚本做一次完整回归openclaw run ./tasks/check-page.js --verbose 21 | tee cdp-verify.log grep -nE Target closed|Session not found|CDP_SESSION_LOST cdp-verify.log日志里搜不到这三类关键字说明本轮的 session 生命周期是干净的。把这个脚本固定成回归用例每次改完工具调用逻辑跑一遍比出事之后再翻几万行日志效率高得多。5. 常见错排查Target closed 与相邻报错对照5.1 一张速查表报错触发时机处理动作Target closed页面已关、crash或关闭后继续发命令isConnected 前置判断 recover 重建Session not found导航或新开标签后沿用旧 sessionId重新 attach取新 sessionIdSession closed对端主动断开常见于独立关闭页面从工具池 discard重新 acquireNo node with given id foundDOM 刷新后沿用旧 nodeId重新 DOM.querySelector 取节点connect ECONNREFUSED 127.0.0.1:9222Chrome 未起或端口被占查 executablePath 与端口监听TimeoutError: waiting for selector页面慢或 target 已切换缩短单次超时 检测 target 变化这张表的使用方式是从上往下对先确认是不是句柄过期前三行再看是不是元素级失效第四行最后才怀疑进程和端口后两行。5.2 加 verbose 之后怎么读日志打开协议日志之后一次成功的 Runtime.evaluate 大约是这样[cdp] -- Runtime.evaluate {expression:document.title,returnByValue:true} [cdp] -- Runtime.evaluate {result:{type:string,value:表单提交}}出现问题的形态非常有辨识度--发出去了但对应的--永远不来取而代之的是一行 Target closed。这说明命令已经离开客户端是对端告诉你目标没了。看这个分界点就能把「客户端写法问题」和「服务端句柄问题」区分开。5.3 滑块类页面上的一个额外注意带滑块验证的表单页拖动过程常常触发模块级重载DOM 整体换一遍。这时候节点 id 全失效表现是 No node with given id found 或者 Target closed 混着出现。稳妥做法是每完成一次拖动动作就重新定位一次元素别把拖动前的节点句柄留到拖动后使用。所有这类操作都要在你有授权的测试环境里进行合规边界别越。5.4 排查顺序建议先跑最小复现脚本只留 Target.createTarget 和一条 Runtime.evaluate确认基础链路通再逐步加 Page.navigate、Page.enable、元素定位每加一步跑一次哪一步开始报 Target closed 就锁定在哪一步。这个「二分加回来」的方式比一上来就怀疑反自动化检测要高效得多。6. 把 Key 与 CDP 排障链路固定下来模型通道这边先去创建或补一把 Key地址在 https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentopenclaw_cdp_api_keys 创建完照着接入文档把 Base URL 填成https://taotoken.net/api文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentopenclaw_cdp_doc 里面按语言给了最小调用示例对着抄一遍最快。想先确认模型通道本身通不通用模型对话页面发一句测试指令就行不用起 Agenthttps://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentopenclaw_cdp_chat 返回正常说明 Key 和 Base URL 都没问题接下来的 Target closed 就纯粹是 CDP 侧的事。如果你的 OpenClaw 是长期挂着跑编码或 Agent 任务Coding Plan 那种更合适省得中途额度断掉把长任务打断https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentopenclaw_cdp_coding_plan 。最后留一个我自己一直在用的习惯把 safeCdpExec 和 withPage 这两个文件单独放任何新写的 chrome_cdp 调用都必须走它们不允许业务代码里直接 page.send。规则定死之后Target closed 基本从「随机炸弹」变成了「日志里一行可预期的 recover 提示」排查成本能降一个数量级。