Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions electron/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
ipcRenderer.on('app:updateAvailable', (_, info) => callback(info))
return () => ipcRenderer.removeAllListeners('app:updateAvailable')
},
getPlatform: () => process.platform,
},

// 日志
Expand Down
7 changes: 5 additions & 2 deletions electron/services/analyticsService.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -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 })
Comment on lines +405 to +407
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里用 require('path').dirname 与顶部的 ESM import 风格不一致,而且已从 path 引入了 join。建议改为直接从 path import dirname(或复用已有 import),避免在 TS 文件里混用 require。

Copilot uses AI. Check for mistakes.
await writeFile(filePath, JSON.stringify(data))
} catch (e) {
console.error('保存统计缓存失败:', e)
}
Expand Down
131 changes: 98 additions & 33 deletions electron/services/wcdbCore.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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
Expand Down Expand Up @@ -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
Comment on lines +154 to +166
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ensureDbJunction 把 junction 放在 tmpdir() 下,但 Windows 的临时目录路径本身可能包含中文(例如用户名为中文),这样 mountPoint 仍然是非 ASCII,可能无法达到“让 DLL 获得全 ASCII 路径”的目的。建议把 mountPoint 基于一个可保证 ASCII 的目录(或对 tmpdir() 先取 8.3 short path/另选 ProgramData 等)。

Copilot uses AI. Check for mistakes.
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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wcdbCloudInit 的加载失败时把 wcdbCloudReport 置空了,而且当前代码也不再加载 wcdb_cloud_report 符号,这会导致 cloudReport() 永远返回“not supported”。建议分别加载 wcdb_cloud_init / wcdb_cloud_report,并在各自的 catch 中置空对应字段。

Suggested change
} catch {
} catch {
this.wcdbCloudInit = null
}
// wcdb_status wcdb_cloud_report()
try {
this.wcdbCloudReport = this.lib.func('int32 wcdb_cloud_report()')
} catch {

Copilot uses AI. Check for mistakes.
this.wcdbCloudReport = null
}
Expand Down Expand Up @@ -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
Expand All @@ -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)) {
Expand All @@ -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) }
}
Comment on lines 1304 to 1317
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

testConnection() 调用 ensureDbJunction() 后在多处失败分支直接返回(如 dbStorage/session.db 不存在、openAccount 失败),目前不会清理可能已创建的 junction。建议将 junction 清理放到 finally,并在成功/失败路径都执行回收。

Copilot uses AI. Check for mistakes.

// 分配输出参数内存
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()
Expand All @@ -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
Expand All @@ -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) }
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

testConnection() 的异常兜底现在直接返回 String(e),会丢失此前用 formatInitProtectionError(-3004) 生成的统一错误码/文案;前端(如 WelcomePage)有按 -3001 等错误码做分支处理的逻辑。建议保持返回统一的格式化错误(并可额外记录原始异常到日志)。

Suggested change
return { success: false, error: String(e) }
return { success: false, error: this.formatInitProtectionError(-3004) }

Copilot uses AI. Check for mistakes.
}
}

Expand Down Expand Up @@ -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)) {
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -1608,6 +1672,7 @@ export class WcdbCore {
}
this.handle = null
this.currentPath = null
this.cleanupDbJunction()
this.currentKey = null
this.currentWxid = null
this.currentDbStoragePath = null
Expand Down
Loading
Loading