# Roadmap: 政务网站信息采集系统 (GovCrawler)

**Created:** 2026-04-22
**Granularity:** coarse
**Mode:** yolo / parallelization=true
**Total v1 requirements:** 36
**Coverage:** 36/36 mapped

## Core Value

在不惊扰目标政务网站的前提下，稳定、增量、可追溯地把 20–30 个政务门户网站的公开内容持续落到本地存储，并让下游 RAG 系统简单地拉到增量。

## Phases

- [ ] **Phase 1: 地基与 PoC 破冰** — 单站端到端跑通，证明反爬方案可行，落定存储骨架
- [ ] **Phase 2: 多站点采集内核** — 配置化 + 分层 Fetcher + Cookie 池 + 增量去重 + 调度
- [ ] **Phase 3: RAG 对接与可运营化** — REST API + 可观测性 + 合规 + Docker Compose 交付

## Phase Details

### Phase 1: 地基与 PoC 破冰
**Goal**: 单站（gdqy.gov.cn）端到端跑通一篇文章的抓取-解析-落盘-入库，验证"真实 Chrome 内核能过 ctct 盾"这个核心假设，并沉淀存储骨架（PG schema + 本地文件布局）。
**Depends on**: Nothing (first phase)
**Requirements**:
- FETCH-02（Playwright stealth 过 JS 挑战）
- PARSE-01, PARSE-02, PARSE-03, PARSE-04（XPath/CSS + GNE 兜底 + 正文清洗）
- STORE-01, STORE-02, STORE-03, STORE-04, STORE-05（raw_html / text / attachments / articles / attachments 表）

**Success Criteria** (what must be TRUE):
  1. 运行一次 PoC 命令能从 `www.gdqy.gov.cn/.../szfwj/` 某一篇文章抓到完整的 title / publish_time / source / 正文 / 附件链接，且自动过 ctct 盾（无 412）
  2. 抓到的文章在本地磁盘同时出现：原始 HTML（`raw_html/gdqy/szfwj/2026/04/*.html`）、正文纯文本（`articles_text/...`）、附件原件（`attachments/...`），目录结构符合 `站点/栏目/年/月` 规范
  3. PG `articles` 表里能查到该文章的元数据行（含 url_hash、simhash、text_path、raw_html_path、status='ready'），`attachments` 表里有对应附件记录（含 file_hash）
  4. 当主 XPath 抽取失败（人为删掉选择器）时，GNE 兜底能依然抽到正文主体，正文纯文本去除了导航/广告
  5. PG schema（sites / columns / articles / attachments / crawl_logs）已建好并通过迁移脚本可重建

**Plans:** 3 plans
- [x] 01-01-PLAN.md — 项目骨架 + Docker PG + Alembic 五表 + SQLAlchemy models
- [x] 01-02-PLAN.md — Parser (list/detail) + GNE/trafilatura 兜底 + utils (url_norm/simhash) + gdqy selectors
- [x] 01-03-PLAN.md — Fetcher (patchright) + Storage (files/attachments) + CLI + 端到端 smoke test

### Phase 2: 多站点采集内核
**Goal**: 把 PoC 扩展为一个可配置、可调度、可增量、对目标站点无感的多站点采集引擎。新站点只需写 YAML，不改代码；分层 Fetcher 自动降级；Cookie 池降本；夜间错峰跑批；列表页遇已采即停。
**Depends on**: Phase 1
**Requirements**:
- FETCH-01（httpx 基础抓取）
- FETCH-03（httpx → Playwright → DrissionPage 自动降级）
- FETCH-04（Valkey Cookie 池 TTL 4h）
- FETCH-05（412/403 连续 3 次放弃本轮）
- SCHED-01, SCHED-02, SCHED-03, SCHED-04（APScheduler + 01:00-05:00 错峰 + 单站并发 1/间隔 5s + 手动触发）
- INC-01, INC-02, INC-03（URL hash + SimHash + 最新时间戳提前停止）
- CFG-01, CFG-02, CFG-03, CFG-04（site YAML / column YAML / enabled 开关 / YAML ↔ PG 热更新）

**Success Criteria** (what must be TRUE):
  1. 只往 `config/sites/` 目录加一个新 YAML 文件（不改 Python 代码），重启/热加载后系统就能按新站点的栏目、选择器、调度表跑起来
  2. 对一个无反爬的站点，系统走 httpx 不起浏览器；对 gdqy 这类强反爬站，httpx 失败后自动降级到 Playwright 取得内容，且过挑战后的 Cookie 写入 Valkey 供同域名后续 httpx 请求命中复用（观察到第二次同域抓取不再启动浏览器）
  3. 同一篇文章重复抓两次：第二次被 URL hash 跳过；另一篇只是 URL 不同但正文几乎相同的文章，被 SimHash 判重；列表页翻到上次最新时间戳以前的文章时抓取停止翻页
  4. 观察实际运行：单站任意时刻同时进行的请求数 ≤ 1，相邻请求间隔 ≥ 5 秒（含 ±20% 抖动），所有栏目的定时窗口分布在 01:00–05:00；连续 3 次 412/403 后本轮任务主动退出，不继续重试
  5. 把某栏目的 `enabled` 置 false 或在 PG 改一个 XPath，下一轮跑批立即生效，不需重启服务；可以通过 CLI 或后续 API 手动触发单栏目补抓

