From d348c725045bbab7b1ac5826f0dd11c17cea2f5a Mon Sep 17 00:00:00 2001 From: qian Date: Sat, 6 Jun 2026 09:27:06 +0800 Subject: [PATCH] =?UTF-8?q?fix(model):=E4=BF=AE=E5=A4=8D=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E6=9C=AA=E4=BD=BF=E7=94=A8=E4=BB=A3=E7=90=86?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/logic/admin/channel_test_request.go | 238 ++++++++----------- internal/logic/admin/channel_testing.go | 28 ++- 2 files changed, 131 insertions(+), 135 deletions(-) diff --git a/internal/logic/admin/channel_test_request.go b/internal/logic/admin/channel_test_request.go index 159c1a6..06f11d9 100644 --- a/internal/logic/admin/channel_test_request.go +++ b/internal/logic/admin/channel_test_request.go @@ -1,121 +1,126 @@ package admin import ( + "bytes" "context" "encoding/json" "fmt" + "io" + "net/http" "strings" - "time" "github.com/gogf/gf/v2/frame/g" - "github.com/qianfree/team-api/api/admin/v1" + v1 "github.com/qianfree/team-api/api/admin/v1" + relaycommon "github.com/qianfree/team-api/relay/common" "github.com/qianfree/team-api/relay/constant" ) // sendTestRequest 发送最小测试请求到上游供应商 -func sendTestRequest(ctx context.Context, channelType int, baseURL, apiKey, modelName string) testResult { +func sendTestRequest(ctx context.Context, channelType int, baseURL, apiKey, modelName string, useProxy bool) testResult { baseURL = strings.TrimSuffix(baseURL, "/") switch constant.ProviderType(channelType) { case constant.ProviderOpenAI: - return testOpenAI(ctx, baseURL, apiKey, modelName) + return testOpenAI(ctx, baseURL, apiKey, modelName, useProxy) case constant.ProviderClaude: - return testClaude(ctx, baseURL, apiKey, modelName) + return testClaude(ctx, baseURL, apiKey, modelName, useProxy) case constant.ProviderGemini: - return testGemini(ctx, baseURL, apiKey, modelName) + return testGemini(ctx, baseURL, apiKey, modelName, useProxy) case constant.ProviderZhipu: - return testZhipu(ctx, baseURL, apiKey, modelName) + return testZhipu(ctx, baseURL, apiKey, modelName, useProxy) default: // 尝试 OpenAI 兼容格式 - return testOpenAI(ctx, baseURL, apiKey, modelName) + return testOpenAI(ctx, baseURL, apiKey, modelName, useProxy) } } -func testOpenAI(ctx context.Context, baseURL, apiKey, modelName string) testResult { +// doTestHTTPRequest 发送测试HTTP请求,根据渠道设置决定是否使用代理 +func doTestHTTPRequest(ctx context.Context, method, reqURL string, headers map[string]string, body []byte, useProxy bool) (*http.Response, error) { + client := relaycommon.NewPooledClient(30, useProxy) + + req, err := http.NewRequestWithContext(ctx, method, reqURL, bytes.NewReader(body)) + if err != nil { + return nil, fmt.Errorf("创建请求失败: %w", err) + } + + for k, v := range headers { + req.Header.Set(k, v) + } + + return client.Do(req) +} + +// executeTest 封装通用的测试请求执行和结果处理逻辑 +func executeTest(ctx context.Context, provider, method, reqURL string, headers map[string]string, body []byte, useProxy bool) testResult { + maskedHdrs := maskHeaders(headers) + bodyStr := string(body) + + g.Log().Infof(ctx, "[ChannelTest] %s | 发送请求 | %s %s | 代理: %v", provider, method, reqURL, useProxy) + + resp, err := doTestHTTPRequest(ctx, method, reqURL, headers, body, useProxy) + if err != nil { + g.Log().Warningf(ctx, "[ChannelTest] %s | 请求失败 | %s | 错误: %v", provider, reqURL, err) + return testResult{ + Error: fmt.Sprintf("请求失败: %v", err), + Request: buildReqDetail(method, reqURL, maskedHdrs, bodyStr), + } + } + defer resp.Body.Close() + + respBody, _ := io.ReadAll(resp.Body) + respStr := string(respBody) + + if resp.StatusCode != 200 { + g.Log().Warningf(ctx, "[ChannelTest] %s | 测试失败 | HTTP %d | 响应: %s", provider, resp.StatusCode, truncateStr(respStr, 200)) + return testResult{ + Success: false, + Error: fmt.Sprintf("HTTP %d: %s", resp.StatusCode, truncateStr(respStr, 500)), + Response: truncateStr(respStr, 500), + Request: buildReqDetail(method, reqURL, maskedHdrs, bodyStr), + } + } + + g.Log().Infof(ctx, "[ChannelTest] %s | 测试成功 | HTTP %d | 响应: %s", provider, resp.StatusCode, truncateStr(respStr, 200)) + return testResult{ + Success: true, + Response: truncateStr(respStr, 500), + Request: buildReqDetail(method, reqURL, maskedHdrs, bodyStr), + } +} + +func testOpenAI(ctx context.Context, baseURL, apiKey, modelName string, useProxy bool) testResult { reqBody := map[string]any{ "model": modelName, "max_tokens": 5, "messages": []map[string]string{{"role": "user", "content": "hi"}}, } bodyJSON, _ := json.Marshal(reqBody) - reqURL := baseURL + "/v1/chat/completions" - - client := g.Client().SetTimeout(30 * time.Second) - client.SetHeader("Authorization", "Bearer "+apiKey) - client.SetHeader("Content-Type", "application/json") - - resp, err := client.DoRequest(ctx, "POST", reqURL, reqBody) - if err != nil { - return testResult{ - Error: fmt.Sprintf("请求失败: %v", err), - Request: buildReqDetail("POST", reqURL, map[string]string{"Authorization": maskKey(apiKey), "Content-Type": "application/json"}, string(bodyJSON)), - } - } - defer resp.Close() - - respBody := string(resp.ReadAll()) - - if resp.StatusCode != 200 { - return testResult{ - Success: false, - Error: fmt.Sprintf("HTTP %d: %s", resp.StatusCode, truncateStr(respBody, 500)), - Response: truncateStr(respBody, 500), - Request: buildReqDetail("POST", reqURL, map[string]string{"Authorization": maskKey(apiKey), "Content-Type": "application/json"}, string(bodyJSON)), - } - } - - return testResult{ - Success: true, - Response: truncateStr(respBody, 500), - Request: buildReqDetail("POST", reqURL, map[string]string{"Authorization": maskKey(apiKey), "Content-Type": "application/json"}, string(bodyJSON)), + headers := map[string]string{ + "Authorization": "Bearer " + apiKey, + "Content-Type": "application/json", } + return executeTest(ctx, "OpenAI", "POST", reqURL, headers, bodyJSON, useProxy) } -func testClaude(ctx context.Context, baseURL, apiKey, modelName string) testResult { +func testClaude(ctx context.Context, baseURL, apiKey, modelName string, useProxy bool) testResult { reqBody := map[string]any{ "model": modelName, "max_tokens": 5, "messages": []map[string]string{{"role": "user", "content": "hi"}}, } bodyJSON, _ := json.Marshal(reqBody) - reqURL := baseURL + "/v1/messages" - - client := g.Client().SetTimeout(30 * time.Second) - client.SetHeader("x-api-key", apiKey) - client.SetHeader("anthropic-version", "2023-06-01") - client.SetHeader("Content-Type", "application/json") - - resp, err := client.DoRequest(ctx, "POST", reqURL, reqBody) - if err != nil { - return testResult{ - Error: fmt.Sprintf("请求失败: %v", err), - Request: buildReqDetail("POST", reqURL, map[string]string{"x-api-key": maskKey(apiKey), "anthropic-version": "2023-06-01", "Content-Type": "application/json"}, string(bodyJSON)), - } - } - defer resp.Close() - - respBody := string(resp.ReadAll()) - - if resp.StatusCode != 200 { - return testResult{ - Success: false, - Error: fmt.Sprintf("HTTP %d: %s", resp.StatusCode, truncateStr(respBody, 500)), - Response: truncateStr(respBody, 500), - Request: buildReqDetail("POST", reqURL, map[string]string{"x-api-key": maskKey(apiKey), "anthropic-version": "2023-06-01", "Content-Type": "application/json"}, string(bodyJSON)), - } - } - - return testResult{ - Success: true, - Response: truncateStr(respBody, 500), - Request: buildReqDetail("POST", reqURL, map[string]string{"x-api-key": maskKey(apiKey), "anthropic-version": "2023-06-01", "Content-Type": "application/json"}, string(bodyJSON)), + headers := map[string]string{ + "x-api-key": apiKey, + "anthropic-version": "2023-06-01", + "Content-Type": "application/json", } + return executeTest(ctx, "Claude", "POST", reqURL, headers, bodyJSON, useProxy) } -func testGemini(ctx context.Context, baseURL, apiKey, modelName string) testResult { +func testGemini(ctx context.Context, baseURL, apiKey, modelName string, useProxy bool) testResult { reqBody := map[string]any{ "contents": []map[string]any{ {"role": "user", "parts": []map[string]string{{"text": "hi"}}}, @@ -125,79 +130,27 @@ func testGemini(ctx context.Context, baseURL, apiKey, modelName string) testResu }, } bodyJSON, _ := json.Marshal(reqBody) - reqURL := fmt.Sprintf("%s/v1beta/models/%s:generateContent", baseURL, modelName) - - client := g.Client().SetTimeout(30 * time.Second) - client.SetHeader("x-goog-api-key", apiKey) - client.SetHeader("Content-Type", "application/json") - - resp, err := client.DoRequest(ctx, "POST", reqURL, reqBody) - if err != nil { - return testResult{ - Error: fmt.Sprintf("请求失败: %v", err), - Request: buildReqDetail("POST", reqURL, map[string]string{"x-goog-api-key": maskKey(apiKey), "Content-Type": "application/json"}, string(bodyJSON)), - } - } - defer resp.Close() - - respBody := string(resp.ReadAll()) - - if resp.StatusCode != 200 { - return testResult{ - Success: false, - Error: fmt.Sprintf("HTTP %d: %s", resp.StatusCode, truncateStr(respBody, 500)), - Response: truncateStr(respBody, 500), - Request: buildReqDetail("POST", reqURL, map[string]string{"x-goog-api-key": maskKey(apiKey), "Content-Type": "application/json"}, string(bodyJSON)), - } - } - - return testResult{ - Success: true, - Response: truncateStr(respBody, 500), - Request: buildReqDetail("POST", reqURL, map[string]string{"x-goog-api-key": maskKey(apiKey), "Content-Type": "application/json"}, string(bodyJSON)), + headers := map[string]string{ + "x-goog-api-key": apiKey, + "Content-Type": "application/json", } + return executeTest(ctx, "Gemini", "POST", reqURL, headers, bodyJSON, useProxy) } -func testZhipu(ctx context.Context, baseURL, apiKey, modelName string) testResult { +func testZhipu(ctx context.Context, baseURL, apiKey, modelName string, useProxy bool) testResult { reqBody := map[string]any{ "model": modelName, "max_tokens": 5, "messages": []map[string]string{{"role": "user", "content": "hi"}}, } bodyJSON, _ := json.Marshal(reqBody) - reqURL := baseURL + "/api/paas/v4/chat/completions" - - client := g.Client().SetTimeout(30 * time.Second) - client.SetHeader("Authorization", "Bearer "+apiKey) - client.SetHeader("Content-Type", "application/json") - - resp, err := client.DoRequest(ctx, "POST", reqURL, reqBody) - if err != nil { - return testResult{ - Error: fmt.Sprintf("请求失败: %v", err), - Request: buildReqDetail("POST", reqURL, map[string]string{"Authorization": maskKey(apiKey), "Content-Type": "application/json"}, string(bodyJSON)), - } - } - defer resp.Close() - - respBody := string(resp.ReadAll()) - - if resp.StatusCode != 200 { - return testResult{ - Success: false, - Error: fmt.Sprintf("HTTP %d: %s", resp.StatusCode, truncateStr(respBody, 500)), - Response: truncateStr(respBody, 500), - Request: buildReqDetail("POST", reqURL, map[string]string{"Authorization": maskKey(apiKey), "Content-Type": "application/json"}, string(bodyJSON)), - } - } - - return testResult{ - Success: true, - Response: truncateStr(respBody, 500), - Request: buildReqDetail("POST", reqURL, map[string]string{"Authorization": maskKey(apiKey), "Content-Type": "application/json"}, string(bodyJSON)), + headers := map[string]string{ + "Authorization": "Bearer " + apiKey, + "Content-Type": "application/json", } + return executeTest(ctx, "Zhipu", "POST", reqURL, headers, bodyJSON, useProxy) } func buildReqDetail(method, url string, headers map[string]string, body string) *v1.ChannelTestReqDetail { @@ -209,6 +162,25 @@ func buildReqDetail(method, url string, headers map[string]string, body string) } } +// maskHeaders 对请求头中的敏感信息进行脱敏 +func maskHeaders(headers map[string]string) map[string]string { + masked := make(map[string]string, len(headers)) + for k, v := range headers { + lower := strings.ToLower(k) + if strings.Contains(lower, "key") || strings.Contains(lower, "auth") { + // 保留 "Bearer " 前缀 + if strings.HasPrefix(v, "Bearer ") { + masked[k] = "Bearer " + maskKey(strings.TrimPrefix(v, "Bearer ")) + } else { + masked[k] = maskKey(v) + } + } else { + masked[k] = v + } + } + return masked +} + // maskKey 对 API Key 进行脱敏处理 func maskKey(key string) string { if len(key) <= 8 { diff --git a/internal/logic/admin/channel_testing.go b/internal/logic/admin/channel_testing.go index 5993310..ced2a4b 100644 --- a/internal/logic/admin/channel_testing.go +++ b/internal/logic/admin/channel_testing.go @@ -2,8 +2,11 @@ package admin import ( "context" + "encoding/json" "time" + "github.com/gogf/gf/v2/frame/g" + "github.com/qianfree/team-api/api/admin/v1" "github.com/qianfree/team-api/internal/dao" "github.com/qianfree/team-api/internal/logic/common" @@ -22,12 +25,13 @@ func (s *sAdmin) TestChannel(ctx context.Context, req *v1.ChannelTestReq) (*v1.C BaseURL string `json:"base_url"` TestModel string `json:"test_model"` Status string `json:"status"` + Settings string `json:"settings"` } var ch *channelRow err := dao.ChnChannels.Ctx(ctx). Where("id", channelID). - Fields("id, name, type, base_url, test_model, status"). + Fields("id, name, type, base_url, test_model, status, settings"). Scan(&ch) if err != nil { return nil, err @@ -69,9 +73,20 @@ func (s *sAdmin) TestChannel(ctx context.Context, req *v1.ChannelTestReq) (*v1.C return nil, common.NewBadRequestError("解密 API Key 失败") } + // 解析渠道设置中的 use_proxy + useProxy := false + if ch.Settings != "" { + var settings struct { + UseProxy bool `json:"use_proxy"` + } + if json.Unmarshal([]byte(ch.Settings), &settings) == nil { + useProxy = settings.UseProxy + } + } + // 发送最小测试请求 startTime := time.Now() - result := sendTestRequest(ctx, ch.Type, ch.BaseURL, apiKey, testModel) + result := sendTestRequest(ctx, ch.Type, ch.BaseURL, apiKey, testModel, useProxy) latencyMs := time.Since(startTime).Milliseconds() // 更新健康度 @@ -81,6 +96,15 @@ func (s *sAdmin) TestChannel(ctx context.Context, req *v1.ChannelTestReq) (*v1.C relay.UpdateHealthScoreDirect(ctx, channelID, false, float64(latencyMs)) } + // 记录测试结果日志 + if result.Success { + g.Log().Infof(ctx, "[ChannelTest] 渠道 %s (%d) 测试成功 | 模型: %s | 延迟: %dms | 代理: %v", + ch.Name, channelID, testModel, latencyMs, useProxy) + } else { + g.Log().Warningf(ctx, "[ChannelTest] 渠道 %s (%d) 测试失败 | 模型: %s | 延迟: %dms | 代理: %v | 错误: %s", + ch.Name, channelID, testModel, latencyMs, useProxy, result.Error) + } + return &v1.ChannelTestRes{ Success: result.Success, Latency: latencyMs,