diff --git a/README.md b/README.md index c3e5418..dd484ff 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,14 @@ 一个本地运行的 Python + SQLite 工具。从群聊和私聊里发现反复提问、风险/事故、重复踩坑、后续复用线索,将其整理为带原文证据的知识需求卡,对应 CREATES 的 Choose → Retrieve → Extract / Validate。 -## 页面连接 Mac 微信(v0.3) +## 页面连接 Mac 微信(v0.4) 新增“连接 Mac 微信”向导:扫描账号 → 读取本机密钥 → 退出微信后备份、解密、解析并导入。密钥仅存运行内存;权限不足或版本不匹配会显示具体失败原因。首次仍需在 Mac 启动本地服务,之后通过页面操作。 v0.3 将进程访问失败细分为签名阻塞、未运行和多进程等情况,管理员模式也保留诊断。新增需要用户预览并明确同意的临时调试副本路径:完整备份账号及登录配置后,只对独立副本添加调试权限,不改原版或 SIP。副本可能访问同一账号数据,且仍可能无法启动或取钥;操作影响和限制均在页面展示。页面标题同时移除重复的英文标签,统一中文。 +v0.4 将“仅完成签名检查”和“实际读取被拒绝”分开显示。检查结果直接给出下一步按钮:未尝试时先读取一次、普通读取被拒后可主动尝试管理员、管理员仍被签名拒绝时预览临时副本。只检查到限制不会显示绿色成功或暗示已取得密钥;任务阶段和结果以中文说明,不再直接展示 JSON。 + **接入属于实验性功能,尚未在目标 Intel Mac 微信 4.1.0.18 实机验证,不保证系统允许读取密钥。** 使用流程、权限与格式限制见 [Mac 接入说明](docs/mac-connection.md)。 ## 交付状态 diff --git a/docs/mac-connection.md b/docs/mac-connection.md index 9ebd01a..8b425f7 100644 --- a/docs/mac-connection.md +++ b/docs/mac-connection.md @@ -1,4 +1,4 @@ -# 页面连接 Mac 微信(v0.3,实验性) +# 页面连接 Mac 微信(v0.4,实验性) 在安装微信的目标 Mac 上更新代码,重新启动 `python3 run.py`,浏览器打开本机地址,进入“连接 Mac 微信”。此后无需手动运行诊断或逐个导出数据库。 @@ -16,6 +16,8 @@ 签名的调试限制和管理员身份是不同的检查。Apple 文档说明,调试工具对第三方应用取得任务端口还取决于目标是否允许调试。管理员按钮并不自动改变目标签名;因此反复输入密码可能一直失败。 +如果看到“尚未尝试”,这只是签名检查,不是一次实际读取失败。新版会显示“检查完成,等待选择读取方式”和“尝试读取一次”按钮;限制提示采用待处理样式,不显示绿色成功。普通读取实际被拒后,可主动点管理员读取一次;管理员也被签名拒绝时,主按钮改为预览副本。已有副本时会引导启动,不要求重复创建。检查本身不会创建副本、提升权限或自动修改签名。 + 页面新增可选的**临时调试副本**流程: 1. 选择账号,点击“预览临时副本方案”,查看原版位置、副本位置、完整备份位置、空间估计与影响。这一步不会修改微信。 diff --git a/scripts/package_release.py b/scripts/package_release.py index 13f2c6f..378e5bd 100644 --- a/scripts/package_release.py +++ b/scripts/package_release.py @@ -9,7 +9,7 @@ DIRS = ['wechat_tool', 'scripts', 'examples', 'tests', 'docs'] def main(): - out = ROOT / 'dist' / 'zhiwei-wechat-analysis-v0.3.zip' + out = ROOT / 'dist' / 'zhiwei-wechat-analysis-v0.4.zip' out.parent.mkdir(exist_ok=True) paths = [ROOT / p for p in FILES] for directory in DIRS: diff --git a/tests/test_mac_recovery.py b/tests/test_mac_recovery.py index 7410638..f1a2516 100644 --- a/tests/test_mac_recovery.py +++ b/tests/test_mac_recovery.py @@ -9,6 +9,7 @@ from unittest.mock import patch from wechat_tool.mac_connect import MacConnection from wechat_tool.mac_keys import permission_diagnostic from wechat_tool.mac_recovery import DebugRecovery +from wechat_tool.mac_guidance import next_step class PermissionTests(unittest.TestCase): @@ -18,12 +19,58 @@ class PermissionTests(unittest.TestCase): result=permission_diagnostic(5,123) self.assertEqual(result['code'],'target_restricted') self.assertEqual(result['task_for_pid_code'],5) - self.assertIn('反复输入密码',result['summary']) + self.assertTrue(result['attempted']) with patch('wechat_tool.mac_keys.wechat_processes',return_value=[]): self.assertEqual(permission_diagnostic()['code'],'no_process') with patch('wechat_tool.mac_keys.wechat_processes',return_value=[proc,dict(proc,pid=456)]): self.assertEqual(permission_diagnostic()['code'],'multiple_processes') + def test_signature_check_is_not_an_actual_read_failure(self): + proc={'pid':123,'path':'/Applications/WeChat.app/Contents/MacOS/WeChat'} + with patch('wechat_tool.mac_keys.wechat_processes',return_value=[proc]),patch('wechat_tool.mac_keys.signing_flags',return_value={'hardened_runtime':True,'get_task_allow':False}): + diagnostic=permission_diagnostic() + self.assertFalse(diagnostic['attempted']) + self.assertIsNone(diagnostic['task_for_pid_code']) + guidance=next_step(diagnostic) + self.assertEqual(guidance['action'],'capture') + self.assertEqual(guidance['level'],'warning') + self.assertIn('尚未实际读取',guidance['title']) + + def test_denied_read_guides_to_admin_then_copy_without_auto_execution(self): + diagnostic={'code':'target_restricted','task_for_pid_code':5,'administrator':False} + self.assertEqual(next_step(diagnostic)['action'],'capture_admin') + diagnostic['administrator']=True + self.assertEqual(next_step(diagnostic)['action'],'preview_copy') + self.assertEqual(next_step(diagnostic,copy_ready=True)['action'],'launch_copy') + with tempfile.TemporaryDirectory() as directory: + service=MacConnection(directory) + service.diagnostic=diagnostic + state=service.status() + self.assertEqual(state['next_step']['action'],'preview_copy') + self.assertFalse(list(Path(directory).rglob('*'))) + self.assertFalse(service.recovery.plans) + + def test_completed_check_does_not_claim_keys_or_import_succeeded(self): + diagnostic={'code':'target_restricted','task_for_pid_code':None,'administrator':False, + 'summary':'只完成签名检查','attempted':False} + with tempfile.TemporaryDirectory() as directory: + service=MacConnection(directory);service.job={'id':'check','action':'permissions','status':'running'} + with patch('wechat_tool.mac_connect.permission_diagnostic',return_value=diagnostic): + service.work('check','permissions',{},None,[]) + self.assertEqual(service.job['status'],'complete') + self.assertEqual(service.job['stage'],'检查完成,等待选择读取方式') + self.assertEqual(service.job['result']['level'],'warning') + self.assertFalse(service.keys) + self.assertEqual(service.status()['next_step']['action'],'capture') + self.assertFalse(list(Path(directory).rglob('*'))) + + def test_other_checks_have_specific_manual_next_steps(self): + self.assertEqual(next_step({'code':'no_process'})['action'],'permissions') + self.assertEqual(next_step({'code':'multiple_processes'})['action'],'permissions') + self.assertEqual(next_step({'code':'can_attempt'})['action'],'capture') + self.assertEqual(next_step({'code':'access_denied','administrator':True,'task_for_pid_code':5})['action'],'key_file') + self.assertIsNone(next_step(None)) + def test_administrator_helper_preserves_structured_failure(self): diagnostic={'code':'target_restricted','summary':'目标签名拒绝调试','task_for_pid_code':5} output=json.dumps({'error':'目标签名拒绝调试','diagnostic':diagnostic}) diff --git a/wechat_tool/mac_connect.py b/wechat_tool/mac_connect.py index 3b2710a..1e82189 100644 --- a/wechat_tool/mac_connect.py +++ b/wechat_tool/mac_connect.py @@ -18,6 +18,7 @@ from .cipher import PAGE, decrypt_database, matches from .importer import import_rows from .mac_keys import AccessDenied, permission_diagnostic, scan_keys, wechat_pid from .mac_recovery import DebugRecovery +from .mac_guidance import next_step from .wechat_native import message_rows @@ -63,7 +64,8 @@ class MacConnection: return {'available': platform.system() == 'Darwin', 'architecture': platform.machine(), 'accounts': [self.describe(key, path) for key,path in self.accounts.items()], 'keys_ready': bool(self.keys), 'job': dict(self.job) if self.job else None, - 'diagnostic': self.diagnostic, 'debug_copy_ready': bool(self.recovery.prepared)} + 'diagnostic': self.diagnostic, 'debug_copy_ready': bool(self.recovery.prepared), + 'next_step': next_step(self.diagnostic, bool(self.recovery.prepared))} def recovery_plan(self, account): self.require_mac() @@ -185,7 +187,8 @@ class MacConnection: elif action == 'permissions': self.stage('正在检查微信的签名与调试权限,不读取消息正文') self.diagnostic = permission_diagnostic() - result = {'message': self.diagnostic['summary']} + guidance = next_step(self.diagnostic, bool(self.recovery.prepared)) + result = {'message': guidance['title']+'。'+guidance['description'], 'level': guidance['level']} elif action == 'debug_copy': result = self.recovery.prepare(body['_plan'], self.stage) elif action == 'launch_copy': @@ -204,7 +207,10 @@ class MacConnection: else: result = self.import_account(path, keys, bool(body.get('include_biz'))) with self.lock: - self.job.update(status='complete', stage='已完成', result=result) + stages = {'permissions': '检查完成,等待选择读取方式', 'capture': '候选密钥已读取,请核对匹配数量', + 'debug_copy': '副本已准备,等待你启动', 'launch_copy': '已请求启动副本,等待你登录', + 'dependencies': '文本解压支持已安装', 'import': '记录已导入,可以开始分析'} + self.job.update(status='complete', stage=stages[action], result=result) except Exception as exc: # Never serialize subprocess stderr, process memory or raw key data. message = str(exc) if isinstance(exc, ValueError) else '接入失败,请检查文件访问权限、磁盘空间与数据格式。已有记录未被覆盖。' diff --git a/wechat_tool/mac_guidance.py b/wechat_tool/mac_guidance.py new file mode 100644 index 0000000..12d6c7e --- /dev/null +++ b/wechat_tool/mac_guidance.py @@ -0,0 +1,39 @@ +"""Actionable presentation of permission checks, without starting any action.""" + + +def next_step(diagnostic, copy_ready=False): + if not diagnostic: + return None + code = diagnostic.get('code') + attempted = diagnostic.get('task_for_pid_code') is not None + if code == 'no_process': + return dict(level='info', title='先打开并登录微信', + description='保持知微运行,在微信中登录本人账号并打开目标聊天,然后回来检查。', + action='permissions', button='我已登录,重新检查') + if code == 'multiple_processes': + return dict(level='warning', title='请只保留一个微信进程', + description='退出多余的原版或副本,仅保留本次要读取的微信,再重新检查。', + action='permissions', button='我已处理,重新检查') + if code == 'target_restricted' and copy_ready: + return dict(level='warning', title='临时副本已准备,请切换到副本', + description='当前检查仍发现签名限制。先正常退出原版,再启动已准备的副本;在副本中登录、打开聊天后读取。不会自动退出或替你登录。', + action='launch_copy', button='启动已准备的临时副本') + if code == 'target_restricted' and not attempted: + return dict(level='warning', title='检查已完成,尚未实际读取', + description='签名检查提示可能受限,这不等于已经取钥失败。先点击下面的按钮读取一次;系统若拒绝,再选择管理员读取或临时副本。', + action='capture', button='尝试读取一次') + if code in ('target_restricted', 'access_denied') and not diagnostic.get('administrator'): + return dict(level='warning', title='普通读取被拒,可选择管理员读取', + description='这是普通进程的实际拒绝结果。你可以主动授权管理员读取一次;这不会改变微信签名,若仍拒绝,页面会给出临时副本入口。', + action='capture_admin', button='授权管理员读取一次') + if code == 'target_restricted': + return dict(level='warning', title='管理员读取仍被拒,下一步可预览临时副本', + description='继续输入相同密码通常没有帮助。先预览完整备份位置和副本操作影响;预览不会修改微信,只有你勾选同意后才创建副本。', + action='preview_copy', button='预览备份与临时副本方案') + if code == 'access_denied': + return dict(level='warning', title='系统仍拒绝访问,请核对终端授权', + description='请在 macOS“隐私与安全性 → 开发者工具”检查运行知微的终端授权;已有本地密钥文件也可直接导入。授权不保证解除其他系统限制。', + action='key_file', button='选择已有的本地密钥文件') + return dict(level='info', title='可以尝试读取密钥', + description='目前未检查到明确的签名阻塞。打开目标聊天后点击读取,是否成功以实际结果为准。', + action='capture', button='尝试读取一次') diff --git a/wechat_tool/mac_keys.py b/wechat_tool/mac_keys.py index 7f54f98..20ffa81 100644 --- a/wechat_tool/mac_keys.py +++ b/wechat_tool/mac_keys.py @@ -45,13 +45,15 @@ def permission_diagnostic(code=None, pid=None): result = {'platform': platform.system(), 'architecture': platform.machine(), 'administrator': hasattr(os, 'geteuid') and os.geteuid() == 0, 'task_for_pid_code': code, 'processes': processes, 'code': 'no_process', + 'attempted': code is not None, 'summary': '请先打开并登录微信,再检查读取权限。'} if len(processes) > 1: result.update(code='multiple_processes', summary='检测到多个微信进程,请仅保留要读取的一个微信。') elif target: result.update(signing_flags(target['pid'])) if result['hardened_runtime'] and result['get_task_allow'] is False: - result.update(code='target_restricted', summary='微信启用了强化运行时,且没有允许调试。普通和管理员读取都可能被签名策略拒绝;反复输入密码不能改变此限制。') + summary = '签名检查发现微信未允许调试;这次只做了检查,尚未尝试读取密钥。请先尝试读取一次,再根据实际结果选择下一步。' if code is None else '实际读取被 macOS 拒绝,微信签名也未允许调试。请根据下方建议选择读取方式,不必反复重复同一操作。' + result.update(code='target_restricted', summary=summary) elif code is not None: result.update(code='access_denied', summary='macOS 拒绝读取进程,请检查系统“隐私与安全性 → 开发者工具”中的终端授权。权限设置不保证能突破目标签名限制。') else: diff --git a/wechat_tool/static/app.js b/wechat_tool/static/app.js index d92c13f..e86f69b 100644 --- a/wechat_tool/static/app.js +++ b/wechat_tool/static/app.js @@ -8,7 +8,7 @@ const fields = {source_id:'消息 ID', conversation:'会话名称', conversation const titles = {connect:'连接 Mac 微信',overview:'看见对话中的知识需求',import:'导入你的聊天记录',analyze:'发现值得沉淀的知识',settings:'模型接口与隐私设置'}; function el(tag, text, className) { const n = document.createElement(tag); if(text !== undefined) n.textContent = text; if(className) n.className=className; return n; } -function notify(text, error=false) { const n=$('#notice'); n.textContent=text; n.hidden=false; n.classList.toggle('error',error); } +function notify(text, error=false, level='success') { const n=$('#notice'); n.textContent=text; n.hidden=false; n.classList.toggle('error',error); n.classList.toggle('warning',!error&&level==='warning'); } async function api(path, body) { const opts=body===undefined?{}:{method:'POST',headers:{'Content-Type':'application/json','X-Workbench-Token':token},body:JSON.stringify(body)}; const response=await fetch(path,opts); const data=await response.json(); if(!response.ok)throw Error(data.error||'请求失败'); return data; @@ -119,7 +119,7 @@ $('#close-detail').addEventListener('click',()=>$('#detail-dialog').close()); $('#merge-cards').addEventListener('click',()=>{const ids=$$('.card-select:checked').map(n=>n.value);if(ids.length<2)return notify('请至少勾选两张主题卡片。',true);$('#merge-title').value=currentRun.cards.find(c=>c.id===ids[0]).title;$('#merge-dialog').showModal();}); $('#cancel-merge').addEventListener('click',()=>$('#merge-dialog').close()); $('#confirm-merge').addEventListener('click',e=>busy(e.target,async()=>{await api('/api/card',{run_id:currentRun.id,ids:$$('.card-select:checked').map(n=>n.value),action:'merge',title:$('#merge-title').value.trim()});await loadRun(currentRun.id);$('#merge-dialog').close();notify('已合并主题,原摘要保留在备注中,请重新审核。');})); -let macTimer = null, lastMacJob = null, macRunning = false, macAvailable = false, macRecoveryPlan = null, macCopyReady = false; +let macTimer = null, lastMacJob = null, macRunning = false, macAvailable = false, macRecoveryPlan = null, macCopyReady = false, macNextAction = null; async function macStatus(){ clearTimeout(macTimer); const state=await api('/api/mac/status'); @@ -129,6 +129,7 @@ async function macStatus(){ macAvailable=state.available;macCopyReady=state.debug_copy_ready; for(const id of ['mac-scan','mac-folder','mac-capture','mac-capture-admin','mac-dependencies','mac-import','mac-permissions','mac-preview-copy'])$('#'+id).disabled=!state.available||running; $('#mac-launch-copy').disabled=!state.available||running||!state.debug_copy_ready; + $('#mac-diagnostic-next').disabled=!state.available||running; $('#mac-create-copy').disabled=!state.available||running||!macRecoveryPlan||!$('#mac-copy-consent').checked; $('#mac-key-file').disabled=running; const select=$('#mac-account'),previous=select.value;select.replaceChildren();$('#mac-accounts').replaceChildren(); @@ -140,21 +141,40 @@ async function macStatus(){ $('#mac-key-state').textContent=state.keys_ready?'候选密钥已在运行内存。账号卡上的匹配数量决定实际可读范围。重启知微后需重新读取。':'尚未读取或导入密钥;未加密的可读数据库可直接进入第 3 步。'; const job=state.job;$('#mac-job-panel').hidden=!job; const diagnostic=state.diagnostic;$('#mac-diagnostic-panel').hidden=!diagnostic; + const guidance=state.next_step;macNextAction=guidance?.action||null; + if(guidance){$('#mac-next-title').textContent=guidance.title;$('#mac-next-description').textContent=guidance.description;$('#mac-diagnostic-next').textContent=guidance.button;} if(diagnostic){$('#mac-diagnostic-summary').textContent=diagnostic.summary;const yes=value=>value===true?'是':value===false?'否':'未知';$('#mac-diagnostic-details').textContent=[`诊断类型:${diagnostic.code}`,`强化运行时:${yes(diagnostic.hardened_runtime)}`,`允许调试:${yes(diagnostic.get_task_allow)}`,`管理员进程:${yes(diagnostic.administrator)}`,`进程读取返回码:${diagnostic.task_for_pid_code??'尚未尝试'}`].join('\n');} - if(job){$('#mac-job-stage').textContent=job.stage;$('#mac-job-error').hidden=job.status!=='failed';$('#mac-job-error').textContent=job.error||'';$('#mac-job-result').hidden=!job.result;$('#mac-job-result').textContent=job.result?JSON.stringify(job.result,null,2):''; - if(job.status==='complete'&&lastMacJob!==job.id){lastMacJob=job.id;await refresh();notify(job.result?.message||'接入操作已完成');} + if(job){$('#mac-job-stage').textContent=job.stage;$('#mac-job-error').hidden=job.status!=='failed';$('#mac-job-error').textContent=job.error||'';$('#mac-job-result').hidden=!job.result;$('#mac-job-result').textContent=job.result?.message||''; + const info=$('#mac-job-details');info.replaceChildren();const labels={parsed:'已解析消息',added:'新增消息',duplicates:'重复消息',backup:'本地备份位置',copy:'临时副本位置'};for(const [field,label] of Object.entries(labels)){if(job.result?.[field]!==undefined){info.append(el('dt',label),el('dd',String(job.result[field])));}}info.hidden=!info.childElementCount; + if(!running&&lastMacJob!==job.id){lastMacJob=job.id;if(job.status==='complete'){if(job.action==='import')await refresh();notify(job.result?.message||'接入操作已完成',false,job.result?.level||'success');}else{notify(guidance?.title||job.error||'接入未完成',true);}if(location.hash==='#connect'&&diagnostic)$('#mac-diagnostic-panel').scrollIntoView({behavior:'smooth',block:'start'});} if(running)macTimer=setTimeout(()=>macStatus().catch(e=>notify(e.message,true)),1800); } return state; } -async function macStart(action,extra={}){await api('/api/mac/start',{action,...extra});await macStatus();$('#mac-job-panel').scrollIntoView({behavior:'smooth',block:'center'});} +async function macStart(action,extra={}){await api('/api/mac/start',{action,...extra});const state=await macStatus();const target=state.diagnostic&&state.job?.status!=='running'?'#mac-diagnostic-panel':'#mac-job-panel';$(target).scrollIntoView({behavior:'smooth',block:'start'});} +async function previewMacCopy(){ + if(!$('#mac-account').value){$('#mac-account').scrollIntoView({behavior:'smooth',block:'center'});throw Error('请先扫描并选择账号,再预览完整备份与临时副本方案');} + const plan=await api('/api/mac/recovery-plan',{account:$('#mac-account').value}); + macRecoveryPlan=plan.plan_id;$('#mac-copy-consent').checked=false;$('#mac-create-copy').disabled=true;$('#mac-copy-plan').hidden=false; + $('#mac-copy-paths').textContent=`原版:${plan.source}\n临时副本:${plan.copy}\n完整备份:${plan.backup}\n预计至少需要 ${(plan.estimated_bytes/1024**3).toFixed(2)} GiB 可用空间`; + $('#mac-copy-effects').replaceChildren(...plan.effects.map(effect=>el('li',effect))); + $('#mac-copy-plan').scrollIntoView({behavior:'smooth',block:'start'}); +} +$('#mac-diagnostic-next').addEventListener('click',e=>busy(e.target,async()=>{ + if(macNextAction==='preview_copy')return previewMacCopy(); + if(macNextAction==='capture_admin')return macStart('capture',{administrator:true}); + if(macNextAction==='key_file'){$('#mac-key-options').open=true;$('#mac-key-options').scrollIntoView({behavior:'smooth',block:'center'});$('#mac-key-file').focus();return;} + if(['capture','launch_copy','permissions'].includes(macNextAction))return macStart(macNextAction); + throw Error('请先重新检查权限,再选择下一步'); +})); +$('#mac-show-recovery').addEventListener('click',()=>$('#mac-recovery-section').scrollIntoView({behavior:'smooth',block:'start'})); $('#mac-scan').addEventListener('click',e=>busy(e.target,async()=>{await api('/api/mac/scan',{});await macStatus();})); $('#mac-folder').addEventListener('click',e=>busy(e.target,async()=>{await api('/api/mac/scan',{choose_folder:true});await macStatus();})); $('#mac-refresh').addEventListener('click',e=>busy(e.target,macStatus)); $('#mac-capture').addEventListener('click',e=>busy(e.target,()=>macStart('capture'))); $('#mac-capture-admin').addEventListener('click',e=>busy(e.target,()=>macStart('capture',{administrator:true}))); $('#mac-permissions').addEventListener('click',e=>busy(e.target,()=>macStart('permissions'))); -$('#mac-preview-copy').addEventListener('click',e=>busy(e.target,async()=>{const plan=await api('/api/mac/recovery-plan',{account:$('#mac-account').value});macRecoveryPlan=plan.plan_id;$('#mac-copy-consent').checked=false;$('#mac-create-copy').disabled=true;$('#mac-copy-plan').hidden=false;$('#mac-copy-paths').textContent=`原版:${plan.source}\n临时副本:${plan.copy}\n完整备份:${plan.backup}\n预计至少需要 ${(plan.estimated_bytes/1024**3).toFixed(2)} GiB 可用空间`;$('#mac-copy-effects').replaceChildren(...plan.effects.map(effect=>el('li',effect)));})); +$('#mac-preview-copy').addEventListener('click',e=>busy(e.target,previewMacCopy)); $('#mac-copy-consent').addEventListener('change',()=>{$('#mac-create-copy').disabled=!macAvailable||macRunning||!macRecoveryPlan||!$('#mac-copy-consent').checked;}); $('#mac-account').addEventListener('change',()=>{macRecoveryPlan=null;$('#mac-copy-plan').hidden=true;$('#mac-copy-consent').checked=false;$('#mac-create-copy').disabled=true;}); $('#mac-create-copy').addEventListener('click',e=>busy(e.target,async()=>{if(!macRecoveryPlan||!$('#mac-copy-consent').checked)throw Error('请先预览并明确同意操作影响');const plan=macRecoveryPlan;macRecoveryPlan=null;$('#mac-copy-plan').hidden=true;await macStart('debug_copy',{account:$('#mac-account').value,plan_id:plan,consent:true});})); diff --git a/wechat_tool/static/index.html b/wechat_tool/static/index.html index e8cde6f..063f079 100644 --- a/wechat_tool/static/index.html +++ b/wechat_tool/static/index.html @@ -17,11 +17,11 @@
自动查找微信 4.x 数据目录。自定义位置可通过系统文件夹选择器指定;旧版目录不会自动混入。
先打开并登录微信,在微信中打开你想分析的聊天,然后点击读取。候选密钥仅保存在本工具的运行内存,不会发送给模型、写入日志或仓库。
管理员按钮会弹出系统密码框,用于尝试读取微信进程;不会修改原版微信签名、关闭 SIP 或保存密码。系统保护仍可能拒绝访问,授权不保证成功。文本解压支持从 PyPI 安装到本工具的数据目录,不发送聊天。
支持包含 key / key_hex 字段的 JSON;64 位 raw key 或 96 位 key+salt。微信登录密码不能用来代替。
签名限制与管理员权限是不同的检查。请不要为了重试而关闭 SIP;下面的临时副本方式仍需你明确同意,也不保证成功。
先预览备份位置与操作影响。只有你明确同意后,工具才会备份账号数据,并对独立副本添加调试权限;原版安装文件保持不变。
确认前请先正常退出所有微信,知微保持运行。预览 30 分钟内有效,使用一次后失效;备份会保留,不自动删除。
副本启动后,按系统提示登录微信、打开目标聊天,再点击“读取本机密钥”;如仍被拒绝,可在页面主动尝试管理员读取。取钥后退出副本,再备份并导入。仅用于本人账号,取钥完成后恢复原版使用。
先打开并登录微信,在微信中打开你想分析的聊天,然后点击读取。候选密钥仅保存在本工具的运行内存,不会发送给模型、写入日志或仓库。
管理员按钮会弹出系统密码框,用于尝试读取微信进程;不会修改原版微信签名、关闭 SIP 或保存密码。系统保护仍可能拒绝访问,授权不保证成功。文本解压支持从 PyPI 安装到本工具的数据目录,不发送聊天。
支持包含 key / key_hex 字段的 JSON;64 位 raw key 或 96 位 key+salt。微信登录密码不能用来代替。
检查和实际读取是不同的步骤。按钮不会自动修改原版微信;创建副本仍需要预览并明确同意。
先预览备份位置与操作影响。只有你明确同意后,工具才会备份账号数据,并对独立副本添加调试权限;原版安装文件保持不变。
确认前请先正常退出所有微信,知微保持运行。预览 30 分钟内有效,使用一次后失效;备份会保留,不自动删除。
副本启动后,按系统提示登录微信、打开目标聊天,再点击“读取本机密钥”;如仍被拒绝,可在页面主动尝试管理员读取。取钥后退出副本,再备份并导入。仅用于本人账号,取钥完成后恢复原版使用。
密钥匹配后,请正常退出微信,保持知微运行,再点击下面的按钮。原始数据库不会被修改,完整副本与可读库留在本机。
请预留至少约四倍所选数据库大小的磁盘空间。未知格式、缺失身份映射或认证失败时会停止,避免静默漏消息。
实验性接入:支持 SQLCipher 4 raw key / 4096 字节页 / SHA512 认证,以及部分微信 4.x 表结构。尚未在你的 Intel Mac 4.1.0.18 实机验收;页面会显示实际结果,不保证任何版本都能一键读取。