career-ops 插件机制全解:从 opt-in 安装、安全信任模型到发布自有插件的完整指南

📅 发布时间:2026/9/8 21:21:19
career-ops 插件机制全解:从 opt-in 安装、安全信任模型到发布自有插件的完整指南
career-ops 插件机制全解从 opt-in 安装、安全信任模型到发布自有插件的完整指南【免费下载链接】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 是一个零密钥zero-keys、本地优先local-first的开源求职自动化项目扫描求职板、把职位整理成结构化 A-H 报告、定制简历并追踪申请。本文围绕 docs/PLUGINS.md 展开讲解其插件层plugins——这是把需要 API Key 或要访问外部服务的集成收纳进来的 opt-in 扩展机制。读完你将掌握如何用node plugins.mjs安装、启用与审核插件四种信任徽章的精确含义插件 manifest 与五类 hook 的完整契约以及如何把自己的插件发布进社区注册表并成为 bundled 插件的受维护继承者。注意本文所述的插件并非 Claude Code 插件.claude-plugin/。两者无任何关系——这里讨论的是扩展career-ops 自身的插件系统。插件层为什么存在opt-in、默认关闭、加法式扩展career-ops 的核心设计原则是零密钥、本地优先一个干净的环境不需要任何 API Key 就能完成扫描、评估、简历定制与追踪。但真实的求职流程必然要接触外部服务——Gmail 收件箱里的职位提醒、Notion 数据库、Apify actor、Google Calendar 的面试事件等而这些集成需要密钥或网络访问。插件层就是给这类需求准备的默认关闭的扩展区。正如 plugins/README.md 所述插件仅在你显式 opt-in 时才加载。没有config/plugins.yml时核心功能与从前完全一致——不运行任何插件代码、不读取.env、一切不变。这意味着插件是纯加法的不启用任何插件核心运行方式与没有这个特性时逐字节相同该不变量由 plugins/_engine.mjs 顶部注释与test-all.mjs共同保证。同时插件机制是经过验证的providers/自动加载模式的推广——放入一个带 manifest 的目录即可被自动发现。需要澄清的是插件层与扫描器 provider是两个层面零密钥的 provider如各种求职板抓取器仍留在providers/保持纯净只有需要密钥或与外部服务通信的集成才进入plugins/。使用插件CLI 命令与双重闸门启用条件常用命令一览plugins.mjs是插件层的显式 CLI 宿主入口实现见 plugins.mjs核心命令如下node plugins.mjs list # 已安装的插件 它的信任徽章与状态 node plugins.mjs available # bundled 插件 社区已批准的插件registry 中的条目 node plugins.mjs add name # 安装一个已批准的社区插件 node plugins.mjs enable id # 先展示能力卡片追加 --confirm 后正式启用 node plugins.mjs skill id # 打印插件的 how-to如果它携带 skill.md除文档列出的命令外源码还提供另外几个生命周期命令见 plugins.mjs 的 main 分发逻辑node plugins.mjs run id [hook] [args…] [--dry-run] # 显式运行某个插件的 hook node plugins.mjs new name # 脚手架生成 plugins.local/name/ node plugins.mjs trust id # 审核后重新固定re-pin一个被篡改检测挡下的插件 node plugins.mjs remove id # 删除插件plugins.local lock 条目 配置置为禁用其中run是 ingest / search / export / notify 等非 provider hook 的唯一执行入口。选择机制是第二个位置参数若匹配 hook 名则显式指定否则在插件只暴露一个可运行 hook 时自动选用暴露多个 hook 时要求显式指定见 plugins.mjs 的cmdRun。provider hook 不在这里运行——它经由portals.yml里显式的provider: id条目随node scan.mjs执行。把 ingest/search/export/notify 放在显式 CLI 之后是刻意设计一次普通的node scan.mjs永远不会悄悄去访问邮箱、Notion 或付费 API。双重闸门启用 密钥缺一不可一个插件要真正运行必须同时满足两个条件缺一则被拦下已启用node plugins.mjs enable id --confirm会先打印一张能力卡片capability card展示该插件的描述、hook、将读取的密钥与网络目标需要你显式确认以记录 consent或者手动编辑config/plugins.yml把对应插件的enabled置为true。密钥就位插件声明的每个密钥都要出现在你自己的.envgitignored绝不写入config/plugins.yml。node doctor.mjs会列出每个插件并指出缺什么node plugins.mjs list也会对缺失密钥的已配置插件给出⚠️ missing env: …提示。在源码层pluginStatus()plugins/_engine.mjs正是用configured missingEnv.length 0来判定enabled的。配置文件的骨架以 config/plugins.example.yml 提供复制为config/plugins.yml后使用。该文件是用户层路径gitignored、永不被自动更新覆盖。它允许在启用条目下附带非机密设置如 Gmail 插件的label、days_back这些会以ctx.settings形式送达插件# cp config/plugins.example.yml config/plugins.yml plugins: apify: enabled: false # provider经 portals.yml 的 provider: apify 触发 notion: enabled: false # export search gmail: enabled: false # label: Job Leads # 要扫描的 Gmail label非机密设置示例 # days_back: 7值得留意的是setEnabledplugins.mjs以合并、绝不覆盖的方式写回config/plugins.yml若该文件存在但 YAML 无法解析如手改出现 typo它会拒绝写入并报错而不是用空对象覆盖——因为config/plugins.yml没有可恢复的备份副本用户层文件。信任徽章与篡改检测插件到底有多可信career-ops 不假装自己能对插件做真沙箱纯 ESM、无构建步骤引擎无法隔离模块的 imports。它的信任体系由来源分类 完整性锁定lock 代码评审组成。node plugins.mjs list会为每个插件显示一枚信任徽章四种徽章的精确语义如下表与 plugins/_registry.mjs 的classifySource()/sourceBadge()对应徽章含义 bundled随plugins/发行在树内in-tree经代码评审随核心自动更新。✓ approved社区插件已在注册表中以精确固定的 commitpinned SHA通过评审。❓ community-unverified你从某个 career-ops 尚未评审的仓库安装的——你在信任该作者。⚠️ off-registry已安装的 commit 与注册表批准的 commit 不一致漂移。来源判定永远来自文件系统目录在plugins/下即 bundled否则为 local而不是用户可写的plugins.lock防止伪造source: bundled绕过后续关卡plugins/_engine.mjs 的pluginSource()。完整性锁定与篡改检测每次启用或信任一个插件cmdEnable都会用 plugins/_lock.mjs 的hashPluginTree()递归计算该目录每一个文件排除node_modules、.git且拒绝符号链接的 sha256连同 consent 能力表面写入用户层的plugins.lock。在插件被 import 之前lockGate()会做一次完整性比对plugins/_engine.mjs结果分四类处理unpinned历史手改启用未固定静默补 pin 后放行match文件与锁定一致放行legit-update文件变了但version有合法提升视为诚实更新静默重 pindrift-nobump文件变了却没有版本号提升这是疑似篡改即 OWASP MCP-04 一类的 postmark 攻击模式插件被阻止加载提示你审查后执行node plugins.mjs trust id重新固定。对 bundled 插件则因分支保护检出的源码本身就等同于评审而直接静默重 pinsurface-widenedhook / requiredEnv / allowedHosts / allowsLocalhost 相对 consent 扩大了视为能力面扩张要求重新走node plugins.mjs enable id重新授权。诚实边界在 plugins/README.md 中写得很清楚lock 是篡改证据而非隔离边界——能写plugins.local/的本地攻击者也同样能改plugins.lock重新 pin。它的真实价值在于拦截bundled 更新中的植入、跨插件篡改、以及你在信任之后文件被改却没升版本的社区插件。编写一个插件manifest、hooks 与 ctx脚手架与目录结构用一行命令生成骨架node plugins.mjs new my-plugin # 生成 plugins.local/my-plugin/cmdNew调用 plugin-install.mjs 的scaffoldNew模板来源见 plugins/_template/。一个插件就是这样一个目录——bundled 插件放plugins/id/你自己的私有/实验插件放plugins.local/id/gitignored永不被自动更新触碰plugins/id/ manifest.json # 纯 JSON只解析不执行——任何代码 import 前先校验 index.mjs # default export 一个按 hook 类型键控的对象 _helpers.mjs # 辅助模块_ 前缀 绝不会被当作插件发现 skill.md # 可选随插件提供的 how-toOpen-Agent-Skill 风格plugins.local/存在的意义私密或实验性插件放这里更新不会覆盖它同 id 冲突时bundled 插件永远优先除非下文受维护继承者机制明确授权覆盖。manifest.json任何代码执行前的契约校验manifest 在插件代码被 import之前由validateManifest()plugins/_engine.mjs逐字段校验任何违例都只是打印⚠️跳过该插件——fail-open绝不拖垮核心。一个典型 manifest{ id: wellfound, apiVersion: 1, description: One mission-framed line., hooks: [provider], requiredEnv: [WELLFOUND_TOKEN], allowedHosts: [api.wellfound.com], humanInTheLoop: true }各字段语义与校验规则同时可参见 plugins/_types.js 的 JSDoc 类型目录插件作者可用typedef引用以获得 IDE 提示字段必填规则与影响id✓匹配^[a-z0-9][a-z0-9-]*$且必须等于目录名去重键 防伪冒。apiVersion✓必须为1。引擎拒绝未知主版本以便未来对 ctx/hook 签名变更做版本化。description✓单行、使命导向的描述不含营收/定价/护城河/托管等措辞——它会出现在公共仓库。hooks✓非空且必须是provider / ingest / search / notify / export的子集。任何其它值如apply/submit都被拒绝——刻意不存在自动投递 hook。requiredEnv✓可为[]只声明环境变量名绝不含值。用户在自己的.env提供值。保留名黑名单核心密钥如GEMINI_API_KEY、OPENAI_API_KEY、PATH、HOME、AWS_*前缀等会被拒绝见 plugins/_engine.mjs 的RESERVED_ENV——防止插件夹带核心密钥让 doctor 误显示为合规。allowedHosts条件必填有requiredEnv时必须声明非空 egress 白名单。拒绝 IP 字面量、metadata.google.internal、*.internal等 SSRF 形态主机loopback 主机需额外声明allowsLocalhost: true。humanInTheLoop✓必须为trueloader 硬性拒绝false。entry可选默认index.mjs必须是.mjs且经过目录穿越防护不能逃出插件目录。optionalEnv/name/version/homepage/skill可选skill指向相对.md文件同样穿越防护且文件必须存在。五类 hooks生产者返回引擎写入index.mjs的 default export 是一个按 hook 类型键控的对象。hook 签名与职责如下表源自 plugins/README.md 与 plugins/_types.js 的PluginHooksHook签名作用provider{ id, detect?, fetch(entry, ctx) → Job[] }需要密钥/认证的职位源形状与providers/_types.js一致经portals.yml中provider: id条目随scan运行。引擎会强制detect()为 null只在显式条目上触发——自动探测期间绝不发起意外付费/带密钥网络请求。ingest(ctx) → Job[]从某个服务邮箱、看板拉取职位。search(query, ctx) → Job[]为某个查询串返回职位。export(snapshot, ctx) → {pushed}把只读的追踪器快照推到你自己拥有的外部存储如 Notion 库。不提供文件句柄。notify(payload, ctx) → void发送一条出站通知。关键设计生产者provider/ingest/search负责returnJob[]{title, url, company, location}引擎——而非插件——负责把它们写入data/pipeline.md。写入必须走 scan.mjs 的规范 writer 并经过sanitizeJob()见 plugins.mjs清洗只保留 title/url/company/location/salary 这几个规范字段丢弃插件多返回的任何额外键并做增量去重已在 pipeline 中出现的 URL 不会重复追加。这意味着插件结构上无法破坏 web 读取的数据格式。ctx受守卫的网络与最小权限注入每个 hook 都会收到引擎构建的ctx对象buildCtx()plugins/_engine.mjs关键成员ctx.fetch(url, opts)——受守卫的原语强制 HTTPS将请求固定在 manifest 声明的allowedHosts以redirect: manual手动跟随重定向每一跳都重新校验主机是否在白名单内SSRF 防护在每跳重查resolveAndValidate且跨主机跳转时剥离authorization/cookie凭据非 2xx 抛错并附响应片段默认 10s 超时、最多 5 跳。ctx.fetchText/ctx.fetchJson是它之上的便捷封装。你的 HTTP 必须走ctx.fetch系——直接调用全局fetch会绕过 egress 守卫bundledapify是唯一刻意的例外其客户端自约束到单个硬编码主机并在代码中注明。ctx.env——冻结对象作用域仅限于该插件声明的requiredEnv ∪ optionalEnv。这只是便捷访问器不是隔离边界process.env对任何模块仍全局可达。ctx.settings——来自config/plugins.yml中该插件条目的非机密设置块冻结。ctx.log——会把声明过的密钥值脱敏为«redacted»的日志函数防意外泄露的卫生措施不是防外传控制。ctx.dryRun——以--dry-run调用时为 true有副作用的 hook 必须遵守。runHook()plugins/_engine.mjs对每个 hook 做 per-call try/catch 加超时默认 15sexport 因逐行 upsert 网络操作会按行数在 15s–120s 间缩放见 plugins.mjs 的cmdRun——一个抛错或超时的插件只会被打上⚠️跳过绝不拖垮整批。需要说明的是超时是协作式的Promise.race能解除等待却无法抢占一个同步死循环或process.exit的插件纯 ESM 没有 worker 无法抢占这正是 bundled 插件要经受代码评审的原因。发布插件并进入注册表从本地仓库到plugins.mjs add name文档给出了从本地开发到全球用户可安装的四步流程本地开发并发布为独立公共仓库仓库名必须是career-ops-plugin-name模板仓库会给你正确结构 release workflow。最低文件要求manifest.json、index.mjs、README.md、LICENSE要能被available列出来还需skill.mdtest/smoke.mjs。提交一个 Plugin registration issue成为插件的 home/changelog。提交 registry PR使用?templateplugin-registry.md模板模板仓库的 release workflow 可以在打 release tag 时替你发起添加你的plugins-registry/id.json文件固定到精确 commit。CIplugin-registry-validate会在维护者人工评审前检查命名、manifest、最少文件、license、egress 白名单与静态审计在无密钥只读沙箱里执行绝不运行插件代码。合并后用户即可node plugins.mjs add name插件随常规更新送达。更新 再一次 registry PR提升条目的shaversionrelease workflow 会从你自己的 fork 发起。用户永远只会拿到我们批准过的那个 commit。registry 采用每插件一个文件plugins-registry/id.json而不是单一数组文件这样两个并发 registry PR 不会冲突。每个条目都要通过validateRegistryEntry()plugins/_registry.mjs的形状校验name 必须是career-ops-plugin-id、repo 必须是https://github.com/owner/repo、sha 必须是 40 位十六进制 commit、keyed 插件必须声明allowedHosts、license 必填。批准后来源归类为✓ approved。审查清单细节见维护者指南 docs/PLUGIN_REVIEW.md命名与身份、diff 是否只做声称的事警惕 time-bomb、env 门控分支、混淆、egress 是否真实公共主机、能力面是否 ⊆ 五类 hook、数据方向是否只读公共数据或用户自己的账号、措辞是否适合公开永存、skill 是否领域受限不越权、license 是否 MIT 兼容。ToS 灰色地带/需要登录的集成如带会话的 LinkedIn既不会 bundled 也不会 registry 列名——它们仍可成为用户显式安装进plugins.local/的career-ops-plugin-name仓库但要承担完整的你在信任该作者提示。Bundled 插件是参考种子为什么核心保持精简广泛有用、低/零密钥的插件会被bundled进plugins/当前例子是apifyprovider、gmailingest、notionexport search——对应的说明与启用块都能在 config/plugins.example.yml 中看到。但 bundled 插件的定位是参考种子reference seeds在树内评审、始终存在、可复制的可运行范例刻意保持最小与稳定不是持续功能开发的归宿。因此项目不接受针对 bundled 插件的功能 PR——只收安全与发布兼容性修复。如果你想扩展一个 bundled 插件更多选项、更丰富的映射、新行为正确的路径是发布一个受维护的继承者发布同名 id 的career-ops-plugin-id从 bundled 插件代码起步——它是 MIT且会署名出处在你的 registry 条目上设置supersedesBundled: true获批并固定后任何人运行node plugins.mjs add career-ops-plugin-id都会装上你的版本而引擎会让你的受维护继承者优先于同 id 的 bundled 参考实现。node plugins.mjs available会显示 gmail — maintained version: career-ops-plugin-gmail。优先级授予是有严格条件的只有注册表批准、且用户安装在精确固定 commit上的继承者才获得优先权判定逻辑在 plugins/_engine.mjs 的resolveSuccessorIds()registry 声明supersedesBundled: true∧plugins.local/id已安装 ∧ 安装 sha registry 固定 sha三者缺一即不覆盖。因此一个未经提示、未经评审的社区插件永远无法遮蔽 bundled 插件no-downgrade 不变量。信任边界说直白些。这套机制保护的是供应链registry 是经过评审的系统文件安装固定精确 commit所以没有上游作者能在维护者未合并其条目的情况下把自己的代码推送到 bundled 插件之上。它不试图阻止你在自己的机器上运行自己修改的代码——career-ops 本地优先、源码属于你如果你编辑了plugins.local/或你的plugins.lock那是你选择运行自己的版本和从前完全一样。移除继承者即恢复 bundled 参考实现。社区插件一览注册表中的每个社区插件都经过评审并固定到精确 commit。截至 plugins-registry/ 中记录可用插件包括插件做什么Hooks所需密钥career-ops-plugin-tavily用于职位扫描、liveness 检查与公司调研的 Tavily 搜索/提取。searchTAVILY_API_KEYcareer-ops-plugin-google-calendarGoogle Calendar ingest——检测即将到来的面试事件并汇入 career-ops 流水线。ingestGOOGLE_CALENDAR_CLIENT_ID、GOOGLE_CALENDAR_CLIENT_SECRET、GOOGLE_CALENDAR_REFRESH_TOKENcareer-ops-plugin-linkedin-alertsLinkedIn 职位提醒 ingest——从 Gmail 收件箱解析 LinkedIn 提醒邮件把追踪链接归一化为规范职位 URL汇入流水线。ingestGMAIL_CLIENT_ID、GMAIL_CLIENT_SECRET、GMAIL_REFRESH_TOKENcareer-ops-plugin-outlook-interviewsOutlook 面试 ingest——经 Microsoft Graph 检测面试邀请邮件提取公司/职位/会议链接汇入流水线。ingestMSGRAPH_CLIENT_ID、MSGRAPH_REFRESH_TOKEN可选MSGRAPH_CLIENT_SECRETcareer-ops-plugin-obsidianObsidian export——把追踪器镜像进你的 vault成为可由 Dataview/Bases 查询的 frontmatter 笔记frontmatter 属于机器笔记正文属于你。export无注上述插件的仓库主页在 plugins-registry/ 对应 JSON 条目中每个条目固定了精确 commit。想要把自己的插件加进注册表走上文发布插件并进入注册表的完整流程即可。不属于插件的东西刻意划出的边界插件层有两个明确的不做边界与整个项目的价值观一致详见 docs/PLUGINS.md 与 plugins/README.md集中式基础设施项目自己运营的托管聚合、共享服务、代理→ 属于一个独立、opt-in 的服务而非 open-core 插件层。插件只读公共数据或用户自己的账号。自动投递 / 盲投简历→ 核心内处处排除。career-ops 是一个决策支持工具而非机器人它为你起草申请由你审查后手动提交。hook 类型学中根本不存在apply/submit且humanInTheLoop: true在所有场景核心与插件都是强制的——这是写进 manifest 校验器、registry 校验器与维护者审查清单三层防线中的硬性约束。理解了这条边界就理解了 career-ops 插件哲学的全部需要密钥的外部集成可以进来但必须 opt-in、必须可见、必须经过供应链级别的信任审查而任何把决定权从你手里拿走的能力永远不会以任何形式存在于任何一层。【免费下载链接】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),仅供参考