ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

career-ops 批量处理模式详解:batch-runner.sh 如何用无头 Worker 并行评估 JD 并自动合并结果

2026/9/7 14:01:29 拓冰建站 浏览量
career-ops 批量处理模式详解:batch-runner.sh 如何用无头 Worker 并行评估 JD 并自动合并结果 career-ops 批量处理模式详解batch-runner.sh 如何用无头 Worker 并行评估 JD 并自动合并结果【免费下载链接】career-opsOpen-source AI job search: scan job portals, evaluate listings into a structured A-H report with a global 1-5 score, tailor your CV, track applications — runs locally in your AI coding CLI (Claude Code, Codex, OpenCode, Antigravity…)项目地址: https://gitcode.com/GitHub_Trending/ca/career-opsbatch/目录是 career-ops 的批量处理子系统把一批职位链接放入 TSV 文件由batch-runner.sh编排、逐个或并行拉起无头 CLI Worker每个 Worker 自主完成完整的 A-G 评估报告、ATS 优化简历 PDF 与 tracker 行最后统一合并进data/applications.md并从data/pipeline.md收件箱中回收。读完本文你将掌握批量流水线的目录布局、完整参数体系、状态机与断点续跑机制、报告号原子分配与 JD 预取等底层实现以及 tracker 合并、pipeline 对账、token 成本统计三大收尾环节能够独立运行、排障并扩展这套批量评估管道。一、子系统定位为什么需要批量模式交互式评估一次只处理一个 JD当手里积压了几十条来自扫描scan.mjs或手动收集的职位时需要一种投放一批 URL、产出 N 份结构化报告的无人值守方式。career-ops 的方案是每个 Worker 是一个干净的无头子进程约 200K token 的全新上下文conductor编排器只负责调度见 modes/batch.md。各 CLI 的无头命令对照摘自 AGENTS.md 的 Headless / Batch Mode 小节CLI无头命令Claude Codeclaude -p promptOpenCodeopencode run promptCopilot CLIcopilot -p promptCodexcodex exec promptQwenqwen -p promptAntigravity CLIagy -p promptGrok Build CLIgrok -p prompt需要注意适用范围batch-runner.sh 头部注释明确说明它是Claude Code 专用编排器——它依赖claude -p的--dangerously-skip-permissions与--append-system-prompt-file两个参数这些参数在其他 CLI 上不可用。脚本原话Multi-CLI support is out of scope for now — contributions welcome. 也就是说批量编排脚本目前以 Claude Code 为运行前提而 AGENTS.md 的对照表是给手工无头评估或其他编排场景使用的命令参考。二、快速上手四步跑通一次批量以下流程完整继承自 batch/README.md。第 1 步准备输入。创建batch/batch-input.tsv制表符分隔四列id、url、source、notesid url source notes 1 https://jobs.example.com/role-a LinkedIn 2 https://greenhouse.io/company/role-b Greenhouse priority第 2 步干跑预览——只列出将被处理的 offer不做任何处理./batch/batch-runner.sh --dry-run第 3 步正式执行./batch/batch-runner.sh第 4 步查看收尾。结果自动合并进data/applications.md已处理的 offer 从data/pipeline.md收件箱回收最后用verify-pipeline.mjs做完整性校验。三、目录结构与数据流batch/README.md 给出的目录布局batch/ batch-runner.sh # 编排器脚本 batch-prompt.md # 发给每个 Worker 的 Prompt 模板 batch-input.tsv # 输入 offer你自己创建 batch-state.tsv # 处理状态自动管理、可续跑 logs/ # 每 offer 一份 Worker 日志{report_num}-{id}.log tracker-additions/ # Worker 产出的 tracker TSV 行 merged/ # 已合并进 applications.md 的 TSVREADME 描述的宏观流程How It Works共四步batch-runner.sh读取batch-input.tsv与batch-state.tsv确定哪些 offer 需要处理对每个待处理 offer分配一个报告号report number并用 batch-prompt.md 作为系统 Prompt 拉起无头 Worker{{URL}}、{{REPORT_NUM}}等占位符在此被解析每个 Worker 评估 offer把报告写入reports/、生成 PDF 到output/、写一行 tracker TSV 到tracker-additions/全部 Worker 结束后编排器依次调用merge-tracker.mjs合并 TSV 进data/applications.md、reconcile-pipeline.mjs把已处理 offer 移出 pipeline 收件箱、verify-pipeline.mjs完整性校验。从源码看第 4 步对应 batch-runner.sh#L1097-L1107 的merge_tracker()三条node命令顺序执行后两条失败只打印警告而不会让整次运行报错退出。四、完整参数体系4.1 README 文档化的核心参数参数默认值说明--parallel N1并发无头 Worker 数--dry-run关预览待处理 offer不执行--retry-failed关只重试 state 中标记为failed的 offer--resume-paused关恢复因 Claude 会话/用量限制而暂停的 offer--start-from N0跳过 ID 小于 N 的 offer--limit N0本次最多处理 N 条0 不限制--max-retries N2单个 offer 放弃前的最大重试次数--rate-limit-sleep N300限流后重试前等待的秒数设为0则立即暂停批量4.2 源码中额外提供的参数batch-runner.sh#L55-L101 的usage()还暴露了若干 README 未列出的开关参数说明--min-score N得分低于 N 的 offer 跳过 PDF/tracker 写入直接标记skipped默认0 关闭--skip-pdf完全跳过 PDF 生成tracker 的 PDF 列写 ❌--model NAME覆盖spend_tier解析出的模型直接传给claude -p --model--status打印批量进度与逐 job 表格后退出不需要 Worker 前置条件--watch每 2 秒实时刷新进度表直到运行结束-h / --help打印帮助--limit、--min-score、--rate-limit-sleep都有参数合法性校验必须是非负整数/数字不合法直接报错退出batch-runner.sh#L128-L141。4.3 模型解析spend_tier 与 --modelWorker 用哪个模型由两级机制决定batch-runner.sh#L351-L410默认读取 config/profile.yml示例文件中的spend_tier缺失或非法时回退为standard映射关系spend_tier_to_model注释要求与 modes/_shared.md 的表保持同步economy→claude-haiku-4-5premium→claude-opus-5standard及兜底→claude-sonnet-5显式传入--model时永远优先运行日志会标注(explicit --model override)与(spend_tier...)以区分来源。与 modes/batch.md 的Pre-screen gate呼应standard/premium档位下可先用经济档模型做廉价预筛明显不匹配的 JD 直接标skippedeconomy档不做预筛。所有被预筛淘汰的职位必须按{ISO8601时间戳}\t{job id}\t{url}\t{reason}追加进batch/logs/discard.log编排器侧对应log_discard()batch-runner.sh#L415-L421保证预过滤不是黑盒。五、单条 Offer 的处理链路源码级process_offer()batch-runner.sh#L726-L1094是整条流水线的核心实际步骤比 README 的四步描述多出不少防御性环节。5.1 报告号预留共享原子分配器Worker 启动前必须先拿到唯一的三位零填充报告号。早期实现是 bash 原生max(现有报告文件, state 中编号)1扫描但它对其他进程的预留完全不可见——交互式 Agent 评估单条 offer 时若也在申请编号双方会算出同一个下一个号并在磁盘上碰撞。当前实现统一走 reserve-report-num.mjs基于O_CREAT|O_EXCL哨兵文件 tracker 锁的共享原子分配器它把报告文件、预留哨兵、tracker 行 ID、tracker 报告链接都视为已占用--count N可一次预留一段如042-049--release释放陈旧哨兵 4 小时后 GC。这与 AGENTS.md 中Parallel fan-outs — reserve report numbers first的约定一致——并行 Worker 永远不能自己算max1。5.2 JD 预取curl 80 词充分性门槛启动 Worker 前编排器会先用静态curl把 JD 的 HTML 抓进一个mktemp临时文件让 Worker 直接读本地文件而不是每次都落到 WebFetch 兜底WebFetch 对 Phenom、Workday、iCIMS 这类 JS 渲染的招聘板不可靠——它命中的是渲染后的 JS 壳而非 JD 正文。这段逻辑batch-runner.sh#L743-L858有几个关键设计安全边界连接前用 Node 单行脚本校验目标拒绝localhost、.local、.internal、环回/链路本地/私网网段含169.254.169.254云元数据地址防止恶意 offer URL 把 curl 引向内网重定向上限 10 跳curl 限制--max-filesize 5000000、--max-time 20仅允许http/https协议临时文件用mktemp而非固定路径可预测的/tmp路径在共享机器上可能被预先建为符号链接从而劫持写入充分性判定抓回的内容先剥掉script/style/标签后统计可见词数低于命名变量prefetch_min_words80就判为JS 壳把文件截断为 0 字节——Worker 的 Step 1 发现文件为空时按设计回落到 WebFetchcurl 不存在或失败同样留空文件走兜底。该行为被 tests/batch-runner-jd-prefetch.test.mjs 钉住测试直接从batch-runner.sh文本中提取真实的 curl 代码块做断言保证实现与测试不会漂移测试注释记录了背景原始故障中 251 条 offer 有 35 条因 WebFetch 取不到 JD 而失败。5.3 Prompt 组装与个性化注入占位符{{URL}}、{{JD_FILE}}、{{REPORT_NUM}}、{{DATE}}、{{ID}}由sed替换替换前对变量做分隔符转义写入一个 gitignored 的临时 resolved prompt 文件随后 modes/_profile.md、config/profile.yml、modes/_custom.md 若存在则按Runtime personalization小节追加进去——resolved prompt 属于运行期用户层状态个人数据因此不进系统层且批量评分与交互评分读取同一套用户规则batch-runner.sh#L878-L909。Worker 的最终命令行构造batch-runner.sh#L911-L930claude -p --dangerously-skip-permissions --strict-mcp-config \ [--model $RESOLVED_MODEL] \ --append-system-prompt-file $resolved_prompt $prompt其中--strict-mcp-config且不带--mcp-config让每个 Worker 以零 MCP server启动Worker 只做评估不需要 MCP。源码注释解释了原因——不加这个参数时--parallel 1的每个并行 Worker 都会继承父会话的 MCP如 Playwright多个 Worker 抢同一个共享浏览器直接死锁issue #506。5.4 Worker 侧batch-prompt.md 的六步 Pipelinebatch-prompt.md 是一个自包含系统 Prompt要求 Worker 依次完成Step 1 — 读 JD先读{{JD_FILE}}为空再 WebFetch{{URL}}两者都失败是硬停——不许写报告、不许写 tracker、不许编造分数或公司/职位名Unknown 或占位分也是无依据的臆断只输出一个真实的json失败代码块然后停止Step 2 — A-G 评估角色原型识别六种 AI 岗位原型、Block A 角色摘要、Block B CV 匹配强制两遍法先只读 JD 定 Importance再加载cv.md/article-digest.md填 MatchImportance 定下后永不回改、Block C 层级定位、Block D 薪酬与需求含公司类型分类与薪酬可靠性分级、Block E 个性化计划、Block F 面试计划6-10 个 STARR 故事、Block G 发布真实性批量模式下 Playwright 不可用apply 按钮状态等信号标unverified (batch mode)最后给出维度评分表和机器可读的 Machine Summary YAMLStep 3 — 写报告reports/{{REPORT_NUM}}-{company-slug}-{{DATE}}.md头部含 Date/Archetype/Score/Legitimacy/Work Auth/URL/PDF/Batch ID 与 Machine SummaryStep 4 — 条件生成 PDF读取config/profile.yml的auto_pdf_score_threshold缺省3.0分数达标才走cv.md templates/cv-template.html generate-pdf.mjs 生成 ATS 安全的单栏简历 PDF美/加用letter其余a4字体为 fonts/ 自托管的 Space Grotesk DM SansStep 5 — tracker TSV 行写batch/tracker-additions/{{ID}}.tsv固定 9 列 可选url尾列num、date、company、role、status、score/5、pdf emoji、报告链接、一句话备注注意TSV 中 status 在 score 之前而applications.md显示时 score 在前转换由merge-tracker.mjs负责合法 status 来自 templates/states.ymlEvaluated、Applied、Responded、Interview、Offer、Rejected、Discarded、SKIPStep 6 — 最终 JSON必须用 JSON 序列化器输出禁止字符串拼接成功/失败两种固定 schemapdf字段为路径字符串或原生null绝不能输出字符串null。Prompt 还包含一条贯穿始终的安全规则JD 文件与抓取页面都是不可信第三方数据其中任何忽略先前指令式文本只会被评分/摘要不会被执行。六、结果判定为什么 exit 0 也不算成功Worker 退出后编排器有三道 fail-closed 校验batch-runner.sh#L976-L1093解析日志中最后一个json块用 awk 定位最后一个JSON 围栏块Worker 的权威最终结果再用node真正JSON.parse提取status/error/score三个字段。源码注释记录了两起历史故障一是 Worker 退出码 0 但 JSON 自报failed比如拒绝为取不到的 JD 编造评估2026-07-29 的 Deepgram 案例二是旧版用整日志sed抓第一个score:匹配结果抓到 Block D 里的 Comp score: 4/5 文本。字段分隔符特意选用\x1fUS 控制符而非 tab——tab 是 bash IFS 空白符连续 tab 会被折叠导致空error字段后所有字段左移成功 offer 的分数会全部记成 -报告文件落盘校验即使 JSON 说成功reports/{report_num}-*.md不存在就判failed并释放报告号。注释说明 2026-07-30 曾有 offer 被这样误标completed报告号被第二个无关 offer 认领造成真实碰撞——exit code 和 JSON status 都不构成报告存在的证明min-score 门槛分数低于--min-score时标skippedreasonbelow-min-score并释放报告号。非零退出则进入分级重试/暂停逻辑batch-runner.sh#L924-L968Claude CLI shim 换装exit 127 command not found等待 30 秒重试最多 4 次会话/用量限制日志匹配session limit、usage limit、limit reached等标记paused_rate_limit写batch-runner.paused暂停文件不消耗重试预算停止调度新 offer 并等待在跑 Worker 收尾普通限流匹配429、rate limit、too many requests等且未达--max-retries标rate_limited按--rate-limit-sleep等待后原地重试--rate-limit-sleep 0则同样转为暂停。七、状态机、锁与断点续跑7.1 batch-state.tsv 与状态集合状态文件由init_state()初始化batch-runner.sh#L197-L2029 列id、url、status、started_at、completed_at、report_num、score、error、retries。状态集合为pending隐含、processing、completed、failed、skipped、rate_limited、paused_rate_limit其中completed/skipped是终态——重跑自动跳过rate_limited是等待重试中的非完成态中断的限流任务在下次普通运行时自然重新入选paused_rate_limit语义不同Worker 撞上 Claude 会话/用量上限编排器已停止调度并保留了重试计数必须在限额重置后用./batch/batch-runner.sh --resume-paused显式恢复。--retry-failed只重跑failed且重试次数未达--max-retries的行--resume-paused只重跑paused_rate_limit的行互斥的筛选逻辑见 batch-runner.sh#L1334-L1368。7.2 三层锁体系从源码结构看编排器有三层互不重叠的锁进程级 PID 锁batch-runner.pidacquire_lock()写入当前 PID启动前用kill -0探活活则报错退出死则自动清理陈旧锁batch-runner.sh#L143-L158trap release_lock EXIT保证正常退出时删除state 文件锁mkdir目录锁.batch-state.lock/所有对batch-state.tsv的改写都包在run_with_state_lock里。由于 Git Bash/Windows 下kill -0 $pid不可靠锁内写入{pid}\t{epoch}元数据PID 无法确认存活且锁龄超过STATE_LOCK_STALE_AGE_SECONDS15才判定可回收batch-runner.sh#L211-L302。注释里明确了安全不变量只要kill -0确认 PID 存活就绝不视为陈旧——宁可偶发锁超时可重试不可在 owner 可能存活时并发重写STATE_FILE.tmp静默数据丢失报告号分配锁即 reserve-report-num.mjs 内的 tracker 锁 O_CREAT|O_EXCL哨兵跨进程bash 编排器与交互式 Node 脚本共享同一把真实锁。7.3 恢复记录锁都拿不到时怎么办最坏情况下 state 锁连续三次拿不到update_state_retrying每次间隔 2 秒。此时不是丢弃状态转换而是append_recovery_record()用mktempO_CREAT|O_EXCL原子创建、文件名全局唯一在batch/batch-state-recovery.d/里每次转换写一个独立文件——因为这类恢复写入恰好在锁卡死、所有并行 Worker 同时失败的时刻发生共享恢复文件会在最需要的时候自己产生竞争batch-runner.sh#L474-L508。下一次运行在main()开头、任何 Worker 启动之前单线程执行reconcile_recovery_records()把这些记录合并回 state 文件若目标行已达终态completed/skipped则记录被判定为过期并安全丢弃避免把已完成的 offer 回卷到旧状态。这是针对一次真实故障的修复Git Bash/Windows 下--parallel 5单次运行曾静默丢掉约 50 个 job 中的 47 个。另外update_state_unlocked()在写入前会把error字段中的字面 tab/换行/回车折叠为空格——一行 TSV 被拆成多列/多行会污染其后所有行batch-runner.sh#L426-L468。7.4 并行调度--parallel 1顺序执行1时用后台子进程 PID 数组实现滑动窗口在跑数量达到上限就轮询kill -0等待任意子进程退出后补位batch-runner.sh#L1414-L1462。暂停标志BATCH_PAUSED或batch-runner.paused文件出现时调度循环不再投放新 offer只等在跑 Worker 收工。八、Tracker 合并merge-tracker.mjs每个 Worker 往batch/tracker-additions/写一个 TSV收尾时由 merge-tracker.mjsnpm run merge脚本定义见 package.json合并进data/applications.md。README 列出的四项能力均可在源码注释中找到对应实现去重多层级——精确 URL、报告号、以及company 归一化 role 模糊匹配roleFuzzyMatch来自 role-matcher.mjs注释提到模糊匹配解决了同一个 Google 职位以新 req 号再次出现以及六家日本公司发布同一句 データエンジニア这类重复行列序转换TSV 中 status 在 score 前applications.md中 score 在 status 前合并时转换原地更新重复条目中若新评估分更高则更新既有行而非追加Notes 列会追加Re-eval {date} ({old}→{new})标记并保持行号稳定merge-tracker.mjs 中Re-eval拼接逻辑归档合并完成的 TSV 移入tracker-additions/merged/。批量运行之外需要手动合并时直接npm run merge。九、Pipeline 对账reconcile-pipeline.mjs批量模式读的是batch-input.tsv而data/pipeline.md收件箱是另一份独立列表。不做对账的话被批量处理过的 offer 会一直留在 pipeline 的 Pendientes 区下一次scan或/career-ops pipeline再次把它捞出来——产出重复报告。reconcile-pipeline.mjsnpm run reconcile在每次 tracker 合并之后执行batch-state.tsv中所有completed或skipped、且 URL 仍在 Pendientes 的 offer 被移入 Procesadas并附上报告链接与分数报告文件在磁盘上不存在的条目原地保留这是批量侧误标完成时的最后一道安全网。整个过程幂等——每次批量后自动跑也可以手动随时跑。十、可观测性与成本统计--status/--watchprint_status_table()输出Total / Completed / Processing / Failed / Pending / Skipped / Rate Limited / Paused汇总、平均分以及ID | Status | Report | Score | URL/Error逐 job 表格--watch通过轮询锁文件里的 PID 判断运行是否存活每 2 秒清屏刷新结束后自动串联verify-pipeline.mjsToken 成本每次运行结束后print_summary()会调用 batch/aggregate-tokens.mjs。它按batch-state.tsv找到每份{report_num}-{id}.logid 与报告号都先做字符白名单校验防路径穿越优先解析日志中 Worker 打印的Token breakdown:块scan / evaluation / pdf payload 三段解析不到时回退扫描原始 CLI token 行从(metadata: model..., provider...)行提取模型后经 utils/token-tracker.mjs 的estimateCost折算美元成本最后输出逐 Worker 与全批次两级汇总表预筛审计batch/logs/discard.log记录每次 pre-screen 淘汰的原因README 之外的 modes/batch.md 要求定期回顾它来校准 North Star 原型防止门槛过严或过松。十一、前置条件与适用边界batch/README.md 列出的前置条件结合源码补充如下CLI 在 PATH编排器脚本会command -v claude硬校验check_prerequisites()中找不到直接退出即当前batch-runner.sh以 Claude Code 可用为前提其他 CLI 的无头命令见 AGENTS.md 对照表用于手工/其他编排场景Node.js 18 Playwright chromium批量评估本身只用 WebFetch 取 JD但仓库约定的校验方式是npm run doctordoctor.mjsbatch-input.tsv至少一条 offer输入文件缺失直接报ERROR: ... Add offers first.批量模式下 Worker没有 PlaywrightBlock G 的 apply 按钮状态、发布新鲜度等信号只能标unverified (batch mode)——这是与交互式评估的能力差异报告评分时应知悉该信号缺失行为回归由一组专项测试守护值得在改动后运行 tests/ 下的 batch-runner-jd-prefetch.test.mjsJD 预取、batch-runner-score-delimiter.test.mjs分数分隔符、batch-evaluate.test.mjs、batch-tailor-flags.test.mjs。小结career-ops 的批量模式是一个薄 Prompt、厚编排的设计智能评估逻辑全部收敛在自包含的 batch-prompt.md 里而 batch-runner.sh 承担了工程上最脏最累的部分——跨进程原子报告号分配、私网 URL 防护、JS 壳 JD 检测、会话限额与限流的分流处理、三层锁加崩溃恢复记录、以及对data/applications.md/data/pipeline.md两份数据源的自动对账。理解这套机制后无论是排查某条 offer 为什么停在paused_rate_limit还是给流水线加上自定义档位规则都能在 state 文件、logs 目录与上述源码位置找到确切依据。【免费下载链接】career-opsOpen-source AI job search: scan job portals, evaluate listings into a structured A-H report with a global 1-5 score, tailor your CV, track applications — runs locally in your AI coding CLI (Claude Code, Codex, OpenCode, Antigravity…)项目地址: https://gitcode.com/GitHub_Trending/ca/career-ops创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考