252 lines
12 KiB
Vue
252 lines
12 KiB
Vue
<template>
|
||
<div class="page-container">
|
||
<div class="page-header">
|
||
<span class="page-title">AI模型配置</span>
|
||
<el-button type="primary" size="small" @click="openCreate">
|
||
<el-icon><Plus /></el-icon> 添加模型
|
||
</el-button>
|
||
</div>
|
||
|
||
<div class="models-grid">
|
||
<div v-for="m in models" :key="m.id" class="model-card" :class="{ 'is-default': m.is_default }">
|
||
<div class="model-head">
|
||
<div class="model-name">
|
||
<span class="provider-badge" :class="m.provider">{{ providerLabels[m.provider] || m.provider }}</span>
|
||
<span class="model-title">{{ m.model_name }}</span>
|
||
</div>
|
||
<div style="display:flex;gap:6px;align-items:center">
|
||
<el-tag :type="m.usage_scope === 'digital_avatar' ? 'warning' : 'info'" size="small">
|
||
{{ scopeLabels[m.usage_scope] || '通用业务' }}
|
||
</el-tag>
|
||
<el-tag v-if="m.is_default" type="success" size="small">默认</el-tag>
|
||
<el-tag v-if="!m.is_enabled" type="danger" size="small">禁用</el-tag>
|
||
</div>
|
||
</div>
|
||
<div class="model-meta">
|
||
<span>版本: {{ m.model_version || '--' }}</span>
|
||
<span>温度: {{ m.temperature }}</span>
|
||
<span>Max Tokens: {{ m.max_tokens }}</span>
|
||
<span>超时: {{ m.timeout_seconds }}s</span>
|
||
<span>API Key: {{ m.has_api_key ? '✓ 已配置' : '✗ 未配置' }}</span>
|
||
</div>
|
||
<div class="model-actions">
|
||
<el-button size="small" @click="openTest(m)">测试</el-button>
|
||
<el-button size="small" @click="openEdit(m)">编辑</el-button>
|
||
<el-button v-if="!m.is_default" size="small" type="primary" @click="setDefault(m)">设为默认</el-button>
|
||
<el-button size="small" type="danger" @click="doDelete(m)">删除</el-button>
|
||
</div>
|
||
</div>
|
||
<div v-if="!models.length" class="empty-state">
|
||
<el-empty description="暂无模型配置,请添加AI模型" />
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Create/Edit Dialog -->
|
||
<el-dialog v-model="dialogVisible" :title="editModel ? '编辑模型' : '添加AI模型'" width="540px">
|
||
<el-form :model="form" label-width="110px" :rules="rules" ref="formRef">
|
||
<el-form-item label="模型名称" prop="model_name">
|
||
<el-input v-model="form.model_name" placeholder="如: GPT-4 生产环境" />
|
||
</el-form-item>
|
||
<el-form-item label="提供商" prop="provider">
|
||
<el-select v-model="form.provider" style="width:100%" @change="onProviderChange">
|
||
<el-option v-for="(l,v) in providerLabels" :key="v" :label="l" :value="v" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="使用场景" prop="usage_scope">
|
||
<el-radio-group v-model="form.usage_scope">
|
||
<el-radio-button value="general">通用业务</el-radio-button>
|
||
<el-radio-button value="digital_avatar">数字分身专用</el-radio-button>
|
||
</el-radio-group>
|
||
<div class="scope-tip">数字分身专用模型仅用于分身对话和主动接管回复</div>
|
||
</el-form-item>
|
||
<el-form-item label="API地址">
|
||
<el-input v-model="form.api_base_url" placeholder="留空使用默认地址" />
|
||
</el-form-item>
|
||
<el-form-item label="API Key">
|
||
<el-input v-model="form.api_key" type="password" show-password :placeholder="editModel ? '不修改请留空' : '输入API Key'" />
|
||
</el-form-item>
|
||
<el-form-item label="模型版本">
|
||
<el-input v-model="form.model_version" placeholder="如: gpt-4-turbo, glm-4" />
|
||
</el-form-item>
|
||
<el-row :gutter="16">
|
||
<el-col :span="12">
|
||
<el-form-item label="温度">
|
||
<el-input-number v-model="form.temperature" :min="0" :max="2" :step="0.1" style="width:100%" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="Max Tokens">
|
||
<el-input-number v-model="form.max_tokens" :min="100" :max="32000" :step="100" style="width:100%" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-form-item label="超时(秒)">
|
||
<el-input-number v-model="form.timeout_seconds" :min="5" :max="300" style="width:160px" />
|
||
</el-form-item>
|
||
<el-form-item label="设为默认">
|
||
<el-switch v-model="form.is_default" :active-value="1" :inactive-value="0" />
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="dialogVisible = false">取消</el-button>
|
||
<el-button type="primary" @click="submit" :loading="submitting">保存</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- Test Dialog -->
|
||
<el-dialog v-model="testVisible" title="模型测试" width="560px">
|
||
<el-form label-width="90px">
|
||
<el-form-item label="测试指令">
|
||
<el-input v-model="testPrompt" type="textarea" :rows="3" />
|
||
</el-form-item>
|
||
</el-form>
|
||
<div v-if="testResult" class="test-result">
|
||
<div class="result-meta">
|
||
<el-tag :type="testResult.success ? 'success' : 'danger'">
|
||
{{ testResult.success ? '成功' : '失败' }}
|
||
</el-tag>
|
||
<span v-if="testResult.success" style="font-size:12px;color:var(--color-text-muted)">
|
||
耗时 {{ testResult.elapsed_seconds }}s · 消耗 {{ testResult.tokens }} tokens
|
||
</span>
|
||
</div>
|
||
<div class="result-content" style="white-space:pre-wrap;word-break:break-all;">
|
||
{{ testResult.success ? (testResult.content || '(响应为空)') : testResult.error }}
|
||
</div>
|
||
<div v-if="testResult.success" style="margin-top:8px;font-size:12px;color:var(--color-text-muted)">
|
||
耗时 {{ testResult.elapsed_seconds }}s · Token {{ testResult.tokens }}
|
||
</div>
|
||
</div>
|
||
<template #footer>
|
||
<el-button @click="testVisible = false">关闭</el-button>
|
||
<el-button type="primary" @click="runTest" :loading="testing">发送测试</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, reactive, onMounted } from 'vue'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import { getAIModels, createAIModel, updateAIModel, deleteAIModel, testAIModel } from '@/api'
|
||
|
||
const models = ref([])
|
||
const dialogVisible = ref(false)
|
||
const editModel = ref(null)
|
||
const submitting = ref(false)
|
||
const formRef = ref()
|
||
const testVisible = ref(false)
|
||
const testingModel = ref(null)
|
||
const testPrompt = ref('你好,请简单介绍一下你自己,并写一条关于"人工智能改变新闻业"的新闻评论。')
|
||
const testResult = ref(null)
|
||
const testing = ref(false)
|
||
|
||
const providerLabels = { openai: 'OpenAI', zhipu: '智谱GLM', wenxin: '文心一言', qianwen: '通义千问', local: '本地模型' }
|
||
const scopeLabels = { general: '通用业务', digital_avatar: '数字分身专用' }
|
||
const form = reactive({ model_name: '', provider: 'openai', usage_scope: 'general', api_base_url: '', api_key: '', model_version: '', temperature: 0.7, max_tokens: 1000, timeout_seconds: 30, is_default: 0 })
|
||
const rules = { model_name: [{ required: true, message: '请输入模型名称' }], provider: [{ required: true }], usage_scope: [{ required: true }] }
|
||
|
||
async function load() {
|
||
const res = await getAIModels()
|
||
models.value = res.data || []
|
||
}
|
||
|
||
const PROVIDER_DEFAULTS = {
|
||
openai: { api_base_url: 'https://api.openai.com/v1', model_version: 'gpt-4-turbo' },
|
||
zhipu: { api_base_url: 'https://open.bigmodel.cn/api/paas/v4', model_version: 'glm-4' },
|
||
wenxin: { api_base_url: 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat', model_version: 'ERNIE-Bot-4' },
|
||
qianwen: { api_base_url: 'https://dashscope.aliyuncs.com/compatible-mode/v1', model_version: 'qwen-turbo' },
|
||
local: { api_base_url: 'http://127.0.0.1:11434/v1', model_version: 'qwen2' },
|
||
}
|
||
|
||
function onProviderChange(provider) {
|
||
const def = PROVIDER_DEFAULTS[provider] || {}
|
||
form.api_base_url = def.api_base_url || ''
|
||
form.model_version = def.model_version || ''
|
||
}
|
||
|
||
function openCreate() {
|
||
editModel.value = null
|
||
Object.assign(form, { model_name: '', provider: 'openai', usage_scope: 'general', api_base_url: PROVIDER_DEFAULTS.openai.api_base_url, api_key: '', model_version: PROVIDER_DEFAULTS.openai.model_version, temperature: 0.7, max_tokens: 1000, timeout_seconds: 30, is_default: 0 })
|
||
dialogVisible.value = true
|
||
}
|
||
|
||
function openEdit(m) {
|
||
editModel.value = m
|
||
Object.assign(form, { model_name: m.model_name, provider: m.provider, usage_scope: m.usage_scope || 'general', api_base_url: m.api_base_url || '', api_key: '', model_version: m.model_version || '', temperature: m.temperature, max_tokens: m.max_tokens, timeout_seconds: m.timeout_seconds, is_default: m.is_default })
|
||
dialogVisible.value = true
|
||
}
|
||
|
||
async function submit() {
|
||
await formRef.value.validate()
|
||
submitting.value = true
|
||
try {
|
||
const data = { ...form }
|
||
if (editModel.value) {
|
||
if (!data.api_key) delete data.api_key
|
||
await updateAIModel(editModel.value.id, data)
|
||
} else {
|
||
await createAIModel(data)
|
||
}
|
||
ElMessage.success('保存成功')
|
||
dialogVisible.value = false
|
||
load()
|
||
} finally { submitting.value = false }
|
||
}
|
||
|
||
async function doDelete(m) {
|
||
await ElMessageBox.confirm(`确认删除模型 "${m.model_name}"?`, '确认', { type: 'warning' })
|
||
await deleteAIModel(m.id)
|
||
ElMessage.success('删除成功')
|
||
load()
|
||
}
|
||
|
||
async function setDefault(m) {
|
||
await updateAIModel(m.id, { is_default: 1 })
|
||
ElMessage.success(`"${m.model_name}" 已设为默认模型`)
|
||
load()
|
||
}
|
||
|
||
function openTest(m) {
|
||
testingModel.value = m
|
||
testResult.value = null
|
||
testVisible.value = true
|
||
}
|
||
|
||
async function runTest() {
|
||
testing.value = true
|
||
testResult.value = null
|
||
try {
|
||
const res = await testAIModel({ model_id: testingModel.value.id, test_prompt: testPrompt.value })
|
||
testResult.value = res.data
|
||
} finally { testing.value = false }
|
||
}
|
||
|
||
onMounted(load)
|
||
</script>
|
||
|
||
<style scoped>
|
||
.models-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); gap: 16px; }
|
||
.model-card {
|
||
background: var(--color-bg-card); border: 1px solid var(--color-border);
|
||
border-radius: 12px; padding: 18px;
|
||
transition: border-color 0.2s, box-shadow 0.2s;
|
||
}
|
||
.model-card.is-default { border-color: var(--color-accent-green); box-shadow: 0 0 12px rgba(63,185,80,0.15); }
|
||
.model-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
|
||
.model-name { display: flex; align-items: center; gap: 8px; }
|
||
.provider-badge { font-size: 11px; padding: 2px 8px; border-radius: 12px; font-weight: 600; }
|
||
.provider-badge.openai { background: rgba(16,163,127,0.15); color: #10a37f; }
|
||
.provider-badge.zhipu { background: rgba(88,166,255,0.15); color: var(--color-accent); }
|
||
.provider-badge.wenxin { background: rgba(240,136,62,0.15); color: var(--color-accent-orange); }
|
||
.provider-badge.qianwen { background: rgba(210,153,34,0.15); color: var(--color-accent-yellow); }
|
||
.provider-badge.local { background: rgba(188,140,255,0.15); color: var(--color-accent-purple); }
|
||
.model-title { font-size: 15px; font-weight: 600; }
|
||
.model-meta { display: flex; flex-wrap: wrap; gap: 10px; font-size: 12px; color: var(--color-text-muted); margin-bottom: 14px; }
|
||
.model-actions { display: flex; gap: 6px; flex-wrap: wrap; }
|
||
.test-result { margin-top: 16px; }
|
||
.result-meta { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
|
||
.result-content { background: var(--color-bg); border: 1px solid var(--color-border); border-radius: 8px; padding: 12px; font-size: 13px; line-height: 1.6; white-space: pre-wrap; max-height: 200px; overflow-y: auto; }
|
||
.empty-state { grid-column: 1/-1; padding: 40px; }
|
||
.scope-tip { margin-top: 6px; color: var(--color-text-muted); font-size: 12px; line-height: 1.5; }
|
||
</style>
|