Files
common/ragflow/client.go
2026-03-12 08:51:07 +08:00

187 lines
5.7 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 ragflow
import (
"bytes"
"context"
"io"
"net"
"net/http"
"net/url"
"strings"
"sync"
"time"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
)
// gclient 完全不能用!
// 1. New() 默认 ResponseHeaderTimeout=30s
// 2. Clone() 内部调用 New(),链式调用会重置 Transport
// 3. 必须用原生 http.Client
var (
// globalClient 全局 RAGFlow 客户端(单例,延迟初始化)
globalClient *Client
clientOnce sync.Once
)
// initClient 延迟初始化客户端
func initClient() {
clientOnce.Do(func() {
ctx := context.Background()
// 读取配置
baseURL, apiKey := loadConfig(ctx)
// 如果配置不完整,跳过初始化
if baseURL == "" || apiKey == "" {
g.Log().Warning(ctx, "⚠️ RAGFlow 配置未找到,请在项目 config.yml 中添加 ragflow.base_url 和 ragflow.api_key")
return
}
// 自定义 Transport增大连接池解决并发连接不足导致的超时
transport := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
MaxIdleConns: 200, // 最大空闲连接数
MaxIdleConnsPerHost: 100, // 每个 host 最大空闲连接数(关键!默认只有 2
MaxConnsPerHost: 100, // 每个 host 最大连接数
IdleConnTimeout: 90 * time.Second, // 空闲连接超时
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
ResponseHeaderTimeout: 180 * time.Second, // 等待响应头超时(关键!)
}
// 使用原生 http.Clientgclient 完全不能用Clone() 内部调用 New() 会重置 Transport
httpClient := &http.Client{
Transport: transport,
Timeout: 0, // 不设置全局超时,由 context 控制
}
// 验证 Transport 设置
g.Log().Infof(ctx, "✅ Transport 配置: ResponseHeaderTimeout=%v, MaxIdleConnsPerHost=%d, DisableKeepAlives=%v",
transport.ResponseHeaderTimeout, transport.MaxIdleConnsPerHost, transport.DisableKeepAlives)
globalClient = &Client{
BaseURL: strings.TrimSuffix(baseURL, "/"),
APIKey: apiKey,
HTTPClient: httpClient,
}
g.Log().Infof(ctx, "✅ RAGFlow 全局客户端初始化成功: baseURL=%s", baseURL)
})
}
// loadConfig 从配置文件加载 RAGFlow 配置
func loadConfig(ctx context.Context) (baseURL, apiKey string) {
// 使用 GoFrame 全局配置(从项目的 config.yml 读取)
baseURL = g.Cfg().MustGet(ctx, "ragflow.base_url", "").String()
apiKey = g.Cfg().MustGet(ctx, "ragflow.api_key", "").String()
return
}
// GetGlobalClient 获取全局客户端(延迟初始化)
// 使用示例client := ragflow.GetGlobalClient()
func GetGlobalClient() *Client {
initClient()
return globalClient
}
// Client RAGFlow API 客户端
type Client struct {
BaseURL string
APIKey string
HTTPClient *http.Client // 原生 HTTP 客户端gclient 不能用)
}
// CommonResponse 通用响应结构
type CommonResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
// IsSuccess 检查响应是否成功
func (r *CommonResponse) IsSuccess() bool {
return r.Code == 0
}
// request 发送 HTTP 请求
func (c *Client) request(ctx context.Context, method, path string, body interface{}, result interface{}) (err error) {
fullURL := c.BaseURL + path
var reqBody string
if body != nil {
jsonData, err := gjson.Encode(body)
if err != nil {
return gerror.Newf("marshal request body failed: %v", err)
}
reqBody = string(jsonData)
}
// 使用独立的 context 设置 300 秒超时RAGFlow 高并发时响应较慢)
reqCtx, cancel := context.WithTimeout(context.Background(), 300*time.Second)
defer cancel()
startTime := time.Now()
// 创建请求
req, err := http.NewRequestWithContext(reqCtx, method, fullURL, bytes.NewReader([]byte(reqBody)))
if err != nil {
return gerror.Newf("create request failed: %v", err)
}
// 设置请求头
req.Header.Set("Authorization", "Bearer "+c.APIKey)
req.Header.Set("Content-Type", "application/json")
// 发送请求
g.Log().Infof(ctx, "[RAGFlow HTTP] 发送请求: method=%s, url=%s", method, fullURL)
resp, err := c.HTTPClient.Do(req)
elapsed := time.Since(startTime)
if err != nil {
g.Log().Errorf(ctx, "[RAGFlow HTTP] 请求失败(耗时 %v): method=%s, url=%s, error=%v", elapsed, method, fullURL, err)
return gerror.Newf("request failed: %v", err)
}
g.Log().Infof(ctx, "[RAGFlow HTTP] 收到响应(耗时 %v): status=%d, url=%s", elapsed, resp.StatusCode, fullURL)
defer resp.Body.Close()
// 读取响应
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return gerror.Newf("read response failed: %v", err)
}
// 打印响应详情
g.Log().Debugf(ctx, "[RAGFlow HTTP] 响应: status=%d, body=%s", resp.StatusCode, respBody)
if resp.StatusCode != http.StatusOK {
g.Log().Errorf(ctx, "[RAGFlow HTTP] 非200响应: status=%d, body=%s", resp.StatusCode, respBody)
return gerror.Newf("http status %d: %s", resp.StatusCode, respBody)
}
if err = gjson.DecodeTo(respBody, result); err != nil {
g.Log().Errorf(ctx, "[RAGFlow HTTP] 解析响应失败: body=%s, error=%v", string(respBody), err)
return gerror.Newf("unmarshal response failed: %v", err)
}
return
}
// buildQueryString 构建查询字符串
func buildQueryString(params map[string]interface{}) string {
if len(params) == 0 {
return ""
}
parts := make([]string, 0, len(params))
for k, v := range params {
parts = append(parts, url.QueryEscape(k)+"="+url.QueryEscape(g.NewVar(v).String()))
}
return strings.Join(parts, "&")
}