PrimeBot 启元 Q1 & T1 技能整活工作台
SIMULATION & UNITTESTS · 假机器人替身试毒

0.4 秒赛博替身演武场源码精讲

虚拟快进时钟 + 假机器人替身 + 全浏览器提 Token 测试 + .rpk 产物解剖校验

😜 调皮课代表大白话划重点(30 秒读懂核心)
没见到真机怎么敢保证代码没 Bug?我们在电脑里造了一个「时间快进器 Clock」和一个「全身长满监控探头的假机器人替身 Robot」!原本真机跑一轮要 1~2 分钟,替身在电脑上 0.4 秒就能把 Q1、T1 双足、T1 四足、0.3秒大喘气假空闲、拔网线断网、声音未播完抢跑、越权调接口等 19 道酷刑全测一遍!(当然啦,替身演习过关不等于真机跑通,现场还得真机实弹验两轮!)

🔍 核心机关与修改指南

⏱️ 虚拟快进时钟 Clock 600秒防死循环

把 time.sleep() 变成瞬间加法!要是谁写了死循环超过虚拟 600 秒,当场抛出断言报错抓现行。

🤖 严苛替身 Robot 专抓违规操作

上一句还没播完就发下一句?报错!动作保持时间没等够就说话?报错!Q1 敢调 T1 接口或用错动作白名单?统统报错!

📦 三大测试军团 Skill / Toolkit / Artifact

同时覆盖技能全分支测试、Chrome/Edge/Firefox/MSIX 提 Token 测试,以及 4 个 .rpk 压缩包结构与 Python 3.10 语法树校验!

💻 完整源码与配置内容(共 341 行 · 支持行号定位与一键复制)

