From f68ad40723e6d799b2166ab43f1a598ee22ac115 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=89=AB=E5=9C=B0=E5=83=A7?= <357099073@qq.com> Date: Thu, 27 Aug 2026 01:02:09 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=99=BA=E8=83=BD=E4=BD=93bu?= =?UTF-8?q?g?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go/controllers/platform_agent_api.go | 167 ++++++++++++++---- go/models/platform_agent_api.go | 18 +- .../agentapimanagement/components/detail.vue | 31 ++++ .../agentapimanagement/components/edit.vue | 71 +++++++- .../agentapimanagement/components/test.vue | 18 ++ .../tools/agentapimanagement/constants.js | 68 ++++++- .../views/tools/agentapimanagement/index.vue | 4 + 7 files changed, 334 insertions(+), 43 deletions(-) diff --git a/go/controllers/platform_agent_api.go b/go/controllers/platform_agent_api.go index 48f5017..73ebda0 100644 --- a/go/controllers/platform_agent_api.go +++ b/go/controllers/platform_agent_api.go @@ -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 { diff --git a/go/models/platform_agent_api.go b/go/models/platform_agent_api.go index d53bc83..7ddece2 100644 --- a/go/models/platform_agent_api.go +++ b/go/models/platform_agent_api.go @@ -3,12 +3,14 @@ package models import "time" // PlatformAgentApi 智能体API管理表: yz_platform_agent_api -// 按上游接口存储调用地址、密钥列表、模型列表 +// 按上游接口存储调用地址、请求协议、密钥列表、模型列表 // Provider 为用户自由填写的上游名称,不做枚举约束 +// Protocol 显式指定请求协议,决定拼哪个端点路径与用哪种鉴权头 // ApiKeys 存 JSON 数组,支持同一上游下挂多个账号的密钥,每条可带备注 type PlatformAgentApi struct { ID uint64 `orm:"column(id);pk;auto" json:"id"` Provider string `orm:"column(provider);size(100)" json:"provider"` + Protocol string `orm:"column(protocol);size(30);default(chat_completions)" json:"protocol"` BaseURL string `orm:"column(base_url);size(500)" json:"base_url"` UseCustomURL int8 `orm:"column(use_custom_url);default(0)" json:"use_custom_url"` CustomURL string `orm:"column(custom_url);size(500)" json:"custom_url"` @@ -42,3 +44,17 @@ type AgentApiKey struct { Key string `json:"key"` Remark string `json:"remark"` } + +// 请求协议枚举,与前端 constants.js 的 PROTOCOLS 保持一致 +const ( + // AgentProtocolChatCompletions OpenAI Chat Completions API,端点 {base}/chat/completions + AgentProtocolChatCompletions = "chat_completions" + // AgentProtocolResponses OpenAI Responses API,端点 {base}/responses + AgentProtocolResponses = "responses" + // AgentProtocolAnthropic Anthropic Messages API,端点 {base}/messages + AgentProtocolAnthropic = "anthropic" + // AgentProtocolGemini Google Gemini generateContent,端点 {base}/models/{model}:generateContent + AgentProtocolGemini = "gemini" + // AgentProtocolCustom 自定义:直接把填写的地址当完整端点请求,不追加任何路径 + AgentProtocolCustom = "custom" +) diff --git a/platform/src/views/tools/agentapimanagement/components/detail.vue b/platform/src/views/tools/agentapimanagement/components/detail.vue index 67edd84..7ab1fab 100644 --- a/platform/src/views/tools/agentapimanagement/components/detail.vue +++ b/platform/src/views/tools/agentapimanagement/components/detail.vue @@ -22,6 +22,12 @@ {{ detail.id }} + + + {{ protocolLabel(detail.protocol) }} + + +
{{ detail.base_url || '—' }}
{{ detail.custom_url || '—' }}
+ +
{{ endpointPreview || '—' }}
+
+
@@ -120,6 +130,9 @@ import { providerInitial, normalizeModels, normalizeApiKeys, + protocolLabel, + previewEndpoint, + effectiveUrl, } from '../constants' const visible = ref(false) @@ -129,6 +142,12 @@ const detail = ref(null) const models = computed(() => normalizeModels(detail.value?.models)) const apiKeys = computed(() => normalizeApiKeys(detail.value?.api_keys)) +// 实际请求地址,规则与后端一致 +const endpointPreview = computed(() => { + if (!detail.value) return '' + return previewEndpoint(detail.value.protocol, effectiveUrl(detail.value)) +}) + async function open(id) { detail.value = null visible.value = true @@ -219,6 +238,18 @@ defineExpose({ open }) line-height: 1.5; } +.endpoint-preview { + padding: 6px 10px; + background: #f5f7fa; + border: 1px dashed var(--el-border-color); + border-radius: 4px; + font-family: Consolas, Monaco, monospace; + font-size: 12px; + color: #409eff; + word-break: break-all; + line-height: 1.5; +} + .mt6 { margin-top: 6px; } diff --git a/platform/src/views/tools/agentapimanagement/components/edit.vue b/platform/src/views/tools/agentapimanagement/components/edit.vue index fb80652..73c8ba3 100644 --- a/platform/src/views/tools/agentapimanagement/components/edit.vue +++ b/platform/src/views/tools/agentapimanagement/components/edit.vue @@ -35,10 +35,22 @@ 用于区分不同上游来源,内容完全由你决定 + + + + + {{ currentProtocolDesc }} + + @@ -56,6 +68,11 @@ 勾选自定义后,实际调用将使用此地址,上方地址仅作留档 + + +
{{ endpointPreview }}
+
+
暂未添加密钥
@@ -168,7 +185,14 @@ import { ref, reactive, computed, nextTick } from 'vue' import { ElMessage } from 'element-plus' import { Plus, Delete } from '@element-plus/icons-vue' import { getAgentApiDetail, createAgentApi, updateAgentApi } from '@/api/agentapimanagement' -import { normalizeModels, normalizeApiKeys } from '../constants' +import { + normalizeModels, + normalizeApiKeys, + PROTOCOLS, + PROTOCOL_MAP, + DEFAULT_PROTOCOL, + previewEndpoint, +} from '../constants' const emit = defineEmits(['saved']) @@ -193,6 +217,7 @@ const inputRef = ref(null) const form = reactive({ id: 0, provider: '', + protocol: DEFAULT_PROTOCOL, base_url: '', use_custom_url: false, custom_url: '', @@ -202,8 +227,34 @@ const form = reactive({ remark: '', }) +// 当前协议的说明文案 +const currentProtocolDesc = computed( + () => PROTOCOL_MAP[form.protocol]?.desc || '' +) + +// 地址输入框的占位提示随协议变化 +const urlPlaceholder = computed(() => { + if (form.protocol === 'custom') { + return '填写完整端点,例如:https://your-gateway.com/invoke' + } + if (form.protocol === 'anthropic') { + return '填根地址,例如:https://api.anthropic.com/v1' + } + if (form.protocol === 'gemini') { + return '填根地址,例如:https://generativelanguage.googleapis.com/v1beta' + } + return '填根地址,例如:https://api.openai.com/v1' +}) + +// 最终请求地址预览,规则与后端一致 +const endpointPreview = computed(() => { + const url = form.use_custom_url ? form.custom_url : form.base_url + return previewEndpoint(form.protocol, url) +}) + const rules = { provider: [{ required: true, message: '请输入上游接口名称', trigger: 'blur' }], + protocol: [{ required: true, message: '请选择请求协议', trigger: 'change' }], base_url: [ { validator: (rule, value, callback) => { @@ -329,6 +380,7 @@ function validateKeys() { function resetForm() { form.id = 0 form.provider = '' + form.protocol = DEFAULT_PROTOCOL form.base_url = '' form.use_custom_url = false form.custom_url = '' @@ -394,6 +446,7 @@ async function loadInto(id) { const d = res.data form.id = d.id form.provider = d.provider || '' + form.protocol = d.protocol || DEFAULT_PROTOCOL form.base_url = d.base_url || '' form.use_custom_url = Number(d.use_custom_url) === 1 form.custom_url = d.custom_url || '' @@ -430,6 +483,7 @@ async function submit() { const payload = { provider: form.provider.trim(), + protocol: form.protocol, base_url: form.base_url || '', use_custom_url: form.use_custom_url ? 1 : 0, custom_url: form.use_custom_url ? form.custom_url : '', @@ -479,6 +533,19 @@ defineExpose({ open, openCopy }) margin-top: 6px; } +.endpoint-preview { + width: 100%; + padding: 6px 10px; + background: var(--el-fill-color-lighter); + border: 1px dashed var(--el-border-color); + border-radius: 4px; + font-family: Consolas, Monaco, monospace; + font-size: 12px; + color: #409eff; + word-break: break-all; + line-height: 1.5; +} + .keys-box { width: 100%; } diff --git a/platform/src/views/tools/agentapimanagement/components/test.vue b/platform/src/views/tools/agentapimanagement/components/test.vue index 367278b..4ecac54 100644 --- a/platform/src/views/tools/agentapimanagement/components/test.vue +++ b/platform/src/views/tools/agentapimanagement/components/test.vue @@ -5,6 +5,9 @@ + + + @@ -88,6 +91,9 @@
{{ r.message || (r.success ? '连接正常' : '连接异常') }}
+
+ 实际请求:{{ r.endpoint }} +
{{ r.response }}
{{ r.detail }}
@@ -111,6 +117,7 @@ import { normalizeModels, normalizeApiKeys, maskKey, + protocolLabel, } from '../constants' const visible = ref(false) @@ -128,6 +135,7 @@ const results = ref([]) const info = reactive({ provider_label: '', + protocol_label: '', url: '', }) @@ -152,6 +160,7 @@ async function open(id) { if (res?.code === 200 && res.data) { const d = res.data info.provider_label = providerLabel(d.provider) + info.protocol_label = protocolLabel(d.protocol) info.url = effectiveUrl(d) models.value = normalizeModels(d.models) selectedModel.value = models.value[0] || '' @@ -308,6 +317,15 @@ defineExpose({ open }) } } + .result-endpoint { + margin-top: 6px; + font-size: 12px; + font-family: Consolas, Monaco, monospace; + color: #909399; + word-break: break-all; + line-height: 1.5; + } + .result-response { margin: 8px 0 0; padding: 8px 10px; diff --git a/platform/src/views/tools/agentapimanagement/constants.js b/platform/src/views/tools/agentapimanagement/constants.js index b98e380..1e5fc99 100644 --- a/platform/src/views/tools/agentapimanagement/constants.js +++ b/platform/src/views/tools/agentapimanagement/constants.js @@ -2,9 +2,75 @@ * 智能体 API 管理 - 共享工具 * * 上游接口(provider)为用户自由填写的文本,不做枚举限制。 - * 这里只提供展示辅助(配色、首字母徽标)与数据规整能力。 + * 请求协议(protocol)显式选择,决定实际请求哪个端点路径。 */ +/** + * 请求协议枚举,与后端 models.AgentProtocol* 常量保持一致 + * path 为在接口地址之后追加的路径,custom 与 gemini 特殊处理 + */ +export const PROTOCOLS = [ + { + value: 'chat_completions', + label: 'Chat Completions API', + path: '/chat/completions', + desc: 'OpenAI 标准对话接口,绝大多数上游与中转都兼容', + }, + { + value: 'responses', + label: 'Responses API', + path: '/responses', + desc: 'OpenAI 新版接口,请求体用 input 替代 messages', + }, + { + value: 'anthropic', + label: 'Anthropic Messages API', + path: '/messages', + desc: 'Claude 官方协议,鉴权头为 x-api-key', + }, + { + value: 'gemini', + label: 'Google Gemini', + path: '/models/{model}:generateContent', + desc: '模型名嵌在路径中,密钥通过 query 传递', + }, + { + value: 'custom', + label: '自定义(直接请求填写的地址)', + path: '', + desc: '不追加任何路径,把填写的地址当完整端点请求', + }, +]; + +export const PROTOCOL_MAP = Object.fromEntries(PROTOCOLS.map((p) => [p.value, p])); + +export const DEFAULT_PROTOCOL = 'chat_completions'; + +/** 协议展示名 */ +export function protocolLabel(value) { + return PROTOCOL_MAP[value]?.label || value || '未设置协议'; +} + +/** + * 预览实际请求地址,与后端 buildAgentEndpoint 的规则一致 + * 供表单实时提示用,避免地址拼错才在测试时发现 + */ +export function previewEndpoint(protocol, url, model = '{model}') { + const base = String(url || '').trim().replace(/\/+$/, ''); + if (!base) return ''; + + if (protocol === 'custom') return base; + + if (protocol === 'gemini') { + return `${base}/models/${model}:generateContent?key=***`; + } + + const path = PROTOCOL_MAP[protocol]?.path || '/chat/completions'; + // 地址已以该路径结尾时不重复追加 + if (base.toLowerCase().endsWith(path.toLowerCase())) return base; + return base + path; +} + /** 徽标配色池,按上游名称哈希稳定取色,保证同一上游每次渲染颜色一致 */ const BADGE_COLORS = [ '#10a37f', diff --git a/platform/src/views/tools/agentapimanagement/index.vue b/platform/src/views/tools/agentapimanagement/index.vue index 6194658..2fdbd03 100644 --- a/platform/src/views/tools/agentapimanagement/index.vue +++ b/platform/src/views/tools/agentapimanagement/index.vue @@ -108,6 +108,9 @@