fix: support conversational mode draft updates

This commit is contained in:
vee
2026-09-07 23:59:42 +08:00
parent 6b2b644ee6
commit f0a248ede3
19 changed files with 2326 additions and 109 deletions
+14
View File
@@ -634,6 +634,15 @@
background: #fff0f2; background: #fff0f2;
} }
.assistant-mode-draft-status.superseded {
color: #687386;
background: #f1f3f6;
}
.assistant-mode-draft.is-superseded {
opacity: 0.72;
}
.assistant-mode-draft.is-saved { .assistant-mode-draft.is-saved {
border-color: #bfe3d4; border-color: #bfe3d4;
} }
@@ -1191,6 +1200,11 @@
background: var(--status-error-soft); background: var(--status-error-soft);
} }
.assistant-mode-draft-status.superseded {
color: var(--assistant-muted);
background: var(--muted);
}
.assistant-mode-draft.is-saved { .assistant-mode-draft.is-saved {
border-color: color-mix(in oklch, var(--status-success) 36%, var(--border)); border-color: color-mix(in oklch, var(--status-success) 36%, var(--border));
} }
+139
View File
@@ -1,6 +1,7 @@
(function initializeUnifiedAssistant() { (function initializeUnifiedAssistant() {
const STORAGE_KEY = "cognitive-agent.assistant-messages"; const STORAGE_KEY = "cognitive-agent.assistant-messages";
const CONVERSATION_STORAGE_KEY = "cognitive-agent.conversation-id"; const CONVERSATION_STORAGE_KEY = "cognitive-agent.conversation-id";
const ACTIVE_MODE_DRAFT_STORAGE_KEY = "cognitive-agent.active-mode-draft";
const MAX_STORED_MESSAGES = 24; const MAX_STORED_MESSAGES = 24;
const COMPOSER_MIN_HEIGHT = 64; const COMPOSER_MIN_HEIGHT = 64;
const COMPOSER_MAX_HEIGHT = 116; const COMPOSER_MAX_HEIGHT = 116;
@@ -8,6 +9,7 @@
let pendingController = null; let pendingController = null;
let pendingMessage = null; let pendingMessage = null;
let latestControlResult = null; let latestControlResult = null;
let activeModeDraftContext = null;
let initialized = false; let initialized = false;
const $ = (selector, root = document) => root.querySelector(selector); const $ = (selector, root = document) => root.querySelector(selector);
@@ -106,6 +108,108 @@
return created; return created;
} }
function normalizeModeName(value) {
return String(value || "").trim().replace(/\s+/g, "").toLowerCase();
}
function sanitizeModeDraftContext(value) {
if (!value || typeof value !== "object") return null;
const name = String(value.name || "").trim();
const sourceRequestId = String(value.source_request_id || "").trim();
const vehicleId = String(value.vehicle_id || "").trim();
const draftConversationId = String(value.conversation_id || "").trim();
if (
!name
|| name.length > 64
|| !/^req-[a-z0-9-]{6,100}$/.test(sourceRequestId)
) return null;
if (!vehicleId || vehicleId.length > 80 || draftConversationId !== conversationId()) return null;
return {
name,
source_request_id: sourceRequestId,
vehicle_id: vehicleId,
conversation_id: draftConversationId,
};
}
function loadActiveModeDraftContext() {
try {
return sanitizeModeDraftContext(
JSON.parse(sessionStorage.getItem(ACTIVE_MODE_DRAFT_STORAGE_KEY) || "null"),
);
} catch (_error) {
return null;
}
}
function rememberModeDraftContext(draft) {
const candidate = sanitizeModeDraftContext({
name: draft?.name,
source_request_id: draft?.source_request_id,
vehicle_id: draft?.vehicle_id || $("#vehicleSelect")?.value || null,
conversation_id: conversationId(),
});
if (!candidate) return;
activeModeDraftContext = candidate;
sessionStorage.setItem(ACTIVE_MODE_DRAFT_STORAGE_KEY, JSON.stringify(candidate));
}
function clearActiveModeDraftContext(sourceRequestId = null) {
if (
sourceRequestId
&& activeModeDraftContext?.source_request_id !== String(sourceRequestId)
) return;
activeModeDraftContext = null;
sessionStorage.removeItem(ACTIVE_MODE_DRAFT_STORAGE_KEY);
}
function isModeDraftUpdateRequest(message, context) {
const compact = String(message || "").trim().replace(/\s+/g, "");
if (!compact || !context?.name) return false;
const politePrefix = "(?:(?:请|麻烦)(?:帮我|帮忙|给我)?|帮我|帮忙|给我)?";
const prefixUpdateVerb = "(?:修改一下|修改|调整|更改|更新|改一下|变更)";
const changePredicate = "(?:修改|改成|改为|更改|更新|变更|变为|换成|调整|调节|调高|调低|调到|调成|调为|调至|设置|设为|设成|设到|设至|打开|开启|启动|关闭|关掉|关上|全关|停止|停用|切换|进入|退出|移动|前移|后退|前进|升高|降低|抬高|放低|降下|升起|调直|放倒|复位|半开|开到|关到|拉开|拉上|上锁|解锁)";
const modeName = "([^,。!?!?、;;::]{1,64}?模式)";
const modePossessiveBoundary = "(?:中的|里的|内的|之中的|的|中|里|内)";
const modeMemberBoundary = `(?:${modePossessiveBoundary}|[,,::])`;
const question = /为什么|什么|怎么|如何|介绍|解释|作用|区别|原理|哪些|多少|几(?:度|档|级|个|项)?|是否|能否|可不可以|合适吗|合理吗|会怎样|列出|告诉我|[??]|吗[。.!!]?$/;
const candidates = [
{
pattern: new RegExp(`^${politePrefix}${prefixUpdateVerb}${modeName}${modeMemberBoundary}[,,::]?(.*)$`),
predicateRequired: false,
},
{
pattern: new RegExp(`^${politePrefix}(?:把|将)${modeName}${modeMemberBoundary}[,,::]?(.+)$`),
predicateRequired: true,
},
{
pattern: new RegExp(`^${politePrefix}${modeName}${modeMemberBoundary}[,,::]?(.+)$`),
predicateRequired: true,
},
];
return candidates.some(({ pattern, predicateRequired }) => {
const matched = compact.match(pattern);
const change = String(matched?.[2] || "");
return Boolean(
matched
&& normalizeModeName(matched[1]) === normalizeModeName(context.name)
&& !question.test(change)
&& (!predicateRequired || new RegExp(changePredicate).test(change))
);
});
}
function requestModeDraftContext(message) {
const context = activeModeDraftContext || loadActiveModeDraftContext();
if (!context || !isModeDraftUpdateRequest(message, context)) return null;
activeModeDraftContext = context;
return {
name: context.name,
source_request_id: context.source_request_id,
vehicle_id: context.vehicle_id,
};
}
function loadStoredMessages() { function loadStoredMessages() {
try { try {
const stored = JSON.parse(sessionStorage.getItem(STORAGE_KEY) || "[]"); const stored = JSON.parse(sessionStorage.getItem(STORAGE_KEY) || "[]");
@@ -475,6 +579,7 @@
status.className = "assistant-mode-draft-status success"; status.className = "assistant-mode-draft-status success";
status.textContent = existingModeId ? "模式已更新" : "模式已保存"; status.textContent = existingModeId ? "模式已更新" : "模式已保存";
card.querySelector(".assistant-mode-draft-actions")?.setAttribute("hidden", ""); card.querySelector(".assistant-mode-draft-actions")?.setAttribute("hidden", "");
clearActiveModeDraftContext(draft.source_request_id);
await window.CognitiveAgentConsole?.refreshModeProfiles?.(payload.mode_profile?.id); await window.CognitiveAgentConsole?.refreshModeProfiles?.(payload.mode_profile?.id);
appendModeCreatedState(message, payload.mode_profile, { updated: Boolean(existingModeId) }); appendModeCreatedState(message, payload.mode_profile, { updated: Boolean(existingModeId) });
persistMessages(); persistMessages();
@@ -506,13 +611,40 @@
} }
} }
function supersedeModeDraftCards(draft) {
const nextName = normalizeModeName(draft?.name);
const nextRequestId = String(draft?.source_request_id || "");
if (!nextName || !nextRequestId) return;
document.querySelectorAll("[data-assistant-mode-draft]").forEach((card) => {
if (
card.dataset.assistantModeName !== nextName
|| card.dataset.assistantModeDraft === nextRequestId
) return;
card.classList.remove("is-busy");
card.classList.add("is-superseded");
card.querySelectorAll("button").forEach((button) => {
button.disabled = true;
});
card.querySelector(".assistant-mode-draft-actions")?.setAttribute("hidden", "");
const priorStatus = card.querySelector(".assistant-mode-draft-status");
if (priorStatus) {
priorStatus.hidden = false;
priorStatus.className = "assistant-mode-draft-status superseded";
priorStatus.textContent = "已由下方最新草稿替换";
}
});
}
function appendModeDraftState(message, draft, actions) { function appendModeDraftState(message, draft, actions) {
if (!message?.body || !draft?.name || !draft?.source_request_id) return; if (!message?.body || !draft?.name || !draft?.source_request_id) return;
rememberModeDraftContext(draft);
supersedeModeDraftCards(draft);
const selector = `[data-assistant-mode-draft="${CSS.escape(String(draft.source_request_id))}"]`; const selector = `[data-assistant-mode-draft="${CSS.escape(String(draft.source_request_id))}"]`;
if ($(selector, message.body)) return; if ($(selector, message.body)) return;
const card = document.createElement("section"); const card = document.createElement("section");
card.className = "assistant-mode-draft"; card.className = "assistant-mode-draft";
card.dataset.assistantModeDraft = String(draft.source_request_id); card.dataset.assistantModeDraft = String(draft.source_request_id);
card.dataset.assistantModeName = normalizeModeName(draft.name);
message.article?.classList.add("assistant-message-mode-draft"); message.article?.classList.add("assistant-message-mode-draft");
const heading = document.createElement("header"); const heading = document.createElement("header");
@@ -613,6 +745,7 @@
const controlTransport = localStorage.getItem("cognitive-agent.control-transport") || "wired_can"; const controlTransport = localStorage.getItem("cognitive-agent.control-transport") || "wired_can";
const hardwareClientId = localStorage.getItem("cognitive-agent.hardware-client-id"); const hardwareClientId = localStorage.getItem("cognitive-agent.hardware-client-id");
const vehicleId = $("#vehicleSelect")?.value || null; const vehicleId = $("#vehicleSelect")?.value || null;
const modeProfileDraftContext = requestModeDraftContext(message);
return { return {
message, message,
conversation_id: conversationId(), conversation_id: conversationId(),
@@ -620,6 +753,9 @@
execution_mode: executionMode, execution_mode: executionMode,
control_transport: controlTransport, control_transport: controlTransport,
...(vehicleId ? { vehicle_id: vehicleId } : {}), ...(vehicleId ? { vehicle_id: vehicleId } : {}),
...(modeProfileDraftContext
? { mode_profile_draft_context: modeProfileDraftContext }
: {}),
...(executionMode === "hardware" && controlTransport !== "wireless_mqtt" && hardwareClientId ...(executionMode === "hardware" && controlTransport !== "wireless_mqtt" && hardwareClientId
? { hardware_client_id: hardwareClientId } ? { hardware_client_id: hardwareClientId }
: {}), : {}),
@@ -681,6 +817,7 @@
event.payload.mode_profile event.payload.mode_profile
&& event.payload.orchestration_route === "mode_profile_create" && event.payload.orchestration_route === "mode_profile_create"
) { ) {
clearActiveModeDraftContext();
appendModeCreatedState(message, event.payload.mode_profile); appendModeCreatedState(message, event.payload.mode_profile);
} }
} }
@@ -859,6 +996,7 @@
resizeComposerInput(input); resizeComposerInput(input);
}, 300); }, 300);
renderInitialMessages(); renderInitialMessages();
activeModeDraftContext = loadActiveModeDraftContext();
setComposerPending(false); setComposerPending(false);
} }
@@ -873,6 +1011,7 @@
const target = event.target instanceof Element ? event.target : null; const target = event.target instanceof Element ? event.target : null;
if (!target?.closest("#newConversation")) return; if (!target?.closest("#newConversation")) return;
sessionStorage.removeItem(STORAGE_KEY); sessionStorage.removeItem(STORAGE_KEY);
clearActiveModeDraftContext();
sessionStorage.setItem(CONVERSATION_STORAGE_KEY, createConversationId()); sessionStorage.setItem(CONVERSATION_STORAGE_KEY, createConversationId());
}, true); }, true);
+2 -2
View File
@@ -13,7 +13,7 @@
<link rel="stylesheet" href="/console-v2.css?v=delivery-state-65" /> <link rel="stylesheet" href="/console-v2.css?v=delivery-state-65" />
<link rel="stylesheet" href="/self-extension.css?v=gscu-tcp-43" /> <link rel="stylesheet" href="/self-extension.css?v=gscu-tcp-43" />
<link rel="stylesheet" href="/speech-input.css?v=sensevoice-7" /> <link rel="stylesheet" href="/speech-input.css?v=sensevoice-7" />
<link rel="stylesheet" href="/assistant-chat.css?v=codex-assistant-22" /> <link rel="stylesheet" href="/assistant-chat.css?v=codex-assistant-23" />
<link rel="stylesheet" href="/energy-monitor.css?v=energy-monitor-4" /> <link rel="stylesheet" href="/energy-monitor.css?v=energy-monitor-4" />
<script src="/vendor/lucide/0.468.0/lucide.min.js" defer></script> <script src="/vendor/lucide/0.468.0/lucide.min.js" defer></script>
<script src="/vendor/three/0.147.0/three.min.js" defer></script> <script src="/vendor/three/0.147.0/three.min.js" defer></script>
@@ -26,7 +26,7 @@
<script src="/pwa.js?v=android-pad-3" defer></script> <script src="/pwa.js?v=android-pad-3" defer></script>
<script src="/console-v2.js?v=delivery-state-62" defer></script> <script src="/console-v2.js?v=delivery-state-62" defer></script>
<script src="/self-extension.js?v=gscu-tcp-43" defer></script> <script src="/self-extension.js?v=gscu-tcp-43" defer></script>
<script src="/assistant-chat.js?v=codex-assistant-22" defer></script> <script src="/assistant-chat.js?v=codex-assistant-23" defer></script>
<script src="/energy-monitor.js?v=energy-monitor-4" defer></script> <script src="/energy-monitor.js?v=energy-monitor-4" defer></script>
</head> </head>
<body class="console-v2"> <body class="console-v2">
+3 -3
View File
@@ -1,4 +1,4 @@
const CACHE_VERSION = "cognitive-agent-pad-v271"; const CACHE_VERSION = "cognitive-agent-pad-v272";
const SHELL_CACHE = `${CACHE_VERSION}-shell`; const SHELL_CACHE = `${CACHE_VERSION}-shell`;
const RUNTIME_CACHE = `${CACHE_VERSION}-runtime`; const RUNTIME_CACHE = `${CACHE_VERSION}-runtime`;
const APP_SHELL = [ const APP_SHELL = [
@@ -16,8 +16,8 @@ const APP_SHELL = [
"/speech-input.css?v=sensevoice-7", "/speech-input.css?v=sensevoice-7",
"/speech-input.js?v=sensevoice-12", "/speech-input.js?v=sensevoice-12",
"/speech-audio-worklet.js?v=sensevoice-5", "/speech-audio-worklet.js?v=sensevoice-5",
"/assistant-chat.css?v=codex-assistant-22", "/assistant-chat.css?v=codex-assistant-23",
"/assistant-chat.js?v=codex-assistant-22", "/assistant-chat.js?v=codex-assistant-23",
"/energy-monitor.css?v=energy-monitor-4", "/energy-monitor.css?v=energy-monitor-4",
"/energy-monitor.js?v=energy-monitor-4", "/energy-monitor.js?v=energy-monitor-4",
"/assets/pets/self-extension-cat.png?v=3", "/assets/pets/self-extension-cat.png?v=3",
+2 -2
View File
@@ -43,8 +43,8 @@ operations:
target_scope: passenger target_scope: passenger
operation: set operation: set
parameters: parameters:
temperature_c: 24.5 temperature_c: 25
reason: 办公座位专属恒温 24.5 度。 reason: 办公座位专属恒温 25 度,使用当前 CAN 协议支持的整数温度。
- domain: climate - domain: climate
capability: climate.set_fan_level capability: climate.set_fan_level
target_scope: vehicle target_scope: vehicle
@@ -44,8 +44,8 @@ operations:
target_scope: passenger target_scope: passenger
operation: set operation: set
parameters: parameters:
temperature_c: 23.5 temperature_c: 24
reason: 副驾恒温锁定 23.5 度。 reason: 副驾恒温锁定 24 度,使用当前 CAN 协议支持的整数温度。
- domain: climate - domain: climate
capability: climate.set_fan_level capability: climate.set_fan_level
target_scope: vehicle target_scope: vehicle
@@ -18,6 +18,12 @@ from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from typing import Protocol from typing import Protocol
from cognitive_agent.application.mode_profile_intent import (
is_mode_profile_query,
normalize_mode_profile_name,
parse_mode_profile_update,
)
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
INTERNAL_SCENE_BUILDER_MARKER = "根据该场景名称使用当前可控原子能力" INTERNAL_SCENE_BUILDER_MARKER = "根据该场景名称使用当前可控原子能力"
@@ -145,6 +151,7 @@ _MODE_PROFILE_CREATE_PATTERN = re.compile(
r"(?:配置)?(?:并(?:添加|加入)(?:到)?功能模式(?:栏)?)?" r"(?:配置)?(?:并(?:添加|加入)(?:到)?功能模式(?:栏)?)?"
r"(?:[,,::](?P<instruction>.{1,800}))?[。.!!]?$" r"(?:[,,::](?P<instruction>.{1,800}))?[。.!!]?$"
) )
_MODE_EDITOR_REQUEST_ID_PATTERN = re.compile(r"^req-[a-z0-9-]{6,100}$")
_HOT_DISCOMFORT_PATTERN = re.compile(r"热|闷热|好热|太晒") _HOT_DISCOMFORT_PATTERN = re.compile(r"热|闷热|好热|太晒")
_COLD_DISCOMFORT_PATTERN = re.compile(r"冷|好冷") _COLD_DISCOMFORT_PATTERN = re.compile(r"冷|好冷")
_IMPLICIT_HOT_REQUEST_PATTERN = re.compile( _IMPLICIT_HOT_REQUEST_PATTERN = re.compile(
@@ -185,6 +192,59 @@ def is_mode_profile_create_request(message: str) -> bool:
return _mode_profile_create_parts(message) is not None 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( def _catalog_exact_mode_match_count(
catalog: Mapping[str, object], catalog: Mapping[str, object],
requested_name: str, requested_name: str,
@@ -314,13 +374,32 @@ def _vehicle_action_state(result: Mapping[str, object]) -> tuple[str, bool, str]
def _authorize_vehicle_request( def _authorize_vehicle_request(
message: str, message: str,
control_terms: Sequence[str] = (), control_terms: Sequence[str] = (),
mode_profile_draft_context: Mapping[str, object] | None = None,
) -> _VehicleAuthorization: ) -> _VehicleAuthorization:
"""Create a server-owned capability envelope from the original user text.""" """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()) normalized = "".join(str(message).lower().split())
if _DENIAL_PATTERN.search(normalized): if _DENIAL_PATTERN.search(normalized):
return _VehicleAuthorization(None, "denied_by_user") return _VehicleAuthorization(None, "denied_by_user")
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")
implicit_request = re.sub(r"[,。!?!?、]", "", normalized) implicit_request = re.sub(r"[,。!?!?、]", "", normalized)
if not _KNOWLEDGE_PATTERN.search(normalized): if not _KNOWLEDGE_PATTERN.search(normalized):
if _IMPLICIT_HOT_REQUEST_PATTERN.fullmatch(implicit_request): if _IMPLICIT_HOT_REQUEST_PATTERN.fullmatch(implicit_request):
@@ -342,6 +421,12 @@ def _authorize_vehicle_request(
# user as a saveable draft. # user as a saveable draft.
return _VehicleAuthorization(message, "explicit_mode_profile_create") return _VehicleAuthorization(message, "explicit_mode_profile_create")
if mode_profile_draft_context is not None:
# A validated opaque reference means this sentence edits a pending
# server-generated preview. It is a configuration operation, never a
# request to execute the mentioned device against the live vehicle.
return _VehicleAuthorization(message, "explicit_mode_profile_draft_update")
normalized_terms = { normalized_terms = {
"".join(str(term).lower().split()) for term in control_terms if str(term).strip() "".join(str(term).lower().split()) for term in control_terms if str(term).strip()
} }
@@ -453,6 +538,7 @@ class CodexAssistantService:
control_terms: Sequence[str] = (), control_terms: Sequence[str] = (),
query_capabilities: CapabilityQuery | None = None, query_capabilities: CapabilityQuery | None = None,
authoritative_answer: str | None = None, authoritative_answer: str | None = None,
mode_profile_draft_context: Mapping[str, object] | None = None,
) -> dict[str, object]: ) -> dict[str, object]:
del history # Codex owns conversation history after the thread is created. del history # Codex owns conversation history after the thread is created.
message = " ".join(str(message).strip().split()) message = " ".join(str(message).strip().split())
@@ -472,11 +558,22 @@ class CodexAssistantService:
authoritative_answer = " ".join(str(authoritative_answer or "").strip().split()) authoritative_answer = " ".join(str(authoritative_answer or "").strip().split())
if len(authoritative_answer) > 16_000: if len(authoritative_answer) > 16_000:
raise ValueError("authoritative_answer must not exceed 16000 characters") raise ValueError("authoritative_answer must not exceed 16000 characters")
authorization = _authorize_vehicle_request(message, control_terms) draft_context = _validated_mode_profile_draft_context(
mode_profile_draft_context,
message=message,
)
authorization = _authorize_vehicle_request(
message,
control_terms,
draft_context,
)
fast_comfort_control = ( fast_comfort_control = (
self._fast_implicit_controls and authorization.reason in _FAST_COMFORT_REASONS self._fast_implicit_controls and authorization.reason in _FAST_COMFORT_REASONS
) )
mode_profile_preview = authorization.reason == "explicit_mode_profile_create" 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 mode_parts = _mode_profile_create_parts(message) if mode_profile_preview else None
exact_mode_matches = ( exact_mode_matches = (
_catalog_exact_mode_match_count(catalog, mode_parts[0]) _catalog_exact_mode_match_count(catalog, mode_parts[0])
@@ -493,11 +590,18 @@ class CodexAssistantService:
or exact_mode_matches != 0 or exact_mode_matches != 0
) )
) )
direct_mode_profile_update = (
authorization.reason == "explicit_mode_profile_draft_update"
)
# Explicit device settings and repository-backed scenes remain on the # Explicit device settings and repository-backed scenes remain on the
# zero-model-latency path. A genuinely new name-only scene needs Codex # zero-model-latency path. A genuinely new name-only scene needs Codex
# to compose a candidate plan from the live capability catalog; the # to compose a candidate plan from the live capability catalog; the
# vehicle service still limits it to dry-run and requires confirmation. # vehicle service still limits it to dry-run and requires confirmation.
direct_vehicle_control = fast_comfort_control or direct_mode_profile_preview direct_vehicle_control = (
fast_comfort_control
or direct_mode_profile_preview
or authorization.reason == "explicit_mode_profile_draft_update"
)
route = ( route = (
"capability_catalog" "capability_catalog"
if authoritative_answer if authoritative_answer
@@ -509,7 +613,7 @@ class CodexAssistantService:
"n7web-capability-registry" "n7web-capability-registry"
if authoritative_answer if authoritative_answer
else "n7web-mode-builder" else "n7web-mode-builder"
if direct_mode_profile_preview if direct_mode_profile_preview or direct_mode_profile_update
else "n7web-control-router" else "n7web-control-router"
if fast_comfort_control if fast_comfort_control
else "codex-app-server" else "codex-app-server"
@@ -538,7 +642,7 @@ class CodexAssistantService:
"正在读取工程能力目录……" "正在读取工程能力目录……"
if authoritative_answer if authoritative_answer
else "正在构建并校验功能模式……" else "正在构建并校验功能模式……"
if direct_mode_profile_preview if direct_mode_profile_preview or direct_mode_profile_update
else "Codex 正在理解场景并组合工程能力……" else "Codex 正在理解场景并组合工程能力……"
if mode_profile_preview if mode_profile_preview
else "正在快速处理座舱舒适需求……" else "正在快速处理座舱舒适需求……"
@@ -622,7 +726,13 @@ class CodexAssistantService:
instruction = authorization.instruction instruction = authorization.instruction
if instruction is None: if instruction is None:
raise CodexAssistantError("direct vehicle control was not authorized") raise CodexAssistantError("direct vehicle control was not authorized")
mode_profile_preview = authorization.reason == "explicit_mode_profile_create" 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"
)
if ( if (
emit( emit(
{ {
@@ -630,7 +740,9 @@ class CodexAssistantService:
"message_id": message_id, "message_id": message_id,
"display_text": ( "display_text": (
"正在生成模式动作预览……" "正在生成模式动作预览……"
if mode_profile_preview if mode_profile_preview and not mode_profile_draft_update
else "正在调整待保存模式动作……"
if mode_profile_draft_update
else "正在通过车辆安全链执行舒适性调节……" else "正在通过车辆安全链执行舒适性调节……"
), ),
} }
@@ -854,6 +966,28 @@ class CodexAssistantService:
if isinstance(draft, Mapping) if isinstance(draft, Mapping)
else "新功能模式" else "新功能模式"
) )
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 "确认后可保存为功能模式。"
)
return (
f"已调整待保存的“{name}”模式,并重新校验全部"
f"{operation_count}项组合动作。原草稿未被保存或执行,"
f"可以继续调整;{persistence_hint}"
)
if isinstance(draft, Mapping) and draft.get("existing_mode_id"): if isinstance(draft, Mapping) and draft.get("existing_mode_id"):
raw_operation_count = draft.get("operation_count") raw_operation_count = draft.get("operation_count")
operation_count = ( operation_count = (
@@ -1614,6 +1748,10 @@ class CodexAssistantService:
"status": str(result.get("status") or "unknown"), "status": str(result.get("status") or "unknown"),
"execution_mode": str(result.get("execution_mode") or "dry_run"), "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"): for key in ("request_id", "utterance", "decision_summary"):
if result.get(key) is not None: if result.get(key) is not None:
safe[key] = CodexAssistantService._safe_result_value( safe[key] = CodexAssistantService._safe_result_value(
@@ -1699,6 +1837,8 @@ class CodexAssistantService:
"existing_mode_id", "existing_mode_id",
"existing_mode_revision", "existing_mode_revision",
"generation_source", "generation_source",
"draft_revision",
"revises_source_request_id",
) )
if key in mode_profile_draft if key in mode_profile_draft
} }
@@ -0,0 +1,121 @@
"""Canonical, side-effect-free parsing for named-mode configuration turns."""
from __future__ import annotations
import re
_LEADING_POLITE = r"(?:(?:请|麻烦)(?:帮我|帮忙|给我)?|(?:帮我|帮忙|给我))?"
_MODE_NAME = r"(?P<name>[^,。,.!!::\s“”\"'「」『』]{1,64}?模式)"
_MODE_POSSESSIVE_BOUNDARY = r"(?:中的|里的|内的|之中的|的|中|里|内)"
_MODE_MEMBER_BOUNDARY = rf"(?:{_MODE_POSSESSIVE_BOUNDARY}|[,,::])"
_PREFIX_UPDATE = re.compile(
rf"^{_LEADING_POLITE}"
rf"(?:修改一下|修改|调整|更改|更新|改一下|变更){_MODE_NAME}"
rf"{_MODE_MEMBER_BOUNDARY}[,,::]?(?P<change>.*?)[。.!!]?$"
)
_BA_UPDATE = re.compile(
rf"^{_LEADING_POLITE}(?:把|将){_MODE_NAME}"
rf"{_MODE_MEMBER_BOUNDARY}[,,::]?"
r"(?P<change>.+?)[。.!!]?$"
)
_NAME_FIRST_UPDATE = re.compile(
rf"^{_LEADING_POLITE}{_MODE_NAME}"
rf"{_MODE_POSSESSIVE_BOUNDARY}[,,::]?"
r"(?P<change>.+?)[。.!!]?$"
)
_CONTEXTUAL_NAME_FIRST_UPDATE = re.compile(
rf"^{_LEADING_POLITE}{_MODE_NAME}[,,::]"
r"(?P<change>.+?)[。.!!]?$"
)
MODE_PROFILE_CHANGE_PREDICATE = re.compile(
r"修改|改成|改为|更改|更新|变更|变为|换成|"
r"调整|调节|调高|调低|调到|调成|调为|调至|"
r"设置|设为|设成|设到|设至|"
r"打开|开启|启动|关闭|关掉|关上|全关|停止|停用|切换|进入|退出|"
r"移动|前移|后退|前进|升高|降低|抬高|放低|降下|升起|调直|放倒|"
r"复位|半开|开到|关到|拉开|拉上|上锁|解锁"
)
MODE_PROFILE_QUERY_CUE = re.compile(
r"什么|哪些|多少|几(?:度|档|级|个|项)?|是否|是不是|"
r"合适(?:吗)?|合理(?:吗)?|会(?:怎么样|怎样|如何)|"
r"怎么|如何|列(?:出|一下)|介绍(?:一下)?|说明(?:一下)?|"
r"告诉我|查询|查看|有什么作用|是何作用|还是|[??]"
)
def compact_mode_profile_text(value: object) -> str:
"""Remove presentation whitespace without changing CJK command meaning."""
return re.sub(r"\s+", "", str(value or "").strip())
def normalize_mode_profile_name(value: object) -> str:
return compact_mode_profile_text(value).strip("“”\"'「」『』").casefold()
def _candidate_matches(
text: str,
expected_mode_name: object | None,
) -> list[tuple[re.Match[str], bool]]:
matches: list[tuple[re.Match[str], bool]] = []
for pattern, predicate_required in (
(_PREFIX_UPDATE, False),
(_BA_UPDATE, True),
(_NAME_FIRST_UPDATE, True),
):
match = pattern.fullmatch(text)
if match is not None:
matches.append((match, predicate_required))
if expected_mode_name is not None:
contextual = _CONTEXTUAL_NAME_FIRST_UPDATE.fullmatch(text)
if (
contextual is not None
and normalize_mode_profile_name(contextual.group("name"))
== normalize_mode_profile_name(expected_mode_name)
):
matches.append((contextual, True))
return matches
def is_mode_profile_query(
value: object,
*,
expected_mode_name: object | None = None,
) -> bool:
"""Return whether a named-mode-shaped sentence asks instead of edits."""
text = compact_mode_profile_text(value)
if not text:
return False
for match, _predicate_required in _candidate_matches(
text,
expected_mode_name,
):
change = str(match.group("change") or "").strip("的中里内,,::。.!!")
if MODE_PROFILE_QUERY_CUE.search(change):
return True
return False
def parse_mode_profile_update(
value: object,
*,
expected_mode_name: object | None = None,
) -> tuple[str, str] | None:
"""Parse one explicit update and reject nouns such as ``空调`` as verbs."""
text = compact_mode_profile_text(value)
if not text:
return None
for match, predicate_required in _candidate_matches(text, expected_mode_name):
change = str(match.group("change") or "").strip("的中里内,,::。.!!")
if (
(not predicate_required or MODE_PROFILE_CHANGE_PREDICATE.search(change))
and not is_mode_profile_query(
text,
expected_mode_name=expected_mode_name,
)
):
return str(match.group("name") or "").strip(), change
return None
+685 -75
View File
@@ -74,6 +74,11 @@ from cognitive_agent.application.semantic_ir import ( # noqa: E402
apply_language_safety_guards, apply_language_safety_guards,
resolve_explicit_seat_scopes, resolve_explicit_seat_scopes,
) )
from cognitive_agent.application.mode_profile_intent import ( # noqa: E402
is_mode_profile_query,
normalize_mode_profile_name,
parse_mode_profile_update,
)
from cognitive_agent.application.native_can_feedback import ( # noqa: E402 from cognitive_agent.application.native_can_feedback import ( # noqa: E402
VehicleSafetyPreflightError, VehicleSafetyPreflightError,
authoritative_physical_ch1_feedback_state, authoritative_physical_ch1_feedback_state,
@@ -82,6 +87,7 @@ from cognitive_agent.application.native_can_feedback import ( # noqa: E402
require_stationary_vehicle, require_stationary_vehicle,
) )
from cognitive_agent.application.native_can_runtime import ( # noqa: E402 from cognitive_agent.application.native_can_runtime import ( # noqa: E402
NativeCanCompileError,
NativeCanExecutor, NativeCanExecutor,
NativeCanRuntime, NativeCanRuntime,
UNVERIFIED_NATIVE_CAPABILITIES, UNVERIFIED_NATIVE_CAPABILITIES,
@@ -297,16 +303,6 @@ CONTROL_CLAUSE_PREDICATE_PATTERN = re.compile(
r"(?:\d+(?:\.\d+)?|[零一二三四五六七八九十百两半]+)" r"(?:\d+(?:\.\d+)?|[零一二三四五六七八九十百两半]+)"
r"(?:%|%|度|℃|档|级|秒|毫秒)" r"(?:%|%|度|℃|档|级|秒|毫秒)"
) )
UPDATE_MODE_PROFILE_PREFIX = re.compile(
r"^(?:请(?:帮我)?|帮我)?(?:修改一下|修改|调整|更改|更新|改一下)"
r"(?P<name>[^,。,.!!\s]{1,64}?模式)"
r"(?:[,。,.!!\s]*(?P<change>.*))?$"
)
UPDATE_MODE_PROFILE_BA = re.compile(
r"^(?:请(?:帮我)?|帮我)?(?:把|将)"
r"(?P<name>[^,。,.!!\s]{1,64}?模式)"
r"的(?P<change>.+)$"
)
RESTORE_MODE_PROFILE_PREFIX = re.compile( RESTORE_MODE_PROFILE_PREFIX = re.compile(
r"^(?:请(?:帮我)?|帮我)?(?:恢复|还原|重置|初始化)" r"^(?:请(?:帮我)?|帮我)?(?:恢复|还原|重置|初始化)"
r"(?P<name>[^,。,.!!\s]{1,64}?模式)" r"(?P<name>[^,。,.!!\s]{1,64}?模式)"
@@ -1515,6 +1511,7 @@ class SeatDemoService:
control_transport: str = WIRED_CAN_TRANSPORT, control_transport: str = WIRED_CAN_TRANSPORT,
on_commands_validated: Callable[[dict[str, Any]], None] | None = None, on_commands_validated: Callable[[dict[str, Any]], None] | None = None,
assistant_plan: dict[str, Any] | None = None, assistant_plan: dict[str, Any] | None = None,
mode_profile_draft_context: dict[str, Any] | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Validate and execute deterministic controls before hosted parsing.""" """Validate and execute deterministic controls before hosted parsing."""
@@ -1545,6 +1542,7 @@ class SeatDemoService:
control_transport=control_transport, control_transport=control_transport,
_on_commands_validated=on_commands_validated, _on_commands_validated=on_commands_validated,
_assistant_plan=assistant_plan, _assistant_plan=assistant_plan,
_mode_profile_draft_context=mode_profile_draft_context,
) )
result.update( result.update(
{ {
@@ -1602,6 +1600,7 @@ class SeatDemoService:
_allow_empty_control_result=True, _allow_empty_control_result=True,
_on_commands_validated=publish_local_commands, _on_commands_validated=publish_local_commands,
_assistant_plan=assistant_plan, _assistant_plan=assistant_plan,
_mode_profile_draft_context=mode_profile_draft_context,
) )
elapsed_ms = round((time.perf_counter() - started) * 1000, 3) elapsed_ms = round((time.perf_counter() - started) * 1000, 3)
supplement_required = bool( supplement_required = bool(
@@ -2048,13 +2047,37 @@ class SeatDemoService:
_direct_mode_profile_id: str | None = None, _direct_mode_profile_id: str | None = None,
_speech_understanding: dict[str, Any] | None = None, _speech_understanding: dict[str, Any] | None = None,
_assistant_plan: dict[str, Any] | None = None, _assistant_plan: dict[str, Any] | None = None,
_mode_profile_draft_context: dict[str, Any] | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
control_transport = self._validate_control_transport(control_transport) control_transport = self._validate_control_transport(control_transport)
# Creating a reusable scene is a configuration workflow. Synthesize # Creating a reusable scene is a configuration workflow. Synthesize
# and validate it in dry-run even when the console currently targets # and validate it in dry-run even when the console currently targets
# the real bench, otherwise virtual domains such as climate or ambient # the real bench, otherwise virtual domains such as climate or ambient
# light would be rejected before the profile can be persisted. # light would be rejected before the profile can be persisted.
if self._parse_mode_profile_create_name(utterance.strip()) is not None: draft_reference_name = (
str(_mode_profile_draft_context.get("name") or "").strip()
if isinstance(_mode_profile_draft_context, dict)
else ""
)
draft_update = parse_mode_profile_update(
utterance.strip(),
expected_mode_name=draft_reference_name or None,
)
mode_profile_query = is_mode_profile_query(
utterance,
expected_mode_name=draft_reference_name or None,
)
draft_update_requested = bool(
draft_update is not None
and draft_reference_name
and normalize_mode_profile_name(draft_update[0])
== normalize_mode_profile_name(draft_reference_name)
)
if (
self._parse_mode_profile_create_name(utterance.strip()) is not None
or draft_update_requested
or mode_profile_query
):
execution_mode = "dry_run" execution_mode = "dry_run"
if execution_mode != "hardware": if execution_mode != "hardware":
return self._process_once( return self._process_once(
@@ -2076,6 +2099,7 @@ class SeatDemoService:
_direct_mode_profile_id, _direct_mode_profile_id,
_speech_understanding, _speech_understanding,
assistant_plan=_assistant_plan, assistant_plan=_assistant_plan,
mode_profile_draft_context=_mode_profile_draft_context,
) )
if control_transport == WIRELESS_MQTT_TRANSPORT: if control_transport == WIRELESS_MQTT_TRANSPORT:
# The head unit owns both physical CAN channels in wireless mode. # The head unit owns both physical CAN channels in wireless mode.
@@ -2100,6 +2124,7 @@ class SeatDemoService:
_direct_mode_profile_id, _direct_mode_profile_id,
_speech_understanding, _speech_understanding,
assistant_plan=_assistant_plan, assistant_plan=_assistant_plan,
mode_profile_draft_context=_mode_profile_draft_context,
) )
if hardware_access_bypass or self._driver_only_vehicle_execution_active( if hardware_access_bypass or self._driver_only_vehicle_execution_active(
control_transport control_transport
@@ -2124,6 +2149,7 @@ class SeatDemoService:
_direct_mode_profile_id, _direct_mode_profile_id,
_speech_understanding, _speech_understanding,
assistant_plan=_assistant_plan, assistant_plan=_assistant_plan,
mode_profile_draft_context=_mode_profile_draft_context,
) )
authorized_client_id = self.hardware_access.ensure_authorized(hardware_client_id) authorized_client_id = self.hardware_access.ensure_authorized(hardware_client_id)
with self.hardware_execution.reserve(): with self.hardware_execution.reserve():
@@ -2147,6 +2173,7 @@ class SeatDemoService:
_direct_mode_profile_id, _direct_mode_profile_id,
_speech_understanding, _speech_understanding,
assistant_plan=_assistant_plan, assistant_plan=_assistant_plan,
mode_profile_draft_context=_mode_profile_draft_context,
) )
def _process_once( def _process_once(
@@ -2171,6 +2198,7 @@ class SeatDemoService:
assistant_plan: dict[str, Any] | None = None, assistant_plan: dict[str, Any] | None = None,
allow_fuzzy_mode_profile_match: bool = True, allow_fuzzy_mode_profile_match: bool = True,
allow_scene_composition: bool = False, allow_scene_composition: bool = False,
mode_profile_draft_context: dict[str, Any] | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
control_transport = self._validate_control_transport(control_transport) control_transport = self._validate_control_transport(control_transport)
utterance = utterance.strip() utterance = utterance.strip()
@@ -2203,11 +2231,24 @@ class SeatDemoService:
mode_save_name = self._parse_mode_profile_save_name(utterance) mode_save_name = self._parse_mode_profile_save_name(utterance)
mode_create_name = self._parse_mode_profile_create_name(utterance) mode_create_name = self._parse_mode_profile_create_name(utterance)
mode_restore_name = self._parse_mode_profile_restore_name(utterance) mode_restore_name = self._parse_mode_profile_restore_name(utterance)
mode_update = self._parse_mode_profile_update(utterance) draft_reference_name = (
str(mode_profile_draft_context.get("name") or "").strip()
if isinstance(mode_profile_draft_context, dict)
else ""
)
mode_update = parse_mode_profile_update(
utterance,
expected_mode_name=draft_reference_name or None,
)
mode_profile_query = is_mode_profile_query(
utterance,
expected_mode_name=draft_reference_name or None,
)
control_plane_request = ( control_plane_request = (
mode_save_name is not None mode_save_name is not None
or mode_restore_name is not None or mode_restore_name is not None
or mode_update is not None or mode_update is not None
or mode_profile_query
) )
hardware_request = execution_mode == "hardware" hardware_request = execution_mode == "hardware"
if hardware_request and _has_mixed_driver_passenger_seat_scope(utterance): if hardware_request and _has_mixed_driver_passenger_seat_scope(utterance):
@@ -2242,7 +2283,12 @@ class SeatDemoService:
assistant_plan=assistant_plan, assistant_plan=assistant_plan,
) )
request_id = f"req-{datetime.now(UTC).strftime('%Y%m%d%H%M%S')}-{uuid.uuid4().hex[:6]}" # A request id is also the opaque capability used to reference a
# server-owned draft artifact. Keep the full 128 bits of randomness;
# the previous six-hex suffix was guessable under multi-user traffic.
request_id = (
f"req-{datetime.now(UTC).strftime('%Y%m%d%H%M%S')}-{uuid.uuid4().hex}"
)
context = self._context_for_vehicle( context = self._context_for_vehicle(
self.conversations.snapshot(conversation_id), self.conversations.snapshot(conversation_id),
vehicle_profile, vehicle_profile,
@@ -2319,6 +2365,24 @@ class SeatDemoService:
} }
started = time.perf_counter() started = time.perf_counter()
if mode_profile_query:
result["status"] = "needs_confirmation"
result["execution_mode"] = "dry_run"
result["hardware_execution_allowed"] = False
result["orchestration_route"] = "mode_profile_query"
result["validation"] = [
{
"severity": "warning",
"code": "MODE_PROFILE_QUERY_NOT_CONTROL",
"path": "utterance",
"message": "该语句是在询问功能模式,不是模式修改或车辆执行指令。",
}
]
result["decision_summary"] = [
"识别为功能模式问答;未生成或执行任何车辆控制动作。"
]
self._persist(result)
return result
if mode_save_name is not None: if mode_save_name is not None:
return self._process_mode_profile_save( return self._process_mode_profile_save(
utterance, utterance,
@@ -2342,6 +2406,28 @@ class SeatDemoService:
) )
if mode_update is not None: if mode_update is not None:
mode_name, change_text = mode_update mode_name, change_text = mode_update
if (
draft_reference_name
and normalize_mode_profile_name(draft_reference_name)
== normalize_mode_profile_name(mode_name)
):
return self._process_mode_profile_draft_update(
utterance,
request_id,
provider,
conversation_id,
context,
mode_name,
change_text,
vehicle_profile,
result,
traces,
mode_profile_draft_context,
approval_token=approval_token,
simulation_fault=simulation_fault,
control_transport=control_transport,
assistant_plan=assistant_plan,
)
return self._process_mode_profile_update( return self._process_mode_profile_update(
utterance, utterance,
request_id, request_id,
@@ -3550,18 +3636,7 @@ class SeatDemoService:
@staticmethod @staticmethod
def _parse_mode_profile_update(utterance: str) -> tuple[str, str] | None: def _parse_mode_profile_update(utterance: str) -> tuple[str, str] | None:
explicit = UPDATE_MODE_PROFILE_PREFIX.search(utterance) return parse_mode_profile_update(utterance)
if explicit is not None:
change_text = str(explicit.group("change") or "").strip()
return explicit.group("name").strip(), change_text
inline = UPDATE_MODE_PROFILE_BA.search(utterance)
if inline is None:
return None
change_text = inline.group("change").strip("的").strip()
if not any(word in change_text for word in ("改", "调", "设", "更新", "变")):
return None
return inline.group("name").strip(), change_text
@staticmethod @staticmethod
def _parse_mode_profile_restore_name(utterance: str) -> str | None: def _parse_mode_profile_restore_name(utterance: str) -> str | None:
@@ -3953,6 +4028,414 @@ class SeatDemoService:
domains.append(domain) domains.append(domain)
return domains or ["seat"] return domains or ["seat"]
@staticmethod
def _preview_mode_operations(
source_result: dict[str, Any],
) -> list[dict[str, Any]]:
"""Recover every reusable operation from server-compiled preview data."""
plans = source_result.get("command_plans")
configs = source_result.get("function_configs")
if not isinstance(plans, dict) or not isinstance(configs, dict):
return []
operations: list[dict[str, Any]] = []
seen: set[tuple[str, str, str]] = set()
for key, plan in plans.items():
if (
not isinstance(plan, dict)
or not isinstance(plan.get("spec"), dict)
or not isinstance(plan["spec"].get("commands"), list)
or not plan["spec"]["commands"]
):
continue
config = configs.get(key)
spec = config.get("spec") if isinstance(config, dict) else None
if not isinstance(spec, dict):
continue
domain = str(spec.get("domain") or "").strip()
raw_operations = spec.get("operations")
if not domain or not isinstance(raw_operations, list):
continue
for operation in raw_operations:
if not isinstance(operation, dict):
continue
capability = str(operation.get("capability") or "").strip()
raw_target = operation.get("target")
target = (
str(
raw_target.get("instance")
or raw_target.get("zone")
or raw_target.get("scope")
or ""
).strip()
if isinstance(raw_target, dict)
else ""
)
parameters = operation.get("parameters")
signature = (domain, capability, target)
if (
not capability
or not target
or not isinstance(parameters, dict)
or signature in seen
):
continue
reusable_parameters = deepcopy(parameters)
if (
capability.startswith("seat.set_")
and reusable_parameters.get("position_pct") is not None
and reusable_parameters.get("angle_deg") is not None
):
# Seat compilation adds the calibrated angle alongside the
# requested percentage. The scene contract intentionally
# permits one absolute representation only; retain the
# portable percentage and let the new compile derive angle.
reusable_parameters.pop("angle_deg", None)
seen.add(signature)
operations.append(
{
"domain": domain,
"capability": capability,
"target_scope": target,
"operation": "set",
"parameters": reusable_parameters,
"reason": "由服务端已校验功能模式草稿保留。",
}
)
return operations
def _load_mode_profile_draft_reference(
self,
reference: dict[str, Any],
*,
conversation_id: str,
mode_name: str,
vehicle_id: str,
) -> tuple[dict[str, Any], dict[str, Any], list[dict[str, Any]]]:
"""Resolve an opaque client reference back to one trusted local artifact."""
if not isinstance(reference, dict) or set(reference) != {
"name",
"source_request_id",
"vehicle_id",
}:
raise ValueError("待保存功能模式草稿引用格式无效。")
clean_name = str(reference.get("name") or "").strip()
clean_request_id = str(reference.get("source_request_id") or "").strip()
clean_vehicle_id = str(reference.get("vehicle_id") or "").strip()
if not MODE_EDITOR_REQUEST_ID_PATTERN.fullmatch(clean_request_id):
raise ValueError("待保存功能模式草稿来源请求编号无效。")
if (
normalize_mode_profile_name(clean_name)
!= normalize_mode_profile_name(mode_name)
):
raise ValueError("修改请求与待保存功能模式草稿名称不匹配。")
if clean_vehicle_id != vehicle_id:
raise ValueError("待保存功能模式草稿不属于当前车型。")
result_path = self.artifact_root / clean_request_id / "result.json"
if not result_path.is_file():
raise ValueError("未找到待保存功能模式草稿,请重新构建。")
try:
source_result = json.loads(result_path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
raise ValueError("待保存功能模式草稿无法读取,请重新构建。") from exc
if not isinstance(source_result, dict):
raise ValueError("待保存功能模式草稿内容无效,请重新构建。")
source_vehicle = source_result.get("vehicle")
source_vehicle_id = (
str(source_vehicle.get("id") or "").strip()
if isinstance(source_vehicle, dict)
else ""
)
if (
source_result.get("request_id") != clean_request_id
or source_result.get("conversation_id") != conversation_id
or source_vehicle_id != vehicle_id
or source_result.get("status") != "succeeded"
or source_result.get("execution_mode") != "dry_run"
or source_result.get("hardware_execution_allowed") is not False
or source_result.get("orchestration_route")
!= "mode_profile_create_preview"
):
raise ValueError("待保存功能模式草稿与当前会话不匹配,请重新构建。")
draft = source_result.get("mode_profile_draft")
if not isinstance(draft, dict):
raise ValueError("来源请求不是可修改的功能模式草稿。")
if (
str(draft.get("name") or "").strip() != clean_name
or str(draft.get("source_request_id") or "").strip()
!= clean_request_id
or str(draft.get("vehicle_id") or "").strip() != vehicle_id
):
raise ValueError("待保存功能模式草稿引用与来源内容不匹配。")
validation = source_result.get("validation")
if isinstance(validation, list) and any(
isinstance(issue, dict) and issue.get("severity") == "error"
for issue in validation
):
raise ValueError("待保存功能模式草稿没有通过完整安全校验。")
operations = self._preview_mode_operations(source_result)
operation_count = draft.get("operation_count")
if (
not isinstance(operation_count, int)
or isinstance(operation_count, bool)
or operation_count <= 0
or len(operations) != operation_count
):
raise ValueError("待保存功能模式草稿动作证据不完整,请重新构建。")
return source_result, draft, operations
@staticmethod
def _absolute_mode_operations(
operations: list[dict[str, Any]],
) -> list[dict[str, Any]]:
"""Convert bounded motion commands into replayable absolute mode targets."""
normalized: list[dict[str, Any]] = []
window_targets = {
"all": ("front_left", "front_right", "rear_left", "rear_right"),
"front": ("front_left", "front_right"),
"rear": ("rear_left", "rear_right"),
}
for operation in operations:
item = deepcopy(operation)
capability = str(item.get("capability") or "")
parameters = dict(item.get("parameters") or {})
if capability == "roof.set_shade_motion":
positions = {"close": 0, "half": 50, "open": 100}
motion = str(parameters.get("motion") or "")
if motion not in positions:
raise ValueError("遮阳帘停止动作没有可保存的最终位置,请改为明确开度。")
item["capability"] = "roof.set_shade_position"
item["parameters"] = {"position_pct": positions[motion]}
normalized.append(item)
continue
if capability == "window.set_motion":
positions = {"close": 0, "open": 100}
motion = str(parameters.get("motion") or "")
if motion not in positions:
raise ValueError("车窗停止动作没有可保存的最终位置,请改为明确开度。")
target = str(item.get("target_scope") or "")
targets = window_targets.get(target, (target,))
for resolved_target in targets:
absolute = deepcopy(item)
absolute["capability"] = "window.set_position"
absolute["target_scope"] = resolved_target
absolute["parameters"] = {"position_pct": positions[motion]}
normalized.append(absolute)
continue
normalized.append(item)
return normalized
@staticmethod
def _mode_profile_draft_plan(
operations: list[dict[str, Any]],
) -> dict[str, Any]:
return {
"detected_domains": list(
dict.fromkeys(str(item["domain"]) for item in operations)
),
"requests": [
{
"domain": str(item["domain"]),
"capability": str(item["capability"]),
"target_scope": str(item["target_scope"]),
"operation": "set",
"duration_ms": None,
"parameters": [
{"name": str(name), "value": deepcopy(value)}
for name, value in dict(item.get("parameters") or {}).items()
],
"reason": "按用户要求修改待保存功能模式草稿。",
}
for item in operations
],
"ambiguities": [],
"decision_summary": ["已在服务端合并并重新校验待保存模式动作。"],
"confidence": 1.0,
}
@staticmethod
def _mode_update_target_is_explicit(change_text: str, target: str) -> bool:
patterns = {
"driver": r"主驾|驾驶位|驾驶员|左前",
"front_left": r"主驾|驾驶位|驾驶员|左前",
"passenger": r"副驾|乘客位|右前",
"front_right": r"副驾|乘客位|右前",
"front_row": r"前排|前舱",
"rear": r"后排|后座|后舱|所有后排",
"rear_row": r"后排|后座|后舱|所有后排",
"rear_left": r"左后|后排左|左侧后排",
"rear_right": r"右后|后排右|右侧后排",
"vehicle": r"全车|整车|所有|全部",
"all": r"全车|整车|所有|全部",
}
pattern = patterns.get(str(target))
return bool(pattern and re.search(pattern, change_text))
def _process_mode_profile_draft_update(
self,
utterance: str,
request_id: str,
provider: str,
conversation_id: str,
context: dict[str, Any],
mode_name: str,
change_text: str,
vehicle_profile: VehicleProfile,
result: dict[str, Any],
traces: list[dict[str, Any]],
draft_reference: dict[str, Any],
*,
approval_token: str | None,
simulation_fault: str,
control_transport: str,
assistant_plan: dict[str, Any] | None,
) -> dict[str, Any]:
"""Patch an unsaved preview and produce a new atomic dry-run artifact."""
del request_id, traces
try:
_source_result, source_draft, source_operations = (
self._load_mode_profile_draft_reference(
draft_reference,
conversation_id=conversation_id,
mode_name=mode_name,
vehicle_id=vehicle_profile.id,
)
)
base_operations = self._absolute_mode_operations(source_operations)
if not change_text:
raise ValueError("请说明要调整的设备和明确目标值。")
parseable_change_text = re.sub(
r"改为|改成|变更为|换成|变为",
"调整为",
change_text,
)
unresolved_change_clauses = [
clause
for clause, _parsed, status in self._local_clause_results(
parseable_change_text,
context,
)
if status != "complete"
]
if unresolved_change_clauses and assistant_plan is None:
raise ValueError(
"调整说明包含未完整识别的内容:"
+ ";".join(unresolved_change_clauses)
)
change_result = self._process_once(
parseable_change_text,
provider,
conversation_id,
"dry_run",
approval_token,
vehicle_profile.id,
simulation_fault,
control_transport=control_transport,
assistant_plan=assistant_plan,
allow_fuzzy_mode_profile_match=False,
)
change_operations = self._absolute_mode_operations(
self._preview_mode_operations(change_result)
)
if (
change_result.get("status") != "succeeded"
or not change_operations
or change_result.get("supplement_required") is True
or bool(change_result.get("unresolved_clauses"))
or bool(change_result.get("deferred_requests"))
):
raise ValueError("没有从调整说明中得到完整、可复用的设备目标。")
validation = change_result.get("validation")
if isinstance(validation, list) and any(
isinstance(issue, dict) and issue.get("severity") == "error"
for issue in validation
):
raise ValueError("调整动作没有通过车辆能力或安全校验。")
normalized_changes: list[dict[str, Any]] = []
for change in change_operations:
same_capability = [
item
for item in base_operations
if item["domain"] == change["domain"]
and item["capability"] == change["capability"]
]
normalized = deepcopy(change)
if len(same_capability) == 1 and not self._mode_update_target_is_explicit(
change_text,
str(change["target_scope"]),
):
normalized["target_scope"] = same_capability[0]["target_scope"]
elif len(same_capability) > 1 and not self._mode_update_target_is_explicit(
change_text,
str(change["target_scope"]),
):
raise ValueError(
f"“{mode_name}”中该设备能力包含多个目标,请明确主驾、副驾或具体区域。"
)
normalized_changes.append(normalized)
change_by_key = {
(
str(item["domain"]),
str(item["capability"]),
str(item["target_scope"]),
): item
for item in normalized_changes
}
merged: list[dict[str, Any]] = []
for operation in base_operations:
key = (
str(operation["domain"]),
str(operation["capability"]),
str(operation["target_scope"]),
)
merged.append(deepcopy(change_by_key.pop(key, operation)))
merged.extend(deepcopy(list(change_by_key.values())))
return self._process_mode_profile_create(
utterance,
"local",
conversation_id,
"dry_run",
approval_token,
vehicle_profile,
simulation_fault,
mode_name,
control_transport,
assistant_plan=self._mode_profile_draft_plan(merged),
source_draft=source_draft,
)
except ValueError as exc:
result["status"] = "needs_confirmation"
result["execution_mode"] = "dry_run"
result["hardware_execution_allowed"] = False
result["orchestration_route"] = "mode_profile_create_preview"
result["validation"] = [
{
"severity": "error",
"code": "MODE_PROFILE_DRAFT_UPDATE_REJECTED",
"path": "mode_profile_draft_context",
"message": str(exc),
}
]
result["mode_creation_failure"] = {
"code": "MODE_PROFILE_DRAFT_UPDATE_REJECTED",
"reason": str(exc),
"next_step": "请保留当前模式卡片,并明确要修改的设备、区域和目标值后重试",
"unresolved_clauses": [change_text] if change_text else [],
"issues": [],
}
result["decision_summary"] = [
f"未修改待保存的“{mode_name}”草稿;原草稿保持不变。"
]
self._persist(result)
return result
def _process_mode_profile_update( def _process_mode_profile_update(
self, self,
utterance: str, utterance: str,
@@ -4303,10 +4786,11 @@ class SeatDemoService:
traces: list[dict[str, Any]], traces: list[dict[str, Any]],
request_id: str, request_id: str,
) -> ModeProfile | None: ) -> ModeProfile | None:
matches = self.mode_profiles.match_text( exact = self.mode_profiles.resolve_exact_name(
mode_name, mode_name,
vehicle_id=vehicle_profile.id, vehicle_id=vehicle_profile.id,
) )
matches = [exact] if exact is not None else []
result["matched_modes"] = [profile.public_dict() for profile in matches] result["matched_modes"] = [profile.public_dict() for profile in matches]
if len(matches) == 1: if len(matches) == 1:
return matches[0] return matches[0]
@@ -4614,6 +5098,7 @@ class SeatDemoService:
mode_name: str, mode_name: str,
control_transport: str, control_transport: str,
assistant_plan: dict[str, Any] | None = None, assistant_plan: dict[str, Any] | None = None,
source_draft: dict[str, Any] | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Build and verify a named-mode draft for explicit user confirmation. """Build and verify a named-mode draft for explicit user confirmation.
@@ -4629,14 +5114,42 @@ class SeatDemoService:
if create_match is not None if create_match is not None
else "" else ""
) )
existing_profile = self.mode_profiles.resolve_exact_name( source_existing_mode_id = (
mode_name, str(source_draft.get("existing_mode_id") or "").strip()
vehicle_id=vehicle_profile.id, if isinstance(source_draft, dict)
else ""
)
if source_draft is not None and source_existing_mode_id:
try:
existing_profile = self.mode_profiles.get_profile(
source_existing_mode_id,
vehicle_profile.id,
)
except KeyError:
existing_profile = None
elif source_draft is not None:
existing_profile = None
else:
existing_profile = self.mode_profiles.resolve_exact_name(
mode_name,
vehicle_id=vehicle_profile.id,
)
source_profile = (
existing_profile
if not requested_instruction and source_draft is None
else None
)
draft_mode_name = (
str(source_draft.get("name") or mode_name).strip()
if isinstance(source_draft, dict)
else existing_profile.name
if existing_profile is not None
else mode_name
) )
source_profile = existing_profile if not requested_instruction else None
draft_mode_name = existing_profile.name if existing_profile is not None else mode_name
generation_source = ( generation_source = (
"registered_mode_profile" "draft_revision"
if source_draft is not None
else "registered_mode_profile"
if source_profile is not None if source_profile is not None
else "codex_scene_composition" else "codex_scene_composition"
if assistant_plan is not None and not requested_instruction if assistant_plan is not None and not requested_instruction
@@ -4647,31 +5160,99 @@ class SeatDemoService:
# of appending a model prompt to the user's utterance. The latter was # of appending a model prompt to the user's utterance. The latter was
# being parsed as a second control clause on the assistant's local fast # being parsed as a second control clause on the assistant's local fast
# path, turning a valid nine-action mode into an incomplete draft. # path, turning a valid nine-action mode into an incomplete draft.
source_utterance = requested_instruction or f"进入{mode_name}" source_utterance = requested_instruction or f"进入{draft_mode_name}"
source_result = self._process_once( try:
source_utterance, source_result = self._process_once(
provider, source_utterance,
conversation_id, provider,
"dry_run", conversation_id,
approval_token, "dry_run",
vehicle_profile.id, approval_token,
simulation_fault, vehicle_profile.id,
control_transport=control_transport, simulation_fault,
direct_mode_profile_id=( control_transport=control_transport,
source_profile.id if source_profile is not None else None direct_mode_profile_id=(
), source_profile.id if source_profile is not None else None
assistant_plan=assistant_plan, ),
# ``source_utterance`` is synthesized for a name-only, unknown assistant_plan=assistant_plan,
# scene. It must not fall back to substring matching (for example # ``source_utterance`` is synthesized for a name-only, unknown
# ``我的小憩模式`` -> ``小憩模式``), otherwise the exact resolver's # scene. It must not fall back to substring matching (for example
# ambiguity and collision checks can be bypassed. # ``我的小憩模式`` -> ``小憩模式``), otherwise the exact resolver's
allow_fuzzy_mode_profile_match=bool(requested_instruction), # ambiguity and collision checks can be bypassed.
allow_scene_composition=( allow_fuzzy_mode_profile_match=bool(requested_instruction),
assistant_plan is not None allow_scene_composition=(
and source_profile is None assistant_plan is not None
and not requested_instruction and source_profile is None
), and not requested_instruction
) ),
)
except NativeCanCompileError:
# Repository modes and model-composed scenes share the same native
# compiler. A stale mode parameter must become a bounded failed
# preview, never an uncaught HTTP 500 or a partially usable card.
failure_context = self._context_for_vehicle(
self.conversations.snapshot(conversation_id),
vehicle_profile,
)
source_result = {
"request_id": (
f"req-{datetime.now(UTC).strftime('%Y%m%d%H%M%S')}-"
f"{uuid.uuid4().hex}"
),
"utterance": source_utterance,
"conversation_id": conversation_id,
"conversation_turn": failure_context["turn_count"],
"status": "validation_failed",
"provider_requested": provider,
"provider_used": "mode_profile_native_can_guard",
"execution_mode": "dry_run",
"control_transport": control_transport,
"hardware_execution_allowed": False,
"vehicle": vehicle_profile.public_dict(),
"matched_modes": (
[source_profile.public_dict()]
if source_profile is not None
else []
),
"mode_profile": None,
"decision_summary": [
f"“{draft_mode_name}”包含当前车型协议无法编译的动作,未生成草稿。"
],
"deferred_requests": [],
"model_requests": [],
"unresolved_clauses": [],
"intent_ir": None,
"function_config": None,
"function_configs": {},
"scene_config": None,
"preset_config": None,
"validation": [
{
"severity": "error",
"code": "MODE_PROFILE_NATIVE_COMPILE_FAILED",
"path": "mode_profile_draft",
"message": "模式动作无法编译为当前车型支持的控制指令。",
}
],
"command_plan": None,
"command_plans": {},
"execution_report": None,
"actuator_state": failure_context["devices"],
"domain_state": failure_context["domain_devices"],
"saved_presets": [],
"trace_events": [],
"can_commands": [],
"orchestration_route": "mode_profile_create_preview",
"mode_creation_failure": {
"code": "MODE_PROFILE_NATIVE_COMPILE_FAILED",
"reason": "模式动作未通过当前车型 CAN 协议编译校验",
"next_step": "请检查模式中的设备参数是否符合当前车型能力范围",
"unresolved_clauses": [],
"issues": [],
},
}
self._persist(source_result)
return source_result
draft_context = self._context_for_vehicle( draft_context = self._context_for_vehicle(
self.conversations.snapshot(conversation_id), self.conversations.snapshot(conversation_id),
vehicle_profile, vehicle_profile,
@@ -4736,7 +5317,7 @@ class SeatDemoService:
] if isinstance(source_actions, list) else [] ] if isinstance(source_actions, list) else []
if ( if (
source_result.get("status") == "succeeded" source_result.get("status") == "succeeded"
and generation_source == "codex_scene_composition" and generation_source in {"codex_scene_composition", "draft_revision"}
and assistant_plan is not None and assistant_plan is not None
and ( and (
self._scene_plan_signatures(assistant_plan) self._scene_plan_signatures(assistant_plan)
@@ -4762,7 +5343,7 @@ class SeatDemoService:
] ]
if source_result.get("status") != "succeeded": if source_result.get("status") != "succeeded":
if generation_source == "codex_scene_composition": if generation_source in {"codex_scene_composition", "draft_revision"}:
# Final fail-closed barrier for errors discovered after the # Final fail-closed barrier for errors discovered after the
# plan guard (adapter, schema, compiler, or safety validation). # plan guard (adapter, schema, compiler, or safety validation).
# A failed reusable scene must not expose a trial-able subset. # A failed reusable scene must not expose a trial-able subset.
@@ -4825,6 +5406,40 @@ class SeatDemoService:
) )
) )
source_request_id = str(source_result.get("request_id") or "") source_request_id = str(source_result.get("request_id") or "")
source_existing_revision = (
str(source_draft.get("existing_mode_revision") or "").strip()
if isinstance(source_draft, dict)
else ""
)
existing_binding = (
{
"existing_mode_id": source_existing_mode_id,
"existing_mode_revision": source_existing_revision,
}
if source_draft is not None and source_existing_mode_id
else {
"existing_mode_id": existing_profile.id,
"existing_mode_revision": (
self.mode_profiles.profile_revision(existing_profile)
),
}
if existing_profile is not None
else {}
)
raw_source_revision = (
source_draft.get("draft_revision")
if isinstance(source_draft, dict)
else None
)
source_revision = (
raw_source_revision
if isinstance(raw_source_revision, int)
and not isinstance(raw_source_revision, bool)
and raw_source_revision >= 1
else 1
if source_draft is not None
else 0
)
source_result["mode_profile_draft"] = { source_result["mode_profile_draft"] = {
"name": draft_mode_name, "name": draft_mode_name,
"instruction": source_utterance, "instruction": source_utterance,
@@ -4832,17 +5447,18 @@ class SeatDemoService:
"vehicle_id": vehicle_profile.id, "vehicle_id": vehicle_profile.id,
"domains": domains, "domains": domains,
"operation_count": len(reusable_actions), "operation_count": len(reusable_actions),
**existing_binding,
"generation_source": generation_source,
"draft_revision": source_revision + 1,
**( **(
{ {
"existing_mode_id": existing_profile.id, "revises_source_request_id": str(
"existing_mode_revision": ( source_draft.get("source_request_id") or ""
self.mode_profiles.profile_revision(existing_profile) ).strip()
),
} }
if existing_profile is not None if isinstance(source_draft, dict)
else {} else {}
), ),
"generation_source": generation_source,
} }
source_result["mode_creation"] = { source_result["mode_creation"] = {
"requested_utterance": utterance, "requested_utterance": utterance,
@@ -4851,17 +5467,9 @@ class SeatDemoService:
"source_status": source_result.get("status"), "source_status": source_result.get("status"),
"source_provider": source_result.get("provider_used"), "source_provider": source_result.get("provider_used"),
"operation_count": len(reusable_actions), "operation_count": len(reusable_actions),
**( **existing_binding,
{
"existing_mode_id": existing_profile.id,
"existing_mode_revision": (
self.mode_profiles.profile_revision(existing_profile)
),
}
if existing_profile is not None
else {}
),
"generation_source": generation_source, "generation_source": generation_source,
"draft_revision": source_revision + 1,
} }
source_result["orchestration_route"] = "mode_profile_create_preview" source_result["orchestration_route"] = "mode_profile_create_preview"
source_result["decision_summary"] = [ source_result["decision_summary"] = [
@@ -4871,6 +5479,8 @@ class SeatDemoService:
if source_profile is not None if source_profile is not None
else f"Codex 已根据场景语义组合并验证“{draft_mode_name}”的工程能力。" else f"Codex 已根据场景语义组合并验证“{draft_mode_name}”的工程能力。"
if generation_source == "codex_scene_composition" if generation_source == "codex_scene_composition"
else f"已调整并重新校验待保存的“{draft_mode_name}”模式草稿。"
if generation_source == "draft_revision"
else f"已构建并验证“{mode_name}”场景。" else f"已构建并验证“{mode_name}”场景。"
), ),
*list(source_result.get("decision_summary", [])), *list(source_result.get("decision_summary", [])),
+33
View File
@@ -43,6 +43,10 @@ from cognitive_agent.application.codex_assistant import ( # noqa: E402
CodexVehicleToolError, CodexVehicleToolError,
) )
from cognitive_agent.application.gesture_bridge import GestureCapabilityBridge # noqa: E402 from cognitive_agent.application.gesture_bridge import GestureCapabilityBridge # noqa: E402
from cognitive_agent.application.mode_profile_intent import ( # noqa: E402
normalize_mode_profile_name,
parse_mode_profile_update,
)
from cognitive_agent.application.seat_service import SeatDemoService # noqa: E402 from cognitive_agent.application.seat_service import SeatDemoService # noqa: E402
from cognitive_agent.application.self_extension import ( # noqa: E402 from cognitive_agent.application.self_extension import ( # noqa: E402
ExtensionIntegrationError, ExtensionIntegrationError,
@@ -1657,6 +1661,25 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
if payload.get("reasoning_effort") is not None if payload.get("reasoning_effort") is not None
else "low" else "low"
) )
mode_profile_draft_context = payload.get("mode_profile_draft_context")
parsed_mode_update = parse_mode_profile_update(
message,
expected_mode_name=(
mode_profile_draft_context.get("name")
if isinstance(mode_profile_draft_context, Mapping)
else None
),
)
matching_mode_profile_draft_context = (
dict(mode_profile_draft_context)
if isinstance(mode_profile_draft_context, Mapping)
and parsed_mode_update is not None
and normalize_mode_profile_name(parsed_mode_update[0])
== normalize_mode_profile_name(
mode_profile_draft_context.get("name")
)
else None
)
common_arguments: dict[str, object] = { common_arguments: dict[str, object] = {
"utterance": message, "utterance": message,
# Codex has already converted an implicit request into one explicit # Codex has already converted an implicit request into one explicit
@@ -1672,6 +1695,15 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
"hardware_access_bypass": self._hardware_access_bypass_allowed(), "hardware_access_bypass": self._hardware_access_bypass_allowed(),
"reasoning_effort": reasoning_effort, "reasoning_effort": reasoning_effort,
"control_transport": str(payload.get("control_transport") or "wired_can"), "control_transport": str(payload.get("control_transport") or "wired_can"),
**(
{
"mode_profile_draft_context": (
matching_mode_profile_draft_context
)
}
if matching_mode_profile_draft_context is not None
else {}
),
} }
capability_catalog = self._assistant_capability_catalog() capability_catalog = self._assistant_capability_catalog()
capability_snapshot = capability_catalog.snapshot() capability_snapshot = capability_catalog.snapshot()
@@ -1783,6 +1815,7 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
control_terms=capability_catalog.control_terms(capability_snapshot), control_terms=capability_catalog.control_terms(capability_snapshot),
query_capabilities=capability_catalog.query, query_capabilities=capability_catalog.query,
authoritative_answer=fast_answer.answer if fast_answer else None, authoritative_answer=fast_answer.answer if fast_answer else None,
mode_profile_draft_context=mode_profile_draft_context,
) )
except HardwareAccessRequiredError as exc: except HardwareAccessRequiredError as exc:
self.vehicle_display_sync.update_task( self.vehicle_display_sync.update_task(
+121 -2
View File
@@ -186,7 +186,10 @@ class FakeVehicleService:
if self.fail: if self.fail:
raise RuntimeError("private adapter failure") raise RuntimeError("private adapter failure")
utterance = str(kwargs["utterance"]) utterance = str(kwargs["utterance"])
mode_preview = utterance.startswith(("请帮我创建一个", "构建一个")) draft_reference = kwargs.get("mode_profile_draft_context")
mode_preview = utterance.startswith(("请帮我创建一个", "构建一个")) or isinstance(
draft_reference, dict
)
callback = kwargs["on_commands_validated"] callback = kwargs["on_commands_validated"]
assert callable(callback) assert callable(callback)
callback( callback(
@@ -199,7 +202,13 @@ class FakeVehicleService:
) )
if mode_preview: if mode_preview:
movie_mode = "观影模式" in utterance movie_mode = "观影模式" in utterance
mode_name = "观影模式" if movie_mode else "宝宝睡眠模式" mode_name = (
str(draft_reference.get("name"))
if isinstance(draft_reference, dict)
else "观影模式"
if movie_mode
else "宝宝睡眠模式"
)
instruction = ( instruction = (
"遮阳帘关闭,车窗关闭,副驾座椅调整为零重力模式,灯光全部关闭" "遮阳帘关闭,车窗关闭,副驾座椅调整为零重力模式,灯光全部关闭"
if movie_mode if movie_mode
@@ -297,6 +306,17 @@ class FakeVehicleService:
"vehicle_id": "denza_n7", "vehicle_id": "denza_n7",
"domains": domains, "domains": domains,
"operation_count": len(actions), "operation_count": len(actions),
**(
{
"generation_source": "draft_revision",
"draft_revision": 2,
"revises_source_request_id": draft_reference[
"source_request_id"
],
}
if isinstance(draft_reference, dict)
else {}
),
}, },
"intent_ir": {"spec": {"actions": actions}}, "intent_ir": {"spec": {"actions": actions}},
} }
@@ -690,6 +710,105 @@ class TestCodexAssistantWebApi:
} }
] ]
def test_assistant_forwards_only_the_opaque_reference_for_pending_draft_update(
self,
) -> None:
reference = {
"name": "小憩模式",
"source_request_id": "req-mode-preview",
"vehicle_id": "denza_n7",
}
events = self.post_assistant(
{
"message": "把小憩模式的空调温度调整到25度",
"conversation_id": "conv-api-update-pending-mode",
"execution_mode": "hardware",
"mode_profile_draft_context": reference,
}
)
assert not FAKE_CODEX.turn_calls
call = AssistantApiHandler.service.calls[-1]
assert call["utterance"] == "把小憩模式的空调温度调整到25度"
assert call["mode_profile_draft_context"] == reference
action = next(event for event in events if event["type"] == "action.completed")
assert action["success"] is True
assert action["payload"]["execution_mode"] == "dry_run"
assert action["payload"]["hardware_execution_allowed"] is False
assert action["payload"]["mode_profile_draft"]["draft_revision"] == 2
assert (
action["payload"]["mode_profile_draft"]["revises_source_request_id"]
== "req-mode-preview"
)
assert events[0]["engine"] == "n7web-mode-builder"
assert "调整待保存" in events[-1]["answer"]
def test_spaced_device_action_update_keeps_the_draft_route_end_to_end(
self,
) -> None:
reference = {
"name": "小憩模式",
"source_request_id": "req-mode-preview",
"vehicle_id": "denza_n7",
}
events = self.post_assistant(
{
"message": "请帮我 把 小憩模式 里的 遮阳帘关闭",
"conversation_id": "conv-api-spaced-mode-update",
"execution_mode": "hardware",
"mode_profile_draft_context": reference,
}
)
assert not FAKE_CODEX.turn_calls
call = AssistantApiHandler.service.calls[-1]
assert call["mode_profile_draft_context"] == reference
assert call["execution_mode"] == "hardware"
action = next(event for event in events if event["type"] == "action.completed")
assert action["success"] is True
assert action["payload"]["execution_mode"] == "dry_run"
assert action["payload"]["hardware_execution_allowed"] is False
assert events[0]["engine"] == "n7web-mode-builder"
def test_active_draft_reference_is_not_forwarded_for_an_unrelated_question(
self,
) -> None:
events = self.post_assistant(
{
"message": "解释一下空调内循环和外循环的区别",
"conversation_id": "conv-api-question-with-active-draft",
"mode_profile_draft_context": {
"name": "小憩模式",
"source_request_id": "req-mode-preview",
"vehicle_id": "denza_n7",
},
}
)
assert events[0]["route"] == "agent"
assert events[-1]["answer"] == "这是 Codex 的测试回答。"
assert not AssistantApiHandler.service.calls
def test_mode_question_with_control_words_never_reaches_vehicle_service(
self,
) -> None:
events = self.post_assistant(
{
"message": "修改小憩模式的空调设置会怎样?",
"conversation_id": "conv-api-mode-question-no-control",
"execution_mode": "hardware",
"mode_profile_draft_context": {
"name": "小憩模式",
"source_request_id": "req-mode-preview",
"vehicle_id": "denza_n7",
},
}
)
assert events[0]["route"] == "agent"
assert events[-1]["answer"] == "这是 Codex 的测试回答。"
assert not AssistantApiHandler.service.calls
def test_exact_movie_mode_request_streams_a_nine_action_preview(self) -> None: def test_exact_movie_mode_request_streams_a_nine_action_preview(self) -> None:
events = self.post_assistant( events = self.post_assistant(
{ {
+6 -6
View File
@@ -656,7 +656,7 @@ operations:
) as response: ) as response:
extension_styles = response.read().decode("utf-8").replace("\r\n", "\n") extension_styles = response.read().decode("utf-8").replace("\r\n", "\n")
with self.opener.open( with self.opener.open(
f"{self.base_url}/assistant-chat.js?v=codex-assistant-22", timeout=3 f"{self.base_url}/assistant-chat.js?v=codex-assistant-23", timeout=3
) as response: ) as response:
assistant_script = response.read().decode("utf-8") assistant_script = response.read().decode("utf-8")
with self.opener.open( with self.opener.open(
@@ -718,8 +718,8 @@ operations:
self.assertNotIn("滚动查看更多", html) self.assertNotIn("滚动查看更多", html)
self.assertIn('aria-label="发送消息"', html) self.assertIn('aria-label="发送消息"', html)
self.assertIn('id="assistantTranscript"', html) self.assertIn('id="assistantTranscript"', html)
self.assertIn('href="/assistant-chat.css?v=codex-assistant-22"', html) self.assertIn('href="/assistant-chat.css?v=codex-assistant-23"', html)
self.assertIn('src="/assistant-chat.js?v=codex-assistant-22"', html) self.assertIn('src="/assistant-chat.js?v=codex-assistant-23"', html)
self.assertIn('href="/energy-monitor.css?v=energy-monitor-4"', html) self.assertIn('href="/energy-monitor.css?v=energy-monitor-4"', html)
self.assertIn('src="/energy-monitor.js?v=energy-monitor-4"', html) self.assertIn('src="/energy-monitor.js?v=energy-monitor-4"', html)
self.assertNotIn('"admin"', console_script) self.assertNotIn('"admin"', console_script)
@@ -992,11 +992,11 @@ operations:
self.assertIn("CAN 指令已提交,等待车辆状态确认", console_script) self.assertIn("CAN 指令已提交,等待车辆状态确认", console_script)
self.assertIn('canvas.addEventListener("webglcontextlost"', app_script) self.assertIn('canvas.addEventListener("webglcontextlost"', app_script)
self.assertIn('"cognitive-agent:module-change"', app_script) self.assertIn('"cognitive-agent:module-change"', app_script)
self.assertIn('const CACHE_VERSION = "cognitive-agent-pad-v271"', service_worker) self.assertIn('const CACHE_VERSION = "cognitive-agent-pad-v272"', service_worker)
self.assertIn('/console-v2.css?v=delivery-state-65', service_worker) self.assertIn('/console-v2.css?v=delivery-state-65', service_worker)
self.assertIn('/console-v2.js?v=delivery-state-62', service_worker) self.assertIn('/console-v2.js?v=delivery-state-62', service_worker)
self.assertIn('/assistant-chat.css?v=codex-assistant-22', service_worker) self.assertIn('/assistant-chat.css?v=codex-assistant-23', service_worker)
self.assertIn('/assistant-chat.js?v=codex-assistant-22', service_worker) self.assertIn('/assistant-chat.js?v=codex-assistant-23', service_worker)
self.assertIn('/energy-monitor.css?v=energy-monitor-4', service_worker) self.assertIn('/energy-monitor.css?v=energy-monitor-4', service_worker)
self.assertIn('/energy-monitor.js?v=energy-monitor-4', service_worker) self.assertIn('/energy-monitor.js?v=energy-monitor-4', service_worker)
self.assertIn('"/app.js?v=dual-seat-127"', service_worker) self.assertIn('"/app.js?v=dual-seat-127"', service_worker)
@@ -2,6 +2,7 @@ from __future__ import annotations
import tempfile import tempfile
import unittest import unittest
from shutil import copytree
from pathlib import Path from pathlib import Path
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
@@ -96,6 +97,9 @@ class MultiDomainServiceTests(unittest.TestCase):
self.temporary_directory = tempfile.TemporaryDirectory() self.temporary_directory = tempfile.TemporaryDirectory()
self.addCleanup(self.temporary_directory.cleanup) self.addCleanup(self.temporary_directory.cleanup)
self.service.artifact_root = Path(self.temporary_directory.name) self.service.artifact_root = Path(self.temporary_directory.name)
isolated_modes = Path(self.temporary_directory.name) / "modes"
copytree(REPOSITORY_ROOT / "config" / "modes", isolated_modes)
self.service.mode_profiles = ModeProfileRegistry(isolated_modes)
def test_driver_massage_mode_and_strength_continuations_compile_locally( def test_driver_massage_mode_and_strength_continuations_compile_locally(
self, self,
@@ -9,8 +9,13 @@ from unittest.mock import MagicMock, patch
import pytest import pytest
from cognitive_agent.application.codex_assistant import CodexAssistantService from cognitive_agent.application.codex_assistant import CodexAssistantService
from cognitive_agent.application.mode_profile_intent import (
is_mode_profile_query,
parse_mode_profile_update,
)
from cognitive_agent.application.assistant_capabilities import AssistantCapabilityCatalog from cognitive_agent.application.assistant_capabilities import AssistantCapabilityCatalog
from cognitive_agent.application.mode_profile import ModeProfileRegistry from cognitive_agent.application.mode_profile import ModeProfileRegistry
from cognitive_agent.application.native_can_runtime import NativeCanCompileError
from cognitive_agent.application.seat_service import ( from cognitive_agent.application.seat_service import (
REPOSITORY_ROOT, REPOSITORY_ROOT,
SeatDemoService, SeatDemoService,
@@ -130,6 +135,68 @@ def _intent_actions(result: dict[str, object]) -> list[dict[str, object]]:
return actions return actions
def _draft_reference(result: dict[str, object]) -> dict[str, str]:
draft = result["mode_profile_draft"]
assert isinstance(draft, dict)
return {
"name": str(draft["name"]),
"source_request_id": str(draft["source_request_id"]),
"vehicle_id": str(draft["vehicle_id"]),
}
def test_every_repository_mode_can_build_a_name_only_draft_without_exception(
vehicle_service: SeatDemoService,
) -> None:
modes = vehicle_service.mode_profiles.public_modes("denza_n7")
assert modes
for index, mode in enumerate(modes):
labels = [str(mode["name"]), *map(str, mode.get("aliases", []))]
label = next((item for item in labels if item.endswith("模式")), None)
assert label is not None, mode["id"]
result = vehicle_service.preview_native_can(
f"构建一个{label}",
provider="local",
conversation_id=f"conv-name-only-mode-{index:02d}",
execution_mode="hardware",
vehicle_id="denza_n7",
)
assert result["status"] == "succeeded", mode["id"]
assert result["execution_mode"] == "dry_run"
assert result["hardware_execution_allowed"] is False
assert result["mode_profile_draft"]["operation_count"] > 0
def test_name_only_mode_native_compile_exception_returns_scrubbed_failure(
vehicle_service: SeatDemoService,
) -> None:
with patch.object(
vehicle_service.native_can_runtime,
"compile",
side_effect=NativeCanCompileError("synthetic compiler rejection"),
):
result = vehicle_service.preview_native_can(
"构建一个办公模式",
provider="local",
conversation_id="conv-name-only-native-compile-failure",
execution_mode="hardware",
vehicle_id="denza_n7",
)
assert result["status"] == "validation_failed"
assert result["execution_mode"] == "dry_run"
assert result["hardware_execution_allowed"] is False
assert result["orchestration_route"] == "mode_profile_create_preview"
assert result.get("mode_profile_draft") is None
assert result["function_configs"] == {}
assert result["command_plans"] == {}
assert result["mode_creation_failure"]["code"] == (
"MODE_PROFILE_NATIVE_COMPILE_FAILED"
)
def test_name_only_nap_request_reuses_the_verified_repository_profile( def test_name_only_nap_request_reuses_the_verified_repository_profile(
vehicle_service: SeatDemoService, vehicle_service: SeatDemoService,
tmp_path: Path, tmp_path: Path,
@@ -202,6 +269,537 @@ def test_inferred_nap_actions_are_bounded_by_the_runtime_capability_catalog(
assert all("media" not in str(action["capability"]) for action in actions) assert all("media" not in str(action["capability"]) for action in actions)
def test_pending_nap_draft_temperature_can_be_revised_without_saving_or_execution(
vehicle_service: SeatDemoService,
) -> None:
conversation_id = "conv-nap-pending-draft-revision"
original_profile = vehicle_service.mode_profiles.get_profile("nap", "denza_n7")
original_revision = vehicle_service.mode_profiles.profile_revision(original_profile)
built = _build_nap_mode(vehicle_service, conversation_id)
built_draft = built["mode_profile_draft"]
assert isinstance(built_draft, dict)
revised = vehicle_service.preview_native_can(
"把小憩模式的空调温度调整到25度",
provider="local",
conversation_id=conversation_id,
execution_mode="hardware",
vehicle_id="denza_n7",
mode_profile_draft_context=_draft_reference(built),
)
draft = revised["mode_profile_draft"]
assert revised["status"] == "succeeded"
assert revised["execution_mode"] == "dry_run"
assert revised["hardware_execution_allowed"] is False
assert revised["orchestration_route"] == "mode_profile_create_preview"
assert draft["name"] == "小憩模式"
assert draft["operation_count"] == built_draft["operation_count"] == 9
assert draft["generation_source"] == "draft_revision"
assert draft["draft_revision"] == 2
assert draft["revises_source_request_id"] == built_draft["source_request_id"]
assert draft["source_request_id"] != built_draft["source_request_id"]
assert draft["existing_mode_id"] == "nap"
assert draft["existing_mode_revision"] == original_revision
revised_actions = _intent_actions(revised)
temperatures = [
action
for action in revised_actions
if action["capability"] == "climate.set_temperature"
]
assert len(temperatures) == 1
assert temperatures[0]["parameters"]["temperature_c"] == 25
assert (
temperatures[0]["target"].get("zone")
or temperatures[0]["target"].get("instance")
) == "vehicle"
def sibling_signature(action: dict[str, object]) -> str:
parameters = dict(action.get("parameters") or {})
if parameters.get("position_pct") is not None:
parameters.pop("angle_deg", None)
return json.dumps(
{
"domain": action.get("domain"),
"capability": action.get("capability"),
"target": action.get("target"),
"parameters": parameters,
},
ensure_ascii=False,
sort_keys=True,
)
assert {
sibling_signature(action)
for action in revised_actions
if action["capability"] != "climate.set_temperature"
} == {
sibling_signature(action)
for action in _intent_actions(built)
if action["capability"] != "climate.set_temperature"
}
# Generating and revising a card is configuration-only. The registered
# profile remains unchanged until the card's explicit save/update action.
unchanged = vehicle_service.mode_profiles.get_profile("nap", "denza_n7")
assert vehicle_service.mode_profiles.profile_revision(unchanged) == original_revision
assert next(
operation.parameters["temperature_c"]
for operation in unchanged.operations
if operation.capability == "climate.set_temperature"
) != 25
@pytest.mark.parametrize(
("message", "capability", "parameter", "expected"),
(
(
"请帮我 把 小憩模式 的 空调温度调整到25度",
"climate.set_temperature",
"temperature_c",
25,
),
(
"把小憩模式中的空调温度调整到25度",
"climate.set_temperature",
"temperature_c",
25,
),
(
"把小憩模式里空调温度设置为25度",
"climate.set_temperature",
"temperature_c",
25,
),
(
"小憩模式,空调温度调到25度。",
"climate.set_temperature",
"temperature_c",
25,
),
(
"把小憩模式的遮阳帘关闭",
"roof.set_shade_position",
"position_pct",
0,
),
(
"把小憩模式里的氛围灯打开",
"ambient_light.set_power",
"state",
"on",
),
),
)
def test_pending_draft_update_variants_never_enter_physical_execution(
vehicle_service: SeatDemoService,
message: str,
capability: str,
parameter: str,
expected: object,
) -> None:
conversation_id = "conv-nap-draft-variant-" + str(abs(hash(message)))[:12]
built = _build_nap_mode(vehicle_service, conversation_id)
with patch.object(
vehicle_service.hardware_execution,
"reserve",
side_effect=AssertionError("pending draft update reached hardware lease"),
):
revised = vehicle_service.preview_native_can(
message,
provider="local",
conversation_id=conversation_id,
execution_mode="hardware",
vehicle_id="denza_n7",
mode_profile_draft_context=_draft_reference(built),
)
assert revised["status"] == "succeeded"
assert revised["execution_mode"] == "dry_run"
assert revised["hardware_execution_allowed"] is False
assert revised["orchestration_route"] == "mode_profile_create_preview"
assert next(
action["parameters"][parameter]
for action in _intent_actions(revised)
if action["capability"] == capability
) == expected
def test_pending_draft_can_be_revised_repeatedly_from_latest_server_artifact(
vehicle_service: SeatDemoService,
) -> None:
conversation_id = "conv-nap-pending-draft-chain"
built = _build_nap_mode(vehicle_service, conversation_id)
second = vehicle_service.preview_native_can(
"把小憩模式的空调温度调整到25度",
provider="local",
conversation_id=conversation_id,
vehicle_id="denza_n7",
mode_profile_draft_context=_draft_reference(built),
)
third = vehicle_service.preview_native_can(
"把小憩模式的空调温度调整到26度",
provider="local",
conversation_id=conversation_id,
vehicle_id="denza_n7",
mode_profile_draft_context=_draft_reference(second),
)
second_draft = second["mode_profile_draft"]
third_draft = third["mode_profile_draft"]
assert third_draft["draft_revision"] == 3
assert third_draft["revises_source_request_id"] == second_draft["source_request_id"]
assert next(
action["parameters"]["temperature_c"]
for action in _intent_actions(third)
if action["capability"] == "climate.set_temperature"
) == 26
def test_pending_draft_reference_is_bound_to_conversation_name_and_vehicle(
vehicle_service: SeatDemoService,
) -> None:
built = _build_nap_mode(vehicle_service, "conv-nap-reference-owner")
reference = _draft_reference(built)
wrong_conversation = vehicle_service.preview_native_can(
"把小憩模式的空调温度调整到25度",
provider="local",
conversation_id="conv-nap-reference-attacker",
vehicle_id="denza_n7",
mode_profile_draft_context=reference,
)
assert wrong_conversation["status"] == "needs_confirmation"
assert wrong_conversation.get("mode_profile_draft") is None
assert any(
issue.get("code") == "MODE_PROFILE_DRAFT_UPDATE_REJECTED"
for issue in wrong_conversation["validation"]
)
for invalid_reference in (
{**reference, "vehicle_id": "another_vehicle"},
{**reference, "source_request_id": "req-forged-preview"},
):
rejected = vehicle_service.preview_native_can(
"把小憩模式的空调温度调整到25度",
provider="local",
conversation_id="conv-nap-reference-owner",
vehicle_id="denza_n7",
mode_profile_draft_context=invalid_reference,
)
assert rejected["status"] == "needs_confirmation"
assert rejected.get("mode_profile_draft") is None
assert rejected["hardware_execution_allowed"] is False
@pytest.mark.parametrize(
"message",
(
"把小憩模式的空调介绍一下",
"小憩模式的空调有什么作用",
"小憩模式的空调和普通空调有什么区别",
"小憩模式的空调设置有哪些?",
"小憩模式的空调该调几度",
"把小憩模式的空调调到25度合适吗",
"调整小憩模式的空调参数,列出来",
),
)
def test_air_conditioner_name_alone_is_not_parsed_as_mode_profile_update(
message: str,
) -> None:
assert SeatDemoService._parse_mode_profile_update(message) is None
@pytest.mark.parametrize(
"message",
(
"空调出风模式设置为吹面和吹脚",
"把空调出风模式设置为吹面和吹脚",
"调整空调出风模式为吹面和吹脚",
"主驾座椅按摩模式设置为放松",
"副驾座椅零重力模式设为开启",
"进入动态摇篮模式,空调24度,打开遮阳帘",
"开启女王副驾模式,天幕透光率调到20%",
),
)
def test_device_property_mode_is_not_a_named_mode_profile_update(
message: str,
) -> None:
assert SeatDemoService._parse_mode_profile_update(message) is None
@pytest.mark.parametrize(
("message", "expected"),
(
("把小憩模式的空调温度调整到25度", ("小憩模式", "空调温度调整到25度")),
("午休模式里的遮阳帘关闭", ("午休模式", "遮阳帘关闭")),
("修改露营模式,天幕透光率改到50%", ("露营模式", "天幕透光率改到50%")),
),
)
def test_named_mode_profile_update_requires_an_explicit_member_boundary(
message: str,
expected: tuple[str, str],
) -> None:
assert SeatDemoService._parse_mode_profile_update(message) == expected
def test_name_first_comma_update_requires_the_active_draft_name() -> None:
message = "小憩模式,空调温度调到25度。"
assert SeatDemoService._parse_mode_profile_update(message) is None
assert parse_mode_profile_update(
message,
expected_mode_name="小憩模式",
) == ("小憩模式", "空调温度调到25度")
assert parse_mode_profile_update(
message,
expected_mode_name="露营模式",
) is None
def test_name_first_comma_question_is_not_a_draft_update() -> None:
message = "小憩模式,空调温度调到25度合适吗?"
assert parse_mode_profile_update(
message,
expected_mode_name="小憩模式",
) is None
assert is_mode_profile_query(
message,
expected_mode_name="小憩模式",
)
@pytest.mark.parametrize(
"message",
(
"修改小憩模式的空调设置是什么",
"小憩模式的空调该调几度",
"把小憩模式的空调调到25度合适吗",
"调整小憩模式的空调参数,列出来",
"小憩模式的空调调到25度会怎样",
),
)
def test_mode_profile_query_is_fail_closed_even_on_direct_hardware_service_call(
vehicle_service: SeatDemoService,
message: str,
) -> None:
with patch.object(
vehicle_service.hardware_execution,
"reserve",
side_effect=AssertionError("mode-profile query reached hardware lease"),
):
result = vehicle_service.preview_native_can(
message,
provider="local",
conversation_id="conv-mode-profile-query-guard",
execution_mode="hardware",
vehicle_id="denza_n7",
)
assert result["status"] == "needs_confirmation"
assert result["execution_mode"] == "dry_run"
assert result["hardware_execution_allowed"] is False
assert result["orchestration_route"] == "mode_profile_query"
assert result["function_configs"] == {}
assert result["command_plans"] == {}
def test_pending_draft_rejects_unsafe_change_without_returning_a_partial_card(
vehicle_service: SeatDemoService,
) -> None:
conversation_id = "conv-nap-pending-invalid-change"
built = _build_nap_mode(vehicle_service, conversation_id)
rejected = vehicle_service.preview_native_can(
"把小憩模式的空调温度调整到99度",
provider="local",
conversation_id=conversation_id,
vehicle_id="denza_n7",
mode_profile_draft_context=_draft_reference(built),
)
assert rejected["status"] == "needs_confirmation"
assert rejected["execution_mode"] == "dry_run"
assert rejected["hardware_execution_allowed"] is False
assert rejected.get("mode_profile_draft") is None
assert rejected["function_configs"] == {}
assert rejected["command_plans"] == {}
def test_pending_draft_revision_fails_closed_if_full_plan_loses_any_action(
vehicle_service: SeatDemoService,
) -> None:
conversation_id = "conv-nap-pending-incomplete-revision"
built = _build_nap_mode(vehicle_service, conversation_id)
with patch.object(
vehicle_service,
"_scene_action_signatures",
return_value={},
):
rejected = vehicle_service.preview_native_can(
"把小憩模式的空调温度调整到25度",
provider="local",
conversation_id=conversation_id,
vehicle_id="denza_n7",
mode_profile_draft_context=_draft_reference(built),
)
assert rejected["status"] != "succeeded"
assert rejected.get("mode_profile_draft") is None
assert not _intent_actions(rejected)
assert rejected["function_configs"] == {}
assert rejected["command_plans"] == {}
assert any(
issue.get("code") == "MODE_PROFILE_SCENE_PLAN_INCOMPLETE"
for issue in rejected["validation"]
)
def test_pending_draft_update_rejects_partially_understood_change_atomically(
vehicle_service: SeatDemoService,
) -> None:
conversation_id = "conv-nap-pending-partial-change"
built = _build_nap_mode(vehicle_service, conversation_id)
rejected = vehicle_service.preview_native_can(
"把小憩模式的空调温度调整到25度,同时启动曲速引擎",
provider="local",
conversation_id=conversation_id,
vehicle_id="denza_n7",
mode_profile_draft_context=_draft_reference(built),
)
assert rejected["status"] == "needs_confirmation"
assert rejected.get("mode_profile_draft") is None
assert rejected["function_configs"] == {}
assert rejected["command_plans"] == {}
assert "未完整识别" in rejected["mode_creation_failure"]["reason"]
def test_revised_unknown_mode_draft_can_be_trialed_and_saved_with_all_actions(
vehicle_service: SeatDemoService,
) -> None:
conversation_id = "conv-custom-pending-save-after-revision"
built = vehicle_service.preview_native_can(
"构建一个午休模式",
provider="local",
conversation_id=conversation_id,
vehicle_id="denza_n7",
assistant_plan=LUNCH_BREAK_SCENE_PLAN,
)
revised = vehicle_service.preview_native_can(
"午休模式的空调温度改为25度",
provider="local",
conversation_id=conversation_id,
vehicle_id="denza_n7",
mode_profile_draft_context=_draft_reference(built),
)
draft = revised["mode_profile_draft"]
trial = vehicle_service.trial_mode_profile_draft(
str(draft["name"]),
str(draft["instruction"]),
str(draft["source_request_id"]),
str(draft["vehicle_id"]),
)
saved = vehicle_service.create_mode_profile_card(
str(draft["name"]),
str(draft["instruction"]),
str(draft["source_request_id"]),
str(draft["vehicle_id"]),
)
assert trial["status"] == "succeeded"
assert trial["mode_profile_draft"]["draft_revision"] == 2
assert len(_intent_actions(trial)) == draft["operation_count"] == 5
assert saved["mode_profile"]["operation_count"] == 5
profile = vehicle_service.mode_profiles.get_profile(
str(saved["mode_profile"]["id"]),
"denza_n7",
)
assert next(
operation.parameters["temperature_c"]
for operation in profile.operations
if operation.capability == "climate.set_temperature"
) == 25
def test_explicit_mode_with_close_motions_can_still_receive_a_temperature_revision(
vehicle_service: SeatDemoService,
) -> None:
conversation_id = "conv-explicit-motion-draft-revision"
built = vehicle_service.preview_native_can(
"创建一个躺平测试模式,副驾座椅靠背向后移动到最大位置,"
"遮阳帘关闭,车窗关闭,灯光全部关闭,空调调整到24度",
provider="local",
conversation_id=conversation_id,
vehicle_id="denza_n7",
)
assert built["status"] == "succeeded"
assert built["mode_profile_draft"]["operation_count"] == 7
revised = vehicle_service.preview_native_can(
"把躺平测试模式的空调温度调整到25度",
provider="local",
conversation_id=conversation_id,
execution_mode="hardware",
vehicle_id="denza_n7",
mode_profile_draft_context=_draft_reference(built),
)
actions = _intent_actions(revised)
assert revised["status"] == "succeeded"
assert revised["execution_mode"] == "dry_run"
assert revised["hardware_execution_allowed"] is False
assert revised["mode_profile_draft"]["operation_count"] == len(actions) == 7
assert next(
action["parameters"]["temperature_c"]
for action in actions
if action["capability"] == "climate.set_temperature"
) == 25
capabilities = {str(action["capability"]) for action in actions}
assert "roof.set_shade_position" in capabilities
assert "window.set_position" in capabilities
assert "roof.set_shade_motion" not in capabilities
assert "window.set_motion" not in capabilities
def test_unscoped_change_rejects_a_draft_with_multiple_targets_for_same_capability(
vehicle_service: SeatDemoService,
) -> None:
conversation_id = "conv-multi-zone-pending-draft"
multi_zone_plan = {
**LUNCH_BREAK_SCENE_PLAN,
"detected_domains": ["climate"],
"requests": [
_scene_request(
"climate", "climate.set_temperature", "driver", {"temperature_c": 23}
),
_scene_request(
"climate", "climate.set_temperature", "passenger", {"temperature_c": 24}
),
],
}
built = vehicle_service.preview_native_can(
"构建一个双区舒适模式",
provider="local",
conversation_id=conversation_id,
vehicle_id="denza_n7",
assistant_plan=multi_zone_plan,
)
assert built["status"] == "succeeded"
rejected = vehicle_service.preview_native_can(
"把双区舒适模式的空调温度调整到25度",
provider="local",
conversation_id=conversation_id,
vehicle_id="denza_n7",
mode_profile_draft_context=_draft_reference(built),
)
assert rejected["status"] == "needs_confirmation"
assert rejected.get("mode_profile_draft") is None
assert "多个目标" in rejected["mode_creation_failure"]["reason"]
def test_codex_composes_an_unregistered_name_only_scene_from_runtime_capabilities( def test_codex_composes_an_unregistered_name_only_scene_from_runtime_capabilities(
vehicle_service: SeatDemoService, vehicle_service: SeatDemoService,
tmp_path: Path, tmp_path: Path,
+215 -2
View File
@@ -72,6 +72,11 @@ class MockElement extends MockEventTarget {
this.value = ""; this.value = "";
} }
get className() { return [...this.classList.values].join(" "); }
set className(value) {
this.classList.values = new Set(String(value).split(/\s+/).filter(Boolean));
}
append(...nodes) { append(...nodes) {
nodes.forEach((node) => { nodes.forEach((node) => {
this.children.push(node); this.children.push(node);
@@ -79,8 +84,31 @@ class MockElement extends MockEventTarget {
}); });
} }
closest(selector) {
if (selector.startsWith("#") && this.id === selector.slice(1)) return this;
return this.parentElement?.closest?.(selector) || null;
}
focus() {} focus() {}
querySelector() { return null; } matches(selector) {
if (selector === "button") return this.tagName === "BUTTON";
if (selector.startsWith(".")) return this.classList.contains(selector.slice(1));
const dataSelector = selector.match(/^\[data-([a-z-]+)(?:="([^"]*)")?\]$/);
if (!dataSelector) return false;
const key = dataSelector[1].replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase());
if (!(key in this.dataset)) return false;
return dataSelector[2] === undefined || this.dataset[key] === dataSelector[2];
}
querySelector(selector) { return this.querySelectorAll(selector)[0] || null; }
querySelectorAll(selector) {
const matches = [];
const visit = (node) => {
if (!(node instanceof MockElement)) return;
if (node.matches(selector)) matches.push(node);
node.children.forEach(visit);
};
this.children.forEach(visit);
return matches;
}
replaceChildren(...nodes) { replaceChildren(...nodes) {
this.children = []; this.children = [];
this.append(...nodes); this.append(...nodes);
@@ -133,7 +161,9 @@ function createHarness(events) {
document.createTextNode = (text) => ({ nodeType: 3, textContent: String(text) }); document.createTextNode = (text) => ({ nodeType: 3, textContent: String(text) });
document.querySelector = (selector) => selectors.get(selector) || null; document.querySelector = (selector) => selectors.get(selector) || null;
document.querySelectorAll = (selector) => ( document.querySelectorAll = (selector) => (
selector === "#assistantMessages [data-message-role]" ? [...messages.children] : [] selector === "#assistantMessages [data-message-role]"
? [...messages.children]
: messages.querySelectorAll(selector)
); );
const storage = new Map(); const storage = new Map();
@@ -181,6 +211,15 @@ function createHarness(events) {
sandbox.CognitiveAgentNetwork = { sandbox.CognitiveAgentNetwork = {
async request(url, options) { async request(url, options) {
requestCalls.push({ url, options }); requestCalls.push({ url, options });
if (url === "/api/mode-profiles" || url.startsWith("/api/mode-profiles/")) {
return {
ok: true,
status: 200,
async json() {
return { mode_profile: { id: "mode-saved", name: "午休模式" } };
},
};
}
return ndjsonResponse(events); return ndjsonResponse(events);
}, },
}; };
@@ -234,3 +273,177 @@ test("a completed NDJSON answer remains the authoritative visible response", asy
assert.equal(harness.completed.length, 1); assert.equal(harness.completed.length, 1);
assert.equal(harness.completed[0].error, null); assert.equal(harness.completed[0].error, null);
}); });
function modeDraftEvents(sourceRequestId, temperatureC) {
return [
{ type: "message.accepted", route: "vehicle_control", engine: "n7web-mode-builder" },
{
type: "action.completed",
success: true,
payload: {
status: "succeeded",
execution_mode: "dry_run",
orchestration_route: "mode_profile_create_preview",
mode_profile_draft: {
name: "午休模式",
instruction: `空调调整到${temperatureC}度`,
operation_count: 1,
source_request_id: sourceRequestId,
vehicle_id: "demo-vehicle",
},
actions: [{
domain: "climate",
capability: "climate.set_temperature",
target: { zone: "all" },
parameters: { temperature_c: temperatureC },
}],
},
},
{ type: "answer.completed", route: "vehicle_control", answer: "模式草稿已生成" },
];
}
test("a follow-up turn carries only the active authoritative draft reference", async () => {
const events = modeDraftEvents("req-draft-one", 24);
const harness = createHarness(events);
harness.input.value = "构建一个午休模式";
await harness.sandbox.CognitiveAgentAssistant.submit();
events.splice(0, events.length,
{ type: "message.accepted", route: "agent", engine: "codex-app-server" },
{ type: "answer.completed", route: "agent", answer: "已收到调整请求" },
);
harness.input.value = "把午休模式的空调温度调整到25度";
await harness.sandbox.CognitiveAgentAssistant.submit();
const request = JSON.parse(harness.requestCalls[1].options.body);
assert.equal(request.mode_profile_draft_context.name, "午休模式");
assert.equal(request.mode_profile_draft_context.source_request_id, "req-draft-one");
assert.equal(request.mode_profile_draft_context.vehicle_id, "demo-vehicle");
assert.equal(Object.hasOwn(request.mode_profile_draft_context, "actions"), false);
});
test("ordinary questions and updates for another mode do not carry the draft reference", async () => {
const events = modeDraftEvents("req-draft-scoped", 24);
const harness = createHarness(events);
harness.input.value = "构建一个午休模式";
await harness.sandbox.CognitiveAgentAssistant.submit();
events.splice(0, events.length,
{ type: "message.accepted", route: "agent", engine: "codex-app-server" },
{ type: "answer.completed", route: "agent", answer: "普通回答" },
);
harness.input.value = "空调制冷原理是什么";
await harness.sandbox.CognitiveAgentAssistant.submit();
harness.input.value = "把露营模式的空调温度调整到25度";
await harness.sandbox.CognitiveAgentAssistant.submit();
const ordinary = JSON.parse(harness.requestCalls[1].options.body);
const anotherMode = JSON.parse(harness.requestCalls[2].options.body);
assert.equal(Object.hasOwn(ordinary, "mode_profile_draft_context"), false);
assert.equal(Object.hasOwn(anotherMode, "mode_profile_draft_context"), false);
});
test("device action predicates carry context while mode questions stay ordinary", async () => {
const events = modeDraftEvents("req-draft-actions", 24);
const harness = createHarness(events);
harness.input.value = "构建一个午休模式";
await harness.sandbox.CognitiveAgentAssistant.submit();
events.splice(0, events.length,
{ type: "message.accepted", route: "agent", engine: "codex-app-server" },
{ type: "answer.completed", route: "agent", answer: "已处理" },
);
harness.input.value = "请帮我把午休模式里的遮阳帘关闭";
await harness.sandbox.CognitiveAgentAssistant.submit();
harness.input.value = "把午休模式的车窗全关";
await harness.sandbox.CognitiveAgentAssistant.submit();
harness.input.value = "午休模式的空调设置有哪些?";
await harness.sandbox.CognitiveAgentAssistant.submit();
const deviceAction = JSON.parse(harness.requestCalls[1].options.body);
const secondDeviceAction = JSON.parse(harness.requestCalls[2].options.body);
const ordinaryQuestion = JSON.parse(harness.requestCalls[3].options.body);
assert.equal(deviceAction.mode_profile_draft_context.source_request_id, "req-draft-actions");
assert.equal(secondDeviceAction.mode_profile_draft_context.source_request_id, "req-draft-actions");
assert.equal(Object.hasOwn(ordinaryQuestion, "mode_profile_draft_context"), false);
});
test("a replacement draft disables the stale same-name card", async () => {
const events = modeDraftEvents("req-draft-old", 24);
const harness = createHarness(events);
harness.input.value = "构建一个午休模式";
await harness.sandbox.CognitiveAgentAssistant.submit();
events.splice(0, events.length, ...modeDraftEvents("req-draft-new", 25));
harness.input.value = "把午休模式的空调温度调整到25度";
await harness.sandbox.CognitiveAgentAssistant.submit();
const oldCard = harness.messages.querySelector(
'[data-assistant-mode-draft="req-draft-old"]',
);
const newCard = harness.messages.querySelector(
'[data-assistant-mode-draft="req-draft-new"]',
);
assert.ok(oldCard);
assert.ok(newCard);
assert.equal(oldCard.classList.contains("is-superseded"), true);
oldCard.querySelectorAll("button").forEach((button) => assert.equal(button.disabled, true));
assert.equal(
oldCard.children.at(-1).textContent,
"已由下方最新草稿替换",
);
});
test("saving the replacement card uses its latest request and clears follow-up context", async () => {
const events = modeDraftEvents("req-draft-before-save", 24);
const harness = createHarness(events);
harness.input.value = "构建一个午休模式";
await harness.sandbox.CognitiveAgentAssistant.submit();
events.splice(0, events.length, ...modeDraftEvents("req-draft-after-edit", 25));
harness.input.value = "把午休模式的空调温度调整到25度";
await harness.sandbox.CognitiveAgentAssistant.submit();
const latestCard = harness.messages.querySelector(
'[data-assistant-mode-draft="req-draft-after-edit"]',
);
latestCard.querySelectorAll("button")[0].dispatchEvent({ type: "click" });
await new Promise((resolve) => setTimeout(resolve, 0));
const saveCall = harness.requestCalls.find((call) => call.url === "/api/mode-profiles");
assert.ok(saveCall);
assert.equal(JSON.parse(saveCall.options.body).source_request_id, "req-draft-after-edit");
events.splice(0, events.length,
{ type: "message.accepted", route: "agent", engine: "codex-app-server" },
{ type: "answer.completed", route: "agent", answer: "已保存后的回答" },
);
harness.input.value = "把午休模式的空调温度调整到26度";
await harness.sandbox.CognitiveAgentAssistant.submit();
const finalStreamCall = harness.requestCalls.filter(
(call) => call.url === "/api/assistant/stream",
).at(-1);
assert.equal(
Object.hasOwn(JSON.parse(finalStreamCall.options.body), "mode_profile_draft_context"),
false,
);
});
test("starting a new conversation clears the active draft reference", async () => {
const events = modeDraftEvents("req-draft-reset", 24);
const harness = createHarness(events);
harness.input.value = "构建一个午休模式";
await harness.sandbox.CognitiveAgentAssistant.submit();
const reset = new MockElement("button");
reset.id = "newConversation";
harness.sandbox.document.dispatchEvent({ type: "click", target: reset });
events.splice(0, events.length,
{ type: "message.accepted", route: "agent", engine: "codex-app-server" },
{ type: "answer.completed", route: "agent", answer: "新的对话" },
);
harness.input.value = "普通问题";
await harness.sandbox.CognitiveAgentAssistant.submit();
const request = JSON.parse(harness.requestCalls[1].options.body);
assert.equal(Object.hasOwn(request, "mode_profile_draft_context"), false);
});
+54 -5
View File
@@ -13,15 +13,15 @@ def test_console_contains_ai_assistant_surface():
assert 'aria-label="AI助手对话"' in html assert 'aria-label="AI助手对话"' in html
assert 'placeholder="向 AI助手提问或交代工作"' in html assert 'placeholder="向 AI助手提问或交代工作"' in html
assert 'aria-label="发送消息"' in html assert 'aria-label="发送消息"' in html
assert 'href="/assistant-chat.css?v=codex-assistant-22"' in html assert 'href="/assistant-chat.css?v=codex-assistant-23"' in html
assert 'src="/assistant-chat.js?v=codex-assistant-22"' in html assert 'src="/assistant-chat.js?v=codex-assistant-23"' in html
def test_codex_assistant_assets_are_versioned_together_in_the_service_worker(): def test_codex_assistant_assets_are_versioned_together_in_the_service_worker():
service_worker = (ROOT / "apps/web-console/service-worker.js").read_text(encoding="utf-8") service_worker = (ROOT / "apps/web-console/service-worker.js").read_text(encoding="utf-8")
assert 'const CACHE_VERSION = "cognitive-agent-pad-v271"' in service_worker assert 'const CACHE_VERSION = "cognitive-agent-pad-v272"' in service_worker
assert '"/assistant-chat.css?v=codex-assistant-22"' in service_worker assert '"/assistant-chat.css?v=codex-assistant-23"' in service_worker
assert '"/assistant-chat.js?v=codex-assistant-22"' in service_worker assert '"/assistant-chat.js?v=codex-assistant-23"' in service_worker
def test_codex_frontend_uses_one_streaming_endpoint_and_agent_work_events(): def test_codex_frontend_uses_one_streaming_endpoint_and_agent_work_events():
@@ -157,6 +157,55 @@ def test_assistant_mode_creation_previews_then_saves_to_the_compact_mode_strip()
assert ".assistant-mode-created" in css assert ".assistant-mode-created" in css
def test_assistant_carries_only_the_active_mode_draft_reference_to_follow_up_turns():
assistant = (ROOT / "apps/web-console/assistant-chat.js").read_text(encoding="utf-8")
request_body = assistant.split("function requestBody(message)", 1)[1].split(
"async function readErrorResponse", 1
)[0]
context_flow = assistant.split("function sanitizeModeDraftContext", 1)[1].split(
"function loadStoredMessages", 1
)[0]
assert (
'const ACTIVE_MODE_DRAFT_STORAGE_KEY = "cognitive-agent.active-mode-draft"'
in assistant
)
assert "mode_profile_draft_context: modeProfileDraftContext" in request_body
assert "requestModeDraftContext(message)" in request_body
assert "name: context.name" in context_flow
assert "source_request_id: context.source_request_id" in context_flow
assert "vehicle_id: context.vehicle_id" in context_flow
assert "actions" not in context_flow
assert "rememberModeDraftContext(draft)" in assistant
assert "activeModeDraftContext = loadActiveModeDraftContext()" in assistant
assert "function isModeDraftUpdateRequest(message, context)" in assistant
assert "normalizeModeName(matched[1]) === normalizeModeName(context.name)" in assistant
def test_latest_same_name_mode_draft_retires_stale_card_and_lifecycle_clears_context():
assistant = (ROOT / "apps/web-console/assistant-chat.js").read_text(encoding="utf-8")
css = (ROOT / "apps/web-console/assistant-chat.css").read_text(encoding="utf-8")
replace_flow = assistant.split("function supersedeModeDraftCards", 1)[1].split(
"function appendModeDraftState", 1
)[0]
save_flow = assistant.split("async function saveModeProfileDraft", 1)[1].split(
"async function tryModeProfileDraft", 1
)[0]
new_conversation_flow = assistant.split(
'if (!target?.closest("#newConversation")) return;', 1
)[1].split("}, true);", 1)[0]
assert "card.dataset.assistantModeName !== nextName" in replace_flow
assert "card.dataset.assistantModeDraft === nextRequestId" in replace_flow
assert 'card.classList.add("is-superseded")' in replace_flow
assert "button.disabled = true" in replace_flow
assert "已由下方最新草稿替换" in replace_flow
assert "clearActiveModeDraftContext(draft.source_request_id)" in save_flow
assert "clearActiveModeDraftContext()" in new_conversation_flow
assert ".assistant-mode-draft.is-superseded" in css
assert ".assistant-mode-draft-status.superseded" in css
def test_assistant_composer_grows_to_a_cap_then_scrolls_its_own_content(): def test_assistant_composer_grows_to_a_cap_then_scrolls_its_own_content():
assistant = (ROOT / "apps/web-console/assistant-chat.js").read_text(encoding="utf-8") assistant = (ROOT / "apps/web-console/assistant-chat.js").read_text(encoding="utf-8")
css = (ROOT / "apps/web-console/assistant-chat.css").read_text(encoding="utf-8") css = (ROOT / "apps/web-console/assistant-chat.css").read_text(encoding="utf-8")
+177
View File
@@ -341,6 +341,7 @@ def _stream(
execute_control: Callable[..., dict[str, object]] | None = None, execute_control: Callable[..., dict[str, object]] | None = None,
capability_snapshot: dict[str, object] | None = None, capability_snapshot: dict[str, object] | None = None,
capability_context: dict[str, object] | None = None, capability_context: dict[str, object] | None = None,
mode_profile_draft_context: dict[str, object] | None = None,
) -> tuple[dict[str, object], list[dict[str, object]]]: ) -> tuple[dict[str, object], list[dict[str, object]]]:
events: list[dict[str, object]] = [] events: list[dict[str, object]] = []
result = assistant.stream( result = assistant.stream(
@@ -354,6 +355,7 @@ def _stream(
control_terms=CONTROL_TERMS, control_terms=CONTROL_TERMS,
capability_snapshot=capability_snapshot, capability_snapshot=capability_snapshot,
capability_context=capability_context, capability_context=capability_context,
mode_profile_draft_context=mode_profile_draft_context,
) )
return result, events return result, events
@@ -426,6 +428,181 @@ def test_registered_name_only_mode_keeps_the_fast_local_route_without_codex() ->
assert events[0]["engine"] == "n7web-mode-builder" assert events[0]["engine"] == "n7web-mode-builder"
def test_pending_mode_draft_update_uses_fast_configuration_route_without_codex() -> None:
client = FakeCodexClient(available=False)
assistant = CodexAssistantService(client)
calls: list[str] = []
result_payload = _sample_mode_preview_result(
"小憩模式",
generation_source="draft_revision",
existing_mode_id="nap",
)
draft = result_payload["mode_profile_draft"]
assert isinstance(draft, dict)
draft.update(
{
"source_request_id": "req-mode-revision",
"draft_revision": 2,
"revises_source_request_id": "req-mode-preview",
}
)
result, events = _stream(
assistant,
"把小憩模式的空调温度调整到25度",
execute_control=lambda instruction, _progress: (
calls.append(instruction) or result_payload
),
mode_profile_draft_context={
"name": "小憩模式",
"source_request_id": "req-mode-preview",
"vehicle_id": "denza_n7",
},
)
assert result["route"] == "vehicle_control"
assert result["success"] is True
assert calls == ["把小憩模式的空调温度调整到25度"]
assert client.turn_calls == []
assert events[0]["engine"] == "n7web-mode-builder"
assert "调整待保存" in result["answer"]
def test_active_draft_context_does_not_hijack_an_ordinary_question() -> None:
client = FakeCodexClient()
assistant = CodexAssistantService(client)
result, events = _stream(
assistant,
"解释一下空调内循环和外循环的区别",
mode_profile_draft_context={
"name": "小憩模式",
"source_request_id": "req-mode-preview",
"vehicle_id": "denza_n7",
},
)
assert result == {"route": "agent", "answer": "这是 Codex 的流式回答。"}
assert client.turn_calls[0]["text"] == "解释一下空调内循环和外循环的区别"
assert events[0]["engine"] == "codex-app-server"
def test_malformed_stale_draft_context_is_ignored_for_an_ordinary_question() -> None:
client = FakeCodexClient()
assistant = CodexAssistantService(client)
events: list[dict[str, object]] = []
result = assistant.stream(
"解释一下空调内循环和外循环的区别",
conversation_id="conv-stale-draft-context",
principal="alice",
history=[],
emit=lambda event: events.append(event),
execute_control=lambda _instruction, _progress: pytest.fail(
"vehicle control was unexpected"
),
control_terms=CONTROL_TERMS,
mode_profile_draft_context={"untrusted": "stale"},
)
assert result["route"] == "agent"
assert events[0]["engine"] == "codex-app-server"
def test_malformed_matching_draft_reference_is_rejected_before_control() -> None:
assistant = CodexAssistantService(FakeCodexClient(available=False))
with pytest.raises(ValueError, match="fields are invalid"):
assistant.stream(
"把小憩模式的空调温度调整到25度",
conversation_id="conv-invalid-draft-context",
principal="alice",
history=[],
emit=lambda _event: True,
execute_control=lambda _instruction, _progress: pytest.fail(
"vehicle control was unexpected"
),
control_terms=CONTROL_TERMS,
mode_profile_draft_context={"name": "小憩模式"},
)
def test_nonmatching_draft_context_does_not_claim_another_mode_update() -> None:
authorization = _authorize_vehicle_request(
"把观影模式的空调温度调整到25度",
CONTROL_TERMS,
{
"name": "小憩模式",
"source_request_id": "req-mode-preview",
"vehicle_id": "denza_n7",
},
)
# The stale card reference has been ignored. The sentence is still an
# explicit registered control under the legacy saved-mode update route.
assert authorization.reason == "explicit_registered_control"
@pytest.mark.parametrize(
"message",
(
"把小憩模式的空调介绍一下",
"小憩模式的空调有什么作用",
"小憩模式的空调和普通空调有什么区别",
"小憩模式的空调设置有哪些?",
"小憩模式的空调该调几度",
"把小憩模式的空调调到25度合适吗",
"调整小憩模式的空调参数,列出来",
),
)
def test_device_word_air_conditioner_does_not_fake_a_draft_update_verb(
message: str,
) -> None:
authorization = _authorize_vehicle_request(
message,
CONTROL_TERMS,
{
"name": "小憩模式",
"source_request_id": "req-mode-preview",
"vehicle_id": "denza_n7",
},
)
assert authorization.reason != "explicit_mode_profile_draft_update"
@pytest.mark.parametrize(
"message",
(
"修改小憩模式的空调设置是什么",
"小憩模式的空调该调几度",
"把小憩模式的空调调到25度合适吗",
"调整小憩模式的空调参数,列出来",
"小憩模式的空调调到25度会怎样",
),
)
def test_mode_profile_question_with_control_words_never_calls_vehicle_executor(
message: str,
) -> None:
assistant = CodexAssistantService(FakeCodexClient())
result, events = _stream(
assistant,
message,
execute_control=lambda _instruction, _progress: pytest.fail(
"mode-profile question reached vehicle executor"
),
mode_profile_draft_context={
"name": "小憩模式",
"source_request_id": "req-mode-preview",
"vehicle_id": "denza_n7",
},
)
assert result["route"] == "agent"
assert events[0]["engine"] == "codex-app-server"
def test_unknown_name_only_mode_uses_codex_and_overrides_model_execution_claim() -> None: def test_unknown_name_only_mode_uses_codex_and_overrides_model_execution_claim() -> None:
client = FakeCodexClient() client = FakeCodexClient()
assistant = CodexAssistantService(client) assistant = CodexAssistantService(client)
@@ -36,10 +36,10 @@ def test_energy_monitor_assets_are_versioned_and_cached() -> None:
): ):
assert asset in html assert asset in html
assert f'"{asset}"' in service_worker assert f'"{asset}"' in service_worker
assert html.index("/assistant-chat.js?v=codex-assistant-22") < html.index( assert html.index("/assistant-chat.js?v=codex-assistant-23") < html.index(
"/energy-monitor.js?v=energy-monitor-4" "/energy-monitor.js?v=energy-monitor-4"
) )
assert 'const CACHE_VERSION = "cognitive-agent-pad-v271"' in service_worker assert 'const CACHE_VERSION = "cognitive-agent-pad-v272"' in service_worker
def test_energy_monitor_uses_the_fifth_shared_dock_icon_and_starts_closed() -> None: def test_energy_monitor_uses_the_fifth_shared_dock_icon_and_starts_closed() -> None:
+1 -1
View File
@@ -48,7 +48,7 @@ def test_theme_asset_versions_are_consistent_with_the_pwa_shell() -> None:
): ):
assert asset in html assert asset in html
assert f'"{asset}"' in service_worker assert f'"{asset}"' in service_worker
assert 'const CACHE_VERSION = "cognitive-agent-pad-v271"' in service_worker assert 'const CACHE_VERSION = "cognitive-agent-pad-v272"' in service_worker
def test_vehicle_twin_icons_and_nested_overlays_use_semantic_tokens() -> None: def test_vehicle_twin_icons_and_nested_overlays_use_semantic_tokens() -> None: