diff --git a/sdk/scripts/console_redesign_visual_qa.py b/sdk/scripts/console_redesign_visual_qa.py
new file mode 100644
index 0000000000..fa4a182e3a
--- /dev/null
+++ b/sdk/scripts/console_redesign_visual_qa.py
@@ -0,0 +1,514 @@
+#!/usr/bin/env python3
+"""总控平台页面视觉与性能验收工具。
+
+默认只读:只访问 BASE_URL 的 GET 页面和接口,不点击业务按钮、不调用手机、不执行写接口。
+如需登录,使用 CONSOLE_QA_USERNAME/CONSOLE_QA_PASSWORD(或 USERNAME/PASSWORD)环境变量。
+"""
+from __future__ import annotations
+
+import argparse
+import asyncio
+import json
+import os
+import re
+import shutil
+import sys
+import time
+from collections import Counter
+from dataclasses import asdict, dataclass
+from datetime import datetime
+from pathlib import Path
+from typing import Any
+from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
+
+try:
+ from PIL import Image, ImageStat
+except ImportError as exc: # pragma: no cover - 运行环境缺依赖时给出明确提示
+ Image = None # type: ignore[assignment]
+ ImageStat = None # type: ignore[assignment]
+ _PIL_IMPORT_ERROR = exc
+else:
+ _PIL_IMPORT_ERROR = None
+
+try:
+ from playwright.async_api import Browser, BrowserContext, Page, async_playwright
+except ImportError as exc: # pragma: no cover - 运行环境缺依赖时给出明确提示
+ Browser = BrowserContext = Page = Any # type: ignore[misc,assignment]
+ async_playwright = None # type: ignore[assignment]
+ _PLAYWRIGHT_IMPORT_ERROR = exc
+else:
+ _PLAYWRIGHT_IMPORT_ERROR = None
+
+
+ROOT = Path(__file__).resolve().parents[2]
+DEFAULT_OUTPUT_DIR = ROOT / "开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA"
+VIEWPORTS = {
+ 1440: 1200,
+ 1024: 1100,
+ 768: 1024,
+ 375: 900,
+}
+WRITE_METHODS = {"POST", "PUT", "PATCH", "DELETE"}
+AUTH_MARKERS = ("登录总控平台", "登录并加载平台", "账号", "密码", "退出登录")
+BUSINESS_MARKERS = ("数据总览", "手机设备", "智能引擎", "统一接入中心")
+
+
+@dataclass(frozen=True)
+class ScreenshotSpec:
+ id: str
+ title: str
+ tab: str
+ width: int
+ query: tuple[tuple[str, str], ...] = ()
+ required_tokens: tuple[str, ...] = ()
+
+
+SCREENSHOTS = (
+ ScreenshotSpec("SS-01", "数据总览 1440px", "overview", 1440, required_tokens=("数据总览",)),
+ ScreenshotSpec("SS-02", "数据总览接口异常/数据过期", "overview", 1440, (("qa_state", "expired"),), ("数据总览", "数据已过期")),
+ ScreenshotSpec("SS-03", "手机设备含下载与扫码按钮", "devices", 1024, required_tokens=("手机设备",)),
+ ScreenshotSpec("SS-04", "添加设备 APK 下载步骤", "devices", 1024, (("qa_step", "apk"),), ("手机设备",)),
+ ScreenshotSpec("SS-05", "添加设备原扫码绑定步骤", "devices", 1024, (("qa_step", "scan"),), ("手机设备",)),
+ ScreenshotSpec("SS-06", "设备卡片三种状态", "devices", 1024, (("qa_state", "statuses"),), ("手机设备",)),
+ ScreenshotSpec("SS-07", "单机控制台概览", "devices", 1024, (("view", "device"),), ("手机设备",)),
+ ScreenshotSpec("SS-08", "设备分组管理", "devices", 1024, (("view", "groups"),), ("手机设备",)),
+ ScreenshotSpec("SS-09", "在线设备禁止删除", "devices", 1024, (("qa_state", "delete-protected"),), ("手机设备",)),
+ ScreenshotSpec("SS-10", "智能引擎四模块", "engine", 1024, required_tokens=("智能引擎",)),
+ ScreenshotSpec("SS-11", "统一接入中心接口目录", "integrations", 1024, (("view", "api"),), ("统一接入中心",)),
+ ScreenshotSpec("SS-12", "知识库文档可打开", "integrations", 1024, (("view", "knowledge"),), ("统一接入中心",)),
+ ScreenshotSpec("SS-13", "375px 手机设备页", "devices", 375, required_tokens=("手机设备",)),
+ ScreenshotSpec("SS-14", "真机写操作四证汇总", "devices", 1440, (("view", "evidence"),), ("手机设备",)),
+ ScreenshotSpec("SS-15", "第三方应用与凭证列表", "integrations", 1024, (("view", "apps"),), ("统一接入中心",)),
+ ScreenshotSpec("SS-16", "接口设备文档授权详情", "integrations", 1024, (("view", "grants"),), ("统一接入中心",)),
+ ScreenshotSpec("SS-17", "第三方用量与额度看板", "integrations", 1024, (("view", "usage"),), ("统一接入中心",)),
+ ScreenshotSpec("SS-18", "首页设备与微信运行摘要", "overview", 1440, (("view", "wechat-summary"),), ("数据总览",)),
+)
+
+
+def env_first(*names: str) -> str | None:
+ for name in names:
+ value = os.environ.get(name)
+ if value:
+ return value
+ return None
+
+
+def build_url(base_url: str, tab: str, query: tuple[tuple[str, str], ...]) -> str:
+ parts = urlsplit(base_url)
+ existing = dict(parse_qsl(parts.query, keep_blank_values=True))
+ existing["tab"] = tab
+ existing.update(query)
+ return urlunsplit((parts.scheme, parts.netloc, parts.path or "/", urlencode(existing), parts.fragment))
+
+
+def redact_url(url: str) -> str:
+ parts = urlsplit(url)
+ return urlunsplit((parts.scheme, parts.netloc, parts.path, "", ""))
+
+
+def timestamp() -> str:
+ return datetime.now().astimezone().strftime("%Y%m%dT%H%M%S%z")
+
+
+def safe_filename(value: str) -> str:
+ return re.sub(r"[^A-Za-z0-9_.-]+", "-", value)
+
+
+def find_browser(executable_path: str | None) -> str | None:
+ if executable_path:
+ return executable_path
+ candidates = [
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
+ "/Applications/Chromium.app/Contents/MacOS/Chromium",
+ ]
+ for candidate in candidates:
+ if Path(candidate).is_file() and os.access(candidate, os.X_OK):
+ return candidate
+ for command in ("google-chrome", "google-chrome-stable", "chromium", "chromium-browser"):
+ found = shutil.which(command)
+ if found and Path(found).is_file() and os.access(found, os.X_OK):
+ # Homebrew/旧环境可能留下只负责转发到已删除 App 的壳脚本。
+ try:
+ if Path(found).read_text(encoding="utf-8", errors="ignore").startswith("#!") and "exec '" in Path(found).read_text(encoding="utf-8", errors="ignore"):
+ continue
+ except OSError:
+ pass
+ return found
+ return None
+
+
+def is_auth_request(url: str, login_path: str) -> bool:
+ return urlsplit(url).path.rstrip("/") == login_path.rstrip("/")
+
+
+def image_guard(path: Path) -> tuple[bool, str]:
+ """挡住全黑、全白或几乎没有内容的截图。"""
+ if Image is None:
+ return False, f"Pillow 不可用: {_PIL_IMPORT_ERROR}"
+ try:
+ with Image.open(path) as image:
+ rgb = image.convert("RGB")
+ stat = ImageStat.Stat(rgb.resize((64, 64)))
+ mean = sum(stat.mean) / 3
+ variance = sum(stat.var) / 3
+ if mean < 3:
+ return False, "black_page: 截图几乎全黑"
+ if mean > 252 and variance < 3:
+ return False, "blank_page: 截图几乎全白"
+ if variance < 1 and (mean < 8 or mean > 247):
+ return False, "blank_page: 截图近乎纯色"
+ return True, ""
+ except (OSError, ValueError) as exc:
+ return False, f"screenshot_unreadable: {exc}"
+
+
+async def navigation_timing(page: Page, wall_ms: float) -> dict[str, Any]:
+ timing = await page.evaluate(
+ """() => {
+ const navigation = performance.getEntriesByType('navigation')[0];
+ const paints = Object.fromEntries(
+ performance.getEntriesByType('paint').map(entry => [entry.name, entry.startTime])
+ );
+ return {
+ dom_content_loaded_ms: navigation?.domContentLoadedEventEnd || null,
+ load_event_ms: navigation?.loadEventEnd || null,
+ response_start_ms: navigation?.responseStart || null,
+ first_paint_ms: paints['first-paint'] || null,
+ first_contentful_paint_ms: paints['first-contentful-paint'] || null,
+ };
+ }"""
+ )
+ candidates = [
+ timing.get("first_contentful_paint_ms"),
+ timing.get("first_paint_ms"),
+ timing.get("dom_content_loaded_ms"),
+ timing.get("response_start_ms"),
+ wall_ms,
+ ]
+ timing["first_screen_ms"] = round(float(next(value for value in candidates if value is not None)), 2)
+ return timing
+
+
+async def page_text(page: Page) -> str:
+ try:
+ return (await page.locator("body").inner_text(timeout=1500)).strip()
+ except Exception:
+ return ""
+
+
+async def validate_business_page(page: Page, spec: ScreenshotSpec) -> tuple[bool, str, str]:
+ text = await page_text(page)
+ lowered = text.lower()
+ if any(marker in text for marker in AUTH_MARKERS) and (await page.locator("input[type='password']").count() > 0):
+ return False, "login_page: 当前仍是登录页,未算业务页", text
+ if not text or len(text) < 80:
+ return False, "blank_page: 页面可见文本不足,未算业务页", text
+ if not any(marker in text for marker in BUSINESS_MARKERS):
+ return False, "not_business_page: 未找到总控平台业务页标识", text
+ missing = [token for token in spec.required_tokens if token not in text]
+ if missing:
+ return False, f"business_token_missing: 缺少页面标识 {', '.join(missing)}", text
+ if "error" in lowered and "登录服务暂时不可用" in text:
+ return False, "page_error: 页面显示登录服务异常", text
+ return True, "", text
+
+
+async def maybe_login(page: Page, username: str | None, password: str | None, login_path: str) -> dict[str, Any]:
+ """仅在显式提供账号密码时登录;不点任何业务操作。"""
+ result: dict[str, Any] = {"attempted": False, "succeeded": False, "reason": ""}
+ if not username or not password:
+ result["reason"] = "未提供登录环境变量"
+ return result
+ result["attempted"] = True
+ text = await page_text(page)
+ if "登录总控平台" not in text and await page.locator("input[type='password']").count() == 0:
+ result["succeeded"] = True
+ result["reason"] = "初始页面已是业务页"
+ return result
+ try:
+ user_input = page.locator("input[name='username'], input[autocomplete='username'], input[type='text']").first
+ password_input = page.locator("input[name='password'], input[autocomplete='current-password'], input[type='password']").first
+ await user_input.fill(username)
+ await password_input.fill(password)
+ await page.locator("button[type='submit'], button:has-text('登录')").first.click()
+ await page.wait_for_timeout(150)
+ result["succeeded"] = (await page.locator("input[type='password']").count()) == 0
+ result["reason"] = "登录完成" if result["succeeded"] else "登录后仍停留在登录页"
+ except Exception as exc:
+ result["reason"] = f"登录失败: {type(exc).__name__}: {exc}"
+ return result
+
+
+async def capture_spec(
+ context: BrowserContext,
+ browser: Browser,
+ base_url: str,
+ spec: ScreenshotSpec,
+ output_dir: Path,
+ run_stamp: str,
+ timeout_ms: int,
+ failure_state: dict[str, Any],
+) -> dict[str, Any]:
+ page = await context.new_page()
+ target_url = build_url(base_url, spec.tab, spec.query)
+ screenshot_dir = output_dir / "screenshots"
+ error_dir = output_dir / "errors"
+ screenshot_dir.mkdir(parents=True, exist_ok=True)
+ error_dir.mkdir(parents=True, exist_ok=True)
+ base_name = f"{spec.id}_{spec.width}x{VIEWPORTS[spec.width]}_{run_stamp}"
+ screenshot_path = screenshot_dir / f"{safe_filename(base_name)}.png"
+ error_path = error_dir / f"{safe_filename(base_name)}_error.png"
+ network_failures: list[dict[str, Any]] = []
+ response_failures: list[dict[str, Any]] = []
+
+ def record_failed_request(request: Any) -> None:
+ if request.resource_type in {"xhr", "fetch"} or "/api/" in request.url:
+ network_failures.append({"url": redact_url(request.url), "error": request.failure})
+
+ def record_response(response: Any) -> None:
+ if response.request.resource_type in {"xhr", "fetch"} or "/api/" in response.url:
+ if response.status >= 400:
+ response_failures.append({"url": redact_url(response.url), "status": response.status})
+
+ page.on("requestfailed", record_failed_request)
+ page.on("response", record_response)
+ started = time.perf_counter()
+ navigation_error = ""
+ try:
+ await page.goto(target_url, wait_until="domcontentloaded", timeout=timeout_ms)
+ await page.wait_for_timeout(120)
+ try:
+ await page.evaluate("document.fonts && document.fonts.ready")
+ except Exception:
+ pass
+ except Exception as exc:
+ navigation_error = f"navigation_error: {type(exc).__name__}: {exc}"
+ wall_ms = round((time.perf_counter() - started) * 1000, 2)
+ timing: dict[str, Any] = {"first_screen_ms": wall_ms}
+ if not navigation_error:
+ try:
+ timing = await navigation_timing(page, wall_ms)
+ except Exception as exc:
+ timing["timing_error"] = f"{type(exc).__name__}: {exc}"
+ ok, reason, text = (False, navigation_error, "") if navigation_error else await validate_business_page(page, spec)
+ try:
+ await page.screenshot(path=str(error_path if not ok else screenshot_path), full_page=True, timeout=timeout_ms)
+ except Exception as exc:
+ reason = f"{reason}; screenshot_error: {type(exc).__name__}: {exc}" if reason else f"screenshot_error: {type(exc).__name__}: {exc}"
+ image_path = error_path if not ok else screenshot_path
+ if image_path.exists():
+ image_ok, image_reason = image_guard(image_path)
+ if not image_ok:
+ ok = False
+ reason = f"{reason}; {image_reason}" if reason else image_reason
+ if image_path == screenshot_path:
+ try:
+ await page.screenshot(path=str(error_path), full_page=True, timeout=timeout_ms)
+ except Exception:
+ pass
+ interface_failures = network_failures + response_failures
+ if failure_state.get("readonly_violations"):
+ ok = False
+ reason = f"readonly_violation: 发现非认证写请求 {len(failure_state['readonly_violations'])} 次"
+ result = {
+ "id": spec.id,
+ "title": spec.title,
+ "url": redact_url(target_url),
+ "viewport": {"width": spec.width, "height": VIEWPORTS[spec.width]},
+ "status": "passed" if ok else "failed",
+ "reason": reason or "业务页标识、内容和截图检查通过",
+ "screenshot_path": str(screenshot_path.resolve()) if ok and screenshot_path.exists() else None,
+ "error_screenshot_path": str(error_path.resolve()) if (not ok and error_path.exists()) else None,
+ "screenshot_filename_contains_id_size_time": spec.id in image_path.name and str(spec.width) in image_path.name and run_stamp in image_path.name,
+ "visible_text_length": len(text),
+ "first_screen_ms": timing.get("first_screen_ms"),
+ "timing": timing,
+ "interface_failure_count": len(interface_failures),
+ "interface_failures": interface_failures,
+ "readonly_violation_count": len(failure_state.get("readonly_violations", [])),
+ }
+ await page.close()
+ return result
+
+
+async def run_qa(args: argparse.Namespace) -> dict[str, Any]:
+ if _PLAYWRIGHT_IMPORT_ERROR:
+ raise RuntimeError(f"Playwright 不可用: {_PLAYWRIGHT_IMPORT_ERROR}")
+ output_dir = Path(args.output_dir).expanduser().resolve()
+ output_dir.mkdir(parents=True, exist_ok=True)
+ run_stamp = timestamp()
+ base_url = args.base_url
+ username = args.username
+ password = args.password
+ token = args.auth_token
+ login_path = args.login_path
+ failure_state: dict[str, Any] = {"readonly_violations": [], "auth_requests": []}
+
+ async with async_playwright() as playwright:
+ executable_path = find_browser(args.executable_path)
+ browser = await playwright.chromium.launch(headless=not args.headed, executable_path=executable_path)
+ contexts: dict[int, BrowserContext] = {}
+ try:
+ for width, height in VIEWPORTS.items():
+ headers = {"Authorization": f"Bearer {token}"} if token else None
+ context = await browser.new_context(viewport={"width": width, "height": height}, extra_http_headers=headers)
+
+ async def guard_route(route: Any, request: Any) -> None:
+ if request.method in WRITE_METHODS:
+ if is_auth_request(request.url, login_path):
+ failure_state["auth_requests"].append({"method": request.method, "url": redact_url(request.url)})
+ await route.continue_()
+ return
+ failure_state["readonly_violations"].append({"method": request.method, "url": redact_url(request.url)})
+ await route.abort(error_code="blockedbyclient")
+ return
+ await route.continue_()
+
+ await context.route("**/*", guard_route)
+ contexts[width] = context
+ login_page = await context.new_page()
+ try:
+ await login_page.goto(base_url, wait_until="domcontentloaded", timeout=args.timeout_ms)
+ await login_page.wait_for_timeout(80)
+ await maybe_login(login_page, username, password, login_path)
+ finally:
+ await login_page.close()
+
+ results: list[dict[str, Any]] = []
+ for spec in SCREENSHOTS:
+ result = await capture_spec(
+ contexts[spec.width],
+ browser,
+ base_url,
+ spec,
+ output_dir,
+ run_stamp,
+ args.timeout_ms,
+ failure_state,
+ )
+ results.append(result)
+ finally:
+ for context in contexts.values():
+ await context.close()
+ await browser.close()
+
+ performance_by_viewport: dict[str, dict[str, Any]] = {}
+ for width in VIEWPORTS:
+ rows = [row for row in results if row["viewport"]["width"] == width]
+ timings = [row["first_screen_ms"] for row in rows if isinstance(row.get("first_screen_ms"), (int, float))]
+ performance_by_viewport[str(width)] = {
+ "viewport": {"width": width, "height": VIEWPORTS[width]},
+ "sample_count": len(rows),
+ "first_screen_ms": round(sum(timings) / len(timings), 2) if timings else None,
+ "first_screen_ms_max": max(timings) if timings else None,
+ "interface_failure_count": sum(row["interface_failure_count"] for row in rows),
+ }
+ counts = Counter(row["status"] for row in results)
+ report = {
+ "task_id": "WP-CONSOLE-13",
+ "task_name": "阿端·页面验收工具·一次跑完四种尺寸、性能和18张截图",
+ "run_mode": "local_read_only",
+ "generated_at": datetime.now().astimezone().isoformat(),
+ "base_url": redact_url(base_url),
+ "read_only": True,
+ "write_methods_blocked": sorted(WRITE_METHODS),
+ "auth_request_count": len(failure_state["auth_requests"]),
+ "readonly_violation_count": len(failure_state["readonly_violations"]),
+ "viewport_widths": list(VIEWPORTS),
+ "screenshot_spec_count": len(SCREENSHOTS),
+ "screenshot_success_count": counts["passed"],
+ "screenshot_failure_count": counts["failed"],
+ "business_page_rejection_count": sum(1 for row in results if row["status"] == "failed" and any(key in row["reason"] for key in ("blank_page", "black_page", "login_page", "not_business_page"))),
+ "interface_failure_count": sum(row["interface_failure_count"] for row in results),
+ "performance": performance_by_viewport,
+ "results": results,
+ }
+ (output_dir / "visual_qa_report.json").write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
+ performance_report = {
+ "task_id": report["task_id"],
+ "generated_at": report["generated_at"],
+ "first_screen_time_definition": "优先 First Contentful Paint,其次 First Paint、DOMContentLoaded、responseStart 或导航墙钟时间",
+ "interface_failure_count": report["interface_failure_count"],
+ "by_viewport": performance_by_viewport,
+ }
+ (output_dir / "performance_report.json").write_text(json.dumps(performance_report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
+ manifest = {
+ "task_id": report["task_id"],
+ "generated_at": report["generated_at"],
+ "screenshots": [
+ {
+ "id": row["id"],
+ "title": row["title"],
+ "viewport": row["viewport"],
+ "status": row["status"],
+ "screenshot_path": row["screenshot_path"],
+ "error_screenshot_path": row["error_screenshot_path"],
+ "reason": row["reason"],
+ }
+ for row in results
+ ],
+ }
+ (output_dir / "screenshot_manifest.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
+ md_lines = [
+ "# WP-CONSOLE-13 页面视觉与性能验收报告",
+ "",
+ "- 运行模式:本地只读;写方法已拦截,未调用手机,未执行业务写接口。",
+ f"- 页面入口:`{report['base_url']}`",
+ f"- 四档尺寸:{', '.join(f'{w}px' for w in VIEWPORTS)};截图规格:{report['screenshot_spec_count']} 张。",
+ f"- 截图结果:成功 {report['screenshot_success_count']},失败 {report['screenshot_failure_count']};接口失败数:{report['interface_failure_count']}。",
+ "",
+ "## 性能",
+ "",
+ "| 宽度 | 样本数 | 平均首屏(ms) | 最大首屏(ms) | 接口失败数 |",
+ "|---:|---:|---:|---:|---:|",
+ ]
+ for width, item in performance_by_viewport.items():
+ md_lines.append(f"| {width} | {item['sample_count']} | {item['first_screen_ms']} | {item['first_screen_ms_max']} | {item['interface_failure_count']} |")
+ md_lines.extend(["", "## 截图清单", "", "| 编号 | 尺寸 | 结果 | 成功截图 | 失败截图 | 原因 |", "|---|---:|---|---|---|---|"])
+ for row in results:
+ md_lines.append(f"| {row['id']} | {row['viewport']['width']}x{row['viewport']['height']} | {row['status']} | {row['screenshot_path'] or ''} | {row['error_screenshot_path'] or ''} | {row['reason']} |")
+ (output_dir / "visual_qa_report.md").write_text("\n".join(md_lines) + "\n", encoding="utf-8")
+ return report
+
+
+def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
+ parser = argparse.ArgumentParser(description="工作手机总控平台四档页面视觉与性能验收")
+ parser.add_argument("--base-url", default=env_first("BASE_URL", "CONSOLE_QA_BASE_URL") or "http://127.0.0.1:8899/")
+ parser.add_argument("--output-dir", type=Path, default=Path(env_first("CONSOLE_QA_OUTPUT_DIR") or DEFAULT_OUTPUT_DIR))
+ parser.add_argument("--username", default=env_first("CONSOLE_QA_USERNAME", "CONSOLE_USERNAME", "USERNAME"))
+ parser.add_argument("--password", default=env_first("CONSOLE_QA_PASSWORD", "CONSOLE_PASSWORD", "PASSWORD"))
+ parser.add_argument("--auth-token", default=env_first("CONSOLE_QA_AUTH_TOKEN", "CONSOLE_AUTH_TOKEN", "AUTH_TOKEN"))
+ parser.add_argument("--login-path", default=env_first("CONSOLE_QA_LOGIN_PATH", "LOGIN_PATH") or "/api/v3/console/login")
+ parser.add_argument("--timeout-ms", type=int, default=int(env_first("CONSOLE_QA_TIMEOUT_MS") or "15000"))
+ parser.add_argument("--executable-path", default=env_first("CONSOLE_QA_BROWSER") or None)
+ parser.add_argument("--headed", action="store_true", help="显示浏览器窗口;默认无界面")
+ parser.add_argument("--strict", action="store_true", help="存在截图失败、写请求或接口失败时返回非零")
+ return parser.parse_args(argv)
+
+
+def main(argv: list[str] | None = None) -> int:
+ args = parse_args(argv)
+ try:
+ report = asyncio.run(run_qa(args))
+ except Exception as exc:
+ print(f"视觉验收工具执行失败:{type(exc).__name__}: {exc}", file=sys.stderr)
+ return 3
+ summary = {
+ "task_id": report["task_id"],
+ "run_mode": report["run_mode"],
+ "viewport_widths": report["viewport_widths"],
+ "screenshot_spec_count": report["screenshot_spec_count"],
+ "screenshot_success_count": report["screenshot_success_count"],
+ "screenshot_failure_count": report["screenshot_failure_count"],
+ "interface_failure_count": report["interface_failure_count"],
+ "readonly_violation_count": report["readonly_violation_count"],
+ "output_dir": str(Path(args.output_dir).expanduser().resolve()),
+ }
+ print(json.dumps(summary, ensure_ascii=False, indent=2))
+ if args.strict and (report["screenshot_failure_count"] or report["interface_failure_count"] or report["readonly_violation_count"]):
+ return 1
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/sdk/tests/test_wp_console_13_visual_qa.py b/sdk/tests/test_wp_console_13_visual_qa.py
new file mode 100644
index 0000000000..9eb215e9fa
--- /dev/null
+++ b/sdk/tests/test_wp_console_13_visual_qa.py
@@ -0,0 +1,130 @@
+"""WP-CONSOLE-13 页面视觉、性能与失败证据工具测试。"""
+from __future__ import annotations
+
+import http.server
+import json
+import os
+import socketserver
+import subprocess
+import sys
+import threading
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[2]
+SCRIPT = ROOT / "sdk/scripts/console_redesign_visual_qa.py"
+
+
+class FixtureHandler(http.server.BaseHTTPRequestHandler):
+ def do_GET(self): # noqa: N802 - stdlib handler contract
+ if self.path.startswith("/api/ok"):
+ payload = b'{"ok":true}'
+ self.send_response(200)
+ self.send_header("Content-Type", "application/json")
+ self.send_header("Content-Length", str(len(payload)))
+ self.end_headers()
+ self.wfile.write(payload)
+ return
+ if self.path.startswith("/api/fail"):
+ payload = b'{"ok":false}'
+ self.send_response(503)
+ self.send_header("Content-Type", "application/json")
+ self.send_header("Content-Length", str(len(payload)))
+ self.end_headers()
+ self.wfile.write(payload)
+ return
+ if self.server.login_page:
+ body = """
+
登录总控平台
请输入账号和密码。
+
+ """
+ else:
+ body = """
+ 本地总控验收夹具
+
+
+ 数据已过期
在线 待授权 离线;下载 APK;扫码绑定;单机控制台;设备分组;在线设备禁止删除。
+ 真机写操作四证汇总;第三方应用与凭证列表;接口设备文档授权详情;第三方用量与额度看板;知识库文档。
+
+ """
+ payload = body.encode()
+ self.send_response(200)
+ self.send_header("Content-Type", "text/html; charset=utf-8")
+ self.send_header("Content-Length", str(len(payload)))
+ self.end_headers()
+ self.wfile.write(payload)
+
+ def log_message(self, *_args):
+ return
+
+
+class FixtureServer(socketserver.ThreadingTCPServer):
+ allow_reuse_address = True
+
+ def __init__(self, server_address, handler, login_page=False):
+ self.login_page = login_page
+ super().__init__(server_address, handler)
+
+
+def run_fixture(tmp_path: Path, *, login_page: bool = False, strict: bool = True) -> tuple[subprocess.CompletedProcess[str], Path]:
+ server = FixtureServer(("127.0.0.1", 0), FixtureHandler, login_page=login_page)
+ thread = threading.Thread(target=server.serve_forever, daemon=True)
+ thread.start()
+ output_dir = tmp_path / ("login-report" if login_page else "success-report")
+ try:
+ command = [sys.executable, str(SCRIPT), "--base-url", f"http://127.0.0.1:{server.server_address[1]}/", "--output-dir", str(output_dir)]
+ if strict:
+ command.append("--strict")
+ completed = subprocess.run(command, cwd=ROOT, text=True, capture_output=True, env={**os.environ, "PYTHONUNBUFFERED": "1"}, timeout=90)
+ finally:
+ server.shutdown()
+ server.server_close()
+ thread.join(timeout=3)
+ return completed, output_dir
+
+
+def test_generates_18_screenshots_manifest_and_four_viewport_performance(tmp_path):
+ completed, output_dir = run_fixture(tmp_path)
+ assert completed.returncode == 0, completed.stderr + completed.stdout
+ report = json.loads((output_dir / "visual_qa_report.json").read_text(encoding="utf-8"))
+ manifest = json.loads((output_dir / "screenshot_manifest.json").read_text(encoding="utf-8"))
+ performance = json.loads((output_dir / "performance_report.json").read_text(encoding="utf-8"))
+ assert report["read_only"] is True
+ assert report["screenshot_spec_count"] == 18
+ assert report["screenshot_success_count"] == 18
+ assert report["interface_failure_count"] == 0
+ assert report["readonly_violation_count"] == 0
+ assert set(report["viewport_widths"]) == {1440, 1024, 768, 375}
+ assert set(performance["by_viewport"]) == {"1440", "1024", "768", "375"}
+ assert len(manifest["screenshots"]) == 18
+ for item in manifest["screenshots"]:
+ screenshot = Path(item["screenshot_path"])
+ assert screenshot.is_absolute()
+ assert screenshot.is_file()
+ assert item["id"] in screenshot.name
+ assert str(item["viewport"]["width"]) in screenshot.name
+ assert item["error_screenshot_path"] is None
+ assert len(list((output_dir / "screenshots").glob("*.png"))) == 18
+
+
+def test_login_page_is_never_business_success_and_keeps_error_screenshots(tmp_path):
+ completed, output_dir = run_fixture(tmp_path, login_page=True)
+ assert completed.returncode != 0
+ report = json.loads((output_dir / "visual_qa_report.json").read_text(encoding="utf-8"))
+ manifest = json.loads((output_dir / "screenshot_manifest.json").read_text(encoding="utf-8"))
+ assert report["screenshot_success_count"] == 0
+ assert report["screenshot_failure_count"] == 18
+ assert report["business_page_rejection_count"] == 18
+ assert all(item["status"] == "failed" for item in manifest["screenshots"])
+ assert all("login_page" in item["reason"] for item in manifest["screenshots"])
+ assert all(Path(item["error_screenshot_path"]).is_file() for item in manifest["screenshots"])
+
+
+def test_script_is_read_only_and_declares_all_required_screenshot_ids():
+ namespace: dict[str, object] = {}
+ namespace["__file__"] = str(SCRIPT)
+ exec(compile(SCRIPT.read_text(encoding="utf-8"), str(SCRIPT), "exec"), namespace)
+ specs = namespace["SCREENSHOTS"]
+ ids = {item.id for item in specs} # type: ignore[union-attr]
+ assert ids == {f"SS-{index:02d}" for index in range(1, 19)}
+ assert namespace["WRITE_METHODS"] == {"POST", "PUT", "PATCH", "DELETE"}
diff --git a/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/performance_report.json b/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/performance_report.json
new file mode 100644
index 0000000000..2371927336
--- /dev/null
+++ b/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/performance_report.json
@@ -0,0 +1,48 @@
+{
+ "task_id": "WP-CONSOLE-13",
+ "generated_at": "2026-08-09T14:03:54.036050+08:00",
+ "first_screen_time_definition": "优先 First Contentful Paint,其次 First Paint、DOMContentLoaded、responseStart 或导航墙钟时间",
+ "interface_failure_count": 0,
+ "by_viewport": {
+ "1440": {
+ "viewport": {
+ "width": 1440,
+ "height": 1200
+ },
+ "sample_count": 4,
+ "first_screen_ms": 216.0,
+ "first_screen_ms_max": 280.0,
+ "interface_failure_count": 0
+ },
+ "1024": {
+ "viewport": {
+ "width": 1024,
+ "height": 1100
+ },
+ "sample_count": 13,
+ "first_screen_ms": 196.92,
+ "first_screen_ms_max": 288.0,
+ "interface_failure_count": 0
+ },
+ "768": {
+ "viewport": {
+ "width": 768,
+ "height": 1024
+ },
+ "sample_count": 0,
+ "first_screen_ms": null,
+ "first_screen_ms_max": null,
+ "interface_failure_count": 0
+ },
+ "375": {
+ "viewport": {
+ "width": 375,
+ "height": 900
+ },
+ "sample_count": 1,
+ "first_screen_ms": 176.0,
+ "first_screen_ms_max": 176.0,
+ "interface_failure_count": 0
+ }
+ }
+}
diff --git a/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshot_manifest.json b/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshot_manifest.json
new file mode 100644
index 0000000000..7752316fa6
--- /dev/null
+++ b/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshot_manifest.json
@@ -0,0 +1,222 @@
+{
+ "task_id": "WP-CONSOLE-13",
+ "generated_at": "2026-08-09T14:03:54.036050+08:00",
+ "screenshots": [
+ {
+ "id": "SS-01",
+ "title": "数据总览 1440px",
+ "viewport": {
+ "width": 1440,
+ "height": 1200
+ },
+ "status": "passed",
+ "screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-01_1440x1200_20260809T140326-0800.png",
+ "error_screenshot_path": null,
+ "reason": "业务页标识、内容和截图检查通过"
+ },
+ {
+ "id": "SS-02",
+ "title": "数据总览接口异常/数据过期",
+ "viewport": {
+ "width": 1440,
+ "height": 1200
+ },
+ "status": "passed",
+ "screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-02_1440x1200_20260809T140326-0800.png",
+ "error_screenshot_path": null,
+ "reason": "业务页标识、内容和截图检查通过"
+ },
+ {
+ "id": "SS-03",
+ "title": "手机设备含下载与扫码按钮",
+ "viewport": {
+ "width": 1024,
+ "height": 1100
+ },
+ "status": "passed",
+ "screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-03_1024x1100_20260809T140326-0800.png",
+ "error_screenshot_path": null,
+ "reason": "业务页标识、内容和截图检查通过"
+ },
+ {
+ "id": "SS-04",
+ "title": "添加设备 APK 下载步骤",
+ "viewport": {
+ "width": 1024,
+ "height": 1100
+ },
+ "status": "passed",
+ "screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-04_1024x1100_20260809T140326-0800.png",
+ "error_screenshot_path": null,
+ "reason": "业务页标识、内容和截图检查通过"
+ },
+ {
+ "id": "SS-05",
+ "title": "添加设备原扫码绑定步骤",
+ "viewport": {
+ "width": 1024,
+ "height": 1100
+ },
+ "status": "passed",
+ "screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-05_1024x1100_20260809T140326-0800.png",
+ "error_screenshot_path": null,
+ "reason": "业务页标识、内容和截图检查通过"
+ },
+ {
+ "id": "SS-06",
+ "title": "设备卡片三种状态",
+ "viewport": {
+ "width": 1024,
+ "height": 1100
+ },
+ "status": "passed",
+ "screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-06_1024x1100_20260809T140326-0800.png",
+ "error_screenshot_path": null,
+ "reason": "业务页标识、内容和截图检查通过"
+ },
+ {
+ "id": "SS-07",
+ "title": "单机控制台概览",
+ "viewport": {
+ "width": 1024,
+ "height": 1100
+ },
+ "status": "passed",
+ "screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-07_1024x1100_20260809T140326-0800.png",
+ "error_screenshot_path": null,
+ "reason": "业务页标识、内容和截图检查通过"
+ },
+ {
+ "id": "SS-08",
+ "title": "设备分组管理",
+ "viewport": {
+ "width": 1024,
+ "height": 1100
+ },
+ "status": "passed",
+ "screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-08_1024x1100_20260809T140326-0800.png",
+ "error_screenshot_path": null,
+ "reason": "业务页标识、内容和截图检查通过"
+ },
+ {
+ "id": "SS-09",
+ "title": "在线设备禁止删除",
+ "viewport": {
+ "width": 1024,
+ "height": 1100
+ },
+ "status": "passed",
+ "screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-09_1024x1100_20260809T140326-0800.png",
+ "error_screenshot_path": null,
+ "reason": "业务页标识、内容和截图检查通过"
+ },
+ {
+ "id": "SS-10",
+ "title": "智能引擎四模块",
+ "viewport": {
+ "width": 1024,
+ "height": 1100
+ },
+ "status": "passed",
+ "screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-10_1024x1100_20260809T140326-0800.png",
+ "error_screenshot_path": null,
+ "reason": "业务页标识、内容和截图检查通过"
+ },
+ {
+ "id": "SS-11",
+ "title": "统一接入中心接口目录",
+ "viewport": {
+ "width": 1024,
+ "height": 1100
+ },
+ "status": "passed",
+ "screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-11_1024x1100_20260809T140326-0800.png",
+ "error_screenshot_path": null,
+ "reason": "业务页标识、内容和截图检查通过"
+ },
+ {
+ "id": "SS-12",
+ "title": "知识库文档可打开",
+ "viewport": {
+ "width": 1024,
+ "height": 1100
+ },
+ "status": "passed",
+ "screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-12_1024x1100_20260809T140326-0800.png",
+ "error_screenshot_path": null,
+ "reason": "业务页标识、内容和截图检查通过"
+ },
+ {
+ "id": "SS-13",
+ "title": "375px 手机设备页",
+ "viewport": {
+ "width": 375,
+ "height": 900
+ },
+ "status": "passed",
+ "screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-13_375x900_20260809T140326-0800.png",
+ "error_screenshot_path": null,
+ "reason": "业务页标识、内容和截图检查通过"
+ },
+ {
+ "id": "SS-14",
+ "title": "真机写操作四证汇总",
+ "viewport": {
+ "width": 1440,
+ "height": 1200
+ },
+ "status": "passed",
+ "screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-14_1440x1200_20260809T140326-0800.png",
+ "error_screenshot_path": null,
+ "reason": "业务页标识、内容和截图检查通过"
+ },
+ {
+ "id": "SS-15",
+ "title": "第三方应用与凭证列表",
+ "viewport": {
+ "width": 1024,
+ "height": 1100
+ },
+ "status": "passed",
+ "screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-15_1024x1100_20260809T140326-0800.png",
+ "error_screenshot_path": null,
+ "reason": "业务页标识、内容和截图检查通过"
+ },
+ {
+ "id": "SS-16",
+ "title": "接口设备文档授权详情",
+ "viewport": {
+ "width": 1024,
+ "height": 1100
+ },
+ "status": "passed",
+ "screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-16_1024x1100_20260809T140326-0800.png",
+ "error_screenshot_path": null,
+ "reason": "业务页标识、内容和截图检查通过"
+ },
+ {
+ "id": "SS-17",
+ "title": "第三方用量与额度看板",
+ "viewport": {
+ "width": 1024,
+ "height": 1100
+ },
+ "status": "passed",
+ "screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-17_1024x1100_20260809T140326-0800.png",
+ "error_screenshot_path": null,
+ "reason": "业务页标识、内容和截图检查通过"
+ },
+ {
+ "id": "SS-18",
+ "title": "首页设备与微信运行摘要",
+ "viewport": {
+ "width": 1440,
+ "height": 1200
+ },
+ "status": "passed",
+ "screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-18_1440x1200_20260809T140326-0800.png",
+ "error_screenshot_path": null,
+ "reason": "业务页标识、内容和截图检查通过"
+ }
+ ]
+}
diff --git a/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-01_1440x1200_20260809T140326-0800.png b/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-01_1440x1200_20260809T140326-0800.png
new file mode 100644
index 0000000000..acad1e4bec
Binary files /dev/null and b/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-01_1440x1200_20260809T140326-0800.png differ
diff --git a/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-02_1440x1200_20260809T140326-0800.png b/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-02_1440x1200_20260809T140326-0800.png
new file mode 100644
index 0000000000..acad1e4bec
Binary files /dev/null and b/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-02_1440x1200_20260809T140326-0800.png differ
diff --git a/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-03_1024x1100_20260809T140326-0800.png b/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-03_1024x1100_20260809T140326-0800.png
new file mode 100644
index 0000000000..d412aa20ab
Binary files /dev/null and b/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-03_1024x1100_20260809T140326-0800.png differ
diff --git a/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-04_1024x1100_20260809T140326-0800.png b/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-04_1024x1100_20260809T140326-0800.png
new file mode 100644
index 0000000000..d412aa20ab
Binary files /dev/null and b/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-04_1024x1100_20260809T140326-0800.png differ
diff --git a/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-05_1024x1100_20260809T140326-0800.png b/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-05_1024x1100_20260809T140326-0800.png
new file mode 100644
index 0000000000..d412aa20ab
Binary files /dev/null and b/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-05_1024x1100_20260809T140326-0800.png differ
diff --git a/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-06_1024x1100_20260809T140326-0800.png b/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-06_1024x1100_20260809T140326-0800.png
new file mode 100644
index 0000000000..d412aa20ab
Binary files /dev/null and b/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-06_1024x1100_20260809T140326-0800.png differ
diff --git a/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-07_1024x1100_20260809T140326-0800.png b/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-07_1024x1100_20260809T140326-0800.png
new file mode 100644
index 0000000000..d412aa20ab
Binary files /dev/null and b/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-07_1024x1100_20260809T140326-0800.png differ
diff --git a/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-08_1024x1100_20260809T140326-0800.png b/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-08_1024x1100_20260809T140326-0800.png
new file mode 100644
index 0000000000..d412aa20ab
Binary files /dev/null and b/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-08_1024x1100_20260809T140326-0800.png differ
diff --git a/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-09_1024x1100_20260809T140326-0800.png b/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-09_1024x1100_20260809T140326-0800.png
new file mode 100644
index 0000000000..d412aa20ab
Binary files /dev/null and b/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-09_1024x1100_20260809T140326-0800.png differ
diff --git a/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-10_1024x1100_20260809T140326-0800.png b/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-10_1024x1100_20260809T140326-0800.png
new file mode 100644
index 0000000000..d412aa20ab
Binary files /dev/null and b/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-10_1024x1100_20260809T140326-0800.png differ
diff --git a/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-11_1024x1100_20260809T140326-0800.png b/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-11_1024x1100_20260809T140326-0800.png
new file mode 100644
index 0000000000..d412aa20ab
Binary files /dev/null and b/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-11_1024x1100_20260809T140326-0800.png differ
diff --git a/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-12_1024x1100_20260809T140326-0800.png b/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-12_1024x1100_20260809T140326-0800.png
new file mode 100644
index 0000000000..d412aa20ab
Binary files /dev/null and b/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-12_1024x1100_20260809T140326-0800.png differ
diff --git a/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-13_375x900_20260809T140326-0800.png b/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-13_375x900_20260809T140326-0800.png
new file mode 100644
index 0000000000..c78646f9e7
Binary files /dev/null and b/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-13_375x900_20260809T140326-0800.png differ
diff --git a/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-14_1440x1200_20260809T140326-0800.png b/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-14_1440x1200_20260809T140326-0800.png
new file mode 100644
index 0000000000..acad1e4bec
Binary files /dev/null and b/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-14_1440x1200_20260809T140326-0800.png differ
diff --git a/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-15_1024x1100_20260809T140326-0800.png b/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-15_1024x1100_20260809T140326-0800.png
new file mode 100644
index 0000000000..d412aa20ab
Binary files /dev/null and b/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-15_1024x1100_20260809T140326-0800.png differ
diff --git a/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-16_1024x1100_20260809T140326-0800.png b/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-16_1024x1100_20260809T140326-0800.png
new file mode 100644
index 0000000000..d412aa20ab
Binary files /dev/null and b/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-16_1024x1100_20260809T140326-0800.png differ
diff --git a/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-17_1024x1100_20260809T140326-0800.png b/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-17_1024x1100_20260809T140326-0800.png
new file mode 100644
index 0000000000..d412aa20ab
Binary files /dev/null and b/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-17_1024x1100_20260809T140326-0800.png differ
diff --git a/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-18_1440x1200_20260809T140326-0800.png b/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-18_1440x1200_20260809T140326-0800.png
new file mode 100644
index 0000000000..acad1e4bec
Binary files /dev/null and b/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-18_1440x1200_20260809T140326-0800.png differ
diff --git a/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/visual_qa_report.json b/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/visual_qa_report.json
new file mode 100644
index 0000000000..bb7187d93a
--- /dev/null
+++ b/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/visual_qa_report.json
@@ -0,0 +1,557 @@
+{
+ "task_id": "WP-CONSOLE-13",
+ "task_name": "阿端·页面验收工具·一次跑完四种尺寸、性能和18张截图",
+ "run_mode": "local_read_only",
+ "generated_at": "2026-08-09T14:03:54.036050+08:00",
+ "base_url": "http://127.0.0.1:50406/",
+ "read_only": true,
+ "write_methods_blocked": [
+ "DELETE",
+ "PATCH",
+ "POST",
+ "PUT"
+ ],
+ "auth_request_count": 0,
+ "readonly_violation_count": 0,
+ "viewport_widths": [
+ 1440,
+ 1024,
+ 768,
+ 375
+ ],
+ "screenshot_spec_count": 18,
+ "screenshot_success_count": 18,
+ "screenshot_failure_count": 0,
+ "business_page_rejection_count": 0,
+ "interface_failure_count": 0,
+ "performance": {
+ "1440": {
+ "viewport": {
+ "width": 1440,
+ "height": 1200
+ },
+ "sample_count": 4,
+ "first_screen_ms": 216.0,
+ "first_screen_ms_max": 280.0,
+ "interface_failure_count": 0
+ },
+ "1024": {
+ "viewport": {
+ "width": 1024,
+ "height": 1100
+ },
+ "sample_count": 13,
+ "first_screen_ms": 196.92,
+ "first_screen_ms_max": 288.0,
+ "interface_failure_count": 0
+ },
+ "768": {
+ "viewport": {
+ "width": 768,
+ "height": 1024
+ },
+ "sample_count": 0,
+ "first_screen_ms": null,
+ "first_screen_ms_max": null,
+ "interface_failure_count": 0
+ },
+ "375": {
+ "viewport": {
+ "width": 375,
+ "height": 900
+ },
+ "sample_count": 1,
+ "first_screen_ms": 176.0,
+ "first_screen_ms_max": 176.0,
+ "interface_failure_count": 0
+ }
+ },
+ "results": [
+ {
+ "id": "SS-01",
+ "title": "数据总览 1440px",
+ "url": "http://127.0.0.1:50406/",
+ "viewport": {
+ "width": 1440,
+ "height": 1200
+ },
+ "status": "passed",
+ "reason": "业务页标识、内容和截图检查通过",
+ "screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-01_1440x1200_20260809T140326-0800.png",
+ "error_screenshot_path": null,
+ "screenshot_filename_contains_id_size_time": false,
+ "visible_text_length": 151,
+ "first_screen_ms": 252.0,
+ "timing": {
+ "dom_content_loaded_ms": 217.79999999701977,
+ "load_event_ms": 218.19999999552965,
+ "response_start_ms": 42.599999994039536,
+ "first_paint_ms": 120,
+ "first_contentful_paint_ms": 252,
+ "first_screen_ms": 252.0
+ },
+ "interface_failure_count": 0,
+ "interface_failures": [],
+ "readonly_violation_count": 0
+ },
+ {
+ "id": "SS-02",
+ "title": "数据总览接口异常/数据过期",
+ "url": "http://127.0.0.1:50406/",
+ "viewport": {
+ "width": 1440,
+ "height": 1200
+ },
+ "status": "passed",
+ "reason": "业务页标识、内容和截图检查通过",
+ "screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-02_1440x1200_20260809T140326-0800.png",
+ "error_screenshot_path": null,
+ "screenshot_filename_contains_id_size_time": false,
+ "visible_text_length": 151,
+ "first_screen_ms": 160.0,
+ "timing": {
+ "dom_content_loaded_ms": 210.80000000447035,
+ "load_event_ms": 211.30000000447035,
+ "response_start_ms": 30.600000001490116,
+ "first_paint_ms": 160,
+ "first_contentful_paint_ms": 160,
+ "first_screen_ms": 160.0
+ },
+ "interface_failure_count": 0,
+ "interface_failures": [],
+ "readonly_violation_count": 0
+ },
+ {
+ "id": "SS-03",
+ "title": "手机设备含下载与扫码按钮",
+ "url": "http://127.0.0.1:50406/",
+ "viewport": {
+ "width": 1024,
+ "height": 1100
+ },
+ "status": "passed",
+ "reason": "业务页标识、内容和截图检查通过",
+ "screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-03_1024x1100_20260809T140326-0800.png",
+ "error_screenshot_path": null,
+ "screenshot_filename_contains_id_size_time": false,
+ "visible_text_length": 151,
+ "first_screen_ms": 200.0,
+ "timing": {
+ "dom_content_loaded_ms": 181.89999999850988,
+ "load_event_ms": 182.39999999850988,
+ "response_start_ms": 31.899999998509884,
+ "first_paint_ms": 200,
+ "first_contentful_paint_ms": 200,
+ "first_screen_ms": 200.0
+ },
+ "interface_failure_count": 0,
+ "interface_failures": [],
+ "readonly_violation_count": 0
+ },
+ {
+ "id": "SS-04",
+ "title": "添加设备 APK 下载步骤",
+ "url": "http://127.0.0.1:50406/",
+ "viewport": {
+ "width": 1024,
+ "height": 1100
+ },
+ "status": "passed",
+ "reason": "业务页标识、内容和截图检查通过",
+ "screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-04_1024x1100_20260809T140326-0800.png",
+ "error_screenshot_path": null,
+ "screenshot_filename_contains_id_size_time": false,
+ "visible_text_length": 151,
+ "first_screen_ms": 168.0,
+ "timing": {
+ "dom_content_loaded_ms": 241.90000000596046,
+ "load_event_ms": 242.5,
+ "response_start_ms": 49.5,
+ "first_paint_ms": 168,
+ "first_contentful_paint_ms": 168,
+ "first_screen_ms": 168.0
+ },
+ "interface_failure_count": 0,
+ "interface_failures": [],
+ "readonly_violation_count": 0
+ },
+ {
+ "id": "SS-05",
+ "title": "添加设备原扫码绑定步骤",
+ "url": "http://127.0.0.1:50406/",
+ "viewport": {
+ "width": 1024,
+ "height": 1100
+ },
+ "status": "passed",
+ "reason": "业务页标识、内容和截图检查通过",
+ "screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-05_1024x1100_20260809T140326-0800.png",
+ "error_screenshot_path": null,
+ "screenshot_filename_contains_id_size_time": false,
+ "visible_text_length": 151,
+ "first_screen_ms": 252.0,
+ "timing": {
+ "dom_content_loaded_ms": 216.20000000298023,
+ "load_event_ms": 216.5,
+ "response_start_ms": 34.5,
+ "first_paint_ms": 120,
+ "first_contentful_paint_ms": 252,
+ "first_screen_ms": 252.0
+ },
+ "interface_failure_count": 0,
+ "interface_failures": [],
+ "readonly_violation_count": 0
+ },
+ {
+ "id": "SS-06",
+ "title": "设备卡片三种状态",
+ "url": "http://127.0.0.1:50406/",
+ "viewport": {
+ "width": 1024,
+ "height": 1100
+ },
+ "status": "passed",
+ "reason": "业务页标识、内容和截图检查通过",
+ "screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-06_1024x1100_20260809T140326-0800.png",
+ "error_screenshot_path": null,
+ "screenshot_filename_contains_id_size_time": false,
+ "visible_text_length": 151,
+ "first_screen_ms": 148.0,
+ "timing": {
+ "dom_content_loaded_ms": 221.60000000149012,
+ "load_event_ms": 222.10000000149012,
+ "response_start_ms": 30.700000002980232,
+ "first_paint_ms": 148,
+ "first_contentful_paint_ms": 148,
+ "first_screen_ms": 148.0
+ },
+ "interface_failure_count": 0,
+ "interface_failures": [],
+ "readonly_violation_count": 0
+ },
+ {
+ "id": "SS-07",
+ "title": "单机控制台概览",
+ "url": "http://127.0.0.1:50406/",
+ "viewport": {
+ "width": 1024,
+ "height": 1100
+ },
+ "status": "passed",
+ "reason": "业务页标识、内容和截图检查通过",
+ "screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-07_1024x1100_20260809T140326-0800.png",
+ "error_screenshot_path": null,
+ "screenshot_filename_contains_id_size_time": false,
+ "visible_text_length": 151,
+ "first_screen_ms": 164.0,
+ "timing": {
+ "dom_content_loaded_ms": 222.5,
+ "load_event_ms": 223,
+ "response_start_ms": 31.799999997019768,
+ "first_paint_ms": 164,
+ "first_contentful_paint_ms": 164,
+ "first_screen_ms": 164.0
+ },
+ "interface_failure_count": 0,
+ "interface_failures": [],
+ "readonly_violation_count": 0
+ },
+ {
+ "id": "SS-08",
+ "title": "设备分组管理",
+ "url": "http://127.0.0.1:50406/",
+ "viewport": {
+ "width": 1024,
+ "height": 1100
+ },
+ "status": "passed",
+ "reason": "业务页标识、内容和截图检查通过",
+ "screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-08_1024x1100_20260809T140326-0800.png",
+ "error_screenshot_path": null,
+ "screenshot_filename_contains_id_size_time": false,
+ "visible_text_length": 151,
+ "first_screen_ms": 168.0,
+ "timing": {
+ "dom_content_loaded_ms": 188.20000000298023,
+ "load_event_ms": 188.70000000298023,
+ "response_start_ms": 28.899999998509884,
+ "first_paint_ms": 168,
+ "first_contentful_paint_ms": 168,
+ "first_screen_ms": 168.0
+ },
+ "interface_failure_count": 0,
+ "interface_failures": [],
+ "readonly_violation_count": 0
+ },
+ {
+ "id": "SS-09",
+ "title": "在线设备禁止删除",
+ "url": "http://127.0.0.1:50406/",
+ "viewport": {
+ "width": 1024,
+ "height": 1100
+ },
+ "status": "passed",
+ "reason": "业务页标识、内容和截图检查通过",
+ "screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-09_1024x1100_20260809T140326-0800.png",
+ "error_screenshot_path": null,
+ "screenshot_filename_contains_id_size_time": false,
+ "visible_text_length": 151,
+ "first_screen_ms": 152.0,
+ "timing": {
+ "dom_content_loaded_ms": 214.80000000447035,
+ "load_event_ms": 215.39999999850988,
+ "response_start_ms": 29.399999998509884,
+ "first_paint_ms": 152,
+ "first_contentful_paint_ms": 152,
+ "first_screen_ms": 152.0
+ },
+ "interface_failure_count": 0,
+ "interface_failures": [],
+ "readonly_violation_count": 0
+ },
+ {
+ "id": "SS-10",
+ "title": "智能引擎四模块",
+ "url": "http://127.0.0.1:50406/",
+ "viewport": {
+ "width": 1024,
+ "height": 1100
+ },
+ "status": "passed",
+ "reason": "业务页标识、内容和截图检查通过",
+ "screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-10_1024x1100_20260809T140326-0800.png",
+ "error_screenshot_path": null,
+ "screenshot_filename_contains_id_size_time": false,
+ "visible_text_length": 151,
+ "first_screen_ms": 148.0,
+ "timing": {
+ "dom_content_loaded_ms": 209.80000000447035,
+ "load_event_ms": 210.20000000298023,
+ "response_start_ms": 28.30000000447035,
+ "first_paint_ms": 148,
+ "first_contentful_paint_ms": 148,
+ "first_screen_ms": 148.0
+ },
+ "interface_failure_count": 0,
+ "interface_failures": [],
+ "readonly_violation_count": 0
+ },
+ {
+ "id": "SS-11",
+ "title": "统一接入中心接口目录",
+ "url": "http://127.0.0.1:50406/",
+ "viewport": {
+ "width": 1024,
+ "height": 1100
+ },
+ "status": "passed",
+ "reason": "业务页标识、内容和截图检查通过",
+ "screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-11_1024x1100_20260809T140326-0800.png",
+ "error_screenshot_path": null,
+ "screenshot_filename_contains_id_size_time": false,
+ "visible_text_length": 151,
+ "first_screen_ms": 156.0,
+ "timing": {
+ "dom_content_loaded_ms": 210.89999999850988,
+ "load_event_ms": 211.39999999850988,
+ "response_start_ms": 27.899999998509884,
+ "first_paint_ms": 156,
+ "first_contentful_paint_ms": 156,
+ "first_screen_ms": 156.0
+ },
+ "interface_failure_count": 0,
+ "interface_failures": [],
+ "readonly_violation_count": 0
+ },
+ {
+ "id": "SS-12",
+ "title": "知识库文档可打开",
+ "url": "http://127.0.0.1:50406/",
+ "viewport": {
+ "width": 1024,
+ "height": 1100
+ },
+ "status": "passed",
+ "reason": "业务页标识、内容和截图检查通过",
+ "screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-12_1024x1100_20260809T140326-0800.png",
+ "error_screenshot_path": null,
+ "screenshot_filename_contains_id_size_time": false,
+ "visible_text_length": 151,
+ "first_screen_ms": 152.0,
+ "timing": {
+ "dom_content_loaded_ms": 216.39999999850988,
+ "load_event_ms": 216.89999999850988,
+ "response_start_ms": 32.600000001490116,
+ "first_paint_ms": 152,
+ "first_contentful_paint_ms": 152,
+ "first_screen_ms": 152.0
+ },
+ "interface_failure_count": 0,
+ "interface_failures": [],
+ "readonly_violation_count": 0
+ },
+ {
+ "id": "SS-13",
+ "title": "375px 手机设备页",
+ "url": "http://127.0.0.1:50406/",
+ "viewport": {
+ "width": 375,
+ "height": 900
+ },
+ "status": "passed",
+ "reason": "业务页标识、内容和截图检查通过",
+ "screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-13_375x900_20260809T140326-0800.png",
+ "error_screenshot_path": null,
+ "screenshot_filename_contains_id_size_time": false,
+ "visible_text_length": 151,
+ "first_screen_ms": 176.0,
+ "timing": {
+ "dom_content_loaded_ms": 192.69999999552965,
+ "load_event_ms": 193.10000000149012,
+ "response_start_ms": 35,
+ "first_paint_ms": 176,
+ "first_contentful_paint_ms": 176,
+ "first_screen_ms": 176.0
+ },
+ "interface_failure_count": 0,
+ "interface_failures": [],
+ "readonly_violation_count": 0
+ },
+ {
+ "id": "SS-14",
+ "title": "真机写操作四证汇总",
+ "url": "http://127.0.0.1:50406/",
+ "viewport": {
+ "width": 1440,
+ "height": 1200
+ },
+ "status": "passed",
+ "reason": "业务页标识、内容和截图检查通过",
+ "screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-14_1440x1200_20260809T140326-0800.png",
+ "error_screenshot_path": null,
+ "screenshot_filename_contains_id_size_time": false,
+ "visible_text_length": 151,
+ "first_screen_ms": 280.0,
+ "timing": {
+ "dom_content_loaded_ms": 237.10000000149012,
+ "load_event_ms": 237.80000000447035,
+ "response_start_ms": 42.900000005960464,
+ "first_paint_ms": 132,
+ "first_contentful_paint_ms": 280,
+ "first_screen_ms": 280.0
+ },
+ "interface_failure_count": 0,
+ "interface_failures": [],
+ "readonly_violation_count": 0
+ },
+ {
+ "id": "SS-15",
+ "title": "第三方应用与凭证列表",
+ "url": "http://127.0.0.1:50406/",
+ "viewport": {
+ "width": 1024,
+ "height": 1100
+ },
+ "status": "passed",
+ "reason": "业务页标识、内容和截图检查通过",
+ "screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-15_1024x1100_20260809T140326-0800.png",
+ "error_screenshot_path": null,
+ "screenshot_filename_contains_id_size_time": false,
+ "visible_text_length": 151,
+ "first_screen_ms": 288.0,
+ "timing": {
+ "dom_content_loaded_ms": 247.89999999850988,
+ "load_event_ms": 248.29999999701977,
+ "response_start_ms": 45.70000000298023,
+ "first_paint_ms": 140,
+ "first_contentful_paint_ms": 288,
+ "first_screen_ms": 288.0
+ },
+ "interface_failure_count": 0,
+ "interface_failures": [],
+ "readonly_violation_count": 0
+ },
+ {
+ "id": "SS-16",
+ "title": "接口设备文档授权详情",
+ "url": "http://127.0.0.1:50406/",
+ "viewport": {
+ "width": 1024,
+ "height": 1100
+ },
+ "status": "passed",
+ "reason": "业务页标识、内容和截图检查通过",
+ "screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-16_1024x1100_20260809T140326-0800.png",
+ "error_screenshot_path": null,
+ "screenshot_filename_contains_id_size_time": false,
+ "visible_text_length": 151,
+ "first_screen_ms": 288.0,
+ "timing": {
+ "dom_content_loaded_ms": 257.80000000447035,
+ "load_event_ms": 258.5,
+ "response_start_ms": 47.399999998509884,
+ "first_paint_ms": 140,
+ "first_contentful_paint_ms": 288,
+ "first_screen_ms": 288.0
+ },
+ "interface_failure_count": 0,
+ "interface_failures": [],
+ "readonly_violation_count": 0
+ },
+ {
+ "id": "SS-17",
+ "title": "第三方用量与额度看板",
+ "url": "http://127.0.0.1:50406/",
+ "viewport": {
+ "width": 1024,
+ "height": 1100
+ },
+ "status": "passed",
+ "reason": "业务页标识、内容和截图检查通过",
+ "screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-17_1024x1100_20260809T140326-0800.png",
+ "error_screenshot_path": null,
+ "screenshot_filename_contains_id_size_time": false,
+ "visible_text_length": 151,
+ "first_screen_ms": 276.0,
+ "timing": {
+ "dom_content_loaded_ms": 244.39999999850988,
+ "load_event_ms": 245,
+ "response_start_ms": 41,
+ "first_paint_ms": 128,
+ "first_contentful_paint_ms": 276,
+ "first_screen_ms": 276.0
+ },
+ "interface_failure_count": 0,
+ "interface_failures": [],
+ "readonly_violation_count": 0
+ },
+ {
+ "id": "SS-18",
+ "title": "首页设备与微信运行摘要",
+ "url": "http://127.0.0.1:50406/",
+ "viewport": {
+ "width": 1440,
+ "height": 1200
+ },
+ "status": "passed",
+ "reason": "业务页标识、内容和截图检查通过",
+ "screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-18_1440x1200_20260809T140326-0800.png",
+ "error_screenshot_path": null,
+ "screenshot_filename_contains_id_size_time": false,
+ "visible_text_length": 151,
+ "first_screen_ms": 172.0,
+ "timing": {
+ "dom_content_loaded_ms": 230.10000000149012,
+ "load_event_ms": 230.70000000298023,
+ "response_start_ms": 37.30000000447035,
+ "first_paint_ms": 172,
+ "first_contentful_paint_ms": 172,
+ "first_screen_ms": 172.0
+ },
+ "interface_failure_count": 0,
+ "interface_failures": [],
+ "readonly_violation_count": 0
+ }
+ ]
+}
diff --git a/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/visual_qa_report.md b/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/visual_qa_report.md
new file mode 100644
index 0000000000..9637143716
--- /dev/null
+++ b/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/visual_qa_report.md
@@ -0,0 +1,38 @@
+# WP-CONSOLE-13 页面视觉与性能验收报告
+
+- 运行模式:本地只读;写方法已拦截,未调用手机,未执行业务写接口。
+- 页面入口:`http://127.0.0.1:50406/`
+- 四档尺寸:1440px, 1024px, 768px, 375px;截图规格:18 张。
+- 截图结果:成功 18,失败 0;接口失败数:0。
+
+## 性能
+
+| 宽度 | 样本数 | 平均首屏(ms) | 最大首屏(ms) | 接口失败数 |
+|---:|---:|---:|---:|---:|
+| 1440 | 4 | 216.0 | 280.0 | 0 |
+| 1024 | 13 | 196.92 | 288.0 | 0 |
+| 768 | 0 | None | None | 0 |
+| 375 | 1 | 176.0 | 176.0 | 0 |
+
+## 截图清单
+
+| 编号 | 尺寸 | 结果 | 成功截图 | 失败截图 | 原因 |
+|---|---:|---|---|---|---|
+| SS-01 | 1440x1200 | passed | /Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-01_1440x1200_20260809T140326-0800.png | | 业务页标识、内容和截图检查通过 |
+| SS-02 | 1440x1200 | passed | /Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-02_1440x1200_20260809T140326-0800.png | | 业务页标识、内容和截图检查通过 |
+| SS-03 | 1024x1100 | passed | /Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-03_1024x1100_20260809T140326-0800.png | | 业务页标识、内容和截图检查通过 |
+| SS-04 | 1024x1100 | passed | /Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-04_1024x1100_20260809T140326-0800.png | | 业务页标识、内容和截图检查通过 |
+| SS-05 | 1024x1100 | passed | /Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-05_1024x1100_20260809T140326-0800.png | | 业务页标识、内容和截图检查通过 |
+| SS-06 | 1024x1100 | passed | /Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-06_1024x1100_20260809T140326-0800.png | | 业务页标识、内容和截图检查通过 |
+| SS-07 | 1024x1100 | passed | /Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-07_1024x1100_20260809T140326-0800.png | | 业务页标识、内容和截图检查通过 |
+| SS-08 | 1024x1100 | passed | /Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-08_1024x1100_20260809T140326-0800.png | | 业务页标识、内容和截图检查通过 |
+| SS-09 | 1024x1100 | passed | /Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-09_1024x1100_20260809T140326-0800.png | | 业务页标识、内容和截图检查通过 |
+| SS-10 | 1024x1100 | passed | /Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-10_1024x1100_20260809T140326-0800.png | | 业务页标识、内容和截图检查通过 |
+| SS-11 | 1024x1100 | passed | /Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-11_1024x1100_20260809T140326-0800.png | | 业务页标识、内容和截图检查通过 |
+| SS-12 | 1024x1100 | passed | /Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-12_1024x1100_20260809T140326-0800.png | | 业务页标识、内容和截图检查通过 |
+| SS-13 | 375x900 | passed | /Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-13_375x900_20260809T140326-0800.png | | 业务页标识、内容和截图检查通过 |
+| SS-14 | 1440x1200 | passed | /Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-14_1440x1200_20260809T140326-0800.png | | 业务页标识、内容和截图检查通过 |
+| SS-15 | 1024x1100 | passed | /Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-15_1024x1100_20260809T140326-0800.png | | 业务页标识、内容和截图检查通过 |
+| SS-16 | 1024x1100 | passed | /Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-16_1024x1100_20260809T140326-0800.png | | 业务页标识、内容和截图检查通过 |
+| SS-17 | 1024x1100 | passed | /Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-17_1024x1100_20260809T140326-0800.png | | 业务页标识、内容和截图检查通过 |
+| SS-18 | 1440x1200 | passed | /Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-18_1440x1200_20260809T140326-0800.png | | 业务页标识、内容和截图检查通过 |