feat: 增强知识库索引与设置页面模块化拆分

扩展知识库索引任务和 RAG 检索支持增量入库和文档去重,优
化本体检测和规则匹配精度,前端设置页面拆分为 LLM、邮件
和 Hermes 员工同步子面板并重构样式,新增日志详情组件和
知识入库日志模型,补充单元测试覆盖。
This commit is contained in:
caoxiaozhu
2026-05-22 23:47:28 +08:00
parent 88ff04bef8
commit 5b388d08c0
84 changed files with 10170 additions and 2599 deletions

View File

@@ -0,0 +1,486 @@
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { useSystemState } from './useSystemState.js'
import { fetchSettings, saveSettings } from '../services/settings.js'
import { useToast } from './useToast.js'
import {
isHermesEmployeeSettingsReady
} from '../utils/hermesEmployeeSettingsModel.js'
import {
LOG_LEVELS,
PROVIDER_OPTIONS,
SECTION_DEFINITIONS,
SESSION_RETENTION_OPTIONS,
buildDefaultState,
buildLlmPayload,
buildRenderPayload,
computeSectionStatus,
isModelConfigReady,
isRenderSecretMask,
maskConfiguredModelSecrets,
maskConfiguredRenderSecret,
mergeState,
normalizeValue,
persistSettings,
readStoredSettings
} from '../utils/settingsModelHelper.js'
export function useSettings() {
const { toast } = useToast()
const { companyProfile, currentUser, updateCompanyProfilePreview } = useSystemState()
const buildResolvedDefaults = () => buildDefaultState(companyProfile.value, currentUser.value)
const pageState = ref(mergeState(buildResolvedDefaults(), readStoredSettings()))
const activeSection = ref('profile')
const sessionRetentionPickerOpen = ref(false)
const sessionRetentionPickerRef = ref(null)
const logoInputRef = ref(null)
const sections = SECTION_DEFINITIONS
const logLevels = LOG_LEVELS
const providerOptions = PROVIDER_OPTIONS
const sessionRetentionOptions = SESSION_RETENTION_OPTIONS
const sectionStatus = computed(() => computeSectionStatus(pageState.value))
const completedSectionCount = computed(() => Object.values(sectionStatus.value).filter(Boolean).length)
const activeSectionConfig = computed(
() => sections.find((section) => section.id === activeSection.value) || sections[0]
)
function updateBrandPreviewFromState(state) {
updateCompanyProfilePreview({
name: normalizeValue(state.companyForm.displayName),
code: normalizeValue(state.companyForm.companyCode),
adminEmail: normalizeValue(state.adminForm.adminEmail),
logo: state.companyForm.logo
})
}
function applyLoadedSnapshot(snapshot, options = {}) {
const {
mergeDraft = false,
preserveModelApiKeys = false,
preserveAdminPasswords = false,
preserveRenderSecret = false,
preserveMailPassword = false
} = options
const currentState = pageState.value
let nextState = mergeState(buildResolvedDefaults(), snapshot)
if (mergeDraft) {
nextState = mergeState(nextState, readStoredSettings())
}
if (preserveModelApiKeys) {
nextState.llmForm.mainApiKey = currentState.llmForm.mainApiKey
nextState.llmForm.backupApiKey = currentState.llmForm.backupApiKey
nextState.llmForm.embeddingApiKey = currentState.llmForm.embeddingApiKey
nextState.llmForm.rerankerApiKey = currentState.llmForm.rerankerApiKey
}
if (preserveAdminPasswords) {
nextState.adminForm.newPassword = currentState.adminForm.newPassword
nextState.adminForm.confirmPassword = currentState.adminForm.confirmPassword
}
if (preserveRenderSecret) {
nextState.renderForm.jwtSecret = currentState.renderForm.jwtSecret
}
if (preserveMailPassword) {
nextState.mailForm.password = currentState.mailForm.password
}
pageState.value = maskConfiguredRenderSecret(maskConfiguredModelSecrets(nextState))
persistSettings(pageState.value)
updateBrandPreviewFromState(pageState.value)
}
async function loadSettingsSnapshot() {
try {
const snapshot = await fetchSettings()
applyLoadedSnapshot(snapshot, { mergeDraft: true })
} catch (error) {
persistSettings(pageState.value)
updateBrandPreviewFromState(pageState.value)
toast(error.message || '无法加载已保存设置,继续使用当前会话草稿。')
}
}
function buildSettingsPayload() {
return {
companyForm: { ...pageState.value.companyForm },
adminForm: { ...pageState.value.adminForm },
sessionForm: { ...pageState.value.sessionForm },
llmForm: buildLlmPayload(pageState.value.llmForm),
renderForm: buildRenderPayload(pageState.value.renderForm),
logForm: { ...pageState.value.logForm },
mailForm: { ...pageState.value.mailForm }
}
}
async function persistRemoteSettings(successMessage, options = {}) {
try {
const snapshot = await saveSettings(buildSettingsPayload())
applyLoadedSnapshot(snapshot, options)
toast(successMessage)
return true
} catch (error) {
toast(error.message || '设置保存失败,请稍后重试。')
return false
}
}
function activateSection(sectionId) {
sessionRetentionPickerOpen.value = false
activeSection.value = sectionId
}
function toggleBoolean(formKey, field) {
pageState.value[formKey][field] = !pageState.value[formKey][field]
}
function toggleSessionRetentionPicker() {
sessionRetentionPickerOpen.value = !sessionRetentionPickerOpen.value
}
function closeSessionRetentionPicker() {
sessionRetentionPickerOpen.value = false
}
function selectSessionRetentionDays(value) {
pageState.value.sessionForm.conversationRetentionDays = Number(value)
closeSessionRetentionPicker()
}
function handleDocumentPointerDown(event) {
if (!sessionRetentionPickerOpen.value) {
return
}
const target = event.target
if (sessionRetentionPickerRef.value?.contains(target)) {
return
}
closeSessionRetentionPicker()
}
function clearRenderSecretMask() {
if (isRenderSecretMask(pageState.value.renderForm.jwtSecret)) {
pageState.value.renderForm.jwtSecret = ''
}
}
async function saveProfileSection() {
const companyForm = pageState.value.companyForm
if (!normalizeValue(companyForm.companyName)) {
toast('请输入企业名称。')
return
}
if (!normalizeValue(companyForm.displayName)) {
toast('请输入系统显示名称。')
return
}
if (!normalizeValue(companyForm.copyright)) {
toast('请输入版权信息。')
return
}
pageState.value.mailForm.senderName = normalizeValue(companyForm.displayName)
await persistRemoteSettings('企业信息已保存并应用到当前系统。', {
preserveModelApiKeys: true,
preserveAdminPasswords: true,
preserveRenderSecret: true,
preserveMailPassword: true
})
}
async function saveAdminSection() {
const adminForm = pageState.value.adminForm
if (!normalizeValue(adminForm.adminAccount)) {
toast('请输入管理员账号。')
return
}
if (!normalizeValue(adminForm.adminEmail)) {
toast('请输入管理员邮箱。')
return
}
if (Number(adminForm.sessionTimeout) < 5) {
toast('会话超时时间不能少于 5 分钟。')
return
}
if (adminForm.newPassword) {
if (adminForm.newPassword.length < 5) {
toast('管理员密码至少需要 5 位。')
return
}
if (adminForm.newPassword !== adminForm.confirmPassword) {
toast('两次输入的管理员密码不一致。')
return
}
}
await persistRemoteSettings('管理员安全设置已保存。', {
preserveModelApiKeys: true,
preserveAdminPasswords: false,
preserveRenderSecret: true,
preserveMailPassword: true
})
}
function toggleHermesMaster() {
pageState.value.hermesForm.masterEnabled = !pageState.value.hermesForm.masterEnabled
}
function toggleHermesFlag(field) {
pageState.value.hermesForm[field] = !pageState.value.hermesForm[field]
}
function toggleHermesTask(taskId) {
const schedule = pageState.value.hermesForm.schedules[taskId]
if (!schedule) {
return
}
const enabled = !(pageState.value.hermesForm.capabilities[taskId] && schedule.enabled)
pageState.value.hermesForm.capabilities[taskId] = enabled
schedule.enabled = enabled
}
function updateHermesTaskTime({ taskId, time }) {
const schedule = pageState.value.hermesForm.schedules[taskId]
if (!schedule) {
return
}
schedule.time = time
}
function saveHermesSection() {
if (!isHermesEmployeeSettingsReady(pageState.value.hermesForm)) {
toast('请至少开启一项 Hermes 能力,或关闭总控开关。')
return
}
persistSettings(pageState.value)
toast('数字员工设置已保存。')
}
async function saveSessionSection() {
const sessionForm = pageState.value.sessionForm
const retentionDays = Number(sessionForm.conversationRetentionDays)
if (retentionDays < 1 || retentionDays > 10) {
toast('会话保留天数必须在 1 到 10 天之间。')
return
}
await persistRemoteSettings('会话设置已保存。', {
preserveModelApiKeys: true,
preserveAdminPasswords: true,
preserveRenderSecret: true,
preserveMailPassword: true
})
}
async function saveLlmSection() {
const llmForm = pageState.value.llmForm
const modelConfigs = [
['主模型', llmForm.mainProvider, llmForm.mainModel, llmForm.mainEndpoint],
['备份模型', llmForm.backupProvider, llmForm.backupModel, llmForm.backupEndpoint],
['Embedding 模型', llmForm.embeddingProvider, llmForm.embeddingModel, llmForm.embeddingEndpoint],
['Reranker 模型', llmForm.rerankerProvider, llmForm.rerankerModel, llmForm.rerankerEndpoint]
]
for (const [label, provider, model, endpoint] of modelConfigs) {
if (!isModelConfigReady(provider, model, endpoint)) {
toast(`请完整填写${label}的供应商、模型名称和接口地址。`)
return
}
}
await persistRemoteSettings('模型配置已保存。', {
preserveModelApiKeys: true,
preserveAdminPasswords: true,
preserveRenderSecret: true,
preserveMailPassword: true
})
}
async function saveRenderingSection() {
const renderForm = pageState.value.renderForm
if (renderForm.enabled && !normalizeValue(renderForm.publicUrl)) {
toast('启用 ONLYOFFICE 时请输入服务地址。')
return
}
if (renderForm.enabled && !normalizeValue(renderForm.jwtSecret) && !renderForm.jwtSecretConfigured) {
toast('启用 ONLYOFFICE 时请输入 JWT 密钥。')
return
}
await persistRemoteSettings('文件渲染配置已保存。', {
preserveModelApiKeys: true,
preserveAdminPasswords: true,
preserveRenderSecret: false,
preserveMailPassword: true
})
}
async function saveLogsSection() {
const logForm = pageState.value.logForm
if (!normalizeValue(logForm.level) || Number(logForm.retentionDays) <= 0) {
toast('请填写有效的日志级别和留存天数。')
return
}
if (!normalizeValue(logForm.logPath)) {
toast('请输入日志路径。')
return
}
await persistRemoteSettings('日志策略已保存。', {
preserveModelApiKeys: true,
preserveAdminPasswords: true,
preserveRenderSecret: true,
preserveMailPassword: true
})
}
async function saveMailSection() {
const mailForm = pageState.value.mailForm
if (!normalizeValue(mailForm.smtpHost) || Number(mailForm.port) <= 0) {
toast('请填写有效的 SMTP Host 和端口。')
return
}
if (!normalizeValue(mailForm.senderAddress) || !normalizeValue(mailForm.username)) {
toast('请填写发件人邮箱和 SMTP 登录账号。')
return
}
await persistRemoteSettings('邮箱配置已保存。', {
preserveModelApiKeys: true,
preserveAdminPasswords: true,
preserveRenderSecret: true,
preserveMailPassword: false
})
}
async function saveActiveSection() {
if (activeSection.value === 'profile') {
await saveProfileSection()
return
}
if (activeSection.value === 'admin') {
await saveAdminSection()
return
}
if (activeSection.value === 'session') {
await saveSessionSection()
return
}
if (activeSection.value === 'hermes') {
saveHermesSection()
return
}
if (activeSection.value === 'llm') {
await saveLlmSection()
return
}
if (activeSection.value === 'logs') {
await saveLogsSection()
return
}
if (activeSection.value === 'rendering') {
await saveRenderingSection()
return
}
await saveMailSection()
}
onMounted(() => {
if (typeof document !== 'undefined') {
document.addEventListener('pointerdown', handleDocumentPointerDown)
}
loadSettingsSnapshot()
})
onBeforeUnmount(() => {
if (typeof document !== 'undefined') {
document.removeEventListener('pointerdown', handleDocumentPointerDown)
}
})
function triggerLogoUpload() {
logoInputRef.value?.click()
}
function handleLogoUpload(event) {
const file = event.target.files?.[0]
if (!file) return
if (!file.type.startsWith('image/')) {
toast('请上传图片文件。')
return
}
if (file.size > 2 * 1024 * 1024) {
toast('图片大小不能超过 2MB。')
return
}
const reader = new FileReader()
reader.onload = (e) => {
pageState.value.companyForm.logo = e.target.result
}
reader.readAsDataURL(file)
}
return {
activeSection,
activeSectionConfig,
activateSection,
clearRenderSecretMask,
completedSectionCount,
logLevels,
logoInputRef,
pageState,
providerOptions,
sessionRetentionOptions,
sessionRetentionPickerOpen,
sessionRetentionPickerRef,
saveActiveSection,
sectionStatus,
sections,
selectSessionRetentionDays,
toggleSessionRetentionPicker,
closeSessionRetentionPicker,
toggleBoolean,
toggleHermesFlag,
toggleHermesMaster,
toggleHermesTask,
updateHermesTaskTime,
triggerLogoUpload,
handleLogoUpload
}
}