# 更新日志

版本号在 `package.json`、`src/server.ts` 的 `APP_VERSION`、界面侧边栏与页脚保持一致，由服务端 `/api/meta` 统一下发。

## 未发布

### 独立管理后台 `/admin`

原先的管理功能是六个隐藏区块，塞在科研工作台最底部，只有管理员登录后往下翻才看得到。现在有了独立的管理后台：网址后面加 `/admin`（线上是 `/conquer/admin`，域名下是 `/admin`），用科研账号登录。管理员看到全部页面；专家复核员只看到留言板与需求对接两页。科研工作台「账号」区给有管理权限的人放了一个入口链接。

- **搬过来的：** 科研账号（启用、停用、改角色）、留言板审核、需求对接审核、读者提问、知识复核、备份状态。
- **补上页面的：** 患者账号（只有总数与按癌种的聚合数字，没有个人明细）；获批通知（先试运行看会发给多少人，表单内容与试运行一致、二次确认后才真发）；数据维护（一键刷新药物清单、各国获批状态、研究进展，重建 ToolUniverse 目录）。
- **新增的：** 概览（各项数字与待办直达）；全站设计任务（可取消卡住的任务）；站点配置（发起人、简介、联系方式、运营主体、备案号，保存即生效，旧版本自动留档）；审计日志（按事件类型筛选）。
- **站点配置存在运行目录里**（`.runtime/site-config.json`），不会被下次部署覆盖，也会随每日备份一起备份；没改过时仍读 `public/site-config.json`。
- 后台页面不进搜索引擎、不进缓存；所有新接口只对管理员开放（匿名 401，其他角色 403），管理动作都写进审计日志。
- **修复：审计日志里的患者事件和留言板审核事件是乱的。** 这些地方调用审计时多传了一个参数，账号 id 被逐字拆成了 `"0"`、`"1"`……这样的字段，真正的详情没写进去。现在已改正。已经写坏的历史条目保持原样。

### 品牌标识、ICP 备案号，以及患者注册那一栏的修复

- **网站 LOGO。** 按《AI Conquer Cancer Logo Design》6b 方案接入：顶栏是深色底，用反白版（分子线条换暖白，红环 #ec3013 与字形不变），并把标准的上下组合改成适合顶栏高度的左右组合；页脚是浅色底，用原色版。浏览器标签页图标用方案里的 favicon 简化版，深色模式下线条自动变浅；另有 16/32 像素 PNG、`favicon.ico` 与手机主屏图标。源文件放在 `public/brand/`（已去掉 SVG 里与显示无关的内容凭证元数据）。注意：设计包里的 PNG 导出缺了分子图形，只有文字，网站没有用它们。
- **ICP 备案号。** 页面最底部居中显示「京ICP备2026013659号-2」，链接到工信部备案管理系统 beian.miit.gov.cn。备案号直接写在页面标记里，脚本没运行也会显示；同时写进 `site-config.json`，以后改一处即可。
- **患者注册的文书同意框。** 它借用了科研注册那张三栏列表的样式，整句「我已阅读并同意《…》」被挤进 22 像素宽的第一栏，一字一行竖排，框里看上去一大片空白、没有滚动条。现在和科研注册一样：每份文书一行，点「阅读并同意」弹出全文，滚到底才能确认。
- **患者注册未开放时如实说明。** 线上还没有短信或邮件验证通道，服务端不收患者注册，但表单照样摆出来，患者会填完整张表才在最后一步被拒。现在直接显示「注册暂未开放」，并指向不注册也能用的功能。
- 页脚不再在联网部署的站点上写「本地部署」。

### 开放注册：邮箱注册即可使用，不再人工审核

站点的方针是让尽可能多的人用起来。现在用任意邮箱注册即开通「注册科研用户」并**自动登录**，直接进科研工作台——不用等审核，也不用再输一遍密码。邀请码与机构邮箱域名两条通道照旧，开通的是「认证机构用户」。

开放的是使用，不是公开对患者说话的资格：注册科研用户能用快速档设计、导出、虚拟药企与社会需求栏；公开回答留言板仍需机构认证，写的社会需求进展先经审核。部署方要收紧时设 `ONCOFORGE_OPEN_REGISTRATION=off`。

文书同步：《用户协议》更新到 2026-09-22（注册路径）；《发布研究需求同意书》更新到 2026-09-22-2，如实写明「需求给所有注册科研人员看，开放注册、不经人工审核」。线上原有 1 个待审核的注册科研账号已一并开通。

### 修复：手机上文书弹窗无法阅读、看不到「同意」按钮（严重）

手机上首次进入时弹出的《医疗免责声明》，正文区只剩约 44 像素高，「同意」被挤出屏幕——而「同意」要滚到正文底部才会解锁，等于手机用户进不了站。原因有两个：全站给页脚写的 `footer` 规则（限宽、上边距 48px 起）误套到了弹窗的按钮区，把它加高、缩窄，两个按钮被挤成上下两行；要点摘要钉在正文上方，本身就占半屏。

现在摘要移进滚动区；手机与矮屏上弹窗铺满整屏、标题压紧，按钮区贴底并避开 iPhone 底部手势条；「不同意并离开」与「同意」固定在同一行，同意按钮文字过长时在按钮内折行。实测 390×664、360×560、320×568 与特大字号下，正文区 350–455 像素，两个按钮都完整可见，滚到底解锁、逐份同意、弹窗关闭全流程可走通。

### 社会需求栏：患者发布需求，科研人员在虚拟药企里接手，进展公开

- **首页的科研人员入口。** 患者首页头图下方一条「虚拟药企」横幅：「进入虚拟药企」切到科研门户（未登录时停在登录提示上），「看研究进展」直达公开的社会需求栏。
- **患者发布。** 在结果页底部「发布研究需求」，用的就是刚核对过的那张表单的答案（封闭词表），加上需求类型与一段 300 字以内的补充说明。报告原文不上传；补充说明里的身份证号、手机号、住院号、邮箱、社交账号一律拒收；急症措辞当场给急诊指引、不落库。发布前必须签新的《发布研究需求同意书》（`docs/NEED-CONSENT.md`，闸门 `need`），没签一个字节都不写。
- **不认人，只认码。** 线上没有短信通道、患者注册是关着的，所以不走患者账号：发布后给一串 12 位查询码，只显示这一次，服务端只存摘要。凭码查看进展、撤回——撤回是真删，连同已公开的进展。需求在最后一次更新后 365 天自动删除。
- **科研人员接手。** 工作台新增「社会需求栏」：按癌种筛选、跟进、「用虚拟药企评估」（预填由指标推出的靶点与适应症），跑完在结果顶部「把这次评估分享到这条需求」。附上的是服务端存下的那一轮评估摘要，不是客户端传来的结论。
- **公开什么。** 公开页只出现癌种、靶点、需求类型与进展；分期、治疗史、困扰、补充说明只给登录的科研人员看。机构认证账号（`board.answer`）的进展直接公开，注册科研用户的先进审核（管理端新增「社会需求栏审核」）。
- 《隐私政策》更新到 2026-09-22，补上 `.runtime/needs.json` 与 `.runtime/biotech/runs.json` 的保留期，以及社会需求栏的可见范围——已注册用户下次登录需要重新确认。
- 备份脚本：每月 1 号那份备份原本从不删除，和注释里的「保留一年」不符；现在按一年清理。同意书对患者承诺了「撤回后备份里的副本最迟一年消失」，这句话靠它兑现。
- 修复：登录状态每变一次，「智能体」与「虚拟药企」的提交按钮就会多绑一次，点一下会并发跑两轮。

接口：`POST /api/needs`、`POST /api/needs/lookup`、`POST /api/needs/withdraw`、`GET /api/needs/public`（公开）；`GET /api/needs`、`POST /api/needs/follow`、`POST /api/needs/progress`、`GET /api/biotech/runs`（需 `design.fast`）；`GET|PATCH /api/needs/queue`（需 `board.moderate`）。测试：`tests/needs.test.ts`。

### 虚拟药企：CSO + 靶点 / 安全 / 临床 + 科学评审

照斯坦福 Zou 实验室 Virtual Biotech（Science，2026）的分工，做了一个最小原型：`src/core/biotech.ts`，界面在科研工作台「虚拟药企」。

- **CSO** 用 Open Targets 直连写立项简报（靶点、别名、可成药性、疾病关联），把问题拆给三个部门并行；
- **靶点发现部**：HPA 癌种预后、DepMap 依赖性、可成药性 → 药物形式建议。「在细胞表面、肿瘤不依赖它」走 ADC（B7-H3），「肿瘤依赖、有口袋」走小分子（EGFR）；
- **靶点安全部**：Open Targets 安全性信号、gnomAD 遗传约束、DepMap 普遍必需、GTEx 正常组织表达；
- **临床开发部**：ClinicalTrials.gov 直连，逐项判断分期、状态与终止原因（「not for safety reasons」不会被数成安全性终止）；
- **科学评审**：核心数据齐备、每条结论指回证据、结论与证据自洽。核心调用缺位就**打回重跑一次**，第二轮仍缺则如实计为缺口；任何一个部门完全没有证据时，CSO 不下结论。

和 `agent.ts` 一样不用大模型生成结论：判断是可复核的规则，每条结论挂着证据编号（T1、S2、C3），证据链到数据源。ToolUniverse（安全性、依赖性、遗传约束、预后四项）走新增的 `batch` 命令——一个 Python 进程加载一次、线程并行，拒绝名单照旧逐条检查。实测一轮约 6.5 秒。

接口：`GET /api/biotech/plan`（公开）、`POST /api/biotech/run`（需 `design.fast`，每人每分钟 6 次）。测试：`tests/biotech.test.ts`。

**回溯验证。** 填一个证据截止日期（界面默认 2026-09-01），系统只用那天以前的数据下判断：ClinicalTrials.gov 按首次登记日期截断，反映临床进展的可成药性标签（Approved Drug / Advanced Clinical / Phase 1 Clinical）一律剔除。CSO 下完结论之后，才去取截止日之后新登记的试验，看它们的药物形式和当时的首选是否同一方向——这部分数据不参与任何部门的判断。其他库（Open Targets、DepMap、GTEx、gnomAD、HPA）无法按日期回退，报告里逐条写明这种信息泄漏。

**GTEx 改为直连。** 从服务器访问时 GTEx 的接口时不时整个挂住，经 ToolUniverse 一次超时 30 秒、还会重试，一轮能拖到两分钟。现在并发三份请求、谁先回用谁，每份最多 10 秒，结果按基因缓存半天。GTEx「成功但是空」按没拿到处理，会被评审打回；一个部门缺了一半以上的核心数据，评审直接不放行。

试验相关性也更严了：只在否定语境里点名靶点的（「HR+/HER2-」「EGFR wild-type」「三阴性」）不算直接相关；HER2-low 仍算——那正是 ADC 在打的人群。

## 0.23.0 — 2026-09-18

