首页
/ Reactive Resume 的 DeepSeek Harness 插件设计:把 33 个简历管理 MCP 工具接入 Agent 会话

Reactive Resume 的 DeepSeek Harness 插件设计:把 33 个简历管理 MCP 工具接入 Agent 会话

2026-09-05 11:58:27作者:董宙帆

本文基于 Reactive Resume 仓库中的设计文档 DeepSeek Harness Plugin for Reactive Resume — Design 展开,完整还原该插件从目标定义、架构决策到最终落地的全过程:如何把 Reactive Resume 远程 MCP 服务器(33 个工具、3 个 prompt、2 个 resource)通过一条 cordis.patch.yml 配置行桥接进 DeepSeek Harness 会话,并额外贡献一段针对"模型经常写错的 JSON Patch 语义"的系统提示词段落;文末还会结合 spike 报告实际源码 讲清 ctx.tools.restrict() 为何被砍、漂移检测方案如何从"定时抓取 server card"演进为 monorepo 内的契约测试。读完后你可以掌握:Harness 插件的 apply(ctx, config) 生命周期、MCP Streamable HTTP 桥接配置、Cordis 作用域(scope)语义对工具可见性的影响,以及一套"设计 → spike 验证 → 按证据裁剪"的插件开发方法论。

1. 目标:一条配置行 + 一个 API Key

设计文档的目标只有一句话:发布一个 DeepSeek Harness 插件,把 Harness 连接到 Reactive Resume 账户,让用户在自己的 agent 会话中读取、创建和编辑简历与求职申请,前提只是一行配置和一个 API Key。

这不是要再造一套简历 API,而是把已有的远程控制面"搬"进 Agent 上下文。文档给出的差距分析(The gap this plugin fills)非常具体:

  • dsh-mcp-client 本身没有工具过滤能力,也无法贡献提示词文本
  • 如果用户直接把一条原始 MCP 配置写进 cordis.yml,33 个工具的完整 schema 会全部挤占模型的 context budget,而且没有任何关于 Reactive Resume JSON Patch 语义的引导——而这类语义恰恰是模型最容易犯错的地方。

这两点就是该插件存在的全部理由:薄桥接 + 提示词段落(prompt section)。

2. 前置能力盘点:Reactive Resume 已提供什么

