Commit Graph

2 Commits

Author SHA1 Message Date
dfd5661e24 refactor(线索树): 回填接口化 —— 数据操作不进结构迁移通道
用户 2026-10-04:「一切数据调用都要接口化」。这条要求直接指向上一轮的两个错误。

## 撤掉的:Migrate 里的自动回填

`Migrate` 是**结构**变更通道(建表、加列)。让��兼做数据改写(给会话写
parent_session_id)是混用通道,后果不是理论上的:

- 不可重跑 —— 上一轮那次回填已经在部署中执行,`app_meta` 标记
  `2026-10-04T02:54:37Z`,**无法通过任何方式重跑或撤销**;
- 不可审计 —— 库上看不出谁在什么时候改的;
- 不可触发 —— 没有接口能主动执行它。

⇒ 移到 admin 端点,与 `DELETE /admin/sessions/{id}`、
`POST /admin/agent-keys` 同一档。

## 加的

    POST /api/v1/admin/sessions/tree/backfill    默认 dry-run,显式 dry_run=0 才写
    GET  /api/v1/admin/sessions/tree/preview     只读

`PreviewSessionParents`(repo 层,只读)是必需的配套:接口化之后**验证本身
也必须走接口**,那么「回填到底会改什么」就得有一条不写任何东西的接口来回答,
否则只剩「先写了再看看对不对」。

## 判据抓到的真 bug:注释说默认 dry-run,代码默认就写

    if v := r.URL.Query().Get("dry_run"); v != "" {   // 参数缺失 → 整个 if 落空
        ...
    }
    n, err := repo.BackfillSessionParents(...)         // ← 直接执行

误点一次 POST 就改了 6 条真实会话的拓扑,且没有任何预览。修成
`dry := true` 起手、只在显式解析出 false 时才写。

## 判据自己假绿了一次(本轮第四次做形状判据)

`TestBackfillDefaultsToDryRun` 初版读源码、数 `AdminPreviewSessionParents`
出现几次。变异 `dry := false`(**正是上面那个 bug**)之后判据**依然全绿** ——
因为 `if dry {…}` 分支在源码里始终存在。

⇒ 改成行为测试:真起 httptest 发一个不带参数的 POST,然后去**库里**看
parent_session_id 有没有被写。重测同一变异,两条断言都红:

    ★ 无参 POST 的响应应表明这是预览,实际:{"dry_run":false,…}
    ★ 无参 POST 就把 parent_session_id 写成了 "31949dc4-…"

同轮修掉判据两次「匹配比语义宽」:`strings.Contains(seg, "UPDATE")`
把 `ORDER BY c.updated_at DESC` 里的 `updated` 当成了写库;
函数边界用 `strings.Index("\nfunc ")` 在「函数是文件最后一个」时落空,
取到全文后把别的函数里的 UPDATE 判成预览写库。

## 尚未验证

树端点只能验到「匿名被正确拒绝(401)」—— **不足以证明树能用**。
缺 admin 凭证,admin 全看 vs 普通用户收窄的差异还没在真数据上跑过。

14 包全绿。
2026-10-04 12:03:58 +08:00
cd894d5e4d feat(线索树)★★: 会话级父子 + 树视图 —— 跨会话分叉第一次可见
## 要解决的问题

用户 2026-10-04:「对话树实现得非常原始,根本没有形成/展示为树结构」。

★ 先纠正我自己的一个误判:我先前只看 `ContactPanel`(会话列表)就断言
「前端纯平铺、零层级」。那是不完整的检查 —— `ThreadView.tsx` 早就在用
`node.depth` 做缩进 + 连接线渲染**会话内**的邮件树,服务端 `DescendantsRaw`
也早就有 `WITH RECURSIVE … lvl`。所以**会话内**的树是有的。

真正缺的是**跨会话**:`sessions` 表根本没有 `parent_session_id`,
而生产库实测有 **6 封**邮件的 parent 指向**另一条会话**,
界面上它们是几行互不相干的东西。而「A 交给 B 之后 B 继续推」正是
协作里最常见的形状 —— 那 6 条里就有 `本机-agent-能力盘点 → 渲染自检`
这种纯分叉。

## 回填后的真实拓扑(生产数据)

    harmony-emu-unblock ─┐
    deploy-pi-bridge-…  ─┴→ 邮件驱动…项目概述 ─┬→ 核实-HomeAgent-mail-bridge
                                              └→ 邮件驱动…项目概述-2 → 时间显示自检
    本机-agent-能力盘点 → 渲染自检

## 可见性:逐节点过滤 + 剪断不可见祖先(安全边界)

树天然会把父节点带给子节点,而「我能看见 B」≠「我能看见 B 的父 A」
(A 可能是别人与别人的对话)。

所以按节点过滤之后**必须重算 parent**:不可见的祖先一律跳过、树在可见处
重新起根。只过滤不重算,输出里就带着不可见父的 session_id 与标题 ——
一条真实的泄露路径,而且它藏在「树视图」这个新功能里,没人会想到去查。

**admin 看全部**(用户 2026-10-04 定的分层)。

★ 与 Agent 侧那条边界无关:`AgentMayReadSession` 是 15e4fe9 / 095213b
修出来的越权防护(Agent 只能读自己参与过的会话);本端点属人类登录态,
admin 全看是显式授权的。两者语义不同,不要混谈。

## 回填只跑一次

回填改的是会话**拓扑**,不是派生数据。若每次 Migrate 都跑,
「某人手工把 parent 改对」会在每次重启时被悄悄改回去 —— 与既有
`backfillMailReads` 同族(其注释:每次跑会把「某抄送方读过」按主收件人
写成已读,正是那次要修的语义错误)。故用 `app_meta` marker 守住。

★ 判据分两包:`repo` 测回填口径与剪枝,`db` 测「二次 Migrate 不覆盖人工设置」。
只写在 repo 包就只测到「回填幂等」,测不到 Migrate 那一层。

## 实现中修掉的三个真 bug(都被判据抓到)

1. **参数顺序反了** —— `t.lvl < ?` 是 SQL 里第一个占位符,我放在 args 末尾
   ⇒ 根查询拿到一个整数、匹配不到任何行,树只返回递归分支那半。
2. **环下打满 55s 超时** —— 计数写成相关子查询,环下每个节点都重跑一次
   mails 计数。改成 `LEFT JOIN` 两个聚合后 0.01s。
3. **`IN (NULL)`** —— parentIDs 为空时该表达式恒不匹配任何行。

## 判据(8 格)

含防环(环下 0.01s + 耗时断言)、不覆盖人工父、不把同会话内邮件父子
当会话父子、不回填不可见祖先(两个变异分别去掉「过滤」「剪断」都转红)。

★ 判据自己错了一次:「隐藏不可见祖先」那格我写成「对每个可见节点都断言
depth==0」,但 B 重起根(0)、C 仍挂在 B 下(1),两者不同 —— 判据红而代码
是对的。(又是「判据比语义宽/窄」那一族。)

14 包全绿。
2026-10-04 12:03:58 +08:00