package inspire import ( "encoding/json" "net/http" "strings" "apps/backend/internal/middleware" "apps/backend/internal/response" "apps/backend/internal/svc" "apps/backend/internal/types" "github.com/zeromicro/go-zero/core/logx" "github.com/zeromicro/go-zero/rest/httpx" ) // ChatStreamHandler — SSE 串流靈感聊天/產文。 // 事件(每行 data: JSON): // // {"type":"delta","text":"..."} // {"type":"done","session":{...},"message_id":"..."} // {"type":"error","message":"..."} func ChatStreamHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req types.InspireChatReq if err := httpx.Parse(r, &req); err != nil { httpx.ErrorCtx(r.Context(), w, err) return } uid, ok := middleware.UIDFrom(r.Context()) if !ok || uid <= 0 { httpx.ErrorCtx(r.Context(), w, response.Biz(401, 401001, "missing authorization")) return } if svcCtx.Inspire == nil { httpx.ErrorCtx(r.Context(), w, response.Biz(503, 503001, "inspire not configured")) return } flusher, ok := w.(http.Flusher) if !ok { httpx.ErrorCtx(r.Context(), w, response.Biz(500, 500000, "streaming unsupported")) return } w.Header().Set("Content-Type", "text/event-stream; charset=utf-8") w.Header().Set("Cache-Control", "no-cache, no-transform") w.Header().Set("Connection", "keep-alive") w.Header().Set("X-Accel-Buffering", "no") w.WriteHeader(http.StatusOK) flusher.Flush() writeEvent := func(v any) bool { b, err := json.Marshal(v) if err != nil { return false } if _, err := w.Write([]byte("data: ")); err != nil { return false } if _, err := w.Write(b); err != nil { return false } if _, err := w.Write([]byte("\n\n")); err != nil { return false } flusher.Flush() return true } mode := strings.TrimSpace(req.Mode) if mode == "" { mode = "chat" } out, err := svcCtx.Inspire.ChatStream(r.Context(), uid, req.Message, req.PinnedIds, mode, req.PersonaId, req.SessionId, req.Material, func(chunk string) error { if chunk == "" { return nil } if !writeEvent(map[string]any{"type": "delta", "text": chunk}) { return http.ErrAbortHandler // client gone } return nil }) if err != nil { _ = writeEvent(map[string]any{"type": "error", "message": err.Error()}) return } sess := out.Session pub := types.SessionFromDomain(sess) msgID := "" if len(sess.Messages) > 0 { msgID = sess.Messages[len(sess.Messages)-1].ID } // 回傳實際送 AI 的 prompt 指紋/全文,供前端與預覽對照 _ = writeEvent(map[string]any{ "type": "done", "session": pub, "message_id": msgID, "prompt": out.Prompt, "prompt_fingerprint": out.Fingerprint, "prompt_char_count": out.CharCount, "prompt_rune_count": out.RuneCount, "prompt_sections": out.Sections, }) logx.WithContext(r.Context()).Infof("inspire chat-stream ok uid=%d mode=%s fp=%s chars=%d", uid, mode, out.Fingerprint, out.CharCount) } }