📄 共 341 行
1"""本机模拟只验证代码流程;不能证明真实播音、动作执行或安全。"""
2
3import ast
4import contextlib
5import importlib
6import io
7import json
8import os
9import sqlite3
10import sys
11import tempfile
12import types
13import unittest
14import zipfile
15from pathlib import Path
16from unittest.mock import patch
17
18ROOT = Path(__file__).resolve().parents[1]
19WORKSPACE = ROOT.parent
20DEV_KIT = ROOT / "primehub_dev_kit"
21sys.path.insert(0, str(DEV_KIT))
22extract_token = importlib.import_module("extract_token")
23primehub_publish = importlib.import_module("primehub_publish")
24SOURCE = (WORKSPACE / "Example/energy_station/src/main.py").read_text(encoding="utf-8")
25UNIVERSAL_SOURCE = (WORKSPACE / "Example/universal_skill/src/main.py").read_text(encoding="utf-8")
26
27
28class Clock(types.ModuleType):
29 def __init__(self):
30 super().__init__("time")
31 self.now = 0.0
32
33 def monotonic(self):
34 return self.now
35
36 def sleep(self, seconds):
37 self.now += seconds
38 if self.now > 600:
39 raise AssertionError("模拟超过600秒,疑似无界等待")
40
41
42class Robot:
43 def __init__(self, clock, model, mode="normal", form="quadruped"):
44 self.clock = clock
45 self.model = model
46 self.mode = mode
47 self.form = form
48 self.started = None
49 self.duration = 0.0
50 self.last_animation_end = 0.0
51 self.lines = []
52 self.animations = []
53 self.lights = []
54
55 def play_tts(self, text):
56 if self.started is not None and self.clock.now < self.started + self.duration:
57 raise AssertionError("上一句尚未播完就发出了下一句")
58 if self.clock.now < self.last_animation_end:
59 raise AssertionError("未等动作保持时长结束就开始下一句")
60 self.lines.append(text)
61 self.started = self.clock.now + 0.5
62 self.duration = max(len(text) * 0.12, 1.0)
63
64 def get_audio_play_state(self):
65 if self.mode == "invalid":
66 return {"state": None}
67 elapsed = self.clock.now - self.started
68 playing = 0 <= elapsed < self.duration
69 if self.mode == "never_start":
70 playing = False
71 elif self.mode == "never_finish":
72 playing = elapsed >= 0
73 elif self.mode == "jitter" and 1.0 < elapsed < 1.3:
74 playing = False
75 return {"pkg_name": "tts", "state": {"value": 1 if playing else 2}}
76
77 def play_animation(self, animation_id, interrupt):
78 if self.clock.now < self.started + self.duration:
79 raise AssertionError("台词未播完就触发了动作")
80 if self.model == "q1":
81 allowed = {3001, 3008, 3007}
82 elif self.form == "quadruped":
83 allowed = {1, 7, 26, 43}
84 else:
85 allowed = {26, 43}
86 if animation_id not in allowed or interrupt:
87 raise AssertionError("动作白名单或不打断约束被破坏")
88 self.animations.append(animation_id)
89 self.last_animation_end = self.clock.now + (6.5 if self.model == "q1" else 16.0)
90 return self.mode != "reject_animation"
91
92 def play_light(self, color, mode):
93 self.lights.append((color, mode))
94
95 def get_t1_robot_form(self):
96 if self.model != "t1":
97 raise AssertionError("Q1 不应调用 T1 专用接口")
98 return self.form
99
100
101def perform(model, mode="normal", form="quadruped", source_code=SOURCE):
102 clock = Clock()
103 robot = Robot(clock, model, mode, form)
104 output = io.StringIO()
105 script = source_code.replace('ROBOT_MODEL = "q1"', 'ROBOT_MODEL = "%s"' % model)
106 with patch.dict(sys.modules, {"time": clock}), contextlib.redirect_stdout(output):
107 exec(compile(script, "skill/src/main.py", "exec"), {"robot": robot})
108 return robot, output.getvalue(), clock.now
109
110
111class SkillTests(unittest.TestCase):
112 def test_both_models_and_t1_forms_finish_twice(self):
113 for model, form in (("q1", "quadruped"), ("t1", "quadruped"), ("t1", "biped")):
114 for run in range(2):
115 with self.subTest(model=model, form=form, run=run):
116 robot, output, elapsed = perform(model, form=form)
117 self.assertEqual(len(robot.lines), 5)
118 self.assertEqual(len(robot.animations), 3)
119 self.assertIn("ENERGY_STATION_DONE steps=3", output)
120 self.assertLess(elapsed, 120)
121
122 def test_universal_skill_both_models_and_forms(self):
123 for model, form in (("q1", "quadruped"), ("t1", "quadruped"), ("t1", "biped")):
124 with self.subTest(model=model, form=form):
125 robot, output, elapsed = perform(model, form=form, source_code=UNIVERSAL_SOURCE)
126 self.assertEqual(len(robot.lines), 5)
127 self.assertEqual(len(robot.animations), 3)
128 self.assertIn("UNIVERSAL_SKILL_DONE steps=3", output)
129 self.assertLess(elapsed, 120)
130
131 def test_universal_skill_network_fallback_when_offline(self):
132 online_script = UNIVERSAL_SOURCE.replace("ENABLE_NET_QUOTE = False", "ENABLE_NET_QUOTE = True")
133 with patch("urllib.request.urlopen", side_effect=OSError("network unreachable")):
134 robot, output, _ = perform("q1", source_code=online_script)
135 self.assertIn("NET_FALLBACK", output)
136 self.assertIn("UNIVERSAL_SKILL_DONE steps=3", output)
137 self.assertEqual(len(robot.lines), 5)
138
139 def test_short_idle_glitch_does_not_cut_speech(self):
140 for model in ("q1", "t1"):
141 with self.subTest(model=model):
142 robot, output, _ = perform(model, "jitter")
143 self.assertEqual(len(robot.lines), 5)
144 self.assertIn("ENERGY_STATION_DONE", output)
145
146 def test_audio_failures_stop_instead_of_claiming_success(self):
147 for mode, error in (("never_start", "TTS_NOT_STARTED"),
148 ("never_finish", "TTS_NOT_FINISHED"),
149 ("invalid", "AUDIO_STATE_INVALID")):
150 with self.subTest(mode=mode), self.assertRaisesRegex(RuntimeError, error):
151 perform("q1", mode)
152
153 def test_rejected_animation_stops(self):
154 with self.assertRaisesRegex(RuntimeError, "ANIMATION_REJECTED"):
155 perform("q1", "reject_animation")
156
157 def test_unknown_t1_form_stops(self):
158 with self.assertRaisesRegex(RuntimeError, "T1_FORM_UNKNOWN"):
159 perform("t1", form="unknown")
160
161 def test_python_310_and_stdlib_contract(self):
162 for name, code, expected_imports in (
163 ("energy_station", SOURCE, ["time"]),
164 ("universal_skill", UNIVERSAL_SOURCE, ["json", "random", "time", "urllib.request"]),
165 ):
166 with self.subTest(skill=name):
167 tree = ast.parse(code, feature_version=(3, 10))
168 imports = sorted(node.names[0].name for node in ast.walk(tree) if isinstance(node, ast.Import))
169 self.assertEqual(imports, expected_imports)
170 self.assertFalse(any(isinstance(node, ast.ImportFrom) for node in ast.walk(tree)))
171
172
173class ToolkitTests(unittest.TestCase):
174 def test_anonymous_search_does_not_read_token(self):
175 with patch.object(sys, "argv", ["primehub_publish.py", "search", "点歌"]), \
176 patch.object(primehub_publish, "search", return_value=[]) as search, \
177 patch.object(Path, "read_text", side_effect=AssertionError("不应读凭证")):
178 primehub_publish.main()
179 search.assert_called_once_with("点歌")
180
181 def test_search_api_error_is_not_reported_as_empty_success(self):
182 with patch.object(primehub_publish, "call", return_value={"code": 9999, "msg": "synthetic"}):
183 with self.assertRaisesRegex(RuntimeError, "9999"):
184 primehub_publish.search("点歌")
185
186 def test_my_skills_queries_authenticated_account_endpoint(self):
187 response = {"code": 1000, "data": {"records": [{"id": 1}]}}
188 with patch.object(primehub_publish, "call", return_value=response) as call:
189 self.assertEqual(primehub_publish.my_skills("synthetic-token"), [{"id": 1}])
190 call.assert_called_once_with(
191 "skillCenter/queryPageByUserCode", {"page": 1, "rows": 50}, "synthetic-token"
192 )
193
194 def test_my_skills_rejects_expired_login(self):
195 with patch.object(primehub_publish, "call", return_value={"code": 1002, "msg": "synthetic"}):
196 with self.assertRaisesRegex(RuntimeError, "1002"):
197 primehub_publish.my_skills("synthetic-token")
198
199 def test_shared_token_path(self):
200 self.assertEqual(primehub_publish.TOKEN_FILE, extract_token.DEFAULT_TOKEN_FILE)
201 self.assertTrue(Path(extract_token.DEFAULT_TOKEN_FILE).is_absolute())
202
203 def test_msix_firefox_profile_discovery(self):
204 with tempfile.TemporaryDirectory(prefix="qiyuan_msix_test_") as folder:
205 database = (
206 Path(folder) / "Packages/Mozilla.MozillaFirefox_synthetic/LocalCache"
207 / "Roaming/Mozilla/Firefox/Profiles/default.synthetic/storage/default"
208 / "https+++primehub.primebot.com/ls/data.sqlite"
209 )
210 database.parent.mkdir(parents=True)
211 database.touch()
212 with patch.dict(os.environ, {
213 "LOCALAPPDATA": folder, "APPDATA": folder, "PRIMEHUB_FIREFOX_PROFILE": ""
214 }):
215 self.assertIn(database, extract_token.firefox_databases())
216
217 def test_publish_accepts_json_argument_and_metadata_file(self):
218 with tempfile.TemporaryDirectory(prefix="qiyuan_metadata_test_") as folder:
219 metadata_path = Path(folder) / "publish.json"
220 metadata_path.write_text('{"skillName":"synthetic"}', encoding="utf-8")
221 cases = [
222 ["publish", "synthetic.rpk", '{"skillName":"synthetic"}'],
223 ["publish", "synthetic.rpk", "--metadata-file", str(metadata_path)],
224 ]
225 for arguments in cases:
226 with self.subTest(arguments=arguments), \
227 patch.object(sys, "argv", ["primehub_publish.py"] + arguments), \
228 patch.object(Path, "read_text", autospec=True) as read_text, \
229 patch.object(primehub_publish, "upload", return_value="synthetic-url") as upload, \
230 patch.object(primehub_publish, "call", return_value={"code": 1000}) as call, \
231 contextlib.redirect_stdout(io.StringIO()):
232 read_text.side_effect = lambda path, **kwargs: (
233 '{"skillName":"synthetic"}' if path == metadata_path else "synthetic-token"
234 )
235 primehub_publish.main()
236 upload.assert_called_once_with("synthetic.rpk", "synthetic-token")
237 call.assert_called_once_with(
238 "skillCenter/publishSkill",
239 {"skillName": "synthetic", "packageUrl": "synthetic-url"},
240 "synthetic-token",
241 )
242
243 def test_sqlite_backup_reads_wal_and_extraction_writes_selected_path(self):
244 with tempfile.TemporaryDirectory(prefix="qiyuan_token_test_") as folder:
245 profile = Path(folder) / "profile[synthetic]"
246 database = profile / "storage/default/https+++primehub.primebot.com/ls/data.sqlite"
247 database.parent.mkdir(parents=True)
248 synthetic = b'"synthetic.test.token"'
249 compressed = bytes([len(synthetic), (len(synthetic) - 1) << 2]) + synthetic
250 connection = sqlite3.connect(database)
251 try:
252 connection.execute("PRAGMA journal_mode=WAL")
253 connection.execute("CREATE TABLE data (key TEXT, value BLOB)")
254 connection.execute("INSERT INTO data VALUES (?, ?)", ("skillhub_token", compressed))
255 connection.commit()
256 output_path = Path(folder) / "output/ph_token.txt"
257 with patch.dict(os.environ, {"PRIMEHUB_FIREFOX_PROFILE": str(profile), "PRIMEHUB_TOKEN": ""}):
258 self.assertEqual(extract_token.firefox_databases(), [database])
259 self.assertEqual(extract_token.read_storage(database)["skillhub_token"], compressed)
260 with patch.object(extract_token, "firefox_databases", return_value=[database]), \
261 patch.object(sys, "argv", ["extract_token.py", str(output_path)]), \
262 contextlib.redirect_stdout(io.StringIO()) as output:
263 extract_token.main()
264 self.assertEqual(output_path.read_text(encoding="utf-8"), "synthetic.test.token")
265 self.assertNotIn("synthetic.test.token", output.getvalue())
266 finally:
267 connection.close()
268 database.unlink() # Windows 下也应已释放读取连接。
269
270 def test_chromium_leveldb_and_direct_token_extraction(self):
271 fake_jwt = "eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE5OTk5OTk5OTl9.signature_part_12345"
272 with tempfile.TemporaryDirectory(prefix="qiyuan_chrome_test_") as folder:
273 ldb_dir = Path(folder) / "Default/Local Storage/leveldb"
274 ldb_dir.mkdir(parents=True)
275 # 1. 测试 UTF-8 编码的 LevelDB .log 记录
276 record_utf8 = b"_https://primehub.primebot.com\x00\x01skillhub_token\x01\"" + fake_jwt.encode("ascii") + b"\""
277 (ldb_dir / "000003.log").write_bytes(record_utf8)
278 output_path = Path(folder) / "out_chrome.txt"
279 with patch.dict(os.environ, {"PRIMEHUB_CHROME_PROFILE": folder, "PRIMEHUB_TOKEN": ""}), \
280 patch.object(extract_token, "firefox_databases", return_value=[]), \
281 patch.object(sys, "argv", ["extract_token.py", "--browser", "chrome", str(output_path)]), \
282 contextlib.redirect_stdout(io.StringIO()) as out:
283 extract_token.main()
284 self.assertEqual(output_path.read_text(encoding="utf-8"), fake_jwt)
285 self.assertNotIn(fake_jwt, out.getvalue())
286
287 # 2. 测试 UTF-16LE 编码的 LevelDB .ldb 记录
288 (ldb_dir / "000003.log").unlink()
289 record_utf16 = ("_https://primehub.primebot.com\x00skillhub_token\x00" + fake_jwt).encode("utf-16le")
290 (ldb_dir / "000005.ldb").write_bytes(record_utf16)
291 output_utf16 = Path(folder) / "out_edge.txt"
292 with patch.dict(os.environ, {"PRIMEHUB_CHROME_PROFILE": folder, "PRIMEHUB_TOKEN": ""}), \
293 patch.object(extract_token, "firefox_databases", return_value=[]), \
294 patch.object(sys, "argv", ["extract_token.py", "--browser", "edge", str(output_utf16)]), \
295 contextlib.redirect_stdout(io.StringIO()):
296 extract_token.main()
297 self.assertEqual(output_utf16.read_text(encoding="utf-8"), fake_jwt)
298
299 # 3. 测试命令行 --token 直接传入
300 output_direct = Path(folder) / "out_direct.txt"
301 with patch.object(sys, "argv", ["extract_token.py", "--token", f'"{fake_jwt}"', str(output_direct)]), \
302 contextlib.redirect_stdout(io.StringIO()):
303 extract_token.main()
304 self.assertEqual(output_direct.read_text(encoding="utf-8"), fake_jwt)
305
306 def test_missing_firefox_has_actionable_failure(self):
307 with patch.dict(os.environ, {"PRIMEHUB_TOKEN": ""}), \
308 patch.object(extract_token, "firefox_databases", return_value=[]), \
309 patch.object(extract_token, "extract_from_chromium", return_value=None), \
310 patch.object(sys, "argv", ["extract_token.py"]), \
311 self.assertRaisesRegex(SystemExit, "Firefox"):
312 extract_token.main()
313
314
315class ArtifactTests(unittest.TestCase):
316 def test_packages_and_metadata_match_each_model(self):
317 for folder, pkg_prefix, pub_prefix in (
318 ("energy_station", "cans518_energy_station", "publish"),
319 ("universal_skill", "cans518_universal_skill", "publish_universal"),
320 ):
321 for model, model_id, model_code in (("q1", 100, "robot_q"), ("t1", 101, "robot_t")):
322 with self.subTest(skill=folder, model=model):
323 package = ROOT / "dist" / ("%s_%s.rpk" % (folder, model))
324 self.assertLess(package.stat().st_size, 200 * 1024 * 1024)
325 with zipfile.ZipFile(package) as archive:
326 self.assertIsNone(archive.testzip())
327 self.assertEqual(set(archive.namelist()), {"cfg/pkg.yaml", "src/main.py", "readme.md"})
328 config = archive.read("cfg/pkg.yaml").decode("utf-8")
329 script = archive.read("src/main.py").decode("utf-8")
330 self.assertIn("robot_model: " + model, config)
331 self.assertIn("package_name: %s_%s_20261024" % (pkg_prefix, model), config)
332 self.assertIn('ROBOT_MODEL = "%s"' % model, script)
333 ast.parse(script, feature_version=(3, 10))
334 metadata = json.loads((ROOT / "dist" / ("%s_%s.json" % (pub_prefix, model))).read_text(encoding="utf-8"))
335 self.assertEqual(metadata["robotModelIds"], [model_id])
336 self.assertEqual(metadata["robotModelCodeList"], [model_code])
337 self.assertEqual(metadata["version"], "0.1.0")
338
339
340if __name__ == "__main__":
341 unittest.main(verbosity=2)