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

265 lines
16 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
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()