improvement(log): 补充图片密钥与媒体解密链路调试日志

- 为解密页关键阶段补充调试日志
- 为图片密钥获取流程补充请求、命中和回退日志
- 为媒体密钥保存与批量解密补充有效密钥日志
- 对 AES 信息做摘要输出,便于排查且避免完整泄露
This commit is contained in:
2977094657
2026-04-13 18:11:25 +08:00
Unverified
parent f5c3fed181
commit 839949a544
4 changed files with 364 additions and 11 deletions
+135 -4
View File
@@ -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) })
}
}
}
+103 -7
View File
@@ -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,
+51
View File
@@ -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)}",
+75
View File
@@ -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"