mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-10-04 03:01:42 +08:00
feat: 支持Mac Intel获取密钥, 提供完整支持
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
## 档案与搜索
|
||||
|
||||
- [第一次使用](./user-guide/getting-started.md):安装、连接微信并完成第一次搜索。
|
||||
- [Intel Mac 获取微信密钥](./user-guide/intel-mac-key.md):按页面检查结果准备环境并获取密钥。
|
||||
- [聊天档案与搜索](./user-guide/chat-archive.md):浏览联系人和群聊,按关键词、备注、昵称、微信号或 wxid 查找消息;也包含档案中的文字转语音入口。
|
||||
|
||||
## AI 与知识库
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# Intel Mac 首次获取密钥
|
||||
|
||||
请先备份微信本地数据。密钥获取和数据库验证都在本机完成,不要把密钥、日志或数据库发给他人。
|
||||
|
||||
## 开始使用
|
||||
|
||||
1. 启动 TraceMemo,在首次连接页面点击“重新检查环境”。
|
||||
2. 如果页面显示需要准备连接环境,点击“准备环境”,按页面提示完成。
|
||||
3. 打开微信,让微信停在未登录页面,不要先点击登录。
|
||||
4. 回到 TraceMemo,选择微信账号,点击“开始获取密钥”。
|
||||
5. 等待页面提示可以登录后,立即回到微信点击登录。
|
||||
6. 等待 TraceMemo 显示“获取成功,验证数据库连接”。
|
||||
|
||||
获取成功后,请保存密钥。如果页面提示需要重新准备,按页面的“查看说明”操作,不要连续重试。
|
||||
|
||||
## 注意
|
||||
|
||||
- 密钥只保存在本机的安全存储中。
|
||||
- 请不要删除微信本地数据目录。
|
||||
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
+14
-3
@@ -885,18 +885,22 @@ app.whenReady().then(async () => {
|
||||
)
|
||||
const self = chat.getSelfAccountInfo()
|
||||
const settings = loadSettings()
|
||||
const intelMac = process.platform === 'darwin' && process.arch === 'x64'
|
||||
const intelEnvironment = intelMac ? await keyServiceMac.getIntelEnvironmentStatus() : undefined
|
||||
const environment = {
|
||||
platform: process.platform,
|
||||
architecture: process.arch,
|
||||
osVersion: getOsVersionLabel(),
|
||||
appVersion: `v${app.getVersion()}`,
|
||||
wechatVersion: await detectWechatVersion(),
|
||||
dataStructureVersion: detectDataStructureVersion(settings.dbRoot),
|
||||
dataDirectoryDetected: validateDbRoot(settings.dbRoot).valid,
|
||||
autoDetectSupported: process.platform === 'win32',
|
||||
autoDetectSupported: process.platform === 'win32' || intelMac,
|
||||
wechatRunning: await isWechatRunning(),
|
||||
accountIdentified: Boolean(self?.wxid),
|
||||
dbConnected: chat.isReady(),
|
||||
encryptionAvailable: storage.encryptionAvailable
|
||||
encryptionAvailable: storage.encryptionAvailable,
|
||||
...(intelEnvironment || {})
|
||||
}
|
||||
return { ...environment, diagnosticSummary: buildSafeDiagnosticSummary(environment) }
|
||||
})
|
||||
@@ -909,6 +913,13 @@ app.whenReady().then(async () => {
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('key:installIntelMacRuntime', async (event) => {
|
||||
const onStatus = (message: string): void => {
|
||||
if (!event.sender.isDestroyed()) event.sender.send('key:dbKeyStatus', { message })
|
||||
}
|
||||
return keyServiceMac.installIntelKeyRuntime(onStatus)
|
||||
})
|
||||
|
||||
ipcMain.handle('key:pasteAndSaveDbKey', async (_, accountRoot: string) => {
|
||||
const clipboardKey = clipboard.readText().trim()
|
||||
return databaseKeyStore.save(String(accountRoot || ''), clipboardKey)
|
||||
@@ -931,7 +942,7 @@ app.whenReady().then(async () => {
|
||||
const result =
|
||||
process.platform === 'win32'
|
||||
? await keyServiceWin.autoGetDbKey(60_000, onStatus)
|
||||
: await keyServiceMac.autoGetDbKey(onStatus)
|
||||
: await keyServiceMac.autoGetDbKey(onStatus, 60_000, accountRoot)
|
||||
if (!result.success || !result.key) return result
|
||||
|
||||
const selectedRoot = String(accountRoot || '').trim()
|
||||
|
||||
+173
-3
@@ -1,5 +1,5 @@
|
||||
import { app } from 'electron'
|
||||
import { execFile } from 'child_process'
|
||||
import { execFile, spawn } from 'child_process'
|
||||
import fs from 'fs-extra'
|
||||
import path from 'path'
|
||||
import { promisify } from 'util'
|
||||
@@ -25,6 +25,164 @@ export interface ImageKeyResult {
|
||||
}
|
||||
|
||||
export class KeyServiceMac {
|
||||
private getMacKeyRuntimeDir(): string {
|
||||
return path.join(app.getPath('userData'), 'key-runtime')
|
||||
}
|
||||
|
||||
private async runMacKeyTool(
|
||||
args: string[],
|
||||
timeoutMs: number,
|
||||
onStatus?: (message: string) => void
|
||||
): Promise<Record<string, unknown>> {
|
||||
const helperPath = findResource('macos-key-tool/intel_mac_key_helper')
|
||||
if (!helperPath) {
|
||||
return {
|
||||
type: 'result',
|
||||
success: false,
|
||||
code: 'KEY_TOOL_UNAVAILABLE',
|
||||
error: '缺少 Intel Mac 工具'
|
||||
}
|
||||
}
|
||||
|
||||
return await new Promise<Record<string, unknown>>((resolve) => {
|
||||
const child = spawn(helperPath, args, {
|
||||
stdio: ['ignore', 'pipe', 'pipe']
|
||||
})
|
||||
let settled = false
|
||||
let pending = ''
|
||||
let lastError = ''
|
||||
let finalPayload: Record<string, unknown> | null = null
|
||||
let timer: ReturnType<typeof setTimeout> | undefined
|
||||
const finish = (payload: Record<string, unknown>): void => {
|
||||
if (settled) return
|
||||
settled = true
|
||||
if (timer) clearTimeout(timer)
|
||||
resolve(payload)
|
||||
}
|
||||
const consume = (chunk: Buffer): void => {
|
||||
pending += chunk.toString()
|
||||
const lines = pending.split(/\r?\n/)
|
||||
pending = lines.pop() || ''
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const payload = JSON.parse(line) as Record<string, unknown>
|
||||
if (payload.type === 'progress' && typeof payload.message === 'string') {
|
||||
onStatus?.(payload.message)
|
||||
}
|
||||
if (payload.type === 'result' || payload.type === 'status') finalPayload = payload
|
||||
} catch {
|
||||
// The helper contract is JSONL; ignore interpreter diagnostics.
|
||||
}
|
||||
}
|
||||
}
|
||||
child.stdout.on('data', consume)
|
||||
child.stderr.on('data', (chunk: Buffer) => {
|
||||
lastError = `${lastError}\n${chunk.toString()}`.trim().slice(-1000)
|
||||
})
|
||||
child.on('error', (error) =>
|
||||
finish({
|
||||
type: 'result',
|
||||
success: false,
|
||||
code: 'KEY_TOOL_FAILED',
|
||||
error: `无法启动 Intel Mac 密钥工具:${error.message}`
|
||||
})
|
||||
)
|
||||
child.on('close', () => {
|
||||
if (pending) consume(Buffer.from('\n'))
|
||||
finish(
|
||||
finalPayload || {
|
||||
type: 'result',
|
||||
success: false,
|
||||
code: 'KEY_TOOL_FAILED',
|
||||
error: lastError || 'Intel Mac 密钥工具未返回结果'
|
||||
}
|
||||
)
|
||||
})
|
||||
timer = setTimeout(() => {
|
||||
try {
|
||||
child.kill('SIGTERM')
|
||||
} catch {
|
||||
// The child may already have exited.
|
||||
}
|
||||
finish({
|
||||
type: 'result',
|
||||
success: false,
|
||||
code: 'KEY_TOOL_TIMEOUT',
|
||||
error: 'Intel Mac 密钥获取超时,请让微信回到未登录界面后重试'
|
||||
})
|
||||
}, timeoutMs)
|
||||
})
|
||||
}
|
||||
|
||||
async getIntelEnvironmentStatus(): Promise<{
|
||||
sipDisabled: boolean
|
||||
pythonAvailable: boolean
|
||||
fridaAvailable: boolean
|
||||
wechatAdhocSigned: boolean
|
||||
}> {
|
||||
const payload = await this.runMacKeyTool(
|
||||
['status', '--runtime-dir', this.getMacKeyRuntimeDir()],
|
||||
15_000
|
||||
)
|
||||
return {
|
||||
sipDisabled: payload.sipDisabled === true,
|
||||
pythonAvailable: payload.pythonAvailable === true,
|
||||
fridaAvailable: payload.fridaAvailable === true,
|
||||
wechatAdhocSigned: payload.wechatAdhocSigned === true
|
||||
}
|
||||
}
|
||||
|
||||
async installIntelKeyRuntime(
|
||||
onStatus?: (message: string) => void
|
||||
): Promise<{ success: boolean; error?: string; code?: string }> {
|
||||
if (process.platform !== 'darwin' || process.arch !== 'x64') {
|
||||
return { success: false, code: 'UNSUPPORTED_PLATFORM', error: '仅 Intel Mac 需要安装此运行环境' }
|
||||
}
|
||||
onStatus?.('正在准备连接环境,请保持网络连接…')
|
||||
const payload = await this.runMacKeyTool(
|
||||
['install', this.getMacKeyRuntimeDir()],
|
||||
5 * 60_000,
|
||||
onStatus
|
||||
)
|
||||
return {
|
||||
success: payload.success === true,
|
||||
code: typeof payload.code === 'string' ? payload.code : undefined,
|
||||
error: typeof payload.error === 'string' ? payload.error : undefined
|
||||
}
|
||||
}
|
||||
|
||||
private async captureIntelDbKey(
|
||||
accountRoot: string | undefined,
|
||||
timeoutMs: number,
|
||||
onStatus?: (message: string) => void
|
||||
): Promise<DatabaseKeyResult> {
|
||||
const selectedRoot = String(accountRoot || '').trim()
|
||||
if (!selectedRoot) return { success: false, code: 'ACCOUNT_REQUIRED', error: '请先选择微信账号' }
|
||||
const waitMs = Math.max(30_000, timeoutMs)
|
||||
const payload = await this.runMacKeyTool(
|
||||
[
|
||||
'capture',
|
||||
'--account-root',
|
||||
selectedRoot,
|
||||
'--runtime-dir',
|
||||
this.getMacKeyRuntimeDir(),
|
||||
'--diagnostic-log',
|
||||
path.join(app.getPath('logs'), 'mac-key-diagnostic.log'),
|
||||
'--timeout-ms',
|
||||
String(waitMs)
|
||||
],
|
||||
waitMs + 15_000,
|
||||
onStatus
|
||||
)
|
||||
const key = typeof payload.key === 'string' ? payload.key.trim().toLowerCase() : ''
|
||||
if (payload.success === true && isValidDatabaseKey(key)) return { success: true, key }
|
||||
return {
|
||||
success: false,
|
||||
code: typeof payload.code === 'string' ? payload.code : 'KEY_TOOL_FAILED',
|
||||
error: typeof payload.error === 'string' ? payload.error : '未捕获到微信数据库密钥'
|
||||
}
|
||||
}
|
||||
|
||||
private getHelperPath(): string {
|
||||
const helperPath = findResource('xkey_helper')
|
||||
if (!helperPath) {
|
||||
@@ -109,19 +267,31 @@ export class KeyServiceMac {
|
||||
|
||||
async autoGetDbKey(
|
||||
onStatus?: (message: string) => void,
|
||||
timeoutMs = 60_000
|
||||
timeoutMs = 60_000,
|
||||
accountRoot?: string
|
||||
): Promise<DatabaseKeyResult> {
|
||||
if (onStatus) {
|
||||
const emitStatus = onStatus
|
||||
onStatus = (message: string): void =>
|
||||
emitStatus(message.replace(/Frida/gi, '连接组件'))
|
||||
}
|
||||
if (process.platform !== 'darwin') {
|
||||
return { success: false, error: '自动获取密钥目前仅支持 macOS' }
|
||||
}
|
||||
if (await this.isSipEnabled()) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'macOS 系统完整性保护(SIP)已开启,自动获取不可用,请使用手动粘贴。'
|
||||
error: '当前系统还未完成连接环境准备,请按页面提示完成设置。'
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (process.arch === 'x64') {
|
||||
onStatus?.('Intel Mac 将通过 Frida 获取微信主密钥,请让微信停留在未登录界面')
|
||||
const result = await this.captureIntelDbKey(accountRoot, timeoutMs, onStatus)
|
||||
onStatus?.(result.success ? '密钥获取成功' : '密钥获取失败')
|
||||
return result
|
||||
}
|
||||
onStatus?.('正在查找微信进程...')
|
||||
const pid = await this.getWeChatPid()
|
||||
const helperPath = this.getHelperPath()
|
||||
|
||||
Vendored
+5
@@ -462,6 +462,11 @@ declare global {
|
||||
value?: string
|
||||
error?: string
|
||||
}>
|
||||
installIntelMacKeyRuntime: () => Promise<{
|
||||
success: boolean
|
||||
error?: string
|
||||
code?: string
|
||||
}>
|
||||
autoGetDbKey: (
|
||||
accountRoot: string,
|
||||
options?: { save?: boolean }
|
||||
|
||||
@@ -377,6 +377,7 @@ const api = {
|
||||
getSavedDbKey: (accountRoot: string) => ipcRenderer.invoke('key:getSavedDbKey', accountRoot),
|
||||
getDatabaseKeyEnvironment: () => ipcRenderer.invoke('key:getEnvironment'),
|
||||
readDatabaseKeyClipboard: () => ipcRenderer.invoke('key:readClipboardDbKey'),
|
||||
installIntelMacKeyRuntime: () => ipcRenderer.invoke('key:installIntelMacRuntime'),
|
||||
autoGetDbKey: (accountRoot: string, options?: { save?: boolean }) =>
|
||||
ipcRenderer.invoke('key:autoGetDbKey', accountRoot, options),
|
||||
autoGetImageKey: (options?: { save?: boolean }) =>
|
||||
|
||||
@@ -1118,6 +1118,25 @@ function App(): React.ReactElement {
|
||||
}
|
||||
}
|
||||
|
||||
const handleInstallIntelKeyRuntime = async (): Promise<void> => {
|
||||
if (isFetchingDbKey) return
|
||||
setIsFetchingDbKey(true)
|
||||
setDbKeyStatus('正在准备连接环境,请保持网络连接…')
|
||||
setDbKeyStatusKind('normal')
|
||||
try {
|
||||
const result = await window.api.installIntelMacKeyRuntime()
|
||||
if (!result.success) throw new Error(result.error || '连接环境准备失败')
|
||||
await refreshConnectionEnvironment()
|
||||
setDbKeyStatus('连接环境已准备,请继续按页面步骤获取密钥')
|
||||
setDbKeyStatusKind('success')
|
||||
} catch (error) {
|
||||
setDbKeyStatus(error instanceof Error ? error.message : String(error))
|
||||
setDbKeyStatusKind('error')
|
||||
} finally {
|
||||
setIsFetchingDbKey(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handlePasteAndSaveDbKey = async (): Promise<void> => {
|
||||
setShowMacKeyFaq(false)
|
||||
if (!selectedAccount) {
|
||||
@@ -2266,6 +2285,7 @@ function App(): React.ReactElement {
|
||||
}}
|
||||
onToggleDbKey={() => setShowDbKey((visible) => !visible)}
|
||||
onAutoGetKey={handleAutoGetDbKey}
|
||||
onInstallIntelKeyRuntime={handleInstallIntelKeyRuntime}
|
||||
onRefreshEnvironment={() => void refreshConnectionEnvironment()}
|
||||
onGuideNext={() =>
|
||||
setConnectionGuideStep((current) => (current === 1 ? 2 : current === 2 ? 3 : current))
|
||||
|
||||
@@ -31,6 +31,7 @@ interface DatabaseConnectionPageProps {
|
||||
onSelectDbRoot: () => void
|
||||
onToggleDbKey: () => void
|
||||
onAutoGetKey: () => void
|
||||
onInstallIntelKeyRuntime: () => void
|
||||
onRefreshEnvironment: () => void
|
||||
onGuideNext: () => void
|
||||
onGuideBack: () => void
|
||||
@@ -123,6 +124,7 @@ export function DatabaseConnectionPage({
|
||||
onSelectDbRoot,
|
||||
onToggleDbKey,
|
||||
onAutoGetKey,
|
||||
onInstallIntelKeyRuntime,
|
||||
onRefreshEnvironment,
|
||||
onGuideNext,
|
||||
onGuideBack,
|
||||
@@ -134,6 +136,7 @@ export function DatabaseConnectionPage({
|
||||
onClearKey
|
||||
}: DatabaseConnectionPageProps): React.ReactElement {
|
||||
const isMac = platform === 'darwin'
|
||||
const isIntelMac = isMac && environment?.architecture === 'x64'
|
||||
const defaultPath = isMac
|
||||
? '~/Library/Containers/com.tencent.xinWeChat/Data/Library/Application Support/com.tencent.xinWeChat/'
|
||||
: 'C:\\Users\\...\\WeChat Files\\Msg'
|
||||
@@ -186,14 +189,18 @@ export function DatabaseConnectionPage({
|
||||
<li>
|
||||
<span>2</span>
|
||||
<div>
|
||||
<strong>准备连接组件</strong>
|
||||
<small>页面会按当前系统给出对应步骤</small>
|
||||
<strong>{isIntelMac ? '让微信停在登录页面' : '准备连接组件'}</strong>
|
||||
<small>
|
||||
{isIntelMac
|
||||
? '启动微信,但暂时不要点击登录'
|
||||
: '页面会按当前系统给出对应步骤'}
|
||||
</small>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<span>3</span>
|
||||
<div>
|
||||
<strong>登录并验证连接</strong>
|
||||
<strong>{isIntelMac ? '获取密钥并验证连接' : '登录并验证连接'}</strong>
|
||||
<small>验证通过后进入主界面</small>
|
||||
</div>
|
||||
</li>
|
||||
@@ -241,9 +248,13 @@ export function DatabaseConnectionPage({
|
||||
: [
|
||||
'检查本机环境',
|
||||
'让微信停在登录页面',
|
||||
'确认开始准备',
|
||||
`正在完成 ${isMac ? 'macOS' : 'Windows'} 授权`,
|
||||
'获取成功, 现在可以重新登录微信了',
|
||||
isIntelMac ? '确认开始获取密钥' : '确认开始准备',
|
||||
isIntelMac
|
||||
? '正在获取 Intel Mac 密钥'
|
||||
: `正在完成 ${isMac ? 'macOS' : 'Windows'} 授权`,
|
||||
isIntelMac
|
||||
? '获取成功,验证数据库连接'
|
||||
: '获取成功, 现在可以重新登录微信了',
|
||||
'验证数据库连接'
|
||||
][guideStep - 1]}
|
||||
</strong>
|
||||
@@ -253,10 +264,16 @@ export function DatabaseConnectionPage({
|
||||
: status ||
|
||||
[
|
||||
'确认下方检测结果;没有找到目录时可以手动选择。',
|
||||
'请退出当前微信账号,让微信停留在登录页面,然后点击“我已准备好”。',
|
||||
'开始后请按页面提示完成系统授权。',
|
||||
'正在准备连接组件,请不要关闭微信或 TraceMemo。',
|
||||
'请回到微信完成登录,登录成功后再回来验证。',
|
||||
isIntelMac
|
||||
? '请启动微信并停留在未登录界面,暂时不要点击登录,然后点击“我已准备好”。'
|
||||
: '请退出当前微信账号,让微信停留在登录页面,然后点击“我已准备好”。',
|
||||
isIntelMac ? '确认开始获取密钥。' : '开始后请按页面提示完成系统授权。',
|
||||
isIntelMac
|
||||
? '等页面提示可以登录后,立即回到微信点击登录。'
|
||||
: '正在准备连接组件,请不要关闭微信或 TraceMemo。',
|
||||
isIntelMac
|
||||
? '密钥已获取,请继续验证数据库。'
|
||||
: '请回到微信完成登录,登录成功后再回来验证。',
|
||||
'正在验证密钥和本地数据库,请稍候。'
|
||||
][guideStep - 1]}
|
||||
</p>
|
||||
@@ -314,6 +331,28 @@ export function DatabaseConnectionPage({
|
||||
<dt>微信状态</dt>
|
||||
<dd>{environment?.wechatRunning ? '运行中' : '未检测到'}</dd>
|
||||
</div>
|
||||
{isIntelMac && (
|
||||
<>
|
||||
<div>
|
||||
<dt>连接环境</dt>
|
||||
<dd>
|
||||
{environment?.pythonAvailable && environment?.fridaAvailable
|
||||
? '已准备好'
|
||||
: '需要准备'}
|
||||
{(!environment?.pythonAvailable || !environment?.fridaAvailable) && (
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
onClick={onInstallIntelKeyRuntime}
|
||||
disabled={isFetching}
|
||||
>
|
||||
准备环境
|
||||
</Button>
|
||||
)}
|
||||
</dd>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</dl>
|
||||
{accounts.length > 0 && (
|
||||
<section className="database-account-list" aria-label="选择微信账号">
|
||||
@@ -406,7 +445,7 @@ export function DatabaseConnectionPage({
|
||||
)}
|
||||
{guideStep === 3 && (
|
||||
<Button className="database-login-primary" onClick={onAutoGetKey}>
|
||||
开始准备连接组件
|
||||
{isIntelMac ? '开始获取密钥' : '开始准备连接组件'}
|
||||
</Button>
|
||||
)}
|
||||
{guideStep === 4 && (
|
||||
@@ -450,8 +489,10 @@ export function DatabaseConnectionPage({
|
||||
<p className="database-login-platform-note">
|
||||
{isMac ? (
|
||||
<>
|
||||
macOS 首次获取密钥需要关闭 SIP。{' '}
|
||||
<a href={macKeyFaqUrl} target="_blank" rel="noreferrer">
|
||||
{isIntelMac
|
||||
? 'Intel Mac 首次使用需要先准备连接环境,按页面提示完成即可。'
|
||||
: 'macOS 首次获取密钥需要关闭 SIP。'}{' '}
|
||||
<a href={isIntelMac ? GUIDE_URL : macKeyFaqUrl} target="_blank" rel="noreferrer">
|
||||
查看说明
|
||||
</a>
|
||||
</>
|
||||
|
||||
@@ -17,7 +17,8 @@ export function DatabaseKeyAutoDetect({
|
||||
}): React.ReactElement {
|
||||
const environment = state.environment
|
||||
const platform = environment?.platform || runtimePlatform
|
||||
if (platform !== 'win32') {
|
||||
const isIntelMac = platform === 'darwin' && environment?.architecture === 'x64'
|
||||
if (platform !== 'win32' && !isIntelMac) {
|
||||
return (
|
||||
<section className="settings-card database-key-auto database-key-auto-manual">
|
||||
<div>
|
||||
@@ -31,8 +32,12 @@ export function DatabaseKeyAutoDetect({
|
||||
<section className="settings-card database-key-auto">
|
||||
<div className="database-key-auto-heading">
|
||||
<div>
|
||||
<strong>Windows 自动获取</strong>
|
||||
<p>TraceMemo 可在微信桌面端正在运行时,通过本机内存扫描尝试获取数据库密钥。</p>
|
||||
<strong>{isIntelMac ? 'Intel Mac 自动获取' : 'Windows 自动获取'}</strong>
|
||||
<p>
|
||||
{isIntelMac
|
||||
? '请让微信停在未登录界面,按页面提示操作即可。'
|
||||
: 'TraceMemo 可在微信桌面端正在运行时,通过本机内存扫描尝试获取数据库密钥。'}
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="outline" onClick={onDetect} disabled={disabled}>
|
||||
{state.status === 'auto-detecting' ? '正在获取…' : '自动获取密钥'}
|
||||
@@ -45,9 +50,19 @@ export function DatabaseKeyAutoDetect({
|
||||
<li className={environment?.accountIdentified ? 'ok' : ''}>
|
||||
当前账号:{environment?.accountIdentified ? '已识别' : '尚未识别'}
|
||||
</li>
|
||||
<li className={platform === 'win32' ? 'ok' : ''}>
|
||||
当前平台:{platform === 'win32' ? '支持' : '不支持'}
|
||||
<li className={platform === 'win32' || isIntelMac ? 'ok' : ''}>
|
||||
当前平台:{platform === 'win32' || isIntelMac ? '支持' : '不支持'}
|
||||
</li>
|
||||
{isIntelMac && (
|
||||
<>
|
||||
<li className={environment?.pythonAvailable && environment?.fridaAvailable ? 'ok' : ''}>
|
||||
连接环境:
|
||||
{environment?.pythonAvailable && environment?.fridaAvailable
|
||||
? '已准备好'
|
||||
: '需要准备'}
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
</ul>
|
||||
{state.status === 'auto-detecting' && (
|
||||
<ol className="database-key-phases">
|
||||
@@ -62,7 +77,11 @@ export function DatabaseKeyAutoDetect({
|
||||
<div className="database-key-auto-error">
|
||||
<strong>暂未找到有效密钥</strong>
|
||||
<span>{state.error}</span>
|
||||
<p>请保持微信正在运行,登录目标账号并打开几个聊天窗口后重试。</p>
|
||||
<p>
|
||||
{isIntelMac
|
||||
? '请让微信回到未登录界面,重新开始后按页面提示操作。'
|
||||
: '请保持微信正在运行,登录目标账号并打开几个聊天窗口后重试。'}
|
||||
</p>
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
|
||||
@@ -6,11 +6,15 @@ export function buildSafeDiagnosticSummary(
|
||||
return [
|
||||
`TraceMemo: ${environment.appVersion}`,
|
||||
`操作系统: ${environment.osVersion}`,
|
||||
...(environment.architecture ? [`处理器架构: ${environment.architecture}`] : []),
|
||||
`微信客户端: ${environment.wechatVersion}`,
|
||||
`数据结构: ${environment.dataStructureVersion}`,
|
||||
`数据目录: ${environment.dataDirectoryDetected ? '已检测到' : '未检测到'}`,
|
||||
`微信进程: ${environment.wechatRunning ? '运行中' : '未运行'}`,
|
||||
`数据库连接: ${environment.dbConnected ? '已连接' : '未连接'}`,
|
||||
`安全存储: ${environment.encryptionAvailable ? '可用' : '不可用'}`
|
||||
`安全存储: ${environment.encryptionAvailable ? '可用' : '不可用'}`,
|
||||
...(typeof environment.pythonAvailable === 'boolean' || typeof environment.fridaAvailable === 'boolean'
|
||||
? [`连接环境: ${environment.pythonAvailable && environment.fridaAvailable ? '已准备' : '未准备'}`]
|
||||
: [])
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ export interface AccountDiscoveryResult {
|
||||
|
||||
export interface DatabaseKeyEnvironment {
|
||||
platform: NodeJS.Platform
|
||||
architecture?: string
|
||||
osVersion: string
|
||||
appVersion: string
|
||||
wechatVersion: string
|
||||
@@ -69,4 +70,8 @@ export interface DatabaseKeyEnvironment {
|
||||
accountIdentified: boolean
|
||||
dbConnected: boolean
|
||||
encryptionAvailable: boolean
|
||||
sipDisabled?: boolean
|
||||
pythonAvailable?: boolean
|
||||
fridaAvailable?: boolean
|
||||
wechatAdhocSigned?: boolean
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ function renderPage(
|
||||
onSelectDbRoot: vi.fn(),
|
||||
onToggleDbKey: vi.fn(),
|
||||
onAutoGetKey: vi.fn(),
|
||||
onInstallIntelKeyRuntime: vi.fn(),
|
||||
onRefreshEnvironment: vi.fn(),
|
||||
onGuideNext: vi.fn(),
|
||||
onGuideBack: vi.fn(),
|
||||
@@ -77,6 +78,37 @@ function renderPage(
|
||||
}
|
||||
|
||||
describe('DatabaseConnectionPage', () => {
|
||||
it('uses the automatic Intel Mac flow without exposing implementation details', () => {
|
||||
renderPage({
|
||||
platform: 'darwin',
|
||||
mode: 'automatic',
|
||||
guideStep: 3,
|
||||
environment: {
|
||||
platform: 'darwin',
|
||||
architecture: 'x64',
|
||||
osVersion: 'macOS fixture',
|
||||
appVersion: 'v2.1.6',
|
||||
wechatVersion: '4.1.10',
|
||||
dataStructureVersion: '微信 4.x(WCDB)',
|
||||
dataDirectoryDetected: true,
|
||||
diagnosticSummary: 'fixture',
|
||||
autoDetectSupported: true,
|
||||
wechatRunning: true,
|
||||
accountIdentified: true,
|
||||
dbConnected: false,
|
||||
encryptionAvailable: true,
|
||||
pythonAvailable: true,
|
||||
fridaAvailable: true,
|
||||
wechatAdhocSigned: true,
|
||||
sipDisabled: true
|
||||
}
|
||||
})
|
||||
|
||||
expect(screen.getByRole('button', { name: '开始获取密钥' })).toBeVisible()
|
||||
expect(screen.queryByText(/管理员授权|电脑密码/)).not.toBeInTheDocument()
|
||||
expect(screen.queryByText(/Frida|Python|SIP|重新签名/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders a discovered nickname and avatar before connection', () => {
|
||||
const { container } = renderPage({
|
||||
mode: 'automatic',
|
||||
|
||||
@@ -31,6 +31,36 @@ const state: DatabaseKeyState = {
|
||||
}
|
||||
|
||||
describe('database key controls', () => {
|
||||
it('offers automatic key detection on Intel Mac without exposing implementation details', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onDetect = vi.fn()
|
||||
render(
|
||||
<DatabaseKeyAutoDetect
|
||||
state={{
|
||||
...state,
|
||||
environment: {
|
||||
...state.environment!,
|
||||
platform: 'darwin',
|
||||
architecture: 'x64',
|
||||
pythonAvailable: true,
|
||||
fridaAvailable: true,
|
||||
wechatAdhocSigned: true,
|
||||
sipDisabled: true
|
||||
}
|
||||
}}
|
||||
disabled={false}
|
||||
onDetect={onDetect}
|
||||
onRefresh={vi.fn()}
|
||||
/>
|
||||
)
|
||||
|
||||
expect(screen.getByText('Intel Mac 自动获取')).toBeVisible()
|
||||
expect(screen.getByText(/按页面提示操作即可/)).toBeVisible()
|
||||
expect(screen.queryByText(/Frida|Python|SIP|签名/)).not.toBeInTheDocument()
|
||||
await user.click(screen.getByRole('button', { name: '自动获取密钥' }))
|
||||
expect(onDetect).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('keeps auto-detect and refresh callbacks separate', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onDetect = vi.fn()
|
||||
|
||||
Reference in New Issue
Block a user