🎨 优化模型配置 && 新增apikey功能 && 完善通用接口

This commit is contained in:
2026-03-03 17:13:24 +08:00
parent 2714e63d2a
commit 7dae1a6e2b
46 changed files with 3063 additions and 278 deletions

View File

@@ -1,7 +1,7 @@
ENV = 'production'
#下方为上线需要用到的程序代理前缀一般用于nginx代理转发
# API 路径前缀
VITE_BASE_API = /api
VITE_FILE_API = /api
#下方修改为你的线上ip如果需要在线使用表单构建工具时使用其余情况无需使用以下环境变量
VITE_BASE_PATH = https://demo.gin-vue-admin.com
# 基础路径(部署时会自动使用当前域名
VITE_BASE_PATH = /

46
web/src/api/aiApiKey.js Normal file
View File

@@ -0,0 +1,46 @@
import service from '@/utils/request'
// 创建API密钥
export const createAiApiKey = (data) => {
return service({
url: '/aiApiKey/createAiApiKey',
method: 'post',
data
})
}
// 删除API密钥
export const deleteAiApiKey = (data) => {
return service({
url: '/aiApiKey/deleteAiApiKey',
method: 'delete',
data
})
}
// 更新API密钥
export const updateAiApiKey = (data) => {
return service({
url: '/aiApiKey/updateAiApiKey',
method: 'put',
data
})
}
// 查询API密钥
export const findAiApiKey = (params) => {
return service({
url: '/aiApiKey/findAiApiKey',
method: 'get',
params
})
}
// 获取API密钥列表
export const getAiApiKeyList = (params) => {
return service({
url: '/aiApiKey/getAiApiKeyList',
method: 'get',
params
})
}

55
web/src/api/aiModel.js Normal file
View File

@@ -0,0 +1,55 @@
import service from '@/utils/request'
// 创建模型
export const createAiModel = (data) => {
return service({
url: '/aiModel/createAiModel',
method: 'post',
data
})
}
// 删除模型
export const deleteAiModel = (data) => {
return service({
url: '/aiModel/deleteAiModel',
method: 'delete',
data
})
}
// 更新模型
export const updateAiModel = (data) => {
return service({
url: '/aiModel/updateAiModel',
method: 'put',
data
})
}
// 查询模型
export const findAiModel = (params) => {
return service({
url: '/aiModel/findAiModel',
method: 'get',
params
})
}
// 获取模型列表
export const getAiModelList = (params) => {
return service({
url: '/aiModel/getAiModelList',
method: 'get',
params
})
}
// 同步提供商模型
export const syncProviderModels = (data) => {
return service({
url: '/aiModel/syncProviderModels',
method: 'post',
data
})
}

View File

