fix: 修复登录报错key问题

This commit is contained in:
电摇小子
2026-07-14 11:07:35 +08:00
parent aebf56b3f7
commit eaf8e9b07d
11 changed files with 215 additions and 77 deletions
+40 -38
View File
@@ -5,6 +5,7 @@ import path from 'path'
import { promisify } from 'util'
import { isValidDatabaseKey } from './database-key-store'
import crypto from 'crypto'
import { findResource, getResourceCandidates } from './resource-paths'
const execFileAsync = promisify(execFile)
@@ -25,33 +26,10 @@ export interface ImageKeyResult {
export class KeyServiceMac {
private getHelperPath(): string {
// 多 candidate fallback:覆盖 extraResources、asarUnpack、dev 三种场景
// (extraResources → Contents/Resources/resources/;asarUnpack 同路径;dev → cwd 或 app.getAppPath)
const candidates = [
// 1) extraResources 标准位置(electron-builder.yml 配的就是这个)
path.join(process.resourcesPath, 'resources', 'xkey_helper'),
// 2) process.resourcesPath 直接(防止 extraResources 没复制成功)
path.join(process.resourcesPath, 'xkey_helper'),
// 3) asarUnpack 路径(如果在 asar 内的 resources/ 被解包到 app.asar.unpacked)
path.join(app.getAppPath(), 'app.asar.unpacked', 'resources', 'xkey_helper'),
// 4) dev 模式 + 打包后某些版本 app.getAppPath() 也指向 .app 根目录
path.join(app.getAppPath(), 'resources', 'xkey_helper'),
// 5) dev 模式:cwd
path.join(process.cwd(), 'resources', 'xkey_helper')
].filter((p, idx, arr) => arr.indexOf(p) === idx) // 去重
// 诊断:即使命中也打 log,方便排查"装了但找不到"的问题(translocation / quarantine)
const statusList = candidates.map((candidate) => ({
path: candidate,
exists: fs.existsSync(candidate)
}))
console.log('[KeyServiceMac] xkey_helper candidates:', JSON.stringify(statusList))
const helperPath = candidates.find((candidate) => fs.existsSync(candidate))
const helperPath = findResource('xkey_helper')
if (!helperPath) {
throw new Error(
`找不到 xkey_helper(尝试 ${candidates.length} 个路径;` +
` app.isPackaged=${app.isPackaged} resourcesPath=${process.resourcesPath} ` +
` appPath=${app.getAppPath()} cwd=${process.cwd()})`
`找不到 xkey_helper(已检查:${getResourceCandidates('xkey_helper').join('')}`
)
}
return helperPath
@@ -163,7 +141,7 @@ export class KeyServiceMac {
'return "ERR::" & errNum & "::" & errMsg',
'end try'
]
onStatus?.('授权后请保持微信登录并活动...')
onStatus?.('授权后请保持微信登录界面 并点击登录微信...')
const { stdout } = await execFileAsync(
'/usr/bin/osascript',
scriptLines.flatMap((line) => ['-e', line]),
@@ -207,7 +185,8 @@ export class KeyServiceMac {
const orderedWxids: string[] = []
this.pushAccountIdCandidates(orderedWxids, path.basename(candidateAccountPath))
for (const candidate of wxidCandidates) this.pushAccountIdCandidates(orderedWxids, candidate)
for (const candidate of wxidCandidates)
this.pushAccountIdCandidates(orderedWxids, candidate)
onStatus?.(`正在校验候选 wxid${orderedWxids.length} 个)...`)
for (const candidateWxid of orderedWxids) {
@@ -255,20 +234,33 @@ export class KeyServiceMac {
private getKvcommCandidates(accountPath?: string): string[] {
const home = app.getPath('home')
const candidates = new Set<string>([
path.join(home, 'Library/Containers/com.tencent.xinWeChat/Data/Documents/app_data/net/kvcomm'),
path.join(home, 'Library/Containers/com.tencent.xinWeChat/Data/Library/Application Support/com.tencent.xinWeChat/xwechat/net/kvcomm'),
path.join(home, 'Library/Containers/com.tencent.xinWeChat/Data/Library/Application Support/com.tencent.xinWeChat/net/kvcomm'),
path.join(
home,
'Library/Containers/com.tencent.xinWeChat/Data/Documents/app_data/net/kvcomm'
),
path.join(
home,
'Library/Containers/com.tencent.xinWeChat/Data/Library/Application Support/com.tencent.xinWeChat/xwechat/net/kvcomm'
),
path.join(
home,
'Library/Containers/com.tencent.xinWeChat/Data/Library/Application Support/com.tencent.xinWeChat/net/kvcomm'
),
path.join(home, 'Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat/net/kvcomm')
])
const normalized = String(accountPath || '').replace(/\\/g, '/').replace(/\/+$/, '')
const normalized = String(accountPath || '')
.replace(/\\/g, '/')
.replace(/\/+$/, '')
const marker = '/xwechat_files'
const markerIndex = normalized.indexOf(marker)
if (markerIndex >= 0) {
candidates.add(`${normalized.slice(0, markerIndex)}/app_data/net/kvcomm`)
}
const newPathMatch = normalized.match(/^(.*\/com\.tencent\.xinWeChat\/(?:\d+\.\d+b\d+\.\d+|\d+\.\d+\.\d+))/)
const newPathMatch = normalized.match(
/^(.*\/com\.tencent\.xinWeChat\/(?:\d+\.\d+b\d+\.\d+|\d+\.\d+\.\d+))/
)
if (newPathMatch) {
candidates.add(`${newPathMatch[1]}/net/kvcomm`)
candidates.add(`${newPathMatch[1]}/xwechat/net/kvcomm`)
@@ -281,7 +273,9 @@ export class KeyServiceMac {
const candidates: string[] = []
this.pushAccountIdCandidates(candidates, wxidParam)
const normalized = String(accountPath || '').replace(/\\/g, '/').replace(/\/+$/, '')
const normalized = String(accountPath || '')
.replace(/\\/g, '/')
.replace(/\/+$/, '')
if (normalized) {
this.pushAccountIdCandidates(candidates, path.basename(normalized))
const root = this.resolveXwechatRootFromPath(normalized)
@@ -290,7 +284,8 @@ export class KeyServiceMac {
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
if (!entry.isDirectory()) continue
const entryPath = path.join(root, entry.name)
if (this.isAccountDirPath(entryPath)) this.pushAccountIdCandidates(candidates, entry.name)
if (this.isAccountDirPath(entryPath))
this.pushAccountIdCandidates(candidates, entry.name)
}
} catch {
// Ignore unreadable directories.
@@ -316,7 +311,8 @@ export class KeyServiceMac {
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
if (!entry.isDirectory()) continue
const entryPath = path.join(root, entry.name)
if (this.isAccountDirPath(entryPath) && this.isReasonableAccountId(entry.name)) push(entryPath)
if (this.isAccountDirPath(entryPath) && this.isReasonableAccountId(entry.name))
push(entryPath)
}
} catch {
// Ignore unreadable directories.
@@ -326,12 +322,16 @@ export class KeyServiceMac {
}
private resolveXwechatRootFromPath(accountPath?: string): string | null {
const normalized = String(accountPath || '').replace(/\\/g, '/').replace(/\/+$/, '')
const normalized = String(accountPath || '')
.replace(/\\/g, '/')
.replace(/\/+$/, '')
if (!normalized) return null
const marker = '/xwechat_files'
const markerIndex = normalized.indexOf(marker)
if (markerIndex >= 0) return normalized.slice(0, markerIndex + marker.length)
const newPathMatch = normalized.match(/^(.*\/com\.tencent\.xinWeChat\/(?:\d+\.\d+b\d+\.\d+|\d+\.\d+\.\d+))(\/|$)/)
const newPathMatch = normalized.match(
/^(.*\/com\.tencent\.xinWeChat\/(?:\d+\.\d+b\d+\.\d+|\d+\.\d+\.\d+))(\/|$)/
)
return newPathMatch ? newPathMatch[1] : null
}
@@ -366,7 +366,9 @@ export class KeyServiceMac {
}
private isReasonableAccountId(value: string): boolean {
const lowered = String(value || '').trim().toLowerCase()
const lowered = String(value || '')
.trim()
.toLowerCase()
if (!lowered || lowered.includes('/') || lowered.includes('\\')) return false
return !['xwechat_files', 'all_users', 'backup', 'wmpf', 'app_data'].includes(lowered)
}
+2 -9
View File
@@ -1,13 +1,13 @@
// @ts-nocheck
// Windows native bridge adapted from WeFlow. Koffi Win32 callbacks and optional
// helpers are intentionally dynamic; keep this file isolated from strict TS.
import { app } from 'electron'
import { join, dirname, delimiter } from 'path'
import { existsSync, copyFileSync, mkdirSync } from 'fs'
import { execFile, spawn } from 'child_process'
import { promisify } from 'util'
import os from 'os'
import crypto from 'crypto'
import { getResourceRoots as getSharedResourceRoots } from './resource-paths'
const execFileAsync = promisify(execFile)
@@ -69,14 +69,7 @@ export class KeyService {
private readonly DB_KEY_PROCESS_CHECK_INTERVAL_MS = 1000
private getResourceRoots(): string[] {
const roots = [
join(process.cwd(), 'resources'),
join(process.cwd(), 'resources', 'resources'),
join(process.resourcesPath || '', 'resources'),
process.resourcesPath || '',
join(app.getAppPath(), 'resources')
]
return Array.from(new Set(roots.filter((root) => root && existsSync(root))))
return getSharedResourceRoots()
}
private getDllPath(): string {
+35
View File
@@ -0,0 +1,35 @@
import { app } from 'electron'
import { existsSync } from 'fs'
import { dirname, join } from 'path'
function unique(values: string[]): string[] {
return Array.from(new Set(values.filter(Boolean)))
}
export function getResourceRoots(): string[] {
const appPath = app.getAppPath()
const appPathDir = dirname(appPath)
const execDir = dirname(process.execPath)
return unique([
process.env.WECHATEXPLORER_RESOURCES_PATH || '',
join(process.cwd(), 'resources'),
join(process.cwd(), 'resources', 'resources'),
join(process.resourcesPath || '', 'resources'),
process.resourcesPath || '',
join(appPath, 'resources'),
join(appPathDir, 'resources'),
appPathDir,
join(execDir, 'resources'),
join(dirname(execDir), 'Resources', 'resources'),
join(dirname(execDir), 'Resources')
]).filter((root) => existsSync(root))
}
export function getResourceCandidates(relativePath: string): string[] {
return unique(getResourceRoots().map((root) => join(root, relativePath)))
}
export function findResource(relativePath: string): string | null {
return getResourceCandidates(relativePath).find((candidate) => existsSync(candidate)) || null
}
+35 -28
View File
@@ -4,6 +4,7 @@ import os from 'os'
import crypto from 'crypto'
import { createRequire } from 'module'
import { createConnection, Socket } from 'net'
import { getResourceRoots } from './resource-paths'
export interface Wcdb4Session {
username: string
@@ -81,14 +82,14 @@ export function bootstrapWcdbNative(libPath?: string, libDirOverride?: string):
resourcePath: string
) => number
const resourceRoots = Array.from(
new Set([
libDir,
path.dirname(libDir),
process.env.WCDB_RESOURCES_PATH || '',
path.join(process.resourcesPath || process.cwd(), 'resources'),
process.resourcesPath || process.cwd(),
path.join(process.cwd(), 'resources')
])
new Set(
[
libDir,
path.dirname(libDir),
process.env.WCDB_RESOURCES_PATH || '',
...getResourceRoots()
].filter(Boolean)
)
)
let lastCode = -1
let initOk = false
@@ -1330,14 +1331,14 @@ export class Wcdb4Client {
) => number
const resourceRoots = Array.from(
new Set([
libDir,
path.dirname(libDir),
process.env.WCDB_RESOURCES_PATH || '',
path.join(process.resourcesPath || process.cwd(), 'resources'),
process.resourcesPath || process.cwd(),
path.join(process.cwd(), 'resources')
])
new Set(
[
libDir,
path.dirname(libDir),
process.env.WCDB_RESOURCES_PATH || '',
...getResourceRoots()
].filter(Boolean)
)
)
let lastCode = -1
@@ -1373,19 +1374,21 @@ export class Wcdb4Client {
? 'libwcdb_api.so'
: 'wcdb_api.dll'
const platformDir =
process.platform === 'darwin' ? 'macos' : process.platform === 'win32' ? 'win32' : process.platform
process.platform === 'darwin'
? 'macos'
: process.platform === 'win32'
? 'win32'
: process.platform
const archDir = process.arch === 'arm64' ? 'arm64' : 'x64'
const resourcesPath = process.resourcesPath || process.cwd()
const resourceRoots = getResourceRoots()
const candidates = [
process.env.WCDB_DLL_PATH,
path.join(resourcesPath, 'resources', 'wcdb', platformDir, archDir, libName),
path.join(resourcesPath, 'resources', 'wcdb', platformDir, 'x64', libName),
path.join(process.cwd(), 'resources', 'wcdb', platformDir, archDir, libName),
path.join(process.cwd(), 'resources', 'wcdb', platformDir, 'x64', libName),
path.join(resourcesPath, 'resources', platformDir, libName),
path.join(resourcesPath, 'resources', libName),
path.join(process.cwd(), 'resources', platformDir, libName),
path.join(process.cwd(), 'resources', libName)
...resourceRoots.flatMap((root) => [
path.join(root, 'wcdb', platformDir, archDir, libName),
path.join(root, 'wcdb', platformDir, 'x64', libName),
path.join(root, platformDir, libName),
path.join(root, libName)
])
].filter(Boolean) as string[]
const found = candidates.find((candidate) => fs.existsSync(candidate))
@@ -1393,9 +1396,13 @@ export class Wcdb4Client {
throw new Error(`找不到 WCDB native 库: ${candidates.join(', ')}`)
}
if (process.platform === 'win32') {
const runtimeDir = path.join(resourcesPath, 'resources', 'runtime', 'win32')
const runtimeDir = resourceRoots
.map((root) => path.join(root, 'runtime', 'win32'))
.find((candidate) => fs.existsSync(candidate))
const dllDir = path.dirname(found)
process.env.PATH = [dllDir, runtimeDir, process.env.PATH || ''].filter(Boolean).join(path.delimiter)
process.env.PATH = [dllDir, runtimeDir || '', process.env.PATH || '']
.filter(Boolean)
.join(path.delimiter)
}
return found
}