完善模式卡片、NFC及多设备控制流程
This commit is contained in:
@@ -608,6 +608,59 @@ class SeatDemoService:
|
||||
"vehicle_id": resolved_vehicle_id,
|
||||
}
|
||||
|
||||
def get_mode_profile_card(
|
||||
self,
|
||||
mode_id: str,
|
||||
vehicle_id: str | None = None,
|
||||
) -> ModeProfile:
|
||||
resolved_vehicle_id = vehicle_id or self.vehicle_profiles.default().id
|
||||
return self.mode_profiles.get_profile(mode_id, resolved_vehicle_id)
|
||||
|
||||
def execute_mode_profile_card(
|
||||
self,
|
||||
mode_id: str,
|
||||
*,
|
||||
conversation_id: str | None = None,
|
||||
execution_mode: str = "dry_run",
|
||||
approval_token: str | None = None,
|
||||
vehicle_id: str | None = None,
|
||||
simulation_fault: str = "normal",
|
||||
hardware_client_id: str | None = None,
|
||||
hardware_access_bypass: bool = True,
|
||||
control_transport: str = WIRED_CAN_TRANSPORT,
|
||||
) -> dict[str, Any]:
|
||||
"""Execute a persisted mode by ID without interpreting natural language."""
|
||||
|
||||
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"):
|
||||
raise ValueError("该模式尚未生成可直接执行的控制指令流。")
|
||||
result = self.process(
|
||||
profile.instruction or f"进入{profile.name}",
|
||||
provider="local",
|
||||
conversation_id=conversation_id,
|
||||
execution_mode=execution_mode,
|
||||
approval_token=approval_token,
|
||||
vehicle_id=resolved_vehicle_id,
|
||||
simulation_fault=simulation_fault,
|
||||
hardware_client_id=hardware_client_id,
|
||||
hardware_access_bypass=hardware_access_bypass,
|
||||
control_transport=control_transport,
|
||||
_direct_mode_profile_id=profile.id,
|
||||
)
|
||||
public_profile = profile.public_dict()
|
||||
result["provider_used"] = "saved_mode_command_flow"
|
||||
result["utterance"] = profile.name
|
||||
result["matched_modes"] = [public_profile]
|
||||
result["mode_profile"] = public_profile
|
||||
result["orchestration_route"] = "saved_mode_command_flow"
|
||||
result["decision_summary"] = [
|
||||
f"直接载入已保存的模式“{profile.name}”,未再次解析自然语言。",
|
||||
*list(result.get("decision_summary") or []),
|
||||
]
|
||||
self._persist(result)
|
||||
return result
|
||||
|
||||
def save_mode_profile_card(
|
||||
self,
|
||||
mode_id: str,
|
||||
@@ -616,7 +669,7 @@ class SeatDemoService:
|
||||
source_request_id: str,
|
||||
vehicle_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Persist one edited mode from a server-generated CAN command flow."""
|
||||
"""Persist one edited mode from a server-generated control flow."""
|
||||
|
||||
resolved_vehicle_id = vehicle_id or self.vehicle_profiles.default().id
|
||||
clean_mode_id = str(mode_id).strip()
|
||||
@@ -632,7 +685,7 @@ class SeatDemoService:
|
||||
if len(clean_instruction) > 1000:
|
||||
raise ValueError("自然语言指令不能超过 1000 个字符。")
|
||||
if not MODE_EDITOR_REQUEST_ID_PATTERN.fullmatch(clean_request_id):
|
||||
raise ValueError("CAN 指令流来源请求编号无效。")
|
||||
raise ValueError("控制指令流来源请求编号无效。")
|
||||
|
||||
visible_profiles = self.mode_profiles._profiles_for_vehicle( # noqa: SLF001
|
||||
resolved_vehicle_id
|
||||
@@ -653,15 +706,15 @@ class SeatDemoService:
|
||||
|
||||
result_path = self.artifact_root / clean_request_id / "result.json"
|
||||
if not result_path.is_file():
|
||||
raise ValueError("未找到执行按钮生成的 CAN 指令流,请重新执行。")
|
||||
raise ValueError("未找到“生成指令”按钮产生的控制指令流,请重新生成。")
|
||||
try:
|
||||
source_result = json.loads(result_path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError) as exc:
|
||||
raise ValueError("CAN 指令流来源结果无法读取,请重新执行。") from exc
|
||||
raise ValueError("控制指令流来源结果无法读取,请重新生成。") from exc
|
||||
if not isinstance(source_result, dict) or source_result.get(
|
||||
"request_id"
|
||||
) != clean_request_id:
|
||||
raise ValueError("CAN 指令流来源结果不完整,请重新执行。")
|
||||
raise ValueError("控制指令流来源结果不完整,请重新生成。")
|
||||
recorded_instruction = str(source_result.get("utterance", "")).strip()
|
||||
if not recorded_instruction:
|
||||
source_spans = {
|
||||
@@ -676,12 +729,12 @@ class SeatDemoService:
|
||||
if len(source_spans) == 1:
|
||||
recorded_instruction = next(iter(source_spans))
|
||||
if recorded_instruction and recorded_instruction != clean_instruction:
|
||||
raise ValueError("自然语言指令已修改,请重新点击执行生成 CAN 指令流。")
|
||||
raise ValueError("自然语言指令已修改,请重新点击“生成指令”。")
|
||||
|
||||
plans = source_result.get("command_plans") or {}
|
||||
configs = source_result.get("function_configs") or {}
|
||||
if not isinstance(plans, dict) or not isinstance(configs, dict):
|
||||
raise ValueError("执行结果中没有可永久化的 CAN 指令流。")
|
||||
raise ValueError("解析结果中没有可永久化的控制指令流。")
|
||||
executable_plan_keys = {
|
||||
key
|
||||
for key, plan in plans.items()
|
||||
@@ -691,7 +744,7 @@ class SeatDemoService:
|
||||
and plan["spec"]["commands"]
|
||||
}
|
||||
if not executable_plan_keys:
|
||||
raise ValueError("请先点击执行并成功生成 CAN 控制指令流。")
|
||||
raise ValueError("请先点击“生成指令”并成功生成控制指令流。")
|
||||
|
||||
operations: list[dict[str, Any]] = []
|
||||
signatures: set[tuple[str, str, str, str]] = set()
|
||||
@@ -707,7 +760,12 @@ class SeatDemoService:
|
||||
if not isinstance(operation, dict):
|
||||
continue
|
||||
capability = str(operation.get("capability", "")).strip()
|
||||
target = str((operation.get("target") or {}).get("zone", "")).strip()
|
||||
operation_target = operation.get("target") or {}
|
||||
target = str(
|
||||
operation_target.get("instance")
|
||||
or operation_target.get("zone")
|
||||
or ""
|
||||
).strip()
|
||||
parameters = operation.get("parameters") or {}
|
||||
if not domain or not capability or not target or not isinstance(
|
||||
parameters, dict
|
||||
@@ -733,37 +791,8 @@ class SeatDemoService:
|
||||
}
|
||||
)
|
||||
if not operations:
|
||||
raise ValueError("CAN 指令流没有关联到可复用的模式操作。")
|
||||
raise ValueError("控制指令流没有关联到可复用的模式操作。")
|
||||
|
||||
execution_steps = []
|
||||
for item in (
|
||||
(source_result.get("execution_report") or {}).get("spec", {}).get(
|
||||
"results", []
|
||||
)
|
||||
):
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
telemetry = item.get("telemetry") or {}
|
||||
command_evidence = {
|
||||
key: deepcopy(telemetry[key])
|
||||
for key in (
|
||||
"command_frame",
|
||||
"sent_frames",
|
||||
"mqtt_messages",
|
||||
"response_frame",
|
||||
"requested_parameters",
|
||||
)
|
||||
if key in telemetry
|
||||
}
|
||||
if command_evidence:
|
||||
execution_steps.append(
|
||||
{
|
||||
"ref": item.get("ref"),
|
||||
"domain": item.get("domain"),
|
||||
"status": item.get("status"),
|
||||
**command_evidence,
|
||||
}
|
||||
)
|
||||
command_flow = {
|
||||
"schema": "cognitive-agent-mode-command-flow/v1",
|
||||
"source_request_id": clean_request_id,
|
||||
@@ -776,7 +805,10 @@ class SeatDemoService:
|
||||
if key in executable_plan_keys and key in configs
|
||||
},
|
||||
"plans": {key: deepcopy(plans[key]) for key in plans if key in executable_plan_keys},
|
||||
"execution_steps": execution_steps,
|
||||
# A mode card stores only the validated, deterministic plan. Live
|
||||
# delivery evidence belongs to each later execution, not to the
|
||||
# generation workflow.
|
||||
"execution_steps": [],
|
||||
}
|
||||
saved = self.mode_profiles.save_mode(
|
||||
clean_name,
|
||||
@@ -1677,6 +1709,7 @@ class SeatDemoService:
|
||||
_allow_empty_control_result: bool = False,
|
||||
_on_commands_validated: Callable[[dict[str, Any]], None] | None = None,
|
||||
_direct_can_batch: tuple[list[str], dict[str, Any]] | None = None,
|
||||
_direct_mode_profile_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
control_transport = self._validate_control_transport(control_transport)
|
||||
# Creating a reusable scene is a configuration workflow. Synthesize
|
||||
@@ -1702,6 +1735,7 @@ class SeatDemoService:
|
||||
_allow_empty_control_result,
|
||||
_on_commands_validated,
|
||||
_direct_can_batch,
|
||||
_direct_mode_profile_id,
|
||||
)
|
||||
if control_transport == WIRELESS_MQTT_TRANSPORT:
|
||||
# The head unit owns both physical CAN channels in wireless mode.
|
||||
@@ -1723,6 +1757,7 @@ class SeatDemoService:
|
||||
_allow_empty_control_result,
|
||||
_on_commands_validated,
|
||||
_direct_can_batch,
|
||||
_direct_mode_profile_id,
|
||||
)
|
||||
if hardware_access_bypass or self._driver_only_vehicle_execution_active(
|
||||
control_transport
|
||||
@@ -1744,6 +1779,7 @@ class SeatDemoService:
|
||||
_allow_empty_control_result,
|
||||
_on_commands_validated,
|
||||
_direct_can_batch,
|
||||
_direct_mode_profile_id,
|
||||
)
|
||||
authorized_client_id = self.hardware_access.ensure_authorized(hardware_client_id)
|
||||
with self.hardware_execution.reserve():
|
||||
@@ -1764,6 +1800,7 @@ class SeatDemoService:
|
||||
_allow_empty_control_result,
|
||||
_on_commands_validated,
|
||||
_direct_can_batch,
|
||||
_direct_mode_profile_id,
|
||||
)
|
||||
|
||||
def _process_once(
|
||||
@@ -1783,6 +1820,7 @@ class SeatDemoService:
|
||||
allow_empty_control_result: bool = False,
|
||||
on_commands_validated: Callable[[dict[str, Any]], None] | None = None,
|
||||
direct_can_batch: tuple[list[str], dict[str, Any]] | None = None,
|
||||
direct_mode_profile_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
control_transport = self._validate_control_transport(control_transport)
|
||||
utterance = utterance.strip()
|
||||
@@ -1831,6 +1869,14 @@ class SeatDemoService:
|
||||
if not CONVERSATION_ID_PATTERN.fullmatch(conversation_id):
|
||||
raise ValueError("conversation_id must start with conv- and contain only lowercase letters, numbers, or hyphens")
|
||||
vehicle_profile = self.vehicle_profiles.get(vehicle_id)
|
||||
direct_mode_profile = (
|
||||
self.mode_profiles.get_profile(
|
||||
direct_mode_profile_id,
|
||||
vehicle_profile.id,
|
||||
)
|
||||
if direct_mode_profile_id
|
||||
else None
|
||||
)
|
||||
|
||||
if mode_create_name is not None:
|
||||
return self._process_mode_profile_create(
|
||||
@@ -1852,7 +1898,9 @@ class SeatDemoService:
|
||||
)
|
||||
context["mode_profiles"] = self.mode_profiles.public_modes(vehicle_profile.id)
|
||||
matched_modes = (
|
||||
[]
|
||||
[direct_mode_profile]
|
||||
if direct_mode_profile is not None
|
||||
else []
|
||||
if direct_can_batch is not None
|
||||
else self.mode_profiles.match_text(
|
||||
utterance,
|
||||
@@ -1941,7 +1989,56 @@ class SeatDemoService:
|
||||
result,
|
||||
traces,
|
||||
)
|
||||
if direct_can_batch is not None:
|
||||
if direct_mode_profile is not None:
|
||||
mode_requests = self.mode_profiles.requests_for_profiles(
|
||||
[direct_mode_profile]
|
||||
)
|
||||
detected_domains = list(
|
||||
dict.fromkeys(
|
||||
str(item.get("domain") or "")
|
||||
for item in mode_requests
|
||||
if str(item.get("domain") or "")
|
||||
)
|
||||
)
|
||||
parsed = self.model_plan_adapter.adapt(
|
||||
{
|
||||
"detected_domains": detected_domains or ["seat"],
|
||||
"requests": mode_requests,
|
||||
"ambiguities": [],
|
||||
"decision_summary": [
|
||||
f"已载入模式“{direct_mode_profile.name}”保存的结构化操作。"
|
||||
],
|
||||
"confidence": 1.0,
|
||||
},
|
||||
context,
|
||||
vehicle_profile,
|
||||
)
|
||||
saved_native_actions: list[dict[str, Any]] = []
|
||||
for request in mode_requests:
|
||||
parameters = {
|
||||
str(item.get("name")): deepcopy(item.get("value"))
|
||||
for item in request.get("parameters", [])
|
||||
if isinstance(item, dict) and str(item.get("name") or "")
|
||||
}
|
||||
action = {
|
||||
"domain": str(request.get("domain") or ""),
|
||||
"capability": str(request.get("capability") or ""),
|
||||
"target": str(request.get("target_scope") or "vehicle"),
|
||||
"parameters": parameters,
|
||||
"operation": str(request.get("operation") or "set"),
|
||||
"reason": str(request.get("reason") or ""),
|
||||
"execution_backend": "native_can",
|
||||
"parse_source": "saved_mode_command_flow",
|
||||
}
|
||||
if self.native_can_runtime.supports(action):
|
||||
saved_native_actions.append(action)
|
||||
parsed["native_can_actions"] = [
|
||||
*list(parsed.get("native_can_actions", [])),
|
||||
*saved_native_actions,
|
||||
]
|
||||
provider_used = "saved_mode_command_flow"
|
||||
provider_notice = None
|
||||
elif direct_can_batch is not None:
|
||||
parsed = deepcopy(direct_can_batch[1])
|
||||
provider_used = "direct_can_control"
|
||||
provider_notice = None
|
||||
@@ -1980,6 +2077,11 @@ class SeatDemoService:
|
||||
relative_resolution.claimed_controls if relative_resolution else None
|
||||
),
|
||||
)
|
||||
if direct_mode_profile is not None:
|
||||
parsed = self._mark_control_parse_source(
|
||||
parsed,
|
||||
"saved_mode_command_flow",
|
||||
)
|
||||
if local_preview:
|
||||
parsed["ambiguities"] = []
|
||||
parsed["unresolved_spans"] = []
|
||||
@@ -1993,11 +2095,14 @@ class SeatDemoService:
|
||||
parsed = self._partition_native_actions(
|
||||
parsed,
|
||||
promote_supported=(
|
||||
hardware_request
|
||||
and (
|
||||
self.vehicle_mqtt_settings.enabled
|
||||
if control_transport == WIRELESS_MQTT_TRANSPORT
|
||||
else self.vehicle_can_settings.enabled
|
||||
direct_mode_profile is not None
|
||||
or (
|
||||
hardware_request
|
||||
and (
|
||||
self.vehicle_mqtt_settings.enabled
|
||||
if control_transport == WIRELESS_MQTT_TRANSPORT
|
||||
else self.vehicle_can_settings.enabled
|
||||
)
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user