**Plans**: TBD

### Phase 3: RAG 对接与可运营化
**Goal**: 让下游 RAG 系统能简单、稳定地拉到增量；让运维能看到抓取健康度并在异常时收到告警；整套系统通过 Docker Compose 单机交付，所有依赖许可证干净。
**Depends on**: Phase 2
**Requirements**:
- API-01, API-02, API-03, API-04（since 增量 / 详情 / 附件下载 / ack）
- OBS-01, OBS-02, OBS-03, OBS-04（crawl_logs 表 / Prometheus 指标 / 成功率告警 / 24h 无新文告警）
- COMP-01, COMP-02, COMP-03, COMP-04（UA 带身份 / robots.txt / 公开路径 / 许可证白名单）
- DEP-01, DEP-02, DEP-03（Docker Compose / 环境变量+挂载 / 浏览器守护重启）

**Success Criteria** (what must be TRUE):
  1. RAG 侧调用 `GET /api/articles?since=<ts>` 能拿到自该时间戳以来未 ack 的文章列表，调用 `/api/articles/{id}` 能拿到完整元数据+正文，`/attachments/{aid}` 能下到附件原件；`POST /ack` 后该文章的 `exported_to_rag_at` 被正确回写，下次 since 查询不再返回
  2. 抓取过程中每次动作都在 `crawl_logs` 表留痕（站点/栏目/URL/策略/http_status/耗时/成功失败/错误），`/metrics` 端点暴露 Prometheus 格式的成功率、耗时分布、412/403 率、浏览器启动次数、新增文章数
  3. 人为制造故障（连续降 5 轮成功率、伪造 412 率 > 30%、或把某栏目的 XPath 改错让它 24h 零新增），飞书/企微 Webhook 能收到对应告警消息
  4. 抓包/访问日志里能看到 UA 形如 `GovCrawlerBot/1.0 (contact: xxx@example.com)`；`/robots.txt` 声明禁止的路径被自动跳过（除非站点配置 `respect_robots: false`）；尝试配置一个 `/admin/` 路径的栏目会被拒绝
  5. `pip-licenses` 扫描运行时依赖树，输出里没有任何 AGPL / SSPL / BSL / Elastic License 条目；在一台干净的 Linux 机器上 `docker compose up -d` 能拉起应用 + PostgreSQL + Valkey 三容器，配置全部通过环境变量 + 挂载目录注入，无硬编码；浏览器任务跑满阈值后 Playwright 进程被自动重启，内存不会无限增长

**Plans**: TBD

## Progress

| Phase | Plans Complete | Status | Completed |
|-------|----------------|--------|-----------|
| 1. 地基与 PoC 破冰 | 3/3 | Code complete (smoke pending) | - |
| 2. 多站点采集内核 | 0/0 | Not started | - |
| 3. RAG 对接与可运营化 | 0/0 | Not started | - |

## Coverage Map

| Category | Requirements | Phase |
|----------|--------------|-------|
| Fetcher | FETCH-02 | Phase 1 |
| Fetcher | FETCH-01, FETCH-03, FETCH-04, FETCH-05 | Phase 2 |
| Parser | PARSE-01, PARSE-02, PARSE-03, PARSE-04 | Phase 1 |
| Scheduler | SCHED-01, SCHED-02, SCHED-03, SCHED-04 | Phase 2 |
| Storage | STORE-01, STORE-02, STORE-03, STORE-04, STORE-05 | Phase 1 |
| Incremental | INC-01, INC-02, INC-03 | Phase 2 |
| Config | CFG-01, CFG-02, CFG-03, CFG-04 | Phase 2 |
| API | API-01, API-02, API-03, API-04 | Phase 3 |
| Compliance | COMP-01, COMP-02, COMP-03, COMP-04 | Phase 3 |
| Observability | OBS-01, OBS-02, OBS-03, OBS-04 | Phase 3 |
| Deployment | DEP-01, DEP-02, DEP-03 | Phase 3 |

**Total:** 36/36 v1 requirements mapped. No orphans. No duplicates.

## Design Notes

- **Phase 0 合并入 Phase 1**：coarse 粒度下 PoC 与存储骨架同属"先让一篇跑通"的工作集，不单独成阶段。
- **Phase 1 故意重存储轻通用抓取**：把 STORE-01..05 放在 Phase 1，是为了让 PoC 直接产出"符合最终目录/表结构的数据"，避免 Phase 2 返工。
- **FETCH-01 放 Phase 2**：Phase 1 只需 Playwright 就能验证最难的 ctct 盾，httpx 基础抓取是多站点铺开时的降本手段，放 Phase 2 更合节奏。
- **COMP-04 (许可证白名单) 放 Phase 3**：实际是贯穿始终的纪律，但它的验收动作（`pip-licenses` 扫描 CI 化）最适合在交付前做一次闭环。
- **v2 (ADMIN-*, ADV-*) 不在本 roadmap 范围**。

---
*Roadmap created: 2026-04-22*
