1
This commit is contained in:
@@ -75,14 +75,13 @@
|
||||
| 库 | 文件 | 内容 |
|
||||
|---|---|---|
|
||||
| business | `data/business.db` | 数据集、文档、分块、向量(vec0)、全文索引(FTS5)、解析任务、知识图谱、合同标注 |
|
||||
| system | `data/system.db` | 全局设置(app_config)与模型配置 |
|
||||
| system | `data/system.db` | 模型配置 |
|
||||
| chat | `data/chat.db` | 会话与消息 |
|
||||
|
||||
表清单(13 张实体表 + 2 张虚拟表):
|
||||
表清单(12 张实体表 + 2 张虚拟表):
|
||||
|
||||
| 表名 | 库 | 类型 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `app_config` | system | 实体 | 全局键值配置(分块默认参数) |
|
||||
| `model_config` | system | 实体 | 模型配置(chat / embedding,is_default 标记默认) |
|
||||
| `kb_dataset` | business | 实体 | 数据集(绑定向量模型、分块参数) |
|
||||
| `kb_document` | business | 实体 | 文档(6 态状态机) |
|
||||
@@ -202,6 +201,7 @@ ollama pull nomic-embed-text
|
||||
| `vector.dim` | 向量维度,须与向量模型一致 |
|
||||
| `database.cache.ttl` | DAO 查询缓存秒数 |
|
||||
| `chat.timeout` / `chat.max_retries` | 对话模型 API 超时(秒)/ 失败重试次数 |
|
||||
| `chunk.default_size` / `chunk.default_overlap` / `react.default_rounds` | 数据集分块大小/重叠/智能体轮次默认值(新建数据集传 0/-1 时使用) |
|
||||
| `pool.*` | 各并行点协程池并发度(缺失或非法回退代码内默认值):`kg_extract`(4) 逐 chunk 图谱抽取、`annotation_clause`(4) 合同逐条款标注、`annotation_dataset`(8) 条款内逐数据集召回、`chat`(4) 问答检索与图增强并行、`chat_retrieve`(4) 检索向量与全文并行 |
|
||||
|
||||
## API 概览
|
||||
@@ -210,7 +210,7 @@ ollama pull nomic-embed-text
|
||||
|
||||
| 分组 | 接口 |
|
||||
|---|---|
|
||||
| `/system-config` | `POST /login`(访问令牌登录)、`GET /settings`、`POST /save-settings`(分块默认值) |
|
||||
| `/system-config` | `POST /login`(访问令牌登录) |
|
||||
| `/model-config` | `GET /list`、`POST /save`、`POST /delete`、`POST /set-default`、`POST /test`(连通性测试) |
|
||||
| `/dataset` | `GET /list`、`POST /save`(新建/编辑)、`POST /delete`(有文档拒绝) |
|
||||
| `/document` | `POST /upload`(multipart)、`GET /list`、`GET /detail`、`POST /delete`、`POST /reembed`(仅重算向量) |
|
||||
@@ -226,12 +226,12 @@ ollama pull nomic-embed-text
|
||||
| 页面 | 功能 |
|
||||
|---|---|
|
||||
| 登录页 | 输入访问令牌登录,无注册入口 |
|
||||
| 数据集列表 | 新建/编辑/删除(绑定向量模型必选、分块参数带出全局默认);变更模型/分块 → 确认弹窗提示重新处理 |
|
||||
| 数据集列表 | 新建/编辑/删除(绑定向量模型必选、分块参数传 0/-1 使用 config.yml 全局默认);变更模型/分块 → 确认弹窗提示重新处理 |
|
||||
| 数据集详情 | 文档上传(拖拽)、6 态解析状态/分块数/失败重试/「图谱未构建」提示、分块预览与编辑 |
|
||||
| 图谱页 | 实体/关系列表 + eCharts 力导向图 |
|
||||
| 合同标注页 | 选语料数据集 → 上传合同 → 任务进度(3s 轮询)→ 条款-标注对照抽屉 → 导出标注版 HTML |
|
||||
| 问答页 | 会话列表 + 知识库下拉,SSE 流式渲染,引用折叠面板 |
|
||||
| 设置页 | 分块默认值 + 模型配置 CRUD(chat/embedding 两个 tab)、连通性测试、设置默认 |
|
||||
| 设置页 | 模型配置 CRUD(chat/embedding 两个 tab)、连通性测试、设置默认 |
|
||||
|
||||
## 项目结构
|
||||
|
||||
|
||||
@@ -29,6 +29,13 @@ chat:
|
||||
timeout: 600 # 对话模型API请求超时时间(秒)
|
||||
max_retries: 3 # 请求失败最大重试次数
|
||||
|
||||
# 数据集默认值(新建数据集传 0/-1 时使用,数据集内可覆盖)
|
||||
chunk:
|
||||
default_size: 800 # 分块最大字数
|
||||
default_overlap: 150 # 分块重叠字数
|
||||
react:
|
||||
default_rounds: 0 # 智能体轮次(0=关闭)
|
||||
|
||||
# 协程池配置(各并行点并发度,缺失或 <1 时回退代码内默认值)
|
||||
pool:
|
||||
kg_extract: 4 # 知识图谱:逐 chunk LLM 抽取
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -26,11 +26,6 @@ const (
|
||||
MaxReactRounds = 10 // ReAct 轮次上限
|
||||
ToolResultMaxChars = 1500 // search 工具返回的单条 chunk 截断字数(控制上下文体积)
|
||||
|
||||
// 全局设置键(app_config 表)
|
||||
SettingsKeyChunkSize = "chunk_default_size"
|
||||
SettingsKeyChunkOverlap = "chunk_default_overlap"
|
||||
SettingsKeyReactRounds = "react_default_rounds"
|
||||
|
||||
ParsePollIntervalSeconds = 5 // 解析/标注任务轮询间隔
|
||||
|
||||
EmbedBatchSize = 16 // 单次向量化请求的文本批量
|
||||
|
||||
@@ -2,7 +2,6 @@ package consts
|
||||
|
||||
const (
|
||||
TableNameModelConfig = "model_config"
|
||||
TableNameAppConfig = "app_config"
|
||||
TableNameDataset = "kb_dataset"
|
||||
TableNameDocument = "kb_document"
|
||||
TableNameChunk = "kb_chunk"
|
||||
|
||||
@@ -18,18 +18,3 @@ func (c *systemConfig) Login(ctx context.Context, req *dto.LoginReq) (res *dto.L
|
||||
}
|
||||
return &dto.LoginRes{Token: token}, nil
|
||||
}
|
||||
|
||||
func (c *systemConfig) GetSettings(ctx context.Context, _ *dto.GetSettingsReq) (*dto.GetSettingsRes, error) {
|
||||
size, overlap, rounds, err := service.SystemConfigService.GetSettings(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dto.GetSettingsRes{ChunkSize: size, ChunkOverlap: overlap, ReactRounds: rounds}, nil
|
||||
}
|
||||
|
||||
func (c *systemConfig) SaveSettings(ctx context.Context, req *dto.SaveSettingsReq) (*dto.SaveSettingsRes, error) {
|
||||
if err := service.SystemConfigService.SaveSettings(ctx, req.ChunkSize, req.ChunkOverlap, req.ReactRounds); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dto.SaveSettingsRes{}, nil
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"rag-local/kb/consts"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
var AppConfig = &appConfigDao{}
|
||||
|
||||
type appConfigDao struct{}
|
||||
|
||||
func init() {
|
||||
ctx := context.Background()
|
||||
_, err := g.DB(consts.DbGroupSystem).Exec(ctx, `CREATE TABLE IF NOT EXISTS `+consts.TableNameAppConfig+` (
|
||||
cfg_key TEXT PRIMARY KEY,
|
||||
cfg_value TEXT NOT NULL DEFAULT '',
|
||||
updated_at DATETIME DEFAULT (datetime('now','localtime'))
|
||||
)`)
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "create app_config table failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// GetInt 读取全局设置值,未设置或非法时返回默认值 def
|
||||
func (d *appConfigDao) GetInt(ctx context.Context, key string, def int) int {
|
||||
r, err := g.DB(consts.DbGroupSystem).Ctx(ctx).GetValue(ctx,
|
||||
"SELECT cfg_value FROM "+consts.TableNameAppConfig+" WHERE cfg_key=?", key)
|
||||
if err != nil || r.IsEmpty() {
|
||||
return def
|
||||
}
|
||||
return r.Int()
|
||||
}
|
||||
|
||||
// SetInt 写入全局设置值(UPSERT)
|
||||
func (d *appConfigDao) SetInt(ctx context.Context, key string, v int) error {
|
||||
_, err := g.DB(consts.DbGroupSystem).Ctx(ctx).Exec(ctx,
|
||||
"INSERT INTO "+consts.TableNameAppConfig+"(cfg_key, cfg_value) VALUES(?, ?) "+
|
||||
"ON CONFLICT(cfg_key) DO UPDATE SET cfg_value=excluded.cfg_value, updated_at=datetime('now','localtime')",
|
||||
key, v)
|
||||
return err
|
||||
}
|
||||
@@ -10,22 +10,3 @@ type LoginReq struct {
|
||||
type LoginRes struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type GetSettingsReq struct {
|
||||
g.Meta `path:"/settings" method:"get" tags:"系统配置" summary:"获取全局设置"`
|
||||
}
|
||||
|
||||
type GetSettingsRes struct {
|
||||
ChunkSize int `json:"chunk_size"` // 默认分块大小
|
||||
ChunkOverlap int `json:"chunk_overlap"` // 默认重叠字数
|
||||
ReactRounds int `json:"react_rounds"` // 默认智能体轮次(0=关闭)
|
||||
}
|
||||
|
||||
type SaveSettingsReq struct {
|
||||
g.Meta `path:"/save-settings" method:"post" tags:"系统配置" summary:"保存全局设置"`
|
||||
ChunkSize int `json:"chunk_size"`
|
||||
ChunkOverlap int `json:"chunk_overlap"`
|
||||
ReactRounds int `json:"react_rounds"`
|
||||
}
|
||||
|
||||
type SaveSettingsRes struct{}
|
||||
|
||||
@@ -34,6 +34,16 @@ func (s *datasetService) Save(ctx context.Context, m *entity.Dataset) (int64, er
|
||||
return 0, gerror.Newf("召回数量需在 -1~%d 之间(-1=尽量多,0=全局默认)", consts.MaxRecallTopK)
|
||||
}
|
||||
}
|
||||
// 0/-1 = 使用 config.yml 全局默认,落库前回填为实际值(Insert/Update 一致)
|
||||
if m.ChunkSize <= 0 {
|
||||
m.ChunkSize = g.Cfg().MustGet(ctx, "chunk.default_size", consts.DefaultChunkSize).Int()
|
||||
}
|
||||
if m.ChunkOverlap < 0 {
|
||||
m.ChunkOverlap = g.Cfg().MustGet(ctx, "chunk.default_overlap", consts.DefaultChunkOverlap).Int()
|
||||
}
|
||||
if m.ReactRounds < 0 {
|
||||
m.ReactRounds = g.Cfg().MustGet(ctx, "react.default_rounds", 0).Int()
|
||||
}
|
||||
if m.Id > 0 {
|
||||
old, err := dao.Dataset.GetOne(ctx, m.Id)
|
||||
if err != nil {
|
||||
@@ -56,12 +66,6 @@ func (s *datasetService) Save(ctx context.Context, m *entity.Dataset) (int64, er
|
||||
}
|
||||
return m.Id, nil
|
||||
}
|
||||
if m.ChunkSize <= 0 {
|
||||
m.ChunkSize = consts.DefaultChunkSize
|
||||
}
|
||||
if m.ChunkOverlap < 0 {
|
||||
m.ChunkOverlap = consts.DefaultChunkOverlap
|
||||
}
|
||||
return dao.Dataset.Insert(ctx, m)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,6 @@ import (
|
||||
"context"
|
||||
|
||||
"rag-local/common"
|
||||
"rag-local/kb/consts"
|
||||
"rag-local/kb/dao"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
)
|
||||
@@ -27,30 +25,3 @@ func (s *systemConfigService) Login(ctx context.Context, token string) (string,
|
||||
}
|
||||
return common.SignToken("owner", common.AccessTokenFingerprint(), common.TokenExpireSeconds)
|
||||
}
|
||||
|
||||
// GetSettings 读取全局分块默认值与智能体轮次默认值(未设置时用内置默认值)
|
||||
func (s *systemConfigService) GetSettings(ctx context.Context) (chunkSize, chunkOverlap, reactRounds int, err error) {
|
||||
return dao.AppConfig.GetInt(ctx, consts.SettingsKeyChunkSize, consts.DefaultChunkSize),
|
||||
dao.AppConfig.GetInt(ctx, consts.SettingsKeyChunkOverlap, consts.DefaultChunkOverlap),
|
||||
dao.AppConfig.GetInt(ctx, consts.SettingsKeyReactRounds, 0), nil
|
||||
}
|
||||
|
||||
// SaveSettings 保存全局分块默认值与智能体轮次默认值
|
||||
func (s *systemConfigService) SaveSettings(ctx context.Context, chunkSize, chunkOverlap, reactRounds int) error {
|
||||
if chunkSize < 50 || chunkSize > 5000 {
|
||||
return gerror.New("分块大小需在 50~5000 之间")
|
||||
}
|
||||
if chunkOverlap < 0 || chunkOverlap > 500 {
|
||||
return gerror.New("重叠字数需在 0~500 之间")
|
||||
}
|
||||
if reactRounds < 0 || reactRounds > consts.MaxReactRounds {
|
||||
return gerror.Newf("智能体轮次需在 0~%d 之间(0=关闭)", consts.MaxReactRounds)
|
||||
}
|
||||
if err := dao.AppConfig.SetInt(ctx, consts.SettingsKeyChunkSize, chunkSize); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := dao.AppConfig.SetInt(ctx, consts.SettingsKeyChunkOverlap, chunkOverlap); err != nil {
|
||||
return err
|
||||
}
|
||||
return dao.AppConfig.SetInt(ctx, consts.SettingsKeyReactRounds, reactRounds)
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import request from './request.js'
|
||||
|
||||
export function getSettings() {
|
||||
return request.get('/system-config/settings')
|
||||
}
|
||||
|
||||
export function saveSettings(data) {
|
||||
return request.post('/system-config/save-settings', data)
|
||||
}
|
||||
@@ -49,22 +49,22 @@
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="12">
|
||||
<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">每块最大字数。系统自动组合分块策略:优先识别文档结构(条文/章节/编号等)按结构切分,无结构时依次按标题感知、语义、递归切分</div>
|
||||
<el-input-number v-model="form.chunk_size" :min="0" :max="5000" :step="50" style="width: 100%" />
|
||||
<div class="ds-tip">每块最大字数,0=使用 config.yml 全局默认。系统自动组合分块策略:优先识别文档结构(条文/章节/编号等)按结构切分,无结构时依次按标题感知、语义、递归切分</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="重叠字数">
|
||||
<el-input-number v-model="form.chunk_overlap" :min="0" :max="500" :step="10" style="width: 100%" />
|
||||
<div class="ds-tip">相邻分块间的重叠字数,用于保持上下文连贯(语义切分路径自动忽略)</div>
|
||||
<el-input-number v-model="form.chunk_overlap" :min="-1" :max="500" :step="10" style="width: 100%" />
|
||||
<div class="ds-tip">相邻分块间的重叠字数,-1=使用 config.yml 全局默认,0=不重叠(语义切分路径自动忽略)</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="智能体轮次">
|
||||
<el-input-number v-model="form.react_rounds" :min="0" :max="10" :step="1" style="width: 100%" />
|
||||
<div class="ds-tip">0=关闭(单次检索问答);1~10=启用 ReAct 智能体模式,模型可自主多次调用检索工具,此为轮次上限</div>
|
||||
<el-input-number v-model="form.react_rounds" :min="-1" :max="10" :step="1" style="width: 100%" />
|
||||
<div class="ds-tip">-1=使用 config.yml 全局默认;0=关闭(单次检索问答);1~10=启用 ReAct 智能体模式,模型可自主多次调用检索工具,此为轮次上限</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
@@ -106,7 +106,6 @@ import { onMounted, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { listDatasets, saveDataset, deleteDataset } from '../api/dataset.js'
|
||||
import { listModelConfigs } from '../api/model_config.js'
|
||||
import { getSettings } from '../api/settings.js'
|
||||
|
||||
// 把识别出的结构正则转成可读名称(第[一二三四五六七八九十百千]+条 → 条文)
|
||||
function humanizePattern(p) {
|
||||
@@ -123,25 +122,16 @@ function humanizePattern(p) {
|
||||
|
||||
const datasets = ref([])
|
||||
const embedders = ref([])
|
||||
const defaults = ref({ chunk_size: 800, chunk_overlap: 150, react_rounds: 0, vec_top_k: 0, fts_top_k: 0, rerank_top_k: 0, recall_top_k: 0 })
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const editing = ref(false)
|
||||
let editingRowCfgId = 0
|
||||
let editingRowChunk = { chunk_size: 0, chunk_overlap: 0 }
|
||||
const form = ref({ id: 0, name: '', description: '', embedding_cfg_id: 0, chunk_size: 800, chunk_overlap: 150, react_rounds: 0, vec_top_k: 0, fts_top_k: 0, rerank_top_k: 0, recall_top_k: 0 })
|
||||
const form = ref({ id: 0, name: '', description: '', embedding_cfg_id: 0, chunk_size: 0, chunk_overlap: -1, react_rounds: -1, vec_top_k: 0, fts_top_k: 0, rerank_top_k: 0, recall_top_k: 0 })
|
||||
|
||||
onMounted(async () => {
|
||||
await load()
|
||||
try {
|
||||
const s = await getSettings()
|
||||
if (s) {
|
||||
defaults.value.chunk_size = s.chunk_size || 800
|
||||
defaults.value.chunk_overlap = s.chunk_overlap ?? 150
|
||||
defaults.value.react_rounds = s.react_rounds ?? 0
|
||||
}
|
||||
} catch { /* 忽略 */ }
|
||||
try {
|
||||
const m = await listModelConfigs('embedding')
|
||||
if (m && m.list) embedders.value = m.list
|
||||
@@ -167,9 +157,9 @@ function openCreate() {
|
||||
editing.value = false
|
||||
editingRowCfgId = 0
|
||||
editingRowChunk = { chunk_size: 0, chunk_overlap: 0 }
|
||||
// 默认选中默认向量模型,其次第一个;分块大小/重叠/智能体轮次带出全局设置值
|
||||
// 默认选中默认向量模型,其次第一个;分块大小/重叠/智能体轮次传 0/-1 使用 config.yml 全局默认
|
||||
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: defaults.value.chunk_size, chunk_overlap: defaults.value.chunk_overlap, react_rounds: defaults.value.react_rounds, vec_top_k: defaults.value.vec_top_k, fts_top_k: defaults.value.fts_top_k, rerank_top_k: defaults.value.rerank_top_k, recall_top_k: defaults.value.recall_top_k }
|
||||
form.value = { id: 0, name: '', description: '', embedding_cfg_id: def?.id || 0, chunk_size: 0, chunk_overlap: -1, react_rounds: -1, vec_top_k: 0, fts_top_k: 0, rerank_top_k: 0, recall_top_k: 0 }
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
<template>
|
||||
<div class="settings-page">
|
||||
<div class="block-card">
|
||||
<div class="block-title">分块默认值</div>
|
||||
<div class="block-tip">新建数据集时自动带出,单个数据集仍可在表单中修改</div>
|
||||
<div class="block-row">
|
||||
<span class="field-label">分块大小</span>
|
||||
<el-input-number v-model="chunkForm.chunk_size" :min="50" :max="5000" :step="50" />
|
||||
<span class="field-label">重叠字数</span>
|
||||
<el-input-number v-model="chunkForm.chunk_overlap" :min="0" :max="500" :step="10" />
|
||||
<span class="field-label">智能体轮次</span>
|
||||
<el-input-number v-model="chunkForm.react_rounds" :min="0" :max="10" :step="1" />
|
||||
<el-button type="primary" :loading="chunkSaving" @click="saveChunkDefaults">保存</el-button>
|
||||
</div>
|
||||
<div class="block-tip" style="margin-top: 6px; margin-bottom: 0;">智能体轮次:0=关闭(单次检索问答);1~10=启用 ReAct 智能体模式,模型自主调用检索工具,此为轮次上限。新建数据集时自动带出。</div>
|
||||
</div>
|
||||
|
||||
<div class="section-head">
|
||||
<el-radio-group v-model="modelType" @change="loadModels">
|
||||
<el-radio-button value="chat">对话模型</el-radio-button>
|
||||
@@ -80,33 +65,9 @@
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { listModelConfigs, saveModelConfig, deleteModelConfig, testModelConfig, setDefaultModelConfig } from '../api/model_config.js'
|
||||
import { getSettings, saveSettings } from '../api/settings.js'
|
||||
|
||||
const modelType = ref('chat')
|
||||
|
||||
const chunkForm = ref({ chunk_size: 800, chunk_overlap: 150, react_rounds: 0 })
|
||||
const chunkSaving = ref(false)
|
||||
|
||||
async function loadChunkDefaults() {
|
||||
try {
|
||||
const s = await getSettings()
|
||||
if (s) {
|
||||
chunkForm.value.chunk_size = s.chunk_size || 800
|
||||
chunkForm.value.chunk_overlap = s.chunk_overlap ?? 150
|
||||
chunkForm.value.react_rounds = s.react_rounds ?? 0
|
||||
}
|
||||
} catch { /* 忽略 */ }
|
||||
}
|
||||
|
||||
async function saveChunkDefaults() {
|
||||
chunkSaving.value = true
|
||||
try {
|
||||
await saveSettings(chunkForm.value)
|
||||
ElMessage.success('分块默认值已保存')
|
||||
} finally {
|
||||
chunkSaving.value = false
|
||||
}
|
||||
}
|
||||
const models = ref([])
|
||||
const allModels = ref([])
|
||||
const modelLoading = ref(false)
|
||||
@@ -118,7 +79,6 @@ const modelForm = ref({ id: 0, name: '', model_type: 'chat', model_name: '', end
|
||||
|
||||
onMounted(async () => {
|
||||
await loadModels()
|
||||
await loadChunkDefaults()
|
||||
try { const all = await listModelConfigs(''); if (all && all.list) allModels.value = all.list } catch { /* 忽略 */ }
|
||||
})
|
||||
|
||||
@@ -221,28 +181,4 @@ async function test(row) {
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.block-card {
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 6px;
|
||||
padding: 14px 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.block-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.block-tip {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.block-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.field-label {
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -88,7 +88,6 @@ kb_dataset --1:N-- kb_document --1:N-- kb_chunk --1:1-- kb_chunk_vec (vec0, chun
|
||||
|
||||
model_config: is_default 标记默认 chat/embedding 配置(GetDefault 取数)
|
||||
chat_conversation --1:N-- chat_message(删会话级联删消息)
|
||||
app_config: 无外联(全局键值)
|
||||
```
|
||||
|
||||
**删除级联矩阵**(已实现行为,删除动作均落库失败回滚):
|
||||
@@ -315,7 +314,7 @@ StartParsePoller(main.go 启动,gtimer 单例 5 秒轮询,job 未结束不
|
||||
```
|
||||
每次启动
|
||||
│ EnsureAccessToken():RandomToken(16) 生成 32 位 hex token,SetAccessToken 存入内存
|
||||
│ (common/auth.go 包级变量,不落库;app_config 仅存分块默认值,无 access_token 键)
|
||||
│ (common/auth.go 包级变量,不落库)
|
||||
▼
|
||||
启动日志打印(main.go):
|
||||
============================================
|
||||
@@ -333,7 +332,7 @@ StartParsePoller(main.go 启动,gtimer 单例 5 秒轮询,job 未结束不
|
||||
|
||||
- **内存持有,不落库**:`common/auth.go` 中 `var accessToken string` 包级变量;`EnsureAccessToken`(system_config_service.go)每次启动无条件重新生成。登录页提示语「请输入服务启动时打印在控制台的访问令牌」
|
||||
- **指纹校验**:JWT 中携带令牌的 SHA-256 指纹(`TokenFingerprint`,取前 16 位 hex);鉴权中间件将 JWT 指纹与内存令牌指纹比对(`claims.TokenFp != AccessTokenFingerprint()`),不一致即 401「访问令牌已变更,请重新登录」。因此**重启服务后所有旧会话立即失效**,前端 401 拦截器自动跳登录页
|
||||
- **无 regenerate-token 接口**:`system_config_dto.go` 仅有 `/login`、`/settings`、`/save-settings` 三个接口;换令牌 = 重启服务(新令牌自动打印到日志)。设置页**没有**令牌管理 UI(Settings.vue 只含分块默认值 + 模型配置)
|
||||
- **无 regenerate-token 接口**:`system_config_dto.go` 仅有 `/login` 一个接口;换令牌 = 重启服务(新令牌自动打印到日志)。设置页**没有**令牌管理 UI(Settings.vue 只含模型配置)
|
||||
- **公开路径白名单**(auth_middleware.go 精确匹配):`/system-config/login`、`GET /`、`GET /assets/*`(hash 路由下 SPA 只请求这两类静态路径,无鉴权绕过);`GET /workspace/*` 前缀放行(浏览器下载/预览请求不带 Authorization),仅做路径穿越防护(拒绝 `..`);其余路径一律校验
|
||||
- 无注册、无角色、无用户表;登录日志不落库(如需审计可在日志文件输出);JWT 密钥为代码内固定常量(单机场景可接受,如需更换改 `common/auth.go` 的 `jwtSecret`)
|
||||
|
||||
@@ -364,7 +363,7 @@ func Auth(r *ghttp.Request) {
|
||||
### 9.3 API 封装
|
||||
|
||||
- `api/request.js`:axios 实例(baseURL `/`、token 注入、401 跳转、错误 toast)
|
||||
- `api/xxx.js`:按后端模块组织,与 dto 对齐(dataset/document/chunk/chat/kg/contract/model_config/settings/auth)
|
||||
- `api/xxx.js`:按后端模块组织,与 dto 对齐(dataset/document/chunk/chat/kg/contract/model_config/auth)
|
||||
- 二进制下载(导出标注版 HTML)走原生 fetch + 手动 `Authorization` 头,绕过拦截器对 blob 的 JSON 误判
|
||||
- 流式:`fetch` + `ReadableStream` 解析 SSE(按 `\n\n` 分块、解析 `data: ` 行 JSON,按字段识别:`citations`(引用 + conversation_id)/ `content`(增量文本)/ `status==='ok'`(完成,或流读完兜底)/ `message`(错误);`data: [DONE]` 与 15 秒心跳注释行静默跳过)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user