Files
MailUI4Agents/server/internal/handler/session_tree_admin.go
JianFeeeee 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

84 lines
2.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package handler
/*
POST /api/v1/admin/sessions/tree/backfill —— 会话级线索树回填(2026-10-04)。
# 为什么是接口而不是 Migrate 里的自动步骤
用户 2026-10-04 明确要求「一切数据调用都要接口化」。而回填是**数据操作**:
它改真实会话的拓扑、可重跑、需要留审计痕迹。`Migrate` 是**结构**变更通道
(建表、加列),让<EFBFBD><EFBFBD>兼做数据改写是混用通道 —— 后果是不可重跑、不可通过
接口触发、看不出谁在什么时候改的。
⇒ 从 Migrate 移到这里,挂在 AdminOnly 组下(同一档的还有
`DELETE /admin/sessions/{id}`、`POST /admin/agent-keys`)。
# dry_run:先看后写
默认 `dry_run=1` —— **只返回将要写入的父子关系,不落库**。回填会改会话拓扑,
不能靠「先写了再检查对不对」。管理员确认后显式传 `dry_run=0` 才真正写入。
# 幂等
`BackfillSessionParents` 只写 `parent_session_id IS NULL` 的会话,
所以重跑安全;人工设置过的父**不会被覆盖**(判据
`TestBackfillSessionParentsRunsOnlyOnce`)。
*/
import (
"net/http"
"strconv"
"github.com/agentmail/gateway/internal/repo"
)
// AdminPreviewSessionParents 只读预览:若现在回填,会写哪些父子关系。
func AdminPreviewSessionParents(w http.ResponseWriter, r *http.Request) {
preview, err := repo.PreviewSessionParents(r.Context())
if err != nil {
Error(w, http.StatusInternalServerError, "预览会话父子失败")
return
}
JSON(w, http.StatusOK, map[string]any{
"would_set": preview,
"count": len(preview),
"dry_run": true,
})
}
// AdminBackfillSessionParents 执行回填。
//
// 默认 dry-run;必须显式 `dry_run=0` 才写 —— 免得误点一次就改了真实拓扑。
func AdminBackfillSessionParents(w http.ResponseWriter, r *http.Request) {
// ★ 默认 dry-run,**且 `dry_run` 未指定时也走预览**。
//
// 初版写成 `if v := …; v != "" { … }` —— 参数缺失时整个 if 落空,
// 直接执行回填。也就是注释写着「默认 dry-run」而代码**默认就写**。
// 后果:误点一次 POST 就改了 6 条真实会话的拓扑,且没有任何预览。
// 判据 TestBackfillDefaultsToDryRun 抓到的就是这一条(它红得对)。
dry := true // 默认预览
if v := r.URL.Query().Get("dry_run"); v != "" {
parsed, err := strconv.ParseBool(v)
if err != nil {
Error(w, http.StatusBadRequest, "dry_run 必须是 true/false")
return
}
dry = parsed
}
if dry {
AdminPreviewSessionParents(w, r)
return
}
n, err := repo.BackfillSessionParents(r.Context())
if err != nil {
Error(w, http.StatusInternalServerError, "回填会话父子失败")
return
}
JSON(w, http.StatusOK, map[string]any{
"status": "backfilled",
"parents": n,
"dry_run": false,
})
}