Files

1 line
112 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "chadban/ai-api", "files": {"app/internal/service/init_domestic_models.go": {"b408060e66b5a4d4d7b6b6eef5b738cd0bcc9cf1": {"t": 1785746101.9964366, "n": 4448, "h": "4eeed21f46d0f094", "c": "package service\n\nimport (\n\t\"encoding/json\"\n\n\t\"ai-api/app/internal/logger\"\n\t\"ai-api/app/internal/model\"\n\n\t\"github.com/shopspring/decimal\"\n\t\"gorm.io/gorm\"\n)\n\n// InitDomesticModels 初始化国内大模型数据\nfunc InitDomesticModels(db *gorm.DB, log *logger.Logger) {\n\tlog.Info(\"Initializing domestic LLM models...\")\n\n\t// 定义国内大模型渠道配置\n\tdomesticChannels := []struct {\n\t\tchannelType int\n\t\tname string\n\t\tbaseURL string\n\t\tmodels []string\n\t}{\n\t\t{\n\t\t\tchannelType: model.ChannelTypeDoubao,\n\t\t\tname: \"豆包官方\",\n\t\t\tbaseURL: \"https://ark.cn-beijing.volces.com/api/v3\",\n\t\t\tmodels: []string{\"doubao-lite-4k\", \"doubao-pro-4k\", \"doubao-lite-32k\", \"doubao-pro-32k\"},\n\t\t},\n\t\t{\n\t\t\tchannelType: model.ChannelTypeAli,\n\t\t\tname: \"阿里通义官方\",\n\t\t\tbaseURL: \"https://dashscope.aliyuncs.com/compatible-mode\",\n\t\t\tmodels: []string{\"qwen-turbo\", \"qwen-plus\", \"qwen-max\", \"qwen-max-longcontext\"},\n\t\t},\n\t\t{\n\t\t\tchannelType: model.ChannelTypeDeepSeek,\n\t\t\tname: \"DeepSeek 官方\",\n\t\t\tbaseURL: \"https://api.deepseek.com\",\n\t\t\tmodels: []string{\"deepseek-chat\", \"deepseek-coder\"},\n\t\t},\n\t\t{\n\t\t\tchannelType: model.ChannelTypeMiniMax,\n\t\t\tname: \"MiniMax 官方\",\n\t\t\tbaseURL: \"https://api.minimax.chat/v1\",\n\t\t\tmodels: []string{\"abab6.5-chat\", \"abab6.5g-chat\", \"abab6.5t-chat\"},\n\t\t},\n\t\t{\n\t\t\tchannelType: model.ChannelTypeZhipu,\n\t\t\tname: \"智谱 AI 官方\",\n\t\t\tbaseURL: \"https://open.bigmodel.cn/api/paas/v4\",\n\t\t\tmodels: []string{\"glm-4\", \"glm-4-flash\", \"glm-3-turbo\"},\n\t\t},\n\t}\n\n\t// 添加渠道(仅当不存在时)\n\tfor _, cfg := range domesticChannels {\n\t\tvar count int64\n\t\tdb.Model(&model.Channel{}).Where(\"type = ? AND name = ?\", cfg.channelType, cfg.name).Count(&count)\n\n\t\tif count == 0 {\n\t\t\tmodelsJSON, _ := json.Marshal(cfg.models)\n\t\t\tchannel := model.Channel{\n\t\t\t\tType: cfg.channelType,\n\t\t\t\tName: cfg.name,\n\t\t\t\tBaseURL: cfg.baseURL,\n\t\t\t\tAPIKey: \"NEED_SETUP\", // 需要管理员后续配置\n\t\t\t\tStatus: 0, // 默认禁用,等待配置 API Key\n\t\t\t\tModels: string(modelsJSON),\n\t\t\t\tPriority: 1,\n\t\t\t\tWeight: 100,\n\t\t\t}\n\n\t\t\tif err := db.Create(&channel).Error; err != nil {\n\t\t\t\tlog.Warn(\"Failed to create domestic channel\", logger.String(\"name\", cfg.name), logger.Err(err))\n\t\t\t} else {\n\t\t\t\tlog.Info(\"Created domestic channel\", logger.String(\"name\", cfg.name), logger.Int(\"type\", cfg.channelType))\n\t\t\t}\n\t\t}\n\t}\n\n\t// 定义模型数据\n\tdomesticModels := []struct {\n\t\tname string\n\t\tdisplayName string\n\t\tcontextWindow int\n\t\tmaxTokens int\n\t\tinputPrice float64\n\t\toutputPrice float64\n\t}{\n\t\t// 豆包\n\t\t{\"doubao-lite-4k\", \"豆包 Lite 4K\", 4096, 2048, 0.0003, 0.0006},\n\t\t{\"doubao-pro-4k\", \"豆包 Pro 4K\", 4096, 2048, 0.0008, 0.0020},\n\t\t{\"doubao-lite-32k\", \"豆包 Lite 32K\", 32768, 4096, 0.0006, 0.0012},\n\t\t{\"doubao-pro-32k\", \"豆包 Pro 32K\", 32768, 4096, 0.0015, 0.0030},\n\t\t// 阿里通义\n\t\t{\"qwen-turbo\", \"通义千问 Turbo\", 8192, 4096, 0.0003, 0.0006},\n\t\t{\"qwen-plus\", \"通义千问 Plus\", 32768, 8192, 0.0008, 0.0020},\n\t\t{\"qwen-max\", \"通义千问 Max\", 32768, 8192, 0.0015, 0.0030},\n\t\t{\"qwen-max-longcontext\", \"通义千问 Max 长文本\", 131072, 8192, 0.0020, 0.0040},\n\t\t// DeepSeek\n\t\t{\"deepseek-chat\", \"DeepSeek Chat\", 32768, 4096, 0.0002, 0.0004},\n\t\t{\"deepseek-coder\", \"DeepSeek Coder\", 16384, 2048, 0.0002, 0.0004},\n\t\t// MiniMax\n\t\t{\"abab6.5-chat\", \"MiniMax Abab6.5 Chat\", 8192, 4096, 0.0005, 0.0010},\n\t\t{\"abab6.5g-chat\", \"MiniMax Abab6.5G Chat\", 8192, 4096, 0.0005, 0.0010},\n\t\t{\"abab6.5t-chat\", \"MiniMax Abab6.5T Chat\", 8192, 4096, 0.0005, 0.0010},\n\t\t// 智谱 AI\n\t\t{\"glm-4\", \"智谱 GLM-4\", 128000, 4096, 0.0010, 0.0020},\n\t\t{\"glm-4-flash\", \"智谱 GLM-4 Flash\", 8192, 4096, 0.0001, 0.0002},\n\t\t{\"glm-3-turbo\", \"智谱 GLM-3 Turbo\", 128000, 4096, 0.0005, 0.0010},\n\t}\n\n\t// 添加模型到 models 表\n\tfor _, m := range domesticModels {\n\t\tvar count int64\n\t\tdb.Model(&model.Model{}).Where(\"name = ?\", m.name).Count(&count)\n\n\t\tif count == 0 {\n\t\t\tmodelItem := model.Model{\n\t\t\t\tProviderID: 1, // 默认提供商\n\t\t\t\tName: m.name,\n\t\t\t\tDisplayName: m.displayName,\n\t\t\t\tType: \"chat\",\n\t\t\t\tContextWindow: m.contextWindow,\n\t\t\t\tMaxTokens: m.maxTokens,\n\t\t\t\tInputPrice: decimal.NewFromFloat(m.inputPrice),\n\t\t\t\tOutputPrice: decimal.NewFromFloat(m.outputPrice),\n\t\t\t\tStatus: 1,\n\t\t\t}\n\n\t\t\tif err := db.Create(&modelItem).Error; err != nil {\n\t\t\t\tlog.Warn(\"Failed to create domestic model\", logger.String(\"name\", m.name), logger.Err(err))\n\t\t\t} else {\n\t\t\t\tlog.Info(\"Created domestic model\", logger.String(\"name\", m.name), logger.String(\"displayName\", m.displayName))\n\t\t\t}\n\t\t}\n\t}\n\n\tlog.Info(\"Domestic LLM models initialization completed\")\n}\n"}, "644d9e61e38f9e26163655416f9ed9003d4338b6": {"t": 1785759879.308801, "n": 4448, "h": "4eeed21f46d0f094", "c": "package service\n\nimport (\n\t\"encoding/json\"\n\n\t\"ai-api/app/internal/logger\"\n\t\"ai-api/app/internal/model\"\n\n\t\"github.com/shopspring/decimal\"\n\t\"gorm.io/gorm\"\n)\n\n// InitDomesticModels 初始化国内大模型数据\nfunc InitDomesticModels(db *gorm.DB, log *logger.Logger) {\n\tlog.Info(\"Initializing domestic LLM models...\")\n\n\t// 定义国内大模型渠道配置\n\tdomesticChannels := []struct {\n\t\tchannelType int\n\t\tname string\n\t\tbaseURL string\n\t\tmodels []string\n\t}{\n\t\t{\n\t\t\tchannelType: model.ChannelTypeDoubao,\n\t\t\tname: \"豆包官方\",\n\t\t\tbaseURL: \"https://ark.cn-beijing.volces.com/api/v3\",\n\t\t\tmodels: []string{\"doubao-lite-4k\", \"doubao-pro-4k\", \"doubao-lite-32k\", \"doubao-pro-32k\"},\n\t\t},\n\t\t{\n\t\t\tchannelType: model.ChannelTypeAli,\n\t\t\tname: \"阿里通义官方\",\n\t\t\tbaseURL: \"https://dashscope.aliyuncs.com/compatible-mode\",\n\t\t\tmodels: []string{\"qwen-turbo\", \"qwen-plus\", \"qwen-max\", \"qwen-max-longcontext\"},\n\t\t},\n\t\t{\n\t\t\tchannelType: model.ChannelTypeDeepSeek,\n\t\t\tname: \"DeepSeek 官方\",\n\t\t\tbaseURL: \"https://api.deepseek.com\",\n\t\t\tmodels: []string{\"deepseek-chat\", \"deepseek-coder\"},\n\t\t},\n\t\t{\n\t\t\tchannelType: model.ChannelTypeMiniMax,\n\t\t\tname: \"MiniMax 官方\",\n\t\t\tbaseURL: \"https://api.minimax.chat/v1\",\n\t\t\tmodels: []string{\"abab6.5-chat\", \"abab6.5g-chat\", \"abab6.5t-chat\"},\n\t\t},\n\t\t{\n\t\t\tchannelType: model.ChannelTypeZhipu,\n\t\t\tname: \"智谱 AI 官方\",\n\t\t\tbaseURL: \"https://open.bigmodel.cn/api/paas/v4\",\n\t\t\tmodels: []string{\"glm-4\", \"glm-4-flash\", \"glm-3-turbo\"},\n\t\t},\n\t}\n\n\t// 添加渠道(仅当不存在时)\n\tfor _, cfg := range domesticChannels {\n\t\tvar count int64\n\t\tdb.Model(&model.Channel{}).Where(\"type = ? AND name = ?\", cfg.channelType, cfg.name).Count(&count)\n\n\t\tif count == 0 {\n\t\t\tmodelsJSON, _ := json.Marshal(cfg.models)\n\t\t\tchannel := model.Channel{\n\t\t\t\tType: cfg.channelType,\n\t\t\t\tName: cfg.name,\n\t\t\t\tBaseURL: cfg.baseURL,\n\t\t\t\tAPIKey: \"NEED_SETUP\", // 需要管理员后续配置\n\t\t\t\tStatus: 0, // 默认禁用,等待配置 API Key\n\t\t\t\tModels: string(modelsJSON),\n\t\t\t\tPriority: 1,\n\t\t\t\tWeight: 100,\n\t\t\t}\n\n\t\t\tif err := db.Create(&channel).Error; err != nil {\n\t\t\t\tlog.Warn(\"Failed to create domestic channel\", logger.String(\"name\", cfg.name), logger.Err(err))\n\t\t\t} else {\n\t\t\t\tlog.Info(\"Created domestic channel\", logger.String(\"name\", cfg.name), logger.Int(\"type\", cfg.channelType))\n\t\t\t}\n\t\t}\n\t}\n\n\t// 定义模型数据\n\tdomesticModels := []struct {\n\t\tname string\n\t\tdisplayName string\n\t\tcontextWindow int\n\t\tmaxTokens int\n\t\tinputPrice float64\n\t\toutputPrice float64\n\t}{\n\t\t// 豆包\n\t\t{\"doubao-lite-4k\", \"豆包 Lite 4K\", 4096, 2048, 0.0003, 0.0006},\n\t\t{\"doubao-pro-4k\", \"豆包 Pro 4K\", 4096, 2048, 0.0008, 0.0020},\n\t\t{\"doubao-lite-32k\", \"豆包 Lite 32K\", 32768, 4096, 0.0006, 0.0012},\n\t\t{\"doubao-pro-32k\", \"豆包 Pro 32K\", 32768, 4096, 0.0015, 0.0030},\n\t\t// 阿里通义\n\t\t{\"qwen-turbo\", \"通义千问 Turbo\", 8192, 4096, 0.0003, 0.0006},\n\t\t{\"qwen-plus\", \"通义千问 Plus\", 32768, 8192, 0.0008, 0.0020},\n\t\t{\"qwen-max\", \"通义千问 Max\", 32768, 8192, 0.0015, 0.0030},\n\t\t{\"qwen-max-longcontext\", \"通义千问 Max 长文本\", 131072, 8192, 0.0020, 0.0040},\n\t\t// DeepSeek\n\t\t{\"deepseek-chat\", \"DeepSeek Chat\", 32768, 4096, 0.0002, 0.0004},\n\t\t{\"deepseek-coder\", \"DeepSeek Coder\", 16384, 2048, 0.0002, 0.0004},\n\t\t// MiniMax\n\t\t{\"abab6.5-chat\", \"MiniMax Abab6.5 Chat\", 8192, 4096, 0.0005, 0.0010},\n\t\t{\"abab6.5g-chat\", \"MiniMax Abab6.5G Chat\", 8192, 4096, 0.0005, 0.0010},\n\t\t{\"abab6.5t-chat\", \"MiniMax Abab6.5T Chat\", 8192, 4096, 0.0005, 0.0010},\n\t\t// 智谱 AI\n\t\t{\"glm-4\", \"智谱 GLM-4\", 128000, 4096, 0.0010, 0.0020},\n\t\t{\"glm-4-flash\", \"智谱 GLM-4 Flash\", 8192, 4096, 0.0001, 0.0002},\n\t\t{\"glm-3-turbo\", \"智谱 GLM-3 Turbo\", 128000, 4096, 0.0005, 0.0010},\n\t}\n\n\t// 添加模型到 models 表\n\tfor _, m := range domesticModels {\n\t\tvar count int64\n\t\tdb.Model(&model.Model{}).Where(\"name = ?\", m.name).Count(&count)\n\n\t\tif count == 0 {\n\t\t\tmodelItem := model.Model{\n\t\t\t\tProviderID: 1, // 默认提供商\n\t\t\t\tName: m.name,\n\t\t\t\tDisplayName: m.displayName,\n\t\t\t\tType: \"chat\",\n\t\t\t\tContextWindow: m.contextWindow,\n\t\t\t\tMaxTokens: m.maxTokens,\n\t\t\t\tInputPrice: decimal.NewFromFloat(m.inputPrice),\n\t\t\t\tOutputPrice: decimal.NewFromFloat(m.outputPrice),\n\t\t\t\tStatus: 1,\n\t\t\t}\n\n\t\t\tif err := db.Create(&modelItem).Error; err != nil {\n\t\t\t\tlog.Warn(\"Failed to create domestic model\", logger.String(\"name\", m.name), logger.Err(err))\n\t\t\t} else {\n\t\t\t\tlog.Info(\"Created domestic model\", logger.String(\"name\", m.name), logger.String(\"displayName\", m.displayName))\n\t\t\t}\n\t\t}\n\t}\n\n\tlog.Info(\"Domestic LLM models initialization completed\")\n}\n"}}, "web/src/views/admin/Channels.vue": {"b408060e66b5a4d4d7b6b6eef5b738cd0bcc9cf1": {"t": 1785747442.1268895, "n": 30736, "h": "5f9f41f1b2f0c988", "c": "<template>\n <div class=\"channels-page\">\n <el-card>\n <template #header>\n <div class=\"card-header\">\n <span>渠道管理</span>\n <div class=\"header-actions\">\n <el-button type=\"primary\" @click=\"showCreateDialog = true\">\n <el-icon><Plus /></el-icon>\n 添加渠道\n </el-button>\n <el-button @click=\"batchTestChannels\" :loading=\"batchTesting\">\n <el-icon><Refresh /></el-icon>\n 批量测试\n </el-button>\n <el-button @click=\"refreshChannels\">\n <el-icon><Refresh /></el-icon>\n 刷新\n </el-button>\n </div>\n </div>\n </template>\n \n <el-table :data=\"channels\" v-loading=\"loading\" border style=\"width: 100%\">\n <el-table-column prop=\"id\" label=\"ID\" width=\"80\" />\n <el-table-column prop=\"name\" label=\"名称\" />\n <el-table-column prop=\"api_key\" label=\"API Key\" show-overflow-tooltip>\n <template #default=\"{ row }\">\n {{ row.api_key ? '******' + row.api_key.slice(-4) : '' }}\n </template>\n </el-table-column>\n <el-table-column prop=\"group\" label=\"分组\" width=\"120\">\n <template #default=\"{ row }\">\n <el-tag size=\"small\">\n {{ row.group || 'default' }}\n </el-tag>\n </template>\n </el-table-column>\n <el-table-column prop=\"type\" label=\"类型\" width=\"120\">\n <template #default=\"{ row }\">\n <el-tag :type=\"getChannelTypeTag(row.type)\">\n {{ channelType(row.type) }}\n </el-tag>\n </template>\n </el-table-column>\n <el-table-column prop=\"base_url\" label=\"BaseURL\" show-overflow-tooltip />\n <el-table-column prop=\"priority\" label=\"优先级\" width=\"80\" />\n <el-table-column prop=\"weight\" label=\"权重\" width=\"80\" />\n <el-table-column prop=\"status\" label=\"状态\" width=\"100\">\n <template #default=\"{ row }\">\n <el-tag :type=\"getStatusTagType(row.status)\" size=\"small\">\n {{ getStatusText(row.status) }}\n </el-tag>\n </template>\n </el-table-column>\n <el-table-column prop=\"last_test_time\" label=\"最后测试时间\" width=\"160\">\n <template #default=\"{ row }\">\n {{ row.last_test_time || '未测试' }}\n </template>\n </el-table-column>\n <el-table-column label=\"操作\" width=\"300\">\n <template #default=\"{ row }\">\n <el-button size=\"small\" @click=\"testChannel(row.id)\" :loading=\"testingChannels.includes(row.id)\">\n <el-icon><Check /></el-icon>\n 测试\n </el-button>\n <el-button size=\"small\" type=\"warning\" @click=\"editChannel(row)\">\n <el-icon><Edit /></el-icon>\n 编辑\n </el-button>\n <el-button size=\"small\" type=\"danger\" @click=\"deleteChannel(row.id)\">\n <el-icon><Delete /></el-icon>\n 删除\n </el-button>\n </template>\n </el-table-column>\n </el-table>\n </el-card>\n \n <!-- 创建渠道对话框 -->\n <el-dialog v-model=\"showCreateDialog\" title=\"添加渠道\" width=\"600px\">\n <el-form :model=\"createForm\" label-width=\"100px\">\n <el-form-item label=\"渠道类型\" required>\n <el-select v-model=\"createForm.type\" placeholder=\"请选择渠道类型\" @change=\"handleChannelTypeChange\">\n <el-option label=\"OpenAI\" :value=\"1\" />\n <el-option label=\"Anthropic\" :value=\"2\" />\n <el-option label=\"Azure\" :value=\"3\" />\n <el-option label=\"Google Gemini\" :value=\"4\" />\n <el-option label=\"豆包 (ByteDance)\" :value=\"14\" />\n <el-option label=\"阿里通义\" :value=\"15\" />\n <el-option label=\"DeepSeek\" :value=\"16\" />\n <el-option label=\"MiniMax\" :value=\"17\" />\n <el-option label=\"智谱 AI\" :value=\"18\" />\n </el-select>\n </el-form-item>\n <el-form-item label=\"名称\" required>\n <el-input v-model=\"createForm.name\" placeholder=\"请输入名称\" />\n </el-form-item>\n <el-form-item label=\"显示名称\" required>\n <el-input v-model=\"createForm.display_name\" placeholder=\"请输入显示名称\" />\n </el-form-item>\n <el-form-item label=\"BaseURL\" required>\n <el-input v-model=\"createForm.base_url\" placeholder=\"https://api.openai.com/v1\" />\n <el-tooltip content=\"根据选择的渠道类型,系统已自动填充默认BaseURL\" placement=\"top\" :visible=\"showBaseUrlTip\">\n <el-button type=\"text\" size=\"small\" @click=\"showBaseUrlTip = !showBaseUrlTip\">\n 显示提示\n </el-button>\n </el-tooltip>\n </el-form-item>\n <el-form-item label=\"API Key\" required>\n <el-input v-model=\"createForm.api_key\" type=\"password\" show-password />\n <el-tooltip content=\"{{ getApiKeyTip(createForm.type) }}\" placement=\"top\" :visible=\"showApiKeyTip\">\n <el-button type=\"text\" size=\"small\" @click=\"showApiKeyTip = !showApiKeyTip\">\n API Key提示\n </el-button>\n </el-tooltip>\n </el-form-item>\n <el-form-item label=\"测试模型\">\n <el-select v-model=\"createForm.test_model\" placeholder=\"请选择测试模型\">\n <el-option \n v-for=\"model in getAvailableModels(createForm.type)\" \n :key=\"model\" \n :label=\"model\" \n :value=\"model\" \n />\n </el-select>\n </el-form-item>\n <el-form-item label=\"优先级\">\n <el-input-number v-model=\"createForm.priority\" :min=\"1\" :step=\"1\" />\n <el-tooltip content=\"优先级越高,越先被使用\" placement=\"top\">\n <el-button type=\"text\" size=\"small\">\n ?\n </el-button>\n </el-tooltip>\n </el-form-item>\n <el-form-item label=\"权重\">\n <el-input-number v-model=\"createForm.weight\" :min=\"1\" :step=\"10\" />\n <el-tooltip content=\"权重越高,被使用的概率越大\" placement=\"top\">\n <el-button type=\"text\" size=\"small\">\n ?\n </el-button>\n </el-tooltip>\n </el-form-item>\n <el-form-item label=\"状态\">\n <el-select v-model=\"createForm.status\">\n <el-option label=\"启用\" :value=\"1\" />\n <el-option label=\"禁用\" :value=\"0\" />\n <el-option label=\"维护中\" :value=\"2\" />\n </el-select>\n </el-form-item>\n <el-form-item label=\"模型\" required>\n <div class=\"model-management\">\n <!-- 模型选择器 -->\n <el-select\n v-model=\"selectedModels\"\n multiple\n filterable\n allow-create\n default-first-option\n placeholder=\"请选择该渠道所支持的模型\"\n style=\"width: 100%\"\n @change=\"updateFilteredModels\"\n >\n <el-option\n v-for=\"model in filteredModels\"\n :key=\"model\"\n :label=\"model\"\n :value=\"model\"\n />\n </el-select>\n \n <!-- 已选择模型标签显示 -->\n <div class=\"selected-model-tags\" v-if=\"selectedModels.length > 0\">\n <el-tag\n v-for=\"model in selectedModels\"\n :key=\"model\"\n closable\n @close=\"removeModel(model)\"\n class=\"selected-tag\"\n >\n {{ model }}\n </el-tag>\n </div>\n \n <div class=\"model-actions\">\n <el-button type=\"text\" size=\"small\" @click=\"selectAllModels\">\n 全选\n </el-button>\n <el-button type=\"text\" size=\"small\" @click=\"clearSelectedModels\">\n 清空\n </el-button>\n <el-button type=\"text\" size=\"small\" @click=\"fillRelatedModels(createForm.type)\">\n 填入相关模型\n </el-button>\n <el-button type=\"text\" size=\"small\" @click=\"syncModels(createForm.type)\" :loading=\"syncingModels\">\n <el-icon><Refresh /></el-icon>\n 同步最新模型\n </el-button>\n </div>\n </div>\n </el-form-item>\n \n <el-form-item label=\"分组\">\n <el-select v-model=\"createForm.group\" placeholder=\"选择分组\">\n <el-option \n v-for=\"group in groups\" \n :key=\"group.name\" \n :label=\"group.display_name || group.name\" \n :value=\"group.name\" \n />\n </el-select>\n </el-form-item>\n </el-form>\n <template #footer>\n <el-button @click=\"resetCreateForm\">取消</el-button>\n <el-button type=\"primary\" @click=\"handleCreate\">创建</el-button>\n </template>\n </el-dialog>\n \n <!-- 编辑渠道对话框 -->\n <el-dialog v-model=\"showEditDialog\" title=\"编辑渠道\" width=\"600px\">\n <el-form :model=\"editForm\" label-width=\"100px\">\n <el-form-item label=\"渠道类型\" required>\n <el-select v-model=\"editForm.type\" @change=\"handleEditChannelTypeChange\">\n <el-option label=\"OpenAI\" :value=\"1\" />\n <el-option label=\"Anthropic\" :value=\"2\" />\n <el-option label=\"Azure\" :value=\"3\" />\n <el-option label=\"Google Gemini\" :value=\"4\" />\n <el-option label=\"豆包 (ByteDance)\" :value=\"14\" />\n <el-option label=\"阿里通义\" :value=\"15\" />\n <el-option label=\"DeepSeek\" :value=\"16\" />\n <el-option label=\"MiniMax\" :value=\"17\" />\n <el-option label=\"智谱 AI\" :value=\"18\" />\n </el-select>\n </el-form-item>\n <el-form-item label=\"名称\" required>\n <el-input v-model=\"editForm.name\" placeholder=\"请输入名称\" />\n </el-form-item>\n <el-form-item label=\"显示名称\" required>\n <el-input v-model=\"editForm.display_name\" placeholder=\"请输入显示名称\" />\n </el-form-item>\n <el-form-item label=\"BaseURL\" required>\n <el-input v-model=\"editForm.base_url\" placeholder=\"https://api.openai.com/v1\" />\n </el-form-item>\n <el-form-item label=\"API Key\" required>\n <el-input v-model=\"editForm.api_key\" type=\"password\" show-password />\n </el-form-item>\n <el-form-item label=\"测试模型\">\n <el-select v-model=\"editForm.test_model\" placeholder=\"请选择测试模型\">\n <el-option \n v-for=\"model in getAvailableModels(editForm.type)\" \n :key=\"model\" \n :label=\"model\" \n :value=\"model\" \n />\n </el-select>\n </el-form-item>\n <el-form-item label=\"优先级\">\n <el-input-number v-model=\"editForm.priority\" :min=\"1\" :step=\"1\" />\n </el-form-item>\n <el-form-item label=\"权重\">\n <el-input-number v-model=\"editForm.weight\" :min=\"1\" :step=\"10\" />\n </el-form-item>\n <el-form-item label=\"状态\">\n <el-select v-model=\"editForm.status\">\n <el-option label=\"启用\" :value=\"1\" />\n <el-option label=\"禁用\" :value=\"0\" />\n <el-option label=\"维护中\" :value=\"2\" />\n </el-select>\n </el-form-item>\n <el-form-item label=\"模型\" required>\n <div class=\"model-management\">\n <!-- 模型选择器 -->\n <el-select\n v-model=\"selectedEditModels\"\n multiple\n filterable\n allow-create\n default-first-option\n placeholder=\"请选择该渠道所支持的模型\"\n style=\"width: 100%\"\n @change=\"updateFilteredEditModels\"\n >\n <el-option\n v-for=\"model in filteredEditModels\"\n :key=\"model\"\n :label=\"model\"\n :value=\"model\"\n />\n </el-select>\n \n <!-- 已选择模型标签显示 -->\n <div class=\"selected-model-tags\" v-if=\"selectedEditModels.length > 0\">\n <el-tag\n v-for=\"model in selectedEditModels\"\n :key=\"model\"\n closable\n @close=\"removeEditModel(model)\"\n class=\"selected-tag\"\n >\n {{ model }}\n </el-tag>\n </div>\n \n <div class=\"model-actions\">\n <el-button type=\"text\" size=\"small\" @click=\"selectAllEditModels\">\n 全选\n </el-button>\n <el-button type=\"text\" size=\"small\" @click=\"clearSelectedEditModels\">\n 清空\n </el-button>\n <el-button type=\"text\" size=\"small\" @click=\"fillEditRelatedModels(editForm.type)\">\n 填入相关模型\n </el-button>\n <el-button type=\"text\" size=\"small\" @click=\"syncEditModels(editForm.type)\" :loading=\"syncingEditModels\">\n <el-icon><Refresh /></el-icon>\n 同步最新模型\n </el-button>\n </div>\n </div>\n </el-form-item>\n \n <el-form-item label=\"分组\">\n <el-select v-model=\"editForm.group\" placeholder=\"选择分组\">\n <el-option \n v-for=\"group in groups\" \n :key=\"group.name\" \n :label=\"group.display_name || group.name\" \n :value=\"group.name\" \n />\n </el-select>\n </el-form-item>\n </el-form>\n <template #footer>\n <el-button @click=\"showEditDialog = false\">取消</el-button>\n <el-button type=\"primary\" @click=\"handleEdit\">保存</el-button>\n </template>\n </el-dialog>\n </div>\n</template>\n\n<script setup>\nimport api from '@/api';\nimport { Check, Delete, Edit, Plus, Refresh } from '@element-plus/icons-vue';\nimport { ElButton, ElMessage, ElTooltip } from 'element-plus';\nimport { onMounted, reactive, ref } from 'vue';\n\nconst loading = ref(false)\nconst batchTesting = ref(false)\nconst testingChannels = ref([])\nconst channels = ref([])\nconst showCreateDialog = ref(false)\nconst showEditDialog = ref(false)\nconst showBaseUrlTip = ref(false)\nconst showApiKeyTip = ref(false)\nconst selectedModels = ref([])\nconst selectedEditModels = ref([])\nconst syncingModels = ref(false)\nconst showManualModelInput = ref(false)\nconst manualModel = ref('')\nconst syncingEditModels = ref(false)\nconst customModels = ref({}) // 存储手动添加的模型\nconst filteredModels = ref([])\nconst filteredEditModels = ref([])\nconst groups = ref([]) // 存储分组列表\nconst hasSyncedModels = ref({}) // 记录是否已经同步过模型\n\nconst createForm = reactive({\n type: 1,\n name: '',\n display_name: '',\n base_url: 'https://api.openai.com/v1',\n api_key: '',\n test_model: 'gpt-3.5-turbo',\n priority: 1,\n weight: 100,\n status: 1,\n models: '',\n group: 'default'\n})\n\nconst editForm = reactive({\n id: '',\n type: 1,\n name: '',\n display_name: '',\n base_url: '',\n api_key: '',\n test_model: '',\n priority: 1,\n weight: 100,\n status: 1,\n models: '',\n group: 'default'\n})\n\n// 模型名称映射\nconst modelNameMap = {\n // 豆包模型映射\n 'Doubao-pro-128k': 'doubao-pro-128k',\n 'Doubao-pro-32k': 'doubao-pro-32k',\n 'Doubao-pro-4k': 'doubao-pro-4k',\n 'Doubao-lite-128k': 'doubao-lite-128k',\n 'Doubao-lite-32k': 'doubao-lite-32k',\n 'Doubao-lite-4k': 'doubao-lite-4k',\n 'Doubao-embedding': 'doubao-embedding-1.0'\n}\n\n// 渠道类型配置\nconst channelConfigs = {\n 1: { // OpenAI\n baseUrl: 'https://api.openai.com/v1',\n testModel: 'gpt-3.5-turbo',\n models: [], // 从API获取\n apiKeyTip: 'OpenAI API Key 格式为 sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'\n },\n 2: { // Anthropic\n baseUrl: 'https://api.anthropic.com/v1',\n testModel: 'claude-3-sonnet-20240229',\n models: [], // 从API获取\n apiKeyTip: 'Anthropic API Key 格式为 sk-ant-api03-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'\n },\n 3: { // Azure\n baseUrl: 'https://your-resource.openai.azure.com/openai/deployments/{deployment-name}',\n testModel: 'gpt-35-turbo',\n models: [], // 从API获取\n apiKeyTip: 'Azure API Key 格式为 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'\n },\n 4: { // Google Gemini\n baseUrl: 'https://generativeai.googleapis.com/v1',\n testModel: 'gemini-pro',\n models: [], // 从API获取\n apiKeyTip: 'Google API Key 格式为 AIzaSyxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'\n },\n 14: { // 豆包\n baseUrl: 'https://ark.cn-beijing.volces.com/api/v3',\n testModel: 'Doubao-pro-128k',\n models: [], // 从API获取\n apiKeyTip: '豆包 API Key 格式为 ak-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'\n },\n 15: { // 阿里通义\n baseUrl: 'https://dashscope.aliyuncs.com/api/v1',\n testModel: 'qwen-turbo',\n models: [], // 从API获取\n apiKeyTip: '阿里通义 API Key 格式为 sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'\n },\n 16: { // DeepSeek\n baseUrl: 'https://api.deepseek.com/v1',\n testModel: 'deepseek-chat',\n models: [], // 从API获取\n apiKeyTip: 'DeepSeek API Key 格式为 sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'\n },\n 17: { // MiniMax\n baseUrl: 'https://api.minimax.chat/v1',\n testModel: 'abab5.5-chat',\n models: [], // 从API获取\n apiKeyTip: 'MiniMax API Key 格式为 sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'\n },\n 18: { // 智谱 AI\n baseUrl: 'https://open.bigmodel.cn/api/mcp',\n testModel: 'chatglm3-6b',\n models: [], // 从API获取\n apiKeyTip: '智谱 AI API Key 格式为 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'\n }\n}\n\nconst channelType = (type) => {\n const types = { \n 1: 'OpenAI', \n 2: 'Anthropic', \n 3: 'Azure', \n 4: 'Google Gemini',\n 14: '豆包',\n 15: '阿里通义',\n 16: 'DeepSeek',\n 17: 'MiniMax',\n 18: '智谱 AI'\n }\n return types[type] || 'Unknown'\n}\n\nconst getChannelTypeTag = (type) => {\n switch (type) {\n case 1: return 'primary'\n case 2: return 'warning'\n case 3: return 'info'\n case 4: return 'success'\n case 14: return 'danger'\n case 15: return 'purple'\n case 16: return 'blue'\n case 17: return 'cyan'\n case 18: return 'teal'\n default: return 'default'\n }\n}\n\nconst getStatusText = (status) => {\n const statuses = { 0: '禁用', 1: '启用', 2: '维护中' }\n return statuses[status] || '未知'\n}\n\nconst getStatusTagType = (status) => {\n switch (status) {\n case 1: return 'success'\n case 0: return 'danger'\n case 2: return 'warning'\n default: return 'default'\n }\n}\n\nconst getAvailableModels = (type) => {\n const defaultModels = channelConfigs[type]?.models || []\n const customTypeModels = customModels.value[type] || []\n return [...defaultModels, ...customTypeModels]\n}\n\n// 计算过滤后的模型列表\nconst updateFilteredModels = () => {\n filteredModels.value = getAvailableModels(createForm.type)\n}\n\nconst updateFilteredEditModels = () => {\n filteredEditModels.value = getAvailableModels(editForm.type)\n}\n\n// 移除模型\nconst removeModel = (model) => {\n const index = selectedModels.value.indexOf(model)\n if (index > -1) {\n selectedModels.value.splice(index, 1)\n }\n}\n\n// 移除编辑模型\nconst removeEditModel = (model) => {\n const index = selectedEditModels.value.indexOf(model)\n if (index > -1) {\n selectedEditModels.value.splice(index, 1)\n }\n}\n\nconst getApiKeyTip = (type) => {\n return channelConfigs[type]?.apiKeyTip || '请输入API Key'\n}\n\nconst handleChannelTypeChange = () => {\n const config = channelConfigs[createForm.type]\n if (config) {\n createForm.base_url = config.baseUrl\n createForm.test_model = config.testModel\n selectedModels.value = []\n }\n // 更新过滤后的模型列表\n updateFilteredModels()\n}\n\nconst handleEditChannelTypeChange = () => {\n const config = channelConfigs[editForm.type]\n if (config) {\n editForm.base_url = config.baseUrl\n editForm.test_model = config.testModel\n selectedEditModels.value = []\n }\n // 更新过滤后的模型列表\n updateFilteredEditModels()\n}\n\nconst selectAllModels = () => {\n selectedModels.value = [...getAvailableModels(createForm.type)]\n}\n\nconst clearSelectedModels = () => {\n selectedModels.value = []\n}\n\nconst selectAllEditModels = () => {\n selectedEditModels.value = [...getAvailableModels(editForm.type)]\n}\n\nconst clearSelectedEditModels = () => {\n selectedEditModels.value = []\n}\n\nconst syncModels = async (type) => {\n syncingModels.value = true\n try {\n // 从后端API获取最新模型列表\n ElMessage.info('正在同步最新模型...')\n \n const response = await api.admin.getLatestModels({\n type,\n api_key: createForm.api_key,\n base_url: createForm.base_url\n })\n const latestModels = response.data.data || []\n \n // 更新渠道配置中的模型列表\n if (channelConfigs[type]) {\n channelConfigs[type].models = latestModels\n }\n \n // 更新过滤后的模型列表\n updateFilteredModels()\n \n ElMessage.success('模型同步成功')\n } catch (error) {\n console.error('Failed to sync models:', error)\n ElMessage.error('模型同步失败')\n } finally {\n syncingModels.value = false\n }\n}\n\nconst syncEditModels = async (type) => {\n syncingEditModels.value = true\n try {\n // 从后端API获取最新模型列表\n ElMessage.info('正在同步最新模型...')\n \n const response = await api.admin.getLatestModels({\n type,\n api_key: editForm.api_key,\n base_url: editForm.base_url\n })\n const latestModels = response.data.data || []\n \n // 更新渠道配置中的模型列表\n if (channelConfigs[type]) {\n channelConfigs[type].models = latestModels\n }\n \n // 更新过滤后的模型列表\n updateFilteredEditModels()\n \n ElMessage.success('模型同步成功')\n } catch (error) {\n console.error('Failed to sync models:', error)\n ElMessage.error('模型同步失败')\n } finally {\n syncingEditModels.value = false\n }\n}\n\n// 初始化模型列表\nconst initModels = async () => {\n try {\n // 为所有渠道类型同步模型列表\n for (const type in channelConfigs) {\n const response = await api.admin.getLatestModels({ type })\n const latestModels = response.data.data || []\n if (channelConfigs[type]) {\n channelConfigs[type].models = latestModels\n }\n }\n // 更新过滤后的模型列表\n updateFilteredModels()\n updateFilteredEditModels()\n } catch (error) {\n console.error('Failed to initialize models:', error)\n }\n}\n\nconst addManualModel = () => {\n if (!manualModel.value.trim()) {\n ElMessage.warning('请输入模型名称')\n return\n }\n \n const type = createForm.type\n if (!customModels.value[type]) {\n customModels.value[type] = []\n }\n \n // 检查模型是否已存在\n if (getAvailableModels(type).includes(manualModel.value.trim())) {\n ElMessage.warning('模型已存在')\n return\n }\n \n // 添加到自定义模型列表\n customModels.value[type].push(manualModel.value.trim())\n ElMessage.success('模型添加成功')\n manualModel.value = ''\n}\n\nconst addEditManualModel = () => {\n if (!editManualModel.value.trim()) {\n ElMessage.warning('请输入模型名称')\n return\n }\n \n const type = editForm.type\n if (!customModels.value[type]) {\n customModels.value[type] = []\n }\n \n // 检查模型是否已存在\n if (getAvailableModels(type).includes(editManualModel.value.trim())) {\n ElMessage.warning('模型已存在')\n return\n }\n \n // 添加到自定义模型列表\n customModels.value[type].push(editManualModel.value.trim())\n ElMessage.success('模型添加成功')\n editManualModel.value = ''\n}\n\n// 根据渠道类型获取最新模型列表\nconst getLatestModelsByType = (type) => {\n // 模拟从API获取最新模型\n // 实际项目中,这里应该调用后端API获取最新模型\n switch (type) {\n case 1: // OpenAI\n return ['gpt-3.5-turbo', 'gpt-4', 'gpt-4o', 'gpt-4-turbo', 'gpt-4o-mini', 'gpt-5-alpha']\n case 2: // Anthropic\n return ['claude-3-sonnet-20240229', 'claude-3-opus-20240229', 'claude-3-haiku-20240307', 'claude-3.5-sonnet-20240620']\n case 3: // Azure\n return ['gpt-35-turbo', 'gpt-4', 'gpt-4-turbo', 'gpt-4o']\n case 4: // Google Gemini\n return ['gemini-pro', 'gemini-1.5-pro', 'gemini-1.5-flash', 'gemini-2.0-pro']\n case 14: // 豆包\n return [\n 'doubao-1.5-pro-20240528',\n 'doubao-1.5-flash-20240528',\n 'doubao-1.0-pro-20240528',\n 'doubao-1.0-flash-20240528',\n 'ep-20240604172028-4q775',\n 'ep-20240610144610-g97pz',\n 'ep-20240701171326-7q7d2',\n 'ep-20240715143158-4v8z9',\n 'ep-20240801150102-5g6h7',\n 'doubao-embedding-1.0'\n ]\n case 15: // 阿里通义\n return ['qwen-turbo', 'qwen-plus', 'qwen-max', 'qwen-2.5-turbo', 'qwen-2.5-plus']\n case 16: // DeepSeek\n return ['deepseek-chat', 'deepseek-llm', 'deepseek-v3.1']\n case 17: // MiniMax\n return ['abab5.5-chat', 'abab6-chat', 'abab6.5-chat']\n case 18: // 智谱 AI\n return ['chatglm3-6b', 'chatglm3-6b-32k', 'glm-4', 'glm-4-flash']\n default:\n return []\n }\n}\n\nconst toggleModelSelection = (model) => {\n const index = selectedModels.value.indexOf(model)\n if (index > -1) {\n selectedModels.value.splice(index, 1)\n } else {\n selectedModels.value.push(model)\n }\n}\n\nconst toggleEditModelSelection = (model) => {\n const index = selectedEditModels.value.indexOf(model)\n if (index > -1) {\n selectedEditModels.value.splice(index, 1)\n } else {\n selectedEditModels.value.push(model)\n }\n}\n\nconst fillRelatedModels = (type) => {\n // 填入该渠道类型的所有模型\n selectedModels.value = [...getAvailableModels(type)]\n ElMessage.success('已填入相关模型')\n}\n\nconst fillEditRelatedModels = (type) => {\n // 填入该渠道类型的所有模型\n selectedEditModels.value = [...getAvailableModels(type)]\n ElMessage.success('已填入相关模型')\n}\n\n// 转换模型名称为API使用的名称\nconst convertModelNames = (models) => {\n return models.map(model => {\n return modelNameMap[model] || model\n })\n}\n\nconst resetCreateForm = () => {\n createForm.type = 1\n createForm.name = ''\n createForm.display_name = ''\n createForm.base_url = 'https://api.openai.com/v1'\n createForm.api_key = ''\n createForm.test_model = 'gpt-3.5-turbo'\n createForm.priority = 1\n createForm.weight = 100\n createForm.status = 1\n createForm.models = ''\n createForm.group = 'default'\n selectedModels.value = []\n showCreateDialog.value = false\n showManualModelInput.value = false\n manualModel.value = ''\n // 清除同步标记\n hasSyncedModels.value = {}\n // 更新过滤后的模型列表\n updateFilteredModels()\n}\n\nconst loadChannels = async () => {\n loading.value = true\n try {\n const res = await api.admin.getChannels({ page: 1, page_size: 100 })\n // 新响应格式: { code, message, data: { data: [...], total: N } }\n channels.value = res.data.data?.data || []\n } catch (error) {\n console.error('Failed to load channels:', error)\n ElMessage.error('加载渠道失败')\n } finally {\n loading.value = false\n }\n}\n\nconst refreshChannels = () => {\n loadChannels()\n}\n\nconst handleCreate = async () => {\n try {\n // 转换模型名称并转换为JSON字符串\n const convertedModels = convertModelNames(selectedModels.value)\n createForm.models = JSON.stringify(convertedModels)\n \n const data = {\n ...createForm\n }\n await api.admin.createChannel(data)\n ElMessage.success('创建成功')\n resetCreateForm()\n loadChannels()\n } catch (error) {\n console.error('Failed to create channel:', error)\n ElMessage.error('创建失败')\n }\n}\n\nconst editChannel = (channel) => {\n editForm.id = channel.id\n editForm.type = channel.type\n editForm.name = channel.name\n editForm.display_name = channel.display_name\n editForm.base_url = channel.base_url\n editForm.api_key = channel.api_key || ''\n editForm.test_model = channel.test_model\n editForm.priority = channel.priority\n editForm.weight = channel.weight\n editForm.status = channel.status\n editForm.models = channel.models\n editForm.group = channel.group || 'default'\n \n // 解析模型列表\n try {\n selectedEditModels.value = JSON.parse(channel.models || '[]')\n } catch (e) {\n selectedEditModels.value = []\n }\n \n // 清除同步标记\n hasSyncedModels.value = {}\n \n // 更新过滤后的模型列表\n updateFilteredEditModels()\n \n showEditDialog.value = true\n}\n\nconst handleEdit = async () => {\n try {\n // 转换模型名称并转换为JSON字符串\n const convertedModels = convertModelNames(selectedEditModels.value)\n editForm.models = JSON.stringify(convertedModels)\n \n const data = {\n ...editForm\n }\n await api.admin.updateChannel(editForm.id, data)\n ElMessage.success('更新成功')\n showEditDialog.value = false\n loadChannels()\n } catch (error) {\n console.error('Failed to update channel:', error)\n ElMessage.error('更新失败')\n }\n}\n\nconst testChannel = async (id) => {\n testingChannels.value.push(id)\n try {\n const response = await api.admin.testChannel(id)\n ElMessage.success(`测试通过,响应时间:${response.data.response_time || 0}ms`)\n } catch (error) {\n ElMessage.error('测试失败:' + (error.response?.data?.error || '未知错误'))\n } finally {\n testingChannels.value = testingChannels.value.filter(item => item !== id)\n }\n}\n\nconst batchTestChannels = async () => {\n batchTesting.value = true\n try {\n const response = await api.admin.batchTestChannels()\n ElMessage.success('批量测试完成')\n loadChannels()\n } catch (error) {\n ElMessage.error('批量测试失败')\n } finally {\n batchTesting.value = false\n }\n}\n\nconst deleteChannel = async (id) => {\n if (!confirm('确定要删除此渠道吗?')) return\n try {\n await api.admin.deleteChannel(id)\n ElMessage.success('删除成功')\n loadChannels()\n } catch (error) {\n console.error('Failed to delete channel:', error)\n ElMessage.error('删除失败')\n }\n}\n\nconst loadGroups = async () => {\n try {\n const res = await api.admin.getGroups({ page: 1, page_size: 100 })\n groups.value = res.data.data?.data || []\n } catch (error) {\n console.error('Failed to load groups:', error)\n }\n}\n\nconst handleModelFocus = async (form, mode) => {\n const key = `${mode}-${form.type}`\n \n // 如果已经同步过,就不再同步\n if (hasSyncedModels.value[key]) {\n return\n }\n \n if (!form.api_key) {\n ElMessage.warning('请先填写 API Key')\n return\n }\n if (!form.base_url) {\n ElMessage.warning('请先填写 Base URL')\n return\n }\n if (!form.type) {\n ElMessage.warning('请先选择渠道类型')\n return\n }\n \n try {\n const response = await api.admin.getLatestModels({\n type: form.type,\n api_key: form.api_key,\n base_url: form.base_url\n })\n const latestModels = response.data.data || []\n \n if (channelConfigs[form.type]) {\n channelConfigs[form.type].models = latestModels\n }\n \n if (mode === 'create') {\n updateFilteredModels()\n } else {\n updateFilteredEditModels()\n }\n \n // 标记为已同步\n hasSyncedModels.value[key] = true\n \n ElMessage.success('模型列表获取成功')\n } catch (error) {\n console.error('Failed to get models:', error)\n // 不显示错误消息,避免影响用户体验\n }\n}\n\nonMounted(() => {\n loadChannels()\n loadGroups()\n // 初始化过滤后的模型列表\n updateFilteredModels()\n updateFilteredEditModels()\n})\n</script>\n\n<style scoped lang=\"scss\">\n.channels-page {\n padding: 20px;\n .card-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n \n .header-actions {\n display: flex;\n gap: 10px;\n }\n }\n \n .el-table {\n margin-top: 20px;\n \n .el-table__row {\n transition: all 0.3s ease;\n \n &:hover {\n background-color: #f5f7fa;\n }\n }\n }\n \n .model-management {\n display: flex;\n flex-direction: column;\n gap: 10px;\n \n .selected-model-tags {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n padding: 10px;\n background-color: #f5f7fa;\n border-radius: 4px;\n min-height: 40px;\n \n .selected-tag {\n font-weight: bold;\n cursor: pointer;\n transition: all 0.3s ease;\n \n &:hover {\n transform: translateY(-2px);\n }\n }\n }\n \n .model-actions {\n display: flex;\n gap: 8px;\n flex-wrap: wrap;\n padding: 5px 0;\n }\n }\n}\n</style>\n"}, "644d9e61e38f9e26163655416f9ed9003d4338b6": {"t": 1785759879.6416724, "n": 30736, "h": "5f9f41f1b2f0c988", "c": "<template>\n <div class=\"channels-page\">\n <el-card>\n <template #header>\n <div class=\"card-header\">\n <span>渠道管理</span>\n <div class=\"header-actions\">\n <el-button type=\"primary\" @click=\"showCreateDialog = true\">\n <el-icon><Plus /></el-icon>\n 添加渠道\n </el-button>\n <el-button @click=\"batchTestChannels\" :loading=\"batchTesting\">\n <el-icon><Refresh /></el-icon>\n 批量测试\n </el-button>\n <el-button @click=\"refreshChannels\">\n <el-icon><Refresh /></el-icon>\n 刷新\n </el-button>\n </div>\n </div>\n </template>\n \n <el-table :data=\"channels\" v-loading=\"loading\" border style=\"width: 100%\">\n <el-table-column prop=\"id\" label=\"ID\" width=\"80\" />\n <el-table-column prop=\"name\" label=\"名称\" />\n <el-table-column prop=\"api_key\" label=\"API Key\" show-overflow-tooltip>\n <template #default=\"{ row }\">\n {{ row.api_key ? '******' + row.api_key.slice(-4) : '' }}\n </template>\n </el-table-column>\n <el-table-column prop=\"group\" label=\"分组\" width=\"120\">\n <template #default=\"{ row }\">\n <el-tag size=\"small\">\n {{ row.group || 'default' }}\n </el-tag>\n </template>\n </el-table-column>\n <el-table-column prop=\"type\" label=\"类型\" width=\"120\">\n <template #default=\"{ row }\">\n <el-tag :type=\"getChannelTypeTag(row.type)\">\n {{ channelType(row.type) }}\n </el-tag>\n </template>\n </el-table-column>\n <el-table-column prop=\"base_url\" label=\"BaseURL\" show-overflow-tooltip />\n <el-table-column prop=\"priority\" label=\"优先级\" width=\"80\" />\n <el-table-column prop=\"weight\" label=\"权重\" width=\"80\" />\n <el-table-column prop=\"status\" label=\"状态\" width=\"100\">\n <template #default=\"{ row }\">\n <el-tag :type=\"getStatusTagType(row.status)\" size=\"small\">\n {{ getStatusText(row.status) }}\n </el-tag>\n </template>\n </el-table-column>\n <el-table-column prop=\"last_test_time\" label=\"最后测试时间\" width=\"160\">\n <template #default=\"{ row }\">\n {{ row.last_test_time || '未测试' }}\n </template>\n </el-table-column>\n <el-table-column label=\"操作\" width=\"300\">\n <template #default=\"{ row }\">\n <el-button size=\"small\" @click=\"testChannel(row.id)\" :loading=\"testingChannels.includes(row.id)\">\n <el-icon><Check /></el-icon>\n 测试\n </el-button>\n <el-button size=\"small\" type=\"warning\" @click=\"editChannel(row)\">\n <el-icon><Edit /></el-icon>\n 编辑\n </el-button>\n <el-button size=\"small\" type=\"danger\" @click=\"deleteChannel(row.id)\">\n <el-icon><Delete /></el-icon>\n 删除\n </el-button>\n </template>\n </el-table-column>\n </el-table>\n </el-card>\n \n <!-- 创建渠道对话框 -->\n <el-dialog v-model=\"showCreateDialog\" title=\"添加渠道\" width=\"600px\">\n <el-form :model=\"createForm\" label-width=\"100px\">\n <el-form-item label=\"渠道类型\" required>\n <el-select v-model=\"createForm.type\" placeholder=\"请选择渠道类型\" @change=\"handleChannelTypeChange\">\n <el-option label=\"OpenAI\" :value=\"1\" />\n <el-option label=\"Anthropic\" :value=\"2\" />\n <el-option label=\"Azure\" :value=\"3\" />\n <el-option label=\"Google Gemini\" :value=\"4\" />\n <el-option label=\"豆包 (ByteDance)\" :value=\"14\" />\n <el-option label=\"阿里通义\" :value=\"15\" />\n <el-option label=\"DeepSeek\" :value=\"16\" />\n <el-option label=\"MiniMax\" :value=\"17\" />\n <el-option label=\"智谱 AI\" :value=\"18\" />\n </el-select>\n </el-form-item>\n <el-form-item label=\"名称\" required>\n <el-input v-model=\"createForm.name\" placeholder=\"请输入名称\" />\n </el-form-item>\n <el-form-item label=\"显示名称\" required>\n <el-input v-model=\"createForm.display_name\" placeholder=\"请输入显示名称\" />\n </el-form-item>\n <el-form-item label=\"BaseURL\" required>\n <el-input v-model=\"createForm.base_url\" placeholder=\"https://api.openai.com/v1\" />\n <el-tooltip content=\"根据选择的渠道类型,系统已自动填充默认BaseURL\" placement=\"top\" :visible=\"showBaseUrlTip\">\n <el-button type=\"text\" size=\"small\" @click=\"showBaseUrlTip = !showBaseUrlTip\">\n 显示提示\n </el-button>\n </el-tooltip>\n </el-form-item>\n <el-form-item label=\"API Key\" required>\n <el-input v-model=\"createForm.api_key\" type=\"password\" show-password />\n <el-tooltip content=\"{{ getApiKeyTip(createForm.type) }}\" placement=\"top\" :visible=\"showApiKeyTip\">\n <el-button type=\"text\" size=\"small\" @click=\"showApiKeyTip = !showApiKeyTip\">\n API Key提示\n </el-button>\n </el-tooltip>\n </el-form-item>\n <el-form-item label=\"测试模型\">\n <el-select v-model=\"createForm.test_model\" placeholder=\"请选择测试模型\">\n <el-option \n v-for=\"model in getAvailableModels(createForm.type)\" \n :key=\"model\" \n :label=\"model\" \n :value=\"model\" \n />\n </el-select>\n </el-form-item>\n <el-form-item label=\"优先级\">\n <el-input-number v-model=\"createForm.priority\" :min=\"1\" :step=\"1\" />\n <el-tooltip content=\"优先级越高,越先被使用\" placement=\"top\">\n <el-button type=\"text\" size=\"small\">\n ?\n </el-button>\n </el-tooltip>\n </el-form-item>\n <el-form-item label=\"权重\">\n <el-input-number v-model=\"createForm.weight\" :min=\"1\" :step=\"10\" />\n <el-tooltip content=\"权重越高,被使用的概率越大\" placement=\"top\">\n <el-button type=\"text\" size=\"small\">\n ?\n </el-button>\n </el-tooltip>\n </el-form-item>\n <el-form-item label=\"状态\">\n <el-select v-model=\"createForm.status\">\n <el-option label=\"启用\" :value=\"1\" />\n <el-option label=\"禁用\" :value=\"0\" />\n <el-option label=\"维护中\" :value=\"2\" />\n </el-select>\n </el-form-item>\n <el-form-item label=\"模型\" required>\n <div class=\"model-management\">\n <!-- 模型选择器 -->\n <el-select\n v-model=\"selectedModels\"\n multiple\n filterable\n allow-create\n default-first-option\n placeholder=\"请选择该渠道所支持的模型\"\n style=\"width: 100%\"\n @change=\"updateFilteredModels\"\n >\n <el-option\n v-for=\"model in filteredModels\"\n :key=\"model\"\n :label=\"model\"\n :value=\"model\"\n />\n </el-select>\n \n <!-- 已选择模型标签显示 -->\n <div class=\"selected-model-tags\" v-if=\"selectedModels.length > 0\">\n <el-tag\n v-for=\"model in selectedModels\"\n :key=\"model\"\n closable\n @close=\"removeModel(model)\"\n class=\"selected-tag\"\n >\n {{ model }}\n </el-tag>\n </div>\n \n <div class=\"model-actions\">\n <el-button type=\"text\" size=\"small\" @click=\"selectAllModels\">\n 全选\n </el-button>\n <el-button type=\"text\" size=\"small\" @click=\"clearSelectedModels\">\n 清空\n </el-button>\n <el-button type=\"text\" size=\"small\" @click=\"fillRelatedModels(createForm.type)\">\n 填入相关模型\n </el-button>\n <el-button type=\"text\" size=\"small\" @click=\"syncModels(createForm.type)\" :loading=\"syncingModels\">\n <el-icon><Refresh /></el-icon>\n 同步最新模型\n </el-button>\n </div>\n </div>\n </el-form-item>\n \n <el-form-item label=\"分组\">\n <el-select v-model=\"createForm.group\" placeholder=\"选择分组\">\n <el-option \n v-for=\"group in groups\" \n :key=\"group.name\" \n :label=\"group.display_name || group.name\" \n :value=\"group.name\" \n />\n </el-select>\n </el-form-item>\n </el-form>\n <template #footer>\n <el-button @click=\"resetCreateForm\">取消</el-button>\n <el-button type=\"primary\" @click=\"handleCreate\">创建</el-button>\n </template>\n </el-dialog>\n \n <!-- 编辑渠道对话框 -->\n <el-dialog v-model=\"showEditDialog\" title=\"编辑渠道\" width=\"600px\">\n <el-form :model=\"editForm\" label-width=\"100px\">\n <el-form-item label=\"渠道类型\" required>\n <el-select v-model=\"editForm.type\" @change=\"handleEditChannelTypeChange\">\n <el-option label=\"OpenAI\" :value=\"1\" />\n <el-option label=\"Anthropic\" :value=\"2\" />\n <el-option label=\"Azure\" :value=\"3\" />\n <el-option label=\"Google Gemini\" :value=\"4\" />\n <el-option label=\"豆包 (ByteDance)\" :value=\"14\" />\n <el-option label=\"阿里通义\" :value=\"15\" />\n <el-option label=\"DeepSeek\" :value=\"16\" />\n <el-option label=\"MiniMax\" :value=\"17\" />\n <el-option label=\"智谱 AI\" :value=\"18\" />\n </el-select>\n </el-form-item>\n <el-form-item label=\"名称\" required>\n <el-input v-model=\"editForm.name\" placeholder=\"请输入名称\" />\n </el-form-item>\n <el-form-item label=\"显示名称\" required>\n <el-input v-model=\"editForm.display_name\" placeholder=\"请输入显示名称\" />\n </el-form-item>\n <el-form-item label=\"BaseURL\" required>\n <el-input v-model=\"editForm.base_url\" placeholder=\"https://api.openai.com/v1\" />\n </el-form-item>\n <el-form-item label=\"API Key\" required>\n <el-input v-model=\"editForm.api_key\" type=\"password\" show-password />\n </el-form-item>\n <el-form-item label=\"测试模型\">\n <el-select v-model=\"editForm.test_model\" placeholder=\"请选择测试模型\">\n <el-option \n v-for=\"model in getAvailableModels(editForm.type)\" \n :key=\"model\" \n :label=\"model\" \n :value=\"model\" \n />\n </el-select>\n </el-form-item>\n <el-form-item label=\"优先级\">\n <el-input-number v-model=\"editForm.priority\" :min=\"1\" :step=\"1\" />\n </el-form-item>\n <el-form-item label=\"权重\">\n <el-input-number v-model=\"editForm.weight\" :min=\"1\" :step=\"10\" />\n </el-form-item>\n <el-form-item label=\"状态\">\n <el-select v-model=\"editForm.status\">\n <el-option label=\"启用\" :value=\"1\" />\n <el-option label=\"禁用\" :value=\"0\" />\n <el-option label=\"维护中\" :value=\"2\" />\n </el-select>\n </el-form-item>\n <el-form-item label=\"模型\" required>\n <div class=\"model-management\">\n <!-- 模型选择器 -->\n <el-select\n v-model=\"selectedEditModels\"\n multiple\n filterable\n allow-create\n default-first-option\n placeholder=\"请选择该渠道所支持的模型\"\n style=\"width: 100%\"\n @change=\"updateFilteredEditModels\"\n >\n <el-option\n v-for=\"model in filteredEditModels\"\n :key=\"model\"\n :label=\"model\"\n :value=\"model\"\n />\n </el-select>\n \n <!-- 已选择模型标签显示 -->\n <div class=\"selected-model-tags\" v-if=\"selectedEditModels.length > 0\">\n <el-tag\n v-for=\"model in selectedEditModels\"\n :key=\"model\"\n closable\n @close=\"removeEditModel(model)\"\n class=\"selected-tag\"\n >\n {{ model }}\n </el-tag>\n </div>\n \n <div class=\"model-actions\">\n <el-button type=\"text\" size=\"small\" @click=\"selectAllEditModels\">\n 全选\n </el-button>\n <el-button type=\"text\" size=\"small\" @click=\"clearSelectedEditModels\">\n 清空\n </el-button>\n <el-button type=\"text\" size=\"small\" @click=\"fillEditRelatedModels(editForm.type)\">\n 填入相关模型\n </el-button>\n <el-button type=\"text\" size=\"small\" @click=\"syncEditModels(editForm.type)\" :loading=\"syncingEditModels\">\n <el-icon><Refresh /></el-icon>\n 同步最新模型\n </el-button>\n </div>\n </div>\n </el-form-item>\n \n <el-form-item label=\"分组\">\n <el-select v-model=\"editForm.group\" placeholder=\"选择分组\">\n <el-option \n v-for=\"group in groups\" \n :key=\"group.name\" \n :label=\"group.display_name || group.name\" \n :value=\"group.name\" \n />\n </el-select>\n </el-form-item>\n </el-form>\n <template #footer>\n <el-button @click=\"showEditDialog = false\">取消</el-button>\n <el-button type=\"primary\" @click=\"handleEdit\">保存</el-button>\n </template>\n </el-dialog>\n </div>\n</template>\n\n<script setup>\nimport api from '@/api';\nimport { Check, Delete, Edit, Plus, Refresh } from '@element-plus/icons-vue';\nimport { ElButton, ElMessage, ElTooltip } from 'element-plus';\nimport { onMounted, reactive, ref } from 'vue';\n\nconst loading = ref(false)\nconst batchTesting = ref(false)\nconst testingChannels = ref([])\nconst channels = ref([])\nconst showCreateDialog = ref(false)\nconst showEditDialog = ref(false)\nconst showBaseUrlTip = ref(false)\nconst showApiKeyTip = ref(false)\nconst selectedModels = ref([])\nconst selectedEditModels = ref([])\nconst syncingModels = ref(false)\nconst showManualModelInput = ref(false)\nconst manualModel = ref('')\nconst syncingEditModels = ref(false)\nconst customModels = ref({}) // 存储手动添加的模型\nconst filteredModels = ref([])\nconst filteredEditModels = ref([])\nconst groups = ref([]) // 存储分组列表\nconst hasSyncedModels = ref({}) // 记录是否已经同步过模型\n\nconst createForm = reactive({\n type: 1,\n name: '',\n display_name: '',\n base_url: 'https://api.openai.com/v1',\n api_key: '',\n test_model: 'gpt-3.5-turbo',\n priority: 1,\n weight: 100,\n status: 1,\n models: '',\n group: 'default'\n})\n\nconst editForm = reactive({\n id: '',\n type: 1,\n name: '',\n display_name: '',\n base_url: '',\n api_key: '',\n test_model: '',\n priority: 1,\n weight: 100,\n status: 1,\n models: '',\n group: 'default'\n})\n\n// 模型名称映射\nconst modelNameMap = {\n // 豆包模型映射\n 'Doubao-pro-128k': 'doubao-pro-128k',\n 'Doubao-pro-32k': 'doubao-pro-32k',\n 'Doubao-pro-4k': 'doubao-pro-4k',\n 'Doubao-lite-128k': 'doubao-lite-128k',\n 'Doubao-lite-32k': 'doubao-lite-32k',\n 'Doubao-lite-4k': 'doubao-lite-4k',\n 'Doubao-embedding': 'doubao-embedding-1.0'\n}\n\n// 渠道类型配置\nconst channelConfigs = {\n 1: { // OpenAI\n baseUrl: 'https://api.openai.com/v1',\n testModel: 'gpt-3.5-turbo',\n models: [], // 从API获取\n apiKeyTip: 'OpenAI API Key 格式为 sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'\n },\n 2: { // Anthropic\n baseUrl: 'https://api.anthropic.com/v1',\n testModel: 'claude-3-sonnet-20240229',\n models: [], // 从API获取\n apiKeyTip: 'Anthropic API Key 格式为 sk-ant-api03-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'\n },\n 3: { // Azure\n baseUrl: 'https://your-resource.openai.azure.com/openai/deployments/{deployment-name}',\n testModel: 'gpt-35-turbo',\n models: [], // 从API获取\n apiKeyTip: 'Azure API Key 格式为 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'\n },\n 4: { // Google Gemini\n baseUrl: 'https://generativeai.googleapis.com/v1',\n testModel: 'gemini-pro',\n models: [], // 从API获取\n apiKeyTip: 'Google API Key 格式为 AIzaSyxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'\n },\n 14: { // 豆包\n baseUrl: 'https://ark.cn-beijing.volces.com/api/v3',\n testModel: 'Doubao-pro-128k',\n models: [], // 从API获取\n apiKeyTip: '豆包 API Key 格式为 ak-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'\n },\n 15: { // 阿里通义\n baseUrl: 'https://dashscope.aliyuncs.com/api/v1',\n testModel: 'qwen-turbo',\n models: [], // 从API获取\n apiKeyTip: '阿里通义 API Key 格式为 sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'\n },\n 16: { // DeepSeek\n baseUrl: 'https://api.deepseek.com/v1',\n testModel: 'deepseek-chat',\n models: [], // 从API获取\n apiKeyTip: 'DeepSeek API Key 格式为 sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'\n },\n 17: { // MiniMax\n baseUrl: 'https://api.minimax.chat/v1',\n testModel: 'abab5.5-chat',\n models: [], // 从API获取\n apiKeyTip: 'MiniMax API Key 格式为 sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'\n },\n 18: { // 智谱 AI\n baseUrl: 'https://open.bigmodel.cn/api/mcp',\n testModel: 'chatglm3-6b',\n models: [], // 从API获取\n apiKeyTip: '智谱 AI API Key 格式为 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'\n }\n}\n\nconst channelType = (type) => {\n const types = { \n 1: 'OpenAI', \n 2: 'Anthropic', \n 3: 'Azure', \n 4: 'Google Gemini',\n 14: '豆包',\n 15: '阿里通义',\n 16: 'DeepSeek',\n 17: 'MiniMax',\n 18: '智谱 AI'\n }\n return types[type] || 'Unknown'\n}\n\nconst getChannelTypeTag = (type) => {\n switch (type) {\n case 1: return 'primary'\n case 2: return 'warning'\n case 3: return 'info'\n case 4: return 'success'\n case 14: return 'danger'\n case 15: return 'purple'\n case 16: return 'blue'\n case 17: return 'cyan'\n case 18: return 'teal'\n default: return 'default'\n }\n}\n\nconst getStatusText = (status) => {\n const statuses = { 0: '禁用', 1: '启用', 2: '维护中' }\n return statuses[status] || '未知'\n}\n\nconst getStatusTagType = (status) => {\n switch (status) {\n case 1: return 'success'\n case 0: return 'danger'\n case 2: return 'warning'\n default: return 'default'\n }\n}\n\nconst getAvailableModels = (type) => {\n const defaultModels = channelConfigs[type]?.models || []\n const customTypeModels = customModels.value[type] || []\n return [...defaultModels, ...customTypeModels]\n}\n\n// 计算过滤后的模型列表\nconst updateFilteredModels = () => {\n filteredModels.value = getAvailableModels(createForm.type)\n}\n\nconst updateFilteredEditModels = () => {\n filteredEditModels.value = getAvailableModels(editForm.type)\n}\n\n// 移除模型\nconst removeModel = (model) => {\n const index = selectedModels.value.indexOf(model)\n if (index > -1) {\n selectedModels.value.splice(index, 1)\n }\n}\n\n// 移除编辑模型\nconst removeEditModel = (model) => {\n const index = selectedEditModels.value.indexOf(model)\n if (index > -1) {\n selectedEditModels.value.splice(index, 1)\n }\n}\n\nconst getApiKeyTip = (type) => {\n return channelConfigs[type]?.apiKeyTip || '请输入API Key'\n}\n\nconst handleChannelTypeChange = () => {\n const config = channelConfigs[createForm.type]\n if (config) {\n createForm.base_url = config.baseUrl\n createForm.test_model = config.testModel\n selectedModels.value = []\n }\n // 更新过滤后的模型列表\n updateFilteredModels()\n}\n\nconst handleEditChannelTypeChange = () => {\n const config = channelConfigs[editForm.type]\n if (config) {\n editForm.base_url = config.baseUrl\n editForm.test_model = config.testModel\n selectedEditModels.value = []\n }\n // 更新过滤后的模型列表\n updateFilteredEditModels()\n}\n\nconst selectAllModels = () => {\n selectedModels.value = [...getAvailableModels(createForm.type)]\n}\n\nconst clearSelectedModels = () => {\n selectedModels.value = []\n}\n\nconst selectAllEditModels = () => {\n selectedEditModels.value = [...getAvailableModels(editForm.type)]\n}\n\nconst clearSelectedEditModels = () => {\n selectedEditModels.value = []\n}\n\nconst syncModels = async (type) => {\n syncingModels.value = true\n try {\n // 从后端API获取最新模型列表\n ElMessage.info('正在同步最新模型...')\n \n const response = await api.admin.getLatestModels({\n type,\n api_key: createForm.api_key,\n base_url: createForm.base_url\n })\n const latestModels = response.data.data || []\n \n // 更新渠道配置中的模型列表\n if (channelConfigs[type]) {\n channelConfigs[type].models = latestModels\n }\n \n // 更新过滤后的模型列表\n updateFilteredModels()\n \n ElMessage.success('模型同步成功')\n } catch (error) {\n console.error('Failed to sync models:', error)\n ElMessage.error('模型同步失败')\n } finally {\n syncingModels.value = false\n }\n}\n\nconst syncEditModels = async (type) => {\n syncingEditModels.value = true\n try {\n // 从后端API获取最新模型列表\n ElMessage.info('正在同步最新模型...')\n \n const response = await api.admin.getLatestModels({\n type,\n api_key: editForm.api_key,\n base_url: editForm.base_url\n })\n const latestModels = response.data.data || []\n \n // 更新渠道配置中的模型列表\n if (channelConfigs[type]) {\n channelConfigs[type].models = latestModels\n }\n \n // 更新过滤后的模型列表\n updateFilteredEditModels()\n \n ElMessage.success('模型同步成功')\n } catch (error) {\n console.error('Failed to sync models:', error)\n ElMessage.error('模型同步失败')\n } finally {\n syncingEditModels.value = false\n }\n}\n\n// 初始化模型列表\nconst initModels = async () => {\n try {\n // 为所有渠道类型同步模型列表\n for (const type in channelConfigs) {\n const response = await api.admin.getLatestModels({ type })\n const latestModels = response.data.data || []\n if (channelConfigs[type]) {\n channelConfigs[type].models = latestModels\n }\n }\n // 更新过滤后的模型列表\n updateFilteredModels()\n updateFilteredEditModels()\n } catch (error) {\n console.error('Failed to initialize models:', error)\n }\n}\n\nconst addManualModel = () => {\n if (!manualModel.value.trim()) {\n ElMessage.warning('请输入模型名称')\n return\n }\n \n const type = createForm.type\n if (!customModels.value[type]) {\n customModels.value[type] = []\n }\n \n // 检查模型是否已存在\n if (getAvailableModels(type).includes(manualModel.value.trim())) {\n ElMessage.warning('模型已存在')\n return\n }\n \n // 添加到自定义模型列表\n customModels.value[type].push(manualModel.value.trim())\n ElMessage.success('模型添加成功')\n manualModel.value = ''\n}\n\nconst addEditManualModel = () => {\n if (!editManualModel.value.trim()) {\n ElMessage.warning('请输入模型名称')\n return\n }\n \n const type = editForm.type\n if (!customModels.value[type]) {\n customModels.value[type] = []\n }\n \n // 检查模型是否已存在\n if (getAvailableModels(type).includes(editManualModel.value.trim())) {\n ElMessage.warning('模型已存在')\n return\n }\n \n // 添加到自定义模型列表\n customModels.value[type].push(editManualModel.value.trim())\n ElMessage.success('模型添加成功')\n editManualModel.value = ''\n}\n\n// 根据渠道类型获取最新模型列表\nconst getLatestModelsByType = (type) => {\n // 模拟从API获取最新模型\n // 实际项目中,这里应该调用后端API获取最新模型\n switch (type) {\n case 1: // OpenAI\n return ['gpt-3.5-turbo', 'gpt-4', 'gpt-4o', 'gpt-4-turbo', 'gpt-4o-mini', 'gpt-5-alpha']\n case 2: // Anthropic\n return ['claude-3-sonnet-20240229', 'claude-3-opus-20240229', 'claude-3-haiku-20240307', 'claude-3.5-sonnet-20240620']\n case 3: // Azure\n return ['gpt-35-turbo', 'gpt-4', 'gpt-4-turbo', 'gpt-4o']\n case 4: // Google Gemini\n return ['gemini-pro', 'gemini-1.5-pro', 'gemini-1.5-flash', 'gemini-2.0-pro']\n case 14: // 豆包\n return [\n 'doubao-1.5-pro-20240528',\n 'doubao-1.5-flash-20240528',\n 'doubao-1.0-pro-20240528',\n 'doubao-1.0-flash-20240528',\n 'ep-20240604172028-4q775',\n 'ep-20240610144610-g97pz',\n 'ep-20240701171326-7q7d2',\n 'ep-20240715143158-4v8z9',\n 'ep-20240801150102-5g6h7',\n 'doubao-embedding-1.0'\n ]\n case 15: // 阿里通义\n return ['qwen-turbo', 'qwen-plus', 'qwen-max', 'qwen-2.5-turbo', 'qwen-2.5-plus']\n case 16: // DeepSeek\n return ['deepseek-chat', 'deepseek-llm', 'deepseek-v3.1']\n case 17: // MiniMax\n return ['abab5.5-chat', 'abab6-chat', 'abab6.5-chat']\n case 18: // 智谱 AI\n return ['chatglm3-6b', 'chatglm3-6b-32k', 'glm-4', 'glm-4-flash']\n default:\n return []\n }\n}\n\nconst toggleModelSelection = (model) => {\n const index = selectedModels.value.indexOf(model)\n if (index > -1) {\n selectedModels.value.splice(index, 1)\n } else {\n selectedModels.value.push(model)\n }\n}\n\nconst toggleEditModelSelection = (model) => {\n const index = selectedEditModels.value.indexOf(model)\n if (index > -1) {\n selectedEditModels.value.splice(index, 1)\n } else {\n selectedEditModels.value.push(model)\n }\n}\n\nconst fillRelatedModels = (type) => {\n // 填入该渠道类型的所有模型\n selectedModels.value = [...getAvailableModels(type)]\n ElMessage.success('已填入相关模型')\n}\n\nconst fillEditRelatedModels = (type) => {\n // 填入该渠道类型的所有模型\n selectedEditModels.value = [...getAvailableModels(type)]\n ElMessage.success('已填入相关模型')\n}\n\n// 转换模型名称为API使用的名称\nconst convertModelNames = (models) => {\n return models.map(model => {\n return modelNameMap[model] || model\n })\n}\n\nconst resetCreateForm = () => {\n createForm.type = 1\n createForm.name = ''\n createForm.display_name = ''\n createForm.base_url = 'https://api.openai.com/v1'\n createForm.api_key = ''\n createForm.test_model = 'gpt-3.5-turbo'\n createForm.priority = 1\n createForm.weight = 100\n createForm.status = 1\n createForm.models = ''\n createForm.group = 'default'\n selectedModels.value = []\n showCreateDialog.value = false\n showManualModelInput.value = false\n manualModel.value = ''\n // 清除同步标记\n hasSyncedModels.value = {}\n // 更新过滤后的模型列表\n updateFilteredModels()\n}\n\nconst loadChannels = async () => {\n loading.value = true\n try {\n const res = await api.admin.getChannels({ page: 1, page_size: 100 })\n // 新响应格式: { code, message, data: { data: [...], total: N } }\n channels.value = res.data.data?.data || []\n } catch (error) {\n console.error('Failed to load channels:', error)\n ElMessage.error('加载渠道失败')\n } finally {\n loading.value = false\n }\n}\n\nconst refreshChannels = () => {\n loadChannels()\n}\n\nconst handleCreate = async () => {\n try {\n // 转换模型名称并转换为JSON字符串\n const convertedModels = convertModelNames(selectedModels.value)\n createForm.models = JSON.stringify(convertedModels)\n \n const data = {\n ...createForm\n }\n await api.admin.createChannel(data)\n ElMessage.success('创建成功')\n resetCreateForm()\n loadChannels()\n } catch (error) {\n console.error('Failed to create channel:', error)\n ElMessage.error('创建失败')\n }\n}\n\nconst editChannel = (channel) => {\n editForm.id = channel.id\n editForm.type = channel.type\n editForm.name = channel.name\n editForm.display_name = channel.display_name\n editForm.base_url = channel.base_url\n editForm.api_key = channel.api_key || ''\n editForm.test_model = channel.test_model\n editForm.priority = channel.priority\n editForm.weight = channel.weight\n editForm.status = channel.status\n editForm.models = channel.models\n editForm.group = channel.group || 'default'\n \n // 解析模型列表\n try {\n selectedEditModels.value = JSON.parse(channel.models || '[]')\n } catch (e) {\n selectedEditModels.value = []\n }\n \n // 清除同步标记\n hasSyncedModels.value = {}\n \n // 更新过滤后的模型列表\n updateFilteredEditModels()\n \n showEditDialog.value = true\n}\n\nconst handleEdit = async () => {\n try {\n // 转换模型名称并转换为JSON字符串\n const convertedModels = convertModelNames(selectedEditModels.value)\n editForm.models = JSON.stringify(convertedModels)\n \n const data = {\n ...editForm\n }\n await api.admin.updateChannel(editForm.id, data)\n ElMessage.success('更新成功')\n showEditDialog.value = false\n loadChannels()\n } catch (error) {\n console.error('Failed to update channel:', error)\n ElMessage.error('更新失败')\n }\n}\n\nconst testChannel = async (id) => {\n testingChannels.value.push(id)\n try {\n const response = await api.admin.testChannel(id)\n ElMessage.success(`测试通过,响应时间:${response.data.response_time || 0}ms`)\n } catch (error) {\n ElMessage.error('测试失败:' + (error.response?.data?.error || '未知错误'))\n } finally {\n testingChannels.value = testingChannels.value.filter(item => item !== id)\n }\n}\n\nconst batchTestChannels = async () => {\n batchTesting.value = true\n try {\n const response = await api.admin.batchTestChannels()\n ElMessage.success('批量测试完成')\n loadChannels()\n } catch (error) {\n ElMessage.error('批量测试失败')\n } finally {\n batchTesting.value = false\n }\n}\n\nconst deleteChannel = async (id) => {\n if (!confirm('确定要删除此渠道吗?')) return\n try {\n await api.admin.deleteChannel(id)\n ElMessage.success('删除成功')\n loadChannels()\n } catch (error) {\n console.error('Failed to delete channel:', error)\n ElMessage.error('删除失败')\n }\n}\n\nconst loadGroups = async () => {\n try {\n const res = await api.admin.getGroups({ page: 1, page_size: 100 })\n groups.value = res.data.data?.data || []\n } catch (error) {\n console.error('Failed to load groups:', error)\n }\n}\n\nconst handleModelFocus = async (form, mode) => {\n const key = `${mode}-${form.type}`\n \n // 如果已经同步过,就不再同步\n if (hasSyncedModels.value[key]) {\n return\n }\n \n if (!form.api_key) {\n ElMessage.warning('请先填写 API Key')\n return\n }\n if (!form.base_url) {\n ElMessage.warning('请先填写 Base URL')\n return\n }\n if (!form.type) {\n ElMessage.warning('请先选择渠道类型')\n return\n }\n \n try {\n const response = await api.admin.getLatestModels({\n type: form.type,\n api_key: form.api_key,\n base_url: form.base_url\n })\n const latestModels = response.data.data || []\n \n if (channelConfigs[form.type]) {\n channelConfigs[form.type].models = latestModels\n }\n \n if (mode === 'create') {\n updateFilteredModels()\n } else {\n updateFilteredEditModels()\n }\n \n // 标记为已同步\n hasSyncedModels.value[key] = true\n \n ElMessage.success('模型列表获取成功')\n } catch (error) {\n console.error('Failed to get models:', error)\n // 不显示错误消息,避免影响用户体验\n }\n}\n\nonMounted(() => {\n loadChannels()\n loadGroups()\n // 初始化过滤后的模型列表\n updateFilteredModels()\n updateFilteredEditModels()\n})\n</script>\n\n<style scoped lang=\"scss\">\n.channels-page {\n padding: 20px;\n .card-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n \n .header-actions {\n display: flex;\n gap: 10px;\n }\n }\n \n .el-table {\n margin-top: 20px;\n \n .el-table__row {\n transition: all 0.3s ease;\n \n &:hover {\n background-color: #f5f7fa;\n }\n }\n }\n \n .model-management {\n display: flex;\n flex-direction: column;\n gap: 10px;\n \n .selected-model-tags {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n padding: 10px;\n background-color: #f5f7fa;\n border-radius: 4px;\n min-height: 40px;\n \n .selected-tag {\n font-weight: bold;\n cursor: pointer;\n transition: all 0.3s ease;\n \n &:hover {\n transform: translateY(-2px);\n }\n }\n }\n \n .model-actions {\n display: flex;\n gap: 8px;\n flex-wrap: wrap;\n padding: 5px 0;\n }\n }\n}\n</style>\n"}, "223a324ebaa7104531eeb80eb2d97930edff6b0a": {"t": 1785759881.1665463, "n": 30794, "h": "36f77c3e7744b453", "c": "<template>\n <div class=\"channels-page\">\n <el-card>\n <template #header>\n <div class=\"card-header\">\n <span>渠道管理</span>\n <div class=\"header-actions\">\n <el-button type=\"primary\" @click=\"showCreateDialog = true\">\n <el-icon><Plus /></el-icon>\n 添加渠道\n </el-button>\n <el-button @click=\"batchTestChannels\" :loading=\"batchTesting\">\n <el-icon><Refresh /></el-icon>\n 批量测试\n </el-button>\n <el-button @click=\"refreshChannels\">\n <el-icon><Refresh /></el-icon>\n 刷新\n </el-button>\n </div>\n </div>\n </template>\n \n <el-table :data=\"channels\" v-loading=\"loading\" border style=\"width: 100%\">\n <el-table-column prop=\"id\" label=\"ID\" width=\"80\" />\n <el-table-column prop=\"name\" label=\"名称\" />\n <el-table-column prop=\"display_name\" label=\"显示名称\" />\n <el-table-column prop=\"type\" label=\"类型\" width=\"120\">\n <template #default=\"{ row }\">\n <el-tag :type=\"getChannelTypeTag(row.type)\">\n {{ channelType(row.type) }}\n </el-tag>\n </template>\n </el-table-column>\n <el-table-column prop=\"base_url\" label=\"BaseURL\" show-overflow-tooltip />\n <el-table-column prop=\"priority\" label=\"优先级\" width=\"80\" />\n <el-table-column prop=\"weight\" label=\"权重\" width=\"80\" />\n <el-table-column prop=\"status\" label=\"状态\" width=\"100\">\n <template #default=\"{ row }\">\n <el-tag :type=\"getStatusTagType(row.status)\" size=\"small\">\n {{ getStatusText(row.status) }}\n </el-tag>\n </template>\n </el-table-column>\n <el-table-column prop=\"last_test_time\" label=\"最后测试时间\" width=\"160\">\n <template #default=\"{ row }\">\n {{ row.last_test_time || '未测试' }}\n </template>\n </el-table-column>\n <el-table-column label=\"操作\" width=\"300\">\n <template #default=\"{ row }\">\n <el-button size=\"small\" @click=\"testChannel(row.id)\" :loading=\"testingChannels.includes(row.id)\">\n <el-icon><Check /></el-icon>\n 测试\n </el-button>\n <el-button size=\"small\" type=\"warning\" @click=\"editChannel(row)\">\n <el-icon><Edit /></el-icon>\n 编辑\n </el-button>\n <el-button size=\"small\" type=\"danger\" @click=\"deleteChannel(row.id)\">\n <el-icon><Delete /></el-icon>\n 删除\n </el-button>\n </template>\n </el-table-column>\n </el-table>\n </el-card>\n \n <!-- 创建渠道对话框 -->\n <el-dialog v-model=\"showCreateDialog\" title=\"添加渠道\" width=\"600px\">\n <el-form :model=\"createForm\" label-width=\"100px\">\n <el-form-item label=\"渠道类型\" required>\n <el-select v-model=\"createForm.type\" placeholder=\"请选择渠道类型\" @change=\"handleChannelTypeChange\">\n <el-option label=\"OpenAI\" :value=\"1\" />\n <el-option label=\"Anthropic\" :value=\"2\" />\n <el-option label=\"Azure\" :value=\"3\" />\n <el-option label=\"Google Gemini\" :value=\"4\" />\n <el-option label=\"豆包 (ByteDance)\" :value=\"14\" />\n <el-option label=\"阿里通义\" :value=\"15\" />\n <el-option label=\"DeepSeek\" :value=\"16\" />\n <el-option label=\"MiniMax\" :value=\"17\" />\n <el-option label=\"智谱 AI\" :value=\"18\" />\n </el-select>\n </el-form-item>\n <el-form-item label=\"名称\" required>\n <el-input v-model=\"createForm.name\" placeholder=\"请输入名称\" />\n </el-form-item>\n <el-form-item label=\"显示名称\" required>\n <el-input v-model=\"createForm.display_name\" placeholder=\"请输入显示名称\" />\n </el-form-item>\n <el-form-item label=\"BaseURL\" required>\n <el-input v-model=\"createForm.base_url\" placeholder=\"https://api.openai.com/v1\" />\n <el-tooltip content=\"根据选择的渠道类型,系统已自动填充默认BaseURL\" placement=\"top\" :visible=\"showBaseUrlTip\">\n <el-button type=\"text\" size=\"small\" @click=\"showBaseUrlTip = !showBaseUrlTip\">\n 显示提示\n </el-button>\n </el-tooltip>\n </el-form-item>\n <el-form-item label=\"API Key\" required>\n <el-input v-model=\"createForm.api_key\" type=\"password\" show-password />\n <el-tooltip content=\"{{ getApiKeyTip(createForm.type) }}\" placement=\"top\" :visible=\"showApiKeyTip\">\n <el-button type=\"text\" size=\"small\" @click=\"showApiKeyTip = !showApiKeyTip\">\n API Key提示\n </el-button>\n </el-tooltip>\n </el-form-item>\n <el-form-item label=\"测试模型\">\n <el-select v-model=\"createForm.test_model\" placeholder=\"请选择测试模型\">\n <el-option \n v-for=\"model in getAvailableModels(createForm.type)\" \n :key=\"model\" \n :label=\"model\" \n :value=\"model\" \n />\n </el-select>\n </el-form-item>\n <el-form-item label=\"优先级\">\n <el-input-number v-model=\"createForm.priority\" :min=\"1\" :step=\"1\" />\n <el-tooltip content=\"优先级越高,越先被使用\" placement=\"top\">\n <el-button type=\"text\" size=\"small\">\n ?\n </el-button>\n </el-tooltip>\n </el-form-item>\n <el-form-item label=\"权重\">\n <el-input-number v-model=\"createForm.weight\" :min=\"1\" :step=\"10\" />\n <el-tooltip content=\"权重越高,被使用的概率越大\" placement=\"top\">\n <el-button type=\"text\" size=\"small\">\n ?\n </el-button>\n </el-tooltip>\n </el-form-item>\n <el-form-item label=\"状态\">\n <el-select v-model=\"createForm.status\">\n <el-option label=\"启用\" :value=\"1\" />\n <el-option label=\"禁用\" :value=\"0\" />\n <el-option label=\"维护中\" :value=\"2\" />\n </el-select>\n </el-form-item>\n <el-form-item label=\"支持模型\" required>\n <div class=\"model-management\">\n <!-- 模型搜索框 -->\n <div class=\"model-search\">\n <el-input \n v-model=\"modelSearchQuery\" \n placeholder=\"搜索模型...\" \n @input=\"handleModelSearch\"\n >\n <template #prefix>\n <el-icon><Search /></el-icon>\n </template>\n <template #suffix>\n <el-button \n v-if=\"modelSearchQuery\" \n type=\"text\" \n @click=\"clearModelSearch\"\n >\n <el-icon><Close /></el-icon>\n </el-button>\n </template>\n </el-input>\n </div>\n \n <!-- 模型标签显示 -->\n <div class=\"model-tags\">\n <el-tag \n v-for=\"model in filteredModels\" \n :key=\"model\" \n :effect=\"selectedModels.includes(model) ? 'dark' : 'plain'\"\n :class=\"{'selected-tag': selectedModels.includes(model)}\"\n @click=\"toggleModelSelection(model)\"\n >\n {{ model }}\n <el-icon v-if=\"selectedModels.includes(model)\">\n <Check />\n </el-icon>\n </el-tag>\n <el-empty v-if=\"filteredModels.length === 0\" description=\"未找到匹配的模型\" />\n </div>\n \n <div class=\"model-actions\">\n <el-button type=\"text\" size=\"small\" @click=\"selectAllModels\">\n 全选\n </el-button>\n <el-button type=\"text\" size=\"small\" @click=\"clearSelectedModels\">\n 清空\n </el-button>\n <el-button type=\"text\" size=\"small\" @click=\"fillRelatedModels(createForm.type)\">\n 填入相关模型\n </el-button>\n <el-button type=\"text\" size=\"small\" @click=\"syncModels(createForm.type)\" :loading=\"syncingModels\">\n <el-icon><Refresh /></el-icon>\n 同步最新模型\n </el-button>\n <el-button type=\"text\" size=\"small\" @click=\"showManualModelInput = !showManualModelInput\">\n 更多\n </el-button>\n </div>\n \n <div v-if=\"showManualModelInput\" class=\"manual-model-input\">\n <el-input v-model=\"manualModel\" placeholder=\"输入自定义模型名称\" @keyup.enter=\"addManualModel\" />\n <el-button type=\"primary\" size=\"small\" @click=\"addManualModel\">\n 填入\n </el-button>\n </div>\n </div>\n </el-form-item>\n \n <el-form-item label=\"分组\">\n <el-select v-model=\"createForm.group\" placeholder=\"选择分组\">\n <el-option label=\"default\" :value=\"'default'\" />\n <el-option label=\"openai\" :value=\"'openai'\" />\n <el-option label=\"anthropic\" :value=\"'anthropic'\" />\n <el-option label=\"google\" :value=\"'google'\" />\n <el-option label=\"azure\" :value=\"'azure'\" />\n <el-option label=\"domestic\" :value=\"'domestic'\" />\n </el-select>\n </el-form-item>\n </el-form>\n <template #footer>\n <el-button @click=\"resetCreateForm\">取消</el-button>\n <el-button type=\"primary\" @click=\"handleCreate\">创建</el-button>\n </template>\n </el-dialog>\n \n <!-- 编辑渠道对话框 -->\n <el-dialog v-model=\"showEditDialog\" title=\"编辑渠道\" width=\"600px\">\n <el-form :model=\"editForm\" label-width=\"100px\">\n <el-form-item label=\"渠道类型\" required>\n <el-select v-model=\"editForm.type\" @change=\"handleEditChannelTypeChange\">\n <el-option label=\"OpenAI\" :value=\"1\" />\n <el-option label=\"Anthropic\" :value=\"2\" />\n <el-option label=\"Azure\" :value=\"3\" />\n <el-option label=\"Google Gemini\" :value=\"4\" />\n <el-option label=\"豆包 (ByteDance)\" :value=\"14\" />\n <el-option label=\"阿里通义\" :value=\"15\" />\n <el-option label=\"DeepSeek\" :value=\"16\" />\n <el-option label=\"MiniMax\" :value=\"17\" />\n <el-option label=\"智谱 AI\" :value=\"18\" />\n </el-select>\n </el-form-item>\n <el-form-item label=\"名称\" required>\n <el-input v-model=\"editForm.name\" placeholder=\"请输入名称\" />\n </el-form-item>\n <el-form-item label=\"显示名称\" required>\n <el-input v-model=\"editForm.display_name\" placeholder=\"请输入显示名称\" />\n </el-form-item>\n <el-form-item label=\"BaseURL\" required>\n <el-input v-model=\"editForm.base_url\" placeholder=\"https://api.openai.com/v1\" />\n </el-form-item>\n <el-form-item label=\"API Key\" required>\n <el-input v-model=\"editForm.api_key\" type=\"password\" show-password />\n </el-form-item>\n <el-form-item label=\"测试模型\">\n <el-select v-model=\"editForm.test_model\" placeholder=\"请选择测试模型\">\n <el-option \n v-for=\"model in getAvailableModels(editForm.type)\" \n :key=\"model\" \n :label=\"model\" \n :value=\"model\" \n />\n </el-select>\n </el-form-item>\n <el-form-item label=\"优先级\">\n <el-input-number v-model=\"editForm.priority\" :min=\"1\" :step=\"1\" />\n </el-form-item>\n <el-form-item label=\"权重\">\n <el-input-number v-model=\"editForm.weight\" :min=\"1\" :step=\"10\" />\n </el-form-item>\n <el-form-item label=\"状态\">\n <el-select v-model=\"editForm.status\">\n <el-option label=\"启用\" :value=\"1\" />\n <el-option label=\"禁用\" :value=\"0\" />\n <el-option label=\"维护中\" :value=\"2\" />\n </el-select>\n </el-form-item>\n <el-form-item label=\"支持模型\" required>\n <div class=\"model-management\">\n <!-- 模型搜索框 -->\n <div class=\"model-search\">\n <el-input \n v-model=\"editModelSearchQuery\" \n placeholder=\"搜索模型...\" \n @input=\"handleEditModelSearch\"\n >\n <template #prefix>\n <el-icon><Search /></el-icon>\n </template>\n <template #suffix>\n <el-button \n v-if=\"editModelSearchQuery\" \n type=\"text\" \n @click=\"clearEditModelSearch\"\n >\n <el-icon><Close /></el-icon>\n </el-button>\n </template>\n </el-input>\n </div>\n \n <!-- 模型标签显示 -->\n <div class=\"model-tags\">\n <el-tag \n v-for=\"model in filteredEditModels\" \n :key=\"model\" \n :effect=\"selectedEditModels.includes(model) ? 'dark' : 'plain'\"\n :class=\"{'selected-tag': selectedEditModels.includes(model)}\"\n @click=\"toggleEditModelSelection(model)\"\n >\n {{ model }}\n <el-icon v-if=\"selectedEditModels.includes(model)\">\n <Check />\n </el-icon>\n </el-tag>\n <el-empty v-if=\"filteredEditModels.length === 0\" description=\"未找到匹配的模型\" />\n </div>\n \n <div class=\"model-actions\">\n <el-button type=\"text\" size=\"small\" @click=\"selectAllEditModels\">\n 全选\n </el-button>\n <el-button type=\"text\" size=\"small\" @click=\"clearSelectedEditModels\">\n 清空\n </el-button>\n <el-button type=\"text\" size=\"small\" @click=\"fillEditRelatedModels(editForm.type)\">\n 填入相关模型\n </el-button>\n <el-button type=\"text\" size=\"small\" @click=\"syncEditModels(editForm.type)\" :loading=\"syncingEditModels\">\n <el-icon><Refresh /></el-icon>\n 同步最新模型\n </el-button>\n <el-button type=\"text\" size=\"small\" @click=\"showEditManualModelInput = !showEditManualModelInput\">\n 更多\n </el-button>\n </div>\n \n <div v-if=\"showEditManualModelInput\" class=\"manual-model-input\">\n <el-input v-model=\"editManualModel\" placeholder=\"输入自定义模型名称\" @keyup.enter=\"addEditManualModel\" />\n <el-button type=\"primary\" size=\"small\" @click=\"addEditManualModel\">\n 填入\n </el-button>\n </div>\n </div>\n </el-form-item>\n \n <el-form-item label=\"分组\">\n <el-select v-model=\"editForm.group\" placeholder=\"选择分组\">\n <el-option label=\"default\" :value=\"'default'\" />\n <el-option label=\"openai\" :value=\"'openai'\" />\n <el-option label=\"anthropic\" :value=\"'anthropic'\" />\n <el-option label=\"google\" :value=\"'google'\" />\n <el-option label=\"azure\" :value=\"'azure'\" />\n <el-option label=\"domestic\" :value=\"'domestic'\" />\n </el-select>\n </el-form-item>\n </el-form>\n <template #footer>\n <el-button @click=\"showEditDialog = false\">取消</el-button>\n <el-button type=\"primary\" @click=\"handleEdit\">保存</el-button>\n </template>\n </el-dialog>\n </div>\n</template>\n\n<script setup>\nimport api from '@/api';\nimport { Check, Delete, Edit, Plus, Refresh } from '@element-plus/icons-vue';\nimport { ElButton, ElMessage, ElTooltip } from 'element-plus';\nimport { onMounted, reactive, ref } from 'vue';\n\nconst loading = ref(false)\nconst batchTesting = ref(false)\nconst testingChannels = ref([])\nconst channels = ref([])\nconst showCreateDialog = ref(false)\nconst showEditDialog = ref(false)\nconst showBaseUrlTip = ref(false)\nconst showApiKeyTip = ref(false)\nconst selectedModels = ref([])\nconst selectedEditModels = ref([])\nconst syncingModels = ref(false)\nconst showManualModelInput = ref(false)\nconst manualModel = ref('')\nconst syncingEditModels = ref(false)\nconst showEditManualModelInput = ref(false)\nconst editManualModel = ref('')\nconst customModels = ref({}) // 存储手动添加的模型\nconst modelSearchQuery = ref('')\nconst editModelSearchQuery = ref('')\nconst filteredModels = ref([])\nconst filteredEditModels = ref([])\n\nconst createForm = reactive({\n type: 1,\n name: '',\n display_name: '',\n base_url: 'https://api.openai.com/v1',\n api_key: '',\n test_model: 'gpt-3.5-turbo',\n priority: 1,\n weight: 100,\n status: 1,\n models: '',\n group: 'default'\n})\n\nconst editForm = reactive({\n id: '',\n type: 1,\n name: '',\n display_name: '',\n base_url: '',\n api_key: '',\n test_model: '',\n priority: 1,\n weight: 100,\n status: 1,\n models: '',\n group: 'default'\n})\n\n// 渠道类型配置\nconst channelConfigs = {\n 1: { // OpenAI\n baseUrl: 'https://api.openai.com/v1',\n testModel: 'gpt-3.5-turbo',\n models: ['gpt-3.5-turbo', 'gpt-4', 'gpt-4o', 'gpt-4-turbo'],\n apiKeyTip: 'OpenAI API Key 格式为 sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'\n },\n 2: { // Anthropic\n baseUrl: 'https://api.anthropic.com/v1',\n testModel: 'claude-3-sonnet-20240229',\n models: ['claude-3-sonnet-20240229', 'claude-3-opus-20240229', 'claude-3-haiku-20240307'],\n apiKeyTip: 'Anthropic API Key 格式为 sk-ant-api03-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'\n },\n 3: { // Azure\n baseUrl: 'https://your-resource.openai.azure.com/openai/deployments/{deployment-name}',\n testModel: 'gpt-35-turbo',\n models: ['gpt-35-turbo', 'gpt-4', 'gpt-4-turbo'],\n apiKeyTip: 'Azure API Key 格式为 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'\n },\n 4: { // Google Gemini\n baseUrl: 'https://generativeai.googleapis.com/v1',\n testModel: 'gemini-pro',\n models: ['gemini-pro', 'gemini-1.5-pro', 'gemini-1.5-flash'],\n apiKeyTip: 'Google API Key 格式为 AIzaSyxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'\n },\n 14: { // 豆包\n baseUrl: 'https://ark.cn-beijing.volces.com/api/v3',\n testModel: 'ep-20240604172028-4q775',\n models: ['ep-20240604172028-4q775', 'ep-20240610144610-g97pz'],\n apiKeyTip: '豆包 API Key 格式为 ak-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'\n },\n 15: { // 阿里通义\n baseUrl: 'https://dashscope.aliyuncs.com/api/v1',\n testModel: 'qwen-turbo',\n models: ['qwen-turbo', 'qwen-plus', 'qwen-max'],\n apiKeyTip: '阿里通义 API Key 格式为 sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'\n },\n 16: { // DeepSeek\n baseUrl: 'https://api.deepseek.com/v1',\n testModel: 'deepseek-chat',\n models: ['deepseek-chat', 'deepseek-llm'],\n apiKeyTip: 'DeepSeek API Key 格式为 sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'\n },\n 17: { // MiniMax\n baseUrl: 'https://api.minimax.chat/v1',\n testModel: 'abab5.5-chat',\n models: ['abab5.5-chat', 'abab6-chat'],\n apiKeyTip: 'MiniMax API Key 格式为 sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'\n },\n 18: { // 智谱 AI\n baseUrl: 'https://open.bigmodel.cn/api/mcp',\n testModel: 'chatglm3-6b',\n models: ['chatglm3-6b', 'chatglm3-6b-32k', 'glm-4'],\n apiKeyTip: '智谱 AI API Key 格式为 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'\n }\n}\n\nconst channelType = (type) => {\n const types = { \n 1: 'OpenAI', \n 2: 'Anthropic', \n 3: 'Azure', \n 4: 'Google Gemini',\n 14: '豆包',\n 15: '阿里通义',\n 16: 'DeepSeek',\n 17: 'MiniMax',\n 18: '智谱 AI'\n }\n return types[type] || 'Unknown'\n}\n\nconst getChannelTypeTag = (type) => {\n switch (type) {\n case 1: return 'primary'\n case 2: return 'warning'\n case 3: return 'info'\n case 4: return 'success'\n case 14: return 'danger'\n case 15: return 'purple'\n case 16: return 'blue'\n case 17: return 'cyan'\n case 18: return 'teal'\n default: return 'default'\n }\n}\n\nconst getStatusText = (status) => {\n const statuses = { 0: '禁用', 1: '启用', 2: '维护中' }\n return statuses[status] || '未知'\n}\n\nconst getStatusTagType = (status) => {\n switch (status) {\n case 1: return 'success'\n case 0: return 'danger'\n case 2: return 'warning'\n default: return 'default'\n }\n}\n\nconst getAvailableModels = (type) => {\n const defaultModels = channelConfigs[type]?.models || []\n const customTypeModels = customModels.value[type] || []\n return [...defaultModels, ...customTypeModels]\n}\n\n// 计算过滤后的模型列表\nconst updateFilteredModels = () => {\n if (!modelSearchQuery.value) {\n filteredModels.value = getAvailableModels(createForm.type)\n } else {\n filteredModels.value = getAvailableModels(createForm.type).filter(model => \n model.toLowerCase().includes(modelSearchQuery.value.toLowerCase())\n )\n }\n}\n\nconst updateFilteredEditModels = () => {\n if (!editModelSearchQuery.value) {\n filteredEditModels.value = getAvailableModels(editForm.type)\n } else {\n filteredEditModels.value = getAvailableModels(editForm.type).filter(model => \n model.toLowerCase().includes(editModelSearchQuery.value.toLowerCase())\n )\n }\n}\n\n// 处理模型搜索\nconst handleModelSearch = () => {\n updateFilteredModels()\n}\n\nconst handleEditModelSearch = () => {\n updateFilteredEditModels()\n}\n\n// 清空搜索\nconst clearModelSearch = () => {\n modelSearchQuery.value = ''\n updateFilteredModels()\n}\n\nconst clearEditModelSearch = () => {\n editModelSearchQuery.value = ''\n updateFilteredEditModels()\n}\n\nconst getApiKeyTip = (type) => {\n return channelConfigs[type]?.apiKeyTip || '请输入API Key'\n}\n\nconst handleChannelTypeChange = () => {\n const config = channelConfigs[createForm.type]\n if (config) {\n createForm.base_url = config.baseUrl\n createForm.test_model = config.testModel\n selectedModels.value = []\n }\n // 更新过滤后的模型列表\n updateFilteredModels()\n}\n\nconst handleEditChannelTypeChange = () => {\n const config = channelConfigs[editForm.type]\n if (config) {\n editForm.base_url = config.baseUrl\n editForm.test_model = config.testModel\n selectedEditModels.value = []\n }\n // 更新过滤后的模型列表\n updateFilteredEditModels()\n}\n\nconst selectAllModels = () => {\n selectedModels.value = [...getAvailableModels(createForm.type)]\n}\n\nconst clearSelectedModels = () => {\n selectedModels.value = []\n}\n\nconst selectAllEditModels = () => {\n selectedEditModels.value = [...getAvailableModels(editForm.type)]\n}\n\nconst clearSelectedEditModels = () => {\n selectedEditModels.value = []\n}\n\nconst syncModels = async (type) => {\n syncingModels.value = true\n try {\n // 从后端API获取最新模型列表\n ElMessage.info('正在同步最新模型...')\n \n const response = await api.admin.getLatestModels({ type })\n const latestModels = response.data.data || []\n \n // 更新渠道配置中的模型列表\n if (channelConfigs[type]) {\n channelConfigs[type].models = latestModels\n }\n \n // 更新过滤后的模型列表\n updateFilteredModels()\n \n ElMessage.success('模型同步成功')\n } catch (error) {\n console.error('Failed to sync models:', error)\n ElMessage.error('模型同步失败')\n } finally {\n syncingModels.value = false\n }\n}\n\nconst syncEditModels = async (type) => {\n syncingEditModels.value = true\n try {\n // 从后端API获取最新模型列表\n ElMessage.info('正在同步最新模型...')\n \n const response = await api.admin.getLatestModels({ type })\n const latestModels = response.data.data || []\n \n // 更新渠道配置中的模型列表\n if (channelConfigs[type]) {\n channelConfigs[type].models = latestModels\n }\n \n // 更新过滤后的模型列表\n updateFilteredEditModels()\n \n ElMessage.success('模型同步成功')\n } catch (error) {\n console.error('Failed to sync models:', error)\n ElMessage.error('模型同步失败')\n } finally {\n syncingEditModels.value = false\n }\n}\n\nconst addManualModel = () => {\n if (!manualModel.value.trim()) {\n ElMessage.warning('请输入模型名称')\n return\n }\n \n const type = createForm.type\n if (!customModels.value[type]) {\n customModels.value[type] = []\n }\n \n // 检查模型是否已存在\n if (getAvailableModels(type).includes(manualModel.value.trim())) {\n ElMessage.warning('模型已存在')\n return\n }\n \n // 添加到自定义模型列表\n customModels.value[type].push(manualModel.value.trim())\n ElMessage.success('模型添加成功')\n manualModel.value = ''\n}\n\nconst addEditManualModel = () => {\n if (!editManualModel.value.trim()) {\n ElMessage.warning('请输入模型名称')\n return\n }\n \n const type = editForm.type\n if (!customModels.value[type]) {\n customModels.value[type] = []\n }\n \n // 检查模型是否已存在\n if (getAvailableModels(type).includes(editManualModel.value.trim())) {\n ElMessage.warning('模型已存在')\n return\n }\n \n // 添加到自定义模型列表\n customModels.value[type].push(editManualModel.value.trim())\n ElMessage.success('模型添加成功')\n editManualModel.value = ''\n}\n\n// 根据渠道类型获取最新模型列表\nconst getLatestModelsByType = (type) => {\n // 模拟从API获取最新模型\n // 实际项目中,这里应该调用后端API获取最新模型\n switch (type) {\n case 1: // OpenAI\n return ['gpt-3.5-turbo', 'gpt-4', 'gpt-4o', 'gpt-4-turbo', 'gpt-4o-mini', 'gpt-5-alpha']\n case 2: // Anthropic\n return ['claude-3-sonnet-20240229', 'claude-3-opus-20240229', 'claude-3-haiku-20240307', 'claude-3.5-sonnet-20240620']\n case 3: // Azure\n return ['gpt-35-turbo', 'gpt-4', 'gpt-4-turbo', 'gpt-4o']\n case 4: // Google Gemini\n return ['gemini-pro', 'gemini-1.5-pro', 'gemini-1.5-flash', 'gemini-2.0-pro']\n case 14: // 豆包\n return ['ep-20240604172028-4q775', 'ep-20240610144610-g97pz', 'doubao-1.5-pro-20240528']\n case 15: // 阿里通义\n return ['qwen-turbo', 'qwen-plus', 'qwen-max', 'qwen-2.5-turbo', 'qwen-2.5-plus']\n case 16: // DeepSeek\n return ['deepseek-chat', 'deepseek-llm', 'deepseek-v3.1']\n case 17: // MiniMax\n return ['abab5.5-chat', 'abab6-chat', 'abab6.5-chat']\n case 18: // 智谱 AI\n return ['chatglm3-6b', 'chatglm3-6b-32k', 'glm-4', 'glm-4-flash']\n default:\n return []\n }\n}\n\nconst toggleModelSelection = (model) => {\n const index = selectedModels.value.indexOf(model)\n if (index > -1) {\n selectedModels.value.splice(index, 1)\n } else {\n selectedModels.value.push(model)\n }\n}\n\nconst toggleEditModelSelection = (model) => {\n const index = selectedEditModels.value.indexOf(model)\n if (index > -1) {\n selectedEditModels.value.splice(index, 1)\n } else {\n selectedEditModels.value.push(model)\n }\n}\n\nconst fillRelatedModels = (type) => {\n // 填入该渠道类型的所有模型\n selectedModels.value = [...getAvailableModels(type)]\n ElMessage.success('已填入相关模型')\n}\n\nconst fillEditRelatedModels = (type) => {\n // 填入该渠道类型的所有模型\n selectedEditModels.value = [...getAvailableModels(type)]\n ElMessage.success('已填入相关模型')\n}\n\nconst resetCreateForm = () => {\n createForm.type = 1\n createForm.name = ''\n createForm.display_name = ''\n createForm.base_url = 'https://api.openai.com/v1'\n createForm.api_key = ''\n createForm.test_model = 'gpt-3.5-turbo'\n createForm.priority = 1\n createForm.weight = 100\n createForm.status = 1\n createForm.models = ''\n createForm.group = 'default'\n selectedModels.value = []\n showCreateDialog.value = false\n showManualModelInput.value = false\n manualModel.value = ''\n}\n\nconst loadChannels = async () => {\n loading.value = true\n try {\n const res = await api.admin.getChannels({ page: 1, page_size: 100 })\n // 新响应格式: { code, message, data: { items: [...], total: N } }\n channels.value = res.data.data?.items || []\n } catch (error) {\n console.error('Failed to load channels:', error)\n ElMessage.error('加载渠道失败')\n } finally {\n loading.value = false\n }\n}\n\nconst refreshChannels = () => {\n loadChannels()\n}\n\nconst handleCreate = async () => {\n try {\n // 将选中的模型转换为JSON字符串\n createForm.models = JSON.stringify(selectedModels.value)\n \n const data = {\n ...createForm\n }\n await api.admin.createChannel(data)\n ElMessage.success('创建成功')\n resetCreateForm()\n loadChannels()\n } catch (error) {\n console.error('Failed to create channel:', error)\n ElMessage.error('创建失败')\n }\n}\n\nconst editChannel = (channel) => {\n editForm.id = channel.id\n editForm.type = channel.type\n editForm.name = channel.name\n editForm.display_name = channel.display_name\n editForm.base_url = channel.base_url\n editForm.api_key = channel.api_key\n editForm.test_model = channel.test_model\n editForm.priority = channel.priority\n editForm.weight = channel.weight\n editForm.status = channel.status\n editForm.models = channel.models\n editForm.group = channel.group || 'default'\n \n // 解析模型列表\n try {\n selectedEditModels.value = JSON.parse(channel.models || '[]')\n } catch (e) {\n selectedEditModels.value = []\n }\n \n showEditDialog.value = true\n showEditManualModelInput.value = false\n editManualModel.value = ''\n}\n\nconst handleEdit = async () => {\n try {\n // 将选中的模型转换为JSON字符串\n editForm.models = JSON.stringify(selectedEditModels.value)\n \n const data = {\n ...editForm\n }\n await api.admin.updateChannel(editForm.id, data)\n ElMessage.success('更新成功')\n showEditDialog.value = false\n loadChannels()\n } catch (error) {\n console.error('Failed to update channel:', error)\n ElMessage.error('更新失败')\n }\n}\n\nconst testChannel = async (id) => {\n testingChannels.value.push(id)\n try {\n const response = await api.admin.testChannel(id)\n ElMessage.success(`测试通过,响应时间:${response.data.response_time || 0}ms`)\n } catch (error) {\n ElMessage.error('测试失败:' + (error.response?.data?.error || '未知错误'))\n } finally {\n testingChannels.value = testingChannels.value.filter(item => item !== id)\n }\n}\n\nconst batchTestChannels = async () => {\n batchTesting.value = true\n try {\n const response = await api.admin.batchTestChannels()\n ElMessage.success('批量测试完成')\n loadChannels()\n } catch (error) {\n ElMessage.error('批量测试失败')\n } finally {\n batchTesting.value = false\n }\n}\n\nconst deleteChannel = async (id) => {\n if (!confirm('确定要删除此渠道吗?')) return\n try {\n await api.admin.deleteChannel(id)\n ElMessage.success('删除成功')\n loadChannels()\n } catch (error) {\n console.error('Failed to delete channel:', error)\n ElMessage.error('删除失败')\n }\n}\n\nonMounted(() => {\n loadChannels()\n // 初始化过滤后的模型列表\n updateFilteredModels()\n updateFilteredEditModels()\n})\n</script>\n\n<style scoped lang=\"scss\">\n.channels-page {\n padding: 20px;\n .card-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n \n .header-actions {\n display: flex;\n gap: 10px;\n }\n }\n \n .el-table {\n margin-top: 20px;\n \n .el-table__row {\n transition: all 0.3s ease;\n \n &:hover {\n background-color: #f5f7fa;\n }\n }\n }\n \n .model-management {\n display: flex;\n flex-direction: column;\n gap: 10px;\n \n .model-search {\n margin-bottom: 5px;\n \n .el-input {\n width: 100%;\n }\n }\n \n .model-tags {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n padding: 10px;\n background-color: #f5f7fa;\n border-radius: 4px;\n min-height: 80px;\n \n .el-tag {\n cursor: pointer;\n transition: all 0.3s ease;\n \n &:hover {\n transform: translateY(-2px);\n }\n }\n \n .selected-tag {\n font-weight: bold;\n }\n }\n \n .model-actions {\n display: flex;\n gap: 8px;\n flex-wrap: wrap;\n padding: 5px 0;\n }\n \n .manual-model-input {\n display: flex;\n gap: 10px;\n margin-top: 5px;\n padding: 10px;\n background-color: #f0f2f5;\n border-radius: 4px;\n \n .el-input {\n flex: 1;\n }\n }\n }\n}\n</style>\n"}}}}