Merge origin/feature/N7Web into feature/N7Web

Integrate the remote firmware workbench, OTA delivery, and timed mode schedules while preserving the local N7 scene and runtime changes.
This commit is contained in:
Miayui
2026-09-17 12:46:51 +08:00
135 changed files with 30180 additions and 513 deletions
+442 -109
View File
@@ -49,6 +49,16 @@ from cognitive_agent.application.direct_can_control import ( # noqa: E402
build_direct_can_intent,
)
from cognitive_agent.application.domain_runtime import ConfiguredDomainRuntime # noqa: E402
from cognitive_agent.application.mode_creation_seat_policy import ( # noqa: E402
ModeCreationSeatPolicyError,
creation_seat_scopes,
project_seed_operations,
project_seed_plan,
project_seed_schedule,
)
from cognitive_agent.application.mode_device_additions import ( # noqa: E402
mode_device_additions,
)
from cognitive_agent.application.mode_profile import ( # noqa: E402
ModeProfile,
ModeProfileRegistry,
@@ -57,6 +67,14 @@ from cognitive_agent.application.mode_profile import ( # noqa: E402
from cognitive_agent.application.mode_scene_ingress import ( # noqa: E402
ModeSceneIngressConsumer,
)
from cognitive_agent.application.mode_profile_intent import ( # noqa: E402
is_mode_profile_query,
mode_creation_source_name,
normalize_mode_profile_name,
parse_mode_profile_create,
parse_mode_profile_delete,
parse_mode_profile_update,
)
from cognitive_agent.application.mode_scene_catalog import ( # noqa: E402
DEFAULT_N7_SCENE_SERVER_ID,
DEFAULT_N7_SCENE_SERVER_NAME,
@@ -64,13 +82,8 @@ from cognitive_agent.application.mode_scene_catalog import ( # noqa: E402
ModeSceneMqttBurstPublisher,
build_mode_scene_catalog,
)
from cognitive_agent.application.mode_profile_intent import ( # noqa: E402
is_mode_profile_query,
normalize_mode_profile_name,
parse_mode_profile_create,
parse_mode_profile_delete,
parse_mode_profile_update,
)
from cognitive_agent.application.mode_schedule import ModeSchedule, ModeScheduleRunner # noqa: E402
from cognitive_agent.application.mode_schedule_service import ModeScheduleServiceMixin # noqa: E402
from cognitive_agent.application.model_plan_adapter import ( # noqa: E402
APPLY_PRESET_CAPABILITY,
DYNAMIC_MODE_STOP_CAPABILITY,
@@ -103,6 +116,10 @@ from cognitive_agent.application.native_can_runtime import ( # noqa: E402
NativeCanRuntime,
)
from cognitive_agent.application.scene_templates import SceneTemplateRegistry # noqa: E402
from cognitive_agent.application.schedule_intent import ( # noqa: E402
is_schedule_stop,
needs_mode_schedule,
)
from cognitive_agent.application.seat_calibration import position_pct_to_angle # noqa: E402
from cognitive_agent.application.semantic_ir import ( # noqa: E402
apply_language_safety_guards,
@@ -356,8 +373,9 @@ DYNAMIC_MODE_WORDS = (
)
class SeatDemoService:
class SeatDemoService(ModeScheduleServiceMixin):
def __init__(self, repository_root: Path = REPOSITORY_ROOT):
self.mode_schedule_runner = ModeScheduleRunner()
self.root = repository_root
self.domain_root = repository_root / "domains" / "seat"
self.contracts = ContractValidator(repository_root)
@@ -1190,7 +1208,7 @@ class SeatDemoService:
resolved_vehicle_id = vehicle_id or self.vehicle_profiles.default().id
profile = self.mode_profiles.get_profile(mode_id, resolved_vehicle_id)
if not profile.command_flow or not profile.command_flow.get("plans"):
if profile.schedule is None and (not profile.command_flow or not profile.command_flow.get("plans")):
raise ValueError("该模式尚未生成可直接执行的控制指令流。")
result = self.process(
profile.instruction or f"进入{profile.name}",
@@ -1205,6 +1223,8 @@ class SeatDemoService:
control_transport=control_transport,
_direct_mode_profile_id=profile.id,
)
if profile.schedule is not None:
return result
public_profile = profile.public_dict()
result["provider_used"] = "saved_mode_command_flow"
result["utterance"] = profile.name
@@ -1323,7 +1343,7 @@ class SeatDemoService:
):
raise ValueError("功能模式草稿未通过安全校验,请重新生成。")
return {
trial = {
"request_id": clean_request_id,
"status": "succeeded",
"execution_mode": "dry_run",
@@ -1336,6 +1356,14 @@ class SeatDemoService:
"validation": deepcopy(validation),
"trial_source": "server_validated_preview_artifact",
}
schedule = self._validated_schedule_artifact(source_result)
if schedule is not None:
trial["schedule"] = schedule.to_dict()
trial["schedule_execution"] = self.mode_schedule_runner.simulate(
schedule, lambda phase: {"status": "succeeded", "phase_id": phase.id,
"operations": deepcopy(list(phase.operations))},
)
return trial
def _persist_mode_profile_card(
self,
@@ -1534,6 +1562,11 @@ class SeatDemoService:
# generation workflow.
"execution_steps": [],
}
schedule = self._validated_schedule_artifact(source_result)
if profile is not None and profile.schedule is not None and schedule is None:
raise ValueError("动态模式更新必须保留完整时间轴,请按阶段重新生成预览。")
if schedule is not None:
operations = [deepcopy(op) for phase in schedule.phases for op in phase.operations]
saved = self.mode_profiles.save_mode(
clean_name,
operations,
@@ -1553,6 +1586,7 @@ class SeatDemoService:
upstream_origin=(
profile.upstream_origin if profile is not None else ""
),
schedule=schedule,
expected_revision=draft_existing_mode_revision or None,
create_only=not bool(clean_mode_id),
)
@@ -1753,6 +1787,7 @@ class SeatDemoService:
self._vehicle_mqtt_transport.start()
def close(self) -> None:
self.mode_schedule_runner.close()
if self._vehicle_state_monitor is not None:
self._vehicle_state_monitor.close()
self._mode_scene_mqtt_publisher.close()
@@ -2761,6 +2796,32 @@ class SeatDemoService:
or mode_profile_query
):
execution_mode = "dry_run"
schedule_requested = "模式" in utterance or _direct_mode_profile_id or is_schedule_stop(utterance)
schedule_vehicle_id = vehicle_id or (self.vehicle_profiles.default().id if schedule_requested else "")
if execution_mode not in {"dry_run", "simulated_hardware", "hardware"}:
raise ValueError(f"unsupported execution mode: {execution_mode}")
if is_schedule_stop(utterance):
return self._cancel_mode_schedule(schedule_vehicle_id, conversation_id, utterance, execution_mode)
schedule_profile = (self.mode_profiles.get_profile(_direct_mode_profile_id, schedule_vehicle_id)
if _direct_mode_profile_id else None)
if schedule_requested and schedule_profile is None and not (draft_update or mode_delete_name or mode_profile_query or parse_mode_profile_create(utterance)):
candidates = self.mode_profiles.match_text(utterance, vehicle_id=schedule_vehicle_id)
if len(candidates) == 1 and candidates[0].schedule is not None:
aliases = "|".join(re.escape(name) for name in (candidates[0].name, *candidates[0].aliases))
if re.fullmatch(
r"(?:(?:请|麻烦|帮我|帮忙|给我|我想|我要|想要)\s*){0,4}"
r"(?:(?:主驾|副驾|驾驶位|副驾驶|主驾驶)(?:座椅)?\s*)?"
r"(?:进入|启动|执行|开启|启用|运行|打开|调用|应用|使用|切换到)\s*"
rf"(?:{aliases})(?:吧|一下)?[。!!\s]*", utterance.strip(),
):
schedule_profile = candidates[0]
if schedule_profile is not None and schedule_profile.schedule is not None:
return self._run_mode_schedule(
schedule_profile, utterance=utterance, conversation_id=conversation_id,
execution_mode=execution_mode, approval_token=approval_token, vehicle_id=schedule_vehicle_id,
simulation_fault=simulation_fault, hardware_client_id=hardware_client_id,
hardware_access_bypass=hardware_access_bypass, control_transport=control_transport,
)
if execution_mode != "hardware":
return self._process_once(
utterance,
@@ -2899,6 +2960,8 @@ 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_creation_scopes: frozenset[str] | None = None,
mode_profile_draft_context: dict[str, Any] | None = None,
cloud_instruction_context: dict[str, Any] | None = None,
) -> dict[str, Any]:
@@ -3158,6 +3221,8 @@ class SeatDemoService:
mode_requests, n7_transfer_actions = (
self._mode_requests_and_n7_transfers(direct_mode_profile)
)
if mode_creation_scopes is not None:
mode_requests = project_seed_operations(mode_requests, mode_creation_scopes)
detected_domains = list(
dict.fromkeys(
str(item.get("domain") or "")
@@ -3253,9 +3318,13 @@ class SeatDemoService:
model_parse_source=model_parse_source,
assistant_plan=assistant_plan,
allow_scene_composition=allow_scene_composition,
scene_scope_utterance=scene_scope_utterance,
cloud_instruction_context=cloud_instruction_context,
)
dynamic_mode_stop = self._is_dynamic_mode_stop_request(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)
)
if dynamic_mode_stop:
parsed = self._apply_dynamic_mode_stop_request(parsed, utterance)
# Reuse the same conservative clause segmentation for the final
@@ -3263,7 +3332,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 = (
@@ -3345,7 +3419,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=(
@@ -4413,6 +4492,16 @@ class SeatDemoService:
) -> dict[str, Any]:
if not matched_modes:
return parsed
if any(mode.schedule is not None for mode in matched_modes):
rejected = deepcopy(parsed)
for key in ("actions", "domain_actions", "native_can_actions", "preset_actions",
"preset_events", "deferred_requests", "model_requests"):
rejected[key] = []
rejected["ambiguities"] = [{
"field": "mode_profile",
"question": "动态模式必须通过时间轴执行,请独立输入“进入模式名称”;不会将各阶段压平执行。",
}]
return rejected
if self._contains_seat_preset_request(parsed):
return parsed
if len(matched_modes) > 1:
@@ -4496,6 +4585,19 @@ class SeatDemoService:
mode_requests, n7_transfer_actions = (
self._mode_requests_and_n7_transfers(matched_profile)
)
selected_seat_scope = self._explicit_mode_seat_selection(
matched_profile, utterance,
)
if selected_seat_scope is not None:
# A cabin profile can carry independent comfort operations for both
# front seats. An explicit single-seat invocation selects only that
# seat; it must not authorize its sibling or mutate the stored mode.
mode_requests = [
request for request in mode_requests
if request["domain"] != "seat"
or self._normalized_mode_seat_scope(request["target_scope"])
in selected_seat_scope
]
detected_domains = []
for request in mode_requests:
if request["domain"] not in detected_domains:
@@ -4516,6 +4618,24 @@ class SeatDemoService:
context,
vehicle_profile,
)
# Native-only comfort capabilities are represented as deferred items by
# the generic adapter. A verified stored mode must materialize them too;
# otherwise a successful named-mode run silently drops its driver seat.
mode_native_actions = []
for item in mode_parsed.get("deferred_requests", []):
if item.get("execution_backend") != "native_can":
continue
action = {
"domain": item["domain"],
"capability": item["capability"],
"target": item["target_scope"],
"parameters": deepcopy(item.get("parameters", {})),
"operation": item.get("desired_change", "set"),
"reason": item.get("reason", ""),
"execution_backend": "native_can",
}
if self.native_can_runtime.supports(action):
mode_native_actions.append(action)
parsed_has_explicit_request = bool(
parsed.get("actions")
or parsed.get("domain_actions")
@@ -4532,6 +4652,10 @@ class SeatDemoService:
mode_parsed.get("domain_actions", []),
parsed.get("domain_actions", []),
)
merged["native_can_actions"] = self._merge_control_actions(
mode_native_actions,
parsed.get("native_can_actions", []),
)
for key in (
"preset_actions",
"preset_events",
@@ -4583,13 +4707,35 @@ class SeatDemoService:
merged["_saved_mode_local_peripheral"] = True
return merged
@staticmethod
@classmethod
def _explicit_mode_target_conflict(
cls,
profile: ModeProfile,
utterance: str,
) -> tuple[str, tuple[str, ...]] | None:
"""Reject a named mode when its persisted seat scope contradicts text."""
requested, normalized_scopes = cls._explicit_mode_seat_scopes(profile, utterance)
if requested is None or requested == normalized_scopes:
return None
if cls._explicit_mode_seat_selection(profile, utterance) is not None:
return None
return ",".join(sorted(requested)), tuple(sorted(normalized_scopes))
@staticmethod
def _normalized_mode_seat_scope(scope: str) -> str:
return (
"driver" if scope == "front_left" else
"passenger" if scope == "front_right" else
"rear" if scope in {"rear", "rear_left", "rear_right"} else scope
)
@classmethod
def _explicit_mode_seat_scopes(
cls,
profile: ModeProfile,
utterance: str,
) -> tuple[frozenset[str] | None, set[str]]:
aliases = (profile.id, profile.name, *profile.aliases)
mode_clause = next(
(
@@ -4605,23 +4751,34 @@ class SeatDemoService:
utterance,
)
requested = resolve_explicit_seat_scopes(
mode_clause,
mode_clause.replace("主副驾", "主驾和副驾"),
assume_seat_context=True,
)
if requested is None:
return None
normalized_scopes = {
"driver" if operation.target_scope == "front_left" else
"passenger" if operation.target_scope == "front_right" else
"rear" if operation.target_scope in {"rear", "rear_left", "rear_right"} else
operation.target_scope
cls._normalized_mode_seat_scope(operation.target_scope)
for operation in profile.operations
if operation.domain == "seat"
}
if requested == normalized_scopes:
return requested, normalized_scopes
@classmethod
def _explicit_mode_seat_selection(
cls,
profile: ModeProfile,
utterance: str,
) -> frozenset[str] | None:
"""Scope a dual-seat mode invocation, never mode creation or editing."""
requested, supported = cls._explicit_mode_seat_scopes(profile, utterance)
if (
requested is None
or len(requested) != 1
or supported != {"driver", "passenger"}
or not requested.issubset(supported)
or cls.is_mode_profile_configuration_request(utterance)
or not re.search(r"进入|开启|启动|执行|启用|运行|切换|打开|调用|应用|使用", utterance)
):
return None
return ",".join(sorted(requested)), tuple(sorted(normalized_scopes))
return requested
@staticmethod
def _apply_implicit_mode_target_policy(
@@ -4954,7 +5111,9 @@ class SeatDemoService:
for issue in validation
):
raise ValueError("待保存功能模式草稿没有通过完整安全校验。")
operations = self._preview_mode_operations(source_result)
schedule = self._validated_schedule_artifact(source_result)
operations = ([deepcopy(op) for phase in schedule.phases for op in phase.operations]
if schedule is not None else self._preview_mode_operations(source_result))
operation_count = draft.get("operation_count")
if (
not isinstance(operation_count, int)
@@ -5035,6 +5194,17 @@ class SeatDemoService:
"confidence": 1.0,
}
def _mode_device_default_operations(
self,
mode_name: str,
) -> list[dict[str, Any]]:
"""Return registered static-scene defaults for a bounded device append."""
template = self.scene_templates.resolve_exact_name(mode_name)
if template is None or template.schedule is not None:
return []
return [operation.to_yaml() for operation in template.operations]
@staticmethod
def _mode_update_target_is_explicit(change_text: str, target: str) -> bool:
patterns = {
@@ -5084,79 +5254,99 @@ class SeatDemoService:
vehicle_id=vehicle_profile.id,
)
)
if source_draft.get("schedule") is not None:
from cognitive_agent.application.mode_schedule_edit import revise_schedule
return revise_schedule(
self, ModeSchedule.from_dict(source_draft["schedule"]),
change_text=change_text, utterance=utterance, name=mode_name,
conversation_id=conversation_id, vehicle_profile=vehicle_profile,
context=context, control_transport=control_transport,
source_draft=source_draft,
)
base_operations = self._absolute_mode_operations(source_operations)
if not change_text:
raise ValueError("请说明要调整的设备和明确目标值。")
parseable_change_text = re.sub(
r"改为|改成|变更为|换成|变成|变为",
"调整为",
registered_additions = mode_device_additions(
change_text,
base_operations,
self._mode_device_default_operations(mode_name),
)
unresolved_change_clauses = [
clause
for clause, _parsed, status in self._local_clause_results(
parseable_change_text,
context,
if registered_additions is not None:
normalized_changes = self._absolute_mode_operations(
registered_additions
)
if status != "complete"
]
if unresolved_change_clauses and assistant_plan is None:
raise ValueError(
"调整说明包含未完整识别的内容:"
+ ";".join(unresolved_change_clauses)
else:
parseable_change_text = re.sub(
r"改为|改成|变更为|换成|变成|变为",
"调整为",
change_text,
)
change_result = self._process_once(
parseable_change_text,
provider,
conversation_id,
"dry_run",
approval_token,
vehicle_profile.id,
simulation_fault,
control_transport=control_transport,
assistant_plan=assistant_plan,
allow_fuzzy_mode_profile_match=False,
)
change_operations = self._absolute_mode_operations(
self._preview_mode_operations(change_result)
)
if (
change_result.get("status") != "succeeded"
or not change_operations
or change_result.get("supplement_required") is True
or bool(change_result.get("unresolved_clauses"))
or bool(change_result.get("deferred_requests"))
):
raise ValueError("没有从调整说明中得到完整、可复用的设备目标。")
validation = change_result.get("validation")
if isinstance(validation, list) and any(
isinstance(issue, dict) and issue.get("severity") == "error"
for issue in validation
):
raise ValueError("调整动作没有通过车辆能力或安全校验。")
normalized_changes: list[dict[str, Any]] = []
for change in change_operations:
same_capability = [
item
for item in base_operations
if item["domain"] == change["domain"]
and item["capability"] == change["capability"]
]
normalized = deepcopy(change)
if len(same_capability) == 1 and not self._mode_update_target_is_explicit(
change_text,
str(change["target_scope"]),
):
normalized["target_scope"] = same_capability[0]["target_scope"]
elif len(same_capability) > 1 and not self._mode_update_target_is_explicit(
change_text,
str(change["target_scope"]),
):
raise ValueError(
f"“{mode_name}”中该设备能力包含多个目标,请明确主驾、副驾或具体区域。"
unresolved_change_clauses = [
clause
for clause, _parsed, status in self._local_clause_results(
parseable_change_text,
context,
)
normalized_changes.append(normalized)
if status != "complete"
]
if unresolved_change_clauses and assistant_plan is None:
raise ValueError(
"调整说明包含未完整识别的内容:"
+ ";".join(unresolved_change_clauses)
)
change_result = self._process_once(
parseable_change_text,
provider,
conversation_id,
"dry_run",
approval_token,
vehicle_profile.id,
simulation_fault,
control_transport=control_transport,
assistant_plan=assistant_plan,
allow_fuzzy_mode_profile_match=False,
)
change_operations = self._absolute_mode_operations(
self._preview_mode_operations(change_result)
)
if (
change_result.get("status") != "succeeded"
or not change_operations
or change_result.get("supplement_required") is True
or bool(change_result.get("unresolved_clauses"))
or bool(change_result.get("deferred_requests"))
):
raise ValueError("没有从调整说明中得到完整、可复用的设备目标。")
validation = change_result.get("validation")
if isinstance(validation, list) and any(
isinstance(issue, dict) and issue.get("severity") == "error"
for issue in validation
):
raise ValueError("调整动作没有通过车辆能力或安全校验。")
normalized_changes = []
for change in change_operations:
same_capability = [
item
for item in base_operations
if item["domain"] == change["domain"]
and item["capability"] == change["capability"]
]
normalized = deepcopy(change)
if len(same_capability) == 1 and not self._mode_update_target_is_explicit(
change_text,
str(change["target_scope"]),
):
normalized["target_scope"] = same_capability[0]["target_scope"]
elif len(same_capability) > 1 and not self._mode_update_target_is_explicit(
change_text,
str(change["target_scope"]),
):
raise ValueError(
f"“{mode_name}”中该设备能力包含多个目标,请明确主驾、副驾或具体区域。"
)
normalized_changes.append(normalized)
change_by_key = {
(
@@ -5433,6 +5623,14 @@ class SeatDemoService:
if profile is None:
self._persist(result)
return result
if profile.schedule is not None:
from cognitive_agent.application.mode_schedule_edit import update_saved_schedule
return update_saved_schedule(
self, profile, change_text=change_text, utterance=utterance,
conversation_id=conversation_id, vehicle_profile=vehicle_profile,
context=context, result=result,
)
if not change_text:
result["status"] = "needs_confirmation"
result["validation"] = [
@@ -5446,27 +5644,64 @@ class SeatDemoService:
self._persist(result)
return result
parsed, provider_used, provider_notice = self._interpret(
registered_additions = mode_device_additions(
change_text,
provider,
context,
vehicle_profile,
[operation.to_yaml() for operation in profile.operations],
self._mode_device_default_operations(mode_name),
)
parsed = apply_language_safety_guards(parsed, change_text)
parsed = self._partition_native_actions(parsed)
result["provider_used"] = f"mode_profile_registry+{provider_used}"
result["decision_summary"].extend(parsed.get("decision_summary", []))
if provider_notice:
result["decision_summary"].append(provider_notice)
result["deferred_requests"] = parsed.get("deferred_requests", [])
result["model_requests"] = parsed.get("model_requests", [])
if registered_additions is not None:
parsed = {
"decision_summary": [
"已从只读场景模板载入缺失设备的登记默认动作。"
],
"deferred_requests": [],
"model_requests": [],
}
result["provider_used"] = (
"mode_profile_registry+registered_scene_template"
)
result["decision_summary"].extend(parsed["decision_summary"])
result["deferred_requests"] = []
result["model_requests"] = []
mode_operations = self._absolute_mode_operations(
registered_additions
)
source_actions = [
{
"domain": operation["domain"],
"capability": operation["capability"],
"target": operation["target_scope"],
"operation": operation.get("operation", "set"),
"parameters": deepcopy(operation.get("parameters", {})),
"reason": operation.get(
"reason",
"从登记场景模板增补的设备默认动作。",
),
}
for operation in mode_operations
]
else:
parsed, provider_used, provider_notice = self._interpret(
change_text,
provider,
context,
vehicle_profile,
)
parsed = apply_language_safety_guards(parsed, change_text)
parsed = self._partition_native_actions(parsed)
result["provider_used"] = f"mode_profile_registry+{provider_used}"
result["decision_summary"].extend(parsed.get("decision_summary", []))
if provider_notice:
result["decision_summary"].append(provider_notice)
result["deferred_requests"] = parsed.get("deferred_requests", [])
result["model_requests"] = parsed.get("model_requests", [])
source_actions = [
*parsed.get("actions", []),
*parsed.get("domain_actions", []),
*parsed.get("native_can_actions", []),
]
mode_operations = self._mode_operations_from_actions(source_actions)
source_actions = [
*parsed.get("actions", []),
*parsed.get("domain_actions", []),
*parsed.get("native_can_actions", []),
]
mode_operations = self._mode_operations_from_actions(source_actions)
mode_operations, ambiguous_targets = self._inherit_mode_update_targets(
profile,
mode_operations,
@@ -6100,6 +6335,25 @@ class SeatDemoService:
result["scene_config"] = None
result["execution_report"] = None
def _mode_creation_seat_failure(
self, utterance: str, conversation_id: str, vehicle: VehicleProfile, reason: str,
) -> dict[str, Any]:
result = {
"request_id": f"req-{datetime.now(UTC).strftime('%Y%m%d%H%M%S')}-{uuid.uuid4().hex}",
"utterance": utterance, "conversation_id": conversation_id,
"vehicle": vehicle.public_dict(), "status": "validation_failed",
"execution_mode": "dry_run", "hardware_execution_allowed": False,
"orchestration_route": "mode_profile_create_preview", "trace_events": [],
"validation": [{"severity": "error", "code": "MODE_SEAT_SCOPE_UNAVAILABLE",
"path": "mode_profile_draft.seat_scope", "message": reason}],
"mode_creation_failure": {"code": "MODE_SEAT_SCOPE_UNAVAILABLE", "reason": reason,
"next_step": "请选择该模式实际支持的座位,或改用当前座位支持的舒适功能"},
"decision_summary": [reason, "没有保存模式或下发控制。"],
}
self._scrub_failed_scene_preview(result)
self._persist(result)
return result
def _process_mode_profile_create(
self,
utterance: str,
@@ -6134,6 +6388,14 @@ class SeatDemoService:
):
raise ValueError("功能模式名称与创建请求不一致。")
requested_instruction = create_parts[1] if create_parts is not None else ""
selected_seats = creation_seat_scopes(utterance) if source_draft is None else None
if selected_seats is not None and "unsupported" in selected_seats and re.search(
r"座椅|座位|靠背|椅背|腿托|脚托|坐垫|坐盆|按摩|零重力", utterance
):
return self._mode_creation_seat_failure(
utterance, conversation_id, vehicle_profile,
"当前模式只支持明确的主驾或副驾座椅,不会将未支持的座位替换为其他座位。",
)
raw_plan_metadata = (
assistant_plan.get("metadata")
if isinstance(assistant_plan, dict)
@@ -6160,6 +6422,7 @@ class SeatDemoService:
or requested_instruction
or assistant_plan.get("requests")
!= candidate_template.plan().get("requests")
or assistant_plan.get("schedule") != candidate_template.plan().get("schedule")
):
raise ValueError("内置常用模式模板已更新,请重新提交。")
scene_template = candidate_template
@@ -6190,6 +6453,10 @@ class SeatDemoService:
if not requested_instruction and source_draft is None
else None
)
if source_profile is None and source_draft is None and not requested_instruction and scene_template is None:
source_name = mode_creation_source_name(mode_name)
if source_name != mode_name:
source_profile = self.mode_profiles.resolve_exact_name(source_name, vehicle_id=vehicle_profile.id)
draft_mode_name = (
str(source_draft.get("name") or mode_name).strip()
if isinstance(source_draft, dict)
@@ -6205,7 +6472,7 @@ class SeatDemoService:
else "registered_mode_profile"
if source_profile is not None
else "codex_scene_composition"
if assistant_plan is not None and not requested_instruction
if assistant_plan is not None and (not requested_instruction or needs_mode_schedule(utterance))
else "explicit_scene_requirements"
)
# A name-only request may already describe a repository-backed scene.
@@ -6214,7 +6481,63 @@ class SeatDemoService:
# being parsed as a second control clause on the assistant's local fast
# path, turning a valid nine-action mode into an incomplete draft.
source_utterance = requested_instruction or f"进入{draft_mode_name}"
if requested_instruction and selected_seats is not None and len(selected_seats) == 1:
# Qualify only explicit seat-device clauses, never an HVAC/window
# zone or unrelated control. General one-shot controls keep their
# existing target policy; this is scoped to new mode construction.
label = "主驾" if selected_seats == {"driver"} else "副驾"
instruction_clauses = self._split_intent_clauses(requested_instruction)
qualified_clauses = [
f"{label}{clause}" if (
re.search(r"座椅|座位|靠背|椅背|腿托|脚托|坐垫|坐盆|按摩", clause)
and resolve_explicit_seat_scopes(clause, assume_seat_context=True) is None
) else clause
for clause in instruction_clauses
]
# Preserve the user's punctuation and wording unless a seat really
# needs qualification; explicit instructions need no rewriting.
if qualified_clauses != instruction_clauses:
source_utterance = ",".join(qualified_clauses)
raw_schedule = (assistant_plan.get("schedule") if isinstance(assistant_plan, dict) else None)
if source_profile is not None and source_profile.schedule is not None:
raw_schedule = source_profile.schedule.to_dict()
if raw_schedule is not None:
if isinstance(assistant_plan, dict) and assistant_plan.get("ambiguities"):
raise ValueError("动态需求仍有未解决的问题,不能生成部分成功的时间轴,请补充或调整需求。")
if source_profile is None and source_draft is None and scene_template is None and not needs_mode_schedule(utterance):
raise ValueError("用户未请求动态或分阶段模式,不能擅自生成定时任务。")
schedule = ModeSchedule.from_dict(raw_schedule)
if selected_seats is not None and (source_profile is not None or scene_template is not None):
try:
schedule = project_seed_schedule(schedule, selected_seats)
except ModeCreationSeatPolicyError as exc:
return self._mode_creation_seat_failure(utterance, conversation_id, vehicle_profile, str(exc))
return self._preview_mode_schedule(
schedule, name=draft_mode_name, utterance=utterance,
conversation_id=conversation_id, vehicle_profile=vehicle_profile,
control_transport=control_transport, generation_source=generation_source,
source_profile=existing_profile, source_template=scene_template, source_draft=source_draft,
scene_scope_utterance="" if source_draft is not None else utterance,
)
if needs_mode_schedule(utterance) and scene_template is None and not (source_profile is not None and source_profile.timeline):
result = {
"request_id": f"req-{datetime.now(UTC).strftime('%Y%m%d%H%M%S')}-{uuid.uuid4().hex}",
"utterance": utterance, "conversation_id": conversation_id,
"vehicle": vehicle_profile.public_dict(), "validation": [], "trace_events": [],
"status": "validation_failed", "execution_mode": "dry_run", "hardware_execution_allowed": False,
"orchestration_route": "mode_profile_create_preview",
}
result["mode_creation_failure"] = {
"code": "MODE_SCHEDULE_REQUIRED",
"reason": "该需求包含时间变化,但没有生成完整的分阶段时间轴",
"next_step": "请重试,或补充每个阶段的设备目标和持续时间;未保存或执行静态替代动作",
}
self._scrub_failed_scene_preview(result)
self._persist(result)
return result
try:
if scene_template is not None and selected_seats is not None:
assistant_plan = project_seed_plan(assistant_plan, selected_seats)
source_result = self._process_once(
source_utterance,
provider,
@@ -6227,6 +6550,7 @@ class SeatDemoService:
direct_mode_profile_id=(
source_profile.id if source_profile is not None else None
),
mode_creation_scopes=selected_seats,
assistant_plan=assistant_plan,
# ``source_utterance`` is synthesized for a name-only, unknown
# scene. It must not fall back to substring matching (for example
@@ -6238,7 +6562,14 @@ 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 utterance,
)
except ModeCreationSeatPolicyError as exc:
return self._mode_creation_seat_failure(utterance, conversation_id, vehicle_profile, str(exc))
except NativeCanCompileError:
# Repository modes and model-composed scenes share the same native
# compiler. A stale mode parameter must become a bounded failed
@@ -8276,6 +8607,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,
cloud_instruction_context: dict[str, Any] | None = None,
) -> tuple[dict[str, Any], str, str | None]:
vehicle_profile = vehicle_profile or self.vehicle_profiles.default()
@@ -8338,6 +8670,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,