package service import ( "bufio" "bytes" "context" "encoding/json" "errors" "fmt" "io" "mime/multipart" "model-gateway/model/dto" "net/http" "strings" "time" commonHttp "gitea.redpowerfuture.com/red-future/common/http" "gitea.redpowerfuture.com/red-future/common/utils" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/gclient" "github.com/gogf/gf/v2/util/gconv" ) // DeductBalanceReq 扣减余额请求 type DeductBalanceReq struct { Id uint64 `json:"id"` Surplus float64 `json:"surplus"` } // DeductBalance 扣减租户余额。走 admin-go 内部接口 /pub/tenant/deduct(无 gftoken/Auth,供 model-gateway 内部调用)。 // admin-go 的 tenant/edit 对 surplus 走 gdb.Counter 增量(正加负减),故扣减须传负值;调用方在本次未产生费用(cost<=0)时应跳过。 func DeductBalance(ctx context.Context, tenantId uint64, amount float64) error { apiURL := "admin-go/api/v1/pub/tenant/deduct" headers := setCtxHeader(ctx) body := DeductBalanceReq{ Id: tenantId, Surplus: -amount, } jsonData, _ := json.Marshal(body) var resp struct{} err := commonHttp.Post(ctx, apiURL, headers, &resp, jsonData) if err != nil { g.Log().Warningf(ctx, "[扣减余额] 失败 tenantId=%d amount=%.6f err=%v", tenantId, amount, err) return err } g.Log().Infof(ctx, "[扣减余额] 成功 tenantId=%d amount=%.6f", tenantId, amount) return nil } // TenantSurplusResp 租户余额返回 type TenantSurplusResp struct { Tenant struct { Surplus float64 `json:"surplus"` } `json:"tenant"` } // GetTenantSurplus 获取租户余额(走 admin-go 内部接口 /pub/tenant/balance,无 gftoken/Auth) func GetTenantSurplus(ctx context.Context, tenantId uint64) (float64, error) { apiURL := fmt.Sprintf("admin-go/api/v1/pub/tenant/balance?tenantId=%d", tenantId) headers := setCtxHeader(ctx) var resp TenantSurplusResp err := commonHttp.Get(ctx, apiURL, headers, &resp, nil) if err != nil { g.Log().Warningf(ctx, "[获取余额] 失败 tenantId=%d err=%v", tenantId, err) return 0, err } return resp.Tenant.Surplus, nil } // IsSuperAdmin 调用admin-go服务检查是否是管理员 func IsSuperAdmin(ctx context.Context) (res bool, err error) { headers := setCtxHeader(ctx) var r = make(map[string]bool) if err = commonHttp.Get(ctx, "admin-go/api/v1/system/user/checkIsSuperAdmin", headers, &r); err != nil { return false, err } return r["isSuperAdmin"], err } func Upload(ctx context.Context, req *dto.UploadFileBytesReq) (*dto.UploadFileBytesRes, error) { body := &bytes.Buffer{} writer := multipart.NewWriter(body) part, err := writer.CreateFormFile("file", req.FileName) if err != nil { return nil, err } if _, err = part.Write(req.FileBytes); err != nil { return nil, err } if err = writer.Close(); err != nil { return nil, err } headers := setCtxHeader(ctx) headers["Content-Type"] = writer.FormDataContentType() // 发起上传请求 res := &dto.UploadFileBytesRes{} httpUrl := "oss/file/uploadFile" if err = commonHttp.Post(ctx, httpUrl, headers, res, body.Bytes()); err != nil { return nil, err } return res, nil } func setCtxHeader(ctx context.Context) map[string]string { headers := make(map[string]string) if r := g.RequestFromCtx(ctx); r != nil { for k, v := range r.Request.Header { if len(v) > 0 { headers[k] = v[0] } } } // 后台异步任务(临时路径转存 OSS)ctx 无 HTTP 请求、无 token: // 用任务体注入的 user(Creator/TenantId)生成 X-User-Info,供 OSS GetUserInfo 识别用户与桶名 if headers["X-User-Info"] == "" { if user := ctx.Value("user"); !g.IsNil(user) { headers["X-User-Info"] = gconv.String(user) } } // 直连场景(请求头无 X-User-Info、ctx 未注入 user):解析调用方 token 得到用户, // 生成 X-User-Info,供 admin-go 内部租户接口做归属校验(调用方只能操作自己所属租户) if headers["X-User-Info"] == "" { if user, err := utils.GetUserInfo(ctx); err == nil && user != nil { headers["X-User-Info"] = gconv.String(user) } } return headers } // modelCallHeaderTimeout 模型响应头等待超时。 // commonHttp 底层 gclient 默认 ResponseHeaderTimeout 只有 30s,模型生成首字节 // (尤其非流式、大 max_tokens)经常超过 30s,导致 http2: timeout awaiting response // headers。模型调用必须用独立 client 并把该超时调大,与模型配置的超时保持一致。 const modelCallHeaderTimeout = 30 * time.Minute // modelHTTPClient 构建模型调用专用 HTTP client: // 克隆 commonHttp 客户端(保留 ContentJson、header 注入等行为),但把 // ResponseHeaderTimeout 从默认 30s 调大到 modelCallHeaderTimeout。 func modelHTTPClient() *gclient.Client { client := commonHttp.Httpclient.Clone() if tr, ok := client.Transport.(*http.Transport); ok { tr = tr.Clone() // 独立拷贝,避免改动全局共享 transport tr.ResponseHeaderTimeout = modelCallHeaderTimeout client.Transport = tr } return client } // modelDoRaw 模型 HTTP 请求(等价 commonHttp.doRequestRaw,但使用调大超时的 client) func modelDoRaw(ctx context.Context, method string, url string, headers map[string]string, data ...any) (*gclient.Response, error) { client := modelHTTPClient() if (method == http.MethodPost || method == http.MethodPut || method == http.MethodDelete) && len(data) > 0 { client = client.ContentJson() } if len(headers) > 0 { client.SetHeaderMap(headers) } else if r := g.RequestFromCtx(ctx); r != nil { client.SetHeader("Authorization", r.Request.Header.Get("Authorization")) } var response *gclient.Response var err error if method == http.MethodGet && len(data) > 0 && len(data)%2 == 0 { queryParams := make(map[string]string) for i := 0; i < len(data); i += 2 { if key, ok := data[i].(string); ok && i+1 < len(data) { queryParams[key] = gconv.String(data[i+1]) } } response, err = client.DoRequest(ctx, method, url, queryParams) } else if len(data) == 1 { response, err = client.DoRequest(ctx, method, url, data[0]) } else { response, err = client.DoRequest(ctx, method, url, data...) } return response, err } // ModelHttpNormalRequest 同步/异步 普通HTTP全量请求 func ModelHttpNormalRequest(ctx context.Context, url string, headers map[string]string, httpMethod string, body map[string]any) (res []byte, err error) { response, err := modelDoRaw(ctx, httpMethod, url, headers, body) if err != nil { g.Log().Errorf(ctx, "[HttpModel] 模型请求失败 [Error]: %v", err) return nil, fmt.Errorf("模型请求失败: %w", err) } defer response.Close() return response.ReadAll(), nil } // ModelHttpStreamRequest 通用流式请求 // stream=true 时设置 SSE 头并验证 Flusher;stream=false 时只返回 Reader,不设置响应头 func ModelHttpStreamRequest(ctx context.Context, w http.ResponseWriter, url string, headers map[string]string, httpMethod string, body map[string]any) (io.Reader, error) { // 1) 先发起上游请求(此时还没写任何 SSE 头,失败可以正常返回 error) response, err := modelDoRaw(ctx, httpMethod, url, headers, body) if err != nil { g.Log().Errorf(ctx, "[HttpModel] 模型流式请求失败 [Error]: %v", err) return nil, fmt.Errorf("模型流式请求失败: %w", err) } // 检查 HTTP 状态码 if response.StatusCode < 200 || response.StatusCode >= 300 { bodyBytes, _ := io.ReadAll(response.Body) response.Close() return nil, fmt.Errorf("[HTTP][Stream] 状态码异常: %d, body=%s", response.StatusCode, string(bodyBytes)) } if w != nil { // 2) 上游连接成功,再设置 SSE 头 h := w.Header() h.Set("Content-Type", "text/event-stream; charset=utf-8") h.Set("Cache-Control", "no-cache") h.Set("Connection", "keep-alive") h.Set("X-Accel-Buffering", "no") if _, ok := w.(http.Flusher); !ok { response.Close() return nil, errors.New("response writer not support flush") } } // 下层统一托管关闭:用包装器保证流最终关闭 return &autoCloseReader{r: response.Body}, nil } // autoCloseReader 包装 io.ReadCloser,读取结束/销毁时自动 Close type autoCloseReader struct { r io.ReadCloser } func (a *autoCloseReader) Read(p []byte) (int, error) { n, err := a.r.Read(p) // 读取完毕 / 读出错,主动关闭流 if err != nil { _ = a.r.Close() } return n, err } // SSE 常量 const ( ssePrefixData = "data:" ssePrefixEvent = "event:" ssePrefixComment = ":" sseStreamDone = "[DONE]" scanBufInitSize = 64 * 1024 // 64KB scanMaxLineSize = 1024 * 1024 // 单行最大 1MB ) // ParseSSEStream 标准 SSE 流式解析,逐分片回调,支持多行data、上下文取消 func ParseSSEStream(ctx context.Context, respBody io.Reader, onChunk func(ctx context.Context, chunk map[string]any) error) { scanner := bufio.NewScanner(respBody) scanner.Buffer(make([]byte, 0, scanBufInitSize), scanMaxLineSize) var dataBuilder strings.Builder for scanner.Scan() { // 监听上下文取消,及时终止 select { case <-ctx.Done(): g.Log().Infof(ctx, "[SSE] 上下文取消,终止流读取: %v", ctx.Err()) return default: } line := scanner.Text() // 跳过注释、事件行 if strings.HasPrefix(line, ssePrefixComment) || strings.HasPrefix(line, ssePrefixEvent) { continue } lineTrim := strings.TrimSpace(line) // 空行 = 一个SSE事件结束 if lineTrim == "" { if dataBuilder.Len() == 0 { continue } dataStr := dataBuilder.String() dataBuilder.Reset() if dataStr == sseStreamDone { continue } var chunk map[string]any if err := json.Unmarshal([]byte(dataStr), &chunk); err != nil { g.Log().Debugf(ctx, "[SSE] JSON解析失败: %s, err: %v", dataStr, err) continue } if onChunk != nil { onChunk(ctx, chunk) } continue } // 拼接多行 data 数据 if strings.HasPrefix(line, ssePrefixData) { raw := strings.TrimPrefix(line, ssePrefixData) dataBuilder.WriteString(strings.TrimSpace(raw)) } } // 捕获读取异常 if err := scanner.Err(); err != nil { g.Log().Errorf(ctx, "[SSE] 流读取异常: %v", err) return } g.Log().Infof(ctx, "[SSE] 流式读取正常结束") }