Files
GenericAgent/memory/tmwebdriver_sop.md

14 KiB
Raw Blame History

TMWebDriver SOP

  • 禁止import直接用web_scan/web_execute_js工具。本文件只记录特性和坑。
  • 底层:../TMWebDriver.py通过Chrome扩展(非Tampermonkey)接管用户浏览器(保留登录态/Cookie
  • 非Selenium/Playwright不需调试浏览器或新数据目录
  • 支撑 web_scan(只读DOM) / web_execute_js(执行JS) 等高层工具
  • ⚠扩展更新后已打开的旧tab不会自动加载新版脚本→scan/execute_js无ACK→需刷新页面或切到新tab

通用特性

  • web_execute_js完美支持顶层awaitv0.4+),可直接await fetch()/await new Promise()
    • ⚠使用await时需**显式return**才能拿到返回值底层async包裹不写return则返回null
  • web_scan自动穿透同源iframe无需手动操作scan直接递归输出iframe内部DOM。跨域iframe则需CDP或postMessage见下方章节

限制(isTrusted)

  • JS dispatch的事件isTrusted=false,敏感操作(文件上传/部分按钮)会被浏览器拦截
  • 首选绕过CDP桥——CDP派发的Input事件是浏览器原生级别(isTrusted=true)且无需前台见下方CDP章节
  • 文件上传JS无法填充<input type=file>
    • 首选CDP batchgetDocument→querySelector→DOM.setFileInputFiles(无需前台/物理点击)
    • 备选ljqCtrl物理点击SetForegroundWindow→点上传按钮→FindWindow轮询对话框→输入路径→轮询关闭
  • 备选:元素→屏幕物理坐标(ljqCtrl/PostMessage点击前必算)JS一次取rect+窗口信息,公式:
    • physX = (screenX + rect中心x) * dprphysY = (screenY + chromeH + rect中心y) * dpr
    • chromeH = outerHeight - innerHeightdpr = devicePixelRatio
    • 注意screenX/Y也是CSS像素所有值先加后统一乘dpr
  • 结论:读信息+普通操作用TMWebDriver需isTrusted事件首选CDP桥文件上传首选CDP三连(备选ljqCtrl)

导航

  • web_scan 仅读当前页不导航,切换网站用 web_execute_js + location.href='url'

Google图搜

  • class名混淆禁硬编码点击结果用 [role=button] div
  • web_scan过滤边栏弹出后用JS文本document.body.innerText大图遍历img按naturalWidth最大取src
  • "访问"链接遍历a找textContent.includes('访问')的href
  • 缩略图:img[src^="data:image"]直接提取大图src可能截断用return img.src

Chrome下载PDF

场景PDF链接在浏览器内预览而非下载

fetch('PDF_URL').then(r=>r.blob()).then(b=>{
  const a=document.createElement('a');
  a.href=URL.createObjectURL(b);
  a.download='filename.pdf';
  a.click();
});

注意需同源或CORS允许跨域先导航到目标域再执行

Chrome后台标签节流

  • 后台标签中setTimeout被Chrome intensive throttling延迟到≥1min/次
  • 扩展content script中detect_newtab的轮询(setTimeout 150ms × 10)会超时
  • 已修复移除脚本内轮询改由Python侧get_session_dict()前后对比检测新标签
  • 同理扩展脚本中任何后台逻辑都应避免依赖setTimeout轮询

CDP桥(tmwd_cdp_bridge扩展) 首选

扩展路径:assets/tmwd_cdp_bridge/(需安装含debugger权限) ⚠TID密钥首次运行自动生成到assets/tmwd_cdp_bridge/config.js(已gitignore)扩展通过manifest引用 调用:web_execute_js script直传JSON字符串工具层自动识别对象格式走WS→background.js cmd路由

// 直接传JSON字符串作为script参数无需DOM操作
web_execute_js script='{"cmd": "cookies"}'
web_execute_js script='{"cmd": "tabs"}'
web_execute_js script='{"cmd": "cdp", "tabId": N, "method": "...", "params": {...}}'
web_execute_js script='{"cmd": "batch", "commands": [...]}'
// 返回值直接是JSON结果