按 0.22.0 的一轮客户测试反馈改。顺序照反馈给出的优先级：**否定词识别 → 试验条件匹配 → 表单冲突与城市过滤 → 手机设置弹层 → 文案与长页整理**。前两项是会把人引向错误结论的事实性错误，其余是可用性。

### 否定词：多种「未检出」被认成阳性（事实性错误）

`ROS1 未检出融合` 被预选成**阳性**，`EGFR 未检出 L858R 突变` 被预选成**检出敏感突变**，`ALK 未检出重排`、`KRAS 未检出 G12C 突变` 同理。而直接写「ALK 阴性」是对的——问题出在否定词和「突变／融合／重排」同时出现的表达上，而报告里这种写法远比「阴性」常见。

原因是取值规则按先后顺序在整段文字上试，`融合` 那一条先命中，没有人问过它是不是被否定的。现在 `src/client/report.ts` 里：

- 否定线索单列一张表（未检出、未检测到、未见、未发现、不支持、阴性、野生型、MSS、pMMR、not detected…），每个否定词的**作用范围到本小句末尾**；
- 判读时先把被否定的那几段挖掉，再拿剩下的「肯定部分」去匹配。所以 `检出 L858R 突变，未检出 T790M` 仍然读成敏感突变，而 `未检出融合` 读成阴性；
- 整项被否定却又出现一条肯定结论（`ALK 检出融合，免疫组化 ALK 阴性`）时，**不预选任何值**，留原文让患者自己定；
- 同一行写了好几个指标时，每一项的取值窗口在**下一个指标名处截断**——这是同行多指标唯一靠得住的边界；指标名匹配也加了词边界，`ER` 不会再在 `HER2` 里面命中。

四条原话与同行多指标的情形都进了 `tests/report.test.ts`。

顺带补齐两处「明确数值仍需人工处理」：`ER(0%)`、`PR(0%)`、`ER <1%` 按词表里那条 1% 的线判读；`PD-L1 TPS <1%` 判低表达、≥50% 判高表达，1–49% 仍旧交给患者——那一段不同方案结论不同。

### 试验匹配：HER2 阳性被匹配到明确要求 HER2 阴性的试验（事实性错误）

填 ER 阴性 / PR 阴性 / HER2 阳性，结果里出现标题写着 `HR+/HER2-` 和 `Hormone Receptor-Positive、HER2-Negative` 的研究，理由还写着「与你填的『HER2：阳性』相关」。这不是匹配得宽，是匹配理由没有区分相反条件。

现在 `src/core/trials.ts` 把「提到靶点」和「条件相关」分开：

- 阴阳性**只看登记方自己的定性**——标题、正式标题、适应症。入排全文里几乎一定有一句不带限定的「HER2」，拿全文去判，任何反向限定都会被那一句盖掉；
- `HER2-negative`、`HER2-low`、`ultra-low HER2`、`HR+/HER2-`、`EGFR wild-type`，以及「三阴性 / TNBC」（它等于 ER、PR、HER2 三项都阴性）都按**方向相反**处理：不加分、排名压后，并在卡片上写明「与你填的方向相反，很可能把你排除在外」；
- 仍然命中的条目，措辞改成「登记信息里出现 X——**关键词检索结果，适用条件未核验**」，不再写「与你相关」。

反向的条目不删除：标题里的写法未必等于入排标准的全部，删掉就等于替研究中心做了判断。

### 表单冲突与城市过滤

「还没开始治疗」和「用过化疗」原先可以一起勾上并照样出结果。现在界面上互斥（勾了前者，其余置灰清空），服务端也拒——界面只是把错误挡在发生之前，不能替代校验。

城市框原先根本没被读取，填「肺癌＋北京」和「肺癌＋Beijing」都照样列出全国 60 家。现在它是**过滤**：只显示该城市的中心，并说明筛了什么；一家都没有时明确说「本次在『X』没有扫到」，而不是把全国名单当成本地名单端上去。常见城市做了中英与拼音对照，填中文也认。

### 未填写的三种情况分开

「报告里有但尚未确认」和「未提供」原先都落到同一句「没有找到」。现在识别到但读不准的项目会**预选「报告里有，但我不确定怎么填」**——这不是替患者判读，恰恰是在说这一项还没确认；结果页的缺口卡片分成三种标签（报告里没有这一项 / 报告里有，待确认 / 这次未填写），门诊问题也按三种分别措辞。

### 手机端

- **显示设置弹层向屏幕左侧溢出**：这个断点上顶栏尾部是整行 `space-between`，「Aa」排在最左边，而弹层写的是 `right: 0`，于是它从最左边往左长，特大字号时「中文」「EN」和几个字号按钮跑到屏幕外。改成靠左展开、按视口铺满一行。
- **四个结果按钮挤在一行**断成「保存清／单」「修改信／息」：改成两列，并禁止词内断行。
- 结果页太长（乳腺癌约 9,900px）：最新进展与外链入口收进可折叠区块，标题上带条数——核对结果、看依据、门诊问题先被看到。

### 文案

| 位置 | 改法 |
|---|---|
| 导航「配靶向药」 | 改为「靶点与药物信息」——原来的说法像是提供选药服务 |
| 生成按钮 | 全流程统一为「生成我的就医准备」，并按原文恢复，英文界面下也不会变回中文 |
| 关于我们 | 「不解读你这一份报告」改成「只解释指标的通用含义和公开资料，不对你这一份报告作诊断或个体化治疗判断」 |
| 药物结果 | 「全世界已获批」改为按来源署名（NCI 清单），并写明**这份清单的适用地区是美国** |
| 行动建议 | `**不代表没有检测过**` 正确渲染成加粗：先转义再认标记，纯文本导出里去掉星号 |
| 资讯分类 | 机构人事、治理、募款按内容归入「产业与机构动态」，不再因为来源是一家研究机构就贴上「同行评议研究」 |
| 第一屏 | 「粘贴报告文字」「手动填写」「选择报告文件」三条入口平铺，粘贴排第一——PDF 与照片仍不支持，最显眼的入口不该是多数人用不上的那个 |

## 0.22.0 — 2026-09-14

按一轮真机走查的反馈改界面与文案。按反馈给出的优先级执行：手机顶栏、步骤提示、未填写状态、结果页操作前置，然后才是留白、编号与长文密度。

### 手机顶栏

390px 宽 + 特大字号时，「登录」「注册」「中文」被挤成上下两个字，「字号」竖排。语言与字号收进一个 Aa 开关（桌面端仍平铺）；顶栏按钮强制单行，品牌副标题与版本号在手机上隐藏。它们是一次性偏好，不该和每次都要点的登录按钮抢同一行。

### 步骤提示按入口说话

手动填写的人根本没上传过报告，却看到「我们读到了这些，对吗」——那句话预设了一次并不存在的识别。现在两条入口两句话：手动是「填写报告中的信息」，识别是「请核对识别结果」。

核对提示的语气也改了：从「机器会读错，你的核对是最后一道关」改成「识别可能有误，请对照原报告确认；不确定的项目可暂不填写」。

退回第一屏、进入结果屏时都会清掉上一阶段的提示——旧提示留在那里会让人以为刚才那份报告还在。

### 未填写 ≠ 未检测

这是一处事实性错误。原来的「确认还缺的 5 项检测」把「这次没填」说成了「没做过」，而这两件事对患者的含义完全不同。

改成「核对这 5 项尚未填写的信息」，并在正文里写明**不代表没有检测过，也不代表报告里缺了它们**。

顺带拆了一个一直合并着的选项：「报告里没有这项／我看不懂」拆成「**报告里没有找到这一项**」和「**报告里有，但我不确定怎么填**」。这两种处境要做的事不一样——前者要问医生该不该做，后者只要把报告拿出来对一眼。85 个指标全部拆开，缺口卡片、门诊问题、下一步都按种类分别措辞。

### 结果页操作前置

手机上整份结果约一万像素高，复制/下载/打印埋在八屏之后，「修改信息」在最底部。

现在开头就是摘要条：本次填写摘要（已填结果 / 相关药物 / 尚未填写 / 门诊问题四个数）、四个操作按钮、以及可跳转的目录。**问题清单移到临床试验之前**——那份清单才是患者真正要带去门诊的东西，不该排在八张英文试验卡后面。试验卡默认只出 3 张，医院名单默认折叠。

### 其他文案

- 首页标题改为「**查找与报告靶点相关的药物信息**」，与「不推荐用药」的定位一致。
- 没有药物卡时不再顶着「与你的靶点对应的药」，改为「本次信息匹配结果」，并区分**没填任何指标**与**填了但没匹配到**——后者明确说明化疗、放疗、手术、免疫治疗不在这份清单覆盖范围内。
- 分期的「还不确定 / 医生没说清楚」改为「**暂不清楚**」，表单上也写明选它不影响出结果。
- 按钮统一：导入报告 / 查看结果 / 修改信息，去掉「交报告」「拿结果」「回去改一改」这类口语变体。
- 复制失败时不再只给一句提示，而是直接把清单摊开、全选好放在那里，并指出「保存清单」按钮。
- 留言板空状态原来出现两遍，合并为一句。

### 收尾

- **装饰性章节编号全部去掉。** 它们已经和可见顺序对不上（两个 08、缺 01 和 04），折叠「更多」之后还会跳号；这些数字不编码任何信息。
- 首屏留白改成上紧下松，介绍文字末行不再贴着绿色区域下沿。
- 导航跟随滚动更新当前栏目（IntersectionObserver），点到留言板之后不会还高亮「配靶向药」。

### 测试

300 → **303 条**。新增三条守这次的口径：两种缺口的措辞必须不同、未填写不得被表述成未检测（整份输出里不许再出现「还缺的 N 项检测」）、分期「暂不清楚」时已填指标照常生效。

## 0.21.1 — 2026-09-11

把患者账号和科研账号在顶栏彻底分开。

### 问题出在哪

患者注册表单本身是对的——姓名、手机号、年龄、性别、密码、病情说明，**没有邮箱**。

出问题的是顶栏：不管在哪个门户，右上角都是**科研账号**的「登录 / 注册科研账号」。患者点了那个「登录」，弹出来的是科研表单——要邮箱、要所属机构、要至少 15 字的研究用途说明。

**很多患者只有一部手机，没有邮箱。** 那张表他填不完，而且他本来也不该去填。而患者自己的注册入口埋在页面第 08 栏，滚不到那里的人就以为这个网站不能注册。

### 改法

顶栏按门户给出各自的账号区：患者门户显示「登录 / 注册」，直接落到患者注册那一栏；科研门户显示科研的。科研那个登录按钮的文案也改成「**科研登录**」——即使被看到，也不会被误认成通用登录。

导航里那一项从「我的订阅」改成「**注册 / 登录**」：还没注册的人扫的是这两个字。

### 测试

297 → **300 条**。新增三条守着这次的错：两个门户的顶栏入口必须分开、患者注册表单里不得出现邮箱字段（连「邮箱」两个字都不该出现）、科研入口的文案必须写明是科研。