@@ -45,7 +45,7 @@ export const getAiPresetList = (params) => {
})
}
// 导入预设
// 导入预设(JSON粘贴)
export const importAiPreset = (data) => {
return service({
url: '/aiPreset/importAiPreset',
@@ -53,3 +53,17 @@ export const importAiPreset = (data) => {
data
})
}
// 导入预设(文件上传)
export const importAiPresetFile = (file) => {
const formData = new FormData()
formData.append('file', file)
return service({
url: '/aiPreset/importAiPresetFile',
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
})
}

View File

@@ -1,6 +1,9 @@
{
"/src/view/about/index.vue": "About",
"/src/view/ai/apikey/index.vue": "Index",
"/src/view/ai/binding/index.vue": "Index",
"/src/view/ai/model/index.vue": "Index",
"/src/view/ai/preset/components/PromptEditor.vue": "PromptEditor",
"/src/view/ai/preset/index.vue": "Index",
"/src/view/ai/provider/index.vue": "Index",
"/src/view/dashboard/components/banner.vue": "Banner",

View File

@@ -0,0 +1,392 @@
<template>
<div>
<div class="gva-search-box">
<el-form :inline="true" :model="searchInfo" class="demo-form-inline">
<el-form-item label="密钥名称">
<el-input v-model="searchInfo.name" placeholder="搜索密钥名称" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit">查询</el-button>
<el-button @click="onReset">重置</el-button>
</el-form-item>
</el-form>
</div>
<div class="gva-table-box">
<div class="gva-btn-list">
<el-button type="primary" @click="openDialog">新增</el-button>
</div>
<el-table :data="tableData" style="width: 100%">
<el-table-column type="selection" width="55" />
<el-table-column label="ID" prop="ID" width="80" />
<el-table-column label="名称" prop="name" width="200" />
<el-table-column label="API Key" prop="key" min-width="300" show-overflow-tooltip>
<template #default="scope">
<span class="key-text">{{ maskKey(scope.row.key) }}</span>
<el-button type="primary" link size="small" @click="copyKey(scope.row.key)">
复制
</el-button>
</template>
</el-table-column>
<el-table-column label="允许的模型" width="150">
<template #default="scope">
<el-tag v-if="scope.row.allowed_models?.length > 0" type="info">
{{ scope.row.allowed_models.length }}
</el-tag>
<el-tag v-else type="success">全部</el-tag>
</template>
</el-table-column>
<el-table-column label="速率限制" width="120">
<template #default="scope">
{{ scope.row.rate_limit || '无限制' }}
</template>
</el-table-column>
<el-table-column label="过期时间" width="180">
<template #default="scope">
{{ scope.row.expires_at ? formatDate(scope.row.expires_at * 1000) : '永不过期' }}
</template>
</el-table-column>
<el-table-column label="状态" prop="enabled" width="80">
<template #default="scope">
<el-switch v-model="scope.row.enabled" @change="handleStatusChange(scope.row)" />
</template>
</el-table-column>
<el-table-column label="创建时间" prop="CreatedAt" width="180">
<template #default="scope">{{ formatDate(scope.row.CreatedAt) }}</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="200">
<template #default="scope">
<el-button type="primary" link @click="updateApiKey(scope.row)">编辑</el-button>
<el-button type="danger" link @click="deleteApiKey(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="gva-pagination">
<el-pagination
:current-page="page"
:page-size="pageSize"
:page-sizes="[10, 30, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
/>
</div>
</div>
<el-dialog
v-model="dialogFormVisible"
:title="type === 'create' ? '新增API密钥' : '编辑API密钥'"
width="700px"
>
<el-form ref="apiKeyForm" :model="formData" :rules="rules" label-width="120px">
<el-form-item label="名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入密钥名称" clearable />
</el-form-item>
<el-form-item v-if="type === 'create'" label="API Key" prop="key">
<el-input v-model="formData.key" placeholder="留空自动生成" clearable />
</el-form-item>
<el-form-item label="允许的模型">
<el-select
v-model="formData.allowed_models"
multiple
filterable
allow-create
placeholder="留空表示允许所有模型"
style="width: 100%"
>
<el-option
v-for="model in availableModels"
:key="model"
:label="model"
:value="model"
/>
</el-select>
<div style="margin-top: 5px; color: #909399; font-size: 12px">
模型列表来自提供商配置也可手动输入其他模型名称
</div>
</el-form-item>
<el-form-item label="允许的预设">
<el-select
v-model="formData.allowed_presets"
multiple
filterable
allow-create
placeholder="留空表示允许所有预设"
style="width: 100%"
>
<el-option
v-for="preset in presetList"
:key="preset.name"
:label="preset.name"
:value="preset.name"
/>
</el-select>
</el-form-item>
<el-form-item label="速率限制">
<el-input-number
v-model="formData.rate_limit"
:min="0"
placeholder="每分钟请求数0表示不限制"
/>
<span style="margin-left: 10px; color: #909399">/分钟</span>
</el-form-item>
<el-form-item label="过期时间">
<el-date-picker
v-model="expiresDate"
type="datetime"
placeholder="选择过期时间"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="启用" prop="enabled">
<el-switch v-model="formData.enabled" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="closeDialog">取消</el-button>
<el-button type="primary" @click="enterDialog">确定</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
getAiApiKeyList,
createAiApiKey,
updateAiApiKey,
deleteAiApiKey
} from '@/api/aiApiKey'
import { getAiPresetList } from '@/api/aiPreset'
import { getAiProviderList } from '@/api/aiProvider'
const page = ref(1)
const pageSize = ref(10)
const total = ref(0)
const tableData = ref([])
const searchInfo = reactive({ name: '' })
const presetList = ref([])
const providerList = ref([])
const availableModels = computed(() => {
const models = new Set()
providerList.value.forEach(provider => {
if (provider.model) {
models.add(provider.model)
}
})
return Array.from(models)
})
const dialogFormVisible = ref(false)
const type = ref('')
const apiKeyForm = ref(null)
const expiresDate = ref(null)
const formData = ref({
name: '',
key: '',
allowed_models: [],
allowed_presets: [],
rate_limit: 0,
expires_at: null,
enabled: true
})
const rules = reactive({
name: [{ required: true, message: '请输入密钥名称', trigger: 'blur' }]
})
const maskKey = (key) => {
if (!key) return ''
if (key.length <= 12) return key
return key.substring(0, 8) + '...' + key.substring(key.length - 4)
}
const copyKey = (key) => {
navigator.clipboard.writeText(key)
ElMessage.success('已复制到剪贴板')
}
const formatDate = (date) => {
if (!date) return ''
return new Date(date).toLocaleString('zh-CN')
}
const getTableData = async () => {
const table = await getAiApiKeyList({
page: page.value,
pageSize: pageSize.value,
...searchInfo
})
if (table.code === 0) {
tableData.value = table.data.list
total.value = table.data.total
page.value = table.data.page
pageSize.value = table.data.pageSize
}
}
const loadPresets = async () => {
const res = await getAiPresetList({ page: 1, pageSize: 100 })
if (res.code === 0) {
presetList.value = res.data.list
}
}
const loadProviders = async () => {
const res = await getAiProviderList({ page: 1, pageSize: 100 })
if (res.code === 0) {
providerList.value = res.data.list
}
}
const onSubmit = () => {
page.value = 1
getTableData()
}
const onReset = () => {
searchInfo.name = ''
getTableData()
}
const handleCurrentChange = (val) => {
page.value = val
getTableData()
}
const handleSizeChange = (val) => {
pageSize.value = val
getTableData()
}
const handleStatusChange = async (row) => {
const res = await updateAiApiKey(row)
if (res.code === 0) {
ElMessage.success('状态更新成功')
} else {
row.enabled = !row.enabled
ElMessage.error('状态更新失败')
}
}
const openDialog = () => {
type.value = 'create'
dialogFormVisible.value = true
}
const closeDialog = () => {
dialogFormVisible.value = false
expiresDate.value = null
formData.value = {
name: '',
key: '',
allowed_models: [],
allowed_presets: [],
rate_limit: 0,
expires_at: null,
enabled: true
}
}
const enterDialog = async () => {
apiKeyForm.value.validate(async (valid) => {
if (valid) {
// 处理过期时间
if (expiresDate.value) {
formData.value.expires_at = Math.floor(new Date(expiresDate.value).getTime() / 1000)
} else {
formData.value.expires_at = null
}
let res
if (type.value === 'create') {
res = await createAiApiKey(formData.value)
if (res.code === 0) {
ElMessage.success('创建成功')
if (res.data?.key) {
ElMessageBox.alert(
`API Key: ${res.data.key}`,
'请保存您的API密钥',
{
confirmButtonText: '复制',
callback: () => {
copyKey(res.data.key)
}
}
)
}
}
} else {
res = await updateAiApiKey(formData.value)
if (res.code === 0) {
ElMessage.success('更新成功')
}
}
if (res.code === 0) {
closeDialog()
getTableData()
}
}
})
}
const updateApiKey = (row) => {
type.value = 'update'
formData.value = { ...row }
if (row.expires_at) {
expiresDate.value = new Date(row.expires_at * 1000)
}
dialogFormVisible.value = true
}
const deleteApiKey = (row) => {
ElMessageBox.confirm('确定要删除该API密钥吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
const res = await deleteAiApiKey({ ID: row.ID })
if (res.code === 0) {
ElMessage.success('删除成功')
getTableData()
}
})
}
getTableData()
loadPresets()
loadProviders()
</script>
<style scoped>
.gva-search-box {
padding: 20px;
background: #fff;
margin-bottom: 10px;
}
.gva-table-box {
padding: 20px;
background: #fff;
}
.gva-btn-list {
margin-bottom: 10px;
}
.gva-pagination {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
.key-text {
font-family: monospace;
color: #606266;
}
</style>

View File

@@ -0,0 +1,491 @@
<template>
<div>
<div class="gva-search-box">
<el-form :inline="true" :model="searchInfo" class="demo-form-inline">
<el-form-item label="显示模式">
<el-radio-group v-model="viewMode" @change="handleViewModeChange">
<el-radio-button value="list">列表视图</el-radio-button>
<el-radio-button value="group">分组视图</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="模型名称">
<el-input v-model="searchInfo.name" placeholder="搜索模型名称" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit">查询</el-button>
<el-button @click="onReset">重置</el-button>
</el-form-item>
</el-form>
</div>
<!-- 列表视图 -->
<div v-if="viewMode === 'list'" class="gva-table-box">
<div class="gva-btn-list">
<el-button type="primary" @click="openDialog">新增</el-button>
<el-button type="success" @click="openSyncDialog">同步模型</el-button>
</div>
<el-table :data="tableData" style="width: 100%">
<el-table-column type="selection" width="55" />
<el-table-column label="ID" prop="ID" width="80" />
<el-table-column label="模型名称" prop="name" width="200" />
<el-table-column label="显示名称" prop="display_name" width="150" />
<el-table-column label="提供商" width="150">
<template #default="scope">
<el-tag>{{ scope.row.provider?.name || '-' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="绑定预设" width="150">
<template #default="scope">
<el-tag v-if="scope.row.preset" type="success">
{{ scope.row.preset.name }}
</el-tag>
<el-tag v-else type="info">未绑定</el-tag>
</template>
</el-table-column>
<el-table-column label="Max Tokens" prop="max_tokens" width="120" />
<el-table-column label="状态" prop="enabled" width="80">
<template #default="scope">
<el-switch v-model="scope.row.enabled" @change="handleStatusChange(scope.row)" />
</template>
</el-table-column>
<el-table-column label="描述" prop="description" min-width="200" show-overflow-tooltip />
<el-table-column label="操作" fixed="right" width="200">
<template #default="scope">
<el-button type="primary" link @click="updateModel(scope.row)">编辑</el-button>
<el-button type="danger" link @click="deleteModel(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="gva-pagination">
<el-pagination
:current-page="page"
:page-size="pageSize"
:page-sizes="[10, 30, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
/>
</div>
</div>
<!-- 分组视图 -->
<div v-else class="gva-table-box">
<div class="gva-btn-list">
<el-button type="primary" @click="openDialog">新增</el-button>
<el-button type="success" @click="openSyncDialog">同步模型</el-button>
</div>
<el-collapse v-model="activeProviders" accordion>
<el-collapse-item
v-for="provider in groupedModels"
:key="provider.id"
:name="provider.id"
>
<template #title>
<div class="provider-header">
<el-tag :type="provider.enabled ? 'success' : 'info'" style="margin-right: 10px">
{{ provider.name }}
</el-tag>
<span class="model-count">{{ provider.models.length }} 个模型</span>
<span class="enabled-count">
({{ provider.models.filter(m => m.enabled).length }} 已启用)
</span>
</div>
</template>
<el-table :data="provider.models" style="width: 100%">
<el-table-column label="模型名称" prop="name" width="200" />
<el-table-column label="显示名称" prop="display_name" width="150" />
<el-table-column label="绑定预设" width="150">
<template #default="scope">
<el-tag v-if="scope.row.preset" type="success">
{{ scope.row.preset.name }}
</el-tag>
<el-tag v-else type="info">未绑定</el-tag>
</template>
</el-table-column>
<el-table-column label="Max Tokens" prop="max_tokens" width="120" />
<el-table-column label="状态" prop="enabled" width="80">
<template #default="scope">
<el-switch v-model="scope.row.enabled" @change="handleStatusChange(scope.row)" />
</template>
</el-table-column>
<el-table-column label="描述" prop="description" min-width="200" show-overflow-tooltip />
<el-table-column label="操作" fixed="right" width="200">
<template #default="scope">
<el-button type="primary" link @click="updateModel(scope.row)">编辑</el-button>
<el-button type="danger" link @click="deleteModel(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-collapse-item>
</el-collapse>
</div>
<!-- 新增/编辑对话框 -->
<el-dialog
v-model="dialogFormVisible"
:title="type === 'create' ? '新增模型' : '编辑模型'"
width="700px"
>
<el-form ref="modelForm" :model="formData" :rules="rules" label-width="120px">
<el-form-item label="模型名称" prop="name">
<el-input v-model="formData.name" placeholder="如: gpt-4" clearable />
</el-form-item>
<el-form-item label="显示名称" prop="display_name">
<el-input v-model="formData.display_name" placeholder="如: GPT-4" clearable />
</el-form-item>
<el-form-item label="提供商" prop="provider_id">
<el-select v-model="formData.provider_id" placeholder="请选择提供商" style="width: 100%">
<el-option
v-for="provider in providerList"
:key="provider.ID"
:label="provider.name"
:value="provider.ID"
/>
</el-select>
</el-form-item>
<el-form-item label="绑定预设" prop="preset_id">
<el-select
v-model="formData.preset_id"
placeholder="选择要绑定的预设(可选)"
clearable
style="width: 100%"
>
<el-option
v-for="preset in presetList"
:key="preset.ID"
:label="preset.name"
:value="preset.ID"
/>
</el-select>
</el-form-item>
<el-form-item label="Max Tokens" prop="max_tokens">
<el-input-number v-model="formData.max_tokens" :min="1" :max="200000" />
</el-form-item>
<el-form-item label="描述" prop="description">
<el-input
v-model="formData.description"
type="textarea"
:rows="3"
placeholder="模型描述"
/>
</el-form-item>
<el-form-item label="启用" prop="enabled">
<el-switch v-model="formData.enabled" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="closeDialog">取消</el-button>
<el-button type="primary" @click="enterDialog">确定</el-button>
</div>
</template>
</el-dialog>
<!-- 同步模型对话框 -->
<el-dialog v-model="syncDialogVisible" title="同步提供商模型" width="500px">
<el-form label-width="100px">
<el-form-item label="选择提供商">
<el-select v-model="syncProviderId" placeholder="请选择提供商" style="width: 100%">
<el-option
v-for="provider in providerList"
:key="provider.ID"
:label="provider.name"
:value="provider.ID"
/>
</el-select>
</el-form-item>
</el-form>
<el-alert
title="同步说明"
type="info"
:closable="false"
style="margin-top: 10px"
>
<p>将从提供商的 /v1/models 接口获取可用模型列表</p>
<p>新模型将被添加到系统中默认禁用状态</p>
<p>已存在的模型不会被修改</p>
</el-alert>
<template #footer>
<div class="dialog-footer">
<el-button @click="syncDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSync" :loading="syncing">开始同步</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
getAiModelList,
createAiModel,
updateAiModel,
deleteAiModel,
syncProviderModels
} from '@/api/aiModel'
import { getAiProviderList } from '@/api/aiProvider'
import { getAiPresetList } from '@/api/aiPreset'
const page = ref(1)
const pageSize = ref(10)
const total = ref(0)
const tableData = ref([])
const searchInfo = reactive({ name: '' })
const providerList = ref([])
const presetList = ref([])
const viewMode = ref('list')
const activeProviders = ref([])
const dialogFormVisible = ref(false)
const syncDialogVisible = ref(false)
const type = ref('')
const modelForm = ref(null)
const syncProviderId = ref(null)
const syncing = ref(false)
const formData = ref({
name: '',
display_name: '',
provider_id: null,
preset_id: null,
max_tokens: 4096,
description: '',
enabled: true
})
const rules = reactive({
name: [{ required: true, message: '请输入模型名称', trigger: 'blur' }],
provider_id: [{ required: true, message: '请选择提供商', trigger: 'change' }]
})
// 按提供商分组模型
const groupedModels = computed(() => {
const groups = {}
tableData.value.forEach(model => {
const providerId = model.provider?.ID
const providerName = model.provider?.name || '未知提供商'
if (!groups[providerId]) {
groups[providerId] = {
id: providerId,
name: providerName,
enabled: model.provider?.enabled || false,
models: []
}
}
groups[providerId].models.push(model)
})
return Object.values(groups).sort((a, b) => a.name.localeCompare(b.name))
})
const formatDate = (date) => {
if (!date) return ''
return new Date(date).toLocaleString('zh-CN')
}
const handleViewModeChange = () => {
if (viewMode.value === 'group') {
// 切换到分组视图时,默认展开第一个提供商
if (groupedModels.value.length > 0) {
activeProviders.value = [groupedModels.value[0].id]
}
}
}
const getTableData = async () => {
const table = await getAiModelList({
page: page.value,
pageSize: pageSize.value,
...searchInfo
})
if (table.code === 0) {
tableData.value = table.data.list
total.value = table.data.total
page.value = table.data.page
pageSize.value = table.data.pageSize
}
}
const loadProviders = async () => {
const res = await getAiProviderList({ page: 1, pageSize: 100 })
if (res.code === 0) {
providerList.value = res.data.list
}
}
const loadPresets = async () => {
const res = await getAiPresetList({ page: 1, pageSize: 100 })
if (res.code === 0) {
presetList.value = res.data.list
}
}
const onSubmit = () => {
page.value = 1
getTableData()
}
const onReset = () => {
searchInfo.name = ''
getTableData()
}
const handleCurrentChange = (val) => {
page.value = val
getTableData()
}
const handleSizeChange = (val) => {
pageSize.value = val
getTableData()
}
const handleStatusChange = async (row) => {
const res = await updateAiModel(row)
if (res.code === 0) {
ElMessage.success('状态更新成功')
} else {
row.enabled = !row.enabled
ElMessage.error('状态更新失败')
}
}
const openDialog = () => {
type.value = 'create'
dialogFormVisible.value = true
}
const closeDialog = () => {
dialogFormVisible.value = false
formData.value = {
name: '',
display_name: '',
provider_id: null,
preset_id: null,
max_tokens: 4096,
description: '',
enabled: true
}
}
const enterDialog = async () => {
modelForm.value.validate(async (valid) => {
if (valid) {
let res
if (type.value === 'create') {
res = await createAiModel(formData.value)
} else {
res = await updateAiModel(formData.value)
}
if (res.code === 0) {
ElMessage.success(type.value === 'create' ? '创建成功' : '更新成功')
closeDialog()
getTableData()
}
}
})
}
const updateModel = (row) => {
type.value = 'update'
formData.value = { ...row, provider_id: row.provider?.ID, preset_id: row.preset?.ID }
dialogFormVisible.value = true
}
const deleteModel = (row) => {
ElMessageBox.confirm('确定要删除该模型吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
const res = await deleteAiModel({ ID: row.ID })
if (res.code === 0) {
ElMessage.success('删除成功')
getTableData()
}
})
}
const openSyncDialog = () => {
syncProviderId.value = null
syncDialogVisible.value = true
}
const handleSync = async () => {
if (!syncProviderId.value) {
ElMessage.warning('请选择提供商')
return
}
syncing.value = true
try {
const res = await syncProviderModels({ ID: syncProviderId.value })
if (res.code === 0) {
ElMessage.success('同步成功')
syncDialogVisible.value = false
getTableData()
}
} finally {
syncing.value = false
}
}
getTableData()
loadProviders()
loadPresets()
</script>
<style scoped>
.gva-search-box {
padding: 20px;
background: #fff;
margin-bottom: 10px;
}
.gva-table-box {
padding: 20px;
background: #fff;
}
.gva-btn-list {
margin-bottom: 10px;
}
.gva-pagination {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
.provider-header {
display: flex;
align-items: center;
width: 100%;
}
.model-count {
color: #606266;
font-size: 14px;
margin-right: 10px;
}
.enabled-count {
color: #67c23a;
font-size: 12px;
}
:deep(.el-collapse-item__header) {
font-weight: 500;
font-size: 15px;
}
:deep(.el-collapse-item__content) {
padding: 10px 20px;
}
</style>

View File

@@ -0,0 +1,143 @@
<template>
<el-dialog v-model="visible" title="编辑提示词" width="1000px" @close="handleClose">
<div class="prompt-editor">
<div class="toolbar">
<el-button type="primary" size="small" @click="addPrompt">
<el-icon><Plus /></el-icon>
添加提示词
</el-button>
</div>
<el-table :data="prompts" style="width: 100%" max-height="500">
<el-table-column label="启用" width="60" fixed>
<template #default="scope">
<el-switch v-model="scope.row.enabled" size="small" />
</template>
</el-table-column>
<el-table-column label="标识符" prop="identifier" width="120" />
<el-table-column label="名称" prop="name" width="150" />
<el-table-column label="角色" width="100">
<template #default="scope">
<el-select v-model="scope.row.role" size="small">
<el-option label="system" value="system" />
<el-option label="user" value="user" />
<el-option label="assistant" value="assistant" />
</el-select>
</template>
</el-table-column>
<el-table-column label="内容" min-width="200">
<template #default="scope">
<el-input
v-model="scope.row.content"
type="textarea"
:rows="2"
placeholder="提示词内容"
/>
</template>
</el-table-column>
<el-table-column label="系统提示" width="90">
<template #default="scope">
<el-checkbox v-model="scope.row.system_prompt" />
</template>
</el-table-column>
<el-table-column label="标记" width="70">
<template #default="scope">
<el-checkbox v-model="scope.row.marker" />
</template>
</el-table-column>
<el-table-column label="注入位置" width="100">
<template #default="scope">
<el-input-number v-model="scope.row.injection_position" :min="0" :max="1" size="small" />
</template>
</el-table-column>
<el-table-column label="注入深度" width="100">
<template #default="scope">
<el-input-number v-model="scope.row.injection_depth" :min="0" size="small" />
</template>
</el-table-column>
<el-table-column label="操作" width="80" fixed="right">
<template #default="scope">
<el-button type="danger" link size="small" @click="removePrompt(scope.$index)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="handleSave">保存</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { ref, watch } from 'vue'
import { Plus } from '@element-plus/icons-vue'
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
prompts: {
type: Array,
default: () => []
}
})
const emit = defineEmits(['update:modelValue', 'save'])
const visible = ref(props.modelValue)
const prompts = ref([])
watch(() => props.modelValue, (val) => {
visible.value = val
if (val) {
prompts.value = JSON.parse(JSON.stringify(props.prompts || []))
}
})
const addPrompt = () => {
prompts.value.push({
identifier: `prompt_${Date.now()}`,
name: '新提示词',
role: 'system',
content: '',
system_prompt: false,
enabled: true,
marker: false,
injection_position: 0,
injection_depth: 4,
injection_order: 100,
injection_trigger: [],
forbid_overrides: false
})
}
const removePrompt = (index) => {
prompts.value.splice(index, 1)
}
const handleSave = () => {
emit('save', prompts.value)
handleClose()
}
const handleClose = () => {
emit('update:modelValue', false)
}
</script>
<style scoped>
.prompt-editor {
padding: 10px 0;
}
.toolbar {
margin-bottom: 15px;
}
</style>

