Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
fa7f3ba4ec chore(deps): bump react-dom from 19.2.4 to 19.2.5
Bumps [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom) from 19.2.4 to 19.2.5.
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.5/packages/react-dom)

---
updated-dependencies:
- dependency-name: react-dom
  dependency-version: 19.2.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-08 23:17:04 +00:00
13 changed files with 130 additions and 414 deletions

View File

@@ -1693,6 +1693,13 @@ function registerIpcHandlers() {
return applyLaunchAtStartupPreference(enabled === true) return applyLaunchAtStartupPreference(enabled === true)
}) })
ipcMain.handle('app:checkWayland', async () => {
if (process.platform !== 'linux') return false;
const sessionType = process.env.XDG_SESSION_TYPE?.toLowerCase();
return Boolean(process.env.WAYLAND_DISPLAY || sessionType === 'wayland');
})
ipcMain.handle('log:getPath', async () => { ipcMain.handle('log:getPath', async () => {
return join(app.getPath('userData'), 'logs', 'wcdb.log') return join(app.getPath('userData'), 'logs', 'wcdb.log')
}) })
@@ -3455,38 +3462,12 @@ app.whenReady().then(async () => {
} }
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
const withTimeout = <T>(task: () => Promise<T>, timeoutMs: number): Promise<{ timedOut: boolean; value?: T; error?: string }> => {
return new Promise((resolve) => {
let settled = false
const timer = setTimeout(() => {
if (settled) return
settled = true
resolve({ timedOut: true, error: `timeout(${timeoutMs}ms)` })
}, timeoutMs)
task()
.then((value) => {
if (settled) return
settled = true
clearTimeout(timer)
resolve({ timedOut: false, value })
})
.catch((error) => {
if (settled) return
settled = true
clearTimeout(timer)
resolve({ timedOut: false, error: String(error) })
})
})
}
// 初始化配置服务 // 初始化配置服务
updateSplashProgress(5, '正在加载配置...') updateSplashProgress(5, '正在加载配置...')
configService = new ConfigService() configService = new ConfigService()
applyAutoUpdateChannel('startup') applyAutoUpdateChannel('startup')
syncLaunchAtStartupPreference() syncLaunchAtStartupPreference()
const onboardingDone = configService.get('onboardingDone') === true
shouldShowMain = onboardingDone
// 将用户主题配置推送给 Splash 窗口 // 将用户主题配置推送给 Splash 窗口
if (splashWindow && !splashWindow.isDestroyed()) { if (splashWindow && !splashWindow.isDestroyed()) {
@@ -3499,7 +3480,7 @@ app.whenReady().then(async () => {
await delay(200) await delay(200)
// 设置资源路径 // 设置资源路径
updateSplashProgress(12, '正在初始化...') updateSplashProgress(10, '正在初始化...')
const candidateResources = app.isPackaged const candidateResources = app.isPackaged
? join(process.resourcesPath, 'resources') ? join(process.resourcesPath, 'resources')
: join(app.getAppPath(), 'resources') : join(app.getAppPath(), 'resources')
@@ -3509,13 +3490,13 @@ app.whenReady().then(async () => {
await delay(200) await delay(200)
// 初始化数据库服务 // 初始化数据库服务
updateSplashProgress(20, '正在初始化...') updateSplashProgress(18, '正在初始化...')
wcdbService.setPaths(resourcesPath, userDataPath) wcdbService.setPaths(resourcesPath, userDataPath)
wcdbService.setLogEnabled(configService.get('logEnabled') === true) wcdbService.setLogEnabled(configService.get('logEnabled') === true)
await delay(200) await delay(200)
// 注册 IPC 处理器 // 注册 IPC 处理器
updateSplashProgress(28, '正在初始化...') updateSplashProgress(25, '正在初始化...')
registerIpcHandlers() registerIpcHandlers()
chatService.addDbMonitorListener((type, json) => { chatService.addDbMonitorListener((type, json) => {
messagePushService.handleDbMonitorChange(type, json) messagePushService.handleDbMonitorChange(type, json)
@@ -3525,54 +3506,12 @@ app.whenReady().then(async () => {
insightService.start() insightService.start()
await delay(200) await delay(200)
// 已完成引导时,在 Splash 阶段预热核心数据(联系人、消息库索引等) // 检查配置状态
if (onboardingDone) { const onboardingDone = configService.get('onboardingDone')
updateSplashProgress(34, '正在连接数据库...') shouldShowMain = onboardingDone === true
const connectWarmup = await withTimeout(() => chatService.connect(), 12000)
const connected = !connectWarmup.timedOut && connectWarmup.value?.success === true
if (!connected) {
const reason = connectWarmup.timedOut
? connectWarmup.error
: (connectWarmup.value?.error || connectWarmup.error || 'unknown')
console.warn('[StartupWarmup] 跳过预热,数据库连接失败:', reason)
updateSplashProgress(68, '数据库预热已跳过')
} else {
const preloadUsernames = new Set<string>()
updateSplashProgress(44, '正在预加载会话...')
const sessionsWarmup = await withTimeout(() => chatService.getSessions(), 12000)
if (!sessionsWarmup.timedOut && sessionsWarmup.value?.success && Array.isArray(sessionsWarmup.value.sessions)) {
for (const session of sessionsWarmup.value.sessions) {
const username = String((session as any)?.username || '').trim()
if (username) preloadUsernames.add(username)
}
}
updateSplashProgress(56, '正在预加载联系人...')
const contactsWarmup = await withTimeout(() => chatService.getContacts(), 15000)
if (!contactsWarmup.timedOut && contactsWarmup.value?.success && Array.isArray(contactsWarmup.value.contacts)) {
for (const contact of contactsWarmup.value.contacts) {
const username = String((contact as any)?.username || '').trim()
if (username) preloadUsernames.add(username)
}
}
updateSplashProgress(63, '正在缓存联系人头像...')
const avatarWarmupUsernames = Array.from(preloadUsernames).slice(0, 2000)
if (avatarWarmupUsernames.length > 0) {
await withTimeout(() => chatService.enrichSessionsContactInfo(avatarWarmupUsernames), 15000)
}
updateSplashProgress(68, '正在初始化消息库索引...')
await withTimeout(() => chatService.warmupMessageDbSnapshot(), 10000)
}
} else {
updateSplashProgress(68, '首次启动准备中...')
}
// 创建主窗口(不显示,由启动流程统一控制) // 创建主窗口(不显示,由启动流程统一控制)
updateSplashProgress(70, '正在准备主窗口...') updateSplashProgress(30, '正在加载界面...')
mainWindow = createWindow({ autoShow: false }) mainWindow = createWindow({ autoShow: false })
let iconName = 'icon.ico'; let iconName = 'icon.ico';
@@ -3644,7 +3583,7 @@ app.whenReady().then(async () => {
) )
// 等待主窗口加载完成(真正耗时阶段,进度条末端呼吸光点) // 等待主窗口加载完成(真正耗时阶段,进度条末端呼吸光点)
updateSplashProgress(70, '正在准备主窗口...', true) updateSplashProgress(30, '正在加载界面...', true)
await new Promise<void>((resolve) => { await new Promise<void>((resolve) => {
if (mainWindowReady) { if (mainWindowReady) {
resolve() resolve()

View File

@@ -71,6 +71,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
ipcRenderer.on('app:updateAvailable', (_, info) => callback(info)) ipcRenderer.on('app:updateAvailable', (_, info) => callback(info))
return () => ipcRenderer.removeAllListeners('app:updateAvailable') return () => ipcRenderer.removeAllListeners('app:updateAvailable')
}, },
checkWayland: () => ipcRenderer.invoke('app:checkWayland'),
}, },
// 日志 // 日志

View File

@@ -323,8 +323,6 @@ class ChatService {
private contactLabelNameMapCacheAt = 0 private contactLabelNameMapCacheAt = 0
private readonly contactLabelNameMapCacheTtlMs = 10 * 60 * 1000 private readonly contactLabelNameMapCacheTtlMs = 10 * 60 * 1000
private contactsLoadInFlight: { mode: 'lite' | 'full'; promise: Promise<{ success: boolean; contacts?: ContactInfo[]; error?: string }> } | null = null private contactsLoadInFlight: { mode: 'lite' | 'full'; promise: Promise<{ success: boolean; contacts?: ContactInfo[]; error?: string }> } | null = null
private contactsMemoryCache = new Map<'lite' | 'full', { scope: string; updatedAt: number; contacts: ContactInfo[] }>()
private readonly contactsMemoryCacheTtlMs = 3 * 60 * 1000
private readonly contactDisplayNameCollator = new Intl.Collator('zh-CN') private readonly contactDisplayNameCollator = new Intl.Collator('zh-CN')
private readonly slowGetContactsLogThresholdMs = 1200 private readonly slowGetContactsLogThresholdMs = 1200
@@ -515,43 +513,6 @@ class ChatService {
} }
} }
async warmupMessageDbSnapshot(): Promise<{ success: boolean; messageDbCount?: number; mediaDbCount?: number; error?: string }> {
try {
const connectResult = await this.ensureConnected()
if (!connectResult.success) {
return { success: false, error: connectResult.error || '数据库未连接' }
}
const [messageSnapshot, mediaResult] = await Promise.all([
this.getMessageDbCountSnapshot(true),
wcdbService.listMediaDbs()
])
let messageDbCount = 0
if (messageSnapshot.success && Array.isArray(messageSnapshot.dbPaths)) {
messageDbCount = messageSnapshot.dbPaths.length
}
let mediaDbCount = 0
if (mediaResult.success && Array.isArray(mediaResult.data)) {
this.mediaDbsCache = [...mediaResult.data]
this.mediaDbsCacheTime = Date.now()
mediaDbCount = mediaResult.data.length
}
if (!messageSnapshot.success && !mediaResult.success) {
return {
success: false,
error: messageSnapshot.error || mediaResult.error || '初始化消息库索引失败'
}
}
return { success: true, messageDbCount, mediaDbCount }
} catch (e) {
return { success: false, error: String(e) }
}
}
private async ensureConnected(): Promise<{ success: boolean; error?: string }> { private async ensureConnected(): Promise<{ success: boolean; error?: string }> {
if (this.connected && wcdbService.isReady()) { if (this.connected && wcdbService.isReady()) {
return { success: true } return { success: true }
@@ -1401,50 +1362,8 @@ class ChatService {
} }
} }
private getContactsCacheScope(): string {
const dbPath = String(this.configService.get('dbPath') || '').trim()
const myWxid = String(this.configService.get('myWxid') || '').trim()
return `${dbPath}::${myWxid}`
}
private cloneContacts(contacts: ContactInfo[]): ContactInfo[] {
return (contacts || []).map((contact) => ({
...contact,
labels: Array.isArray(contact.labels) ? [...contact.labels] : contact.labels
}))
}
private getContactsFromMemoryCache(mode: 'lite' | 'full', scope: string): ContactInfo[] | null {
const cached = this.contactsMemoryCache.get(mode)
if (!cached) return null
if (cached.scope !== scope) return null
if (Date.now() - cached.updatedAt > this.contactsMemoryCacheTtlMs) return null
return this.cloneContacts(cached.contacts)
}
private setContactsMemoryCache(mode: 'lite' | 'full', scope: string, contacts: ContactInfo[]): void {
this.contactsMemoryCache.set(mode, {
scope,
updatedAt: Date.now(),
contacts: this.cloneContacts(contacts)
})
}
private async getContactsInternal(options?: GetContactsOptions): Promise<{ success: boolean; contacts?: ContactInfo[]; error?: string }> { private async getContactsInternal(options?: GetContactsOptions): Promise<{ success: boolean; contacts?: ContactInfo[]; error?: string }> {
const isLiteMode = options?.lite === true const isLiteMode = options?.lite === true
const mode: 'lite' | 'full' = isLiteMode ? 'lite' : 'full'
const cacheScope = this.getContactsCacheScope()
const cachedContacts = this.getContactsFromMemoryCache(mode, cacheScope)
if (cachedContacts) {
return { success: true, contacts: cachedContacts }
}
if (isLiteMode) {
const fullCachedContacts = this.getContactsFromMemoryCache('full', cacheScope)
if (fullCachedContacts) {
return { success: true, contacts: fullCachedContacts }
}
}
const startedAt = Date.now() const startedAt = Date.now()
const stageDurations: Array<{ stage: string; ms: number }> = [] const stageDurations: Array<{ stage: string; ms: number }> = []
const captureStage = (stage: string, stageStartedAt: number) => { const captureStage = (stage: string, stageStartedAt: number) => {
@@ -1568,10 +1487,6 @@ class ChatService {
.join(', ') .join(', ')
console.warn(`[ChatService] getContacts(${isLiteMode ? 'lite' : 'full'}) 慢查询 total=${totalMs}ms, ${stageSummary}`) console.warn(`[ChatService] getContacts(${isLiteMode ? 'lite' : 'full'}) 慢查询 total=${totalMs}ms, ${stageSummary}`)
} }
this.setContactsMemoryCache(mode, cacheScope, result)
if (!isLiteMode) {
this.setContactsMemoryCache('lite', cacheScope, result)
}
return { success: true, contacts: result } return { success: true, contacts: result }
} catch (e) { } catch (e) {
console.error('ChatService: 获取通讯录失败:', e) console.error('ChatService: 获取通讯录失败:', e)
@@ -2971,7 +2886,6 @@ class ChatService {
this.sessionTablesCache.clear() this.sessionTablesCache.clear()
this.messageTableColumnsCache.clear() this.messageTableColumnsCache.clear()
this.messageDbCountSnapshotCache = null this.messageDbCountSnapshotCache = null
this.contactsMemoryCache.clear()
this.refreshSessionStatsCacheScope(scope) this.refreshSessionStatsCacheScope(scope)
this.refreshGroupMyMessageCountCacheScope(scope) this.refreshGroupMyMessageCountCacheScope(scope)
} }
@@ -6069,7 +5983,6 @@ class ChatService {
if (includeContacts) { if (includeContacts) {
this.avatarCache.clear() this.avatarCache.clear()
this.contactCacheService.clear() this.contactCacheService.clear()
this.contactsMemoryCache.clear()
} }
if (includeMessages) { if (includeMessages) {

View File

@@ -92,7 +92,6 @@ export interface ExportOptions {
dateRange?: { start: number; end: number } | null dateRange?: { start: number; end: number } | null
senderUsername?: string senderUsername?: string
fileNameSuffix?: string fileNameSuffix?: string
fileNamingMode?: 'classic' | 'date-range'
exportMedia?: boolean exportMedia?: boolean
exportAvatars?: boolean exportAvatars?: boolean
exportImages?: boolean exportImages?: boolean
@@ -495,80 +494,6 @@ class ExportService {
} }
} }
private sanitizeExportFileNamePart(value: string): string {
return String(value || '')
.replace(/[<>:"\/\\|?*]/g, '_')
.replace(/\.+$/, '')
.trim()
}
private normalizeFileNamingMode(value: unknown): 'classic' | 'date-range' {
return String(value || '').trim().toLowerCase() === 'date-range' ? 'date-range' : 'classic'
}
private formatDateTokenBySeconds(seconds?: number): string | null {
if (!Number.isFinite(seconds) || (seconds || 0) <= 0) return null
const date = new Date(Math.floor(Number(seconds)) * 1000)
if (Number.isNaN(date.getTime())) return null
const y = date.getFullYear()
const m = `${date.getMonth() + 1}`.padStart(2, '0')
const d = `${date.getDate()}`.padStart(2, '0')
return `${y}${m}${d}`
}
private buildDateRangeFileNamePart(dateRange?: { start: number; end: number } | null): string {
const start = this.formatDateTokenBySeconds(dateRange?.start)
const end = this.formatDateTokenBySeconds(dateRange?.end)
if (start && end) {
if (start === end) return start
return start < end ? `${start}-${end}` : `${end}-${start}`
}
if (start) return `${start}-至今`
if (end) return `截至-${end}`
return '全部时间'
}
private buildSessionExportBaseName(
sessionId: string,
displayName: string,
options: ExportOptions
): string {
const baseName = this.sanitizeExportFileNamePart(displayName || sessionId) || this.sanitizeExportFileNamePart(sessionId) || 'session'
const suffix = this.sanitizeExportFileNamePart(options.fileNameSuffix || '')
const namingMode = this.normalizeFileNamingMode(options.fileNamingMode)
const parts = [baseName]
if (suffix) parts.push(suffix)
if (namingMode === 'date-range') {
parts.push(this.buildDateRangeFileNamePart(options.dateRange))
}
return this.sanitizeExportFileNamePart(parts.join('_')) || 'session'
}
private async reserveUniqueOutputPath(preferredPath: string, reservedPaths: Set<string>): Promise<string> {
const dir = path.dirname(preferredPath)
const ext = path.extname(preferredPath)
const base = path.basename(preferredPath, ext)
for (let attempt = 0; attempt < 10000; attempt += 1) {
const candidate = attempt === 0
? preferredPath
: path.join(dir, `${base}_${attempt + 1}${ext}`)
if (reservedPaths.has(candidate)) continue
const exists = await this.pathExists(candidate)
if (reservedPaths.has(candidate)) continue
if (exists) continue
reservedPaths.add(candidate)
return candidate
}
const fallback = path.join(dir, `${base}_${Date.now()}${ext}`)
reservedPaths.add(fallback)
return fallback
}
private isCloneUnsupportedError(code: string | undefined): boolean { private isCloneUnsupportedError(code: string | undefined): boolean {
return code === 'ENOTSUP' || code === 'ENOSYS' || code === 'EINVAL' || code === 'EXDEV' || code === 'ENOTTY' return code === 'ENOTSUP' || code === 'ENOSYS' || code === 'EINVAL' || code === 'EXDEV' || code === 'ENOTTY'
} }
@@ -8986,7 +8911,6 @@ class ExportService {
? path.join(outputDir, 'texts') ? path.join(outputDir, 'texts')
: outputDir : outputDir
const createdTaskDirs = new Set<string>() const createdTaskDirs = new Set<string>()
const reservedOutputPaths = new Set<string>()
const ensureTaskDir = async (dirPath: string) => { const ensureTaskDir = async (dirPath: string) => {
if (createdTaskDirs.has(dirPath)) return if (createdTaskDirs.has(dirPath)) return
await fs.promises.mkdir(dirPath, { recursive: true }) await fs.promises.mkdir(dirPath, { recursive: true })
@@ -9235,8 +9159,10 @@ class ExportService {
phaseLabel: '准备导出' phaseLabel: '准备导出'
}) })
const fileNamingMode = this.normalizeFileNamingMode(effectiveOptions.fileNamingMode) const sanitizeName = (value: string) => value.replace(/[<>:"\/\\|?*]/g, '_').replace(/\.+$/, '').trim()
const safeName = this.buildSessionExportBaseName(sessionId, sessionInfo.displayName, effectiveOptions) const baseName = sanitizeName(sessionInfo.displayName || sessionId) || sanitizeName(sessionId) || 'session'
const suffix = sanitizeName(effectiveOptions.fileNameSuffix || '')
const safeName = suffix ? `${baseName}_${suffix}` : baseName
const sessionNameWithTypePrefix = effectiveOptions.sessionNameWithTypePrefix !== false const sessionNameWithTypePrefix = effectiveOptions.sessionNameWithTypePrefix !== false
const sessionTypePrefix = sessionNameWithTypePrefix ? await this.getSessionFilePrefix(sessionId) : '' const sessionTypePrefix = sessionNameWithTypePrefix ? await this.getSessionFilePrefix(sessionId) : ''
const fileNameWithPrefix = `${sessionTypePrefix}${safeName}` const fileNameWithPrefix = `${sessionTypePrefix}${safeName}`
@@ -9254,13 +9180,13 @@ class ExportService {
else if (effectiveOptions.format === 'txt') ext = '.txt' else if (effectiveOptions.format === 'txt') ext = '.txt'
else if (effectiveOptions.format === 'weclone') ext = '.csv' else if (effectiveOptions.format === 'weclone') ext = '.csv'
else if (effectiveOptions.format === 'html') ext = '.html' else if (effectiveOptions.format === 'html') ext = '.html'
const preferredOutputPath = path.join(sessionDir, `${fileNameWithPrefix}${ext}`) const outputPath = path.join(sessionDir, `${fileNameWithPrefix}${ext}`)
const canTrySkipUnchanged = canTrySkipUnchangedTextSessions && const canTrySkipUnchanged = canTrySkipUnchangedTextSessions &&
typeof messageCountHint === 'number' && typeof messageCountHint === 'number' &&
messageCountHint >= 0 && messageCountHint >= 0 &&
typeof latestTimestampHint === 'number' && typeof latestTimestampHint === 'number' &&
latestTimestampHint > 0 && latestTimestampHint > 0 &&
await this.pathExists(preferredOutputPath) await this.pathExists(outputPath)
if (canTrySkipUnchanged) { if (canTrySkipUnchanged) {
const latestRecord = exportRecordService.getLatestRecord(sessionId, effectiveOptions.format) const latestRecord = exportRecordService.getLatestRecord(sessionId, effectiveOptions.format)
const hasNoDataChange = Boolean( const hasNoDataChange = Boolean(
@@ -9287,10 +9213,6 @@ class ExportService {
} }
} }
const outputPath = fileNamingMode === 'date-range'
? await this.reserveUniqueOutputPath(preferredOutputPath, reservedOutputPaths)
: preferredOutputPath
let result: { success: boolean; error?: string } let result: { success: boolean; error?: string }
if (effectiveOptions.format === 'json' || effectiveOptions.format === 'arkme-json') { if (effectiveOptions.format === 'json' || effectiveOptions.format === 'arkme-json') {
result = await this.exportSessionToDetailedJson(sessionId, outputPath, effectiveOptions, sessionProgress, control) result = await this.exportSessionToDetailedJson(sessionId, outputPath, effectiveOptions, sessionProgress, control)

16
package-lock.json generated
View File

@@ -23,7 +23,7 @@
"koffi": "^2.9.0", "koffi": "^2.9.0",
"lucide-react": "^1.7.0", "lucide-react": "^1.7.0",
"react": "^19.2.3", "react": "^19.2.3",
"react-dom": "^19.2.3", "react-dom": "^19.2.5",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
"react-router-dom": "^7.14.0", "react-router-dom": "^7.14.0",
"react-virtuoso": "^4.18.1", "react-virtuoso": "^4.18.1",
@@ -8470,24 +8470,24 @@
} }
}, },
"node_modules/react": { "node_modules/react": {
"version": "19.2.4", "version": "19.2.5",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz",
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/react-dom": { "node_modules/react-dom": {
"version": "19.2.4", "version": "19.2.5",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz",
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"scheduler": "^0.27.0" "scheduler": "^0.27.0"
}, },
"peerDependencies": { "peerDependencies": {
"react": "^19.2.4" "react": "^19.2.5"
} }
}, },
"node_modules/react-markdown": { "node_modules/react-markdown": {

View File

@@ -37,7 +37,7 @@
"koffi": "^2.9.0", "koffi": "^2.9.0",
"lucide-react": "^1.7.0", "lucide-react": "^1.7.0",
"react": "^19.2.3", "react": "^19.2.3",
"react-dom": "^19.2.3", "react-dom": "^19.2.5",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
"react-router-dom": "^7.14.0", "react-router-dom": "^7.14.0",
"react-virtuoso": "^4.18.1", "react-virtuoso": "^4.18.1",

View File

@@ -107,6 +107,44 @@ function App() {
const [showAnalyticsConsent, setShowAnalyticsConsent] = useState(false) const [showAnalyticsConsent, setShowAnalyticsConsent] = useState(false)
const [analyticsConsent, setAnalyticsConsent] = useState<boolean | null>(null) const [analyticsConsent, setAnalyticsConsent] = useState<boolean | null>(null)
const [showWaylandWarning, setShowWaylandWarning] = useState(false)
useEffect(() => {
const checkWaylandStatus = async () => {
try {
// 防止在非客户端环境报错,先检查 API 是否存在
if (!window.electronAPI?.app?.checkWayland) return
// 通过 configService 检查是否已经弹过窗
const hasWarned = await window.electronAPI.config.get('waylandWarningShown')
if (!hasWarned) {
const isWayland = await window.electronAPI.app.checkWayland()
if (isWayland) {
setShowWaylandWarning(true)
}
}
} catch (e) {
console.error('检查 Wayland 状态失败:', e)
}
}
// 只有在协议同意之后并且已经进入主应用流程才检查
if (!isAgreementWindow && !isOnboardingWindow && !agreementLoading) {
checkWaylandStatus()
}
}, [isAgreementWindow, isOnboardingWindow, agreementLoading])
const handleDismissWaylandWarning = async () => {
try {
// 记录到本地配置中,下次不再提示
await window.electronAPI.config.set('waylandWarningShown', true)
} catch (e) {
console.error('保存 Wayland 提示状态失败:', e)
}
setShowWaylandWarning(false)
}
useEffect(() => { useEffect(() => {
if (location.pathname !== '/settings') { if (location.pathname !== '/settings') {
settingsBackgroundRef.current = location settingsBackgroundRef.current = location
@@ -647,6 +685,33 @@ function App() {
</div> </div>
)} )}
{/*{showWaylandWarning && (*/}
{/* <div className="agreement-overlay">*/}
{/* <div className="agreement-modal">*/}
{/* <div className="agreement-header">*/}
{/* <Shield size={32} />*/}
{/* <h2>环境兼容性提示 (Wayland)</h2>*/}
{/* </div>*/}
{/* <div className="agreement-content">*/}
{/* <div className="agreement-text">*/}
{/* <p>检测到您当前正在使用 <strong>Wayland</strong> 显示服务器。</p>*/}
{/* <p>在 Wayland 环境下,出于系统级的安全与设计机制,<strong>应用程序无法直接控制新弹出窗口的位置</strong>。</p>*/}
{/* <p>这可能导致某些独立窗口(如消息通知、图片查看器等)出现位置随机、或不受控制的情况。这是底层机制导致的,对此我们无能为力。</p>*/}
{/* <br />*/}
{/* <p>如果您觉得窗口位置异常严重影响了使用体验,建议尝试:</p>*/}
{/* <p>1. 在系统登录界面,将会话切换回 <strong>X11 (Xorg)</strong> 模式。</p>*/}
{/* <p>2. 修改您的桌面管理器 (WM/DE) 配置,强制指定该应用程序的窗口规则。</p>*/}
{/* </div>*/}
{/* </div>*/}
{/* <div className="agreement-footer">*/}
{/* <div className="agreement-actions">*/}
{/* <button className="btn btn-primary" onClick={handleDismissWaylandWarning}>我知道了,不再提示</button>*/}
{/* </div>*/}
{/* </div>*/}
{/* </div>*/}
{/* </div>*/}
{/*)}*/}
{/* 更新提示对话框 */} {/* 更新提示对话框 */}
<UpdateDialog <UpdateDialog
open={showUpdateDialog} open={showUpdateDialog}

View File

@@ -15,7 +15,6 @@ export interface ExportDefaultsSettingsPatch {
format?: string format?: string
avatars?: boolean avatars?: boolean
dateRange?: ExportDateRangeSelection dateRange?: ExportDateRangeSelection
fileNamingMode?: configService.ExportFileNamingMode
media?: configService.ExportDefaultMediaConfig media?: configService.ExportDefaultMediaConfig
voiceAsText?: boolean voiceAsText?: boolean
excelCompactColumns?: boolean excelCompactColumns?: boolean
@@ -45,11 +44,6 @@ const exportExcelColumnOptions = [
{ value: 'full', label: '完整列', desc: '含发送者昵称/微信ID/备注' } { value: 'full', label: '完整列', desc: '含发送者昵称/微信ID/备注' }
] as const ] as const
const exportFileNamingModeOptions: Array<{ value: configService.ExportFileNamingMode; label: string; desc: string }> = [
{ value: 'classic', label: '简洁模式', desc: '示例私聊_张三兼容旧版' },
{ value: 'date-range', label: '时间范围模式', desc: '示例私聊_张三_20250101-20250331推荐' }
]
const exportConcurrencyOptions = [1, 2, 3, 4, 5, 6] as const const exportConcurrencyOptions = [1, 2, 3, 4, 5, 6] as const
const getOptionLabel = (options: ReadonlyArray<{ value: string; label: string }>, value: string) => { const getOptionLabel = (options: ReadonlyArray<{ value: string; label: string }>, value: string) => {
@@ -62,15 +56,12 @@ export function ExportDefaultsSettingsForm({
layout = 'stacked' layout = 'stacked'
}: ExportDefaultsSettingsFormProps) { }: ExportDefaultsSettingsFormProps) {
const [showExportExcelColumnsSelect, setShowExportExcelColumnsSelect] = useState(false) const [showExportExcelColumnsSelect, setShowExportExcelColumnsSelect] = useState(false)
const [showExportFileNamingModeSelect, setShowExportFileNamingModeSelect] = useState(false)
const [isExportDateRangeDialogOpen, setIsExportDateRangeDialogOpen] = useState(false) const [isExportDateRangeDialogOpen, setIsExportDateRangeDialogOpen] = useState(false)
const exportExcelColumnsDropdownRef = useRef<HTMLDivElement>(null) const exportExcelColumnsDropdownRef = useRef<HTMLDivElement>(null)
const exportFileNamingModeDropdownRef = useRef<HTMLDivElement>(null)
const [exportDefaultFormat, setExportDefaultFormat] = useState('excel') const [exportDefaultFormat, setExportDefaultFormat] = useState('excel')
const [exportDefaultAvatars, setExportDefaultAvatars] = useState(true) const [exportDefaultAvatars, setExportDefaultAvatars] = useState(true)
const [exportDefaultDateRange, setExportDefaultDateRange] = useState<ExportDateRangeSelection>(() => createDefaultExportDateRangeSelection()) const [exportDefaultDateRange, setExportDefaultDateRange] = useState<ExportDateRangeSelection>(() => createDefaultExportDateRangeSelection())
const [exportDefaultFileNamingMode, setExportDefaultFileNamingMode] = useState<configService.ExportFileNamingMode>('classic')
const [exportDefaultMedia, setExportDefaultMedia] = useState<configService.ExportDefaultMediaConfig>({ const [exportDefaultMedia, setExportDefaultMedia] = useState<configService.ExportDefaultMediaConfig>({
images: true, images: true,
videos: true, videos: true,
@@ -85,11 +76,10 @@ export function ExportDefaultsSettingsForm({
useEffect(() => { useEffect(() => {
let cancelled = false let cancelled = false
void (async () => { void (async () => {
const [savedFormat, savedAvatars, savedDateRange, savedFileNamingMode, savedMedia, savedVoiceAsText, savedExcelCompactColumns, savedConcurrency] = await Promise.all([ const [savedFormat, savedAvatars, savedDateRange, savedMedia, savedVoiceAsText, savedExcelCompactColumns, savedConcurrency] = await Promise.all([
configService.getExportDefaultFormat(), configService.getExportDefaultFormat(),
configService.getExportDefaultAvatars(), configService.getExportDefaultAvatars(),
configService.getExportDefaultDateRange(), configService.getExportDefaultDateRange(),
configService.getExportDefaultFileNamingMode(),
configService.getExportDefaultMedia(), configService.getExportDefaultMedia(),
configService.getExportDefaultVoiceAsText(), configService.getExportDefaultVoiceAsText(),
configService.getExportDefaultExcelCompactColumns(), configService.getExportDefaultExcelCompactColumns(),
@@ -101,7 +91,6 @@ export function ExportDefaultsSettingsForm({
setExportDefaultFormat(savedFormat || 'excel') setExportDefaultFormat(savedFormat || 'excel')
setExportDefaultAvatars(savedAvatars ?? true) setExportDefaultAvatars(savedAvatars ?? true)
setExportDefaultDateRange(resolveExportDateRangeConfig(savedDateRange)) setExportDefaultDateRange(resolveExportDateRangeConfig(savedDateRange))
setExportDefaultFileNamingMode(savedFileNamingMode ?? 'classic')
setExportDefaultMedia(savedMedia ?? { setExportDefaultMedia(savedMedia ?? {
images: true, images: true,
videos: true, videos: true,
@@ -125,19 +114,15 @@ export function ExportDefaultsSettingsForm({
if (showExportExcelColumnsSelect && exportExcelColumnsDropdownRef.current && !exportExcelColumnsDropdownRef.current.contains(target)) { if (showExportExcelColumnsSelect && exportExcelColumnsDropdownRef.current && !exportExcelColumnsDropdownRef.current.contains(target)) {
setShowExportExcelColumnsSelect(false) setShowExportExcelColumnsSelect(false)
} }
if (showExportFileNamingModeSelect && exportFileNamingModeDropdownRef.current && !exportFileNamingModeDropdownRef.current.contains(target)) {
setShowExportFileNamingModeSelect(false)
}
} }
document.addEventListener('mousedown', handleClickOutside) document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside) return () => document.removeEventListener('mousedown', handleClickOutside)
}, [showExportExcelColumnsSelect, showExportFileNamingModeSelect]) }, [showExportExcelColumnsSelect])
const exportExcelColumnsValue = exportDefaultExcelCompactColumns ? 'compact' : 'full' const exportExcelColumnsValue = exportDefaultExcelCompactColumns ? 'compact' : 'full'
const exportDateRangeLabel = useMemo(() => getExportDateRangeLabel(exportDefaultDateRange), [exportDefaultDateRange]) const exportDateRangeLabel = useMemo(() => getExportDateRangeLabel(exportDefaultDateRange), [exportDefaultDateRange])
const exportExcelColumnsLabel = useMemo(() => getOptionLabel(exportExcelColumnOptions, exportExcelColumnsValue), [exportExcelColumnsValue]) const exportExcelColumnsLabel = useMemo(() => getOptionLabel(exportExcelColumnOptions, exportExcelColumnsValue), [exportExcelColumnsValue])
const exportFileNamingModeLabel = useMemo(() => getOptionLabel(exportFileNamingModeOptions, exportDefaultFileNamingMode), [exportDefaultFileNamingMode])
const notify = (text: string, success = true) => { const notify = (text: string, success = true) => {
onNotify?.(text, success) onNotify?.(text, success)
@@ -239,7 +224,6 @@ export function ExportDefaultsSettingsForm({
className={`settings-time-range-trigger ${isExportDateRangeDialogOpen ? 'open' : ''}`} className={`settings-time-range-trigger ${isExportDateRangeDialogOpen ? 'open' : ''}`}
onClick={() => { onClick={() => {
setShowExportExcelColumnsSelect(false) setShowExportExcelColumnsSelect(false)
setShowExportFileNamingModeSelect(false)
setIsExportDateRangeDialogOpen(true) setIsExportDateRangeDialogOpen(true)
}} }}
> >
@@ -263,50 +247,6 @@ export function ExportDefaultsSettingsForm({
}} }}
/> />
<div className="form-group">
<div className="form-copy">
<label></label>
<span className="form-hint"></span>
</div>
<div className="form-control">
<div className="select-field" ref={exportFileNamingModeDropdownRef}>
<button
type="button"
className={`select-trigger ${showExportFileNamingModeSelect ? 'open' : ''}`}
onClick={() => {
setShowExportFileNamingModeSelect(!showExportFileNamingModeSelect)
setShowExportExcelColumnsSelect(false)
setIsExportDateRangeDialogOpen(false)
}}
>
<span className="select-value">{exportFileNamingModeLabel}</span>
<ChevronDown size={16} />
</button>
{showExportFileNamingModeSelect && (
<div className="select-dropdown">
{exportFileNamingModeOptions.map((option) => (
<button
key={option.value}
type="button"
className={`select-option ${exportDefaultFileNamingMode === option.value ? 'active' : ''}`}
onClick={async () => {
setExportDefaultFileNamingMode(option.value)
await configService.setExportDefaultFileNamingMode(option.value)
onDefaultsChanged?.({ fileNamingMode: option.value })
notify('已更新导出文件命名方式', true)
setShowExportFileNamingModeSelect(false)
}}
>
<span className="option-label">{option.label}</span>
<span className="option-desc">{option.desc}</span>
</button>
))}
</div>
)}
</div>
</div>
</div>
<div className="form-group"> <div className="form-group">
<div className="form-copy"> <div className="form-copy">
<label>Excel </label> <label>Excel </label>
@@ -319,7 +259,6 @@ export function ExportDefaultsSettingsForm({
className={`select-trigger ${showExportExcelColumnsSelect ? 'open' : ''}`} className={`select-trigger ${showExportExcelColumnsSelect ? 'open' : ''}`}
onClick={() => { onClick={() => {
setShowExportExcelColumnsSelect(!showExportExcelColumnsSelect) setShowExportExcelColumnsSelect(!showExportExcelColumnsSelect)
setShowExportFileNamingModeSelect(false)
setIsExportDateRangeDialogOpen(false) setIsExportDateRangeDialogOpen(false)
}} }}
> >

View File

@@ -1621,7 +1621,6 @@ function ExportPage() {
const [exportDefaultFormat, setExportDefaultFormat] = useState<TextExportFormat>('excel') const [exportDefaultFormat, setExportDefaultFormat] = useState<TextExportFormat>('excel')
const [exportDefaultAvatars, setExportDefaultAvatars] = useState(true) const [exportDefaultAvatars, setExportDefaultAvatars] = useState(true)
const [exportDefaultDateRangeSelection, setExportDefaultDateRangeSelection] = useState<ExportDateRangeSelection>(() => createDefaultExportDateRangeSelection()) const [exportDefaultDateRangeSelection, setExportDefaultDateRangeSelection] = useState<ExportDateRangeSelection>(() => createDefaultExportDateRangeSelection())
const [exportDefaultFileNamingMode, setExportDefaultFileNamingMode] = useState<configService.ExportFileNamingMode>('classic')
const [exportDefaultMedia, setExportDefaultMedia] = useState<configService.ExportDefaultMediaConfig>({ const [exportDefaultMedia, setExportDefaultMedia] = useState<configService.ExportDefaultMediaConfig>({
images: true, images: true,
videos: true, videos: true,
@@ -2271,7 +2270,7 @@ function ExportPage() {
setIsBaseConfigLoading(true) setIsBaseConfigLoading(true)
let isReady = true let isReady = true
try { try {
const [savedPath, savedFormat, savedAvatars, savedMedia, savedVoiceAsText, savedExcelCompactColumns, savedTxtColumns, savedConcurrency, savedImageDeepSearchOnMiss, savedSessionMap, savedContentMap, savedSessionRecordMap, savedSnsPostCount, savedWriteLayout, savedSessionNameWithTypePrefix, savedDefaultDateRange, savedFileNamingMode, exportCacheScope] = await Promise.all([ const [savedPath, savedFormat, savedAvatars, savedMedia, savedVoiceAsText, savedExcelCompactColumns, savedTxtColumns, savedConcurrency, savedImageDeepSearchOnMiss, savedSessionMap, savedContentMap, savedSessionRecordMap, savedSnsPostCount, savedWriteLayout, savedSessionNameWithTypePrefix, savedDefaultDateRange, exportCacheScope] = await Promise.all([
configService.getExportPath(), configService.getExportPath(),
configService.getExportDefaultFormat(), configService.getExportDefaultFormat(),
configService.getExportDefaultAvatars(), configService.getExportDefaultAvatars(),
@@ -2288,7 +2287,6 @@ function ExportPage() {
configService.getExportWriteLayout(), configService.getExportWriteLayout(),
configService.getExportSessionNamePrefixEnabled(), configService.getExportSessionNamePrefixEnabled(),
configService.getExportDefaultDateRange(), configService.getExportDefaultDateRange(),
configService.getExportDefaultFileNamingMode(),
ensureExportCacheScope() ensureExportCacheScope()
]) ])
@@ -2320,7 +2318,6 @@ function ExportPage() {
setExportDefaultExcelCompactColumns(savedExcelCompactColumns ?? true) setExportDefaultExcelCompactColumns(savedExcelCompactColumns ?? true)
setExportDefaultConcurrency(savedConcurrency ?? 2) setExportDefaultConcurrency(savedConcurrency ?? 2)
setExportDefaultImageDeepSearchOnMiss(savedImageDeepSearchOnMiss ?? true) setExportDefaultImageDeepSearchOnMiss(savedImageDeepSearchOnMiss ?? true)
setExportDefaultFileNamingMode(savedFileNamingMode ?? 'classic')
const resolvedDefaultDateRange = resolveExportDateRangeConfig(savedDefaultDateRange) const resolvedDefaultDateRange = resolveExportDateRangeConfig(savedDefaultDateRange)
setExportDefaultDateRangeSelection(resolvedDefaultDateRange) setExportDefaultDateRangeSelection(resolvedDefaultDateRange)
setTimeRangeSelection(resolvedDefaultDateRange) setTimeRangeSelection(resolvedDefaultDateRange)
@@ -4400,7 +4397,6 @@ function ExportPage() {
displayNamePreference: options.displayNamePreference, displayNamePreference: options.displayNamePreference,
exportConcurrency: options.exportConcurrency, exportConcurrency: options.exportConcurrency,
imageDeepSearchOnMiss: options.imageDeepSearchOnMiss, imageDeepSearchOnMiss: options.imageDeepSearchOnMiss,
fileNamingMode: exportDefaultFileNamingMode,
sessionLayout, sessionLayout,
sessionNameWithTypePrefix, sessionNameWithTypePrefix,
dateRange: options.useAllTime dateRange: options.useAllTime
@@ -7093,9 +7089,6 @@ function ExportPage() {
if (patch.dateRange) { if (patch.dateRange) {
setExportDefaultDateRangeSelection(patch.dateRange) setExportDefaultDateRangeSelection(patch.dateRange)
} }
if (patch.fileNamingMode) {
setExportDefaultFileNamingMode(patch.fileNamingMode)
}
if (patch.media) { if (patch.media) {
const mediaPatch = patch.media const mediaPatch = patch.media
setExportDefaultMedia(mediaPatch) setExportDefaultMedia(mediaPatch)

View File

@@ -238,6 +238,23 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
const [aiInsightTelegramToken, setAiInsightTelegramToken] = useState('') const [aiInsightTelegramToken, setAiInsightTelegramToken] = useState('')
const [aiInsightTelegramChatIds, setAiInsightTelegramChatIds] = 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 可用性 // 检查 Hello 可用性
useEffect(() => { useEffect(() => {
setHelloAvailable(isWindows) setHelloAvailable(isWindows)
@@ -1457,11 +1474,13 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
{ {
value: 'quote-top' as const, value: 'quote-top' as const,
label: '引用在上', label: '引用在上',
description: '更接近当前 WeFlow 风格',
successMessage: '已切换为引用在上样式' successMessage: '已切换为引用在上样式'
}, },
{ {
value: 'quote-bottom' as const, value: 'quote-bottom' as const,
label: '正文在上', label: '正文在上',
description: '更接近微信 / 密语风格',
successMessage: '已切换为正文在上样式' successMessage: '已切换为正文在上样式'
} }
].map(option => { ].map(option => {
@@ -1511,6 +1530,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
<div className="quote-layout-card-footer"> <div className="quote-layout-card-footer">
<div className="quote-layout-card-title-group"> <div className="quote-layout-card-title-group">
<span className="quote-layout-card-title">{option.label}</span> <span className="quote-layout-card-title">{option.label}</span>
<span className="quote-layout-card-desc">{option.description}</span>
</div> </div>
</div> </div>
</button> </button>
@@ -1676,6 +1696,11 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
<div className="form-group"> <div className="form-group">
<label></label> <label></label>
<span className="form-hint"></span> <span className="form-hint"></span>
{isWayland && (
<span className="form-hint" style={{ color: '#ff4d4f', marginTop: '4px', display: 'block' }}>
Wayland
</span>
)}
<div className="custom-select"> <div className="custom-select">
<div <div
className={`custom-select-trigger ${positionDropdownOpen ? 'open' : ''}`} className={`custom-select-trigger ${positionDropdownOpen ? 'open' : ''}`}

View File

@@ -31,7 +31,6 @@ const steps = [
{ id: 'image', title: '图片密钥', desc: '获取 XOR 与 AES 密钥' }, { id: 'image', title: '图片密钥', desc: '获取 XOR 与 AES 密钥' },
{ id: 'security', title: '安全防护', desc: '保护你的数据' } { id: 'security', title: '安全防护', desc: '保护你的数据' }
] ]
type SetupStepId = typeof steps[number]['id']
interface WelcomePageProps { interface WelcomePageProps {
standalone?: boolean standalone?: boolean
@@ -439,48 +438,6 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
} }
} }
const jumpToStep = (stepId: SetupStepId) => {
const targetIndex = steps.findIndex(step => step.id === stepId)
if (targetIndex >= 0) setStepIndex(targetIndex)
}
const validateDbStepBeforeNext = async (): Promise<string | null> => {
if (!dbPath) return '数据库目录步骤未完成:请先选择数据库目录'
if (dbPathValidationError) return `数据库目录步骤配置有误:${dbPathValidationError}`
try {
const wxids = await window.electronAPI.dbPath.scanWxids(dbPath)
if (!Array.isArray(wxids) || wxids.length === 0) {
return '数据库目录步骤配置有误:当前目录下未找到可用账号数据(缺少 db_storage请重新选择微信数据目录'
}
} catch (e) {
return `数据库目录步骤配置有误:目录读取失败,请确认该路径可访问(${String(e)}`
}
return null
}
const findConfigIssueBeforeConnect = async (): Promise<{ stepId: SetupStepId; message: string } | null> => {
const dbIssue = await validateDbStepBeforeNext()
if (dbIssue) return { stepId: 'db', message: dbIssue }
let scannedWxids: Array<{ wxid: string }> = []
try {
scannedWxids = await window.electronAPI.dbPath.scanWxids(dbPath)
} catch {
scannedWxids = []
}
if (!wxid) {
return { stepId: 'key', message: '解密密钥步骤未完成:请先选择微信账号 (wxid)' }
}
if (!scannedWxids.some(item => item.wxid === wxid)) {
return { stepId: 'key', message: `解密密钥步骤配置有误:微信账号「${wxid}」不在当前数据库目录中,请重新选择账号` }
}
if (!decryptKey || decryptKey.length !== 64) {
return { stepId: 'key', message: '解密密钥步骤未完成:请填写 64 位解密密钥' }
}
return null
}
const canGoNext = () => { const canGoNext = () => {
if (currentStep.id === 'intro') return true if (currentStep.id === 'intro') return true
if (currentStep.id === 'db') return Boolean(dbPath) && !dbPathValidationError if (currentStep.id === 'db') return Boolean(dbPath) && !dbPathValidationError
@@ -496,15 +453,7 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
return false return false
} }
const handleNext = async () => { const handleNext = () => {
if (currentStep.id === 'db') {
const dbStepIssue = await validateDbStepBeforeNext()
if (dbStepIssue) {
setError(dbStepIssue)
return
}
}
if (!canGoNext()) { if (!canGoNext()) {
if (currentStep.id === 'db' && !dbPath) setError('请先选择数据库目录') if (currentStep.id === 'db' && !dbPath) setError('请先选择数据库目录')
else if (currentStep.id === 'db' && dbPathValidationError) setError(dbPathValidationError) else if (currentStep.id === 'db' && dbPathValidationError) setError(dbPathValidationError)
@@ -524,12 +473,9 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
} }
const handleConnect = async () => { const handleConnect = async () => {
const configIssue = await findConfigIssueBeforeConnect() if (!dbPath) { setError('请先选择数据库目录'); return }
if (configIssue) { if (!wxid) { setError('请填写微信ID'); return }
setError(configIssue.message) if (!decryptKey || decryptKey.length !== 64) { setError('请填写 64 位解密密钥'); return }
jumpToStep(configIssue.stepId)
return
}
setIsConnecting(true) setIsConnecting(true)
setError('') setError('')
@@ -538,19 +484,7 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
try { try {
const result = await window.electronAPI.wcdb.testConnection(dbPath, decryptKey, wxid) const result = await window.electronAPI.wcdb.testConnection(dbPath, decryptKey, wxid)
if (!result.success) { if (!result.success) {
const errorMessage = result.error || 'WCDB 连接失败' setError(result.error || 'WCDB 连接失败')
if (errorMessage.includes('-3001')) {
const fallbackIssue = await findConfigIssueBeforeConnect()
if (fallbackIssue) {
setError(fallbackIssue.message)
jumpToStep(fallbackIssue.stepId)
} else {
setError(`数据库目录步骤配置有误:${errorMessage}`)
jumpToStep('db')
}
} else {
setError(errorMessage)
}
setLoading(false) setLoading(false)
return return
} }

View File

@@ -30,7 +30,6 @@ export const CONFIG_KEYS = {
EXPORT_DEFAULT_FORMAT: 'exportDefaultFormat', EXPORT_DEFAULT_FORMAT: 'exportDefaultFormat',
EXPORT_DEFAULT_AVATARS: 'exportDefaultAvatars', EXPORT_DEFAULT_AVATARS: 'exportDefaultAvatars',
EXPORT_DEFAULT_DATE_RANGE: 'exportDefaultDateRange', EXPORT_DEFAULT_DATE_RANGE: 'exportDefaultDateRange',
EXPORT_DEFAULT_FILE_NAMING_MODE: 'exportDefaultFileNamingMode',
EXPORT_DEFAULT_MEDIA: 'exportDefaultMedia', EXPORT_DEFAULT_MEDIA: 'exportDefaultMedia',
EXPORT_DEFAULT_VOICE_AS_TEXT: 'exportDefaultVoiceAsText', EXPORT_DEFAULT_VOICE_AS_TEXT: 'exportDefaultVoiceAsText',
EXPORT_DEFAULT_EXCEL_COMPACT_COLUMNS: 'exportDefaultExcelCompactColumns', EXPORT_DEFAULT_EXCEL_COMPACT_COLUMNS: 'exportDefaultExcelCompactColumns',
@@ -115,8 +114,6 @@ export interface ExportDefaultMediaConfig {
files: boolean files: boolean
} }
export type ExportFileNamingMode = 'classic' | 'date-range'
export type WindowCloseBehavior = 'ask' | 'tray' | 'quit' export type WindowCloseBehavior = 'ask' | 'tray' | 'quit'
export type QuoteLayout = 'quote-top' | 'quote-bottom' export type QuoteLayout = 'quote-top' | 'quote-bottom'
export type UpdateChannel = 'stable' | 'preview' | 'dev' export type UpdateChannel = 'stable' | 'preview' | 'dev'
@@ -437,18 +434,6 @@ export async function setExportDefaultDateRange(range: ExportDefaultDateRangeCon
await config.set(CONFIG_KEYS.EXPORT_DEFAULT_DATE_RANGE, range) await config.set(CONFIG_KEYS.EXPORT_DEFAULT_DATE_RANGE, range)
} }
// 获取导出默认文件命名方式
export async function getExportDefaultFileNamingMode(): Promise<ExportFileNamingMode | null> {
const value = await config.get(CONFIG_KEYS.EXPORT_DEFAULT_FILE_NAMING_MODE)
if (value === 'classic' || value === 'date-range') return value
return null
}
// 设置导出默认文件命名方式
export async function setExportDefaultFileNamingMode(mode: ExportFileNamingMode): Promise<void> {
await config.set(CONFIG_KEYS.EXPORT_DEFAULT_FILE_NAMING_MODE, mode)
}
// 获取导出默认媒体设置 // 获取导出默认媒体设置
export async function getExportDefaultMedia(): Promise<ExportDefaultMediaConfig | null> { export async function getExportDefaultMedia(): Promise<ExportDefaultMediaConfig | null> {
const value = await config.get(CONFIG_KEYS.EXPORT_DEFAULT_MEDIA) const value = await config.get(CONFIG_KEYS.EXPORT_DEFAULT_MEDIA)

View File

@@ -69,6 +69,7 @@ export interface ElectronAPI {
ignoreUpdate: (version: string) => Promise<{ success: boolean }> ignoreUpdate: (version: string) => Promise<{ success: boolean }>
onDownloadProgress: (callback: (progress: number) => void) => () => void onDownloadProgress: (callback: (progress: number) => void) => () => void
onUpdateAvailable: (callback: (info: { version: string; releaseNotes: string }) => void) => () => void onUpdateAvailable: (callback: (info: { version: string; releaseNotes: string }) => void) => () => void
checkWayland: () => Promise<boolean>
} }
notification: { notification: {
show: (data: { title: string; content: string; avatarUrl?: string; sessionId: string }) => Promise<{ success?: boolean; error?: string } | void> show: (data: { title: string; content: string; avatarUrl?: string; sessionId: string }) => Promise<{ success?: boolean; error?: string } | void>
@@ -1005,7 +1006,6 @@ export interface ExportOptions {
exportVoiceAsText?: boolean exportVoiceAsText?: boolean
excelCompactColumns?: boolean excelCompactColumns?: boolean
txtColumns?: string[] txtColumns?: string[]
fileNamingMode?: 'classic' | 'date-range'
sessionLayout?: 'shared' | 'per-session' sessionLayout?: 'shared' | 'per-session'
sessionNameWithTypePrefix?: boolean sessionNameWithTypePrefix?: boolean
displayNamePreference?: 'group-nickname' | 'remark' | 'nickname' displayNamePreference?: 'group-nickname' | 'remark' | 'nickname'