diff --git a/README.md b/README.md index abc440d..2cdc0bb 100755 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ 面向车载执行器的认知编排 Agent 单仓工程。系统把自然语言请求转换为可校验的语义配置,再由确定性的领域插件编译为 CAN 命令计划,最后经安全策略和执行器下发。 +启动前端:PYTHONPATH=src:domains/seat/src .omx/python311-venv/bin/python -m cognitive_agent.server + ## 建设阶段 - 阶段一:每个领域独立完成能力定义、功能配置生成、配置校验、CAN 编译和单执行器测试。 diff --git a/apps/web-console/app.js b/apps/web-console/app.js index 0a77b38..5853616 100755 --- a/apps/web-console/app.js +++ b/apps/web-console/app.js @@ -14,6 +14,7 @@ const state = { provider: "openai", executionMode: "dry_run", conversationId: loadConversationId(), + vehicleId: null, runtime: null, result: null, progressTimer: null, @@ -37,8 +38,8 @@ const TWIN_DEFAULT_POSITIONS = { legrest_extension: 0, }; const TWIN_ANGLE_RANGES = { - backrest_recline: [95, 140], - legrest_height: [95, 160], + backrest_recline: [95, 150], + legrest_height: [95, 163], }; function initJointTwinLayout() { @@ -2436,11 +2437,132 @@ function selectExecutionMode(mode) { refreshIcons(); } +function vehicleProfiles() { + return Array.isArray(state.runtime?.vehicles) ? state.runtime.vehicles : []; +} + +function findVehicleProfile(vehicleId) { + return vehicleProfiles().find((vehicle) => vehicle.id === vehicleId) + || state.runtime?.vehicle + || null; +} + +function applyTwinAngleRanges(vehicle) { + Object.keys(TWIN_ANGLE_RANGES).forEach((axis) => delete TWIN_ANGLE_RANGES[axis]); + const motors = vehicle?.seat?.motors || {}; + Object.entries(motors).forEach(([axis, motor]) => { + const display = motor?.display_angle; + if (!display) return; + const zero = Number(display.zero_deg); + const maximum = Number(display.max_deg); + if (Number.isFinite(zero) && Number.isFinite(maximum) && maximum > zero) { + TWIN_ANGLE_RANGES[axis] = [zero, maximum]; + } + }); +} + +function selectVehicle(vehicleId, { silent = false } = {}) { + const vehicle = findVehicleProfile(vehicleId); + if (!vehicle) return; + state.vehicleId = vehicle.id; + const select = $("#vehicleSelect"); + if (select) select.value = vehicle.id; + applyTwinAngleRanges(vehicle); + const longitudinal = vehicle.seat?.motors?.longitudinal; + const backrest = vehicle.seat?.motors?.backrest_recline; + $("#vehicleNote").textContent = longitudinal && backrest + ? `${vehicle.name} · 水平 ${longitudinal.range}${longitudinal.unit} · 靠背 ${backrest.range}${backrest.unit}` + : `${vehicle.name} · 已加载车型配置`; + if (state.result?.actuator_state) { + renderSeatState(state.result.actuator_state); + } else if (state.seatTwin?.actuatorState) { + updateSeatTwin(state.seatTwin.actuatorState); + } + if (!silent) showToast(`已切换车型:${vehicle.name}`); +} + +function populateVehicleSelector() { + const select = $("#vehicleSelect"); + if (!select) return; + const vehicles = vehicleProfiles(); + select.innerHTML = vehicles.length + ? vehicles.map((vehicle) => ``).join("") + : ''; + const defaultVehicleId = state.runtime?.default_vehicle_id + || state.runtime?.vehicle?.id + || vehicles[0]?.id + || ""; + select.disabled = vehicles.length === 0; + if (defaultVehicleId) selectVehicle(defaultVehicleId, { silent: true }); +} + +function modeProfiles() { + return Array.isArray(state.runtime?.modes) ? state.runtime.modes : []; +} + +function modeChipLabel(mode) { + const name = String(mode?.name || mode?.id || "模式"); + return name.endsWith("模式") && name.length > 2 + ? name.slice(0, -2) + : name; +} + +function populateModeChips() { + const group = $(".mode-chips"); + if (!group) return; + const buttons = () => $$("button", group); + modeProfiles().forEach((mode) => { + const label = modeChipLabel(mode); + let button = buttons().find((item) => item.dataset.modeProfileId === mode.id) + || buttons().find((item) => item.textContent.trim() === label); + if (!button) { + button = document.createElement("button"); + button.type = "button"; + const icon = document.createElement("i"); + icon.dataset.lucide = mode.icon || "bookmark"; + button.append(icon, document.createTextNode(label)); + group.append(button); + } + button.dataset.modeProfileId = mode.id; + button.dataset.modeCommand = `进入${mode.name}`; + button.title = mode.description || mode.name; + }); + refreshIcons(); +} + +function applyModeCommand(command) { + if (!command) return; + $("#utterance").value = command; + $("#requestForm").requestSubmit(); +} + +function toggleSaveModeComposer() { + const composer = $("#saveModeComposer"); + composer.hidden = !composer.hidden; + if (!composer.hidden) $("#saveModeName").focus(); +} + +function saveCurrentModeProfile() { + const input = $("#saveModeName"); + const rawName = input.value.trim(); + if (!rawName) { + showToast("请输入模式名称"); + return; + } + const modeName = rawName.endsWith("模式") ? rawName : `${rawName}模式`; + $("#utterance").value = `这个模式不错,固化为${modeName}`; + $("#saveModeComposer").hidden = true; + input.value = ""; + $("#requestForm").requestSubmit(); +} + async function loadRuntime() { try { const response = await fetch("/api/runtime", { cache: "no-store" }); if (!response.ok) throw new Error(`HTTP ${response.status}`); state.runtime = await response.json(); + populateVehicleSelector(); + populateModeChips(); const hardware = state.runtime.hardware || {}; const badge = $("#modelBadge"); const gptButton = $('[data-provider="openai"]'); @@ -3099,13 +3221,16 @@ function renderExecution(result) { const reportSucceeded = report.spec.status === "succeeded"; summary.className = `execution-summary ${reportSucceeded ? "succeeded" : "failed"}`; const presetSave = Boolean(result.preset_config); + const modeSave = Boolean(result.mode_profile); const physical = report.spec.mode === "hil"; const executedDomains = [...new Set(report.spec.results.map((item) => item.domain))]; const motionCount = report.spec.results.filter((item) => item.telemetry?.motion_axis).length; const firmwareResult = report.spec.results.find((item) => item.telemetry?.firmware_verified); const operationResults = report.spec.results.filter((item) => item.ref !== "seat-controller-firmware"); - let executionDetail = presetSave - ? "当前五轴姿态已持久化到会话预设仓库,未生成 CAN 命令" + let executionDetail = modeSave + ? `长期模式 ${result.mode_profile.name} 已写入配置库,未生成 CAN 命令` + : presetSave + ? "当前五轴姿态已持久化到会话预设仓库,未生成 CAN 命令" : physical ? `${executedDomains.join(" / ")} 已下发真实 CAN,并以 IAR 固件实际反馈确认 ${operationResults.filter((item) => item.status === "succeeded").length} 个步骤` : `${executedDomains.join(" / ")} 共 ${report.spec.results.length} 个操作已完成 CAN 编码与虚拟执行`; @@ -3132,7 +3257,7 @@ function renderExecution(result) { ? "真实台架未返回满足目标的反馈,移动链路已尝试发送 STOP_ALL。" : "真实台架未返回满足目标的物理反馈。"; } - summary.innerHTML = `
${reportSucceeded ? (presetSave ? "姿态预设已保存" : (physical ? "真实台架执行成功" : "虚拟执行成功")) : "真实台架执行失败"}${executionDetail}
`; + summary.innerHTML = `
${reportSucceeded ? (modeSave ? "长期模式已保存" : presetSave ? "姿态预设已保存" : (physical ? "真实台架执行成功" : "虚拟执行成功")) : "真实台架执行失败"}${executionDetail}
`; $("#resultNavMeta").textContent = reportSucceeded ? `${report.spec.results.length} 项完成 · ${physical ? "HIL" : "DRY-RUN"}` : "执行失败 · 查看回读"; @@ -3171,7 +3296,9 @@ function renderResult(result) { ambient_light: "氛围灯", }; $("#resultTitle").textContent = executionSucceeded - ? result.preset_config + ? result.mode_profile + ? "长期模式配置已保存" + : result.preset_config ? "座椅姿态预设已保存" : result.scene_config ? `${domains.length} 域场景闭环完成` @@ -3192,6 +3319,7 @@ function renderResult(result) { renderDomainState(result.domain_state); updateTwinExplicitAngles(result); renderSavedPresets(result); + if (result.mode_profile) loadRuntime(); renderCode(result); renderExecution(result); window.dispatchEvent(new CustomEvent("cognitive-agent:result", { detail: result })); @@ -3226,6 +3354,7 @@ async function submitRequest(event) { utterance, provider: state.provider, conversation_id: state.conversationId, + vehicle_id: state.vehicleId || state.runtime?.default_vehicle_id, execution_mode: state.executionMode, ...(state.executionMode === "hardware" ? { approval_token: approvalToken } @@ -3352,6 +3481,22 @@ document.addEventListener("DOMContentLoaded", async () => { }); $("#newConversation").addEventListener("click", startNewConversation); $("#logoutButton").addEventListener("click", logout); + $("#vehicleSelect").addEventListener("change", (event) => { + selectVehicle(event.target.value); + }); + $(".mode-chips").addEventListener("click", (event) => { + const target = event.target instanceof Element ? event.target : null; + const button = target?.closest("button[data-mode-command]"); + if (button) applyModeCommand(button.dataset.modeCommand); + }); + $("#saveModeToggle").addEventListener("click", toggleSaveModeComposer); + $("#saveModeConfirm").addEventListener("click", saveCurrentModeProfile); + $("#saveModeName").addEventListener("keydown", (event) => { + if (event.key === "Enter" && !event.isComposing) { + event.preventDefault(); + saveCurrentModeProfile(); + } + }); $$(".segment[data-provider]").forEach((button) => button.addEventListener("click", () => selectProvider(button.dataset.provider))); $$('[data-execution-mode]').forEach((button) => button.addEventListener("click", () => selectExecutionMode(button.dataset.executionMode))); $$(".example-command").forEach((button) => button.addEventListener("click", () => { diff --git a/apps/web-console/index.html b/apps/web-console/index.html index ba1887e..f1bca97 100755 --- a/apps/web-console/index.html +++ b/apps/web-console/index.html @@ -10,7 +10,7 @@ - + @@ -51,7 +51,7 @@ @@ -95,6 +95,17 @@

主驾和副驾暂时共享同一组实体电机、加热与通风执行器

+
+ +
+ + +
+

正在读取座椅量程与角度标定

+
+