多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

career-ops Provider 层扩展实战:为 scan.mjs 零 Token 扫描器编写公共职位数据源适配模块

career-ops Provider 层扩展实战:为 scan.mjs 零 Token 扫描器编写公共职位数据源适配模块 career-ops Provider 层扩展实战为 scan.mjs 零 Token 扫描器编写公共职位数据源适配模块【免费下载链接】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-opscareer-ops 的 providers/ 目录是零 Token 职位门户扫描器scan.mjs的数据源适配层目录下每个非_开头的*.mjs模块都把某一个公共的、免登录的职位来源ATS API、RSS/XML 源或服务端渲染的 HTML 页面映射成扫描器统一的Job结构。本文以 providers/README.md 与 providers/ADDING_A_PROVIDER.md 为骨架结合仓库内 加载路由实现、HTTP 传输层、类型契约 与 Greenhouse、Workday 等真实适配器源码完整讲清新增一个 Provider从资格评审、契约编写、安全护栏到测试与合并前清单的全部要求。读完你将能够独立为 career-ops 贡献一个符合源码规范、能通过健康检查与全量测试的职位源模块。Provider 目录的定位零 Token 数据源适配层providers/的设计出发点可以概括为zero-token by design每个 Provider 直接请求公共端点全程无 LLM 调用、无登录。scan.mjs、verify-portals.mjs、doctor.mjs --strict都经由 providers/_registry.mjs 加载这些模块扫描主流程scan.mjs消费 Provider 返回的归一化Job[]再进入后续的 A-H 评级、内容过滤、去重等环节。对用户而言一个数据源是否被支持看的是用户侧目录 docs/SUPPORTED_JOB_BOARDS.md而这个来源是怎么被读进来的就是 Provider 层回答的问题。两个视角的对应关系见 docs/ARCHITECTURE.md 中关于scan.mjs providers/的 Discovery 一节。目录内部文件的命名约定非常明确Provider 模块providers/*.mjs不以_开头每个文件默认导出一个符合契约的 Provider 对象共享辅助模块以_开头的文件永远不会被当作 Provider 加载包括 _types.js契约 typedef 目录、_registry.mjs加载器/路由器、_http.mjsHTTP 传输、_html-entities.mjsHTML 实体解码、_html-to-text.mjs描述 HTML → 纯文本、_config-utils.mjs、_trust-validator.mjs 等。从源码结构看还有 _ip-guard.mjs 与 _dns-cache.mjs 这类更底层的防护模块DNS 查询被进程级记忆化并对 Provider 流量做地址校验它们由_http.mjs引入体现的是共享逻辑全部收敛到辅助文件、Provider 只写自己的解析的分层思想。数据源资格动手编码之前先过 Source Indexing Policy一个能跑通的 Provider并不等于应该合入的 Provider——它读取的数据源必须先满足 Source Indexing Policy见 CONTRIBUTING.md。这个评审针对的是数据源本身的性质与客户端代码写得是否优雅无关。单公司 ATS 适配器一个新的 Greenhouse/Workday/Ashby 类厂商或某家公司在自有招聘 API 上的适配默认通过postings 属于雇主自己适配器只读取一个来源不需要额外操作直接进入下述契约章节即可。职位板、聚合器或人才网络才是政策真正约束的对象核心三条真实、可归属雇主的职位列表且对候选人免费——posting 能解析到可识别的雇主、候选人无需付费或注册即可阅读和申请对列表或申请设付费墙即不合格每个 Provider 只对应一个来源——Meta 聚合器转发其他职位板内容的站点不是 career-ops 索引的对象跨源聚合逻辑属于核心层完整库存、无付费置顶——Provider 必须遍历来源的完整职位库存而不能只爬推广视图或默认过滤后的视图。如果某个来源是运营方自营、或资格边界不清晰应在写代码之前用仓库的 source proposal问题模板见.github目录下的模板文件发起评审——在设计文档上做路由决策比在完成 PR 之后再推翻便宜得多。政策在历史上如何落到具体来源上记录在 docs/SOURCE_INDEXING_LOG.mdSource Indexing Log中。Provider 模块契约default export、detect 与 fetch一个providers/{name}.mjs不以_开头的默认导出形状如下来自 ADDING_A_PROVIDER.md// ts-check /** typedef {import(./_types.js).Provider} Provider */ /** type {Provider} */ export default { id: unique-id, // required, unique across all providers detect(entry) { ... }, // optional: claim a portals.yml entry async fetch(entry, ctx) { ... }, // required: return Job[] };权威的类型目录在 providers/_types.js文件本身只有 JSDoctypedef注解、纯文档性质——项目是无构建步骤的纯 ESM JavaScript运行时契约由scan.mjs强制id必须存在、fetch必须是函数、fetch返回数组而不是由注解强制。Provider 作者在文件头加// ts-check和类型引用可以获得 IDE 提示。三个字段的行为要点id必填、全库唯一。若出现重复 id先加载的 Provider 胜出后加载的文件被跳过并打印警告见 _registry.mjs。detect(entry)可选返回{ url }或null。三种合法形态详见下文三种 detect 形态。fetch(entry, ctx)必填。必须使用ctx.fetchJson/ctx.fetchText绝不能裸用全局fetch需要响应头时用ctx.fetchResponse拿原始Response。可选的ctx.maxPages与ctx.sleep(ms)用于分页协作。返回值是归一化后的Job[]。Job 归一化数据结构Job是全扫描器流通的货币单位providers/_types.jstitle、url必填url必须是绝对 URL且作为去重键company、location可为空字符串列表页拿不到时留空由下游填充postedAtepoch 毫秒与description可选——description仅当列表响应天然携带、不需要额外按职位发请求时才填充扫描器是零 Token 的salary{min, max, currency}仅在来源暴露真实数字时挂载绝不推断ashby.mjs是参考形状多数 Provider 直接省略salary_filter读min ?? max容忍只有一边边界trustScore/trustFlags/trustLevel由 _trust-validator.mjs 打标0-100 分并给出 high/medium/low 分级。description的唯一例外是主动选择的增强portals.yml条目若带fetchDetails: true外加可选的detailLimit上限Provider 会逐个抓取详情填充description受detailLimit约束并且在健康探针运行期间整体跳过——目前只有vdab、smartrecruiters走这条路径两者源码中均有对fetchDetails/detailLimit的解析与probe 期间跳过的分支见 providers/vdab.mjs、providers/smartrecruiters.mjs。当响应中一个 posting 暴露多个候选 URL聚合器通常同时带雇主上游 ATS/申请链接和自己的详情页时Job.url取雇主链接——对应 Source Indexing Policy 第 2 条到雇主的最短可验证路径来源自己的页面只在缺失或非https:时兜底。参考实现是 providers/yourator.mjs 的resolveYouratorUrl与 providers/remotli.mjs 中的等价函数。单公司 ATS 适配器只有一个天然 URL无需选择。tracked_companies 与 job_boards 共用同一契约portals.yml把条目放在两个列表里但 Provider 层被两者共享tracked_companies:是每个雇主一个条目job_boards:是每个聚合器/Feed 一个条目包含多个雇主。两者使用完全相同的条目契约name/careers_url/api/provider/parser、相同的detect()与相同的注册表。单公司 Provider 按tracked_companies:文档化与测试聚合器/Feed 则按job_boards:。文件头注释必须写明目标列表参考 providers/remotli.mjs、providers/yourator.mjs。文件系统约定的加载与路由无索引文件靠 _registry.mjsproviders/没有索引文件——发现机制是文件系统约定实现在 _registry.mjs目录下所有不以_开头的*.mjs被readdirSync(...).filter(...).sort()后按字母序动态import字母序让detect()的优先级跨机器确定格式错误的模块缺fetch、缺id、重复 id、import 出错被记录并跳过绝不致命对每个portals.yml条目按 resolveProvider 的顺序路由条目显式provider: id字段最优先完全绕过detect()→ 配置了parser.command 脚本的local-parser→ 各 Provider 按加载顺序逐个跑detect()第一个非空命中胜出。路由还支持skipIds让纯网络的健康检查不执行配置好的本地命令。也就是说把文件丢进providers/目录就完成了注册无需手工登记。核心来源必须是针对公共端点的零鉴权访问需要登录或鉴权的来源应放到插件层而不是 Provider 层参见 ARCHITECTURE.md 与 CONTRIBUTING.md。三种 detect 形态URL 模式把entry.careers_url/entry.api与已知主机模式匹配providers/greenhouse.mjs、providers/lever.mjs、providers/remotli.mjs。一个品牌化、无法辨识的域名绝不能被URL 模式detect()认领显式专用return entry?.provider {id} ? { url: FEED_URL } : null用于全站 Feed、无逐条目 URL的场景providers/larajobs.mjs省略detect()该 Provider 只能靠portals.yml中显式的provider: {id}触达providers/yourator.mjs。形状 2/3 的约束保证了一个 Provider永远不会认领用户没有指向它的条目。强制安全护栏一SSRF 加固Provider 的一切网络请求都要过 SSRF 硬化每次调用fetchJson/fetchText必须传redirect: error。_http.mjs 默认redirect: follow——这个默认值对 Provider 并不安全因为服务端 302 可能把请求引向内网地址若最终 URL 由portals.yml数据entry.api、entry.careers_url拼装而来必须在任何网络调用之前把主机名校验进 allowlist。参考 providers/greenhouse.mjs 的assertGreenhouseUrl解析 URL畸形直接抛错→ 拒绝非https:协议 → 拒绝不在ALLOWED_GREENHOUSE_HOSTSgreenhouse.mjs中的主机名。测试必须证明该护栏先于ctx.fetchJson/ctx.fetchText执行若整个 URL 由 Provider 从固定的字面量主机拼接不需要 allowlist但redirect: error仍然必需jobvite.mjs 与 telegram-channel.mjs 例外地传redirect: manual一样不跟随任何跳转但抛出的错误携带Location使302 到登录页读起来是具名失败而不是笼统报错。_http.mjs还为此在错误对象上挂.status、.body、.retryAfter、.location等元数据其重试判定isRetryableError会把被拒的 302refused redirect当作确定性失败而非可重试网络错误避免对租户不存在类回答做无谓重试。强制安全护栏二防御式解析fail empty vs fail loudfetch()一旦抛出会丢掉整个目标一家公司/一个职位板该轮扫描的结果——抛错会上浮为 run error并在verify-portals/doctor --strict里呈现为missingboard 404s, will silently drop这是假警报掩盖了诚实的空结果。因此一条畸形数据只应被continue/null.filter跳过绝不能让异常逃出fetch()。具体规则空或无内容 bodynull、{}、[]、{jobs: null}——端点活着、只是没匹配到→ 返回[]结构明显不是端点文档承诺的样子嵌套容器缺失或类型错、键名完全不同→ 允许且通常应该抛带描述的异常把实际拿到的键名说出来让 API 静默变更浮出水面而不是让职位板永远安静地返回 0。参考 providers/ibm.mjs 的parseIbmResponse。scan.mjs也会在fetch()返回非数组时抛错verify-portals捕获它分页 Provider 的循环终止如果读原始页面形状如json.hits.hits.length PAGE_SIZE仅让解析器返回[]不够必须守卫该边界或故意抛错——这是下文绝对页数上限中fail loud vs 交还一个残缺职位板决策的一部分日期日期字符串过Date.parse可能返回NaN。不要写Date.parse(s) || undefined它还会把合法的 epoch0、即1970-01-01时间戳一起清空应使用 NaN 安全的辅助函数且!value守卫只用于解析前的空字段判断function toEpochMs(value) { if (!value) return undefined; const parsed Date.parse(value); return Number.isNaN(parsed) ? undefined : parsed; }缺必填字段title、url的行——过滤掉不要抛错。强制安全护栏三宿主控制 id / slug 的 URL 编码当job.url在.map()/for循环内由响应字段id、slug、refnr拼装时该段必须用safeEncodeURIComponent编码不要用裸encodeURIComponentimport { safeEncodeURIComponent } from ./_safe-url.mjs; // ... const seg safeEncodeURIComponent(job.id); if (seg null) continue; // 或对 .map() 结果做 .filter(Boolean) const url https://example.com/jobs/${seg};原因在文档中讲得很透encodeURIComponent遇到孤立 UTF-16 代理对会抛URIError而\uD800这样的转义能挺过JSON.parse——一次抛出就会离开整个循环scan.mjs的按公司catch随之丢掉该页已经解析的所有 posting。辅助函数改为返回null于是只丢弃那一条坏 posting与没有 id 的 posting同等对待。它刻意返回null而非UFFFD替代符劣化值会以畸形 UTF-8 一路流进data/scan-history.tsv、tracker 和生成的文档而在编码值同时是去重键的场景arbeitsagentur、vdab替代符还会把互不相同的坏 posting 撞到同一个键上。适用范围被严格限定仅限宿主控制的 API 字段在循环里变成 URL 路径段。来自配置的值portals.yml的公司 slug、关键词、locale、已在自己 try/catch 里的调用、已按 slug 字符集校验过的值不做此处理——因为配置里一个坏字符就丢真实 posting 是错误权衡。镜像情形在解码侧对抓来的 href 段做decodeURIComponent遇到畸形百分号转义%ZZ同样抛URIError需用 try/catch 包裹并以原始段兜底参考 workday.mjs、successfactors.mjs、rheinmetall.mjs。共享解码器HTML 实体处理凡是解析 HTML/XML而非 JSON API的 Provider实体amp;、#252;一律通过 providers/_html-entities.mjs 的decodeEntities解码import { decodeEntities } from ./_html-entities.mjs;绝不写本地副本。项目曾因本地重写而反复出 bug#1555、#1639——一个合并了decimal|hex的正则把两种形式悄悄误解析一旦有人重新引入私有解码器源码级测试会直接失败#2902。测试层面只验证本 Provider 的输出确实经过了共享解码器共享解码器自身由 _html-entities.test.mjs 覆盖这类辅助模块测试位于 tests/providers/。强制安全护栏四绝对页数上限MUST分页 Provider 的页数永远不能仅由来源上报值pagination.pageCount、total决定——那是不可信的第三方数据一个增长或遭篡改的响应会把一条portals.yml变成无限请求循环架构上没有按 Provider 的超时只有单请求超时。必须定义与ctx.maxPages、entry.max_pages相互独立的自有常量const DEFAULT_MAX_PAGES 100; // 条目未设 max_pages 时的默认值 const MAX_PAGES_CAP 1500; // 硬上限——即便用户显式覆盖也不超过 // ——两者都不与 ctx.maxPages 或来源上报值挂钩 function resolveMaxPages(entry) { const v entry?.max_pages; if (Number.isInteger(v) v 0) return Math.min(v, MAX_PAGES_CAP); return DEFAULT_MAX_PAGES; }来源上报值只有通过Math.min(...)与该上限相与后才进入公式绝不单独使用。参考 providers/workday.mjsDEFAULT_MAX_PAGES、MAX_PAGES_CAP、resolveMaxPages()源码中真实取值为默认 100 页。当上限截断了列表要警告用户raise max_pages on this entry避免把残缺列表误认为完整列表。来源上报的total还可能只是错了不只是缺失——某些后端静默截断它所以按total限界走完并不能证明完整。参考 workday 通过 facet 拆分恢复截断清单的方案#3310。ctx.maxPages 与健康探针协作当ctx.maxPages存在时verify-portals正在跑活性探针传maxPages: 1而不是扫描。随之而来两条约束限制遍历SHOULD在ctx.maxPages页后停止并跳过任何逐 posting 的fetchDetails详情增强smartrecruiters、vdab——探针用不上它。参考 providers/workday.mjsconst ctxMaxPages Number(ctx?.maxPages); const ctxCap ctxMaxPages 0 ? ctxMaxPages : Infinity; const pagesToFetch Math.min(resolveMaxPages(entry), ctxCap);忽略该提示的 Provider 并不是错误——探针会用硬性的PROBE_REQUEST_BUDGET4 个请求包住ctx.fetchJson/ctx.fetchText超预算的调用抛ProbePageBudgetReached所以无论 Provider 是否配合都受限。但不配合会让探针变慢、真实消耗来源的请求配额且按 Provider 的测试会断言在maxPages: 1下恰好一个列表请求。探针期间不得包裹或吞掉ctx.fetch*的 rejectionMUST仅当你有逐页 catchverify-portals靠err instanceof ProbePageBudgetReached识别预算截断把它解读为端点活着、数量未知而非职位板坏了。如果逐页/逐关键词的catch把它吞成[]或重包成new Error(...)探针会把健康的职位板误判成missing。因此在ctx.maxPages存在时ctx.fetch*的 rejection 必须原样上抛真实扫描无ctx.maxPages下吞掉并保留已得页面的召回优先行为依然合理——providers/vdab.mjs 同时演示了两个分支。raisemax_pages的警告要始终与entry.max_pages/DEFAULT_MAX_PAGES的停止挂钩——绝不能因ctx.maxPages上限或探针预算截断而触发。分页节流与重试pacing retry大型职位板走完一轮是 100 个顺序请求workday、radancy且若干来源在 WAF 后按 burst 限速。两个机制都预期出现在分页 Provider 中页间延迟模块常量只作用于第一页之后——if (page 0) await sleep(INTER_PAGE_DELAY_MS, ctx)。sleep从 _http.mjs 导入它尊重 ctx 提供的测试时钟绝不手写本地副本。150–250 ms 是常态workday.mjs 真实取 250 ms只在实测到节流careerviet.mjs、itviec.mjs 用 750 ms或存在公开速率限制agentic-jobs.mjs 用 2100 ms、30 次/60 秒时才调高对从未报怨的 Feed 不要过度设计有界重试把每个页面请求——以及分页前一次性解析配置的请求——包进fetchJsonWithRetry/fetchTextWithRetry_http.mjs。它们对 429、任何 5xx 与传输错误超时/中止/DNS做指数退避 抖动绝不重试非 429 的 4xx 或拒绝的重定向。Retry-After头会被尊重但被钳制敌对值Retry-After: 86400无法拖死整轮扫描。共享默认策略是{ retries: 2, baseDelayMs: 500, maxDelayMs: 8_000 }_http.mjs需要不同节奏时传第 4 个policy参数workday.mjs、oraclecloud.mjs 因其 API 有 WAF 前置而用{ retries: 3 }。耗尽后的处理是你的决策不是辅助函数的。withRetry重抛错误携带.attempts真实请求次数。逐 Provider 决定保留已收集页面并warnworkday.mjs或宁可响亮失败也不交还静默的残缺职位板a16z-speedrun-talent.mjs。无论哪种raisemax_pages警告都不能在分页因取数错误停止时触发——那句提示的意思是上限截断了健康职位板不是职位板坏了。verify-portals 健康检查的两层覆盖npm run verify:portals与node validate-portals.mjs以及委托给前者的doctor.mjs --strict会同时扫tracked_companies与job_boards——两个列表共享同一 entry 模式与同一启用的名字命名空间职位板与公司同名会被标记。validate-portals.mjs 以两档探测可达性tier 1当tracked_companies的careers_url/api带可辨识的 ATS slug 时直接对 Greenhouse/Ashby/Lever slug 做探针。只有这三个 ATS 会被给出suggested修复因此 fix-slugs.mjs 只重写它们的 slug——job_boards聚合器属于 Provider 层条目永远不会带suggested备选tier 2其余每个条目Workday、SmartRecruiters、品牌化 careers 页、任何job_boardsFeed交给扫描器的 Provider 层并传ctx.maxPages: 1让检查真的调用fetch(entry, ctx)。没有被任何 Provider 认领的条目无provider:、无detect()命中落入skipped——这是覆盖漏洞不是ok--strict只把missing活性探针实际 404标红从不标红skipped。所以 templates/portals.example.yml 中为你 Provider 准备的示例条目见下方清单无论它在哪个列表都必须能被你的detect()认领或带显式provider:。超时与 User-Agent统一走 providers/_http.mjsfetchJson/fetchText/makeHttpCtx——它已内置AbortController超时默认 10 秒_http.mjs慢 Feed 通过在单次调用 options 传timeoutMs调高与共享 User-Agent非 2xx 响应抛出携带.status、.body、.retryAfter的Error。若来源在 WAF/CDN 拦截了默认 UA从同一模块导入BROWSER_LIKE_USER_AGENT——不要自定义自己的常量。ctx.fetchResponse返回经过超时与非 2xx 守卫的原始Response会在计时窗口内读完 body 后重建等价的 Response需要响应头的 Provider如 csod.mjs 读取Set-Cookie为搜索 API 预热会话由此走 ctx 而非重实现 fetch。公共无鉴权与浏览器型扫描器的边界Provider 只读取开放 API/Feed无登录。把用户数据CV、流程管线发给外部服务属于核心之外的范畴见 CONTRIBUTING.md 的 What we do NOT accept。有些来源没有可达 API、只在浏览器里渲染列表scan-interamt.mjs 是先例Dayforce 类 careers 站同形。驱动真实浏览器Playwright的扫描器可以存在但只能作为独立的顶层脚本绝不能写成providers/*.mjs模块且满足三个条件独立成脚本以scan-source.mjs形态发布自带 npm script、测试、SUPPORTED_JOB_BOARDS.md行、portals.example.yml段与SYSTEM_PATHS条目providers/保持纯 fetch只读公共页面读取任何访客可见的内容无登录、无真实用户的会话或 cookie、无鉴权区域不做绕过绝不求解或中转 CAPTCHA绝不伪造他人客户端的 cookie、token 或头。若来源在公共列表前放了交互式挑战扫描器把它报告为具名错误并停止不去绕过。浏览器扫描器比 Provider 更慢更脆标记变更即坏PR 里要说明实测情况哪些页面、多少条列表、来源对裸fetch回答什么——让审阅者看到为什么一个 Provider 不够。测试要求测试文件只有一个tests/providers/{name}.test.mjs被tests/**/*.test.mjs自动发现——无需在test-all.mjs里注册。RSS/HTML Provider 应导出纯解析函数以便直接单测。不要重复验证共享辅助模块它们有自己的测试Provider 测试只检查本 Provider 的输出确实经过了它们。必须覆盖Provider 的iddetect()——URL 模式正向用例不可信主机、非 HTTPS、畸形 URL、null/ 非字符串 / 缺careers_url全部 →null不抛。显式专用entry.provider命中时在无careers_url/api的情况下返回{ url }其余 →nullfetch()对来源真实响应形状的归一化缺必填字段的行被过滤每次请求都传了redirect: error——断言opts.redirect error而不只是调用发生过allowlist 守卫在fetchJson/fetchText被调用之前抛出空或无内容 body →[]非文档承诺形状的 body → 带描述的抛错。两个分支都要断言分页如有即使来源上报更多页Provider 自己的DEFAULT_MAX_PAGES也要能截停ctx.maxPages能更早截停分页 瞬时失败如有第 2 页的 429/5xx 重试无法清除时要么保留 1..N 页并 warn、要么响亮失败——按你选择的行为——且raisemax_pages警告不在该取数错误停止上触发探针协作如分页ctx.maxPages: 1下恰好一个列表请求、无fetchDetails/增强调用且ctx.maxPages存在期间的ctx.fetch*rejection原样上抛——既不吞成[]也不重包参考vdab测试HTML 解析如有带实体的 fixture 标题在关键词匹配之前就解出编码后的amp;不能弄丢这条职位——#2923而不只是调用过decodeEntities。若job.url在循环内由宿主控制的id/slug拼装向共享的 surrogate 行为测试批量文件补一个行为用例一条孤立代理对值 一条干净值 → 不抛、干净 posting 保留、坏值被丢弃该文件还承载跨 Provider 的源码守卫url:行不出现裸encodeURIComponent。Fixture 中真正会进入调用的值name/careers_url/api一律虚构Acme、ExampleCo、BigCo——绝不用真实公司引用真实观察数据的注释如页数常量为何取这个值则欢迎它证明数字并非随意。开发循环node test-all.mjs --only providers/{name}提交前完整跑node test-all.mjs--only不是合并门槛。参考模块速查表你需要什么参考样例简单 JSON API无分页providers/greenhouse.mjs尊重ctx.maxPages的分页providers/workday.mjs用共享decodeEntities抓 HTMLproviders/icims.mjsHTML 内 SSR JSON__NEXT_DATA__providers/join.mjs进程内解析 RSSproviders/larajobs.mjsjob.url用safeEncodeURIComponent拼宿主控制id/slugproviders/phenom.mjs、providers/bamboohr.mjs共享辅助实现的重试/退避默认策略providers/a16z-speedrun-talent.mjs、providers/getro.mjs共享辅助实现的重试/退避策略覆盖providers/workday.mjs、providers/oraclecloud.mjs探测被钳制的total用查询扇出 去重恢复providers/workday.mjsfacet split需要与共享默认{ retries: 2, baseDelayMs: 500, maxDelayMs: 8_000 }不同节奏的 Provider给fetchJsonWithRetry/fetchTextWithRetry传第 4 个参数policy: { retries, baseDelayMs, maxDelayMs }。每个样例在 tests/providers/ 下都有同名.test.mjs与之配对是实现 测试如何一一对应的最直接教材。Pre-PR 完整清单新增 Provider 的合并前清单编辑既有 Provider 时同样适用变更即同步数据源资格仅职位板/聚合器/人才网络来源通过 Source Indexing Policy——真实可归属雇主的列表、对候选人免费、一 Provider 一来源非其他职位板的 meta 聚合器运营方自营或边界情形 → 先开 source proposal。单公司 ATS 适配器跳过此项URL 归属若 posting 载荷同时带上游雇主 URL 与来源自身页面job.url取雇主链接规则 2、来源页面兜底单来源 ATS 只有一个 URLid唯一文件名不以_开头detect()对垃圾输入绝不抛错以返回null代替失败每个网络调用传redirect: error配置派生的 URL 在请求前过 allowlistfetch()对空/无内容 body 返回[]null/{}/[]/{jobs: null}对真实 API 错误或非文档承诺形状的 envelope 抛错单条坏行跳过continue/null.filter不致命日期 NaN 安全toEpochMs模式HTML/XML 实体走 providers/_html-entities.mjs不写本地副本job.url若来自逐 posting 的id/slug过safeEncodeURIComponentnull→ 丢弃该 postingurl:行无裸encodeURIComponent抓取的 href 段进decodeURIComponent时用 try/catch 并以原始段兜底分页有自有DEFAULT_MAX_PAGES——页数不由来源单独决定pageCount/total分页尊重ctx.maxPages存在时跳过fetchDetails增强探针期间逐页catch把ctx.fetch*rejection原样上抛保住ProbePageBudgetReached的身份分页页间延迟走共享sleep仅第一页之后页面请求包进fetchJsonWithRetry/fetchTextWithRetry声明耗尽策略保留部分 warn或响亮失败且不误触发 raisemax_pages 警告测试tests/providers/ 下的{name}.test.mjs覆盖上文第三节全部要求全量测试node test-all.mjs全绿不只--only文档同步一在 docs/SUPPORTED_JOB_BOARDS.md 按职位板名字母序加一行表格是排序的文档同步二templates/portals.example.yml 三件事① 若detect()匹配主机在 Provider auto-detection 下加 URL 模式行否则在段内写显式provider:② 在 Built-in provider examples 块加一段带注释的Example {Name} Co段所有字段用默认值所在分组的标题格式(→ tracked_companies: …)/(→ job_boards: …)照抄相邻分组对无detect()的 Provider还要把 id 加进 job boards / aggregators … explicitprovider: 列表URL 模式detect()已被 ① 覆盖③ 在匹配的区域/主题段加一条真实、未注释的条目仅当它比 ② 多携带信息时——真实careers_url/api、可解析 slug 或 board id、或 Provider 专属键每个公司 ATS以及 Getro 这类带 slug/URL 的职位板都满足。无逐条目 URL 或配置的裸provider: {id}Feed区域职位板、RSS Feed跳过 ③——真实条目与 ② 逐字节相同编辑既有 Provider 而非新增上面的文档项同为变更即同步要求。若修复改变了可观察行为分页、默认值、URL 格式、什么算错误或空职位板用 Provider 名和变更实质不只是一个函数名去grep仓库里每一处相关描述并同 PR 修复。小结career-ops 的 Provider 层把接入一个新职位源从一次性的抓取脚本升级成了受契约与护栏约束的插件机制文件系统约定即注册、三条显式路由优先级、统一的Job归一化、SSRF/解析/编码/页数四类强制护栏、与健康探针的协作协议以及覆盖到行为细节的测试要求。理解 providers/_registry.mjs 的加载路由与 providers/_http.mjs 的传输语义再对照greenhouse简单 JSON、workday复杂分页 上限、vdab/smartrecruitersfetchDetails增强 探针分支等参考实现新 Provider 的开发、评审与长期维护就都有了可复制的基准。【免费下载链接】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),仅供参考
返回列表