feat: improve mode and peripheral orchestration

This commit is contained in:
Miayui
2026-09-21 00:13:47 +08:00
parent 89707a42e2
commit 448c8b31a0
39 changed files with 3068 additions and 277 deletions
+19 -1
View File
@@ -17,6 +17,8 @@ function createConversationId() {
return createOpaqueId("conv");
}
const HARDWARE_CLIENT_ID_PATTERN = /^hwc-[a-z0-9-]{8,80}$/;
function loadConversationId() {
const stored = sessionStorage.getItem("cognitive-agent.conversation-id");
// A browser runtime restart is a new virtual run. Do not reuse the old
@@ -33,12 +35,19 @@ function loadConversationId() {
function loadHardwareClientId() {
const key = "cognitive-agent.hardware-client-id";
const stored = localStorage.getItem(key);
if (/^hwc-[a-z0-9-]{8,80}$/.test(stored || "")) return stored;
if (HARDWARE_CLIENT_ID_PATTERN.test(stored || "")) return stored;
const clientId = createOpaqueId("hwc");
localStorage.setItem(key, clientId);
return clientId;
}
function validatedHardwareClientId() {
if (!HARDWARE_CLIENT_ID_PATTERN.test(state.hardwareClientId || "")) {
state.hardwareClientId = loadHardwareClientId();
}
return state.hardwareClientId;
}
function loadControlTransport() {
const stored = localStorage.getItem("cognitive-agent.control-transport");
return ["wired_can", "wireless_mqtt"].includes(stored) ? stored : "wired_can";
@@ -5131,6 +5140,7 @@ const MODE_DOMAIN_LABELS = {
window: "车窗",
front_light: "车灯",
vehicle_status: "车辆状态",
android: "车机媒体",
};
const MODE_TARGET_LABELS = {
@@ -5140,6 +5150,7 @@ const MODE_TARGET_LABELS = {
front_right: "右前 / 副驾",
rear_left: "左后",
rear_right: "右后",
headunit: "车机",
front_row: "前排",
rear_row: "后排",
vehicle: "全车",
@@ -8241,6 +8252,7 @@ const CONFIG_DOMAIN_META = {
window: { label: "车窗", icon: "panels-top-left", className: "generic" },
outlet: { label: "电动风口", icon: "wind", className: "climate" },
interior_light: { label: "车内灯", icon: "lightbulb", className: "ambient" },
android: { label: "车机媒体", icon: "clapperboard", className: "generic" },
};
const CONFIG_CAPABILITY_LABELS = {
@@ -8286,6 +8298,7 @@ const CONFIG_CAPABILITY_LABELS = {
"roof.set_shade_position": "设置遮阳帘位置",
"roof.set_shade_motion": "控制遮阳帘动作",
"roof.set_automatic_dimming": "设置自动调光",
"android.launch_app": "打开视频媒体",
};
const CONFIG_PARAMETER_LABELS = {
@@ -8309,6 +8322,8 @@ const CONFIG_PARAMETER_LABELS = {
outlet_mode: "出风模式",
mode: "工作模式",
mode_id: "固件模式编号",
package_name: "应用包名",
activity_name: "启动页面",
};
function makeConfigElement(tagName, className, textValue) {
@@ -9412,6 +9427,9 @@ window.CognitiveAgentConsole = {
getExecutionMode() {
return state.executionMode;
},
getHardwareClientId() {
return validatedHardwareClientId();
},
getConversationId() {
return state.conversationId;
},
+51 -6
View File
@@ -548,6 +548,7 @@
"door.set_rear_lock": "后排门锁",
"door.set_vehicle_lock": "整车门锁",
"infrared.emit": "红外外设",
"android.launch_app": "打开视频媒体",
};
function assistantControlCategory(operation = {}, fallback = "控制指令") {
@@ -745,6 +746,22 @@
const brightness = numericValue(parameters.brightness_level);
if (brightness !== null) return `${brightness}级`;
const level = numericValue(parameters.level ?? parameters.fan_level);
const capability = String(item.operation?.capability || "");
if (capability === "android.launch_app") {
const packageName = String(parameters.package_name || "").trim();
const activityName = String(parameters.activity_name || "").trim();
return [
packageName ? `应用 ${packageName}` : "视频应用",
activityName ? `页面 ${activityName}` : "",
].filter(Boolean).join(" · ");
}
if (
level !== null
&& ["seat.set_driver_ventilation_native", "seat.set_driver_heating_native"]
.includes(capability)
) {
return ({ 1: "3档", 2: "1档" })[level] || "档位待确认";
}
if (level !== null) return `${level}档`;
const state = parameters.motion
?? parameters.state
@@ -769,6 +786,7 @@
if (capability.startsWith("seat.") || category.includes("座椅")) return "armchair";
if (capability.startsWith("door.") || category.includes("门锁")) return "lock-keyhole";
if (capability === "infrared.emit") return "radio-tower";
if (capability === "android.launch_app") return "clapperboard";
return "binary";
}
@@ -985,6 +1003,7 @@
"ambient_light.set_power": "氛围灯",
"ambient_light.set_mode": "氛围灯模式",
"infrared.emit": "红外外设",
"android.launch_app": "打开视频媒体",
};
const zeroGravityIndexes = actions
@@ -1166,9 +1185,10 @@
? `${params.position_pct}%${params.angle_deg != null ? `(${params.angle_deg}°)` : ""}`
: motionLabel(params.state || params.mode_id);
if (["seat.set_driver_ventilation_native", "seat.set_driver_heating_native"].includes(capability)) {
// CH1 driver controls encode high as 1 and low as 2; passenger
// comfort levels use a different scale and must not reuse this map.
const level = { 1: "高档", 2: "低档" }[params.level ?? 2] || "档位待确认";
// CH1 encodes high as 1 and low as 2 internally. Present the
// occupant-facing three-speed scale instead of leaking CAN enums.
const level = { 1: "3档(高)", 2: "1档(低)" }[params.level ?? 2]
|| "档位待确认";
value = params.state === "off" ? "关闭"
: params.state === "on" ? `开启 · ${level}` : "开关状态待确认";
} else if (["seat.set_ventilation", "seat.set_heating"].includes(capability)) {
@@ -1529,10 +1549,23 @@
return /(?:构建|创建|新建|新增|生成|添加)(?:一下)?(?:一个|个|一套|套)?[^,。!?!?]{1,64}?模式/.test(compact);
}
function validatedHardwareClientId() {
const sharedClientId = window.CognitiveAgentConsole?.getHardwareClientId?.();
let storedClientId = null;
try {
storedClientId = localStorage.getItem("cognitive-agent.hardware-client-id");
} catch (_error) {
// The shared console identity remains authoritative when storage is unavailable.
}
return [sharedClientId, storedClientId]
.map((value) => String(value || "").trim())
.find((value) => /^hwc-[a-z0-9-]{8,80}$/.test(value)) || null;
}
function requestBody(message) {
const configuredExecutionMode = window.CognitiveAgentConsole?.getExecutionMode?.() || "dry_run";
const controlTransport = localStorage.getItem("cognitive-agent.control-transport") || "wired_can";
const hardwareClientId = localStorage.getItem("cognitive-agent.hardware-client-id");
const hardwareClientId = validatedHardwareClientId();
const vehicleId = $("#vehicleSelect")?.value || null;
const modeProfileDraftContext = requestModeDraftContext(message);
const modeConfigurationPreview = Boolean(modeProfileDraftContext)
@@ -1967,6 +2000,11 @@
&& result?.schedule_execution?.status === "running";
const dryRun = result?.execution_mode === "dry_run";
const waitingForFeedback = result?.execution_outcome === "submitted_unverified";
const delegatedCount = Array.isArray(result?.deferred_requests)
? result.deferred_requests.filter(
(item) => item?.execution_backend === "n7_vehicle_transfer",
).length
: 0;
let content = `${modeName}执行未完成,请查看控制指令状态。`;
let statusText = "功能模式执行未完成";
@@ -1991,8 +2029,15 @@
: "功能模式执行已完成";
statusState = "success";
} else if (status === "partially_succeeded") {
content = `${modeName}仅部分完成,请查看未完成的控制指令。`;
statusText = "功能模式部分完成";
content = delegatedCount > 0
? `${modeName}指令已提交,未由 PC 执行的 ${delegatedCount} 项动作将由车机继续处理。`
: waitingForFeedback
? `${modeName}指令已提交,正在等待车辆状态确认。`
: `${modeName}执行结果已更新,请查看各控制指令状态。`;
statusText = waitingForFeedback || delegatedCount > 0
? "功能模式指令已提交"
: "功能模式执行结果已更新";
statusState = waitingForFeedback || delegatedCount > 0 ? "working" : "success";
}
setMessageContent(message, content, { final: true });
+14 -8
View File
@@ -571,9 +571,20 @@
const explicitlyBlocked = executionStatus === "blocked"
|| verificationStatus === "blocked"
|| Boolean(telemetry.authorization_reason);
const executionFailed = executionStatus === "failed"
// Closed-loop evidence is authoritative. Some MQTT window executors
// retain a legacy `failed` operation status even after fresh 0x388
// feedback confirms the requested target; do not render that as a
// failed dispatch once the physical result is verified.
const verified = !aborted && (
verificationStatus === "verified_success"
|| telemetry.physical_effect_confirmed === true
|| telemetry.feedback_match === true
);
const executionFailed = !verified && (
executionStatus === "failed"
|| explicitlyBlocked
|| verificationStatus === "failed_or_blocked";
|| verificationStatus === "failed_or_blocked"
);
const missingWindowFeedback = /0x388/i.test(failureMessage) && (
/no fresh valid/i.test(failureMessage)
|| /未收到.*(?:有效|新鲜).*车窗位置/.test(failureMessage)
@@ -591,11 +602,6 @@
|| /thermal protection|热保护/i.test(failureMessage)
);
const dispatchFailed = executionFailed && !submitted && !safetyBlocked;
const verified = !executionFailed && !aborted && (
verificationStatus === "verified_success"
|| telemetry.physical_effect_confirmed === true
|| telemetry.feedback_match === true
);
const deliveryState = aborted
? "aborted"
: safetyBlocked
@@ -1325,7 +1331,7 @@
}
const levelOptions = [
["off", "关闭"], ["high", "1 档(高)"], ["low", "2 档(低)"],
["off", "关闭"], ["low", "1 档(低)"], ["high", "3 档(高)"],
];
const toggleOptions = [["off", "关闭"], ["on", "开启"]];
const massageModeOptions = [
+4 -4
View File
@@ -39,13 +39,13 @@
<script src="/vendor/three/0.147.0/fflate.min.js" defer></script>
<script src="/vendor/three/0.147.0/FBXLoader.js" defer></script>
<script src="/network.js?v=android-pad-2" defer></script>
<script src="/app.js?v=dual-seat-145-n7-dispatch" defer></script>
<script src="/app.js?v=dual-seat-147-video-media" defer></script>
<script src="/speech-input.js?v=sensevoice-13" defer></script>
<script type="module" src="/gesture-control.js?v=android-pad-9"></script>
<script src="/pwa.js?v=android-pad-3" defer></script>
<script src="/console-v2.js?v=local-first-77-performance" defer></script>
<script src="/self-extension.js?v=nfc-mqtt-56" defer></script>
<script src="/assistant-chat.js?v=codex-assistant-35" defer></script>
<script src="/console-v2.js?v=local-first-79-cinema-status" defer></script>
<script src="/self-extension.js?v=nfc-persistent-57" defer></script>
<script src="/assistant-chat.js?v=codex-assistant-39-video-media" defer></script>
<script src="/energy-monitor.js?v=energy-monitor-5-performance" defer></script>
<script src="/firmware-workbench-entry.js?v=3" defer></script>
</head>
+83 -19
View File
@@ -382,6 +382,8 @@
};
let peripheralCatalogRevision = null;
let peripheralCatalogSyncQueue = Promise.resolve();
let durablePeripheralCatalog = [];
let durablePeripheralCatalogHydrated = false;
let petCardSnapshot = {
revision: -1,
cards: [],
@@ -1973,11 +1975,40 @@
}).filter((item) => item.device_id && item.card_uid && validSlot(item.slot));
}
function applyDurablePeripheralCatalog(
catalog,
revision = null,
{ render = true } = {},
) {
const nextCatalog = (Array.isArray(catalog) ? catalog : [])
.filter((item) => item && typeof item === "object"
&& item.id && item.device_id && normalizeCardUid(item.card_uid)
&& validSlot(item.slot))
.map((item) => ({ ...item, card_uid: normalizeCardUid(item.card_uid) }));
const changed = JSON.stringify(durablePeripheralCatalog) !== JSON.stringify(nextCatalog)
|| !durablePeripheralCatalogHydrated;
durablePeripheralCatalog = nextCatalog;
durablePeripheralCatalogHydrated = true;
if (revision != null && String(revision).trim()) {
peripheralCatalogRevision = String(revision).trim();
}
if (changed) unifiedExtensionDirty = true;
if (render && changed) renderUnifiedExtensionView?.({ force: true });
return changed;
}
async function hydratePeripheralCatalog() {
const payload = await api("peripherals");
peripheralCatalogRevision = payload.catalog_revision || peripheralCatalogRevision;
const catalog = Array.isArray(payload.catalog) ? payload.catalog : [];
if (!catalog.length) return 0;
applyDurablePeripheralCatalog(
catalog,
payload.catalog_revision,
{ render: false },
);
if (!catalog.length) {
renderAll();
return 0;
}
const restoredDeviceIds = new Set();
catalog.forEach((item) => {
if (!item || typeof item !== "object" || !item.id || !validSlot(item.slot)) return;
@@ -3806,12 +3837,13 @@
}
async function syncPeripheralCatalog(device = null, { throwOnError = false } = {}) {
const nextCatalog = peripheralCatalog();
const operation = peripheralCatalogSyncQueue
.catch(() => null)
.then(() => api("display-sync", {
method: "POST",
body: JSON.stringify({
peripherals: peripheralCatalog(),
peripherals: nextCatalog,
device,
catalog_revision: peripheralCatalogRevision,
}),
@@ -3819,7 +3851,10 @@
peripheralCatalogSyncQueue = operation.catch(() => null);
try {
const result = await operation;
peripheralCatalogRevision = result.catalog_revision || peripheralCatalogRevision;
applyDurablePeripheralCatalog(
nextCatalog,
result.catalog_revision,
);
return result;
} catch (error) {
console.warn("外设能力同步失败", error);
@@ -4169,6 +4204,18 @@
status: item.status,
enabled: item.enabled,
})),
durableCatalogHydrated: durablePeripheralCatalogHydrated,
durableCatalog: durablePeripheralCatalog.map((item) => ({
id: item.id,
deviceId: item.device_id,
deviceName: item.device_name,
deviceType: item.device_type,
typeLabel: item.type_label,
action: item.action,
cardUid: item.card_uid,
status: item.status,
enabled: item.enabled,
})),
lastAction: snapshot?.extension?.last_action ? {
command: snapshot.extension.last_action.command,
type: snapshot.extension.last_action.type,
@@ -4249,27 +4296,28 @@
const sharedCatalog = Array.isArray(snapshot?.extension?.catalog)
? snapshot.extension.catalog
: [];
const mergedCatalog = [...sharedCatalog, ...peripheralCatalog()]
const fallbackCatalog = [...sharedCatalog, ...peripheralCatalog()]
.filter((item, index, catalog) => {
const identity = item.id || `${item.device_id}:${item.action}`;
return catalog.findIndex((candidate) => (
candidate.id || `${candidate.device_id}:${candidate.action}`
) === identity) === index;
});
const persistentCatalog = (
durablePeripheralCatalogHydrated
? durablePeripheralCatalog
: fallbackCatalog
).filter((item) => (
normalizeCardUid(item.card_uid)
&& item.status === "learned"
));
const nfcReader = nfcSnapshot.reader || {};
const nfcCard = nfcSnapshot.card;
const sensedCardUid = normalizeCardUid(nfcCard?.uid);
const nfcDevice = linkedDeviceForCard(sensedCardUid);
const nfcPet = linkedPetForCard(sensedCardUid);
const activeCatalog = sensedCardUid
? mergedCatalog.filter((item) => (
normalizeCardUid(item.card_uid) === sensedCardUid
&& item.status === "learned"
&& item.enabled !== false
))
: [];
const grouped = new Map();
activeCatalog.forEach((item) => {
persistentCatalog.forEach((item) => {
const deviceId = item.device_id || "external-device";
if (!grouped.has(deviceId)) grouped.set(deviceId, {
id: deviceId,
@@ -4278,7 +4326,10 @@
cardUid: item.card_uid || null,
actions: [],
});
grouped.get(deviceId).actions.push(item.action || item.name || "已学习能力");
const actionName = item.action || item.name || "已学习能力";
grouped.get(deviceId).actions.push(
item.enabled === false ? `${actionName}(已停用)` : actionName,
);
});
const devices = [...grouped.values()];
const lastAction = snapshot?.extension?.last_action;
@@ -4294,8 +4345,10 @@
: petCardSnapshot.publisher?.state === "burst_failed"
? "宠物消息发送失败"
: "已感应宠物 NFC"
: devices.length
: nfcDevice
? "已感应 NFC 外设"
: devices.length
? `${devices.length} 个外设已绑定`
: sensedCardUid
? "当前 NFC 暂无已授权命令"
: nfcReader.online
@@ -4361,11 +4414,11 @@
<div>${device.actions.slice(0, 8).map((action) => `<span>${escapeHtml(action)}</span>`).join("")}</div>
</article>`).join("") : nfcPet ? "" : `
<div class="v2-extension-empty">
<i data-lucide="${sensedCardUid ? "badge-alert" : "scan-line"}"></i><div><strong>${sensedCardUid ? "当前 NFC 暂无已授权命令" : "当前未感应到外设"}</strong><span>${sensedCardUid ? "请检查设备 ID 绑定及命令复选框" : "将已绑定设备 ID 的 NFC 卡靠近读卡器后显示命令"}</span></div>
<i data-lucide="link-2-off"></i><div><strong>当前没有已绑定外设</strong><span>外设绑定 NFC 后将持续显示,解除绑定后移除</span></div>
</div>`}
</div>
<footer class="v2-extension-linkage" data-active="${String(Boolean(visibleLastAction))}">
<i data-lucide="workflow"></i><div><small>最近联动</small><strong>${escapeHtml(visibleLastAction?.command || visibleLastAction?.type || "等待 NFC 外设感应")}</strong></div>
<i data-lucide="workflow"></i><div><small>最近联动</small><strong>${escapeHtml(visibleLastAction?.command || visibleLastAction?.type || "暂无外设联动")}</strong></div>
<span>${escapeHtml(visibleLastAction ? (lastResult?.status || lastResult?.message || "STANDBY") : "STANDBY")}</span>
</footer>`;
unifiedExtensionFingerprint = renderFingerprint;
@@ -4377,6 +4430,17 @@
window.addEventListener("cognitive-agent:display-sync", (event) => {
snapshot = event.detail || null;
const extensionSnapshot = snapshot?.extension;
if (
Array.isArray(extensionSnapshot?.catalog)
&& extensionSnapshot?.result?.catalog_revision
) {
applyDurablePeripheralCatalog(
extensionSnapshot.catalog,
extensionSnapshot.result.catalog_revision,
{ render: false },
);
}
if (snapshot?.extension_health?.nfc) {
applyNfcSnapshot(snapshot.extension_health.nfc, { announce: false });
}
@@ -4402,8 +4466,8 @@
void hydratePeripheralCatalog()
.catch((error) => {
console.warn("恢复后端红外能力目录失败,将保留浏览器本地目录。", error);
})
.finally(() => syncPeripheralCatalog());
return syncPeripheralCatalog();
});
return true;
}
+5 -5
View File
@@ -1,4 +1,4 @@
const CACHE_VERSION = "cognitive-agent-pad-v329-mode-preview";
const CACHE_VERSION = "cognitive-agent-pad-v334-video-media";
const SHELL_CACHE = `${CACHE_VERSION}-shell`;
const RUNTIME_CACHE = `${CACHE_VERSION}-runtime`;
const APP_SHELL = [
@@ -6,18 +6,18 @@ const APP_SHELL = [
"/index.html",
"/styles.css?v=dual-seat-24-dialog-size",
"/network.js?v=android-pad-2",
"/app.js?v=dual-seat-145-n7-dispatch",
"/app.js?v=dual-seat-147-video-media",
"/gesture-control.js?v=android-pad-9",
"/pwa.js?v=android-pad-3",
"/console-v2.css?v=delivery-state-69",
"/console-v2.js?v=local-first-77-performance",
"/console-v2.js?v=local-first-79-cinema-status",
"/self-extension.css?v=pet-settings-54-dialog-size",
"/self-extension.js?v=nfc-mqtt-56",
"/self-extension.js?v=nfc-persistent-57",
"/speech-input.css?v=sensevoice-7",
"/speech-input.js?v=sensevoice-13",
"/speech-audio-worklet.js?v=sensevoice-5",
"/assistant-chat.css?v=codex-assistant-30",
"/assistant-chat.js?v=codex-assistant-35",
"/assistant-chat.js?v=codex-assistant-39-video-media",
"/energy-monitor.css?v=energy-monitor-5-dialog-size",
"/energy-monitor.js?v=energy-monitor-5-performance",
"/ui-consistency.css?v=spatial-ui-8",