This commit is contained in:
2026-08-05 13:39:37 +08:00
parent 13672fb217
commit b11ed5b433
40 changed files with 503 additions and 434 deletions
-16
View File
@@ -3,19 +3,3 @@ import request from './request.js'
export function login(data) {
return request.post('/system-config/login', data)
}
export function getSystemConfig() {
return request.get('/system-config')
}
export function updateSystemConfig(data) {
return request.put('/system-config', data)
}
export function regenerateToken() {
return request.post('/system-config/regenerate-token')
}
export function getToken() {
return request.get('/system-config/token')
}
+4
View File
@@ -15,3 +15,7 @@ export function deleteModelConfig(id) {
export function testModelConfig(id) {
return request.post('/model-config/test', { id })
}
export function setDefaultModelConfig(id) {
return request.post('/model-config/set-default', { id })
}
+4 -2
View File
@@ -89,7 +89,8 @@ function scrollBottom() {
}
async function refreshConversations() {
conversations.value = await listConversations()
const c = await listConversations()
conversations.value = (c && c.list) || []
}
function newConversation() {
@@ -100,7 +101,8 @@ function newConversation() {
async function openConversation(c) {
currentConvId.value = c.id
if (c.dataset_id) datasetId.value = c.dataset_id
const list = await listMessages(c.id)
const r = await listMessages(c.id)
const list = (r && r.list) || []
messages.value = list.map(m => {
let citations = []
if (m.citations) {
+1 -1
View File
@@ -62,7 +62,7 @@
<el-dialog v-model="editDialog" title="编辑分块" width="640px">
<el-input v-model="editContent" type="textarea" :rows="10" />
<div class="edit-tip">保存后重新分词并向量化向量模型未绑定时仅更新全文索引</div>
<div class="edit-tip">保存后重新分词并向量化</div>
<template #footer>
<el-button @click="editDialog = false">取消</el-button>
<el-button type="primary" :loading="editSaving" @click="saveChunk">保存</el-button>
+67 -7
View File
@@ -9,13 +9,17 @@
<el-table-column label="向量模型" width="200">
<template #default="{ row }">
<span v-if="row.embedding_cfg_id">{{ embeddingName(row.embedding_cfg_id) }}</span>
<el-tag v-else size="small" type="info">绑定仅全文检索</el-tag>
<el-tag v-else size="small" type="info">设置</el-tag>
</template>
</el-table-column>
<el-table-column label="分块" width="140">
<template #default="{ row }">{{ STRATEGY_NAMES[row.chunk_strategy || 'title'] }} {{ row.chunk_size || 800 }}{{ row.chunk_strategy === 'semantic' ? '' : ' / ' + (row.chunk_overlap ?? 150) }}</template>
</el-table-column>
<el-table-column prop="created_at" label="创建时间" width="170" />
<el-table-column label="操作" width="150" fixed="right">
<el-table-column label="操作" width="190" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="$router.push('/datasets/' + row.id)">详情</el-button>
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
<el-button link type="danger" @click="remove(row)">删除</el-button>
</template>
</el-table-column>
@@ -29,11 +33,27 @@
<el-form-item label="描述">
<el-input v-model="form.description" type="textarea" :rows="2" />
</el-form-item>
<el-form-item label="向量模型">
<el-select v-model="form.embedding_cfg_id" clearable placeholder="不绑定则仅全文检索" style="width: 100%">
<el-form-item label="向量模型" required>
<el-select v-model="form.embedding_cfg_id" placeholder="请选择向量模型" style="width: 100%">
<el-option v-for="m in embedders" :key="m.id" :label="m.name" :value="m.id" />
</el-select>
</el-form-item>
<el-form-item label="分块策略">
<el-select v-model="form.chunk_strategy" style="width: 100%">
<el-option label="标题分块(推荐)" value="title" />
<el-option label="递归字符分块" value="recursive" />
<el-option label="语义分块" value="semantic" />
</el-select>
<div class="ds-tip">标题分块保留标题与段落结构递归字符分块按分隔符切分通用文本语义分块按语义相似度切分质量更高但解析更慢且需绑定向量模型</div>
</el-form-item>
<el-form-item label="分块大小">
<el-input-number v-model="form.chunk_size" :min="50" :max="5000" :step="50" style="width: 100%" />
<div class="ds-tip">{{ form.chunk_strategy === 'semantic' ? '语义分块按句子相似度切分,此值决定最小分块大小(低于该值的候选块会继续合并)' : '每块最大字数,超长段落自动按句号/换行切分' }}</div>
</el-form-item>
<el-form-item v-if="form.chunk_strategy !== 'semantic'" label="重叠字数">
<el-input-number v-model="form.chunk_overlap" :min="0" :max="500" :step="10" style="width: 100%" />
<div class="ds-tip">相邻分块间的重叠字数用于保持上下文连贯</div>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
@@ -49,13 +69,17 @@ import { ElMessage, ElMessageBox } from 'element-plus'
import { listDatasets, saveDataset, deleteDataset } from '../api/dataset.js'
import { listModelConfigs } from '../api/model_config.js'
const STRATEGY_NAMES = { title: '标题', recursive: '递归', semantic: '语义' }
const datasets = ref([])
const embedders = ref([])
const loading = ref(false)
const saving = ref(false)
const dialogVisible = ref(false)
const editing = ref(false)
const form = ref({ id: 0, name: '', description: '', embedding_cfg_id: 0 })
let editingRowCfgId = 0
let editingRowChunk = { chunk_size: 0, chunk_overlap: 0, chunk_strategy: '' }
const form = ref({ id: 0, name: '', description: '', embedding_cfg_id: 0, chunk_size: 800, chunk_overlap: 150, chunk_strategy: 'title' })
onMounted(async () => {
await load()
@@ -82,7 +106,22 @@ function embeddingName(id) {
function openCreate() {
editing.value = false
form.value = { id: 0, name: '', description: '', embedding_cfg_id: 0 }
editingRowCfgId = 0
editingRowChunk = { chunk_size: 0, chunk_overlap: 0, chunk_strategy: '' }
// 默认选中默认向量模型,其次第一个
const def = embedders.value.find(x => x.is_default === 1) || embedders.value[0]
form.value = { id: 0, name: '', description: '', embedding_cfg_id: def?.id || 0, chunk_size: 800, chunk_overlap: 150, chunk_strategy: 'title' }
dialogVisible.value = true
}
function openEdit(row) {
editing.value = true
editingRowCfgId = row.embedding_cfg_id
editingRowChunk = { chunk_size: row.chunk_size || 800, chunk_overlap: row.chunk_overlap ?? 150, chunk_strategy: row.chunk_strategy || 'title' }
form.value = {
id: row.id, name: row.name, description: row.description, embedding_cfg_id: row.embedding_cfg_id,
chunk_size: editingRowChunk.chunk_size, chunk_overlap: editingRowChunk.chunk_overlap, chunk_strategy: editingRowChunk.chunk_strategy,
}
dialogVisible.value = true
}
@@ -91,10 +130,26 @@ async function save() {
ElMessage.warning('请输入数据集名称')
return
}
if (!form.value.embedding_cfg_id) {
ElMessage.warning('请选择向量模型(数据集必须绑定向量模型)')
return
}
const modelChanged = editing.value && form.value.embedding_cfg_id !== editingRowCfgId
const chunkChanged = editing.value && (form.value.chunk_size !== editingRowChunk.chunk_size || form.value.chunk_overlap !== editingRowChunk.chunk_overlap || form.value.chunk_strategy !== editingRowChunk.chunk_strategy)
if (modelChanged || chunkChanged) {
const changed = []
if (modelChanged) changed.push('向量模型')
if (chunkChanged) changed.push('分块策略')
try {
await ElMessageBox.confirm(`${changed.join('与')}已变更,保存后将重新处理该数据集下所有文档(异步进行,可在文档列表查看进度)。是否继续?`, '重新处理文档', { type: 'warning' })
} catch {
return
}
}
saving.value = true
try {
await saveDataset(form.value)
ElMessage.success('保存成功')
ElMessage.success(modelChanged || chunkChanged ? '已保存,正在重新处理全部文档' : '保存成功')
dialogVisible.value = false
await load()
} finally {
@@ -120,4 +175,9 @@ async function remove(row) {
.ds-head {
margin-bottom: 12px;
}
.ds-tip {
font-size: 12px;
color: #909399;
line-height: 1.4;
}
</style>
+37 -130
View File
@@ -1,66 +1,33 @@
<template>
<div class="settings-page">
<el-tabs v-model="tab">
<el-tab-pane label="模型配置" name="models">
<div class="section-head">
<el-radio-group v-model="modelType" @change="loadModels">
<el-radio-button value="chat">对话模型</el-radio-button>
<el-radio-button value="embedding">向量模型</el-radio-button>
</el-radio-group>
<el-button type="primary" @click="openModelDialog()">新增配置</el-button>
</div>
<el-table :data="models" v-loading="modelLoading">
<el-table-column prop="name" label="名称" min-width="140" />
<el-table-column prop="model_name" label="模型" min-width="160" show-overflow-tooltip />
<el-table-column prop="endpoint_url" label="接口地址" min-width="220" show-overflow-tooltip />
<el-table-column prop="dimension" label="维度" width="80">
<template #default="{ row }">{{ row.model_type === 'embedding' ? row.dimension : '-' }}</template>
</el-table-column>
<el-table-column label="操作" width="160" fixed="right">
<template #default="{ row }">
<el-button link type="primary" :loading="testingId === row.id" @click="test(row)">测试</el-button>
<el-button link type="primary" @click="openModelDialog(row)">编辑</el-button>
<el-button link type="danger" @click="removeModel(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="默认设置" name="defaults">
<el-form label-width="120px" style="max-width: 480px">
<el-form-item label="默认对话模型">
<el-select v-model="defaults.default_chat_model" clearable placeholder="选择默认对话模型" style="width: 100%">
<el-option v-for="m in chatModels" :key="m.id" :label="m.name" :value="m.id" />
</el-select>
</el-form-item>
<el-form-item label="默认数据集">
<el-select v-model="defaults.default_dataset" clearable placeholder="选择默认数据集" style="width: 100%">
<el-option v-for="d in datasets" :key="d.id" :label="d.name" :value="d.id" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="defaultSaving" @click="saveDefaults">保存</el-button>
</el-form-item>
</el-form>
<div class="defaults-tip">默认对话模型用于知识图谱抽取与问答默认数据集用于问答页初始选择</div>
</el-tab-pane>
<el-tab-pane label="访问令牌" name="token">
<el-form label-width="120px" style="max-width: 520px">
<el-form-item label="当前令牌">
<el-input :model-value="currentToken" readonly>
<template #append>
<el-button @click="copyToken">复制</el-button>
</template>
</el-input>
</el-form-item>
<el-form-item>
<el-button type="danger" plain :loading="tokenRegenerating" @click="regenerate">重新生成令牌</el-button>
</el-form-item>
</el-form>
<div class="defaults-tip">重新生成后旧令牌立即失效当前会话将被登出需使用新令牌重新登录</div>
</el-tab-pane>
</el-tabs>
<div class="section-head">
<el-radio-group v-model="modelType" @change="loadModels">
<el-radio-button value="chat">对话模型</el-radio-button>
<el-radio-button value="embedding">向量模型</el-radio-button>
</el-radio-group>
<el-button type="primary" @click="openModelDialog()">新增配置</el-button>
</div>
<el-table :data="models" v-loading="modelLoading">
<el-table-column prop="name" label="名称" min-width="140" />
<el-table-column prop="model_name" label="模型" min-width="160" show-overflow-tooltip />
<el-table-column prop="endpoint_url" label="接口地址" min-width="220" show-overflow-tooltip />
<el-table-column prop="dimension" label="维度" width="80">
<template #default="{ row }">{{ row.model_type === 'embedding' ? row.dimension : '-' }}</template>
</el-table-column>
<el-table-column label="默认" width="110" align="center">
<template #default="{ row }">
<el-tag v-if="row.is_default === 1" type="success" size="small">默认</el-tag>
<el-button v-else link type="primary" @click="setDefault(row)">设默认</el-button>
</template>
</el-table-column>
<el-table-column label="操作" width="160" fixed="right">
<template #default="{ row }">
<el-button link type="primary" :loading="testingId === row.id" @click="test(row)">测试</el-button>
<el-button link type="primary" @click="openModelDialog(row)">编辑</el-button>
<el-button link type="danger" @click="removeModel(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog v-model="modelDialog" :title="modelForm.id ? '编辑模型配置' : '新增模型配置'" width="520px">
<el-form :model="modelForm" label-width="90px">
@@ -97,39 +64,21 @@
<script setup>
import { onMounted, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { listModelConfigs, saveModelConfig, deleteModelConfig, testModelConfig } from '../api/model_config.js'
import { listDatasets } from '../api/dataset.js'
import { getSystemConfig, updateSystemConfig, regenerateToken, getToken } from '../api/auth.js'
import { listModelConfigs, saveModelConfig, deleteModelConfig, testModelConfig, setDefaultModelConfig } from '../api/model_config.js'
const tab = ref('models')
const modelType = ref('chat')
const models = ref([])
const allModels = ref([])
const modelLoading = ref(false)
const testingId = ref(0)
const chatModels = ref([])
const modelDialog = ref(false)
const modelSaving = ref(false)
const modelForm = ref({ id: 0, name: '', model_type: 'chat', model_name: '', endpoint_url: '', api_key: '', dimension: 1024 })
const defaults = ref({ default_chat_model: 0, default_dataset: 0 })
const datasets = ref([])
const defaultSaving = ref(false)
const currentToken = ref('')
const tokenRegenerating = ref(false)
onMounted(async () => {
await loadModels()
try { const all = await listModelConfigs(''); if (all && all.list) allModels.value = all.list } catch { /* 忽略 */ }
try { const m = await listModelConfigs('chat'); if (m && m.list) chatModels.value = m.list } catch { /* 忽略 */ }
try {
const d = await listDatasets()
if (d && d.list) datasets.value = d.list
} catch { /* 忽略 */ }
await loadDefaults()
await loadToken()
})
async function loadModels() {
@@ -142,19 +91,6 @@ async function loadModels() {
}
}
async function loadDefaults() {
try {
defaults.value = await getSystemConfig()
} catch { /* 忽略 */ }
}
async function loadToken() {
try {
const res = await getToken()
currentToken.value = res.token
} catch { /* 忽略 */ }
}
function openModelDialog(row) {
if (row) {
modelForm.value = { ...row }
@@ -193,7 +129,6 @@ async function saveModel() {
ElMessage.success('保存成功')
modelDialog.value = false
await loadModels()
if (modelType.value === 'chat') { const m = await listModelConfigs('chat'); if (m && m.list) chatModels.value = m.list }
} finally {
modelSaving.value = false
}
@@ -212,6 +147,14 @@ async function removeModel(row) {
} catch { /* request.js 已提示 */ }
}
async function setDefault(row) {
try {
await setDefaultModelConfig(row.id)
ElMessage.success(`已将「${row.name}」设为默认${row.model_type === 'chat' ? '对话模型' : '向量模型'}`)
await loadModels()
} catch { /* request.js 已提示 */ }
}
async function test(row) {
testingId.value = row.id
try {
@@ -225,38 +168,6 @@ async function test(row) {
testingId.value = 0
}
}
async function saveDefaults() {
defaultSaving.value = true
try {
await updateSystemConfig(defaults.value)
ElMessage.success('已保存')
} finally {
defaultSaving.value = false
}
}
async function regenerate() {
try {
await ElMessageBox.confirm('重新生成后旧令牌立即失效,确认继续?', '重新生成令牌', { type: 'warning' })
} catch {
return
}
tokenRegenerating.value = true
try {
const res = await regenerateToken()
currentToken.value = res.token
ElMessage.success('新令牌已生成,请用新令牌重新登录')
} finally {
tokenRegenerating.value = false
}
}
function copyToken() {
if (!currentToken.value) return
navigator.clipboard?.writeText(currentToken.value)
ElMessage.success('已复制')
}
</script>
<style scoped>
@@ -269,8 +180,4 @@ function copyToken() {
align-items: center;
margin-bottom: 12px;
}
.defaults-tip {
font-size: 12px;
color: #909399;
}
</style>