# dsh-job-ledger · 产品需求文档(PRD) | 项 | 内容 | |---|---| | 版本 | v0.3-draft | | 日期 | 2026-11-03 | | 状态 | 待评审(28 项决策已锁定,无待确认项) | | 形态 | DSH 插件(npm 包,cordis bundle)· **双端**:host 工具 + Web 面板 | | 包名 | `dsh-job-ledger`(npm 上空闲,已核) | | 一句话 | **一个能和 BOSS 对账的本地投递账本**:判重、冷却、证据链、漏斗;投递由插件自己点,面板里勾选即投,全程不离开你的机器。 | --- ## 1. 问题 自动投递工具已经很多,但没有一个解决真正的问题。三种典型失效: | 失效模式 | 表现 | 根因 | |---|---|---| | **无记忆** | 今天投过的明天还投;同一岗位多个 HR 发还投;跨平台重复投 | 插件/脚本只做"点",不记账 | | **数据不在你手里** | 服务关停,投递队列和历史一起消失(Sonara 2024 年的真实事故) | 云端 SaaS 持有账本 | | **双真源缝隙** | 投递器和账本各记一套,重复恰好发生在两者之间 | 记账与点击不在同一进程 | BOSS 官方也在做(内测 App「职决」含自动投递),但官方能力必然受限——平台没有动机让你无限海投。**第三方的机会不在"投得更快",在"投得清楚"。** 本地表格(飞书/Excel)能记账,但它没有判重、没有状态机、没有证据链、无法和平台对账,也无法在投递过程中监督进度。 **产品切入点不是"投递器",是"能与平台对账、能在面板上监督的账本"。** --- ## 2. 目标用户 **v0.1:技术半熟型求职者** - 能让 AI 帮忙装环境,或愿意照教程走完安装; - 有真实、持续的投递行为(当前处于找工作的状态); - **对话为主界面,看板用于批量确认与状态监督**;不要求图形化配置。 **优先服务对象:作者本人。** v0.1 的第一验收人是自己,反馈闭环最短。 **明确不服务(v0.2 再谈):** 完全不懂技术、需要双击安装的人。触达他们需要桌面打包与签名分发,那是分发问题,不是产品问题,不应污染 v0.1 的范围。 **用户渠道:小红书。** 获客形态是"教程 + 真实截图"——安装三步截图、一次真投递过程、一次对账抓出的差异。面向"能让 AI 帮装环境"的人,不投信息流广告。 --- ## 3. 目标与非目标 ### 3.1 目标 1. **同一「公司 + 岗位」永不重复投递**(含跨来源、多 HR 重复发布)。 2. **每一条投递可追溯**:何时、投的哪份简历、什么状态、证据在哪。 3. **能与平台对账**:账本说投了、平台说没投,或者反过来,必须能被发现。 4. **可视监督**:待投清单可勾选、可一键投、可看进度、可中断。 5. **产出可支撑决策的漏斗**:哪个来源回复率高,值得加投。 ### 3.2 非目标(v0.1 明确不做) | 不做 | 原因 | |---|---| | 多平台(猎聘 / 51job / 智联 / 应届生 / 鱼泡) | 去重逻辑必须先在一个平台上被验证正确 | | 简历自动改写 / 按 JD 定制 | 会破坏漏斗归因,且引入 AI 编造风险 | | 按 JD 自由生成招呼语 | 改为"模板 + 填槽",槽位值必须能在简历或 JD 中找到出处(见 §5.5) | | 上传本地 PDF 简历 | 在线简历是结构化 JSON,且就是 HR 看到的那份 | | 云端同步 / 多设备 | 与"数据在你手里"的产品定位冲突 | | 多用户 / 猎头模式 | 需要候选人维度与数据隔离,范围翻倍 | | **面板里的漏斗图表** | 先导出 Markdown;图表要等"回复率怎么算才不误导"定了再做 | | **面板里的设置页** | 配置走 YAML(`cordis.patch.yml`),少写一屏表单 | | **面板里的证据查看 / 疑似重复裁决** | 对话里解决;面板只做三屏 | | 移动端适配 | 桌面 Web 优先 | | 自动回复 HR、自动换微信 | 不在"投递"的定义内,风险另算 | | 绕过验证码 / 破解签名 | 硬性红线,遇验证码一律转人工 | --- ## 4. 核心概念 | 术语 | 定义 | |---|---| | **岗位(job)** | 一条岗位帖。身份由平台 ID + 指纹共同确定,见 §5.3 | | **指纹(fingerprint)** | `公司归一 + 职位归一 + 城市归一` 的哈希,用于跨来源判重 | | **申请(application)** | 一次投递尝试。一个岗位在冷却期内至多一条活跃申请 | | **账本(ledger)** | 只追加的事件日志 `events.jsonl`,是唯一真源 | | **租约(lease)** | 短期独占标记,防止两个并发运行投同一个岗位 | | **一轮(run)** | 一次"确认清单 → 批量投递"的完整执行,是长任务,有独立状态机 | | **对账(reconcile)** | 用平台的 `geekDeliverList` 校验账本,只补不改 | --- ## 5. 功能需求 ### 5.1 简历读取(在线简历,非上传) **需求**:插件直接读取 BOSS 在线简历,作为匹配与打招呼的唯一依据。 **实现**:插件拉起一个**专用 Chrome profile**(`--remote-debugging-port` + 独立 `--user-data-dir`),首次由用户扫码登录,之后复用登录态。打开 `https://www.zhipin.com/web/geek/resume`,**监听页面自身发出的请求**并读取响应体: | 数据 | 来源 | |---|---| | 基本信息 / 工作经历 / 教育 / 项目 | `/wapi/zpgeek/resume/baseinfo/query.json`、`geek/preview/data.json` | | 求职期望(岗位 / 城市 / 薪资) | `/wapi/zpgeek/resume/expect/query.json` | | 完整度 | `/wapi/zpgeek/resume/status.json` | **硬性约束**: - **不重放接口、不构造请求、不伪造请求头**。只监听页面自己发的请求。这是与"爬虫"的分界线,也是风控风险最低的路径。 - 读取为**用户显式触发**,不做定时轮询。 - 失败时(未登录 / 页面改版 / 超时)返回结构化错误并提示重新登录,不静默降级。 **副产品**:`expect/query.json` 里的期望职位 / 城市 / 薪资,直接作为默认筛选条件,省掉手动"选择目标岗位"。 ### 5.2 岗位发现 **需求**:把候选岗位批量灌进账本,并给出判重结论。 **来源**(同一浏览器,监听页面自身请求): - 搜索:`/wapi/zpgeek/search/joblist.json` - 推荐:`/wapi/zpgeek/pc/recommend/job/list.json` **约束**: - 单次发现上限 50 条,翻页深度上限 5 页。 - 发现与判重必须**同一次调用完成**,避免"先入库后判重"导致中间态。 - 每条岗位必须落 `encryptJobId`(作为 `externalId`),缺失则以规范化 URL 代替。 ### 5.3 判重(产品核心语义) **两种错的代价不对称**: - 漏判 = 多投一份,可恢复; - 误杀 = 该岗位**从此不再出现**,不可恢复,且用户不会知道。 **因此采用三级判定:** | 级别 | 判定条件 | 处理 | |---|---|---| | **确定重复** | ① 同平台 + 同 `externalId`;② 同「公司归一 + 职位归一 + 城市」且 45 天内;③ JD SimHash 汉明距离 ≤ 3 **且城市相同** | 自动剔除 | | **疑似重复** | 同「公司 + 职位」但 JD 不相似;或同公司同 JD 但职位名不同 | **进"待裁决"区**,由用户决定 | | **新岗位** | 以上皆不满足 | 入账 | **硬规则**: - **同公司不同职位 → 永远算新岗位**,哪怕 JD 很像。 - **冷却期 90 天**:同「公司 + 职位」投过(含 `uncertain`),90 天内不再出现。 - 判重**必须是确定性算法**,不允许模型参与。模型只能解释理由,不能改变结论。 - 每条判重结论必须带 `reason` 与 `duplicateOf`,可在清单里点开看。 **归一化规则**(`lib/normalize.js`): - 公司:去「有限公司 / 股份有限公司 / 集团 / 科技 / 网络 / 信息技术」等噪声与「北京市」类前缀。 - 职位:去括号内容、去「高级 / 资深 / 初级 / 急招」等修饰、去「岗位 / 职位」后缀。 - 城市:去「市 / 地区」,远程统一为 `remote`;**城市不同不判重**。 - URL:去 query 与 hash(跟踪参数不得产生第二身份)。 ### 5.4 筛选规则 用户可配置(均有默认值,可空): | 规则 | 默认 | |---|---| | 目标职位关键词 | 取自在线简历的期望职位 | | 城市 | 取自期望城市;`remote` 始终放行 | | 薪资下限 | 取自期望薪资下限,仅当岗位薪资可解析时比较 | | 排除关键词 | `销售, 外包, 驻场, 培训, 中介, 保险` | | 公司黑名单 | 空 | | 单轮上限 | 10 | | 每日上限 | 30 | **被筛掉的岗位必须给出 `reason`**,且进入"已过滤"列表——用户可以捞回来。沉默丢弃是不可接受的。 ### 5.5 投递执行 **流程**: ``` 生成待投清单(含判重理由) ↓ 整批确认(面板勾选 + 点投 / 对话里说"投") 对账前置校验 → 创建 run → 逐个:领取租约(claim) → 打开岗位页 → 点击"立即沟通" → 输入/确认招呼语 → 发送简历 → 校验"简历卡片进入聊天流" → 判定 submitted / uncertain → 写证据 → 释放租约 → 下一个 ``` **"投递成功"的定义(B 级判据)**: - 点下"立即沟通"只算 `applying`; - **招呼语发出 + 简历卡片出现在聊天记录中**,才算 `submitted`; - 两者任一无法确认 → `uncertain`。 理由:A 级判据(按钮消失)会把"只打了个招呼、简历没送达"记为成功,漏斗虚高比没有漏斗更危险。 **节奏与熔断**: | 参数 | 值 | |---|---| | 单轮上限 | 10 | | 每日上限 | 30 | | 投递间隔 | 30–90 秒(随机) | | 长休 | 每 5 个成功,休 3–5 分钟 | | 熔断条件 | `uncertain` 出现 / 验证码 / 风控码 9·31·32·36·37 / 连续 2 次失败 | | 熔断行为 | **立即停止整轮**(不是跳过继续),已投记录保留,剩余回到 `queued` | **证据链**:每次 `submitted` 保存一张截图(`Page.captureScreenshot`)+ 聊天记录文本片段 + 时间戳,落 `evidence/.png` 与事件里的 `evidence` 字段。 **招呼语:BOSS 预设 + 填槽,不是自由生成** 以 BOSS 的预设招呼语为基座,把 JD 里的关键信息填进固定槽位。**模型只负责选模板和取值,不负责造句。** | 槽位 | 取值来源 | 取不到时 | |---|---|---| | `{岗位}` | 岗位 title | 必填;取不到则不改写,直接用预设 | | `{方向}` | 在线简历「期望职位」 | 退回岗位 title | | `{年限}` | 由简历工作经历计算 | 省略整个分句 | | `{技能1}` `{技能2}` | 简历技能 ∩ JD 关键词 | 只填 1 个,或省略 | | `{匹配点}` | JD 中的硬性要求(如「高并发」「Go」「ToB」) | 省略整个分句 | **反编造规则(硬)**: - 任何槽位值必须能在**在线简历**或**该岗位 JD** 中找到原文出处,找不到就不填,退回预设模板; - 不生成简历里没有的经历、不编数字、不承诺「保证胜任」; - 不放链接、邮箱、微信(触发风控,且平台侧也不允许); - 长度 ≤ 100 字(BOSS 招呼语输入限制内)。 理由:自由生成看起来更"智能",但它同时引入三类风险——编造事实(面试一问就露)、千篇一律(HR 一眼看出是 AI)、触犯风控。填槽在保持个性化的同时,把风险压到"取错值"这一种,而取错值是可以被"必须找到原文出处"这条规则挡住的。 ### 5.6 账本与状态机 ``` ┌──────────────► skipped(终态) │ new ─► queued ─► applying ─┬─► submitted ─┬─► replied ─┬─► interview ─┬─► offer(终态) │ │ │ └─► rejected(终态) │ │ └─► rejected │ └─► rejected / withdrawn ├─► uncertain ─┬─► submitted(人工裁决) │ ├─► replied │ └─► failed / withdrawn └─► failed ────► queued(人工允许重试) ``` **硬规则**: - `submitted` 之后,**任何自动化都不得再改状态**,只能由用户或对账(仅补)改动。 - `uncertain` 是独立状态,**永不自动重试**。它表示"点了,但不知道成没成"。 - 每次状态变更追加一条事件,不修改历史。 - `rejected` / `offer` / `withdrawn` / `skipped` 为终态。 ### 5.7 对账 **原则:平台只用来补"投没投出去",绝不用来改"结果如何"。** `geekDeliverList` 只证明"沟通过",不提供面试/拒绝信息,因此: | 情况 | 处理 | |---|---| | 平台有、账本无 | 补记 `submitted`,`source: reconcile`,时间取平台字段,缺失用对账时间 | | 账本 `submitted`/`uncertain`、平台无 | 标 `discrepancy`,**不改状态**,进人工队列 | | 账本 `queued`/`applying`、平台有 | 补记 `submitted`(点击成功但没记上,正是要防的缝) | | 账本 `rejected`/`interview`、平台只说沟通过 | **不动** | **时机**(三个): 1. 每轮投递**开始前**自动跑一次(前置校验); 2. 每轮投递**结束后**自动跑一次(验证本轮记账); 3. 用户手动触发(面板或对话)。 **冷启动**:首次安装执行**全量回填**,把 `geekDeliverList` 能翻到的历史投递全部导入,标 `source: reconcile-backfill`。 - 回填数据**只参与判重,不参与转化率统计**(回填没有后续状态,会污染漏斗)。 - 回填没有 JD,只能靠「公司 + 职位 + 城市」指纹判重,用不上 SimHash。这是可接受的降级。 ### 5.8 报表 三张表 + 一个结论: | 表 | 内容 | |---|---| | 漏斗 | 投递 / 回复 / 面试 / offer 各阶段数量与转化率(排除回填数据) | | 来源 | 各平台/各搜索来源的投递量与回复率 | | 周趋势 | 最近 8 周投递量 | | **结论** | 一句自然语言:"XX 来源回复率最高,建议加投" | **报表必须能导出 Markdown**,落到工作区,便于回看与分享。v0.1 图表不做,用表格代替。 ### 5.9 Skill:把模型约束在正确的位置 插件注册一个 `job-ledger` skill,内容为**硬性编排约束**,不是建议: 1. 投递前必须 `reconcile`;不平账不许投。 2. 必须先把清单(含判重理由)展示给用户,得到**明确同意**后才可 `apply`。 3. `apply` 后必须展示结果,`uncertain` 必须单独标红并等待人工。 4. 不得自动重试 `uncertain`,不得自动扩大单轮上限。 5. 模型不得修改判重结论,只能解释。 6. 简历原文仅在用户要求时展示,不得主动复述 PII。 7. **能引导到面板的操作,优先引导到面板**(勾选投递比对话确认更可靠)。 ### 5.10 Web 面板(三屏) **定位**:对话负责"想"和"解释",面板负责"确认"和"监督"。两者读写同一份账本,不允许有第二套逻辑。 **注册方式**(与 `dsh-superagent` 同构): - 侧边栏图标:`ctx.slots.register({ name: 'sidebar.panellist', id, order, label }, Icon)` - 主区面板:`ctx.slots.register({ name: 'main', key, ... }, Panel)` - 数据:host 侧 typed API + 客户端 `usePoll` 轮询(运行中 1 秒,空闲 5 秒) **三屏:** | 屏 | 内容 | 空态 / 异常态 | |---|---|---| | **待投** | 清单(公司 / 职位 / 薪资 / 城市 / 判重理由 / 来源)+ 多选 + 「投递 N 个」按钮 + 实时进度 + 暂停 / 中断 | 未登录 → 引导扫码;清单为空 → 说明为什么空(今天投满了 / 没有新岗位 / 都被判重) | | **概览** | 今日已投 / 剩余额度 / `uncertain` 待处理 / 对账差异数 / 昨日回复 | 每个数字必须能点进对应列表——**数字与列表长度必须一致** | | **对账差异** | 「平台有、账本无」与「账本有、平台无」两条列表 + 单条/批量补记 | 无差异 → 一行"已平账";对账失败 → 显示原因与重试 | **投递 run 状态机(面板与对话共用):** ``` queued ──► running ──┬──► done ▲ ├──► halted(熔断:uncertain / 验证码 / 风控 / 连续失败) │ └──► paused(用户点暂停) └──── resume ─────┘ ``` - `halted` 后剩余项回到 `queued`,**不自动继续**。 - 宿主重启或浏览器崩溃:run 标 `halted: host-restart`,已投的不重投,未投的回 `queued`。 - run 状态写事件日志,重启后可恢复展示(不恢复执行)。 **单一投递入口(硬约束)**: 面板按钮和模型工具**必须调用同一个 host 函数** `startRun(ids, grantedBy)`。`grantedBy` 记录 `{kind: 'panel' | 'model', at}`。任何绕过该入口直接点击的路径都视为缺陷。 理由:加面板后出现第二条写入路径,若不收敛,会得到两个行为不一致的投递器——而这正是本项目要消灭的东西。 --- ## 6. 数据模型(事件 schema v1) **存储位置**:`$DSH_HOME/job-ledger/` ``` events.jsonl # 唯一真源,只追加 evidence/.png # 投递证据截图 chrome-profile/ # 专用浏览器登录态(含 cookie,权限 700) ``` **事件信封**(每行一条,`v` 为 schema 版本): ```json { "v": 1, "at": "2026-11-03T06:12:00.000Z", "type": "app/state", "appId": "a_1", "state": "submitted", "evidence": "evidence/a_1.png" } ``` | type | 载荷 | 说明 | |---|---|---| | `profile/set` | `profile` | 合并式配置更新 | | `job/add` | `job` | 新岗位入账 | | `job/decision` | `jobId, decision, reason, duplicateOf` | 判重结论(可追溯) | | `job/status` | `jobId, status` | 岗位状态投影 | | `app/create` | `app` | 创建申请 | | `app/state` | `appId, state, note, evidence` | 状态迁移 | | `lease/grant` | `jobId, owner, expiresAt` | 领取租约 | | `lease/release` | `jobId` | 释放租约 | | `ingest/summary` | `added, duplicates, filtered, invalid` | 一次发现的汇总 | | `reconcile/summary` | `matched, backfilled, discrepancies` | 一次对账的汇总 | | `run/start` | `runId, ids, grantedBy` | 一轮开始,记录授权来源 | | `run/halt` | `runId, reason` | 熔断 / 暂停 / 宿主重启 | | `run/end` | `runId, summary` | 一轮结束(含 submitted/uncertain/failed 计数) | **job 字段**:`id, platform, externalId, url, title, company, companyNorm, titleNorm, city, cityNorm, salaryRaw, salaryMin, jdText, jdHash, fingerprint, postedAt, firstSeenAt, status, source` **app 字段**:`id, jobId, state, source(applied|reconcile|backfill), resumeId, greetText, evidence, runId, createdAt, updatedAt` > `resumeId` 即使 v0.1 只有 `"online-v1"` 也必须存在——否则未来引入多版本简历时,历史数据无法回溯,漏斗归因能力永久缺失。 **持久化保证**: - 写入 `write + fsync` 后才算成功; - 启动时 replay;**遇到无法解析的行即停止 replay**(截断的尾巴不可靠,不静默重同步); - 事件不修改、不删除;需要"撤回"时追加补偿事件; - run 进度在内存 + 事件双写,重启后以事件为准。 --- ## 7. 非功能需求 | 类别 | 要求 | |---|---| | **零原生依赖** | host 侧只用 Node 内置:`child_process` 起 Chrome、原生 `fetch` 读 `/json/list`、原生 `WebSocket` 说 CDP。`npm i` 不需要编译。 | | **构建** | 客户端用 TypeScript + tsdown 预构建,产物入库(`files: lib`);用户安装无需构建。 | | **安装** | `dsh plugin --profile web add dsh-job-ledger`,无其他步骤 | | **兼容** | `engines.dsh >= 0.2.1-rc.1`、Node `^22.19 \|\| >=24` | | **性能** | 判重 O(n) 线性扫描;1 万条岗位内,单次发现(50 条)< 2 秒;面板首屏 < 300 ms;客户端包 < 200 KB gzip | | **隐私(本地)** | 全量简历 JSON 存本地;目录 700、文件 600;**v0.1 不加密**(加密会引入密钥管理,破坏一键装) | | **隐私(模型)** | 配置项 `privacy.resumeExposure: redacted \| full`,**默认 `redacted`**;作者本机可设 `full` | | **可移植** | 账本是纯文本 JSONL,可整包拷走;不锁定任何服务 | | **失败可见** | 所有失败返回结构化错误码与人类可读原因,不静默吞;面板上失败必须可见且可重试 | **关于 `redacted`**:剥离手机号、邮箱、身份证、住址、真实姓名;保留期望岗位/城市/薪资、工作年限、技能、公司+职位+时间段。理由:匹配不需要手机号,需要手机号的场景不该由模型做。 --- ## 8. 交互设计 ### 8.1 首次安装(约 3 分钟) ``` 1. 插件拉起专用 Chrome → 用户扫码登录 BOSS 2. 读取在线简历 → 展示摘要:"后端工程师 / 深圳 / 25-35K / 5 年" 3. 确认筛选条件(默认取自期望,可改) 4. 全量回填历史投递 → "已导入 87 条历史投递,其中 12 条在 90 天内" ``` ### 8.2 一轮投递(两条路径,同一入口) **路径 A:面板(推荐)** ``` 侧边栏图标 → 待投屏 [x] 公司A / 后端工程师 / 30-45K / 深圳 新岗位 [x] 公司B / Golang 开发 / 25-40K / 深圳 新岗位 [ ] 公司C / 后端工程师 / 20-30K / 深圳 疑似重复:与 8 月投过的岗位 JD 相似度 91% ↓ 点「投递 2 个」→ 进度 1/2 → 完成:✓1 ✗1(岗位已下线) ``` **路径 B:对话** ``` 用户:帮我找今天能投的岗位 模型:发现 43 个 → 剔除 18 个重复、9 个不符合条件 待投 10 个(表格)+ ⚠3 个疑似重复待裁决 建议在面板勾选投递(更可靠) 用户:第 3 个不要,其余投吧 模型:对账通过 → 开始投递 ……(进度同样可在面板看) ``` ### 8.3 关键文案原则 - 任何被剔除的岗位都要能点开看理由; - 任何 `uncertain` 都要显眼且**不可被自动处理**; - 面板上的数字必须等于它链接的列表长度(诚实状态); - 健康状态不占版面,问题必须可操作。 --- ## 9. 技术架构 ``` ┌─────────────────── DSH Web 前端 ────────────────────┐ │ client plugin: 侧边栏图标 + 三屏面板 │ │ 待投 / 概览 / 对账差异 │ │ usePoll(1s|5s) + caller(命令) │ └───────────────────────┬─────────────────────────────┘ │ typed API(host 暴露) ┌───────────────────────▼─────────────────────────────┐ │ DSH host 进程(cordis 插件 dsh-job-ledger) │ │ ├─ host-api/ 只读投影 + 命令(startRun/pause/…) │ │ ├─ tools/ profile · resume · discover · queue │ │ │ apply · record · reconcile · report │ │ ├─ run/ 长任务状态机(唯一投递入口) │ │ ├─ ledger/ events.jsonl(append + fsync + replay)│ │ ├─ dedupe/ 归一化 · 指纹 · SimHash · 冷却 │ │ ├─ cdp/ Chrome 启动 · 连接 · 监听 · 点击 · 截图│ │ └─ skill/ job-ledger(编排硬约束) │ └───────────────────────┬─────────────────────────────┘ │ CDP (127.0.0.1:9222, WebSocket) ┌────────▼────────┐ │ 专用 Chrome │ ← 用户扫码登录一次 │ (独立 profile) │ └─────────────────┘ ``` **为什么单进程是对的**:账本与点击在同一个进程,`claim` 和 `click` 之间不存在进程边界,重复投递的缝从架构上消失。这是移除 job-hunter 之后最大的收益——原先为跨进程共享设计的"文件即 IPC"不再需要。 **保留 append-only 日志**:不是为了跨进程,是为了崩溃安全(截断可恢复)与审计(每条结论可追溯)。 **两条路径一个入口**:面板按钮与模型工具都调 `startRun()`;`grantedBy` 记录授权来源。这是加面板后最重要的一条架构约束。 **模块边界**:所有平台相关的选择器、URL、字段映射集中在 `cdp/boss.js` 一个文件,平台改版只改这里,且带版本号。 --- ## 10. 风险与缓解 | 风险 | 概率 | 影响 | 缓解 | |---|---|---|---| | **账号被限流/封禁** | 中 | 高 | 整批确认 + 单轮/每日上限 + 随机间隔 + 熔断;不伪造请求;不绕过验证码;出问题立即停 | | **BOSS 改版导致选择器失效** | 高(必然) | 中 | 选择器集中一处、版本化;失败即 `uncertain` 而非误判成功;提供"自检"命令 | | **判重误杀** | 中 | 高(不可恢复) | 三级判定 + 疑似区人工裁决 + 同公司不同职位永不判重 | | **`uncertain` 堆积** | 中 | 中 | 熔断整轮;对账兜底;人工裁决入口 | | **长任务中断**(宿主重启 / 浏览器崩溃) | 中 | 中 | run 标 `halted`,已投不重投,未投回 `queued`;面板可恢复展示 | | **两条投递路径行为漂移** | 中 | 高 | 强制单一入口 `startRun()`;面板与工具共用同一实现 | | **AI 误判匹配** | 高 | 中 | 模型只排序不改判重;理由可展开;用户可捞回被过滤项 | | **违反平台协议** | — | 中 | 只用自己账号;只监听不重放;不提供绕过风控能力;文档明确风险由使用者承担 | | **账本损坏** | 低 | 高 | fsync + 截断即停 + 每周自动导出 Markdown 快照 | --- ## 11. 验收标准 **效果验收(连续使用 2 周)** | # | 标准 | 测法 | |---|---|---| | 1 | **零重复** | 对账差异率 < 2%,同一「公司+岗位」0 次重复投递 | | 2 | **每轮 ≤ 5 分钟** | 从"开始"到 10 个岗位投完(含确认时间) | | 3 | **能支撑一个决策** | 报表能回答"哪个来源回复率高",且据此调整过一次策略 | > 第 3 条是硬门槛:**如果账本产出的数据没有改变过你的任何一个行为,它就只是记账本,不是产品。** **技术验收(每版必过)** 1. 全新机器:`dsh plugin add` → 扫码 → 读到在线简历,全程 ≤ 5 分钟。 2. 同一岗位投两次:第二次被拦截,`reason` 正确。 3. 模拟点击超时:状态为 `uncertain`,整轮熔断,重启后不自动重试。 4. 手工删掉一条 `submitted` 事件后对账:能补回,且标 `source: reconcile`。 5. 断网后重启:账本 replay 正常,不丢不重。 6. 1 万条岗位下:单次发现 + 判重 < 2 秒。 7. **面板点投 3 个,中途拔网线**:run 进 `halted`,已投不重投,剩余回 `queued`,面板显示一致。 8. **面板与对话同时投同一批**:不重复投递(租约生效),第二次被拒绝且有明确提示。 --- ## 12. 里程碑 | 阶段 | 内容 | 出口标准 | |---|---|---| | **P0 账本内核** | 事件日志、判重三级、状态机、租约、报表 | 纯逻辑测试全绿;无浏览器依赖 | | **P1 读简历** | Chrome 启动、CDP 连接、简历三接口监听、脱敏层 | 能读到结构化简历并展示摘要 | | **P2 发现+判重** | 搜索/推荐监听、批量入账、待投清单 | 43 个岗位 → 正确剔除 | | **P3 真投递** | 点击、招呼语、简历送达校验、证据、熔断、run 状态机 | 真投 1 个成功 + 1 个 uncertain 正确熔断 | | **P4 对账+回填** | deliverList、四类冲突、冷启动回填 | 差异率可计算;回填不污染漏斗 | | **P5 面板** | 双端构建、三屏、轮询、勾选投递、进度与中断 | 面板点投 3 个、中断、恢复展示全通 | | **P6 打磨** | Skill 文案、报表导出、自检命令、README | 陌生机器按 README 能装通 | --- ## 13. 已知限制(不藏) 1. **对话路径的授权仍不可技术强制**:模型可以违反 SKILL.md 直接调 `apply`。缓解:面板路径是技术强制的(点按钮=授权),Skill 引导用户优先用面板;`apply` 要求显式 `approvedIds`,单轮上限写死在代码里。 2. **成功判定依赖 DOM**:BOSS 改版可能让"简历卡片"识别失效 → 退化为 `uncertain`(宁可不确定,不可假成功)。 3. **对账只能验"投没投"**:回复、面试、offer 必须手动记,或等 v0.2 接聊天记录。 4. **仅 BOSS**:换平台等于重写 `cdp/` 与判重的城市/薪资解析。 5. **不加密**:本地文件是可读明文,含完整简历。备份与分享需自行注意。 6. **改了 client 要重新构建**:客户端产物是预构建的;DSH 的 HMR 只在 `pnpm run dev:web` 同时运行时才热更,否则需重启面板。 --- ## 14. 已决问题(原开放问题,v0.3 全部关闭) | # | 问题 | 结论 | 落地位置 | |---|---|---|---| | 1 | 招呼语怎么来? | **BOSS 预设 + 填槽**,槽位值必须能在简历或 JD 中找到出处;不自由生成 | §5.5「招呼语」 | | 2 | 用户从哪来? | **小红书**,教程 + 真实截图(安装、真投递、对账抓差异);不投信息流 | §2「用户渠道」 | | 3 | 数据目录可否改? | 允许,配置项 `dataDir`,默认 `$DSH_HOME/job-ledger` | §6、§7 | | 4 | 回填页数上限? | **10 页**;超出提示"更早的历史未导入" | §5.7 | --- ## 15. 附录:决策记录 | # | 决策 | 结论 | |---|---|---| | 1 | 产品形态 | 不做客户端,做 DSH 插件 + npm 分发 | | 2 | 目标用户 | 技术半熟;v0.1 先作者本人 | | 3 | 分发路径 | v0.1 一条命令;桌面打包留 v0.2 | | 4 | 平台范围 | 仅 BOSS | | 5 | 投递授权 | 整批确认(非逐个、非全自动) | | 6 | 真投递 | v0.1 就要真投 | | 7 | 跨进程一致性 | 曾定"共享追加日志";移除 job-hunter 后简化为单进程内部日志 | | 8 | 判重策略 | 三级:确定 / 疑似 / 新 | | 9 | 冷却期 | 90 天 | | 10 | JD 相似阈值 | SimHash ≤ 3,且城市必须相同 | | 11 | 简历来源 | BOSS 在线简历(非上传 PDF) | | 12 | 在线简历获取 | CDP 监听页面自身请求,不重放接口 | | 13 | 对账 | 进 v0.1 | | 14 | 冷启动 | 全量回填历史投递 | | 15 | 回填口径 | 只参与判重,不参与转化率 | | 16 | PII | 全量本地存;`resumeExposure` 开关,默认 `redacted`,本机 `full` | | 17 | 岗位发现 | 插件自己拉(搜索/推荐),不用 job-hunter | | 18 | 投递执行 | 插件自己投(CDP 点击),job-hunter 移出 v0.1 | | 19 | 成功定义 | 招呼语发出 + 简历卡片进入聊天流 | | 20 | 对账原则 | 平台只补"投没投出去",不改"结果如何" | | 21 | 验收 | 零重复 / 每轮 ≤ 5 分钟 / 能支撑一个决策 | | 22 | 交互形态 | 对话 + Web 面板双轨,读写同一份账本 | | 23 | 面板范围 | 三屏:待投 / 概览 / 对账差异;图表与设置页延后 | | 24 | 投递入口 | 面板与模型共用 `startRun()`,单一入口,记录 `grantedBy` | | 25 | 招呼语 | BOSS 预设 + 填槽;槽位须有原文出处,不自由生成 | | 26 | 获客渠道 | 小红书,教程 + 真实截图 | | 27 | 数据目录 | 可配置 `dataDir`,默认 `$DSH_HOME/job-ledger` | | 28 | 回填上限 | 10 页 |