(function () { "use strict"; const STORAGE_KEY = "cognitive-agent.ir-toy-console.v5"; const MODE_KEY = "cognitive-agent.extension-mode"; const CONVERSATION_KEY = "cognitive-agent.conversation-id"; const SLOT_MIN = 1; const SLOT_MAX = 248; const ACTION_PAGE_SIZE = 6; const HISTORY_LIMIT = 40; const MAX_PLAN_STEPS = 12; const MIN_PLAN_DELAY_MS = 1; const MAX_PLAN_DELAY_MS = 30000; const MAX_TOTAL_PLAN_DELAY_MS = 60000; const HEALTH_POLL_INTERVAL_MS = 3000; const HEALTH_REQUEST_TIMEOUT_MS = 5000; const $ = (selector, root = document) => root.querySelector(selector); const $$ = (selector, root = document) => [...root.querySelectorAll(selector)]; function preset(key, name, aliases, category = "custom", risk = "low", visual = null) { return { key, name, aliases, category, risk, visual }; } const TOY_TYPES = { light: { label: "灯光类玩具", shortLabel: "灯光", icon: "lightbulb", color: "#eab858", description: "颜色、亮灭与闪烁", presets: [ preset("on", "开启", ["打开", "开灯"], "power"), preset("off", "关闭", ["关掉", "关灯", "熄灭"], "power"), preset("red", "红色灯光", ["红灯", "变红", "红色"], "color", "low", "#ef6267"), preset("green", "绿色灯光", ["绿灯", "变绿", "绿色"], "color", "low", "#48b982"), preset("blue", "蓝色灯光", ["蓝灯", "变蓝", "蓝色"], "color", "low", "#4f7fd2"), preset("white", "白色灯光", ["白灯", "白色"], "color", "low", "#d9d5c8"), preset("fast_flash", "快速闪烁", ["快闪", "快速闪灯"], "effect"), preset("slow_flash", "慢速闪烁", ["慢闪", "慢速闪灯"], "effect"), preset("warm", "暖色灯光", ["暖光", "暖色"], "color", "low", "#f0a958"), preset("brightness_up", "亮度增加", ["调亮", "亮一点"], "effect"), preset("brightness_down", "亮度降低", ["调暗", "暗一点"], "effect"), preset("breath", "呼吸灯效", ["呼吸灯", "呼吸闪烁"], "effect"), ], }, sound: { label: "声音类玩具", shortLabel: "声音", icon: "volume-2", color: "#658bd5", description: "播放、音量与切换", presets: [ preset("on", "开启", ["打开", "启动"], "power"), preset("off", "关闭", ["关掉", "停止播放"], "power"), preset("play_pause", "播放 / 暂停", ["播放", "暂停"], "sound"), preset("volume_up", "音量加", ["大声一点", "调高音量"], "sound"), preset("volume_down", "音量减", ["小声一点", "调低音量"], "sound"), preset("next", "下一首", ["下一个声音"], "sound"), preset("previous", "上一首", ["上一个声音"], "sound"), preset("mode", "模式切换", ["换个模式"], "effect"), preset("mute", "静音", ["不要声音", "关闭声音"], "sound"), preset("loop", "循环播放", ["单曲循环", "循环"], "sound"), preset("random", "随机播放", ["随机切换"], "sound"), preset("stop", "停止播放", ["停止", "停止声音"], "sound"), ], }, action: { label: "动作类玩具", shortLabel: "动作", icon: "car-front", color: "#db7468", description: "移动、转向与表演", presets: [ preset("on", "开启", ["打开", "启动"], "power"), preset("off", "关闭", ["关掉", "关闭玩具"], "power"), preset("forward", "前进", ["向前", "往前走"], "motion", "high"), preset("backward", "后退", ["向后", "倒退"], "motion", "high"), preset("left", "左转", ["向左", "左拐"], "direction", "high"), preset("right", "右转", ["向右", "右拐"], "direction", "high"), preset("stop", "停止", ["停下", "不要动"], "motion"), preset("perform", "表演动作", ["表演", "做动作"], "effect", "medium"), preset("speed_up", "速度增加", ["加速", "快一点"], "motion", "high"), preset("speed_down", "速度降低", ["减速", "慢一点"], "motion"), preset("spin", "原地旋转", ["转圈", "旋转"], "direction", "medium"), preset("dance", "舞蹈动作", ["跳舞", "舞蹈"], "effect", "medium"), ], }, doll: { label: "玩偶类玩具", shortLabel: "玩偶", icon: "bot", color: "#cb86a4", description: "语音、舞蹈与互动", presets: [ preset("on", "开启", ["打开", "唤醒"], "power"), preset("off", "关闭", ["关掉", "休息"], "power"), preset("speak", "说话", ["讲句话", "发声"], "sound"), preset("sing", "唱歌", ["唱一首歌"], "sound"), preset("dance", "跳舞", ["舞蹈"], "effect", "medium"), preset("wave", "挥手", ["招手"], "motion"), preset("interact", "互动", ["陪我玩", "互动一下"], "effect"), preset("stop", "停止", ["停下", "停止动作"], "motion"), preset("laugh", "笑声", ["笑一下", "开心"], "sound"), preset("story", "讲故事", ["故事", "讲个故事"], "sound"), preset("repeat", "复读", ["重复说", "跟我说"], "sound"), preset("sleep", "睡眠模式", ["睡觉", "休眠"], "effect"), ], }, mechanical: { label: "机械类玩具", shortLabel: "机械", icon: "cog", color: "#758e94", description: "升降、伸缩与旋转", presets: [ preset("on", "开启", ["打开", "启动"], "power"), preset("off", "关闭", ["关掉", "停止设备"], "power"), preset("up", "上升", ["升高", "向上"], "motion", "medium"), preset("down", "下降", ["降低", "向下"], "motion", "medium"), preset("extend", "伸出", ["向外伸", "展开"], "motion", "medium"), preset("retract", "收回", ["缩回", "收起"], "motion", "medium"), preset("clockwise", "正转", ["顺时针", "正向旋转"], "direction", "medium"), preset("counterclockwise", "反转", ["逆时针", "反向旋转"], "direction", "medium"), preset("stop", "停止", ["停下", "急停"], "motion"), preset("speed_up", "速度增加", ["加速", "快一点"], "motion", "medium"), preset("speed_down", "速度降低", ["减速", "慢一点"], "motion"), preset("home", "复位", ["回到原位", "归位"], "motion", "medium"), ], }, other: { label: "其他玩具", shortLabel: "其他", icon: "gamepad-2", color: "#579a88", description: "通用自定义功能", presets: [ preset("on", "开启", ["打开", "启动"], "power"), preset("off", "关闭", ["关掉", "停止"], "power"), preset("function_1", "功能一", ["第一个功能"]), preset("function_2", "功能二", ["第二个功能"]), preset("function_3", "功能三", ["第三个功能"]), preset("function_4", "功能四", ["第四个功能"]), preset("function_5", "功能五", ["第五个功能"]), preset("function_6", "功能六", ["第六个功能"]), preset("function_7", "功能七", ["第七个功能"]), preset("function_8", "功能八", ["第八个功能"]), preset("mode", "模式切换", ["换个模式"]), preset("reset", "复位", ["恢复", "回到初始状态"]), ], }, }; const steppedActions = (values, makeAction) => values.map(makeAction); const percentSteps = [0, 25, 50, 75, 100]; const VEHICLE_DEVICES = [ { id: "passenger-seat", name: "副驾座椅", code: "SEAT · CH0 T2G CAN", channel: "CH0", icon: "armchair", parameters: ["通风 0–3 档", "加热 0–2 档", "水平 0–100%", "坐盆 0–100%", "靠背 95–163°", "腿托 95–167°"], actions: [ ...steppedActions([1, 2, 3], (level) => ({ name: `副驾通风 ${level} 档`, command: `副驾座椅通风${level}档` })), { name: "关闭副驾通风", command: "关闭副驾座椅通风" }, ...steppedActions([1, 2], (level) => ({ name: `副驾加热 ${level} 档`, command: `副驾座椅加热${level}档` })), { name: "关闭副驾加热", command: "关闭副驾座椅加热" }, ...["水平", "坐盆", "靠背", "腿托"].flatMap((axis) => steppedActions(percentSteps, (position) => ({ name: `副驾${axis} ${position}%`, command: `副驾${axis}调到${position}%`, }))), ], }, { id: "driver-seat", name: "主驾座椅", code: "SEAT · CH1 BODY CAN", channel: "CH1", icon: "armchair", parameters: ["通风 / 加热", "按摩强度 / 模式", "水平前后", "整椅高低", "靠背前后", "坐盆高低"], actions: [ { name: "通风低档", command: "seat driver vent low" }, { name: "通风高档", command: "seat driver vent high" }, { name: "关闭通风", command: "seat driver vent off" }, { name: "加热低档", command: "seat driver heat low" }, { name: "加热高档", command: "seat driver heat high" }, { name: "关闭加热", command: "seat driver heat off" }, ...steppedActions(["weak", "medium", "strong"], (strength) => ({ name: `按摩 ${strength}`, command: `seat driver massage strength ${strength}` })), ...steppedActions(["wave", "pulse", "stretch", "slack", "relax"], (mode) => ({ name: `按摩 ${mode}`, command: `seat driver massage mode ${mode}` })), { name: "关闭按摩", command: "seat driver massage off" }, ...["horizontal", "backrest"].flatMap((axis) => ["forward", "backward", "stop"].map((direction) => ({ name: `${axis} ${direction}`, command: `seat driver ${axis} ${direction}${direction === "stop" ? "" : " 500"}`, }))), ...["height", "basin"].flatMap((axis) => ["up", "down", "stop"].map((direction) => ({ name: `${axis} ${direction}`, command: `seat driver ${axis} ${direction}${direction === "stop" ? "" : " 500"}`, }))), ], }, { id: "climate", name: "空调", code: "CLIMATE · CH1 CAN", channel: "CH1", icon: "snowflake", parameters: ["电源", "主 / 副驾 18–32℃", "风量 1–7", "出风模式", "内外循环", "前除霜"], actions: [ { name: "开启空调", command: "ac on" }, { name: "关闭空调", command: "ac off" }, ...["driver", "passenger"].flatMap((zone) => steppedActions( Array.from({ length: 15 }, (_, index) => index + 18), (temperature) => ({ name: `${zone} ${temperature}℃`, command: `ac temp ${zone} ${temperature}` }), )), ...steppedActions(Array.from({ length: 7 }, (_, index) => index + 1), (level) => ({ name: `风量 ${level}`, command: `ac fan ${level}` })), ...steppedActions(["face", "feet", "face-feet", "defrost", "feet-defrost"], (mode) => ({ name: `出风 ${mode}`, command: `ac mode ${mode}` })), { name: "自动模式", command: "ac control auto" }, { name: "手动模式", command: "ac control manual" }, { name: "内循环", command: "ac cycle internal" }, { name: "外循环", command: "ac cycle external" }, { name: "开启前除霜", command: "ac defrost on" }, { name: "关闭前除霜", command: "ac defrost off" }, ], }, { id: "roof", name: "遮阳帘", code: "ROOF · CH1 CAN", channel: "CH1", icon: "panels-top-left", parameters: ["位置 0–100%", "开启", "关闭", "暂停"], actions: steppedActions(Array.from({ length: 11 }, (_, index) => index * 10), (position) => ({ name: `遮阳帘 ${position}%`, command: `shade position ${position}`, })), }, { id: "interior-light", name: "车内灯光", code: "INTERIOR · CH1 CAN", channel: "CH1", icon: "lamp-ceiling", parameters: ["室内灯", "四区阅读灯", "氛围灯电源", "亮度 1–5", "色号 1–127"], actions: [ { name: "室内灯开启", command: "light indoor on" }, { name: "室内灯关闭", command: "light indoor off" }, { name: "阅读灯开启", command: "light reading all on" }, { name: "阅读灯关闭", command: "light reading all off" }, { name: "氛围灯开启", command: "light ambient on" }, { name: "氛围灯关闭", command: "light ambient off" }, ...steppedActions([1, 2, 3, 4, 5], (level) => ({ name: `氛围亮度 ${level}`, command: `light ambient brightness ${level}` })), ...steppedActions([1, 16, 32, 48, 64, 80, 96, 112, 127], (color) => ({ name: `氛围色号 ${color}`, command: `light ambient color ${color}` })), ], }, { id: "windows", name: "车窗", code: "WINDOW · CH1 CAN", channel: "CH1", icon: "gallery-vertical-end", parameters: ["左前 0–100%", "右前 0–100%", "左后 0–100%", "右后 0–100%", "开 / 关 / 停"], actions: ["front-left", "front-right", "rear-left", "rear-right"].flatMap((positionName) => steppedActions(percentSteps, (position) => ({ name: `${positionName} ${position}%`, command: `window ${positionName} position ${position}`, }))), }, ]; const DEFAULT_DEVICE = { id: "toy-light-test", name: "灯光测试玩具", type: "light", aliases: ["灯光测试玩具", "灯光玩具", "玩具灯"], createdAt: new Date().toISOString(), }; const state = loadState(); const pages = Object.fromEntries(Object.keys(TOY_TYPES).map((key) => [key, 0])); let activeMode = sessionStorage.getItem(MODE_KEY) === "control" ? "control" : "study"; let selectedAddType = "light"; let learning = freshLearningSession(); let isPlanning = false; let isExecuting = false; let healthState = null; let healthRefreshPromise = null; let healthPollTimer = null; let healthToastRequested = false; let latestVehicleStateTurn = -1; let sharedTwinBand = null; let sharedTwinHome = null; function uid(prefix) { if (globalThis.crypto?.randomUUID) return `${prefix}-${crypto.randomUUID()}`; return `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}`; } function clone(value) { return JSON.parse(JSON.stringify(value)); } function escapeHtml(value) { return String(value ?? "").replace(/[&<>'"]/g, (char) => ({ "&": "&", "<": "<", ">": ">", "'": "'", '"': """, })[char]); } function delay(ms) { return new Promise((resolve) => window.setTimeout(resolve, ms)); } function validSlot(value) { const slot = Number(value); return Number.isInteger(slot) && slot >= SLOT_MIN && slot <= SLOT_MAX; } function initialState() { const device = clone(DEFAULT_DEVICE); return { version: 5, devices: [device], capabilities: seedPresets(device), selectedDeviceByType: { light: device.id }, sessionStartedAt: null, settings: { actionDelayMs: 520 }, history: [], lastProtocol: null, vehicleExecutions: {}, }; } function loadState() { try { const saved = JSON.parse(localStorage.getItem(STORAGE_KEY) || "null"); if (!saved || saved.version !== 5 || !Array.isArray(saved.devices)) { return initialState(); } saved.capabilities = Array.isArray(saved.capabilities) ? saved.capabilities : []; saved.history = Array.isArray(saved.history) ? saved.history : []; saved.vehicleExecutions = saved.vehicleExecutions && typeof saved.vehicleExecutions === "object" ? saved.vehicleExecutions : {}; saved.selectedDeviceByType = saved.selectedDeviceByType || {}; saved.settings = { actionDelayMs: Number(saved.settings?.actionDelayMs) || 520 }; return saved; } catch (error) { console.warn("读取自扩展能力数据失败,已恢复默认状态。", error); return initialState(); } } function saveState() { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } function seedPresets(device) { const type = TOY_TYPES[device.type] || TOY_TYPES.other; return type.presets.map((item) => ({ id: uid("cap"), deviceId: device.id, key: item.key, name: item.name, aliases: [item.name, ...item.aliases], category: item.category, risk: item.risk, visual: item.visual, slot: null, status: "pending", verified: false, enabled: false, createdAt: new Date().toISOString(), })); } function freshLearningSession() { return { phase: "idle", draft: null, countdownTimer: null, abortController: null, tested: false, }; } function deviceCapabilities(deviceId) { return state.capabilities.filter((item) => item.deviceId === deviceId); } function learnedCapabilities(deviceId = null) { return state.capabilities.filter((item) => ( (!deviceId || item.deviceId === deviceId) && item.status === "learned" && item.verified && item.enabled !== false && validSlot(item.slot) )); } function allocateSlot() { const used = new Set(learnedCapabilities().map((item) => Number(item.slot))); for (let slot = SLOT_MIN; slot <= SLOT_MAX; slot += 1) { if (!used.has(slot)) return slot; } return null; } function selectedDeviceForType(typeKey) { const devices = state.devices.filter((item) => item.type === typeKey); const selectedId = state.selectedDeviceByType[typeKey]; const selected = devices.find((item) => item.id === selectedId) || devices[0] || null; if (selected && selected.id !== selectedId) state.selectedDeviceByType[typeKey] = selected.id; return selected; } function buildShell() { const sidebar = $(".v2-sidebar"); const mainPanel = $(".main-panel"); const moduleSwitcher = $("#v2ModuleSwitcher"); if (!sidebar || !mainPanel || !moduleSwitcher) return false; const extensionSidebar = document.createElement("section"); extensionSidebar.className = "v2-sidebar-section v2-extension-sidebar"; extensionSidebar.innerHTML = `
EXTENSION DESK

控制台

工作模式
配置信息
当前车型腾势 N7
CHECK
推理模型模型检测中
CHECK
红外学习器正在检查 GSCU1B
CHECK
玩具信息
已接入外设玩具0
已学习能力0
`; sidebar.insertBefore(extensionSidebar, $(".v2-sidebar-footer")); const extensionFooter = document.createElement("footer"); extensionFooter.className = "v2-sidebar-footer v2-extension-footer"; extensionFooter.innerHTML = `
自扩展控制链路正在检查 GSCU1B
`; sidebar.append(extensionFooter); const root = document.createElement("section"); root.className = "v2-self-extension"; root.id = "v2SelfExtension"; root.setAttribute("aria-label", "自扩展工作台"); root.innerHTML = `
PERIPHERAL LEARNING

外设玩具能力一览与学习

为六类外设添加具体硬件,学习并验证遥控动作。

`; moduleSwitcher.insertAdjacentElement("afterend", root); buildDialogs(); return true; } function buildDialogs() { const layer = document.createElement("div"); layer.className = "v2-extension-dialog-layer"; layer.innerHTML = `
NEW PERIPHERAL

添加外设玩具

选择类别后会自动建立一组待学习动作。

将建立的预设动作
GSCU1B LEARNING

学习遥控动作

选择动作后开始

1配置2接收3验证
目标玩具--
待学习动作--
RESET LEARNING

重置学习

将擦除学习器并把所有动作恢复为待学习。

`; document.body.append(layer); } function openDialog(id) { const dialog = $(`#${id}`); if (dialog?.showModal) dialog.showModal(); else dialog?.setAttribute("open", ""); } function closeDialog(id) { const dialog = $(`#${id}`); if (dialog?.close) dialog.close(); else dialog?.removeAttribute("open"); } function showToast(message, type = "") { const toast = document.createElement("div"); toast.className = `v2-extension-toast ${type}`.trim(); toast.textContent = message; $("#extensionToastStack")?.append(toast); window.setTimeout(() => toast.remove(), 3800); } async function api(path, options = {}) { let response; try { response = await fetch(`/api/extension/${path}`, { credentials: "same-origin", ...options, headers: options.body ? { "Content-Type": "application/json", ...(options.headers || {}) } : options.headers, }); } catch (_error) { throw new Error("无法连接自扩展服务。"); } const payload = await response.json().catch(() => ({})); if (!response.ok || payload.success === false) { const error = new Error(payload.message || `自扩展请求失败(HTTP ${response.status})`); error.payload = payload; throw error; } return payload; } function sharedConversationId() { const fromConsole = window.CognitiveAgentConsole?.getConversationId?.(); if (typeof fromConsole === "string" && fromConsole.startsWith("conv-")) { sessionStorage.setItem(CONVERSATION_KEY, fromConsole); return fromConsole; } const stored = sessionStorage.getItem(CONVERSATION_KEY); if (stored?.startsWith("conv-")) return stored; const conversationId = uid("conv"); sessionStorage.setItem(CONVERSATION_KEY, conversationId); return conversationId; } function isPhysicalVehicleSource(value) { return ["physical_can", "physical_can_ch0", "physical_can_ch1", "physical_partial"].includes(value); } function containsAuthoritativePhysicalCh1State(value) { if (!value || typeof value !== "object") return false; if (isPhysicalVehicleSource(value.source) || isPhysicalVehicleSource(value.feedback_source)) { return true; } return Object.values(value).some((item) => containsAuthoritativePhysicalCh1State(item)); } function isAuthoritativeVehicleStatePayload(payload) { if (!payload || typeof payload !== "object") return false; if (payload.conversation_id !== sharedConversationId()) return false; return containsAuthoritativePhysicalCh1State(payload.actuator_state) || containsAuthoritativePhysicalCh1State(payload.domain_state); } function renderAuthoritativeVehicleState(payload) { if (!isAuthoritativeVehicleStatePayload(payload)) return false; const turn = Number(payload.conversation_turn); if (Number.isFinite(turn) && turn < latestVehicleStateTurn) return false; if (Number.isFinite(turn)) latestVehicleStateTurn = turn; const summary = $("#extensionVehicleStateSummary"); if (!summary) return false; const requestLabel = String(payload.request_id || "-").slice(-8); const hasCh0 = containsAuthoritativePhysicalCh1State(payload.actuator_state?.passenger); const hasCh1 = containsAuthoritativePhysicalCh1State(payload.actuator_state?.driver) || containsAuthoritativePhysicalCh1State(payload.domain_state); const channelLabel = hasCh0 && hasCh1 ? "CH0 + CH1" : hasCh0 ? "CH0" : "CH1"; const items = [ `${channelLabel}真实车况 · TURN ${Number.isFinite(turn) ? turn : "-"} · ${escapeHtml(requestLabel)}`, ]; const feedback = Array.isArray(payload.physical_feedback) ? payload.physical_feedback.filter((item) => ( item?.confirmed === true && [0, 1].includes(Number(item?.channel)) && isPhysicalVehicleSource(item?.source) )) : []; if (feedback.length) { const frames = [...new Set(feedback.map((item) => item.frame?.frame_id).filter(Boolean))]; items.push(`FB${feedback.length}/${payload.physical_feedback.length} MATCH · ${escapeHtml(frames.join(" / ") || channelLabel)}`); } const driver = payload.actuator_state?.driver || {}; const passenger = payload.actuator_state?.passenger || {}; if (isPhysicalVehicleSource(passenger.ventilation?.feedback_source)) { items.push(`P-VENT${passenger.ventilation.state === "on" ? `ON L${escapeHtml(passenger.ventilation.level)}` : "OFF"}`); } if (isPhysicalVehicleSource(passenger.heating?.feedback_source)) { items.push(`P-HEAT${passenger.heating.state === "on" ? `ON L${escapeHtml(passenger.heating.level)}` : "OFF"}`); } const passengerPosition = passenger.position || {}; Object.entries(passengerPosition).forEach(([axis, value]) => { if (!isPhysicalVehicleSource(value?.feedback_source)) return; items.push(`P-SEAT${escapeHtml(axis)} ${escapeHtml(value.position_pct ?? "-")}%`); }); const ventilation = driver.ventilation; const heating = driver.heating; if (ventilation?.feedback_source === "physical_can_ch1") { items.push(`VENT${ventilation.state === "on" ? `ON L${escapeHtml(ventilation.level)}` : "OFF"}`); } if (heating?.feedback_source === "physical_can_ch1") { items.push(`HEAT${heating.state === "on" ? `ON L${escapeHtml(heating.level)}` : "OFF"}`); } const massage = driver.massage; if (massage?.feedback_source === "physical_can_ch1") { items.push(`MASSAGE${escapeHtml(massage.mode || "off")} · ${escapeHtml(massage.strength || "off")}`); } const climate = payload.domain_state?.climate; if (climate?.feedback_source === "physical_can_ch1") { const temperature = climate.zones?.driver?.temperature_c ?? "-"; const outletMode = climate.zones?.driver?.outlet_mode || "-"; items.push(`AC${escapeHtml(climate.power || climate.ac || "unknown")} · ${escapeHtml(temperature)}℃ · FAN ${escapeHtml(climate.fan_level ?? "-")} · ${escapeHtml(outletMode)}`); } const roof = payload.domain_state?.roof; if (roof?.feedback_source === "physical_can_ch1") { items.push(`ROOF${escapeHtml(roof.shade_position_pct ?? "-")}% · ${escapeHtml(roof.motion_state || "unknown")}`); } const frontLeftWindow = payload.domain_state?.window?.positions?.front_left; if (frontLeftWindow?.feedback_source === "physical_can_ch1") { items.push(`WINDOW左前 ${escapeHtml(frontLeftWindow.position_pct ?? "-")}%`); } const interiorLight = payload.domain_state?.interior_light; if (interiorLight?.feedback_source === "physical_can_ch1") { items.push(`INTERIOR${escapeHtml(interiorLight.power || "unknown")}`); } const ambientLight = payload.domain_state?.ambient_light; if (ambientLight?.feedback_source === "physical_can_ch1") { const brightness = `${ambientLight.front_brightness_level ?? "-"}/${ambientLight.rear_brightness_level ?? "-"}`; const color = `${ambientLight.front_color_index ?? "-"}/${ambientLight.rear_color_index ?? "-"}`; items.push(`AMBIENT${escapeHtml(ambientLight.power || "unknown")} · L${escapeHtml(brightness)} · C${escapeHtml(color)}`); } summary.dataset.state = hasCh0 && hasCh1 ? "physical_can_dual" : hasCh0 ? "physical_can_ch0" : "physical_can_ch1"; summary.innerHTML = items.join(""); return true; } function syncSharedTwinMount() { const host = $("#extensionTwinHost"); const shouldMount = document.body.dataset.module === "self-extension" && activeMode === "control"; if (!sharedTwinBand) { sharedTwinBand = $("#seatTwin")?.closest(".twin-band") || null; if (sharedTwinBand?.parentNode) { sharedTwinHome = document.createComment("shared-vehicle-twin-home"); sharedTwinBand.parentNode.insertBefore(sharedTwinHome, sharedTwinBand); } } if (!sharedTwinBand || !sharedTwinHome || !host) return; if (shouldMount) { if (sharedTwinBand.parentNode !== host) host.replaceChildren(sharedTwinBand); sharedTwinBand.classList.add("v2-extension-shared-twin"); } else if (sharedTwinHome.parentNode && sharedTwinBand.parentNode !== sharedTwinHome.parentNode) { sharedTwinHome.parentNode.insertBefore(sharedTwinBand, sharedTwinHome.nextSibling); sharedTwinBand.classList.remove("v2-extension-shared-twin"); } window.setTimeout(() => window.dispatchEvent(new Event("resize")), 80); } function setMode(mode) { activeMode = mode === "control" ? "control" : "study"; sessionStorage.setItem(MODE_KEY, activeMode); document.body.dataset.extensionMode = activeMode; $$('[data-extension-mode]').forEach((button) => { const active = button.dataset.extensionMode === activeMode; button.classList.toggle("active", active); button.setAttribute("aria-pressed", String(active)); }); $$('[data-extension-view]').forEach((view) => { view.hidden = view.dataset.extensionView !== activeMode; }); if (activeMode === "control") renderControl(); syncSharedTwinMount(); window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } }); } function renderAll() { renderMetrics(); renderHardwareGrid(); renderControl(); renderTypeChooser(); window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } }); } function renderMetrics() { $("#extensionHardwareCount").textContent = String(state.devices.length); $("#extensionCapabilityCount").textContent = String(learnedCapabilities().length); } function renderHardwareGrid() { $("#extensionHardwareGrid").innerHTML = Object.keys(TOY_TYPES) .map((typeKey) => renderTypeCard(typeKey)) .join(""); } function renderTypeCard(typeKey) { const type = TOY_TYPES[typeKey]; const devices = state.devices.filter((item) => item.type === typeKey); const selected = selectedDeviceForType(typeKey); const capabilities = selected ? deviceCapabilities(selected.id) : type.presets.map((item) => ({ ...item, id: null, status: "pending", slot: null })); const totalPages = Math.max(1, Math.ceil(capabilities.length / ACTION_PAGE_SIZE)); pages[typeKey] = Math.min(pages[typeKey] || 0, totalPages - 1); const page = pages[typeKey]; const visible = capabilities.slice(page * ACTION_PAGE_SIZE, (page + 1) * ACTION_PAGE_SIZE); const learned = selected ? learnedCapabilities(selected.id).length : 0; const selector = devices.length ? `` : `尚未添加具体硬件`; return `
${type.label}${selector}
${learned}/${capabilities.length}${selected ? `` : ""}
${visible.map((capability) => renderCapability(capability, typeKey, Boolean(selected))).join("")}
`; } function renderCapability(capability, typeKey, hasDevice) { const learned = capability.status === "learned" && validSlot(capability.slot); if (!hasDevice) { return ``; } return `
${learned ? `` : ``}
`; } function renderControl() { const learned = learnedCapabilities(); $("#extensionLearnedTotal").textContent = `${learned.length} 项可执行`; if (!learned.length) { $("#extensionLearnedGrid").innerHTML = `
还没有已学习动作切换到学习模式,为外设玩具录入遥控能力。
`; } else { $("#extensionLearnedGrid").innerHTML = learned.map((capability) => { const device = state.devices.find((item) => item.id === capability.deviceId); const type = TOY_TYPES[device?.type] || TOY_TYPES.other; return ``; }).join(""); } $("#extensionVehicleGrid").innerHTML = VEHICLE_DEVICES.map((device) => { const execution = state.vehicleExecutions?.[device.id]; const executionState = execution?.state || "idle"; return `
${escapeHtml(device.name)}${escapeHtml(device.code)}
${escapeHtml(device.channel)}
${device.parameters.map((parameter) => `${escapeHtml(parameter)}`).join("")}
`; }).join(""); } function findVehicleAction(command) { for (const device of VEHICLE_DEVICES) { const action = device.actions.find((item) => item.command === command); if (action) return { device, action }; } const inferredDeviceId = /^副驾/.test(command) ? "passenger-seat" : /^seat driver /.test(command) ? "driver-seat" : /^ac /.test(command) ? "climate" : /^shade /.test(command) ? "roof" : /^light /.test(command) ? "interior-light" : /^window /.test(command) ? "windows" : null; const device = VEHICLE_DEVICES.find((item) => item.id === inferredDeviceId); if (device) return { device, action: { name: command, command } }; return null; } function updateVehicleCardExecution(command, executionState, detail) { const match = findVehicleAction(command); if (!match) return; const actionText = match.action.name || command; state.vehicleExecutions ||= {}; state.vehicleExecutions[match.device.id] = { state: executionState, action: actionText, detail, command, at: new Date().toISOString(), }; const card = $(`[data-extension-vehicle-device="${match.device.id}"]`); if (!card) return; card.dataset.executionState = executionState; const actionNode = card.querySelector("[data-extension-current-action]"); const detailNode = card.querySelector("[data-extension-action-detail]"); if (actionNode) actionNode.textContent = actionText; if (detailNode) detailNode.textContent = detail; } function renderTypeChooser() { $("#extensionTypeChooser").innerHTML = Object.entries(TOY_TYPES).map(([key, type]) => ` `).join(""); $("#extensionPresetPreview").innerHTML = TOY_TYPES[selectedAddType].presets .map((item) => `${escapeHtml(item.name)}`) .join(""); } function openAdd(typeKey = "light") { selectedAddType = TOY_TYPES[typeKey] ? typeKey : "other"; $("#extensionHardwareName").value = ""; renderTypeChooser(); openDialog("extensionAddDialog"); window.setTimeout(() => $("#extensionHardwareName")?.focus(), 80); window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } }); } function addHardware(event) { event.preventDefault(); const name = $("#extensionHardwareName").value.trim(); if (!name) return; if (state.devices.some((item) => item.name === name)) { showToast("已经存在同名外设玩具。", "error"); return; } const device = { id: uid("toy"), name, type: selectedAddType, aliases: [name], createdAt: new Date().toISOString(), }; state.devices.push(device); state.capabilities.push(...seedPresets(device)); state.selectedDeviceByType[device.type] = device.id; pages[device.type] = 0; saveState(); closeDialog("extensionAddDialog"); renderAll(); showToast(`已添加“${name}”,可以开始学习遥控动作。`, "success"); } function deleteHardware(deviceId) { const device = state.devices.find((item) => item.id === deviceId); if (!device || !window.confirm(`删除“${device.name}”及其全部前端能力记录?`)) return; state.devices = state.devices.filter((item) => item.id !== deviceId); state.capabilities = state.capabilities.filter((item) => item.deviceId !== deviceId); state.selectedDeviceByType[device.type] = state.devices.find( (item) => item.type === device.type, )?.id || null; saveState(); renderAll(); showToast(`已删除“${device.name}”。`); } function showLearnPage(page) { $$('[data-extension-learn-page]').forEach((element) => { element.hidden = Number(element.dataset.extensionLearnPage) !== page; }); $$('[data-extension-learn-step]').forEach((element) => { element.classList.toggle("active", Number(element.dataset.extensionLearnStep) <= page); }); } function resetLearnStatus() { window.clearInterval(learning.countdownTimer); learning = freshLearningSession(); $("#extensionLearnAnimation").dataset.state = "ready"; $("#extensionLearnStatusTitle").textContent = "准备接收遥控信号"; $("#extensionLearnStatusMessage").textContent = "请将遥控器对准学习器并按一下对应按键。"; $("#extensionLearnCountdown").textContent = "15"; $("#extensionStartLearn").disabled = false; $("#extensionStartLearn").textContent = "开始接收"; $("#extensionCancelLearn").hidden = false; $("#extensionTestEmit").disabled = false; $("#extensionTestEmit").textContent = "发射试一下"; $("#extensionConfirmLearn").disabled = true; } function openLearn(capabilityId) { const capability = state.capabilities.find((item) => item.id === capabilityId); const device = state.devices.find((item) => item.id === capability?.deviceId); if (!capability || !device) return; const type = TOY_TYPES[device.type] || TOY_TYPES.other; resetLearnStatus(); learning.draft = { capabilityId: capability.id, deviceId: device.id, slot: allocateSlot() }; $("#extensionLearnTitle").textContent = `学习“${capability.name}”`; $("#extensionLearnPath").textContent = `${type.label} · ${device.name}`; $("#extensionLearnIcon").innerHTML = ``; $("#extensionLearnDevice").textContent = device.name; $("#extensionLearnAction").textContent = capability.name; $("#extensionLearnAliases").value = (capability.aliases || []).join("、"); $("#extensionLearnRisk").value = capability.risk || "low"; $("#extensionLearnSlot").value = allocateSlot() || ""; showLearnPage(1); openDialog("extensionLearnDialog"); window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } }); } function collectLearningDraft() { const capability = state.capabilities.find( (item) => item.id === learning.draft?.capabilityId, ); const device = state.devices.find((item) => item.id === capability?.deviceId); const slot = Number($("#extensionLearnSlot").value || allocateSlot()); if (!capability || !device) throw new Error("预设动作不存在。"); if (!validSlot(slot)) throw new Error("没有可用红外槽位。"); const occupied = learnedCapabilities().find( (item) => Number(item.slot) === slot && item.id !== capability.id, ); if (occupied) throw new Error(`槽位 ${slot} 已被“${occupied.name}”使用。`); return { capabilityId: capability.id, deviceId: device.id, slot, aliases: $("#extensionLearnAliases").value .split(/[,,、;;]+/) .map((item) => item.trim()) .filter(Boolean), risk: $("#extensionLearnRisk").value, }; } async function sendGatewayCommand(action, payload = {}, signal = undefined) { const result = await api(`gscu/${action}`, { method: "POST", body: JSON.stringify(payload), signal, }); state.lastProtocol = { at: new Date().toISOString(), action, payload, response: result, }; saveState(); return result; } async function startLearning() { if (learning.phase === "listening") return; try { learning.draft = collectLearningDraft(); } catch (error) { showToast(error.message, "error"); showLearnPage(1); return; } const capability = state.capabilities.find((item) => item.id === learning.draft.capabilityId); const device = state.devices.find((item) => item.id === learning.draft.deviceId); learning.phase = "listening"; learning.abortController = new AbortController(); $("#extensionLearnAnimation").dataset.state = "listening"; $("#extensionLearnStatusTitle").textContent = `正在学习“${capability.name}”…`; $("#extensionLearnStatusMessage").textContent = `请将“${device.name}”的遥控器对准学习器。`; $("#extensionStartLearn").disabled = true; $("#extensionStartLearn").textContent = "正在接收"; let countdown = 15; $("#extensionLearnCountdown").textContent = String(countdown); learning.countdownTimer = window.setInterval(() => { countdown -= 1; $("#extensionLearnCountdown").textContent = countdown > 0 ? String(countdown) : "…"; if (countdown <= 0) window.clearInterval(learning.countdownTimer); }, 1000); try { await sendGatewayCommand( "learn", { slot: learning.draft.slot }, learning.abortController.signal, ); window.clearInterval(learning.countdownTimer); learning.phase = "learned"; $("#extensionLearnAnimation").dataset.state = "success"; await delay(300); showLearnPage(3); } catch (error) { if (error.name === "AbortError") return; window.clearInterval(learning.countdownTimer); learning.phase = "idle"; $("#extensionLearnAnimation").dataset.state = "ready"; $("#extensionLearnStatusTitle").textContent = "这次没有捕获到按键"; $("#extensionLearnStatusMessage").textContent = error.message; $("#extensionLearnCountdown").textContent = ""; $("#extensionStartLearn").disabled = false; $("#extensionStartLearn").textContent = "重新接收"; showToast(error.message, "error"); } } async function cancelLearning() { window.clearInterval(learning.countdownTimer); learning.abortController?.abort(); try { await sendGatewayCommand("cancel"); } catch (error) { console.warn(error); } resetLearnStatus(); showLearnPage(1); } async function testLearning() { if (!validSlot(learning.draft?.slot)) return; const button = $("#extensionTestEmit"); button.disabled = true; button.textContent = "正在发射…"; try { await sendGatewayCommand("emit", { slot: learning.draft.slot }); learning.tested = true; $("#extensionConfirmLearn").disabled = false; $("#extensionTestTitle").textContent = "信号已发射,请确认玩具反应"; $("#extensionTestMessage").textContent = "动作正确即可完成学习;不正确请重新接收。"; showToast("红外信号已发射,请观察玩具。", "success"); } catch (error) { showToast(error.message, "error"); } finally { button.disabled = false; button.textContent = "再发射一次"; } } function confirmLearning() { if (!learning.tested || !learning.draft) return; const capability = state.capabilities.find( (item) => item.id === learning.draft.capabilityId, ); if (!capability) return; Object.assign(capability, { aliases: [...new Set([capability.name, ...learning.draft.aliases])], risk: learning.draft.risk, slot: Number(learning.draft.slot), status: "learned", verified: true, enabled: true, learnedAt: new Date().toISOString(), }); state.sessionStartedAt ||= new Date().toISOString(); saveState(); closeDialog("extensionLearnDialog"); renderAll(); showToast(`“${capability.name}”已学习完成。`, "success"); learning = freshLearningSession(); } function forgetCapability(capabilityId) { const capability = state.capabilities.find((item) => item.id === capabilityId); if (!capability || !window.confirm(`忘记动作“${capability.name}”?`)) return; Object.assign(capability, { slot: null, status: "pending", verified: false, enabled: false, learnedAt: null, }); saveState(); renderAll(); showToast(`“${capability.name}”已恢复为待学习。`); } function peripheralCatalog() { return learnedCapabilities().map((capability) => { const device = state.devices.find((item) => item.id === capability.deviceId); const type = TOY_TYPES[device?.type] || TOY_TYPES.other; return { id: capability.id, status: "learned", device_id: device?.id, device_name: device?.name, device_type: device?.type, type_label: type.label, action: capability.name, aliases: capability.aliases || [], risk: capability.risk || "low", }; }).filter((item) => item.device_id); } function vehicleCatalog() { return VEHICLE_DEVICES.map((device) => ({ id: device.id, name: device.name, code: device.code, channel: device.channel, parameters: device.parameters, actions: device.actions, })); } function hydratePlan(steps) { if (!Array.isArray(steps)) throw new Error("联动计划步骤格式无效。"); if (steps.length > MAX_PLAN_STEPS) throw new Error("联动计划步骤过多。"); const plan = steps.map((step) => { if (!step || typeof step !== "object" || Array.isArray(step)) { throw new Error("联动计划包含无效步骤。"); } if (step.kind === "delay") { if (!Number.isInteger(step.delay_ms) || step.delay_ms < MIN_PLAN_DELAY_MS || step.delay_ms > MAX_PLAN_DELAY_MS) { throw new Error("延时步骤必须使用安全范围内的整数毫秒。"); } if ([step.capability_id, step.command].some( (value) => value != null && String(value).trim() !== "", )) { throw new Error("延时步骤不能包含设备命令。"); } return { kind: "delay", delayMs: step.delay_ms, label: step.delay_ms % 1000 === 0 ? `等待 ${step.delay_ms / 1000} 秒` : `等待 ${step.delay_ms} 毫秒`, risk: "low", }; } if (step.delay_ms !== undefined && step.delay_ms !== 0) { throw new Error("非延时步骤不能携带延时值。"); } if (step.kind === "vehicle") { const command = String(step.command || "").trim(); if (!command) throw new Error("车端计划步骤缺少命令。"); return { kind: "vehicle", command, label: step.label || command, risk: step.risk || "medium", }; } if (step.kind !== "peripheral") throw new Error("联动计划包含未知步骤。"); const capability = state.capabilities.find((item) => item.id === step.capability_id); const device = state.devices.find((item) => item.id === capability?.deviceId); if (!capability || !device || !learnedCapabilities(device.id).some( (item) => item.id === capability.id, )) throw new Error("联动计划引用了尚未学习的外设能力。"); return { kind: "peripheral", device, capability, label: `${device.name} · ${capability.name}`, risk: capability.risk || "low", }; }); let totalDelayMs = 0; plan.forEach((step, index) => { if (step.kind !== "delay") return; if (index === 0 || index === plan.length - 1) { throw new Error("延时步骤必须位于两个动作之间。"); } if (plan[index - 1].kind === "delay") { throw new Error("联动计划不能包含连续延时。"); } totalDelayMs += step.delayMs; }); if (totalDelayMs > MAX_TOTAL_PLAN_DELAY_MS) { throw new Error("联动计划累计延时超过安全上限。"); } return plan; } function setExecutionState(stateName, title, note, progress = 0) { const card = $("#extensionExecutionState"); card.dataset.state = stateName; $("#extensionRunTitle").textContent = title; $("#extensionRunNote").textContent = note; $("#extensionRunProgressBar").style.width = `${Math.max(0, Math.min(100, progress))}%`; } function renderRunPlan(plan, active = -1) { const rail = $("#extensionRunPlan"); rail.hidden = false; rail.innerHTML = plan.map((step, index) => ` ${String(index + 1).padStart(2, "0")}${escapeHtml(step.label || "待执行动作")}`).join(""); } async function executeInstruction(text) { const input = String(text || "").trim(); if (!input) { showToast("请先输入要执行的动作。", "error"); return; } if (isPlanning || isExecuting) { showToast("上一条指令还在处理,请稍等。", "error"); return; } const button = $("#extensionExecuteButton"); isPlanning = true; button.disabled = true; setExecutionState("running", "GPT 正在理解意图", "匹配已学习能力与车端能力", 10); try { const result = await api("assistant/plan", { method: "POST", body: JSON.stringify({ utterance: input, peripherals: peripheralCatalog(), vehicle_devices: vehicleCatalog(), }), }); if (result.needs_confirmation) { setExecutionState("warning", "需要补充目标", result.question || "请说明具体硬件", 0); showToast(result.question || "请说明具体硬件。", "error"); return; } const plan = hydratePlan(result.steps); if (!plan.length) { setExecutionState("warning", "没有可执行动作", result.summary || "未匹配到能力", 0); showToast(result.summary || "没有找到可执行动作。", "error"); return; } const risky = plan.filter((item) => item.risk === "high"); if (risky.length && !window.confirm( `将执行 ${risky.length} 个可能让玩具移动的动作,请确认周围安全。`, )) { setExecutionState("idle", "已取消执行", "高风险动作未获确认", 0); return; } isPlanning = false; await executePlan(plan); } catch (error) { setExecutionState("error", "联动计划生成失败", error.message, 0); showToast(error.message, "error"); } finally { isPlanning = false; button.disabled = false; } } function vehicleExecutionSucceeded(result) { const operations = Array.isArray(result?.operation_results) ? result.operation_results : []; const everyOperationSucceeded = operations.length > 0 && operations.every((item) => item?.status === "succeeded"); const hardwareRequest = result?.execution_mode === "hardware"; const feedback = Array.isArray(result?.physical_feedback) ? result.physical_feedback : []; const everyHardwareOperationConfirmed = !hardwareRequest || ( result?.physical_feedback_confirmed === true && feedback.length === operations.length && feedback.every((item) => item?.confirmed === true) ); return result?.success === true && result?.status === "succeeded" && everyOperationSucceeded && everyHardwareOperationConfirmed; } function synchronizeVehicleExecution(command, result) { if (!result || typeof result !== "object") return; const completed = vehicleExecutionSucceeded(result); state.lastVehicleExecution = { at: new Date().toISOString(), command, success: completed, status: result.status || "unknown", executionMode: result.execution_mode || null, requestId: result.request_id || null, operationResults: result.operation_results || [], executionReport: result.execution_report || null, domainState: result.domain_state || null, actuatorState: result.actuator_state || null, physicalState: result.physical_state || null, physicalFeedback: result.physical_feedback || [], physicalFeedbackConfirmed: result.physical_feedback_confirmed === true, conversationId: result.conversation_id || null, conversationTurn: result.conversation_turn ?? null, }; renderAuthoritativeVehicleState(result); const operationCount = Array.isArray(result.operation_results) ? result.operation_results.length : 0; const physicalCount = Array.isArray(result.physical_feedback) ? result.physical_feedback.filter((item) => item?.confirmed === true).length : 0; const detail = completed ? result.execution_mode === "hardware" ? `物理回读已确认 · ${physicalCount || operationCount} 项` : `安全演练已完成 · ${operationCount || 1} 项` : vehicleExecutionError(result); updateVehicleCardExecution(command, completed ? "succeeded" : "failed", detail); saveState(); window.dispatchEvent(new CustomEvent("cognitive-agent:extension-vehicle-result", { detail: result, })); } function vehicleExecutionError(result) { const failed = (result?.operation_results || []).find( (item) => item?.status !== "succeeded", ); return result?.message || failed?.message || `车端动作未成功完成(${result?.status || "unknown"})`; } async function executeVehicle(command) { let result; try { result = await api("vehicle/execute", { method: "POST", // Channel, seat target, provider, vehicle profile and execution mode // are intentionally absent: the server owns CH0/CH1 routing and safety policy. body: JSON.stringify({ command, conversation_id: sharedConversationId() }), }); } catch (error) { if (error.payload) synchronizeVehicleExecution(command, error.payload); throw error; } synchronizeVehicleExecution(command, result); if (!vehicleExecutionSucceeded(result)) { const error = new Error(vehicleExecutionError(result)); error.payload = result; throw error; } return result; } async function executePlan(plan) { if (isExecuting || !plan.length) return; isExecuting = true; renderRunPlan(plan); let completed = 0; let vehicleDryRun = false; let vehicleHardwareFeedback = 0; for (let index = 0; index < plan.length; index += 1) { const step = plan[index]; renderRunPlan(plan, index); setExecutionState( "running", step.label, `正在处理第 ${index + 1} / ${plan.length} 个动作`, (index / plan.length) * 100, ); try { if (step.kind === "delay") { await delay(step.delayMs); } else if (step.kind === "vehicle") { updateVehicleCardExecution(step.command, "running", `正在执行:${step.label || step.command}`); saveState(); const result = await executeVehicle(step.command); vehicleDryRun ||= result.execution_mode === "dry_run"; if (result.execution_mode === "hardware") { vehicleHardwareFeedback += result.physical_feedback.length; } } else { await sendGatewayCommand("emit", { slot: step.capability.slot }); } completed += 1; addHistory(step, true); } catch (error) { addHistory(step, false, error.message); setExecutionState("error", "联动执行已中断", error.message, (completed / plan.length) * 100); showToast(error.message, "error"); break; } const nextStep = plan[index + 1]; if (nextStep && step.kind !== "delay" && nextStep.kind !== "delay") { await delay(state.settings.actionDelayMs); } } renderRunPlan(plan, completed); if (completed === plan.length) { setExecutionState( "success", "联动计划已全部处理", vehicleDryRun ? "外设已发射,车端处于安全演练模式" : vehicleHardwareFeedback > 0 ? `车端 ${vehicleHardwareFeedback} 项动作均已同步物理反馈` : "外设与车端链路处理完成", 100, ); showToast("联动计划已全部处理。", "success"); } isExecuting = false; } function addHistory(step, success, error = null) { state.history.unshift({ id: uid("history"), at: new Date().toISOString(), kind: step.kind || "peripheral", deviceId: step.device?.id || null, capabilityName: step.label || step.capability?.name || step.command, slot: step.capability?.slot || null, success, error, }); state.history = state.history.slice(0, HISTORY_LIMIT); saveState(); } async function runCapability(capabilityId) { const capability = state.capabilities.find((item) => item.id === capabilityId); const device = state.devices.find((item) => item.id === capability?.deviceId); if (!capability || !device) return; if (capability.risk === "high" && !window.confirm( `“${capability.name}”可能让玩具移动,请确认周围安全。`, )) return; await executePlan([{ kind: "peripheral", device, capability, label: `${device.name} · ${capability.name}`, risk: capability.risk || "low", }]); } async function requestHealth() { const controller = new AbortController(); const timeoutId = window.setTimeout(() => controller.abort(), HEALTH_REQUEST_TIMEOUT_MS); try { return await api("health", { signal: controller.signal }); } finally { window.clearTimeout(timeoutId); } } function refreshHealth(showMessage = false) { healthToastRequested ||= showMessage; if (healthRefreshPromise) return healthRefreshPromise; healthRefreshPromise = (async () => { if (!healthState || healthToastRequested) { $("#extensionStudyHealth").dataset.state = "checking"; $("#extensionStudyHealthState").textContent = "CHECK"; $("#extensionVehicleCard").dataset.state = "checking"; $("#extensionVehicleState").textContent = "CHECK"; } try { healthState = await requestHealth(); } catch (error) { healthState = { connected: false, listening: false, transport: "unavailable", message: error.message, gpt: { configured: false }, vehicle: { connected: false }, }; } const connected = Boolean(healthState.connected); const listening = !connected && Boolean(healthState.listening); const transport = String(healthState.transport || "--").toUpperCase(); let connectionLabel; if (connected) { const peer = String(healthState.peer || "").trim(); connectionLabel = healthState.transport === "simulation" ? "模拟学习器已就绪" : `${transport} 学习器已连接${peer ? ` · ${peer}` : ""}`; } else if (listening) { connectionLabel = `${transport} 监听已就绪,等待 GSCU1B 接入`; } else { connectionLabel = healthState.message || "GSCU1B 尚未连接"; } const studyHealthText = $("#extensionStudyHealthText"); studyHealthText.textContent = connectionLabel; studyHealthText.title = healthState.message || connectionLabel; $("#extensionStudyHealthState").textContent = connected ? "READY" : (listening ? "WAIT" : "OFFLINE"); $("#extensionStudyHealth").dataset.state = connected ? "online" : (listening ? "checking" : "offline"); const vehicleConnected = Boolean(healthState.vehicle?.connected); $("#extensionVehicleState").textContent = vehicleConnected ? "READY" : "OFFLINE"; $("#extensionVehicleCard").dataset.state = vehicleConnected ? "online" : "offline"; const connectionText = $("#extensionConnectionText"); connectionText.textContent = connectionLabel; connectionText.title = healthState.message || connectionLabel; $(".v2-extension-footer").classList.toggle("offline", !connected); $("#extensionModelName").textContent = healthState.gpt?.configured ? healthState.gpt.model || "GPT" : "GPT 未配置"; $("#extensionModelState").textContent = healthState.gpt?.configured ? "READY" : "LOCAL"; $("#extensionModelCard").classList.toggle("unavailable", !healthState.gpt?.configured); if (healthToastRequested) { const ready = [connected, healthState.gpt?.configured, healthState.vehicle?.connected] .filter(Boolean).length; showToast(`自扩展链路:${ready} / 3 项就绪。`, ready === 3 ? "success" : "error"); } healthToastRequested = false; return healthState; })().finally(() => { healthRefreshPromise = null; }); return healthRefreshPromise; } function startHealthPolling() { if (healthPollTimer !== null) return; healthPollTimer = window.setInterval(() => { if (document.visibilityState === "visible" && document.body.dataset.module === "self-extension") refreshHealth(); }, HEALTH_POLL_INTERVAL_MS); } function stopHealthPolling() { if (healthPollTimer !== null) window.clearInterval(healthPollTimer); healthPollTimer = null; } async function resetSession() { const button = $("#extensionConfirmReset"); button.disabled = true; button.textContent = "正在擦除学习器…"; try { await sendGatewayCommand("erase", { confirm: "ERASE_ALL" }); state.capabilities = state.devices.flatMap((device) => seedPresets(device)); state.history = []; state.sessionStartedAt = new Date().toISOString(); saveState(); closeDialog("extensionResetDialog"); renderAll(); showToast("学习器已擦除,所有动作恢复为待学习。", "success"); } catch (error) { showToast(error.message, "error"); } finally { button.textContent = "擦除并重新开始"; button.disabled = $("#extensionResetConfirm").value.trim() !== "清空"; } } function bindEvents() { $$('[data-extension-mode]').forEach((button) => { button.addEventListener("click", () => setMode(button.dataset.extensionMode)); }); $("#extensionHardwareGrid").addEventListener("click", (event) => { const add = event.target.closest("[data-add-type]"); if (add) return openAdd(add.dataset.addType); const pageButton = event.target.closest("[data-extension-page]"); if (pageButton) { const typeKey = pageButton.dataset.extensionPage; pages[typeKey] = Math.max( 0, (pages[typeKey] || 0) + Number(pageButton.dataset.pageDirection), ); renderHardwareGrid(); window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } }); return; } const remove = event.target.closest("[data-delete-extension-device]"); if (remove) return deleteHardware(remove.dataset.deleteExtensionDevice); const forget = event.target.closest("[data-forget-extension-capability]"); if (forget) return forgetCapability(forget.dataset.forgetExtensionCapability); const learn = event.target.closest("[data-learn-extension-capability]"); if (learn) return openLearn(learn.dataset.learnExtensionCapability); const run = event.target.closest("[data-run-extension-capability]"); if (run) runCapability(run.dataset.runExtensionCapability); }); $("#extensionHardwareGrid").addEventListener("change", (event) => { const selector = event.target.closest("[data-extension-device-select]"); if (!selector) return; state.selectedDeviceByType[selector.dataset.extensionDeviceSelect] = selector.value; pages[selector.dataset.extensionDeviceSelect] = 0; saveState(); renderHardwareGrid(); window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } }); }); $("#extensionTypeChooser").addEventListener("click", (event) => { const choice = event.target.closest("[data-extension-type-choice]"); if (!choice) return; selectedAddType = choice.dataset.extensionTypeChoice; renderTypeChooser(); window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } }); }); $("#extensionAddForm").addEventListener("submit", addHardware); $("#extensionContinueLearn").addEventListener("click", () => { try { learning.draft = collectLearningDraft(); showLearnPage(2); } catch (error) { showToast(error.message, "error"); } }); $("#extensionStartLearn").addEventListener("click", startLearning); $("#extensionCancelLearn").addEventListener("click", cancelLearning); $("#extensionTestEmit").addEventListener("click", testLearning); $("#extensionConfirmLearn").addEventListener("click", confirmLearning); $("#extensionLearnDialog").addEventListener("cancel", (event) => { if (learning.phase === "listening") { event.preventDefault(); cancelLearning().then(() => closeDialog("extensionLearnDialog")); } }); $("#extensionLearnedGrid").addEventListener("click", (event) => { const switcher = event.target.closest("[data-switch-extension-mode]"); if (switcher) return setMode(switcher.dataset.switchExtensionMode); const run = event.target.closest("[data-run-extension-capability]"); if (run) runCapability(run.dataset.runExtensionCapability); }); $("#extensionExecuteButton").addEventListener( "click", () => executeInstruction($("#extensionUtterance").value), ); $("#extensionUtterance").addEventListener("keydown", (event) => { if (event.key === "Enter" && !event.shiftKey) { event.preventDefault(); executeInstruction(event.currentTarget.value); } }); $("#extensionFooterRefresh").addEventListener("click", () => refreshHealth(true)); $("#extensionResetButton").addEventListener("click", () => { $("#extensionResetConfirm").value = ""; $("#extensionConfirmReset").disabled = true; openDialog("extensionResetDialog"); }); $("#extensionResetConfirm").addEventListener("input", (event) => { $("#extensionConfirmReset").disabled = event.currentTarget.value.trim() !== "清空"; }); $("#extensionConfirmReset").addEventListener("click", resetSession); $$('[data-close-extension-dialog]').forEach((button) => { button.addEventListener("click", async () => { if (button.dataset.closeExtensionDialog === "extensionLearnDialog" && learning.phase === "listening") await cancelLearning(); closeDialog(button.dataset.closeExtensionDialog); }); }); window.addEventListener("cognitive-agent:module-change", (event) => { if (event.detail?.module === "self-extension") { setMode(activeMode); refreshHealth(); } else { syncSharedTwinMount(); } }); window.addEventListener("cognitive-agent:result", (event) => { renderAuthoritativeVehicleState(event.detail); }); document.addEventListener("visibilitychange", () => { if (document.visibilityState === "visible" && document.body.dataset.module === "self-extension") refreshHealth(); }); window.addEventListener("pagehide", stopHealthPolling); window.addEventListener("pageshow", () => { startHealthPolling(); if (document.visibilityState === "visible" && document.body.dataset.module === "self-extension") refreshHealth(); }); } function init() { if (!buildShell()) return; bindEvents(); renderAll(); setMode(activeMode); if (document.body.dataset.module === "self-extension") refreshHealth(); startHealthPolling(); window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } }); } document.addEventListener("DOMContentLoaded", init); }());