设计文档强调"已对发布包做了验证,而不仅仅依赖文档"。Reactive Resume 仓库中已具备完整的远程 MCP 服务器,插件无需改动服务器端任何东西:

  1. 33 个工具的注册packages/mcp 注册了 list_resumesread_resumeapply_resume_patchtailor_resume_for_application 等 33 个工具,外加 3 个 prompts 和 2 个 resources。工具名的权威清单集中在 MCP_TOOL_NAME 常量表:resume 组 13 个(list_resumesread_resumedownload_resume_pdfcreate_resumeimport_resumeduplicate_resumeapply_resume_patchupdate_resumedelete_resumelock_resumeunlock_resumeget_resume_statisticslist_resume_tags),applications 组 20 个(list_applicationscreate_applicationbulk_update_applicationsautofill_application_from_jobscore_application_matchdraft_application_message 等)。
  2. 端点挂载apps/server/src/http/app.ts 挂载了 /mcp/mcp/*(Streamable HTTP 传输),以及 /.well-known/mcp/server-card.json(SEP-1649 server card)。
  3. 双路鉴权apps/server/src/mcp/auth.ts 同时接受 OAuth 的 Authorization: Bearer token 或 x-api-key header。插件选择 API key 路径,因为它不需要任何交互式流程。
  4. API key 管理已内建。Web 应用在 /dashboard/settings/api-keys 提供 key 的创建与管理,用户开通(provisioning)是已解决问题。

DeepSeek Harness 侧提供的能力(同样经过对发布包的实际核验):

  • 插件是一个 TypeScript 模块,导出 name、可选的 injectapply(ctx, config);配置 schema 使用 @deepseek-ai/schemastery
  • @deepseek-ai/dsh-mcp-client 每个插件实例桥接一个外部 MCP 服务器,其 StreamableHttpConfig 的完整形状为:
interface StreamableHttpConfig {
  transport: 'streamable-http'
  serverName: string      // [A-Za-z0-9_-]{1,32},且在存活的实例间唯一
  url: string
  headers: Record<string, string>
  toolCallTimeoutMs: number
  failOnStartupError: boolean
}

被桥接的工具对模型呈现为 mcp__<serverName>__<rawName>

  • ctx.toolsToolRegistry,暴露 restrict(filter: ToolRestriction): () => void,其中 ToolRestriction{ allow?: readonly string[]; deny?: readonly string[] }
  • ctx.systemPrompt(来自 @deepseek-ai/dsh-system-prompt)暴露 section(section: PromptSection): () => voidPromptSection{ name, order, text, complete? }。order 约定:-100 是 harness 身份,0 是部署人格,100–199 是工具引导;
  • 插件通过 npm 分发,经 GitHub 上的 dsh-plugin topic 被发现。

3. 四个关键设计决策

设计文档用一个决策表锁定了方案边界:

决策 选择 理由
范围 薄 MCP 桥接 + 提示词段落 工具层已存在且由 Reactive Resume 维护。对 oRPC 重新实现 33 个工具契约,会让每个版本都产生漂移。
仓库 (原设计)独立仓库,放在 monorepo 之外 插件不 import Reactive Resume 的任何东西——它只说 HTTP。而 monorepo 当时没有 npm 发布管线:根包和每个包都是 private: true,没有构建产物、没有 changesets、没有 NPM_TOKEN、没有发布 workflow。为发一个零依赖的包去搭一条管线,是无收益的成本。
鉴权 x-api-key 在 Reactive Resume 设置里点两下即可。OAuth 需要一个 token 存储和交互式流程,没有收益。
漂移防护 CI 中对 server card 做契约测试 替代"同在 monorepo 内"本应提供的 lockstep 保障,而不需要发布管线。

后续演变(重要):决策表中"独立仓库"一项后来被推翻——插件最终落进了 monorepo,就放在它桥接的 MCP 服务器旁边(packages/dsh-plugin),与 packages/mcp 相邻。实际 package.json 也印证了这一点:publishConfig.accesspublicprepublishOnly 触发构建,且 devDependencies 中以 workspace:* 直接依赖 @reactive-resume/mcp。漂移检测因此不再需要网络抓取,改为直接读 workspace 内的工具名表(见第 7 节)。

4. 包形态与配置面

4.1 包形态(package shape)

设计文档给出的模块导出契约,镜像 dsh-mcp-client 自身的导出形式:

export const name = 'reactive-resume'
export const inject = ['tools', 'systemPrompt']
export const Config: z<Config>
export async function apply(ctx: Context, config: Config): Promise<void>

@deepseek-ai/cordis@deepseek-ai/dsh-tools@deepseek-ai/dsh-system-prompt@deepseek-ai/dsh-mcp-client 全部声明为 peerDependencies,让插件绑定宿主已安装的运行时版本,而不是装入第二份副本。实际 package.json 中的 peer 依赖为 @deepseek-ai/cordis@^4.0.1@deepseek-ai/dsh-mcp-client@^0.1.0-rc.6@deepseek-ai/dsh-system-prompt@^0.1.0-rc.6@deepseek-ai/schemastery@^3.18.1,并有 "dsh": { "bundle": { "patch": "./cordis.patch.yml" } } 字段——这正是 README 所说"声明了 dsh.bundle,profile 会自动把它作为一层挂载"的机制来源。

对照 src/index.ts,最终实现与设计有一处刻意的偏差:inject 只有 ["systemPrompt"]没有 tools。源码注释解释了原因:ctx.tools.restrict() 需要 agent 作用域的上下文,而插件自己的 apply(ctx, config) 永远拿不到;dsh-mcp-client 会自己声明对 tools 服务的依赖,桥接层拿得到它需要的东西,插件不必等待。这是 spike 结论(第 6 节)直接塑造的 API 面。

4.2 Config

设计文档中的配置接口(含后来被砍掉的 tools 键):

interface Config {
  /** API key from <url>/dashboard/settings/api-keys. */
  apiKey: string
  /** Reactive Resume instance origin. Default 'https://rxresu.me'. */
  url?: string
  /** Tool namespace: tools appear as mcp__<serverName>__<rawName>. Default 'resume'. */
  serverName?: string
  /** Which tool group to expose. Default 'all'. */
  tools?: 'resume' | 'applications' | 'all'
  /** Per-tool-call timeout. Default inherited from dsh-mcp-client. */
  toolCallTimeoutMs?: number
}

