diff --git a/docs/README.md b/docs/README.md index d47a66c..5a5f11d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -5,6 +5,7 @@ ## 档案与搜索 - [第一次使用](./user-guide/getting-started.md):安装、连接微信并完成第一次搜索。 +- [Intel Mac 获取微信密钥](./user-guide/intel-mac-key.md):按页面检查结果准备环境并获取密钥。 - [聊天档案与搜索](./user-guide/chat-archive.md):浏览联系人和群聊,按关键词、备注、昵称、微信号或 wxid 查找消息;也包含档案中的文字转语音入口。 ## AI 与知识库 diff --git a/docs/user-guide/intel-mac-key.md b/docs/user-guide/intel-mac-key.md new file mode 100644 index 0000000..788f55a --- /dev/null +++ b/docs/user-guide/intel-mac-key.md @@ -0,0 +1,19 @@ +# Intel Mac 首次获取密钥 + +请先备份微信本地数据。密钥获取和数据库验证都在本机完成,不要把密钥、日志或数据库发给他人。 + +## 开始使用 + +1. 启动 TraceMemo,在首次连接页面点击“重新检查环境”。 +2. 如果页面显示需要准备连接环境,点击“准备环境”,按页面提示完成。 +3. 打开微信,让微信停在未登录页面,不要先点击登录。 +4. 回到 TraceMemo,选择微信账号,点击“开始获取密钥”。 +5. 等待页面提示可以登录后,立即回到微信点击登录。 +6. 等待 TraceMemo 显示“获取成功,验证数据库连接”。 + +获取成功后,请保存密钥。如果页面提示需要重新准备,按页面的“查看说明”操作,不要连续重试。 + +## 注意 + +- 密钥只保存在本机的安全存储中。 +- 请不要删除微信本地数据目录。 diff --git a/resources/macos-key-tool/intel_mac_key_helper b/resources/macos-key-tool/intel_mac_key_helper new file mode 100755 index 0000000..a96572f Binary files /dev/null and b/resources/macos-key-tool/intel_mac_key_helper differ diff --git a/resources/wcdb/macos/arm64/libWCDB.dylib b/resources/wcdb/macos/arm64/libWCDB.dylib new file mode 100755 index 0000000..18f8509 Binary files /dev/null and b/resources/wcdb/macos/arm64/libWCDB.dylib differ diff --git a/resources/wcdb/macos/arm64/libwcdb_api.dylib b/resources/wcdb/macos/arm64/libwcdb_api.dylib new file mode 100755 index 0000000..5f6211f Binary files /dev/null and b/resources/wcdb/macos/arm64/libwcdb_api.dylib differ diff --git a/resources/wcdb/macos/x64/libWCDB.dylib b/resources/wcdb/macos/x64/libWCDB.dylib new file mode 100755 index 0000000..2475a7a Binary files /dev/null and b/resources/wcdb/macos/x64/libWCDB.dylib differ diff --git a/resources/wcdb/macos/x64/libwcdb_api.dylib b/resources/wcdb/macos/x64/libwcdb_api.dylib new file mode 100755 index 0000000..2d482ca Binary files /dev/null and b/resources/wcdb/macos/x64/libwcdb_api.dylib differ diff --git a/src/main/index.ts b/src/main/index.ts index ae458e0..73d5248 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -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() diff --git a/src/main/key-service-mac.ts b/src/main/key-service-mac.ts index 6ba31e8..7a88fa4 100644 --- a/src/main/key-service-mac.ts +++ b/src/main/key-service-mac.ts @@ -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> { + 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>((resolve) => { + const child = spawn(helperPath, args, { + stdio: ['ignore', 'pipe', 'pipe'] + }) + let settled = false + let pending = '' + let lastError = '' + let finalPayload: Record | null = null + let timer: ReturnType | undefined + const finish = (payload: Record): 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 + 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 { + 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 { + 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() diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index f4f8214..6df2003 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -462,6 +462,11 @@ declare global { value?: string error?: string }> + installIntelMacKeyRuntime: () => Promise<{ + success: boolean + error?: string + code?: string + }> autoGetDbKey: ( accountRoot: string, options?: { save?: boolean } diff --git a/src/preload/index.ts b/src/preload/index.ts index 69f05b7..ad99fd8 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -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 }) => diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index db6c574..22f1aad 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -1118,6 +1118,25 @@ function App(): React.ReactElement { } } + const handleInstallIntelKeyRuntime = async (): Promise => { + 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 => { 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)) diff --git a/src/renderer/src/components/DatabaseConnectionPage.tsx b/src/renderer/src/components/DatabaseConnectionPage.tsx index 326d36f..4038184 100644 --- a/src/renderer/src/components/DatabaseConnectionPage.tsx +++ b/src/renderer/src/components/DatabaseConnectionPage.tsx @@ -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({
  • 2
    - 准备连接组件 - 页面会按当前系统给出对应步骤 + {isIntelMac ? '让微信停在登录页面' : '准备连接组件'} + + {isIntelMac + ? '启动微信,但暂时不要点击登录' + : '页面会按当前系统给出对应步骤'} +
  • 3
    - 登录并验证连接 + {isIntelMac ? '获取密钥并验证连接' : '登录并验证连接'} 验证通过后进入主界面
  • @@ -241,9 +248,13 @@ export function DatabaseConnectionPage({ : [ '检查本机环境', '让微信停在登录页面', - '确认开始准备', - `正在完成 ${isMac ? 'macOS' : 'Windows'} 授权`, - '获取成功, 现在可以重新登录微信了', + isIntelMac ? '确认开始获取密钥' : '确认开始准备', + isIntelMac + ? '正在获取 Intel Mac 密钥' + : `正在完成 ${isMac ? 'macOS' : 'Windows'} 授权`, + isIntelMac + ? '获取成功,验证数据库连接' + : '获取成功, 现在可以重新登录微信了', '验证数据库连接' ][guideStep - 1]} @@ -253,10 +264,16 @@ export function DatabaseConnectionPage({ : status || [ '确认下方检测结果;没有找到目录时可以手动选择。', - '请退出当前微信账号,让微信停留在登录页面,然后点击“我已准备好”。', - '开始后请按页面提示完成系统授权。', - '正在准备连接组件,请不要关闭微信或 TraceMemo。', - '请回到微信完成登录,登录成功后再回来验证。', + isIntelMac + ? '请启动微信并停留在未登录界面,暂时不要点击登录,然后点击“我已准备好”。' + : '请退出当前微信账号,让微信停留在登录页面,然后点击“我已准备好”。', + isIntelMac ? '确认开始获取密钥。' : '开始后请按页面提示完成系统授权。', + isIntelMac + ? '等页面提示可以登录后,立即回到微信点击登录。' + : '正在准备连接组件,请不要关闭微信或 TraceMemo。', + isIntelMac + ? '密钥已获取,请继续验证数据库。' + : '请回到微信完成登录,登录成功后再回来验证。', '正在验证密钥和本地数据库,请稍候。' ][guideStep - 1]}

    @@ -314,6 +331,28 @@ export function DatabaseConnectionPage({
    微信状态
    {environment?.wechatRunning ? '运行中' : '未检测到'}
    + {isIntelMac && ( + <> +
    +
    连接环境
    +
    + {environment?.pythonAvailable && environment?.fridaAvailable + ? '已准备好' + : '需要准备'} + {(!environment?.pythonAvailable || !environment?.fridaAvailable) && ( + + )} +
    +
    + + )} {accounts.length > 0 && (
    @@ -406,7 +445,7 @@ export function DatabaseConnectionPage({ )} {guideStep === 3 && ( )} {guideStep === 4 && ( @@ -450,8 +489,10 @@ export function DatabaseConnectionPage({

    {isMac ? ( <> - macOS 首次获取密钥需要关闭 SIP。{' '} - + {isIntelMac + ? 'Intel Mac 首次使用需要先准备连接环境,按页面提示完成即可。' + : 'macOS 首次获取密钥需要关闭 SIP。'}{' '} + 查看说明 diff --git a/src/renderer/src/features/settings/database-key/DatabaseKeyAutoDetect.tsx b/src/renderer/src/features/settings/database-key/DatabaseKeyAutoDetect.tsx index 5ec29d6..d0ad2ee 100644 --- a/src/renderer/src/features/settings/database-key/DatabaseKeyAutoDetect.tsx +++ b/src/renderer/src/features/settings/database-key/DatabaseKeyAutoDetect.tsx @@ -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 (

    @@ -31,8 +32,12 @@ export function DatabaseKeyAutoDetect({
    - Windows 自动获取 -

    TraceMemo 可在微信桌面端正在运行时,通过本机内存扫描尝试获取数据库密钥。

    + {isIntelMac ? 'Intel Mac 自动获取' : 'Windows 自动获取'} +

    + {isIntelMac + ? '请让微信停在未登录界面,按页面提示操作即可。' + : 'TraceMemo 可在微信桌面端正在运行时,通过本机内存扫描尝试获取数据库密钥。'} +