@sophiacyl/dsh-plugin-waves 0.0.0-stage → 0.1.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/DSH-GO-VERIFICATION.md +57 -0
  2. package/ICON.png +0 -0
  3. package/LICENSE +674 -0
  4. package/README.md +251 -2
  5. package/cordis.patch.yml +8 -0
  6. package/install.ps1 +0 -0
  7. package/install.sh +209 -0
  8. package/lib/client.js +1511 -0
  9. package/lib/host/aliashint.js +70 -0
  10. package/lib/host/attachments.js +25 -0
  11. package/lib/host/command.js +34 -0
  12. package/lib/host/consts.js +16 -0
  13. package/lib/host/dataguide.js +35 -0
  14. package/lib/host/imagebridge.js +41 -0
  15. package/lib/host/native-result.js +91 -0
  16. package/lib/host/native.js +191 -0
  17. package/lib/host/proxy.js +59 -0
  18. package/lib/host/results.js +67 -0
  19. package/lib/host/service.js +263 -0
  20. package/lib/host/tool.js +185 -0
  21. package/lib/host/troubleshoot.js +31 -0
  22. package/lib/host/worldbook.js +80 -0
  23. package/lib/index.js +74 -0
  24. package/locale/en.json +6 -0
  25. package/locale/zh.json +6 -0
  26. package/package.json +136 -4
  27. package/service/ICON.png +0 -0
  28. package/service/ai/AI_DATA.md +221 -0
  29. package/service/knowledge/additional/manual.jsonl +5 -0
  30. package/service/knowledge/wiki/baike.jsonl +1231 -0
  31. package/service/knowledge/wiki/chengjiu.jsonl +36 -0
  32. package/service/knowledge/wiki/gonglve.jsonl +383 -0
  33. package/service/knowledge/wiki/huodong.jsonl +427 -0
  34. package/service/knowledge/wiki/renwu.jsonl +412 -0
  35. package/service/knowledge/wiki/tujian.jsonl +2889 -0
  36. package/service/knowledge/wiki/zixun.jsonl +47 -0
  37. package/service/plugins/__init__.py +0 -0
  38. package/service/pyproject.toml +40 -0
  39. package/service/rover/__init__.py +0 -0
  40. package/service/rover/ai_core/__init__.py +0 -0
  41. package/service/rover/ai_core/models.py +50 -0
  42. package/service/rover/ai_core/rag/__init__.py +5 -0
  43. package/service/rover/ai_core/rag/knowledge.py +23 -0
  44. package/service/rover/ai_core/register.py +86 -0
  45. package/service/rover/ai_core/resource.py +6 -0
  46. package/service/rover/ai_core/skills/__init__.py +0 -0
  47. package/service/rover/ai_core/skills/operations.py +8 -0
  48. package/service/rover/api.py +720 -0
  49. package/service/rover/api_render.py +161 -0
  50. package/service/rover/app_life.py +154 -0
  51. package/service/rover/aps.py +47 -0
  52. package/service/rover/avatar.py +85 -0
  53. package/service/rover/bot.py +133 -0
  54. package/service/rover/builtin.py +152 -0
  55. package/service/rover/cards/__init__.py +26 -0
  56. package/service/rover/cards/_shared.py +129 -0
  57. package/service/rover/cards/char_list.py +258 -0
  58. package/service/rover/cards/char_panel.py +1353 -0
  59. package/service/rover/cards/echo_list.py +206 -0
  60. package/service/rover/cards/matrix_roles.py +25 -0
  61. package/service/rover/cards/rank_boards.py +714 -0
  62. package/service/rover/cards/rank_total.py +664 -0
  63. package/service/rover/compat.py +215 -0
  64. package/service/rover/config.py +163 -0
  65. package/service/rover/custom_panel.py +220 -0
  66. package/service/rover/data_store.py +31 -0
  67. package/service/rover/gss.py +19 -0
  68. package/service/rover/handler.py +191 -0
  69. package/service/rover/help/__init__.py +0 -0
  70. package/service/rover/help/draw_new_plugin_help.py +332 -0
  71. package/service/rover/help/model.py +16 -0
  72. package/service/rover/help/utils.py +7 -0
  73. package/service/rover/hooks.py +420 -0
  74. package/service/rover/html_actions.py +236 -0
  75. package/service/rover/i18n.py +19 -0
  76. package/service/rover/knowledge.py +235 -0
  77. package/service/rover/logger.py +102 -0
  78. package/service/rover/main.py +33 -0
  79. package/service/rover/media.py +222 -0
  80. package/service/rover/message_models.py +23 -0
  81. package/service/rover/models.py +92 -0
  82. package/service/rover/native_media.py +64 -0
  83. package/service/rover/ocr.py +79 -0
  84. package/service/rover/pool.py +43 -0
  85. package/service/rover/restart.py +36 -0
  86. package/service/rover/segment.py +143 -0
  87. package/service/rover/server.py +131 -0
  88. package/service/rover/status/__init__.py +0 -0
  89. package/service/rover/status/models.py +10 -0
  90. package/service/rover/status/plugin_status.py +17 -0
  91. package/service/rover/subscribe.py +148 -0
  92. package/service/rover/sv.py +403 -0
  93. package/service/rover/templates/char_list.html +182 -0
  94. package/service/rover/templates/char_panel.html +465 -0
  95. package/service/rover/templates/echo_list.html +221 -0
  96. package/service/rover/templates/rank_all_total.html +193 -0
  97. package/service/rover/templates/rank_board.html +107 -0
  98. package/service/rover/templates/rank_level_total.html +57 -0
  99. package/service/rover/templates/rank_matrix_total.html +66 -0
  100. package/service/rover/templates/rank_phantom_total.html +180 -0
  101. package/service/rover/templates/rank_slash_total.html +38 -0
  102. package/service/rover/trigger.py +98 -0
  103. package/service/rover/utils/__init__.py +0 -0
  104. package/service/rover/utils/boardcast/__init__.py +0 -0
  105. package/service/rover/utils/boardcast/models.py +15 -0
  106. package/service/rover/utils/cookie_manager/__init__.py +0 -0
  107. package/service/rover/utils/cookie_manager/qrlogin.py +24 -0
  108. package/service/rover/utils/database/__init__.py +0 -0
  109. package/service/rover/utils/database/base_models.py +1771 -0
  110. package/service/rover/utils/database/models.py +80 -0
  111. package/service/rover/utils/database/startup.py +60 -0
  112. package/service/rover/utils/default_bg/bg.jpg +0 -0
  113. package/service/rover/utils/download_resource/__init__.py +0 -0
  114. package/service/rover/utils/download_resource/download_core.py +164 -0
  115. package/service/rover/utils/download_resource/download_file.py +46 -0
  116. package/service/rover/utils/fonts/__init__.py +0 -0
  117. package/service/rover/utils/fonts/fonts.py +33 -0
  118. package/service/rover/utils/image/__init__.py +0 -0
  119. package/service/rover/utils/image/convert.py +51 -0
  120. package/service/rover/utils/image/image_tools.py +30 -0
  121. package/service/rover/utils/image/utils.py +70 -0
  122. package/service/rover/utils/plugins_config/__init__.py +0 -0
  123. package/service/rover/utils/plugins_config/gs_config.py +506 -0
  124. package/service/rover/utils/plugins_config/models.py +182 -0
  125. package/service/rover/web_app.py +3 -0
  126. package/service/rover/webconsole/__init__.py +0 -0
  127. package/service/rover/webconsole/mount_app.py +26 -0
  128. package/service/tools/migrate.py +351 -0
  129. package/service/tools/regression.py +87 -0
  130. package/service/tools/smoke.py +63 -0
  131. package/service/tools/update_plugins.py +212 -0
  132. package/service/tools/verify_actions.py +104 -0
  133. package/service/tools/verify_card.py +82 -0
  134. package/service/tools/wwdata.py +665 -0
  135. package/tests/compat.test.js +181 -0
  136. package/tests/imagebridge.test.js +58 -0
  137. package/tests/media.test.js +68 -0
  138. package/tests/native.test.js +127 -0
  139. package/tests/runtime.test.js +69 -0
  140. package/tests/service.test.js +48 -0
