feat: improve mode and peripheral orchestration

This commit is contained in:
Miayui
2026-09-21 00:13:47 +08:00
parent 89707a42e2
commit 448c8b31a0
39 changed files with 3068 additions and 277 deletions
@@ -2465,8 +2465,19 @@ class AssistantCapabilityCatalog:
if isinstance(command_flow, Mapping)
else None
)
local_n7_transfer_actions = (
command_flow.get("n7_transfer_actions")
if isinstance(command_flow, Mapping)
else None
)
n7_transfer_ready = (
raw.get("source_topic") == "from_n7/scenes"
(
raw.get("source_topic") == "from_n7/scenes"
or (
isinstance(local_n7_transfer_actions, list)
and bool(local_n7_transfer_actions)
)
)
and isinstance(command_plans, Mapping)
and bool(command_plans)
)
@@ -726,6 +726,17 @@ def _level_matches(text: str) -> set[int]:
return values
def _driver_native_thermal_level_matches(text: str) -> set[int]:
"""Translate occupant-facing 1/3 gears to CH1 low/high enum values."""
values: set[int] = set()
if re.search(r"(?:1|一)\s*(?:档|级)|最低|低档|低速", text):
values.add(2)
if re.search(r"(?:3|三)\s*(?:档|级)|最高|高档|高速", text):
values.add(1)
return values
def _axis_number_matches(
text: str,
axis: str,
@@ -861,7 +872,16 @@ def _semantic_parameter_error(
return "temperature_c 没有原始文本数值依据"
for name in {"level", "brightness_level"} & values.keys():
levels = _level_matches(text)
levels = (
_driver_native_thermal_level_matches(text)
if name == "level"
and capability
in {
"seat.set_driver_ventilation_native",
"seat.set_driver_heating_native",
}
else _level_matches(text)
)
value = values[name]
if levels:
if not any(_numeric_equal(value, expected) for expected in levels):
@@ -2727,7 +2727,22 @@ class CodexAssistantService:
raise TypeError("vehicle control returned an invalid result")
safe_result = self._safe_vehicle_result(result)
action_state, successful, completion_text = _vehicle_action_state(result)
if mode_profile_preview:
if (
saved_mode_execution
and result.get("status") == "partially_succeeded"
):
successful = True
action_state = (
"simulated"
if result.get("execution_mode") != "hardware"
else "submitted"
)
completion_text = (
"功能模式模拟编排已完成"
if action_state == "simulated"
else "功能模式指令已提交"
)
elif mode_profile_preview:
successful = _mode_profile_result_succeeded(result)
action_state = "simulated" if successful else "failed"
completion_text = (
@@ -4232,6 +4247,7 @@ class CodexAssistantService:
"local_control_elapsed_ms",
"local_control_within_budget",
"natural_language_routing",
"parallel_dispatch",
):
if result.get(key) is not None:
payload[key] = CodexAssistantService._safe_result_value(
@@ -4444,6 +4460,11 @@ class CodexAssistantService:
}
if result.get("orchestration_route") is not None:
safe["orchestration_route"] = str(result["orchestration_route"])
if isinstance(result.get("parallel_dispatch"), Mapping):
safe["parallel_dispatch"] = CodexAssistantService._safe_result_value(
result["parallel_dispatch"],
secret=secret,
)
validation = result.get("validation")
if isinstance(validation, Sequence) and not isinstance(
validation,
@@ -17,6 +17,11 @@ from typing import Any
from cognitive_agent.application.extension_toy_publisher import (
normalize_extension_nfc_id,
)
from cognitive_agent.application.self_extension import (
is_peripheral_color_capability,
is_peripheral_power_off_capability,
is_peripheral_power_on_capability,
)
EXTENSION_CONTROL_TOPIC = "extension/ctl"
@@ -194,7 +199,7 @@ def authorize_extension_control_actions(
request: Mapping[str, Any],
catalog: object,
) -> list[dict[str, Any]]:
"""Authorize every action before any TCP command may be sent."""
"""Authorize every action and inject mandatory light power prerequisites."""
actions = request.get("actions")
if not isinstance(actions, list):
@@ -251,9 +256,86 @@ def authorize_extension_control_actions(
"action": deepcopy(dict(action)),
"capability_id": str(capability.get("id") or ""),
"extension_name": str(capability.get("device_name") or ""),
"capability": capability,
"auto_inserted": False,
"sequence_order": index * 2 + 1,
}
)
return authorized
allowed_by_device: dict[str, list[dict[str, Any]]] = {}
for capability in allowlist.values():
device_id = str(capability.get("device_id") or "").strip()
if device_id:
allowed_by_device.setdefault(device_id, []).append(capability)
expanded: list[dict[str, Any]] = []
powered_devices: set[str] = set()
next_index = len(actions)
for item in sorted(
authorized,
key=lambda value: (value["at_ms"], value["sequence_order"]),
):
capability = item["capability"]
device_id = str(capability.get("device_id") or "").strip()
if is_peripheral_power_on_capability(capability):
powered_devices.add(device_id)
elif is_peripheral_power_off_capability(capability):
powered_devices.discard(device_id)
elif (
is_peripheral_color_capability(capability)
and device_id not in powered_devices
):
prerequisite = next(
(
candidate
for candidate in sorted(
allowed_by_device.get(device_id, []),
key=lambda value: str(value.get("id") or ""),
)
if is_peripheral_power_on_capability(candidate)
),
None,
)
if prerequisite is None:
raise ExtensionControlAuthorizationError(
"extension/ctl color action requires an enabled, NFC-bound "
f"power-on action for the same device: {device_id}"
)
prerequisite_slot = int(prerequisite["slot"])
prerequisite_action = str(prerequisite.get("action") or "").strip()
expanded.append(
{
"index": next_index,
"at_ms": item["at_ms"],
"action": {
"type": "infrared_emit",
"at_ms": item["at_ms"],
"domain": "self_extension",
"capability": "infrared.emit",
"target_scope": device_id,
"operation": "emit",
"parameters": {
"action": prerequisite_action,
"slot": prerequisite_slot,
},
},
"capability_id": str(prerequisite.get("id") or ""),
"extension_name": str(
prerequisite.get("device_name") or ""
),
"capability": prerequisite,
"auto_inserted": True,
"dependency": "power_on_before_color",
"sequence_order": int(item["sequence_order"]) - 1,
}
)
next_index += 1
powered_devices.add(device_id)
expanded.append(item)
if len(expanded) > MAX_EXTENSION_CONTROL_ACTIONS:
raise ExtensionControlAuthorizationError(
"extension/ctl exceeds the action limit after adding power-on prerequisites"
)
return expanded
class ExtensionControlConsumer:
@@ -477,7 +559,14 @@ class ExtensionControlConsumer:
self._record_rejection(exc, request=request)
return
ordered_actions = sorted(actions, key=lambda item: (item["at_ms"], item["index"]))
ordered_actions = sorted(
actions,
key=lambda item: (
item["at_ms"],
item.get("sequence_order", item["index"]),
item["index"],
),
)
action_statuses = [
{
"index": authorized["index"],
@@ -488,6 +577,8 @@ class ExtensionControlConsumer:
"target_scope": authorized["action"]["target_scope"],
"action": authorized["action"]["parameters"]["action"],
"slot": authorized["action"]["parameters"]["slot"],
"auto_inserted": authorized.get("auto_inserted") is True,
"dependency": authorized.get("dependency"),
"state": "pending",
"state_label": "等待下发",
"state_detail": f"计划在接收后 {authorized['at_ms']} ms 下发",
@@ -583,6 +674,8 @@ class ExtensionControlConsumer:
"target_scope": authorized["action"]["target_scope"],
"action": authorized["action"]["parameters"]["action"],
"slot": authorized["action"]["parameters"]["slot"],
"auto_inserted": authorized.get("auto_inserted") is True,
"dependency": authorized.get("dependency"),
"transport": str(result.get("transport") or "tcp"),
"gateway_acknowledged": gateway_acknowledged,
"physical_state_confirmed": physical_state_confirmed,
@@ -267,6 +267,33 @@ def _scene_from_profile(
for operation, target, operation_ref in effective_operations
]
)
local_transfer_actions = (
profile.command_flow.get("n7_transfer_actions")
if not profile.sync_origin
and isinstance(profile.command_flow, dict)
and isinstance(profile.command_flow.get("n7_transfer_actions"), list)
else []
)
transfer_by_identity: dict[str, dict[str, Any]] = {}
for raw_action in local_transfer_actions:
if not isinstance(raw_action, dict):
continue
try:
transfer_operation = ModeOperation.from_dict(raw_action)
except (KeyError, TypeError, ValueError):
continue
transfer_by_identity[_operation_identity(transfer_operation)] = deepcopy(
raw_action
)
if transfer_by_identity:
actions = [
transfer_by_identity.get(_operation_identity(operation), action)
for (operation, _, _), action in zip(
effective_operations,
actions,
strict=True,
)
]
scene = {
"id": profile.id,
"label": profile.name,
@@ -106,9 +106,13 @@ OUTLET_MODE_DISABLE_PATTERN = re.compile(
)
DRIVER_SEAT_THERMAL_MIDDLE_PATTERN = re.compile(
r"(?:通风|加热)[^,,;;。!?!?\n]{0,16}"
r"(?:中等(?:档|级)?|中档|中级|中间档|适中|(?:调到|调为|调|设为|设置为|调整到|调整为)中)|"
r"(?:中等(?:档|级)?|中档|中级|中间档|适中|(?:2|二)(?:档|级)|"
r"(?:调到|调为|调|设为|设置为|调整到|调整为)中)|"
r"(?:中等(?:档|级)?|中档|中级|中间档|适中)"
r"[^,,;;。!?!?\n]{0,16}(?:通风|加热)"
r"[^,,;;。!?!?\n]{0,16}(?:通风|加热)|"
r"(?:通风|加热)[^,,;;。!?!?\n]{0,16}[,,;;]"
r"(?:挡位|档位)?(?:调到|调为|设为|设置为|调整到|调整为)?"
r"(?:2|二)(?:档|级)"
)
DRIVER_SEAT_MASSAGE_CONTINUATION_PATTERN = re.compile(
r"^(?:(?:然后|随后|接着|同时|并且|再|并|以及)?(?:把|将)?(?:按摩)?)"
@@ -116,6 +120,12 @@ DRIVER_SEAT_MASSAGE_CONTINUATION_PATTERN = re.compile(
r"(?:切换|改成|改为|调整|调成|调为|设置|设为)"
r".{0,8}(?:波浪|脉冲|伸展|拉伸|松弛|舒展|放松)(?:模式)?)"
)
DRIVER_SEAT_THERMAL_CONTINUATION_PATTERN = re.compile(
r"^(?:(?:然后|随后|接着|同时|并且|再|并|以及)?(?:把|将)?)"
r"(?:通风|加热)?(?:挡位|档位)?"
r"(?:切换|改成|改为|调整|调到|调成|调为|设置|设为)?"
r"(?:[1-3一二三](?:档|级)|最低|低档|最高|高档|中档|中等|适中)$"
)
_FEATURE_BOUNDARY = re.compile(
r"并且|同时|然后|随后|接着|以及|并|再|和|与|、|,"
)
@@ -775,6 +785,23 @@ def _parse_chinese_driver_seat_commands(compact: str) -> list[dict[str, Any]]:
clause = f"{clause}{continuation}"
continuation_index += 1
index = continuation_index - 1
if "通风" in clause or "加热" in clause:
continuation_index = index + 1
while (
continuation_index < len(seat_clauses)
and DRIVER_SEAT_THERMAL_CONTINUATION_PATTERN.search(
seat_clauses[continuation_index]
)
):
continuation = re.sub(
r"^(?:然后|随后|接着|同时|并且|再|并|以及)?"
r"(?:把|将)?(?:通风|加热)?",
"",
seat_clauses[continuation_index],
)
clause = f"{clause}{continuation}"
continuation_index += 1
index = continuation_index - 1
scope = _seat_target_scope(clause)
if scope == "driver":
inherited_driver_scope = True
@@ -864,11 +891,13 @@ def _parse_chinese_driver_seat_commands(compact: str) -> list[dict[str, Any]]:
if re.search(r"关闭|关掉|停止|取消|不要|别开", thermal_text):
parameters = {"state": "off"}
elif thermal_grade == "high" or re.search(
r"高速|(?:1|一)(?:档|级)", thermal_text
r"高速|(?:3|三)(?:档|级)", thermal_text
):
parameters = {"state": "on", "level": 1}
elif re.search(r"(?:2|二)(?:档|级)|中档|中等|适中", thermal_text):
parameters = None
elif thermal_grade == "low" or re.search(
r"低速|(?:2|二)(?:档|级)", thermal_text
r"低速|(?:1|一)(?:档|级)", thermal_text
) or re.search(r"打开|开启|启动|启用", thermal_text):
parameters = {"state": "on", "level": 2}
elif shared_thermal_state == "off":
@@ -1177,7 +1206,8 @@ def _parse_chinese_commands(text: str) -> list[dict[str, Any]]:
compact,
r"座椅|座位|靠背|椅背|坐盆|坐垫|腿托|脚托|按摩|记忆|预设|"
r"通风|加热|前移|后移|向前移动|向后移动",
DRIVER_SEAT_MASSAGE_CONTINUATION_PATTERN.pattern,
rf"(?:{DRIVER_SEAT_MASSAGE_CONTINUATION_PATTERN.pattern})|"
rf"(?:{DRIVER_SEAT_THERMAL_CONTINUATION_PATTERN.pattern})",
)
actions: list[dict[str, Any]] = _parse_chinese_driver_seat_commands(
seat_text
@@ -1794,7 +1824,7 @@ def merge_native_can_intent(
utterance
)
if unsupported_thermal_middle is not None and not re.search(
r"副驾|副驾驶|乘客|右前", utterance
r"副驾|副驾驶|乘客|乘员|右前", utterance
):
result = dict(parsed)
feature = (
@@ -1334,7 +1334,10 @@ class NativeCanExecutor:
# separate evidence level in ``verification_status`` and
# ``physical_effect_confirmed``. A feedback timeout must
# not be rewritten as a transport/orchestration failure.
succeeded = delivered
# A closed-loop target may already be satisfied before a
# new frame is needed. Fresh matching feedback is a
# successful no-op, not a failed dispatch.
succeeded = delivered or verified
verification_status = (
"verified_success"
if verified
@@ -1354,7 +1357,11 @@ class NativeCanExecutor:
publication_start:
]
)
delivery_status = "submitted"
delivery_status = (
"not_required"
if verified and not delivered
else "submitted"
)
if not feedback_required and delivered:
delivery_status = (
"published"
@@ -1371,7 +1378,9 @@ class NativeCanExecutor:
"domain": function_config["spec"]["domain"],
"status": "succeeded" if succeeded else "failed",
"message": (
"MQTT 白名单控制帧已由 Broker 确认;车辆效果以状态话题回读为准。"
"车辆反馈已确认目标状态,无需重复下发控制帧。"
if verified and not delivered
else "MQTT 白名单控制帧已由 Broker 确认;车辆效果以状态话题回读为准。"
if not feedback_required and delivery_status == "published"
else "MQTT 白名单控制帧已进入客户端发布队列;车辆效果以状态话题回读为准。"
if not feedback_required
+506 -8
View File
@@ -104,6 +104,7 @@ from cognitive_agent.application.native_can_feedback import ( # noqa: E402
)
from cognitive_agent.application.native_can_intent import ( # noqa: E402
DRIVER_SEAT_MASSAGE_CONTINUATION_PATTERN,
DRIVER_SEAT_THERMAL_CONTINUATION_PATTERN,
merge_native_can_intent,
)
from cognitive_agent.application.native_can_relative import ( # noqa: E402
@@ -202,6 +203,39 @@ LOCAL_CONTROL_BUDGET_MS = 1000
VEHICLE_CONTROL_TRANSPORTS = frozenset(
{WIRED_CAN_TRANSPORT, WIRELESS_MQTT_TRANSPORT}
)
N7_CINEMA_RELATED_PATTERN = re.compile(
r"影院|观影|电影|看电影|cinema|movie|theater|theatre",
re.IGNORECASE,
)
N7_CINEMA_BASE_NAMES = frozenset(
{"影院", "影院模式", "观影", "观影模式", "cinema", "cinemamode"}
)
N7_FLASHING_LIGHT_PATTERN = re.compile(
r"闪烁灯|闪光灯|闪灯玩具|flashing\s*light",
re.IGNORECASE,
)
N7_LIGHT_STRIP_PATTERN = re.compile(
r"灯带|灯条|light\s*strip|led\s*strip",
re.IGNORECASE,
)
N7_PERIPHERAL_ACTIVATE_PATTERN = re.compile(
r"打开|开启|启动|点亮|\bon\b|power\s*on",
re.IGNORECASE,
)
N7_PERIPHERAL_OFF_PATTERN = re.compile(
r"关闭|关掉|熄灭|\boff\b|power\s*off",
re.IGNORECASE,
)
N7_PERIPHERAL_COLOR_PATTERN = re.compile(
r"红色|绿色|蓝色|白色|紫色|黄色|橙色|粉色|"
r"\bred\b|\bgreen\b|\bblue\b|\bwhite\b|\bpurple\b|"
r"\byellow\b|\borange\b|\bpink\b",
re.IGNORECASE,
)
N7_CINEMA_VIDEO_APP_PATTERN = re.compile(
r"视频|媒体|爱奇艺|video|media|videoplay|iqiyi",
re.IGNORECASE,
)
def _wireless_mqtt_publish_preflight() -> dict[str, Any]:
@@ -472,6 +506,9 @@ class SeatDemoService(ModeScheduleServiceMixin):
lambda scene: self._vehicle_mqtt_transport.publish_mode_scene(scene),
topic=MQTT_MODE_SCENE_TOPIC,
)
self._mode_scene_peripheral_catalog_provider: Callable[
[], list[dict[str, Any]]
] = lambda: []
self._mode_scene_ingress = ModeSceneIngressConsumer(
self.import_n7_mode_scene,
)
@@ -637,6 +674,378 @@ class SeatDemoService(ModeScheduleServiceMixin):
return self.publish_saved_mode_scene(mode_id, vehicle_id)
@staticmethod
def _n7_scene_identity_text(scene: Mapping[str, Any]) -> str:
aliases = scene.get("aliases", [])
return " ".join(
str(value or "")
for value in (
scene.get("id"),
scene.get("name"),
scene.get("label"),
scene.get("description"),
*(aliases if isinstance(aliases, list) else []),
)
)
@classmethod
def _is_n7_cinema_scene(cls, scene: Mapping[str, Any]) -> bool:
return N7_CINEMA_RELATED_PATTERN.search(
cls._n7_scene_identity_text(scene)
) is not None
@staticmethod
def _is_n7_cinema_base_scene(scene: Mapping[str, Any]) -> bool:
scene_id = re.sub(r"\s+", "", str(scene.get("id") or "")).casefold()
names = {
re.sub(r"\s+", "", str(scene.get(field) or "")).casefold()
for field in ("name", "label")
}
return scene_id == "cinema" or bool(names & N7_CINEMA_BASE_NAMES)
@staticmethod
def _n7_scene_action_selector(action: Mapping[str, Any]) -> tuple[object, ...]:
parameters = action.get("parameters")
slot = (
parameters.get("slot")
if isinstance(parameters, Mapping)
and action.get("capability") == "infrared.emit"
else None
)
return (
str(action.get("domain") or ""),
str(action.get("capability") or ""),
str(action.get("target_scope") or ""),
str(action.get("operation") or "set"),
int(action.get("at_ms") or 0),
slot,
)
@classmethod
def _merge_n7_scene_actions(
cls,
base_actions: object,
overriding_actions: object,
) -> list[dict[str, Any]]:
merged: list[dict[str, Any]] = []
positions: dict[tuple[object, ...], int] = {}
for source in (base_actions, overriding_actions):
if not isinstance(source, list):
continue
for raw_action in source:
if not isinstance(raw_action, Mapping):
continue
action = deepcopy(dict(raw_action))
selector = cls._n7_scene_action_selector(action)
existing = positions.get(selector)
if existing is None:
positions[selector] = len(merged)
merged.append(action)
else:
merged[existing] = action
return merged
def _n7_cinema_base_scene(
self,
incoming_scene: Mapping[str, Any],
) -> dict[str, Any] | None:
if self._is_n7_cinema_base_scene(incoming_scene):
return None
vehicle_id = self.vehicle_profiles.default().id
candidates: list[tuple[int, dict[str, Any]]] = []
for public_profile in self.mode_profiles.public_modes(vehicle_id):
try:
profile = self.mode_profiles.get_profile(
str(public_profile.get("id") or ""),
vehicle_id,
)
except (KeyError, ValueError):
continue
if profile.sync_origin != MQTT_MODE_SCENE_INGRESS_TOPIC:
continue
command_flow = profile.command_flow
source_scene = (
command_flow.get("source_scene")
if isinstance(command_flow, Mapping)
else None
)
if not isinstance(source_scene, Mapping) or not self._is_n7_cinema_base_scene(
source_scene
):
continue
priority = 0 if str(source_scene.get("id") or "") == "cinema" else 1
candidates.append((priority, deepcopy(dict(source_scene))))
if not candidates:
return None
candidates.sort(key=lambda item: (item[0], str(item[1].get("id") or "")))
return candidates[0][1]
@staticmethod
def _is_n7_cinema_video_launch_action(action: Mapping[str, Any]) -> bool:
parameters = action.get("parameters")
if (
action.get("domain") != "android"
or action.get("capability") != "android.launch_app"
or action.get("target_scope") != "headunit"
or action.get("operation") != "launch"
or not isinstance(parameters, Mapping)
):
return False
package_name = str(parameters.get("package_name") or "").strip()
activity_name = str(parameters.get("activity_name") or "").strip()
if (
not package_name
or not activity_name
or len(package_name) > 256
or len(activity_name) > 256
):
return False
return N7_CINEMA_VIDEO_APP_PATTERN.search(
" ".join(
(
str(action.get("type") or ""),
package_name,
activity_name,
)
)
) is not None
def _local_cinema_n7_transfer_actions(
self,
mode_name: str,
instruction: str = "",
) -> list[dict[str, Any]]:
"""Reuse the N7 cinema card's video launch for a local cinema variant."""
scene_identity = {
"id": "local-user-mode-variant",
"name": f"本地扩展 {str(mode_name or '').strip()}",
"label": f"本地扩展 {str(mode_name or '').strip()}",
"description": str(instruction or ""),
}
if not self._is_n7_cinema_scene(scene_identity):
return []
base_scene = self._n7_cinema_base_scene(scene_identity)
if base_scene is None:
return []
actions = base_scene.get("actions", [])
if not isinstance(actions, list):
return []
return [
deepcopy(dict(action))
for action in actions
if isinstance(action, Mapping)
and self._is_n7_cinema_video_launch_action(action)
][:1]
@staticmethod
def _n7_transfer_operation_selector(
domain: object,
capability: object,
target_scope: object,
operation: object,
parameters: object,
) -> tuple[str, str, str, str, str]:
clean_parameters = dict(parameters) if isinstance(parameters, Mapping) else {}
return (
str(domain or ""),
str(capability or ""),
str(target_scope or ""),
str(operation or "set"),
json.dumps(
clean_parameters,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
),
)
@classmethod
def _cinema_preferred_color_patterns(
cls,
scene: Mapping[str, Any],
) -> list[re.Pattern[str]]:
text = cls._n7_scene_identity_text(scene)
colors = (
(r"蓝色|\bblue\b",),
(r"紫色|\bpurple\b",),
(r"红色|\bred\b",),
(r"白色|\bwhite\b",),
(r"绿色|\bgreen\b",),
(r"黄色|\byellow\b",),
(r"橙色|\borange\b",),
(r"粉色|\bpink\b",),
)
explicit = [
re.compile(patterns[0], re.IGNORECASE)
for patterns in colors
if re.search(patterns[0], text, re.IGNORECASE)
]
defaults = [re.compile(patterns[0], re.IGNORECASE) for patterns in colors]
return [*explicit, *(item for item in defaults if item not in explicit)]
def _n7_cinema_peripheral_actions(
self,
scene: Mapping[str, Any],
) -> list[dict[str, Any]]:
try:
raw_catalog = self._mode_scene_peripheral_catalog_provider()
except Exception: # noqa: BLE001 - an unavailable catalog must not block import
return []
if not isinstance(raw_catalog, list):
return []
grouped: dict[str, list[dict[str, Any]]] = {}
identities: dict[str, str] = {}
for raw in raw_catalog:
if not isinstance(raw, Mapping):
continue
device_id = str(raw.get("device_id") or "").strip()
card_uid = str(raw.get("card_uid") or "").strip()
slot = raw.get("slot")
if (
not device_id
or not card_uid
or raw.get("status") != "learned"
or raw.get("enabled", True) is False
or isinstance(slot, bool)
or not isinstance(slot, int)
or not 1 <= slot <= 248
):
continue
item = deepcopy(dict(raw))
grouped.setdefault(device_id, []).append(item)
names = [
raw.get("device_name"),
*(raw.get("device_aliases", []) if isinstance(raw.get("device_aliases"), list) else []),
*(raw.get("object_names", []) if isinstance(raw.get("object_names"), list) else []),
]
identities[device_id] = " ".join(str(value or "") for value in names)
def action_text(item: Mapping[str, Any]) -> str:
return " ".join(
[
str(item.get("action") or ""),
*(item.get("aliases", []) if isinstance(item.get("aliases"), list) else []),
*(item.get("action_names", []) if isinstance(item.get("action_names"), list) else []),
]
)
def as_scene_action(
item: Mapping[str, Any],
*,
category: str,
kind: str,
) -> dict[str, Any]:
device_id = str(item.get("device_id") or "")
device_name = str(item.get("device_name") or device_id)
slot = int(item["slot"])
action_name = str(item.get("action") or "").strip()
return {
"type": "infrared_emit",
"at_ms": 0,
"domain": "self_extension",
"capability": "infrared.emit",
"target_scope": device_id,
"target": {
"device": device_id,
"name": device_name,
"card_uid": str(item.get("card_uid") or ""),
},
"operation": "emit",
"parameters": {"action": action_name, "slot": slot},
"operation_ref": f"pc-cinema-{category}-{kind}-{slot}",
}
selected_devices: set[str] = set()
result: list[dict[str, Any]] = []
color_patterns = self._cinema_preferred_color_patterns(scene)
for category, identity_pattern in (
("flashing-light", N7_FLASHING_LIGHT_PATTERN),
("light-strip", N7_LIGHT_STRIP_PATTERN),
):
device_id = next(
(
key
for key in sorted(grouped)
if key not in selected_devices
and identity_pattern.search(identities.get(key, key))
),
None,
)
if device_id is None:
continue
selected_devices.add(device_id)
actions = sorted(grouped[device_id], key=lambda item: int(item["slot"]))
activation = next(
(
item
for item in actions
if N7_PERIPHERAL_ACTIVATE_PATTERN.search(action_text(item))
and N7_PERIPHERAL_OFF_PATTERN.search(action_text(item)) is None
and N7_PERIPHERAL_COLOR_PATTERN.search(action_text(item)) is None
),
None,
)
color = next(
(
item
for pattern in color_patterns
for item in actions
if pattern.search(action_text(item))
and N7_PERIPHERAL_COLOR_PATTERN.search(action_text(item))
),
None,
)
if color is None:
color = next(
(
item
for item in actions
if N7_PERIPHERAL_COLOR_PATTERN.search(action_text(item))
),
None,
)
if activation is not None:
result.append(
as_scene_action(activation, category=category, kind="activate")
)
if color is not None and (
activation is None or color.get("id") != activation.get("id")
):
result.append(as_scene_action(color, category=category, kind="color"))
return result
def _augment_n7_cinema_scene(
self,
scene: dict[str, Any],
) -> dict[str, object] | None:
if not self._is_n7_cinema_scene(scene):
return None
base_scene = self._n7_cinema_base_scene(scene)
if base_scene is None:
return None
base_actions = base_scene.get("actions", [])
incoming_actions = scene.get("actions", [])
inherited = self._merge_n7_scene_actions(base_actions, incoming_actions)
peripheral_actions = self._n7_cinema_peripheral_actions(scene)
augmented = self._merge_n7_scene_actions(inherited, peripheral_actions)
if len(augmented) > 256:
raise ValueError("影院相关模式合并后的动作数量超过 256 项。")
scene["actions"] = augmented
scene["operation_count"] = len(augmented)
scene["domains"] = list(
dict.fromkeys(str(action.get("domain") or "") for action in augmented)
)
return {
"base_scene_id": str(base_scene.get("id") or ""),
"base_action_count": len(base_actions) if isinstance(base_actions, list) else 0,
"incoming_action_count": (
len(incoming_actions) if isinstance(incoming_actions, list) else 0
),
"peripheral_action_count": len(peripheral_actions),
"merged_action_count": len(augmented),
}
@staticmethod
def _imported_scene_function_configs(
scene: dict[str, Any],
@@ -694,6 +1103,7 @@ class SeatDemoService(ModeScheduleServiceMixin):
"""Upsert one validated N7 card without publishing it back to the broker."""
document = deepcopy(dict(scene))
cinema_augmentation = self._augment_n7_cinema_scene(document)
actions = document.get("actions")
if not isinstance(actions, list) or not actions:
raise ValueError("from_n7/scenes card must contain executable actions")
@@ -764,7 +1174,7 @@ class SeatDemoService(ModeScheduleServiceMixin):
sync_origin=MQTT_MODE_SCENE_INGRESS_TOPIC,
upstream_origin=str(document.get("origin") or ""),
)
return {
result = {
"status": "imported",
"scene_id": saved.id,
"scene_name": saved.name,
@@ -772,6 +1182,9 @@ class SeatDemoService(ModeScheduleServiceMixin):
"profile_revision": self.mode_profiles.profile_revision(saved),
"storage": "persistent_yaml",
}
if cinema_augmentation is not None:
result["cinema_augmentation"] = cinema_augmentation
return result
@property
def driver_only_ch1_execution_active(self) -> bool:
@@ -1015,14 +1428,42 @@ class SeatDemoService(ModeScheduleServiceMixin):
requests = self.mode_profiles.requests_for_profiles([profile])
if profile.sync_origin != MQTT_MODE_SCENE_INGRESS_TOPIC:
return [
request
for operation, request in zip(profile.operations, requests, strict=True)
if not (
transfer_actions = self._local_cinema_n7_transfer_actions(
profile.name,
profile.instruction,
)
transfer_selectors = {
self._n7_transfer_operation_selector(
action.get("domain"),
action.get("capability"),
action.get("target_scope"),
action.get("operation"),
action.get("parameters"),
)
for action in transfer_actions
}
local_requests = []
for operation, request in zip(
profile.operations,
requests,
strict=True,
):
if (
operation.domain == "self_extension"
and operation.capability == "infrared.emit"
):
continue
selector = self._n7_transfer_operation_selector(
operation.domain,
operation.capability,
operation.target_scope,
operation.operation,
operation.parameters,
)
], []
if selector in transfer_selectors:
continue
local_requests.append(request)
return local_requests, transfer_actions
source_scene = (
profile.command_flow.get("source_scene")
if isinstance(profile.command_flow, dict)
@@ -1109,7 +1550,7 @@ class SeatDemoService(ModeScheduleServiceMixin):
execution_mode: str,
local_preview: bool,
) -> dict[str, Any]:
"""Forward only N7-originated actions never submitted by this PC."""
"""Forward validated N7-only actions never submitted by this PC."""
actions = result.get("n7_transfer_candidates")
scene = result.get("n7_source_scene")
@@ -1579,6 +2020,48 @@ class SeatDemoService(ModeScheduleServiceMixin):
raise ValueError("动态模式更新必须保留完整时间轴,请按阶段重新生成预览。")
if schedule is not None:
operations = [deepcopy(op) for phase in schedule.phases for op in phase.operations]
n7_transfer_actions = (
self._local_cinema_n7_transfer_actions(
clean_name,
clean_instruction,
)
if schedule is None
else []
)
if n7_transfer_actions:
command_flow["n7_transfer_actions"] = deepcopy(n7_transfer_actions)
existing_operation_selectors = {
self._n7_transfer_operation_selector(
operation.get("domain"),
operation.get("capability"),
operation.get("target_scope"),
operation.get("operation"),
operation.get("parameters"),
)
for operation in operations
if isinstance(operation, Mapping)
}
for action in n7_transfer_actions:
selector = self._n7_transfer_operation_selector(
action.get("domain"),
action.get("capability"),
action.get("target_scope"),
action.get("operation"),
action.get("parameters"),
)
if selector in existing_operation_selectors:
continue
operations.append(
{
"domain": str(action.get("domain") or ""),
"capability": str(action.get("capability") or ""),
"target_scope": str(action.get("target_scope") or ""),
"operation": str(action.get("operation") or "launch"),
"parameters": deepcopy(dict(action.get("parameters") or {})),
"reason": "继承车机基础影院模式的视频媒体启动动作。",
}
)
existing_operation_selectors.add(selector)
saved = self.mode_profiles.save_mode(
clean_name,
operations,
@@ -1843,6 +2326,16 @@ class SeatDemoService(ModeScheduleServiceMixin):
self._vehicle_mqtt_transport.set_extension_control_rejection_handler(handler)
def set_mode_scene_peripheral_catalog_provider(
self,
provider: Callable[[], list[dict[str, Any]]] | None,
) -> None:
"""Supply the durable NFC-authorized IR catalog for N7 scene imports."""
if provider is not None and not callable(provider):
raise TypeError("mode scene peripheral catalog provider must be callable")
self._mode_scene_peripheral_catalog_provider = provider or (lambda: [])
def _synchronize_vehicle_state_once(
self,
conversation_id: str,
@@ -8372,6 +8865,11 @@ class SeatDemoService(ModeScheduleServiceMixin):
and "按摩" in clauses[-1]
and DRIVER_SEAT_MASSAGE_CONTINUATION_PATTERN.search(fragment)
)
thermal_continuation = bool(
clauses
and re.search(r"通风|加热", clauses[-1])
and DRIVER_SEAT_THERMAL_CONTINUATION_PATTERN.search(fragment)
)
# An exclusion is a qualifier of the control immediately before it,
# not an independent command. Keeping both pieces together lets
# the deterministic window parser remove the excluded targets
@@ -8379,7 +8877,7 @@ class SeatDemoService(ModeScheduleServiceMixin):
exclusion_qualifier = bool(
clauses and re.search(r"除外|除去|不包括|不含", fragment)
)
if massage_continuation or exclusion_qualifier:
if massage_continuation or thermal_continuation or exclusion_qualifier:
clauses[-1] = f"{clauses[-1]},{fragment}"
else:
clauses.append(fragment)
@@ -1050,6 +1050,152 @@ def _peripheral_execution_identity_authorized(
return bool(_device_match_score(_compact_match_text(utterance), capability))
_PERIPHERAL_COLOR_ACTION_PATTERN = re.compile(
r"颜色|色彩|红色|红灯|红光|绿色|绿灯|绿光|蓝色|蓝灯|蓝光|白色|白灯|白光|"
r"紫色|紫灯|紫光|黄色|黄灯|黄光|橙色|橙灯|橙光|粉色|粉灯|粉光|"
r"\b(?:color|red|green|blue|white|purple|yellow|orange|pink)\b",
re.IGNORECASE,
)
_PERIPHERAL_POWER_ON_ACTION_PATTERN = re.compile(
r"打开|开启|启动|点亮|开灯|\b(?:on|power\s*on|turn\s*on)\b",
re.IGNORECASE,
)
_PERIPHERAL_POWER_OFF_ACTION_PATTERN = re.compile(
r"关闭|关掉|停止|关灯|熄灭|\b(?:off|power\s*off|turn\s*off)\b",
re.IGNORECASE,
)
def peripheral_capability_action_text(capability: Mapping[str, Any]) -> str:
"""Return the complete learned action vocabulary used for dependencies."""
values: list[str] = [str(capability.get("action") or "")]
for field in ("aliases", "action_names"):
aliases = capability.get(field, [])
if isinstance(aliases, str):
values.append(aliases)
elif isinstance(aliases, list):
values.extend(str(value or "") for value in aliases)
return " ".join(value.strip() for value in values if value.strip())
def is_peripheral_color_capability(capability: Mapping[str, Any]) -> bool:
"""Return whether an infrared ability changes a light-like device color."""
return _PERIPHERAL_COLOR_ACTION_PATTERN.search(
peripheral_capability_action_text(capability)
) is not None
def is_peripheral_power_on_capability(capability: Mapping[str, Any]) -> bool:
"""Return whether an infrared ability is a stable device activation."""
text = peripheral_capability_action_text(capability)
return bool(
_PERIPHERAL_POWER_ON_ACTION_PATTERN.search(text)
and _PERIPHERAL_POWER_OFF_ACTION_PATTERN.search(text) is None
and _PERIPHERAL_COLOR_ACTION_PATTERN.search(text) is None
)
def is_peripheral_power_off_capability(capability: Mapping[str, Any]) -> bool:
"""Return whether an infrared ability explicitly powers a device off."""
return _PERIPHERAL_POWER_OFF_ACTION_PATTERN.search(
peripheral_capability_action_text(capability)
) is not None
def add_peripheral_activation_prerequisites(
steps: list[dict[str, Any]],
catalog: list[dict[str, Any]],
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
"""Insert an authorized power-on step before each device's first color.
Infrared color commands do not prove that the target is powered. The
executor therefore treats a learned power-on command for the same durable
device binding as a mandatory dependency. The returned catalog carries
the original parse source onto an injected prerequisite so saved modes can
still pass their immutable-flow authorization check.
"""
ordered_ids: list[str] = []
catalog_by_id: dict[str, dict[str, Any]] = {}
for raw in catalog[:MAX_CATALOG_ITEMS]:
if not isinstance(raw, Mapping):
continue
capability_id = str(raw.get("id") or "").strip()
if not capability_id:
continue
if capability_id not in catalog_by_id:
ordered_ids.append(capability_id)
catalog_by_id[capability_id] = dict(raw)
prepared_steps: list[dict[str, Any]] = []
powered_devices: set[str] = set()
for raw_step in steps:
step = dict(raw_step) if isinstance(raw_step, Mapping) else raw_step
if not isinstance(step, dict) or step.get("kind") != "peripheral":
prepared_steps.append(step)
continue
capability_id = str(step.get("capability_id") or "").strip()
capability = catalog_by_id.get(capability_id)
if capability is None:
prepared_steps.append(step)
continue
device_id = str(capability.get("device_id") or "").strip()
if is_peripheral_power_on_capability(capability):
if device_id:
powered_devices.add(device_id)
prepared_steps.append(step)
continue
if is_peripheral_power_off_capability(capability):
if device_id:
powered_devices.discard(device_id)
prepared_steps.append(step)
continue
if not is_peripheral_color_capability(capability) or not device_id:
prepared_steps.append(step)
continue
if device_id not in powered_devices:
prerequisite = next(
(
item
for item in catalog_by_id.values()
if str(item.get("device_id") or "").strip() == device_id
and item.get("status") == "learned"
and item.get("enabled", True) is not False
and bool(_normalized_card_uid(item.get("card_uid")))
and is_peripheral_power_on_capability(item)
),
None,
)
if prerequisite is None:
raise ExtensionIntegrationError(
f"外设 {capability.get('device_name') or device_id} 设置颜色前需要先打开,"
"但当前没有已学习、已启用且已绑定 NFC 的打开指令。"
)
prerequisite_id = str(prerequisite.get("id") or "").strip()
parse_source = str(capability.get("parse_source") or "").strip()
if parse_source and not prerequisite.get("parse_source"):
prerequisite = {**prerequisite, "parse_source": parse_source}
catalog_by_id[prerequisite_id] = prerequisite
prepared_steps.append(
{
"kind": "peripheral",
"capability_id": prerequisite_id,
"dependency": "power_on_before_color",
"auto_inserted": True,
}
)
powered_devices.add(device_id)
prepared_steps.append(step)
if len(prepared_steps) > MAX_PLAN_STEPS * 2:
raise ExtensionIntegrationError("补齐外设打开前置指令后,动作数量超过安全上限。")
return prepared_steps, [catalog_by_id[item_id] for item_id in ordered_ids]
def _normalize_peripheral_capability(value: Any) -> dict[str, Any]:
"""Validate one browser-synchronized learned infrared capability."""
@@ -2615,6 +2761,36 @@ class SelfExtensionService:
raise ExtensionIntegrationError(
"外设名称未在原始指令中被明确提及,已拒绝红外执行。"
)
if is_peripheral_color_capability(capability):
device_id = str(capability.get("device_id") or "").strip()
dependency_catalog = [
dict(item)
for item in self.list_peripheral_capabilities()
if isinstance(item, Mapping)
and str(item.get("device_id") or "").strip() == device_id
and str(item.get("id") or "").strip()
!= str(capability.get("id") or "").strip()
]
dependency_catalog.append(dict(capability))
return self.execute_peripheral_plan_request(
utterance,
{
"provider_used": "infrared_dependency_guard",
"summary": "设置外设颜色前自动补齐同设备的打开指令。",
"steps": [
{
"kind": "peripheral",
"capability_id": str(capability.get("id") or ""),
}
],
},
dependency_catalog,
conversation_id=conversation_id,
conversation_turn=conversation_turn,
execution_mode=execution_mode,
provider_requested=provider_requested,
on_commands_validated=on_commands_validated,
)
slot = validate_slot(capability.get("slot"))
request_id = f"req-extension-{uuid.uuid4().hex}"
conversation_id = conversation_id or f"conv-{uuid.uuid4().hex}"
@@ -2819,6 +2995,39 @@ class SelfExtensionService:
raise ExtensionIntegrationError("GPT 没有生成可执行的外设动作。")
if any(step.get("kind") == "vehicle" for step in steps if isinstance(step, dict)):
raise ExtensionIntegrationError("当前主编排只允许执行持久绑定的 NFC 外设计划。")
selected_device_ids = {
str(item.get("device_id") or "").strip()
for item in catalog
if isinstance(item, Mapping)
and str(item.get("device_id") or "").strip()
}
effective_catalog: list[dict[str, Any]] = [
dict(item) for item in catalog if isinstance(item, Mapping)
]
known_ids = {
str(item.get("id") or "").strip()
for item in effective_catalog
if str(item.get("id") or "").strip()
}
for raw_item in self.list_peripheral_capabilities():
if not isinstance(raw_item, Mapping):
continue
item = dict(raw_item)
capability_id = str(item.get("id") or "").strip()
if (
not capability_id
or capability_id in known_ids
or str(item.get("device_id") or "").strip()
not in selected_device_ids
):
continue
effective_catalog.append(dict(item))
known_ids.add(capability_id)
steps, catalog = add_peripheral_activation_prerequisites(
[dict(step) if isinstance(step, Mapping) else step for step in steps],
effective_catalog,
)
plan_result = {**plan_result, "steps": steps}
catalog_by_id = {
str(item.get("id")): item
for item in catalog
@@ -2884,6 +3093,7 @@ class SelfExtensionService:
function_operations.append(
{
"id": operation_ref,
"capability_id": capability_id,
"capability": "infrared.emit",
"target": {
"device": capability.get("device_id"),
@@ -2897,6 +3107,8 @@ class SelfExtensionService:
"parse_source": capability.get(
"parse_source", "local_rule"
),
"auto_inserted": step.get("auto_inserted") is True,
"dependency": step.get("dependency"),
"timeout_ms": int(self.command_timeout * 1000),
"failure_policy": "abort",
}
@@ -36,9 +36,13 @@ POSITIVE_OUTLET_MODE = re.compile(
)
DRIVER_SEAT_THERMAL_MIDDLE = re.compile(
r"(?:通风|加热)[^,,;;。!?!?\n]{0,16}"
r"(?:中等(?:档|级)?|中档|中级|中间档|适中|(?:调到|调为|调|设为|设置为|调整到|调整为)中)|"
r"(?:中等(?:档|级)?|中档|中级|中间档|适中|(?:2|二)(?:档|级)|"
r"(?:调到|调为|调|设为|设置为|调整到|调整为)中)|"
r"(?:中等(?:档|级)?|中档|中级|中间档|适中)"
r"[^,,;;。!?!?\n]{0,16}(?:通风|加热)"
r"[^,,;;。!?!?\n]{0,16}(?:通风|加热)|"
r"(?:通风|加热)[^,,;;。!?!?\n]{0,16}[,,;;]"
r"(?:挡位|档位)?(?:调到|调为|设为|设置为|调整到|调整为)?"
r"(?:2|二)(?:档|级)"
)
META_NON_EXECUTION = re.compile(
r"复述|引用|翻译|改写|总结|解释|分析|造句|"
@@ -542,7 +546,7 @@ def apply_language_safety_guards(
thermal_middle = DRIVER_SEAT_THERMAL_MIDDLE.search(utterance)
if thermal_middle is not None and not re.search(
r"副驾|副驾驶|乘客|右前", utterance
r"副驾|副驾驶|乘客|乘员|右前", utterance
):
feature = "通风" if "通风" in thermal_middle.group(0) else "加热"
native_capability = (
@@ -864,6 +864,19 @@ class UnderstandingTranscriptProcessor:
while index < len(commands):
current = commands[index]
following = commands[index + 1] if index + 1 < len(commands) else None
if (
following is not None
and current.capability == following.capability
and current.target_scope == following.target_scope
and current.property_name == following.property_name
and current.property_name in {"seat.ventilation", "seat.heating"}
and "level" in current.qualifiers.get("implicit_parameters", ())
and "level" not in following.qualifiers.get("implicit_parameters", ())
):
obsolete_issues.update(current.validation_issues)
output.append(following)
index += 2
continue
if (
following is not None
and current.property_name == following.property_name == "ambient.color"
+641 -105
View File
@@ -3,6 +3,7 @@
from __future__ import annotations
import argparse
import hashlib
import hmac
import ipaddress
import json
@@ -14,6 +15,7 @@ import sys
import threading
import time
from collections.abc import Callable, Mapping
from concurrent.futures import ThreadPoolExecutor
from copy import deepcopy
from http import HTTPStatus
from http.cookies import CookieError, SimpleCookie
@@ -95,6 +97,7 @@ from cognitive_agent.auth import ( # noqa: E402
verify_password,
)
from cognitive_agent.infrastructure.can.access import ( # noqa: E402
HARDWARE_CLIENT_ID_PATTERN,
HardwareAccessRequiredError,
)
from cognitive_agent.infrastructure.can.gateway import CanGatewayError # noqa: E402
@@ -147,6 +150,25 @@ PUBLIC_OTA_DOWNLOAD_PATH = re.compile(
NON_DISPATCH_DISPLAY_SOURCES = frozenset({"mode_editor_compile"})
def _compatible_hardware_client_id(value: object) -> str | None:
"""Map legacy browser IDs onto the current opaque hardware-client contract.
Older cached consoles used UUIDs or identifiers containing underscores.
They cannot have a valid lease under the current controller contract, but
rejecting them before the access workflow runs leaves every hardware mode
unusable. A one-way, stable mapping lets those consoles request and reuse
an ordinary lease without granting access or exposing the legacy value.
"""
raw_value = str(value or "").strip().lower()
if not raw_value:
return None
if HARDWARE_CLIENT_ID_PATTERN.fullmatch(raw_value):
return raw_value
digest = hashlib.sha256(raw_value.encode("utf-8")).hexdigest()[:32]
return f"hwc-legacy-{digest}"
class VehicleDisplaySync:
"""Process-local snapshot consumed by passive vehicle display clients."""
@@ -272,6 +294,9 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
catalog_provider=extension.list_peripheral_capabilities,
executor=extension.execute_bound_infrared_action,
)
service.set_mode_scene_peripheral_catalog_provider(
extension.list_peripheral_capabilities
)
service.set_extension_control_handler(extension_control.submit)
service.set_extension_control_rejection_handler(
extension_control.reject_transport_message
@@ -361,7 +386,9 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
try:
self._json(
self.service.hardware_access.status(
str(query.get("client_id", [""])[0])
_compatible_hardware_client_id(
query.get("client_id", [""])[0]
)
)
)
except ValueError as exc:
@@ -478,7 +505,9 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
control_transport = str(
query.get("control_transport", ["wired_can"])[0]
)
hardware_client_id = str(query.get("hardware_client_id", [""])[0]) or None
hardware_client_id = _compatible_hardware_client_id(
query.get("hardware_client_id", [""])[0]
)
try:
self._json(
(
@@ -769,10 +798,8 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
utterance=utterance,
source=source,
)
hardware_client_id = (
str(payload["hardware_client_id"])
if payload.get("hardware_client_id")
else None
hardware_client_id = _compatible_hardware_client_id(
payload.get("hardware_client_id")
)
reasoning_effort = (
str(payload["reasoning_effort"])
@@ -1231,6 +1258,7 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
conversation_id: str | None,
execution_mode: str,
on_commands_validated: Callable[[dict[str, object]], None] | None = None,
update_display: bool = True,
) -> dict[str, object]:
"""Execute already validated infrared capabilities."""
@@ -1281,17 +1309,257 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
on_commands_validated=on_commands_validated,
)
result["provider_used"] = "infrared_local_rules"
self.vehicle_display_sync.update_extension(
action={
"type": "emit_sequence" if len(capabilities) > 1 else "emit",
"source": "main_orchestration_local_rule",
"capability_ids": [item["id"] for item in capabilities],
"slots": [item["slot"] for item in capabilities],
},
result=result,
)
if update_display:
self.vehicle_display_sync.update_extension(
action={
"type": "emit_sequence" if len(capabilities) > 1 else "emit",
"source": "main_orchestration_local_rule",
"capability_ids": [item["id"] for item in capabilities],
"slots": [item["slot"] for item in capabilities],
},
result=result,
)
return result
@staticmethod
def _run_parallel_control_branches(
can_branch: Callable[[], dict[str, object]],
peripheral_branch: Callable[[], dict[str, object]],
) -> tuple[dict[str, object], dict[str, object], dict[str, object]]:
"""Start two independent transports together and retain timing evidence."""
barrier = threading.Barrier(2)
timing_lock = threading.Lock()
starts: dict[str, float] = {}
durations: dict[str, float] = {}
def run(
name: str,
branch: Callable[[], dict[str, object]],
) -> dict[str, object]:
barrier.wait(timeout=5)
started = time.perf_counter()
with timing_lock:
starts[name] = started
try:
return branch()
finally:
with timing_lock:
durations[name] = round(
(time.perf_counter() - started) * 1000,
3,
)
total_started = time.perf_counter()
with ThreadPoolExecutor(
max_workers=2,
thread_name_prefix="can-ir-dispatch",
) as pool:
can_future = pool.submit(run, "can", can_branch)
peripheral_future = pool.submit(run, "peripheral", peripheral_branch)
can_result = can_future.result()
peripheral_result = peripheral_future.result()
started_values = list(starts.values())
timing = {
"started_skew_ms": round(
(max(started_values) - min(started_values)) * 1000,
3,
) if len(started_values) == 2 else None,
"total_ms": round(
(time.perf_counter() - total_started) * 1000,
3,
),
"branches": {
name: {"duration_ms": duration}
for name, duration in sorted(durations.items())
},
}
return can_result, peripheral_result, timing
@staticmethod
def _parallel_branch_succeeded(result: Mapping[str, object]) -> bool:
if str(result.get("status") or "") not in {
"succeeded",
"partially_succeeded",
"no_action",
}:
return False
if str(result.get("execution_outcome") or "") in {
"failed",
"failed_or_blocked",
}:
return False
validation = result.get("validation", [])
return not any(
isinstance(issue, Mapping) and issue.get("severity") == "error"
for issue in (
validation
if isinstance(validation, list)
else []
)
)
def _ensure_parallel_hardware_authorized(
self,
*,
execution_mode: str,
control_transport: str,
hardware_access_bypass: bool,
hardware_client_id: str | None,
) -> None:
"""Resolve local-bench access before an external branch can start.
Wireless MQTT is executed by the head unit and is deliberately outside
the local T2G/ZLG lease. Requiring a browser hardware client ID there
contradicts :meth:`SeatDemoService.process` and rejects every mixed
CAN/IR mode because wireless requests correctly omit that identifier.
"""
if (
execution_mode == "hardware"
and control_transport != WIRELESS_MQTT_TRANSPORT
and not hardware_access_bypass
):
self.service.hardware_access.ensure_authorized(hardware_client_id)
def _execute_can_and_peripheral_branches(
self,
*,
utterance: str,
entries: list[dict[str, object]],
execution_mode: str,
provider: str,
conversation_id: str | None,
can_execute: Callable[[], dict[str, object]],
can_preflight: Callable[[], dict[str, object]],
parallel_eligible: bool,
on_preflight_validated: Callable[[dict[str, object]], None] | None = None,
) -> dict[str, object]:
"""Preflight both branches, then dispatch hardware concurrently."""
if not entries:
return can_execute()
if not parallel_eligible or execution_mode != "hardware":
can_result = can_execute()
infrared_result = self._execute_local_peripheral_actions(
utterance,
entries,
provider=provider,
conversation_id=conversation_id,
execution_mode=execution_mode,
)
return self._merge_can_and_peripheral_results(
utterance,
can_result,
infrared_result,
entries,
)
can_preview, infrared_preview, preflight_timing = (
self._run_parallel_control_branches(
can_preflight,
lambda: self._execute_local_peripheral_actions(
utterance,
entries,
provider=provider,
conversation_id=None,
execution_mode="dry_run",
update_display=False,
),
)
)
if not (
self._parallel_branch_succeeded(can_preview)
and self._parallel_branch_succeeded(infrared_preview)
):
rejected = self._merge_can_and_peripheral_results(
utterance,
can_preview,
infrared_preview,
entries,
)
rejected.update(
{
"status": "validation_failed",
"success": False,
"execution_mode": "dry_run",
"hardware_execution_allowed": False,
"parallel_dispatch": {
"enabled": False,
"preflight_status": "failed",
"preflight": preflight_timing,
},
}
)
rejected["decision_summary"] = [
*list(rejected.get("decision_summary") or []),
"CAN 或外设预校验未通过;两个物理传输分支均未启动。",
]
return rejected
if on_preflight_validated is not None:
validated = self._merge_can_and_peripheral_results(
utterance,
can_preview,
infrared_preview,
entries,
)
validated["status"] = "processing"
validated["phase"] = "commands_validated"
validated["execution_mode"] = "hardware"
validated["parallel_dispatch"] = {
"enabled": False,
"preflight_status": "succeeded",
"preflight": preflight_timing,
}
on_preflight_validated(validated)
can_result, infrared_result, dispatch_timing = (
self._run_parallel_control_branches(
can_execute,
lambda: self._execute_local_peripheral_actions(
utterance,
entries,
provider=provider,
conversation_id=conversation_id,
execution_mode=execution_mode,
),
)
)
result = self._merge_can_and_peripheral_results(
utterance,
can_result,
infrared_result,
entries,
)
result["parallel_dispatch"] = {
"enabled": True,
"preflight_status": "succeeded",
"preflight": preflight_timing,
"dispatch": dispatch_timing,
}
result["decision_summary"] = [
*list(result.get("decision_summary") or []),
"CAN 与外设控制在完整预校验通过后并行启动。",
]
return result
@staticmethod
def _mixed_request_requires_transport_order(utterance: str) -> bool:
"""Keep explicit sequencing and delays out of concurrent dispatch."""
return bool(
re.search(
r"然后|随后|接着|之后|"
r"先.{0,120}?(?:再|然后|随后|接着)|"
r"(?:\d+(?:\.\d+)?|[一二三四五六七八九十两半]+)"
r"(?:毫秒|秒(?:钟)?|分钟|小时|ms(?![A-Za-z])|s(?![A-Za-z]))",
utterance,
flags=re.IGNORECASE,
)
)
@staticmethod
def _split_scene_peripheral_plan(
assistant_plan: Mapping[str, object],
@@ -1800,24 +2068,111 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
labels = metadata.setdefault("labels", {})
labels["route"] = "unified_can_infrared"
infrared_actions = [
{
"index": entry.get("index"),
"clause": entry.get("clause"),
"capability_id": capability.get("id"),
"device_id": capability.get("device_id"),
"device_name": capability.get("device_name"),
"action": capability.get("action"),
"slot": capability.get("slot"),
"parse_source": str(
capability.get("parse_source") or "local_rule"
),
"transport": str(infrared_result.get("control_transport") or "gscu_tcp"),
}
for entry in entries
for capability in [entry.get("capability")]
if isinstance(capability, dict)
]
entry_lookup: dict[tuple[str, object, str], dict[str, object]] = {}
for entry in entries:
capability = entry.get("capability") if isinstance(entry, dict) else None
if not isinstance(capability, dict):
continue
entry_lookup[
(
str(capability.get("device_id") or ""),
capability.get("slot"),
str(capability.get("action") or ""),
)
] = entry
extension_config = (infrared_result.get("function_configs") or {}).get(
"self_extension"
)
extension_spec = (
extension_config.get("spec")
if isinstance(extension_config, Mapping)
else None
)
compiled_operations = (
extension_spec.get("operations")
if isinstance(extension_spec, Mapping)
and isinstance(extension_spec.get("operations"), list)
else []
)
infrared_actions: list[dict[str, object]] = []
for operation_index, operation in enumerate(compiled_operations):
if not isinstance(operation, Mapping):
continue
target = operation.get("target")
parameters = operation.get("parameters")
if not isinstance(target, Mapping) or not isinstance(parameters, Mapping):
continue
device_id = str(target.get("device") or "")
slot = parameters.get("slot")
action_name = str(parameters.get("action") or "")
matched_entry = entry_lookup.get((device_id, slot, action_name), {})
matched_capability = (
matched_entry.get("capability")
if isinstance(matched_entry, dict)
else None
)
capability_id = str(operation.get("capability_id") or "") or str(
matched_capability.get("id")
if isinstance(matched_capability, Mapping)
else ""
)
auto_inserted = operation.get("auto_inserted") is True
infrared_actions.append(
{
"index": (
matched_entry.get("index")
if isinstance(matched_entry, dict) and matched_entry
else operation_index
),
"clause": (
matched_entry.get("clause")
if isinstance(matched_entry, dict) and matched_entry
else "自动前置:打开设备"
if auto_inserted
else action_name
),
"capability_id": capability_id,
"device_id": device_id,
"device_name": str(target.get("name") or device_id),
"action": action_name,
"slot": slot,
"parse_source": str(
operation.get("parse_source") or "local_rule"
),
**(
{
"auto_inserted": True,
"dependency": operation.get("dependency"),
}
if auto_inserted
else {}
),
"transport": str(
infrared_result.get("control_transport") or "gscu_tcp"
),
}
)
if not infrared_actions:
infrared_actions = [
{
"index": entry.get("index"),
"clause": entry.get("clause"),
"capability_id": capability.get("id"),
"device_id": capability.get("device_id"),
"device_name": capability.get("device_name"),
"action": capability.get("action"),
"slot": capability.get("slot"),
"parse_source": str(
capability.get("parse_source") or "local_rule"
),
"transport": str(
infrared_result.get("control_transport") or "gscu_tcp"
),
}
for entry in entries
for capability in [entry.get("capability")]
if isinstance(capability, dict)
]
preview_only = (
str(can_result.get("execution_mode") or "dry_run") != "hardware"
and str(infrared_result.get("execution_mode") or "dry_run")
@@ -2262,47 +2617,78 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
else None
)
entries = self._saved_mode_infrared_entries(profile)
can_result = self.service.execute_mode_profile_card(
str(getattr(profile, "id")),
conversation_id=conversation_id,
execution_mode=execution_mode,
approval_token=(
str(common_arguments["approval_token"])
if common_arguments.get("approval_token")
else None
),
vehicle_id=vehicle_id,
simulation_fault=str(
common_arguments.get("simulation_fault") or "normal"
),
hardware_client_id=(
str(common_arguments["hardware_client_id"])
if common_arguments.get("hardware_client_id")
else None
),
hardware_access_bypass=bool(
common_arguments.get("hardware_access_bypass", True)
),
control_transport=str(
common_arguments.get("control_transport") or "wired_can"
),
mode_id = str(getattr(profile, "id"))
simulation_fault = str(common_arguments.get("simulation_fault") or "normal")
control_transport = str(
common_arguments.get("control_transport") or "wired_can"
)
if not entries:
can_result["utterance"] = utterance
return can_result
infrared_result = self._execute_local_peripheral_actions(
utterance,
entries,
hardware_client_id = _compatible_hardware_client_id(
common_arguments.get("hardware_client_id")
)
hardware_access_bypass = bool(
common_arguments.get("hardware_access_bypass", True)
)
command_flow = getattr(profile, "command_flow", None)
execution_steps = (
command_flow.get("execution_steps", [])
if isinstance(command_flow, Mapping)
else []
)
parallel_eligible = bool(
getattr(profile, "schedule", None) is None
and not execution_steps
)
if entries and parallel_eligible:
self._ensure_parallel_hardware_authorized(
execution_mode=execution_mode,
control_transport=control_transport,
hardware_access_bypass=hardware_access_bypass,
hardware_client_id=hardware_client_id,
)
def execute_can() -> dict[str, object]:
return self.service.execute_mode_profile_card(
mode_id,
conversation_id=conversation_id,
execution_mode=execution_mode,
approval_token=(
str(common_arguments["approval_token"])
if common_arguments.get("approval_token")
else None
),
vehicle_id=vehicle_id,
simulation_fault=simulation_fault,
hardware_client_id=hardware_client_id,
hardware_access_bypass=hardware_access_bypass,
control_transport=control_transport,
)
def preflight_can() -> dict[str, object]:
return self.service.execute_mode_profile_card(
mode_id,
conversation_id=None,
execution_mode="dry_run",
approval_token=None,
vehicle_id=vehicle_id,
simulation_fault="normal",
hardware_client_id=None,
hardware_access_bypass=True,
control_transport=control_transport,
)
result = self._execute_can_and_peripheral_branches(
utterance=utterance,
entries=entries,
execution_mode=execution_mode,
provider="saved_mode_command_flow",
conversation_id=conversation_id,
execution_mode=execution_mode,
)
result = self._merge_can_and_peripheral_results(
utterance,
can_result,
infrared_result,
entries,
can_execute=execute_can,
can_preflight=preflight_can,
parallel_eligible=parallel_eligible,
)
if not entries:
result["utterance"] = utterance
return result
result["provider_used"] = "saved_mode_command_flow"
result["orchestration_route"] = "saved_mode_command_flow"
result["mode_profile"] = profile.public_dict()
@@ -2516,6 +2902,125 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
)
return result
parallel_mixed_control = bool(
path != "/api/requests/preview"
and execution_mode == "hardware"
and entries
and residual
and not unresolved
and not denied
and not self._mixed_request_requires_transport_order(utterance)
)
if parallel_mixed_control:
can_arguments = {
**common_arguments,
"utterance": residual,
"_cloud_instruction_context": natural_language_routing,
}
def execute_can() -> dict[str, object]:
return self.service.process(
**can_arguments,
_on_commands_validated=None,
)
preflight_arguments = {
**can_arguments,
"execution_mode": "dry_run",
"approval_token": None,
"hardware_client_id": None,
"hardware_access_bypass": True,
}
def preflight_can() -> dict[str, object]:
return self.service.preview_native_can(
**preflight_arguments,
on_commands_validated=None,
)
def publish_parallel_validation(
validated: dict[str, object],
) -> None:
nonlocal peripheral_validation_elapsed_ms
if callback is None:
return
elapsed_ms = round(
(time.perf_counter() - local_stage_started) * 1000,
3,
)
peripheral_validation_elapsed_ms = elapsed_ms
validated["natural_language_routing"] = deepcopy(
natural_language_routing
)
validated.update(
{
"local_control_budget_ms": LOCAL_CONTROL_BUDGET_MS,
"local_control_elapsed_ms": elapsed_ms,
"local_control_within_budget": (
elapsed_ms < LOCAL_CONTROL_BUDGET_MS
),
}
)
callback(validated)
hardware_client_id = _compatible_hardware_client_id(
common_arguments.get("hardware_client_id")
)
hardware_access_bypass = bool(
common_arguments.get("hardware_access_bypass", True)
)
self._ensure_parallel_hardware_authorized(
execution_mode=execution_mode,
control_transport=str(
common_arguments.get("control_transport") or "wired_can"
),
hardware_access_bypass=hardware_access_bypass,
hardware_client_id=hardware_client_id,
)
result = self._execute_can_and_peripheral_branches(
utterance=utterance,
entries=entries,
execution_mode=execution_mode,
provider=provider,
conversation_id=conversation_id,
can_execute=execute_can,
can_preflight=preflight_can,
parallel_eligible=True,
on_preflight_validated=publish_parallel_validation,
)
routed = result.get("natural_language_routing")
if isinstance(routed, dict):
natural_language_routing = deepcopy(routed)
result["natural_language_routing"] = deepcopy(
natural_language_routing
)
if peripheral_validation_elapsed_ms is not None:
result.update(
{
"local_control_budget_ms": LOCAL_CONTROL_BUDGET_MS,
"local_control_elapsed_ms": (
peripheral_validation_elapsed_ms
),
"local_control_within_budget": (
peripheral_validation_elapsed_ms
< LOCAL_CONTROL_BUDGET_MS
),
}
)
for key in (
"parsed_clauses",
"resolved_clauses",
"unresolved_clauses",
):
value = natural_language_routing.get(key)
if isinstance(value, list):
result[key] = deepcopy(value)
result = self._apply_nfc_command_denials(result, denied)
self.service.persist_composed_result(result)
if callback is not None:
callback(result)
return result
# Compile, publish and execute every deterministic IR action before
# any hosted planner is contacted.
if entries:
@@ -2892,10 +3397,8 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
approval_token=os.getenv(
"COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN"
),
hardware_client_id=(
str(payload["hardware_client_id"])
if payload.get("hardware_client_id")
else None
hardware_client_id=_compatible_hardware_client_id(
payload.get("hardware_client_id")
),
hardware_access_bypass=self._hardware_access_bypass_allowed(),
reasoning_effort=(
@@ -3006,8 +3509,8 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
history_payload = payload.get("history")
history = history_payload if isinstance(history_payload, list) else []
source = str(payload.get("client_type") or "web_console_assistant")
hardware_client_id = (
str(payload["hardware_client_id"]) if payload.get("hardware_client_id") else None
hardware_client_id = _compatible_hardware_client_id(
payload.get("hardware_client_id")
)
reasoning_effort = (
str(payload["reasoning_effort"])
@@ -3815,10 +4318,8 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
if payload.get("conversation_id")
else None
)
hardware_client_id = (
str(payload["hardware_client_id"])
if payload.get("hardware_client_id")
else None
hardware_client_id = _compatible_hardware_client_id(
payload.get("hardware_client_id")
)
self.vehicle_display_sync.update_task(
status="running",
@@ -3828,39 +4329,69 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
# Validate the complete saved flow against the durable NFC binding
# before any transport is allowed to execute.
entries = self._saved_mode_infrared_entries(profile)
can_result = self.service.execute_mode_profile_card(
mode_id,
conversation_id=conversation_id,
execution_mode=execution_mode,
approval_token=os.getenv(
"COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN"
),
vehicle_id=vehicle_id,
simulation_fault=str(payload.get("simulation_fault", "normal")),
hardware_client_id=hardware_client_id,
hardware_access_bypass=self._hardware_access_bypass_allowed(),
control_transport=control_transport,
simulation_fault = str(payload.get("simulation_fault", "normal"))
hardware_access_bypass = self._hardware_access_bypass_allowed()
command_flow = getattr(profile, "command_flow", None)
execution_steps = (
command_flow.get("execution_steps", [])
if isinstance(command_flow, Mapping)
else []
)
if entries:
infrared_result = self._execute_local_peripheral_actions(
utterance,
entries,
provider="saved_mode_command_flow",
parallel_eligible = bool(
getattr(profile, "schedule", None) is None
and not execution_steps
)
if entries and parallel_eligible:
self._ensure_parallel_hardware_authorized(
execution_mode=execution_mode,
control_transport=control_transport,
hardware_access_bypass=hardware_access_bypass,
hardware_client_id=hardware_client_id,
)
def execute_can() -> dict[str, object]:
return self.service.execute_mode_profile_card(
mode_id,
conversation_id=conversation_id,
execution_mode=execution_mode,
approval_token=os.getenv(
"COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN"
),
vehicle_id=vehicle_id,
simulation_fault=simulation_fault,
hardware_client_id=hardware_client_id,
hardware_access_bypass=hardware_access_bypass,
control_transport=control_transport,
)
result = self._merge_can_and_peripheral_results(
utterance,
can_result,
infrared_result,
entries,
def preflight_can() -> dict[str, object]:
return self.service.execute_mode_profile_card(
mode_id,
conversation_id=None,
execution_mode="dry_run",
approval_token=None,
vehicle_id=vehicle_id,
simulation_fault="normal",
hardware_client_id=None,
hardware_access_bypass=True,
control_transport=control_transport,
)
result = self._execute_can_and_peripheral_branches(
utterance=utterance,
entries=entries,
execution_mode=execution_mode,
provider="saved_mode_command_flow",
conversation_id=conversation_id,
can_execute=execute_can,
can_preflight=preflight_can,
parallel_eligible=parallel_eligible,
)
if entries:
result["provider_used"] = "saved_mode_command_flow"
result["orchestration_route"] = "saved_mode_command_flow"
result["mode_profile"] = profile.public_dict()
self.service.persist_composed_result(result)
else:
result = can_result
self.vehicle_display_sync.update_task(
status=str(result.get("status") or "succeeded"),
utterance=utterance,
@@ -4035,6 +4566,9 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
self._json(result)
return
if path == "/api/extension/vehicle/execute":
payload["hardware_client_id"] = _compatible_hardware_client_id(
payload.get("hardware_client_id")
)
result = self.extension.execute_vehicle(
payload,
self.service,
@@ -4362,7 +4896,9 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
return
try:
payload = self._read_json_body(max_bytes=4096)
client_id = str(payload.get("client_id", ""))
client_id = _compatible_hardware_client_id(
payload.get("client_id")
)
if path == "/api/hardware/access/request":
status = self.service.hardware_access.request_access(
client_id,