## 0.21.0 — 2026-09-11

新增靶向药设计智能体。这一层补的是「工具集」与「智能体」之间真正的差距。

### 差距不在功能数量，在编排与判断

在这之前，站里有十三个数据源，但每一个都要人自己决定查不查、什么时候查、查完自己看。工具集回答「我问的这件事，答案是什么」；智能体要回答「要弄清这件事，**需要先知道什么**；哪一步没拿到；以及基于拿到的，**该不该做下去**」。

所以这一层做三件工具台不做的事：

1. **规划。** 给一个靶点，自己排出八个数据源、并行执行，每一步都带着「为什么要查它」——一串没有理由的调用是流水线，不是计划。计划在跑之前就公开展示：一个不肯说自己要查什么的东西，没有理由被信任。
2. **判断。** 基于检索到的事实决定该不该进入候选设计。四种结论：可以做 / 缺实验结构 / 靶点太拥挤 / 不给结论。
3. **交代。** 哪个源没返回、结论因此弱在哪里，逐条点名。**静默少给一块是这一层最不能犯的错。**

### 不引入大模型生成结论

这个站最贵的资产是「每句话都能追到来源」。塞一个会编的东西进去，等于把最稀缺的东西换成最廉价的功能。这里每条依据都指回具体数据源与具体数字，判断规则写在 `src/core/agent.ts` 里可以逐条复核，也可以反对。

### 写的时候抓到两个会让它「假装在判断」的 bug

**一、按返回行数判断，等于永远给同一个答案。**

第一版数的是各适配器返回了多少行。实测发现 EGFR 与 KRAS 的 chembl/dgidb/pdb 返回行数**完全相同**（22/40/15）——那是分页上限，不是景观规模。按它判断的话，任何有数据的靶点都会被判成「拥挤」。

改成从各适配器的 summary 里读真实总量：EGFR 是 291 条相互作用、71 个已获批药、685 个结构；KRAS 是 122 / 63 / 730。这两组数字才有区分力。

**二、「没人做过」被误判成「数据库挂了」。**

判定「空」的正则把窗口限成了 8 个字，于是 DGIdb 的「没有「TSPAN6」的药物-基因相互作用记录」被归进了失败。这两者导出的结论**完全相反**：前者说明这个靶点还没人碰过，后者说明这一轮不作数。

现在三种状态分开：`ok` / `empty` / `failed`，界面上颜色也分开。

### 实测四个靶点，四种结论

| 靶点 | 结论 | 依据 |
|---|---|---|
| EGFR | 拥挤 | 291 条相互作用 · 71 个已获批药 · 685 个结构 |
| KRAS | 拥挤 | 122 / 63 / 730，ChEMBL 最高研发阶段 4（已有上市药） |
| TSPAN6 | 不给结论 | 两个化学库都没有记录——先回答能不能成药，再谈设计分子 |
| NOTAREALGENE | 不给结论 | 连靶点基本信息都没拿到 |

八个源并行，EGFR 一轮 4.5 秒。

### 测试

287 → **297 条**。其中几条专门守这次的坑：不得回到数行数、空与失败必须分开、阈值要显式写出来且说明它是工程约定而不是行业标准、一个源挂掉不能让整轮失败。

## 0.20.0 — 2026-09-11

患者注册改成六项，密码登录。反馈是「完全不对」，确实不对。

### 之前错在哪

上一版的注册要患者从 39 个癌种里勾选、再填关注靶点、再填指标取值、再上传报告。那是把「配靶向药」那条流程的复杂度搬进了注册——**注册越长，真正需要它的人越填不完**。而这些东西本来就在那条流程里，是选择题、不落盘、随时可以重填。

### 现在只问六件事

姓名、手机号、年龄、性别、密码、病情说明。一屏填完。

- **姓名**可以是化名，站里不核对。
- **年龄按岁填**，不再让患者先把自己的年龄换算成一个区间——那是把系统的方便转嫁给用户。
- **病情说明**是一段自己写的话，也是账号里唯一的自由文本。癌种从这段话里自动认出来（复用「配靶向药」同一套识别规则），认不出就空着，**不猜**；认错了可以在订阅里改——收错癌种的通知比收不到更糟。

### 密码登录，注册当场可用

改成密码而不是验证码，顺带解开了上一版的死结：验证码要么靠短信、要么靠邮箱，两条都没配时注册整个用不了。现在**不依赖任何通道，注册立刻能用**。

代价说清楚：此刻没有验证手机号，所以 `phoneVerified` 为 false，而**未验证的手机号不发短信通知**——那个号可能根本不是他的。资料页上明确写着这句话，不让人以为已经订阅上了。短信通道就绪后可以补验。

登录失败时「号码没注册」与「密码错了」给**同一句话**：分开说等于把「哪些手机号注册过」告诉任何人，而这在癌症网站上本身就是敏感信息。

### 端到端监测抓到三个真 bug

重写测试时整条路径走了一遍，抓到的都不是测试问题：

1. **姓名以明文落盘。** 记录里还留着旧设计的 `nickname` 字段，我把姓名同时写进了明文字段和加密档案。姓名（哪怕是化名）与手机号摘要放在同一条明文记录里，就构成了可关联的个人信息。已删除，并加断言扫描落盘文件。
2. **身份证号根本没被拦下。** 正则里的 `` 在编辑时变成了**真正的退格字符**（0x08），于是 `/\d{17}[\dXx]/` 永远不匹配。这次产出的是**静默失效的正则**而不是语法错误——这种才会真的上线。改成前后非数字的断言，并补了手机号一条。
3. **注册限流 5 次/10 分钟，且在校验之前计数。** 六个字段的表单，一个患者把密码确认打错 5 次就被锁 10 分钟——那是在惩罚用户而不是拦住滥用。调到 20 次，仍然拦得住批量注册。

### 其他

联系邮箱全站改为 `hujiayu2008@126.com`，项目发起人只保留胡嘉裕。

### 测试

280 → **286 条**。`patient-flow.test.ts` 走真实 HTTP：注册 → 读资料 → 改订阅 → 改资料 → 改密码 → 退出 → 旧密码失效 → 新密码登录 → 删除 → 401。

## 0.19.0 — 2026-09-11

患者注册/登录入口、邮件验证通道、联系方式与发起人更新。

### 患者注册入口

患者账号的后端在 0.17.0 就建好了，这一版补上界面：注册、验证码登录、订阅管理、删除账号。

界面上三件事是显性的：**注册是自愿的**（不注册也能用上面全部功能，注册只多一件事——新药获批时收到通知，这句话写在表单最上面而不是藏在协议里）；**两份同意书逐份勾**（「单独同意」的意思就是不能被裹进一个总的「我同意」里）；**通道没配好时入口直接说明不可用**，而不是让人填完一整张表才被拒。

### 邮件也能验证

原先只有短信一条通道，而国内短信要签名与模板审核，通常要几天——结果是注册功能建好了却用不了。

新增邮件通道，与短信**对等而不是主备**：记录里存「验证的是哪一条」，没验证过的那一条不发通知。用邮箱注册时手机号仍要填（日后接短信用），但界面会明说「你验证的是邮箱，手机号尚未验证，因此暂不会收到短信通知」。

这比「先存着以后再验」诚实得多——后者的实际效果是永远不验。一条通道都没配时注册直接关闭，不降级、不放行。

### 监测：整条路径真的走了一遍

新增 `tests/patient-flow.test.ts`，走**真实 HTTP**：发码 → 未签同意书被拒 → 注册 → 读资料 → 改订阅 → 追加报告 → 退出 → 验证码登录 → 错误验证码被拒 → 删除 → 删除后 401。

单元测试全绿而注册按钮点了没反应是完全可能的——路由没接、Cookie 没下发、字段名两头对不上，任何一处都够。这条路径必须有人整条走一遍，而那个人应该是测试，不是用户。

### 其他

- 联系邮箱全站改为 `hujiayu2008@126.com`（含 `site-config.json`、页脚、加入我们、转人工问题的收件箱默认值）。
- 项目发起人只保留胡嘉裕。
- 英文入口：顶栏语言开关旁补了「我的订阅」的中英文案；新增的患者账号栏目已纳入字典。

### 测试

276 → **280 条**。

## 0.18.0 — 2026-09-09

修好登录键，加上英文界面。

### 登录键从来就没工作过

不是「坏了」，是从一开始就没被绑上。`renderAccount()` 用 innerHTML 生成顶栏的登录按钮，而绑定用的是启动时的一次性 `querySelectorAll`——那一刻 `#account-area` 还是空的，什么都没绑到；而且每次会话变化都会重新生成一次，即便当时绑上了也会立刻失效。

改成事件委托：一次绑在 `document` 上，谁在什么时候被造出来都算数。`#research-gate` 里那两个同样动态生成的按钮也一并覆盖，`main.ts` 里的重复绑定删掉。

`tests/ui-state.test.ts` 加了两条守这一类错误：认证入口必须委托，且不得回到一次性绑定。

### 英文界面

顶栏加语言开关（中文 / EN），首次访问按浏览器语言自动选——英文访问者不该先看到一屏中文再去找开关。

**这个站的内容分两类，英文版对它们的处理不一样，这是整个设计的前提：**

- **界面骨架**（按钮、栏目名、流程说明、边界声明）是我们自己写的，翻译没有信息损失——这一层逐条对照翻译。
- **内容本身**（药物清单、临床试验、靶点与指标名）**英文才是原文**：它们来自 NCI、ClinicalTrials.gov、UniProt，中文才是本站加的翻译层。切到英文时直接显示源数据，不做二次翻译——那样反而更接近来源。

**有一类东西故意不翻**：`patient-cn.json` 里那 47 条患者语料，是逐条带来源与核对日期人工整理的中文。机器翻译会产出**没有来源背书的英文医学表述**，那正是这个站反复声明不做的事。英文界面下这类条目会说明「暂无经核对的英文版本」并给出原始来源。**承认空缺，好过给一段没人核对过的英文**——与「没有可核对来源的中文药名一律留空」是同一条口径。

### 测试

265 → **276 条**。i18n 那 9 条里有两条值得单说：英文侧不许残留中文字符（漏翻要看得见，不能混着上线）；字典里的中文必须与标记里的默认文案逐字一致（否则改了文案不改字典，切回中文时会跳字）。

## 0.17.0 — 2026-09-08

患者账号、订阅通知与静态加密。**这一版改变的是这个网站的法律性质，不只是多了几个字段。**

### 为什么

在此之前站对患者是一次性的：填报告、拿结果、走了。而癌症治疗是长期的——今天没有对应药物的靶点，明年可能就有。站里已经每天向各国药监核对获批状态，把「你关注的靶点有新药获批了」送到人手上，是这份数据唯一能直接改变某个人处境的用法。

要送达就得知道怎么联系，要判断该不该送就得知道他关注什么。这是处理这些信息的唯一目的，也是范围的上限。

### 合规机制写进了代码，不只写在文书里

