# 变更记录 所有值得关注的项目变化都会记录在此。格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),版本号遵循 [Semantic Versioning](https://semver.org/lang/zh-CN/)。 ## Unreleased ## 2.1.0 - 2026-11-10 ### Added - **实施批次(2026-11-10):问题清单 24 条落地(1–13、15–24 共 23 条实施,第 14 条仅存档)。** 无新增依赖;数据库迁移两条(`0009`、`0010`,head `0010_external_resume`);真实数据集与模型链路均做过实机验证。分域摘要: - **语义检索(第 1、16 条)**:远程服务由「单份扁平配置 + 6 张厂商卡」改为**命名配置列表**(新建/编辑/复制/删除、重名 409、上限 20),存储走 `data/global_settings.json` 顶层 `embedding_profiles`(旧 `embedding` 段保留为当前生效镜像,**未升 GLOBAL_SETTINGS_VERSION**);支持**本地 ↔ 远程一键切换**(ready 态切换只改 `active_profile_id`,失败不污染当前生效项);空密钥只沿用同一条 profile、绝不跨 profile 继承;「清除密钥」语义收窄为只清当前生效项并保留命名配置。API 新增 `POST /api/settings/embedding/profiles*`,`GET /status` 增加 `profiles` / `active_profile_id` / `local_profile_id`(向后兼容新增)。 - **语音转写(第 2 条)**:改为**命名配置列表 + 使用/复制/删除**,「使用」不探测;配置从「每个数据集一份」改为**全局一份**(`global_settings.json` 顶层 `asr_profiles`),首次打开设置页时把当前数据集的旧 `asr_config` 幂等迁移成一条「默认配置」;旧配置行保留作回滚面包屑(其它数据集旧配置留在原库但不再生效)。新增 6 个 `/api/settings/asr/profiles*` 接口,旧接口保留。**安全修复**:备份勾选「不包含 API Key」时,此前仍会把 `app_setting.asr_api_key` 与 `app_setting.search_deepseek_api_key` 带进包(macOS/Linux 上是明文);现已收集并剥离,并补反证测试。 - **联网搜索(第 3、4、5 条)**:搜索来源/地址/Key 改为**全局一份配置**(`global_settings.json` 顶层 `web_search`,旧 `app_setting.search_config` 懒迁移一次、旧行保留不删、不随数据集备份恢复);来源卡网格改 `alignItems: start`、卡片去掉 `height:100%`,DeepSeek 计费/隐私提示移到网格下方全宽常驻 Alert(卡内不再留白);「每次最多返回的结果条数」上限 **15 → 50**、语义改为「最多返回」,`web_search` 助手工具新增可选 `limit`(模型可指定更少),「通用参数」由多选改单选 Radio,提示写明「实际条数受来源限制(Bing 最多 8 条)」。 - **示例数据(第 6 条)**:欢迎向导「载入示例数据体验」改为载入**随包发布的完整演示数据集**(内置 `backend/app/assets/sample/sample_dataset.zip`,由 `scripts/seed_test_dataset.py` 生成并 `--check` 自检):14 岗位 / 6 简历 / 52 资料 / 8 资料箱 / 8 知识库 / 4 面经 / 10 台账 / 12 进度 / 60 网申资料 / 10 附件;再次点击复用同一份(`reused:true`),旧示例保留不删、主数据零字节影响。 - **超额页预览(第 7 条)**:简历草稿预览超出「最大篇幅」的页现在**照常显示正文**并保留红框与「超出上限,不计入导出」标注(导出请求新增 `include_overflow_pages`,仅 `preview=true && format=html` 跳过截断,`render_config_fingerprint` 同批计入防缓存串号);普通导出 / PDF / Word / HTML 仍按上限截断。 - **面试设定(第 8 条)**:「面试官设定」由网格改纵向堆叠 + 字段 4 等列(宽屏填满整行、`@container` 768px 两列、440px 单列),右侧不再出现空列;轮数说明移出格子放字段下方全宽,点「指定轮数」只增高输入框那一档;数据/校验/提交协议未变。 - **并发生成(第 9 条)**:支持后台同时生成多份简历——API 由「任何活动任务都 409」改为**同岗位去重**;Runner 改为「管理器 + 每任务 worker」,`RESUME_GENERATE_MAX_CONCURRENT`(1–8,默认 4)限制同时运行、`RESUME_GENERATE_MAX_PENDING`(默认 50)超出排队、有空位自动开始、排满才 429;取消只影响选中的那一份;进度提交粒度 200 → 400 字缓解 SQLite 单写争用;前端弹窗关闭即回配置态(后台继续),任务条支持多条滚动展示。 - **思考参数(第 10、11 条)**:修复 Responses 链路「设置页写着思考关闭、实际一直在思考」——关闭时显式发 `reasoning: {"effort": "none"}`(DeepSeek `/responses` 实测唯一有效写法;`thinking:{type:disabled}` 在该端点无效);实测同一岗位生成 **148.6s → 27.5s(−81.5%)、输出 tokens 25,149 → 6,118(−75.7%)**。思考规则表拆为 `thinking_vendor_table / thinking_vendors / thinking_vendor_rules_{cn,global}` 四模块(30 条规则覆盖 22 个预设、按 chat/responses 区分关闭形态);强制思考型号(GPT-6 Astra / GPT-6.1 Sol / GLM-5.3 / Kimi K3 / MiniMax M3.1-Flash)一个关闭参数都不发、设置页置灰并说明原因;前端协议矩阵 4 处修正(豆包/xAI 的 Anthropic 入口、Spark/MiniMax 域名)+ 预设 `config.ts` 同步,表尾补思考开关说明。 - **首页与求职进度(第 12.1、12.2、13 条)**:首页任务卡 `.home-tasks-card` 标题留白与副标题下留白相等(不再贴顶);求职进度页顶部统计改为**指标带 + 一条 100% 堆叠构成条**(五段按「进行中总数」`flex:{count} 0 0` 切分、图例最大余数法百分比合计 100、0 段不画、图例可点筛选),记录列表**去卡片化**(容器 + 分隔线行、`.is-active` 只留一条竖条、状态改圆点、元信息压一行、每 10 行细分隔),`TrackerPage.tsx` 467 → 377 行。 - **采集筛选(第 15 条)**:投递台「自动采集」筛选项按站点真实能力区分——多选 `experience/degree/industry/scale/payType/partTime/stage`、单选 `jobType/salary`;兼职(`jobType=1903`)时「薪资待遇」整组消失、换结算方式 + 兼职时间(反向不成立);`GROUP_SPECS` 新增 `multi` / `visible_when` 由后端随选项下发,前端不写死;切换求职类型时前后端都清残留值。 - **求职统计(第 17 条)**:时间线空态改「还没有状态变化记录,改一次状态就会开始累积」(**不伪造事件**);补「进行中不含 Offer」说明;区间选择器归位到趋势图卡头;新增渠道转化率(招聘平台 7 / 内推 2 / 官网 1 / 猎头 1 / 其他 1)、投递→当前阶段耗时中位数、同公司多投与止损清单、简历版本效果(带样本量/小样本徽标、不做归因)、周投递目标(`PUT /api/analytics/weekly-goal`)、生成成本(只报 token 与耗时,不报金额);停滞口径改为「最后一次状态变化(事件 > status_date > updated_at)」,`/api/stats` 与首页提示同批对齐。 - **桌面版(第 18、19 条)**:窗口 × 改为「最小化到托盘 / 退出应用」二选一,可勾「记住我的选择」(存 `userData/close-behavior.json`,删掉恢复询问;记住退出有二次确认;托盘提供「关闭窗口时询问」);桌面版隐藏页内「退出」(浏览器发行版保留);新增**自绘 32px 标题栏** + 文件/编辑/视图/窗口/帮助/关于六菜单(无字母跳转;Alt→方向键→Enter 键盘可达;菜单/下拉全部 `no-drag`;Win11 贴靠与双击最大化;深浅色覆盖层按钮颜色跟随;净增 0px)。 - **事实台账(第 20 条)**:`claim_record` 新增 `entry_ids`(JSON,存 24 位 `entry_id`)与 `entry_ids_auto`(迁移 `0009`),一次性保守回填(按主体名去空白双向包含;技能/语言分区不参与;学历同校用专业名消歧);生成判据改为 `entry_ids ∩ 快照` 或 `sources[].location` 命中;表单新增「关联资料条目」多选、「证据来源」只做定位、列表对未关联条目标「不会进入生成」、对自动关联标「已自动关联,请核对」、页面读数拆「全量 M 条已确认 / 其中 K 条已关联资料」;提示词把「基线没提到的一律不得出现」限定到台账覆盖的条目并渲染覆盖清单,`blocked_wording` 独立于基线渲染;「从资料生成」支持从资料库选条目预填。真实数据集 **10/10 已确认全部回填成功**,页面口径 == 生成口径。 - **外部简历(第 21 条)**:简历中心新增「外部简历」区块(只读快照),新表 `external_resume`(迁移 `0010`,引用 `user_file` + 提取文本 + 软删);上传 pdf/docx/txt/md 时本地提取文本(扫描件不识别视觉、返回 warning 并标注「未提取到文字」);提供原件预览、文本、条目化文本检查(占位符/量化/联系方式/长度/弱表述/有 JD 才出关键词覆盖,**无总分**)、软删进回收站可恢复(不删物理文件)、「用于面试」跳转并预填面试材料;与 `resume_record` 完全隔离,不进制作台/导出。备份表清单 76 → 77、回收站 23 → 24 类(含外部简历)。 - **资料箱附件(第 23+24 条)**:附件支持图片 / PDF / Word(.docx) / txt / md / json / csv 上传并**保存原件**(`material.files` 增 `file_id`,引用 `user_file`),新增 `POST /api/materials/attachments`;附件行提供「预览 / 复制 / 另存为」(图片复制写真实剪贴板 `image/png`、PDF 复制文本、另存为下载原件);历史无原件附件只显示「复制」;扫描件 PDF 不识别只给 warning;「添加图片 / 添加文本文件」两个按钮合并为唯一「添加附件」(`Upload` 与 `FileDropZone` 共用同一 `accept` 常量),`.doc` 选文件即拒并提示另存为 `.docx`,多选时上限检查按串行队列累加;删除「PDF/Word 请把文字粘贴到正文,程序不保存原始文件」等已失效文案。 ### Changed - **第二轮复核与补完(2026-11-10)的口径与交互修正:** 资料箱附件上传的请求体上限**单独放宽到 12 MB**(新增配置 `MAX_ATTACHMENT_UPLOAD_MB`,默认 12;端点自身仍按 10 MB 校验单个文件)——此前的全局 8 MB 默认会让 8–10 MB 的 PDF/Word 在进入端点前就被中间件以「请求体过大,最大允许 8 MB」拦下,界面写着支持却没真正可用;只放宽 `POST /api/materials/attachments`,其余接口(含同前缀的 `/api/materials`)仍受全局上限。语义检索的命名配置卡不再「点整卡打开编辑弹窗」(卡根去掉 `role="button"` / `tabIndex`),键盘等价入口是「⋮ → 编辑」,鼠标仍可整卡点击。求职助手的两处能力描述纠正为事实:渠道字段存在(`channel_stats` 可答各渠道投递/面试/Offer 数与转化率,与「内推」模块是两套口径、不可相加)、进度记录可显式关联简历(`resume_effects` 带样本量),不再回答「本库没有渠道字段」「关联入口还没有」。README 功能表补「外部简历」「附件多格式」两行(中英各 82 项)并登记进助手能力地图。 - **实施批次(2026-11-10)的行为变化(需要用户知道的口径变更):** 语义检索/语音转写/联网搜索三类设置由「每个数据集一份」改为**全局一份**(切数据集不再变化;旧数据集配置迁移一次,回退旧版只能看到升级前配置);联网搜索「每次返回条数」上限 15 → 50、分类由多选改单选;求职进度「停滞」口径改为最后一次状态变化;简历生成同时运行默认 4 条、同岗位去重;采集筛选接口 `GET /api/collect/filters` 的分组新增 `multi` / `visible_when` 字段,`PUT /api/collect/config` 与 `POST /api/collect/filters/test` 的 `filters` 由 `Record` 改为 `Record`(旧单字符串仍可读、自动归一;求职类型/薪资待遇单选,其余多选,兼职联动换组);资料箱附件图片仍最多 4 张(文档 4 份,多格式共用上限);备份包新增 `external_resume` 表(表清单 77),回收站 24 类。 ### Fixed - **第二轮复核与补完(2026-11-10)修复的无障碍问题(都会让 `ui-quality` / `a11y` 冒烟在真实数据下变红):**首页提醒弹窗的链接色字面量 `#5b8ff9` 在白底只有 **3.11:1** → 改用 `--rf-info-strong`(7.26:1),弹窗另外三处硬编码色(标题图标、分隔线、hover)一并换成语义 token;岗位卡技能标签的 antd cyan `#08979c` 在 `#e6fffb` 底上只有 **3.39:1** → 复用 `--rf-info-strong`(6.94:1);求职进度构成条的 `aria-label` 含「条,占」导致可见文本不在可访问名内(WCAG 2.5.3)→ 改为可见文本充当名称、完整口语版放 `title`;语义检索命名配置卡的 `role="button"` 套着三个按钮(`nested-interactive`)→ 卡根降级为普通容器。 - **实施批次(2026-11-10)修复的问题(对应问题清单):** Responses 链路思考未真正关闭导致生成慢一倍(第 10 条);事实台账「已确认事实基线」在真实生成中恒为空(第 20 条);草稿预览超额页只有红框没有正文(第 7 条);搜索来源卡勾选后卡内大片留白(第 4 条);资料箱卡片同行高矮不齐(第 22 条,同行底边差 22.39px → 0);首页任务卡标题贴顶(第 12.1 条);面试设定右侧空列与「指定轮数」撑高整行(第 8 条);桌面版「页内退出 + × 直接退出」双入口冲突(第 18 条);事实台账关联判据与界面提示永远对不上(第 20 条);思考参数对不可关闭型号会发出去导致 400(第 11 条)。 ### Security - **备份密钥剥离补齐(2026-11-10):** 「不包含 API Key」导出此前仍会带出 `app_setting.asr_api_key` 与 `app_setting.search_deepseek_api_key`(非 Windows 平台为明文);现已加入收集与剥离名单,并补反证测试(导出包内搜不到明文、`include_api_keys=true` 时保留)。 - **求职 Agent 三步落地:跟进巡检 ·「我已提交」+ 投递交付单 · 求职偏好档(2026-11,迁移 `0006`–`0008`)。** 把「投出去之后」这一段接上,全部本地运行、不新增账号/云依赖,**不自动提交、不自动发消息、不读邮箱、不保存招聘网站凭据**。 - **跟进巡检(期一)**:复用「任务卡」总开关与同一后台节拍,扫描「进行中且超过 7 天没有任何更新」的投递——>7 天且 ≤21 天生成**跟进清单卡**,>21 天生成**确认状态卡**(两桶互斥);每桶最多 5 家、按最久未更新优先,同一桶同一 ISO 周只生成一张卡(`agent_task` 新增 `source_ref` / `occurrence_key` + 唯一索引,旧提醒卡去重语义不变);「投出后 7 天」提醒 7 天内出过卡的让位,近期已决策(`contacted`/`replied`)与 `stopped`(永久)不再入选;文案只由确定性模板产出、不调模型,禁词 + 必备要素双向守卫。新增 `POST /api/followups/decisions`(整卡一事务、乱序可提交、幂等;**只记录决策,不改投递记录的状态与更新时间**)与新表 `application_follow_up`。 - **「我已提交」+ 交付单(期一)**:`POST /api/apply/records/{id}/submitted` 把成功的投递记录补成一条「已投递」进度(合并键去重、已到更后阶段不降级)、挂一条 +7 天的 `hr_reply` 提醒(同 track 待办提醒复用,幂等)、冻结投递快照(复用既有 `freeze_delivery` 与幂等键 `apply_task_item:{id}`),并维护一次性引导键;`apply_task_item` 新增 `receipt_acked_at`(迁移 `0007`)。`GET /api/apply/records/{id}/receipt` 把既有快照读成可验收单据:投递版本/目的地/导出产物/填充字段统计/事实台账疑点/待人工步骤/跟进计划,三态优先级 **卡在人手上 > 有疑点 > 可验收**;「投出后简历又改过」只展示、不参与三态;填充记录按 URL 精确匹配、匹配不上标「未知」而不是缺件。`POST .../receipt/ack` 只写验收时间(幂等、不碰漏斗状态)。 - **求职偏好档(期二,迁移 `0008`)**:新表 `job_preference` 存 13 类闭集偏好(城市 / 行业与公司排除 / 薪资底线 / 工作方式 / 用工形式(正职 / 实习 / 外包 / 兼职 / 劳务派遣)/ 写作红线 / 可到岗时间 / 仅本次对话等),一行 = 一次确认的完整取值;版本状态 `state`(active/superseded/undone)与 `deleted_at`(只表示用户删除)分离,活动行唯一用条件索引 `(dataset_key, kind) WHERE state='active' AND deleted_at IS NULL`。写入必须经用户确认的提案(`create_job_preference` / `update_job_preference` 登记为永远走提案的写工具;绕过提案直写会被服务层拒绝);「以后都这样」落长期偏好,「只在这次对话」落 `session_note`。偏好块以「不是发生过的事实」的显式格式注入系统提示(24 条 / 单值 120 字符 / 列表 8 项 / 整块 900 字符,先省来源时间再按优先级裁尾;四档数据权限全注入),**永不进入简历生成、纸面渲染、网申取数与事实台账**(有隔离反证测试)。设置页新增偏好管理:编辑 / 撤销到上一版 / 删除进回收站 / 恢复;恢复撞同 kind 活动行返回 409 `trash_restore_conflict`。 - **回收站 22 → 23 类**(新增「求职偏好」):`TRASH_SPECS` 仍为唯一注册表(拆到 `services/trash_registry.py` 以守住 500 行预算,`trash.py` 原样重导出全部符号);能力地图、README 与用户指南同步为 23 类。 - **接口与迁移**:新增 `POST /api/followups/decisions`、`POST /api/apply/records/{id}/submitted`、`GET /api/apply/records/{id}/receipt`、`POST /api/apply/records/{id}/receipt/ack`、`GET/POST/PATCH/DELETE /api/job-preferences[/{pref_id}]`、`POST /api/job-preferences/{pref_id}/undo`、`POST /api/job-preferences/proposals/{id}/session`;迁移链保持单线 `0005 → 0006 → 0007 → 0008`,旧数据零破坏(`agent_task` 旧行 NULL 不受新唯一约束影响、`receipt_acked_at` 旧行 NULL = 未验收、`job_preference` 空表起步不回填)。 ### Changed - **可用性收口 8 项:列表行「···」、设置页三张 AI 卡对齐、面试轮数与人设互斥、更新公告渲染、清理文件定位(2026-11-09)。** 无数据库迁移、无新增依赖;旧配置与旧数据零破坏。 - **列表行操作**:岗位广场与简历中心每行最右侧新增「···」,内容与该行右键菜单一致(不含已平铺的「详情/生成简历」「预览/导出」,也不含右键独有的「复制」子菜单)。**进入选择模式(勾选)后,三点与所有单行操作一并禁用**(含表格收藏星标、窄档卡片的预览、星标与标题),批量勾选框保持可用;窄档卡片补上「批量选择」入口(此前 ≤1092 宽度下进不了选择模式)。岗位广场操作列 170 → 190、`scroll.x` 1330 → 1350。 - **设置页三张卡向「大模型 API 配置」看齐**(LLM 卡与其弹窗一行未改):卡头统一「标题 + 唯一主操作」、说明统一 `Alert info`、字段统一 `SettingsField`、状态统一 `Tag`(蓝=生效中 / 绿=已配置 / 灰=未配置)、测试结果统一 `Alert`。语义检索远程服务由下拉改为厂商卡片网格(6 家,选中态浅色底 + 主色边框,下方展开字段);语音转写新增 OpenAI / Groq / 硅基流动 / 阿里云百炼 / 自定义预设;联网搜索改为 4 张可多选来源卡 + 下方「通用参数」,**保留 400ms 自动保存与三态提示**;语义检索三处裸的红按钮(删除已下载文件 / 删除本地模型文件 / 清除已保存的密钥)收进「···」并沿用列表同款二次确认。 - **语音转写新增调用协议**:`AsrSettings.protocol`(`openai` | `dashscope`,默认 `openai`),配置仍存 `AppSetting.asr_config` JSON,**无需迁移**;`dashscope` 走 `POST {base}/chat/completions` + `messages[0].content=[{type:"input_audio",...}]`(Base64 Data URI、`stream:false`、可选 `asr_options.language`),响应兼容字符串与分段数组,400 时提取上游 `error.message`。新增 `POST /api/settings/asr/models`(`GET {base}/models`,上限 100,失败返回 `[]`),前端「拉取模型列表」按 `whisper|transcribe|asr|audio|speech|sensevoice` 分组、默认只显示疑似转写模型。**旧配置无 `protocol` 时按 `openai` 处理,行为不变。** - **面试设定**:轮数改为「自动 / 指定轮数」两档(**默认自动**,此前默认固定 6 轮),指定时手输 3–12;面试官风格与自定义人设合并为同一分组并纵向排列,**填了人设即禁用风格**且 `extra` 明示原因(值不清空、后端 Literal 仍必填)。 - **行为变化(进行中的会话也生效)**:后端提示词从「风格 + 人设叠加」改为**人设优先**——`persona` 非空时 `interviewer_style` 传入占位文案、`behavior_directives_for(..., None)` 跳过风格行为指令,收尾提示词同样不注入具体风格名。老会话下一轮即走新逻辑。 - **更新公告渲染**:补齐 `#{1,4}` 标题、表格(首行进 `thead`、列数不齐补空)、`---/***/___` 分隔线、行内代码、`* ` 与 `1. ` 列表、`[文字](地址)` 链接(**只放行 http/https**,其余降级为纯文字);全程 React 文本节点,不引入 `dangerouslySetInnerHTML` 与 markdown 依赖。容器 `maxHeight` 200 → `min(40vh, 420px)`(1440×900 下实测 360px),保持内部滚动。后端公告截断由「硬切 4000 字符」改为**按行回退**(预算 3986,回退点不早于 60%,末尾追加「……(完整说明见发布页)」),不再把表格或句子从中间切断。 - **清理旧文件可定位**:新增 `POST /api/stats/storage/cleanup-reveal`,请求体为清单里的**相对路径**(不下发绝对路径);拒绝绝对路径与 `..`、`resolve()` 后必须仍在数据目录内、首段白名单 `backups`/`auto-backups`/`logs`、文件不存在返回 404。前端文件行末尾加「打开文件夹」图标按钮(`preventDefault` + `stopPropagation`,点它不会切换勾选)。`reveal_path()` 抽到 `services/system_reveal.py`,与「我的文件」的行内定位共用一份实现(Windows `explorer /select,`、macOS `open -R`、Linux `xdg-open` 目录)。 - **设置页「各服务商协议支持」按 2026-11-09 官方文档复核并重排(热门优先)。** 修正 Qwen / StepFun 的 Responses 判定为部分支持(前者仅基础兼容、后者仅两款模型),xAI 的 Anthropic 判定改为部分支持(官方 OpenAPI 已定义 `/v1/messages`);Perplexity 的 Anthropic 改为部分支持(Orchestrator 私有预览,需申请)。Groq 预设模型改为 `openai/gpt-oss-120b`(`llama-3.3-70b-versatile` 已于 2026-08-16 对自助层关停),Perplexity 改为 `perplexity/sonar`。23 行按热门度重排,表尾仍标核对日期。 - **助手提示词预算守卫阈值显式调整(2026-11)**:`test_assistant_prompt_budget` 的系统提示固定成本上限 23,800 → 24,100。原因是工作树里**既有**(非本批)的助手装配 / 平台事实改动已把基线推到约 23,824(本批之前即已超 23,800 共 24 字符),本批偏好能力点名再 +174 字符(`test_assistant_prompt_surface` 要求每个写工具名出现在静态提示里);真正的成本闸门 `MAX_PROMPT_TOKENS_ESTIMATE = 60,000` 未动,测试注释记录了实测数字与"既有基线"归属。 - **新增 RAG 语义补召回(默认关闭;v2 检索链路 + v3 真实本地 / 远程 provider)。** 给简历生成链加一条**候选选择层的可选增强**,两个开关**默认都是 `false`**:`rag_retrieval_enabled`(向量补召回 + 加权融合)与 `rag_query_expansion_enabled`(Phase 0 查询扩展)。**都关闭时(默认)** `prepare_selection_boost()` 返回空作用域,生成链行为与改造前逐字节一致——RAG 只决定「哪些条目进入候选」,**不碰 prompt 字节**(候选的字段形状、键序与文本不变)。**不新增数据库表 / 迁移 / 数据目录**:向量索引是进程内 LRU 缓存(可重建派生数据,重启重算、不进备份,备份导入后没有索引也能正常生成)。 - **链路**:冻结的 `ConsumedProfile` → 模型输入白名单投影出的 chunk(`canonical_item_hash` 桥接回下游候选字典,下游候选里已经没有 `entry_id`)→ 进程内向量索引 → 关键词分 + `10 × cosine` 加权融合(向量权值 10 < 技能命中权重 12,保留精确命中的硬优先级;融合分先 `round(4)` 再比较、平局回落原顺序)→ `rag_seam.selection_scope` 作用域 → `_select_entries()` 的 boost / `build_job_focus()` 的扩展词。boost 只对教育 / 工作 / 校园 / 项目四个内置分区与 `filter_by_job=True` 的自建板块改变顺序;`skills` 走独立硬优先级路径,`awards` 与其它分区不受影响。SSE 与后台任务两个生成入口都已接入。 - **Phase 0 查询扩展**复用已配置的大模型与岗位已有的 `analysis_cache` 列做缓存(岗位内容一改指纹即失效,不需要第二张缓存表),扩展词**只进 `JobFocus.terms`、不进 `skills`**;未配置模型 / 超时 / 坏 JSON / 空结果一律静默回退。 - **fake provider 守卫**:`FakeEmbeddingProvider` 是确定性 hash 向量、与语义无关;默认配置下即使打开 `RAG_RETRIEVAL_ENABLED` 也只回退(`fallback=fake_provider`),只有显式 `RAG_EMBEDDING_ALLOW_FAKE=true`(测试/链路对比)才参与排序。 - **请求预算与后台预热**:单次请求最多花 `RAG_REQUEST_BUDGET_SECONDS`(默认 1.5s)算 boost;冷缓存超预算立即回退(`fallback=budget_exceeded`),缺失向量由 `ai_task` 的 `rag_index_warmup` kind 在后台补齐(复用现成执行器,不阻塞首字节)。作用域只覆盖**候选构造段**(以 `mark_selection_done()` 为界),不再覆盖整条流。 - **真实 provider(v3)**:设置页「AI 模型 → 语义检索」二选一——**本地模型**(按需下载约 110–126MB(按平台 zip 传输体积:Windows 111MB / macOS 110MB / Linux 122–126MB;打包模型约 74.6MB + 运行时按平台约 35–51MB,解压后合计约 237–280MB;设置页显示本机实际值);按平台选资产、每个下载源各自 sha256 校验、目录带版本;数据不出本机)与**远程服务**(任意 OpenAI 兼容 `/embeddings`,0 下载;首次按 host 弹隐私确认、维度自动探测、按 `batch_size=25` 切片;内置百炼 / OpenAI / 智谱 / SiliconFlow / 本机 Ollama 预设与自定义端点模板)。**在设置页启用任一真实 provider 即等于启用语义检索**(无需改环境变量;`RAG_RETRIEVAL_ENABLED` 保留为开发 / 评测总闸)。配置持久化在 `global_settings.json` 的 `embedding` 段(不进备份),切换 provider / 模型 / 维度会清空索引并记 `provider_switched`;请求体只含 chunk 的白名单投影,姓名 / 电话 / 邮箱 / 私密链接不出现。 - **回退语义**:未配置 provider / fake 未允许 / 超预算 / 超时 / 坏响应 / 空索引 / 本地下载或校验失败 / 运行时不可用 / 远程 401·429·维度不符 → 一律空作用域并回退关键词链;设置页如实显示失败原因,**不伪装成已启用**。日志只记 provider、host、候选数、命中来源、耗时、回退原因、哈希前缀 7 类元数据,**不含 JD / 简历正文、向量、密钥或完整 URL**。 - **配置项**:v2 的开关与模型属性继续可用(`RAG_RETRIEVAL_ENABLED=false`、`RAG_QUERY_EXPANSION_ENABLED=false`、`RAG_EMBEDDING_PROVIDER=fake`、`RAG_EMBEDDING_*` 等,见 `backend/app/config.py`);v3 新增 `RAG_ASSET_BASE_URL`(默认 `https://github.com/magicapple123/ResumeForge/releases/download/rag-assets-v1`)、`RAG_ASSET_HF_BASE_URL`(HuggingFace 备用源)、`RAG_LOCAL_MODEL_DIRNAME=rag-model`、`RAG_LOCAL_RUNTIME_DIRNAME=rag-runtime`、`RAG_EMBEDDING_ALLOW_PRIVATE_ENDPOINT=false`(本机 Ollama / LM Studio 等回环 / 内网地址默认拒绝,需在「高级」显式允许)。 - **平台矩阵与体积**:本地模型统一 onnxruntime **1.31.1**,支持 Windows x64 / macOS **14+** arm64 / Linux x86_64 / Linux aarch64;**Intel Mac 没有 onnxruntime macOS x86_64 wheel,本地模型不可用,只能用远程**(应用本身仍可运行,dmg 仍出 x64 / arm64)。权重与运行时按需下载,**不进安装包、发布压缩包与备份**(登记为 `RUNTIME_ONLY`);只用远程的用户 0 下载。因此备份包与安装包体积不变;`global_settings.json` 不在备份成员里,provider 配置与加密密钥不随备份走。 - **未验证项(如实记录)**:真实远程端点**未做端到端验证**——本机没有 `EMBEDDING_API_KEY`,只有 mock transport 的协议证据,报告给出将来验证命令;**ARM 平台(macOS arm64 / Linux aarch64)的本地量化变体未真机验证**(若只能退到 fp32 必须回给用户决策);Qwen3-Embedding / harrier 等其它模型未下载未验证;`FakeEmbeddingProvider` 不得用于质量指标。 - **已知边界**:接缝用请求内 ContextVar 作用域而不是给 `build_targeted_consumed_profile_context()` 加透传参数(两个候选接入文件已贴 500 行硬闸门且被 `monkeypatch` 钉死模块命名空间,详见交付报告);本机 / 内网地址需显式允许;不兼容厂商(DeepSeek / Groq / OpenRouter / Together / xAI 等)没有 `/embeddings`,界面给可读提示而不是裸 404(名单有时效性,不写成不可配置的白名单)。 - **离线评测**:`scripts/rag_eval_dataset.py` 生成 60 组「同义改写型 JD × 种子条目」评测集;`scripts/rag_eval_run.py --mode keyword|expanded` 量 baseline / Phase 0,`scripts/rag_eval_embedding_spike.py` 用隔离 venv 的真实 ONNX 量 Phase 3(v2 实测 Recall@10 0.4056 → 0.6361);v3 接入产品路径后的本地 / 远程 60 组对照、平台矩阵与失败矩阵数据见交付报告。 - **新增 Linux 发行版(x86_64 / aarch64)。** 三种形态并存:`start.sh` / `stop.sh` / `update.sh` 一键脚本(零依赖,首次启动自备便携运行时)、`ResumeForge-<版本>-linux.zip` 便携包、 `ResumeForge-<版本>-linux-.AppImage` 单文件。验收基线 **glibc ≥ 2.31** (Ubuntu 22.04+ / Debian 12+ / Fedora 38+ 一类),更老的发行版与其它架构未经验证。 - **便携 Python 换成 `install_only_stripped`**:默认的 `install_only` 包 106MB,精简包 x86_64 34MB / aarch64 29MB,与 macOS 的 25MB 同一量级。摘要钉在仓库里,镜像只能加速。 Linux 上运行期**不需要 Node**——发布包自带 `frontend/dist`,页面由后端同源提供。 - **多一道 venv 预检**:Debian/Ubuntu 把 `venv` 与 `ensurepip` 拆进了单独的 `python3-venv` 包,系统里有 `python3` 也照样建不出虚拟环境,而报错指向 `ensurepip` 完全不解释原因。 启动器会实测这两者可用性,缺了先给 `sudo apt install python3-venv` 提示,再回落便携版。 - **修复退出时在 Linux 上跑错停止脚本**:`_stop_command()` 此前把所有非 Windows 平台一律 指向 `scripts/macos/stop.sh`,而那个脚本把状态目录写死成 `~/Library/Application Support`, 于是 Linux 上「点退出」会去一个不存在的路径找记录、静默地什么都不停。现在按 `sys.platform` 分流到 `scripts/linux/stop.sh`,并让「脚本存在性守卫」与「实际执行」 从同一个函数取路径(此前两处各写一份 `if`,守卫看 A、执行 B)。 - **修复 Electron 壳在 Linux 上调用 macOS 启动器**:`backend-manager.js` 的 `launcherCommand()` 同样只判了 `win32`,其余平台一律走 `scripts/macos/start.sh`; 现在按平台在 `macos` / `linux` 之间选择。 - **PDF 中文字体补上兜底**:`_FONT_CANDIDATES` 里的 Linux 条目(Noto CJK / 文泉驿 / arphic)没有任何主流发行版默认安装,此前 Linux 上探测必然返回空、导出直接 409。 现在系统字体缺失时回落到**已下载的内置思源黑体**(`resume_font_builtin` 按需下载的 那两个字重);两边都没有时仍如实返回 409 并保留浏览器打印,界面给出下载入口。 兜底**排在系统字体之后**,Windows/macOS 上「用系统字体」的语义不变。 - **测试隔离补齐**:`conftest.py` 此前只把 `user_fonts.DATA_DIR` 指到测试目录,没有隔离 `resume_font_builtin.DATA_DIR`。字体兜底上线后,开发机上真下载过 33MB 字体就会让 「缺字体」类断言随机器状态漂移,所以同批隔离并纳入每个用例的清空夹具。 - **`update.sh` 支持「下载新包 → 校验 `.sha256` → 只覆盖程序文件」**:`backend/data/`、 `backend/.env`、`runtime/`、`desktop/` 一律不动;覆盖前把将被替换的内容复制到 `runtime/backup-<时间戳>/`(本机没有回收站,禁止硬删),失败可回滚;`--check` 只检查。 - **新增 CI 三道 Linux 把关**:`linux-launcher-guards`(跑守卫测试,并断言输出里确有 `ok` 行,防止脚本空转而"绿得没有内容")、`linux-runtimes`(真下载两架构运行时、验 sha256 与字节数,并实际验证 `venv` + `ensurepip` 可用)、`linux-end-to-end`(干净检出 走完整启动链、浏览器冒烟、只读安装根状态外落、停止脚本);`release-assets` 新增 `linux-appimage` 作业产出双架构 AppImage 并附 `.sha256`。 - **已知边界**:`scripts/tests/Test-Build-Release.ps1` 的平台清单仍是 `windows|macos`, `-Platform linux` 要等根目录启动脚本进 git(`git archive` 只打包已跟踪文件)之后才能加上; `desktop/main.js` 与 `desktop/backend-manager.js` 现为**恰好 500 行**,再到这两个文件里 加代码必须先外移一段。 - **求职助手输入区重做:四档数据权限 + 只读模型 chip(T11)。** 输入框旁新增四档权限—— **仅查看 / 放宽读取 / 可修改 / 完全权限**,一档同时决定「读」(是否看姓名、电话等真实资料)与「写」 (是否直接改数据)。设置页「求职助手 · 数据权限」存全局默认;输入框下拉只覆盖**当前对话** (本机 localStorage、按会话保存,刷新/切页保留、**换设备不同步**);选放宽读取 / 完全权限时输入框上方 出现可关闭的琥珀色提示,**首次**切到写档位有一次确认。请求体发送解算后的两轴值 (新可选字段 `relaxed_mode`,缺省沿用全局,旧客户端零改动)。输入框发送键左侧新增**只读模型 chip** (显示模型名,悬停「记录名 · 模型名 / 在顶部切换」,无记录不显示、<480px 隐藏;顶栏切模型后跟随更新)。 **安全边界**:完全权限下写操作不再逐条出提案、直接执行;**技能的新建与修改仍永远走确认**。 - **接口:`GET/PUT /api/assistant/permission`。** 读写全局默认档位;写入时同步写旧键 `assistant_relaxed_mode`(兼容其它读取点),旧键不删。 - **设置页新增第三种接口协议 OpenAI Responses(实验性,T09/T09.1)。** 默认仍是 Chat Completions, **已保存配置零影响**——只有用户主动切到 Responses 才走新链路;Claude / Gemini / 硅基流动 不支持该协议 (MiniMax 自 2026-11 起已支持,见下方 Fixed)。切换协议时只有声明支持 Responses 的预设才改 `Base URL` (智谱→`https://open.bigmodel.cn/api/v1`,其余在预设地址后追加 `/responses`;DeepSeek 实测可用)。 设置页「模型配置」区新增可展开的**「各服务商协议支持矩阵」**(23 行,含核对日期列;表尾 「核对日期 2026-11-08」,2026-11-08 逐家复核后重写;2026-11-09 再次复核,见本段 Changed 顶部),并新增 6 家预设(腾讯混元 / 百度千帆 / 讯飞星火 / 阶跃星辰 / Together AI / Perplexity),**预设总数 16 → 22**。同时修正 MiniMax 域名(`api.minimaxi.com/v1` + MiniMax-M3.1-Flash)、 Gemini(`gemini-3.8-flash`)、xAI(`grok-4.7`)等预设,标签缩短为「Claude(Anthropic 原生)」 「Google Gemini」。**没有各厂商的真实 Key,新协议与预设按接口文档实现,未逐家真机验证。** - **助手输入框、模拟面试答题框与面试复盘「追问」支持语音听写(T12/T13)。** 说话→文字先填进输入框, 由用户确认后提交,**不自动发送**;面试追问同样只填入、不触发发送。设置页新增「语音转写」卡 (OpenAI 兼容 `/audio/transcriptions`:base_url / model / Key,Key 掩码回读,可测试连接)。 单段上限:助手与答题框 **3 分钟**、复盘追问 **1 分钟**(音频超过 10 MB 也会被拒);音频会发送到 用户配置的语音服务商,本应用不保存音频。**真实识别质量未验证(无 ASR 服务商 Key,本地假 ASR 服务器只验证了管道与交互)。** - **首页新增「任务」卡,任务卡从助手页搬来(T02/T03)。** 待确认在前、进行中在后,两者合计最多 3 条 (超出收成「还有 N 条」,点一下就地展开);「已结束(N)」默认收起,展开先看最近 5 条 + 「查看全部」; 折叠状态不记忆(D4)。不按会话过滤(提醒到期生成的卡没有会话归属);没有可显示任务时整卡不渲染, 空壳任务(无步骤的草稿 / 已取消)不显示也不计入任何计数;轮询仍为 5s,页面隐藏时暂停。查看步骤改成 手风琴(同一时间只展开一条),展开后第一个未完成的步骤加粗并标「当前」。 - **提醒列表新增逾期红标与「时间范围」筛选(T06)。** 列表接口在原有字段外补 `urgency` / `days_until` (复用 `reminder_service` 的唯一实现,前端不按小时重算);pending 且 `days_until < 0` 的行显示红色 「已逾期 N 天」标签。新增「全部 / 已逾期 / 今天 / 以后」筛选,筛选后回到第 1 页并重算总数。 - **提醒新建弹层 5 处改进(T05)。** 默认时间改为「1 小时后」;新建时校验「提醒时间需要晚于当前时间」 (编辑时放行,只改备注不被旧时间卡住);时间只显示到分钟;时间字段下方按「任务卡」开关状态提示到期 会不会生成卡片;进度 / 岗位 / 简历三个关联字段收进「关联(选填)」折叠区,默认收起并显示「已关联 N 项」。 - **求职助手写操作状态可跨轮、跨重启如实回答(P0)。** 写入提案从进程内存改为数据库单一事实源 (新表 `assistant_write_proposal`,迁移 `0002`):新增 `pending/approved/executed/failed/unknown/rejected/expired` 状态机(TTL 30 分钟)、创建时记录目标版本指纹并在批准前重查(目标被改过返回 409,不用旧快照写库)、 终态写一条 `kind=proposal_result` 的派生消息(幂等、写失败不影响状态)、下一轮系统提示注入本会话最近 10 条 写操作状态(以此为准)。启动时把上一次进程遗留的 pending/approved 置为失效并清理 24 小时前过期提案的参数与预览。 - **助手页新增「任务」区块与任务层(`agent_task` / `agent_task_step`)。** 任务支持 `draft/running/paused/waiting_approval/done/failed/cancelled` 跨轮推进、等待人工确认不自动放行、 重启后变「已暂停」可继续、同一步幂等重试只产生一次副作用;新增 `/api/agent-tasks` REST 与助手页任务区。 - **设置 →「助手与 AI 行为」新增「提醒到期生成任务卡」开关(默认关闭)。** 打开后到期的日历提醒自动生成 「准备:<提醒标题>」任务卡;去重键为 `(reminder_id, occurrence_due_at)`,只补最近 24 小时,关闭后不再生成、 已生成的卡保留;不自动执行任何写操作、不发消息、不弹窗。 - **助手可感知当前界面上下文(shared application state)。** 聊天请求新增可选 `client_context` (route/view + 选中对象 type/id);后端按当前数据集重查对象并取服务端标题(截断 40 字),查不到时如实 注入「选中项已失效」,前端输入一律视为不可信线索。 - **助手新增 4 个只读工具 + 1 个错误诊断工具。** 简历质量检测(只跑本地规则,不触发远端增强)、 岗位 ATS 匹配估计(强制免责文案)、导出预检、简历版本对比,以及 `diagnose_recent_errors` (只回白名单结构化字段:时间/级别/logger/异常类/HTTP 状态/路由模板/request_id,日志原文不出进程)。 - **文档检索质量重做。** 切块从 `##` 降到 `###`(保留父级面包屑),打分改为「标题命中 > 正文密度 > 次数」, 步骤类片段上限 1500 字,返回结构新增 `source`(相对路径 + 标题路径);20 问标注集 top1 命中率 1/20 → 20/20。 - **action 契约层内核与 3 个试点。** 新增 `services/actions/`(ActionSpec/Registry/tool_adapter/route_adapter): 参数 schema、权限级别、确认预览与实现收成一份定义;路由注册顺序仍由 `application_routers.ROUTERS` 显式决定。 - **action 契约层按域迁移(T28)完成:64/79 个助手工具收成一份 ActionSpec。** 覆盖 jobs / materials / claims / profile / tracker / referrals / reminders / knowledge / interview-experiences / candidate jobs / skills / format templates / resumes(列表与版本/版式)/ interview / drill / apply / analytics / share packages / chat / webform / match analyses / ATS / 版本对比等域的读写动作(新增 services/actions/domains/ 与自动聚合的 actions/catalog.py;路由 path/method/响应模型/状态码与工具 name/description/permission/preview 保持不变)。新增五个兼容机制:tool_defaults(仅工具侧默认值)、 ignore_unknown_arguments(旧路由忽略未知字段)、run_tool(路由与工具既有写语义不同时让工具侧复用旧 read-modify-write 实现)、tool_fields(工具侧字段投影,路由专属字段不暴露给模型)、 tool_argument_aliases(旧工具参数名与路由路径参数名不同时对齐)。路由与工具统一由「旧实现委托」kit (actions/legacy.py)生成,零业务逻辑复制;工具 schema 增加 compact 模式,去掉 default/maxLength/additionalProperties 等模型侧装饰,prompt 固定成本从 46k 降到约 41k 字符。 剩余 15 个静态声明逐个登记在 backend/tests/fixtures/actions_legacy_allowlist.json(含 reason 与 review_after;删除/新增都会让守卫变红),新增工具一律必须走 define_action + as_tool。 - **工具权限四级化。** `Tool.permission` 取代单一 `writes` 布尔:read_only / reversible_write / irreversible_write / external_side_effect(当前无 external_side_effect 工具),守卫要求每个工具显式声明。 - **安全验收清单。** 新增 `docs/security-agent-checklist.md`(OWASP Agentic / NIST AI RMF / OTel GenAI 落点)。 - **开发者文档新增四篇方法论,并与「提交前检查」串起来。** [验证手册](docs/verification-playbook.md) 给出证据六级阶梯、反证三件套与四类假绿判据;[协作规程](docs/ai-collaboration-sop.md) 固化任务书九节、按文件所有权的并行边界、脏工作区纪律与交付报告结构;[缺陷模式](docs/defect-patterns.md) 归纳 31 类高频缺陷的判据与守卫方式;[环境排查](docs/troubleshooting-stale-build.md) 给出「改了没用」的固定判定链。另附[任务书与交付报告模板](docs/templates/)。README 文档表与 CONTRIBUTING 已同步索引。 - **文档 PII 巡检进 CI(`docs-guards` 作业)。** 新增 `scripts/check_docs_pii.py`,扫描 `docs/`、README、CONTRIBUTING 与 `.github/`,拦下密钥、私钥块、平台令牌、手机号、邮箱与带用户名的家目录路径(含中文用户名);占位符与带 `pii-ok` 注释的行自动放过。配套 `backend/tests/test_docs_pii_guard.py` 用正例/反例/空目录/打码四组用例做反证,避免"正则失效→恒绿"。 - **Bug 反馈模板升级为七字段。** `.github/ISSUE_TEMPLATE/bug_report.yml` 增加版本号、期望行为、证据与「怎样算修好」,并在提交前自查里要求先升级到最新版本、先搜重复 issue。 - **Windows 安装包接入 SignPath Foundation 的免费代码签名计划(test-signing 已跑通,生产证书待签发)。** `release-assets.yml` 的 `windows-installer` 作业在打包后把安装包作为本次 run 的 artifact 上传,经 SignPath 校验来源(origin verification)后签名并取回,再用**签名后**的文件写 `.sha256`——先算哈希会与发布文件对不上,所以写哈希的步骤整体后移。`SIGNPATH_ORGANIZATION_ID` 未配置时整段跳过,产物、哈希与 Release 附件与接入前完全一致;配了它但其余必需项(Variable `SIGNPATH_PROJECT_SLUG` / `SIGNPATH_ARTIFACT_CONFIGURATION_SLUG`、Secret `SIGNPATH_API_TOKEN`)不全则直接报错终止,不做静默降级。官网新增中英《代码签名策略》页(签名覆盖范围、签名请求来源、Authors / Reviewers / Approvers 角色与隐私说明)。2026-11-06 的演练(run 42077336929)走通全链路,签名请求 `ec1837db-…` 状态 Completed,实测附带 DigiCert 的 RFC 3161 时间戳。 ### Changed - **macOS 兼容性地板从 13 抬到 14(行为变更)。** 官方支持改为 **macOS 14+**,**macOS 13 及更早不再获得官方支持**(应用可能仍能启动,但不再测试、也不提供支持)。原因:本地语义检索的隔离运行时 onnxruntime 1.31.1 只发布 `macosx_14_0_arm64` 等 wheel,**没有 macOS x86_64 wheel**。README(中英)、`docs/upgrading.md`、`docs/user-guide.md`、`docs/mac-build-handoff.md` 同批更新;**启动链没有版本闸门、脚本不改**。Intel Mac 可运行应用并使用远程语义检索,但不能用本地模型。 - **README 删除过期说法「Apple Silicon via Rosetta 2」。** 启动链按 `uname -m` 下载原生 `aarch64-apple-darwin` 的 python-build-standalone,Apple Silicon 一直是原生 arm64;dmg 也提供 x64 / arm64 两份(见 README 中英与使用指南)。 - **打开联网搜索不再先自动搜一次(R08,行为变更)。** 打开输入框下方的「联网搜索」现在只等于 **允许联网**:`web_search` 工具随之下发,搜不搜、搜什么、换几次关键词都由模型自己判断;**开箱不再有 「开场资料」**——此前每条消息都会先用用户原话自动搜一遍,模型还没看到问题、网络请求就已经发出去了, 看到"帮我把某项目整理成条目"这类问题时还会被带偏成查某个字的字典解释。搜索次数上限仍是 **3 次**,现在 这 3 次**全部由工具发起**(此前那次自动预搜也占一次额度,所以模型自己实际只剩 2 次)。系统提示里随附的 「这次回答已经自动搜过一次……你自己最多还能搜 2 次」整段删除:联网工具的完整度口径(只看摘要 / 会抓正文) 本来就写在工具描述里,跟设置里的「抓取正文条数」走,不需要在系统提示里重复一份会漂移的副本。 「简历通自身的使用问题不搜公开网页」这条保留,拦截改由工具完成(工具直接回本地指引、不打网络)。 使用指南与助手页内嵌引导的联网搜索一节同步改写(含英文文案)。 - **技能由「全局启用」改为「按对话启用」(T14,行为变更)。** 新增表 `assistant_conversation_skill` (迁移 `0005`,含完整 downgrade;备份表快照 73→74):一段对话启用哪些技能成为唯一真源。 **新对话默认不启用任何技能**,在助手输入区用 chip /「+ 技能」菜单勾选,**只对当前对话生效、换对话各有一套**; 空态可以先勾,发第一条消息时随创建会话一次写入;fork 对话时技能一并复制。单个对话**最多 8 个技能**, 注入预算仍是 12,000 字,靠后技能超预算会被截断并在菜单 / chip 上提示(不再悄悄少加载)。 **技能库(工作台 → 技能)里的「启用」列与开关已移除**,技能只在对话里启用;旧 `AssistantSkill.enabled` 列保留但不再参与注入(数据不迁移、不回填)。新增对话技能接口 `GET/PUT /api/assistant/conversations/{id}/skills`(整体替换,最多 8 个)与创建会话时的可选 `skill_ids`。 - **技能上限统一并收紧(T14)。** 提示词 4,000 字 / 最多 10 个知识文件 / 单文件 20,000 字 / 合计 60,000 字 (此前后端校验口径是 200,000 字、50 个文件,与工具描述、注入预算三处打架);新建/编辑弹层去掉「启用」开关, 提示词输入框带 4,000 字上限与字数反馈,知识文件区显示个数与合计字数。 - **技能的新建与修改永远走确认(T14)。** 即使把数据权限切到「完全权限」,`create_skill` / `update_skill` 仍逐次出提案(提示词下一轮就进系统提示,不能让模型自己放开);助手消息 context 记录本轮用到的技能名 (只记名字、不记正文),运行轨迹显示「本轮技能」。 - **助手的数据权限从「放宽模式」改为四档控件(T11,行为变更)。** 设置页「助手放宽模式」卡片升级为 「求职助手 · 数据权限」四档(仅查看 / 放宽读取 / 可修改 / 完全权限),**「放宽模式」在助手侧成为历史名**; 网申填表页的「放宽模式」是另一个开关,仍按原样工作,两者不再混用。旧键 `assistant_relaxed_mode` 保留且 与新键同步写,不做迁移。 - **Anthropic 协议升级到 2026-10-21(T10)。** 官方 Claude 4.7 及以后改用自适应思考 (`thinking: adaptive` + `output_config.effort`),不再发送 `temperature` / `top_p` / `top_k`; 思考强度 minimal→low、high 档 `max_tokens` 至少 16k。**更早的 Claude 模型与第三方 Anthropic 兼容 网关保持 2023-06-01 行为不变**。没有官方 Anthropic Key,本轮以假服务器验证请求体与分支,**未真机验证** 上游真实响应。 - **任务卡位置与开关语义变化(行为变更,T01/T04)。** 助手页的任务区整块移除——它原本是 `.assistant-workspace` 的第 4 个直接子元素,会把消息区那一行顶替成 320px 高,表现为标题下方一大片 空白、消息区失去滚动;工作区同时改成 flex 布局加固,以后再加直接子元素不会再错位。任务统一在首页 「任务」卡查看。设置里「提醒到期 · 生成任务卡」更名为「任务卡」并升级为**总开关**:**关闭 = 不再生成 + 首页不再显示**(旧版关闭只停止生成,已生成的卡仍显示),数据保留、重新打开即恢复;关闭前若还有未完成 任务(未开始 / 进行中 / 已暂停 / 待确认)会先弹确认。配置键 `agent_task_events` 不变、不做迁移; 后端列表接口不按开关返回空数组(显示层由前端 gate)。 - **术语「绑定漏斗」改为「关联求职进度」。** 提醒表单与详情抽屉统一为「关联求职进度 / 关联岗位 / 关联简历」,动作统一叫「关联」;`docs/ui-copy-guide.md` 新增术语 #66,并停用「绑定漏斗」。 - **能力地图(助手功能目录)纠偏 8 处。** 简历照片上限改为 5MB;回收站类别改为 22 类并注明「求职进度」与 「投递记录」在回收站里同名;全局搜索第 8 域改为「我的资料」(技能域已下线);面试深挖补第 5 档「未覆盖」; 日历提醒补第 4 档「稍后(later)」;快捷入口改为「默认 7 个,可自定义」;知识库入口改「新建知识」; 「导入参考模板」补「当前界面不可达(已下线)」前缀。新增内容护栏(文案里的 N 类/N 套/N 个/N 档必须能从 同句清单数出来)。 - **写操作提案有效期从 5 分钟改为 30 分钟**;token 仍只在服务端内存流转、同一请求内核销。 - **助手运行轨迹进入消息 context。** run id/step/tool/起止时间/状态/模型与用量/授权决策/脱敏摘要随助手消息 `context.run_trace` 落库,不含简历正文、附件、Key、思维链与提示词原文。 - **新增「路由+工具」对必须走 define_action**,存量在 allowlist 登记、只减不增(T28 已迁移 64/79, 静态声明剩 15 个、逐名登记 reason 与 review_after);路由注册顺序仍由 application_routers.ROUTERS 显式决定。 - **多代理/多会话协作纪律。** AGENTS.md 补充文件所有权、禁止回滚命令、改前快照与反证要求等规则。 - **运营手册补充「凭据泄露的处置顺序」。** 明确"先吊销再清理"、历史与产物一起查、替换为占位符而不是部分打码,并指向新增的扫描守卫。 - **`AGENTS.md` 校正 2.0 后的过期口径,并补四条规则。** 迁移章节从"grep 旧 head revision"改为"单基线 `0001_baseline_v2` + `app_schema_epoch` 闸门 + 测试统一从 `migration_refs` 取常量";用例规模与耗时改为不写精确数字;`-n auto` 补"内存受限用 `-n 4`"。新增「多会话/多代理并行」(文件所有权、禁 `checkout`/`stash`/`restore`、改前快照、基线三件套、回报格式)、「规模约束(500/800 行预算与豁免规则)」、「文档与个人信息」(PII 巡检与事故处置)、以及"新增守卫必须反证"一条;并把"并行/覆盖率/构建"的历史叙事下沉到 `docs/maintenance.md` §5.1,同步写清两边的权威分工(**红线看 AGENTS.md,执行看 maintenance.md**)。 ### Removed - **清理 20 个零引用源码文件(1,945 行),全部为重构或功能下线后的残留。** 判定方式不是"看着像没用",而是建立导入图逐条验证:前端 18 个 TS/TSX + 1 个 CSS 在整仓(含测试)0 处真实 import,后端 1 个 API 模块在自身文件头就写着"已下线、不再注册路由"。具体为:`StyleTemplateEditorModal.tsx`、`ReferenceFileField.tsx`、`useWorkbenchInlineMarks.ts`、`entryReorder.ts`、`RiskScanWatcher.tsx`、`SuggestionScanWatcher.tsx`、`UpdateCheckButton.tsx`、`WebFormTargetFloatingBalls.tsx`、`CurrentGroupSummary.tsx`、`paginationDomain.ts`(re-export 兼容层,实现在 `features/resume-pagination/`)、单数版 `utils/backgroundTask.ts`(现役是 `backgroundTasks.ts`)、`api/resumeRisk.ts`、`styles/resume-structure-example.css`,以及被通用板块渲染器取代的 7 个资料页组件(`AwardSection` / `SkillSection` / `ProjectSection` / `ExperienceSection` / `CampusExperienceSection` / `SectionVisibilitySwitch` / 见上 `CurrentGroupSummary`);后端为 `app/api/resume_template_import.py`。**这与 1.0 时"文件保留、只在文件头标注已下线"的口径相反**——2.0 换了全新基线,回滚价值已由 git 历史承担,留着只会让导入图和代码搜索持续失真。同时删掉 `AppShell.test.tsx` 与 `ResumeRiskPanel.test.tsx` 中三行指向这些模块的 `vi.mock`(模块不存在时它们会直接报解析失败)。 - **`docs/ui-copy-guide.md` 标记对应条目的定位失效。** 映射表第 7 / 8 / 23 条以 `StyleTemplateEditorModal.tsx` 的行号定位,组件删除后不再可查,就地加注说明作废,保留编号以免破坏历史索引。 ### Fixed - **本地语义检索资产下载的失败原因被混为一谈:`rag-assets-v1` 还没发布时,404 被报成「网络不可用」。** 两个整包 URL 都返回 HTTP 404,设置页却提示检查网络,用户只能一直等一个不会出现的包。现在 4xx/5xx 与连接失败分开归类(可判定关键词「下载源不可用」/「网络不可用」)。同时修正 HuggingFace 备用源的语义:它是**一组必须全部到位的文件**,不是另一个整包——任一文件失败即 整体判失败并清掉 `*.tmp*`,不再把「第一个文件下载成功」当成整个资产就绪(否则磁盘上留下 一份 88% 的残缺目录,而 `assets_present()` 会把它认作可用);逐文件下载的进度分母随文件数 抬高,不倒退、不中途假到 100%。 - **切到 Responses / Anthropic 协议后被判「Base URL 与配置记录不一致」,且 Anthropic 入口此前根本走不通(用户反馈)。** 两处根因:① 密钥引用与 Base URL 的绑定用的是**完整地址(含 path)**——切协议时前端把 `https://api.deepseek.com` 改写成 `https://api.deepseek.com/responses`,path 变了就判「不一致」, 用户手里没有第二个 Key,只能卡死。现在按 **origin(scheme + host + port,默认端口归一)** 绑定: 同一主机上换 path(`/responses`、`/anthropic/v1`、`/v1`)照常解析已保存的密钥;**换主机仍旧拒绝** (含 `api.deepseek.com.evil.example.com` 这类子域伪装),安全底线不变。② 「获取可用模型」只剥离 `/chat/completions`,切到协议专用地址后请求打到 `.../responses/models`、`.../messages/models` 返回 404;现已一并剥离 `/responses` 与 `/messages`。 - **「各服务商协议支持」矩阵与预设逐家重核(首次核对 2026-11-08;2026-11-09 复核见本段 Changed 顶部),并让协议切换真正落到可用地址。** 原表把 DeepSeek、豆包、通义千问、百度千帆、讯飞星火、阶跃星辰、OpenRouter、Together、Ollama (≥ 0.14.0)、LM Studio(≥ 0.4.1)的 Anthropic Messages 入口全标成「不支持」,MiniMax 的 Responses 也标成不支持——后者还被 `responses_unsupported_hint` **硬拦截**(连接测试直接返回失败、 一个请求都不发),用户永远测不通。现在:矩阵按官方文档重写 23 行;预设新增 `supports_anthropic` / `anthropic_base_url`,切协议时按预设自动改写为该服务商的专用地址 (DeepSeek → `.../anthropic/v1`、豆包 → `.../api/compatible/v1`、Kimi → `.../anthropic/v1`、 通义千问 → `.../apps/anthropic/v1`、智谱 → `.../api/anthropic/v1`、MiniMax → `.../anthropic/v1`、 千帆 → `.../anthropic/v1`、星火 → `.../anthropic/v1`;硅基流动 / OpenRouter / 混元 / 阶跃 / Together / Ollama / LM Studio 与 chat 同址),切回 Chat Completions 自动还原;`matchingPreset` 也认这三条地址,编辑已切过协议的记录不会掉成「自定义」。预设同步更新:豆包 `doubao-seed-2-1-pro-260915`、混元 `hy3`、星火 `spark-x2.6`(换到新 MaaS 域名 `maas-api.cn-huabei-1.xf-yun.com`)、阶跃 `step-5-preview`、Together `zai-org/GLM-5.3`、 Mistral `mistral-large-4`、硅基流动 `deepseek-ai/DeepSeek-V4-Pro-0813`、OpenAI `gpt-6-astra` (模型名一以各家控制台为准)。**DeepSeek Responses(`POST /responses`)与 Anthropic (`POST /anthropic/v1/messages`)已用真实 Key 各发一次最小请求,均返回 200;其余厂商仍按官方文档 实现,未逐家真机验证。** - **联网搜索失败时工具会把异常原文回给模型(R08 顺带修复)。** `assistant_tools/search_tools.py` 的失败 分支直接拼了 `{exc}`,而 `AssistantSearchError` 的 `str()` 就是构造时传进来的原文——只有白名单里的 公开文案才允许进 SSE、模型上下文与落库历史(`search_errors.py` 的既有约定)。现在改用 `exc.public_message`,并补上"请勿声称已获得联网资料"的提示;回归用例见 `test_codeql_error_privacy.py::test_search_error_does_not_enter_sse_history_or_model_context`。 - **确认执行写操作后,助手仍说"等待确认"(P0 + 任务/界面上下文/诊断等本轮修复;逐条证据见交付报告)。** P0 根因是提案状态只存进程内存、执行成功后不回写会话、下一轮上下文也不读提案表;现在状态以数据库为准并在 下一轮注入权威状态块——已执行仍回答已执行,重启后未确认的显示已失效。 - **助手工具调用轨迹与对话内容错位**:轨迹按消息 context 持久化,历史回看能看到每一步的工具与授权决策。 - **清理过期 xfail、备份表覆盖守卫同源恒等、文档耗时数字失真** (详见 T12/T15 证据)。 - **官网与 README 的演示数据脚本在 2.0 上重新可用。** `scripts/demo_data/base.py::init_db()` 原来用 `Base.metadata.create_all` 建库,而 2.0 的 `app_schema_epoch` 那一行由基线迁移播种——造出来的库既没有 epoch 行也没有 `alembic_version`,启动闸门判成 `REFUSE_UNKNOWN`,用 `uvicorn` 起服务直接拒绝启动;原有测试因为 `TestClient` 不触发 lifespan 而一直是绿的。现在改为跑真实 `alembic upgrade head`,并在脚本内硬断言「恰好一行 epoch=2 且 revision 非空」。同时把演示岗位状态从旧值 `active` 改成规范值 `开放中`(首页「开放中岗位」不再恒为 0),时间锚点改为「运行日的昨天 08:30」(需要字节级复现时用 `RF_DEMO_NOW` 钉住),首页「近 7 天生成」与提醒分色不再依赖某个固定月份。 - **官网在线体验站的 ATS 检测不再被当成「写操作」拒掉。** `frontend/src/demo/demoFetch.ts` 的只读豁免清单里还留着早已不存在的旧路径 `/api/ats/check`,而真实接口是 `POST /api/resumes/{id}/ats-check`——演示站里点「ATS 检测」拿到的是 403「这是在线体验版,无法真正保存改动」,看起来像功能坏了。现在豁免按路径形状匹配 `/api/resumes/{数字}/ats-check`,该形状走快照回放;其它写操作照旧 403。 ## 2.0.0 - 2026-11-05 ### Changed - **2.0 以全新库为起点:迁移链只保留一条基线迁移。** 原 74 条历史迁移(v1.0.2 已发行的 46 条 + 2.0 开发期新增的 28 条)全部下线,空库执行一次 `upgrade head` 即建全 2.0 的表 / 列 / 索引;数据库只由这条基线代表当前结构,不再有"逐版本演进"的兼容分支。 - **库版本闸门按 epoch 判定,不再按 revision 判定。** 新增单行表 `app_schema_epoch`(由基线迁移写入唯一一行 `epoch=2`)。启动时先读 epoch:缺失 epoch 表且库里有业务表 → 视为 pre-2.0 库,整包归档后建空库;epoch > 2 → 拒绝启动;epoch == 2 时 revision 才用于判断"在不在迁移链上"。这样"老库的 revision 不在新链上"不会再被误判成库损坏。 - **1.x → 2.0 的启动顺序改为「判定 → 整包归档 → 校验 → 移走旧库 → 建空库 → 迁移」。** 任一步失败都中止启动、原库原样保留(不移动、不建库)。 - **文档与界面文案同步到 2.0 口径。** README 版本号改 `2.0.0`、「旧资料升级说明」整行改为归档说明(功能条目数与表格行数一致);`docs/upgrading.md` 新增「从 1.x 到 2.0:整包归档 + 全新库」;「我的资料」升级说明与使用指南改为"已归档、2.0 使用空库"。 - **更新前快照从"只拷数据库"改为完整整包。** Windows 更新器在覆盖程序文件之前先调用 `backend/scripts/create_user_data_snapshot.py`:复用「导出全部数据集」链路把数据库 + 按引用收集的附件 + 字体 + 全局模型配置打成整包 zip,并用 `verify_archive` 校验包可恢复;失败即中止更新(不再继续覆盖程序文件),随后仍保留旧的 `*.db` 拷贝兜底。跨 major 更新的快照目录标 `pre-major-<时间戳>` 且永不被轮转清理(普通快照仍保留最近 3 份)。 ### Removed - **补充事实(`profile_facts`)整套下线。** 删除后端路由 / schema / 服务 / 模型与前端 `features/profile-facts/` 模块;2.0 基线不再创建 `profile_fact`、`profile_fact_field_pref`。 - **57 个迁移测试文件随历史迁移一并下线。** 它们守住的不变量改由 2.0 基线 schema 测试重建:`upgrade → downgrade base → upgrade` 往返可用、表集合与模型注册表一致、`app_schema_epoch` 单行 = 2。 ### Migration - **1.0.x → 2.0 不搬数据。** 首次启动检测到 pre-2.0 库时,先把整个库导出成与「导出全部数据集」同格式的整包归档到 `backend/data/legacy-archive/<时间戳>/`(含 `package/` 内的 zip、`files/` 内的原库与数据集文件、`ARCHIVE-README.txt`),校验归档通过后才移走旧库并建空库。1.0 系列只提示、没有搬数据,新库从空白开始。 - **1.x 备份包在 2.0 不导入。** 导入侧在迁移候选库之前校验候选库的 epoch:缺失 `app_schema_epoch` 的包直接拒收,提示用 ResumeForge 1.0.2 打开;2.0 自己导出的包照常可导入。 - **2.0 的库 / 备份包在 1.0.2 上会被拒收。** 既有 revision 校验会以"来自更新版本"拒绝,方向正确,本轮不改 `BACKUP_FORMAT_VERSION`。 ### Fixed - **「不限篇幅」在制作台里真的可选、可保存、可预览了;`PATCH /layout` 不再"只改一项、顺手重置其它项"。** - **版式局部更新**:`PATCH /api/resumes/{id}/layout` 一直是"全量替换"——只传 `{font_scale: "large"}` 也会把 `page_limit` 写回 1(`ResumeLayoutUpdate` 的字段带默认值 1)、把悬空的 `format_name` 清成空串,返回 200 看不出异常。现在路由按 `payload.model_fields_set` 只写**请求里真的出现过的键**,没带的保持库里原值(schema 默认值一个字节没动,老客户端行为不变)。显式传 `page_limit: 0` 或 `format_name: ""` 仍然照写。 - **制作台可选「不限」**:篇幅上限控件以前只给 1/2/3(长 CV 1–10),「不限」只存在于生成配置。现在按 `document_mode` 传 `pageLimitOptions()`(末尾带 0),并把 `layoutFromDetail`(`|| 默认值` → `??`)、`buildPaginationRequest`(`Number(...) || 1`)两处折弯改掉;`DocumentModeSwitch` 的选项与 `clampFor` 也认 0。选「不限」后保存、刷新、再打开仍是「不限」。 - **预览/画布/导出接 0**:`ExportRequest` / `PreflightRequest.layout` 的 `page_limit` 从 `ge=1` 放宽到 `ge=0`(否则"选了不限"一点导出就 422);`PreviewPdfRequest` 同理;分页报告多返回 `page_limit_is_unlimited`,前端在不限时把缩略图摘要切成"实际 N 页"(不再说"上限 10 页");画布链不再把 0 夹成 1——`ResumePreview` 的 `pageLimit` 保留 0、`resolvePageCounts` 对不限单列一支(`overflow=false`、不画红框、不弹"第 N 页起不计入导出"),精确预览与测页请求都按 0 发。 - **没动的边界**:`build_pagination_report` 的数字 `page_limit` 仍回报渲染保护上限(10),由新增的布尔字段区分;「不限」在长 CV 模式下同样走 10 页保护。 - **「不限篇幅」(`page_limit = 0`)不再在任何链路里被悄悄折成 1 页。** `page_limit` 的 0 是**语义值**(不限),但一串 `X or 1` 把它当成了"缺省":投递存档记成 1 页、按存档重放就只剩 1 页;简历详情接口回 1,编辑器显示"上限 1 页";版本快照/恢复一次也会把它变成 1(恢复路径甚至会变成 10);版式预览、助手导出、生成的字数预算同样按 1 页算。现在分成两层:**语义字段一律原样保留 0**(`X if X is not None else 1`,只有 NULL 才回落 1)——API 出入参、投递存档与 freeze/replay、版本快照与恢复、版式保存与生成落库;**渲染入口才折成保护上限**(`mode_page_limit` / `UNLIMITED_RENDER_MAX_PAGES`,即长 CV 的 10 页),并且 `build_export` 加了一层中央兜底,任何把语义 0 直接交给渲染上下文的入口都不会再被 `max(1, …)` 压成 1 页。助手的状态描述在「不限」时改说"不限篇幅",真超 10 页保护上限时才提示"超出渲染保护上限"。原先遗留的两处(精确预览/分页报告请求体 `ge=1`、分页报告对不限记录只回数字)也在同批收口,见上一条。 - **2026-11-04 收口(③④):PDF 也会说"文件里少了内容"、「不限」不再回一个假的上限、生成顺序不再漏科研四栏。** - **PDF 的裁剪事实**:`X-Resume-Truncated` 以前只有 Word/HTML/Markdown/纯文本会发——同一份简历被裁到 1 页,Word 会说"从某板块起没有",PDF 只说"溢出(`X-Resume-Overflow`)",界面读到的口径不一致。现在 `build_export` 把"PDF 引擎真的按页删了内容"(`pages_needed > pages` 且不是「不限」)也算进 `truncated`;PDF **不**发 `X-Resume-Truncated-At`——它按页裁,指不出板块/条目。 - **「不限篇幅」的 page-limit 头**:`X-Resume-Page-Limit` 以前会把 `mode_page_limit(0)` 折出来的 **10 页渲染保护上限**当成用户选的值回给界面,于是选了「不限」却显示"上限 10 页"。现在「不限」回语义值 **0**(前端解析为"没有上限"),保护上限只在渲染内部生效。选「不限」时也不会被报成"已按上限裁剪"(那份 10 页保护不是用户的篇幅承诺)。 - **生成弹窗的板块顺序**:`PROFILE_SECTION_TO_RESUME` 补上 **publications / research_projects / patents / credentials** 四栏。此前它们两侧都存在却漏了映射,后果是「调整板块顺序」只列出 9 个板块(这四栏永远排不了),且新建简历的默认顺序把它们一律丢到末尾、与资料页排好的先后不一致。现在清单是 13 项(12 个内置正文 + 自建板块),默认顺序跟随资料顺序;空板块所有渲染器都不产生节点,旧产物不变。`basic_info` 仍在页眉、不进清单(所以摘要的"N 个板块"会比清单多 1,这是设计)。 - **2026-11-04 问题清单(12 条)一次性收口。** 清单与逐条验收见 `.workbuddy/issues/ResumeForge-问题清单-2026-11-04-定稿.md`,交付报告见 `deliverables/software-company/resumeforge-2026-11-04-delivery.md`。 - **Word 导出补齐三处保真**:页头最后一段现在画 `w:pBdr/w:bottom`(粗细/颜色取模板的 `header_border_width`/`header_border_color`,`soft_accent` 页头不叠加);技能标签按 PDF 同一套 `tag_*` 形状落成 run 级 `w:shd` + 同色 `w:bdr` 内边距(**按 PDF 的流式排列,不强行一行一个**——定稿方案里"一行一个 + `w:br`"与验收里"换行位置与 PDF 一致"互斥,取后者;圆角、`tag_columns` 网格、逐边 padding 是 OOXML 表达不了的,已在模块 docstring 写明);项目符号从 `·` 改为 `•`,不再显得比正文小一号。(1-A / 1-B / 1-C) - **「最大篇幅」对五种导出格式成为同一件事**:`page_limit` 以前只管 PDF,Word / HTML / TXT / Markdown 会把全部内容导出来。现在唯一出口 `export_pipeline._render_with_page_limit` 先按 PDF 版式真渲染一次,超限时按**整块**(要点/条目/板块)裁掉尾部再渲染,Word 最多收敛 3 轮;`X-Resume-Truncated` / `X-Resume-Truncated-At` 回裁剪事实,JSON 保持完整,「不限篇幅」靠 `page_limit_is_unlimited` 保住语义。(1-D) - **未收录资料清单重做**:一条一行、组头 `分区名 · N 条` + 原因汇总、默认前 5 条可展开、按钮移到组底;每条 note 新增显式 `reasons`(旧记录按 `capacity > filtered > model` 回退)。(5-A) - **自建板块「补进简历」**:新增 `POST /api/resumes/{id}/append-profile-entries`,按 `entry_ids` 确定性写入并修剪清单;「AI 补上这段」只留给能靠改写完成的技能/语言/个人总结,条目级改为「去制作台添加」并说明 AI 不能新增条目。(3-A) - **基本信息的 5 个字段有了纸面去路**:意向城市 / 作品集链接 / 政治面貌 / 驾照 / 到岗时间进入页眉目录(`header_fields`),字段目录同时下发当前值,加入时连值一起拷贝;默认生成产物不变。(6-A) - **生成弹窗只列当前组参与的板块**:模板目录新增 `group_section_keys` / `group_section_order`,前端按组过滤并在字段缺失时回退全量;删掉无人调用的 `sectionPresence.ts`。(4-A) - **资料自定义字段可隐藏**:字段设置行补上与默认字段同形态的隐藏开关;全局搜索改为排除 `hidden ∪ private`(不含「仅素材」),标题也不再回显被隐藏的值。(7-A / 7-B) - **求职进度漏斗删掉百分比**:条长仍按组内相对值归一,tooltip 写明"不是转化率";**悬浮球旁的小卡贴左不再被裁**:抽 `clampAnchoredCard` 纯函数 + `useTouTouAnchoredCard` 夹进视口,尾巴继续对准球心。(2-A / 8-A) - **拖动 chip 时不再弹「松开鼠标以搜索文本」。** `user-select: none` 只关掉了"选中高亮",**管不住拖拽语义**——Chrome 判断"这次拖的是文本还是元素"看的是 `-webkit-user-drag`,实测 `.document-chip-value` 当时仍是 `auto`,于是按下拖动被当成拖动那段文字,浏览器弹搜索浮层、条目却没动。现在编辑器里除 `[draggable="true"]`(把手与 chip 本体,它们才是拖拽源)之外,所有元素一律 `-webkit-user-drag: none`;一条通用规则而不是逐个类补——`user-select` 那轮已经因为"漏一个类就重现"踩过一次。`-webkit-user-drag` 只影响"把选中的字拖出去",不影响输入框里的选中、复制、剪切。 - **制作台:选中文字后右键只出一张卡片、正文块又能拖了(两个回归一起修)。** 上一轮为了让拖动不再弹 Chrome 的「松开鼠标以搜索文本」,给正文块把手加了 `mousedown` 上的 `preventDefault()`——它连 HTML5 拖拽的起点一起吃掉,表现为**按住把手完全拖不动**(自动化实测 `dragstart` 计数为 0,去掉后恢复为 1);防选区改由 `user-select: none` 加容器上的 `selectstart` 拦截承担,两者都不碰默认拖拽。另一处是"两张卡片叠在一起":选区菜单走全局单例浮层,字段菜单走 `DocumentEditor` 自己的 `onContextMenu`,**同一个 `contextmenu` 被两套监听各处理一次**;现在 `features/context-menu/selectionGuard.ts` 给出一份判据(输入框 `selectionStart/End` 或文档选区,纯空白不算),`useDropdownContextMenu` 与 `DocumentEditor.handleContextMenu` 打开前都问它一次——选了字就只出选区菜单(剪切/复制/润色),没选字照旧出字段菜单(收藏/排版/隐藏/移除),键盘 Shift+F10 不受影响。实测:有选区 `{rf:1, antd:0}`、无选区 `{rf:0, antd:1}`。 - **模拟面试答完最后一题:面试官先回应这一答、再说结束。** 此前轮数用尽直接收盘——界面上最后一句话是候选人的「不知道」,紧跟着就是「本场正式问答已结束」,真实面试里不会这样。现在收盘前多跑一次模型(新提示词 `interview_closing.md`),产出「针对最后一答的点评 + 收尾话(回去等通知这类)」,落成一条 `context.closing` 的面试官消息,之后才出现决策卡;「继续追问 / 结束并生成报告」都不变。这次调用失败时**不写空消息**、照样收盘,界面用本地化兜底文案补一句;**模型自己收尾(`done`)时那句就是告别**,不会再补第二段。代价是每场面试多一次模型调用(约 2–5 秒),换取「话说完才落幕」。 - **面试「开始面试」请求体混入上传接口的 `warnings`,两侧用导入文件时必 422。** 上传端点返回的是读取 DTO(`{name, text, warnings}`,警告供界面上的警告条使用),前端把它整份存进表单、整份塞进 `POST /api/interview`,而写端点 `InterviewMaterialIn` 只有 `name`/`text` 且 `extra="forbid"`——于是两侧各报一条「包含不支持的字段」,面试停在设定页开不起来;文件本身其实已经解析成功。现在提交边界上有唯一的投影函数(`frontend/src/features/interview/materialPayload.ts`),state 继续保留 `warnings` 供展示,只在提交时丢弃。**没有放宽后端 `extra="forbid"`**:那会让"客户端字段拼错"一律静默吞掉。这与 `PUT /api/apply/config` 上把 GET 响应整份回传导致 422 属同一类错误,决策已补进 `docs/architecture.md` 的决策表;单测从"断言某个字段的值"改为断言 `Object.keys(job_material)` 恰好是 `["name","text"]`。 - **ATS 总分与覆盖率重新挂钩(评分口径变更)。** 此前关键词分项与覆盖率完全脱钩:缺 1 个核心词和缺 12 个都只扣一条 `medium`(12 分),"量化加分"传的其实是简历里带数字条目的占比、与关键词无关,而核心词为空的 JD 会被判成覆盖率 100%——于是一份对岗位零关键词命中的简历能拿到覆盖率 0%、关键词 98、总分 92。现在关键词分项 = `round(100 × 覆盖率) − 堆砌扣分`,量化加分挪进格式分项;核心词为空时退回"核心+补充"做分母,两者都为空才显示"未识别到可比对关键词"(不再显示 100%)。**同一份简历同一份 JD 的分数会普遍下降**:完全不对口的组合从 92–95 降到约 55,区分度才出现。 - **「没写进这份简历」的技能归因不再冤枉模型。** 技能名额在候选阶段就被上限(默认 14 条)压掉,旧实现把没写进去的一律记成"模型省略",用户会看到「这些技能在候选资料里,但模型没有写进简历」——事实上模型一条都没漏。现在按"是否进过候选"三分:被相关性/预算拦下的记 `filtered`、**被条数上限裁掉的记 `capacity_omitted`(新字段,旧记录缺省时按无处理)**、真正进了候选而模型没写的才记 `model_omitted`。说明文本随生成落库,**存量简历不回填**(候选集未持久化、无法可靠重算),重新生成后即用新口径。 - **资料条目写入按读取侧的字段上限校验,"写入成功但资料页打不开"的坑被堵上。** 极端数据验证发现:资料工作区的条目写入(`EntryCreateIn.values`)不校验读取模型的字段上限,160 字的奖项标题能写入成功(201),但 `GET /api/profile` 随即 500(`AwardOut.name` 上限 128)——**一页资料整页打不开**,且用户看不出是哪条惹的祸。现在写入收口(`entry_store._validated_values`,upsert/PATCH/bulk 三条路径共用)按投影真正使用的同一组读取模型注解构建上限表(`services/profile/read_field_limits.py`,43 个字段,field_id 复用 profile_service 的映射、两侧永不漂),超限返回 400 `value_too_long`,detail 写明字段、上限与当前长度。只校验提交值:存量已超限的行 PATCH 其他字段仍能保存(写合并值会把已中毒的行彻底锁死,用户连改别的字段都不行),把超限字段本身改成合法值即可自行修复。自建分区字段与照片没有读取模型上限,维持宽松不新增拒绝面。 - **「为什么是这样一份简历」的标题式段落对真实文本生效了。** 问题 11 的首版判据要求"整行以「:」结尾且不含句号",而后端生成的是「事实约束:……。」(标题与正文同一行、带句号),6 份真实 rationale 里 0 行命中——单测夹具用了假的独立行所以一直是绿的。现在识别「短标签:正文」形态,只把标签加粗、正文保持常规字重;整行标题的旧形态仍按原样处理。 - **制作台第三批修复(18 条,用户逐条实测上报)。** 清单与逐条验收写在 `.workbuddy/issues/ResumeForge-问题清单-2026-11-03.md`。 - **预览与生成说明**:「这些资料内容没有写进这份简历」改为**默认收起**并用条数徽标保留"还有 N 条"(1),同一分区的多条 note 先按归一化分区键聚合,重复的 React key 与三行同标签因此消失(2);「为什么是这样一份简历」不再是一整块 `pre-wrap` 密文,按段渲染(段距 10px / 行高 1.7),未收录清单上移到覆盖块、说明里只留一句指路 + 条数(11)。 - **底栏按钮**:主网格从 `auto-fill` 改 `auto-fit`,装不满时不再留空列;同一行只剩一个主按钮(「完成」降为次按钮);间距收敛为组内 8px / 组间 12px 一套口径,左右同排的换行阈值随之从 623/563 重算为 615/555(3)。 - **任务完成弹窗**:「查看简历」从正文句子里挪进 `footer`,排在「知道了」左边,不再把一句话切断(6)。 - **AI 修订**:模型返回空 updates、或返回与原文逐字相同的内容时不再落库并报「修改成功」,改为 409 `revision_not_applied`,并逐条列出被事实锁挡下的字段(16)。**同时保留**"隐藏/排版/锁标记不算正文变化"的既有契约:模型只在非正文标记上作出响应时仍按成功处理,不误报未执行。 - **生成结果**:技能列表按名称归一去重(`MySQL` / `SQL` 各两次,14);未收录清单比对前剥掉行首列表标记(`- ` / `* ` / `1. `),大量误报因此消失(15);本次候选资料里根本没有的分区不再进提示词的输出模板,模型不会再为没启用的板块编造内容(4)。 - **ATS 覆盖率**:JD 抽词不再把 `C++` / `CI/CD` / `.NET` 拆碎,补充词命中可加分但不进核心分母,13% 这类"假低"消失;格式问题另算一个分项,不混进关键词命中(5)。 - **面试**:消息正文接上 Markdown 渲染(`components/common/MarkdownBlock`,与助手共用同一份),`**粗体**` 与 `- 列表` 不再裸露(9)。 - **关闭专用浏览器**:从 `Popen.terminate()`(`TerminateProcess`)改为走 CDP `Browser.close`,让 Chromium 自己 flush Cookie 后退出,登录态不再丢(10);`StopAwareCdpClient` 补上 `close_browser` 转发,并在基类给空实现的能力上按"漏转发即静默失效"的判据加了守卫测试。 - **制作台条目**:正文块新增拖拽把手(`draggable` 只挂在把手上,正文文本区仍可选可编辑),上移/下移与键盘重排沿用同一份 `placement` 判据(12)。 - **补充要求**:不美化档位下 grounding 会把正文换回资料原文,补充要求改不了任何东西——该档位下输入框直接禁用并说明原因,后端也把这条写进 warnings(13)。 - **面试官设定**:「从记录选 / 导入文件」开关与上下元素之间的间距补齐、选中的分段项不再与主按钮抢权重(7)。 - **网申面板空态**:把「资料里还没填」和「当前方案未包含」拆成两个独立原因——前者给「换个资料…」入口,后者给「切换为全部资料」入口,不再用同一句话糊过去(8)。**同时修掉一个从没被发现的必修 bug**:整页自动填写把 `filtered_fields` 传给了不接收它的函数,`TypeError` 被宽泛的 `except` 吞掉,于是「整页自动填写」在没有待填字段的页面上永远失败、还只报"读取当前页面失败"。 - **排版优化器**:「保持条目完整」此前只约束结构化条目,内容候选不受它管,开关开了照样给"删内容"的方案——现在两个候选来源共用同一个判据;「最小页边距」从"候选筛选条件"改回"排版参数下界",不再因为保住边距而把候选整体砍掉(17)。 - **方案与板块**:三层界面按条重排——方案行改为单选并显式标注当前方案,板块/字段行去掉嵌套的多余操作,重复说明合并,导航不再出现两个入口(18)。 - **桌面打包的 Python 运行时重建在 CI 上稳定失败(get-pip 的 Windows 自我保护)。** 2026-11 起 bootstrap.pypa.io 的滚动版 get-pip.py 改为把内嵌 pip 解包后以 `python \pip` 运行,`sys.argv[0]` 的 basename 恰为 `pip`,在 Windows 上命中 pip 自身的防自改保护(`protect_pip_from_modification_on_windows`,pypa/pip#1299)直接报错退出——缓存失效触发的「一次性重建」从此必挂。现改为绕开 get-pip:下载 pin 版本(25.3)+ SHA-256 校验的 pip wheel 直接解包进 site-packages,`python -m pip`(argv[0] 是 `__main__.py`)天然不受该保护影响;pin 版本让重建产物可复现。只影响打包机,用户侧产物与运行行为不变。 - **制作台第二批修复(22 条,用户逐条实测上报)。** 逐条证据见 `.workbuddy/issues/report.md`,实机脚本在 `.workbuddy/issues/verify/qNN-*.mjs`,截图在 `.workbuddy/issues/shots/`。 - **草稿预览与页数链**:草稿画布不再双重滚动、不再按列跳(1);页数改由服务端分页报告驱动(`pages_needed`),左栏「约需 N 页」、黄条、画布三处同源,拿不到报告时自动退回浏览器测量且不阻塞编辑(3、20);内容超过篇幅上限时**超限页照样画出来**,第 N 页起标「超出上限,不计入导出」并加淡红虚线,导出仍按上限截断(2);12 号这类「上限 10 页」的简历不再被算成 21 页假超页(11);切换精确/草稿模式后预览不再永久冻结(12)。 - **渲染引擎**:多行字段在 HTML / PDF / Word 与草稿里都按原文换行(16);灰字组的段距下沉到组首尾,PDF 行距与草稿一致(22.1px,原 28px),组高 168→132.8(17);条目灰字段支持「正文前 / 正文后分侧列」与「分布:靠左 / 两端」,`gap` 上限由 8 调到 16 且单位为字号倍数,HTML / PDF / Word 同语义(4);Word 导出的联系方式改为**组内按可用宽度装行**,与预览、PDF 的断点一致(9)。 - **交互与控件**:条目位置下拉全中文,不再漏出 `inline` / `body` 等枚举(10);「从资料填入」菜单滚到底不遮不截、标题吸顶(5);浮层滑杆轨道宽度还原(15);拖拽落点的 `toIndex` 只有一份实现,落在原位不再画假插入线,四种拒绝原因各有文案(13);提示条改为 portal 到滚动容器外,字段区零位移,info 2.5s / warning 8s 分流(14);「跟随模板」在 44px 值列里不再溢出(21);三个标题布局图标从卡片头搬到「标题与元信息」域标题行(6);板块 / 条目选中改为按粒度高亮,不再是几十个灰方块(7);字段值输入框可纵向拖高,不再是塞满一行的单行框(8);4 处密度问题(新页区可收起、末页签可见、说明不折行、底部提示距视口 ≥16px)(22)。 - **照片**:裁剪框 8 个手柄各自只动对应边,1:1 中心夹回、Ctrl 全程等比、键盘可微调(18);modern 模板照片的白边去掉了,8 套模板里只剩它多画一条 2px 白框(19)。 - **Word 联系方式与标题竖条的排版修正(问题 9 的产物层变化)。** 同一联系方式组内按可用宽度装行(超宽字段整串独占一行,不切开),组间不合并——用户显式换行仍是意图;分隔符从两个全角空格改为两个半角空格(CJK 字体下约 1em,贴近预览的 1.14em)。分区标题前的竖条不再把「段前距 + 行盒 + 段后距」一起包住(原实现视觉高度约为行盒的 3 倍):分区间距落到上一段段后、0.4em 落到下一段段前,竖条高度等于行盒。 ### Changed - **「最大篇幅」现在约束**每一**种导出格式,不再只管 PDF。** 以前用户选「1 页」,PDF 是 1 页,Word / HTML / TXT / Markdown 却把全部内容都导出来——`page_limit` 在代码里从来只是"PDF 的页数上限",而界面文案叫「最大篇幅」,承诺与实现不一致。现在唯一出口 `export_pipeline.build_export` 统一分派:**PDF** 走原有的真渲染 + 按页截断(不额外测量);**Word / HTML / Markdown / 纯文本** 先按 PDF 同一套版式真渲染一次拿到 `pages_needed` 与绘制日志(`resume/document_truncate.py`,纯函数),超限时取第一条越界内容所在的**整块**(要点 → 该要点及其后;条目 → 该条目及其后;板块 → 该板块及其后)裁掉尾部再渲染,绝不切半句;Word 因页数只是连续流估算(±1),裁后再核对一次、最多收敛 3 轮,仍超就保留内容并回 `X-Resume-Overflow: true`。**JSON 不受约束**(数据交付物,导出面板在它旁边标注了这一点)。响应头新增 `X-Resume-Truncated` / `X-Resume-Truncated-At`(`板块/条目`),`X-Resume-Pages-Needed` 继续回真实需求页数(html/md/txt 也带了);导出成功的提示改为「已按 N 页篇幅上限裁剪,超出的部分不会出现在文件里」。**「不限篇幅」的语义也被保住了**:`RenderContext.page_limit_is_unlimited` 单独承载原始 `page_limit == 0`(`mode_page_limit(0)` 会把它折成 10,只看数字分不清"选了 10 页"与"选了不限"),五格式都不裁。性能:非 PDF 导出每次多一趟 PDF 测量,本机实测 93–125ms(字体缓存已热),远低于 1.5s 预算。**两处不可根除的取舍**已写进 `docs/user-guide.md`:裁点按 PDF 版式计算,Word 打开仍可能多出半页;单条要点本身超过整页时会被整条丢掉(与 PDF 既有行为一致)。 - **批量生成 / 批量导出的参数改成"当场设置、只作用于这一批",不再把人赶去设置页。** 以前点「批量生成简历」会先弹「先完成批量操作设置」,`onOk` 直接 `navigate("/settings?tab=appearance#batch-operation")`——SPA 里跳转等于卸载岗位页,已勾选的岗位(常是 3 个)全部丢失。现在两个动作都改为打开一张**受控**弹窗(`components/batch/BatchConfigModal`):初值在打开时重读一次全局配置,从未配置过就用与后端一致的默认值并提示「当前是默认参数,可直接开始或先调整」;确认后才开跑,参数**不写回**设置页(那两张卡保留为默认值入口,intro 文案已说明两者关系)。为此字段组抽成 `BatchGenerationFields` / `BatchExportFields` 一份实现,卡与弹窗共用,不做两份会漂移的表单。**导出侧有后端契约变更**:`POST /api/resumes/batch-export` 的请求体新增可选 `config`(`extra="forbid"` 保持),路由按「本次参数 > 全局默认」取值,**既没传 `config`、全局也没配置**才 409——只传 `resume_ids` 的老调用方行为逐字不变;传入的 `config` 走与设置页同一套校验(字号/页数/语言/格式枚举 + 模板名白名单),非法值 422。前端同时补上此前完全缺失的份数上限:一次最多 50 份(与后端 `MAX_BATCH_EXPORT` 同值),超出时弹窗确认按钮直接禁用并写明原因,不再等一次 422。旧的 `useBatchOperationGuard.ensure()` 与其两条 `batchOperation.guard.*` 文案已删除(没有第二个入口依赖它),未配置不再是一道"看起来还在的墙"。 - **批量 AI 进行中现在跨页可见:新增全局浮条 `BulkAiFloatingBar`。** 挂在 `AppShell`(与 `BackgroundTaskBar` 同级),订阅既有的 `useBulkAiState()`,显示「正在批量生成简历 2/3 · 当前:岗位名」+「查看」(回岗位页)/「停止」(沿用 `stopBulkAi` 的"当前岗位跑完再收尾"语义)。**只在非岗位页渲染**:岗位页已有工具栏内联进度条,两条同时出现是重复信息;完成通知仍由 `notifyTaskDone` 单一来源负责,浮条不再挂第二套提示。刻意没有并入 `utils/backgroundTasks` 登记表——那是 adapter + 轮询服务端任务的模型,而批量 AI 没有服务端任务实体。 - **面试材料的接受范围两侧统一。** 此前岗位侧只收 PDF / TXT、简历侧只收 PNG / JPEG,那是 #41 实现时的假设而不是产品约束。现在两侧一致接受 图片(png / jpg / jpeg / webp)、PDF、TXT、Word(.docx)、Markdown(.md / .markdown),**解析路径由文件类型决定**——`kind` 只表示"放哪一侧"。文本类走本机提取(更快更准,未配置模型也能用),图片与扫描件 PDF 走视觉识别;扫描件判定为「PDF 文字层 < 200 字」,命中后把前 8 页渲染成位图识别并与文字层拼接,警告会写明「已按图片识别」。**有文字层的 PDF 不会被这条兜底拖去要求配置模型**。`.doc`(Word 2003 二进制格式)在选文件那一刻就被拒绝并提示另存为 `.docx`。字数上限从"文本 20,000 / 图片 24,000"拉平为 20,000,同一份内容存成 PDF 与 PNG 导入后字数相同。 - **面试设定卡的材料行改为横跨整行。** 边界文案搬出简历列的 `extra` 之后必须另起一行,首行只剩两个材料字段,而外层栅格是 `auto-fit`(列数随窗口变化),于是右侧留出一大片随窗口"呼吸"的空白。现在两个材料字段包进 `.interview-setup-materials`(自身也是 `auto-fit` 两列、容器宽度不足 496px 时塌成一列,不需要媒体查询),`grid-column: 1 / -1` 让这一行占满整行;岗位下拉因此变宽,长岗位名不再被截成「选中后按该岗位的 JD 提问(…」。 - **草稿画布的页数改为跟随服务端分页报告,而不是只靠浏览器测量。** 一次排版变化会多一次 `GET /api/resumes/{id}/pagination/report`(原先只有「获取分页报告」按钮才请求);报告不可用时退回浏览器测量,功能不降级但页数可能与真实 PDF 差 1 页。**刻意没有做 `page_breaks` 注入**:注入会在整数页之间插半空页,改用「报告页数直接定页帧数」后整数页数已与 PDF 一致。 - **Word 里没有 `contact_breaks` 的旧简历,联系方式行数可能变化**(由 Word 自由断行改为按宽度装行)。这是让三端断点一致的必要变更,字段内容、顺序、不丢不重都保持不变;`tests/test_export_bundle_contact_breaks.py` 的旧断言已按新契约更新。 - **超限内容的画布呈现变了**:以前只画到篇幅上限,现在会把超限页也画出来并标注,方便用户在导出前看到「多出来的是哪几页」。导出行为、页数上限、缓存键都没有变。 ### Added - **深色主题下草稿画布以「深色纸」呈现,导出与打印不受影响。** 预览 iframe 整体做 `invert(1) hue-rotate(180deg)`,照片与内联矢量图再抵消一次(证件照不会变负片);纸面容器底色改走新 token `--rf-preview-paper`(浅色 `#fff` / 深色 `#000`),深色下不再有投影。**只作用于预览**:`--rf-paper`、后端模板、`render_config_fingerprint` 一行未动,`@media print` 里把滤镜关掉(`Ctrl+P` 仍是白纸),导出 PDF / HTML / DOCX 逐字节不变。切主题只刷 iframe 文档的 class,不重载(不闪白、不丢滚动位置)。 - **导出前自动预检,超页先问一句。** 导出弹窗点「确认导出」时先跑一次预检(与导出同一份版式草稿),存在必须解决的问题(当前只有 `product_pdf_overflow` 这一类)就在面板内挂一条确认——**文案带三个具体数字**:内容需要 N 页、当前上限 M 页、第 M+1 页起不会导出;按钮「仍然导出 / 先调整」。同一读数只问一次,无 blocker 不打断,预检本身失败(网络断、500)一律放行并 `console.warn` 留痕——预检是"多一道提醒",不能变成新的导出失败点。此前 `page_limit` 小于实际页数时后端照截断并返回 200,第三页就这样静默丢失。 - **网申资料的「取数范围」可切换:仅当前方案 / 全部资料。** 悬浮面板清单顶部与网申页各有一个分段开关(两处同一份语义、同一份实现),切换后**清单、批量预览与自动填表三处同源**——不会出现"清单按全部、预填按当前方案"。范围行为发生在快照构造处(`profile_snapshot` 按 scope 组装),不是"渲染时挑一挑",所以自动填写真的会拿到全部资料。页面侧记住用户的选择(默认仍是"仅当前方案"),空态按原因分两句;**资料里从来没配过的字段即使不在当前板块也会被补进来**并标注「资料里还没填」——这两个 pin 原本就存在,只是以前被范围逻辑盖住了。 - **面试收尾改成「先问是否追问,再出报告」。** 正式问答结束后不再直接生成报告:后端把 `finish` 拆成"收盘"(插收尾 note、置 FINISHED)与"出报告"(幂等)两步,自动收尾只收盘并在响应里给 `needs_report`;前端据此显示一张决策卡(继续追问 / 结束并生成报告),刷新后仍回到这张卡而不是空白报告。`POST /finish` 三分支(未收盘→收盘并生成;已收盘无报告→只生成;已有报告→直接返回)保证"提前结束"与"补生成"都不会吞掉报告。报告只收正式问答的 transcript,**报告前的追问不进报告**。顺手修掉一个语义错误:走流式接口答完最后一轮时,最后一条回答被标成 `interrupted`(它其实是正常答完),于是「重新回答」按钮点了必 409——现在这条分支置 `completed` 并按正常结束处理。 - **自建板块可以选择是否参与岗位筛选。** 板块级开关(`profile_custom_section.filter_by_job`,迁移 `0075`,默认 `false`),随板块走;开启后该板块的条目参与岗位关键词筛选,判据与内置分区**同一份实现**,被筛掉的条目会出现在「没写进这份简历」里——否则用户只会看到"东西少了"却查不到原因。旧数据零迁移成本(缺列按 false 读),备份表清单不变。 - **ATS 检测第三层:核心词 / 补充词 / 格式规则,全部纯规则、不调用模型。** 核心词决定覆盖率分母,补充词(同义、上下位、工具举例)命中加分但不进分母,含符号的库内词(`C++`、`C#`、`CI/CD`、`.NET`、`Node.js`)整体保留不被拆碎;「格式」成为独立分项(解析类问题落在它自己身上,不混进关键词命中)。别名与 JD 分词分别住在 `services/ats_aliases.py` 与 `services/ats_jd_tokens.py`,格式检查在 `services/ats_parse_check.py`——三处都不依赖模型,因此同一份简历同一份 JD 的分数可复现。 - **CSS 类名存在性守卫:JSX 里写的项目类名必须在样式表里真的存在。** 防的是"组件不报错、测试全绿、只是没排过版"的静默退化(`.task-notice-actions`、`.interview-material-source` 这一族都这么退化过)。守卫扫 `src/**/*.tsx` 的 `className`(只取可静态确定的部分:字面量、模板字符串静态片段、`clsx` 的字符串参数),按项目自有前缀过滤,再要求 token 作为选择器出现在**项目 CSS** 里;以分隔符结尾的动态修饰符按基类判定。存量零样式类名进豁免册,每条必须写具体理由 + 到期日,且有一个用例专门查"幽灵豁免"(类名已补样式或已不再使用时条目必须删除)。 - **求职助手联网搜索接入 DeepSeek Search / Fetch(可选来源)。** 与"搜索来源"里其他项并列,但**只在填了自己的 API Key 且勾选它时**参与并发;没填 Key 时它不参与、也不会显示成"搜索成功"。它不跟随大模型的对话配置(额度与账单独立计费),已保存的 Key 回显为 `********`,清空输入框即移除;勾选后抓取正文会把网页地址发给 DeepSeek 读取,失败才回落本机读取——这两条隐私与计费口径都写在设置页的说明里。 - **篇幅支持「不限篇幅」,并让收敛目标跟随用户选的目标页数。** 篇幅控件末尾新增「不限」(`page_limit = 0`,后端同名字面量 `UNLIMITED_PAGE_LIMIT`):提示词改为"按内容需要展开",不再给总字数上限;篇幅闸门不裁剪、不发超页/删减警告,但仍然会提示"某个核心板块一条都没留下"。渲染侧总要有个 `max_pages`,所以 0 在渲染/导出路径上映射到长 CV 的上限 10 页(`UNLIMITED_RENDER_MAX_PAGES`)——这是保护,不是约束。**收敛目标不再恒为 1 页**:排版收敛从用户选的目标页数起试,超页提示补上"目标 N 页、实际 M 页"(保留原有的"超出"子串,既有守卫不受影响);「自动一页」入口是 `page_limit = 1` 的特例,行为逐字不变。**生成期刻意不做自动收敛**(要真实 PDF 渲染逐档试版,一次生成跑 N 次渲染会撞长 CV 的 20s 资源预算,失败时也没有确定的可回退版本),这条决定与理由写在 `resume_fit.py` 的目标页数入口。 - **`load_all_consumed_profile` 的使用契约有了守卫。** 该函数是"AI 能看到哪些资料"的唯一入口,此前任何新代码都能绕过它直接读资料表,导致"界面显示已启用、模型却看不到"。现在有测试扫描全部调用点(`tests/test_all_consumed_callers.py`),新出现的直读会被点名。 - **自建板块可选择是否参与岗位筛选,且选了就真的参与(问题台账需求 6)。** 「资料设置」→「板块与字段」的自建板块行下方新增**板块级**「参与岗位筛选」开关(跟随方案保存)。**默认关闭 = 与升级前逐字节一致**:整块无条件进简历,适合「作品集」「通用补充」这类本来就该全带的板块。开启后该板块的条目与系统板块用**同一个判据**(`profile_matching._score_item` + 同一个 `JobFocus`,没有第二套打分)判断与岗位关键词的交集:交集不足的条目不写进这份简历,并逐条出现在生成结果上方的「这些资料内容没有写进这份简历」里(说明原因、可在那里补回),生成说明的「候选资料组成」也会带上该板块的入选条数。存量数据只加一列 `profile_custom_section.filter_by_job`(迁移 `0075_profile_section_job_filter`,旧库/旧备份缺列按 false 读)。 - **制作台可以新建「仅本份简历」的板块,也能删掉自建的。** 中栏页签条末尾的「+」打开输入框,建好即出现在末尾并选中、左栏结构树同步;保存后重开仍在,资料页(「我的资料」)不会多出这个板块——它只属于这份简历,所以「从资料填入」也不会包含它。自建板块可在页签右键「删除板块」删除(系统板块不提供删除,只能隐藏或改标题),删除可撤销。后端 `normalize_document` 相应放宽一条:空的 `custom:*` 板块且有标题时保留(新建后先保存、内容随后再填是合法中间态),空的系统板块照旧丢弃。 - **条目灰字段支持「逐列宽度」与「逐间隔」。** 两列以上(且对齐方式为等宽列)时,左栏「条目字段排版」多出两行:「列宽」按列填百分比(合计必须 100%,不等于 100 时「应用列宽」禁用并标红合计),「逐间隔」按相邻列分别填字号倍数。三个出口共用一份判据(`column_geometry.resolved_column_geometry`:长度对不上就整体作废、回落等宽):HTML 走 `grid-template-columns` 加逐格内边距,PDF 的独立绘制器按同一组百分比折算列宽与列间距,Word 落成表格列宽加单元格右边距。旧数据没有这两个字段时产物逐字节不变(回落等宽 + 统一列间距)。 - **导出弹窗新增「导出形式:交付包 / 单独文件」。** 单独文件可多选格式(PDF / Word / HTML / 纯文本),逐个串行导出并显示「正在导出 2/3:Word」;某一项失败会报「已完成 1/3,Word 失败:原因」,不会静默中断。交付包的请求与产物逐字节不变,选单独文件时「归档原件」禁用并说明只属于交付包。 - **模板试用卡片可以直接「采用模板默认」。** 采纳前弹出二次确认写清「会清空全部排版参数(字号、行距、颜色、页边距与局部排版),换模板后找不回来」;「保留当前排版覆盖」走另一条分支。 - **新建简历支持「板块顺序」与一键清空。** 从头编写通用简历时,默认板块顺序跟随「我的资料」;顶部「顺序」可在保存前逐个上移 / 下移,或一键「与资料顺序同步」;底部新增「清空所有内容」(保留板块与页眉字段结构,清空条目与值)。AI 生成的配置页同样新增「顺序」面板,手动调整结果随生成请求落库。(Q36) - **来源核对:新建来源可以填类型 / 引用 / 原文摘要(P5-1)。** 「建立来源关联」弹窗里新建来源不再只有标题:可选来源类型(事实台账 / 资料箱 / 岗位 / 个人资料 / 手工登记)、可选的「来源引用」(同一类型 + 同一引用会复用那条来源)与可选的「来源原文」摘要(只参与内容指纹与修订判断,来源表仍不存全文)。只填标题的旧行为完全不变:kind=manual、默认 payload 仍是 `{"title": 标题}`,历史来源指纹不会因此变化。后端 `POST /api/provenance/sources` 只增可选字段 `kind` / `external_ref` / `payload`(非法 payload 类型/超长返回 400),新建后仍走既有 `POST /api/provenance/links`,没有第二套写入链路。 - **来源核对:旧关联无法定位时不再静默消失(P5-2)。** 旧记录里只存旧路径、当前正文找不到对应内容时,来源视图与界面会明确提示「有 N 条旧关联无法定位」,逐条给出来源、旧路径与原因(对象标识不合法 / 没有可定位信息 / 当前正文找不到这个位置)。用户可以直接「删除旧关联」,或先在列表里选中目标块、再点「重新关联」;此时只预选来源、不自动挑块,新关联建立后才删除那条失效旧行。读侧只增字段 `unresolved_links`,**不迁移、不改写、不按同下标猜目标**,旧行原样留在库里。 - **条目字段排版:看得出「继承板块」还是「本条覆盖」,并可逐项恢复继承(P5-4)。** 只调一条条目时,列数 / 对齐方式 / 列间距三项各自显示来源标记,覆盖项另给「恢复继承」按钮——只把 `entry.layout` 的对应项置空,**不写板块默认**,也不影响同一条目的其它覆盖项。整体「恢复默认排版」仍然保留。 - **第三阶段:剩余入口收尾 + 制作台按 v2 模型精简。** 页缩略图与「获取分页报告」回到分页面板(页数/末页占用/溢出与手动分页标记来自**真实 PDF 同一条渲染管线**);来源核对从只读升级为可写(按选中内容建立 / 解除来源关联、新建手工来源、**采纳最新来源写回正文**并带 revision 条件写、事实锁增删);双语「在工作台查看这份译文」接上只读预览(不改主稿、不推进 revision、不产生脏值)。 - **制作台左栏按 v2 语义重组为「模板 / 版面 / 样式」三组。** 模板只管 shell + CSS 与版式预设;版面集中文档模式、篇幅上限(去重后唯一入口)、联系方式换行、分区/条目分页与页缩略图;样式集中字体字号、行距、页边距、颜色、角色/对象/文字级别/格式刷/批量。「本份简历结构」页签吸收**条目字段排版**,成为唯一结构编辑中心(结构树选中条目 → 条目排版区跟随);**删除** v1「板块顺序」控件与「简历结构示例」示意弹窗,重复挂载的角色局部排版 / 局部排版工具分组收进「样式」。 - **导出 / 分享 / 交付重放 / 模板预览不再折 v1 分析视图(T4 首批)。** 新增 v2 原生只读访问器 document_view.py;这些导出衍生链路直接消费 v2 文档(导出管线本就 v2 直通),避免自定义位置/隐藏语义在只读路径上失真。 - **补完批次:制作台的暂缓入口已补回。** 文档模式(「排版参数」顶部,普通 / 长 CV,保存走版式条件写);分页控制(分区级 + 条目级「从新页开始 / 整块不跨页」,随简历保存,HTML / PDF / Word 同口径);局部排版工具(对象级排版 / 文字级别 / 格式刷 / 多选批量);双语版本(按 v2 字段路径对照,采纳只写语言变体、不替换主稿);表达资产库;来源核对(只读);**新建简历弹窗切到与制作台相同的新编辑器**;中栏「从资料填入」菜单(点开才拉当前方案投影、按来源去重、只补空字段)。**仍暂缓**:页缩略图、来源核对里的建立 / 采纳写操作、双语「在工作台查看这份译文」入口(占位)。 - **真实 AI 生成验收通过(deepseek-flash)。** 通用简历与岗位导向简历各真实生成 1 份,`v2_check --profile-coverage` **45/45 通过**(内容 v2 / 字段位置 / 仅素材不泄漏 / 资料覆盖 / 六出口 / 事实一致);首轮发现的 5 项生成后处理问题已修复并复验。证据见计划 §20。 - **简历内容升级为「字段级文档模型 v2」。** 每个字段(含自定义字段)都是一条可拖动的独立内容,位置本身是数据:**标题行 / 元信息 / 正文前灰字 / 正文 / 正文后灰字 / 隐藏**;同一条目内可任意排序、隐藏、放进正文,跨位置拖拽即改位置。v1 的「承载区文本」与模板写死的「固定灰行」取消——绩点、核心课程、技术栈、证书详情等从此和别的字段一样可调。旧简历打开时自动转换(文字与条目数不丢,允许视觉变化),保存后即写为新结构。 - **自定义字段可编排。** 「我的资料」添加自定义字段时可选「纸面位置」(短字段默认正文后灰字、多行文本默认正文);制作台里它作为真实字段出现,可拖动排序、隐藏、放进正文;「仅素材」字段标注「不上纸面,仅生成素材」。 - **条目头布局三选一。** 条目的标题与元信息支持 **左右 / 上下 / 元信息在上** 三种排法,长元信息换行显示,不自动缩字号。 - **旧简历自动升级提示。** 打开旧简历时服务端在内存里完成转换(不写库),制作台显示提示条「旧简历已转换 / 已整理为字段级结构,文字与条目都在」:「查看转换摘要」可看识别字段数、保留文本行数、警告与空板块,可收起、不阻塞编辑。保存前每次打开都会提示(收起只隐藏本次会话);保存后即为新版结构,不再提示。逐条报告仍由 `scripts/convert_resume_documents.py`(默认 dry-run)产出,不自动跑真实库。 - **制作台新增「简历结构示例」只读弹窗。** 左栏入口打开一张用 HTML/CSS 画的示意 A4 简历:16 个编号标注 + 图例逐条说明每个参数管纸上哪一块(页边距、板块、板块标题、条目、条目标题行与日期、正文与要点、项目符号、行距、段前段后、缩进、对齐、区块间距、条目间距、分页线、字体字号、照片大小);悬停或点图例时对应区域高亮,点一下固定、再点取消。图只用于对照,不读也不改这份简历;中英文界面的示例内容与图例都在语言包里。 - **条目里的单行补充字段可以整组移到要点/描述之前。** 制作台左栏「排版参数 → 条目字段排版」新增「短字段位置:正文后 / 正文前」,作用于分区默认或单条条目(默认「正文后」= 现有产物逐字不变)。选「正文前」后,教育 / 工作 / 校园 / 项目 / 奖项 / 论文 / 科研项目七个分区的信息头按「标题与时间 → 单行补充字段 → 要点或描述」排列;奖项分区会把「名称 · 时间」「颁发机构/级别/获奖比例」「说明」拆成三层。HTML / PDF / Word / Markdown 四格式同序。 - **补充字段可以单独隐藏(内容保留)。** 「条目字段排版」每个字段行最左边新增眼睛按钮:隐藏后该字段不再出现在 HTML / PDF / Word / Markdown 纸面,但正文数据、保存与 AI 回填都不受影响;支持分区级与单条覆盖,隐藏全部字段不会留下空白行。 - **设置页新增「批量操作」,批量生成与批量导出各自一套参数。** 两张卡分开配置:生成卡(版式、字号、页数、语言、AI 润色、附加要求)与导出卡(版式、字号、页数、语言、导出格式),改一张不影响另一张。**没有保存按钮**——改动 500ms 后自动保存;第一次打开卡片时若从未配置,会用当前默认值启用一次,避免"只想用默认值"的用户被守卫反复挡回去。未启用时两个动作仍会先弹「去设置」,后端也会拒绝未配置的批量导出请求(409)。 - **简历中心新增「批量导出」:按导出卡的参数把所选简历打成一个 zip。** 多选工具条与整行右键改成与岗位广场同一形态的「批量操作」下拉(**批量删除 / 批量导出**),原来单独的「批量删除」按钮收进下拉;「退出多选」统一改名「退出选择」,计数样式、按钮尺寸与岗位广场对齐。批量导出一次最多 50 份,单份正文仍有未完成标记时跳过该份并在完成提示里说明,其余照常导出。 - **左侧导航可以手动收起。** 宽屏侧栏由 200px 加宽到 216px,品牌区右上角新增「收起侧栏」按钮;收起后只剩图标,页头左侧出现「展开侧栏」。选择保存在本机(localStorage),刷新与重开应用保持;≤1092 的窄档仍按断点自动折叠,不显示手动切换。 - **岗位广场多选态新增统一的「批量操作」下拉,整行右键菜单也换成同一组动作。** 下拉含 **匹配度分析 / 岗位需求解读 / AI 生成简历 / 加入投递队列 / 导出 CSV / 批量删除** 六项,原来是平铺的按钮(导出、入队、删除)加三个只能逐条点的 AI 动作。语义是**逐个执行原来的单项操作**,不是合成一份结果:生成简历与岗位解读在页面上串行执行(一次只打一次模型),显示 `已完成/总数` 与当前岗位,可「当前岗位完成后停止」,结束时弹完成通知;删除仍可撤销,导出/入队沿用原行为。 - **「我的资料」编辑态会标出改过的字段。** 此前改过的字段和没改的看起来一模一样,用户看不出"这次要提交哪些改动"。现在每格字段以自己的**已保存值**为基线比对:改过的字段标签变强调色、输入框换浅底,改回原值就自动恢复;新加的条目写完内容同样会标出来,只读查看态不标。 - **费用估算按「缓存命中 / 未命中 / 输出」三档分开算,单价按每百万 token。** 用量账本新增 `cache_hit_tokens` / `cache_miss_tokens` 两列(迁移 0067,可空——旧账本没这个信息),OpenAI 兼容端点的 `prompt_tokens_details.cached_tokens` 与 Anthropic 的 `cache_read_input_tokens` 都会被抓到并落库;用量卡上直接列出三档 token,命中数未知的旧记录单独计数说明,不混进合计。缓存命中价通常只有未命中的 1/10,合并成一个"输入价"会把省下来的钱算没。 - **题库每题都能写自己的答案,并让 AI 打分 + 解析 + 给出修改建议。** 每道题下面新增「先写我的答案」:写完点「交给 AI 批改」,得到 0–100 分(按切题/结构/证据/岗位相关性/表达五维综合)、档位评语(可以上场 / 还能更好 / 需要重写)、总评、做得好的地方、差在哪、怎么改,以及一段保留你自己经历与口吻的**改写示范**。批改会把已生成的参考答案一并带上做对照,所以"差在哪"是相对标准答法说的;作答与批改结果随题库历史保存,重新打开还在,改过内容会提示"重新批改才会更新分数"。 - **「我的文件」里能预览文档了。** 原先只有 PDF 能内嵌预览、图片只能点缩略图,Word 文档只能去文件夹里打开;现在 docx / pdf / txt / md / csv / json 等都能在抽屉里直接看——文字由后端**在本机**提取(不发给第三方),并明确标注"这是提取出的文字、不是原件排版",想看原件仍可用「打开所在文件夹」。 - **模拟面试新增「关联岗位 + 关联简历」必选:面试官只读这两份材料。** 此前面试官拿到的是「个人资料」,问什么都可能跑出简历与 JD 之外。现在开场两项都必填(缺任一项 422,指向已删除的记录 404),会话落库 `resume_id` 与简历标题快照(简历被删后历史仍能说清当时面的是哪份);送给模型的上下文只剩「候选人的简历」与「该岗位的招聘信息」,提示词新增「信息边界(硬性)」:不得引用、猜测或索要这两份之外的背景,简历里没写过的公司/项目/数字一律不许编。关联简历下拉的候选是「本岗位关联的简历 + 通用简历」(`GET /api/resumes?job_id=X&include_general=true` 新增参数,默认关闭、旧调用产物不变)。 - **模拟面试新增计时器:先问「准备好了吗」,点了才走表。** 进房间先过一道闸门(首次「准备好了」、离开后回来是「继续面试」),点下之后才开始计时、作答区才出现;离开房间会暂停,回到房间必须再点一次才继续。累计有效时长随每轮与结束上报(`PUT /api/interview/{id}/duration`,按绝对值取大值防回退),报告卡展示「面试用时」,生成评分报告时把用时作为输入之一。 - **资料箱的附件都能点开预览。** 详情抽屉里每个附件行改成原生 `