/** * ⚙️ AI 模型配置管理组件 * 展示模型配置列表、新增/编辑/激活/删除配置 */ import { ref, onMounted, watch } from 'vue' import * as api from '../js/api.js' import { toast, formatDate } from '../js/utils.js' // 应用类型选项 const APP_TYPE_OPTIONS = [ { value: '', label: '全部类型' }, { value: 'CHAT', label: '智能客服对话' }, { value: 'EMBEDDING', label: '文本向量化' }, { value: 'RAG_REWRITE', label: 'RAG查询重写' } ] // 提供商选项 const PROVIDER_OPTIONS = [ { value: 'dashscope', label: '通义千问 (DashScope)' }, { value: 'deepseek', label: 'DeepSeek (深度求索)' }, { value: 'volcengine', label: '豆包 (字节跳动)' }, { value: 'moonshot', label: 'Kimi (月之暗面)' }, { value: 'zhipu', label: '智谱 AI (GLM)' }, { value: 'openai', label: 'OpenAI' }, { value: 'other', label: '其他' } ] // 提供商默认配置(切换时自动填充) // chatModels / embeddingModels 按 app_type 分别展示,models 作为向后兼容兜底 const PROVIDER_DEFAULTS = { dashscope: { baseUrl: '', chatModels: ['qwen-turbo', 'qwen-plus', 'qwen-max', 'qwen-long'], embeddingModels: ['text-embedding-v2'], defaultChatModel: 'qwen-turbo', defaultEmbeddingModel: 'text-embedding-v2', tip: '通义千问使用 DashScope 自动配置,Base URL 留空即可' }, deepseek: { baseUrl: 'https://api.deepseek.com', chatModels: ['deepseek-chat', 'deepseek-reasoner'], embeddingModels: [], defaultChatModel: 'deepseek-chat', defaultEmbeddingModel: '', tip: '' }, volcengine: { baseUrl: 'https://ark.cn-beijing.volces.com/api/v3', chatModels: ['doubao-1-5-pro-32k', 'doubao-1-5-lite-32k', 'doubao-pro-32k', 'doubao-1-5-thinking-pro', 'doubao-seed-1.6'], embeddingModels: ['doubao-embedding-text-240515', 'doubao-embedding-large', 'doubao-embedding-vision-251215'], defaultChatModel: 'doubao-1-5-pro-32k', defaultEmbeddingModel: 'doubao-embedding-text-240515', tip: '豆包对话推荐 doubao-1-5-pro-32k;向量化推荐 doubao-embedding-text-240515(2048维)。模型名称可在火山引擎 ARK 控制台获取' }, moonshot: { baseUrl: 'https://api.moonshot.cn/v1', chatModels: ['moonshot-v1-8k', 'moonshot-v1-32k', 'moonshot-v1-128k'], embeddingModels: [], defaultChatModel: 'moonshot-v1-8k', defaultEmbeddingModel: '', tip: '' }, zhipu: { baseUrl: 'https://open.bigmodel.cn/api/paas/v4', chatModels: ['glm-4-plus', 'glm-4-flash', 'glm-4-long', 'glm-4'], embeddingModels: [], defaultChatModel: 'glm-4-flash', defaultEmbeddingModel: '', tip: '' }, openai: { baseUrl: 'https://api.openai.com', chatModels: ['gpt-4o', 'gpt-4o-mini', 'gpt-3.5-turbo'], embeddingModels: ['text-embedding-3-small', 'text-embedding-3-large', 'text-embedding-ada-002'], defaultChatModel: 'gpt-4o-mini', defaultEmbeddingModel: 'text-embedding-3-small', tip: '' }, other: { baseUrl: '', chatModels: [], embeddingModels: [], defaultChatModel: '', defaultEmbeddingModel: '', tip: '使用 OpenAI 兼容 API 的其他提供商,请填写 Base URL 和模型名称' } } export default { template: `

⚙️ AI 大模型配置管理

