package handler /* POST /api/v1/admin/sessions/tree/backfill —— 会话级线索树回填(2026-10-04)。 # 为什么是接口而不是 Migrate 里的自动步骤 用户 2026-10-04 明确要求「一切数据调用都要接口化」。而回填是**数据操作**: 它改真实会话的拓扑、可重跑、需要留审计痕迹。`Migrate` 是**结构**变更通道 (建表、加列),让��兼做数据改写是混用通道 —— 后果是不可重跑、不可通过 接口触发、看不出谁在什么时候改的。 ⇒ 从 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, }) }