域名状态机¶
网站抓取多阶段决策的智能记忆层 — 每个 domain 独立持久化状态,按行为自动转换,按 TTL 自动重置。
6 种状态¶
┌─────────────┐
│ unknown │ (默认 / 首次)
│ 完整 pipeline│
└──────┬──────┘
│
┌────────────────┼────────────────┬──────────────┐
↓ ↓ ↓ ↓
┌──────────┐ ┌─────────────┐ ┌──────────┐ ┌──────────┐
│ dead │ │ antibot-soft │ │ friendly │ │ cold │
│ DNS/SSL/404│ │ challenge 偶发│ │ 高 fetch ok│ │ 真无 contact│
│ 永远 SKIP │ │ fetch+兜底 tab│ │ 永远 fetch│ │ 永远 SKIP │
└──────────┘ └──────┬──────┘ └──────────┘ └──────────┘
│ × 3
↓
┌─────────────┐
│antibot-hard │
│ 永远 tab │
└─────────────┘
状态详解¶
| 状态 | 触发条件 | 决策行为 | TTL | 重置后状态 |
|---|---|---|---|---|
| unknown | 首次访问 / TTL 重置 | 完整 pipeline (HEAD → GET → tab fallback) | 7d | 重新评估 |
| dead | HEAD: DNS fail / SSL invalid / 404 / 410 | 永远 SKIP(不抓取) | 30d | unknown |
| antibot-soft | HEAD: cf-mitigated:challenge / 403+cf-ray / body 含 challenge marker | fetch 试 + 失败 tab fallback | 7d (有衰减) | unknown |
| antibot-hard | antibot-soft 触发 × 3 连续 / cf-mitigated:block | 永远 tab fallback(不再 fetch 浪费) | 90d | antibot-soft |
| friendly | fetch 成功率 > 70%(最近 10 次) | 永远 fetch(不 fallback tab) | 无 TTL(行为驱动) | fetch fail × 5 → unknown |
| cold | fetch+tab 都拿不到 contact × 5 次 | 永远 SKIP(除非用户强制 force) | 90d | unknown |
转换规则¶
进入 dead¶
HEAD response:
- DNS resolution fail
- TCP connection refused
- SSL certificate invalid/expired
- HTTP 404 / 410 (Gone)
进入 antibot-soft¶
HEAD response:
- cf-mitigated: challenge
- status 403/503 + cf-ray header present
- status 429 (rate limit)
GET body:
- isChallengeBody(html) === true(见 SPEC-004 关键词列表)
升级 antibot-soft → antibot-hard¶
进入 friendly¶
进入 cold¶
重置(TTL 到期)¶
任何状态 TTL 到期 → 自动重置为 unknown,清空 recent 历史。
(除 friendly:永远不主动重置,但 fetch fail × 5 自动降级)
数据结构¶
interface DomainStat {
domain: string; // 主键
state: DomainState; // 当前状态
recent: RecentEntry[]; // FIFO max 20
// 派生指标(cron 每日重算)
fetchTotal: number;
fetchOk: number; // outcome='ok-contact' 计数
fetchOkEmpty: number; // outcome='ok-empty' 计数
tabTotal: number;
tabOk: number;
// 时间戳
firstSeen: number;
lastSeen: number;
stateChangedAt: number;
stateExpiresAt: number;
}
interface RecentEntry {
at: number;
method: 'head' | 'fetch' | 'tab';
outcome: 'ok-contact' | 'ok-empty' | 'http-error' | 'network-error' | 'antibot' | 'dead';
// 可选 evidence(便于事后分析)
status?: number; // HTTP status
errorClass?: string;
}
type DomainState =
| 'unknown'
| 'dead'
| 'antibot-soft'
| 'antibot-hard'
| 'friendly'
| 'cold';
存储¶
- Key:
local:domainStats:v1 - Value:
Record<domain, DomainStat> - Max 5,000 domains(LRU 淘汰按
lastSeen) - 序列化大小:每条 ~ 500 bytes → 5k 条 ~ 2.5 MB
与云端协同(Phase 3)¶
| 时机 | 客户端动作 |
|---|---|
| 首次见 domain(unknown) | 先 GET /api/scrape/domain-state?domains=... 拿云端 state(如有) |
| 本地确认 dead / antibot-hard | POST /api/scrape/domain-state 上报 + 积分奖励 |
| 云端 state 与本地不一致 | 优先用更新的(lastSeen 新的) |
| 云端共识强度高(consensus > 0.8) | 客户端直接采纳云端 state,不必本地实验 |
详见 SPEC-004-网站采集多阶段优化-云端协同 Phase 3.4。
调试 / 可观测¶
日志字段¶
PageLogEntry {
domainState: DomainState; // 决策时的状态
domainStateExpiresAt?: number; // TTL
method: 'head' | 'fetch' | 'tab' | 'cache' | 'skip';
}
Settings 调试入口(未来)¶
「高级 → 域名状态管理」:
- 查看当前所有域名状态分布
- 手动强制某 domain 状态(如 friendly 强制 fetch)
- 一键 reset all states(数据迁移用)
- 导出 domainStats CSV
FAQ¶
Q: domain 颗粒度 vs URL 颗粒度?
A: 状态机用 domain 级(一个站通常状态一致)。但 dead 例外 — 同 domain 的不同 url 可能各自 404,所以也维护 urlContactCache 标 dead url。
Q: 子域名怎么算?
A: 默认按 eTLD+1(如 mail.example.com 和 www.example.com 都算 example.com)。除非用户开启「子域独立追踪」。
Q: 状态错误怎么纠正? A: Settings UI 提供「reset domain state」按钮,强制回到 unknown 重新评估。
Q: 状态机会让 22w 网站越抓越慢吗? A: 不会。LRU 5k 上限 + 大部分 lookup 是 O(1) hash map。每次抓取额外开销 < 1ms。
相关¶
- SPEC-004-网站采集多阶段优化-云端协同 — 完整方案
- 共享队列架构 — 抓取调度
- [[2026-05-28-cloud-sync-contribution-proposal|2026-05-28-网站采集云端协同-贡献度构想]] — 用户原始构想