package migu import ( "context" "encoding/json" "fmt" "io" "net/http" "strings" "yunyan/lego/sys/log" "yunyan/sys/migu" "github.com/google/uuid" ) // handleChat 处理普通聊天接口 POST /chat(支持流式 SSE 与非流式 JSON) // // @Summary 咪咕灵犀聊天 // @Description 普通聊天接口,内部对接咪咕灵犀。 // @Description stream=false:返回完整 JSON(ChatResponse),结构化音乐卡片在 cards 字段; // @Description stream=true:返回 SSE 流,每帧一个 ChatChunk(音乐卡片在 card 字段),结束以 data: [DONE]。 // @Description 鉴权:服务端配置了 ApiKey 时需带请求头 Authorization: Bearer 。 // @Tags Migu // @Accept json // @Produce json // @Produce text/event-stream // @Param body body ChatRequest true "聊天请求" // @Success 200 {object} ChatResponse "非流式完整响应;流式时为 SSE 流(data: {ChatChunk} ... data: [DONE])" // @Failure 400 {object} map[string]string "参数错误" // @Failure 401 {object} map[string]string "鉴权失败" // @Router /chat [post] func (this *Migu) handleChat(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { writeError(w, http.StatusMethodNotAllowed, "method not allowed") return } // 入站鉴权(配置了 ApiKey 才校验) if this.options.ApiKey != "" { token := strings.TrimSpace(strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer")) if token != this.options.ApiKey { writeError(w, http.StatusUnauthorized, "invalid api key") return } } body, err := io.ReadAll(r.Body) if err != nil { writeError(w, http.StatusBadRequest, "read body failed: "+err.Error()) return } var req ChatRequest if err = json.Unmarshal(body, &req); err != nil { writeError(w, http.StatusBadRequest, "invalid json: "+err.Error()) return } if strings.TrimSpace(req.Message) == "" { writeError(w, http.StatusBadRequest, "message is required") return } // deviceId / sessionId 来源:请求体优先,其次请求头 deviceId := req.DeviceId if deviceId == "" { deviceId = r.Header.Get("X-Device-Id") } sessionId := req.SessionId if sessionId == "" { sessionId = r.Header.Get("X-Session-Id") } miguReq := buildMiguRequest(&req, deviceId, sessionId) // 统一从咪咕流式拉取 ch := make(chan *migu.StreamResp, 8) go func() { if e := migu.Chat(context.Background(), miguReq, ch); e != nil { log.Errorf("migu lingxi chat err: %v", e) } }() if req.Stream { this.streamResponse(w, r, miguReq, ch) } else { this.jsonResponse(w, miguReq, ch) } } // buildMiguRequest 把聊天请求转换成咪咕灵犀请求 func buildMiguRequest(req *ChatRequest, deviceId, sessionId string) *migu.Request { mr := &migu.Request{ ReqId: uuid.NewString(), DeviceId: deviceId, Messages: []migu.Message{{Role: "user", Content: req.Message}}, Stream: true, } if len(req.History) > 0 { history := make([]migu.Message, 0, len(req.History)) for _, m := range req.History { history = append(history, migu.Message{Role: m.Role, Content: m.Content}) } mr.History = history } // 咪咕要求 session 必填:未携带 sessionId 时用 reqId 兜底,保证字段存在 if sessionId == "" { sessionId = mr.ReqId } mr.Session = &migu.Session{SessionId: sessionId} return mr } // streamResponse 把咪咕流式数据逐帧转换为 SSE(data: {ChatChunk})写回客户端 func (this *Migu) streamResponse(w http.ResponseWriter, r *http.Request, mr *migu.Request, ch chan *migu.StreamResp) { flusher, ok := w.(http.Flusher) if !ok { writeError(w, http.StatusInternalServerError, "streaming unsupported") return } w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Connection", "keep-alive") w.Header().Set("X-Accel-Buffering", "no") w.WriteHeader(http.StatusOK) ctx := r.Context() for { select { case <-ctx.Done(): return case sr, alive := <-ch: if !alive { fmt.Fprint(w, "data: [DONE]\n\n") flusher.Flush() return } chunk := convertChunk(sr) if chunk == nil { continue } data, _ := json.Marshal(chunk) fmt.Fprintf(w, "data: %s\n\n", data) flusher.Flush() } } } // jsonResponse 累积咪咕流式数据,聚合为一个完整的结构化响应 func (this *Migu) jsonResponse(w http.ResponseWriter, mr *migu.Request, ch chan *migu.StreamResp) { resp := ChatResponse{ ReqId: mr.ReqId, SessionId: mr.Session.SessionId, } var contentBuf, reasoningBuf strings.Builder for sr := range ch { chunk := convertChunk(sr) if chunk == nil { continue } reasoningBuf.WriteString(chunk.Reasoning) contentBuf.WriteString(chunk.Content) if chunk.Card != nil { resp.Cards = append(resp.Cards, *chunk.Card) } resp.RecommendPrompts = append(resp.RecommendPrompts, chunk.RecommendPrompts...) if chunk.FinishReason != "" { resp.FinishReason = chunk.FinishReason } } resp.Reasoning = reasoningBuf.String() resp.Content = contentBuf.String() w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) _ = json.NewEncoder(w).Encode(resp) }