diff --git a/src/composables/useSystemConfig.ts b/src/composables/useSystemConfig.ts index 1d27030..b6ac3b7 100644 --- a/src/composables/useSystemConfig.ts +++ b/src/composables/useSystemConfig.ts @@ -2,7 +2,13 @@ import { ref, computed } from 'vue' import { ConfigService } from '@/services' import { useAlertStore } from '@/stores/alertStore' import { useConfigStore } from '@/stores/configStore' -import type { ConfigState } from '@/types' +import type { + ConfigDiagnosticItem, + ConfigDiagnosticsResponse, + ConfigDiagnosticSeverity, + ConfigDiagnosticSummary, + ConfigState +} from '@/types' import { DEFAULT_CONFIG_STATE, readStoredConfig } from '@/utils/config-storage' import { getErrorMessage } from '@/utils/common' import { @@ -15,6 +21,122 @@ import { type ConfigFlagKey = 'enableChunk' | 's3_proxy' | 'openUpload' +type NormalizedConfigDiagnostics = { + items: ConfigDiagnosticItem[] + diagnosticItems: ConfigDiagnosticItem[] + diagnostic_items: ConfigDiagnosticItem[] + summary: ConfigDiagnosticSummary + diagnosticSummary: ConfigDiagnosticSummary + diagnostic_summary: ConfigDiagnosticSummary +} + +type ConfigDiagnosticsSource = + | (Partial & + ConfigDiagnosticsResponse & { + diagnostics?: ConfigDiagnosticsResponse + }) + | null + | undefined + +const diagnosticSeverities: ConfigDiagnosticSeverity[] = ['danger', 'warning', 'success', 'neutral'] + +const emptyDiagnosticSummary = (): ConfigDiagnosticSummary => ({ + total: 0, + dangerCount: 0, + warningCount: 0, + successCount: 0, + neutralCount: 0, + strongestSeverity: 'success' +}) + +const emptyConfigDiagnostics = (): NormalizedConfigDiagnostics => { + const items: ConfigDiagnosticItem[] = [] + const summary = emptyDiagnosticSummary() + return { + items, + diagnosticItems: items, + diagnostic_items: items, + summary, + diagnosticSummary: summary, + diagnostic_summary: summary + } +} + +const normalizeDiagnosticSeverity = (severity: string | undefined): ConfigDiagnosticSeverity => + diagnosticSeverities.includes(severity as ConfigDiagnosticSeverity) + ? (severity as ConfigDiagnosticSeverity) + : 'neutral' + +const normalizeConfigDiagnostics = ( + value: ConfigDiagnosticsSource +): NormalizedConfigDiagnostics => { + const diagnostics = value?.diagnostics + const items = ( + diagnostics?.items ?? + diagnostics?.diagnosticItems ?? + diagnostics?.diagnostic_items ?? + value?.items ?? + value?.diagnosticItems ?? + value?.diagnostic_items ?? + [] + ) + .filter((item) => item && item.key) + .map((item) => ({ + ...item, + count: Number(item.count || 0), + priority: Number(item.priority || 0), + severity: normalizeDiagnosticSeverity(item.severity), + fields: item.fields || (item.field ? [String(item.field)] : []) + })) + + const rawSummary = + diagnostics?.summary ?? + diagnostics?.diagnosticSummary ?? + diagnostics?.diagnostic_summary ?? + value?.summary ?? + value?.diagnosticSummary ?? + value?.diagnostic_summary ?? + {} + const summary: ConfigDiagnosticSummary = { + total: Number(rawSummary.total ?? items.length), + dangerCount: Number(rawSummary.dangerCount ?? rawSummary.danger_count ?? 0), + danger_count: Number(rawSummary.dangerCount ?? rawSummary.danger_count ?? 0), + warningCount: Number(rawSummary.warningCount ?? rawSummary.warning_count ?? 0), + warning_count: Number(rawSummary.warningCount ?? rawSummary.warning_count ?? 0), + successCount: Number(rawSummary.successCount ?? rawSummary.success_count ?? 0), + success_count: Number(rawSummary.successCount ?? rawSummary.success_count ?? 0), + neutralCount: Number(rawSummary.neutralCount ?? rawSummary.neutral_count ?? 0), + neutral_count: Number(rawSummary.neutralCount ?? rawSummary.neutral_count ?? 0), + strongestSeverity: normalizeDiagnosticSeverity( + rawSummary.strongestSeverity ?? rawSummary.strongest_severity + ), + strongest_severity: normalizeDiagnosticSeverity( + rawSummary.strongestSeverity ?? rawSummary.strongest_severity + ) + } + + return { + items, + diagnosticItems: items, + diagnostic_items: items, + summary, + diagnosticSummary: summary, + diagnostic_summary: summary + } +} + +const stripDiagnosticFields = (nextConfig: Partial): Partial => { + const { + diagnostics: _diagnostics, + diagnosticItems: _diagnosticItems, + diagnostic_items: _diagnosticItemsSnake, + diagnosticSummary: _diagnosticSummary, + diagnostic_summary: _diagnosticSummarySnake, + ...editableConfig + } = nextConfig + return editableConfig +} + export function useSystemConfig() { const alertStore = useAlertStore() const configStore = useConfigStore() @@ -23,7 +145,9 @@ export function useSystemConfig() { const config = ref({ ...DEFAULT_CONFIG_STATE }) const isRefreshing = ref(false) const isSaving = ref(false) + const isDiagnosticsRefreshing = ref(false) const savedPayloadSnapshot = ref('') + const configDiagnostics = ref(emptyConfigDiagnostics()) const fileSize = ref(1) const sizeUnit = ref('MB') const saveTime = ref(1) @@ -47,11 +171,14 @@ export function useSystemConfig() { const snapshotPayload = (payload: Partial) => JSON.stringify(payload) - const normalizeEditableConfig = (nextConfig: Partial): ConfigState => ({ - ...DEFAULT_CONFIG_STATE, - ...nextConfig, - admin_token: '' - }) + const normalizeEditableConfig = (nextConfig: Partial): ConfigState => { + const editableConfig = stripDiagnosticFields(nextConfig) + return { + ...DEFAULT_CONFIG_STATE, + ...editableConfig, + admin_token: '' + } + } const markConfigSaved = () => { savedPayloadSnapshot.value = snapshotPayload(buildSubmitPayload()) @@ -76,6 +203,24 @@ export function useSystemConfig() { configStore.updateConfig(configData) } + const refreshDiagnostics = async (showError = true) => { + try { + isDiagnosticsRefreshing.value = true + const response = await ConfigService.getDiagnostics() + if (response.code === 200 && response.detail) { + configDiagnostics.value = normalizeConfigDiagnostics(response.detail) + } else { + throw new Error(response.message || '获取配置诊断失败') + } + } catch (error) { + if (showError) { + alertStore.showAlert(getErrorMessage(error, '获取配置诊断失败'), 'error') + } + } finally { + isDiagnosticsRefreshing.value = false + } + } + // 获取系统配置 const fetchConfig = async (): Promise => { try { @@ -84,6 +229,7 @@ export function useSystemConfig() { const response = await ConfigService.getConfig() if (response.code === 200 && response.detail) { + configDiagnostics.value = normalizeConfigDiagnostics(response.detail) config.value = normalizeEditableConfig(response.detail) const notifyMessage = configStore.applyRemoteConfig(config.value) if (notifyMessage) { @@ -99,6 +245,7 @@ export function useSystemConfig() { const storedConfig = getStoredConfig() if (storedConfig) { config.value = storedConfig + configDiagnostics.value = emptyConfigDiagnostics() return config.value } @@ -121,6 +268,7 @@ export function useSystemConfig() { syncConfigForm(config.value) markConfigSaved() saveConfigToStorage(config.value) + await refreshDiagnostics(false) alertStore.showAlert('配置更新成功!', 'success') return true } else { @@ -183,6 +331,9 @@ export function useSystemConfig() { return Math.round(config.value.uploadSize / 1024 / 1024) }) + const configDiagnosticItems = computed(() => configDiagnostics.value.items) + const configDiagnosticSummary = computed(() => configDiagnostics.value.summary) + const isConfigLoaded = computed(() => { return config.value.name !== DEFAULT_CONFIG_STATE.name || !isLoading.value }) @@ -193,7 +344,10 @@ export function useSystemConfig() { isLoading, isRefreshing, isSaving, + isDiagnosticsRefreshing, isDirty, + configDiagnosticItems, + configDiagnosticSummary, fileSize, sizeUnit, saveTime, @@ -207,6 +361,7 @@ export function useSystemConfig() { fetchConfig, updateConfig, refreshConfig, + refreshDiagnostics, submitConfig, toggleConfigFlag, initConfig, diff --git a/src/i18n/locales/en-US.ts b/src/i18n/locales/en-US.ts index 9d11baa..983e3ae 100644 --- a/src/i18n/locales/en-US.ts +++ b/src/i18n/locales/en-US.ts @@ -822,7 +822,66 @@ export default { saving: 'Saving', unsavedChanges: 'Unsaved configuration changes', allChangesSaved: 'All configuration changes are saved', - refreshBlocked: 'Save current changes before refreshing' + refreshBlocked: 'Save current changes before refreshing', + diagnosticsTitle: 'Configuration Checkup', + diagnosticsDesc: + 'Automatically checks security, storage, upload, and retention policies. Click a suggestion to jump to the related field.', + diagnosticsRefresh: 'Refresh checkup', + diagnosticsTotal: 'Suggestions', + diagnosticsDanger: 'High risk', + diagnosticsWarning: 'Action needed', + diagnosticsFocusAction: 'Locate setting', + diagnosticSeverity: { + danger: 'High risk', + warning: 'Action needed', + success: 'Stable', + neutral: 'Suggestion' + }, + diagnosticsItems: { + default_admin_password: { + title: 'Default admin password is still in use', + description: + 'The default admin password is a security risk. Set a strong password as soon as possible.' + }, + s3_incomplete: { + title: 'S3 configuration is incomplete', + description: 'S3 is missing {count} fields. Complete them before enabling object storage.' + }, + webdav_incomplete: { + title: 'WebDAV configuration is incomplete', + description: + 'WebDAV is missing {count} fields. Complete them before enabling remote storage.' + }, + guest_upload_retention: { + title: 'Guest uploads have no retention limit', + description: + 'Guest upload is enabled with unlimited retention. Set a default retention window to reduce storage pressure.' + }, + chunking_recommended: { + title: 'Chunked upload is recommended', + description: + 'Enable chunked upload for large files to improve upload stability and recovery after failures.' + }, + upload_guard_disabled: { + title: 'Upload rate limiting is disabled', + description: + 'Upload count limits are not active. Set an upload window and maximum upload count.' + }, + access_guard_disabled: { + title: 'Access protection is disabled', + description: + 'Error access protection is not active. Set a detection window and maximum error count.' + }, + expiration_style_empty: { + title: 'Expiration method is not configured', + description: + 'Choose a default expiration method so uploaded files are retained or expired as expected.' + }, + healthy: { + title: 'Configuration looks stable', + description: 'No configuration risks need priority attention right now.' + } + } }, systemSettings: { title: 'System Settings', diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index 583d9c3..4d53d0d 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -842,7 +842,58 @@ export default { saving: '保存中', unsavedChanges: '有未保存的配置变更', allChangesSaved: '所有配置已保存', - refreshBlocked: '请先保存当前变更再刷新' + refreshBlocked: '请先保存当前变更再刷新', + diagnosticsTitle: '配置体检', + diagnosticsDesc: '自动检查安全、存储、上传和保留策略,点击建议可定位到相关配置。', + diagnosticsRefresh: '刷新体检', + diagnosticsTotal: '建议项', + diagnosticsDanger: '高风险', + diagnosticsWarning: '需处理', + diagnosticsFocusAction: '定位配置', + diagnosticSeverity: { + danger: '高风险', + warning: '需处理', + success: '稳定', + neutral: '建议' + }, + diagnosticsItems: { + default_admin_password: { + title: '管理员密码仍为默认值', + description: '默认管理员密码存在安全风险,建议立即设置一个强密码。' + }, + s3_incomplete: { + title: 'S3 配置不完整', + description: '当前 S3 存储缺少 {count} 项必要配置,补齐后再启用对象存储。' + }, + webdav_incomplete: { + title: 'WebDAV 配置不完整', + description: '当前 WebDAV 存储缺少 {count} 项必要配置,补齐后再启用远程存储。' + }, + guest_upload_retention: { + title: '游客上传缺少保留上限', + description: '游客上传已开启且保存时间不受限,建议设置默认保留期降低存储压力。' + }, + chunking_recommended: { + title: '建议开启分片上传', + description: '大文件上传场景建议启用分片上传,提升上传稳定性和失败恢复能力。' + }, + upload_guard_disabled: { + title: '上传限流未启用', + description: '当前上传次数限制未生效,建议设置上传窗口和次数上限。' + }, + access_guard_disabled: { + title: '访问保护未启用', + description: '当前错误访问保护未生效,建议设置检测窗口和错误次数上限。' + }, + expiration_style_empty: { + title: '未配置过期方式', + description: '建议选择默认过期方式,确保上传文件按预期保留或失效。' + }, + healthy: { + title: '配置状态稳定', + description: '当前未发现需要优先处理的配置风险。' + } + } }, dashboard: { title: '仪表盘', diff --git a/src/services/config.ts b/src/services/config.ts index 0f183bf..d7c5e56 100644 --- a/src/services/config.ts +++ b/src/services/config.ts @@ -1,5 +1,5 @@ import api from './client' -import type { ApiResponse, ConfigState } from '@/types' +import type { ApiResponse, ConfigDiagnosticsResponse, ConfigState } from '@/types' type PublicConfigEnvelope = { config?: Partial @@ -17,6 +17,10 @@ export class ConfigService { return api.get('/admin/config/get') } + static async getDiagnostics(): Promise> { + return api.get('/admin/config/diagnostics') + } + static async getUserConfig(): Promise> { try { const response = (await api.get('/api/v1/config')) as ApiResponse< diff --git a/src/types/config.ts b/src/types/config.ts index 1272c0d..ee1e728 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -15,6 +15,53 @@ export interface ThemeChoice { version: string } +export type ConfigDiagnosticSeverity = 'danger' | 'warning' | 'success' | 'neutral' + +export interface ConfigDiagnosticAction { + type?: string + field?: keyof ConfigState | string | null + fields?: string[] + category?: string +} + +export interface ConfigDiagnosticItem { + key: string + severity: ConfigDiagnosticSeverity | string + category: string + priority?: number + count?: number + field?: keyof ConfigState | string | null + fields?: string[] + action?: ConfigDiagnosticAction + actionType?: string + action_type?: string + targetField?: keyof ConfigState | string | null + target_field?: keyof ConfigState | string | null +} + +export interface ConfigDiagnosticSummary { + total: number + dangerCount: number + danger_count?: number + warningCount: number + warning_count?: number + successCount: number + success_count?: number + neutralCount: number + neutral_count?: number + strongestSeverity: ConfigDiagnosticSeverity + strongest_severity?: ConfigDiagnosticSeverity +} + +export interface ConfigDiagnosticsResponse { + items?: ConfigDiagnosticItem[] + diagnosticItems?: ConfigDiagnosticItem[] + diagnostic_items?: ConfigDiagnosticItem[] + summary?: Partial + diagnosticSummary?: Partial + diagnostic_summary?: Partial +} + export interface ConfigState { name: string description: string @@ -52,4 +99,9 @@ export interface ConfigState { webdav_url: string webdav_username: string webdav_password: string + diagnostics?: ConfigDiagnosticsResponse + diagnosticItems?: ConfigDiagnosticItem[] + diagnostic_items?: ConfigDiagnosticItem[] + diagnosticSummary?: Partial + diagnostic_summary?: Partial } diff --git a/src/views/manage/SystemSettingsView.vue b/src/views/manage/SystemSettingsView.vue index 317ed4c..7dae7d2 100644 --- a/src/views/manage/SystemSettingsView.vue +++ b/src/views/manage/SystemSettingsView.vue @@ -1,11 +1,20 @@