添加NFC绑定红外设备方式,融合车端设备和扩展设备统一控制
This commit is contained in:
+499
-18
@@ -12,6 +12,7 @@ import re
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
from copy import deepcopy
|
||||
from http.cookies import CookieError, SimpleCookie
|
||||
from http import HTTPStatus
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
@@ -39,6 +40,7 @@ from cognitive_agent.application.self_extension import ( # noqa: E402
|
||||
GatewayError,
|
||||
SelfExtensionService,
|
||||
active_peripheral_plan_catalog,
|
||||
partition_active_peripheral_intent,
|
||||
resolve_active_peripheral_command,
|
||||
)
|
||||
from cognitive_agent.application.self_repair import SelfRepairService # noqa: E402
|
||||
@@ -324,6 +326,26 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
||||
HTTPStatus.SERVICE_UNAVAILABLE,
|
||||
)
|
||||
return
|
||||
if path == "/api/extension/peripherals":
|
||||
try:
|
||||
catalog = self.extension.list_peripheral_capabilities()
|
||||
self._json(
|
||||
{
|
||||
"success": True,
|
||||
"catalog": catalog,
|
||||
"count": len(catalog),
|
||||
}
|
||||
)
|
||||
except GatewayError as exc:
|
||||
self._json(
|
||||
{
|
||||
"success": False,
|
||||
"error": "catalog_unavailable",
|
||||
"message": str(exc),
|
||||
},
|
||||
HTTPStatus.SERVICE_UNAVAILABLE,
|
||||
)
|
||||
return
|
||||
if path == "/api/extension/gscu/info":
|
||||
self._handle_extension_gateway("info", {})
|
||||
return
|
||||
@@ -537,11 +559,9 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
||||
else:
|
||||
result = None
|
||||
if path in {"/api/requests", "/api/requests/preview"}:
|
||||
result = self._execute_active_peripheral_request(
|
||||
utterance,
|
||||
provider=provider,
|
||||
conversation_id=conversation_id,
|
||||
execution_mode=execution_mode,
|
||||
result = self._execute_request_with_peripherals(
|
||||
path,
|
||||
common_arguments,
|
||||
)
|
||||
if result is None:
|
||||
if path == "/api/requests/preview":
|
||||
@@ -670,7 +690,10 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
||||
) -> dict[str, object] | None:
|
||||
"""Route a command to the NFC-bound peripheral when one is present."""
|
||||
|
||||
extension_catalog = self.vehicle_display_sync.extension_catalog()
|
||||
# The browser may disappear or reload between learning and execution.
|
||||
# Natural-language routing therefore reads the durable, validated
|
||||
# whitelist instead of trusting the display-sync snapshot.
|
||||
extension_catalog = self.extension.list_peripheral_capabilities()
|
||||
nfc_snapshot = self.nfc.snapshot()
|
||||
plan_catalog = active_peripheral_plan_catalog(
|
||||
utterance,
|
||||
@@ -703,6 +726,7 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
||||
"utterance": utterance,
|
||||
"peripherals": plan_catalog,
|
||||
"vehicle_devices": [],
|
||||
"provider": provider,
|
||||
}
|
||||
)
|
||||
if plan_result.get("needs_confirmation"):
|
||||
@@ -748,6 +772,465 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
||||
)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _residual_has_vehicle_language(utterance: str) -> bool:
|
||||
"""Recognize only obvious vehicle-domain text in a claimed mixed request."""
|
||||
|
||||
return bool(
|
||||
re.search(
|
||||
r"空调|温度|风量|循环|主驾|副驾|座椅|车窗|车门|后门|"
|
||||
r"遮阳帘|阅读灯|氛围灯|室内灯|出风口|风口|"
|
||||
r"\b(?:ac|seat|window|door|shade|light|outlet)\b",
|
||||
utterance,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
)
|
||||
|
||||
def _execute_local_peripheral_actions(
|
||||
self,
|
||||
utterance: str,
|
||||
entries: list[dict[str, object]],
|
||||
*,
|
||||
provider: str,
|
||||
conversation_id: str | None,
|
||||
execution_mode: str,
|
||||
) -> dict[str, object]:
|
||||
"""Execute already validated local-rule infrared capabilities."""
|
||||
|
||||
capabilities = [
|
||||
{
|
||||
**dict(entry["capability"]),
|
||||
"parse_source": "local_rule",
|
||||
}
|
||||
for entry in entries
|
||||
if isinstance(entry, dict) and isinstance(entry.get("capability"), dict)
|
||||
]
|
||||
if not capabilities:
|
||||
raise ExtensionIntegrationError("没有可执行的已学习红外能力。")
|
||||
conversation_turn = 1
|
||||
if conversation_id:
|
||||
context = self.service.conversations.snapshot(conversation_id)
|
||||
conversation_turn = int(context.get("turn_count", 0)) + 1
|
||||
if len(capabilities) == 1:
|
||||
result = self.extension.execute_peripheral_request(
|
||||
utterance,
|
||||
capabilities[0],
|
||||
conversation_id=conversation_id,
|
||||
conversation_turn=conversation_turn,
|
||||
execution_mode=execution_mode,
|
||||
provider_requested=provider,
|
||||
)
|
||||
else:
|
||||
result = self.extension.execute_peripheral_plan_request(
|
||||
utterance,
|
||||
{
|
||||
"summary": f"本地规则命中 {len(capabilities)} 个已学习红外动作。",
|
||||
"provider_used": "infrared_local_rules",
|
||||
"steps": [
|
||||
{
|
||||
"kind": "peripheral",
|
||||
"capability_id": capability["id"],
|
||||
}
|
||||
for capability in capabilities
|
||||
],
|
||||
},
|
||||
capabilities,
|
||||
conversation_id=conversation_id,
|
||||
conversation_turn=conversation_turn,
|
||||
execution_mode=execution_mode,
|
||||
provider_requested=provider,
|
||||
)
|
||||
result["provider_used"] = "infrared_local_rules"
|
||||
self.vehicle_display_sync.update_extension(
|
||||
action={
|
||||
"type": "emit_sequence" if len(capabilities) > 1 else "emit",
|
||||
"source": "main_orchestration_local_rule",
|
||||
"capability_ids": [item["id"] for item in capabilities],
|
||||
"slots": [item["slot"] for item in capabilities],
|
||||
},
|
||||
result=result,
|
||||
)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _merge_can_and_peripheral_results(
|
||||
utterance: str,
|
||||
can_result: dict[str, object],
|
||||
infrared_result: dict[str, object],
|
||||
entries: list[dict[str, object]],
|
||||
) -> dict[str, object]:
|
||||
"""Create one response while retaining each transport's evidence."""
|
||||
|
||||
merged = deepcopy(can_result)
|
||||
can_status = str(can_result.get("status") or "failed")
|
||||
infrared_status = str(infrared_result.get("status") or "failed")
|
||||
successful = {"succeeded", "partially_succeeded", "no_action"}
|
||||
can_ok = can_status in successful
|
||||
infrared_ok = infrared_status in successful
|
||||
|
||||
function_configs = deepcopy(can_result.get("function_configs") or {})
|
||||
function_configs.update(
|
||||
deepcopy(infrared_result.get("function_configs") or {})
|
||||
)
|
||||
command_plans = deepcopy(can_result.get("command_plans") or {})
|
||||
command_plans.update(deepcopy(infrared_result.get("command_plans") or {}))
|
||||
can_operations = list(can_result.get("operation_results") or [])
|
||||
infrared_operations = list(infrared_result.get("operation_results") or [])
|
||||
operation_results = [*can_operations, *infrared_operations]
|
||||
|
||||
can_report = deepcopy(can_result.get("execution_report"))
|
||||
infrared_report = deepcopy(infrared_result.get("execution_report"))
|
||||
execution_report = can_report or infrared_report
|
||||
if isinstance(execution_report, dict):
|
||||
spec = execution_report.setdefault("spec", {})
|
||||
if execution_report is can_report and isinstance(infrared_report, dict):
|
||||
spec["results"] = [
|
||||
*list(spec.get("results") or []),
|
||||
*list((infrared_report.get("spec") or {}).get("results") or []),
|
||||
]
|
||||
spec["status"] = (
|
||||
"succeeded" if can_ok and infrared_ok else "partially_succeeded"
|
||||
)
|
||||
metadata = execution_report.setdefault("metadata", {})
|
||||
labels = metadata.setdefault("labels", {})
|
||||
labels["route"] = "unified_can_infrared"
|
||||
|
||||
infrared_actions = [
|
||||
{
|
||||
"index": entry.get("index"),
|
||||
"clause": entry.get("clause"),
|
||||
"capability_id": capability.get("id"),
|
||||
"device_id": capability.get("device_id"),
|
||||
"device_name": capability.get("device_name"),
|
||||
"action": capability.get("action"),
|
||||
"slot": capability.get("slot"),
|
||||
"parse_source": str(
|
||||
capability.get("parse_source") or "local_rule"
|
||||
),
|
||||
"transport": str(infrared_result.get("control_transport") or "gscu_tcp"),
|
||||
}
|
||||
for entry in entries
|
||||
for capability in [entry.get("capability")]
|
||||
if isinstance(capability, dict)
|
||||
]
|
||||
merged.update(
|
||||
{
|
||||
"utterance": utterance,
|
||||
"status": (
|
||||
"succeeded"
|
||||
if can_ok and infrared_ok
|
||||
else "partially_succeeded"
|
||||
if can_ok or infrared_ok
|
||||
else "failed"
|
||||
),
|
||||
"success": can_ok and infrared_ok,
|
||||
"orchestration_route": "unified_can_infrared",
|
||||
"provider_used": (
|
||||
f"{can_result.get('provider_used') or 'vehicle'}+"
|
||||
f"{infrared_result.get('provider_used') or 'infrared_local_rules'}"
|
||||
),
|
||||
"decision_summary": [
|
||||
*list(can_result.get("decision_summary") or []),
|
||||
*list(infrared_result.get("decision_summary") or []),
|
||||
"CAN 与红外动作已通过独立白名单校验并按各自传输链路执行。",
|
||||
],
|
||||
"function_configs": function_configs,
|
||||
"command_plans": command_plans,
|
||||
"function_config": next(iter(function_configs.values()), None),
|
||||
"command_plan": next(iter(command_plans.values()), None),
|
||||
"execution_report": execution_report,
|
||||
"operation_results": operation_results,
|
||||
"infrared_actions": infrared_actions,
|
||||
"peripheral": infrared_result.get("peripheral"),
|
||||
"control_transports": {
|
||||
"vehicle": can_result.get("control_transport"),
|
||||
"infrared": infrared_result.get("control_transport"),
|
||||
},
|
||||
"phase_execution_reports": {
|
||||
**dict(can_result.get("phase_execution_reports") or {}),
|
||||
"vehicle": can_report,
|
||||
"infrared": infrared_report,
|
||||
},
|
||||
}
|
||||
)
|
||||
return merged
|
||||
|
||||
@staticmethod
|
||||
def _apply_nfc_command_denials(
|
||||
result: dict[str, object],
|
||||
entries: list[dict[str, object]],
|
||||
) -> dict[str, object]:
|
||||
"""Expose command-level NFC denials without discarding allowed work."""
|
||||
|
||||
if not entries:
|
||||
return result
|
||||
denied_actions = [
|
||||
{
|
||||
"index": entry.get("index"),
|
||||
"clause": entry.get("clause"),
|
||||
"capability_id": capability.get("id"),
|
||||
"device_id": capability.get("device_id"),
|
||||
"device_name": capability.get("device_name"),
|
||||
"action": capability.get("action"),
|
||||
"slot": capability.get("slot"),
|
||||
"reason": str(entry.get("reason") or "nfc_command_disabled"),
|
||||
}
|
||||
for entry in entries
|
||||
for capability in [entry.get("capability")]
|
||||
if isinstance(entry, dict) and isinstance(capability, dict)
|
||||
]
|
||||
if not denied_actions:
|
||||
return result
|
||||
|
||||
result["denied_peripheral_actions"] = denied_actions
|
||||
result["authorization_filtered"] = True
|
||||
summaries = list(result.get("decision_summary") or [])
|
||||
labels = "、".join(
|
||||
str(item.get("clause") or item.get("action") or "未命名动作")
|
||||
for item in denied_actions
|
||||
)
|
||||
summaries.append(
|
||||
f"当前 NFC 未授权 {len(denied_actions)} 条红外命令,已过滤:{labels}。"
|
||||
)
|
||||
result["decision_summary"] = summaries
|
||||
|
||||
prior_status = str(result.get("status") or "failed")
|
||||
if prior_status in {"succeeded", "partially_succeeded", "no_action"}:
|
||||
result["status"] = "partially_succeeded"
|
||||
result["success"] = False
|
||||
execution_report = result.get("execution_report")
|
||||
if isinstance(execution_report, dict):
|
||||
spec = execution_report.get("spec")
|
||||
if isinstance(spec, dict):
|
||||
spec["status"] = "partially_succeeded"
|
||||
return result
|
||||
|
||||
def _execute_request_with_peripherals(
|
||||
self,
|
||||
path: str,
|
||||
common_arguments: dict[str, object],
|
||||
*,
|
||||
on_commands_validated: object | None = None,
|
||||
) -> dict[str, object] | None:
|
||||
"""Route IR-only or mixed IR/CAN requests without changing CAN parsing."""
|
||||
|
||||
utterance = str(common_arguments.get("utterance") or "").strip()
|
||||
catalog = self.extension.list_peripheral_capabilities()
|
||||
nfc_snapshot = self.nfc.snapshot()
|
||||
partition = partition_active_peripheral_intent(
|
||||
utterance,
|
||||
catalog,
|
||||
nfc_snapshot,
|
||||
)
|
||||
entries = list(partition.get("peripheral_actions") or [])
|
||||
residual = str(partition.get("residual_utterance") or "").strip()
|
||||
unresolved = list(partition.get("unresolved_peripheral_clauses") or [])
|
||||
denied = list(partition.get("denied_peripheral_actions") or [])
|
||||
provider = str(common_arguments.get("provider") or "auto")
|
||||
conversation_id = (
|
||||
str(common_arguments["conversation_id"])
|
||||
if common_arguments.get("conversation_id")
|
||||
else None
|
||||
)
|
||||
execution_mode = str(common_arguments.get("execution_mode") or "dry_run")
|
||||
infrared_result: dict[str, object] | None = None
|
||||
if unresolved:
|
||||
if provider == "local":
|
||||
raise ExtensionIntegrationError(
|
||||
"本地规则已识别外设,但以下动作未唯一命中已学习能力:"
|
||||
+ "、".join(str(item) for item in unresolved)
|
||||
)
|
||||
candidates = list(partition.get("unresolved_candidates") or [])
|
||||
if not candidates:
|
||||
raise ExtensionIntegrationError("没有可供云端补充选择的已学习红外能力。")
|
||||
plan_result = self.extension.plan(
|
||||
{
|
||||
"utterance": ",".join(str(item) for item in unresolved),
|
||||
"peripherals": candidates,
|
||||
"vehicle_devices": [],
|
||||
"provider": provider,
|
||||
}
|
||||
)
|
||||
if plan_result.get("needs_confirmation"):
|
||||
raise ExtensionIntegrationError(
|
||||
str(plan_result.get("question") or "请明确要控制的红外动作。")
|
||||
)
|
||||
cloud_steps = list(plan_result.get("steps") or [])
|
||||
if not cloud_steps:
|
||||
raise ExtensionIntegrationError("云端模型没有命中已学习红外能力。")
|
||||
candidate_by_id = {
|
||||
str(item.get("id")): {**item, "parse_source": "cloud_model"}
|
||||
for item in candidates
|
||||
if isinstance(item, dict) and item.get("id")
|
||||
}
|
||||
local_capabilities = [
|
||||
{
|
||||
**dict(entry["capability"]),
|
||||
"parse_source": "local_rule",
|
||||
}
|
||||
for entry in entries
|
||||
if isinstance(entry, dict)
|
||||
and isinstance(entry.get("capability"), dict)
|
||||
]
|
||||
selected_cloud = [
|
||||
candidate_by_id[str(step.get("capability_id"))]
|
||||
for step in cloud_steps
|
||||
if isinstance(step, dict)
|
||||
and step.get("kind") == "peripheral"
|
||||
and str(step.get("capability_id")) in candidate_by_id
|
||||
]
|
||||
execution_catalog = {
|
||||
str(item["id"]): item for item in [*local_capabilities, *selected_cloud]
|
||||
}
|
||||
combined_steps = [
|
||||
{"kind": "peripheral", "capability_id": item["id"]}
|
||||
for item in local_capabilities
|
||||
] + cloud_steps
|
||||
conversation_turn = 1
|
||||
if conversation_id:
|
||||
context = self.service.conversations.snapshot(conversation_id)
|
||||
conversation_turn = int(context.get("turn_count", 0)) + 1
|
||||
infrared_result = self.extension.execute_peripheral_plan_request(
|
||||
utterance,
|
||||
{
|
||||
**plan_result,
|
||||
"steps": combined_steps,
|
||||
},
|
||||
list(execution_catalog.values()),
|
||||
conversation_id=conversation_id,
|
||||
conversation_turn=conversation_turn,
|
||||
execution_mode=execution_mode,
|
||||
provider_requested=provider,
|
||||
)
|
||||
next_index = max(
|
||||
[int(entry.get("index", 0)) for entry in entries if isinstance(entry, dict)],
|
||||
default=-1,
|
||||
) + 1
|
||||
entries.extend(
|
||||
{
|
||||
"index": next_index + index,
|
||||
"clause": ",".join(str(item) for item in unresolved),
|
||||
"capability": capability,
|
||||
}
|
||||
for index, capability in enumerate(selected_cloud)
|
||||
)
|
||||
if not entries and not denied:
|
||||
return self._execute_active_peripheral_request(
|
||||
utterance,
|
||||
provider=provider,
|
||||
conversation_id=conversation_id,
|
||||
execution_mode=execution_mode,
|
||||
)
|
||||
if not entries:
|
||||
if not residual:
|
||||
labels = "、".join(
|
||||
str(item.get("clause") or "未命名动作")
|
||||
for item in denied
|
||||
if isinstance(item, dict)
|
||||
)
|
||||
raise ExtensionIntegrationError(
|
||||
f"当前 NFC 未授权执行以下红外命令:{labels}。"
|
||||
)
|
||||
can_arguments = {**common_arguments, "utterance": residual}
|
||||
callback = (
|
||||
on_commands_validated
|
||||
if callable(on_commands_validated)
|
||||
else None
|
||||
)
|
||||
if path == "/api/requests/preview":
|
||||
result = self.service.preview_native_can(
|
||||
**can_arguments,
|
||||
on_commands_validated=callback,
|
||||
)
|
||||
else:
|
||||
result = self.service.process(
|
||||
**can_arguments,
|
||||
_on_commands_validated=callback,
|
||||
)
|
||||
result = self._apply_nfc_command_denials(result, denied)
|
||||
self.service.persist_composed_result(result)
|
||||
if callback is not None:
|
||||
callback(result)
|
||||
return result
|
||||
if (
|
||||
infrared_result is None
|
||||
and residual
|
||||
and not self._residual_has_vehicle_language(residual)
|
||||
):
|
||||
# Preserve the richer existing ordered/delay peripheral planner
|
||||
# when the remaining text contains no vehicle-domain request.
|
||||
planned = self._execute_active_peripheral_request(
|
||||
utterance,
|
||||
provider=provider,
|
||||
conversation_id=conversation_id,
|
||||
execution_mode=execution_mode,
|
||||
)
|
||||
if planned is not None:
|
||||
return planned
|
||||
if infrared_result is None:
|
||||
infrared_result = self._execute_local_peripheral_actions(
|
||||
utterance,
|
||||
entries,
|
||||
provider=provider,
|
||||
conversation_id=conversation_id,
|
||||
execution_mode=execution_mode,
|
||||
)
|
||||
if not residual:
|
||||
infrared_result = self._apply_nfc_command_denials(
|
||||
infrared_result, denied
|
||||
)
|
||||
if denied:
|
||||
self.service.persist_composed_result(infrared_result)
|
||||
return infrared_result
|
||||
|
||||
can_arguments = {**common_arguments, "utterance": residual}
|
||||
callback = on_commands_validated if callable(on_commands_validated) else None
|
||||
can_validation_callback = None
|
||||
if callback is not None:
|
||||
def publish_combined_validation(
|
||||
validated_can: dict[str, object],
|
||||
) -> None:
|
||||
# Native CAN publishes its fully compiled commands before the
|
||||
# physical executor starts. A mixed request has already
|
||||
# compiled (and in hardware mode may already have submitted)
|
||||
# the infrared phase, so never replace the UI with this
|
||||
# CAN-only intermediate snapshot. If CAN subsequently fails,
|
||||
# this combined validation event remains the authoritative
|
||||
# parsed-command view with per-operation delivery evidence.
|
||||
validated = self._merge_can_and_peripheral_results(
|
||||
utterance,
|
||||
validated_can,
|
||||
infrared_result,
|
||||
entries,
|
||||
)
|
||||
validated = self._apply_nfc_command_denials(validated, denied)
|
||||
validated["status"] = "processing"
|
||||
validated["phase"] = "commands_validated"
|
||||
callback(validated)
|
||||
|
||||
can_validation_callback = publish_combined_validation
|
||||
if path == "/api/requests/preview":
|
||||
can_result = self.service.preview_native_can(
|
||||
**can_arguments,
|
||||
on_commands_validated=can_validation_callback,
|
||||
)
|
||||
else:
|
||||
can_result = self.service.process(
|
||||
**can_arguments,
|
||||
_on_commands_validated=can_validation_callback,
|
||||
)
|
||||
combined = self._merge_can_and_peripheral_results(
|
||||
utterance,
|
||||
can_result,
|
||||
infrared_result,
|
||||
entries,
|
||||
)
|
||||
combined = self._apply_nfc_command_denials(combined, denied)
|
||||
self.service.persist_composed_result(combined)
|
||||
if callback is not None:
|
||||
callback(combined)
|
||||
return combined
|
||||
|
||||
def _stream_control_request(
|
||||
self,
|
||||
path: str,
|
||||
@@ -819,17 +1302,10 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
||||
try:
|
||||
result = None
|
||||
if path in {"/api/requests", "/api/requests/preview"}:
|
||||
result = self._execute_active_peripheral_request(
|
||||
utterance,
|
||||
provider=str(common_arguments.get("provider") or "auto"),
|
||||
conversation_id=(
|
||||
str(common_arguments["conversation_id"])
|
||||
if common_arguments.get("conversation_id")
|
||||
else None
|
||||
),
|
||||
execution_mode=str(
|
||||
common_arguments.get("execution_mode") or "dry_run"
|
||||
),
|
||||
result = self._execute_request_with_peripherals(
|
||||
path,
|
||||
common_arguments,
|
||||
on_commands_validated=on_commands_validated,
|
||||
)
|
||||
if result is None:
|
||||
if path == "/api/requests/preview":
|
||||
@@ -1052,7 +1528,12 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
||||
catalog = payload.get("peripherals", payload.get("catalog", []))
|
||||
if not isinstance(catalog, list) or len(catalog) > 248:
|
||||
raise ValueError("外设能力目录必须是至多 248 项的数组。")
|
||||
sync_result = {"status": "ready", "count": len(catalog)}
|
||||
durable_sync = self.extension.sync_peripheral_catalog(catalog)
|
||||
sync_result = {
|
||||
"status": "ready",
|
||||
"count": len(catalog),
|
||||
"learned_count": durable_sync["count"],
|
||||
}
|
||||
self.vehicle_display_sync.update_extension(
|
||||
catalog=catalog,
|
||||
action={
|
||||
|
||||
Reference in New Issue
Block a user