挂在 DeepSeek Harness 上的求职 Agent——不是求职网站,也不是聊天框,而是「了解我,然后替我持续找出真正值得投的岗位」。
Agent 插件 · TypeScript · 2026
源码 · GitHub
| 项 | 内容 |
|---|---|
| 类型 | Agent 插件(DeepSeek Harness / dsh bundle) |
| 形态 | 单 Agent + 三层技能编排,无 Multi-Agent |
| 语言 / 运行时 | TypeScript 5.9 · Node ≥ 22.19 · pnpm |
| 构建 | tsdown → lib/(ESM + d.ts),tsc --noEmit 卡类型 |
| 代码规模 | 25 个 TS 源文件 / 3,020 行(不含构建产物与依赖) |
| 技能规模 | 3 个 SKILL.md,合计 381 行(job-hunter 252 / job-match 87 / job-search 42) |
| 运行依赖 | 仅 2 个运行时依赖(dsh-home-paths、schemastery) |
| 测试 | 34 个离线用例 + 6 组实站测试脚本 |
| 数据源 | Boss直聘、小红书(bsk 实时抓取,本机浏览器 + 登录态) |
| 状态 | Phase 2 完成,两个数据源已实站校准 |
用户一句话(「我想找美国 Remote 的 AI Engineer,5 年经验,180K 以上」),Agent 自主走完:
理解求职目标 → 建立 / 更新 Job Profile → 搜索岗位
→ 精读 JD → 判断匹配 → 筛掉垃圾岗位 → 只报值得投的 Top N(附原因)
边界划得很清楚——MVP 明确不做简历修改、Cover Letter、面试准备、Application Tracking,尤其不做自动投递:「不要给 LLM 一个黑盒自动投递工具。」这是价值观,也是风险控制的底线。
最初的做法是搜索后点开岗位卡片拿详情——直接触发风控。改成列表与详情在同一次浏览会话内抓齐,不在调用之间二次导航,Boss 详情改走 job_detail 直连 URL。这不是「加个重试」,而是把抓取时机前移到一次会话里,从源头避开反直链。
Boss 是 append:DOM 只增不减,滚到底、数字达到 limit 即停。
小红书是 virtualized:DOM 只保留视口,必须每轮自然滚 300px、evaluate 当前 .note-item 的 outerHTML、再按 data-note-id 增量去重,最多 20 轮。
解法是把滚动抽象成 listScroll 声明,机制由 bskRunner 通用实现,适配器只声明 mode 和自己的计数/快照规则,不写死任何站点选择器。实测:Boss 滚动抓 25+ 条通过,小红书 43 条通过。
一开始用进程级的 bskCommandChain 排队浏览器命令——多用户并发时会互相阻塞、抢同一个浏览器窗口。改成按运行域隔离:每个 agent / session 一个运行域,各自持有独立的命令队列、常驻自动会话和列表 tab;命中失效会话(daemon 重启 / 会话过期 / 断连)时自动停旧重建,并整体重试一次。
完整 JD 塞进工具返回值,几屏就烧完上下文。做法是反过来的:
search_jobs 先存档:每岗完整记录落盘 jobs/<id>.json,index.json 做索引,只返回轻量摘要(不含 JD);match 即缓存:画像没变就复用已有评估结果,skip 的岗位连详情文件都不读;index.json 条目带 match 轻量标识,判断「已评 / 未评、apply / skip」只读索引即可,不必逐个开文件。匹配档位(match_level / recommendation)和阈值唯一定义在 AGENTS.md,由技能落地,禁止在工具或其他文案里另写一套。
更狠的一次取舍:原本有一套确定性权重规则引擎(技能/年限/薪资/远程/地点加权打分),后来整个删掉——score_job 工具和 src/match/ 目录一起移除,评估全部移进 job-match 技能,交给 Agent 做业务判断。分数降级为「量化信号」,结论以业务事实为准(薪资必须先统一币种与周期)。
job-hunter 总工作流(编排器:先看画像 → 选最小工作流 → 分流 → Top N 汇报)
├── job-search 搜索:把用户原话转成 search_jobs 参数,多源并行,返回即自动存档
└── job-match 匹配评估:5 步业务判断 → 统一结果卡
技能不硬编码在代码里,而是 repo 内 skills/<kebab-name>/SKILL.md,随插件自动注册进 dsh——改 prompt 策略不用重新编译。
三个工具 / 库:
| 模块 | 作用 |
|---|---|
get_job_profile / update_job_profile | 求职画像记忆,持久化到 $DSH_HOME/profiles/<profile>/jobhunter_profile.json |
search_jobs | 一次搜一个源;成功即落盘 <id>.json + index.json,返回轻量摘要 |
jobsArchive | 落盘的确定性实现:按 id 去重合并、保留后补的 match/note、进程内串行防并发覆盖 |
一条硬性工程约束:工具内不能调 LLM(工具执行上下文里没有 ctx)。LLM 的理解动作只发生在 Agent loop 里,工具只做确定性的事——这条约束把「什么该进代码、什么该进 prompt」这条线画得很清楚。
| match_level | 区间 | recommendation |
|---|---|---|
strong_match | > 85 | apply |
good_match | 70 – 85 | apply |
stretch | 55 – 70 | apply / skip(看缺口性质) |
low_match | < 55 | skip |
致命硬冲突(强制 onsite 而用户只要 Remote、薪资远低于底线、卡应届/年龄、命中 avoid 名单)→ 直接 skip,即使分数偏高。
每岗统一输出结果卡:match_score / match_level / matched / gaps / concerns / recommendation / reason。评估结果写回 jobs/<id>.json 顶层并同步 index.json——既是结论,也是缓存。
<cwd>/jobs/
├── index.json 索引:id → file、first/last_seen、match 轻量标识
└── <id>.json 每岗完整记录(唯一真源):search 字段 + 完整 JD + match / note
| 项 | 内容 |
|---|---|
| 离线单测 | 34 个用例,纯解析 / 落盘,不联网(解析、Boss、小红书、site、searchJobs) |
| 实站测试 | 6 组脚本:Boss / 小红书 / Boss 滚动 / 小红书滚动 / 并行 / 落盘持久化 |
| 适配器可测性 | buildSearchUrl / parseListPage / parseDetailText 全为纯函数 |
| 实站校准结果 | Boss 滚动 25+ 条通过;小红书 virtual-list 43 条通过 |
| 类型门槛 | tsc --noEmit 进 build 链路,类型不过不产物 |
| Phase | 内容 | 状态 |
|---|---|---|
| 1 | Mock 岗位库跑通「给我一批岗位 → 帮我挑」 | 已完成 |
| 2(当前) | 真实搜索:bsk 实时抓 Boss直聘 / 小红书 + 自动存档 | 已完成,两源实站校准 |
| 3 | 持续搜索 + 个人 Memory(已看岗位去重 / 定时跑) | 待做 |
| 4 | 自建 Web UI 包装 | 待做 |
xsec_token,有时效),jobs/ 目录已 gitignore。SiteAdapter(3 个纯函数)→ 在 ADAPTERS 注册表登记 → 加进 Config.sources,即接入猎聘 / 拉勾等新源,滚动抓取复用通用实现、无需重写。