thread-master/apps/backend/internal/handler/inspire/chat_stream_handler.go

108 lines
3.0 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 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)
}
}