@@ -0,0 +1,212 @@
1
+ #!/usr/bin/env python3
2
+ """拉取/更新上游插件。
3
+
4
+ 插件目录保持上游原样(带 .git), 更新就是 git pull。本地差异全在 rover/hooks.py,
5
+ 不改插件源码, 所以更新不会冲突。
6
+ GitHub 不可达时自动改走 cnb.cool 镜像。
7
+ """
8
+
9
+ import argparse
10
+ import os
11
+ import shutil
12
+ import subprocess
13
+ import sys
14
+ from pathlib import Path
15
+ from typing import Optional
16
+
17
+ PLUGINS_DIR = Path(__file__).resolve().parents[1] / "plugins"
18
+
19
+ REPOS = {
20
+ "XutheringWavesUID": {
21
+ "github": "https://github.com/Loping151/XutheringWavesUID.git",
22
+ "cnb": "https://cnb.cool/gscore-mirror/XutheringWavesUID.git",
23
+ },
24
+ "RoverSign": {
25
+ "github": "https://github.com/Loping151/RoverSign.git",
26
+ "cnb": "https://cnb.cool/gscore-mirror/RoverSign.git",
27
+ },
28
+ "ScoreEcho": {
29
+ "github": "https://github.com/Loping151/ScoreEcho.git",
30
+ "cnb": "https://cnb.cool/gscore-mirror/ScoreEcho.git",
31
+ },
32
+ }
33
+
34
+ # clone/pull 卡住太久也算不可达, 好切镜像
35
+ CLONE_TIMEOUT = 90
36
+ PULL_TIMEOUT = 60
37
+
38
+ _UNREACHABLE = (
39
+ "could not resolve host",
40
+ "failed to connect",
41
+ "connection timed out",
42
+ "operation timed out",
43
+ "network is unreachable",
44
+ "no route to host",
45
+ "unable to access",
46
+ "connection reset",
47
+ "connection refused",
48
+ "the remote end hung up unexpectedly",
49
+ "early eof",
50
+ "rpc failed",
51
+ "gnutlsrecv error",
52
+ "openssl ssl_connect",
53
+ "ssl_connect",
54
+ "tls connect error",
55
+ "couldn't connect to server",
56
+ "empty reply from server",
57
+ "proxy connect aborted",
58
+ "failed to connect to github.com",
59
+ )
60
+
61
+
62
+ def run(args, cwd=None, timeout: Optional[int] = None) -> subprocess.CompletedProcess:
63
+ env = os.environ.copy()
64
+ env.setdefault("GIT_TERMINAL_PROMPT", "0")
65
+ try:
66
+ return subprocess.run(
67
+ args,
68
+ cwd=cwd,
69
+ capture_output=True,
70
+ text=True,
71
+ encoding="utf-8",
72
+ errors="replace",
73
+ timeout=timeout,
74
+ env=env,
75
+ )
76
+ except subprocess.TimeoutExpired as e:
77
+ out = e.stdout.decode("utf-8", "replace") if isinstance(e.stdout, bytes) else (e.stdout or "")
78
+ err = e.stderr.decode("utf-8", "replace") if isinstance(e.stderr, bytes) else (e.stderr or "")
79
+ return subprocess.CompletedProcess(
80
+ args, 124, out, (err + "\nconnection timed out").strip()
81
+ )
82
+
83
+
84
+ def is_unreachable(result: subprocess.CompletedProcess) -> bool:
85
+ if result.returncode == 0:
86
+ return False
87
+ text = f"{result.stderr} {result.stdout}".lower()
88
+ return any(token in text for token in _UNREACHABLE)
89
+
90
+
91
+ def _rm_incomplete(dest: Path) -> None:
92
+ if dest.exists():
93
+ shutil.rmtree(dest, ignore_errors=True)
94
+
95
+
96
+ def clone_with_fallback(name: str, dest: Path, github: str, cnb: str) -> str:
97
+ result = run(["git", "clone", "--depth", "1", github, str(dest)], timeout=CLONE_TIMEOUT)
98
+ source = "github"
99
+ if result.returncode:
100
+ _rm_incomplete(dest)
101
+ if not is_unreachable(result):
102
+ raise RuntimeError(f"{name}: 克隆失败 {result.stderr.strip()[:160]}")
103
+ print(f"{name}: GitHub 不可达, 改用 cnb {cnb}")
104
+ result = run(["git", "clone", "--depth", "1", cnb, str(dest)], timeout=CLONE_TIMEOUT)
105
+ source = "cnb"
106
+ if result.returncode:
107
+ _rm_incomplete(dest)
108
+ raise RuntimeError(f"{name}: cnb 克隆也失败 {result.stderr.strip()[:160]}")
109
+ head = run(["git", "log", "--oneline", "-1"], cwd=dest).stdout.strip()
110
+ return f"{name}: 已克隆 ({source}) {head}"
111
+
112
+
113
+ def _other_remote(origin: str, github: str, cnb: str):
114
+ """当前 origin 不可达时该换去哪一边; 第三方 origin 不擅自改。"""
115
+ if "github.com" in origin:
116
+ return cnb, "cnb"
117
+ if "cnb.cool" in origin:
118
+ return github, "GitHub"
119
+ return None, ""
120
+
121
+
122
+ def pull_with_fallback(name: str, dest: Path, github: str, cnb: str) -> Optional[str]:
123
+ """origin 不可达时切到另一个源再拉一次(两边互为备份)。失败返回错误文案。"""
124
+ result = run(["git", "pull", "--ff-only"], cwd=dest, timeout=PULL_TIMEOUT)
125
+ if result.returncode == 0:
126
+ return None
127
+ if not is_unreachable(result):
128
+ return f"{name}: 拉取失败 {result.stderr.strip()[:160]}"
129
+ origin = run(["git", "remote", "get-url", "origin"], cwd=dest).stdout.strip()
130
+ other, label = _other_remote(origin, github, cnb)
131
+ if not other:
132
+ return f"{name}: 拉取失败 {result.stderr.strip()[:160]}"
133
+ print(f"{name}: 当前源不可达, origin 改为 {label}")
134
+ setu = run(["git", "remote", "set-url", "origin", other], cwd=dest)
135
+ if setu.returncode:
136
+ return f"{name}: 无法改 origin {setu.stderr.strip()[:120]}"
137
+ result = run(["git", "pull", "--ff-only"], cwd=dest, timeout=PULL_TIMEOUT)
138
+ if result.returncode:
139
+ return f"{name}: {label} 拉取也失败 {result.stderr.strip()[:160]}"
140
+ return None
141
+
142
+
143
+ def sync_one(name: str, urls: dict) -> str:
144
+ dest = PLUGINS_DIR / name
145
+ github, cnb = urls["github"], urls["cnb"]
146
+ if not (dest / ".git").is_dir():
147
+ if dest.exists():
148
+ raise RuntimeError(f"{name}: 目录已存在但不是 git 仓库, 请手工处理 {dest}")
149
+ return clone_with_fallback(name, dest, github, cnb)
150
+
151
+ dirty = run(["git", "status", "--porcelain"], cwd=dest).stdout
152
+ local_edits = [ln for ln in dirty.splitlines() if not ln.startswith("??")]
153
+ if local_edits:
154
+ return f"{name}: 有本地改动, 已跳过(本地差异应写进 rover/hooks.py):\n " + "\n ".join(
155
+ local_edits[:5]
156
+ )
157
+
158
+ before = run(["git", "rev-parse", "--short", "HEAD"], cwd=dest).stdout.strip()
159
+ err = pull_with_fallback(name, dest, github, cnb)
160
+ if err:
161
+ raise RuntimeError(err)
162
+ after = run(["git", "rev-parse", "--short", "HEAD"], cwd=dest).stdout.strip()
163
+ if before == after:
164
+ return f"{name}: 已是最新 {after}"
165
+ log = run(["git", "log", "--oneline", f"{before}..{after}"], cwd=dest).stdout.strip()
166
+ return f"{name}: {before} → {after}\n " + "\n ".join(log.splitlines()[:8])
167
+
168
+
169
+ def check_compat() -> int:
170
+ """更新后自检: 上游有没有用到本运行时还没提供的模块。"""
171
+ sys.path.insert(0, str(PLUGINS_DIR.parent))
172
+ from rover import compat
173
+
174
+ absent = [name for name in REPOS if not (PLUGINS_DIR / name / name / "__init__.py").is_file()]
175
+ if absent:
176
+ print("\n上游插件未完整安装: " + ", ".join(absent))
177
+ return 1
178
+ missing = compat.missing_imports(PLUGINS_DIR)
179
+ if missing:
180
+ print("\n运行时缺少这些上游模块, 需要在 rover/compat.py 补映射:")
181
+ for name in missing:
182
+ print(f" {name}")
183
+ return 1
184
+ print("\n兼容层自检通过")
185
+ return 0
186
+
187
+
188
+ def main() -> None:
189
+ parser = argparse.ArgumentParser()
190
+ parser.add_argument("--check-only", action="store_true", help="只做兼容层自检, 不拉取")
191
+ args = parser.parse_args()
192
+
193
+ PLUGINS_DIR.mkdir(parents=True, exist_ok=True)
194
+ (PLUGINS_DIR / "__init__.py").touch()
195
+
196
+ sync_failed = False
197
+ if not args.check_only:
198
+ for name, urls in REPOS.items():
199
+ try:
200
+ print(sync_one(name, urls))
201
+ except RuntimeError as error:
202
+ sync_failed = True
203
+ print(str(error), file=sys.stderr)
204
+
205
+ code = 1 if sync_failed else check_compat()
206
+ if not args.check_only:
207
+ print("\n更新后请重启服务, 并跑 tools/regression.py 自检。")
208
+ sys.exit(code)
209
+
210
+
211
+ if __name__ == "__main__":
212
+ main()
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env python3
2
+ """校验点击命令与卡片里的角色是否一一对上: 用每个格子后面的等级/共鸣链回查上下文。"""
3
+
4
+ import asyncio
5
+ import re
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
10
+
11
+ CASES = {
12
+ "ww深塔": "abyss/abyss_card.html",
13
+ "ww海墟": "abyss/slash_card.html",
14
+ "ww矩阵": "abyss/matrix_detail_card.html",
15
+ "ww全息": "abyss/challenge_card.html",
16
+ }
17
+
18
+ _CELL = re.compile(
19
+ r'<div class="role-mini[^>]*?(?:data-ww-cmd="(?P<cmd>[^"]*)")?[^>]*>(?P<body>.*?)(?=<div class="role-mini|</div>\s*</div>\s*</div>)',
20
+ re.S,
21
+ )
22
+
23
+
24
+ def local_user() -> str:
25
+ """本机使用者标识, 从配置里取, 不写死。"""
26
+ from rover.config import core_config
27
+
28
+ masters = core_config.get_config("masters") or []
29
+ return str(masters[0]) if masters else "local"
30
+
31
+
32
+ async def main() -> None:
33
+ from rover import html_actions
34
+
35
+ captured = []
36
+ original = html_actions.annotate_html
37
+
38
+ def spy(html, template_name, context):
39
+ out = original(html, template_name, context)
40
+ captured.append((template_name, context, out))
41
+ return out
42
+
43
+ html_actions.annotate_html = spy
44
+
45
+ from rover.app_life import load_plugins
46
+ from rover.handler import build_message, dispatch
47
+ from rover.server import core_start_before_execute
48
+ from rover.utils.database.base_models import init_database
49
+ from rover.utils.database.startup import init_all
50
+
51
+ await init_database()
52
+ load_plugins()
53
+ await init_all()
54
+ await core_start_before_execute()
55
+
56
+ # hooks 里的渲染函数按值抓过 annotate_html, 载入之后再把间谍塞进去
57
+ import sys as _sys
58
+
59
+ for module in list(_sys.modules.values()):
60
+ if getattr(module, "annotate_html", None) is original:
61
+ module.annotate_html = spy
62
+
63
+ failures = 0
64
+ for command, template in CASES.items():
65
+ captured.clear()
66
+ await dispatch(build_message(command, user_id=local_user()))
67
+ hit = [c for c in captured if c[0] == template]
68
+ if not hit:
69
+ print(f"{command}: 未渲染 {template}")
70
+ failures += 1
71
+ continue
72
+ _, context, html = hit[-1]
73
+ roles = html_actions._EXTRACTORS[template](context)
74
+ expect = [
75
+ (html_actions._role_command(r), str(r.get("level") or ""), str(r.get("chain_name") or ""))
76
+ for r in roles
77
+ ]
78
+ cells = [m.group(0) for m in html_actions._ROLE_TAG.finditer(html)]
79
+ got_cmds = [
80
+ (re.search(r'data-ww-cmd="([^"]+)"', c).group(1) if 'data-ww-cmd=' in c else None)
81
+ for c in cells
82
+ ]
83
+ mismatch = []
84
+ for i, (want, _lvl, _chain) in enumerate(expect):
85
+ if i >= len(got_cmds):
86
+ mismatch.append(f"#{i} 缺格子")
87
+ elif got_cmds[i] != want:
88
+ mismatch.append(f"#{i} 期望{want} 实得{got_cmds[i]}")
89
+ extra = len(got_cmds) - len(expect)
90
+ names = [c for c in got_cmds if c][:3]
91
+ status = "对齐" if not mismatch and extra == 0 else "错位"
92
+ if mismatch or extra:
93
+ failures += 1
94
+ print(f"{command}: 格子={len(got_cmds)} 上下文={len(expect)} {status} 示例={names}")
95
+ for m in mismatch[:5]:
96
+ print(f" {m}")
97
+ if extra:
98
+ print(f" 多出 {extra} 个格子")
99
+
100
+ print("\n结果:", "全部对齐" if failures == 0 else f"{failures} 项需修")
101
+
102
+
103
+ if __name__ == "__main__":
104
+ asyncio.run(main())
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env python3
2
+ """在真实浏览器里打开一张卡片, 检查渲染尺寸、字体与点击回传。"""
3
+
4
+ import argparse
5
+ import json
6
+ import sys
7
+ import urllib.request
8
+
9
+
10
+ def fetch_card(base: str, command: str) -> str:
11
+ body = json.dumps({"command": command}).encode()
12
+ req = urllib.request.Request(
13
+ f"{base}/api/chat", data=body, headers={"content-type": "application/json"}
14
+ )
15
+ with urllib.request.urlopen(req, timeout=180) as resp:
16
+ data = json.loads(resp.read())
17
+ for seg in data.get("segments") or []:
18
+ if seg.get("type") == "html":
19
+ return seg["url"]
20
+ sys.exit(f"{command} 没有返回 HTML 卡片")
21
+
22
+
23
+ def main() -> None:
24
+ parser = argparse.ArgumentParser()
25
+ parser.add_argument("--base", default="http://127.0.0.1:9777")
26
+ parser.add_argument("--command", default="ww深塔")
27
+ parser.add_argument("--shot", default="/tmp/waves-card.png")
28
+ args = parser.parse_args()
29
+
30
+ url = args.base + fetch_card(args.base, args.command)
31
+ from playwright.sync_api import sync_playwright
32
+
33
+ with sync_playwright() as p:
34
+ browser = p.chromium.launch()
35
+ page = browser.new_page(viewport={"width": 1200, "height": 900})
36
+ messages = []
37
+ page.expose_function("__wwCapture", lambda m: messages.append(m))
38
+ page.add_init_script(
39
+ "window.parent.postMessage = (m) => window.__wwCapture(JSON.stringify(m));"
40
+ )
41
+ errors = []
42
+ page.on("pageerror", lambda e: errors.append(str(e)))
43
+ page.goto(url, wait_until="load", timeout=60000)
44
+ page.wait_for_timeout(1500)
45
+
46
+ size = page.evaluate(
47
+ "() => { const el = document.querySelector('.container') || document.body;"
48
+ " const r = el.getBoundingClientRect();"
49
+ " return { w: Math.round(r.width), h: Math.round(el.scrollHeight) }; }"
50
+ )
51
+ clickable = page.locator("[data-ww-cmd]")
52
+ count = clickable.count()
53
+ font_ok = page.evaluate(
54
+ "() => [...document.styleSheets].some(s => { try { return s.href && s.href.includes('fonts.css') }"
55
+ " catch(e) { return false } }) || document.fonts.size > 0"
56
+ )
57
+
58
+ first_cmd = clickable.first.get_attribute("data-ww-cmd") if count else None
59
+ if count:
60
+ clickable.first.click()
61
+ page.wait_for_timeout(400)
62
+
63
+ page.screenshot(path=args.shot, full_page=False)
64
+ browser.close()
65
+
66
+ sizes = [m for m in messages if '"size"' in m]
67
+ commands = [json.loads(m) for m in messages if '"command"' in m]
68
+ print(f"卡片: {url}")
69
+ print(f"渲染尺寸: {size['w']}x{size['h']} 字体已加载: {font_ok}")
70
+ print(f"可点击角色: {count} 个, 首个命令: {first_cmd}")
71
+ print(f"尺寸回传: {len(sizes)} 次")
72
+ print(f"点击回传: {commands}")
73
+ print(f"页面错误: {errors or '无'}")
74
+ print(f"截图: {args.shot}")
75
+
76
+ ok = count > 0 and commands and commands[0].get("command") == first_cmd and not errors
77
+ print("\n结论:", "交互正常" if ok else "需检查")
78
+ sys.exit(0 if ok else 1)
79
+
80
+
81
+ if __name__ == "__main__":
82
+ main()