import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue' import { useRoute, useRouter } from 'vue-router' import { useSystemState } from './useSystemState.js' import { useThemeSkin } from './useThemeSkin.js' import { clearSystemCaches, 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' const sectionIds = new Set(SECTION_DEFINITIONS.map((section) => section.id)) function resolveSectionId(value) { const sectionId = String(value || '').trim() return sectionIds.has(sectionId) ? sectionId : 'profile' } function resolveInitialSectionId(route) { return route.name === 'app-log-detail' ? 'systemLogs' : resolveSectionId(route.query.section) } export function useSettings() { const route = useRoute() const router = useRouter() const { toast } = useToast() const { companyProfile, currentUser, updateCompanyProfilePreview } = useSystemState() const { activeThemeSkin, activeThemeSkinId, setThemeSkin, themeSkinOptions } = useThemeSkin() const buildResolvedDefaults = () => buildDefaultState(companyProfile.value, currentUser.value) const pageState = ref(mergeState(buildResolvedDefaults(), readStoredSettings())) const activeSection = ref(resolveInitialSectionId(route)) const sessionRetentionPickerOpen = ref(false) const sessionRetentionPickerRef = ref(null) const logoInputRef = ref(null) const cacheClearing = ref(false) const cacheClearItems = ref([]) const cacheClearMessage = ref('') const cacheClearFailed = ref(false) const sections = SECTION_DEFINITIONS const logLevels = LOG_LEVELS const providerOptions = PROVIDER_OPTIONS const sessionRetentionOptions = SESSION_RETENTION_OPTIONS const archiveCycleOptions = [ { label: '按天归档', value: 'daily' }, { label: '按周归档', value: 'weekly' }, { label: '按月归档', value: 'monthly' } ] const sectionStatus = computed(() => computeSectionStatus(pageState.value)) const completedSectionCount = computed(() => Object.values(sectionStatus.value).filter(Boolean).length) const systemLogDetailMode = computed(() => route.name === 'app-log-detail') 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) if (nextState.appearanceForm?.themeSkin) { setThemeSkin(nextState.appearanceForm.themeSkin) } } 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 }, appearanceForm: { ...pageState.value.appearanceForm }, adminForm: { ...pageState.value.adminForm }, sessionForm: { ...pageState.value.sessionForm }, llmForm: buildLlmPayload(pageState.value.llmForm), hermesForm: { ...pageState.value.hermesForm }, 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 syncActiveSectionRoute(sectionId) { if (route.name !== 'app-settings') { return } const nextQuery = { ...route.query } if (sectionId === 'profile') { delete nextQuery.section } else { nextQuery.section = sectionId } if (String(route.query.section || '') === String(nextQuery.section || '')) { return } void router.replace({ name: 'app-settings', query: nextQuery, hash: route.hash }) } function activateSection(sectionId, options = {}) { const nextSectionId = resolveSectionId(sectionId) sessionRetentionPickerOpen.value = false activeSection.value = nextSectionId if (!options.skipRouteSync) { syncActiveSectionRoute(nextSectionId) } } 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 }) } function selectThemeSkin(skinId) { setThemeSkin(skinId) pageState.value.appearanceForm.themeSkin = skinId } async function saveAppearanceSection() { 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 }) } function normalizeCacheClearErrorMessage(error) { const message = String(error?.message || '').trim() if (!message || /^not found$/i.test(message)) { return '缓存清理接口暂不可用,请确认后端服务已加载最新路由后重试。' } return message } async function clearAllCaches() { if (cacheClearing.value) { return } cacheClearing.value = true cacheClearMessage.value = '' cacheClearItems.value = [] cacheClearFailed.value = false try { const payload = await clearSystemCaches() const items = Array.isArray(payload?.items) ? payload.items : [] const totalCleared = Number(payload?.totalCleared || 0) cacheClearItems.value = items cacheClearMessage.value = totalCleared > 0 ? `已清理 ${totalCleared} 条缓存。` : '当前没有可清理的缓存。' cacheClearFailed.value = false toast(cacheClearMessage.value) } catch (error) { const message = normalizeCacheClearErrorMessage(error) cacheClearFailed.value = true cacheClearMessage.value = message toast(message) } finally { cacheClearing.value = false } } 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 === 'appearance') { saveAppearanceSection() 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 === 'systemLogs') { return } if (activeSection.value === 'cacheManagement') { return } if (activeSection.value === 'rendering') { await saveRenderingSection() return } await saveMailSection() } onMounted(() => { if (typeof document !== 'undefined') { document.addEventListener('pointerdown', handleDocumentPointerDown) } loadSettingsSnapshot() }) watch( () => [route.name, route.query.section], () => { const nextSectionId = resolveInitialSectionId(route) if (activeSection.value !== nextSectionId) { activateSection(nextSectionId, { skipRouteSync: true }) } } ) 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, activeThemeSkin, activeThemeSkinId, archiveCycleOptions, activateSection, cacheClearFailed, cacheClearItems, cacheClearMessage, cacheClearing, clearAllCaches, clearRenderSecretMask, completedSectionCount, logLevels, logoInputRef, pageState, providerOptions, sessionRetentionOptions, sessionRetentionPickerOpen, sessionRetentionPickerRef, saveActiveSection, sectionStatus, sections, systemLogDetailMode, selectThemeSkin, selectSessionRetentionDays, themeSkinOptions, toggleSessionRetentionPicker, closeSessionRetentionPicker, toggleBoolean, toggleHermesFlag, toggleHermesMaster, toggleHermesTask, updateHermesTaskTime, triggerLogoUpload, handleLogoUpload } }