mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-10-04 03:01:42 +08:00
Merge pull request #41 from Wxw-Gu/codex/develop-xkey-4-1-13
Codex/develop xkey 4 1 13
This commit is contained in:
Executable
BIN
Binary file not shown.
@@ -0,0 +1 @@
|
||||
f1bbd88584075c0e6d3487357a06424949de26d7e6dcad803d210aab83b0cbda xkey_helper_4_1_13
|
||||
+1
-1
@@ -895,7 +895,7 @@ app.whenReady().then(async () => {
|
||||
wechatVersion: await detectWechatVersion(),
|
||||
dataStructureVersion: detectDataStructureVersion(settings.dbRoot),
|
||||
dataDirectoryDetected: validateDbRoot(settings.dbRoot).valid,
|
||||
autoDetectSupported: process.platform === 'win32' || intelMac,
|
||||
autoDetectSupported: process.platform === 'win32' || process.platform === 'darwin',
|
||||
wechatRunning: await isWechatRunning(),
|
||||
accountIdentified: Boolean(self?.wxid),
|
||||
dbConnected: chat.isReady(),
|
||||
|
||||
+162
-62
@@ -6,6 +6,7 @@ import { promisify } from 'util'
|
||||
import { isValidDatabaseKey } from './database-key-store'
|
||||
import crypto from 'crypto'
|
||||
import { findResource, getResourceCandidates } from './resource-paths'
|
||||
import { detectWechatVersion } from './services/connection-diagnostics'
|
||||
|
||||
const execFileAsync = promisify(execFile)
|
||||
|
||||
@@ -24,6 +25,115 @@ export interface ImageKeyResult {
|
||||
error?: string
|
||||
}
|
||||
|
||||
export type XkeyHelperMode = 'legacy' | 'wechat-4.1.13'
|
||||
|
||||
export function resolveXkeyHelperMode(wechatVersion: string): XkeyHelperMode {
|
||||
return /^4\.1\.13(?:\.|$)/.test(wechatVersion.trim()) ? 'wechat-4.1.13' : 'legacy'
|
||||
}
|
||||
|
||||
export function buildXkeyHelperArguments(pid: number, timeoutMs: number): string[] {
|
||||
return [String(pid), String(timeoutMs), '--profile', 'wechat-4.1.13', '--account']
|
||||
}
|
||||
|
||||
export function buildAppleSiliconXkeyInvocation(
|
||||
wechatVersion: string,
|
||||
pid: number,
|
||||
timeoutMs: number
|
||||
): {
|
||||
mode: XkeyHelperMode
|
||||
resourceName: string
|
||||
args: string[]
|
||||
waitMs: number
|
||||
timeoutSeconds: number
|
||||
execTimeoutMs: number
|
||||
} {
|
||||
const mode = resolveXkeyHelperMode(wechatVersion)
|
||||
const isWechat413 = mode === 'wechat-4.1.13'
|
||||
const waitMs = Math.max(isWechat413 ? 120_000 : 30_000, timeoutMs)
|
||||
const timeoutSeconds = Math.ceil(waitMs / 1000) + (isWechat413 ? 10 : 30)
|
||||
return {
|
||||
mode,
|
||||
resourceName: isWechat413 ? 'xkey_helper_4_1_13' : 'xkey_helper',
|
||||
args: isWechat413 ? buildXkeyHelperArguments(pid, waitMs) : [String(pid), String(waitMs)],
|
||||
waitMs,
|
||||
timeoutSeconds,
|
||||
execTimeoutMs: isWechat413 ? timeoutSeconds * 1000 + 5_000 : waitMs + 20_000
|
||||
}
|
||||
}
|
||||
|
||||
export function mapXkeyHelperFailure(
|
||||
rawError: string,
|
||||
fallbackCode = 'HELPER_RESULT_INVALID'
|
||||
): DatabaseKeyResult {
|
||||
const normalizedError = rawError.trim().toLowerCase()
|
||||
const parsedError = rawError.match(/(?:^|[\s"])(?:\\?"?)ERROR:([^:\s"}]+):?([^"}\r\n]*)/i)
|
||||
const code = parsedError?.[1]?.toUpperCase()
|
||||
const detail = parsedError?.[2]?.trim() || ''
|
||||
|
||||
if (
|
||||
code === 'CAPTURE_TIMEOUT' ||
|
||||
normalizedError.includes('timeout waiting for breakpoint hit') ||
|
||||
normalizedError.includes('timeout waiting for sink hit') ||
|
||||
normalizedError.includes('no_breakpoint_hit')
|
||||
) {
|
||||
return {
|
||||
success: false,
|
||||
code: 'CAPTURE_TIMEOUT',
|
||||
error:
|
||||
'已完成管理员授权,但监听期间微信没有触发账号密钥派生。请先停留在微信登录界面,在 TraceMemo 点击“自动获取密钥”,授权后点击微信“登录”;已有登录凭据时通常不需要扫码。'
|
||||
}
|
||||
}
|
||||
if (code === 'SCAN_FAILED' && detail.toLowerCase().includes('sink pattern not found')) {
|
||||
return {
|
||||
success: false,
|
||||
code,
|
||||
error:
|
||||
'内存扫描失败:未匹配到目标函数特征(Sink pattern not found),当前微信版本可能暂未适配。\n' +
|
||||
'建议步骤:降级微信到 4.1.8 (点击顶部"上手教程"获取下载链接) -> 重启电脑(冷启动) -> 自动获取密钥 -> 成功后再升级微信。\n' +
|
||||
'请不要连续重试,以免触发微信安全模式或系统内存保护。'
|
||||
}
|
||||
}
|
||||
if (code === 'SCAN_FAILED') {
|
||||
return {
|
||||
success: false,
|
||||
code,
|
||||
error: '内存扫描失败:当前微信版本或运行状态暂未适配。'
|
||||
}
|
||||
}
|
||||
if (normalizedError.includes('permission denied') || code === 'PERMISSION_DENIED') {
|
||||
return {
|
||||
success: false,
|
||||
code: code || 'PERMISSION_DENIED',
|
||||
error: '管理员授权不足,无法读取微信进程内存。'
|
||||
}
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
code: code || fallbackCode,
|
||||
error: code
|
||||
? `密钥工具执行未完成(${code}),请确认微信仍在运行后重试。`
|
||||
: '密钥工具未返回有效密钥,请确认微信仍在运行后重试。'
|
||||
}
|
||||
}
|
||||
|
||||
export function parseXkeyHelperOutput(output: string): DatabaseKeyResult {
|
||||
const payloads: Record<string, unknown>[] = []
|
||||
for (const match of output.matchAll(/\{[^{}]*\}/g)) {
|
||||
try {
|
||||
payloads.push(JSON.parse(match[0]) as Record<string, unknown>)
|
||||
} catch {
|
||||
// Ignore helper progress that is not JSON.
|
||||
}
|
||||
}
|
||||
const payload = payloads.find((item) => item.success === true && typeof item.key === 'string')
|
||||
const rawKey = typeof payload?.key === 'string' ? payload.key.trim().replace(/^0x/i, '') : ''
|
||||
if (isValidDatabaseKey(rawKey)) return { success: true, key: rawKey }
|
||||
|
||||
const errorPayload = payloads.find((item) => typeof item.result === 'string')
|
||||
const rawError = typeof errorPayload?.result === 'string' ? errorPayload.result.trim() : ''
|
||||
return mapXkeyHelperFailure(rawError)
|
||||
}
|
||||
|
||||
export class KeyServiceMac {
|
||||
private getMacKeyRuntimeDir(): string {
|
||||
return path.join(app.getPath('userData'), 'key-runtime')
|
||||
@@ -183,11 +293,11 @@ export class KeyServiceMac {
|
||||
}
|
||||
}
|
||||
|
||||
private getHelperPath(): string {
|
||||
const helperPath = findResource('xkey_helper')
|
||||
private getHelperPath(resourceName = 'xkey_helper'): string {
|
||||
const helperPath = findResource(resourceName)
|
||||
if (!helperPath) {
|
||||
throw new Error(
|
||||
`找不到 xkey_helper(已检查:${getResourceCandidates('xkey_helper').join(';')})`
|
||||
`找不到 ${resourceName}(已检查:${getResourceCandidates(resourceName).join(';')})`
|
||||
)
|
||||
}
|
||||
return helperPath
|
||||
@@ -219,50 +329,7 @@ export class KeyServiceMac {
|
||||
// Try the next process lookup strategy.
|
||||
}
|
||||
}
|
||||
throw new Error('未找到微信主进程,请先启动并登录微信')
|
||||
}
|
||||
|
||||
private parseHelperOutput(output: string): DatabaseKeyResult {
|
||||
const payloads: Record<string, unknown>[] = []
|
||||
for (const match of output.matchAll(/\{[^{}]*\}/g)) {
|
||||
try {
|
||||
payloads.push(JSON.parse(match[0]) as Record<string, unknown>)
|
||||
} catch {
|
||||
// Ignore helper progress that is not JSON.
|
||||
}
|
||||
}
|
||||
const payload = payloads.find((item) => item.success === true && typeof item.key === 'string')
|
||||
const rawKey = typeof payload?.key === 'string' ? payload.key.trim().replace(/^0x/i, '') : ''
|
||||
if (!isValidDatabaseKey(rawKey)) {
|
||||
const errorPayload = payloads.find((item) => typeof item.result === 'string')
|
||||
const rawError = typeof errorPayload?.result === 'string' ? errorPayload.result.trim() : ''
|
||||
const parsedError = rawError.match(/^ERROR:([^:]+):?(.*)$/i)
|
||||
const code = parsedError?.[1]?.toUpperCase()
|
||||
const detail = parsedError?.[2]?.trim() || ''
|
||||
if (code === 'SCAN_FAILED' && detail.toLowerCase().includes('sink pattern not found')) {
|
||||
return {
|
||||
success: false,
|
||||
code,
|
||||
error:
|
||||
'内存扫描失败:未匹配到目标函数特征(Sink pattern not found),当前微信版本可能暂未适配。\n' +
|
||||
'建议步骤:降级微信到 4.1.8 (点击顶部"上手教程"获取下载链接) -> 重启电脑(冷启动) -> 自动获取密钥 -> 成功后再升级微信。\n' +
|
||||
'请不要连续重试,以免触发微信安全模式或系统内存保护。'
|
||||
}
|
||||
}
|
||||
if (code === 'SCAN_FAILED') {
|
||||
return {
|
||||
success: false,
|
||||
code,
|
||||
error: `内存扫描失败:${detail || '未匹配到可用特征,当前微信版本可能暂未适配。'}`
|
||||
}
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
code,
|
||||
error: rawError || '密钥工具未返回有效的 64 位密钥'
|
||||
}
|
||||
}
|
||||
return { success: true, key: rawKey }
|
||||
throw new Error('未找到微信主进程,请先启动微信并停留在登录界面')
|
||||
}
|
||||
|
||||
async autoGetDbKey(
|
||||
@@ -292,16 +359,17 @@ export class KeyServiceMac {
|
||||
onStatus?.(result.success ? '密钥获取成功' : '密钥获取失败')
|
||||
return result
|
||||
}
|
||||
const wechatVersion = await detectWechatVersion()
|
||||
onStatus?.('正在查找微信进程...')
|
||||
const pid = await this.getWeChatPid()
|
||||
const helperPath = this.getHelperPath()
|
||||
const waitMs = Math.max(30_000, timeoutMs)
|
||||
const timeoutSeconds = Math.ceil(waitMs / 1000) + 30
|
||||
const invocation = buildAppleSiliconXkeyInvocation(wechatVersion, pid, timeoutMs)
|
||||
const isWechat413 = invocation.mode === 'wechat-4.1.13'
|
||||
const helperPath = this.getHelperPath(invocation.resourceName)
|
||||
onStatus?.('正在请求管理员授权...')
|
||||
const scriptLines = [
|
||||
`set helperPath to ${JSON.stringify(helperPath)}`,
|
||||
`set cmd to quoted form of helperPath & " ${pid} ${waitMs}"`,
|
||||
`set timeoutSec to ${timeoutSeconds}`,
|
||||
`set cmd to quoted form of helperPath & " ${invocation.args.join(' ')}"`,
|
||||
`set timeoutSec to ${invocation.timeoutSeconds}`,
|
||||
'try',
|
||||
'with timeout of timeoutSec seconds',
|
||||
'set outText to do shell script cmd with administrator privileges',
|
||||
@@ -311,25 +379,57 @@ export class KeyServiceMac {
|
||||
'return "ERR::" & errNum & "::" & errMsg',
|
||||
'end try'
|
||||
]
|
||||
onStatus?.('授权后 需要在微信登录界面 点击登录微信')
|
||||
onStatus?.(
|
||||
isWechat413
|
||||
? '授权后请在微信登录界面点击“登录”,已有登录凭据时通常不需要扫码'
|
||||
: '授权后 需要在微信登录界面 点击登录微信'
|
||||
)
|
||||
const { stdout } = await execFileAsync(
|
||||
'/usr/bin/osascript',
|
||||
scriptLines.flatMap((line) => ['-e', line]),
|
||||
{ timeout: waitMs + 20_000 }
|
||||
{ timeout: invocation.execTimeoutMs }
|
||||
)
|
||||
const output = String(stdout).trim()
|
||||
if (output.startsWith('ERR::-128')) return { success: false, error: '已取消管理员授权' }
|
||||
if (output.startsWith('ERR::')) {
|
||||
return {
|
||||
success: false,
|
||||
error: output.split('::').slice(2).join('::') || '密钥工具执行失败'
|
||||
}
|
||||
if (output.startsWith('ERR::-128')) {
|
||||
return { success: false, error: '已取消管理员授权' }
|
||||
}
|
||||
const result = this.parseHelperOutput(output.startsWith('OK::') ? output.slice(4) : output)
|
||||
if (output.startsWith('ERR::')) {
|
||||
const [, errorNumber = 'UNKNOWN', ...errorParts] = output.split('::')
|
||||
const result = mapXkeyHelperFailure(errorParts.join('::'), `OSASCRIPT_${errorNumber}`)
|
||||
onStatus?.('密钥获取失败')
|
||||
return result
|
||||
}
|
||||
const result = parseXkeyHelperOutput(output.startsWith('OK::') ? output.slice(4) : output)
|
||||
onStatus?.(result.success ? '密钥获取成功' : '密钥获取失败')
|
||||
return result
|
||||
} catch (error) {
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) }
|
||||
const processError = error as NodeJS.ErrnoException & {
|
||||
killed?: boolean
|
||||
signal?: NodeJS.Signals | null
|
||||
}
|
||||
if (processError.message?.includes('未找到微信主进程')) {
|
||||
return {
|
||||
success: false,
|
||||
code: 'WECHAT_NOT_RUNNING',
|
||||
error: '未找到微信主进程,请先启动微信并停留在登录界面。'
|
||||
}
|
||||
}
|
||||
if (
|
||||
processError.killed ||
|
||||
processError.code === 'ETIMEDOUT' ||
|
||||
processError.signal === 'SIGTERM'
|
||||
) {
|
||||
return {
|
||||
success: false,
|
||||
code: 'AUTH_TIMEOUT',
|
||||
error: '管理员授权等待超时,请点击“自动获取密钥”后及时完成系统授权。'
|
||||
}
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
code: typeof processError.code === 'string' ? processError.code : 'HELPER_EXEC_FAILED',
|
||||
error: '密钥工具执行失败,请确认微信仍在运行后重试。'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -137,6 +137,7 @@ export function DatabaseConnectionPage({
|
||||
}: DatabaseConnectionPageProps): React.ReactElement {
|
||||
const isMac = platform === 'darwin'
|
||||
const isIntelMac = isMac && environment?.architecture === 'x64'
|
||||
const isAppleSiliconMac = isMac && !isIntelMac
|
||||
const defaultPath = isMac
|
||||
? '~/Library/Containers/com.tencent.xinWeChat/Data/Library/Application Support/com.tencent.xinWeChat/'
|
||||
: 'C:\\Users\\...\\WeChat Files\\Msg'
|
||||
@@ -189,18 +190,16 @@ export function DatabaseConnectionPage({
|
||||
<li>
|
||||
<span>2</span>
|
||||
<div>
|
||||
<strong>{isIntelMac ? '让微信停在登录页面' : '准备连接组件'}</strong>
|
||||
<strong>{isMac ? '让微信停在登录页面' : '准备连接组件'}</strong>
|
||||
<small>
|
||||
{isIntelMac
|
||||
? '启动微信,但暂时不要点击登录'
|
||||
: '页面会按当前系统给出对应步骤'}
|
||||
{isMac ? '启动微信,但暂时不要点击登录' : '页面会按当前系统给出对应步骤'}
|
||||
</small>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<span>3</span>
|
||||
<div>
|
||||
<strong>{isIntelMac ? '获取密钥并验证连接' : '登录并验证连接'}</strong>
|
||||
<strong>{isMac ? '获取密钥并验证连接' : '登录并验证连接'}</strong>
|
||||
<small>验证通过后进入主界面</small>
|
||||
</div>
|
||||
</li>
|
||||
@@ -248,11 +247,13 @@ export function DatabaseConnectionPage({
|
||||
: [
|
||||
'检查本机环境',
|
||||
'让微信停在登录页面',
|
||||
isIntelMac ? '确认开始获取密钥' : '确认开始准备',
|
||||
isMac ? '确认开始获取密钥' : '确认开始准备',
|
||||
isIntelMac
|
||||
? '正在获取 Intel Mac 密钥'
|
||||
: `正在完成 ${isMac ? 'macOS' : 'Windows'} 授权`,
|
||||
isIntelMac
|
||||
: isAppleSiliconMac
|
||||
? '正在监听 macOS 登录密钥'
|
||||
: '正在完成 Windows 授权',
|
||||
isMac
|
||||
? '获取成功,验证数据库连接'
|
||||
: '获取成功, 现在可以重新登录微信了',
|
||||
'验证数据库连接'
|
||||
@@ -264,14 +265,14 @@ export function DatabaseConnectionPage({
|
||||
: status ||
|
||||
[
|
||||
'确认下方检测结果;没有找到目录时可以手动选择。',
|
||||
isIntelMac
|
||||
isMac
|
||||
? '请启动微信并停留在未登录界面,暂时不要点击登录,然后点击“我已准备好”。'
|
||||
: '请退出当前微信账号,让微信停留在登录页面,然后点击“我已准备好”。',
|
||||
isIntelMac ? '确认开始获取密钥。' : '开始后请按页面提示完成系统授权。',
|
||||
isIntelMac
|
||||
isMac ? '确认开始获取密钥。' : '开始后请按页面提示完成系统授权。',
|
||||
isMac
|
||||
? '等页面提示可以登录后,立即回到微信点击登录。'
|
||||
: '正在准备连接组件,请不要关闭微信或 TraceMemo。',
|
||||
isIntelMac
|
||||
isMac
|
||||
? '密钥已获取,请继续验证数据库。'
|
||||
: '请回到微信完成登录,登录成功后再回来验证。',
|
||||
'正在验证密钥和本地数据库,请稍候。'
|
||||
@@ -445,12 +446,12 @@ export function DatabaseConnectionPage({
|
||||
)}
|
||||
{guideStep === 3 && (
|
||||
<Button className="database-login-primary" onClick={onAutoGetKey}>
|
||||
{isIntelMac ? '开始获取密钥' : '开始准备连接组件'}
|
||||
{isMac ? '开始获取密钥' : '开始准备连接组件'}
|
||||
</Button>
|
||||
)}
|
||||
{guideStep === 4 && (
|
||||
<Button className="database-login-primary" disabled>
|
||||
正在准备连接组件…
|
||||
{isMac ? '正在获取密钥…' : '正在准备连接组件…'}
|
||||
</Button>
|
||||
)}
|
||||
{guideStep === 5 && (
|
||||
@@ -491,7 +492,7 @@ export function DatabaseConnectionPage({
|
||||
<>
|
||||
{isIntelMac
|
||||
? 'Intel Mac 首次使用需要先准备连接环境,按页面提示完成即可。'
|
||||
: 'macOS 首次获取密钥需要关闭 SIP。'}{' '}
|
||||
: 'macOS Apple 芯片首次获取密钥需要关闭 SIP,并在监听期间点击微信登录。'}{' '}
|
||||
<a href={isIntelMac ? GUIDE_URL : macKeyFaqUrl} target="_blank" rel="noreferrer">
|
||||
查看说明
|
||||
</a>
|
||||
|
||||
@@ -2,7 +2,14 @@ import type { DatabaseKeyState } from './types'
|
||||
import { runtimePlatform } from '../../../utils/runtime-environment'
|
||||
import { Button } from '../../../components/ui'
|
||||
|
||||
const PHASES = ['查找微信进程', '识别微信版本', '扫描候选密钥', '验证数据库', '获取完成']
|
||||
const DEFAULT_PHASES = ['查找微信进程', '识别微信版本', '扫描候选密钥', '验证数据库', '获取完成']
|
||||
const MAC_LOGIN_PHASES = [
|
||||
'查找微信进程',
|
||||
'等待管理员授权',
|
||||
'监听登录密钥',
|
||||
'验证数据库',
|
||||
'获取完成'
|
||||
]
|
||||
|
||||
export function DatabaseKeyAutoDetect({
|
||||
state,
|
||||
@@ -17,8 +24,12 @@ export function DatabaseKeyAutoDetect({
|
||||
}): React.ReactElement {
|
||||
const environment = state.environment
|
||||
const platform = environment?.platform || runtimePlatform
|
||||
const isMac = platform === 'darwin'
|
||||
const isIntelMac = platform === 'darwin' && environment?.architecture === 'x64'
|
||||
if (platform !== 'win32' && !isIntelMac) {
|
||||
const isAppleSiliconMac = isMac && !isIntelMac
|
||||
const supported = environment?.autoDetectSupported ?? (platform === 'win32' || isMac)
|
||||
const phases = isAppleSiliconMac ? MAC_LOGIN_PHASES : DEFAULT_PHASES
|
||||
if (!supported) {
|
||||
return (
|
||||
<section className="settings-card database-key-auto database-key-auto-manual">
|
||||
<div>
|
||||
@@ -32,11 +43,15 @@ export function DatabaseKeyAutoDetect({
|
||||
<section className="settings-card database-key-auto">
|
||||
<div className="database-key-auto-heading">
|
||||
<div>
|
||||
<strong>{isIntelMac ? 'Intel Mac 自动获取' : 'Windows 自动获取'}</strong>
|
||||
<strong>
|
||||
{isIntelMac ? 'Intel Mac 自动获取' : isMac ? 'macOS 自动获取' : 'Windows 自动获取'}
|
||||
</strong>
|
||||
<p>
|
||||
{isIntelMac
|
||||
? '请让微信停在未登录界面,按页面提示操作即可。'
|
||||
: 'TraceMemo 可在微信桌面端正在运行时,通过本机内存扫描尝试获取数据库密钥。'}
|
||||
: isAppleSiliconMac
|
||||
? '执行时会请求管理员授权;授权后在微信登录界面点击“登录”即可,已有登录凭据时通常不需要扫码。监听最长两分钟,结束后会明确显示结果。'
|
||||
: 'TraceMemo 可在微信桌面端正在运行时,通过本机内存扫描尝试获取数据库密钥。'}
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="outline" onClick={onDetect} disabled={disabled}>
|
||||
@@ -50,9 +65,7 @@ export function DatabaseKeyAutoDetect({
|
||||
<li className={environment?.accountIdentified ? 'ok' : ''}>
|
||||
当前账号:{environment?.accountIdentified ? '已识别' : '尚未识别'}
|
||||
</li>
|
||||
<li className={platform === 'win32' || isIntelMac ? 'ok' : ''}>
|
||||
当前平台:{platform === 'win32' || isIntelMac ? '支持' : '不支持'}
|
||||
</li>
|
||||
<li className={supported ? 'ok' : ''}>当前平台:{supported ? '支持' : '不支持'}</li>
|
||||
{isIntelMac && (
|
||||
<>
|
||||
<li className={environment?.pythonAvailable && environment?.fridaAvailable ? 'ok' : ''}>
|
||||
@@ -66,7 +79,7 @@ export function DatabaseKeyAutoDetect({
|
||||
</ul>
|
||||
{state.status === 'auto-detecting' && (
|
||||
<ol className="database-key-phases">
|
||||
{PHASES.map((phase, index) => (
|
||||
{phases.map((phase, index) => (
|
||||
<li key={phase} className={state.autoPhase >= index + 1 ? 'active' : ''}>
|
||||
{phase}
|
||||
</li>
|
||||
@@ -80,7 +93,9 @@ export function DatabaseKeyAutoDetect({
|
||||
<p>
|
||||
{isIntelMac
|
||||
? '请让微信回到未登录界面,重新开始后按页面提示操作。'
|
||||
: '请保持微信正在运行,登录目标账号并打开几个聊天窗口后重试。'}
|
||||
: isAppleSiliconMac
|
||||
? '请先让微信停留在登录界面,点击自动获取并完成管理员授权,然后点击微信“登录”。'
|
||||
: '请保持微信正在运行,登录目标账号并打开几个聊天窗口后重试。'}
|
||||
</p>
|
||||
<Button
|
||||
variant="link"
|
||||
|
||||
@@ -155,15 +155,22 @@ export function useDatabaseKeyController({
|
||||
|
||||
const autoDetectKey = useCallback(async (): Promise<void> => {
|
||||
dispatch({ type: 'AUTO_START' })
|
||||
await refreshEnvironment()
|
||||
const result = await window.api.autoGetDbKey(selfInfo?.accountRoot || '', { save: false })
|
||||
if (!result.success || !result.key) {
|
||||
dispatch({ type: 'AUTO_ERROR', error: result.error || '暂未找到有效密钥' })
|
||||
return
|
||||
try {
|
||||
await refreshEnvironment()
|
||||
const result = await window.api.autoGetDbKey(selfInfo?.accountRoot || '', { save: false })
|
||||
if (!result.success || !result.key) {
|
||||
dispatch({ type: 'AUTO_ERROR', error: result.error || '暂未找到有效密钥' })
|
||||
return
|
||||
}
|
||||
onDbKeyChange(result.key)
|
||||
dispatch({ type: 'AUTO_SUCCESS' })
|
||||
await runValidation(result.key)
|
||||
} catch {
|
||||
dispatch({
|
||||
type: 'AUTO_ERROR',
|
||||
error: '自动获取密钥未完成,请确认微信仍在运行后重试。'
|
||||
})
|
||||
}
|
||||
onDbKeyChange(result.key)
|
||||
dispatch({ type: 'AUTO_SUCCESS' })
|
||||
await runValidation(result.key)
|
||||
}, [onDbKeyChange, refreshEnvironment, runValidation, selfInfo?.accountRoot])
|
||||
|
||||
const clearSavedKey = useCallback(async (): Promise<void> => {
|
||||
|
||||
@@ -8,8 +8,8 @@ export const isDatabaseKeyFormatValid = (value: string): boolean =>
|
||||
export function mapAutoDetectPhase(message: string): number {
|
||||
if (/完成|成功|已获取/.test(message)) return 5
|
||||
if (/验证|校验/.test(message)) return 4
|
||||
if (/扫描|候选|获取/.test(message)) return 3
|
||||
if (/版本|组件|窗口/.test(message)) return 2
|
||||
if (/登录|监听|扫描|候选|获取/.test(message)) return 3
|
||||
if (/授权|版本|组件|窗口/.test(message)) return 2
|
||||
return 1
|
||||
}
|
||||
|
||||
|
||||
@@ -78,6 +78,43 @@ function renderPage(
|
||||
}
|
||||
|
||||
describe('DatabaseConnectionPage', () => {
|
||||
it('guides Apple Silicon users to log in while key capture is active', async () => {
|
||||
const onAutoGetKey = vi.fn()
|
||||
const { props, rerender } = renderPage({
|
||||
platform: 'darwin',
|
||||
mode: 'automatic',
|
||||
guideStep: 3,
|
||||
onAutoGetKey,
|
||||
environment: {
|
||||
platform: 'darwin',
|
||||
architecture: 'arm64',
|
||||
osVersion: 'macOS fixture',
|
||||
appVersion: 'v2.3.0',
|
||||
wechatVersion: '4.1.13',
|
||||
dataStructureVersion: '微信 4.x(WCDB)',
|
||||
dataDirectoryDetected: true,
|
||||
diagnosticSummary: 'fixture',
|
||||
autoDetectSupported: true,
|
||||
wechatRunning: true,
|
||||
accountIdentified: true,
|
||||
dbConnected: false,
|
||||
encryptionAvailable: true
|
||||
}
|
||||
})
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: '开始获取密钥' }))
|
||||
expect(onAutoGetKey).toHaveBeenCalledOnce()
|
||||
|
||||
rerender(
|
||||
<TooltipProvider>
|
||||
<DatabaseConnectionPage {...props} mode="automatic" guideStep={4} isFetching />
|
||||
</TooltipProvider>
|
||||
)
|
||||
expect(screen.getByText('正在监听 macOS 登录密钥')).toBeVisible()
|
||||
expect(screen.getByText(/立即回到微信点击登录/)).toBeVisible()
|
||||
expect(screen.getByRole('button', { name: '正在获取密钥…' })).toBeDisabled()
|
||||
})
|
||||
|
||||
it('uses the automatic Intel Mac flow without exposing implementation details', () => {
|
||||
renderPage({
|
||||
platform: 'darwin',
|
||||
|
||||
@@ -31,6 +31,34 @@ const state: DatabaseKeyState = {
|
||||
}
|
||||
|
||||
describe('database key controls', () => {
|
||||
it('offers login-time key capture on Apple Silicon macOS', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onDetect = vi.fn()
|
||||
render(
|
||||
<DatabaseKeyAutoDetect
|
||||
state={{
|
||||
...state,
|
||||
environment: {
|
||||
...state.environment!,
|
||||
platform: 'darwin',
|
||||
architecture: 'arm64',
|
||||
osVersion: 'macOS fixture',
|
||||
wechatVersion: '4.1.13',
|
||||
autoDetectSupported: true
|
||||
}
|
||||
}}
|
||||
disabled={false}
|
||||
onDetect={onDetect}
|
||||
onRefresh={vi.fn()}
|
||||
/>
|
||||
)
|
||||
|
||||
expect(screen.getByText('macOS 自动获取')).toBeVisible()
|
||||
expect(screen.getByText(/通常不需要扫码/)).toBeVisible()
|
||||
await user.click(screen.getByRole('button', { name: '自动获取密钥' }))
|
||||
expect(onDetect).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('offers automatic key detection on Intel Mac without exposing implementation details', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onDetect = vi.fn()
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
app: {
|
||||
getPath: vi.fn(() => '/tmp/tracememo-key-service-test')
|
||||
}
|
||||
}))
|
||||
|
||||
import {
|
||||
buildAppleSiliconXkeyInvocation,
|
||||
buildXkeyHelperArguments,
|
||||
mapXkeyHelperFailure,
|
||||
parseXkeyHelperOutput,
|
||||
resolveXkeyHelperMode
|
||||
} from '../../src/main/key-service-mac'
|
||||
|
||||
describe('parseXkeyHelperOutput', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('returns a valid helper key without exposing progress output', () => {
|
||||
const key = 'ab'.repeat(32)
|
||||
expect(
|
||||
parseXkeyHelperOutput(`progress line\n${JSON.stringify({ success: true, key })}`)
|
||||
).toEqual({ success: true, key })
|
||||
})
|
||||
|
||||
it('maps a login-time capture timeout to an actionable message', () => {
|
||||
const result = parseXkeyHelperOutput(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
result:
|
||||
'sink found at 0x1234\nsoftware breakpoint unavailable\nhardware breakpoint armed\ntimeout waiting for breakpoint hit'
|
||||
})
|
||||
)
|
||||
|
||||
expect(result).toEqual({
|
||||
success: false,
|
||||
code: 'CAPTURE_TIMEOUT',
|
||||
error:
|
||||
'已完成管理员授权,但监听期间微信没有触发账号密钥派生。请先停留在微信登录界面,在 TraceMemo 点击“自动获取密钥”,授权后点击微信“登录”;已有登录凭据时通常不需要扫码。'
|
||||
})
|
||||
})
|
||||
|
||||
it('does not expose raw helper diagnostics for an unknown failure', () => {
|
||||
const result = parseXkeyHelperOutput(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
result: 'private diagnostic /Users/example/source/xkey.mm:123'
|
||||
})
|
||||
)
|
||||
|
||||
expect(result).toEqual({
|
||||
success: false,
|
||||
code: 'HELPER_RESULT_INVALID',
|
||||
error: '密钥工具未返回有效密钥,请确认微信仍在运行后重试。'
|
||||
})
|
||||
expect(result.error).not.toContain('/Users/example')
|
||||
})
|
||||
|
||||
it('sanitizes diagnostics returned through an AppleScript command error', () => {
|
||||
const result = mapXkeyHelperFailure(
|
||||
'sink found at 0x1234\nhardware breakpoint armed on 81/81 threads\nWAIT_FAILED:no_breakpoint_hit',
|
||||
'OSASCRIPT_1'
|
||||
)
|
||||
|
||||
expect(result).toEqual({
|
||||
success: false,
|
||||
code: 'CAPTURE_TIMEOUT',
|
||||
error:
|
||||
'已完成管理员授权,但监听期间微信没有触发账号密钥派生。请先停留在微信登录界面,在 TraceMemo 点击“自动获取密钥”,授权后点击微信“登录”;已有登录凭据时通常不需要扫码。'
|
||||
})
|
||||
})
|
||||
|
||||
it.each([
|
||||
['4.1.8', 'legacy'],
|
||||
['4.1.9.57', 'legacy'],
|
||||
['4.1.10', 'legacy'],
|
||||
['4.1.13', 'wechat-4.1.13'],
|
||||
['4.1.13.91', 'wechat-4.1.13'],
|
||||
['4.1.130', 'legacy'],
|
||||
['未检测到', 'legacy']
|
||||
])('selects the helper mode for WeChat %s', (version, mode) => {
|
||||
expect(resolveXkeyHelperMode(version)).toBe(mode)
|
||||
})
|
||||
|
||||
it('builds the verified 4.1.13 account-key capture contract', () => {
|
||||
expect(buildXkeyHelperArguments(60037, 120_000)).toEqual([
|
||||
'60037',
|
||||
'120000',
|
||||
'--profile',
|
||||
'wechat-4.1.13',
|
||||
'--account'
|
||||
])
|
||||
})
|
||||
|
||||
it('uses the new helper only for WeChat 4.1.13', () => {
|
||||
expect(buildAppleSiliconXkeyInvocation('4.1.13.91', 60037, 60_000)).toEqual({
|
||||
mode: 'wechat-4.1.13',
|
||||
resourceName: 'xkey_helper_4_1_13',
|
||||
args: ['60037', '120000', '--profile', 'wechat-4.1.13', '--account'],
|
||||
waitMs: 120_000,
|
||||
timeoutSeconds: 130,
|
||||
execTimeoutMs: 135_000
|
||||
})
|
||||
})
|
||||
|
||||
it.each(['4.1.8', '4.1.9.57', '4.1.10', '未检测到'])(
|
||||
'preserves the legacy helper invocation for WeChat %s',
|
||||
(version) => {
|
||||
expect(buildAppleSiliconXkeyInvocation(version, 60037, 60_000)).toEqual({
|
||||
mode: 'legacy',
|
||||
resourceName: 'xkey_helper',
|
||||
args: ['60037', '60000'],
|
||||
waitMs: 60_000,
|
||||
timeoutSeconds: 90,
|
||||
execTimeoutMs: 80_000
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
it('extracts a helper error embedded in AppleScript diagnostics', () => {
|
||||
expect(
|
||||
mapXkeyHelperFailure(
|
||||
'hardware breakpoint armed\n{"success":false,"result":"ERROR:HOOK_FAILED:ptrace"}',
|
||||
'OSASCRIPT_1'
|
||||
)
|
||||
).toEqual({
|
||||
success: false,
|
||||
code: 'HOOK_FAILED',
|
||||
error: '密钥工具执行未完成(HOOK_FAILED),请确认微信仍在运行后重试。'
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -25,6 +25,8 @@ describe('database key validation', () => {
|
||||
|
||||
it('maps automatic detection progress into stable phases', () => {
|
||||
expect(mapAutoDetectPhase('正在查找微信进程')).toBeGreaterThan(0)
|
||||
expect(mapAutoDetectPhase('正在请求管理员授权')).toBe(2)
|
||||
expect(mapAutoDetectPhase('授权后请在微信登录界面点击“登录”')).toBe(3)
|
||||
expect(mapAutoDetectPhase('已获取数据库密钥')).toBe(5)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user