fix: restore mode clicks and compose seat-aware assistant scenes
This commit is contained in:
+22
-2
@@ -5910,22 +5910,34 @@ function installModeChipScroller() {
|
||||
pointerStartX = event.clientX;
|
||||
pointerStartScroll = group.scrollLeft;
|
||||
dragged = false;
|
||||
group.setPointerCapture?.(pointerId);
|
||||
suppressClick = false;
|
||||
});
|
||||
group.addEventListener("pointermove", (event) => {
|
||||
if (pointerId !== event.pointerId) return;
|
||||
if (event.buttons !== 1) {
|
||||
finishPointerDrag(event);
|
||||
return;
|
||||
}
|
||||
const distance = event.clientX - pointerStartX;
|
||||
if (!dragged && Math.abs(distance) < 5) return;
|
||||
if (!dragged) {
|
||||
// Capturing on pointerdown retargets a normal button click to the rail.
|
||||
// Capture only after movement establishes that this gesture is a drag.
|
||||
dragged = true;
|
||||
group.setPointerCapture?.(pointerId);
|
||||
group.classList.add("is-dragging");
|
||||
}
|
||||
group.scrollLeft = Math.max(0, Math.min(maximumScroll(), pointerStartScroll - distance));
|
||||
updateScrollState();
|
||||
event.preventDefault();
|
||||
});
|
||||
const finishPointerDrag = (event) => {
|
||||
if (pointerId !== event.pointerId) return;
|
||||
group.releasePointerCapture?.(pointerId);
|
||||
const finishedPointerId = pointerId;
|
||||
pointerId = null;
|
||||
if (group.hasPointerCapture?.(finishedPointerId)) {
|
||||
group.releasePointerCapture(finishedPointerId);
|
||||
}
|
||||
group.classList.remove("is-dragging");
|
||||
if (dragged) {
|
||||
suppressClick = true;
|
||||
@@ -5935,6 +5947,7 @@ function installModeChipScroller() {
|
||||
};
|
||||
group.addEventListener("pointerup", finishPointerDrag);
|
||||
group.addEventListener("pointercancel", finishPointerDrag);
|
||||
group.addEventListener("lostpointercapture", finishPointerDrag);
|
||||
group.addEventListener("click", (event) => {
|
||||
if (!suppressClick) return;
|
||||
event.preventDefault();
|
||||
@@ -6424,10 +6437,17 @@ function applyModeCommand(command, modeProfileId = null) {
|
||||
showToast("AI助手正在处理上一条指令,请稍候");
|
||||
return false;
|
||||
}
|
||||
const speech = window.CognitiveAgentSpeech;
|
||||
if (["capturing", "finalizing"].includes(speech?.phase)
|
||||
|| (speech?.phase === "arming" && speech?.inputMode === "manual")) {
|
||||
showToast("请等待语音整理完成后再发送");
|
||||
return false;
|
||||
}
|
||||
if ($("#runButton")?.disabled) {
|
||||
showToast("上一条指令仍在执行,请稍候");
|
||||
return false;
|
||||
}
|
||||
hideModeActionPopover({ immediate: true });
|
||||
const mode = modeProfiles().find((item) => item.id === modeProfileId);
|
||||
setModeRailSelection(mode);
|
||||
closeModeGallery({ restoreFocus: false });
|
||||
|
||||
@@ -1478,7 +1478,14 @@
|
||||
}
|
||||
|
||||
.console-v2.assistant-chat-enabled.v2-passive-display.v2-interactive-sync .v2-operation-shell {
|
||||
top: 60px;
|
||||
/* Share the content row on compact screens, never cover the mode rail. */
|
||||
position: relative;
|
||||
grid-column: 1;
|
||||
grid-row: 3;
|
||||
width: 100%;
|
||||
top: auto;
|
||||
right: auto;
|
||||
bottom: auto;
|
||||
}
|
||||
|
||||
.console-v2.assistant-chat-enabled.v2-passive-display.v2-interactive-sync .v2-module-switcher {
|
||||
|
||||
@@ -647,11 +647,39 @@
|
||||
"seat.set_cushion_tilt": "坐垫角度",
|
||||
"seat.set_backrest_recline": "靠背角度",
|
||||
"seat.set_legrest_height": "腿托高度",
|
||||
"seat.set_ventilation": "座椅通风",
|
||||
"seat.set_heating": "座椅加热",
|
||||
"seat.set_driver_ventilation_native": "座椅通风",
|
||||
"seat.set_driver_heating_native": "座椅加热",
|
||||
"seat.set_driver_massage_native": "座椅按摩",
|
||||
};
|
||||
label = `${seatLabel}${seatCapabilityLabels[capability] || "座椅"}`;
|
||||
value = params.position_pct != null
|
||||
? `${params.position_pct}%${params.angle_deg != null ? `(${params.angle_deg}°)` : ""}`
|
||||
: motionLabel(params.state || params.mode_id);
|
||||
if (["seat.set_driver_ventilation_native", "seat.set_driver_heating_native"].includes(capability)) {
|
||||
// CH1 driver controls encode high as 1 and low as 2; passenger
|
||||
// comfort levels use a different scale and must not reuse this map.
|
||||
const level = { 1: "高档", 2: "低档" }[params.level ?? 2] || "档位待确认";
|
||||
value = params.state === "off" ? "关闭"
|
||||
: params.state === "on" ? `开启 · ${level}` : "开关状态待确认";
|
||||
} else if (["seat.set_ventilation", "seat.set_heating"].includes(capability)) {
|
||||
value = params.state === "off" ? "关闭"
|
||||
: params.state === "on"
|
||||
? `开启${params.level != null ? ` · ${params.level}档` : ""}`
|
||||
: "开关状态待确认";
|
||||
} else if (capability === "seat.set_driver_massage_native") {
|
||||
const massageModes = {
|
||||
wave: "波浪", pulse: "脉冲", stretch: "舒展", slack: "舒缓", relax: "放松",
|
||||
};
|
||||
const massageStrengths = { 1: "轻柔", 2: "中等", 3: "较强" };
|
||||
value = params.action === "off" ? "关闭"
|
||||
: params.action === "mode"
|
||||
? (massageModes[params.mode] ? `${massageModes[params.mode]}模式` : "按摩模式待确认")
|
||||
: params.action === "strength"
|
||||
? (massageStrengths[params.strength] ? `${massageStrengths[params.strength]}力度` : "按摩力度待确认")
|
||||
: "按摩动作待确认";
|
||||
}
|
||||
icon = "armchair";
|
||||
order = instructionOrder("座椅", "副驾", "主驾");
|
||||
} else if (capability === "climate.set_temperature") {
|
||||
@@ -660,11 +688,21 @@
|
||||
icon = "thermometer";
|
||||
order = instructionOrder("空调", "温度");
|
||||
} else if (domain === "climate") {
|
||||
const cycleLabels = { internal: "内循环", external: "外循环", auto: "自动循环" };
|
||||
const climateModeLabels = {
|
||||
"climate.set_cycle_mode": { internal: "内循环", external: "外循环" },
|
||||
"climate.set_control_mode": { manual: "手动控制", auto: "自动控制" },
|
||||
"climate.set_outlet_mode": {
|
||||
face: "吹面", face_feet: "吹面 / 吹脚", feet: "吹脚",
|
||||
feet_defrost: "吹脚 / 除霜", defrost: "除霜",
|
||||
face_feet_defrost: "吹面 / 吹脚 / 除霜", face_defrost: "吹面 / 除霜",
|
||||
},
|
||||
};
|
||||
label = capability === "climate.set_fan_level" ? "空调风量"
|
||||
: capability === "climate.set_cycle_mode" ? "空调循环" : "空调";
|
||||
: capability === "climate.set_cycle_mode" ? "空调循环"
|
||||
: capability === "climate.set_control_mode" ? "空调控制模式"
|
||||
: capability === "climate.set_outlet_mode" ? "空调出风模式" : "空调";
|
||||
value = params.level != null ? `${params.level}档`
|
||||
: params.mode != null ? (cycleLabels[String(params.mode)] || String(params.mode))
|
||||
: params.mode != null ? (climateModeLabels[capability]?.[String(params.mode)] || String(params.mode))
|
||||
: motionLabel(params.state);
|
||||
icon = "fan";
|
||||
order = instructionOrder("空调", "风量");
|
||||
@@ -676,6 +714,18 @@
|
||||
} else if (["interior_light", "ambient_light"].includes(domain)) {
|
||||
label = domain === "ambient_light" ? "氛围灯" : "室内灯";
|
||||
value = motionLabel(params.state);
|
||||
if (capability === "ambient_light.set_brightness_level") {
|
||||
label = "氛围灯亮度";
|
||||
value = params.brightness_level != null ? `${params.brightness_level}档` : "亮度档位待确认";
|
||||
} else if (capability === "ambient_light.set_color_index") {
|
||||
label = "氛围灯颜色";
|
||||
value = params.color_index != null ? `色号 ${params.color_index}` : "颜色待确认";
|
||||
} else if (capability === "ambient_light.set_illumination") {
|
||||
value = [
|
||||
params.color_hex != null ? `颜色 ${params.color_hex}` : "颜色待确认",
|
||||
params.brightness_pct != null ? `亮度 ${params.brightness_pct}%` : "亮度待确认",
|
||||
].join(" · ");
|
||||
}
|
||||
icon = "lightbulb";
|
||||
order = instructionOrder(label, "灯光");
|
||||
}
|
||||
|
||||
@@ -13,20 +13,20 @@
|
||||
<link rel="stylesheet" href="/console-v2.css?v=delivery-state-66" />
|
||||
<link rel="stylesheet" href="/self-extension.css?v=gscu-tcp-43" />
|
||||
<link rel="stylesheet" href="/speech-input.css?v=sensevoice-7" />
|
||||
<link rel="stylesheet" href="/assistant-chat.css?v=codex-assistant-26" />
|
||||
<link rel="stylesheet" href="/assistant-chat.css?v=codex-assistant-27" />
|
||||
<link rel="stylesheet" href="/energy-monitor.css?v=energy-monitor-4" />
|
||||
<script src="/vendor/lucide/0.468.0/lucide.min.js" defer></script>
|
||||
<script src="/vendor/three/0.147.0/three.min.js" defer></script>
|
||||
<script src="/vendor/three/0.147.0/fflate.min.js" defer></script>
|
||||
<script src="/vendor/three/0.147.0/FBXLoader.js" defer></script>
|
||||
<script src="/network.js?v=android-pad-2" defer></script>
|
||||
<script src="/app.js?v=dual-seat-130" defer></script>
|
||||
<script src="/app.js?v=dual-seat-131" defer></script>
|
||||
<script src="/speech-input.js?v=sensevoice-13" defer></script>
|
||||
<script type="module" src="/gesture-control.js?v=android-pad-9"></script>
|
||||
<script src="/pwa.js?v=android-pad-3" defer></script>
|
||||
<script src="/console-v2.js?v=delivery-state-62" defer></script>
|
||||
<script src="/self-extension.js?v=gscu-tcp-43" defer></script>
|
||||
<script src="/assistant-chat.js?v=codex-assistant-26" defer></script>
|
||||
<script src="/assistant-chat.js?v=codex-assistant-27" defer></script>
|
||||
<script src="/energy-monitor.js?v=energy-monitor-4" defer></script>
|
||||
</head>
|
||||
<body class="console-v2">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const CACHE_VERSION = "cognitive-agent-pad-v277";
|
||||
const CACHE_VERSION = "cognitive-agent-pad-v278";
|
||||
const SHELL_CACHE = `${CACHE_VERSION}-shell`;
|
||||
const RUNTIME_CACHE = `${CACHE_VERSION}-runtime`;
|
||||
const APP_SHELL = [
|
||||
@@ -6,7 +6,7 @@ const APP_SHELL = [
|
||||
"/index.html",
|
||||
"/styles.css?v=dual-seat-23",
|
||||
"/network.js?v=android-pad-2",
|
||||
"/app.js?v=dual-seat-130",
|
||||
"/app.js?v=dual-seat-131",
|
||||
"/gesture-control.js?v=android-pad-9",
|
||||
"/pwa.js?v=android-pad-3",
|
||||
"/console-v2.css?v=delivery-state-66",
|
||||
@@ -16,8 +16,8 @@ const APP_SHELL = [
|
||||
"/speech-input.css?v=sensevoice-7",
|
||||
"/speech-input.js?v=sensevoice-13",
|
||||
"/speech-audio-worklet.js?v=sensevoice-5",
|
||||
"/assistant-chat.css?v=codex-assistant-26",
|
||||
"/assistant-chat.js?v=codex-assistant-26",
|
||||
"/assistant-chat.css?v=codex-assistant-27",
|
||||
"/assistant-chat.js?v=codex-assistant-27",
|
||||
"/energy-monitor.css?v=energy-monitor-4",
|
||||
"/energy-monitor.js?v=energy-monitor-4",
|
||||
"/assets/pets/self-extension-cat.png?v=3",
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
id: long-trip-comfort
|
||||
name: 长途舒适模式
|
||||
aliases: [长途乘车模式, 高速舒适模式]
|
||||
description: 为长途乘坐提供副驾恒温、柔和风量、外循环与适度遮光。
|
||||
description: 为长途乘坐提供主副驾低档座椅通风、副驾恒温、柔和风量、外循环与适度遮光,不改变座椅驾驶姿态。
|
||||
icon: route
|
||||
operations:
|
||||
- {domain: seat, capability: seat.set_driver_ventilation_native, target_scope: driver, operation: set, parameters: {state: "on", level: 2}, reason: 使用主驾原生 CAN 的低档通风;该接口 2 为低档。}
|
||||
- {domain: seat, capability: seat.set_ventilation, target_scope: passenger, operation: set, parameters: {state: "on", level: 1}, reason: 使用副驾独立接口的低档通风;该接口 1 为低档。}
|
||||
- {domain: climate, capability: climate.set_power, target_scope: vehicle, operation: set, parameters: {state: "on"}, reason: 开启空调。}
|
||||
- {domain: climate, capability: climate.set_temperature, target_scope: passenger, operation: set, parameters: {temperature_c: 24}, reason: 保持副驾舒适温度。}
|
||||
- {domain: climate, capability: climate.set_fan_level, target_scope: vehicle, operation: set, parameters: {level: 2}, reason: 使用中低风量。}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
id: relaxation
|
||||
name: 放松模式
|
||||
aliases: [休闲模式, 舒缓模式, 轻松模式, 放空模式]
|
||||
description: 使用舒展的副驾姿态、安静恒温、适度遮光和低亮暖光,营造适合放松休息的座舱环境。
|
||||
aliases: [休闲模式, 舒缓模式, 轻松模式, 放空模式, 坐着放松模式, 乘坐放松模式, 座椅放松模式, 座位放松模式]
|
||||
description: 使用主驾放松按摩、舒展的副驾姿态、安静恒温、适度遮光和低亮暖光,营造适合驻车放松休息的座舱环境;不改变主驾驾驶姿态。
|
||||
icon: sofa
|
||||
operations:
|
||||
- {domain: seat, capability: seat.set_driver_massage_native, target_scope: driver, operation: set, parameters: {action: mode, mode: relax}, reason: 使用已验证的主驾放松按摩模式;主驾没有绝对位置反馈,不自动移动主驾座椅。}
|
||||
- {domain: seat, capability: seat.set_longitudinal_position, target_scope: passenger, operation: set, parameters: {position_pct: 51}, reason: 将副驾移动到兼顾腿部空间与安全的休闲位置。}
|
||||
- {domain: seat, capability: seat.set_cushion_tilt, target_scope: passenger, operation: set, parameters: {position_pct: 30}, reason: 调整坐盆角度以提供稳定支撑。}
|
||||
- {domain: seat, capability: seat.set_backrest_recline, target_scope: passenger, operation: set, parameters: {position_pct: 66}, reason: 将副驾靠背调整到舒展的休闲角度。}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
id: seat-comfort
|
||||
name: 座椅舒适模式
|
||||
aliases: [座位舒适模式, 乘坐舒适模式, 坐着舒服模式, 坐着舒适模式, 座椅舒服模式, 座位舒服模式]
|
||||
description: 为主副驾组合低档通风、主驾放松按摩和副驾适度靠背及腿托支撑;按两座实际能力分别编排,主驾没有绝对位置反馈,不自动移动主驾座椅。适合驻车调整并预览后使用。
|
||||
icon: armchair
|
||||
operations:
|
||||
- {domain: seat, capability: seat.set_driver_ventilation_native, target_scope: driver, operation: set, parameters: {state: "on", level: 2}, reason: 主驾通风使用独立 CAN 接口的低档 2,减少背部闷热。}
|
||||
- {domain: seat, capability: seat.set_driver_massage_native, target_scope: driver, operation: set, parameters: {action: mode, mode: relax}, reason: 使用主驾已验证的放松按摩模式,不假定副驾具有按摩能力。}
|
||||
- {domain: seat, capability: seat.set_ventilation, target_scope: passenger, operation: set, parameters: {state: "on", level: 1}, reason: 副驾通风使用独立接口的低档 1。}
|
||||
- {domain: seat, capability: seat.set_backrest_recline, target_scope: passenger, operation: set, parameters: {position_pct: 45}, reason: 副驾靠背使用可反馈的适度舒展目标。}
|
||||
- {domain: seat, capability: seat.set_legrest_height, target_scope: passenger, operation: set, parameters: {position_pct: 35}, reason: 副驾腿托提供适度腿部支撑。}
|
||||
- {domain: climate, capability: climate.set_power, target_scope: vehicle, operation: set, parameters: {state: "on"}, reason: 开启座舱恒温。}
|
||||
- {domain: climate, capability: climate.set_temperature, target_scope: vehicle, operation: set, parameters: {temperature_c: 24}, reason: 保持中性舒适温度。}
|
||||
- {domain: climate, capability: climate.set_fan_level, target_scope: vehicle, operation: set, parameters: {level: 1}, reason: 降低风噪和直吹打扰。}
|
||||
@@ -21,6 +21,7 @@ from cognitive_agent.application.assistant_plan_guard import (
|
||||
)
|
||||
from cognitive_agent.application.mode_profile_intent import (
|
||||
normalize_mode_profile_name,
|
||||
parse_mode_profile_create,
|
||||
parse_mode_profile_detail_query,
|
||||
)
|
||||
from cognitive_agent.generated.can_semantic_protocol import CAN0_SUPPORTED_CAPABILITIES
|
||||
@@ -428,6 +429,9 @@ _MODE_CAPABILITY_LABELS = {
|
||||
"seat.set_legrest_extension": "座椅腿托伸展",
|
||||
"seat.set_heating": "座椅加热",
|
||||
"seat.set_ventilation": "座椅通风",
|
||||
"seat.set_driver_ventilation_native": "座椅通风",
|
||||
"seat.set_driver_heating_native": "座椅加热",
|
||||
"seat.set_driver_massage_native": "座椅按摩",
|
||||
"seat.start_dynamic_mode": "座椅动态模式",
|
||||
"climate.set_power": "空调",
|
||||
"climate.set_temperature": "空调温度",
|
||||
@@ -575,6 +579,21 @@ def _mode_operation_text(raw: Mapping[str, object]) -> str:
|
||||
}:
|
||||
target_label = ""
|
||||
subject = f"{target_label}{label}"
|
||||
if capability in {"seat.set_driver_ventilation_native", "seat.set_driver_heating_native"}:
|
||||
if parameters.get("state") == "off":
|
||||
return f"{subject}关闭"
|
||||
level = {1: "高档", 2: "低档"}.get(parameters.get("level", 2), "")
|
||||
return f"{subject}开启{level}"
|
||||
if capability == "seat.set_driver_massage_native":
|
||||
if parameters.get("action") == "off":
|
||||
return f"{subject}关闭"
|
||||
if parameters.get("action") == "strength":
|
||||
strength = {1: "轻柔", 2: "中等", 3: "较强"}.get(parameters.get("strength"), "")
|
||||
return f"{subject}设为{strength}力度"
|
||||
mode = {
|
||||
"wave": "波浪", "pulse": "脉冲", "stretch": "舒展", "slack": "舒缓", "relax": "放松",
|
||||
}.get(str(parameters.get("mode") or ""), "")
|
||||
return f"{subject}设为{mode}模式"
|
||||
if set(parameters) == {"state"}:
|
||||
return f"{subject}{values[0]}"
|
||||
if capability == "roof.set_shade_position" and parameters.get("position_pct") in {0, 100}:
|
||||
@@ -1750,6 +1769,11 @@ class AssistantCapabilityCatalog:
|
||||
normalized = "".join(text.lower().split())
|
||||
if not normalized:
|
||||
return None
|
||||
# A creation request can contain both a device and the noun 功能模式.
|
||||
# That noun is not evidence that the user is asking about capabilities.
|
||||
# Use the guarded intent parser, so genuine questions remain answerable.
|
||||
if parse_mode_profile_create(text) is not None:
|
||||
return None
|
||||
catalog = snapshot or self.snapshot()
|
||||
unsupported_control = _unsupported_control_answer(
|
||||
_canonical_control_text(text),
|
||||
|
||||
@@ -25,6 +25,8 @@ from collections.abc import Set as AbstractSet
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from cognitive_agent.application.semantic_ir import resolve_scene_seat_scopes
|
||||
|
||||
MAX_PLAN_BYTES = 24 * 1024
|
||||
MAX_PLAN_REQUESTS = 12
|
||||
MAX_PLAN_PARAMETERS = 12
|
||||
@@ -39,6 +41,9 @@ SCENE_COMPOSABLE_CAPABILITIES = frozenset(
|
||||
"seat.set_legrest_extension",
|
||||
"seat.set_ventilation",
|
||||
"seat.set_heating",
|
||||
"seat.set_driver_ventilation_native",
|
||||
"seat.set_driver_heating_native",
|
||||
"seat.set_driver_massage_native",
|
||||
"climate.set_power",
|
||||
"climate.set_temperature",
|
||||
"climate.set_fan_level",
|
||||
@@ -1085,6 +1090,7 @@ def guard_assistant_plan(
|
||||
fallback_intents: Sequence[Mapping[str, Any]] = (),
|
||||
trusted_local: Mapping[str, Any] | None = None,
|
||||
allow_scene_composition: bool = False,
|
||||
scene_scope_utterance: str | None = None,
|
||||
) -> GuardedAssistantPlan:
|
||||
"""Authorize a Codex plan against the server-owned unresolved text.
|
||||
|
||||
@@ -1153,6 +1159,10 @@ def guard_assistant_plan(
|
||||
parser_claims.update(_intent_claims(intent))
|
||||
local_keys = _resolved_keys(trusted_local or {})
|
||||
non_execution = bool(_NON_EXECUTION_PATTERN.search(text))
|
||||
scene_seat_scopes = (
|
||||
resolve_scene_seat_scopes(text if scene_scope_utterance is None else scene_scope_utterance)
|
||||
if allow_scene_composition else None
|
||||
)
|
||||
|
||||
accepted: list[dict[str, Any]] = []
|
||||
rejected: list[dict[str, str]] = []
|
||||
@@ -1204,6 +1214,24 @@ def guard_assistant_plan(
|
||||
}
|
||||
)
|
||||
continue
|
||||
if (
|
||||
allow_scene_composition
|
||||
and domain == "seat"
|
||||
and scene_seat_scopes is not None
|
||||
and (
|
||||
"unsupported" in scene_seat_scopes
|
||||
or {"front_left": "driver", "front_right": "passenger"}.get(
|
||||
target_scope, target_scope
|
||||
) not in scene_seat_scopes
|
||||
)
|
||||
):
|
||||
rejected.append(
|
||||
{
|
||||
"field": f"requests[{index}].target_scope",
|
||||
"question": "场景座椅动作超出了用户明确指定的座位范围,已拒绝生成该功能模式。",
|
||||
}
|
||||
)
|
||||
continue
|
||||
request_text = _relevant_text(capability, text)
|
||||
if non_execution or (
|
||||
not allow_scene_composition
|
||||
|
||||
@@ -34,6 +34,9 @@ _MODE_PROFILE_PURPOSE_WRAPPERS = (
|
||||
r"^(?:可以|能够|能)?让(?:我们|我|人们?|乘员们?|乘客们?|大家)"
|
||||
r"(?P<purpose>.+?)的(?:功能|场景|自定义)?模式$"
|
||||
),
|
||||
re.compile(
|
||||
r"^(?P<purpose>(?:座椅|座位|乘坐|坐着).+?)的(?:功能|场景|自定义)?模式$"
|
||||
),
|
||||
)
|
||||
_MODE_PROFILE_PURPOSE_INVALID = frozenset(
|
||||
{"", "功能", "场景", "自定义", "使用", "日常使用", "合适", "需要"}
|
||||
|
||||
@@ -14,7 +14,7 @@ from cognitive_agent.application.assistant_plan_guard import (
|
||||
MAX_PLAN_REQUESTS,
|
||||
)
|
||||
|
||||
SCENE_PLANNER_VERSION = "n7web-scene-composer/v2"
|
||||
SCENE_PLANNER_VERSION = "n7web-scene-composer/v3"
|
||||
|
||||
_MAX_OUTPUT_SCHEMA_BYTES = 64 * 1024
|
||||
_JSON_PARAMETER_TYPES = frozenset({"boolean", "integer", "number", "string"})
|
||||
@@ -46,6 +46,19 @@ For a commonly understood scene, infer conservative cabin settings. If the name
|
||||
meaningless, unsafe, contradictory, or cannot be grounded in the supplied catalog,
|
||||
return no requests and a short ambiguity. The application will independently check
|
||||
the complete live registry, parameters, compiler, safety policy, and dry-run result.
|
||||
|
||||
The primary user need must be represented in the actions. For sitting comfort,
|
||||
seating relaxation or rest, consider supported seat functions before auxiliary
|
||||
climate and lights; climate and lights alone do not fulfil a seating request.
|
||||
For an unqualified front-seat comfort scene, consider both driver and passenger
|
||||
using each seat's own listed capabilities. Respect an explicitly named seat.
|
||||
Driver heating/ventilation native levels use 2 for low and 1 for high; passenger
|
||||
levels use 1 for low. Driver massage supports action=mode with one listed mode,
|
||||
action=strength with strength, or action=off. Do not mix these action parameters.
|
||||
Driver absolute axis positions, unlearned memory slots and passenger massage must
|
||||
not be invented. Do not use timed axis movement as a substitute for an absolute
|
||||
posture; leave unsupported axes unchanged. If the primary requested seat function
|
||||
is unavailable, return an ambiguity rather than an unrelated cabin-only scene.
|
||||
Return only the JSON object required by the output schema.
|
||||
"""
|
||||
|
||||
@@ -179,6 +192,29 @@ def _capability_schema_branch(capability: Mapping[str, object]) -> dict[str, obj
|
||||
}
|
||||
for names in parameter_combinations
|
||||
]
|
||||
if capability_id == "seat.set_driver_massage_native":
|
||||
# The native command discriminates mode, strength and off. Its YAML
|
||||
# scalar contracts cannot express that dependency on their own.
|
||||
parameter_object_branches = []
|
||||
action_schema = value_schemas.get("action", {})
|
||||
for action, dependent_name in (("off", None), ("mode", "mode"), ("strength", "strength")):
|
||||
if action not in action_schema.get("enum", []):
|
||||
continue
|
||||
if dependent_name is not None and dependent_name not in value_schemas:
|
||||
continue
|
||||
properties = {"action": {"type": "string", "enum": [action]}}
|
||||
if dependent_name is not None:
|
||||
properties[dependent_name] = value_schemas[dependent_name]
|
||||
parameter_object_branches.append(
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": False,
|
||||
"required": list(properties),
|
||||
"properties": properties,
|
||||
}
|
||||
)
|
||||
if not parameter_object_branches:
|
||||
return None
|
||||
return {
|
||||
"type": "object",
|
||||
"additionalProperties": False,
|
||||
|
||||
@@ -2195,6 +2195,7 @@ class SeatDemoService:
|
||||
assistant_plan: dict[str, Any] | None = None,
|
||||
allow_fuzzy_mode_profile_match: bool = True,
|
||||
allow_scene_composition: bool = False,
|
||||
scene_scope_utterance: str | None = None,
|
||||
mode_profile_draft_context: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
control_transport = self._validate_control_transport(control_transport)
|
||||
@@ -2526,8 +2527,12 @@ class SeatDemoService:
|
||||
model_parse_source=model_parse_source,
|
||||
assistant_plan=assistant_plan,
|
||||
allow_scene_composition=allow_scene_composition,
|
||||
scene_scope_utterance=scene_scope_utterance,
|
||||
)
|
||||
scene_label_only = assistant_plan is not None and allow_scene_composition
|
||||
dynamic_mode_stop = (
|
||||
not scene_label_only and self._is_dynamic_mode_stop_request(utterance)
|
||||
)
|
||||
dynamic_mode_stop = self._is_dynamic_mode_stop_request(utterance)
|
||||
if dynamic_mode_stop:
|
||||
parsed = self._apply_dynamic_mode_stop_request(parsed, utterance)
|
||||
# Reuse the same conservative clause segmentation for the final
|
||||
@@ -2535,7 +2540,12 @@ class SeatDemoService:
|
||||
# comma between independent controls can leak an early target
|
||||
# (for example ``副驾``) into a later HVAC clause and can also
|
||||
# hide otherwise valid window/temperature actions.
|
||||
segmented_utterance = ",".join(
|
||||
# Name-only scene plans have already been checked against exact
|
||||
# targets by _interpret. Their synthesized 进入<name> label is not
|
||||
# another atomic instruction: reparsing 座椅舒适模式 here applies
|
||||
# the unscoped-seat driver default and discards valid passenger
|
||||
# actions. Explicit user instructions never enter this branch.
|
||||
segmented_utterance = "" if scene_label_only else ",".join(
|
||||
self._split_intent_clauses(utterance)
|
||||
)
|
||||
relative_resolution = (
|
||||
@@ -2617,7 +2627,12 @@ class SeatDemoService:
|
||||
parsed,
|
||||
excluded_control_signatures,
|
||||
)
|
||||
parsed = apply_language_safety_guards(parsed, utterance)
|
||||
parsed = apply_language_safety_guards(
|
||||
parsed,
|
||||
utterance,
|
||||
scene_composition=assistant_plan is not None and allow_scene_composition,
|
||||
scene_scope_utterance=scene_scope_utterance,
|
||||
)
|
||||
parsed = self._partition_native_actions(
|
||||
parsed,
|
||||
promote_supported=(
|
||||
@@ -5474,6 +5489,11 @@ class SeatDemoService:
|
||||
and source_profile is None
|
||||
and not requested_instruction
|
||||
),
|
||||
# A revision combines a server-loaded draft with changes that
|
||||
# already passed the ordinary text/target authorization path.
|
||||
# Its old display name cannot veto an explicitly requested new
|
||||
# seat. This override is internal, never accepted from payloads.
|
||||
scene_scope_utterance="" if source_draft is not None else None,
|
||||
)
|
||||
except NativeCanCompileError:
|
||||
# Repository modes and model-composed scenes share the same native
|
||||
@@ -7500,6 +7520,7 @@ class SeatDemoService:
|
||||
model_parse_source: str = "cloud_model",
|
||||
assistant_plan: dict[str, Any] | None = None,
|
||||
allow_scene_composition: bool = False,
|
||||
scene_scope_utterance: str | None = None,
|
||||
) -> tuple[dict[str, Any], str, str | None]:
|
||||
vehicle_profile = vehicle_profile or self.vehicle_profiles.default()
|
||||
if assistant_plan is not None and allow_scene_composition:
|
||||
@@ -7561,6 +7582,7 @@ class SeatDemoService:
|
||||
],
|
||||
trusted_local=trusted_local,
|
||||
allow_scene_composition=allow_scene_composition,
|
||||
scene_scope_utterance=scene_scope_utterance,
|
||||
)
|
||||
model = self.model_plan_adapter.adapt(
|
||||
guarded.plan,
|
||||
|
||||
@@ -71,6 +71,28 @@ _SEAT_UNSUPPORTED_SCOPE = re.compile(
|
||||
r"左后|右后|前舱|后舱|左侧|右侧|左边|右边|"
|
||||
r"所有座椅|全部座椅|全车座椅"
|
||||
)
|
||||
_SCENE_NON_SEAT_SCOPED_DEVICE = re.compile(
|
||||
rf"(?:{_SEAT_DRIVER_SCOPE.pattern}|{_SEAT_PASSENGER_SCOPE.pattern}|"
|
||||
rf"{_SEAT_UNSUPPORTED_SCOPE.pattern})\s*(?:的)?\s*"
|
||||
r"(?=空调|温度|风量|出风|风口|除霜|车窗|窗户|车门|门锁|"
|
||||
r"阅读灯|氛围灯|灯光|遮阳帘|天幕)",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def resolve_scene_seat_scopes(utterance: str) -> frozenset[str] | None:
|
||||
"""Keep a scene's explicit seat scope without borrowing an HVAC/light zone.
|
||||
|
||||
A name such as 主驾舒适模式 still constrains seat actions even when it
|
||||
omits 座椅. But 副驾空调与座椅舒适模式 does not authorize restricting
|
||||
every seat action to the passenger. Only remove scope tokens directly
|
||||
attached to another device; all negation text remains untouched.
|
||||
"""
|
||||
|
||||
scoped_text = _SCENE_NON_SEAT_SCOPED_DEVICE.sub(
|
||||
"", utterance.replace("主副驾", "主驾和副驾")
|
||||
)
|
||||
return resolve_explicit_seat_scopes(scoped_text, assume_seat_context=True)
|
||||
|
||||
|
||||
def resolve_explicit_seat_scopes(
|
||||
@@ -356,7 +378,13 @@ def _remove_conflicting_controls(
|
||||
result[field] = kept
|
||||
|
||||
|
||||
def apply_language_safety_guards(parsed: dict[str, Any], utterance: str) -> dict[str, Any]:
|
||||
def apply_language_safety_guards(
|
||||
parsed: dict[str, Any],
|
||||
utterance: str,
|
||||
*,
|
||||
scene_composition: bool = False,
|
||||
scene_scope_utterance: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Preserve negation and unresolved control language without producing CAN."""
|
||||
|
||||
result = deepcopy(parsed)
|
||||
@@ -386,7 +414,25 @@ def apply_language_safety_guards(parsed: dict[str, Any], utterance: str) -> dict
|
||||
]
|
||||
result["confidence"] = min(float(result.get("confidence", 0)), 0.8)
|
||||
|
||||
explicit_seat_scopes = resolve_explicit_seat_scopes(utterance)
|
||||
explicit_seat_scopes = (
|
||||
resolve_scene_seat_scopes(
|
||||
utterance if scene_scope_utterance is None else scene_scope_utterance
|
||||
)
|
||||
if scene_composition
|
||||
else resolve_explicit_seat_scopes(utterance)
|
||||
)
|
||||
if scene_composition and not any(
|
||||
isinstance(item, dict)
|
||||
and (
|
||||
item.get("domain") == "seat"
|
||||
or str(item.get("capability", "")).startswith("seat.")
|
||||
)
|
||||
for key in _EXECUTION_LIST_FIELDS
|
||||
for item in result.get(key, [])
|
||||
):
|
||||
# A rear-window ventilation scene is not a rear-seat request. The
|
||||
# scene guard already checks every seat candidate before adaptation.
|
||||
explicit_seat_scopes = None
|
||||
if explicit_seat_scopes is not None:
|
||||
removed = _filter_explicit_seat_scope(result, explicit_seat_scopes)
|
||||
if removed:
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
const assert = require("node:assert/strict");
|
||||
const path = require("node:path");
|
||||
const { chromium } = require("playwright");
|
||||
|
||||
const [baseURL, evidenceDir] = process.argv.slice(2);
|
||||
const desktop = { width: 1600, height: 1000 };
|
||||
|
||||
async function main() {
|
||||
const browser = await chromium.launch({
|
||||
headless: true,
|
||||
...(process.env.N7WEB_BROWSER_CHANNEL
|
||||
? { channel: process.env.N7WEB_BROWSER_CHANNEL }
|
||||
: process.platform === "win32" ? { channel: "msedge" } : {}),
|
||||
});
|
||||
const context = await browser.newContext({ viewport: desktop, serviceWorkers: "block" });
|
||||
const page = await context.newPage();
|
||||
const requests = [];
|
||||
const errors = [];
|
||||
const unexpectedWrites = [];
|
||||
let heldResponse = null;
|
||||
let nextReply = "success";
|
||||
const answer = (success) => [
|
||||
{ type: "action.completed", success, payload: {
|
||||
status: success ? "succeeded" : "failed", execution_mode: "dry_run",
|
||||
} },
|
||||
{ type: "answer.completed", answer: success ? "模式执行测试完成" : "模式执行测试失败" },
|
||||
].map((item) => JSON.stringify(item)).join("\n") + "\n";
|
||||
const fulfill = (route, success = true) => route.fulfill({
|
||||
status: 200, contentType: "application/x-ndjson", body: answer(success),
|
||||
});
|
||||
page.on("pageerror", (error) => errors.push(error.message));
|
||||
page.on("console", (message) => {
|
||||
if (message.type() === "error") errors.push(`${message.text()} ${message.location().url || ''}`);
|
||||
});
|
||||
async function handleApiRoute(route) {
|
||||
const pathname = new URL(route.request().url()).pathname;
|
||||
if (pathname === '/api/hardware/seat-state') {
|
||||
// This legacy GET may attempt automatic firmware recovery. Never touch
|
||||
// the physical gateway while verifying browser interactions.
|
||||
await route.fulfill({json:{status:'unavailable',hardware_execution_allowed:false}});
|
||||
return;
|
||||
}
|
||||
// The isolated server deliberately does not launch Codex or publish a
|
||||
// physical display mirror. Keep these background integration probes local.
|
||||
if (pathname === '/api/health' || pathname === '/api/extension/display-sync') {
|
||||
await route.fulfill({json:{status:'ok',assistant:{status:'ready',available:true}}});
|
||||
return;
|
||||
}
|
||||
if (pathname === "/api/assistant/stream") {
|
||||
requests.push(route.request().postDataJSON());
|
||||
if (nextReply === "hold") { heldResponse = route; return; }
|
||||
const success = nextReply !== "fail";
|
||||
nextReply = "success";
|
||||
await fulfill(route, success);
|
||||
return;
|
||||
}
|
||||
if (route.request().method() !== "GET") {
|
||||
unexpectedWrites.push(route.request().url());
|
||||
await route.fulfill({ status: 403, body: "test forbids vehicle writes" });
|
||||
return;
|
||||
}
|
||||
await route.continue();
|
||||
}
|
||||
await context.route("**/api/**", handleApiRoute);
|
||||
const rail = page.locator(".mode-chips");
|
||||
const office = rail.locator('button[data-mode-profile-id="office"]');
|
||||
const waitIdle = () => page.waitForFunction(() => !window.CognitiveAgentAssistant.isPending());
|
||||
const waitState = (id, state) => page.waitForFunction(
|
||||
({ id, state }) => document.querySelector(`.mode-chips [data-mode-profile-id="${id}"]`)?.dataset.modeState === state,
|
||||
{ id, state },
|
||||
);
|
||||
async function expectOneCommand(action, command) {
|
||||
const before = requests.length;
|
||||
await action();
|
||||
await page.waitForFunction(() => document.querySelector('#utterance').getAttribute('aria-busy') === 'false');
|
||||
await waitIdle();
|
||||
assert.equal(requests.length, before + 1, "gesture must submit exactly once");
|
||||
assert.equal(requests.at(-1).message, command);
|
||||
}
|
||||
try {
|
||||
await page.goto(baseURL, { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.querySelectorAll('.mode-chips button').length > 3);
|
||||
await page.waitForFunction(() => document.body.classList.contains('assistant-chat-enabled'));
|
||||
assert.equal(await page.title(), "511工程-智能座舱");
|
||||
assert.equal(new URL(page.url()).pathname, "/");
|
||||
assert.ok((await page.locator('body').innerText()).includes("整车孪生界面"));
|
||||
assert.ok(await rail.evaluate((element) => element.scrollWidth > element.clientWidth));
|
||||
|
||||
// This is a real mouse sequence: pointerdown -> pointerup -> click. The
|
||||
// prior pointer capture bug cannot be detected by button.dispatchEvent().
|
||||
await page.evaluate(() => window.CognitiveAgentAssistant.close());
|
||||
await expectOneCommand(() => office.click(), "进入办公模式");
|
||||
await waitState("office", "succeeded");
|
||||
assert.equal(await page.locator('#assistantDrawerToggle').getAttribute('aria-expanded'), 'true');
|
||||
assert.ok((await page.locator('#assistantMessages').innerText()).includes('进入办公模式'));
|
||||
|
||||
// A small physical hand movement is still a click.
|
||||
const box = await office.boundingBox();
|
||||
await expectOneCommand(async () => {
|
||||
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(box.x + box.width / 2 + 2, box.y + box.height / 2);
|
||||
await page.mouse.up();
|
||||
}, "进入办公模式");
|
||||
|
||||
// Dragging from a card must scroll, without accidentally running it.
|
||||
const beforeDrag = requests.length;
|
||||
const startScroll = await rail.evaluate((element) => element.scrollLeft);
|
||||
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(box.x + box.width / 2 - 90, box.y + box.height / 2, { steps: 8 });
|
||||
await page.mouse.up();
|
||||
assert.ok(await rail.evaluate((element) => element.scrollLeft) > startScroll);
|
||||
assert.equal(requests.length, beforeDrag);
|
||||
await rail.evaluate((element) => { element.scrollLeft = 0; });
|
||||
await expectOneCommand(() => office.click(), "进入办公模式");
|
||||
|
||||
// Keyboard activation and management cards share the same submission path.
|
||||
await office.focus();
|
||||
await expectOneCommand(() => page.keyboard.press('Enter'), "进入办公模式");
|
||||
await office.focus();
|
||||
await expectOneCommand(() => page.keyboard.press('Space'), "进入办公模式");
|
||||
await page.locator('#modeGalleryToggle').click();
|
||||
await expectOneCommand(() => page.locator('#modeGalleryGroups button[data-mode-profile-id="nap"]').click(), "进入小憩模式");
|
||||
await waitState("nap", "succeeded");
|
||||
|
||||
// Busy requests are not duplicated, nor does another card cancel them.
|
||||
nextReply = 'hold';
|
||||
const beforeBusy = requests.length;
|
||||
await office.click();
|
||||
await waitState('office', 'running');
|
||||
await office.click();
|
||||
assert.equal(requests.length, beforeBusy + 1);
|
||||
assert.equal(await page.evaluate(() => window.CognitiveAgentAssistant.isPending()), true);
|
||||
await fulfill(heldResponse);
|
||||
nextReply = 'success';
|
||||
await waitIdle();
|
||||
await waitState('office', 'succeeded');
|
||||
nextReply = 'fail';
|
||||
await expectOneCommand(() => office.click(), "进入办公模式");
|
||||
await waitState('office', 'failed');
|
||||
|
||||
// A catalog with few visible cards must also work without overflow.
|
||||
await rail.evaluate(element => {
|
||||
for (const child of element.children) {
|
||||
if (child.dataset.modeProfileId !== 'office') child.style.display = 'none';
|
||||
}
|
||||
});
|
||||
await page.waitForFunction(() => {const e=document.querySelector('.mode-chips');return e.scrollWidth <= e.clientWidth;});
|
||||
await expectOneCommand(() => office.click(), "进入办公模式");
|
||||
await rail.evaluate(element => {
|
||||
for (const child of element.children) child.style.removeProperty('display');
|
||||
});
|
||||
await page.setViewportSize(desktop);
|
||||
await page.screenshot({path:path.join(evidenceDir,'mode-rail-desktop.png')});
|
||||
assert.deepEqual(errors, []);
|
||||
assert.deepEqual(unexpectedWrites, []);
|
||||
|
||||
// Touch input uses native scrolling and also submits once when tapped.
|
||||
const touchContext = await browser.newContext({viewport:{width:720,height:1000},hasTouch:true,serviceWorkers:'block'});
|
||||
await touchContext.route('**/api/**', handleApiRoute);
|
||||
const touchPage = await touchContext.newPage();
|
||||
await touchPage.goto(baseURL, {waitUntil:'domcontentloaded'});
|
||||
await touchPage.waitForFunction(() => document.querySelectorAll('.mode-chips button').length > 3);
|
||||
const beforeTouch = requests.length;
|
||||
await touchPage.locator('.mode-chips button[data-mode-profile-id="office"]').tap();
|
||||
await touchPage.waitForFunction(() => document.querySelector('.mode-chips [data-mode-profile-id="office"]').dataset.modeState === 'succeeded');
|
||||
assert.equal(requests.length,beforeTouch+1);
|
||||
assert.equal(requests.at(-1).message,'进入办公模式');
|
||||
await touchPage.screenshot({path:path.join(evidenceDir,'mode-rail-touch.png')});
|
||||
await touchContext.close();
|
||||
console.log('PASS: click, jitter, drag, Enter, Space, gallery, busy, failure, resize, touch; no runtime errors');
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(error => { console.error(error); process.exitCode = 1; });
|
||||
@@ -0,0 +1,86 @@
|
||||
"""Exercise real pointer/click dispatch on the complete console in Chromium."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import threading
|
||||
from http import HTTPStatus
|
||||
from http.server import ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from cognitive_agent.application.seat_service import SeatDemoService
|
||||
from cognitive_agent.auth import AuthConfig, AuthRuntime
|
||||
from cognitive_agent.server import REPOSITORY_ROOT, SeatDemoHandler
|
||||
|
||||
|
||||
def test_mode_rail_real_browser_interactions(tmp_path: Path) -> None:
|
||||
node = shutil.which("node")
|
||||
if not node:
|
||||
pytest.skip("Node.js is required for the browser interaction regression")
|
||||
env = dict(os.environ)
|
||||
bundled_modules = Path(node).resolve().parent.parent / "node_modules"
|
||||
if bundled_modules.is_dir():
|
||||
env["NODE_PATH"] = os.pathsep.join(
|
||||
item for item in (str(bundled_modules), env.get("NODE_PATH")) if item
|
||||
)
|
||||
available = subprocess.run(
|
||||
[node, "-e", "require.resolve('playwright')"],
|
||||
env=env,
|
||||
capture_output=True,
|
||||
encoding="utf-8",
|
||||
check=False,
|
||||
)
|
||||
if available.returncode:
|
||||
pytest.skip("Playwright is required for the real browser regression")
|
||||
with patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"COGNITIVE_AGENT_HARDWARE_EXECUTION_ENABLED": "false",
|
||||
"COGNITIVE_AGENT_VEHICLE_CAN_ENABLED": "false",
|
||||
"COGNITIVE_AGENT_WIRED_PASSENGER_CAN_ENABLED": "false",
|
||||
"COGNITIVE_AGENT_VEHICLE_MQTT_ENABLED": "false",
|
||||
},
|
||||
):
|
||||
service = SeatDemoService(REPOSITORY_ROOT)
|
||||
service.artifact_root = tmp_path / "artifacts"
|
||||
|
||||
class BrowserHandler(SeatDemoHandler):
|
||||
auth = AuthRuntime(AuthConfig(False, "", "", b"", 3600, 5, 300, 120, False))
|
||||
|
||||
def do_POST(self) -> None: # noqa: N802
|
||||
# Only GET reaches this test server. Browser requests to the
|
||||
# assistant are intercepted; unexpected writes fail closed.
|
||||
self._json({"error": "browser_test_write_forbidden"}, HTTPStatus.FORBIDDEN)
|
||||
|
||||
def log_message(self, *_args: object) -> None:
|
||||
pass
|
||||
|
||||
BrowserHandler.service = service
|
||||
server = ThreadingHTTPServer(("127.0.0.1", 0), BrowserHandler)
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
try:
|
||||
completed = subprocess.run(
|
||||
[
|
||||
node,
|
||||
str(Path(__file__).with_name("mode_rail_browser.cjs")),
|
||||
f"http://127.0.0.1:{server.server_port}",
|
||||
str(tmp_path),
|
||||
],
|
||||
env=env,
|
||||
capture_output=True,
|
||||
encoding="utf-8",
|
||||
timeout=120,
|
||||
check=False,
|
||||
)
|
||||
assert completed.returncode == 0, completed.stdout + completed.stderr
|
||||
finally:
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
thread.join(timeout=3)
|
||||
service.close()
|
||||
@@ -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-130", timeout=3) as response:
|
||||
with self.opener.open(f"{self.base_url}/app.js?v=dual-seat-131", 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
|
||||
@@ -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-26", timeout=3
|
||||
f"{self.base_url}/assistant-chat.js?v=codex-assistant-27", timeout=3
|
||||
) as response:
|
||||
assistant_script = response.read().decode("utf-8")
|
||||
with self.opener.open(
|
||||
@@ -697,7 +697,7 @@ 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-130"', html)
|
||||
self.assertIn('src="/app.js?v=dual-seat-131"', 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)
|
||||
@@ -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-26"', html)
|
||||
self.assertIn('src="/assistant-chat.js?v=codex-assistant-26"', html)
|
||||
self.assertIn('href="/assistant-chat.css?v=codex-assistant-27"', html)
|
||||
self.assertIn('src="/assistant-chat.js?v=codex-assistant-27"', 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)
|
||||
@@ -992,14 +992,14 @@ 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-v277"', service_worker)
|
||||
self.assertIn('const CACHE_VERSION = "cognitive-agent-pad-v278"', 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-26', service_worker)
|
||||
self.assertIn('/assistant-chat.js?v=codex-assistant-26', service_worker)
|
||||
self.assertIn('/assistant-chat.css?v=codex-assistant-27', service_worker)
|
||||
self.assertIn('/assistant-chat.js?v=codex-assistant-27', 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-130"', service_worker)
|
||||
self.assertIn('"/app.js?v=dual-seat-131"', 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)
|
||||
@@ -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-130", timeout=3
|
||||
f"{self.base_url}/app.js?v=dual-seat-131", timeout=3
|
||||
) as response:
|
||||
app_script = response.read().decode("utf-8")
|
||||
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from cognitive_agent.application.assistant_capabilities import AssistantCapabilityCatalog
|
||||
from cognitive_agent.application.mode_profile import ModeProfileRegistry
|
||||
from cognitive_agent.application.scene_composer import normalize_scene_output
|
||||
from cognitive_agent.application.seat_service import REPOSITORY_ROOT, SeatDemoService
|
||||
from cognitive_agent.application.semantic_ir import resolve_scene_seat_scopes
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def scene_service(tmp_path_factory):
|
||||
service = SeatDemoService()
|
||||
service.artifact_root = tmp_path_factory.mktemp("scene-explicit-seat-scope")
|
||||
service.mode_profiles = ModeProfileRegistry(
|
||||
REPOSITORY_ROOT / "config" / "modes",
|
||||
user_root=tmp_path_factory.mktemp("scene-explicit-seat-user-modes"),
|
||||
)
|
||||
try:
|
||||
yield service, AssistantCapabilityCatalog(service).scene_composition_context()
|
||||
finally:
|
||||
service.close()
|
||||
|
||||
|
||||
def _plan(context, scopes):
|
||||
requests = []
|
||||
if "driver" in scopes:
|
||||
requests.append({
|
||||
"capability": "seat.set_driver_massage_native",
|
||||
"target_scope": "driver",
|
||||
"parameters": {"action": "mode", "mode": "relax"},
|
||||
})
|
||||
if "passenger" in scopes:
|
||||
requests.append({
|
||||
"capability": "seat.set_backrest_recline",
|
||||
"target_scope": "passenger",
|
||||
"parameters": {"position_pct": 45},
|
||||
})
|
||||
requests.append({
|
||||
"capability": "climate.set_temperature",
|
||||
"target_scope": "vehicle",
|
||||
"parameters": {"temperature_c": 24},
|
||||
})
|
||||
return normalize_scene_output({
|
||||
"requests": requests, "ambiguities": [], "confidence": 0.95,
|
||||
}, context)
|
||||
|
||||
|
||||
def _preview(
|
||||
scene_service, message, scopes=("driver", "passenger"), *, execution_mode="hardware",
|
||||
):
|
||||
service, context = scene_service
|
||||
with patch.object(
|
||||
service.hardware_execution, "reserve",
|
||||
side_effect=AssertionError("scene scope validation must not execute hardware"),
|
||||
):
|
||||
return service.preview_native_can(
|
||||
message,
|
||||
provider="local",
|
||||
conversation_id="conv-explicit-scene-seat-scope",
|
||||
execution_mode=execution_mode,
|
||||
vehicle_id="denza_n7",
|
||||
assistant_plan=_plan(context, scopes),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("message", (
|
||||
"构建一个主驾座椅舒适模式",
|
||||
"创建一个仅主驾舒适模式",
|
||||
"创建一个只给驾驶员坐着放松的功能模式",
|
||||
"构建一个副驾座椅舒适模式",
|
||||
"构建一个仅副驾舒适模式",
|
||||
"构建一个右前座椅舒适模式",
|
||||
"构建一个后排座椅舒适模式",
|
||||
"构建一个主驾座椅与副驾空调舒适模式",
|
||||
"构建一个主驾阅读灯与副驾座椅舒适模式",
|
||||
))
|
||||
def test_explicit_scene_seat_scope_rejects_cross_seat_plan_atomically(scene_service, message):
|
||||
result = _preview(scene_service, message)
|
||||
assert result["status"] != "succeeded"
|
||||
assert result.get("mode_profile_draft") is None
|
||||
assert result["hardware_execution_allowed"] is False
|
||||
assert result.get("command_plan") is None
|
||||
assert result.get("command_plans") == {}
|
||||
intent = result.get("intent_ir") or {}
|
||||
assert (intent.get("spec") or {}).get("actions", []) == []
|
||||
assert any("座位范围" in item["message"] for item in result["validation"])
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("message", "scopes"), (
|
||||
("构建一个主驾座椅舒适模式", ("driver",)),
|
||||
("构建一个副驾座椅舒适模式", ("passenger",)),
|
||||
("创建一个仅主驾舒适模式", ("driver",)),
|
||||
("构建一个主驾和副驾座椅舒适模式", ("driver", "passenger")),
|
||||
("构建一个主副驾座椅舒适模式", ("driver", "passenger")),
|
||||
("构建一个云端座椅舒适模式", ("driver", "passenger")),
|
||||
("构建一个副驾空调与座椅舒适模式", ("driver", "passenger")),
|
||||
("构建一个座椅舒适与副驾阅读灯模式", ("driver", "passenger")),
|
||||
("构建一个主驾座椅与副驾空调舒适模式", ("driver",)),
|
||||
("构建一个主驾阅读灯与副驾座椅舒适模式", ("passenger",)),
|
||||
))
|
||||
def test_scene_scope_preserves_supported_targets_without_borrowing_other_device_zones(
|
||||
scene_service, message, scopes,
|
||||
):
|
||||
result = _preview(scene_service, message, scopes)
|
||||
assert result["status"] == "succeeded", result.get("validation")
|
||||
assert result["mode_profile_draft"]
|
||||
assert result["execution_mode"] == "dry_run"
|
||||
assert result["hardware_execution_allowed"] is False
|
||||
actions = result["intent_ir"]["spec"]["actions"]
|
||||
assert {
|
||||
action["target"]["instance"]
|
||||
for action in actions if action["domain"] == "seat"
|
||||
} == set(scopes)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("message", (
|
||||
"构建一个主驾舒适模式,副驾不要动",
|
||||
"构建一个主驾舒适模式,不要动副驾",
|
||||
"构建一个舒展模式,座椅靠背调整到45%",
|
||||
))
|
||||
def test_explicit_atomic_requirements_do_not_receive_scene_scope_relaxation(
|
||||
scene_service, message,
|
||||
):
|
||||
result = _preview(scene_service, message, execution_mode="dry_run")
|
||||
assert result["status"] != "succeeded"
|
||||
assert result.get("mode_profile_draft") is None
|
||||
assert result["hardware_execution_allowed"] is False
|
||||
|
||||
|
||||
def test_explicit_atomic_requirements_override_label_without_adding_model_actions(scene_service):
|
||||
result = _preview(scene_service, "构建一个主驾舒适模式,副驾靠背调整到45%")
|
||||
assert result["status"] == "succeeded"
|
||||
assert result["execution_mode"] == "dry_run"
|
||||
actions = result["intent_ir"]["spec"]["actions"]
|
||||
assert len(actions) == 1
|
||||
assert actions[0]["capability"] == "seat.set_backrest_recline"
|
||||
assert actions[0]["target"]["instance"] == "passenger"
|
||||
assert actions[0]["parameters"]["position_pct"] == 45
|
||||
|
||||
|
||||
def test_rear_window_scene_is_not_rejected_as_an_unsupported_seat(scene_service):
|
||||
service, context = scene_service
|
||||
plan = normalize_scene_output({
|
||||
"requests": [{
|
||||
"capability": "window.set_position",
|
||||
"target_scope": "rear_right",
|
||||
"parameters": {"position_pct": 45},
|
||||
}],
|
||||
"ambiguities": [], "confidence": 0.95,
|
||||
}, context)
|
||||
result = service.preview_native_can(
|
||||
"构建一个后排换气模式", provider="local",
|
||||
conversation_id="conv-rear-window-not-seat",
|
||||
execution_mode="dry_run", vehicle_id="denza_n7", assistant_plan=plan,
|
||||
)
|
||||
assert result["status"] == "succeeded"
|
||||
assert result["mode_profile_draft"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("text", "expected"), (
|
||||
("进入主驾舒适模式", frozenset({"driver"})),
|
||||
("进入仅副驾舒适模式", frozenset({"passenger"})),
|
||||
("进入主副驾座椅舒适模式", frozenset({"driver", "passenger"})),
|
||||
("进入主驾座椅与副驾的空调舒适模式", frozenset({"driver"})),
|
||||
("进入副驾座椅与主驾的阅读灯模式", frozenset({"passenger"})),
|
||||
("进入座椅舒适与右前车窗模式", None),
|
||||
("进入座椅舒适与后排阅读灯模式", None),
|
||||
("进入后排座椅舒适模式", frozenset({"unsupported"})),
|
||||
))
|
||||
def test_scene_scope_resolution_only_uses_seat_or_unqualified_scene_targets(text, expected):
|
||||
assert resolve_scene_seat_scopes(text) == expected
|
||||
|
||||
|
||||
def test_new_creation_cannot_borrow_draft_reference_to_bypass_explicit_scope(scene_service):
|
||||
service, context = scene_service
|
||||
built = _preview(scene_service, "构建一个主驾独享舒适模式", ("driver",))
|
||||
reference = {key: built["mode_profile_draft"][key] for key in (
|
||||
"name", "source_request_id", "vehicle_id",
|
||||
)}
|
||||
result = service.preview_native_can(
|
||||
"构建一个主驾独享舒适模式", provider="local",
|
||||
conversation_id="conv-explicit-scene-seat-scope", execution_mode="dry_run",
|
||||
vehicle_id="denza_n7", assistant_plan=_plan(context, ("driver", "passenger")),
|
||||
mode_profile_draft_context=reference,
|
||||
)
|
||||
assert result["status"] != "succeeded"
|
||||
assert not result.get("mode_profile_draft")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("initial_scope", "name", "change", "added_capability"), (
|
||||
("driver", "主驾释压舒适模式", "副驾座椅通风调整为1档", "seat.set_ventilation"),
|
||||
("passenger", "副驾释压舒适模式", "主驾座椅通风调整为低档", "seat.set_driver_ventilation_native"),
|
||||
))
|
||||
def test_explicit_revision_can_extend_old_label_scope_then_save_and_replay(
|
||||
scene_service, initial_scope, name, change, added_capability,
|
||||
):
|
||||
service, _context = scene_service
|
||||
built = _preview(scene_service, f"构建一个{name}", (initial_scope,))
|
||||
source_draft = built["mode_profile_draft"]
|
||||
reference = {key: source_draft[key] for key in (
|
||||
"name", "source_request_id", "vehicle_id",
|
||||
)}
|
||||
revised = service.preview_native_can(
|
||||
f"把{name}的{change}", provider="local",
|
||||
conversation_id="conv-explicit-scene-seat-scope", execution_mode="dry_run",
|
||||
vehicle_id="denza_n7", mode_profile_draft_context=reference,
|
||||
)
|
||||
assert revised["status"] == "succeeded", revised.get("validation")
|
||||
draft = revised["mode_profile_draft"]
|
||||
assert draft["generation_source"] == "draft_revision"
|
||||
assert draft["operation_count"] == source_draft["operation_count"] + 1
|
||||
actions = revised["intent_ir"]["spec"]["actions"]
|
||||
assert added_capability in {action["capability"] for action in actions}
|
||||
assert {
|
||||
action["target"]["instance"] for action in actions if action["domain"] == "seat"
|
||||
} == {"driver", "passenger"}
|
||||
saved = service.create_mode_profile_card(
|
||||
draft["name"], draft["instruction"], draft["source_request_id"], draft["vehicle_id"],
|
||||
)
|
||||
assert saved["status"] == "succeeded"
|
||||
replayed = service.execute_mode_profile_card(
|
||||
saved["mode_profile"]["id"], execution_mode="dry_run", vehicle_id="denza_n7",
|
||||
)
|
||||
assert replayed["status"] == "succeeded", replayed.get("validation")
|
||||
assert {
|
||||
(action["capability"], action["target"]["instance"])
|
||||
for action in replayed["intent_ir"]["spec"]["actions"] if action["domain"] == "seat"
|
||||
} == {
|
||||
(action["capability"], action["target"]["instance"])
|
||||
for action in actions if action["domain"] == "seat"
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import pytest
|
||||
from cognitive_agent.application.assistant_capabilities import AssistantCapabilityCatalog
|
||||
from cognitive_agent.application.codex_assistant import CodexAssistantService
|
||||
from cognitive_agent.application.mode_profile import ModeProfileRegistry
|
||||
from cognitive_agent.application.scene_composer import normalize_scene_output
|
||||
from cognitive_agent.application.seat_service import REPOSITORY_ROOT, SeatDemoService
|
||||
|
||||
|
||||
@@ -94,6 +95,7 @@ def _assistant_preview(
|
||||
client.available = False
|
||||
assistant = CodexAssistantService(client)
|
||||
snapshot = catalog.snapshot(vehicle_id="denza_n7")
|
||||
fast_answer = catalog.fast_answer(message, snapshot)
|
||||
events: list[dict[str, object]] = []
|
||||
raw_results: list[dict[str, object]] = []
|
||||
passed_plans: list[dict[str, object] | None] = []
|
||||
@@ -122,6 +124,7 @@ def _assistant_preview(
|
||||
conversation_id=conversation_id,
|
||||
principal="scene-template-hybrid-test",
|
||||
history=[],
|
||||
authoritative_answer=fast_answer.answer if fast_answer else None,
|
||||
emit=lambda event: events.append(event) or True,
|
||||
execute_control=execute_control,
|
||||
capability_snapshot=snapshot,
|
||||
@@ -141,7 +144,7 @@ def test_all_production_scene_templates_complete_the_full_local_dry_run(
|
||||
templates = snapshot["scene_templates"]
|
||||
|
||||
assert isinstance(templates, list)
|
||||
assert len(templates) == 12
|
||||
assert len(templates) == 13
|
||||
assert all(template["assistant_callable"] is True for template in templates)
|
||||
|
||||
with patch.object(
|
||||
@@ -339,6 +342,115 @@ def test_explicit_settings_on_a_purpose_scene_bypass_template_defaults(
|
||||
client.start_turn.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("message", "expected_name", "template_id"),
|
||||
(
|
||||
("构建一个座椅舒适的功能模式", "座椅舒适模式", "seat-comfort"),
|
||||
("构建一个让人坐着放松的功能模式", "坐着放松模式", "relaxation"),
|
||||
("请帮我创建一个适合乘坐舒适的模式吧", "乘坐舒适模式", "seat-comfort"),
|
||||
("帮我构建一个让我们坐着舒服的功能模式", "坐着舒服模式", "seat-comfort"),
|
||||
("创建一个座椅放松模式并预览", "座椅放松模式", "relaxation"),
|
||||
("构建一个长途舒适模式", "长途舒适模式", "long-trip-comfort"),
|
||||
),
|
||||
)
|
||||
def test_seating_comfort_requests_preview_both_seats_with_no_model_or_hardware_call(
|
||||
hybrid_service: tuple[SeatDemoService, AssistantCapabilityCatalog, Path],
|
||||
message: str,
|
||||
expected_name: str,
|
||||
template_id: str,
|
||||
) -> None:
|
||||
service, catalog, user_root = hybrid_service
|
||||
with patch.object(
|
||||
service.hardware_execution,
|
||||
"reserve",
|
||||
side_effect=AssertionError("seat comfort preview reached hardware execution"),
|
||||
):
|
||||
response, result, _events, client, _plans = _assistant_preview(
|
||||
service, catalog, message, conversation_id="conv-seat-comfort-preview",
|
||||
)
|
||||
assert response["route"] == "scene_template"
|
||||
assert response["success"] is True
|
||||
assert result["status"] == "succeeded"
|
||||
assert result["execution_mode"] == "dry_run"
|
||||
assert result["hardware_execution_allowed"] is False
|
||||
assert result["mode_profile_draft"]["name"] == expected_name
|
||||
assert result["mode_profile_draft"]["source_template_id"] == template_id
|
||||
seat_actions = {
|
||||
(capability, target): parameters
|
||||
for (capability, target), parameters in _action_map(result).items()
|
||||
if capability.startswith("seat.")
|
||||
}
|
||||
assert {target for _capability, target in seat_actions} == {"driver", "passenger"}
|
||||
assert all(
|
||||
"driver_axis" not in capability and "driver_position" not in capability
|
||||
for capability, _target in seat_actions
|
||||
)
|
||||
assert ("seat.set_driver_massage_native", "passenger") not in seat_actions
|
||||
if template_id in {"seat-comfort", "long-trip-comfort"}:
|
||||
assert seat_actions["seat.set_driver_ventilation_native", "driver"] == {
|
||||
"state": "on", "level": 2,
|
||||
}
|
||||
assert seat_actions["seat.set_ventilation", "passenger"] == {
|
||||
"state": "on", "level": 1,
|
||||
}
|
||||
client.start_structured_turn.assert_not_called()
|
||||
client.start_turn.assert_not_called()
|
||||
assert not list(user_root.rglob("*.yaml"))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", ("云端座椅舒展模式", "晚间乘坐释压模式"))
|
||||
def test_unregistered_seat_scene_keeps_both_explicit_targets_through_native_merge(
|
||||
hybrid_service: tuple[SeatDemoService, AssistantCapabilityCatalog, Path],
|
||||
name: str,
|
||||
) -> None:
|
||||
service, catalog, user_root = hybrid_service
|
||||
plan = normalize_scene_output({
|
||||
"requests": [
|
||||
{"capability": "seat.set_driver_massage_native", "target_scope": "driver",
|
||||
"parameters": {"action": "mode", "mode": "relax"}},
|
||||
{"capability": "seat.set_driver_heating_native", "target_scope": "driver",
|
||||
"parameters": {"state": "on", "level": 2}},
|
||||
{"capability": "seat.set_backrest_recline", "target_scope": "passenger",
|
||||
"parameters": {"position_pct": 45}},
|
||||
{"capability": "seat.set_heating", "target_scope": "passenger",
|
||||
"parameters": {"state": "on", "level": 1}},
|
||||
],
|
||||
"ambiguities": [], "confidence": 0.95,
|
||||
}, catalog.scene_composition_context())
|
||||
with patch.object(
|
||||
service.hardware_execution, "reserve",
|
||||
side_effect=AssertionError("scene composition reached hardware execution"),
|
||||
):
|
||||
result = service.preview_native_can(
|
||||
f"构建一个{name}", provider="local", conversation_id="conv-seat-generated",
|
||||
execution_mode="hardware", vehicle_id="denza_n7", assistant_plan=plan,
|
||||
)
|
||||
assert result["status"] == "succeeded"
|
||||
assert result["execution_mode"] == "dry_run"
|
||||
assert result["mode_profile_draft"]["generation_source"] == "codex_scene_composition"
|
||||
assert set(_action_map(result)) == {
|
||||
("seat.set_driver_massage_native", "driver"),
|
||||
("seat.set_driver_heating_native", "driver"),
|
||||
("seat.set_backrest_recline", "passenger"),
|
||||
("seat.set_heating", "passenger"),
|
||||
}
|
||||
assert not list(user_root.rglob("*.yaml"))
|
||||
|
||||
|
||||
def test_explicit_unscoped_seat_instruction_keeps_target_ambiguity_guard(
|
||||
hybrid_service: tuple[SeatDemoService, AssistantCapabilityCatalog, Path],
|
||||
) -> None:
|
||||
service, _catalog, _user_root = hybrid_service
|
||||
result = service.preview_native_can(
|
||||
"构建一个舒展模式,座椅靠背调整到45%", provider="local",
|
||||
conversation_id="conv-seat-explicit-target", execution_mode="hardware",
|
||||
vehicle_id="denza_n7",
|
||||
)
|
||||
assert result["status"] != "succeeded"
|
||||
assert result.get("mode_profile_draft") is None
|
||||
assert result["hardware_execution_allowed"] is False
|
||||
|
||||
|
||||
def test_template_metadata_cannot_authorize_modified_or_stale_template_actions(
|
||||
hybrid_service: tuple[SeatDemoService, AssistantCapabilityCatalog, Path],
|
||||
) -> None:
|
||||
|
||||
@@ -358,7 +358,7 @@ def test_common_scene_templates_are_read_only_callable_catalog_entries(
|
||||
templates = snapshot["scene_templates"]
|
||||
|
||||
assert snapshot["source_status"]["scene_templates"] == "ready"
|
||||
assert len(templates) == 12
|
||||
assert len(templates) == 13
|
||||
assert {item["name"] for item in templates} >= {
|
||||
"观影模式",
|
||||
"露营模式",
|
||||
|
||||
@@ -13,15 +13,15 @@ def test_console_contains_ai_assistant_surface():
|
||||
assert 'aria-label="AI助手对话"' in html
|
||||
assert 'placeholder="向 AI助手提问或交代工作"' in html
|
||||
assert 'aria-label="发送消息"' in html
|
||||
assert 'href="/assistant-chat.css?v=codex-assistant-26"' in html
|
||||
assert 'src="/assistant-chat.js?v=codex-assistant-26"' in html
|
||||
assert 'href="/assistant-chat.css?v=codex-assistant-27"' in html
|
||||
assert 'src="/assistant-chat.js?v=codex-assistant-27"' in html
|
||||
|
||||
|
||||
def test_codex_assistant_assets_are_versioned_together_in_the_service_worker():
|
||||
service_worker = (ROOT / "apps/web-console/service-worker.js").read_text(encoding="utf-8")
|
||||
assert 'const CACHE_VERSION = "cognitive-agent-pad-v277"' in service_worker
|
||||
assert '"/assistant-chat.css?v=codex-assistant-26"' in service_worker
|
||||
assert '"/assistant-chat.js?v=codex-assistant-26"' in service_worker
|
||||
assert 'const CACHE_VERSION = "cognitive-agent-pad-v278"' in service_worker
|
||||
assert '"/assistant-chat.css?v=codex-assistant-27"' in service_worker
|
||||
assert '"/assistant-chat.js?v=codex-assistant-27"' in service_worker
|
||||
|
||||
|
||||
def test_codex_frontend_uses_one_streaming_endpoint_and_agent_work_events():
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
"""Execute the mode preview formatter against actual seat/light parameter shapes."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def render_rows():
|
||||
node = shutil.which("node")
|
||||
if not node:
|
||||
runtime = (
|
||||
Path.home() / ".cache" / "codex-runtimes" / "codex-primary-runtime"
|
||||
/ "dependencies" / "node" / "bin"
|
||||
)
|
||||
node = next((str(path) for path in (runtime / "node.exe", runtime / "node") if path.is_file()), None)
|
||||
assert node, "Node.js is required to validate rendered mode action text"
|
||||
source = (ROOT / "apps" / "web-console" / "assistant-chat.js").read_text(encoding="utf-8")
|
||||
start = source.index(" function modeDraftRows(")
|
||||
end = source.index("\n function ", start + 1)
|
||||
script = source[start:end] + "\nconst input = JSON.parse(require('node:fs').readFileSync(0, 'utf8'));\n"
|
||||
script += "console.log(JSON.stringify(modeDraftRows(input.draft, input.actions)));"
|
||||
|
||||
def render(actions, instruction=""):
|
||||
completed = subprocess.run(
|
||||
[node, "-e", script],
|
||||
input=json.dumps({"draft": {"instruction": instruction}, "actions": actions}, ensure_ascii=False),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
check=True,
|
||||
)
|
||||
return json.loads(completed.stdout)
|
||||
|
||||
return render
|
||||
|
||||
|
||||
def action(capability, parameters, zone="driver"):
|
||||
return {
|
||||
"domain": capability.split(".")[0],
|
||||
"capability": capability,
|
||||
"target": {"zone": zone},
|
||||
"parameters": parameters,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("kind", ["ventilation", "heating"])
|
||||
@pytest.mark.parametrize(
|
||||
("parameters", "expected"),
|
||||
[
|
||||
({"state": "on", "level": 1}, "开启 · 高档"),
|
||||
({"state": "on", "level": 2}, "开启 · 低档"),
|
||||
({"state": "on"}, "开启 · 低档"),
|
||||
({"state": "off", "level": 1}, "关闭"),
|
||||
],
|
||||
)
|
||||
def test_driver_native_comfort_uses_its_own_level_scale(render_rows, kind, parameters, expected):
|
||||
rows = render_rows([action(f"seat.set_driver_{kind}_native", parameters, "front_left")])
|
||||
assert rows[0]["label"] == f"主驾座椅{'通风' if kind == 'ventilation' else '加热'}"
|
||||
assert rows[0]["value"] == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("kind", ["ventilation", "heating"])
|
||||
@pytest.mark.parametrize("level", [1, 2, 3, 4, 5])
|
||||
def test_passenger_levels_are_not_translated_using_driver_can_codes(render_rows, kind, level):
|
||||
rows = render_rows([action(f"seat.set_{kind}", {"state": "on", "level": level}, "passenger")])
|
||||
assert rows[0]["label"] == f"副驾座椅{'通风' if kind == 'ventilation' else '加热'}"
|
||||
assert rows[0]["value"] == f"开启 · {level}档"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("parameters", "expected"),
|
||||
[
|
||||
({"action": "off", "mode": "relax"}, "关闭"),
|
||||
({"action": "mode", "mode": "wave"}, "波浪模式"),
|
||||
({"action": "mode", "mode": "pulse"}, "脉冲模式"),
|
||||
({"action": "mode", "mode": "stretch"}, "舒展模式"),
|
||||
({"action": "mode", "mode": "slack"}, "舒缓模式"),
|
||||
({"action": "mode", "mode": "relax"}, "放松模式"),
|
||||
({"action": "strength", "strength": 1}, "轻柔力度"),
|
||||
({"action": "strength", "strength": 2}, "中等力度"),
|
||||
({"action": "strength", "strength": 3}, "较强力度"),
|
||||
({"action": "mode"}, "按摩模式待确认"),
|
||||
({"action": "strength"}, "按摩力度待确认"),
|
||||
],
|
||||
)
|
||||
def test_massage_preview_distinguishes_operation_mode_and_strength(render_rows, parameters, expected):
|
||||
rows = render_rows([action("seat.set_driver_massage_native", parameters)])
|
||||
assert rows[0]["label"] == "主驾座椅按摩"
|
||||
assert rows[0]["value"] == expected
|
||||
|
||||
|
||||
def test_ambient_native_color_and_brightness_remain_distinct_rows(render_rows):
|
||||
rows = render_rows([
|
||||
action("ambient_light.set_color_index", {"color_index": 127}, "vehicle"),
|
||||
action("ambient_light.set_brightness_level", {"brightness_level": 2}, "vehicle"),
|
||||
])
|
||||
assert [(row["label"], row["value"]) for row in rows] == [
|
||||
("氛围灯颜色", "色号 127"),
|
||||
("氛围灯亮度", "2档"),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("brightness", [0, 20, 100])
|
||||
def test_rgb_illumination_keeps_exact_color_and_brightness(render_rows, brightness):
|
||||
rows = render_rows([
|
||||
action("ambient_light.set_illumination", {"color_hex": "#FFB347", "brightness_pct": brightness}, "vehicle"),
|
||||
])
|
||||
assert rows[0]["value"] == f"颜色 #FFB347 · 亮度 {brightness}%"
|
||||
|
||||
|
||||
def test_zero_gravity_grouping_keeps_driver_comfort_visible(render_rows):
|
||||
rows = render_rows([
|
||||
action("seat.set_longitudinal_position", {"position_pct": 100}, "passenger"),
|
||||
action("seat.set_backrest_recline", {"position_pct": 81, "angle_deg": 150}, "front_right"),
|
||||
action("seat.set_driver_massage_native", {"action": "mode", "mode": "relax"}),
|
||||
], "副驾零重力模式,并开启主驾放松按摩")
|
||||
assert [(row["label"], row["value"]) for row in rows] == [
|
||||
("副驾座椅", "零重力模式"),
|
||||
("主驾座椅按摩", "放松模式"),
|
||||
]
|
||||
|
||||
|
||||
def test_position_and_power_rows_keep_existing_values(render_rows):
|
||||
rows = render_rows([
|
||||
action("seat.set_backrest_recline", {"position_pct": 81, "angle_deg": 150}, "passenger"),
|
||||
action("ambient_light.set_power", {"state": "off"}, "vehicle"),
|
||||
action("seat.set_ventilation", {"state": "off", "level": 2}, "passenger"),
|
||||
])
|
||||
assert [(row["label"], row["value"]) for row in rows] == [
|
||||
("副驾靠背角度", "81%(150°)"),
|
||||
("氛围灯", "关闭"),
|
||||
("副驾座椅通风", "关闭"),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("capability", "mode", "label", "value"),
|
||||
[
|
||||
("climate.set_control_mode", "auto", "空调控制模式", "自动控制"),
|
||||
("climate.set_control_mode", "manual", "空调控制模式", "手动控制"),
|
||||
("climate.set_cycle_mode", "internal", "空调循环", "内循环"),
|
||||
("climate.set_cycle_mode", "external", "空调循环", "外循环"),
|
||||
("climate.set_outlet_mode", "face", "空调出风模式", "吹面"),
|
||||
("climate.set_outlet_mode", "face_feet", "空调出风模式", "吹面 / 吹脚"),
|
||||
("climate.set_outlet_mode", "feet", "空调出风模式", "吹脚"),
|
||||
("climate.set_outlet_mode", "feet_defrost", "空调出风模式", "吹脚 / 除霜"),
|
||||
("climate.set_outlet_mode", "defrost", "空调出风模式", "除霜"),
|
||||
("climate.set_outlet_mode", "face_feet_defrost", "空调出风模式", "吹面 / 吹脚 / 除霜"),
|
||||
("climate.set_outlet_mode", "face_defrost", "空调出风模式", "吹面 / 除霜"),
|
||||
],
|
||||
)
|
||||
def test_climate_modes_use_capability_specific_labels(render_rows, capability, mode, label, value):
|
||||
rows = render_rows([action(capability, {"mode": mode}, "vehicle")])
|
||||
assert (rows[0]["label"], rows[0]["value"]) == (label, value)
|
||||
|
||||
|
||||
def test_climate_control_circulation_and_outlet_are_not_conflated(render_rows):
|
||||
rows = render_rows([
|
||||
action("climate.set_control_mode", {"mode": "auto"}, "vehicle"),
|
||||
action("climate.set_cycle_mode", {"mode": "internal"}, "vehicle"),
|
||||
action("climate.set_outlet_mode", {"mode": "face_feet"}, "vehicle"),
|
||||
action("climate.set_fan_level", {"level": 2}, "vehicle"),
|
||||
])
|
||||
assert [(row["label"], row["value"]) for row in rows] == [
|
||||
("空调控制模式", "自动控制"),
|
||||
("空调循环", "内循环"),
|
||||
("空调出风模式", "吹面 / 吹脚"),
|
||||
("空调风量", "2档"),
|
||||
]
|
||||
@@ -35,10 +35,10 @@ def test_energy_monitor_assets_are_versioned_and_cached() -> None:
|
||||
):
|
||||
assert asset in html
|
||||
assert f'"{asset}"' in service_worker
|
||||
assert html.index("/assistant-chat.js?v=codex-assistant-26") < html.index(
|
||||
assert html.index("/assistant-chat.js?v=codex-assistant-27") < html.index(
|
||||
"/energy-monitor.js?v=energy-monitor-4"
|
||||
)
|
||||
assert 'const CACHE_VERSION = "cognitive-agent-pad-v277"' in service_worker
|
||||
assert 'const CACHE_VERSION = "cognitive-agent-pad-v278"' in service_worker
|
||||
|
||||
|
||||
def test_energy_monitor_uses_the_fifth_shared_dock_icon_and_starts_closed() -> None:
|
||||
|
||||
@@ -47,7 +47,7 @@ def test_theme_asset_versions_are_consistent_with_the_pwa_shell() -> None:
|
||||
):
|
||||
assert asset in html
|
||||
assert f'"{asset}"' in service_worker
|
||||
assert 'const CACHE_VERSION = "cognitive-agent-pad-v277"' in service_worker
|
||||
assert 'const CACHE_VERSION = "cognitive-agent-pad-v278"' in service_worker
|
||||
|
||||
|
||||
def test_vehicle_twin_icons_and_nested_overlays_use_semantic_tokens() -> None:
|
||||
|
||||
@@ -5,6 +5,8 @@ import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from cognitive_agent.application.mode_profile import ModeProfileRegistry
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
@@ -285,6 +287,7 @@ const context = {
|
||||
setModeRailSelection(mode) { calls.push(["select", mode?.id]); },
|
||||
setModeExecutionState(modeId, status) { calls.push(["state", modeId, status]); },
|
||||
closeModeGallery(options) { calls.push(["close", options.restoreFocus]); },
|
||||
hideModeActionPopover() {},
|
||||
showToast(message) { calls.push(["toast", message]); },
|
||||
$(selector) {
|
||||
return { "#utterance": input, "#requestForm": form, "#runButton": runButton }[selector];
|
||||
@@ -355,6 +358,44 @@ console.log(JSON.stringify({ result: context.result, calls }));
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("phase", "input_mode"), (
|
||||
("capturing", "manual"),
|
||||
("capturing", "wake"),
|
||||
("finalizing", "manual"),
|
||||
("finalizing", "wake"),
|
||||
("arming", "manual"),
|
||||
))
|
||||
def test_mode_click_during_dictation_does_not_create_stuck_running_state(
|
||||
phase: str, input_mode: str,
|
||||
) -> None:
|
||||
app = (WEB / "app.js").read_text(encoding="utf-8")
|
||||
start = app.index("function applyModeCommand")
|
||||
end = app.index("\n\nfunction applyRuntimeTargetPolicy", start)
|
||||
script = "const snippet = " + json.dumps(app[start:end]) + ";\n"
|
||||
script += "const speech = " + json.dumps({"phase": phase, "inputMode": input_mode}) + ";\n"
|
||||
script += r"""
|
||||
const vm = require("node:vm");
|
||||
const calls = [];
|
||||
const context = {
|
||||
window: {
|
||||
CognitiveAgentAssistant: { isPending() { return false; } },
|
||||
CognitiveAgentSpeech: speech,
|
||||
},
|
||||
showToast(message) { calls.push(message); },
|
||||
};
|
||||
vm.createContext(context);
|
||||
vm.runInContext(snippet + '\nthis.result = applyModeCommand("进入办公模式", "office");', context);
|
||||
console.log(JSON.stringify({ result: context.result, calls }));
|
||||
"""
|
||||
completed = subprocess.run(
|
||||
[node_runtime(), "-e", script], check=True, capture_output=True, encoding="utf-8",
|
||||
)
|
||||
assert json.loads(completed.stdout) == {
|
||||
"result": False,
|
||||
"calls": ["请等待语音整理完成后再发送"],
|
||||
}
|
||||
|
||||
|
||||
def test_top_and_management_mode_cards_share_the_assistant_submission_entrypoint() -> None:
|
||||
app = (WEB / "app.js").read_text(encoding="utf-8")
|
||||
install_start = app.index('document.addEventListener("DOMContentLoaded"')
|
||||
|
||||
@@ -36,6 +36,9 @@ EXPECTED_SCENE_CAPABILITIES = {
|
||||
"seat.set_legrest_height",
|
||||
"seat.set_longitudinal_position",
|
||||
"seat.set_ventilation",
|
||||
"seat.set_driver_ventilation_native",
|
||||
"seat.set_driver_heating_native",
|
||||
"seat.set_driver_massage_native",
|
||||
"window.set_position",
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ ROOT = Path(__file__).resolve().parents[2]
|
||||
(
|
||||
("帮我构建一个适合看书的功能模式", ("看书模式", "")),
|
||||
("构建一个适合放松的功能模式", ("放松模式", "")),
|
||||
("构建一个座椅舒适的功能模式", ("座椅舒适模式", "")),
|
||||
("构建一个让人坐着放松的功能模式", ("坐着放松模式", "")),
|
||||
("请帮我创建适合阅读的模式吧", ("阅读模式", "")),
|
||||
("我想生成一个用来读书的模式并预览", ("读书模式", "")),
|
||||
("请生成用于放松的模式,先看看", ("放松模式", "")),
|
||||
@@ -67,6 +69,8 @@ def test_explicit_names_are_never_rewritten(
|
||||
("帮我构建一个适合看书的功能模式", "reading"),
|
||||
("请创建一个用于阅读的功能模式", "reading"),
|
||||
("构建一个适合放松的功能模式", "relaxation"),
|
||||
("构建一个座椅舒适的功能模式", "seat-comfort"),
|
||||
("构建一个让人坐着放松的功能模式", "relaxation"),
|
||||
("我想创建一套用来舒缓的模式", "relaxation"),
|
||||
("帮我构建一个适合看电影的功能模式", "cinema"),
|
||||
),
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
|
||||
import pytest
|
||||
from jsonschema import Draft202012Validator
|
||||
|
||||
from cognitive_agent.application.assistant_capabilities import AssistantCapabilityCatalog
|
||||
from cognitive_agent.application.mode_profile_intent import parse_mode_profile_create
|
||||
from cognitive_agent.application.scene_composer import build_scene_output_schema
|
||||
from cognitive_agent.application.seat_service import SeatDemoService
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def catalog():
|
||||
service = SeatDemoService()
|
||||
try:
|
||||
yield AssistantCapabilityCatalog(service)
|
||||
finally:
|
||||
service.close()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("message", (
|
||||
"构建一个座椅舒适的功能模式",
|
||||
"构建一个让人坐着放松的功能模式",
|
||||
"可以帮我创建一个适合座椅放松的功能模式吗?",
|
||||
"构建一个主驾按摩的功能模式",
|
||||
"创建一个空调舒适的功能模式",
|
||||
"构建一个适合看书的功能模式,阅读灯打开",
|
||||
))
|
||||
def test_mode_creation_is_not_claimed_by_capability_fast_answer(catalog, message):
|
||||
assert parse_mode_profile_create(message) is not None
|
||||
assert catalog.fast_answer(message) is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("message", (
|
||||
"支持主驾座椅按摩吗?",
|
||||
"主驾与副驾座椅有哪些功能?",
|
||||
"座椅通风支持哪些档位?",
|
||||
"副驾座椅支持按摩吗?",
|
||||
))
|
||||
def test_genuine_seat_questions_remain_read_only_capability_answers(catalog, message):
|
||||
assert parse_mode_profile_create(message) is None
|
||||
assert catalog.fast_answer(message) is not None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("message", (
|
||||
"不要构建座椅舒适模式",
|
||||
"构建一个座椅舒适模式,但不要执行",
|
||||
"取消创建坐着放松模式",
|
||||
"如何构建一个座椅舒适模式?",
|
||||
"解释构建一个座椅舒适的功能模式这句话",
|
||||
))
|
||||
def test_negations_and_read_only_seat_mode_requests_never_authorize_creation(message):
|
||||
assert parse_mode_profile_create(message) is None
|
||||
|
||||
|
||||
def _response(capability, target, parameters):
|
||||
return {
|
||||
"requests": [{"capability": capability, "target_scope": target, "parameters": parameters}],
|
||||
"ambiguities": [],
|
||||
"confidence": 0.95,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("capability", "target", "parameters", "valid"), (
|
||||
("seat.set_driver_ventilation_native", "driver", {"state": "on", "level": 2}, True),
|
||||
("seat.set_driver_heating_native", "driver", {"state": "on", "level": 1}, True),
|
||||
("seat.set_driver_heating_native", "front_left", {"state": "off"}, True),
|
||||
("seat.set_driver_massage_native", "driver", {"action": "mode", "mode": "relax"}, True),
|
||||
("seat.set_driver_massage_native", "driver", {"action": "strength", "strength": 1}, True),
|
||||
("seat.set_driver_massage_native", "driver", {"action": "off"}, True),
|
||||
("seat.set_driver_massage_native", "passenger", {"action": "mode", "mode": "relax"}, False),
|
||||
("seat.set_driver_massage_native", "driver", {"action": "mode"}, False),
|
||||
("seat.set_driver_massage_native", "driver", {"action": "mode", "strength": 1}, False),
|
||||
("seat.set_driver_massage_native", "driver", {"action": "off", "mode": "relax"}, False),
|
||||
("seat.set_driver_massage_native", "driver", {"action": "mode", "mode": "relax", "strength": 1}, False),
|
||||
("seat.set_driver_massage_native", "driver", {"action": "mode", "mode": "unknown"}, False),
|
||||
("seat.set_driver_heating_native", "passenger", {"state": "on", "level": 2}, False),
|
||||
("seat.set_driver_ventilation_native", "driver", {"state": "on", "level": 3}, False),
|
||||
("seat.set_driver_axis_native", "driver", {"axis": "backrest", "direction": "backward", "duration_ms": 500}, False),
|
||||
("seat.set_driver_position_native", "driver", {"slot": 1}, False),
|
||||
("seat.set_backrest_recline", "driver", {"position_pct": 45}, False),
|
||||
))
|
||||
def test_scene_schema_respects_real_driver_and_passenger_capabilities(
|
||||
catalog, capability, target, parameters, valid,
|
||||
):
|
||||
context = catalog.scene_composition_context()
|
||||
validator = Draft202012Validator(build_scene_output_schema(context))
|
||||
assert validator.is_valid(_response(capability, target, parameters)) is valid
|
||||
|
||||
|
||||
def test_scene_context_removes_driver_capabilities_when_runtime_marks_them_unavailable(catalog):
|
||||
snapshot = deepcopy(catalog.snapshot())
|
||||
for capability in snapshot["capabilities"]:
|
||||
if capability["id"].startswith("seat.set_driver_"):
|
||||
capability["assistant_callable"] = False
|
||||
context = catalog.scene_composition_context(snapshot)
|
||||
assert not any(item["id"].startswith("seat.set_driver_") for item in context["capabilities"])
|
||||
validator = Draft202012Validator(build_scene_output_schema(context))
|
||||
assert not validator.is_valid(_response(
|
||||
"seat.set_driver_massage_native", "driver", {"action": "mode", "mode": "relax"},
|
||||
))
|
||||
|
||||
|
||||
def test_seat_comfort_detail_answer_uses_seat_specific_natural_language(catalog):
|
||||
answer = catalog.fast_answer("座椅舒适模式会控制哪些设备?")
|
||||
assert answer is not None
|
||||
assert "主驾座椅通风开启低档" in answer.answer
|
||||
assert "主驾座椅按摩设为放松模式" in answer.answer
|
||||
assert "副驾座椅通风" in answer.answer
|
||||
assert "副驾座椅靠背" in answer.answer
|
||||
assert "action=" not in answer.answer
|
||||
assert "主驾主驾" not in answer.answer
|
||||
Reference in New Issue
Block a user