fix: distinguish permission checks from failed reads and show next actions

This commit is contained in:
luke 2026-10-22 09:10:41 -06:00
parent 3726edcb77
commit 74027b41b2
10 changed files with 140 additions and 18 deletions

View File

@ -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)。
## 交付状态

View File

@ -1,4 +1,4 @@
# 页面连接 Mac 微信(v0.3,实验性)
# 页面连接 Mac 微信(v0.4,实验性)
在安装微信的目标 Mac 上更新代码,重新启动 `python3 run.py`,浏览器打开本机地址,进入“连接 Mac 微信”。此后无需手动运行诊断或逐个导出数据库。
@ -16,6 +16,8 @@
签名的调试限制和管理员身份是不同的检查。Apple 文档说明,调试工具对第三方应用取得任务端口还取决于目标是否允许调试。管理员按钮并不自动改变目标签名;因此反复输入密码可能一直失败。
如果看到“尚未尝试”,这只是签名检查,不是一次实际读取失败。新版会显示“检查完成,等待选择读取方式”和“尝试读取一次”按钮;限制提示采用待处理样式,不显示绿色成功。普通读取实际被拒后,可主动点管理员读取一次;管理员也被签名拒绝时,主按钮改为预览副本。已有副本时会引导启动,不要求重复创建。检查本身不会创建副本、提升权限或自动修改签名。
页面新增可选的**临时调试副本**流程:
1. 选择账号,点击“预览临时副本方案”,查看原版位置、副本位置、完整备份位置、空间估计与影响。这一步不会修改微信。

View File

@ -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:

View File

@ -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})

View File

@ -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 '接入失败,请检查文件访问权限、磁盘空间与数据格式。已有记录未被覆盖。'

View File

@ -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='尝试读取一次')

View File

@ -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:

View File

@ -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});}));

View File

