feat: improve mode and peripheral orchestration

This commit is contained in:
Miayui
2026-09-21 00:13:47 +08:00
parent 89707a42e2
commit 448c8b31a0
39 changed files with 3068 additions and 277 deletions
+19 -1
View File
@@ -17,6 +17,8 @@ function createConversationId() {
return createOpaqueId("conv");
}
const HARDWARE_CLIENT_ID_PATTERN = /^hwc-[a-z0-9-]{8,80}$/;
function loadConversationId() {
const stored = sessionStorage.getItem("cognitive-agent.conversation-id");
// A browser runtime restart is a new virtual run. Do not reuse the old
@@ -33,12 +35,19 @@ function loadConversationId() {
function loadHardwareClientId() {
const key = "cognitive-agent.hardware-client-id";
const stored = localStorage.getItem(key);
if (/^hwc-[a-z0-9-]{8,80}$/.test(stored || "")) return stored;
if (HARDWARE_CLIENT_ID_PATTERN.test(stored || "")) return stored;
const clientId = createOpaqueId("hwc");
localStorage.setItem(key, clientId);
return clientId;
}
function validatedHardwareClientId() {
if (!HARDWARE_CLIENT_ID_PATTERN.test(state.hardwareClientId || "")) {
state.hardwareClientId = loadHardwareClientId();
}
return state.hardwareClientId;
}
function loadControlTransport() {
const stored = localStorage.getItem("cognitive-agent.control-transport");
return ["wired_can", "wireless_mqtt"].includes(stored) ? stored : "wired_can";
@@ -5131,6 +5140,7 @@ const MODE_DOMAIN_LABELS = {
window: "车窗",
front_light: "车灯",
vehicle_status: "车辆状态",
android: "车机媒体",
};
const MODE_TARGET_LABELS = {
@@ -5140,6 +5150,7 @@ const MODE_TARGET_LABELS = {
front_right: "右前 / 副驾",
rear_left: "左后",
rear_right: "右后",
headunit: "车机",
front_row: "前排",
rear_row: "后排",
vehicle: "全车",
@@ -8241,6 +8252,7 @@ const CONFIG_DOMAIN_META = {
window: { label: "车窗", icon: "panels-top-left", className: "generic" },
outlet: { label: "电动风口", icon: "wind", className: "climate" },
interior_light: { label: "车内灯", icon: "lightbulb", className: "ambient" },
android: { label: "车机媒体", icon: "clapperboard", className: "generic" },
};
const CONFIG_CAPABILITY_LABELS = {
@@ -8286,6 +8298,7 @@ const CONFIG_CAPABILITY_LABELS = {
"roof.set_shade_position": "设置遮阳帘位置",
"roof.set_shade_motion": "控制遮阳帘动作",
"roof.set_automatic_dimming": "设置自动调光",
"android.launch_app": "打开视频媒体",
};
const CONFIG_PARAMETER_LABELS = {
@@ -8309,6 +8322,8 @@ const CONFIG_PARAMETER_LABELS = {
outlet_mode: "出风模式",
mode: "工作模式",
mode_id: "固件模式编号",
package_name: "应用包名",
activity_name: "启动页面",
};
function makeConfigElement(tagName, className, textValue) {
@@ -9412,6 +9427,9 @@ window.CognitiveAgentConsole = {
getExecutionMode() {
return state.executionMode;
},
getHardwareClientId() {
return validatedHardwareClientId();
},
getConversationId() {
return state.conversationId;
},
+51 -6
View File
@@ -548,6 +548,7 @@
"door.set_rear_lock": "后排门锁",
"door.set_vehicle_lock": "整车门锁",
"infrared.emit": "红外外设",
"android.launch_app": "打开视频媒体",
};
function assistantControlCategory(operation = {}, fallback = "控制指令") {
@@ -745,6 +746,22 @@
const brightness = numericValue(parameters.brightness_level);
if (brightness !== null) return `${brightness}级`;
const level = numericValue(parameters.level ?? parameters.fan_level);
const capability = String(item.operation?.capability || "");
if (capability === "android.launch_app") {
const packageName = String(parameters.package_name || "").trim();
const activityName = String(parameters.activity_name || "").trim();
return [
packageName ? `应用 ${packageName}` : "视频应用",
activityName ? `页面 ${activityName}` : "",
].filter(Boolean).join(" · ");
}
if (
level !== null
&& ["seat.set_driver_ventilation_native", "seat.set_driver_heating_native"]
.includes(capability)
) {
return ({ 1: "3档", 2: "1档" })[level] || "档位待确认";
}
if (level !== null) return `${level}档`;
const state = parameters.motion
?? parameters.state
@@ -769,6 +786,7 @@
if (capability.startsWith("seat.") || category.includes("座椅")) return "armchair";
if (capability.startsWith("door.") || category.includes("门锁")) return "lock-keyhole";
if (capability === "infrared.emit") return "radio-tower";
if (capability === "android.launch_app") return "clapperboard";
return "binary";
}
@@ -985,6 +1003,7 @@
"ambient_light.set_power": "氛围灯",
"ambient_light.set_mode": "氛围灯模式",
"infrared.emit": "红外外设",
"android.launch_app": "打开视频媒体",
};
const zeroGravityIndexes = actions
@@ -1166,9 +1185,10 @@
? `${params.position_pct}%${params.angle_deg != null ? `(${params.angle_deg}°)` : ""}`
: motionLabel(params.state || params.mode_id);
if (["seat.set_driver_ventilation_native", "seat.set_driver_heating_native"].includes(capability)) {
// CH1 driver controls encode high as 1 and low as 2; passenger
// comfort levels use a different scale and must not reuse this map.
const level = { 1: "高档", 2: "低档" }[params.level ?? 2] || "档位待确认";
// CH1 encodes high as 1 and low as 2 internally. Present the
// occupant-facing three-speed scale instead of leaking CAN enums.
const level = { 1: "3档(高)", 2: "1档(低)" }[params.level ?? 2]
|| "档位待确认";
value = params.state === "off" ? "关闭"
: params.state === "on" ? `开启 · ${level}` : "开关状态待确认";
} else if (["seat.set_ventilation", "seat.set_heating"].includes(capability)) {
@@ -1529,10 +1549,23 @@
return /(?:构建|创建|新建|新增|生成|添加)(?:一下)?(?:一个|个|一套|套)?[^,。!?!?]{1,64}?模式/.test(compact);
}
function validatedHardwareClientId() {
const sharedClientId = window.CognitiveAgentConsole?.getHardwareClientId?.();
let storedClientId = null;
try {
storedClientId = localStorage.getItem("cognitive-agent.hardware-client-id");
} catch (_error) {
// The shared console identity remains authoritative when storage is unavailable.
}
return [sharedClientId, storedClientId]
.map((value) => String(value || "").trim())
.find((value) => /^hwc-[a-z0-9-]{8,80}$/.test(value)) || null;
}
function requestBody(message) {
const configuredExecutionMode = window.CognitiveAgentConsole?.getExecutionMode?.() || "dry_run";
const controlTransport = localStorage.getItem("cognitive-agent.control-transport") || "wired_can";
const hardwareClientId = localStorage.getItem("cognitive-agent.hardware-client-id");
const hardwareClientId = validatedHardwareClientId();
const vehicleId = $("#vehicleSelect")?.value || null;
const modeProfileDraftContext = requestModeDraftContext(message);
const modeConfigurationPreview = Boolean(modeProfileDraftContext)
@@ -1967,6 +2000,11 @@
&& result?.schedule_execution?.status === "running";
const dryRun = result?.execution_mode === "dry_run";
const waitingForFeedback = result?.execution_outcome === "submitted_unverified";
const delegatedCount = Array.isArray(result?.deferred_requests)
? result.deferred_requests.filter(
(item) => item?.execution_backend === "n7_vehicle_transfer",
).length
: 0;
let content = `${modeName}执行未完成,请查看控制指令状态。`;
let statusText = "功能模式执行未完成";
@@ -1991,8 +2029,15 @@
: "功能模式执行已完成";
statusState = "success";
} else if (status === "partially_succeeded") {
content = `${modeName}仅部分完成,请查看未完成的控制指令。`;
statusText = "功能模式部分完成";
content = delegatedCount > 0
? `${modeName}指令已提交,未由 PC 执行的 ${delegatedCount} 项动作将由车机继续处理。`
: waitingForFeedback
? `${modeName}指令已提交,正在等待车辆状态确认。`
: `${modeName}执行结果已更新,请查看各控制指令状态。`;
statusText = waitingForFeedback || delegatedCount > 0
? "功能模式指令已提交"
: "功能模式执行结果已更新";
statusState = waitingForFeedback || delegatedCount > 0 ? "working" : "success";
}
setMessageContent(message, content, { final: true });
+14 -8
View File
@@ -571,9 +571,20 @@
const explicitlyBlocked = executionStatus === "blocked"
|| verificationStatus === "blocked"
|| Boolean(telemetry.authorization_reason);
const executionFailed = executionStatus === "failed"
// Closed-loop evidence is authoritative. Some MQTT window executors
// retain a legacy `failed` operation status even after fresh 0x388
// feedback confirms the requested target; do not render that as a
// failed dispatch once the physical result is verified.
const verified = !aborted && (
verificationStatus === "verified_success"
|| telemetry.physical_effect_confirmed === true
|| telemetry.feedback_match === true
);
const executionFailed = !verified && (
executionStatus === "failed"
|| explicitlyBlocked
|| verificationStatus === "failed_or_blocked";
|| verificationStatus === "failed_or_blocked"
);
const missingWindowFeedback = /0x388/i.test(failureMessage) && (
/no fresh valid/i.test(failureMessage)
|| /未收到.*(?:有效|新鲜).*车窗位置/.test(failureMessage)
@@ -591,11 +602,6 @@
|| /thermal protection|热保护/i.test(failureMessage)
);
const dispatchFailed = executionFailed && !submitted && !safetyBlocked;
const verified = !executionFailed && !aborted && (
verificationStatus === "verified_success"
|| telemetry.physical_effect_confirmed === true
|| telemetry.feedback_match === true
);
const deliveryState = aborted
? "aborted"
: safetyBlocked
@@ -1325,7 +1331,7 @@
}
const levelOptions = [
["off", "关闭"], ["high", "1 档(高)"], ["low", "2 档(低)"],
["off", "关闭"], ["low", "1 档(低)"], ["high", "3 档(高)"],
];
const toggleOptions = [["off", "关闭"], ["on", "开启"]];
const massageModeOptions = [
+4 -4
View File
@@ -39,13 +39,13 @@
<script src="/vendor/three/0.147.0/fflate.min.js" defer></script>
<script src="/vendor/three/0.147.0/FBXLoader.js" defer></script>
<script src="/network.js?v=android-pad-2" defer></script>
<script src="/app.js?v=dual-seat-145-n7-dispatch" defer></script>
<script src="/app.js?v=dual-seat-147-video-media" defer></script>
<script src="/speech-input.js?v=sensevoice-13" defer></script>
<script type="module" src="/gesture-control.js?v=android-pad-9"></script>
<script src="/pwa.js?v=android-pad-3" defer></script>
<script src="/console-v2.js?v=local-first-77-performance" defer></script>
<script src="/self-extension.js?v=nfc-mqtt-56" defer></script>
<script src="/assistant-chat.js?v=codex-assistant-35" defer></script>
<script src="/console-v2.js?v=local-first-79-cinema-status" defer></script>
<script src="/self-extension.js?v=nfc-persistent-57" defer></script>
<script src="/assistant-chat.js?v=codex-assistant-39-video-media" defer></script>
<script src="/energy-monitor.js?v=energy-monitor-5-performance" defer></script>
<script src="/firmware-workbench-entry.js?v=3" defer></script>
</head>
+83 -19
View File
@@ -382,6 +382,8 @@
};
let peripheralCatalogRevision = null;
let peripheralCatalogSyncQueue = Promise.resolve();
let durablePeripheralCatalog = [];
let durablePeripheralCatalogHydrated = false;
let petCardSnapshot = {
revision: -1,
cards: [],
@@ -1973,11 +1975,40 @@
}).filter((item) => item.device_id && item.card_uid && validSlot(item.slot));
}
function applyDurablePeripheralCatalog(
catalog,
revision = null,
{ render = true } = {},
) {
const nextCatalog = (Array.isArray(catalog) ? catalog : [])
.filter((item) => item && typeof item === "object"
&& item.id && item.device_id && normalizeCardUid(item.card_uid)
&& validSlot(item.slot))
.map((item) => ({ ...item, card_uid: normalizeCardUid(item.card_uid) }));
const changed = JSON.stringify(durablePeripheralCatalog) !== JSON.stringify(nextCatalog)
|| !durablePeripheralCatalogHydrated;
durablePeripheralCatalog = nextCatalog;
durablePeripheralCatalogHydrated = true;
if (revision != null && String(revision).trim()) {
peripheralCatalogRevision = String(revision).trim();
}
if (changed) unifiedExtensionDirty = true;
if (render && changed) renderUnifiedExtensionView?.({ force: true });
return changed;
}
async function hydratePeripheralCatalog() {
const payload = await api("peripherals");
peripheralCatalogRevision = payload.catalog_revision || peripheralCatalogRevision;
const catalog = Array.isArray(payload.catalog) ? payload.catalog : [];
if (!catalog.length) return 0;
applyDurablePeripheralCatalog(
catalog,
payload.catalog_revision,
{ render: false },
);
if (!catalog.length) {
renderAll();
return 0;
}
const restoredDeviceIds = new Set();
catalog.forEach((item) => {
if (!item || typeof item !== "object" || !item.id || !validSlot(item.slot)) return;
@@ -3806,12 +3837,13 @@
}
async function syncPeripheralCatalog(device = null, { throwOnError = false } = {}) {
const nextCatalog = peripheralCatalog();
const operation = peripheralCatalogSyncQueue
.catch(() => null)
.then(() => api("display-sync", {
method: "POST",
body: JSON.stringify({
peripherals: peripheralCatalog(),
peripherals: nextCatalog,
device,
catalog_revision: peripheralCatalogRevision,
}),
@@ -3819,7 +3851,10 @@
peripheralCatalogSyncQueue = operation.catch(() => null);
try {
const result = await operation;
peripheralCatalogRevision = result.catalog_revision || peripheralCatalogRevision;
applyDurablePeripheralCatalog(
nextCatalog,
result.catalog_revision,
);
return result;
} catch (error) {
console.warn("外设能力同步失败", error);
@@ -4169,6 +4204,18 @@
status: item.status,
enabled: item.enabled,
})),
durableCatalogHydrated: durablePeripheralCatalogHydrated,
durableCatalog: durablePeripheralCatalog.map((item) => ({
id: item.id,
deviceId: item.device_id,
deviceName: item.device_name,
deviceType: item.device_type,
typeLabel: item.type_label,
action: item.action,
cardUid: item.card_uid,
status: item.status,
enabled: item.enabled,
})),
lastAction: snapshot?.extension?.last_action ? {
command: snapshot.extension.last_action.command,
type: snapshot.extension.last_action.type,
@@ -4249,27 +4296,28 @@
const sharedCatalog = Array.isArray(snapshot?.extension?.catalog)
? snapshot.extension.catalog
: [];
const mergedCatalog = [...sharedCatalog, ...peripheralCatalog()]
const fallbackCatalog = [...sharedCatalog, ...peripheralCatalog()]
.filter((item, index, catalog) => {
const identity = item.id || `${item.device_id}:${item.action}`;
return catalog.findIndex((candidate) => (
candidate.id || `${candidate.device_id}:${candidate.action}`
) === identity) === index;
});
const persistentCatalog = (
durablePeripheralCatalogHydrated
? durablePeripheralCatalog
: fallbackCatalog
).filter((item) => (
normalizeCardUid(item.card_uid)
&& item.status === "learned"
));
const nfcReader = nfcSnapshot.reader || {};
const nfcCard = nfcSnapshot.card;
const sensedCardUid = normalizeCardUid(nfcCard?.uid);
const nfcDevice = linkedDeviceForCard(sensedCardUid);
const nfcPet = linkedPetForCard(sensedCardUid);
const activeCatalog = sensedCardUid
? mergedCatalog.filter((item) => (
normalizeCardUid(item.card_uid) === sensedCardUid
&& item.status === "learned"
&& item.enabled !== false
))
: [];
const grouped = new Map();
activeCatalog.forEach((item) => {
persistentCatalog.forEach((item) => {
const deviceId = item.device_id || "external-device";
if (!grouped.has(deviceId)) grouped.set(deviceId, {
id: deviceId,
@@ -4278,7 +4326,10 @@
cardUid: item.card_uid || null,
actions: [],
});
grouped.get(deviceId).actions.push(item.action || item.name || "已学习能力");
const actionName = item.action || item.name || "已学习能力";
grouped.get(deviceId).actions.push(
item.enabled === false ? `${actionName}(已停用)` : actionName,
);
});
const devices = [...grouped.values()];
const lastAction = snapshot?.extension?.last_action;
@@ -4294,8 +4345,10 @@
: petCardSnapshot.publisher?.state === "burst_failed"
? "宠物消息发送失败"
: "已感应宠物 NFC"
: devices.length
: nfcDevice
? "已感应 NFC 外设"
: devices.length
? `${devices.length} 个外设已绑定`
: sensedCardUid
? "当前 NFC 暂无已授权命令"
: nfcReader.online
@@ -4361,11 +4414,11 @@
<div>${device.actions.slice(0, 8).map((action) => `<span>${escapeHtml(action)}</span>`).join("")}</div>
</article>`).join("") : nfcPet ? "" : `
<div class="v2-extension-empty">
<i data-lucide="${sensedCardUid ? "badge-alert" : "scan-line"}"></i><div><strong>${sensedCardUid ? "当前 NFC 暂无已授权命令" : "当前未感应到外设"}</strong><span>${sensedCardUid ? "请检查设备 ID 绑定及命令复选框" : "将已绑定设备 ID 的 NFC 卡靠近读卡器后显示命令"}</span></div>
<i data-lucide="link-2-off"></i><div><strong>当前没有已绑定外设</strong><span>外设绑定 NFC 后将持续显示,解除绑定后移除</span></div>
</div>`}
</div>
<footer class="v2-extension-linkage" data-active="${String(Boolean(visibleLastAction))}">
<i data-lucide="workflow"></i><div><small>最近联动</small><strong>${escapeHtml(visibleLastAction?.command || visibleLastAction?.type || "等待 NFC 外设感应")}</strong></div>
<i data-lucide="workflow"></i><div><small>最近联动</small><strong>${escapeHtml(visibleLastAction?.command || visibleLastAction?.type || "暂无外设联动")}</strong></div>
<span>${escapeHtml(visibleLastAction ? (lastResult?.status || lastResult?.message || "STANDBY") : "STANDBY")}</span>
</footer>`;
unifiedExtensionFingerprint = renderFingerprint;
@@ -4377,6 +4430,17 @@
window.addEventListener("cognitive-agent:display-sync", (event) => {
snapshot = event.detail || null;
const extensionSnapshot = snapshot?.extension;
if (
Array.isArray(extensionSnapshot?.catalog)
&& extensionSnapshot?.result?.catalog_revision
) {
applyDurablePeripheralCatalog(
extensionSnapshot.catalog,
extensionSnapshot.result.catalog_revision,
{ render: false },
);
}
if (snapshot?.extension_health?.nfc) {
applyNfcSnapshot(snapshot.extension_health.nfc, { announce: false });
}
@@ -4402,8 +4466,8 @@
void hydratePeripheralCatalog()
.catch((error) => {
console.warn("恢复后端红外能力目录失败,将保留浏览器本地目录。", error);
})
.finally(() => syncPeripheralCatalog());
return syncPeripheralCatalog();
});
return true;
}
+5 -5
View File
@@ -1,4 +1,4 @@
const CACHE_VERSION = "cognitive-agent-pad-v329-mode-preview";
const CACHE_VERSION = "cognitive-agent-pad-v334-video-media";
const SHELL_CACHE = `${CACHE_VERSION}-shell`;
const RUNTIME_CACHE = `${CACHE_VERSION}-runtime`;
const APP_SHELL = [
@@ -6,18 +6,18 @@ const APP_SHELL = [
"/index.html",
"/styles.css?v=dual-seat-24-dialog-size",
"/network.js?v=android-pad-2",
"/app.js?v=dual-seat-145-n7-dispatch",
"/app.js?v=dual-seat-147-video-media",
"/gesture-control.js?v=android-pad-9",
"/pwa.js?v=android-pad-3",
"/console-v2.css?v=delivery-state-69",
"/console-v2.js?v=local-first-77-performance",
"/console-v2.js?v=local-first-79-cinema-status",
"/self-extension.css?v=pet-settings-54-dialog-size",
"/self-extension.js?v=nfc-mqtt-56",
"/self-extension.js?v=nfc-persistent-57",
"/speech-input.css?v=sensevoice-7",
"/speech-input.js?v=sensevoice-13",
"/speech-audio-worklet.js?v=sensevoice-5",
"/assistant-chat.css?v=codex-assistant-30",
"/assistant-chat.js?v=codex-assistant-35",
"/assistant-chat.js?v=codex-assistant-39-video-media",
"/energy-monitor.css?v=energy-monitor-5-dialog-size",
"/energy-monitor.js?v=energy-monitor-5-performance",
"/ui-consistency.css?v=spatial-ui-8",
@@ -2465,8 +2465,19 @@ class AssistantCapabilityCatalog:
if isinstance(command_flow, Mapping)
else None
)
local_n7_transfer_actions = (
command_flow.get("n7_transfer_actions")
if isinstance(command_flow, Mapping)
else None
)
n7_transfer_ready = (
raw.get("source_topic") == "from_n7/scenes"
(
raw.get("source_topic") == "from_n7/scenes"
or (
isinstance(local_n7_transfer_actions, list)
and bool(local_n7_transfer_actions)
)
)
and isinstance(command_plans, Mapping)
and bool(command_plans)
)
@@ -726,6 +726,17 @@ def _level_matches(text: str) -> set[int]:
return values
def _driver_native_thermal_level_matches(text: str) -> set[int]:
"""Translate occupant-facing 1/3 gears to CH1 low/high enum values."""
values: set[int] = set()
if re.search(r"(?:1|一)\s*(?:档|级)|最低|低档|低速", text):
values.add(2)
if re.search(r"(?:3|三)\s*(?:档|级)|最高|高档|高速", text):
values.add(1)
return values
def _axis_number_matches(
text: str,
axis: str,
@@ -861,7 +872,16 @@ def _semantic_parameter_error(
return "temperature_c 没有原始文本数值依据"
for name in {"level", "brightness_level"} & values.keys():
levels = _level_matches(text)
levels = (
_driver_native_thermal_level_matches(text)
if name == "level"
and capability
in {
"seat.set_driver_ventilation_native",
"seat.set_driver_heating_native",
}
else _level_matches(text)
)
value = values[name]
if levels:
if not any(_numeric_equal(value, expected) for expected in levels):
@@ -2727,7 +2727,22 @@ class CodexAssistantService:
raise TypeError("vehicle control returned an invalid result")
safe_result = self._safe_vehicle_result(result)
action_state, successful, completion_text = _vehicle_action_state(result)
if mode_profile_preview:
if (
saved_mode_execution
and result.get("status") == "partially_succeeded"
):
successful = True
action_state = (
"simulated"
if result.get("execution_mode") != "hardware"
else "submitted"
)
completion_text = (
"功能模式模拟编排已完成"
if action_state == "simulated"
else "功能模式指令已提交"
)
elif mode_profile_preview:
successful = _mode_profile_result_succeeded(result)
action_state = "simulated" if successful else "failed"
completion_text = (
@@ -4232,6 +4247,7 @@ class CodexAssistantService:
"local_control_elapsed_ms",
"local_control_within_budget",
"natural_language_routing",
"parallel_dispatch",
):
if result.get(key) is not None:
payload[key] = CodexAssistantService._safe_result_value(
@@ -4444,6 +4460,11 @@ class CodexAssistantService:
}
if result.get("orchestration_route") is not None:
safe["orchestration_route"] = str(result["orchestration_route"])
if isinstance(result.get("parallel_dispatch"), Mapping):
safe["parallel_dispatch"] = CodexAssistantService._safe_result_value(
result["parallel_dispatch"],
secret=secret,
)
validation = result.get("validation")
if isinstance(validation, Sequence) and not isinstance(
validation,
@@ -17,6 +17,11 @@ from typing import Any
from cognitive_agent.application.extension_toy_publisher import (
normalize_extension_nfc_id,
)
from cognitive_agent.application.self_extension import (
is_peripheral_color_capability,
is_peripheral_power_off_capability,
is_peripheral_power_on_capability,
)
EXTENSION_CONTROL_TOPIC = "extension/ctl"
@@ -194,7 +199,7 @@ def authorize_extension_control_actions(
request: Mapping[str, Any],
catalog: object,
) -> list[dict[str, Any]]:
"""Authorize every action before any TCP command may be sent."""
"""Authorize every action and inject mandatory light power prerequisites."""
actions = request.get("actions")
if not isinstance(actions, list):
@@ -251,9 +256,86 @@ def authorize_extension_control_actions(
"action": deepcopy(dict(action)),
"capability_id": str(capability.get("id") or ""),
"extension_name": str(capability.get("device_name") or ""),
"capability": capability,
"auto_inserted": False,
"sequence_order": index * 2 + 1,
}
)
return authorized
allowed_by_device: dict[str, list[dict[str, Any]]] = {}
for capability in allowlist.values():
device_id = str(capability.get("device_id") or "").strip()
if device_id:
allowed_by_device.setdefault(device_id, []).append(capability)
expanded: list[dict[str, Any]] = []
powered_devices: set[str] = set()
next_index = len(actions)
for item in sorted(
authorized,
key=lambda value: (value["at_ms"], value["sequence_order"]),
):
capability = item["capability"]
device_id = str(capability.get("device_id") or "").strip()
if is_peripheral_power_on_capability(capability):
powered_devices.add(device_id)
elif is_peripheral_power_off_capability(capability):
powered_devices.discard(device_id)
elif (
is_peripheral_color_capability(capability)
and device_id not in powered_devices
):
prerequisite = next(
(
candidate
for candidate in sorted(
allowed_by_device.get(device_id, []),
key=lambda value: str(value.get("id") or ""),
)
if is_peripheral_power_on_capability(candidate)
),
None,
)
if prerequisite is None:
raise ExtensionControlAuthorizationError(
"extension/ctl color action requires an enabled, NFC-bound "
f"power-on action for the same device: {device_id}"
)
prerequisite_slot = int(prerequisite["slot"])
prerequisite_action = str(prerequisite.get("action") or "").strip()
expanded.append(
{
"index": next_index,
"at_ms": item["at_ms"],
"action": {
"type": "infrared_emit",
"at_ms": item["at_ms"],
"domain": "self_extension",
"capability": "infrared.emit",
"target_scope": device_id,
"operation": "emit",
"parameters": {
"action": prerequisite_action,
"slot": prerequisite_slot,
},
},
"capability_id": str(prerequisite.get("id") or ""),
"extension_name": str(
prerequisite.get("device_name") or ""
),
"capability": prerequisite,
"auto_inserted": True,
"dependency": "power_on_before_color",
"sequence_order": int(item["sequence_order"]) - 1,
}
)
next_index += 1
powered_devices.add(device_id)
expanded.append(item)
if len(expanded) > MAX_EXTENSION_CONTROL_ACTIONS:
raise ExtensionControlAuthorizationError(
"extension/ctl exceeds the action limit after adding power-on prerequisites"
)
return expanded
class ExtensionControlConsumer:
@@ -477,7 +559,14 @@ class ExtensionControlConsumer:
self._record_rejection(exc, request=request)
return
ordered_actions = sorted(actions, key=lambda item: (item["at_ms"], item["index"]))
ordered_actions = sorted(
actions,
key=lambda item: (
item["at_ms"],
item.get("sequence_order", item["index"]),
item["index"],
),
)
action_statuses = [
{
"index": authorized["index"],
@@ -488,6 +577,8 @@ class ExtensionControlConsumer:
"target_scope": authorized["action"]["target_scope"],
"action": authorized["action"]["parameters"]["action"],
"slot": authorized["action"]["parameters"]["slot"],
"auto_inserted": authorized.get("auto_inserted") is True,
"dependency": authorized.get("dependency"),
"state": "pending",
"state_label": "等待下发",
"state_detail": f"计划在接收后 {authorized['at_ms']} ms 下发",
@@ -583,6 +674,8 @@ class ExtensionControlConsumer:
"target_scope": authorized["action"]["target_scope"],
"action": authorized["action"]["parameters"]["action"],
"slot": authorized["action"]["parameters"]["slot"],
"auto_inserted": authorized.get("auto_inserted") is True,
"dependency": authorized.get("dependency"),
"transport": str(result.get("transport") or "tcp"),
"gateway_acknowledged": gateway_acknowledged,
"physical_state_confirmed": physical_state_confirmed,
@@ -267,6 +267,33 @@ def _scene_from_profile(
for operation, target, operation_ref in effective_operations
]
)
local_transfer_actions = (
profile.command_flow.get("n7_transfer_actions")
if not profile.sync_origin
and isinstance(profile.command_flow, dict)
and isinstance(profile.command_flow.get("n7_transfer_actions"), list)
else []
)
transfer_by_identity: dict[str, dict[str, Any]] = {}
for raw_action in local_transfer_actions:
if not isinstance(raw_action, dict):
continue
try:
transfer_operation = ModeOperation.from_dict(raw_action)
except (KeyError, TypeError, ValueError):
continue
transfer_by_identity[_operation_identity(transfer_operation)] = deepcopy(
raw_action
)
if transfer_by_identity:
actions = [
transfer_by_identity.get(_operation_identity(operation), action)
for (operation, _, _), action in zip(
effective_operations,
actions,
strict=True,
)
]
scene = {
"id": profile.id,
"label": profile.name,
@@ -106,9 +106,13 @@ OUTLET_MODE_DISABLE_PATTERN = re.compile(
)
DRIVER_SEAT_THERMAL_MIDDLE_PATTERN = re.compile(
r"(?:通风|加热)[^,,;;。!?!?\n]{0,16}"
r"(?:中等(?:档|级)?|中档|中级|中间档|适中|(?:调到|调为|调|设为|设置为|调整到|调整为)中)|"
r"(?:中等(?:档|级)?|中档|中级|中间档|适中|(?:2|二)(?:档|级)|"
r"(?:调到|调为|调|设为|设置为|调整到|调整为)中)|"
r"(?:中等(?:档|级)?|中档|中级|中间档|适中)"
r"[^,,;;。!?!?\n]{0,16}(?:通风|加热)"
r"[^,,;;。!?!?\n]{0,16}(?:通风|加热)|"
r"(?:通风|加热)[^,,;;。!?!?\n]{0,16}[,,;;]"
r"(?:挡位|档位)?(?:调到|调为|设为|设置为|调整到|调整为)?"
r"(?:2|二)(?:档|级)"
)
DRIVER_SEAT_MASSAGE_CONTINUATION_PATTERN = re.compile(
r"^(?:(?:然后|随后|接着|同时|并且|再|并|以及)?(?:把|将)?(?:按摩)?)"
@@ -116,6 +120,12 @@ DRIVER_SEAT_MASSAGE_CONTINUATION_PATTERN = re.compile(
r"(?:切换|改成|改为|调整|调成|调为|设置|设为)"
r".{0,8}(?:波浪|脉冲|伸展|拉伸|松弛|舒展|放松)(?:模式)?)"
)
DRIVER_SEAT_THERMAL_CONTINUATION_PATTERN = re.compile(
r"^(?:(?:然后|随后|接着|同时|并且|再|并|以及)?(?:把|将)?)"
r"(?:通风|加热)?(?:挡位|档位)?"
r"(?:切换|改成|改为|调整|调到|调成|调为|设置|设为)?"
r"(?:[1-3一二三](?:档|级)|最低|低档|最高|高档|中档|中等|适中)$"
)
_FEATURE_BOUNDARY = re.compile(
r"并且|同时|然后|随后|接着|以及|并|再|和|与|、|,"
)
@@ -775,6 +785,23 @@ def _parse_chinese_driver_seat_commands(compact: str) -> list[dict[str, Any]]:
clause = f"{clause}{continuation}"
continuation_index += 1
index = continuation_index - 1
if "通风" in clause or "加热" in clause:
continuation_index = index + 1
while (
continuation_index < len(seat_clauses)
and DRIVER_SEAT_THERMAL_CONTINUATION_PATTERN.search(
seat_clauses[continuation_index]
)
):
continuation = re.sub(
r"^(?:然后|随后|接着|同时|并且|再|并|以及)?"
r"(?:把|将)?(?:通风|加热)?",
"",
seat_clauses[continuation_index],
)
clause = f"{clause}{continuation}"
continuation_index += 1
index = continuation_index - 1
scope = _seat_target_scope(clause)
if scope == "driver":
inherited_driver_scope = True
@@ -864,11 +891,13 @@ def _parse_chinese_driver_seat_commands(compact: str) -> list[dict[str, Any]]:
if re.search(r"关闭|关掉|停止|取消|不要|别开", thermal_text):
parameters = {"state": "off"}
elif thermal_grade == "high" or re.search(
r"高速|(?:1|一)(?:档|级)", thermal_text
r"高速|(?:3|三)(?:档|级)", thermal_text
):
parameters = {"state": "on", "level": 1}
elif re.search(r"(?:2|二)(?:档|级)|中档|中等|适中", thermal_text):
parameters = None
elif thermal_grade == "low" or re.search(
r"低速|(?:2|二)(?:档|级)", thermal_text
r"低速|(?:1|一)(?:档|级)", thermal_text
) or re.search(r"打开|开启|启动|启用", thermal_text):
parameters = {"state": "on", "level": 2}
elif shared_thermal_state == "off":
@@ -1177,7 +1206,8 @@ def _parse_chinese_commands(text: str) -> list[dict[str, Any]]:
compact,
r"座椅|座位|靠背|椅背|坐盆|坐垫|腿托|脚托|按摩|记忆|预设|"
r"通风|加热|前移|后移|向前移动|向后移动",
DRIVER_SEAT_MASSAGE_CONTINUATION_PATTERN.pattern,
rf"(?:{DRIVER_SEAT_MASSAGE_CONTINUATION_PATTERN.pattern})|"
rf"(?:{DRIVER_SEAT_THERMAL_CONTINUATION_PATTERN.pattern})",
)
actions: list[dict[str, Any]] = _parse_chinese_driver_seat_commands(
seat_text
@@ -1794,7 +1824,7 @@ def merge_native_can_intent(
utterance
)
if unsupported_thermal_middle is not None and not re.search(
r"副驾|副驾驶|乘客|右前", utterance
r"副驾|副驾驶|乘客|乘员|右前", utterance
):
result = dict(parsed)
feature = (
@@ -1334,7 +1334,10 @@ class NativeCanExecutor:
# separate evidence level in ``verification_status`` and
# ``physical_effect_confirmed``. A feedback timeout must
# not be rewritten as a transport/orchestration failure.
succeeded = delivered
# A closed-loop target may already be satisfied before a
# new frame is needed. Fresh matching feedback is a
# successful no-op, not a failed dispatch.
succeeded = delivered or verified
verification_status = (
"verified_success"
if verified
@@ -1354,7 +1357,11 @@ class NativeCanExecutor:
publication_start:
]
)
delivery_status = "submitted"
delivery_status = (
"not_required"
if verified and not delivered
else "submitted"
)
if not feedback_required and delivered:
delivery_status = (
"published"
@@ -1371,7 +1378,9 @@ class NativeCanExecutor:
"domain": function_config["spec"]["domain"],
"status": "succeeded" if succeeded else "failed",
"message": (
"MQTT 白名单控制帧已由 Broker 确认;车辆效果以状态话题回读为准。"
"车辆反馈已确认目标状态,无需重复下发控制帧。"
if verified and not delivered
else "MQTT 白名单控制帧已由 Broker 确认;车辆效果以状态话题回读为准。"
if not feedback_required and delivery_status == "published"
else "MQTT 白名单控制帧已进入客户端发布队列;车辆效果以状态话题回读为准。"
if not feedback_required
+506 -8
View File
@@ -104,6 +104,7 @@ from cognitive_agent.application.native_can_feedback import ( # noqa: E402
)
from cognitive_agent.application.native_can_intent import ( # noqa: E402
DRIVER_SEAT_MASSAGE_CONTINUATION_PATTERN,
DRIVER_SEAT_THERMAL_CONTINUATION_PATTERN,
merge_native_can_intent,
)
from cognitive_agent.application.native_can_relative import ( # noqa: E402
@@ -202,6 +203,39 @@ LOCAL_CONTROL_BUDGET_MS = 1000
VEHICLE_CONTROL_TRANSPORTS = frozenset(
{WIRED_CAN_TRANSPORT, WIRELESS_MQTT_TRANSPORT}
)
N7_CINEMA_RELATED_PATTERN = re.compile(
r"影院|观影|电影|看电影|cinema|movie|theater|theatre",
re.IGNORECASE,
)
N7_CINEMA_BASE_NAMES = frozenset(
{"影院", "影院模式", "观影", "观影模式", "cinema", "cinemamode"}
)
N7_FLASHING_LIGHT_PATTERN = re.compile(
r"闪烁灯|闪光灯|闪灯玩具|flashing\s*light",
re.IGNORECASE,
)
N7_LIGHT_STRIP_PATTERN = re.compile(
r"灯带|灯条|light\s*strip|led\s*strip",
re.IGNORECASE,
)
N7_PERIPHERAL_ACTIVATE_PATTERN = re.compile(
r"打开|开启|启动|点亮|\bon\b|power\s*on",
re.IGNORECASE,
)
N7_PERIPHERAL_OFF_PATTERN = re.compile(
r"关闭|关掉|熄灭|\boff\b|power\s*off",
re.IGNORECASE,
)
N7_PERIPHERAL_COLOR_PATTERN = re.compile(
r"红色|绿色|蓝色|白色|紫色|黄色|橙色|粉色|"
r"\bred\b|\bgreen\b|\bblue\b|\bwhite\b|\bpurple\b|"
r"\byellow\b|\borange\b|\bpink\b",
re.IGNORECASE,
)
N7_CINEMA_VIDEO_APP_PATTERN = re.compile(
r"视频|媒体|爱奇艺|video|media|videoplay|iqiyi",
re.IGNORECASE,
)
def _wireless_mqtt_publish_preflight() -> dict[str, Any]:
@@ -472,6 +506,9 @@ class SeatDemoService(ModeScheduleServiceMixin):
lambda scene: self._vehicle_mqtt_transport.publish_mode_scene(scene),
topic=MQTT_MODE_SCENE_TOPIC,
)
self._mode_scene_peripheral_catalog_provider: Callable[
[], list[dict[str, Any]]
] = lambda: []
self._mode_scene_ingress = ModeSceneIngressConsumer(
self.import_n7_mode_scene,
)
@@ -637,6 +674,378 @@ class SeatDemoService(ModeScheduleServiceMixin):
return self.publish_saved_mode_scene(mode_id, vehicle_id)
@staticmethod
def _n7_scene_identity_text(scene: Mapping[str, Any]) -> str:
aliases = scene.get("aliases", [])
return " ".join(
str(value or "")
for value in (
scene.get("id"),
scene.get("name"),
scene.get("label"),
scene.get("description"),
*(aliases if isinstance(aliases, list) else []),
)
)
@classmethod
def _is_n7_cinema_scene(cls, scene: Mapping[str, Any]) -> bool:
return N7_CINEMA_RELATED_PATTERN.search(
cls._n7_scene_identity_text(scene)
) is not None
@staticmethod
def _is_n7_cinema_base_scene(scene: Mapping[str, Any]) -> bool:
scene_id = re.sub(r"\s+", "", str(scene.get("id") or "")).casefold()
names = {
re.sub(r"\s+", "", str(scene.get(field) or "")).casefold()
for field in ("name", "label")
}
return scene_id == "cinema" or bool(names & N7_CINEMA_BASE_NAMES)
@staticmethod
def _n7_scene_action_selector(action: Mapping[str, Any]) -> tuple[object, ...]:
parameters = action.get("parameters")
slot = (
parameters.get("slot")
if isinstance(parameters, Mapping)
and action.get("capability") == "infrared.emit"
else None
)
return (
str(action.get("domain") or ""),
str(action.get("capability") or ""),
str(action.get("target_scope") or ""),
str(action.get("operation") or "set"),
int(action.get("at_ms") or 0),
slot,
)
@classmethod
def _merge_n7_scene_actions(
cls,
base_actions: object,
overriding_actions: object,
) -> list[dict[str, Any]]:
merged: list[dict[str, Any]] = []
positions: dict[tuple[object, ...], int] = {}
for source in (base_actions, overriding_actions):
if not isinstance(source, list):
continue
for raw_action in source:
if not isinstance(raw_action, Mapping):
continue
action = deepcopy(dict(raw_action))
selector = cls._n7_scene_action_selector(action)
existing = positions.get(selector)
if existing is None:
positions[selector] = len(merged)
merged.append(action)
else:
merged[existing] = action
return merged
def _n7_cinema_base_scene(
self,
incoming_scene: Mapping[str, Any],
) -> dict[str, Any] | None:
if self._is_n7_cinema_base_scene(incoming_scene):
return None
vehicle_id = self.vehicle_profiles.default().id
candidates: list[tuple[int, dict[str, Any]]] = []
for public_profile in self.mode_profiles.public_modes(vehicle_id):
try:
profile = self.mode_profiles.get_profile(
str(public_profile.get("id") or ""),
vehicle_id,
)
except (KeyError, ValueError):
continue
if profile.sync_origin != MQTT_MODE_SCENE_INGRESS_TOPIC:
continue
command_flow = profile.command_flow
source_scene = (
command_flow.get("source_scene")
if isinstance(command_flow, Mapping)
else None
)
if not isinstance(source_scene, Mapping) or not self._is_n7_cinema_base_scene(
source_scene
):
continue
priority = 0 if str(source_scene.get("id") or "") == "cinema" else 1
candidates.append((priority, deepcopy(dict(source_scene))))
if not candidates:
return None
candidates.sort(key=lambda item: (item[0], str(item[1].get("id") or "")))
return candidates[0][1]
@staticmethod
def _is_n7_cinema_video_launch_action(action: Mapping[str, Any]) -> bool:
parameters = action.get("parameters")
if (
action.get("domain") != "android"
or action.get("capability") != "android.launch_app"
or action.get("target_scope") != "headunit"
or action.get("operation") != "launch"
or not isinstance(parameters, Mapping)
):
return False
package_name = str(parameters.get("package_name") or "").strip()
activity_name = str(parameters.get("activity_name") or "").strip()
if (
not package_name
or not activity_name
or len(package_name) > 256
or len(activity_name) > 256
):
return False
return N7_CINEMA_VIDEO_APP_PATTERN.search(
" ".join(
(
str(action.get("type") or ""),
package_name,
activity_name,
)
)
) is not None
def _local_cinema_n7_transfer_actions(
self,
mode_name: str,
instruction: str = "",
) -> list[dict[str, Any]]:
"""Reuse the N7 cinema card's video launch for a local cinema variant."""
scene_identity = {
"id": "local-user-mode-variant",
"name": f"本地扩展 {str(mode_name or '').strip()}",
"label": f"本地扩展 {str(mode_name or '').strip()}",
"description": str(instruction or ""),
}
if not self._is_n7_cinema_scene(scene_identity):
return []
base_scene = self._n7_cinema_base_scene(scene_identity)
if base_scene is None:
return []
actions = base_scene.get("actions", [])
if not isinstance(actions, list):
return []
return [
deepcopy(dict(action))
for action in actions
if isinstance(action, Mapping)
and self._is_n7_cinema_video_launch_action(action)
][:1]
@staticmethod
def _n7_transfer_operation_selector(
domain: object,
capability: object,
target_scope: object,
operation: object,
parameters: object,
) -> tuple[str, str, str, str, str]:
clean_parameters = dict(parameters) if isinstance(parameters, Mapping) else {}
return (
str(domain or ""),
str(capability or ""),
str(target_scope or ""),
str(operation or "set"),
json.dumps(
clean_parameters,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
),
)
@classmethod
def _cinema_preferred_color_patterns(
cls,
scene: Mapping[str, Any],
) -> list[re.Pattern[str]]:
text = cls._n7_scene_identity_text(scene)
colors = (
(r"蓝色|\bblue\b",),
(r"紫色|\bpurple\b",),
(r"红色|\bred\b",),
(r"白色|\bwhite\b",),
(r"绿色|\bgreen\b",),
(r"黄色|\byellow\b",),
(r"橙色|\borange\b",),
(r"粉色|\bpink\b",),
)
explicit = [
re.compile(patterns[0], re.IGNORECASE)
for patterns in colors
if re.search(patterns[0], text, re.IGNORECASE)
]
defaults = [re.compile(patterns[0], re.IGNORECASE) for patterns in colors]
return [*explicit, *(item for item in defaults if item not in explicit)]
def _n7_cinema_peripheral_actions(
self,
scene: Mapping[str, Any],
) -> list[dict[str, Any]]:
try:
raw_catalog = self._mode_scene_peripheral_catalog_provider()
except Exception: # noqa: BLE001 - an unavailable catalog must not block import
return []
if not isinstance(raw_catalog, list):
return []
grouped: dict[str, list[dict[str, Any]]] = {}
identities: dict[str, str] = {}
for raw in raw_catalog:
if not isinstance(raw, Mapping):
continue
device_id = str(raw.get("device_id") or "").strip()
card_uid = str(raw.get("card_uid") or "").strip()
slot = raw.get("slot")
if (
not device_id
or not card_uid
or raw.get("status") != "learned"
or raw.get("enabled", True) is False
or isinstance(slot, bool)
or not isinstance(slot, int)
or not 1 <= slot <= 248
):
continue
item = deepcopy(dict(raw))
grouped.setdefault(device_id, []).append(item)
names = [
raw.get("device_name"),
*(raw.get("device_aliases", []) if isinstance(raw.get("device_aliases"), list) else []),
*(raw.get("object_names", []) if isinstance(raw.get("object_names"), list) else []),
]
identities[device_id] = " ".join(str(value or "") for value in names)
def action_text(item: Mapping[str, Any]) -> str:
return " ".join(
[
str(item.get("action") or ""),
*(item.get("aliases", []) if isinstance(item.get("aliases"), list) else []),
*(item.get("action_names", []) if isinstance(item.get("action_names"), list) else []),
]
)
def as_scene_action(
item: Mapping[str, Any],
*,
category: str,
kind: str,
) -> dict[str, Any]:
device_id = str(item.get("device_id") or "")
device_name = str(item.get("device_name") or device_id)
slot = int(item["slot"])
action_name = str(item.get("action") or "").strip()
return {
"type": "infrared_emit",
"at_ms": 0,
"domain": "self_extension",
"capability": "infrared.emit",
"target_scope": device_id,
"target": {
"device": device_id,
"name": device_name,
"card_uid": str(item.get("card_uid") or ""),
},
"operation": "emit",
"parameters": {"action": action_name, "slot": slot},
"operation_ref": f"pc-cinema-{category}-{kind}-{slot}",
}
selected_devices: set[str] = set()
result: list[dict[str, Any]] = []
color_patterns = self._cinema_preferred_color_patterns(scene)
for category, identity_pattern in (
("flashing-light", N7_FLASHING_LIGHT_PATTERN),
("light-strip", N7_LIGHT_STRIP_PATTERN),
):
device_id = next(
(
key
for key in sorted(grouped)
if key not in selected_devices
and identity_pattern.search(identities.get(key, key))
),
None,
)
if device_id is None:
continue
selected_devices.add(device_id)
actions = sorted(grouped[device_id], key=lambda item: int(item["slot"]))
activation = next(
(
item
for item in actions
if N7_PERIPHERAL_ACTIVATE_PATTERN.search(action_text(item))
and N7_PERIPHERAL_OFF_PATTERN.search(action_text(item)) is None
and N7_PERIPHERAL_COLOR_PATTERN.search(action_text(item)) is None
),
None,
)
color = next(
(
item
for pattern in color_patterns
for item in actions
if pattern.search(action_text(item))
and N7_PERIPHERAL_COLOR_PATTERN.search(action_text(item))
),
None,
)
if color is None:
color = next(
(
item
for item in actions
if N7_PERIPHERAL_COLOR_PATTERN.search(action_text(item))
),
None,
)
if activation is not None:
result.append(
as_scene_action(activation, category=category, kind="activate")
)
if color is not None and (
activation is None or color.get("id") != activation.get("id")
):
result.append(as_scene_action(color, category=category, kind="color"))
return result
def _augment_n7_cinema_scene(
self,
scene: dict[str, Any],
) -> dict[str, object] | None:
if not self._is_n7_cinema_scene(scene):
return None
base_scene = self._n7_cinema_base_scene(scene)
if base_scene is None:
return None
base_actions = base_scene.get("actions", [])
incoming_actions = scene.get("actions", [])
inherited = self._merge_n7_scene_actions(base_actions, incoming_actions)
peripheral_actions = self._n7_cinema_peripheral_actions(scene)
augmented = self._merge_n7_scene_actions(inherited, peripheral_actions)
if len(augmented) > 256:
raise ValueError("影院相关模式合并后的动作数量超过 256 项。")
scene["actions"] = augmented
scene["operation_count"] = len(augmented)
scene["domains"] = list(
dict.fromkeys(str(action.get("domain") or "") for action in augmented)
)
return {
"base_scene_id": str(base_scene.get("id") or ""),
"base_action_count": len(base_actions) if isinstance(base_actions, list) else 0,
"incoming_action_count": (
len(incoming_actions) if isinstance(incoming_actions, list) else 0
),
"peripheral_action_count": len(peripheral_actions),
"merged_action_count": len(augmented),
}
@staticmethod
def _imported_scene_function_configs(
scene: dict[str, Any],
@@ -694,6 +1103,7 @@ class SeatDemoService(ModeScheduleServiceMixin):
"""Upsert one validated N7 card without publishing it back to the broker."""
document = deepcopy(dict(scene))
cinema_augmentation = self._augment_n7_cinema_scene(document)
actions = document.get("actions")
if not isinstance(actions, list) or not actions:
raise ValueError("from_n7/scenes card must contain executable actions")
@@ -764,7 +1174,7 @@ class SeatDemoService(ModeScheduleServiceMixin):
sync_origin=MQTT_MODE_SCENE_INGRESS_TOPIC,
upstream_origin=str(document.get("origin") or ""),
)
return {
result = {
"status": "imported",
"scene_id": saved.id,
"scene_name": saved.name,
@@ -772,6 +1182,9 @@ class SeatDemoService(ModeScheduleServiceMixin):
"profile_revision": self.mode_profiles.profile_revision(saved),
"storage": "persistent_yaml",
}
if cinema_augmentation is not None:
result["cinema_augmentation"] = cinema_augmentation
return result
@property
def driver_only_ch1_execution_active(self) -> bool:
@@ -1015,14 +1428,42 @@ class SeatDemoService(ModeScheduleServiceMixin):
requests = self.mode_profiles.requests_for_profiles([profile])
if profile.sync_origin != MQTT_MODE_SCENE_INGRESS_TOPIC:
return [
request
for operation, request in zip(profile.operations, requests, strict=True)
if not (
transfer_actions = self._local_cinema_n7_transfer_actions(
profile.name,
profile.instruction,
)
transfer_selectors = {
self._n7_transfer_operation_selector(
action.get("domain"),
action.get("capability"),
action.get("target_scope"),
action.get("operation"),
action.get("parameters"),
)
for action in transfer_actions
}
local_requests = []
for operation, request in zip(
profile.operations,
requests,
strict=True,
):
if (
operation.domain == "self_extension"
and operation.capability == "infrared.emit"
):
continue
selector = self._n7_transfer_operation_selector(
operation.domain,
operation.capability,
operation.target_scope,
operation.operation,
operation.parameters,
)
], []
if selector in transfer_selectors:
continue
local_requests.append(request)
return local_requests, transfer_actions
source_scene = (
profile.command_flow.get("source_scene")
if isinstance(profile.command_flow, dict)
@@ -1109,7 +1550,7 @@ class SeatDemoService(ModeScheduleServiceMixin):
execution_mode: str,
local_preview: bool,
) -> dict[str, Any]:
"""Forward only N7-originated actions never submitted by this PC."""
"""Forward validated N7-only actions never submitted by this PC."""
actions = result.get("n7_transfer_candidates")
scene = result.get("n7_source_scene")
@@ -1579,6 +2020,48 @@ class SeatDemoService(ModeScheduleServiceMixin):
raise ValueError("动态模式更新必须保留完整时间轴,请按阶段重新生成预览。")
if schedule is not None:
operations = [deepcopy(op) for phase in schedule.phases for op in phase.operations]
n7_transfer_actions = (
self._local_cinema_n7_transfer_actions(
clean_name,
clean_instruction,
)
if schedule is None
else []
)
if n7_transfer_actions:
command_flow["n7_transfer_actions"] = deepcopy(n7_transfer_actions)
existing_operation_selectors = {
self._n7_transfer_operation_selector(
operation.get("domain"),
operation.get("capability"),
operation.get("target_scope"),
operation.get("operation"),
operation.get("parameters"),
)
for operation in operations
if isinstance(operation, Mapping)
}
for action in n7_transfer_actions:
selector = self._n7_transfer_operation_selector(
action.get("domain"),
action.get("capability"),
action.get("target_scope"),
action.get("operation"),
action.get("parameters"),
)
if selector in existing_operation_selectors:
continue
operations.append(
{
"domain": str(action.get("domain") or ""),
"capability": str(action.get("capability") or ""),
"target_scope": str(action.get("target_scope") or ""),
"operation": str(action.get("operation") or "launch"),
"parameters": deepcopy(dict(action.get("parameters") or {})),
"reason": "继承车机基础影院模式的视频媒体启动动作。",
}
)
existing_operation_selectors.add(selector)
saved = self.mode_profiles.save_mode(
clean_name,
operations,
@@ -1843,6 +2326,16 @@ class SeatDemoService(ModeScheduleServiceMixin):
self._vehicle_mqtt_transport.set_extension_control_rejection_handler(handler)
def set_mode_scene_peripheral_catalog_provider(
self,
provider: Callable[[], list[dict[str, Any]]] | None,
) -> None:
"""Supply the durable NFC-authorized IR catalog for N7 scene imports."""
if provider is not None and not callable(provider):
raise TypeError("mode scene peripheral catalog provider must be callable")
self._mode_scene_peripheral_catalog_provider = provider or (lambda: [])
def _synchronize_vehicle_state_once(
self,
conversation_id: str,
@@ -8372,6 +8865,11 @@ class SeatDemoService(ModeScheduleServiceMixin):
and "按摩" in clauses[-1]
and DRIVER_SEAT_MASSAGE_CONTINUATION_PATTERN.search(fragment)
)
thermal_continuation = bool(
clauses
and re.search(r"通风|加热", clauses[-1])
and DRIVER_SEAT_THERMAL_CONTINUATION_PATTERN.search(fragment)
)
# An exclusion is a qualifier of the control immediately before it,
# not an independent command. Keeping both pieces together lets
# the deterministic window parser remove the excluded targets
@@ -8379,7 +8877,7 @@ class SeatDemoService(ModeScheduleServiceMixin):
exclusion_qualifier = bool(
clauses and re.search(r"除外|除去|不包括|不含", fragment)
)
if massage_continuation or exclusion_qualifier:
if massage_continuation or thermal_continuation or exclusion_qualifier:
clauses[-1] = f"{clauses[-1]},{fragment}"
else:
clauses.append(fragment)
@@ -1050,6 +1050,152 @@ def _peripheral_execution_identity_authorized(
return bool(_device_match_score(_compact_match_text(utterance), capability))
_PERIPHERAL_COLOR_ACTION_PATTERN = re.compile(
r"颜色|色彩|红色|红灯|红光|绿色|绿灯|绿光|蓝色|蓝灯|蓝光|白色|白灯|白光|"
r"紫色|紫灯|紫光|黄色|黄灯|黄光|橙色|橙灯|橙光|粉色|粉灯|粉光|"
r"\b(?:color|red|green|blue|white|purple|yellow|orange|pink)\b",
re.IGNORECASE,
)
_PERIPHERAL_POWER_ON_ACTION_PATTERN = re.compile(
r"打开|开启|启动|点亮|开灯|\b(?:on|power\s*on|turn\s*on)\b",
re.IGNORECASE,
)
_PERIPHERAL_POWER_OFF_ACTION_PATTERN = re.compile(
r"关闭|关掉|停止|关灯|熄灭|\b(?:off|power\s*off|turn\s*off)\b",
re.IGNORECASE,
)
def peripheral_capability_action_text(capability: Mapping[str, Any]) -> str:
"""Return the complete learned action vocabulary used for dependencies."""
values: list[str] = [str(capability.get("action") or "")]
for field in ("aliases", "action_names"):
aliases = capability.get(field, [])
if isinstance(aliases, str):
values.append(aliases)
elif isinstance(aliases, list):
values.extend(str(value or "") for value in aliases)
return " ".join(value.strip() for value in values if value.strip())
def is_peripheral_color_capability(capability: Mapping[str, Any]) -> bool:
"""Return whether an infrared ability changes a light-like device color."""
return _PERIPHERAL_COLOR_ACTION_PATTERN.search(
peripheral_capability_action_text(capability)
) is not None
def is_peripheral_power_on_capability(capability: Mapping[str, Any]) -> bool:
"""Return whether an infrared ability is a stable device activation."""
text = peripheral_capability_action_text(capability)
return bool(
_PERIPHERAL_POWER_ON_ACTION_PATTERN.search(text)
and _PERIPHERAL_POWER_OFF_ACTION_PATTERN.search(text) is None
and _PERIPHERAL_COLOR_ACTION_PATTERN.search(text) is None
)
def is_peripheral_power_off_capability(capability: Mapping[str, Any]) -> bool:
"""Return whether an infrared ability explicitly powers a device off."""
return _PERIPHERAL_POWER_OFF_ACTION_PATTERN.search(
peripheral_capability_action_text(capability)
) is not None
def add_peripheral_activation_prerequisites(
steps: list[dict[str, Any]],
catalog: list[dict[str, Any]],
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
"""Insert an authorized power-on step before each device's first color.
Infrared color commands do not prove that the target is powered. The
executor therefore treats a learned power-on command for the same durable
device binding as a mandatory dependency. The returned catalog carries
the original parse source onto an injected prerequisite so saved modes can
still pass their immutable-flow authorization check.
"""
ordered_ids: list[str] = []
catalog_by_id: dict[str, dict[str, Any]] = {}
for raw in catalog[:MAX_CATALOG_ITEMS]:
if not isinstance(raw, Mapping):
continue
capability_id = str(raw.get("id") or "").strip()
if not capability_id:
continue
if capability_id not in catalog_by_id:
ordered_ids.append(capability_id)
catalog_by_id[capability_id] = dict(raw)
prepared_steps: list[dict[str, Any]] = []
powered_devices: set[str] = set()
for raw_step in steps:
step = dict(raw_step) if isinstance(raw_step, Mapping) else raw_step
if not isinstance(step, dict) or step.get("kind") != "peripheral":
prepared_steps.append(step)
continue
capability_id = str(step.get("capability_id") or "").strip()
capability = catalog_by_id.get(capability_id)
if capability is None:
prepared_steps.append(step)
continue
device_id = str(capability.get("device_id") or "").strip()
if is_peripheral_power_on_capability(capability):
if device_id:
powered_devices.add(device_id)
prepared_steps.append(step)
continue
if is_peripheral_power_off_capability(capability):
if device_id:
powered_devices.discard(device_id)
prepared_steps.append(step)
continue
if not is_peripheral_color_capability(capability) or not device_id:
prepared_steps.append(step)
continue
if device_id not in powered_devices:
prerequisite = next(
(
item
for item in catalog_by_id.values()
if str(item.get("device_id") or "").strip() == device_id
and item.get("status") == "learned"
and item.get("enabled", True) is not False
and bool(_normalized_card_uid(item.get("card_uid")))
and is_peripheral_power_on_capability(item)
),
None,
)
if prerequisite is None:
raise ExtensionIntegrationError(
f"外设 {capability.get('device_name') or device_id} 设置颜色前需要先打开,"
"但当前没有已学习、已启用且已绑定 NFC 的打开指令。"
)
prerequisite_id = str(prerequisite.get("id") or "").strip()
parse_source = str(capability.get("parse_source") or "").strip()
if parse_source and not prerequisite.get("parse_source"):
prerequisite = {**prerequisite, "parse_source": parse_source}
catalog_by_id[prerequisite_id] = prerequisite
prepared_steps.append(
{
"kind": "peripheral",
"capability_id": prerequisite_id,
"dependency": "power_on_before_color",
"auto_inserted": True,
}
)
powered_devices.add(device_id)
prepared_steps.append(step)
if len(prepared_steps) > MAX_PLAN_STEPS * 2:
raise ExtensionIntegrationError("补齐外设打开前置指令后,动作数量超过安全上限。")
return prepared_steps, [catalog_by_id[item_id] for item_id in ordered_ids]
def _normalize_peripheral_capability(value: Any) -> dict[str, Any]:
"""Validate one browser-synchronized learned infrared capability."""
@@ -2615,6 +2761,36 @@ class SelfExtensionService:
raise ExtensionIntegrationError(
"外设名称未在原始指令中被明确提及,已拒绝红外执行。"
)
if is_peripheral_color_capability(capability):
device_id = str(capability.get("device_id") or "").strip()
dependency_catalog = [
dict(item)
for item in self.list_peripheral_capabilities()
if isinstance(item, Mapping)
and str(item.get("device_id") or "").strip() == device_id
and str(item.get("id") or "").strip()
!= str(capability.get("id") or "").strip()
]
dependency_catalog.append(dict(capability))
return self.execute_peripheral_plan_request(
utterance,
{
"provider_used": "infrared_dependency_guard",
"summary": "设置外设颜色前自动补齐同设备的打开指令。",
"steps": [
{
"kind": "peripheral",
"capability_id": str(capability.get("id") or ""),
}
],
},
dependency_catalog,
conversation_id=conversation_id,
conversation_turn=conversation_turn,
execution_mode=execution_mode,
provider_requested=provider_requested,
on_commands_validated=on_commands_validated,
)
slot = validate_slot(capability.get("slot"))
request_id = f"req-extension-{uuid.uuid4().hex}"
conversation_id = conversation_id or f"conv-{uuid.uuid4().hex}"
@@ -2819,6 +2995,39 @@ class SelfExtensionService:
raise ExtensionIntegrationError("GPT 没有生成可执行的外设动作。")
if any(step.get("kind") == "vehicle" for step in steps if isinstance(step, dict)):
raise ExtensionIntegrationError("当前主编排只允许执行持久绑定的 NFC 外设计划。")
selected_device_ids = {
str(item.get("device_id") or "").strip()
for item in catalog
if isinstance(item, Mapping)
and str(item.get("device_id") or "").strip()
}
effective_catalog: list[dict[str, Any]] = [
dict(item) for item in catalog if isinstance(item, Mapping)
]
known_ids = {
str(item.get("id") or "").strip()
for item in effective_catalog
if str(item.get("id") or "").strip()
}
for raw_item in self.list_peripheral_capabilities():
if not isinstance(raw_item, Mapping):
continue
item = dict(raw_item)
capability_id = str(item.get("id") or "").strip()
if (
not capability_id
or capability_id in known_ids
or str(item.get("device_id") or "").strip()
not in selected_device_ids
):
continue
effective_catalog.append(dict(item))
known_ids.add(capability_id)
steps, catalog = add_peripheral_activation_prerequisites(
[dict(step) if isinstance(step, Mapping) else step for step in steps],
effective_catalog,
)
plan_result = {**plan_result, "steps": steps}
catalog_by_id = {
str(item.get("id")): item
for item in catalog
@@ -2884,6 +3093,7 @@ class SelfExtensionService:
function_operations.append(
{
"id": operation_ref,
"capability_id": capability_id,
"capability": "infrared.emit",
"target": {
"device": capability.get("device_id"),
@@ -2897,6 +3107,8 @@ class SelfExtensionService:
"parse_source": capability.get(
"parse_source", "local_rule"
),
"auto_inserted": step.get("auto_inserted") is True,
"dependency": step.get("dependency"),
"timeout_ms": int(self.command_timeout * 1000),
"failure_policy": "abort",
}
@@ -36,9 +36,13 @@ POSITIVE_OUTLET_MODE = re.compile(
)
DRIVER_SEAT_THERMAL_MIDDLE = re.compile(
r"(?:通风|加热)[^,,;;。!?!?\n]{0,16}"
r"(?:中等(?:档|级)?|中档|中级|中间档|适中|(?:调到|调为|调|设为|设置为|调整到|调整为)中)|"
r"(?:中等(?:档|级)?|中档|中级|中间档|适中|(?:2|二)(?:档|级)|"
r"(?:调到|调为|调|设为|设置为|调整到|调整为)中)|"
r"(?:中等(?:档|级)?|中档|中级|中间档|适中)"
r"[^,,;;。!?!?\n]{0,16}(?:通风|加热)"
r"[^,,;;。!?!?\n]{0,16}(?:通风|加热)|"
r"(?:通风|加热)[^,,;;。!?!?\n]{0,16}[,,;;]"
r"(?:挡位|档位)?(?:调到|调为|设为|设置为|调整到|调整为)?"
r"(?:2|二)(?:档|级)"
)
META_NON_EXECUTION = re.compile(
r"复述|引用|翻译|改写|总结|解释|分析|造句|"
@@ -542,7 +546,7 @@ def apply_language_safety_guards(
thermal_middle = DRIVER_SEAT_THERMAL_MIDDLE.search(utterance)
if thermal_middle is not None and not re.search(
r"副驾|副驾驶|乘客|右前", utterance
r"副驾|副驾驶|乘客|乘员|右前", utterance
):
feature = "通风" if "通风" in thermal_middle.group(0) else "加热"
native_capability = (
@@ -864,6 +864,19 @@ class UnderstandingTranscriptProcessor:
while index < len(commands):
current = commands[index]
following = commands[index + 1] if index + 1 < len(commands) else None
if (
following is not None
and current.capability == following.capability
and current.target_scope == following.target_scope
and current.property_name == following.property_name
and current.property_name in {"seat.ventilation", "seat.heating"}
and "level" in current.qualifiers.get("implicit_parameters", ())
and "level" not in following.qualifiers.get("implicit_parameters", ())
):
obsolete_issues.update(current.validation_issues)
output.append(following)
index += 2
continue
if (
following is not None
and current.property_name == following.property_name == "ambient.color"
+641 -105
View File
@@ -3,6 +3,7 @@
from __future__ import annotations
import argparse
import hashlib
import hmac
import ipaddress
import json
@@ -14,6 +15,7 @@ import sys
import threading
import time
from collections.abc import Callable, Mapping
from concurrent.futures import ThreadPoolExecutor
from copy import deepcopy
from http import HTTPStatus
from http.cookies import CookieError, SimpleCookie
@@ -95,6 +97,7 @@ from cognitive_agent.auth import ( # noqa: E402
verify_password,
)
from cognitive_agent.infrastructure.can.access import ( # noqa: E402
HARDWARE_CLIENT_ID_PATTERN,
HardwareAccessRequiredError,
)
from cognitive_agent.infrastructure.can.gateway import CanGatewayError # noqa: E402
@@ -147,6 +150,25 @@ PUBLIC_OTA_DOWNLOAD_PATH = re.compile(
NON_DISPATCH_DISPLAY_SOURCES = frozenset({"mode_editor_compile"})
def _compatible_hardware_client_id(value: object) -> str | None:
"""Map legacy browser IDs onto the current opaque hardware-client contract.
Older cached consoles used UUIDs or identifiers containing underscores.
They cannot have a valid lease under the current controller contract, but
rejecting them before the access workflow runs leaves every hardware mode
unusable. A one-way, stable mapping lets those consoles request and reuse
an ordinary lease without granting access or exposing the legacy value.
"""
raw_value = str(value or "").strip().lower()
if not raw_value:
return None
if HARDWARE_CLIENT_ID_PATTERN.fullmatch(raw_value):
return raw_value
digest = hashlib.sha256(raw_value.encode("utf-8")).hexdigest()[:32]
return f"hwc-legacy-{digest}"
class VehicleDisplaySync:
"""Process-local snapshot consumed by passive vehicle display clients."""
@@ -272,6 +294,9 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
catalog_provider=extension.list_peripheral_capabilities,
executor=extension.execute_bound_infrared_action,
)
service.set_mode_scene_peripheral_catalog_provider(
extension.list_peripheral_capabilities
)
service.set_extension_control_handler(extension_control.submit)
service.set_extension_control_rejection_handler(
extension_control.reject_transport_message
@@ -361,7 +386,9 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
try:
self._json(
self.service.hardware_access.status(
str(query.get("client_id", [""])[0])
_compatible_hardware_client_id(
query.get("client_id", [""])[0]
)
)
)
except ValueError as exc:
@@ -478,7 +505,9 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
control_transport = str(
query.get("control_transport", ["wired_can"])[0]
)
hardware_client_id = str(query.get("hardware_client_id", [""])[0]) or None
hardware_client_id = _compatible_hardware_client_id(
query.get("hardware_client_id", [""])[0]
)
try:
self._json(
(
@@ -769,10 +798,8 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
utterance=utterance,
source=source,
)
hardware_client_id = (
str(payload["hardware_client_id"])
if payload.get("hardware_client_id")
else None
hardware_client_id = _compatible_hardware_client_id(
payload.get("hardware_client_id")
)
reasoning_effort = (
str(payload["reasoning_effort"])
@@ -1231,6 +1258,7 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
conversation_id: str | None,
execution_mode: str,
on_commands_validated: Callable[[dict[str, object]], None] | None = None,
update_display: bool = True,
) -> dict[str, object]:
"""Execute already validated infrared capabilities."""
@@ -1281,17 +1309,257 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
on_commands_validated=on_commands_validated,
)
result["provider_used"] = "infrared_local_rules"
self.vehicle_display_sync.update_extension(
action={
"type": "emit_sequence" if len(capabilities) > 1 else "emit",
"source": "main_orchestration_local_rule",
"capability_ids": [item["id"] for item in capabilities],
"slots": [item["slot"] for item in capabilities],
},
result=result,
)
if update_display:
self.vehicle_display_sync.update_extension(
action={
"type": "emit_sequence" if len(capabilities) > 1 else "emit",
"source": "main_orchestration_local_rule",
"capability_ids": [item["id"] for item in capabilities],
"slots": [item["slot"] for item in capabilities],
},
result=result,
)
return result
@staticmethod
def _run_parallel_control_branches(
can_branch: Callable[[], dict[str, object]],
peripheral_branch: Callable[[], dict[str, object]],
) -> tuple[dict[str, object], dict[str, object], dict[str, object]]:
"""Start two independent transports together and retain timing evidence."""
barrier = threading.Barrier(2)
timing_lock = threading.Lock()
starts: dict[str, float] = {}
durations: dict[str, float] = {}
def run(
name: str,
branch: Callable[[], dict[str, object]],
) -> dict[str, object]:
barrier.wait(timeout=5)
started = time.perf_counter()
with timing_lock:
starts[name] = started
try:
return branch()
finally:
with timing_lock:
durations[name] = round(
(time.perf_counter() - started) * 1000,
3,
)
total_started = time.perf_counter()
with ThreadPoolExecutor(
max_workers=2,
thread_name_prefix="can-ir-dispatch",
) as pool:
can_future = pool.submit(run, "can", can_branch)
peripheral_future = pool.submit(run, "peripheral", peripheral_branch)
can_result = can_future.result()
peripheral_result = peripheral_future.result()
started_values = list(starts.values())
timing = {
"started_skew_ms": round(
(max(started_values) - min(started_values)) * 1000,
3,
) if len(started_values) == 2 else None,
"total_ms": round(
(time.perf_counter() - total_started) * 1000,
3,
),
"branches": {
name: {"duration_ms": duration}
for name, duration in sorted(durations.items())
},
}
return can_result, peripheral_result, timing
@staticmethod
def _parallel_branch_succeeded(result: Mapping[str, object]) -> bool:
if str(result.get("status") or "") not in {
"succeeded",
"partially_succeeded",
"no_action",
}:
return False
if str(result.get("execution_outcome") or "") in {
"failed",
"failed_or_blocked",
}:
return False
validation = result.get("validation", [])
return not any(
isinstance(issue, Mapping) and issue.get("severity") == "error"
for issue in (
validation
if isinstance(validation, list)
else []
)
)
def _ensure_parallel_hardware_authorized(
self,
*,
execution_mode: str,
control_transport: str,
hardware_access_bypass: bool,
hardware_client_id: str | None,
) -> None:
"""Resolve local-bench access before an external branch can start.
Wireless MQTT is executed by the head unit and is deliberately outside
the local T2G/ZLG lease. Requiring a browser hardware client ID there
contradicts :meth:`SeatDemoService.process` and rejects every mixed
CAN/IR mode because wireless requests correctly omit that identifier.
"""
if (
execution_mode == "hardware"
and control_transport != WIRELESS_MQTT_TRANSPORT
and not hardware_access_bypass
):
self.service.hardware_access.ensure_authorized(hardware_client_id)
def _execute_can_and_peripheral_branches(
self,
*,
utterance: str,
entries: list[dict[str, object]],
execution_mode: str,
provider: str,
conversation_id: str | None,
can_execute: Callable[[], dict[str, object]],
can_preflight: Callable[[], dict[str, object]],
parallel_eligible: bool,
on_preflight_validated: Callable[[dict[str, object]], None] | None = None,
) -> dict[str, object]:
"""Preflight both branches, then dispatch hardware concurrently."""
if not entries:
return can_execute()
if not parallel_eligible or execution_mode != "hardware":
can_result = can_execute()
infrared_result = self._execute_local_peripheral_actions(
utterance,
entries,
provider=provider,
conversation_id=conversation_id,
execution_mode=execution_mode,
)
return self._merge_can_and_peripheral_results(
utterance,
can_result,
infrared_result,
entries,
)
can_preview, infrared_preview, preflight_timing = (
self._run_parallel_control_branches(
can_preflight,
lambda: self._execute_local_peripheral_actions(
utterance,
entries,
provider=provider,
conversation_id=None,
execution_mode="dry_run",
update_display=False,
),
)
)
if not (
self._parallel_branch_succeeded(can_preview)
and self._parallel_branch_succeeded(infrared_preview)
):
rejected = self._merge_can_and_peripheral_results(
utterance,
can_preview,
infrared_preview,
entries,
)
rejected.update(
{
"status": "validation_failed",
"success": False,
"execution_mode": "dry_run",
"hardware_execution_allowed": False,
"parallel_dispatch": {
"enabled": False,
"preflight_status": "failed",
"preflight": preflight_timing,
},
}
)
rejected["decision_summary"] = [
*list(rejected.get("decision_summary") or []),
"CAN 或外设预校验未通过;两个物理传输分支均未启动。",
]
return rejected
if on_preflight_validated is not None:
validated = self._merge_can_and_peripheral_results(
utterance,
can_preview,
infrared_preview,
entries,
)
validated["status"] = "processing"
validated["phase"] = "commands_validated"
validated["execution_mode"] = "hardware"
validated["parallel_dispatch"] = {
"enabled": False,
"preflight_status": "succeeded",
"preflight": preflight_timing,
}
on_preflight_validated(validated)
can_result, infrared_result, dispatch_timing = (
self._run_parallel_control_branches(
can_execute,
lambda: self._execute_local_peripheral_actions(
utterance,
entries,
provider=provider,
conversation_id=conversation_id,
execution_mode=execution_mode,
),
)
)
result = self._merge_can_and_peripheral_results(
utterance,
can_result,
infrared_result,
entries,
)
result["parallel_dispatch"] = {
"enabled": True,
"preflight_status": "succeeded",
"preflight": preflight_timing,
"dispatch": dispatch_timing,
}
result["decision_summary"] = [
*list(result.get("decision_summary") or []),
"CAN 与外设控制在完整预校验通过后并行启动。",
]
return result
@staticmethod
def _mixed_request_requires_transport_order(utterance: str) -> bool:
"""Keep explicit sequencing and delays out of concurrent dispatch."""
return bool(
re.search(
r"然后|随后|接着|之后|"
r"先.{0,120}?(?:再|然后|随后|接着)|"
r"(?:\d+(?:\.\d+)?|[一二三四五六七八九十两半]+)"
r"(?:毫秒|秒(?:钟)?|分钟|小时|ms(?![A-Za-z])|s(?![A-Za-z]))",
utterance,
flags=re.IGNORECASE,
)
)
@staticmethod
def _split_scene_peripheral_plan(
assistant_plan: Mapping[str, object],
@@ -1800,24 +2068,111 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
labels = metadata.setdefault("labels", {})
labels["route"] = "unified_can_infrared"
infrared_actions = [
{
"index": entry.get("index"),
"clause": entry.get("clause"),
"capability_id": capability.get("id"),
"device_id": capability.get("device_id"),
"device_name": capability.get("device_name"),
"action": capability.get("action"),
"slot": capability.get("slot"),
"parse_source": str(
capability.get("parse_source") or "local_rule"
),
"transport": str(infrared_result.get("control_transport") or "gscu_tcp"),
}
for entry in entries
for capability in [entry.get("capability")]
if isinstance(capability, dict)
]
entry_lookup: dict[tuple[str, object, str], dict[str, object]] = {}
for entry in entries:
capability = entry.get("capability") if isinstance(entry, dict) else None
if not isinstance(capability, dict):
continue
entry_lookup[
(
str(capability.get("device_id") or ""),
capability.get("slot"),
str(capability.get("action") or ""),
)
] = entry
extension_config = (infrared_result.get("function_configs") or {}).get(
"self_extension"
)
extension_spec = (
extension_config.get("spec")
if isinstance(extension_config, Mapping)
else None
)
compiled_operations = (
extension_spec.get("operations")
if isinstance(extension_spec, Mapping)
and isinstance(extension_spec.get("operations"), list)
else []
)
infrared_actions: list[dict[str, object]] = []
for operation_index, operation in enumerate(compiled_operations):
if not isinstance(operation, Mapping):
continue
target = operation.get("target")
parameters = operation.get("parameters")
if not isinstance(target, Mapping) or not isinstance(parameters, Mapping):
continue
device_id = str(target.get("device") or "")
slot = parameters.get("slot")
action_name = str(parameters.get("action") or "")
matched_entry = entry_lookup.get((device_id, slot, action_name), {})
matched_capability = (
matched_entry.get("capability")
if isinstance(matched_entry, dict)
else None
)
capability_id = str(operation.get("capability_id") or "") or str(
matched_capability.get("id")
if isinstance(matched_capability, Mapping)
else ""
)
auto_inserted = operation.get("auto_inserted") is True
infrared_actions.append(
{
"index": (
matched_entry.get("index")
if isinstance(matched_entry, dict) and matched_entry
else operation_index
),
"clause": (
matched_entry.get("clause")
if isinstance(matched_entry, dict) and matched_entry
else "自动前置:打开设备"
if auto_inserted
else action_name
),
"capability_id": capability_id,
"device_id": device_id,
"device_name": str(target.get("name") or device_id),
"action": action_name,
"slot": slot,
"parse_source": str(
operation.get("parse_source") or "local_rule"
),
**(
{
"auto_inserted": True,
"dependency": operation.get("dependency"),
}
if auto_inserted
else {}
),
"transport": str(
infrared_result.get("control_transport") or "gscu_tcp"
),
}
)
if not infrared_actions:
infrared_actions = [
{
"index": entry.get("index"),
"clause": entry.get("clause"),
"capability_id": capability.get("id"),
"device_id": capability.get("device_id"),
"device_name": capability.get("device_name"),
"action": capability.get("action"),
"slot": capability.get("slot"),
"parse_source": str(
capability.get("parse_source") or "local_rule"
),
"transport": str(
infrared_result.get("control_transport") or "gscu_tcp"
),
}
for entry in entries
for capability in [entry.get("capability")]
if isinstance(capability, dict)
]
preview_only = (
str(can_result.get("execution_mode") or "dry_run") != "hardware"
and str(infrared_result.get("execution_mode") or "dry_run")
@@ -2262,47 +2617,78 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
else None
)
entries = self._saved_mode_infrared_entries(profile)
can_result = self.service.execute_mode_profile_card(
str(getattr(profile, "id")),
conversation_id=conversation_id,
execution_mode=execution_mode,
approval_token=(
str(common_arguments["approval_token"])
if common_arguments.get("approval_token")
else None
),
vehicle_id=vehicle_id,
simulation_fault=str(
common_arguments.get("simulation_fault") or "normal"
),
hardware_client_id=(
str(common_arguments["hardware_client_id"])
if common_arguments.get("hardware_client_id")
else None
),
hardware_access_bypass=bool(
common_arguments.get("hardware_access_bypass", True)
),
control_transport=str(
common_arguments.get("control_transport") or "wired_can"
),
mode_id = str(getattr(profile, "id"))
simulation_fault = str(common_arguments.get("simulation_fault") or "normal")
control_transport = str(
common_arguments.get("control_transport") or "wired_can"
)
if not entries:
can_result["utterance"] = utterance
return can_result
infrared_result = self._execute_local_peripheral_actions(
utterance,
entries,
hardware_client_id = _compatible_hardware_client_id(
common_arguments.get("hardware_client_id")
)
hardware_access_bypass = bool(
common_arguments.get("hardware_access_bypass", True)
)
command_flow = getattr(profile, "command_flow", None)
execution_steps = (
command_flow.get("execution_steps", [])
if isinstance(command_flow, Mapping)
else []
)
parallel_eligible = bool(
getattr(profile, "schedule", None) is None
and not execution_steps
)
if entries and parallel_eligible:
self._ensure_parallel_hardware_authorized(
execution_mode=execution_mode,
control_transport=control_transport,
hardware_access_bypass=hardware_access_bypass,
hardware_client_id=hardware_client_id,
)
def execute_can() -> dict[str, object]:
return self.service.execute_mode_profile_card(
mode_id,
conversation_id=conversation_id,
execution_mode=execution_mode,
approval_token=(
str(common_arguments["approval_token"])
if common_arguments.get("approval_token")
else None
),
vehicle_id=vehicle_id,
simulation_fault=simulation_fault,
hardware_client_id=hardware_client_id,
hardware_access_bypass=hardware_access_bypass,
control_transport=control_transport,
)
def preflight_can() -> dict[str, object]:
return self.service.execute_mode_profile_card(
mode_id,
conversation_id=None,
execution_mode="dry_run",
approval_token=None,
vehicle_id=vehicle_id,
simulation_fault="normal",
hardware_client_id=None,
hardware_access_bypass=True,
control_transport=control_transport,
)
result = self._execute_can_and_peripheral_branches(
utterance=utterance,
entries=entries,
execution_mode=execution_mode,
provider="saved_mode_command_flow",
conversation_id=conversation_id,
execution_mode=execution_mode,
)
result = self._merge_can_and_peripheral_results(
utterance,
can_result,
infrared_result,
entries,
can_execute=execute_can,
can_preflight=preflight_can,
parallel_eligible=parallel_eligible,
)
if not entries:
result["utterance"] = utterance
return result
result["provider_used"] = "saved_mode_command_flow"
result["orchestration_route"] = "saved_mode_command_flow"
result["mode_profile"] = profile.public_dict()
@@ -2516,6 +2902,125 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
)
return result
parallel_mixed_control = bool(
path != "/api/requests/preview"
and execution_mode == "hardware"
and entries
and residual
and not unresolved
and not denied
and not self._mixed_request_requires_transport_order(utterance)
)
if parallel_mixed_control:
can_arguments = {
**common_arguments,
"utterance": residual,
"_cloud_instruction_context": natural_language_routing,
}
def execute_can() -> dict[str, object]:
return self.service.process(
**can_arguments,
_on_commands_validated=None,
)
preflight_arguments = {
**can_arguments,
"execution_mode": "dry_run",
"approval_token": None,
"hardware_client_id": None,
"hardware_access_bypass": True,
}
def preflight_can() -> dict[str, object]:
return self.service.preview_native_can(
**preflight_arguments,
on_commands_validated=None,
)
def publish_parallel_validation(
validated: dict[str, object],
) -> None:
nonlocal peripheral_validation_elapsed_ms
if callback is None:
return
elapsed_ms = round(
(time.perf_counter() - local_stage_started) * 1000,
3,
)
peripheral_validation_elapsed_ms = elapsed_ms
validated["natural_language_routing"] = deepcopy(
natural_language_routing
)
validated.update(
{
"local_control_budget_ms": LOCAL_CONTROL_BUDGET_MS,
"local_control_elapsed_ms": elapsed_ms,
"local_control_within_budget": (
elapsed_ms < LOCAL_CONTROL_BUDGET_MS
),
}
)
callback(validated)
hardware_client_id = _compatible_hardware_client_id(
common_arguments.get("hardware_client_id")
)
hardware_access_bypass = bool(
common_arguments.get("hardware_access_bypass", True)
)
self._ensure_parallel_hardware_authorized(
execution_mode=execution_mode,
control_transport=str(
common_arguments.get("control_transport") or "wired_can"
),
hardware_access_bypass=hardware_access_bypass,
hardware_client_id=hardware_client_id,
)
result = self._execute_can_and_peripheral_branches(
utterance=utterance,
entries=entries,
execution_mode=execution_mode,
provider=provider,
conversation_id=conversation_id,
can_execute=execute_can,
can_preflight=preflight_can,
parallel_eligible=True,
on_preflight_validated=publish_parallel_validation,
)
routed = result.get("natural_language_routing")
if isinstance(routed, dict):
natural_language_routing = deepcopy(routed)
result["natural_language_routing"] = deepcopy(
natural_language_routing
)
if peripheral_validation_elapsed_ms is not None:
result.update(
{
"local_control_budget_ms": LOCAL_CONTROL_BUDGET_MS,
"local_control_elapsed_ms": (
peripheral_validation_elapsed_ms
),
"local_control_within_budget": (
peripheral_validation_elapsed_ms
< LOCAL_CONTROL_BUDGET_MS
),
}
)
for key in (
"parsed_clauses",
"resolved_clauses",
"unresolved_clauses",
):
value = natural_language_routing.get(key)
if isinstance(value, list):
result[key] = deepcopy(value)
result = self._apply_nfc_command_denials(result, denied)
self.service.persist_composed_result(result)
if callback is not None:
callback(result)
return result
# Compile, publish and execute every deterministic IR action before
# any hosted planner is contacted.
if entries:
@@ -2892,10 +3397,8 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
approval_token=os.getenv(
"COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN"
),
hardware_client_id=(
str(payload["hardware_client_id"])
if payload.get("hardware_client_id")
else None
hardware_client_id=_compatible_hardware_client_id(
payload.get("hardware_client_id")
),
hardware_access_bypass=self._hardware_access_bypass_allowed(),
reasoning_effort=(
@@ -3006,8 +3509,8 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
history_payload = payload.get("history")
history = history_payload if isinstance(history_payload, list) else []
source = str(payload.get("client_type") or "web_console_assistant")
hardware_client_id = (
str(payload["hardware_client_id"]) if payload.get("hardware_client_id") else None
hardware_client_id = _compatible_hardware_client_id(
payload.get("hardware_client_id")
)
reasoning_effort = (
str(payload["reasoning_effort"])
@@ -3815,10 +4318,8 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
if payload.get("conversation_id")
else None
)
hardware_client_id = (
str(payload["hardware_client_id"])
if payload.get("hardware_client_id")
else None
hardware_client_id = _compatible_hardware_client_id(
payload.get("hardware_client_id")
)
self.vehicle_display_sync.update_task(
status="running",
@@ -3828,39 +4329,69 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
# Validate the complete saved flow against the durable NFC binding
# before any transport is allowed to execute.
entries = self._saved_mode_infrared_entries(profile)
can_result = self.service.execute_mode_profile_card(
mode_id,
conversation_id=conversation_id,
execution_mode=execution_mode,
approval_token=os.getenv(
"COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN"
),
vehicle_id=vehicle_id,
simulation_fault=str(payload.get("simulation_fault", "normal")),
hardware_client_id=hardware_client_id,
hardware_access_bypass=self._hardware_access_bypass_allowed(),
control_transport=control_transport,
simulation_fault = str(payload.get("simulation_fault", "normal"))
hardware_access_bypass = self._hardware_access_bypass_allowed()
command_flow = getattr(profile, "command_flow", None)
execution_steps = (
command_flow.get("execution_steps", [])
if isinstance(command_flow, Mapping)
else []
)
if entries:
infrared_result = self._execute_local_peripheral_actions(
utterance,
entries,
provider="saved_mode_command_flow",
parallel_eligible = bool(
getattr(profile, "schedule", None) is None
and not execution_steps
)
if entries and parallel_eligible:
self._ensure_parallel_hardware_authorized(
execution_mode=execution_mode,
control_transport=control_transport,
hardware_access_bypass=hardware_access_bypass,
hardware_client_id=hardware_client_id,
)
def execute_can() -> dict[str, object]:
return self.service.execute_mode_profile_card(
mode_id,
conversation_id=conversation_id,
execution_mode=execution_mode,
approval_token=os.getenv(
"COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN"
),
vehicle_id=vehicle_id,
simulation_fault=simulation_fault,
hardware_client_id=hardware_client_id,
hardware_access_bypass=hardware_access_bypass,
control_transport=control_transport,
)
result = self._merge_can_and_peripheral_results(
utterance,
can_result,
infrared_result,
entries,
def preflight_can() -> dict[str, object]:
return self.service.execute_mode_profile_card(
mode_id,
conversation_id=None,
execution_mode="dry_run",
approval_token=None,
vehicle_id=vehicle_id,
simulation_fault="normal",
hardware_client_id=None,
hardware_access_bypass=True,
control_transport=control_transport,
)
result = self._execute_can_and_peripheral_branches(
utterance=utterance,
entries=entries,
execution_mode=execution_mode,
provider="saved_mode_command_flow",
conversation_id=conversation_id,
can_execute=execute_can,
can_preflight=preflight_can,
parallel_eligible=parallel_eligible,
)
if entries:
result["provider_used"] = "saved_mode_command_flow"
result["orchestration_route"] = "saved_mode_command_flow"
result["mode_profile"] = profile.public_dict()
self.service.persist_composed_result(result)
else:
result = can_result
self.vehicle_display_sync.update_task(
status=str(result.get("status") or "succeeded"),
utterance=utterance,
@@ -4035,6 +4566,9 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
self._json(result)
return
if path == "/api/extension/vehicle/execute":
payload["hardware_client_id"] = _compatible_hardware_client_id(
payload.get("hardware_client_id")
)
result = self.extension.execute_vehicle(
payload,
self.service,
@@ -4362,7 +4896,9 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
return
try:
payload = self._read_json_body(max_bytes=4096)
client_id = str(payload.get("client_id", ""))
client_id = _compatible_hardware_client_id(
payload.get("client_id")
)
if path == "/api/hardware/access/request":
status = self.service.hardware_access.request_access(
client_id,
+145 -54
View File
@@ -997,14 +997,14 @@ operations:
def runtime_url(file_name: str, source_url: str) -> str:
hashed = runtime_assets.get(file_name)
return f"/{hashed}" if hashed else source_url
with self.opener.open(f"{self.base_url}/app.js?v=dual-seat-145-n7-dispatch", timeout=3) as response:
with self.opener.open(f"{self.base_url}/app.js?v=dual-seat-147-video-media", timeout=3) as response:
app_script = response.read().decode("utf-8")
with self.opener.open(
f"{self.base_url}/gesture-control.js?v=android-pad-9", timeout=3
) as response:
gesture_script = response.read().decode("utf-8")
with self.opener.open(
f"{self.base_url}/console-v2.js?v=local-first-77-performance", timeout=3
f"{self.base_url}/console-v2.js?v=local-first-79-cinema-status", timeout=3
) as response:
console_script = response.read().decode("utf-8")
with self.opener.open(
@@ -1024,7 +1024,7 @@ operations:
) as response:
speech_worklet = response.read().decode("utf-8")
with self.opener.open(
f"{self.base_url}/self-extension.js?v=nfc-mqtt-56", timeout=3
f"{self.base_url}/self-extension.js?v=nfc-persistent-57", timeout=3
) as response:
extension_script = response.read().decode("utf-8")
with self.opener.open(
@@ -1032,7 +1032,7 @@ operations:
) as response:
extension_styles = response.read().decode("utf-8").replace("\r\n", "\n")
with self.opener.open(
f"{self.base_url}/assistant-chat.js?v=codex-assistant-35", timeout=3
f"{self.base_url}/assistant-chat.js?v=codex-assistant-39-video-media", timeout=3
) as response:
assistant_script = response.read().decode("utf-8")
with self.opener.open(
@@ -1084,13 +1084,13 @@ operations:
self.assertIn("CMD 0x431 · FB 0x388 · CH1", template)
self.assertNotIn("透明车体 · 实体设备", template)
self.assertIn(
f'src="{runtime_url("app.js", "/app.js?v=dual-seat-145-n7-dispatch")}"',
f'src="{runtime_url("app.js", "/app.js?v=dual-seat-147-video-media")}"',
html,
)
self.assertIn('import "../styles.css";', vite_entry)
self.assertNotIn('class="vehicle-system-hud"', template)
self.assertIn(
f'src="{runtime_url("console-v2.js", "/console-v2.js?v=local-first-77-performance")}"',
f'src="{runtime_url("console-v2.js", "/console-v2.js?v=local-first-79-cinema-status")}"',
html,
)
self.assertIn('import "../console-v2.css";', vite_entry)
@@ -1105,7 +1105,7 @@ operations:
self.assertIn('id="v2BootScreen"', template)
self.assertIn('正在加载车机控制与实时状态', template)
self.assertIn(
f'src="{runtime_url("self-extension.js", "/self-extension.js?v=nfc-mqtt-56")}"',
f'src="{runtime_url("self-extension.js", "/self-extension.js?v=nfc-persistent-57")}"',
html,
)
self.assertIn('import "../self-extension.css";', vite_entry)
@@ -1117,7 +1117,7 @@ operations:
self.assertIn('id="assistantTranscript"', template)
self.assertIn('import "../assistant-chat.css";', vite_entry)
self.assertIn(
f'src="{runtime_url("assistant-chat.js", "/assistant-chat.js?v=codex-assistant-35")}"',
f'src="{runtime_url("assistant-chat.js", "/assistant-chat.js?v=codex-assistant-39-video-media")}"',
html,
)
self.assertIn('import "../energy-monitor.css";', vite_entry)
@@ -1433,16 +1433,16 @@ operations:
self.assertIn(f'/{runtime_assets[asset_name]}', service_worker)
else:
self.assertIn(
'const CACHE_VERSION = "cognitive-agent-pad-v329-mode-preview"',
'const CACHE_VERSION = "cognitive-agent-pad-v334-video-media"',
service_worker,
)
self.assertIn('/console-v2.css?v=delivery-state-69', service_worker)
self.assertIn('/console-v2.js?v=local-first-77-performance', service_worker)
self.assertIn('/console-v2.js?v=local-first-79-cinema-status', service_worker)
self.assertIn('/assistant-chat.css?v=codex-assistant-30', service_worker)
self.assertIn('/assistant-chat.js?v=codex-assistant-35', service_worker)
self.assertIn('/assistant-chat.js?v=codex-assistant-39-video-media', service_worker)
self.assertIn('/energy-monitor.css?v=energy-monitor-5-dialog-size', service_worker)
self.assertIn('/energy-monitor.js?v=energy-monitor-5-performance', service_worker)
self.assertIn('"/app.js?v=dual-seat-145-n7-dispatch"', service_worker)
self.assertIn('"/app.js?v=dual-seat-147-video-media"', service_worker)
self.assertIn('/firmware-workbench-entry.css?v=2', service_worker)
self.assertIn('/firmware-workbench-entry.js?v=3', service_worker)
self.assertIn('/firmware-workbench-theme.js?v=1', service_worker)
@@ -1910,7 +1910,7 @@ operations:
) as response:
extension_script = response.read().decode("utf-8")
with self.opener.open(
f"{self.base_url}/app.js?v=dual-seat-145-n7-dispatch", timeout=3
f"{self.base_url}/app.js?v=dual-seat-147-video-media", timeout=3
) as response:
app_script = response.read().decode("utf-8")
@@ -2904,47 +2904,51 @@ operations:
).encode("utf-8"),
)
def fail_can_after_validation(*_args, **kwargs):
callback = kwargs.get("_on_commands_validated")
self.assertTrue(callable(callback))
callback(
{
"request_id": "req-can-validation-only",
"utterance": "打开空调,温度设置为24度,风速调整为3档,打开遮阳帘",
"status": "processing",
"execution_mode": "hardware",
"control_transport": "wired_can",
"provider_used": "local_rules",
"decision_summary": [],
"function_configs": {
"native_climate": {
"spec": {
"operations": [
{
"id": "vehicle_set_power_1",
"capability": "climate.set_power",
"target": {"zone": "vehicle"},
"parameters": {"state": "on"},
"parse_source": "local_rule",
}
]
can_preview = {
"request_id": "req-can-validation-only",
"utterance": "打开空调,温度设置为24度,风速调整为3档,打开遮阳帘",
"status": "succeeded",
"execution_mode": "dry_run",
"control_transport": "wired_can",
"provider_used": "local_rules",
"decision_summary": [],
"function_configs": {
"native_climate": {
"spec": {
"operations": [
{
"id": "vehicle_set_power_1",
"capability": "climate.set_power",
"target": {"zone": "vehicle"},
"parameters": {"state": "on"},
"parse_source": "local_rule",
}
}
},
"command_plans": {
"native_climate": {"spec": {"commands": []}}
},
"operation_results": [],
"execution_report": None,
]
}
}
)
},
"command_plans": {
"native_climate": {"spec": {"commands": []}}
},
"operation_results": [],
"execution_report": None,
}
def fail_can_after_validation(*_args, **_kwargs):
raise CanGatewayError("simulated CAN transport failure")
try:
with patch.object(
self.service,
"process",
side_effect=fail_can_after_validation,
with (
patch.object(
self.service,
"preview_native_can",
return_value=can_preview,
),
patch.object(
self.service,
"process",
side_effect=fail_can_after_validation,
),
):
with self.opener.open(request, timeout=3) as response:
events = [
@@ -2963,11 +2967,8 @@ operations:
for item in events
if item["type"] == "validated_commands"
]
self.assertEqual(set(validated_payloads[0]["function_configs"]), {"self_extension"})
validated = next(
payload for payload in validated_payloads
if {"native_climate", "self_extension"}.issubset(payload["function_configs"])
)
self.assertEqual(len(validated_payloads), 1)
validated = validated_payloads[0]
self.assertEqual(validated["status"], "processing")
self.assertEqual(
set(validated["function_configs"]),
@@ -2977,6 +2978,10 @@ operations:
[item["slot"] for item in validated["infrared_actions"]],
[1, 4],
)
self.assertEqual(
validated["parallel_dispatch"]["preflight_status"],
"succeeded",
)
def test_disabled_nfc_ir_command_does_not_discard_allowed_can_and_ir(
self,
@@ -3173,6 +3178,18 @@ operations:
)
extension.sync_peripheral_catalog(
[
{
"id": "toy-001-light-on",
"status": "learned",
"enabled": True,
"device_id": "toy-001",
"device_name": "闪烁灯玩具",
"object_names": ["闪烁灯玩具", "闪烁灯"],
"card_uid": "2CC8B103",
"action": "打开",
"action_names": ["打开", "开启", "启动"],
"slot": 1,
},
{
"id": "toy-001-light-red",
"status": "learned",
@@ -3239,6 +3256,11 @@ operations:
self.assertEqual(
result["infrared_actions"][0]["parse_source"], "cloud_model"
)
self.assertEqual(
[action["slot"] for action in result["infrared_actions"]],
[1, 3],
)
self.assertTrue(result["infrared_actions"][0]["auto_inserted"])
self.assertFalse(result["supplement_required"])
def test_main_request_executes_gpt_nfc_sequence_without_vehicle_can(self) -> None:
@@ -4113,6 +4135,75 @@ operations:
self.assertEqual(released["state"], "none")
self.assertIsNone(self.service.hardware_access.admin_snapshot()["active"])
def test_legacy_hardware_client_id_can_request_access_and_execute_mode_card(
self,
) -> None:
legacy_client_id = "legacy_mode_client_with_underscores"
def post(path: str, payload: dict, *, public: bool = False):
headers = {"Content-Type": "application/json"}
if public:
headers["X-Forwarded-For"] = "203.0.113.31"
request = urllib.request.Request(
f"{self.base_url}{path}",
method="POST",
headers=headers,
data=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
)
return self.opener.open(request, timeout=3)
with post(
"/api/hardware/access/request",
{"client_id": legacy_client_id, "label": "旧版模式终端"},
public=True,
) as response:
requested = json.loads(response.read().decode("utf-8"))
canonical_client_id = requested["client_id"]
self.assertRegex(canonical_client_id, r"^hwc-legacy-[0-9a-f]{32}$")
with post(
"/api/local/hardware-access/approve",
{"client_id": canonical_client_id},
):
pass
fake_result = {
"status": "succeeded",
"request_id": "req-legacy-mode-client",
"execution_mode": "hardware",
}
with patch.object(
self.service,
"execute_mode_profile_card",
return_value=fake_result,
) as execute:
with post(
"/api/mode-profiles/office/execute",
{
"conversation_id": "conv-legacy-mode-client",
"vehicle_id": "denza_n7",
"execution_mode": "hardware",
"hardware_client_id": legacy_client_id,
"client_type": "web_console_mode_card",
},
public=True,
) as response:
result = json.loads(response.read().decode("utf-8"))
self.assertEqual(result["status"], "succeeded")
self.assertEqual(
execute.call_args.kwargs["hardware_client_id"],
canonical_client_id,
)
with post(
"/api/hardware/access/release",
{"client_id": legacy_client_id},
public=True,
) as response:
released = json.loads(response.read().decode("utf-8"))
self.assertEqual(released["state"], "none")
def test_local_hardware_decision_page_is_not_exposed_through_proxy(self) -> None:
with self.opener.open(
f"{self.base_url}/hardware-control", timeout=3
@@ -271,7 +271,7 @@ CAPABILITY_CASES = (
),
_CapabilityCase(
"seat.set_driver_heating_native",
"打开主驾座椅加热2档",
"打开主驾座椅加热低档",
"driver",
{"state": "on", "level": 2},
"activate",
@@ -291,7 +291,7 @@ CAPABILITY_CASES = (
),
_CapabilityCase(
"seat.set_driver_ventilation_native",
"打开主驾座椅通风2档",
"打开主驾座椅通风低档",
"driver",
{"state": "on", "level": 2},
"activate",
@@ -48,6 +48,23 @@ def _learned_light() -> dict[str, object]:
}
def _learned_light_power_on() -> dict[str, object]:
return {
"id": "ir-slot:romance-light:1",
"status": "learned",
"enabled": True,
"device_id": "romance-light",
"device_name": "氛围灯带",
"object_names": ["氛围灯带", "灯带"],
"device_type": "light",
"card_uid": "2CC8B103",
"action": "打开",
"action_names": ["打开", "开启", "点亮"],
"risk": "low",
"slot": 1,
}
def test_scene_composer_mixed_can_ir_preview_save_and_revalidate(
tmp_path: Path,
) -> None:
@@ -61,13 +78,19 @@ def test_scene_composer_mixed_can_ir_preview_save_and_revalidate(
SimulationGateway(),
catalog_db_path=tmp_path / "infrared.sqlite3",
)
extension.sync_peripheral_catalog([_learned_light()])
extension.sync_peripheral_catalog(
[_learned_light_power_on(), _learned_light()]
)
catalog = AssistantCapabilityCatalog(
service,
peripheral_provider=extension.list_peripheral_routing_capabilities,
)
context = catalog.scene_composition_context(catalog.snapshot())
capability_ref = context["peripheral_actions"][0]["ref"]
capability_ref = next(
item["ref"]
for item in context["peripheral_actions"]
if "红色" in str(item.get("action_label") or "")
)
encoded_context = json.dumps(context, ensure_ascii=False)
assert "2CC8B103" not in encoded_context
assert '"slot":7' not in encoded_context
@@ -126,13 +149,19 @@ def test_scene_composer_mixed_can_ir_preview_save_and_revalidate(
"climate",
"self_extension",
]
assert preview["mode_profile_draft"]["operation_count"] == 2
assert preview["mode_profile_draft"]["operation_count"] == 3
assert set(preview["function_configs"]) == {"native_climate", "self_extension"}
infrared_command = preview["command_plans"]["self_extension"]["spec"][
infrared_commands = preview["command_plans"]["self_extension"]["spec"][
"commands"
][0]
assert infrared_command["message"] == "infrared.emit"
assert infrared_command["signals"] == {"slot": 7}
]
assert [command["message"] for command in infrared_commands] == [
"infrared.emit",
"infrared.emit",
]
assert [command["signals"] for command in infrared_commands] == [
{"slot": 1},
{"slot": 7},
]
assert preview["infrared_actions"][0]["parse_source"] == (
"scene_composer_capability_ref"
)
@@ -156,9 +185,10 @@ def test_scene_composer_mixed_can_ir_preview_save_and_revalidate(
assert [operation.capability for operation in profile.operations] == [
"climate.set_temperature",
"infrared.emit",
"infrared.emit",
]
entries = handler._saved_mode_infrared_entries(profile)
assert [entry["capability"]["slot"] for entry in entries] == [7]
assert [entry["capability"]["slot"] for entry in entries] == [1, 7]
native_execution = service.execute_mode_profile_card(
profile.id,
conversation_id="conv-saved-scene-peripheral",
@@ -179,7 +209,7 @@ def test_scene_composer_mixed_can_ir_preview_save_and_revalidate(
entries,
)
assert executed["status"] == "succeeded"
assert [action["slot"] for action in executed["infrared_actions"]] == [7]
assert [action["slot"] for action in executed["infrared_actions"]] == [1, 7]
invoked = handler._execute_request_with_peripherals(
"/api/requests/preview",
{
@@ -196,7 +226,7 @@ def test_scene_composer_mixed_can_ir_preview_save_and_revalidate(
assert invoked is not None
assert invoked["status"] == "succeeded"
assert invoked["execution_mode"] == "dry_run"
assert [action["slot"] for action in invoked["infrared_actions"]] == [7]
assert [action["slot"] for action in invoked["infrared_actions"]] == [1, 7]
template_plan = _catalog_exact_scene_template_plan(
catalog.snapshot(),
@@ -301,8 +331,8 @@ def test_scene_composer_mixed_can_ir_preview_save_and_revalidate(
assert explicit is not None
assert explicit["status"] == "succeeded"
assert explicit["execution_mode"] == "dry_run"
assert explicit["mode_profile_draft"]["operation_count"] == 2
assert explicit["infrared_actions"][0]["slot"] == 7
assert explicit["mode_profile_draft"]["operation_count"] == 3
assert [action["slot"] for action in explicit["infrared_actions"]] == [1, 7]
# Removing the durable NFC whitelist invalidates both cached refs and saved
# mode execution before any transport can be reached.
@@ -560,6 +560,64 @@ test("mode creation always requests dry-run even while the console is in hardwar
assert.equal(Object.hasOwn(request, "hardware_client_id"), false);
});
test("hardware requests use only the console's validated hardware client id", async () => {
const events = [
{ type: "message.accepted", route: "mode_profile_execute", engine: "n7web-mode-profile-runner" },
{ type: "answer.completed", route: "mode_profile_execute", answer: "阅读模式执行完成。" },
];
const validHarness = createHarness(events, "hardware");
validHarness.sandbox.CognitiveAgentConsole.getHardwareClientId = (
() => "hwc-runtime-client-1234"
);
validHarness.input.value = "进入阅读模式";
await validHarness.sandbox.CognitiveAgentAssistant.submit();
const validRequest = JSON.parse(validHarness.requestCalls[0].options.body);
assert.equal(validRequest.hardware_client_id, "hwc-runtime-client-1234");
const invalidHarness = createHarness(events, "hardware");
invalidHarness.sandbox.CognitiveAgentConsole.getHardwareClientId = (
() => "legacy_invalid_client"
);
invalidHarness.input.value = "进入浪漫观影模式";
await invalidHarness.sandbox.CognitiveAgentAssistant.submit();
const invalidRequest = JSON.parse(invalidHarness.requestCalls[0].options.body);
assert.equal(Object.hasOwn(invalidRequest, "hardware_client_id"), false);
});
test("partially completed imported mode uses neutral delegated wording", () => {
const harness = createHarness([]);
harness.sandbox.CognitiveAgentAssistant.announceModeExecution(
{ id: "cinema", name: "影院模式", command_flow: { function_configs: {}, plans: {} } },
{
status: "partially_succeeded",
execution_mode: "hardware",
execution_outcome: "submitted_unverified",
deferred_requests: [
{ execution_backend: "n7_vehicle_transfer" },
{ execution_backend: "n7_vehicle_transfer" },
],
function_configs: {},
command_plans: {},
execution_report: null,
operation_results: [],
},
{ command: "进入影院模式" },
);
const assistantMessage = harness.messages.children.at(-1);
assert.equal(
assistantMessage.dataset.messageContent,
"影院模式指令已提交,未由 PC 执行的 2 项动作将由车机继续处理。",
);
const actionStatus = assistantMessage.querySelector(".assistant-action-status").children.at(-1);
assert.equal(actionStatus.textContent, "功能模式指令已提交");
});
test("a follow-up turn carries only the active authoritative draft reference", async () => {
const events = modeDraftEvents("req-draft-one", 24);
const harness = createHarness(events);
+34 -3
View File
@@ -21,17 +21,17 @@ def test_console_contains_ai_assistant_surface():
assert 'placeholder="向 AI助手提问或交代工作"' in template
assert 'aria-label="发送消息"' in template
assert 'import "../assistant-chat.css";' in vite_entry
assert 'src="/assistant-chat.js?v=codex-assistant-35"' in html
assert 'src="/assistant-chat.js?v=codex-assistant-39-video-media"' in html
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")
assert (
'const CACHE_VERSION = "cognitive-agent-pad-v329-mode-preview"'
'const CACHE_VERSION = "cognitive-agent-pad-v334-video-media"'
in service_worker
)
assert '"/assistant-chat.css?v=codex-assistant-30"' in service_worker
assert '"/assistant-chat.js?v=codex-assistant-35"' in service_worker
assert '"/assistant-chat.js?v=codex-assistant-39-video-media"' in service_worker
def test_codex_frontend_uses_one_streaming_endpoint_and_agent_work_events():
@@ -95,6 +95,8 @@ def test_validated_controls_are_mirrored_into_a_compact_assistant_card():
assert 'count.className = "assistant-dispatched-controls-count"' in script
assert 'list.className = "assistant-dispatched-controls-list"' in script
assert "value.textContent = assistantControlValue(item)" in script
assert 'return ({ 1: "3档", 2: "1档" })[level]' in script
assert '{ 1: "3档(高)", 2: "1档(低)" }' in script
assert "语义控制指令:" in script
assert "实际控制指令:" in script
assert "appendDispatchedControls(message, event.payload)" in script
@@ -105,6 +107,9 @@ def test_validated_controls_are_mirrored_into_a_compact_assistant_card():
assert "appendDispatchedControls(message, modeExecutionPreviewResult(profile))" in script
assert "beginModeExecution," in script
assert "completeModeExecution," in script
assert "功能模式部分完成" not in script
assert "功能模式指令已提交" in script
assert "未由 PC 执行的" in script
assert "const operationByRef = new Map()" in script
assert 'deliveryState: "submitting"' in script
assert 'deliveryLabel: "正在下发"' in script
@@ -113,6 +118,10 @@ def test_validated_controls_are_mirrored_into_a_compact_assistant_card():
assert '"seat.set_legrest_height": "腿托"' in script
assert '"seat.set_legrest_extension": "腿托"' in script
assert '"ambient_light.set_illumination": "氛围灯颜色与亮度"' in script
assert '"android.launch_app": "打开视频媒体"' in script
assert 'if (capability === "android.launch_app")' in script
assert "parameters.package_name" in script
assert "parameters.activity_name" in script
assert '.replaceAll("_", "-")' in script
assert ".assistant-dispatched-controls > summary" in css
assert ".assistant-dispatched-controls-list > li" in css
@@ -130,6 +139,16 @@ def test_explicit_nfc_authorization_block_is_rendered_as_safety_blocked():
assert '? "安全阻止"' in console
def test_n7_video_launch_uses_chinese_mode_card_labels():
app = (ROOT / "apps/web-console/app.js").read_text(encoding="utf-8")
assert 'android: "车机媒体"' in app
assert 'headunit: "车机"' in app
assert '"android.launch_app": "打开视频媒体"' in app
assert 'package_name: "应用包名"' in app
assert 'activity_name: "启动页面"' in app
def test_latest_dispatched_control_group_is_the_only_group_expanded():
assistant = (ROOT / "apps/web-console/assistant-chat.js").read_text(
encoding="utf-8"
@@ -189,6 +208,18 @@ def test_codex_request_keeps_trusted_vehicle_context_without_legacy_model_routin
assert 'const executionMode = modeConfigurationPreview ? "dry_run"' in request_body
def test_codex_request_reuses_only_the_console_validated_hardware_client_id():
app = (ROOT / "apps/web-console/app.js").read_text(encoding="utf-8")
assistant = (ROOT / "apps/web-console/assistant-chat.js").read_text(encoding="utf-8")
assert "const HARDWARE_CLIENT_ID_PATTERN" in app
assert "function validatedHardwareClientId()" in app
assert "getHardwareClientId()" in app
assert "return validatedHardwareClientId();" in app
assert "window.CognitiveAgentConsole?.getHardwareClientId?.()" in assistant
assert "/^hwc-[a-z0-9-]{8,80}$/.test(value)" in assistant
def test_starting_a_new_codex_conversation_rotates_the_conversation_id():
script = (ROOT / "apps/web-console/assistant-chat.js").read_text(encoding="utf-8")
assert 'const CONVERSATION_STORAGE_KEY = "cognitive-agent.conversation-id"' in script
@@ -24,6 +24,14 @@ def compiled_mixed_result() -> dict[str, object]:
"provider_used": "local_rules+infrared_local_rules",
"execution_mode": "dry_run",
"control_transport": "wired_can",
"parallel_dispatch": {
"enabled": True,
"preflight_status": "succeeded",
"dispatch": {
"started_skew_ms": 1.2,
"total_ms": 120.5,
},
},
"function_configs": {
"native_climate": {
"spec": {
@@ -155,6 +163,10 @@ class ValidatedControlProjectionTest(unittest.TestCase):
"compile",
],
)
self.assertEqual(
projection["parallel_dispatch"]["preflight_status"],
"succeeded",
)
serialized = json.dumps(projection, ensure_ascii=False)
self.assertNotIn("command_plans", serialized)
self.assertNotIn("data_hex", serialized)
@@ -362,6 +374,10 @@ class ValidatedControlProjectionTest(unittest.TestCase):
)
self.assertNotIn("command_plans", safe_result)
self.assertNotIn("validated_control_commands", safe_result)
self.assertEqual(
safe_result["parallel_dispatch"]["preflight_status"],
"succeeded",
)
def test_agent_tool_path_queues_projection_for_the_browser_thread(self) -> None:
client = type("UnavailableClient", (), {"available": False})()
@@ -724,6 +724,50 @@ class Can1SemanticWhitelistTests(unittest.TestCase):
self.assertEqual(preview["unresolved_clauses"], [])
self.assertEqual(self._debug_commands(preview), [])
def test_driver_ventilation_numeric_gear_followup_uses_occupant_scale(self) -> None:
cases = (
(
"打开主驾座椅通风,挡位调到3档",
"seat driver vent high",
1,
),
(
"打开主驾座椅通风,挡位调到1档",
"seat driver vent low",
2,
),
)
for index, (utterance, command, internal_level) in enumerate(cases):
with self.subTest(utterance=utterance):
result = self.service.process(
utterance,
provider="local",
conversation_id=f"conv-driver-vent-gear-{index}",
execution_mode="dry_run",
)
self.assertEqual(result["status"], "succeeded")
self.assertEqual(self._debug_commands(result), [command])
operation = result["function_configs"]["native_seat"]["spec"][
"operations"
][0]
self.assertEqual(operation["parameters"]["level"], internal_level)
middle = self.service.process(
"打开主驾座椅通风,挡位调到2档",
provider="local",
conversation_id="conv-driver-vent-gear-middle",
execution_mode="dry_run",
)
self.assertEqual(middle["status"], "needs_confirmation")
self.assertEqual(self._debug_commands(middle), [])
self.assertIn(
"driver_seat_thermal_middle_level_unsupported",
{
item.get("reason")
for item in middle["intent_ir"]["spec"]["ambiguities"]
},
)
def test_vehicle_position_aliases_stay_scoped_to_the_device_domain(self) -> None:
cases = (
("打开左前车窗", ["window front-left open"]),
@@ -523,6 +523,56 @@ def test_registered_mode_execution_uses_fast_local_route_without_codex(
assert "模拟编排" in result["answer"]
def test_partially_supported_imported_mode_uses_submitted_not_partial_wording() -> None:
client = FakeCodexClient(available=False)
assistant = CodexAssistantService(client)
catalog = {
"modes": [
{
"id": "cinema",
"name": "影院模式",
"aliases": [],
"assistant_callable": True,
}
],
"source_status": {"modes": "ready"},
}
def execute(instruction: str, progress: Callable[[dict[str, object]], None]):
result = {
"request_id": "req-imported-cinema-partial",
"utterance": instruction,
"status": "partially_succeeded",
"execution_mode": "hardware",
"execution_outcome": "submitted_unverified",
"deferred_requests": [
{
"execution_backend": "n7_vehicle_transfer",
"capability": "android.launch_app",
}
],
}
progress(result)
return result
result, events = _stream(
assistant,
"进入影院模式",
execute_control=execute,
capability_snapshot=catalog,
)
assert result["route"] == "mode_profile_execute"
assert result["success"] is True
assert "已提交" in result["answer"]
assert "部分完成" not in result["answer"]
assert "未能完成" not in result["answer"]
assert any(
event.get("display_text") == "功能模式已提交,等待车辆状态确认"
for event in events
)
def test_registered_name_only_mode_keeps_the_fast_local_route_without_codex() -> None:
client = FakeCodexClient(available=False)
assistant = CodexAssistantService(client)
+42
View File
@@ -554,6 +554,48 @@ class DriverSeatNativeCanTests(unittest.TestCase):
self.assertEqual(bus.send_calls, 4)
self.assertEqual(len(bus.sent), 3)
def test_window_already_at_target_is_verified_success_without_resend(self) -> None:
config = self.runtime.build_function_config(
"window",
[
{
"domain": "window",
"capability": "window.set_position",
"target": "rear_left",
"operation": "set",
"parameters": {"position_pct": 0},
}
],
"req-window-already-closed",
"local_rules",
)
plan = self.runtime.compile(config)
feedback = CanFrame(
0x388,
bytes.fromhex("00 00 01 00 01 00 66 00"),
channel=1,
is_fd=True,
bitrate_switch=True,
)
bus = _FakeBus([feedback])
report = NativeCanExecutor(
lambda: bus,
feedback_timeout_ms=1,
feedback_required=False,
).execute(self.runtime, plan, config)
self.assertEqual(report["spec"]["status"], "succeeded")
operation = report["spec"]["results"][0]
self.assertEqual(operation["status"], "succeeded")
self.assertEqual(operation["telemetry"]["sent_frames"], [])
self.assertEqual(
operation["telemetry"]["verification_status"],
"verified_success",
)
self.assertEqual(operation["telemetry"]["delivery_status"], "not_required")
self.assertTrue(operation["telemetry"]["physical_effect_confirmed"])
def test_memory_compile_rejects_missing_or_malformed_learned_slots(self) -> None:
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "driver_memory.json"
@@ -37,11 +37,11 @@ def test_energy_monitor_assets_are_versioned_and_cached() -> None:
assert f'"{asset}"' in service_worker
assert 'import "../energy-monitor.css";' in vite_entry
assert "/energy-monitor.js?v=energy-monitor-5-performance" in html
assert html.index("/assistant-chat.js?v=codex-assistant-35") < html.index(
assert html.index("/assistant-chat.js?v=codex-assistant-39-video-media") < html.index(
"/energy-monitor.js?v=energy-monitor-5-performance"
)
assert (
'const CACHE_VERSION = "cognitive-agent-pad-v329-mode-preview"'
'const CACHE_VERSION = "cognitive-agent-pad-v334-video-media"'
in service_worker
)
+46 -4
View File
@@ -157,6 +157,42 @@ def test_authorization_requires_exact_enabled_learned_bound_ability() -> None:
authorize_extension_control_actions(parsed, disallowed)
def test_authorization_injects_same_device_power_on_before_color() -> None:
parsed = parse_extension_control_payload(
encoded(request(action(3, "切换为红色")))
)
catalog = [
capability("light-on", 2, "打开"),
capability("light-red", 3, "切换为红色"),
]
authorized = authorize_extension_control_actions(parsed, catalog)
ordered = sorted(
authorized,
key=lambda item: (item["at_ms"], item["sequence_order"]),
)
assert [item["action"]["parameters"]["slot"] for item in ordered] == [2, 3]
assert ordered[0]["auto_inserted"] is True
assert ordered[0]["dependency"] == "power_on_before_color"
assert ordered[1]["auto_inserted"] is False
def test_authorization_rejects_color_without_same_device_power_on() -> None:
parsed = parse_extension_control_payload(
encoded(request(action(3, "切换为红色")))
)
with pytest.raises(
ExtensionControlAuthorizationError,
match="power-on action",
):
authorize_extension_control_actions(
parsed,
[capability("light-red", 3, "切换为红色")],
)
def test_consumer_validates_entire_request_then_executes_by_at_ms() -> None:
catalog = [
capability("light-on", 2, "打开"),
@@ -184,15 +220,21 @@ def test_consumer_validates_entire_request_then_executes_by_at_ms() -> None:
)
)
assert wait_until(lambda: consumer.snapshot()["state"] == "completed")
assert [item[1]["parameters"]["slot"] for item in executions] == [3, 2]
assert [item[1]["parameters"]["slot"] for item in executions] == [2, 3, 2]
status = consumer.snapshot()
assert status["processed_count"] == 1
assert status["executed_action_count"] == 2
assert [item["slot"] for item in status["last_results"]] == [3, 2]
assert [item["slot"] for item in status["action_statuses"]] == [3, 2]
assert status["executed_action_count"] == 3
assert [item["slot"] for item in status["last_results"]] == [2, 3, 2]
assert [item["slot"] for item in status["action_statuses"]] == [2, 3, 2]
assert [item["auto_inserted"] for item in status["action_statuses"]] == [
True,
False,
False,
]
assert [item["state"] for item in status["action_statuses"]] == [
"submitted_unverified",
"submitted_unverified",
"submitted_unverified",
]
assert all(
item["state_label"] == "已下发 · 物理效果未确认"
@@ -92,12 +92,12 @@ def test_performance_assets_have_a_new_cache_generation() -> None:
worker = source("service-worker.js")
for asset in (
"/app.js?v=dual-seat-145-n7-dispatch",
"/console-v2.js?v=local-first-77-performance",
"/self-extension.js?v=nfc-mqtt-56",
"/app.js?v=dual-seat-147-video-media",
"/console-v2.js?v=local-first-79-cinema-status",
"/self-extension.js?v=nfc-persistent-57",
"/energy-monitor.js?v=energy-monitor-5-performance",
):
assert asset in html
assert f'"{asset}"' in worker
assert 'const CACHE_VERSION = "cognitive-agent-pad-v329-mode-preview"' in worker
assert 'const CACHE_VERSION = "cognitive-agent-pad-v334-video-media"' in worker
assert '"/codepen-glass-theme.css?v=creative-cloud-6-performance"' in worker
+1 -1
View File
@@ -54,7 +54,7 @@ def test_theme_asset_versions_are_consistent_with_the_pwa_shell() -> None:
assert source_asset in main
assert f'"{cached_asset}"' in service_worker
assert 'import "./styles/design-system.css"' in main
assert 'const CACHE_VERSION = "cognitive-agent-pad-v329-mode-preview"' in service_worker
assert 'const CACHE_VERSION = "cognitive-agent-pad-v334-video-media"' in service_worker
def test_reference_glass_theme_covers_the_complete_workspace() -> None:
+312
View File
@@ -205,6 +205,55 @@ def cinema_scene_card() -> dict[str, object]:
}
def cinema_peripheral_catalog() -> list[dict[str, object]]:
def capability(
identity: str,
device_id: str,
device_name: str,
action: str,
slot: int,
) -> dict[str, object]:
return {
"id": identity,
"status": "learned",
"enabled": True,
"device_id": device_id,
"device_name": device_name,
"device_aliases": [device_name],
"object_names": [device_name],
"card_uid": "2CC8B103" if "flash" in device_id else "1A3AB103",
"action": action,
"aliases": [action],
"action_names": [action],
"risk": "low",
"slot": slot,
}
return [
capability("flash-on", "flash-light", "闪烁灯", "打开", 1),
capability("flash-blue", "flash-light", "闪烁灯", "切换为蓝色", 3),
capability("flash-red", "flash-light", "闪烁灯", "切换为红色", 4),
capability("strip-on", "light-strip", "灯带", "打开", 21),
capability("strip-blue", "light-strip", "灯带", "切换为蓝色", 23),
capability("strip-red", "light-strip", "灯带", "切换为红色", 24),
]
def cinema_video_launch_action() -> dict[str, object]:
return {
"type": "android_app_launch",
"at_ms": 3000,
"domain": "android",
"capability": "android.launch_app",
"target_scope": "headunit",
"operation": "launch",
"parameters": {
"activity_name": "com.byd.videoplay.activity.WelComeActivity",
"package_name": "com.byd.videoplay",
},
}
def wait_until(predicate, timeout: float = 1.5) -> bool:
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
@@ -337,6 +386,269 @@ def test_import_upserts_yaml_refreshes_catalog_and_does_not_echo(
assert "upstream_origin: user" in yaml_text
def test_later_n7_cinema_mode_inherits_base_and_adds_external_light_colors(
tmp_path: Path,
) -> None:
service = configured_service(tmp_path)
service.set_mode_scene_peripheral_catalog_provider(cinema_peripheral_catalog)
base = cinema_scene_card()
base_action_count = len(base["actions"])
base_result = service.import_n7_mode_scene(base)
assert "cinema_augmentation" not in base_result
variant = cinema_scene_card()
variant.update(
id="blue-cinema",
label="蓝色影院模式",
name="蓝色影院模式",
aliases=["蓝色观影模式"],
description="在车机影院模式基础上增加蓝色外设灯光。",
domains=["climate"],
operation_count=1,
actions=[
{
"type": "climate_temperature",
"at_ms": 0,
"domain": "climate",
"capability": "climate.set_temperature",
"target_scope": "vehicle",
"operation": "set",
"parameters": {"temperature_c": 23},
}
],
profile_revision="blue-cinema-revision",
)
imported = service.import_n7_mode_scene(variant)
augmentation = imported["cinema_augmentation"]
assert augmentation == {
"base_scene_id": "cinema",
"base_action_count": base_action_count,
"incoming_action_count": 1,
"peripheral_action_count": 4,
"merged_action_count": base_action_count + 4,
}
profile = service.get_mode_profile_card("blue-cinema", "denza_n7")
source_scene = profile.command_flow["source_scene"]
actions = source_scene["actions"]
assert source_scene["operation_count"] == base_action_count + 4
assert "self_extension" in source_scene["domains"]
temperatures = [
action["parameters"]["temperature_c"]
for action in actions
if action["capability"] == "climate.set_temperature"
]
assert temperatures == [23]
peripheral_actions = [
action
for action in actions
if action["capability"] == "infrared.emit"
]
assert [
(
action["target_scope"],
action["parameters"]["action"],
action["parameters"]["slot"],
)
for action in peripheral_actions
] == [
("flash-light", "打开", 1),
("flash-light", "切换为蓝色", 3),
("light-strip", "打开", 21),
("light-strip", "切换为蓝色", 23),
]
repeated = service.import_n7_mode_scene(variant)
assert (
repeated["cinema_augmentation"]["merged_action_count"]
== base_action_count + 4
)
reloaded = service.get_mode_profile_card("blue-cinema", "denza_n7")
assert (
len(reloaded.command_flow["source_scene"]["actions"])
== base_action_count + 4
)
def test_n7_cinema_variant_is_not_augmented_before_base_arrives(
tmp_path: Path,
) -> None:
service = configured_service(tmp_path)
service.set_mode_scene_peripheral_catalog_provider(cinema_peripheral_catalog)
variant = cinema_scene_card()
variant.update(
id="family-movie",
name="家庭影院模式",
label="家庭影院模式",
profile_revision="family-movie-revision",
)
imported = service.import_n7_mode_scene(variant)
assert "cinema_augmentation" not in imported
profile = service.get_mode_profile_card("family-movie", "denza_n7")
assert not any(
operation.capability == "infrared.emit"
for operation in profile.operations
)
def test_created_romantic_cinema_persists_and_transfers_n7_video_launch(
tmp_path: Path,
) -> None:
service = configured_service(tmp_path)
base = cinema_scene_card()
base["actions"] = [*base["actions"], cinema_video_launch_action()]
base["operation_count"] = len(base["actions"])
base["domains"] = [*base["domains"], "android"]
service.import_n7_mode_scene(base)
request_id = "req-romantic-cinema-video"
instruction = "进入浪漫观影模式"
result_dir = service.artifact_root / request_id
result_dir.mkdir(parents=True)
(result_dir / "result.json").write_text(
json.dumps(
{
"request_id": request_id,
"status": "succeeded",
"execution_mode": "dry_run",
"control_transport": "wireless_mqtt",
"utterance": instruction,
"vehicle": {"id": "denza_n7"},
"function_configs": {
"climate": {
"spec": {
"domain": "climate",
"operations": [
{
"id": "vehicle-climate-power",
"capability": "climate.set_power",
"target": {"zone": "vehicle"},
"parameters": {"state": "on"},
}
],
}
}
},
"command_plans": {
"climate": {
"spec": {
"commands": [
{
"operation_ref": "vehicle-climate-power",
"message": "climate.set_power",
}
]
}
}
},
},
ensure_ascii=False,
),
encoding="utf-8",
)
saved = service.create_mode_profile_card(
"浪漫观影模式",
instruction,
request_id,
"denza_n7",
)
profile = service.get_mode_profile_card(
str(saved["mode_profile"]["id"]),
"denza_n7",
)
assert [
operation.capability for operation in profile.operations
] == ["climate.set_power", "android.launch_app"]
assert profile.command_flow["n7_transfer_actions"] == [
cinema_video_launch_action()
]
local_requests, transfer_actions = service._mode_requests_and_n7_transfers(
profile
)
assert [request["capability"] for request in local_requests] == [
"climate.set_power"
]
assert transfer_actions == [cinema_video_launch_action()]
exported = next(
scene
for scene in service.mode_scene_catalog("denza_n7")["scenes"]
if scene["id"] == profile.id
)
video = next(
action
for action in exported["actions"]
if action["capability"] == "android.launch_app"
)
assert video == cinema_video_launch_action()
def test_existing_local_cinema_mode_publishes_video_action_only_in_hardware(
tmp_path: Path,
) -> None:
service = configured_service(tmp_path)
base = cinema_scene_card()
base["actions"] = [*base["actions"], cinema_video_launch_action()]
base["operation_count"] = len(base["actions"])
base["domains"] = [*base["domains"], "android"]
service.import_n7_mode_scene(base)
profile = service.mode_profiles.save_mode(
"浪漫观影模式",
[
{
"domain": "android",
"capability": "android.launch_app",
"target_scope": "headunit",
"operation": "launch",
"parameters": deepcopy(cinema_video_launch_action()["parameters"]),
}
],
vehicle_id="denza_n7",
instruction="进入浪漫观影模式",
command_flow={
"schema": "cognitive-agent-mode-command-flow/v1",
"plans": {"saved": {"spec": {"commands": [{"message": "saved"}]}}},
},
)
publisher = MagicMock(
return_value={
"topic": "transto_n7/ctl",
"qos": 1,
"retain": False,
"delivery_status": "published",
"message_id": 19,
}
)
service._vehicle_mqtt_transport.publish_n7_control_transfer = publisher
preview = service.execute_mode_profile_card(
profile.id,
execution_mode="dry_run",
control_transport="wireless_mqtt",
)
publisher.assert_not_called()
assert preview["n7_transfer_candidates"] == [cinema_video_launch_action()]
executed = service.execute_mode_profile_card(
profile.id,
execution_mode="hardware",
control_transport="wireless_mqtt",
)
assert executed["n7_control_transfer"]["state"] == "published"
publisher.assert_called_once()
payload = publisher.call_args.args[0]
assert payload["scene_id"] == profile.id
assert payload["scene_name"] == "浪漫观影模式"
assert payload["operation_count"] == 1
assert payload["actions"] == [cinema_video_launch_action()]
def test_unknown_n7_action_is_forwarded_once_only_during_hardware_execution(
tmp_path: Path,
) -> None:
+9 -7
View File
@@ -17,7 +17,7 @@ def test_unified_console_exposes_device_id_based_nfc_authorization() -> None:
vite_entry = source("src/main.js")
assert 'data-open-nfc-binding' in script
assert '靠近读卡器后' in script
assert '请将 NFC 卡靠近读卡器' in script
assert '<h2>NFC 设备授权</h2>' in script
assert 'NFC 设备授权' in script
assert 'api("nfc/status")' in script
@@ -55,23 +55,25 @@ def test_unified_console_exposes_device_id_based_nfc_authorization() -> None:
assert '学习指令已下发,但物理槽位回读为空' in script
assert 'signalSource: "n7web"' in script
assert 'if (capability.signalSource === "n7web") return' in script
assert 'const activeCatalog = sensedCardUid' in script
assert 'normalizeCardUid(item.card_uid) === sensedCardUid' in script
assert 'const persistentCatalog = (' in script
assert 'durablePeripheralCatalogHydrated' in script
assert 'durablePeripheralCatalog' in script
assert 'normalizeCardUid(item.card_uid)' in script
assert '&& item.status === "learned"' in script
assert 'slot: capability.slot' in script
assert 'device_aliases: device?.aliases || []' in script
assert 'async function hydratePeripheralCatalog()' in script
assert 'api("peripherals")' in script
assert 'signalSource: "backend_catalog"' in script
assert '当前未感应到外设' in script
assert '将已绑定设备 ID 的 NFC 卡靠近读卡器后显示命令' in script
assert '当前没有已绑定外设' in script
assert '外设绑定 NFC 后将持续显示,解除绑定后移除' in script
assert '.v2-nfc-inline' in styles
assert '.v2-nfc-dialog' in styles
assert '.v2-nfc-command-list' in styles
assert '.v2-nfc-command-option' in styles
assert '.v2-nfc-select-all' in styles
assert 'self-extension.js?v=nfc-mqtt-56' in html
assert 'self-extension.js?v=nfc-persistent-57' in html
assert 'import "../self-extension.css";' in vite_entry
@@ -219,5 +221,5 @@ def test_main_parsed_control_panel_renders_validated_infrared_operations() -> No
assert "下发控制指令" in console
assert "已解析控制指令" not in console
assert 'li[data-can-channel="IR"]' in styles
assert "console-v2.js?v=local-first-77-performance" in html
assert "console-v2.js?v=local-first-79-cinema-status" in html
assert 'speech_semantic: "语音语义"' in console
@@ -0,0 +1,305 @@
from __future__ import annotations
import threading
import time
from types import SimpleNamespace
from unittest.mock import patch
import pytest
from cognitive_agent.server import SeatDemoHandler
def _result(mode: str, *, status: str = "succeeded") -> dict[str, object]:
return {
"request_id": f"req-{mode}-{status}",
"status": status,
"success": status == "succeeded",
"execution_mode": mode,
"decision_summary": [],
"function_configs": {},
"command_plans": {},
"trace_events": [],
"operation_results": [],
"execution_report": None,
"control_transport": "test",
}
def _entries() -> list[dict[str, object]]:
return [
{
"index": 0,
"clause": "打开闪烁灯",
"capability": {
"id": "ir-slot:flash:2",
"device_id": "flash",
"device_name": "闪烁灯",
"action": "打开",
"slot": 2,
"parse_source": "local_rule",
},
}
]
def test_hardware_branches_preflight_then_start_together() -> None:
handler = object.__new__(SeatDemoHandler)
lock = threading.Lock()
preflight_completed: set[str] = set()
hardware_started: dict[str, float] = {}
def mark_preflight(name: str) -> dict[str, object]:
time.sleep(0.03)
with lock:
preflight_completed.add(name)
return _result("dry_run")
def execute_can() -> dict[str, object]:
with lock:
assert preflight_completed == {"can", "peripheral"}
hardware_started["can"] = time.perf_counter()
time.sleep(0.12)
return _result("hardware")
def execute_peripheral(
_utterance: str,
_entries_value: list[dict[str, object]],
*,
execution_mode: str,
**_kwargs: object,
) -> dict[str, object]:
if execution_mode == "dry_run":
return mark_preflight("peripheral")
with lock:
assert preflight_completed == {"can", "peripheral"}
hardware_started["peripheral"] = time.perf_counter()
time.sleep(0.12)
return _result("hardware")
with patch.object(
handler,
"_execute_local_peripheral_actions",
side_effect=execute_peripheral,
):
result = handler._execute_can_and_peripheral_branches(
utterance="打开空调和闪烁灯",
entries=_entries(),
execution_mode="hardware",
provider="local",
conversation_id="conv-parallel-unit",
can_execute=execute_can,
can_preflight=lambda: mark_preflight("can"),
parallel_eligible=True,
)
assert result["status"] == "succeeded"
assert result["parallel_dispatch"]["enabled"] is True
assert result["parallel_dispatch"]["preflight_status"] == "succeeded"
assert abs(hardware_started["can"] - hardware_started["peripheral"]) < 0.05
dispatch = result["parallel_dispatch"]["dispatch"]
branch_durations = [
item["duration_ms"] for item in dispatch["branches"].values()
]
assert dispatch["total_ms"] < sum(branch_durations)
def test_failed_preflight_starts_neither_hardware_branch() -> None:
handler = object.__new__(SeatDemoHandler)
hardware_calls: list[str] = []
def execute_peripheral(
_utterance: str,
_entries_value: list[dict[str, object]],
*,
execution_mode: str,
**_kwargs: object,
) -> dict[str, object]:
if execution_mode == "hardware":
hardware_calls.append("peripheral")
return _result(execution_mode)
with patch.object(
handler,
"_execute_local_peripheral_actions",
side_effect=execute_peripheral,
):
result = handler._execute_can_and_peripheral_branches(
utterance="打开空调和闪烁灯",
entries=_entries(),
execution_mode="hardware",
provider="local",
conversation_id="conv-parallel-preflight-failure",
can_execute=lambda: hardware_calls.append("can") or _result("hardware"),
can_preflight=lambda: _result(
"dry_run",
status="validation_failed",
),
parallel_eligible=True,
)
assert hardware_calls == []
assert result["status"] == "validation_failed"
assert result["execution_mode"] == "dry_run"
assert result["hardware_execution_allowed"] is False
assert result["parallel_dispatch"]["enabled"] is False
assert result["parallel_dispatch"]["preflight_status"] == "failed"
def test_wireless_parallel_dispatch_does_not_require_local_bench_lease() -> None:
handler = object.__new__(SeatDemoHandler)
authorization_calls: list[str | None] = []
handler.service = SimpleNamespace(
hardware_access=SimpleNamespace(
ensure_authorized=lambda client_id: authorization_calls.append(client_id)
)
)
handler._ensure_parallel_hardware_authorized(
execution_mode="hardware",
control_transport="wireless_mqtt",
hardware_access_bypass=False,
hardware_client_id=None,
)
assert authorization_calls == []
def test_wired_parallel_dispatch_keeps_local_bench_lease_gate() -> None:
handler = object.__new__(SeatDemoHandler)
authorization_calls: list[str | None] = []
handler.service = SimpleNamespace(
hardware_access=SimpleNamespace(
ensure_authorized=lambda client_id: authorization_calls.append(client_id)
)
)
handler._ensure_parallel_hardware_authorized(
execution_mode="hardware",
control_transport="wired_can",
hardware_access_bypass=False,
hardware_client_id="hwc-wired-parallel-test",
)
assert authorization_calls == ["hwc-wired-parallel-test"]
@pytest.mark.parametrize(
"utterance",
(
"先打开空调,再打开闪烁灯",
"打开空调,然后打开闪烁灯",
"打开空调,2秒后打开闪烁灯",
),
)
def test_explicit_cross_transport_order_disables_parallelism(
utterance: str,
) -> None:
assert SeatDemoHandler._mixed_request_requires_transport_order(utterance)
def test_ineligible_flow_keeps_existing_can_then_peripheral_order() -> None:
handler = object.__new__(SeatDemoHandler)
calls: list[str] = []
def execute_peripheral(
_utterance: str,
_entries_value: list[dict[str, object]],
*,
execution_mode: str,
**_kwargs: object,
) -> dict[str, object]:
calls.append(f"peripheral:{execution_mode}")
return _result(execution_mode)
def unexpected_preflight() -> dict[str, object]:
raise AssertionError("ordered flow must not run a duplicate preflight")
with patch.object(
handler,
"_execute_local_peripheral_actions",
side_effect=execute_peripheral,
):
result = handler._execute_can_and_peripheral_branches(
utterance="先打开空调,再打开闪烁灯",
entries=_entries(),
execution_mode="hardware",
provider="local",
conversation_id="conv-ordered-unit",
can_execute=lambda: calls.append("can:hardware") or _result("hardware"),
can_preflight=unexpected_preflight,
parallel_eligible=False,
)
assert calls == ["can:hardware", "peripheral:hardware"]
assert result["status"] == "succeeded"
assert "parallel_dispatch" not in result
def test_saved_static_mode_uses_parallel_can_and_peripheral_path() -> None:
handler = object.__new__(SeatDemoHandler)
can_modes: list[str] = []
peripheral_modes: list[str] = []
persisted: list[dict[str, object]] = []
profile = SimpleNamespace(
id="mode-parallel",
schedule=None,
command_flow={"execution_steps": []},
public_dict=lambda: {"id": "mode-parallel", "name": "并行模式"},
)
def execute_mode_profile_card(
_mode_id: str,
**kwargs: object,
) -> dict[str, object]:
mode = str(kwargs.get("execution_mode") or "")
can_modes.append(mode)
return _result(mode)
handler.service = SimpleNamespace(
execute_mode_profile_card=execute_mode_profile_card,
persist_composed_result=lambda result: persisted.append(result),
)
def execute_peripheral(
_utterance: str,
_entries_value: list[dict[str, object]],
*,
execution_mode: str,
**_kwargs: object,
) -> dict[str, object]:
peripheral_modes.append(execution_mode)
return _result(execution_mode)
with (
patch.object(
handler,
"_exact_local_peripheral_mode_invocation",
return_value=profile,
),
patch.object(handler, "_exact_n7_mode_invocation", return_value=None),
patch.object(handler, "_saved_mode_infrared_entries", return_value=_entries()),
patch.object(
handler,
"_execute_local_peripheral_actions",
side_effect=execute_peripheral,
),
):
result = handler._execute_n7_named_mode_request(
"/api/requests",
{
"utterance": "进入并行模式",
"execution_mode": "hardware",
"provider": "local",
"conversation_id": "conv-saved-parallel",
"vehicle_id": "denza_n7",
"control_transport": "wired_can",
},
)
assert result is not None
assert can_modes == ["dry_run", "hardware"]
assert peripheral_modes == ["dry_run", "hardware"]
assert result["parallel_dispatch"]["enabled"] is True
assert result["provider_used"] == "saved_mode_command_flow"
assert persisted == [result]
@@ -937,6 +937,22 @@ def test_delivery_status_distinguishes_blocked_failed_aborted_and_verified() ->
"physical_effect_confirmed": True,
},
),
sample(
{
"id": "window-feedback-confirmed",
"capability": "window.set_position",
"target": {"zone": "rear-left"},
"parameters": {"position_pct": 0},
},
status="failed",
message="MQTT 白名单控制帧已进入客户端发布队列;车辆效果以状态话题回读为准。",
telemetry={
"delivery_status": "submitted",
"verification_status": "verified_success",
"feedback_match": True,
"physical_effect_confirmed": True,
},
),
sample(
{
"id": "climate-post-submit-failed",
@@ -970,6 +986,7 @@ def test_delivery_status_distinguishes_blocked_failed_aborted_and_verified() ->
"下发失败",
"已中止",
"执行已确认",
"执行已确认",
"执行失败",
]
assert [item["deliveryState"] for item in commands] == [
@@ -977,6 +994,7 @@ def test_delivery_status_distinguishes_blocked_failed_aborted_and_verified() ->
"failed",
"aborted",
"submitted",
"submitted",
"failed",
]
assert "0x388 实时位置" in commands[0]["deliveryDetail"]
+122 -4
View File
@@ -1406,6 +1406,15 @@ class SelfExtensionTests(unittest.TestCase):
def test_main_peripheral_sequence_executes_validated_actions_and_delays(self) -> None:
service = SelfExtensionService(SimulationGateway())
catalog = [
{
"id": "on",
"status": "learned",
"device_id": "third-party-light",
"device_name": "第三方氛围灯",
"card_uid": "1A53DD95",
"action": "打开灯光",
"slot": 1,
},
{
"id": "red",
"status": "learned",
@@ -1457,16 +1466,24 @@ class SelfExtensionTests(unittest.TestCase):
self.assertEqual(
result["orchestration_route"], "nfc_peripheral_sequence"
)
self.assertEqual(result["sequence"]["step_count"], 3)
self.assertEqual(result["sequence"]["action_count"], 2)
self.assertEqual(result["sequence"]["emit_count"], 2)
self.assertEqual(result["sequence"]["step_count"], 4)
self.assertEqual(result["sequence"]["action_count"], 3)
self.assertEqual(result["sequence"]["emit_count"], 3)
self.assertEqual(result["sequence"]["total_delay_ms"], 5000)
self.assertEqual(result["sequence"]["steady_blink_fallbacks"], 1)
self.assertEqual(
[item["status"] for item in result["operation_results"]],
["succeeded", "succeeded", "succeeded"],
["succeeded", "succeeded", "succeeded", "succeeded"],
)
self.assertEqual(
[
item["parameters"]["slot"]
for item in result["function_config"]["spec"]["operations"]
],
[1, 3, 4],
)
sleep.assert_any_call(5.0)
sleep.assert_any_call(0.8)
self.assertNotIn(0.5, [call.args[0] for call in sleep.call_args_list])
def test_failed_first_emit_retains_complete_compiled_infrared_plan(self) -> None:
@@ -1630,6 +1647,107 @@ class SelfExtensionTests(unittest.TestCase):
)
)
def test_color_only_request_automatically_executes_power_on_first(self) -> None:
with TemporaryDirectory() as directory:
service = SelfExtensionService(
SimulationGateway(),
catalog_db_path=Path(directory, "infrared.sqlite3"),
)
service.sync_peripheral_catalog(
[
{
"id": "flash-on",
"status": "learned",
"enabled": True,
"device_id": "flash-light",
"device_name": "闪烁灯",
"object_names": ["闪烁灯"],
"card_uid": "2CC8B103",
"action": "打开",
"action_names": ["打开", "开启", "启动"],
"slot": 1,
},
{
"id": "flash-blue",
"status": "learned",
"enabled": True,
"device_id": "flash-light",
"device_name": "闪烁灯",
"object_names": ["闪烁灯"],
"card_uid": "2CC8B103",
"action": "切换为蓝色",
"action_names": ["蓝色", "设置为蓝色"],
"slot": 3,
},
]
)
color = next(
item
for item in service.list_peripheral_capabilities()
if item["id"] == "flash-blue"
)
result = service.execute_peripheral_request(
"把闪烁灯设置为蓝色",
color,
conversation_id="conv-color-power-dependency",
execution_mode="dry_run",
)
self.assertEqual(result["status"], "succeeded")
self.assertEqual(result["sequence"]["action_count"], 2)
self.assertEqual(
[
item["parameters"]["slot"]
for item in result["function_config"]["spec"]["operations"]
],
[1, 3],
)
self.assertEqual(
[
item["signals"]["slot"]
for item in result["command_plan"]["spec"]["commands"]
],
[1, 3],
)
service.close()
def test_color_request_is_rejected_when_power_on_is_not_authorized(self) -> None:
with TemporaryDirectory() as directory:
service = SelfExtensionService(
SimulationGateway(),
catalog_db_path=Path(directory, "infrared.sqlite3"),
)
service.sync_peripheral_catalog(
[
{
"id": "strip-red",
"status": "learned",
"enabled": True,
"device_id": "light-strip",
"device_name": "灯带",
"object_names": ["灯带"],
"card_uid": "1A3AB103",
"action": "切换为红色",
"action_names": ["红色", "设置为红色"],
"slot": 24,
}
]
)
color = service.list_peripheral_capabilities()[0]
with self.assertRaisesRegex(
ExtensionIntegrationError,
"设置颜色前需要先打开",
):
service.execute_peripheral_request(
"把灯带设置为红色",
color,
conversation_id="conv-color-missing-power",
execution_mode="dry_run",
)
service.close()
def test_color_sequence_never_cycles_the_power_off_capability(self) -> None:
service = SelfExtensionService(SimulationGateway())
catalog = [
@@ -37,11 +37,29 @@ def test_peripheral_catalog_sync_is_serialized_and_tracks_backend_revision() ->
)
assert "peripheralCatalogSyncQueue" in sync
assert "const nextCatalog = peripheralCatalog();" in sync
assert "catalog_revision: peripheralCatalogRevision" in sync
assert "result.catalog_revision" in sync
assert "applyDurablePeripheralCatalog(" in sync
assert "if (throwOnError) throw error;" in sync
def test_backend_catalog_is_authoritative_for_persistent_device_cards() -> None:
source = SOURCE_PATH.read_text(encoding="utf-8")
hydrate = _function_source(
source,
" async function hydratePeripheralCatalog() {",
"\n function vehicleCatalog",
)
bootstrap = source[source.index(" void hydratePeripheralCatalog()") :]
assert "applyDurablePeripheralCatalog(" in hydrate
assert "if (!catalog.length)" in hydrate
assert "renderAll();" in hydrate
assert ".finally(() => syncPeripheralCatalog())" not in bootstrap
assert "return syncPeripheralCatalog();" in bootstrap
def test_bound_card_can_be_reassigned_to_a_new_device() -> None:
source = SOURCE_PATH.read_text(encoding="utf-8")
render = _function_source(
+16 -9
View File
@@ -429,23 +429,26 @@ class SpeechCapabilityAlignmentTests(unittest.TestCase):
result = self.processor.process("打开座椅通风并调到三档")
self.assertEqual(
[command.capability for command in result.commands],
[
"seat.set_driver_ventilation_native",
"seat.set_driver_ventilation_native",
],
["seat.set_driver_ventilation_native"],
)
self.assertEqual(
[command.target_scope for command in result.commands],
["driver", "driver"],
["driver"],
)
self.assertIn("3档", result.clean_text)
self.assertEqual(result.semantic_status, "recognized_unsupported")
self.assertFalse(result.execution_eligible)
self.assertEqual(result.commands[0].parameters, {"state": "on", "level": 1})
self.assertEqual(result.commands[0].qualifiers["display_level"], "3档")
self.assertEqual(result.semantic_status, "recognized_executable")
self.assertTrue(result.execution_eligible)
passenger = self.processor.process("打开副驾座椅通风并调到三档")
self.assertEqual(
[command.target_scope for command in passenger.commands],
["passenger", "passenger"],
["passenger"],
)
self.assertEqual(
passenger.commands[0].parameters,
{"state": "on", "level": 3},
)
self.assertTrue(passenger.execution_eligible)
@@ -489,7 +492,11 @@ class SpeechCapabilityAlignmentTests(unittest.TestCase):
self.assertNotIn("设置为", result.clean_text)
explicit_level = self.processor.process("主驾座椅通风开到一档")
self.assertEqual(explicit_level.commands[0].parameters["level"], 1)
self.assertEqual(explicit_level.commands[0].parameters["level"], 2)
self.assertEqual(
explicit_level.commands[0].qualifiers["display_level"],
"1档",
)
def test_discourse_connectors_preserve_actions_without_leaking_scope(self) -> None:
cases = (
+1 -1
View File
@@ -31,7 +31,7 @@ def test_vue_vite_application_is_the_primary_frontend_entry() -> None:
assert package["scripts"]["build"] == "vite build"
assert 'import vue from "@vitejs/plugin-vue"' in vite
assert 'outDir: outputRoot' in vite
assert 'const CACHE_VERSION = "cognitive-agent-pad-v329-mode-preview"' in service_worker
assert 'const CACHE_VERSION = "cognitive-agent-pad-v334-video-media"' in service_worker
assert 'createHash("sha256")' in vite
assert 'runtime-assets.json' in vite
assert 'rewriteRuntimeReferences' in vite