feat: tolerate recoverable seat intent inputs

This commit is contained in:
vee
2026-08-02 06:12:36 +08:00
parent 55d0d400cb
commit a6d974cb29
9 changed files with 942 additions and 31 deletions
@@ -0,0 +1,823 @@
{
"suite": "t2g-seat-frontend-comprehensive-hil",
"started_at": "2026-08-01T22:06:10.662623Z",
"base_url": "http://127.0.0.1:8000",
"vehicle_id": "denza_n7",
"conversation_id": "conv-hil-full-20260801220610",
"runtime": {
"hardware_execution_allowed": true,
"firmware_profiles": [
"combined"
]
},
"results": [
{
"id": "MOTION-M0-ABS-DECIMAL-ROUND",
"group": "motion",
"utterance": "副驾座椅水平位置调到50.5%",
"started_at": "2026-08-01T22:06:33.819391Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801220633-35a9ee",
"provider_used": "local_rules",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"backrest_recline": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 3,
"motor_selector": 2,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 30 18 0B 00 00 00 00",
"timestamp": 52437.943305
}
},
"legrest_height": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 3,
"motor_selector": 3,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 30 18 0B 00 00 00 00",
"timestamp": 52437.968795
}
},
"longitudinal": {
"position_pct": 51,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 0,
"value_x100": 5110,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 26 08 0C 00 F6 13 00",
"timestamp": 52437.995098
}
},
"cushion_tilt": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 3,
"motor_selector": 1,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 30 18 0B 00 00 00 00",
"timestamp": 52438.021395
}
}
},
"ventilation": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 52438.031724
}
},
"heating": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 52438.031724
}
},
"captured_at": "2026-08-01T22:06:42.683267Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T22:06:42.768028Z",
"duration_ms": 8949
},
{
"id": "MOTION-M0-ABS-NEGATIVE-CLAMP",
"group": "motion",
"utterance": "副驾座椅水平位置调到-10%",
"started_at": "2026-08-01T22:06:42.768083Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801220642-ffc18b",
"provider_used": "local_rules",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"cushion_tilt": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 3,
"motor_selector": 1,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 30 18 0D 00 00 00 00",
"timestamp": 52448.169066
}
},
"backrest_recline": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 3,
"motor_selector": 2,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 30 18 0D 00 00 00 00",
"timestamp": 52448.196
}
},
"legrest_height": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 3,
"motor_selector": 3,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 30 18 0D 00 00 00 00",
"timestamp": 52448.221821
}
},
"longitudinal": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 0,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 26 18 0E 00 00 00 00",
"timestamp": 52448.247656
}
}
},
"ventilation": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 52448.255615
}
},
"heating": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 52448.255615
}
},
"captured_at": "2026-08-01T22:06:52.906777Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T22:06:52.966157Z",
"duration_ms": 10198
},
{
"id": "MOTION-M2-TIMED-ZERO-MIN-CLAMP",
"group": "motion",
"utterance": "副驾靠背向后移动0秒",
"started_at": "2026-08-01T22:06:52.966239Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801220652-ebe458",
"provider_used": "local_rules",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"longitudinal": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 3,
"motor_selector": 0,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 30 18 0F 00 00 00 00",
"timestamp": 52449.295516
}
},
"cushion_tilt": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 3,
"motor_selector": 1,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 30 18 0F 00 00 00 00",
"timestamp": 52449.321809
}
},
"backrest_recline": {
"position_pct": 1,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 2,
"value_x100": 120,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 26 08 10 00 78 00 00",
"timestamp": 52449.347017
}
},
"legrest_height": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 3,
"motor_selector": 3,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 30 18 0F 00 00 00 00",
"timestamp": 52449.37364
}
}
},
"ventilation": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 52449.382777
}
},
"heating": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 52449.382777
}
},
"captured_at": "2026-08-01T22:06:54.033862Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T22:06:54.110745Z",
"duration_ms": 1145
},
{
"id": "MODEL-FUZZY-COMFORT-ONLY",
"group": "model",
"utterance": "把座椅调舒服一点",
"started_at": "2026-08-01T22:06:54.110804Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801220654-5f0af4",
"provider_used": "openai:gpt-5.6-sol",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"legrest_height": {
"position_pct": 10,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 3,
"value_x100": 1005,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 26 08 15 00 ED 03 00",
"timestamp": 52491.95849
}
},
"longitudinal": {
"position_pct": 80,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 0,
"value_x100": 8016,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 26 08 12 00 50 1F 00",
"timestamp": 52491.984306
}
},
"cushion_tilt": {
"position_pct": 30,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 1,
"value_x100": 2995,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 26 08 13 00 B3 0B 00",
"timestamp": 52492.010881
}
},
"backrest_recline": {
"position_pct": 35,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 2,
"value_x100": 3501,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 26 08 14 00 AD 0D 00",
"timestamp": 52492.036223
}
}
},
"ventilation": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 52492.046648
}
},
"heating": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 52492.046648
}
},
"captured_at": "2026-08-01T22:07:36.697054Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T22:07:36.773665Z",
"duration_ms": 42663
},
{
"id": "MODEL-UNKNOWN-BOSS-MODE",
"group": "model",
"utterance": "打开老板模式",
"started_at": "2026-08-01T22:07:36.773730Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801220736-be8392",
"provider_used": "openai:gpt-5.6-sol",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"legrest_height": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 3,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 26 18 1A 00 00 00 00",
"timestamp": 52526.819003
}
},
"longitudinal": {
"position_pct": 20,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 0,
"value_x100": 2004,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 26 08 17 00 D4 07 00",
"timestamp": 52526.845094
}
},
"cushion_tilt": {
"position_pct": 30,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 1,
"value_x100": 2995,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 26 08 18 00 B3 0B 00",
"timestamp": 52526.87121
}
},
"backrest_recline": {
"position_pct": 10,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 2,
"value_x100": 1003,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 26 08 19 00 EB 03 00",
"timestamp": 52526.897575
}
}
},
"ventilation": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 52526.90671
}
},
"heating": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 52526.90671
}
},
"captured_at": "2026-08-01T22:08:11.556208Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T22:08:11.634298Z",
"duration_ms": 34861
}
],
"position_memory_sync": {
"id": "PREFLIGHT-POSITION-MEMORY-SYNC",
"group": "preflight",
"utterance": "关闭副驾座椅加热和通风,并把水平、坐盆、靠背、腿托四轴归零",
"started_at": "2026-08-01T22:06:10.662651Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801220610-fe1cde",
"provider_used": "local_rules",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"legrest_height": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 3,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 26 18 0A 00 00 00 00",
"timestamp": 52428.988603
}
},
"longitudinal": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 0,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 26 18 07 00 00 00 00",
"timestamp": 52429.015565
}
},
"cushion_tilt": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 1,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 26 18 08 00 00 00 00",
"timestamp": 52429.041376
}
},
"backrest_recline": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 2,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 26 18 09 00 00 00 00",
"timestamp": 52429.06754
}
}
},
"ventilation": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 52429.075411
}
},
"heating": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 52429.075411
}
},
"captured_at": "2026-08-01T22:06:33.726921Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T22:06:33.819288Z",
"duration_ms": 23157
},
"final_state": {
"id": "FINAL-THERMAL-OFF",
"group": "cleanup",
"utterance": "关闭副驾座椅加热和通风,并把水平、坐盆、靠背、腿托四轴归零",
"started_at": "2026-08-01T22:08:11.634351Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801220811-9d1330",
"provider_used": "local_rules",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"cushion_tilt": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 1,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 26 18 1D 00 00 00 00",
"timestamp": 52552.285415
}
},
"backrest_recline": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 2,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 26 18 1E 00 00 00 00",
"timestamp": 52552.311577
}
},
"legrest_height": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 3,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 26 18 1F 00 00 00 00",
"timestamp": 52552.337346
}
},
"longitudinal": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 0,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 26 18 1C 00 00 00 00",
"timestamp": 52552.364354
}
}
},
"ventilation": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 52552.372264
}
},
"heating": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 52552.372264
}
},
"captured_at": "2026-08-01T22:08:37.021239Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T22:08:37.100192Z",
"duration_ms": 25466
},
"finished_at": "2026-08-01T22:08:37.100232Z",
"summary": {
"total": 5,
"passed": 5,
"failed": 0,
"safety_blocked": 0,
"executed": 5,
"position_memory_sync_passed": true,
"final_state_passed": true
}
}
@@ -26,7 +26,7 @@
"requested_delta_angle_deg": {"type": "number", "minimum": 0},
"requested_delta_position_pct": {"type": "integer", "minimum": 0},
"range_clamped": {"type": "boolean"},
"requested_duration_ms": {"type": "integer", "minimum": 1},
"requested_duration_ms": {"type": "integer", "minimum": 0},
"applied_duration_ms": {"type": "integer", "minimum": 1},
"max_duration_ms": {"type": "integer", "minimum": 1},
"min_duration_ms": {"type": "integer", "minimum": 1},
+3 -1
View File
@@ -41,7 +41,7 @@ python .\tools\run_seat_frontend_hil_suite.py `
--output .\artifacts\hil\seat_frontend_hil_latest.json
```
可用 `--only-group thermal|motion|combined|mode|model|safety` 只执行一组,用 `--start-at CASE-ID` 从指定用例续跑,或重复传入 `--case CASE-ID` 精确复测一个有序子集。每次运行先强制执行四轴物理归零,重建霍尔位置记忆;失败时停止后续位置测试。当前清单共 186 条:129 条热管理/移动/组合输入、33 条模式输入、4 条 GPT-5.6-sol 模糊意图输入和 20 条安全拒绝输入。新增覆盖包括取暖/吹风口语、正常秒数/半秒/毫秒与最小时长钳制、四轴独立端点表达、坐垫与腿托超量程、靠背角度下溢,以及后排/左前/全部座椅、小数/负位置和零时长安全拒绝。每条用例均核对:所有座椅操作为 `passenger/front_right`、物理状态来源为 `physical_can`、目标位置误差不超过 2%、联合孪生值与物理状态相同;模型用例还会防止静默降级为本地规则,时长和超量程用例会核对界面可见的时长上限、实际执行时长和到达端点提示,拒绝用例会比较动作前后物理指纹。调试环境设置 `COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN_REQUIRED=false`,执行器和前端均不要求逐次输入硬件令牌。
可用 `--only-group thermal|motion|combined|mode|model|safety` 只执行一组,用 `--start-at CASE-ID` 从指定用例续跑,或重复传入 `--case CASE-ID` 精确复测一个有序子集。每次运行先强制执行四轴物理归零,重建霍尔位置记忆;失败时停止后续位置测试。当前清单共 186 条:132 条热管理/移动/组合输入、33 条模式输入、6 条 GPT-5.6-sol 模糊/未知模式输入和 15 条安全拒绝输入。体验容错覆盖取暖/吹风口语、正常秒数/半秒/毫秒、零时长按最小安全时长执行、小数位置四舍五入、负位置钳制到近端、四轴独立端点、超量程备用端点,以及模糊舒适和未保存老板模式的 GPT 姿态合成。只有后排/左前/全部座椅等不受支持目标、未知物理能力或真实语义冲突保留安全拒绝。每条用例均核对:所有座椅操作为 `passenger/front_right`、物理状态来源为 `physical_can`、目标位置误差不超过 2%、联合孪生值与物理状态相同;模型用例还会防止静默降级为本地规则,时长、取整和超量程用例会核对界面可见的实际执行提示,拒绝用例会比较动作前后物理指纹。调试环境设置 `COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN_REQUIRED=false`,执行器和前端均不要求逐次输入硬件令牌。
## 真实台架执行顺序
@@ -64,3 +64,5 @@ python .\tools\run_seat_frontend_hil_suite.py `
2026-08-01 的 M0/M2 无 Hall 与 M1 高电流无 Hall 记录最终定位为水平、坐盆和靠背 Hall 地线断开。2026-08-02 地线恢复、四轴手工归零后,revision 6 的完整自动 HIL 结果为 33/33 通过、0 失败、0 `safety_blocked`:覆盖加热/通风全部档位、M0~M3 双向中间位置、单轴及四轴归零、组合指令和全部 11 个预设模式。临时轴向安全阻塞已从测试清单移除;报告保存于 `artifacts/hil/seat_frontend_hil_20260802_full_retest_run1.json`。
2026-08-02 补齐时长和超量程容错后,扩充后的完整 HIL 为 161/161 通过、0 失败、0 `safety_blocked`,最终清理通过;独立 CAN 复核确认加热/通风均为 0 档、M0~M3 均为 0% 且四轴 `Position_Valid=1`。其中“靠背向后移动70度”、四轴双向超长时长、水平101%、靠背200度以及“打开零重力模式”均在完整序列中通过。完整报告为 `artifacts/hil/seat_frontend_hil_20260802_full_timed_overflow.json`,新增规则的定向 14/14 报告为 `artifacts/hil/seat_frontend_hil_20260802_timed_overflow_targeted.json`。
2026-08-02 调整体验容错边界后,软件完整回归为 158/158 通过并包含 248 个参数化子用例;真实台架定向回归为 5/5 通过。定向用例验证了 50.5% 取整为 51%、-10% 钳制到 0% 机械近端、0 秒按 300 ms 最小时长执行,以及 GPT-5.6-sol 将“把座椅调舒服一点”和未保存的“老板模式”分别合成为 80/30/35/10 与 20/30/10/0 四轴目标。预检位置记忆同步和最终清理均通过,独立物理 CAN 复核确认加热/通风为 0 档、M0~M3 为 0%、四轴 `Position_Valid=1`;报告为 `artifacts/hil/seat_frontend_hil_20260802_experience_tolerance_targeted.json`。
+58 -17
View File
@@ -2,6 +2,7 @@
from __future__ import annotations
import math
import re
import unicodedata
from copy import deepcopy
@@ -20,7 +21,7 @@ STATE_PATTERN = re.compile(
LEVEL_PATTERN = re.compile(r"(最高|最低|[1-5一二三四五高中低])\s*档")
POSITION_PATTERN_TEMPLATE = (
r"{alias}(?:位置)?(?:调到|移动到|移到|设为|设置为|调至|移动至|到|为)?"
r"(?P<position>\d{{1,3}})%"
r"(?P<position>[+-]?\d{{1,3}}(?:\.\d+)?)%"
)
ANGLE_PATTERN_TEMPLATE = (
r"{alias}(?:夹角|角度)?(?:调到|放倒到|抬到|移动到|移到|设为|设置为|"
@@ -300,6 +301,16 @@ def _format_seconds(duration_ms: int) -> str:
return f"{seconds:g}"
def _round_half_away_from_zero(value: float) -> int:
"""Use the integer a user normally expects from a .5 position request."""
return math.floor(value + 0.5) if value >= 0 else math.ceil(value - 0.5)
def _format_numeric_request(value: float) -> str:
return f"{value:g}"
def _timed_motion_action(
axis: str,
direction_token: str,
@@ -309,8 +320,8 @@ def _timed_motion_action(
operation = _relative_direction(axis, direction_token)
if operation is None:
return None, f"{direction_token}不适用于该座椅轴,请确认移动方向。"
if requested_duration_ms <= 0:
return None, "座椅按时长移动的时长必须大于0。"
if requested_duration_ms < 0:
return None, "座椅按时长移动的时长不能为负数。"
state = _context_motion_state(context, axis)
if state is None:
return None, "缺少当前座椅位置,无法安全计算按时长移动的方向和备用端点。"
@@ -894,12 +905,18 @@ def parse_local_intent(
for axis, aliases in MOTION_AXIS_ALIASES.items()
if any(alias in clause for alias in aliases)
]
percentages = list(re.finditer(r"(?P<position>\d{1,3})%", clause))
percentages = list(
re.finditer(r"(?P<position>[+-]?\d{1,3}(?:\.\d+)?)%", clause)
)
shared_position = None
if len(clause_axes) > 1 and len(percentages) == 1 and re.search(
r"都|全部|均", clause
):
shared_position = int(percentages[0].group("position"))
requested_shared_value = float(percentages[0].group("position"))
rounded_shared_position = _round_half_away_from_zero(
requested_shared_value
)
shared_position = max(0, min(100, rounded_shared_position))
target_zh = (
_nearest_preceding_target(clause, percentages[0].start()) or "副驾"
)
@@ -908,8 +925,6 @@ def parse_local_intent(
{"field": "target", "question": PASSENGER_ONLY_QUESTION}
)
else:
requested_shared_position = shared_position
shared_position = min(100, shared_position)
motion_actions.extend(
{
"target": "passenger",
@@ -920,11 +935,22 @@ def parse_local_intent(
{
"user_notice": (
f"{MOTION_AXIS_LABELS[axis]}请求位置"
f"{requested_shared_position}%超过最大行程;"
"已采用备用目标并移动到对应机械末端。"
f"{_format_numeric_request(requested_shared_value)}%;"
+ (
f"已取整为{rounded_shared_position}%。"
if requested_shared_value
!= rounded_shared_position
else ""
)
+ (
f" 取整后超过0到100%行程,已采用"
f"{shared_position}%备用目标并移动到对应机械末端。"
if rounded_shared_position != shared_position
else ""
)
)
}
if requested_shared_position > 100
if requested_shared_value != shared_position
else {}
),
}
@@ -1104,8 +1130,13 @@ def parse_local_intent(
break
absolute_angle = None
if position_match is not None:
requested_position_pct = int(position_match.group("position"))
position_pct = min(100, requested_position_pct)
requested_position_value = float(
position_match.group("position")
)
requested_position_pct = _round_half_away_from_zero(
requested_position_value
)
position_pct = max(0, min(100, requested_position_pct))
else:
requested_angle = int(angle_match.group("angle"))
minimum, maximum = MOTION_ANGLE_RANGES[axis]
@@ -1133,11 +1164,21 @@ def parse_local_intent(
f"{minimum}到{maximum}度行程;已采用备用目标"
f"{absolute_angle}度并移动到{endpoint_label}。"
)
elif requested_position_pct != position_pct:
motion_action["user_notice"] = (
f"{MOTION_AXIS_LABELS[axis]}请求位置{requested_position_pct}%超过"
"0到100%行程;已采用100%的备用目标并移动到机械末端。"
)
else:
notices: list[str] = []
if requested_position_value != requested_position_pct:
notices.append(
f"{MOTION_AXIS_LABELS[axis]}请求位置"
f"{_format_numeric_request(requested_position_value)}%;"
f"已取整为{requested_position_pct}%。"
)
if requested_position_pct != position_pct:
notices.append(
f"取整后超过0到100%行程;已采用{position_pct}%的备用目标"
"并移动到对应机械末端。"
)
if notices:
motion_action["user_notice"] = " ".join(notices)
motion_actions.append(motion_action)
break
+23
View File
@@ -301,6 +301,29 @@ class LocalIntentParserTests(unittest.TestCase):
self.assertTrue(minimum["actions"][0]["motion_intent"]["duration_clamped"])
self.assertIn("最小允许时长为0.3秒", minimum["actions"][0]["user_notice"])
def test_rounds_decimal_and_clamps_negative_absolute_positions(self) -> None:
decimal = parse_local_intent("副驾座椅水平位置调到50.5%")
negative = parse_local_intent("副驾座椅水平位置调到-10%")
self.assertEqual(decimal["ambiguities"], [])
self.assertEqual(decimal["actions"][0]["position_pct"], 51)
self.assertIn("取整为51%", decimal["actions"][0]["user_notice"])
self.assertEqual(negative["ambiguities"], [])
self.assertEqual(negative["actions"][0]["position_pct"], 0)
self.assertIn("0%的备用目标", negative["actions"][0]["user_notice"])
def test_zero_duration_uses_minimum_safe_motion_instead_of_rejecting(self) -> None:
result = parse_local_intent(
"副驾靠背向后移动0秒", self.motion_context()
)
self.assertEqual(result["ambiguities"], [])
motion = result["actions"][0]["motion_intent"]
self.assertEqual(motion["requested_duration_ms"], 0)
self.assertEqual(motion["applied_duration_ms"], 300)
self.assertTrue(motion["duration_clamped"])
self.assertIn("已按0.3秒", result["actions"][0]["user_notice"])
def test_rejects_unsupported_level_and_conflicting_state(self) -> None:
unsupported = parse_local_intent("副驾加热开到4档")
conflicting = parse_local_intent("打开副驾加热然后关闭副驾加热")
@@ -139,6 +139,11 @@ with this conservative deterministic baseline: longitudinal 80%, cushion tilt 30
remaining upright and reversible. State this inferred ergonomic baseline in decision_summary. Apply
the same causal reasoning to paraphrases such as insufficient legroom, feeling squeezed, wanting more
space, or asking for a comfortable posture; do not require the user to name a motor or percentage.
When a user simply asks to make the seat more comfortable without numeric targets, use that same
80/30/35/10 ergonomic baseline. For an unsaved "boss", "executive", or equivalent front-seat
clearance mode, infer the conventional intent of moving this controllable front passenger seat out
of the way and use this deterministic baseline: longitudinal 20%, cushion tilt 30%, backrest recline
10%, and legrest height 0%. State the inferred mode meaning and targets in decision_summary.
When execution_policy.planning_environment is dry_run_candidate_synthesis, a named preset is not
required. For a commonly understood mode, activity, or comfort goal, decompose the goal into the
@@ -156,8 +161,10 @@ capabilities as usual.
Return every understood goal in requests using an exact capability name from the registry. Use
target_scope and operation to preserve the user's intent. Put concrete capability parameters in the
parameters list when they are known or defensibly inferred. Do not drop catalog_only requests; the
runtime router, not the model, decides which domain adapter can execute them.
parameters list when they are known or defensibly inferred. Preserve a catalog_only request when the
user explicitly asks for that exact feature, but do not invent catalog_only capabilities while
synthesizing a vague comfort goal or an unknown named mode. Inferred seat postures must use only the
registered executable seat motion capabilities so the plan can safely reach hardware.
For executable capabilities, resolve follow-up and relative intent against runtime state and return
absolute target parameter values that the adapter can execute. Do not delegate that semantic decision
to downstream code. Catalog-only requests may remain relative when their current state is unavailable.
+21 -9
View File
@@ -14,8 +14,8 @@
"coverage_dimensions": {
"targets": ["副驾", "副驾驶", "副驾位", "右前座", "乘客座", "省略座位", "后排/左前/全部座椅安全拒绝"],
"verbs": ["打开", "开启", "启动", "调到", "设置为", "进入", "切换到", "执行", "关闭", "停止", "归零", "复位"],
"values": ["阿拉伯数字", "中文数字", "全角百分号", "百分之表达", "高/中/低档", "正常秒数/半秒/毫秒", "时长上下限钳制", "绝对角度", "相对角度", "相对百分比", "逐轴机械端点", "超量程备用端点", "小数/负数安全拒绝"],
"functions": ["加热/取暖", "通风/吹风", "水平", "坐盆", "靠背", "腿托", "四轴组合", "相对移动", "按时长移动", "预设模式", "GPT模糊舒适意图"],
"values": ["阿拉伯数字", "中文数字", "全角百分号", "百分之表达", "高/中/低档", "正常秒数/半秒/毫秒", "时长上下限钳制", "零时长最小动作", "绝对角度", "相对角度", "相对百分比", "逐轴机械端点", "超量程备用端点", "小数取整/负数端点容错"],
"functions": ["加热/取暖", "通风/吹风", "水平", "坐盆", "靠背", "腿托", "四轴组合", "相对移动", "按时长移动", "预设模式", "GPT模糊舒适意图", "GPT未知模式姿态合成"],
"safety": ["主驾", "前排范围", "越界容错", "缺少状态", "缺少当前位置", "矛盾指令", "未知能力", "多模式冲突", "非端点堵转不容错"]
},
"cases": [
@@ -315,7 +315,10 @@
{"id": "MOTION-M1-ABS-OVERFLOW", "group": "motion", "utterance": "坐垫位置调到150%", "expected": {"cushion_tilt": 100}, "expected_notice_contains": ["备用目标", "机械末端"]},
{"id": "MOTION-M2-ABS-ANGLE-UNDERFLOW", "group": "motion", "utterance": "副驾靠背调到80度", "expected": {"backrest_recline": 0}, "expected_notice_contains": ["备用目标", "95度", "最前方"]},
{"id": "MOTION-M3-ABS-ANGLE-OVERFLOW", "group": "motion", "utterance": "腿托调到200度", "expected": {"legrest_height": 100}, "expected_notice_contains": ["备用目标", "167度", "最上方"]}
{"id": "MOTION-M3-ABS-ANGLE-OVERFLOW", "group": "motion", "utterance": "腿托调到200度", "expected": {"legrest_height": 100}, "expected_notice_contains": ["备用目标", "167度", "最上方"]},
{"id": "MOTION-M0-ABS-DECIMAL-ROUND", "group": "motion", "utterance": "副驾座椅水平位置调到50.5%", "expected": {"longitudinal": 51}, "expected_notice_contains": ["取整为51%"]},
{"id": "MOTION-M0-ABS-NEGATIVE-CLAMP", "group": "motion", "utterance": "副驾座椅水平位置调到-10%", "expected": {"longitudinal": 0}, "expected_notice_contains": ["备用目标", "0%", "机械末端"]},
{"id": "MOTION-M2-TIMED-ZERO-MIN-CLAMP", "group": "motion", "utterance": "副驾靠背向后移动0秒", "expected": {}, "expected_motion": {"axis": "backrest_recline", "mode": "timed", "direction": "increase", "requested_duration_ms": 0, "applied_duration_ms": 300, "max_duration_ms": 30000, "min_duration_ms": 300, "duration_clamped": true}, "expected_notice_contains": ["最小允许时长为0.3秒", "已按0.3秒"]}
],
"mode_cases": [
{"mode_id": "easy_entry_exit", "expected": {"longitudinal": 100, "cushion_tilt": 0, "backrest_recline": 0, "legrest_height": 0}, "variants": [
@@ -385,6 +388,20 @@
"provider": "openai",
"expected_provider_prefix": "openai:gpt-5.6-sol",
"expected": {"longitudinal": 80, "cushion_tilt": 30, "backrest_recline": 35, "legrest_height": 10}
},
{
"id": "MODEL-FUZZY-COMFORT-ONLY",
"utterance": "把座椅调舒服一点",
"provider": "openai",
"expected_provider_prefix": "openai:gpt-5.6-sol",
"expected": {"longitudinal": 80, "cushion_tilt": 30, "backrest_recline": 35, "legrest_height": 10}
},
{
"id": "MODEL-UNKNOWN-BOSS-MODE",
"utterance": "打开老板模式",
"provider": "openai",
"expected_provider_prefix": "openai:gpt-5.6-sol",
"expected": {"longitudinal": 20, "cushion_tilt": 30, "backrest_recline": 10, "legrest_height": 0}
}
],
"rejection_cases": [
@@ -396,18 +413,13 @@
{"id": "SAFE-MISSING-STATE", "utterance": "副驾座椅通风", "expected_status": "needs_confirmation"},
{"id": "SAFE-CONTRADICTORY", "utterance": "打开副驾加热然后关闭副驾加热", "expected_status": "needs_confirmation"},
{"id": "SAFE-UNKNOWN-CAPABILITY", "utterance": "把副驾座椅按摩打开", "expected_status": "needs_confirmation"},
{"id": "SAFE-UNKNOWN-MODE", "utterance": "打开老板模式", "expected_status": "needs_confirmation"},
{"id": "SAFE-MULTIPLE-MODES", "utterance": "进入办公模式和零重力模式", "expected_status": "needs_confirmation"},
{"id": "SAFE-LEGREST-EXTENSION", "utterance": "把副驾腿托伸长到50%", "expected_status": "needs_confirmation"},
{"id": "SAFE-RELATIVE-WRONG-UNIT", "utterance": "坐垫向前移动10度", "expected_status": "needs_confirmation"},
{"id": "SAFE-RELATIVE-MISSING-AXIS", "utterance": "向前移动10度", "expected_status": "needs_confirmation"},
{"id": "SAFE-VAGUE", "utterance": "把座椅调舒服一点", "expected_status": "needs_confirmation"},
{"id": "SAFE-UNSUPPORTED-REAR-ROW", "utterance": "打开后排座椅加热", "expected_status": "needs_confirmation"},
{"id": "SAFE-UNSUPPORTED-FRONT-LEFT", "utterance": "打开左前座椅通风", "expected_status": "needs_confirmation"},
{"id": "SAFE-UNSUPPORTED-ALL-SEATS", "utterance": "打开所有座椅加热", "expected_status": "needs_confirmation"},
{"id": "SAFE-DECIMAL-POSITION", "utterance": "副驾座椅水平位置调到50.5%", "expected_status": "needs_confirmation"},
{"id": "SAFE-NEGATIVE-POSITION", "utterance": "副驾座椅水平位置调到-10%", "expected_status": "needs_confirmation"},
{"id": "SAFE-ZERO-DURATION", "utterance": "副驾靠背向后移动0秒", "expected_status": "needs_confirmation"}
{"id": "SAFE-UNSUPPORTED-ALL-SEATS", "utterance": "打开所有座椅加热", "expected_status": "needs_confirmation"}
],
"final_state": {
"utterance": "关闭副驾座椅加热和通风,并把水平、坐盆、靠背、腿托四轴归零",
@@ -60,7 +60,7 @@ class SeatFrontendInputCoverageTests(unittest.TestCase):
def test_suite_has_broad_equivalence_and_safety_coverage(self) -> None:
self.assertGreaterEqual(len(self.positive_cases), 160)
self.assertGreaterEqual(len(self.model_cases), 4)
self.assertGreaterEqual(len(self.rejection_cases), 20)
self.assertGreaterEqual(len(self.rejection_cases), 15)
self.assertEqual(
{case["group"] for case in self.positive_cases},
{"thermal", "motion", "combined", "mode"},
+3
View File
@@ -72,6 +72,9 @@ class OpenAIResponsesTests(unittest.TestCase):
self.assertIn("longitudinal 80%", normalized)
self.assertIn("backrest recline 35%", normalized)
self.assertIn("do not manufacture a driver-seat target", normalized)
self.assertIn("boss", normalized)
self.assertIn("longitudinal 20%", normalized)
self.assertIn("do not invent catalog_only capabilities", normalized)
def test_non_streaming_structured_output(self) -> None:
response_body = json.dumps({"output_text": json.dumps(INTENT)})