如何用 career-ops 的 check-liveness.mjs 批量验证失效的职位链接并清理 pipeline
在 career-ops 的求职流水线里,data/pipeline.md 的 "Pending" 区会不断累积待处理的职位 URL。其中由扫描器在 headless/batch 模式下加入的条目带有 **Verification:** unconfirmed (batch mode) 标记——因为扫描时 Playwright 不可用,它们从未做过 liveness 检查。如果直接逐条处理,失效职位会被一个一个打开、抽取 JD、消耗评估 token。modes/pipeline.md 给出的做法是:在逐 URL 循环之前,先用零 token 的 check-liveness.mjs 把所有 pending URL 批量扫一遍(liveness sweep),把失效链接从 pipeline 里清掉,再处理幸存者。
这篇文章只覆盖这一条路径:批量验证 + 按判定结果清理 pipeline。
检查器如何判定一条链接
check-liveness.mjs 对每个 URL 采用两级检查(见 check-liveness.mjs 与 docs/SCRIPTS.md 的 liveness 小节):
- API 级(零 token、无浏览器):liveness-api.mjs 先尝试公共 ATS 端点——Greenhouse、Lever、Ashby、Workday、LinkedIn。对 Greenhouse/Lever/Workday 这类单职位端点,HTTP 200 即证明职位仍在;权威的 404/410 直接判定 expired 并跳过浏览器检查。
- 浏览器级(回退):API 无法覆盖或结果不明确时,liveness-browser.mjs 用 headless Chromium 打开页面,检测失效模式("job no longer available" 之类的文案,支持英/德/法多语言)、HTTP 404/410、ATS 重定向模式、apply 按钮是否存在。
设计上是保守的:宁可返回 uncertain 也不轻易误判 expired,因为漏掉一个真实职位的代价更高。每个 URL 得到三种判定之一:active、expired、uncertain,附一条 reason。整个过程不消耗 LLM token。
准备条件
- 在 career-ops 项目根目录操作,
data/pipeline.md中已有 pending 条目。 - 先运行
npm run doctor确认前置条件:Node.js >= 18、依赖已安装、Playwright chromium 可用、所需文件(cv.md、config/profile.yml、portals.yml)存在。doctor 退出码0表示全部通过,1表示有失败项并打印修复提示。
第一步:把 pending URL 收集到临时文件
从 data/pipeline.md 的 "Pending" 区收集所有 - [ ] 行的 URL,写入一个临时文件,每行一个。--file 的解析规则(来自 check-liveness.mjs 源码):按行读取、去除首尾空白、跳过空行和 # 开头的注释行——所以临时文件里可以带注释头。
示例文件(URL 为示意,请替换为自己 pipeline 中的真实链接):
# pipeline.md pending URLs, 2026-09-08
https://boards.greenhouse.io/example-company/jobs/123456
https://jobs.lever.co/example/abc123
https://careers.example.com/posting/789
第二步:运行批量检查
node check-liveness.mjs --file urls.txt
等价的 npm 入口是 npm run liveness -- --file urls.txt。URL 数量较多时加 --throttle(默认基线 5000ms,每次等待在 base 到 2×base 之间抖动),目的是让浏览器级请求之间的间隔落在基于速率的 WAF 阈值之下——例如 pracuj.pl 的 Cloudflare 在约 2 次快速请求后就会标记会话,--throttle=5000 就是文档中给出的用法。检查是顺序执行的,且浏览器按需懒启动:所有 URL 都能被 API 级判定时,Playwright 根本不会启动。
无显示器的机器(headless 环境无法弹出有头浏览器兜底)加 --no-fallback,全程保持无头模式。
第三步:读取结果
每个 URL 打印一行判定,非 active 的附 reason,最后是一行汇总。输出格式示意(以下为文档示例,数值与 URL 以你的实际运行为准):
Checking 3 URL(s)...
✅ active (api) https://boards.greenhouse.io/example-company/jobs/123456
❌ expired https://jobs.lever.co/example/abc123
HTTP 404
⚠️ uncertain https://careers.example.com/posting/789
navigation timeout
Results: 1 active 1 expired 1 uncertain (1 via API, no browser)
退出码是明确的判断依据:0 表示全部 active;只要存在任何 expired 或 uncertain,退出码就是 1。也就是说,非零退出并不意味着检查失败,而是"这批链接里有需要处理的条目"。
第四步:按判定结果清理 pipeline
modes/pipeline.md 的 Liveness sweep 对三种判定给出了明确的处理方式:
| 判定 | 处理 |
|---|---|
expired |
不再处理该职位:把条目从 "Pending" 移到 "Processed",写成 - [x] ~~URL | Company | Role~~ — posting expired (liveness sweep);如果该条目已经有 tracker 行(data/applications.md),将其标记为 Discarded(templates/states.yml 中的规范状态)。不要为其抽取 JD、执行评估或生成报告/PDF |
uncertain |
留在 "Pending" 原位不动,交给后续逐 URL 提取阶段确认——瞬时超时不应该直接丢掉一个可能仍然有效的职位 |
active |
继续进入正常的逐 URL 处理流程(提取 JD → 评估 → 报告/tracker) |
处理完过期条目后,可以用 npm run verify 复核 pipeline 数据完整性:它对 data/applications.md 做九条规则校验(规范状态、无重复 company+role、报告链接有效等),退出码 0 表示零错误。改动了 tracker 状态(如标 Discarded)之后跑一次是顺手的确认动作。
边界与限制
uncertain不算通过。 它与expired一样会让整体退出码为1,但处理方式完全不同:expired 直接清理,uncertain 保留待确认。- 无头浏览器可能被 WAF 拦截。 headless 请求默认会伪装成普通桌面 Chrome UA(避免 Cloudflare 类 WAF 直接出挑战页);遇到仍无法通过的反爬墙时,检查器会重试一次有头浏览器。
--no-fallback下不做这次重试,可能得到更多uncertain。 - 批量节流只作用于浏览器级请求之间,API 级请求本身廉价且不计入节流。
- 与
verify:portals的区别。npm run verify:portals探测的是portals.yml里各公司 ATS slug 是否还能解析(board 级),本文的 liveness sweep 针对的是data/pipeline.md里具体的职位链接(posting 级),两者互补,不能互相替代。 - 本工具只读、不修改任何文件:pipeline 的清理是按上面的判定手工/由 agent 写入
data/pipeline.md的,check-liveness.mjs本身不碰数据文件。
sweep 之后,"Pending" 区剩下的就是活着的职位链接,可以按 modes/pipeline.md 的 Workflow 继续逐条处理;失效链接已经以 posting expired (liveness sweep) 的留痕形式归档在 "Processed" 区,随时可以回查为什么某个职位没有被评估。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00