fix(sites): Cloudflare 挑战不算凭据错误(CF-2)

- http.ts:cf-mitigated: challenge 或 403/503/429 挑战页 → SiteError(challenge),503 不再算维护、403 不再落到 auth_invalid
- 挑战页分类 classifyChallengePage:自动 / 可点(Turnstile)/ 真验证码
- 刷新登录 / 用户数据遇 Cloudflare 挑战只记 last_error,账号状态不变、不开登录失效事项

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-30 21:26:53 +08:00
1 parent d403514adc
commit a11ef16e40
5 files changed
+221 -2

No files matched your search

+12 -1
View File
@@ -1,4 +1,5 @@
import { redactText } from "../rss/redact.ts";
import { detectChallenge } from "./challenge.ts";
import type { SiteRateLimiter } from "./rate-limit.ts";
import { SiteError } from "./types.ts";
@@ -20,7 +21,8 @@ function retryAfterMs(res: Response, now: Date): number | undefined {
}
/**
* 站点请求:先取站点令牌,再发请求;429/503 统一映射并顺延限流器。
* 站点请求:先取站点令牌,再发请求;Cloudflare 挑战(`cf-mitigated: challenge`,或 403 / 503 / 429 的挑战页)→ `challenge`
* (不是凭据错误,CF-2);其余 429/503 统一映射并顺延限流器。
* 返回的 Response 由调用方消费。错误消息只含脱敏后的 URL。
*/
export async function siteFetch(
@@ -42,6 +44,15 @@ export async function siteFetch(
const reason = timeout.aborted ? "超时" : (e as Error).message;
throw new SiteError(ctx.siteId, "network", redactText(`请求 ${safeUrl} 失败:${reason}`));
}
const ch = await detectChallenge(res);
res = ch.res;
if (ch.rule) {
await res.body?.cancel().catch(() => {});
throw new SiteError(ctx.siteId, "challenge", `请求 ${safeUrl}:站点要求人机验证(Cloudflare)`, {
httpStatus: res.status,
detail: { reason: "challenge", rule: ch.rule, httpStatus: res.status },
});
}
if (res.status === 429 || res.status === 503) {
const ra = retryAfterMs(res, ctx.now());
await res.body?.cancel().catch(() => {});