Files
content-harvester/README.md
T
huyue 497fae99c6 init: content-harvester —— 内容采集服务
FastAPI + Playwright(headed Chrome/xvfb 过 Cloudflare)抓内容详情页元数据;
可选把 m3u8 视频转 mp4 + 封面上传对象存储(配置由调用方下发,本服务不持密钥);
走主站 lobster 执行器契约(invoke/poll),回传 ResourceIngestItem 形式的 resources。
当前内置 xchina.co 的 scraper(core/scraper.py)。
2026-05-12 16:20:39 +08:00

6.3 KiB

content-harvester

内容采集服务(FastAPI + Playwright)。给一个来源页 URL,用有头 Chrome(容器内靠 Xvfb,能过 Cloudflare)逐个打开内容详情页,抓出每条内容的 标题 / 详情页 URL / 封面图 / 媒体地址;如果调用方下发了对象存储(MinIO/S3)配置,就把媒体(目前是 m3u8 视频)拉成 mp4、连同封面一起上传,把 video_url/cover_image 换成对象存储上的稳定地址(很多站点的媒体地址是带签名的短时 URL)。结果以 resources 列表回传。

当前内置一个 xchina.co 的 scraper(core/scraper.py);以后接别的来源就再加一个 scraper 模块。

设计文档见 PRD-minio-upload.md,实现细节见 CLAUDE.md。

用法定位

对接「启源 AI 平台」主站的 lobster 执行器:主站「多内容数据采集」任务里选「来源大类 = 内容网站」时,把作业 POST 给本服务,本服务返回 resources,主站写进资源库。

HTTP 接口

  • POST /agents/{agent_id}/invoke — body 是主站的调用载荷:content_url(必填,来源页/详情页 URL)、platform_name(可选 → source_name,默认 xchina)、source_kind(可选,非 website 直接 400)、start_page/end_page(可选,默认只爬 content_url 这一页/这一条)、minio_endpoint/minio_access_key/minio_secret_key/minio_bucket(可选,主站把对应 Skill 变量并进来)。立即返回 {"task_id":"...","status":"running"},作业后台跑。
  • GET /tasks/{task_id} — {"status":"running"|"succeeded"|"failed","output":{...}|null,"error":str|null,"log_tail":[...]}。
    • 成功时 output = {"resources":[...], "pages_scanned":n, "item_errors":[{"source_url","reason"},...]}。resources 每项字段对齐主站 ResourceIngestItem:source_type:"video" / source_name / source_url / content_type:"video" / title / cover_image / video_url / extra_data。
    • extra_data 含 referer / page_no / collector / video_id;上传成功还有 original_m3u8 / original_cover / minio_object_video / minio_object_cover / video_bytes;没上传则有 upload_skipped_reason(如 minio config absent / download failed: ... / video too large: ...)。
  • POST /tasks/{task_id}/cancel — 标记取消(仅 running)。GET /healthz — 健康检查。

鉴权:设了 HARVESTER_TOKEN 则要求 Authorization: Bearer <token>(= 主站 Skill 的 lobster_api_key);没设则不鉴权(仅内网/本地)。

并发:进程内单作业串行(防风控)。作业状态只在内存,重启即丢。

环境变量

变量 默认 说明
HARVESTER_TOKEN 空 Bearer token;空=不鉴权
HEADLESS 0 0=有头(容器内靠 Xvfb);过 Cloudflare 必须;1=无头(本地调试,过不了详情页)
BROWSER_CHANNEL 空 x86_64 服务器可设 chrome(需 Dockerfile 里 playwright install chromium chrome);arm64 无 Chrome 包
BROWSER_PROFILE_DIR /data/browser_profile 持久浏览器资料目录(挂卷;可预置过了 Cloudflare 的 cookie)
ALLOWED_HOSTS xchina.co content_url host 白名单(SSRF)
MAX_PAGES 50 单作业最多翻页数
CONCURRENT_FRAGMENTS 8 yt-dlp 并发分片数
MAX_VIDEO_MB 0 单视频大小上限(0=不限),超过则跳过上传、降级保留原媒体地址
TMP_DIR /tmp 临时下载目录(<TMP_DIR>/content-harvester/<jobid>/,用完即删)
MINIO_ENDPOINT / MINIO_ACCESS_KEY / MINIO_SECRET_KEY / MINIO_BUCKET 空 仅作 payload 缺失时的兜底;生产默认不配,由主站下发

本地跑

python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
python -m playwright install chromium
# 本地没 Xvfb 用 HEADLESS=1(但详情页会被 Cloudflare 拦)
HEADLESS=1 uvicorn server.app:app --reload   # http://localhost:8000

curl -s -X POST localhost:8000/agents/xchina/invoke -H 'Content-Type: application/json' \
  -d '{"source_kind":"website","content_url":"https://xchina.co/videos/series-XXXX.html","minio_endpoint":"<MINIO_ENDPOINT>","minio_access_key":"<KEY>","minio_secret_key":"<SECRET>","minio_bucket":"<BUCKET>"}'
curl -s localhost:8000/tasks/<task_id>

Docker(推荐)

docker build -t content-harvester .
docker run -d --name content-harvester -p 8000:8000 \
  -e HARVESTER_TOKEN=xxxxx \
  -v harvester_profile:/data/browser_profile \
  --memory=2g --cpus=2 \
  content-harvester

镜像里已装 xvfb(跑有头浏览器)+ ffmpeg(yt-dlp 合并 m3u8 分片)。部署到主站所在 VPS 时,把这个容器加进主站 docker compose 同一网络,主站 Skill 的 lobster_api_url 填 http://content-harvester:8000。

主站侧对接

  1. 「Skill 管理」新建/编辑那个采集 Skill:executor_type=LOBSTER、skill_category=CRAWL、lobster_api_url=http://content-harvester:8000、lobster_api_key=<HARVESTER_TOKEN>、lobster_agent_id=xchina(随意);input_schema 含 source_kind 字段(前端才渲染采集表单)+ 声明 content_url(必填),可选 start_page/end_page;变量关联里勾上 minio_endpoint/minio_access_key/minio_secret_key/minio_bucket 这 4 个 Skill 变量(它们的实际值由运维在主站配置,本服务不持有任何密钥)。
  2. 主站后端配套改动:非批次 lobster 改后台跑、成功后自动 _maybe_ingest_to_resource_pool、ingest 兼容 output.resources、_dispatch_lobster 把 Skill 变量并入 payload、crawl 类用 CRAWL_LOBSTER_TIMEOUT(env,默认 7200s)。
  3. 创建任务:选该 Skill → 来源大类「内容网站」→「地址」填来源页/详情页 URL → 设可见性/去重/入库后状态 → 提交。任务成功后资源库出现这些内容,video_url 指向对象存储,长期有效。

反爬 / 限制

  • 详情页常被 Cloudflare 挡,只有真实有头浏览器(HEADLESS=0 + Xvfb,或真实 Chrome)才能自动过 JS 质询;_assert_not_blocked 会等质询自动放行(最多 ~24s),仍卡住则该条进 item_errors。站点策略变化可能导致采集失效,退路是预置一份过了验证的 browser_profile 卷。
  • 下载+转码+上传慢:每条 15s 抓 + 几十秒几分钟下载 + 上传;多页/大量视频会很慢,属预期。
  • 任一条目下载/上传失败 → 该条降级:video_url 保留原始媒体地址(extra_data.upload_skipped_reason 记原因),不让整作业白跑。