修复智能体bug
This commit is contained in:
@@ -31,32 +31,31 @@ var agentApiHTTPClient = &http.Client{
|
||||
},
|
||||
}
|
||||
|
||||
// agentApiProtocol 上游协议族,决定请求路径、鉴权头与响应解析方式
|
||||
type agentApiProtocol string
|
||||
// 支持的请求协议白名单,与前端 constants.js 的 PROTOCOLS 保持一致
|
||||
var agentApiProtocols = map[string]bool{
|
||||
models.AgentProtocolChatCompletions: true,
|
||||
models.AgentProtocolResponses: true,
|
||||
models.AgentProtocolAnthropic: true,
|
||||
models.AgentProtocolGemini: true,
|
||||
models.AgentProtocolCustom: true,
|
||||
}
|
||||
|
||||
const (
|
||||
protocolAnthropic agentApiProtocol = "anthropic"
|
||||
protocolGemini agentApiProtocol = "gemini"
|
||||
protocolOpenAI agentApiProtocol = "openai"
|
||||
)
|
||||
// agentApiEndpointPath 各协议在根地址之后要追加的路径
|
||||
// custom 与 gemini 不在此表中:custom 不追加任何路径,gemini 需要嵌入模型名
|
||||
var agentApiEndpointPath = map[string]string{
|
||||
models.AgentProtocolChatCompletions: "/chat/completions",
|
||||
models.AgentProtocolResponses: "/responses",
|
||||
models.AgentProtocolAnthropic: "/messages",
|
||||
}
|
||||
|
||||
// detectAgentApiProtocol 由上游名称与地址关键词推断协议族
|
||||
// 上游名称由用户自由填写,因此这里用包含匹配而非枚举比对;
|
||||
// 未命中任何关键词时按 OpenAI 兼容协议处理(绝大多数中转与国产网关都兼容该协议)
|
||||
func detectAgentApiProtocol(provider, url string) agentApiProtocol {
|
||||
hay := strings.ToLower(provider + " " + url)
|
||||
|
||||
for _, k := range []string{"anthropic", "claude"} {
|
||||
if strings.Contains(hay, k) {
|
||||
return protocolAnthropic
|
||||
}
|
||||
// normalizeProtocol 缺省或非法值统一回落到 Chat Completions
|
||||
// 该协议被绝大多数官方接口、国产网关与自建中转支持,作为兜底最安全
|
||||
func normalizeProtocol(p string) string {
|
||||
p = strings.TrimSpace(strings.ToLower(p))
|
||||
if p == "" || !agentApiProtocols[p] {
|
||||
return models.AgentProtocolChatCompletions
|
||||
}
|
||||
for _, k := range []string{"gemini", "generativelanguage", "googleapis"} {
|
||||
if strings.Contains(hay, k) {
|
||||
return protocolGemini
|
||||
}
|
||||
}
|
||||
return protocolOpenAI
|
||||
return p
|
||||
}
|
||||
|
||||
func (c *PlatformAgentApiController) jsonErr(httpStatus, bizCode int, msg string) {
|
||||
@@ -97,6 +96,7 @@ func (c *PlatformAgentApiController) platformClaims() (*jwtutil.Claims, error) {
|
||||
// ApiKeys 用指针:编辑时不传该字段表示不修改已存密钥列表
|
||||
type agentApiPayload struct {
|
||||
Provider string `json:"provider"`
|
||||
Protocol string `json:"protocol"`
|
||||
BaseURL string `json:"base_url"`
|
||||
UseCustomURL int8 `json:"use_custom_url"`
|
||||
CustomURL string `json:"custom_url"`
|
||||
@@ -214,9 +214,11 @@ func modelsFromJSON(raw string) []string {
|
||||
// 密钥以明文返回,前端卡片默认脱敏、可切换显示;接口本身已受平台端鉴权保护
|
||||
func agentApiToMap(row *models.PlatformAgentApi) map[string]interface{} {
|
||||
keys := apiKeysFromJSON(row.ApiKeys)
|
||||
protocol := normalizeProtocol(row.Protocol)
|
||||
out := map[string]interface{}{
|
||||
"id": row.ID,
|
||||
"provider": row.Provider,
|
||||
"protocol": protocol,
|
||||
"api_keys": keys,
|
||||
"key_count": len(keys),
|
||||
"base_url": row.BaseURL,
|
||||
@@ -247,6 +249,15 @@ func validateAgentApiPayload(p *agentApiPayload, isCreate bool) error {
|
||||
return fmt.Errorf("上游接口名称过长")
|
||||
}
|
||||
|
||||
// 请求协议必须是白名单内的值,缺省回落到 Chat Completions
|
||||
p.Protocol = strings.TrimSpace(strings.ToLower(p.Protocol))
|
||||
if p.Protocol == "" {
|
||||
p.Protocol = models.AgentProtocolChatCompletions
|
||||
}
|
||||
if !agentApiProtocols[p.Protocol] {
|
||||
return fmt.Errorf("请求协议不正确")
|
||||
}
|
||||
|
||||
p.BaseURL = strings.TrimSpace(p.BaseURL)
|
||||
p.CustomURL = strings.TrimSpace(p.CustomURL)
|
||||
|
||||
@@ -297,6 +308,33 @@ func isHTTPURL(s string) bool {
|
||||
return strings.HasPrefix(low, "http://") || strings.HasPrefix(low, "https://")
|
||||
}
|
||||
|
||||
// buildAgentEndpoint 依据协议算出实际请求地址
|
||||
// custom 协议直接把填写的地址当完整端点,不做任何追加;
|
||||
// 其余协议在根地址后追加各自路径,若地址已以该路径结尾则不重复追加
|
||||
func buildAgentEndpoint(protocol, base, model, apiKey string) string {
|
||||
base = strings.TrimRight(base, "/")
|
||||
|
||||
switch protocol {
|
||||
case models.AgentProtocolCustom:
|
||||
return base
|
||||
|
||||
case models.AgentProtocolGemini:
|
||||
// 模型名嵌在路径中间,无固定后缀可判重
|
||||
return fmt.Sprintf("%s/models/%s:generateContent?key=%s", base, model, apiKey)
|
||||
|
||||
default:
|
||||
path := agentApiEndpointPath[protocol]
|
||||
if path == "" {
|
||||
path = "/chat/completions"
|
||||
}
|
||||
// 用户可能填根地址,也可能直接填完整端点,后者不再追加
|
||||
if strings.HasSuffix(strings.ToLower(base), strings.ToLower(path)) {
|
||||
return base
|
||||
}
|
||||
return base + path
|
||||
}
|
||||
}
|
||||
|
||||
// List GET /platform/agentApi/list?page=1&pageSize=24&keyword=&provider=&status=
|
||||
func (c *PlatformAgentApiController) List() {
|
||||
if _, err := c.platformClaims(); err != nil {
|
||||
@@ -446,6 +484,7 @@ func (c *PlatformAgentApiController) Create() {
|
||||
userID := uint64(claims.UserID)
|
||||
row := &models.PlatformAgentApi{
|
||||
Provider: p.Provider,
|
||||
Protocol: p.Protocol,
|
||||
BaseURL: p.BaseURL,
|
||||
UseCustomURL: p.UseCustomURL,
|
||||
CustomURL: p.CustomURL,
|
||||
@@ -500,6 +539,7 @@ func (c *PlatformAgentApiController) Update() {
|
||||
now := time.Now()
|
||||
updates := map[string]interface{}{
|
||||
"provider": p.Provider,
|
||||
"protocol": p.Protocol,
|
||||
"base_url": p.BaseURL,
|
||||
"use_custom_url": p.UseCustomURL,
|
||||
"custom_url": p.CustomURL,
|
||||
@@ -640,6 +680,7 @@ func (c *PlatformAgentApiController) ToggleStatus() {
|
||||
}
|
||||
|
||||
// agentApiTestResult 测试结果统一结构,与前端 test.vue 字段对应
|
||||
// Endpoint 回显实际请求的完整地址,便于排查路径拼接类问题(如上游根地址少了版本前缀)
|
||||
type agentApiTestResult struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
@@ -647,10 +688,20 @@ type agentApiTestResult struct {
|
||||
LatencyMs int64 `json:"latency_ms"`
|
||||
Model string `json:"model,omitempty"`
|
||||
KeyRemark string `json:"key_remark,omitempty"`
|
||||
Endpoint string `json:"endpoint,omitempty"`
|
||||
Response string `json:"response,omitempty"`
|
||||
Detail string `json:"detail,omitempty"`
|
||||
}
|
||||
|
||||
// redactKeyInURL 隐去 URL 查询串里的密钥
|
||||
// Gemini 协议把 key 放在 query 中,回显 endpoint 前必须脱敏
|
||||
func redactKeyInURL(rawURL, key string) string {
|
||||
if key == "" {
|
||||
return rawURL
|
||||
}
|
||||
return strings.ReplaceAll(rawURL, key, "***")
|
||||
}
|
||||
|
||||
// Test POST /platform/agentApi/test
|
||||
// 请求体:{ "id": 1, "model": "gpt-4o", "key_index": 0, "prompt": "你好" }
|
||||
// key_index 指定用密钥列表中的第几个密钥,缺省用第一个
|
||||
@@ -726,7 +777,7 @@ func (c *PlatformAgentApiController) Test() {
|
||||
c.ok("success", result)
|
||||
}
|
||||
|
||||
// probeAgentApi 用指定密钥按上游协议族发起一次最小化对话请求
|
||||
// probeAgentApi 用指定密钥按配置的请求协议发起一次最小化对话请求
|
||||
func probeAgentApi(
|
||||
row *models.PlatformAgentApi,
|
||||
apiKey models.AgentApiKey,
|
||||
@@ -740,18 +791,18 @@ func probeAgentApi(
|
||||
}
|
||||
}
|
||||
|
||||
protocol := detectAgentApiProtocol(row.Provider, base)
|
||||
// 协议由配置显式指定,不再根据上游名称猜测
|
||||
protocol := normalizeProtocol(row.Protocol)
|
||||
endpoint := buildAgentEndpoint(protocol, base, model, apiKey.Key)
|
||||
|
||||
var (
|
||||
endpoint string
|
||||
body []byte
|
||||
headers = map[string]string{"Content-Type": "application/json"}
|
||||
err error
|
||||
body []byte
|
||||
headers = map[string]string{"Content-Type": "application/json"}
|
||||
err error
|
||||
)
|
||||
|
||||
switch protocol {
|
||||
case protocolAnthropic:
|
||||
endpoint = base + "/messages"
|
||||
case models.AgentProtocolAnthropic:
|
||||
headers["x-api-key"] = apiKey.Key
|
||||
headers["anthropic-version"] = "2023-06-01"
|
||||
body, err = json.Marshal(map[string]interface{}{
|
||||
@@ -762,17 +813,25 @@ func probeAgentApi(
|
||||
},
|
||||
})
|
||||
|
||||
case protocolGemini:
|
||||
endpoint = fmt.Sprintf("%s/models/%s:generateContent?key=%s", base, model, apiKey.Key)
|
||||
case models.AgentProtocolGemini:
|
||||
// 密钥已放在 query 中,无需鉴权头
|
||||
body, err = json.Marshal(map[string]interface{}{
|
||||
"contents": []map[string]interface{}{
|
||||
{"parts": []map[string]string{{"text": prompt}}},
|
||||
},
|
||||
})
|
||||
|
||||
case models.AgentProtocolResponses:
|
||||
// Responses API 用 input 字段替代 messages
|
||||
headers["Authorization"] = "Bearer " + apiKey.Key
|
||||
body, err = json.Marshal(map[string]interface{}{
|
||||
"model": model,
|
||||
"input": prompt,
|
||||
"max_output_tokens": 64,
|
||||
})
|
||||
|
||||
default:
|
||||
// OpenAI 兼容协议,覆盖大多数官方接口、国产网关与自建中转
|
||||
endpoint = base + "/chat/completions"
|
||||
// Chat Completions 与 custom 均按 OpenAI 标准报文发送
|
||||
headers["Authorization"] = "Bearer " + apiKey.Key
|
||||
body, err = json.Marshal(map[string]interface{}{
|
||||
"model": model,
|
||||
@@ -795,12 +854,15 @@ func probeAgentApi(
|
||||
return agentApiTestResult{
|
||||
Success: false, Message: "构造请求失败", Detail: err.Error(),
|
||||
Model: model, KeyRemark: apiKey.Remark,
|
||||
Endpoint: redactKeyInURL(endpoint, apiKey.Key),
|
||||
}
|
||||
}
|
||||
for k, v := range headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
|
||||
safeEndpoint := redactKeyInURL(endpoint, apiKey.Key)
|
||||
|
||||
start := time.Now()
|
||||
resp, err := agentApiHTTPClient.Do(req)
|
||||
latency := time.Since(start).Milliseconds()
|
||||
@@ -813,6 +875,7 @@ func probeAgentApi(
|
||||
LatencyMs: latency,
|
||||
Model: model,
|
||||
KeyRemark: apiKey.Remark,
|
||||
Endpoint: safeEndpoint,
|
||||
}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
@@ -822,13 +885,19 @@ func probeAgentApi(
|
||||
preview := string(respBody)
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
msg := fmt.Sprintf("上游返回 HTTP %d", resp.StatusCode)
|
||||
// 404 多半是接口地址少了版本前缀,给出可操作的提示
|
||||
if resp.StatusCode == 404 {
|
||||
msg += ":接口地址可能不正确,请检查是否缺少版本路径(如 /v1、/compatible-mode/v2)"
|
||||
}
|
||||
return agentApiTestResult{
|
||||
Success: false,
|
||||
Message: fmt.Sprintf("上游返回 HTTP %d", resp.StatusCode),
|
||||
Message: msg,
|
||||
StatusCode: resp.StatusCode,
|
||||
LatencyMs: latency,
|
||||
Model: model,
|
||||
KeyRemark: apiKey.Remark,
|
||||
Endpoint: safeEndpoint,
|
||||
Detail: preview,
|
||||
}
|
||||
}
|
||||
@@ -840,14 +909,15 @@ func probeAgentApi(
|
||||
LatencyMs: latency,
|
||||
Model: model,
|
||||
KeyRemark: apiKey.Remark,
|
||||
Endpoint: safeEndpoint,
|
||||
Response: extractAgentReply(protocol, respBody, preview),
|
||||
}
|
||||
}
|
||||
|
||||
// extractAgentReply 从上游响应里提取模型回复文本,解析失败则回退为原始预览
|
||||
func extractAgentReply(protocol agentApiProtocol, respBody []byte, fallback string) string {
|
||||
func extractAgentReply(protocol string, respBody []byte, fallback string) string {
|
||||
switch protocol {
|
||||
case protocolAnthropic:
|
||||
case models.AgentProtocolAnthropic:
|
||||
var r struct {
|
||||
Content []struct {
|
||||
Text string `json:"text"`
|
||||
@@ -857,7 +927,7 @@ func extractAgentReply(protocol agentApiProtocol, respBody []byte, fallback stri
|
||||
return r.Content[0].Text
|
||||
}
|
||||
|
||||
case protocolGemini:
|
||||
case models.AgentProtocolGemini:
|
||||
var r struct {
|
||||
Candidates []struct {
|
||||
Content struct {
|
||||
@@ -872,6 +942,25 @@ func extractAgentReply(protocol agentApiProtocol, respBody []byte, fallback stri
|
||||
return r.Candidates[0].Content.Parts[0].Text
|
||||
}
|
||||
|
||||
case models.AgentProtocolResponses:
|
||||
// Responses API 优先取聚合字段 output_text,回退到 output 数组
|
||||
var r struct {
|
||||
OutputText string `json:"output_text"`
|
||||
Output []struct {
|
||||
Content []struct {
|
||||
Text string `json:"text"`
|
||||
} `json:"content"`
|
||||
} `json:"output"`
|
||||
}
|
||||
if json.Unmarshal(respBody, &r) == nil {
|
||||
if r.OutputText != "" {
|
||||
return r.OutputText
|
||||
}
|
||||
if len(r.Output) > 0 && len(r.Output[0].Content) > 0 {
|
||||
return r.Output[0].Content[0].Text
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
var r struct {
|
||||
Choices []struct {
|
||||
|
||||
Reference in New Issue
Block a user