⚠旧DOM方式(TID元素+MutationObserver)仍可用但已不推荐 单命令:{cmd:'tabs'} | {cmd:'cookies'} | {cmd:'cdp', tabId:N, method:'...', params:{...}}

  • batch混合{cmd:'batch', commands:[{cmd:'cookies'},{cmd:'tabs'},{cmd:'cdp',...},...]}
    • 返回{ok:true, results:[...]}一次请求多命令CDP懒attach复用session
    • 子命令会自动继承外层batch的tabId如cookies命令可正确获取当前页面URL
    • $N.path引用第N个结果字段(0-indexed),如"nodeId":"$2.root.nodeId"
    • ⚠batch前序命令失败时后续$N引用拿到undefined整条链路静默失败不报错需检查返回results数组中每项的ok状态未验证BBS#46
    • 典型:文件上传三连 getDocument(depth:1性能优化200ms+→个位数ms)→querySelector(input[type=file])→setFileInputFiles未验证BBS#38
    • ⚠nodeId路径一致性getDocument+querySelector路径和performSearch+getSearchResults路径的nodeId不互通同一batch内不可混用未验证BBS#45
    • ⚠文件上传后前端框架(React/Vue)可能不感知→JS补发两个事件Vue3需input事件而非仅change未验证BBS#35/#39
      el.dispatchEvent(new Event('input', {bubbles:true}));
      el.dispatchEvent(new Event('change', {bubbles:true}));
      
      • Electron<12/旧WebView可能无InputEvent构造函数防御性降级未验证BBS#42 const Ctor = typeof InputEvent !== 'undefined' ? InputEvent : Event; el.dispatchEvent(new Ctor('input', {bubbles:true}));
      • 极端情况(框架仍不响应)Runtime.evaluate直接访问React __reactFiber 或 Vue __vue__ 触发状态更新未验证BBS#43
    • ⚠上传前检查input.accept属性setFileInputFiles不校验类型但前端框架change handler会检查不匹配会静默丢弃未验证BBS#38
    • ⚠多file input定位DOM.querySelectorAll返回nodeId数组用accept/父容器类名区分用途未验证BBS#38/#39
      • 框架选择器Element UI .el-upload__input | Ant Design .ant-upload input[type=file] | Naive UI .n-upload-trigger input[type=file] | Dropzone .dz-hidden-input未验证BBS#39
    • ⚠Dropzone拖拽上传90%底层仍创建隐藏<input type=file>先querySelectorAll('input[type=file]')全局扫未验证BBS#35/#38
    • 轻量元素存在检测:DOM.performSearch({query:'input[type=file]'})返回resultCount不触发DOM树构建轮询等待元素时避免重复getDocument未验证BBS#39
      • performSearch支持三种语法CSS选择器 / XPath(//input[@type='file']) / 纯文本自动识别未验证BBS#41
    • 瞬态file input处理Ant Design等框架点击上传按钮时动态创建input上传完立即销毁未验证BBS#42/#43
      • 方案A(批处理)在同一batch内完成 performSearch→getSearchResults→setFileInputFiles→discardSearchResults缩小input被销毁的时间窗口discardSearchResults防searchId泄漏未验证BBS#46
      • 方案B(事件监听)DOM.enable后监听DOM.childNodeInserted事件捕获input创建瞬间零延迟拿到nodeId
        • 前提须先对document.body的nodeId调DOM.requestChildNodes否则CDP不推送子树变更
        • DOM.disable会使所有已获取nodeId失效setFileInputFiles必须在disable之前。正确时序DOM.enable→requestChildNodes→[等事件]→setFileInputFiles→DOM.disable未验证BBS#45
      • 方案C(猴子补丁兜底)Runtime.evaluate注入MutationObserver标记新增file input阻止框架销毁争取时间窗口
        • ⚠React/Vue用parentNode.removeChild(node)而非node.remove()需patch Element.prototype.removeChild过滤input[type=file]未验证BBS#45
        • ⚠Svelte等框架可能用replaceChildtextContent=''清空父容器间接移除绕过removeChild补丁极端场景性价比低建议回退方案B未验证BBS#46
        • ⚠阻止销毁会内存泄漏用完后手动清理被标记的节点未验证BBS#45
        • FileList只读最终仍需CDP setFileInputFiles
    • ⚠tabIdCDP默认sender.tab.id(当前注入页)跨tab需显式tabId或先batch内tabs查
  • CDP可用任意方法(Input/Network/DOM/Page/Runtime/Emulation等)单条每次attach→send→detach
  • 跨tab无需前台指定tabId即可操作后台标签页
  • 绕过isTrustedCDP派发的Input事件是浏览器原生级别

CDP点击完整生命周期未验证BBS#23

  • 通用点击需三事件序列mouseMoved → mousePressed → mouseReleased间隔50-100ms
    • 省略mouseMoved会导致MUI Tooltip/Ant Design Dropdown等hover依赖组件失效
    • ⚠autofill释放是特例只需mousePressed即可见下方autofill章节
  • 坐标修正页面有transform:scale/zoom时
    var scale = window.visualViewport ? window.visualViewport.scale : 1;
    var zoom = parseFloat(getComputedStyle(document.documentElement).zoom) || 1;
    var realX = x * zoom; var realY = y * zoom;
    
  • iframe内元素CDP点击坐标需合成 finalX = iframeRect.x + elRect.x
    • 跨域iframe拿不到contentDocument
    • Target.getTargets/Target.attachToTarget在CDP桥中返回"Not allowed"(chrome.debugger权限限制)
    • 已验证方案Page.getFrameTree找iframe frameId → Page.createIsolatedWorld({frameId})获取contextId → Runtime.evaluate({expression, contextId})在iframe中执行JS
    • batch链式引用$0.frameTree.childFrames遍历找url匹配的frame$1.executionContextId传给evaluate
    • postMessage中继方案仅在content script已注入iframe时有效第三方支付iframe通常无注入

CDP文本输入未验证BBS#23

  • Input.insertText({text:'...'}) — 直接插入不触发keydown/keyup
  • Input.dispatchKeyEvent — 逐键派发,慢但完整模拟
  • React/Vue受控组件先insertText再JS手动dispatch input事件input事件不检查isTrusted
  • 简单输入框用insertText够用

CDP DOM域穿透 closed Shadow DOM未验证BBS#24/#25

  • DOM.getDocument({depth:-1, pierce:true}) 穿透所有Shadow边界含closed
  • DOM.querySelector({nodeId, selector}) 定位 → DOM.getBoxModel({nodeId}) 取坐标
  • getBoxModel返回content八值[x1,y1,...x4,y4],中心用四点平均centerX=sum(x)/4, centerY=sum(y)/4
    • ⚠不能简化为对角线平均——元素有transform:rotate/skew时四点非矩形
  • querySelector不能跨Shadow边界写组合选择器需分步先找host再在其shadow内找子元素
  • ⚠nodeId在DOM变更后失效 → 用backendNodeId更稳定或重新getDocument刷新
  • 渲染检查:DOM.resolveNodeRuntime.callFunctionOn 检查offsetHeight>0
  • 完整pipeline: getDocument(pierce) → querySelector → getBoxModel → 四点平均坐标 → Input三事件点击

autofill获取与登录 (需 v0.4+ 脚本支持 await)

检测web_scan输出input带data-autofilled="true"value显示为受保护提示(非真实值Chrome安全保护需点击释放)

  • 前置条件必须先CDP Page.bringToFront 切tab到前台Chrome仅在前台tab释放autofill保护值后台tab物理点击无效
  • 一键释放与登录:利用顶层 await,在单次 web_execute_js 中连贯完成:
    1. CDP batch发送 Page.bringToFront 切到前台。
    2. JS获取输入框坐标。
    3. CDP发送 Input.dispatchMouseEvent (mousePressed) 物理点击释放autofill。
    4. await new Promise(r => setTimeout(r, 500)) 等待释放。
    5. 派发 input/change 事件唤醒前端框架(解禁登录按钮)。
    6. 触发登录点击。
  • ⚠只需 mousePressed,无需 mouseReleased。点击一个字段即释放全页。
  • ⚠使用await时需显式return返回值否则async包裹层默认返回null。

验证码/页面视觉截图

  • 首选CDP截图Page.captureScreenshot(format:'png')→返回base64无需前台/后台tab也行全页高清
  • 验证码canvas/imgJS canvas.toDataURL() 直接拿base64最干净
  • 备选:window.open(location.href,'_blank') 前台开新标签→win32截图→完后close

simphtml与TMWebDriver调试

  • simphtml唯一调试方法:必须通过 code_run 注入JS到真实浏览器执行因为Python端无法完全模拟DOM行为。
    import sys
    sys.path.append('../')
    from TMWebDriver import *
    from simphtml import *
    driver = TMWebDriver()
    res = driver.execute_js(js_optHTML) # js_optHTML为simphtml中注入的JS代码
    
  • d=TMWebDriver(), d.set_session('url_pattern'), d.execute_js('code') → 返回{'data': value}(非裸值)
  • 配合simphtmlstr(simphtml.optimize_html_for_tokens(html)) → 注意返回BS4 Tag需str()
  • DOMRect坑点(hasOverlap)DOMRect 对象在某些浏览器/上下文中可能缺少 xy 属性(只有 left/top),直接访问 rect.x 会得到 undefined,导致数学计算(如重叠判定)变成 NaN,从而引发逻辑错误(如错误判定为重叠导致元素被误删)。必须兼容:const x = rect.x !== undefined ? rect.x : rect.left;

跨域iframe操控(postMessage中继)

  • 跨域iframe的contentDocument不可访问web_execute_js只在顶层执行
  • 扩展content script已支持iframe内不return改为监听postMessage并eval执行+回传结果
  • 顶层发送:iframe.contentWindow.postMessage({type:'ljq_exec', id, code}, '*')
  • iframe回传{type:'ljq_result', id, result} 通过window.addEventListener('message')接收
  • ⚠只能eval表达式不支持return/函数体包装,构造代码时注意
  • 流程发postMessage→等→读window._ljqResults[id]获取结果
  • 已验证读取iframe内DOM(document.title)、填写input均成功

连不上排查

web_scan失败时按序排查 ①扩展没装→检查Chrome扩展列表(chrome://extensions)是否有TMWebDriver扩展 没找到→走web_setup_sop找到→确认已启用 ②浏览器没开?→检查①对应的浏览器进程是否在跑(tasklist/ps)没有则启动并打开正常URL⚠about:blank等内部页不加载扩展 ③WS后台挂了→socket.connect_ex(('127.0.0.1',18766))非0即dead→手动from TMWebDriver import TMWebDriver; TMWebDriver()起master

性能

  • ⚠ URL必须用127.0.0.1不用localhost。Windows下localhost先尝试IPv6(::1)超时2s再回退IPv4每次HTTP请求多2s