
Ani 弹幕追番平台 Web 数据源验证码处理架构解析PageEvaluator 唯一判决与 WebSessionManager 会话编排【免费下载链接】animation-garden集找番、追番、看番的一站式弹幕追番平台云收藏同步 (Bangumi)离线缓存BitTorrent弹幕云过滤。100% Kotlin/Compose Multiplatform项目地址: https://gitcode.com/gh_mirrors/an/animation-garden本文基于仓库文档 docs/dev/media/web-captcha.md 展开并结合app/shared下的源码实现进行深度印证。该文档描述的是SelectorMediaSource查询链路中验证码Cloudflare / Turnstile / 图片验证码 / 滑块等与限流的处理架构是 Anianimation-garden集找番、追番、看番的一站式弹幕追番平台对旧版WebCaptchaCoordinator体系的整体重写状态为已实施2026-07。读完本文你将掌握该平台如何以唯一判决函数 无陈旧缓存 浏览器逃生通道 平台层最薄化四原则构建验证码处理链路以及桌面JCEF、AndroidWebView、iOS降级三端各自的落地方式。背景为什么需要重写验证码处理体系Ani 的SelectorMediaSource数据源通过 HTTP 直连抓取公开站点页面并解析番剧条目与剧集。这类公开数据源普遍部署 Cloudflare Challenge、Turnstile、图片验证码、滑块等反爬挑战同时还有 HTTP 429 与站内冷却页等限流手段。旧版WebCaptchaCoordinator体系存在判定标准不一致导致死循环、陈旧缓存不可恢复、EDT 阻塞、限流被包装成验证码、检测器误报面过大等 13 个问题完整清单见文末附录因此在 2026-07 被整体重写。新的架构以 v1 为边界验证码由用户在浏览器里交互解决自动识别图片验证码 CNN、MacCMS 后台协议、挑战自动通过不在 v1 实现但架构预留了干净的接入点CaptchaSolver与SearchRoute两个接缝。设计目标六条硬性原则文档明确给出了本次重写的六条设计目标它们决定了后续所有组件划分唯一真相来源页面是否被挡、验证码是否解决必须用同一个判定函数且 selector 能解析出内容优先于一切启发式检测。无陈旧缓存不存在任何记录在案的成功可被盲目返回已解决只体现为可现场验证的 cookie 和活浏览器会话。浏览器是逃生通道不是常驻模式直连 HTTP 优先仅在挑战期间走浏览器站点恢复后自动降级回直连。平台层最薄化平台代码只实现一个能被驱动的浏览器全部业务逻辑在 commonMain用假浏览器在 commonTest 全覆盖。限流不是验证码HTTP 429 / 站内冷却页走独立重试路径不弹浏览器、不误导用户无结构化 selector 期望的页面若返回 HTTP 403 仍按未知验证码处理公开数据源的反爬挑战可能只返回空白Forbidden需保留浏览器恢复路径。身份一致性HTTP 请求呈现的 cookie 与 User-Agent 必须和清掉挑战的浏览器完全一致cf_clearance绑定 UA。总体结构commonMain 业务 平台浏览器适配器架构图引自原文档┌─ commonMain ────────────────────────────────────────────────────────┐ │ │ │ PageEvaluator 页面判决: Ok / EmptyContent / Blocked │ │ WebSessionManager 解决编排 · 会话注册表 · 生命周期 │ │ WebSourceCookieJar ktor CookiesStorage 实现 (构造时注入) │ │ WebSourceIdentity per-host User-Agent 对齐的 ktor 插件 │ │ InteractiveSolveDialog 交互解决对话框外壳 (Compose) │ │ · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · │ │ CaptchaSolver[] [预留, v1 为空] 自动解决策略链 │ │ SearchRoute[] [预留, v1 为空] 备用取数路由 │ │ │ ├─ platform (每端只实现 CaptchaBrowser) ─────────────────────────────┤ │ │ │ desktop → CefCaptchaBrowser (JCEF) │ │ android → WebViewCaptchaBrowser (android.webkit.WebView) │ │ ios → 无实现, isSupported false (未来可接 WKWebView) │ │ │ └─────────────────────────────────────────────────────────────────────┘一次搜索的数据流引自原文档SelectorMediaSource.search └─ WebSessionManager.fetchPage(url, expectation) ├─ [直连] ktor GET (带 jar cookies UA 覆写) ──→ PageEvaluator │ ├─ Ok / EmptyContent ──→ 返回 (不碰浏览器) │ └─ Blocked(Captcha) ──→ 记录 lastHttpBlockedAt │ └─ 有暖会话? ──→ 浏览器重载 ──→ PageEvaluator ──→ 仍 Blocked → 上抛 ├─ [浏览器粘滞] 60s 内 HTTP 刚被挡过且有暖会话 ──→ 直接浏览器加载 └─ Blocked 上抛后: ├─ Captcha → solve(auto) [v1: solver 列表为空, 直接失败] → CaptchaRequired → UI chip │ chip 点击 → solve(interactive) → 用户在浏览器解决 → 成功则 restart └─ RateLimited → delay 后重试一次; 失败 → RateLimited 状态 → UI 倒计时上述组件在仓库中均有对应源码文件集中在 app/shared/app-data/src/commonMain/kotlin/domain/mediasource/web/ 及其captcha/子目录。PageEvaluator唯一判决函数所有这个页面算不算被挡的判断都必须经过 PageEvaluator.kt。引擎解析路径、交互对话框自动关闭、浏览器会话页面加载以及未来的 auto-solve 成功判定共用同一个函数从而保证solve 的成功标准与 retry 的成功标准恒等——解决成功但重试仍失败在结构上不可能发生。核心类型定义源码中 PageEvaluator.kt#L38-L80 定义了三个核心密封类型sealed interface PageExpectationout T { /** 期望解析出条目列表 (搜索结果页) */ data class SearchResults(val config: SelectorSearchConfig) : PageExpectationListWebSearchSubjectInfo /** 期望解析出剧集列表 (条目详情页) */ data class SubjectDetails( val config: SelectorSearchConfig, /** episode 所属条目的完整 URL, 用于解析相对链接. */ val subjectUrl: String, ) : PageExpectationSelectedChannelEpisodes /** 无 selector 可用时的兜底 (视频页等): 只要没有被挡特征就算 Ok. */ data object AnyContent : PageExpectationDocument } sealed interface PageVerdictout T { /** 解析成功. */ data class OkT(val value: T, val document: Document) : PageVerdictT /** 正常页面, 合法的 无结果. */ data class EmptyContent(val document: Document?) : PageVerdictNothing /** 页面被挡 (验证码 / 限流 / 404 / 403). */ data class Blocked(val reason: BlockReason) : PageVerdictNothing } sealed interface BlockReason { data class Captcha(val kind: WebCaptchaKind) : BlockReason /** HTTP 429 或站内冷却页. */ data class RateLimited(val retryAfter: Duration?) : BlockReason data object NotFound : BlockReason /** 无结构化 selector 期望的页面返回 HTTP 403, 且无验证码特征. */ data class Forbidden(val status: Int) : BlockReason }同文件还定义了LoadedPage携带finalUrl、html、HTTPstatus、retryAfter浏览器加载时status为null、SolveRequest含mediaSourceId、pageUrl、kind、用于判定 solve 是否成功的expectation以及BlockedException页面被挡时上抛由MediaFetcher按reason映射为对应 fetch 状态。判决顺序解析优先是硬规则文档给出的判决顺序有对应测试如下HTTP 404 →NotFound按 expectation 解析解析出内容 →Ok直接结束——即使启发式检测报警、即使状态码是 4xx。selector 能解析就是最终真相站内冷却页如请不要频繁操作→RateLimitedHTTP 429 →RateLimited(Retry-After)启发式检测分类出验证码 →Captcha(kind)无特征的 HTTP 403搜索页或条目详情页 →Captcha(Unknown)其他页面 →ForbiddenHTTP 468 →Captcha(Unknown)以上都不是 →EmptyContent。规则 2 的解析优先是整个设计的基石页面上出现 captcha 字样、嵌了 reCAPTCHA 脚本的正常页面只要 selector 能解析出条目/剧集就不会被误判为被挡。这一点在 PageEvaluatorTest.kt 中有对应用例。结构化 selector 页面上的无特征 403 保留浏览器逃生通道部分 WAF如次元城使用的防护会直接返回不带稳定特征的 403若将其归类为Forbidden交互验证和已注册的自动 solver 都不会运行。而AnyContent没有可验证的 selector 结果因此仍将同类响应归为Forbidden避免把普通权限错误误报为验证码。启发式检测器WebCaptchaDetector降级为纯分类器重写后WebCaptchaDetector降级为纯分类器只在解析失败后运行职责只是猜测验证码类型用于决定 UI 文案与 auto-solve 策略判错的代价因此很低。规则相对旧版收紧删除captcha (img || verify)这类宽泛兜底旧版几乎命中所有提到 captcha 的正常页面图片验证码必须有结构证据验证码输入框 提交按钮 验证码图片三件套Cloudflare challenge / Turnstile / SafeLine 的特征规则保留。对应的测试在 WebCaptchaDetectorTest.kt。CaptchaBrowser平台只实现一个能被驱动的浏览器平台适配器接口定义于 CaptchaBrowser.ktinterface CaptchaBrowser : AutoCloseable { /** 真实 UA (CEF / WebView 各自的), 用于 HTTP 侧身份对齐 */ val userAgent: String /** 主 frame 每次加载完成 (url, html) */ val pageLoads: SharedFlowLoadedPage suspend fun navigate(url: String) suspend fun currentPage(): LoadedPage? /** 带 domain/path/expiry 等完整属性 (CEF); Android 降级为 namevalue */ suspend fun collectCookies(urls: ListString): ListBrowserCookie /** 视频资源嗅探 */ fun setResourceInterceptor(handler: ((String) - InterceptDecision)?) /** SwingPanel / AndroidView */ Composable fun View(modifier: Modifier) } interface CaptchaBrowserFactory { /** iOS false */ val isSupported: Boolean suspend fun create(): CaptchaBrowser }两端的实现分别是桌面端 CefCaptchaBrowser.kt基于 JCEFuserAgent为 CEF 真实 UAcookie 收集带 domain/path/expiry 完整属性Android 端 WebViewCaptchaBrowser.kt基于android.webkit.WebViewcookie 属性降级为 namevalueiOS 端 v1 无实现isSupported false。线程铁律适配器方法全部是suspend内部自行 marshal 到 CEF/Main 线程浏览器回调线程CEF 的 EDT、Android 的 Main上只允许tryEmit/complete禁止任何形式的等待runBlocking、invokeAndWait、信号量等cookie 收集用suspendCancellableCoroutine桥接回调由 manager 的协程消费永不阻塞 UI 线程。这条铁律直接针对旧版问题 3桌面端页面观察者回调内runBlocking收集 cookie导致每 URL 1s 超时 ×4 ≈ 4s UI 冻结且收到的 cookie 为空。WebSessionManager编排核心WebSessionManager.kt 是整套体系的编排核心构造时注入browserFactory、cookieJar、evaluator、scope关键参数为idleTtl 5.minutes、stickyWindow 60.seconds、solveFailCooldown 60.seconds与文档60 秒粘滞窗口5 分钟闲置 TTL60s 失败冷却一一对应。class WebSessionManager( private val browserFactory: CaptchaBrowserFactory, private val cookieJar: WebSourceCookieJar, private val evaluator: PageEvaluator, private val scope: CoroutineScope, ) { /** 引擎的唯一页面入口: 直连优先, 按需走浏览器 */ suspend fun fetchPage(url: String, expectation: PageExpectation): PageVerdict* suspend fun solve(request: SolveRequest, interactive: Boolean): SolveOutcome suspend fun extractVideoResource( pageUrl: String, timeoutMillis: Long, resourceMatcher: (String) - WebViewVideoExtractor.Instruction, ): WebResource? /** 只取消进行中的 auto-solve, 不清暖会话、不清 cookie */ fun cancelAutoSolves() /** app 根部唯一 dialog host 消费此状态 */ val interactiveUi: StateFlowInteractiveSolveUi? val isInteractiveSupported: Boolean get() browserFactory.isSupported }会话注册表key 为host去www.前缀。cookie 本就是 host 级的同 host 的多个数据源共享一次解决成果不再有mediaSourceIdhost与 per-source fallback map每 host 最多一个活浏览器会话LRU 上限桌面 3 / Android 2闲置 TTL5 分钟自动回收回收时真正释放资源Android 端webView.destroy()桌面端 dispose CEF browser client permit注册表全部状态由单个Mutex保护。直连优先与浏览器粘滞per-host 记录lastHttpBlockedAt默认走直连 HTTP带 jar cookies UA 覆写交给PageEvaluator直连被挡且有暖会话 → 同一请求内用浏览器重载一次60 秒内 HTTP 刚被挡过且有暖会话 → 后续fetchPage直接走浏览器避免一次搜索的 1N 个页面每个都先失败一次直连一旦恢复Ok→ 回到直连浏览器闲置直至 TTL 回收。站点停止挑战后系统自愈不存在solved 一次该源终身走浏览器的路径。solve 语义没有 solvedResults 缓存。已解决只体现为 jar 里的 cookie 和注册表里的暖会话两者都可现场验证、可失效solve(interactive true)必定呈现对话框。同 host 已有进行中的 solve 则 joinsingle-flight。入口不查任何缓存——用户点处理验证码本身就是当前状态不行的证明solve(interactive false)auto遍历注入的solvers列表首个让evaluate返回Ok的策略胜出。v1 该列表为空 → 立即失败 → 上报CaptchaRequired自动失效闭环fetchPage发现刚 solve 成功却又 Blocked时自动丢弃该 host 的暖会话与 jar 中相关 cookie下次 solve 从干净状态开始失效不依赖任何人手动调用 reset防浏览器风暴全局并发上限 2per-host 失败冷却 60s。交互解决对话框对话框外壳黑底 顶栏返回 / 刷新 / ✓ 手动确认在 commonMainInteractiveSolveDialog.kt只有browser.View()来自平台自动关闭manager 消费pageLoadsflow每次主 frame 加载完成后 evaluate另保留 2 秒慢速快照兜底应付纯前端路由无导航事件的站点用户按 ✓ 手动确认时以当前页面快照 evaluate 的结果为准记录成败但都关闭对话框。WebSourceCookieJar 与 WebSourceIdentitycookie 与 UA 双通道身份对齐cookie 通道WebSourceCookieJar : CookiesStorage实现位于 WebSourceCookieJar.kt通过HttpClientProvider的新 featureCookieJarFeature在 HttpClient构造时注入取代旧版对 ktorHttpCookies私有字段的反射写入旧问题 11。注入点在 WebSourceHttpFeatures.kt实际使用在 HttpClientProvider.kt同时供SelectorMediaSource.matcher.patchConfig给播放器 WebView 注入 cookie——HTTP、播放器、两个平台共用同一份 cookie 真相cookie 存完整属性domain / path / expiry / secure。域匹配规则精确 host或.domain后缀匹配保证cf_clearance这类域级 cookie 能覆盖到播放页所在子域Android 拿不到属性时降级按页面 host去 www存后缀匹配不做磁盘持久化有意取舍CEF / WebView 自身的 cookie store 天然持久重启后首次被挡时 auto-solve 会因浏览器仍持有 clearance 而快速通过。UA 通道WebSourceIdentityktor 插件cf_clearance绑定 User-Agent。旧版 HTTP 侧用 ktor 通用BrowserUserAgent()与 CEF/WebView 真实 UA 不一致导致 cookie 同步正确也可能被再次挑战独立隐患见附录末条。新设计在 solve 成功时记录browser.userAgentWebSourceIdentityRegistry插件WebSourceIdentityFeature见 WebSourceHttpFeatures.kt对该 host 的后续请求覆写User-Agent保证 HTTP 侧身份与清掉挑战的浏览器一致。扩展点自动解决与备用取数原文档 v1 范围声明v1 不实现任何自动解决本节定义两个预留接缝使未来接入成为纯增量WebSessionManager构造时接收两个列表v1 均注入空列表接入时只需注册实现manager 与平台层不改动纯 HTTP 策略除外。从当前仓库源码结构看这两个接缝的实现文件已存在ImageCaptchaSolver、GirigiriSearchRoute、ONNX 识别器可作为后续演进参考。接缝一CaptchaSolver —— 自动解决策略链自动与交互的唯一区别是谁在驱动浏览器自动是一串策略依次尝试交互是最后兜底的人肉 solver。solve(interactive false)遍历solvers列表按canAttempt过滤 → 从便宜到贵依次attempt→ 首个让evaluate返回Ok的策略胜出cookie 同步进 jar → 全部失败则上报CaptchaRequired。所有策略共用同一个PageEvaluator判定成功因此自动解成功与解完能继续搜索恒等。接口定义见 CaptchaSolver.ktinterface CaptchaSolver { val id: String /** 便宜的预判 (按 kind / host 允许表) */ fun canAttempt(reason: BlockReason.Captcha, host: String): Boolean /** 尝试解决; 成功与否由 ctx.evaluate 判定 */ suspend fun attempt(ctx: SolveContext): SolveOutcome } class SolveContext( val request: SolveRequest, // url / expectation / kind / mediaSourceId val http: ScopedHttpClient, // jar 背书、UA 已对齐 val acquireBrowser: suspend () - CaptchaBrowser, // 懒创建; 纯 HTTP 策略永不调用 val evaluate: suspend (LoadedPage) - PageVerdict*, // 唯一真相来源 )原文档列出的候选策略按便宜→贵排序纯 HTTP 图片验证码MacCMS不建浏览器——请求验证码图 → recognizer → POSTverify_check→evaluate验证搜索页手动维护PHPSESSID会话。不依赖CaptchaBrowser因此 iOS 等无浏览器平台也能跑。仓库中 ImageCaptchaSolver.kt 即包含该协议实现/index.php/ajax/verify_check?typesearchverify...挑战自动通过Cloudflare/Turnstile无头浏览器等 JS 挑战。Image/Slider 不触发浏览器 DOM 图片验证码截图/填写/提交作为 HTTP 模式不适用时的后备。接入所需的三处增量除此之外不动现有代码recognizer 注入点ImageCaptchaRecognizerONNX每端各自 runtime是注入 solver 的叶子只做图片字节 → 四位数字不重试、不操作页面、不判成败——这些留在 commonMain 的策略里保证平台一致。仓库已有 ImageCaptchaModel.kt随应用交付的captcha-v1.0ONNX 模型Android/Desktop 以字节形式交给 ONNX RuntimeiOS 走文件路径以及三端识别器 AndroidOnnxImageCaptchaRecognizer.kt、DesktopOnnxImageCaptchaRecognizer.kt、IosOnnxImageCaptchaRecognizer.ktDOM 驱动浏览器 DOM 类策略需要给CaptchaBrowser增加一个suspend fun executeJavaScript(script): String?。纯 HTTP 策略不需要任何平台改动注册把策略实例放进solvers列表通过 DI 注入 manager。为何这样切分原文档指出旧方案中为绕开永不过期的solvedResults缓存专门给图片验证码加了特判分支新架构无该缓存每次 solve 都现场验证特判根本不需要——这正是新架构无陈旧缓存目标的价值体现。同理用hasSearchResultPage判定图片验证码是否解开正是PageEvaluator的解析优先原则接入时统一走ctx.evaluate一个函数。接缝二SearchRoute —— 备用取数路由备用取数不是解验证码而是换一条路取数在 HTML 搜索页被验证页挡住时改走站点公开 JSON API再把结果转成 selector 认得的 HTML 形状。这类路由建模为fetchPage在发起常规请求之前查询的SearchRoute列表命中 host 的路由直接取数并交给PageEvaluator绕过验证码而非解决它未命中或路由失败则回落到常规请求 → 验证码流程。接口定义于 CaptchaSolver.kt#L76仓库已有具体实现 GirigiriSearchRoute.kt。路由是硬编码的 per-site 适配host 允许表 手写 HTML 拼接隔离在这一层不污染通用解析与 solver 抽象。Fetch 状态与 UIMediaSourceFetchState新增RateLimited(retryAt)异常体系改为BlockedException(reason)MediaFetcher按 reason 映射状态。源码见 MediaSourceFetchState.kt含CaptchaRequired与RateLimited两个状态类与 MediaSourceFetchResult.kt验证码 chip 点击链路resolveCaptcha→solve(interactive true)→ Solved 则 restart 该源。因为 solve 入口无缓存点击必然弹框限流chip 显示限流中 · Xs 后自动重试到点自动 restart 一次不弹浏览器iOSv1无CaptchaBrowser实现isInteractiveSupported false。UI 不显示处理验证码按钮改为提示此源需要网页验证当前平台暂不支持请在桌面 / Android 端使用或更换数据源fetch 快速失败并携带明确 reason。未来接 WKWebView实现CaptchaBrowser即获得交互能力纯 HTTP 类 solver 落地后甚至能在 iOS 无头自动解决无需浏览器。生命周期规则事件行为EpisodeViewModel.onCleared/ 退出编辑源页cancelAutoSolves()— 只取消进行中的 auto-solve暖会话闲置超过 TTL / LRU 淘汰释放浏览器资源solve 成功后同 host 再次 Blocked自动失效丢弃暖会话 相关 cookie导航 / 换集不影响暖会话与 cookie测试策略commonTest 全覆盖 平台真机冒烟核心逻辑全部在 commonMain用FakeCaptchaBrowser脚本化页面序列在 commonTest 覆盖。关键用例均对应旧版真实故障启发式误报 selector 可解析 →Ok解析优先cookie 过期后站点再次挑战 → interactive 必定弹框无陈旧缓存路径solve 成功后立刻又 Blocked → 自动失效 → 下次 solve 从干净状态开始HTTP 429 →RateLimited不创建浏览器同 host 并发 solve → single-flight只弹一个对话框闲置 TTL 回收 →close()被调用防泄漏回归jar 域后缀匹配UA 覆写只作用于已 solve 的 host空 solver 列表v1solve(auto)立即失败并上报CaptchaRequired不创建浏览器。对应测试文件PageEvaluatorTest.kt、WebSessionManagerTest.kt、WebCaptchaDetectorTest.kt、WebCaptchaSupportTest.kt。平台侧用desktop-ui-verify/android-ui-verifyskill 各做一次真机冒烟真实对话框弹出 → 解决 → restart 成功。扩展点落地时补充假 recognizer 脚本化 solver验证成功走PageEvaluator与交互同一判据、失败链式回落、全失败上报CaptchaRequired命中 host 的SearchRoute绕过验证码直接取数。实施顺序每步独立可发布。v1 到第 4 步为止交付用户交互解决验证码的完整能力。步骤内容消灭的问题见附录编号1PageEvaluatorBlockReason 检测器收紧引擎接入兼容旧 coordinator1、4、5、122CookieJarFeatureWebSourceIdentityUA 对齐删反射11、UA 隐患3WebSessionManager含空solvers/searchRoutes接缝 两端CaptchaBrowser删旧 coordinatorUI 状态接入2、3、6、7、8、9、134iOS 降级 UX、死代码清理、测试迁移10扩展点不属于 v1未来按需注册CaptchaSolver/SearchRoute实现即可接入自动解决——manager 与平台层无需改动DOM 类 solver 需给CaptchaBrowser加executeJavaScript。随步骤 3/4 删除的旧代码WebCaptchaCoordinator及两端实现、WebCaptchaSearchProbe、selectSolvedSessionKey/solvedByMediaSource、NoopWebCaptchaCoordinator、WebCaptchaCoordinatorHolder、三端WebCaptchaCookieStorage.*反射实现。附录旧实现的问题与重写对照重写前2026-07对旧WebCaptchaCoordinator体系的审查结论编号与上文实施顺序表对应。严重问题判定标准不一致导致死循环solve 成功由 selector probe 判定但解决后重试路径parseSearchResult只看启发式检测器。被误报的正常页面 → solve 成功但重试永远失败无限循环陈旧缓存不可恢复solvedResults永不过期且tryAutoSolve/solveInteractively入口命中缓存直接返回旧Solved。cookie 过期后站点再次挑战时用户点处理验证码瞬间返回旧缓存对话框不弹resetSolvedSession在整个 App 无调用方只能重启 AppEDT 阻塞 空 cookie桌面端页面观察者回调CEF 在 EDT 上触发内runBlocking收集 cookie而收集又需向 EDT 投任务 → 每 URL 1s 超时 ×4 ≈ 4s UI 冻结且收到的 cookie 为空限流被包装成验证码429 被按验证码处理真限流时弹浏览器也不可能通过用户被误导去解一个不存在的验证码检测器误报面过大captcha (img || verify)兜底规则几乎命中所有提到 captcha 的正常页面条目页无 probe 兜底误报导致条目被静默跳过。架构 / 生命周期问题桌面 auto-solve 成功即销毁会话rememberSolved放回 map 后finally { disposeSession }又移除并 cancel之后每次solved 会话页面加载都新建整个 CEF browser一次搜索 1N 次浏览器创建销毁且 solved 后永远走浏览器加载无退出机制cancelAutoResolutionRequests名不副实且两端不一致桌面端把交互解决后特意保留的会话连同 solvedResults 一起清掉换集即失效Android 端根本没有 override空实现Android 端线程安全与泄漏三个注册表 map 无锁并发读写WebView 从不destroy()平台行为不一致AndroidgetSolvedCookies只查精确 key桌面走 fallback 查找注入播放器的 cookie 两端可能不同iOS 空架子Noop coordinator no-op cookie 存储但 UI 共享——用户看到验证码 chip点击无任何反应、无提示。健壮性问题反射写 ktor 私有字段storeCaptchaCookies反射拿HttpCookies.storagektor 升级即碎、失败静默cookie 丢失 domain/expiry 属性域级 cookie 覆盖不到子域detectCaptchaKindFromBlockedResponse存在?: if (...) Unknown else Unknown死分支交互解决单槽位interactiveSolveState只有一个并发两个源需要交互验证时后者顶掉前者的对话框。另有一个独立隐患HTTP 侧 UAktorBrowserUserAgent()与浏览器真实 UA 不一致cf_clearance绑定 UA即使 cookie 同步正确也可能被再次挑战——这正是新架构WebSourceIdentityUA 对齐通道要解决的问题。小结Ani 的验证码处理架构重写本质上是把验证码是否解决从一套分散、可缓存的启发式判断收敛为一个可现场验证的唯一判决函数PageEvaluator并把浏览器从常驻渲染路径降级为按需创建的逃生通道。六条设计目标中的解析优先无陈旧缓存身份一致性三者互相咬合配合 host 级会话注册表、60 秒粘滞窗口、5 分钟闲置 TTL 与 single-flight 的 solve 语义使得系统在面对 Cloudflare / Turnstile / 图片验证码 / 限流等混合场景时既能自愈站点停止挑战后自动回到直连又不存在任何不可恢复的失效状态。对于希望接入自动识别能力的场景CaptchaSolver与SearchRoute两个接缝保证了纯增量演进。【免费下载链接】animation-garden集找番、追番、看番的一站式弹幕追番平台云收藏同步 (Bangumi)离线缓存BitTorrent弹幕云过滤。100% Kotlin/Compose Multiplatform项目地址: https://gitcode.com/gh_mirrors/an/animation-garden创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考