This commit is contained in:
2026-08-05 11:32:05 +08:00
parent e54c3fff84
commit 13672fb217
10 changed files with 111 additions and 28 deletions
+6 -2
View File
@@ -29,7 +29,8 @@ func Tokenize(text string) string {
return strings.TrimSpace(sb.String())
}
// TokenizeQuery FTS5 查询串:每个词用双引号包裹(AND 语义),过滤 FTS5 特殊字符
// TokenizeQuery FTS5 查询串:过滤 FTS5 特殊字符与单字虚词,OR 语义。
// 中文问题分词后通常只有少数词与文档重合,AND 全命中会空召回,OR + BM25 排序更稳健。
func TokenizeQuery(text string) string {
tokens := seg.Cut(text, true)
var parts []string
@@ -41,7 +42,10 @@ func TokenizeQuery(text string) string {
if strings.ContainsAny(t, "\"*:()") {
continue
}
if len([]rune(t)) < 2 {
continue
}
parts = append(parts, "\""+t+"\"")
}
return strings.Join(parts, " ")
return strings.Join(parts, " OR ")
}
+24 -3
View File
@@ -11,7 +11,6 @@ import (
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gcache"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/gconv"
)
@@ -53,6 +52,27 @@ func (d *modelConfigDao) GetOne(ctx context.Context, id int64) (*entity.ModelCon
return &m, nil
}
// GetByName 按 model_name 查找已有配置(排除指定 id),供新配置继承端点与维度
func (d *modelConfigDao) GetByName(ctx context.Context, modelName string, excludeId int64) (*entity.ModelConfig, error) {
var m entity.ModelConfig
err := g.DB(consts.DbGroupSystem).Model(consts.TableNameModelConfig).Ctx(ctx).
Where("model_name", modelName).
Where("id <> ?", excludeId).
OrderAsc("id").
Limit(1).
Scan(&m)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
return nil, err
}
if m.Id == 0 {
return nil, nil
}
return &m, nil
}
func (d *modelConfigDao) List(ctx context.Context, modelType string) ([]*entity.ModelConfig, error) {
var list []*entity.ModelConfig
m := g.DB(consts.DbGroupSystem).Model(consts.TableNameModelConfig).Ctx(ctx).OrderAsc("id")
@@ -96,7 +116,8 @@ func (d *modelConfigDao) Update(ctx context.Context, data *entity.ModelConfig) e
if err != nil {
return err
}
_, _ = gcache.Remove(ctx, "model_config_GetOne_"+gconv.String(data.Id))
// 查询缓存挂在 DB 内部缓存实例上且键带 SelectCache: 前缀,全局 gcache.Remove 无效
_, _ = g.DB(consts.DbGroupSystem).GetCache().Remove(ctx, "SelectCache:model_config_GetOne_"+gconv.String(data.Id))
return nil
}
@@ -105,6 +126,6 @@ func (d *modelConfigDao) Delete(ctx context.Context, id int64) error {
if err != nil {
return err
}
_, _ = gcache.Remove(ctx, "model_config_GetOne_"+gconv.String(id))
_, _ = g.DB(consts.DbGroupSystem).GetCache().Remove(ctx, "SelectCache:model_config_GetOne_"+gconv.String(id))
return nil
}
+2 -2
View File
@@ -8,7 +8,6 @@ import (
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gcache"
)
var SystemConfig = &systemConfigDao{}
@@ -52,6 +51,7 @@ func (d *systemConfigDao) Set(ctx context.Context, key, value string) error {
if err != nil {
return err
}
_, _ = gcache.Remove(ctx, "system_config_Get_"+key)
// 查询缓存挂在 DB 内部缓存实例上且键带 SelectCache: 前缀,必须用 GetCache() 按完整键移除
_, _ = g.DB(consts.DbGroupSystem).GetCache().Remove(ctx, "SelectCache:system_config_Get_"+key)
return nil
}
+9
View File
@@ -21,6 +21,15 @@ func (s *modelConfigService) List(ctx context.Context, modelType string) ([]*ent
}
func (s *modelConfigService) Save(ctx context.Context, m *entity.ModelConfig) (int64, error) {
// 留空的接口地址/维度按表中已有同名模型记录继承(用户显式填写的值优先,不覆盖)
if other, err := dao.ModelConfig.GetByName(ctx, m.ModelName, m.Id); err == nil && other != nil {
if m.EndpointUrl == "" && other.EndpointUrl != "" {
m.EndpointUrl = other.EndpointUrl
}
if m.ModelType == consts.ModelTypeEmbedding && m.Dimension <= 0 && other.Dimension > 0 {
m.Dimension = other.Dimension
}
}
if m.Dimension <= 0 {
m.Dimension = consts.DefaultEmbeddingDim
}
+5
View File
@@ -1,5 +1,6 @@
import axios from 'axios'
import { ElMessage } from 'element-plus'
import { useAuthStore } from '../stores/auth.js'
const request = axios.create({
timeout: 300000
@@ -14,6 +15,10 @@ request.interceptors.request.use(config => {
})
function redirectLogin() {
// 同时清空 Pinia store,否则路由守卫仍视为已登录,会拦回首页形成死循环
try {
useAuthStore().logout()
} catch { /* store 未初始化时忽略 */ }
localStorage.removeItem('token')
window.location.href = '/#/login'
}
+2 -1
View File
@@ -73,7 +73,8 @@ const msgBox = ref(null)
onMounted(async () => {
try {
datasets.value = await listDatasets()
const d = await listDatasets()
if (d && d.list) datasets.value = d.list
if (datasets.value.length && !datasetId.value) {
datasetId.value = datasets.value[0].id
}
+31 -12
View File
@@ -72,7 +72,7 @@
</template>
<script setup>
import { computed, onMounted, ref } from 'vue'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ArrowLeft, UploadFilled } from '@element-plus/icons-vue'
@@ -123,21 +123,12 @@ function formatSize(bytes) {
return (bytes / 1024 / 1024).toFixed(1) + ' MB'
}
onMounted(async () => {
try {
const ds = await listDatasets()
const cur = ds.find(x => x.id === datasetId.value)
datasetName.value = cur ? cur.name : '数据集'
} catch { /* 忽略 */ }
await load()
})
async function load() {
loading.value = true
try {
const d = await listDocuments({ dataset_id: datasetId.value, page: page.value, page_size: pageSize })
documents.value = d.list
total.value = d.total
documents.value = d.list || []
total.value = d.total || 0
} finally {
loading.value = false
}
@@ -152,11 +143,39 @@ async function doUpload({ file }) {
await uploadDocument(formData)
ElMessage.success('上传成功,正在解析')
await load()
startPolling()
} finally {
uploading.value = false
}
}
// 有待处理/解析中的文档时轮询刷新状态,全部完成即停止
let pollTimer = null
async function startPolling() {
stopPolling()
pollTimer = setInterval(async () => {
await load()
if (!documents.value.some(d => d.status === 0 || d.status === 1)) stopPolling()
}, 3000)
}
function stopPolling() {
if (pollTimer) {
clearInterval(pollTimer)
pollTimer = null
}
}
onMounted(async () => {
try {
const dsRes = await listDatasets()
const ds = dsRes && dsRes.list ? dsRes.list : []
const cur = ds.find(x => x.id === datasetId.value)
datasetName.value = cur ? cur.name : '数据集'
} catch { /* 忽略 */ }
await load()
if (documents.value.some(d => d.status === 0 || d.status === 1)) startPolling()
})
onUnmounted(stopPolling)
async function remove(row) {
try {
await ElMessageBox.confirm(`删除文档「${row.filename}」?将同时删除分块、向量与全文索引。`, '删除确认', { type: 'warning' })
+4 -2
View File
@@ -60,14 +60,16 @@ const form = ref({ id: 0, name: '', description: '', embedding_cfg_id: 0 })
onMounted(async () => {
await load()
try {
embedders.value = await listModelConfigs('embedding')
const m = await listModelConfigs('embedding')
if (m && m.list) embedders.value = m.list
} catch { /* 忽略 */ }
})
async function load() {
loading.value = true
try {
datasets.value = await listDatasets()
const d = await listDatasets()
if (d && d.list) datasets.value = d.list
} finally {
loading.value = false
}
+2 -1
View File
@@ -58,7 +58,8 @@ const relationPageSize = 20
onMounted(async () => {
try {
datasets.value = await listDatasets()
const d = await listDatasets()
if (d && d.list) datasets.value = d.list
if (datasets.value.length) datasetId.value = datasets.value[0].id
} catch { /* 忽略 */ }
await load()
+26 -5
View File
@@ -74,7 +74,7 @@
</el-radio-group>
</el-form-item>
<el-form-item label="模型名" required>
<el-input v-model="modelForm.model_name" placeholder="如:qwen2.5:7b" />
<el-input v-model="modelForm.model_name" placeholder="如:qwen2.5:7b" @change="inheritFromExisting" />
</el-form-item>
<el-form-item label="接口地址">
<el-input v-model="modelForm.endpoint_url" placeholder="如:http://localhost:11434/v1" />
@@ -104,6 +104,7 @@ import { getSystemConfig, updateSystemConfig, regenerateToken, getToken } from '
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([])
@@ -121,10 +122,11 @@ const tokenRegenerating = ref(false)
onMounted(async () => {
await loadModels()
try { chatModels.value = await listModelConfigs('chat') } catch { /* 忽略 */ }
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()
datasets.value = d
if (d && d.list) datasets.value = d.list
} catch { /* 忽略 */ }
await loadDefaults()
await loadToken()
@@ -133,7 +135,8 @@ onMounted(async () => {
async function loadModels() {
modelLoading.value = true
try {
models.value = await listModelConfigs(modelType.value)
const m = await listModelConfigs(modelType.value)
if (m && m.list) models.value = m.list
} finally {
modelLoading.value = false
}
@@ -161,6 +164,24 @@ function openModelDialog(row) {
modelDialog.value = true
}
// 输入模型名后,从表中已有同名配置带出接口地址与维度(仅填空字段,不覆盖手填值)
function inheritFromExisting() {
const name = modelForm.value.model_name.trim()
if (!name) return
const other = allModels.value.find(x => x.model_name === name && x.id !== modelForm.value.id)
if (!other) return
let filled = false
if (!modelForm.value.endpoint_url && other.endpoint_url) {
modelForm.value.endpoint_url = other.endpoint_url
filled = true
}
if (modelForm.value.model_type === 'embedding' && (modelForm.value.dimension <= 0 || modelForm.value.dimension === 1024) && other.dimension > 0) {
modelForm.value.dimension = other.dimension
filled = true
}
if (filled) ElMessage.info(`已按已有配置「${other.name}」带出接口地址${modelForm.value.model_type === 'embedding' ? '与维度' : ''}`)
}
async function saveModel() {
if (!modelForm.value.name.trim() || !modelForm.value.model_name.trim()) {
ElMessage.warning('请填写名称与模型名')
@@ -172,7 +193,7 @@ async function saveModel() {
ElMessage.success('保存成功')
modelDialog.value = false
await loadModels()
if (modelType.value === 'chat') chatModels.value = await listModelConfigs('chat')
if (modelType.value === 'chat') { const m = await listModelConfigs('chat'); if (m && m.list) chatModels.value = m.list }
} finally {
modelSaving.value = false
}