Files
X-Financial/web/src/utils/settingsModelHelper.js
caoxiaozhu 5b388d08c0 feat: 增强知识库索引与设置页面模块化拆分
扩展知识库索引任务和 RAG 检索支持增量入库和文档去重,优
化本体检测和规则匹配精度,前端设置页面拆分为 LLM、邮件
和 Hermes 员工同步子面板并重构样式,新增日志详情组件和
知识入库日志模型,补充单元测试覆盖。
2026-05-22 23:47:28 +08:00

462 lines
14 KiB
JavaScript

import {
buildDefaultHermesEmployeeForm,
isHermesEmployeeSettingsReady,
mergeHermesEmployeeForm
} from './hermesEmployeeSettingsModel.js'
export const SETTINGS_STORAGE_KEY = 'x-financial-settings-draft'
export const CURRENT_YEAR = new Date().getFullYear()
export const CUSTOM_OPENAI_PROVIDER = 'Custom OpenAI Compatible'
export const MODEL_SECRET_MASK = '********'
export const RENDER_SECRET_MASK = '********'
export const SECTION_DEFINITIONS = [
{
id: 'profile',
label: '企业信息',
title: '系统基本信息',
desc: '公司名称、品牌与版权',
longDesc: '统一维护企业名称、系统显示名称和版权信息,保存后会同步更新当前系统品牌展示。',
actionLabel: '保存企业信息'
},
{
id: 'admin',
label: '管理员安全',
title: '管理员账号与安全策略',
desc: '账号、密码与登录安全',
longDesc: '集中管理管理员账号、邮箱和登录安全策略。密码仅在当前输入时可见,不会写入浏览器草稿。',
actionLabel: '保存安全设置'
},
{
id: 'session',
label: '会话设置',
title: '会话留存设置',
desc: '会话保留天数',
longDesc: '统一配置智能体会话的保留天数。超过保留期的历史会话会在后端清理,避免上下文和管理成本无限增长。',
actionLabel: '保存会话设置'
},
{
id: 'hermes',
label: '数字员工设置',
title: '数字员工设置',
desc: 'Hermes 自动任务',
longDesc: '选择需要自动执行的任务,并设置每天的执行时间。无需了解 Cron 或复杂调度规则。',
actionLabel: '保存数字员工设置'
},
{
id: 'llm',
label: '大语言模型',
title: '模型接入配置',
desc: '主模型、备份模型与检索模型',
longDesc: '集中维护主模型、备份模型、Embedding 模型和 Reranker 模型的接入参数,供 AI 助手和检索链路调用。',
actionLabel: '保存模型配置'
},
{
id: 'rendering',
label: '文件渲染',
title: '文件渲染',
desc: '文档预览服务与访问密钥',
longDesc: '维护文件渲染开关、文档服务对外地址和 JWT 密钥,后端回调地址继续由部署配置管理。',
actionLabel: '保存文件渲染配置'
},
{
id: 'logs',
label: '日志策略',
title: '日志与审计策略',
desc: '日志级别、留存与脱敏',
longDesc: '定义系统日志级别、留存周期和审计策略,保证问题排查和合规审计可追溯。',
actionLabel: '保存日志策略'
},
{
id: 'mail',
label: '邮箱设置',
title: '邮箱通知配置',
desc: 'SMTP 与通知投递策略',
longDesc: '维护系统邮件发送配置和通知投递策略,审批、告警和摘要邮件都会依赖这里的设置。',
actionLabel: '保存邮箱配置'
}
]
export const LOG_LEVELS = ['DEBUG', 'INFO', 'WARN', 'ERROR']
export const PROVIDER_OPTIONS = [
'MiniMax',
'GLM',
'Kimi',
'Ali',
'Codex',
'Claude',
'Gemini',
CUSTOM_OPENAI_PROVIDER
]
export const PROVIDER_ENDPOINTS = {
MiniMax: 'https://api.minimaxi.com/v1',
GLM: 'https://open.bigmodel.cn/api/paas/v4/',
Kimi: 'https://api.moonshot.ai/v1',
Ali: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
Codex: 'https://api.openai.com/v1',
Claude: 'https://api.anthropic.com/v1/',
Gemini: 'https://generativelanguage.googleapis.com/v1beta/openai/',
[CUSTOM_OPENAI_PROVIDER]: ''
}
export const RERANKER_PROVIDER_ENDPOINTS = {
Ali: 'https://dashscope.aliyuncs.com/api/v1/services/rerank/text-rerank/text-rerank',
[CUSTOM_OPENAI_PROVIDER]: ''
}
export const LEGACY_PROVIDER_MAP = {
'OpenAI Compatible': 'Codex',
'Azure OpenAI': CUSTOM_OPENAI_PROVIDER,
Ollama: CUSTOM_OPENAI_PROVIDER,
'自定义网关': CUSTOM_OPENAI_PROVIDER
}
export const MODEL_TEST_CONFIGS = {
main: {
label: '主模型',
providerKey: 'mainProvider',
modelKey: 'mainModel',
endpointKey: 'mainEndpoint',
apiKeyKey: 'mainApiKey',
capability: 'chat'
},
backup: {
label: '备份模型',
providerKey: 'backupProvider',
modelKey: 'backupModel',
endpointKey: 'backupEndpoint',
apiKeyKey: 'backupApiKey',
capability: 'chat'
},
embedding: {
label: 'Embedding 模型',
providerKey: 'embeddingProvider',
modelKey: 'embeddingModel',
endpointKey: 'embeddingEndpoint',
apiKeyKey: 'embeddingApiKey',
capability: 'embedding'
},
reranker: {
label: 'Reranker 模型',
providerKey: 'rerankerProvider',
modelKey: 'rerankerModel',
endpointKey: 'rerankerEndpoint',
apiKeyKey: 'rerankerApiKey',
capability: 'reranker'
}
}
export const MODEL_API_KEY_CONFIGS = Object.values(MODEL_TEST_CONFIGS)
export const SESSION_RETENTION_OPTIONS = Array.from({ length: 10 }, (_item, index) => ({
value: index + 1,
label: `${index + 1}`
}))
export function normalizeValue(value) {
return String(value ?? '').trim()
}
export function normalizeProviderValue(value, fallback = 'Codex') {
const normalized = normalizeValue(value)
if (PROVIDER_OPTIONS.includes(normalized)) {
return normalized
}
if (LEGACY_PROVIDER_MAP[normalized]) {
return LEGACY_PROVIDER_MAP[normalized]
}
return fallback
}
export function getProviderEndpoint(provider) {
return PROVIDER_ENDPOINTS[provider] ?? ''
}
export function getRerankerEndpoint(provider) {
return RERANKER_PROVIDER_ENDPOINTS[provider] ?? getProviderEndpoint(provider)
}
export function buildDefaultState(companyProfile, currentUser) {
const companyName = normalizeValue(companyProfile?.name) || 'X-Financial'
const companyCode = normalizeValue(companyProfile?.code) || 'XF-001'
const adminEmail =
normalizeValue(companyProfile?.adminEmail) ||
normalizeValue(currentUser?.email) ||
'admin@example.com'
const adminAccount = normalizeValue(currentUser?.username) || 'superadmin'
return {
companyForm: {
companyName,
displayName: companyName,
companyCode,
logo: normalizeValue(companyProfile?.logo) || '',
recordNumber: '',
copyright: `Copyright © 2024-${CURRENT_YEAR} ${companyName}. All Rights Reserved.`
},
adminForm: {
adminAccount,
adminEmail,
newPassword: '',
confirmPassword: '',
sessionTimeout: Number(import.meta.env.VITE_AUTH_IDLE_TIMEOUT_MINUTES || 30),
noticeEmail: adminEmail,
mfaEnabled: true,
strongPassword: true,
loginAlertEnabled: true,
adminPasswordConfigured: false
},
sessionForm: {
conversationRetentionDays: 3
},
llmForm: {
mainProvider: 'Codex',
mainModel: 'codex-mini-latest',
mainEndpoint: getProviderEndpoint('Codex'),
mainApiKey: '',
mainApiKeyConfigured: false,
backupProvider: 'GLM',
backupModel: 'glm-5.1',
backupEndpoint: getProviderEndpoint('GLM'),
backupApiKey: '',
backupApiKeyConfigured: false,
embeddingProvider: 'GLM',
embeddingModel: 'Embedding-3',
embeddingEndpoint: getProviderEndpoint('GLM'),
embeddingApiKey: '',
embeddingApiKeyConfigured: false,
rerankerProvider: 'Ali',
rerankerModel: 'gte-rerank-v2',
rerankerEndpoint: getRerankerEndpoint('Ali'),
rerankerApiKey: '',
rerankerApiKeyConfigured: false
},
renderForm: {
enabled: false,
publicUrl: '',
jwtSecret: '',
jwtSecretConfigured: false
},
logForm: {
level: 'INFO',
retentionDays: 180,
archiveCycle: 'weekly',
logPath: 'server/logs/app.log',
alertEmail: adminEmail,
operationAudit: true,
loginAudit: true,
maskSensitive: true
},
hermesForm: buildDefaultHermesEmployeeForm(),
mailForm: {
smtpHost: 'smtp.exmail.qq.com',
port: 465,
encryption: 'SSL/TLS',
senderName: companyName,
senderAddress: adminEmail,
username: adminEmail,
password: '',
passwordConfigured: false,
alertEnabled: true,
digestEnabled: false,
digestTime: '09:00',
defaultReceiver: adminEmail
}
}
}
export function readStoredSettings() {
if (typeof window === 'undefined') {
return null
}
const raw = window.sessionStorage.getItem(SETTINGS_STORAGE_KEY)
if (!raw) {
return null
}
try {
return JSON.parse(raw)
} catch {
return null
}
}
export function mergeState(baseState, overrideState) {
const mergedLlmForm = { ...baseState.llmForm, ...(overrideState?.llmForm || {}) }
mergedLlmForm.mainProvider = normalizeProviderValue(mergedLlmForm.mainProvider, baseState.llmForm.mainProvider)
mergedLlmForm.backupProvider = normalizeProviderValue(mergedLlmForm.backupProvider, baseState.llmForm.backupProvider)
mergedLlmForm.embeddingProvider = normalizeProviderValue(
mergedLlmForm.embeddingProvider,
baseState.llmForm.embeddingProvider
)
mergedLlmForm.rerankerProvider = normalizeProviderValue(
mergedLlmForm.rerankerProvider,
baseState.llmForm.rerankerProvider
)
return {
companyForm: { ...baseState.companyForm, ...(overrideState?.companyForm || {}) },
adminForm: { ...baseState.adminForm, ...(overrideState?.adminForm || {}) },
sessionForm: { ...baseState.sessionForm, ...(overrideState?.sessionForm || {}) },
hermesForm: mergeHermesEmployeeForm({
...baseState.hermesForm,
...(overrideState?.hermesForm || {})
}),
llmForm: mergedLlmForm,
renderForm: { ...baseState.renderForm, ...(overrideState?.renderForm || {}) },
logForm: { ...baseState.logForm, ...(overrideState?.logForm || {}) },
mailForm: { ...baseState.mailForm, ...(overrideState?.mailForm || {}) }
}
}
export function sanitizeForStorage(state) {
return {
companyForm: { ...state.companyForm },
adminForm: {
...state.adminForm,
newPassword: '',
confirmPassword: ''
},
sessionForm: { ...state.sessionForm },
hermesForm: mergeHermesEmployeeForm(state.hermesForm),
llmForm: {
...state.llmForm,
mainApiKey: '',
backupApiKey: '',
embeddingApiKey: '',
rerankerApiKey: ''
},
renderForm: {
...state.renderForm,
jwtSecret: ''
},
logForm: { ...state.logForm },
mailForm: {
...state.mailForm,
password: ''
}
}
}
export function getModelConfiguredKey(apiKeyKey) {
return `${apiKeyKey}Configured`
}
export function isModelSecretMask(value) {
return value === MODEL_SECRET_MASK
}
export function maskConfiguredModelSecrets(state) {
for (const config of MODEL_API_KEY_CONFIGS) {
const configuredKey = getModelConfiguredKey(config.apiKeyKey)
if (state.llmForm[configuredKey] && !normalizeValue(state.llmForm[config.apiKeyKey])) {
state.llmForm[config.apiKeyKey] = MODEL_SECRET_MASK
}
}
return state
}
export function buildLlmPayload(llmForm) {
const payload = { ...llmForm }
for (const config of MODEL_API_KEY_CONFIGS) {
if (isModelSecretMask(payload[config.apiKeyKey])) {
payload[config.apiKeyKey] = ''
}
}
return payload
}
export function isRenderSecretMask(value) {
return value === RENDER_SECRET_MASK
}
export function maskConfiguredRenderSecret(state) {
if (state.renderForm.jwtSecretConfigured && !normalizeValue(state.renderForm.jwtSecret)) {
state.renderForm.jwtSecret = RENDER_SECRET_MASK
}
return state
}
export function buildRenderPayload(renderForm) {
const payload = { ...renderForm }
if (isRenderSecretMask(payload.jwtSecret)) {
payload.jwtSecret = ''
}
return payload
}
export function persistSettings(state) {
if (typeof window === 'undefined') {
return
}
window.sessionStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(sanitizeForStorage(state)))
}
export function isModelConfigReady(provider, model, endpoint) {
return Boolean(normalizeValue(provider) && normalizeValue(model) && normalizeValue(endpoint))
}
export function computeSectionStatus(state) {
return {
profile: Boolean(
normalizeValue(state.companyForm.companyName) &&
normalizeValue(state.companyForm.displayName) &&
normalizeValue(state.companyForm.copyright)
),
admin: Boolean(
normalizeValue(state.adminForm.adminAccount) &&
normalizeValue(state.adminForm.adminEmail) &&
Number(state.adminForm.sessionTimeout) >= 5
),
session: Boolean(
Number(state.sessionForm.conversationRetentionDays) >= 1 &&
Number(state.sessionForm.conversationRetentionDays) <= 10
),
hermes: isHermesEmployeeSettingsReady(state.hermesForm),
llm: Boolean(
isModelConfigReady(state.llmForm.mainProvider, state.llmForm.mainModel, state.llmForm.mainEndpoint) &&
isModelConfigReady(state.llmForm.backupProvider, state.llmForm.backupModel, state.llmForm.backupEndpoint) &&
isModelConfigReady(
state.llmForm.embeddingProvider,
state.llmForm.embeddingModel,
state.llmForm.embeddingEndpoint
) &&
isModelConfigReady(
state.llmForm.rerankerProvider,
state.llmForm.rerankerModel,
state.llmForm.rerankerEndpoint
)
),
rendering: Boolean(
!state.renderForm.enabled ||
(normalizeValue(state.renderForm.publicUrl) &&
(normalizeValue(state.renderForm.jwtSecret) || state.renderForm.jwtSecretConfigured))
),
logs: Boolean(
normalizeValue(state.logForm.level) &&
Number(state.logForm.retentionDays) > 0 &&
normalizeValue(state.logForm.logPath)
),
mail: Boolean(
normalizeValue(state.mailForm.smtpHost) &&
Number(state.mailForm.port) > 0 &&
normalizeValue(state.mailForm.senderAddress) &&
normalizeValue(state.mailForm.username)
)
}
}