两种模式的**目标是不同的东西**,原来的 fetcher 却一条路走到底: * 「作品」模式(粘贴作品链接)—— 目标本身就是作品 id,直接取详情即可。**这个接口没被 那道真校验挡,今天就能用。** * 「博主」模式 —— 目标是主页 sec_uid,要先拉作品列表;那个接口被挡,退化成刷新已知作品。 原来两种都去调 author_videos(它要的是博主 sec_uid),于是「作品」模式的监控拿作品号当 sec_uid 去查,必然失败 —— 而且失败原因说得很难懂(接口回你「未登录/不是浏览器」)。 结果就是:**新建「作品」模式的抖音监控永远抓不到东西**,而那本来是现有条件下唯一能用的。 现在按 mode 分岔。测试 +2:作品模式必须走 detail 且**不得**去调列表接口(走错了会 直接抛断言);一件作品坏掉不连累其他作品。 顺带记一条排查结论:博主主页的 HTML 里**没有**作品列表(RENDER_DATA 解出来只有 {isLogin, statusCode, isSpider}),所以「走页面 HTML 免接口」那条路也是死的。
跑真任务时踩到的: sqlalchemy.exc.IntegrityError: (1062, "Duplicate entry '7-7690458980574358513-45' for key 'uq_note_metric'") 根因是我上一版写错了一处作用域:退化路径里那个「遍历已知作品」的循环写在了**目标循环内部**, 所以任务有多个目标时,同一批已知作品会被拉两遍 → 同一件作品在一轮里出现两条记录 → ingest 给同一件作品写两份本轮快照 → 撞 (task_id, note_id, run_id) 唯一键。 两处都修,各挡一层: * douyin_fetch:去重集合挪到 collect 的最外层,**跨目标**只算一次;退化时也先查一遍 已知作品是否已刷过。 * ingest:`_ingest_notes` 对「一轮里重复出现的 note_id」免疫。一层在源头、一层在入口, 因为产物里重复并不罕见(多个目标指向同一个人、上游重跑、退化路径),不该靠上游自觉。 测试 +2:多目标时已知作品只刷一次;同一轮里重复的作品只落一份快照(这条会崩在 唯一键上,所以它测的正是运行时的那个崩法)。
上一版只把客户端写出来、验证了它单独可用,**但没有接进任何地方** —— 所以你建的任务跑起来 仍然在调爬虫子进程,报的仍然是那句自己编的「account blocked」。这一步把它接上。 * runner 的 Phase 2 按平台分岔:dy 走进程内 HTTP 客户端(douyin_fetch),其余平台照旧走 爬虫子进程。抖音那条不再起 Playwright、不再构造那串自相矛盾的浏览器指纹参数。 * 新增 douyin_fetch:把采到的东西写成 store/douyin 那套 jsonl 形状 —— **ingest 完全不知道 数据是从哪来的**,重采样/差分/事件/通知/报表全都照旧,一个字没改。 * 失败不再假装:一条都没采到就以非零退出码 + **真实原因**交给 ingest,落成 「采集进程异常退出(code=1):…」。绝不会再掉进「疑似登录失效」那个分支。 * 已知作品列表接口(aweme/post)被抖音单独加了真校验(200 + 空 body),所以加了退化: 拿不到列表就用库里已知的 aweme_id 逐条走 detail 刷新。**边界是:已知作品的指标能继续 更新,新作品发现不了** —— 这个边界会以一条 warning 日志留下痕迹,不让它看起来一切正常。 * 顺带给客户端补上 video_detail(实测可用:200 / 45425 字节),退化路径靠它。 测试 +6:产物目录与文件名、评论文件即使为空也要建(ingest 靠它区分「没评论」和 「什么都没抓到」)、重复作品只写一次、列表被挡时的退化、彻底失败仍写出产物与原因、 评论失败不连累作品。