feat: synchronize CH1 vehicle control and feedback

This commit is contained in:
vee
2026-08-11 10:42:13 +08:00
parent 3701192a11
commit 32379a1573
50 changed files with 13636 additions and 736 deletions
+194 -16
View File
@@ -35,7 +35,10 @@ class SeatWebApiTests(unittest.TestCase):
# per-request approval token.
with patch.dict(
os.environ,
{"COGNITIVE_AGENT_HARDWARE_EXECUTION_ENABLED": "false"},
{
"COGNITIVE_AGENT_HARDWARE_EXECUTION_ENABLED": "false",
"COGNITIVE_AGENT_VEHICLE_CAN_ENABLED": "false",
},
clear=False,
):
service = SeatDemoService(REPOSITORY_ROOT)
@@ -115,7 +118,7 @@ class SeatWebApiTests(unittest.TestCase):
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=seat-hil-44", timeout=3) as response:
with self.opener.open(f"{self.base_url}/app.js?v=seat-hil-51", 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-6", timeout=3
@@ -130,7 +133,7 @@ class SeatWebApiTests(unittest.TestCase):
) as response:
console_styles = response.read().decode("utf-8")
with self.opener.open(
f"{self.base_url}/self-extension.js?v=extension-6", timeout=3
f"{self.base_url}/self-extension.js?v=extension-15", timeout=3
) as response:
extension_script = response.read().decode("utf-8")
with self.opener.open(
@@ -148,14 +151,21 @@ class SeatWebApiTests(unittest.TestCase):
self.assertIn('id="seatTwinCanvas"', html)
self.assertIn('data-vehicle-model="byd-tang-2024-reference"', html)
self.assertIn("整车孪生界面", html)
self.assertIn("<strong>阅读灯</strong>", html)
self.assertIn("<strong>车内灯光</strong>", html)
self.assertIn('id="interiorLightPower"', html)
self.assertIn('id="ambientBrightness"', html)
self.assertIn('id="ambientColorIndex"', html)
self.assertIn('data-function="massage"', html)
self.assertIn("CMD 0x1DE · FB 0x404 · CH1", html)
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=seat-hil-44"', html)
self.assertIn('src="/app.js?v=seat-hil-51"', html)
self.assertIn('src="/gesture-control.js?v=android-pad-6"', html)
self.assertNotIn('class="vehicle-system-hud"', html)
self.assertIn('src="/console-v2.js?v=desktop-refactor-28"', html)
self.assertIn('href="/console-v2.css?v=desktop-refactor-68"', html)
self.assertIn('src="/self-extension.js?v=extension-6"', html)
self.assertIn('src="/self-extension.js?v=extension-15"', html)
self.assertIn('href="/self-extension.css?v=extension-9"', html)
self.assertNotIn("滚动查看更多", html)
self.assertIn('aria-label="执行车舱控制指令"', html)
@@ -227,6 +237,10 @@ class SeatWebApiTests(unittest.TestCase):
self.assertNotIn('id="hardwareClientLabel"', html)
self.assertNotIn('id="requestHardwareAccess"', html)
self.assertNotIn('id="releaseHardwareAccess"', html)
self.assertNotIn('id="approvalField"', html)
self.assertNotIn('id="approvalToken"', html)
self.assertNotIn("approval_token: approvalToken", app_script)
self.assertNotIn("授权并真实执行", app_script)
self.assertIn('const dynamicModeOrder = ["rocking_horse", "dynamic_cradle"]', app_script)
self.assertIn('divider.className = "mode-group-divider"', app_script)
self.assertIn(
@@ -234,6 +248,10 @@ class SeatWebApiTests(unittest.TestCase):
app_script,
)
self.assertIn("hardwareButton.disabled = !hardwareExecutionReady", app_script)
self.assertIn('const seat = driverOnly ? "driver" : "passenger"', app_script)
self.assertIn('data-mode-command="seat driver vent low"', app_script)
self.assertIn("CH1 主驾座椅与车身设备物理反馈联合展示", app_script)
self.assertIn("CH1 主驾实车测试不启用无闭环的座椅位置手势", app_script)
self.assertIn("车端 CAN 未启用,其他领域保持虚拟执行", app_script)
self.assertIn("function extractReferenceWindowPanes", app_script)
self.assertIn("function collectReferenceFrontLampMeshes", app_script)
@@ -268,6 +286,12 @@ class SeatWebApiTests(unittest.TestCase):
self.assertNotIn('id="extensionConnectionButton"', extension_script)
self.assertNotIn('id="extensionStatusDialog"', extension_script)
self.assertIn('/api/extension/${path}', extension_script)
self.assertIn("HEALTH_POLL_INTERVAL_MS", extension_script)
self.assertIn("HEALTH_REQUEST_TIMEOUT_MS", extension_script)
self.assertIn("healthState.listening", extension_script)
self.assertIn("healthState.peer", extension_script)
self.assertIn('healthState.message || "GSCU1B 尚未连接"', extension_script)
self.assertIn('window.addEventListener("pageshow"', extension_script)
self.assertIn('[data-module="self-extension"]', extension_styles)
self.assertIn('[data-module="self-extension"] .v2-role-select', extension_styles)
self.assertIn('.v2-extension-composer .primary-action {', extension_styles)
@@ -305,7 +329,7 @@ class SeatWebApiTests(unittest.TestCase):
self.assertIn("FBXLoader.js", html)
self.assertIn('data-seat-layout="five-seat"', html)
self.assertIn('data-active-seat="front-right"', html)
self.assertIn('placeholder="请输入服务端配置令牌"', html)
self.assertNotIn('placeholder="请输入服务端配置令牌"', html)
self.assertIn('data-callout="backrest_recline"', html)
self.assertIn('data-callout="legrest_extension"', html)
self.assertIn('data-callout="backrest_included_angle"', html)
@@ -325,13 +349,16 @@ class SeatWebApiTests(unittest.TestCase):
self.assertIn('id="passengerAirDirection"', html)
self.assertNotIn('id="transmittanceValue"', html)
self.assertNotIn('id="roofMotorState"', html)
self.assertNotIn('id="ambientBrightness"', html)
self.assertIn('id="ambientBrightness"', html)
self.assertIn("坐垫脚托夹角133度", html)
self.assertNotIn('data-axis="height"', html)
self.assertEqual(runtime["domain"], "cabin")
self.assertEqual(
runtime["hardware_execution_allowed"],
runtime["hardware"]["configured"],
(
runtime["hardware"]["configured"]
or runtime["vehicle_can"]["configured"]
),
)
self.assertEqual(runtime["hardware"]["supported_targets"], ["front_right"])
self.assertEqual(
@@ -380,13 +407,164 @@ class SeatWebApiTests(unittest.TestCase):
self.assertIn("climate.set_temperature", capability_names)
self.assertIn("roof.set_transmittance", capability_names)
self.assertIn("ambient_light.set_illumination", capability_names)
seat_capabilities = [
item for item in runtime["capabilities"] if item["name"].startswith("seat.")
passenger_seat_capabilities = [
item
for item in runtime["capabilities"]
if item["name"].startswith("seat.")
and not item["name"].startswith("seat.set_driver_")
]
self.assertTrue(seat_capabilities)
driver_seat_capabilities = [
item
for item in runtime["capabilities"]
if item["name"].startswith("seat.set_driver_")
]
self.assertTrue(passenger_seat_capabilities)
self.assertTrue(
all(item["target_scopes"] == ["passenger"] for item in seat_capabilities)
all(
item["target_scopes"] == ["passenger"]
for item in passenger_seat_capabilities
)
)
self.assertTrue(driver_seat_capabilities)
self.assertTrue(
all(
item["target_scopes"] == ["driver", "front_left"]
for item in driver_seat_capabilities
)
)
def test_self_extension_vehicle_success_contract_is_strict(self) -> None:
with self.opener.open(
f"{self.base_url}/self-extension.js?v=extension-15", timeout=3
) as response:
extension_script = response.read().decode("utf-8")
with self.opener.open(
f"{self.base_url}/app.js?v=seat-hil-51", timeout=3
) as response:
app_script = response.read().decode("utf-8")
self.assertIn('code: "SEAT · CH1 BODY CAN"', extension_script)
self.assertIn('command: "seat driver vent low"', extension_script)
self.assertIn('command: "seat driver vent high"', extension_script)
self.assertIn('command: "seat driver vent off"', extension_script)
self.assertIn('command: "seat driver heat off"', extension_script)
self.assertIn('command: "ac off"', extension_script)
self.assertIn('command: "light indoor off"', extension_script)
self.assertIn('command: "light reading all off"', extension_script)
self.assertIn('command: "light ambient off"', extension_script)
self.assertIn('command: "window front-left position 50"', extension_script)
self.assertIn('command: "window front-left position 0"', extension_script)
self.assertNotIn('command: "shade stop"', extension_script)
self.assertNotIn('command: "door vehicle lock"', extension_script)
self.assertNotIn('command: "door vehicle unlock"', extension_script)
self.assertNotIn('command: "door rear unlock"', extension_script)
self.assertNotIn('code: "OUTLET · 4 ZONES"', extension_script)
self.assertNotIn('command: "打开全部车窗"', extension_script)
self.assertNotIn('command: "打开副驾座椅通风"', extension_script)
self.assertNotIn('command: "把副驾座椅向后调一点"', extension_script)
self.assertIn('result?.status === "succeeded"', extension_script)
self.assertIn("everyOperationSucceeded", extension_script)
self.assertIn("everyHardwareOperationConfirmed", extension_script)
self.assertIn("result?.physical_feedback_confirmed === true", extension_script)
self.assertIn("cognitive-agent:extension-vehicle-result", extension_script)
self.assertIn("MASSAGE", extension_script)
self.assertIn("INTERIOR", extension_script)
self.assertIn("AMBIENT", extension_script)
self.assertIn(
"body: JSON.stringify({ command, conversation_id: sharedConversationId() })",
extension_script,
)
self.assertIn('window.addEventListener("cognitive-agent:result"', extension_script)
self.assertIn('id="extensionVehicleStateSummary"', extension_script)
self.assertNotIn("/api/hardware/seat-state", extension_script)
self.assertIn(
'window.addEventListener("cognitive-agent:extension-vehicle-result"',
app_script,
)
self.assertIn("applyAuthoritativeVehicleState", app_script)
self.assertIn('payload.conversation_id !== state.conversationId', app_script)
self.assertIn(
'target_policy === "ch1_body_can_driver_only"',
app_script,
)
seat_sync_body = app_script.split(
"async function synchronizePhysicalSeatState()", 1
)[1].split("async function loadAuthStatus()", 1)[0]
self.assertLess(
seat_sync_body.index('target_policy === "ch1_body_can_driver_only"'),
seat_sync_body.index("/api/hardware/seat-state"),
)
self.assertNotIn("approvalText", app_script)
def test_self_extension_vehicle_endpoint_preserves_unsuccessful_feedback(
self,
) -> None:
service_result = {
"status": "partially_succeeded",
"provider_used": "local_rules",
"request_id": "req-extension-partial",
"conversation_id": "conv-extension-shared-e2e",
"conversation_turn": 4,
"decision_summary": ["空调完成,遮阳帘反馈超时。"],
"validation": [],
"execution_report": {
"spec": {
"status": "failed",
"physical_state": {
"source": "physical_can_ch1",
"complete": False,
},
"results": [
{
"ref": "climate-1",
"domain": "climate",
"status": "succeeded",
"telemetry": {
"simulated": False,
"feedback_observed": True,
"feedback_match": True,
"feedback_frame": {"frame_id": "0x404"},
},
},
{
"ref": "roof-1",
"domain": "roof",
"status": "failed",
"message": "反馈超时",
"telemetry": {"simulated": False},
},
],
}
},
"domain_state": {"climate": {"power": "on"}},
"actuator_state": {"driver": {"heating": {"state": "off"}}},
"deferred_requests": [],
}
request = urllib.request.Request(
f"{self.base_url}/api/extension/vehicle/execute",
method="POST",
headers={"Content-Type": "application/json"},
data=json.dumps(
{
"command": "打开空调并关闭遮阳帘",
"conversation_id": "conv-extension-shared-e2e",
}
).encode("utf-8"),
)
with patch.object(self.service, "process", return_value=service_result):
with self.opener.open(request, timeout=3) as response:
result = json.loads(response.read().decode("utf-8"))
self.assertFalse(result["success"])
self.assertEqual(result["status"], "partially_succeeded")
self.assertEqual(result["conversation_id"], "conv-extension-shared-e2e")
self.assertEqual(result["conversation_turn"], 4)
self.assertEqual(result["execution_report"], service_result["execution_report"])
self.assertEqual(result["domain_state"], service_result["domain_state"])
self.assertEqual(result["actuator_state"], service_result["actuator_state"])
self.assertEqual(result["physical_state"]["source"], "physical_can_ch1")
self.assertEqual(len(result["physical_feedback"]), 2)
self.assertFalse(result["physical_feedback_confirmed"])
def test_self_extension_gateway_uses_unified_authenticated_service(self) -> None:
original_extension = SeatDemoHandler.extension
@@ -441,7 +619,7 @@ class SeatWebApiTests(unittest.TestCase):
def test_relative_backrest_phrase_uses_current_conversation_position(self) -> None:
conversation_id = "conv-e2e-relative-backrest"
for utterance in ("副驾靠背角度调到130度", "靠背向前移动10度"):
for utterance in ("副驾靠背角度调到130度", "副驾靠背向前移动10度"):
request = urllib.request.Request(
f"{self.base_url}/api/requests",
method="POST",
@@ -491,7 +669,7 @@ class SeatWebApiTests(unittest.TestCase):
return json.loads(response.read().decode("utf-8"))
submit("副驾靠背归零")
timed = submit("靠背向后移动70秒")
timed = submit("副驾靠背向后移动70秒")
self.assertEqual(timed["status"], "succeeded")
timed_operation = timed["function_config"]["spec"]["operations"][0]
@@ -503,7 +681,7 @@ class SeatWebApiTests(unittest.TestCase):
self.assertIn("最大允许时长为30秒", "\n".join(timed["decision_summary"]))
submit("副驾靠背角度调到130度")
overflow = submit("靠背向后移动70度")
overflow = submit("副驾靠背向后移动70度")
self.assertEqual(overflow["status"], "succeeded")
overflow_operation = overflow["function_config"]["spec"]["operations"][0]