feat(sites): 出口统一与 Cloudflare 过挑战级联(CF-2)
- 出口:站点请求不再隐式继承 HTTP(S)_PROXY,没开代理明确直连(创建客户端时临时拿掉代理环境变量,Deno 2.9 语义见 egress.ts), 开了走站点代理;TMDB / Brave / 图片改为显式按环境变量(行为不变) - CloudflareGate:浏览器代理期直接代抓 → HTTP(可复用时带 cf_clearance)→ 挑战 → 浏览器过挑战(同站 30 分钟一次、单飞) → cf_clearance 仅在 UA 跟随浏览器且两端同出口时给 HTTP,重试 1 次记 cfReuse → 仍被挑战则浏览器代抓并进入 6 小时代理期 - 浏览器:solveSiteChallenge(自动放行 ≤15s / 系统发起视觉子流程最多 2 次 / 真验证码转人工待处理)、fetchSiteHtml(profile 锁 + 站点令牌)、 交还后读回 cf_clearance 继续;site.config.cloudflare 状态契约 Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
6b14b0da45
commit
16083942c0
14 files changed
+1711
-43
No files matched your search
@@ -124,6 +124,41 @@ export const SiteLastHealth = z.object({
|
||||
});
|
||||
export type SiteLastHealth = z.infer<typeof SiteLastHealth>;
|
||||
|
||||
/**
|
||||
* 站点的 Cloudflare 处理状态(CF-2,存 site.config.cloudflare;docs/runbook/sites.md「Cloudflare」):
|
||||
* - browserUntil:在此之前该站能走浏览器的页面请求直接由浏览器代抓(HTTP 仍被挑战时置 6 小时,到期再试 HTTP);
|
||||
* - cfReuse:浏览器拿到的 cf_clearance 给 HTTP 用的结果(ok 有效 / fails 无效,之后不再复用);
|
||||
* - lastSolvedBy:最近一次怎么过的挑战(auto 等它自己过 / vision 视觉子流程点了一下 / human 人在原站视图里过);
|
||||
* - pending = human:遇到选图 / 输码类验证码,等人在原站视图里过一次;
|
||||
* - lastAttemptAt:最近一次尝试过挑战(同站 30 分钟最多一次)。
|
||||
*/
|
||||
export const SiteCloudflareState = z.object({
|
||||
browserUntil: Timestamp.nullable().default(null),
|
||||
cfReuse: z.enum(["ok", "fails"]).nullable().default(null),
|
||||
lastChallengeAt: Timestamp.nullable().default(null),
|
||||
lastSolvedBy: z.enum(["auto", "vision", "human"]).nullable().default(null),
|
||||
lastSolvedAt: Timestamp.nullable().default(null),
|
||||
lastAttemptAt: Timestamp.nullable().default(null),
|
||||
pending: z.enum(["human"]).nullable().default(null),
|
||||
/** 等人处理的浏览器会话(原站视图接管入口) */
|
||||
sessionId: Id.nullable().default(null),
|
||||
/** 最近一次处理结果(技术信息):passed / reuse_ok / reuse_fails / browser / needs_human / failed / throttled / skipped */
|
||||
lastResult: z.string().nullable().default(null),
|
||||
/** 命中的挑战规则(技术信息) */
|
||||
lastRule: z.string().nullable().default(null),
|
||||
/** 最近一次失败原因(技术信息) */
|
||||
lastError: z.string().nullable().default(null),
|
||||
});
|
||||
export type SiteCloudflareState = z.infer<typeof SiteCloudflareState>;
|
||||
|
||||
/** 从 site.config 读 Cloudflare 状态;没有或格式不对返回 null */
|
||||
export function siteCloudflareState(config: unknown): SiteCloudflareState | null {
|
||||
const raw = (config as { cloudflare?: unknown } | null | undefined)?.cloudflare;
|
||||
if (!raw || typeof raw !== "object") return null;
|
||||
const r = SiteCloudflareState.safeParse(raw);
|
||||
return r.success ? r.data : null;
|
||||
}
|
||||
|
||||
export const Site = EntityBase.extend({
|
||||
key: z.string().regex(/^[a-z][a-z0-9_-]*$/),
|
||||
name: z.string().min(1),
|
||||
|
||||
Reference in new issue
Block a user