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}, }, } // agentApiProtocol 上游协议族,决定请求路径、鉴权头与响应解析方式 type agentApiProtocol string const ( protocolAnthropic agentApiProtocol = "anthropic" protocolGemini agentApiProtocol = "gemini" protocolOpenAI agentApiProtocol = "openai" ) // 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 } } for _, k := range []string{"gemini", "generativelanguage", "googleapis"} { if strings.Contains(hay, k) { return protocolGemini } } return protocolOpenAI } 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"` 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) out := map[string]interface{}{ "id": row.ID, "provider": row.Provider, "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("上游接口名称过长") } 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://") } // 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, 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, "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 字段对应 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"` Response string `json:"response,omitempty"` Detail string `json:"detail,omitempty"` } // 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 := detectAgentApiProtocol(row.Provider, base) var ( endpoint string body []byte headers = map[string]string{"Content-Type": "application/json"} err error ) switch protocol { case protocolAnthropic: endpoint = base + "/messages" 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 protocolGemini: endpoint = fmt.Sprintf("%s/models/%s:generateContent?key=%s", base, model, apiKey.Key) body, err = json.Marshal(map[string]interface{}{ "contents": []map[string]interface{}{ {"parts": []map[string]string{{"text": prompt}}}, }, }) default: // OpenAI 兼容协议,覆盖大多数官方接口、国产网关与自建中转 endpoint = base + "/chat/completions" 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, } } for k, v := range headers { req.Header.Set(k, v) } 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, } } defer resp.Body.Close() // 限制读取长度,避免超长响应占满内存 respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 8*1024)) preview := string(respBody) if resp.StatusCode < 200 || resp.StatusCode >= 300 { return agentApiTestResult{ Success: false, Message: fmt.Sprintf("上游返回 HTTP %d", resp.StatusCode), StatusCode: resp.StatusCode, LatencyMs: latency, Model: model, KeyRemark: apiKey.Remark, Detail: preview, } } return agentApiTestResult{ Success: true, Message: "连接正常,密钥与模型可用", StatusCode: resp.StatusCode, LatencyMs: latency, Model: model, KeyRemark: apiKey.Remark, Response: extractAgentReply(protocol, respBody, preview), } } // extractAgentReply 从上游响应里提取模型回复文本,解析失败则回退为原始预览 func extractAgentReply(protocol agentApiProtocol, respBody []byte, fallback string) string { switch protocol { case protocolAnthropic: 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 protocolGemini: 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 } 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 }