手机号 + 性别 + 年龄 + 诊断 + 化验单合在一起，是一份能定位到具体个人的健康档案。昵称可以是假的，但那**不构成去标识化**——手机号本身就是强标识符。所以这一层的重点不是把字段存下来：

- **单独同意是拒绝，不是提示。** 未签《敏感个人信息处理单独同意书》与《人类遗传资源承诺书》时，注册接口直接报错，**一个字节都不落盘**。签署绑定文书版本，文书更新后旧签署不被接受。
- **静态加密。** 手机号、性别、年龄段、分期、指标、报告全部 AES-256-GCM 加密（`vault.ts`）。**密钥缺失时绝不临时生成**——那会让重启后的数据永久无法解密，比不加密更糟；没有密钥就直接关闭注册。
- **最小化，且理由写在代码里。** 只有癌种与靶点明文存，因为匹配要用；若加密，每次发通知都要解开**全表**病历，暴露面反而更大。**手机号只在真正要发送的那一行解开**，用完不留、不进日志、不进返回值。
- **手机号必须验证。** 未配置短信通道时注册直接关闭，不接受「先存着以后再验」——否则任何人都能用别人的号码把病历挂上去。验证码十分钟有效、五次上限、失败即作废。
- **删除是真删。** 记录从文件移除，会话立即失效。留一条 `deleted: true` 等于没删。
- **保留期 730 天**，按最后活跃计算，每日自动清除。「用户可删除」不能只写在文书里。
- **管理端看不到个人信息。** 统计只有总数与按癌种分布，连昵称都不返回。

磁盘实测：文件里找不到手机号、姓名、住院号、报告正文、性别与年龄段；能找到的只有 `"lung"` 与 `EGFR` 这两个匹配字段。

### 订阅通知

新药获批时通知关注该癌种或靶点的人。三条边界：**只发事实不发判断**（短信里没有上下文、没有免责声明、也没有医生，是全站最不该出现判断的地方，文案模板写死不接受自由拼接）；**只发新增且只发一次**（已发记在案，重启重跑都不重发——给癌症患者重复推同一条消息比不发更糟）；**匹配不解密**。

管理端触发默认是**试运行**，要真发必须显式传 `dryRun:false`——短信有成本，发错了收不回来。

### 短信与签名

腾讯云短信，境内、不出境。TC3 签名从 `translate.ts` 抽到 `tencent.ts`，翻译与短信共用一份——签名算法与服务无关，两处各写一遍迟早漂移，而漂移的表现只是一个 401。

### 文书

新增《[个人信息保护影响评估 · 患者账号](docs/PIA-PATIENT-ACCOUNTS.md)》——《个人信息保护法》第五十五条要求处理敏感个人信息前完成评估并留存记录。其中第 8 节列的是**代码解决不了的事**：ICP 经营许可证（收费的前提）、个人信息保护负责人、HTTPS、与短信服务商的数据处理协议、人类遗传资源许可、异地备份。

《隐私政策》《用户协议》同步更新（新增患者账号与收费两节），版本升至 `2026-09-08`。

### 测试

246 → **265 条**。新增 19 条全部对应一件法定义务：未签同意不落盘、旧版本签署被拒、磁盘无明文、视图不含报告正文、匹配不解密、删除是真删、删除后会话失效、保留期到期清除、统计不泄露个人信息、无短信通道时不假装可用。

## 0.16.0 — 2026-09-08

三维分子结构、加入我们、页脚运营主体。

### 三维分子结构

科研工作台会输出候选分子的 SMILES，但一串 `N(c2ccc(NC(=O)C=C)cc2)c1nccc(CC)n1` 对空间直觉毫无帮助。现在可以直接看它长什么样：球棍模型，拖动旋转，键盘方向键也能转。

服务端用 RDKit 嵌构象（ETKDGv3 + 力场优化），**固定随机种子**——同一个 SMILES 每次得到同一份坐标，和引擎「记录种子、结果可复算」是同一条口径。一张每次刷新都不一样的分子图没法拿去和别人对照。

浏览器端**用 Canvas 2D 自己画，不引任何三维库**。这个仓库没有 node_modules，从 CDN 拉一个三维引擎会同时打破零依赖，以及「境外 CDN 对这个站的用户未必可达」。球棍模型需要的其实很少：旋转矩阵、透视除法、按深度排序从远到近画。

两条边界写进了渲染：**只呈现几何**，不叠加任何评分或颜色编码的「好坏」——一张图太容易被读成结论；**构象是算出来的**，结果区常驻一句说明，讲清这不是实验测定的结构，分子在体内的真实构象取决于环境与结合对象。

服务端实测：阿司匹林 C9H8O4（21 原子 21 键）、伊马替尼 C29H31N7O（68 原子 72 键，分子量 493.62）。无法解析的 SMILES 如实拒绝，不返回假坐标。

### 加入我们

留言板右侧新增栏目，面向投资、科研合作与产品渠道。

里面写明了一件事：**本站不接受药品、医疗器械、保健食品、诊疗服务与就医推荐类投放**。《广告法》对这几类有明确限制，非医疗机构不得发布医疗广告；而且这类内容一旦出现在患者页面上，「不荐药、不推荐医院」的立场就没有了。品牌合作与非医疗类推广可以谈。

### 页脚运营主体

页脚新增运营主体与联系电话，内容来自 `site-config.json`，不在标记里写死。标明主体是《互联网信息服务管理办法》的要求，也是让访问者知道出了问题找谁。备案号字段已留好，填了才显示——这里不该出现一个编出来的号。

### 测试

235 → **246 条**。新增 11 条：SMILES 校验（含 shell 元字符在进子进程之前就挡掉）、桥接脚本永不抛异常、构象可复现、失败不返回假坐标、渲染层不引三维库、说明必须显示、不叠加评分、尊重减少动态效果且键盘可用。

## 0.15.1 — 2026-09-08

服务器装上 RDKit，标准档与深度档解锁。顺带修好一个会骗人的能力探测。

### 装了什么

`/opt/zhansheng/.venv` 里装 RDKit 2026.03.6（Python 3.14 有 cp314 轮子，走清华镜像）。`resolvePython()` 本来就会自动找 `ROOT/.venv/bin/python`，所以不用配环境变量。

### 探测说「可用」，任务却会在 51 秒后崩

装完之后启动日志写着「RDKit 已安装（标准/深度档可用）」，于是我跑了一个真实的标准档任务验证——**算了 51 秒，在渲染分子结构图时崩了**：

```
ImportError: libXrender.so.1: cannot open shared object file
```

`import rdkit` 不需要 X11 共享库，但 `rdkit.Chem.Draw` 需要，而报告里的结构图走的正是它。原来的探测只做了 `import rdkit`，所以它**承诺了一个会在任务跑到一半时失败的档位**。

补了 `libxrender1 libxext6 libsm6`，重跑通过：51 秒出候选分子（SMILES、评分、分子量、cLogP）、报告、结构图 SVG 与机器可读结果。

### 探测改成测「真正会用到的东西」

现在会实际嵌一个 3D 构象、加载一次绘图模块，任何一步失败都算这一档不可用；并且**把失败原因带到界面上**——「装 RDKit」这句话对一个已经装了 RDKit、只是缺 libXrender 的部署毫无帮助。

这类问题的共性是：探测越省事，它越容易在「能力其实不可用」的时候说可用。宁可探测慢 200 毫秒，也不要让人等 51 秒才发现。

## 0.15.0 — 2026-09-08

新增备份机制。这是此前整个部署里唯一没有任何保护的一环。

### 在此之前：零备份

上线前查了一遍服务器：没有 crontab、没有 systemd timer、没有快照任务。`.runtime/` 里存着账号、同意签署记录、留言板与知识复核签署，**一次磁盘故障就全没了**。其中同意签署记录最要紧——丢了就无法证明用户签署过什么，这在合规上很难解释。

### 备份什么

只备不可再生的：`.runtime/`、`data/knowledge/`（含爬不回来的中国获批与医保人工登记）、`data/consents.json`。合计 1.7 MB，所以可以每天全量、保留很多份。

翻译缓存也在其中——它是对着一个有日配额的免费服务一点点攒出来的，重建要花很多天。

### 三条设计约束

- **先验证再算数。** 打完包立刻读一遍清单，并确认关键目录确实在包里（空包也能通过 `tar -tzf`）。校验不过就删掉坏包，**并且不清理旧备份**——宁可占着磁盘，也不能拿一个坏包换掉好包。
- **失败必须看得见。** 每次写状态文件，管理端新增「备份状态」面板。从没跑过、上次失败、超过 48 小时没有成功记录，三种都按告警显示。备份最常见的死法就是「悄悄失败了半年」，而「从没跑过」和「跑成功了」必须区分得开。
- **说清楚它挡不住什么。** 同盘备份挡得住误删和写坏，**挡不住磁盘故障**。这句话写在脚本里、README 里和管理端面板上，不留给读的人自己想。

### 恢复演练

`scripts/restore-drill.sh` 在临时目录里解开最新的包，逐项核对关键文件能不能读、结构对不对，不碰生产数据。首次演练已通过：药物清单 39 个癌种、获批核对 283 条、翻译缓存 174 条、文书 10 份。

**没有验证过能恢复的备份不算备份**——备份最常见的失败不是没跑，是恢复的时候才发现包里是空的。

### 写脚本时踩到的一个坑

`tar -tzf "$ARCHIVE" | grep -q "^$must"` 会误报失败：`grep -q` 命中后立刻退出，`tar` 收到 SIGPIPE 以非零结束，在 `pipefail` 下整条管道被判失败——**备份是好的，脚本却报错并删掉了它**。改成先把清单读进变量再匹配。

### 测试

227 → **235 条**。新增 8 条：三种失败状态各一条，加上脚本本身的契约（清理必须排在验证之后、备份范围覆盖全部不可再生数据、失败时也写状态、同盘局限要写明）。

## 0.14.1 — 2026-09-07

重做「关于我们」。

原来这一栏是一个圆圈套着「战」字，加三个飘着的词（公开来源 / 标注边界 / 持续更新）。圆圈套一个字是通用装饰，和这个项目没有关系；三个词也只是词，背后没有东西。

更要紧的是：小标题写着「谁在维护这个站点、依据什么、**以及我们明确不做什么**」，而下面的内容里**根本没有最后那一段**。

换掉图标的不是另一个图标，是内容：

- **本站由什么构成**——癌种数、药物与联合方案数、公开数据源数，全部当场从站内接口取回。这一栏讲的是「这个站的结论可以核对」，那它自己就不该是一串手写的数字：手写的数字会随着数据增长慢慢变成假话。测试会拦住把数字写死进标记的做法。
- **我们怎么做**——原来那三个词展开成有依据的句子：核对日期到期会自己回队列、链接每天检查、拿不准的中文药名一律留空。
- **我们明确不做**——补上小标题承诺了却一直缺失的那一段：不诊断、不推荐用药、不预测预后、不推荐医院和医生。

