首页
/ career-ops 源索引日志详解:用可验证证据审计每一条招聘源索引决策

career-ops 源索引日志详解:用可验证证据审计每一条招聘源索引决策

2026-09-04 17:01:34作者:庞队千Virginia

docs/SOURCE_INDEXING_LOG.md 是 career-ops 的数据来源审计账本:每一个通过 Source Indexing Policy 的招聘源(job board、ATS、人才网络)都会在这里留下一条可复核的条目——谁提议的、检查了哪些规则、用何种方式验证、结果如何。读完本文,你能理解 career-ops 如何在"索引不背书、分发不欠付"的原则下,用采样数据与源码行为约束每一个源的收录决定,并能复现日志中展示的抽样审计方法,将其应用到自己的数据源接入场景中。

这份日志记录什么、刻意不记录什么

日志的第一原则是外部可重建性:一个陌生人不询问任何人,仅凭日志就能重建任何一条收录决策的全过程——提案内容、提案人、检查的规则、验证方式。

它刻意不是两样东西:不是一个排名,也不是一个承诺。第 4 条规则说得很直白——索引不等于背书(indexing is not endorsement),分发也不欠付(distribution is not owed)。源被收录意味着其职位会出现在安装用户面前,但这不构成项目对任何源的流量、位置或持久性的义务。

理解日志条目时还有一个关键语义约定:

  • "Verified" 的含义:有人执行了一条命令并如实报告了输出,而不是某个声明被接受了。验证是动作,不是表态。
  • 在线检查会过期:当某项检查只能对着活端点(live endpoint)做时,条目会注明采样了什么、何时采样。一个源 8 月通过验证,11 月可能已经漂移——重新验证是常态,不是指控。日志中 remotli 条目的采样数据(392/921)与 providers/remotli.mjs 注释中记录的数据(395/925)就是略有不同的两个时点快照,这正是"活检查过期"原则的实证。

条目的产生时机:当一个源的 provider 被合入时记录。任何人都可以发起 source proposal(issue 模板),讨论发生在 issue 和 PR 中,而这份文件是持久的摘要,链接回两者。

五条规则:每个条目背后的审计标准

日志中的每条 Rule 检查都指向 CONTRIBUTING.md 中定义的 Source Indexing Policy。这是每个源必须跨过的统一标准线——无论提议者是谁,包括源运营商提交自己的 board。政策不评判源的商业模型,只评判它的数据。五条规则及其对应的 Manifesto 权利如下:

规则 内容 对应原则
1. 索引什么 职位必须真实、归属于可识别的雇主、对候选人免费阅读和应用 Manifesto 第 4 条:"你永远不付费"——对候选人设置付费墙(职位或申请流程)的源不予索引
2. 规范 URL 每条职位携带源所暴露的"通往雇主的最短可验证路径"(有 ATS 或直投 URL 时优先);源自身页面只能作为次级归属 数据层保证候选人直达雇主
3. 付费位置到不了候选人 推广内容不能购买 career-ops 中的位置:排序在用户本地机器上发生,provider 遍历源的完整库存,维护者审计源是否存在响应偏差(API 总数 vs 站点总数、分页分布) Manifesto 第 8 条:"你的 agent 为你工作,不为平台、不为雇主"——在数据层强制执行
4. 索引不是背书,分发不是义务 源以运营商声明(operator declared)方式列名;没有任何单一源可以超过注册表的 40% 防止任何源——无论多大规模——主导注册表
5. 聚合层属于项目 provider 只读自己的源;跨源聚合、排序、匹配与注册表都在 core,永不委托给源 保持 first-party 边界

条目一:remotli.ch —— 书面政策下审查的第一个源

提议人 @eliador90,该源的运营商(在提案中声明)
Provider providers/remotli.mjs
PR / Issue #2465 / #2464
合入日期 2026-08-07
状态 Listed

这是书面政策下审查的第一个源。运营商提议自己的 board,恰恰是这些规则存在的用例:披露在先,规则做出裁决,而不是一场对话。

Rule 1:真实职位、可识别雇主、对候选人免费

验证方式:职位解析到具名雇主,源端没有候选人侧付费墙或注册要求。

Rule 2:规范 URL 是雇主的

Provider 优先使用每条职位的上游 applyUrl,仅当该字段缺失或非 https: 时回退到 board 页面。验证方式为采样 121 行,覆盖活 feed 的第 1、9、19 页:零行缺少雇主 URL、零行非 https、零行回指 remotli.ch。主机分布是普通的雇主基础设施(Greenhouse、Workday、Ashby、Lever、Recruitee、公司招聘页),中间没有跟踪跳板。

源码印证这一行为:resolveUrl()applyUrlnew URL() 解析并校验 https: 协议,接受任意 https 源(因为该 URL 仅展示、本 provider 永不抓取它),slug 走正则 ^[a-z0-9._~-]+$ 校验后才拼入回退的 board 页。注释(L33-L43)还点出一个设计收益:同一职位若在 remotli 与雇主直连 ATS provider 上交叉列出,会解析到同一 URL,从而精确去重,不再仅依赖 SimHash 指纹兜底。

Rule 3:完整库存、无付费位置

运营商在自己的提案中就披露了覆盖缺口:不带 remote=all 参数时,feed 只提供 392/921 个职位(占整个 board 的 42.6%)。合入的 provider 遍历全部 19 页。载荷中不存在任何 promoted 或 sponsored 字段。

这条规则在源码中对应 providers/remotli.mjsALL_WORK_MODES = 'remote=all' 常量,注释解释了为什么不能显式枚举四种工作模式:显式命名会返回 924(漏掉一行没有工作模式的职位),且 board 下次新增工作模式就会过期——所以 board 方加了一个语义为"不做工作模式过滤"的单一标志位。同时 L75-L76 明确了职责边界:过滤是消费者(Rule 5 意义上的下游)的工作,provider 全量取回,由 scan 侧的内容/位置过滤器决定去留。

分页行为同样服务于完整库存:服务器把 limit 硬顶在 50(请求 ?limit=200 仍返回 50),因此 PAGE_SIZE = 50 且默认最多走 20 页(L78-L81)。

Rule 4:运营商声明

docs/SUPPORTED_JOB_BOARDS.md 中的 Remotli 行携带 operator: eliador90 并链接回该政策。

Rule 5:聚合留在核心

Provider 只读自己的源。排序与跨源处理留在核心中。

政策之外的额外检查

  • 零鉴权:一条不带 key、cookie 或 session 的普通 curl 就能拿到 feed。
  • 安全审查:评审中提出的两个 HIGH CodeQL 告警在合入前解决。源码注释(L98-L107)记录了来龙去脉:早期版本在实体解码前后各 strip 一次标签,以补偿 board 端逐行混发实体编码/原始 HTML 的缺陷,结果触发 js/double-escapingjs/bad-tag-filter 两个 HIGH 告警;当 API 改为出口即解码后,补偿性 pass 被删除,htmlToText() 回归"strip 一次再 decode"的规范顺序。

状态过滤:fail-closed 的实现细节

remotli.mjs 的 normalizeRemotliJob() 中,状态检查采用拒绝一切未知策略:只放行严格等于 active 的行,缺失或非字符串状态一律丢弃。注释解释了两种失败模式的不对称性:API 漏发 status 字段会把已关闭职位静默发布成死链,而拒绝未知状态只是产生一个"可见的空白 board"——后者能被 verify-portals 健康探针捕获。分页循环同样贯彻这一思路(L278-L313):第 1 页失败必须抛出(无法区分"活 board"与"坏 board");一旦某页成功解析,后续瞬时故障只保留已收集页面而不丢弃全部结果。所有请求经 assertRemotliUrl() 锁定 HTTPS + remotli.ch 主机,并配合 redirect: 'error' 防止服务端重定向型 SSRF。

该 provider 的用户侧配置形态(见源码头注释 L17-L23):

# job_boards 或 tracked_companies 条目
- name: Remotli (Swiss remote board)
  provider: remotli
  careers_url: https://remotli.ch/
  enabled: true

单元测试位于 tests/providers/remotli.test.mjs,覆盖 normalizeRemotliJob 的导出函数。

条目二:a16z speedrun talent network —— 促使政策诞生的案例

提议人 @justma16ze(社区贡献者,与源无关联)
Provider providers/a16z-speedrun-talent.mjs
PR #2231
合入日期 2026-07-29
状态 Listed

它合入时政策尚未写成,而被记录于此,因为它正是促使政策诞生的案例:一个庞大、关系紧密的人才网络,恰好让"索引是否意味着背书"从理论问题变成现实问题。

Rule 3 的追溯适用:两个覆盖缺陷

两个覆盖缺陷在列名之后被发现并修复,且都是由贡献者阅读活 feed(而非代码)发现的:

  1. 页尺寸错误(#2419):page size 被设为 100,而 feed 每页实际提供 50 条——抓取在第一页后静默停止
  2. 瞬时故障中断全程(#2506):单次上游瞬时失败会中止整个 board 的抓取。

两者都是 Rule 3 所针对的失败模式:读起来像完整的部分覆盖(partial coverage that reads as complete)。

源码级的修复证据

对照 providers/a16z-speedrun-talent.mjs,两个修复都清晰可查:

  • 页尺寸对齐PER_PAGE = 50L28)与 feed 实际能力一致;DEFAULT_MAX_PAGES = 6(即默认 300 职位扫描),MAX_PAGES_CAP = 1000 作为失控边界而非覆盖目标(L29-L37)。
  • 带重试的抓取:每次分页请求走 fetchJsonWithRetry()(来自 providers/_http.mjs),对 429/5xx/超时做有界重试,支持解析 Retry-After 头。重试耗尽后错误仍会抛出——正如 provider 注释 所写:这个 board 会分页到上百页,中途一次抖动曾使 provider 整体返回空;对 a16z 而言"响亮的空结果"优于"静默的部分 board"。共享层刻意不在重试耗尽后替调用方做决定(_http.mjs L210-L217):workday provider 选择截断租户并带警告保留已得页面,而 a16z 选择大声失败——重试策略的调用方语义按 provider 而不同。
  • 终止条件的三级优先级L179-L202,对应 #2547):① 空页总是结束迭代(同时约束虚报 total_pages 的 feed);② feed 自报的 total_pages(若存在且为正)——因为扫描中途职位被删导致的 49/50 短页不是末页,把它当末页会让清扫在干净退出码下静默结束;③ 短页检查仅作 total_pages 缺失或非正时的降级兜底。非正的 total_pages(如与 50 行实际数据并存的 0)视为自相矛盾、按缺失处理。
  • 截断可见化L203-L210):当 feed 页数超出被允许的读取量时,向 stderr 打印警告并附修复方式(调高该条目的 max_pages,或用 q: 收窄服务端搜索)。这与 Rule 3 的精神直接呼应——部分覆盖必须可见,不能伪装成完整。

该 feed 的其他审计相关事实(源码头注释 L6-L24):公开零鉴权 JSON 端点,OpenAPI 位于 /api/v1/openapi.json;每个请求携带文档化的可选归属参数 source=career-ops,响应会回显它,但不改变结果;q:(或 keywords:)触发 feed 的服务端全文搜索(含同义词扩展)。URL 输出侧同样 host-locked 到 speedrun-talent-network.com,离站或非 https 的 url 直接丢弃该职位(normalizeSpeedrunJob())。单元测试位于 tests/providers/a16z-speedrun-talent.test.mjs

Rule 4:运营商声明与 40% 上限

该源以运营商声明方式列名。Rule 4 的 40% 上限确保没有任何单一源——无论其规模多大——主导注册表。

从日志中提炼的可复用审计清单

两条条目合起来,展示了把政策规则转化为可执行检查的完整方法,值得任何数据源接入项目借鉴:

  1. 抽样要跨分页分布:不只看第 1 页,而是采样第 1、9、19 页共 121 行,并统计"零缺少雇主 URL / 零非 https / 零回指自身"三类硬指标。
  2. 主机分布人工过目:核对解析出的 URL 主机集合是否落在合理的雇主 ATS 基础设施上,检查有无跟踪跳板。
  3. 完整库存核对:将 API 报告总量与站点可见总量对比(响应偏差审计),确认过滤参数(如 remote=all)不是隐性截断;用代码常量(PER_PAGE)与 feed 实际页尺寸对照,防止"静默单页"。
  4. 零鉴权验证:无 key/cookie/session 的裸 curl 必须能复现 feed。
  5. 故障模式演练:第 1 页失败、中途瞬时失败、短页、虚报总页数——每种情况的行为都应明确(大声失败、保留已得页面、截断警告),并有测试覆盖。
  6. 声明与链接:支持源表的行内携带 operator: 声明并链接回政策;安全告警(如 CodeQL)在合入前清零。

参与方式:如何提出一个新源

CONTRIBUTING.md 的流程:

  • 通过 source proposal issue 模板发起,逐条走查五条规则;直接带 provider 提 PR 也可以,合入前同样适用这五条规则
  • 运营商声明在列名前带外验证:一个在该源自有域名下可达的联系方式,或等效的域名控制权证明。
  • 运营商提议自己的 board 是被欢迎的——"这正是规则化门槛的用武之地"(remotli 条目就是这一点的实例:披露在先,规则裁决)。
  • provider 的编写规范参见 providers/ADDING_A_PROVIDER.md,列名状态查询 docs/SUPPORTED_JOB_BOARDS.md,决策历史回查 docs/SOURCE_INDEXING_LOG.md

这套机制的核心价值在于:收录决定不依赖维护者的主观好恶,而依赖可以被任何外部人用相同命令复算的证据。活检查会过期、覆盖会漂移,所以日志允许重新验证、允许追溯修复——但每一次决定都留有可重建的纸面痕迹。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
981
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384