2026-05-29 13:17:39 +08:00
|
|
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
|
|
|
import { useRoute, useRouter } from 'vue-router'
|
2026-05-22 23:47:28 +08:00
|
|
|
|
|
|
|
|
import { useSystemState } from './useSystemState.js'
|
2026-05-27 09:17:57 +08:00
|
|
|
import { useThemeSkin } from './useThemeSkin.js'
|
2026-06-24 12:35:59 +08:00
|
|
|
import { clearSystemCaches, fetchSettings, saveSettings } from '../services/settings.js'
|
2026-05-22 23:47:28 +08:00
|
|
|
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'
|
|
|
|
|
|
2026-05-29 13:17:39 +08:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-22 23:47:28 +08:00
|
|
|
export function useSettings() {
|
2026-05-29 13:17:39 +08:00
|
|
|
const route = useRoute()
|
|
|
|
|
const router = useRouter()
|
2026-05-22 23:47:28 +08:00
|
|
|
const { toast } = useToast()
|
|
|
|
|
const { companyProfile, currentUser, updateCompanyProfilePreview } = useSystemState()
|
2026-05-27 09:17:57 +08:00
|
|
|
const {
|
|
|
|
|
activeThemeSkin,
|
|
|
|
|
activeThemeSkinId,
|
|
|
|
|
setThemeSkin,
|
|
|
|
|
themeSkinOptions
|
|
|
|
|
} = useThemeSkin()
|
2026-05-22 23:47:28 +08:00
|
|
|
|
|
|
|
|
const buildResolvedDefaults = () => buildDefaultState(companyProfile.value, currentUser.value)
|
|
|
|
|
const pageState = ref(mergeState(buildResolvedDefaults(), readStoredSettings()))
|
2026-05-29 13:17:39 +08:00
|
|
|
const activeSection = ref(resolveInitialSectionId(route))
|
2026-05-22 23:47:28 +08:00
|
|
|
const sessionRetentionPickerOpen = ref(false)
|
|
|
|
|
const sessionRetentionPickerRef = ref(null)
|
|
|
|
|
const logoInputRef = ref(null)
|
2026-06-24 12:35:59 +08:00
|
|
|
const cacheClearing = ref(false)
|
|
|
|
|
const cacheClearItems = ref([])
|
|
|
|
|
const cacheClearMessage = ref('')
|
|
|
|
|
const cacheClearFailed = ref(false)
|
2026-05-22 23:47:28 +08:00
|
|
|
|
|
|
|
|
const sections = SECTION_DEFINITIONS
|
|
|
|
|
const logLevels = LOG_LEVELS
|
|
|
|
|
const providerOptions = PROVIDER_OPTIONS
|
|
|
|
|
const sessionRetentionOptions = SESSION_RETENTION_OPTIONS
|
2026-05-27 09:17:57 +08:00
|
|
|
const archiveCycleOptions = [
|
|
|
|
|
{ label: '按天归档', value: 'daily' },
|
|
|
|
|
{ label: '按周归档', value: 'weekly' },
|
|
|
|
|
{ label: '按月归档', value: 'monthly' }
|
|
|
|
|
]
|
2026-05-22 23:47:28 +08:00
|
|
|
|
|
|
|
|
const sectionStatus = computed(() => computeSectionStatus(pageState.value))
|
|
|
|
|
const completedSectionCount = computed(() => Object.values(sectionStatus.value).filter(Boolean).length)
|
2026-05-29 13:17:39 +08:00
|
|
|
const systemLogDetailMode = computed(() => route.name === 'app-log-detail')
|
2026-05-22 23:47:28 +08:00
|
|
|
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)
|
2026-05-28 16:24:59 +08:00
|
|
|
|
|
|
|
|
if (nextState.appearanceForm?.themeSkin) {
|
|
|
|
|
setThemeSkin(nextState.appearanceForm.themeSkin)
|
|
|
|
|
}
|
2026-05-22 23:47:28 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 },
|
2026-05-28 16:24:59 +08:00
|
|
|
appearanceForm: { ...pageState.value.appearanceForm },
|
2026-05-22 23:47:28 +08:00
|
|
|
adminForm: { ...pageState.value.adminForm },
|
|
|
|
|
sessionForm: { ...pageState.value.sessionForm },
|
|
|
|
|
llmForm: buildLlmPayload(pageState.value.llmForm),
|
2026-05-24 21:44:17 +08:00
|
|
|
hermesForm: { ...pageState.value.hermesForm },
|
2026-05-22 23:47:28 +08:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-29 13:17:39 +08:00
|
|
|
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)
|
2026-05-22 23:47:28 +08:00
|
|
|
sessionRetentionPickerOpen.value = false
|
2026-05-29 13:17:39 +08:00
|
|
|
activeSection.value = nextSectionId
|
|
|
|
|
|
|
|
|
|
if (!options.skipRouteSync) {
|
|
|
|
|
syncActiveSectionRoute(nextSectionId)
|
|
|
|
|
}
|
2026-05-22 23:47:28 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-27 09:17:57 +08:00
|
|
|
function selectThemeSkin(skinId) {
|
|
|
|
|
setThemeSkin(skinId)
|
2026-05-28 16:24:59 +08:00
|
|
|
pageState.value.appearanceForm.themeSkin = skinId
|
2026-05-27 09:17:57 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-28 16:24:59 +08:00
|
|
|
async function saveAppearanceSection() {
|
|
|
|
|
await persistRemoteSettings('界面皮肤已保存并应用到企业配置。', {
|
|
|
|
|
preserveModelApiKeys: true,
|
|
|
|
|
preserveAdminPasswords: true,
|
|
|
|
|
preserveRenderSecret: true,
|
|
|
|
|
preserveMailPassword: true
|
|
|
|
|
})
|
2026-05-27 09:17:57 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-22 23:47:28 +08:00
|
|
|
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
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-24 12:35:59 +08:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-22 23:47:28 +08:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-27 09:17:57 +08:00
|
|
|
if (activeSection.value === 'appearance') {
|
|
|
|
|
saveAppearanceSection()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-22 23:47:28 +08:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-29 13:17:39 +08:00
|
|
|
if (activeSection.value === 'systemLogs') {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-24 12:35:59 +08:00
|
|
|
if (activeSection.value === 'cacheManagement') {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-22 23:47:28 +08:00
|
|
|
if (activeSection.value === 'rendering') {
|
|
|
|
|
await saveRenderingSection()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await saveMailSection()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
if (typeof document !== 'undefined') {
|
|
|
|
|
document.addEventListener('pointerdown', handleDocumentPointerDown)
|
|
|
|
|
}
|
|
|
|
|
loadSettingsSnapshot()
|
|
|
|
|
})
|
|
|
|
|
|
2026-05-29 13:17:39 +08:00
|
|
|
watch(
|
|
|
|
|
() => [route.name, route.query.section],
|
|
|
|
|
() => {
|
|
|
|
|
const nextSectionId = resolveInitialSectionId(route)
|
|
|
|
|
if (activeSection.value !== nextSectionId) {
|
|
|
|
|
activateSection(nextSectionId, { skipRouteSync: true })
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
2026-05-22 23:47:28 +08:00
|
|
|
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,
|
2026-05-27 09:17:57 +08:00
|
|
|
activeThemeSkin,
|
|
|
|
|
activeThemeSkinId,
|
|
|
|
|
archiveCycleOptions,
|
2026-05-22 23:47:28 +08:00
|
|
|
activateSection,
|
2026-06-24 12:35:59 +08:00
|
|
|
cacheClearFailed,
|
|
|
|
|
cacheClearItems,
|
|
|
|
|
cacheClearMessage,
|
|
|
|
|
cacheClearing,
|
|
|
|
|
clearAllCaches,
|
2026-05-22 23:47:28 +08:00
|
|
|
clearRenderSecretMask,
|
|
|
|
|
completedSectionCount,
|
|
|
|
|
logLevels,
|
|
|
|
|
logoInputRef,
|
|
|
|
|
pageState,
|
|
|
|
|
providerOptions,
|
|
|
|
|
sessionRetentionOptions,
|
|
|
|
|
sessionRetentionPickerOpen,
|
|
|
|
|
sessionRetentionPickerRef,
|
|
|
|
|
saveActiveSection,
|
|
|
|
|
sectionStatus,
|
|
|
|
|
sections,
|
2026-05-29 13:17:39 +08:00
|
|
|
systemLogDetailMode,
|
2026-05-27 09:17:57 +08:00
|
|
|
selectThemeSkin,
|
2026-05-22 23:47:28 +08:00
|
|
|
selectSessionRetentionDays,
|
2026-05-27 09:17:57 +08:00
|
|
|
themeSkinOptions,
|
2026-05-22 23:47:28 +08:00
|
|
|
toggleSessionRetentionPicker,
|
|
|
|
|
closeSessionRetentionPicker,
|
|
|
|
|
toggleBoolean,
|
|
|
|
|
toggleHermesFlag,
|
|
|
|
|
toggleHermesMaster,
|
|
|
|
|
toggleHermesTask,
|
|
|
|
|
updateHermesTaskTime,
|
|
|
|
|
triggerLogoUpload,
|
|
|
|
|
handleLogoUpload
|
|
|
|
|
}
|
|
|
|
|
}
|