From 466959d3c39d29a3f539aa3508c58a64933370d4 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Mon, 20 Jul 2026 15:49:13 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E9=9D=99=E9=BB=98=E5=90=9E=E9=94=99?= =?UTF-8?q?=E6=A8=AA=E6=89=AB=20=E2=80=94=E2=80=94=20=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E8=90=BD=E5=BA=93/=E9=85=8D=E7=BD=AE=E5=B9=BF=E6=92=AD/?= =?UTF-8?q?=E5=AE=A1=E8=AE=A1=E5=A4=B1=E8=B4=A5=E4=B8=8D=E5=86=8D=E6=97=A0?= =?UTF-8?q?=E5=A3=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 把检索链路那次的做法推到同类位置。判据是"丢了这个 error 的代价",只改代价 高的,不动刻意的 fire-and-forget。 改(丢了就是数据永久丢失或行为不可解释): - task_handler: 任务**输出**与**执行轨迹**的收尾落库。这是唯一的持久副本 (Redis 流 10min TTL),失败则永远无法复盘,界面上只显示"这次运行没有 轨迹"。轨迹的 json.Marshal 失败分支同样是静默跳过,一并补上。 - admin.broadcastActive: 模型配置热更新广播。失败 = dispatcher/mcp-go 仍用 旧配置,症状是"控制台改了模型却不生效",而操作者这边一切正常——正是今天 排查半天的那类问题。 - middleware/audit: 审计留痕写入。不阻断主流程是对的(业务已完成),但静默 失败意味着敏感操作没有记录,且没人知道记录缺了,这是合规缺口。 不改(确认过是合理的): - dispatcher 的 PublishToken/PublishExec:往前端推流,丢一帧是 UI 瑕疵, 且按 token 打日志会刷屏; - 计量回写 PublishUsage:本来就检查 error 并打日志,钱的路径是干净的。 另修一处误导文案:任务下钻的轨迹空态原本把原因说死为"早于该功能上线", 现在"落库失败"也是已知原因,文案改为并列并指向日志里的 [task] 告警。 Co-Authored-By: Claude Opus 4.8 --- .../src/components/TaskDetailDrawer.tsx | 2 +- sundynix-gateway/internal/handler/admin.go | 8 ++++++-- .../internal/handler/task_handler.go | 16 ++++++++++++---- sundynix-gateway/internal/middleware/audit.go | 7 ++++++- 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/sundynix-admin/src/components/TaskDetailDrawer.tsx b/sundynix-admin/src/components/TaskDetailDrawer.tsx index 9411f17..057182f 100644 --- a/sundynix-admin/src/components/TaskDetailDrawer.tsx +++ b/sundynix-admin/src/components/TaskDetailDrawer.tsx @@ -132,7 +132,7 @@ export function TaskDetailDrawer({ task, onClose }: { task: AdminTask; onClose: text={ task.status === "running" || task.status === "submitted" ? "任务还在跑,轨迹要等收尾才落库(这里是复盘视图,不是实时流)" - : "这次运行没有留下轨迹(可能早于轨迹落库功能上线)" + : "这次运行没有留下轨迹(早于轨迹落库功能上线,或收尾时落库失败——后者会在 gateway 日志里有 [task] 告警)" } /> ) : ( diff --git a/sundynix-gateway/internal/handler/admin.go b/sundynix-gateway/internal/handler/admin.go index c219ecf..52f3b8a 100644 --- a/sundynix-gateway/internal/handler/admin.go +++ b/sundynix-gateway/internal/handler/admin.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "log" "net/http" "strconv" "strings" @@ -382,7 +383,6 @@ func (h *Handler) AdminSetTenantStatus(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "ok"}) } - // AdminMembers: GET /api/v1/admin/tenants/:id/members —— 某租户成员列表。 func (h *Handler) AdminMembers(c *gin.Context) { rows, err := h.db.ListMembers(c.Request.Context(), c.Param("id")) @@ -564,7 +564,11 @@ func (h *Handler) TestModel(c *gin.Context) { func (h *Handler) broadcastActive(ctx context.Context) { for _, kind := range []string{contract.ConfigKindChat, contract.ConfigKindEmbedding} { if cfg := h.db.ActiveConfig(ctx, kind); cfg != nil { - _ = h.bus.PublishConfigUpdated(kind, cfg) + // 广播失败 = dispatcher/mcp-go 拿不到新配置,症状是"控制台改了模型却不生效", + // 而改配置的人这边一切正常。必须留痕,否则只能靠猜。 + if err := h.bus.PublishConfigUpdated(kind, cfg); err != nil { + log.Printf("[admin] ⚠️ %s 配置广播失败: %v(dispatcher/mcp-go 仍在用旧配置,需重启或重试保存)", kind, err) + } } } } diff --git a/sundynix-gateway/internal/handler/task_handler.go b/sundynix-gateway/internal/handler/task_handler.go index b532b69..013eca3 100644 --- a/sundynix-gateway/internal/handler/task_handler.go +++ b/sundynix-gateway/internal/handler/task_handler.go @@ -16,11 +16,11 @@ import ( "github.com/gin-contrib/sse" "github.com/gin-gonic/gin" - "github.com/sundynix/sundynix-shared/blob" "github.com/sundynix/sundynix-gateway/internal/dsl" "github.com/sundynix/sundynix-gateway/internal/nats" "github.com/sundynix/sundynix-gateway/internal/payment" "github.com/sundynix/sundynix-gateway/internal/store" + "github.com/sundynix/sundynix-shared/blob" "github.com/sundynix/sundynix-shared/contract" ) @@ -139,7 +139,11 @@ func (h *Handler) startTokenRecorder(taskID string) { func() { _ = h.cache.StreamAppend(ctx, store.ChannelToken, taskID, "done", "") if out.Len() > 0 { - _ = h.db.SaveTaskOutput(context.Background(), taskID, out.String()) + // 失败必须出声:这是**唯一**的持久副本(Redis 流 10min 后就没了), + // 丢了就再也复盘不了,界面上只会显示"这次运行没有输出"。 + if err := h.db.SaveTaskOutput(context.Background(), taskID, out.String()); err != nil { + log.Printf("[task] ⚠️ 输出落库失败 task=%s len=%d: %v(该次运行将无法复盘)", taskID, out.Len(), err) + } } cancel() }, @@ -170,8 +174,12 @@ func (h *Handler) startExecRecorder(taskID string) { func() { _ = h.cache.StreamAppend(ctx, store.ChannelExec, taskID, "done", "") if len(evs) > 0 { - if b, err := json.Marshal(evs); err == nil { - _ = h.db.SaveTaskTrace(context.Background(), taskID, string(b)) + // 同上:轨迹只有这一份持久副本,静默失败会表现为"这次运行没有轨迹"。 + b, err := json.Marshal(evs) + if err != nil { + log.Printf("[task] ⚠️ 轨迹序列化失败 task=%s events=%d: %v(轨迹将丢失)", taskID, len(evs), err) + } else if err := h.db.SaveTaskTrace(context.Background(), taskID, string(b)); err != nil { + log.Printf("[task] ⚠️ 轨迹落库失败 task=%s events=%d: %v(该次运行将无法复盘)", taskID, len(evs), err) } } cancel() diff --git a/sundynix-gateway/internal/middleware/audit.go b/sundynix-gateway/internal/middleware/audit.go index 8eab759..67f826f 100644 --- a/sundynix-gateway/internal/middleware/audit.go +++ b/sundynix-gateway/internal/middleware/audit.go @@ -2,6 +2,7 @@ package middleware import ( "context" + "log" "time" "github.com/gin-gonic/gin" @@ -35,6 +36,10 @@ func Audit(db *store.Postgres) gin.HandlerFunc { } ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() - _ = db.AppendAudit(ctx, entry) + // best-effort:审计失败不阻断主流程(业务已经做完了),但必须留痕 —— + // 静默失败意味着敏感操作没有记录,而没人知道记录缺了。 + if err := db.AppendAudit(ctx, entry); err != nil { + log.Printf("[audit] ⚠️ 审计留痕写入失败 actor=%s %s %s: %v", entry.Actor, entry.Action, entry.Path, err) + } } }