url 接受任意 origin,自托管实例无需改动即可工作。

实际发布的 config.ts 与上表相比,除了移除 tools 键,还有三处值得注意的实现细节,均超出设计文档的描述:

  1. serverName 的模式校验前置const SERVER_NAME_PATTERN = /^[A-Za-z0-9_-]{1,32}$/ 通过 z.string().pattern(SERVER_NAME_PATTERN).default("resume")解析期拒绝非法命名空间——源码注释明确"Config 在 apply 运行之前就已拒绝非法 serverName"。这与 StreamableHttpConfigserverName 的约束一致。
  2. toolCallTimeoutMs 有了明确默认值 60_000z.natural() 保证为正整数),而非设计稿中的"继承自 dsh-mcp-client"。
  3. apiKey 的默认值是空字符串而非 required。这是为配合 dsh.bundle 的安装路径设计的:bundle patch 在包安装时就把插件行挂载进 profile,彼时用户还没有生成 key。如果 apiKey 是必填项,缺失的 key 会在启动时造成校验错误、拖垮整个 profile;改为 .default("") 后,空 key 在 apply() 中只触发一条 warn 并什么都不挂载——"安装它永远不会让 profile 无法启动"。

对应 README 中的选项表:

默认值 说明
apiKey '' 来自 <url>/dashboard/settings/api-keys。为空时不挂载任何东西。
url https://rxresu.me 实例 origin,自托管时设置。
serverName resume 工具命名空间,工具以 mcp__<serverName>__<rawName> 呈现。
toolCallTimeoutMs 60000 单次工具调用超时。

5. apply():三步到两步的落地

5.1 设计稿中的三步

  1. 挂载桥接ctx.plugin(mcpClient, { transport: 'streamable-http', serverName, url: '${url}/mcp', headers: { 'x-api-key': apiKey }, toolCallTimeoutMs, failOnStartupError: true })。其中 failOnStartupError: true 把"API key 写错"变成一次响亮的激活期失败,而不是在调用时才静默报错的工具集。
  2. 策划工具面:当 tools !== 'all' 时,调用 ctx.tools.restrict({ deny: [...] }),用从生成的工具名清单计算出的命名空间名排除掉被裁掉的组。(此步后证实不可行,见第 6 节。)
  3. 贡献提示词引导ctx.systemPrompt.section({ name: 'reactive-resume', order: 150, text: PATCH_GUIDE })

三步都返回 disposer;Cordis 的 effect scoping 会在卸载时统一回收,无需手动清理。

5.2 最终实现

apply() 的最终代码 保留了第 1、3 步,并新增了防御与命名空间处理:

export async function apply(ctx: Context, config: Config): Promise<void> {
  // bundle patch 在还没人生成 key 之前就挂载了这行。
  // 宁可不挂载任何东西,也别让整个 profile 启动失败。
  if (config.apiKey === "") {
    ctx.logger.warn("no apiKey configured — set one at %s/dashboard/settings/api-keys to enable the tools", config.url);
    return;
  }

  const origin = config.url.replace(/\/+$/, "");

  await ctx.plugin(mcpClient, {
    transport: "streamable-http",
    serverName: config.serverName,
    url: `${origin}/mcp`,
    headers: { "x-api-key": config.apiKey },
    toolCallTimeoutMs: config.toolCallTimeoutMs,
    failOnStartupError: true,
  });

  ctx.systemPrompt.section({
    // 按 serverName 加命名空间,避免第二个实例(比如自托管
    // 账户与托管账户并存)在 section name 上撞车。
    name: `reactive-resume:${config.serverName}`,
    order: 150,
    text: buildPatchGuide(config.serverName),
  });
}

