index.vue 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523
  1. <template>
  2. <div class="app-container">
  3. <el-card>
  4. <div slot="header" class="card-header">
  5. <span>AI模型配置管理</span>
  6. <div>
  7. <el-button type="primary" size="small" @click="handleAddModel" v-if="activeTab === 'models'">
  8. <i class="el-icon-plus"></i>新增模型
  9. </el-button>
  10. <el-button type="success" size="small" @click="handleRefresh">
  11. <i class="el-icon-refresh"></i>刷新缓存
  12. </el-button>
  13. </div>
  14. </div>
  15. <el-alert
  16. title="统一模型配置说明"
  17. description="所有文本AI模型统一在此管理,按sort_order排序优先级(越小越优先)。每个场景可独立配置使用哪些模型及流水线顺序。FastGPT、TTS语音、豆包视觉及图像/语音相关模型不在此处管理。"
  18. type="info" show-icon :closable="false" style="margin-bottom: 20px"
  19. />
  20. <el-tabs v-model="activeTab" @tab-click="handleTabClick">
  21. <!-- ===== 模型列表 Tab ===== -->
  22. <el-tab-pane label="模型列表" name="models">
  23. <el-table :data="modelList" v-loading="modelLoading" border size="small" row-key="id"
  24. style="width:100%">
  25. <el-table-column label="排序" width="70" align="center">
  26. <template slot-scope="{ row, $index }">
  27. <span class="sort-handle" style="cursor: move; font-size: 18px;">☰</span>
  28. <span style="margin-left: 4px;">{{ $index + 1 }}</span>
  29. </template>
  30. </el-table-column>
  31. <el-table-column prop="modelName" label="模型名称" min-width="120" />
  32. <el-table-column prop="providerCode" label="供应商" width="90" />
  33. <el-table-column prop="modelIdentifier" label="模型标识" min-width="160" show-overflow-tooltip />
  34. <el-table-column prop="apiEndpoint" label="API地址" min-width="200" show-overflow-tooltip />
  35. <el-table-column prop="maxTokens" label="MaxToken" width="90" align="center" />
  36. <el-table-column prop="temperature" label="温度" width="70" align="center" />
  37. <el-table-column prop="sortOrder" label="排序号" width="70" align="center" />
  38. <el-table-column prop="status" label="状态" width="70" align="center">
  39. <template slot-scope="{ row }">
  40. <el-tag :type="row.status === 1 ? 'success' : 'danger'" size="small">
  41. {{ row.status === 1 ? '启用' : '禁用' }}
  42. </el-tag>
  43. </template>
  44. </el-table-column>
  45. <el-table-column label="操作" width="280" fixed="right">
  46. <template slot-scope="{ row }">
  47. <el-button type="primary" size="mini" @click="handleEditModel(row)">编辑</el-button>
  48. <el-button type="success" size="mini" @click="handleTestModel(row)">测试</el-button>
  49. <el-popconfirm title="确定删除该模型吗?" @confirm="handleDeleteModel(row)">
  50. <el-button slot="reference" type="danger" size="mini">删除</el-button>
  51. </el-popconfirm>
  52. </template>
  53. </el-table-column>
  54. </el-table>
  55. <div style="margin-top: 12px; text-align: right;">
  56. <el-button type="warning" size="small" @click="handleSaveSort" :disabled="!sortChanged">
  57. <i class="el-icon-sort"></i>保存排序
  58. </el-button>
  59. </div>
  60. </el-tab-pane>
  61. <!-- ===== 场景配置 Tab ===== -->
  62. <el-tab-pane label="场景配置" name="scenes">
  63. <el-row :gutter="16">
  64. <!-- 左侧:场景列表 -->
  65. <el-col :span="8">
  66. <el-card shadow="never" class="scene-list-card">
  67. <div slot="header" class="sub-header">使用场景</div>
  68. <div v-loading="sceneLoading">
  69. <div v-for="scene in sceneList" :key="scene.sceneCode"
  70. class="scene-item"
  71. :class="{ active: selectedScene === scene.sceneCode }"
  72. @click="selectScene(scene)">
  73. <div class="scene-name">
  74. {{ scene.sceneName }}
  75. <el-tag v-if="scene.sceneType === 'multi_pipeline'" size="mini" type="warning" style="margin-left: 6px;">多模型</el-tag>
  76. </div>
  77. <div class="scene-code">{{ scene.sceneCode }}</div>
  78. </div>
  79. </div>
  80. </el-card>
  81. </el-col>
  82. <!-- 右侧:场景详情 -->
  83. <el-col :span="16">
  84. <el-card shadow="never" v-if="selectedSceneInfo">
  85. <div slot="header" class="sub-header">
  86. {{ selectedSceneInfo.sceneName }} - 模型配置
  87. <el-tag size="small" :type="selectedSceneInfo.status === 1 ? 'success' : 'danger'" style="margin-left: 8px;">
  88. {{ selectedSceneInfo.status === 1 ? '启用' : '禁用' }}
  89. </el-tag>
  90. </div>
  91. <!-- 场景基础信息 -->
  92. <el-form :model="sceneForm" label-width="100px" size="small" inline>
  93. <el-form-item label="场景编码">{{ selectedSceneInfo.sceneCode }}</el-form-item>
  94. <el-form-item label="流水线类型" v-if="selectedSceneInfo.sceneType === 'multi_pipeline'">
  95. {{ selectedSceneInfo.pipelineType === 'scoring' ? '质量评分链' : selectedSceneInfo.pipelineType === 'sequential' ? '顺序调用' : '-' }}
  96. </el-form-item>
  97. <el-form-item label="质量阈值" v-if="selectedSceneInfo.pipelineType === 'scoring'">
  98. <el-input-number v-model="sceneForm.qualityThreshold" :min="60" :max="160" :step="10" size="mini" style="width: 100px;" />
  99. <el-button type="primary" size="mini" @click="saveThreshold" style="margin-left: 8px;">保存</el-button>
  100. </el-form-item>
  101. </el-form>
  102. <el-divider />
  103. <!-- 场景关联模型列表 -->
  104. <div style="margin-bottom: 10px; display: flex; justify-content: space-between; align-items: center;">
  105. <span>关联模型流水线(按 pipeline_order 排序)</span>
  106. <el-button type="primary" size="mini" @click="handleBindModel">
  107. <i class="el-icon-plus"></i>绑定模型
  108. </el-button>
  109. </div>
  110. <el-table :data="sceneModelList" v-loading="sceneModelLoading" border size="small"
  111. row-key="id" style="width:100%">
  112. <el-table-column label="顺序" width="60" align="center">
  113. <template slot-scope="{ $index }">
  114. <span class="sort-handle" style="cursor: move;">☰</span>
  115. <span style="margin-left: 4px;">{{ $index + 1 }}</span>
  116. </template>
  117. </el-table-column>
  118. <el-table-column label="模型名称" min-width="120">
  119. <template slot-scope="{ row }">
  120. {{ row.model ? row.model.modelName : '模型#' + row.modelId }}
  121. </template>
  122. </el-table-column>
  123. <el-table-column label="供应商" width="80">
  124. <template slot-scope="{ row }">
  125. {{ row.model ? row.model.providerCode : '-' }}
  126. </template>
  127. </el-table-column>
  128. <el-table-column label="流水线角色" width="110" align="center">
  129. <template slot-scope="{ row }">
  130. <el-tag size="mini" :type="row.role === 'scorer' ? 'warning' : 'success'">
  131. {{ row.role === 'scorer' ? '评分者' : '生成者' }}
  132. </el-tag>
  133. </template>
  134. </el-table-column>
  135. <el-table-column prop="pipelineOrder" label="顺序号" width="70" align="center" />
  136. <el-table-column prop="sortWeight" label="权重" width="60" align="center" />
  137. <el-table-column label="操作" width="120" fixed="right">
  138. <template slot-scope="{ row }">
  139. <el-button type="text" size="mini" @click="handleEditSceneModel(row)">编辑</el-button>
  140. <el-popconfirm title="确定移除此关联?" @confirm="handleRemoveSceneModel(row)">
  141. <el-button slot="reference" type="text" size="mini" style="color: #F56C6C;">移除</el-button>
  142. </el-popconfirm>
  143. </template>
  144. </el-table-column>
  145. </el-table>
  146. <div v-if="sceneModelList.length === 0" style="text-align: center; color: #999; padding: 40px 0;">
  147. 暂未绑定模型,请点击"绑定模型"添加
  148. </div>
  149. </el-card>
  150. <el-empty v-else description="请从左侧选择一个场景" />
  151. </el-col>
  152. </el-row>
  153. </el-tab-pane>
  154. </el-tabs>
  155. </el-card>
  156. <!-- 模型编辑弹窗 -->
  157. <el-dialog :title="modelDialogTitle" :visible.sync="modelDialogVisible" width="560px" @closed="resetModelForm">
  158. <el-form :model="modelForm" :rules="modelRules" ref="modelFormRef" label-width="120px">
  159. <el-form-item label="模型名称" prop="modelName">
  160. <el-input v-model="modelForm.modelName" placeholder="如:豆包Pro 32K" />
  161. </el-form-item>
  162. <el-form-item label="供应商" prop="providerCode">
  163. <el-select v-model="modelForm.providerCode" placeholder="请选择" style="width: 100%">
  164. <el-option label="豆包(Doubao)" value="doubao" />
  165. <el-option label="通义千问(Qwen)" value="qwen" />
  166. <el-option label="元宝(Yuanbao)" value="yuanbao" />
  167. <el-option label="DeepSeek" value="deepseek" />
  168. </el-select>
  169. </el-form-item>
  170. <el-form-item label="模型标识" prop="modelIdentifier">
  171. <el-input v-model="modelForm.modelIdentifier" placeholder="如:doubao-1-5-pro-32k" />
  172. </el-form-item>
  173. <el-form-item label="API地址" prop="apiEndpoint">
  174. <el-input v-model="modelForm.apiEndpoint" placeholder="API Endpoint" />
  175. </el-form-item>
  176. <el-form-item label="API Key" prop="apiKey">
  177. <el-input v-model="modelForm.apiKey" placeholder="API Key" show-password />
  178. </el-form-item>
  179. <el-form-item label="最大Token">
  180. <el-input-number v-model="modelForm.maxTokens" :min="512" :max="131072" :step="512" style="width: 180px;" />
  181. </el-form-item>
  182. <el-form-item label="温度参数">
  183. <el-slider v-model="modelForm.temperature" :min="0" :max="2" :step="0.1" show-input style="width: 200px;" />
  184. </el-form-item>
  185. <el-form-item label="排序号">
  186. <el-input-number v-model="modelForm.sortOrder" :min="0" :max="9999" style="width: 180px;" />
  187. <span style="color: #909399; font-size: 12px; margin-left: 8px;">越小越优先</span>
  188. </el-form-item>
  189. <el-form-item label="状态">
  190. <el-switch v-model="modelForm.status" :active-value="1" :inactive-value="0" />
  191. </el-form-item>
  192. </el-form>
  193. <div slot="footer">
  194. <el-button @click="modelDialogVisible = false">取消</el-button>
  195. <el-button type="primary" @click="handleSubmitModel">确定</el-button>
  196. </div>
  197. </el-dialog>
  198. <!-- 场景绑定模型弹窗 -->
  199. <el-dialog title="绑定模型到场景" :visible.sync="bindDialogVisible" width="480px">
  200. <el-form label-width="100px" size="small">
  201. <el-form-item label="选择模型">
  202. <el-select v-model="bindForm.modelId" placeholder="请选择模型" style="width: 100%" filterable>
  203. <el-option v-for="m in modelList" :key="m.id"
  204. :label="m.modelName + ' (' + m.providerCode + ')'" :value="m.id"
  205. :disabled="m.status !== 1" />
  206. </el-select>
  207. </el-form-item>
  208. <el-form-item label="流水线角色">
  209. <el-radio-group v-model="bindForm.role">
  210. <el-radio label="generator">生成者</el-radio>
  211. <el-radio label="scorer">评分者</el-radio>
  212. </el-radio-group>
  213. </el-form-item>
  214. <el-form-item label="流水线顺序">
  215. <el-input-number v-model="bindForm.pipelineOrder" :min="0" :max="99" size="small" />
  216. </el-form-item>
  217. <el-form-item label="权重">
  218. <el-input-number v-model="bindForm.sortWeight" :min="0" :max="99" size="small" />
  219. </el-form-item>
  220. </el-form>
  221. <div slot="footer">
  222. <el-button @click="bindDialogVisible = false">取消</el-button>
  223. <el-button type="primary" @click="handleConfirmBind">确定</el-button>
  224. </div>
  225. </el-dialog>
  226. <!-- 场景模型编辑弹窗 -->
  227. <el-dialog title="编辑场景模型" :visible.sync="editSceneModelDialog" width="420px">
  228. <el-form label-width="100px" size="small">
  229. <el-form-item label="流水线角色">
  230. <el-radio-group v-model="editSceneModelForm.role">
  231. <el-radio label="generator">生成者</el-radio>
  232. <el-radio label="scorer">评分者</el-radio>
  233. </el-radio-group>
  234. </el-form-item>
  235. <el-form-item label="流水线顺序">
  236. <el-input-number v-model="editSceneModelForm.pipelineOrder" :min="0" :max="99" size="small" />
  237. </el-form-item>
  238. <el-form-item label="权重">
  239. <el-input-number v-model="editSceneModelForm.sortWeight" :min="0" :max="99" size="small" />
  240. </el-form-item>
  241. </el-form>
  242. <div slot="footer">
  243. <el-button @click="editSceneModelDialog = false">取消</el-button>
  244. <el-button type="primary" @click="handleUpdateSceneModel">确定</el-button>
  245. </div>
  246. </el-dialog>
  247. </div>
  248. </template>
  249. <script>
  250. import {
  251. listAiModel, getAiModel, addAiModel, updateAiModel, deleteAiModel,
  252. batchSortAiModel, testAiModel, refreshAiModel,
  253. listAiScene, updateAiScene, updateSceneThreshold,
  254. getSceneModels, addSceneModel, deleteSceneModel, updateSceneModel, refreshAiScene
  255. } from '@/api/admin/aiModel'
  256. export default {
  257. name: 'AiModelConfig',
  258. data() {
  259. return {
  260. activeTab: 'models',
  261. // 模型
  262. modelLoading: false,
  263. modelList: [],
  264. modelDialogVisible: false,
  265. modelDialogTitle: '',
  266. modelForm: {
  267. id: null, modelName: '', providerCode: '', modelIdentifier: '',
  268. apiEndpoint: '', apiKey: '', maxTokens: 4096, temperature: 0.7,
  269. sortOrder: 0, status: 1
  270. },
  271. modelRules: {
  272. modelName: [{ required: true, message: '请输入模型名称', trigger: 'blur' }],
  273. providerCode: [{ required: true, message: '请选择供应商', trigger: 'change' }],
  274. modelIdentifier: [{ required: true, message: '请输入模型标识', trigger: 'blur' }],
  275. apiEndpoint: [{ required: true, message: '请输入API地址', trigger: 'blur' }],
  276. apiKey: [{ required: true, message: '请输入API Key', trigger: 'blur' }]
  277. },
  278. sortChanged: false,
  279. // 场景
  280. sceneLoading: false,
  281. sceneList: [],
  282. selectedScene: '',
  283. selectedSceneInfo: null,
  284. sceneForm: { qualityThreshold: 120 },
  285. sceneModelLoading: false,
  286. sceneModelList: [],
  287. bindDialogVisible: false,
  288. bindForm: { modelId: null, role: 'generator', pipelineOrder: 0, sortWeight: 0 },
  289. editSceneModelDialog: false,
  290. editSceneModelForm: { id: null, role: 'generator', pipelineOrder: 0, sortWeight: 0 }
  291. }
  292. },
  293. created() {
  294. this.loadModels()
  295. this.loadScenes()
  296. },
  297. methods: {
  298. // ─── 模型 CRUD ───
  299. async loadModels() {
  300. this.modelLoading = true
  301. try {
  302. const res = await listAiModel()
  303. this.modelList = res.data || []
  304. } catch (e) {
  305. this.$message.error('获取模型列表失败')
  306. } finally {
  307. this.modelLoading = false
  308. }
  309. },
  310. handleAddModel() {
  311. this.modelDialogTitle = '新增模型'
  312. this.modelForm = {
  313. id: null, modelName: '', providerCode: '', modelIdentifier: '',
  314. apiEndpoint: '', apiKey: '', maxTokens: 4096, temperature: 0.7,
  315. sortOrder: 0, status: 1
  316. }
  317. this.modelDialogVisible = true
  318. this.$nextTick(() => { this.$refs.modelFormRef && this.$refs.modelFormRef.resetFields() })
  319. },
  320. handleEditModel(row) {
  321. this.modelDialogTitle = '编辑模型'
  322. this.modelForm = { ...row }
  323. this.modelDialogVisible = true
  324. },
  325. async handleSubmitModel() {
  326. try {
  327. await this.$refs.modelFormRef.validate()
  328. if (this.modelForm.id) {
  329. await updateAiModel(this.modelForm.id, this.modelForm)
  330. this.$message.success('修改成功')
  331. } else {
  332. await addAiModel(this.modelForm)
  333. this.$message.success('新增成功')
  334. }
  335. this.modelDialogVisible = false
  336. await this.loadModels()
  337. } catch (e) {
  338. if (e !== false) this.$message.error('操作失败')
  339. }
  340. },
  341. async handleDeleteModel(row) {
  342. try {
  343. await deleteAiModel(row.id)
  344. this.$message.success('删除成功')
  345. await this.loadModels()
  346. } catch (e) {
  347. this.$message.error('删除失败')
  348. }
  349. },
  350. async handleTestModel(row) {
  351. try {
  352. const res = await testAiModel(row.id)
  353. if (res.code === 200 && res.data) {
  354. const result = res.data
  355. if (result.success) {
  356. this.$message.success('连接成功!响应:' + (result.response || 'OK'))
  357. } else {
  358. this.$message.error('连接失败:' + (result.error || '未知错误'))
  359. }
  360. } else {
  361. this.$message.error('测试失败')
  362. }
  363. } catch (e) {
  364. this.$message.error('测试请求失败')
  365. }
  366. },
  367. handleSaveSort() {
  368. const sortList = this.modelList.map((m, i) => ({ id: m.id, sortOrder: i }))
  369. batchSortAiModel(sortList).then(() => {
  370. this.$message.success('排序已保存')
  371. this.sortChanged = false
  372. this.loadModels()
  373. }).catch(() => {
  374. this.$message.error('保存排序失败')
  375. })
  376. },
  377. resetModelForm() {
  378. this.modelForm = {
  379. id: null, modelName: '', providerCode: '', modelIdentifier: '',
  380. apiEndpoint: '', apiKey: '', maxTokens: 4096, temperature: 0.7,
  381. sortOrder: 0, status: 1
  382. }
  383. },
  384. // ─── 场景 ───
  385. async loadScenes() {
  386. this.sceneLoading = true
  387. try {
  388. const res = await listAiScene()
  389. this.sceneList = res.data || []
  390. } catch (e) {
  391. this.$message.error('获取场景列表失败')
  392. } finally {
  393. this.sceneLoading = false
  394. }
  395. },
  396. selectScene(scene) {
  397. this.selectedScene = scene.sceneCode
  398. this.selectedSceneInfo = scene
  399. this.sceneForm.qualityThreshold = scene.qualityThreshold || 120
  400. this.loadSceneModels(scene.sceneCode)
  401. },
  402. async loadSceneModels(sceneCode) {
  403. this.sceneModelLoading = true
  404. try {
  405. const res = await getSceneModels(sceneCode)
  406. this.sceneModelList = res.data || []
  407. } catch (e) {
  408. this.$message.error('获取场景模型列表失败')
  409. } finally {
  410. this.sceneModelLoading = false
  411. }
  412. },
  413. async saveThreshold() {
  414. try {
  415. await updateSceneThreshold(this.selectedScene, this.sceneForm.qualityThreshold)
  416. this.$message.success('阈值已更新')
  417. this.selectedSceneInfo.qualityThreshold = this.sceneForm.qualityThreshold
  418. } catch (e) {
  419. this.$message.error('更新失败')
  420. }
  421. },
  422. handleBindModel() {
  423. this.bindForm = { modelId: null, role: 'generator', pipelineOrder: this.sceneModelList.length, sortWeight: 0 }
  424. this.bindDialogVisible = true
  425. },
  426. async handleConfirmBind() {
  427. if (!this.bindForm.modelId) {
  428. this.$message.warning('请选择模型')
  429. return
  430. }
  431. try {
  432. await addSceneModel(this.selectedScene, this.bindForm)
  433. this.$message.success('绑定成功')
  434. this.bindDialogVisible = false
  435. this.loadSceneModels(this.selectedScene)
  436. } catch (e) {
  437. this.$message.error('绑定失败')
  438. }
  439. },
  440. handleEditSceneModel(row) {
  441. this.editSceneModelForm = {
  442. id: row.id,
  443. role: row.role || 'generator',
  444. pipelineOrder: row.pipelineOrder || 0,
  445. sortWeight: row.sortWeight || 0
  446. }
  447. this.editSceneModelDialog = true
  448. },
  449. async handleUpdateSceneModel() {
  450. try {
  451. await updateSceneModel(this.selectedScene, this.editSceneModelForm.id, this.editSceneModelForm)
  452. this.$message.success('更新成功')
  453. this.editSceneModelDialog = false
  454. this.loadSceneModels(this.selectedScene)
  455. } catch (e) {
  456. this.$message.error('更新失败')
  457. }
  458. },
  459. async handleRemoveSceneModel(row) {
  460. try {
  461. await deleteSceneModel(this.selectedScene, row.id)
  462. this.$message.success('已移除')
  463. this.loadSceneModels(this.selectedScene)
  464. } catch (e) {
  465. this.$message.error('移除失败')
  466. }
  467. },
  468. // ─── 通用 ───
  469. handleTabClick() {
  470. if (this.activeTab === 'models') this.loadModels()
  471. if (this.activeTab === 'scenes') this.loadScenes()
  472. },
  473. async handleRefresh() {
  474. try {
  475. await refreshAiModel()
  476. await refreshAiScene()
  477. this.$message.success('缓存已刷新')
  478. if (this.activeTab === 'models') this.loadModels()
  479. if (this.activeTab === 'scenes') this.loadScenes()
  480. } catch (e) {
  481. this.$message.error('刷新失败')
  482. }
  483. }
  484. }
  485. }
  486. </script>
  487. <style scoped>
  488. .card-header { display: flex; justify-content: space-between; align-items: center; }
  489. .sub-header { font-weight: 600; font-size: 14px; }
  490. .scene-list-card { max-height: 600px; overflow-y: auto; }
  491. .scene-item {
  492. padding: 10px 12px;
  493. border: 1px solid #EBEEF5;
  494. border-radius: 4px;
  495. margin-bottom: 8px;
  496. cursor: pointer;
  497. transition: all 0.2s;
  498. }
  499. .scene-item:hover { border-color: #409EFF; background: #ECF5FF; }
  500. .scene-item.active { border-color: #409EFF; background: #ECF5FF; }
  501. .scene-name { font-weight: 500; font-size: 13px; }
  502. .scene-code { color: #909399; font-size: 11px; margin-top: 2px; }
  503. .sort-handle { cursor: move; color: #909399; display: inline-block; vertical-align: middle; }
  504. .sort-handle:hover { color: #409EFF; }
  505. </style>