完善模式卡片、NFC及多设备控制流程
This commit is contained in:
@@ -289,6 +289,247 @@ operations:
|
||||
self.service.mode_profiles = original_registry
|
||||
self.service.artifact_root = original_artifact_root
|
||||
|
||||
def test_office_mode_generation_and_card_execution_reuse_saved_flow(self) -> None:
|
||||
original_registry = self.service.mode_profiles
|
||||
original_artifact_root = self.service.artifact_root
|
||||
original_extension = SeatDemoHandler.extension
|
||||
original_nfc = SeatDemoHandler.nfc
|
||||
instruction = (
|
||||
"空调温度设置为25度,风速设置为1档,"
|
||||
"启动闪烁灯,颜色设置为红色"
|
||||
)
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
base = Path(directory)
|
||||
preset_root = base / "presets"
|
||||
user_root = base / "users"
|
||||
preset = preset_root / "vehicles" / "denza_n7" / "office.yaml"
|
||||
preset.parent.mkdir(parents=True)
|
||||
preset.write_text(
|
||||
"""
|
||||
id: office
|
||||
name: 办公模式
|
||||
icon: briefcase
|
||||
scope:
|
||||
vehicle_ids: [denza_n7]
|
||||
operations:
|
||||
- domain: climate
|
||||
capability: climate.set_temperature
|
||||
target_scope: driver
|
||||
parameters:
|
||||
temperature_c: 24
|
||||
""".strip()
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
extension = SelfExtensionService(
|
||||
SimulationGateway(),
|
||||
catalog_db_path=base / "office-mode-ir.sqlite3",
|
||||
)
|
||||
extension.sync_peripheral_catalog(
|
||||
[
|
||||
{
|
||||
"id": "ir-slot:tony-light-001:1",
|
||||
"status": "learned",
|
||||
"enabled": True,
|
||||
"device_id": "tony-light-001",
|
||||
"device_name": "闪灯玩具",
|
||||
"object_names": ["闪灯玩具", "闪烁灯", "闪光灯"],
|
||||
"card_uid": "2CC8B103",
|
||||
"action": "打开",
|
||||
"action_names": ["打开", "开启", "启动"],
|
||||
"risk": "low",
|
||||
"slot": 1,
|
||||
},
|
||||
{
|
||||
"id": "ir-slot:tony-light-001:3",
|
||||
"status": "learned",
|
||||
"enabled": True,
|
||||
"device_id": "tony-light-001",
|
||||
"device_name": "闪灯玩具",
|
||||
"object_names": ["闪灯玩具", "闪烁灯", "闪光灯"],
|
||||
"card_uid": "2CC8B103",
|
||||
"action": "切换到红色",
|
||||
"action_names": [
|
||||
"切换到红色",
|
||||
"切换为红色",
|
||||
"设置为红色",
|
||||
],
|
||||
"risk": "low",
|
||||
"slot": 3,
|
||||
},
|
||||
]
|
||||
)
|
||||
self.service.mode_profiles = ModeProfileRegistry(
|
||||
preset_root,
|
||||
user_root=user_root,
|
||||
)
|
||||
self.service.artifact_root = base / "runs"
|
||||
SeatDemoHandler.extension = extension
|
||||
SeatDemoHandler.nfc = SimpleNamespace(
|
||||
snapshot=lambda: {
|
||||
"revision": 1,
|
||||
"reader": {"online": True},
|
||||
"card": {"action": "present", "uid": "2CC8B103"},
|
||||
}
|
||||
)
|
||||
try:
|
||||
generate_request = urllib.request.Request(
|
||||
f"{self.base_url}/api/requests/preview",
|
||||
method="POST",
|
||||
headers={"Content-Type": "application/json"},
|
||||
data=json.dumps(
|
||||
{
|
||||
"utterance": instruction,
|
||||
"provider": "local",
|
||||
"conversation_id": "conv-office-mode-generate",
|
||||
"vehicle_id": "denza_n7",
|
||||
"execution_mode": "dry_run",
|
||||
"control_transport": "wired_can",
|
||||
"client_type": "mode_editor_compile",
|
||||
},
|
||||
ensure_ascii=False,
|
||||
).encode("utf-8"),
|
||||
)
|
||||
with self.opener.open(generate_request, timeout=3) as response:
|
||||
generated = json.loads(response.read().decode("utf-8"))
|
||||
|
||||
save_request = urllib.request.Request(
|
||||
f"{self.base_url}/api/mode-profiles/office",
|
||||
method="POST",
|
||||
headers={"Content-Type": "application/json"},
|
||||
data=json.dumps(
|
||||
{
|
||||
"name": "办公模式",
|
||||
"instruction": instruction,
|
||||
"source_request_id": generated["request_id"],
|
||||
"vehicle_id": "denza_n7",
|
||||
},
|
||||
ensure_ascii=False,
|
||||
).encode("utf-8"),
|
||||
)
|
||||
with self.opener.open(save_request, timeout=3) as response:
|
||||
saved = json.loads(response.read().decode("utf-8"))
|
||||
|
||||
execute_request = urllib.request.Request(
|
||||
f"{self.base_url}/api/mode-profiles/office/execute",
|
||||
method="POST",
|
||||
headers={"Content-Type": "application/json"},
|
||||
data=json.dumps(
|
||||
{
|
||||
"conversation_id": "conv-office-mode-execute",
|
||||
"vehicle_id": "denza_n7",
|
||||
"execution_mode": "dry_run",
|
||||
"control_transport": "wired_can",
|
||||
"client_type": "web_console_mode_card",
|
||||
},
|
||||
ensure_ascii=False,
|
||||
).encode("utf-8"),
|
||||
)
|
||||
with patch.object(
|
||||
self.service,
|
||||
"_interpret",
|
||||
side_effect=AssertionError(
|
||||
"saved mode card must not reinterpret natural language"
|
||||
),
|
||||
), self.opener.open(execute_request, timeout=3) as response:
|
||||
executed = json.loads(response.read().decode("utf-8"))
|
||||
finally:
|
||||
self.service.mode_profiles = original_registry
|
||||
self.service.artifact_root = original_artifact_root
|
||||
SeatDemoHandler.extension = original_extension
|
||||
SeatDemoHandler.nfc = original_nfc
|
||||
|
||||
generated_commands = [
|
||||
(
|
||||
command.get("signals", {}).get("debug_command")
|
||||
or (
|
||||
f"infrared emit {command.get('signals', {}).get('slot')}"
|
||||
if command.get("message") == "infrared.emit"
|
||||
else None
|
||||
)
|
||||
)
|
||||
for plan in generated.get("command_plans", {}).values()
|
||||
for command in plan.get("spec", {}).get("commands", [])
|
||||
]
|
||||
self.assertEqual(
|
||||
generated_commands,
|
||||
[
|
||||
"ac temp driver 25",
|
||||
"ac fan 1",
|
||||
"infrared emit 1",
|
||||
"infrared emit 3",
|
||||
],
|
||||
)
|
||||
self.assertEqual(saved["mode_profile"]["command_flow"]["execution_steps"], [])
|
||||
self.assertEqual(executed["status"], "succeeded")
|
||||
self.assertEqual(executed["utterance"], "办公模式")
|
||||
self.assertEqual(executed["provider_used"], "saved_mode_command_flow")
|
||||
self.assertEqual(executed["orchestration_route"], "saved_mode_command_flow")
|
||||
self.assertEqual(
|
||||
[item["slot"] for item in executed["infrared_actions"]],
|
||||
[1, 3],
|
||||
)
|
||||
self.assertTrue(
|
||||
all(
|
||||
item["parse_source"] == "saved_mode_command_flow"
|
||||
for item in executed["infrared_actions"]
|
||||
)
|
||||
)
|
||||
|
||||
def test_saved_mode_accepts_prefixed_self_extension_flow_key(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
extension = SelfExtensionService(
|
||||
SimulationGateway(),
|
||||
catalog_db_path=Path(directory, "prefixed-mode-ir.sqlite3"),
|
||||
)
|
||||
extension.sync_peripheral_catalog(
|
||||
[
|
||||
{
|
||||
"id": "ir-slot:tony-light-001:1",
|
||||
"status": "learned",
|
||||
"enabled": True,
|
||||
"device_id": "tony-light-001",
|
||||
"device_name": "闪灯玩具",
|
||||
"card_uid": "2CC8B103",
|
||||
"action": "打开",
|
||||
"slot": 1,
|
||||
}
|
||||
]
|
||||
)
|
||||
handler = object.__new__(SeatDemoHandler)
|
||||
handler.extension = extension
|
||||
handler.nfc = SimpleNamespace(
|
||||
snapshot=lambda: {
|
||||
"card": {"action": "present", "uid": "2CC8B103"}
|
||||
}
|
||||
)
|
||||
profile = SimpleNamespace(
|
||||
command_flow={
|
||||
"function_configs": {
|
||||
"local_self_extension": {
|
||||
"spec": {
|
||||
"domain": "self_extension",
|
||||
"operations": [
|
||||
{
|
||||
"capability": "infrared.emit",
|
||||
"target": {"device": "tony-light-001"},
|
||||
"parameters": {"action": "打开", "slot": 1},
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
entries = handler._saved_mode_infrared_entries(profile)
|
||||
|
||||
self.assertEqual([item["capability"]["slot"] for item in entries], [1])
|
||||
self.assertEqual(
|
||||
entries[0]["capability"]["parse_source"],
|
||||
"saved_mode_command_flow",
|
||||
)
|
||||
|
||||
def test_head_unit_display_sync_tracks_tasks_and_usb_catalog(self) -> None:
|
||||
task_request = urllib.request.Request(
|
||||
f"{self.base_url}/api/requests",
|
||||
@@ -344,14 +585,14 @@ operations:
|
||||
def test_static_console_and_runtime_endpoint(self) -> None:
|
||||
with self.opener.open(f"{self.base_url}/", timeout=3) as response:
|
||||
html = response.read().decode("utf-8")
|
||||
with self.opener.open(f"{self.base_url}/app.js?v=dual-seat-107", timeout=3) as response:
|
||||
with self.opener.open(f"{self.base_url}/app.js?v=dual-seat-109", timeout=3) as response:
|
||||
app_script = response.read().decode("utf-8")
|
||||
with self.opener.open(
|
||||
f"{self.base_url}/gesture-control.js?v=android-pad-9", timeout=3
|
||||
) as response:
|
||||
gesture_script = response.read().decode("utf-8")
|
||||
with self.opener.open(
|
||||
f"{self.base_url}/console-v2.js?v=delivery-state-49", timeout=3
|
||||
f"{self.base_url}/console-v2.js?v=delivery-state-50", timeout=3
|
||||
) as response:
|
||||
console_script = response.read().decode("utf-8")
|
||||
with self.opener.open(
|
||||
@@ -394,11 +635,11 @@ operations:
|
||||
self.assertIn("CMD 0x431 · FB 0x41A · CH1", html)
|
||||
self.assertIn("CMD 0x431 · FB 0x388 · CH1", html)
|
||||
self.assertNotIn("透明车体 · 实体设备", html)
|
||||
self.assertIn('src="/app.js?v=dual-seat-107"', html)
|
||||
self.assertIn('src="/app.js?v=dual-seat-109"', html)
|
||||
self.assertIn('href="/styles.css?v=dual-seat-16"', html)
|
||||
self.assertIn('src="/gesture-control.js?v=android-pad-9"', html)
|
||||
self.assertNotIn('class="vehicle-system-hud"', html)
|
||||
self.assertIn('src="/console-v2.js?v=delivery-state-49"', html)
|
||||
self.assertIn('src="/console-v2.js?v=delivery-state-50"', html)
|
||||
self.assertIn('href="/console-v2.css?v=delivery-state-43"', html)
|
||||
self.assertIn('<body class="console-v2">', html)
|
||||
self.assertIn('id="v2BootScreen"', html)
|
||||
@@ -641,7 +882,8 @@ operations:
|
||||
)
|
||||
self.assertIn("const stageHasLayout = () =>", app_script)
|
||||
self.assertIn("window.requestAnimationFrame(initSeatTwin)", app_script)
|
||||
self.assertIn('client_type: "web_console"', app_script)
|
||||
self.assertIn('clientType = "web_console"', app_script)
|
||||
self.assertIn("client_type: clientType", app_script)
|
||||
self.assertIn("function executionVerificationStatus(result = {})", app_script)
|
||||
self.assertIn('"submitted_unverified"', app_script)
|
||||
self.assertIn("指令已提交,等待状态确认", app_script)
|
||||
@@ -649,8 +891,8 @@ operations:
|
||||
self.assertIn("CAN 指令已提交,等待车辆状态确认", console_script)
|
||||
self.assertIn('canvas.addEventListener("webglcontextlost"', app_script)
|
||||
self.assertIn('"cognitive-agent:module-change"', app_script)
|
||||
self.assertIn('const CACHE_VERSION = "cognitive-agent-pad-v218"', service_worker)
|
||||
self.assertIn('"/app.js?v=dual-seat-107"', service_worker)
|
||||
self.assertIn('const CACHE_VERSION = "cognitive-agent-pad-v220"', service_worker)
|
||||
self.assertIn('"/app.js?v=dual-seat-109"', service_worker)
|
||||
self.assertIn('"/assets/pets/self-extension-cat.png?v=3"', service_worker)
|
||||
self.assertIn(".then(() => self.skipWaiting())", service_worker)
|
||||
self.assertNotIn("const cached = await caches.match(request)", service_worker)
|
||||
@@ -890,7 +1132,7 @@ operations:
|
||||
) as response:
|
||||
extension_script = response.read().decode("utf-8")
|
||||
with self.opener.open(
|
||||
f"{self.base_url}/app.js?v=dual-seat-107", timeout=3
|
||||
f"{self.base_url}/app.js?v=dual-seat-109", timeout=3
|
||||
) as response:
|
||||
app_script = response.read().decode("utf-8")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user