feat: integrate production cockpit AI assistant
This commit is contained in:
@@ -9,6 +9,9 @@ from collections.abc import Callable
|
||||
from http.server import ThreadingHTTPServer
|
||||
from unittest.mock import patch
|
||||
|
||||
from cognitive_agent.application.assistant_capabilities import (
|
||||
AssistantCapabilityCatalog,
|
||||
)
|
||||
from cognitive_agent.application.codex_assistant import (
|
||||
ActiveCodexTurn,
|
||||
CodexAssistantService,
|
||||
@@ -41,6 +44,101 @@ ROOF_TRANSMITTANCE_PLAN: dict[str, object] = {
|
||||
}
|
||||
|
||||
|
||||
SAVED_MODE_CAPABILITY_SNAPSHOT: dict[str, object] = {
|
||||
"schema_version": "n7web-assistant-capabilities/v1",
|
||||
"catalog_revision": "saved-mode-http-r1",
|
||||
"authority": "test runtime registry",
|
||||
"vehicle": {"id": "denza_n7", "name": "腾势 N7"},
|
||||
"device_groups": [],
|
||||
"capabilities": [
|
||||
{
|
||||
"id": "climate.set_temperature",
|
||||
"domain": "climate",
|
||||
"assistant_callable": True,
|
||||
"target_scopes": ["vehicle", "passenger"],
|
||||
},
|
||||
{
|
||||
"id": "seat.set_backrest_recline",
|
||||
"domain": "seat",
|
||||
"assistant_callable": True,
|
||||
"target_scopes": ["passenger"],
|
||||
},
|
||||
],
|
||||
"modes": [
|
||||
{
|
||||
"id": "office",
|
||||
"name": "办公模式",
|
||||
"aliases": ["办公"],
|
||||
"description": "用户保存的办公场景。",
|
||||
"domains": ["seat", "climate"],
|
||||
"operation_count": 2,
|
||||
"operation_ids": [
|
||||
"seat.set_backrest_recline",
|
||||
"climate.set_temperature",
|
||||
],
|
||||
"operations": [
|
||||
{
|
||||
"domain": "seat",
|
||||
"capability": "seat.set_backrest_recline",
|
||||
"target_scope": "passenger",
|
||||
"operation": "set",
|
||||
"parameters": [
|
||||
{"name": "position_pct", "value": 22},
|
||||
{"name": "angle_deg", "value": 110},
|
||||
],
|
||||
},
|
||||
{
|
||||
"domain": "climate",
|
||||
"capability": "climate.set_temperature",
|
||||
"target_scope": "passenger",
|
||||
"operation": "set",
|
||||
"parameters": [{"name": "temperature_c", "value": 25}],
|
||||
},
|
||||
],
|
||||
"assistant_callable": True,
|
||||
}
|
||||
],
|
||||
"scene_templates": [],
|
||||
"learned_peripherals": [],
|
||||
"service_surfaces": [],
|
||||
"source_status": {
|
||||
"core_capabilities": "ready",
|
||||
"modes": "ready",
|
||||
"scene_templates": "ready",
|
||||
"learned_peripherals": "ready",
|
||||
},
|
||||
"semantics": {},
|
||||
}
|
||||
|
||||
|
||||
class StaticSavedModeCatalog:
|
||||
@staticmethod
|
||||
def snapshot(vehicle_id: str | None = None) -> dict[str, object]:
|
||||
snapshot = json.loads(json.dumps(SAVED_MODE_CAPABILITY_SNAPSHOT))
|
||||
snapshot["vehicle"]["id"] = vehicle_id or "denza_n7"
|
||||
return snapshot
|
||||
|
||||
@staticmethod
|
||||
def compact_context(snapshot: dict[str, object]) -> dict[str, object]:
|
||||
return snapshot
|
||||
|
||||
@staticmethod
|
||||
def scene_composition_context(snapshot: dict[str, object]) -> dict[str, object]:
|
||||
return {"vehicle": snapshot["vehicle"], "capabilities": []}
|
||||
|
||||
@staticmethod
|
||||
def fast_answer(message: str, snapshot: dict[str, object]) -> object:
|
||||
return AssistantCapabilityCatalog.fast_answer(None, message, snapshot)
|
||||
|
||||
@staticmethod
|
||||
def control_terms(_snapshot: dict[str, object]) -> tuple[str, ...]:
|
||||
return ("办公模式", "办公", "空调", "温度", "座椅", "靠背")
|
||||
|
||||
@staticmethod
|
||||
def query(snapshot: dict[str, object], _query: str) -> dict[str, object]:
|
||||
return snapshot
|
||||
|
||||
|
||||
def _answer_events(turn: ActiveCodexTurn, answer: str) -> None:
|
||||
item_id = f"answer-{turn.turn_id}"
|
||||
turn.events.put(
|
||||
@@ -548,6 +646,355 @@ class TestCodexAssistantWebApi:
|
||||
assert not AssistantApiHandler.service.calls
|
||||
assert not FAKE_CODEX.bridge_results
|
||||
|
||||
def test_assistant_capability_snapshot_uses_requested_vehicle_id(self) -> None:
|
||||
requested_vehicle_ids: list[str | None] = []
|
||||
|
||||
class RecordingCatalog:
|
||||
def snapshot(self, vehicle_id: str | None = None) -> dict[str, object]:
|
||||
requested_vehicle_ids.append(vehicle_id)
|
||||
return {
|
||||
"vehicle": {
|
||||
"id": vehicle_id or "denza_n7",
|
||||
"name": "测试车型",
|
||||
},
|
||||
"capabilities": [],
|
||||
"modes": [],
|
||||
"source_status": {"modes": "ready"},
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def compact_context(snapshot: dict[str, object]) -> dict[str, object]:
|
||||
return {"vehicle": snapshot["vehicle"]}
|
||||
|
||||
@staticmethod
|
||||
def scene_composition_context(snapshot: dict[str, object]) -> dict[str, object]:
|
||||
return {"vehicle": snapshot["vehicle"]}
|
||||
|
||||
@staticmethod
|
||||
def fast_answer(
|
||||
_message: str,
|
||||
_snapshot: dict[str, object],
|
||||
) -> None:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def control_terms(_snapshot: dict[str, object]) -> tuple[str, ...]:
|
||||
return ()
|
||||
|
||||
@staticmethod
|
||||
def query(
|
||||
snapshot: dict[str, object],
|
||||
_query: str,
|
||||
) -> dict[str, object]:
|
||||
return snapshot
|
||||
|
||||
with patch.object(
|
||||
AssistantApiHandler,
|
||||
"assistant_capability_catalog",
|
||||
RecordingCatalog(),
|
||||
):
|
||||
events = self.post_assistant(
|
||||
{
|
||||
"message": "解释一下 CAN FD",
|
||||
"conversation_id": "conv-api-selected-vehicle-catalog",
|
||||
"vehicle_id": "alternate_vehicle",
|
||||
"history": [],
|
||||
}
|
||||
)
|
||||
|
||||
assert requested_vehicle_ids == ["alternate_vehicle"]
|
||||
assert events[-1]["type"] == "answer.completed"
|
||||
|
||||
def test_saved_mode_detail_question_is_answered_from_catalog_over_http(
|
||||
self,
|
||||
) -> None:
|
||||
message = "办公模式下会控制哪些车端设备,执行哪些动作?"
|
||||
with patch.object(
|
||||
AssistantApiHandler,
|
||||
"assistant_capability_catalog",
|
||||
StaticSavedModeCatalog(),
|
||||
):
|
||||
events = self.post_assistant(
|
||||
{
|
||||
"message": message,
|
||||
"conversation_id": "conv-api-saved-mode-detail",
|
||||
"execution_mode": "hardware",
|
||||
}
|
||||
)
|
||||
|
||||
assert events[0]["route"] == "capability_catalog"
|
||||
assert events[0]["engine"] == "n7web-capability-registry"
|
||||
assert events[-1]["route"] == "capability_catalog"
|
||||
assert "办公模式" in events[-1]["answer"]
|
||||
assert "副驾" in events[-1]["answer"]
|
||||
assert "25" in events[-1]["answer"]
|
||||
assert "空调" in events[-1]["answer"]
|
||||
assert not AssistantApiHandler.service.calls
|
||||
assert not FAKE_CODEX.turn_calls
|
||||
assert not FAKE_CODEX.bridge_results
|
||||
|
||||
def test_unknown_saved_mode_detail_question_returns_a_bounded_answer_over_http(
|
||||
self,
|
||||
) -> None:
|
||||
with patch.object(
|
||||
AssistantApiHandler,
|
||||
"assistant_capability_catalog",
|
||||
StaticSavedModeCatalog(),
|
||||
):
|
||||
events = self.post_assistant(
|
||||
{
|
||||
"message": "火星模式中包含哪些设备和动作?",
|
||||
"conversation_id": "conv-api-missing-mode-detail",
|
||||
"execution_mode": "hardware",
|
||||
}
|
||||
)
|
||||
|
||||
assert events[0]["route"] == "capability_catalog"
|
||||
assert events[-1]["route"] == "capability_catalog"
|
||||
assert "火星模式" in events[-1]["answer"]
|
||||
assert any(word in events[-1]["answer"] for word in ("未找到", "不存在"))
|
||||
assert not AssistantApiHandler.service.calls
|
||||
assert not FAKE_CODEX.turn_calls
|
||||
assert not FAKE_CODEX.bridge_results
|
||||
|
||||
def test_saved_mode_update_uses_deterministic_management_route_over_http(
|
||||
self,
|
||||
) -> None:
|
||||
message = "调整办公模式,把办公模式的副驾空调温度调整为26度。"
|
||||
|
||||
def update_mode(**kwargs: object) -> dict[str, object]:
|
||||
AssistantApiHandler.service.calls.append(dict(kwargs))
|
||||
callback = kwargs.get("on_commands_validated")
|
||||
if callable(callback):
|
||||
callback({"status": "processing", "request_id": "req-api-mode-update"})
|
||||
return {
|
||||
"request_id": "req-api-mode-update",
|
||||
"utterance": message,
|
||||
"conversation_id": kwargs["conversation_id"],
|
||||
"status": "succeeded",
|
||||
"execution_mode": "dry_run",
|
||||
"hardware_execution_allowed": False,
|
||||
"supplement_required": False,
|
||||
"orchestration_route": "mode_profile_update",
|
||||
"decision_summary": ["已更新长期模式“办公模式”。"],
|
||||
"mode_profile": {
|
||||
"id": "office",
|
||||
"name": "办公模式",
|
||||
"origin": "customized",
|
||||
"domains": ["seat", "climate"],
|
||||
"operation_count": 2,
|
||||
},
|
||||
"intent_ir": None,
|
||||
"command_plans": {},
|
||||
"can_commands": [],
|
||||
}
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
AssistantApiHandler,
|
||||
"assistant_capability_catalog",
|
||||
StaticSavedModeCatalog(),
|
||||
),
|
||||
patch.object(
|
||||
AssistantApiHandler.service,
|
||||
"preview_native_can",
|
||||
side_effect=update_mode,
|
||||
),
|
||||
):
|
||||
events = self.post_assistant(
|
||||
{
|
||||
"message": message,
|
||||
"conversation_id": "conv-api-saved-mode-update",
|
||||
"execution_mode": "hardware",
|
||||
}
|
||||
)
|
||||
|
||||
assert events[0]["route"] == "mode_profile_update"
|
||||
assert events[0]["engine"] == "n7web-mode-profile-manager"
|
||||
action = next(event for event in events if event["type"] == "action.completed")
|
||||
assert action["success"] is True
|
||||
assert action["payload"]["orchestration_route"] == "mode_profile_update"
|
||||
assert action["payload"]["execution_mode"] == "dry_run"
|
||||
assert action["payload"]["hardware_execution_allowed"] is False
|
||||
assert events[-1]["route"] == "mode_profile_update"
|
||||
assert "办公模式" in events[-1]["answer"]
|
||||
assert "车辆控制未完成" not in events[-1]["answer"]
|
||||
assert len(AssistantApiHandler.service.calls) == 1
|
||||
assert AssistantApiHandler.service.calls[0]["utterance"] == message
|
||||
assert not FAKE_CODEX.turn_calls
|
||||
assert not FAKE_CODEX.bridge_results
|
||||
|
||||
def test_saved_mode_delete_uses_deterministic_management_route_over_http(
|
||||
self,
|
||||
) -> None:
|
||||
message = "删除办公模式"
|
||||
|
||||
def delete_mode(**kwargs: object) -> dict[str, object]:
|
||||
AssistantApiHandler.service.calls.append(dict(kwargs))
|
||||
return {
|
||||
"request_id": "req-api-mode-delete",
|
||||
"utterance": message,
|
||||
"conversation_id": kwargs["conversation_id"],
|
||||
"status": "succeeded",
|
||||
"execution_mode": "dry_run",
|
||||
"hardware_execution_allowed": False,
|
||||
"supplement_required": False,
|
||||
"orchestration_route": "mode_profile_delete",
|
||||
"decision_summary": ["已删除自定义模式“办公模式”。"],
|
||||
"deleted_mode_profile": {
|
||||
"id": "office",
|
||||
"name": "办公模式",
|
||||
"origin": "user",
|
||||
"operation_count": 2,
|
||||
},
|
||||
"intent_ir": None,
|
||||
"command_plans": {},
|
||||
"can_commands": [],
|
||||
}
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
AssistantApiHandler,
|
||||
"assistant_capability_catalog",
|
||||
StaticSavedModeCatalog(),
|
||||
),
|
||||
patch.object(
|
||||
AssistantApiHandler.service,
|
||||
"preview_native_can",
|
||||
side_effect=delete_mode,
|
||||
),
|
||||
):
|
||||
events = self.post_assistant(
|
||||
{
|
||||
"message": message,
|
||||
"conversation_id": "conv-api-saved-mode-delete",
|
||||
"execution_mode": "hardware",
|
||||
}
|
||||
)
|
||||
|
||||
assert events[0]["route"] == "mode_profile_delete"
|
||||
assert events[0]["engine"] == "n7web-mode-profile-manager"
|
||||
action = next(event for event in events if event["type"] == "action.completed")
|
||||
assert action["success"] is True
|
||||
assert action["payload"]["orchestration_route"] == "mode_profile_delete"
|
||||
assert action["payload"]["execution_mode"] == "dry_run"
|
||||
assert action["payload"]["hardware_execution_allowed"] is False
|
||||
assert events[-1]["route"] == "mode_profile_delete"
|
||||
assert "办公模式" in events[-1]["answer"]
|
||||
assert "删除" in events[-1]["answer"]
|
||||
assert "仍保留" not in events[-1]["answer"]
|
||||
assert len(AssistantApiHandler.service.calls) == 1
|
||||
assert not FAKE_CODEX.turn_calls
|
||||
assert not FAKE_CODEX.bridge_results
|
||||
|
||||
def test_oral_template_preview_request_is_zero_codex_over_http(self) -> None:
|
||||
template_plan = {
|
||||
"detected_domains": ["climate"],
|
||||
"requests": [
|
||||
{
|
||||
"domain": "climate",
|
||||
"capability": "climate.set_temperature",
|
||||
"target_scope": "vehicle",
|
||||
"operation": "set",
|
||||
"duration_ms": None,
|
||||
"parameters": [{"name": "temperature_c", "value": 24}],
|
||||
"reason": "观影模式模板温度。",
|
||||
}
|
||||
],
|
||||
"ambiguities": [],
|
||||
"decision_summary": ["命中测试观影模板。"],
|
||||
"confidence": 1.0,
|
||||
"metadata": {
|
||||
"source": "scene_template",
|
||||
"template_id": "cinema",
|
||||
"template_revision": "http-template-r1",
|
||||
},
|
||||
}
|
||||
|
||||
class TemplateCatalog:
|
||||
@staticmethod
|
||||
def snapshot(vehicle_id: str | None = None) -> dict[str, object]:
|
||||
return {
|
||||
"vehicle": {"id": vehicle_id or "denza_n7", "name": "腾势 N7"},
|
||||
"capabilities": [],
|
||||
"modes": [],
|
||||
"scene_templates": [
|
||||
{
|
||||
"id": "cinema",
|
||||
"name": "观影模式",
|
||||
"aliases": ["影院模式"],
|
||||
"assistant_callable": True,
|
||||
"template_revision": "http-template-r1",
|
||||
"plan": template_plan,
|
||||
}
|
||||
],
|
||||
"source_status": {
|
||||
"modes": "ready",
|
||||
"scene_templates": "ready",
|
||||
},
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def compact_context(snapshot: dict[str, object]) -> dict[str, object]:
|
||||
return {"vehicle": snapshot["vehicle"]}
|
||||
|
||||
@staticmethod
|
||||
def scene_composition_context(snapshot: dict[str, object]) -> dict[str, object]:
|
||||
return {
|
||||
"composition_revision": "http-scene-r1",
|
||||
"vehicle": snapshot["vehicle"],
|
||||
"capabilities": [],
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def fast_answer(
|
||||
_message: str,
|
||||
_snapshot: dict[str, object],
|
||||
) -> None:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def control_terms(_snapshot: dict[str, object]) -> tuple[str, ...]:
|
||||
return ("空调", "车窗", "遮阳帘")
|
||||
|
||||
@staticmethod
|
||||
def query(
|
||||
snapshot: dict[str, object],
|
||||
_query: str,
|
||||
) -> dict[str, object]:
|
||||
return snapshot
|
||||
|
||||
message = "请帮我创建一个观影模式,先给我看看吧?"
|
||||
thread_count = len(FAKE_CODEX.thread_instructions)
|
||||
with patch.object(
|
||||
AssistantApiHandler,
|
||||
"assistant_capability_catalog",
|
||||
TemplateCatalog(),
|
||||
):
|
||||
events = self.post_assistant(
|
||||
{
|
||||
"message": message,
|
||||
"conversation_id": "conv-api-oral-scene-template",
|
||||
"execution_mode": "hardware",
|
||||
}
|
||||
)
|
||||
|
||||
assert events[0]["route"] == "scene_template"
|
||||
assert events[0]["engine"] == "n7web-scene-template"
|
||||
assert events[-1]["route"] == "scene_template"
|
||||
assert not FAKE_CODEX.turn_calls
|
||||
assert not FAKE_CODEX.bridge_results
|
||||
assert len(FAKE_CODEX.thread_instructions) == thread_count
|
||||
assert len(AssistantApiHandler.service.calls) == 1
|
||||
call = AssistantApiHandler.service.calls[0]
|
||||
assert call["utterance"] == message
|
||||
assert call["provider"] == "local"
|
||||
assert call["assistant_plan"]["metadata"] == template_plan["metadata"]
|
||||
action = next(event for event in events if event["type"] == "action.completed")
|
||||
assert action["success"] is True
|
||||
assert action["payload"]["execution_mode"] == "dry_run"
|
||||
assert action["payload"]["hardware_execution_allowed"] is False
|
||||
|
||||
def test_model_auth_failure_is_a_safe_terminal_ndjson_event(self) -> None:
|
||||
secret = "deployment-secret-must-never-reach-the-browser"
|
||||
provider_error = CodexAppServerError(
|
||||
@@ -571,7 +1018,7 @@ class TestCodexAssistantWebApi:
|
||||
assert events[-1] == {
|
||||
"type": "error",
|
||||
"error": "assistant_model_failed",
|
||||
"message": "Codex AI 助手暂时没有响应,请稍后再试。",
|
||||
"message": "小迪暂时没有响应,请稍后再试。",
|
||||
}
|
||||
serialized = json.dumps(events, ensure_ascii=False)
|
||||
assert secret not in serialized
|
||||
@@ -836,9 +1283,12 @@ class TestCodexAssistantWebApi:
|
||||
}
|
||||
)
|
||||
|
||||
assert events[0]["route"] == "agent"
|
||||
assert events[-1]["answer"] == "这是 Codex 的测试回答。"
|
||||
assert events[0]["route"] == "capability_catalog"
|
||||
assert events[-1]["route"] == "capability_catalog"
|
||||
assert "小憩模式" in events[-1]["answer"]
|
||||
assert "未下发车辆控制" in events[-1]["answer"]
|
||||
assert not AssistantApiHandler.service.calls
|
||||
assert not FAKE_CODEX.turn_calls
|
||||
|
||||
def test_exact_movie_mode_request_streams_a_nine_action_preview(self) -> None:
|
||||
events = self.post_assistant(
|
||||
@@ -999,7 +1449,9 @@ class TestCodexAssistantWebApi:
|
||||
assert capabilities["roof.set_shade_position"]["native_can_verified"] is True
|
||||
assert capabilities["door.set_vehicle_lock"]["assistant_callable"] is False
|
||||
assert capabilities["interior_light.set_wall_power"]["assistant_callable"] is False
|
||||
assert len(payload["modes"]) == 13
|
||||
# The endpoint is backed by the live registry and may legitimately
|
||||
# include user-created profiles in addition to the 13 bundled modes.
|
||||
assert len(payload["modes"]) >= 13
|
||||
|
||||
serialized = json.dumps(payload, ensure_ascii=False)
|
||||
assert "approval_token" not in serialized
|
||||
@@ -1036,6 +1488,64 @@ class TestCodexAssistantWebApi:
|
||||
assert "车窗" in events[-1]["answer"]
|
||||
assert "车门锁" in events[-1]["answer"]
|
||||
|
||||
def test_unsupported_vehicle_control_returns_clickable_suggestions_without_codex(
|
||||
self,
|
||||
) -> None:
|
||||
FAKE_CODEX.available = False
|
||||
|
||||
events = self.post_assistant(
|
||||
{
|
||||
"message": "打开后备箱",
|
||||
"conversation_id": "conv-api-fast-unsupported-control",
|
||||
"history": [],
|
||||
}
|
||||
)
|
||||
|
||||
assert [event["type"] for event in events] == [
|
||||
"message.accepted",
|
||||
"work.started",
|
||||
"answer.delta",
|
||||
"control.suggestions",
|
||||
"work.completed",
|
||||
"answer.completed",
|
||||
]
|
||||
assert events[0]["route"] == "unsupported_control"
|
||||
assert events[0]["engine"] == "n7web-capability-registry"
|
||||
assert events[-1]["route"] == "unsupported_control"
|
||||
assert events[-1]["topic"] == "unsupported_vehicle_control"
|
||||
assert events[-1]["answer"].startswith(
|
||||
"当前车端暂不支持该功能控制哦,您可以尝试以下控制:"
|
||||
)
|
||||
suggestion_event = events[3]["suggestions"]
|
||||
assert suggestion_event == events[-1]["suggestions"]
|
||||
assert {item["command"] for item in suggestion_event} == {
|
||||
"关闭所有车窗",
|
||||
"关闭遮阳帘",
|
||||
"打开空调,将全车温度设置为24度",
|
||||
}
|
||||
assert all(item["capability_ids"] for item in suggestion_event)
|
||||
assert not AssistantApiHandler.service.calls
|
||||
assert not FAKE_CODEX.turn_calls
|
||||
assert not FAKE_CODEX.bridge_results
|
||||
|
||||
clicked = self.post_assistant(
|
||||
{
|
||||
"message": "关闭所有车窗",
|
||||
"conversation_id": "conv-api-click-quick-control",
|
||||
"history": [],
|
||||
}
|
||||
)
|
||||
|
||||
assert clicked[0]["route"] == "vehicle_control"
|
||||
assert clicked[0]["engine"] == "n7web-control-router"
|
||||
assert clicked[-1]["route"] == "vehicle_control"
|
||||
assert "关闭所有车窗" in clicked[-1]["answer"]
|
||||
assert "升温" not in clicked[-1]["answer"]
|
||||
assert len(AssistantApiHandler.service.calls) == 1
|
||||
assert AssistantApiHandler.service.calls[0]["utterance"] == "关闭所有车窗"
|
||||
assert not FAKE_CODEX.turn_calls
|
||||
assert not FAKE_CODEX.bridge_results
|
||||
|
||||
def test_internal_capability_query_rejects_missing_turn_context(self) -> None:
|
||||
status, content_type, body = self._request_json(
|
||||
"/api/internal/codex/vehicle-control",
|
||||
|
||||
@@ -621,7 +621,7 @@ 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-127", timeout=3) as response:
|
||||
with self.opener.open(f"{self.base_url}/app.js?v=dual-seat-128", 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
|
||||
@@ -632,11 +632,11 @@ operations:
|
||||
) as response:
|
||||
console_script = response.read().decode("utf-8")
|
||||
with self.opener.open(
|
||||
f"{self.base_url}/console-v2.css?v=delivery-state-65", timeout=3
|
||||
f"{self.base_url}/console-v2.css?v=delivery-state-66", timeout=3
|
||||
) as response:
|
||||
console_styles = response.read().decode("utf-8")
|
||||
with self.opener.open(
|
||||
f"{self.base_url}/speech-input.js?v=sensevoice-12", timeout=3
|
||||
f"{self.base_url}/speech-input.js?v=sensevoice-13", timeout=3
|
||||
) as response:
|
||||
speech_script = response.read().decode("utf-8")
|
||||
with self.opener.open(
|
||||
@@ -656,7 +656,7 @@ operations:
|
||||
) as response:
|
||||
extension_styles = response.read().decode("utf-8").replace("\r\n", "\n")
|
||||
with self.opener.open(
|
||||
f"{self.base_url}/assistant-chat.js?v=codex-assistant-24", timeout=3
|
||||
f"{self.base_url}/assistant-chat.js?v=codex-assistant-26", timeout=3
|
||||
) as response:
|
||||
assistant_script = response.read().decode("utf-8")
|
||||
with self.opener.open(
|
||||
@@ -697,13 +697,13 @@ 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-127"', html)
|
||||
self.assertIn('src="/app.js?v=dual-seat-128"', html)
|
||||
self.assertIn('href="/styles.css?v=dual-seat-23"', 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-62"', html)
|
||||
self.assertIn('href="/console-v2.css?v=delivery-state-65"', html)
|
||||
self.assertIn('src="/speech-input.js?v=sensevoice-12"', html)
|
||||
self.assertIn('href="/console-v2.css?v=delivery-state-66"', html)
|
||||
self.assertIn('src="/speech-input.js?v=sensevoice-13"', html)
|
||||
self.assertIn('href="/speech-input.css?v=sensevoice-7"', html)
|
||||
self.assertIn('id="speechDictationButton"', html)
|
||||
self.assertIn('class="speech-waveform"', html)
|
||||
@@ -718,8 +718,8 @@ operations:
|
||||
self.assertNotIn("滚动查看更多", html)
|
||||
self.assertIn('aria-label="发送消息"', html)
|
||||
self.assertIn('id="assistantTranscript"', html)
|
||||
self.assertIn('href="/assistant-chat.css?v=codex-assistant-24"', html)
|
||||
self.assertIn('src="/assistant-chat.js?v=codex-assistant-24"', html)
|
||||
self.assertIn('href="/assistant-chat.css?v=codex-assistant-26"', html)
|
||||
self.assertIn('src="/assistant-chat.js?v=codex-assistant-26"', html)
|
||||
self.assertIn('href="/energy-monitor.css?v=energy-monitor-4"', html)
|
||||
self.assertIn('src="/energy-monitor.js?v=energy-monitor-4"', html)
|
||||
self.assertNotIn('"admin"', console_script)
|
||||
@@ -843,7 +843,7 @@ operations:
|
||||
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('const dynamicOrder = new Map([["rocking_horse", 0]', app_script)
|
||||
self.assertIn('divider.className = "mode-group-divider"', app_script)
|
||||
self.assertIn("function isHardwareExecutionReady()", app_script)
|
||||
self.assertIn("function hardwareExecutionIssue(", app_script)
|
||||
@@ -992,15 +992,15 @@ 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-v272"', service_worker)
|
||||
self.assertIn('/console-v2.css?v=delivery-state-65', service_worker)
|
||||
self.assertIn('const CACHE_VERSION = "cognitive-agent-pad-v275"', service_worker)
|
||||
self.assertIn('/console-v2.css?v=delivery-state-66', service_worker)
|
||||
self.assertIn('/console-v2.js?v=delivery-state-62', service_worker)
|
||||
self.assertIn('/assistant-chat.css?v=codex-assistant-24', service_worker)
|
||||
self.assertIn('/assistant-chat.js?v=codex-assistant-24', service_worker)
|
||||
self.assertIn('/assistant-chat.css?v=codex-assistant-26', service_worker)
|
||||
self.assertIn('/assistant-chat.js?v=codex-assistant-26', service_worker)
|
||||
self.assertIn('/energy-monitor.css?v=energy-monitor-4', service_worker)
|
||||
self.assertIn('/energy-monitor.js?v=energy-monitor-4', service_worker)
|
||||
self.assertIn('"/app.js?v=dual-seat-127"', service_worker)
|
||||
self.assertIn('"/speech-input.js?v=sensevoice-12"', service_worker)
|
||||
self.assertIn('"/app.js?v=dual-seat-128"', service_worker)
|
||||
self.assertIn('"/speech-input.js?v=sensevoice-13"', service_worker)
|
||||
self.assertIn('"/speech-input.css?v=sensevoice-7"', service_worker)
|
||||
self.assertIn('"/speech-audio-worklet.js?v=sensevoice-5"', service_worker)
|
||||
self.assertIn('fetch("/api/speech/preview"', speech_script)
|
||||
@@ -1026,7 +1026,7 @@ operations:
|
||||
self.assertIn("speechExecutionEligible", app_script)
|
||||
self.assertIn("speech_utterance_id", app_script)
|
||||
self.assertIn("speechUtteranceId", speech_script)
|
||||
self.assertIn('const DEFAULT_WAKE_WORD = "你好,双十一"', speech_script)
|
||||
self.assertIn('const DEFAULT_WAKE_WORD = "你好,小迪"', speech_script)
|
||||
self.assertIn('replaceInput(liveText, "partial")', speech_script)
|
||||
self.assertIn('speech_semantic: "语音语义"', console_script)
|
||||
self.assertIn("语音意图已识别,但当前车辆能力不支持执行", app_script)
|
||||
@@ -1121,10 +1121,10 @@ operations:
|
||||
self.assertNotIn('id: "frequent"', gallery_groups)
|
||||
self.assertNotIn('id: "comfort"', gallery_groups)
|
||||
self.assertIn("function isDynamicModeProfile", app_script)
|
||||
self.assertIn('if (group.id === "static") grid.append(createModeGalleryAddCard())', app_script)
|
||||
self.assertIn('if (group.id === "custom") grid.append(createModeGalleryAddCard())', app_script)
|
||||
self.assertIn('button.dataset.modeOrigin === "user"', app_script)
|
||||
self.assertIn('appendGroupLabel("我的模式", "user")', app_script)
|
||||
self.assertIn('appendDivider("user")', app_script)
|
||||
self.assertIn('appendGroupLabel("自定义模式", "custom")', app_script)
|
||||
self.assertIn('appendDivider("custom")', app_script)
|
||||
self.assertIn('deleteUserModeProfile(deleteButton.dataset.modeDelete)', app_script)
|
||||
self.assertIn('method: "DELETE"', app_script)
|
||||
self.assertIn('deleteButton.dataset.modeDelete = mode.id', app_script)
|
||||
@@ -1456,7 +1456,7 @@ operations:
|
||||
) as response:
|
||||
extension_script = response.read().decode("utf-8")
|
||||
with self.opener.open(
|
||||
f"{self.base_url}/app.js?v=dual-seat-127", timeout=3
|
||||
f"{self.base_url}/app.js?v=dual-seat-128", timeout=3
|
||||
) as response:
|
||||
app_script = response.read().decode("utf-8")
|
||||
|
||||
|
||||
@@ -32,8 +32,8 @@ class SpeechStub:
|
||||
},
|
||||
"wake_word": {
|
||||
"enabled": True,
|
||||
"primary": "你好,双十一",
|
||||
"aliases": ["你好,双十一", "你好,双11"],
|
||||
"primary": "你好,小迪",
|
||||
"aliases": ["你好,小迪"],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -62,8 +62,8 @@ class SpeechStub:
|
||||
"utterance_id": utterance_id,
|
||||
"sequence": sequence,
|
||||
"state": "partial",
|
||||
"raw_text": "你好双十一打开空",
|
||||
"text": "你好双十一打开空",
|
||||
"raw_text": "你好小迪打开空",
|
||||
"text": "你好小迪打开空",
|
||||
"live_text": "打开空",
|
||||
"wake_word": {"detected": True, "remainder": "打开空"},
|
||||
"semantic_cleaned": False,
|
||||
@@ -88,7 +88,7 @@ class SpeechStub:
|
||||
)
|
||||
return {
|
||||
"utterance_id": utterance_id,
|
||||
"captured_text": "你好双十一车上有点热",
|
||||
"captured_text": "你好小迪车上有点热",
|
||||
"raw_text": "车上有点热",
|
||||
"assistant_text": "车上有点热。",
|
||||
"text": "车上有点热。",
|
||||
@@ -161,7 +161,7 @@ class TestSpeechLiveWebApi:
|
||||
"final_default": "manual",
|
||||
}
|
||||
assert payload["wake_word"]["enabled"] is True
|
||||
assert payload["wake_word"]["primary"] == "你好,双十一"
|
||||
assert payload["wake_word"]["primary"] == "你好,小迪"
|
||||
|
||||
def test_preview_endpoint_forwards_accumulated_audio_identity_and_sequence(self) -> None:
|
||||
status, payload = self.post_audio(
|
||||
@@ -183,7 +183,7 @@ class TestSpeechLiveWebApi:
|
||||
]
|
||||
assert payload["state"] == "partial"
|
||||
assert payload["sequence"] == 7
|
||||
assert payload["raw_text"] == "你好双十一打开空"
|
||||
assert payload["raw_text"] == "你好小迪打开空"
|
||||
assert payload["live_text"] == "打开空"
|
||||
assert payload["semantic_cleaned"] is False
|
||||
|
||||
|
||||
Reference in New Issue
Block a user