View File

@@ -140,12 +140,36 @@
:closable="false"
style="margin-bottom: 20px"
/>
<el-input
v-model="importJson"
type="textarea"
:rows="15"
placeholder="请粘贴预设 JSON 内容"
/>
<el-tabs v-model="importTabActive">
<el-tab-pane label="文件上传" name="file">
<el-upload
ref="uploadRef"
:auto-upload="false"
:limit="1"
accept=".json"
drag
:on-change="handleFileChange"
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">
JSON 文件拖到此处<em>点击上传</em>
</div>
<template #tip>
<div class="el-upload__tip">
只支持 .json 格式的预设文件
</div>
</template>
</el-upload>
</el-tab-pane>
<el-tab-pane label="JSON 粘贴" name="json">
<el-input
v-model="importJson"
type="textarea"
:rows="15"
placeholder="请粘贴预设 JSON 内容"
/>
</el-tab-pane>
</el-tabs>
<template #footer>
<div class="dialog-footer">
<el-button @click="importDialogVisible = false">取消</el-button>
@@ -154,6 +178,13 @@
</template>
</el-dialog>
<!-- 提示词编辑器 -->
<PromptEditor
v-model="promptEditorVisible"
:prompts="formData.prompts"
@save="handlePromptSave"
/>
<!-- 查看预设对话框 -->
<el-dialog v-model="viewDialogVisible" title="预设详情" width="900px">
<el-descriptions :column="2" border>
@@ -212,13 +243,16 @@
<script setup>
import { ref, reactive } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { UploadFilled } from '@element-plus/icons-vue'
import {
getAiPresetList,
createAiPreset,
updateAiPreset,
deleteAiPreset,
importAiPreset
importAiPreset,
importAiPresetFile
} from '@/api/aiPreset'
import PromptEditor from './components/PromptEditor.vue'
const page = ref(1)
const pageSize = ref(10)
@@ -233,6 +267,10 @@ const type = ref('')
const presetForm = ref(null)
const importJson = ref('')
const viewData = ref({})
const importTabActive = ref('file')
const uploadRef = ref(null)
const uploadFile = ref(null)
const promptEditorVisible = ref(false)
const formData = ref({
name: '',
@@ -370,13 +408,34 @@ const deletePreset = (row) => {
const openImportDialog = () => {
importJson.value = ''
uploadFile.value = null
importTabActive.value = 'file'
importDialogVisible.value = true
}
const handleFileChange = (file) => {
uploadFile.value = file.raw
}
const handleImport = async () => {
try {
const data = JSON.parse(importJson.value)
const res = await importAiPreset(data)
let res
if (importTabActive.value === 'file') {
// 文件上传导入
if (!uploadFile.value) {
ElMessage.warning('请选择要上传的文件')
return
}
res = await importAiPresetFile(uploadFile.value)
} else {
// JSON 粘贴导入
if (!importJson.value.trim()) {
ElMessage.warning('请粘贴预设 JSON 内容')
return
}
const data = JSON.parse(importJson.value)
res = await importAiPreset(data)
}
if (res.code === 0) {
ElMessage.success('导入成功')
importDialogVisible.value = false
@@ -400,7 +459,12 @@ const exportPreset = (row) => {
}
const openPromptEditor = () => {
ElMessage.info('提示词编辑器功能开发中...')
promptEditorVisible.value = true
}
const handlePromptSave = (prompts) => {
formData.value.prompts = prompts
ElMessage.success('提示词已更新')
}
const openRegexEditor = () => {

View File

@@ -32,6 +32,14 @@
<el-table-column label="模型" prop="model" width="150" />
<el-table-column label="优先级" prop="priority" width="80" />
<el-table-column label="超时(秒)" prop="timeout" width="100" />
<el-table-column label="默认" width="80">
<template #default="scope">
<el-tag v-if="scope.row.is_default" type="success">默认</el-tag>
<el-button v-else type="primary" link size="small" @click="setDefault(scope.row)">
设为默认
</el-button>
</template>
</el-table-column>
<el-table-column label="状态" prop="enabled" width="80">
<template #default="scope">
<el-switch
@@ -107,6 +115,9 @@
<el-form-item label="启用" prop="enabled">
<el-switch v-model="formData.enabled" />
</el-form-item>
<el-form-item label="设为默认" prop="is_default">
<el-switch v-model="formData.is_default" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
@@ -148,7 +159,8 @@ const formData = ref({
priority: 0,
timeout: 60,
max_retries: 3,
enabled: true
enabled: true,
is_default: false
})
const rules = reactive({
@@ -237,7 +249,8 @@ const closeDialog = () => {
priority: 0,
timeout: 60,
max_retries: 3,
enabled: true
enabled: true,
is_default: false
}
}
@@ -279,6 +292,16 @@ const deleteProvider = (row) => {
})
}
const setDefault = async (row) => {
const res = await updateAiProvider({ ...row, is_default: true })
if (res.code === 0) {
ElMessage.success('已设为默认提供商')
getTableData()
} else {
ElMessage.error('设置失败')
}
}
getTableData()
</script>