fix: WS session reconnect + replace tornado with wsgiref + simplify SOP

This commit is contained in:
Liang Jiaqing
2026-02-18 19:06:21 +08:00
parent 38b5b3b86d
commit e3d2b3d14c
2 changed files with 40 additions and 61 deletions

View File

@@ -1,4 +1,4 @@
import json, threading, time, uuid, queue, socket, requests import json, threading, time, uuid, queue, socket, requests, traceback
from typing import Dict, Any, Optional, List from typing import Dict, Any, Optional, List
from simple_websocket_server import WebSocketServer, WebSocket from simple_websocket_server import WebSocketServer, WebSocket
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
@@ -59,6 +59,7 @@ class TMWebDriver:
print(f"Browser http connected: {session.url} (Session: {session_id})") print(f"Browser http connected: {session.url} (Session: {session_id})")
self.sessions[session_id] = session self.sessions[session_id] = session
session = self.sessions[session_id] session = self.sessions[session_id]
if session.disconnect_at is not None and session.type != 'http': session.reconnect(queue.Queue(), session_info)
session.disconnect_at = None session.disconnect_at = None
if session.type == 'http': msgQ = session.http_queue if session.type == 'http': msgQ = session.http_queue
else: return json.dumps({"id": "", "ret": "use ws"}) else: return json.dumps({"id": "", "ret": "use ws"})
@@ -67,7 +68,7 @@ class TMWebDriver:
try: try:
msg = msgQ.get(timeout=0.2) msg = msgQ.get(timeout=0.2)
try: self.acks[json.loads(msg).get('id','')] = True try: self.acks[json.loads(msg).get('id','')] = True
except: pass except: traceback.print_exc()
return msg return msg
except queue.Empty: continue except queue.Empty: continue
return json.dumps({"id": "", "ret": "next long-poll"}) return json.dumps({"id": "", "ret": "next long-poll"})
@@ -102,12 +103,13 @@ class TMWebDriver:
return json.dumps({'error': str(e)}, ensure_ascii=False) return json.dumps({'error': str(e)}, ensure_ascii=False)
return 'ok' return 'ok'
def run(): def run():
import asyncio from wsgiref.simple_server import make_server, WSGIServer, WSGIRequestHandler
loop = asyncio.new_event_loop() from socketserver import ThreadingMixIn
asyncio.set_event_loop(loop) class _T(ThreadingMixIn, WSGIServer): pass
bottle.run(app, host=self.host, port=self.port+1, server='tornado', threads=20) class _H(WSGIRequestHandler):
http_thread = threading.Thread(target=run) def log_request(self, *a): pass
http_thread.daemon = True make_server(self.host, self.port+1, app, server_class=_T, handler_class=_H).serve_forever()
http_thread = threading.Thread(target=run, daemon=True)
http_thread.start() http_thread.start()
def clean_sessions(self): def clean_sessions(self):
@@ -216,7 +218,7 @@ class TMWebDriver:
hasjump = acked = False hasjump = acked = False
while exec_id not in self.results: while exec_id not in self.results:
time.sleep(0.1) time.sleep(0.5)
if not acked and exec_id in self.acks: if not acked and exec_id in self.acks:
acked = True; start_time = time.time() acked = True; start_time = time.time()
if tp == 'ws': if tp == 'ws':

View File

