diff --git a/electron/preload.ts b/electron/preload.ts index 97393322..cc9922c9 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -71,6 +71,7 @@ contextBridge.exposeInMainWorld('electronAPI', { ipcRenderer.on('app:updateAvailable', (_, info) => callback(info)) return () => ipcRenderer.removeAllListeners('app:updateAvailable') }, + getPlatform: () => process.platform, }, // 日志 diff --git a/electron/services/analyticsService.ts b/electron/services/analyticsService.ts index 1ba6c004..dd05f7ce 100644 --- a/electron/services/analyticsService.ts +++ b/electron/services/analyticsService.ts @@ -1,7 +1,7 @@ import { ConfigService } from './config' import { wcdbService } from './wcdbService' import { join } from 'path' -import { readFile, writeFile, rm } from 'fs/promises' +import { readFile, writeFile, rm, mkdir } from 'fs/promises' import { app } from 'electron' import { createHash } from 'crypto' @@ -402,7 +402,10 @@ class AnalyticsService { private async saveCacheToFile(data: any) { try { - await writeFile(this.getCacheFilePath(), JSON.stringify(data)) + const filePath = this.getCacheFilePath() + const dir = require('path').dirname(filePath) + await mkdir(dir, { recursive: true }) + await writeFile(filePath, JSON.stringify(data)) } catch (e) { console.error('保存统计缓存失败:', e) } diff --git a/electron/services/wcdbCore.ts b/electron/services/wcdbCore.ts index 116ba457..cb2cc1bd 100644 --- a/electron/services/wcdbCore.ts +++ b/electron/services/wcdbCore.ts @@ -1,5 +1,5 @@ import { join, dirname, basename } from 'path' -import { appendFileSync, existsSync, mkdirSync, readdirSync, statSync, readFileSync } from 'fs' +import { appendFileSync, existsSync, mkdirSync, readdirSync, statSync, readFileSync, symlinkSync, rmdirSync, linkSync } from 'fs' import { tmpdir } from 'os' import * as fzstd from 'fzstd' @@ -22,6 +22,9 @@ export class WcdbCore { private currentKey: string | null = null private currentWxid: string | null = null private currentDbStoragePath: string | null = null + private kernel32: any = null + private dbJunctionPath: string | null = null + private dbJunctionTarget: string | null = null // 函数引用 private wcdbInitProtection: any = null @@ -131,6 +134,57 @@ export class WcdbCore { private lastCursorForceReopenAt = 0 private readonly cursorForceReopenCooldownMs = 15000 + private resolveShortPath(longPath: string): string | null { + if (process.platform !== 'win32' || !this.kernel32?.GetShortPathNameW) return longPath + try { + const buf = Buffer.alloc(520) // MAX_PATH * 2 for UTF-16 + const len = this.kernel32.GetShortPathNameW(longPath, buf, 260) + if (len > 0 && len < 260) { + return buf.toString('utf16le', 0, len * 2) + } + } catch (e) { + this.writeLog(`[wcdbCore] resolveShortPath failed: ${String(e)}`, true) + } + return null + } + + /** + * 为包含非 ASCII 字符的路径创建 Windows Junction Point + */ + private ensureDbJunction(dbBasePath: string): string { + if (process.platform !== 'win32') return dbBasePath + if (!/[^\x00-\x7F]/.test(dbBasePath)) return dbBasePath + try { + const mountPoint = join(tmpdir(), 'weflow_db_junc') + if (this.dbJunctionPath === mountPoint && this.dbJunctionTarget === dbBasePath && existsSync(mountPoint)) { + return mountPoint + } + if (existsSync(mountPoint)) { + try { rmdirSync(mountPoint) } catch {} + } + symlinkSync(dbBasePath, mountPoint, 'junction') + this.dbJunctionPath = mountPoint + this.dbJunctionTarget = dbBasePath + this.writeLog(`[wcdbCore] junction created: ${mountPoint} -> ${dbBasePath}`, true) + return mountPoint + } catch (e) { + this.writeLog(`[wcdbCore] junction failed: ${String(e)}`, true) + return dbBasePath + } + } + + private cleanupDbJunction() { + if (process.platform !== 'win32' || !this.dbJunctionPath) return + try { + if (existsSync(this.dbJunctionPath)) rmdirSync(this.dbJunctionPath) + this.writeLog(`[wcdbCore] junction removed: ${this.dbJunctionPath}`, true) + } catch (e) { + this.writeLog(`[wcdbCore] junction removal failed: ${String(e)}`, true) + } + this.dbJunctionPath = null + this.dbJunctionTarget = null + } + setPaths(resourcesPath: string, userDataPath: string): void { this.resourcesPath = resourcesPath this.userDataPath = userDataPath @@ -707,6 +761,15 @@ export class WcdbCore { } } + if (process.platform === 'win32') { + try { + this.kernel32 = this.koffi.load('kernel32.dll') + this.kernel32.GetShortPathNameW = this.kernel32.func('uint32 __stdcall GetShortPathNameW(const char16* longPath, _Out_ char16* shortPath, uint32 bufferSize)') + } catch (e) { + this.writeLog(`[bootstrap] failed to load kernel32 for short paths: ${String(e)}`, true) + } + } + this.writeLog(`[bootstrap] koffi.load begin path=${dllPath}`, true) this.lib = this.koffi.load(dllPath) this.writeLog('[bootstrap] koffi.load ok', true) @@ -1187,13 +1250,6 @@ export class WcdbCore { // wcdb_status wcdb_cloud_init(int32_t interval_seconds) try { this.wcdbCloudInit = this.lib.func('int32 wcdb_cloud_init(int32 intervalSeconds)') - } catch { - this.wcdbCloudInit = null - } - - // wcdb_status wcdb_cloud_report(const char* stats_json) - try { - this.wcdbCloudReport = this.lib.func('int32 wcdb_cloud_report(const char* statsJson)') } catch { this.wcdbCloudReport = null } @@ -1231,14 +1287,6 @@ export class WcdbCore { */ async testConnection(dbPath: string, hexKey: string, wxid: string): Promise<{ success: boolean; error?: string; sessionCount?: number }> { try { - // 如果当前已经有相同参数的活动连接,直接返回成功 - if (this.handle !== null && - this.currentPath === dbPath && - this.currentKey === hexKey && - this.currentWxid === wxid) { - return { success: true, sessionCount: 0 } - } - // 记录当前活动连接,用于在测试结束后恢复(避免影响聊天页等正在使用的连接) const hadActiveConnection = this.handle !== null const prevPath = this.currentPath @@ -1253,8 +1301,9 @@ export class WcdbCore { } } - // 构建 db_storage 目录路径 - const dbStoragePath = this.resolveDbStoragePath(dbPath, wxid) + // 构建 db_storage 目录路径(使用 junction 确保 DLL 获得全 ASCII 路径) + const effectiveTestPath = this.ensureDbJunction(dbPath) + const dbStoragePath = this.resolveDbStoragePath(effectiveTestPath, wxid) this.writeLog(`testConnection dbPath=${dbPath} wxid=${wxid} dbStorage=${dbStoragePath || 'null'}`) if (!dbStoragePath || !existsSync(dbStoragePath)) { @@ -1263,15 +1312,16 @@ export class WcdbCore { // 递归查找 session.db const sessionDbPath = this.findSessionDb(dbStoragePath) - this.writeLog(`testConnection sessionDb=${sessionDbPath || 'null'}`) - if (!sessionDbPath) { return { success: false, error: this.formatInitProtectionError(-3002) } } // 分配输出参数内存 const handleOut = [0] - const result = this.wcdbOpenAccount(sessionDbPath, hexKey, handleOut) + // 为了兼容 Junction 和中文路径,仅在必要时转为 8.3 短路径 + const isAlreadyAscii = sessionDbPath && !/[^\x00-\x7F]/.test(sessionDbPath); + const finalSessionPath = isAlreadyAscii ? sessionDbPath : (this.resolveShortPath(sessionDbPath) || sessionDbPath); + const result = this.wcdbOpenAccount(finalSessionPath, hexKey, handleOut) if (result !== 0) { await this.printLogs() @@ -1290,6 +1340,7 @@ export class WcdbCore { this.wcdbShutdown() this.handle = null this.currentPath = null + this.cleanupDbJunction() this.currentKey = null this.currentWxid = null this.initialized = false @@ -1308,9 +1359,8 @@ export class WcdbCore { return { success: true, sessionCount: 0 } } catch (e) { - console.error('测试连接异常:', e) - this.writeLog(`testConnection exception: ${String(e)}`) - return { success: false, error: this.formatInitProtectionError(-3004) } + console.error('测试数据库连接异常:', e) + return { success: false, error: String(e) } } } @@ -1529,7 +1579,8 @@ export class WcdbCore { if (!initOk) return false } - const dbStoragePath = this.resolveDbStoragePath(dbPath, wxid) + const effectiveDbPath = this.ensureDbJunction(dbPath) + const dbStoragePath = this.resolveDbStoragePath(effectiveDbPath, wxid) this.writeLog(`open dbPath=${dbPath} wxid=${wxid} dbStorage=${dbStoragePath || 'null'}`, true) if (!dbStoragePath || !existsSync(dbStoragePath)) { @@ -1548,8 +1599,26 @@ export class WcdbCore { return false } + // [核心修复] 解决朋友圈 -3 错误:微信 DLL 逻辑会从 session.db 所在目录找 sns.db + if (process.platform === 'win32') { + try { + const snsDbSource = join(dbStoragePath, 'sns', 'sns.db'); + const sessionDir = join(dbStoragePath, 'session'); + const snsDbTarget = join(sessionDir, 'sns.db'); + if (existsSync(snsDbSource) && existsSync(sessionDir) && !existsSync(snsDbTarget)) { + this.writeLog(`[wcdbCore] creating sns.db hardlink for compat: ${snsDbTarget} -> ${snsDbSource}`, true); + linkSync(snsDbSource, snsDbTarget); + } + } catch (linkErr) { + this.writeLog(`[wcdbCore] sns.db link failed: ${String(linkErr)}`, true); + } + } + const handleOut = [0] - const result = this.wcdbOpenAccount(sessionDbPath, hexKey, handleOut) + // 为了兼容 Junction 和中文路径,仅在必要时转为 8.3 短路径 + const isAlreadyAscii = sessionDbPath && !/[^\x00-\x7F]/.test(sessionDbPath); + const finalSessionPath = isAlreadyAscii ? sessionDbPath : (this.resolveShortPath(sessionDbPath) || sessionDbPath); + const result = this.wcdbOpenAccount(finalSessionPath, hexKey, handleOut) if (result !== 0) { console.error('打开数据库失败:', result) @@ -1576,15 +1645,10 @@ export class WcdbCore { try { this.wcdbSetMyWxid(this.handle, wxid) } catch (e) { - // 静默失败 + console.error('设置 wxid 失败:', e) } } - if (this.isLogEnabled()) { - this.startLogPolling() - } - this.writeLog(`open ok handle=${handle}`, true) - await this.dumpDbStatus('open') - await this.runPostOpenDiagnostics(dbPath, dbStoragePath, sessionDbPath, wxid) + this.startLogPolling() return true } catch (e) { console.error('打开数据库异常:', e) @@ -1608,6 +1672,7 @@ export class WcdbCore { } this.handle = null this.currentPath = null + this.cleanupDbJunction() this.currentKey = null this.currentWxid = null this.currentDbStoragePath = null diff --git a/package-lock.json b/package-lock.json index 0c06ec13..6f7649f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -79,6 +79,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -770,7 +771,6 @@ "dev": true, "license": "BSD-2-Clause", "optional": true, - "peer": true, "dependencies": { "cross-dirname": "^0.1.0", "debug": "^4.3.4", @@ -792,7 +792,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -809,7 +808,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "universalify": "^2.0.0" }, @@ -824,7 +822,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">= 10.0.0" } @@ -2974,6 +2971,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -3099,6 +3097,7 @@ "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -3654,6 +3653,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -4390,8 +4390,7 @@ "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", "dev": true, "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/cross-spawn": { "version": "7.0.6", @@ -4662,6 +4661,7 @@ "integrity": "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "app-builder-lib": "26.8.1", "builder-util": "26.8.1", @@ -4848,6 +4848,7 @@ "resolved": "https://registry.npmjs.org/echarts/-/echarts-6.0.0.tgz", "integrity": "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==", "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "2.3.0", "zrender": "6.0.0" @@ -5115,7 +5116,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", @@ -5136,7 +5136,6 @@ "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -8351,7 +8350,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "commander": "^9.4.0" }, @@ -8369,7 +8367,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": "^12.20.0 || >=14" } @@ -8474,6 +8471,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -8483,6 +8481,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -8919,6 +8918,7 @@ "integrity": "sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.1.5", @@ -9549,7 +9549,6 @@ "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" @@ -10070,6 +10069,7 @@ "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -10159,7 +10159,8 @@ "resolved": "https://registry.npmjs.org/vite-plugin-electron-renderer/-/vite-plugin-electron-renderer-0.14.6.tgz", "integrity": "sha512-oqkWFa7kQIkvHXG7+Mnl1RTroA4sP0yesKatmAy0gjZC4VwUqlvF9IvOpHd1fpLWsqYX/eZlVxlhULNtaQ78Jw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/wcwidth": { "version": "1.0.1", diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index b62f101e..baa05b3b 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -1057,7 +1057,8 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { const validatePath = (path: string): string | null => { if (!path) return null - if (/[\u4e00-\u9fa5]/.test(path)) { + const isWindows = window.electronAPI.app.getPlatform() === 'win32' + if (!isWindows && /[\u4e00-\u9fa5]/.test(path)) { return '路径包含中文字符,请迁移至全英文目录' } return null diff --git a/src/pages/WelcomePage.tsx b/src/pages/WelcomePage.tsx index 1dda111c..eaa39ac4 100644 --- a/src/pages/WelcomePage.tsx +++ b/src/pages/WelcomePage.tsx @@ -352,8 +352,10 @@ function WelcomePage({ standalone = false }: WelcomePageProps) { const validatePath = (path: string): string | null => { if (!path) return null - // 检测中文字符和其他可能有问题的特殊字符 - if (/[\u4e00-\u9fa5]/.test(path)) { + // 仅针对非 Windows 平台恢复中文路径校验 + // Windows 端已通过 wcdbCore 的 Junction Point 修复,无需拦截 + const isWindows = window.electronAPI.app.getPlatform() === 'win32' + if (!isWindows && /[\u4e00-\u9fa5]/.test(path)) { return DB_PATH_CHINESE_ERROR } return null diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index 244896d2..ede476fc 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -69,6 +69,7 @@ export interface ElectronAPI { ignoreUpdate: (version: string) => Promise<{ success: boolean }> onDownloadProgress: (callback: (progress: number) => void) => () => void onUpdateAvailable: (callback: (info: { version: string; releaseNotes: string }) => void) => () => void + getPlatform: () => string } notification: { show: (data: { title: string; content: string; avatarUrl?: string; sessionId: string }) => Promise<{ success?: boolean; error?: string } | void>