顺带修了一处会自相矛盾的地方：数字旁边写着「每 24 小时重抓」，就得敢报上一次成功更新是什么时候。接口本身带 `stale` 标志，过期了就直说过期——写着每日更新、旁边挂着一个三天前的日期，比不写更糟。

测试 222 → **227 条**。

## 0.14.0 — 2026-09-07

新增留言板：患者提问，科研人员回答。以及首屏改成绿色。

### 留言板

站里原本只有一条单向通道——答不上来的问题进队列、转邮件、由维护者补进语料。那条通道服务的是**语料**，不是提问的人：他问完就走，永远不知道有没有下文。留言板补的是这一段。

**边界写在代码里，不只写在文案里：**

- **急症提问当场挡下，不落库、不排队。** 让一个正在出血的人等论坛回复是说不过去的——这条比「互动」重要。页面直接给急诊指引。
- **涉及个人病情的提问收，但挂牌。** 一律拒收会让留言板对最需要它的人变成空的。所以照收，但整条帖子常驻提醒，回答框里再提醒一次：这类问题网上判断不了，能给的是通用信息和「该去问医生什么」。
- **会被公开出去的身份信息一律拒收，不打码放行。** 身份证号、手机号、住院号、邮箱、社交账号——拒收时说清是哪一类，让人改了再发。打码等于替提问者决定「这样就够安全了」，而他本来可以换一种写法。
- **回答按资质署名，不按个人署名。** 显示角色与机构（「认证机构用户 · 某某肿瘤医院」），不显示姓名。回答的分量应当来自资质，而不是来自某个人。

**权限**：新增 `board.answer`（机构认证起）与 `board.moderate`（专家复核员起）。仅注册的科研用户不能公开回答患者——公开回答的分量要有个可核对的来源。能回答不等于能审核别人的回答。

**先审后发**：留言板公开、提问匿名，没有别人能替提问者把关。管理端新增「留言板审核」，可以公开、隐藏整条提问，也可以只隐藏越界的那条回答——撤掉提问等于惩罚了提问的人。

**提问匿名的代价**：不记 IP、不记账号、不记会话，所以我们无法确认某条留言是谁发的，**提问者自己也删不掉**。这一点在提交框里就写明了，不藏在隐私政策里。

### 文书

这是全站第一个公开的用户生成内容，《用户协议》《隐私政策》《医疗免责声明》都必须跟上：

- 用户协议新增 4.2 节，写明公开性、禁止内容、回答者义务与处置规则；
- 隐私政策写明留言**会公开显示且无法删除**；
- 医疗免责声明新增 3.3 节：回答者是科研人员、**不必然是执业医师**，署名里的机构表示所属单位，不表示临床诊疗资质；阅读或回答均不建立医患关系。

三份文书版本升至 `2026-09-07-2`（同日第二次修订），已签署的用户下次触发对应闸门会重新确认。

### 首屏改成绿色

首屏那条横栏从近黑改成绿——用的就是全站的 `--accent` 这支深青绿，不是另配一个绿。标题改为「为你找到适合的靶向药」。

改完发现眉标的暖橙色在绿底上对比度只有 **1.93:1**，副标题 3.85:1，都读不出来。实测后眉标换成暖调浅色（5.18:1）、副标题调亮（4.89:1）、主标题 5.41:1。本站有老年友好的放大字号，字放大了却读不清是硬伤。

### 测试

208 → **222 条**。新增 14 条全在守留言板的边界：急症不落库、身份信息拒收、个人病情挂牌不拒收、公开视图不含 authorId、待审帖子不外露、回答与审核是两种能力。

## 0.13.0 — 2026-09-07

改版一期：患者门户从「一张长页面并列八个板块」改成三屏；科研侧把数据后盾摊开。

### 为什么要改结构，而不只是改文案

反馈是「文字太多，分散注意力」。去数了一下：整个网站是一张长页面，患者门户 8 个板块、科研门户 9 个，靠滚动并列。用户每往下滚一屏，就要重新判断一次「这一块是不是给我的」——**判断本身就是成本**，而刚拿到病理报告的人此刻判断力最稀缺。

所以改的不是删字，是把并列改成串行：主干只有一条路径，一屏一件事，一件事一个交互。

### 患者：三屏

1. **交报告**——拖文件、粘贴文字，或者手动填写。
2. **核对**——机器读到的预填进指标表单，患者对照报告改。
3. **拿结果**——药物、缺口、问题清单、试验，可打印带去门诊。

第二屏不是新建的一张确认表，**就是原来那张指标表单**，只是被填了一部分并标出了依据。这样「一个指标有哪些取值」只有一处定义，识别再怎么变，患者看到的和服务端校验的永远是同一份词表。

原来的治疗路径、知识库、临床试验检索、权威入口四栏**折进「更多」**——是折起来，不是删掉，需要时仍然找得到。

### 报告识别：三条硬约束

- **原件不出设备。** `src/client/report.ts` 不发任何请求，也不引任何会发请求的东西；离开浏览器的只有 `her2=positive` 这样的结构化取值。姓名、住院号、身份证号没有出口。合规状态与改版前一致。
- **判读口径来自词表，不是代码里发明的。** HER2 的「IHC 3+ 或 FISH 扩增为阳性」、Ki-67 的「通常 >20–30%」都写在 `biomarkers.json` 的选项标签里，识别只是照着执行。
- **宁可漏认，不可错认。** HER2 2+ 而报告没写 FISH 时不下结论；Ki-67 落在 20–30% 区间内交给患者判断。人对「已经填好的东西」的核对是很弱的，所以拿不准就留空。

一期只接文本类报告（粘贴、txt/csv/tsv、基因检测导出文件）。PDF 与照片走「手动填写」，二期再补。

### 科研：数据后盾

新增 `GET /api/sources`，把散在四份注册表里的上游汇成一张清单：**23 个公开数据源**——靶点与通路 6、药物与化学 3、变异注释 1、文献 2、临床试验 1、药品监管 9、药物清单 1。每条写明它回答什么问题、什么许可、服务哪一端。

按原样列出，不打分不排名；也不在这一层探活——「现在通不通」由适配器在真实调用时报告，为一个说明性页面去定时敲打十几个免费服务是给上游添麻烦。

### 修复：连接策略放错了地方

0.12.1 把跨境连接预算写在了 `server.ts` 里。那是错的：脚本、测试和任何别的入口都拿不到它，于是**同一份适配器在服务端能跑、在脚本里必崩**。实测 PubMed 与 PubChem 就是这样——不设预算时 `fetch failed`，设了就 2 秒返回；根因是 `eutils.ncbi.nlm.nih.gov` 的握手耗时 254 毫秒，正好压在 Node 250 毫秒的默认值上。

策略移到 `src/core/net.ts`，由每个会对外发请求的模块显式导入。`tests/net.test.ts` 逐个模块检查——新增一个会联网的模块却忘了导入，测试会指名道姓。

### 测试

181 → **208 条**。新增的都在守这次真会犯的错：报告识别 16 条（含「不跨行串项」「产出取值必在词表内」「模块不联网」）、三屏结构 8 条、连接策略 3 条。

## 0.12.2 — 2026-09-07

翻译多一条路：腾讯云机器翻译。顺带把「能不能用谷歌」这个问题查清楚并写进文档。

### 谷歌翻译：不能，而且不是配置问题

在部署机上实测过三个入口——免费的 `translate_a`、官方 Cloud Translation API、`clients5`——全部不可达。

值得说清楚失败的形状，因为上一个版本刚修过一个**长得很像但完全不同**的 bug：

- DNS 能解析，而且拿到的是真实的 Google IP（142.250.69.170）；
- TCP 443 握手 **30 秒内没有任何应答**，`connect=0.000000s`；裸 `/dev/tcp` 测试同样不通；
- `www.google.com` 解析到 174.132.167.252——这不在 Google 的地址段里。

0.12.1 修的那个是「每次尝试只给 250 毫秒，来不及握手」；这次特意用 30 秒宽超时复测，**包发出去没有任何回应**。换成官方付费接口也一样，墙不区分你付没付钱。让患者浏览器直连同样不行——用户也在墙内。

### 新增：腾讯云机器翻译

配 `ONCOFORGE_TENCENT_SECRET_ID` 与 `ONCOFORGE_TENCENT_SECRET_KEY` 即启用。TC3-HMAC-SHA256 签名只用 `node:crypto` 写，为一种请求形状引一个 SDK 不划算。

除了同区域延迟低，还有一个 `docs/DEPLOYMENT-COMPLIANCE.md` 第 4 节点过名的好处：**这一次调用不出境**。试验标题是公开登记文本、不含个人信息，但少一处出境就少一处要解释的地方。

签名是最容易静默出错的一段——改错了不会崩，只会一直 401。所以做了两层验证：

1. 规范化请求的**确切形状**被测试钉死（请求头顺序、大小写、结尾换行、载荷哈希）；
2. 用一个格式合法但不存在的 SecretId 打了线上接口，返回 `AuthFailure.SecretIdNotFound`——说明端点、Action、Version 与整个 Authorization 结构都被接受，只有凭据查找失败。

第 2 步够不到 HMAC 推导链：SecretId 不存在时根本没有密钥可以验签。**推导若有错，会在第一次带真实密钥调用时以 `AuthFailure.SignatureFailure` 暴露**，代码里的注释如实写了这一点。

## 0.12.1 — 2026-09-07

临床试验那一层其实一直是通的，是网络把它掐了。顺带把站点挪到子路径下。

### 修复：所有境外数据源的连接在这台服务器上必然失败

界面上的表现是「临床试验检索本次没有完成：fetch failed」。查下来不是 ClinicalTrials.gov 的问题，也不是代码的问题：

Node 的 Happy Eyeballs 给**每一次连接尝试** 250 毫秒，超时就换下一个地址。从中国大陆的服务器连美国主机，TCP 握手根本不可能在 250 毫秒内完成——于是每个地址都被判超时，整个请求以 `ETIMEDOUT` 结束。在部署机上量过：250ms 五次全败，耗时约 300ms；放宽到 2000ms 五次全成，耗时约 1.2 秒。`curl` 一直是好的，因为它自己实现了退避。

修法是把这个**连接尝试预算**调到 2000ms（`ONCOFORGE_CONNECT_ATTEMPT_MS` 可改）。这不是请求超时——每个请求仍有自己的 `AbortSignal`。受影响的不止试验检索：NCI、openFDA、EMA、Europe PMC 走的是同一条路。

### 新增：试验标题的中文

登记标题全是英文。`translate.ts` 早就为「最新研究进展」写下过这个判断——「对中国患者来说，英文标题和没有标题是一回事」——试验栏一直没跟上。

现在跟上了，规则和新闻栏一致：**译文永不替换原文**，英文原题就排在中文下面，并标注这是机器翻译，一个被译错的肿瘤学术语能一眼核对回去。

试验和新闻有一点不同：新闻在后台刷新，试验是有人盯着转圈时实时取的。所以请求路径**只读翻译缓存，不发翻译请求**——患者一秒都不为翻译等待；没命中的标题在响应发出后再后台补，同一个癌种下次检索就有中文了。缓存是永久的。

