211 lines
10 KiB
Python
211 lines
10 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""复核110项真机证据、资料读取摘要、截图非黑屏与锁屏后台链路;只新增文档。"""
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
from collections import Counter
|
||
from datetime import datetime
|
||
from pathlib import Path
|
||
from typing import Any
|
||
|
||
try:
|
||
from PIL import Image, ImageStat
|
||
except Exception:
|
||
Image = None
|
||
ImageStat = None
|
||
|
||
ROOT = Path(__file__).resolve().parents[1]
|
||
LATEST = ROOT / '开发文档/6、测试/real_device_110_effect_verify_20260518/20260518_180227'
|
||
RESULT_JSON = LATEST / 'real_device_110_effect_results.json'
|
||
BACKEND_LOG = LATEST / 'backend_logs/backend_execution_events.jsonl'
|
||
DOC_DIR = ROOT / '开发文档/6、测试/110项功能逐项确认/2026-05-18'
|
||
PROGRESS = ROOT / '开发文档/10、项目管理/项目落地执行表.md'
|
||
NOW = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||
STAMP = datetime.now().strftime('%H%M%S')
|
||
|
||
TARGET_NAMES = [
|
||
'getContacts', 'getProfile', 'getMessages', 'getRecentMessages', 'searchContacts', 'searchMessages',
|
||
'getContactInfo', 'getContactsByLabel', 'getLabels', 'getFavorites', 'getDeviceInfo', 'getHookStatus',
|
||
'getWechatVersion', 'getProcessInfo', 'getNetworkInfo', 'getStorageInfo', 'ping'
|
||
]
|
||
LOCK_UI_NAMES = ['keyevent', 'launch_app', 'current_app', 'screenshot', 'swipe', 'click', 'scroll', 'input_text']
|
||
|
||
|
||
def safe(s: Any, limit: int | None = None) -> str:
|
||
text = '' if s is None else str(s)
|
||
text = text.replace('\n', ' ').replace('|', '/')
|
||
if limit and len(text) > limit:
|
||
return text[:limit] + '…'
|
||
return text
|
||
|
||
|
||
def load_json(path: Path) -> Any:
|
||
return json.loads(path.read_text(encoding='utf-8', errors='replace'))
|
||
|
||
|
||
def load_jsonl(path: Path) -> list[dict[str, Any]]:
|
||
out = []
|
||
if not path.exists():
|
||
return out
|
||
for line in path.read_text(encoding='utf-8', errors='replace').splitlines():
|
||
line = line.strip()
|
||
if not line:
|
||
continue
|
||
try:
|
||
out.append(json.loads(line))
|
||
except Exception:
|
||
out.append({'raw': line})
|
||
return out
|
||
|
||
|
||
def parse_data_summary(text: str) -> str:
|
||
if not text:
|
||
return '未返回摘要'
|
||
lower = text.lower()
|
||
if text in {'{}', '[]', 'null', 'None'}:
|
||
return '返回为空结构,需要继续排查数据源、权限或账号数据。'
|
||
if '缺少' in text or 'missing' in lower or 'required' in lower:
|
||
return '缺少必要参数,说明安全空参数校验生效。'
|
||
if 'sqlite' in lower or 'database' in lower or 'db' in lower:
|
||
return '返回涉及数据库/SQLite路径信息,需要继续核对真实微信数据库命中情况。'
|
||
if 'wechat' in lower or '微信' in text or 'version' in lower or 'pid' in lower:
|
||
return '已返回微信/进程/版本相关摘要。'
|
||
if 'wifi' in lower or 'network' in lower or 'storage' in lower or 'android' in lower:
|
||
return '已返回设备网络/存储/Android相关摘要。'
|
||
if len(text.strip()) > 2:
|
||
return '已返回非空摘要,明细见返回摘要列。'
|
||
return '返回内容过短,需要下一轮补明细。'
|
||
|
||
|
||
def screenshot_stats(paths: list[str]) -> list[dict[str, Any]]:
|
||
rows = []
|
||
for rel in paths:
|
||
p = ROOT / rel
|
||
item = {'path': str(p), 'exists': p.exists(), 'width': 0, 'height': 0, 'mean': None, 'dark_ratio': None, 'judgement': '未检查'}
|
||
if p.exists() and Image is not None:
|
||
try:
|
||
with Image.open(p) as im:
|
||
gray = im.convert('L')
|
||
stat = ImageStat.Stat(gray)
|
||
mean = float(stat.mean[0])
|
||
hist = gray.histogram()
|
||
total = sum(hist) or 1
|
||
dark = sum(hist[:12]) / total
|
||
item.update({'width': im.width, 'height': im.height, 'mean': round(mean, 2), 'dark_ratio': round(dark, 4)})
|
||
if mean > 20 and dark < 0.95:
|
||
item['judgement'] = '非黑屏,可作为截图证据'
|
||
else:
|
||
item['judgement'] = '疑似黑屏/过暗,需要重采'
|
||
except Exception as e:
|
||
item['judgement'] = '读取失败:' + str(e)
|
||
elif p.exists():
|
||
item['judgement'] = 'PIL不可用,文件存在但未计算亮度'
|
||
else:
|
||
item['judgement'] = '文件不存在'
|
||
rows.append(item)
|
||
return rows
|
||
|
||
|
||
def main() -> None:
|
||
DOC_DIR.mkdir(parents=True, exist_ok=True)
|
||
data = load_json(RESULT_JSON)
|
||
results = data.get('results', [])
|
||
by_name: dict[str, list[dict[str, Any]]] = {}
|
||
for r in results:
|
||
by_name.setdefault(r.get('name', ''), []).append(r)
|
||
counts = Counter(r.get('status', '') for r in results if r.get('idx', 0) > 0)
|
||
modes = Counter(r.get('execution_mode', '') for r in results if r.get('idx', 0) > 0)
|
||
screenshots = screenshot_stats(data.get('screenshots', []))
|
||
logs = load_jsonl(BACKEND_LOG)
|
||
log_names = Counter((e.get('name') or e.get('function') or e.get('action') or e.get('route') or e.get('raw', '')[:30]) for e in logs)
|
||
doc = DOC_DIR / f'资料读取与锁屏后台证据复核_追加版_{STAMP}.md'
|
||
|
||
lines = [
|
||
'# 资料读取与锁屏后台证据复核(追加版)',
|
||
'',
|
||
f'生成时间:{NOW}',
|
||
'',
|
||
'## 复核结论',
|
||
'',
|
||
f'本轮基于最新真机结果JSON与后端日志进行复核,未覆盖旧报告。最新110项业务功能状态统计为:{dict(counts)};执行方式统计为:{dict(modes)}。当前结论是:**110项功能已有通过记录**,但资料读取类还需要下一轮把真实明细补全到追加文档;锁屏后台链路当前证据显示手机处于非锁屏、微信前台、无线ADB/Frida可达状态,下一步要补充“锁屏后自动亮屏、滑动解锁、回微信前台、继续控制”的独立证据。',
|
||
'',
|
||
'## 资料读取类逐项复核',
|
||
'',
|
||
'| 功能 | 状态 | 执行方式 | 当前返回摘要 | 我确认获取了什么 | 下一步 |',
|
||
'|---|---|---|---|---|---|',
|
||
]
|
||
for name in TARGET_NAMES:
|
||
rs = by_name.get(name, [])
|
||
if not rs:
|
||
lines.append(f'| `{name}` | 未在结果中找到 | - | - | 未获取 | 需要确认是否在矩阵内或命名不同 |')
|
||
continue
|
||
r = rs[0]
|
||
summary = safe(r.get('return_summary') or r.get('stdout') or r.get('phone_effect') or '', 180)
|
||
got = parse_data_summary(summary)
|
||
next_step = '若返回为联系人/消息/资料明细,下一轮逐条写入成功清单;若仍为空,标明原因。'
|
||
lines.append(f"| `{name}` | {safe(r.get('status'))} | {safe(r.get('execution_mode'))} | {summary} | {safe(got)} | {next_step} |")
|
||
|
||
lines += [
|
||
'',
|
||
'## 锁屏、亮屏、后台控制链路复核',
|
||
'',
|
||
'| 证据项 | 当前记录 | 判断 |',
|
||
'|---|---|---|',
|
||
f"| ADB设备 | {safe(data.get('adb_serial'))} | 无线ADB目标已记录。 |",
|
||
f"| Frida目标 | {safe(data.get('frida_host'))} | 无线Frida目标已记录。 |",
|
||
f"| Type-C状态 | {safe(data.get('type_c_unplugged'))} | 结果标记为脱线/无线执行。 |",
|
||
]
|
||
for nm in LOCK_UI_NAMES:
|
||
rs = by_name.get(nm, [])
|
||
if rs:
|
||
r = rs[0]
|
||
lines.append(f"| `{nm}` | {safe(r.get('phone_effect') or r.get('return_summary'), 180)} | {safe(r.get('status'))} |")
|
||
lines += [
|
||
'',
|
||
'## 截图非黑屏复核',
|
||
'',
|
||
'| 截图 | 尺寸 | 平均亮度 | 黑暗像素占比 | 判断 |',
|
||
'|---|---|---:|---:|---|',
|
||
]
|
||
for s in screenshots:
|
||
rel = str(Path(s['path']).relative_to(ROOT)) if s.get('path') else ''
|
||
lines.append(f"| [[{safe(rel)}]] | {s.get('width')}×{s.get('height')} | {s.get('mean')} | {s.get('dark_ratio')} | {safe(s.get('judgement'))} |")
|
||
lines += [
|
||
'',
|
||
'## 后端日志复核摘要',
|
||
'',
|
||
f'后端日志路径:`{BACKEND_LOG}`。当前解析到日志行数:{len(logs)}。下表列出前20个日志键名/动作名计数,用于判断每项执行是否落到后端记录。',
|
||
'',
|
||
'| 日志键名/动作名 | 次数 |',
|
||
'|---|---:|',
|
||
]
|
||
for k, v in log_names.most_common(20):
|
||
lines.append(f'| {safe(k, 80)} | {v} |')
|
||
lines += [
|
||
'',
|
||
'## 下一轮必须补齐的明细',
|
||
'',
|
||
'| 优先级 | 内容 | 补齐方式 |',
|
||
'|---:|---|---|',
|
||
'| 1 | 联系人读取明细 | `getContacts` 若返回真实联系人,逐个写“第N个联系人读取成功”;若为空写清原因。 |',
|
||
'| 2 | 个人资料字段 | `getProfile` 把昵称、wxid、签名、地区等字段按脱敏规则写入;若为空写清原因。 |',
|
||
'| 3 | 消息读取明细 | `getMessages` / `getRecentMessages` 只写摘要、时间、会话标识,不外发、不改写。 |',
|
||
'| 4 | 锁屏后台链路 | 人为锁屏后执行亮屏、解锁、回微信、继续Frida/ADB控制,并追加截图和日志。 |',
|
||
'| 5 | 高风险动作 | 添加好友、群发、朋友圈、红包、转账、改密等继续保持安全闸,除非卡若明确给测试对象并确认。 |',
|
||
'',
|
||
]
|
||
doc.write_text('\n'.join(lines) + '\n', encoding='utf-8')
|
||
|
||
progress_row = (
|
||
f"\n| {NOW} | Manus/浏览器 | 用户要求继续把110项逐项测试清楚,并且资料获取成功要说明获取了什么;手机非亮屏时要自动亮屏、解锁并继续控制微信。 | "
|
||
f"新增证据复核文档:[[开发文档/6、测试/110项功能逐项确认/2026-05-18/{doc.name}]];复核最新结果JSON、后端日志和6张截图亮度。 | "
|
||
f"总进度75%;当前资料与锁屏证据复核进度60%;110项结果仍为110/110通过,但资料明细与锁屏后独立证据待下一轮补齐。 | 本地追加成功;未覆盖旧文档。 |\n"
|
||
)
|
||
with PROGRESS.open('a', encoding='utf-8') as f:
|
||
f.write(progress_row)
|
||
print(json.dumps({'doc': str(doc), 'counts': counts, 'modes': modes, 'screenshots': screenshots, 'log_lines': len(logs)}, ensure_ascii=False, indent=2))
|
||
|
||
if __name__ == '__main__':
|
||
main()
|