Files
backend_v2/docs/superpowers/specs/2026-10-25-spider-console-design.md
toom1996 e930ac41c8 feat: 爬虫页取代抓取任务/入库任务两页,抓取改全量+覆盖率跳过
一、后台只留一个「爬虫」页(/admin/spider)
抓取与入库本就是同一条流水线的上下游,分两页看谁都看不全。
新页三块:状态条(在线/卡住 + 队列数 + 待入库数)、正在抓(含进度与心跳)、
最近入库结果(品牌/场次/结果/说明)。只读,取消与重试一并下线。

二、抓取一律全量
删除品牌页的场次上限下拉与 hidden maxShows=3,入队固定 0(全量)。
crawl_tasks.max_shows 列保留,历史任务照常显示。

三、跳过判定改覆盖率
- brand_runways 增加 expected_images(spider 上报的应有张数)
- /shows/covered 返回 images + expected
- 实际张数 >= 应有 - 5 才跳过;expected=0 时回落 10 张阈值
- 实体已存在时不再直接跳过:按 key 比对把缺的图补进库(repaired)
  否则「判定不完整→重爬→后端跳过」会形成死循环,带宽白花、缺图永远补不上

四、租约 900→300 秒,且心跳算存活
只看 locked_at 会把跑了几小时的全量抓取误判成卡死;
加上 progress_at 后,真卡住 5 分钟才回收。
2026-10-25 23:59:54 +08:00