翻译方失败时（免费额度用尽是最常见的）后台预热退避 30 分钟，不让每次检索都去撞一遍——这是对免费服务应有的礼貌。

### 变更：站点挂到 `/conquer` 下

一台服务器要放多个站点，所以本站不再占用域名根：

- 新增 `ONCOFORGE_BASE_PATH`，应用自己剥掉前缀，路由代码不用逐条改；
- 页面里的静态资源引用全部改成文档相对路径，浏览器端的接口地址由 `import.meta.url` 反推站点根——一份构建产物在根和子路径下都能用，不需要构建时注入；
- 会话 Cookie 的 `Path` 收敛到挂载点，同机上的另一个站点不会覆盖它；
- `/conquer`（不带斜杠）301 到 `/conquer/`，否则相对路径解析到上一层，整站资源 404；
- 前缀之外的地址一律 404，留给别的站点。

### 新增：`ONCOFORGE_TRUST_PROXY`

挪到反向代理后面之后，应用看到的来源 IP 全是 `127.0.0.1`——限流会塌缩成一个全局桶，同意记录里也会留下一个没有意义的地址。开启后从 `X-Forwarded-For` 取最左一跳。

**默认关闭**，而且必须默认关闭：这个头任何调用方都能伪造，直接暴露在公网的服务端只能相信 socket 地址。

## 0.12.0 — 2026-09-07

去掉症状分流。患者门户只剩一条路径：你已经确诊了，把报告填进来。

### 为什么

来这个站点的人几乎都是已经确诊的。分流回答的是「我要不要去医院」——那是确诊之前的问题，而拿到病理报告和基因检测报告、满纸缩写没人解释的那一刻，才是这个站点真正能帮上忙的时候。

原来两者是首页第一栏里并列的两个 tab，**默认选中的偏偏是分流**。已确诊的人进来，看到的是一张勾选症状的表，要先意识到「这不是给我的」，再点一下第二个 tab，才够得着真正有用的东西。中间这一步没有为任何人创造价值，只是把最需要帮助的人挡了一下。

### 改了什么

- 首页第一栏不再是选项卡，直接就是报告输入表，打开页面即可填，癌种清单随页面一起加载，不用等点击。
- 首屏文案换成确诊之后的处境；顶部「就医路径概览」从「发现异常 → 规范检查」改为「读懂报告 → 补齐检测 → 看清方向 → 共同决策 → 疗效随访」。
- 主按钮从「开始症状分流」改为「填写我的报告」，直接落到癌种选择框。
- 删除 `src/core/triage.ts`、`POST /api/triage`、OpenAPI 条目、前端表单与相关测试。**这个接口不再存在，调用会 404。**

输出没有变，因为它本来就是具体的：报告逐项翻译、与你的靶点对应的已获批药物、报告里可能还缺的检测、可复制的门诊问题清单、急症警示、相关的在招试验。变的只是拿到它需要几步。

### 隐私模型不变

输入仍是**封闭词表**——癌种、分期、指标取值、既往治疗、当前困扰全部是从固定列表选的标识符，没有任何自由文本框，也就没有姓名或住院号能填进去的地方；服务端同样按词表校验，不落盘。

### 文书

《医疗免责声明》删去「症状分流工具的性质」一节（原第 3 节），「确诊后导航的性质」升为第 3 节。《用户协议》《隐私政策》《Cookie 说明》《人工智能生成内容声明》中提及分流的条目一并移除。这五份文书的版本号升至 `2026-09-07`，已签署的用户下次触发对应闸门时会重新确认。

## 0.11.0 — 2026-09-03

科研工作台：修掉两处真的坏了的地方，并加上一整层科研工具。

### 先说坏在哪

报告说「使用引导不工作、提交任务没有输入框、任务工作区不工作、结果与证据不工作」。查下来是两件事：

1. **整个工作台在登录闸门后面**。未登录时 `#research-workbench` 是 `hidden`，而科研导航仍然列着六个入口——点了什么也不会发生，读起来就是「全都坏了」。后端其实是好的：我建了账号跑通了完整流程（提交 → 排队 → 完成 → 取结果，219ms）。
2. **「使用引导」确实是死的**。它只在首次访问自动弹出，之后永久隐藏；代码里监听了一个 `#open-onboarding` 元素，而这个元素在 HTML 里根本不存在。所以第二次之后点它必然没反应。

修法：未登录时隐藏指向工作台内部的导航项，并在闸门下方列出登录后能用什么；「使用引导」改为直接绑到那个一直缺失的触发器上，随时可以重新打开。

### 新增：科研工具台（13 个公开数据源）

| 分类 | 数据源 |
|---|---|
| 靶点与通路 | Open Targets、UniProt、AlphaFold DB、RCSB PDB、Reactome |
| 药物与化学 | ChEMBL、PubChem、DGIdb |
| 变异注释 | MyVariant.info（ClinVar / CADD / SIFT / gnomAD） |
| 文献与试验 | Europe PMC、PubMed、ClinicalTrials.gov |

**靶点研究简报**是综合入口：输入一个基因，并行调用六个源，出九个板块——基本信息、可成药性、疾病关联、安全性信号、结构域、实验结构、已知药物、高被引文献、在研试验。实测 `KRAS` 一次返回全部九块。某个源没返回时明确写出是哪一个，不悄悄少给。

服务端把所有数据源归一化成「若干段 × 列 + 行」，所以加一个源是写一个适配器加一条注册表记录，前端不用动。结果缓存 12 小时，可「重新获取」跳过；可导出 TSV，**导出文件里带着数据源、获取时间与许可**——这页上做的事可能会进论文。

口径与全站一致：按原样呈现，不改写、不合并、不给结论。

### 边界

工具台需要登录并限流（每账号每分钟 60 次）。每次查询都是一次对免费公共服务的外发调用，开放给匿名访问等于把本站变成它们的代理。

### 一处踩坑

Open Targets 的 GraphQL 接口在没有浏览器形态 User-Agent 时直接返回 403，且 v4 的 `Target` 类型上没有 `knownDrugs` 字段——是先做了一次 schema introspection 才把查询写对的。这类事没有捷径，只能问接口本身。
## 0.10.0 — 2026-09-03

六项：各国获批、字号、手机适配、复核机制、联系与转人工、研究进展中文翻译。

### 药物层：从「美国已获批」到逐国核对

| 国家 / 地区 | 方式 | 当前已获批 |
|---|---|---|
| 美国 FDA | openFDA 接口，每日自动 | 263 / 283 |
| 欧盟 EMA | 每日公开药品清单（xlsx），每日自动 | 184 |
| 加拿大 Health Canada | 药品产品数据库接口，每日自动 | 227 |
| 中国 NMPA | **人工登记** | 0（待补） |
| 日本、英国、澳大利亚、瑞士、韩国 | 无公开接口 | 显示「未核对」+ 官方查询入口 |

EMA 那份清单是 xlsx，为此写了个最小 ZIP + SpreadsheetML 读取器（`node:zlib` 足够），只取状态为 Authorised 的品种——已撤回、被拒的不计入。

中国这一项我没有自动化，也**没有凭印象填**：药监局、药审中心与医保局都设了访问拦截。`data/knowledge/approvals-cn.json` 是人工登记表，每条要求写明批准文号、是否进医保、核对日期与核对人。未登记的一律显示「未核对」，并给出 NMPA 检索入口。**未核对不等于未获批**，页面直接这么写。

新增国家是改 `data/knowledge/jurisdictions.json`：auto 的加一个探针，manual 的只要一个官方检索链接。

### 老年友好：字号三档

全站 295 处 px 字号改为 `calc(Npx * var(--ts))`，一个自定义属性带动整站等比放大（行高是无单位倍数，自动跟随）。放大到大档以上时按钮与导航项最小点击高度升到 44px。选择存在浏览器本地，不上传、不关联账号。同时支持 `prefers-contrast: more`。

### 手机：按屏幕适配，不做机型识别

用户要求「自动监测手机型号」——这一条我按屏幕宽度实现，**没有做 UA 机型识别**。理由：UA 字符串可以伪造、新机型层出不穷，按型号分支永远追不完，而视口宽度是可靠的。720px 以下顶栏换行、入口切换占满一行、多列网格收单列、点击目标不小于 44px；420px 以下表单与筛选也收单列。

### 复核机制

两个独立信号都会把条目放回队列：**到期**（周期按内容变化速度分级：治疗与检测 60 天 / 副作用 90 天 / 就医准备 120 天 / 基础概念与预防 180 天），或**来源失效**（每天检查全部引用链接，含 NCI 用 200 返回的「Page Not Found」软失效）。逾期超 30 天升级为「欠账」。

管理端新增「知识复核」：待办队列、一键重查链接、签署复核。签署记录（谁、何时、说明）写入运行目录，内容仍以版本化数据文件为准。启动与每日检查在日志里报告待处理数量。

上一版我算过：47 条语料原本会在 11 月底集体到期。现在分级之后到期时间被摊开，且到期会有人被告知。

### 联系我们 + 答不上来转人工

关于我们新增联系区块，邮箱 huhaixin@msn.com。问答答不出来时，问题原文进入本机队列并转发到该邮箱。

- 转发的只有问题本身、未作答原因与时间；**不含 IP、账号、会话、浏览记录**。
- **涉及个人病情的提问不转发**——这类问题本站按设计拒答，转给别人既回答不了，又把健康信息送了出去。页面明确说明这一点，并给出「再提交一次」按钮供读者自行决定。
- 提问框上方常驻提示：不要写姓名、电话、住院号。
- 邮件尽力而为：没配 SMTP 或发送失败，问题仍在队列，管理端可重试。不会因为邮件服务器出问题丢掉一个问题。
- SMTP 客户端自己实现（`src/core/mailer.ts`，465 隐式 TLS 与 587 STARTTLS、AUTH LOGIN/PLAIN），项目仍然零运行时依赖。

### 研究进展：中文翻译

英文标题与摘要要点自动译成中文展示，**原文与原文链接始终并列**，每条标注翻译来源与「术语以原文为准」。译文永久缓存，同一条只翻一次；每轮限量，超出的下一轮继续。默认走 MyMemory 公开接口（只发送公开的标题与摘要），可换自建/商业接口或关闭。中文标题同时进入检索，中文关键词也能搜到英文源的条目。

### 隐私与文书

转发问题意味着内容离开本机，这是实质变更：隐私政策新增 3.2 节，Cookie 说明新增字号偏好一节，第三方声明新增获批数据来源与机器翻译两节；三份文书版本提升至 2026-09-03，现有用户下次会重新确认。

### 修正

- `queryName` 取最短的 ASCII 名称，导致商品名压过通用名（`Tagrisso` 短于 `Osimertinib`），三个接口几乎全部落空。改为只从通用名推导，并剥离生物类似药的四字母后缀。修正后美国命中率从 38/283 升到 263/283。

