#!/usr/bin/env python3 """离线网页验证码/验证类型分类器。 脚本接收 HTML、文本、URL 和截图元信息;不会发送请求、打开浏览器、 提交答案或调用任何打码/solver 服务。 """ from __future__ import annotations import argparse import html as html_lib import json import math import re import sys from dataclasses import dataclass from pathlib import Path from typing import Any, Iterable def configure_utf8_stdio() -> None: for stream in (sys.stdout, sys.stderr): if hasattr(stream, "reconfigure"): stream.reconfigure(encoding="utf-8") configure_utf8_stdio() @dataclass(frozen=True) class Signal: label: str pattern: str weight: float = 1.0 @dataclass(frozen=True) class Match: label: str weight: float evidence: str source: str @dataclass(frozen=True) class EvidenceSource: kind: str text: str SOURCE_WEIGHTS = { "url": 1.8, "text": 1.45, "screenshot_meta": 1.45, "html": 0.75, } HTML_FOCUS_KEYWORDS = re.compile( r"captcha|验证码|geetest|hcaptcha|recaptcha|turnstile|cf-turnstile|g-recaptcha|h-captcha|" r"yidun|NECaptcha|dun\.163|TCaptcha|TencentCaptcha|gtimg|AWSC|NoCaptcha|nc_|" r"TJCaptcha|turing\.captcha|aliyun|aws-waf|awswaf|DataDome|arkoselabs|FunCaptcha|" r"shumei|ishumei|fengkongcloud|initSMCaptcha|SMCaptcha|dingxiang|dx-captcha|_dx|" r"baidu|bdstatic|bce\.baidu|jdcloud|Jcap|yunpian|riddler-sdk|huawei|HuaWei|OneAccess|" r"tongdun|fraudmetrix|yandex|smart-captcha|CaptchaFox|procaptcha|prosopo|trustcaptcha|" r"privatecaptcha|cap-widget|@cap\.js|mcaptcha|IconCaptcha|BotDetect|BDC_|securimage|" r"visualcaptcha|opfcaptcha|validateCaptcha|amzn-captcha|CyberSiARA|siara|akamai|_abck|bm_sz|imperva|incapsula|reese84|" r"perimeterx|px-captcha|_px3|kasada|kpsdk|x-kpsdk|netacea|radware|f5|TSPD|" r"AJ-Captcha|aj-captcha|blockPuzzle|clickWord|tianai|TAC|happy-captcha|easy-captcha|" r"mtcaptcha|friendlycaptcha|slider|slide|jigsaw|picture-click|click|rotate|grid|" r"audio|voice|one-click|checkbox|press|hold|invisible|frictionless|risk[- ]?score|" r"proof-of-work|pow|hashcash|drag|drop|trace|draw|scratch|restore|unscramble|area|" r"difference|spot[- ]the[- ]difference|font|dice|game|3d|paged|multi[- ]step|logic|qa|" r"liveness|biometric|face|" r"语音|一键|一点即过|无感|无痕|评分|拖放|轨迹|绘制|连线|刮刮卡|还原|复原|乱序|" r"面积|区域|框选|差异|找茬|字体|空间语义|语义|小游戏|多轮|分页|问答|逻辑|活体|人脸|" r"sitekey|challenge", re.IGNORECASE, ) PROVIDER_SIGNALS: dict[str, list[Signal]] = { "recaptcha": [ Signal("命中 Google reCAPTCHA 脚本", r"(?:google\.com|recaptcha\.net)/recaptcha", 3), Signal("命中 g-recaptcha 标记", r"\bg-recaptcha\b", 2.5), Signal("命中 grecaptcha API", r"\bgrecaptcha\b", 2), Signal("命中 reCAPTCHA api2 iframe", r"/api2/(?:anchor|reload|bframe)", 2.5), Signal("命中 reCAPTCHA Enterprise", r"recaptcha/(?:enterprise|enterprise\.js)", 2.5), Signal("命中 g-recaptcha-response 字段", r"g-recaptcha-response", 2), ], "hcaptcha": [ Signal("命中 hCaptcha 脚本", r"hcaptcha\.com/(?:1/)?api\.js|newassets\.hcaptcha\.com", 3), Signal("命中 h-captcha 标记", r"\bh-captcha\b", 2.5), Signal("命中 hcaptcha API", r"\bhcaptcha\b", 2), Signal("命中 hCaptcha get/check 接口", r"/(?:getcaptcha|checkcaptcha)/", 2), Signal("命中 h-captcha-response 字段", r"h-captcha-response", 2), Signal("命中 hCaptcha rqdata/rqtoken", r"\brq(?:data|token)\b", 1.5), ], "cloudflare-turnstile": [ Signal("命中 Turnstile 脚本", r"challenges\.cloudflare\.com/turnstile", 3), Signal("命中 cf-turnstile 标记", r"\bcf-turnstile\b", 3), Signal("命中 turnstile API", r"\bturnstile\.(?:render|execute|reset)\b", 2), Signal("命中 cf-turnstile-response 字段", r"cf-turnstile-response", 2.5), Signal("命中 Turnstile cdata/action", r"\b(?:cdata|data-cdata|data-action)\b", 1), ], "geetest": [ Signal("命中极验关键词", r"\bgeetest\b|极验", 2.5), Signal("命中极验 gt/challenge", r"\bgt\s*[:=]|challenge\s*[:=]", 1.5), Signal("命中极验 captcha_id", r"\bcaptcha_id\b", 2), Signal("命中极验 v4 lot/pass/gen 参数", r"\b(?:lot_number|pass_token|gen_time)\b", 2), Signal("命中极验 w 载荷", r"(?]+(?:captcha|verify|code)|(?:captcha|verify|code)[-_]?(?:img|image|url)", 1.5), ], } TILE_SCRAMBLE_SIGNALS = [ Signal("命中切片乱序中文文案", r"切片乱序|分块乱序|图片分割|瓦片重排|分割顺序打乱|乱序图片还原|图片被切成多块|切成多块顺序打乱", 3), Signal("命中切片乱序英文文案", r"\b(?:scrambled tiles|tile scramble|scrambled image tiles|image tile restore|reorder tiles|slice order|piece order)\b", 3), Signal("命中切片顺序字段", r"\b(?:tileOrder|pieceOrder|tile_order|piece_order|restoreOrder|sliceOrder)\b", 2.5), Signal("命中 CSS sprite/背景定位切片", r"background-position|background-size|sprite", 2), Signal("命中 Canvas drawImage 切片绘制", r"\bdrawImage\s*\(", 2), Signal("命中 shuffle/scramble 切片逻辑", r"\b(?:shuffle|scramble|unscramble)\b.{0,100}\b(?:tile|piece|slice|image|captcha)\b|\b(?:tile|piece|slice|image|captcha)\b.{0,100}\b(?:shuffle|scramble|unscramble)\b", 1.8), ] PLAYBOOKS = { "audio": "references/solution-playbooks.md#audio", "drag-drop": "references/solution-playbooks.md#drag-drop", "trace-draw": "references/solution-playbooks.md#trace-draw", "scratch": "references/solution-playbooks.md#scratch", "image-restore": "references/solution-playbooks.md#image-restore", "area-select": "references/solution-playbooks.md#area-select", "difference-click": "references/solution-playbooks.md#difference-click", "font-identify": "references/solution-playbooks.md#font-identify", "semantic-reasoning": "references/solution-playbooks.md#semantic-reasoning", "game-challenge": "references/solution-playbooks.md#game-challenge", "pow-challenge": "references/solution-playbooks.md#pow-challenge", "risk-score": "references/solution-playbooks.md#risk-score", "one-click": "references/solution-playbooks.md#one-click", "multi-step": "references/solution-playbooks.md#multi-step", "qa-logic": "references/solution-playbooks.md#qa-logic", "biometric-liveness": "references/solution-playbooks.md#biometric-liveness", "text": "references/solution-playbooks.md#text", "math": "references/solution-playbooks.md#math", "slider": "references/solution-playbooks.md#slider", "click-select": "references/solution-playbooks.md#click-select", "rotate": "references/solution-playbooks.md#rotate", "grid": "references/solution-playbooks.md#grid", "token-widget": "references/solution-playbooks.md#token-widget", "waf-challenge": "references/solution-playbooks.md#waf-challenge", "unknown-custom": "references/solution-playbooks.md#unknown-custom", } NEXT_EVIDENCE = { "audio": ["音频文件或播放接口", "题面文本", "答案格式", "是否有视觉替代方案"], "drag-drop": ["完整挑战截图", "被拖动元素和目标区域边界", "坐标系", "释放判定接口"], "trace-draw": ["轨迹图/题面截图", "目标路径坐标", "画布尺寸", "轨迹采样格式"], "scratch": ["刮刮卡区域截图", "刮开前后状态", "画布尺寸", "通过阈值"], "image-restore": ["乱序/待还原图片", "分块边界", "目标排列规则", "tileOrder/pieceOrder 或 CSS/canvas 切片线索", "拖动或滑动映射"], "area-select": ["完整题面截图", "待选择区域", "元素边界", "坐标原点"], "difference-click": ["两张对比图或完整截图", "差异点坐标", "题面", "点击次数要求"], "font-identify": ["题面文本", "候选文字截图", "字体目标规则", "点击顺序"], "semantic-reasoning": ["完整题面", "截图", "目标关系描述", "坐标系"], "game-challenge": ["小游戏截图/帧", "题面", "交互控件", "厂商 public key 或 challenge id"], "pow-challenge": ["组件脚本 URL", "challenge/payload", "difficulty/nonce 字段", "服务端校验接口"], "risk-score": ["脚本/iframe URL", "sitekey/action", "score/阈值文档", "服务端校验结果"], "one-click": ["组件 HTML", "按钮/checkbox 截图", "callback 名", "是否存在二次挑战"], "multi-step": ["每一轮题面截图", "轮次状态", "上一轮结果", "最终成功标记"], "qa-logic": ["问题原文", "答案格式", "语言/知识范围", "是否为动态题库"], "biometric-liveness": ["SDK/厂商标识", "活体动作要求", "授权和隐私边界", "人工复核流程"], "text": ["验证码图片或元素截图", "期望字符集/长度", "刷新接口或刷新行为"], "math": ["表达式图片/文本", "运算符集合", "服务端接受的答案格式"], "slider": ["背景图", "滑块/拼图块图片", "轨道宽度", "厂商 verify 接口名"], "click-select": ["题面文本", "完整挑战截图", "元素边界框", "目标点击顺序"], "rotate": ["挑战图片", "轨道宽度", "角度范围", "已知校准样本"], "grid": ["题面文本", "完整网格截图", "格子边界", "轮次/状态信息"], "token-widget": ["脚本/iframe URL", "sitekey", "页面 URL", "action/cdata/rqdata", "callback 名"], "waf-challenge": ["响应头", "challenge HTML", "cookie 名", "状态码", "challenge JS URL"], "unknown-custom": ["组件附近 HTML", "脚本 URL", "截图", "网络接口名"], } VERIFICATION_FLOW_BY_TYPE: dict[str, dict[str, list[str]]] = { "text": { "references": ["references/verification-workflow.md", "references/open-source-recipes.md"], "scripts": ["scripts/inspect_assets.py", "scripts/solver_request_template.py"], }, "math": { "references": ["references/verification-workflow.md", "references/open-source-recipes.md"], "scripts": ["scripts/inspect_assets.py", "scripts/solver_request_template.py"], }, "slider": { "references": [ "references/verification-workflow.md", "references/open-source-recipes.md", "references/motion-and-coordinate.md", "references/provider-execution-notes.md", ], "scripts": ["scripts/inspect_assets.py", "scripts/map_coordinates.py", "scripts/generate_motion_track.py"], }, "click-select": { "references": ["references/verification-workflow.md", "references/open-source-recipes.md", "references/motion-and-coordinate.md"], "scripts": ["scripts/inspect_assets.py", "scripts/map_coordinates.py", "scripts/solver_request_template.py"], }, "rotate": { "references": ["references/verification-workflow.md", "references/open-source-recipes.md", "references/motion-and-coordinate.md"], "scripts": ["scripts/inspect_assets.py", "scripts/map_coordinates.py", "scripts/generate_motion_track.py"], }, "grid": { "references": ["references/verification-workflow.md", "references/open-source-recipes.md", "references/motion-and-coordinate.md"], "scripts": ["scripts/inspect_assets.py", "scripts/map_coordinates.py", "scripts/solver_request_template.py"], }, "audio": { "references": ["references/verification-workflow.md", "references/open-source-recipes.md"], "scripts": ["scripts/inspect_assets.py", "scripts/solver_request_template.py"], }, "drag-drop": { "references": ["references/verification-workflow.md", "references/motion-and-coordinate.md"], "scripts": ["scripts/inspect_assets.py", "scripts/map_coordinates.py", "scripts/generate_motion_track.py"], }, "trace-draw": { "references": ["references/verification-workflow.md", "references/motion-and-coordinate.md", "references/open-source-recipes.md"], "scripts": ["scripts/inspect_assets.py", "scripts/generate_motion_track.py"], }, "scratch": { "references": ["references/verification-workflow.md", "references/motion-and-coordinate.md"], "scripts": ["scripts/inspect_assets.py", "scripts/generate_motion_track.py"], }, "image-restore": { "references": ["references/verification-workflow.md", "references/open-source-recipes.md", "references/motion-and-coordinate.md"], "scripts": ["scripts/inspect_assets.py", "scripts/analyze_tile_restore.py", "scripts/map_coordinates.py", "scripts/solver_request_template.py"], }, "area-select": { "references": ["references/verification-workflow.md", "references/open-source-recipes.md", "references/motion-and-coordinate.md"], "scripts": ["scripts/inspect_assets.py", "scripts/map_coordinates.py", "scripts/solver_request_template.py"], }, "difference-click": { "references": ["references/verification-workflow.md", "references/open-source-recipes.md", "references/motion-and-coordinate.md"], "scripts": ["scripts/inspect_assets.py", "scripts/map_coordinates.py", "scripts/solver_request_template.py"], }, "font-identify": { "references": ["references/verification-workflow.md", "references/open-source-recipes.md", "references/motion-and-coordinate.md"], "scripts": ["scripts/inspect_assets.py", "scripts/map_coordinates.py", "scripts/solver_request_template.py"], }, "semantic-reasoning": { "references": ["references/verification-workflow.md", "references/open-source-recipes.md", "references/motion-and-coordinate.md"], "scripts": ["scripts/inspect_assets.py", "scripts/map_coordinates.py", "scripts/solver_request_template.py"], }, "game-challenge": { "references": [ "references/verification-workflow.md", "references/provider-execution-notes.md", "references/solver-platform-recipes.md", ], "scripts": ["scripts/inspect_assets.py", "scripts/solver_request_template.py"], }, "pow-challenge": { "references": ["references/verification-workflow.md", "references/provider-execution-notes.md"], "scripts": ["scripts/inspect_assets.py", "scripts/solver_request_template.py"], }, "risk-score": { "references": [ "references/verification-workflow.md", "references/provider-execution-notes.md", "references/solver-platform-recipes.md", ], "scripts": ["scripts/inspect_assets.py", "scripts/solver_request_template.py"], }, "one-click": { "references": ["references/verification-workflow.md", "references/provider-execution-notes.md", "references/solver-platform-recipes.md"], "scripts": ["scripts/inspect_assets.py", "scripts/solver_request_template.py"], }, "multi-step": { "references": ["references/verification-workflow.md", "references/open-source-recipes.md", "references/motion-and-coordinate.md"], "scripts": ["scripts/inspect_assets.py", "scripts/map_coordinates.py", "scripts/generate_motion_track.py"], }, "qa-logic": { "references": ["references/verification-workflow.md", "references/open-source-recipes.md"], "scripts": ["scripts/inspect_assets.py", "scripts/solver_request_template.py"], }, "biometric-liveness": { "references": ["references/verification-workflow.md", "references/provider-execution-notes.md"], "scripts": ["scripts/inspect_assets.py"], }, "token-widget": { "references": [ "references/verification-workflow.md", "references/provider-execution-notes.md", "references/solver-platform-recipes.md", ], "scripts": ["scripts/inspect_assets.py", "scripts/solver_request_template.py"], }, "waf-challenge": { "references": [ "references/verification-workflow.md", "references/browser-acquisition.md", "references/provider-execution-notes.md", ], "scripts": ["scripts/inspect_assets.py", "scripts/solver_request_template.py"], }, "unknown-custom": { "references": ["references/verification-workflow.md", "references/captcha-types.md", "references/provider-products.md"], "scripts": ["scripts/inspect_assets.py", "scripts/solver_request_template.py"], }, } SOLUTION_OPTIONS: dict[str, dict[str, Any]] = { "text": { "open_source_first": ["ddddocr", "Tesseract + 字符白名单", "OpenCV 预处理后 OCR"], "fallback_platforms": ["云码/JFBYM", "超级鹰", "2Captcha ImageToText", "Anti-Captcha Image"], "when_to_switch": ["字符扭曲严重", "干扰线/背景噪声导致本地 OCR 低于目标通过率", "需要人工兜底"], "notes": "先做图片裁剪、二值化、降噪、字符集和长度约束;平台方案只作为授权 QA 的备选。", }, "math": { "open_source_first": ["ddddocr/Tesseract 识别表达式", "正则归一化运算符", "安全表达式求值"], "fallback_platforms": ["云码/JFBYM 计算题类型", "超级鹰", "2Captcha Normal Captcha"], "when_to_switch": ["表达式被强干扰", "中文数字/竖排/应用题导致 OCR 不稳定"], "notes": "输出归一化表达式和结果,遇到歧义不要猜。", }, "slider": { "open_source_first": ["ddddocr slide-match/slide-comparison", "OpenCV 模板/边缘/差分匹配", "轨迹模型与坐标校准"], "fallback_platforms": ["云码/JFBYM 滑块类型", "超级鹰滑块/坐标类型", "CapSolver", "2Captcha GeeTest/slider 类任务"], "when_to_switch": ["缺口弱边缘", "背景乱序/透明块", "厂商行为评分导致视觉偏移正确但验证失败"], "notes": "视觉偏移、DOM 坐标、拖动轨迹和厂商加密参数要分开分析。", }, "click-select": { "open_source_first": ["OCR + 模板匹配", "YOLO/目标检测", "VLM 返回坐标与顺序"], "fallback_platforms": ["云码/JFBYM 坐标/点选类型", "超级鹰坐标类型", "2Captcha Coordinates/ClickCaptcha", "CapSolver ComplexImageTask"], "when_to_switch": ["目标重叠", "题面语义复杂", "多轮点选或顺序评分导致本地模型不稳定"], "notes": "必须同时输出题面、目标顺序、截图坐标和元素相对坐标。", }, "rotate": { "open_source_first": ["OpenCV 特征/主方向检测", "图像匹配估计角度", "VLM 辅助判断正向"], "fallback_platforms": ["云码/JFBYM 旋转类型", "超级鹰旋转/坐标类型", "CapSolver/2Captcha 的图片任务"], "when_to_switch": ["图片对称", "角度映射非线性", "固定偏移需要大量样本校准"], "notes": "返回角度、拖动距离、轨道宽度和校准偏移。", }, "grid": { "open_source_first": ["图像分割 + 分类模型", "YOLO/CLIP/VLM 按题面分类", "每格置信度复核"], "fallback_platforms": ["2Captcha Grid/reCAPTCHA", "CapSolver", "CapMonster Cloud", "Anti-Captcha", "NopeCHA"], "when_to_switch": ["reCAPTCHA/hCaptcha 多轮题", "目标跨格或低分辨率", "题面语义超出本地模型"], "notes": "输出格子编号和中心坐标,预期多轮验证。", }, "audio": { "open_source_first": ["faster-whisper/Whisper 本地转写", "降噪和静音裁剪", "数字/字符白名单纠错"], "fallback_platforms": ["2Captcha Audio", "CapMonster/Anti-Captcha 支持的音频或人工任务", "人工复核"], "when_to_switch": ["强噪声", "口音/多语言", "音频一次性有效且本地 ASR 不稳定"], "notes": "只处理网页音频验证码,不处理电话语音 OTP、短信、邮箱或 MFA。", }, "drag-drop": { "open_source_first": ["OpenCV/目标检测定位源对象和目标区", "VLM 判断拖放关系", "DOM 坐标和释放点校准"], "fallback_platforms": ["云码/JFBYM 坐标类型", "超级鹰坐标类型", "CapSolver ComplexImageTask", "人工接管"], "when_to_switch": ["目标区域不明确", "动画/吸附规则复杂", "拖放路径被行为评分"], "notes": "和水平滑块不同,重点是源对象、目标区域和释放点。", }, "trace-draw": { "open_source_first": ["OpenCV 边缘/颜色分割", "骨架化提取路径", "点列重采样和轨迹平滑"], "fallback_platforms": ["云码/JFBYM 轨迹类型", "超级鹰坐标/轨迹类", "人工标注"], "when_to_switch": ["路径被遮挡", "轨迹采样格式未知", "需要大量样本校准速度/间隔"], "notes": "输出点列格式、采样间隔、坐标原点和画布尺寸。", }, "scratch": { "open_source_first": ["canvas 区域识别", "规则化覆盖轨迹", "刮开前后状态差分"], "fallback_platforms": ["人工接管", "云码/JFBYM 其他类型定制", "超级鹰人工/定制"], "when_to_switch": ["通过阈值未知", "移动端触摸事件差异", "刮开后还有二次文字/图像题"], "notes": "优先分析覆盖比例和状态变化;刮开后出现题面再转入对应子类型。", }, "image-restore": { "open_source_first": ["先判定 tile-scramble 切片乱序", "优先从 tileOrder/pieceOrder/CSS background-position/canvas drawImage 还原顺序", "再用 OpenCV/Pillow 边缘连续性和纹理匹配"], "fallback_platforms": ["云码/JFBYM 滑块/拼图/其他类型", "超级鹰滑块/坐标类型", "CapSolver/2Captcha 图片任务"], "when_to_switch": ["重复纹理", "随机裁剪", "页面顺序字段被加密或缺失", "视觉复原与提交参数强绑定"], "notes": "区别于 slider:目标是复原完整图片或分块顺序;切片乱序时先找页面逻辑,再做图片边缘匹配。", }, "area-select": { "open_source_first": ["目标检测/图像分割", "SAM/YOLO + 题面解析", "VLM 返回框/多边形"], "fallback_platforms": ["云码/JFBYM 坐标/区域类型", "超级鹰坐标类型", "CapSolver ComplexImageTask"], "when_to_switch": ["边界模糊", "多目标区域", "平台要求多边形而不是单点"], "notes": "输出矩形、多边形或中心点集合,并说明坐标原点。", }, "difference-click": { "open_source_first": ["图像配准 + 差分", "显著性检测", "VLM 复核差异点"], "fallback_platforms": ["云码/JFBYM 坐标类型", "超级鹰坐标类型", "人工标注"], "when_to_switch": ["压缩噪声强", "差异极细微", "差异点数量/顺序不明确"], "notes": "先配准再差分,避免把平移缩放误判为差异。", }, "font-identify": { "open_source_first": ["OCR 识别内容 + 字形特征比对", "模板匹配", "VLM 判断字体关系"], "fallback_platforms": ["云码/JFBYM 坐标/定制类型", "超级鹰人工/坐标类型", "人工标注"], "when_to_switch": ["字体相近", "抗锯齿/缩放影响字形", "题面要求复杂排序"], "notes": "不要只看文字内容,核心是字形/字体关系。", }, "semantic-reasoning": { "open_source_first": ["YOLO/目标检测 + 规则推理", "CLIP/VLM 语义理解", "候选目标置信度复核"], "fallback_platforms": ["云码/JFBYM 坐标/定制类型", "超级鹰人工/坐标类型", "CapSolver ComplexImageTask"], "when_to_switch": ["空间关系歧义", "目标遮挡", "VLM 置信度低或输出不稳定"], "notes": "输出选择理由和坐标,题面歧义时让用户确认。", }, "game-challenge": { "open_source_first": ["截图/帧状态识别", "VLM 解析题面", "必要时拆成 rotate/grid/click-select 子类型"], "fallback_platforms": ["2Captcha FunCaptcha/Arkose", "CapSolver FunCaptcha", "Anti-Captcha FunCaptcha", "NopeCHA Arkose"], "when_to_switch": ["3D 状态连续变化", "题目频繁更新", "纯视觉模型无法稳定控制交互"], "notes": "优先人工接管或授权 QA 模型辅助,不提供未授权自动通关脚本。", }, "pow-challenge": { "open_source_first": ["按官方协议实现 challenge 校验", "检查 difficulty/nonce/TTL/防重放", "ALTCHA/Cap.js/mCaptcha 自托管校验"], "fallback_platforms": ["通常不需要打码平台", "2Captcha/Anti-Captcha 的 ALTCHA 或 PoW 支持仅作兼容备选"], "when_to_switch": ["第三方组件接入失败", "payload/session 绑定异常", "服务端签名校验不通过"], "notes": "这是计算挑战,不是图片识别;自有系统应优先修正集成。", }, "risk-score": { "open_source_first": ["官方服务端校验日志", "action/hostname/score 阈值诊断", "误伤样本回放"], "fallback_platforms": ["2Captcha reCAPTCHA v3/Enterprise", "CapSolver", "CapMonster Cloud", "Anti-Captcha"], "when_to_switch": ["授权测试需要对比人工/平台 token", "评分低但无法定位 action/session/环境原因"], "notes": "不要把低分规避写成方案;优先做集成诊断和阈值评估。", }, "one-click": { "open_source_first": ["组件状态和 callback 诊断", "人工点击/按住复现", "失败后识别升级出的子类型"], "fallback_platforms": ["2Captcha/CapSolver/CapMonster 对 Turnstile/reCAPTCHA/hCaptcha 的 token 任务", "人工接管"], "when_to_switch": ["点击后升级二次挑战", "组件状态和服务端校验不同步", "行为评分不稳定"], "notes": "普通表单 checkbox 不是验证码,必须有验证码/厂商证据。", }, "multi-step": { "open_source_first": ["逐轮截图和状态记录", "每轮按具体子类型选择工具", "轮次状态机复盘"], "fallback_platforms": ["按子类型选择平台", "人工接管多轮流程"], "when_to_switch": ["上一轮结果影响下一轮", "局部成功被误判为完成", "多轮题型混合"], "notes": "它是流程标签,报告里应同时列每轮的具体类型和方案。", }, "qa-logic": { "open_source_first": ["文本解析/规则库", "本地 LLM/VLM 解析题面", "人工复核"], "fallback_platforms": ["云码/JFBYM 文字/定制类型", "超级鹰人工类型", "2Captcha Normal Captcha"], "when_to_switch": ["题库动态变化", "问题依赖上下文", "自然语言歧义"], "notes": "简单算术应转为 math;安全问题/MFA 不进入验证码求解方案。", }, "biometric-liveness": { "open_source_first": ["官方 SDK 接入诊断", "设备权限/摄像头/动作提示检查", "人工复核和可访问性替代"], "fallback_platforms": ["不建议使用打码平台", "仅在合规授权下联系厂商或人工审核服务"], "when_to_switch": ["涉及隐私/合规", "误识别/设备兼容", "需要人工审核链路"], "notes": "不提供绕过、伪造、替身或自动化活体方案。", }, "token-widget": { "open_source_first": ["官方测试 key/测试环境", "sitekey/action/callback/TTL 诊断", "服务端校验日志复核"], "fallback_platforms": ["2Captcha", "CapSolver", "CapMonster Cloud", "Anti-Captcha", "YesCaptcha/NoCaptchaAI"], "when_to_switch": ["授权 QA 需要对照平台 token", "本地无法稳定复现人工验证", "只需验证集成链路而非训练模型"], "notes": "记录 pageurl、sitekey、action、callback 和 token TTL;不要注入 token 到未授权流程。", }, "waf-challenge": { "open_source_first": ["真实浏览器取证", "服务端/WAF 日志", "TLS/HTTP/JS 环境一致性诊断"], "fallback_platforms": ["CapSolver/CapMonster/2Captcha 对 AWS WAF、Turnstile、DataDome 等的授权测试支持", "厂商支持/人工复核"], "when_to_switch": ["自有系统误伤真实用户", "challenge cookie/session 绑定异常", "环境指纹诊断无法定位"], "notes": "这不是普通图片验证码,优先定位风控产品和环境问题。", }, "unknown-custom": { "open_source_first": ["收集 HTML/脚本/截图/接口名", "先归入最接近的可见类型", "建立小样本测试夹具"], "fallback_platforms": ["云码/JFBYM 定制", "超级鹰人工/定制", "2Captcha/CapSolver 通用图片或坐标任务"], "when_to_switch": ["证据不足", "自研 canvas 混淆", "现有分类无法稳定覆盖"], "notes": "先补证据,不要只凭一张截图过拟合。", }, } BOUNDARY_PATTERNS = [ r"绕过登录|绕过验证码|自动请求通过|请求通过|批量注册|薅羊毛|撞库|代过验证|破解验证码", r"\b(?:bypass|circumvent|mass signup|bulk signup|credential stuffing|account takeover)\b", r"短信验证码|邮箱验证码|手机验证码|动态口令|\bMFA\b|\b2FA\b|一次性密码|\bOTP\b", ] def read_value(value: str | None) -> str: if not value: return "" path = Path(value) if path.exists() and path.is_file(): for encoding in ("utf-8", "utf-8-sig", "gb18030"): try: return path.read_text(encoding=encoding) except UnicodeDecodeError: continue return path.read_text(errors="replace") return value def read_many(values: Iterable[str] | None) -> list[str]: if not values: return [] return [read_value(value) for value in values if value is not None] def focus_html(value: str) -> str: decoded = html_lib.unescape(value) snippets: list[str] = [] for pattern in ( r"