From 839949a54495145754c73fbf45fa9c1791647eb9 Mon Sep 17 00:00:00 2001 From: 2977094657 <2977094657@qq.com> Date: Mon, 13 Apr 2026 18:11:25 +0800 Subject: [PATCH] =?UTF-8?q?improvement(log):=20=E8=A1=A5=E5=85=85=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E5=AF=86=E9=92=A5=E4=B8=8E=E5=AA=92=E4=BD=93=E8=A7=A3?= =?UTF-8?q?=E5=AF=86=E9=93=BE=E8=B7=AF=E8=B0=83=E8=AF=95=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 为解密页关键阶段补充调试日志 - 为图片密钥获取流程补充请求、命中和回退日志 - 为媒体密钥保存与批量解密补充有效密钥日志 - 对 AES 信息做摘要输出,便于排查且避免完整泄露 --- frontend/pages/decrypt.vue | 139 ++++++++++++++++++++++- src/wechat_decrypt_tool/key_service.py | 110 ++++++++++++++++-- src/wechat_decrypt_tool/routers/keys.py | 51 +++++++++ src/wechat_decrypt_tool/routers/media.py | 75 ++++++++++++ 4 files changed, 364 insertions(+), 11 deletions(-) diff --git a/frontend/pages/decrypt.vue b/frontend/pages/decrypt.vue index 662885e..7da84fd 100644 --- a/frontend/pages/decrypt.vue +++ b/frontend/pages/decrypt.vue @@ -487,6 +487,44 @@ const manualKeyErrors = reactive({ }) const normalizeAccountId = (value) => String(value || '').trim() +const summarizeAesForLog = (value) => { + const raw = String(value || '').trim() + if (!raw) return '' + if (raw.length <= 8) return raw + return `${raw.slice(0, 4)}...${raw.slice(-4)}(len=${raw.length})` +} +const summarizeKeyStateForLog = (xorKey, aesKey) => ({ + xor_key: String(xorKey || '').trim(), + aes_key: summarizeAesForLog(aesKey), + has_xor: !!String(xorKey || '').trim(), + has_aes: !!String(aesKey || '').trim() +}) +const formatLogError = (error) => { + if (!error) return '' + if (error instanceof Error) { + return { + name: String(error.name || 'Error'), + message: String(error.message || ''), + stack: String(error.stack || '') + } + } + if (typeof error === 'object') { + try { + return JSON.parse(JSON.stringify(error)) + } catch {} + } + return String(error) +} +const logDecryptDebug = (phase, details = {}) => { + if (process.client && typeof window !== 'undefined') { + try { + window.wechatDesktop?.logDebug?.('decrypt-page', phase, details) + } catch {} + } + try { + console.info(`[decrypt-page] ${phase}`, details) + } catch {} +} const normalizeXorKey = (value) => { const raw = String(value || '').trim() @@ -508,6 +546,7 @@ const normalizeAesKey = (value) => { const prefillKeysForAccount = async (account) => { const acc = normalizeAccountId(account) if (!acc) return + logDecryptDebug('prefill:start', { account: acc }) try { const resp = await getSavedKeys({ account: acc }) if (!resp || resp.status !== 'success') return @@ -527,22 +566,46 @@ const prefillKeysForAccount = async (account) => { if (aesKey && !String(manualKeys.aes_key || '').trim()) { manualKeys.aes_key = aesKey } + logDecryptDebug('prefill:done', { + request_account: acc, + response_account: String(resp.account || '').trim(), + db_key_present: !!dbKey, + ...summarizeKeyStateForLog( + String(keys.image_xor_key || '').trim(), + String(keys.image_aes_key || '').trim() + ), + applied: summarizeKeyStateForLog(manualKeys.xor_key, manualKeys.aes_key) + }) } catch (e) { - // ignore + logDecryptDebug('prefill:error', { account: acc, error: formatLogError(e) }) } } const tryAutoFetchImageKeys = async (account) => { const acc = normalizeAccountId(account) if (!acc) return - if (String(manualKeys.xor_key || '').trim() || String(manualKeys.aes_key || '').trim()) return + if (String(manualKeys.xor_key || '').trim() || String(manualKeys.aes_key || '').trim()) { + logDecryptDebug('auto-fetch:skip-existing', { + account: acc, + keys: summarizeKeyStateForLog(manualKeys.xor_key, manualKeys.aes_key) + }) + return + } warning.value = '正在通过云端/本地算法自动获取图片密钥,请稍候...' + logDecryptDebug('auto-fetch:start', { account: acc }) try { const imgRes = await getImageKey({ account: acc, db_storage_path: String(formData.db_storage_path || '').trim() }) + logDecryptDebug('auto-fetch:response', { + account: acc, + status: imgRes?.status, + errmsg: String(imgRes?.errmsg || ''), + data_account: String(imgRes?.data?.account || '').trim(), + keys: summarizeKeyStateForLog(imgRes?.data?.xor_key, imgRes?.data?.aes_key) + }) if (imgRes && imgRes.status === 0) { if (imgRes.data?.xor_key) manualKeys.xor_key = imgRes.data.xor_key @@ -554,6 +617,7 @@ const tryAutoFetchImageKeys = async (account) => { } } catch (e) { warning.value = '网络请求失败,请手动填写图片密钥。' + logDecryptDebug('auto-fetch:error', { account: acc, error: formatLogError(e) }) } } @@ -561,13 +625,27 @@ const ensureKeysForAccount = async (account) => { const acc = normalizeAccountId(account) if (!acc) return + logDecryptDebug('ensure-keys:start', { + account: acc, + previous_account: activeKeyAccount.value, + current_manual: summarizeKeyStateForLog(manualKeys.xor_key, manualKeys.aes_key) + }) if (activeKeyAccount.value && activeKeyAccount.value !== acc) { + logDecryptDebug('ensure-keys:switch-account', { + from: activeKeyAccount.value, + to: acc, + cleared_keys: summarizeKeyStateForLog(manualKeys.xor_key, manualKeys.aes_key) + }) clearManualKeys() } activeKeyAccount.value = acc await prefillKeysForAccount(acc) await tryAutoFetchImageKeys(acc) + logDecryptDebug('ensure-keys:done', { + account: acc, + manual: summarizeKeyStateForLog(manualKeys.xor_key, manualKeys.aes_key) + }) } const handleGetDbKey = async () => { @@ -640,6 +718,11 @@ const applyManualKeys = () => { } const clearManualKeys = () => { + logDecryptDebug('keys:clear', { + active_account: activeKeyAccount.value, + manual: summarizeKeyStateForLog(manualKeys.xor_key, manualKeys.aes_key), + applied: summarizeKeyStateForLog(mediaKeys.xor_key, mediaKeys.aes_key) + }) manualKeys.xor_key = '' manualKeys.aes_key = '' manualKeyErrors.xor_key = '' @@ -769,6 +852,10 @@ const handleDecrypt = async () => { return } + logDecryptDebug('decrypt:start', { + db_storage_path: String(formData.db_storage_path || '').trim(), + db_key_length: String(formData.key || '').trim().length + }) loading.value = true error.value = '' warning.value = '' @@ -799,6 +886,10 @@ const handleDecrypt = async () => { if (match) mediaAccount.value = match[1] } } catch (e) {} + logDecryptDebug('decrypt:completed-fallback', { + media_account: mediaAccount.value, + accounts: Object.keys(result.account_results || {}) + }) currentStep.value = 1 await ensureKeysForAccount(mediaAccount.value) @@ -877,6 +968,10 @@ const handleDecrypt = async () => { if (match) mediaAccount.value = match[1] } } catch (e) {} + logDecryptDebug('decrypt:completed-sse', { + media_account: mediaAccount.value, + accounts: Object.keys(data.account_results || {}) + }) try { eventSource.close() @@ -929,6 +1024,10 @@ const decryptAllImages = async () => { mediaDecryptResult.value = null error.value = '' warning.value = '' + logDecryptDebug('media-decrypt:start', { + account: mediaAccount.value, + keys: summarizeKeyStateForLog(mediaKeys.xor_key, mediaKeys.aes_key) + }) // 重置进度 resetMediaDecryptProgress() @@ -973,9 +1072,20 @@ const decryptAllImages = async () => { decryptProgress.fail_count = data.fail_count mediaDecryptResult.value = data mediaDecrypting.value = false + logDecryptDebug('media-decrypt:complete', { + account: mediaAccount.value, + total: data.total, + success_count: data.success_count, + skip_count: data.skip_count, + fail_count: data.fail_count + }) closeMediaDecryptEventSource() } else if (data.type === 'error') { error.value = data.message + logDecryptDebug('media-decrypt:error-event', { + account: mediaAccount.value, + message: data.message + }) mediaDecrypting.value = false closeMediaDecryptEventSource() } @@ -1016,19 +1126,30 @@ const goToMediaDecryptStep = async () => { warning.value = '' // 校验并应用(未填写则允许直接进入,后端会使用已保存密钥或报错提示) const ok = applyManualKeys() + logDecryptDebug('media-step:apply-manual', { + account: mediaAccount.value, + ok, + manual: summarizeKeyStateForLog(manualKeys.xor_key, manualKeys.aes_key), + applied: summarizeKeyStateForLog(mediaKeys.xor_key, mediaKeys.aes_key), + errors: { ...manualKeyErrors } + }) if (!ok || manualKeyErrors.xor_key || manualKeyErrors.aes_key) return // 用户已输入 XOR 时,自动保存一次,避免下次重复输入(失败不影响继续) if (mediaKeys.xor_key) { try { const aesVal = String(mediaKeys.aes_key || '').trim() + logDecryptDebug('media-step:save-keys', { + account: mediaAccount.value, + keys: summarizeKeyStateForLog(mediaKeys.xor_key, aesVal) + }) await saveMediaKeys({ account: mediaAccount.value || null, xor_key: mediaKeys.xor_key, aes_key: aesVal ? aesVal : null }) } catch (e) { - // ignore + logDecryptDebug('media-step:save-keys-error', { account: mediaAccount.value, error: formatLogError(e) }) } } currentStep.value = 2 @@ -1040,6 +1161,10 @@ const skipToChat = async () => { const ok = applyManualKeys() if (ok && mediaKeys.xor_key) { const aesVal = String(mediaKeys.aes_key || '').trim() + logDecryptDebug('skip-chat:save-keys', { + account: mediaAccount.value, + keys: summarizeKeyStateForLog(mediaKeys.xor_key, aesVal) + }) await saveMediaKeys({ account: mediaAccount.value || null, xor_key: mediaKeys.xor_key, @@ -1047,7 +1172,7 @@ const skipToChat = async () => { }) } } catch (e) { - // ignore + logDecryptDebug('skip-chat:save-keys-error', { account: mediaAccount.value, error: formatLogError(e) }) } navigateTo('/chat') } @@ -1056,6 +1181,7 @@ const skipToChat = async () => { onMounted(async () => { if (process.client && typeof window !== 'undefined') { const selectedAccount = sessionStorage.getItem('selectedAccount') + logDecryptDebug('mounted:selected-account-raw', { raw: selectedAccount || '' }) if (selectedAccount) { try { const account = JSON.parse(selectedAccount) @@ -1068,9 +1194,14 @@ onMounted(async () => { } // 清除sessionStorage sessionStorage.removeItem('selectedAccount') + logDecryptDebug('mounted:selected-account-parsed', { + account_name: String(account.account_name || '').trim(), + data_dir: String(account.data_dir || '').trim() + }) await ensureKeysForAccount(mediaAccount.value) } catch (e) { console.error('解析账户信息失败:', e) + logDecryptDebug('mounted:selected-account-error', { error: formatLogError(e) }) } } } diff --git a/src/wechat_decrypt_tool/key_service.py b/src/wechat_decrypt_tool/key_service.py index c3d1e9c..706dcbc 100644 --- a/src/wechat_decrypt_tool/key_service.py +++ b/src/wechat_decrypt_tool/key_service.py @@ -30,6 +30,24 @@ from .media_helpers import _resolve_account_dir, _resolve_account_wxid_dir logger = logging.getLogger(__name__) +def _summarize_aes_key(value: Any) -> str: + raw = str(value or "").strip() + if not raw: + return "" + if len(raw) <= 8: + return raw + return f"{raw[:4]}...{raw[-4:]}(len={len(raw)})" + + +def _summarize_key_payload(payload: Optional[Dict[str, Any]]) -> Dict[str, Any]: + payload = payload or {} + return { + "wxid": str(payload.get("wxid") or "").strip(), + "xor_key": str(payload.get("xor_key") or "").strip(), + "aes_key": _summarize_aes_key(payload.get("aes_key")), + } + + def _resolve_wxid_dir_for_image_key( account: Optional[str] = None, *, @@ -40,6 +58,7 @@ def _resolve_wxid_dir_for_image_key( if explicit_wxid_dir: candidate = Path(explicit_wxid_dir).expanduser() if candidate.exists() and candidate.is_dir(): + logger.info("[image_key] 使用显式 wxid_dir: %s", str(candidate)) return candidate raise FileNotFoundError(f"指定的 wxid_dir 不存在或不是目录: {candidate}") @@ -50,19 +69,42 @@ def _resolve_wxid_dir_for_image_key( if db_storage_dir.name.lower() == "db_storage": candidate = db_storage_dir.parent if candidate.exists() and candidate.is_dir(): + logger.info( + "[image_key] 通过 db_storage_path 反推出 wxid_dir: db_storage_path=%s wxid_dir=%s", + str(db_storage_dir), + str(candidate), + ) return candidate nested_db_storage = db_storage_dir / "db_storage" if nested_db_storage.exists() and nested_db_storage.is_dir(): + logger.info( + "[image_key] db_storage_path 指向 wxid_dir,自动使用其子目录: wxid_dir=%s", + str(db_storage_dir), + ) return db_storage_dir + logger.info( + "[image_key] 提供的 db_storage_path 无法解析 wxid_dir: %s", + explicit_db_storage_path, + ) if account: try: account_dir = _resolve_account_dir(account) wx_id_dir = _resolve_account_wxid_dir(account_dir) if wx_id_dir: + logger.info( + "[image_key] 通过已解密账号目录解析 wxid_dir: account=%s account_dir=%s wxid_dir=%s", + str(account).strip(), + str(account_dir), + str(wx_id_dir), + ) return wx_id_dir - except Exception: - pass + except Exception as e: + logger.info( + "[image_key] 无法通过已解密账号目录解析 wxid_dir: account=%s error=%s", + str(account).strip(), + str(e), + ) raise FileNotFoundError("无法定位该账号的 wxid_dir,请传入有效的 db_storage_path 或先完成数据库解密") @@ -180,11 +222,13 @@ def get_wechat_internal_global_config(wx_dir: Path, file_name1) -> bytes: def try_get_local_image_keys() -> List[Dict[str, Any]]: """尝试通过本地算法提取图片密钥 (无需 Hook)""" if wx_key is None or not hasattr(wx_key, 'get_image_key'): + logger.info("[image_key] 本地算法不可用:wx_key.get_image_key 缺失") return [] try: res_json = wx_key.get_image_key() if not res_json: + logger.info("[image_key] 本地算法返回空结果") return [] data = json.loads(res_json) @@ -202,6 +246,11 @@ def try_get_local_image_keys() -> List[Dict[str, Any]]: "xor_key": f"0x{int(xor_key):02X}", "aes_key": aes_key }) + logger.info( + "[image_key] 本地算法完成:accounts=%s results=%s", + len(accounts), + [_summarize_key_payload(item) for item in results], + ) return results except Exception as e: logger.error(f"本地提取图片密钥失败: {e}") @@ -234,6 +283,14 @@ async def get_image_key_integrated_workflow( except Exception: target_account_wxid = account target_account_wxid = str(target_account_wxid or "").strip().lower() + logger.info( + "[image_key] 开始集成流程:request_account=%s target_wxid=%s local_key_count=%s db_storage_path=%s wxid_dir=%s", + str(account or "").strip(), + target_account_wxid, + len(local_keys), + str(db_storage_path or "").strip(), + str(wxid_dir or "").strip(), + ) if local_keys: # 如果指定了账号,尝试在本地结果中找匹配的 @@ -241,17 +298,29 @@ async def get_image_key_integrated_workflow( for k in local_keys: local_wxid = str(k.get("wxid") or "").strip().lower() if local_wxid and local_wxid == target_account_wxid: + logger.info( + "[image_key] 本地算法精确匹配成功:target_wxid=%s payload=%s", + target_account_wxid, + _summarize_key_payload(k), + ) upsert_account_keys_in_store( account=str(k.get("wxid") or "").strip(), image_xor_key=k['xor_key'], image_aes_key=k['aes_key'] ) return k + logger.info( + "[image_key] 本地算法未匹配到目标账号:target_wxid=%s local_wxids=%s", + target_account_wxid, + [str(item.get("wxid") or "").strip() for item in local_keys], + ) else: # 如果没指定账号,返回第一个发现的并存入 store (如果有的话) k = local_keys[0] - logger.info(f"本地算法提取成功 (未指定账号,返回首个): {k['wxid']}") - # logger.info(local_keys) + logger.info( + "[image_key] 未指定账号,返回本地首个结果:payload=%s", + _summarize_key_payload(k), + ) upsert_account_keys_in_store( account=k['wxid'], image_xor_key=k['xor_key'], @@ -260,7 +329,7 @@ async def get_image_key_integrated_workflow( return k # 2. 本地提取失败或不匹配,尝试远程解析 - logger.info("本地算法提取未命中,尝试远程 API 解析...") + logger.info("[image_key] 本地算法未命中,尝试远程 API 解析") return await fetch_and_save_remote_keys( account, wxid_dir=wxid_dir, @@ -284,13 +353,25 @@ async def fetch_and_save_remote_keys( url = "https://view.free.c3o.re/api/key" data = {"weixinIDFolder": wxid} - logger.info(f"正在为账号 {wxid} 获取云端备选图片密钥...") + logger.info( + "[image_key] 准备请求远程密钥:request_account=%s resolved_account=%s wxid_dir=%s db_storage_path=%s", + str(account or "").strip(), + wxid, + str(wx_id_dir), + str(db_storage_path or "").strip(), + ) try: blob1_bytes = get_wechat_internal_global_config(wx_id_dir, file_name1="global_config") blob2_bytes = get_wechat_internal_global_config(wx_id_dir, file_name1="global_config.crc") except Exception as e: raise RuntimeError(f"读取微信内部文件失败: {e}") + logger.info( + "[image_key] 远程请求输入文件已读取:wxid=%s global_config_bytes=%s crc_bytes=%s", + wxid, + len(blob1_bytes), + len(blob2_bytes), + ) files = { 'fileBytes': ('file', blob1_bytes, 'application/octet-stream'), @@ -298,7 +379,7 @@ async def fetch_and_save_remote_keys( } async with httpx.AsyncClient(timeout=30) as client: - logger.info("向云端 API 发送请求...") + logger.info("[image_key] 向云端 API 发送请求:url=%s wxid=%s", url, wxid) response = await client.post(url, data=data, files=files) if response.status_code != 200: @@ -307,6 +388,15 @@ async def fetch_and_save_remote_keys( config = response.json() if not config: raise RuntimeError("云端解析失败: 返回数据为空") + logger.info( + "[image_key] 收到远程响应:status_code=%s keys=%s nick_name=%s", + response.status_code, + { + "xor_key": str(config.get("xorKey", config.get("xor_key", ""))), + "aes_key": _summarize_aes_key(config.get("aesKey", config.get("aes_key", ""))), + }, + str(config.get("nickName", config.get("nick_name", ""))), + ) # 新 API 的字段兼容处理 xor_raw = str(config.get("xorKey", config.get("xor_key", ""))) @@ -326,6 +416,12 @@ async def fetch_and_save_remote_keys( image_xor_key=xor_hex_str, image_aes_key=aes_val ) + logger.info( + "[image_key] 远程密钥已保存:account=%s xor_key=%s aes_key=%s", + wxid, + xor_hex_str, + _summarize_aes_key(aes_val), + ) return { "wxid": wxid, diff --git a/src/wechat_decrypt_tool/routers/keys.py b/src/wechat_decrypt_tool/routers/keys.py index 754c7ec..7ede175 100644 --- a/src/wechat_decrypt_tool/routers/keys.py +++ b/src/wechat_decrypt_tool/routers/keys.py @@ -2,12 +2,23 @@ from typing import Optional from fastapi import APIRouter +from ..logging_config import get_logger from ..key_store import get_account_keys_from_store from ..key_service import get_db_key_workflow, get_image_key_integrated_workflow from ..media_helpers import _load_media_keys, _resolve_account_dir from ..path_fix import PathFixRoute router = APIRouter(route_class=PathFixRoute) +logger = get_logger(__name__) + + +def _summarize_aes_key(value: str) -> str: + raw = str(value or "").strip() + if not raw: + return "" + if len(raw) <= 8: + return raw + return f"{raw[:4]}...{raw[-4:]}(len={len(raw)})" @router.get("/api/keys", summary="获取账号已保存的密钥") @@ -23,6 +34,13 @@ async def get_saved_keys(account: Optional[str] = None): # 账号可能尚未解密;仍允许从全局 store 读取(如果传入了 account) account_name = str(account or "").strip() or None + logger.info( + "[keys] get_saved_keys start: request_account=%s resolved_account=%s account_dir=%s", + str(account or "").strip(), + str(account_name or ""), + str(account_dir) if account_dir else "", + ) + keys: dict = {} if account_name: keys = get_account_keys_from_store(account_name) @@ -45,6 +63,14 @@ async def get_saved_keys(account: Optional[str] = None): "image_aes_key": str(keys.get("image_aes_key") or "").strip(), "updated_at": str(keys.get("updated_at") or "").strip(), } + logger.info( + "[keys] get_saved_keys done: account=%s db_key_present=%s xor_key=%s aes_key=%s updated_at=%s", + str(account_name or ""), + bool(result["db_key"]), + result["image_xor_key"], + _summarize_aes_key(result["image_aes_key"]), + result["updated_at"], + ) return { "status": "success", @@ -101,11 +127,24 @@ async def get_image_key( 4. 解析返回流,自动存入本地数据库 """ try: + logger.info( + "[keys] get_image_key start: request_account=%s db_storage_path=%s wxid_dir=%s", + str(account or "").strip(), + str(db_storage_path or "").strip(), + str(wxid_dir or "").strip(), + ) result = await get_image_key_integrated_workflow( account, db_storage_path=db_storage_path, wxid_dir=wxid_dir, ) + logger.info( + "[keys] get_image_key done: request_account=%s response_account=%s xor_key=%s aes_key=%s", + str(account or "").strip(), + str(result.get("wxid") or "").strip(), + str(result.get("xor_key") or "").strip(), + _summarize_aes_key(str(result.get("aes_key") or "").strip()), + ) return { "status": 0, @@ -118,6 +157,12 @@ async def get_image_key( } } except FileNotFoundError as e: + logger.exception( + "[keys] get_image_key file missing: request_account=%s db_storage_path=%s wxid_dir=%s", + str(account or "").strip(), + str(db_storage_path or "").strip(), + str(wxid_dir or "").strip(), + ) return { "status": -1, "errmsg": f"文件缺失: {str(e)}", @@ -126,6 +171,12 @@ async def get_image_key( except Exception as e: import traceback traceback.print_exc() + logger.exception( + "[keys] get_image_key failed: request_account=%s db_storage_path=%s wxid_dir=%s", + str(account or "").strip(), + str(db_storage_path or "").strip(), + str(wxid_dir or "").strip(), + ) return { "status": -1, "errmsg": f"获取失败: {str(e)}", diff --git a/src/wechat_decrypt_tool/routers/media.py b/src/wechat_decrypt_tool/routers/media.py index cae8e99..c9806c6 100644 --- a/src/wechat_decrypt_tool/routers/media.py +++ b/src/wechat_decrypt_tool/routers/media.py @@ -27,6 +27,26 @@ logger = get_logger(__name__) router = APIRouter(route_class=PathFixRoute) +def _summarize_aes_key(value: Optional[str]) -> str: + raw = str(value or "").strip() + if not raw: + return "" + if len(raw) <= 8: + return raw + return f"{raw[:4]}...{raw[-4:]}(len={len(raw)})" + + +def _summarize_media_keys(*, xor_key: Optional[str] = None, aes_key: Optional[str] = None) -> dict: + xor_str = str(xor_key or "").strip() + aes_str = str(aes_key or "").strip() + return { + "xor_key": xor_str, + "aes_key": _summarize_aes_key(aes_str), + "has_xor": bool(xor_str), + "has_aes": bool(aes_str), + } + + class MediaKeysSaveRequest(BaseModel): """媒体密钥保存请求模型(用户手动提供)""" @@ -52,6 +72,12 @@ async def save_media_keys_api(request: MediaKeysSaveRequest): - aes_key: AES密钥(可选,至少16个字符;V4-V2需要) """ account_dir = _resolve_account_dir(request.account) + logger.info( + "[media] save_media_keys start: request_account=%s resolved_account=%s keys=%s", + str(request.account or "").strip(), + account_dir.name, + _summarize_media_keys(xor_key=request.xor_key, aes_key=request.aes_key), + ) # 解析XOR密钥 try: @@ -76,6 +102,11 @@ async def save_media_keys_api(request: MediaKeysSaveRequest): ) except Exception: pass + logger.info( + "[media] save_media_keys done: account=%s keys=%s", + account_dir.name, + _summarize_media_keys(xor_key=f"0x{xor_int:02X}", aes_key=aes_str[:16] if aes_str else ""), + ) return { "status": "success", @@ -99,6 +130,12 @@ async def decrypt_all_media(request: MediaDecryptRequest): """ account_dir = _resolve_account_dir(request.account) wxid_dir = _resolve_account_wxid_dir(account_dir) + logger.info( + "[media] decrypt_all start: request_account=%s resolved_account=%s provided_keys=%s", + str(request.account or "").strip(), + account_dir.name, + _summarize_media_keys(xor_key=request.xor_key, aes_key=request.aes_key), + ) if not wxid_dir: raise HTTPException( @@ -125,12 +162,28 @@ async def decrypt_all_media(request: MediaDecryptRequest): # 如果未提供密钥,尝试从缓存加载 if xor_key_int is None or aes_key16 is None: cached = _load_media_keys(account_dir) + logger.info( + "[media] decrypt_all cache lookup: account=%s cached_keys=%s", + account_dir.name, + _summarize_media_keys( + xor_key=f"0x{int(cached.get('xor')):02X}" if cached.get("xor") is not None else "", + aes_key=str(cached.get("aes") or "").strip(), + ), + ) if xor_key_int is None: xor_key_int = cached.get("xor") if aes_key16 is None: aes_str = str(cached.get("aes") or "").strip() if len(aes_str) >= 16: aes_key16 = aes_str[:16].encode("ascii", errors="ignore") + logger.info( + "[media] decrypt_all effective_keys: account=%s keys=%s", + account_dir.name, + _summarize_media_keys( + xor_key=f"0x{int(xor_key_int):02X}" if xor_key_int is not None else "", + aes_key=(aes_key16 or b"").decode("ascii", errors="ignore") if aes_key16 else "", + ), + ) if xor_key_int is None: raise HTTPException( @@ -267,6 +320,12 @@ async def decrypt_all_media_stream( account_dir = _resolve_account_dir(account) wxid_dir = _resolve_account_wxid_dir(account_dir) + logger.info( + "[media] decrypt_all_stream start: request_account=%s resolved_account=%s provided_keys=%s", + str(account or "").strip(), + account_dir.name, + _summarize_media_keys(xor_key=xor_key, aes_key=aes_key), + ) if not wxid_dir: yield f"data: {json.dumps({'type': 'error', 'message': '未找到微信数据目录'})}\n\n" @@ -292,12 +351,28 @@ async def decrypt_all_media_stream( # 如果未提供密钥,尝试从缓存加载 if xor_key_int is None or aes_key16 is None: cached = _load_media_keys(account_dir) + logger.info( + "[media] decrypt_all_stream cache lookup: account=%s cached_keys=%s", + account_dir.name, + _summarize_media_keys( + xor_key=f"0x{int(cached.get('xor')):02X}" if cached.get("xor") is not None else "", + aes_key=str(cached.get("aes") or "").strip(), + ), + ) if xor_key_int is None: xor_key_int = cached.get("xor") if aes_key16 is None: aes_str = str(cached.get("aes") or "").strip() if len(aes_str) >= 16: aes_key16 = aes_str[:16].encode("ascii", errors="ignore") + logger.info( + "[media] decrypt_all_stream effective_keys: account=%s keys=%s", + account_dir.name, + _summarize_media_keys( + xor_key=f"0x{int(xor_key_int):02X}" if xor_key_int is not None else "", + aes_key=(aes_key16 or b"").decode("ascii", errors="ignore") if aes_key16 else "", + ), + ) if xor_key_int is None: yield f"data: {json.dumps({'type': 'error', 'message': '未找到XOR密钥,请先使用 wx_key 获取并保存/填写'})}\n\n"