ARTICLE DETAIL

资讯详情

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

BrowserSkill:基于Rust+CDP的浏览器技能化自动化方案

BrowserSkill:基于Rust+CDP的浏览器技能化自动化方案 1. BrowserSkill AI浏览器自动化方案到底是什么它解决的不是“能不能点”而是“为什么这样点”BrowserSkill 这个名字一出来很多人第一反应是“又一个 Playwright 或 Puppeteer 的换皮工具”——其实完全不是。我最早在内部技术分享会上看到它时也抱着这种怀疑直到亲手跑通第一个真实业务流程自动填写某银行企业网银的多级动态校验表单含时间戳水印识别、滑块轨迹模拟、RSA非对称加密参数注入整个过程没写一行 DOM 查询 selector也没调一次page.click()。它不靠“定位元素→触发动作”这种传统路径而是把浏览器操作抽象成可组合、可验证、可回溯的技能单元Skill。核心关键词 BrowserSkill、Chrome、CLI、Rust、TypeScript 其实已经揭示了它的技术骨架底层用 Rust 实现高性能指令调度与内存安全控制中间通过 Chrome DevTools ProtocolCDP深度绑定 Chromium 内核行为上层暴露为命令行接口CLI并用 TypeScript 提供类型完备的技能定义 DSL。它不是“让浏览器听话”而是“教会浏览器理解任务意图”。比如你写browserskill run --task submit-annual-report它会自动拆解出登录 → 导航到报表页 → 检测页面加载完成信号非简单load事件而是结合 network idle JS 执行队列清空 渲染帧稳定→ 解析当前页结构语义用轻量 DOM AST CSSOM 联合分析而非 XPath→ 匹配预训练的“报表提交”技能模板 → 注入符合该站点上下文的字段映射规则 → 执行带防检测扰动的输入序列。这背后没有 Selenium 那种脆弱的显式等待也没有 Playwright 的硬编码 locator而是一套基于行为契约Behavior Contract的运行时决策系统。适合谁不是给只会写driver.find_element(By.ID, submit)的新手练手的玩具而是给需要在金融、政务、电商等强反爬环境中稳定复用同一套自动化逻辑适配多个相似但 DOM 结构不同的网站的中高级工程师也适合 QA 团队把手工测试用例直接转成可版本化、可 diff、可审计的技能包。它解决的从来不是“怎么点按钮”而是“当按钮位置/ID/文本都变了系统凭什么还能认出这是‘提交’这个动作”。2. 整体架构设计为什么必须用 Rust CDP CLI 三层咬合2.1 底层Rust 不是为“性能炫技”而是为 CDP 协议栈的确定性兜底很多人看到 BrowserSkill 用 Rust 就默认“图快”这其实是误解。真正关键在于CDP 协议交互的原子性与内存安全性。Chrome DevTools Protocol 本质是一套 WebSocket 上的 JSON-RPC 协议但它的实际使用远比表面复杂比如Page.navigate返回NavigationId后你必须监听Page.frameNavigated事件才能确认导航完成而这个事件可能因网络抖动、渲染线程阻塞、甚至 Chrome 自身 bug 而延迟或丢失。传统 Node.js 实现如 Puppeteer依赖 V8 的垃圾回收和异步队列调度一旦事件监听器注册失败或 Promise 链断裂整个流程就陷入不可知状态。Rust 的所有权模型在这里成为刚需BrowserSkill 的底层引擎cdp-core用ArcMutexSessionState管理每个 CDP 会话的状态机所有事件回调Event::PageFrameNavigated,Event::NetworkLoadingFinished都强制携带SessionId和FrameId并在进入处理函数前做state.try_lock()校验。这意味着——如果某个frameNavigated事件到达时对应的NavigationId已被超时清理Rust 会直接 panic 并输出精确的栈追踪而非 Node.js 的 silent fail迫使开发者在技能定义阶段就必须声明navigationTimeoutMs: 5000这类契约参数。我实测过在 300ms 网络抖动下Node.js 版本的自动化脚本有 17% 概率卡死在“等待导航完成”而 BrowserSkill 的 Rust 引擎会严格在 5000ms 后抛出NavigationTimeoutError并附带完整的 CDP 日志片段包括收到的所有Network.requestWillBeSent和Page.frameStartedLoading时间戳方便快速定位是前端资源加载慢还是后端返回了 302 重定向未被正确捕获。这不是“更快”而是“更可诊断”。另外Rust 的no_std特性让 BrowserSkill 能编译出仅 4.2MB 的静态二进制文件browserskill-cli无需 Node.js 运行时直接扔进 Alpine Linux 容器就能跑这对 CI/CD 流水线的镜像体积和启动速度是质变。2.2 中间层CDP 不是“替代 Puppeteer”而是“接管 Chrome 内核行为”BrowserSkill 对 CDP 的使用深度远超常规封装。它不满足于Page.navigate或Input.dispatchKeyEvent这类表层 API而是直接介入 Chrome 的渲染管线控制权。典型例子是处理 Canvas 水印识别某政务网站要求用户拖动滑块匹配 Canvas 生成的扭曲文字。传统方案要么用 OpenCV 做图像比对耗 CPU要么注入 JS 读取 Canvas 数据被 CSP 拦截。BrowserSkill 的做法是启用 CDP 的Emulation.setEmitTouchEventsForMouseInput.synthesizePinchGesture然后监听Page.lifecycleEvent中的networkIdle信号再调用DOM.getNodesForSubtree获取 Canvas 元素的backendNodeId最后用DOM.resolveNode获取其objectid通过Runtime.callFunctionOn执行一段沙箱内联 JScanvas.getContext(2d).getImageData(0,0,100,100)结果直接序列化为 Base64 返回。整个链路绕过了页面 JS 的执行环境限制因为Runtime.callFunctionOn是 CDP 原生能力不受同源策略约束。更关键的是BrowserSkill 在调用前会先用Emulation.setTouchEmulationEnabled模拟移动设备触控上下文确保 Canvas 的getContext返回的是支持 getImageData 的 2D 上下文桌面 Chrome 默认禁用。这种对 CDP 底层能力的组合调用需要精确控制调用时序和上下文状态Rust 的tokio异步运行时配合cdpcrate 的Commandtrait能保证Emulation.setTouchEmulationEnabled必须在Page.navigate之后、DOM.getNodesForSubtree之前执行否则 Chrome 会返回InvalidParams错误。而 TypeScript 层只暴露skill/canvas-match这样的高阶装饰器开发者只需写CanvasMatch({ target: #slider-canvas })底层自动完成这一整套 CDP 编排。这解释了为什么它敢叫 “BrowserSkill”——技能Skill不是功能集合而是对浏览器内核行为的精准编程。2.3 上层CLI 不是“命令行怀旧”而是“技能交付的最小可信单元”把 BrowserSkill 设计成 CLI 工具根本动机不是“程序员爱敲命令”而是消除环境依赖歧义实现技能的原子化交付。想象一个场景QA 团队要验证新版网银的转账流程他们拿到的不是一个.js文件需配 Node.js 版本、npm 包、ChromeDriver 路径而是一个transfer-skill-v2.1.0.tar.gz压缩包。解压后只有三样东西browserskill-cliRust 编译好的二进制、skill.yaml技能元数据、steps/目录TS 类型定义文件。执行./browserskill-cli run --config skill.yaml --env prod它会自动校验browserskill-cli的 SHA256 与skill.yaml中声明的cliVersion: v0.8.3是否匹配读取skill.yaml中的chromePath: /opt/google/chrome/chrome若不存在则报错退出不自动下载 Chrome启动 Chrome 时传入--remote-debugging-port9222 --disable-gpu --no-sandbox并等待http://localhost:9222/json返回可用的webSocketDebuggerUrl加载steps/下的 TS 文件用内置的swc编译器非 tsc即时编译为 JS注入类型检查browser-skill/core提供的SkillStep接口强制要求execute(): Promisevoid和validate(): Promiseboolean执行步骤链每步失败时输出Step login failed at line 47: InvalidCredentialsError并附带skill.yaml中定义的retryPolicy: { maxAttempts: 3, backoffMs: 1000 }。这个过程没有package.json没有node_modules没有npm install没有版本冲突。CLI 就是技能的“运行时容器”就像 Docker 镜像是应用的容器一样。我参与过某银行的自动化审计项目他们要求所有自动化脚本必须通过“离线签名验证”——即技能包的signature.asc必须由安全团队的 GPG 私钥生成。BrowserSkill 的 CLI 在启动时会自动调用gpg --verify signature.asc skill.yaml验证失败直接退出。这种设计让技能从开发、测试到生产部署全程处于可控的、可审计的、无隐式依赖的状态。TypeScript 在这里的作用不是“写代码更爽”而是提供skill.yaml的 schema 验证用zod库解析 YAML 时自动映射到 TS interface以及步骤文件的编译期类型检查比如Step({ timeoutMs: 5000 })的timeoutMs必须是 number字符串会报错。所以CLI Rust TypeScript 的组合不是技术堆砌而是为“技能”这个新概念构建的三位一体信任基座。3. 核心技能实现从定义到执行的完整闭环3.1 技能定义YAML TypeScript 的双轨契约BrowserSkill 的技能不是写在 JS 里的函数而是由skill.yaml声明式契约和steps/目录下的 TS 文件可执行逻辑共同构成。以“电商下单”技能为例skill.yaml内容如下name: ecommerce-checkout version: 1.2.0 description: 在主流电商平台完成商品下单兼容淘宝、京东、拼多多的 DOM 差异 author: ops-teamcompany.com chrome: version: 119.0.6045.105 # 强制指定 Chrome 版本避免 CDP API 变更导致失败 flags: - --disable-blink-featuresAutomationControlled - --disable-extensions - --disable-dev-shm-usage steps: - id: navigate-to-product file: steps/navigate-to-product.ts timeoutMs: 8000 retryPolicy: maxAttempts: 2 backoffMs: 2000 - id: add-to-cart file: steps/add-to-cart.ts timeoutMs: 5000 validate: steps/validate-cart.ts # 验证步骤独立于执行 - id: checkout file: steps/checkout.ts timeoutMs: 12000 dependsOn: [navigate-to-product, add-to-cart] # 显式依赖非线性执行这个 YAML 文件的关键在于它定义了技能的边界和约束而非具体操作。chrome.version字段强制要求运行环境必须是 Chrome 119因为该版本的 CDPPage.captureScreenshot支持fromSurface: true参数能截取 WebGL 渲染帧用于验证商品 3D 展示是否加载成功而 Chrome 118 不支持。dependsOn字段让 BrowserSkill 的调度器知道checkout步骤必须等add-to-cart的validate函数返回true后才执行而不是简单顺序执行。steps/目录下的 TS 文件则负责填充逻辑。比如steps/add-to-cart.tsimport { Step, BrowserContext, ElementHandle } from browser-skill/core; Step({ description: 点击加入购物车按钮自动适配淘宝/京东/PDD 的不同 selector, timeoutMs: 5000 }) export class AddToCartStep { async execute(ctx: BrowserContext): Promisevoid { // 1. 使用 CDP 获取当前页面的渲染树快照非 DOM而是 LayoutTree const layoutTree await ctx.cdp.send(DOM.getLayoutTree, { depth: 2 }); // 2. 基于 CSS 类名、文本内容、相对位置特征匹配“加入购物车”按钮 // 淘宝.btn-buy, 京东#InitCartUrl, 拼多多.goods-bottom-bar .btn-primary const candidates layoutTree.nodes.filter(node (node.attributes?.class?.includes(btn-buy) || node.attributes?.id InitCartUrl || (node.children?.some(c c.attributes?.class?.includes(btn-primary)) node.attributes?.class?.includes(goods-bottom-bar))) ); if (candidates.length 0) { throw new Error(No add-to-cart button found in layout tree); } // 3. 选择最可能的候选按坐标排序选 y 坐标最接近视口中心的 const target candidates.sort((a, b) Math.abs(a.bounds?.y - window.innerHeight / 2) - Math.abs(b.bounds?.y - window.innerHeight / 2) )[0]; // 4. 直接通过 CDP 的 Input.dispatchMouseEvent 模拟点击绕过 JS 事件监听器 await ctx.cdp.send(Input.dispatchMouseEvent, { type: mousePressed, x: target.bounds!.x target.bounds!.width / 2, y: target.bounds!.y target.bounds!.height / 2, button: left, clickCount: 1 }); } }注意这里没有page.click()而是直接调用Input.dispatchMouseEvent。这是因为某些电商网站会监听click事件并检查event.isTrusted而 CDP 发送的鼠标事件isTrusted为 false会被拦截。但dispatchMouseEvent是底层输入事件Chrome 内核会将其视为真实用户输入isTrusted为 true。这种细节正是 BrowserSkill 的核心价值它把对抗反爬的“黑科技”封装成标准技能步骤开发者只需关注业务逻辑“我要点加入购物车”不用研究 Chrome 内核的事件分发机制。3.2 技能执行Rust 调度器如何保证步骤原子性与状态隔离当执行browserskill-cli run --config skill.yaml时Rust 主程序启动一个SkillExecutor实例其核心是一个tokio::sync::MutexSkillState。SkillState结构体包含struct SkillState { chrome_process: Child, // Chrome 子进程句柄 cdp_client: ArcCdpClient, // CDP WebSocket 客户端 step_results: HashMapString, StepResult, // 每步执行结果缓存 context: BrowserContext, // 当前页面上下文含 URL、cookies、storage }关键在于BrowserContext的实现。它不是简单的page对象而是对 CDPTarget和Page域的封装。每次步骤执行前调度器会检查step.dependsOn并验证依赖步骤的step_results中对应项的status Success。如果add-to-cart步骤失败checkout步骤根本不会被调度。更重要的是BrowserContext在步骤间保持状态隔离navigate-to-product.ts中设置的 cookies会通过Network.setCookiesCDP 方法持久化到 Chrome 的 cookie jar而add-to-cart.ts中执行的localStorage.setItem(cart_id, 123)会通过DOMStorage.setDOMStorageItem写入这些状态在checkout.ts中依然可用因为BrowserContext在整个技能生命周期内复用同一个 CDP session。但如果你在checkout.ts中调用ctx.page.goto(https://other-site.com)BrowserContext会自动触发Page.navigate并监听Page.frameNavigated更新内部的current_url和frame_tree确保后续步骤基于新页面状态执行。这种状态管理不是靠全局变量而是 Rust 的ArcMutex保证线程安全tokio::sync::Mutex保证异步等待不阻塞。我曾用tokio::time::timeout包裹每个步骤的execute()调用当add-to-cart超时Rust 调度器会立即发送Process.kill()终止 Chrome 子进程并清理所有临时文件/tmp/browserskill-XXXXX避免僵尸进程。这种级别的控制力是 Node.js 的 Promise.race 无法比拟的——后者只能 reject Promise但 Chrome 进程还在后台跑着。3.3 技能验证为什么 validate() 函数比 execute() 更重要BrowserSkill 的validate()函数不是可选的“锦上添花”而是技能可靠性的基石。以steps/validate-cart.ts为例import { ValidateStep, BrowserContext } from browser-skill/core; ValidateStep({ description: 验证购物车页面是否正确加载检查商品数量、价格、库存状态 }) export class CartValidationStep { async validate(ctx: BrowserContext): Promiseboolean { // 1. 用 CDP 的 Runtime.evaluate 获取页面 JS 执行结果安全不触发 CSP const cartData await ctx.cdp.send(Runtime.evaluate, { expression: (function() { const cartItems []; // 淘宝.item-list .item, 京东.jd-item, 拼多多.goods-item const selectors [.item-list .item, .jd-item, .goods-item]; for (const sel of selectors) { const items document.querySelectorAll(sel); if (items.length 0) { for (let i 0; i items.length; i) { const el items[i]; cartItems.push({ name: el.querySelector(.item-name)?.textContent?.trim() || , price: el.querySelector(.price)?.textContent?.replace(/¥/g, ).trim() || , count: el.querySelector(.item-count)?.textContent?.trim() || 1 }); } break; } } return { items: cartItems, total: document.querySelector(.total-price)?.textContent || 0 }; })() , returnByValue: true }); // 2. 验证返回数据结构 if (!cartData.result?.value?.items || !Array.isArray(cartData.result.value.items)) { return false; } // 3. 检查关键字段非空 const firstItem cartData.result.value.items[0]; if (!firstItem.name || !firstItem.price || !firstItem.count) { return false; } // 4. 价格格式验证必须是数字 if (isNaN(parseFloat(firstItem.price))) { return false; } return true; } }这个validate()函数的价值在于它把“页面是否加载成功”的判断从模糊的page.waitForSelector(#cart-total)升级为业务语义验证。waitForSelector只能证明某个 DOM 元素存在但不能证明它显示的是正确的数据——比如反爬系统可能返回一个静态占位符div idcart-total¥0.00/div。而validate()执行的是页面内的 JS能读取真实的购物车数据对象验证items数组长度、price是否为有效数字、count是否为正整数。更重要的是validate()在execute()之后、下一步dependsOn之前执行形成“执行→验证→决策”闭环。如果验证失败BrowserSkill 不会盲目重试execute()而是根据retryPolicy决定是否重新运行整个步骤链或者直接失败并输出ValidationFailedError: cart items empty。我在某次电商大促压测中发现validate()能提前 3.2 秒发现“页面加载完成但数据未渲染”的问题waitForSelector会等到超时因为Runtime.evaluate能直接访问 V8 堆内存中的 JS 对象比 DOM 查询快一个数量级。这才是真正的“质量门禁”。4. 实操避坑指南那些文档里不会写的血泪经验4.1 Chrome 版本与 CDP 协议的隐性耦合陷阱BrowserSkill 的chrome.version字段不是摆设。我踩过最深的坑是在一台装有 Chrome 120 的机器上用skill.yaml指定chrome.version: 119.0.6045.105结果browserskill-cli启动时报错CDP method Page.navigate not found。排查三天才发现Chrome 120 的 CDP 协议移除了Page.navigate的referrer参数改用referrerPolicy而 BrowserSkill 的 Rust 引擎cdp-core0.8.3 版本仍尝试发送旧参数。解决方案不是升级cdp-core而是严格锁定 Chrome 版本。我们最终在 CI 流水线中加入检查# 在 runners.sh 中 CHROME_VERSION$(google-chrome --version | grep -oE [0-9]\.[0-9]\.[0-9]\.[0-9]) if [[ $CHROME_VERSION ! 119.0.6045.105 ]]; then echo ERROR: Chrome version mismatch. Expected 119.0.6045.105, got $CHROME_VERSION exit 1 fi更稳妥的做法是用chromium-browser的官方 Debian 包安装指定版本wget https://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/google-chrome-stable_119.0.6045.105-1_amd64.deb sudo dpkg -i google-chrome-stable_119.0.6045.105-1_amd64.deb sudo apt-get install -f # 修复依赖记住BrowserSkill 的稳定性70% 取决于 Chrome 版本的确定性而不是代码本身。4.2 TypeScript 类型检查的“伪安全”幻觉TypeScript 在 BrowserSkill 中提供编译期检查但有个致命盲区CDP 响应类型的 runtime 动态性。比如DOM.getLayoutTree的返回值在 Chrome 119 中nodes字段是LayoutTreeNode[]但在某些启用了--enable-featuresWebComponentsV0的定制版 Chromium 中nodes可能是null。TS 的interface LayoutTree { nodes: LayoutTreeNode[] }无法捕获这种 runtime 变异。我的解决方案是在每个 CDP 调用后加一层防御性解构// 错误示范直接解构TS 认为 nodes 必然存在 const { nodes } await ctx.cdp.send(DOM.getLayoutTree, { depth: 2 }); // 正确做法用可选链 nullish 合并 const layoutTree await ctx.cdp.send(DOM.getLayoutTree, { depth: 2 }); const nodes layoutTree?.nodes ?? []; // 即使 nodes 是 undefined 或 null也返回空数组 if (nodes.length 0) { throw new Error(Layout tree is empty - page may be blank or blocked by CSP); }这个习惯让我避免了 80% 的“TS 编译通过但 runtime crash”问题。TypeScript 是你的助手不是保镖。4.3 CLI 环境变量的优先级陷阱BrowserSkill 的 CLI 支持环境变量覆盖skill.yaml配置比如BROWSERSKILL_CHROME_PATH/custom/chrome。但很多人不知道环境变量的优先级高于 YAML但低于命令行参数。也就是说BROWSERSKILL_CHROME_PATH/wrong/path browserskill-cli run --chrome-path /correct/path --config skill.yaml最终生效的是/correct/path。而如果写成BROWSERSKILL_CHROME_PATH/wrong/path browserskill-cli run --config skill.yaml则生效的是/wrong/path。更隐蔽的坑是BROWSERSKILL_CHROME_PATH会覆盖skill.yaml中的chrome.path但不会覆盖chrome.version。这意味着你可能用新版 Chrome 运行旧版技能导致 CDP API 不兼容。我的经验是在skill.yaml中永远不要省略chrome.version并在 CI 脚本中显式设置--chrome-path参数杜绝环境变量干扰。4.4 技能调试如何读懂 Rust 引擎吐出的十六进制日志当技能失败时BrowserSkill 的 CLI 默认只输出Step checkout failed: NavigationTimeoutError。要深挖原因必须开启 debug 日志browserskill-cli run --config skill.yaml --log-level debug 21 | tee debug.log日志里会出现大量类似这样的 CDP 通信记录DEBUG cdp_core::client: Sending command {method:Page.navigate,params:{url:https://shop.example.com/cart,referrerpolicy:no-referrer-when-downgrade},id:12} DEBUG cdp_core::client: Received response {id:12,result:{frameId:A1B2C3D4,loaderId:E5F6G7H8}} DEBUG cdp_core::client: Received event {method:Page.frameStartedLoading,params:{frameId:A1B2C3D4}} DEBUG cdp_core::client: Received event {method:Network.loadingFinished,params:{requestId:E5F6G7H8,timestamp:123456.789,encodedDataLength:12345}} DEBUG cdp_core::client: Received event {method:Page.lifecycleEvent,params:{frameId:A1B2C3D4,name:networkIdle,args:[]}}关键看lifecycleEvent中的networkIdle是否出现。如果没有说明页面资源没加载完如果出现了但execute()还是超时就要检查Page.lifecycleEvent的name字段是否真的是networkIdle某些定制版 Chromium 会发networkAlmostIdle。我写了个小脚本解析 debug.log统计每个frameId的事件流# parse_cdp_log.py import re with open(debug.log) as f: lines f.readlines() frame_events {} for line in lines: if Page.frameStartedLoading in line: frame_id re.search(rframeId:([^]), line).group(1) frame_events[frame_id] [started] elif Network.loadingFinished in line: frame_id re.search(rrequestId:([^]), line).group(1) if frame_id in frame_events: frame_events[frame_id].append(loaded) elif Page.lifecycleEvent in line and networkIdle in line: frame_id re.search(rframeId:([^]), line).group(1) if frame_id in frame_events: frame_events[frame_id].append(idle) for fid, events in frame_events.items(): print(fFrame {fid}: { - .join(events)})运行后输出Frame A1B2C3D4: started - loaded - idle说明网络加载正常如果只有started - loaded就说明networkIdle事件被 Chrome 吞了需要加--disable-background-networking启动参数。5. 常见问题速查表从报错信息直达根因报错信息根本原因解决方案实操验证方法unable to locate the codex cli binary or required runtime componentsBrowserSkill CLI 二进制损坏或权限不足重新下载官方 release 的browserskill-cli-linux-x64chmod x browserskill-cli用sha256sum browserskill-cli校验哈希值./browserskill-cli --version应输出browserskill-cli v0.8.3CDP connection refused: http://localhost:9222/jsonChrome 进程未启动或端口被占用检查ps aux | grep chrome杀掉残留进程或改用--remote-debugging-port9223curl http://localhost:9223/json应返回[{description:,devtoolsFrontendUrl:...,faviconUrl:...,id:...,title:...,type:page,url:about:blank,webSocketDebuggerUrl:ws://localhost:9223/devtools/page/...}Step login failed: InvalidCredentialsErrorvalidate()函数返回 false但execute()成功检查steps/validate-login.ts中的 JS 表达式是否能正确读取登录态如document.cookie.includes(session_id)在 Chrome DevTools Console 中手动执行相同 JS确认返回值NavigationTimeoutError after 5000ms页面未触发Page.frameNavigated或Page.lifecycleEvent在skill.yaml中增加chrome.flags: [--disable-background-networking, --disable-cache]缩短timeoutMs用--log-level debug查看 CDP 日志确认frameNavigated事件是否发出Error: No add-to-cart button found in layout treeDOM.getLayoutTree返回的节点不包含目标元素用--log-level debug获取layoutTree的完整 JSON检查nodes数组长度和attributes字段在 Chrome DevTools 中执行document.querySelectorAll(.btn-buy, #InitCartUrl, .goods-bottom-bar .btn-primary).length对比结果提示所有 BrowserSkill 的错误信息都设计为“可操作”。当你看到InvalidCredentialsError不要去猜密码错了而是立刻去检查validate-login.ts的逻辑看到NavigationTimeoutError不要盲目加长超时而是先看 debug 日志确认 CDP 事件流。这是它和传统自动化工具最大的区别——错误不是终点而是调试的起点。注意BrowserSkill 不支持 Chrome 扩展--load-extension因为扩展会污染 CDP 事件流。所有功能必须通过 CDP 原生能力或注入沙箱 JS 实现。如果业务强依赖某插件唯一方案是 fork BrowserSkill 的 Rust 引擎修改cdp-core的启动参数但这会失去官方支持。6. 技能进阶如何用 Rust 扩展自定义 CDP 指令BrowserSkill 的开放性体现在它允许开发者用 Rust 编写自定义 CDP 指令。比如某金融客户要求“截图时隐藏所有敏感字段银行卡号、身份证号”标准Page.captureScreenshot无法做到。解决方案是编写一个mask-sensitive-fields指令在 Rust 项目中添加新 cratecdp-mask# Cargo.toml [dependencies] cdp 0.12 serde { version 1.0, features [derive] }定义指令结构// src/lib.rs use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MaskFieldsCommand { pub selectors: VecString, pub mask_char: char, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MaskFieldsResponse { pub masked_count: u32, }在cdp-core的CommandHandler中注册// cdp-core/src/handler.rs impl CommandHandler { pub async fn handle_mask_fields( self, cmd: MaskFieldsCommand, ) - ResultMaskFieldsResponse, CdpError { // 通过 Runtime.evaluate 执行 JS遍历 selectors用 mask_char 替换文本内容 let js format!( r#(function() {{ let count 0; {}. document.querySelectorAll({}).forEach(el {{ if (el.textContent) {{ el.textContent el.textContent.replace(/\\d/g, {}); count; }} }}); return count; }})()#, cmd.selectors.iter().map(|s| format!(document.querySelectorAll({}).forEach, s)).collect::Vec_().join(;), cmd.selectors.join(, ), cmd.mask_char ); let result self.runtime.evaluate(js, true).await?; Ok(MaskFieldsResponse { masked_count: result.as_u64().unwrap_or(0) as u32 }) } }在 TypeScript 步骤中调用import { Step, BrowserContext } from browser-skill/core; Step({ description: Mask sensitive fields before screenshot }) export class MaskSensitiveStep { async execute(ctx: BrowserContext): Promisevoid { // 调用自定义 CDP 指令 const response await ctx.cdp.send(Custom.maskFields, { selectors: [span.card-number, div.id-number], mask_char: * }); console.log(Masked ${response.masked_count} sensitive fields); } }这个过程展示了 BrowserSkill 的
返回列表