| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523 |
- <template>
- <div class="app-container">
- <el-card>
- <div slot="header" class="card-header">
- <span>AI模型配置管理</span>
- <div>
- <el-button type="primary" size="small" @click="handleAddModel" v-if="activeTab === 'models'">
- <i class="el-icon-plus"></i>新增模型
- </el-button>
- <el-button type="success" size="small" @click="handleRefresh">
- <i class="el-icon-refresh"></i>刷新缓存
- </el-button>
- </div>
- </div>
- <el-alert
- title="统一模型配置说明"
- description="所有文本AI模型统一在此管理,按sort_order排序优先级(越小越优先)。每个场景可独立配置使用哪些模型及流水线顺序。FastGPT、TTS语音、豆包视觉及图像/语音相关模型不在此处管理。"
- type="info" show-icon :closable="false" style="margin-bottom: 20px"
- />
- <el-tabs v-model="activeTab" @tab-click="handleTabClick">
- <!-- ===== 模型列表 Tab ===== -->
- <el-tab-pane label="模型列表" name="models">
- <el-table :data="modelList" v-loading="modelLoading" border size="small" row-key="id"
- style="width:100%">
- <el-table-column label="排序" width="70" align="center">
- <template slot-scope="{ row, $index }">
- <span class="sort-handle" style="cursor: move; font-size: 18px;">☰</span>
- <span style="margin-left: 4px;">{{ $index + 1 }}</span>
- </template>
- </el-table-column>
- <el-table-column prop="modelName" label="模型名称" min-width="120" />
- <el-table-column prop="providerCode" label="供应商" width="90" />
- <el-table-column prop="modelIdentifier" label="模型标识" min-width="160" show-overflow-tooltip />
- <el-table-column prop="apiEndpoint" label="API地址" min-width="200" show-overflow-tooltip />
- <el-table-column prop="maxTokens" label="MaxToken" width="90" align="center" />
- <el-table-column prop="temperature" label="温度" width="70" align="center" />
- <el-table-column prop="sortOrder" label="排序号" width="70" align="center" />
- <el-table-column prop="status" label="状态" width="70" align="center">
- <template slot-scope="{ row }">
- <el-tag :type="row.status === 1 ? 'success' : 'danger'" size="small">
- {{ row.status === 1 ? '启用' : '禁用' }}
- </el-tag>
- </template>
- </el-table-column>
- <el-table-column label="操作" width="280" fixed="right">
- <template slot-scope="{ row }">
- <el-button type="primary" size="mini" @click="handleEditModel(row)">编辑</el-button>
- <el-button type="success" size="mini" @click="handleTestModel(row)">测试</el-button>
- <el-popconfirm title="确定删除该模型吗?" @confirm="handleDeleteModel(row)">
- <el-button slot="reference" type="danger" size="mini">删除</el-button>
- </el-popconfirm>
- </template>
- </el-table-column>
- </el-table>
- <div style="margin-top: 12px; text-align: right;">
- <el-button type="warning" size="small" @click="handleSaveSort" :disabled="!sortChanged">
- <i class="el-icon-sort"></i>保存排序
- </el-button>
- </div>
- </el-tab-pane>
- <!-- ===== 场景配置 Tab ===== -->
- <el-tab-pane label="场景配置" name="scenes">
- <el-row :gutter="16">
- <!-- 左侧:场景列表 -->
- <el-col :span="8">
- <el-card shadow="never" class="scene-list-card">
- <div slot="header" class="sub-header">使用场景</div>
- <div v-loading="sceneLoading">
- <div v-for="scene in sceneList" :key="scene.sceneCode"
- class="scene-item"
- :class="{ active: selectedScene === scene.sceneCode }"
- @click="selectScene(scene)">
- <div class="scene-name">
- {{ scene.sceneName }}
- <el-tag v-if="scene.sceneType === 'multi_pipeline'" size="mini" type="warning" style="margin-left: 6px;">多模型</el-tag>
- </div>
- <div class="scene-code">{{ scene.sceneCode }}</div>
- </div>
- </div>
- </el-card>
- </el-col>
- <!-- 右侧:场景详情 -->
- <el-col :span="16">
- <el-card shadow="never" v-if="selectedSceneInfo">
- <div slot="header" class="sub-header">
- {{ selectedSceneInfo.sceneName }} - 模型配置
- <el-tag size="small" :type="selectedSceneInfo.status === 1 ? 'success' : 'danger'" style="margin-left: 8px;">
- {{ selectedSceneInfo.status === 1 ? '启用' : '禁用' }}
- </el-tag>
- </div>
- <!-- 场景基础信息 -->
- <el-form :model="sceneForm" label-width="100px" size="small" inline>
- <el-form-item label="场景编码">{{ selectedSceneInfo.sceneCode }}</el-form-item>
- <el-form-item label="流水线类型" v-if="selectedSceneInfo.sceneType === 'multi_pipeline'">
- {{ selectedSceneInfo.pipelineType === 'scoring' ? '质量评分链' : selectedSceneInfo.pipelineType === 'sequential' ? '顺序调用' : '-' }}
- </el-form-item>
- <el-form-item label="质量阈值" v-if="selectedSceneInfo.pipelineType === 'scoring'">
- <el-input-number v-model="sceneForm.qualityThreshold" :min="60" :max="160" :step="10" size="mini" style="width: 100px;" />
- <el-button type="primary" size="mini" @click="saveThreshold" style="margin-left: 8px;">保存</el-button>
- </el-form-item>
- </el-form>
- <el-divider />
- <!-- 场景关联模型列表 -->
- <div style="margin-bottom: 10px; display: flex; justify-content: space-between; align-items: center;">
- <span>关联模型流水线(按 pipeline_order 排序)</span>
- <el-button type="primary" size="mini" @click="handleBindModel">
- <i class="el-icon-plus"></i>绑定模型
- </el-button>
- </div>
- <el-table :data="sceneModelList" v-loading="sceneModelLoading" border size="small"
- row-key="id" style="width:100%">
- <el-table-column label="顺序" width="60" align="center">
- <template slot-scope="{ $index }">
- <span class="sort-handle" style="cursor: move;">☰</span>
- <span style="margin-left: 4px;">{{ $index + 1 }}</span>
- </template>
- </el-table-column>
- <el-table-column label="模型名称" min-width="120">
- <template slot-scope="{ row }">
- {{ row.model ? row.model.modelName : '模型#' + row.modelId }}
- </template>
- </el-table-column>
- <el-table-column label="供应商" width="80">
- <template slot-scope="{ row }">
- {{ row.model ? row.model.providerCode : '-' }}
- </template>
- </el-table-column>
- <el-table-column label="流水线角色" width="110" align="center">
- <template slot-scope="{ row }">
- <el-tag size="mini" :type="row.role === 'scorer' ? 'warning' : 'success'">
- {{ row.role === 'scorer' ? '评分者' : '生成者' }}
- </el-tag>
- </template>
- </el-table-column>
- <el-table-column prop="pipelineOrder" label="顺序号" width="70" align="center" />
- <el-table-column prop="sortWeight" label="权重" width="60" align="center" />
- <el-table-column label="操作" width="120" fixed="right">
- <template slot-scope="{ row }">
- <el-button type="text" size="mini" @click="handleEditSceneModel(row)">编辑</el-button>
- <el-popconfirm title="确定移除此关联?" @confirm="handleRemoveSceneModel(row)">
- <el-button slot="reference" type="text" size="mini" style="color: #F56C6C;">移除</el-button>
- </el-popconfirm>
- </template>
- </el-table-column>
- </el-table>
- <div v-if="sceneModelList.length === 0" style="text-align: center; color: #999; padding: 40px 0;">
- 暂未绑定模型,请点击"绑定模型"添加
- </div>
- </el-card>
- <el-empty v-else description="请从左侧选择一个场景" />
- </el-col>
- </el-row>
- </el-tab-pane>
- </el-tabs>
- </el-card>
- <!-- 模型编辑弹窗 -->
- <el-dialog :title="modelDialogTitle" :visible.sync="modelDialogVisible" width="560px" @closed="resetModelForm">
- <el-form :model="modelForm" :rules="modelRules" ref="modelFormRef" label-width="120px">
- <el-form-item label="模型名称" prop="modelName">
- <el-input v-model="modelForm.modelName" placeholder="如:豆包Pro 32K" />
- </el-form-item>
- <el-form-item label="供应商" prop="providerCode">
- <el-select v-model="modelForm.providerCode" placeholder="请选择" style="width: 100%">
- <el-option label="豆包(Doubao)" value="doubao" />
- <el-option label="通义千问(Qwen)" value="qwen" />
- <el-option label="元宝(Yuanbao)" value="yuanbao" />
- <el-option label="DeepSeek" value="deepseek" />
- </el-select>
- </el-form-item>
- <el-form-item label="模型标识" prop="modelIdentifier">
- <el-input v-model="modelForm.modelIdentifier" placeholder="如:doubao-1-5-pro-32k" />
- </el-form-item>
- <el-form-item label="API地址" prop="apiEndpoint">
- <el-input v-model="modelForm.apiEndpoint" placeholder="API Endpoint" />
- </el-form-item>
- <el-form-item label="API Key" prop="apiKey">
- <el-input v-model="modelForm.apiKey" placeholder="API Key" show-password />
- </el-form-item>
- <el-form-item label="最大Token">
- <el-input-number v-model="modelForm.maxTokens" :min="512" :max="131072" :step="512" style="width: 180px;" />
- </el-form-item>
- <el-form-item label="温度参数">
- <el-slider v-model="modelForm.temperature" :min="0" :max="2" :step="0.1" show-input style="width: 200px;" />
- </el-form-item>
- <el-form-item label="排序号">
- <el-input-number v-model="modelForm.sortOrder" :min="0" :max="9999" style="width: 180px;" />
- <span style="color: #909399; font-size: 12px; margin-left: 8px;">越小越优先</span>
- </el-form-item>
- <el-form-item label="状态">
- <el-switch v-model="modelForm.status" :active-value="1" :inactive-value="0" />
- </el-form-item>
- </el-form>
- <div slot="footer">
- <el-button @click="modelDialogVisible = false">取消</el-button>
- <el-button type="primary" @click="handleSubmitModel">确定</el-button>
- </div>
- </el-dialog>
- <!-- 场景绑定模型弹窗 -->
- <el-dialog title="绑定模型到场景" :visible.sync="bindDialogVisible" width="480px">
- <el-form label-width="100px" size="small">
- <el-form-item label="选择模型">
- <el-select v-model="bindForm.modelId" placeholder="请选择模型" style="width: 100%" filterable>
- <el-option v-for="m in modelList" :key="m.id"
- :label="m.modelName + ' (' + m.providerCode + ')'" :value="m.id"
- :disabled="m.status !== 1" />
- </el-select>
- </el-form-item>
- <el-form-item label="流水线角色">
- <el-radio-group v-model="bindForm.role">
- <el-radio label="generator">生成者</el-radio>
- <el-radio label="scorer">评分者</el-radio>
- </el-radio-group>
- </el-form-item>
- <el-form-item label="流水线顺序">
- <el-input-number v-model="bindForm.pipelineOrder" :min="0" :max="99" size="small" />
- </el-form-item>
- <el-form-item label="权重">
- <el-input-number v-model="bindForm.sortWeight" :min="0" :max="99" size="small" />
- </el-form-item>
- </el-form>
- <div slot="footer">
- <el-button @click="bindDialogVisible = false">取消</el-button>
- <el-button type="primary" @click="handleConfirmBind">确定</el-button>
- </div>
- </el-dialog>
- <!-- 场景模型编辑弹窗 -->
- <el-dialog title="编辑场景模型" :visible.sync="editSceneModelDialog" width="420px">
- <el-form label-width="100px" size="small">
- <el-form-item label="流水线角色">
- <el-radio-group v-model="editSceneModelForm.role">
- <el-radio label="generator">生成者</el-radio>
- <el-radio label="scorer">评分者</el-radio>
- </el-radio-group>
- </el-form-item>
- <el-form-item label="流水线顺序">
- <el-input-number v-model="editSceneModelForm.pipelineOrder" :min="0" :max="99" size="small" />
- </el-form-item>
- <el-form-item label="权重">
- <el-input-number v-model="editSceneModelForm.sortWeight" :min="0" :max="99" size="small" />
- </el-form-item>
- </el-form>
- <div slot="footer">
- <el-button @click="editSceneModelDialog = false">取消</el-button>
- <el-button type="primary" @click="handleUpdateSceneModel">确定</el-button>
- </div>
- </el-dialog>
- </div>
- </template>
- <script>
- import {
- listAiModel, getAiModel, addAiModel, updateAiModel, deleteAiModel,
- batchSortAiModel, testAiModel, refreshAiModel,
- listAiScene, updateAiScene, updateSceneThreshold,
- getSceneModels, addSceneModel, deleteSceneModel, updateSceneModel, refreshAiScene
- } from '@/api/admin/aiModel'
- export default {
- name: 'AiModelConfig',
- data() {
- return {
- activeTab: 'models',
- // 模型
- modelLoading: false,
- modelList: [],
- modelDialogVisible: false,
- modelDialogTitle: '',
- modelForm: {
- id: null, modelName: '', providerCode: '', modelIdentifier: '',
- apiEndpoint: '', apiKey: '', maxTokens: 4096, temperature: 0.7,
- sortOrder: 0, status: 1
- },
- modelRules: {
- modelName: [{ required: true, message: '请输入模型名称', trigger: 'blur' }],
- providerCode: [{ required: true, message: '请选择供应商', trigger: 'change' }],
- modelIdentifier: [{ required: true, message: '请输入模型标识', trigger: 'blur' }],
- apiEndpoint: [{ required: true, message: '请输入API地址', trigger: 'blur' }],
- apiKey: [{ required: true, message: '请输入API Key', trigger: 'blur' }]
- },
- sortChanged: false,
- // 场景
- sceneLoading: false,
- sceneList: [],
- selectedScene: '',
- selectedSceneInfo: null,
- sceneForm: { qualityThreshold: 120 },
- sceneModelLoading: false,
- sceneModelList: [],
- bindDialogVisible: false,
- bindForm: { modelId: null, role: 'generator', pipelineOrder: 0, sortWeight: 0 },
- editSceneModelDialog: false,
- editSceneModelForm: { id: null, role: 'generator', pipelineOrder: 0, sortWeight: 0 }
- }
- },
- created() {
- this.loadModels()
- this.loadScenes()
- },
- methods: {
- // ─── 模型 CRUD ───
- async loadModels() {
- this.modelLoading = true
- try {
- const res = await listAiModel()
- this.modelList = res.data || []
- } catch (e) {
- this.$message.error('获取模型列表失败')
- } finally {
- this.modelLoading = false
- }
- },
- handleAddModel() {
- this.modelDialogTitle = '新增模型'
- this.modelForm = {
- id: null, modelName: '', providerCode: '', modelIdentifier: '',
- apiEndpoint: '', apiKey: '', maxTokens: 4096, temperature: 0.7,
- sortOrder: 0, status: 1
- }
- this.modelDialogVisible = true
- this.$nextTick(() => { this.$refs.modelFormRef && this.$refs.modelFormRef.resetFields() })
- },
- handleEditModel(row) {
- this.modelDialogTitle = '编辑模型'
- this.modelForm = { ...row }
- this.modelDialogVisible = true
- },
- async handleSubmitModel() {
- try {
- await this.$refs.modelFormRef.validate()
- if (this.modelForm.id) {
- await updateAiModel(this.modelForm.id, this.modelForm)
- this.$message.success('修改成功')
- } else {
- await addAiModel(this.modelForm)
- this.$message.success('新增成功')
- }
- this.modelDialogVisible = false
- await this.loadModels()
- } catch (e) {
- if (e !== false) this.$message.error('操作失败')
- }
- },
- async handleDeleteModel(row) {
- try {
- await deleteAiModel(row.id)
- this.$message.success('删除成功')
- await this.loadModels()
- } catch (e) {
- this.$message.error('删除失败')
- }
- },
- async handleTestModel(row) {
- try {
- const res = await testAiModel(row.id)
- if (res.code === 200 && res.data) {
- const result = res.data
- if (result.success) {
- this.$message.success('连接成功!响应:' + (result.response || 'OK'))
- } else {
- this.$message.error('连接失败:' + (result.error || '未知错误'))
- }
- } else {
- this.$message.error('测试失败')
- }
- } catch (e) {
- this.$message.error('测试请求失败')
- }
- },
- handleSaveSort() {
- const sortList = this.modelList.map((m, i) => ({ id: m.id, sortOrder: i }))
- batchSortAiModel(sortList).then(() => {
- this.$message.success('排序已保存')
- this.sortChanged = false
- this.loadModels()
- }).catch(() => {
- this.$message.error('保存排序失败')
- })
- },
- resetModelForm() {
- this.modelForm = {
- id: null, modelName: '', providerCode: '', modelIdentifier: '',
- apiEndpoint: '', apiKey: '', maxTokens: 4096, temperature: 0.7,
- sortOrder: 0, status: 1
- }
- },
- // ─── 场景 ───
- async loadScenes() {
- this.sceneLoading = true
- try {
- const res = await listAiScene()
- this.sceneList = res.data || []
- } catch (e) {
- this.$message.error('获取场景列表失败')
- } finally {
- this.sceneLoading = false
- }
- },
- selectScene(scene) {
- this.selectedScene = scene.sceneCode
- this.selectedSceneInfo = scene
- this.sceneForm.qualityThreshold = scene.qualityThreshold || 120
- this.loadSceneModels(scene.sceneCode)
- },
- async loadSceneModels(sceneCode) {
- this.sceneModelLoading = true
- try {
- const res = await getSceneModels(sceneCode)
- this.sceneModelList = res.data || []
- } catch (e) {
- this.$message.error('获取场景模型列表失败')
- } finally {
- this.sceneModelLoading = false
- }
- },
- async saveThreshold() {
- try {
- await updateSceneThreshold(this.selectedScene, this.sceneForm.qualityThreshold)
- this.$message.success('阈值已更新')
- this.selectedSceneInfo.qualityThreshold = this.sceneForm.qualityThreshold
- } catch (e) {
- this.$message.error('更新失败')
- }
- },
- handleBindModel() {
- this.bindForm = { modelId: null, role: 'generator', pipelineOrder: this.sceneModelList.length, sortWeight: 0 }
- this.bindDialogVisible = true
- },
- async handleConfirmBind() {
- if (!this.bindForm.modelId) {
- this.$message.warning('请选择模型')
- return
- }
- try {
- await addSceneModel(this.selectedScene, this.bindForm)
- this.$message.success('绑定成功')
- this.bindDialogVisible = false
- this.loadSceneModels(this.selectedScene)
- } catch (e) {
- this.$message.error('绑定失败')
- }
- },
- handleEditSceneModel(row) {
- this.editSceneModelForm = {
- id: row.id,
- role: row.role || 'generator',
- pipelineOrder: row.pipelineOrder || 0,
- sortWeight: row.sortWeight || 0
- }
- this.editSceneModelDialog = true
- },
- async handleUpdateSceneModel() {
- try {
- await updateSceneModel(this.selectedScene, this.editSceneModelForm.id, this.editSceneModelForm)
- this.$message.success('更新成功')
- this.editSceneModelDialog = false
- this.loadSceneModels(this.selectedScene)
- } catch (e) {
- this.$message.error('更新失败')
- }
- },
- async handleRemoveSceneModel(row) {
- try {
- await deleteSceneModel(this.selectedScene, row.id)
- this.$message.success('已移除')
- this.loadSceneModels(this.selectedScene)
- } catch (e) {
- this.$message.error('移除失败')
- }
- },
- // ─── 通用 ───
- handleTabClick() {
- if (this.activeTab === 'models') this.loadModels()
- if (this.activeTab === 'scenes') this.loadScenes()
- },
- async handleRefresh() {
- try {
- await refreshAiModel()
- await refreshAiScene()
- this.$message.success('缓存已刷新')
- if (this.activeTab === 'models') this.loadModels()
- if (this.activeTab === 'scenes') this.loadScenes()
- } catch (e) {
- this.$message.error('刷新失败')
- }
- }
- }
- }
- </script>
- <style scoped>
- .card-header { display: flex; justify-content: space-between; align-items: center; }
- .sub-header { font-weight: 600; font-size: 14px; }
- .scene-list-card { max-height: 600px; overflow-y: auto; }
- .scene-item {
- padding: 10px 12px;
- border: 1px solid #EBEEF5;
- border-radius: 4px;
- margin-bottom: 8px;
- cursor: pointer;
- transition: all 0.2s;
- }
- .scene-item:hover { border-color: #409EFF; background: #ECF5FF; }
- .scene-item.active { border-color: #409EFF; background: #ECF5FF; }
- .scene-name { font-weight: 500; font-size: 13px; }
- .scene-code { color: #909399; font-size: 11px; margin-top: 2px; }
- .sort-handle { cursor: move; color: #909399; display: inline-block; vertical-align: middle; }
- .sort-handle:hover { color: #409EFF; }
- </style>
|