1 line
112 KiB
JSON
1 line
112 KiB
JSON
{"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"}}}} |