/**
* ⚙️ 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 || '-' }} |
🟢 活跃
⚫ 未激活
|
|
{{ editModal.mode === 'add' ? '➕ 新建模型配置' : '✏️ 编辑模型配置' }}
`,
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
}
}
}