Files
Cognitive_Agent/apps/web-console/self-extension.js
T

1224 lines
56 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(function () {
"use strict";
const STORAGE_KEY = "cognitive-agent.ir-toy-console.v5";
const MODE_KEY = "cognitive-agent.extension-mode";
const SLOT_MIN = 1;
const SLOT_MAX = 248;
const ACTION_PAGE_SIZE = 6;
const HISTORY_LIMIT = 40;
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 VEHICLE_DEVICES = [
{ name: "座椅", code: "SEAT · CAN", icon: "armchair", actions: [
{ name: "主驾通风", command: "打开主驾座椅通风" },
{ name: "主驾加热", command: "打开主驾座椅加热" },
{ name: "副驾通风", command: "打开副驾座椅通风" },
{ name: "座椅位置", command: "把副驾座椅向后调一点" },
] },
{ name: "空调", code: "CLIMATE · CAN", icon: "snowflake", actions: [
{ name: "开启空调", command: "打开空调" },
{ name: "24℃", command: "把空调温度调到24度" },
{ name: "风量 3", command: "把空调风量调到3档" },
{ name: "自动模式", command: "把空调设置为自动模式" },
] },
{ name: "电动出风口", code: "OUTLET · 4 ZONES", icon: "wind", actions: [
{ name: "主驾左", command: "把主驾左出风口调到中间位置" },
{ name: "主驾右", command: "把主驾右出风口调到中间位置" },
{ name: "副驾左", command: "把副驾左出风口调到中间位置" },
{ name: "副驾右", command: "把副驾右出风口调到中间位置" },
] },
{ name: "遮阳帘", code: "ROOF · SHADE", icon: "panels-top-left", actions: [
{ name: "打开", command: "打开遮阳帘" },
{ name: "关闭", command: "关闭遮阳帘" },
{ name: "半开", command: "把遮阳帘打开一半" },
{ name: "停止", command: "停止遮阳帘" },
] },
{ name: "车内灯光", code: "INTERIOR · LIGHT", icon: "lamp-ceiling", actions: [
{ name: "室内灯", command: "打开车内室内灯" },
{ name: "阅读灯", command: "打开全部阅读灯" },
{ name: "氛围灯", command: "打开车内氛围灯" },
{ name: "氛围亮度", command: "把氛围灯亮度调到3档" },
] },
{ name: "车窗与门锁", code: "WINDOW · DOOR", icon: "gallery-vertical-end", actions: [
{ name: "全部开窗", command: "打开全部车窗" },
{ name: "全部关窗", command: "关闭全部车窗" },
{ name: "左前窗 50%", command: "把左前车窗打开到50%" },
{ name: "后排门锁", command: "锁上后排门锁" },
] },
];
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;
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) => ({
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
"'": "&#39;",
'"': "&quot;",
})[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,
};
}
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.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 = `
<div class="v2-section-heading">
<span>EXTENSION DESK</span>
<h2>控制台</h2>
</div>
<span class="v2-field-caption">工作模式</span>
<div class="v2-extension-mode-switch" role="group" aria-label="工作模式">
<button type="button" data-extension-mode="study"><i data-lucide="book-open-check"></i><span>学习模式</span></button>
<button type="button" data-extension-mode="control"><i data-lucide="send"></i><span>控制模式</span></button>
</div>
<div class="v2-divider"></div>
<span class="v2-field-caption">配置信息</span>
<div class="v2-extension-config-stack">
<div class="v2-extension-config-card" id="extensionVehicleCard" data-state="checking"><i data-lucide="car-front"></i><div><small>当前车型</small><strong>腾势 N7</strong></div><span id="extensionVehicleState">CHECK</span></div>
<div class="v2-extension-config-card" id="extensionModelCard"><i data-lucide="sparkles"></i><div><small>推理模型</small><strong id="extensionModelName">模型检测中</strong></div><span id="extensionModelState">CHECK</span></div>
<div class="v2-extension-config-card" id="extensionStudyHealth" data-state="checking"><i data-lucide="radio-tower"></i><div><small>红外学习器</small><strong id="extensionStudyHealthText">正在检查 GSCU1B</strong></div><span id="extensionStudyHealthState">CHECK</span></div>
</div>
<div class="v2-divider"></div>
<span class="v2-field-caption">玩具信息</span>
<div class="v2-extension-metrics">
<div><i data-lucide="radio-tower"></i><span>已接入外设玩具</span><strong id="extensionHardwareCount">0</strong></div>
<div><i data-lucide="badge-check"></i><span>已学习能力</span><strong id="extensionCapabilityCount">0</strong></div>
</div>`;
sidebar.insertBefore(extensionSidebar, $(".v2-sidebar-footer"));
const extensionFooter = document.createElement("footer");
extensionFooter.className = "v2-sidebar-footer v2-extension-footer";
extensionFooter.innerHTML = `
<span class="v2-live-dot"></span>
<div><strong>自扩展控制链路</strong><small id="extensionConnectionText">正在检查 GSCU1B</small></div>
<button type="button" id="extensionFooterRefresh" title="刷新连接" aria-label="刷新自扩展连接"><i data-lucide="refresh-cw"></i></button>`;
sidebar.append(extensionFooter);
const root = document.createElement("section");
root.className = "v2-self-extension";
root.id = "v2SelfExtension";
root.setAttribute("aria-label", "自扩展工作台");
root.innerHTML = `
<section class="v2-extension-view v2-extension-study" data-extension-view="study">
<header class="v2-extension-heading">
<div><span class="section-kicker">PERIPHERAL LEARNING</span><h1>外设玩具能力一览与学习</h1><p>为六类外设添加具体硬件,学习并验证遥控动作。</p></div>
<div class="v2-extension-heading-actions">
<button class="v2-extension-reset-action" type="button" id="extensionResetButton"><i data-lucide="rotate-ccw"></i>重置学习</button>
<button type="button" data-add-type="light"><i data-lucide="plus"></i>添加玩具</button>
</div>
</header>
<div class="v2-extension-hardware-grid" id="extensionHardwareGrid"></div>
</section>
<section class="v2-extension-view v2-extension-control" data-extension-view="control" hidden>
<header class="v2-extension-heading">
<div><span class="section-kicker">UNIFIED CONTROL</span><h1>说一句,联动外设玩具与车端设备</h1><p>GPT 只从已学习能力与受控车端能力中生成计划。</p></div>
</header>
<div class="v2-extension-command-row">
<div class="v2-extension-composer">
<textarea id="extensionUtterance" rows="1" maxlength="500" placeholder="例如:发出红光,然后打开车内阅读灯" aria-label="输入自扩展联动指令"></textarea>
<button class="primary-action" type="button" id="extensionExecuteButton" aria-label="执行自扩展联动指令"><i data-lucide="play"></i><span>执行</span></button>
</div>
<aside class="v2-extension-execution-state" id="extensionExecutionState" data-state="idle">
<span>执行情况</span><strong id="extensionRunTitle">等待指令</strong><small id="extensionRunNote">尚未生成联动计划</small>
<div class="v2-extension-run-progress"><i id="extensionRunProgressBar"></i></div>
</aside>
</div>
<div class="v2-extension-control-grid">
<section class="v2-extension-action-board">
<header><div><span class="section-kicker">LEARNED ACTIONS</span><h2>玩具动作</h2></div><strong id="extensionLearnedTotal">0 项可执行</strong></header>
<div class="v2-extension-learned-grid" id="extensionLearnedGrid"></div>
</section>
<section class="v2-extension-action-board">
<header><div><span class="section-kicker">VEHICLE ACTIONS</span><h2>车端设备动作</h2></div><strong>同源安全编排</strong></header>
<div class="v2-extension-vehicle-grid" id="extensionVehicleGrid"></div>
</section>
</div>
<div class="v2-extension-run-plan" id="extensionRunPlan" hidden></div>
</section>`;
moduleSwitcher.insertAdjacentElement("afterend", root);
buildDialogs();
return true;
}
function buildDialogs() {
const layer = document.createElement("div");
layer.className = "v2-extension-dialog-layer";
layer.innerHTML = `
<dialog class="v2-extension-dialog" id="extensionAddDialog">
<form id="extensionAddForm">
<header><div><span class="section-kicker">NEW PERIPHERAL</span><h2>添加外设玩具</h2><p>选择类别后会自动建立一组待学习动作。</p></div><button type="button" data-close-extension-dialog="extensionAddDialog" aria-label="关闭"><i data-lucide="x"></i></button></header>
<div class="v2-extension-type-chooser" id="extensionTypeChooser"></div>
<label><span>玩具名称</span><input id="extensionHardwareName" maxlength="48" autocomplete="off" placeholder="例如:床头灯光玩具" required /></label>
<div class="v2-extension-preset-preview"><span>将建立的预设动作</span><div id="extensionPresetPreview"></div></div>
<button class="v2-extension-primary" type="submit">添加并开始学习</button>
</form>
</dialog>
<dialog class="v2-extension-dialog v2-extension-learn-dialog" id="extensionLearnDialog">
<header><div><span class="section-kicker">GSCU1B LEARNING</span><h2 id="extensionLearnTitle">学习遥控动作</h2><p id="extensionLearnPath">选择动作后开始</p></div><button type="button" data-close-extension-dialog="extensionLearnDialog" aria-label="关闭"><i data-lucide="x"></i></button></header>
<div class="v2-extension-learn-steps"><span data-extension-learn-step="1"><b>1</b>配置</span><i></i><span data-extension-learn-step="2"><b>2</b>接收</span><i></i><span data-extension-learn-step="3"><b>3</b>验证</span></div>
<section class="v2-extension-learn-page" data-extension-learn-page="1">
<div class="v2-extension-learn-target"><span id="extensionLearnIcon"><i data-lucide="radio"></i></span><div><small>目标玩具</small><strong id="extensionLearnDevice">--</strong></div><i data-lucide="arrow-right"></i><div><small>待学习动作</small><strong id="extensionLearnAction">--</strong></div></div>
<div class="v2-extension-learn-fields">
<label><span>说法与别名</span><input id="extensionLearnAliases" /></label>
<label><span>风险级别</span><select id="extensionLearnRisk"><option value="low">低</option><option value="medium">中</option><option value="high">高</option></select></label>
<label><span>红外槽位</span><input id="extensionLearnSlot" type="number" min="1" max="248" /></label>
</div>
<button class="v2-extension-primary" type="button" id="extensionContinueLearn">继续接收遥控信号</button>
</section>
<section class="v2-extension-learn-page v2-extension-learning-page" data-extension-learn-page="2" hidden>
<div class="v2-extension-learn-animation" id="extensionLearnAnimation"><span><i data-lucide="radio-tower"></i></span><i></i><i></i></div>
<h3 id="extensionLearnStatusTitle">准备接收遥控信号</h3><p id="extensionLearnStatusMessage">请将遥控器对准学习器并按一下对应按键。</p><strong id="extensionLearnCountdown">15</strong>
<div class="v2-extension-dialog-actions"><button type="button" id="extensionCancelLearn">取消</button><button class="v2-extension-primary" type="button" id="extensionStartLearn">开始接收</button></div>
</section>
<section class="v2-extension-learn-page v2-extension-test-page" data-extension-learn-page="3" hidden>
<span><i data-lucide="badge-check"></i></span><h3 id="extensionTestTitle">信号已捕获</h3><p id="extensionTestMessage">先发射测试,确认玩具有正确反应后再保存。</p>
<div class="v2-extension-dialog-actions"><button type="button" id="extensionTestEmit">发射试一下</button><button class="v2-extension-primary" type="button" id="extensionConfirmLearn" disabled>确认并完成学习</button></div>
</section>
</dialog>
<dialog class="v2-extension-dialog v2-extension-reset-dialog" id="extensionResetDialog">
<header><div><span class="section-kicker">RESET LEARNING</span><h2>重置学习</h2><p>将擦除学习器并把所有动作恢复为待学习。</p></div><button type="button" data-close-extension-dialog="extensionResetDialog" aria-label="关闭"><i data-lucide="x"></i></button></header>
<label><span>输入“清空”确认操作</span><input id="extensionResetConfirm" autocomplete="off" /></label>
<button class="v2-extension-danger" type="button" id="extensionConfirmReset" disabled>擦除并重新开始</button>
</dialog>
<div class="v2-extension-toast-stack" id="extensionToastStack" aria-live="polite"></div>`;
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) {
throw new Error(payload.message || `自扩展请求失败(HTTP ${response.status})`);
}
return payload;
}
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();
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
? `<select data-extension-device-select="${typeKey}" aria-label="选择${type.label}">${devices.map((device) => `<option value="${escapeHtml(device.id)}" ${device.id === selected?.id ? "selected" : ""}>${escapeHtml(device.name)}</option>`).join("")}</select>`
: `<span class="v2-extension-no-device">尚未添加具体硬件</span>`;
return `<article class="v2-extension-type-card" data-extension-type="${typeKey}" data-extension-connected="${Boolean(selected)}">
<header><span class="v2-extension-type-icon"><i data-lucide="${type.icon}"></i></span><div><small>${type.label}</small>${selector}</div><strong>${learned}/${capabilities.length}</strong><button type="button" data-add-type="${typeKey}" title="添加${type.label}" aria-label="添加${type.label}"><i data-lucide="plus"></i></button>${selected ? `<button type="button" data-delete-extension-device="${escapeHtml(selected.id)}" title="删除当前玩具" aria-label="删除当前玩具"><i data-lucide="trash-2"></i></button>` : ""}</header>
<div class="v2-extension-capability-grid">${visible.map((capability) => renderCapability(capability, typeKey, Boolean(selected))).join("")}</div>
<footer><span>${devices.length} 台硬件 · ${capabilities.length} 个动作</span><div><button type="button" data-extension-page="${typeKey}" data-page-direction="-1" ${page === 0 ? "disabled" : ""}><i data-lucide="chevron-left"></i></button><b>${page + 1}/${totalPages}</b><button type="button" data-extension-page="${typeKey}" data-page-direction="1" ${page >= totalPages - 1 ? "disabled" : ""}><i data-lucide="chevron-right"></i></button></div></footer>
</article>`;
}
function renderCapability(capability, typeKey, hasDevice) {
const learned = capability.status === "learned" && validSlot(capability.slot);
if (!hasDevice) {
return `<button class="v2-extension-capability preview" type="button" data-add-type="${typeKey}"><span>${escapeHtml(capability.name)}</span><small>添加硬件后可学习</small><i>添加</i></button>`;
}
return `<div class="v2-extension-capability ${learned ? "learned" : "pending"}">
<button type="button" ${learned ? `data-run-extension-capability="${escapeHtml(capability.id)}"` : `data-learn-extension-capability="${escapeHtml(capability.id)}"`}><span>${escapeHtml(capability.name)}</span><small>${learned ? `IR ${String(capability.slot).padStart(3, "0")} · 已学习` : "待学习"}</small></button>
<div>${learned ? `<button type="button" data-run-extension-capability="${escapeHtml(capability.id)}">执行</button><button type="button" data-forget-extension-capability="${escapeHtml(capability.id)}" aria-label="忘记动作"><i data-lucide="x"></i></button>` : `<button type="button" data-learn-extension-capability="${escapeHtml(capability.id)}">学习</button>`}</div>
</div>`;
}
function renderControl() {
const learned = learnedCapabilities();
$("#extensionLearnedTotal").textContent = `${learned.length} 项可执行`;
if (!learned.length) {
$("#extensionLearnedGrid").innerHTML = `<div class="v2-extension-empty"><i data-lucide="book-open-check"></i><strong>还没有已学习动作</strong><span>切换到学习模式,为外设玩具录入遥控能力。</span><button type="button" data-switch-extension-mode="study">前往学习</button></div>`;
} 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 `<button class="v2-extension-learned-action" type="button" data-run-extension-capability="${escapeHtml(capability.id)}" data-extension-type="${escapeHtml(device?.type || "other")}"><span><i data-lucide="${type.icon}"></i></span><div><small>${escapeHtml(device?.name || type.label)}</small><strong>${escapeHtml(capability.name)}</strong></div><i data-lucide="send"></i></button>`;
}).join("");
}
$("#extensionVehicleGrid").innerHTML = VEHICLE_DEVICES.map((device) => `
<article class="v2-extension-vehicle-card"><header><span><i data-lucide="${device.icon}"></i></span><div><strong>${device.name}</strong><small>${device.code}</small></div></header><div>${device.actions.map((action) => `<button type="button" data-extension-vehicle-command="${escapeHtml(action.command)}" title="${escapeHtml(action.command)}">${escapeHtml(action.name)}</button>`).join("")}</div></article>`).join("");
}
function renderTypeChooser() {
$("#extensionTypeChooser").innerHTML = Object.entries(TOY_TYPES).map(([key, type]) => `
<button class="${key === selectedAddType ? "active" : ""}" type="button" data-extension-type-choice="${key}" data-extension-type="${key}"><span><i data-lucide="${type.icon}"></i></span><div><strong>${type.shortLabel}</strong><small>${type.description}</small></div></button>`).join("");
$("#extensionPresetPreview").innerHTML = TOY_TYPES[selectedAddType].presets
.map((item) => `<i>${escapeHtml(item.name)}</i>`)
.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 = `<i data-lucide="${type.icon}"></i>`;
$("#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) => ({
name: device.name,
code: device.code,
actions: device.actions,
}));
}
function hydratePlan(steps) {
return (steps || []).map((step) => {
if (step.kind === "vehicle") {
return {
kind: "vehicle",
command: step.command,
label: step.label || step.command,
risk: step.risk || "medium",
};
}
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,
)) return null;
return {
kind: "peripheral",
device,
capability,
label: `${device.name} · ${capability.name}`,
risk: capability.risk || "low",
};
}).filter(Boolean);
}
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) => `
<span class="${index < active ? "done" : index === active ? "active" : ""}"><i>${String(index + 1).padStart(2, "0")}</i>${escapeHtml(step.label || "待执行动作")}</span>`).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;
}
}
async function executeVehicle(command) {
return api("vehicle/execute", {
method: "POST",
body: JSON.stringify({ command, vehicle_id: "denza_n7" }),
});
}
async function executePlan(plan) {
if (isExecuting || !plan.length) return;
isExecuting = true;
renderRunPlan(plan);
let completed = 0;
let vehicleDryRun = false;
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 === "vehicle") {
const result = await executeVehicle(step.command);
vehicleDryRun ||= result.execution_mode === "dry_run";
} 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;
}
if (index < plan.length - 1) await delay(state.settings.actionDelayMs);
}
renderRunPlan(plan, completed);
if (completed === plan.length) {
setExecutionState(
"success",
"联动计划已全部处理",
vehicleDryRun ? "外设已发射,车端处于安全演练模式" : "外设与车端链路处理完成",
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 refreshHealth(showMessage = false) {
$("#extensionStudyHealth").dataset.state = "checking";
$("#extensionStudyHealthState").textContent = "CHECK";
$("#extensionVehicleCard").dataset.state = "checking";
$("#extensionVehicleState").textContent = "CHECK";
try {
healthState = await api("health");
} catch (error) {
healthState = {
connected: false,
transport: "unavailable",
message: error.message,
gpt: { configured: false },
vehicle: { connected: false },
};
}
const connected = Boolean(healthState.connected);
const transport = String(healthState.transport || "--").toUpperCase();
const connectionLabel = connected
? (healthState.transport === "simulation" ? "模拟学习器已就绪" : `${transport} 学习器已连接`)
: "GSCU1B 尚未连接";
$("#extensionStudyHealthText").textContent = connectionLabel;
$("#extensionStudyHealthState").textContent = connected ? "READY" : "OFFLINE";
$("#extensionStudyHealth").dataset.state = connected ? "online" : "offline";
const vehicleConnected = Boolean(healthState.vehicle?.connected);
$("#extensionVehicleState").textContent = vehicleConnected ? "READY" : "OFFLINE";
$("#extensionVehicleCard").dataset.state = vehicleConnected ? "online" : "offline";
$("#extensionConnectionText").textContent = 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 (showMessage) {
const ready = [connected, healthState.gpt?.configured, healthState.vehicle?.connected]
.filter(Boolean).length;
showToast(`自扩展链路:${ready} / 3 项就绪。`, ready === 3 ? "success" : "error");
}
return healthState;
}
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);
});
$("#extensionVehicleGrid").addEventListener("click", (event) => {
const action = event.target.closest("[data-extension-vehicle-command]");
if (!action) return;
$("#extensionUtterance").value = action.dataset.extensionVehicleCommand;
executeInstruction(action.dataset.extensionVehicleCommand);
});
$("#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();
}
});
}
function init() {
if (!buildShell()) return;
bindEvents();
renderAll();
setMode(activeMode);
if (document.body.dataset.module === "self-extension") refreshHealth();
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
}
document.addEventListener("DOMContentLoaded", init);
}());