138 lines
5.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 爬虫管理台(/admin/spider)设计
日期:2026-10-25
状态:已与需求方确认,待实现
## 背景
后台把「抓取任务」和「入库任务」拆成两个页面,但它们其实是同一条流水线的上下游:
```
spider 抓图 → POST /ingest → ingest_jobs 队列 → 后端 worker 下载/写库 → brand_runways + images
↑ 抓取进度写 crawl_tasks ↑ 入库结果写 ingest_jobs.outcome/note
```
分开放导致排障时要在两页之间跳,且任何一页都看不全「现在到底在干什么」。
同时存在两个问题:
1. **小批量模式**(`max_shows=3`)让「入队」这件事语义含糊,实际使用时总是想全量。
2. **跳过判定太粗**:只要某场秀已入库 ≥10 张就整场跳过。如果当初只成功抓到 12 张(其余失败),
剩下的图永远补不上。
## 目标
1. 入队只有一个语义:**全量**。
2. 跳过判定改为**覆盖率**:入库张数接近应有张数才跳过,缺得多就补抓。
3. 用一页只读的「爬虫管理」替代原两个页面,能看清当前在抓什么、队列有多长、最近入库结果。
4. 卡死任务 5 分钟自动回收,不依赖人工点「重试」。
## 决策
| 决策 | 结论 | 理由 |
|---|---|---|
| 页面形态 | 一条流水线看全(状态条 + 正在抓 + 待抓队列 + 最近入库) | 两表本就是一条链路,合并不需要改数据模型 |
| 操作能力 | 纯只读 | 抓取卡死由租约自动回收;入库失败多为不可恢复的数据问题 |
| 场次上限 | 后台入口全部移除,入队恒为全量 | 用户明确只保留全量模式 |
| 跳过判定 | 覆盖率(允许缺 5 张) | 个别图在 Vogue 侧永久 404,要求一张不差会导致整场无限重抓 |
| 抓取租约 | 900s → 300s,且「有心跳就不回收」 | 单纯缩短会误回收长任务;心跳是现成的存活信号 |
## 设计
### 1. 全量抓取
- `brands.html`:删除批量入队的「每品牌场次上限」下拉、删除单品牌按钮里的 `hidden maxShows=3`。
- `CrawlBulk` / `CrawlEnqueue` 不再读取 `maxShows`,入队固定传 0。
- `crawl_tasks.max_shows` 列保留:历史任务仍按原值渲染,新任务恒为 0。
- spider `serve` 用任务里的 `max_shows`(恒 0 = 全量);`vogue` 子命令的 `-s` 仅留作本地调试。
### 2. 覆盖率判定
**数据**
- `brand_runways` 新增 `expected_images int not null default 0`:本场秀应有图片数。
- `RunwayIngest` 新增 `expected_images`(spider 侧已知,日志里的「共 N 张图」)。
- ingest 处理时写入:`expected_images = max(旧值, 载荷值)`,避免重抓变少时退化。
**接口**
`GET /admin/internal/shows/covered` 每项返回 `{year, season, collection_type, images, expected}`。
**判定(spider 侧)**
```
if expected > 0: 跳过条件 = images >= expected - 5
else: 跳过条件 = images >= 10 (历史数据无 expected)
```
- 余量 5:缺 1~5 张视为完整。少了再补抓是整场重下(内容寻址无法只补缺的),代价高。
- 缺超过 5 张才补抓,补抓后 `expected_images` 可能更新,逐步收敛。
### 3. 页面 `/admin/spider`
**① 状态条**
```
爬虫 在线 · 正在抓 1 个 · 队列 3 个 · 待入库 2 条
```
- 在线判定:存在 running 任务且最近心跳 < 10 分钟 → 在线;
running 但心跳 ≥ 10 分钟 → `可能卡住`;无 running → `空闲`。
**② 正在抓**(`crawl_tasks.status = running`)
| 品牌 | 进度 | 当前场次 | 心跳 |
|---|---|---|---|
| Amiri | 12/47 场 + 进度条 | Fall 2021 Menswear(24/38 张) | 8 秒前 |
心跳 > 10 分钟整行标黄。
**③ 待抓队列**(`status = queued`):品牌 / 入队时间 / 已等待。
**④ 最近入库结果**(`ingest_jobs` 最近 50 条,`kind='crawl'`)
| 时间 | 品牌 | 场次 | 结果 |
|---|---|---|---|
| 23:12 | Amiri | Fall 2019 Menswear | 新建 34 张图 |
| 23:10 | Celine | Spring 2020 RTW | 已存在跳过 |
| 23:08 | Gucci | Fall 2020 RTW | 失败:图片下载超时 |
场次标题直接取 `payload::json->>'title_en'`,结果取 `outcome` + `note`,无需改表。
**轮询**:`GET /admin/spider/live` 每 4 秒,返回 ①②③ 三块的结构化数据;④ 不轮询(历史不会变)。
### 4. 租约改 300 秒 + 心跳存活
```sql
WHERE status = 'running' AND locked_at > 0
AND locked_at < ?
AND coalesce(progress_at, 0) < ?
```
两个时间点都取 `now - 300`。心不断 → 永不回收;真卡住 → 5 分钟回收。
`CrawlStuckTimeoutSec` 由 900 改为 300。
### 5. 删除项
- 页面与模板:`/admin/crawl`、`/admin/ingest-jobs`
- 路由:`CrawlList`、`CrawlLive`、`CrawlBulk`、`CrawlRetry`、`CrawlCancel`、`CrawlDelete`、
`IngestJobs`、`IngestJobRetry`
- 导航:两项合并为一个「爬虫」
- 入队接口 `/admin/crawl/brands/:uid` → 改名 `/admin/spider/brands/:uid`(品牌页按钮同步改)
保留:品牌页的「抓取状态 / 上次抓取 / 更新记录」(品牌维度,不是爬虫维度)。
## 测试
- 渲染测试:`/admin/spider` 三区块、轮询锚点、卡住样式。
- 单元测试:
- 覆盖率判定:`expected>0 缺≤5 跳过`、`缺>5 补抓`、`expected=0 回落 10 张`
- 租约回收 SQL:有心跳不回收(集成测试落地时验证)
- 手动验证:Celine 全量重跑应只补抓缺图场次;新品牌全量抓取进度可见。
## 风险
- 全量抓取一个品牌可能数小时,若 spider 进程被杀,已入库的场次会被跳过,只重抓当前场次——可接受。
- 覆盖率依赖 spider 上报;老数据 `expected_images = 0` 时退回 10 张阈值,行为与现在一致。
- 只读页面意味着入库 `failed` 任务无法在界面上恢复,需改库处理(已确认接受)。