docs: 新增 Windows 移植交接文档

docs/WINDOWS_PORT.md 覆盖:
- 逐文件可移植性审计(核心逻辑层约 90% 可直接复用)
- 核心难点:iOS 17+ 的 RSD 隧道无法在 Windows 复用(macOS 实现依赖 XPC +
  ObjC runtime 私有框架),改用 pymobiledevice3 的 userspace 隧道
- 已实测确认的命令与 API(--userspace 挂在 lockdown start-tunnel / mounter
  auto-mount 上,remote start-tunnel 不支持;类名是 UserspaceRsdTunnel)
- 窗口启动、数据目录、打包与分发(DMG → Inno Setup)、图标(.icns → .ico)
- macOS 侧踩过的坑:镜像挂载用 Python API、必须打包包元数据、不要把 distutils
  放进 excludes、AlreadyMountedError 当成功处理
- 设计约束(DVT 长连接、只能改经纬度、last_point 与 last_reported 分离等)
- 验收基线(复用现有测试套件)与建议的工作顺序
This commit is contained in:
jbjf6666 committed 2026-10-16 01:05:09 +08:00
commit 85e9a53750
65 files changed
+18603

No files matched your search

+100
View File
@@ -0,0 +1,100 @@
#!/usr/bin/env python3
"""给界面截图(视觉验收用)。
做法:headless Chrome 的一次性 --screenshot 模式 + 独立 user-data-dir,
不依赖 CDP WebSocket(本机没有 websocket-client 库,Chrome 的 CDP 又需要它)。
--screenshot 在页面 load 事件后立即截图,用一次注入脚本做等待/交互,
脚本通过 URL fragment 传不进去,所以这里用临时 HTML 包装页来间接执行前置动作。
用法:
./.venv/bin/python tools/screenshot.py http://127.0.0.1:8787/ out.png [--wait 9]
"""
from __future__ import annotations
import argparse
import json
import os
import shutil
import signal
import subprocess
import sys
import time
from pathlib import Path
CHROME_CANDIDATES = [
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
"/Applications/Chromium.app/Contents/MacOS/Chromium",
]
def find_chrome() -> str:
for path in CHROME_CANDIDATES:
if os.path.exists(path):
return path
raise SystemExit("未找到 Chrome / Edge / Chromium")
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("url")
parser.add_argument("out", nargs="?", default="screenshot.png")
parser.add_argument("--wait", type=float, default=9.0, help="页面加载后额外等待秒数(等瓦片/WS 渲染)")
parser.add_argument("--width", type=int, default=1440)
parser.add_argument("--height", type=int, default=900)
parser.add_argument("--profile", default="/tmp/iosloc-shot-profile")
parser.add_argument("--json", action="store_true", help="同时打印页面控制台日志(--enable-logging)")
args = parser.parse_args()
chrome = find_chrome()
profile = Path(args.profile)
if profile.exists():
shutil.rmtree(profile, ignore_errors=True)
profile.mkdir(parents=True, exist_ok=True)
out = Path(args.out).resolve()
if out.exists():
out.unlink()
command = [
chrome,
"--headless=new",
"--disable-gpu",
"--no-first-run",
"--no-default-browser-check",
"--hide-scrollbars",
"--disable-extensions",
"--force-device-scale-factor=2",
f"--window-size={args.width},{args.height}",
f"--user-data-dir={profile}",
# Chrome 没有"加载后等待 N 秒"的开关,用虚拟时间预算让定时器与网络请求跑完
f"--virtual-time-budget={int(args.wait * 1000)}",
f"--screenshot={out}",
args.url,
]
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
text=True, start_new_session=True)
try:
output, _ = process.communicate(timeout=max(60.0, args.wait * 6))
except subprocess.TimeoutExpired:
try:
os.killpg(os.getpgid(process.pid), signal.SIGKILL)
except Exception:
process.kill()
output, _ = process.communicate()
print("Chrome 超时被终止")
if args.json and output:
for line in output.splitlines():
if any(key in line for key in ("ERROR", "SEVERE", "Uncaught", "console")):
print(" [chrome]", line[:220])
if not out.exists() or out.stat().st_size == 0:
print("截图失败")
if output:
print(output[-1500:])
return 1
print(f"截图已保存:{out}({out.stat().st_size // 1024} KB)")
return 0
if __name__ == "__main__":
raise SystemExit(main())