扩展知识库索引任务和 RAG 检索支持增量入库和文档去重,优 化本体检测和规则匹配精度,前端设置页面拆分为 LLM、邮件 和 Hermes 员工同步子面板并重构样式,新增日志详情组件和 知识入库日志模型,补充单元测试覆盖。
462 lines
14 KiB
JavaScript
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)
|
|
)
|
|
}
|
|
}
|