fix(model):修复模型测试未使用代理的问题

This commit is contained in:
qian committed 2026-06-06 09:27:06 +08:00
1 parent 586439a19a
commit d348c72504
2 files changed
+131 -135

No files matched your search

+105 -133
View File
@@ -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 {
+26 -2
View File
@@ -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,