fix: restore mode clicks and compose seat-aware assistant scenes

This commit is contained in:
vee
2026-09-12 13:33:38 +08:00
parent 76057be044
commit d09b5967f8
28 changed files with 1247 additions and 44 deletions
+24 -4
View File
@@ -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;
dragged = true;
group.classList.add("is-dragging");
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 });
+8 -1
View File
@@ -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 {
+53 -3
View File
@@ -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, "灯光");
}
+3 -3
View File
@@ -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">
+4 -4
View File
@@ -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: 使用中低风量。}
+3 -2
View File
@@ -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: 将副驾靠背调整到舒展的休闲角度。}
+14
View File
@@ -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,
+48 -2
View File
@@ -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:
+178
View File
@@ -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; });
+86
View File
@@ -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()
+10 -10
View File
@@ -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"
}
+113 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+41
View File
@@ -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