Files
workphone-sdk/tools/analyze_real_device_evidence_append_doc.py
2026-05-18 18:21:57 +08:00

211 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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()