jerry il y a 3 semaines
Parent
commit
d4ac2d8611
12 fichiers modifiés avec 356 ajouts et 335 suppressions
  1. 4 1
      .gitignore
  2. 14 22
      Dockerfile
  3. 19 13
      config/config.json
  4. 0 192
      config/proxies.json
  5. 1 10
      data/proxy_states.json
  6. 44 44
      docker-compose.yml
  7. 69 37
      plugins/ita_plugin.py
  8. 14 4
      plugins/pol_plugin.py
  9. 47 3
      plugins/tls_plugin.py
  10. 50 4
      plugins/vfs_plugin.py
  11. 80 0
      utils/fingerprint_utils.py
  12. 14 5
      utils/mouse.py

+ 4 - 1
.gitignore

@@ -5,4 +5,7 @@ logs
 *.jpg
 node_modules*
 temp_browser_data
-venv
+venv
+data/squashfs-root
+*.AppImage
+data/temp_browser_data

+ 14 - 22
Dockerfile

@@ -1,25 +1,16 @@
 FROM python:3.10-slim-bookworm
 
-# 设置环境变量
 ENV PYTHONDONTWRITEBYTECODE=1 \
     PYTHONUNBUFFERED=1 \
     TZ=Asia/Shanghai \
     LANG=C.UTF-8 \
-    # 显式指定 Chromium 路径
-    CHROME_BIN=/usr/bin/chromium \
-    # 设置显示端口 (配合 Xvfb)
     DISPLAY=:99 \
-    # 防止 XDG 目录报错
-    XDG_CONFIG_HOME=/tmp/xdg_config
+    XDG_CONFIG_HOME=/tmp/xdg_config \
+    CHROME_BIN=/opt/ungoogled-chromium/chrome
 
-# 1. 安装系统依赖
-# xvfb: 虚拟显示器
-# chromium: 浏览器
-# libgl1: opencv/ddddocr 必须的图形库依赖
-# dumb-init: 僵尸进程回收
+# 安装依赖(移除 chromium / driver)
 RUN apt-get update && apt-get install -y --no-install-recommends \
-    chromium \
-    chromium-driver \
+    xz-utils \
     xvfb \
     xauth \
     fonts-noto-cjk \
@@ -28,10 +19,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
     dumb-init \
     libgl1 \
     libglib2.0-0 \
-    # --- 新增 DBus 依赖 ---
     dbus \
     dbus-x11 \
-    # --- 原有的 Chromium 依赖 ---
     libnss3 \
     libnspr4 \
     libatk1.0-0 \
@@ -52,23 +41,26 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
 
 RUN dbus-uuidgen > /var/lib/dbus/machine-id
 
-# 2. 设置工作目录
 WORKDIR /app
 
-# 3. 复制依赖并安装
+# 复制并解压 chromium
+COPY ungoogled-chromium-144.0.7559.132-1-x86_64_linux.tar.xz /tmp/chrome.tar.xz
+RUN mkdir -p /opt/ungoogled-chromium && \
+    tar -xJf /tmp/chrome.tar.xz -C /opt/ungoogled-chromium --strip-components=1 && \
+    rm /tmp/chrome.tar.xz
+
+# 依赖安装
 COPY requirements.txt .
-# 使用清华源,并额外指定 torch 的源(如果是 requirements.txt 里写了 index-url 则这里不需要 -f)
 RUN pip install --no-cache-dir -r requirements.txt
 
-# 4. 复制启动脚本 (处理 Xvfb 锁文件)
+# 启动脚本
 COPY entrypoint.sh /entrypoint.sh
 RUN chmod +x /entrypoint.sh
 
-# 5. 复制项目代码
+# 项目代码
 COPY . .
 
-# 6. 创建临时目录权限
+# 临时目录
 RUN mkdir -p /app/data/temp_browser_data && chmod 777 /app/data/temp_browser_data
 
-# 7. 入口点
 ENTRYPOINT ["/usr/bin/dumb-init", "--", "/entrypoint.sh"]

+ 19 - 13
config/config.json

