WORKFLOW · 工作流
机器管每天,人管判断,会话管建设
医械HOT 采集、打分、出报,每天 08:30 自动同步推送上线;收藏在 /admin 在线运营;站本身的三次大改版都是 AI 编程会话完成的。这一页写它现在怎么转,以及建设路上摔过的跤。
这条链路 2026-10-04 才接通,之前是另一条跑了半个月的 AI 新闻管线(已退役)。所以下面写的是它每一步怎么接起来的、建设过程里摔过哪几跤,而不是「最佳实践」。等它跑满一个月,这一页会重写一遍。
PRINCIPLES · 三条原则
先立标准,再看内容
机器管每日,人管判断
每天必跑的(采集、同步、构建、部署)全部无人值守;要不要收一条链接、一篇手记怎么写,留给人。两边各干各的,谁也别替谁。
建设交给 AI 会话
这个站的三次大改版 —— v2 全站落地、收藏接入数据库并上线、日报切换医械HOT —— 都是 AI 编程会话完成的。人负责验收和决策,会话负责实现与排障。
数字可核,出事先留证据
页面上的每个数字要么来自数据库实查,要么注明核对日期;故障先取证再动手 —— 下面三个坑的每条证据都有日期和字节级细节,不写「偶尔会挂」。
PIPELINE · 链路
整条链路长这样
- 01采集Collect
16 个源 → articles 表
- 02事实化与打分Rank
articles → facts / stories
- 03出报Report
facts → reports 表
- 04同步Sync
reports 表 → daily.json
- 05发布Ship
daily.json → Vercel 部署
- 06运营Operate
/admin → Turso → 前台实时
采集、打分、出报、同步、发布五环全自动,每天的 08:00–08:35 之间自己跑完; 运营那环留给人。每一层的输入输出都固定,换掉任何一层不会牵动其他层。
STAGES · 六个环节
每个环节到底在干什么
采集
医械HOT 盯住 16 个信源 —— 药监局、器审中心的官方发布页加行业媒体 —— 抓回来的文章连同正文、发布时间、来源一起落进 PostgreSQL。
- 已累计采集 284 篇(2026-10-04 核对),字段里发布时间和采集时间分开存,混在一起新鲜度会算错
- 数据库和站点是两个独立项目,凭据只存在医械HOT 的 .env 里,不进任何仓库
- 采集失败的源当天空缺就空缺,不拿旧数据顶 —— 国庆假期连续几天 0 收录,日报照实出空档
事实化与打分
文章被抽成一条条「事实」(谁、做了什么、何时),关联到事件线上;热度和来源性质(官方一手 / 行业)参与打分。
- 医疗器械领域里,「是不是官方一手」比「几个源在报」更关键 —— 站上日报的红灰方块就取自这个字段
- 同一事件的多篇报道聚到一条 story 上,不聚会天天重复收录
出报
每天 08:00,模型把窗口期内的事实汇成一期日报:一段导语加按类分组的条目,写回 reports 表。没有内容的日子就出一期空档,不硬凑。
- 导语是当期的综合判断,正文条目带精确发布时间 —— 时间是抓到的真值,不是反推的
- 报告只增不改:修订走 revision,出了错能追到是哪一版
同步
每天 08:30,Windows 计划任务跑同步脚本:读医械HOT 的 PostgreSQL,固化成站点仓库里的 daily.json。
- 流水线刚起跑时每天生成的空骨架日(无条目也无导语)自动跳过,不然归档里会躺一排「当日无头条」
- 显示用的时间标签在同步期按上海时区固化,页面端不做任何时区换算 —— 避免 hydration 期服务器和浏览器算出两个值
- 数据库没在跑就静默退出,不动仓库,第二天接着来
发布
daily.json 有变化才 commit + push;GitHub 通知 Vercel 自动构建,1~2 分钟后新版上线。没变化就什么都不发生。
- 「有变化才推」是脚本里显式判断的 —— 每天一次空提交会让部署历史没法看
- Vercel 上的环境变量(数据库连接串、管理密码)先配好才推代码,顺序反了线上会挂
运营
收藏不进这条自动链 —— 126 条书签存在 Turso 云数据库,/admin 里密码登录直接增删改,前台实时生效,全程不碰代码、不碰部署。
- 单管理员密码 + HMAC 签名 Cookie,没有注册体系 —— 个人站不需要用户表
- 首次访问自动建表播种,本地 SQLite 和线上 Turso 走同一套代码,只差一个连接串
PITFALLS · 踩过的坑
建设过程中摔的 3 跤
磁盘在悄悄写坏文件
构建报「文件是二进制」「stream did not contain valid UTF-8」。起初以为是依赖坏了,重装后换一批文件继续报 —— 损坏根本不在代码里。
2026-10-04 排查:daily.ts 在第 4094 字节、markdown.tsx 在 4096、notes.ts 在 4098 —— 三份没动过的源文件全部在 4KB 边界被写入垃圾字节;两张 PNG 被清零;.git 的 index 截断、多个对象文件为空。这是磁盘级的损坏,不是提交出来的。
远端仓库当唯一真身:npm ci 重装依赖,从 GitHub 克隆一份,用好文件覆盖损坏文件、整个换掉坏掉的 .git,再 chkdsk 检查磁盘。恢复后 fsck 全绿、构建一次过。教训:重要工作当天就要有远端提交。
同一个仓库连了两次 Vercel
环境变量加好了、代码推上去,线上首页却读不到数据库。检查才发现:GitHub 仓库被导入成两个 Vercel 项目,正式域名挂在 A 项目,环境变量加在 B 项目。
2026-10-04 处理:项目列表里 web 与 ai-coupon-platform 连着同一个仓库;aideals.me 及其跳转都在 web 名下,而 DATABASE_URL 和 ADMIN_PASSWORD 在 ai-coupon-platform 里 —— 两边各对一半,合起来必挂。
动代码前先理清单一:把 www 与 apex 域名都转到正确的项目(apex 顺带配成 307 跳 www),删掉重复项目,环境变量确认就位之后才 push。顺序错了就是一次可预见的事故。
错误被包在 cause 链里,顶层看不到
重复 URL 新增书签,接口该返回 409「已收过」,实际却 500。测试环境里一切正常,唯独这个分支炸。
2026-10-04 实测:libsql 抛的 UNIQUE 约束错误被 Drizzle 包了一层,顶层错误对象的 message 只有 SQL 文本,用 String(e).includes("UNIQUE") 永远匹配不上 —— 真正的错误码在 e.cause.cause 里。
判断约束冲突沿 cause 链逐层找错误码(message + code 一起查),不信任顶层的字符串。修完同一请求返回 409,管理台能给出「这个 URL 已经收过了」而不是一屏红字。