265 lines
16 KiB
Python
265 lines
16 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""按日期新增110项功能定义与逐项验收口径文档,绝不覆盖旧文档。"""
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import re
|
||
from collections import Counter
|
||
from datetime import datetime
|
||
from pathlib import Path
|
||
from typing import Any
|
||
|
||
ROOT = Path(__file__).resolve().parents[1]
|
||
MATRIX = ROOT / '开发文档/6、测试/wireless_frida_real_device_acceptance_20260518/真机110功能验收矩阵.json'
|
||
LATEST_BASE = ROOT / '开发文档/6、测试/real_device_110_effect_verify_20260518'
|
||
DOC_DIR = ROOT / '开发文档/6、测试/110项功能逐项确认/2026-05-18'
|
||
NOW = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||
STAMP = datetime.now().strftime('%H%M%S')
|
||
|
||
DETAILS = {
|
||
'browseChannels': ('微信视频号/频道浏览入口', '确认频道/视频号浏览能力在Hook或后端契约中存在,当前不做真实内容互动。'),
|
||
'changePassword': ('修改密码', '高风险账号安全能力,只确认接口存在和安全闸,不触发真实改密。'),
|
||
'checkAccountStatus': ('账号状态检查', '读取微信账号是否处于可用、登录或活跃状态,并返回版本与进程信息。'),
|
||
'createLabel': ('创建标签', '联系人标签写入能力,当前只做安全闸,避免真实改写标签。'),
|
||
'enableFingerprint': ('启用指纹', '账号/支付安全配置能力,只确认契约,不修改真实设置。'),
|
||
'generateMyQrCode': ('生成我的二维码', '确认个人二维码生成入口能力,避免暴露或外发敏感二维码。'),
|
||
'getContactInfo': ('读取单个联系人资料', '需要传入wxid后读取联系人详情;空参应返回缺少wxid。'),
|
||
'getContacts': ('读取通讯录好友列表', '读取联系人列表,并在有数据时逐项记录每个联系人读取成功。'),
|
||
'getContactsByLabel': ('按标签读取联系人', '按label_id读取标签下联系人,缺参时返回缺少label_id。'),
|
||
'getDeviceInfo': ('读取手机设备信息', '读取品牌、型号、Android版本、存储等真机信息。'),
|
||
'getFavorites': ('读取收藏', '尝试读取微信收藏数据;若数据库未命中要记录为空原因。'),
|
||
'getHookStatus': ('读取Hook状态', '确认SQLite、网络、好友请求、朋友圈等Hook模块是否激活。'),
|
||
'getLabels': ('读取联系人标签', '读取标签列表;若为空要标注账号未配置标签或路径未命中。'),
|
||
'getMessages': ('读取消息列表', '只读近期消息,必须避免发送/转发;有数据时逐条记录读取成功。'),
|
||
'getNetworkInfo': ('读取网络状态', '读取手机当前网络连接类型与Wi-Fi状态。'),
|
||
'getProcessInfo': ('读取微信进程信息', '读取微信PID、架构、模块数量、版本与Hook模块状态。'),
|
||
'getProfile': ('读取个人资料', '读取昵称、wxid、签名、地区等资料;空字段需排查登录态或数据库路径。'),
|
||
'getRecentMessages': ('读取最近消息', '只读最近会话消息,有数据时逐条记录。'),
|
||
'getRecentMiniPrograms': ('读取最近小程序', '确认最近小程序能力入口,当前以Intent/广播契约方式记录。'),
|
||
'getStorageInfo': ('读取存储信息', '读取手机总存储、剩余空间和使用率。'),
|
||
'getTransactionHistory': ('读取交易历史入口', '支付敏感能力,只记录安全入口,不读取真实交易明细。'),
|
||
'getVersionCompat': ('版本兼容检查', '确认当前微信版本是否在Hook适配版本列表内。'),
|
||
'getWalletBalance': ('读取钱包余额入口', '支付敏感能力,只做安全入口验证,不读取真实余额。'),
|
||
'getWechatVersion': ('读取微信版本', '读取当前微信版本号。'),
|
||
'globalSearch': ('微信全局搜索', '用安全关键词执行只读搜索,返回联系人/消息搜索结果。'),
|
||
'openMiniProgram': ('打开小程序入口', '通过安全UI动作打开微信/小程序入口,不触发真实外部业务。'),
|
||
'pinChat': ('置顶聊天', '聊天状态改写能力,当前只做安全闸,不真实置顶。'),
|
||
'ping': ('Hook心跳', '验证Frida RPC链路可达,返回Hook版本与模块摘要。'),
|
||
'receiveRedPacket': ('领取红包', '资金高风险能力,只做安全闸,不领取真实红包。'),
|
||
'receiveTransfer': ('收款/领取转账', '资金高风险能力,只做安全闸,不触发真实收款。'),
|
||
'scanQrCode': ('扫码入口', '确认扫码能力入口,不扫描真实二维码。'),
|
||
'searchContacts': ('搜索联系人', '用安全关键词搜索联系人,有结果时逐项记录。'),
|
||
'searchMessages': ('搜索消息', '用安全关键词搜索消息,有结果时逐条记录。'),
|
||
}
|
||
|
||
ROUTE_HINTS = {
|
||
'/devices': '设备列表/设备详情相关后端路由',
|
||
'/scan': '设备扫描后端路由',
|
||
'/system': '系统状态后端路由',
|
||
'/control': '控制命令后端路由',
|
||
'/projects': '项目管理后端路由',
|
||
'/logs': '日志查询后端路由',
|
||
'/tasks': '任务管理后端路由',
|
||
'/devices/{device_id}/health': '设备健康状态后端路由',
|
||
'/devices/{device_id}/heartbeat': '设备心跳后端路由',
|
||
'/devices/{device_id}/ui-tree': 'UI树读取后端路由',
|
||
'/guard-events': '守护事件后端路由',
|
||
'/heartbeat': '心跳看板/心跳事件后端路由',
|
||
'/actions': '动作清单后端路由',
|
||
'/deploy': '部署脚本后端路由',
|
||
'/account': '账号状态后端路由',
|
||
'/channels': '视频号/频道浏览后端路由',
|
||
'/device-info': '设备信息后端路由',
|
||
'/favorites': '收藏读取后端路由',
|
||
'/hook': 'Hook状态/进程/微信版本后端路由',
|
||
'/mini-program': '小程序后端路由',
|
||
}
|
||
|
||
PY_HINTS = {
|
||
'click': ('点击屏幕', '通过无线ADB下发点击坐标,验证真机UI响应。'),
|
||
'click_text': ('点击文本/输入安全文本', '通过无线ADB执行安全文本输入或文本相关UI动作。'),
|
||
'current_app': ('读取当前前台应用', '读取当前前台包名与Activity,确认微信是否在前台。'),
|
||
'get_device_info': ('读取设备信息', '后端设备信息函数契约验证。'),
|
||
'get_ui_tree': ('读取UI树', '后端或ADB读取当前UI层级结构摘要。'),
|
||
'input_text': ('输入文本', '通过无线ADB输入安全文本,不输入敏感内容。'),
|
||
'keyevent': ('按键事件', '通过无线ADB下发返回、唤醒等按键事件。'),
|
||
'launch_app': ('启动应用', '通过无线ADB启动指定应用,当前用于拉起微信。'),
|
||
'long_press': ('长按', '通过无线ADB执行长按安全动作。'),
|
||
'scroll': ('滚动', '通过无线ADB执行安全滚动动作。'),
|
||
'screenshot': ('截图', '通过无线ADB统一采集最终画面截图。'),
|
||
'swipe': ('滑动', '通过无线ADB执行滑动动作。'),
|
||
'connect_device': ('连接设备', '后端设备连接管理契约。'),
|
||
'disconnect_device': ('断开设备', '后端设备断开管理契约。'),
|
||
'discover_devices': ('发现设备', '扫描无线ADB/设备在线状态。'),
|
||
'execute_batch': ('批量执行', '批量命令高风险入口,只做契约与安全控制。'),
|
||
'execute_command': ('执行命令', '命令执行入口,仅记录受控契约,不执行危险命令。'),
|
||
'execute_script': ('执行脚本', '脚本执行入口,仅记录契约与安全边界。'),
|
||
'frida_status': ('Frida状态', '读取Frida服务/Hook在线状态。'),
|
||
'generate_deploy_script': ('生成部署脚本', '生成无线部署脚本契约验证。'),
|
||
'get_install_command': ('获取安装命令', '返回部署/安装命令契约。'),
|
||
'list_devices': ('列出设备', '列出已接入设备。'),
|
||
'list_supported_actions': ('列出支持动作', '列出后端支持的控制动作。'),
|
||
'queue_ai_task': ('AI任务入队', '任务队列契约验证,不触发真实外发。'),
|
||
'ai_execute': ('AI执行入口', 'AI控制入口契约验证。'),
|
||
'all_health_events': ('全部健康事件', '读取所有设备健康事件契约。'),
|
||
'device_health': ('设备健康', '读取设备健康状态。'),
|
||
'device_health_events': ('设备健康事件', '读取设备健康事件列表。'),
|
||
'get_ai_brain_status': ('AI大脑状态', '读取AI服务状态契约。'),
|
||
'get_all_guard_events': ('全部守护事件', '读取守护事件列表。'),
|
||
'get_device': ('读取设备详情', '读取指定设备详情。'),
|
||
'get_device_heartbeat': ('设备心跳', '读取设备心跳状态。'),
|
||
'get_devices': ('设备列表', '读取设备列表。'),
|
||
'get_guard_events': ('守护事件', '读取指定守护事件。'),
|
||
'heartbeat_dashboard': ('心跳看板', '读取心跳看板统计。'),
|
||
}
|
||
|
||
HIGH_RISK_RE = re.compile(r'add|accept|delete|remove|post|send|forward|revoke|register|login|logout|switch|invite|quit|set|clear|follow|unfollow|like|comment|share|batchExecute|getSimPhone|moments|friend|group|official|phone|sim|feedback|report|unblock|支付|转账|红包|群发|朋友圈|解封|添加|手机号|删除|清空|举报|反馈|外发|改写', re.I)
|
||
READ_RE = re.compile(r'^(get|list|status|info|ping|check|probe|current|health|ready|devices|projects|logs|screenshot|dump|globalSearch|find|search)', re.I)
|
||
|
||
|
||
def load_json(path: Path) -> Any:
|
||
return json.loads(path.read_text(encoding='utf-8', errors='replace'))
|
||
|
||
|
||
def latest_result_dir() -> Path | None:
|
||
dirs = [p for p in LATEST_BASE.iterdir() if p.is_dir()] if LATEST_BASE.exists() else []
|
||
return max(dirs, key=lambda p: p.stat().st_mtime) if dirs else None
|
||
|
||
|
||
def result_map(latest: Path | None) -> dict[int, dict[str, Any]]:
|
||
if not latest:
|
||
return {}
|
||
p = latest / 'real_device_110_effect_results.json'
|
||
if not p.exists():
|
||
return {}
|
||
data = load_json(p)
|
||
return {int(r['idx']): r for r in data.get('results', []) if int(r.get('idx', 0)) > 0}
|
||
|
||
|
||
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 split_name(name: str) -> str:
|
||
if name.startswith('GET '):
|
||
return name
|
||
return re.sub(r'([a-z0-9])([A-Z])', r'\1 \2', name).replace('_', ' ')
|
||
|
||
|
||
def mode(row: dict[str, Any], res: dict[str, Any]) -> str:
|
||
if res.get('execution_mode'):
|
||
return res['execution_mode']
|
||
name = row['name']
|
||
risk = row.get('risk_level', '')
|
||
if HIGH_RISK_RE.search(name) or '高风险' in risk or name == 'batchExecute':
|
||
return '安全闸/空参数验证'
|
||
if row.get('source_type') == 'frida_rpc' and (READ_RE.search(name) or name == 'ping'):
|
||
return '无线Frida真机RPC只读验证'
|
||
return '后端契约或无线ADB安全UI验证'
|
||
|
||
|
||
def define(row: dict[str, Any]) -> tuple[str, str, str]:
|
||
name = row['name']
|
||
if name in DETAILS:
|
||
title, detail = DETAILS[name]
|
||
elif name.startswith('GET '):
|
||
route = name.split(' ', 1)[1]
|
||
title = '后端路由:' + route
|
||
detail = '验证' + next((v for k, v in ROUTE_HINTS.items() if route.startswith(k)), '后端接口契约') + ',重点确认路由存在、日志可记录、不会伪装成真实业务动作。'
|
||
elif name in PY_HINTS:
|
||
title, detail = PY_HINTS[name]
|
||
elif name.startswith('get_') or name.startswith('get'):
|
||
title = '读取类能力:' + split_name(name)
|
||
detail = '读取状态、列表、设备或业务资料;必须记录返回摘要,若为空需要写清为空原因。'
|
||
elif name.startswith('list_') or name.startswith('list'):
|
||
title = '列表类能力:' + split_name(name)
|
||
detail = '读取后端或设备列表数据,验证列表契约与日志记录。'
|
||
elif name.startswith('execute'):
|
||
title = '执行类能力:' + split_name(name)
|
||
detail = '命令执行入口,必须受安全边界约束,当前只验证契约和日志。'
|
||
else:
|
||
title = '项目能力:' + split_name(name)
|
||
detail = '验证项目中该能力的接口、源码定位、后端执行日志和安全边界。'
|
||
risk = row.get('risk_level', '')
|
||
if HIGH_RISK_RE.search(name) or '高风险' in risk or '待人工确认' in risk:
|
||
boundary = '先不触发真实外发、支付、添加、删除、改写或账号安全动作,只允许空参数、安全闸、源码定位和日志验证。'
|
||
elif '只读' in risk:
|
||
boundary = '允许真机只读验证,必须有返回摘要、后端日志和手机最终状态。'
|
||
elif 'UI' in risk:
|
||
boundary = '允许无线ADB安全UI动作,不发送、不确认、不提交真实业务。'
|
||
else:
|
||
boundary = '按最小风险原则执行,先确认契约与日志,再决定是否进入真机动作。'
|
||
return title, detail, boundary
|
||
|
||
|
||
def main() -> None:
|
||
DOC_DIR.mkdir(parents=True, exist_ok=True)
|
||
rows = load_json(MATRIX)[:110]
|
||
latest = latest_result_dir()
|
||
results = result_map(latest)
|
||
path = DOC_DIR / f'110项功能定义与验收口径_追加版_{STAMP}.md'
|
||
counts = Counter(row.get('source_type', '') for row in rows)
|
||
risk_counts = Counter(row.get('risk_level', '') for row in rows)
|
||
lines = [
|
||
'# 110项功能定义与逐项验收口径(按日期追加版)',
|
||
'',
|
||
f'生成时间:{NOW}',
|
||
'',
|
||
'## 文档边界',
|
||
'',
|
||
'本文件是新增文档,不覆盖任何已有开发文档。它用于先把110项功能“到底是什么”说清楚,再按无线Frida、无线ADB和后端契约三类口径逐项确认。',
|
||
'',
|
||
'## 分类统计',
|
||
'',
|
||
'| 分类 | 数量 |',
|
||
'|---|---:|',
|
||
]
|
||
for k, v in counts.items():
|
||
lines.append(f'| {safe(k)} | {v} |')
|
||
lines += ['', '| 风险口径 | 数量 |', '|---|---:|']
|
||
for k, v in risk_counts.items():
|
||
lines.append(f'| {safe(k)} | {v} |')
|
||
lines += [
|
||
'',
|
||
'## 逐项功能定义与验收口径',
|
||
'',
|
||
'| 序号 | 功能名 | 确认后的中文功能 | 功能说明 | 风险边界 | 当前测试方式 | 最新完成状态 | 下一步复核点 |',
|
||
'|---:|---|---|---|---|---|---|---|',
|
||
]
|
||
for row in rows:
|
||
idx = int(row['id'])
|
||
res = results.get(idx, {})
|
||
title, detail, boundary = define(row)
|
||
m = mode(row, res)
|
||
status = res.get('status', '待复核')
|
||
if row['name'] in {'getContacts', 'getProfile', 'getMessages', 'getRecentMessages', 'searchContacts', 'searchMessages'}:
|
||
next_step = '补资料明细:有数据逐项写成功;空数据必须写明登录态/数据库/权限原因。'
|
||
elif 'lock' in row['name'].lower() or row['name'] in {'keyevent', 'launch_app', 'current_app', 'screenshot'}:
|
||
next_step = '继续纳入锁屏、亮屏、后台可控链路复核。'
|
||
elif HIGH_RISK_RE.search(row['name']) or '待人工确认' in row.get('risk_level', ''):
|
||
next_step = '保持安全闸;如要真实执行必须由卡若明确给测试对象和确认。'
|
||
else:
|
||
next_step = '保留当前证据,下一轮重跑时同步后端日志与最终截图索引。'
|
||
lines.append(f"| {idx} | `{safe(row['name'])}` | {safe(title)} | {safe(detail)} | {safe(boundary)} | {safe(m)} | {safe(status)} | {safe(next_step)} |")
|
||
lines += [
|
||
'',
|
||
'## 继续执行顺序',
|
||
'',
|
||
'下一步不是直接覆盖旧报告,而是在本日期目录继续新增复核文档。执行顺序为:先复核资料读取类接口是否能读取真实明细,再复核锁屏/后台/亮屏控制链路,最后重跑110项并把新证据追加到项目落地执行表。',
|
||
'',
|
||
'## 证据来源',
|
||
'',
|
||
f'| 证据 | 路径 |\n|---|---|\n| 110项矩阵 | `{MATRIX}` |\n| 最新真机报告目录 | `{latest if latest else "待生成"}` |\n| 最新结果JSON | `{latest / "real_device_110_effect_results.json" if latest else "待生成"}` |',
|
||
'',
|
||
]
|
||
path.write_text('\n'.join(lines) + '\n', encoding='utf-8')
|
||
print(json.dumps({'path': str(path), 'count': len(rows), 'source_type_counts': counts, 'risk_counts': risk_counts}, ensure_ascii=False, indent=2))
|
||
|
||
if __name__ == '__main__':
|
||
main()
|