NickLin
← 项目

dsh-find-jobs — 个人求职 Agent 插件

挂在 DeepSeek Harness 上的求职 Agent——不是求职网站,也不是聊天框,而是「了解我,然后替我持续找出真正值得投的岗位」。

  • TypeScript
  • Agent / Skill 编排
  • 浏览器自动化
  • 反爬风控
  • 插件架构
  • Node 22

Agent 插件 · TypeScript · 2026

源码 · GitHub

dsh-find-jobs 个人求职 Agent

概览

项内容
类型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 一个黑盒自动投递工具。」这是价值观,也是风险控制的底线。

难点与解法

1. 招聘站的风控,决定了架构而不是事后补丁

最初的做法是搜索后点开岗位卡片拿详情——直接触发风控。改成列表与详情在同一次浏览会话内抓齐,不在调用之间二次导航,Boss 详情改走 job_detail 直连 URL。这不是「加个重试」,而是把抓取时机前移到一次会话里,从源头避开反直链。

2. 两个站点,两种无限滚动——机制完全不同

Boss 是 append:DOM 只增不减,滚到底、数字达到 limit 即停。

小红书是 virtualized:DOM 只保留视口,必须每轮自然滚 300px、evaluate 当前 .note-item 的 outerHTML、再按 data-note-id 增量去重,最多 20 轮。

解法是把滚动抽象成 listScroll 声明,机制由 bskRunner 通用实现,适配器只声明 mode 和自己的计数/快照规则,不写死任何站点选择器。实测:Boss 滚动抓 25+ 条通过,小红书 43 条通过。

3. 多用户 / 多会话下,进程级串行链会互相掐死

一开始用进程级的 bskCommandChain 排队浏览器命令——多用户并发时会互相阻塞、抢同一个浏览器窗口。改成按运行域隔离:每个 agent / session 一个运行域,各自持有独立的命令队列、常驻自动会话和列表 tab;命中失效会话(daemon 重启 / 会话过期 / 断连)时自动停旧重建,并整体重试一次。

4. Token 成本要按「文件是唯一真源」来设计

完整 JD 塞进工具返回值,几屏就烧完上下文。做法是反过来的:

5. 同一个口径,出现在三个地方就一定会分叉

匹配档位(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> 85apply
good_match70 – 85apply
stretch55 – 70apply / skip(看缺口性质)
low_match< 55skip

致命硬冲突(强制 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 链路,类型不过不产物

Roadmap

Phase内容状态
1Mock 岗位库跑通「给我一批岗位 → 帮我挑」已完成
2(当前)真实搜索:bsk 实时抓 Boss直聘 / 小红书 + 自动存档已完成,两源实站校准
3持续搜索 + 个人 Memory(已看岗位去重 / 定时跑)待做
4自建 Web UI 包装待做

安全与隐私

其他作品