229 lines
11 KiB
Python
229 lines
11 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""生成工作手机110项功能清单与只追加开发文档。"""
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import re
|
||
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'
|
||
PROGRESS = ROOT / '开发文档/10、项目管理/项目落地执行表.md'
|
||
|
||
NOW = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||
|
||
FUNC_DESC = {
|
||
'browse': '浏览或读取相关页面/内容状态',
|
||
'change': '修改类能力,当前仅做安全闸或契约确认',
|
||
'check': '检查账号、环境或兼容状态',
|
||
'create': '创建类能力,当前仅做安全闸或契约确认',
|
||
'enable': '启用配置类能力,当前仅做安全闸或契约确认',
|
||
'generate': '生成类能力,当前仅做接口存在和安全边界确认',
|
||
'get': '读取资料、设备、消息、状态或列表数据',
|
||
'global': '全局搜索类只读能力',
|
||
'open': '打开应用、页面或小程序的安全UI能力',
|
||
'pin': '置顶/固定类改写能力,当前仅做安全闸',
|
||
'ping': 'Hook连通性心跳验证',
|
||
'receive': '领取类高风险能力,当前只做安全闸',
|
||
'scan': '扫码入口能力,当前只做契约/安全验证',
|
||
'search': '搜索联系人或消息的只读能力',
|
||
'click': 'ADB点击类安全UI能力',
|
||
'current': '读取当前应用或页面状态',
|
||
'input': 'ADB输入类安全UI能力',
|
||
'keyevent': 'ADB按键控制能力',
|
||
'launch': '启动应用能力',
|
||
'long': '长按类UI能力',
|
||
'scroll': '滚动类UI能力',
|
||
'screenshot': '截图采集能力',
|
||
'swipe': '滑动类UI能力',
|
||
'connect': '设备连接管理能力',
|
||
'disconnect': '设备断开管理能力',
|
||
'discover': '设备发现能力',
|
||
'execute': '命令或批量执行能力',
|
||
'frida': 'Frida服务状态或Hook状态能力',
|
||
'list': '列表读取能力',
|
||
'queue': '任务入队或异步执行记录能力',
|
||
}
|
||
|
||
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 split_name(name: str) -> str:
|
||
if ' ' in name or '/' in name:
|
||
return name
|
||
s = re.sub(r'([a-z0-9])([A-Z])', r'\1 \2', name).replace('_', ' ')
|
||
return s.strip()
|
||
|
||
|
||
def infer_desc(name: str, source_type: str, risk: str) -> str:
|
||
lname = name.lower()
|
||
if name.startswith('GET '):
|
||
return '后端GET路由契约与状态读取验证。'
|
||
for prefix, desc in FUNC_DESC.items():
|
||
if lname.startswith(prefix):
|
||
return desc + '。'
|
||
if source_type == 'api_route':
|
||
return '后端API路由契约验证。'
|
||
return '项目能力接口契约、执行日志与安全边界验证。'
|
||
|
||
|
||
def infer_mode(row: dict[str, Any]) -> str:
|
||
name = row.get('name', '')
|
||
risk = row.get('risk_level', '')
|
||
source_type = row.get('source_type', '')
|
||
if HIGH_RISK_RE.search(name) or '高风险' in risk or name in {'batchExecute'}:
|
||
return '安全闸/空参数验证'
|
||
if source_type == 'frida_rpc' and (READ_RE.search(name) or name == 'ping'):
|
||
return '无线Frida真机RPC只读验证'
|
||
if 'UI' in risk or any(k in name.lower() for k in ['tap', 'swipe', 'input', 'key', 'open', 'launch', 'back', 'scroll', 'click']):
|
||
return '无线ADB真机UI安全操作'
|
||
return '后端契约日志验证'
|
||
|
||
|
||
def load_json(path: Path) -> Any:
|
||
return json.loads(path.read_text(encoding='utf-8', errors='replace'))
|
||
|
||
|
||
def latest_result_dir() -> Path | None:
|
||
if not LATEST_BASE.exists():
|
||
return None
|
||
dirs = [p for p in LATEST_BASE.iterdir() if p.is_dir()]
|
||
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)
|
||
out: dict[int, dict[str, Any]] = {}
|
||
for r in data.get('results', []):
|
||
try:
|
||
idx = int(r.get('idx', 0))
|
||
except Exception:
|
||
continue
|
||
if idx > 0:
|
||
out[idx] = r
|
||
return out
|
||
|
||
|
||
def escape_pipe(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 main() -> None:
|
||
DOC_DIR.mkdir(parents=True, exist_ok=True)
|
||
rows = load_json(MATRIX)[:110]
|
||
latest = latest_result_dir()
|
||
results = result_map(latest)
|
||
latest_rel = latest.relative_to(ROOT) if latest else ''
|
||
report_rel = latest_rel / '真实脱线手机110功能最终效果验证报告.md' if latest else ''
|
||
|
||
md_path = DOC_DIR / '110项功能逐项确认清单_追加版.md'
|
||
csv_path = DOC_DIR / '110项功能逐项确认清单_追加版.csv'
|
||
|
||
lines = [
|
||
'# 工作手机110项功能逐项确认清单(只追加版)',
|
||
'',
|
||
f'生成时间:{NOW}',
|
||
'',
|
||
'## 追加原则',
|
||
'',
|
||
'本文件为按日期新增的追加文档,不覆盖项目中已经开发好的任何文档。后续每轮验收只在本日期目录下新增文件,或在项目落地执行表末尾追加记录。',
|
||
'',
|
||
'## 当前确认口径',
|
||
'',
|
||
f'本轮从验收矩阵读取前110项功能,并合并最新真机验证结果目录:`{escape_pipe(latest_rel)}`。其中只读项优先通过无线Frida RPC验证,安全UI项通过无线ADB验证,高风险外发或改写项只允许安全闸与空参数契约验证,不触发真实添加好友、群发、朋友圈、解封、支付、删除或资料改写。',
|
||
'',
|
||
'## 110项功能总表',
|
||
'',
|
||
'| 序号 | 功能名 | 中文拆解 | 功能要做什么 | 类型 | 风险/边界 | 测试方式 | 最新状态 | 结果断言 | 证据链接 |',
|
||
'|---:|---|---|---|---|---|---|---|---|---|',
|
||
]
|
||
csv_lines = ['序号,功能名,中文拆解,功能要做什么,类型,风险边界,测试方式,最新状态,结果断言,返回摘要']
|
||
for row in rows:
|
||
idx = int(row.get('id', 0))
|
||
name = row.get('name', '')
|
||
res = results.get(idx, {})
|
||
status = res.get('status', '待逐项复核')
|
||
assertion = res.get('assertion') or row.get('acceptance_assertion', '')
|
||
mode = res.get('execution_mode') or infer_mode(row)
|
||
desc = infer_desc(name, row.get('source_type', ''), row.get('risk_level', ''))
|
||
risk = row.get('risk_level', '')
|
||
evidence = f'[[{report_rel}]]' if report_rel else '待生成'
|
||
lines.append(f"| {idx} | `{escape_pipe(name)}` | {escape_pipe(split_name(name))} | {escape_pipe(desc)} | {escape_pipe(row.get('source_type',''))} | {escape_pipe(risk)} | {escape_pipe(mode)} | {escape_pipe(status)} | {escape_pipe(assertion, 90)} | {evidence} |")
|
||
vals = [idx, name, split_name(name), desc, row.get('source_type',''), risk, mode, status, assertion, res.get('return_summary','')]
|
||
csv_lines.append(','.join('"' + escape_pipe(v).replace('"', '""') + '"' for v in vals))
|
||
|
||
lines += [
|
||
'',
|
||
'## 逐项完成原则',
|
||
'',
|
||
'后续执行时,先确认功能含义和风险边界,再确认测试方式,最后记录真机证据。读取资料类能力必须把成功读取到的明细写入后端日志和本追加文档;如果返回为空,必须标注为空的原因,例如微信账号资料为空、数据库路径未命中、权限不足或安全策略限制。',
|
||
'',
|
||
'## 当前待继续确认事项',
|
||
'',
|
||
'| 事项 | 当前判断 | 下一步 |',
|
||
'|---|---|---|',
|
||
'| 截图可见性 | 最新截图已可见,至少微信首页截图不是黑屏 | 继续抽查6张截图并把可见性写入下一轮证据 |',
|
||
'| 资料获取明细 | `getContacts`、`getProfile`、`getMessages` 曾返回空字段或空列表 | 先确认微信登录态与SQLite路径,再补充逐项成功/为空原因 |',
|
||
'| 锁屏后台控制 | 脚本已加入唤醒、滑动解锁、回到微信前台 | 继续验证锁屏后Frida进程可控和ADB自动亮屏链路 |',
|
||
'| 110项逐项完成 | 最新报告显示110/110通过,但需要按功能名逐项解释清楚 | 以本清单为准逐项复核并追加状态,不覆盖旧文档 |',
|
||
'',
|
||
'## 关联文件',
|
||
'',
|
||
'| 文件 | 说明 |',
|
||
'|---|---|',
|
||
f'| `{MATRIX}` | 110项验收矩阵源文件 |',
|
||
f'| `{latest / "真实脱线手机110功能最终效果验证报告.md" if latest else "待生成"}` | 最新110项真机验收报告 |',
|
||
f'| `{latest / "backend_logs/backend_execution_events.jsonl" if latest else "待生成"}` | 后端逐项执行日志 |',
|
||
f'| `{latest / "screenshots" if latest else "待生成"}` | 统一截图目录 |',
|
||
'',
|
||
]
|
||
if md_path.exists():
|
||
with md_path.open('a', encoding='utf-8') as f:
|
||
f.write('\n---\n\n')
|
||
f.write('\n'.join(lines) + '\n')
|
||
else:
|
||
md_path.write_text('\n'.join(lines) + '\n', encoding='utf-8')
|
||
if csv_path.exists():
|
||
serial = datetime.now().strftime('%H%M%S')
|
||
csv_path = DOC_DIR / f'110项功能逐项确认清单_追加版_{serial}.csv'
|
||
csv_path.write_text('\n'.join(csv_lines) + '\n', encoding='utf-8')
|
||
|
||
progress_row = (
|
||
f"\n| {NOW} | Manus/浏览器 | 用户要求先列清楚110个功能;所有开发文档只能追加,不能覆盖;按日期分类;先确定功能、再逐项确认完成。 | "
|
||
f"新增追加文档:[[开发文档/6、测试/110项功能逐项确认/2026-05-18/110项功能逐项确认清单_追加版.md]];CSV:[[开发文档/6、测试/110项功能逐项确认/2026-05-18/{csv_path.name}]] | "
|
||
f"总进度65%,当前清单确认进度100%;下一步逐项补资料读取明细、锁屏后台控制与截图可见性。 | 本地追加成功,未覆盖既有文档 |\n"
|
||
)
|
||
if PROGRESS.exists():
|
||
with PROGRESS.open('a', encoding='utf-8') as f:
|
||
f.write(progress_row)
|
||
else:
|
||
PROGRESS.parent.mkdir(parents=True, exist_ok=True)
|
||
PROGRESS.write_text('# 项目落地执行表\n\n| 时间 | 人设 | 用户原话整理/扫描 | 本轮新增/修改 | 进度 | 本地同步 |\n|---|---|---|---|---|---|\n' + progress_row, encoding='utf-8')
|
||
|
||
print(json.dumps({
|
||
'md_path': str(md_path),
|
||
'csv_path': str(csv_path),
|
||
'progress_path': str(PROGRESS),
|
||
'latest_result_dir': str(latest) if latest else '',
|
||
'function_count': len(rows),
|
||
'status_count': {s: sum(1 for r in results.values() if r.get('status') == s) for s in sorted(set(r.get('status','') for r in results.values()))},
|
||
}, ensure_ascii=False, indent=2))
|
||
|
||
if __name__ == '__main__':
|
||
main()
|