@@ -75,7 +75,7 @@
         {
             "identifier": "vfs.sg.fr",
             "debug": false,
-            "enable": true,
+            "enable": false,
             "need_account": true,
             "need_proxy": true,
             "proxy_pool": "isp_all",
@@ -223,7 +223,7 @@
         {
             "identifier": "vfs.gb.it",
             "debug": false,
-            "enable": true,
+            "enable": false,
             "need_account": true,
             "need_proxy": true,
             "proxy_pool": "isp_all",
@@ -305,7 +305,7 @@
         {
             "identifier": "vfs.gb.nl",
             "debug": false,
-            "enable": true,
+            "enable": false,
             "need_account": true,
             "need_proxy": true,
             "proxy_pool": "isp_all",
@@ -387,7 +387,7 @@
         {
             "identifier": "vfs.gb.no",
             "debug": false,
-            "enable": true,
+            "enable": false,
             "need_account": true,
             "need_proxy": true,
             "proxy_pool": "isp_all",
@@ -519,7 +519,7 @@
         {
             "identifier": "vfs.ie.dk",
             "debug": false,
-            "enable": true,
+            "enable": false,
             "need_account": true,
             "need_proxy": true,
             "proxy_pool": "isp_all",
@@ -585,7 +585,7 @@
         {
             "identifier": "vfs.ie.fi",
             "debug": false,
-            "enable": true,
+            "enable": false,
             "need_account": true,
             "need_proxy": true,
             "proxy_pool": "isp_all",
@@ -717,7 +717,7 @@
         {
             "identifier": "vfs.ie.is",
             "debug": false,
-            "enable": true,
+            "enable": false,
             "need_account": true,
             "need_proxy": true,
             "proxy_pool": "isp_all",
@@ -852,7 +852,7 @@
             "enable": true,
             "need_account": true,
             "need_proxy": true,
-            "proxy_pool": "iproyal",
+            "proxy_pool": "isp_all",
             "proxy_cd": 5,
             "session_max_life": 60,
             "sentinel": {
@@ -1017,6 +1017,12 @@
                 "tls_url": "https://visas-fr.tlscontact.com/en-us/country/gb/vac/gbLON2fr",
                 "location": "London",
                 "capsolver_key": "CAP-5441DD341DD3CC2FAEF0BE6FE493EE9A",
+                "apt_config": {
+                    "code": "gbLON2fr",
+                    "country": "gb",
+                    "mission": "fr",
+                    "city": "London"
+                },
                 "interest_month": "06-2026",
                 "target_labels": [
                     "",
@@ -1027,8 +1033,8 @@
         {
             "identifier": "tls.cn.cng.fr",
             "debug": false,
-            "enable": true,
-            "need_account": true,
+            "enable": false,
+            "need_account": false,
             "need_proxy": true,
             "proxy_pool": "local",
             "proxy_cd": 5,
@@ -1087,7 +1093,7 @@
             "need_proxy": true,
             "proxy_pool": "isp_all",
             "proxy_cd": 5,
-            "session_max_life": 60,
+            "session_max_life": 15,
             "sentinel": {
                 "account_source": "",
                 "account_pool_id": "",
@@ -1136,7 +1142,7 @@
             "enable": false,
             "need_account": false,
             "need_proxy": true,
-            "proxy_pool": "local",
+            "proxy_pool": "isp_all",
             "proxy_cd": 5,
             "session_max_life": 10000000,
             "sentinel": {
@@ -1289,7 +1295,7 @@
             "need_proxy": true,
             "proxy_pool": "isp_all",
             "proxy_cd": 5,
-            "session_max_life": 120,
+            "session_max_life": 600,
             "sentinel": {
                 "account_source": "built-in",
                 "account_pool_id": "ie.gr.sentinel",

+ 0 - 192
config/proxies.json

@@ -1,101 +1,5 @@
 {
     "isp_all": [
-        {
-            "id": 100021,
-            "ip": "195.178.151.14",
-            "password": "919e0ee7ee",
-            "port": 12323,
-            "scheme": "http",
-            "username": "14a7fe11fea49"
-        },
-        {
-            "id": 100022,
-            "ip": "195.178.151.20",
-            "password": "919e0ee7ee",
-            "port": 12323,
-            "scheme": "http",
-            "username": "14a7fe11fea49"
-        },
-        {
-            "id": 100023,
-            "ip": "195.178.151.134",
-            "password": "919e0ee7ee",
-            "port": 12323,
-            "scheme": "http",
-            "username": "14a7fe11fea49"
-        },
-        {
-            "id": 100024,
-            "ip": "195.178.151.187",
-            "password": "919e0ee7ee",
-            "port": 12323,
-            "scheme": "http",
-            "username": "14a7fe11fea49"
-        },
-        {
-            "id": 100025,
-            "ip": "165.254.9.248",
-            "password": "919e0ee7ee",
-            "port": 12323,
-            "scheme": "http",
-            "username": "14a7fe11fea49"
-        },
-        {
-            "id": 100026,
-            "ip": "165.254.9.156",
-            "password": "919e0ee7ee",
-            "port": 12323,
-            "scheme": "http",
-            "username": "14a7fe11fea49"
-        },
-        {
-            "id": 100027,
-            "ip": "109.72.116.223",
-            "password": "919e0ee7ee",
-            "port": 12323,
-            "scheme": "http",
-            "username": "14a7fe11fea49"
-        },
-        {
-            "id": 100028,
-            "ip": "109.72.116.124",
-            "password": "919e0ee7ee",
-            "port": 12323,
-            "scheme": "http",
-            "username": "14a7fe11fea49"
-        },
-        {
-            "id": 100029,
-            "ip": "109.72.116.184",
-            "password": "919e0ee7ee",
-            "port": 12323,
-            "scheme": "http",
-            "username": "14a7fe11fea49"
-        },
-        {
-            "id": 100030,
-            "ip": "89.33.195.58",
-            "password": "919e0ee7ee",
-            "port": 12323,
-            "scheme": "http",
-            "username": "14a7fe11fea49"
-        },
-        {
-            "id": 100031,
-            "ip": "89.33.195.43",
-            "password": "919e0ee7ee",
-            "port": 12323,
-            "scheme": "http",
-            "username": "14a7fe11fea49"
-        },
-        {
-            "id": 100032,
-            "ip": "95.170.29.68",
-            "password": "919e0ee7ee",
-            "port": 12323,
-            "scheme": "http",
-            "username": "14a7fe11fea49"
-        },
         {
             "id": 110000,
             "ip": "95.135.130.10",
@@ -178,102 +82,6 @@
         }
     ],
     "iproyal": [
-        {
-            "id": 100021,
-            "ip": "195.178.151.14",
-            "password": "919e0ee7ee",
-            "port": 12323,
-            "scheme": "http",
-            "username": "14a7fe11fea49"
-        },
-        {
-            "id": 100022,
-            "ip": "195.178.151.20",
-            "password": "919e0ee7ee",
-            "port": 12323,
-            "scheme": "http",
-            "username": "14a7fe11fea49"
-        },
-        {
-            "id": 100023,
-            "ip": "195.178.151.134",
-            "password": "919e0ee7ee",
-            "port": 12323,
-            "scheme": "http",
-            "username": "14a7fe11fea49"
-        },
-        {
-            "id": 100024,
-            "ip": "195.178.151.187",
-            "password": "919e0ee7ee",
-            "port": 12323,
-            "scheme": "http",
-            "username": "14a7fe11fea49"
-        },
-        {
-            "id": 100025,
-            "ip": "165.254.9.248",
-            "password": "919e0ee7ee",
-            "port": 12323,
-            "scheme": "http",
-            "username": "14a7fe11fea49"
-        },
-        {
-            "id": 100026,
-            "ip": "165.254.9.156",
-            "password": "919e0ee7ee",
-            "port": 12323,
-            "scheme": "http",
-            "username": "14a7fe11fea49"
-        },
-        {
-            "id": 100027,
-            "ip": "109.72.116.223",
-            "password": "919e0ee7ee",
-            "port": 12323,
-            "scheme": "http",
-            "username": "14a7fe11fea49"
-        },
-        {
-            "id": 100028,
-            "ip": "109.72.116.124",
-            "password": "919e0ee7ee",
-            "port": 12323,
-            "scheme": "http",
-            "username": "14a7fe11fea49"
-        },
-        {
-            "id": 100029,
-            "ip": "109.72.116.184",
-            "password": "919e0ee7ee",
-            "port": 12323,
-            "scheme": "http",
-            "username": "14a7fe11fea49"
-        },
-        {
-            "id": 100030,
-            "ip": "89.33.195.58",
-            "password": "919e0ee7ee",
-            "port": 12323,
-            "scheme": "http",
-            "username": "14a7fe11fea49"
-        },
-        {
-            "id": 100031,
-            "ip": "89.33.195.43",
-            "password": "919e0ee7ee",
-            "port": 12323,
-            "scheme": "http",
-            "username": "14a7fe11fea49"
-        },
-        {
-            "id": 100032,
-            "ip": "95.170.29.68",
-            "password": "919e0ee7ee",
-            "port": 12323,
-            "scheme": "http",
-            "username": "14a7fe11fea49"
-        }
     ],
     "proxy-cheap": [
         {

+ 1 - 10
data/proxy_states.json

@@ -1,12 +1,3 @@
 {
-    "isp_all::110000": 1775421010.5651941,
-    "isp_all::110005": 1775421010.5735455,
-    "isp_all::100032": 1775421010.6466098,
-    "isp_all::100030": 1775421010.705492,
-    "isp_all::100028": 1775421010.7220783,
-    "isp_all::100031": 1775421010.7356896,
-    "isp_all::110001": 1775421010.7431846,
-    "isp_all::110002": 1775421010.7575579,
-    "isp_all::110009": 1775421010.7711172,
-    "isp_all::110006": 1775421010.8721054
+    "isp_all::110009": 1777038464.6069372
 }

+ 44 - 44
docker-compose.yml

@@ -1,34 +1,11 @@
 version: '3.8'
 
 services:
-  # visa-sentinel:
-  #   build: .
-  #   image: coordinator:latest
-  #   container_name: coordinator-sentinel
-  #   command: ["python3", "main_sentinel.py"]
-  #   restart: unless-stopped
-  #   shm_size: '2gb'
-  #   volumes:
-  #     - ./config:/app/config
-  #     - ./logs:/app/logs
-  #     - ./data:/app/data
-  #     - ./plugins:/app/plugins
-  #   environment:
-  #     - TZ=Asia/Shanghai
-  #     - DISPLAY=:99
-  #     - CHROME_BIN=/usr/bin/chromium
-  #   # 资源限制
-  #   deploy:
-  #     resources:
-  #       limits:
-  #         cpus: '2.0'
-  #         memory: 4G
-
-  visa-booker:
+  visa-sentinel:
     build: .
     image: coordinator:latest
-    container_name: coordinator-booker
-    command: ["python3", "main_booker.py"]
+    container_name: coordinator-sentinel
+    command: ["python3", "main_sentinel.py"]
     restart: unless-stopped
     shm_size: '2gb'
     volumes:
@@ -39,7 +16,7 @@ services:
     environment:
       - TZ=Asia/Shanghai
       - DISPLAY=:99
-      - CHROME_BIN=/usr/bin/chromium
+      - CHROME_BIN=/opt/ungoogled-chromium/chrome
     # 资源限制
     deploy:
       resources:
@@ -47,20 +24,43 @@ services:
           cpus: '2.0'
           memory: 4G
 
-  visa-sweeper:
-    build: .
-    image: coordinator:latest
-    container_name: coordinator-sweeper
-    command: ["python3", "main_sweeper.py"]
-    restart: unless-stopped
-    volumes:
-      - ./config:/app/config
-      - ./logs:/app/logs
-    environment:
-      - TZ=Asia/Shanghai
-    # 资源限制极低,因为它只是个网络请求脚本,不运行浏览器
-    deploy:
-      resources:
-        limits:
-          cpus: '0.2'
-          memory: 256M
+  # visa-booker:
+  #   build: .
+  #   image: coordinator:latest
+  #   container_name: coordinator-booker
+  #   command: ["python3", "main_booker.py"]
+  #   restart: unless-stopped
+  #   shm_size: '2gb'
+  #   volumes:
+  #     - ./config:/app/config
+  #     - ./logs:/app/logs
+  #     - ./data:/app/data
+  #     - ./plugins:/app/plugins
+  #   environment:
+  #     - TZ=Asia/Shanghai
+  #     - DISPLAY=:99
+  #     - CHROME_BIN=/opt/ungoogled-chromium/chrome
+  #   # 资源限制
+  #   deploy:
+  #     resources:
+  #       limits:
+  #         cpus: '2.0'
+  #         memory: 4G
+
+  # visa-sweeper:
+  #   build: .
+  #   image: coordinator:latest
+  #   container_name: coordinator-sweeper
+  #   command: ["python3", "main_sweeper.py"]
+  #   restart: unless-stopped
+  #   volumes:
+  #     - ./config:/app/config
+  #     - ./logs:/app/logs
+  #   environment:
+  #     - TZ=Asia/Shanghai
+  #   # 资源限制极低,因为它只是个网络请求脚本,不运行浏览器
+  #   deploy:
+  #     resources:
+  #       limits:
+  #         cpus: '0.2'
+  #         memory: 256M

+ 69 - 37
plugins/ita_plugin.py

@@ -1,6 +1,7 @@
 import time
 import json
 import random
+import socket
 import uuid
 import shutil
 import re
@@ -18,7 +19,9 @@ from vs_types import VSPlgConfig, AppointmentType, VSQueryResult, VSBookResult,
 from toolkit.proxy_tunnel import ProxyTunnel
 from toolkit.vs_cloud_api import VSCloudApi
 from utils.mouse import HumanMouse
-from utils.scroll import HumanScroll
+from utils.keyboard import HumanKeyboard
+from utils.fingerprint_utils import FingerprintGenerator
+
 
 class BrowserResponse:
     def __init__(self, result_dict):
@@ -116,13 +119,6 @@ class ItaPlugin(IVSPlg):
         """
         self._log(f"Initializing Session (ID: {self.instance_id})...")
         co = ChromiumOptions()
-        # -------------------------------------------------------------
-        # [核心修复] 解决 'not enough values to unpack'
-        # -------------------------------------------------------------
-        # 1. 不要用 co.auto_port(),因为它依赖解析 stdout,会被 DBus 报错干扰
-        # 2. 我们手动随机生成一个端口
-        import random
-        import socket
         
         def get_free_port():
             with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
@@ -131,80 +127,116 @@ class ItaPlugin(IVSPlg):
         
         debug_port = get_free_port()
         self._log(f"Assigned Debug Port: {debug_port}")
-        
-        # --- [关键配置] 设置独立的用户数据目录 ---
-        # 这样每个实例的 Cache, Cookies, LocalStorage 都是完全隔离的
-        # 同时也防止了多进程争抢同一个 Default 文件夹导致的崩溃
         co.set_user_data_path(self.user_data_path)
         
-        # --- 1. 指定浏览器路径 (适配 Docker) ---
         chrome_path = os.getenv("CHROME_BIN")
         if chrome_path and os.path.exists(chrome_path):
             co.set_paths(browser_path=chrome_path)
         
-        # --- [核心修改] 代理配置 ---
         if self.config.proxy and self.config.proxy.ip:
             p = self.config.proxy
             
             if p.username and p.password:
                 self._log(f"Starting Proxy Tunnel for {p.ip}...")
                 
-                # 1. 启动本地隧道
                 self.tunnel = ProxyTunnel(p.ip, p.port, p.username, p.password)
                 local_proxy = self.tunnel.start()
                 
                 self._log(f"Tunnel started at {local_proxy}")
-                
-                # 2. Chrome 连接本地免密端口
-                # 必须使用 --proxy-server 强制指定,绝对稳健
                 co.set_argument(f'--proxy-server={local_proxy}')
                 
             else:
-                # 无密码代理,直接用
                 proxy_str = f"{p.scheme}://{p.ip}:{p.port}"
                 co.set_argument(f'--proxy-server={proxy_str}')
         else:
             self._log("[WARN] No proxy configured!")
 
+        fingerprint_gen = FingerprintGenerator()
+        specific_fp = fingerprint_gen.generate(self.config.account.username)
+        self._log(f'browser fingerprint={specific_fp}')
+
         co.headless(False) 
         co.set_argument('--no-sandbox')
-        co.set_argument('--disable-gpu')
+        # co.set_argument('--disable-gpu')
         co.set_argument('--disable-dev-shm-usage')
         co.set_argument('--window-size=1920,1080')
         co.set_argument('--disable-blink-features=AutomationControlled')
-
+        co.set_argument('--ignore-gpu-blocklist') # 忽略无显卡黑名单
+        co.set_argument('--enable-webgl')         # 强制开启 WebGL
+        co.set_argument('--use-gl=angle')         # 使用 ANGLE 渲染后端
+        co.set_argument('--use-angle=swiftshader')# 强制使用 CPU 进行 3D 渲染 (这步最关键!)
+        co.set_argument(f"--fingerprint={specific_fp.get('seed')}")
+        co.set_argument(f"--fingerprint-platform={specific_fp.get('platform')}")
+        co.set_argument(f"--fingerprint-brand={specific_fp.get('brand')}")
         try:
             self.page = ChromiumPage(co)
+            if self.config.debug:
+                self.page.get('https://example.com')
+                js_script = """
+                function getFingerprint() {
+                    let webglVendor = 'Unknown';
+                    let webglRenderer = 'Unknown';
+                    try {
+                        let canvas = document.createElement('canvas');
+                        let gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
+                        if (gl) {
+                            let debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
+                            if (debugInfo) {
+                                webglVendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);
+                                webglRenderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
+                            }
+                        }
+                    } catch(e) {}
+
+                    return {
+                        "User-Agent": navigator.userAgent,
+                        "Platform": navigator.userAgentData ? navigator.userAgentData.platform : navigator.platform,
+                        "Brands": navigator.userAgentData ? navigator.userAgentData.brands.map(b => b.brand).join(', ') : 'Not Supported',
+                        "CPU Cores": navigator.hardwareConcurrency,
+                        "Language": navigator.language,
+                        "Timezone": Intl.DateTimeFormat().resolvedOptions().timeZone,
+                        "WebGL Vendor": webglVendor,
+                        "WebGL Renderer": webglRenderer
+                    };
+                }
+                return getFingerprint();
+                """
+
+                fp_data = self.page.run_js(js_script)
+                self._log("================ 预检浏览器指纹数据 ================")
+                self._log(json.dumps(fp_data, indent=4, ensure_ascii=False))
+                self._log("====================================================")
             
             login_url = f"{self._host}/Home"
             self._log(f"Navigating to {login_url}")
             self.page.get(login_url)
             
+            self._log("Init humanize tools...")
+            self.mouse = HumanMouse(self.page, debug=True)
+            self.keyboard = HumanKeyboard(self.page)
+            self._log("Random mouse start position...")
+            viewport_width = self.page.rect.viewport_size[0]
+            viewport_height = self.page.rect.viewport_size[1]
+            init_x = random.randint(10, viewport_width - 10)
+            init_y = random.randint(10, viewport_height - 10)
+            self.mouse.move(init_x, init_y) 
+            
             # 等待登录框
             if not self.page.wait.ele_displayed('#login-email', timeout=20):
                 raise BizLogicError("Login page not loaded")
 
             # 填充用户名密码
-            self.page.ele('#login-email').input(self.config.account.username)
-            self.page.ele('#login-password').input(self.config.account.password)
+            self.mouse.human_click_ele(self.page.ele('#login-email'))
+            self.keyboard.type_text(self.config.account.username)
+            
+            self.mouse.human_click_ele(self.page.ele('#login-password'))
+            self.keyboard.type_text(self.config.account.password)
             
-            # 4. [核心修改] 解决 ReCaptcha V3 Enterprise 并注入
-            # Prenotami 使用的是 Enterprise V3, Action = 'LOGIN'
-            self._solve_and_inject_prenotami_captcha()
-            human_mouse = HumanMouse(self.page, debug=True)
-            human_scroll = HumanScroll(self.page)
             # 先定位
             self._log("Locating Login button...")
-            login_btn = self.page.ele('@id=captcha-trigger')
-
-            self._log("Scrolling to make button visible...")
-            human_scroll.scroll_to_element(login_btn, humanize=True)   
+            login_btn = self.page.ele('#captcha-trigger')
             
-            self._log("Moving mouse to Login button...")
-            human_mouse.move_to(login_btn, duration=random.uniform(0.6, 1.0))
-            time.sleep(random.uniform(0.3, 0.5))
-            self._log("Clicking Login button...")
-            login_btn.click()
+            self.mouse.human_click_ele(login_btn)
             self._log("Login button clicked.")
             
             # 等待 URL 变化或特定元素出现

+ 14 - 4
plugins/pol_plugin.py

@@ -20,7 +20,7 @@ from vs_plg import IVSPlg
 from vs_types import VSPlgConfig, AppointmentType, VSQueryResult, VSBookResult, AvailabilityStatus, TimeSlot, DateAvailability, NotFoundError, PermissionDeniedError, RateLimiteddError, SessionExpiredOrInvalidError, BizLogicError 
 from toolkit.vs_cloud_api import VSCloudApi
 from toolkit.proxy_tunnel import ProxyTunnel
-
+from utils.fingerprint_utils import FingerprintGenerator
 
 class BrowserResponse:
     def __init__(self, result_dict):
@@ -48,7 +48,7 @@ def get_alias_email(email: str, new_domain: str = "gmail-app.com") -> str:
 
 class PolPlugin(IVSPlg):
     """
-    Germany (Visametric) 签证预约插件 (Browser + Tunnel Mode)
+    Poland (e-konsulat) 签证预约插件 (Browser + Tunnel Mode)
     """
 
     def __init__(self, group_id: str):
@@ -139,14 +139,24 @@ class PolPlugin(IVSPlg):
         else:
             self._log("[WARN] No proxy configured!")
 
+        fingerprint_gen = FingerprintGenerator()
+        specific_fp = fingerprint_gen.generate(self.config.account.username)
+        self._log(f'browser fingerprint={specific_fp}')
+        
         co.headless(False)
         co.set_argument('--no-sandbox')
-        co.set_argument('--disable-gpu')
+        # co.set_argument('--disable-gpu')
         co.set_argument('--disable-dev-shm-usage') 
         co.set_argument('--window-size=1920,1080')
         co.set_argument('--disable-blink-features=AutomationControlled')
         co.set_argument('--ignore-certificate-errors')
-
+        co.set_argument('--ignore-gpu-blocklist') # 忽略无显卡黑名单
+        co.set_argument('--enable-webgl')         # 强制开启 WebGL
+        co.set_argument('--use-gl=angle')         # 使用 ANGLE 渲染后端
+        co.set_argument('--use-angle=swiftshader')# 强制使用 CPU 进行 3D 渲染 (这步最关键!)
+        co.set_argument(f"--fingerprint={specific_fp.get('seed')}")
+        co.set_argument(f"--fingerprint-platform={specific_fp.get('platform')}")
+        co.set_argument(f"--fingerprint-brand={specific_fp.get('brand')}")
         try:
             self.page = ChromiumPage(co)
             url_home = "https://secure.e-konsulat.gov.pl"

+ 47 - 3
plugins/tls_plugin.py

@@ -19,6 +19,7 @@ from utils.cloudflare_bypass_for_scraping import CloudflareBypasser
 from toolkit.proxy_tunnel import ProxyTunnel
 from utils.mouse import HumanMouse
 from utils.keyboard import HumanKeyboard
+from utils.fingerprint_utils import FingerprintGenerator
 
 
 class BrowserResponse:
@@ -162,16 +163,59 @@ class TlsPlugin(IVSPlg):
                 co.set_argument(f'--proxy-server={proxy_str}')
         else:
             self._log("[WARN] No proxy configured!")
+            
+        fingerprint_gen = FingerprintGenerator()
+        specific_fp = fingerprint_gen.generate(self.config.account.username)
+        self._log(f'browser fingerprint={specific_fp}')
 
         co.headless(False)
         co.set_argument('--no-sandbox')
-        co.set_argument('--disable-gpu')
+        # co.set_argument('--disable-gpu')
         co.set_argument('--disable-dev-shm-usage')
         co.set_argument('--window-size=1920,1080')
         co.set_argument('--disable-blink-features=AutomationControlled')
-
+        co.set_argument(f"--fingerprint={specific_fp.get('seed')}")
+        co.set_argument(f"--fingerprint-platform={specific_fp.get('platform')}")
+        co.set_argument(f"--fingerprint-brand={specific_fp.get('brand')}")
         try:
             self.page = ChromiumPage(co)
+            if self.config.debug:
+                self.page.get('https://example.com')
+                js_script = """
+                function getFingerprint() {
+                    let webglVendor = 'Unknown';
+                    let webglRenderer = 'Unknown';
+                    try {
+                        let canvas = document.createElement('canvas');
+                        let gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
+                        if (gl) {
+                            let debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
+                            if (debugInfo) {
+                                webglVendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);
+                                webglRenderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
+                            }
+                        }
+                    } catch(e) {}
+
+                    return {
+                        "User-Agent": navigator.userAgent,
+                        "Platform": navigator.userAgentData ? navigator.userAgentData.platform : navigator.platform,
+                        "Brands": navigator.userAgentData ? navigator.userAgentData.brands.map(b => b.brand).join(', ') : 'Not Supported',
+                        "CPU Cores": navigator.hardwareConcurrency,
+                        "Language": navigator.language,
+                        "Timezone": Intl.DateTimeFormat().resolvedOptions().timeZone,
+                        "WebGL Vendor": webglVendor,
+                        "WebGL Renderer": webglRenderer
+                    };
+                }
+                return getFingerprint();
+                """
+
+                fp_data = self.page.run_js(js_script)
+                self._log("================ 预检浏览器指纹数据 ================")
+                self._log(json.dumps(fp_data, indent=4, ensure_ascii=False))
+                self._log("====================================================")
+
             tls_url = self.free_config.get('tls_url', '')
             self._log(f"Navigating: {tls_url}")
             self.page.get(tls_url)
@@ -298,7 +342,7 @@ class TlsPlugin(IVSPlg):
             #     "applicants-information" in self.page.url
             # ]
             # if any(no_applicant_indicators):
-            #     raise BizLogicError(message=f"No applicant added")
+            #     raise BizLogicError(message=f"No applicant added") 
             
             btn_selector = '#book-appointment-btn'
             self._log(f"Waiting for selector={btn_selector} to render...")

+ 50 - 4
plugins/vfs_plugin.py

@@ -25,6 +25,7 @@ from vs_types import VSPlgConfig, AppointmentType, VSQueryResult, VSBookResult,
 from toolkit.vs_cloud_api import VSCloudApi
 from toolkit.proxy_tunnel import ProxyTunnel
 from utils.cloudflare_bypass_for_scraping import CloudflareBypasser
+from utils.fingerprint_utils import FingerprintGenerator
 
 
 VFS_PUBLIC_KEY_PEM = """-----BEGIN PUBLIC KEY-----
@@ -205,8 +206,6 @@ class VfsPlugin(IVSPlg):
         """
         self._log(f"Initializing Session (ID: {self.instance_id})...")
         co = ChromiumOptions()
-        import random
-        import socket
         
         def get_free_port():
             with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
@@ -239,16 +238,63 @@ class VfsPlugin(IVSPlg):
         else:
             self._log("[WARN] No proxy configured!")
             
+        fingerprint_gen = FingerprintGenerator()
+        specific_fp = fingerprint_gen.generate(self.config.account.username)
+        self._log(f'browser fingerprint={specific_fp}')
+            
         co.headless(False) 
         co.set_argument('--no-sandbox')
-        co.set_argument('--disable-gpu')
+        # co.set_argument('--disable-gpu')
         co.set_argument('--disable-dev-shm-usage') 
         co.set_argument('--window-size=1920,1080')
         co.set_argument('--disable-blink-features=AutomationControlled')
-
+        co.set_argument('--ignore-gpu-blocklist') # 忽略无显卡黑名单
+        co.set_argument('--enable-webgl')         # 强制开启 WebGL
+        co.set_argument('--use-gl=angle')         # 使用 ANGLE 渲染后端
+        co.set_argument('--use-angle=swiftshader')# 强制使用 CPU 进行 3D 渲染 (这步最关键!)
+        co.set_argument(f"--fingerprint={specific_fp.get('seed')}")
+        co.set_argument(f"--fingerprint-platform={specific_fp.get('platform')}")
+        co.set_argument(f"--fingerprint-brand={specific_fp.get('brand')}")
         try:
             self.page = ChromiumPage(co)
             
+            if self.config.debug:
+                self.page.get('https://example.com')
+                js_script = """
+                function getFingerprint() {
+                    let webglVendor = 'Unknown';
+                    let webglRenderer = 'Unknown';
+                    try {
+                        let canvas = document.createElement('canvas');
+                        let gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
+                        if (gl) {
+                            let debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
+                            if (debugInfo) {
+                                webglVendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);
+                                webglRenderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
+                            }
+                        }
+                    } catch(e) {}
+
+                    return {
+                        "User-Agent": navigator.userAgent,
+                        "Platform": navigator.userAgentData ? navigator.userAgentData.platform : navigator.platform,
+                        "Brands": navigator.userAgentData ? navigator.userAgentData.brands.map(b => b.brand).join(', ') : 'Not Supported',
+                        "CPU Cores": navigator.hardwareConcurrency,
+                        "Language": navigator.language,
+                        "Timezone": Intl.DateTimeFormat().resolvedOptions().timeZone,
+                        "WebGL Vendor": webglVendor,
+                        "WebGL Renderer": webglRenderer
+                    };
+                }
+                return getFingerprint();
+                """
+
+                fp_data = self.page.run_js(js_script)
+                self._log("================ 预检浏览器指纹数据 ================")
+                self._log(json.dumps(fp_data, indent=4, ensure_ascii=False))
+                self._log("====================================================")
+            
             mission = self.free_config.get("mission_code", "")
             country = self.free_config.get("country_code", "")
             lang = self.free_config.get("language", "en")

+ 80 - 0
utils/fingerprint_utils.py

@@ -0,0 +1,80 @@
+import hashlib
+from typing import Dict
+
+class FingerprintGenerator:
+    def __init__(self):
+        # 按现实世界市场份额设计的权重分配(数组长度为100,代表百分比)
+        self.platforms = (
+            ['windows'] * 70 +  # 70% 概率生成 Windows
+            ['macos'] * 25 +    # 25% 概率生成 macOS
+            ['linux'] * 5       # 5%  概率生成 Linux
+        )
+        
+        self.brands = (
+            ['Chrome'] * 70 +   # 70% 概率生成 Chrome
+            ['Edge'] * 20 +     # 20% 概率生成 Edge
+            ['Opera'] * 5 +     # 5%  概率生成 Opera
+            ['Vivaldi'] * 5     # 5%  概率生成 Vivaldi
+        )
+
+    def generate(self, username: str) -> Dict:
+        """
+        根据用户名生成固定的指纹配置
+        """
+        md5_hash = hashlib.md5(username.encode('utf-8')).hexdigest()
+        seed = int(md5_hash[:8], 16) & 0x7FFFFFFF
+        platform_index = int(md5_hash[8:12], 16) % len(self.platforms)
+        platform = self.platforms[platform_index]
+        brand_index = int(md5_hash[12:16], 16) % len(self.brands)
+        brand = self.brands[brand_index]
+        
+        return {
+            "username": username,
+            "seed": seed,
+            "platform": platform,
+            "brand": brand
+        }
+
+    def get_cli_args(self, username: str) -> list:
+        """
+        直接生成用于启动浏览器的命令行参数列表
+        """
+        fp = self.generate(username)
+        return [
+            f"--fingerprint={fp['seed']}",
+            f"--fingerprint-platform={fp['platform']}",
+            f"--fingerprint-brand={fp['brand']}"
+        ]
+
+# ================= 测试代码 =================
+if __name__ == "__main__":
+    fp_tool = FingerprintGenerator()
+
+    # 测试用户 A
+    user_a = fp_tool.generate("alice_2024")
+    print(f"User: alice_2024 -> Seed: {user_a['seed']}, OS: {user_a['platform']}, Browser: {user_a['brand']}")
+
+    # 测试用户 B
+    user_b = fp_tool.generate("bob_crypto")
+    print(f"User: bob_crypto -> Seed: {user_b['seed']}, OS: {user_b['platform']}, Browser: {user_b['brand']}")
+    
+    # 测试用户 C
+    user_c = fp_tool.generate("jerry_crypto")
+    print(f"User: bob_crypto -> Seed: {user_c['seed']}, OS: {user_c['platform']}, Browser: {user_c['brand']}")
+    
+    # 测试用户 D
+    user_d = fp_tool.generate("luna_crypto")
+    print(f"User: bob_crypto -> Seed: {user_d['seed']}, OS: {user_d['platform']}, Browser: {user_d['brand']}")
+    
+    # 测试用户 E
+    user_e = fp_tool.generate("jone_crypto")
+    print(f"User: bob_crypto -> Seed: {user_e['seed']}, OS: {user_e['platform']}, Browser: {user_e['brand']}")
+    
+    # 测试用户 F
+    user_f = fp_tool.generate("tom_crypto")
+    print(f"User: bob_crypto -> Seed: {user_f['seed']}, OS: {user_f['platform']}, Browser: {user_f['brand']}")
+    
+    # 模拟获取命令行参数
+    args = fp_tool.get_cli_args("alice_2024")
+    print("\n生成的命令行参数:")
+    print(" ".join(args))

+ 14 - 5
utils/mouse.py

@@ -169,13 +169,22 @@ class HumanMouse:
         
     def human_click_ele(self, element):
         """
-        拟人化点击元素。
-        
+        拟人化点击元素 (修复版)
         :param element: DrissionPage 的元素对象
-        :param button: 鼠标按键
-        :param click_count: 点击次数 (默认 1 次)
         """
-        mid_x, mid_y = element.rect.midpoint
+        # 1. 强制滚动,把元素尽量移到屏幕正中间,防止被顶部导航栏或底部悬浮窗遮盖
+        element.scroll.to_see(center=True)
+        time.sleep(0.5)  # 等待滚动动画完成,防止移动时坐标还在变化
+        
+        # 2. 【核心修复】获取相对于当前屏幕视口的坐标,而不是页面绝对坐标
+        # 注意:这里使用的是 viewport_midpoint
+        mid_x, mid_y = element.rect.viewport_midpoint 
+        
+        # 3. 如果元素太靠近边缘,可能会获取失败,做一个基础保护
+        if mid_x is None or mid_y is None:
+            logger.warning("Failed to get viewport midpoint, falling back to absolute midpoint.")
+            mid_x, mid_y = element.rect.midpoint
+            
         self.click(mid_x, mid_y, humanize=True)
 
     def click(