@ -17,11 +17,11 @@
<div id="mac-platform" class="callout">正在检查本机环境…</div>
<div class="connect-steps"><span>01 找到账号</span><span>02 本机读取密钥</span><span>03 备份并导入</span></div>
<div class="panel form-panel"><h3>01 · 找到你的微信账号</h3><p class="muted">自动查找微信 4.x 数据目录。自定义位置可通过系统文件夹选择器指定;旧版目录不会自动混入。</p><div class="actions"><button id="mac-scan" class="primary">扫描本机微信</button><button id="mac-folder">选择数据文件夹</button><button id="mac-refresh">刷新状态</button></div><label>选择账号<select id="mac-account"><option value="">请先扫描</option></select></label><div id="mac-accounts"></div></div>
<div class="panel form-panel"><h3>02 · 让本机读取消息</h3><p>先打开并登录微信,在微信中打开你想分析的聊天,然后点击读取。候选密钥仅保存在本工具的运行内存,不会发送给模型、写入日志或仓库。</p><div class="actions"><button id="mac-permissions">检查签名与权限</button><button id="mac-capture" class="primary">读取本机密钥</button><button id="mac-capture-admin">使用 macOS 管理员授权读取</button><button id="mac-dependencies">安装文本解压支持</button></div><p class="muted">管理员按钮会弹出系统密码框,用于尝试读取微信进程;不会修改原版微信签名、关闭 SIP 或保存密码。系统保护仍可能拒绝访问,授权不保证成功。文本解压支持从 PyPI 安装到本工具的数据目录,不发送聊天。</p><details><summary>已有本地密钥文件?选择文件继续</summary><label>密钥 JSON(只在本机处理,请勿分享)<input id="mac-key-file" type="file" accept=".json"></label><p class="muted">支持包含 key / key_hex 字段的 JSON;64 位 raw key 或 96 位 key+salt。微信登录密码不能用来代替。</p></details><p id="mac-key-state" class="muted"></p></div>
<div id="mac-diagnostic-panel" class="panel" hidden><h3>读取权限诊断</h3><p id="mac-diagnostic-summary"></p><pre id="mac-diagnostic-details"></pre><p class="muted">签名限制与管理员权限是不同的检查。请不要为了重试而关闭 SIP;下面的临时副本方式仍需你明确同意,也不保证成功。</p></div>
<div class="panel form-panel"><h3>读取被签名限制时:使用临时调试副本</h3><p>先预览备份位置与操作影响。只有你明确同意后,工具才会备份账号数据,并对独立副本添加调试权限;原版安装文件保持不变。</p><div class="actions"><button id="mac-preview-copy">预览临时副本方案</button><button id="mac-launch-copy" disabled>启动已准备的临时副本</button></div><div id="mac-copy-plan" hidden><pre id="mac-copy-paths"></pre><ul id="mac-copy-effects"></ul><label class="check"><input id="mac-copy-consent" type="checkbox">我同意备份账号数据、为临时副本添加调试权限;理解副本登录可能更新同一账号的本地数据,且仍可能无法启动或取钥。</label><p class="muted">确认前请先正常退出所有微信,知微保持运行。预览 30 分钟内有效,使用一次后失效;备份会保留,不自动删除。</p><button id="mac-create-copy" class="primary" disabled>同意并备份、创建临时副本</button></div><p class="muted">副本启动后,按系统提示登录微信、打开目标聊天,再点击“读取本机密钥”;如仍被拒绝,可在页面主动尝试管理员读取。取钥后退出副本,再备份并导入。仅用于本人账号,取钥完成后恢复原版使用。</p></div>
<div class="panel form-panel"><h3>02 · 让本机读取消息</h3><p>先打开并登录微信,在微信中打开你想分析的聊天,然后点击读取。候选密钥仅保存在本工具的运行内存,不会发送给模型、写入日志或仓库。</p><div class="actions"><button id="mac-permissions">检查签名与权限</button><button id="mac-capture" class="primary">读取本机密钥</button><button id="mac-capture-admin">使用 macOS 管理员授权读取</button><button id="mac-dependencies">安装文本解压支持</button></div><p class="muted">管理员按钮会弹出系统密码框,用于尝试读取微信进程;不会修改原版微信签名、关闭 SIP 或保存密码。系统保护仍可能拒绝访问,授权不保证成功。文本解压支持从 PyPI 安装到本工具的数据目录,不发送聊天。</p><details id="mac-key-options"><summary>已有本地密钥文件?选择文件继续</summary><label>密钥 JSON(只在本机处理,请勿分享)<input id="mac-key-file" type="file" accept=".json"></label><p class="muted">支持包含 key / key_hex 字段的 JSON;64 位 raw key 或 96 位 key+salt。微信登录密码不能用来代替。</p></details><p id="mac-key-state" class="muted"></p></div>
<div id="mac-diagnostic-panel" class="panel mac-guidance" hidden><h3 id="mac-next-title">读取方式建议</h3><p id="mac-next-description"></p><div class="actions"><button id="mac-diagnostic-next" class="primary">继续下一步</button><button id="mac-show-recovery">查看临时副本说明</button></div><details><summary>查看检查信息</summary><p id="mac-diagnostic-summary"></p><pre id="mac-diagnostic-details"></pre></details><p class="muted">检查和实际读取是不同的步骤。按钮不会自动修改原版微信;创建副本仍需要预览并明确同意。</p></div>
<div id="mac-recovery-section" class="panel form-panel"><h3>读取被签名限制时:使用临时调试副本</h3><p>先预览备份位置与操作影响。只有你明确同意后,工具才会备份账号数据,并对独立副本添加调试权限;原版安装文件保持不变。</p><div class="actions"><button id="mac-preview-copy">预览临时副本方案</button><button id="mac-launch-copy" disabled>启动已准备的临时副本</button></div><div id="mac-copy-plan" hidden><pre id="mac-copy-paths"></pre><ul id="mac-copy-effects"></ul><label class="check"><input id="mac-copy-consent" type="checkbox">我同意备份账号数据、为临时副本添加调试权限;理解副本登录可能更新同一账号的本地数据,且仍可能无法启动或取钥。</label><p class="muted">确认前请先正常退出所有微信,知微保持运行。预览 30 分钟内有效,使用一次后失效;备份会保留,不自动删除。</p><button id="mac-create-copy" class="primary" disabled>同意并备份、创建临时副本</button></div><p class="muted">副本启动后,按系统提示登录微信、打开目标聊天,再点击“读取本机密钥”;如仍被拒绝,可在页面主动尝试管理员读取。取钥后退出副本,再备份并导入。仅用于本人账号,取钥完成后恢复原版使用。</p></div>
<div class="panel form-panel"><h3>03 · 备份、解密、校验并导入</h3><p>密钥匹配后,请正常退出微信,保持知微运行,再点击下面的按钮。原始数据库不会被修改,完整副本与可读库留在本机。</p><label class="check"><input id="mac-include-biz" type="checkbox">同时导入业务消息分片(可能包括公众号或服务消息)</label><div class="actions"><button id="mac-import" class="primary">备份并导入所选账号</button><button data-go="analyze">去分析记录 →</button></div><p class="muted">请预留至少约四倍所选数据库大小的磁盘空间。未知格式、缺失身份映射或认证失败时会停止,避免静默漏消息。</p></div>
<div id="mac-job-panel" class="panel" hidden><h3>接入进度</h3><p id="mac-job-stage" role="status"></p><div id="mac-job-error" class="callout" hidden></div><pre id="mac-job-result" hidden></pre></div>
<div id="mac-job-panel" class="panel" hidden><h3>接入进度</h3><p id="mac-job-stage" role="status"></p><div id="mac-job-error" class="callout" hidden></div><p id="mac-job-result" hidden></p><dl id="mac-job-details" hidden></dl></div>
<p class="muted">实验性接入:支持 SQLCipher 4 raw key / 4096 字节页 / SHA512 认证,以及部分微信 4.x 表结构。尚未在你的 Intel Mac 4.1.0.18 实机验收;页面会显示实际结果,不保证任何版本都能一键读取。</p>
</section>
<section id="page-import" class="page" hidden>

File diff suppressed because one or more lines are too long