import { useState, useEffect, useRef } from 'react' import { useLocation } from 'react-router-dom' import { useAppStore } from '../stores/appStore' import { useChatStore } from '../stores/chatStore' import { useThemeStore, themes } from '../stores/themeStore' import { useAnalyticsStore } from '../stores/analyticsStore' import { dialog } from '../services/ipc' import * as configService from '../services/config' import { Eye, EyeOff, FolderSearch, FolderOpen, Search, Copy, RotateCcw, Trash2, Plug, Check, Sun, Moon, Monitor, Palette, Database, HardDrive, Info, RefreshCw, ChevronDown, Download, Mic, ShieldCheck, Fingerprint, Lock, KeyRound, Bell, Globe, BarChart2, X, UserRound, Sparkles, Loader2, CheckCircle2, XCircle } from 'lucide-react' import { Avatar } from '../components/Avatar' import './SettingsPage.scss' type SettingsTab = 'appearance' | 'notification' | 'antiRevoke' | 'database' | 'models' | 'cache' | 'api' | 'updates' | 'security' | 'about' | 'analytics' | 'insight' const tabs: { id: SettingsTab; label: string; icon: React.ElementType }[] = [ { id: 'appearance', label: '外观', icon: Palette }, { id: 'notification', label: '通知', icon: Bell }, { id: 'antiRevoke', label: '防撤回', icon: RotateCcw }, { id: 'database', label: '数据库连接', icon: Database }, { id: 'models', label: '模型管理', icon: Mic }, { id: 'cache', label: '缓存', icon: HardDrive }, { id: 'api', label: 'API 服务', icon: Globe }, { id: 'analytics', label: '分析', icon: BarChart2 }, { id: 'insight', label: 'AI 见解', icon: Sparkles }, { id: 'security', label: '安全', icon: ShieldCheck }, { id: 'updates', label: '版本更新', icon: RefreshCw }, { id: 'about', label: '关于', icon: Info } ] const isMac = navigator.userAgent.toLowerCase().includes('mac') const isLinux = navigator.userAgent.toLowerCase().includes('linux') const isWindows = !isMac && !isLinux const dbDirName = isMac ? '2.0b4.0.9 目录' : 'xwechat_files 目录' const dbPathPlaceholder = isMac ? '例如: ~/Library/Containers/com.tencent.xinWeChat/Data/Library/Application Support/com.tencent.xinWeChat/2.0b4.0.9' : isLinux ? '例如: ~/.local/share/WeChat/xwechat_files 或者 ~/Documents/xwechat_files' : '例如: C:\\Users\\xxx\\Documents\\xwechat_files' interface WxidOption { wxid: string modifiedTime: number nickname?: string avatarUrl?: string } interface SettingsPageProps { onClose?: () => void } function SettingsPage({ onClose }: SettingsPageProps = {}) { const location = useLocation() const { isDbConnected, setDbConnected, setLoading, reset, updateInfo, setUpdateInfo, isDownloading, setIsDownloading, downloadProgress, setDownloadProgress, showUpdateDialog, setShowUpdateDialog, } = useAppStore() const chatSessions = useChatStore((state) => state.sessions) const setChatSessions = useChatStore((state) => state.setSessions) const resetChatStore = useChatStore((state) => state.reset) const { currentTheme, themeMode, setTheme, setThemeMode } = useThemeStore() const [systemDark, setSystemDark] = useState(() => window.matchMedia('(prefers-color-scheme: dark)').matches) useEffect(() => { const mq = window.matchMedia('(prefers-color-scheme: dark)') const handler = (e: MediaQueryListEvent) => setSystemDark(e.matches) mq.addEventListener('change', handler) return () => mq.removeEventListener('change', handler) }, []) const effectiveMode = themeMode === 'system' ? (systemDark ? 'dark' : 'light') : themeMode const clearAnalyticsStoreCache = useAnalyticsStore((state) => state.clearCache) const [activeTab, setActiveTab] = useState('appearance') const [decryptKey, setDecryptKey] = useState('') const [imageXorKey, setImageXorKey] = useState('') const [imageAesKey, setImageAesKey] = useState('') const [dbPath, setDbPath] = useState('') const [wxid, setWxid] = useState('') const [wxidOptions, setWxidOptions] = useState([]) const [showWxidSelect, setShowWxidSelect] = useState(false) const [cachePath, setCachePath] = useState('') const [imageKeyProgress, setImageKeyProgress] = useState(0) const [imageKeyPercent, setImageKeyPercent] = useState(null) const [logEnabled, setLogEnabled] = useState(false) const [whisperModelName, setWhisperModelName] = useState('base') const [whisperModelDir, setWhisperModelDir] = useState('') const [isWhisperDownloading, setIsWhisperDownloading] = useState(false) const [whisperDownloadProgress, setWhisperDownloadProgress] = useState(0) const [whisperProgressData, setWhisperProgressData] = useState<{ downloaded: number; total: number; speed: number }>({ downloaded: 0, total: 0, speed: 0 }) const [whisperModelStatus, setWhisperModelStatus] = useState<{ exists: boolean; modelPath?: string; tokensPath?: string } | null>(null) const [httpApiToken, setHttpApiToken] = useState('') const formatBytes = (bytes: number) => { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }; const generateRandomToken = async () => { // 生成 32 字符的十六进制随机字符串 (16 bytes) const array = new Uint8Array(16) crypto.getRandomValues(array) const token = Array.from(array).map(b => b.toString(16).padStart(2, '0')).join('') setHttpApiToken(token) await configService.setHttpApiToken(token) showMessage('已生成��保存新的 Access Token', true) } const clearApiToken = async () => { setHttpApiToken('') await configService.setHttpApiToken('') showMessage('已清除 Access Token,API 将允许无鉴权访问', true) } const [autoTranscribeVoice, setAutoTranscribeVoice] = useState(false) const [transcribeLanguages, setTranscribeLanguages] = useState(['zh']) const [notificationEnabled, setNotificationEnabled] = useState(true) const [notificationPosition, setNotificationPosition] = useState<'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'top-center'>('top-right') const [notificationFilterMode, setNotificationFilterMode] = useState<'all' | 'whitelist' | 'blacklist'>('all') const [notificationFilterList, setNotificationFilterList] = useState([]) const [launchAtStartup, setLaunchAtStartup] = useState(false) const [launchAtStartupSupported, setLaunchAtStartupSupported] = useState(isWindows || isMac) const [launchAtStartupReason, setLaunchAtStartupReason] = useState('') const [windowCloseBehavior, setWindowCloseBehavior] = useState('ask') const [quoteLayout, setQuoteLayout] = useState('quote-top') const [updateChannel, setUpdateChannel] = useState('stable') const [filterSearchKeyword, setFilterSearchKeyword] = useState('') const [filterModeDropdownOpen, setFilterModeDropdownOpen] = useState(false) const [positionDropdownOpen, setPositionDropdownOpen] = useState(false) const [closeBehaviorDropdownOpen, setCloseBehaviorDropdownOpen] = useState(false) const [wordCloudExcludeWords, setWordCloudExcludeWords] = useState([]) const [excludeWordsInput, setExcludeWordsInput] = useState('') // 数据收集同意状态 const [analyticsConsent, setAnalyticsConsent] = useState(false) const [isLoading, setIsLoadingState] = useState(false) const [isTesting, setIsTesting] = useState(false) const [isDetectingPath, setIsDetectingPath] = useState(false) const [isFetchingDbKey, setIsFetchingDbKey] = useState(false) const [isFetchingImageKey, setIsFetchingImageKey] = useState(false) const [isCheckingUpdate, setIsCheckingUpdate] = useState(false) const [isUpdatingLaunchAtStartup, setIsUpdatingLaunchAtStartup] = useState(false) const [appVersion, setAppVersion] = useState('') const [message, setMessage] = useState<{ text: string; success: boolean } | null>(null) const [showDecryptKey, setShowDecryptKey] = useState(false) const [dbKeyStatus, setDbKeyStatus] = useState('') const [imageKeyStatus, setImageKeyStatus] = useState('') const [isManualStartPrompt, setIsManualStartPrompt] = useState(false) const [isClearingAnalyticsCache, setIsClearingAnalyticsCache] = useState(false) const [isClearingImageCache, setIsClearingImageCache] = useState(false) const [isClearingAllCache, setIsClearingAllCache] = useState(false) const [isClosing, setIsClosing] = useState(false) const saveTimersRef = useRef>>({}) // 安全设置 state const [authEnabled, setAuthEnabled] = useState(false) const [authUseHello, setAuthUseHello] = useState(false) const [helloAvailable, setHelloAvailable] = useState(false) const [newPassword, setNewPassword] = useState('') const [confirmPassword, setConfirmPassword] = useState('') const [oldPassword, setOldPassword] = useState('') const [helloPassword, setHelloPassword] = useState('') const [disableLockPassword, setDisableLockPassword] = useState('') const [showDisableLockInput, setShowDisableLockInput] = useState(false) const [isLockMode, setIsLockMode] = useState(false) const [isSettingHello, setIsSettingHello] = useState(false) // HTTP API 设置 state const [httpApiEnabled, setHttpApiEnabled] = useState(false) const [httpApiPort, setHttpApiPort] = useState(5031) const [httpApiHost, setHttpApiHost] = useState('127.0.0.1') const [httpApiRunning, setHttpApiRunning] = useState(false) const [httpApiMediaExportPath, setHttpApiMediaExportPath] = useState('') const [isTogglingApi, setIsTogglingApi] = useState(false) const [showApiWarning, setShowApiWarning] = useState(false) const [messagePushEnabled, setMessagePushEnabled] = useState(false) const [antiRevokeSearchKeyword, setAntiRevokeSearchKeyword] = useState('') const [antiRevokeSelectedIds, setAntiRevokeSelectedIds] = useState>(new Set()) const [antiRevokeStatusMap, setAntiRevokeStatusMap] = useState>({}) const [isAntiRevokeRefreshing, setIsAntiRevokeRefreshing] = useState(false) const [isAntiRevokeInstalling, setIsAntiRevokeInstalling] = useState(false) const [isAntiRevokeUninstalling, setIsAntiRevokeUninstalling] = useState(false) const [antiRevokeSummary, setAntiRevokeSummary] = useState<{ action: 'refresh' | 'install' | 'uninstall'; success: number; failed: number } | null>(null) const isClearingCache = isClearingAnalyticsCache || isClearingImageCache || isClearingAllCache // AI 见解 state const [aiInsightEnabled, setAiInsightEnabled] = useState(false) const [aiInsightApiBaseUrl, setAiInsightApiBaseUrl] = useState('') const [aiInsightApiKey, setAiInsightApiKey] = useState('') const [aiInsightApiModel, setAiInsightApiModel] = useState('gpt-4o-mini') const [aiInsightSilenceDays, setAiInsightSilenceDays] = useState(3) const [aiInsightAllowContext, setAiInsightAllowContext] = useState(false) const [isTestingInsight, setIsTestingInsight] = useState(false) const [insightTestResult, setInsightTestResult] = useState<{ success: boolean; message: string } | null>(null) const [showInsightApiKey, setShowInsightApiKey] = useState(false) const [isTriggeringInsightTest, setIsTriggeringInsightTest] = useState(false) const [insightTriggerResult, setInsightTriggerResult] = useState<{ success: boolean; message: string } | null>(null) const [aiInsightWhitelistEnabled, setAiInsightWhitelistEnabled] = useState(false) const [aiInsightWhitelist, setAiInsightWhitelist] = useState>(new Set()) const [insightWhitelistSearch, setInsightWhitelistSearch] = useState('') const [aiInsightCooldownMinutes, setAiInsightCooldownMinutes] = useState(120) const [aiInsightScanIntervalHours, setAiInsightScanIntervalHours] = useState(4) const [aiInsightContextCount, setAiInsightContextCount] = useState(40) const [aiInsightSystemPrompt, setAiInsightSystemPrompt] = useState('') const [aiInsightTelegramEnabled, setAiInsightTelegramEnabled] = useState(false) const [aiInsightTelegramToken, setAiInsightTelegramToken] = useState('') const [aiInsightTelegramChatIds, setAiInsightTelegramChatIds] = useState('') const [isWayland, setIsWayland] = useState(false) useEffect(() => { const checkWaylandStatus = async () => { if (window.electronAPI?.app?.checkWayland) { try { const wayland = await window.electronAPI.app.checkWayland() setIsWayland(wayland) } catch (e) { console.error('检查 Wayland 状态失败:', e) } } } checkWaylandStatus() }, []) // 检查 Hello 可用性 useEffect(() => { setHelloAvailable(isWindows) }, []) // 检查 HTTP API 服务状态 useEffect(() => { const checkApiStatus = async () => { try { const status = await window.electronAPI.http.status() setHttpApiRunning(status.running) if (status.port) { setHttpApiPort(status.port) } if (status.mediaExportPath) { setHttpApiMediaExportPath(status.mediaExportPath) } } catch (e) { console.error('检查 API 状态失败:', e) } } checkApiStatus() }, []) useEffect(() => { loadConfig() loadAppVersion() return () => { Object.values(saveTimersRef.current).forEach((timer) => clearTimeout(timer)) } }, []) useEffect(() => { const initialTab = (location.state as { initialTab?: SettingsTab } | null)?.initialTab if (!initialTab) return setActiveTab(initialTab) }, [location.state]) useEffect(() => { if (!onClose) return const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') { handleClose() } } document.addEventListener('keydown', handleKeyDown) return () => document.removeEventListener('keydown', handleKeyDown) }, [onClose]) useEffect(() => { const removeDb = window.electronAPI.key.onDbKeyStatus((payload: { message: string; level: number }) => { setDbKeyStatus(payload.message) }) const removeImage = window.electronAPI.key.onImageKeyStatus((payload: { message: string, percent?: number }) => { let msg = payload.message; let pct = payload.percent; // 如果后端没有显式传 percent,则用正则从字符串中提取如 "(12.5%)" if (pct === undefined) { const match = msg.match(/\(([\d.]+)%\)/); if (match) { pct = parseFloat(match[1]); // 将百分比从文本中剥离,让 UI 更清爽 msg = msg.replace(/\s*\([\d.]+%\)/, ''); } } setImageKeyStatus(msg); if (pct !== undefined) { setImageKeyPercent(pct); } else if (msg.includes('启动多核') || msg.includes('定位') || msg.includes('准备')) { // 预热阶段 setImageKeyPercent(0); } }) return () => { removeDb?.() removeImage?.() } }, []) // 点击外部关闭自定义下拉框 useEffect(() => { const handleClickOutside = (e: MouseEvent) => { const target = e.target as HTMLElement if (!target.closest('.custom-select')) { setFilterModeDropdownOpen(false) setPositionDropdownOpen(false) setCloseBehaviorDropdownOpen(false) } } if (filterModeDropdownOpen || positionDropdownOpen || closeBehaviorDropdownOpen) { document.addEventListener('click', handleClickOutside) } return () => { document.removeEventListener('click', handleClickOutside) } }, [closeBehaviorDropdownOpen, filterModeDropdownOpen, positionDropdownOpen]) const loadConfig = async () => { try { const savedKey = await configService.getDecryptKey() const savedPath = await configService.getDbPath() const savedWxid = await configService.getMyWxid() const savedCachePath = await configService.getCachePath() const savedExportPath = await configService.getExportPath() const savedLogEnabled = await configService.getLogEnabled() const savedImageXorKey = await configService.getImageXorKey() const savedImageAesKey = await configService.getImageAesKey() const savedWhisperModelName = await configService.getWhisperModelName() const savedWhisperModelDir = await configService.getWhisperModelDir() const savedAutoTranscribe = await configService.getAutoTranscribeVoice() const savedTranscribeLanguages = await configService.getTranscribeLanguages() const savedNotificationEnabled = await configService.getNotificationEnabled() const savedNotificationPosition = await configService.getNotificationPosition() const savedNotificationFilterMode = await configService.getNotificationFilterMode() const savedNotificationFilterList = await configService.getNotificationFilterList() const savedMessagePushEnabled = await configService.getMessagePushEnabled() const savedLaunchAtStartupStatus = await window.electronAPI.app.getLaunchAtStartupStatus() const savedWindowCloseBehavior = await configService.getWindowCloseBehavior() const savedQuoteLayout = await configService.getQuoteLayout() const savedUpdateChannel = await configService.getUpdateChannel() const savedAuthEnabled = await window.electronAPI.auth.verifyEnabled() const savedAuthUseHello = await configService.getAuthUseHello() const savedIsLockMode = await window.electronAPI.auth.isLockMode() const savedHttpApiToken = await configService.getHttpApiToken() if (savedHttpApiToken) setHttpApiToken(savedHttpApiToken) const savedApiPort = await configService.getHttpApiPort() if (savedApiPort) setHttpApiPort(savedApiPort) const savedApiHost = await configService.getHttpApiHost() if (savedApiHost) setHttpApiHost(savedApiHost) setAuthEnabled(savedAuthEnabled) setAuthUseHello(savedAuthUseHello) setIsLockMode(savedIsLockMode) if (savedPath) setDbPath(savedPath) if (savedWxid) setWxid(savedWxid) if (savedCachePath) setCachePath(savedCachePath) const wxidConfig = savedWxid ? await configService.getWxidConfig(savedWxid) : null const decryptKeyToUse = wxidConfig?.decryptKey ?? savedKey ?? '' const imageXorKeyToUse = typeof wxidConfig?.imageXorKey === 'number' ? wxidConfig.imageXorKey : savedImageXorKey const imageAesKeyToUse = wxidConfig?.imageAesKey ?? savedImageAesKey ?? '' setDecryptKey(decryptKeyToUse) if (typeof imageXorKeyToUse === 'number') { setImageXorKey(`0x${imageXorKeyToUse.toString(16).toUpperCase().padStart(2, '0')}`) } else { setImageXorKey('') } setImageAesKey(imageAesKeyToUse) setLogEnabled(savedLogEnabled) setAutoTranscribeVoice(savedAutoTranscribe) setTranscribeLanguages(savedTranscribeLanguages) setNotificationEnabled(savedNotificationEnabled) setNotificationPosition(savedNotificationPosition) setNotificationFilterMode(savedNotificationFilterMode) setNotificationFilterList(savedNotificationFilterList) setMessagePushEnabled(savedMessagePushEnabled) setLaunchAtStartup(savedLaunchAtStartupStatus.enabled) setLaunchAtStartupSupported(savedLaunchAtStartupStatus.supported) setLaunchAtStartupReason(savedLaunchAtStartupStatus.reason || '') setWindowCloseBehavior(savedWindowCloseBehavior) setQuoteLayout(savedQuoteLayout) if (savedUpdateChannel) { setUpdateChannel(savedUpdateChannel) } else { const currentVersion = await window.electronAPI.app.getVersion() if (/^0\.\d{2}\.\d+$/i.test(currentVersion) || /-preview\.\d+\.\d+$/i.test(currentVersion)) { setUpdateChannel('preview') } else if (/^\d{2}\.\d{1,2}\.\d{1,2}$/i.test(currentVersion) || /-dev\.\d+\.\d+\.\d+$/i.test(currentVersion) || /(alpha|beta|rc)/i.test(currentVersion)) { setUpdateChannel('dev') } else { setUpdateChannel('stable') } } const savedExcludeWords = await configService.getWordCloudExcludeWords() setWordCloudExcludeWords(savedExcludeWords) setExcludeWordsInput(savedExcludeWords.join('\n')) const savedAnalyticsConsent = await configService.getAnalyticsConsent() setAnalyticsConsent(savedAnalyticsConsent ?? false) // 如果语言列表为空,保存默认值 if (!savedTranscribeLanguages || savedTranscribeLanguages.length === 0) { const defaultLanguages = ['zh'] setTranscribeLanguages(defaultLanguages) await configService.setTranscribeLanguages(defaultLanguages) } if (savedWhisperModelDir) setWhisperModelDir(savedWhisperModelDir) // 加载 AI 见解配置 const savedAiInsightEnabled = await configService.getAiInsightEnabled() const savedAiInsightApiBaseUrl = await configService.getAiInsightApiBaseUrl() const savedAiInsightApiKey = await configService.getAiInsightApiKey() const savedAiInsightApiModel = await configService.getAiInsightApiModel() const savedAiInsightSilenceDays = await configService.getAiInsightSilenceDays() const savedAiInsightAllowContext = await configService.getAiInsightAllowContext() const savedAiInsightWhitelistEnabled = await configService.getAiInsightWhitelistEnabled() const savedAiInsightWhitelist = await configService.getAiInsightWhitelist() const savedAiInsightCooldownMinutes = await configService.getAiInsightCooldownMinutes() const savedAiInsightScanIntervalHours = await configService.getAiInsightScanIntervalHours() const savedAiInsightContextCount = await configService.getAiInsightContextCount() const savedAiInsightSystemPrompt = await configService.getAiInsightSystemPrompt() const savedAiInsightTelegramEnabled = await configService.getAiInsightTelegramEnabled() const savedAiInsightTelegramToken = await configService.getAiInsightTelegramToken() const savedAiInsightTelegramChatIds = await configService.getAiInsightTelegramChatIds() setAiInsightEnabled(savedAiInsightEnabled) setAiInsightApiBaseUrl(savedAiInsightApiBaseUrl) setAiInsightApiKey(savedAiInsightApiKey) setAiInsightApiModel(savedAiInsightApiModel) setAiInsightSilenceDays(savedAiInsightSilenceDays) setAiInsightAllowContext(savedAiInsightAllowContext) setAiInsightWhitelistEnabled(savedAiInsightWhitelistEnabled) setAiInsightWhitelist(new Set(savedAiInsightWhitelist)) setAiInsightCooldownMinutes(savedAiInsightCooldownMinutes) setAiInsightScanIntervalHours(savedAiInsightScanIntervalHours) setAiInsightContextCount(savedAiInsightContextCount) setAiInsightSystemPrompt(savedAiInsightSystemPrompt) setAiInsightTelegramEnabled(savedAiInsightTelegramEnabled) setAiInsightTelegramToken(savedAiInsightTelegramToken) setAiInsightTelegramChatIds(savedAiInsightTelegramChatIds) } catch (e: any) { console.error('加载配置失败:', e) } } const handleLaunchAtStartupChange = async (enabled: boolean) => { if (isUpdatingLaunchAtStartup) return try { setIsUpdatingLaunchAtStartup(true) const result = await window.electronAPI.app.setLaunchAtStartup(enabled) setLaunchAtStartup(result.enabled) setLaunchAtStartupSupported(result.supported) setLaunchAtStartupReason(result.reason || '') if (result.success) { showMessage(enabled ? '已开启开机自启动' : '已关闭开机自启动', true) return } showMessage(result.error || result.reason || '设置开机自启动失败', false) } catch (e: any) { showMessage(`设置开机自启动失败: ${e?.message || String(e)}`, false) } finally { setIsUpdatingLaunchAtStartup(false) } } const refreshWhisperStatus = async (modelDirValue = whisperModelDir) => { try { const result = await window.electronAPI.whisper?.getModelStatus() if (result?.success) { setWhisperModelStatus({ exists: Boolean(result.exists), modelPath: result.modelPath, tokensPath: result.tokensPath }) } } catch { setWhisperModelStatus(null) } } const loadAppVersion = async () => { try { const version = await window.electronAPI.app.getVersion() setAppVersion(version) } catch (e: any) { console.error('获取版本号失败:', e) } } // 监听下载进度 useEffect(() => { const removeListener = window.electronAPI.app.onDownloadProgress?.((progress: any) => { setDownloadProgress(progress) }) return () => removeListener?.() }, []) useEffect(() => { const removeListener = window.electronAPI.whisper?.onDownloadProgress?.((payload: { modelName: string; downloadedBytes: number; totalBytes?: number; percent?: number; speed?: number }) => { setWhisperProgressData({ downloaded: payload.downloadedBytes, total: payload.totalBytes || 0, speed: payload.speed || 0 }) if (typeof payload.percent === 'number') { setWhisperDownloadProgress(payload.percent) } }) return () => removeListener?.() }, []) useEffect(() => { void refreshWhisperStatus(whisperModelDir) }, [whisperModelDir]) const handleCheckUpdate = async () => { if (isCheckingUpdate) return setIsCheckingUpdate(true) setUpdateInfo(null) try { const result = await window.electronAPI.app.checkForUpdates() if (result.hasUpdate) { setUpdateInfo(result) setShowUpdateDialog(true) showMessage(`发现新版:${result.version}`, true) } else { showMessage('当前已是最新版', true) } } catch (e: any) { showMessage(`检查更新失败: ${e}`, false) } finally { setIsCheckingUpdate(false) } } const handleUpdateNow = async () => { setShowUpdateDialog(false) setIsDownloading(true) setDownloadProgress({ percent: 0 }) try { showMessage('正在下载更新...', true) await window.electronAPI.app.downloadAndInstall() } catch (e: any) { showMessage(`更新失败: ${e}`, false) setIsDownloading(false) } } const handleIgnoreUpdate = async () => { if (!updateInfo || !updateInfo.version) return try { await window.electronAPI.app.ignoreUpdate(updateInfo.version) setShowUpdateDialog(false) setUpdateInfo(null) showMessage(`已忽略版本 ${updateInfo.version}`, true) } catch (e: any) { showMessage(`操作失败: ${e}`, false) } } const handleUpdateChannelChange = async (channel: configService.UpdateChannel) => { if (channel === updateChannel) return try { setUpdateChannel(channel) await configService.setUpdateChannel(channel) await configService.setIgnoredUpdateVersion('') setUpdateInfo(null) setShowUpdateDialog(false) const channelLabel = channel === 'stable' ? '稳定版' : channel === 'preview' ? '预览版' : '开发版' showMessage(`已切换到${channelLabel}更新渠道,正在检查更新`, true) await handleCheckUpdate() } catch (e: any) { showMessage(`切换更新渠道��败: ${e}`, false) } } const showMessage = (text: string, success: boolean) => { setMessage({ text, success }) setTimeout(() => setMessage(null), 3000) } const handleClose = () => { if (!onClose) return setIsClosing(true) setTimeout(() => { onClose() }, 200) } const normalizeSessionIds = (sessionIds: string[]): string[] => Array.from(new Set((sessionIds || []).map((id) => String(id || '').trim()).filter(Boolean))) const getCurrentAntiRevokeSessionIds = (): string[] => normalizeSessionIds(chatSessions.map((session) => session.username)) const ensureAntiRevokeSessionsLoaded = async (): Promise => { const current = getCurrentAntiRevokeSessionIds() if (current.length > 0) return current const sessionsResult = await window.electronAPI.chat.getSessions() if (!sessionsResult.success || !sessionsResult.sessions) { throw new Error(sessionsResult.error || '加载会话失败') } setChatSessions(sessionsResult.sessions) return normalizeSessionIds(sessionsResult.sessions.map((session) => session.username)) } const markAntiRevokeRowsLoading = (sessionIds: string[]) => { setAntiRevokeStatusMap((prev) => { const next = { ...prev } for (const sessionId of sessionIds) { next[sessionId] = { ...(next[sessionId] || {}), loading: true, error: undefined } } return next }) } const handleRefreshAntiRevokeStatus = async (sessionIds?: string[]) => { if (isAntiRevokeRefreshing || isAntiRevokeInstalling || isAntiRevokeUninstalling) return setAntiRevokeSummary(null) setIsAntiRevokeRefreshing(true) try { const targetIds = normalizeSessionIds( sessionIds && sessionIds.length > 0 ? sessionIds : await ensureAntiRevokeSessionsLoaded() ) if (targetIds.length === 0) { setAntiRevokeStatusMap({}) showMessage('暂无可检查的会话', true) return } markAntiRevokeRowsLoading(targetIds) const result = await window.electronAPI.chat.checkAntiRevokeTriggers(targetIds) if (!result.success || !result.rows) { const errorText = result.error || '防撤回状态检查失败' setAntiRevokeStatusMap((prev) => { const next = { ...prev } for (const sessionId of targetIds) { next[sessionId] = { ...(next[sessionId] || {}), loading: false, error: errorText } } return next }) showMessage(errorText, false) return } const rowMap = new Map() for (const row of result.rows || []) { const sessionId = String(row.sessionId || '').trim() if (!sessionId) continue rowMap.set(sessionId, row) } const mergedRows = targetIds.map((sessionId) => ( rowMap.get(sessionId) || { sessionId, success: false, error: '状态查询未返回结果' } )) const successCount = mergedRows.filter((row) => row.success).length const failedCount = mergedRows.length - successCount setAntiRevokeStatusMap((prev) => { const next = { ...prev } for (const row of mergedRows) { const sessionId = String(row.sessionId || '').trim() if (!sessionId) continue next[sessionId] = { installed: row.installed === true, loading: false, error: row.success ? undefined : (row.error || '状态查询失败') } } return next }) setAntiRevokeSummary({ action: 'refresh', success: successCount, failed: failedCount }) showMessage(`状态刷新完成:成功 ${successCount},失败 ${failedCount}`, failedCount === 0) } catch (e: any) { showMessage(`防撤回状态刷新失败: ${e?.message || String(e)}`, false) } finally { setIsAntiRevokeRefreshing(false) } } const handleInstallAntiRevokeTriggers = async () => { if (isAntiRevokeRefreshing || isAntiRevokeInstalling || isAntiRevokeUninstalling) return const sessionIds = normalizeSessionIds(Array.from(antiRevokeSelectedIds)) if (sessionIds.length === 0) { showMessage('请先选择至少一个会话', false) return } setAntiRevokeSummary(null) setIsAntiRevokeInstalling(true) try { markAntiRevokeRowsLoading(sessionIds) const result = await window.electronAPI.chat.installAntiRevokeTriggers(sessionIds) if (!result.success || !result.rows) { const errorText = result.error || '批量安装失败' setAntiRevokeStatusMap((prev) => { const next = { ...prev } for (const sessionId of sessionIds) { next[sessionId] = { ...(next[sessionId] || {}), loading: false, error: errorText } } return next }) showMessage(errorText, false) return } const rowMap = new Map() for (const row of result.rows || []) { const sessionId = String(row.sessionId || '').trim() if (!sessionId) continue rowMap.set(sessionId, row) } const mergedRows = sessionIds.map((sessionId) => ( rowMap.get(sessionId) || { sessionId, success: false, error: '安装未返回结果' } )) const successCount = mergedRows.filter((row) => row.success).length const failedCount = mergedRows.length - successCount setAntiRevokeStatusMap((prev) => { const next = { ...prev } for (const row of mergedRows) { const sessionId = String(row.sessionId || '').trim() if (!sessionId) continue next[sessionId] = { installed: row.success ? true : next[sessionId]?.installed, loading: false, error: row.success ? undefined : (row.error || '安装失败') } } return next }) setAntiRevokeSummary({ action: 'install', success: successCount, failed: failedCount }) showMessage(`批量安装完成:成功 ${successCount},失败 ${failedCount}`, failedCount === 0) } catch (e: any) { showMessage(`批量安装失败: ${e?.message || String(e)}`, false) } finally { setIsAntiRevokeInstalling(false) } } const handleUninstallAntiRevokeTriggers = async () => { if (isAntiRevokeRefreshing || isAntiRevokeInstalling || isAntiRevokeUninstalling) return const sessionIds = normalizeSessionIds(Array.from(antiRevokeSelectedIds)) if (sessionIds.length === 0) { showMessage('请先选择至少一个会话', false) return } setAntiRevokeSummary(null) setIsAntiRevokeUninstalling(true) try { markAntiRevokeRowsLoading(sessionIds) const result = await window.electronAPI.chat.uninstallAntiRevokeTriggers(sessionIds) if (!result.success || !result.rows) { const errorText = result.error || '批量卸载失败' setAntiRevokeStatusMap((prev) => { const next = { ...prev } for (const sessionId of sessionIds) { next[sessionId] = { ...(next[sessionId] || {}), loading: false, error: errorText } } return next }) showMessage(errorText, false) return } const rowMap = new Map() for (const row of result.rows || []) { const sessionId = String(row.sessionId || '').trim() if (!sessionId) continue rowMap.set(sessionId, row) } const mergedRows = sessionIds.map((sessionId) => ( rowMap.get(sessionId) || { sessionId, success: false, error: '卸载未返回结果' } )) const successCount = mergedRows.filter((row) => row.success).length const failedCount = mergedRows.length - successCount setAntiRevokeStatusMap((prev) => { const next = { ...prev } for (const row of mergedRows) { const sessionId = String(row.sessionId || '').trim() if (!sessionId) continue next[sessionId] = { installed: row.success ? false : next[sessionId]?.installed, loading: false, error: row.success ? undefined : (row.error || '卸载失败') } } return next }) setAntiRevokeSummary({ action: 'uninstall', success: successCount, failed: failedCount }) showMessage(`批量卸载完成:成功 ${successCount},失败 ${failedCount}`, failedCount === 0) } catch (e: any) { showMessage(`批量卸载失败: ${e?.message || String(e)}`, false) } finally { setIsAntiRevokeUninstalling(false) } } useEffect(() => { if (activeTab !== 'antiRevoke' && activeTab !== 'insight') return let canceled = false ;(async () => { try { // 两个 Tab 都需要会话列表;antiRevoke 还需要额外检查防撤回状态 const sessionIds = await ensureAntiRevokeSessionsLoaded() if (canceled) return if (activeTab === 'antiRevoke') { await handleRefreshAntiRevokeStatus(sessionIds) } } catch (e: any) { if (!canceled) { showMessage(`加载会话失败: ${e?.message || String(e)}`, false) } } })() return () => { canceled = true } }, [activeTab]) type WxidKeys = { decryptKey: string imageXorKey: number | null imageAesKey: string } const formatImageXorKey = (value: number) => `0x${value.toString(16).toUpperCase().padStart(2, '0')}` const parseImageXorKey = (value: string) => { if (!value) return null const parsed = parseInt(value.replace(/^0x/i, ''), 16) return Number.isNaN(parsed) ? null : parsed } const buildKeysFromState = (): WxidKeys => ({ decryptKey: decryptKey || '', imageXorKey: parseImageXorKey(imageXorKey), imageAesKey: imageAesKey || '' }) const buildKeysFromInputs = (overrides?: { decryptKey?: string; imageXorKey?: string; imageAesKey?: string }): WxidKeys => ({ decryptKey: overrides?.decryptKey ?? decryptKey ?? '', imageXorKey: parseImageXorKey(overrides?.imageXorKey ?? imageXorKey), imageAesKey: overrides?.imageAesKey ?? imageAesKey ?? '' }) const buildKeysFromConfig = (wxidConfig: configService.WxidConfig | null): WxidKeys => ({ decryptKey: wxidConfig?.decryptKey || '', imageXorKey: typeof wxidConfig?.imageXorKey === 'number' ? wxidConfig.imageXorKey : null, imageAesKey: wxidConfig?.imageAesKey || '' }) const applyKeysToState = (keys: WxidKeys) => { setDecryptKey(keys.decryptKey) if (typeof keys.imageXorKey === 'number') { setImageXorKey(formatImageXorKey(keys.imageXorKey)) } else { setImageXorKey('') } setImageAesKey(keys.imageAesKey) } const syncKeysToConfig = async (keys: WxidKeys) => { await configService.setDecryptKey(keys.decryptKey) await configService.setImageXorKey(typeof keys.imageXorKey === 'number' ? keys.imageXorKey : 0) await configService.setImageAesKey(keys.imageAesKey) } const applyWxidSelection = async ( selectedWxid: string, options?: { preferCurrentKeys?: boolean; showToast?: boolean; toastText?: string; keysOverride?: WxidKeys } ) => { if (!selectedWxid) return const currentWxid = wxid const isSameWxid = currentWxid === selectedWxid if (currentWxid && currentWxid !== selectedWxid) { const currentKeys = buildKeysFromState() await configService.setWxidConfig(currentWxid, { decryptKey: currentKeys.decryptKey, imageXorKey: typeof currentKeys.imageXorKey === 'number' ? currentKeys.imageXorKey : 0, imageAesKey: currentKeys.imageAesKey }) } const preferCurrentKeys = options?.preferCurrentKeys ?? false const keys = options?.keysOverride ?? (preferCurrentKeys ? buildKeysFromState() : buildKeysFromConfig(await configService.getWxidConfig(selectedWxid))) setWxid(selectedWxid) applyKeysToState(keys) await configService.setMyWxid(selectedWxid) await syncKeysToConfig(keys) await configService.setWxidConfig(selectedWxid, { decryptKey: keys.decryptKey, imageXorKey: typeof keys.imageXorKey === 'number' ? keys.imageXorKey : 0, imageAesKey: keys.imageAesKey }) setShowWxidSelect(false) if (isDbConnected) { try { await window.electronAPI.chat.close() const result = await window.electronAPI.chat.connect() setDbConnected(result.success, dbPath || undefined) if (!result.success && result.error) { showMessage(result.error, false) } } catch (e: any) { showMessage(`切换账号后重新连接失败: ${e}`, false) setDbConnected(false) } } if (!isSameWxid) { clearAnalyticsStoreCache() resetChatStore() window.dispatchEvent(new CustomEvent('wxid-changed', { detail: { wxid: selectedWxid } })) } if (options?.showToast ?? true) { showMessage(options?.toastText || `已选择账号:${selectedWxid}`, true) } } const validatePath = (path: string): string | null => { if (!path) return null if (/[\u4e00-\u9fa5]/.test(path)) { return '路径包含中文字符,请迁移至全英文目录' } return null } const handleAutoDetectPath = async () => { if (isDetectingPath) return setIsDetectingPath(true) try { const result = await window.electronAPI.dbPath.autoDetect() if (result.success && result.path) { const validationError = validatePath(result.path) if (validationError) { showMessage(validationError, false) } else { setDbPath(result.path) await configService.setDbPath(result.path) showMessage(`自动检测成功:${result.path}`, true) const wxids = await window.electronAPI.dbPath.scanWxids(result.path) setWxidOptions(wxids) if (wxids.length === 1) { await applyWxidSelection(wxids[0].wxid, { toastText: `已检测到账号:${wxids[0].wxid}` }) } else if (wxids.length > 1) { setShowWxidSelect(true) } } } else { showMessage(result.error || '未能自动检测到数据库目录', false) } } catch (e: any) { showMessage(`自动检测失败: ${e}`, false) } finally { setIsDetectingPath(false) } } const handleSelectDbPath = async () => { try { const result = await dialog.openFile({ title: '选择微信数据库根目录', properties: ['openDirectory'] }) if (!result.canceled && result.filePaths.length > 0) { const selectedPath = result.filePaths[0] const validationError = validatePath(selectedPath) if (validationError) { showMessage(validationError, false) } else { setDbPath(selectedPath) await configService.setDbPath(selectedPath) showMessage('已选择数据库目录', true) } } } catch (e: any) { showMessage('选择目录失败', false) } } const handleScanWxid = async ( silent = false, options?: { preferCurrentKeys?: boolean; showDialog?: boolean; keysOverride?: WxidKeys } ) => { if (!dbPath) { if (!silent) showMessage('请先选择数据库目录', false) return } try { const wxids = await window.electronAPI.dbPath.scanWxids(dbPath) setWxidOptions(wxids) const allowDialog = options?.showDialog ?? !silent if (wxids.length === 1) { await applyWxidSelection(wxids[0].wxid, { preferCurrentKeys: options?.preferCurrentKeys ?? false, showToast: !silent, toastText: `已检测到账号:${wxids[0].wxid}`, keysOverride: options?.keysOverride }) } else if (wxids.length > 1 && allowDialog) { setShowWxidSelect(true) } else { if (!silent) showMessage('未检测到账号目录,请检查路径', false) } } catch (e: any) { if (!silent) showMessage(`扫描失败: ${e}`, false) } } const handleSelectWxid = async (selectedWxid: string) => { await applyWxidSelection(selectedWxid) } const handleSelectCachePath = async () => { try { const result = await dialog.openFile({ title: '选择缓存目录', properties: ['openDirectory'] }) if (!result.canceled && result.filePaths.length > 0) { const selectedPath = result.filePaths[0] setCachePath(selectedPath) await configService.setCachePath(selectedPath) showMessage('已选择缓存目录', true) } } catch (e: any) { showMessage('选择目录失败', false) } } const handleSelectWhisperModelDir = async () => { try { const result = await dialog.openFile({ title: '选择 Whisper 模型下载目录', properties: ['openDirectory'] }) if (!result.canceled && result.filePaths.length > 0) { const dir = result.filePaths[0] setWhisperModelDir(dir) await configService.setWhisperModelDir(dir) showMessage('已选择 Whisper 模型目录', true) } } catch (e: any) { showMessage('选择目录失败', false) } } const handleWhisperModelChange = async (value: string) => { setWhisperModelName(value) setWhisperDownloadProgress(0) await configService.setWhisperModelName(value) } const handleDownloadWhisperModel = async () => { if (isWhisperDownloading) return setIsWhisperDownloading(true) setWhisperDownloadProgress(0) try { const result = await window.electronAPI.whisper.downloadModel() if (result.success) { setWhisperDownloadProgress(100) showMessage('SenseVoiceSmall 模型下载完成', true) await refreshWhisperStatus(whisperModelDir) } else { showMessage(result.error || '模型下载失败', false) } } catch (e: any) { showMessage(`模型下载失败: ${e}`, false) } finally { setIsWhisperDownloading(false) } } const handleResetWhisperModelDir = async () => { setWhisperModelDir('') await configService.setWhisperModelDir('') } const handleAutoGetDbKey = async () => { if (isFetchingDbKey) return setIsFetchingDbKey(true) setIsManualStartPrompt(false) setDbKeyStatus('正在连接微信进程...') try { const result = await window.electronAPI.key.autoGetDbKey() if (result.success && result.key) { setDecryptKey(result.key) setDbKeyStatus('密钥获取成功') showMessage('已自动获取解密密钥', true) await syncCurrentKeys({ decryptKey: result.key, wxid }) const keysOverride = buildKeysFromInputs({ decryptKey: result.key }) await handleScanWxid(true, { preferCurrentKeys: true, showDialog: false, keysOverride }) } else { if (result.error?.includes('未找到微信安装路径') || result.error?.includes('启动微信失败')) { setIsManualStartPrompt(true) setDbKeyStatus('需要手动启动微信') } else { showMessage(result.error || '自动获取密钥失败', false) } } } catch (e: any) { showMessage(`自动获取密钥失败: ${e}`, false) } finally { setIsFetchingDbKey(false) } } const handleManualConfirm = async () => { setIsManualStartPrompt(false) handleAutoGetDbKey() } // Debounce config writes to avoid excessive disk IO const scheduleConfigSave = (key: string, task: () => Promise | void, delay = 300) => { const timers = saveTimersRef.current if (timers[key]) { clearTimeout(timers[key]) } timers[key] = setTimeout(() => { Promise.resolve(task()).catch((e) => { console.error('保存配置失败:', e) }) }, delay) } const syncCurrentKeys = async (options?: { decryptKey?: string; imageXorKey?: string; imageAesKey?: string; wxid?: string }) => { const keys = buildKeysFromInputs(options) await syncKeysToConfig(keys) const wxidToUse = options?.wxid ?? wxid if (wxidToUse) { await configService.setWxidConfig(wxidToUse, { decryptKey: keys.decryptKey, imageXorKey: typeof keys.imageXorKey === 'number' ? keys.imageXorKey : 0, imageAesKey: keys.imageAesKey }) } } const handleAutoGetImageKey = async () => { if (isFetchingImageKey) return; if (!dbPath) { showMessage('请先选择数据库目录', false); return; } setIsFetchingImageKey(true); setImageKeyPercent(0) setImageKeyStatus('正在初始化...'); setImageKeyProgress(0); try { const accountPath = wxid ? `${dbPath}/${wxid}` : dbPath; const result = await window.electronAPI.key.autoGetImageKey(accountPath, wxid) if (result.success && result.aesKey) { if (typeof result.xorKey === 'number') setImageXorKey(`0x${result.xorKey.toString(16).toUpperCase().padStart(2, '0')}`) setImageAesKey(result.aesKey) setImageKeyStatus('已获取图片��钥') showMessage('已自动获取图片密钥', true) const newXorKey = typeof result.xorKey === 'number' ? result.xorKey : 0 const newAesKey = result.aesKey await configService.setImageXorKey(newXorKey) await configService.setImageAesKey(newAesKey) if (wxid) await configService.setWxidConfig(wxid, { decryptKey, imageXorKey: newXorKey, imageAesKey: newAesKey }) } else { showMessage(result.error || '自动获取图片密钥失败', false) } } catch (e: any) { showMessage(`自动获取图片密钥失败: ${e}`, false) } finally { setIsFetchingImageKey(false) } } const handleScanImageKeyFromMemory = async () => { if (isFetchingImageKey) return; if (!dbPath) { showMessage('请先选择数据库目录', false); return; } setIsFetchingImageKey(true); setImageKeyPercent(0) setImageKeyStatus('正在扫描内存...'); try { const accountPath = wxid ? `${dbPath}/${wxid}` : dbPath; const result = await window.electronAPI.key.scanImageKeyFromMemory(accountPath) if (result.success && result.aesKey) { if (typeof result.xorKey === 'number') setImageXorKey(`0x${result.xorKey.toString(16).toUpperCase().padStart(2, '0')}`) setImageAesKey(result.aesKey) setImageKeyStatus('内存扫描成功,已获取图片密钥') showMessage('内存扫描成功,已获取图片密钥', true) const newXorKey = typeof result.xorKey === 'number' ? result.xorKey : 0 const newAesKey = result.aesKey await configService.setImageXorKey(newXorKey) await configService.setImageAesKey(newAesKey) if (wxid) await configService.setWxidConfig(wxid, { decryptKey, imageXorKey: newXorKey, imageAesKey: newAesKey }) } else { showMessage(result.error || '内存扫描获取图片密钥失败', false) } } catch (e: any) { showMessage(`内存扫描失败: ${e}`, false) } finally { setIsFetchingImageKey(false) } } const handleTestConnection = async () => { if (!dbPath) { showMessage('请先选择数据库目录', false); return } if (!decryptKey) { showMessage('请先输入解密密钥', false); return } if (decryptKey.length !== 64) { showMessage('密钥长度必须为64个字符', false); return } if (!wxid) { showMessage('请先输入或扫描 wxid', false); return } setIsTesting(true) try { const result = await window.electronAPI.wcdb.testConnection(dbPath, decryptKey, wxid) if (result.success) { showMessage('连接测试成功!数据库可正常访问', true) } else { showMessage(result.error || '连接测试失败', false) } } catch (e: any) { showMessage(`连接测试失败: ${e}`, false) } finally { setIsTesting(false) } } // Removed manual save config function const handleClearConfig = async () => { const confirmed = window.confirm('确定要清除当前配置吗?清除后需要重新完成首次配置?') if (!confirmed) return setIsLoadingState(true) setLoading(true, '正在清除配置...') try { await window.electronAPI.wcdb.close() await configService.clearConfig() reset() setDecryptKey('') setImageXorKey('') setImageAesKey('') setDbPath('') setWxid('') setCachePath('') setLogEnabled(false) setAutoTranscribeVoice(false) setTranscribeLanguages(['zh']) setWhisperModelDir('') setWhisperModelStatus(null) setWhisperDownloadProgress(0) setIsWhisperDownloading(false) setDbConnected(false) await window.electronAPI.window.openOnboardingWindow() } catch (e: any) { showMessage(`清除配置失败: ${e}`, false) } finally { setIsLoadingState(false) setLoading(false) } } const handleOpenLog = async () => { try { const logPath = await window.electronAPI.log.getPath() await window.electronAPI.shell.openPath(logPath) } catch (e: any) { showMessage(`打开日志失败: ${e}`, false) } } const handleCopyLog = async () => { try { const result = await window.electronAPI.log.read() if (!result.success) { showMessage(result.error || '读取日志失败', false) return } await navigator.clipboard.writeText(result.content || '') showMessage('日志已复制到剪贴板', true) } catch (e: any) { showMessage(`复制日志失败: ${e}`, false) } } const handleClearLog = async () => { const confirmed = window.confirm('确定清空 wcdb.log 吗?') if (!confirmed) return try { const result = await window.electronAPI.log.clear() if (!result.success) { showMessage(result.error || '清空日志失败', false) return } showMessage('日志已清空', true) } catch (e: any) { showMessage(`清空日志失败: ${e}`, false) } } const handleClearAnalyticsCache = async () => { if (isClearingCache) return setIsClearingAnalyticsCache(true) try { const result = await window.electronAPI.cache.clearAnalytics() if (result.success) { clearAnalyticsStoreCache() showMessage('已清除分析缓存', true) } else { showMessage(`清除分析缓存失败: ${result.error || '未知错误'}`, false) } } catch (e: any) { showMessage(`清除分析缓存失败: ${e}`, false) } finally { setIsClearingAnalyticsCache(false) } } const handleClearImageCache = async () => { if (isClearingCache) return setIsClearingImageCache(true) try { const result = await window.electronAPI.cache.clearImages() if (result.success) { showMessage('已清除图片缓存', true) } else { showMessage(`清除图片缓存失败: ${result.error || '未知错误'}`, false) } } catch (e: any) { showMessage(`清除图片缓存失败: ${e}`, false) } finally { setIsClearingImageCache(false) } } const handleClearAllCache = async () => { if (isClearingCache) return setIsClearingAllCache(true) try { const result = await window.electronAPI.cache.clearAll() if (result.success) { clearAnalyticsStoreCache() showMessage('已清除所有缓存', true) } else { showMessage(`清除所有缓存失败: ${result.error || '未知错误'}`, false) } } catch (e: any) { showMessage(`清除所有缓存失败: ${e}`, false) } finally { setIsClearingAllCache(false) } } const renderAppearanceTab = () => (
{themes.map((theme) => (
setTheme(theme.id)}>
{theme.name} {theme.description}
{currentTheme === theme.id &&
}
))}
选择聊天中引用消息与正文的上下顺序,下方预览会同步展示布局差异。
{[ { value: 'quote-top' as const, label: '引用在上', description: '更接近当前 WeFlow 风格', successMessage: '已切换为引用在上样式' }, { value: 'quote-bottom' as const, label: '正文在上', description: '更接近微信 / 密语风格', successMessage: '已切换为正文在上样式' } ].map(option => { const selected = quoteLayout === option.value const isQuoteBottom = option.value === 'quote-bottom' return ( ) })}
{launchAtStartupSupported ? '开启后,登录系统时会自动启动 WeFlow。' : launchAtStartupReason || '当前环境暂不支持开机自启动。'}
{isUpdatingLaunchAtStartup ? '保存中...' : launchAtStartupSupported ? (launchAtStartup ? '已开启' : '已关闭') : '当前不可用'}
设置点击关闭按钮后的默认行为;选择“每次询问”时会弹出关闭确认。
setCloseBehaviorDropdownOpen(!closeBehaviorDropdownOpen)} > {windowCloseBehavior === 'tray' ? '最小化到系统托盘' : windowCloseBehavior === 'quit' ? '完全关闭' : '每次询问'}
{[ { value: 'ask' as const, label: '每次询问', successMessage: '已恢复关闭确认弹窗' }, { value: 'tray' as const, label: '最小化到系统托盘', successMessage: '关闭按钮已改为最小化到托盘' }, { value: 'quit' as const, label: '完全关闭', successMessage: '关闭按钮已改为完全关闭' } ].map(option => (
{ setWindowCloseBehavior(option.value) setCloseBehaviorDropdownOpen(false) await configService.setWindowCloseBehavior(option.value) showMessage(option.successMessage, true) }} > {option.label} {windowCloseBehavior === option.value && }
))}
) const renderNotificationTab = () => { // 获取已过滤会话的信息 const getSessionInfo = (username: string) => { const session = chatSessions.find(s => s.username === username) return { displayName: session?.displayName || username, avatarUrl: session?.avatarUrl || '' } } // 添加会话到过滤列表 const handleAddToFilterList = async (username: string) => { if (notificationFilterList.includes(username)) return const newList = [...notificationFilterList, username] setNotificationFilterList(newList) await configService.setNotificationFilterList(newList) showMessage('已添加到过滤列表', true) } // 从过滤列表移除会话 const handleRemoveFromFilterList = async (username: string) => { const newList = notificationFilterList.filter(u => u !== username) setNotificationFilterList(newList) await configService.setNotificationFilterList(newList) showMessage('已从过滤列表移除', true) } // 过滤掉已在列表中的会话,并根据搜索关键字过滤 const availableSessions = chatSessions.filter(s => { if (notificationFilterList.includes(s.username)) return false if (filterSearchKeyword) { const keyword = filterSearchKeyword.toLowerCase() const displayName = (s.displayName || '').toLowerCase() const username = s.username.toLowerCase() return displayName.includes(keyword) || username.includes(keyword) } return true }) return (
开启后,收到新消息时将显示桌面弹窗通知
{notificationEnabled ? '已开启' : '已关闭'}
选择通知弹窗在屏幕上的显示位置 {isWayland && ( ⚠️ 注意:Wayland 环境下该配置可能无效! )}
setPositionDropdownOpen(!positionDropdownOpen)} > {notificationPosition === 'top-right' ? '右上角' : notificationPosition === 'bottom-right' ? '右下角' : notificationPosition === 'top-left' ? '左上角' : notificationPosition === 'top-center' ? '中间上方' : '左下角'}
{[ { value: 'top-center', label: '中间上方' }, { value: 'top-right', label: '右上角' }, { value: 'bottom-right', label: '右下角' }, { value: 'top-left', label: '左上角' }, { value: 'bottom-left', label: '左下角' } ].map(option => (
{ const val = option.value as 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'top-center' setNotificationPosition(val) setPositionDropdownOpen(false) await configService.setNotificationPosition(val) showMessage('通知位置已更新', true) }} > {option.label} {notificationPosition === option.value && }
))}
选择只接收特定会话的通知,或屏蔽特定会话的通知
setFilterModeDropdownOpen(!filterModeDropdownOpen)} > {notificationFilterMode === 'all' ? '接收所有通知' : notificationFilterMode === 'whitelist' ? '仅接收白名单' : '屏蔽黑名单'}
{[ { value: 'all', label: '接收所有通知' }, { value: 'whitelist', label: '仅接收白名单' }, { value: 'blacklist', label: '屏蔽黑名单' } ].map(option => (
{ const val = option.value as 'all' | 'whitelist' | 'blacklist' setNotificationFilterMode(val) setFilterModeDropdownOpen(false) await configService.setNotificationFilterMode(val) showMessage( val === 'all' ? '已设为接收所有通知' : val === 'whitelist' ? '已设为仅接收白名单通知' : '已设为屏蔽黑名单通知', true ) }} > {option.label} {notificationFilterMode === option.value && }
))}
{notificationFilterMode !== 'all' && (
{notificationFilterMode === 'whitelist' ? '点击左侧会话添加到白名单,点击右侧会话从白名单移除' : '点击左侧会话添加到黑名单,点击右侧会话从黑名单移除'}
{/* 可选会话列表 */}
可选会话
setFilterSearchKeyword(e.target.value)} />
{availableSessions.length > 0 ? ( availableSessions.map(session => (
handleAddToFilterList(session.username)} > {session.displayName || session.username} +
)) ) : (
{filterSearchKeyword ? '没有匹配的会话' : '暂无可添加的会话'}
)}
{/* 已选会话列表 */}
{notificationFilterMode === 'whitelist' ? '白名单' : '黑名单'} {notificationFilterList.length > 0 && ( {notificationFilterList.length} )}
{notificationFilterList.length > 0 ? ( notificationFilterList.map(username => { const info = getSessionInfo(username) return (
handleRemoveFromFilterList(username)} > {info.displayName} ×
) }) ) : (
尚未添加任何会话
)}
)}
) } const renderAntiRevokeTab = () => { const sortedSessions = [...chatSessions].sort((a, b) => (b.sortTimestamp || 0) - (a.sortTimestamp || 0)) const keyword = antiRevokeSearchKeyword.trim().toLowerCase() const filteredSessions = sortedSessions.filter((session) => { if (!keyword) return true const displayName = String(session.displayName || '').toLowerCase() const username = String(session.username || '').toLowerCase() return displayName.includes(keyword) || username.includes(keyword) }) const filteredSessionIds = filteredSessions.map((session) => session.username) const selectedCount = antiRevokeSelectedIds.size const selectedInFilteredCount = filteredSessionIds.filter((sessionId) => antiRevokeSelectedIds.has(sessionId)).length const allFilteredSelected = filteredSessionIds.length > 0 && selectedInFilteredCount === filteredSessionIds.length const busy = isAntiRevokeRefreshing || isAntiRevokeInstalling || isAntiRevokeUninstalling const statusStats = filteredSessions.reduce( (acc, session) => { const rowState = antiRevokeStatusMap[session.username] if (rowState?.error) acc.failed += 1 else if (rowState?.installed === true) acc.installed += 1 else if (rowState?.installed === false) acc.notInstalled += 1 return acc }, { installed: 0, notInstalled: 0, failed: 0 } ) const toggleSelected = (sessionId: string) => { setAntiRevokeSelectedIds((prev) => { const next = new Set(prev) if (next.has(sessionId)) next.delete(sessionId) else next.add(sessionId) return next }) } const selectAllFiltered = () => { if (filteredSessionIds.length === 0) return setAntiRevokeSelectedIds((prev) => { const next = new Set(prev) for (const sessionId of filteredSessionIds) { next.add(sessionId) } return next }) } const clearSelection = () => { setAntiRevokeSelectedIds(new Set()) } return (

防撤回

你可以根据会话进行防撤回部署,安装后无需保持 WeFlow 运行即可实现防撤回

筛选会话 {filteredSessionIds.length}
已安装 {statusStats.installed}
未安装 {statusStats.notInstalled}
异常 {statusStats.failed}
setAntiRevokeSearchKeyword(e.target.value)} />
已选 {selectedCount} 个会话 筛选命中 {selectedInFilteredCount} / {filteredSessionIds.length}
{antiRevokeSummary && (
0 ? 'error' : 'success'}`}> {antiRevokeSummary.action === 'refresh' ? '刷新' : antiRevokeSummary.action === 'install' ? '安装' : '卸载'} 完成:成功 {antiRevokeSummary.success},失败 {antiRevokeSummary.failed}
)}
{filteredSessions.length === 0 ? (
{antiRevokeSearchKeyword ? '没有匹配的会话' : '暂无会话可配置'}
) : ( <>
会话({filteredSessions.length}) 状态
{filteredSessions.map((session) => { const rowState = antiRevokeStatusMap[session.username] let statusClass = 'unknown' let statusLabel = '未检查' if (rowState?.loading) { statusClass = 'checking' statusLabel = '检查中' } else if (rowState?.error) { statusClass = 'error' statusLabel = '失败' } else if (rowState?.installed === true) { statusClass = 'installed' statusLabel = '已安装' } else if (rowState?.installed === false) { statusClass = 'not-installed' statusLabel = '未安装' } return (