feat: 完善 N7 智能座舱交互与控制链路

This commit is contained in:
vee
2026-09-07 15:34:59 +08:00
parent 32d179143f
commit c96970f068
100 changed files with 30088 additions and 1174 deletions
+577 -52
View File
@@ -38,6 +38,9 @@ from seat_domain import ( # noqa: E402
validate_semantics,
)
from cognitive_agent.application.assistant_plan_guard import ( # noqa: E402
guard_assistant_plan,
)
from cognitive_agent.application.capability_registry import CapabilityRegistry # noqa: E402
from cognitive_agent.application.conversation_state import ConversationStateStore # noqa: E402
from cognitive_agent.application.domain_runtime import ConfiguredDomainRuntime # noqa: E402
@@ -68,6 +71,7 @@ from cognitive_agent.application.native_can_relative import ( # noqa: E402
)
from cognitive_agent.application.semantic_ir import ( # noqa: E402
apply_language_safety_guards,
resolve_explicit_seat_scopes,
)
from cognitive_agent.application.native_can_feedback import ( # noqa: E402
VehicleSafetyPreflightError,
@@ -79,6 +83,7 @@ from cognitive_agent.application.native_can_feedback import ( # noqa: E402
from cognitive_agent.application.native_can_runtime import ( # noqa: E402
NativeCanExecutor,
NativeCanRuntime,
UNVERIFIED_NATIVE_CAPABILITIES,
)
from cognitive_agent.application.seat_calibration import position_pct_to_angle # noqa: E402
from cognitive_agent.application.vehicle_profile import ( # noqa: E402
@@ -134,12 +139,12 @@ MODE_EDITOR_REQUEST_ID_PATTERN = re.compile(r"^req-[a-z0-9-]{6,100}$")
SEAT_SCOPE_PATTERN = re.compile(r"\bseats?\b|座椅|坐椅", re.IGNORECASE)
DRIVER_SEAT_SCOPE_PATTERN = re.compile(
r"\bdriver\b|\bfront[-_ ]?left\b|\bleft[-_ ]?front\b|"
r"主驾|主驾驶(?:座|座椅|席)?|驾驶位|驾驶席|驾驶座(?:椅)?|"
r"主驾|主驾驶(?:座|座椅|席)?|驾驶位|驾驶员|驾驶席|驾驶座(?:椅)?|"
r"司机位|司机座(?:椅)?|左前",
re.IGNORECASE,
)
PASSENGER_SEAT_SCOPE_PATTERN = re.compile(
r"\bpassenger\b|\bfront[-_ ]?right\b|\bright[-_ ]?front\b|副驾|乘客|右前",
r"\bpassenger\b|\bfront[-_ ]?right\b|\bright[-_ ]?front\b|副驾|乘客|乘员|右前",
re.IGNORECASE,
)
WIRED_CAN_TRANSPORT = "wired_can"
@@ -258,10 +263,38 @@ SAVE_MODE_PROFILE_NAME = re.compile(
r"(?P<name>[^,。,.!!\s]{1,64}?模式)"
)
CREATE_MODE_PROFILE_NAME = re.compile(
r"^(?:请(?:帮我)?|帮我)?(?:构建|创建|新建|新增|生成|添加)"
r"(?:一个|个|一套|套)?"
r"(?P<name>[^,。,.!!\s]{1,64}?模式)"
r"(?:配置)?(?:并(?:添加|加入)(?:到)?功能模式(?:栏)?)?[。.!!]?$"
r"^\s*(?:(?:请|麻烦)\s*(?:帮我|帮忙|给我)?\s*|"
r"(?:帮我|帮忙|给我)\s*)?"
r"(?:构建|创建|新建|新增|生成|添加)\s*(?:一下)?\s*"
r"(?:一个|个|一套|套)?\s*"
r"(?:名为|叫做|叫)?\s*[“\"'「『]?"
r"(?P<name>[^,。,.!!::\s“”\"'「」『』]{1,64}?模式)"
r"[”\"'」』]?"
r"\s*(?:配置)?(?:并(?:添加|加入)(?:到)?功能模式(?:栏)?)?"
r"(?:[,,::]\s*(?P<instruction>[\s\S]{1,800}))?[。.!!]?\s*$"
)
# ``、`` is used both as a command separator (``车窗关闭、空调24度``)
# and as an enumeration separator (``主驾、副驾车窗关闭``). Treat it as a
# boundary only when both sides already look like self-contained controls;
# otherwise keep it intact for the target/feature parsers below.
CONTROL_CLAUSE_SUBJECT_PATTERN = re.compile(
r"座椅|座位|靠背|椅背|坐盆|坐垫|腿托|脚托|滑轨|"
r"通风|加热|按摩|零重力|半躺|女王副驾|休闲躺椅|"
r"遮阳帘|天幕帘|天幕|车窗|窗户|"
r"灯光|室内灯|车内灯|阅读灯|氛围灯|"
r"空调|压缩机|温度|风量|风速|内循环|外循环|"
r"车门|门锁|后备箱|前大灯|近光灯|远光灯|雾灯|转向灯|双闪|"
r"出风口"
)
CONTROL_CLAUSE_PREDICATE_PATTERN = re.compile(
r"打开|开启|启动|关闭|关掉|关上|停止|暂停|取消|"
r"调整|调节|调到|调成|调为|调至|设置|设为|设到|切换|"
r"移动|移到|移至|前移|后移|上移|下移|升高|降低|抬起|放倒|"
r"调直|立直|竖直|归零|复位|锁上|上锁|落锁|解锁|"
r"零重力|半躺|最大|最小|最高|最低|最前|最后|到底|"
r"(?:\d+(?:\.\d+)?|[零一二三四五六七八九十百两半]+)"
r"(?:%|%|度|℃|档|级|秒|毫秒)"
)
UPDATE_MODE_PROFILE_PREFIX = re.compile(
r"^(?:请(?:帮我)?|帮我)?(?:修改一下|修改|调整|更改|更新|改一下)"
@@ -430,9 +463,6 @@ class SeatDemoService:
return normalized
def runtime_info(self) -> dict[str, Any]:
catalog = yaml.safe_load(
(self.domain_root / "config" / "capabilities.yaml").read_text(encoding="utf-8")
)
domain_runtime_info = [
runtime.runtime_info() for runtime in self.domain_runtimes.values()
]
@@ -756,6 +786,30 @@ class SeatDemoService:
"request_id"
) != clean_request_id:
raise ValueError("控制指令流来源结果不完整,请重新生成。")
if source_result.get("status") != "succeeded":
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_vehicle_id and source_vehicle_id != resolved_vehicle_id:
raise ValueError("控制指令流不属于当前车型,请重新生成。")
source_draft = source_result.get("mode_profile_draft")
if isinstance(source_draft, dict):
draft_request_id = str(source_draft.get("source_request_id") or "").strip()
draft_vehicle_id = str(source_draft.get("vehicle_id") or "").strip()
draft_instruction = str(source_draft.get("instruction") or "").strip()
draft_name = str(source_draft.get("name") or "").strip()
if draft_request_id != clean_request_id:
raise ValueError("功能模式草稿与控制指令流不匹配,请重新生成。")
if draft_vehicle_id and draft_vehicle_id != resolved_vehicle_id:
raise ValueError("功能模式草稿不属于当前车型,请重新生成。")
if draft_instruction and draft_instruction != clean_instruction:
raise ValueError("自然语言指令已修改,请重新生成模式预览。")
if draft_name and draft_name != clean_name:
raise ValueError("功能模式名称已修改,请重新生成模式预览。")
recorded_instruction = str(source_result.get("utterance", "")).strip()
if not recorded_instruction:
source_spans = {
@@ -1353,6 +1407,7 @@ class SeatDemoService:
reasoning_effort: str | None = None,
control_transport: str = WIRED_CAN_TRANSPORT,
on_commands_validated: Callable[[dict[str, Any]], None] | None = None,
assistant_plan: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Validate and execute deterministic controls before hosted parsing."""
@@ -1390,12 +1445,15 @@ class SeatDemoService:
reasoning_effort=reasoning_effort,
control_transport=control_transport,
_on_commands_validated=on_commands_validated,
_assistant_plan=assistant_plan,
)
result.update(
{
"phase": "control_plane_complete",
"preview_request_id": result["request_id"],
"unresolved_clauses": [],
"unresolved_clauses": list(
result.get("unresolved_clauses") or []
),
"supplement_required": False,
}
)
@@ -1444,6 +1502,7 @@ class SeatDemoService:
_local_preview=True,
_allow_empty_control_result=True,
_on_commands_validated=publish_local_commands,
_assistant_plan=assistant_plan,
)
elapsed_ms = round((time.perf_counter() - started) * 1000, 3)
supplement_required = bool(
@@ -1889,6 +1948,7 @@ class SeatDemoService:
_direct_can_batch: tuple[list[str], dict[str, Any]] | None = None,
_direct_mode_profile_id: str | None = None,
_speech_understanding: dict[str, Any] | None = None,
_assistant_plan: 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
@@ -1916,6 +1976,7 @@ class SeatDemoService:
_direct_can_batch,
_direct_mode_profile_id,
_speech_understanding,
assistant_plan=_assistant_plan,
)
if control_transport == WIRELESS_MQTT_TRANSPORT:
# The head unit owns both physical CAN channels in wireless mode.
@@ -1939,6 +2000,7 @@ class SeatDemoService:
_direct_can_batch,
_direct_mode_profile_id,
_speech_understanding,
assistant_plan=_assistant_plan,
)
if hardware_access_bypass or self._driver_only_vehicle_execution_active(
control_transport
@@ -1962,6 +2024,7 @@ class SeatDemoService:
_direct_can_batch,
_direct_mode_profile_id,
_speech_understanding,
assistant_plan=_assistant_plan,
)
authorized_client_id = self.hardware_access.ensure_authorized(hardware_client_id)
with self.hardware_execution.reserve():
@@ -1984,6 +2047,7 @@ class SeatDemoService:
_direct_can_batch,
_direct_mode_profile_id,
_speech_understanding,
assistant_plan=_assistant_plan,
)
def _process_once(
@@ -2005,6 +2069,7 @@ class SeatDemoService:
direct_can_batch: tuple[list[str], dict[str, Any]] | None = None,
direct_mode_profile_id: str | None = None,
speech_understanding: dict[str, Any] | None = None,
assistant_plan: dict[str, Any] | None = None,
) -> dict[str, Any]:
control_transport = self._validate_control_transport(control_transport)
utterance = utterance.strip()
@@ -2073,6 +2138,7 @@ class SeatDemoService:
simulation_fault,
mode_create_name,
control_transport,
assistant_plan=assistant_plan,
)
request_id = f"req-{datetime.now(UTC).strftime('%Y%m%d%H%M%S')}-{uuid.uuid4().hex[:6]}"
@@ -2258,18 +2324,44 @@ class SeatDemoService:
reasoning_effort=reasoning_effort,
local_preview=local_preview,
model_parse_source=model_parse_source,
assistant_plan=assistant_plan,
)
dynamic_mode_stop = self._is_dynamic_mode_stop_request(utterance)
if dynamic_mode_stop:
parsed = self._apply_dynamic_mode_stop_request(parsed, utterance)
# Reuse the same conservative clause segmentation for the final
# native pass. Without this normalization a Chinese enumeration
# 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(
self._split_intent_clauses(utterance)
)
relative_resolution = (
self.native_can_relative_resolver.resolve(utterance, context)
if self.native_can_relative_resolver.matches(utterance)
self.native_can_relative_resolver.resolve(
segmented_utterance,
context,
)
if self.native_can_relative_resolver.matches(
segmented_utterance
)
else None
)
native_merge_utterance = segmented_utterance
if any(
request.get("capability") in {SAVE_PRESET_CAPABILITY, APPLY_PRESET_CAPABILITY}
for request in parsed.get("model_requests", [])
):
# These registered capabilities currently have one executable
# target (the passenger-seat posture store). Keep that
# adapter-resolved target from being discarded by the native
# parser's general unscoped-seat => driver default. The
# original utterance is still used by the language safety
# guards below.
native_merge_utterance = f"副驾座椅,{utterance}"
parsed = merge_native_can_intent(
parsed,
utterance,
native_merge_utterance,
relative_actions=(
relative_resolution.actions if relative_resolution else None
),
@@ -2299,8 +2391,25 @@ class SeatDemoService:
"saved_mode_command_flow",
)
if local_preview:
parsed["ambiguities"] = []
parsed["unresolved_spans"] = []
# A preview may discard ordinary model-supplement questions, but
# target and safety conflicts are execution gates. Keeping these
# makes the assistant path no less strict than normal process().
parsed["ambiguities"] = [
item
for item in parsed.get("ambiguities", [])
if str(item.get("field", ""))
in {"target", "target_scope", "mode_profile", "risk", "safety", "utterance"}
or str(item.get("reason", "")).startswith(
("mode_target_", "safety_", "risk_", "mixed_")
)
]
parsed["unresolved_spans"] = [
item
for item in parsed.get("unresolved_spans", [])
if str(item.get("reason", "")).startswith(
("mode_target_", "safety_", "risk_", "mixed_")
)
]
parsed["model_requests"] = []
if excluded_control_signatures:
parsed = self._filter_excluded_controls(
@@ -2332,9 +2441,18 @@ class SeatDemoService:
]
has_control = any(
parsed.get(key)
for key in ("actions", "domain_actions", "native_can_actions")
for key in (
"actions",
"domain_actions",
"native_can_actions",
"preset_actions",
)
)
if allow_empty_control_result and not has_control:
if (
allow_empty_control_result
and not has_control
and not parsed.get("ambiguities")
):
result["provider_used"] = provider_used
result["decision_summary"] = [
*parsed.get("decision_summary", []),
@@ -3367,6 +3485,47 @@ class SeatDemoService:
]
return rejected
target_conflict = self._explicit_mode_target_conflict(
matched_modes[0],
utterance,
)
if target_conflict is not None:
rejected = deepcopy(parsed)
for key in (
"actions",
"domain_actions",
"native_can_actions",
"preset_actions",
"preset_events",
"deferred_requests",
"model_requests",
):
rejected[key] = []
requested_scope, supported_scopes = target_conflict
rejected["ambiguities"] = [
*rejected.get("ambiguities", []),
{
"field": "mode_profile",
"question": (
f"该模式的座椅目标为 {', '.join(supported_scopes)},"
f"与用户指定的 {requested_scope} 不一致,请重新确认。"
),
"reason": "mode_target_scope_mismatch",
},
]
rejected["unresolved_spans"] = [
*rejected.get("unresolved_spans", []),
{
"text": utterance,
"reason": "mode_target_scope_mismatch",
},
]
rejected["decision_summary"] = [
*rejected.get("decision_summary", []),
"用户显式座位与功能模式登记目标不一致,已停止所有模式动作。",
]
return rejected
parsed = self._apply_implicit_mode_target_policy(
parsed,
matched_modes,
@@ -3449,6 +3608,46 @@ class SeatDemoService:
)
return merged
@staticmethod
def _explicit_mode_target_conflict(
profile: ModeProfile,
utterance: str,
) -> tuple[str, tuple[str, ...]] | None:
"""Reject a named mode when its persisted seat scope contradicts text."""
aliases = (profile.id, profile.name, *profile.aliases)
mode_clause = next(
(
clause
for clause in re.split(r"[,,;;。!?!?\n]+", utterance)
if any(
re.sub(r"\s+", "", alias.casefold())
in re.sub(r"\s+", "", clause.casefold())
for alias in aliases
if alias
)
),
utterance,
)
requested = resolve_explicit_seat_scopes(
mode_clause,
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
for operation in profile.operations
if operation.domain == "seat"
}
if requested == normalized_scopes:
return None
return ",".join(sorted(requested)), tuple(sorted(normalized_scopes))
@staticmethod
def _apply_implicit_mode_target_policy(
parsed: dict[str, Any],
@@ -4071,6 +4270,114 @@ class SeatDemoService:
except ValueError:
return str(path)
@staticmethod
def _mode_feedback_text(value: object, *, limit: int = 180) -> str:
text = re.sub(r"[\x00-\x1f\x7f]+", " ", str(value or ""))
return re.sub(r"\s+", " ", text).strip(" ,,。;;::")[:limit]
@classmethod
def _mode_failure_issue(
cls,
clause: str,
parsed: dict[str, Any],
) -> dict[str, str]:
display_clause = cls._mode_feedback_text(
re.sub(r"^(?:副驾座椅|主驾座椅)[,,]", "", clause),
limit=120,
)
ambiguities = [
item
for item in parsed.get("ambiguities", [])
if isinstance(item, dict) and str(item.get("field") or "") != "capability"
]
ambiguity = ambiguities[0] if ambiguities else None
field = str(ambiguity.get("field") or "") if ambiguity else ""
question = cls._mode_feedback_text(
ambiguity.get("question") if ambiguity else "",
limit=220,
)
# Parser questions may contain both the diagnosis and a concrete
# correction. Split those parts so every public failure has the same
# reason/next-step contract.
reason_from_question = question
next_from_question = ""
for separator in (";请", "。请"):
if separator in question:
reason_from_question, suffix = question.split(separator, 1)
next_from_question = f"请{suffix}"
break
if reason_from_question and not re.search(
r"(?:CAN|CH[01]|JSON|schema|0x[0-9a-f]+|内部路径)",
reason_from_question,
re.IGNORECASE,
):
reason = f"设置“{display_clause}”未通过:{reason_from_question}"
else:
reason = (
f"设置“{display_clause}”未被识别为当前支持的座舱控制动作"
)
if next_from_question:
next_step = next_from_question
elif "temperature" in field or "温度" in display_clause:
next_step = (
"请只保留一个18至32℃的整数温度,例如“空调调整到24度”,然后重试"
)
elif field == "target" or "target_scope" in field:
next_step = (
"请改用当前支持的副驾座椅目标,并明确部件与位置,例如"
"“副驾腿托调整到80%”"
)
elif "position" in field or re.search(
r"座椅|靠背|椅背|坐垫|坐盆|腿托|脚托|滑轨",
display_clause,
):
next_step = (
"请按“副驾 + 座椅部件 + 方向或0到100%位置”重述,例如"
"“副驾腿托向上调整到最大位置”或“副驾腿托调整到80%”"
)
else:
next_step = (
"请按“设备 + 动作 + 明确目标”重述,例如“关闭所有车窗”、"
"“副驾靠背调整到80%”或“空调调整到24度”"
)
return {
"clause": display_clause,
"reason": reason,
"next_step": cls._mode_feedback_text(next_step, limit=260),
}
@classmethod
def _mode_creation_failure_from_clauses(
cls,
clause_results: list[tuple[str, dict[str, Any], str]],
unresolved_clauses: list[str],
) -> dict[str, Any]:
unresolved = set(unresolved_clauses)
issues = [
cls._mode_failure_issue(clause, parsed)
for clause, parsed, status in clause_results
if status != "complete" and clause in unresolved
][:3]
if not issues:
issues = [
cls._mode_failure_issue(clause, {})
for clause in unresolved_clauses[:3]
]
reason = ";".join(dict.fromkeys(item["reason"] for item in issues))
next_step = ";".join(
dict.fromkeys(item["next_step"] for item in issues)
)
return {
"code": "MODE_PROFILE_DRAFT_INCOMPLETE",
"reason": reason or "部分座舱设置没有转换为可保存的控制动作",
"next_step": next_step
or "请将未识别设置改写为明确的设备、动作和目标值后重试",
"unresolved_clauses": [item["clause"] for item in issues],
"issues": issues,
}
def _process_mode_profile_create(
self,
utterance: str,
@@ -4082,17 +4389,23 @@ class SeatDemoService:
simulation_fault: str,
mode_name: str,
control_transport: str,
assistant_plan: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Build, verify, and persist a new named mode in one user turn.
"""Build and verify a named-mode draft for explicit user confirmation.
A request such as ``构建一个K歌模式`` used to be interpreted as an
ordinary temporary scene. Although that scene could execute
successfully, it never reached the mode registry. Run the generated
scene first so only validated successful actions are persisted, then
use the existing control-plane save path to create the YAML profile.
ordinary temporary scene. Generate the scene through the normal
dry-run path, but leave persistence to the existing mode-card API so
the assistant can show the exact actions before the user saves them.
"""
source_utterance = (
create_match = CREATE_MODE_PROFILE_NAME.fullmatch(utterance)
requested_instruction = (
str(create_match.group("instruction") or "").strip(" ,,::。.!!")
if create_match is not None
else ""
)
source_utterance = requested_instruction or (
f"进入{mode_name},请根据该场景名称使用当前可控原子能力构建并执行场景"
)
source_result = self._process_once(
@@ -4104,8 +4417,67 @@ class SeatDemoService:
vehicle_profile.id,
simulation_fault,
control_transport=control_transport,
assistant_plan=assistant_plan,
)
if source_result.get("status") not in {"succeeded", "partially_succeeded"}:
draft_context = self._context_for_vehicle(
self.conversations.snapshot(conversation_id),
vehicle_profile,
)
draft_context["mode_profiles"] = self.mode_profiles.public_modes(
vehicle_profile.id
)
local_clause_results = self._local_clause_results(
source_utterance,
draft_context,
)
unresolved_draft_clauses = [
clause
for clause, _parsed, status in local_clause_results
if status != "complete"
and not self.mode_profiles.match_text(
clause,
vehicle_id=vehicle_profile.id,
)
]
if provider == "local" and assistant_plan is None and unresolved_draft_clauses:
source_result["status"] = "needs_confirmation"
source_result["unresolved_clauses"] = unresolved_draft_clauses
source_result["mode_creation_failure"] = (
self._mode_creation_failure_from_clauses(
local_clause_results,
unresolved_draft_clauses,
)
)
source_result["supplement_required"] = False
source_result["validation"] = [
*list(source_result.get("validation", [])),
{
"severity": "error",
"code": "MODE_PROFILE_DRAFT_INCOMPLETE",
"path": "mode_profile_draft.instruction",
"message": (
"以下模式设置尚未完整解析:"
+ ";".join(unresolved_draft_clauses)
),
},
]
source_result["decision_summary"] = [
f"未能完整构建“{mode_name}”,因此没有生成可保存草稿。",
*list(source_result.get("decision_summary", [])),
"请补充为当前支持的明确设备动作后重试。",
]
self._persist(source_result)
return source_result
if source_result.get("status") != "succeeded":
source_result["mode_creation_failure"] = {
"code": "MODE_PROFILE_ACTION_VALIDATION_FAILED",
"reason": "场景动作未通过车辆能力或安全校验",
"next_step": (
"请检查每项设备目标是否在当前支持范围内;仍失败时可分条输入后重试"
),
"unresolved_clauses": [],
"issues": [],
}
source_result["decision_summary"] = [
f"未能完成“{mode_name}”的场景动作验证,因此没有创建长期模式。",
*list(source_result.get("decision_summary", [])),
@@ -4113,40 +4485,75 @@ class SeatDemoService:
self._persist(source_result)
return source_result
saved_result = self._process_once(
f"这个模式不错,固化为{mode_name}",
"local",
conversation_id,
"dry_run",
None,
vehicle_profile.id,
"normal",
control_transport=control_transport,
intent_ir = source_result.get("intent_ir")
intent_spec = intent_ir.get("spec") if isinstance(intent_ir, dict) else None
raw_intent_actions = (
intent_spec.get("actions", []) if isinstance(intent_spec, dict) else []
)
if saved_result.get("status") != "succeeded" or not saved_result.get(
"mode_profile"
):
return saved_result
reusable_actions = [
item for item in raw_intent_actions if isinstance(item, dict)
] if isinstance(raw_intent_actions, list) else []
if not reusable_actions:
source_result["status"] = "needs_confirmation"
source_result["mode_creation_failure"] = {
"code": "MODE_PROFILE_DRAFT_EMPTY",
"reason": "没有识别到可保存的座舱控制动作",
"next_step": (
"请按“设备 + 动作 + 明确目标”补充,例如“关闭所有车窗,空调调整到24度”"
),
"unresolved_clauses": [],
"issues": [],
}
source_result["validation"] = [
{
"severity": "error",
"code": "MODE_PROFILE_DRAFT_EMPTY",
"path": "intent_ir.spec.actions",
"message": "没有生成可保存的功能模式动作,请补充更明确的座舱设置。",
}
]
source_result["decision_summary"] = [
f"未能为“{mode_name}”生成可保存的场景动作。",
*list(source_result.get("decision_summary", [])),
]
self._persist(source_result)
return source_result
saved_result["mode_creation"] = {
domains = list(
dict.fromkeys(
str(item.get("domain") or "")
for item in reusable_actions
if str(item.get("domain") or "")
)
)
source_request_id = str(source_result.get("request_id") or "")
source_result["mode_profile_draft"] = {
"name": mode_name,
"instruction": source_utterance,
"source_request_id": source_request_id,
"vehicle_id": vehicle_profile.id,
"domains": domains,
"operation_count": len(reusable_actions),
}
source_result["mode_creation"] = {
"requested_utterance": utterance,
"source_utterance": source_utterance,
"source_request_id": source_result.get("request_id"),
"source_request_id": source_request_id,
"source_status": source_result.get("status"),
"source_provider": source_result.get("provider_used"),
"operation_count": saved_result["mode_profile"].get(
"operation_count", 0
),
"operation_count": len(reusable_actions),
}
source_summary = list(source_result.get("decision_summary", []))
saved_result["decision_summary"] = [
source_result["orchestration_route"] = "mode_profile_create_preview"
source_result["decision_summary"] = [
f"已构建并验证“{mode_name}”场景。",
*source_summary,
*list(saved_result.get("decision_summary", [])),
f"“{mode_name}”已加入功能模式,可直接点击执行。",
*list(source_result.get("decision_summary", [])),
"请确认动作后保存为功能模式,或先进行一次模拟试运行。",
]
self._persist(saved_result)
return saved_result
# Overwrite the persisted source result with the draft metadata. The
# mode-card endpoint later re-reads this server-owned artifact and
# refuses any client-side instruction or request-id substitution.
self._persist(source_result)
return source_result
def _process_mode_profile_save(
self,
@@ -4415,6 +4822,21 @@ class SeatDemoService:
for action in candidate_native_actions
}
virtual_actions: list[dict[str, Any]] = []
demoted_signatures: set[tuple[str, str, str, str]] = set()
for action in candidate_native_actions:
if str(action.get("capability")) not in UNVERIFIED_NATIVE_CAPABILITIES:
continue
demoted = dict(action)
demoted["execution_backend"] = "configured_domain"
virtual_actions.append(demoted)
demoted_signatures.add(
(
str(action.get("domain")),
str(action.get("capability")),
str(action.get("target", action.get("target_scope", "vehicle"))),
json.dumps(action.get("parameters", {}), sort_keys=True, ensure_ascii=False),
)
)
for action in result.get("domain_actions", []):
signature = (
str(action.get("domain")),
@@ -4458,7 +4880,16 @@ class SeatDemoService:
for item in result.get("deferred_requests", [])
if not (
item.get("execution_backend") == "native_can"
and self.native_can_runtime.supports(item)
and (
self.native_can_runtime.supports(item)
or (
str(item.get("domain")),
str(item.get("capability")),
str(item.get("target", item.get("target_scope", "vehicle"))),
json.dumps(item.get("parameters", {}), sort_keys=True, ensure_ascii=False),
)
in demoted_signatures
)
)
]
for item in deferred_requests:
@@ -5480,13 +5911,62 @@ class SeatDemoService:
@staticmethod
def _split_intent_clauses(utterance: str) -> list[str]:
fragments = [
def looks_like_control(value: str) -> bool:
return bool(
CONTROL_CLAUSE_SUBJECT_PATTERN.search(value)
and CONTROL_CLAUSE_PREDICATE_PATTERN.search(value)
)
def split_independent_controls(
value: str,
separator_pattern: str,
) -> list[str]:
tokens = re.split(f"({separator_pattern})", value)
current = tokens[0].strip()
separated: list[str] = []
for index in range(1, len(tokens), 2):
separator = tokens[index]
next_item = tokens[index + 1].strip()
if looks_like_control(current) and looks_like_control(next_item):
if current:
separated.append(current)
current = next_item
else:
current = f"{current}{separator}{next_item}".strip()
if current:
separated.append(current)
return separated
coarse_fragments = [
item.strip()
for item in re.split(r"[,,。;;::!!??]+", utterance)
for item in re.split(r"[,,。;;::!!??\r\n]+", utterance)
if item.strip()
]
fragments: list[str] = []
for coarse_fragment in coarse_fragments:
list_fragments = split_independent_controls(coarse_fragment, r"、")
for list_fragment in list_fragments:
fragments.extend(
split_independent_controls(
list_fragment,
r"然后|随后|接着|最后|同时|并且|以及|再|并",
)
)
clauses: list[str] = []
for fragment in fragments:
fragment = re.sub(
r"^(?:然后|随后|接着|最后|同时|并且|以及|并)",
"",
fragment,
).strip()
fragment = re.sub(
r"^再(?=(?:打开|开启|启动|关闭|关掉|关上|停止|暂停|取消))",
"",
fragment,
).strip()
if not fragment:
continue
# A massage mode/strength phrase may omit both "主驾" and "按摩"
# after the first clause. Keep the whole request atomic so the ON
# fallback does not emit relax before an explicit later mode.
@@ -5755,6 +6235,7 @@ class SeatDemoService:
reasoning_effort: str | None = None,
local_preview: bool = False,
model_parse_source: str = "cloud_model",
assistant_plan: dict[str, Any] | None = None,
) -> tuple[dict[str, Any], str, str | None]:
vehicle_profile = vehicle_profile or self.vehicle_profiles.default()
clause_results = self._local_clause_results(utterance, context)
@@ -5774,6 +6255,50 @@ class SeatDemoService:
for key in ("actions", "domain_actions", "native_can_actions")
)
safe_fallback = trusted_local if trusted_controls else all_local
# A Codex-authored plan is an untrusted normalization hint for only
# the clauses the deterministic parser could not finish. The guard
# binds every proposed capability back to that server-owned text and
# prevents it from replacing a locally resolved capability/target.
# ModelPlanAdapter then applies the same target, parameter, range and
# runtime-availability checks as the established hosted-model path.
if assistant_plan is not None and fallback_clauses:
fallback_utterance = ",".join(fallback_clauses)
guarded = guard_assistant_plan(
assistant_plan,
fallback_text=fallback_utterance,
capability_context=self.capability_context,
executable_capabilities={
str(capability.get("name") or "")
for domain in self.capability_context.get("domains", [])
for capability in domain.get("capabilities", [])
if domain.get("availability") == "executable"
or str(capability.get("name") or "")
in self.native_can_runtime.supported_capabilities
},
fallback_intents=[
parsed
for _, parsed, status in clause_results
if status != "complete"
],
trusted_local=trusted_local,
)
model = self.model_plan_adapter.adapt(
guarded.plan,
context,
vehicle_profile,
utterance=fallback_utterance,
)
model = self._mark_control_parse_source(
model,
model_parse_source,
overwrite=True,
)
return (
self._merge_local_and_model_intent(trusted_local, model),
"local_rules+codex_app_server",
"本地规则结果优先;Codex 结构化计划仅补足未解析子句,并已重新通过能力注册表校验。",
)
if provider == "local" or not fallback_clauses:
return (
(