4563 lines
185 KiB
Python
4563 lines
185 KiB
Python
"""Codex App Server orchestration for the web-console assistant."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import hmac
|
||
import json
|
||
import logging
|
||
import os
|
||
import queue
|
||
import re
|
||
import secrets
|
||
import threading
|
||
import time
|
||
import uuid
|
||
from collections import OrderedDict
|
||
from collections.abc import Callable, Mapping, Sequence
|
||
from copy import deepcopy
|
||
from dataclasses import dataclass, field
|
||
from pathlib import Path
|
||
from typing import Protocol
|
||
|
||
from cognitive_agent.application.assistant_plan_guard import MAX_PLAN_REQUESTS
|
||
from cognitive_agent.application.mode_profile_intent import (
|
||
is_mode_profile_query,
|
||
mode_creation_source_name,
|
||
normalize_mode_profile_name,
|
||
parse_mode_profile_create,
|
||
parse_mode_profile_delete,
|
||
parse_mode_profile_update,
|
||
)
|
||
from cognitive_agent.application.mode_creation_seat_policy import (
|
||
ModeCreationSeatPolicyError,
|
||
project_seed_plan,
|
||
)
|
||
from cognitive_agent.application.mode_schedule import ModeSchedule
|
||
from cognitive_agent.application.mode_schedule_service import public_schedule_status
|
||
from cognitive_agent.application.mode_scene_ingress import MAX_MODE_SCENE_ACTIONS
|
||
from cognitive_agent.application.romantic_lighting import (
|
||
romantic_light_override_refs,
|
||
romantic_scene,
|
||
)
|
||
from cognitive_agent.application.scene_composer import (
|
||
SCENE_COMPOSER_INSTRUCTIONS,
|
||
SCENE_PLANNER_VERSION,
|
||
SCENE_TEMPLATE_AUGMENTATION_INSTRUCTIONS,
|
||
SCHEDULE_COMPOSER_INSTRUCTIONS,
|
||
SceneCompositionError,
|
||
build_scene_output_schema,
|
||
normalize_scene_output,
|
||
scene_context_for_mode_creation,
|
||
scene_has_explicit_control_object,
|
||
scene_required_native_fallbacks,
|
||
scene_required_peripheral_refs,
|
||
scene_semantic_guidance,
|
||
)
|
||
from cognitive_agent.application.scene_templates import (
|
||
PRESERVE_BASE_EXTERNAL_LIGHT_TEMPLATE_IDS,
|
||
normalize_scene_template_name,
|
||
)
|
||
from cognitive_agent.application.schedule_intent import is_schedule_stop, needs_mode_schedule
|
||
|
||
LOGGER = logging.getLogger(__name__)
|
||
INTERNAL_SCENE_BUILDER_MARKER = "根据该场景名称使用当前可控原子能力"
|
||
|
||
|
||
class CodexAssistantError(RuntimeError):
|
||
"""Safe failure raised by the embedded Codex assistant."""
|
||
|
||
|
||
class CodexVehicleToolError(CodexAssistantError):
|
||
"""Safe failure returned to the vehicle MCP bridge."""
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class ActiveCodexTurn:
|
||
thread_id: str
|
||
turn_id: str
|
||
events: queue.Queue[dict[str, object]]
|
||
|
||
|
||
class CodexClient(Protocol):
|
||
@property
|
||
def available(self) -> bool: ...
|
||
|
||
def start(self, callback_url: str, callback_token: str) -> None: ...
|
||
|
||
def close(self) -> None: ...
|
||
|
||
@property
|
||
def status(self) -> str: ...
|
||
|
||
def start_thread(
|
||
self,
|
||
*,
|
||
developer_instructions: str,
|
||
ephemeral: bool = True,
|
||
replace_developer_instructions: bool = False,
|
||
timeout: float | None = None,
|
||
) -> str: ...
|
||
|
||
def start_turn(
|
||
self,
|
||
thread_id: str,
|
||
text: str,
|
||
request_token: str,
|
||
*,
|
||
application_context: Mapping[str, object] | None = None,
|
||
) -> ActiveCodexTurn: ...
|
||
|
||
def start_structured_turn(
|
||
self,
|
||
thread_id: str,
|
||
text: str,
|
||
*,
|
||
application_context: Mapping[str, object],
|
||
output_schema: Mapping[str, object],
|
||
timeout: float | None = None,
|
||
) -> ActiveCodexTurn: ...
|
||
|
||
def finish_turn(
|
||
self,
|
||
turn: ActiveCodexTurn,
|
||
timeout: float | None = None,
|
||
) -> None: ...
|
||
|
||
def interrupt(
|
||
self,
|
||
turn: ActiveCodexTurn,
|
||
timeout: float | None = None,
|
||
) -> None: ...
|
||
|
||
def unsubscribe_thread(
|
||
self,
|
||
thread_id: str,
|
||
timeout: float | None = None,
|
||
) -> None: ...
|
||
|
||
def abandon_turn(self, turn: ActiveCodexTurn) -> None: ...
|
||
|
||
|
||
EmitEvent = Callable[[dict[str, object]], bool | None]
|
||
# The third argument is an optional, untrusted structured plan. Legacy/local
|
||
# calls omit it; the application validates it against the original request
|
||
# before it can reach the normal orchestration pipeline.
|
||
ControlExecutor = Callable[..., dict[str, object]]
|
||
CapabilityQuery = Callable[[Mapping[str, object], str], dict[str, object]]
|
||
|
||
|
||
@dataclass
|
||
class _Conversation:
|
||
thread_id: str
|
||
touched_at: float = field(default_factory=time.monotonic)
|
||
lock: threading.Lock = field(default_factory=threading.Lock)
|
||
in_use: int = 0
|
||
|
||
|
||
@dataclass
|
||
class _SceneFlight:
|
||
ready: threading.Event = field(default_factory=threading.Event)
|
||
plan: dict[str, object] | None = None
|
||
error: BaseException | None = None
|
||
|
||
|
||
@dataclass
|
||
class _VehicleTurnContext:
|
||
original_message: str
|
||
execute_control: ControlExecutor
|
||
authorization: _VehicleAuthorization
|
||
capability_snapshot: Mapping[str, object]
|
||
query_capabilities: CapabilityQuery
|
||
events: queue.Queue[dict[str, object]] = field(default_factory=queue.Queue)
|
||
lock: threading.Lock = field(default_factory=threading.Lock)
|
||
called: bool = False
|
||
capability_queries: int = 0
|
||
action_succeeded: bool | None = None
|
||
action_state: str | None = None
|
||
action_result: Mapping[str, object] | None = None
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class _VehicleAuthorization:
|
||
instruction: str | None
|
||
reason: str
|
||
|
||
|
||
class _ScenePlannerFallback(RuntimeError):
|
||
"""Internal signal that the general Codex path should handle this request."""
|
||
|
||
|
||
class _ScenePlannerTimeout(RuntimeError):
|
||
"""Internal signal that the bounded scene-planning deadline expired."""
|
||
|
||
|
||
class _ScenePlannerOverloaded(RuntimeError):
|
||
"""Internal signal that all bounded scene-planning slots are busy."""
|
||
|
||
|
||
class _ScenePlannerCancelled(RuntimeError):
|
||
"""Internal signal that the browser stopped accepting streamed events."""
|
||
|
||
|
||
_CONTROL_VERB_PATTERN = re.compile(
|
||
r"打开|开启|关闭|关掉|关上|启动|停止|设置|调节|调整|调高|调低|升高|降低|"
|
||
r"增加|减少|切换|进入|退出|复位|移动|伸出|收回|开到|关到|调到|降到|升到|"
|
||
r"拉开|拉上|锁车|上锁|解锁|执行|运行|启用|停用|保存|记住|恢复|召回|调用|"
|
||
r"前移|后退|前进|调直|放倒|抬高|放低|半开|标定|抬到|降下|升起|"
|
||
r"帮我|替我|给我|请把|请将"
|
||
)
|
||
_CONTROL_VALUE_PATTERN = re.compile(r"-?\d+(?:\.\d+)?(?:%|%|度|档|秒|毫秒|ms)", re.IGNORECASE)
|
||
_SUPPORT_QUERY_PATTERN = re.compile(
|
||
r"支持|能否|能不能|可不可以|有没有|是否|哪些(?:设备|能力|功能)|能做什么"
|
||
)
|
||
_DENIAL_PATTERN = re.compile(
|
||
r"不要|无需|不用|禁止|不许|不得|取消操作|"
|
||
r"(?:不|别)(?:执行|下发|发送|操作|控制|调整)|"
|
||
r"(?:只|仅)(?:需|要)?(?:做)?(?:解析|预览|展示|显示)|"
|
||
r"(?:只|仅)(?:需|要)?(?:给出|返回)(?:解析(?:结果)?|控制(?:指令|命令))|"
|
||
r"只回答|仅回答"
|
||
)
|
||
_KNOWLEDGE_PATTERN = re.compile(
|
||
r"为什么|什么是|解释|介绍|分析|科普|教程|如何|怎么做|"
|
||
r"如果|假如|假设|举例|示例|翻译|改写|总结|复述|引用|造句|"
|
||
r"故事|剧本|小说|作文|角色|主角|这句话|以下内容|"
|
||
r"忽略.{0,12}(规则|指令|提示)|系统提示|调用工具"
|
||
)
|
||
_NON_VEHICLE_WORK_PATTERN = re.compile(
|
||
r"文件|代码|源码|网页|浏览器|文档|数据库|目录|仓库|项目|终端|命令行|日志"
|
||
)
|
||
_MODE_EDITOR_REQUEST_ID_PATTERN = re.compile(r"^req-[a-z0-9-]{6,100}$")
|
||
_HOT_DISCOMFORT_PATTERN = re.compile(r"热|闷热|好热|太晒")
|
||
_COLD_DISCOMFORT_PATTERN = re.compile(r"冷|好冷")
|
||
_IMPLICIT_HOT_REQUEST_PATTERN = re.compile(
|
||
r"^(?:(?:我|这里|车里|车内|车上|座舱|舱内)(?:感觉|觉得)?(?:有点|太|很|好)?|"
|
||
r"(?:有点|太|很|好))(?:闷热|热|晒)(?:了|啊|呀)?"
|
||
r"(?:(?:请)?(?:帮我)?(?:处理|调节|调整|解决)(?:一下)?|怎么办)?$"
|
||
)
|
||
_IMPLICIT_COLD_REQUEST_PATTERN = re.compile(
|
||
r"^(?:(?:我|这里|车里|车内|车上|座舱|舱内)(?:感觉|觉得)?(?:有点|太|很|好)?|"
|
||
r"(?:有点|太|很|好))冷(?:了|啊|呀)?"
|
||
r"(?:(?:请)?(?:帮我)?(?:处理|调节|调整|解决)(?:一下)?|怎么办)?$"
|
||
)
|
||
_COOLING_INSTRUCTION = "打开空调,将全车温度设置为23度,风量设置为2档"
|
||
_WARMING_INSTRUCTION = "打开空调,将全车温度设置为26度,风量设置为2档"
|
||
_FAST_COMFORT_REASONS = frozenset({"implicit_hot_comfort", "implicit_cold_comfort"})
|
||
|
||
|
||
def _normalize_instruction(value: object) -> str:
|
||
return re.sub(r"[\s,。!?!?、;;::]", "", str(value).lower())
|
||
|
||
|
||
def _safe_authoritative_suggestions(
|
||
value: Sequence[Mapping[str, object]],
|
||
) -> list[dict[str, object]]:
|
||
"""Bound server-owned quick controls before placing them on the stream."""
|
||
|
||
suggestions: list[dict[str, object]] = []
|
||
seen_ids: set[str] = set()
|
||
for raw in value[:6]:
|
||
suggestion_id = str(raw.get("id") or "").strip()
|
||
label = " ".join(str(raw.get("label") or "").strip().split())
|
||
command = " ".join(str(raw.get("command") or "").strip().split())
|
||
domain = str(raw.get("domain") or "").strip()
|
||
if (
|
||
not re.fullmatch(r"[a-z0-9][a-z0-9-]{0,63}", suggestion_id)
|
||
or suggestion_id in seen_ids
|
||
or not label
|
||
or len(label) > 80
|
||
or not command
|
||
or len(command) > 160
|
||
or not re.fullmatch(r"[a-z][a-z0-9_]{0,39}", domain)
|
||
):
|
||
continue
|
||
raw_capability_ids = raw.get("capability_ids")
|
||
capability_ids = (
|
||
[
|
||
capability_id
|
||
for item in raw_capability_ids[:8]
|
||
for capability_id in [str(item or "").strip()]
|
||
if re.fullmatch(r"[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*", capability_id)
|
||
]
|
||
if isinstance(raw_capability_ids, Sequence)
|
||
and not isinstance(raw_capability_ids, (str, bytes, bytearray))
|
||
else []
|
||
)
|
||
if not capability_ids:
|
||
continue
|
||
seen_ids.add(suggestion_id)
|
||
suggestions.append(
|
||
{
|
||
"id": suggestion_id,
|
||
"label": label,
|
||
"command": command,
|
||
"domain": domain,
|
||
"capability_ids": list(dict.fromkeys(capability_ids)),
|
||
}
|
||
)
|
||
return suggestions
|
||
|
||
|
||
def _mode_profile_create_parts(message: str) -> tuple[str, str] | None:
|
||
"""Return the requested scene name and optional explicit settings."""
|
||
|
||
return parse_mode_profile_create(message)
|
||
|
||
|
||
def is_mode_profile_create_request(message: str) -> bool:
|
||
"""Return whether the original user text is a bounded mode-create request."""
|
||
|
||
return _mode_profile_create_parts(message) is not None
|
||
|
||
|
||
def _mode_profile_update_parts(
|
||
message: str,
|
||
*,
|
||
expected_mode_name: object | None = None,
|
||
) -> tuple[str, str] | None:
|
||
"""Return a bounded named-mode update without interpreting its action."""
|
||
|
||
return parse_mode_profile_update(
|
||
message,
|
||
expected_mode_name=expected_mode_name,
|
||
)
|
||
|
||
|
||
def _validated_mode_profile_draft_context(
|
||
value: Mapping[str, object] | None,
|
||
*,
|
||
message: str,
|
||
) -> dict[str, str] | None:
|
||
"""Validate the browser's opaque draft reference, never client actions."""
|
||
|
||
if value is None:
|
||
return None
|
||
update = _mode_profile_update_parts(
|
||
message,
|
||
expected_mode_name=(value.get("name") if isinstance(value, Mapping) else None),
|
||
)
|
||
if update is None:
|
||
return None
|
||
if not isinstance(value, Mapping):
|
||
raise ValueError("mode_profile_draft_context must be an object")
|
||
referenced_name = " ".join(str(value.get("name") or "").strip().split())
|
||
if normalize_mode_profile_name(update[0]) != normalize_mode_profile_name(
|
||
referenced_name
|
||
):
|
||
return None
|
||
required = {"name", "source_request_id", "vehicle_id"}
|
||
if set(value) != required:
|
||
raise ValueError("mode_profile_draft_context fields are invalid")
|
||
clean = {key: " ".join(str(value.get(key) or "").strip().split()) for key in required}
|
||
if (
|
||
not clean["name"]
|
||
or len(clean["name"]) > 64
|
||
or not clean["vehicle_id"]
|
||
or len(clean["vehicle_id"]) > 80
|
||
or not _MODE_EDITOR_REQUEST_ID_PATTERN.fullmatch(clean["source_request_id"])
|
||
):
|
||
raise ValueError("mode_profile_draft_context values are invalid")
|
||
# The UI keeps the latest card selected while ordinary questions are
|
||
# asked. A reference is only a routing hint when this exact sentence edits
|
||
# that exact draft; all non-matching references returned above are inert.
|
||
return clean
|
||
|
||
|
||
def _catalog_exact_mode_match_count(
|
||
catalog: Mapping[str, object],
|
||
requested_name: str,
|
||
) -> int | None:
|
||
"""Count exact visible mode matches; ``None`` means catalog unavailable."""
|
||
|
||
needle = "".join(str(requested_name).casefold().split())
|
||
source_status = catalog.get("source_status")
|
||
if "modes" not in catalog or (
|
||
isinstance(source_status, Mapping)
|
||
and source_status.get("modes") != "ready"
|
||
):
|
||
return None
|
||
matched_ids: set[str] = set()
|
||
raw_modes = catalog.get("modes", [])
|
||
if not isinstance(raw_modes, Sequence) or isinstance(
|
||
raw_modes,
|
||
(str, bytes, bytearray),
|
||
):
|
||
return None
|
||
for raw_mode in raw_modes:
|
||
if not isinstance(raw_mode, Mapping):
|
||
continue
|
||
candidates = [raw_mode.get("id"), raw_mode.get("name")]
|
||
aliases = raw_mode.get("aliases", [])
|
||
if isinstance(aliases, Sequence) and not isinstance(
|
||
aliases,
|
||
(str, bytes, bytearray),
|
||
):
|
||
candidates.extend(aliases)
|
||
if needle in {
|
||
"".join(str(candidate or "").casefold().split())
|
||
for candidate in candidates
|
||
if str(candidate or "").strip()
|
||
}:
|
||
matched_ids.add(str(raw_mode.get("id") or ""))
|
||
matched_ids.discard("")
|
||
subject = mode_creation_source_name(requested_name)
|
||
if not matched_ids and normalize_mode_profile_name(subject) != needle:
|
||
return _catalog_exact_mode_match_count(catalog, subject)
|
||
return len(matched_ids)
|
||
|
||
|
||
def _catalog_exact_mode_execution_id(
|
||
catalog: Mapping[str, object],
|
||
message: str,
|
||
) -> str | None:
|
||
"""Resolve an explicit saved-mode run without asking the model to route it."""
|
||
|
||
source_status = catalog.get("source_status")
|
||
if not isinstance(source_status, Mapping) or source_status.get("modes") != "ready":
|
||
return None
|
||
|
||
normalized = re.sub(r"[\s,。!?!?、;;::]", "", str(message).casefold())
|
||
if not normalized:
|
||
return None
|
||
prefixes = ("", "请", "请帮我", "帮我", "麻烦", "麻烦帮我", "现在", "立即", "马上")
|
||
verbs = ("进入", "启动", "执行", "开启", "打开", "切换到")
|
||
suffixes = ("", "吧", "一下", "现在", "立即")
|
||
matched_ids: set[str] = set()
|
||
raw_modes = catalog.get("modes", [])
|
||
if not isinstance(raw_modes, Sequence) or isinstance(
|
||
raw_modes,
|
||
(str, bytes, bytearray),
|
||
):
|
||
return None
|
||
for raw_mode in raw_modes:
|
||
if not isinstance(raw_mode, Mapping) or raw_mode.get("assistant_callable") is not True:
|
||
continue
|
||
mode_id = str(raw_mode.get("id") or "").strip()
|
||
if not mode_id:
|
||
continue
|
||
aliases = raw_mode.get("aliases", [])
|
||
candidates = [
|
||
raw_mode.get("name"),
|
||
*(
|
||
aliases
|
||
if isinstance(aliases, Sequence)
|
||
and not isinstance(aliases, (str, bytes, bytearray))
|
||
else []
|
||
),
|
||
]
|
||
for candidate in candidates:
|
||
mode_name = re.sub(
|
||
r"[\s,。!?!?、;;::]",
|
||
"",
|
||
str(candidate or "").casefold(),
|
||
)
|
||
if not mode_name:
|
||
continue
|
||
accepted = {
|
||
f"{prefix}{verb}{mode_name}{suffix}"
|
||
for prefix in prefixes
|
||
for verb in verbs
|
||
for suffix in suffixes
|
||
}
|
||
if normalized in accepted:
|
||
matched_ids.add(mode_id)
|
||
break
|
||
return next(iter(matched_ids)) if len(matched_ids) == 1 else None
|
||
|
||
|
||
def _catalog_exact_scene_template_plan(
|
||
catalog: Mapping[str, object],
|
||
requested_name: str,
|
||
) -> dict[str, object] | None:
|
||
"""Return one exact, callable template plan from the server-owned catalog."""
|
||
|
||
source_status = catalog.get("source_status")
|
||
if (
|
||
not isinstance(source_status, Mapping)
|
||
or source_status.get("scene_templates") != "ready"
|
||
):
|
||
return None
|
||
raw_templates = catalog.get("scene_templates")
|
||
if not isinstance(raw_templates, Sequence) or isinstance(
|
||
raw_templates, (str, bytes, bytearray)
|
||
):
|
||
return None
|
||
needle = normalize_scene_template_name(requested_name)
|
||
matches: dict[str, Mapping[str, object]] = {}
|
||
for raw_template in raw_templates:
|
||
if (
|
||
not isinstance(raw_template, Mapping)
|
||
or raw_template.get("assistant_callable") is not True
|
||
):
|
||
continue
|
||
candidates = [raw_template.get("id"), raw_template.get("name")]
|
||
aliases = raw_template.get("aliases", [])
|
||
if isinstance(aliases, Sequence) and not isinstance(
|
||
aliases, (str, bytes, bytearray)
|
||
):
|
||
candidates.extend(aliases)
|
||
if needle and needle in {
|
||
normalize_scene_template_name(candidate)
|
||
for candidate in candidates
|
||
if str(candidate or "").strip()
|
||
}:
|
||
template_id = str(raw_template.get("id") or "").strip()
|
||
if template_id:
|
||
matches[template_id] = raw_template
|
||
if not matches:
|
||
subject = mode_creation_source_name(requested_name)
|
||
if normalize_scene_template_name(subject) != needle:
|
||
return _catalog_exact_scene_template_plan(catalog, subject)
|
||
if len(matches) != 1:
|
||
return None
|
||
template = next(iter(matches.values()))
|
||
plan = template.get("plan")
|
||
metadata = plan.get("metadata") if isinstance(plan, Mapping) else None
|
||
if (
|
||
not isinstance(plan, Mapping)
|
||
or not isinstance(metadata, Mapping)
|
||
or metadata.get("source") != "scene_template"
|
||
or metadata.get("template_id") != template.get("id")
|
||
or metadata.get("template_revision") != template.get("template_revision")
|
||
):
|
||
return None
|
||
try:
|
||
return json.loads(
|
||
json.dumps(dict(plan), ensure_ascii=False, allow_nan=False)
|
||
)
|
||
except (TypeError, ValueError):
|
||
return None
|
||
|
||
|
||
def _vehicle_result_succeeded(result: Mapping[str, object]) -> bool:
|
||
"""Only a terminal, fully successful service result completes a tool call."""
|
||
|
||
schedule = result.get("schedule_execution")
|
||
return str(result.get("status") or "").lower() == "succeeded" or (
|
||
result.get("status") == "running"
|
||
and result.get("orchestration_route") == "mode_schedule_execute"
|
||
and isinstance(schedule, Mapping) and schedule.get("status") == "running"
|
||
)
|
||
|
||
|
||
def _mode_profile_result_succeeded(result: Mapping[str, object]) -> bool:
|
||
"""Require a complete, non-physical mode preview before claiming success."""
|
||
|
||
if (
|
||
not _vehicle_result_succeeded(result)
|
||
or str(result.get("execution_mode") or "").lower() != "dry_run"
|
||
or result.get("hardware_execution_allowed") is not False
|
||
or result.get("orchestration_route") != "mode_profile_create_preview"
|
||
):
|
||
return False
|
||
draft = result.get("mode_profile_draft")
|
||
if not isinstance(draft, Mapping):
|
||
return False
|
||
operation_count = draft.get("operation_count")
|
||
if (
|
||
not isinstance(operation_count, int)
|
||
or isinstance(operation_count, bool)
|
||
or operation_count <= 0
|
||
):
|
||
return False
|
||
intent_ir = result.get("intent_ir")
|
||
spec = intent_ir.get("spec") if isinstance(intent_ir, Mapping) else None
|
||
actions = spec.get("actions") if isinstance(spec, Mapping) else None
|
||
if not isinstance(actions, Sequence) or isinstance(
|
||
actions,
|
||
(str, bytes, bytearray),
|
||
):
|
||
return False
|
||
if len(actions) != operation_count or not all(
|
||
isinstance(action, Mapping) for action in actions
|
||
):
|
||
return False
|
||
validation = result.get("validation", [])
|
||
return not (
|
||
isinstance(validation, Sequence)
|
||
and not isinstance(validation, (str, bytes, bytearray))
|
||
and any(
|
||
isinstance(issue, Mapping) and issue.get("severity") == "error"
|
||
for issue in validation
|
||
)
|
||
)
|
||
|
||
|
||
def _mode_profile_management_result_succeeded(
|
||
result: Mapping[str, object],
|
||
route: str,
|
||
) -> bool:
|
||
"""Require complete control-plane evidence for a saved-mode mutation."""
|
||
|
||
if (
|
||
not _vehicle_result_succeeded(result)
|
||
or str(result.get("execution_mode") or "").lower() != "dry_run"
|
||
or result.get("hardware_execution_allowed") is not False
|
||
or result.get("orchestration_route") != route
|
||
):
|
||
return False
|
||
evidence_key = (
|
||
"deleted_mode_profile"
|
||
if route == "mode_profile_delete"
|
||
else "mode_profile"
|
||
)
|
||
evidence = result.get(evidence_key)
|
||
return isinstance(evidence, Mapping) and bool(evidence.get("id"))
|
||
|
||
|
||
def _vehicle_verification_status(result: Mapping[str, object]) -> str:
|
||
"""Normalize the service's execution evidence without inventing feedback."""
|
||
|
||
execution_mode = str(result.get("execution_mode") or "").strip().lower()
|
||
if execution_mode in {"dry_run", "simulated_hardware"}:
|
||
return "simulated"
|
||
for key in ("execution_outcome", "verification_status"):
|
||
value = str(result.get(key) or "")
|
||
if value:
|
||
return value
|
||
execution_report = result.get("execution_report")
|
||
if isinstance(execution_report, Mapping):
|
||
spec = execution_report.get("spec")
|
||
if isinstance(spec, Mapping):
|
||
value = str(spec.get("verification_status") or "")
|
||
if value:
|
||
return value
|
||
if result.get("physical_feedback_confirmed") is True:
|
||
return "verified_success"
|
||
return "unknown"
|
||
|
||
|
||
def _vehicle_action_state(result: Mapping[str, object]) -> tuple[str, bool, str]:
|
||
"""Describe accepted, simulated and physically verified outcomes distinctly."""
|
||
|
||
if not _vehicle_result_succeeded(result):
|
||
return "failed", False, "车辆控制未完成"
|
||
if result.get("orchestration_route") == "mode_schedule_stop":
|
||
return ("simulated" if result.get("execution_mode") != "hardware" else "cancelled"), True, "动态模式停止请求已处理"
|
||
if result.get("status") == "running":
|
||
return "running", True, "时间轴已启动,尚未完成所有阶段"
|
||
execution_mode = str(result.get("execution_mode") or "").lower()
|
||
if execution_mode == "dry_run":
|
||
return "simulated", True, "车辆控制模拟编排已完成"
|
||
if execution_mode == "simulated_hardware":
|
||
return "simulated", True, "车辆控制已在模拟硬件完成"
|
||
if _vehicle_verification_status(result) == "verified_success":
|
||
return "verified", True, "车辆控制已完成并通过状态确认"
|
||
return "submitted", True, "控制指令已提交,等待车辆状态确认"
|
||
|
||
|
||
def _authorize_vehicle_request(
|
||
message: str,
|
||
control_terms: Sequence[str] = (),
|
||
mode_profile_draft_context: Mapping[str, object] | None = None,
|
||
) -> _VehicleAuthorization:
|
||
"""Create a server-owned capability envelope from the original user text."""
|
||
|
||
expected_draft_name = (
|
||
mode_profile_draft_context.get("name")
|
||
if isinstance(mode_profile_draft_context, Mapping)
|
||
else None
|
||
)
|
||
mode_profile_draft_context = _validated_mode_profile_draft_context(
|
||
mode_profile_draft_context,
|
||
message=message,
|
||
)
|
||
normalized = "".join(str(message).lower().split())
|
||
if is_schedule_stop(message):
|
||
return _VehicleAuthorization(message, "explicit_schedule_stop")
|
||
mode_profile_create = _mode_profile_create_parts(message)
|
||
mode_profile_update = _mode_profile_update_parts(message)
|
||
mode_profile_delete = parse_mode_profile_delete(message)
|
||
if (
|
||
mode_profile_create is None
|
||
and mode_profile_update is None
|
||
and mode_profile_delete is None
|
||
and _DENIAL_PATTERN.search(normalized)
|
||
):
|
||
return _VehicleAuthorization(None, "denied_by_user")
|
||
|
||
if (
|
||
mode_profile_create is not None
|
||
and not _KNOWLEDGE_PATTERN.search(normalized)
|
||
and not _NON_VEHICLE_WORK_PATTERN.search(normalized)
|
||
):
|
||
# Performative questions such as ``能不能帮我创建……`` are still
|
||
# concrete configuration requests. The canonical parser has already
|
||
# rejected bare capability questions and quoted examples, so handle
|
||
# this before the broader question detector below.
|
||
return _VehicleAuthorization(message, "explicit_mode_profile_create")
|
||
|
||
if (
|
||
"模式" in normalized
|
||
and re.search(r"构建|创建|新建|新增|生成|添加", normalized)
|
||
and _KNOWLEDGE_PATTERN.search(normalized)
|
||
):
|
||
return _VehicleAuthorization(None, "knowledge_or_non_vehicle_request")
|
||
|
||
if is_mode_profile_query(
|
||
normalized,
|
||
expected_mode_name=expected_draft_name,
|
||
):
|
||
# A question about a named mode may contain device names, control
|
||
# verbs and numeric targets. It remains a question and must never
|
||
# fall through to the registered-control executor.
|
||
return _VehicleAuthorization(None, "mode_profile_query")
|
||
|
||
if mode_profile_update is not None:
|
||
return _VehicleAuthorization(
|
||
message,
|
||
(
|
||
"explicit_mode_profile_draft_update"
|
||
if mode_profile_draft_context is not None
|
||
else "explicit_mode_profile_update"
|
||
),
|
||
)
|
||
|
||
if mode_profile_delete is not None:
|
||
return _VehicleAuthorization(message, "explicit_mode_profile_delete")
|
||
|
||
implicit_request = re.sub(r"[,。!?!?、]", "", normalized)
|
||
if not _KNOWLEDGE_PATTERN.search(normalized):
|
||
if _IMPLICIT_HOT_REQUEST_PATTERN.fullmatch(implicit_request):
|
||
return _VehicleAuthorization(_COOLING_INSTRUCTION, "implicit_hot_comfort")
|
||
if _IMPLICIT_COLD_REQUEST_PATTERN.fullmatch(implicit_request):
|
||
return _VehicleAuthorization(_WARMING_INSTRUCTION, "implicit_cold_comfort")
|
||
|
||
if _KNOWLEDGE_PATTERN.search(normalized) or _NON_VEHICLE_WORK_PATTERN.search(
|
||
normalized
|
||
):
|
||
return _VehicleAuthorization(None, "knowledge_or_non_vehicle_request")
|
||
|
||
if _SUPPORT_QUERY_PATTERN.search(normalized):
|
||
return _VehicleAuthorization(None, "knowledge_or_non_vehicle_request")
|
||
|
||
normalized_terms = {
|
||
"".join(str(term).lower().split()) for term in control_terms if str(term).strip()
|
||
}
|
||
matched = any(term and term in normalized for term in normalized_terms)
|
||
if matched and (
|
||
_CONTROL_VERB_PATTERN.search(normalized) or _CONTROL_VALUE_PATTERN.search(normalized)
|
||
):
|
||
# The callback executes this exact server-owned original instruction.
|
||
# The model can choose whether to request execution, but cannot broaden it.
|
||
return _VehicleAuthorization(message, "explicit_registered_control")
|
||
return _VehicleAuthorization(None, "not_a_registered_control")
|
||
|
||
|
||
class _StreamingSecretRedactor:
|
||
"""Redact a token even when it is split across streamed model deltas."""
|
||
|
||
def __init__(self, secret: str) -> None:
|
||
self._secret = secret
|
||
self._buffer = ""
|
||
|
||
def feed(self, text: str) -> str:
|
||
self._buffer += text
|
||
self._buffer = self._buffer.replace(self._secret, "[内部令牌已隐藏]")
|
||
keep = 0
|
||
maximum = min(len(self._secret) - 1, len(self._buffer))
|
||
for length in range(maximum, 0, -1):
|
||
if self._buffer.endswith(self._secret[:length]):
|
||
keep = length
|
||
break
|
||
if keep:
|
||
ready, self._buffer = self._buffer[:-keep], self._buffer[-keep:]
|
||
return ready
|
||
ready, self._buffer = self._buffer, ""
|
||
return ready
|
||
|
||
def finish(self) -> str:
|
||
ready = self._buffer.replace(self._secret, "[内部令牌已隐藏]")
|
||
self._buffer = ""
|
||
return ready
|
||
|
||
|
||
class CodexAssistantService:
|
||
"""Map browser conversations onto Codex threads and trusted vehicle tools."""
|
||
|
||
_conversation_pattern = re.compile(r"^conv-[a-z0-9-]{8,100}$")
|
||
|
||
def __init__(
|
||
self,
|
||
client: CodexClient,
|
||
*,
|
||
prompt_path: Path | None = None,
|
||
max_sessions: int = 128,
|
||
fast_implicit_controls: bool = False,
|
||
) -> None:
|
||
self.client = client
|
||
self._prompt_path = prompt_path or (
|
||
Path(__file__).resolve().parents[1] / "agent" / "prompts" / "codex_cockpit_assistant.md"
|
||
)
|
||
self._instructions = self._prompt_path.read_text(encoding="utf-8")
|
||
self._max_sessions = max(8, max_sessions)
|
||
self._fast_implicit_controls = bool(fast_implicit_controls)
|
||
self._sessions: OrderedDict[tuple[str, str], _Conversation] = OrderedDict()
|
||
self._sessions_lock = threading.Lock()
|
||
self._tool_contexts: dict[str, _VehicleTurnContext] = {}
|
||
self._tool_contexts_lock = threading.Lock()
|
||
self._scene_cache: OrderedDict[
|
||
tuple[str, str, str, str], tuple[float, dict[str, object]]
|
||
] = OrderedDict()
|
||
self._scene_flights: dict[tuple[str, str, str, str], _SceneFlight] = {}
|
||
self._scene_cache_lock = threading.Lock()
|
||
self._scene_cache_ttl = max(
|
||
1.0,
|
||
min(float(os.getenv("CODEX_ASSISTANT_SCENE_CACHE_TTL_SECONDS", "3600")), 86400.0),
|
||
)
|
||
self._scene_cache_limit = max(
|
||
8,
|
||
min(int(os.getenv("CODEX_ASSISTANT_SCENE_CACHE_MAX_ENTRIES", "256")), 4096),
|
||
)
|
||
self._scene_turn_timeout = max(
|
||
5.0,
|
||
min(float(os.getenv("CODEX_ASSISTANT_SCENE_TIMEOUT_SECONDS", "45")), 300.0),
|
||
)
|
||
self._scene_max_concurrent_plans = max(
|
||
1,
|
||
min(
|
||
int(os.getenv("CODEX_ASSISTANT_SCENE_MAX_CONCURRENT_PLANS", "8")),
|
||
64,
|
||
),
|
||
)
|
||
self._scene_planner_slots = threading.BoundedSemaphore(
|
||
self._scene_max_concurrent_plans
|
||
)
|
||
self._scene_progress_interval = 2.0
|
||
self._internal_token = secrets.token_urlsafe(32)
|
||
self._turn_timeout = max(
|
||
10.0,
|
||
min(float(os.getenv("CODEX_ASSISTANT_TURN_TIMEOUT_SECONDS", "300")), 1800.0),
|
||
)
|
||
|
||
@property
|
||
def internal_token(self) -> str:
|
||
return self._internal_token
|
||
|
||
def start(self, callback_url: str) -> None:
|
||
self.client.start(callback_url, self._internal_token)
|
||
|
||
def close(self) -> None:
|
||
with self._sessions_lock:
|
||
conversations = list(self._sessions.values())
|
||
self._sessions.clear()
|
||
for conversation in conversations:
|
||
self._unsubscribe_thread(conversation.thread_id)
|
||
self.client.close()
|
||
|
||
def status(self) -> dict[str, object]:
|
||
client_status = self.client.status
|
||
payload: dict[str, object] = {
|
||
"status": str(client_status),
|
||
"available": self.client.available,
|
||
}
|
||
with self._sessions_lock:
|
||
payload["active_conversations"] = len(self._sessions)
|
||
with self._scene_cache_lock:
|
||
payload["scene_composer"] = {
|
||
"planner_version": SCENE_PLANNER_VERSION,
|
||
"cached_plans": len(self._scene_cache),
|
||
"in_flight": len(self._scene_flights),
|
||
"max_concurrent_plans": self._scene_max_concurrent_plans,
|
||
}
|
||
payload["engine"] = "codex-app-server"
|
||
return payload
|
||
|
||
def stream(
|
||
self,
|
||
message: str,
|
||
*,
|
||
conversation_id: str,
|
||
principal: str,
|
||
history: Sequence[dict[str, str]],
|
||
emit: EmitEvent,
|
||
execute_control: ControlExecutor,
|
||
preview_control: ControlExecutor | None = None,
|
||
supplement_control: ControlExecutor | None = None,
|
||
capability_snapshot: Mapping[str, object] | None = None,
|
||
capability_context: Mapping[str, object] | None = None,
|
||
scene_capability_context: Mapping[str, object] | None = None,
|
||
control_terms: Sequence[str] = (),
|
||
query_capabilities: CapabilityQuery | None = None,
|
||
authoritative_answer: str | None = None,
|
||
authoritative_topic: str | None = None,
|
||
authoritative_suggestions: Sequence[Mapping[str, object]] = (),
|
||
quick_control_commands: Sequence[str] = (),
|
||
mode_profile_draft_context: Mapping[str, object] | None = None,
|
||
) -> dict[str, object]:
|
||
del history # Codex owns conversation history after the thread is created.
|
||
message = " ".join(str(message).strip().split())
|
||
if not message:
|
||
raise ValueError("message must not be empty")
|
||
if len(message) > 4000:
|
||
raise ValueError("message must not exceed 4000 characters")
|
||
if not self._conversation_pattern.fullmatch(conversation_id):
|
||
raise ValueError("invalid conversation_id")
|
||
catalog = json.loads(
|
||
json.dumps(dict(capability_snapshot or {}), ensure_ascii=False, default=str)
|
||
)
|
||
model_context = json.loads(
|
||
json.dumps(dict(capability_context or {}), ensure_ascii=False, default=str)
|
||
)
|
||
scene_context = json.loads(
|
||
json.dumps(dict(scene_capability_context or {}), ensure_ascii=False, default=str)
|
||
)
|
||
catalog_query = query_capabilities or (lambda snapshot, _query: dict(snapshot))
|
||
authoritative_answer = " ".join(str(authoritative_answer or "").strip().split())
|
||
if len(authoritative_answer) > 16_000:
|
||
raise ValueError("authoritative_answer must not exceed 16000 characters")
|
||
authoritative_topic = str(authoritative_topic or "").strip()
|
||
if not re.fullmatch(r"[a-z][a-z0-9_]{0,63}", authoritative_topic):
|
||
authoritative_topic = ""
|
||
authoritative_suggestions = _safe_authoritative_suggestions(
|
||
authoritative_suggestions
|
||
)
|
||
normalized_quick_controls = {
|
||
normalized
|
||
for raw in quick_control_commands[:6]
|
||
for normalized in [_normalize_instruction(str(raw)[:160])]
|
||
if normalized
|
||
}
|
||
draft_context = _validated_mode_profile_draft_context(
|
||
mode_profile_draft_context,
|
||
message=message,
|
||
)
|
||
authorization = _authorize_vehicle_request(
|
||
message,
|
||
control_terms,
|
||
draft_context,
|
||
)
|
||
saved_mode_execution_id = _catalog_exact_mode_execution_id(catalog, message)
|
||
if saved_mode_execution_id is not None:
|
||
authorization = _VehicleAuthorization(
|
||
message,
|
||
"explicit_saved_mode_execute",
|
||
)
|
||
fast_comfort_control = (
|
||
self._fast_implicit_controls and authorization.reason in _FAST_COMFORT_REASONS
|
||
)
|
||
exact_quick_control = (
|
||
authorization.reason == "explicit_registered_control"
|
||
and _normalize_instruction(message) in normalized_quick_controls
|
||
)
|
||
explicit_local_control = (
|
||
authorization.reason == "explicit_registered_control"
|
||
)
|
||
local_first_control = bool(
|
||
explicit_local_control
|
||
and preview_control is not None
|
||
and supplement_control is not None
|
||
)
|
||
mode_profile_preview = authorization.reason in {
|
||
"explicit_mode_profile_create",
|
||
"explicit_mode_profile_draft_update",
|
||
}
|
||
mode_parts = _mode_profile_create_parts(message) if mode_profile_preview else None
|
||
if mode_parts:
|
||
scene_context = scene_context_for_mode_creation(scene_context, message)
|
||
model_context = scene_context_for_mode_creation(model_context, message)
|
||
semantic_guidance = scene_semantic_guidance(mode_parts[0])
|
||
live_scene_capabilities = {
|
||
str(item.get("id") or "")
|
||
for item in scene_context.get("capabilities", [])
|
||
if isinstance(item, Mapping)
|
||
}
|
||
raw_preferred_capabilities = semantic_guidance.get(
|
||
"preferred_capability_ids", []
|
||
)
|
||
semantic_guidance["preferred_capability_ids"] = [
|
||
str(capability_id)
|
||
for capability_id in (
|
||
raw_preferred_capabilities
|
||
if isinstance(raw_preferred_capabilities, Sequence)
|
||
and not isinstance(
|
||
raw_preferred_capabilities,
|
||
(str, bytes, bytearray),
|
||
)
|
||
else []
|
||
)
|
||
if str(capability_id) in live_scene_capabilities
|
||
]
|
||
include_default_bundle = (
|
||
not needs_mode_schedule(message)
|
||
and not scene_has_explicit_control_object(
|
||
message,
|
||
scene_context,
|
||
)
|
||
)
|
||
required_peripheral_refs = scene_required_peripheral_refs(
|
||
mode_parts[0],
|
||
scene_context,
|
||
include_default_bundle=include_default_bundle,
|
||
)
|
||
required_native_fallbacks = scene_required_native_fallbacks(
|
||
mode_parts[0],
|
||
scene_context,
|
||
include_default_bundle=include_default_bundle,
|
||
)
|
||
semantic_guidance["required_native_domains"] = list(dict.fromkeys(
|
||
str(item.get("domain") or "")
|
||
for item in required_native_fallbacks
|
||
if str(item.get("domain") or "")
|
||
))
|
||
semantic_guidance["required_native_controls"] = [
|
||
{
|
||
"capability": str(item.get("capability") or ""),
|
||
"target_scope": str(item.get("target_scope") or ""),
|
||
}
|
||
for item in required_native_fallbacks
|
||
]
|
||
semantic_guidance["native_fallback_requests"] = (
|
||
required_native_fallbacks
|
||
)
|
||
semantic_guidance["default_control_bundle_applied"] = (
|
||
include_default_bundle
|
||
)
|
||
raw_peripheral_actions = scene_context.get("peripheral_actions", [])
|
||
semantic_guidance["peripheral_policy"] = {
|
||
"available_action_count": (
|
||
len(raw_peripheral_actions)
|
||
if isinstance(raw_peripheral_actions, list)
|
||
else 0
|
||
),
|
||
"required_refs": required_peripheral_refs,
|
||
"selection_goal": (
|
||
"Include as many semantically relevant scene-safe peripheral "
|
||
"devices as are useful; choose only one authorized power-on "
|
||
"action per device, without color or effect settings."
|
||
),
|
||
"required_reason": (
|
||
"Name-only scenes must include the available authorized "
|
||
"flashing-light, ambient-light and light-strip controls."
|
||
if include_default_bundle and required_peripheral_refs
|
||
else "Romantic and Mid-Autumn related scenes must include the "
|
||
"authorized flashing-light and light-strip controls."
|
||
if required_peripheral_refs
|
||
else "No peripheral action is mandatory for this name."
|
||
),
|
||
}
|
||
scene_context["scene_semantics"] = semantic_guidance
|
||
model_context["scene_semantics"] = deepcopy(semantic_guidance)
|
||
exact_mode_matches = (
|
||
_catalog_exact_mode_match_count(catalog, mode_parts[0])
|
||
if mode_parts
|
||
else None
|
||
)
|
||
candidate_scene_template_plan = (
|
||
_catalog_exact_scene_template_plan(catalog, mode_parts[0])
|
||
if mode_parts and not mode_parts[1]
|
||
else None
|
||
)
|
||
candidate_scene_template_metadata = (
|
||
candidate_scene_template_plan.get("metadata")
|
||
if isinstance(candidate_scene_template_plan, Mapping)
|
||
else None
|
||
)
|
||
candidate_scene_template_id = (
|
||
str(candidate_scene_template_metadata.get("template_id") or "")
|
||
if isinstance(candidate_scene_template_metadata, Mapping)
|
||
else ""
|
||
)
|
||
scene_template_plan = (
|
||
candidate_scene_template_plan
|
||
if exact_mode_matches == 0
|
||
or candidate_scene_template_id
|
||
in PRESERVE_BASE_EXTERNAL_LIGHT_TEMPLATE_IDS
|
||
else None
|
||
)
|
||
scene_template_metadata = (
|
||
scene_template_plan.get("metadata")
|
||
if isinstance(scene_template_plan, Mapping)
|
||
else None
|
||
)
|
||
preserve_base_external_lights = bool(
|
||
isinstance(scene_template_metadata, Mapping)
|
||
and str(scene_template_metadata.get("template_id") or "")
|
||
in PRESERVE_BASE_EXTERNAL_LIGHT_TEMPLATE_IDS
|
||
)
|
||
if preserve_base_external_lights:
|
||
# The template already owns every native cinema action. Do not let
|
||
# generic romantic-scene defaults replace or append native actions;
|
||
# only the deterministic, NFC-authorized light refs may be added.
|
||
scene_semantics = scene_context.get("scene_semantics")
|
||
if isinstance(scene_semantics, dict):
|
||
scene_semantics["required_native_domains"] = []
|
||
scene_semantics["required_native_controls"] = []
|
||
scene_semantics["native_fallback_requests"] = []
|
||
scene_semantics["default_control_bundle_applied"] = False
|
||
dynamic_scene_request = bool(mode_parts and needs_mode_schedule(message))
|
||
scene_peripheral_actions = scene_context.get("peripheral_actions", [])
|
||
template_augmentation_preview = bool(
|
||
scene_template_plan is not None
|
||
and not preserve_base_external_lights
|
||
and not dynamic_scene_request
|
||
and isinstance(scene_peripheral_actions, list)
|
||
and scene_peripheral_actions
|
||
and self.client.available
|
||
and callable(getattr(self.client, "start_structured_turn", None))
|
||
)
|
||
direct_scene_template_plan = (
|
||
self._with_required_scene_peripherals(
|
||
scene_template_plan,
|
||
scene_context,
|
||
)
|
||
if scene_template_plan is not None
|
||
else None
|
||
)
|
||
direct_mode_profile_preview = bool(
|
||
mode_parts
|
||
and (
|
||
(mode_parts[1] and not dynamic_scene_request)
|
||
# Ambiguous or unavailable mode catalogs fail closed in the
|
||
# local builder. Only a known-zero match is safe to hand to
|
||
# semantic scene composition.
|
||
or exact_mode_matches != 0
|
||
or (
|
||
scene_template_plan is not None
|
||
and not template_augmentation_preview
|
||
)
|
||
)
|
||
)
|
||
direct_mode_profile_update = (
|
||
authorization.reason == "explicit_mode_profile_draft_update"
|
||
)
|
||
saved_mode_management_route = {
|
||
"explicit_mode_profile_update": "mode_profile_update",
|
||
"explicit_mode_profile_delete": "mode_profile_delete",
|
||
}.get(authorization.reason)
|
||
scene_composition_preview = bool(
|
||
mode_parts
|
||
and (not mode_parts[1] or dynamic_scene_request)
|
||
and exact_mode_matches == 0
|
||
and scene_template_plan is None
|
||
and scene_context.get("composition_revision")
|
||
and callable(getattr(self.client, "start_structured_turn", None))
|
||
)
|
||
# Explicit device settings and repository-backed scenes remain on the
|
||
# zero-model-latency path. A genuinely new name-only scene needs Codex
|
||
# to compose a candidate plan from the live capability catalog; the
|
||
# vehicle service still limits it to dry-run and requires confirmation.
|
||
direct_vehicle_control = (
|
||
fast_comfort_control
|
||
or authorization.reason == "explicit_schedule_stop"
|
||
or exact_quick_control
|
||
or local_first_control
|
||
or saved_mode_execution_id is not None
|
||
or direct_mode_profile_preview
|
||
or authorization.reason == "explicit_mode_profile_draft_update"
|
||
or saved_mode_management_route is not None
|
||
)
|
||
route = (
|
||
"unsupported_control"
|
||
if authoritative_answer and authoritative_topic == "unsupported_vehicle_control"
|
||
else "capability_catalog"
|
||
if authoritative_answer
|
||
else "scene_template"
|
||
if scene_template_plan is not None
|
||
else saved_mode_management_route
|
||
if saved_mode_management_route is not None
|
||
else "mode_profile_execute"
|
||
if saved_mode_execution_id is not None
|
||
else "vehicle_control"
|
||
if direct_vehicle_control
|
||
else "scene_composer"
|
||
if scene_composition_preview
|
||
else "agent"
|
||
)
|
||
engine = (
|
||
"n7web-capability-registry"
|
||
if authoritative_answer
|
||
else "n7web-scene-template"
|
||
if scene_template_plan is not None
|
||
else "n7web-mode-builder"
|
||
if direct_mode_profile_preview or direct_mode_profile_update
|
||
else "n7web-mode-profile-manager"
|
||
if saved_mode_management_route is not None
|
||
else "n7web-mode-profile-runner"
|
||
if saved_mode_execution_id is not None
|
||
else "codex-app-server-scene-composer"
|
||
if scene_composition_preview
|
||
else "n7web-control-router"
|
||
if fast_comfort_control or exact_quick_control or local_first_control
|
||
else "codex-app-server"
|
||
)
|
||
|
||
message_id = f"msg-{uuid.uuid4().hex}"
|
||
if (
|
||
emit(
|
||
{
|
||
"type": "message.accepted",
|
||
"message_id": message_id,
|
||
"conversation_id": conversation_id,
|
||
"route": route,
|
||
"engine": engine,
|
||
}
|
||
)
|
||
is False
|
||
):
|
||
return {"cancelled": True}
|
||
if (
|
||
emit(
|
||
{
|
||
"type": "work.started",
|
||
"message_id": message_id,
|
||
"display_text": (
|
||
"正在核对车端控制能力……"
|
||
if route == "unsupported_control"
|
||
else "正在读取工程能力目录……"
|
||
if authoritative_answer
|
||
else "正在载入并校验常用模式模板……"
|
||
if scene_template_plan is not None
|
||
else "正在构建并校验功能模式……"
|
||
if direct_mode_profile_preview or direct_mode_profile_update
|
||
else "正在更新已保存的功能模式……"
|
||
if saved_mode_management_route == "mode_profile_update"
|
||
else "正在删除自定义功能模式……"
|
||
if saved_mode_management_route == "mode_profile_delete"
|
||
else "正在载入并执行已保存的功能模式……"
|
||
if saved_mode_execution_id is not None
|
||
else "正在根据实时能力目录快速组合模式……"
|
||
if scene_composition_preview
|
||
else "小迪正在理解场景并组合工程能力……"
|
||
if mode_profile_preview
|
||
else "正在快速处理座舱舒适需求……"
|
||
if fast_comfort_control
|
||
else "正在执行快捷车控……"
|
||
if exact_quick_control
|
||
else "正在本地解析并下发明确控制指令……"
|
||
if local_first_control
|
||
else "小迪正在理解并处理……"
|
||
),
|
||
}
|
||
)
|
||
is False
|
||
):
|
||
return {"cancelled": True}
|
||
|
||
if authoritative_answer:
|
||
if (
|
||
emit(
|
||
{
|
||
"type": "answer.delta",
|
||
"message_id": message_id,
|
||
"delta": authoritative_answer,
|
||
}
|
||
)
|
||
is False
|
||
):
|
||
return {"cancelled": True}
|
||
if authoritative_suggestions and (
|
||
emit(
|
||
{
|
||
"type": "control.suggestions",
|
||
"message_id": message_id,
|
||
"suggestions": authoritative_suggestions,
|
||
}
|
||
)
|
||
is False
|
||
):
|
||
return {"cancelled": True}
|
||
emit(
|
||
{
|
||
"type": "work.completed",
|
||
"message_id": message_id,
|
||
"success": True,
|
||
"display_text": (
|
||
"已核对当前车端能力"
|
||
if route == "unsupported_control"
|
||
else "已按工程能力目录核对"
|
||
),
|
||
}
|
||
)
|
||
emit(
|
||
{
|
||
"type": "answer.completed",
|
||
"message_id": message_id,
|
||
"route": route,
|
||
"answer": authoritative_answer,
|
||
**({"topic": authoritative_topic} if authoritative_topic else {}),
|
||
**(
|
||
{"suggestions": authoritative_suggestions}
|
||
if authoritative_suggestions
|
||
else {}
|
||
),
|
||
}
|
||
)
|
||
return {
|
||
"route": route,
|
||
"answer": authoritative_answer,
|
||
**({"topic": authoritative_topic} if authoritative_topic else {}),
|
||
**(
|
||
{"suggestions": authoritative_suggestions}
|
||
if authoritative_suggestions
|
||
else {}
|
||
),
|
||
}
|
||
|
||
if (
|
||
local_first_control
|
||
and preview_control is not None
|
||
and supplement_control is not None
|
||
):
|
||
return self._stream_local_first_vehicle_control(
|
||
authorization,
|
||
message_id=message_id,
|
||
emit=emit,
|
||
preview_control=preview_control,
|
||
supplement_control=supplement_control,
|
||
conversation_id=conversation_id,
|
||
principal=principal,
|
||
capability_snapshot=catalog,
|
||
capability_context=model_context,
|
||
control_terms=control_terms,
|
||
query_capabilities=catalog_query,
|
||
)
|
||
|
||
if template_augmentation_preview and scene_template_plan is not None:
|
||
try:
|
||
return self._stream_scene_template_augmentation(
|
||
message,
|
||
message_id=message_id,
|
||
emit=emit,
|
||
execute_control=execute_control,
|
||
scene_context=scene_context,
|
||
template_plan=scene_template_plan,
|
||
)
|
||
except _ScenePlannerCancelled:
|
||
return {"cancelled": True}
|
||
except Exception as exc: # noqa: BLE001 - trusted base remains usable
|
||
LOGGER.warning(
|
||
"Scene template peripheral augmentation failed; using base: %s",
|
||
exc,
|
||
)
|
||
return self._stream_direct_vehicle_control(
|
||
authorization,
|
||
message_id=message_id,
|
||
emit=emit,
|
||
execute_control=execute_control,
|
||
assistant_plan=direct_scene_template_plan,
|
||
response_route="scene_template",
|
||
)
|
||
|
||
if direct_vehicle_control:
|
||
return self._stream_direct_vehicle_control(
|
||
(
|
||
_VehicleAuthorization(
|
||
authorization.instruction,
|
||
"exact_quick_control",
|
||
)
|
||
if exact_quick_control
|
||
else authorization
|
||
),
|
||
message_id=message_id,
|
||
emit=emit,
|
||
execute_control=execute_control,
|
||
assistant_plan=direct_scene_template_plan,
|
||
response_route=(
|
||
"scene_template"
|
||
if scene_template_plan is not None
|
||
else saved_mode_management_route
|
||
if saved_mode_management_route is not None
|
||
else "mode_profile_execute"
|
||
if saved_mode_execution_id is not None
|
||
else "vehicle_control"
|
||
),
|
||
)
|
||
|
||
if not self.client.available:
|
||
raise CodexAssistantError("Codex App Server is not available")
|
||
|
||
if scene_composition_preview and mode_parts is not None:
|
||
try:
|
||
return self._stream_scene_composition(
|
||
message,
|
||
mode_name=mode_parts[0],
|
||
message_id=message_id,
|
||
emit=emit,
|
||
execute_control=execute_control,
|
||
scene_context=scene_context,
|
||
)
|
||
except _ScenePlannerCancelled:
|
||
return {"cancelled": True}
|
||
except _ScenePlannerTimeout:
|
||
return self._stream_scene_planner_failure(
|
||
message_id=message_id,
|
||
emit=emit,
|
||
display_text="功能模式规划超时",
|
||
answer=(
|
||
f"功能模式暂未生成:场景规划超时,已超过"
|
||
f"{self._scene_turn_timeout:g}秒。未执行或保存任何车辆操作,"
|
||
"请重试。"
|
||
),
|
||
)
|
||
except _ScenePlannerOverloaded:
|
||
return self._stream_scene_planner_failure(
|
||
message_id=message_id,
|
||
emit=emit,
|
||
display_text="功能模式规划任务较多",
|
||
answer=(
|
||
"功能模式暂未生成:当前场景规划任务较多。"
|
||
"未执行或保存任何车辆操作,请稍后重试。"
|
||
),
|
||
)
|
||
except _ScenePlannerFallback as exc:
|
||
LOGGER.warning("Scene Composer fell back to the general agent: %s", exc)
|
||
if (
|
||
emit(
|
||
{
|
||
"type": "work.progress",
|
||
"message_id": message_id,
|
||
"display_text": "快速规划暂不可用,正在切换完整理解链路……",
|
||
}
|
||
)
|
||
is False
|
||
):
|
||
return {"cancelled": True}
|
||
|
||
conversation, conversation_key = self._conversation(principal, conversation_id)
|
||
try:
|
||
with conversation.lock:
|
||
return self._stream_locked(
|
||
conversation,
|
||
message,
|
||
message_id=message_id,
|
||
emit=emit,
|
||
execute_control=execute_control,
|
||
capability_snapshot=catalog,
|
||
capability_context=model_context,
|
||
control_terms=control_terms,
|
||
query_capabilities=catalog_query,
|
||
)
|
||
finally:
|
||
self._release_conversation(conversation_key, conversation)
|
||
|
||
@staticmethod
|
||
def _stream_scene_planner_failure(
|
||
*,
|
||
message_id: str,
|
||
emit: EmitEvent,
|
||
display_text: str,
|
||
answer: str,
|
||
) -> dict[str, object]:
|
||
if (
|
||
emit(
|
||
{
|
||
"type": "work.completed",
|
||
"message_id": message_id,
|
||
"success": False,
|
||
"display_text": display_text,
|
||
}
|
||
)
|
||
is False
|
||
):
|
||
return {"cancelled": True}
|
||
if (
|
||
emit(
|
||
{
|
||
"type": "answer.delta",
|
||
"message_id": message_id,
|
||
"delta": answer,
|
||
}
|
||
)
|
||
is False
|
||
):
|
||
return {"cancelled": True}
|
||
emit(
|
||
{
|
||
"type": "answer.completed",
|
||
"message_id": message_id,
|
||
"route": "scene_composer",
|
||
"answer": answer,
|
||
}
|
||
)
|
||
return {
|
||
"route": "scene_composer",
|
||
"success": False,
|
||
"answer": answer,
|
||
}
|
||
|
||
@staticmethod
|
||
def _copy_scene_plan(plan: Mapping[str, object]) -> dict[str, object]:
|
||
return json.loads(json.dumps(dict(plan), ensure_ascii=False, allow_nan=False))
|
||
|
||
@staticmethod
|
||
def _required_scene_peripheral_refs(
|
||
scene_context: Mapping[str, object],
|
||
) -> list[str]:
|
||
semantics = scene_context.get("scene_semantics")
|
||
policy = (
|
||
semantics.get("peripheral_policy")
|
||
if isinstance(semantics, Mapping)
|
||
else None
|
||
)
|
||
raw_refs = policy.get("required_refs", []) if isinstance(policy, Mapping) else []
|
||
available_refs = {
|
||
str(item.get("ref") or "").strip()
|
||
for item in scene_context.get("peripheral_actions", [])
|
||
if isinstance(item, Mapping) and str(item.get("ref") or "").strip()
|
||
}
|
||
return list(
|
||
dict.fromkeys(
|
||
str(value).strip()
|
||
for value in (
|
||
raw_refs
|
||
if isinstance(raw_refs, Sequence)
|
||
and not isinstance(raw_refs, (str, bytes, bytearray))
|
||
else []
|
||
)
|
||
if str(value).strip() in available_refs
|
||
)
|
||
)
|
||
|
||
@classmethod
|
||
def _with_required_scene_peripherals(
|
||
cls,
|
||
plan: Mapping[str, object],
|
||
scene_context: Mapping[str, object],
|
||
*,
|
||
maximum_requests: int = MAX_PLAN_REQUESTS,
|
||
) -> dict[str, object]:
|
||
"""Enforce required native domains and opaque peripheral references."""
|
||
|
||
normalized = cls._copy_scene_plan(plan)
|
||
requests = normalized.get("requests")
|
||
if not isinstance(requests, list):
|
||
raise SceneCompositionError("scene plan requests must be an array")
|
||
semantics = scene_context.get("scene_semantics")
|
||
if isinstance(semantics, Mapping) and romantic_scene(semantics.get("mode_name")):
|
||
replaced_refs = romantic_light_override_refs(scene_context.get("peripheral_actions", []))
|
||
requests[:] = [request for request in requests if not (
|
||
isinstance(request, Mapping) and request.get("capability") == "infrared.emit"
|
||
and any(isinstance(parameter, Mapping)
|
||
and parameter.get("name") == "capability_ref"
|
||
and isinstance(parameter.get("value"), str)
|
||
and parameter.get("value") in replaced_refs
|
||
for parameter in request.get("parameters", []))
|
||
)]
|
||
existing_refs = {
|
||
str(parameter.get("value") or "").strip()
|
||
for request in requests
|
||
if isinstance(request, Mapping)
|
||
and request.get("domain") == "self_extension"
|
||
and request.get("capability") == "infrared.emit"
|
||
for parameter in (
|
||
request.get("parameters", [])
|
||
if isinstance(request.get("parameters"), list)
|
||
else []
|
||
)
|
||
if isinstance(parameter, Mapping)
|
||
and parameter.get("name") == "capability_ref"
|
||
}
|
||
required_refs = cls._required_scene_peripheral_refs(scene_context)
|
||
missing_refs = [ref for ref in required_refs if ref not in existing_refs]
|
||
semantics = scene_context.get("scene_semantics")
|
||
raw_native_fallbacks = (
|
||
semantics.get("native_fallback_requests", [])
|
||
if isinstance(semantics, Mapping)
|
||
else []
|
||
)
|
||
native_fallbacks = [
|
||
deepcopy(dict(item))
|
||
for item in (
|
||
raw_native_fallbacks
|
||
if isinstance(raw_native_fallbacks, Sequence)
|
||
and not isinstance(raw_native_fallbacks, (str, bytes, bytearray))
|
||
else []
|
||
)
|
||
if isinstance(item, Mapping)
|
||
]
|
||
|
||
flexible_native_values = {
|
||
"climate.set_temperature",
|
||
"climate.set_fan_level",
|
||
"climate.set_cycle_mode",
|
||
"roof.set_shade_position",
|
||
"seat.set_backrest_recline",
|
||
"seat.set_legrest_height",
|
||
}
|
||
|
||
def parameter_map(request: Mapping[str, object]) -> dict[str, object]:
|
||
raw_parameters = request.get("parameters", [])
|
||
return {
|
||
str(parameter.get("name") or ""): parameter.get("value")
|
||
for parameter in (
|
||
raw_parameters
|
||
if isinstance(raw_parameters, Sequence)
|
||
and not isinstance(raw_parameters, (str, bytes, bytearray))
|
||
else []
|
||
)
|
||
if isinstance(parameter, Mapping)
|
||
and str(parameter.get("name") or "")
|
||
}
|
||
|
||
def native_requirement_satisfied(
|
||
request: Mapping[str, object],
|
||
fallback: Mapping[str, object],
|
||
) -> bool:
|
||
capability = str(fallback.get("capability") or "")
|
||
target_scope = str(fallback.get("target_scope") or "")
|
||
if (
|
||
request.get("capability") != capability
|
||
or request.get("target_scope") != target_scope
|
||
):
|
||
return False
|
||
if capability in flexible_native_values:
|
||
return True
|
||
required_parameters = parameter_map(fallback)
|
||
actual_parameters = parameter_map(request)
|
||
return all(
|
||
actual_parameters.get(name) == value
|
||
for name, value in required_parameters.items()
|
||
)
|
||
|
||
missing_native: list[dict[str, object]] = []
|
||
replaced_native = 0
|
||
for fallback in native_fallbacks:
|
||
matching_indexes = [
|
||
index
|
||
for index, request in enumerate(requests)
|
||
if isinstance(request, Mapping)
|
||
and request.get("capability") == fallback.get("capability")
|
||
and request.get("target_scope") == fallback.get("target_scope")
|
||
]
|
||
if any(
|
||
native_requirement_satisfied(requests[index], fallback)
|
||
for index in matching_indexes
|
||
):
|
||
continue
|
||
if matching_indexes:
|
||
requests[matching_indexes[0]] = deepcopy(fallback)
|
||
replaced_native += 1
|
||
continue
|
||
missing_native.append(fallback)
|
||
native_changed = bool(missing_native or replaced_native)
|
||
metadata = normalized.get("metadata")
|
||
if (
|
||
native_changed
|
||
and isinstance(metadata, Mapping)
|
||
and metadata.get("source") == "scene_template"
|
||
):
|
||
scope_policy = scene_context.get("seat_scope_policy")
|
||
raw_selected_scopes = (
|
||
scope_policy.get("selected_scopes", [])
|
||
if isinstance(scope_policy, Mapping)
|
||
else []
|
||
)
|
||
selected_scopes = frozenset(
|
||
str(scope)
|
||
for scope in (
|
||
raw_selected_scopes
|
||
if isinstance(raw_selected_scopes, Sequence)
|
||
and not isinstance(
|
||
raw_selected_scopes,
|
||
(str, bytes, bytearray),
|
||
)
|
||
else []
|
||
)
|
||
if str(scope)
|
||
)
|
||
if selected_scopes:
|
||
try:
|
||
normalized = project_seed_plan(normalized, selected_scopes)
|
||
except ModeCreationSeatPolicyError as exc:
|
||
raise SceneCompositionError(str(exc)) from exc
|
||
projected_requests = normalized.get("requests")
|
||
if not isinstance(projected_requests, list):
|
||
raise SceneCompositionError(
|
||
"projected scene template requests must be an array"
|
||
)
|
||
requests = projected_requests
|
||
if (
|
||
len(requests) + len(missing_native) + len(missing_refs)
|
||
> maximum_requests
|
||
):
|
||
raise SceneCompositionError(
|
||
"required scene controls exceed the scene action limit"
|
||
)
|
||
requests.extend(missing_native)
|
||
requests.extend(
|
||
{
|
||
"domain": "self_extension",
|
||
"capability": "infrared.emit",
|
||
"target_scope": capability_ref,
|
||
"operation": "emit",
|
||
"duration_ms": None,
|
||
"parameters": [
|
||
{"name": "capability_ref", "value": capability_ref}
|
||
],
|
||
"reason": (
|
||
"Mode semantics require this currently authorized learned-device "
|
||
"action; runtime binding resolution is still required."
|
||
),
|
||
}
|
||
for capability_ref in missing_refs
|
||
)
|
||
if native_changed or missing_refs:
|
||
if native_changed:
|
||
# The trusted template remains the source seed, but adding a
|
||
# native domain makes this an ordinary fully guarded semantic
|
||
# composition. First project its seat variants to the user's
|
||
# selected scope above, then remove metadata because template
|
||
# metadata requires byte-identical base requests.
|
||
normalized.pop("metadata", None)
|
||
domains = normalized.get("detected_domains")
|
||
if isinstance(domains, list):
|
||
for fallback in missing_native:
|
||
domain = str(fallback.get("domain") or "")
|
||
if domain and domain not in domains:
|
||
domains.append(domain)
|
||
if missing_refs and "self_extension" not in domains:
|
||
domains.append("self_extension")
|
||
summaries = normalized.get("decision_summary")
|
||
if isinstance(summaries, list):
|
||
summaries.append(
|
||
"已按模式语义补齐 "
|
||
f"{len(missing_native) + replaced_native} 项原生控制和 "
|
||
f"{len(missing_refs)} 项已授权外设动作。"
|
||
)
|
||
return normalized
|
||
|
||
def _scene_cache_key(
|
||
self,
|
||
*,
|
||
mode_name: str,
|
||
scene_context: Mapping[str, object],
|
||
) -> tuple[str, str, str, str]:
|
||
vehicle = scene_context.get("vehicle")
|
||
vehicle_id = (
|
||
str(vehicle.get("id") or "default")
|
||
if isinstance(vehicle, Mapping)
|
||
else "default"
|
||
)
|
||
revision = str(scene_context.get("composition_revision") or "")
|
||
if not revision:
|
||
raise _ScenePlannerFallback("scene capability revision is unavailable")
|
||
scope_policy = scene_context.get("seat_scope_policy")
|
||
selected_scopes = (
|
||
scope_policy.get("selected_scopes", [])
|
||
if isinstance(scope_policy, Mapping) else []
|
||
)
|
||
scope_key = ",".join(sorted(str(scope) for scope in selected_scopes))
|
||
return (
|
||
vehicle_id,
|
||
revision,
|
||
normalize_mode_profile_name(mode_name),
|
||
f"{SCENE_PLANNER_VERSION}:seats={scope_key}",
|
||
)
|
||
|
||
def _begin_scene_plan(
|
||
self,
|
||
key: tuple[str, str, str, str],
|
||
) -> tuple[dict[str, object] | None, _SceneFlight | None, bool]:
|
||
now = time.monotonic()
|
||
with self._scene_cache_lock:
|
||
expired = [
|
||
cached_key
|
||
for cached_key, (expires_at, _plan) in self._scene_cache.items()
|
||
if expires_at <= now
|
||
]
|
||
for cached_key in expired:
|
||
self._scene_cache.pop(cached_key, None)
|
||
cached = self._scene_cache.get(key)
|
||
if cached is not None:
|
||
self._scene_cache.move_to_end(key)
|
||
return self._copy_scene_plan(cached[1]), None, False
|
||
flight = self._scene_flights.get(key)
|
||
if flight is not None:
|
||
return None, flight, False
|
||
flight = _SceneFlight()
|
||
self._scene_flights[key] = flight
|
||
return None, flight, True
|
||
|
||
def _publish_scene_plan(
|
||
self,
|
||
key: tuple[str, str, str, str],
|
||
flight: _SceneFlight,
|
||
*,
|
||
plan: Mapping[str, object] | None = None,
|
||
error: BaseException | None = None,
|
||
) -> None:
|
||
with self._scene_cache_lock:
|
||
if plan is not None:
|
||
flight.plan = self._copy_scene_plan(plan)
|
||
flight.error = error
|
||
if error is not None:
|
||
self._scene_flights.pop(key, None)
|
||
flight.ready.set()
|
||
|
||
def _release_scene_flight(
|
||
self,
|
||
key: tuple[str, str, str, str],
|
||
flight: _SceneFlight,
|
||
) -> None:
|
||
with self._scene_cache_lock:
|
||
if self._scene_flights.get(key) is flight:
|
||
self._scene_flights.pop(key, None)
|
||
|
||
def _cache_validated_scene_plan(
|
||
self,
|
||
key: tuple[str, str, str, str],
|
||
plan: Mapping[str, object],
|
||
) -> None:
|
||
with self._scene_cache_lock:
|
||
self._scene_cache[key] = (
|
||
time.monotonic() + self._scene_cache_ttl,
|
||
self._copy_scene_plan(plan),
|
||
)
|
||
self._scene_cache.move_to_end(key)
|
||
while len(self._scene_cache) > self._scene_cache_limit:
|
||
self._scene_cache.popitem(last=False)
|
||
|
||
def _stream_scene_template_augmentation(
|
||
self,
|
||
message: str,
|
||
*,
|
||
message_id: str,
|
||
emit: EmitEvent,
|
||
execute_control: ControlExecutor,
|
||
scene_context: Mapping[str, object],
|
||
template_plan: Mapping[str, object],
|
||
) -> dict[str, object]:
|
||
"""Add only model-selected opaque IR refs to an immutable template base."""
|
||
|
||
peripheral_actions = scene_context.get("peripheral_actions", [])
|
||
if not isinstance(peripheral_actions, list) or not peripheral_actions:
|
||
return self._stream_direct_vehicle_control(
|
||
_VehicleAuthorization(message, "explicit_mode_profile_create"),
|
||
message_id=message_id,
|
||
emit=emit,
|
||
execute_control=execute_control,
|
||
assistant_plan=self._with_required_scene_peripherals(
|
||
template_plan,
|
||
scene_context,
|
||
),
|
||
response_route="scene_template",
|
||
)
|
||
base_requests = template_plan.get("requests", [])
|
||
if not isinstance(base_requests, list) or not base_requests:
|
||
raise _ScenePlannerFallback("scene template base is empty")
|
||
augmentation_context = {
|
||
**dict(scene_context),
|
||
"capabilities": [],
|
||
"scene_semantics": deepcopy(scene_context.get("scene_semantics")),
|
||
"device_inventory": [
|
||
deepcopy(item)
|
||
for item in scene_context.get("device_inventory", [])
|
||
if isinstance(item, Mapping)
|
||
and item.get("kind") == "learned_infrared"
|
||
],
|
||
"template_base": {
|
||
"immutable": True,
|
||
"actions": [
|
||
{
|
||
"capability": request.get("capability"),
|
||
"target_scope": request.get("target_scope"),
|
||
"parameters": request.get("parameters"),
|
||
}
|
||
for request in base_requests
|
||
if isinstance(request, Mapping)
|
||
],
|
||
},
|
||
}
|
||
augmentation_context["maximum_generated_action_count"] = max(
|
||
0,
|
||
MAX_PLAN_REQUESTS
|
||
- len(base_requests)
|
||
- len(self._required_scene_peripheral_refs(scene_context))
|
||
- len(
|
||
(
|
||
scene_context.get("scene_semantics") or {}
|
||
).get("native_fallback_requests", [])
|
||
if isinstance(scene_context.get("scene_semantics"), Mapping)
|
||
else []
|
||
),
|
||
)
|
||
augmentation_semantics = augmentation_context.get("scene_semantics")
|
||
if isinstance(augmentation_semantics, dict):
|
||
augmentation_semantics["grounded"] = False
|
||
augmentation_semantics["minimum_action_count"] = 0
|
||
augmentation_semantics["ambiguities_allowed"] = True
|
||
if (
|
||
emit(
|
||
{
|
||
"type": "work.progress",
|
||
"message_id": message_id,
|
||
"display_text": "正在根据已授权外设补充常用模式……",
|
||
}
|
||
)
|
||
is False
|
||
):
|
||
raise _ScenePlannerCancelled()
|
||
deadline = time.monotonic() + self._scene_turn_timeout
|
||
augmentation = self._request_structured_scene_plan(
|
||
message,
|
||
message_id=message_id,
|
||
emit=emit,
|
||
scene_context=augmentation_context,
|
||
deadline=deadline,
|
||
developer_instructions=SCENE_TEMPLATE_AUGMENTATION_INSTRUCTIONS,
|
||
dynamic_override=False,
|
||
enforce_required_controls=False,
|
||
)
|
||
proposed = augmentation.get("requests", [])
|
||
if not isinstance(proposed, list) or any(
|
||
not isinstance(request, Mapping)
|
||
or request.get("domain") != "self_extension"
|
||
or request.get("capability") != "infrared.emit"
|
||
for request in proposed
|
||
):
|
||
raise _ScenePlannerFallback("template augmentation returned a native mutation")
|
||
if len(base_requests) + len(proposed) > MAX_PLAN_REQUESTS:
|
||
raise _ScenePlannerFallback("template augmentation exceeds the scene action limit")
|
||
combined = self._copy_scene_plan(template_plan)
|
||
combined["requests"] = [
|
||
*deepcopy(base_requests),
|
||
*deepcopy(proposed),
|
||
]
|
||
combined = self._with_required_scene_peripherals(
|
||
combined,
|
||
scene_context,
|
||
)
|
||
return self._stream_direct_vehicle_control(
|
||
_VehicleAuthorization(message, "explicit_mode_profile_create"),
|
||
message_id=message_id,
|
||
emit=emit,
|
||
execute_control=execute_control,
|
||
assistant_plan=combined,
|
||
response_route="scene_template",
|
||
)
|
||
|
||
def _stream_scene_composition(
|
||
self,
|
||
message: str,
|
||
*,
|
||
mode_name: str,
|
||
message_id: str,
|
||
emit: EmitEvent,
|
||
execute_control: ControlExecutor,
|
||
scene_context: Mapping[str, object],
|
||
planning_deadline: float | None = None,
|
||
) -> dict[str, object]:
|
||
started_at = time.monotonic()
|
||
deadline = planning_deadline or (started_at + self._scene_turn_timeout)
|
||
key = self._scene_cache_key(
|
||
mode_name=mode_name,
|
||
scene_context=scene_context,
|
||
)
|
||
plan, flight, leader = self._begin_scene_plan(key)
|
||
cache_hit = plan is not None
|
||
if plan is not None:
|
||
if (
|
||
emit(
|
||
{
|
||
"type": "work.progress",
|
||
"message_id": message_id,
|
||
"display_text": "已复用场景组合,正在按实时能力重新校验……",
|
||
}
|
||
)
|
||
is False
|
||
):
|
||
raise _ScenePlannerCancelled()
|
||
elif not leader:
|
||
assert flight is not None
|
||
if (
|
||
emit(
|
||
{
|
||
"type": "work.progress",
|
||
"message_id": message_id,
|
||
"display_text": "相同场景正在组合,完成后将立即复用……",
|
||
}
|
||
)
|
||
is False
|
||
):
|
||
raise _ScenePlannerCancelled()
|
||
next_progress_at = time.monotonic() + self._scene_progress_interval
|
||
while not flight.ready.is_set():
|
||
remaining = deadline - time.monotonic()
|
||
if remaining <= 0:
|
||
raise _ScenePlannerTimeout(
|
||
"timed out waiting for an identical scene plan"
|
||
)
|
||
flight.ready.wait(min(0.25, remaining))
|
||
if not flight.ready.is_set() and time.monotonic() >= next_progress_at:
|
||
if (
|
||
emit(
|
||
{
|
||
"type": "work.progress",
|
||
"message_id": message_id,
|
||
"display_text": "相同场景仍在组合,请稍候……",
|
||
}
|
||
)
|
||
is False
|
||
):
|
||
raise _ScenePlannerCancelled()
|
||
next_progress_at = time.monotonic() + self._scene_progress_interval
|
||
if flight.error is not None:
|
||
if isinstance(flight.error, _ScenePlannerCancelled):
|
||
# The planner belongs to request scope, not to the browser
|
||
# connection that happened to become singleflight leader.
|
||
# Re-elect one surviving follower instead of forcing every
|
||
# waiter onto the slower general-agent path.
|
||
return self._stream_scene_composition(
|
||
message,
|
||
mode_name=mode_name,
|
||
message_id=message_id,
|
||
emit=emit,
|
||
execute_control=execute_control,
|
||
scene_context=scene_context,
|
||
planning_deadline=deadline,
|
||
)
|
||
if isinstance(flight.error, _ScenePlannerTimeout):
|
||
raise _ScenePlannerTimeout(str(flight.error)) from flight.error
|
||
if isinstance(flight.error, _ScenePlannerOverloaded):
|
||
raise _ScenePlannerOverloaded(str(flight.error)) from flight.error
|
||
raise _ScenePlannerFallback(str(flight.error)) from flight.error
|
||
if flight.plan is None:
|
||
raise _ScenePlannerFallback("identical scene planning returned no plan")
|
||
plan = self._copy_scene_plan(flight.plan)
|
||
cache_hit = True
|
||
else:
|
||
assert flight is not None
|
||
if not self._scene_planner_slots.acquire(blocking=False):
|
||
error = _ScenePlannerOverloaded("all Scene Composer slots are busy")
|
||
self._publish_scene_plan(key, flight, error=error)
|
||
raise error
|
||
planner_error: Exception | None = None
|
||
try:
|
||
plan = self._request_structured_scene_plan(
|
||
message,
|
||
message_id=message_id,
|
||
emit=emit,
|
||
scene_context=scene_context,
|
||
deadline=deadline,
|
||
)
|
||
except Exception as exc:
|
||
planner_error = exc
|
||
finally:
|
||
self._scene_planner_slots.release()
|
||
if planner_error is not None:
|
||
self._publish_scene_plan(key, flight, error=planner_error)
|
||
if isinstance(
|
||
planner_error,
|
||
(
|
||
_ScenePlannerCancelled,
|
||
_ScenePlannerTimeout,
|
||
_ScenePlannerOverloaded,
|
||
_ScenePlannerFallback,
|
||
),
|
||
):
|
||
raise planner_error
|
||
raise _ScenePlannerFallback(str(planner_error)) from planner_error
|
||
self._publish_scene_plan(key, flight, plan=plan)
|
||
|
||
assert plan is not None
|
||
try:
|
||
if (
|
||
emit(
|
||
{
|
||
"type": "work.progress",
|
||
"message_id": message_id,
|
||
"display_text": "模式候选动作已生成,正在进行参数与安全校验……",
|
||
}
|
||
)
|
||
is False
|
||
):
|
||
raise _ScenePlannerCancelled()
|
||
validation_started_at = time.monotonic()
|
||
response = self._stream_direct_vehicle_control(
|
||
_VehicleAuthorization(message, "explicit_mode_profile_create"),
|
||
message_id=message_id,
|
||
emit=emit,
|
||
execute_control=execute_control,
|
||
assistant_plan=plan,
|
||
response_route="scene_composer",
|
||
)
|
||
if response.get("success") is True:
|
||
self._cache_validated_scene_plan(key, plan)
|
||
LOGGER.info(
|
||
"Scene Composer finished revision=%s cache_hit=%s success=%s "
|
||
"validation_ms=%d elapsed_ms=%d",
|
||
key[1],
|
||
cache_hit,
|
||
response.get("success") is True,
|
||
round((time.monotonic() - validation_started_at) * 1000),
|
||
round((time.monotonic() - started_at) * 1000),
|
||
)
|
||
return response
|
||
finally:
|
||
if leader and flight is not None:
|
||
self._release_scene_flight(key, flight)
|
||
|
||
def _request_structured_scene_plan(
|
||
self,
|
||
message: str,
|
||
*,
|
||
message_id: str,
|
||
emit: EmitEvent,
|
||
scene_context: Mapping[str, object],
|
||
deadline: float,
|
||
developer_instructions: str | None = None,
|
||
dynamic_override: bool | None = None,
|
||
enforce_required_controls: bool = True,
|
||
) -> dict[str, object]:
|
||
start_structured_turn = getattr(self.client, "start_structured_turn", None)
|
||
if not callable(start_structured_turn):
|
||
raise _ScenePlannerFallback("structured turns are not supported")
|
||
|
||
planning_started_at = time.monotonic()
|
||
dynamic = (
|
||
needs_mode_schedule(message)
|
||
if dynamic_override is None
|
||
else dynamic_override
|
||
)
|
||
planner_context = deepcopy(dict(scene_context))
|
||
if dynamic:
|
||
# Timed IR playback is not yet phase-aware. Keep learned devices in
|
||
# inventory so the model can explain availability, but do not offer
|
||
# them as selectable actions in a schedule.
|
||
planner_context["peripheral_actions"] = []
|
||
dynamic_semantics = planner_context.get("scene_semantics")
|
||
dynamic_policy = (
|
||
dynamic_semantics.get("peripheral_policy")
|
||
if isinstance(dynamic_semantics, dict)
|
||
else None
|
||
)
|
||
if isinstance(dynamic_policy, dict):
|
||
dynamic_policy["available_action_count"] = 0
|
||
dynamic_policy["required_refs"] = []
|
||
dynamic_policy["required_reason"] = (
|
||
"Timed learned-infrared actions are not supported."
|
||
)
|
||
for item in planner_context.get("device_inventory", []):
|
||
if (
|
||
isinstance(item, dict)
|
||
and item.get("kind") == "learned_infrared"
|
||
and item.get("availability") == "available"
|
||
):
|
||
item["availability"] = "unavailable"
|
||
item["availability_reason"] = "timed_ir_not_supported"
|
||
output_schema = build_scene_output_schema(planner_context, dynamic=dynamic)
|
||
context_bytes = len(
|
||
json.dumps(
|
||
planner_context,
|
||
ensure_ascii=False,
|
||
separators=(",", ":"),
|
||
).encode("utf-8")
|
||
)
|
||
schema_bytes = len(
|
||
json.dumps(
|
||
output_schema,
|
||
ensure_ascii=False,
|
||
separators=(",", ":"),
|
||
).encode("utf-8")
|
||
)
|
||
thread_id: str | None = None
|
||
active: ActiveCodexTurn | None = None
|
||
final_text = ""
|
||
streamed_text = ""
|
||
progress_emitted = False
|
||
next_heartbeat_at = time.monotonic() + self._scene_progress_interval
|
||
thread_ready_at: float | None = None
|
||
turn_ready_at: float | None = None
|
||
first_event_at: float | None = None
|
||
terminal_received = False
|
||
interrupt_requested = False
|
||
try:
|
||
try:
|
||
thread_id = self.client.start_thread(
|
||
developer_instructions=(
|
||
developer_instructions
|
||
or (
|
||
SCHEDULE_COMPOSER_INSTRUCTIONS
|
||
if dynamic
|
||
else SCENE_COMPOSER_INSTRUCTIONS
|
||
)
|
||
),
|
||
ephemeral=True,
|
||
replace_developer_instructions=True,
|
||
timeout=self._remaining_scene_time(deadline),
|
||
)
|
||
except Exception as exc:
|
||
self._raise_if_scene_timeout(exc, deadline, "thread/start")
|
||
raise
|
||
thread_ready_at = time.monotonic()
|
||
try:
|
||
active = start_structured_turn(
|
||
thread_id,
|
||
message,
|
||
application_context={
|
||
**planner_context,
|
||
"natural_language_routing": {
|
||
"original_utterance": message,
|
||
"parsed_clauses": [],
|
||
"resolved_clauses": [],
|
||
"unresolved_clauses": [message],
|
||
"cloud_input_utterance": message,
|
||
},
|
||
},
|
||
output_schema=output_schema,
|
||
timeout=self._remaining_scene_time(deadline),
|
||
)
|
||
except Exception as exc:
|
||
self._raise_if_scene_timeout(exc, deadline, "turn/start")
|
||
raise
|
||
turn_ready_at = time.monotonic()
|
||
terminal: dict[str, object] | None = None
|
||
while terminal is None:
|
||
remaining = deadline - time.monotonic()
|
||
if remaining <= 0:
|
||
self._interrupt_scene_turn(active)
|
||
interrupt_requested = True
|
||
raise _ScenePlannerTimeout("Scene Composer turn timed out")
|
||
try:
|
||
event = active.events.get(timeout=min(0.2, remaining))
|
||
except queue.Empty:
|
||
if time.monotonic() >= next_heartbeat_at:
|
||
if (
|
||
emit(
|
||
{
|
||
"type": "work.progress",
|
||
"message_id": message_id,
|
||
"display_text": "小迪正在生成结构化模式方案……",
|
||
}
|
||
)
|
||
is False
|
||
):
|
||
self._interrupt_scene_turn(active)
|
||
interrupt_requested = True
|
||
raise _ScenePlannerCancelled()
|
||
next_heartbeat_at = (
|
||
time.monotonic() + self._scene_progress_interval
|
||
)
|
||
continue
|
||
if first_event_at is None:
|
||
first_event_at = time.monotonic()
|
||
method = str(event.get("method") or "")
|
||
params = event.get("params")
|
||
params = params if isinstance(params, dict) else {}
|
||
if not progress_emitted and method in {
|
||
"turn/started",
|
||
"item/started",
|
||
"item/agentMessage/delta",
|
||
}:
|
||
progress_emitted = True
|
||
if (
|
||
emit(
|
||
{
|
||
"type": "work.progress",
|
||
"message_id": message_id,
|
||
"display_text": "小迪已开始生成结构化模式方案……",
|
||
}
|
||
)
|
||
is False
|
||
):
|
||
self._interrupt_scene_turn(active)
|
||
interrupt_requested = True
|
||
raise _ScenePlannerCancelled()
|
||
if method == "transport/error":
|
||
raise _ScenePlannerFallback(
|
||
str(params.get("message") or "Codex disconnected")
|
||
)
|
||
if method == "item/agentMessage/delta":
|
||
streamed_text += str(params.get("delta") or "")
|
||
if len(streamed_text.encode("utf-8")) > 64 * 1024:
|
||
self._interrupt_scene_turn(active)
|
||
interrupt_requested = True
|
||
raise SceneCompositionError("scene composer response is too large")
|
||
elif method == "item/completed":
|
||
item = params.get("item")
|
||
if isinstance(item, Mapping) and item.get("type") == "agentMessage":
|
||
final_text = str(item.get("text") or final_text)
|
||
elif method == "error":
|
||
detail = params.get("error")
|
||
if isinstance(detail, Mapping):
|
||
detail = detail.get("message") or detail.get("codexErrorInfo")
|
||
raise _ScenePlannerFallback(
|
||
str(detail or params.get("message") or "Scene Composer turn failed")
|
||
)
|
||
elif method == "turn/completed":
|
||
terminal = params
|
||
terminal_received = True
|
||
if terminal is None and time.monotonic() >= next_heartbeat_at:
|
||
if (
|
||
emit(
|
||
{
|
||
"type": "work.progress",
|
||
"message_id": message_id,
|
||
"display_text": "小迪正在生成结构化模式方案……",
|
||
}
|
||
)
|
||
is False
|
||
):
|
||
self._interrupt_scene_turn(active)
|
||
interrupt_requested = True
|
||
raise _ScenePlannerCancelled()
|
||
next_heartbeat_at = time.monotonic() + self._scene_progress_interval
|
||
|
||
turn = terminal.get("turn") if terminal else None
|
||
turn = turn if isinstance(turn, Mapping) else {}
|
||
status = str(turn.get("status") or "failed")
|
||
if status != "completed":
|
||
detail = turn.get("error")
|
||
if isinstance(detail, Mapping):
|
||
detail = detail.get("message") or detail.get("codexErrorInfo")
|
||
raise _ScenePlannerFallback(str(detail or f"Scene Composer turn {status}"))
|
||
output_text = final_text or streamed_text
|
||
if not output_text:
|
||
raise SceneCompositionError("scene composer produced an empty response")
|
||
try:
|
||
raw_output = json.loads(output_text)
|
||
except json.JSONDecodeError as exc:
|
||
raise SceneCompositionError("scene composer output was not valid JSON") from exc
|
||
if not isinstance(raw_output, Mapping):
|
||
raise SceneCompositionError("scene composer output must be an object")
|
||
normalized = normalize_scene_output(raw_output, planner_context, dynamic=dynamic)
|
||
if not dynamic and enforce_required_controls:
|
||
normalized = self._with_required_scene_peripherals(
|
||
normalized,
|
||
planner_context,
|
||
)
|
||
finished_at = time.monotonic()
|
||
LOGGER.info(
|
||
"Scene Composer model completed context_bytes=%d schema_bytes=%d "
|
||
"thread_start_ms=%d turn_start_ms=%d first_event_ms=%d model_ms=%d",
|
||
context_bytes,
|
||
schema_bytes,
|
||
round(((thread_ready_at or finished_at) - planning_started_at) * 1000),
|
||
round(
|
||
(
|
||
(turn_ready_at or finished_at)
|
||
- (thread_ready_at or planning_started_at)
|
||
)
|
||
* 1000
|
||
),
|
||
(
|
||
round((first_event_at - planning_started_at) * 1000)
|
||
if first_event_at is not None
|
||
else -1
|
||
),
|
||
round((finished_at - planning_started_at) * 1000),
|
||
)
|
||
return normalized
|
||
finally:
|
||
if active is not None:
|
||
if not terminal_received and not interrupt_requested:
|
||
self._interrupt_scene_turn(active)
|
||
if not self._finish_scene_turn(active):
|
||
abandon = getattr(self.client, "abandon_turn", None)
|
||
if callable(abandon):
|
||
abandon(active)
|
||
if thread_id is not None:
|
||
self._unsubscribe_scene_thread(thread_id)
|
||
|
||
@staticmethod
|
||
def _remaining_scene_time(deadline: float) -> float:
|
||
remaining = deadline - time.monotonic()
|
||
if remaining <= 0:
|
||
raise _ScenePlannerTimeout("Scene Composer deadline expired")
|
||
return remaining
|
||
|
||
@staticmethod
|
||
def _raise_if_scene_timeout(
|
||
exc: BaseException,
|
||
deadline: float,
|
||
stage: str,
|
||
) -> None:
|
||
if (
|
||
exc.__class__.__name__ == "CodexAppServerTimeout"
|
||
or time.monotonic() >= deadline
|
||
):
|
||
raise _ScenePlannerTimeout(
|
||
f"Scene Composer {stage} exceeded its deadline"
|
||
) from exc
|
||
|
||
def _interrupt_scene_turn(self, active: ActiveCodexTurn) -> None:
|
||
try:
|
||
self.client.interrupt(active, timeout=0.25)
|
||
except (RuntimeError, TypeError) as exc:
|
||
LOGGER.debug("Scene Composer turn interrupt failed", exc_info=exc)
|
||
|
||
def _finish_scene_turn(self, active: ActiveCodexTurn) -> bool:
|
||
try:
|
||
self.client.finish_turn(active, timeout=0.25)
|
||
return True
|
||
except (RuntimeError, TypeError) as exc:
|
||
LOGGER.debug("Scene Composer turn cleanup failed", exc_info=exc)
|
||
return False
|
||
|
||
def _unsubscribe_scene_thread(self, thread_id: str) -> None:
|
||
unsubscribe = getattr(self.client, "unsubscribe_thread", None)
|
||
if not callable(unsubscribe):
|
||
return
|
||
try:
|
||
unsubscribe(thread_id, timeout=0.25)
|
||
except (RuntimeError, TypeError) as exc:
|
||
LOGGER.debug("Scene Composer thread cleanup failed", exc_info=exc)
|
||
|
||
def _stream_local_first_vehicle_control(
|
||
self,
|
||
authorization: _VehicleAuthorization,
|
||
*,
|
||
message_id: str,
|
||
emit: EmitEvent,
|
||
preview_control: ControlExecutor,
|
||
supplement_control: ControlExecutor,
|
||
conversation_id: str,
|
||
principal: str,
|
||
capability_snapshot: Mapping[str, object],
|
||
capability_context: Mapping[str, object],
|
||
control_terms: Sequence[str],
|
||
query_capabilities: CapabilityQuery,
|
||
) -> dict[str, object]:
|
||
"""Publish deterministic controls, then delegate only unresolved text."""
|
||
|
||
instruction = authorization.instruction
|
||
if instruction is None:
|
||
raise CodexAssistantError("local-first vehicle control was not authorized")
|
||
if (
|
||
emit(
|
||
{
|
||
"type": "action.started",
|
||
"message_id": message_id,
|
||
"display_text": "正在通过本地规则解析明确控制指令……",
|
||
}
|
||
)
|
||
is False
|
||
):
|
||
return {"cancelled": True}
|
||
|
||
stream_open = True
|
||
|
||
def on_progress(payload: dict[str, object]) -> None:
|
||
nonlocal stream_open
|
||
if not stream_open:
|
||
return
|
||
validated_payload = self._validated_control_payload(payload)
|
||
if validated_payload is not None:
|
||
stream_open = (
|
||
emit(
|
||
{
|
||
"type": "validated_control_commands",
|
||
"message_id": message_id,
|
||
"payload": validated_payload,
|
||
}
|
||
)
|
||
is not False
|
||
)
|
||
if not stream_open:
|
||
return
|
||
stream_open = (
|
||
emit(
|
||
{
|
||
"type": "action.progress",
|
||
"message_id": message_id,
|
||
"display_text": "本地控制指令已解析并进入下发链路……",
|
||
"payload": self._safe_vehicle_result(payload),
|
||
}
|
||
)
|
||
is not False
|
||
)
|
||
|
||
try:
|
||
preview = preview_control(instruction, on_progress)
|
||
if not isinstance(preview, dict):
|
||
raise TypeError("vehicle preview returned an invalid result")
|
||
except Exception: # noqa: BLE001 - stable browser-facing failure
|
||
answer = "车辆控制未完成,本地安全执行链没有返回有效结果。"
|
||
for event in (
|
||
{
|
||
"type": "action.completed",
|
||
"message_id": message_id,
|
||
"success": False,
|
||
"state": "failed",
|
||
"display_text": "本地控制未完成",
|
||
},
|
||
{
|
||
"type": "answer.delta",
|
||
"message_id": message_id,
|
||
"delta": answer,
|
||
},
|
||
{
|
||
"type": "work.completed",
|
||
"message_id": message_id,
|
||
"success": False,
|
||
"display_text": "车辆控制未完成",
|
||
},
|
||
{
|
||
"type": "answer.completed",
|
||
"message_id": message_id,
|
||
"route": "vehicle_control",
|
||
"answer": answer,
|
||
},
|
||
):
|
||
if emit(event) is False:
|
||
return {"cancelled": True, "answer": answer}
|
||
return {
|
||
"route": "vehicle_control",
|
||
"answer": answer,
|
||
"success": False,
|
||
}
|
||
|
||
if not stream_open:
|
||
return {"cancelled": True}
|
||
|
||
routing_value = preview.get("natural_language_routing")
|
||
routing = dict(routing_value) if isinstance(routing_value, Mapping) else {}
|
||
raw_unresolved = routing.get("unresolved_clauses")
|
||
if not isinstance(raw_unresolved, Sequence) or isinstance(
|
||
raw_unresolved,
|
||
(str, bytes, bytearray),
|
||
):
|
||
raw_unresolved = preview.get("unresolved_clauses")
|
||
unresolved_clauses = list(
|
||
dict.fromkeys(
|
||
text
|
||
for item in (
|
||
list(raw_unresolved)[:64]
|
||
if isinstance(raw_unresolved, Sequence)
|
||
and not isinstance(raw_unresolved, (str, bytes, bytearray))
|
||
else []
|
||
)
|
||
for text in [str(item).strip()[:1000]]
|
||
if text
|
||
)
|
||
)
|
||
safe_preview = self._safe_vehicle_result(preview)
|
||
|
||
if not unresolved_clauses:
|
||
action_state, successful, completion_text = _vehicle_action_state(preview)
|
||
answer = self._fast_comfort_answer(
|
||
"explicit_registered_control",
|
||
preview,
|
||
successful,
|
||
)
|
||
if not stream_open:
|
||
return {"cancelled": True, "answer": answer}
|
||
for event in (
|
||
{
|
||
"type": "action.completed",
|
||
"message_id": message_id,
|
||
"success": successful,
|
||
"state": action_state,
|
||
"display_text": completion_text,
|
||
"payload": safe_preview,
|
||
},
|
||
{
|
||
"type": "answer.delta",
|
||
"message_id": message_id,
|
||
"delta": answer,
|
||
},
|
||
{
|
||
"type": "work.completed",
|
||
"message_id": message_id,
|
||
"success": successful,
|
||
"state": action_state,
|
||
"display_text": completion_text,
|
||
},
|
||
{
|
||
"type": "answer.completed",
|
||
"message_id": message_id,
|
||
"route": "vehicle_control",
|
||
"answer": answer,
|
||
},
|
||
):
|
||
if emit(event) is False:
|
||
return {"cancelled": True, "answer": answer}
|
||
return {
|
||
"route": "vehicle_control",
|
||
"answer": answer,
|
||
"success": successful,
|
||
"result": safe_preview,
|
||
}
|
||
|
||
local_has_commands = bool(
|
||
preview.get("function_configs") and preview.get("command_plans")
|
||
)
|
||
local_status = str(preview.get("status") or "")
|
||
local_succeeded = local_has_commands and local_status in {
|
||
"succeeded",
|
||
"partially_succeeded",
|
||
"no_action",
|
||
}
|
||
if stream_open and emit(
|
||
{
|
||
"type": "action.completed",
|
||
"message_id": message_id,
|
||
"success": local_succeeded,
|
||
"state": "submitted" if local_succeeded else "failed",
|
||
"display_text": (
|
||
"本地明确控制已先行处理"
|
||
if local_succeeded
|
||
else "本地阶段未生成控制指令"
|
||
),
|
||
"payload": safe_preview,
|
||
}
|
||
) is False:
|
||
return {"cancelled": True}
|
||
if emit(
|
||
{
|
||
"type": "work.progress",
|
||
"message_id": message_id,
|
||
"display_text": "本地明确子句已处理,正在补充未解析子句……",
|
||
}
|
||
) is False:
|
||
return {"cancelled": True}
|
||
|
||
cloud_message = ",".join(unresolved_clauses)
|
||
routing.update(
|
||
{
|
||
"original_utterance": str(
|
||
routing.get("original_utterance") or instruction
|
||
)[:1000],
|
||
"parsed_clauses": list(routing.get("parsed_clauses") or []),
|
||
"resolved_clauses": list(routing.get("resolved_clauses") or []),
|
||
"unresolved_clauses": unresolved_clauses,
|
||
"cloud_input_utterance": cloud_message,
|
||
}
|
||
)
|
||
if not self.client.available:
|
||
answer = (
|
||
"已先行处理本地可解析的控制;其余子句暂无法连接云端模型,"
|
||
"未生成额外控制。"
|
||
)
|
||
emit(
|
||
{
|
||
"type": "answer.delta",
|
||
"message_id": message_id,
|
||
"delta": answer,
|
||
}
|
||
)
|
||
emit(
|
||
{
|
||
"type": "work.completed",
|
||
"message_id": message_id,
|
||
"success": local_succeeded,
|
||
"display_text": "本地控制已处理,云端补充不可用",
|
||
}
|
||
)
|
||
emit(
|
||
{
|
||
"type": "answer.completed",
|
||
"message_id": message_id,
|
||
"route": "vehicle_control",
|
||
"answer": answer,
|
||
}
|
||
)
|
||
return {
|
||
"route": "vehicle_control",
|
||
"answer": answer,
|
||
"success": local_succeeded,
|
||
"result": safe_preview,
|
||
}
|
||
|
||
conversation, conversation_key = self._conversation(
|
||
principal,
|
||
conversation_id,
|
||
)
|
||
try:
|
||
with conversation.lock:
|
||
return self._stream_locked(
|
||
conversation,
|
||
cloud_message,
|
||
message_id=message_id,
|
||
emit=emit,
|
||
execute_control=supplement_control,
|
||
capability_snapshot=capability_snapshot,
|
||
capability_context={
|
||
**dict(capability_context),
|
||
"natural_language_routing": routing,
|
||
},
|
||
control_terms=control_terms,
|
||
query_capabilities=query_capabilities,
|
||
)
|
||
finally:
|
||
self._release_conversation(conversation_key, conversation)
|
||
|
||
def _stream_direct_vehicle_control(
|
||
self,
|
||
authorization: _VehicleAuthorization,
|
||
*,
|
||
message_id: str,
|
||
emit: EmitEvent,
|
||
execute_control: ControlExecutor,
|
||
assistant_plan: Mapping[str, object] | None = None,
|
||
response_route: str = "vehicle_control",
|
||
) -> dict[str, object]:
|
||
"""Execute deterministic high-confidence routes without model latency."""
|
||
|
||
instruction = authorization.instruction
|
||
if instruction is None:
|
||
raise CodexAssistantError("direct vehicle control was not authorized")
|
||
mode_profile_preview = authorization.reason in {
|
||
"explicit_mode_profile_create",
|
||
"explicit_mode_profile_draft_update",
|
||
}
|
||
mode_profile_draft_update = (
|
||
authorization.reason == "explicit_mode_profile_draft_update"
|
||
)
|
||
exact_quick_control = authorization.reason == "exact_quick_control"
|
||
explicit_local_control = authorization.reason in {
|
||
"exact_quick_control",
|
||
"explicit_registered_control",
|
||
}
|
||
saved_mode_execution = authorization.reason == "explicit_saved_mode_execute"
|
||
mode_profile_management_route = {
|
||
"explicit_mode_profile_update": "mode_profile_update",
|
||
"explicit_mode_profile_delete": "mode_profile_delete",
|
||
}.get(authorization.reason)
|
||
if (
|
||
emit(
|
||
{
|
||
"type": "action.started",
|
||
"message_id": message_id,
|
||
"display_text": (
|
||
"正在生成模式动作预览……"
|
||
if mode_profile_preview and not mode_profile_draft_update
|
||
else "正在调整待保存模式动作……"
|
||
if mode_profile_draft_update
|
||
else "正在更新已保存的功能模式……"
|
||
if mode_profile_management_route == "mode_profile_update"
|
||
else "正在删除自定义功能模式……"
|
||
if mode_profile_management_route == "mode_profile_delete"
|
||
else "正在通过安全执行链进入功能模式……"
|
||
if saved_mode_execution
|
||
else "正在通过车辆安全链执行快捷控制……"
|
||
if exact_quick_control
|
||
else "正在通过本地规则解析并下发控制……"
|
||
if explicit_local_control
|
||
else "正在通过车辆安全链执行舒适性调节……"
|
||
),
|
||
}
|
||
)
|
||
is False
|
||
):
|
||
return {"cancelled": True}
|
||
|
||
stream_open = True
|
||
|
||
def on_progress(payload: dict[str, object]) -> None:
|
||
nonlocal stream_open
|
||
if not stream_open:
|
||
return
|
||
validated_payload = self._validated_control_payload(payload)
|
||
if validated_payload is not None:
|
||
stream_open = (
|
||
emit(
|
||
{
|
||
"type": "validated_control_commands",
|
||
"message_id": message_id,
|
||
"payload": validated_payload,
|
||
}
|
||
)
|
||
is not False
|
||
)
|
||
if not stream_open:
|
||
return
|
||
stream_open = (
|
||
emit(
|
||
{
|
||
"type": "action.progress",
|
||
"message_id": message_id,
|
||
"display_text": (
|
||
"模式动作已解析,正在进行安全校验……"
|
||
if mode_profile_preview
|
||
else "已保存模式动作已载入,正在校验并执行……"
|
||
if saved_mode_execution
|
||
else "功能模式存储请求已解析,正在校验……"
|
||
if mode_profile_management_route is not None
|
||
else "本地控制指令已通过校验并进入下发链路……"
|
||
if explicit_local_control
|
||
else "控制指令已通过校验,正在执行……"
|
||
),
|
||
"payload": self._safe_vehicle_result(payload),
|
||
}
|
||
)
|
||
is not False
|
||
)
|
||
|
||
try:
|
||
result = (
|
||
execute_control(instruction, on_progress, self._copy_scene_plan(assistant_plan))
|
||
if assistant_plan is not None
|
||
else execute_control(instruction, on_progress)
|
||
)
|
||
if not isinstance(result, dict):
|
||
raise TypeError("vehicle control returned an invalid result")
|
||
safe_result = self._safe_vehicle_result(result)
|
||
action_state, successful, completion_text = _vehicle_action_state(result)
|
||
if (
|
||
saved_mode_execution
|
||
and result.get("status") == "partially_succeeded"
|
||
):
|
||
successful = True
|
||
action_state = (
|
||
"simulated"
|
||
if result.get("execution_mode") != "hardware"
|
||
else "submitted"
|
||
)
|
||
completion_text = (
|
||
"功能模式模拟编排已完成"
|
||
if action_state == "simulated"
|
||
else "功能模式指令已提交"
|
||
)
|
||
elif mode_profile_preview:
|
||
successful = _mode_profile_result_succeeded(result)
|
||
action_state = "simulated" if successful else "failed"
|
||
completion_text = (
|
||
"功能模式预览已生成" if successful else "功能模式预览生成失败"
|
||
)
|
||
elif mode_profile_management_route is not None:
|
||
successful = _mode_profile_management_result_succeeded(
|
||
result,
|
||
mode_profile_management_route,
|
||
)
|
||
action_state = "simulated" if successful else "failed"
|
||
completion_text = (
|
||
"功能模式已更新"
|
||
if successful
|
||
and mode_profile_management_route == "mode_profile_update"
|
||
else "自定义功能模式已删除"
|
||
if successful
|
||
else "功能模式更新未完成"
|
||
if mode_profile_management_route == "mode_profile_update"
|
||
else "功能模式删除未完成"
|
||
)
|
||
except Exception: # noqa: BLE001 - return a stable public failure
|
||
result = {}
|
||
safe_result = {"status": "failed", "execution_mode": "unknown", "verified": False}
|
||
action_state = "failed"
|
||
completion_text = "车辆控制未完成"
|
||
successful = False
|
||
|
||
answer = (
|
||
self._mode_profile_preview_answer(result, successful)
|
||
if mode_profile_preview
|
||
else self._mode_profile_management_answer(
|
||
result,
|
||
successful,
|
||
mode_profile_management_route,
|
||
)
|
||
if mode_profile_management_route is not None
|
||
else self._fast_comfort_answer(authorization.reason, result, successful)
|
||
)
|
||
if not stream_open:
|
||
return {
|
||
"cancelled": True,
|
||
"route": response_route,
|
||
"answer": answer,
|
||
"success": successful,
|
||
}
|
||
events = (
|
||
{
|
||
"type": "action.completed",
|
||
"message_id": message_id,
|
||
"success": successful,
|
||
"state": action_state,
|
||
"display_text": completion_text,
|
||
"payload": safe_result,
|
||
},
|
||
{
|
||
"type": "answer.delta",
|
||
"message_id": message_id,
|
||
"delta": answer,
|
||
},
|
||
{
|
||
"type": "work.completed",
|
||
"message_id": message_id,
|
||
"success": successful,
|
||
"state": action_state,
|
||
"display_text": (
|
||
completion_text
|
||
if action_state == "running" or result.get("orchestration_route") == "mode_schedule_stop"
|
||
else "功能模式预览已生成"
|
||
if mode_profile_preview and successful
|
||
else "功能模式预览生成失败"
|
||
if mode_profile_preview
|
||
else completion_text
|
||
if mode_profile_management_route is not None
|
||
else "功能模式已提交,等待车辆状态确认"
|
||
if saved_mode_execution and action_state == "submitted"
|
||
else "功能模式模拟编排已完成"
|
||
if saved_mode_execution and action_state == "simulated"
|
||
else "功能模式执行已完成"
|
||
if saved_mode_execution and successful
|
||
else "功能模式执行未完成"
|
||
if saved_mode_execution
|
||
else "快捷控制已提交,等待车辆状态确认"
|
||
if exact_quick_control and action_state == "submitted"
|
||
else "快捷控制模拟编排已完成"
|
||
if exact_quick_control and action_state == "simulated"
|
||
else "快捷控制已完成"
|
||
if exact_quick_control and successful
|
||
else "快捷控制未完成"
|
||
if exact_quick_control
|
||
else "明确控制已提交,等待车辆状态确认"
|
||
if explicit_local_control and action_state == "submitted"
|
||
else "明确控制模拟编排已完成"
|
||
if explicit_local_control and action_state == "simulated"
|
||
else "明确控制已完成"
|
||
if explicit_local_control and successful
|
||
else "明确控制未完成"
|
||
if explicit_local_control
|
||
else "舒适性调节已提交,等待车辆状态确认"
|
||
if action_state == "submitted"
|
||
else "舒适性调节模拟编排已完成"
|
||
if action_state == "simulated"
|
||
else "舒适性调节已完成"
|
||
if successful
|
||
else "舒适性调节未完成"
|
||
),
|
||
},
|
||
{
|
||
"type": "answer.completed",
|
||
"message_id": message_id,
|
||
"route": response_route,
|
||
"answer": answer,
|
||
},
|
||
)
|
||
for event in events:
|
||
if emit(event) is False:
|
||
return {
|
||
"cancelled": True,
|
||
"route": response_route,
|
||
"answer": answer,
|
||
"success": successful,
|
||
}
|
||
return {
|
||
"route": response_route,
|
||
"answer": answer,
|
||
"success": successful,
|
||
"result": safe_result,
|
||
}
|
||
|
||
@staticmethod
|
||
def _mode_profile_failure_feedback(
|
||
result: Mapping[str, object],
|
||
) -> tuple[str, str]:
|
||
def clean(value: object, *, limit: int = 420) -> str:
|
||
text = re.sub(r"[\x00-\x1f\x7f]+", " ", str(value or ""))
|
||
return re.sub(r"\s+", " ", text).strip(" ,,。;;::")[:limit]
|
||
|
||
failure = result.get("mode_creation_failure")
|
||
if isinstance(failure, Mapping):
|
||
reason = clean(failure.get("reason"))
|
||
next_step = clean(failure.get("next_step"))
|
||
if reason and next_step:
|
||
if (
|
||
INTERNAL_SCENE_BUILDER_MARKER in reason
|
||
or INTERNAL_SCENE_BUILDER_MARKER in next_step
|
||
):
|
||
return (
|
||
"未能根据场景名称生成通过工程能力校验的座舱动作",
|
||
"请稍后重试;若该场景尚未登记,可补充希望联动的设备状态",
|
||
)
|
||
return reason, next_step
|
||
|
||
unresolved = result.get("unresolved_clauses")
|
||
if isinstance(unresolved, Sequence) and not isinstance(
|
||
unresolved,
|
||
(str, bytes, bytearray),
|
||
):
|
||
clauses = list(
|
||
dict.fromkeys(
|
||
clean(item, limit=120)
|
||
for item in unresolved[:3]
|
||
if clean(item, limit=120)
|
||
and INTERNAL_SCENE_BUILDER_MARKER
|
||
not in clean(item, limit=120)
|
||
)
|
||
)
|
||
if clauses:
|
||
rendered = "、".join(f"“{item}”" for item in clauses)
|
||
return (
|
||
f"以下设置未能转换为可保存的座舱动作:{rendered}",
|
||
(
|
||
"请按“设备 + 动作 + 明确目标”重述,例如“副驾腿托调整到80%”"
|
||
"或“空调调整到24度”,然后重试"
|
||
),
|
||
)
|
||
|
||
validation = result.get("validation")
|
||
if isinstance(validation, Sequence) and not isinstance(
|
||
validation,
|
||
(str, bytes, bytearray),
|
||
):
|
||
safe_codes = {
|
||
"AMBIGUOUS_INTENT",
|
||
"MODE_PROFILE_DRAFT_INCOMPLETE",
|
||
"MODE_PROFILE_DRAFT_EMPTY",
|
||
"NO_EXECUTABLE_OPERATION",
|
||
"UNRESOLVED_UTTERANCE_SPAN",
|
||
}
|
||
for item in validation[:12]:
|
||
if not isinstance(item, Mapping):
|
||
continue
|
||
if str(item.get("severity") or "") != "error":
|
||
continue
|
||
if str(item.get("code") or "") not in safe_codes:
|
||
continue
|
||
message = clean(item.get("message"), limit=240)
|
||
if not message or re.search(
|
||
r"(?:CAN|CH[01]|JSON|schema|0x[0-9a-f]+|\\|/[A-Za-z])",
|
||
message,
|
||
re.IGNORECASE,
|
||
):
|
||
continue
|
||
return (
|
||
message,
|
||
"请根据上述原因改为当前支持且目标唯一的座舱设置,然后重新提交",
|
||
)
|
||
|
||
status = clean(result.get("status"), limit=60)
|
||
if status in {"needs_confirmation", "validation_failed"}:
|
||
return (
|
||
"场景设置存在未识别、冲突或超出支持范围的内容",
|
||
"请逐项明确设备、动作和单一目标值后重试;仍失败时可将设置分条提交",
|
||
)
|
||
return (
|
||
"模式构建服务未返回可验证的成功结果",
|
||
"请稍后重试;若持续失败,请将各项设置按“设备 + 动作 + 目标值”分条输入",
|
||
)
|
||
|
||
@staticmethod
|
||
def _mode_profile_preview_answer(
|
||
result: Mapping[str, object], successful: bool
|
||
) -> str:
|
||
if not successful:
|
||
reason, next_step = CodexAssistantService._mode_profile_failure_feedback(
|
||
result
|
||
)
|
||
return (
|
||
f"功能模式未生成。失败原因:{reason}。"
|
||
f"下一步:{next_step}。"
|
||
)
|
||
draft = result.get("mode_profile_draft")
|
||
name = (
|
||
str(draft.get("name") or "新功能模式")
|
||
if isinstance(draft, Mapping)
|
||
else "新功能模式"
|
||
)
|
||
if isinstance(draft, Mapping) and isinstance(draft.get("schedule"), dict):
|
||
schedule = ModeSchedule.from_dict(draft["schedule"])
|
||
return (f"已为你生成“{name}”的分阶段时间轴:{len(schedule.phases)}个阶段,"
|
||
f"每轮{schedule.operation_count}项动作,重复{schedule.repeat_count}次,"
|
||
f"总时长{schedule.total_duration_ms // 1000}秒。"
|
||
"每个阶段的设备目标和持续时间见卡片。当前未保存或执行;确认后保存,进入模式才开始计时。"
|
||
"执行中可输入“停止动态模式”取消后续阶段,已下发目标不会自动回滚。")
|
||
if (
|
||
isinstance(draft, Mapping)
|
||
and draft.get("generation_source") == "draft_revision"
|
||
):
|
||
raw_operation_count = draft.get("operation_count")
|
||
operation_count = (
|
||
raw_operation_count
|
||
if isinstance(raw_operation_count, int)
|
||
and not isinstance(raw_operation_count, bool)
|
||
and raw_operation_count > 0
|
||
else 0
|
||
)
|
||
persistence_hint = (
|
||
"确认后可更新现有模式。"
|
||
if draft.get("existing_mode_id")
|
||
else "确认后可保存为功能模式。"
|
||
)
|
||
requested_operation_count = draft.get("requested_operation_count")
|
||
changed_operation_count = draft.get("changed_operation_count")
|
||
no_effective_change = (
|
||
isinstance(requested_operation_count, int)
|
||
and not isinstance(requested_operation_count, bool)
|
||
and requested_operation_count > 0
|
||
and isinstance(changed_operation_count, int)
|
||
and not isinstance(changed_operation_count, bool)
|
||
and changed_operation_count == 0
|
||
)
|
||
if no_effective_change:
|
||
return (
|
||
f"待保存的“{name}”模式中,该项原本已是目标状态,"
|
||
f"已保持不变并重新校验全部{operation_count}项组合动作。"
|
||
f"原草稿未被保存或执行,可以继续调整;{persistence_hint}"
|
||
)
|
||
return (
|
||
f"已调整待保存的“{name}”模式,并重新校验全部"
|
||
f"{operation_count}项组合动作。原草稿未被保存或执行,"
|
||
f"可以继续调整;{persistence_hint}"
|
||
)
|
||
if isinstance(draft, Mapping) and draft.get("existing_mode_id"):
|
||
raw_operation_count = draft.get("operation_count")
|
||
operation_count = (
|
||
raw_operation_count
|
||
if isinstance(raw_operation_count, int)
|
||
and not isinstance(raw_operation_count, bool)
|
||
and raw_operation_count >= 0
|
||
else 0
|
||
)
|
||
if draft.get("generation_source") == "registered_mode_profile":
|
||
return (
|
||
f"工程中已有“{name}”,已按当前车型载入预览并验证"
|
||
f"{operation_count}项设备动作。创建阶段未下发,确认后可更新现有模式。"
|
||
)
|
||
return (
|
||
f"已按你提供的设置为现有“{name}”生成预览并验证"
|
||
f"{operation_count}项设备动作。创建阶段未下发,确认后可更新现有模式。"
|
||
)
|
||
if (
|
||
isinstance(draft, Mapping)
|
||
and draft.get("generation_source") == "scene_template_copy"
|
||
):
|
||
raw_operation_count = draft.get("operation_count")
|
||
operation_count = (
|
||
raw_operation_count
|
||
if isinstance(raw_operation_count, int)
|
||
and not isinstance(raw_operation_count, bool)
|
||
and raw_operation_count > 0
|
||
else 0
|
||
)
|
||
return (
|
||
f"已从常用模式模板快速生成“{name}”,并按当前工程能力验证了"
|
||
f"{operation_count}项设备动作。当前只是独立草稿,可以继续调整;"
|
||
"创建阶段不会下发,确认预览后可保存为新的功能模式。"
|
||
)
|
||
if (
|
||
isinstance(draft, Mapping)
|
||
and draft.get("generation_source") == "codex_scene_composition"
|
||
):
|
||
raw_operation_count = draft.get("operation_count")
|
||
operation_count = (
|
||
raw_operation_count
|
||
if isinstance(raw_operation_count, int)
|
||
and not isinstance(raw_operation_count, bool)
|
||
and raw_operation_count > 0
|
||
else 0
|
||
)
|
||
return (
|
||
f"已理解“{name}”的场景意图,并从当前工程能力中组合、验证了"
|
||
f"{operation_count}项设备动作。创建阶段不会下发,确认预览后可保存。"
|
||
)
|
||
return f"已为你生成“{name}”预览。创建阶段不会下发,确认动作后可保存。"
|
||
|
||
@staticmethod
|
||
def _mode_profile_management_answer(
|
||
result: Mapping[str, object],
|
||
successful: bool,
|
||
route: str,
|
||
) -> str:
|
||
"""Render saved-mode mutations from service evidence, not model claims."""
|
||
|
||
deleting = route == "mode_profile_delete"
|
||
evidence = result.get(
|
||
"deleted_mode_profile" if deleting else "mode_profile"
|
||
)
|
||
if successful and isinstance(evidence, Mapping):
|
||
name = str(evidence.get("name") or "该模式")
|
||
if deleting:
|
||
return (
|
||
f"已删除自定义模式“{name}”。本次只修改模式库,"
|
||
"没有向车辆下发控制动作。"
|
||
)
|
||
operation_count = evidence.get("operation_count")
|
||
count_text = (
|
||
f",当前共包含{operation_count}项设备动作"
|
||
if isinstance(operation_count, int)
|
||
and not isinstance(operation_count, bool)
|
||
else ""
|
||
)
|
||
change_text = CodexAssistantService._mode_profile_change_summary(
|
||
result.get("mode_profile_changes")
|
||
)
|
||
return (
|
||
f"已更新并保存“{name}”{count_text}。"
|
||
f"{change_text}"
|
||
"本次只修改模式配置,"
|
||
"没有向车辆下发控制动作。"
|
||
)
|
||
|
||
issue_code = ""
|
||
issue_message = ""
|
||
validation = result.get("validation")
|
||
if isinstance(validation, Sequence) and not isinstance(
|
||
validation,
|
||
(str, bytes, bytearray),
|
||
):
|
||
issue = next(
|
||
(item for item in validation if isinstance(item, Mapping)),
|
||
None,
|
||
)
|
||
if isinstance(issue, Mapping):
|
||
issue_code = str(issue.get("code") or "")
|
||
issue_message = re.sub(
|
||
r"\s+",
|
||
" ",
|
||
str(issue.get("message") or ""),
|
||
).strip()[:300]
|
||
if deleting and issue_code == "MODE_PROFILE_DELETE_PROTECTED":
|
||
return issue_message or "工程预设模式受保护,不能删除。"
|
||
if issue_code == "MODE_PROFILE_NOT_FOUND":
|
||
return issue_message or "未找到指定的功能模式,未修改模式库。"
|
||
action = "删除" if deleting else "更新"
|
||
if issue_message:
|
||
return f"功能模式{action}未完成:{issue_message}"
|
||
return f"功能模式{action}未完成,模式库没有发生变化。"
|
||
|
||
@staticmethod
|
||
def _mode_profile_change_summary(value: object) -> str:
|
||
if not isinstance(value, Sequence) or isinstance(
|
||
value,
|
||
(str, bytes, bytearray),
|
||
):
|
||
return ""
|
||
target_labels = {
|
||
"driver": "主驾",
|
||
"passenger": "副驾",
|
||
"rear": "后排",
|
||
"vehicle": "全车",
|
||
"all": "全部区域",
|
||
}
|
||
summaries: list[str] = []
|
||
for item in list(value)[:4]:
|
||
if not isinstance(item, Mapping):
|
||
continue
|
||
capability = str(item.get("capability") or "")
|
||
target = target_labels.get(str(item.get("target_scope") or ""), "")
|
||
parameters = item.get("parameters")
|
||
if not isinstance(parameters, Mapping):
|
||
continue
|
||
if capability == "climate.set_temperature" and isinstance(
|
||
parameters.get("temperature_c"),
|
||
(int, float),
|
||
):
|
||
summaries.append(
|
||
f"{target}空调温度调整为{parameters['temperature_c']}℃"
|
||
)
|
||
elif capability == "climate.set_fan_level" and isinstance(
|
||
parameters.get("level"),
|
||
int,
|
||
):
|
||
summaries.append(f"{target}空调风量调整为{parameters['level']}档")
|
||
elif capability == "roof.set_shade_position" and isinstance(
|
||
parameters.get("position_pct"),
|
||
(int, float),
|
||
):
|
||
summaries.append(
|
||
f"遮阳帘位置调整为{parameters['position_pct']}%"
|
||
)
|
||
return "已应用:" + ";".join(summaries) + "。" if summaries else ""
|
||
|
||
@staticmethod
|
||
def _fast_comfort_answer(
|
||
reason: str,
|
||
result: Mapping[str, object],
|
||
successful: bool,
|
||
) -> str:
|
||
if result.get("orchestration_route") == "mode_schedule_stop":
|
||
if result.get("execution_mode") == "hardware" and (result.get("schedule_execution") or {}).get("status") != "cancelled":
|
||
return "当前没有正在执行的动态时间轴。"
|
||
return ("已停止动态模式后续阶段。已下发的设备目标保持,不自动回滚。"
|
||
if result.get("execution_mode") == "hardware"
|
||
else "已模拟停止动态模式,未改变正在运行的车辆任务。")
|
||
if result.get("orchestration_route") == "mode_schedule_execute":
|
||
if not successful:
|
||
return "时间轴未能完成,后续阶段已停止。请检查车辆状态后重试。"
|
||
return ("动态模式时间轴已启动,将按卡片中的时长逐阶段调节。尚未完成所有阶段;输入“停止动态模式”可取消后续阶段。"
|
||
if result.get("status") == "running"
|
||
else "已按虚拟时钟模拟验证动态模式的全部阶段,未向实际车辆下发操作。")
|
||
if reason in {
|
||
"exact_quick_control",
|
||
"explicit_registered_control",
|
||
"explicit_saved_mode_execute",
|
||
}:
|
||
instruction = re.sub(
|
||
r"\s+",
|
||
" ",
|
||
str(
|
||
result.get("utterance")
|
||
or (
|
||
"功能模式"
|
||
if reason == "explicit_saved_mode_execute"
|
||
else "控制指令"
|
||
)
|
||
),
|
||
).strip()[:160]
|
||
if not successful:
|
||
return f"未能完成“{instruction}”,请检查车辆状态后重试。"
|
||
execution_mode = str(result.get("execution_mode") or "dry_run")
|
||
if execution_mode == "dry_run":
|
||
return f"已完成模拟编排:{instruction}。"
|
||
if execution_mode == "simulated_hardware":
|
||
return f"已在模拟硬件环境完成:{instruction}。"
|
||
physically_confirmed = (
|
||
result.get("physical_feedback_confirmed") is True
|
||
or _vehicle_verification_status(result) == "verified_success"
|
||
)
|
||
if physically_confirmed:
|
||
return f"已完成:{instruction}。"
|
||
return f"已提交:{instruction},正在等待车辆状态确认。"
|
||
cooling = reason == "implicit_hot_comfort"
|
||
task = "降温调节" if cooling else "升温调节"
|
||
temperature = 23 if cooling else 26
|
||
if not successful:
|
||
return f"{task}未完成,请检查车辆状态后重试。"
|
||
detail = f"空调已开启,全车温度设置为{temperature}℃,风量设置为2档。"
|
||
execution_mode = str(result.get("execution_mode") or "dry_run")
|
||
if execution_mode == "dry_run":
|
||
return f"已完成模拟编排:{detail}"
|
||
if execution_mode == "simulated_hardware":
|
||
return f"已在模拟硬件环境完成{task}:{detail}"
|
||
physically_confirmed = (
|
||
result.get("physical_feedback_confirmed") is True
|
||
or _vehicle_verification_status(result) == "verified_success"
|
||
)
|
||
if physically_confirmed:
|
||
return f"已完成{task}:{detail}"
|
||
return f"已提交{task},等待车辆状态确认。"
|
||
|
||
def _stream_locked(
|
||
self,
|
||
conversation: _Conversation,
|
||
message: str,
|
||
*,
|
||
message_id: str,
|
||
emit: EmitEvent,
|
||
execute_control: ControlExecutor,
|
||
capability_snapshot: Mapping[str, object],
|
||
capability_context: Mapping[str, object],
|
||
control_terms: Sequence[str],
|
||
query_capabilities: CapabilityQuery,
|
||
) -> dict[str, object]:
|
||
request_token = secrets.token_urlsafe(24)
|
||
tool_context = _VehicleTurnContext(
|
||
message,
|
||
execute_control,
|
||
_authorize_vehicle_request(message, control_terms),
|
||
capability_snapshot,
|
||
query_capabilities,
|
||
)
|
||
vehicle_control_expected = tool_context.authorization.instruction is not None
|
||
mode_profile_expected = (
|
||
tool_context.authorization.reason == "explicit_mode_profile_create"
|
||
)
|
||
with self._tool_contexts_lock:
|
||
self._tool_contexts[request_token] = tool_context
|
||
|
||
active: ActiveCodexTurn | None = None
|
||
item_phases: dict[str, str] = {}
|
||
final_answer = ""
|
||
streamed_answer = ""
|
||
answer_redactor = _StreamingSecretRedactor(request_token)
|
||
cancelled = False
|
||
deadline = time.monotonic() + self._turn_timeout
|
||
application_context = dict(capability_context)
|
||
if not isinstance(
|
||
application_context.get("natural_language_routing"),
|
||
Mapping,
|
||
):
|
||
application_context["natural_language_routing"] = {
|
||
"original_utterance": message,
|
||
"parsed_clauses": [],
|
||
"resolved_clauses": [],
|
||
"unresolved_clauses": [message],
|
||
"cloud_input_utterance": message,
|
||
}
|
||
try:
|
||
active = self.client.start_turn(
|
||
conversation.thread_id,
|
||
message,
|
||
request_token,
|
||
application_context=application_context,
|
||
)
|
||
terminal: dict[str, object] | None = None
|
||
while terminal is None:
|
||
if time.monotonic() >= deadline:
|
||
self.client.interrupt(active)
|
||
raise CodexAssistantError("Codex turn timed out")
|
||
|
||
while True:
|
||
try:
|
||
tool_event = tool_context.events.get_nowait()
|
||
except queue.Empty:
|
||
break
|
||
if emit(tool_event) is False:
|
||
cancelled = True
|
||
self.client.interrupt(active)
|
||
break
|
||
if cancelled:
|
||
break
|
||
|
||
try:
|
||
event = active.events.get(timeout=0.2)
|
||
except queue.Empty:
|
||
continue
|
||
method = str(event.get("method") or "")
|
||
params = event.get("params")
|
||
params = params if isinstance(params, dict) else {}
|
||
|
||
if method == "transport/error":
|
||
raise CodexAssistantError(str(params.get("message") or "Codex disconnected"))
|
||
if method == "item/started":
|
||
item = params.get("item")
|
||
if isinstance(item, dict):
|
||
item_id = str(item.get("id") or "")
|
||
item_phases[item_id] = str(item.get("phase") or "")
|
||
ui_event = self._item_started_event(item, message_id)
|
||
if ui_event and emit(ui_event) is False:
|
||
cancelled = True
|
||
elif method == "item/agentMessage/delta":
|
||
delta = str(params.get("delta") or "")
|
||
item_id = str(params.get("itemId") or "")
|
||
phase = item_phases.get(item_id, "")
|
||
if delta and phase == "commentary":
|
||
if (
|
||
emit(
|
||
{
|
||
"type": "work.progress",
|
||
"message_id": message_id,
|
||
"display_text": "小迪正在处理……",
|
||
}
|
||
)
|
||
is False
|
||
):
|
||
cancelled = True
|
||
elif delta:
|
||
safe_delta = answer_redactor.feed(delta)
|
||
if safe_delta:
|
||
streamed_answer += safe_delta
|
||
if (
|
||
not vehicle_control_expected
|
||
and emit(
|
||
{
|
||
"type": "answer.delta",
|
||
"message_id": message_id,
|
||
"delta": safe_delta,
|
||
}
|
||
)
|
||
is False
|
||
):
|
||
cancelled = True
|
||
elif method == "item/mcpToolCall/progress":
|
||
if (
|
||
emit(
|
||
{
|
||
"type": "action.progress",
|
||
"message_id": message_id,
|
||
# App Server progress text is model/tool
|
||
# controlled and may split secrets across
|
||
# multiple notifications. Keep this UI event
|
||
# fixed instead of attempting per-event
|
||
# redaction.
|
||
"display_text": "正在执行受控座舱操作……",
|
||
}
|
||
)
|
||
is False
|
||
):
|
||
cancelled = True
|
||
elif method == "item/completed":
|
||
item = params.get("item")
|
||
if isinstance(item, dict):
|
||
if item.get("type") == "agentMessage":
|
||
phase = str(
|
||
item.get("phase") or item_phases.get(str(item.get("id") or ""), "")
|
||
)
|
||
if phase != "commentary":
|
||
final_answer = str(item.get("text") or final_answer).replace(
|
||
request_token, "[内部令牌已隐藏]"
|
||
)
|
||
ui_event = self._item_completed_event(item, message_id)
|
||
if ui_event and emit(ui_event) is False:
|
||
cancelled = True
|
||
elif method == "error":
|
||
if bool(params.get("willRetry")):
|
||
if (
|
||
emit(
|
||
{
|
||
"type": "work.progress",
|
||
"message_id": message_id,
|
||
"display_text": "小迪正在重新连接……",
|
||
}
|
||
)
|
||
is False
|
||
):
|
||
cancelled = True
|
||
else:
|
||
detail = params.get("error")
|
||
if isinstance(detail, dict):
|
||
detail = detail.get("message") or detail.get("codexErrorInfo")
|
||
if not detail:
|
||
detail = params.get("message")
|
||
raise CodexAssistantError(str(detail or "Codex turn failed"))
|
||
elif method == "turn/completed":
|
||
terminal = params
|
||
|
||
if cancelled:
|
||
self.client.interrupt(active)
|
||
break
|
||
|
||
trailing_answer = answer_redactor.finish()
|
||
if trailing_answer:
|
||
streamed_answer += trailing_answer
|
||
if (
|
||
not cancelled
|
||
and not vehicle_control_expected
|
||
and emit(
|
||
{
|
||
"type": "answer.delta",
|
||
"message_id": message_id,
|
||
"delta": trailing_answer,
|
||
}
|
||
)
|
||
is False
|
||
):
|
||
cancelled = True
|
||
self.client.interrupt(active)
|
||
if cancelled:
|
||
return {"cancelled": True, "answer": final_answer or streamed_answer}
|
||
|
||
turn = terminal.get("turn") if terminal else None
|
||
turn = turn if isinstance(turn, dict) else {}
|
||
status = str(turn.get("status") or "failed")
|
||
if status != "completed":
|
||
detail = turn.get("error")
|
||
if isinstance(detail, dict):
|
||
detail = detail.get("message") or detail.get("codexErrorInfo")
|
||
raise CodexAssistantError(str(detail or f"Codex turn {status}"))
|
||
if not final_answer:
|
||
final_answer = streamed_answer.strip()
|
||
if vehicle_control_expected:
|
||
if not tool_context.called:
|
||
tool_context.action_succeeded = False
|
||
final_answer = (
|
||
"车辆控制未完成:AI 助手没有调用受控执行链,未向车辆下发任何操作。"
|
||
)
|
||
emit(
|
||
{
|
||
"type": "action.completed",
|
||
"message_id": message_id,
|
||
"success": False,
|
||
"display_text": "车辆控制未执行",
|
||
}
|
||
)
|
||
elif tool_context.action_succeeded is not True:
|
||
final_answer = (
|
||
self._mode_profile_preview_answer(
|
||
tool_context.action_result or {},
|
||
False,
|
||
)
|
||
if mode_profile_expected
|
||
else "车辆控制未完成,受控执行链未返回成功结果。"
|
||
)
|
||
elif mode_profile_expected:
|
||
final_answer = self._mode_profile_preview_answer(
|
||
tool_context.action_result or {},
|
||
True,
|
||
)
|
||
elif tool_context.action_state == "submitted":
|
||
final_answer = "控制指令已提交,正在等待车辆状态确认。"
|
||
elif not final_answer and tool_context.action_state == "simulated":
|
||
final_answer = "车辆控制模拟编排已完成。"
|
||
elif not final_answer and tool_context.action_state == "verified":
|
||
final_answer = "车辆控制已完成并通过状态确认。"
|
||
if (
|
||
emit(
|
||
{
|
||
"type": "answer.delta",
|
||
"message_id": message_id,
|
||
"delta": final_answer,
|
||
}
|
||
)
|
||
is False
|
||
):
|
||
return {"cancelled": True, "answer": final_answer}
|
||
if not final_answer:
|
||
raise CodexAssistantError("Codex produced an empty response")
|
||
work_succeeded = (
|
||
tool_context.called and tool_context.action_succeeded is True
|
||
if vehicle_control_expected
|
||
else tool_context.action_succeeded is not False
|
||
)
|
||
emit(
|
||
{
|
||
"type": "work.completed",
|
||
"message_id": message_id,
|
||
"success": work_succeeded,
|
||
"display_text": (
|
||
"功能模式预览已生成"
|
||
if mode_profile_expected and work_succeeded
|
||
else "功能模式预览生成失败"
|
||
if mode_profile_expected
|
||
else "车辆控制模拟编排已完成"
|
||
if vehicle_control_expected
|
||
and work_succeeded
|
||
and tool_context.action_state == "simulated"
|
||
else "控制指令已提交,等待车辆状态确认"
|
||
if vehicle_control_expected
|
||
and work_succeeded
|
||
and tool_context.action_state == "submitted"
|
||
else "车辆控制已完成并通过状态确认"
|
||
if vehicle_control_expected and work_succeeded
|
||
else "小迪已完成"
|
||
if work_succeeded
|
||
else "车辆控制未完成"
|
||
),
|
||
}
|
||
)
|
||
emit(
|
||
{
|
||
"type": "answer.completed",
|
||
"message_id": message_id,
|
||
"route": "agent",
|
||
"answer": final_answer,
|
||
}
|
||
)
|
||
conversation.touched_at = time.monotonic()
|
||
return {"route": "agent", "answer": final_answer}
|
||
finally:
|
||
if active is not None:
|
||
try:
|
||
self.client.finish_turn(active)
|
||
except RuntimeError as exc:
|
||
# The stream error has already been translated for the
|
||
# browser. Cleanup must not replace it with a second error.
|
||
LOGGER.debug("Codex turn cleanup failed", exc_info=exc)
|
||
with self._tool_contexts_lock:
|
||
self._tool_contexts.pop(request_token, None)
|
||
|
||
def execute_vehicle_control(
|
||
self,
|
||
request_token: str,
|
||
instruction: str,
|
||
*,
|
||
assistant_plan: Mapping[str, object] | None = None,
|
||
) -> dict[str, object]:
|
||
request_token = str(request_token).strip()
|
||
instruction = " ".join(str(instruction).strip().split())
|
||
if not request_token or not instruction:
|
||
raise ValueError("request_token and instruction are required")
|
||
if len(instruction) > 1000:
|
||
raise ValueError("instruction must not exceed 1000 characters")
|
||
with self._tool_contexts_lock:
|
||
context = self._tool_contexts.get(request_token)
|
||
if context is None:
|
||
raise CodexVehicleToolError("vehicle control context is missing or expired")
|
||
|
||
with context.lock:
|
||
if context.called:
|
||
raise CodexVehicleToolError("vehicle control was already requested in this turn")
|
||
context.called = True
|
||
|
||
authorization = context.authorization
|
||
if authorization.instruction is None or not hmac.compare_digest(
|
||
_normalize_instruction(instruction).encode("utf-8"),
|
||
_normalize_instruction(authorization.instruction).encode("utf-8"),
|
||
):
|
||
context.action_succeeded = False
|
||
context.events.put(
|
||
{
|
||
"type": "action.completed",
|
||
"success": False,
|
||
"display_text": "已阻止未获授权的车辆控制",
|
||
}
|
||
)
|
||
raise CodexVehicleToolError(
|
||
"vehicle control was not authorized by the original user request"
|
||
)
|
||
|
||
context.events.put(
|
||
{
|
||
"type": "action.started",
|
||
"display_text": "正在通过车辆安全链执行控制……",
|
||
}
|
||
)
|
||
|
||
def on_progress(payload: dict[str, object]) -> None:
|
||
validated_payload = self._validated_control_payload(
|
||
payload,
|
||
secret=request_token,
|
||
)
|
||
if validated_payload is not None:
|
||
context.events.put(
|
||
{
|
||
"type": "validated_control_commands",
|
||
"payload": validated_payload,
|
||
}
|
||
)
|
||
context.events.put(
|
||
{
|
||
"type": "action.progress",
|
||
"display_text": "控制指令已通过校验,正在执行……",
|
||
"payload": self._safe_vehicle_result(
|
||
payload,
|
||
secret=request_token,
|
||
),
|
||
}
|
||
)
|
||
|
||
try:
|
||
# Execute the server-owned instruction, never a model-expanded
|
||
# variant. This binds the capability token to the user's intent.
|
||
if assistant_plan is None:
|
||
result = context.execute_control(
|
||
authorization.instruction,
|
||
on_progress,
|
||
)
|
||
else:
|
||
result = context.execute_control(
|
||
authorization.instruction,
|
||
on_progress,
|
||
assistant_plan,
|
||
)
|
||
except Exception as exc:
|
||
context.action_succeeded = False
|
||
context.events.put(
|
||
{
|
||
"type": "action.completed",
|
||
"success": False,
|
||
"display_text": "车辆控制未完成",
|
||
}
|
||
)
|
||
raise CodexVehicleToolError(self._safe_control_error(exc)) from exc
|
||
|
||
safe_result = self._safe_vehicle_result(
|
||
result,
|
||
secret=request_token,
|
||
)
|
||
action_state, successful, completion_text = _vehicle_action_state(result)
|
||
if context.authorization.reason == "explicit_mode_profile_create":
|
||
successful = _mode_profile_result_succeeded(result)
|
||
action_state = "simulated" if successful else "failed"
|
||
completion_text = (
|
||
"功能模式预览已生成"
|
||
if successful
|
||
else "功能模式预览生成失败"
|
||
)
|
||
context.action_result = result
|
||
context.action_succeeded = successful
|
||
context.action_state = action_state
|
||
context.events.put(
|
||
{
|
||
"type": "action.completed",
|
||
"success": successful,
|
||
"state": action_state,
|
||
"display_text": completion_text,
|
||
"payload": safe_result,
|
||
}
|
||
)
|
||
return safe_result
|
||
|
||
def execute_cockpit_request(
|
||
self,
|
||
request_token: str,
|
||
assistant_plan: Mapping[str, object] | None = None,
|
||
) -> dict[str, object]:
|
||
"""Execute the bound request, optionally using a guarded semantic proposal."""
|
||
|
||
request_token = str(request_token).strip()
|
||
with self._tool_contexts_lock:
|
||
context = self._tool_contexts.get(request_token)
|
||
if context is None:
|
||
raise CodexVehicleToolError("vehicle control context is missing or expired")
|
||
instruction = context.authorization.instruction
|
||
if instruction is None:
|
||
raise CodexVehicleToolError(
|
||
"vehicle control was not authorized by the original user request"
|
||
)
|
||
normalized_plan: dict[str, object] | None = None
|
||
if assistant_plan is not None:
|
||
if not isinstance(assistant_plan, Mapping):
|
||
raise ValueError("assistant_plan must be an object")
|
||
try:
|
||
encoded_plan = json.dumps(
|
||
dict(assistant_plan),
|
||
ensure_ascii=False,
|
||
allow_nan=False,
|
||
separators=(",", ":"),
|
||
)
|
||
except (TypeError, ValueError) as exc:
|
||
raise ValueError("assistant_plan must contain JSON values") from exc
|
||
if len(encoded_plan.encode("utf-8")) > 24 * 1024:
|
||
raise ValueError("assistant_plan is too large")
|
||
normalized_plan = json.loads(encoded_plan.replace(request_token, "[内部令牌已隐藏]"))
|
||
# Free-form model prose is neither needed for execution nor safe
|
||
# to persist. Keep only structural intent and replace narrative
|
||
# fields before the plan reaches artifacts or progress payloads.
|
||
if "decision_summary" in normalized_plan:
|
||
normalized_plan["decision_summary"] = ["小迪生成的结构化计划已提交应用安全链复核。"]
|
||
if "ambiguities" in normalized_plan:
|
||
normalized_plan["ambiguities"] = (
|
||
[
|
||
{
|
||
"field": "assistant_plan",
|
||
"question": "小迪生成的候选计划包含未解决歧义。",
|
||
}
|
||
]
|
||
if isinstance(normalized_plan["ambiguities"], list)
|
||
and normalized_plan["ambiguities"]
|
||
else []
|
||
)
|
||
requests = normalized_plan.get("requests")
|
||
if isinstance(requests, list):
|
||
for request in requests:
|
||
if isinstance(request, dict) and "reason" in request:
|
||
request["reason"] = "原始用户请求的结构化候选动作。"
|
||
return self.execute_vehicle_control(
|
||
request_token,
|
||
instruction,
|
||
assistant_plan=normalized_plan,
|
||
)
|
||
|
||
def get_n7web_capabilities(
|
||
self,
|
||
request_token: str,
|
||
query: str = "",
|
||
) -> dict[str, object]:
|
||
"""Return the turn-bound authoritative catalog through the read-only MCP tool."""
|
||
|
||
request_token = str(request_token).strip()
|
||
query = " ".join(str(query).strip().split())
|
||
if not request_token:
|
||
raise ValueError("request_token is required")
|
||
if len(query) > 160:
|
||
raise ValueError("query must not exceed 160 characters")
|
||
with self._tool_contexts_lock:
|
||
context = self._tool_contexts.get(request_token)
|
||
if context is None:
|
||
raise CodexVehicleToolError("capability context is missing or expired")
|
||
|
||
with context.lock:
|
||
if context.capability_queries >= 4:
|
||
raise CodexVehicleToolError("capability query limit exceeded for this turn")
|
||
context.capability_queries += 1
|
||
try:
|
||
result = context.query_capabilities(context.capability_snapshot, query)
|
||
serialized = json.dumps(result, ensure_ascii=False, default=str)
|
||
except Exception as exc:
|
||
raise CodexVehicleToolError(
|
||
"capability catalog is temporarily unavailable"
|
||
) from exc
|
||
if len(serialized.encode("utf-8")) > 512 * 1024:
|
||
raise CodexVehicleToolError("capability catalog response is too large")
|
||
normalized = json.loads(serialized)
|
||
if not isinstance(normalized, dict):
|
||
raise CodexVehicleToolError("capability catalog returned an invalid result")
|
||
return {
|
||
"ok": True,
|
||
"source": "n7web_runtime_registry",
|
||
"catalog": normalized,
|
||
}
|
||
|
||
def authenticate_internal_request(self, candidate: str) -> bool:
|
||
return hmac.compare_digest(
|
||
str(candidate).encode("utf-8"),
|
||
self._internal_token.encode("utf-8"),
|
||
)
|
||
|
||
def _conversation(
|
||
self,
|
||
principal: str,
|
||
conversation_id: str,
|
||
) -> tuple[_Conversation, tuple[str, str]]:
|
||
key = (str(principal or "anonymous")[:128], conversation_id)
|
||
with self._sessions_lock:
|
||
existing = self._sessions.pop(key, None)
|
||
if existing is not None:
|
||
existing.in_use += 1
|
||
self._sessions[key] = existing
|
||
evicted = self._prune_sessions_locked(protected=key)
|
||
if existing is not None:
|
||
for conversation in evicted:
|
||
self._unsubscribe_thread(conversation.thread_id)
|
||
return existing, key
|
||
|
||
# App Server startup can involve model-provider I/O. Keep that latency
|
||
# outside the global session lock so unrelated conversations continue.
|
||
created = _Conversation(
|
||
self.client.start_thread(
|
||
developer_instructions=self._instructions,
|
||
ephemeral=True,
|
||
)
|
||
)
|
||
redundant: _Conversation | None = None
|
||
with self._sessions_lock:
|
||
existing = self._sessions.pop(key, None)
|
||
if existing is None:
|
||
existing = created
|
||
else:
|
||
# Another request won the same-conversation creation race. Both
|
||
# callers share its per-conversation lock; release our extra
|
||
# App Server subscription after leaving the global lock.
|
||
redundant = created
|
||
existing.in_use += 1
|
||
self._sessions[key] = existing
|
||
evicted = self._prune_sessions_locked(protected=key)
|
||
if redundant is not None:
|
||
self._unsubscribe_thread(redundant.thread_id)
|
||
for conversation in evicted:
|
||
self._unsubscribe_thread(conversation.thread_id)
|
||
return existing, key
|
||
|
||
def _release_conversation(
|
||
self,
|
||
key: tuple[str, str],
|
||
conversation: _Conversation,
|
||
) -> None:
|
||
with self._sessions_lock:
|
||
current = self._sessions.get(key)
|
||
if current is conversation:
|
||
conversation.in_use = max(0, conversation.in_use - 1)
|
||
conversation.touched_at = time.monotonic()
|
||
evicted = self._prune_sessions_locked()
|
||
for stale in evicted:
|
||
self._unsubscribe_thread(stale.thread_id)
|
||
|
||
def _prune_sessions_locked(
|
||
self,
|
||
*,
|
||
protected: tuple[str, str] | None = None,
|
||
) -> list[_Conversation]:
|
||
evicted: list[_Conversation] = []
|
||
while len(self._sessions) > self._max_sessions:
|
||
stale_key = next(
|
||
(
|
||
candidate_key
|
||
for candidate_key, candidate in self._sessions.items()
|
||
if candidate_key != protected and candidate.in_use == 0
|
||
),
|
||
None,
|
||
)
|
||
if stale_key is None:
|
||
break
|
||
evicted.append(self._sessions.pop(stale_key))
|
||
return evicted
|
||
|
||
def _unsubscribe_thread(self, thread_id: str) -> None:
|
||
unsubscribe = getattr(self.client, "unsubscribe_thread", None)
|
||
if not callable(unsubscribe):
|
||
return
|
||
try:
|
||
unsubscribe(thread_id)
|
||
except RuntimeError as exc:
|
||
LOGGER.debug("Codex thread unsubscribe failed", exc_info=exc)
|
||
|
||
@staticmethod
|
||
def _item_started_event(
|
||
item: dict[str, object],
|
||
message_id: str,
|
||
) -> dict[str, object] | None:
|
||
item_type = str(item.get("type") or "")
|
||
if item_type in {"agentMessage", "reasoning", "mcpToolCall"}:
|
||
return None
|
||
labels = {
|
||
"commandExecution": "正在执行受限工作步骤……",
|
||
"webSearch": "正在检索信息……",
|
||
"fileChange": "正在处理文件变更……",
|
||
}
|
||
return {
|
||
"type": "work.started",
|
||
"message_id": message_id,
|
||
"work_type": item_type,
|
||
"display_text": labels.get(item_type, "小迪正在处理……"),
|
||
}
|
||
|
||
@staticmethod
|
||
def _item_completed_event(
|
||
item: dict[str, object],
|
||
message_id: str,
|
||
) -> dict[str, object] | None:
|
||
item_type = str(item.get("type") or "")
|
||
if item_type in {"agentMessage", "reasoning", "mcpToolCall"}:
|
||
return None
|
||
if item_type not in {"commandExecution", "webSearch", "fileChange"}:
|
||
return None
|
||
status = str(item.get("status") or "completed")
|
||
success = status not in {"failed", "declined", "cancelled"}
|
||
return {
|
||
"type": "work.completed",
|
||
"message_id": message_id,
|
||
"work_type": item_type,
|
||
"success": success,
|
||
"display_text": "工作步骤已完成" if success else "工作步骤未完成",
|
||
}
|
||
|
||
@staticmethod
|
||
def _safe_result_value(
|
||
value: object,
|
||
*,
|
||
secret: str,
|
||
depth: int = 0,
|
||
) -> object:
|
||
"""Project bounded JSON data and remove tool-boundary secrets."""
|
||
|
||
if value is None or isinstance(value, (bool, int, float)):
|
||
return value
|
||
if isinstance(value, str):
|
||
text = value.replace(secret, "[内部令牌已隐藏]") if secret else value
|
||
return text[:1000]
|
||
if depth >= 2:
|
||
return None
|
||
if isinstance(value, Mapping):
|
||
safe: dict[str, object] = {}
|
||
for raw_key, raw_value in list(value.items())[:24]:
|
||
key = str(raw_key)[:80]
|
||
if re.search(
|
||
r"token|secret|password|authorization|approval|api.?key|"
|
||
r"credential|cookie|bearer|private.?key|access.?key|"
|
||
r"raw.?can|can.?frame|data.?hex|payload.?hex",
|
||
key,
|
||
re.IGNORECASE,
|
||
):
|
||
continue
|
||
safe[key] = CodexAssistantService._safe_result_value(
|
||
raw_value,
|
||
secret=secret,
|
||
depth=depth + 1,
|
||
)
|
||
return safe
|
||
if isinstance(value, Sequence) and not isinstance(
|
||
value,
|
||
(str, bytes, bytearray),
|
||
):
|
||
return [
|
||
CodexAssistantService._safe_result_value(
|
||
item,
|
||
secret=secret,
|
||
depth=depth + 1,
|
||
)
|
||
for item in list(value)[:24]
|
||
]
|
||
text = str(value)
|
||
if secret:
|
||
text = text.replace(secret, "[内部令牌已隐藏]")
|
||
return text[:240]
|
||
|
||
@staticmethod
|
||
def _validated_control_payload(
|
||
result: Mapping[str, object],
|
||
*,
|
||
secret: str = "",
|
||
) -> dict[str, object] | None:
|
||
"""Build a browser-only allowlist projection of compiled controls.
|
||
|
||
MCP tool results intentionally exclude command plans and raw CAN
|
||
material. The web console still needs a deterministic view after
|
||
validation, so copy only semantic fields, canonical debug commands,
|
||
reconstructed IR payloads, display-safe CAN bytes, and stage timings.
|
||
"""
|
||
|
||
raw_configurations = result.get("function_configs")
|
||
configuration_entries: list[tuple[str, Mapping[str, object]]] = []
|
||
if isinstance(raw_configurations, Mapping):
|
||
configuration_entries.extend(
|
||
(str(key), value)
|
||
for key, value in raw_configurations.items()
|
||
if isinstance(value, Mapping)
|
||
)
|
||
if not configuration_entries and isinstance(
|
||
result.get("function_config"), Mapping
|
||
):
|
||
configuration_entries.append(("default", result["function_config"]))
|
||
|
||
raw_plans = result.get("command_plans")
|
||
plan_entries: list[tuple[str, Mapping[str, object]]] = []
|
||
if isinstance(raw_plans, Mapping):
|
||
plan_entries.extend(
|
||
(str(key), value)
|
||
for key, value in raw_plans.items()
|
||
if isinstance(value, Mapping)
|
||
)
|
||
if not plan_entries and isinstance(result.get("command_plan"), Mapping):
|
||
plan_entries.append(("default", result["command_plan"]))
|
||
if not configuration_entries or not plan_entries:
|
||
return None
|
||
|
||
def safe_text(value: object, limit: int) -> str:
|
||
"""Return bounded display text without copying the request secret."""
|
||
|
||
text = str(value or "")
|
||
if secret and secret in text:
|
||
return ""
|
||
return text[:limit]
|
||
|
||
def command_refs(command: Mapping[str, object]) -> tuple[str, ...]:
|
||
raw_refs = command.get("operation_ref")
|
||
if isinstance(raw_refs, Sequence) and not isinstance(
|
||
raw_refs, (str, bytes, bytearray)
|
||
):
|
||
values = raw_refs
|
||
else:
|
||
values = str(raw_refs or "").split(",")
|
||
return tuple(str(value).strip() for value in values if str(value).strip())
|
||
|
||
def matching_commands(
|
||
entries: Sequence[tuple[str, Mapping[str, object]]],
|
||
operation_id: str,
|
||
) -> list[tuple[Mapping[str, object], Mapping[str, object]]]:
|
||
matches: list[tuple[Mapping[str, object], Mapping[str, object]]] = []
|
||
for _plan_key, plan in entries:
|
||
spec = plan.get("spec")
|
||
if not isinstance(spec, Mapping):
|
||
continue
|
||
commands = spec.get("commands")
|
||
if not isinstance(commands, Sequence) or isinstance(
|
||
commands, (str, bytes, bytearray)
|
||
):
|
||
continue
|
||
matches.extend(
|
||
(command, plan)
|
||
for command in commands
|
||
if isinstance(command, Mapping)
|
||
and operation_id in command_refs(command)
|
||
)
|
||
return matches
|
||
|
||
presentations: list[dict[str, object]] = []
|
||
for config_key, configuration in configuration_entries[:16]:
|
||
spec = configuration.get("spec")
|
||
if not isinstance(spec, Mapping):
|
||
continue
|
||
operations = spec.get("operations")
|
||
if not isinstance(operations, Sequence) or isinstance(
|
||
operations, (str, bytes, bytearray)
|
||
):
|
||
continue
|
||
raw_domain = str(spec.get("domain") or "")
|
||
domain = safe_text(raw_domain, 80)
|
||
preferred_plans = [
|
||
entry for entry in plan_entries if entry[0] == config_key
|
||
]
|
||
for operation in operations[:24]:
|
||
if not isinstance(operation, Mapping):
|
||
continue
|
||
raw_operation_id = str(operation.get("id") or "").strip()[:160]
|
||
operation_id = safe_text(raw_operation_id, 160).strip()
|
||
capability = safe_text(operation.get("capability"), 160).strip()
|
||
if not raw_operation_id or not operation_id or not capability:
|
||
continue
|
||
compiled = matching_commands(preferred_plans, raw_operation_id)
|
||
if (
|
||
not preferred_plans
|
||
and len(configuration_entries) == 1
|
||
and len(plan_entries) == 1
|
||
):
|
||
# Keep the legacy function_config/command_plan pair working,
|
||
# but never search globally across multi-config results. A
|
||
# repeated operation id must fail closed instead of borrowing
|
||
# a compiled command from another domain.
|
||
compiled = matching_commands(plan_entries, raw_operation_id)
|
||
if not compiled:
|
||
continue
|
||
|
||
channel = "IR" if raw_domain == "self_extension" else "CAN1"
|
||
actual_commands: list[str] = []
|
||
frames: list[dict[str, str]] = []
|
||
for command, plan in compiled[:16]:
|
||
signals = command.get("signals")
|
||
signals = signals if isinstance(signals, Mapping) else {}
|
||
plan_spec = plan.get("spec")
|
||
plan_domain = (
|
||
str(plan_spec.get("domain") or "")
|
||
if isinstance(plan_spec, Mapping)
|
||
else ""
|
||
)
|
||
message = str(command.get("message") or "")
|
||
bus = str(command.get("bus") or "").lower()
|
||
if message == "infrared.emit" or plan_domain == "self_extension":
|
||
command_channel = "IR"
|
||
elif "seat" in bus or "can0" in bus:
|
||
command_channel = "CAN0"
|
||
else:
|
||
command_channel = "CAN1"
|
||
channel = command_channel
|
||
|
||
debug_command = " ".join(
|
||
safe_text(signals.get("debug_command"), 256).split()
|
||
)
|
||
if re.fullmatch(r"[A-Za-z0-9_.:+% -]{1,256}", debug_command):
|
||
if debug_command not in actual_commands:
|
||
actual_commands.append(debug_command)
|
||
|
||
slot_value = signals.get("slot")
|
||
slot = (
|
||
int(slot_value)
|
||
if isinstance(slot_value, int)
|
||
and not isinstance(slot_value, bool)
|
||
and 1 <= slot_value <= 248
|
||
else None
|
||
)
|
||
if message == "infrared.emit" and slot is not None:
|
||
actual = f"infrared emit {slot}"
|
||
if actual not in actual_commands:
|
||
actual_commands.append(actual)
|
||
frame = {
|
||
"label": "GSCU1B TCP",
|
||
"value": json.dumps(
|
||
{
|
||
"action": "emit",
|
||
"data": {"no": slot},
|
||
"type": "infrared",
|
||
},
|
||
ensure_ascii=False,
|
||
separators=(",", ":"),
|
||
),
|
||
}
|
||
if frame not in frames:
|
||
frames.append(frame)
|
||
continue
|
||
|
||
frame_id = str(signals.get("frame_id") or "").strip()
|
||
data_bytes = " ".join(
|
||
str(signals.get("data_hex") or "").strip().split()
|
||
).upper()
|
||
if re.fullmatch(
|
||
r"0[xX][0-9A-Fa-f]{1,8}", frame_id
|
||
) and re.fullmatch(
|
||
r"[0-9A-F]{2}(?: [0-9A-F]{2}){0,63}", data_bytes
|
||
):
|
||
frame = {
|
||
"label": f"{command_channel} 帧",
|
||
"value": f"0x{frame_id[2:].upper()} · {data_bytes}",
|
||
}
|
||
if frame not in frames:
|
||
frames.append(frame)
|
||
|
||
safe_target = CodexAssistantService._safe_result_value(
|
||
operation.get("target") or {}, secret=secret
|
||
)
|
||
if raw_domain == "self_extension" and isinstance(safe_target, dict):
|
||
safe_target = {
|
||
key: safe_target[key]
|
||
for key in ("device", "name")
|
||
if key in safe_target
|
||
}
|
||
safe_parameters = CodexAssistantService._safe_result_value(
|
||
operation.get("parameters") or {}, secret=secret
|
||
)
|
||
presentations.append(
|
||
{
|
||
"operation": {
|
||
"id": operation_id,
|
||
"capability": capability,
|
||
"target": safe_target if isinstance(safe_target, dict) else {},
|
||
"parameters": (
|
||
safe_parameters
|
||
if isinstance(safe_parameters, dict)
|
||
else {}
|
||
),
|
||
**(
|
||
{
|
||
"parse_source": safe_text(
|
||
operation.get("parse_source"), 80
|
||
)
|
||
}
|
||
if safe_text(operation.get("parse_source"), 80)
|
||
else {}
|
||
),
|
||
},
|
||
"domain": domain,
|
||
"channel": channel,
|
||
"actual_command": ";".join(actual_commands),
|
||
"frames": frames,
|
||
}
|
||
)
|
||
|
||
if not presentations:
|
||
return None
|
||
|
||
allowed_trace_stages = {
|
||
"intent_parse",
|
||
"context_resolve",
|
||
"domain_route",
|
||
"function_generate",
|
||
"schema_validate",
|
||
"domain_validate",
|
||
"compile",
|
||
"nfc_peripheral_route",
|
||
"local_peripheral_plan",
|
||
"nfc_peripheral_sequence",
|
||
}
|
||
safe_trace: list[dict[str, object]] = []
|
||
raw_trace = result.get("trace_events")
|
||
if isinstance(raw_trace, Sequence) and not isinstance(
|
||
raw_trace, (str, bytes, bytearray)
|
||
):
|
||
for item in raw_trace[:32]:
|
||
if not isinstance(item, Mapping):
|
||
continue
|
||
stage = str(item.get("stage") or "")
|
||
duration = item.get("duration_ms")
|
||
if (
|
||
stage in allowed_trace_stages
|
||
and isinstance(duration, (int, float))
|
||
and not isinstance(duration, bool)
|
||
and duration >= 0
|
||
):
|
||
safe_trace.append({"stage": stage, "duration_ms": duration})
|
||
|
||
payload: dict[str, object] = {
|
||
"validated_control_commands": presentations,
|
||
"trace_events": safe_trace,
|
||
}
|
||
for key in (
|
||
"request_id",
|
||
"preview_request_id",
|
||
"supplement_of",
|
||
"utterance",
|
||
"provider_used",
|
||
"execution_mode",
|
||
"control_transport",
|
||
"local_control_budget_ms",
|
||
"local_control_elapsed_ms",
|
||
"local_control_within_budget",
|
||
"natural_language_routing",
|
||
"parallel_dispatch",
|
||
):
|
||
if result.get(key) is not None:
|
||
payload[key] = CodexAssistantService._safe_result_value(
|
||
result[key], secret=secret
|
||
)
|
||
return payload
|
||
|
||
@staticmethod
|
||
def _safe_vehicle_result(
|
||
result: dict[str, object],
|
||
*,
|
||
secret: str = "",
|
||
) -> dict[str, object]:
|
||
safe: dict[str, object] = {
|
||
"status": str(result.get("status") or "unknown"),
|
||
"execution_mode": str(result.get("execution_mode") or "dry_run"),
|
||
}
|
||
if isinstance(result.get("hardware_execution_allowed"), bool):
|
||
safe["hardware_execution_allowed"] = result[
|
||
"hardware_execution_allowed"
|
||
]
|
||
for key in ("request_id", "utterance", "decision_summary"):
|
||
if result.get(key) is not None:
|
||
safe[key] = CodexAssistantService._safe_result_value(
|
||
result[key],
|
||
secret=secret,
|
||
)
|
||
actions: list[dict[str, object]] = []
|
||
action_limit = (
|
||
MAX_MODE_SCENE_ACTIONS
|
||
if isinstance(result.get("mode_profile_draft"), Mapping)
|
||
or isinstance(result.get("mode_profile"), Mapping)
|
||
else 96 if result.get("schedule") else MAX_PLAN_REQUESTS
|
||
)
|
||
intent_ir = result.get("intent_ir")
|
||
if isinstance(intent_ir, dict):
|
||
spec = intent_ir.get("spec")
|
||
if isinstance(spec, dict) and isinstance(spec.get("actions"), list):
|
||
for item in spec["actions"][:action_limit]:
|
||
if not isinstance(item, dict):
|
||
continue
|
||
safe_action = {
|
||
key: CodexAssistantService._safe_result_value(
|
||
item[key],
|
||
secret=secret,
|
||
)
|
||
for key in ("domain", "capability", "target", "parameters")
|
||
if key in item
|
||
}
|
||
if (
|
||
item.get("domain") == "self_extension"
|
||
and isinstance(item.get("target"), Mapping)
|
||
):
|
||
safe_action["target"] = {
|
||
key: CodexAssistantService._safe_result_value(
|
||
item["target"][key],
|
||
secret=secret,
|
||
)
|
||
for key in ("device", "name")
|
||
if key in item["target"]
|
||
}
|
||
actions.append(safe_action)
|
||
safe["actions"] = actions
|
||
infrared_actions: list[dict[str, object]] = []
|
||
raw_infrared = result.get("infrared_actions")
|
||
if isinstance(raw_infrared, list):
|
||
for item in raw_infrared[:action_limit]:
|
||
if not isinstance(item, dict):
|
||
continue
|
||
infrared_actions.append(
|
||
{
|
||
key: CodexAssistantService._safe_result_value(
|
||
item[key],
|
||
secret=secret,
|
||
)
|
||
for key in (
|
||
"capability_id",
|
||
"device_id",
|
||
"device_name",
|
||
"action",
|
||
"slot",
|
||
"transport",
|
||
"parse_source",
|
||
)
|
||
if key in item
|
||
}
|
||
)
|
||
if infrared_actions:
|
||
safe["infrared_actions"] = infrared_actions
|
||
mode_profile = result.get("mode_profile")
|
||
if isinstance(mode_profile, dict):
|
||
safe["mode_profile"] = {
|
||
key: CodexAssistantService._safe_result_value(
|
||
mode_profile[key],
|
||
secret=secret,
|
||
)
|
||
for key in (
|
||
"id",
|
||
"name",
|
||
"description",
|
||
"domains",
|
||
"operation_count",
|
||
"origin",
|
||
)
|
||
if key in mode_profile
|
||
}
|
||
mode_profile_changes = result.get("mode_profile_changes")
|
||
if isinstance(mode_profile_changes, list):
|
||
safe["mode_profile_changes"] = [
|
||
{
|
||
key: CodexAssistantService._safe_result_value(
|
||
item[key],
|
||
secret=secret,
|
||
)
|
||
for key in ("domain", "capability", "target_scope", "parameters")
|
||
if key in item
|
||
}
|
||
for item in mode_profile_changes[:MAX_PLAN_REQUESTS]
|
||
if isinstance(item, Mapping)
|
||
]
|
||
deleted_mode_profile = result.get("deleted_mode_profile")
|
||
if isinstance(deleted_mode_profile, dict):
|
||
safe["deleted_mode_profile"] = {
|
||
key: CodexAssistantService._safe_result_value(
|
||
deleted_mode_profile[key],
|
||
secret=secret,
|
||
)
|
||
for key in (
|
||
"id",
|
||
"name",
|
||
"description",
|
||
"domains",
|
||
"operation_count",
|
||
"origin",
|
||
)
|
||
if key in deleted_mode_profile
|
||
}
|
||
mode_profile_draft = result.get("mode_profile_draft")
|
||
if isinstance(mode_profile_draft, dict):
|
||
safe["mode_profile_draft"] = {
|
||
key: CodexAssistantService._safe_result_value(
|
||
mode_profile_draft[key],
|
||
secret=secret,
|
||
)
|
||
for key in (
|
||
"name",
|
||
"instruction",
|
||
"source_request_id",
|
||
"vehicle_id",
|
||
"domains",
|
||
"operation_count",
|
||
"existing_mode_id",
|
||
"existing_mode_revision",
|
||
"source_template_id",
|
||
"source_template_revision",
|
||
"generation_source",
|
||
"draft_revision",
|
||
"revises_source_request_id",
|
||
"requested_operation_count",
|
||
"changed_operation_count",
|
||
)
|
||
if key in mode_profile_draft
|
||
}
|
||
for key in ("mode_profile_draft", "mode_profile"):
|
||
source = result.get(key)
|
||
if isinstance(source, dict) and isinstance(source.get("schedule"), dict):
|
||
try:
|
||
safe[key]["schedule"] = ModeSchedule.from_dict(source["schedule"]).to_dict()
|
||
except ValueError:
|
||
pass
|
||
if isinstance(result.get("schedule"), dict):
|
||
try:
|
||
safe["schedule"] = ModeSchedule.from_dict(result["schedule"]).to_dict()
|
||
except ValueError:
|
||
pass
|
||
if isinstance(result.get("schedule_execution"), dict):
|
||
safe["schedule_execution"] = public_schedule_status(result["schedule_execution"])
|
||
if isinstance(result.get("vehicle"), dict):
|
||
safe["vehicle"] = {"id": str(result["vehicle"].get("id") or "")[:128]}
|
||
failure = result.get("mode_creation_failure")
|
||
if isinstance(failure, Mapping):
|
||
reason, next_step = CodexAssistantService._mode_profile_failure_feedback(
|
||
result
|
||
)
|
||
raw_unresolved = failure.get("unresolved_clauses")
|
||
unresolved_items = (
|
||
list(raw_unresolved)[:3]
|
||
if isinstance(raw_unresolved, Sequence)
|
||
and not isinstance(raw_unresolved, (str, bytes, bytearray))
|
||
else []
|
||
)
|
||
safe["failure_feedback"] = {
|
||
"reason": reason,
|
||
"next_step": next_step,
|
||
"unresolved_clauses": [
|
||
CodexAssistantService._safe_result_value(
|
||
item,
|
||
secret=secret,
|
||
)
|
||
for item in unresolved_items
|
||
if isinstance(item, str)
|
||
and INTERNAL_SCENE_BUILDER_MARKER not in item
|
||
],
|
||
}
|
||
peripheral = result.get("peripheral")
|
||
if isinstance(peripheral, dict):
|
||
safe["peripheral"] = {
|
||
key: CodexAssistantService._safe_result_value(
|
||
peripheral[key],
|
||
secret=secret,
|
||
)
|
||
for key in ("id", "device_id", "device_name", "action", "slot")
|
||
if key in peripheral
|
||
}
|
||
if result.get("orchestration_route") is not None:
|
||
safe["orchestration_route"] = str(result["orchestration_route"])
|
||
if isinstance(result.get("parallel_dispatch"), Mapping):
|
||
safe["parallel_dispatch"] = CodexAssistantService._safe_result_value(
|
||
result["parallel_dispatch"],
|
||
secret=secret,
|
||
)
|
||
validation = result.get("validation")
|
||
if isinstance(validation, Sequence) and not isinstance(
|
||
validation,
|
||
(str, bytes, bytearray),
|
||
):
|
||
safe["validation"] = [
|
||
{
|
||
key: CodexAssistantService._safe_result_value(
|
||
item[key],
|
||
secret=secret,
|
||
)
|
||
for key in ("severity", "code", "message")
|
||
if key in item
|
||
}
|
||
for item in list(validation)[:8]
|
||
if isinstance(item, Mapping)
|
||
]
|
||
deferred = result.get("deferred_requests")
|
||
if isinstance(deferred, list):
|
||
safe["deferred_count"] = len(deferred)
|
||
verification_status = _vehicle_verification_status(result)
|
||
safe["verification_status"] = verification_status
|
||
safe["verified"] = verification_status == "verified_success"
|
||
return safe
|
||
|
||
@staticmethod
|
||
def _safe_control_error(error: Exception) -> str:
|
||
names = {
|
||
"HardwareAccessRequiredError": "vehicle hardware access is not available",
|
||
"HardwareBusyError": "the vehicle control channel is busy",
|
||
"HardwareSafetyError": "the request did not pass the vehicle safety gate",
|
||
"CanGatewayError": "the vehicle control channel is unavailable",
|
||
}
|
||
return names.get(type(error).__name__, "vehicle control failed safely")
|