career-ops 源索引日志详解:用可验证证据审计每一条招聘源索引决策
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() 对 applyUrl 做 new 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.mjs 的 ALL_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-escaping与js/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(而非代码)发现的:
- 页尺寸错误(#2419):
page size被设为 100,而 feed 每页实际提供 50 条——抓取在第一页后静默停止。 - 瞬时故障中断全程(#2506):单次上游瞬时失败会中止整个 board 的抓取。
两者都是 Rule 3 所针对的失败模式:读起来像完整的部分覆盖(partial coverage that reads as complete)。
源码级的修复证据
对照 providers/a16z-speedrun-talent.mjs,两个修复都清晰可查:
- 页尺寸对齐:
PER_PAGE = 50(L28)与 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、9、19 页共 121 行,并统计"零缺少雇主 URL / 零非 https / 零回指自身"三类硬指标。
- 主机分布人工过目:核对解析出的 URL 主机集合是否落在合理的雇主 ATS 基础设施上,检查有无跟踪跳板。
- 完整库存核对:将 API 报告总量与站点可见总量对比(响应偏差审计),确认过滤参数(如
remote=all)不是隐性截断;用代码常量(PER_PAGE)与 feed 实际页尺寸对照,防止"静默单页"。 - 零鉴权验证:无 key/cookie/session 的裸
curl必须能复现 feed。 - 故障模式演练:第 1 页失败、中途瞬时失败、短页、虚报总页数——每种情况的行为都应明确(大声失败、保留已得页面、截断警告),并有测试覆盖。
- 声明与链接:支持源表的行内携带
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。
这套机制的核心价值在于:收录决定不依赖维护者的主观好恶,而依赖可以被任何外部人用相同命令复算的证据。活检查会过期、覆盖会漂移,所以日志允许重新验证、允许追溯修复——但每一次决定都留有可重建的纸面痕迹。
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 StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00