From c38ac2a85ce7e57de4133114f85311b23a363cc0 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 30 Sep 2026 21:26:54 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20CF-2=20=E5=87=BA=E5=8F=A3=E8=A1=A8?= =?UTF-8?q?=E3=80=81Cloudflare=20=E7=BA=A7=E8=81=94=E8=BF=90=E7=BB=B4?= =?UTF-8?q?=E4=B8=8E=E6=8F=92=E4=BB=B6=E7=82=B9=E5=87=BB=E6=A0=B8=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 10 +++++ docs/design/cloudflare-handling.md | 5 +++ docs/plan/00-first-version-plan.md | 2 + docs/runbook/agent.md | 2 +- docs/runbook/browser.md | 38 ++++++++++++++-- docs/runbook/sites.md | 69 +++++++++++++++++++++++++++++- 6 files changed, 120 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70b940f8..2123f2ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ ## [Unreleased] +### 新增(CF-2 Cloudflare 验证处理) +- 站点请求遇 Cloudflare 挑战(`cf-mitigated: challenge`,或 403 / 503 / 429 的挑战页)不再算凭据错误:不置账号失效、不开「登录失效」事项、不排自动重登;503 挑战不再算维护;健康检测记「需要人机验证」。 +- 过挑战级联(只处理有登录态账号的站点;同站 30 分钟最多一次、同一时刻只处理一次;不接打码服务):浏览器用该账号的 profile 打开同一地址 → 等它自己过(≤ 15 秒)→ 「我是人类」复选框 / Turnstile 由系统发起视觉子流程点一下(最多 2 次,预算取设置,自动化总开关关闭时跳过)→ 选图 / 输码类转人工,待处理事项「<站点>需要你在原站视图里过一次验证」,接管过完交还即继续。 +- 过了之后读回 `cf_clearance` 与浏览器 UA:账号 UA 跟随浏览器且两端出口一致时交给 HTTP(`__cf_bm` 仍不带),重试 1 次,按站点记复用是否有效;仍被挑战时这次页面请求改由浏览器导航取 HTML 交给原解析器,之后 6 小时该站页面请求直接走浏览器,到期再试 HTTP。 +- 站点卡片 / 概览显示「Cloudflare:自动通过 / 浏览器代理中,至 hh:mm / 需要你验证」,原始记录进技术信息。 +- camofox 插件新端点 `POST /umt/tabs/{tabId}/mouse-click`:`click_at` 发真实鼠标事件,跨域 iframe 里的 Turnstile 也点得到(需要重建 camofox 镜像)。 + +### 变更(CF-2 出口统一) +- 站点请求(适配器、RSS、登录、用户数据)不再隐式继承容器的 `HTTP(S)_PROXY`:站点没开代理明确直连,开了走站点 / 全局代理,与浏览器会话同一出口。TMDB、Brave、图片缓存等外网请求照旧按环境变量走代理,改为代码里显式指定。出口表见 `docs/runbook/sites.md`「出口」。 + ## [0.3.4] - 2026-09-30 ## [0.3.3] - 2026-09-30 diff --git a/docs/design/cloudflare-handling.md b/docs/design/cloudflare-handling.md index e5aef947..21958764 100644 --- a/docs/design/cloudflare-handling.md +++ b/docs/design/cloudflare-handling.md @@ -1,5 +1,10 @@ # CF-1 Cloudflare 验证处理(调研) +> 状态:CF-2 已按本文 §3 推荐方案 A→B 级联落地,另加出口统一(站点请求明确直连 / 站点代理)。运维与字段见 +> `docs/runbook/sites.md`「出口」「Cloudflare(CF-2)」、`docs/runbook/browser.md` §13。与本文的差异:同步 cf_clearance 单独存 +> `site_account.user_info.cloudflare`(值在 secret),普通浏览器回写仍剔除;视觉子流程最多 2 次;`site.config.cloudflare` 多了 +> `lastAttemptAt / lastSolvedAt / pending / sessionId / lastResult / lastRule / lastError`。 + ## 1. 复现(2026-09-30,HTTP 5 次、浏览器 5 次,间隔 ≥15s) | 请求 | 结果 | | --- | --- | diff --git a/docs/plan/00-first-version-plan.md b/docs/plan/00-first-version-plan.md index 39c2b153..13e1beab 100644 --- a/docs/plan/00-first-version-plan.md +++ b/docs/plan/00-first-version-plan.md @@ -212,3 +212,5 @@ NFO / 图片投影、身份巡检与挂载根因聚合、HTTP 失败自动切浏 - **健康检查误判(HC-1,用户 2026-09-30 接观众站)**:"测试"登录成功但健康检测报"未登录"——健康检查抓 index.php 找 logout 链接,与用户数据解析两套判定打架;改为以用户数据解析成功为准并细化失败原因。 - **观众详情页适配核对(DET-2)**:用真实详情页(≤3 次请求)逐项核对并修,脱敏样本进差分测试。 - **Cloudflare 验证处理(CF-1,用户 2026-09-30)**:搜索入口有时随机出 CF 验证。先用浏览器复现并研究:HTTP 端遇 CF 挑战时的处理策略(浏览器过挑战后同步 cf_clearance+UA 重试 / 该请求改由浏览器执行并解析 / 站点级"走浏览器"标记与时效),对照 MP(FlareSolverr / CloakBrowser)与 BR-4 的固定身份,出方案后实现。 + **CF-2 已实现(分支 cf2,待合并)**:出口统一(站点请求明确直连 / 站点代理)、挑战不算凭据错误、浏览器过挑战级联(自动 / 视觉子流程 / 转人工)、 + cf_clearance 有条件给 HTTP 重试、浏览器代抓与 6 小时浏览器代理期、站点页展示;见 `docs/runbook/sites.md`「Cloudflare(CF-2)」。 diff --git a/docs/runbook/agent.md b/docs/runbook/agent.md index e017cae1..452e6016 100644 --- a/docs/runbook/agent.md +++ b/docs/runbook/agent.md @@ -13,7 +13,7 @@ | 查询(read) | `catalog.lookup`、`release.search`、`release.inspect`、`inventory.query`、`subscription.list`、`decision.list`、`download.list`、`download.inspect`、`downloader.list`、`task.inspect`、`site.list`、`site.user_data` | | 计划(preview / propose) | `identity.resolve`、`download.plan`、`organize.plan`、`policy.preview` | | 执行(execute) | `download.submit`、`download.recheck`、`organize.execute`、`task.retry`、`task.cancel`、`policy.apply`、`site.health_check`、`site.refresh_login` | -| 原站浏览器 | `browser.snapshot`、`browser.navigate`、`browser.interact`、`browser.download`、`browser.screenshot`、`browser.click_at`、`browser.type_at`、`browser.scroll`、`browser.wait`、`browser.solve_challenge`(见 [browser.md](browser.md)) | +| 原站浏览器 | `browser.snapshot`、`browser.navigate`、`browser.interact`、`browser.download`、`browser.screenshot`、`browser.click_at`、`browser.type_at`、`browser.scroll`、`browser.wait`、`browser.solve_challenge`(见 [browser.md](browser.md));站点请求遇 Cloudflare 复选框时系统也会发起 `solve_challenge` 视觉子流程(发起人 `system:cloudflare`、trigger event、没有父会话,预算取设置,见 browser.md §13) | | 会话 | `agent.plan`、`agent.propose_options`、`agent.spawn`、`agent.await` | | 网页搜索 | `web.search`、`web.fetch`(见 [websearch.md](websearch.md)) | | 元数据补全 | `metadata.enrich`(propose,只出建议;网页搜索开启才提供,见 [identity.md](identity.md#元数据补全meta-1)) | diff --git a/docs/runbook/browser.md b/docs/runbook/browser.md index 67d080b6..7f5ed003 100644 --- a/docs/runbook/browser.md +++ b/docs/runbook/browser.md @@ -126,8 +126,9 @@ execute 类需要持续授权(`automation_grant`)或审批。元素引用只来 不支持时截图不发送,能力改给已脱敏的文字快照并提示;`solve_challenge` 直接判定需人工。fake 上游按支持图片处理。 - **坐标系**:服务端对每次截图读页面视口与 DPR(`evaluate`),`页面 CSS 像素 = 截图像素 × 视口宽 / 截图宽`; 输出的 `coordinates` 记录截图尺寸、原始尺寸、视口、DPR(取不到时 `assumed = true`,按原图、DPR 1)。动作之后旧坐标系作废, - 必须用新截图的坐标。点击沿用 1.17.0 的做法:`elementFromPoint` 打标记再按选择器点,**画布(canvas)内的位置、跨域 iframe - 里的控件(如 Cloudflare Turnstile)点不到具体位置**,这类挑战实际只能转人工。 + 必须用新截图的坐标。点击(CF-2 起)先走 umt 插件的 `POST /umt/tabs/{id}/mouse-click`——Playwright `page.mouse.click` 发真实鼠标事件, + **跨域 iframe 里的控件(Cloudflare Turnstile)和画布也点得到**;插件没有这个端点(旧镜像、纯截图镜像)时退回 1.17.0 的做法: + `elementFromPoint` 打标记再按选择器点(跨域 iframe / 画布点不到具体位置)。 - **动作前提**:会话在自动化控制下(人工接管 / 等待处理中一律拒绝),当前页面主机在站点域名白名单内。 - **敏感截图**:登录页地址、页面上有密码框、地址带凭据 → `sensitive = true`,默认不交给模型(不做像素遮蔽),改给已脱敏的文字快照; 要交给模型需要 `automation_grant.action = browser.sensitive_screenshot`(或 `*`)的持续授权,且调用时传 `allowSensitive: true`。 @@ -368,6 +369,7 @@ camofox 1.17.0 只有 `POST /sessions/:userId/cookies`(导入),而且要另一 | `GET /umt/instance` | `{instanceId, startedAt}`:插件加载时生成,camofox 进程重启即变(BR-3 重启检测,§10) | | `GET /umt/identity` | 固定浏览器身份的公开部分(UA / OS / 版本,BR-4,§11.1) | | `PUT /umt/sessions/{userId}/proxy` `{proxy}` | 该 profile 的出口:`{server, username?, password?}` 或 `null` 直连(BR-4,§11.5) | +| `POST /umt/tabs/{tabId}/mouse-click` `{userId, x, y}` | 按视口坐标发真实鼠标事件(`page.mouse.click`,CF-2,§13);标签页不在回 422 | 适配器先走插件;插件不在(404)再试上游导入端点(`CAMOFOX_API_KEY` 与访问凭据相同时可用,只能写不能读);都不行 → 会话照常打开, `credential.status = unsupported` 并说明。profile 落盘在 `/root/.camofox/profiles//storage-state.json` @@ -550,7 +552,7 @@ Windows 的真值);TLS / HTTP2 指纹是 Firefox 152 本身(与 OS 无关)。屏 - Cookie 账号如果一直不开原站视图、也不换 Cookie,会一直待切换(界面写"待切换:下次刷新登录,或在原站视图里用过后改为跟随浏览器")。 刷新登录对 Cookie 账号只校验,不切换。 -### 11.4 Cloudflare Cookie 仅浏览器 +### 11.4 Cloudflare Cookie 仅浏览器(CF-2 起有条件给 HTTP) `cf_clearance`、`__cf_bm`(及挑战过程中的 `cf_chl_*`)与取得它的浏览器绑定(UA + IP + TLS 指纹),Deno 的 TLS 指纹与 Firefox 必然不同, HTTP 端拿去用多半无效还可能触发风控。所以: @@ -559,6 +561,9 @@ HTTP 端拿去用多半无效还可能触发风控。所以: - 比较"有没有变化"时也不算它们(CF Cookie 轮换不会触发回写);refresh-login 从浏览器读回的 Cookie 同样剔除; - 站点账号页技术信息里显示"仅浏览器使用的 Cookie"; - 用户手填的 Cookie 里带 `cf_clearance` 不处理(用户自己的选择)。 +- **CF-2 例外**:站点遇 Cloudflare 挑战、浏览器过了之后,账号 UA 来源 = 跟随浏览器、浏览器 UA 与 HTTP UA 相同、两端出口相同时, + `cf_clearance`(只有它,`__cf_bm` 仍不带)单独存进 `site_account.user_info.cloudflare`(值在 secret)并由 HTTP 带上重试 1 次, + 结果按站点记 `cfReuse`;普通回写仍剔除它(见 sites.md「Cloudflare(CF-2)」)。 ### 11.5 出口 IP 按站点统一 @@ -642,3 +647,30 @@ camofox 按 **API 调用**判断活动,有三道空闲回收(`docker-compose.yml 核验:`apps/server/src/modules/browser/__tests__/browser-keepalive.test.ts`(登记 / 引用计数 / 复查;对 mock camofox:接管期间保活、 不记自动化活动、交还后停;桌面连接期间标签页被回收 → 保活时自愈重连;智能体会话结束后停、会话关闭撤销)。真机未核验。 + +## 13. Cloudflare 挑战(CF-2) + +站点 HTTP 遇挑战时由站点请求一侧(sources-identity `CloudflareGate`)经钩子调用浏览器模块,流程与状态见 sites.md「Cloudflare(CF-2)」。 +浏览器这边: + +- `solveSiteChallenge({siteId, accountId, url})`:该账号 profile 的自动化会话(没有就按站点打开;人工接管中 / 等人处理中不打扰)导航到 + 同一地址(取站点令牌)→ 用 `POST /tabs/{id}/evaluate` 读页面(地址、标题、outerHTML、正文样本、`navigator.userAgent`,适配器 + `pageContent`)按 `classifyChallengePage` 分类:自动(JS 检查)/ 可点(Turnstile、「确认您是真人」)/ 真验证码(hCaptcha、reCAPTCHA、 + 选图、输码)→ ① 每 1.5 秒读一次,最多 15 秒 → ② 可点的派视觉子流程(`browser.solve_challenge`,系统发起:发起人 `system:cloudflare`、 + trigger event、没有父会话;步数 4、时长 ≤ 120 秒、token / 费用取 `agent.maxTokensPerRun` / `maxCostUsdPerRun`;最多 2 次; + 自动化总开关关闭或只读对账时不派)→ ③ 没过:会话转等待 `cf_challenge`,待处理事项「<站点>需要你在原站视图里过一次验证」 + (视觉子流程失败留下的 `challenge_unsolved` 事项改成这个说法)。通过后读回 profile 里的 `cf_clearance`、UA 与会话出口(`recovery_state.egress`, + 插件按站点下发出口成功才有值)。 +- `fetchSiteHtml({siteId, accountId, url})`:同一会话导航(取站点令牌)后读 outerHTML;仍是挑战页 / 会话在人工处理 / 站点停用时不返回页面。 +- 同一 profile 的过挑战与代抓在进程内串行(profile 锁);跨进程同站 30 分钟一次靠站点状态的条件更新。 +- 交还会话(给自动化)时:站点在等人过验证 → 读回 `cf_clearance` / UA 通知站点请求继续(`CloudflareGate.afterHuman`),待处理事项随交还关闭。 +- 插件新端点 `POST /umt/tabs/{tabId}/mouse-click`:`click_at`(与原站视图截图模式的坐标点击)都先走它。**需要重建 camofox 镜像**才有 + (`docker compose build camofox && docker compose up -d camofox`,只重跑插件层);没重建时退回 elementFromPoint,Turnstile 点不到, + 可点类挑战会转人工。 + +核验:mock camofox(`cloudflare` 模式:auto / interactive / captcha)见 `browser-cloudflare.test.ts`、`umt-plugin.test.ts`、E14。 +插件鼠标点击的真实容器核验见下表(本地临时容器,只开本地页面,不访问真实验证页)。 + +| 日期 | 做法 | 结果 | +| --- | --- | --- | +| 2026-09-30 | 临时容器 `umt-cf2-camofox`(镜像 `umt-browser-desktop:1.17.0`,只读挂载新插件目录),容器内 node 起两个本地页面:父页 `localhost:8001`(标题 Just a moment...)内嵌**跨域** iframe `localhost:8002`(复选框,`change` 事件 `isTrusted` 才向父页 postMessage);适配器 `openSession` → `pageContent` → `click({x, y})` → `pageContent` | 插件路径:`POST /umt/tabs/{id}/mouse-click` 命中复选框,iframe 里收到可信事件,父页标题变为 passed;`pageContent` 读到 UA `…Windows NT 10.0; Win64; x64; rv:152.0…`。旧路径(elementFromPoint)只能标到 iframe 元素本身、按元素中心点击,点不到指定坐标。未访问任何外部站点,容器已删除 | diff --git a/docs/runbook/sites.md b/docs/runbook/sites.md index 98c993ad..448c3696 100644 --- a/docs/runbook/sites.md +++ b/docs/runbook/sites.md @@ -97,7 +97,7 @@ | `defaultHealthCheckIntervalSec` | 21600 | 定时健康检测的最短间隔(与定义取较长) | | `defaultDetailCacheSec` | 86400 | 详情缓存:期内 `GET /sites/{id}/releases/{rid}?refresh=true` 与 `release.inspect` 不再请求站点(与定义取较长);0 = 不缓存 | | `userAgent` | 空(跟随浏览器) | 全局 UA 覆盖:空 = 站点请求跟随浏览器固定身份的 UA(Windows Firefox);填了则账号没手动 UA 时都用它。优先级:账号手动 > 待切换 > 全局 > 跟随浏览器,见 browser.md §11.2 | -| `proxyUrl` | null | 站点 `proxy=true` 时,适配器、登录、用户数据、RSS 请求都经此代理(`Deno.createHttpClient`),浏览器会话也走同一代理(browser.md §11.5);地址要让 app 与 camofox 容器都能连 | +| `proxyUrl` | null | 站点 `proxy=true` 时,适配器、登录、用户数据、RSS 请求都经此代理(`Deno.createHttpClient`),浏览器会话也走同一代理(browser.md §11.5);没开代理的站点**明确直连**,不读容器的 `HTTP(S)_PROXY`(见下「出口」);地址要让 app 与 camofox 容器都能连 | | `userDataMaxSeedingPages` | 10 | 用户数据做种分页上限 | 运行时每 30 s 最多重读一次设置;设置变化会让站点适配器按新签名重建。 @@ -136,9 +136,74 @@ - 注册数:首批能力 17 → 21,启动断言 `assertCapabilityCount` 跟随 `CAPABILITY_IDS.length`。 - 未接:`agent/invoke-limits.ts` 的站点并发上限还没把 `site.health_check` / `site.refresh_login` 算作外部站点调用;Agent 默认只读白名单(`agent/runner.ts`)也还没加 `site.list` / `site.user_data`。两处都在 agent 模块,留给该模块的负责人。 +## 出口:哪些请求走哪条(CF-2) + +代码 `apps/server/src/egress.ts`。之前 app 的站点请求用 `globalThis.fetch`,Deno 会读容器的 `HTTPS_PROXY`(本机栈 override 里的 mihomo 中继), +站点没开代理时 HTTP 端走代理、浏览器却直连,两端出口 IP 不同(CF-1 实测 HKG / DFW),这是 Cloudflare 随机拦截的来源之一。 + +| 请求 | 出口 | +| --- | --- | +| 站点请求:适配器(列表 / 搜索 / 详情 / 下载 / 健康检测)、RSS、账密登录、用户数据(`SourcesRuntime.siteFetch`) | 站点开「使用代理」且设了 `sites.proxyUrl` → 该代理;否则**明确直连** | +| 浏览器会话(camofox) | 同一站点同一出口:插件按 profile 设代理或直连(browser.md §11.5);`UMT_BROWSER_PROXY_MODE=env` 时按容器环境变量(与 HTTP 不一定一致) | +| TMDB、Brave 搜索 / 网页读取、TMDB 图片缓存、海报与站点截图代理 | 照旧按环境变量 `HTTPS_PROXY` / `HTTP_PROXY` / `ALL_PROXY` / `NO_PROXY`(进程启动时读一次),改为代码里显式建客户端(`externalFetch`),行为不变 | +| 模型上游(pi SDK)、下载器、媒体服务器、camofox 本身 | 不在本表范围,仍是各自的 fetch(模型上游随环境变量) | + +**Deno 2.9 的语义**(2026-09-30 本机实测,`test/sources-identity/egress.test.ts` 锁住):缺省 `fetch` 与 `Deno.createHttpClient({})` 都读 +`HTTP_PROXY` / `HTTPS_PROXY` / `ALL_PROXY`(大小写都认)与 `NO_PROXY`,读取发生在**客户端创建时**;`createHttpClient` 没有"不走代理"的选项, +`NO_PROXY=*` 不认;显式 `proxy` 排在环境变量代理前面,但 `NO_PROXY` 命中的主机连显式代理也绕过。所以直连客户端是在创建那一刻临时拿掉 +这几个环境变量(同步调用,同一 isolate 内不会插进别的代码)、建完立即还原;代理客户端同样在拿掉后创建,只走给定代理。 + +排查:站点 HTTP 与浏览器是否同出口,在 app 容器里 `curl` 不能代表 Deno(curl 也读环境变量);看站点页技术信息里的 Cloudflare 记录, +或临时对一个会回显来源 IP 的地址各发一次(不要对 PT 站做)。 + +## Cloudflare(CF-2) + +方案见 `docs/design/cloudflare-handling.md`。代码:`packages/adapters/src/sites/challenge.ts`(识别)、 +`apps/server/src/modules/sources-identity/service/cloudflare.ts`(级联,`CloudflareGate`)、`modules/browser/service.ts` +(`solveSiteChallenge` / `fetchSiteHtml`)、`modules/agent/challenge.ts`(`systemChallengeVision`)。 + +**识别**:站点响应带 `cf-mitigated: challenge`(任何状态码),或 403 / 503 / 429 且正文是挑战页(标题 Just a moment / Attention Required、 +`cf_chl_opt` 等,不认正常页面也会注入的 `/cdn-cgi/challenge-platform/`)→ `SiteError("challenge")`。**不是凭据错误**:快照 / 详情不调 +`onAuthError`、不置账号失效、不排自动重登;健康检测记 `challenge`(degraded);刷新登录 / 用户数据遇挑战只写 `last_error`,账号状态不变 +(验证码、二次验证仍是账号状态 `challenge`)。 + +**级联**(站点请求经 `siteFetch` 自动走,调用方无感): + +1. 站点在浏览器代理期(`config.cloudflare.browserUntil` 未到)且请求是页面(GET + Accept `text/html`)→ 直接由浏览器代抓,不发 HTTP; +2. 发 HTTP;满足复用条件时带上浏览器拿到的 `cf_clearance`(`__cf_bm` 不带); +3. 遇挑战 → 记 `lastChallengeAt` / `lastRule`;只处理**有登录态账号**的站点(第一个启用、未失效、确有 Cookie 的 Cookie / 账密账号); +4. 过挑战:同站 30 分钟最多一次(`lastAttemptAt` 条件更新,跨进程)、进程内同一时刻只跑一次,浏览器用该账号 profile 打开同一地址: + ① 轮询页面等它自己过(≤ 15 秒)→ ② 「我是人类」复选框 / Turnstile:派视觉子流程点一下(系统发起、没有父会话,预算取设置,最多 2 次; + 自动化总开关关闭或只读对账时跳过)→ ③ 选图 / 输码类或仍没过:会话转等待(`cf_challenge`),待处理事项「<站点>需要你在原站视图里过一次验证」, + 站点记 `pending = human`。不接任何打码服务; +5. 通过 → 读回 `cf_clearance` 与 `navigator.userAgent`;**只有账号 UA 来源 = 跟随浏览器、UA 相同、两端出口相同**(浏览器出口按站点下发成功才算) + 时才存进账号(`site_account.user_info.cloudflare = {secretRef, userAgent, egress, at}`,值在 secret 里),HTTP 重试 1 次:过了记 + `cfReuse = ok`,仍被挑战记 `cfReuse = fails`(之后跳过复用;UA 或出口变了自动清掉重试); +6. 仍被挑战 → 页面请求改由浏览器导航取 outerHTML 交给原解析器(受站点限流令牌与 profile 锁约束),站点记 6 小时浏览器代理期,到期再试 HTTP; + 下载 .torrent、RSS、POST 这类不能代抓的请求照常报挑战。 + +人在原站视图里过完验证、交还会话(交还给自动化)→ 读回 `cf_clearance` / UA,清 `pending`,记 `lastSolvedBy = human`,并允许马上再试(清 +`lastAttemptAt`)。 + +`site.config.cloudflare` 字段(契约 `SiteCloudflareState`):`browserUntil`、`cfReuse`(ok / fails)、`lastChallengeAt`、`lastSolvedBy` +(auto / vision / human)、`lastSolvedAt`、`lastAttemptAt`、`pending`(human)、`sessionId`、`lastResult`、`lastRule`、`lastError`。 +改这个键不会让站点适配器重建。 + +**界面**:站点卡片与站点概览「站点」一栏显示一行 Cloudflare 状态(没遇到过挑战不显示):「需要你验证」(卡片上可点,进原站视图)/ +「浏览器代理中,至 hh:mm」/「自动通过」/「没能通过」;原始记录进概览的技术信息。 + +**测试**:`packages/adapters/src/sites/challenge.test.ts`(分类)、`apps/server/test/sources-identity/egress.test.ts`(出口)、 +`cloudflare.test.ts`(级联:自动通过 / 视觉 / 转人工 / 复用失败回退浏览器 / 节流单飞 / 同步条件 / 健康)、 +`modules/browser/__tests__/browser-cloudflare.test.ts`(mock camofox:自动放行、插件真实鼠标点击、选图转人工、交还继续、代抓与 profile 锁)、 +`test/e2e/e14-vision.test.ts`(系统发起的视觉子流程)。真机:未对真实站点验证(CF-2 只用 mock);插件鼠标点击见 browser.md §13。 + +**要人处理时**:站点页 Cloudflare 行显示「需要你验证」→ 原站视图接管 → 过验证 → 交还。一直过不去(站点绑定 TLS 指纹、代理 IP 被拉黑): +关掉站点代理或换出口;实在不行关闭该站点的定时快照,只保留 RSS。 + ## 待核验 1. takelogin 表单字段、二次验证字段名、错误文本全部只对 `@umt/testkit` mock 站点验证过,未连接真实站点。 2. 失败文本归类是关键词匹配(`classifyLoginError`),真实站点的措辞可能落到 `unknown`(状态记为 expired,原文截 200 字写进 `last_error`)。 -3. Cloudflare 站点只识别并转待处理,不做浏览器登录。 +3. Cloudflare:识别与级联见上「Cloudflare(CF-2)」,只对 mock 验证过;真实站点的挑战页类型、cf_clearance 能否给 HTTP 复用(是否绑 TLS) + 按站点记 `cfReuse` 在线验证。账密登录遇挑战仍不做浏览器登录。 4. 馒头用户数据:`MTorrent` 解析器的做种列表首页按 GET 请求,真实 API 可能要求 POST。