@@ -1,56 +1,33 @@
```markdown # TMWebDriver SOP
# TMWebDriver 极简说明L3
- 位置:`../TMWebDriver.py` - 禁止import直接用web_scan/web_execute_js工具。本文件只记录特性和坑。
- 角色:本项目的 **Web 桥接层**,支撑 `web_scan` / `web_execute_js` 等高层工具。 - 底层:`../TMWebDriver.py`通过Tampermonkey脚本接管用户浏览器保留登录态/Cookie
- 非Selenium/Playwright不需调试浏览器或新数据目录
- 支撑 `web_scan`(只读DOM) / `web_execute_js`(执行JS) 等高层工具
## 和 WebDriver / Playwright 的关键差异 ## 限制(isTrusted)
- JS dispatch的事件`isTrusted=false`,敏感操作(文件上传/部分按钮)会被浏览器拦截
- 文件上传JS无法填充`<input type=file>`必须ljqCtrl物理点击+Win32轮询文件对话框
- 流程SetForegroundWindow→ljqCtrl点上传按钮→FindWindow轮询对话框→输入路径→轮询关闭
- 结论:读信息+普通操作用TMWebDriver文件上传等敏感操作需配合ljqCtrl
- 它**不是** Selenium WebDriver 或 Playwright。 ## 导航
- 主要优势: - `web_scan` 仅读当前页不导航,切换网站用 `web_execute_js` + `location.href='url'`
- 不需要单独开“调试浏览器”或新用户数据目录。
- 可以**直接接管你当前已经在用的浏览器**含现成登录状态、Cookie 等),由 Tampermonkey 脚本转发命令和结果。
- 典型适用场景:
- 在用户日常使用的浏览器里做轻量自动化:读 DOM、执行简单 JS、在当前页面上点按/滚动等。
## 关键限制(未来可能踩坑的点) ## 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`
- 受浏览器 **InTrusted / 权限策略** 限制,有些操作不能单靠 TMWebDriver 完成: ## Chrome下载PDF
1. **打开新窗口 / 新标签** 场景PDF链接在浏览器内预览而非下载
- 已通过 GM_openInTab 替换 window.open 解决。潜在问题:部分浏览器可能仍需用户显式允许脚本打开新标签。 ```js
2. **上传文件等受信任交互** fetch('PDF_URL').then(r=>r.blob()).then(b=>{
- 通常无法单纯用 JS 填充 `<input type="file">` 等敏感控件。
- 需要配合键鼠控制工具(`ljqCtrl.py` / 控制 SOP在前台模拟真实点击和文件选择。
- **文件上传操作要点**:①点击前用 `SetForegroundWindow` 确保浏览器窗口最前;②用 ljqCtrl 物理点击上传按钮禁止JS click③用 Win32 `FindWindow` 轮询检测文件对话框是否弹出,确认后再输入路径;④操作后同样轮询检测对话框是否关闭,再继续后续步骤。
- 结论:
- TMWebDriver 适合“读信息 + 普通页面操作”;
- 对“新窗口授权、文件上传”这类敏感操作,应默认联想到:**需要和 Ctrl 工具协同**,而不是强行在 JS 里搞定。
```
## 导航避坑
- `web_scan` 仅读当前页,不会导航。
- 切换网站用 `web_execute_js` + `location.href = 'url'`
## Google图片搜索操作
- **class名不可靠**Google的class均为混淆名(如F0uyec),随版本变化,禁止硬编码
- 点击图片结果:找搜索结果区内 `[role=button]` 的div而非外层容器或内部a/img
- `web_scan` 会过滤边栏内容边栏弹出后用JS提取
- 文本:`document.body.innerText`
- 大图遍历所有img`naturalWidth` 最大的那个取src通常>600px
- "访问"链接:遍历所有`a``textContent.includes('访问')`的href
- 缩略图base64结果中`img[src^="data:image"]`可直接提取保存
- 下载大图时注意JS返回的src可能被截断`return img.src`获取完整URL
## Chrome下载PDFfetch+blob方案
- 场景页面上的PDF链接点击后会在浏览器内预览而非下载
- 方案用JS `fetch(url)` 获取blob → `URL.createObjectURL(blob)` → 创建隐藏`<a>`标签设`download`属性 → 触发click → 自动下载到~/Downloads
- 模板:
```js
fetch('PDF_URL').then(r=>r.blob()).then(b=>{
const a=document.createElement('a'); const a=document.createElement('a');
a.href=URL.createObjectURL(b); a.href=URL.createObjectURL(b);
a.download='filename.pdf'; a.download='filename.pdf';
a.click(); a.click();
}); });
``` ```
- 注意需同源或CORS允许跨域时可能需要先导航到目标域再执行 注意需同源或CORS允许跨域先导航到目标域再执行