fix: distinguish permission checks from failed reads and show next actions
This commit is contained in:
parent
3726edcb77
commit
74027b41b2
@ -2,12 +2,14 @@
|
|||||||
|
|
||||||
一个本地运行的 Python + SQLite 工具。从群聊和私聊里发现反复提问、风险/事故、重复踩坑、后续复用线索,将其整理为带原文证据的知识需求卡,对应 CREATES 的 Choose → Retrieve → Extract / Validate。
|
一个本地运行的 Python + SQLite 工具。从群聊和私聊里发现反复提问、风险/事故、重复踩坑、后续复用线索,将其整理为带原文证据的知识需求卡,对应 CREATES 的 Choose → Retrieve → Extract / Validate。
|
||||||
|
|
||||||
## 页面连接 Mac 微信(v0.3)
|
## 页面连接 Mac 微信(v0.4)
|
||||||
|
|
||||||
新增“连接 Mac 微信”向导:扫描账号 → 读取本机密钥 → 退出微信后备份、解密、解析并导入。密钥仅存运行内存;权限不足或版本不匹配会显示具体失败原因。首次仍需在 Mac 启动本地服务,之后通过页面操作。
|
新增“连接 Mac 微信”向导:扫描账号 → 读取本机密钥 → 退出微信后备份、解密、解析并导入。密钥仅存运行内存;权限不足或版本不匹配会显示具体失败原因。首次仍需在 Mac 启动本地服务,之后通过页面操作。
|
||||||
|
|
||||||
v0.3 将进程访问失败细分为签名阻塞、未运行和多进程等情况,管理员模式也保留诊断。新增需要用户预览并明确同意的临时调试副本路径:完整备份账号及登录配置后,只对独立副本添加调试权限,不改原版或 SIP。副本可能访问同一账号数据,且仍可能无法启动或取钥;操作影响和限制均在页面展示。页面标题同时移除重复的英文标签,统一中文。
|
v0.3 将进程访问失败细分为签名阻塞、未运行和多进程等情况,管理员模式也保留诊断。新增需要用户预览并明确同意的临时调试副本路径:完整备份账号及登录配置后,只对独立副本添加调试权限,不改原版或 SIP。副本可能访问同一账号数据,且仍可能无法启动或取钥;操作影响和限制均在页面展示。页面标题同时移除重复的英文标签,统一中文。
|
||||||
|
|
||||||
|
v0.4 将“仅完成签名检查”和“实际读取被拒绝”分开显示。检查结果直接给出下一步按钮:未尝试时先读取一次、普通读取被拒后可主动尝试管理员、管理员仍被签名拒绝时预览临时副本。只检查到限制不会显示绿色成功或暗示已取得密钥;任务阶段和结果以中文说明,不再直接展示 JSON。
|
||||||
|
|
||||||
**接入属于实验性功能,尚未在目标 Intel Mac 微信 4.1.0.18 实机验证,不保证系统允许读取密钥。** 使用流程、权限与格式限制见 [Mac 接入说明](docs/mac-connection.md)。
|
**接入属于实验性功能,尚未在目标 Intel Mac 微信 4.1.0.18 实机验证,不保证系统允许读取密钥。** 使用流程、权限与格式限制见 [Mac 接入说明](docs/mac-connection.md)。
|
||||||
|
|
||||||
## 交付状态
|
## 交付状态
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# 页面连接 Mac 微信(v0.3,实验性)
|
# 页面连接 Mac 微信(v0.4,实验性)
|
||||||
|
|
||||||
在安装微信的目标 Mac 上更新代码,重新启动 `python3 run.py`,浏览器打开本机地址,进入“连接 Mac 微信”。此后无需手动运行诊断或逐个导出数据库。
|
在安装微信的目标 Mac 上更新代码,重新启动 `python3 run.py`,浏览器打开本机地址,进入“连接 Mac 微信”。此后无需手动运行诊断或逐个导出数据库。
|
||||||
|
|
||||||
@ -16,6 +16,8 @@
|
|||||||
|
|
||||||
签名的调试限制和管理员身份是不同的检查。Apple 文档说明,调试工具对第三方应用取得任务端口还取决于目标是否允许调试。管理员按钮并不自动改变目标签名;因此反复输入密码可能一直失败。
|
签名的调试限制和管理员身份是不同的检查。Apple 文档说明,调试工具对第三方应用取得任务端口还取决于目标是否允许调试。管理员按钮并不自动改变目标签名;因此反复输入密码可能一直失败。
|
||||||
|
|
||||||
|
如果看到“尚未尝试”,这只是签名检查,不是一次实际读取失败。新版会显示“检查完成,等待选择读取方式”和“尝试读取一次”按钮;限制提示采用待处理样式,不显示绿色成功。普通读取实际被拒后,可主动点管理员读取一次;管理员也被签名拒绝时,主按钮改为预览副本。已有副本时会引导启动,不要求重复创建。检查本身不会创建副本、提升权限或自动修改签名。
|
||||||
|
|
||||||
页面新增可选的**临时调试副本**流程:
|
页面新增可选的**临时调试副本**流程:
|
||||||
|
|
||||||
1. 选择账号,点击“预览临时副本方案”,查看原版位置、副本位置、完整备份位置、空间估计与影响。这一步不会修改微信。
|
1. 选择账号,点击“预览临时副本方案”,查看原版位置、副本位置、完整备份位置、空间估计与影响。这一步不会修改微信。
|
||||||
|
|||||||
@ -9,7 +9,7 @@ DIRS = ['wechat_tool', 'scripts', 'examples', 'tests', 'docs']
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
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)
|
out.parent.mkdir(exist_ok=True)
|
||||||
paths = [ROOT / p for p in FILES]
|
paths = [ROOT / p for p in FILES]
|
||||||
for directory in DIRS:
|
for directory in DIRS:
|
||||||
|
|||||||
@ -9,6 +9,7 @@ from unittest.mock import patch
|
|||||||
from wechat_tool.mac_connect import MacConnection
|
from wechat_tool.mac_connect import MacConnection
|
||||||
from wechat_tool.mac_keys import permission_diagnostic
|
from wechat_tool.mac_keys import permission_diagnostic
|
||||||
from wechat_tool.mac_recovery import DebugRecovery
|
from wechat_tool.mac_recovery import DebugRecovery
|
||||||
|
from wechat_tool.mac_guidance import next_step
|
||||||
|
|
||||||
|
|
||||||
class PermissionTests(unittest.TestCase):
|
class PermissionTests(unittest.TestCase):
|
||||||
@ -18,12 +19,58 @@ class PermissionTests(unittest.TestCase):
|
|||||||
result=permission_diagnostic(5,123)
|
result=permission_diagnostic(5,123)
|
||||||
self.assertEqual(result['code'],'target_restricted')
|
self.assertEqual(result['code'],'target_restricted')
|
||||||
self.assertEqual(result['task_for_pid_code'],5)
|
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=[]):
|
with patch('wechat_tool.mac_keys.wechat_processes',return_value=[]):
|
||||||
self.assertEqual(permission_diagnostic()['code'],'no_process')
|
self.assertEqual(permission_diagnostic()['code'],'no_process')
|
||||||
with patch('wechat_tool.mac_keys.wechat_processes',return_value=[proc,dict(proc,pid=456)]):
|
with patch('wechat_tool.mac_keys.wechat_processes',return_value=[proc,dict(proc,pid=456)]):
|
||||||
self.assertEqual(permission_diagnostic()['code'],'multiple_processes')
|
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):
|
def test_administrator_helper_preserves_structured_failure(self):
|
||||||
diagnostic={'code':'target_restricted','summary':'目标签名拒绝调试','task_for_pid_code':5}
|
diagnostic={'code':'target_restricted','summary':'目标签名拒绝调试','task_for_pid_code':5}
|
||||||
output=json.dumps({'error':'目标签名拒绝调试','diagnostic':diagnostic})
|
output=json.dumps({'error':'目标签名拒绝调试','diagnostic':diagnostic})
|
||||||
|
|||||||
@ -18,6 +18,7 @@ from .cipher import PAGE, decrypt_database, matches
|
|||||||
from .importer import import_rows
|
from .importer import import_rows
|
||||||
from .mac_keys import AccessDenied, permission_diagnostic, scan_keys, wechat_pid
|
from .mac_keys import AccessDenied, permission_diagnostic, scan_keys, wechat_pid
|
||||||
from .mac_recovery import DebugRecovery
|
from .mac_recovery import DebugRecovery
|
||||||
|
from .mac_guidance import next_step
|
||||||
from .wechat_native import message_rows
|
from .wechat_native import message_rows
|
||||||
|
|
||||||
|
|
||||||
@ -63,7 +64,8 @@ class MacConnection:
|
|||||||
return {'available': platform.system() == 'Darwin', 'architecture': platform.machine(),
|
return {'available': platform.system() == 'Darwin', 'architecture': platform.machine(),
|
||||||
'accounts': [self.describe(key, path) for key,path in self.accounts.items()],
|
'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,
|
'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):
|
def recovery_plan(self, account):
|
||||||
self.require_mac()
|
self.require_mac()
|
||||||
@ -185,7 +187,8 @@ class MacConnection:
|
|||||||
elif action == 'permissions':
|
elif action == 'permissions':
|
||||||
self.stage('正在检查微信的签名与调试权限,不读取消息正文')
|
self.stage('正在检查微信的签名与调试权限,不读取消息正文')
|
||||||
self.diagnostic = permission_diagnostic()
|
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':
|
elif action == 'debug_copy':
|
||||||
result = self.recovery.prepare(body['_plan'], self.stage)
|
result = self.recovery.prepare(body['_plan'], self.stage)
|
||||||
elif action == 'launch_copy':
|
elif action == 'launch_copy':
|
||||||
@ -204,7 +207,10 @@ class MacConnection:
|
|||||||
else:
|
else:
|
||||||
result = self.import_account(path, keys, bool(body.get('include_biz')))
|
result = self.import_account(path, keys, bool(body.get('include_biz')))
|
||||||
with self.lock:
|
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:
|
except Exception as exc:
|
||||||
# Never serialize subprocess stderr, process memory or raw key data.
|
# Never serialize subprocess stderr, process memory or raw key data.
|
||||||
message = str(exc) if isinstance(exc, ValueError) else '接入失败,请检查文件访问权限、磁盘空间与数据格式。已有记录未被覆盖。'
|
message = str(exc) if isinstance(exc, ValueError) else '接入失败,请检查文件访问权限、磁盘空间与数据格式。已有记录未被覆盖。'
|
||||||
|
|||||||
39
wechat_tool/mac_guidance.py
Normal file
39
wechat_tool/mac_guidance.py
Normal 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='尝试读取一次')
|
||||||
@ -45,13 +45,15 @@ def permission_diagnostic(code=None, pid=None):
|
|||||||
result = {'platform': platform.system(), 'architecture': platform.machine(),
|
result = {'platform': platform.system(), 'architecture': platform.machine(),
|
||||||
'administrator': hasattr(os, 'geteuid') and os.geteuid() == 0,
|
'administrator': hasattr(os, 'geteuid') and os.geteuid() == 0,
|
||||||
'task_for_pid_code': code, 'processes': processes, 'code': 'no_process',
|
'task_for_pid_code': code, 'processes': processes, 'code': 'no_process',
|
||||||
|
'attempted': code is not None,
|
||||||
'summary': '请先打开并登录微信,再检查读取权限。'}
|
'summary': '请先打开并登录微信,再检查读取权限。'}
|
||||||
if len(processes) > 1:
|
if len(processes) > 1:
|
||||||
result.update(code='multiple_processes', summary='检测到多个微信进程,请仅保留要读取的一个微信。')
|
result.update(code='multiple_processes', summary='检测到多个微信进程,请仅保留要读取的一个微信。')
|
||||||
elif target:
|
elif target:
|
||||||
result.update(signing_flags(target['pid']))
|
result.update(signing_flags(target['pid']))
|
||||||
if result['hardened_runtime'] and result['get_task_allow'] is False:
|
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:
|
elif code is not None:
|
||||||
result.update(code='access_denied', summary='macOS 拒绝读取进程,请检查系统“隐私与安全性 → 开发者工具”中的终端授权。权限设置不保证能突破目标签名限制。')
|
result.update(code='access_denied', summary='macOS 拒绝读取进程,请检查系统“隐私与安全性 → 开发者工具”中的终端授权。权限设置不保证能突破目标签名限制。')
|
||||||
else:
|
else:
|
||||||
|
|||||||
@ -8,7 +8,7 @@ const fields = {source_id:'消息 ID', conversation:'会话名称', conversation
|
|||||||
const titles = {connect:'连接 Mac 微信',overview:'看见对话中的知识需求',import:'导入你的聊天记录',analyze:'发现值得沉淀的知识',settings:'模型接口与隐私设置'};
|
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 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) {
|
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 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;
|
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();});
|
$('#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());
|
$('#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('已合并主题,原摘要保留在备注中,请重新审核。');}));
|
$('#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(){
|
async function macStatus(){
|
||||||
clearTimeout(macTimer);
|
clearTimeout(macTimer);
|
||||||
const state=await api('/api/mac/status');
|
const state=await api('/api/mac/status');
|
||||||
@ -129,6 +129,7 @@ async function macStatus(){
|
|||||||
macAvailable=state.available;macCopyReady=state.debug_copy_ready;
|
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;
|
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-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-create-copy').disabled=!state.available||running||!macRecoveryPlan||!$('#mac-copy-consent').checked;
|
||||||
$('#mac-key-file').disabled=running;
|
$('#mac-key-file').disabled=running;
|
||||||
const select=$('#mac-account'),previous=select.value;select.replaceChildren();$('#mac-accounts').replaceChildren();
|
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 步。';
|
$('#mac-key-state').textContent=state.keys_ready?'候选密钥已在运行内存。账号卡上的匹配数量决定实际可读范围。重启知微后需重新读取。':'尚未读取或导入密钥;未加密的可读数据库可直接进入第 3 步。';
|
||||||
const job=state.job;$('#mac-job-panel').hidden=!job;
|
const job=state.job;$('#mac-job-panel').hidden=!job;
|
||||||
const diagnostic=state.diagnostic;$('#mac-diagnostic-panel').hidden=!diagnostic;
|
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(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){$('#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||'';
|
||||||
if(job.status==='complete'&&lastMacJob!==job.id){lastMacJob=job.id;await refresh();notify(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);
|
if(running)macTimer=setTimeout(()=>macStatus().catch(e=>notify(e.message,true)),1800);
|
||||||
}
|
}
|
||||||
return state;
|
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-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-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-refresh').addEventListener('click',e=>busy(e.target,macStatus));
|
||||||
$('#mac-capture').addEventListener('click',e=>busy(e.target,()=>macStart('capture')));
|
$('#mac-capture').addEventListener('click',e=>busy(e.target,()=>macStart('capture')));
|
||||||
$('#mac-capture-admin').addEventListener('click',e=>busy(e.target,()=>macStart('capture',{administrator:true})));
|
$('#mac-capture-admin').addEventListener('click',e=>busy(e.target,()=>macStart('capture',{administrator:true})));
|
||||||
$('#mac-permissions').addEventListener('click',e=>busy(e.target,()=>macStart('permissions')));
|
$('#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-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-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});}));
|
$('#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});}));
|
||||||
|
|||||||
@ -17,11 +17,11 @@
|
|||||||
<div id="mac-platform" class="callout">正在检查本机环境…</div>
|
<div id="mac-platform" class="callout">正在检查本机环境…</div>
|
||||||
<div class="connect-steps"><span>01 找到账号</span><span>02 本机读取密钥</span><span>03 备份并导入</span></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>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 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" hidden><h3>读取权限诊断</h3><p id="mac-diagnostic-summary"></p><pre id="mac-diagnostic-details"></pre><p class="muted">签名限制与管理员权限是不同的检查。请不要为了重试而关闭 SIP;下面的临时副本方式仍需你明确同意,也不保证成功。</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 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 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 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>
|
<p class="muted">实验性接入:支持 SQLCipher 4 raw key / 4096 字节页 / SHA512 认证,以及部分微信 4.x 表结构。尚未在你的 Intel Mac 4.1.0.18 实机验收;页面会显示实际结果,不保证任何版本都能一键读取。</p>
|
||||||
</section>
|
</section>
|
||||||
<section id="page-import" class="page" hidden>
|
<section id="page-import" class="page" hidden>
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user