配置名称 应用类型 模型名称 提供商 温度 API Key 状态 操作
暂无配置记录
{{ c.name || '-' }}
{{ c.description }}
{{ getAppTypeLabel(c.app_type) }} {{ c.model_name }} {{ getProviderLabel(c.provider) }} {{ c.temperature != null ? c.temperature : '-' }} {{ c.api_key || '-' }} 🟢 活跃 ⚫ 未激活
`, setup() { const configs = ref([]) const currentPage = ref(1) const totalPages = ref(1) const total = ref(0) const filterAppType = ref('') const showApiKey = ref(false) const customModelName = ref('') const providerTip = ref('') // 计算当前提供商的模型列表和默认值 const currentProviderModels = ref([]) const currentProviderDefaultModel = ref('') const providerBaseUrlPlaceholder = ref('') // 编辑弹窗状态 const editModal = ref({ visible: false, mode: 'add', editId: null, form: createEmptyForm() }) function createEmptyForm() { return { name: '', app_type: 'CHAT', provider: 'dashscope', api_key: '', model_name: '', temperature: 0.7, max_tokens: 2000, base_url: '', embeddingDimensions: 1024, priority: 0, is_active: false, description: '' } } // 提供商切换时自动填充(按 app_type 区分对话/向量模型) function onProviderChange() { const provider = editModal.value.form.provider const defaults = PROVIDER_DEFAULTS[provider] || PROVIDER_DEFAULTS.other const isEmbedding = editModal.value.form.app_type === 'EMBEDDING' // 自动填充 Base URL editModal.value.form.base_url = defaults.baseUrl // 按 app_type 选择模型列表和默认模型 const models = isEmbedding ? (defaults.embeddingModels || []) : (defaults.chatModels || []) const defaultModel = isEmbedding ? (defaults.defaultEmbeddingModel || '') : (defaults.defaultChatModel || '') // 仅新建模式且模型名为空时自动填充默认模型 if (editModal.value.mode === 'add' && !editModal.value.form.model_name) { editModal.value.form.model_name = defaultModel } currentProviderModels.value = models currentProviderDefaultModel.value = defaultModel providerTip.value = defaults.tip || '' providerBaseUrlPlaceholder.value = defaults.baseUrl || '私有化部署时可填写自定义地址' customModelName.value = '' } // 监听 app_type 变化,刷新模型列表 watch(() => editModal.value.form.app_type, () => { if (editModal.value.visible) { onProviderChange() } }) // ==================== 数据加载 ==================== async function load(p = 1) { currentPage.value = p try { const json = await api.listModelConfigs(p, 10, filterAppType.value || undefined) if (json.success) { configs.value = json.data || [] total.value = json.total || 0 totalPages.value = json.pages || 1 } else { toast(json.message || '查询失败', 'error') } } catch (e) { toast('加载配置列表失败:' + e.message, 'error') } } // ==================== 弹窗操作 ==================== function openAddModal() { editModal.value = { visible: true, mode: 'add', editId: null, form: createEmptyForm() } showApiKey.value = false onProviderChange() } function openEditModal(config) { // 从 extraConfig 中读取 embeddingDimensions let extraConfig = {} if (config.extraConfig) { try { extraConfig = typeof config.extraConfig === 'string' ? JSON.parse(config.extraConfig) : config.extraConfig } catch (e) {} } editModal.value = { visible: true, mode: 'edit', editId: config.id, form: { name: config.name || '', app_type: config.app_type || 'CHAT', provider: config.provider || 'dashscope', api_key: '', model_name: config.model_name || '', temperature: config.temperature, max_tokens: config.max_tokens, base_url: config.base_url || '', embeddingDimensions: extraConfig.dimensions || 1024, priority: config.priority || 0, is_active: config.is_active || false, description: config.description || '' } } showApiKey.value = false onProviderChange() } function closeEditModal() { editModal.value.visible = false } // ==================== 保存配置 ==================== /** * 将前端表单字段名(snake_case)转换为后端 Java 实体字段名(camelCase) * 因为 Jackson 默认使用 camelCase 反序列化,前端发送 snake_case 会导致字段无法映射 */ function toCamelCase(form) { const data = { name: form.name, appType: form.app_type, provider: form.provider, apiKey: form.api_key, modelName: form.model_name, temperature: form.temperature, maxTokens: form.max_tokens, baseUrl: form.base_url, priority: form.priority, isActive: form.is_active, description: form.description } // EMBEDDING 类型:将向量维度写入 extraConfig if (form.app_type === 'EMBEDDING') { data.extraConfig = { dimensions: form.embeddingDimensions || 1024 } } return data } async function saveConfig() { const form = editModal.value.form // 如果选了"自定义输入",取自定义名称 if (form.model_name === '__custom__') { if (!customModelName.value.trim()) { toast('请输入自定义模型名称', 'error') return } form.model_name = customModelName.value.trim() } if (!form.name || !form.name.trim()) { toast('请填写配置名称', 'error') return } if (!form.app_type) { toast('请选择应用类型', 'error') return } if (!form.model_name || !form.model_name.trim()) { toast('请填写模型名称', 'error') return } if (editModal.value.mode === 'add' && (!form.api_key || !form.api_key.trim())) { toast('请填写 API Key', 'error') return } try { let json // 转换为 camelCase 后再发送,确保 Jackson 正确反序列化 const camelData = toCamelCase(form) if (editModal.value.mode === 'add') { json = await api.createModelConfig(camelData) } else { // 编辑模式:API Key 留空则不发送(不覆盖原值) if (!camelData.apiKey || !camelData.apiKey.trim()) { delete camelData.apiKey } json = await api.updateModelConfig(editModal.value.editId, camelData) } if (json.success) { toast(editModal.value.mode === 'add' ? '配置创建成功,已切换生效' : '配置更新成功,已切换生效', 'success') closeEditModal() load(currentPage.value) } else { toast(json.message || '操作失败', 'error') } } catch (e) { toast('保存失败:' + e.message, 'error') } } // ==================== 激活配置 ==================== async function activate(id) { if (!confirm('确定激活此配置?同类型的其他配置将被自动停用,新配置将立即生效。')) return try { const json = await api.activateModelConfig(id) if (json.success) { toast('配置已激活,立即生效', 'success') load(currentPage.value) } else { toast(json.message || '激活失败', 'error') } } catch (e) { toast('激活失败:' + e.message, 'error') } } // ==================== 删除配置 ==================== async function remove(id, name) { if (!confirm('确定删除配置「' + (name || id) + '」?')) return try { const json = await api.deleteModelConfig(id) if (json.success) { toast('配置删除成功', 'success') load(currentPage.value) } else { toast(json.message || '删除失败', 'error') } } catch (e) { toast('删除失败:' + e.message, 'error') } } // ==================== 工具函数 ==================== function getAppTypeLabel(appType) { const map = { CHAT: '智能客服对话', EMBEDDING: '文本向量化', RAG_REWRITE: 'RAG查询重写' } return map[appType] || appType } function getAppTypeBadgeClass(appType) { const map = { CHAT: 'badge-get', EMBEDDING: '', RAG_REWRITE: '' } return map[appType] || '' } function getProviderLabel(provider) { const found = PROVIDER_OPTIONS.find(o => o.value === provider) return found ? found.label : provider } // 初始加载 onMounted(() => { load() }) return { configs, currentPage, totalPages, total, filterAppType, showApiKey, editModal, appTypeOptions: APP_TYPE_OPTIONS, providerOptions: PROVIDER_OPTIONS, providerTip, currentProviderModels, currentProviderDefaultModel, providerBaseUrlPlaceholder, customModelName, load, openAddModal, openEditModal, closeEditModal, saveConfig, activate, remove, onProviderChange, getAppTypeLabel, getAppTypeBadgeClass, getProviderLabel, formatDate } } }