package controllers import ( "bytes" "crypto/tls" "encoding/json" "fmt" "io" "net/http" "strconv" "strings" "time" "server/models" "server/pkg/jwtutil" "github.com/beego/beego/v2/client/orm" beego "github.com/beego/beego/v2/server/web" ) // PlatformAgentApiController 智能体API管理(yz_platform_agent_api,平台端) type PlatformAgentApiController struct { beego.Controller } // agentApiHTTPClient 用于探测上游接口连通性 var agentApiHTTPClient = &http.Client{ Timeout: 30 * time.Second, Transport: &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, }, } // 支持的请求协议白名单,与前端 constants.js 的 PROTOCOLS 保持一致 var agentApiProtocols = map[string]bool{ models.AgentProtocolChatCompletions: true, models.AgentProtocolResponses: true, models.AgentProtocolAnthropic: true, models.AgentProtocolGemini: true, models.AgentProtocolCustom: true, } // agentApiEndpointPath 各协议在根地址之后要追加的路径 // custom 与 gemini 不在此表中:custom 不追加任何路径,gemini 需要嵌入模型名 var agentApiEndpointPath = map[string]string{ models.AgentProtocolChatCompletions: "/chat/completions", models.AgentProtocolResponses: "/responses", models.AgentProtocolAnthropic: "/messages", } // normalizeProtocol 缺省或非法值统一回落到 Chat Completions // 该协议被绝大多数官方接口、国产网关与自建中转支持,作为兜底最安全 func normalizeProtocol(p string) string { p = strings.TrimSpace(strings.ToLower(p)) if p == "" || !agentApiProtocols[p] { return models.AgentProtocolChatCompletions } return p } func (c *PlatformAgentApiController) jsonErr(httpStatus, bizCode int, msg string) { c.Ctx.Output.SetStatus(httpStatus) c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg} _ = c.ServeJSON() } func (c *PlatformAgentApiController) ok(msg string, data interface{}) { resp := map[string]interface{}{"code": 200, "msg": msg} if data != nil { resp["data"] = data } c.Data["json"] = resp _ = c.ServeJSON() } func (c *PlatformAgentApiController) platformClaims() (*jwtutil.Claims, error) { auth := c.Ctx.Request.Header.Get("Authorization") if auth == "" { return nil, fmt.Errorf("未登录") } parts := strings.SplitN(auth, " ", 2) if len(parts) != 2 || parts[0] != "Bearer" { return nil, fmt.Errorf("认证信息格式错误") } claims, err := jwtutil.ParseToken(parts[1]) if err != nil { return nil, fmt.Errorf("无效的token") } if claims.UserType != "platform" { return nil, fmt.Errorf("无权访问") } return claims, nil } // agentApiPayload 新增/更新请求体 // 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"` ApiKeys *[]models.AgentApiKey `json:"api_keys"` Models []string `json:"models"` Status *int8 `json:"status"` Remark string `json:"remark"` } // apiKeysToJSON 把密钥列表序列化为 JSON 字符串入库 // 去掉空密钥、按 key 去重,备注允许为空 func apiKeysToJSON(list []models.AgentApiKey) string { cleaned := make([]models.AgentApiKey, 0, len(list)) seen := make(map[string]bool, len(list)) for _, item := range list { k := strings.TrimSpace(item.Key) if k == "" || seen[k] { continue } seen[k] = true cleaned = append(cleaned, models.AgentApiKey{ Key: k, Remark: strings.TrimSpace(item.Remark), }) } b, err := json.Marshal(cleaned) if err != nil { return "[]" } return string(b) } // apiKeysFromJSON 把库里的 JSON 字符串反序列化为密钥列表 // 兼容三种历史/异常格式:对象数组、纯字符串数组、单个裸密钥字符串 func apiKeysFromJSON(raw string) []models.AgentApiKey { raw = strings.TrimSpace(raw) if raw == "" { return []models.AgentApiKey{} } if strings.HasPrefix(raw, "[") { // 标准格式:[{"key":"sk-x","remark":"mimo198"}] var objs []models.AgentApiKey if err := json.Unmarshal([]byte(raw), &objs); err == nil { out := make([]models.AgentApiKey, 0, len(objs)) for _, o := range objs { if strings.TrimSpace(o.Key) != "" { out = append(out, o) } } return out } // 退化格式:["sk-x","sk-y"] var strs []string if err := json.Unmarshal([]byte(raw), &strs); err == nil { out := make([]models.AgentApiKey, 0, len(strs)) for _, s := range strs { if s = strings.TrimSpace(s); s != "" { out = append(out, models.AgentApiKey{Key: s}) } } return out } return []models.AgentApiKey{} } // 迁移遗漏时的兜底:整个字段就是一个裸密钥 return []models.AgentApiKey{{Key: raw}} } // modelsToJSON 把模型数组序列化为 JSON 字符串入库 func modelsToJSON(list []string) string { cleaned := make([]string, 0, len(list)) seen := make(map[string]bool, len(list)) for _, m := range list { m = strings.TrimSpace(m) if m == "" || seen[m] { continue } seen[m] = true cleaned = append(cleaned, m) } b, err := json.Marshal(cleaned) if err != nil { return "[]" } return string(b) } // modelsFromJSON 把库里的 JSON 字符串反序列化为模型数组 // 兼容历史数据可能存的逗号分隔格式 func modelsFromJSON(raw string) []string { raw = strings.TrimSpace(raw) if raw == "" { return []string{} } if strings.HasPrefix(raw, "[") { var out []string if err := json.Unmarshal([]byte(raw), &out); err == nil { return out } return []string{} } parts := strings.Split(raw, ",") out := make([]string, 0, len(parts)) for _, p := range parts { if p = strings.TrimSpace(p); p != "" { out = append(out, p) } } return out } // agentApiToMap 统一输出结构 // 密钥以明文返回,前端卡片默认脱敏、可切换显示;接口本身已受平台端鉴权保护 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, "use_custom_url": row.UseCustomURL, "custom_url": row.CustomURL, "models": modelsFromJSON(row.Models), "status": row.Status, "remark": row.Remark, "user_id": row.UserID, "user_name": row.UserName, "create_time": row.CreateTime.Format("2006-01-02 15:04:05"), "update_time": "", } if row.UpdateTime != nil { out["update_time"] = row.UpdateTime.Format("2006-01-02 15:04:05") } return out } // validateAgentApiPayload 校验必填与地址格式 func validateAgentApiPayload(p *agentApiPayload, isCreate bool) error { // 上游接口为用户自由填写的文本,不做枚举校验 p.Provider = strings.TrimSpace(p.Provider) if p.Provider == "" { return fmt.Errorf("请输入上游接口名称") } if len([]rune(p.Provider)) > 100 { 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) if p.UseCustomURL == 1 { if p.CustomURL == "" { return fmt.Errorf("请输入自定义地址") } if !isHTTPURL(p.CustomURL) { return fmt.Errorf("自定义地址需以 http:// 或 https:// 开头") } } else { p.CustomURL = "" if p.BaseURL == "" { return fmt.Errorf("请输入接口地址") } if !isHTTPURL(p.BaseURL) { return fmt.Errorf("接口地址需以 http:// 或 https:// 开头") } } if len(p.Models) == 0 { return fmt.Errorf("请至少添加一个模型") } // 新增时必须带密钥;编辑时不传表示沿用已存密钥列表 if p.ApiKeys != nil { valid := 0 for _, k := range *p.ApiKeys { if strings.TrimSpace(k.Key) != "" { valid++ } if len([]rune(k.Remark)) > 100 { return fmt.Errorf("密钥备注过长,单条限 100 字") } } if valid == 0 { return fmt.Errorf("请至少添加一个 API Key") } } else if isCreate { return fmt.Errorf("请至少添加一个 API Key") } return nil } func isHTTPURL(s string) bool { low := strings.ToLower(s) 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 { c.jsonErr(401, 401, err.Error()) return } page, _ := c.GetInt("page", 1) pageSize, _ := c.GetInt("pageSize", 24) if page < 1 { page = 1 } if pageSize < 1 { pageSize = 24 } if pageSize > 200 { pageSize = 200 } keyword := strings.TrimSpace(c.GetString("keyword")) provider := strings.TrimSpace(c.GetString("provider")) statusStr := strings.TrimSpace(c.GetString("status")) qs := models.Orm.QueryTable(new(models.PlatformAgentApi)).Filter("is_deleted", 0) cond := orm.NewCondition() needCond := false // 上游名称为自由文本,用模糊匹配而非精确相等 if provider != "" { cond = cond.And("provider__icontains", provider) needCond = true } if statusStr != "" { if st, err := strconv.Atoi(statusStr); err == nil { cond = cond.And("status", st) needCond = true } } if keyword != "" { kw := orm.NewCondition(). Or("provider__icontains", keyword). Or("base_url__icontains", keyword). Or("custom_url__icontains", keyword). Or("models__icontains", keyword). Or("remark__icontains", keyword) cond = cond.AndCond(kw) needCond = true } if needCond { qs = qs.SetCond(cond) } total, err := qs.Count() if err != nil { c.jsonErr(500, 500, "查询失败: "+err.Error()) return } var rows []models.PlatformAgentApi _, err = qs.OrderBy("-id").Limit(pageSize, (page-1)*pageSize).All(&rows) if err != nil && err != orm.ErrNoRows { c.jsonErr(500, 500, "查询失败: "+err.Error()) return } list := make([]map[string]interface{}, 0, len(rows)) for i := range rows { list = append(list, agentApiToMap(&rows[i])) } c.ok("success", map[string]interface{}{ "list": list, "total": total, }) } // Detail GET /platform/agentApi/:id func (c *PlatformAgentApiController) Detail() { if _, err := c.platformClaims(); err != nil { c.jsonErr(401, 401, err.Error()) return } row, err := c.findByID() if err != nil { return } c.ok("success", agentApiToMap(row)) } // findByID 读取路径参数并查询记录,出错时已写入响应 func (c *PlatformAgentApiController) findByID() (*models.PlatformAgentApi, error) { id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64) if err != nil || id == 0 { c.jsonErr(400, 400, "无效ID") return nil, fmt.Errorf("无效ID") } var row models.PlatformAgentApi err = models.Orm.QueryTable(new(models.PlatformAgentApi)). Filter("id", id). Filter("is_deleted", 0). One(&row) if err != nil { if err == orm.ErrNoRows { c.jsonErr(404, 404, "配置不存在") } else { c.jsonErr(500, 500, "查询失败: "+err.Error()) } return nil, err } return &row, nil } // Create POST /platform/agentApi func (c *PlatformAgentApiController) Create() { claims, err := c.platformClaims() if err != nil { c.jsonErr(401, 401, err.Error()) return } raw, err := io.ReadAll(c.Ctx.Request.Body) if err != nil { c.jsonErr(400, 400, "参数错误") return } var p agentApiPayload if err := json.Unmarshal(raw, &p); err != nil { c.jsonErr(400, 400, "参数错误") return } if err := validateAgentApiPayload(&p, true); err != nil { c.jsonErr(400, 400, err.Error()) return } status := int8(1) if p.Status != nil { status = *p.Status } userID := uint64(claims.UserID) row := &models.PlatformAgentApi{ Provider: p.Provider, Protocol: p.Protocol, BaseURL: p.BaseURL, UseCustomURL: p.UseCustomURL, CustomURL: p.CustomURL, ApiKeys: apiKeysToJSON(*p.ApiKeys), Models: modelsToJSON(p.Models), Status: status, Remark: strings.TrimSpace(p.Remark), UserID: &userID, UserName: &claims.Username, IsDeleted: 0, } id, err := models.Orm.Insert(row) if err != nil { c.jsonErr(500, 500, "创建失败: "+err.Error()) return } row.ID = uint64(id) c.ok("创建成功", agentApiToMap(row)) } // Update PUT /platform/agentApi/:id func (c *PlatformAgentApiController) Update() { if _, err := c.platformClaims(); err != nil { c.jsonErr(401, 401, err.Error()) return } row, err := c.findByID() if err != nil { return } raw, err := io.ReadAll(c.Ctx.Request.Body) if err != nil { c.jsonErr(400, 400, "参数错误") return } var p agentApiPayload if err := json.Unmarshal(raw, &p); err != nil { c.jsonErr(400, 400, "参数错误") return } if err := validateAgentApiPayload(&p, false); err != nil { c.jsonErr(400, 400, err.Error()) return } 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, "models": modelsToJSON(p.Models), "remark": strings.TrimSpace(p.Remark), "update_time": now, } if p.Status != nil { updates["status"] = *p.Status } // 未上送 api_keys 表示不修改已存密钥列表 if p.ApiKeys != nil { updates["api_keys"] = apiKeysToJSON(*p.ApiKeys) } _, err = models.Orm.QueryTable(new(models.PlatformAgentApi)). Filter("id", row.ID). Update(updates) if err != nil { c.jsonErr(500, 500, "更新失败: "+err.Error()) return } c.ok("更新成功", nil) } // Delete DELETE /platform/agentApi/:id func (c *PlatformAgentApiController) Delete() { if _, err := c.platformClaims(); err != nil { c.jsonErr(401, 401, err.Error()) return } row, err := c.findByID() if err != nil { return } now := time.Now() _, err = models.Orm.QueryTable(new(models.PlatformAgentApi)). Filter("id", row.ID). Update(map[string]interface{}{ "is_deleted": 1, "delete_time": now, }) if err != nil { c.jsonErr(500, 500, "删除失败: "+err.Error()) return } c.ok("删除成功", nil) } // BatchDelete POST /platform/agentApi/batchDelete func (c *PlatformAgentApiController) BatchDelete() { if _, err := c.platformClaims(); err != nil { c.jsonErr(401, 401, err.Error()) return } raw, err := io.ReadAll(c.Ctx.Request.Body) if err != nil { c.jsonErr(400, 400, "参数错误") return } var p struct { IDs []uint64 `json:"ids"` } if err := json.Unmarshal(raw, &p); err != nil { c.jsonErr(400, 400, "参数错误") return } if len(p.IDs) == 0 { c.jsonErr(400, 400, "请选择要删除的配置") return } now := time.Now() _, err = models.Orm.QueryTable(new(models.PlatformAgentApi)). Filter("id__in", p.IDs). Filter("is_deleted", 0). Update(map[string]interface{}{ "is_deleted": 1, "delete_time": now, }) if err != nil { c.jsonErr(500, 500, "批量删除失败: "+err.Error()) return } c.ok("批量删除成功", nil) } // ToggleStatus POST /platform/agentApi/:id/status func (c *PlatformAgentApiController) ToggleStatus() { if _, err := c.platformClaims(); err != nil { c.jsonErr(401, 401, err.Error()) return } row, err := c.findByID() if err != nil { return } raw, err := io.ReadAll(c.Ctx.Request.Body) if err != nil { c.jsonErr(400, 400, "参数错误") return } var p struct { Status *int8 `json:"status"` } if err := json.Unmarshal(raw, &p); err != nil || p.Status == nil { c.jsonErr(400, 400, "参数错误") return } if *p.Status != 0 && *p.Status != 1 { c.jsonErr(400, 400, "状态值不正确") return } now := time.Now() _, err = models.Orm.QueryTable(new(models.PlatformAgentApi)). Filter("id", row.ID). Update(map[string]interface{}{ "status": *p.Status, "update_time": now, }) if err != nil { c.jsonErr(500, 500, "状态切换失败: "+err.Error()) return } c.ok("操作成功", nil) } // agentApiTestResult 测试结果统一结构,与前端 test.vue 字段对应 // Endpoint 回显实际请求的完整地址,便于排查路径拼接类问题(如上游根地址少了版本前缀) type agentApiTestResult struct { Success bool `json:"success"` Message string `json:"message"` StatusCode int `json:"status_code,omitempty"` 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 指定用密钥列表中的第几个密钥,缺省用第一个 func (c *PlatformAgentApiController) Test() { if _, err := c.platformClaims(); err != nil { c.jsonErr(401, 401, err.Error()) return } raw, err := io.ReadAll(c.Ctx.Request.Body) if err != nil { c.jsonErr(400, 400, "参数错误") return } var p struct { ID uint64 `json:"id"` Model string `json:"model"` KeyIndex *int `json:"key_index"` Prompt string `json:"prompt"` } if err := json.Unmarshal(raw, &p); err != nil { c.jsonErr(400, 400, "参数错误") return } if p.ID == 0 { c.jsonErr(400, 400, "缺少配置ID") return } var row models.PlatformAgentApi err = models.Orm.QueryTable(new(models.PlatformAgentApi)). Filter("id", p.ID). Filter("is_deleted", 0). One(&row) if err != nil { c.jsonErr(404, 404, "配置不存在") return } model := strings.TrimSpace(p.Model) if model == "" { list := modelsFromJSON(row.Models) if len(list) == 0 { c.jsonErr(400, 400, "该配置未添加模型") return } model = list[0] } // 挑选待测密钥 keys := apiKeysFromJSON(row.ApiKeys) if len(keys) == 0 { c.jsonErr(400, 400, "该配置未添加 API Key") return } idx := 0 if p.KeyIndex != nil { idx = *p.KeyIndex } if idx < 0 || idx >= len(keys) { c.jsonErr(400, 400, "指定的密钥不存在") return } chosen := keys[idx] prompt := strings.TrimSpace(p.Prompt) if prompt == "" { prompt = "你好" } result := probeAgentApi(&row, chosen, model, prompt) c.ok("success", result) } // probeAgentApi 用指定密钥按配置的请求协议发起一次最小化对话请求 func probeAgentApi( row *models.PlatformAgentApi, apiKey models.AgentApiKey, model, prompt string, ) agentApiTestResult { base := strings.TrimRight(row.EffectiveURL(), "/") if base == "" { return agentApiTestResult{ Success: false, Message: "接口地址为空", Model: model, KeyRemark: apiKey.Remark, } } // 协议由配置显式指定,不再根据上游名称猜测 protocol := normalizeProtocol(row.Protocol) endpoint := buildAgentEndpoint(protocol, base, model, apiKey.Key) var ( body []byte headers = map[string]string{"Content-Type": "application/json"} err error ) switch protocol { case models.AgentProtocolAnthropic: headers["x-api-key"] = apiKey.Key headers["anthropic-version"] = "2023-06-01" body, err = json.Marshal(map[string]interface{}{ "model": model, "max_tokens": 64, "messages": []map[string]string{ {"role": "user", "content": prompt}, }, }) 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: // Chat Completions 与 custom 均按 OpenAI 标准报文发送 headers["Authorization"] = "Bearer " + apiKey.Key body, err = json.Marshal(map[string]interface{}{ "model": model, "max_tokens": 64, "messages": []map[string]string{ {"role": "user", "content": prompt}, }, }) } if err != nil { return agentApiTestResult{ Success: false, Message: "构造请求失败", Detail: err.Error(), Model: model, KeyRemark: apiKey.Remark, } } req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewReader(body)) if err != nil { 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() if err != nil { return agentApiTestResult{ Success: false, Message: "请求上游失败(网络不通或地址错误)", Detail: err.Error(), LatencyMs: latency, Model: model, KeyRemark: apiKey.Remark, Endpoint: safeEndpoint, } } defer resp.Body.Close() // 限制读取长度,避免超长响应占满内存 respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 8*1024)) 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: msg, StatusCode: resp.StatusCode, LatencyMs: latency, Model: model, KeyRemark: apiKey.Remark, Endpoint: safeEndpoint, Detail: preview, } } return agentApiTestResult{ Success: true, Message: "连接正常,密钥与模型可用", StatusCode: resp.StatusCode, LatencyMs: latency, Model: model, KeyRemark: apiKey.Remark, Endpoint: safeEndpoint, Response: extractAgentReply(protocol, respBody, preview), } } // extractAgentReply 从上游响应里提取模型回复文本,解析失败则回退为原始预览 func extractAgentReply(protocol string, respBody []byte, fallback string) string { switch protocol { case models.AgentProtocolAnthropic: var r struct { Content []struct { Text string `json:"text"` } `json:"content"` } if json.Unmarshal(respBody, &r) == nil && len(r.Content) > 0 { return r.Content[0].Text } case models.AgentProtocolGemini: var r struct { Candidates []struct { Content struct { Parts []struct { Text string `json:"text"` } `json:"parts"` } `json:"content"` } `json:"candidates"` } if json.Unmarshal(respBody, &r) == nil && len(r.Candidates) > 0 && len(r.Candidates[0].Content.Parts) > 0 { 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 { Message struct { Content string `json:"content"` } `json:"message"` } `json:"choices"` } if json.Unmarshal(respBody, &r) == nil && len(r.Choices) > 0 { return r.Choices[0].Message.Content } } return fallback }