feat: improve mode and peripheral orchestration
This commit is contained in:
+19
-1
@@ -17,6 +17,8 @@ function createConversationId() {
|
|||||||
return createOpaqueId("conv");
|
return createOpaqueId("conv");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const HARDWARE_CLIENT_ID_PATTERN = /^hwc-[a-z0-9-]{8,80}$/;
|
||||||
|
|
||||||
function loadConversationId() {
|
function loadConversationId() {
|
||||||
const stored = sessionStorage.getItem("cognitive-agent.conversation-id");
|
const stored = sessionStorage.getItem("cognitive-agent.conversation-id");
|
||||||
// A browser runtime restart is a new virtual run. Do not reuse the old
|
// A browser runtime restart is a new virtual run. Do not reuse the old
|
||||||
@@ -33,12 +35,19 @@ function loadConversationId() {
|
|||||||
function loadHardwareClientId() {
|
function loadHardwareClientId() {
|
||||||
const key = "cognitive-agent.hardware-client-id";
|
const key = "cognitive-agent.hardware-client-id";
|
||||||
const stored = localStorage.getItem(key);
|
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");
|
const clientId = createOpaqueId("hwc");
|
||||||
localStorage.setItem(key, clientId);
|
localStorage.setItem(key, clientId);
|
||||||
return clientId;
|
return clientId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validatedHardwareClientId() {
|
||||||
|
if (!HARDWARE_CLIENT_ID_PATTERN.test(state.hardwareClientId || "")) {
|
||||||
|
state.hardwareClientId = loadHardwareClientId();
|
||||||
|
}
|
||||||
|
return state.hardwareClientId;
|
||||||
|
}
|
||||||
|
|
||||||
function loadControlTransport() {
|
function loadControlTransport() {
|
||||||
const stored = localStorage.getItem("cognitive-agent.control-transport");
|
const stored = localStorage.getItem("cognitive-agent.control-transport");
|
||||||
return ["wired_can", "wireless_mqtt"].includes(stored) ? stored : "wired_can";
|
return ["wired_can", "wireless_mqtt"].includes(stored) ? stored : "wired_can";
|
||||||
@@ -5131,6 +5140,7 @@ const MODE_DOMAIN_LABELS = {
|
|||||||
window: "车窗",
|
window: "车窗",
|
||||||
front_light: "车灯",
|
front_light: "车灯",
|
||||||
vehicle_status: "车辆状态",
|
vehicle_status: "车辆状态",
|
||||||
|
android: "车机媒体",
|
||||||
};
|
};
|
||||||
|
|
||||||
const MODE_TARGET_LABELS = {
|
const MODE_TARGET_LABELS = {
|
||||||
@@ -5140,6 +5150,7 @@ const MODE_TARGET_LABELS = {
|
|||||||
front_right: "右前 / 副驾",
|
front_right: "右前 / 副驾",
|
||||||
rear_left: "左后",
|
rear_left: "左后",
|
||||||
rear_right: "右后",
|
rear_right: "右后",
|
||||||
|
headunit: "车机",
|
||||||
front_row: "前排",
|
front_row: "前排",
|
||||||
rear_row: "后排",
|
rear_row: "后排",
|
||||||
vehicle: "全车",
|
vehicle: "全车",
|
||||||
@@ -8241,6 +8252,7 @@ const CONFIG_DOMAIN_META = {
|
|||||||
window: { label: "车窗", icon: "panels-top-left", className: "generic" },
|
window: { label: "车窗", icon: "panels-top-left", className: "generic" },
|
||||||
outlet: { label: "电动风口", icon: "wind", className: "climate" },
|
outlet: { label: "电动风口", icon: "wind", className: "climate" },
|
||||||
interior_light: { label: "车内灯", icon: "lightbulb", className: "ambient" },
|
interior_light: { label: "车内灯", icon: "lightbulb", className: "ambient" },
|
||||||
|
android: { label: "车机媒体", icon: "clapperboard", className: "generic" },
|
||||||
};
|
};
|
||||||
|
|
||||||
const CONFIG_CAPABILITY_LABELS = {
|
const CONFIG_CAPABILITY_LABELS = {
|
||||||
@@ -8286,6 +8298,7 @@ const CONFIG_CAPABILITY_LABELS = {
|
|||||||
"roof.set_shade_position": "设置遮阳帘位置",
|
"roof.set_shade_position": "设置遮阳帘位置",
|
||||||
"roof.set_shade_motion": "控制遮阳帘动作",
|
"roof.set_shade_motion": "控制遮阳帘动作",
|
||||||
"roof.set_automatic_dimming": "设置自动调光",
|
"roof.set_automatic_dimming": "设置自动调光",
|
||||||
|
"android.launch_app": "打开视频媒体",
|
||||||
};
|
};
|
||||||
|
|
||||||
const CONFIG_PARAMETER_LABELS = {
|
const CONFIG_PARAMETER_LABELS = {
|
||||||
@@ -8309,6 +8322,8 @@ const CONFIG_PARAMETER_LABELS = {
|
|||||||
outlet_mode: "出风模式",
|
outlet_mode: "出风模式",
|
||||||
mode: "工作模式",
|
mode: "工作模式",
|
||||||
mode_id: "固件模式编号",
|
mode_id: "固件模式编号",
|
||||||
|
package_name: "应用包名",
|
||||||
|
activity_name: "启动页面",
|
||||||
};
|
};
|
||||||
|
|
||||||
function makeConfigElement(tagName, className, textValue) {
|
function makeConfigElement(tagName, className, textValue) {
|
||||||
@@ -9412,6 +9427,9 @@ window.CognitiveAgentConsole = {
|
|||||||
getExecutionMode() {
|
getExecutionMode() {
|
||||||
return state.executionMode;
|
return state.executionMode;
|
||||||
},
|
},
|
||||||
|
getHardwareClientId() {
|
||||||
|
return validatedHardwareClientId();
|
||||||
|
},
|
||||||
getConversationId() {
|
getConversationId() {
|
||||||
return state.conversationId;
|
return state.conversationId;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -548,6 +548,7 @@
|
|||||||
"door.set_rear_lock": "后排门锁",
|
"door.set_rear_lock": "后排门锁",
|
||||||
"door.set_vehicle_lock": "整车门锁",
|
"door.set_vehicle_lock": "整车门锁",
|
||||||
"infrared.emit": "红外外设",
|
"infrared.emit": "红外外设",
|
||||||
|
"android.launch_app": "打开视频媒体",
|
||||||
};
|
};
|
||||||
|
|
||||||
function assistantControlCategory(operation = {}, fallback = "控制指令") {
|
function assistantControlCategory(operation = {}, fallback = "控制指令") {
|
||||||
@@ -745,6 +746,22 @@
|
|||||||
const brightness = numericValue(parameters.brightness_level);
|
const brightness = numericValue(parameters.brightness_level);
|
||||||
if (brightness !== null) return `${brightness}级`;
|
if (brightness !== null) return `${brightness}级`;
|
||||||
const level = numericValue(parameters.level ?? parameters.fan_level);
|
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}档`;
|
if (level !== null) return `${level}档`;
|
||||||
const state = parameters.motion
|
const state = parameters.motion
|
||||||
?? parameters.state
|
?? parameters.state
|
||||||
@@ -769,6 +786,7 @@
|
|||||||
if (capability.startsWith("seat.") || category.includes("座椅")) return "armchair";
|
if (capability.startsWith("seat.") || category.includes("座椅")) return "armchair";
|
||||||
if (capability.startsWith("door.") || category.includes("门锁")) return "lock-keyhole";
|
if (capability.startsWith("door.") || category.includes("门锁")) return "lock-keyhole";
|
||||||
if (capability === "infrared.emit") return "radio-tower";
|
if (capability === "infrared.emit") return "radio-tower";
|
||||||
|
if (capability === "android.launch_app") return "clapperboard";
|
||||||
return "binary";
|
return "binary";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -985,6 +1003,7 @@
|
|||||||
"ambient_light.set_power": "氛围灯",
|
"ambient_light.set_power": "氛围灯",
|
||||||
"ambient_light.set_mode": "氛围灯模式",
|
"ambient_light.set_mode": "氛围灯模式",
|
||||||
"infrared.emit": "红外外设",
|
"infrared.emit": "红外外设",
|
||||||
|
"android.launch_app": "打开视频媒体",
|
||||||
};
|
};
|
||||||
|
|
||||||
const zeroGravityIndexes = actions
|
const zeroGravityIndexes = actions
|
||||||
@@ -1166,9 +1185,10 @@
|
|||||||
? `${params.position_pct}%${params.angle_deg != null ? `(${params.angle_deg}°)` : ""}`
|
? `${params.position_pct}%${params.angle_deg != null ? `(${params.angle_deg}°)` : ""}`
|
||||||
: motionLabel(params.state || params.mode_id);
|
: motionLabel(params.state || params.mode_id);
|
||||||
if (["seat.set_driver_ventilation_native", "seat.set_driver_heating_native"].includes(capability)) {
|
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
|
// CH1 encodes high as 1 and low as 2 internally. Present the
|
||||||
// comfort levels use a different scale and must not reuse this map.
|
// occupant-facing three-speed scale instead of leaking CAN enums.
|
||||||
const level = { 1: "高档", 2: "低档" }[params.level ?? 2] || "档位待确认";
|
const level = { 1: "3档(高)", 2: "1档(低)" }[params.level ?? 2]
|
||||||
|
|| "档位待确认";
|
||||||
value = params.state === "off" ? "关闭"
|
value = params.state === "off" ? "关闭"
|
||||||
: params.state === "on" ? `开启 · ${level}` : "开关状态待确认";
|
: params.state === "on" ? `开启 · ${level}` : "开关状态待确认";
|
||||||
} else if (["seat.set_ventilation", "seat.set_heating"].includes(capability)) {
|
} else if (["seat.set_ventilation", "seat.set_heating"].includes(capability)) {
|
||||||
@@ -1529,10 +1549,23 @@
|
|||||||
return /(?:构建|创建|新建|新增|生成|添加)(?:一下)?(?:一个|个|一套|套)?[^,。!?!?]{1,64}?模式/.test(compact);
|
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) {
|
function requestBody(message) {
|
||||||
const configuredExecutionMode = window.CognitiveAgentConsole?.getExecutionMode?.() || "dry_run";
|
const configuredExecutionMode = window.CognitiveAgentConsole?.getExecutionMode?.() || "dry_run";
|
||||||
const controlTransport = localStorage.getItem("cognitive-agent.control-transport") || "wired_can";
|
const controlTransport = localStorage.getItem("cognitive-agent.control-transport") || "wired_can";
|
||||||
const hardwareClientId = localStorage.getItem("cognitive-agent.hardware-client-id");
|
const hardwareClientId = validatedHardwareClientId();
|
||||||
const vehicleId = $("#vehicleSelect")?.value || null;
|
const vehicleId = $("#vehicleSelect")?.value || null;
|
||||||
const modeProfileDraftContext = requestModeDraftContext(message);
|
const modeProfileDraftContext = requestModeDraftContext(message);
|
||||||
const modeConfigurationPreview = Boolean(modeProfileDraftContext)
|
const modeConfigurationPreview = Boolean(modeProfileDraftContext)
|
||||||
@@ -1967,6 +2000,11 @@
|
|||||||
&& result?.schedule_execution?.status === "running";
|
&& result?.schedule_execution?.status === "running";
|
||||||
const dryRun = result?.execution_mode === "dry_run";
|
const dryRun = result?.execution_mode === "dry_run";
|
||||||
const waitingForFeedback = result?.execution_outcome === "submitted_unverified";
|
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 content = `${modeName}执行未完成,请查看控制指令状态。`;
|
||||||
let statusText = "功能模式执行未完成";
|
let statusText = "功能模式执行未完成";
|
||||||
@@ -1991,8 +2029,15 @@
|
|||||||
: "功能模式执行已完成";
|
: "功能模式执行已完成";
|
||||||
statusState = "success";
|
statusState = "success";
|
||||||
} else if (status === "partially_succeeded") {
|
} else if (status === "partially_succeeded") {
|
||||||
content = `${modeName}仅部分完成,请查看未完成的控制指令。`;
|
content = delegatedCount > 0
|
||||||
statusText = "功能模式部分完成";
|
? `${modeName}指令已提交,未由 PC 执行的 ${delegatedCount} 项动作将由车机继续处理。`
|
||||||
|
: waitingForFeedback
|
||||||
|
? `${modeName}指令已提交,正在等待车辆状态确认。`
|
||||||
|
: `${modeName}执行结果已更新,请查看各控制指令状态。`;
|
||||||
|
statusText = waitingForFeedback || delegatedCount > 0
|
||||||
|
? "功能模式指令已提交"
|
||||||
|
: "功能模式执行结果已更新";
|
||||||
|
statusState = waitingForFeedback || delegatedCount > 0 ? "working" : "success";
|
||||||
}
|
}
|
||||||
|
|
||||||
setMessageContent(message, content, { final: true });
|
setMessageContent(message, content, { final: true });
|
||||||
|
|||||||
@@ -571,9 +571,20 @@
|
|||||||
const explicitlyBlocked = executionStatus === "blocked"
|
const explicitlyBlocked = executionStatus === "blocked"
|
||||||
|| verificationStatus === "blocked"
|
|| verificationStatus === "blocked"
|
||||||
|| Boolean(telemetry.authorization_reason);
|
|| 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
|
|| explicitlyBlocked
|
||||||
|| verificationStatus === "failed_or_blocked";
|
|| verificationStatus === "failed_or_blocked"
|
||||||
|
);
|
||||||
const missingWindowFeedback = /0x388/i.test(failureMessage) && (
|
const missingWindowFeedback = /0x388/i.test(failureMessage) && (
|
||||||
/no fresh valid/i.test(failureMessage)
|
/no fresh valid/i.test(failureMessage)
|
||||||
|| /未收到.*(?:有效|新鲜).*车窗位置/.test(failureMessage)
|
|| /未收到.*(?:有效|新鲜).*车窗位置/.test(failureMessage)
|
||||||
@@ -591,11 +602,6 @@
|
|||||||
|| /thermal protection|热保护/i.test(failureMessage)
|
|| /thermal protection|热保护/i.test(failureMessage)
|
||||||
);
|
);
|
||||||
const dispatchFailed = executionFailed && !submitted && !safetyBlocked;
|
const dispatchFailed = executionFailed && !submitted && !safetyBlocked;
|
||||||
const verified = !executionFailed && !aborted && (
|
|
||||||
verificationStatus === "verified_success"
|
|
||||||
|| telemetry.physical_effect_confirmed === true
|
|
||||||
|| telemetry.feedback_match === true
|
|
||||||
);
|
|
||||||
const deliveryState = aborted
|
const deliveryState = aborted
|
||||||
? "aborted"
|
? "aborted"
|
||||||
: safetyBlocked
|
: safetyBlocked
|
||||||
@@ -1325,7 +1331,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const levelOptions = [
|
const levelOptions = [
|
||||||
["off", "关闭"], ["high", "1 档(高)"], ["low", "2 档(低)"],
|
["off", "关闭"], ["low", "1 档(低)"], ["high", "3 档(高)"],
|
||||||
];
|
];
|
||||||
const toggleOptions = [["off", "关闭"], ["on", "开启"]];
|
const toggleOptions = [["off", "关闭"], ["on", "开启"]];
|
||||||
const massageModeOptions = [
|
const massageModeOptions = [
|
||||||
|
|||||||
@@ -39,13 +39,13 @@
|
|||||||
<script src="/vendor/three/0.147.0/fflate.min.js" defer></script>
|
<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="/vendor/three/0.147.0/FBXLoader.js" defer></script>
|
||||||
<script src="/network.js?v=android-pad-2" 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 src="/speech-input.js?v=sensevoice-13" defer></script>
|
||||||
<script type="module" src="/gesture-control.js?v=android-pad-9"></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="/pwa.js?v=android-pad-3" defer></script>
|
||||||
<script src="/console-v2.js?v=local-first-77-performance" defer></script>
|
<script src="/console-v2.js?v=local-first-79-cinema-status" defer></script>
|
||||||
<script src="/self-extension.js?v=nfc-mqtt-56" defer></script>
|
<script src="/self-extension.js?v=nfc-persistent-57" defer></script>
|
||||||
<script src="/assistant-chat.js?v=codex-assistant-35" 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="/energy-monitor.js?v=energy-monitor-5-performance" defer></script>
|
||||||
<script src="/firmware-workbench-entry.js?v=3" defer></script>
|
<script src="/firmware-workbench-entry.js?v=3" defer></script>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -382,6 +382,8 @@
|
|||||||
};
|
};
|
||||||
let peripheralCatalogRevision = null;
|
let peripheralCatalogRevision = null;
|
||||||
let peripheralCatalogSyncQueue = Promise.resolve();
|
let peripheralCatalogSyncQueue = Promise.resolve();
|
||||||
|
let durablePeripheralCatalog = [];
|
||||||
|
let durablePeripheralCatalogHydrated = false;
|
||||||
let petCardSnapshot = {
|
let petCardSnapshot = {
|
||||||
revision: -1,
|
revision: -1,
|
||||||
cards: [],
|
cards: [],
|
||||||
@@ -1973,11 +1975,40 @@
|
|||||||
}).filter((item) => item.device_id && item.card_uid && validSlot(item.slot));
|
}).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() {
|
async function hydratePeripheralCatalog() {
|
||||||
const payload = await api("peripherals");
|
const payload = await api("peripherals");
|
||||||
peripheralCatalogRevision = payload.catalog_revision || peripheralCatalogRevision;
|
|
||||||
const catalog = Array.isArray(payload.catalog) ? payload.catalog : [];
|
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();
|
const restoredDeviceIds = new Set();
|
||||||
catalog.forEach((item) => {
|
catalog.forEach((item) => {
|
||||||
if (!item || typeof item !== "object" || !item.id || !validSlot(item.slot)) return;
|
if (!item || typeof item !== "object" || !item.id || !validSlot(item.slot)) return;
|
||||||
@@ -3806,12 +3837,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function syncPeripheralCatalog(device = null, { throwOnError = false } = {}) {
|
async function syncPeripheralCatalog(device = null, { throwOnError = false } = {}) {
|
||||||
|
const nextCatalog = peripheralCatalog();
|
||||||
const operation = peripheralCatalogSyncQueue
|
const operation = peripheralCatalogSyncQueue
|
||||||
.catch(() => null)
|
.catch(() => null)
|
||||||
.then(() => api("display-sync", {
|
.then(() => api("display-sync", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
peripherals: peripheralCatalog(),
|
peripherals: nextCatalog,
|
||||||
device,
|
device,
|
||||||
catalog_revision: peripheralCatalogRevision,
|
catalog_revision: peripheralCatalogRevision,
|
||||||
}),
|
}),
|
||||||
@@ -3819,7 +3851,10 @@
|
|||||||
peripheralCatalogSyncQueue = operation.catch(() => null);
|
peripheralCatalogSyncQueue = operation.catch(() => null);
|
||||||
try {
|
try {
|
||||||
const result = await operation;
|
const result = await operation;
|
||||||
peripheralCatalogRevision = result.catalog_revision || peripheralCatalogRevision;
|
applyDurablePeripheralCatalog(
|
||||||
|
nextCatalog,
|
||||||
|
result.catalog_revision,
|
||||||
|
);
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("外设能力同步失败", error);
|
console.warn("外设能力同步失败", error);
|
||||||
@@ -4169,6 +4204,18 @@
|
|||||||
status: item.status,
|
status: item.status,
|
||||||
enabled: item.enabled,
|
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 ? {
|
lastAction: snapshot?.extension?.last_action ? {
|
||||||
command: snapshot.extension.last_action.command,
|
command: snapshot.extension.last_action.command,
|
||||||
type: snapshot.extension.last_action.type,
|
type: snapshot.extension.last_action.type,
|
||||||
@@ -4249,27 +4296,28 @@
|
|||||||
const sharedCatalog = Array.isArray(snapshot?.extension?.catalog)
|
const sharedCatalog = Array.isArray(snapshot?.extension?.catalog)
|
||||||
? snapshot.extension.catalog
|
? snapshot.extension.catalog
|
||||||
: [];
|
: [];
|
||||||
const mergedCatalog = [...sharedCatalog, ...peripheralCatalog()]
|
const fallbackCatalog = [...sharedCatalog, ...peripheralCatalog()]
|
||||||
.filter((item, index, catalog) => {
|
.filter((item, index, catalog) => {
|
||||||
const identity = item.id || `${item.device_id}:${item.action}`;
|
const identity = item.id || `${item.device_id}:${item.action}`;
|
||||||
return catalog.findIndex((candidate) => (
|
return catalog.findIndex((candidate) => (
|
||||||
candidate.id || `${candidate.device_id}:${candidate.action}`
|
candidate.id || `${candidate.device_id}:${candidate.action}`
|
||||||
) === identity) === index;
|
) === identity) === index;
|
||||||
});
|
});
|
||||||
|
const persistentCatalog = (
|
||||||
|
durablePeripheralCatalogHydrated
|
||||||
|
? durablePeripheralCatalog
|
||||||
|
: fallbackCatalog
|
||||||
|
).filter((item) => (
|
||||||
|
normalizeCardUid(item.card_uid)
|
||||||
|
&& item.status === "learned"
|
||||||
|
));
|
||||||
const nfcReader = nfcSnapshot.reader || {};
|
const nfcReader = nfcSnapshot.reader || {};
|
||||||
const nfcCard = nfcSnapshot.card;
|
const nfcCard = nfcSnapshot.card;
|
||||||
const sensedCardUid = normalizeCardUid(nfcCard?.uid);
|
const sensedCardUid = normalizeCardUid(nfcCard?.uid);
|
||||||
const nfcDevice = linkedDeviceForCard(sensedCardUid);
|
const nfcDevice = linkedDeviceForCard(sensedCardUid);
|
||||||
const nfcPet = linkedPetForCard(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();
|
const grouped = new Map();
|
||||||
activeCatalog.forEach((item) => {
|
persistentCatalog.forEach((item) => {
|
||||||
const deviceId = item.device_id || "external-device";
|
const deviceId = item.device_id || "external-device";
|
||||||
if (!grouped.has(deviceId)) grouped.set(deviceId, {
|
if (!grouped.has(deviceId)) grouped.set(deviceId, {
|
||||||
id: deviceId,
|
id: deviceId,
|
||||||
@@ -4278,7 +4326,10 @@
|
|||||||
cardUid: item.card_uid || null,
|
cardUid: item.card_uid || null,
|
||||||
actions: [],
|
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 devices = [...grouped.values()];
|
||||||
const lastAction = snapshot?.extension?.last_action;
|
const lastAction = snapshot?.extension?.last_action;
|
||||||
@@ -4294,8 +4345,10 @@
|
|||||||
: petCardSnapshot.publisher?.state === "burst_failed"
|
: petCardSnapshot.publisher?.state === "burst_failed"
|
||||||
? "宠物消息发送失败"
|
? "宠物消息发送失败"
|
||||||
: "已感应宠物 NFC"
|
: "已感应宠物 NFC"
|
||||||
: devices.length
|
: nfcDevice
|
||||||
? "已感应 NFC 外设"
|
? "已感应 NFC 外设"
|
||||||
|
: devices.length
|
||||||
|
? `${devices.length} 个外设已绑定`
|
||||||
: sensedCardUid
|
: sensedCardUid
|
||||||
? "当前 NFC 暂无已授权命令"
|
? "当前 NFC 暂无已授权命令"
|
||||||
: nfcReader.online
|
: nfcReader.online
|
||||||
@@ -4361,11 +4414,11 @@
|
|||||||
<div>${device.actions.slice(0, 8).map((action) => `<span>${escapeHtml(action)}</span>`).join("")}</div>
|
<div>${device.actions.slice(0, 8).map((action) => `<span>${escapeHtml(action)}</span>`).join("")}</div>
|
||||||
</article>`).join("") : nfcPet ? "" : `
|
</article>`).join("") : nfcPet ? "" : `
|
||||||
<div class="v2-extension-empty">
|
<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>`}
|
||||||
</div>
|
</div>
|
||||||
<footer class="v2-extension-linkage" data-active="${String(Boolean(visibleLastAction))}">
|
<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>
|
<span>${escapeHtml(visibleLastAction ? (lastResult?.status || lastResult?.message || "STANDBY") : "STANDBY")}</span>
|
||||||
</footer>`;
|
</footer>`;
|
||||||
unifiedExtensionFingerprint = renderFingerprint;
|
unifiedExtensionFingerprint = renderFingerprint;
|
||||||
@@ -4377,6 +4430,17 @@
|
|||||||
|
|
||||||
window.addEventListener("cognitive-agent:display-sync", (event) => {
|
window.addEventListener("cognitive-agent:display-sync", (event) => {
|
||||||
snapshot = event.detail || null;
|
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) {
|
if (snapshot?.extension_health?.nfc) {
|
||||||
applyNfcSnapshot(snapshot.extension_health.nfc, { announce: false });
|
applyNfcSnapshot(snapshot.extension_health.nfc, { announce: false });
|
||||||
}
|
}
|
||||||
@@ -4402,8 +4466,8 @@
|
|||||||
void hydratePeripheralCatalog()
|
void hydratePeripheralCatalog()
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.warn("恢复后端红外能力目录失败,将保留浏览器本地目录。", error);
|
console.warn("恢复后端红外能力目录失败,将保留浏览器本地目录。", error);
|
||||||
})
|
return syncPeripheralCatalog();
|
||||||
.finally(() => syncPeripheralCatalog());
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 SHELL_CACHE = `${CACHE_VERSION}-shell`;
|
||||||
const RUNTIME_CACHE = `${CACHE_VERSION}-runtime`;
|
const RUNTIME_CACHE = `${CACHE_VERSION}-runtime`;
|
||||||
const APP_SHELL = [
|
const APP_SHELL = [
|
||||||
@@ -6,18 +6,18 @@ const APP_SHELL = [
|
|||||||
"/index.html",
|
"/index.html",
|
||||||
"/styles.css?v=dual-seat-24-dialog-size",
|
"/styles.css?v=dual-seat-24-dialog-size",
|
||||||
"/network.js?v=android-pad-2",
|
"/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",
|
"/gesture-control.js?v=android-pad-9",
|
||||||
"/pwa.js?v=android-pad-3",
|
"/pwa.js?v=android-pad-3",
|
||||||
"/console-v2.css?v=delivery-state-69",
|
"/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.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.css?v=sensevoice-7",
|
||||||
"/speech-input.js?v=sensevoice-13",
|
"/speech-input.js?v=sensevoice-13",
|
||||||
"/speech-audio-worklet.js?v=sensevoice-5",
|
"/speech-audio-worklet.js?v=sensevoice-5",
|
||||||
"/assistant-chat.css?v=codex-assistant-30",
|
"/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.css?v=energy-monitor-5-dialog-size",
|
||||||
"/energy-monitor.js?v=energy-monitor-5-performance",
|
"/energy-monitor.js?v=energy-monitor-5-performance",
|
||||||
"/ui-consistency.css?v=spatial-ui-8",
|
"/ui-consistency.css?v=spatial-ui-8",
|
||||||
|
|||||||
@@ -2465,8 +2465,19 @@ class AssistantCapabilityCatalog:
|
|||||||
if isinstance(command_flow, Mapping)
|
if isinstance(command_flow, Mapping)
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
local_n7_transfer_actions = (
|
||||||
|
command_flow.get("n7_transfer_actions")
|
||||||
|
if isinstance(command_flow, Mapping)
|
||||||
|
else None
|
||||||
|
)
|
||||||
n7_transfer_ready = (
|
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 isinstance(command_plans, Mapping)
|
||||||
and bool(command_plans)
|
and bool(command_plans)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -726,6 +726,17 @@ def _level_matches(text: str) -> set[int]:
|
|||||||
return values
|
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(
|
def _axis_number_matches(
|
||||||
text: str,
|
text: str,
|
||||||
axis: str,
|
axis: str,
|
||||||
@@ -861,7 +872,16 @@ def _semantic_parameter_error(
|
|||||||
return "temperature_c 没有原始文本数值依据"
|
return "temperature_c 没有原始文本数值依据"
|
||||||
|
|
||||||
for name in {"level", "brightness_level"} & values.keys():
|
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]
|
value = values[name]
|
||||||
if levels:
|
if levels:
|
||||||
if not any(_numeric_equal(value, expected) for expected in 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")
|
raise TypeError("vehicle control returned an invalid result")
|
||||||
safe_result = self._safe_vehicle_result(result)
|
safe_result = self._safe_vehicle_result(result)
|
||||||
action_state, successful, completion_text = _vehicle_action_state(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)
|
successful = _mode_profile_result_succeeded(result)
|
||||||
action_state = "simulated" if successful else "failed"
|
action_state = "simulated" if successful else "failed"
|
||||||
completion_text = (
|
completion_text = (
|
||||||
@@ -4232,6 +4247,7 @@ class CodexAssistantService:
|
|||||||
"local_control_elapsed_ms",
|
"local_control_elapsed_ms",
|
||||||
"local_control_within_budget",
|
"local_control_within_budget",
|
||||||
"natural_language_routing",
|
"natural_language_routing",
|
||||||
|
"parallel_dispatch",
|
||||||
):
|
):
|
||||||
if result.get(key) is not None:
|
if result.get(key) is not None:
|
||||||
payload[key] = CodexAssistantService._safe_result_value(
|
payload[key] = CodexAssistantService._safe_result_value(
|
||||||
@@ -4444,6 +4460,11 @@ class CodexAssistantService:
|
|||||||
}
|
}
|
||||||
if result.get("orchestration_route") is not None:
|
if result.get("orchestration_route") is not None:
|
||||||
safe["orchestration_route"] = str(result["orchestration_route"])
|
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")
|
validation = result.get("validation")
|
||||||
if isinstance(validation, Sequence) and not isinstance(
|
if isinstance(validation, Sequence) and not isinstance(
|
||||||
validation,
|
validation,
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ from typing import Any
|
|||||||
from cognitive_agent.application.extension_toy_publisher import (
|
from cognitive_agent.application.extension_toy_publisher import (
|
||||||
normalize_extension_nfc_id,
|
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"
|
EXTENSION_CONTROL_TOPIC = "extension/ctl"
|
||||||
@@ -194,7 +199,7 @@ def authorize_extension_control_actions(
|
|||||||
request: Mapping[str, Any],
|
request: Mapping[str, Any],
|
||||||
catalog: object,
|
catalog: object,
|
||||||
) -> list[dict[str, Any]]:
|
) -> 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")
|
actions = request.get("actions")
|
||||||
if not isinstance(actions, list):
|
if not isinstance(actions, list):
|
||||||
@@ -251,9 +256,86 @@ def authorize_extension_control_actions(
|
|||||||
"action": deepcopy(dict(action)),
|
"action": deepcopy(dict(action)),
|
||||||
"capability_id": str(capability.get("id") or ""),
|
"capability_id": str(capability.get("id") or ""),
|
||||||
"extension_name": str(capability.get("device_name") 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:
|
class ExtensionControlConsumer:
|
||||||
@@ -477,7 +559,14 @@ class ExtensionControlConsumer:
|
|||||||
self._record_rejection(exc, request=request)
|
self._record_rejection(exc, request=request)
|
||||||
return
|
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 = [
|
action_statuses = [
|
||||||
{
|
{
|
||||||
"index": authorized["index"],
|
"index": authorized["index"],
|
||||||
@@ -488,6 +577,8 @@ class ExtensionControlConsumer:
|
|||||||
"target_scope": authorized["action"]["target_scope"],
|
"target_scope": authorized["action"]["target_scope"],
|
||||||
"action": authorized["action"]["parameters"]["action"],
|
"action": authorized["action"]["parameters"]["action"],
|
||||||
"slot": authorized["action"]["parameters"]["slot"],
|
"slot": authorized["action"]["parameters"]["slot"],
|
||||||
|
"auto_inserted": authorized.get("auto_inserted") is True,
|
||||||
|
"dependency": authorized.get("dependency"),
|
||||||
"state": "pending",
|
"state": "pending",
|
||||||
"state_label": "等待下发",
|
"state_label": "等待下发",
|
||||||
"state_detail": f"计划在接收后 {authorized['at_ms']} ms 下发",
|
"state_detail": f"计划在接收后 {authorized['at_ms']} ms 下发",
|
||||||
@@ -583,6 +674,8 @@ class ExtensionControlConsumer:
|
|||||||
"target_scope": authorized["action"]["target_scope"],
|
"target_scope": authorized["action"]["target_scope"],
|
||||||
"action": authorized["action"]["parameters"]["action"],
|
"action": authorized["action"]["parameters"]["action"],
|
||||||
"slot": authorized["action"]["parameters"]["slot"],
|
"slot": authorized["action"]["parameters"]["slot"],
|
||||||
|
"auto_inserted": authorized.get("auto_inserted") is True,
|
||||||
|
"dependency": authorized.get("dependency"),
|
||||||
"transport": str(result.get("transport") or "tcp"),
|
"transport": str(result.get("transport") or "tcp"),
|
||||||
"gateway_acknowledged": gateway_acknowledged,
|
"gateway_acknowledged": gateway_acknowledged,
|
||||||
"physical_state_confirmed": physical_state_confirmed,
|
"physical_state_confirmed": physical_state_confirmed,
|
||||||
|
|||||||
@@ -267,6 +267,33 @@ def _scene_from_profile(
|
|||||||
for operation, target, operation_ref in effective_operations
|
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 = {
|
scene = {
|
||||||
"id": profile.id,
|
"id": profile.id,
|
||||||
"label": profile.name,
|
"label": profile.name,
|
||||||
|
|||||||
@@ -106,9 +106,13 @@ OUTLET_MODE_DISABLE_PATTERN = re.compile(
|
|||||||
)
|
)
|
||||||
DRIVER_SEAT_THERMAL_MIDDLE_PATTERN = re.compile(
|
DRIVER_SEAT_THERMAL_MIDDLE_PATTERN = re.compile(
|
||||||
r"(?:通风|加热)[^,,;;。!?!?\n]{0,16}"
|
r"(?:通风|加热)[^,,;;。!?!?\n]{0,16}"
|
||||||
r"(?:中等(?:档|级)?|中档|中级|中间档|适中|(?:调到|调为|调|设为|设置为|调整到|调整为)中)|"
|
r"(?:中等(?:档|级)?|中档|中级|中间档|适中|(?:2|二)(?:档|级)|"
|
||||||
|
r"(?:调到|调为|调|设为|设置为|调整到|调整为)中)|"
|
||||||
r"(?:中等(?:档|级)?|中档|中级|中间档|适中)"
|
r"(?:中等(?:档|级)?|中档|中级|中间档|适中)"
|
||||||
r"[^,,;;。!?!?\n]{0,16}(?:通风|加热)"
|
r"[^,,;;。!?!?\n]{0,16}(?:通风|加热)|"
|
||||||
|
r"(?:通风|加热)[^,,;;。!?!?\n]{0,16}[,,;;]"
|
||||||
|
r"(?:挡位|档位)?(?:调到|调为|设为|设置为|调整到|调整为)?"
|
||||||
|
r"(?:2|二)(?:档|级)"
|
||||||
)
|
)
|
||||||
DRIVER_SEAT_MASSAGE_CONTINUATION_PATTERN = re.compile(
|
DRIVER_SEAT_MASSAGE_CONTINUATION_PATTERN = re.compile(
|
||||||
r"^(?:(?:然后|随后|接着|同时|并且|再|并|以及)?(?:把|将)?(?:按摩)?)"
|
r"^(?:(?:然后|随后|接着|同时|并且|再|并|以及)?(?:把|将)?(?:按摩)?)"
|
||||||
@@ -116,6 +120,12 @@ DRIVER_SEAT_MASSAGE_CONTINUATION_PATTERN = re.compile(
|
|||||||
r"(?:切换|改成|改为|调整|调成|调为|设置|设为)"
|
r"(?:切换|改成|改为|调整|调成|调为|设置|设为)"
|
||||||
r".{0,8}(?:波浪|脉冲|伸展|拉伸|松弛|舒展|放松)(?:模式)?)"
|
r".{0,8}(?:波浪|脉冲|伸展|拉伸|松弛|舒展|放松)(?:模式)?)"
|
||||||
)
|
)
|
||||||
|
DRIVER_SEAT_THERMAL_CONTINUATION_PATTERN = re.compile(
|
||||||
|
r"^(?:(?:然后|随后|接着|同时|并且|再|并|以及)?(?:把|将)?)"
|
||||||
|
r"(?:通风|加热)?(?:挡位|档位)?"
|
||||||
|
r"(?:切换|改成|改为|调整|调到|调成|调为|设置|设为)?"
|
||||||
|
r"(?:[1-3一二三](?:档|级)|最低|低档|最高|高档|中档|中等|适中)$"
|
||||||
|
)
|
||||||
_FEATURE_BOUNDARY = re.compile(
|
_FEATURE_BOUNDARY = re.compile(
|
||||||
r"并且|同时|然后|随后|接着|以及|并|再|和|与|、|,"
|
r"并且|同时|然后|随后|接着|以及|并|再|和|与|、|,"
|
||||||
)
|
)
|
||||||
@@ -775,6 +785,23 @@ def _parse_chinese_driver_seat_commands(compact: str) -> list[dict[str, Any]]:
|
|||||||
clause = f"{clause}{continuation}"
|
clause = f"{clause}{continuation}"
|
||||||
continuation_index += 1
|
continuation_index += 1
|
||||||
index = 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)
|
scope = _seat_target_scope(clause)
|
||||||
if scope == "driver":
|
if scope == "driver":
|
||||||
inherited_driver_scope = True
|
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):
|
if re.search(r"关闭|关掉|停止|取消|不要|别开", thermal_text):
|
||||||
parameters = {"state": "off"}
|
parameters = {"state": "off"}
|
||||||
elif thermal_grade == "high" or re.search(
|
elif thermal_grade == "high" or re.search(
|
||||||
r"高速|(?:1|一)(?:档|级)", thermal_text
|
r"高速|(?:3|三)(?:档|级)", thermal_text
|
||||||
):
|
):
|
||||||
parameters = {"state": "on", "level": 1}
|
parameters = {"state": "on", "level": 1}
|
||||||
|
elif re.search(r"(?:2|二)(?:档|级)|中档|中等|适中", thermal_text):
|
||||||
|
parameters = None
|
||||||
elif thermal_grade == "low" or re.search(
|
elif thermal_grade == "low" or re.search(
|
||||||
r"低速|(?:2|二)(?:档|级)", thermal_text
|
r"低速|(?:1|一)(?:档|级)", thermal_text
|
||||||
) or re.search(r"打开|开启|启动|启用", thermal_text):
|
) or re.search(r"打开|开启|启动|启用", thermal_text):
|
||||||
parameters = {"state": "on", "level": 2}
|
parameters = {"state": "on", "level": 2}
|
||||||
elif shared_thermal_state == "off":
|
elif shared_thermal_state == "off":
|
||||||
@@ -1177,7 +1206,8 @@ def _parse_chinese_commands(text: str) -> list[dict[str, Any]]:
|
|||||||
compact,
|
compact,
|
||||||
r"座椅|座位|靠背|椅背|坐盆|坐垫|腿托|脚托|按摩|记忆|预设|"
|
r"座椅|座位|靠背|椅背|坐盆|坐垫|腿托|脚托|按摩|记忆|预设|"
|
||||||
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(
|
actions: list[dict[str, Any]] = _parse_chinese_driver_seat_commands(
|
||||||
seat_text
|
seat_text
|
||||||
@@ -1794,7 +1824,7 @@ def merge_native_can_intent(
|
|||||||
utterance
|
utterance
|
||||||
)
|
)
|
||||||
if unsupported_thermal_middle is not None and not re.search(
|
if unsupported_thermal_middle is not None and not re.search(
|
||||||
r"副驾|副驾驶|乘客|右前", utterance
|
r"副驾|副驾驶|乘客|乘员|右前", utterance
|
||||||
):
|
):
|
||||||
result = dict(parsed)
|
result = dict(parsed)
|
||||||
feature = (
|
feature = (
|
||||||
|
|||||||
@@ -1334,7 +1334,10 @@ class NativeCanExecutor:
|
|||||||
# separate evidence level in ``verification_status`` and
|
# separate evidence level in ``verification_status`` and
|
||||||
# ``physical_effect_confirmed``. A feedback timeout must
|
# ``physical_effect_confirmed``. A feedback timeout must
|
||||||
# not be rewritten as a transport/orchestration failure.
|
# 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 = (
|
verification_status = (
|
||||||
"verified_success"
|
"verified_success"
|
||||||
if verified
|
if verified
|
||||||
@@ -1354,7 +1357,11 @@ class NativeCanExecutor:
|
|||||||
publication_start:
|
publication_start:
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
delivery_status = "submitted"
|
delivery_status = (
|
||||||
|
"not_required"
|
||||||
|
if verified and not delivered
|
||||||
|
else "submitted"
|
||||||
|
)
|
||||||
if not feedback_required and delivered:
|
if not feedback_required and delivered:
|
||||||
delivery_status = (
|
delivery_status = (
|
||||||
"published"
|
"published"
|
||||||
@@ -1371,7 +1378,9 @@ class NativeCanExecutor:
|
|||||||
"domain": function_config["spec"]["domain"],
|
"domain": function_config["spec"]["domain"],
|
||||||
"status": "succeeded" if succeeded else "failed",
|
"status": "succeeded" if succeeded else "failed",
|
||||||
"message": (
|
"message": (
|
||||||
"MQTT 白名单控制帧已由 Broker 确认;车辆效果以状态话题回读为准。"
|
"车辆反馈已确认目标状态,无需重复下发控制帧。"
|
||||||
|
if verified and not delivered
|
||||||
|
else "MQTT 白名单控制帧已由 Broker 确认;车辆效果以状态话题回读为准。"
|
||||||
if not feedback_required and delivery_status == "published"
|
if not feedback_required and delivery_status == "published"
|
||||||
else "MQTT 白名单控制帧已进入客户端发布队列;车辆效果以状态话题回读为准。"
|
else "MQTT 白名单控制帧已进入客户端发布队列;车辆效果以状态话题回读为准。"
|
||||||
if not feedback_required
|
if not feedback_required
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ from cognitive_agent.application.native_can_feedback import ( # noqa: E402
|
|||||||
)
|
)
|
||||||
from cognitive_agent.application.native_can_intent import ( # noqa: E402
|
from cognitive_agent.application.native_can_intent import ( # noqa: E402
|
||||||
DRIVER_SEAT_MASSAGE_CONTINUATION_PATTERN,
|
DRIVER_SEAT_MASSAGE_CONTINUATION_PATTERN,
|
||||||
|
DRIVER_SEAT_THERMAL_CONTINUATION_PATTERN,
|
||||||
merge_native_can_intent,
|
merge_native_can_intent,
|
||||||
)
|
)
|
||||||
from cognitive_agent.application.native_can_relative import ( # noqa: E402
|
from cognitive_agent.application.native_can_relative import ( # noqa: E402
|
||||||
@@ -202,6 +203,39 @@ LOCAL_CONTROL_BUDGET_MS = 1000
|
|||||||
VEHICLE_CONTROL_TRANSPORTS = frozenset(
|
VEHICLE_CONTROL_TRANSPORTS = frozenset(
|
||||||
{WIRED_CAN_TRANSPORT, WIRELESS_MQTT_TRANSPORT}
|
{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]:
|
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),
|
lambda scene: self._vehicle_mqtt_transport.publish_mode_scene(scene),
|
||||||
topic=MQTT_MODE_SCENE_TOPIC,
|
topic=MQTT_MODE_SCENE_TOPIC,
|
||||||
)
|
)
|
||||||
|
self._mode_scene_peripheral_catalog_provider: Callable[
|
||||||
|
[], list[dict[str, Any]]
|
||||||
|
] = lambda: []
|
||||||
self._mode_scene_ingress = ModeSceneIngressConsumer(
|
self._mode_scene_ingress = ModeSceneIngressConsumer(
|
||||||
self.import_n7_mode_scene,
|
self.import_n7_mode_scene,
|
||||||
)
|
)
|
||||||
@@ -637,6 +674,378 @@ class SeatDemoService(ModeScheduleServiceMixin):
|
|||||||
|
|
||||||
return self.publish_saved_mode_scene(mode_id, vehicle_id)
|
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
|
@staticmethod
|
||||||
def _imported_scene_function_configs(
|
def _imported_scene_function_configs(
|
||||||
scene: dict[str, Any],
|
scene: dict[str, Any],
|
||||||
@@ -694,6 +1103,7 @@ class SeatDemoService(ModeScheduleServiceMixin):
|
|||||||
"""Upsert one validated N7 card without publishing it back to the broker."""
|
"""Upsert one validated N7 card without publishing it back to the broker."""
|
||||||
|
|
||||||
document = deepcopy(dict(scene))
|
document = deepcopy(dict(scene))
|
||||||
|
cinema_augmentation = self._augment_n7_cinema_scene(document)
|
||||||
actions = document.get("actions")
|
actions = document.get("actions")
|
||||||
if not isinstance(actions, list) or not actions:
|
if not isinstance(actions, list) or not actions:
|
||||||
raise ValueError("from_n7/scenes card must contain executable 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,
|
sync_origin=MQTT_MODE_SCENE_INGRESS_TOPIC,
|
||||||
upstream_origin=str(document.get("origin") or ""),
|
upstream_origin=str(document.get("origin") or ""),
|
||||||
)
|
)
|
||||||
return {
|
result = {
|
||||||
"status": "imported",
|
"status": "imported",
|
||||||
"scene_id": saved.id,
|
"scene_id": saved.id,
|
||||||
"scene_name": saved.name,
|
"scene_name": saved.name,
|
||||||
@@ -772,6 +1182,9 @@ class SeatDemoService(ModeScheduleServiceMixin):
|
|||||||
"profile_revision": self.mode_profiles.profile_revision(saved),
|
"profile_revision": self.mode_profiles.profile_revision(saved),
|
||||||
"storage": "persistent_yaml",
|
"storage": "persistent_yaml",
|
||||||
}
|
}
|
||||||
|
if cinema_augmentation is not None:
|
||||||
|
result["cinema_augmentation"] = cinema_augmentation
|
||||||
|
return result
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def driver_only_ch1_execution_active(self) -> bool:
|
def driver_only_ch1_execution_active(self) -> bool:
|
||||||
@@ -1015,14 +1428,42 @@ class SeatDemoService(ModeScheduleServiceMixin):
|
|||||||
|
|
||||||
requests = self.mode_profiles.requests_for_profiles([profile])
|
requests = self.mode_profiles.requests_for_profiles([profile])
|
||||||
if profile.sync_origin != MQTT_MODE_SCENE_INGRESS_TOPIC:
|
if profile.sync_origin != MQTT_MODE_SCENE_INGRESS_TOPIC:
|
||||||
return [
|
transfer_actions = self._local_cinema_n7_transfer_actions(
|
||||||
request
|
profile.name,
|
||||||
for operation, request in zip(profile.operations, requests, strict=True)
|
profile.instruction,
|
||||||
if not (
|
)
|
||||||
|
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"
|
operation.domain == "self_extension"
|
||||||
and operation.capability == "infrared.emit"
|
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 = (
|
source_scene = (
|
||||||
profile.command_flow.get("source_scene")
|
profile.command_flow.get("source_scene")
|
||||||
if isinstance(profile.command_flow, dict)
|
if isinstance(profile.command_flow, dict)
|
||||||
@@ -1109,7 +1550,7 @@ class SeatDemoService(ModeScheduleServiceMixin):
|
|||||||
execution_mode: str,
|
execution_mode: str,
|
||||||
local_preview: bool,
|
local_preview: bool,
|
||||||
) -> dict[str, Any]:
|
) -> 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")
|
actions = result.get("n7_transfer_candidates")
|
||||||
scene = result.get("n7_source_scene")
|
scene = result.get("n7_source_scene")
|
||||||
@@ -1579,6 +2020,48 @@ class SeatDemoService(ModeScheduleServiceMixin):
|
|||||||
raise ValueError("动态模式更新必须保留完整时间轴,请按阶段重新生成预览。")
|
raise ValueError("动态模式更新必须保留完整时间轴,请按阶段重新生成预览。")
|
||||||
if schedule is not None:
|
if schedule is not None:
|
||||||
operations = [deepcopy(op) for phase in schedule.phases for op in phase.operations]
|
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(
|
saved = self.mode_profiles.save_mode(
|
||||||
clean_name,
|
clean_name,
|
||||||
operations,
|
operations,
|
||||||
@@ -1843,6 +2326,16 @@ class SeatDemoService(ModeScheduleServiceMixin):
|
|||||||
|
|
||||||
self._vehicle_mqtt_transport.set_extension_control_rejection_handler(handler)
|
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(
|
def _synchronize_vehicle_state_once(
|
||||||
self,
|
self,
|
||||||
conversation_id: str,
|
conversation_id: str,
|
||||||
@@ -8372,6 +8865,11 @@ class SeatDemoService(ModeScheduleServiceMixin):
|
|||||||
and "按摩" in clauses[-1]
|
and "按摩" in clauses[-1]
|
||||||
and DRIVER_SEAT_MASSAGE_CONTINUATION_PATTERN.search(fragment)
|
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,
|
# An exclusion is a qualifier of the control immediately before it,
|
||||||
# not an independent command. Keeping both pieces together lets
|
# not an independent command. Keeping both pieces together lets
|
||||||
# the deterministic window parser remove the excluded targets
|
# the deterministic window parser remove the excluded targets
|
||||||
@@ -8379,7 +8877,7 @@ class SeatDemoService(ModeScheduleServiceMixin):
|
|||||||
exclusion_qualifier = bool(
|
exclusion_qualifier = bool(
|
||||||
clauses and re.search(r"除外|除去|不包括|不含", fragment)
|
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}"
|
clauses[-1] = f"{clauses[-1]},{fragment}"
|
||||||
else:
|
else:
|
||||||
clauses.append(fragment)
|
clauses.append(fragment)
|
||||||
|
|||||||
@@ -1050,6 +1050,152 @@ def _peripheral_execution_identity_authorized(
|
|||||||
return bool(_device_match_score(_compact_match_text(utterance), capability))
|
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]:
|
def _normalize_peripheral_capability(value: Any) -> dict[str, Any]:
|
||||||
"""Validate one browser-synchronized learned infrared capability."""
|
"""Validate one browser-synchronized learned infrared capability."""
|
||||||
|
|
||||||
@@ -2615,6 +2761,36 @@ class SelfExtensionService:
|
|||||||
raise ExtensionIntegrationError(
|
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"))
|
slot = validate_slot(capability.get("slot"))
|
||||||
request_id = f"req-extension-{uuid.uuid4().hex}"
|
request_id = f"req-extension-{uuid.uuid4().hex}"
|
||||||
conversation_id = conversation_id or f"conv-{uuid.uuid4().hex}"
|
conversation_id = conversation_id or f"conv-{uuid.uuid4().hex}"
|
||||||
@@ -2819,6 +2995,39 @@ class SelfExtensionService:
|
|||||||
raise ExtensionIntegrationError("GPT 没有生成可执行的外设动作。")
|
raise ExtensionIntegrationError("GPT 没有生成可执行的外设动作。")
|
||||||
if any(step.get("kind") == "vehicle" for step in steps if isinstance(step, dict)):
|
if any(step.get("kind") == "vehicle" for step in steps if isinstance(step, dict)):
|
||||||
raise ExtensionIntegrationError("当前主编排只允许执行持久绑定的 NFC 外设计划。")
|
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 = {
|
catalog_by_id = {
|
||||||
str(item.get("id")): item
|
str(item.get("id")): item
|
||||||
for item in catalog
|
for item in catalog
|
||||||
@@ -2884,6 +3093,7 @@ class SelfExtensionService:
|
|||||||
function_operations.append(
|
function_operations.append(
|
||||||
{
|
{
|
||||||
"id": operation_ref,
|
"id": operation_ref,
|
||||||
|
"capability_id": capability_id,
|
||||||
"capability": "infrared.emit",
|
"capability": "infrared.emit",
|
||||||
"target": {
|
"target": {
|
||||||
"device": capability.get("device_id"),
|
"device": capability.get("device_id"),
|
||||||
@@ -2897,6 +3107,8 @@ class SelfExtensionService:
|
|||||||
"parse_source": capability.get(
|
"parse_source": capability.get(
|
||||||
"parse_source", "local_rule"
|
"parse_source", "local_rule"
|
||||||
),
|
),
|
||||||
|
"auto_inserted": step.get("auto_inserted") is True,
|
||||||
|
"dependency": step.get("dependency"),
|
||||||
"timeout_ms": int(self.command_timeout * 1000),
|
"timeout_ms": int(self.command_timeout * 1000),
|
||||||
"failure_policy": "abort",
|
"failure_policy": "abort",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,9 +36,13 @@ POSITIVE_OUTLET_MODE = re.compile(
|
|||||||
)
|
)
|
||||||
DRIVER_SEAT_THERMAL_MIDDLE = re.compile(
|
DRIVER_SEAT_THERMAL_MIDDLE = re.compile(
|
||||||
r"(?:通风|加热)[^,,;;。!?!?\n]{0,16}"
|
r"(?:通风|加热)[^,,;;。!?!?\n]{0,16}"
|
||||||
r"(?:中等(?:档|级)?|中档|中级|中间档|适中|(?:调到|调为|调|设为|设置为|调整到|调整为)中)|"
|
r"(?:中等(?:档|级)?|中档|中级|中间档|适中|(?:2|二)(?:档|级)|"
|
||||||
|
r"(?:调到|调为|调|设为|设置为|调整到|调整为)中)|"
|
||||||
r"(?:中等(?:档|级)?|中档|中级|中间档|适中)"
|
r"(?:中等(?:档|级)?|中档|中级|中间档|适中)"
|
||||||
r"[^,,;;。!?!?\n]{0,16}(?:通风|加热)"
|
r"[^,,;;。!?!?\n]{0,16}(?:通风|加热)|"
|
||||||
|
r"(?:通风|加热)[^,,;;。!?!?\n]{0,16}[,,;;]"
|
||||||
|
r"(?:挡位|档位)?(?:调到|调为|设为|设置为|调整到|调整为)?"
|
||||||
|
r"(?:2|二)(?:档|级)"
|
||||||
)
|
)
|
||||||
META_NON_EXECUTION = re.compile(
|
META_NON_EXECUTION = re.compile(
|
||||||
r"复述|引用|翻译|改写|总结|解释|分析|造句|"
|
r"复述|引用|翻译|改写|总结|解释|分析|造句|"
|
||||||
@@ -542,7 +546,7 @@ def apply_language_safety_guards(
|
|||||||
|
|
||||||
thermal_middle = DRIVER_SEAT_THERMAL_MIDDLE.search(utterance)
|
thermal_middle = DRIVER_SEAT_THERMAL_MIDDLE.search(utterance)
|
||||||
if thermal_middle is not None and not re.search(
|
if thermal_middle is not None and not re.search(
|
||||||
r"副驾|副驾驶|乘客|右前", utterance
|
r"副驾|副驾驶|乘客|乘员|右前", utterance
|
||||||
):
|
):
|
||||||
feature = "通风" if "通风" in thermal_middle.group(0) else "加热"
|
feature = "通风" if "通风" in thermal_middle.group(0) else "加热"
|
||||||
native_capability = (
|
native_capability = (
|
||||||
|
|||||||
@@ -864,6 +864,19 @@ class UnderstandingTranscriptProcessor:
|
|||||||
while index < len(commands):
|
while index < len(commands):
|
||||||
current = commands[index]
|
current = commands[index]
|
||||||
following = commands[index + 1] if index + 1 < len(commands) else None
|
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 (
|
if (
|
||||||
following is not None
|
following is not None
|
||||||
and current.property_name == following.property_name == "ambient.color"
|
and current.property_name == following.property_name == "ambient.color"
|
||||||
|
|||||||
+641
-105
@@ -3,6 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import hashlib
|
||||||
import hmac
|
import hmac
|
||||||
import ipaddress
|
import ipaddress
|
||||||
import json
|
import json
|
||||||
@@ -14,6 +15,7 @@ import sys
|
|||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from collections.abc import Callable, Mapping
|
from collections.abc import Callable, Mapping
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from http.cookies import CookieError, SimpleCookie
|
from http.cookies import CookieError, SimpleCookie
|
||||||
@@ -95,6 +97,7 @@ from cognitive_agent.auth import ( # noqa: E402
|
|||||||
verify_password,
|
verify_password,
|
||||||
)
|
)
|
||||||
from cognitive_agent.infrastructure.can.access import ( # noqa: E402
|
from cognitive_agent.infrastructure.can.access import ( # noqa: E402
|
||||||
|
HARDWARE_CLIENT_ID_PATTERN,
|
||||||
HardwareAccessRequiredError,
|
HardwareAccessRequiredError,
|
||||||
)
|
)
|
||||||
from cognitive_agent.infrastructure.can.gateway import CanGatewayError # noqa: E402
|
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"})
|
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:
|
class VehicleDisplaySync:
|
||||||
"""Process-local snapshot consumed by passive vehicle display clients."""
|
"""Process-local snapshot consumed by passive vehicle display clients."""
|
||||||
|
|
||||||
@@ -272,6 +294,9 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
|||||||
catalog_provider=extension.list_peripheral_capabilities,
|
catalog_provider=extension.list_peripheral_capabilities,
|
||||||
executor=extension.execute_bound_infrared_action,
|
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_handler(extension_control.submit)
|
||||||
service.set_extension_control_rejection_handler(
|
service.set_extension_control_rejection_handler(
|
||||||
extension_control.reject_transport_message
|
extension_control.reject_transport_message
|
||||||
@@ -361,7 +386,9 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
|||||||
try:
|
try:
|
||||||
self._json(
|
self._json(
|
||||||
self.service.hardware_access.status(
|
self.service.hardware_access.status(
|
||||||
str(query.get("client_id", [""])[0])
|
_compatible_hardware_client_id(
|
||||||
|
query.get("client_id", [""])[0]
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
@@ -478,7 +505,9 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
|||||||
control_transport = str(
|
control_transport = str(
|
||||||
query.get("control_transport", ["wired_can"])[0]
|
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:
|
try:
|
||||||
self._json(
|
self._json(
|
||||||
(
|
(
|
||||||
@@ -769,10 +798,8 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
|||||||
utterance=utterance,
|
utterance=utterance,
|
||||||
source=source,
|
source=source,
|
||||||
)
|
)
|
||||||
hardware_client_id = (
|
hardware_client_id = _compatible_hardware_client_id(
|
||||||
str(payload["hardware_client_id"])
|
payload.get("hardware_client_id")
|
||||||
if payload.get("hardware_client_id")
|
|
||||||
else None
|
|
||||||
)
|
)
|
||||||
reasoning_effort = (
|
reasoning_effort = (
|
||||||
str(payload["reasoning_effort"])
|
str(payload["reasoning_effort"])
|
||||||
@@ -1231,6 +1258,7 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
|||||||
conversation_id: str | None,
|
conversation_id: str | None,
|
||||||
execution_mode: str,
|
execution_mode: str,
|
||||||
on_commands_validated: Callable[[dict[str, object]], None] | None = None,
|
on_commands_validated: Callable[[dict[str, object]], None] | None = None,
|
||||||
|
update_display: bool = True,
|
||||||
) -> dict[str, object]:
|
) -> dict[str, object]:
|
||||||
"""Execute already validated infrared capabilities."""
|
"""Execute already validated infrared capabilities."""
|
||||||
|
|
||||||
@@ -1281,17 +1309,257 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
|||||||
on_commands_validated=on_commands_validated,
|
on_commands_validated=on_commands_validated,
|
||||||
)
|
)
|
||||||
result["provider_used"] = "infrared_local_rules"
|
result["provider_used"] = "infrared_local_rules"
|
||||||
self.vehicle_display_sync.update_extension(
|
if update_display:
|
||||||
action={
|
self.vehicle_display_sync.update_extension(
|
||||||
"type": "emit_sequence" if len(capabilities) > 1 else "emit",
|
action={
|
||||||
"source": "main_orchestration_local_rule",
|
"type": "emit_sequence" if len(capabilities) > 1 else "emit",
|
||||||
"capability_ids": [item["id"] for item in capabilities],
|
"source": "main_orchestration_local_rule",
|
||||||
"slots": [item["slot"] for item in capabilities],
|
"capability_ids": [item["id"] for item in capabilities],
|
||||||
},
|
"slots": [item["slot"] for item in capabilities],
|
||||||
result=result,
|
},
|
||||||
)
|
result=result,
|
||||||
|
)
|
||||||
return 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
|
@staticmethod
|
||||||
def _split_scene_peripheral_plan(
|
def _split_scene_peripheral_plan(
|
||||||
assistant_plan: Mapping[str, object],
|
assistant_plan: Mapping[str, object],
|
||||||
@@ -1800,24 +2068,111 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
|||||||
labels = metadata.setdefault("labels", {})
|
labels = metadata.setdefault("labels", {})
|
||||||
labels["route"] = "unified_can_infrared"
|
labels["route"] = "unified_can_infrared"
|
||||||
|
|
||||||
infrared_actions = [
|
entry_lookup: dict[tuple[str, object, str], dict[str, object]] = {}
|
||||||
{
|
for entry in entries:
|
||||||
"index": entry.get("index"),
|
capability = entry.get("capability") if isinstance(entry, dict) else None
|
||||||
"clause": entry.get("clause"),
|
if not isinstance(capability, dict):
|
||||||
"capability_id": capability.get("id"),
|
continue
|
||||||
"device_id": capability.get("device_id"),
|
entry_lookup[
|
||||||
"device_name": capability.get("device_name"),
|
(
|
||||||
"action": capability.get("action"),
|
str(capability.get("device_id") or ""),
|
||||||
"slot": capability.get("slot"),
|
capability.get("slot"),
|
||||||
"parse_source": str(
|
str(capability.get("action") or ""),
|
||||||
capability.get("parse_source") or "local_rule"
|
)
|
||||||
),
|
] = entry
|
||||||
"transport": str(infrared_result.get("control_transport") or "gscu_tcp"),
|
extension_config = (infrared_result.get("function_configs") or {}).get(
|
||||||
}
|
"self_extension"
|
||||||
for entry in entries
|
)
|
||||||
for capability in [entry.get("capability")]
|
extension_spec = (
|
||||||
if isinstance(capability, dict)
|
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 = (
|
preview_only = (
|
||||||
str(can_result.get("execution_mode") or "dry_run") != "hardware"
|
str(can_result.get("execution_mode") or "dry_run") != "hardware"
|
||||||
and str(infrared_result.get("execution_mode") or "dry_run")
|
and str(infrared_result.get("execution_mode") or "dry_run")
|
||||||
@@ -2262,47 +2617,78 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
|||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
entries = self._saved_mode_infrared_entries(profile)
|
entries = self._saved_mode_infrared_entries(profile)
|
||||||
can_result = self.service.execute_mode_profile_card(
|
mode_id = str(getattr(profile, "id"))
|
||||||
str(getattr(profile, "id")),
|
simulation_fault = str(common_arguments.get("simulation_fault") or "normal")
|
||||||
conversation_id=conversation_id,
|
control_transport = str(
|
||||||
execution_mode=execution_mode,
|
common_arguments.get("control_transport") or "wired_can"
|
||||||
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"
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
if not entries:
|
hardware_client_id = _compatible_hardware_client_id(
|
||||||
can_result["utterance"] = utterance
|
common_arguments.get("hardware_client_id")
|
||||||
return can_result
|
)
|
||||||
infrared_result = self._execute_local_peripheral_actions(
|
hardware_access_bypass = bool(
|
||||||
utterance,
|
common_arguments.get("hardware_access_bypass", True)
|
||||||
entries,
|
)
|
||||||
|
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",
|
provider="saved_mode_command_flow",
|
||||||
conversation_id=conversation_id,
|
conversation_id=conversation_id,
|
||||||
execution_mode=execution_mode,
|
can_execute=execute_can,
|
||||||
)
|
can_preflight=preflight_can,
|
||||||
result = self._merge_can_and_peripheral_results(
|
parallel_eligible=parallel_eligible,
|
||||||
utterance,
|
|
||||||
can_result,
|
|
||||||
infrared_result,
|
|
||||||
entries,
|
|
||||||
)
|
)
|
||||||
|
if not entries:
|
||||||
|
result["utterance"] = utterance
|
||||||
|
return result
|
||||||
result["provider_used"] = "saved_mode_command_flow"
|
result["provider_used"] = "saved_mode_command_flow"
|
||||||
result["orchestration_route"] = "saved_mode_command_flow"
|
result["orchestration_route"] = "saved_mode_command_flow"
|
||||||
result["mode_profile"] = profile.public_dict()
|
result["mode_profile"] = profile.public_dict()
|
||||||
@@ -2516,6 +2902,125 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
|||||||
)
|
)
|
||||||
return result
|
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
|
# Compile, publish and execute every deterministic IR action before
|
||||||
# any hosted planner is contacted.
|
# any hosted planner is contacted.
|
||||||
if entries:
|
if entries:
|
||||||
@@ -2892,10 +3397,8 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
|||||||
approval_token=os.getenv(
|
approval_token=os.getenv(
|
||||||
"COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN"
|
"COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN"
|
||||||
),
|
),
|
||||||
hardware_client_id=(
|
hardware_client_id=_compatible_hardware_client_id(
|
||||||
str(payload["hardware_client_id"])
|
payload.get("hardware_client_id")
|
||||||
if payload.get("hardware_client_id")
|
|
||||||
else None
|
|
||||||
),
|
),
|
||||||
hardware_access_bypass=self._hardware_access_bypass_allowed(),
|
hardware_access_bypass=self._hardware_access_bypass_allowed(),
|
||||||
reasoning_effort=(
|
reasoning_effort=(
|
||||||
@@ -3006,8 +3509,8 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
|||||||
history_payload = payload.get("history")
|
history_payload = payload.get("history")
|
||||||
history = history_payload if isinstance(history_payload, list) else []
|
history = history_payload if isinstance(history_payload, list) else []
|
||||||
source = str(payload.get("client_type") or "web_console_assistant")
|
source = str(payload.get("client_type") or "web_console_assistant")
|
||||||
hardware_client_id = (
|
hardware_client_id = _compatible_hardware_client_id(
|
||||||
str(payload["hardware_client_id"]) if payload.get("hardware_client_id") else None
|
payload.get("hardware_client_id")
|
||||||
)
|
)
|
||||||
reasoning_effort = (
|
reasoning_effort = (
|
||||||
str(payload["reasoning_effort"])
|
str(payload["reasoning_effort"])
|
||||||
@@ -3815,10 +4318,8 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
|||||||
if payload.get("conversation_id")
|
if payload.get("conversation_id")
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
hardware_client_id = (
|
hardware_client_id = _compatible_hardware_client_id(
|
||||||
str(payload["hardware_client_id"])
|
payload.get("hardware_client_id")
|
||||||
if payload.get("hardware_client_id")
|
|
||||||
else None
|
|
||||||
)
|
)
|
||||||
self.vehicle_display_sync.update_task(
|
self.vehicle_display_sync.update_task(
|
||||||
status="running",
|
status="running",
|
||||||
@@ -3828,39 +4329,69 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
|||||||
# Validate the complete saved flow against the durable NFC binding
|
# Validate the complete saved flow against the durable NFC binding
|
||||||
# before any transport is allowed to execute.
|
# before any transport is allowed to execute.
|
||||||
entries = self._saved_mode_infrared_entries(profile)
|
entries = self._saved_mode_infrared_entries(profile)
|
||||||
can_result = self.service.execute_mode_profile_card(
|
simulation_fault = str(payload.get("simulation_fault", "normal"))
|
||||||
mode_id,
|
hardware_access_bypass = self._hardware_access_bypass_allowed()
|
||||||
conversation_id=conversation_id,
|
command_flow = getattr(profile, "command_flow", None)
|
||||||
execution_mode=execution_mode,
|
execution_steps = (
|
||||||
approval_token=os.getenv(
|
command_flow.get("execution_steps", [])
|
||||||
"COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN"
|
if isinstance(command_flow, Mapping)
|
||||||
),
|
else []
|
||||||
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,
|
|
||||||
)
|
)
|
||||||
if entries:
|
parallel_eligible = bool(
|
||||||
infrared_result = self._execute_local_peripheral_actions(
|
getattr(profile, "schedule", None) is None
|
||||||
utterance,
|
and not execution_steps
|
||||||
entries,
|
)
|
||||||
provider="saved_mode_command_flow",
|
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,
|
conversation_id=conversation_id,
|
||||||
execution_mode=execution_mode,
|
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,
|
def preflight_can() -> dict[str, object]:
|
||||||
can_result,
|
return self.service.execute_mode_profile_card(
|
||||||
infrared_result,
|
mode_id,
|
||||||
entries,
|
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["provider_used"] = "saved_mode_command_flow"
|
||||||
result["orchestration_route"] = "saved_mode_command_flow"
|
result["orchestration_route"] = "saved_mode_command_flow"
|
||||||
result["mode_profile"] = profile.public_dict()
|
result["mode_profile"] = profile.public_dict()
|
||||||
self.service.persist_composed_result(result)
|
self.service.persist_composed_result(result)
|
||||||
else:
|
|
||||||
result = can_result
|
|
||||||
self.vehicle_display_sync.update_task(
|
self.vehicle_display_sync.update_task(
|
||||||
status=str(result.get("status") or "succeeded"),
|
status=str(result.get("status") or "succeeded"),
|
||||||
utterance=utterance,
|
utterance=utterance,
|
||||||
@@ -4035,6 +4566,9 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
|||||||
self._json(result)
|
self._json(result)
|
||||||
return
|
return
|
||||||
if path == "/api/extension/vehicle/execute":
|
if path == "/api/extension/vehicle/execute":
|
||||||
|
payload["hardware_client_id"] = _compatible_hardware_client_id(
|
||||||
|
payload.get("hardware_client_id")
|
||||||
|
)
|
||||||
result = self.extension.execute_vehicle(
|
result = self.extension.execute_vehicle(
|
||||||
payload,
|
payload,
|
||||||
self.service,
|
self.service,
|
||||||
@@ -4362,7 +4896,9 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
|||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
payload = self._read_json_body(max_bytes=4096)
|
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":
|
if path == "/api/hardware/access/request":
|
||||||
status = self.service.hardware_access.request_access(
|
status = self.service.hardware_access.request_access(
|
||||||
client_id,
|
client_id,
|
||||||
|
|||||||
+145
-54
@@ -997,14 +997,14 @@ operations:
|
|||||||
def runtime_url(file_name: str, source_url: str) -> str:
|
def runtime_url(file_name: str, source_url: str) -> str:
|
||||||
hashed = runtime_assets.get(file_name)
|
hashed = runtime_assets.get(file_name)
|
||||||
return f"/{hashed}" if hashed else source_url
|
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")
|
app_script = response.read().decode("utf-8")
|
||||||
with self.opener.open(
|
with self.opener.open(
|
||||||
f"{self.base_url}/gesture-control.js?v=android-pad-9", timeout=3
|
f"{self.base_url}/gesture-control.js?v=android-pad-9", timeout=3
|
||||||
) as response:
|
) as response:
|
||||||
gesture_script = response.read().decode("utf-8")
|
gesture_script = response.read().decode("utf-8")
|
||||||
with self.opener.open(
|
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:
|
) as response:
|
||||||
console_script = response.read().decode("utf-8")
|
console_script = response.read().decode("utf-8")
|
||||||
with self.opener.open(
|
with self.opener.open(
|
||||||
@@ -1024,7 +1024,7 @@ operations:
|
|||||||
) as response:
|
) as response:
|
||||||
speech_worklet = response.read().decode("utf-8")
|
speech_worklet = response.read().decode("utf-8")
|
||||||
with self.opener.open(
|
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:
|
) as response:
|
||||||
extension_script = response.read().decode("utf-8")
|
extension_script = response.read().decode("utf-8")
|
||||||
with self.opener.open(
|
with self.opener.open(
|
||||||
@@ -1032,7 +1032,7 @@ operations:
|
|||||||
) as response:
|
) as response:
|
||||||
extension_styles = response.read().decode("utf-8").replace("\r\n", "\n")
|
extension_styles = response.read().decode("utf-8").replace("\r\n", "\n")
|
||||||
with self.opener.open(
|
with self.opener.open(
|
||||||
f"{self.base_url}/assistant-chat.js?v=codex-assistant-35", timeout=3
|
f"{self.base_url}/assistant-chat.js?v=codex-assistant-39-video-media", timeout=3
|
||||||
) as response:
|
) as response:
|
||||||
assistant_script = response.read().decode("utf-8")
|
assistant_script = response.read().decode("utf-8")
|
||||||
with self.opener.open(
|
with self.opener.open(
|
||||||
@@ -1084,13 +1084,13 @@ operations:
|
|||||||
self.assertIn("CMD 0x431 · FB 0x388 · CH1", template)
|
self.assertIn("CMD 0x431 · FB 0x388 · CH1", template)
|
||||||
self.assertNotIn("透明车体 · 实体设备", template)
|
self.assertNotIn("透明车体 · 实体设备", template)
|
||||||
self.assertIn(
|
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,
|
html,
|
||||||
)
|
)
|
||||||
self.assertIn('import "../styles.css";', vite_entry)
|
self.assertIn('import "../styles.css";', vite_entry)
|
||||||
self.assertNotIn('class="vehicle-system-hud"', template)
|
self.assertNotIn('class="vehicle-system-hud"', template)
|
||||||
self.assertIn(
|
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,
|
html,
|
||||||
)
|
)
|
||||||
self.assertIn('import "../console-v2.css";', vite_entry)
|
self.assertIn('import "../console-v2.css";', vite_entry)
|
||||||
@@ -1105,7 +1105,7 @@ operations:
|
|||||||
self.assertIn('id="v2BootScreen"', template)
|
self.assertIn('id="v2BootScreen"', template)
|
||||||
self.assertIn('正在加载车机控制与实时状态', template)
|
self.assertIn('正在加载车机控制与实时状态', template)
|
||||||
self.assertIn(
|
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,
|
html,
|
||||||
)
|
)
|
||||||
self.assertIn('import "../self-extension.css";', vite_entry)
|
self.assertIn('import "../self-extension.css";', vite_entry)
|
||||||
@@ -1117,7 +1117,7 @@ operations:
|
|||||||
self.assertIn('id="assistantTranscript"', template)
|
self.assertIn('id="assistantTranscript"', template)
|
||||||
self.assertIn('import "../assistant-chat.css";', vite_entry)
|
self.assertIn('import "../assistant-chat.css";', vite_entry)
|
||||||
self.assertIn(
|
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,
|
html,
|
||||||
)
|
)
|
||||||
self.assertIn('import "../energy-monitor.css";', vite_entry)
|
self.assertIn('import "../energy-monitor.css";', vite_entry)
|
||||||
@@ -1433,16 +1433,16 @@ operations:
|
|||||||
self.assertIn(f'/{runtime_assets[asset_name]}', service_worker)
|
self.assertIn(f'/{runtime_assets[asset_name]}', service_worker)
|
||||||
else:
|
else:
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
'const CACHE_VERSION = "cognitive-agent-pad-v329-mode-preview"',
|
'const CACHE_VERSION = "cognitive-agent-pad-v334-video-media"',
|
||||||
service_worker,
|
service_worker,
|
||||||
)
|
)
|
||||||
self.assertIn('/console-v2.css?v=delivery-state-69', 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.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.css?v=energy-monitor-5-dialog-size', service_worker)
|
||||||
self.assertIn('/energy-monitor.js?v=energy-monitor-5-performance', 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.css?v=2', service_worker)
|
||||||
self.assertIn('/firmware-workbench-entry.js?v=3', service_worker)
|
self.assertIn('/firmware-workbench-entry.js?v=3', service_worker)
|
||||||
self.assertIn('/firmware-workbench-theme.js?v=1', service_worker)
|
self.assertIn('/firmware-workbench-theme.js?v=1', service_worker)
|
||||||
@@ -1910,7 +1910,7 @@ operations:
|
|||||||
) as response:
|
) as response:
|
||||||
extension_script = response.read().decode("utf-8")
|
extension_script = response.read().decode("utf-8")
|
||||||
with self.opener.open(
|
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:
|
) as response:
|
||||||
app_script = response.read().decode("utf-8")
|
app_script = response.read().decode("utf-8")
|
||||||
|
|
||||||
@@ -2904,47 +2904,51 @@ operations:
|
|||||||
).encode("utf-8"),
|
).encode("utf-8"),
|
||||||
)
|
)
|
||||||
|
|
||||||
def fail_can_after_validation(*_args, **kwargs):
|
can_preview = {
|
||||||
callback = kwargs.get("_on_commands_validated")
|
"request_id": "req-can-validation-only",
|
||||||
self.assertTrue(callable(callback))
|
"utterance": "打开空调,温度设置为24度,风速调整为3档,打开遮阳帘",
|
||||||
callback(
|
"status": "succeeded",
|
||||||
{
|
"execution_mode": "dry_run",
|
||||||
"request_id": "req-can-validation-only",
|
"control_transport": "wired_can",
|
||||||
"utterance": "打开空调,温度设置为24度,风速调整为3档,打开遮阳帘",
|
"provider_used": "local_rules",
|
||||||
"status": "processing",
|
"decision_summary": [],
|
||||||
"execution_mode": "hardware",
|
"function_configs": {
|
||||||
"control_transport": "wired_can",
|
"native_climate": {
|
||||||
"provider_used": "local_rules",
|
"spec": {
|
||||||
"decision_summary": [],
|
"operations": [
|
||||||
"function_configs": {
|
{
|
||||||
"native_climate": {
|
"id": "vehicle_set_power_1",
|
||||||
"spec": {
|
"capability": "climate.set_power",
|
||||||
"operations": [
|
"target": {"zone": "vehicle"},
|
||||||
{
|
"parameters": {"state": "on"},
|
||||||
"id": "vehicle_set_power_1",
|
"parse_source": "local_rule",
|
||||||
"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")
|
raise CanGatewayError("simulated CAN transport failure")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with patch.object(
|
with (
|
||||||
self.service,
|
patch.object(
|
||||||
"process",
|
self.service,
|
||||||
side_effect=fail_can_after_validation,
|
"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:
|
with self.opener.open(request, timeout=3) as response:
|
||||||
events = [
|
events = [
|
||||||
@@ -2963,11 +2967,8 @@ operations:
|
|||||||
for item in events
|
for item in events
|
||||||
if item["type"] == "validated_commands"
|
if item["type"] == "validated_commands"
|
||||||
]
|
]
|
||||||
self.assertEqual(set(validated_payloads[0]["function_configs"]), {"self_extension"})
|
self.assertEqual(len(validated_payloads), 1)
|
||||||
validated = next(
|
validated = validated_payloads[0]
|
||||||
payload for payload in validated_payloads
|
|
||||||
if {"native_climate", "self_extension"}.issubset(payload["function_configs"])
|
|
||||||
)
|
|
||||||
self.assertEqual(validated["status"], "processing")
|
self.assertEqual(validated["status"], "processing")
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
set(validated["function_configs"]),
|
set(validated["function_configs"]),
|
||||||
@@ -2977,6 +2978,10 @@ operations:
|
|||||||
[item["slot"] for item in validated["infrared_actions"]],
|
[item["slot"] for item in validated["infrared_actions"]],
|
||||||
[1, 4],
|
[1, 4],
|
||||||
)
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
validated["parallel_dispatch"]["preflight_status"],
|
||||||
|
"succeeded",
|
||||||
|
)
|
||||||
|
|
||||||
def test_disabled_nfc_ir_command_does_not_discard_allowed_can_and_ir(
|
def test_disabled_nfc_ir_command_does_not_discard_allowed_can_and_ir(
|
||||||
self,
|
self,
|
||||||
@@ -3173,6 +3178,18 @@ operations:
|
|||||||
)
|
)
|
||||||
extension.sync_peripheral_catalog(
|
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",
|
"id": "toy-001-light-red",
|
||||||
"status": "learned",
|
"status": "learned",
|
||||||
@@ -3239,6 +3256,11 @@ operations:
|
|||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
result["infrared_actions"][0]["parse_source"], "cloud_model"
|
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"])
|
self.assertFalse(result["supplement_required"])
|
||||||
|
|
||||||
def test_main_request_executes_gpt_nfc_sequence_without_vehicle_can(self) -> None:
|
def test_main_request_executes_gpt_nfc_sequence_without_vehicle_can(self) -> None:
|
||||||
@@ -4113,6 +4135,75 @@ operations:
|
|||||||
self.assertEqual(released["state"], "none")
|
self.assertEqual(released["state"], "none")
|
||||||
self.assertIsNone(self.service.hardware_access.admin_snapshot()["active"])
|
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:
|
def test_local_hardware_decision_page_is_not_exposed_through_proxy(self) -> None:
|
||||||
with self.opener.open(
|
with self.opener.open(
|
||||||
f"{self.base_url}/hardware-control", timeout=3
|
f"{self.base_url}/hardware-control", timeout=3
|
||||||
|
|||||||
@@ -271,7 +271,7 @@ CAPABILITY_CASES = (
|
|||||||
),
|
),
|
||||||
_CapabilityCase(
|
_CapabilityCase(
|
||||||
"seat.set_driver_heating_native",
|
"seat.set_driver_heating_native",
|
||||||
"打开主驾座椅加热2档",
|
"打开主驾座椅加热低档",
|
||||||
"driver",
|
"driver",
|
||||||
{"state": "on", "level": 2},
|
{"state": "on", "level": 2},
|
||||||
"activate",
|
"activate",
|
||||||
@@ -291,7 +291,7 @@ CAPABILITY_CASES = (
|
|||||||
),
|
),
|
||||||
_CapabilityCase(
|
_CapabilityCase(
|
||||||
"seat.set_driver_ventilation_native",
|
"seat.set_driver_ventilation_native",
|
||||||
"打开主驾座椅通风2档",
|
"打开主驾座椅通风低档",
|
||||||
"driver",
|
"driver",
|
||||||
{"state": "on", "level": 2},
|
{"state": "on", "level": 2},
|
||||||
"activate",
|
"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(
|
def test_scene_composer_mixed_can_ir_preview_save_and_revalidate(
|
||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -61,13 +78,19 @@ def test_scene_composer_mixed_can_ir_preview_save_and_revalidate(
|
|||||||
SimulationGateway(),
|
SimulationGateway(),
|
||||||
catalog_db_path=tmp_path / "infrared.sqlite3",
|
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(
|
catalog = AssistantCapabilityCatalog(
|
||||||
service,
|
service,
|
||||||
peripheral_provider=extension.list_peripheral_routing_capabilities,
|
peripheral_provider=extension.list_peripheral_routing_capabilities,
|
||||||
)
|
)
|
||||||
context = catalog.scene_composition_context(catalog.snapshot())
|
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)
|
encoded_context = json.dumps(context, ensure_ascii=False)
|
||||||
assert "2CC8B103" not in encoded_context
|
assert "2CC8B103" not in encoded_context
|
||||||
assert '"slot":7' 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",
|
"climate",
|
||||||
"self_extension",
|
"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"}
|
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"
|
"commands"
|
||||||
][0]
|
]
|
||||||
assert infrared_command["message"] == "infrared.emit"
|
assert [command["message"] for command in infrared_commands] == [
|
||||||
assert infrared_command["signals"] == {"slot": 7}
|
"infrared.emit",
|
||||||
|
"infrared.emit",
|
||||||
|
]
|
||||||
|
assert [command["signals"] for command in infrared_commands] == [
|
||||||
|
{"slot": 1},
|
||||||
|
{"slot": 7},
|
||||||
|
]
|
||||||
assert preview["infrared_actions"][0]["parse_source"] == (
|
assert preview["infrared_actions"][0]["parse_source"] == (
|
||||||
"scene_composer_capability_ref"
|
"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] == [
|
assert [operation.capability for operation in profile.operations] == [
|
||||||
"climate.set_temperature",
|
"climate.set_temperature",
|
||||||
"infrared.emit",
|
"infrared.emit",
|
||||||
|
"infrared.emit",
|
||||||
]
|
]
|
||||||
entries = handler._saved_mode_infrared_entries(profile)
|
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(
|
native_execution = service.execute_mode_profile_card(
|
||||||
profile.id,
|
profile.id,
|
||||||
conversation_id="conv-saved-scene-peripheral",
|
conversation_id="conv-saved-scene-peripheral",
|
||||||
@@ -179,7 +209,7 @@ def test_scene_composer_mixed_can_ir_preview_save_and_revalidate(
|
|||||||
entries,
|
entries,
|
||||||
)
|
)
|
||||||
assert executed["status"] == "succeeded"
|
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(
|
invoked = handler._execute_request_with_peripherals(
|
||||||
"/api/requests/preview",
|
"/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 is not None
|
||||||
assert invoked["status"] == "succeeded"
|
assert invoked["status"] == "succeeded"
|
||||||
assert invoked["execution_mode"] == "dry_run"
|
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(
|
template_plan = _catalog_exact_scene_template_plan(
|
||||||
catalog.snapshot(),
|
catalog.snapshot(),
|
||||||
@@ -301,8 +331,8 @@ def test_scene_composer_mixed_can_ir_preview_save_and_revalidate(
|
|||||||
assert explicit is not None
|
assert explicit is not None
|
||||||
assert explicit["status"] == "succeeded"
|
assert explicit["status"] == "succeeded"
|
||||||
assert explicit["execution_mode"] == "dry_run"
|
assert explicit["execution_mode"] == "dry_run"
|
||||||
assert explicit["mode_profile_draft"]["operation_count"] == 2
|
assert explicit["mode_profile_draft"]["operation_count"] == 3
|
||||||
assert explicit["infrared_actions"][0]["slot"] == 7
|
assert [action["slot"] for action in explicit["infrared_actions"]] == [1, 7]
|
||||||
|
|
||||||
# Removing the durable NFC whitelist invalidates both cached refs and saved
|
# Removing the durable NFC whitelist invalidates both cached refs and saved
|
||||||
# mode execution before any transport can be reached.
|
# 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);
|
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 () => {
|
test("a follow-up turn carries only the active authoritative draft reference", async () => {
|
||||||
const events = modeDraftEvents("req-draft-one", 24);
|
const events = modeDraftEvents("req-draft-one", 24);
|
||||||
const harness = createHarness(events);
|
const harness = createHarness(events);
|
||||||
|
|||||||
@@ -21,17 +21,17 @@ def test_console_contains_ai_assistant_surface():
|
|||||||
assert 'placeholder="向 AI助手提问或交代工作"' in template
|
assert 'placeholder="向 AI助手提问或交代工作"' in template
|
||||||
assert 'aria-label="发送消息"' in template
|
assert 'aria-label="发送消息"' in template
|
||||||
assert 'import "../assistant-chat.css";' in vite_entry
|
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():
|
def test_codex_assistant_assets_are_versioned_together_in_the_service_worker():
|
||||||
service_worker = (ROOT / "apps/web-console/service-worker.js").read_text(encoding="utf-8")
|
service_worker = (ROOT / "apps/web-console/service-worker.js").read_text(encoding="utf-8")
|
||||||
assert (
|
assert (
|
||||||
'const CACHE_VERSION = "cognitive-agent-pad-v329-mode-preview"'
|
'const CACHE_VERSION = "cognitive-agent-pad-v334-video-media"'
|
||||||
in service_worker
|
in service_worker
|
||||||
)
|
)
|
||||||
assert '"/assistant-chat.css?v=codex-assistant-30"' 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():
|
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 'count.className = "assistant-dispatched-controls-count"' in script
|
||||||
assert 'list.className = "assistant-dispatched-controls-list"' in script
|
assert 'list.className = "assistant-dispatched-controls-list"' in script
|
||||||
assert "value.textContent = assistantControlValue(item)" 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 "实际控制指令:" in script
|
assert "实际控制指令:" in script
|
||||||
assert "appendDispatchedControls(message, event.payload)" 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 "appendDispatchedControls(message, modeExecutionPreviewResult(profile))" in script
|
||||||
assert "beginModeExecution," in script
|
assert "beginModeExecution," in script
|
||||||
assert "completeModeExecution," 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 "const operationByRef = new Map()" in script
|
||||||
assert 'deliveryState: "submitting"' in script
|
assert 'deliveryState: "submitting"' in script
|
||||||
assert 'deliveryLabel: "正在下发"' 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_height": "腿托"' in script
|
||||||
assert '"seat.set_legrest_extension": "腿托"' in script
|
assert '"seat.set_legrest_extension": "腿托"' in script
|
||||||
assert '"ambient_light.set_illumination": "氛围灯颜色与亮度"' 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 '.replaceAll("_", "-")' in script
|
||||||
assert ".assistant-dispatched-controls > summary" in css
|
assert ".assistant-dispatched-controls > summary" in css
|
||||||
assert ".assistant-dispatched-controls-list > li" 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
|
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():
|
def test_latest_dispatched_control_group_is_the_only_group_expanded():
|
||||||
assistant = (ROOT / "apps/web-console/assistant-chat.js").read_text(
|
assistant = (ROOT / "apps/web-console/assistant-chat.js").read_text(
|
||||||
encoding="utf-8"
|
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
|
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():
|
def test_starting_a_new_codex_conversation_rotates_the_conversation_id():
|
||||||
script = (ROOT / "apps/web-console/assistant-chat.js").read_text(encoding="utf-8")
|
script = (ROOT / "apps/web-console/assistant-chat.js").read_text(encoding="utf-8")
|
||||||
assert 'const CONVERSATION_STORAGE_KEY = "cognitive-agent.conversation-id"' in script
|
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",
|
"provider_used": "local_rules+infrared_local_rules",
|
||||||
"execution_mode": "dry_run",
|
"execution_mode": "dry_run",
|
||||||
"control_transport": "wired_can",
|
"control_transport": "wired_can",
|
||||||
|
"parallel_dispatch": {
|
||||||
|
"enabled": True,
|
||||||
|
"preflight_status": "succeeded",
|
||||||
|
"dispatch": {
|
||||||
|
"started_skew_ms": 1.2,
|
||||||
|
"total_ms": 120.5,
|
||||||
|
},
|
||||||
|
},
|
||||||
"function_configs": {
|
"function_configs": {
|
||||||
"native_climate": {
|
"native_climate": {
|
||||||
"spec": {
|
"spec": {
|
||||||
@@ -155,6 +163,10 @@ class ValidatedControlProjectionTest(unittest.TestCase):
|
|||||||
"compile",
|
"compile",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
projection["parallel_dispatch"]["preflight_status"],
|
||||||
|
"succeeded",
|
||||||
|
)
|
||||||
serialized = json.dumps(projection, ensure_ascii=False)
|
serialized = json.dumps(projection, ensure_ascii=False)
|
||||||
self.assertNotIn("command_plans", serialized)
|
self.assertNotIn("command_plans", serialized)
|
||||||
self.assertNotIn("data_hex", serialized)
|
self.assertNotIn("data_hex", serialized)
|
||||||
@@ -362,6 +374,10 @@ class ValidatedControlProjectionTest(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
self.assertNotIn("command_plans", safe_result)
|
self.assertNotIn("command_plans", safe_result)
|
||||||
self.assertNotIn("validated_control_commands", 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:
|
def test_agent_tool_path_queues_projection_for_the_browser_thread(self) -> None:
|
||||||
client = type("UnavailableClient", (), {"available": False})()
|
client = type("UnavailableClient", (), {"available": False})()
|
||||||
|
|||||||
@@ -724,6 +724,50 @@ class Can1SemanticWhitelistTests(unittest.TestCase):
|
|||||||
self.assertEqual(preview["unresolved_clauses"], [])
|
self.assertEqual(preview["unresolved_clauses"], [])
|
||||||
self.assertEqual(self._debug_commands(preview), [])
|
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:
|
def test_vehicle_position_aliases_stay_scoped_to_the_device_domain(self) -> None:
|
||||||
cases = (
|
cases = (
|
||||||
("打开左前车窗", ["window front-left open"]),
|
("打开左前车窗", ["window front-left open"]),
|
||||||
|
|||||||
@@ -523,6 +523,56 @@ def test_registered_mode_execution_uses_fast_local_route_without_codex(
|
|||||||
assert "模拟编排" in result["answer"]
|
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:
|
def test_registered_name_only_mode_keeps_the_fast_local_route_without_codex() -> None:
|
||||||
client = FakeCodexClient(available=False)
|
client = FakeCodexClient(available=False)
|
||||||
assistant = CodexAssistantService(client)
|
assistant = CodexAssistantService(client)
|
||||||
|
|||||||
@@ -554,6 +554,48 @@ class DriverSeatNativeCanTests(unittest.TestCase):
|
|||||||
self.assertEqual(bus.send_calls, 4)
|
self.assertEqual(bus.send_calls, 4)
|
||||||
self.assertEqual(len(bus.sent), 3)
|
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:
|
def test_memory_compile_rejects_missing_or_malformed_learned_slots(self) -> None:
|
||||||
with tempfile.TemporaryDirectory() as directory:
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
path = Path(directory) / "driver_memory.json"
|
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 f'"{asset}"' in service_worker
|
||||||
assert 'import "../energy-monitor.css";' in vite_entry
|
assert 'import "../energy-monitor.css";' in vite_entry
|
||||||
assert "/energy-monitor.js?v=energy-monitor-5-performance" in html
|
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"
|
"/energy-monitor.js?v=energy-monitor-5-performance"
|
||||||
)
|
)
|
||||||
assert (
|
assert (
|
||||||
'const CACHE_VERSION = "cognitive-agent-pad-v329-mode-preview"'
|
'const CACHE_VERSION = "cognitive-agent-pad-v334-video-media"'
|
||||||
in service_worker
|
in service_worker
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -157,6 +157,42 @@ def test_authorization_requires_exact_enabled_learned_bound_ability() -> None:
|
|||||||
authorize_extension_control_actions(parsed, disallowed)
|
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:
|
def test_consumer_validates_entire_request_then_executes_by_at_ms() -> None:
|
||||||
catalog = [
|
catalog = [
|
||||||
capability("light-on", 2, "打开"),
|
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 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()
|
status = consumer.snapshot()
|
||||||
assert status["processed_count"] == 1
|
assert status["processed_count"] == 1
|
||||||
assert status["executed_action_count"] == 2
|
assert status["executed_action_count"] == 3
|
||||||
assert [item["slot"] for item in status["last_results"]] == [3, 2]
|
assert [item["slot"] for item in status["last_results"]] == [2, 3, 2]
|
||||||
assert [item["slot"] for item in status["action_statuses"]] == [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"]] == [
|
assert [item["state"] for item in status["action_statuses"]] == [
|
||||||
"submitted_unverified",
|
"submitted_unverified",
|
||||||
"submitted_unverified",
|
"submitted_unverified",
|
||||||
|
"submitted_unverified",
|
||||||
]
|
]
|
||||||
assert all(
|
assert all(
|
||||||
item["state_label"] == "已下发 · 物理效果未确认"
|
item["state_label"] == "已下发 · 物理效果未确认"
|
||||||
|
|||||||
@@ -92,12 +92,12 @@ def test_performance_assets_have_a_new_cache_generation() -> None:
|
|||||||
worker = source("service-worker.js")
|
worker = source("service-worker.js")
|
||||||
|
|
||||||
for asset in (
|
for asset in (
|
||||||
"/app.js?v=dual-seat-145-n7-dispatch",
|
"/app.js?v=dual-seat-147-video-media",
|
||||||
"/console-v2.js?v=local-first-77-performance",
|
"/console-v2.js?v=local-first-79-cinema-status",
|
||||||
"/self-extension.js?v=nfc-mqtt-56",
|
"/self-extension.js?v=nfc-persistent-57",
|
||||||
"/energy-monitor.js?v=energy-monitor-5-performance",
|
"/energy-monitor.js?v=energy-monitor-5-performance",
|
||||||
):
|
):
|
||||||
assert asset in html
|
assert asset in html
|
||||||
assert f'"{asset}"' in worker
|
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
|
assert '"/codepen-glass-theme.css?v=creative-cloud-6-performance"' in worker
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ def test_theme_asset_versions_are_consistent_with_the_pwa_shell() -> None:
|
|||||||
assert source_asset in main
|
assert source_asset in main
|
||||||
assert f'"{cached_asset}"' in service_worker
|
assert f'"{cached_asset}"' in service_worker
|
||||||
assert 'import "./styles/design-system.css"' in main
|
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:
|
def test_reference_glass_theme_covers_the_complete_workspace() -> None:
|
||||||
|
|||||||
@@ -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:
|
def wait_until(predicate, timeout: float = 1.5) -> bool:
|
||||||
deadline = time.monotonic() + timeout
|
deadline = time.monotonic() + timeout
|
||||||
while time.monotonic() < deadline:
|
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
|
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(
|
def test_unknown_n7_action_is_forwarded_once_only_during_hardware_execution(
|
||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ def test_unified_console_exposes_device_id_based_nfc_authorization() -> None:
|
|||||||
vite_entry = source("src/main.js")
|
vite_entry = source("src/main.js")
|
||||||
|
|
||||||
assert 'data-open-nfc-binding' in script
|
assert 'data-open-nfc-binding' in script
|
||||||
assert '靠近读卡器后' in script
|
assert '请将 NFC 卡靠近读卡器' in script
|
||||||
assert '<h2>NFC 设备授权</h2>' in script
|
assert '<h2>NFC 设备授权</h2>' in script
|
||||||
assert 'NFC 设备授权' in script
|
assert 'NFC 设备授权' in script
|
||||||
assert 'api("nfc/status")' 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 '学习指令已下发,但物理槽位回读为空' in script
|
||||||
assert 'signalSource: "n7web"' in script
|
assert 'signalSource: "n7web"' in script
|
||||||
assert 'if (capability.signalSource === "n7web") return' in script
|
assert 'if (capability.signalSource === "n7web") return' in script
|
||||||
assert 'const activeCatalog = sensedCardUid' in script
|
assert 'const persistentCatalog = (' in script
|
||||||
assert 'normalizeCardUid(item.card_uid) === sensedCardUid' in script
|
assert 'durablePeripheralCatalogHydrated' in script
|
||||||
|
assert 'durablePeripheralCatalog' in script
|
||||||
|
assert 'normalizeCardUid(item.card_uid)' in script
|
||||||
assert '&& item.status === "learned"' in script
|
assert '&& item.status === "learned"' in script
|
||||||
assert 'slot: capability.slot' in script
|
assert 'slot: capability.slot' in script
|
||||||
assert 'device_aliases: device?.aliases || []' in script
|
assert 'device_aliases: device?.aliases || []' in script
|
||||||
assert 'async function hydratePeripheralCatalog()' in script
|
assert 'async function hydratePeripheralCatalog()' in script
|
||||||
assert 'api("peripherals")' in script
|
assert 'api("peripherals")' in script
|
||||||
assert 'signalSource: "backend_catalog"' in script
|
assert 'signalSource: "backend_catalog"' in script
|
||||||
assert '当前未感应到外设' in script
|
assert '当前没有已绑定外设' in script
|
||||||
assert '将已绑定设备 ID 的 NFC 卡靠近读卡器后显示命令' in script
|
assert '外设绑定 NFC 后将持续显示,解除绑定后移除' in script
|
||||||
|
|
||||||
assert '.v2-nfc-inline' in styles
|
assert '.v2-nfc-inline' in styles
|
||||||
assert '.v2-nfc-dialog' in styles
|
assert '.v2-nfc-dialog' in styles
|
||||||
assert '.v2-nfc-command-list' in styles
|
assert '.v2-nfc-command-list' in styles
|
||||||
assert '.v2-nfc-command-option' in styles
|
assert '.v2-nfc-command-option' in styles
|
||||||
assert '.v2-nfc-select-all' 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
|
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 "下发控制指令" in console
|
||||||
assert "已解析控制指令" not in console
|
assert "已解析控制指令" not in console
|
||||||
assert 'li[data-can-channel="IR"]' in styles
|
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
|
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,
|
"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(
|
sample(
|
||||||
{
|
{
|
||||||
"id": "climate-post-submit-failed",
|
"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] == [
|
assert [item["deliveryState"] for item in commands] == [
|
||||||
@@ -977,6 +994,7 @@ def test_delivery_status_distinguishes_blocked_failed_aborted_and_verified() ->
|
|||||||
"failed",
|
"failed",
|
||||||
"aborted",
|
"aborted",
|
||||||
"submitted",
|
"submitted",
|
||||||
|
"submitted",
|
||||||
"failed",
|
"failed",
|
||||||
]
|
]
|
||||||
assert "0x388 实时位置" in commands[0]["deliveryDetail"]
|
assert "0x388 实时位置" in commands[0]["deliveryDetail"]
|
||||||
|
|||||||
@@ -1406,6 +1406,15 @@ class SelfExtensionTests(unittest.TestCase):
|
|||||||
def test_main_peripheral_sequence_executes_validated_actions_and_delays(self) -> None:
|
def test_main_peripheral_sequence_executes_validated_actions_and_delays(self) -> None:
|
||||||
service = SelfExtensionService(SimulationGateway())
|
service = SelfExtensionService(SimulationGateway())
|
||||||
catalog = [
|
catalog = [
|
||||||
|
{
|
||||||
|
"id": "on",
|
||||||
|
"status": "learned",
|
||||||
|
"device_id": "third-party-light",
|
||||||
|
"device_name": "第三方氛围灯",
|
||||||
|
"card_uid": "1A53DD95",
|
||||||
|
"action": "打开灯光",
|
||||||
|
"slot": 1,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "red",
|
"id": "red",
|
||||||
"status": "learned",
|
"status": "learned",
|
||||||
@@ -1457,16 +1466,24 @@ class SelfExtensionTests(unittest.TestCase):
|
|||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
result["orchestration_route"], "nfc_peripheral_sequence"
|
result["orchestration_route"], "nfc_peripheral_sequence"
|
||||||
)
|
)
|
||||||
self.assertEqual(result["sequence"]["step_count"], 3)
|
self.assertEqual(result["sequence"]["step_count"], 4)
|
||||||
self.assertEqual(result["sequence"]["action_count"], 2)
|
self.assertEqual(result["sequence"]["action_count"], 3)
|
||||||
self.assertEqual(result["sequence"]["emit_count"], 2)
|
self.assertEqual(result["sequence"]["emit_count"], 3)
|
||||||
self.assertEqual(result["sequence"]["total_delay_ms"], 5000)
|
self.assertEqual(result["sequence"]["total_delay_ms"], 5000)
|
||||||
self.assertEqual(result["sequence"]["steady_blink_fallbacks"], 1)
|
self.assertEqual(result["sequence"]["steady_blink_fallbacks"], 1)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
[item["status"] for item in result["operation_results"]],
|
[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(5.0)
|
||||||
|
sleep.assert_any_call(0.8)
|
||||||
self.assertNotIn(0.5, [call.args[0] for call in sleep.call_args_list])
|
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:
|
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:
|
def test_color_sequence_never_cycles_the_power_off_capability(self) -> None:
|
||||||
service = SelfExtensionService(SimulationGateway())
|
service = SelfExtensionService(SimulationGateway())
|
||||||
catalog = [
|
catalog = [
|
||||||
|
|||||||
@@ -37,11 +37,29 @@ def test_peripheral_catalog_sync_is_serialized_and_tracks_backend_revision() ->
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert "peripheralCatalogSyncQueue" in sync
|
assert "peripheralCatalogSyncQueue" in sync
|
||||||
|
assert "const nextCatalog = peripheralCatalog();" in sync
|
||||||
assert "catalog_revision: peripheralCatalogRevision" in sync
|
assert "catalog_revision: peripheralCatalogRevision" in sync
|
||||||
assert "result.catalog_revision" in sync
|
assert "result.catalog_revision" in sync
|
||||||
|
assert "applyDurablePeripheralCatalog(" in sync
|
||||||
assert "if (throwOnError) throw error;" 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:
|
def test_bound_card_can_be_reassigned_to_a_new_device() -> None:
|
||||||
source = SOURCE_PATH.read_text(encoding="utf-8")
|
source = SOURCE_PATH.read_text(encoding="utf-8")
|
||||||
render = _function_source(
|
render = _function_source(
|
||||||
|
|||||||
@@ -429,23 +429,26 @@ class SpeechCapabilityAlignmentTests(unittest.TestCase):
|
|||||||
result = self.processor.process("打开座椅通风并调到三档")
|
result = self.processor.process("打开座椅通风并调到三档")
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
[command.capability for command in result.commands],
|
[command.capability for command in result.commands],
|
||||||
[
|
["seat.set_driver_ventilation_native"],
|
||||||
"seat.set_driver_ventilation_native",
|
|
||||||
"seat.set_driver_ventilation_native",
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
[command.target_scope for command in result.commands],
|
[command.target_scope for command in result.commands],
|
||||||
["driver", "driver"],
|
["driver"],
|
||||||
)
|
)
|
||||||
self.assertIn("3档", result.clean_text)
|
self.assertIn("3档", result.clean_text)
|
||||||
self.assertEqual(result.semantic_status, "recognized_unsupported")
|
self.assertEqual(result.commands[0].parameters, {"state": "on", "level": 1})
|
||||||
self.assertFalse(result.execution_eligible)
|
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("打开副驾座椅通风并调到三档")
|
passenger = self.processor.process("打开副驾座椅通风并调到三档")
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
[command.target_scope for command in passenger.commands],
|
[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)
|
self.assertTrue(passenger.execution_eligible)
|
||||||
|
|
||||||
@@ -489,7 +492,11 @@ class SpeechCapabilityAlignmentTests(unittest.TestCase):
|
|||||||
self.assertNotIn("设置为", result.clean_text)
|
self.assertNotIn("设置为", result.clean_text)
|
||||||
|
|
||||||
explicit_level = self.processor.process("主驾座椅通风开到一档")
|
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:
|
def test_discourse_connectors_preserve_actions_without_leaking_scope(self) -> None:
|
||||||
cases = (
|
cases = (
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ def test_vue_vite_application_is_the_primary_frontend_entry() -> None:
|
|||||||
assert package["scripts"]["build"] == "vite build"
|
assert package["scripts"]["build"] == "vite build"
|
||||||
assert 'import vue from "@vitejs/plugin-vue"' in vite
|
assert 'import vue from "@vitejs/plugin-vue"' in vite
|
||||||
assert 'outDir: outputRoot' 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 'createHash("sha256")' in vite
|
||||||
assert 'runtime-assets.json' in vite
|
assert 'runtime-assets.json' in vite
|
||||||
assert 'rewriteRuntimeReferences' in vite
|
assert 'rewriteRuntimeReferences' in vite
|
||||||
|
|||||||
Reference in New Issue
Block a user