feat(plan): 关闭超时自动回收 + 新增 plan 生命周期事件账本

## 1. 自动回收改为默认关闭(PAC_PLAN_AUTO_RECYCLE=on 才跑)

它一直在生产跑着(ScheduleModule 已启用、服务已注册),日志可查:两天触发 9 次、
累计回收 32 单。前端那句"暂无自动回收机制"说的是倒计时控件被隐藏,不是后端。

关闭理由:回收会把 assignee_user_id / assigned_at **就地置 null**,而「认领」现在要
当作「该患者已被客服处理」用于统计 —— 每 10 分钟抹一次,统计地基是漏的。

代码保留不删:超时兜底的原始问题(领了不做 → 患者被锁死)依然成立,后续想改成
更长超时或"只提醒不回收",打开开关即可。启动时打印开关状态(它会静默改数据)。

注:生产当前登录角色全是 leader,自认领 / 自返池都在权限内,无"staff 无法释放工单"
的沉淀风险。将来放开 staff 角色需重新评估(staff 无 PLAN_RECYCLE)。

## 2. PlanEventLog — plan 生命周期事件账本(append-only)

为什么 PlanGenerationLog 顶不上:它是**每次引擎跑批一行**的管道账本,**连 plan_id
都没有**,只记"引擎跑了没有、产出几个"(生产 750 万行 / 24 万患者,约 80 万行/天)。
回答不了"这个 plan 经历了什么"。

已收事件:claim / assign / release / auto_release(归属)+ feedback(召回 👍👎)。
assign 为门诊经理分配预留 —— actor != assignee 时自动区分,无需事后反推。

 收录边界(注释里划死,防止变垃圾桶):
   人工动作 + 会被就地覆盖且事后无法还原的状态变更
   引擎重算 / supersede —— 80 万行/天,与人工动作(约 30~100/天)差 4 个数量级,
     混入会淹没人工动作;引擎侧已有 PlanGenerationLog + followup_plans 版本流
   前端行为埋点(浏览/点击)—— 噪声大、可刷,属产品分析那一摊

### 写入方式:应用层 + 同事务,**不用数据库触发器**
  1. 触发器拿不到操作人 —— actor 在 JWT 里,DB 只看得见行变了;靠 session 变量传递
     还是得应用配合,只是变隐式更易漏。本项目 AsyncLocalStorage 目前也不含 user。
  2. 拿不到 actor 就分不出 claim / assign —— 这恰是本表最核心的价值。
  3. 触发器会被运维脚本误触发:任何一句 `UPDATE followup_plans SET assignee_user_id=NULL`
     (数据修复 / 清理测试数据)都会凭空造出假的 release 事件。
  4. Prisma 不管触发器,得走 raw SQL 迁移,schema 里看不见、易随漂移丢失。
  弱点(已认):靠自觉。缓解 = 写入收口到 recordPlanEvent() 单一入口 + 只接受事务客户端
  (状态变更与账本同生共死)+ 事件类型走枚举编译期拦拼写错误。

### 扩展性
  新增 PlanEventType + PLAN_EVENT_META(@pac/types,照 EXECUTION_OUTCOME_META 的样子):
  事件类型的单一真理源,带 labelZh / group / byHuman / holdsPatient。
  新增事件 = META 加一行,不是在调用处随手写字符串。
  HUMAN_TOUCH_EVENTS 收口「算不算客服处理过」的口径(auto_release 是系统行为,不算)。

两个关键字段的存在理由:
  · heldSeconds 必须在清空 assigned_at **之前**算好(computeHeldSeconds),事后算不出来
  · feedback 立 reason 列(up/down):followup_plans.recall_feedback 是就地覆盖的,
    先 👍👎 会把前一次冲掉,准确度统计只看最终值会低估分子、也看不出"改判"

配套:assign / recycle 接口补传操作人(controller 原先根本没取 user.sub,
自认领与指派他人在数据上完全无法区分)。

## 验证
- 340 单测通过(新增 15 例:四种归属事件、幂等不重复记账、事件类型登记完整性、
  auto_release 不计入人工、computeHeldSeconds 边界、开关默认关且只认显式 'on')
- 本地端到端:认领 → 👎(带 note)→ 返池,账本 3 行齐全、held_seconds 准确;
  同期 followup_plans.recall_feedback 只剩最终值 —— 正是就地覆盖会丢的那部分

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Found errors in your .gitlab-ci.yml:

  • jobs:openapi-drift config contains unknown keys: rules
You can also test your .gitlab-ci.yml in the Lint
Status Job ID Name Coverage