与设计稿的差异点:空 key 早退(对应 4.2 的默认值设计)、url 尾斜杠归一化(replace(/\/+$/, ""),保证 ${origin}/mcp 不产生双斜杠)、prompt section 的 name 从固定的 reactive-resume 变成 `reactive-resume:${serverName}`(多实例防碰撞),以及 order: 150 落在设计文档约定的工具引导区间 100–199 内。

6. Open Risk 成为主事件:restrict() 为什么砍掉了 tools

设计文档在"Open risk"一节引用了 ToolRegistry.restrict 的契约原文:"Per-scope filter over the tools a scope INHERITS — the global layer and every ancestor layer on its chain. Restrictions intersect, and do not affect the scope's own registrations."(作用域继承的工具过滤器——全局层与链上所有祖先层。限制互相求交,不影响作用域自身的注册。)

问题在于拓扑:ctx.plugin(mcpClient, …) 把桥接挂在插件上下文的作用域里,被桥接的工具注册在一个后代层,而非调用 restrict() 的作用域的祖先层。父作用域的限制能否触达它们,未经验证——而 tools 是公开配置键,事后移除属于破坏性变更,所以必须在承诺配置面之前用 spike 定案。

spike 报告给出了明确裁决:

Verdict up front: restrict reaches child-scope tools: NO

spike 的关键发现(全部基于 0.0.1-rc.1 版本的编译产物,逐行阅读并实际运行验证):

  1. ctx.tools 是 Cordis 服务单例declare module '@deepseek-ai/cordis' { interface Context { tools: ToolRegistry } }),沿整个上下文树共享;真正决定 register()/restrict() 可见性的不是 Cordis 插件上下文树,而是 @deepseek-ai/dsh-scope 提供的另一套显式作用域层——上下文必须被 createScope(ctx, key) 打过标才携带 scope tag,普通的 ctx.plugin(child) 上下文不受其约束。
  2. Probe 1(字面拓扑复现):在无 scope 的 root 上挂载 stub 桥接后从 rootrestrict({ deny: [...] }),第一行 scopeOf(this.ctx) 判定为 undefined 即抛错,实际输出:
RESTRICT_THREW tools.restrict() requires a scoped context (agent.ctx): a context-global restriction would mask every agent — deny the tool for the intended agent instead

这不是静默 no-op,是硬抛异常。 3. Probe 2(最优修复:给插件一个真 scope):spike 定位到真正构造 agent.ctx 的代码在 dsh-agent-loopcreateScope(loopCtx, this) 一行),并据此搭建"有 scope 的 restrictor + 同 scope 内桥接"的组合,仍然抛错:

RESTRICT_THREW tools.restrict() names unknown inherited tool "mcp__resume__list_applications"; a restriction filters what this scope inherits, never what it registers itself. Restrictable tools: (none)

桥接的工具落在该 scope 自身层(它作为 scope.ctx 的 Cordis 子节点挂载,因此继承了该 scope tag),而 restrict() 只过滤 scope 继承的表面——两条路都到不了该插件所需的拓扑。 4. spike 还顺带确认了 0.0.1-rc.1 中真实的 ToolDefinition 形状(output 是必填的 { schema, render }execute() 返回规范 JSON 值而非 ContentBlock[]),以及 pnpm 安装该依赖树时的 dsh-type-meta 404 陷阱与 auto-install-peers=false 的解法——这些是"对发布包做了验证而非只看文档"这句话的具体产物。