## 0.9.0 — 2026-09-02

四件事：指标覆盖补齐、中文药名的诚实解法、确诊后导航接入研究进展、就医准备可打印可导出。

### 指标覆盖 22 → 39 个癌种（全覆盖）

补上皮肤癌、软组织肉瘤、骨癌、间皮瘤、神经母细胞瘤、视网膜母细胞瘤、横纹肌肉瘤、睾丸癌、肛门癌、阴茎癌、阴道癌、外阴癌、卡波西肉瘤、妊娠滋养细胞疾病、嗜铬细胞瘤与副神经节瘤、多中心型 Castleman 病，以及**泛实体瘤**（不限癌种获批的 MSI-H / TMB-H / NTRK / RET / BRAF / HER2）。指标条目合计 113 条。

几个刻意的取舍：睾丸癌把「生育力保存」做成一项必答指标，因为精子冻存必须在治疗开始前完成，错过无法补救；嗜铬细胞瘤强调术前药物准备，这是本病与多数肿瘤最不同、也最危险的一点；卡波西肉瘤把 HIV 状态列为核心，因为抗逆转录病毒治疗本身就是治疗手段而非附加项。

38 个权威链接**逐一发请求验证**，全部 200。

### 中文药名：不造词

340 种药里有 23 种没有可核对的中文通用名。国家药监局、药品审评中心与药物临床试验登记平台全部有 WAF 拦截，无法程序化核对——**所以这一版没有新增任何中文通用名**。

改为给这 23 种各写一条中文**说明**（descriptor）：`Datopotamab deruxtecan-dlnk` 显示为「靶向 TROP-2 的抗体偶联药物｜暂无可核对的中文通用名，此处为英文通用名」。说明的是它属于哪一类药、作用于什么靶点，而不是它叫什么。对患者来说，这比一个没人用过的音译名更有用，也不会造成错误引用。

### 确诊后导航接入研究进展

填了乳腺癌 HER2 阳性，结果页会带出该癌种的近期公开进展：监管审批排最前，其次临床试验，提到你报告里靶点的条目越级上升；每条标注「因 breast cancer、HER2 与你相关」并链接原文。

`newsForTopic()` 的匹配规则：中文按包含匹配；英文按词边界匹配；**长度 ≤5 的全大写别名按大小写敏感匹配**——`ALL` 是急性淋巴细胞白血病，也是英语里最常见的词，`GIST` 同理。实测把白血病的误匹配从 28 条降到 20 条，间质瘤的误匹配清零。

### 就医准备可打印、可导出

- **打印 / 存 PDF**：`@media print` 只保留结果本身，页眉自动加上「本页为健康教育信息，不构成诊断或用药建议」，链接在打印时展开为完整 URL，急症区块加粗描边，问题清单禁止跨页断行。
- **下载文本**：带 BOM 的 UTF-8 `.txt`，Windows 记事本直接可读，文件名为「就医准备-乳腺癌-2026-09-02.txt」。
- **复制清单**改为复制完整的就医准备（解读、药物、缺口、问题、下一步、急症、进展），不只是问题。

### 修正

- NCI 页面解析时用未归一化的 slug 做比较，`retifanlimab-dlwr` 因此把商品名 `Zynyz` 当成了通用名。slug 现在与标签同样归一化后再比较，`zoledronicacid → Zometa` 等同类错误一并修正。

## 0.8.0 — 2026-09-02

「症状与就医分流」改为**症状分流 · 确诊后导航**。已经确诊的人，从这一栏开始就有东西可用。

### 起因

原来的分流只服务「还没确诊、不知道要不要去医院」的人。可是拿到病理报告和基因检测报告、满纸 ER / HER2 / EGFR / MSI 却没人解释的那一刻，恐惧最重——那时候这个站点什么也给不了他。

### 新增：确诊后导航

栏目分成两条路径。「我已经确诊了」这条路上，患者对着自己的报告做选择题，得到六样东西：

1. **报告解读** —— 每一项翻译成普通话，说明它通常与哪一类治疗相关。
2. **和你结果相关的药物** —— 在已获批用于该癌种的药物里，筛出作用于你报告中靶点的那些，按类别分组，逐条标注依据来自你填的哪一项。
3. **报告里可能还缺的检测** —— 这一项最容易被忽略。患者常常不知道某项检查根本没做过，而多数免疫组化可以用原有蜡块补做。
4. **可以直接念出来的问题清单** —— 由缺口、分期、既往治疗和当前困扰生成，可一键复制。
5. **急症警示** —— 化疗后发热 38℃、免疫治疗期间持续腹泻等，什么时候必须立刻去医院。
6. **对应困扰的支持内容** —— 从患者语料里按所选困扰调取。

### 新增：`data/knowledge/biomarkers.json`

22 个癌种的关键检测指标：乳腺癌 ER/PR/HER2/Ki-67/BRCA/PIK3CA、肺癌 EGFR/ALK/ROS1/KRAS 及其他驱动基因、结直肠癌 RAS/BRAF/HER2、胃癌 HER2/CLDN18.2、肝癌 Child-Pugh/乙肝/AFP、前列腺癌 Gleason/去势抵抗/HRR/PSMA、胶质瘤 IDH/MGMT/1p19q、白血病与骨髓瘤的分型与细胞遗传学等。MSI/MMR、TMB、NTRK、PD-L1 作为跨癌种共享指标，未收录的癌种回落到这一组。

每项都写明：这项是什么、报告上通常出现在哪、为什么重要、每个取值意味着什么、以及它对应哪一类治疗方向。

### 边界

这一栏不诊断、不推荐用药、不排序方案、不预测预后。药物清单的措辞是「已获批用于这个癌种，且作用于你报告里的靶点」——这是药理学上的对应关系，不是「你该用它」。测试里有一条断言直接扫描输出文本，禁止出现「推荐使用」「首选方案」「生存期」「治愈率」这类词。

### 隐私

输入是**封闭词表**：癌种、分期、指标取值、既往治疗、当前困扰，全部是从固定列表里选的标识符，没有任何自由文本框，也就没有姓名或住院号能填进去的地方。服务端校验同样按词表进行，不在词表内一律 400。与症状分流一致，不落盘。

### 修正

- 靶点匹配原先用子串比较，`EGFR` 会命中 `VEGFR-2`，导致 EGFR 突变的肺癌患者看到抗血管生成抗体。改为按词元匹配并允许前缀（`IDH` 仍能匹配 `IDH1 / IDH2`），加了两条回归测试。
- 多发性骨髓瘤的 CD38/BCMA 条目只有一个选项，作为「选择题」不成立，内容并入该癌种导语；霍奇金淋巴瘤补上 CD30 阴性（结节性淋巴细胞为主型）选项。

### 接口

`GET /api/careplan/form?cancer=`、`POST /api/careplan`，均为公开接口并限流。已写入 OpenAPI。

## 0.7.0 — 2026-09-02

知识库扩容：新增按癌种可查的**已获批药物清单**（39 个癌种、340 种药物与方案），患者语料由 10 条扩到 47 条，并把这两层都接入每日更新。

### 起因

上一版被问到「治疗乳腺癌适用的药物有哪些」时只能拒答——语料里根本没有癌种维度的用药信息。补的不是几条文案，而是一整层数据和它的更新机制。

### 新增：药物知识层

- `src/core/pharmacopeia.ts` + `data/knowledge/cancer-types.json` + `data/knowledge/drug-names-cn.json`。
- **清单本身不是我们编的**：直接取自 NCI 维护的「Drugs Approved for <癌种>」页面，每天重新抓取一次。本站加的是中文层——通用名、药物类别（靶向/抗体/ADC/免疫/细胞治疗/内分泌/化疗/放射性药物/联合方案/支持用药）与作用靶点。
- 覆盖 39 个癌种、340 种药物与联合方案，其中 317 种有经过对照的中文通用名。
- **没把握的中文名一律留空**，页面显示英文通用名并注明「暂无中文通用名对照」。音译臆造一个药名比承认空缺更糟。
- 单个抓取失败不会清空已有清单：旧数据保留并带自己的时间戳，页面据实显示更新时间。

### 新增：检索意图

`/api/knowledge/ask` 现在能分辨三类问题：

| 提问 | 结果 |
|---|---|
| 「治疗乳腺癌适用的药物有哪些」 | 按类别分组的 54 种已获批药物，标注靶点与来源 |
| 「曲妥珠单抗是什么药」 | 药物卡片：类别、靶点、商品名、已获批用于哪些癌种 |
| 「我妈乳腺癌该吃什么药」 | **仍然拒答** |

最后一行是重点。「这个癌种有哪些药」是公开的审批事实，属于健康教育；「我该吃什么药」是对个人病情的判断，本站不做。中文盐前缀（甲磺酸奥希替尼）与英文盐后缀（Osimertinib Mesylate）都可省略匹配。

### 新增：患者语料 10 → 47 条

补齐化疗/放疗/手术/免疫/内分泌/CAR-T/移植、分期与病理报告解读、肿瘤标志物、遗传检测与 BRCA、姑息治疗、癌痛与七类副作用管理、生育力保存、心理与照护者、随访与费用、控烟控酒与筛查、以及对「偏方」与常见误解的正面回应。新增「副作用管理」「预防与筛查」两个主题分类。**每条来源 URL 都逐一请求验证过**，没有一条是凭印象写的链接。

### 每日更新

- 药物清单：启动时若快照过期则补抓，此后每 `ONCOFORGE_PHARMACOPEIA_INTERVAL_HOURS`（默认 24 小时）一次。
- 研究进展：仍为 30 分钟一轮（0.6.0 起）。
- 语料：仍是人工维护的版本化快照，每条带核对日期与复核到期——这一层刻意不自动写入。

### 接口

`GET /api/knowledge/drugs`（总览或按癌种）、`GET /api/knowledge/drug?name=`、`GET /api/knowledge/cancer-types`、`POST /api/knowledge/refresh`（需登录并限流）。已写入 OpenAPI。

### 修正

- `pharmacopeia.ts` 的盐后缀正则里混进了一个字面退格符（`` 被当成转义写坏），导致英文药名的盐后缀无法剥离；已修复并加测试。

## 0.6.0 — 2026-09-02

新增**最新研究进展**栏目：每天自动抓取并即时发布癌症治疗与药物研发的公开进展。

### 新增

- `src/core/news.ts`：21 个信息源的抓取与归一化。三种取数方式——RSS 2.0 / RSS 1.0(RDF) / Atom 统一解析、PubMed E-utilities（esearch + esummary）、ClinicalTrials.gov v2 API。无第三方依赖。
- `data/news/sources.json`：信息源注册表，逐条记录发布者、类别、语言、地区与是否已限定肿瘤范围。
- 栏目在患者门户与科研工作台的导航中都可直达（`#research-news`），按监管审批 / 临床试验 / 同行评议研究 / 预印本 / 产业与研发 / 指南与公共卫生分类，支持按信息源、语言与关键词筛选。
- 接口：`GET /api/news`、`GET /api/news/sources`、`GET /api/news/daily?date=`、`POST /api/news/refresh`（需登录并限流）。已写入 OpenAPI。
- 定时调度：启动时若快照过期则补抓一次，此后按 `ONCOFORGE_NEWS_INTERVAL_MINUTES`（默认 30 分钟）轮询；每轮成功后写当天日报存档。

