feat(n7web): enhance control workflows and status feedback
This commit is contained in:
@@ -12,6 +12,7 @@ import ssl
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
from collections.abc import Mapping
|
||||
from contextlib import closing
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
@@ -955,7 +956,7 @@ def _peripheral_execution_identity_authorized(
|
||||
capability: dict[str, Any],
|
||||
) -> bool:
|
||||
# Saved modes execute an immutable, previously validated command flow and
|
||||
# are rebound to the same live NFC/device/slot tuple before reaching here.
|
||||
# are rebound to the same persisted NFC/device/slot tuple before reaching here.
|
||||
if capability.get("parse_source") == "saved_mode_command_flow":
|
||||
return True
|
||||
return bool(_device_match_score(_compact_match_text(utterance), capability))
|
||||
@@ -1089,7 +1090,7 @@ def _deterministic_peripheral_steps(
|
||||
utterance: str,
|
||||
catalog: list[dict[str, Any]],
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Build an ordered live-peripheral plan when every phrase is unambiguous."""
|
||||
"""Build an ordered bound-peripheral plan when every phrase is unambiguous."""
|
||||
|
||||
compact_utterance = _compact_match_text(utterance)
|
||||
learned = [
|
||||
@@ -1229,11 +1230,13 @@ def resolve_active_peripheral_command(
|
||||
catalog: list[dict[str, Any]],
|
||||
nfc_snapshot: dict[str, Any],
|
||||
) -> dict[str, Any] | None:
|
||||
"""Resolve an exact learned action for the NFC card that is present now.
|
||||
"""Resolve an exact learned action from the durable NFC-bound catalog.
|
||||
|
||||
An exact peripheral name is required so generic vehicle phrases such as
|
||||
``打开氛围灯`` keep using the native vehicle domain. Action synonyms are
|
||||
accepted only after the peripheral identity and live card binding match.
|
||||
accepted only after the peripheral identity and persisted binding match.
|
||||
``nfc_snapshot`` is retained in the public signature for compatibility;
|
||||
card presence is intentionally not an execution prerequisite.
|
||||
"""
|
||||
|
||||
return _resolve_bound_peripheral_command(
|
||||
@@ -1251,20 +1254,17 @@ def _resolve_bound_peripheral_command(
|
||||
*,
|
||||
require_enabled: bool,
|
||||
) -> dict[str, Any] | None:
|
||||
"""Resolve a card-bound command, optionally including disabled entries.
|
||||
"""Resolve a persistently card-bound command, including disabled entries.
|
||||
|
||||
Disabled entries are inspected only so the partitioner can report that a
|
||||
requested action was filtered by the NFC command allowlist. Execution
|
||||
requested action was filtered by the persisted command allowlist. Execution
|
||||
continues to use :func:`resolve_active_peripheral_command`, which always
|
||||
requires ``enabled`` to be true.
|
||||
"""
|
||||
|
||||
del nfc_snapshot # Presence is display state, not persistent authorization.
|
||||
compact_utterance = _compact_match_text(utterance)
|
||||
card = nfc_snapshot.get("card") if isinstance(nfc_snapshot, dict) else None
|
||||
active_uid = _normalized_card_uid(
|
||||
card.get("uid") if isinstance(card, dict) else None
|
||||
)
|
||||
if not compact_utterance or not active_uid:
|
||||
if not compact_utterance:
|
||||
return None
|
||||
|
||||
matches: list[tuple[int, int, dict[str, Any]]] = []
|
||||
@@ -1278,7 +1278,8 @@ def _resolve_bound_peripheral_command(
|
||||
)
|
||||
):
|
||||
continue
|
||||
if _normalized_card_uid(raw_item.get("card_uid")) != active_uid:
|
||||
bound_uid = _normalized_card_uid(raw_item.get("card_uid"))
|
||||
if not bound_uid:
|
||||
continue
|
||||
device_score = _device_match_score(compact_utterance, raw_item)
|
||||
if not device_score:
|
||||
@@ -1308,7 +1309,7 @@ def _resolve_bound_peripheral_command(
|
||||
|
||||
resolved = dict(matches[0][2])
|
||||
resolved["slot"] = validate_slot(resolved.get("slot"))
|
||||
resolved["card_uid"] = active_uid
|
||||
resolved["card_uid"] = _normalized_card_uid(resolved.get("card_uid"))
|
||||
return resolved
|
||||
|
||||
|
||||
@@ -1326,17 +1327,13 @@ def partition_active_peripheral_intent(
|
||||
"""Claim only explicit learned-peripheral clauses and preserve CAN text.
|
||||
|
||||
The function is deliberately conservative: a clause must contain both a
|
||||
live NFC-bound device identity and one learned action. When no peripheral
|
||||
persistently NFC-bound device identity and one learned action. When no peripheral
|
||||
clause is claimed, ``residual_utterance`` is byte-for-byte the original
|
||||
trimmed input so the existing CAN parser observes exactly its old input.
|
||||
"""
|
||||
|
||||
original = str(utterance or "").strip()
|
||||
card = nfc_snapshot.get("card") if isinstance(nfc_snapshot, dict) else None
|
||||
active_uid = _normalized_card_uid(
|
||||
card.get("uid") if isinstance(card, dict) else None
|
||||
)
|
||||
if not original or not active_uid:
|
||||
if not original:
|
||||
return {
|
||||
"peripheral_actions": [],
|
||||
"residual_utterance": original,
|
||||
@@ -1350,7 +1347,7 @@ def partition_active_peripheral_intent(
|
||||
for item in catalog[:MAX_CATALOG_ITEMS]
|
||||
if isinstance(item, dict)
|
||||
and item.get("status") == "learned"
|
||||
and _normalized_card_uid(item.get("card_uid")) == active_uid
|
||||
and bool(_normalized_card_uid(item.get("card_uid")))
|
||||
]
|
||||
active_catalog = [
|
||||
item
|
||||
@@ -1541,14 +1538,11 @@ def active_peripheral_plan_catalog(
|
||||
catalog: list[dict[str, Any]],
|
||||
nfc_snapshot: dict[str, Any],
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Return the live-card catalog when a phrase needs an ordered plan."""
|
||||
"""Return the durable bound catalog when a phrase needs an ordered plan."""
|
||||
|
||||
del nfc_snapshot # Presence is display state, not persistent authorization.
|
||||
compact_utterance = _compact_match_text(utterance)
|
||||
card = nfc_snapshot.get("card") if isinstance(nfc_snapshot, dict) else None
|
||||
active_uid = _normalized_card_uid(
|
||||
card.get("uid") if isinstance(card, dict) else None
|
||||
)
|
||||
if not compact_utterance or not active_uid:
|
||||
if not compact_utterance:
|
||||
return []
|
||||
has_timing = bool(
|
||||
re.search(r"\d+(?:\.\d+)?(?:ms|s|毫秒|秒|分钟|分)", compact_utterance)
|
||||
@@ -1568,13 +1562,14 @@ def active_peripheral_plan_catalog(
|
||||
or raw_item.get("enabled", True) is False
|
||||
):
|
||||
continue
|
||||
if _normalized_card_uid(raw_item.get("card_uid")) != active_uid:
|
||||
bound_uid = _normalized_card_uid(raw_item.get("card_uid"))
|
||||
if not bound_uid:
|
||||
continue
|
||||
if not _device_match_score(compact_utterance, raw_item):
|
||||
continue
|
||||
item = dict(raw_item)
|
||||
item["slot"] = validate_slot(item.get("slot"))
|
||||
item["card_uid"] = active_uid
|
||||
item["card_uid"] = bound_uid
|
||||
active.append(item)
|
||||
if _has_unshadowed_vehicle_device_phrase(compact_utterance, active):
|
||||
return []
|
||||
@@ -2378,6 +2373,61 @@ class SelfExtensionService:
|
||||
result["learning_capture_confirmed"] = False
|
||||
return result
|
||||
|
||||
def execute_bound_infrared_action(
|
||||
self,
|
||||
action: Mapping[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
"""Re-authorize and emit one car-originated ``extension/ctl`` action."""
|
||||
|
||||
if (
|
||||
not isinstance(action, Mapping)
|
||||
or action.get("type") != "infrared_emit"
|
||||
or action.get("domain") != "self_extension"
|
||||
or action.get("capability") != "infrared.emit"
|
||||
or action.get("operation") != "emit"
|
||||
):
|
||||
raise ExtensionIntegrationError("车端外设控制动作格式无效。")
|
||||
device_id = str(action.get("target_scope") or "").strip()
|
||||
parameters = action.get("parameters")
|
||||
if not device_id or not isinstance(parameters, Mapping):
|
||||
raise ExtensionIntegrationError("车端外设控制动作缺少设备或参数。")
|
||||
slot = validate_slot(parameters.get("slot"))
|
||||
action_name = str(parameters.get("action") or "").strip()
|
||||
if not action_name:
|
||||
raise ExtensionIntegrationError("车端外设控制动作名称不能为空。")
|
||||
|
||||
capability = next(
|
||||
(
|
||||
item
|
||||
for item in self.list_peripheral_capabilities()
|
||||
if isinstance(item, dict)
|
||||
and item.get("status") == "learned"
|
||||
and item.get("enabled") is True
|
||||
and bool(_normalized_card_uid(item.get("card_uid")))
|
||||
and str(item.get("device_id") or "").strip() == device_id
|
||||
and item.get("slot") == slot
|
||||
and str(item.get("action") or "").strip() == action_name
|
||||
),
|
||||
None,
|
||||
)
|
||||
if capability is None:
|
||||
raise ExtensionIntegrationError(
|
||||
"车端外设控制动作未通过本机持久能力鉴权:"
|
||||
f"{device_id} / {action_name} / IR {slot}。"
|
||||
)
|
||||
|
||||
result = self.execute_gateway("emit", {"slot": slot})
|
||||
return {
|
||||
**result,
|
||||
"authorization": {
|
||||
"source": "local_persistent_extension_allowlist",
|
||||
"capability_id": capability.get("id"),
|
||||
"device_id": device_id,
|
||||
"action": action_name,
|
||||
"slot": slot,
|
||||
},
|
||||
}
|
||||
|
||||
def execute_peripheral_request(
|
||||
self,
|
||||
utterance: str,
|
||||
@@ -2389,7 +2439,7 @@ class SelfExtensionService:
|
||||
provider_requested: str = "auto",
|
||||
on_commands_validated: Callable[[dict[str, Any]], None] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Execute a live-NFC peripheral match through the main request API."""
|
||||
"""Execute a persistently NFC-bound match through the main request API."""
|
||||
|
||||
if not _peripheral_execution_identity_authorized(utterance, capability):
|
||||
raise ExtensionIntegrationError(
|
||||
@@ -2536,14 +2586,14 @@ class SelfExtensionService:
|
||||
"execution_mode": execution_mode,
|
||||
"control_transport": control_transport,
|
||||
"decision_summary": [
|
||||
f"检测到在场 NFC {capability.get('card_uid')},已优先匹配绑定外设。",
|
||||
f"已通过持久 NFC 绑定 {capability.get('card_uid')} 匹配外设能力。",
|
||||
f"命中已学习动作:{capability.get('device_name')} · {capability.get('action')}(槽位 {slot})。",
|
||||
],
|
||||
"trace_events": [
|
||||
{
|
||||
"stage": "nfc_peripheral_route",
|
||||
"status": "succeeded",
|
||||
"summary": "在场 NFC、外设名称与已学习动作均匹配",
|
||||
"summary": "持久 NFC 绑定、外设名称与已学习动作均匹配",
|
||||
"duration_ms": 0,
|
||||
}
|
||||
],
|
||||
@@ -2592,13 +2642,13 @@ class SelfExtensionService:
|
||||
provider_requested: str = "auto",
|
||||
on_commands_validated: Callable[[dict[str, Any]], None] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Execute a validated ordered peripheral/delay plan for the live NFC."""
|
||||
"""Execute a validated ordered plan from durable NFC-bound abilities."""
|
||||
|
||||
steps = plan_result.get("steps", [])
|
||||
if not isinstance(steps, list) or not steps:
|
||||
raise ExtensionIntegrationError("GPT 没有生成可执行的外设动作。")
|
||||
if any(step.get("kind") == "vehicle" for step in steps if isinstance(step, dict)):
|
||||
raise ExtensionIntegrationError("当前主编排只允许执行在场 NFC 外设计划。")
|
||||
raise ExtensionIntegrationError("当前主编排只允许执行持久绑定的 NFC 外设计划。")
|
||||
catalog_by_id = {
|
||||
str(item.get("id")): item
|
||||
for item in catalog
|
||||
@@ -2639,9 +2689,14 @@ class SelfExtensionService:
|
||||
raise ExtensionIntegrationError("GPT 计划包含未知步骤类型。")
|
||||
capability_id = str(step.get("capability_id") or "")
|
||||
capability = catalog_by_id.get(capability_id)
|
||||
if capability is None or capability.get("status") != "learned":
|
||||
if (
|
||||
capability is None
|
||||
or capability.get("status") != "learned"
|
||||
or capability.get("enabled", True) is False
|
||||
or not _normalized_card_uid(capability.get("card_uid"))
|
||||
):
|
||||
raise ExtensionIntegrationError(
|
||||
"GPT 计划引用了不在当前 NFC 目录中的能力。"
|
||||
"GPT 计划引用了未学习、未启用或缺少持久 NFC 绑定的能力。"
|
||||
)
|
||||
if not _peripheral_execution_identity_authorized(utterance, capability):
|
||||
raise ExtensionIntegrationError(
|
||||
@@ -2880,6 +2935,37 @@ class SelfExtensionService:
|
||||
}
|
||||
operations.append(operation)
|
||||
if interrupted:
|
||||
for remaining_index in range(index + 1, len(steps)):
|
||||
remaining_step = steps[remaining_index]
|
||||
if remaining_step.get("kind") != "peripheral":
|
||||
continue
|
||||
(
|
||||
_remaining_capability_id,
|
||||
_remaining_capability,
|
||||
remaining_slot,
|
||||
remaining_operation_ref,
|
||||
) = compiled_peripheral_steps[remaining_index]
|
||||
operations.append(
|
||||
{
|
||||
"ref": remaining_operation_ref,
|
||||
"domain": "self_extension",
|
||||
"status": "aborted",
|
||||
"message": "前序红外指令失败,未执行",
|
||||
"telemetry": {
|
||||
"transport": transport,
|
||||
"slot": remaining_slot,
|
||||
"delivery_status": "not_submitted",
|
||||
"transport_submitted": False,
|
||||
"gateway_acknowledged": False,
|
||||
"physical_effect_confirmed": False,
|
||||
"command": build_gateway_command(
|
||||
"emit", {"slot": remaining_slot}
|
||||
),
|
||||
"effect_mode": "one_shot",
|
||||
"abort_reason": "prior_operation_failed",
|
||||
},
|
||||
}
|
||||
)
|
||||
break
|
||||
if (
|
||||
hardware_request
|
||||
@@ -2943,10 +3029,10 @@ class SelfExtensionService:
|
||||
else "GPT 已生成外设顺序计划。"
|
||||
)
|
||||
),
|
||||
f"计划仅引用当前在场 NFC {first_capability.get('card_uid')} 的已学习能力。",
|
||||
f"计划仅引用持久 NFC 绑定 {first_capability.get('card_uid')} 的已学习能力。",
|
||||
*(
|
||||
[
|
||||
"当前 NFC 未学习专用闪烁能力;为避免灯具停在关闭态及学习器持续蜂鸣,"
|
||||
"当前绑定未学习专用闪烁能力;为避免灯具停在关闭态及学习器持续蜂鸣,"
|
||||
"闪烁已安全降级为定色保持。"
|
||||
]
|
||||
if steady_blink_fallbacks
|
||||
|
||||
Reference in New Issue
Block a user