
Animation Garden 图片验证码自动识别全链路解析从 ONNX 模型契约到 MacCMS 后台求解协议【免费下载链接】animation-garden集找番、追番、看番的一站式弹幕追番平台云收藏同步 (Bangumi)离线缓存BitTorrent弹幕云过滤。100% Kotlin/Compose Multiplatform项目地址: https://gitcode.com/gh_mirrors/an/animation-garden图片验证码是 Web 数据源在搜索、播放时最常见的拦截手段。本文以 Animation Gardenani开源仓库中 docs/contributing/code/image-captcha.md 为骨架结合app/shared/app-data模块的源码与测试系统拆解这套「公共编排 平台识别器 后台 HTTP 会话」的验证码自动识别架构。读完本文你将理解验证码链路中各组件如何分工、captcha-v1.0.onnx模型的输入输出契约、Android/Desktop/iOS 三端如何共享同一模型以及 MacCMS 站点验证码的后台求解协议与训练样本采集流程。链路总览三件套式的验证码处理架构图片验证码链路整体位于app/shared/app-data模块由三部分构成公共编排WebSessionManager是页面加载与验证码处理的统一入口CaptchaSolver实现自动处理策略浏览器 / HTTP 会话WebSourceCookieJar在普通请求与求解请求之间共享站点会话平台识别器ImageCaptchaRecognizer是模型接入点Android、Desktop 与 iOS 分别注入各自实现。对应的源码文件全部位于 app/shared/app-data/src/commonMain/kotlin/domain/mediasource/web/captcha/其中WebSessionManager.kt —— 会话注册表、页面加载、solve 编排CaptchaSolver.kt —— 自动求解策略接口WebSourceCookieJar.kt —— 统一 Cookie 真相源ImageCaptchaSolver.kt ——ImageCaptchaRecognizer接口与两种求解策略实现。识别器只做一件事图片字节 → 四位数字ImageCaptchaRecognizer是一个函数式接口契约极其精简fun interface ImageCaptchaRecognizer { suspend fun recognize(sample: ImageCaptchaSample): String? }输入是 ImageCaptchaSample原始图片字节、媒体类型、来源 URL输出是四位数字字符串或null。平台实现注入到公共 solver 后识别器完全不需要关心重试、网页操作或结果校验——这些行为全部保留在公共逻辑中以保证三个平台行为一致。这一点有明确的边界约束模型不应负责重试、操作网页或判断验证码是否通过。从源码看solveImageCaptcha 公共函数统一负责「取图 → 识别 → 提交 → 校验 → 重试」的完整循环。自动求解策略浏览器 DOM 与 MacCMS 后台协议公共逻辑中内置了两个CaptchaSolver实现ImageCaptchaSolver.kt策略id适用场景实现方式MacCmsImageCaptchaSolvermaccms-image-captchaMacCMS 后台协议站点纯 HTTP 会话不创建浏览器BrowserImageCaptchaSolverbrowser-image-captcha通用图片验证码WebView/JCEF DOM 注入MacCmsImageCaptchaSolver.canAttempt在recognizer UnsupportedImageCaptchaRecognizer时直接返回false或当验证码类型为Image、或 host 命中已知 MacCMS 站点列表acgfta.com、cycani.org、youknow.tv时返回true。测试 ImageCaptchaSolverTest.kt 专门验证了「未知类型验证码但 host 命中列表」时也能发起求解说明该策略是按站点协议而非仅按验证码类型触发。WebSessionManager页面加载与求解的统一编排WebSessionManager 是整个 Web 数据源页面加载与验证码处理的编排核心职责集中在几个关键机制会话注册表与浏览器生命周期会话以host去掉www.前缀后的归一化值为 key每个 host 最多持有一个活跃浏览器通过 LRU 上限默认maxSessions 3与闲置 TTL默认idleTtl 5.minutes自动回收后台协程每 30 秒执行一次 sweepIdleSessions 清理闲置会话浏览器创建受Semaphore(MAX_CONCURRENT_BROWSER_CREATIONS)值为 2限流防止并发创建风暴。fetchPage直连优先、浏览器兜底的取页策略fetchPage是引擎唯一的页面入口执行顺序为消费已暂存的已验证业务页consumePendingSolvedPage尝试备用取数路由searchRoutes浏览器粘滞窗口60 秒内 HTTP 刚被验证码挡过且存在暖会话时直接走浏览器避免先失败一次直连 HTTP 拉取用PageEvaluator判决直连被验证码挡住后在暖会话中loadInBrowser重载若暖会话也被挡则invalidate会话并从干净状态重新开始。其中值得注意的细节站点返回 4xx 时httpFetch 会把ClientRequestException转回LoadedPage交给判决——被挡页面是「内容」而非「错误」不能直接抛异常。solve交互与自动的双通道solve(request, interactive)按interactive参数分流interactive true必定呈现交互对话框且入口不查任何缓存——用户主动点「处理验证码」本身就说明当前状态不可用。同 host 已有进行中的 solve 时执行 single-flight 合并join 已有任务interactive false遍历solvers策略链没有可用策略时立即失败、不创建浏览器每个 host 有 60 秒求解失败冷却防止浏览器风暴。自动求解成功后retainPendingSolvedPage只会暂存已经由PageEvaluator验证过的业务页并且仅允许紧接着的精确同 URL 请求在 60 秒内消费一次若后续搜索再次检测到验证码必须重新取图、识别、提交并更新 Cookie。WebSessionManager不缓存通用的Solved结果——这是为了避免把单次验证码的临时会话误当成长期可用的「免验证」凭证。交互式 UI 队列interactiveUi: StateFlowInteractiveSolveUi?使用多槽位队列并发多次 solve 排队依次呈现不会互相顶掉。UI 通过onConfirm/onDismiss/onRefresh三个回调与求解协程通信App 根部唯一的 dialog host 消费该 Flow。WebSourceCookieJar普通请求与求解请求的会话粘合剂WebSourceCookieJar 实现了 Ktor 的CookiesStorage是 Web 数据源的统一 Cookie 真相源三处共用同一份数据HTTP 请求侧通过CookieJarFeature在HttpClient构造时注入播放器 WebView 注入getCookieHeaderValues(url)以namevalue形式同步返回可见 Cookie供播放器配置注入非 suspend 上下文浏览器求解成果导入addBrowserCookies把浏览器解决验证码后收集到的 Cookie 导入共享存储。域匹配规则支持精确 host去www.或.domain后缀匹配保证cf_clearance这类域级 Cookie 能覆盖到播放页所在子域。设计上有意不做磁盘持久化CEF/WebView 自身的 cookie store 天然持久重启后首次被挡时浏览器仍持有 clearance可快速重新通过。同文件中的 WebSourceIdentityRegistry 维护 per-host 的 User-Agent 对齐cf_clearance绑定 UAsolve 成功时记录浏览器的真实 UAWebSourceIdentityFeature对后续 HTTP 请求覆写User-Agent保证 HTTP 侧身份与清掉挑战的浏览器完全一致。平台实现一份模型、三种加载方式三个客户端平台共用同一份captcha-v1.0.onnx模型与同一套输入输出契约。模型只保存一份位于 app/shared/app-data/src/commonMain/composeResources/files/captcha-v1.0.onnx由 Compose Multiplatform 资源管理统一打包到 Android assets、Desktop classpath 与 iOS app bundle。识别器不再各自定位模型文件而是通过app-datacommonMain 的公共入口读取这同一份资源。统一资源入口ImageCaptchaModel.kt 定义了两个平台入口internal const val IMAGE_CAPTCHA_MODEL_RESOURCE files/captcha-v1.0.onnx // Android 与 Desktop资源在 apk/jar 内不是文件系统路径只能读字节交给 ONNX Runtime suspend fun readImageCaptchaModelBytes(): ByteArray Res.readBytes(IMAGE_CAPTCHA_MODEL_RESOURCE) // iOSCompose 资源是 app bundle 内的真实文件ORTSession 只接受文件路径 fun imageCaptchaModelUri(): String Res.getUri(IMAGE_CAPTCHA_MODEL_RESOURCE)Androidonnxruntime-androidAndroidOnnxImageCaptchaRecognizer 使用onnxruntime-android通过readImageCaptchaModelBytes()读字节建立OrtSession。其预处理逻辑preprocess与模型契约一一对应用BitmapFactory.decodeByteArray解码Bitmap.createScaledBitmap以最近邻缩放false关闭双线性滤波到 96×32按(299*R 587*G 114*B 500) / 1000计算灰度再除以 255 归一化到[0, 1]推理输出[B, 4, 10]的 logits每个位置取maxBy对应数字拼接成四位字符串。识别器使用SuspendLazy懒加载 session推理在Dispatchers.Default上执行任何异常都会runCatching捕获并返回null不中断公共链路。DesktopONNX Runtime JVMDesktopOnnxImageCaptchaRecognizer 同样通过readImageCaptchaModelBytes()读字节建 session与 Android 共享同一资源入口。平台实现通过 Koin 模块注入Android 在 AndroidModules.kt、Desktop 在 DesktopModules.kt 中完成装配。iOSonnxruntime-objc 与 file:// URIiOS 使用onnxruntime-objcCocoaPod。由于 Compose 资源在 iOS 上是 app bundle 内的真实文件而ORTSession只接受文件路径iOS 改用 imageCaptchaModelUri() 拿到资源的file://URI解析为路径后直接交给ORTSession——无需读入字节也无需落临时文件。实现位于 app/shared/application/src/iosMain/kotlin/ios/IosOnnxImageCaptchaRecognizer.kt并有对应的 IosOnnxImageCaptchaRecognizerTest.kt 覆盖。平台能力差异与限制Android 与 Desktop 同时支持从浏览器 DOM 提取图片BrowserImageCaptchaSolver路径Android 与 iOS 还支持MacCMS 后台请求协议MacCmsImageCaptchaSolver路径iOS 当前没有交互式验证码 WebView自动识别失败后返回现有CaptchaRequired状态且用户主动打开交互式验证暂不支持——这个限制与模型推理能力无关。模型契约输入输出与评估基线模型契约固定为三模型集成的 ONNX 模型随应用资源交付输入inputfloat32 [B, 1, 32, 96]。原图先转灰度再以最近邻缩放像素范围[0, 1]输出logitsfloat32 [B, 4, 10]四个位置分别取最大 logit 对应的数字即四位验证码模型源文件app/shared/app-data/src/commonMain/composeResources/files/captcha-v1.0.onnx。生产模型指纹与评估数据当前生产模型来自captcha-final-prod_20260717_124015文件 1,173,896 bytesSHA-256 为97731e093e77c69a768de81ed9d565bb5f81c6bef88df261b6dd460bca2cfd9a它由三个 93,904 参数的position_ds成员组成集成参数总数281,712训练数据快照 SHA-256 为def0786403436641ef5f56309dc466a289cdbece1e8dd16f552354b314345e96对应评估运行在481 张分来源平衡留出集上四位完全准确率96.05%字符准确率98.80%分来源四位完全准确率新优酷 96.27%、次元城动画 96.88%、饭团动漫 95.00%。需要特别说明的是该留出集覆盖多个来源但不是完全盲测不能直接作为真实部署准确率结论。生产模型使用完整冻结快照重新训练本身不重复报告准确率。运行时通过刷新图片并最多尝试三次提高成功机会后续替换模型时仍应保留独立、分来源的真实验证码评估报告。模型初始化或推理失败时识别器返回无结果公共链路继续执行刷新重试并最终返回失败自动识别分支不会向用户展示图片验证码页面而是降级到CaptchaRequired状态。后台请求协议MacCMS 站点的无界面求解流程Android 与 iOS 对检测为图片验证码的MacCMS 搜索页使用后台 HTTP 会话完成验证不加载交互页面。该流程不按数据源逐个实现而是复用站点共同使用的协议完整步骤为取图直接请求/index.php/verify/index.html获取验证码图片。该请求会创建同一验证会话所需的PHPSESSID因此无需在求解前重复请求搜索页提交答案将 CNN 返回的四位数字通过POST /index.php/ajax/verify_check?typesearchverify...提交校验JSON 响应中的code为1后后台求解器在同一 Cookie 会话中请求搜索 URL并用数据源现有 selector 验证返回的 HTML。selector 能解析出搜索结果或搜索 URL 返回明确的空结果提示时均视为有效搜索页首页、冷却页和验证码页不会被空结果提示误判为成功交接已验证的搜索页一次性交给原数据源流程直接解析避免验证码刚通过就因重复请求搜索 URL 再次触发验证失败刷新识别失败时重新请求图片只有图片字节哈希发生变化后才执行下一次推理。对应源码中的实现细节ImageCaptchaSolver.kt取图 URL 带_ani_captcha${currentTimeMillis()}-${requestIndex}时间戳参数强制绕过缓存captureSample最多尝试IMAGE_CAPTCHA_HTTP_REFRESH_ATTEMPTS10次间隔 250ms只有字节发生变化才返回样本POST 提交携带Referrer、X-Requested-With: XMLHttpRequest与表单 Content-Type 头模拟 AJAX 请求请求通过WebSourceCookieJar维护 Cookie并在验证码图片请求发生同路径跨域重定向时采用新的 originadoptRedirectOrigin。这样可以处理数据源从旧域名迁移到新域名的情况同时避免把跳转到外部图片 CDN 的 URL 当作站点 origin。公共重试循环solveImageCaptcha的maxAttempts默认为 3每次先取「已变化」的新图 → 识别 →trim()后用 isValidImageCaptchaAnswer 校验长度 4 且全为数字→ 提交 → 用isSuccessfulSolve判决PageVerdict.Ok或「空内容 明确的空结果页 非冷却页 非验证码页」也算成功。测试 ImageCaptchaSolverTest.kt 验证了「识别 → 提交 → 校验成功」的完整路径以及「识别错误后重试两次再报告 Blocked」的行为。浏览器 DOM 提取的实现细节BrowserImageCaptchaSolver通过注入 JavaScript 操作页面ImageCaptchaSolver.ktCAPTURE_IMAGE_CAPTCHA_SCRIPT在页面插入#ani-image-captcha-samplemeta 标记用fetch(source, { credentials: include, cache: no-store })拉取验证码图片转成 base64 data URL写入data-state/data-image/data-source属性parseImageCaptchaSample解析该标记提取字节与媒体类型REFRESH_IMAGE_CAPTCHA_SCRIPT点击验证码图片并追加时间戳参数强制刷新buildSubmitImageCaptchaScript(answer)按input[nameverify]、input[nameverifycode]、input[id*captcha]等选择器定位输入框用原生 value setter 注入答案并派发input/change事件最后点击.verify-submit或表单提交按钮。采集训练样本Debug 构建的自动标注流水线Debug 构建的数据源测试页面在检测到图片验证码后会显示「采集 100 个训练样本」。采集器使用与运行时完全相同的浏览器会话和图片提取逻辑即BrowserImageCaptchaSolver的 capture 路径图片默认写入应用媒体下载目录下的captcha-samplesDesktop应用数据目录下的media-downloads/captcha-samplesAndroid应用专属外部存储的Movies/captcha-samples外部存储不可用时回退到应用内部目录。目录中包含原始图片和追加写入的manifest.jsonl。清单记录文件名、数据源 ID、页面 URL、图片 URL、媒体类型和采集时间对应源码中的ImageCaptchaSampleManifestEntryImageCaptchaSolver.kt。采集器不会猜测标签训练前应完成人工标注或将已确认标签写入训练集避免把错误预测作为真值——这是样本质量的生命线。防重复采集机制批量采集会在保存首张图片后主动刷新验证码并等待下一张图片完成加载。若新图片的原始字节与上一张完全相同采集器会继续刷新单张样本最多尝试 10 次达到上限仍未变化时停止当前批次避免把缓存图片重复写入训练集。文件名格式为${mediaSourceId}-${capturedAtMillis}-${index}.${扩展名}扩展名按媒体类型映射jpeg→jpg、webp→webp、gif→gif其余→png。在其他开发工具中直接调用如需在自定义工具或测试中采集样本可调用公共 APIcollectImageCaptchaSamplesToDirectory( browser browser, request captchaRequest, count 100, outputDirectory outputDirectory, )单次调用最多采集10,000个样本MAX_IMAGE_CAPTCHA_SAMPLE_COUNTrequire(count in 1..10_000)防止误操作无限占用存储空间。采集成功后返回ImageCaptchaSampleCollectionResult(savedCount, outputDirectory)。责任边界与降级策略小结整套架构的核心设计原则可以归纳为三条识别器职责最小化只做「图片字节 → 四位数字」重试、网页操作、结果校验全部留在公共逻辑保证三端行为一致会话状态单一真源Cookie 统一收敛到WebSourceCookieJar浏览器身份Cookie UA通过syncBrowserIdentity对齐到 HTTP 侧求解成功页仅在 60 秒内、精确同 URL 下可被消费一次自动识别失败不打断用户自动分支三次识别或验证仍失败时直接进入现有CaptchaRequired状态不会自动打开交互式 WebView/JCEF仅当用户主动选择处理验证码时才复用原有交互式填写流程。这套设计把「模型能力」与「站点协议」解耦未来替换更强模型只需替换ImageCaptchaRecognizer实现站点协议演进只需更新 solver 策略公共编排与平台会话管理保持不变。相关测试覆盖了答案合法性校验、MacCMS 策略触发条件、完整求解链路与失败重试行为可在 ImageCaptchaSolverTest.kt 与 WebSessionManagerTest.kt 中继续深入。【免费下载链接】animation-garden集找番、追番、看番的一站式弹幕追番平台云收藏同步 (Bangumi)离线缓存BitTorrent弹幕云过滤。100% Kotlin/Compose Multiplatform项目地址: https://gitcode.com/gh_mirrors/an/animation-garden创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考