### 口径

沿用知识库的规则——**结论始终连着提出它的来源**。栏目只做抓取、过滤、分类、去重与摘录，不改写原文结论，不把不同来源的说法合并成一句话，每条都带发布者、发布时间与原文链接。

- 非肿瘤条目按术语表丢弃；期刊、预印本分区与 PubMed 检索式这类本身已限定范围的源跳过该过滤。
- 同一标题在不同 URL 下只保留一条；重新抓到的旧条目保留首次出现时间，不会因发布方刷新而重新算作「今日新增」。
- 信息源没给出可解析时间的条目，明确标注「来源未标注时间，按抓取时间排列」，不伪装成刚发布。
- 单源失败只记录该源状态；整轮全部失败时不推进新鲜度时间戳，页面显示当前是哪一时刻的快照，并提示检查网络。
- 条目在库保留 45 天；过期后仍可通过日报存档查到当天实际发布过什么。

### 其他

- `tests/_setup.ts` 设置 `ONCOFORGE_NEWS=off`，测试不再触发对外抓取。
- 清理 `public/app.css` 末尾一个无对应开括号的多余 `}`。

## 0.5.0 — 2026-09-01

网站更名与整体视觉重做。

### 更名

- 站点名称由 OncoForge 改为**战胜癌症**，副标「人工智能为生命领航」。界面、标题、文档、启动日志、OpenAPI 标题全部更新。
- 打包的 Python 科学引擎仍名为 OncoForge，方法与口径页、引用格式、第三方声明中保留该名称——那是它真实的名字，改掉会造成错误归属。代码标识符、环境变量（`ONCOFORGE_*`）与目录名同样保持不变，避免破坏既有配置。

### 视觉重做：方向「破晓」

- **色彩**：深松墨作底（`#0c1614`），承载顶栏、首屏与结果头；单一曙光琥珀（`#c9812c`）只用在首屏与「当前步骤」；深青（`#0f6e6a`）负责结构与操作；纸面中性色带轻微绿偏，避免默认灰的随意感。语义色（红/琥珀/绿）与主色分离。
- **布局**：左侧深色边栏改为**顶部细横栏 + 吸顶分节导航**。整页宽度还给内容，主标题因此能一行放下；正文统一在 1180px 测量线内。
- **首屏**：满幅深色，眉标—标题—曙光线—导语—行动按钮的纵向节奏；标题「让我们共同战胜癌症」用 `white-space: nowrap` 加视口比例字号，9 个字在任何宽度下都不换行。曙光线在载入时横向展开一次，遵循 `prefers-reduced-motion`。
- **就医路径**从竖排列表改为首屏底部的五段编号横栏，序号真实对应流程顺序。
- 字体不引入任何外部资源（本站承诺零外部请求），层级完全由字重、字号、字距与颜色建立。

### 修正

- 页脚导航新增后，`document.querySelector("footer a")` 会错选到文书链接；改为按 `#footer-back` / `#footer-note` 定位。
- OpenAPI 的注册接口仍在描述已移除的 `consentVersion` 字段，改为 `accept` 数组并补上 412 响应。

## 0.4.0 — 2026-09-01

建立完整的法律文书体系，并把它们变成**强制的、分环节的确认步骤**，而不是页脚里的链接。

### 新增：十份法律文书（`docs/`）

| 文书 | 何时弹出 |
|---|---|
| 医疗免责声明 | 首次进入本站 |
| Cookie 与本地存储说明 | 首次进入本站 |
| 用户协议（重写） | 注册时 |
| 隐私政策 | 注册时 |
| 科研用途声明 | 注册时 |
| 敏感个人信息处理单独同意书 | 首次提交任务或上传文件前 |
| 人类遗传资源与生物安全合规承诺书 | 首次提交任务或上传文件前 |
| 人工智能生成内容声明 | 首次查看或下载结果前 |
| 第三方数据、开源许可与知识产权声明 | 参考 |
| 部署方合规须知 | 参考，面向部署方 |

`docs/README.md` 是索引，说明为什么拆成多份而不是一份长协议：同意的时点不同，法律性质也不同——敏感个人信息需要单独同意，不能混进注册时的一次性勾选。

### 新增：同意闸门（`src/core/consent.ts`）

- 文书在 `data/consents.json` 中声明，含 `gate`（在哪一步拦截）、版本号与弹窗摘要。
- 服务端在四个闸门强制校验：`visit`、`register`、`data`（提交任务 / 上传文件 / 同步分析）、`output`（读取结果 / 下载报告）。
- 未签署时返回 **412**，响应体带上要展示的文书列表；浏览器据此弹窗，同意后自动重放原请求。
- 签署记录（文书 ID、版本、时间、来源 IP）写入账号，可在「账号与 API」页查看；文书版本提升后自动要求重新确认，历史记录不覆盖。
- 声明的版本与正文不符、或只签了闸门中的一部分，均被拒绝——签名必须指向实际展示的文本。

### 新增：文书弹窗（`src/client/consent.ts`）

- 弹窗内渲染 Markdown **全文**，不是链接跳转；内置轻量渲染器，支持标题、列表、表格、引用块（无外部依赖）。
- 同意按钮在正文滚动到底前保持禁用；文档短到不需要滚动时不设障碍。
- 注册表单改为**逐份确认清单**，三份文书全部签署后提交按钮才可用。
- 匿名访客的确认存在浏览器本地（无账号可挂靠），登录用户额外落库；Cookie 说明中已披露这一差异。
- 报告与 JSON 下载改为 fetch + blob，使闸门能像拦截其他操作一样拦截下载。

### 变更

- 版本号统一为 0.4.0；`/api/meta` 下发文书注册表。
- 页脚补全文书链接，新增「法律文书」板块可随时阅读任意一份全文。
- 注册接口不再接受 `consentVersion` 单值，改为 `accept: [{id, version}]` 数组。

## 0.3.0 — 2026-09-01

按整改纲要完成一次全面升级，关闭全部 BLOCKER 项。

### 新增：账号与准入（D-01）

- 四级角色：注册科研用户 / 认证机构用户 / 专家复核员 / 管理员；访客仅可使用患者门户。
- 能力（capability）取代角色比较：`design.fast`、`design.standard`、`design.deep`、`ingest.file`、`jobs.export`、`api.token`、`review.queue`、`admin.users`。
- 三条准入路径：邀请码（`ONCOFORGE_INVITE_CODES`）、机构邮箱域名白名单（`ONCOFORGE_INSTITUTION_DOMAINS`）、人工审核。首个注册账号自动成为管理员。
- 注册需填写研究用途说明并签署三份声明，签署版本与时间随账号留存。
- 服务端鉴权：`/api/jobs`、`/api/analyze`、`/api/ingest`、`/api/tooluniverse/*` 未认证一律 401，不再依赖前端隐藏。
- 会话使用 HttpOnly + SameSite=Strict Cookie；注册、登录、提交、检索均有速率限制。
- 管理员审核队列：查看用途承诺、启用/停用账号、调整角色。

### 新增：科研人员上手路径（D-02）

- 首次进入科研门户强制三步引导：准备数据 → 提交任务 → 读懂报告。
- 字段规范由 `data/glossary.json` 驱动，含正例与反例（如核酸坐标为何不被接受）。
- 支持 VCF / MAF / TSV / CSV / JSON 上传与拖拽，解析结果进入可编辑确认表；被跳过的行逐条给出原因。
- 蛋白改变写法归一化：`p.Thr315Ile` → `T315I`。
- 四个教学案例附「预期看到什么」说明，含一个拒答案例。
- 结果页展示九轴口径、拒答代码含义与证据等级说明。
- `/api/openapi.json` 提供完整接口文档；机构账号可签发 API 令牌接入自有流水线。

### 新增：任务队列与工作区（D-03）

- 提交改为异步入队，返回 job_id；前端轮询显示排队位次与运行时长，可取消。
- 全局并发上限 `ONCOFORGE_MAX_CONCURRENCY`（默认 2），叠加按角色的每日配额与并发上限。
- 工作区保存全部历史任务：状态、耗时、输入快照、结果、报告下载、命名、备注、检索。
- 每次运行记录引擎版本、算力档位、随机种子与**实际执行的完整命令行**，结果页可展开复现。
- 删除任务会真正移除 `.runtime/jobs/<id>/` 下的变异快照、结果与报告。

### 变更：知识层不再硬编码（D-04）

- 患者知识条目迁至 `data/knowledge/patient-cn.json`，含语料版本、快照日期、复核周期。
- 每条带来源机构、原文链接、本站核对日期、维护者；超过复核周期在界面标记「待复核」。
- 检索改为关键词 + 字符二元组混合匹配，答案逐句绑定引用编号；低于置信门槛时返回结构化拒答，不再兜底。
- 界面显示的核对日期改由数据驱动，此前是无数据支撑的表述。

### 变更：算力档位（D-05）

- 三档：快速（群贡献近似，无 3D 几何）/ 标准（RDKit 描述符 + 3D 共价可达性）/ 深度（多种子重复搜索 + 结构图）。
- 启动时探测 Python 与 RDKit，检测到即解锁对应档位；不可用时给出可执行的原因，不再静默降级。
- 每档的靶点数、候选数、代数、群体规模、重复次数与超时在界面明示。

### 新增：合规与数据治理（D-06）

- `docs/TERMS.md`、`docs/PRIVACY.md`、`docs/RESEARCH-USE.md` 三份文本，注册时留存签署版本。
- 隐私政策区分本机部署与联网部署；"只在本机处理"的表述仅在本机部署下展示。
- 明确数据分级、存储位置与保留期；提供任务级删除；审计日志不记录原始变异。

### 变更：主体信息与可信度（D-07）

- 版本号三处不一致（README 0.1.0 / 侧边栏 0.2.0 / 页脚 0.1.0）已统一为 0.3.0，并由 `/api/meta` 下发。
- 新增方法与口径页：应用版本、引擎版本、知识库快照、部署形态、基准报告直达链接。
- 提供标准引用格式，包含任务编号，便于他人复算。
- `site-config.json` 增加 `placeholder` 标记，占位信息未替换时界面会明确提示。

### 变更：工程底座（D-08）

- 服务端拆分为 `src/core/`（store / auth / engine / profile / jobs / knowledge / triage / tooluniverse / openapi），浏览器端拆分为 `src/client/` 模块。
- `npm run build` 编译全部客户端模块到 `dist/app/`。
- 测试扩充至覆盖鉴权、配额、准入、文件解析、检索拒答与端到端任务。

## 0.1.0

初始版本：患者门户与科研设计工作台双入口，同步分析，硬编码知识数组，无账号体系。