于是文档预设的 fallback 生效:0.1.0 不带 tools 键(等价于 'all'),策展留待 0.2.0——"prompt section 本身就足以证明这个发布的价值"。设计文档头部的 Post-implementation note 也明确:tools 配置键和 ctx.tools.restrict() 调用从 v0.1.0 中被切除,0.1.0 不做任何过滤地发布全部工具;文档其余部分按原始写作保留以存档,不代表实际发布内容。

7. PATCH_GUIDE:插件的真正实质

设计文档称 PATCH_GUIDE 是"the plugin's substance",约 40 行,覆盖 Reactive Resume 已经以 errorHint 形式编码进 packages/mcp/src/tools.ts 的失败模式——这些错误提示存在本身,就是因为模型会踩这些坑。设计稿列出的六条:

  • apply_resume_patch 之前先调 read_resume,绝不盲打 patch;
  • apply_resume_patch 接收的是对 resume 数据文档的 RFC 6902 操作;
  • 构造路径前先取 resume://_meta/schema resource;
  • section 条目是以 UUID 为键的对象数组id 不是索引;
  • 被锁定的简历拒绝写入——先调 unlock_resume
  • list_resumes 是 404 之后恢复有效 id 的手段。

写成静态文本而非 provider 函数,因为它不随组装变化。最终实现 prompt.tsbuildPatchGuide(serverName) 比设计稿更进一步:它把每个工具名都动态包装成 `mcp__${serverName}__${raw}`,使提示词与配置的命名空间严格一致,内容上扩充为四节——

  • Reading before writinglist_resumes 发现 id(id 是 UUID,绝不是标题或 slug);编辑前必须先 read_resume;"not found" 时重跑 list_resumes 而不是猜 id。
  • Editing:RFC 6902 语义;路径要基于本会话已读到的简历构造,并对齐 apply_resume_patch 工具描述里自带的具体示例(/basics/name/sections/experience/items/-/sections/experience/items/0/company/metadata/template);条目按"从刚读到的文档里定位索引"寻址;多个操作合并进一个 patch 优于多个单操作 patch(操作按序应用、整体原子失败);并划清 update_resume(只改元数据:name、slug、tags、public 可见性)与 import_resume(从完整数据文档创建简历,不覆盖已有简历)的边界。
  • Locking:锁定的简历拒绝一切写入;因锁定失败时 unlock_resume → 修改 → lock_resume,"把锁恢复到你发现它时的状态"。
  • Scope:除非用户在本对话中明确要求删除,否则不删除任何简历或申请;用户用散文描述变更时,先复述即将执行的具体编辑再动手。

7.1 漂移检测:从"定时抓 server card"到 monorepo 契约测试

设计稿的原始方案(现已标注"Superseded by the move into the monorepo"):

  • 构建步骤抓取 <url>/.well-known/mcp/server-card.json,生成 src/tool-names.generated.ts,内含拆分为 resume/applications 两组的原始工具名;
  • 生成文件被提交进仓库,npm install 永远不需要网络;
  • 一个定时 CI jobhttps://rxresu.me 重新抓取,当已提交清单与线上 card 不再一致时失败——这个失败就是"该切一个新插件版本"的信号。

落进 monorepo 后,方案简化为一行测试:tool-names.test.ts 直接从 workspace 依赖 @reactive-resume/mcp/tool-names 读取工具名表,用正则 mcp__resume__([a-z0-9_]+) 提取 PATCH_GUIDE 中引用的每个工具名,断言它们都存在于 MCP_TOOL_NAME 的 33 个值之中——工具一改名,同一个 PR 里这个包就先挂。另有一条防"正则改废"的自守护测试(断言提取到的工具名非空)。这正是决策表里"漂移防护"一行在 monorepo 内的最终形态,比定时网络 job 更早、更确定。

8. 安装体验与测试

8.1 安装 UX

设计稿给出的 README 复制块:

- insert:
    - id: reactive-resume
      name: dsh-plugin-reactive-resume
      config:
        apiKey: !!js process.env.RXRESUME_API_KEY

实际 README 的路径更进一步:dsh plugin --profile <name> add dsh-plugin-reactive-resume 一条命令安装,dsh.bundle 声明使 profile 自动挂载该行;key 通过 https://rxresu.me/dashboard/settings/api-keys 生成并导出为 RXRESUME_API_KEY,需要显式覆盖时按 id patch cordis.patch.yml。自托管示例:

- id: reactive-resume
  config:
    apiKey: !!js process.env.RXRESUME_API_KEY
    url: http://localhost:3000

8.2 测试策略

  • Spike(阻塞项):本地起 Reactive Resume(dotenvx run -f .env.local -- pnpm dev,端口 3000),把一份临时 Harness 配置指向 http://localhost:3000/mcp 并带 x-api-key,确认 (a) Streamable HTTP 桥接通且列出 33 个工具,(b) 从插件作用域调 ctx.tools.restrict 能否隐藏被桥接工具。(结论见第 6 节。)
  • 单元:Config schema 的默认值与校验、从生成工具名计算 deny 清单、PATCH_GUIDE section 以 order 150 与预期名称注册。
  • 契约:server card 抓取与 src/tool-names.generated.ts 一致(monorepo 化后等价于 tool-names.test.ts)。
  • 发布前手动冒烟:在真实 Harness 会话中对着 rxresu.me——列出简历、读一个、打一个 patch、在 Web UI 里验证变更生效。

8.3 构建顺序(设计稿原样保留)

  1. Spike:restrict() 语义 + 对 localhost 的 Streamable HTTP 桥;
  2. 仓库脚手架、Config schema、桥接挂载、README——端到端可用的安装;
  3. PATCH_GUIDE 提示词段落;
  4. 工具 profile、生成名清单、漂移 CI(monorepo 化后此步收敛为契约测试);
  5. 发布 0.1.0、仓库打 dsh-plugin topic、提交进 awesome-deepseek-harness。

实际发布状态dsh-plugin-reactive-resume@0.1.0,MIT 许可,Node ^22.19.0 || >=24.0.0,构建器 tsdown。

9. 范围外与适用前提

设计文档明确排除在 0.1.0 之外:OAuth 支持、ctx.commands 快捷方式、随包附带的 Harness skill、本地简历缓存、Harness 内的 PDF 渲染——每一项都是纯增量,不阻塞一个有用的 0.1.0。

适用前提需要说清:本插件是 DeepSeek Harness 生态的组件,依赖宿主的 Cordis 运行时(@deepseek-ai/cordis@^4.0.1 等 peer 依赖);被桥接端要求 Reactive Resume 实例开放 /mcp(Streamable HTTP)端点并支持 x-api-key 鉴权,托管实例默认 origin 为 https://rxresu.me,自托管实例通过 url 指向即可。0.1.0 的一个已知取舍是不做工具面策展——33 个工具全量进入上下文,README 对此有直接说明:"Narrowing that set is not currently possible from a plugin"。

10. 小结

这份设计文档的价值不在"写了一个插件",而在它完整演示了一条可复制的证据链:先把双方(Reactive Resume 的 33 工具 MCP 服务器与 Harness 的插件/桥接/提示词 API)的能力逐条验证到发布包级别;用决策表锁定"薄桥接 + prompt section"的最小范围;把唯一的架构疑点(restrict() 的作用域可达性)显式标为阻塞项,用两个 probe 拿到两条不同的抛错路径后,果断把 tools 键从公开配置面中切除;漂移防护则随仓库归属的变化从"网络定时任务"自然演进为"同 PR 契约测试"。最终交付的 packages/dsh-plugin 只做了两件 dsh-mcp-client 做不了的事:把错误 API key 变成启动期响亮失败(failOnStartupError: true),以及把模型最容易写错的 JSON Patch 语义写成与命名空间严格一致的系统提示词段落。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
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
982
503
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384