ResumeForge v0.10.0
从上一版升级:双击 update.cmd(或 git pull --ff-only 后重装依赖)即可,data/ 与 .env 不受影响。
本次没有数据库迁移,回滚只需切回旧版本;升级前仍建议在「设置 → 数据集」里导出一份备份。
直接下载:ResumeForge-0.10.0.zip,解压后双击 start.cmd(首次运行会自动准备 Python / Node 环境)。
截图与完整说明见 README。
0.10.0 - 2026-09-21
Added
-
采集条件里多了「按招聘网站的条件筛」——把站点筛选栏整条搬了进来(求职类型 / 薪资待遇 / 工作经验 / 学历要求 / 公司行业 / 公司规模 / 融资阶段)。这些是站点自己的筛选参数,网站在搜索时就替你筛掉了,比"采回来再按岗位字段本地筛"更准、也少翻很多页;原来那几个「我的期望薪资 / 我的经验 / 我的学历」保留不动(语义不同:那几个筛的是"你的条件 vs 岗位要求",例如"我是本科";新增的这组筛的是"岗位要求本科"),两者可以同时用。
三项设计决定,各自都对应一类踩过的坑:
- 选项清单读自站点,不写死。写死的一份在站点改编码之后会静默筛错(用户以为按「本科」筛了,实际发出去的是另一个编码)。清单有三条来源并按可信度降级:你的登录会话(页面筛选栏,最准)→ 全网通用清单(免登录接口)→ 内置快照(离线兜底,只覆盖 6 个短清单)。来源在界面上如实标注。
- 「求职类型」必须读登录态。2026-09-21 实测:这个标签的选项因人而异——你的账号能看到「不限 / 全职 / 兼职 / 实习」,未登录时只能拿到「不限 / 全职 / 兼职」。写死一份就等于替所有用户决定了他们能选什么,所以浏览器在跑时优先读页面筛选栏,读不到才退回通用清单并明确告诉你"当前是通用清单"。
- 编码用之前必须校验。采集开始前拿当次读到的清单逐项核对,核对不过的不发给站点、并如实计入"未能生效"(
task.config.site_filter_unapplied,界面上单独提示)。旧编码在站点看来同样"合法",发出去会静默筛错,那比不筛更糟。
参数名与编码全部由真实点击得到,不是从 HTML 推的:埋点属性写的是
sel-job-rec-exp,而地址栏里的参数名是experience——照 HTML 抄就错了。逐条实测记录(2026-09-21,登录会话,点一下读一次地址栏):jobType=1901/1903/1902、salary=402–407、experience=101–108、degree=202–209、scale=301–306、stage=801–808、industry=1000xx。行业的编码只认接口那份:页面上的ka是渲染序号(sel-industry-23对应的真实编码是101407),照抄会发出一个"合法但不相干"的编码。实现落在
services/sites/boss_filters.py(纯函数:解析/合并/校验,可离线逐条测)、BossSearchMixin.prepare_collect_filters(解析与校验)与BossAdapter.build_search_url(拼参数)。新增GET /api/collect/filters下发清单;CollectConfigIn新增filters: dict[str,str](向后兼容:不给就是空,行为与改动前一致)。job_type与站点筛选撞车时以用户显式选择为准:否则同一个jobType参数会在地址里出现两次,站点取哪一个不确定——那正是"我选了实习却混进全职"的成因。 -
投递/采集批次「跳页不打断 + 完成全局通知」。批次由后端线程执行,此前切到其他页面只是看不到进度;现在全局布局挂了一个批次监听(
useTaskCompletionWatcher+TaskCompletionNotifier):只要本会话里见过某批次进行中、随后它进入终态,就弹应用内通知(成功/失败/停止各有文案与统计)并尽力发一条系统桌面通知(仅在已授予通知权限时,绝不主动弹权限框),点击通知跳回投递台。判定是 transition 语义:应用启动时早已结束的批次不会误报。 -
投递记录按批次分组展示。一次「开始投递」= 一个批次,一次投出的 N 个岗位在「投递记录」里折叠成一组:组头显示批次号、时间、状态与该批次自己的统计(成功/失败/跳过),点击展开看组内每条记录(重投/详情照旧)。新增
GET /api/apply/records/grouped:筛选(关键词/结果)作用在记录上、分页按批次计,没有命中记录的批次整体不出现;旧的扁平/records保留。 -
岗位类型真正参与筛选(此前仅入库标注)。2026-09-20 用真实登录会话实测校准:BOSS 官方「求职类型」参数为 全职=1901 / 兼职=1903 / 实习=1902(无校招档,校招是独立专区),接口响应的岗位类型编码为 0=全职(社招)、4=实习、5=校招、6=兼职。因此:实习/社招直接映射到站点官方参数(搜索时就由站点筛掉,省翻页),校招由采集后的本地筛选按接口编码判定;同时所有类型都保留本地二次校验——站点参数将来若失效(历史上
jobType=4就不是有效参数),本地仍能兜住。接口没给类型字段(DOM 兜底路径)的岗位保留并如实计入"未能判断",绝不凭标题猜类型。界面上岗位类型的标签随所选值变化(实习/社招=「站点筛选」、校招=「采集后筛选」),不再标「仅标注」。 -
卡片点一下就能看详情(内推 / 资料箱 / 面经 / 提醒 / 知识库 / 投递记录 / 投递队列)。这些列表与卡片此前只放得下摘要:内推看不到联系方式与内推码、资料箱看不到正文全文与附件、面经看不到完整问题清单、提醒看不到绑定对象与备注、投递队列看不到招呼语全文。现在统一为「点卡片 → 右侧详情抽屉」,字段与正文分区展示,抽屉里带「编辑」等后续动作。抽出一个
components/common/RecordDetail.tsx(DetailTrigger+RecordDetailDrawer)作为唯一实现,避免六处各写一套排版与关闭方式。可点击卡片用role="button"+tabIndex而不是<button>——卡片里有编辑/删除按钮,按钮套按钮是无效 HTML;点击时跳过内层可交互元素(isFromInnerControl),否则点「删除」会顺带打开详情。键盘 Enter / 空格同样可打开,并补了可见的焦点描边。 -
月历点任意一天看当天安排。提醒的月历视图(首页「近期提醒」与「求职进度 → 提醒」用的是同一个
CalendarView)此前点日期格没有任何反应。现在点一格就弹出那一天的完整安排:标题、类型、状态、时间与备注;没有安排时明确说「这一天没有安排」。明细内建在组件里而不是由各页面各接一次,这样首页与进度页的行为不会漂移;原有的onSelectDate回调保留,传了就由父组件接管、不弹内建明细。 -
投递台只接纳「来源在招聘网站内」的岗位(用户反馈:手动录入、来源指不到任何招聘网站的岗位以前也能加进队列,强行开始投递只得到一条「未知失败」)。原来的判据只在执行层体现——
registry.for_job()解析不出站点就抛FAILURE_UNKNOWN,于是用户看到的是含糊的「未知失败」,也说不清是谁的问题。现在把闸门提到入队与开始投递两处,执行层保留兜底:- 新增
SiteRegistry.resolve_for_job()(for_job的不抛异常版本)与apply_service.job_apply_site()作为唯一判据:岗位的source/source_url必须能落到已注册站点上。入队、开始投递、列表标记、读取模型四处共用它,不再各判一遍。 - 入队:不满足的岗位返回 409 +
site_unsupported,文案写明"当前支持哪些站点"与"请到对应网站手动投递",并保证不写进队列。注意判据是"来源/投递链接能否归属到站点",所以手动录入但贴了招聘网站投递链接的岗位照常允许——这比"必须用采集"更贴合实际用法。 - 开始投递:队列里若有来源不支持的条目(只可能是闸门上线前的历史数据),在建批次之前一次性拦下并点名是哪几个岗位、怎么处理(移出队列),不再留下一批注定失败的条目;勾选投递时同样受此约束。
- 失败分类新增
site_unsupported("岗位来源不支持自动投递"):万一真漏到执行层,记录里写的是真实原因而不是「未知失败」。它是前后端共用的一份取值,新增了一条镜像守卫测试逐字比对FAILURE_CATEGORIES/FAILURE_CATEGORY_LABELS与frontend/src/types/apply.ts,防止两处漂移后"记录里的分类在前端筛选里找不到"。 - 界面:
JobOut/ApplyQueueItemOut新增apply_supported(后端算好下发,前端不写死站点名或主机名)。岗位详情的「加入投递台」对这一类岗位禁用并把原因写在页面上(含"把投递链接改成该岗位在招聘网站上的地址即可自动投递"这条出路);队列里这类条目在「岗位」列标红「来源不支持」、勾选框禁用,队列顶部给出一次性说明与两种处理方式;后端拒收时弹提示而不给「仍然加入」按钮(这一类确认也没用)。
- 新增
-
启动器的失败提示改成了中文,并给了可操作的下一步(2026-09-21 在干净副本里真跑首次安装时核对)。首次安装本身是通的(建 venv → 装 38 个后端包 → 装 366 个前端包 → 两端 200),但失败时只说一句
Failed to install backend dependencies.,既不说是为什么、也不说下一步做什么——而"网络/代理导致依赖装不上"恰恰是全新机器最常见的失败。现在:- 全部用户可见提示改成中文(Python/Node 的获取与安装、端口占用、venv 创建、pip 与 npm 安装、启动就绪)。
- pip / npm / venv 失败各给出 3~4 步处置,含可直接粘贴的命令(如换国内镜像
-m pip install -i https://mirrors.aliyun.com/pypi/simple -r backend\requirements.txt)与"打不开这个地址就是网络被拦了"这类可自查的判据。 - 顶层捕获把
throw变成一段干净的话:此前 PowerShell 会先打印脚本路径、字偏移和出错的源码行,真正有用的建议被埋在噪音后面;现在只输出「启动失败。」+ 原因 + 怎么办 + 已停止,start.cmd的pause保证窗口不会一闪而过。 - 中文提示要求脚本带 UTF-8 BOM,这是必须的而不是风格问题:
start.cmd用的 PowerShell 5.1 会把无 BOM 的文件按 ANSI 码页解码,中文字符串会直接把脚本读崩(实测去掉 BOM 后 PS 5.1 报 33 处语法错误)。启动器测试新增一条字节级守卫:scripts/*.ps1里有非 ASCII 就必须有 BOM。(AGENTS.md原先写的"必须保持纯 ASCII"不准确——那条真正逐字节校验的是backend/requirements*.txt。) - 顺带修好一处既有隐患:
Update-ResumeForge.ps1里有中文注释却没有 BOM(只影响注释的可读性,未造成解析失败),现已补上 BOM。
-
数据集可以「导出全部」(2026-09-21 逐表验证备份覆盖情况时发现的缺口)。应用支持多份数据集、每份各占一个数据库文件,而原来的导出只带当前活动的那一份——列表里其余几份不在包里。一个用户"备份了"却丢了几份,通常要到需要恢复时才发现,那时原始文件可能早就不在(真实复测:本机那份「resumeforge-demo-data-20260915」就不在任何备份里)。现在列表里不止一份时会出现 导出全部数据集,把当前这份加上其余每一份装进同一个包。
- 格式号随内容变化:只有活动数据集时仍是格式 1(老版本照常可读、旧备份照常可导入);一旦带上其余数据集就标成 2,老版本会明确拒收("请先升级应用再导入")而不是安静地只恢复一份。老版本读不懂
datasets/这一段,放行就等于让用户以为恢复成功了。 - 随行的每一份都做同样的密钥剥离:不是只处理活动的那一份。这个包正是用户会长期保存或转发的东西,漏一份就是把那份数据集里的明文 API Key 一起送出去。
- 导入是"要么全成、要么不动":包里所有数据集先全部校验完再开始写盘。边写边验的下场是——后面某份不合法时前面几份已经落盘,用户看到一次失败、列表里却多出几份半截的数据集。(这条是被自己的测试抓出来的:第一版实现留下了半截数据。)
- 包内路径不可信:清单里的
file只认datasets/<合法 id>.db这一种形状,指向主库成员或带..的一律拒收——一个改过的包能靠这个字段让导入过程把别的东西当数据集写盘。 - 恢复出来的数据集用全新标识:沿用包里的 id 会覆盖本机早已存在的那一份(不可逆)。
- 格式号随内容变化:只有活动数据集时仍是格式 1(老版本照常可读、旧备份照常可导入);一旦带上其余数据集就标成 2,老版本会明确拒收("请先升级应用再导入")而不是安静地只恢复一份。老版本读不懂
Fixed
-
让后端与前端测试在 Linux CI 上也能全绿(此前只有 Windows 全绿)。三处原因、三处修法:
- 浏览器定位在非 Windows 上是坏的。
BrowserManager._existing_env_paths用Path(base) / r"Google\Chrome\Application\chrome.exe"拼接,而反斜杠在 Linux/macOS 上只是普通字符、不是路径分隔符,于是候选路径永远找不到,浏览器发现只能靠shutil.which兜底——在没装浏览器的 CI 上 7 个浏览器定位测试全部失败。现在按\\拆段后用Path.joinpath(*parts)拼接,跨平台一致;这也顺带修好了一处真实缺陷(Linux/macOS 用户以前无法靠PROGRAMFILES这类环境变量定位浏览器)。 - 服务端 PDF 生成需要中文字体,而 Linux CI 没装。
pdf_exporter已经认 Noto CJK(/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc),只是 runner 上没有这个字体,导致 14 个分享包用例在「生成 PDF」那一步报 409。修复是给 Linux job 加一步sudo apt-get install -y fonts-noto-cjk(分享包这个功能本来就是要测 PDF 的,不该为了迁就 CI 而跳过)。 - 两个前端用例在慢速 runner 上不稳定。
SettingsPage的「切换技能开关」在列表异步渲染完成前就用同步getByRole取开关(改成findByRole等它出现);AssistantPage的「流式回复隔离」用例给 10 秒上限太紧(放宽到 20 秒)。这两处都不是断言变松,只是给异步 UI 测试留足环境余量。
- 浏览器定位在非 Windows 上是坏的。
-
补齐发布包的必需文件清单,让 Windows 的「启动器 + 打包」测试重新变绿。
scripts/Build-Release.ps1的$RequiredFiles一度落后于backend/app/preflight.py的_REQUIRED_FILES22 项:app/data/ats_keywords.json、app/services/feature_catalog.py与 20 个提示词文件。后果只落在开发侧——scripts/tests/Test-Build-Release.ps1会抛「Build-Release.ps1 does not require 'backend/app/data/ats_keywords.json'」而让 Windows job 连续多轮失败,但实际分发的压缩包是完整的(git archive会带上全部受追踪文件,这份清单只是发布前的自检网,不是打包过滤)。修复是把清单补齐到与 preflight 一致(50 项),并留注释说明这条单方向守卫:往 preflight 加资源而不在这里登记,测试就会变红。对终端用户没有任何行为变化。 -
分享包换数据集/恢复备份之后「下载」必定 404(2026-09-21 核对备份覆盖情况时发现)。分享包的内容在数据库里(
snapshot列,随备份走),渲染出来的resume.html/resume.pdf/resume_snapshot.json/manifest.json落在磁盘目录里(不随备份走)。于是换数据集或从备份恢复到另一台机器后,列表、详情、备注全都正常,唯独点「下载」报「文件不存在」——看起来像功能坏了。现在把磁盘文件当成缓存而不是数据源:缺了就从库里的快照按同一套导出管线重新渲染,点「打开目录」也会先补齐,不会把一个空文件夹摊给用户。重新渲染的内容逐字一致;版式优先沿用原简历的模板与版式设置,原简历已被删除时退回默认版式(不去猜当初用的是哪套模板,那只会造出一个看起来对、其实对不上的东西)。comments.md刻意不补:那是收件人写进来、回传给用户的内容,数据库里没有第二份。补一个空白模板会在界面上假装"评论还在",比如实 404 更糟。- 顺带修掉一处同族隐患:分享包目录原本是
<库文件所在目录>/share_packages/<id>,而多份数据集共用data/datasets/这一个目录、分享包 id 又是每个库各自自增的——数据集 A 的第 1 份会和数据集 B 的第 1 份落到同一个目录里互相覆盖。现在非主数据额外带一层库文件名;主数据保持原路径不变(那里可能已经有用户的分享包,迁走等于让它们消失)。
-
「同公司只投一个岗位」是个空开关——投递逻辑从来没读过它(2026-09-21 用真实投递逐项验证设置时发现)。配置字段、设置界面的开关、前端类型镜像三处都在,唯独
services/apply/里一次都没出现。用户打开它、保存它,很自然会以为最多给一家公司发一条,实际队列里有几条就发几条。现在真的生效了,作用域取本批次内(与它旁边那几个选项同一粒度:每批上限 / 每日上限 / 间隔),语义与其余跳过路径一致:- 记的是"尝试过"而不是"成功过":投递失败可能发生在确认环节——招呼语其实已经发出去了,只是没读到对方回执。这时再给同一家公司发第二条,恰恰是这个开关要避免的事。所以只要动手了,这家公司就算数;方向始终是宁可少发一条,不可重发一家。
- 公司名要归一化(压掉空白、统一大小写):招聘网站上的公司名带前后空格、写法不一,不归一化就会把同一家当成两家,开关照样形同虚设。
- 跳过要说明原因:跳过的条目在
failure_detail里写明「按『同公司只投一个岗位』跳过:本批次已经投过「X」的岗位」。同一个字段有两种含义(失败诊断 / 跳过原因),所以进度面板的明细标签跟着状态走(失败=「诊断信息」、跳过=「跳过原因」),否则用户看到跳过条目会以为出错了。 - 没有公司名的条目不作同公司判断(判断不了就不做判断),不会被误吞。
-
多核机器上
npm test会在打印任何结果之前直接崩掉(JavaScript heap out of memory)。vitest 默认按 CPU 核数开 worker,而每个 worker 是一整个 Node 进程:一个 antd 重的页面文件导入起来就要一个 GB 上下,32 核机器上就是三十几个进程一起把堆撑爆——报错发生在测试跑起来之前,所以看起来像"测试跑不动"而不是"某个用例挂了"。vitest.config.ts现在把并发上限钉在 4。选 4 是实测出来的:4 与 8 的耗时分别是 ~217s 与 ~250s(瓶颈在逐文件的模块导入,不在 CPU),而 8 的时候一个自带 10 秒预算的流式用例会因争抢开始超时;核数少的 CI 本来就取不到 4 个 worker,这个上限只会在崩溃的那类机器上生效。验证:不带任何参数跑npm test,88 个文件 / 550 个用例全绿。 -
采集批次在「当前批次」里不可见,导致两件事直接失效(
GET /api/apply/tasks/current写死kind=apply)。后果不是少显示一行进度:①开始采集后切到别的页面再回投递台,进度面板是空的,看不到采集在跑;②全局完成通知靠这个接口判断"这个批次刚才在跑",采集批次从不出现,于是采集跑完永远不弹通知。两种批次本来就互斥(运行器同一时刻只跑一个),所以默认改成"不限类型",需要限定的调用方传kind即可。真实复测:采集跑起来后/tasks/current报collect #62→ 暂停 12 秒进度纹丝不动 → 继续后 0→2 正常推进 → 跑完 8/8 →current收敛为null(完成通知的跃迁信号)。 -
薪资本地筛选在真实数据下从不生效(2026-09-20 真实采集复测发现)。本地筛选代码期望招聘网站列表接口返回
lowSalary/highSalary数字字段,而真实接口只有salaryDesc展示文本("12-24K·14薪"、"500-1000元/天")——于是每个岗位的薪资都落进"未能判断"被保留,低于期望下限的岗位全部漏进来(真实复测:填了"最低薪资 20K"仍暂存 10-15K、7-11K 的岗位)。修复:新增parse_salary_text把展示文本解析成月薪区间(K/万/元·天 三种真实写法,年薪口径与"面议"如实返回"判断不了"),数字字段缺失时回退到文本解析,数字字段仍在时优先。真实复测:同一查询修复后筛掉原因出现「薪资」、"未能判断"计数从 2 归零;正向验证 ≥12K 的岗位正常保留(上限恰等于期望下限按"有交集"保留,与既有含端点口径一致)。 -
自动投递在浏览器窗口最小化/被遮挡时必然失败(2026-09-20 真实投递失败的根因,已用真实浏览器复现验证)。窗口最小化、被完全遮挡或位于未激活的虚拟桌面时,页面
document.visibilityState为hidden,BOSS 对这种页面上的点击静默忽略——事件以isTrusted=true正常到达按钮(已实测),但页面毫无反应,投递最终在"等聊天输入框"上超时,记录里只留下一条莫名的「页面结构可能已变化」。而Page.bringToFront只能在窗口内切标签页、救不了最小化的窗口。现在每次可信点击(沟通入口/发送按钮)前先做窗口可见性保障:首选Target.activateTarget(实测一步就能把最小化窗口还原并前置),辅以Browser.setWindowBounds(最小化→还原;仍不可见时用「最小化→还原」切换强制 Windows 重新激活);无法还原的环境只降级为写日志并继续,不打死流程。新增cdp_client.WindowAwareMixin.ensure_page_visible。 -
新会话的「打招呼对话框」形态不支持(2026-09-20 真实投递发现)。对从未沟通过的岗位,点击「立即沟通」后 BOSS 不再跳转聊天页,而是在详情页内弹出打招呼对话框(BOSS 会自动发出用户在站点配置的默认招呼语)。旧代码靠
.dialog-container textarea的兼容选择器"碰巧"找到了对话框输入框并成功填入,但发送按钮是div.send-message——不是<button>、类名不含 btn,所有候选选择器都匹配不到,投递必然在"等发送按钮"上失败。修复:发送按钮选择器补上.send-message;成功判定兼容对话框的「已发送」态,并明确排除"对话框开着、草稿没发出去"的反例(点击没生效时如实报失败而不是假成功)。 -
聊天页开在新标签页时投递卡死在岗位详情页。点击「立即沟通」后聊天页可能以新标签页打开、原标签页停在岗位详情——等待循环此前只盯原标签页,永远等不到输入框。现在等待循环比对点击前后的标签页快照,把新出现的聊天标签页收养为后续填写/发送的目标(
switch_to_target);点击前就存在的聊天标签页(可能是用户自己开着的旧会话)绝不收养,防止招呼语发进错误的会话。 -
「进入投递台就白屏」——根因是文件名只差大小写,Vite 把模块解析缓存住了。开发期把
components/common/recordDetail.ts改名成recordDetailCore.ts之后,components/common/下曾同时存在recordDetail.ts与RecordDetail.tsx(只差大小写)。Windows 文件名大小写不敏感,而 Vite 解析无扩展名的../common/RecordDetail时按resolve.extensions先试.ts,于是命中了recordDetail.ts并把这个解析结果缓存进了正在运行的 dev server;文件改名/删除之后它照旧请求那个已不存在的路径 → 懒加载的页面代码取不到 → React Lazy 抛错 → 错误边界显示「页面暂时无法显示」。受影响的是六个页面(投递台 / 内推 / 面经 / 提醒 / 资料箱 / 知识库),且刷新页面永远无效(坏映射在 dev server 内存里,必须重启前端)。修法与加固:① 模块名改成语义不同的词(rowActionMenu.ts/recordDetailCore.ts这类,不是只差大小写);② 新增守卫测试backend/tests/test_frontend_module_naming.py,扫frontend/src只要同一目录内出现"去扩展名后归一小写重名"就失败(放后端是刻意的:前端 tsconfig 不含@types/node,读不到node:fs;后端也已有守前端文件的先例)。注意:本次修复后需要重启前端 dev server 一次,已被缓存的旧解析不会自己消失。 -
错误边界不再只给一句「界面运行时发生异常」。它现在会把原始报错显示在页面上(以前只能靠猜),并在识别出是「懒加载模块取不到」(
Failed to fetch dynamically imported module/ChunkLoadError等稳定文案)时换成专门的文案与处置指引——这一类的正解是重启前端开发服务器,而不是刷新页面,原来的通用文案恰好把人往"多刷几次"上引。两处文案都有用例钉着(AppErrorBoundary.test.tsx)。 -
投递队列编辑过招呼语之后整张表挤在一起、操作表头与按钮不对齐。根因是列宽不固定 +
table-layout: auto:招呼语是用户可编辑的长文本,它会和「准入 / 状态」抢空间,编辑一条之后列宽就重排;而「操作」列表头默认左对齐、单元格里的按钮靠右,看起来像没对齐。现在五列全部定宽 +tableLayout="fixed"(内容长短不再影响排版)、状态列居中、操作列align="right"让表头与按钮同侧,招呼语与简历名过长时省略号 + 悬停看全文;同时用scroll.x固定为列宽之和,窄窗口下横向滚动而不是压缩每一列。 -
岗位广场的「导入时间」提示只在岗位名那几个字上有效。Tooltip 原先挂在岗位名那颗链接里,鼠标停在关键字标签或同一格的空白处时什么都不弹——等于只有一半时候管用。现在整格悬停都给「导入于 YYYY-MM-DD HH:mm」;投递队列的岗位格同理,给的是「加入队列于 ……」(那才是队列条目的时间口径)。
-
结构化错误信息被吞成「请求失败(HTTP 409)」。统一请求封装
extractError只认字符串与校验错误数组两种detail,而投递台的 409 是结构化的({message, unanalyzed, gaps, site_unsupported…})。于是所有走request()的调用方(例如「开始投递」)只能弹出一句没有信息量的话,而真正的原因——"「某岗位」的来源不是投递台支持的招聘网站,请先移出队列"——就躺在响应体里。现在会取detail.message。这条与上面的来源闸门配套:闸门给的提示必须真的能到达用户眼前。 -
浏览器明明在运行,却被报「未启动」,采集与投递全被拦住。根因是判断"浏览器在不在跑"用的是进程句柄(
self._process.poll()),而投递专用浏览器是一个独立的 OS 进程、并且有意活得比应用久(登录态就持久化在专用user-data-dir里)。所以只要重启过应用,句柄就没了——哪怕那个窗口好端端开着、调试端口答得好好的(实测:浏览器进程 00:28 启动,后端 15:34 才启动,/json/version返回 200),状态依旧报"未启动"。更糟的是点「启动浏览器」也救不回来:用同一个user-data-dir再拉一次,Chromium 会把这次启动转交给已在运行的实例然后自己立刻退出,句柄依然是死的,按多少次都没用(现场能看到多出来的渲染进程,就是被转交开出的新窗口)。现在判据换成调试端口(_probe_ready()),"归谁"则作为独立事实BrowserStatus.owned单独下发——两者是不同的东西,混在一起正是这个 bug 的来源。同时补上一个会踩坑的中间态:start()的防重复拉起改用新的is_active()(端口通 或 句柄活),否则"进程已拉起、端口还没就绪"的那几秒会拉起第二个浏览器。「关闭浏览器」不再撒谎:浏览器不是本次运行拉起的时,stop()本就是静默 no-op(应用只关自己持有的句柄,绝不按 PID 去猜进程,免得误关用户自己的浏览器),但接口原来照样返回 204、界面照样弹"已关闭",而窗口还开着——现在这种情况返回 409 并说明"请直接关闭那个窗口",界面则禁用按钮并把原因写在页面上(不放进悬停提示:antd 的 Tooltip 不特殊处理禁用子元素,而浏览器不给disabled按钮派发鼠标事件,悬停上去什么都不会弹)。这一改也修好了一个测试隔离问题:test_apply_queue_and_api.py的 docstring 写着"不连浏览器",但状态判断改走端口后,那几个用例会真的连上开发机上占着 9333 的浏览器(/browser/open甚至真的导航了它)——现在该文件统一注入假调试端口,兑现自己的约定。
Changed
-
投递队列补上「怎么用」的说明。用户反馈过两次"点进来不知道下一步该干什么"。队列页顶部现在常驻四步流程:岗位从哪来(岗位广场 → 岗位详情 → 加入投递台,自动采集导入也是同一入口)、在这里核对什么(准入结论,不确定的先做匹配度分析)、需要时怎么改(点岗位名看详情/编辑,换简历、改招呼语)、什么时候才真的投出去(勾选后点「开始投递」,在那之前不会打开任何招聘网站,不勾选=按顺序投整个队列)。空队列时给出可直接照做的下一步,不再只有一句"队列是空的"。
-
面向所有岗位的求职者:去掉"只给技术岗用"的假设(用户反馈"希望所有岗位的求职者都有相同体验")。逐项检查后修掉三类不合适的地方:
- 示例简历(每个新用户打开模板工作台看到的第一份内容):此前是"后端开发 + 高并发 + Go/Redis/Docker + 算法竞赛",非技术岗第一眼就会觉得"这不是给我用的"。改成不依赖具体行业的市场运营示例(活动策划 / Excel / 数据复盘),仍然保留原有的结构与长度(单页版式的守卫用例一行未改仍通过)。
- 用户可见的文案与占位符:岗位名称、职位、求职意向、项目角色、考察重点、面试官人设、全局搜索、采集关键词、面经标签等示例全部换成跨行业写法(如"市场营销专员""客户沟通、数据透视表");模拟面试的默认类型从「技术面」改为「综合面」(技术面只是八种类型之一,把它当默认等于让非技术岗每次先改一次);润色风格「简洁技术风」改为「简洁干练风」。
- 确实会误导非技术岗的措辞:项目里的「技术栈」标签统一改为「技术栈/工具/方法」(个人资料表单、导出 HTML/PDF/Word/Markdown、导出前完整性检查),导出用的标签词已同步进
profile_parser的项目字段识别正则,避免"导出→再导入"时该项目字段识别不出来;简历生成/润色/追问/面试报告等提示词里把「技术难点」「技术/专业能力」「技术选型」「不虚构项目、技术、职责」统一改为「专业难点」「专业能力」「方案选型」「技能」,面试报告的评分维度名随之变成「专业能力」。 - 技能词典补齐非技术词条:
data/skills.json的「跨行业专业能力」补市场营销 / 用户运营 / 内容运营 / 活动策划 / 人力资源 / 法务合规 / 平面设计 / 跨境电商等,「办公与业务工具」补 WPS / Visio / SPSS / 金蝶 / 钉钉 / Photoshop 等,「证书与资质」补初级与中级会计职称 / CFA / PMP / 人力资源管理师 / 证券与银行从业资格等,「通用能力」补数据分析 / 公文写作 / 汇报演示 / 沟通协调 / 流程优化等(原先这一栏只有数据结构、算法、操作系统这类技术课程,名不副实)。知识库与面经的默认分类保持不变。 - 仍然存在的限制(不假装已解决):JD 技能提取依然以技术词为主,技术岗 JD 能提取到的关键词密度高于非技术岗;这是词典规模问题而不是逻辑问题,只能靠继续补词条缓解。
-
README 重写为"项目主页":新增 17 张真实运行截图,并补齐定位、隐私边界与贡献入口。此前 README 是一份功能清单,能查但不像一个开源项目的主页——没有截图、没有目录、没有"为什么做这个"、也没有把隐私边界讲清楚,而这三件事恰恰是访客判断"要不要用 / 敢不敢用"的依据。
- 截图来自真实运行的应用,数据是虚构的,而且这份数据是可复现的。新增
scripts/seed_demo_data.py:生成一份纯虚构的演示数据集(人物统一是「张示例 / 示例科技有限公司 / example.com」),并支持--check把 58 个只读接口挨个读一遍。截图覆盖首页、岗位广场、简历中心与预览、A4 导出效果、投递台、求职进度、求职统计、模拟面试、求职助手、资料箱、知识库、工作台、设置、我的资料与使用指南,放在docs/images/。A4 那张是服务端真正渲染出来的导出 HTML(与用户点「导出 HTML」拿到的是同一份),不是示意图。演示数据用隔离的DATABASE_URL单独启动后端,不碰backend/data/resume_forge.db。 --check是有来历的:第一版演示数据就因为job.keywords写成了字符串数组(实际是SkillTag列表)、claim_record.interview_details写成了字符串列表(实际是{decisions, difficulties, verification, result}字典)、chat_message.status用了不存在的done(实际是complete)而让 5 个接口 500——这类错误在真实用户那里表现为"某个页面打不开",所以写数据的人必须自己先撞一次墙。backend/tests/test_seed_demo_data.py进一步把两件事钉住:脚本能跑通,以及每个人名、公司、学校都带虚构标记(手机号固定0000、邮箱走example.com保留域、岗位链接只允许/job_detail/demoN.html)——截图会出现在公开的仓库首页上,一次误填真实资料就是一次公开泄露。- README 结构与内容:新增徽章组(CI / Release / 许可证 / Python / Node / 依赖 / 测试规模)、目录、定位说明("把事实从表述里拆出来单独管")、截图长廊、隐私与边界章节(默认不出本机、哪些动作才会发请求、不保存招聘网站凭据、默认只监听回环)、路线图、文档索引与贡献入口;功能特性表、快速开始、更新与 FAQ 的既有内容整体保留,只重新编排并补上遗漏(如"不配模型也能用"——岗位解析、匹配度五类结论、版式渲染与全部导出格式都是本地规则)。
- 功能特性表是防呆闸门,表结构不能动。
test_assistant_knowledge_audit.py会从## ✨ 功能特性段落里提取每一行的第一列,与README_FEATURE_KEYS双向比对;所以这一节只能有那一张表,重写时新增的截图与目录都必须放在别的段落里,否则会被当成新的功能行(如| 📸 界面一览 |)而让守卫变红。截图流程与这条约束都已写进AGENTS.md。
- 截图来自真实运行的应用,数据是虚构的,而且这份数据是可复现的。新增