fix: support conversational mode draft updates

This commit is contained in:
vee
2026-09-07 23:59:42 +08:00
parent 6b2b644ee6
commit f0a248ede3
19 changed files with 2326 additions and 109 deletions
+685 -75
View File
@@ -74,6 +74,11 @@ from cognitive_agent.application.semantic_ir import ( # noqa: E402
apply_language_safety_guards,
resolve_explicit_seat_scopes,
)
from cognitive_agent.application.mode_profile_intent import ( # noqa: E402
is_mode_profile_query,
normalize_mode_profile_name,
parse_mode_profile_update,
)
from cognitive_agent.application.native_can_feedback import ( # noqa: E402
VehicleSafetyPreflightError,
authoritative_physical_ch1_feedback_state,
@@ -82,6 +87,7 @@ from cognitive_agent.application.native_can_feedback import ( # noqa: E402
require_stationary_vehicle,
)
from cognitive_agent.application.native_can_runtime import ( # noqa: E402
NativeCanCompileError,
NativeCanExecutor,
NativeCanRuntime,
UNVERIFIED_NATIVE_CAPABILITIES,
@@ -297,16 +303,6 @@ CONTROL_CLAUSE_PREDICATE_PATTERN = re.compile(
r"(?:\d+(?:\.\d+)?|[零一二三四五六七八九十百两半]+)"
r"(?:%|%|度|℃|档|级|秒|毫秒)"
)
UPDATE_MODE_PROFILE_PREFIX = re.compile(
r"^(?:请(?:帮我)?|帮我)?(?:修改一下|修改|调整|更改|更新|改一下)"
r"(?P<name>[^,。,.!!\s]{1,64}?模式)"
r"(?:[,。,.!!\s]*(?P<change>.*))?$"
)
UPDATE_MODE_PROFILE_BA = re.compile(
r"^(?:请(?:帮我)?|帮我)?(?:把|将)"
r"(?P<name>[^,。,.!!\s]{1,64}?模式)"
r"的(?P<change>.+)$"
)
RESTORE_MODE_PROFILE_PREFIX = re.compile(
r"^(?:请(?:帮我)?|帮我)?(?:恢复|还原|重置|初始化)"
r"(?P<name>[^,。,.!!\s]{1,64}?模式)"
@@ -1515,6 +1511,7 @@ class SeatDemoService:
control_transport: str = WIRED_CAN_TRANSPORT,
on_commands_validated: Callable[[dict[str, Any]], None] | None = None,
assistant_plan: dict[str, Any] | None = None,
mode_profile_draft_context: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Validate and execute deterministic controls before hosted parsing."""
@@ -1545,6 +1542,7 @@ class SeatDemoService:
control_transport=control_transport,
_on_commands_validated=on_commands_validated,
_assistant_plan=assistant_plan,
_mode_profile_draft_context=mode_profile_draft_context,
)
result.update(
{
@@ -1602,6 +1600,7 @@ class SeatDemoService:
_allow_empty_control_result=True,
_on_commands_validated=publish_local_commands,
_assistant_plan=assistant_plan,
_mode_profile_draft_context=mode_profile_draft_context,
)
elapsed_ms = round((time.perf_counter() - started) * 1000, 3)
supplement_required = bool(
@@ -2048,13 +2047,37 @@ class SeatDemoService:
_direct_mode_profile_id: str | None = None,
_speech_understanding: dict[str, Any] | None = None,
_assistant_plan: dict[str, Any] | None = None,
_mode_profile_draft_context: dict[str, Any] | None = None,
) -> dict[str, Any]:
control_transport = self._validate_control_transport(control_transport)
# Creating a reusable scene is a configuration workflow. Synthesize
# and validate it in dry-run even when the console currently targets
# the real bench, otherwise virtual domains such as climate or ambient
# light would be rejected before the profile can be persisted.
if self._parse_mode_profile_create_name(utterance.strip()) is not None:
draft_reference_name = (
str(_mode_profile_draft_context.get("name") or "").strip()
if isinstance(_mode_profile_draft_context, dict)
else ""
)
draft_update = parse_mode_profile_update(
utterance.strip(),
expected_mode_name=draft_reference_name or None,
)
mode_profile_query = is_mode_profile_query(
utterance,
expected_mode_name=draft_reference_name or None,
)
draft_update_requested = bool(
draft_update is not None
and draft_reference_name
and normalize_mode_profile_name(draft_update[0])
== normalize_mode_profile_name(draft_reference_name)
)
if (
self._parse_mode_profile_create_name(utterance.strip()) is not None
or draft_update_requested
or mode_profile_query
):
execution_mode = "dry_run"
if execution_mode != "hardware":
return self._process_once(
@@ -2076,6 +2099,7 @@ class SeatDemoService:
_direct_mode_profile_id,
_speech_understanding,
assistant_plan=_assistant_plan,
mode_profile_draft_context=_mode_profile_draft_context,
)
if control_transport == WIRELESS_MQTT_TRANSPORT:
# The head unit owns both physical CAN channels in wireless mode.
@@ -2100,6 +2124,7 @@ class SeatDemoService:
_direct_mode_profile_id,
_speech_understanding,
assistant_plan=_assistant_plan,
mode_profile_draft_context=_mode_profile_draft_context,
)
if hardware_access_bypass or self._driver_only_vehicle_execution_active(
control_transport
@@ -2124,6 +2149,7 @@ class SeatDemoService:
_direct_mode_profile_id,
_speech_understanding,
assistant_plan=_assistant_plan,
mode_profile_draft_context=_mode_profile_draft_context,
)
authorized_client_id = self.hardware_access.ensure_authorized(hardware_client_id)
with self.hardware_execution.reserve():
@@ -2147,6 +2173,7 @@ class SeatDemoService:
_direct_mode_profile_id,
_speech_understanding,
assistant_plan=_assistant_plan,
mode_profile_draft_context=_mode_profile_draft_context,
)
def _process_once(
@@ -2171,6 +2198,7 @@ class SeatDemoService:
assistant_plan: dict[str, Any] | None = None,
allow_fuzzy_mode_profile_match: bool = True,
allow_scene_composition: bool = False,
mode_profile_draft_context: dict[str, Any] | None = None,
) -> dict[str, Any]:
control_transport = self._validate_control_transport(control_transport)
utterance = utterance.strip()
@@ -2203,11 +2231,24 @@ class SeatDemoService:
mode_save_name = self._parse_mode_profile_save_name(utterance)
mode_create_name = self._parse_mode_profile_create_name(utterance)
mode_restore_name = self._parse_mode_profile_restore_name(utterance)
mode_update = self._parse_mode_profile_update(utterance)
draft_reference_name = (
str(mode_profile_draft_context.get("name") or "").strip()
if isinstance(mode_profile_draft_context, dict)
else ""
)
mode_update = parse_mode_profile_update(
utterance,
expected_mode_name=draft_reference_name or None,
)
mode_profile_query = is_mode_profile_query(
utterance,
expected_mode_name=draft_reference_name or None,
)
control_plane_request = (
mode_save_name is not None
or mode_restore_name is not None
or mode_update is not None
or mode_profile_query
)
hardware_request = execution_mode == "hardware"
if hardware_request and _has_mixed_driver_passenger_seat_scope(utterance):
@@ -2242,7 +2283,12 @@ class SeatDemoService:
assistant_plan=assistant_plan,
)
request_id = f"req-{datetime.now(UTC).strftime('%Y%m%d%H%M%S')}-{uuid.uuid4().hex[:6]}"
# A request id is also the opaque capability used to reference a
# server-owned draft artifact. Keep the full 128 bits of randomness;
# the previous six-hex suffix was guessable under multi-user traffic.
request_id = (
f"req-{datetime.now(UTC).strftime('%Y%m%d%H%M%S')}-{uuid.uuid4().hex}"
)
context = self._context_for_vehicle(
self.conversations.snapshot(conversation_id),
vehicle_profile,
@@ -2319,6 +2365,24 @@ class SeatDemoService:
}
started = time.perf_counter()
if mode_profile_query:
result["status"] = "needs_confirmation"
result["execution_mode"] = "dry_run"
result["hardware_execution_allowed"] = False
result["orchestration_route"] = "mode_profile_query"
result["validation"] = [
{
"severity": "warning",
"code": "MODE_PROFILE_QUERY_NOT_CONTROL",
"path": "utterance",
"message": "该语句是在询问功能模式,不是模式修改或车辆执行指令。",
}
]
result["decision_summary"] = [
"识别为功能模式问答;未生成或执行任何车辆控制动作。"
]
self._persist(result)
return result
if mode_save_name is not None:
return self._process_mode_profile_save(
utterance,
@@ -2342,6 +2406,28 @@ class SeatDemoService:
)
if mode_update is not None:
mode_name, change_text = mode_update
if (
draft_reference_name
and normalize_mode_profile_name(draft_reference_name)
== normalize_mode_profile_name(mode_name)
):
return self._process_mode_profile_draft_update(
utterance,
request_id,
provider,
conversation_id,
context,
mode_name,
change_text,
vehicle_profile,
result,
traces,
mode_profile_draft_context,
approval_token=approval_token,
simulation_fault=simulation_fault,
control_transport=control_transport,
assistant_plan=assistant_plan,
)
return self._process_mode_profile_update(
utterance,
request_id,
@@ -3550,18 +3636,7 @@ class SeatDemoService:
@staticmethod
def _parse_mode_profile_update(utterance: str) -> tuple[str, str] | None:
explicit = UPDATE_MODE_PROFILE_PREFIX.search(utterance)
if explicit is not None:
change_text = str(explicit.group("change") or "").strip()
return explicit.group("name").strip(), change_text
inline = UPDATE_MODE_PROFILE_BA.search(utterance)
if inline is None:
return None
change_text = inline.group("change").strip("的").strip()
if not any(word in change_text for word in ("改", "调", "设", "更新", "变")):
return None
return inline.group("name").strip(), change_text
return parse_mode_profile_update(utterance)
@staticmethod
def _parse_mode_profile_restore_name(utterance: str) -> str | None:
@@ -3953,6 +4028,414 @@ class SeatDemoService:
domains.append(domain)
return domains or ["seat"]
@staticmethod
def _preview_mode_operations(
source_result: dict[str, Any],
) -> list[dict[str, Any]]:
"""Recover every reusable operation from server-compiled preview data."""
plans = source_result.get("command_plans")
configs = source_result.get("function_configs")
if not isinstance(plans, dict) or not isinstance(configs, dict):
return []
operations: list[dict[str, Any]] = []
seen: set[tuple[str, str, str]] = set()
for key, plan in plans.items():
if (
not isinstance(plan, dict)
or not isinstance(plan.get("spec"), dict)
or not isinstance(plan["spec"].get("commands"), list)
or not plan["spec"]["commands"]
):
continue
config = configs.get(key)
spec = config.get("spec") if isinstance(config, dict) else None
if not isinstance(spec, dict):
continue
domain = str(spec.get("domain") or "").strip()
raw_operations = spec.get("operations")
if not domain or not isinstance(raw_operations, list):
continue
for operation in raw_operations:
if not isinstance(operation, dict):
continue
capability = str(operation.get("capability") or "").strip()
raw_target = operation.get("target")
target = (
str(
raw_target.get("instance")
or raw_target.get("zone")
or raw_target.get("scope")
or ""
).strip()
if isinstance(raw_target, dict)
else ""
)
parameters = operation.get("parameters")
signature = (domain, capability, target)
if (
not capability
or not target
or not isinstance(parameters, dict)
or signature in seen
):
continue
reusable_parameters = deepcopy(parameters)
if (
capability.startswith("seat.set_")
and reusable_parameters.get("position_pct") is not None
and reusable_parameters.get("angle_deg") is not None
):
# Seat compilation adds the calibrated angle alongside the
# requested percentage. The scene contract intentionally
# permits one absolute representation only; retain the
# portable percentage and let the new compile derive angle.
reusable_parameters.pop("angle_deg", None)
seen.add(signature)
operations.append(
{
"domain": domain,
"capability": capability,
"target_scope": target,
"operation": "set",
"parameters": reusable_parameters,
"reason": "由服务端已校验功能模式草稿保留。",
}
)
return operations
def _load_mode_profile_draft_reference(
self,
reference: dict[str, Any],
*,
conversation_id: str,
mode_name: str,
vehicle_id: str,
) -> tuple[dict[str, Any], dict[str, Any], list[dict[str, Any]]]:
"""Resolve an opaque client reference back to one trusted local artifact."""
if not isinstance(reference, dict) or set(reference) != {
"name",
"source_request_id",
"vehicle_id",
}:
raise ValueError("待保存功能模式草稿引用格式无效。")
clean_name = str(reference.get("name") or "").strip()
clean_request_id = str(reference.get("source_request_id") or "").strip()
clean_vehicle_id = str(reference.get("vehicle_id") or "").strip()
if not MODE_EDITOR_REQUEST_ID_PATTERN.fullmatch(clean_request_id):
raise ValueError("待保存功能模式草稿来源请求编号无效。")
if (
normalize_mode_profile_name(clean_name)
!= normalize_mode_profile_name(mode_name)
):
raise ValueError("修改请求与待保存功能模式草稿名称不匹配。")
if clean_vehicle_id != vehicle_id:
raise ValueError("待保存功能模式草稿不属于当前车型。")
result_path = self.artifact_root / clean_request_id / "result.json"
if not result_path.is_file():
raise ValueError("未找到待保存功能模式草稿,请重新构建。")
try:
source_result = json.loads(result_path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
raise ValueError("待保存功能模式草稿无法读取,请重新构建。") from exc
if not isinstance(source_result, dict):
raise ValueError("待保存功能模式草稿内容无效,请重新构建。")
source_vehicle = source_result.get("vehicle")
source_vehicle_id = (
str(source_vehicle.get("id") or "").strip()
if isinstance(source_vehicle, dict)
else ""
)
if (
source_result.get("request_id") != clean_request_id
or source_result.get("conversation_id") != conversation_id
or source_vehicle_id != vehicle_id
or source_result.get("status") != "succeeded"
or source_result.get("execution_mode") != "dry_run"
or source_result.get("hardware_execution_allowed") is not False
or source_result.get("orchestration_route")
!= "mode_profile_create_preview"
):
raise ValueError("待保存功能模式草稿与当前会话不匹配,请重新构建。")
draft = source_result.get("mode_profile_draft")
if not isinstance(draft, dict):
raise ValueError("来源请求不是可修改的功能模式草稿。")
if (
str(draft.get("name") or "").strip() != clean_name
or str(draft.get("source_request_id") or "").strip()
!= clean_request_id
or str(draft.get("vehicle_id") or "").strip() != vehicle_id
):
raise ValueError("待保存功能模式草稿引用与来源内容不匹配。")
validation = source_result.get("validation")
if isinstance(validation, list) and any(
isinstance(issue, dict) and issue.get("severity") == "error"
for issue in validation
):
raise ValueError("待保存功能模式草稿没有通过完整安全校验。")
operations = self._preview_mode_operations(source_result)
operation_count = draft.get("operation_count")
if (
not isinstance(operation_count, int)
or isinstance(operation_count, bool)
or operation_count <= 0
or len(operations) != operation_count
):
raise ValueError("待保存功能模式草稿动作证据不完整,请重新构建。")
return source_result, draft, operations
@staticmethod
def _absolute_mode_operations(
operations: list[dict[str, Any]],
) -> list[dict[str, Any]]:
"""Convert bounded motion commands into replayable absolute mode targets."""
normalized: list[dict[str, Any]] = []
window_targets = {
"all": ("front_left", "front_right", "rear_left", "rear_right"),
"front": ("front_left", "front_right"),
"rear": ("rear_left", "rear_right"),
}
for operation in operations:
item = deepcopy(operation)
capability = str(item.get("capability") or "")
parameters = dict(item.get("parameters") or {})
if capability == "roof.set_shade_motion":
positions = {"close": 0, "half": 50, "open": 100}
motion = str(parameters.get("motion") or "")
if motion not in positions:
raise ValueError("遮阳帘停止动作没有可保存的最终位置,请改为明确开度。")
item["capability"] = "roof.set_shade_position"
item["parameters"] = {"position_pct": positions[motion]}
normalized.append(item)
continue
if capability == "window.set_motion":
positions = {"close": 0, "open": 100}
motion = str(parameters.get("motion") or "")
if motion not in positions:
raise ValueError("车窗停止动作没有可保存的最终位置,请改为明确开度。")
target = str(item.get("target_scope") or "")
targets = window_targets.get(target, (target,))
for resolved_target in targets:
absolute = deepcopy(item)
absolute["capability"] = "window.set_position"
absolute["target_scope"] = resolved_target
absolute["parameters"] = {"position_pct": positions[motion]}
normalized.append(absolute)
continue
normalized.append(item)
return normalized
@staticmethod
def _mode_profile_draft_plan(
operations: list[dict[str, Any]],
) -> dict[str, Any]:
return {
"detected_domains": list(
dict.fromkeys(str(item["domain"]) for item in operations)
),
"requests": [
{
"domain": str(item["domain"]),
"capability": str(item["capability"]),
"target_scope": str(item["target_scope"]),
"operation": "set",
"duration_ms": None,
"parameters": [
{"name": str(name), "value": deepcopy(value)}
for name, value in dict(item.get("parameters") or {}).items()
],
"reason": "按用户要求修改待保存功能模式草稿。",
}
for item in operations
],
"ambiguities": [],
"decision_summary": ["已在服务端合并并重新校验待保存模式动作。"],
"confidence": 1.0,
}
@staticmethod
def _mode_update_target_is_explicit(change_text: str, target: str) -> bool:
patterns = {
"driver": r"主驾|驾驶位|驾驶员|左前",
"front_left": r"主驾|驾驶位|驾驶员|左前",
"passenger": r"副驾|乘客位|右前",
"front_right": r"副驾|乘客位|右前",
"front_row": r"前排|前舱",
"rear": r"后排|后座|后舱|所有后排",
"rear_row": r"后排|后座|后舱|所有后排",
"rear_left": r"左后|后排左|左侧后排",
"rear_right": r"右后|后排右|右侧后排",
"vehicle": r"全车|整车|所有|全部",
"all": r"全车|整车|所有|全部",
}
pattern = patterns.get(str(target))
return bool(pattern and re.search(pattern, change_text))
def _process_mode_profile_draft_update(
self,
utterance: str,
request_id: str,
provider: str,
conversation_id: str,
context: dict[str, Any],
mode_name: str,
change_text: str,
vehicle_profile: VehicleProfile,
result: dict[str, Any],
traces: list[dict[str, Any]],
draft_reference: dict[str, Any],
*,
approval_token: str | None,
simulation_fault: str,
control_transport: str,
assistant_plan: dict[str, Any] | None,
) -> dict[str, Any]:
"""Patch an unsaved preview and produce a new atomic dry-run artifact."""
del request_id, traces
try:
_source_result, source_draft, source_operations = (
self._load_mode_profile_draft_reference(
draft_reference,
conversation_id=conversation_id,
mode_name=mode_name,
vehicle_id=vehicle_profile.id,
)
)
base_operations = self._absolute_mode_operations(source_operations)
if not change_text:
raise ValueError("请说明要调整的设备和明确目标值。")
parseable_change_text = re.sub(
r"改为|改成|变更为|换成|变为",
"调整为",
change_text,
)
unresolved_change_clauses = [
clause
for clause, _parsed, status in self._local_clause_results(
parseable_change_text,
context,
)
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: 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}”中该设备能力包含多个目标,请明确主驾、副驾或具体区域。"
)
normalized_changes.append(normalized)
change_by_key = {
(
str(item["domain"]),
str(item["capability"]),
str(item["target_scope"]),
): item
for item in normalized_changes
}
merged: list[dict[str, Any]] = []
for operation in base_operations:
key = (
str(operation["domain"]),
str(operation["capability"]),
str(operation["target_scope"]),
)
merged.append(deepcopy(change_by_key.pop(key, operation)))
merged.extend(deepcopy(list(change_by_key.values())))
return self._process_mode_profile_create(
utterance,
"local",
conversation_id,
"dry_run",
approval_token,
vehicle_profile,
simulation_fault,
mode_name,
control_transport,
assistant_plan=self._mode_profile_draft_plan(merged),
source_draft=source_draft,
)
except ValueError as exc:
result["status"] = "needs_confirmation"
result["execution_mode"] = "dry_run"
result["hardware_execution_allowed"] = False
result["orchestration_route"] = "mode_profile_create_preview"
result["validation"] = [
{
"severity": "error",
"code": "MODE_PROFILE_DRAFT_UPDATE_REJECTED",
"path": "mode_profile_draft_context",
"message": str(exc),
}
]
result["mode_creation_failure"] = {
"code": "MODE_PROFILE_DRAFT_UPDATE_REJECTED",
"reason": str(exc),
"next_step": "请保留当前模式卡片,并明确要修改的设备、区域和目标值后重试",
"unresolved_clauses": [change_text] if change_text else [],
"issues": [],
}
result["decision_summary"] = [
f"未修改待保存的“{mode_name}”草稿;原草稿保持不变。"
]
self._persist(result)
return result
def _process_mode_profile_update(
self,
utterance: str,
@@ -4303,10 +4786,11 @@ class SeatDemoService:
traces: list[dict[str, Any]],
request_id: str,
) -> ModeProfile | None:
matches = self.mode_profiles.match_text(
exact = self.mode_profiles.resolve_exact_name(
mode_name,
vehicle_id=vehicle_profile.id,
)
matches = [exact] if exact is not None else []
result["matched_modes"] = [profile.public_dict() for profile in matches]
if len(matches) == 1:
return matches[0]
@@ -4614,6 +5098,7 @@ class SeatDemoService:
mode_name: str,
control_transport: str,
assistant_plan: dict[str, Any] | None = None,
source_draft: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Build and verify a named-mode draft for explicit user confirmation.
@@ -4629,14 +5114,42 @@ class SeatDemoService:
if create_match is not None
else ""
)
existing_profile = self.mode_profiles.resolve_exact_name(
mode_name,
vehicle_id=vehicle_profile.id,
source_existing_mode_id = (
str(source_draft.get("existing_mode_id") or "").strip()
if isinstance(source_draft, dict)
else ""
)
if source_draft is not None and source_existing_mode_id:
try:
existing_profile = self.mode_profiles.get_profile(
source_existing_mode_id,
vehicle_profile.id,
)
except KeyError:
existing_profile = None
elif source_draft is not None:
existing_profile = None
else:
existing_profile = self.mode_profiles.resolve_exact_name(
mode_name,
vehicle_id=vehicle_profile.id,
)
source_profile = (
existing_profile
if not requested_instruction and source_draft is None
else None
)
draft_mode_name = (
str(source_draft.get("name") or mode_name).strip()
if isinstance(source_draft, dict)
else existing_profile.name
if existing_profile is not None
else mode_name
)
source_profile = existing_profile if not requested_instruction else None
draft_mode_name = existing_profile.name if existing_profile is not None else mode_name
generation_source = (
"registered_mode_profile"
"draft_revision"
if source_draft is not None
else "registered_mode_profile"
if source_profile is not None
else "codex_scene_composition"
if assistant_plan is not None and not requested_instruction
@@ -4647,31 +5160,99 @@ class SeatDemoService:
# of appending a model prompt to the user's utterance. The latter was
# 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"进入{mode_name}"
source_result = self._process_once(
source_utterance,
provider,
conversation_id,
"dry_run",
approval_token,
vehicle_profile.id,
simulation_fault,
control_transport=control_transport,
direct_mode_profile_id=(
source_profile.id if source_profile is not None else None
),
assistant_plan=assistant_plan,
# ``source_utterance`` is synthesized for a name-only, unknown
# scene. It must not fall back to substring matching (for example
# ``我的小憩模式`` -> ``小憩模式``), otherwise the exact resolver's
# ambiguity and collision checks can be bypassed.
allow_fuzzy_mode_profile_match=bool(requested_instruction),
allow_scene_composition=(
assistant_plan is not None
and source_profile is None
and not requested_instruction
),
)
source_utterance = requested_instruction or f"进入{draft_mode_name}"
try:
source_result = self._process_once(
source_utterance,
provider,
conversation_id,
"dry_run",
approval_token,
vehicle_profile.id,
simulation_fault,
control_transport=control_transport,
direct_mode_profile_id=(
source_profile.id if source_profile is not None else None
),
assistant_plan=assistant_plan,
# ``source_utterance`` is synthesized for a name-only, unknown
# scene. It must not fall back to substring matching (for example
# ``我的小憩模式`` -> ``小憩模式``), otherwise the exact resolver's
# ambiguity and collision checks can be bypassed.
allow_fuzzy_mode_profile_match=bool(requested_instruction),
allow_scene_composition=(
assistant_plan is not None
and source_profile is None
and not requested_instruction
),
)
except NativeCanCompileError:
# Repository modes and model-composed scenes share the same native
# compiler. A stale mode parameter must become a bounded failed
# preview, never an uncaught HTTP 500 or a partially usable card.
failure_context = self._context_for_vehicle(
self.conversations.snapshot(conversation_id),
vehicle_profile,
)
source_result = {
"request_id": (
f"req-{datetime.now(UTC).strftime('%Y%m%d%H%M%S')}-"
f"{uuid.uuid4().hex}"
),
"utterance": source_utterance,
"conversation_id": conversation_id,
"conversation_turn": failure_context["turn_count"],
"status": "validation_failed",
"provider_requested": provider,
"provider_used": "mode_profile_native_can_guard",
"execution_mode": "dry_run",
"control_transport": control_transport,
"hardware_execution_allowed": False,
"vehicle": vehicle_profile.public_dict(),
"matched_modes": (
[source_profile.public_dict()]
if source_profile is not None
else []
),
"mode_profile": None,
"decision_summary": [
f"“{draft_mode_name}”包含当前车型协议无法编译的动作,未生成草稿。"
],
"deferred_requests": [],
"model_requests": [],
"unresolved_clauses": [],
"intent_ir": None,
"function_config": None,
"function_configs": {},
"scene_config": None,
"preset_config": None,
"validation": [
{
"severity": "error",
"code": "MODE_PROFILE_NATIVE_COMPILE_FAILED",
"path": "mode_profile_draft",
"message": "模式动作无法编译为当前车型支持的控制指令。",
}
],
"command_plan": None,
"command_plans": {},
"execution_report": None,
"actuator_state": failure_context["devices"],
"domain_state": failure_context["domain_devices"],
"saved_presets": [],
"trace_events": [],
"can_commands": [],
"orchestration_route": "mode_profile_create_preview",
"mode_creation_failure": {
"code": "MODE_PROFILE_NATIVE_COMPILE_FAILED",
"reason": "模式动作未通过当前车型 CAN 协议编译校验",
"next_step": "请检查模式中的设备参数是否符合当前车型能力范围",
"unresolved_clauses": [],
"issues": [],
},
}
self._persist(source_result)
return source_result
draft_context = self._context_for_vehicle(
self.conversations.snapshot(conversation_id),
vehicle_profile,
@@ -4736,7 +5317,7 @@ class SeatDemoService:
] if isinstance(source_actions, list) else []
if (
source_result.get("status") == "succeeded"
and generation_source == "codex_scene_composition"
and generation_source in {"codex_scene_composition", "draft_revision"}
and assistant_plan is not None
and (
self._scene_plan_signatures(assistant_plan)
@@ -4762,7 +5343,7 @@ class SeatDemoService:
]
if source_result.get("status") != "succeeded":
if generation_source == "codex_scene_composition":
if generation_source in {"codex_scene_composition", "draft_revision"}:
# Final fail-closed barrier for errors discovered after the
# plan guard (adapter, schema, compiler, or safety validation).
# A failed reusable scene must not expose a trial-able subset.
@@ -4825,6 +5406,40 @@ class SeatDemoService:
)
)
source_request_id = str(source_result.get("request_id") or "")
source_existing_revision = (
str(source_draft.get("existing_mode_revision") or "").strip()
if isinstance(source_draft, dict)
else ""
)
existing_binding = (
{
"existing_mode_id": source_existing_mode_id,
"existing_mode_revision": source_existing_revision,
}
if source_draft is not None and source_existing_mode_id
else {
"existing_mode_id": existing_profile.id,
"existing_mode_revision": (
self.mode_profiles.profile_revision(existing_profile)
),
}
if existing_profile is not None
else {}
)
raw_source_revision = (
source_draft.get("draft_revision")
if isinstance(source_draft, dict)
else None
)
source_revision = (
raw_source_revision
if isinstance(raw_source_revision, int)
and not isinstance(raw_source_revision, bool)
and raw_source_revision >= 1
else 1
if source_draft is not None
else 0
)
source_result["mode_profile_draft"] = {
"name": draft_mode_name,
"instruction": source_utterance,
@@ -4832,17 +5447,18 @@ class SeatDemoService:
"vehicle_id": vehicle_profile.id,
"domains": domains,
"operation_count": len(reusable_actions),
**existing_binding,
"generation_source": generation_source,
"draft_revision": source_revision + 1,
**(
{
"existing_mode_id": existing_profile.id,
"existing_mode_revision": (
self.mode_profiles.profile_revision(existing_profile)
),
"revises_source_request_id": str(
source_draft.get("source_request_id") or ""
).strip()
}
if existing_profile is not None
if isinstance(source_draft, dict)
else {}
),
"generation_source": generation_source,
}
source_result["mode_creation"] = {
"requested_utterance": utterance,
@@ -4851,17 +5467,9 @@ class SeatDemoService:
"source_status": source_result.get("status"),
"source_provider": source_result.get("provider_used"),
"operation_count": len(reusable_actions),
**(
{
"existing_mode_id": existing_profile.id,
"existing_mode_revision": (
self.mode_profiles.profile_revision(existing_profile)
),
}
if existing_profile is not None
else {}
),
**existing_binding,
"generation_source": generation_source,
"draft_revision": source_revision + 1,
}
source_result["orchestration_route"] = "mode_profile_create_preview"
source_result["decision_summary"] = [
@@ -4871,6 +5479,8 @@ class SeatDemoService:
if source_profile is not None
else f"Codex 已根据场景语义组合并验证“{draft_mode_name}”的工程能力。"
if generation_source == "codex_scene_composition"
else f"已调整并重新校验待保存的“{draft_mode_name}”模式草稿。"
if generation_source == "draft_revision"
else f"已构建并验证“{mode_name}”场景。"
),
*list(source_result.get("decision_summary", [])),