2026/9/7 2:49:01

用 Apify Actor 扩展 career-ops 职位扫描:接入 keyed provider 的完整实战

用 Apify Actor 扩展 career-ops 职位扫描:接入 keyed provider 的完整实战 用 Apify Actor 扩展 career-ops 职位扫描接入 keyed 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 求职工作流career-ops中加入付费/外部职位源如 Indeed、LinkedIn 或任意 Apify 平台上的现成爬虫 Actor的开发者。文章以 skill.md 为主线系统讲解如何在portals.yml中配置provider: apify、通过field_map把 Actor 返回的数据集字段映射为扫描器认识的 Job 结构并结合 index.mjs 与 _apify.mjs 的源码深入解析底层运行机制、JD 本地缓存与安全边界。读完后你将能在不触碰零密钥核心的前提下把任何一个带 API Token 的 Apify Actor 变成自己的简历职位源。什么是「keyed provider」它与零密钥提供方的边界career-ops 的providers/目录坚持零密钥、本地优先原则绝大多数招聘网站Greenhouse、Lever、Workday 等都能免费直连。但有些职位数据只存在于需要 API Token 或需要付费执行的第三方平台上——这类「需要密钥」或「对接外部服务」的集成被归入独立的插件层plugins/并按插件类型注册到 manifest 中具体机制见 plugins/README.md。Apify 插件就是其中之一其标识为career-ops-plugin-apify。它的核心职责一句话可以概括为运行一个指定的 Apify Actor然后把该 Actor 输出数据集dataset里的条目映射成扫描器需要的{ title, url, company, location }Job 结构。它属于 provider 类插件manifest 中hooks: [provider]因此调用签名与providers/_types.js中定义的核心 provider 完全一致返回Job[]由引擎而非插件自身负责写入data/pipeline.md。这样即使插件出问题也不会破坏整个 Web 前端依赖的数据格式。关键行为约定它只在portals.yml中显式声明了provider: apify的条目上触发永远不会通过 URL 自动检测命中。这一点有两层含义插件的detect()直接返回null见 index.mjs即便有人手动写了detect插件引擎在合并时也会强制把它置空见 plugins/_engine.mjs 中mergeProviderPlugins的「detect-EXEMPT」保证目的就是让一次普通node scan.mjs不会因为误检测而突然打向一个需要付费/密钥的第三方接口。启用前置条件两道门都必须通过插件系统默认关闭且遵循「fail-open」设计——没有config/plugins.yml时核心的运行行为与未引入插件前完全一致不会读取任何.env。要让 apify 插件真正加载必须同时满足两个条件详见 plugins/README.md 与 config/plugins.example.yml在config/plugins.yml中启用它参考示例cp config/plugins.example.yml config/plugins.yml并把apify.enabled设为true。config/plugins.yml属于用户文件被 gitignore永远不会被自动升级覆盖。把密钥放进自己的.env插件在 manifest.json 中声明requiredEnv: [APIFY_TOKEN]因此必须在.env中加入APIFY_TOKENapify_api_xxxxxxxxxxxxxxxx注意密钥值只能放.env绝不能写进config/plugins.yml那是非密钥配置区。想确认还缺什么运行node doctor.mjs # 列出每个插件及其密钥是否就绪 node plugins.mjs list # 或查看插件发现/启用状态plugins.mjs skill apify则可以在命令行随时调出本文讲解的这份 how-toskill 是按需被 Agent 拉取而不是自动注入到 Agent 上下文中参见 docs/PLUGINS.md 中的 skill 机制。portals.yml 条目配置详解在portals.yml参考完整样例 templates/portals.example.yml中为每个你想通过 Apify 抓取的职位源添加一条tracked_companies或job_boards条目。skill.md 给出的标准形态如下tracked_companies: - name: Indeed — VP Engineering (Chicago) provider: apify actor: misceres/indeed-scraper input: { position: VP of Engineering, location: Chicago, IL, maxItems: 25 } field_map: title: [positionName, title] # array first non-empty wins url: url company: [company, companyName] location: [location, formattedLocation]各字段含义与约束如下表字段必填说明name是该条目的显示名会出现在扫描日志与 pipeline 中provider: apify是必须显式声明插件只认这一个显式开关不做自动检测actor是Apify 上的 Actor 标识形如owner/actor-name例如misceres/indeed-scraper。缺失时会直接报错apify: entry name missing actorinput否传给 Actor 的启动输入 JSON 对象完全透传给 Actor。示例中即position/location/maxItems等实际可写什么取决于具体 Actor 的输入 schemafield_map是定义数据集条目字段到标准 Job 字段的映射title与url必须存在company、location、description可选详见下节defaults否当某个字段在条目中缺失时提供的兜底值仅允许title/url/company/location四个键timeout_ms否覆盖默认的 Actor 运行端到端超时默认 180 秒单位为毫秒其中input与 Actor 平台侧的输入契约绑定。从源码看runActor会把input原样作为 JSON 请求体 POST 到https://api.apify.com/v2/acts/actor/runs若 Actor 实际需要更多参数如proxyConfiguration、maxResults等同样加进input对象即可——插件层对此不做白名单限制完全由 Actor 决定。field_map数据集字段到 Job 结构的映射规则这是 apify 插件最具价值的配置点它让同一套插件可以适配成千上万个字段命名各异的 Apify Actor。规则如下单个字符串表示直接取该键url: url等价于取条目对象的item.url。字符串数组表示按顺序尝试的 fallback 列表「第一个非空的值胜出」title: [positionName, title]表示优先读positionName若为空或不存在再读title。不同 Actor 输出的同义字段如company与companyName正是用这种方式兼容的。键支持点路径访问深层嵌套pickField用p.split(.)逐层下钻例如location: geo.formatted可以读取嵌套对象中的值见 index.mjs 的getPath。每一项的值要么是字符串、要么是非空字符串数组任何其他形态都会在配置加载期直接报错而不是扫描到一半才崩溃。校验逻辑isFieldSpec与fetch入口处的逐项检查保证title/url为必填、company/location/description为可选。字段映射完成后会经过defaults 兜底与净化工序defaults中不属于白名单title/url/company/location的键会被忽略非 https 的 URL 会被直接丢弃isHttpsUrl通过new URL()校验协议必须是https:从而避免javascript:、data:、file:、http:等 URL 进入可点击的 pipeline 输出或缓存文件名哈希没有title或没有合法url的条目会被过滤掉。底层运行原理start → poll → fetch 三段式调用如果你想知道一次node scan.mjs背后到底发生了什么源码在 _apify.mjs。这段代码由社区 PR 移植而来文件中注明来自 #693 的贡献同时承载了 #791/#1202 的 LinkedIn-via-Apify 用例并且刻意选择了异步模式启动 → 轮询 → 拉取数据集而不是/run-sync-get-dataset-items长轮询接口——后者会在整个 Actor 运行期间保持一条 HTTP 连接打开部分网络环境或 Windows SChannel 会在 Apify 刷出响应前把连接掐断。核心入口runActor(actorId, input, { timeoutMs, token })的完整生命周期startRunPOST /acts/normalized-actor/runs请求体为input鉴权走Authorization: Bearer token请求头——刻意不用?token查询串形式避免 Token 泄漏到 HTTP 访问日志或任何带 URL 的错误行中。返回runId。waitForRun以 3 秒为间隔轮询GET /actor-runs/runId直到出现终止态之一SUCCEEDED、FAILED、ABORTED、TIMED-OUT。fetchDatasetItems运行成功后GET /actor-runs/runId/dataset/items取回数据集数组并校验返回必须是数组。值得注意的超时与重试设计常量均定义在 _apify.mjs 顶部常量默认值作用DEFAULT_RUN_TIMEOUT_MS180_000整个 run 的端到端时间预算可被条目上的timeout_ms覆盖POLL_INTERVAL_MS3_000轮询 Actor 状态的时间间隔PER_REQUEST_TIMEOUT_MS15_000单次 HTTP 请求的超时拉数据集时放宽为 2 倍CONNECT_RETRY_ATTEMPTS3冷连接偶发超过 Undici 10s 内部连接超时时的重试次数退避为 500ms × 第几次一个容易被忽视但很关键的设计单一 deadline 贯穿 startRun → waitForRun → fetchDatasetItems 三个阶段因此调用方设置的timeout_ms是「端到端上限」而不仅仅是等待循环的上限。若运行在期限内未结束插件会先POST /actor-runs/runId/abort尽力停止 Actorfire-and-forget且不计入自身耗时预算避免白白浪费 Apify 积分然后抛出Apify run id did not finish within Ns的明确错误。重试方面只对瞬时/5xx 类错误退避重试4xx如 401/403 Token 失效、404 run 不存在不会白等直接抛出。另外normalizeActorId用严格正则^[A-Za-z0-9][A-Za-z0-9_.-]*[~/][A-Za-z0-9][A-Za-z0-9_.-]*$校验 Actor 标识并统一为owner~actor形式——既兼容 Apify 同时接受的/与~两种写法也从源头杜绝了畸形配置夹带/、..、?、#等字符、把 Token 打到api.apify.com上非预期路径的可能。配合整个 helper 只从单一硬编码 basehttps://api.apify.com/v2构造请求插件事实上只能访问 Apify API 这一个出口与 manifest 中allowedHosts: [api.apify.com]互相印证manifest 层面的声明用于 doctor/review 的可见性展示。可选深化用 field_map.description 缓存 JD 到本地这是 skill.md 提到的「Then」段落中最实用的进阶能力。若在field_map中加入description键插件就会把职位描述JD抓下来并缓存到本地jds/目录让后续的 JD 抓取、全文评估不再依赖远端页面。写入流程如下htmlToText把 Actor 返回的 HTML 描述清洗为纯文本先剔除script/style块把br转行、/p转空行、li转列表符再循环剥掉残余标签并顺序解码nbsp;、lt;、gt;、quot;、数字实体最后解码amp;刻意把amp;放最后保证amp;#60;能往返成#60;而不是被过早解成。它是有意为之的「轻量解析」够下游做摘要与全文评估用但不追求成为完整 HTML 解析器。若清洗后正文长度 ≥ 50 个字符MIN_JD_BODY_CHARS则把文件写入jds/company-title-url-hash.md。文件名由「公司-职位」slug 加「URL 的 SHA-1 前 10 位」构成保证两个同公司同职位但 URL 不同的帖子不会互相覆盖写入用flag: wx原子创建防止并发 worker 下的 TOCTOU 竞态可从测试与注释中确认这是针对多 worker 场景的设计。文件带 YAML frontmattertitle/company/url/location/scraped/sourcesource是actor的规范化标签。值都经过yamlEscape转义杜绝标题中的引号或换行破坏 frontmatter 结构。返回给引擎的 Job 中url被替换为local:jds/xxx.md这种本地路径原远端 URL 保存在_remote_url字段后续评估链路优先读本地文件、省一次网络请求。写入失败例如 FS 异常时不会让扫描崩溃插件打印一条警告并回退到远端 URL保证职位不丢。若清洗后正文过短说明 Actor 的 description 字段本身没内容同样回退到原始 URL。安全与信任模型插件不能做的事情career-ops 的插件体系是「决策辅助工具而非自动投递机器人」这一点对 apify 插件同样成立详见 plugins/README.md 的 Trust model 与 HOOK_KINDS 说明manifest 强制humanInTheLoop: true插件钩子集合中不存在submit/apply 这类自动提交能力插件返回的Job[]由引擎统一写库插件本身无法直接破坏 Web 端读取的数据格式Actor 返回的数据被视为不可信输入URL 仅收 httpsHTML 中的脚本、样式被剥离标题/描述会做 YAML 转义该插件是「允许自己持有 fetch」的少数例外其客户端只请求单一硬编码主机、且 actorId 被严格校验而一般插件应通过ctx.fetch走带 allowedHosts 白名单与逐跳重定向校验的守卫网络。若插件未启用、或缺少 Token扫描不会静默失败mergeProviderPlugins会注册一个「inactive stub」其fetch会抛出带操作指引的错误例如提示补.env或运行node doctor.mjs把「未知 provider」式的困惑降到最低。常见错误速查报错信息原因与处理APIFY_TOKEN not set — enable apify in config/plugins.yml and add the token to .env密钥缺失。确认config/plugins.yml中apify.enabled: true且.env含APIFY_TOKENapify: entry name missing actor (e.g. misceres/indeed-scraper)portals.yml 条目忘了写actorapify: invalid actorId ... Expected owner/actor or owner~actor ...Actor 标识格式非法或含特殊字符修正为owner/name形态apify: entry name has invalid field_map. ... title and url are required.field_map结构不合法各值必须是字符串或非空字符串数组且至少含title与urlApify actor id finished with status FAILED/ABORTED/TIMED-OUT: msgActor 自身运行失败看statusMessageABORTED/TIMED-OUT 多为输入参数不当或超时Apify run id did not finish within Ns超过端到端预算默认 180s已自动 abort 止损。可增大条目上的timeout_msplugin apify inactive: ...插件未启用或完整性/许可检查未过按提示运行node doctor.mjs、node plugins.mjs trust apify或enable apify运行与验证完成配置后一切照常node scan.mjs扫描器会为该条目运行 apify provider取回的数据集条目经field_map归一化后与其他任何职位源一样经过后续的 title/location/content 过滤、去重并写入 pipeline 供评估使用。你可以在 index.mjs 的 providerfetch中看到完整的数据流取回 items →normalizeItem归一化 → https 校验 → 可选 JD 本地缓存 → 过滤空 title/url → 返回Job[]交给引擎。延伸阅读plugins/apify/skill.md —— 本文对应的插件使用说明原始文档plugins/apify/index.mjs —— provider 钩子、字段映射、JD 缓存逻辑plugins/apify/_apify.mjs —— Apify API 传输层start/poll/fetch、重试与超时plugins/apify/manifest.json —— 插件声明requiredEnv/allowedHosts/humanInTheLoopplugins/_engine.mjs —— 插件引擎发现、启用门槛、ctx 构造、detect 强制置空config/plugins.example.yml —— 启用示例plugins/README.md 与 docs/PLUGINS.md —— 插件架构、信任模型与开发指南【免费下载链接】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),仅供参考