59 Commits

Author SHA1 Message Date
Miayui 4a31950023 fix(web-console): reconcile merged mode interactions 2026-09-16 15:15:33 +08:00
Miayui d74d039044 fix(web-console): align status cards and command history 2026-09-16 15:05:36 +08:00
Miayui 97d10c5fcd merge: sync feature/N7Web into feature/N7Web-Tech
# Conflicts:
#	apps/web-console/app.js
#	apps/web-console/assistant-chat.js
#	apps/web-console/index.html
#	apps/web-console/service-worker.js
#	src/cognitive_agent/server.py
#	tests/e2e/test_seat_web_api.py
#	tests/unit/test_assistant_frontend_contract.py
#	tests/unit/test_energy_monitor_frontend_contract.py
#	tests/unit/test_frontend_theme_contract.py
#	tests/unit/test_mode_scene_catalog.py
2026-09-16 15:03:54 +08:00
Miayui 0e4328f548 feat(n7web): enhance control workflows and status feedback 2026-09-16 12:52:07 +08:00
Miayui 84a3cd73bb feat(web-console): unify spatial UI and chat reset 2026-09-14 18:48:11 +08:00
Miayui 58f1a2da42 feat(n7web): add pet settings and day night themes 2026-09-14 17:12:08 +08:00
Miayui aa6f59d3a6 feat(n7web): add pet NFC workflow and glass UI 2026-09-14 15:16:02 +08:00
vee 3eeca2ea75 merge: integrate remote MQTT routing with seat-scoped dynamic modes 2026-09-12 15:10:16 +08:00
vee d8dad22ec0 feat: add seat-scoped static and dynamic assistant modes 2026-09-12 14:56:30 +08:00
vee d09b5967f8 fix: restore mode clicks and compose seat-aware assistant scenes 2026-09-12 13:33:38 +08:00
Miayui 812e67422d 优化后端 2026-09-11 17:56:56 +08:00
Miayui 7eed11b774 mqtt切换为远程服务器部署,优化简单指令解析时长 2026-09-11 10:26:23 +08:00
Miayui 3fc6f2c2a4 mqtt切换为远程服务器部署,优化简单指令解析时长 2026-09-11 10:22:24 +08:00
Miayui 51ca12b624 添加模式卡片下发 2026-09-09 14:37:25 +08:00
vee 76057be044 fix: execute mode cards through deterministic assistant route 2026-09-09 09:04:55 +08:00
vee 2af8b8f9ac feat: route mode cards through AI assistant 2026-09-09 08:32:51 +08:00
vee 93103c2579 feat: integrate production cockpit AI assistant 2026-09-08 17:46:07 +08:00
vee 5e228e12ae fix: harden conversational mode draft revisions 2026-09-08 00:55:45 +08:00
vee f0a248ede3 fix: support conversational mode draft updates 2026-09-07 23:59:42 +08:00
vee 6b2b644ee6 fix: compose named cockpit modes from capabilities 2026-09-07 20:52:19 +08:00
vee cf9b89508c fix: configure Codex provider authentication 2026-09-07 18:17:10 +08:00
vee 53d80ab662 test: cover assistant stream deployment failures 2026-09-07 17:54:15 +08:00
vee c96970f068 feat: 完善 N7 智能座舱交互与控制链路 2026-09-07 15:34:59 +08:00
minghanchen 32d179143f 优化语音输入/普通文本输入 2026-09-03 13:36:40 +08:00
minghanchen 009d8ca73e 优化后端和前端逻辑 2026-09-03 10:21:01 +08:00
minghanchen ccd64adcfe 优化的前端UI 2026-09-02 11:39:31 +08:00
minghanchen 00c7d6b874 merge: integrate remote speech capabilities into feature/N7Web 2026-09-01 17:01:50 +08:00
minghanchen cbf230182d 完善模式卡片、NFC及多设备控制流程 2026-09-01 16:24:26 +08:00
minghanchen ca26259e63 添加NFC绑定红外设备方式,融合车端设备和扩展设备统一控制 2026-09-01 11:04:59 +08:00
minghanchen 6d75944c0f 添加红外扩展 2026-08-28 17:27:43 +08:00
vee aa12135b22 feat: integrate Chinese speech understanding and capability routing 2026-08-28 15:19:44 +08:00
minghanchen fd13d9f1ee Merge branch 'feature/N7Web' of https://git.kendeji.fun/vee/Cognitive_Agent into feature/N7Web
# Conflicts:
#	apps/web-console/app.js
#	apps/web-console/index.html
#	apps/web-console/service-worker.js
#	src/cognitive_agent/server.py
#	tests/e2e/test_seat_web_api.py
2026-08-27 14:43:44 +08:00
minghanchen 4a0570706f 优化无线/有线CAN,以及控制指令解析 2026-08-26 20:11:09 +08:00
vee 2529b4896a feat: integrate NFC infrared peripherals and orchestration 2026-08-25 11:50:49 +08:00
minghanchen 9ac8fdface 优化了副驾座椅控制 2026-08-25 11:39:41 +08:00
minghanchen 33c92d1352 优化后端和前端 2026-08-24 14:15:53 +08:00
minghanchen 8d05815773 优化前端,优化前端指令,优化自定义控制 2026-08-24 09:40:16 +08:00
minghanchen fff7657fea merge: integrate wired and wireless CAN into N7Web 2026-08-19 10:21:27 +08:00
minghanchen 52c4dd08f9 添加无线模块 2026-08-19 09:59:36 +08:00
vee 772ab685d6 feat(web-console): refine intelligent cockpit sync interface 2026-08-19 08:36:38 +08:00
vee 9b24d5c8c9 feat(web-console): rebuild intelligent cockpit sync interface 2026-08-18 13:24:36 +08:00
vee 344f662970 feat(web-console): add personalized seat weighing evolution 2026-08-14 15:53:08 +08:00
vee f77dda70fe update 2026-08-14 15:01:29 +08:00
vee 6d768aaef4 update model 2026-08-14 11:17:17 +08:00
vee 1a96d39426 feat: default users to safe virtual CAN execution 2026-08-14 09:56:22 +08:00
vee 0ec43052f3 feat: refine vehicle console and self-repair workflow 2026-08-13 17:49:37 +08:00
vee fd3c219c39 feat: integrate safer vehicle CAN reasoning 2026-08-12 22:50:55 +08:00
vee 9ce64b3a01 feat: complete dual-seat whole-vehicle orchestration 2026-08-12 15:12:40 +08:00
vee 32379a1573 feat: synchronize CH1 vehicle control and feedback 2026-08-11 10:42:13 +08:00
vee 3701192a11 feat: validate real-bench seat orchestration and self-repair 2026-08-09 19:00:16 +08:00
vee 3bceb0d5a1 feat: integrate calibration extension and self-repair modules 2026-08-07 23:39:45 +08:00
vee 156b10fda1 feat: refactor vehicle and gesture control console 2026-08-07 09:18:17 +08:00
vee dc7c91800e feat: integrate cabin CAN and verified dynamic seat modes 2026-08-04 17:51:38 +08:00
vee 155fbb59ba docs: describe hardware approval gateway 2026-08-03 10:25:05 +08:00
vee a952f37ceb feat: add local hardware approval leases 2026-08-03 10:20:55 +08:00
vee 85b4740d29 feat: serialize public hardware execution 2026-08-03 09:25:38 +08:00
vee a6d974cb29 feat: tolerate recoverable seat intent inputs 2026-08-02 06:12:36 +08:00
vee 55d0d400cb feat: harden seat intent and expand HIL coverage 2026-08-02 05:20:38 +08:00
vee 4aded5c21d feat: update CAN integration and T2G seat firmware 2026-08-01 15:55:21 +08:00
9210 changed files with 3046953 additions and 7097 deletions
+12
View File
@@ -0,0 +1,12 @@
.git
.venv
.env
.env.auth
__pycache__
*.py[cod]
artifacts
output
tmp
tests
firmware
docs/product
+28
View File
@@ -4,4 +4,32 @@
__pycache__/
*.py[cod]
artifacts/runs/
artifacts/mode-profiles/
artifacts/server/
artifacts/tmp/
artifacts/self-repair/
artifacts/models/
artifacts/*.log
tmp/*.log
.codex_tmp/
/build/
/output/
/apps/web-console/node_modules/
/apps/web-console/dist/
/tmp/imagegen/
firmware/t2g/Seat_Controller_demo/Tests/manual_hil/logs/
firmware/t2g/Seat_Controller_demo/Tests/manual_hil/seat_function_runtime/
tools/zcan/seat_function_runtime/
artifacts/hil/*_server_*.log
artifacts/hil/frontend_service_*.log
tmp/can-matrix-artifact/
tmp/reference-project-code-CAN_DEV/
firmware/t2g/Seat_Controller_demo/IAR/settings/
firmware/t2g/Seat_Controller_demo/IAR/Seat_Controller_demo.eww
firmware/t2g/Seat_Controller_demo/IAR/rev_d/.Seat_Controller_demo_build_cache
firmware/t2g/Seat_Controller_demo/IAR/rev_d/.ninja_deps
firmware/t2g/Seat_Controller_demo/IAR/rev_d/.ninja_log
firmware/t2g/Seat_Controller_demo/IAR/rev_d/BrowseInfo/
firmware/t2g/Seat_Controller_demo/IAR/rev_d/List/
firmware/t2g/Seat_Controller_demo/IAR/rev_d/Obj/
firmware/t2g/tviibh8m/src/examples/crypto/ClientRunsOnCM7_0/RSA/key_data/private_key*.pem
+28 -5
View File
@@ -1,14 +1,37 @@
.PHONY: validate test dev dev-lan
.PHONY: validate test test-frontend dev dev-lan can-protocol-generate can-protocol-check can-read-trans can-read-trans-test
validate:
CAN_READ_TRANS_BUILD_DIR ?= build/can_read_trans
CAN_READ_TRANS_CANFD_ROOT ?= /home/orin/project511/project-code/canfd
TEST_PYTHONPATH := src:domains/_template/src:domains/ambient_light/src:domains/climate/src:domains/roof/src:domains/seat/src
can-protocol-generate:
python tools/generate_can_semantic_protocol.py
can-protocol-check:
python tools/generate_can_semantic_protocol.py --check
validate: can-protocol-check
python tools/validate_repository.py
test:
PYTHONPATH=src:domains/seat/src python -m unittest discover -s domains/seat/tests -p 'test_*.py'
PYTHONPATH=src:domains/seat/src python -m unittest discover -s tests -p 'test_*.py'
test-frontend:
python tools/run_node.py --test tests/unit/assistant_stream_runtime.test.cjs
python tools/run_node.py --test tests/unit/speech_input_runtime.test.cjs
python -m pytest tests/unit/test_energy_monitor_frontend_contract.py -q
test: can-protocol-check test-frontend
PYTHONPATH=$(TEST_PYTHONPATH)$${PYTHONPATH:+:$${PYTHONPATH}} python -m pytest
dev:
PYTHONPATH=src:domains/seat/src python -m cognitive_agent.server
dev-lan:
PYTHONPATH=src:domains/seat/src python -m cognitive_agent.server --host 0.0.0.0
can-read-trans: can-protocol-generate
cmake -S tools/can_read_trans -B $(CAN_READ_TRANS_BUILD_DIR) \
-DCMAKE_BUILD_TYPE=Release \
-DCANFD_ROOT=$(CAN_READ_TRANS_CANFD_ROOT)
cmake --build $(CAN_READ_TRANS_BUILD_DIR) --parallel
can-read-trans-test: can-read-trans
ctest --test-dir $(CAN_READ_TRANS_BUILD_DIR) --output-on-failure
+165 -6
View File
@@ -2,7 +2,41 @@
面向车载执行器的认知编排 Agent 单仓工程。系统把自然语言请求转换为可校验的语义配置,再由确定性的领域插件编译为 CAN 命令计划,最后经安全策略和执行器下发。
启动前端:PYTHONPATH=src:domains/seat/src .omx/python311-venv/bin/python -m cognitive_agent.server
Web 控制台与同源 API 由同一个 Python 服务提供。本地启动:
```bash
.\.venv\Scripts\python.exe .\tools\start_n7web.py
```
启动脚本会先安装缺失的前端依赖并执行 Vue 3 + Vite 生产构建,然后启动 `8000` 主服务和独立的 `8090` 只读模式卡服务。已经有可复用的 `apps/web-console/dist` 时,可使用 `--skip-web-build` 跳过本次构建。
### Vue 3 + Vite 前端
前端根应用位于 `apps/web-console/src/App.vue`,入口为 `src/main.js`。Vue 和 Vite 均由 npm 锁定版本管理,浏览器不再依赖 CDN 或全局 `window.Vue`。现有 MQTT、NFC、语音、模式卡和车辆控制脚本作为兼容运行层继续使用原 DOM 契约,由 Vue 根应用承载页面结构。
单独进行前端开发时,先保持 Python 主服务运行,再启动 Vite:
```powershell
cd .\apps\web-console
npm ci
npm run dev
```
开发页面为 `http://127.0.0.1:5173/`,`/api` 与 `/artifacts` 会代理到 `http://127.0.0.1:8000`。生成生产静态资源:
```powershell
cd .\apps\web-console
npm run build
```
构建输出位于 `apps/web-console/dist`;Python 主服务检测到该目录后会优先提供生产构建。上述流程不会改变主页的 `8000` 端口,也不会改变模式卡服务的 `8090` 端口。
Windows PowerShell 未安装 `make` 时可直接运行:
```powershell
$env:PYTHONPATH = "src;domains/seat/src"
python -m cognitive_agent.server
```
## 建设阶段
@@ -77,9 +111,11 @@ artifacts/runs/<request_id>/
## 可执行领域
座椅与空调等车身设备使用同一 ZLG CAN 盒双通道联动时,参见 [T2G 座椅与车身双通道 CAN 联动](docs/workflows/t2g-cabin-dual-channel-can.md)。
当前已接入四个相互隔离的可执行领域:
- 座椅:基于 `座椅部分功能CAN矩阵表.xlsx` 的主/副驾通风和加热,以及明确标识为 synthetic 的五轴位置电机演示。
- 座椅:所有热管理、位置移动和姿态预设能力仅面向副驾;未指定座位时默认副驾,显式主驾请求拒绝执行。
- 空调:基于 `空调-天幕-氛围灯CAN信号矩阵表.xlsx` 的电源、18–32℃ 分区温度、0–7 档风量、A/C、内外循环、风向和前除霜。
- 天幕:基于同一矩阵的 0–100% 透光率、后遮阳帘绝对位置和自动调光。
- 氛围灯:基于同一矩阵的总开关、24-bit RGB、0–100% 亮度和 ECU 场景模式。
@@ -92,11 +128,59 @@ make test
make dev
```
浏览器访问 `http://127.0.0.1:8000`。设置 `OPENAI_API_KEY` 后,GPT Structured Outputs 会基于各领域的 `config/capabilities.yaml` 和当前会话状态直接生成通用能力请求;后端只做结构校验、领域路由和确定性执行适配,不用本地语义规则改写 GPT 动作。本地解析器仅是未配置 GPT 或 `auto` 模式调用失败时的显式降级路径。模型由 `OPENAI_MODEL=gpt-5.6-sol` 指定;兼容 OpenAI Responses API 的服务可通过 `OPENAI_BASE_URL` 指定。无论使用哪种解析器,配置校验、CAN 编译和虚拟执行均为同一确定性路径。
浏览器访问 `http://127.0.0.1:8000`。设置 `OPENAI_API_KEY` 后,GPT Structured Outputs 会基于各领域的 `config/capabilities.yaml` 和当前会话状态直接生成通用能力请求;后端只做结构校验、领域路由和确定性执行适配,不用本地语义规则改写 GPT 动作。本地解析器用于未配置 GPT,或 GPT/`auto` 模式因 API Key、模型权限、网络或结构化响应异常而失败时的安全降级;结果会明确标记 `local_rules_fallback`。模型由 `OPENAI_MODEL=gpt-5.6-sol` 指定;兼容 OpenAI Responses API 的服务可通过 `OPENAI_BASE_URL` 指定。无论使用哪种解析器,配置校验、CAN 编译和虚拟执行均为同一确定性路径。
### 一句话、快捷模式与手势控制
### Codex AI 助手
首页左侧提供一句话控制,可同时描述座椅、空调、天幕和氛围灯目标;Enter 直接执行,Shift+Enter 换行。观影、午休和零重力是可直接调用的快捷模式,“保存当前”可把当前副驾驶姿态保存为命名模式。复杂跨域自然语言优先使用 GPT 解析,本地规则用于已明确覆盖的离线指令。
首页 AI 助手以长驻 `codex app-server --stdio` 为会话与推理核心。浏览器仍只连接同源 `/api/assistant/stream`,不会接触 Codex 协议或凭据;后端把 App Server 的 thread、turn 和 item 事件转换为 NDJSON 流。普通问题由 Codex 直接回答。涉及本工程能力时,`/api/assistant/capabilities`、回合上下文和只读 MCP 工具 `n7_vehicle.get_n7web_capabilities` 都从同一个运行时能力注册表生成,座舱模式、NFC 绑定红外外设及 CAN0/CAN1 执行绑定会随实际状态更新,不允许模型凭常识猜测。常见能力询问直接由本地权威目录回答,无需等待模型生成。
显式及复杂车辆需求通过唯一放行的 `n7_vehicle.execute_cockpit_request` MCP 工具进入工程。服务端把一次性工具令牌绑定到本回合原始指令;Codex 可以为原子设备控制提交结构化语义计划,但不能提交新的自然语言指令或原始 CAN。计划必须通过原文证据、能力白名单、目标和参数契约校验,本地确定性解析结果优先,之后仍进入既有 Schema、硬件授权、安全预检、互斥和 CAN 执行链。模型不能扩展用户未请求的控制,也不能覆盖执行模式和硬件授权。
“车上有点热”“车里有点冷”等与受限模式完整匹配、且不包含否定或知识问法的高置信度舒适请求,使用 N7Web 内部快速路由:分别归一化为空调开启、全车 23℃/26℃、2 档风量等确定性指令,绕过模型和 MCP 往返以尽快给出进度与结果。该快速路由只缩短意图理解路径,不绕过能力注册、Schema、执行模式、硬件授权、安全预检、互斥、CAN 编译或反馈确认;其他隐式、复合或含糊请求仍交给 Codex 判断,并受上述 MCP 边界约束。
能力目录中的 `service_surfaces` 仅用于显示语音输入、手势输入、NFC 绑定、座椅自标定和自修复等服务的运行状态,条目固定标记为 `assistant_access=informational`。这里的 `available=true` 只表示对应服务面可用,不表示 AI 助手可以调用它;可执行边界以 `capabilities`、`modes`、`learned_peripherals` 中的 `assistant_callable` 以及服务端再次校验结果为准。
本地运行前需要安装并登录固定兼容版本的 Codex CLI:
```bash
npm install --global @openai/codex@0.150.0-alpha.8
codex login status
make dev
```
也可以让 Codex 使用进程环境中的 `OPENAI_API_KEY`。未配置自定义地址时,N7Web 会在 App Server 没有缓存账户的情况下通过 API-key 登录;配置非官方 `OPENAI_BASE_URL` 时,会自动为线程注入完整的 OpenAI-compatible provider,使 Codex 确实请求该地址,而不是回落到 `api.openai.com`。助手默认选择低推理强度的 `gpt-5.6-luna`,可用 `CODEX_ASSISTANT_MODEL` 覆盖;`CODEX_ASSISTANT_REASONING_EFFORT` 默认为 `low`,`CODEX_ASSISTANT_TURN_TIMEOUT_SECONDS` 默认为 300 秒,`CODEX_ASSISTANT_WORKSPACE` 用来指定隔离的只读工作目录。当前工程的常用场景登记在 `config/scene_templates`:明确名称会直接做严格别名匹配;“适合/用于/用来……的功能模式”等用途描述会先通过受限语法提炼为简洁模式名,再做同样的严格匹配,不使用子串或相似度猜测。命中且没有同名已保存模式时,模板会在本地复制为新的待保存草稿,不调用 Codex,也不会修改内置模板;草稿仍会经过实时能力契约、完整性、CAN 编译和 dry-run 校验。未命中的长尾名称继续进入独立的单轮结构化 Scene Composer;该路径默认超时 45 秒,已验证的候选计划在当前进程内按车型和能力契约版本缓存 3600 秒,同场景跨用户并发请求会合并,但每个请求仍会独立经过实时安全校验。可分别通过 `CODEX_ASSISTANT_SCENE_TIMEOUT_SECONDS`、`CODEX_ASSISTANT_SCENE_CACHE_TTL_SECONDS`、`CODEX_ASSISTANT_SCENE_CACHE_MAX_ENTRIES` 和 `CODEX_ASSISTANT_SCENE_MAX_CONCURRENT_PLANS` 调整超时、缓存和并行规划上限(默认 8)。为避免兼容服务的 WebSocket 断流造成长时间重试,自定义 provider 默认关闭 WebSocket,并把模型流重试限制为一次;可通过 `CODEX_ASSISTANT_DISABLE_WEBSOCKETS` 和 `CODEX_ASSISTANT_STREAM_MAX_RETRIES` 调整。
启动采用失败关闭语义:CLI 缺失、App Server 握手失败、模型认证配置缺失/登录失败,或有效 MCP 配置未能收敛并验证为仅启用 `n7_vehicle` 时,`cognitive_agent.server` 会直接启动失败,不会继续提供一个看似可用的降级网页。进程启动后若 App Server 退出,`/api/health` 返回 `503`,`/api/assistant/status` 显示不可用;凭据被服务端撤销、模型权限、网络或生成失败等只有真实模型请求才能发现的问题,则由当前问答流返回 `assistant_model_failed`。这些情况都不会静默回退到旧的正则问答路由;本地能力快答和冷热快速控制仍只按各自严格的确定性条件工作。
App Server 的协议与审批字段以 [Codex App Server 官方文档](https://learn.chatgpt.com/docs/app-server) 和 [Codex 配置参考](https://learn.chatgpt.com/zh-Hans/docs/config-file/config-reference) 为准。线程级 `approvalPolicy=never` 与 MCP 工具级 `default_tools_approval_mode=approve` 作用于不同边界:前者禁止命令/文件修改请求进入交互审批,后者仅避免受限的 N7Web MCP 工具产生前端无法处理的第二次审批;真实执行授权仍完全由 N7Web 的一次性令牌、原文绑定和车辆安全链决定。
### AI 助手语音输入、快捷模式与手势控制
首页左侧的 Codex AI 助手接受普通问题、工程工作和车辆控制需求;Enter 发送,Shift+Enter 换行。观影、午休和零重力是可直接调用的快捷模式,“保存当前”可把当前副驾驶姿态保存为命名模式。能力快答和高置信度舒适请求走确定性快速链,其他请求由 Codex App Server 理解后再通过工程能力与安全边界执行。
语音入口分为两种明确模式:用户点击输入框右侧的麦克风按钮时,以 `manual` 模式立即开始本地中文录音,不要求也不剥离唤醒词;浏览器已获得麦克风权限、AI 助手处于展开状态且页面可见时,则自动进入低干扰的 `wake` 待机,只在识别到“你好,小迪”后展示并继续接收问题。首次使用仍需通过一次手动点击完成浏览器麦克风授权。前端优先选择 `TAKSTAR V2`,使用 `AudioWorklet` 在音频线程采集(旧浏览器才降级到 `ScriptProcessor`),并通过 `X-Speech-Input-Mode: manual|wake` 将两条链路显式隔离。
累积音频按节流窗口发送到同源 `/api/speech/preview`:手动模式直接把未语义清洗的实时转录显示在输入框,唤醒模式在命中前不改动输入框、命中后只显示唤醒词之后的内容,partial 阶段均不会提交或执行。浏览器检测到静音端点后立即停止音轨,再把 16 kHz PCM WAV 发送到 `/api/speech/transcribe` 生成权威 final。服务端使用 Silero VAD 保留句首/句尾保护区,SenseVoiceSmall 负责声学识别,统一 `TranscriptAssembler` 负责 final commit、顺序、重复回调和保守的相邻重叠消除。
最终响应把不同信任层明确拆开:`captured_text` 保留完整声学识别结果;`raw_text` 在 `manual` 模式保留完整原文,在 `wake` 模式仅剥离已确认的唤醒词;`assistant_text` 在保留普通问答的前提下处理重复、结巴、false start、修正、撤销、否定、多设备、多数字和小数,供 AI 助手输入框使用;`clean_text/control_text`、结构化 `commands`、`cancelled_fragments` 和 `execution_eligible` 继续作为严格车控旁路。未知能力或非车控内容不会被清空,也不会绕过 Codex 助手直接执行。听写本身不自动发送消息、不自动执行控制,也不保存录音。首次部署:
```bash
python -m pip install -e ".[speech]"
python tools/install_sensevoice_model.py
```
安装脚本同时校验并部署 `artifacts/models/silero-vad/silero_vad.onnx`。默认使用完整精度 SenseVoice、`language=zh`、ITN 和最多 8 个 CPU 线程;VAD 针对短会话使用阈值 `0.45`、最短语音 `0.16s`、结束静音 `0.65s`、最长分段 `12s`、前置 `0.25s`、后置 `0.35s`。可通过 `SPEECH_MODEL_*`、`SPEECH_LANGUAGE`、`SPEECH_USE_ITN`、`SPEECH_NUM_THREADS`、`SPEECH_PROVIDER` 与 `SPEECH_VAD_*` 覆盖;`SPEECH_WAKE_WORD_ENABLED` 控制唤醒门,`SPEECH_WAKE_WORDS` 使用 `|` 分隔主唤醒词和别名。
当前 sherpa-onnx 版本支持 SenseVoice 的 `hr_lexicon`/`hr_rule_fsts`,只有同时提供经过验证的文件时才会启用:`SPEECH_HR_LEXICON` 和 `SPEECH_HR_RULE_FSTS`。未对非 transducer 的 SenseVoice 使用 hotword decoding。可插拔 `RecoveryRecognizer` 默认关闭,只为将来让 uncertain case 使用二次识别,不会下载大型模型。
诊断时设置 `SPEECH_DEBUG=1` 输出按 `utteranceId`、segment 边界、raw final、拼接前后、理解结果、Intent 与阶段耗时组织的 JSON 日志;只有额外设置 `SPEECH_DEBUG_RESPONSE=1` 才把 trace 返回浏览器。前端访问参数 `?speechDebug=1`(或 localStorage 同名值)启用控制台日志,生产默认不打印。回归与指标基准:
```bash
pytest tests/unit/test_speech_transcription.py tests/unit/test_transcript_assembler.py
python tools/benchmark_speech_pipeline.py --audio-dir artifacts/tmp/speech-regression-audio
```
麦克风仅可用于安全来源:本机使用 `http://127.0.0.1:8000`,远程使用 HTTPS。
顶部“手势控制”打开同页抽屉。浏览器在本机完成 MediaPipe 推理,识别结果仍通过同一自然语言编排、安全校验与执行链路处理。运行必需资源已经纳入本工程:
@@ -111,6 +195,81 @@ config/gesture/default.yaml
同一局域网的其他电脑需要体验时,使用 `make dev-lan` 监听全部网络接口。服务启动日志会打印实际 LAN 地址,例如 `http://172.20.10.5:8000`。前端和 API 使用同源访问,不需要 CORS,也不会把 `OPENAI_API_KEY` 发送到浏览器。若没有启用 `.env.auth`,任何可达设备都能触发服务端 GPT 请求,因此未认证的 LAN 模式只应在可信网络内临时演示。
### N7 模式卡片局域网目录
服务正常启动时,会把当前车型可见的工程预设模式和用户自定义模式原子同步到相邻目录 `../scenses/demo-scenes.json`,并提供无需登录、只读的 `GET /n7/scenes`。YAML 模式库仍是唯一真源;创建、更新、删除、恢复模式都会刷新 JSON,原有模式执行和安全校验链路不变。JSON 保留 `n7-scenes` v1 的 `id/label/background_time/description/actions` 字段,同时在动作中保留完整的 `domain/capability/target_scope/operation/parameters`,因此座椅、空调、天幕和氛围灯配置不会在导出时丢失。可用 `N7_SCENE_CATALOG_PATH` 或启动参数 `--scene-catalog` 覆盖输出路径。
分阶段动态模式额外保留完整 `schedule` 和用于展示单轮阶段起点的 `preview_actions`,并标记 `execution.kind=server_schedule`。此类模式的旧版 `actions` 列表为空,避免不认识时间轴的 v1 客户端把多个阶段同时下发;必须通过主工程已认证、受控的模式执行链运行。静态模式和已有 `timeline` 格式不变。
主项目和客户端只读目录服务可以用一个监督脚本同时启动:
```powershell
.\.venv\Scripts\python.exe tools\start_n7web.py
```
脚本自动设置 `PYTHONPATH`,主页面默认仅监听 `127.0.0.1:8000`,模式卡片 HTTP/UDP 服务监听 `0.0.0.0:8090`。需要让可信局域网内的设备同时访问主页面时,使用 `--host 0.0.0.0`;可通过 `--port`、`--scene-port`、`--catalog` 和 `--server-id` 覆盖其他参数。任一子服务异常退出或按一次 `Ctrl+C` 时,脚本会停止另一个服务。
同步由模式注册表的创建、更新、删除、恢复和显式重载事件触发,不会持续扫描目录。每次变更只重新生成并原子替换一份小型 JSON;独立目录服务在收到 HTTP 请求时才读取它。因此空闲时的额外开销主要是一个等待请求的 Python 进程和一个 UDP 发现线程,而不是持续的 CPU 或磁盘轮询。
前端通过 `POST /api/mode-profiles` 新增模式卡片,或通过 `POST /api/mode-profiles/{id}` 修改模式卡控制流后,主服务会先完成 YAML 持久化和 `/n7/scenes` 原子同步,再从同一份目录投影中按模式 ID 提取一个 `scenes[]` 元素,并在 1 秒内以约 333 ms 间隔向 MQTT `from_pc/scenes` 发布 3 次。每帧都是完全相同的更新后单卡 JSON,不包含目录外层或其他已有卡片,QoS 沿用车辆 MQTT 配置且 `retain=false`。删除和页面刷新只更新原有持久化/目录链路,不会触发该 Topic。
检测到已绑定外设的 NFC 卡时,主服务会按英文稳定 `device_id` 对已学习且启用的红外能力分组,并向 MQTT `extension/toy` 发布 3 帧相同的设备能力消息(约在 0、333、666 ms 发布,QoS 1、`retain=false`)。`extension_name` 使用中文显示名,`actions[].target_scope` 使用英文 `device_id`;卡片持续停留不会重复发送,移开后再次检测才会重新发送。该消息仅广播能力目录,不会触发红外发射。
车端需要控制这些 TCP 红外外设时,向同一个车辆 MQTT Broker 的 `extension/ctl` 发布 QoS 1、`retain=false` 的 JSON。PC 主服务固定订阅该 Topic;消息外层必须且只能包含 `domain`、`operation_count`、`actions`,其中 `domain` 为 `self_extension`,`operation_count` 必须与动作数量一致(1–64),`actions` 使用 `/n7/scenes` 中的红外动作格式:
```json
{
"domain": "self_extension",
"operation_count": 2,
"actions": [
{
"type": "infrared_emit",
"at_ms": 0,
"domain": "self_extension",
"capability": "infrared.emit",
"target_scope": "toy-light-001",
"operation": "emit",
"parameters": {"action": "打开", "slot": 2}
},
{
"type": "infrared_emit",
"at_ms": 1000,
"domain": "self_extension",
"capability": "infrared.emit",
"target_scope": "toy-light-001",
"operation": "emit",
"parameters": {"action": "切换为蓝色", "slot": 5}
}
]
}
```
服务在执行首条动作前会对完整控制流做原子鉴权:每一项都必须与本机目录中“已学习、已启用、具有持久 NFC 绑定”的 `device_id + action + slot` 完全一致;NFC 卡此时无需仍在感应区。执行前还会逐项重新鉴权,以阻止排队期间已被禁用或删除的能力。动作按 `at_ms` 排序后通过本机 TCP 红外网关串行下发,相邻发射至少间隔 800 ms;任一动作失败即停止剩余控制流。为避免重连误执行,retained 消息和 MQTT `dup` 重投消息都会被拒绝,消息体上限为 256 KiB。接收、鉴权、排队、执行结果和拒绝原因可在 `GET /api/extension/health` 的 `extension_control` 字段查看。
如果只需单独启动 UDP 发现和只读目录服务,也可以在另一个 PowerShell 终端运行:
```powershell
$env:PYTHONPATH = "src;domains/seat/src"
.\.venv\Scripts\python.exe -m cognitive_agent.lan_scene_server --bind 0.0.0.0 --port 8090
```
客户端向 UDP `8090` 发送 `N7_SCENES_DISCOVER_V1`,收到 `N7_SCENES_V1` 后访问 `http://<服务端IP>:8090/n7/scenes`。独立服务每次请求都读取最新的 JSON,并为只读接口开放 CORS;它不提供模式创建、修改、删除或执行能力。
Linux 上使用 USBCANFD-200U 被动同步 CH1 车辆状态时,使用
`zlgcan-linux` 后端。USB 权限、60 MHz/500k/2M 位时序、只读配置和状态接口
验证见 [Linux ZLG 车辆状态接入](docs/workflows/linux-zlg-vehicle-state.md)。
有线 ZLG 与本机 NanoMQ 无线链路使用相同的通道角色:CAN0 只对应副驾座椅,
CAN1 对应空调、车窗、灯光、遮阳帘、主驾座椅等其余车辆状态和控制。车辆控制
后端不允许回退到 CAN0;MQTT 主题、原始 CAN 帧 JSON 合约和有线/无线共用安全
门禁见同一文档。
需要在终端独立核对双通道状态或下发一条明确的白名单语义命令时,可构建
`can_read_trans`。该程序由同一个 C++ 进程持有 ZLG 设备:CAN0 监听并控制副驾
座椅固件原生协议,CAN1 监听并控制其余车辆矩阵协议;默认只监听、默认不启用
适配器内部终端电阻,也不提供任意原始帧注入入口。`console --execute` 可在同一
设备会话中持续接收状态并交互下发 CAN0/CAN1 白名单命令。构建、命令示例、反馈
判定和安全限制见 [can_read_trans 双通道检测与控制](docs/workflows/can-read-trans.md)。
## 认证与公网体验
公网演示必须启用 `.env.auth` 中的服务端认证。密码使用 PBKDF2-SHA256 哈希保存;认证成功后签发有有效期的 HMAC 会话 Cookie,Cookie 使用 `HttpOnly` 和 `SameSite=Strict`,经 HTTPS 代理时自动增加 `Secure`。登录失败按来源 IP 限流,GPT 请求按体验账号限制每分钟调用次数。未登录访问页面会跳转 `/login`,未登录 API 请求返回 `401`。
@@ -134,4 +293,4 @@ PYTHONPATH=src python -m cognitive_agent.auth generate-secret
完整能力边界、矩阵解释和验收用例见 [座椅阶段一交付](docs/workflows/seat-stage1-delivery.md)。
已验证的 T2G 加热、通风和四电机能力已合并到仓库内 `firmware/t2g/Seat_Controller_demo`,并提供一条默认锁定的真实 HIL 路径。主驾与副驾目前映射到同一物理座椅;硬件请求会先做固件身份握手,可在明确启用后通过 IAR C-SPY 自动烧录,再发送 CAN 并等待真实回读。固件拓扑、IAR 构建、ZLG 32 位 bridge、CAN 接线、安全门禁和上电步骤见 [T2G 座椅真实硬件台架接入](docs/workflows/t2g-seat-hardware-bench.md)。
已验证的 T2G 加热、通风、四电机和原生动态模式能力已完整纳入 `firmware/t2g`,包括 IAR 工程及仓库内 `common/tviibh8m` 构建依赖。M0/M1/M2/M3 分别对应水平、坐盆、靠背和腿托;revision 13通过`0x306`反馈带斜向耦合补偿的木马模式与四轴全程联动动态摇篮的阶段/循环状态。木马模式围绕中间姿态在`[270,250,460,400]`和`[370,130,540,460]`之间连续俯仰往复:坐盆峰峰行程120 Hall、约18 mm垂直分量,水平峰峰行程100 Hall、90%巡航Duty,用额外水平行程抵消坐盆斜向运动约16.4 mm的反向水平分量;参数集中在`seat_dynamic_mode_config.h`并由N7长期库镜像。动态模式收到一次启动命令后由ECU连续循环,收到停止/全停命令或累计运行10分钟时停止;目标到位式阶段在底层堵转/无运动保护继续生效的前提下使用独立保护窗口。真实硬件请求先被动核对新版周期反馈与 480/374/917/647 Hall 量程,未检测到匹配固件时默认通过 IAR C-SPY 自动烧写并复验,再发送 CAN 并等待真实回读。当前前端、能力注册、编译、安全门禁和状态反馈只允许副驾 `passenger/front_right`;固件中的 `Driver_Seat_*` 仅是板端既有协议字段名。构建、ZLG bridge、接线、安全门禁、前端测试集和已知量程归零见 [T2G 座椅真实硬件台架接入](docs/workflows/t2g-seat-hardware-bench.md) 与 [前端能力测试集](docs/testing/t2g-seat-frontend-test-suite.md)。
+160 -7
View File
@@ -1,22 +1,90 @@
# Web Console
Web 控制台提供一句话、快捷功能模式与手势控制入口,不在浏览器中生成 CAN。所有动作统一提交给后端编排、配置校验、安全门禁和执行链路。
## 宠物卡片与 NFC 上报
主页“宠物卡片”区域支持创建多张宠物卡片。`pet_name` 与 `action` 可在卡片中编辑,
`nfc_id` 为只读字段,只能在“NFC 设备授权”弹窗中使用读卡器当前识别到的实体 NFC
完成绑定,不能由浏览器或 API 请求直接填写。同一个 NFC UID 最多绑定一张宠物卡片;重新
绑定时会自动解除旧卡片上的相同 UID。新卡片必须先完成编辑并点击保存,之后才能从
“NFC 设备绑定”或“关联卡片”入口继续绑定;右侧卡片区域不再直接触发 NFC 绑定。
卡片由后端持久化到 `artifacts/server/pet_cards.sqlite3`,网页关闭或服务重启不会丢失。
同源接口如下:
- `GET /api/extension/pets`:读取卡片及发布器状态。
- `POST /api/extension/pets`:创建卡片,只接受 `pet_name`、`action`。
- `PUT /api/extension/pets/{pet_id}`:修改宠物名和动作。
- `DELETE /api/extension/pets/{pet_id}`:删除卡片。
- `POST /api/extension/pets/{pet_id}/bind-current-nfc`:绑定读卡器中当前在场的 NFC。
- `POST /api/extension/pets/{pet_id}/unbind`:解除绑定。
服务端首次检测到已绑定 NFC 进入读卡区时,后台线程复用当前车辆 MQTT Broker 连接,
在 1 秒内向固定话题 `n7/pet` 发布 3 帧非保留消息,随后保持静默。只有将 NFC 移出读卡区
后再次靠近,才会触发下一组 3 帧。消息只包含以下三个字段:
```json
{"pet_name":"团团","nfc_id":"E113B203","action":"摇尾巴"}
```
发布行为独立于网页是否打开。若本次 3 帧中存在 Broker 发布失败,卡片与绑定关系仍会
保存,前端会显示本次突发发送失败;为避免持续发送,服务不会在 NFC 一直停留时无限重试,
移开并重新靠近后才会发起下一组 3 帧。
Web 控制台是车机端的远程自编排与同步显示界面。网页保留一句话指令和预设功能模式入口,请求通过同源服务发送至车机执行;同时同步显示车机任务、编排结果、设备反馈和 USB 扩展能力。真实 CAN 与硬件执行权限仍由服务端安全门禁控制。
当前页面包括:
- 一句话联动座椅、空调、天幕和氛围灯,支持 Enter 执行和 GPT/本地规则切换。
- 观影、午休、零重力快捷模式,以及当前副驾驶姿态的命名保存入口。
- 六阶段执行链路、整车联合孪生、领域状态、配置、CAN 计划和执行结果。
- 右侧手势控制抽屉,包含摄像头画面、关键点骨架、保持进度、动作速查与自定义映射。
- 自编排和自扩展合并为一个实时界面,登录身份固定为 `user`,不再提供模块切换。
- AI助手默认展开;左侧控制台、车端设备状态、外设与扩展能力默认收起。四个板块统一通过页面右上角的图标按钮展开或收回,主工作区随面板状态自适应。
- AI助手支持普通问答、工程能力核对和车控工作。能力问答读取运行时权威目录,高置信度冷热请求走确定性快速控制链,其他问题和复杂车控由 Codex App Server 流式处理;车控结果仍以自然语言和可审计的动作进度呈现。
- 网页端可输入一句话指令或点击预设模式,车机端同样可发起任务;两端任务与结果统一同步。
- AI助手语音入口支持双模式:手动点击麦克风按钮会立即以 `manual` 模式录音,不要求唤醒词;浏览器已授权麦克风、助手展开且页面可见时自动进入 `wake` 待机,只有听到“你好,小迪”(兼容无标点与字间停顿)后才展示并继续转录。AudioWorklet 采集后,服务端 SenseVoiceSmall 以节流预览返回未语义清洗的实时文字;静音端点立即停止采音,再由 Silero VAD、幂等 TranscriptAssembler 和理解层生成最终 `assistant_text`。普通问答、未知能力、否定和撤销表达均可回填,`commands` 仅作为受信任车控旁路,不能反向过滤助手输入。
- 整车数字孪生位于工作区中央,点击或悬停热点可查看车窗、遮阳帘、主/副驾座椅、阅读灯、氛围灯、前大灯和空调的完整状态。
- 车端设备状态与外设能力板块均可独立隐藏或恢复,面板状态在当前浏览器会话中保留。
- USB 外设接入后同步显示 GSCU 链路、外设名称、类型、已学习能力和最近一次跨设备联动。
- 左侧运行日志集中显示网页/车机任务、链路、设备与外设同步事件。
UI 只展示可审计的“决策摘要”和“处理轨迹”,不展示模型私有思维过程。
## AI助手接口
浏览器只使用同源接口,不直接连接 App Server、MCP 或模型服务:
- `GET /api/assistant/status`:返回 App Server 状态、可用性和当前会话数。
- `GET /api/assistant/capabilities`:返回由运行时注册表、模式目录、CAN 绑定和已学习外设共同生成的权威能力快照。`service_surfaces` 只提供语音、手势、NFC、自标定和自修复的状态信息,不代表助手可调用;执行资格以 `assistant_callable` 和服务端校验为准。
- `POST /api/assistant/stream`:提交 `message`、`conversation_id`、执行模式和当前车型等上下文,返回 `application/x-ndjson`。前端消费 `message.accepted`、`work.*`、`action.*`、`answer.delta`、`control.suggestions`、`answer.completed` 与 `error` 事件,以增量方式展示回复和控制进度。不支持的车控请求可随 `control.suggestions` 返回当前车型可执行的替代指令;前端去重后渲染为按钮卡片,点击会作为新指令沿同一受控链路提交。
`POST /api/internal/codex/vehicle-control` 是 App Server MCP 子进程专用的回环接口,要求一次性内部令牌且与原始用户文本绑定,不是浏览器或第三方集成接口。浏览器也不能通过响应内容扩大执行目标、切换执行模式或提交原始 CAN。
当前座椅阶段一控制台为无构建静态页面,由 `cognitive_agent.server` 同源提供 API 和资源。运行:
```bash
make dev
```
语音听写首次运行前安装运行时、官方完整精度模型和 Silero VAD:
```bash
python -m pip install -e ".[speech]"
python tools/install_sensevoice_model.py
```
录音只用于当前请求,不落盘;`captured_text` 保留完整声学结果,`raw_text` 在 `manual` 模式保留完整原文、在 `wake` 模式剥离唤醒词,`assistant_text` 是面向问答的轻清洗会话,`clean_text/control_text` 与结构化 Intent 则保持严格车控语义。前后端通过 `X-Speech-Input-Mode: manual|wake` 显式区分模式;可用 `SPEECH_WAKE_WORD_ENABLED=false` 关闭唤醒门,或用竖线分隔的 `SPEECH_WAKE_WORDS` 配置主唤醒词与别名。远程浏览器必须通过 HTTPS 才能访问麦克风。诊断可设置后端 `SPEECH_DEBUG=1` 并用 `?speechDebug=1` 打开前端日志,默认关闭。
Android PAD/模拟器联调使用:
```powershell
powershell -ExecutionPolicy Bypass -File .\tools\android_pad_dev.ps1
```
脚本会启动已配置的 Pixel Tablet AVD、建立 `adb reverse tcp:8000 tcp:8000`、
以真实硬件关闭/模拟台架开启的方式启动服务,并在 Android Chrome 中打开
`http://127.0.0.1:8000`。停止联调环境:
```powershell
powershell -ExecutionPolicy Bypass -File .\tools\android_pad_dev.ps1 -Stop
```
局域网演示使用:
```bash
@@ -25,13 +93,89 @@ make dev-lan
其他设备访问启动日志打印的 `http://<LAN-IP>:8000`。页面与后端 API 由同一服务提供,无需单独配置前端地址。
自扩展接口统一位于 `/api/extension/*`。红外模块默认使用已验证的 GSCU1B TCP
模式:Linux 端在 `0.0.0.0:4444` 监听,GSCU1B 作为 TCP Client 主动接入。
“外设与扩展能力”中的“TCP 设置”可修改监听 IP、端口并立即重启监听,配置持久化到
`artifacts/tmp/gscu_gateway.json`;同一弹窗可执行查询、学习、取消、发射、读取、
写入、软重启、WiFi 配网锁、擦除、恢复出厂和原始 JSON 调试动作。模块配置的
服务器地址必须填写 Linux 的实际局域网 IP,不能填写 `0.0.0.0`。
TCP 发射、取消、写入和设置类动作在操作系统确认写入连接后立即返回“已下发”,
不会因设备不回 ACK 而阻塞前端。固件 1.1.5 的学习动作同样不主动返回 ACK,因此
前端在学习命令下发后自动读取目标槽位,以实际 `ir_data` 回读确认学习完成;设备查询
和读取槽位仍等待带 `messageId` 的实际回执。
学习时可为每个 `1–248` 红外槽位填写信号名称;槽位与名称持久化在本机
`artifacts/server/gscu_ir_catalog.sqlite3` 数据库中,并在 TCP 动作调试区按槽位号
升序显示为小卡片。浏览器中已有的旧名称会自动迁移到数据库。单卡片删除操作只删除
本地目录记录,不会擦除设备红外码;已验证的设备协议仅支持全部擦除。全部擦除会先
弹出不可恢复确认,设备命令提交成功后才同步清空本地槽位数据库。
如需回退 MQTT,可设置 `GSCU_TRANSPORT=mqtt`,通过 `GSCU_MQTT_*`、
`GSCU_COMMAND_TOPIC` 和 `GSCU_RESPONSE_TOPIC` 指定 Broker 与话题;通过
`GSCU_TRANSPORT=simulation` 使用演示学习器。MQTT 依赖已包含在核心环境中;
NFC 串口依赖通过 `ir` 可选依赖安装。自扩展不再需要独立的 `8010` 前端服务。
## NFC 玩具识别与红外学习
统一页面的“外设与扩展能力”区域提供“感应玩具 NFC”入口。点击后将玩具上的
NFC 卡片靠近 PCR532/PN532(CH340/CH341 串口)读卡器:新卡可建立或绑定
玩具档案,并立即进入现有 GSCU1B 红外学习流程;已绑定卡片会自动识别玩具,
展示全部已学习/待学习动作,并可直接发射已验证动作。
安装串口支持并启动:
```bash
python -m pip install -e ".[ir]"
make dev
```
服务会优先复用本机 `ir-toy-learning` 的 MQTT NFC 桥(默认
`127.0.0.1:1883`),避免两个进程同时抢占同一个 `COM` 口;桥不可用时再自动选择
唯一的 CH340/CH341、PN532 或 PCR532 串口。可通过 `NFC_PORT=COM5` 指定串口,
或用 `NFC_MQTT_HOST`、`NFC_MQTT_PORT` 调整桥地址;`NFC_MQTT_DISABLED=true`
强制只使用串口,`NFC_DISABLED=true` 可完全禁用读卡线程。
首次加载时页面会从 `http://localhost:3001/n7web-profile-bridge.html` 安全迁移旧
`ir-toy-learning` 浏览器档案,保留工牌 UID、玩具编号、动作名称和 GSCU1B 槽位。
不同浏览器运行时,旧工作台会把不含红外原始波形的档案元数据同步到本机 MQTT
保留主题 `toy/ir/profiles`,当前服务同样会自动导入。迁移按档案与槽位幂等合并,
不会覆盖当前工程中已经建立的能力。NFC UID 与玩具档案的绑定保存在当前浏览器,
红外波形仍保存在 GSCU1B 的 1–248 号物理槽位。
TCP 模式下本服务是服务器,GSCU1B 是客户端:设备配网页中的“TCP服务器”
必须填写运行本服务的主机地址,“TCP端口”必须与 `GSCU_TCP_PORT` 一致。
如需绑定指定网卡地址,可设置 `GSCU_TCP_LISTEN_HOST`;网卡晚于服务就绪或
端口被短暂占用时,监听器会按 `GSCU_TCP_BIND_RETRY_SECONDS`(默认 1 秒)
自动重试。`/api/extension/health` 的 `listening` 表示服务器已监听,只有收到
真实设备会话后 `connected`/`peer_connected` 才会变为 `true`。
车机与网页同步使用以下同源接口:
- `GET /api/vehicle-display-sync`:网页轮询最新车机任务、结果、外设目录与扩展链路状态。
- `POST /api/requests`:车机执行时附带 `client_type: "head_unit"`;网页控制台附带 `client_type: "web_console"`。两类任务的执行结果都会写入同步快照。
- `POST /api/extension/display-sync`:车机刚识别 USB 外设时上报 `device` 与 `peripherals` 能力目录,无需等待首次执行。
- `POST /api/extension/assistant/plan` 和 `/api/extension/vehicle/execute`:联动规划与车端执行结果同样写入同步快照。
右上角“能耗监测”入口以独立固定页面展示 7 个当前可控车端设备的实时功率、
本次监测累计能耗、总功率趋势和分设备趋势。能耗链路优先接收
`cognitive-agent:power-telemetry` 事件,也兼容调用
`window.updateDevicePowerTelemetry(payload)` 的现有集成方式;载荷可使用
`devices.<device>.power_w` 或 `total_power_w`,并可附带 `energy_wh`、
`trip_energy_kwh` 或 `today_energy_kwh`。累计能耗独立保留,瞬时功率超过 15 秒
未更新时只会回退到状态估算,不会让已上报的车端累计值消失。每条遥测应显式提供
`source: "vehicle_power_sensor"`(实测)或 `source: "estimated"`(估算)。
当前 CAN 状态协议没有独立电流/
功率电表信号,因此未注入新鲜功率数据的设备会显示带 `≈` 和“状态估算”标签的
确定性实时估算,绝不标记为车端实测。真实功率通道接入后页面会逐设备自动切换为
“实测”,无需更换页面或曲线实现。
公网入口由后端统一认证,前端不保存密码或 API Key。未认证访问自动进入
`/login`,主控制台提供退出按钮。固定入口是
`https://511.kendeji.fun`,应用容器只监听服务器回环地址,再由 Caddy 提供
HTTPS。部署方式见
[`deploy/server/README.md`](../../deploy/server/README.md)。
页面支持 GPT/本地解析切换、六阶段流水线、主副驾热舒适、五轴位置状态和 3D 座椅孪生、GPT 结构化能力请求、决策与校验轨迹、FunctionConfig、CommandPlan、虚拟帧和 ExecutionReport。硬件执行在服务端固定关闭,不是仅靠前端按钮限制。
页面支持 GPT/本地解析切换、六阶段流水线、主副驾热舒适、五轴位置状态和 3D 座椅孪生、GPT 结构化能力请求、决策与校验轨迹、FunctionConfig、CommandPlan、虚拟帧和 ExecutionReport。真实硬件执行由服务端门禁控制,不是仅靠前端按钮限制;Android 联调脚本会显式关闭真实硬件执行。
## 手势资源与安全规则
@@ -42,7 +186,16 @@ assets/gesture/gesture_recognizer.task
../../config/gesture/default.yaml
```
后端提供 `/api/gesture/runtime` 和 `/api/gesture/model`,前端以二进制缓冲区加载模型,避免依赖外部 `seat_gesture_control` 目录。MediaPipe Web 运行时从 jsDelivr 按需加载;摄像头帧和手部关键点只在当前浏览器中处理。
后端提供 `/api/gesture/runtime` 和 `/api/gesture/model`,前端以二进制缓冲区加载模型,避免依赖外部 `seat_gesture_control` 目录。MediaPipe Web、Lucide 和 Three.js 均按固定版本保存在 `vendor/`,运行时不访问外部 CDN;摄像头帧和手部关键点只在当前浏览器中处理。
## PAD、PWA 与离线行为
- 竖屏 PAD 在 900 CSS px 以下切换为上下布局,横屏保留指令栏与结果区;触控目标最小 48 dp,并处理安全区和动态视口。
- 顶部连接徽标由 `/api/health` 周期探测驱动;GET 可有限重试,POST 控制请求不自动重试。
- Service Worker 缓存页面壳、固定版本依赖和按需访问的 3D/手势资源;`/api/requests` 等控制 API 始终走网络且不会写入缓存。
- 从 Android Chrome 的“安装应用”可进入 standalone PWA;首次完整在线加载后可离线打开控制台壳,实际编排仍要求 Windows 服务可达。
模拟台架需要 `COGNITIVE_AGENT_SIMULATED_HARDWARE_ENABLED=true`。选择“模拟台架”后可注入反馈超时、适配器断开、电机堵转、固件不匹配和部分反馈。结果会标记 `simulation=true`、`seat_feedback=simulated_hardware` 和 `physical_can_opened=false`,不会调用真实 CAN、ZLG 或 IAR。
安全规则:张掌保持 1 秒激活;动作保持 600 ms 且至少连续 8 帧才执行;换动作不重新锁止;握拳立即停止但不解除激活;3 秒无有效动作自动锁止;单次连续运动最多 1.5 秒;检测到多只手时不执行。自定义映射保存在浏览器 `localStorage`。
+6229 -533
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+897
View File
@@ -0,0 +1,897 @@
/* Independent whole-vehicle energy monitor -------------------------------- */
.console-v2 .energy-monitor-layer {
position: fixed;
z-index: 5000;
inset: 0;
display: grid;
place-items: center;
padding: 68px 20px 20px;
background: color-mix(in oklch, var(--background) 58%, transparent);
backdrop-filter: blur(6px) saturate(.9);
-webkit-backdrop-filter: blur(6px) saturate(.9);
overscroll-behavior: contain;
}
.console-v2 .energy-monitor-layer[hidden] {
display: none !important;
}
.console-v2 .energy-monitor-page {
width: min(1180px, 100%);
height: min(664px, 100%);
min-height: 0;
display: grid;
grid-template-rows: auto minmax(0, 1fr);
overflow: hidden;
color: var(--foreground);
background:
radial-gradient(circle at 7% 0%, color-mix(in oklch, var(--primary) 10%, transparent), transparent 30%),
color-mix(in oklch, var(--card) 97%, var(--background));
border: 1px solid color-mix(in oklch, var(--primary) 18%, var(--border));
border-radius: 22px;
box-shadow:
0 28px 70px color-mix(in oklch, var(--foreground) 22%, transparent),
0 4px 18px color-mix(in oklch, var(--primary) 10%, transparent),
inset 0 1px 0 color-mix(in oklch, var(--primary-foreground) 85%, transparent);
font-variant-numeric: tabular-nums;
animation: energy-monitor-enter 180ms ease-out both;
}
.console-v2 .energy-monitor-header {
min-height: 76px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px;
padding: 14px 18px 13px 20px;
border-bottom: 1px solid color-mix(in oklch, var(--primary) 10%, var(--border));
background: color-mix(in oklch, var(--card) 85%, transparent);
}
.console-v2 .energy-monitor-heading,
.console-v2 .energy-monitor-live-state,
.console-v2 .energy-monitor-heading > div,
.console-v2 .energy-monitor-heading > span,
.console-v2 .energy-monitor-live-label,
.console-v2 .energy-trend-card > header,
.console-v2 .energy-devices-card > header,
.console-v2 .energy-trend-card > footer,
.console-v2 .energy-device-identity small {
display: flex;
align-items: center;
}
.console-v2 .energy-monitor-heading {
min-width: 0;
gap: 12px;
}
.console-v2 .energy-monitor-heading > span {
width: 46px;
height: 46px;
flex: 0 0 46px;
justify-content: center;
color: var(--primary);
background: color-mix(in oklch, var(--primary) 10%, var(--card));
border: 1px solid color-mix(in oklch, var(--primary) 22%, var(--border));
border-radius: 15px;
box-shadow: inset 0 1px 0 color-mix(in oklch, var(--primary-foreground) 70%, transparent);
}
.console-v2 .energy-monitor-heading > span svg {
width: 24px;
height: 24px;
stroke-width: 1.8;
}
.console-v2 .energy-monitor-heading > div {
min-width: 0;
align-items: flex-start;
flex-direction: column;
gap: 3px;
}
.console-v2 .energy-monitor-heading h2,
.console-v2 .energy-trend-card h3,
.console-v2 .energy-devices-card h3 {
margin: 0;
color: var(--heading-foreground, var(--foreground));
letter-spacing: -.025em;
}
.console-v2 .energy-monitor-heading h2 {
font-size: 20px;
line-height: 1.2;
}
.console-v2 .energy-monitor-heading p,
.console-v2 .energy-trend-card header p,
.console-v2 .energy-devices-card header p {
margin: 0;
color: var(--muted-foreground);
font-size: 12px;
line-height: 1.45;
}
.console-v2 .energy-monitor-live-state {
flex: 0 0 auto;
gap: 10px;
color: var(--muted-foreground);
font-size: 12px;
}
.console-v2 .energy-monitor-live-label {
gap: 6px;
color: var(--primary);
font-weight: 700;
}
.console-v2 .energy-monitor-live-label > i,
.console-v2 .energy-trend-card > footer span > i {
width: 7px;
height: 7px;
display: block;
background: var(--primary);
border-radius: 50%;
box-shadow: 0 0 0 4px color-mix(in oklch, var(--primary) 12%, transparent);
}
.console-v2 .energy-monitor-source {
padding: 5px 9px;
color: var(--primary);
background: color-mix(in oklch, var(--primary) 9%, var(--card));
border: 1px solid color-mix(in oklch, var(--primary) 18%, var(--border));
border-radius: 999px;
font-weight: 700;
}
.console-v2 .energy-monitor-source[data-source="simulated"],
.console-v2 .energy-monitor-source[data-source="simulated_estimated"],
.console-v2 .energy-monitor-source[data-source="measured_simulated"],
.console-v2 .energy-monitor-source[data-source="measured_simulated_estimated"] {
color: color-mix(in oklch, var(--primary) 72%, #7c3aed);
}
.console-v2 #energyMonitorClose {
width: 38px;
height: 38px;
display: grid;
flex: 0 0 38px;
place-items: center;
padding: 0;
color: var(--foreground);
background: color-mix(in oklch, var(--card) 88%, var(--secondary));
border: 1px solid color-mix(in oklch, var(--primary) 13%, var(--border));
border-radius: 12px;
box-shadow: 0 4px 12px color-mix(in oklch, var(--foreground) 7%, transparent);
cursor: pointer;
transition: color 150ms ease, border-color 150ms ease, background 150ms ease, transform 150ms ease;
}
.console-v2 #energyMonitorClose:hover,
.console-v2 #energyMonitorClose:focus-visible {
color: var(--primary);
background: var(--accent);
border-color: color-mix(in oklch, var(--primary) 28%, var(--border));
outline: 3px solid color-mix(in oklch, var(--ring) 16%, transparent);
outline-offset: 1px;
transform: translateY(-1px);
}
.console-v2 #energyMonitorClose svg {
width: 18px;
height: 18px;
}
.console-v2 .energy-monitor-content {
min-height: 0;
display: grid;
grid-template-rows: auto minmax(0, 1fr);
gap: 12px;
padding: 14px 16px 16px;
}
.console-v2 .energy-monitor-kpis {
display: grid;
grid-template-columns: 1.18fr 1fr 1fr 1fr;
gap: 10px;
}
.console-v2 .energy-kpi {
min-width: 0;
min-height: 94px;
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
gap: 5px;
overflow: hidden;
padding: 13px 15px;
background: color-mix(in oklch, var(--card) 94%, var(--secondary));
border: 1px solid color-mix(in oklch, var(--primary) 11%, var(--border));
border-radius: 15px;
box-shadow: 0 6px 16px color-mix(in oklch, var(--foreground) 5%, transparent);
}
.console-v2 .energy-kpi-primary {
background:
linear-gradient(135deg, color-mix(in oklch, var(--primary) 10%, var(--card)), color-mix(in oklch, var(--primary) 3%, var(--card)));
border-color: color-mix(in oklch, var(--primary) 42%, var(--border));
box-shadow: 0 8px 20px color-mix(in oklch, var(--primary) 10%, transparent);
}
.console-v2 .energy-kpi > small {
color: var(--muted-foreground);
font-size: 11px;
font-weight: 650;
}
.console-v2 .energy-kpi > strong {
min-width: 0;
color: var(--heading-foreground, var(--foreground));
font-size: clamp(19px, 1.9vw, 28px);
line-height: 1.1;
white-space: nowrap;
}
.console-v2 .energy-kpi-primary > strong {
display: flex;
align-items: baseline;
gap: 6px;
color: var(--primary);
font-size: clamp(27px, 2.6vw, 38px);
}
.console-v2 .energy-kpi-primary > strong span {
color: var(--foreground);
font-size: 14px;
font-weight: 700;
}
.console-v2 .energy-kpi > em {
max-width: 100%;
overflow: hidden;
color: var(--muted-foreground);
font-size: 10px;
font-style: normal;
text-overflow: ellipsis;
white-space: nowrap;
}
.console-v2 .energy-kpi-primary > svg {
width: 42px;
height: 42px;
position: absolute;
right: 10px;
top: 10px;
color: color-mix(in oklch, var(--primary) 20%, transparent);
stroke-width: 1.4;
}
.console-v2 .energy-monitor-dashboard {
min-height: 0;
display: grid;
grid-template-columns: minmax(0, 1.58fr) minmax(330px, .92fr);
gap: 12px;
}
.console-v2 .energy-trend-card,
.console-v2 .energy-devices-card {
min-width: 0;
min-height: 0;
overflow: hidden;
background: color-mix(in oklch, var(--card) 96%, var(--secondary));
border: 1px solid color-mix(in oklch, var(--primary) 11%, var(--border));
border-radius: 16px;
box-shadow: 0 7px 18px color-mix(in oklch, var(--foreground) 5%, transparent);
}
.console-v2 .energy-trend-card {
display: grid;
grid-template-rows: auto auto minmax(0, 1fr) auto;
padding: 15px 16px 12px;
}
.console-v2 .energy-trend-card > header,
.console-v2 .energy-devices-card > header {
justify-content: space-between;
gap: 12px;
}
.console-v2 .energy-trend-card h3,
.console-v2 .energy-devices-card h3 {
font-size: 15px;
}
.console-v2 .energy-trend-card > header > strong {
color: var(--primary);
font-size: 16px;
white-space: nowrap;
}
.console-v2 .energy-trend-tabs {
display: flex;
gap: 7px;
padding: 11px 0 4px;
}
.console-v2 .energy-trend-tabs button {
min-height: 30px;
padding: 5px 13px;
color: var(--muted-foreground);
background: color-mix(in oklch, var(--card) 88%, var(--secondary));
border: 1px solid var(--border);
border-radius: 9px;
font-size: 11px;
font-weight: 700;
cursor: pointer;
transition: color 150ms ease, background 150ms ease, border-color 150ms ease;
}
.console-v2 .energy-trend-tabs button:hover,
.console-v2 .energy-trend-tabs button:focus-visible {
color: var(--primary);
border-color: color-mix(in oklch, var(--primary) 28%, var(--border));
outline: 2px solid color-mix(in oklch, var(--ring) 14%, transparent);
outline-offset: 1px;
}
.console-v2 .energy-trend-tabs button.active {
color: var(--primary-foreground);
background: var(--primary);
border-color: var(--primary);
box-shadow: 0 5px 13px color-mix(in oklch, var(--primary) 20%, transparent);
}
.console-v2 .energy-chart-wrap {
min-height: 210px;
position: relative;
overflow: hidden;
}
.console-v2 .energy-monitor-chart {
width: 100%;
height: 100%;
min-height: 210px;
display: block;
color: var(--primary);
overflow: visible;
}
.console-v2 .energy-chart-grid line {
stroke: color-mix(in oklch, var(--primary) 10%, var(--border));
stroke-width: 1;
stroke-dasharray: 3 5;
}
.console-v2 .energy-chart-area {
fill: url(#energyMonitorAreaGradient);
stroke: none;
}
.console-v2 .energy-chart-line {
fill: none;
stroke: currentColor;
stroke-width: 3;
stroke-linecap: round;
stroke-linejoin: round;
vector-effect: non-scaling-stroke;
filter: drop-shadow(0 3px 5px color-mix(in oklch, var(--primary) 18%, transparent));
transition: points 260ms linear;
}
.console-v2 .energy-chart-y {
position: absolute;
z-index: 2;
left: 0;
color: var(--muted-foreground);
background: color-mix(in oklch, var(--card) 82%, transparent);
font-size: 9px;
line-height: 1;
pointer-events: none;
}
.console-v2 .energy-chart-y-top { top: 8px; }
.console-v2 .energy-chart-y-mid { top: 49%; }
.console-v2 .energy-chart-y-bottom { bottom: 26px; }
.console-v2 .energy-chart-x {
position: absolute;
right: 0;
bottom: 3px;
left: 42px;
display: flex;
justify-content: space-between;
color: var(--muted-foreground);
font-size: 9px;
}
.console-v2 .energy-trend-card > footer {
justify-content: space-between;
gap: 10px;
padding-top: 5px;
color: var(--muted-foreground);
border-top: 1px solid color-mix(in oklch, var(--primary) 8%, var(--border));
font-size: 10px;
}
.console-v2 .energy-trend-card > footer span {
display: flex;
align-items: center;
gap: 8px;
color: var(--foreground);
font-weight: 650;
}
.console-v2 .energy-trend-card > footer p {
margin: 0;
}
.console-v2 .energy-devices-card {
display: grid;
grid-template-rows: auto minmax(0, 1fr) auto;
padding: 15px 12px 10px;
}
.console-v2 .energy-devices-card > header {
padding: 0 3px 10px;
}
.console-v2 .energy-devices-card > header > span {
padding: 5px 8px;
color: var(--primary);
background: color-mix(in oklch, var(--primary) 8%, var(--card));
border-radius: 999px;
font-size: 10px;
font-weight: 700;
white-space: nowrap;
}
.console-v2 .energy-device-list {
min-height: 0;
display: grid;
align-content: start;
gap: 6px;
overflow: auto;
padding: 1px 3px 4px;
scrollbar-color: color-mix(in oklch, var(--primary) 24%, var(--border)) transparent;
scrollbar-width: thin;
}
.console-v2 .energy-device-row {
width: 100%;
min-width: 0;
display: grid;
grid-template-columns: 34px minmax(82px, 1fr) auto 72px 34px;
align-items: center;
gap: 8px;
padding: 8px 7px;
color: var(--foreground);
text-align: left;
background: color-mix(in oklch, var(--card) 88%, var(--secondary));
border: 1px solid transparent;
border-radius: 11px;
cursor: pointer;
transition: color 150ms ease, background 150ms ease, border-color 150ms ease, transform 150ms ease;
}
.console-v2 .energy-device-row:hover,
.console-v2 .energy-device-row:focus-visible,
.console-v2 .energy-device-row.active {
color: var(--foreground);
background: color-mix(in oklch, var(--primary) 7%, var(--card));
border-color: color-mix(in oklch, var(--primary) 25%, var(--border));
outline: none;
}
.console-v2 .energy-device-row:focus-visible {
box-shadow: 0 0 0 3px color-mix(in oklch, var(--ring) 14%, transparent);
}
.console-v2 .energy-device-row.active {
box-shadow: inset 3px 0 0 var(--primary);
}
.console-v2 .energy-device-icon {
width: 32px;
height: 32px;
display: grid;
place-items: center;
color: var(--primary);
background: color-mix(in oklch, var(--primary) 9%, var(--card));
border: 1px solid color-mix(in oklch, var(--primary) 14%, var(--border));
border-radius: 10px;
}
.console-v2 .energy-device-icon svg {
width: 16px;
height: 16px;
}
.console-v2 .energy-device-identity {
min-width: 0;
display: flex;
flex-direction: column;
gap: 3px;
}
.console-v2 .energy-device-identity strong {
overflow: hidden;
font-size: 11px;
text-overflow: ellipsis;
white-space: nowrap;
}
.console-v2 .energy-device-identity small {
min-width: 0;
gap: 4px;
color: var(--muted-foreground);
font-size: 8px;
}
.console-v2 .energy-device-identity small b {
color: var(--primary);
font-weight: 750;
}
.console-v2 .energy-device-identity small b[data-source="estimated"] {
color: var(--muted-foreground);
}
.console-v2 .energy-device-identity small i {
width: 2px;
height: 2px;
flex: 0 0 2px;
background: currentColor;
border-radius: 50%;
}
.console-v2 .energy-device-identity small span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.console-v2 .energy-device-metrics {
min-width: 64px;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 2px;
white-space: nowrap;
}
.console-v2 .energy-device-metrics strong {
color: var(--heading-foreground, var(--foreground));
font-size: 11px;
}
.console-v2 .energy-device-metrics small {
color: var(--muted-foreground);
font-size: 8px;
}
.console-v2 .energy-device-sparkline {
width: 72px;
height: 24px;
display: block;
overflow: visible;
}
.console-v2 .energy-device-sparkline polyline {
fill: none;
stroke: var(--primary);
stroke-width: 1.8;
stroke-linecap: round;
stroke-linejoin: round;
vector-effect: non-scaling-stroke;
}
.console-v2 .energy-device-share {
min-width: 0;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 4px;
}
.console-v2 .energy-device-share > b {
color: var(--primary);
font-size: 8px;
}
.console-v2 .energy-device-share > i {
width: 30px;
height: 3px;
display: block;
overflow: hidden;
background: color-mix(in oklch, var(--primary) 10%, var(--border));
border-radius: 999px;
}
.console-v2 .energy-device-share em {
width: var(--energy-share, 0%);
height: 100%;
display: block;
background: var(--primary);
border-radius: inherit;
transition: width 260ms linear;
}
.console-v2 .energy-devices-card > footer {
display: flex;
align-items: flex-start;
gap: 7px;
margin-top: 6px;
padding: 8px 9px;
color: var(--muted-foreground);
background: color-mix(in oklch, var(--primary) 5%, var(--secondary));
border-radius: 9px;
font-size: 9px;
line-height: 1.45;
}
.console-v2 .energy-devices-card > footer svg {
width: 13px;
height: 13px;
flex: 0 0 13px;
color: var(--primary);
}
.console-v2 #v2PanelControlDock > .energy-monitor-toggle {
overflow: visible !important;
}
.console-v2 .energy-monitor-toggle-live {
width: 6px;
height: 6px;
position: absolute;
right: 6px;
top: 6px;
display: block;
background: var(--primary);
border: 1.5px solid var(--card);
border-radius: 50%;
box-shadow: 0 0 0 2px color-mix(in oklch, var(--primary) 10%, transparent);
}
@keyframes energy-monitor-enter {
from { opacity: 0; transform: translateY(8px) scale(.992); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
@media (max-width: 980px) {
.console-v2 .energy-monitor-content {
overflow: auto;
}
.console-v2 .energy-monitor-dashboard {
grid-template-columns: minmax(0, 1fr);
}
.console-v2 .energy-trend-card {
min-height: 390px;
}
.console-v2 .energy-devices-card {
min-height: 450px;
}
.console-v2 .energy-device-list {
grid-template-columns: repeat(2, minmax(0, 1fr));
overflow: visible;
}
.console-v2 .energy-device-row {
grid-template-columns: 34px minmax(72px, 1fr) auto 52px;
}
.console-v2 .energy-device-share {
display: none;
}
.console-v2 .energy-device-sparkline {
width: 52px;
}
}
@media (max-width: 760px) {
.console-v2 .energy-monitor-layer {
inset: 0;
padding: 56px 8px 8px;
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
}
.console-v2 .energy-monitor-page {
width: 100%;
height: 100%;
border-radius: 17px;
}
.console-v2 .energy-monitor-header {
min-height: 66px;
gap: 8px;
padding: 10px 10px 9px 12px;
}
.console-v2 .energy-monitor-heading {
gap: 8px;
}
.console-v2 .energy-monitor-heading > span {
width: 38px;
height: 38px;
flex-basis: 38px;
border-radius: 12px;
}
.console-v2 .energy-monitor-heading > span svg {
width: 20px;
height: 20px;
}
.console-v2 .energy-monitor-heading h2 {
font-size: 16px;
}
.console-v2 .energy-monitor-heading p,
.console-v2 #energyMonitorFreshness {
display: none;
}
.console-v2 .energy-monitor-live-state {
gap: 6px;
}
.console-v2 .energy-monitor-live-label {
font-size: 10px;
}
.console-v2 .energy-monitor-source {
padding: 4px 7px;
font-size: 9px;
}
.console-v2 #energyMonitorClose {
width: 34px;
height: 34px;
flex-basis: 34px;
}
.console-v2 .energy-monitor-content {
gap: 9px;
padding: 10px;
}
.console-v2 .energy-monitor-kpis {
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 7px;
}
.console-v2 .energy-kpi {
min-height: 82px;
padding: 10px 11px;
border-radius: 12px;
}
.console-v2 .energy-kpi > strong {
font-size: 18px;
}
.console-v2 .energy-kpi-primary > strong {
font-size: 26px;
}
.console-v2 .energy-trend-card {
min-height: 340px;
padding: 12px 11px 9px;
}
.console-v2 .energy-chart-wrap,
.console-v2 .energy-monitor-chart {
min-height: 190px;
}
.console-v2 .energy-trend-tabs {
gap: 5px;
overflow-x: auto;
padding-bottom: 7px;
}
.console-v2 .energy-trend-tabs button {
flex: 0 0 auto;
padding-inline: 11px;
}
.console-v2 .energy-devices-card {
min-height: 560px;
}
.console-v2 .energy-device-list {
grid-template-columns: minmax(0, 1fr);
}
.console-v2 .energy-device-row {
grid-template-columns: 34px minmax(70px, 1fr) auto 62px 34px;
}
.console-v2 .energy-device-share {
display: flex;
}
.console-v2 .energy-device-sparkline {
width: 62px;
}
}
@media (max-width: 460px) {
.console-v2 .energy-monitor-heading p,
.console-v2 .energy-monitor-live-label {
display: none;
}
.console-v2 .energy-monitor-source {
max-width: 74px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.console-v2 .energy-monitor-kpis {
gap: 6px;
}
.console-v2 .energy-kpi {
min-height: 78px;
}
.console-v2 .energy-kpi > small {
font-size: 9px;
}
.console-v2 .energy-kpi > em {
font-size: 8px;
}
.console-v2 .energy-device-row {
grid-template-columns: 32px minmax(64px, 1fr) auto 50px;
gap: 6px;
padding-inline: 5px;
}
.console-v2 .energy-device-share {
display: none;
}
.console-v2 .energy-device-sparkline {
width: 50px;
}
.console-v2 .energy-trend-card > footer p {
display: none;
}
}
@media (max-width: 340px) {
.console-v2 .energy-monitor-heading > span {
display: none;
}
.console-v2 .energy-monitor-source {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
.console-v2 .energy-monitor-page {
animation: none;
}
.console-v2 .energy-chart-line,
.console-v2 .energy-device-share em,
.console-v2 .energy-device-row,
.console-v2 #energyMonitorClose {
transition: none;
}
}
+905
View File
@@ -0,0 +1,905 @@
(function () {
"use strict";
const $ = (selector, root = document) => root.querySelector(selector);
const $$ = (selector, root = document) => [...root.querySelectorAll(selector)];
const SAMPLE_INTERVAL_MS = 1000;
const MEASURED_STALE_MS = 15000;
const HISTORY_LIMIT = 60;
const MAX_DEVICE_POWER_W = 100000;
const MAX_REPORTED_ENERGY_WH = 1000000000;
const ENERGY_MONITORED_DEVICES = Object.freeze([
{ id: "passenger", telemetryKeys: ["passenger", "passenger_seat"], name: "副驾座椅", icon: "armchair", phase: 1 },
{ id: "driver", telemetryKeys: ["driver", "driver_seat"], name: "主驾座椅", icon: "armchair", phase: 2 },
{ id: "climate", telemetryKeys: ["climate", "hvac"], name: "空调", icon: "snowflake", phase: 3 },
{ id: "shade", telemetryKeys: ["shade", "roof", "sunshade"], name: "遮阳帘", icon: "panels-top-left", phase: 4 },
{ id: "windows", telemetryKeys: ["windows", "window"], name: "车窗", icon: "gallery-vertical-end", phase: 5 },
{ id: "reading", telemetryKeys: ["reading", "reading_light", "interior_light"], name: "阅读灯", icon: "book-open", phase: 6 },
{ id: "ambient", telemetryKeys: ["ambient", "ambient_light"], name: "氛围灯", icon: "sparkles", phase: 7 },
]);
const SERIES = Object.freeze([
{ id: "total", name: "总功率", devices: ENERGY_MONITORED_DEVICES.map(({ id }) => id) },
{ id: "climate", name: "空调", devices: ["climate"] },
{ id: "seats", name: "座椅", devices: ["passenger", "driver"] },
{ id: "lights", name: "灯光", devices: ["reading", "ambient"] },
]);
const aliasToDevice = new Map();
ENERGY_MONITORED_DEVICES.forEach((device) => {
device.telemetryKeys.forEach((key) => aliasToDevice.set(key, device.id));
});
const monitor = {
mounted: false,
open: false,
activeSeries: "total",
timer: null,
startedAt: Date.now(),
lastSampleAt: null,
frames: [],
latest: null,
measured: new Map(),
reportedEnergy: new Map(),
energyWh: Object.fromEntries(ENERGY_MONITORED_DEVICES.map(({ id }) => [id, 0])),
motionUntil: new Map(),
controlRequests: new Map(),
returnFocus: null,
backgroundInert: [],
};
function finiteNonNegative(value) {
if (value === null || value === undefined || value === "") return null;
const number = Number(value);
return Number.isFinite(number) && number >= 0 ? number : null;
}
function finitePower(value) {
const number = finiteNonNegative(value);
return number !== null && number <= MAX_DEVICE_POWER_W ? number : null;
}
function finiteReportedEnergy(value) {
const number = finiteNonNegative(value);
return number !== null && number <= MAX_REPORTED_ENERGY_WH ? number : null;
}
function normalizeSource(value) {
return String(value || "").trim().toLowerCase().replace(/[\s-]+/g, "_");
}
function telemetrySource(telemetry, payload) {
if (telemetry?.simulated === true || payload?.simulated === true) return "simulated";
if (telemetry?.estimated === true || payload?.estimated === true) return "estimated";
const declared = normalizeSource(
telemetry?.source
|| telemetry?.data_source
|| payload?.source
|| payload?.data_source,
);
if (/(^|_)(simulated|simulation|demo|mock|synthetic|virtual)(_|$)/.test(declared)) {
return "simulated";
}
if (/(^|_)(estimated|estimate|derived|inferred|calculated|projected|unknown)(_|$)/.test(declared)) {
return "estimated";
}
if (telemetry?.measured === true || payload?.measured === true) return "measured";
if (/(^|_)(measured|meter|sensor|hardware|physical|vehicle|can|mqtt)(_|$)/.test(declared)) {
return "measured";
}
// An unlabelled caller is not enough evidence to claim hardware measurement.
return "estimated";
}
function text(selector, fallback = "") {
return $(selector)?.textContent?.trim() || fallback;
}
function stateFlag(value) {
const normalized = String(value || "").trim();
if (/OFF|关闭|停止|未运行/i.test(normalized)) return false;
if (/ON|ACTIVE|AMBIENT|INTERIOR|开启|运行|L[1-9]|高|中|低/i.test(normalized)) return true;
return null;
}
function numericText(selector, fallback) {
const match = text(selector).match(/-?\d+(?:\.\d+)?/);
const value = match ? Number(match[0]) : NaN;
return Number.isFinite(value) ? value : fallback;
}
function deterministicWave(now, phase, amplitude = 0.04) {
const seconds = now / 1000;
return 1
+ Math.sin(seconds / 2.4 + phase * 1.37) * amplitude
+ Math.sin(seconds / 5.7 + phase * 0.71) * amplitude * 0.45;
}
function controlIsActive(deviceId, now) {
if ((monitor.motionUntil.get(deviceId) || 0) > now) return true;
const card = $(`.v2-synced-device[data-device="${deviceId}"]`);
return /等待车机同步/.test(card?.querySelector("[data-device-control-apply]")?.textContent || "");
}
function estimateDevicePower(device, now) {
let watts = 0;
let activity = "待机";
const moving = controlIsActive(device.id, now);
if (device.id === "climate") {
const power = stateFlag(text("#climatePower"));
const fan = Math.max(0, Math.min(7, numericText("#fanLevel", 3)));
const target = (numericText("#driverTemperature", 24) + numericText("#passengerTemperature", 24)) / 2;
if (power === false) {
watts = 3.2;
activity = "待机";
} else {
watts = 390 + fan * 48 + Math.abs(24 - target) * 32;
activity = power === true ? `${fan} 档运行` : "状态估算";
}
if (moving) {
watts += 95;
activity = "调节执行中";
}
} else if (device.id === "passenger") {
const card = $('.seat-card[data-seat="passenger"]');
const ventilation = stateFlag(card?.dataset.ventilationState);
const heating = stateFlag(card?.dataset.heatingState);
const ventilationLevel = Math.max(1, Math.min(3, Number(card?.dataset.ventilationLevel) || 1));
const heatingLevel = Math.max(1, Math.min(3, Number(card?.dataset.heatingLevel) || 1));
watts = 5.6 + (ventilation ? 7 + ventilationLevel * 7 : 0) + (heating ? 18 + heatingLevel * 16 : 0) + (moving ? 130 : 0);
activity = moving ? "执行器动作" : ventilation || heating ? "舒适功能运行" : "待机";
} else if (device.id === "driver") {
const card = $('.seat-card[data-seat="driver"]');
const ventilation = stateFlag(card?.dataset.ventilationState || text('.seat-card[data-seat="driver"] [data-function="ventilation"] b'));
const heating = stateFlag(card?.dataset.heatingState || text('.seat-card[data-seat="driver"] [data-function="heating"] b'));
const massage = stateFlag(card?.dataset.massageState || text('.seat-card[data-seat="driver"] [data-function="massage"] b'));
const ventilationLevel = Math.max(1, Math.min(3, Number(card?.dataset.ventilationLevel) || 1));
const heatingLevel = Math.max(1, Math.min(3, Number(card?.dataset.heatingLevel) || 1));
watts = 6.4 + (ventilation ? 7 + ventilationLevel * 7 : 0) + (heating ? 18 + heatingLevel * 16 : 0) + (massage ? 31 : 0) + (moving ? 92 : 0);
activity = moving ? "执行器动作" : ventilation || heating || massage ? "舒适功能运行" : "待机";
} else if (device.id === "windows") {
watts = moving ? 118 : 2.8;
activity = moving ? "车窗动作" : "待机";
} else if (device.id === "shade") {
watts = moving ? 86 : 1.9;
activity = moving ? "遮阳帘动作" : "待机";
} else if (device.id === "reading") {
const power = stateFlag(text("#interiorLightPower"));
watts = power === false ? 0.8 : power === true ? 18 : 7.5;
activity = power === true ? "已开启" : power === false ? "已关闭" : "状态估算";
if (moving) {
watts += 6;
activity = "切换执行中";
}
} else if (device.id === "ambient") {
const power = stateFlag(text("#ambientPower"));
const brightness = Math.max(1, Math.min(5, numericText("#ambientBrightness", 3)));
watts = power === false ? 1.1 : power === true ? 8 + brightness * 6.2 : 18;
activity = power === true ? `${brightness} 级亮度` : power === false ? "已关闭" : "状态估算";
if (moving) {
watts += 5;
activity = "调光执行中";
}
}
const amplitude = watts < 10 ? 0.025 : device.id === "climate" ? 0.055 : 0.04;
return {
watts: Math.max(0, watts * deterministicWave(now, device.phase, amplitude)),
source: "estimated",
activity,
updatedAt: now,
};
}
function readingForDevice(device, now) {
const measured = monitor.measured.get(device.id);
const reportedEnergy = monitor.reportedEnergy.get(device.id);
if (measured && now - measured.receivedAt <= MEASURED_STALE_MS) {
const activity = measured.source === "measured"
? "车端功率通道"
: measured.source === "simulated" ? "模拟功率通道" : "外部估算通道";
return {
watts: measured.watts,
source: measured.source,
activity,
updatedAt: measured.updatedAt || measured.receivedAt,
reportedEnergyWh: reportedEnergy?.wattsHours ?? null,
reportedEnergyLabel: reportedEnergy?.label || "",
reportedEnergySource: reportedEnergy?.source || "",
};
}
return {
...estimateDevicePower(device, now),
reportedEnergyWh: reportedEnergy?.wattsHours ?? null,
reportedEnergyLabel: reportedEnergy?.label || "",
reportedEnergySource: reportedEnergy?.source || "",
};
}
function frameSeriesValue(frame, seriesId) {
const exactDevice = ENERGY_MONITORED_DEVICES.find(({ id }) => id === seriesId);
const deviceIds = exactDevice
? [exactDevice.id]
: SERIES.find(({ id }) => id === seriesId)?.devices || SERIES[0].devices;
return deviceIds.reduce((sum, id) => sum + (frame.values[id] || 0), 0);
}
function publicSnapshot() {
if (!monitor.latest) return null;
return {
timestamp: monitor.latest.timestamp,
total_power_w: monitor.latest.total,
total_energy_wh: Object.values(monitor.energyWh).reduce((sum, value) => sum + value, 0),
source: monitor.latest.source,
measured_count: monitor.latest.measuredCount,
simulated_count: monitor.latest.simulatedCount,
estimated_count: monitor.latest.estimatedCount,
total_energy_scope: "monitor_power_integral_estimate",
devices: Object.fromEntries(ENERGY_MONITORED_DEVICES.map((device) => {
const reading = monitor.latest.readings[device.id];
return [device.id, {
name: device.name,
power_w: reading.watts,
energy_wh: monitor.energyWh[device.id],
reported_energy_wh: reading.reportedEnergyWh,
reported_energy_label: reading.reportedEnergyLabel,
reported_energy_source: reading.reportedEnergySource,
source: reading.source,
activity: reading.activity,
}];
})),
sample_count: monitor.frames.length,
};
}
function pruneExpiredControlRequests(now) {
monitor.controlRequests.forEach((request, requestId) => {
if (request.until <= now) monitor.controlRequests.delete(requestId);
});
}
function sampleNow({ now = Date.now(), integrate = true, render = true } = {}) {
const timestamp = Number(now);
if (!Number.isFinite(timestamp)) return publicSnapshot();
pruneExpiredControlRequests(timestamp);
const readings = {};
const values = {};
let total = 0;
let measuredCount = 0;
let simulatedCount = 0;
ENERGY_MONITORED_DEVICES.forEach((device) => {
const reading = readingForDevice(device, timestamp);
readings[device.id] = reading;
values[device.id] = reading.watts;
total += finitePower(reading.watts) ?? 0;
if (reading.source === "measured") measuredCount += 1;
if (reading.source === "simulated") simulatedCount += 1;
});
const elapsedMs = monitor.lastSampleAt === null
? 0
: Math.max(0, timestamp - monitor.lastSampleAt);
if (integrate && elapsedMs > 0) {
ENERGY_MONITORED_DEVICES.forEach(({ id }) => {
const previous = monitor.frames.at(-1)?.values?.[id] ?? values[id];
monitor.energyWh[id] += ((previous + values[id]) / 2) * elapsedMs / 3600000;
});
}
monitor.lastSampleAt = timestamp;
monitor.frames.push({ timestamp, values, total });
if (monitor.frames.length > HISTORY_LIMIT) monitor.frames.splice(0, monitor.frames.length - HISTORY_LIMIT);
const estimatedCount = ENERGY_MONITORED_DEVICES.length - measuredCount - simulatedCount;
const source = measuredCount === ENERGY_MONITORED_DEVICES.length
? "measured"
: simulatedCount === ENERGY_MONITORED_DEVICES.length
? "simulated"
: estimatedCount === ENERGY_MONITORED_DEVICES.length
? "estimated"
: measuredCount && simulatedCount && estimatedCount
? "measured_simulated_estimated"
: measuredCount && simulatedCount
? "measured_simulated"
: measuredCount ? "mixed" : "simulated_estimated";
monitor.latest = {
timestamp,
readings,
total,
measuredCount,
simulatedCount,
estimatedCount,
source,
};
if (render && monitor.mounted) renderMonitor();
window.dispatchEvent(new CustomEvent("cognitive-agent:energy-snapshot", { detail: publicSnapshot() }));
return publicSnapshot();
}
function ingestPowerTelemetry(payload, { sample = true } = {}) {
const receivedAt = Date.now();
const updates = payload?.devices && typeof payload.devices === "object"
? Object.entries(payload.devices)
: payload?.device ? [[payload.device, payload]] : [];
updates.forEach(([rawKey, rawTelemetry]) => {
const deviceId = aliasToDevice.get(String(rawKey || "").trim().toLowerCase());
if (!deviceId) return;
const telemetry = rawTelemetry && typeof rawTelemetry === "object"
? rawTelemetry
: { power_w: rawTelemetry };
const directEnergyWh = finiteReportedEnergy(telemetry.energy_wh ?? telemetry.total_energy_wh);
const tripEnergyKwh = finiteNonNegative(telemetry.trip_energy_kwh);
const todayEnergyKwh = finiteNonNegative(telemetry.today_energy_kwh);
const tripEnergyWh = tripEnergyKwh === null ? null : finiteReportedEnergy(tripEnergyKwh * 1000);
const todayEnergyWh = todayEnergyKwh === null ? null : finiteReportedEnergy(todayEnergyKwh * 1000);
const reportedEnergyWh = directEnergyWh
?? tripEnergyWh
?? todayEnergyWh
?? null;
const reportedEnergyLabel = directEnergyWh !== null
? String(telemetry.energy_label || "车端累计")
: tripEnergyWh !== null ? "本次行程"
: todayEnergyWh !== null ? "今日累计"
: "";
const source = telemetrySource(telemetry, payload);
if (reportedEnergyWh !== null) {
monitor.reportedEnergy.set(deviceId, {
wattsHours: reportedEnergyWh,
label: reportedEnergyLabel,
source,
receivedAt,
});
}
const watts = finitePower(
telemetry.power_w ?? telemetry.total_power_w ?? telemetry.current_power_w,
);
if (watts === null) return;
const parsedUpdatedAt = telemetry.updated_at ? new Date(telemetry.updated_at).getTime() : NaN;
monitor.measured.set(deviceId, {
watts,
receivedAt,
updatedAt: Number.isFinite(parsedUpdatedAt) ? parsedUpdatedAt : receivedAt,
source,
});
});
return sample ? sampleNow() : publicSnapshot();
}
function formatPowerParts(watts) {
const value = finiteNonNegative(watts) || 0;
if (value >= 1000) return { value: (value / 1000).toFixed(value >= 10000 ? 1 : 2), unit: "kW" };
return { value: String(Math.round(value)), unit: "W" };
}
function formatPower(watts, estimated = false) {
const parts = formatPowerParts(watts);
return `${estimated ? "≈ " : ""}${parts.value} ${parts.unit}`;
}
function formatEnergy(wh, estimated = false) {
const value = finiteNonNegative(wh) || 0;
const prefix = estimated ? "≈ " : "";
if (value >= 1000) return `${prefix}${(value / 1000).toFixed(3)} kWh`;
return `${prefix}${value < 10 ? value.toFixed(2) : value.toFixed(1)} Wh`;
}
function sourceLabel(source) {
return {
measured: "车端实测",
simulated: "模拟数据",
simulated_estimated: "模拟 + 估算",
measured_simulated: "实测 + 模拟",
measured_simulated_estimated: "实测 + 模拟 + 估算",
mixed: "实测 + 估算",
estimated: "状态估算",
}[source] || "状态估算";
}
function sourceShortLabel(source) {
return source === "measured" ? "实测" : source === "simulated" ? "模拟" : "估算";
}
function totalPowerQualifier(source) {
if (source === "measured") return "实测总功率";
if (source === "simulated") return "模拟总功率";
if (source === "estimated") return "估算总功率";
return "混合来源总功率";
}
function niceMaximum(value) {
const safe = Math.max(1, value);
const magnitude = 10 ** Math.floor(Math.log10(safe));
const normalized = safe / magnitude;
const nice = normalized <= 1 ? 1 : normalized <= 2 ? 2 : normalized <= 5 ? 5 : 10;
return nice * magnitude;
}
function chartPoints(values, maxValue, width = 700, height = 238, padding = { left: 42, right: 14, top: 16, bottom: 28 }) {
if (values.length < 2) return [];
const innerWidth = width - padding.left - padding.right;
const innerHeight = height - padding.top - padding.bottom;
return values.map((value, index) => ({
x: padding.left + innerWidth * index / (values.length - 1),
y: padding.top + innerHeight * (1 - Math.max(0, value) / maxValue),
}));
}
function polylinePoints(values, maxValue, width, height, padding) {
return chartPoints(values, maxValue, width, height, padding)
.map(({ x, y }) => `${x.toFixed(1)},${y.toFixed(1)}`)
.join(" ");
}
function renderMainChart() {
const values = monitor.frames.map((frame) => frameSeriesValue(frame, monitor.activeSeries));
const maximum = niceMaximum(Math.max(...values, 1));
const points = chartPoints(values, maximum);
const line = $("#energyMonitorChartLine");
const area = $("#energyMonitorChartArea");
const svg = $("#energyMonitorChart");
const baseline = 210;
line?.setAttribute("points", points.map(({ x, y }) => `${x.toFixed(1)},${y.toFixed(1)}`).join(" "));
area?.setAttribute("d", points.length
? `M ${points[0].x.toFixed(1)} ${baseline} L ${points.map(({ x, y }) => `${x.toFixed(1)} ${y.toFixed(1)}`).join(" L ")} L ${points.at(-1).x.toFixed(1)} ${baseline} Z`
: "");
const selectedDevice = ENERGY_MONITORED_DEVICES.find(({ id }) => id === monitor.activeSeries);
const selectedSeries = SERIES.find(({ id }) => id === monitor.activeSeries);
const label = selectedDevice?.name || selectedSeries?.name || "总功率";
const current = values.at(-1) || 0;
const currentReading = selectedDevice ? monitor.latest?.readings?.[selectedDevice.id] : null;
const estimated = currentReading ? currentReading.source !== "measured" : monitor.latest?.source !== "measured";
$("#energyMonitorTrendName").textContent = label;
$("#energyMonitorTrendCurrent").textContent = formatPower(current, estimated);
$("#energyMonitorChartMax").textContent = formatPower(maximum);
$("#energyMonitorChartMid").textContent = formatPower(maximum / 2);
const spanSeconds = Math.max(1, Math.round(((monitor.frames.at(-1)?.timestamp || 0) - (monitor.frames[0]?.timestamp || 0)) / 1000));
const windowSeconds = monitor.frames.length >= HISTORY_LIMIT ? 60 : Math.min(60, spanSeconds);
$("#energyMonitorTrendWindow").textContent = `最近 ${windowSeconds} 秒`;
$("#energyMonitorChartStart").textContent = `${windowSeconds} 秒前`;
if (svg) svg.setAttribute("aria-label", `${label}最近 ${windowSeconds} 秒功率趋势,当前${formatPower(current, estimated)}`);
$$('[data-energy-series]').forEach((button) => {
const selected = button.dataset.energySeries === monitor.activeSeries;
button.classList.toggle("active", selected);
button.setAttribute("aria-pressed", String(selected));
});
$$('[data-energy-device]').forEach((button) => {
const selected = button.dataset.energyDevice === monitor.activeSeries;
button.classList.toggle("active", selected);
button.setAttribute("aria-pressed", String(selected));
});
}
function renderDeviceRows() {
if (!monitor.latest) return;
const total = Math.max(monitor.latest.total, 0.0001);
ENERGY_MONITORED_DEVICES.forEach((device) => {
const row = $(`[data-energy-device="${device.id}"]`);
const reading = monitor.latest.readings[device.id];
if (!row || !reading) return;
const estimated = reading.source !== "measured";
$("[data-energy-device-power]", row).textContent = formatPower(reading.watts, estimated);
const hasReportedEnergy = reading.reportedEnergyWh !== null && reading.reportedEnergyWh !== undefined;
const displayedEnergyWh = hasReportedEnergy ? reading.reportedEnergyWh : monitor.energyWh[device.id];
const reportedEnergyEstimated = !hasReportedEnergy || reading.reportedEnergySource !== "measured";
$("[data-energy-device-energy]", row).textContent = formatEnergy(displayedEnergyWh, reportedEnergyEstimated);
$("[data-energy-device-energy]", row).title = hasReportedEnergy
? `${reading.reportedEnergyLabel || "车端累计"}能耗`
: "本次监测功率积分估算";
$("[data-energy-device-source]", row).textContent = sourceShortLabel(reading.source);
$("[data-energy-device-source]", row).dataset.source = reading.source;
$("[data-energy-device-activity]", row).textContent = reading.activity;
const share = Math.max(0, Math.min(100, reading.watts / total * 100));
$("[data-energy-device-share]", row).textContent = `${share.toFixed(1)}%`;
row.style.setProperty("--energy-share", `${share}%`);
const values = monitor.frames.map((frame) => frame.values[device.id] || 0);
const maximum = Math.max(...values, 1);
$("[data-energy-device-sparkline]", row)?.setAttribute(
"points",
polylinePoints(values, maximum, 92, 28, { left: 1, right: 1, top: 3, bottom: 3 }),
);
const energyScope = hasReportedEnergy ? reading.reportedEnergyLabel || "车端累计" : "本次监测估算";
row.setAttribute("aria-label", `${device.name},当前${formatPower(reading.watts, estimated)},${energyScope}${formatEnergy(displayedEnergyWh, reportedEnergyEstimated)},点击查看趋势`);
});
}
function renderMonitor() {
if (!monitor.latest) return;
const totalParts = formatPowerParts(monitor.latest.total);
const estimated = monitor.latest.source !== "measured";
$("#energyMonitorTotalPower").textContent = totalParts.value;
$("#energyMonitorTotalUnit").textContent = totalParts.unit;
$("#energyMonitorTotalQualifier").textContent = totalPowerQualifier(monitor.latest.source);
$("#energyMonitorAccumulated").textContent = formatEnergy(
Object.values(monitor.energyWh).reduce((sum, value) => sum + value, 0),
true,
);
const elapsedHours = Math.max(1 / 3600, (monitor.latest.timestamp - monitor.startedAt) / 3600000);
$("#energyMonitorAverage").textContent = formatPower(
Object.values(monitor.energyWh).reduce((sum, value) => sum + value, 0) / elapsedHours,
true,
);
$("#energyMonitorCoverage").textContent = `${ENERGY_MONITORED_DEVICES.length} / ${ENERGY_MONITORED_DEVICES.length}`;
$("#energyMonitorSource").textContent = sourceLabel(monitor.latest.source);
$("#energyMonitorSource").dataset.source = monitor.latest.source;
const sourceBreakdown = [
monitor.latest.measuredCount ? `${monitor.latest.measuredCount} 路实测` : "",
monitor.latest.simulatedCount ? `${monitor.latest.simulatedCount} 路模拟` : "",
monitor.latest.estimatedCount ? `${monitor.latest.estimatedCount} 路估算` : "",
].filter(Boolean).join(" · ");
$("#energyMonitorCoverageNote").textContent = sourceBreakdown || "等待设备采样";
$("#energyMonitorLiveLabelText").textContent = monitor.latest.measuredCount
? "实时监测"
: monitor.latest.simulatedCount ? "动态模拟" : "状态估算";
$("#energyMonitorFreshness").textContent = `采样更新 ${new Date(monitor.latest.timestamp).toLocaleTimeString("zh-CN", { hour12: false })}`;
$("#energyMonitorPage").dataset.revision = String(Number($("#energyMonitorPage").dataset.revision || 0) + 1);
$("#energyMonitorPage").dataset.sampleCount = String(monitor.frames.length);
renderMainChart();
renderDeviceRows();
}
function deviceRowsMarkup() {
return ENERGY_MONITORED_DEVICES.map((device) => `
<button class="energy-device-row" type="button" data-energy-device="${device.id}" aria-pressed="false">
<span class="energy-device-icon"><i data-lucide="${device.icon}"></i></span>
<span class="energy-device-identity"><strong>${device.name}</strong><small><b data-energy-device-source data-source="estimated">估算</b><i></i><span data-energy-device-activity>等待采样</span></small></span>
<span class="energy-device-metrics"><strong data-energy-device-power>-- W</strong><small data-energy-device-energy>-- Wh</small></span>
<svg class="energy-device-sparkline" viewBox="0 0 92 28" preserveAspectRatio="none" aria-hidden="true"><polyline data-energy-device-sparkline points=""></polyline></svg>
<span class="energy-device-share"><b data-energy-device-share>0%</b><i><em></em></i></span>
</button>`).join("");
}
function createLayer() {
const trigger = document.createElement("button");
trigger.id = "energyMonitorToggle";
trigger.className = "energy-monitor-toggle v2-panel-control-button";
trigger.type = "button";
trigger.setAttribute("aria-controls", "energyMonitorLayer");
trigger.setAttribute("aria-pressed", "false");
trigger.setAttribute("aria-expanded", "false");
trigger.setAttribute("aria-label", "打开整车能耗监测");
trigger.title = "打开整车能耗监测";
trigger.innerHTML = '<i data-lucide="activity"></i><span class="visually-hidden">整车能耗监测</span><b class="energy-monitor-toggle-live" aria-hidden="true"></b>';
const layer = document.createElement("div");
layer.id = "energyMonitorLayer";
layer.className = "energy-monitor-layer";
layer.hidden = true;
layer.inert = true;
layer.setAttribute("role", "dialog");
layer.setAttribute("aria-modal", "true");
layer.setAttribute("aria-labelledby", "energyMonitorTitle");
layer.innerHTML = `
<section class="energy-monitor-page" id="energyMonitorPage" data-revision="0" data-sample-count="0">
<header class="energy-monitor-header">
<div class="energy-monitor-heading">
<span><i data-lucide="gauge"></i></span>
<div><h2 id="energyMonitorTitle">整车能耗监测</h2><p>实时追踪各可控车端设备功率与累计能耗</p></div>
</div>
<div class="energy-monitor-live-state">
<span class="energy-monitor-live-label"><i></i><b id="energyMonitorLiveLabelText">状态估算</b></span>
<span id="energyMonitorFreshness">最后更新 --:--:--</span>
<span class="energy-monitor-source" id="energyMonitorSource" data-source="estimated">状态估算</span>
<button id="energyMonitorClose" type="button" aria-label="关闭整车能耗监测" title="关闭"><i data-lucide="x"></i></button>
</div>
</header>
<div class="energy-monitor-content">
<section class="energy-monitor-kpis" aria-label="整车能耗摘要">
<article class="energy-kpi energy-kpi-primary"><small>实时总功率</small><strong><b id="energyMonitorTotalPower">--</b><span id="energyMonitorTotalUnit">W</span></strong><em id="energyMonitorTotalQualifier">等待采样</em><i data-lucide="zap"></i></article>
<article class="energy-kpi"><small>本次监测累计能耗</small><strong id="energyMonitorAccumulated">-- Wh</strong><em>按实时功率积分</em></article>
<article class="energy-kpi"><small>本次监测平均功率</small><strong id="energyMonitorAverage">-- W</strong><em>自页面加载开始</em></article>
<article class="energy-kpi"><small>监测设备</small><strong id="energyMonitorCoverage">7 / 7</strong><em id="energyMonitorCoverageNote">等待设备采样</em></article>
</section>
<div class="energy-monitor-dashboard">
<section class="energy-trend-card" aria-labelledby="energyTrendTitle">
<header>
<div><h3 id="energyTrendTitle">功率趋势</h3><p><span id="energyMonitorTrendName">总功率</span> · <span id="energyMonitorTrendWindow">最近 24 秒</span></p></div>
<strong id="energyMonitorTrendCurrent">-- W</strong>
</header>
<div class="energy-trend-tabs" role="group" aria-label="选择功率趋势">
${SERIES.map((series) => `<button type="button" data-energy-series="${series.id}" aria-pressed="${String(series.id === "total")}" class="${series.id === "total" ? "active" : ""}">${series.name}</button>`).join("")}
</div>
<div class="energy-chart-wrap">
<span class="energy-chart-y energy-chart-y-top" id="energyMonitorChartMax">-- W</span>
<span class="energy-chart-y energy-chart-y-mid" id="energyMonitorChartMid">-- W</span>
<span class="energy-chart-y energy-chart-y-bottom">0</span>
<svg id="energyMonitorChart" class="energy-monitor-chart" viewBox="0 0 700 238" preserveAspectRatio="none" role="img" aria-label="功率趋势">
<defs><linearGradient id="energyMonitorAreaGradient" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="currentColor" stop-opacity=".22"></stop><stop offset="1" stop-color="currentColor" stop-opacity=".015"></stop></linearGradient></defs>
<g class="energy-chart-grid"><line x1="42" y1="16" x2="686" y2="16"></line><line x1="42" y1="113" x2="686" y2="113"></line><line x1="42" y1="210" x2="686" y2="210"></line></g>
<path id="energyMonitorChartArea" class="energy-chart-area" d=""></path>
<polyline id="energyMonitorChartLine" class="energy-chart-line" points=""></polyline>
</svg>
<div class="energy-chart-x"><span id="energyMonitorChartStart">60 秒前</span><span>现在</span></div>
</div>
<footer><span><i></i>当前选中曲线</span><p>点击右侧设备可查看其独立趋势</p></footer>
</section>
<section class="energy-devices-card" aria-labelledby="energyDevicesTitle">
<header><div><h3 id="energyDevicesTitle">设备实时功率</h3><p>功率 · 累计能耗 · 占比</p></div><span>${ENERGY_MONITORED_DEVICES.length} 个可控设备</span></header>
<div class="energy-device-list" id="energyMonitorDeviceList">${deviceRowsMarkup()}</div>
<footer><i data-lucide="info"></i><span>优先读取车端功率通道;未接入独立电表的设备显示明确标注的状态估算值。</span></footer>
</section>
</div>
</div>
</section>`;
$("#v2PanelControlDock").append(trigger);
document.body.append(layer);
trigger.addEventListener("click", () => setOpen(!monitor.open));
$("#energyMonitorClose", layer).addEventListener("click", () => setOpen(false));
layer.addEventListener("click", (event) => {
if (event.target === layer) setOpen(false);
});
layer.addEventListener("click", (event) => {
const seriesButton = event.target.closest("[data-energy-series]");
const deviceButton = event.target.closest("[data-energy-device]");
if (seriesButton) selectSeries(seriesButton.dataset.energySeries);
if (deviceButton) selectSeries(deviceButton.dataset.energyDevice);
});
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
}
function focusableElements() {
return $$('button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])', $("#energyMonitorLayer"))
.filter((element) => !element.hidden && element.offsetParent !== null);
}
function setBackgroundInert(inert) {
const layer = $("#energyMonitorLayer");
if (!layer || !document.body) return;
if (inert) {
monitor.backgroundInert = [...document.body.children]
.filter((element) => element !== layer && !["SCRIPT", "STYLE", "LINK"].includes(element.tagName))
.map((element) => ({ element, wasInert: element.hasAttribute("inert") }));
monitor.backgroundInert.forEach(({ element }) => { element.toggleAttribute("inert", true); });
return;
}
monitor.backgroundInert.forEach(({ element, wasInert }) => { element.toggleAttribute("inert", wasInert); });
monitor.backgroundInert = [];
}
function captureModalKeys(event) {
if (!monitor.open) return;
if (event.key === "Escape") {
event.preventDefault();
event.stopImmediatePropagation();
setOpen(false);
return;
}
if (event.key !== "Tab") return;
const focusable = focusableElements();
if (!focusable.length) return;
const first = focusable[0];
const last = focusable.at(-1);
const layer = $("#energyMonitorLayer");
if (!layer?.contains(document.activeElement)) {
event.preventDefault();
(event.shiftKey ? last : first).focus();
} else if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
}
function setOpen(open) {
const next = Boolean(open);
const layer = $("#energyMonitorLayer");
const trigger = $("#energyMonitorToggle");
if (!layer || !trigger || monitor.open === next) return;
monitor.open = next;
trigger.classList.toggle("active", next);
trigger.setAttribute("aria-pressed", String(next));
trigger.setAttribute("aria-expanded", String(next));
trigger.setAttribute("aria-label", next ? "关闭整车能耗监测" : "打开整车能耗监测");
trigger.title = next ? "关闭整车能耗监测" : "打开整车能耗监测";
if (next) {
monitor.returnFocus = document.activeElement;
layer.hidden = false;
layer.inert = false;
layer.dataset.open = "true";
setBackgroundInert(true);
renderMonitor();
requestAnimationFrame(() => $("#energyMonitorClose", layer)?.focus());
} else {
layer.dataset.open = "false";
layer.inert = true;
layer.hidden = true;
setBackgroundInert(false);
const target = monitor.returnFocus instanceof HTMLElement && monitor.returnFocus.isConnected
? monitor.returnFocus
: trigger;
monitor.returnFocus = null;
target.focus();
}
}
function selectSeries(seriesId) {
const valid = SERIES.some(({ id }) => id === seriesId)
|| ENERGY_MONITORED_DEVICES.some(({ id }) => id === seriesId);
if (!valid) return;
monitor.activeSeries = seriesId;
renderMainChart();
}
function deviceIdsForControlDomain(domain, context = "") {
const normalizedDomain = normalizeSource(domain).replace(/^native_/, "");
const normalizedContext = normalizeSource(context).replace(/[^a-z0-9\u4e00-\u9fff]+/g, "_");
if (normalizedDomain === "seat" || /(^|_)seat(_|$)/.test(normalizedContext)) {
if (/(^|_)(driver|front_left|left_front)(_|$)|主驾/.test(normalizedContext)) return ["driver"];
if (/(^|_)(passenger|front_right|right_front)(_|$)|副驾/.test(normalizedContext)) return ["passenger"];
return ["passenger"];
}
if (["window", "windows"].includes(normalizedDomain) || /(^|_)window(s)?(_|$)/.test(normalizedContext)) return ["windows"];
if (["roof", "shade", "sunshade"].includes(normalizedDomain) || /(^|_)(roof|shade|sunshade)(_|$)/.test(normalizedContext)) return ["shade"];
if (["climate", "hvac", "outlet"].includes(normalizedDomain) || /(^|_)(climate|hvac|outlet|ac)(_|$)/.test(normalizedContext)) return ["climate"];
if (["interior_light", "reading", "reading_light"].includes(normalizedDomain) || /(^|_)(interior|reading)_?light(_|$)/.test(normalizedContext)) return ["reading"];
if (["ambient", "ambient_light"].includes(normalizedDomain) || /(^|_)ambient_?light(_|$)/.test(normalizedContext)) return ["ambient"];
return [];
}
function controlActivityDevices(payload = {}) {
const wrappedResult = payload?.result && typeof payload.result === "object";
const eventPayload = wrappedResult ? payload.result : payload;
const status = String(eventPayload?.status || "");
if (payload?.error) return [];
if (wrappedResult && !["succeeded", "partially_succeeded"].includes(status)) return [];
if (!wrappedResult && status && !["processing", "succeeded", "partially_succeeded", "submitted"].includes(status)) return [];
if (eventPayload?.preview_only === true || /_preview$/.test(String(eventPayload?.orchestration_route || ""))) {
return [];
}
const deviceIds = new Set();
const directId = aliasToDevice.get(normalizeSource(eventPayload?.device_id))
|| normalizeSource(eventPayload?.device_id);
if (ENERGY_MONITORED_DEVICES.some(({ id }) => id === directId)) deviceIds.add(directId);
const actions = [
...(Array.isArray(eventPayload?.actions) ? eventPayload.actions : []),
...(Array.isArray(eventPayload?.validated_actions) ? eventPayload.validated_actions : []),
...(Array.isArray(eventPayload?.intent_ir?.spec?.actions) ? eventPayload.intent_ir.spec.actions : []),
];
actions.forEach((action) => {
const capabilityDomain = String(action?.capability || "").split(".")[0];
const context = JSON.stringify({
capability: action?.capability,
target: action?.target,
});
deviceIdsForControlDomain(action?.domain || capabilityDomain, context).forEach((id) => deviceIds.add(id));
});
const functionConfigs = Object.entries(eventPayload?.function_configs || {});
if (eventPayload?.function_config && typeof eventPayload.function_config === "object") {
functionConfigs.push([eventPayload.function_config?.spec?.domain || "function", eventPayload.function_config]);
}
functionConfigs.forEach(([configKey, config]) => {
const domain = config?.spec?.domain || config?.domain || configKey;
const operations = config?.spec?.operations || config?.operations || [];
if (!operations.length) {
deviceIdsForControlDomain(domain, configKey).forEach((id) => deviceIds.add(id));
return;
}
operations.forEach((operation) => {
const context = JSON.stringify({
capability: operation?.capability,
target: operation?.target,
});
deviceIdsForControlDomain(operation?.domain || domain, context).forEach((id) => deviceIds.add(id));
});
});
Object.entries(eventPayload?.command_plans || {}).forEach(([planKey, plan]) => {
const domain = plan?.spec?.domain || plan?.domain || planKey;
const commands = plan?.spec?.commands || plan?.commands || [];
const context = commands.length ? JSON.stringify(commands) : planKey;
deviceIdsForControlDomain(domain, context).forEach((id) => deviceIds.add(id));
});
return [...deviceIds];
}
function markControlActivity(payload) {
const until = Date.now() + 4000;
const requestId = controlRequestId(payload);
const currentDeviceIds = controlActivityDevices(payload);
const existing = requestId ? monitor.controlRequests.get(requestId) : null;
const deviceIds = [...new Set([...(existing?.deviceIds || []), ...currentDeviceIds])];
deviceIds.forEach((deviceId) => {
monitor.motionUntil.set(deviceId, Math.max(monitor.motionUntil.get(deviceId) || 0, until));
});
if (requestId && deviceIds.length) {
monitor.controlRequests.set(requestId, { deviceIds, until });
}
return currentDeviceIds;
}
function controlRequestId(payload = {}) {
const eventPayload = payload?.result && typeof payload.result === "object"
? payload.result
: payload;
return String(eventPayload?.request_id || payload?.request_id || "").trim();
}
function completeControlActivity(payload = {}) {
const eventPayload = payload?.result && typeof payload.result === "object"
? payload.result
: payload;
const requestId = controlRequestId(payload);
const tracked = requestId ? monitor.controlRequests.get(requestId) : null;
const succeeded = !payload?.error
&& ["succeeded", "partially_succeeded"].includes(String(eventPayload?.status || ""));
if (tracked) {
monitor.controlRequests.delete(requestId);
if (!succeeded) {
const now = Date.now();
tracked.deviceIds.forEach((deviceId) => {
if ((monitor.motionUntil.get(deviceId) || 0) > tracked.until) return;
const remainingUntil = [...monitor.controlRequests.values()]
.filter((request) => request.deviceIds.includes(deviceId))
.reduce((maximum, request) => Math.max(maximum, request.until), now);
monitor.motionUntil.set(deviceId, remainingUntil);
});
}
return tracked.deviceIds;
}
return succeeded ? markControlActivity(payload) : [];
}
function seedHistory() {
const now = Date.now();
for (let offset = 23; offset >= 1; offset -= 1) {
sampleNow({ now: now - offset * SAMPLE_INTERVAL_MS, integrate: false, render: false });
}
monitor.lastSampleAt = now - SAMPLE_INTERVAL_MS;
sampleNow({ now, integrate: false, render: false });
monitor.startedAt = now;
}
function mount(attempt = 0) {
if (monitor.mounted) return;
if (!$("#v2PanelControlDock")) {
if (attempt < 20) window.setTimeout(() => mount(attempt + 1), 50);
return;
}
monitor.mounted = true;
createLayer();
seedHistory();
renderMonitor();
monitor.timer = window.setInterval(() => sampleNow(), SAMPLE_INTERVAL_MS);
document.addEventListener("keydown", captureModalKeys, true);
window.addEventListener("cognitive-agent:power-telemetry", (event) => ingestPowerTelemetry(event.detail || {}, { sample: false }));
const existingUpdater = window.updateDevicePowerTelemetry;
if (typeof existingUpdater === "function" && !existingUpdater.energyMonitorLinked) {
const linkedUpdater = (payload) => {
const result = existingUpdater(payload);
ingestPowerTelemetry(payload, { sample: false });
return result;
};
linkedUpdater.energyMonitorLinked = true;
window.updateDevicePowerTelemetry = linkedUpdater;
}
window.addEventListener("cognitive-agent:vehicle-can-control-request", (event) => markControlActivity(event.detail || {}));
window.addEventListener("cognitive-agent:validated-can-commands", (event) => markControlActivity(event.detail || {}));
window.addEventListener("cognitive-agent:vehicle-control-activity", (event) => markControlActivity(event.detail || {}));
window.addEventListener("cognitive-agent:request-complete", (event) => completeControlActivity(event.detail || {}));
}
window.CognitiveAgentEnergyMonitor = {
close: () => setOpen(false),
getDevices: () => ENERGY_MONITORED_DEVICES.map(({ id, name, telemetryKeys }) => ({ id, name, telemetryKeys: [...telemetryKeys] })),
getSnapshot: publicSnapshot,
ingestTelemetry: ingestPowerTelemetry,
isOpen: () => monitor.open,
completeControlActivity,
notifyControlActivity: markControlActivity,
open: () => setOpen(true),
sampleNow,
selectSeries,
};
document.addEventListener("DOMContentLoaded", () => mount());
}());
+156 -48
View File
@@ -1,6 +1,6 @@
const VISION_VERSION = "0.10.14";
const VISION_MODULE = `https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@${VISION_VERSION}/vision_bundle.mjs`;
const VISION_WASM = `https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@${VISION_VERSION}/wasm`;
const VISION_MODULE = `/vendor/mediapipe/tasks-vision/${VISION_VERSION}/vision_bundle.mjs`;
const VISION_WASM = `/vendor/mediapipe/tasks-vision/${VISION_VERSION}/wasm`;
const CONNECTIONS = [
[0, 1], [1, 2], [2, 3], [3, 4],
@@ -55,6 +55,12 @@ const state = {
customMapping: loadCustomMapping(),
customizeOpen: false,
engine: null,
runtimeReady: false,
enabled: true,
disabledReason: "",
cameraStatus: "checking",
cameraMessage: "正在检测摄像头…",
cameraCheckSequence: 0,
};
const $ = (selector) => document.querySelector(selector);
@@ -80,6 +86,92 @@ function notify(message) {
window.setTimeout(() => toast.classList.remove("visible"), 2600);
}
function cameraFailureDetails(error) {
if (!navigator.mediaDevices?.getUserMedia) {
return window.isSecureContext === false
? { status: "unsupported", message: "当前页面不是安全连接,浏览器已阻止摄像头访问" }
: { status: "unsupported", message: "当前浏览器不支持摄像头访问" };
}
switch (error?.name) {
case "NotFoundError":
case "DevicesNotFoundError":
return { status: "unavailable", message: "未检测到可用摄像头" };
case "NotAllowedError":
case "PermissionDeniedError":
case "SecurityError":
return { status: "blocked", message: "摄像头权限被拒绝,请在浏览器设置中允许访问" };
case "NotReadableError":
case "TrackStartError":
return { status: "busy", message: "摄像头正被其他程序占用或暂时不可用" };
case "OverconstrainedError":
case "ConstraintNotSatisfiedError":
return { status: "incompatible", message: "摄像头不支持当前画面参数" };
case "AbortError":
return { status: "unknown", message: "摄像头启动被中断,请重试" };
default:
return { status: "unknown", message: error?.message || "摄像头启动失败" };
}
}
function syncCameraButtonAvailability() {
const button = $("#gestureStartButton");
if (!button || state.stream) return;
button.disabled = !state.enabled
|| !state.runtimeReady
|| ["checking", "unavailable", "unsupported"].includes(state.cameraStatus);
}
function setCameraAvailability(status, message, { announce = true } = {}) {
state.cameraStatus = status;
state.cameraMessage = message;
$("#gestureDrawer")?.setAttribute("data-camera-status", status);
syncCameraButtonAvailability();
if (announce && !state.stream) $("#gestureSafetyText").textContent = message;
}
async function refreshCameraAvailability({ announce = true } = {}) {
if (!navigator.mediaDevices?.enumerateDevices || !navigator.mediaDevices?.getUserMedia) {
const failure = cameraFailureDetails();
setCameraAvailability(failure.status, failure.message, { announce });
return false;
}
const checkSequence = ++state.cameraCheckSequence;
if (!state.stream) setCameraAvailability("checking", "正在检测摄像头…", { announce });
try {
const devices = await navigator.mediaDevices.enumerateDevices();
if (checkSequence !== state.cameraCheckSequence) return state.cameraStatus === "available";
const available = devices.some((device) => device.kind === "videoinput");
setCameraAvailability(
available ? "available" : "unavailable",
available ? "已检测到摄像头,点击开启开始识别" : "未检测到可用摄像头",
{ announce },
);
return available;
} catch (error) {
if (checkSequence !== state.cameraCheckSequence) return state.cameraStatus === "available";
const failure = cameraFailureDetails(error);
const message = failure.status === "blocked"
? "尚未获得摄像头权限,可点击开启并在浏览器中授权"
: "暂时无法检测摄像头,可点击开启时再次确认";
setCameraAvailability("unknown", message, { announce });
return null;
}
}
async function handleCameraDeviceChange() {
const streamWasActive = Boolean(state.stream);
const available = await refreshCameraAvailability({ announce: !streamWasActive });
const streamStillLive = state.stream?.getVideoTracks().some((track) => track.readyState === "live");
if (streamWasActive && (available === false || !streamStillLive)) {
const message = available === false
? "摄像头连接已断开,未检测到其他可用摄像头"
: "当前摄像头连接已断开,请重新开启摄像头";
stopCamera({ message });
notify(message);
}
}
function distance(a, b) {
return Math.hypot(a.x - b.x, a.y - b.y);
}
@@ -386,7 +478,6 @@ function updateEngineUI(engine, message) {
pill.className = `gesture-state-pill ${engine.armed ? (engine.currentAction ? "moving" : "armed") : "idle"}`;
pill.querySelector("b").textContent = engine.armed ? (engine.currentAction ? "执行中" : "已激活") : "已锁止";
$("#gestureSafetyText").textContent = message;
$("#gestureArmButton").textContent = engine.armed ? "已激活" : "激活控制";
}
function executeGestureAction(action) {
@@ -464,7 +555,7 @@ async function recognizeLoop() {
}
async function loadGestureRuntime() {
const response = await fetch("/api/gesture/runtime", { cache: "no-store" });
const response = await window.CognitiveAgentNetwork.request("/api/gesture/runtime", { cache: "no-store" });
if (!response.ok) throw new Error("无法读取手势能力配置");
state.runtime = await response.json();
state.engine = new GestureEngine(state.runtime.safety, executeGestureAction);
@@ -475,9 +566,12 @@ async function loadGestureRuntime() {
async function loadRecognizer() {
if (state.recognizer) return;
$("#gestureSafetyText").textContent = "正在加载手势模型…";
const { FilesetResolver, GestureRecognizer } = await import(VISION_MODULE);
const { FilesetResolver, GestureRecognizer } = await import(/* @vite-ignore */ VISION_MODULE);
const fileset = await FilesetResolver.forVisionTasks(VISION_WASM);
const modelResponse = await fetch("/api/gesture/model", { cache: "force-cache" });
const modelResponse = await window.CognitiveAgentNetwork.request("/api/gesture/model", {
cache: "force-cache",
timeoutMs: 60000,
});
if (!modelResponse.ok) throw new Error("内置手势模型加载失败");
const modelAssetBuffer = new Uint8Array(await modelResponse.arrayBuffer());
state.recognizer = await GestureRecognizer.createFromOptions(fileset, {
@@ -494,6 +588,8 @@ async function startCamera() {
const button = $("#gestureStartButton");
button.disabled = true;
button.querySelector("span").textContent = "正在启动…";
button.setAttribute("aria-label", "正在启动摄像头");
button.setAttribute("title", "正在启动摄像头");
try {
if (!state.runtime) await loadGestureRuntime();
await loadRecognizer();
@@ -501,27 +597,30 @@ async function startCamera() {
video: { width: { ideal: 1280 }, height: { ideal: 720 }, facingMode: "user" },
audio: false,
});
setCameraAvailability("available", "摄像头可用,点击开启开始识别", { announce: false });
const video = $("#gestureVideo");
video.srcObject = state.stream;
await video.play();
$("#gestureCameraEmpty").hidden = true;
$("#gestureLiveBadge").hidden = false;
$("#gestureArmButton").disabled = false;
$("#gestureStopButton").disabled = false;
button.classList.add("active");
button.querySelector("span").textContent = "关闭摄像头";
button.setAttribute("aria-label", "关闭摄像头");
button.setAttribute("title", "关闭摄像头");
$("#gestureSafetyText").textContent = "张开手掌并保持 1 秒以激活";
state.frameRequest = requestAnimationFrame(recognizeLoop);
} catch (error) {
$("#gestureSafetyText").textContent = error.message || "摄像头启动失败";
notify(error.message || "摄像头启动失败");
stopCamera();
const failure = cameraFailureDetails(error);
setCameraAvailability(failure.status, failure.message, { announce: false });
stopCamera({ message: failure.message });
notify(failure.message);
} finally {
button.disabled = false;
syncCameraButtonAvailability();
}
}
function stopCamera() {
function stopCamera({ message = null } = {}) {
if (state.frameRequest) cancelAnimationFrame(state.frameRequest);
state.frameRequest = null;
state.stream?.getTracks().forEach((track) => track.stop());
@@ -533,15 +632,17 @@ function stopCamera() {
canvas?.getContext("2d")?.clearRect(0, 0, canvas.width, canvas.height);
$("#gestureCameraEmpty").hidden = false;
$("#gestureLiveBadge").hidden = true;
$("#gestureArmButton").disabled = true;
$("#gestureStopButton").disabled = true;
const button = $("#gestureStartButton");
button.classList.remove("active");
button.querySelector("span").textContent = "开启摄像头";
button.setAttribute("aria-label", "开启摄像头");
button.setAttribute("title", "开启摄像头");
state.engine?.setArmed(false);
$("#gestureDetected").textContent = "—";
$("#gestureExpected").textContent = "等待启动";
$("#gestureSafetyText").textContent = "摄像头尚未开启";
$("#gestureSafetyText").textContent = message || state.cameraMessage || "摄像头尚未开启";
syncCameraButtonAvailability();
}
function toggleCamera() {
@@ -595,63 +696,70 @@ function resetMapping() {
}
function openDrawer() {
$("#gestureDrawer").classList.add("open");
$("#gestureDrawer").setAttribute("aria-hidden", "false");
$("#gestureBackdrop").hidden = false;
requestAnimationFrame(() => $("#gestureBackdrop").classList.add("visible"));
$("#gestureToggleButton").setAttribute("aria-expanded", "true");
if (!state.enabled) {
notify(state.disabledReason || "当前运行模式未启用手势控制");
return false;
}
const drawer = $("#gestureDrawer");
if (!drawer) return false;
drawer.classList.add("open");
drawer.setAttribute("aria-hidden", "false");
document.body.classList.add("gesture-open");
return true;
}
function closeDrawer() {
stopCamera();
$("#gestureDrawer").classList.remove("open");
$("#gestureDrawer").setAttribute("aria-hidden", "true");
$("#gestureBackdrop").classList.remove("visible");
window.setTimeout(() => { $("#gestureBackdrop").hidden = true; }, 240);
$("#gestureToggleButton").setAttribute("aria-expanded", "false");
const drawer = $("#gestureDrawer");
drawer?.classList.remove("open");
drawer?.setAttribute("aria-hidden", "true");
document.body.classList.remove("gesture-open");
}
function initModeRail() {
document.querySelectorAll("[data-mode-command]").forEach((button) => button.addEventListener("click", () => {
window.CognitiveAgentConsole?.executeUtterance?.(button.dataset.modeCommand, { autoSubmit: true });
}));
$("#saveModeToggle")?.addEventListener("click", () => {
const composer = $("#saveModeComposer");
composer.hidden = !composer.hidden;
if (!composer.hidden) $("#saveModeName").focus();
});
$("#saveModeConfirm")?.addEventListener("click", () => {
const name = $("#saveModeName").value.trim();
if (!name) return notify("请先输入模式名称");
window.CognitiveAgentConsole?.executeUtterance?.(`将当前副驾驶座椅姿态保存为${name}模式`, { autoSubmit: true });
$("#saveModeComposer").hidden = true;
$("#saveModeName").value = "";
});
function setEnabled(enabled, { reason = "" } = {}) {
state.enabled = Boolean(enabled);
state.disabledReason = state.enabled ? "" : String(reason || "当前运行模式未启用手势控制");
if (!state.enabled) closeDrawer();
syncCameraButtonAvailability();
const drawer = $("#gestureDrawer");
if (drawer) {
drawer.dataset.enabled = String(state.enabled);
drawer.title = state.disabledReason;
}
}
window.CognitiveAgentGesture = {
open: openDrawer,
close: closeDrawer,
isOpen: () => $("#gestureDrawer")?.classList.contains("open") === true,
setEnabled,
};
document.addEventListener("DOMContentLoaded", async () => {
$("#gestureToggleButton")?.addEventListener("click", openDrawer);
$("#gestureStartButton").disabled = true;
$("#gestureCloseButton")?.addEventListener("click", closeDrawer);
$("#gestureBackdrop")?.addEventListener("click", closeDrawer);
$("#gestureStartButton")?.addEventListener("click", toggleCamera);
$("#gestureArmButton")?.addEventListener("click", () => state.engine?.setArmed(!state.engine.armed));
$("#gestureStopButton")?.addEventListener("click", () => state.engine?.setArmed(false));
$("#gestureCustomizeButton")?.addEventListener("click", toggleCustomize);
$("#gestureBindButton")?.addEventListener("click", bindCurrentGesture);
$("#gestureResetMapping")?.addEventListener("click", resetMapping);
document.addEventListener("keydown", (event) => {
if (event.key === "Escape" && $("#gestureDrawer")?.classList.contains("open")) closeDrawer();
if (event.key !== "Escape") return;
if ($("#gestureDrawer")?.classList.contains("open")) closeDrawer();
});
document.addEventListener("visibilitychange", () => { if (document.hidden && state.stream) stopCamera(); });
window.addEventListener("beforeunload", stopCamera);
initModeRail();
navigator.mediaDevices?.addEventListener?.("devicechange", handleCameraDeviceChange);
let runtimeError = null;
try {
await loadGestureRuntime();
$("#gestureSafetyText").textContent = state.runtime.available ? "能力就绪,开启摄像头开始识别" : "内置手势模型不可用";
state.runtimeReady = Boolean(state.runtime.available && state.runtime.model_available);
if (!state.runtimeReady) runtimeError = new Error("内置手势模型不可用");
} catch (error) {
$("#gestureSafetyText").textContent = error.message;
$("#gestureStartButton").disabled = true;
runtimeError = error;
state.runtimeReady = false;
}
await refreshCameraAvailability({ announce: !runtimeError });
if (runtimeError) $("#gestureSafetyText").textContent = runtimeError.message;
syncCameraButtonAvailability();
});
+76
View File
@@ -0,0 +1,76 @@
:root {
color-scheme: light;
--background: oklch(0.968 0.019 244);
--foreground: oklch(0.29 0.042 252);
--card: oklch(0.993 0.007 245);
--card-foreground: oklch(0.34 0.032 250);
--primary: oklch(0.57 0.19 258);
--primary-foreground: oklch(0.99 0.004 245);
--secondary: oklch(0.94 0.03 244);
--secondary-foreground: oklch(0.38 0.045 250);
--muted: oklch(0.955 0.017 245);
--muted-foreground: oklch(0.53 0.035 248);
--accent: oklch(0.918 0.045 249);
--destructive: oklch(0.61 0.19 24);
--border: oklch(0.85 0.04 245);
--ring: oklch(0.65 0.14 256);
--radius: 0.875rem;
font-family: Inter, "Microsoft YaHei", system-ui, sans-serif;
background: var(--background);
color: var(--foreground);
}
.dark {
color-scheme: dark;
--background: oklch(0.2 0.035 252);
--foreground: oklch(0.94 0.02 245);
--card: oklch(0.25 0.035 251);
--card-foreground: oklch(0.91 0.022 245);
--primary: oklch(0.7 0.15 252);
--primary-foreground: oklch(0.19 0.04 253);
--secondary: oklch(0.285 0.045 250);
--secondary-foreground: oklch(0.92 0.02 245);
--muted: oklch(0.275 0.032 250);
--muted-foreground: oklch(0.72 0.04 245);
--accent: oklch(0.32 0.065 250);
--destructive: oklch(0.7 0.18 24);
--border: oklch(1 0 0 / 10%);
--ring: oklch(0.68 0.13 252);
}
* { box-sizing: border-box; }
body { margin: 0; min-height: 100vh; background: radial-gradient(circle at 18% 0%, color-mix(in oklch, var(--primary) 12%, transparent) 0, transparent 35%), var(--background); }
.shell { width: min(1040px, calc(100% - 40px)); margin: 0 auto; padding: 54px 0 80px; }
header, .section-title, .policy, .request, .lease-details { display: flex; align-items: center; justify-content: space-between; gap: 20px; }
header { margin-bottom: 34px; }
h1, h2, p { margin: 0; }
h1 { font-size: clamp(30px, 5vw, 48px); letter-spacing: -.04em; }
h2 { font-size: 22px; }
.eyebrow { color: var(--primary); font-size: 12px; letter-spacing: .16em; font-weight: 800; margin-bottom: 8px; }
.lead, small { color: var(--muted-foreground); }
.lead { margin-top: 12px; }
a, button { font: inherit; }
a { color: var(--primary); text-decoration: none; border: 1px solid var(--border); border-radius: var(--radius); padding: 10px 14px; }
.policy, .card { color: var(--card-foreground); background: color-mix(in oklch, var(--card) 94%, transparent); border: 1px solid var(--border); border-radius: calc(var(--radius) + 8px); box-shadow: 0 20px 60px oklch(0.29 0.042 252 / 12%); }
.policy { padding: 18px 22px; justify-content: flex-start; margin-bottom: 18px; }
.policy div { display: grid; gap: 3px; flex: 1; }
.dot { width: 10px; height: 10px; border-radius: 50%; background: var(--primary); box-shadow: 0 0 16px color-mix(in oklch, var(--primary) 55%, transparent); }
#clock { color: var(--muted-foreground); font-variant-numeric: tabular-nums; }
.card { padding: 24px; margin-top: 18px; }
.active-card { border-color: var(--ring); }
.section-title { margin-bottom: 20px; }
.count { min-width: 30px; height: 30px; display: grid; place-items: center; border-radius: 999px; background: var(--accent); color: var(--primary); font-weight: 800; }
.empty { padding: 26px 4px; color: var(--muted-foreground); text-align: center; }
.request-list { display: grid; gap: 12px; }
.request { padding: 16px; border-radius: calc(var(--radius) + 3px); border: 1px solid var(--border); background: var(--muted); }
.identity { display: grid; gap: 5px; min-width: 0; }
.identity strong { font-size: 17px; }
.identity code, .identity span { color: var(--muted-foreground); font-size: 12px; overflow-wrap: anywhere; }
.actions { display: flex; gap: 8px; }
button { color: var(--primary-foreground); background: var(--primary); border: 0; border-radius: var(--radius); padding: 10px 14px; cursor: pointer; font-weight: 750; }
button:hover { filter: brightness(1.12); }
button:disabled { opacity: .45; cursor: wait; }
button.secondary { color: var(--secondary-foreground); background: var(--secondary); }
button.danger { color: var(--primary-foreground); background: var(--destructive); }
.lease-details { justify-content: flex-start; flex-wrap: wrap; padding: 18px; border-radius: calc(var(--radius) + 3px); background: var(--muted); border: 1px solid var(--ring); }
.lease-details .timer { margin-left: auto; color: var(--primary); font-weight: 850; font-size: 22px; font-variant-numeric: tabular-nums; }
.message { margin-top: 18px; min-height: 24px; color: var(--primary); }
@media (max-width: 680px) { .shell { width: min(100% - 24px, 1040px); padding-top: 26px; } header, .request, .lease-details { align-items: flex-start; flex-direction: column; } .actions { width: 100%; } .actions button { flex: 1; } .lease-details .timer { margin-left: 0; } }
+48
View File
@@ -0,0 +1,48 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<meta name="theme-color" content="oklch(0.57 0.19 258)">
<title>真实台架本地决策台</title>
<link rel="stylesheet" href="/hardware-control.css?v=ai-blue-2">
</head>
<body>
<main class="shell">
<header>
<div>
<p class="eyebrow">LOCAL BENCH AUTHORITY</p>
<h1>真实台架本地决策台</h1>
<p class="lead">只有本机可以批准、拒绝或停止终端的真实台架使用权。</p>
</div>
<a href="/">返回控制台</a>
</header>
<section class="policy">
<span class="dot"></span>
<div><strong>单终端租约</strong><small>获批终端可连续测试;最后一次真实执行后空闲 180 秒自动停止。</small></div>
<span id="clock">正在连接…</span>
</section>
<section class="card active-card">
<div class="section-title">
<div><p class="eyebrow">ACTIVE LEASE</p><h2>当前使用者</h2></div>
<button id="revokeButton" class="danger" type="button" hidden>立即停止使用</button>
</div>
<div id="activeLease" class="empty">当前无人获批使用真实台架。</div>
</section>
<section class="card">
<div class="section-title">
<div><p class="eyebrow">PENDING REQUESTS</p><h2>等待审批</h2></div>
<span id="pendingCount" class="count">0</span>
</div>
<div id="pendingRequests" class="request-list empty">暂时没有申请。</div>
</section>
<p id="message" class="message" role="status"></p>
</main>
<script src="/hardware-control.js" defer></script>
</body>
</html>
+104
View File
@@ -0,0 +1,104 @@
const activeLease = document.querySelector("#activeLease");
const pendingRequests = document.querySelector("#pendingRequests");
const pendingCount = document.querySelector("#pendingCount");
const revokeButton = document.querySelector("#revokeButton");
const message = document.querySelector("#message");
let snapshot = null;
function escapeHtml(value) {
const node = document.createElement("span");
node.textContent = String(value ?? "");
return node.innerHTML;
}
function showMessage(value, error = false) {
message.textContent = value;
message.style.color = error ? "#ff8797" : "#7fbbff";
}
async function api(path, body) {
const response = await fetch(path, {
method: body ? "POST" : "GET",
credentials: "same-origin",
headers: body ? { "Content-Type": "application/json" } : {},
body: body ? JSON.stringify(body) : undefined,
});
if (response.status === 401) {
location.replace(`/login?next=${encodeURIComponent(location.pathname)}`);
throw new Error("请先登录本机服务");
}
const payload = await response.json();
if (!response.ok) throw new Error(payload.message || payload.error);
return payload;
}
function render(data) {
snapshot = data;
pendingCount.textContent = String(data.pending.length);
if (data.active) {
const lease = data.active;
activeLease.className = "lease-details";
activeLease.innerHTML = `
<div class="identity"><strong>${escapeHtml(lease.label)}</strong><code>${escapeHtml(lease.client_id)}</code><span>来源 ${escapeHtml(lease.client_address)} · 最近执行 ${escapeHtml(lease.last_activity_at)}</span></div>
<div class="timer">${Number(lease.idle_remaining_seconds)}s</div>`;
revokeButton.hidden = false;
revokeButton.dataset.clientId = lease.client_id;
} else {
activeLease.className = "empty";
activeLease.textContent = "当前无人获批使用真实台架。";
revokeButton.hidden = true;
}
if (!data.pending.length) {
pendingRequests.className = "request-list empty";
pendingRequests.textContent = "暂时没有申请。";
return;
}
pendingRequests.className = "request-list";
pendingRequests.innerHTML = data.pending.map((request) => `
<article class="request">
<div class="identity"><strong>${escapeHtml(request.label)}</strong><code>${escapeHtml(request.client_id)}</code><span>来源 ${escapeHtml(request.client_address)} · ${escapeHtml(request.requested_at)}</span></div>
<div class="actions">
<button type="button" data-action="approve" data-client-id="${escapeHtml(request.client_id)}">同意</button>
<button type="button" class="secondary" data-action="deny" data-client-id="${escapeHtml(request.client_id)}">拒绝</button>
</div>
</article>`).join("");
}
async function refresh() {
try {
render(await api("/api/local/hardware-access"));
document.querySelector("#clock").textContent = new Date().toLocaleTimeString("zh-CN", { hour12: false });
} catch (error) {
showMessage(error.message, true);
}
}
pendingRequests.addEventListener("click", async (event) => {
const button = event.target.closest("button[data-action]");
if (!button) return;
button.disabled = true;
try {
const action = button.dataset.action;
render(await api(`/api/local/hardware-access/${action}`, { client_id: button.dataset.clientId }));
showMessage(action === "approve" ? "已授予该终端真实台架使用权。" : "已拒绝该终端的申请。");
} catch (error) {
showMessage(error.message, true);
} finally {
button.disabled = false;
}
});
revokeButton.addEventListener("click", async () => {
revokeButton.disabled = true;
try {
render(await api("/api/local/hardware-access/revoke", { client_id: revokeButton.dataset.clientId }));
showMessage("已停止该终端的真实台架使用权;再次使用必须重新申请。");
} catch (error) {
showMessage(error.message, true);
} finally {
revokeButton.disabled = false;
}
});
refresh();
setInterval(refresh, 2000);
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

+48 -490
View File
@@ -2,496 +2,54 @@
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="color-scheme" content="light" />
<title>智舱随行 · 一句话与手势控制</title>
<link rel="stylesheet" href="/styles.css" />
<script src="https://unpkg.com/lucide@0.468.0/dist/umd/lucide.min.js" defer></script>
<script src="https://unpkg.com/three@0.147.0/build/three.min.js" defer></script>
<script src="https://unpkg.com/three@0.147.0/examples/js/libs/fflate.min.js" defer></script>
<script src="https://unpkg.com/three@0.147.0/examples/js/loaders/FBXLoader.js" defer></script>
<script src="/app.js?v=unified-cabin-22" defer></script>
<script type="module" src="/gesture-control.js?v=gesture-core-3"></script>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="color-scheme" content="light dark" />
<meta name="theme-color" content="#17182b" id="themeColorMeta" />
<meta name="mobile-web-app-capable" content="yes" />
<link rel="manifest" href="/manifest.webmanifest" />
<link rel="icon" href="/icons/app-icon-192.png" />
<title>511工程-智能座舱</title>
<script>
(() => {
const key = "cognitive-agent.glass-theme";
let theme = "dark";
try {
const stored = localStorage.getItem(key);
if (stored === "light" || stored === "dark") theme = stored;
} catch (_error) {
// Storage can be unavailable in hardened or private browser contexts.
}
try {
const cookieTheme = document.cookie
.split(";")
.map((part) => part.trim())
.find((part) => part.startsWith(`${key}=`))
?.slice(key.length + 1);
if (cookieTheme === "light" || cookieTheme === "dark") theme = cookieTheme;
} catch (_error) {
// The default still applies when cookies are disabled.
}
document.documentElement.dataset.glassTheme = theme;
document.documentElement.style.colorScheme = theme;
})();
</script>
<script src="/vendor/lucide/0.468.0/lucide.min.js" defer></script>
<script src="/vendor/three/0.147.0/three.min.js" defer></script>
<script src="/vendor/three/0.147.0/fflate.min.js" defer></script>
<script src="/vendor/three/0.147.0/FBXLoader.js" defer></script>
<script src="/network.js?v=android-pad-2" defer></script>
<script src="/app.js?v=dual-seat-141" defer></script>
<script src="/speech-input.js?v=sensevoice-13" defer></script>
<script type="module" src="/gesture-control.js?v=android-pad-9"></script>
<script src="/pwa.js?v=android-pad-3" defer></script>
<script src="/console-v2.js?v=local-first-76" defer></script>
<script src="/self-extension.js?v=pet-settings-52" defer></script>
<script src="/assistant-chat.js?v=codex-assistant-34" defer></script>
<script src="/energy-monitor.js?v=energy-monitor-4" defer></script>
</head>
<body>
<header class="topbar">
<div class="brand-block">
<span class="brand-mark" aria-hidden="true"><i data-lucide="brain-circuit"></i></span>
<div>
<h1>Cognitive Agent</h1>
<p>CABIN ORCHESTRATION <span>/</span> MULTI-DOMAIN</p>
</div>
</div>
<div class="runtime-badges" aria-label="运行环境">
<button class="gesture-entry" id="gestureToggleButton" type="button" aria-controls="gestureDrawer" aria-expanded="false">
<span class="gesture-entry-dot"></span><i data-lucide="scan-face"></i>手势控制
</button>
<span class="status-badge neutral" id="executionBadge"><i data-lucide="flask-conical"></i>DRY-RUN</span>
<span class="status-badge" id="modelBadge"><i data-lucide="sparkles"></i>模型检测中</span>
<span class="status-badge danger" id="hardwareBadge"><i data-lucide="shield-off"></i>硬件检测中</span>
<button class="status-badge auth-logout" id="logoutButton" type="button" title="退出登录" aria-label="退出登录" hidden>
<i data-lucide="log-out"></i>
</button>
</div>
</header>
<div class="workspace">
<aside class="command-panel">
<div class="panel-heading">
<span class="section-kicker">NATURAL CABIN CONTROL</span>
<h2>说一句,就调好了</h2>
<p class="panel-intro">座椅、空调、天幕与氛围灯,一句话联动完成。</p>
</div>
<div class="mode-rail" aria-label="快捷功能模式">
<div class="mode-rail-heading"><span>快捷模式</span><button id="saveModeToggle" type="button"><i data-lucide="bookmark-plus"></i>保存当前</button></div>
<div class="mode-chips">
<button type="button" data-mode-command="进入观影模式:副驾驶调整为舒适观影姿态,空调24度、风量2档,天幕调暗,氛围灯切换成暖橙色"><i data-lucide="clapperboard"></i>观影</button>
<button type="button" data-mode-command="进入午休模式:副驾驶靠背后倾,腿托伸出,空调25度,天幕最暗,氛围灯切换为柔和暖色"><i data-lucide="moon-star"></i>午休</button>
<button type="button" data-mode-command="副驾驶调整为零重力模式"><i data-lucide="orbit"></i>零重力</button>
</div>
<div class="save-mode-composer" id="saveModeComposer" hidden>
<input id="saveModeName" maxlength="24" placeholder="给当前模式起个名字" />
<button id="saveModeConfirm" type="button">保存</button>
</div>
</div>
<form id="requestForm">
<div class="field-label-row">
<label for="utterance">自然语言指令</label>
<button class="context-reset" id="newConversation" type="button" title="开始新会话" aria-label="开始新会话">
<i data-lucide="message-square-plus"></i>
</button>
</div>
<div class="command-composer">
<textarea id="utterance" name="utterance" maxlength="1000" placeholder="例如:有点热,把副驾驶调舒服些,再把天幕调暗">进入观影状态:空调24度、风量2档,天幕调暗,氛围灯换成暖橙色</textarea>
<span class="composer-hint">Enter 执行 · Shift + Enter 换行</span>
</div>
<div class="field-group">
<span class="field-label">意图解析器</span>
<div class="segmented" role="group" aria-label="意图解析器">
<button type="button" class="segment active" data-provider="openai">
<i data-lucide="sparkles"></i>GPT
</button>
<button type="button" class="segment" data-provider="local">
<i data-lucide="braces"></i>本地规则
</button>
</div>
<p class="provider-note" id="providerNote">正在读取运行时配置</p>
</div>
<div class="field-group">
<span class="field-label">执行目标</span>
<div class="segmented" role="group" aria-label="执行目标">
<button type="button" class="mode-option active" data-execution-mode="dry_run">
<i data-lucide="flask-conical"></i>虚拟 CAN
</button>
<button type="button" class="mode-option" id="hardwareMode" data-execution-mode="hardware" disabled>
<i data-lucide="radio-tower"></i>真实台架
</button>
</div>
<p class="provider-note" id="hardwareNote">正在检查 CAN 适配器与 IAR 固件配置</p>
<p class="provider-note" id="hardwareAliasNote">主驾和副驾暂时共享同一组实体电机、加热与通风执行器</p>
</div>
<div class="field-group vehicle-field">
<label class="field-label" for="vehicleSelect">车型配置</label>
<div class="vehicle-select-control">
<i data-lucide="car-front"></i>
<select id="vehicleSelect" name="vehicle_id" aria-label="选择车型配置">
<option value="">加载车型配置</option>
</select>
</div>
<p class="provider-note" id="vehicleNote">正在读取座椅量程与角度标定</p>
</div>
<div class="field-group approval-field" id="approvalField" hidden>
<label class="field-label" for="approvalToken">硬件执行授权令牌</label>
<input id="approvalToken" name="approval_token" type="password" inputmode="numeric" maxlength="6" autocomplete="off" spellcheck="false" placeholder="请输入 123456" />
<p class="provider-note">演示授权令牌为 123456,仅用于本次安全校验,不会写入日志、会话或执行制品。</p>
<p class="provider-note" id="autoFlashNote">正在检查自动烧录配置</p>
</div>
<button class="primary-action" id="runButton" type="submit">
<i data-lucide="play"></i>
<span>生成并执行</span>
</button>
</form>
<div class="example-group">
<span class="field-label">示例请求</span>
<button type="button" class="example-command" data-example="空调打开,车内温度调到24度,风量3档">
<i data-lucide="fan"></i><span>空调 24℃ · 风量 3 档</span><i data-lucide="chevron-right"></i>
</button>
<button type="button" class="example-command" data-example="天幕透光率调到30%,遮阳帘打开一半">
<i data-lucide="panels-top-left"></i><span>天幕透光 30% · 遮阳帘 50%</span><i data-lucide="chevron-right"></i>
</button>
<button type="button" class="example-command" data-example="打开氛围灯,调成冰蓝色,亮度60%">
<i data-lucide="lamp-ceiling"></i><span>冰蓝氛围灯 · 亮度 60%</span><i data-lucide="chevron-right"></i>
</button>
<button type="button" class="example-command featured-example" data-example="进入观影状态:副驾调整为舒适观影姿态,空调24度、风量2档,天幕调暗,氛围灯换成暖橙色">
<i data-lucide="clapperboard"></i><span>跨域观影场景</span><i data-lucide="chevron-right"></i>
</button>
<button type="button" class="example-command" data-example="副驾有点热,有点闷">
<i data-lucide="wind"></i><span>副驾有点热,有点闷</span><i data-lucide="chevron-right"></i>
</button>
<button type="button" class="example-command" data-example="副驾靠背向后移动5度">
<i data-lucide="move-horizontal"></i><span>副驾靠背向后 5°</span><i data-lucide="chevron-right"></i>
</button>
<button type="button" class="example-command" data-example="副驾调整为零重力模式,要求靠背座椅夹角128度,坐垫脚托夹角133度">
<i data-lucide="compass"></i><span>副驾零重力精确夹角</span><i data-lucide="chevron-right"></i>
</button>
<button type="button" class="example-command" data-example="将当前状态保存为零重力模式">
<i data-lucide="bookmark-plus"></i><span>保存当前座椅姿态</span><i data-lucide="chevron-right"></i>
</button>
<button type="button" class="example-command warning-example" data-example="副驾座椅加热开到三档">
<i data-lucide="triangle-alert"></i><span>三档请求(档位虚拟)</span><i data-lucide="chevron-right"></i>
</button>
</div>
<div class="safety-strip" id="safetyStrip">
<i data-lucide="shield-check"></i>
<div>
<strong id="safetyTitle">虚拟执行边界</strong>
<span id="safetyText">四域 CAN 均为 dry-run,实车发送被强制关闭。</span>
</div>
</div>
</aside>
<main class="main-panel">
<section class="request-header">
<div>
<span class="section-kicker">ORCHESTRATION RUN</span>
<div class="request-title-row">
<h2 id="resultTitle">等待请求</h2>
<span class="run-status idle" id="runStatus">IDLE</span>
</div>
<p id="requestMeta">Cabin / reference_vehicle / dry-run</p>
</div>
<div class="matrix-stat">
<i data-lucide="table-properties"></i>
<div><strong id="matrixCount">10 / 57</strong><span>报文 / 信号</span></div>
</div>
</section>
<section class="pipeline" aria-label="执行链路">
<div class="pipeline-step" data-stage="intent_parse"><span>01</span><i data-lucide="message-square-text"></i><b>意图抽取</b></div>
<i class="pipeline-arrow" data-lucide="chevron-right"></i>
<div class="pipeline-step" data-stage="function_generate"><span>02</span><i data-lucide="file-code-2"></i><b>配置生成</b></div>
<i class="pipeline-arrow" data-lucide="chevron-right"></i>
<div class="pipeline-step" data-stage="domain_validate"><span>03</span><i data-lucide="badge-check"></i><b>双层校验</b></div>
<i class="pipeline-arrow" data-lucide="chevron-right"></i>
<div class="pipeline-step" data-stage="compile"><span>04</span><i data-lucide="binary"></i><b>CAN 编译</b></div>
<i class="pipeline-arrow" data-lucide="chevron-right"></i>
<div class="pipeline-step" data-stage="safety_check"><span>05</span><i data-lucide="shield-check"></i><b>安全门禁</b></div>
<i class="pipeline-arrow" data-lucide="chevron-right"></i>
<div class="pipeline-step" data-stage="execute"><span>06</span><i data-lucide="radio-tower"></i><b id="executeStageLabel">虚拟执行</b></div>
</section>
<section class="cabin-band" aria-labelledby="cabinStateTitle">
<div class="section-title-row cabin-heading">
<div>
<span class="section-kicker">JOINT DEVICE STATE</span>
<h3 id="cabinStateTitle">联合设备状态</h3>
</div>
<span class="simulation-label" id="feedbackModeLabel">MATRIX-BACKED · SIMULATED FEEDBACK</span>
</div>
<div class="domain-grid">
<article class="domain-panel climate-panel" data-domain-panel="climate">
<header>
<span class="domain-icon"><i data-lucide="fan"></i></span>
<div><strong>空调</strong><small>CLIMATE · 0x064</small></div>
<span class="device-state" id="climatePower">OFF</span>
</header>
<div class="climate-display">
<div class="zone-temperature">
<span>主驾</span><strong id="driverTemperature">24<sup>℃</sup></strong>
</div>
<div class="airflow-visual" id="airflowVisual" data-level="0" aria-label="空调风量">
<i></i><i></i><i></i><i></i><i></i><i></i><i></i>
</div>
<div class="zone-temperature passenger-temperature">
<span>副驾</span><strong id="passengerTemperature">24<sup>℃</sup></strong>
</div>
</div>
<dl class="domain-readouts">
<div><dt>风量</dt><dd id="fanLevel">0 档</dd></div>
<div><dt>循环</dt><dd id="cycleMode">外循环</dd></div>
<div><dt>A/C</dt><dd id="acState">关闭</dd></div>
<div><dt>前除霜</dt><dd id="defrostState">关闭</dd></div>
</dl>
</article>
<article class="domain-panel roof-panel" data-domain-panel="roof">
<header>
<span class="domain-icon"><i data-lucide="panels-top-left"></i></span>
<div><strong>智能天幕</strong><small>ROOF · 0x4C1 / 0x3E6</small></div>
<span class="device-state" id="roofState">READY</span>
</header>
<div class="roof-visual" aria-label="天幕透光率与遮阳帘位置">
<div class="roof-glass" id="roofGlass">
<span class="roof-shade" id="roofShade"></span>
<i></i><i></i><i></i>
</div>
</div>
<dl class="domain-readouts">
<div><dt>透光率</dt><dd id="transmittanceValue">100%</dd></div>
<div><dt>遮阳帘</dt><dd id="shadePositionValue">0%</dd></div>
<div><dt>自动调光</dt><dd id="automaticDimming">关闭</dd></div>
<div><dt>电机反馈</dt><dd id="roofMotorState">已停止</dd></div>
</dl>
</article>
<article class="domain-panel ambient-panel" data-domain-panel="ambient_light">
<header>
<span class="domain-icon"><i data-lucide="lamp-ceiling"></i></span>
<div><strong>氛围灯</strong><small>AMBIENT · RGB 16.8M</small></div>
<span class="device-state" id="ambientPower">OFF</span>
</header>
<div class="ambient-visual" id="ambientVisual" aria-label="副驾手套箱上方九段氛围灯排灯">
<div class="ambient-dashboard-sketch" aria-hidden="true">
<span class="ambient-dashboard-top"></span>
<span class="ambient-center-screen"></span>
<span class="ambient-glovebox"></span>
<span class="ambient-glovebox-lamp">
<i class="ambient-line"></i><i class="ambient-line"></i><i class="ambient-line"></i>
<i class="ambient-line"></i><i class="ambient-line"></i><i class="ambient-line"></i>
<i class="ambient-line"></i><i class="ambient-line"></i><i class="ambient-line"></i>
</span>
</div>
<small class="ambient-location-label">副驾 · 手套箱上方排灯</small>
</div>
<dl class="domain-readouts">
<div><dt>颜色</dt><dd><i class="color-swatch" id="ambientSwatch"></i><span id="ambientColor">#74D9FF</span></dd></div>
<div><dt>亮度</dt><dd id="ambientBrightness">0%</dd></div>
<div><dt>模式</dt><dd id="ambientMode">关闭</dd></div>
<div><dt>控制精度</dt><dd>24-bit RGB</dd></div>
</dl>
</article>
</div>
</section>
<section class="twin-band" aria-labelledby="twinTitle">
<div class="twin-heading">
<div>
<span class="section-kicker">FULL VEHICLE DIGITAL TWIN</span>
<h3 id="twinTitle">整车孪生界面</h3>
<div class="preset-indicator" id="presetIndicator" hidden>
<i data-lucide="bookmark-check"></i><span></span>
</div>
</div>
<div class="twin-toolbar">
<span class="twin-focus-badge"><i data-lucide="scan"></i>透明车体 · 实体设备</span>
<button class="icon-button twin-reset" id="twinReset" type="button" title="复位 3D 视角" aria-label="复位 3D 视角">
<i data-lucide="rotate-3d"></i>
</button>
</div>
</div>
<div class="twin-stage" id="seatTwin" data-selected-seat="passenger">
<canvas id="seatTwinCanvas" data-vehicle-model="byd-tang-2024-reference" data-seat-layout="five-seat" data-active-seat="front-right" aria-label="具有前后遮挡明暗层次的半透明整车外壳、始终清晰显示的动态副驾、全景天幕、仪表台空调出风口与副驾手套箱上方氛围灯三维状态"></canvas>
<div class="vehicle-system-hud" aria-label="整车孪生系统状态">
<span data-twin-system="roof"><i data-lucide="panels-top-left"></i><small>全景天幕</small><b id="twinRoofState">透光 100%</b></span>
<span data-twin-system="climate"><i data-lucide="wind"></i><small>仪表台 HVAC</small><b id="twinClimateState">关闭</b></span>
<span data-twin-system="ambient"><i data-lucide="lamp-ceiling"></i><small>手套箱氛围灯</small><b id="twinAmbientState">关闭</b></span>
</div>
<div class="twin-seat-label passenger"><i></i><span>副驾动态座椅</span><small>ACTIVE · FRONT RIGHT</small></div>
<div class="twin-callout-layer" aria-hidden="true">
<div class="twin-callout" data-callout="longitudinal"><span>水平位置</span><b>50%</b><small>导轨</small></div>
<div class="twin-callout" data-callout="cushion_tilt"><span>坐垫俯仰</span><b>7°</b><small>50%</small></div>
<div class="twin-callout" data-callout="backrest_recline"><span>靠背夹角</span><b>20.5°</b><small>20% · 相对竖直</small></div>
<div class="twin-callout" data-callout="legrest_height"><span>腿托夹角</span><b>78.5°</b><small>0% · 相对地板</small></div>
<div class="twin-callout" data-callout="legrest_extension"><span>腿托伸缩</span><b>0%</b><small>行程</small></div>
<div class="twin-callout included-angle-callout" data-callout="backrest_included_angle" hidden><span>靠背-坐垫夹角</span><b>104°</b><small>本轮指定夹角</small></div>
<div class="twin-callout included-angle-callout" data-callout="legrest_included_angle" hidden><span>坐垫-腿托夹角</span><b>95°</b><small>本轮指定夹角</small></div>
</div>
<div class="twin-orientation"><i data-lucide="move-down"></i><span>车头方向</span></div>
<aside class="twin-readout" aria-live="polite">
<div class="twin-readout-heading">
<div><strong id="twinSeatName">副驾</strong><span id="twinSeatZone">front_right</span></div>
<span class="twin-feedback"><i></i>FEEDBACK</span>
</div>
<div class="twin-axis-list">
<div data-twin-axis="longitudinal"><span>水平位置</span><b>50%</b><i></i></div>
<div data-twin-axis="cushion_tilt"><span>坐垫俯仰</span><b>50%</b><i></i></div>
<div data-twin-axis="backrest_recline"><span>靠背夹角</span><b>20% · 104°</b><i></i></div>
<div data-twin-axis="legrest_height"><span>腿托夹角</span><b>0% · 95°</b><i></i></div>
<div data-twin-axis="legrest_extension"><span>腿托伸缩</span><b>0%</b><i></i></div>
</div>
<div class="twin-thermal-row" aria-label="座椅热舒适状态">
<span data-twin-function="ventilation" data-level="0">
<i data-lucide="wind"></i>
<span class="twin-thermal-copy"><small>通风</small><b>关闭</b></span>
<span class="twin-level-meter" aria-hidden="true"><i></i><i></i><i></i></span>
</span>
<span data-twin-function="heating" data-level="0">
<i data-lucide="flame"></i>
<span class="twin-thermal-copy"><small>加热</small><b>关闭</b></span>
<span class="twin-level-meter" aria-hidden="true"><i></i><i></i><i></i></span>
</span>
</div>
</aside>
<div class="twin-fallback" id="twinFallback"><i data-lucide="loader-circle"></i><span>正在加载整车参考模型</span></div>
</div>
</section>
<section class="state-band">
<div class="seat-state-section">
<div class="section-title-row">
<div><span class="section-kicker">ACTUATOR STATE</span><h3>前排座椅状态</h3></div>
<span class="simulation-label" id="seatFeedbackModeLabel">SIMULATED FEEDBACK</span>
</div>
<div class="seat-grid">
<article class="seat-card" data-seat="driver">
<div class="seat-identity"><i data-lucide="armchair"></i><div><strong>主驾</strong><span>front_left</span></div></div>
<div class="thermal-states">
<span data-function="ventilation"><i data-lucide="wind"></i>通风 <b>--</b></span>
<span data-function="heating"><i data-lucide="flame"></i>加热 <b>--</b></span>
</div>
<div class="motion-state-grid" aria-live="polite"></div>
</article>
<article class="seat-card" data-seat="passenger">
<div class="seat-identity"><i data-lucide="armchair"></i><div><strong>副驾</strong><span>front_right</span></div></div>
<div class="thermal-states">
<span data-function="ventilation"><i data-lucide="wind"></i>通风 <b>--</b></span>
<span data-function="heating"><i data-lucide="flame"></i>加热 <b>--</b></span>
</div>
<div class="motion-state-grid" aria-live="polite"></div>
</article>
</div>
</div>
<div class="coverage-section">
<span class="section-kicker">MATRIX COVERAGE</span>
<h3>当前控制边界</h3>
<dl>
<div><dt>空调 18–32℃ / 0–7 档</dt><dd class="available">可编译</dd></div>
<div><dt>天幕 / 遮阳帘 0–100%</dt><dd class="available">可编译</dd></div>
<div><dt>氛围灯 RGB / 亮度</dt><dd class="available">可编译</dd></div>
<div><dt>副驾座椅统一执行器</dt><dd class="simulated">t2g_primary_seat</dd></div>
<div><dt>真实台架 CAN 下发</dt><dd class="blocked" id="hardwareCoverage">未配置</dd></div>
</dl>
</div>
</section>
<section class="detail-section result-workspace" aria-labelledby="resultWorkspaceTitle">
<header class="result-workspace-heading">
<div><span class="section-kicker">ORCHESTRATION WORKSPACE</span><h3 id="resultWorkspaceTitle">编排结果工作台</h3><p>四类结果在同一页面连续呈现,也可以通过下方入口快速定位。</p></div>
<span class="result-workspace-badge"><i data-lucide="route"></i>REQUEST → CONFIG → CAN → RESULT</span>
</header>
<nav class="tabs result-nav" aria-label="结果快速导航">
<button class="tab active" type="button" data-tab="trace"><span class="tab-index">01</span><i data-lucide="list-tree"></i><span class="tab-copy"><strong>决策轨迹</strong><small id="traceNavMeta">等待请求</small></span><i data-lucide="arrow-down"></i></button>
<button class="tab" type="button" data-tab="config"><span class="tab-index">02</span><i data-lucide="layout-grid"></i><span class="tab-copy"><strong>配置卡片</strong><small id="configNavMeta">等待生成</small></span><i data-lucide="arrow-down"></i></button>
<button class="tab" type="button" data-tab="plan"><span class="tab-index">03</span><i data-lucide="binary"></i><span class="tab-copy"><strong>CAN 计划</strong><small id="planNavMeta">等待编译</small></span><i data-lucide="arrow-down"></i></button>
<button class="tab" type="button" data-tab="result"><span class="tab-index">04</span><i data-lucide="circle-check-big"></i><span class="tab-copy"><strong>执行结果</strong><small id="resultNavMeta">尚未执行</small></span><i data-lucide="arrow-down"></i></button>
</nav>
<div class="tab-panel active" id="panel-trace" role="tabpanel">
<div class="workspace-panel-title"><div><span class="section-kicker">01 · DECISION TRACE</span><h3>决策轨迹与校验</h3><p>从自然语言识别到领域路由,集中查看每一步决策依据。</p></div><span class="workspace-panel-state" id="tracePanelState">IDLE</span></div>
<div class="trace-layout">
<div>
<div class="detail-heading"><h3>可审计决策摘要</h3><span id="providerUsed">--</span></div>
<ol class="decision-list" id="decisionList"><li>提交请求后显示识别目标、能力选择和约束来源。</li></ol>
</div>
<div>
<div class="detail-heading"><h3>阶段事件</h3><span id="traceDuration">0 ms</span></div>
<div class="trace-list" id="traceList"><div class="empty-line">暂无执行事件</div></div>
</div>
</div>
<div class="validation-block">
<div class="detail-heading"><h3>校验结果</h3><span id="validationCount">0 条</span></div>
<div id="validationList" class="validation-list"><div class="empty-line">等待配置生成</div></div>
</div>
</div>
<div class="tab-panel config-panel" id="panel-config" role="tabpanel">
<div class="config-panel-heading">
<div><span class="section-kicker">02 · INSTRUCTION → HARDWARE</span><h3>指令配置与硬件映射</h3><p>每张卡片对应一个控制对象,依次展示目标参数和最终硬件计划。</p></div>
<div class="config-actions"><button class="config-action" type="button" data-copy="config"><i data-lucide="copy"></i>复制 YAML</button><button class="config-action" type="button" data-download="config"><i data-lucide="download"></i>下载配置</button></div>
</div>
<div class="config-card-grid" id="configCards">
<div class="config-empty"><i data-lucide="panel-top-dashed"></i><strong>等待生成配置</strong><span>提交车舱控制指令后,这里会显示控制对象、目标参数和硬件信号。</span></div>
</div>
<details class="config-source-details">
<summary><span><i data-lucide="braces"></i>查看原始配置</span><small id="configFileName">functions/seat.yaml</small></summary>
<pre id="configCode"><code># 等待生成 FunctionConfig</code></pre>
</details>
</div>
<div class="tab-panel plan-panel" id="panel-plan" role="tabpanel">
<div class="workspace-panel-title plan-panel-heading"><div><span class="section-kicker">03 · CAN COMMAND PLAN</span><h3>CAN 编译计划</h3><p>快速核对帧数量、首帧载荷、反馈帧和执行模式。</p></div><button class="config-action" type="button" data-copy="plan"><i data-lucide="copy"></i>复制计划</button></div>
<div class="frame-summary" id="frameSummary"><span>Frame ID</span><b>--</b><span>Payload</span><code>--</code><span>Mode</span><b>DRY-RUN</b></div>
<details class="config-source-details plan-source-details">
<summary><span><i data-lucide="binary"></i>查看原始 CAN 计划</span><small id="planFileName">command-plans/seat.yaml</small></summary>
<pre id="planCode"><code># 等待 CAN 编译</code></pre>
</details>
</div>
<div class="tab-panel result-panel" id="panel-result" role="tabpanel">
<div class="workspace-panel-title"><div><span class="section-kicker">04 · EXECUTION RESULT</span><h3>执行结果与硬件回读</h3><p>汇总每个领域的执行状态、CAN 回读和失败原因。</p></div><span class="workspace-panel-state" id="resultPanelState">IDLE</span></div>
<div class="execution-summary" id="executionSummary">
<i data-lucide="circle-dashed"></i><div><strong>尚未执行</strong><span>运行结果将在此处显示</span></div>
</div>
<div class="result-list" id="resultList"></div>
</div>
</section>
</main>
</div>
<div class="gesture-backdrop" id="gestureBackdrop" hidden></div>
<aside class="gesture-drawer" id="gestureDrawer" aria-hidden="true" aria-labelledby="gestureDrawerTitle">
<header class="gesture-drawer-header">
<div>
<span class="section-kicker">VISION CONTROL</span>
<h2 id="gestureDrawerTitle">手势控制</h2>
<p>本机识别,动作通过当前编排链路执行。</p>
</div>
<button class="gesture-close" id="gestureCloseButton" type="button" aria-label="关闭手势控制"><i data-lucide="x"></i></button>
</header>
<div class="gesture-status-strip">
<span class="gesture-state-pill idle" id="gestureStatePill"><i></i><b>未启动</b></span>
<span id="gestureSafetyText">摄像头尚未开启</span>
</div>
<div class="gesture-camera-stage">
<video id="gestureVideo" playsinline muted></video>
<canvas id="gestureCanvas"></canvas>
<div class="gesture-camera-empty" id="gestureCameraEmpty">
<span><i data-lucide="scan-face"></i></span>
<strong>用手势控制副驾驶座椅</strong>
<p>画面只在本机处理,不上传视频。</p>
</div>
<div class="gesture-live-badge" id="gestureLiveBadge" hidden><i></i>LIVE</div>
</div>
<div class="gesture-now">
<div><span>当前手势</span><strong id="gestureDetected">—</strong></div>
<i data-lucide="arrow-right"></i>
<div><span>期望动作</span><strong id="gestureExpected">等待启动</strong></div>
</div>
<div class="gesture-progress"><i id="gestureProgressBar"></i></div>
<div class="gesture-actions">
<button class="gesture-primary" id="gestureStartButton" type="button"><i data-lucide="camera"></i><span>开启摄像头</span></button>
<button id="gestureArmButton" type="button" disabled><i data-lucide="hand"></i>激活控制</button>
<button id="gestureStopButton" type="button" disabled><i data-lucide="octagon"></i>停止</button>
</div>
<section class="gesture-guide-card">
<div class="gesture-card-heading"><div><span>动作速查</span><small>张掌激活 · 握拳只停止</small></div><button id="gestureCustomizeButton" type="button"><i data-lucide="sliders-horizontal"></i>自定义</button></div>
<div class="gesture-map-grid" id="gestureMapGrid"></div>
<div class="gesture-customize" id="gestureCustomizePanel" hidden>
<div><span>识别到</span><strong id="gestureCustomizeToken">请先摆出手势</strong></div>
<select id="gestureActionSelect" aria-label="选择座椅动作"></select>
<button id="gestureBindButton" type="button">绑定当前手势</button>
<button id="gestureResetMapping" type="button">恢复默认</button>
</div>
</section>
<footer class="gesture-footnote"><i data-lucide="shield-check"></i><span>换动作无需重新张掌;连续 3 秒无有效动作后自动锁止。</span></footer>
</aside>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<body class="console-v2">
<div id="app" aria-busy="true"></div>
<noscript>需要启用 JavaScript 才能使用智能座舱控制台。</noscript>
<script type="module" src="/src/main.js"></script>
</body>
</html>
+73 -29
View File
@@ -1,10 +1,52 @@
:root {
color: #17232b;
background: #e8efed;
color-scheme: light;
--background: oklch(0.968 0.019 244);
--foreground: oklch(0.29 0.042 252);
--card: oklch(0.993 0.007 245);
--card-foreground: oklch(0.34 0.032 250);
--popover: oklch(0.998 0.004 245);
--popover-foreground: oklch(0.29 0.042 252);
--primary: oklch(0.57 0.19 258);
--primary-foreground: oklch(0.99 0.004 245);
--secondary: oklch(0.94 0.03 244);
--secondary-foreground: oklch(0.38 0.045 250);
--muted: oklch(0.955 0.017 245);
--muted-foreground: oklch(0.53 0.035 248);
--accent: oklch(0.918 0.045 249);
--accent-foreground: oklch(0.38 0.1 258);
--destructive: oklch(0.61 0.19 24);
--border: oklch(0.85 0.04 245);
--input: oklch(0.82 0.045 246);
--ring: oklch(0.65 0.14 256);
--radius: 0.875rem;
color: var(--foreground);
background: var(--background);
font-family: Inter, "Noto Sans SC", "Microsoft YaHei", system-ui, sans-serif;
letter-spacing: 0;
}
.dark {
color-scheme: dark;
--background: oklch(0.2 0.035 252);
--foreground: oklch(0.94 0.02 245);
--card: oklch(0.25 0.035 251);
--card-foreground: oklch(0.91 0.022 245);
--popover: oklch(0.235 0.038 252);
--popover-foreground: oklch(0.94 0.02 245);
--primary: oklch(0.7 0.15 252);
--primary-foreground: oklch(0.19 0.04 253);
--secondary: oklch(0.285 0.045 250);
--secondary-foreground: oklch(0.92 0.02 245);
--muted: oklch(0.275 0.032 250);
--muted-foreground: oklch(0.72 0.04 245);
--accent: oklch(0.32 0.065 250);
--accent-foreground: oklch(0.91 0.03 244);
--destructive: oklch(0.7 0.18 24);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.68 0.13 252);
}
* {
box-sizing: border-box;
}
@@ -14,9 +56,9 @@ body {
min-height: 100vh;
margin: 0;
background:
linear-gradient(rgba(217, 229, 225, 0.7) 1px, transparent 1px),
linear-gradient(90deg, rgba(217, 229, 225, 0.7) 1px, transparent 1px),
#eef3f1;
linear-gradient(color-mix(in oklch, var(--border) 70%, transparent) 1px, transparent 1px),
linear-gradient(90deg, color-mix(in oklch, var(--border) 70%, transparent) 1px, transparent 1px),
linear-gradient(145deg, var(--background), var(--secondary));
background-size: 34px 34px;
}
@@ -30,10 +72,12 @@ body {
.login-panel {
width: min(420px, 100%);
padding: 28px;
background: #fff;
border: 1px solid #cbd7d3;
border-top: 4px solid #08766e;
box-shadow: 0 18px 50px rgba(24, 42, 48, 0.12);
color: var(--card-foreground);
background: var(--card);
border: 1px solid var(--border);
border-top: 4px solid var(--primary);
border-radius: var(--radius);
box-shadow: 0 18px 50px oklch(0.29 0.042 252 / 12%);
}
.login-brand {
@@ -41,7 +85,7 @@ body {
align-items: center;
gap: 12px;
padding-bottom: 24px;
border-bottom: 1px solid #d9e1de;
border-bottom: 1px solid var(--border);
}
.login-brand > span {
@@ -49,8 +93,8 @@ body {
height: 42px;
display: grid;
place-items: center;
color: #fff;
background: #08766e;
color: var(--primary-foreground);
background: var(--primary);
border-radius: 5px;
font-size: 13px;
font-weight: 800;
@@ -67,7 +111,7 @@ body {
.login-brand small {
margin-top: 4px;
color: #67747b;
color: var(--muted-foreground);
font: 8px "SFMono-Regular", Consolas, monospace;
}
@@ -77,7 +121,7 @@ body {
.login-heading p {
margin: 0 0 7px;
color: #08766e;
color: var(--primary);
font: 750 9px "SFMono-Regular", Consolas, monospace;
}
@@ -92,7 +136,7 @@ form {
label {
margin: 0 0 7px;
color: #4f5f66;
color: var(--muted-foreground);
font-size: 11px;
font-weight: 700;
}
@@ -102,32 +146,32 @@ input {
height: 44px;
margin-bottom: 18px;
padding: 0 12px;
color: #17232b;
background: #f7f9f8;
border: 1px solid #bbc8c4;
border-radius: 4px;
color: var(--foreground);
background: var(--background);
border: 1px solid var(--input);
border-radius: calc(var(--radius) - 2px);
font: inherit;
letter-spacing: 0;
}
input:focus {
outline: 3px solid rgba(50, 105, 168, 0.16);
border-color: #3269a8;
outline: 3px solid color-mix(in oklch, var(--ring) 24%, transparent);
border-color: var(--ring);
}
.login-message {
min-height: 17px;
margin: -4px 0 10px;
color: #bd3f49;
color: var(--destructive);
font-size: 11px;
}
button {
height: 44px;
color: #fff;
background: #08766e;
color: var(--primary-foreground);
background: var(--primary);
border: 0;
border-radius: 4px;
border-radius: calc(var(--radius) - 2px);
font: inherit;
font-weight: 750;
letter-spacing: 0;
@@ -135,7 +179,7 @@ button {
}
button:hover {
background: #075c57;
background: color-mix(in oklch, var(--primary) 86%, var(--foreground));
}
button:disabled {
@@ -148,7 +192,7 @@ footer {
align-items: center;
gap: 8px;
margin-top: 26px;
color: #67747b;
color: var(--muted-foreground);
font: 8px "SFMono-Regular", Consolas, monospace;
}
@@ -156,8 +200,8 @@ footer i {
width: 7px;
height: 7px;
border-radius: 50%;
background: #08766e;
box-shadow: 0 0 0 4px rgba(8, 118, 110, 0.12);
background: var(--primary);
box-shadow: 0 0 0 4px color-mix(in oklch, var(--primary) 12%, transparent);
}
@media (max-width: 460px) {
+4 -2
View File
@@ -3,9 +3,11 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="color-scheme" content="light" />
<meta name="color-scheme" content="light dark" />
<meta name="theme-color" content="oklch(0.57 0.19 258)" />
<title>登录 · Cognitive Agent</title>
<link rel="stylesheet" href="/login.css" />
<link rel="stylesheet" href="/login.css?v=ai-blue-2" />
<script src="/network.js?v=android-pad-2" defer></script>
<script src="/login.js" defer></script>
</head>
<body>
+2 -2
View File
@@ -4,7 +4,7 @@ function nextPath() {
}
async function readAuthStatus() {
const response = await fetch("/api/auth/status", { cache: "no-store" });
const response = await window.CognitiveAgentNetwork.request("/api/auth/status", { cache: "no-store" });
if (!response.ok) return;
const status = await response.json();
if (status.authenticated) window.location.replace(nextPath());
@@ -18,7 +18,7 @@ async function submitLogin(event) {
message.textContent = "";
try {
const response = await fetch("/api/auth/login", {
const response = await window.CognitiveAgentNetwork.request("/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
+26
View File
@@ -0,0 +1,26 @@
{
"name": "Cognitive Agent PAD Console",
"short_name": "Cognitive Agent",
"description": "车舱认知编排、模拟台架与手势控制 PAD 控制台",
"lang": "zh-CN",
"start_url": "/?source=pwa",
"scope": "/",
"display": "standalone",
"orientation": "any",
"background_color": "oklch(0.968 0.019 244)",
"theme_color": "oklch(0.57 0.19 258)",
"icons": [
{
"src": "/icons/app-icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/icons/app-icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
]
}
+118
View File
@@ -0,0 +1,118 @@
(function initializeCognitiveAgentNetwork() {
const nativeFetch = window.fetch.bind(window);
const listeners = new Set();
const state = {
status: navigator.onLine === false ? "offline" : "connecting",
lastSuccessAt: null,
lastError: null,
};
let probeTimer = null;
function snapshot() {
return { ...state };
}
function publish(status, error = null) {
const changed = state.status !== status || state.lastError !== error;
state.status = status;
state.lastError = error;
if (status === "online") state.lastSuccessAt = new Date().toISOString();
if (!changed) return;
const detail = snapshot();
listeners.forEach((listener) => listener(detail));
window.dispatchEvent(new CustomEvent("cognitive-agent:connection", { detail }));
}
function sleep(milliseconds) {
return new Promise((resolve) => window.setTimeout(resolve, milliseconds));
}
async function request(input, options = {}) {
const {
timeoutMs = 10000,
retries: configuredRetries,
signal: callerSignal,
...fetchOptions
} = options;
const method = String(fetchOptions.method || "GET").toUpperCase();
const retries = configuredRetries ?? (method === "GET" ? 1 : 0);
let lastError;
for (let attempt = 0; attempt <= retries; attempt += 1) {
const controller = new AbortController();
const abortFromCaller = () => controller.abort(callerSignal?.reason);
callerSignal?.addEventListener("abort", abortFromCaller, { once: true });
const timeout = window.setTimeout(() => controller.abort("timeout"), timeoutMs);
try {
const response = await nativeFetch(input, {
...fetchOptions,
signal: controller.signal,
});
publish(response.status >= 500 ? "degraded" : "online",
response.status >= 500 ? `服务返回 HTTP ${response.status}` : null);
return response;
} catch (error) {
lastError = error;
const message = controller.signal.aborted && !callerSignal?.aborted
? `请求超时(${Math.round(timeoutMs / 1000)} 秒)`
: (error?.message || "网络请求失败");
publish(attempt < retries ? "degraded" : "offline", message);
if (callerSignal?.aborted || attempt >= retries || method !== "GET") {
throw new Error(message, { cause: error });
}
await sleep(300 * (attempt + 1));
} finally {
window.clearTimeout(timeout);
callerSignal?.removeEventListener("abort", abortFromCaller);
}
}
throw lastError;
}
async function probe() {
if (document.hidden) return;
if (navigator.onLine === false) {
publish("offline", "设备当前处于离线状态");
return;
}
try {
const response = await request("/api/health", {
cache: "no-store",
timeoutMs: 4000,
retries: 0,
});
if (!response.ok && response.status !== 401) {
publish("degraded", `健康检查返回 HTTP ${response.status}`);
}
} catch (_error) {
// request() already published the detailed state.
}
}
function startMonitoring() {
if (probeTimer !== null) return;
probe();
probeTimer = window.setInterval(probe, 10000);
window.addEventListener("online", () => {
publish("connecting");
probe();
});
window.addEventListener("offline", () => publish("offline", "设备当前处于离线状态"));
document.addEventListener("visibilitychange", () => {
if (!document.hidden) probe();
});
}
window.CognitiveAgentNetwork = {
request,
probe,
subscribe(listener) {
listeners.add(listener);
listener(snapshot());
return () => listeners.delete(listener);
},
getState: snapshot,
nativeFetch,
};
document.addEventListener("DOMContentLoaded", startMonitoring, { once: true });
})();
+1394
View File
File diff suppressed because it is too large Load Diff
+18
View File
@@ -0,0 +1,18 @@
{
"name": "@n7/cognitive-agent-web-console",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite --host 127.0.0.1 --port 5173",
"build": "vite build",
"preview": "vite preview --host 127.0.0.1 --port 4173"
},
"dependencies": {
"vue": "3.5.21"
},
"devDependencies": {
"@vitejs/plugin-vue": "6.0.1",
"vite": "7.3.6"
}
}
+38
View File
@@ -0,0 +1,38 @@
(function initializePwa() {
function notify(message) {
window.CognitiveAgentConsole?.notify?.(message);
}
function showUpdate(registration) {
notify("检测到前端更新,正在刷新页面");
registration.waiting?.postMessage({ type: "SKIP_WAITING" });
}
async function register() {
if (!("serviceWorker" in navigator) || !window.isSecureContext) return;
try {
const registration = await navigator.serviceWorker.register("/service-worker.js", {
scope: "/",
updateViaCache: "none",
});
await registration.update();
if (registration.waiting) showUpdate(registration);
registration.addEventListener("updatefound", () => {
const worker = registration.installing;
worker?.addEventListener("statechange", () => {
if (worker.state === "installed" && navigator.serviceWorker.controller) {
showUpdate(registration);
}
});
});
navigator.serviceWorker.addEventListener("controllerchange", () => window.location.reload());
navigator.serviceWorker.addEventListener("message", (event) => {
if (event.data?.type === "OFFLINE_READY") notify("PAD 离线资源已准备完成");
});
} catch (error) {
console.warn("PWA registration failed", error);
}
}
window.addEventListener("load", register, { once: true });
})();
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+107
View File
@@ -0,0 +1,107 @@
const CACHE_VERSION = "cognitive-agent-pad-v316-sidebar-accordion";
const SHELL_CACHE = `${CACHE_VERSION}-shell`;
const RUNTIME_CACHE = `${CACHE_VERSION}-runtime`;
const APP_SHELL = [
"/",
"/index.html",
"/styles.css?v=dual-seat-23",
"/network.js?v=android-pad-2",
"/app.js?v=dual-seat-141",
"/gesture-control.js?v=android-pad-9",
"/pwa.js?v=android-pad-3",
"/console-v2.css?v=delivery-state-69",
"/console-v2.js?v=local-first-76",
"/self-extension.css?v=pet-settings-52",
"/self-extension.js?v=pet-settings-52",
"/speech-input.css?v=sensevoice-7",
"/speech-input.js?v=sensevoice-13",
"/speech-audio-worklet.js?v=sensevoice-5",
"/assistant-chat.css?v=codex-assistant-29",
"/assistant-chat.js?v=codex-assistant-34",
"/energy-monitor.css?v=energy-monitor-4",
"/energy-monitor.js?v=energy-monitor-4",
"/ui-consistency.css?v=spatial-ui-8",
"/codepen-glass-theme.css?v=creative-cloud-5",
"/vue-ui.js?v=vue-presentation-3",
"/assets/pets/self-extension-cat.png?v=3",
"/assets/pets/self-extension-cat-entrance.png?v=3",
"/manifest.webmanifest",
"/icons/app-icon-192.png",
"/icons/app-icon-512.png",
"/vendor/lucide/0.468.0/lucide.min.js",
"/vendor/vue/3.5.21/vue.runtime.global.prod.js",
"/vendor/three/0.147.0/three.min.js",
"/vendor/three/0.147.0/fflate.min.js",
"/vendor/three/0.147.0/FBXLoader.js",
"/vendor/mediapipe/tasks-vision/0.10.14/vision_bundle.mjs",
"/vendor/mediapipe/tasks-vision/0.10.14/wasm/vision_wasm_internal.js",
"/vendor/mediapipe/tasks-vision/0.10.14/wasm/vision_wasm_internal.wasm",
"/vendor/mediapipe/tasks-vision/0.10.14/wasm/vision_wasm_nosimd_internal.js",
"/vendor/mediapipe/tasks-vision/0.10.14/wasm/vision_wasm_nosimd_internal.wasm"
];
self.addEventListener("install", (event) => {
event.waitUntil(
caches.open(SHELL_CACHE)
.then((cache) => cache.addAll(APP_SHELL))
.then(() => self.skipWaiting())
.then(() => self.clients.matchAll({ includeUncontrolled: true }))
.then((clients) => clients.forEach((client) => client.postMessage({ type: "OFFLINE_READY" })))
);
});
self.addEventListener("activate", (event) => {
event.waitUntil(
caches.keys()
.then((keys) => Promise.all(
keys.filter((key) => key.startsWith("cognitive-agent-pad-")
&& ![SHELL_CACHE, RUNTIME_CACHE].includes(key))
.map((key) => caches.delete(key))
))
.then(() => self.clients.claim())
);
});
async function cacheFirst(request) {
const shellCache = await caches.open(SHELL_CACHE);
const runtimeCache = await caches.open(RUNTIME_CACHE);
const cached = (await shellCache.match(request)) || (await runtimeCache.match(request));
if (cached) return cached;
const response = await fetch(request);
if (response.ok) {
await runtimeCache.put(request, response.clone());
}
return response;
}
async function navigationResponse(request) {
try {
const response = await fetch(request);
if (response.ok) {
const cache = await caches.open(RUNTIME_CACHE);
await cache.put("/index.html", response.clone());
}
return response;
} catch (_error) {
const shellCache = await caches.open(SHELL_CACHE);
const runtimeCache = await caches.open(RUNTIME_CACHE);
return (await runtimeCache.match("/index.html"))
|| (await shellCache.match("/index.html"))
|| Response.error();
}
}
self.addEventListener("fetch", (event) => {
const url = new URL(event.request.url);
if (url.origin !== self.location.origin || event.request.method !== "GET") return;
if (event.request.mode === "navigate") {
event.respondWith(navigationResponse(event.request));
return;
}
if (url.pathname.startsWith("/api/") && url.pathname !== "/api/gesture/model") return;
event.respondWith(cacheFirst(event.request));
});
self.addEventListener("message", (event) => {
if (event.data?.type === "SKIP_WAITING") self.skipWaiting();
});
+29
View File
@@ -0,0 +1,29 @@
class SpeechCaptureProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.frameSize = 2048;
this.frame = new Float32Array(this.frameSize);
this.offset = 0;
}
process(inputs) {
const input = inputs[0]?.[0];
if (!input) return true;
let inputOffset = 0;
while (inputOffset < input.length) {
const available = Math.min(this.frameSize - this.offset, input.length - inputOffset);
this.frame.set(input.subarray(inputOffset, inputOffset + available), this.offset);
this.offset += available;
inputOffset += available;
if (this.offset === this.frameSize) {
const ready = this.frame;
this.port.postMessage(ready, [ready.buffer]);
this.frame = new Float32Array(this.frameSize);
this.offset = 0;
}
}
return true;
}
}
registerProcessor("speech-capture-processor", SpeechCaptureProcessor);
+298
View File
@@ -0,0 +1,298 @@
.speech-visually-hidden {
width: 1px;
height: 1px;
position: absolute;
overflow: hidden;
clip: rect(0 0 0 0);
clip-path: inset(50%);
white-space: nowrap;
}
/* Keep all composer actions on one calm, optically aligned rail. */
.console-v2 .v2-request-form .speech-dictation {
width: 36px;
height: 36px;
min-height: 36px;
position: absolute;
top: 50%;
right: 56px;
bottom: auto;
z-index: 13;
display: grid;
place-items: center;
margin: 0;
padding: 0;
color: var(--foreground);
background: transparent;
border: 0;
border-radius: 50%;
box-shadow: none;
cursor: pointer;
transform: translateY(-50%);
transition: color 160ms ease, background 160ms ease, box-shadow 160ms ease,
transform 160ms ease;
}
.console-v2 .v2-request-form .speech-dictation:hover:not(:disabled),
.console-v2 .v2-request-form .speech-dictation:focus-visible {
color: var(--primary);
background: color-mix(in oklch, var(--primary) 9%, var(--background));
box-shadow: 0 0 0 3px color-mix(in oklch, var(--primary) 8%, transparent);
transform: translateY(-50%) scale(1.05);
}
.console-v2 .v2-request-form .speech-dictation:focus-visible {
outline: 2px solid var(--ring);
outline-offset: 2px;
}
.console-v2 .v2-request-form .speech-dictation:disabled {
color: var(--muted-foreground);
cursor: not-allowed;
opacity: 0.56;
}
.console-v2 .v2-request-form .speech-dictation .speech-mic-icon {
width: 18px;
height: 18px;
stroke-width: 2;
}
.console-v2 .v2-request-form .speech-waveform {
height: 16px;
display: none;
align-items: center;
justify-content: center;
gap: 2px;
}
.console-v2 .v2-request-form .speech-waveform i {
width: 2px;
height: 5px;
display: block;
background: currentColor;
border-radius: 999px;
}
.console-v2 .v2-request-form .speech-waveform i:nth-child(2),
.console-v2 .v2-request-form .speech-waveform i:nth-child(4) {
height: 10px;
}
.console-v2 .v2-request-form .speech-waveform i:nth-child(3) {
height: 15px;
}
.console-v2 .v2-request-form .speech-dictation.is-listening,
.console-v2 .v2-request-form .speech-dictation.is-listening:hover:not(:disabled),
.console-v2 .v2-request-form .speech-dictation.is-listening:focus-visible {
color: var(--primary-foreground);
background: var(--primary);
box-shadow: 0 0 0 5px color-mix(in oklch, var(--primary) 9%, transparent), 0 7px 18px color-mix(in oklch, var(--primary) 20%, transparent);
}
.console-v2 .v2-request-form .speech-dictation.is-armed,
.console-v2 .v2-request-form .speech-dictation.is-armed:hover:not(:disabled),
.console-v2 .v2-request-form .speech-dictation.is-armed:focus-visible {
color: var(--primary);
background: color-mix(in oklch, var(--primary) 10%, var(--background));
box-shadow: 0 0 0 4px color-mix(in oklch, var(--primary) 8%, transparent);
}
.console-v2 .v2-request-form .speech-dictation.is-armed::after {
width: 7px;
height: 7px;
position: absolute;
top: 4px;
right: 3px;
content: "";
background: var(--primary);
border: 2px solid var(--background);
border-radius: 50%;
}
.console-v2 .v2-request-form .speech-dictation.is-listening .speech-mic-icon,
.console-v2 .v2-request-form .speech-dictation.is-processing .speech-mic-icon {
display: none;
}
.console-v2 .v2-request-form .speech-dictation.is-listening .speech-waveform,
.console-v2 .v2-request-form .speech-dictation.is-processing .speech-waveform {
display: flex;
}
.console-v2 .v2-request-form .speech-dictation.is-listening .speech-waveform i {
animation: speech-wave 760ms ease-in-out infinite alternate;
}
.console-v2 .v2-request-form .speech-dictation.is-listening .speech-waveform i:nth-child(2) {
animation-delay: -420ms;
}
.console-v2 .v2-request-form .speech-dictation.is-listening .speech-waveform i:nth-child(3) {
animation-delay: -180ms;
}
.console-v2 .v2-request-form .speech-dictation.is-listening .speech-waveform i:nth-child(4) {
animation-delay: -560ms;
}
.console-v2 .v2-request-form .speech-dictation.is-processing {
color: var(--primary);
background: color-mix(in oklch, var(--primary) 10%, var(--background));
}
.console-v2 .v2-request-form .speech-dictation.is-processing .speech-waveform {
animation: speech-processing-breathe 900ms ease-in-out infinite alternate;
}
.console-v2 .v2-request-form .speech-status {
min-height: 16px;
position: absolute;
right: 140px;
bottom: 10px;
left: 194px;
z-index: 12;
overflow: hidden;
color: var(--muted-foreground);
font-size: 10px;
font-weight: 650;
line-height: 16px;
letter-spacing: 0.01em;
text-overflow: ellipsis;
white-space: nowrap;
pointer-events: none;
}
.console-v2 .v2-request-form .command-composer[data-speech-state="listening"] {
border-color: color-mix(in oklch, var(--primary) 36%, transparent);
box-shadow: 0 12px 35px color-mix(in oklch, var(--primary) 10%, transparent), inset 0 0 0 1px color-mix(in oklch, var(--primary) 8%, transparent);
}
.console-v2 .v2-request-form .command-composer[data-speech-state="armed"],
.console-v2 .v2-request-form .command-composer[data-speech-state="capturing"] {
border-color: color-mix(in oklch, var(--primary) 36%, transparent);
box-shadow: 0 12px 35px color-mix(in oklch, var(--primary) 10%, transparent), inset 0 0 0 1px color-mix(in oklch, var(--primary) 8%, transparent);
}
.console-v2 .v2-request-form .command-composer[data-speech-state="listening"] .speech-status {
color: var(--foreground);
}
.console-v2 .v2-request-form .command-composer[data-speech-state="armed"] .speech-status,
.console-v2 .v2-request-form .command-composer[data-speech-state="capturing"] .speech-status {
color: var(--foreground);
}
.console-v2 .v2-request-form .command-composer[data-speech-state="processing"] .speech-status {
color: color-mix(in oklch, var(--primary) 72%, var(--foreground));
}
.console-v2 .v2-request-form .command-composer[data-speech-state="warming"] .speech-status,
.console-v2 .v2-request-form .command-composer[data-speech-state="arming"] .speech-status,
.console-v2 .v2-request-form .command-composer[data-speech-state="finalizing"] .speech-status {
color: color-mix(in oklch, var(--primary) 72%, var(--foreground));
}
.console-v2 .v2-request-form .command-composer[data-speech-state="success"] .speech-status {
color: var(--primary);
}
.console-v2 .v2-request-form #utterance[data-speech-uncertain="true"] {
background: linear-gradient(90deg, color-mix(in oklch, var(--chart-1) 10%, transparent), transparent 42%);
box-shadow: inset 3px 0 0 color-mix(in oklch, var(--chart-1) 58%, transparent);
}
.console-v2 .v2-request-form #utterance[data-speech-stage="partial"] {
color: color-mix(in oklch, var(--foreground) 82%, var(--muted-foreground));
}
.console-v2 .v2-request-form .command-composer[data-speech-state="error"] .speech-status {
color: var(--destructive);
}
.console-v2 .v2-request-form .command-composer:not([data-speech-state="idle"]) .composer-hint {
opacity: 0;
}
.console-v2 .v2-request-form #utterance,
.console-v2.v2-interactive-sync .v2-request-form #utterance,
.console-v2.v2-passive-display.v2-interactive-sync .v2-request-form #utterance {
padding-right: 142px;
}
.console-v2 .v2-request-form .context-reset,
.console-v2.v2-interactive-sync .v2-request-form .context-reset,
.console-v2.v2-passive-display.v2-interactive-sync .v2-request-form .context-reset {
width: 30px;
height: 30px;
top: 50%;
right: 98px;
bottom: auto;
color: var(--muted-foreground);
border-radius: 50%;
transform: translateY(-50%);
}
.console-v2 .v2-request-form .context-reset:hover,
.console-v2 .v2-request-form .context-reset:focus-visible {
color: var(--primary);
background: color-mix(in oklch, var(--primary) 8%, var(--background));
}
.console-v2 .v2-request-form .composer-hint,
.console-v2.v2-interactive-sync .v2-request-form .composer-hint,
.console-v2.v2-passive-display.v2-interactive-sync .v2-request-form .composer-hint {
right: 140px;
}
@keyframes speech-wave {
from { transform: scaleY(0.42); }
to { transform: scaleY(1); }
}
@keyframes speech-processing-breathe {
from { opacity: 0.42; transform: scaleX(0.78); }
to { opacity: 1; transform: scaleX(1); }
}
@media (max-width: 720px) {
.console-v2 .v2-request-form .speech-status {
top: 8px;
right: 140px;
bottom: auto;
left: 14px;
font-size: 9px;
}
.console-v2 .v2-request-form .command-composer:not([data-speech-state="idle"]) #utterance {
padding-top: 28px;
}
}
@media (max-width: 460px) {
.console-v2 .v2-request-form #utterance,
.console-v2.v2-interactive-sync .v2-request-form #utterance,
.console-v2.v2-passive-display.v2-interactive-sync .v2-request-form #utterance {
padding-right: 132px;
}
.console-v2 .v2-request-form .speech-dictation {
right: 52px;
}
.console-v2 .v2-request-form .context-reset,
.console-v2.v2-interactive-sync .v2-request-form .context-reset,
.console-v2.v2-passive-display.v2-interactive-sync .v2-request-form .context-reset {
right: 92px;
}
}
@media (prefers-reduced-motion: reduce) {
.console-v2 .v2-request-form .speech-dictation,
.console-v2 .v2-request-form .speech-waveform,
.console-v2 .v2-request-form .speech-waveform i {
animation: none !important;
transition: none;
}
}
File diff suppressed because it is too large Load Diff
+584
View File
@@ -0,0 +1,584 @@
<script setup>
defineOptions({ name: "N7ConsoleApp" });
</script>
<template>
<div class="v2-boot-screen" id="v2BootScreen" role="status" aria-live="polite">
<span class="v2-boot-mark" aria-hidden="true"><i></i><i></i><i></i></span>
<strong>511工程-智能座舱</strong>
<small>正在加载车机控制与实时状态</small>
</div>
<div class="workspace">
<aside class="command-panel">
<div class="panel-heading">
<h2>说一句,就调好了</h2>
</div>
<div class="mode-rail" aria-label="功能模式">
<div class="mode-rail-bar">
<div class="mode-rail-identity">
<span class="mode-rail-mark" aria-hidden="true"><i data-lucide="sparkles"></i></span>
<span><strong class="mode-rail-title">功能模式</strong><small>预设与我的模式</small></span>
</div>
<div class="mode-rail-content" id="modeRailContent" role="region" aria-label="功能模式列表">
<div class="mode-chips" tabindex="0" aria-label="快捷功能模式,可横向滚动" aria-live="polite">
<button type="button" data-mode-profile-id="zero_gravity" data-mode-origin="preset" data-mode-command="进入零重力模式"><i data-lucide="orbit"></i><span>零重力</span></button>
<button type="button" data-mode-profile-id="office" data-mode-origin="preset" data-mode-command="进入办公模式"><i data-lucide="briefcase"></i><span>办公</span></button>
<button type="button" data-mode-profile-id="nap" data-mode-origin="preset" data-mode-command="进入小憩模式"><i data-lucide="moon-star"></i><span>小憩</span></button>
</div>
</div>
<div class="mode-rail-actions">
<button class="mode-gallery-toggle" id="modeGalleryToggle" type="button" aria-expanded="false" aria-controls="modeGallery"><i data-lucide="settings-2" data-icon="inline-start"></i><span>管理</span><strong id="modeGalleryCount">0</strong></button>
<button class="mode-create-toggle" id="saveModeToggle" type="button"><i data-lucide="sparkles" data-icon="inline-start"></i><span>AI 新建</span></button>
<button class="mode-rail-toggle" id="modeRailToggle" type="button" aria-expanded="true" aria-controls="modeRailContent" aria-label="收起功能模式" title="收起功能模式">
<span class="visually-hidden" id="modeRailSelection">静态模式</span><i data-lucide="chevron-up"></i>
</button>
</div>
</div>
</div>
<form id="requestForm" autocomplete="off">
<section class="assistant-transcript" id="assistantTranscript" role="log" aria-live="polite" aria-label="AI助手对话">
<div class="assistant-messages" id="assistantMessages">
<article class="assistant-message assistant-message-assistant">
<span class="assistant-avatar" aria-hidden="true"><i data-lucide="waypoints"></i></span>
<div class="assistant-message-body">
<div class="assistant-message-content">你好,我是 AI助手。你可以向我提问,也可以直接交代需要完成的工作。</div>
</div>
</article>
</div>
</section>
<div class="field-label-row">
<label for="utterance">向 AI助手提问或交代工作</label>
<button class="context-reset" id="newConversation" type="button" title="开始新会话" aria-label="开始新会话">
<i data-lucide="message-square-plus"></i>
</button>
</div>
<div class="command-composer">
<textarea id="utterance" name="utterance" maxlength="4000" autocomplete="off" placeholder="向 AI助手提问或交代工作"></textarea>
<button class="speech-dictation" id="speechDictationButton" type="button" aria-label="开始语音输入" aria-pressed="false" title="开始语音输入">
<i class="speech-mic-icon" data-lucide="mic" aria-hidden="true"></i>
<span class="speech-waveform" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i></span>
<span class="speech-visually-hidden">开始语音输入</span>
</button>
<span class="speech-status" id="speechDictationStatus" role="status" aria-live="polite"></span>
<span class="composer-hint">Enter 发送 · Shift + Enter 换行</span>
</div>
<div class="field-group">
<span class="field-label">意图解析器</span>
<div class="segmented" role="group" aria-label="意图解析器">
<button type="button" class="segment active" data-provider="openai">
<i data-lucide="sparkles"></i>GPT
</button>
<button type="button" class="segment" data-provider="local">
<i data-lucide="braces"></i>本地规则
</button>
</div>
<p class="provider-note" id="providerNote">正在读取运行时配置</p>
</div>
<div class="field-group" id="reasoningField">
<span class="field-label">模型推理强度</span>
<div class="segmented reasoning-segmented" role="group" aria-label="模型推理强度">
<button type="button" class="segment reasoning-option" data-reasoning-effort="low">
<i data-lucide="zap"></i><span><strong>极速</strong><small>简单指令,优先响应速度</small></span><i class="reasoning-check" data-lucide="check"></i>
</button>
<button type="button" class="segment reasoning-option active" data-reasoning-effort="medium">
<i data-lucide="scale"></i><span><strong>均衡</strong><small>日常控制,兼顾速度与理解</small></span><i class="reasoning-check" data-lucide="check"></i>
</button>
<button type="button" class="segment reasoning-option" data-reasoning-effort="high">
<i data-lucide="brain"></i><span><strong>深度</strong><small>复杂联动、相对动作与约束</small></span><i class="reasoning-check" data-lucide="check"></i>
</button>
</div>
<p class="provider-note" id="reasoningNote">均衡模式兼顾自然语言解析精度与响应速度</p>
</div>
<div class="field-group">
<span class="field-label">执行目标</span>
<div class="segmented execution-mode-options" role="group" aria-label="执行目标">
<button type="button" class="mode-option" data-execution-mode="dry_run">
<i data-lucide="flask-conical"></i>模拟下发
</button>
<button type="button" class="mode-option active" id="hardwareMode" data-execution-mode="hardware">
<i data-lucide="radio-tower"></i>真实下发
</button>
</div>
</div>
<div class="field-group" id="vehicleTransportField">
<span class="field-label">车辆连接方式</span>
<div class="segmented vehicle-transport-options" role="group" aria-label="车辆连接方式">
<button type="button" class="mode-option active" data-control-transport="wired_can">
<i data-lucide="cable"></i>有线 CAN
</button>
<button type="button" class="mode-option" data-control-transport="wireless_mqtt">
<i data-lucide="wifi"></i>无线 CAN
</button>
</div>
</div>
<div class="field-group simulation-fault-field" id="simulationFaultField" hidden>
<label class="field-label" for="simulationFault">故障注入场景</label>
<select id="simulationFault" aria-label="选择模拟硬件故障">
<option value="normal">正常反馈</option>
<option value="feedback_timeout">反馈超时</option>
<option value="adapter_disconnect">适配器断开</option>
<option value="motor_stall">电机堵转</option>
<option value="firmware_mismatch">固件不匹配</option>
<option value="partial_feedback">部分反馈</option>
</select>
<p class="provider-note">仅生成模拟 HIL 结果,不会打开物理 CAN 适配器或调用 IAR。</p>
</div>
<div class="field-group vehicle-field">
<label class="field-label" for="vehicleSelect">车型配置</label>
<div class="vehicle-select-control">
<i data-lucide="car-front"></i>
<select id="vehicleSelect" name="vehicle_id" aria-label="选择车型配置">
<option value="">加载车型配置</option>
</select>
</div>
<p class="provider-note" id="vehicleNote">正在读取座椅量程与角度标定</p>
</div>
<button class="primary-action" id="runButton" type="submit" aria-label="发送消息">
<i data-lucide="send-horizontal"></i>
<span>发送</span>
</button>
</form>
</aside>
<main class="main-panel">
<section class="request-header">
<div>
<div class="request-title-row">
<h2 id="resultTitle">等待请求</h2>
<span class="run-status idle" id="runStatus">IDLE</span>
</div>
<p id="requestMeta">Cabin / reference_vehicle / dry-run</p>
</div>
<div class="matrix-stat">
<i data-lucide="table-properties"></i>
<div><strong id="matrixCount">10 / 57</strong><span>报文 / 信号</span></div>
</div>
</section>
<section class="cabin-band" aria-labelledby="cabinStateTitle">
<div class="section-title-row cabin-heading">
<div class="cabin-title-copy">
<h3 id="cabinStateTitle">整车设备状态</h3>
</div>
<div class="cabin-channel-legend" aria-label="整车设备通道说明">
<span data-channel="ch0"><i>CH0</i>副驾座椅</span>
<span data-channel="ch1"><i>CH1</i>主驾 / 车身</span>
<span class="simulation-label" id="feedbackModeLabel">MATRIX-BACKED · SIMULATED FEEDBACK</span>
</div>
</div>
<div class="domain-grid">
<article class="domain-panel climate-panel" data-domain-panel="climate" data-device-channel="ch1">
<header>
<span class="domain-icon"><i data-lucide="fan"></i></span>
<div><strong>空调</strong><small>CMD 0x1DE · FB 0x404 · CH1</small></div>
<span class="device-state" id="climatePower">UNKNOWN</span>
</header>
<p class="domain-capability-line"><span>可控参数</span>电源 · 温度 · 风量/风向</p>
<div class="climate-display">
<div class="zone-temperature">
<span>主驾</span><strong id="driverTemperature">--<sup>℃</sup></strong>
<small>风向<b id="driverAirDirection">-</b></small>
</div>
<div class="airflow-visual" id="airflowVisual" data-level="0" aria-label="空调风量">
<i></i><i></i><i></i><i></i><i></i><i></i><i></i>
</div>
<div class="zone-temperature passenger-temperature">
<span>副驾</span><strong id="passengerTemperature">--<sup>℃</sup></strong>
<small>风向<b id="passengerAirDirection">-</b></small>
</div>
</div>
<dl class="domain-readouts">
<div><dt>风量</dt><dd id="fanLevel">未知</dd></div>
<div><dt>循环</dt><dd id="cycleMode">未知</dd></div>
<div><dt>A/C</dt><dd id="acState">未知</dd></div>
<div><dt>前除霜</dt><dd id="defrostState">未知</dd></div>
</dl>
</article>
<article class="domain-panel vehicle-status-panel" data-domain-panel="vehicle_status" data-device-channel="ch1">
<header>
<span class="domain-icon"><i data-lucide="gauge"></i></span>
<div><strong>车辆状态</strong><small>FB 0x342 · 0x4EB · 0x407 · CH1</small></div>
<span class="device-state readonly-state" id="vehicleTelemetryState">只读</span>
</header>
<p class="domain-capability-line readonly-capability"><span>只读信号</span>档位 · 车速 · 四门锁状态</p>
<div class="vehicle-primary-readout">
<div><span>当前档位</span><strong id="vehicleGear">--</strong></div>
<div><span>实时车速</span><strong id="vehicleSpeed">-- km/h</strong></div>
</div>
<div class="door-state-grid" aria-label="四门锁状态">
<span data-door-state="front_left"><i></i>左前门<b id="frontLeftDoorState">未知</b></span>
<span data-door-state="front_right"><i></i>右前门<b id="frontRightDoorState">未知</b></span>
<span data-door-state="rear_left"><i></i>左后门<b id="rearLeftDoorState">未知</b></span>
<span data-door-state="rear_right"><i></i>右后门<b id="rearRightDoorState">未知</b></span>
</div>
</article>
<article class="domain-panel roof-panel" data-domain-panel="roof" data-device-channel="ch1">
<header>
<span class="domain-icon"><i data-lucide="panels-top-left"></i></span>
<div><strong>遮阳帘</strong><small>CMD 0x431 · FB 0x41A · CH1</small></div>
<span class="device-state" id="roofState">UNKNOWN</span>
</header>
<p class="domain-capability-line"><span>可控参数</span>开关 / 停止 · 位置</p>
<div class="domain-summary-visual shade-summary-visual" aria-hidden="true">
<span>闭合</span><i><b id="shadePositionMeter"></b></i><span>打开</span>
</div>
<dl class="domain-readouts">
<div><dt>遮阳帘</dt><dd id="shadePositionValue">未知</dd></div>
<div><dt>动作</dt><dd id="shadeMotionState">未知</dd></div>
<div><dt>初始化</dt><dd id="shadeInitializedState">未知</dd></div>
<div><dt>热保护</dt><dd id="shadeThermalState">未知</dd></div>
</dl>
</article>
<article class="domain-panel ambient-panel" data-domain-panel="ambient_light" data-device-channel="ch1">
<header>
<span class="domain-icon"><i data-lucide="lamp-ceiling"></i></span>
<div><strong>车内灯光</strong><small>INTERIOR / AMBIENT · CH1</small></div>
<span class="device-state" id="ambientPower">UNKNOWN</span>
</header>
<p class="domain-capability-line"><span>可控参数</span>阅读灯 · 氛围亮度 · 色号</p>
<div class="domain-summary-visual ambient-summary-visual" aria-hidden="true">
<span data-ambient-zone="front"><i id="ambientFrontMeter"></i><b>前排</b></span>
<span data-ambient-zone="rear"><i id="ambientRearMeter"></i><b>后排</b></span>
</div>
<dl class="domain-readouts">
<div><dt>氛围灯</dt><dd id="ambientMode">未知</dd></div>
<div><dt>室内/阅读灯</dt><dd id="interiorLightPower">未知</dd></div>
<div><dt>氛围亮度</dt><dd id="ambientBrightness">- / -</dd></div>
<div><dt>氛围色号</dt><dd id="ambientColorIndex">- / -</dd></div>
</dl>
</article>
<article class="domain-panel window-panel" data-domain-panel="window" data-device-channel="ch1">
<header>
<span class="domain-icon"><i data-lucide="gallery-vertical-end"></i></span>
<div><strong>车窗</strong><small>CMD 0x431 · FB 0x388 · CH1</small></div>
<span class="device-state" id="windowState">UNKNOWN</span>
</header>
<p class="domain-capability-line"><span>可控参数</span>四窗独立 · 开/关/停 · 位置</p>
<div class="domain-summary-visual window-summary-visual" aria-hidden="true">
<span data-window-meter="front_left"><i></i><b>左前</b></span>
<span data-window-meter="front_right"><i></i><b>右前</b></span>
<span data-window-meter="rear_left"><i></i><b>左后</b></span>
<span data-window-meter="rear_right"><i></i><b>右后</b></span>
</div>
<dl class="domain-readouts">
<div><dt>左前窗</dt><dd id="frontLeftWindow">未知</dd></div>
<div><dt>右前窗</dt><dd id="frontRightWindow">未知</dd></div>
<div><dt>左后窗</dt><dd id="rearLeftWindow">未知</dd></div>
<div><dt>右后窗</dt><dd id="rearRightWindow">未知</dd></div>
</dl>
</article>
<article class="domain-panel lighting-panel" data-domain-panel="front_light" data-device-channel="ch1">
<header>
<span class="domain-icon"><i data-lucide="car-front"></i></span>
<div><strong>前大灯</strong><small>FB 0x38A · CH1</small></div>
<span class="device-state" id="frontLightState">UNKNOWN</span>
</header>
<p class="domain-capability-line"><span>可控参数</span>近/远光 · 雾灯 · 转向灯</p>
<div class="domain-summary-visual lighting-summary-visual" aria-hidden="true">
<span data-light-meter="lowBeamState"><i></i><b>近光</b></span>
<span data-light-meter="highBeamState"><i></i><b>远光</b></span>
<span data-light-meter="fogLightState"><i></i><b>雾灯</b></span>
<span data-light-meter="turnSignalState"><i></i><b>转向</b></span>
</div>
<dl class="domain-readouts">
<div><dt>近光灯</dt><dd id="lowBeamState">未知</dd></div>
<div><dt>远光灯</dt><dd id="highBeamState">未知</dd></div>
<div><dt>雾灯</dt><dd id="fogLightState">未知</dd></div>
<div><dt>转向灯</dt><dd id="turnSignalState">未知</dd></div>
</dl>
</article>
</div>
</section>
<section class="twin-band" aria-labelledby="twinTitle">
<div class="twin-heading">
<div class="twin-title-copy">
<h3 id="twinTitle">整车孪生界面</h3>
<div class="preset-indicator" id="presetIndicator" hidden>
<i data-lucide="bookmark-check"></i><span></span>
</div>
</div>
<div class="twin-toolbar">
<div class="twin-selector" role="tablist" aria-label="前排座椅孪生选择">
<button class="twin-seat-button active" type="button" role="tab" aria-selected="true" data-twin-seat="passenger">副驾座椅 · CH0</button>
<button class="twin-seat-button" type="button" role="tab" aria-selected="false" data-twin-seat="driver">主驾座椅 · CH1</button>
</div>
<button class="icon-button twin-reset" id="twinReset" type="button" title="复位 3D 视角" aria-label="复位 3D 视角">
<i data-lucide="rotate-3d"></i>
</button>
</div>
</div>
<div class="twin-stage" id="seatTwin" data-selected-seat="passenger">
<canvas id="seatTwinCanvas" data-vehicle-model="byd-tang-2024-reference" data-seat-layout="five-seat" data-active-seat="front-right" aria-label="通过模型动画展示 CH0 副驾座椅与 CH1 主驾座椅、空调气流、智能天幕、阅读灯、四门车窗与前大灯状态的整车数字孪生"></canvas>
<div class="twin-seat-label passenger"><i></i><span>副驾座椅</span><small>CH0 · 右前排</small></div>
<div class="twin-seat-label driver"><i></i><span>主驾座椅</span><small>CH1 · 左前排</small></div>
<div class="twin-orientation"><i data-lucide="move-down"></i><span>车头方向</span></div>
<aside class="twin-readout" aria-live="polite">
<div class="twin-readout-heading">
<div><strong id="twinSeatName">副驾座椅</strong><span id="twinSeatZone">CH0 · 右前排</span></div>
<span class="twin-feedback"><i></i>FEEDBACK</span>
</div>
<div class="twin-axis-list">
<div data-twin-axis="longitudinal"><span>水平位置</span><b>50%</b><i></i></div>
<div data-twin-axis="cushion_tilt"><span>坐垫俯仰</span><b>50%</b><i></i></div>
<div data-twin-axis="height"><span>座椅高度</span><b>50%</b><i></i></div>
<div data-twin-axis="backrest_recline"><span>靠背夹角</span><b>20% · 104°</b><i></i></div>
<div data-twin-axis="legrest_height"><span>腿托夹角</span><b>0% · 95°</b><i></i></div>
<div data-twin-axis="legrest_extension"><span>腿托伸缩</span><b>0%</b><i></i></div>
</div>
<div class="twin-thermal-row" aria-label="座椅热舒适状态">
<span data-twin-function="ventilation" data-level="0">
<i data-lucide="wind"></i>
<span class="twin-thermal-copy"><small>通风</small><b>关闭</b></span>
<span class="twin-level-meter" aria-hidden="true"><i></i><i></i><i></i></span>
</span>
<span data-twin-function="heating" data-level="0">
<i data-lucide="flame"></i>
<span class="twin-thermal-copy"><small>加热</small><b>关闭</b></span>
<span class="twin-level-meter" aria-hidden="true"><i></i><i></i><i></i></span>
</span>
</div>
</aside>
<div class="twin-fallback" id="twinFallback"><i data-lucide="loader-circle"></i><span>正在加载整车参考模型</span></div>
</div>
</section>
<section class="state-band">
<div class="seat-state-section">
<div class="section-title-row">
<div><h3>前排座椅状态</h3></div>
<span class="simulation-label" id="seatFeedbackModeLabel">CH0 / CH1 FEEDBACK</span>
</div>
<div class="seat-grid">
<article class="seat-card" data-seat="passenger">
<div class="seat-identity"><i data-lucide="armchair"></i><div><strong>副驾座椅</strong><span>右前排</span></div></div>
<span class="seat-channel-badge" data-seat-feedback="passenger">CH0 · 等待反馈</span>
<div class="dynamic-mode-state" data-dynamic-mode>
<i data-lucide="waves"></i><span>动态模式</span><b>未运行</b>
</div>
<div class="motion-state-grid" aria-live="polite"></div>
</article>
<article class="seat-card" data-seat="driver">
<div class="seat-identity"><i data-lucide="armchair"></i><div><strong>主驾座椅</strong><span>左前排</span></div></div>
<span class="seat-channel-badge" data-seat-feedback="driver">CH1 · 等待反馈</span>
<div class="dynamic-mode-state" data-dynamic-mode>
<i data-lucide="radio-tower"></i><span>车端 CAN FD</span><b>只读同步</b>
</div>
<div class="thermal-states">
<span data-function="ventilation"><i data-lucide="wind"></i>通风 <b>--</b></span>
<span data-function="heating"><i data-lucide="flame"></i>加热 <b>--</b></span>
<span data-function="massage"><i data-lucide="waves"></i>按摩 <b>--</b></span>
</div>
<div class="motion-state-grid" aria-live="polite"></div>
</article>
</div>
</div>
<div class="coverage-section">
<h3>当前控制边界</h3>
<dl>
<div><dt>空调 18–32℃ / 0–7 档</dt><dd class="available">可编译</dd></div>
<div><dt>天幕 / 遮阳帘 0–100%</dt><dd class="available">可编译</dd></div>
<div><dt>氛围灯 RGB / 亮度</dt><dd class="available">可编译</dd></div>
<div><dt>副驾座椅统一执行器</dt><dd class="simulated">t2g_primary_seat</dd></div>
<div><dt>主驾座椅车端执行器</dt><dd class="simulated">CH1 · 左前排</dd></div>
<div><dt>真实台架 CAN 下发</dt><dd class="blocked" id="hardwareCoverage">未配置</dd></div>
</dl>
</div>
</section>
<section class="detail-section result-workspace" aria-labelledby="resultWorkspaceTitle">
<header class="result-workspace-heading">
<div><h3 id="resultWorkspaceTitle">编排结果工作台</h3></div>
<span class="result-workspace-badge"><i data-lucide="route"></i>REQUEST → CONFIG → CAN → RESULT</span>
</header>
<nav class="tabs result-nav" aria-label="结果快速导航">
<button class="tab active" type="button" data-tab="trace"><span class="tab-index">01</span><i data-lucide="list-tree"></i><span class="tab-copy"><strong>决策轨迹</strong><small id="traceNavMeta">等待请求</small></span><i data-lucide="arrow-down"></i></button>
<button class="tab" type="button" data-tab="config"><span class="tab-index">02</span><i data-lucide="layout-grid"></i><span class="tab-copy"><strong>配置卡片</strong><small id="configNavMeta">等待生成</small></span><i data-lucide="arrow-down"></i></button>
<button class="tab" type="button" data-tab="plan"><span class="tab-index">03</span><i data-lucide="binary"></i><span class="tab-copy"><strong>CAN 计划</strong><small id="planNavMeta">等待编译</small></span><i data-lucide="arrow-down"></i></button>
<button class="tab" type="button" data-tab="result"><span class="tab-index">04</span><i data-lucide="circle-check-big"></i><span class="tab-copy"><strong>执行结果</strong><small id="resultNavMeta">尚未执行</small></span><i data-lucide="arrow-down"></i></button>
</nav>
<div class="tab-panel active" id="panel-trace" role="tabpanel">
<div class="workspace-panel-title"><div><h3>决策轨迹与校验</h3></div><span class="workspace-panel-state" id="tracePanelState">IDLE</span></div>
<div class="trace-layout">
<div>
<div class="detail-heading"><h3>可审计决策摘要</h3><span id="providerUsed">--</span></div>
<ol class="decision-list" id="decisionList"><li>提交请求后显示识别目标、能力选择和约束来源。</li></ol>
</div>
<div>
<div class="detail-heading"><h3>阶段事件</h3><span id="traceDuration">0 ms</span></div>
<div class="trace-list" id="traceList"><div class="empty-line">暂无执行事件</div></div>
</div>
</div>
<div class="validation-block">
<div class="detail-heading"><h3>校验结果</h3><span id="validationCount">0 条</span></div>
<div id="validationList" class="validation-list"><div class="empty-line">等待配置生成</div></div>
</div>
</div>
<div class="tab-panel config-panel" id="panel-config" role="tabpanel">
<div class="config-panel-heading">
<div><h3>指令配置与硬件映射</h3></div>
<div class="config-actions"><button class="config-action" type="button" data-copy="config"><i data-lucide="copy"></i>复制 YAML</button><button class="config-action" type="button" data-download="config"><i data-lucide="download"></i>下载配置</button></div>
</div>
<div class="config-card-grid" id="configCards">
<div class="config-empty"><i data-lucide="panel-top-dashed"></i><strong>等待生成配置</strong><span>提交车舱控制指令后,这里会显示控制对象、目标参数和硬件信号。</span></div>
</div>
<details class="config-source-details">
<summary><span><i data-lucide="braces"></i>查看原始配置</span><small id="configFileName">functions/seat.yaml</small></summary>
<pre id="configCode"><code># 等待生成 FunctionConfig</code></pre>
</details>
</div>
<div class="tab-panel plan-panel" id="panel-plan" role="tabpanel">
<div class="workspace-panel-title plan-panel-heading"><div><h3>CAN 编译计划</h3></div><button class="config-action" type="button" data-copy="plan"><i data-lucide="copy"></i>复制计划</button></div>
<div class="frame-summary" id="frameSummary"><span>Frame ID</span><b>--</b><span>Payload</span><code>--</code><span>Mode</span><b>DRY-RUN</b></div>
<details class="config-source-details plan-source-details">
<summary><span><i data-lucide="binary"></i>查看原始 CAN 计划</span><small id="planFileName">command-plans/seat.yaml</small></summary>
<pre id="planCode"><code># 等待 CAN 编译</code></pre>
</details>
</div>
<div class="tab-panel result-panel" id="panel-result" role="tabpanel">
<div class="workspace-panel-title"><div><h3>执行结果与硬件回读</h3></div><span class="workspace-panel-state" id="resultPanelState">IDLE</span></div>
<div class="execution-summary" id="executionSummary">
<i data-lucide="circle-dashed"></i><div><strong>尚未执行</strong><span>运行结果将在此处显示</span></div>
</div>
<div class="result-list" id="resultList"></div>
</div>
</section>
</main>
</div>
<aside class="gesture-drawer" id="gestureDrawer" aria-hidden="true" aria-labelledby="gestureDrawerTitle">
<header class="gesture-drawer-header">
<div>
<h2 id="gestureDrawerTitle">手势控制</h2>
</div>
<button class="gesture-close" id="gestureCloseButton" type="button" aria-label="关闭手势控制"><i data-lucide="x"></i></button>
</header>
<div class="gesture-status-strip">
<span class="gesture-state-pill idle" id="gestureStatePill"><i></i><b>未启动</b></span>
<span id="gestureSafetyText">摄像头尚未开启</span>
</div>
<div class="gesture-camera-stage">
<video id="gestureVideo" playsinline muted></video>
<canvas id="gestureCanvas"></canvas>
<div class="gesture-camera-empty" id="gestureCameraEmpty">
<span><i data-lucide="scan-face"></i></span>
<strong>用手势控制副驾驶座椅</strong>
<p>画面只在本机处理,不上传视频。</p>
</div>
<div class="gesture-live-badge" id="gestureLiveBadge" hidden><i></i>LIVE</div>
</div>
<div class="gesture-now">
<div><span>当前手势</span><strong id="gestureDetected">—</strong></div>
<i data-lucide="arrow-right"></i>
<div><span>期望动作</span><strong id="gestureExpected">等待启动</strong></div>
</div>
<div class="gesture-progress"><i id="gestureProgressBar"></i></div>
<div class="gesture-actions">
<button class="gesture-primary" id="gestureStartButton" type="button"><i data-lucide="camera"></i><span>开启摄像头</span></button>
<button id="gestureStopButton" type="button" disabled><i data-lucide="octagon"></i>停止</button>
</div>
<section class="gesture-guide-card">
<div class="gesture-card-heading"><div><span>手势速查</span><small>张掌激活 · 握拳只停止</small></div><button id="gestureCustomizeButton" type="button"><i data-lucide="sliders-horizontal"></i>自定义</button></div>
<div class="gesture-map-grid" id="gestureMapGrid"></div>
<div class="gesture-customize" id="gestureCustomizePanel" hidden>
<div><span>识别到</span><strong id="gestureCustomizeToken">请先摆出手势</strong></div>
<select id="gestureActionSelect" aria-label="选择座椅动作"></select>
<button id="gestureBindButton" type="button">绑定当前手势</button>
<button id="gestureResetMapping" type="button">恢复默认</button>
</div>
</section>
<footer class="gesture-footnote"><i data-lucide="shield-check"></i><span>换动作无需重新张掌;连续 3 秒无有效动作后自动锁止。</span></footer>
</aside>
<div class="mode-gallery-backdrop" id="modeGalleryBackdrop" hidden>
<section class="mode-gallery" id="modeGallery" role="dialog" aria-modal="true" aria-labelledby="modeGalleryTitle">
<header class="mode-gallery-header">
<div>
<h2 id="modeGalleryTitle">管理功能模式</h2>
<p>按来源与运行方式整理,快速定位并管理模式</p>
</div>
<button class="mode-gallery-close" id="modeGalleryClose" type="button" aria-label="关闭功能模式管理"><i data-lucide="x"></i></button>
</header>
<div class="mode-gallery-summary"><span><i data-lucide="layers-3"></i>自定义模式 · 静态模式 · 动态模式</span><strong id="modeGallerySummaryCount">0</strong></div>
<div class="mode-gallery-groups" id="modeGalleryGroups" aria-live="polite"></div>
<footer class="mode-gallery-footer"><i data-lucide="info"></i><span>自定义模式支持编辑与删除;静态模式执行后保持目标状态;动态模式持续变化,可随时停止。</span></footer>
</section>
</div>
<div class="mode-editor-backdrop" id="modeEditorBackdrop" hidden>
<section class="mode-editor" role="dialog" aria-modal="true" aria-labelledby="modeEditorTitle">
<div class="mode-editor-setup">
<header class="mode-editor-header">
<div>
<h2 id="modeEditorTitle">编辑功能模式</h2>
</div>
<button id="modeEditorClose" type="button" aria-label="关闭模式编辑器"><i data-lucide="x"></i></button>
</header>
<div class="mode-editor-inputs">
<label class="mode-editor-field mode-editor-name-field" for="modeEditorName">
<span>模式名称</span>
<input id="modeEditorName" maxlength="64" autocomplete="off" placeholder="输入模式名称" />
</label>
<label class="mode-editor-field mode-editor-instruction-field" for="modeEditorInstruction">
<span>自然语言指令</span>
<textarea id="modeEditorInstruction" maxlength="1000" rows="3" placeholder="例如:空调24度、风量2档,打开遮阳帘"></textarea>
</label>
</div>
</div>
<section class="mode-editor-flow" aria-live="polite">
<header>
<span><i data-lucide="workflow"></i>控制指令流</span>
<div class="mode-editor-flow-tools">
<b id="modeEditorFlowCount">尚未生成</b>
<button id="modeEditorFlowExpand" type="button" aria-expanded="false" aria-label="展开控制指令区域" title="展开控制指令区域">
<i data-lucide="maximize-2"></i>
</button>
</div>
</header>
<div class="mode-editor-flow-preview empty" id="modeEditorFlowPreview">
<p>点击“生成指令”后,这里会显示由后端解析、校验并编译的控制指令流。</p>
</div>
</section>
<p class="mode-editor-status" id="modeEditorStatus">修改自然语言后需要重新生成指令,才能点击完成永久保存。</p>
<footer class="mode-editor-actions">
<button id="modeEditorCancel" type="button">取消</button>
<button class="mode-editor-execute" id="modeEditorExecute" type="button"><i data-lucide="wand-sparkles" data-icon="inline-start"></i><span>生成指令</span></button>
<button class="mode-editor-complete" id="modeEditorComplete" type="button" disabled><i data-lucide="save" data-icon="inline-start"></i><span>完成</span></button>
</footer>
</section>
</div>
<button class="glass-theme-toggle" id="glassThemeToggle" type="button" aria-label="切换到白日模式" aria-pressed="true" title="切换到白日模式">
<i data-lucide="sun" data-theme-icon="sun" aria-hidden="true"></i>
<i data-lucide="moon" data-theme-icon="moon" aria-hidden="true"></i>
<span class="visually-hidden">切换白日/夜间模式</span>
</button>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
</template>
+33
View File
@@ -0,0 +1,33 @@
import { createApp, nextTick } from "vue";
import App from "./App.vue";
import "../styles.css";
import "../console-v2.css";
import "../self-extension.css";
import "../speech-input.css";
import "../assistant-chat.css";
import "../energy-monitor.css";
import "../ui-consistency.css";
import "../codepen-glass-theme.css";
import "./styles/design-system.css";
import "../vue-ui.js";
const application = createApp(App);
application.config.errorHandler = (error, instance, info) => {
console.error("N7 Vue application error", { error, instance, info });
};
application.mount("#app");
document.querySelector("#app")?.removeAttribute("aria-busy");
document.documentElement.classList.add("vue-vite-application");
document.documentElement.dataset.vueVersion = application.version || "3";
nextTick(() => {
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
window.dispatchEvent(new CustomEvent("cognitive-agent:vue-mounted"));
});
window.CognitiveAgentVueApp = Object.freeze({
application,
version: application.version || "3"
});
@@ -0,0 +1,389 @@
/* N7 Web Console design-system contract.
* Loaded last by Vite so every legacy surface consumes one geometry,
* typography, state and elevation scale while the runtime is migrated to
* Vue components incrementally. */
:root {
--n7-control-height-sm: 32px;
--n7-control-height: 38px;
--n7-icon-button-size: 38px;
--n7-radius-control: 10px;
--n7-radius-card: 14px;
--n7-radius-section: 16px;
--n7-radius-panel: 18px;
--n7-radius-dialog: 22px;
--n7-font-caption: 12px;
--n7-font-body: 13px;
--n7-font-card-title: 14px;
--n7-font-panel-title: 16px;
--n7-font-dialog-title: 22px;
--n7-shadow-card: inset 0 1px 0 var(--glass-highlight), 0 7px 20px color-mix(in oklch, var(--foreground) 8%, transparent);
--n7-shadow-panel: inset 0 1px 0 var(--glass-highlight), 0 14px 34px color-mix(in oklch, var(--foreground) 13%, transparent);
--n7-shadow-dialog: inset 0 1px 0 var(--glass-highlight), 0 28px 84px color-mix(in oklch, var(--foreground) 28%, transparent);
}
#app {
min-width: inherit;
min-height: 100dvh;
display: contents;
}
/* Workspace panels: one top-level geometry, distinct from their children. */
body.console-v2.v2-passive-display.v2-interactive-sync :is(
.main-panel > .v2-mode-rail,
#assistantDrawer,
.joint-twin-shell > .twin-band,
#v2DevicePanel,
#v2ExtensionPanel .v2-extension-sync-mount
) {
border-radius: var(--n7-radius-panel) !important;
box-shadow: var(--n7-shadow-panel) !important;
}
body.console-v2 :is(
.v2-sync-transport-field,
.v2-sync-execution-field,
.v2-can-command-panel,
.v2-control-link-card,
.v2-nfc-inline,
.v2-extension-gateway,
.v2-extension-synced-device,
.v2-extension-empty,
.v2-pet-card,
.v2-pet-presence-card,
.v2-device-card,
.v2-synced-device,
.mode-gallery-card,
.mode-editor-command-card,
.energy-kpi,
.energy-device-row,
.assistant-dispatched-controls,
.assistant-mode-draft,
.assistant-mode-created,
.assistant-control-suggestion
) {
border-radius: var(--n7-radius-card) !important;
box-shadow: var(--n7-shadow-card) !important;
}
body.console-v2 :is(
.v2-sync-log,
.v2-pet-card-region,
.mode-gallery-group,
.mode-editor-flow,
.v2-nfc-stage,
.energy-trend-card,
.energy-devices-card
) {
border-radius: var(--n7-radius-section) !important;
}
/* Only actionable cards float. Read-only panels remain spatially stable. */
body.console-v2 :is(
.v2-sync-log,
.v2-pet-card-region,
.mode-editor-flow,
.energy-trend-card,
.energy-devices-card
):is(:hover, :focus-within) {
transform: none !important;
box-shadow: var(--n7-shadow-card) !important;
}
/* The current DOM uses .twin-selector; the previous glass selector targeted
* the removed .twin-seat-switch class and produced white-on-white tabs. */
body.console-v2 .joint-twin-shell :is(.twin-selector, .twin-selector button, .twin-reset, .twin-focus-badge) {
color: var(--muted-foreground) !important;
background: var(--glass-card) !important;
border-color: var(--glass-line) !important;
box-shadow: inset 0 1px 0 var(--glass-highlight) !important;
}
body.console-v2 .joint-twin-shell .twin-selector button.active {
color: var(--primary-foreground) !important;
background: var(--primary) !important;
border-color: color-mix(in oklch, var(--primary) 72%, white 18%) !important;
}
/* Shared heading hierarchy. */
body.console-v2 :is(
#assistantDrawer h2,
.joint-twin-shell .twin-heading h3,
.v2-device-sync-board > header h3,
.v2-extension-sync-heading h3,
.v2-mode-rail .mode-rail-title
) {
font-size: var(--n7-font-panel-title) !important;
font-weight: 750 !important;
line-height: 1.25 !important;
letter-spacing: -0.02em;
}
/* The log title shares a 217 px header with its filter, so it needs the
* compact card-title scale instead of the full panel-title scale. */
body.console-v2 .v2-sync-log h3 {
min-width: 0;
margin: 0;
overflow: hidden;
font-size: var(--n7-font-card-title) !important;
font-weight: 750 !important;
line-height: 1.25 !important;
letter-spacing: -0.02em;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Sidebar disclosure, connection status and control-command readability use
* the same semantic tokens in both day and night themes. */
body.console-v2 .v2-sync-log[data-collapsed="true"] {
min-height: 0;
align-self: start;
grid-template-rows: auto;
flex: 0 0 auto;
}
body.console-v2 .v2-sync-log[data-collapsed="true"] > header {
border-bottom-color: transparent;
}
body.console-v2 .v2-sync-log[data-collapsed="true"] #v2LogFilter {
display: none;
}
body.console-v2 .v2-sync-log #v2SyncLogList[hidden] {
display: none !important;
}
body.console-v2 .v2-sync-log > header > div:first-child {
min-width: 0;
}
body.console-v2 .v2-sync-log .v2-sync-log-actions {
display: inline-flex;
flex: 0 0 auto;
align-items: center;
gap: 6px;
}
body.console-v2 .v2-sync-log .v2-sync-log-toggle {
min-width: 46px;
min-height: var(--n7-control-height-sm);
display: inline-flex;
align-items: center;
justify-content: center;
gap: 4px;
padding: 0 7px;
color: var(--muted-foreground);
background: var(--secondary);
border: 1px solid var(--input);
border-radius: var(--n7-radius-control);
cursor: pointer;
}
body.console-v2 .v2-sync-log .v2-sync-log-toggle:is(:hover, :focus-visible) {
color: var(--accent-foreground);
background: var(--accent);
border-color: var(--ring);
outline: none;
}
body.console-v2 .v2-sync-log .v2-sync-log-toggle svg {
width: 15px;
height: 15px;
transition: transform 160ms ease;
}
body.console-v2 .v2-sync-log .v2-sync-log-toggle[aria-expanded="true"] svg {
transform: rotate(180deg);
}
body.console-v2.v2-passive-display.v2-interactive-sync .v2-control-link-card {
min-height: 56px;
grid-template-columns: 10px minmax(0, 1fr);
gap: 10px;
margin: 0 !important;
padding: 10px 11px !important;
color: var(--card-foreground);
background: var(--glass-card) !important;
border: 1px solid var(--glass-line) !important;
border-radius: var(--n7-radius-card) !important;
box-shadow: var(--n7-shadow-card) !important;
-webkit-backdrop-filter: blur(16px) saturate(1.2);
backdrop-filter: blur(16px) saturate(1.2);
}
body.console-v2 .v2-control-link-card strong {
color: var(--card-foreground) !important;
}
body.console-v2 .v2-control-link-card small {
color: var(--muted-foreground) !important;
}
body.console-v2 .v2-control-link-card[data-state="online"] .v2-live-dot {
background: var(--status-online);
box-shadow: 0 0 0 4px color-mix(in oklch, var(--status-online) 18%, transparent);
}
body.console-v2 .v2-control-link-card[data-state="connecting"] .v2-live-dot {
background: var(--status-info);
box-shadow: 0 0 0 4px color-mix(in oklch, var(--status-info) 18%, transparent);
}
body.console-v2 .v2-control-link-card[data-state="degraded"] .v2-live-dot {
background: var(--status-warning);
box-shadow: 0 0 0 4px color-mix(in oklch, var(--status-warning) 18%, transparent);
}
body.console-v2 .v2-control-link-card[data-state="offline"] .v2-live-dot {
background: var(--status-offline);
box-shadow: 0 0 0 4px color-mix(in oklch, var(--status-offline) 18%, transparent);
}
body.console-v2.v2-passive-display.v2-interactive-sync
.v2-can-command-list li[data-can-channel]
.v2-can-command-actual code {
color: var(--card-foreground) !important;
}
body.console-v2 :is(
.mode-gallery-header h2,
.mode-editor-header h2,
.v2-extension-dialog > header h2,
.v2-extension-dialog form > header h2,
.energy-monitor-heading h2
) {
font-size: var(--n7-font-dialog-title) !important;
font-weight: 750 !important;
line-height: 1.2 !important;
}
body.console-v2 :is(
.mode-gallery-card-heading strong,
.v2-device-card-focus strong,
.v2-nfc-inline strong,
.v2-extension-gateway strong,
.v2-pet-card > header > strong,
.energy-trend-card h3,
.energy-devices-card h3
) {
font-size: var(--n7-font-card-title) !important;
line-height: 1.35 !important;
}
body.console-v2 :is(
.mode-editor-status,
.mode-gallery-footer,
.v2-pet-settings-footnote,
.v2-pet-card-fields > label > span,
.v2-pet-nfc-field output,
.energy-kpi > small,
.energy-kpi > em,
.energy-devices-card > footer
) {
font-size: var(--n7-font-caption) !important;
line-height: 1.45 !important;
}
/* One dialog radius, elevation and close-button size. */
body.console-v2 :is(
.mode-gallery,
.mode-editor,
.v2-nfc-dialog,
.v2-gscu-tcp-dialog,
.v2-pet-settings-dialog,
.energy-monitor-page
) {
border-radius: var(--n7-radius-dialog) !important;
box-shadow: var(--n7-shadow-dialog) !important;
}
body.console-v2 :is(
.mode-gallery-close,
.mode-editor-header > button,
.v2-extension-dialog [data-dialog-close],
.v2-extension-dialog [data-close-extension-dialog],
.v2-extension-dialog [data-close-nfc-dialog],
#energyMonitorClose
) {
width: var(--n7-icon-button-size) !important;
height: var(--n7-icon-button-size) !important;
min-width: var(--n7-icon-button-size);
min-height: var(--n7-icon-button-size);
border-radius: var(--n7-radius-control) !important;
}
/* Compact icon actions still meet a usable hit target without becoming a
* second visual system. */
body.console-v2 :is(
.twin-reset,
.mode-gallery-edit,
.mode-gallery-delete,
.v2-pet-card > header > button
) {
width: var(--n7-control-height-sm) !important;
height: var(--n7-control-height-sm) !important;
min-width: var(--n7-control-height-sm);
min-height: var(--n7-control-height-sm);
border-radius: var(--n7-radius-control) !important;
}
/* Offline hardware is a destructive state, not a primary/active state. */
body.console-v2 .v2-nfc-stage:has(.v2-nfc-waiting.offline) > header {
color: var(--status-error-foreground) !important;
background: color-mix(in oklch, var(--destructive) 16%, var(--card)) !important;
border-bottom: 1px solid color-mix(in oklch, var(--destructive) 32%, var(--border));
}
body.console-v2 .v2-nfc-stage:has(.v2-nfc-waiting.offline) > header :is(span, small, h3, p, b) {
color: var(--status-error-foreground) !important;
}
/* Keep the global appearance control out of content and below every modal. */
.glass-theme-toggle {
left: max(18px, env(safe-area-inset-left)) !important;
bottom: max(18px, env(safe-area-inset-bottom)) !important;
z-index: 1100 !important;
}
body.mode-gallery-open .glass-theme-toggle,
body.mode-editor-open .glass-theme-toggle,
body:has(.v2-extension-dialog[open]) .glass-theme-toggle,
body:has(.energy-monitor-layer:not([hidden])) .glass-theme-toggle {
opacity: 0;
pointer-events: none;
}
/* The sidebar reserves the floating control's footprint instead of allowing
* it to cover the log timeline. */
body.console-v2 .v2-sync-log {
margin-bottom: 58px;
}
/* Top mode rail communicates that more content is available. */
body.console-v2 .v2-mode-rail .mode-rail-content {
position: relative;
min-width: 0;
}
body.console-v2 .v2-mode-rail .mode-rail-content::after {
width: 28px;
content: "";
position: absolute;
inset: 0 0 0 auto;
pointer-events: none;
background: linear-gradient(90deg, transparent, color-mix(in oklch, var(--glass-panel) 94%, transparent));
}
@media (max-width: 1360px) {
.glass-theme-toggle {
width: 42px !important;
height: 42px !important;
}
}
@media (prefers-reduced-motion: reduce) {
body.console-v2 * {
scroll-behavior: auto !important;
}
}
+2009 -462
View File
File diff suppressed because it is too large Load Diff
+419
View File
@@ -0,0 +1,419 @@
/* Shared spatial UI contract --------------------------------------------
Keep geometry consistent across legacy modules without changing their
domain-specific colors, content density, or interaction behavior.
Hierarchy: control 10px -> card 13px -> section 15px -> panel 16px ->
modal 20px. All modules consume the same semantic color tokens. */
:root {
--ui-control-height-compact: 30px;
--ui-control-height: 38px;
--ui-control-size-icon: 36px;
--ui-font-card-title: 13px;
--ui-font-section-title: 15px;
--ui-font-panel-title: 16px;
--ui-inline-action-width: 88px;
--ui-radius-control-inner: calc(var(--radius) - 2px);
--ui-radius-control: var(--radius);
--ui-radius-item: calc(var(--radius) + 2px);
--ui-radius-section: calc(var(--radius) + 6px);
--ui-radius-panel: calc(var(--radius) + 8px);
--ui-radius-modal: calc(var(--radius) + 12px);
--ui-space-card: 10px;
--ui-border-subtle: 1px solid var(--border);
--ui-shadow-item: 0 5px 14px color-mix(in oklch, var(--foreground) 6%, transparent);
--ui-shadow-panel:
0 14px 32px color-mix(in oklch, var(--foreground) 8%, transparent),
inset 0 1px 0 color-mix(in oklch, var(--card) 78%, transparent);
--ui-shadow-modal:
0 28px 72px color-mix(in oklch, var(--foreground) 22%, transparent),
0 4px 18px color-mix(in oklch, var(--primary) 8%, transparent),
inset 0 1px 0 color-mix(in oklch, var(--card) 84%, transparent);
--ui-focus-ring: 0 0 0 3px color-mix(in oklch, var(--ring) 22%, transparent);
}
/* One readable type scale prevents large legacy headings and tiny newer card
titles from competing inside the same workspace. */
.console-v2 :is(
.v2-operation-heading h2,
.joint-twin-shell .twin-heading h3,
.v2-device-sync-board > header h3,
.v2-extension-sync-heading h3,
.v2-sync-sidebar-heading h2,
.workspace-panel-title h3,
.config-panel-heading h3
) {
font-size: var(--ui-font-panel-title) !important;
line-height: 1.25;
letter-spacing: -0.02em;
}
.console-v2 :is(
.v2-pet-card-region > header h4,
.v2-gscu-actions > header strong,
.v2-gscu-command-catalog > header strong
) {
font-size: var(--ui-font-section-title) !important;
line-height: 1.3;
}
.console-v2 :is(
.v2-device-card-focus strong,
.v2-nfc-inline strong,
.v2-extension-gateway strong,
.v2-extension-synced-device header strong,
.v2-pet-presence-card > header strong,
.v2-pet-card > header > strong
) {
font-size: var(--ui-font-card-title) !important;
line-height: 1.35;
}
/* Top-level workspace surfaces share one frame. */
.console-v2.v2-passive-display.v2-interactive-sync :is(
.v2-operation-shell,
.joint-twin-shell > .twin-band,
#v2DevicePanel,
#v2ExtensionPanel .v2-extension-sync-mount
) {
border: var(--ui-border-subtle);
border-radius: var(--ui-radius-panel);
box-shadow: var(--ui-shadow-panel);
}
/* The operation shell owns the frame; its active view must not draw a
second border directly inside the same spatial boundary. */
.console-v2.v2-passive-display.v2-interactive-sync
.v2-operation-shell > .v2-natural-panel {
border: 0;
border-radius: inherit;
box-shadow: none;
}
/* Modal surfaces and their backdrops use one elevation level. */
:is(
.mode-gallery-backdrop .mode-gallery,
.mode-editor-backdrop .mode-editor,
.energy-monitor-layer .energy-monitor-page
) {
border: 1px solid color-mix(in oklch, var(--primary) 18%, var(--border));
border-radius: var(--ui-radius-modal);
box-shadow: var(--ui-shadow-modal);
}
.mode-gallery-backdrop,
.mode-editor-backdrop,
.energy-monitor-layer {
background: color-mix(in oklch, var(--foreground) 42%, transparent);
-webkit-backdrop-filter: blur(14px);
backdrop-filter: blur(14px);
}
/* Section containers sit between panels and individual cards. */
.v2-sync-log,
.mode-gallery-group,
.energy-trend-card,
.energy-devices-card {
border-radius: var(--ui-radius-section);
}
/* Repeated content cards share radius and elevation while retaining their
own semantic state colors and specialized internal layouts. */
.v2-synced-device,
.v2-extension-gateway,
.v2-nfc-inline,
.mode-gallery-card,
.mode-editor-flow,
.mode-editor-command-card,
.energy-kpi {
border-radius: var(--ui-radius-item);
box-shadow: var(--ui-shadow-item);
}
.energy-kpi-primary {
box-shadow: 0 8px 20px color-mix(in oklch, var(--primary) 10%, transparent);
}
.v2-extension-gateway,
.v2-nfc-inline {
padding: var(--ui-space-card);
}
/* Sidebar cards and their nested segmented controls preserve a clear
parent/child radius relationship. */
.console-v2 :is(
.v2-sync-transport-field,
.v2-sync-execution-field,
.v2-can-command-panel
) {
border-radius: var(--ui-radius-item);
}
/* The compact-sidebar rules in console-v2.css intentionally use highly
specific selectors. Keep all peer cards on the shared item radius even
when that layout is active. */
.console-v2.v2-passive-display.v2-interactive-sync :is(
.v2-sync-transport-field,
.v2-sync-execution-field,
.v2-can-command-panel
) {
border-radius: var(--ui-radius-item) !important;
}
.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-log {
border-radius: var(--ui-radius-section) !important;
}
.console-v2 :is(.segmented, .twin-selector) {
gap: 4px;
padding: 3px;
background: var(--muted);
border: var(--ui-border-subtle);
border-radius: var(--ui-radius-control);
}
.console-v2 .segmented > :is(.segment, .mode-option),
.console-v2 .twin-selector > .twin-seat-button {
border-radius: var(--ui-radius-control-inner);
}
.console-v2 .segmented > .mode-option {
min-height: var(--ui-control-height);
height: var(--ui-control-height);
gap: 5px;
}
.console-v2 .twin-selector > .twin-seat-button {
height: 32px;
}
/* Primary header controls use one medium size. */
.console-v2 #v2PanelControlDock > .v2-panel-control-button,
.console-v2.v2-passive-display.v2-interactive-sync
.v2-mode-rail .mode-rail-actions > :is(
#modeGalleryToggle,
#saveModeToggle,
#modeRailToggle
) {
min-height: var(--ui-control-height);
height: var(--ui-control-height);
border-radius: var(--ui-radius-control);
}
.console-v2.v2-passive-display.v2-interactive-sync
#v2PanelControlDock > .v2-panel-control-button,
.console-v2.v2-passive-display.v2-interactive-sync
#v2PanelControlDock > #v2SidebarToggle.v2-panel-control-button {
border-radius: var(--ui-radius-control) !important;
}
.console-v2.v2-passive-display.v2-interactive-sync
.v2-mode-rail #modeRailToggle {
width: var(--ui-control-height);
min-width: var(--ui-control-height);
}
/* Repeated inline actions use one compact size. */
.console-v2 :is(
.v2-device-adjust-toggle,
.v2-pet-settings-launch,
.v2-extension-nfc-launch,
.v2-extension-gateway > button,
.v2-nfc-inline button
),
.energy-trend-tabs button {
min-height: var(--ui-control-height-compact);
height: var(--ui-control-height-compact);
border-radius: var(--ui-radius-control-inner);
}
/* Related extension actions use equal columns and a shared trailing action
width, so every card keeps the same horizontal rhythm. */
.console-v2 .v2-extension-sync-actions {
width: calc(var(--ui-inline-action-width) * 2 + 6px);
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 6px;
}
.console-v2 .v2-extension-sync-actions > :is(
.v2-pet-settings-launch,
.v2-extension-nfc-launch
) {
width: 100%;
min-width: 0;
height: var(--ui-control-height-compact);
justify-content: center;
padding-inline: 7px;
font-size: 10px;
}
.console-v2 :is(
.v2-nfc-inline > button,
.v2-extension-gateway > button
) {
width: var(--ui-inline-action-width);
min-width: var(--ui-inline-action-width);
justify-content: center;
}
/* Header icon actions are distinct from circular record/send actions. */
.assistant-drawer-actions .context-reset,
.assistant-drawer-close,
.mode-gallery-close,
.mode-editor-header > button,
#energyMonitorClose {
width: var(--ui-control-size-icon);
min-width: var(--ui-control-size-icon);
height: var(--ui-control-size-icon);
min-height: var(--ui-control-size-icon);
border-radius: var(--ui-radius-control);
}
/* Text fields align with medium controls and use the same focus treatment. */
.mode-editor-field input,
.mode-editor-field textarea {
border-radius: var(--ui-radius-control);
}
/* Reserve the right side for the microphone/send actions. Anchoring the hint
from the content edge prevents the legacy right offsets from pushing it
outside narrow assistant composers. */
.console-v2.assistant-chat-enabled.v2-passive-display.v2-interactive-sync
.v2-request-form .composer-hint {
left: 16px !important;
right: auto !important;
bottom: 8px;
max-width: calc(100% - 132px);
}
/* Delivery states use the same semantic feedback language in the sidebar and
in the compact assistant command card. */
.console-v2.v2-passive-display.v2-interactive-sync
.v2-can-command-list
li[data-can-channel]
.v2-can-command-delivery[data-delivery-state="failed"] {
color: var(--destructive);
background: color-mix(in oklch, var(--destructive) 10%, var(--card));
border-color: color-mix(in oklch, var(--destructive) 28%, var(--border));
}
.console-v2.v2-passive-display.v2-interactive-sync
.v2-can-command-list
li[data-can-channel]
.v2-can-command-delivery[data-delivery-state="blocked"] {
color: var(--status-warning-foreground);
background: var(--status-warning-soft);
border-color: color-mix(in oklch, var(--status-warning) 38%, var(--border));
}
.console-v2.v2-passive-display.v2-interactive-sync
.v2-can-command-list
li[data-can-channel]
.v2-can-command-delivery[data-delivery-state="aborted"] {
color: var(--muted-foreground);
background: var(--muted);
border-color: var(--border);
}
.console-v2.v2-passive-display.v2-interactive-sync
.v2-can-command-list
li[data-can-channel]
.v2-can-command-delivery-detail {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
gap: 6px;
margin: 1px 0 2px;
padding: 5px 7px;
color: var(--muted-foreground);
background: color-mix(in oklch, var(--muted) 62%, transparent);
border-left: 2px solid var(--border);
border-radius: var(--ui-radius-control-inner);
font-size: 9px;
line-height: 1.45;
white-space: normal;
}
.console-v2.v2-passive-display.v2-interactive-sync
.v2-can-command-list
li[data-delivery-state="failed"]
.v2-can-command-delivery-detail {
color: var(--status-error-foreground);
border-left-color: var(--destructive);
}
.console-v2.v2-passive-display.v2-interactive-sync
.v2-can-command-list
li[data-delivery-state="blocked"]
.v2-can-command-delivery-detail {
color: var(--status-warning-foreground);
border-left-color: var(--status-warning);
}
.assistant-dispatched-controls-list > li[data-delivery-state="failed"] {
background: color-mix(in oklch, var(--destructive) 4%, var(--card));
}
.assistant-dispatched-controls-list
> li[data-delivery-state="failed"]
.assistant-dispatched-control-icon {
color: var(--destructive);
}
.assistant-dispatched-controls-list > li[data-delivery-state="blocked"] {
background: var(--status-warning-soft);
}
.assistant-dispatched-controls-list
> li[data-delivery-state="blocked"]
.assistant-dispatched-control-icon {
color: var(--status-warning-foreground);
}
.assistant-dispatched-controls-list > li[data-delivery-state="aborted"] {
background: color-mix(in oklch, var(--muted) 68%, transparent);
}
.assistant-dispatched-controls-list
> li[data-delivery-state="aborted"]
.assistant-dispatched-control-icon {
color: var(--muted-foreground);
}
.console-v2 :is(
#v2PanelControlDock > .v2-panel-control-button,
.v2-mode-rail .mode-rail-actions > button,
.segmented > button,
.twin-selector > button,
.v2-device-adjust-toggle,
.v2-pet-settings-launch,
.v2-extension-nfc-launch,
.v2-extension-gateway > button,
.v2-nfc-inline button
):focus-visible,
.mode-gallery :is(button, input, textarea):focus-visible,
.mode-editor :is(button, input, textarea):focus-visible,
.energy-monitor-page button:focus-visible {
outline: none;
box-shadow: var(--ui-focus-ring);
}
@media (max-width: 900px) {
:root {
--ui-control-height: 40px;
--ui-control-height-compact: 32px;
--ui-control-size-icon: 38px;
}
.console-v2 .v2-extension-sync-actions {
width: 100%;
}
}
@media (prefers-reduced-motion: reduce) {
.v2-synced-device,
.mode-gallery-card,
.energy-kpi,
.console-v2 button {
transition-duration: 0.01ms !important;
}
}
+15
View File
@@ -0,0 +1,15 @@
ISC License
Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2022 as part of Feather (MIT). All other copyright (c) for Lucide are held by Lucide Contributors 2022.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
File diff suppressed because one or more lines are too long
@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
+21
View File
@@ -0,0 +1,21 @@
The MIT License
Copyright © 2010-2022 three.js authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2018-present, Yuxi (Evan) You
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
File diff suppressed because one or more lines are too long
+94
View File
@@ -0,0 +1,94 @@
import { cpSync, copyFileSync, existsSync, mkdirSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import vue from "@vitejs/plugin-vue";
import { defineConfig } from "vite";
const webRoot = dirname(fileURLToPath(import.meta.url));
const outputRoot = resolve(webRoot, "dist");
const backendOrigin = "http://127.0.0.1:8000";
const runtimeDirectories = ["assets", "icons", "vendor"];
const runtimeFiles = [
"manifest.webmanifest",
"service-worker.js",
"speech-audio-worklet.js",
"login.html",
"login.css",
"login.js",
"hardware-control.html",
"hardware-control.css",
"hardware-control.js",
"network.js",
"app.js",
"gesture-control.js",
"pwa.js",
"console-v2.js",
"self-extension.js",
"speech-input.js",
"assistant-chat.js",
"energy-monitor.js",
"styles.css",
"console-v2.css",
"self-extension.css",
"speech-input.css",
"assistant-chat.css",
"energy-monitor.css",
"ui-consistency.css",
"codepen-glass-theme.css",
"vue-ui.js"
];
function copyRuntimeAssets() {
return {
name: "copy-n7-runtime-assets",
apply: "build",
closeBundle() {
for (const directory of runtimeDirectories) {
const source = resolve(webRoot, directory);
if (existsSync(source)) {
cpSync(source, resolve(outputRoot, directory), { recursive: true });
}
}
for (const file of runtimeFiles) {
const source = resolve(webRoot, file);
if (!existsSync(source)) continue;
const target = resolve(outputRoot, file);
mkdirSync(dirname(target), { recursive: true });
copyFileSync(source, target);
}
}
};
}
function backendProxy() {
return {
target: backendOrigin,
changeOrigin: true,
configure(proxy) {
proxy.on("proxyReq", (request) => request.setHeader("origin", backendOrigin));
}
};
}
export default defineConfig({
root: webRoot,
publicDir: false,
plugins: [vue(), copyRuntimeAssets()],
server: {
strictPort: true,
fs: { allow: [webRoot] },
proxy: {
"/api": backendProxy(),
"/artifacts": backendProxy()
}
},
build: {
outDir: outputRoot,
emptyOutDir: true,
assetsDir: "vite-assets",
target: "es2022",
sourcemap: true
}
});
+170
View File
@@ -0,0 +1,170 @@
import { Fragment, createApp, h, nextTick, reactive } from "vue";
(function () {
"use strict";
const VUE_VERSION = "3.5.21";
const modeMounts = new WeakMap();
const commandMounts = new WeakMap();
const text = (value, fallback = "") => String(value ?? fallback);
function refreshIcons() {
nextTick(() => window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } }));
}
function replaceReactiveArray(target, values) {
target.splice(0, target.length, ...values.map((value) => ({ ...value })));
}
function renderModeEntry(entry) {
const key = text(entry.key || `${entry.type}-${entry.id || entry.label}`);
if (entry.type === "label") {
return h("span", {
key,
class: "mode-group-label",
"data-mode-group-label": text(entry.id),
}, text(entry.label));
}
if (entry.type === "divider") {
return h("span", {
key,
class: "mode-group-divider",
"data-mode-divider": text(entry.id),
"aria-hidden": "true",
});
}
const children = [
h("i", {
class: "mode-chip-icon",
"data-lucide": text(entry.icon, "bookmark"),
}),
h("span", { class: "mode-chip-label" }, text(entry.label, "模式")),
h("small", { class: "mode-chip-summary" }, text(entry.summary, "整车联动")),
];
if (entry.cycleLabel) {
children.push(h("small", { class: "mode-cycle-badge" }, text(entry.cycleLabel)));
}
children.push(h("i", {
class: "mode-chip-run",
"data-lucide": "play",
"data-mode-run": "true",
title: `执行${text(entry.label, "模式")}`,
}));
return h("button", {
key,
type: "button",
title: text(entry.title),
"data-mode-profile-id": text(entry.id),
"data-mode-command": text(entry.command),
"data-mode-origin": text(entry.origin, "preset"),
...(entry.behavior ? { "data-mode-behavior": text(entry.behavior) } : {}),
"aria-label": text(entry.ariaLabel, `${text(entry.label, "模式")},点击执行`),
"aria-pressed": "false",
}, children);
}
function createModeMount(container) {
const state = reactive({ entries: [] });
const app = createApp({
name: "N7ModeChipRail",
setup() {
return () => h(Fragment, null, state.entries.map(renderModeEntry));
},
});
app.mount(container);
const mount = { app, state };
modeMounts.set(container, mount);
return mount;
}
function renderModeChips(container, entries) {
if (!(container instanceof Element) || !Array.isArray(entries)) return false;
const mount = modeMounts.get(container) || createModeMount(container);
replaceReactiveArray(mount.state.entries, entries);
container.dataset.vueSurface = "mode-chips";
refreshIcons();
return true;
}
function renderCommandRow(command, index) {
const semantic = text(command.semantic);
const actual = text(command.command);
const accessibleText = [
semantic ? `语义控制指令:${semantic}` : "",
actual ? `实际控制指令:${actual}` : "",
command.deliveryLabel
? `下发状态:${text(command.deliveryLabel)}${command.deliveryDetail ? `(${text(command.deliveryDetail)})` : ""}`
: "",
].filter(Boolean).join(";");
return h("li", {
key: text(command.key, `command-${index}`),
title: accessibleText,
"data-delivery-state": text(command.deliveryState, "not_submitted"),
}, [
h("span", { class: "assistant-dispatched-control-icon" }, [
h("i", { "data-lucide": text(command.icon, "binary") }),
]),
h("span", { class: "assistant-dispatched-control-label" }, text(command.category, "控制指令")),
h("strong", { class: "assistant-dispatched-control-value" }, text(command.value, "控制")),
h("small", { class: "visually-hidden" }, accessibleText),
]);
}
function createCommandMount(container) {
const state = reactive({ commands: [], open: true });
const app = createApp({
name: "N7DispatchedControlsCard",
setup() {
return () => h("details", {
class: "assistant-dispatched-controls",
open: state.open,
onToggle(event) {
state.open = event.currentTarget.open;
if (!state.open) return;
const transcript = document.querySelector("#assistantMessages");
transcript?.querySelectorAll(".assistant-dispatched-controls[open]").forEach((card) => {
if (card !== event.currentTarget) card.open = false;
});
},
}, [
h("summary", null, [
h("span", { class: "assistant-dispatched-controls-heading-icon" }, [
h("i", { "data-lucide": "list-tree" }),
]),
h("b", null, "下发控制指令"),
h("strong", { class: "assistant-dispatched-controls-count" }, String(state.commands.length)),
h("i", {
class: "assistant-dispatched-controls-chevron",
"data-lucide": "chevron-down",
}),
]),
h("ol", { class: "assistant-dispatched-controls-list" }, state.commands.map(renderCommandRow)),
]);
},
});
app.mount(container);
const mount = { app, state };
commandMounts.set(container, mount);
return mount;
}
function renderDispatchedControls(container, commands) {
if (!(container instanceof Element) || !Array.isArray(commands) || !commands.length) {
return false;
}
const mount = commandMounts.get(container) || createCommandMount(container);
replaceReactiveArray(mount.state.commands, commands);
container.dataset.vueSurface = "dispatched-controls";
refreshIcons();
return true;
}
document.documentElement.classList.add("vue-presentation-enabled");
document.documentElement.dataset.vueVersion = VUE_VERSION;
window.CognitiveAgentVueUI = Object.freeze({
available: true,
version: VUE_VERSION,
renderDispatchedControls,
renderModeChips,
});
}());
Binary file not shown.

Before

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

@@ -0,0 +1,87 @@
{
"schema_version": "1.0.0",
"validated_at": "2026-08-04T09:55:52+08:00",
"platform": "t2g_cabin_bench",
"adapter": {
"model": "USBCANFD-200U",
"device_type": 41,
"device_index": 0,
"driver_self_test": {
"passed": true,
"pointer_bits": 32,
"missing_exports": [],
"classic_frame_size": 16,
"can_fd_frame_size": 72,
"can_fd_tx_size": 76,
"can_fd_rx_size": 80
}
},
"channels": {
"seat_can": {
"channel": 0,
"format": "classic_can_11_bit",
"bitrate": 500000,
"diagnostic_self_reception": {
"frame_id": "0x7AA",
"payload_hex": "43 48 30 53 45 41 54 30",
"passed": true
},
"external_passive_observation_seconds": 6,
"external_frames_observed": 0,
"physical_feedback_verified": false
},
"body_can": {
"channel": 1,
"format": "iso_can_fd_11_bit",
"arbitration_bitrate": 500000,
"data_bitrate": 2000000,
"bitrate_switch": true,
"diagnostic_self_reception": {
"frame_id": "0x7AA",
"payload_hex": "43 48 31 2D 42 4F 44 59 2D 43 41 4E 46 44 21 21",
"payload_size": 16,
"passed": true
},
"external_passive_observation_seconds": 5,
"external_frames_observed": 0,
"physical_feedback_verified": false
}
},
"linked_request": {
"utterance": "关闭副驾座椅通风,同时关闭空调",
"dry_run": {
"status": "succeeded",
"function_configs": ["seat", "native_climate"],
"command_plans": ["seat", "native_climate"]
},
"hardware_attempts": [
{
"request_id": "req-20260804015505-713de5",
"status": "execution_failed",
"body_can": "0x1DE transmit rejected; channel_error=0x00000004",
"seat_can": "passive firmware signature not observed within 1200 ms; no seat control frame sent"
},
{
"request_id": "req-20260804015552-674fb4",
"status": "execution_failed",
"body_can": "0x1DE transmit rejected; channel_error=0x00000000",
"seat_can": "passive firmware signature not observed within 3500 ms; no seat control frame sent"
}
]
},
"verdict": {
"adapter_dual_channel_transport": "passed",
"classic_can_transport": "passed",
"iso_can_fd_brs_transport": "passed",
"one_sentence_compilation_and_routing": "passed",
"seat_ecu_closed_loop": "not_verified_no_external_frames",
"body_ecu_closed_loop": "not_verified_no_bus_ack_or_0x404_feedback",
"overall_hardware_requirement": "blocked_by_external_bus_connectivity_or_power"
},
"safety_cleanup": {
"seat_control_acknowledged": false,
"body_control_acknowledged": false,
"actuator_state_changed_by_verified_command": false,
"zcanpro_restarted": true
}
}
@@ -0,0 +1,267 @@
{
"suite": "ch1-vehicle-door-smoke-hil",
"started_with": {
"source": "physical_can_ch1",
"channel": 1,
"read_only": true,
"captured_at": "2026-08-10T18:26:38.774392Z",
"gear": "P",
"speed_kmh": 0.0,
"frame_count": 2751,
"evidence": {
"gear_frame_id": "0x342",
"speed_frame_id": "0x4EB"
}
},
"finished_at": "2026-08-10T18:27:06.208449Z",
"results": [
{
"at": "2026-08-10T18:26:43.864787Z",
"module": "self_orchestration",
"case_id": "DOOR-VEHICLE-LOCK",
"status": "failed",
"duration_ms": 4032,
"safety_snapshot": {
"source": "physical_can_ch1",
"channel": 1,
"read_only": true,
"captured_at": "2026-08-10T18:26:39.775825Z",
"gear": "P",
"speed_kmh": 0.0,
"frame_count": 2256,
"evidence": {
"gear_frame_id": "0x342",
"speed_frame_id": "0x4EB"
}
},
"error": "Ch1HilContractError: DOOR-VEHICLE-LOCK: request status is execution_failed"
},
{
"at": "2026-08-10T18:27:06.208449Z",
"module": "cleanup",
"case_id": "suite_finally",
"status": "passed",
"results": [
{
"case_id": "WINDOW-FRONT-LEFT-0",
"command": "window front-left position 0",
"http_status": 201,
"status": "passed",
"duration_ms": 2843,
"safety_snapshot": {
"source": "physical_can_ch1",
"channel": 1,
"read_only": true,
"captured_at": "2026-08-10T18:26:44.790088Z",
"gear": "P",
"speed_kmh": 0.0,
"frame_count": 2279,
"evidence": {
"gear_frame_id": "0x342",
"speed_frame_id": "0x4EB"
}
},
"request_id": "req-20260810182644-0f4f23"
},
{
"case_id": "SHADE-POSITION-0",
"command": "shade position 0",
"http_status": 201,
"status": "passed",
"duration_ms": 2235,
"safety_snapshot": {
"source": "physical_can_ch1",
"channel": 1,
"read_only": true,
"captured_at": "2026-08-10T18:26:47.795715Z",
"gear": "P",
"speed_kmh": 0.0,
"frame_count": 2694,
"evidence": {
"gear_frame_id": "0x342",
"speed_frame_id": "0x4EB"
}
},
"request_id": "req-20260810182647-436fd7"
},
{
"case_id": "DOOR-VEHICLE-UNLOCK",
"command": "door vehicle unlock",
"http_status": 201,
"status": "passed",
"duration_ms": 2906,
"safety_snapshot": {
"source": "physical_can_ch1",
"channel": 1,
"read_only": true,
"captured_at": "2026-08-10T18:26:49.802764Z",
"gear": "P",
"speed_kmh": 0.0,
"frame_count": 2017,
"evidence": {
"gear_frame_id": "0x342",
"speed_frame_id": "0x4EB"
}
},
"request_id": "req-20260810182649-4a52a7"
},
{
"case_id": "SEAT-DRIVER-MASSAGE-OFF",
"command": "seat driver massage off",
"http_status": 201,
"status": "passed",
"duration_ms": 2500,
"safety_snapshot": {
"source": "physical_can_ch1",
"channel": 1,
"read_only": true,
"captured_at": "2026-08-10T18:26:52.820116Z",
"gear": "P",
"speed_kmh": 0.0,
"frame_count": 2374,
"evidence": {
"gear_frame_id": "0x342",
"speed_frame_id": "0x4EB"
}
},
"request_id": "req-20260810182652-028f24"
},
{
"case_id": "SEAT-DRIVER-HEAT-OFF",
"command": "seat driver heat off",
"http_status": 201,
"status": "passed",
"duration_ms": 2000,
"safety_snapshot": {
"source": "physical_can_ch1",
"channel": 1,
"read_only": true,
"captured_at": "2026-08-10T18:26:54.817949Z",
"gear": "P",
"speed_kmh": 0.0,
"frame_count": 697,
"evidence": {
"gear_frame_id": "0x342",
"speed_frame_id": "0x4EB"
}
},
"request_id": "req-20260810182654-200daa"
},
{
"case_id": "SEAT-DRIVER-VENT-OFF",
"command": "seat driver vent off",
"http_status": 201,
"status": "passed",
"duration_ms": 2000,
"safety_snapshot": {
"source": "physical_can_ch1",
"channel": 1,
"read_only": true,
"captured_at": "2026-08-10T18:26:56.821969Z",
"gear": "P",
"speed_kmh": 0.0,
"frame_count": 709,
"evidence": {
"gear_frame_id": "0x342",
"speed_frame_id": "0x4EB"
}
},
"request_id": "req-20260810182656-77bc93"
},
{
"case_id": "CLIMATE-POWER-OFF",
"command": "ac off",
"http_status": 201,
"status": "passed",
"duration_ms": 1859,
"safety_snapshot": {
"source": "physical_can_ch1",
"channel": 1,
"read_only": true,
"captured_at": "2026-08-10T18:26:58.826087Z",
"gear": "P",
"speed_kmh": 0.0,
"frame_count": 735,
"evidence": {
"gear_frame_id": "0x342",
"speed_frame_id": "0x4EB"
}
},
"request_id": "req-20260810182658-1509d2"
},
{
"case_id": "LIGHT-INDOOR-OFF",
"command": "light indoor off",
"http_status": 201,
"status": "passed",
"duration_ms": 1953,
"safety_snapshot": {
"source": "physical_can_ch1",
"channel": 1,
"read_only": true,
"captured_at": "2026-08-10T18:27:00.833034Z",
"gear": "P",
"speed_kmh": 0.0,
"frame_count": 1204,
"evidence": {
"gear_frame_id": "0x342",
"speed_frame_id": "0x4EB"
}
},
"request_id": "req-20260810182700-64ded1"
},
{
"case_id": "AMBIENT-POWER-OFF",
"command": "light ambient off",
"http_status": 201,
"status": "passed",
"duration_ms": 2172,
"safety_snapshot": {
"source": "physical_can_ch1",
"channel": 1,
"read_only": true,
"captured_at": "2026-08-10T18:27:02.837333Z",
"gear": "P",
"speed_kmh": 0.0,
"frame_count": 1445,
"evidence": {
"gear_frame_id": "0x342",
"speed_frame_id": "0x4EB"
}
},
"request_id": "req-20260810182702-78f422"
},
{
"case_id": "LIGHT-READING-ALL-OFF",
"command": "light reading all off",
"http_status": 201,
"status": "passed",
"duration_ms": 1875,
"safety_snapshot": {
"source": "physical_can_ch1",
"channel": 1,
"read_only": true,
"captured_at": "2026-08-10T18:27:04.842348Z",
"gear": "P",
"speed_kmh": 0.0,
"frame_count": 829,
"evidence": {
"gear_frame_id": "0x342",
"speed_frame_id": "0x4EB"
}
},
"request_id": "req-20260810182704-244a2c"
}
]
}
],
"summary": {
"total": 1,
"passed": 0,
"failed": 1,
"aborted_on_first_failure": true,
"cleanup_runs": 1,
"cleanup_failed": false,
"offline_rejection_cases": 24
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,63 @@
{
"validated_at": "2026-08-04",
"firmware_revision": 10,
"firmware_image_sha256": "85f9d67ff1efb36c37f38bb7584dffdbb6ecbee33bfc70ab491bad0d57da63b2",
"change": "remove delayed M1 clearance/catch-up and keep all four axes moving from the phase start",
"max_hall": [499, 374, 917, 647],
"known_range_home": {
"hall_before": [379, 255, 417, 416],
"hall_after": [0, 0, 0, 0],
"all_complete_ok": true,
"all_position_valid": true
},
"step_1_to_step_2": {
"start_hall": [391, 140, 406, 405],
"target_hall": [246, 281, 701, 494],
"observed_hall": [246, 281, 701, 494],
"four_axis_start_spread_s": 0.070,
"four_axis_arrival_spread_s": 0.342,
"cruise_checkpoint": {
"elapsed_s": 3.090,
"hall": [354, 202, 510, 468],
"normalized_progress": {
"longitudinal": 0.255,
"cushion_tilt": 0.440,
"backrest_recline": 0.353,
"legrest_height": 0.708
}
},
"result": "PASS"
},
"step_2_to_step_3": {
"start_hall": [246, 281, 701, 494],
"target_hall": [391, 140, 405, 404],
"observed_hall": [391, 140, 405, 404],
"four_axis_start_spread_s": 0.050,
"four_axis_arrival_spread_s": 0.339,
"cruise_checkpoint": {
"elapsed_s": 2.591,
"hall": [283, 226, 616, 435],
"normalized_progress": {
"longitudinal": 0.255,
"cushion_tilt": 0.390,
"backrest_recline": 0.287,
"legrest_height": 0.656
}
},
"result": "PASS"
},
"continuous_rollover": {
"next_cycle_all_four_axes_active": true,
"next_cycle_start_spread_s": 0.051,
"result": "PASS"
},
"stop_verification": {
"ack": 1,
"reject": 0,
"final_0x306": "00 00 00 00 00 00 00 00",
"active_mask": 0,
"fault_mask": 0,
"result": "PASS"
},
"result": "PASS"
}
@@ -0,0 +1,88 @@
{
"validated_at": "2026-08-04",
"firmware_revision": 9,
"firmware_image_sha256": "216f3f6b2d7c441f2050e99c2b9d54153a69b46466235232dec1b7ab7644b7c4",
"mode": "dynamic_cradle",
"coordinate_precondition": {
"motor": 1,
"operation": "HOME_KNOWN_RANGE",
"ack": 1,
"reject": 0,
"hall_before": 169,
"hall_after": 0,
"max_hall_preserved": 374,
"near": true,
"position_valid": true,
"result": "PASS"
},
"full_cycle": {
"entry_step_1": {
"target_hall": [391, 140, 405, 404],
"observed_hall": [392, 141, 405, 403]
},
"step_1_to_step_2": {
"target_hall": [246, 281, 701, 494],
"observed_hall": [246, 281, 701, 494],
"duration_s": 17.329,
"four_axis_start_spread_s": 0.077,
"four_axis_arrival_spread_s": 0.338,
"result": "PASS"
},
"step_2_to_step_3": {
"target_hall": [391, 140, 405, 404],
"observed_hall": [391, 140, 405, 405],
"duration_s": 15.155,
"four_axis_start_spread_s": 0.079,
"four_axis_arrival_spread_s": 0.495,
"result": "PASS"
},
"cycle_index_after_return": 1,
"fault_mask": 0,
"result": "PASS"
},
"continuous_rollover": {
"cycle_0_forward_start_spread_s": 0.052,
"cycle_0_return_start_spread_s": 0.053,
"cycle_1_forward_start_spread_s": 0.082,
"phase_transitions_s": [0.370, 17.888, 32.781],
"result": "PASS"
},
"step_3_to_step_1_semantics": "Step 3 and Step 1 have identical Hall targets, so their boundary has zero physical displacement. The ECU rolls the cycle index atomically and synchronously launches the next Step 1-to-Step 2 leg.",
"stop_verification": {
"stop_dynamic_sent": true,
"final_0x306": "00 00 00 00 00 00 00 00",
"result": "PASS"
},
"frontend_natural_language_hil": {
"start": {
"utterance": "开启动态摇篮模式",
"request_id": "req-20260804071706-45a1df",
"result": "SUCCEEDED",
"firmware_signature": "seat-controller-smv8-dynamic-modes-v9",
"command": "0x300 0E 10 06 00 02 00 00 00",
"ui_state": "动态摇篮模式 · 循环中 · 最长10分钟"
},
"active_physical_can_observation": {
"source": "physical_can",
"complete": true,
"moving_axes": {
"longitudinal_pct": 59,
"backrest_recline_pct": 75
},
"all_axes_position_valid": true,
"faults": false
},
"stop": {
"utterance": "关闭动态摇篮模式",
"request_id": "req-20260804072028-55179f",
"result": "SUCCEEDED",
"command": "0x300 0F 10 07 00 00 00 00 00",
"ui_state": "未运行",
"all_axes_stopped": true,
"all_axes_position_valid": true,
"faults": false
},
"hardware_access_after_test": "automatically released by the 180-second local idle policy",
"result": "PASS"
}
}
@@ -0,0 +1,343 @@
{
"mode": "rocking_horse",
"firmware_revision": 11,
"max_hall": [
499,
374,
917,
647
],
"homes": [
{
"motor": 0,
"hall_before": 349,
"hall_after": 0,
"ack": {
"ack": 1,
"reject": 0,
"sequence": 21,
"opcode": 11,
"firmware_revision": 11
},
"terminal": {
"motor": 0,
"task": 6,
"result": 2,
"active": false,
"position_valid": true,
"sequence": 21,
"position_x100": 0
}
},
{
"motor": 1,
"hall_before": 163,
"hall_after": 0,
"ack": {
"ack": 1,
"reject": 0,
"sequence": 22,
"opcode": 11,
"firmware_revision": 11
},
"terminal": {
"motor": 1,
"task": 6,
"result": 2,
"active": false,
"position_valid": true,
"sequence": 22,
"position_x100": 0
}
},
{
"motor": 2,
"hall_before": 534,
"hall_after": 0,
"ack": {
"ack": 1,
"reject": 0,
"sequence": 23,
"opcode": 11,
"firmware_revision": 11
},
"terminal": {
"motor": 2,
"task": 6,
"result": 2,
"active": false,
"position_valid": true,
"sequence": 23,
"position_x100": 0
}
},
{
"motor": 3,
"hall_before": 456,
"hall_after": 0,
"ack": {
"ack": 1,
"reject": 0,
"sequence": 24,
"opcode": 11,
"firmware_revision": 11
},
"terminal": {
"motor": 3,
"task": 6,
"result": 2,
"active": false,
"position_valid": true,
"sequence": 24,
"position_x100": 0
}
}
],
"start_ack": {
"ack": 1,
"reject": 0,
"sequence": 37,
"opcode": 14,
"firmware_revision": 11
},
"segments": [
{
"cycle_index": 0,
"phase": 2,
"phase_start": 262550.8121217,
"start_hall": [
320,
191,
500,
431
],
"first_active": {
"2": 262550.8883221,
"3": 262550.9146271,
"0": 262550.9401055,
"1": 262550.9661217
},
"arrival": {
"3": 262552.7830827,
"1": 262552.8335206,
"2": 262553.0696745,
"0": 262553.1213695
},
"cruise_checkpoint": {
"elapsed_s": 0.6343701000441797,
"hall": [
312,
201,
481,
417
],
"normalized_progress": [
0.26666666666666666,
0.3448275862068966,
0.475,
0.45161290322580644
]
},
"end_hall": [
290,
219,
460,
400
],
"start_spread_s": 0.07779960002517328,
"arrival_spread_s": 0.3382868000189774,
"all_axes_started": true,
"all_axes_arrived": true,
"target": [
290,
220,
460,
400
]
},
{
"cycle_index": 0,
"phase": 3,
"phase_start": 262553.4043395,
"start_hall": [
290,
219,
460,
400
],
"first_active": {
"2": 262553.4973208,
"3": 262553.5228996,
"0": 262553.5485228,
"1": 262553.5743734
},
"arrival": {
"3": 262557.0745088,
"1": 262557.1252219,
"2": 262557.2577929,
"0": 262557.3081852
},
"cruise_checkpoint": {
"elapsed_s": 1.079516199999489,
"hall": [
306,
199,
489,
422
],
"normalized_progress": [
0.26666666666666666,
0.3389830508474576,
0.3625,
0.36666666666666664
]
},
"end_hall": [
350,
160,
540,
460
],
"start_spread_s": 0.0770526000414975,
"arrival_spread_s": 0.23367639997741207,
"all_axes_started": true,
"all_axes_arrived": true,
"target": [
350,
160,
540,
460
]
},
{
"cycle_index": 1,
"phase": 2,
"phase_start": 262557.4375185,
"start_hall": [
350,
160,
540,
460
],
"first_active": {
"3": 262557.4893286,
"0": 262557.5161744,
"1": 262557.5424914,
"2": 262557.5696487
},
"arrival": {
"3": 262561.8903825,
"1": 262561.9425291,
"2": 262562.0729115,
"0": 262562.1245799
},
"cruise_checkpoint": {
"elapsed_s": 1.337460899958387,
"hall": [
335,
185,
498,
430
],
"normalized_progress": [
0.25,
0.4166666666666667,
0.525,
0.5
]
},
"end_hall": [
290,
220,
460,
400
],
"start_spread_s": 0.08032010000897571,
"arrival_spread_s": 0.23419739998644218,
"all_axes_started": true,
"all_axes_arrived": true,
"target": [
290,
220,
460,
400
]
},
{
"cycle_index": 1,
"phase": 3,
"phase_start": 262562.35135919997,
"start_hall": [
290,
220,
460,
400
],
"first_active": {
"3": 262562.3911222,
"0": 262562.4169718,
"1": 262562.4437546,
"2": 262562.4700679
},
"arrival": {
"3": 262566.0670384,
"1": 262566.1195433,
"2": 262566.1444,
"0": 262566.2991242
},
"cruise_checkpoint": {
"elapsed_s": 1.1310953000211157,
"hall": [
305,
198,
497,
423
],
"normalized_progress": [
0.25,
0.36666666666666664,
0.4625,
0.38333333333333336
]
},
"end_hall": [
350,
160,
540,
459
],
"start_spread_s": 0.07894570002099499,
"arrival_spread_s": 0.23208580003120005,
"all_axes_started": true,
"all_axes_arrived": true,
"target": [
350,
160,
540,
460
]
}
],
"repeat_first_active": {
"0": 262566.4901705,
"1": 262566.5153454,
"2": 262566.542698,
"3": 262566.5681911
},
"stop_ack": {
"ack": 1,
"reject": 0,
"sequence": 38,
"opcode": 15,
"firmware_revision": 11
},
"idle": {
"mode_id": 0,
"state": 0,
"phase": 0,
"active_mask": 0,
"fault_mask": 0,
"cycle_index": 0,
"phase_elapsed_ms": 0
}
}
@@ -0,0 +1,343 @@
{
"mode": "rocking_horse",
"firmware_revision": 12,
"max_hall": [
499,
374,
917,
647
],
"homes": [
{
"motor": 0,
"hall_before": 342,
"hall_after": 0,
"ack": {
"ack": 1,
"reject": 0,
"sequence": 1,
"opcode": 11,
"firmware_revision": 12
},
"terminal": {
"motor": 0,
"task": 6,
"result": 2,
"active": false,
"position_valid": true,
"sequence": 1,
"position_x100": 0
}
},
{
"motor": 1,
"hall_before": 166,
"hall_after": 0,
"ack": {
"ack": 1,
"reject": 0,
"sequence": 2,
"opcode": 11,
"firmware_revision": 12
},
"terminal": {
"motor": 1,
"task": 6,
"result": 2,
"active": false,
"position_valid": true,
"sequence": 2,
"position_x100": 0
}
},
{
"motor": 2,
"hall_before": 534,
"hall_after": 0,
"ack": {
"ack": 1,
"reject": 0,
"sequence": 3,
"opcode": 11,
"firmware_revision": 12
},
"terminal": {
"motor": 2,
"task": 6,
"result": 2,
"active": false,
"position_valid": true,
"sequence": 3,
"position_x100": 0
}
},
{
"motor": 3,
"hall_before": 454,
"hall_after": 0,
"ack": {
"ack": 1,
"reject": 0,
"sequence": 4,
"opcode": 11,
"firmware_revision": 12
},
"terminal": {
"motor": 3,
"task": 6,
"result": 2,
"active": false,
"position_valid": true,
"sequence": 4,
"position_x100": 0
}
}
],
"start_ack": {
"ack": 1,
"reject": 0,
"sequence": 17,
"opcode": 14,
"firmware_revision": 12
},
"segments": [
{
"cycle_index": 0,
"phase": 2,
"phase_start": 264077.9579478,
"start_hall": [
320,
191,
500,
430
],
"first_active": {
"0": 264078.038142,
"1": 264078.0663315,
"2": 264078.0774349,
"3": 264078.1169224
},
"arrival": {
"3": 264079.9439302,
"1": 264079.9959276,
"2": 264080.2325822,
"0": 264080.2840808
},
"cruise_checkpoint": {
"elapsed_s": 0.6441632999922149,
"hall": [
312,
214,
481,
416
],
"normalized_progress": [
0.26666666666666666,
0.3898305084745763,
0.475,
0.4666666666666667
]
},
"end_hall": [
290,
250,
460,
400
],
"start_spread_s": 0.07878040004288778,
"arrival_spread_s": 0.34015060000820085,
"all_axes_started": true,
"all_axes_arrived": true,
"target": [
290,
250,
460,
400
]
},
{
"cycle_index": 0,
"phase": 3,
"phase_start": 264080.62360189995,
"start_hall": [
290,
250,
460,
400
],
"first_active": {
"0": 264080.6481176,
"1": 264080.6733371,
"2": 264080.7001602,
"3": 264080.7257557
},
"arrival": {
"1": 264084.2944588,
"3": 264084.344339,
"0": 264084.475855,
"2": 264084.5271018
},
"cruise_checkpoint": {
"elapsed_s": 1.0275715000461787,
"hall": [
305,
206,
492,
421
],
"normalized_progress": [
0.25,
0.36666666666666664,
0.4,
0.35
]
},
"end_hall": [
350,
132,
540,
460
],
"start_spread_s": 0.07763810001779348,
"arrival_spread_s": 0.2326430000248365,
"all_axes_started": true,
"all_axes_arrived": true,
"target": [
350,
130,
540,
460
]
},
{
"cycle_index": 1,
"phase": 2,
"phase_start": 264084.65867120004,
"start_hall": [
350,
132,
540,
460
],
"first_active": {
"2": 264084.7388101,
"1": 264084.7410872,
"0": 264084.7599586,
"3": 264084.7926978
},
"arrival": {
"3": 264089.0549796,
"1": 264089.108405,
"0": 264089.3941196,
"2": 264089.4443601
},
"cruise_checkpoint": {
"elapsed_s": 1.391282099939417,
"hall": [
335,
187,
497,
429
],
"normalized_progress": [
0.25,
0.4661016949152542,
0.5375,
0.5166666666666667
]
},
"end_hall": [
290,
250,
460,
400
],
"start_spread_s": 0.05388769996352494,
"arrival_spread_s": 0.38938050001161173,
"all_axes_started": true,
"all_axes_arrived": true,
"target": [
290,
250,
460,
400
]
},
{
"cycle_index": 1,
"phase": 3,
"phase_start": 264089.7385309,
"start_hall": [
290,
250,
460,
400
],
"first_active": {
"1": 264089.857611,
"2": 264089.8838599,
"3": 264089.910015,
"0": 264089.938548
},
"arrival": {
"1": 264093.3849972,
"3": 264093.4384044,
"2": 264093.5165479,
"0": 264093.5667763
},
"cruise_checkpoint": {
"elapsed_s": 1.011584700027015,
"hall": [
305,
207,
492,
420
],
"normalized_progress": [
0.25,
0.35833333333333334,
0.4,
0.3333333333333333
]
},
"end_hall": [
350,
132,
540,
460
],
"start_spread_s": 0.08093699999153614,
"arrival_spread_s": 0.1817791000357829,
"all_axes_started": true,
"all_axes_arrived": true,
"target": [
350,
130,
540,
460
]
}
],
"repeat_first_active": {
"0": 264093.8496089,
"2": 264093.8578157,
"3": 264093.8843526,
"1": 264093.9379308
},
"stop_ack": {
"ack": 1,
"reject": 0,
"sequence": 18,
"opcode": 15,
"firmware_revision": 12
},
"idle": {
"mode_id": 0,
"state": 0,
"phase": 0,
"active_mask": 0,
"fault_mask": 0,
"cycle_index": 0,
"phase_elapsed_ms": 0
}
}
@@ -0,0 +1,343 @@
{
"mode": "rocking_horse",
"firmware_revision": 13,
"max_hall": [
499,
374,
917,
647
],
"homes": [
{
"motor": 0,
"hall_before": 326,
"hall_after": 0,
"ack": {
"ack": 1,
"reject": 0,
"sequence": 1,
"opcode": 11,
"firmware_revision": 13
},
"terminal": {
"motor": 0,
"task": 6,
"result": 2,
"active": false,
"position_valid": true,
"sequence": 1,
"position_x100": 0
}
},
{
"motor": 1,
"hall_before": 138,
"hall_after": 0,
"ack": {
"ack": 1,
"reject": 0,
"sequence": 2,
"opcode": 11,
"firmware_revision": 13
},
"terminal": {
"motor": 1,
"task": 6,
"result": 2,
"active": false,
"position_valid": true,
"sequence": 2,
"position_x100": 0
}
},
{
"motor": 2,
"hall_before": 532,
"hall_after": 0,
"ack": {
"ack": 1,
"reject": 0,
"sequence": 3,
"opcode": 11,
"firmware_revision": 13
},
"terminal": {
"motor": 2,
"task": 6,
"result": 2,
"active": false,
"position_valid": true,
"sequence": 3,
"position_x100": 0
}
},
{
"motor": 3,
"hall_before": 451,
"hall_after": 0,
"ack": {
"ack": 1,
"reject": 0,
"sequence": 4,
"opcode": 11,
"firmware_revision": 13
},
"terminal": {
"motor": 3,
"task": 6,
"result": 2,
"active": false,
"position_valid": true,
"sequence": 4,
"position_x100": 0
}
}
],
"start_ack": {
"ack": 1,
"reject": 0,
"sequence": 17,
"opcode": 14,
"firmware_revision": 13
},
"segments": [
{
"cycle_index": 0,
"phase": 2,
"phase_start": 265437.2473564,
"start_hall": [
320,
190,
500,
430
],
"first_active": {
"2": 265437.2955111,
"3": 265437.3198522,
"0": 265437.346692,
"1": 265437.3733125
},
"arrival": {
"1": 265438.9329059,
"3": 265438.9846889,
"0": 265439.2201775,
"2": 265439.2731793
},
"cruise_checkpoint": {
"elapsed_s": 0.5042391999741085,
"hall": [
306,
209,
486,
419
],
"normalized_progress": [
0.28,
0.31666666666666665,
0.35,
0.36666666666666664
]
},
"end_hall": [
270,
250,
460,
400
],
"start_spread_s": 0.07780140003887936,
"arrival_spread_s": 0.34027340001193807,
"all_axes_started": true,
"all_axes_arrived": true,
"target": [
270,
250,
460,
400
]
},
{
"cycle_index": 0,
"phase": 3,
"phase_start": 265439.64164610003,
"start_hall": [
270,
250,
460,
400
],
"first_active": {
"2": 265439.6948618,
"3": 265439.7208077,
"0": 265439.7458537,
"1": 265439.7726268
},
"arrival": {
"1": 265442.9056772,
"3": 265442.9558483,
"2": 265443.0339587,
"0": 265443.0861803
},
"cruise_checkpoint": {
"elapsed_s": 0.8318679999792948,
"hall": [
295,
216,
484,
417
],
"normalized_progress": [
0.25,
0.2833333333333333,
0.3,
0.2833333333333333
]
},
"end_hall": [
370,
130,
540,
460
],
"start_spread_s": 0.07776499999454245,
"arrival_spread_s": 0.18050309998216107,
"all_axes_started": true,
"all_axes_arrived": true,
"target": [
370,
130,
540,
460
]
},
{
"cycle_index": 1,
"phase": 2,
"phase_start": 265443.25305889995,
"start_hall": [
370,
130,
540,
460
],
"first_active": {
"0": 265443.2954135,
"1": 265443.3206536,
"2": 265443.3348354,
"3": 265443.3728105
},
"arrival": {
"3": 265446.8221541,
"1": 265446.8749014,
"2": 265447.0045205,
"0": 265447.159461
},
"cruise_checkpoint": {
"elapsed_s": 1.084834800043609,
"hall": [
343,
175,
507,
436
],
"normalized_progress": [
0.27,
0.375,
0.4125,
0.4
]
},
"end_hall": [
270,
250,
460,
401
],
"start_spread_s": 0.07739699998637661,
"arrival_spread_s": 0.3373069000081159,
"all_axes_started": true,
"all_axes_arrived": true,
"target": [
270,
250,
460,
400
]
},
{
"cycle_index": 1,
"phase": 3,
"phase_start": 265447.3484032,
"start_hall": [
270,
250,
460,
401
],
"first_active": {
"1": 265447.3961179,
"0": 265447.400082,
"2": 265447.4222059,
"3": 265447.4483388
},
"arrival": {
"1": 265450.6434356,
"3": 265450.693563,
"2": 265450.7765666,
"0": 265450.8258244
},
"cruise_checkpoint": {
"elapsed_s": 0.9676259999978356,
"hall": [
297,
209,
490,
421
],
"normalized_progress": [
0.27,
0.3416666666666667,
0.375,
0.3389830508474576
]
},
"end_hall": [
370,
130,
540,
460
],
"start_spread_s": 0.05222090001916513,
"arrival_spread_s": 0.18238880002172664,
"all_axes_started": true,
"all_axes_arrived": true,
"target": [
370,
130,
540,
460
]
}
],
"repeat_first_active": {
"3": 265450.9977942,
"0": 265451.025777,
"2": 265451.0380102,
"1": 265451.0497914
},
"stop_ack": {
"ack": 1,
"reject": 0,
"sequence": 18,
"opcode": 15,
"firmware_revision": 13
},
"idle": {
"mode_id": 0,
"state": 0,
"phase": 0,
"active_mask": 0,
"fault_mask": 0,
"cycle_index": 0,
"phase_elapsed_ms": 0
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,764 @@
{
"suite": "t2g-seat-frontend-full-hil",
"started_at": "2026-08-01T14:53:29.226676Z",
"base_url": "http://127.0.0.1:8000",
"vehicle_id": "denza_n7",
"conversation_id": "conv-hil-full-20260801145329",
"runtime": {
"hardware_execution_allowed": true,
"firmware_profiles": [
"combined"
]
},
"results": [
{
"id": "MODE-QUEEN",
"group": "mode",
"utterance": "副驾执行女王副驾行驶模式",
"started_at": "2026-08-01T14:53:29.226724Z",
"status": "failed",
"errors": [
"T2G motor 0 TIMEOUT: task_state=7, Hall=499->499"
],
"http_status": 201,
"request_id": "req-20260801145329-c441d5",
"failure_telemetry": {
"motor_selector": 0,
"motion_axis": "longitudinal",
"task_state": 7,
"motor_result": 6,
"motor_result_name": "TIMEOUT",
"position_valid": true,
"hall_before": 499,
"hall_after": 499,
"hall_delta": 0,
"endpoint_recovery_max_near_hall": 72,
"command_frame": {
"frame_id": "0x300",
"payload_size": 8,
"data_hex": "01 10 30 00 20 1C 30 75",
"timestamp": null
},
"ack_frame": {
"frame_id": "0x301",
"payload_size": 8,
"data_hex": "21 00 30 00 01 01 F0 4B",
"timestamp": 26444.37267
},
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 67 28 30 00 10 27 00",
"timestamp": 26450.902006
},
"diagnostic_before_frame": {
"frame_id": "0x303",
"payload_size": 8,
"data_hex": "00 00 F3 01 F3 01 00 90",
"timestamp": 26444.360963
},
"diagnostic_frame": {
"frame_id": "0x303",
"payload_size": 8,
"data_hex": "00 05 F3 01 F3 01 00 A0",
"timestamp": 26450.903153
},
"diagnostic_read_error": null,
"current_diagnostic": {
"sample_count": 65,
"valid_sample_count": 65,
"max_current_raw": 214,
"max_current_delta_raw": 213,
"high_current_seen": false,
"low_hall_seen": true,
"stall_overlap_seen": false,
"peak": {
"current_raw": 214,
"current_baseline_raw": 1,
"current_delta_raw": 213,
"stall_current_delta_threshold_raw": 500,
"current_sample_valid": true,
"current_baseline_valid": true,
"high_current": false,
"hall_activity_low": true,
"current_calibrating": false,
"frame": {
"frame_id": "0x304",
"payload_size": 8,
"data_hex": "00 0B D6 00 01 00 F4 01",
"timestamp": 26449.53215
}
},
"latest": {
"current_raw": 212,
"current_baseline_raw": 1,
"current_delta_raw": 211,
"stall_current_delta_threshold_raw": 500,
"current_sample_valid": true,
"current_baseline_valid": true,
"high_current": false,
"hall_activity_low": true,
"current_calibrating": false,
"frame": {
"frame_id": "0x304",
"payload_size": 8,
"data_hex": "00 0B D4 00 01 00 F4 01",
"timestamp": 26450.886864
}
}
},
"error_type": "HardwareExecutionError",
"stop_all_attempted": true,
"simulated": false
},
"finished_at": "2026-08-01T14:53:36.269029Z",
"duration_ms": 7042
},
{
"id": "MODE-BABY-NAP",
"group": "mode",
"utterance": "副驾执行宝宝午睡模式",
"status": "safety_blocked",
"started_at": "2026-08-01T14:53:36.269096Z",
"finished_at": "2026-08-01T14:53:36.269098Z",
"duration_ms": 0,
"errors": [],
"safety_block": {
"axis": "cushion_tilt",
"target_pct": 30,
"minimum_safe_target_pct": 35,
"reason": "M1 near-direction mechanical obstruction: repeated Hall stop at 122/123 with zero pulses and high current; do not drive toward the near endpoint until the mechanism is inspected.",
"evidence": {
"hall_count": 122,
"pulse_count": 0,
"peak_current_delta_raw": 1205,
"stall_threshold_raw": 500
}
}
},
{
"id": "MODE-CRUISE-SOFA",
"group": "mode",
"utterance": "副驾执行巡航沙发模式",
"status": "safety_blocked",
"started_at": "2026-08-01T14:53:36.269107Z",
"finished_at": "2026-08-01T14:53:36.269108Z",
"duration_ms": 0,
"errors": [],
"safety_block": {
"axis": "cushion_tilt",
"target_pct": 30,
"minimum_safe_target_pct": 35,
"reason": "M1 near-direction mechanical obstruction: repeated Hall stop at 122/123 with zero pulses and high current; do not drive toward the near endpoint until the mechanism is inspected.",
"evidence": {
"hall_count": 122,
"pulse_count": 0,
"peak_current_delta_raw": 1205,
"stall_threshold_raw": 500
}
}
},
{
"id": "MODE-MEDITATION",
"group": "mode",
"utterance": "副驾执行冥想空间模式",
"started_at": "2026-08-01T14:53:36.269121Z",
"status": "failed",
"errors": [
"T2G motor 1 STALL: task_state=7, Hall=374->374"
],
"http_status": 201,
"request_id": "req-20260801145336-cea6e3",
"failure_telemetry": {
"motor_selector": 1,
"motion_axis": "cushion_tilt",
"task_state": 7,
"motor_result": 5,
"motor_result_name": "STALL",
"position_valid": true,
"hall_before": 374,
"hall_after": 374,
"hall_delta": 0,
"endpoint_recovery_max_near_hall": 56,
"command_frame": {
"frame_id": "0x300",
"payload_size": 8,
"data_hex": "01 11 34 00 30 11 30 75",
"timestamp": null
},
"ack_frame": {
"frame_id": "0x301",
"payload_size": 8,
"data_hex": "21 00 34 00 01 02 F0 49",
"timestamp": 26453.41257
},
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 57 68 34 00 10 27 00",
"timestamp": 26462.801669
},
"diagnostic_before_frame": {
"frame_id": "0x303",
"payload_size": 8,
"data_hex": "01 00 76 01 76 01 00 B0",
"timestamp": 26451.441787
},
"diagnostic_frame": {
"frame_id": "0x303",
"payload_size": 8,
"data_hex": "01 14 76 01 76 01 00 C1",
"timestamp": 26462.802815
},
"diagnostic_read_error": null,
"current_diagnostic": {
"sample_count": 113,
"valid_sample_count": 113,
"max_current_raw": 1353,
"max_current_delta_raw": 1351,
"high_current_seen": true,
"low_hall_seen": true,
"stall_overlap_seen": true,
"peak": {
"current_raw": 1353,
"current_baseline_raw": 2,
"current_delta_raw": 1351,
"stall_current_delta_threshold_raw": 500,
"current_sample_valid": true,
"current_baseline_valid": true,
"high_current": true,
"hall_activity_low": false,
"current_calibrating": false,
"frame": {
"frame_id": "0x304",
"payload_size": 8,
"data_hex": "01 07 49 05 02 00 F4 01",
"timestamp": 26461.7585
}
},
"latest": {
"current_raw": 1199,
"current_baseline_raw": 2,
"current_delta_raw": 1197,
"stall_current_delta_threshold_raw": 500,
"current_sample_valid": true,
"current_baseline_valid": true,
"high_current": true,
"hall_activity_low": true,
"current_calibrating": false,
"frame": {
"frame_id": "0x304",
"payload_size": 8,
"data_hex": "01 0F AF 04 02 00 F4 01",
"timestamp": 26462.798972
}
}
},
"error_type": "HardwareExecutionError",
"stop_all_attempted": true,
"simulated": false
},
"finished_at": "2026-08-01T14:53:48.119388Z",
"duration_ms": 11850
},
{
"id": "MODE-LEISURE",
"group": "mode",
"utterance": "副驾执行休闲躺椅模式",
"status": "safety_blocked",
"started_at": "2026-08-01T14:53:48.119450Z",
"finished_at": "2026-08-01T14:53:48.119452Z",
"duration_ms": 0,
"errors": [],
"safety_block": {
"axis": "cushion_tilt",
"target_pct": 30,
"minimum_safe_target_pct": 35,
"reason": "M1 near-direction mechanical obstruction: repeated Hall stop at 122/123 with zero pulses and high current; do not drive toward the near endpoint until the mechanism is inspected.",
"evidence": {
"hall_count": 122,
"pulse_count": 0,
"peak_current_delta_raw": 1205,
"stall_threshold_raw": 500
}
}
},
{
"id": "MODE-CALF",
"group": "mode",
"utterance": "副驾执行小腿解压悬浮模式",
"started_at": "2026-08-01T14:53:48.119461Z",
"status": "failed",
"errors": [
"T2G motor 0 TIMEOUT: task_state=7, Hall=499->499"
],
"http_status": 201,
"request_id": "req-20260801145348-bae676",
"failure_telemetry": {
"motor_selector": 0,
"motion_axis": "longitudinal",
"task_state": 7,
"motor_result": 6,
"motor_result_name": "TIMEOUT",
"position_valid": true,
"hall_before": 499,
"hall_after": 499,
"hall_delta": 0,
"endpoint_recovery_max_near_hall": 72,
"command_frame": {
"frame_id": "0x300",
"payload_size": 8,
"data_hex": "01 10 37 00 EC 13 30 75",
"timestamp": null
},
"ack_frame": {
"frame_id": "0x301",
"payload_size": 8,
"data_hex": "21 00 37 00 01 01 F0 41",
"timestamp": 26463.239675
},
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 67 28 37 00 10 27 00",
"timestamp": 26469.752618
},
"diagnostic_before_frame": {
"frame_id": "0x303",
"payload_size": 8,
"data_hex": "00 00 F3 01 F3 01 00 10",
"timestamp": 26463.237716
},
"diagnostic_frame": {
"frame_id": "0x303",
"payload_size": 8,
"data_hex": "00 05 F3 01 F3 01 00 20",
"timestamp": 26469.754985
},
"diagnostic_read_error": null,
"current_diagnostic": {
"sample_count": 65,
"valid_sample_count": 65,
"max_current_raw": 253,
"max_current_delta_raw": 250,
"high_current_seen": false,
"low_hall_seen": true,
"stall_overlap_seen": false,
"peak": {
"current_raw": 253,
"current_baseline_raw": 3,
"current_delta_raw": 250,
"stall_current_delta_threshold_raw": 500,
"current_sample_valid": true,
"current_baseline_valid": true,
"high_current": false,
"hall_activity_low": false,
"current_calibrating": false,
"frame": {
"frame_id": "0x304",
"payload_size": 8,
"data_hex": "00 03 FD 00 03 00 F4 01",
"timestamp": 26463.272395
}
},
"latest": {
"current_raw": 207,
"current_baseline_raw": 3,
"current_delta_raw": 204,
"stall_current_delta_threshold_raw": 500,
"current_sample_valid": true,
"current_baseline_valid": true,
"high_current": false,
"hall_activity_low": true,
"current_calibrating": false,
"frame": {
"frame_id": "0x304",
"payload_size": 8,
"data_hex": "00 0B CF 00 03 00 F4 01",
"timestamp": 26469.728393
}
}
},
"error_type": "HardwareExecutionError",
"stop_all_attempted": true,
"simulated": false
},
"finished_at": "2026-08-01T14:53:55.135955Z",
"duration_ms": 7017
},
{
"id": "MODE-BEACH",
"group": "mode",
"utterance": "副驾执行电感沙滩椅模式",
"status": "safety_blocked",
"started_at": "2026-08-01T14:53:55.136004Z",
"finished_at": "2026-08-01T14:53:55.136006Z",
"duration_ms": 0,
"errors": [],
"safety_block": {
"axis": "cushion_tilt",
"target_pct": 0,
"minimum_safe_target_pct": 35,
"reason": "M1 near-direction mechanical obstruction: repeated Hall stop at 122/123 with zero pulses and high current; do not drive toward the near endpoint until the mechanism is inspected.",
"evidence": {
"hall_count": 122,
"pulse_count": 0,
"peak_current_delta_raw": 1205,
"stall_threshold_raw": 500
}
}
},
{
"id": "MODE-NAP",
"group": "mode",
"utterance": "副驾执行小憩模式",
"started_at": "2026-08-01T14:53:55.136014Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801145355-391520",
"matched_modes": [
"nap"
],
"state_http_status": 200,
"physical_state": {
"position": {
"longitudinal": {
"position_pct": 100,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 0,
"value_x100": 10000,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 26 28 3A 00 10 27 00",
"timestamp": 26476.732654
}
},
"cushion_tilt": {
"position_pct": 100,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 1,
"value_x100": 10000,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 26 28 3B 00 10 27 00",
"timestamp": 26476.759174
}
},
"backrest_recline": {
"position_pct": 81,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 2,
"value_x100": 8103,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 26 08 3C 00 A7 1F 00",
"timestamp": 26476.785245
}
},
"legrest_height": {
"position_pct": 94,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 3,
"value_x100": 9397,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 26 08 3D 00 B5 24 00",
"timestamp": 26476.810211
}
}
},
"ventilation": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 26476.820464
}
},
"heating": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 26476.820464
}
},
"captured_at": "2026-08-01T14:54:01.990826Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T14:54:02.155719Z",
"duration_ms": 7020
},
{
"id": "MODE-ZERO-GRAVITY",
"group": "mode",
"utterance": "副驾执行零重力模式",
"started_at": "2026-08-01T14:54:02.155796Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801145402-f6ff8b",
"matched_modes": [
"zero_gravity"
],
"state_http_status": 200,
"physical_state": {
"position": {
"longitudinal": {
"position_pct": 100,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 0,
"value_x100": 10000,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 26 28 3F 00 10 27 00",
"timestamp": 26483.63127
}
},
"cushion_tilt": {
"position_pct": 100,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 1,
"value_x100": 10000,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 26 28 40 00 10 27 00",
"timestamp": 26483.657826
}
},
"backrest_recline": {
"position_pct": 81,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 2,
"value_x100": 8103,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 26 08 41 00 A7 1F 00",
"timestamp": 26483.682735
}
},
"legrest_height": {
"position_pct": 94,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 3,
"value_x100": 9397,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 26 08 42 00 B5 24 00",
"timestamp": 26483.708846
}
}
},
"ventilation": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 26483.717983
}
},
"heating": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 26483.717983
}
},
"captured_at": "2026-08-01T14:54:08.888287Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T14:54:08.963843Z",
"duration_ms": 6808
}
],
"final_state": {
"id": "FINAL-THERMAL-OFF",
"group": "cleanup",
"utterance": "关闭副驾座椅加热和通风",
"started_at": "2026-08-01T14:54:08.963900Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801145408-c833b5",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"legrest_height": {
"position_pct": 94,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 3,
"value_x100": 9397,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 26 08 42 00 B5 24 00",
"timestamp": 26484.485071
}
},
"longitudinal": {
"position_pct": 100,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 0,
"value_x100": 10000,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 26 28 3F 00 10 27 00",
"timestamp": 26484.511148
}
},
"cushion_tilt": {
"position_pct": 100,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 1,
"value_x100": 10000,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 26 28 40 00 10 27 00",
"timestamp": 26484.537248
}
},
"backrest_recline": {
"position_pct": 81,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 2,
"value_x100": 8103,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 26 08 41 00 A7 1F 00",
"timestamp": 26484.562609
}
}
},
"ventilation": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 26484.572862
}
},
"heating": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 26484.572862
}
},
"captured_at": "2026-08-01T14:54:09.742991Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T14:54:09.821001Z",
"duration_ms": 857
},
"finished_at": "2026-08-01T14:54:09.821038Z",
"summary": {
"total": 9,
"passed": 2,
"failed": 3,
"safety_blocked": 4,
"executed": 5,
"final_state_passed": true
}
}
@@ -0,0 +1,240 @@
{
"suite": "t2g-seat-frontend-full-hil",
"started_at": "2026-08-01T14:55:16.839705Z",
"base_url": "http://127.0.0.1:8000",
"vehicle_id": "denza_n7",
"conversation_id": "conv-hil-full-20260801145516",
"runtime": {
"hardware_execution_allowed": true,
"firmware_profiles": [
"combined"
]
},
"results": [
{
"id": "MOTION-M0-HOME",
"group": "motion",
"utterance": "把副驾座椅水平位置归零",
"started_at": "2026-08-01T14:55:16.839746Z",
"status": "failed",
"errors": [
"T2G motor 0 CALIBRATION_FAILED: task_state=7, Hall=499->499"
],
"http_status": 201,
"request_id": "req-20260801145516-496358",
"failure_telemetry": {
"motor_selector": 0,
"motion_axis": "longitudinal",
"task_state": 7,
"motor_result": 11,
"motor_result_name": "CALIBRATION_FAILED",
"position_valid": false,
"hall_before": 499,
"hall_after": 499,
"hall_delta": 0,
"endpoint_recovery_max_near_hall": 72,
"command_frame": {
"frame_id": "0x300",
"payload_size": 8,
"data_hex": "0B 10 44 00 52 4B 78 00",
"timestamp": null
},
"ack_frame": {
"frame_id": "0x301",
"payload_size": 8,
"data_hex": "51 00 44 00 0B 41 F0 4F",
"timestamp": 26552.027376
},
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 B7 00 44 00 FF FF 00",
"timestamp": 26555.266451
},
"diagnostic_before_frame": {
"frame_id": "0x303",
"payload_size": 8,
"data_hex": "00 00 F3 01 F3 01 00 C0",
"timestamp": 26552.016761
},
"diagnostic_frame": {
"frame_id": "0x303",
"payload_size": 8,
"data_hex": "00 88 F3 01 F3 01 00 00",
"timestamp": 26555.267587
},
"diagnostic_read_error": null,
"current_diagnostic": {
"sample_count": 36,
"valid_sample_count": 36,
"max_current_raw": 223,
"max_current_delta_raw": 220,
"high_current_seen": false,
"low_hall_seen": true,
"stall_overlap_seen": false,
"peak": {
"current_raw": 223,
"current_baseline_raw": 3,
"current_delta_raw": 220,
"stall_current_delta_threshold_raw": 500,
"current_sample_valid": true,
"current_baseline_valid": true,
"high_current": false,
"hall_activity_low": false,
"current_calibrating": true,
"frame": {
"frame_id": "0x304",
"payload_size": 8,
"data_hex": "00 13 DF 00 03 00 F4 01",
"timestamp": 26552.116307
}
},
"latest": {
"current_raw": 10,
"current_baseline_raw": 23,
"current_delta_raw": 0,
"stall_current_delta_threshold_raw": 500,
"current_sample_valid": true,
"current_baseline_valid": true,
"high_current": false,
"hall_activity_low": true,
"current_calibrating": false,
"frame": {
"frame_id": "0x304",
"payload_size": 8,
"data_hex": "00 0B 0A 00 17 00 F4 01",
"timestamp": 26555.246382
}
}
},
"error_type": "HardwareExecutionError",
"stop_all_attempted": true,
"simulated": false
},
"finished_at": "2026-08-01T14:55:20.627987Z",
"duration_ms": 3788
}
],
"final_state": {
"id": "FINAL-THERMAL-OFF",
"group": "cleanup",
"utterance": "关闭副驾座椅加热和通风",
"started_at": "2026-08-01T14:55:20.628029Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801145520-65fe81",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"backrest_recline": {
"position_pct": 81,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 3,
"motor_selector": 2,
"value_x100": 8103,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 30 08 45 00 A7 1F 00",
"timestamp": 26556.1968
}
},
"legrest_height": {
"position_pct": 94,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 3,
"motor_selector": 3,
"value_x100": 9397,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 30 08 45 00 B5 24 00",
"timestamp": 26556.222629
}
},
"longitudinal": {
"position_pct": 100,
"position_valid": false,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 3,
"motor_selector": 0,
"value_x100": 65535,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 30 00 45 00 FF FF 00",
"timestamp": 26556.248442
}
},
"cushion_tilt": {
"position_pct": 100,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 3,
"motor_selector": 1,
"value_x100": 10000,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 30 28 45 00 10 27 00",
"timestamp": 26556.274646
}
}
},
"ventilation": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 26556.283681
}
},
"heating": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 26556.283681
}
},
"captured_at": "2026-08-01T14:55:21.452124Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T14:55:21.531805Z",
"duration_ms": 904
},
"finished_at": "2026-08-01T14:55:21.531835Z",
"summary": {
"total": 1,
"passed": 0,
"failed": 1,
"safety_blocked": 0,
"executed": 1,
"final_state_passed": true
}
}
@@ -0,0 +1,62 @@
{
"suite": "t2g-seat-frontend-full-hil",
"started_at": "2026-08-01T15:32:33.035150Z",
"base_url": "http://127.0.0.1:8000",
"vehicle_id": "denza_n7",
"conversation_id": "conv-hil-full-20260801153233",
"runtime": {
"hardware_execution_allowed": true,
"firmware_profiles": [
"combined"
]
},
"results": [
{
"id": "MOTION-M0-HOME",
"group": "motion",
"utterance": "把副驾座椅水平位置归零",
"started_at": "2026-08-01T15:32:33.035187Z",
"status": "failed",
"errors": [
"ZCAN_SetValue(canfd_abit_baud_rate) failed"
],
"http_status": 201,
"request_id": "req-20260801153233-4d8760",
"failure_telemetry": {
"error_type": "CanGatewayError",
"stop_all_attempted": false,
"simulated": false
},
"finished_at": "2026-08-01T15:32:33.233128Z",
"duration_ms": 198
}
],
"final_state": {
"id": "FINAL-THERMAL-OFF",
"group": "cleanup",
"utterance": "关闭副驾座椅加热和通风",
"started_at": "2026-08-01T15:32:33.233166Z",
"status": "failed",
"errors": [
"ZCAN_SetValue(canfd_abit_baud_rate) failed"
],
"http_status": 201,
"request_id": "req-20260801153233-dc02d8",
"failure_telemetry": {
"error_type": "CanGatewayError",
"stop_all_attempted": false,
"simulated": false
},
"finished_at": "2026-08-01T15:32:33.390312Z",
"duration_ms": 157
},
"finished_at": "2026-08-01T15:32:33.390330Z",
"summary": {
"total": 1,
"passed": 0,
"failed": 1,
"safety_blocked": 0,
"executed": 1,
"final_state_passed": false
}
}
@@ -0,0 +1,240 @@
{
"suite": "t2g-seat-frontend-full-hil",
"started_at": "2026-08-01T15:38:36.967401Z",
"base_url": "http://127.0.0.1:8000",
"vehicle_id": "denza_n7",
"conversation_id": "conv-hil-full-20260801153836",
"runtime": {
"hardware_execution_allowed": true,
"firmware_profiles": [
"combined"
]
},
"results": [
{
"id": "MOTION-M0-HOME",
"group": "motion",
"utterance": "把副驾座椅水平位置归零",
"started_at": "2026-08-01T15:38:36.967442Z",
"status": "failed",
"errors": [
"T2G motor 0 CALIBRATION_FAILED: task_state=7, Hall=499->499"
],
"http_status": 201,
"request_id": "req-20260801153836-3cd030",
"failure_telemetry": {
"motor_selector": 0,
"motion_axis": "longitudinal",
"task_state": 7,
"motor_result": 11,
"motor_result_name": "CALIBRATION_FAILED",
"position_valid": false,
"hall_before": 499,
"hall_after": 499,
"hall_delta": 0,
"endpoint_recovery_max_near_hall": 72,
"command_frame": {
"frame_id": "0x300",
"payload_size": 8,
"data_hex": "0B 10 02 00 52 4B 78 00",
"timestamp": null
},
"ack_frame": {
"frame_id": "0x301",
"payload_size": 8,
"data_hex": "51 00 02 00 0B 41 E0 55",
"timestamp": 29165.165292
},
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 B7 00 02 00 FF FF 00",
"timestamp": 29168.401573
},
"diagnostic_before_frame": {
"frame_id": "0x303",
"payload_size": 8,
"data_hex": "00 00 F3 01 F3 01 00 20",
"timestamp": 29165.154003
},
"diagnostic_frame": {
"frame_id": "0x303",
"payload_size": 8,
"data_hex": "00 88 F3 01 F3 01 00 60",
"timestamp": 29168.402701
},
"diagnostic_read_error": null,
"current_diagnostic": {
"sample_count": 36,
"valid_sample_count": 36,
"max_current_raw": 238,
"max_current_delta_raw": 233,
"high_current_seen": false,
"low_hall_seen": true,
"stall_overlap_seen": false,
"peak": {
"current_raw": 236,
"current_baseline_raw": 3,
"current_delta_raw": 233,
"stall_current_delta_threshold_raw": 500,
"current_sample_valid": true,
"current_baseline_valid": true,
"high_current": false,
"hall_activity_low": false,
"current_calibrating": true,
"frame": {
"frame_id": "0x304",
"payload_size": 8,
"data_hex": "00 13 EC 00 03 00 F4 01",
"timestamp": 29165.251397
}
},
"latest": {
"current_raw": 12,
"current_baseline_raw": 36,
"current_delta_raw": 0,
"stall_current_delta_threshold_raw": 500,
"current_sample_valid": true,
"current_baseline_valid": true,
"high_current": false,
"hall_activity_low": true,
"current_calibrating": false,
"frame": {
"frame_id": "0x304",
"payload_size": 8,
"data_hex": "00 0B 0C 00 24 00 F4 01",
"timestamp": 29168.379476
}
}
},
"error_type": "HardwareExecutionError",
"stop_all_attempted": true,
"simulated": false
},
"finished_at": "2026-08-01T15:38:53.697878Z",
"duration_ms": 16730
}
],
"final_state": {
"id": "FINAL-THERMAL-OFF",
"group": "cleanup",
"utterance": "关闭副驾座椅加热和通风",
"started_at": "2026-08-01T15:38:53.697934Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801153853-1d639f",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"cushion_tilt": {
"position_pct": 100,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 3,
"motor_selector": 1,
"value_x100": 10000,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 30 28 03 00 10 27 00",
"timestamp": 29169.295418
}
},
"backrest_recline": {
"position_pct": 81,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 3,
"motor_selector": 2,
"value_x100": 8103,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 30 08 03 00 A7 1F 00",
"timestamp": 29169.322276
}
},
"legrest_height": {
"position_pct": 94,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 3,
"motor_selector": 3,
"value_x100": 9397,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 30 08 03 00 B5 24 00",
"timestamp": 29169.347323
}
},
"longitudinal": {
"position_pct": 100,
"position_valid": false,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 3,
"motor_selector": 0,
"value_x100": 65535,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 30 00 03 00 FF FF 00",
"timestamp": 29169.374233
}
}
},
"ventilation": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 29169.380975
}
},
"heating": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 29169.380975
}
},
"captured_at": "2026-08-01T15:38:54.491848Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T15:38:54.576238Z",
"duration_ms": 878
},
"finished_at": "2026-08-01T15:38:54.576286Z",
"summary": {
"total": 1,
"passed": 0,
"failed": 1,
"safety_blocked": 0,
"executed": 1,
"final_state_passed": true
}
}
@@ -0,0 +1,363 @@
{
"suite": "t2g-seat-frontend-full-hil",
"started_at": "2026-08-01T15:43:36.160681Z",
"base_url": "http://127.0.0.1:8000",
"vehicle_id": "denza_n7",
"conversation_id": "conv-hil-full-20260801154336",
"runtime": {
"hardware_execution_allowed": true,
"firmware_profiles": [
"combined"
]
},
"results": [
{
"id": "THERM-H-OFF",
"group": "thermal",
"utterance": "关闭副驾座椅加热",
"started_at": "2026-08-01T15:43:36.160725Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801154336-0eefe8",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"backrest_recline": {
"position_pct": 81,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 0,
"motor_selector": 2,
"value_x100": 8103,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 00 08 00 00 A7 1F 00",
"timestamp": 29463.233038
}
},
"legrest_height": {
"position_pct": 94,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 0,
"motor_selector": 3,
"value_x100": 9397,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 00 08 00 00 B5 24 00",
"timestamp": 29463.259912
}
},
"longitudinal": {
"position_pct": 100,
"position_valid": false,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 0,
"motor_selector": 0,
"value_x100": 65535,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 00 00 00 00 FF FF 00",
"timestamp": 29463.284973
}
},
"cushion_tilt": {
"position_pct": 100,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 0,
"motor_selector": 1,
"value_x100": 10000,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 00 28 00 00 10 27 00",
"timestamp": 29463.311881
}
}
},
"ventilation": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 29463.319822
}
},
"heating": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 29463.319822
}
},
"captured_at": "2026-08-01T15:43:48.424601Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T15:43:48.512170Z",
"duration_ms": 12351
},
{
"id": "THERM-V-OFF",
"group": "thermal",
"utterance": "关闭副驾座椅通风",
"started_at": "2026-08-01T15:43:48.512241Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801154348-d8018c",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"backrest_recline": {
"position_pct": 81,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 0,
"motor_selector": 2,
"value_x100": 8103,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 00 08 00 00 A7 1F 00",
"timestamp": 29464.079538
}
},
"legrest_height": {
"position_pct": 94,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 0,
"motor_selector": 3,
"value_x100": 9397,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 00 08 00 00 B5 24 00",
"timestamp": 29464.105253
}
},
"longitudinal": {
"position_pct": 100,
"position_valid": false,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 0,
"motor_selector": 0,
"value_x100": 65535,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 00 00 00 00 FF FF 00",
"timestamp": 29464.131102
}
},
"cushion_tilt": {
"position_pct": 100,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 0,
"motor_selector": 1,
"value_x100": 10000,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 00 28 00 00 10 27 00",
"timestamp": 29464.157968
}
}
},
"ventilation": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 29464.165819
}
},
"heating": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 29464.165819
}
},
"captured_at": "2026-08-01T15:43:49.270734Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T15:43:49.336080Z",
"duration_ms": 824
}
],
"final_state": {
"id": "FINAL-THERMAL-OFF",
"group": "cleanup",
"utterance": "关闭副驾座椅加热和通风",
"started_at": "2026-08-01T15:43:49.336131Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801154349-f42e7f",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"cushion_tilt": {
"position_pct": 100,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 0,
"motor_selector": 1,
"value_x100": 10000,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 00 28 00 00 10 27 00",
"timestamp": 29464.904964
}
},
"backrest_recline": {
"position_pct": 81,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 0,
"motor_selector": 2,
"value_x100": 8103,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 00 08 00 00 A7 1F 00",
"timestamp": 29464.931901
}
},
"legrest_height": {
"position_pct": 94,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 0,
"motor_selector": 3,
"value_x100": 9397,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 00 08 00 00 B5 24 00",
"timestamp": 29464.95768
}
},
"longitudinal": {
"position_pct": 100,
"position_valid": false,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 0,
"motor_selector": 0,
"value_x100": 65535,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 00 00 00 00 FF FF 00",
"timestamp": 29464.98386
}
}
},
"ventilation": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 29464.990659
}
},
"heating": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 29464.990659
}
},
"captured_at": "2026-08-01T15:43:50.095435Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T15:43:50.176751Z",
"duration_ms": 841
},
"finished_at": "2026-08-01T15:43:50.176781Z",
"summary": {
"total": 2,
"passed": 2,
"failed": 0,
"safety_blocked": 0,
"executed": 2,
"final_state_passed": true
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,250 @@
{
"suite": "t2g-seat-frontend-full-hil",
"started_at": "2026-08-01T17:39:16.430915Z",
"base_url": "http://127.0.0.1:8000",
"vehicle_id": "denza_n7",
"conversation_id": "conv-hil-full-20260801173916",
"runtime": {
"hardware_execution_allowed": true,
"firmware_profiles": [
"combined"
]
},
"results": [
{
"id": "MOTION-ALL-ZERO",
"group": "motion",
"utterance": "把副驾水平、坐盆、靠背、腿托四轴归零",
"started_at": "2026-08-01T17:39:16.430950Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801173916-90206d",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"cushion_tilt": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 1,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 26 18 6D 01 00 00 00",
"timestamp": 36459.051975
}
},
"backrest_recline": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 2,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 26 18 6E 01 00 00 00",
"timestamp": 36459.077734
}
},
"legrest_height": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 3,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 26 18 6F 01 00 00 00",
"timestamp": 36459.104324
}
},
"longitudinal": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 0,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 26 18 6C 01 00 00 00",
"timestamp": 36459.129781
}
}
},
"ventilation": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 36459.136472
}
},
"heating": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 36459.136472
}
},
"captured_at": "2026-08-01T17:40:24.101369Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T17:40:24.299645Z",
"duration_ms": 67869
}
],
"final_state": {
"id": "FINAL-THERMAL-OFF",
"group": "cleanup",
"utterance": "关闭副驾座椅加热和通风",
"started_at": "2026-08-01T17:40:24.299777Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801174024-2bef93",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"longitudinal": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 0,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 26 18 6C 01 00 00 00",
"timestamp": 36460.001759
}
},
"cushion_tilt": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 1,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 26 18 6D 01 00 00 00",
"timestamp": 36460.028327
}
},
"backrest_recline": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 2,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 26 18 6E 01 00 00 00",
"timestamp": 36460.055218
}
},
"legrest_height": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 3,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 26 18 6F 01 00 00 00",
"timestamp": 36460.08066
}
}
},
"ventilation": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 36460.087354
}
},
"heating": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 36460.087354
}
},
"captured_at": "2026-08-01T17:40:25.052281Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T17:40:25.130245Z",
"duration_ms": 830
},
"finished_at": "2026-08-01T17:40:25.130276Z",
"summary": {
"total": 1,
"passed": 1,
"failed": 0,
"safety_blocked": 0,
"executed": 1,
"final_state_passed": true
}
}
@@ -0,0 +1,823 @@
{
"suite": "t2g-seat-frontend-comprehensive-hil",
"started_at": "2026-08-01T22:06:10.662623Z",
"base_url": "http://127.0.0.1:8000",
"vehicle_id": "denza_n7",
"conversation_id": "conv-hil-full-20260801220610",
"runtime": {
"hardware_execution_allowed": true,
"firmware_profiles": [
"combined"
]
},
"results": [
{
"id": "MOTION-M0-ABS-DECIMAL-ROUND",
"group": "motion",
"utterance": "副驾座椅水平位置调到50.5%",
"started_at": "2026-08-01T22:06:33.819391Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801220633-35a9ee",
"provider_used": "local_rules",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"backrest_recline": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 3,
"motor_selector": 2,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 30 18 0B 00 00 00 00",
"timestamp": 52437.943305
}
},
"legrest_height": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 3,
"motor_selector": 3,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 30 18 0B 00 00 00 00",
"timestamp": 52437.968795
}
},
"longitudinal": {
"position_pct": 51,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 0,
"value_x100": 5110,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 26 08 0C 00 F6 13 00",
"timestamp": 52437.995098
}
},
"cushion_tilt": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 3,
"motor_selector": 1,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 30 18 0B 00 00 00 00",
"timestamp": 52438.021395
}
}
},
"ventilation": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 52438.031724
}
},
"heating": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 52438.031724
}
},
"captured_at": "2026-08-01T22:06:42.683267Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T22:06:42.768028Z",
"duration_ms": 8949
},
{
"id": "MOTION-M0-ABS-NEGATIVE-CLAMP",
"group": "motion",
"utterance": "副驾座椅水平位置调到-10%",
"started_at": "2026-08-01T22:06:42.768083Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801220642-ffc18b",
"provider_used": "local_rules",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"cushion_tilt": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 3,
"motor_selector": 1,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 30 18 0D 00 00 00 00",
"timestamp": 52448.169066
}
},
"backrest_recline": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 3,
"motor_selector": 2,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 30 18 0D 00 00 00 00",
"timestamp": 52448.196
}
},
"legrest_height": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 3,
"motor_selector": 3,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 30 18 0D 00 00 00 00",
"timestamp": 52448.221821
}
},
"longitudinal": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 0,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 26 18 0E 00 00 00 00",
"timestamp": 52448.247656
}
}
},
"ventilation": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 52448.255615
}
},
"heating": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 52448.255615
}
},
"captured_at": "2026-08-01T22:06:52.906777Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T22:06:52.966157Z",
"duration_ms": 10198
},
{
"id": "MOTION-M2-TIMED-ZERO-MIN-CLAMP",
"group": "motion",
"utterance": "副驾靠背向后移动0秒",
"started_at": "2026-08-01T22:06:52.966239Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801220652-ebe458",
"provider_used": "local_rules",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"longitudinal": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 3,
"motor_selector": 0,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 30 18 0F 00 00 00 00",
"timestamp": 52449.295516
}
},
"cushion_tilt": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 3,
"motor_selector": 1,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 30 18 0F 00 00 00 00",
"timestamp": 52449.321809
}
},
"backrest_recline": {
"position_pct": 1,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 2,
"value_x100": 120,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 26 08 10 00 78 00 00",
"timestamp": 52449.347017
}
},
"legrest_height": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 3,
"motor_selector": 3,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 30 18 0F 00 00 00 00",
"timestamp": 52449.37364
}
}
},
"ventilation": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 52449.382777
}
},
"heating": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 52449.382777
}
},
"captured_at": "2026-08-01T22:06:54.033862Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T22:06:54.110745Z",
"duration_ms": 1145
},
{
"id": "MODEL-FUZZY-COMFORT-ONLY",
"group": "model",
"utterance": "把座椅调舒服一点",
"started_at": "2026-08-01T22:06:54.110804Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801220654-5f0af4",
"provider_used": "openai:gpt-5.6-sol",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"legrest_height": {
"position_pct": 10,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 3,
"value_x100": 1005,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 26 08 15 00 ED 03 00",
"timestamp": 52491.95849
}
},
"longitudinal": {
"position_pct": 80,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 0,
"value_x100": 8016,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 26 08 12 00 50 1F 00",
"timestamp": 52491.984306
}
},
"cushion_tilt": {
"position_pct": 30,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 1,
"value_x100": 2995,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 26 08 13 00 B3 0B 00",
"timestamp": 52492.010881
}
},
"backrest_recline": {
"position_pct": 35,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 2,
"value_x100": 3501,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 26 08 14 00 AD 0D 00",
"timestamp": 52492.036223
}
}
},
"ventilation": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 52492.046648
}
},
"heating": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 52492.046648
}
},
"captured_at": "2026-08-01T22:07:36.697054Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T22:07:36.773665Z",
"duration_ms": 42663
},
{
"id": "MODEL-UNKNOWN-BOSS-MODE",
"group": "model",
"utterance": "打开老板模式",
"started_at": "2026-08-01T22:07:36.773730Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801220736-be8392",
"provider_used": "openai:gpt-5.6-sol",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"legrest_height": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 3,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 26 18 1A 00 00 00 00",
"timestamp": 52526.819003
}
},
"longitudinal": {
"position_pct": 20,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 0,
"value_x100": 2004,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 26 08 17 00 D4 07 00",
"timestamp": 52526.845094
}
},
"cushion_tilt": {
"position_pct": 30,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 1,
"value_x100": 2995,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 26 08 18 00 B3 0B 00",
"timestamp": 52526.87121
}
},
"backrest_recline": {
"position_pct": 10,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 2,
"value_x100": 1003,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 26 08 19 00 EB 03 00",
"timestamp": 52526.897575
}
}
},
"ventilation": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 52526.90671
}
},
"heating": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 52526.90671
}
},
"captured_at": "2026-08-01T22:08:11.556208Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T22:08:11.634298Z",
"duration_ms": 34861
}
],
"position_memory_sync": {
"id": "PREFLIGHT-POSITION-MEMORY-SYNC",
"group": "preflight",
"utterance": "关闭副驾座椅加热和通风,并把水平、坐盆、靠背、腿托四轴归零",
"started_at": "2026-08-01T22:06:10.662651Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801220610-fe1cde",
"provider_used": "local_rules",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"legrest_height": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 3,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 26 18 0A 00 00 00 00",
"timestamp": 52428.988603
}
},
"longitudinal": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 0,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 26 18 07 00 00 00 00",
"timestamp": 52429.015565
}
},
"cushion_tilt": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 1,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 26 18 08 00 00 00 00",
"timestamp": 52429.041376
}
},
"backrest_recline": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 2,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 26 18 09 00 00 00 00",
"timestamp": 52429.06754
}
}
},
"ventilation": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 52429.075411
}
},
"heating": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 52429.075411
}
},
"captured_at": "2026-08-01T22:06:33.726921Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T22:06:33.819288Z",
"duration_ms": 23157
},
"final_state": {
"id": "FINAL-THERMAL-OFF",
"group": "cleanup",
"utterance": "关闭副驾座椅加热和通风,并把水平、坐盆、靠背、腿托四轴归零",
"started_at": "2026-08-01T22:08:11.634351Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801220811-9d1330",
"provider_used": "local_rules",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"cushion_tilt": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 1,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 26 18 1D 00 00 00 00",
"timestamp": 52552.285415
}
},
"backrest_recline": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 2,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 26 18 1E 00 00 00 00",
"timestamp": 52552.311577
}
},
"legrest_height": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 3,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 26 18 1F 00 00 00 00",
"timestamp": 52552.337346
}
},
"longitudinal": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 0,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 26 18 1C 00 00 00 00",
"timestamp": 52552.364354
}
}
},
"ventilation": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 52552.372264
}
},
"heating": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 52552.372264
}
},
"captured_at": "2026-08-01T22:08:37.021239Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T22:08:37.100192Z",
"duration_ms": 25466
},
"finished_at": "2026-08-01T22:08:37.100232Z",
"summary": {
"total": 5,
"passed": 5,
"failed": 0,
"safety_blocked": 0,
"executed": 5,
"position_memory_sync_passed": true,
"final_state_passed": true
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,364 @@
{
"suite": "t2g-seat-frontend-comprehensive-hil",
"started_at": "2026-08-01T20:30:03.347380Z",
"base_url": "http://127.0.0.1:8000",
"vehicle_id": "denza_n7",
"conversation_id": "conv-hil-full-20260801203003",
"runtime": {
"hardware_execution_allowed": true,
"firmware_profiles": [
"combined"
]
},
"results": [
{
"id": "MODEL-FUZZY-CRAMPED-COMFORT",
"group": "model",
"utterance": "前舱空间太挤了,把座椅调整到舒服的位置",
"started_at": "2026-08-01T20:30:43.772650Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801203043-6dba03",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"backrest_recline": {
"position_pct": 35,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 2,
"value_x100": 3501,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 26 08 09 00 AD 0D 00",
"timestamp": 46720.941274
}
},
"legrest_height": {
"position_pct": 10,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 3,
"value_x100": 1005,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 26 08 0A 00 ED 03 00",
"timestamp": 46720.967852
}
},
"longitudinal": {
"position_pct": 80,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 0,
"value_x100": 8016,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 26 08 07 00 50 1F 00",
"timestamp": 46720.993263
}
},
"cushion_tilt": {
"position_pct": 30,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 1,
"value_x100": 2995,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 26 08 08 00 B3 0B 00",
"timestamp": 46721.019784
}
}
},
"ventilation": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 46721.030217
}
},
"heating": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 46721.030217
}
},
"captured_at": "2026-08-01T20:31:25.795596Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T20:31:25.863040Z",
"duration_ms": 42090
}
],
"position_memory_sync": {
"id": "PREFLIGHT-POSITION-MEMORY-SYNC",
"group": "preflight",
"utterance": "关闭副驾座椅加热和通风,并把水平、坐盆、靠背、腿托四轴归零",
"started_at": "2026-08-01T20:30:03.347411Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801203003-f00e7a",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"cushion_tilt": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 1,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 26 18 03 00 00 00 00",
"timestamp": 46678.830874
}
},
"backrest_recline": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 2,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 26 18 04 00 00 00 00",
"timestamp": 46678.856952
}
},
"legrest_height": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 3,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 26 18 05 00 00 00 00",
"timestamp": 46678.882795
}
},
"longitudinal": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 0,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 26 18 02 00 00 00 00",
"timestamp": 46678.908649
}
}
},
"ventilation": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 46678.916532
}
},
"heating": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 46678.916532
}
},
"captured_at": "2026-08-01T20:30:43.683001Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T20:30:43.772568Z",
"duration_ms": 40425
},
"final_state": {
"id": "FINAL-THERMAL-OFF",
"group": "cleanup",
"utterance": "关闭副驾座椅加热和通风,并把水平、坐盆、靠背、腿托四轴归零",
"started_at": "2026-08-01T20:31:25.863092Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801203125-a4af68",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"longitudinal": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 0,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 26 18 0C 00 00 00 00",
"timestamp": 46759.956435
}
},
"cushion_tilt": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 1,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 26 18 0D 00 00 00 00",
"timestamp": 46759.982688
}
},
"backrest_recline": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 2,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 26 18 0E 00 00 00 00",
"timestamp": 46760.008938
}
},
"legrest_height": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 3,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 26 18 0F 00 00 00 00",
"timestamp": 46760.041009
}
}
},
"ventilation": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 46760.053878
}
},
"heating": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 46760.053878
}
},
"captured_at": "2026-08-01T20:32:04.818553Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T20:32:04.895597Z",
"duration_ms": 39033
},
"finished_at": "2026-08-01T20:32:04.895635Z",
"summary": {
"total": 1,
"passed": 1,
"failed": 0,
"safety_blocked": 0,
"executed": 1,
"position_memory_sync_passed": true,
"final_state_passed": true
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,250 @@
{
"suite": "t2g-seat-frontend-full-hil",
"started_at": "2026-08-01T17:16:40.264559Z",
"base_url": "http://127.0.0.1:8000",
"vehicle_id": "denza_n7",
"conversation_id": "conv-hil-full-20260801171640",
"runtime": {
"hardware_execution_allowed": true,
"firmware_profiles": [
"combined"
]
},
"results": [
{
"id": "MOTION-ALL-ZERO",
"group": "motion",
"utterance": "把副驾水平、坐盆、靠背、腿托四轴归零",
"started_at": "2026-08-01T17:16:40.264614Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801171640-4a5f1e",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"backrest_recline": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 2,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 26 18 19 01 00 00 00",
"timestamp": 35105.608647
}
},
"legrest_height": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 3,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 26 18 1A 01 00 00 00",
"timestamp": 35105.634423
}
},
"longitudinal": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 0,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 26 18 17 01 00 00 00",
"timestamp": 35105.661016
}
},
"cushion_tilt": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 1,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 26 18 18 01 00 00 00",
"timestamp": 35105.68652
}
}
},
"ventilation": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 35105.692065
}
},
"heating": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 35105.692065
}
},
"captured_at": "2026-08-01T17:17:50.683023Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T17:17:50.762718Z",
"duration_ms": 70498
}
],
"final_state": {
"id": "FINAL-THERMAL-OFF",
"group": "cleanup",
"utterance": "关闭副驾座椅加热和通风",
"started_at": "2026-08-01T17:17:50.762772Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801171750-2f0f2f",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"backrest_recline": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 2,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 26 18 19 01 00 00 00",
"timestamp": 35106.475011
}
},
"legrest_height": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 3,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 26 18 1A 01 00 00 00",
"timestamp": 35106.500476
}
},
"longitudinal": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 0,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 26 18 17 01 00 00 00",
"timestamp": 35106.526322
}
},
"cushion_tilt": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 1,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 26 18 18 01 00 00 00",
"timestamp": 35106.552849
}
}
},
"ventilation": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 35106.558466
}
},
"heating": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 35106.558466
}
},
"captured_at": "2026-08-01T17:17:51.549417Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T17:17:51.636183Z",
"duration_ms": 873
},
"finished_at": "2026-08-01T17:17:51.636217Z",
"summary": {
"total": 1,
"passed": 1,
"failed": 0,
"safety_blocked": 0,
"executed": 1,
"final_state_passed": true
}
}
@@ -0,0 +1,363 @@
{
"suite": "t2g-seat-frontend-full-hil",
"started_at": "2026-08-01T16:40:47.172484Z",
"base_url": "http://127.0.0.1:8000",
"vehicle_id": "denza_n7",
"conversation_id": "conv-hil-full-20260801164047",
"runtime": {
"hardware_execution_allowed": true,
"firmware_profiles": [
"combined"
]
},
"results": [
{
"id": "THERM-H-OFF",
"group": "thermal",
"utterance": "关闭副驾座椅加热",
"started_at": "2026-08-01T16:40:47.172525Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801164047-8cddb4",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"backrest_recline": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 0,
"motor_selector": 2,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 00 18 00 00 00 00 00",
"timestamp": 32897.543083
}
},
"legrest_height": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 0,
"motor_selector": 3,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 00 18 00 00 00 00 00",
"timestamp": 32897.568506
}
},
"longitudinal": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 0,
"motor_selector": 0,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 00 18 00 00 00 00 00",
"timestamp": 32897.595022
}
},
"cushion_tilt": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 0,
"motor_selector": 1,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 00 18 00 00 00 00 00",
"timestamp": 32897.620753
}
}
},
"ventilation": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 32897.62524
}
},
"heating": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 32897.62524
}
},
"captured_at": "2026-08-01T16:41:02.659736Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T16:41:02.738362Z",
"duration_ms": 15566
},
{
"id": "THERM-V-OFF",
"group": "thermal",
"utterance": "关闭副驾座椅通风",
"started_at": "2026-08-01T16:41:02.738421Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801164102-401927",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"backrest_recline": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 0,
"motor_selector": 2,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 00 18 00 00 00 00 00",
"timestamp": 32898.36913
}
},
"legrest_height": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 0,
"motor_selector": 3,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 00 18 00 00 00 00 00",
"timestamp": 32898.394558
}
},
"longitudinal": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 0,
"motor_selector": 0,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 00 18 00 00 00 00 00",
"timestamp": 32898.421367
}
},
"cushion_tilt": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 0,
"motor_selector": 1,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 00 18 00 00 00 00 00",
"timestamp": 32898.446823
}
}
},
"ventilation": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 32898.453497
}
},
"heating": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 32898.453497
}
},
"captured_at": "2026-08-01T16:41:03.487981Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T16:41:03.570272Z",
"duration_ms": 832
}
],
"final_state": {
"id": "FINAL-THERMAL-OFF",
"group": "cleanup",
"utterance": "关闭副驾座椅加热和通风",
"started_at": "2026-08-01T16:41:03.570317Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801164103-4efef7",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"cushion_tilt": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 0,
"motor_selector": 1,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 00 18 00 00 00 00 00",
"timestamp": 32899.183035
}
},
"backrest_recline": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 0,
"motor_selector": 2,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 00 18 00 00 00 00 00",
"timestamp": 32899.209498
}
},
"legrest_height": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 0,
"motor_selector": 3,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 00 18 00 00 00 00 00",
"timestamp": 32899.23527
}
},
"longitudinal": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 0,
"motor_selector": 0,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 00 18 00 00 00 00 00",
"timestamp": 32899.260679
}
}
},
"ventilation": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 32899.267388
}
},
"heating": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 32899.267388
}
},
"captured_at": "2026-08-01T16:41:04.301854Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T16:41:04.424088Z",
"duration_ms": 854
},
"finished_at": "2026-08-01T16:41:04.424118Z",
"summary": {
"total": 2,
"passed": 2,
"failed": 0,
"safety_blocked": 0,
"executed": 2,
"final_state_passed": true
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,250 @@
{
"suite": "t2g-seat-frontend-full-hil",
"started_at": "2026-08-01T17:32:24.627065Z",
"base_url": "http://127.0.0.1:8000",
"vehicle_id": "denza_n7",
"conversation_id": "conv-hil-full-20260801173224",
"runtime": {
"hardware_execution_allowed": true,
"firmware_profiles": [
"combined"
]
},
"results": [
{
"id": "MOTION-ALL-ZERO",
"group": "motion",
"utterance": "把副驾水平、坐盆、靠背、腿托四轴归零",
"started_at": "2026-08-01T17:32:24.627106Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801173224-cedf76",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"longitudinal": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 0,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 26 18 30 01 00 00 00",
"timestamp": 36047.100786
}
},
"cushion_tilt": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 1,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 26 18 31 01 00 00 00",
"timestamp": 36047.126273
}
},
"backrest_recline": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 2,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 26 18 32 01 00 00 00",
"timestamp": 36047.152829
}
},
"legrest_height": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 3,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 26 18 33 01 00 00 00",
"timestamp": 36047.178304
}
}
},
"ventilation": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 36047.185019
}
},
"heating": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 36047.185019
}
},
"captured_at": "2026-08-01T17:33:32.157834Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T17:33:32.231664Z",
"duration_ms": 67605
}
],
"final_state": {
"id": "FINAL-THERMAL-OFF",
"group": "cleanup",
"utterance": "关闭副驾座椅加热和通风",
"started_at": "2026-08-01T17:33:32.231720Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801173332-3d22e2",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"longitudinal": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 0,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 26 18 30 01 00 00 00",
"timestamp": 36047.928676
}
},
"cushion_tilt": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 1,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 26 18 31 01 00 00 00",
"timestamp": 36047.954191
}
},
"backrest_recline": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 2,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 26 18 32 01 00 00 00",
"timestamp": 36047.980699
}
},
"legrest_height": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 3,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 26 18 33 01 00 00 00",
"timestamp": 36048.006522
}
}
},
"ventilation": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 36048.013163
}
},
"heating": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 36048.013163
}
},
"captured_at": "2026-08-01T17:33:32.986103Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T17:33:33.067889Z",
"duration_ms": 836
},
"finished_at": "2026-08-01T17:33:33.067920Z",
"summary": {
"total": 1,
"passed": 1,
"failed": 0,
"safety_blocked": 0,
"executed": 1,
"final_state_passed": true
}
}
@@ -0,0 +1,250 @@
{
"suite": "t2g-seat-frontend-full-hil",
"started_at": "2026-08-01T17:26:35.994926Z",
"base_url": "http://127.0.0.1:8000",
"vehicle_id": "denza_n7",
"conversation_id": "conv-hil-full-20260801172635",
"runtime": {
"hardware_execution_allowed": true,
"firmware_profiles": [
"combined"
]
},
"results": [
{
"id": "MOTION-ALL-ZERO",
"group": "motion",
"utterance": "把副驾水平、坐盆、靠背、腿托四轴归零",
"started_at": "2026-08-01T17:26:35.994963Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801172635-cf368d",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"backrest_recline": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 2,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 26 18 23 01 00 00 00",
"timestamp": 35698.280073
}
},
"legrest_height": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 3,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 26 18 24 01 00 00 00",
"timestamp": 35698.306668
}
},
"longitudinal": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 0,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 26 18 21 01 00 00 00",
"timestamp": 35698.332116
}
},
"cushion_tilt": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 1,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 26 18 22 01 00 00 00",
"timestamp": 35698.357889
}
}
},
"ventilation": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 35698.36569
}
},
"heating": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 35698.36569
}
},
"captured_at": "2026-08-01T17:27:43.345246Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T17:27:43.418173Z",
"duration_ms": 67423
}
],
"final_state": {
"id": "FINAL-THERMAL-OFF",
"group": "cleanup",
"utterance": "关闭副驾座椅加热和通风",
"started_at": "2026-08-01T17:27:43.418231Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801172743-1f95ae",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"backrest_recline": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 2,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 26 18 23 01 00 00 00",
"timestamp": 35699.098847
}
},
"legrest_height": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 3,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 26 18 24 01 00 00 00",
"timestamp": 35699.124304
}
},
"longitudinal": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 0,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 26 18 21 01 00 00 00",
"timestamp": 35699.150901
}
},
"cushion_tilt": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 1,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 26 18 22 01 00 00 00",
"timestamp": 35699.176715
}
}
},
"ventilation": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 35699.184476
}
},
"heating": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 35699.184476
}
},
"captured_at": "2026-08-01T17:27:44.164137Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T17:27:44.276096Z",
"duration_ms": 858
},
"finished_at": "2026-08-01T17:27:44.276129Z",
"summary": {
"total": 1,
"passed": 1,
"failed": 0,
"safety_blocked": 0,
"executed": 1,
"final_state_passed": true
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,252 @@
{
"suite": "t2g-seat-frontend-full-hil",
"started_at": "2026-08-01T17:32:02.775803Z",
"base_url": "http://127.0.0.1:8000",
"vehicle_id": "denza_n7",
"conversation_id": "conv-hil-full-20260801173202",
"runtime": {
"hardware_execution_allowed": true,
"firmware_profiles": [
"combined"
]
},
"results": [
{
"id": "MODE-ZERO-GRAVITY",
"group": "mode",
"utterance": "打开零重力模式",
"started_at": "2026-08-01T17:32:02.775839Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801173202-68d1da",
"matched_modes": [
"zero_gravity"
],
"state_http_status": 200,
"physical_state": {
"position": {
"cushion_tilt": {
"position_pct": 100,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 1,
"value_x100": 10000,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 26 28 2C 01 10 27 00",
"timestamp": 35964.438236
}
},
"backrest_recline": {
"position_pct": 81,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 2,
"value_x100": 8103,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 26 08 2D 01 A7 1F 00",
"timestamp": 35964.464297
}
},
"legrest_height": {
"position_pct": 94,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 3,
"value_x100": 9397,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 26 08 2E 01 B5 24 00",
"timestamp": 35964.490843
}
},
"longitudinal": {
"position_pct": 100,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 0,
"value_x100": 10000,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 26 28 2B 01 10 27 00",
"timestamp": 35964.51698
}
}
},
"ventilation": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 35964.526089
}
},
"heating": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 35964.526089
}
},
"captured_at": "2026-08-01T17:32:09.500643Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T17:32:09.582995Z",
"duration_ms": 6807
}
],
"final_state": {
"id": "FINAL-THERMAL-OFF",
"group": "cleanup",
"utterance": "关闭副驾座椅加热和通风",
"started_at": "2026-08-01T17:32:09.583051Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801173209-29898b",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"longitudinal": {
"position_pct": 100,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 0,
"value_x100": 10000,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 26 28 2B 01 10 27 00",
"timestamp": 35965.246437
}
},
"cushion_tilt": {
"position_pct": 100,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 1,
"value_x100": 10000,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 26 28 2C 01 10 27 00",
"timestamp": 35965.27405
}
},
"backrest_recline": {
"position_pct": 81,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 2,
"value_x100": 8103,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 26 08 2D 01 A7 1F 00",
"timestamp": 35965.3002
}
},
"legrest_height": {
"position_pct": 94,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 3,
"value_x100": 9397,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 26 08 2E 01 B5 24 00",
"timestamp": 35965.326337
}
}
},
"ventilation": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 35965.334306
}
},
"heating": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 35965.334306
}
},
"captured_at": "2026-08-01T17:32:10.308851Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T17:32:10.379863Z",
"duration_ms": 797
},
"finished_at": "2026-08-01T17:32:10.379894Z",
"summary": {
"total": 1,
"passed": 1,
"failed": 0,
"safety_blocked": 0,
"executed": 1,
"final_state_passed": true
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,825 @@
{
"suite": "t2g-seat-frontend-comprehensive-hil",
"started_at": "2026-08-07T18:17:38.577477Z",
"base_url": "http://127.0.0.1:8003",
"vehicle_id": "denza_n7",
"conversation_id": "conv-hil-full-20260807181738",
"runtime": {
"hardware_execution_allowed": true,
"firmware_profiles": [
"combined"
]
},
"results": [
{
"id": "MODE-ROCKING-HORSE-START",
"group": "mode",
"utterance": "开启木马模式",
"started_at": "2026-08-07T18:18:01.681906Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260807181801-e5096b",
"provider_used": "local_rules",
"matched_modes": [
"rocking_horse"
],
"state_http_status": 200,
"physical_state": {
"position": {
"backrest_recline": {
"position_pct": 2,
"position_valid": true,
"motion_state": "moving",
"fault": false,
"task_state": 2,
"motor_result": 1,
"motor_selector": 2,
"value_x100": 153,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 12 0D 35 00 99 00 5A",
"timestamp": 106973.993523,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"legrest_height": {
"position_pct": 2,
"position_valid": true,
"motion_state": "moving",
"fault": false,
"task_state": 2,
"motor_result": 1,
"motor_selector": 3,
"value_x100": 216,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 12 0D 35 00 D8 00 5A",
"timestamp": 106974.0196,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"longitudinal": {
"position_pct": 2,
"position_valid": true,
"motion_state": "moving",
"fault": false,
"task_state": 2,
"motor_result": 1,
"motor_selector": 0,
"value_x100": 240,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 12 0D 35 00 F0 00 5A",
"timestamp": 106974.045704,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"cushion_tilt": {
"position_pct": 6,
"position_valid": true,
"motion_state": "moving",
"fault": false,
"task_state": 2,
"motor_result": 1,
"motor_selector": 1,
"value_x100": 588,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 12 0D 35 00 4C 02 5A",
"timestamp": 106974.072431,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
}
},
"ventilation": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 106974.092381,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"heating": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 106974.092381,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"captured_at": "2026-08-07T18:18:02.485490Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-07T18:18:02.550409Z",
"duration_ms": 869
},
{
"id": "MODE-ROCKING-HORSE-STOP",
"group": "mode",
"utterance": "关闭木马模式",
"started_at": "2026-08-07T18:18:02.552655Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260807181802-7609ee",
"provider_used": "local_rules",
"matched_modes": [
"rocking_horse"
],
"state_http_status": 200,
"physical_state": {
"position": {
"backrest_recline": {
"position_pct": 3,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 3,
"motor_selector": 2,
"value_x100": 338,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 30 08 35 00 52 01 00",
"timestamp": 106974.915491,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"legrest_height": {
"position_pct": 5,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 3,
"motor_selector": 3,
"value_x100": 541,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 30 08 35 00 1D 02 00",
"timestamp": 106974.942088,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"longitudinal": {
"position_pct": 5,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 3,
"motor_selector": 0,
"value_x100": 541,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 30 08 35 00 1D 02 00",
"timestamp": 106974.968094,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"cushion_tilt": {
"position_pct": 11,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 3,
"motor_selector": 1,
"value_x100": 1123,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 30 08 35 00 63 04 00",
"timestamp": 106974.993545,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
}
},
"ventilation": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 106975.005142,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"heating": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 106975.005142,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"captured_at": "2026-08-07T18:18:03.397863Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-07T18:18:03.474083Z",
"duration_ms": 921
},
{
"id": "MODE-DYNAMIC-CRADLE-START",
"group": "mode",
"utterance": "开启动态摇篮模式",
"started_at": "2026-08-07T18:18:03.517741Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260807181803-e97616",
"provider_used": "local_rules",
"matched_modes": [
"dynamic_cradle"
],
"state_http_status": 200,
"physical_state": {
"position": {
"legrest_height": {
"position_pct": 8,
"position_valid": true,
"motion_state": "moving",
"fault": false,
"task_state": 2,
"motor_result": 1,
"motor_selector": 3,
"value_x100": 804,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 12 0D 38 00 24 03 5A",
"timestamp": 106975.856713,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"longitudinal": {
"position_pct": 8,
"position_valid": true,
"motion_state": "moving",
"fault": false,
"task_state": 2,
"motor_result": 1,
"motor_selector": 0,
"value_x100": 802,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 12 0D 38 00 22 03 5A",
"timestamp": 106975.881732,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"cushion_tilt": {
"position_pct": 17,
"position_valid": true,
"motion_state": "moving",
"fault": false,
"task_state": 2,
"motor_result": 1,
"motor_selector": 1,
"value_x100": 1684,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 12 0D 38 00 94 06 5A",
"timestamp": 106975.907916,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"backrest_recline": {
"position_pct": 6,
"position_valid": true,
"motion_state": "moving",
"fault": false,
"task_state": 2,
"motor_result": 1,
"motor_selector": 2,
"value_x100": 556,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 12 0D 38 00 2C 02 5A",
"timestamp": 106975.934169,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
}
},
"ventilation": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 106975.955173,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"heating": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 106975.955173,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"captured_at": "2026-08-07T18:18:04.348455Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-07T18:18:04.413787Z",
"duration_ms": 896
},
{
"id": "MODE-DYNAMIC-CRADLE-STOP",
"group": "mode",
"utterance": "关闭动态摇篮模式",
"started_at": "2026-08-07T18:18:04.416034Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260807181804-bcf153",
"provider_used": "local_rules",
"matched_modes": [
"dynamic_cradle"
],
"state_http_status": 200,
"physical_state": {
"position": {
"longitudinal": {
"position_pct": 11,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 3,
"motor_selector": 0,
"value_x100": 1122,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 30 08 38 00 62 04 00",
"timestamp": 106976.740494,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"cushion_tilt": {
"position_pct": 22,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 3,
"motor_selector": 1,
"value_x100": 2246,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 30 08 38 00 C6 08 00",
"timestamp": 106976.76544,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"backrest_recline": {
"position_pct": 7,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 3,
"motor_selector": 2,
"value_x100": 742,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 30 08 38 00 E6 02 00",
"timestamp": 106976.792049,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"legrest_height": {
"position_pct": 11,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 3,
"motor_selector": 3,
"value_x100": 1144,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 30 08 38 00 78 04 00",
"timestamp": 106976.817508,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
}
},
"ventilation": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 106976.829112,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"heating": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 106976.829112,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"captured_at": "2026-08-07T18:18:05.221617Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-07T18:18:05.302650Z",
"duration_ms": 887
}
],
"position_memory_sync": {
"id": "PREFLIGHT-POSITION-MEMORY-SYNC",
"group": "preflight",
"utterance": "关闭副驾座椅加热和通风,并把水平、坐盆、靠背、腿托四轴归零",
"started_at": "2026-08-07T18:17:38.579391Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260807181738-c1fa94",
"provider_used": "local_rules",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"backrest_recline": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 2,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 26 18 31 00 00 00 00",
"timestamp": 106973.120321,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"legrest_height": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 3,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 26 18 32 00 00 00 00",
"timestamp": 106973.146175,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"longitudinal": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 0,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 26 18 2F 00 00 00 00",
"timestamp": 106973.171397,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"cushion_tilt": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 1,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 26 18 30 00 00 00 00",
"timestamp": 106973.198366,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
}
},
"ventilation": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 106973.204067,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"heating": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 106973.204067,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"captured_at": "2026-08-07T18:18:01.596265Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-07T18:18:01.678120Z",
"duration_ms": 23099
},
"final_state": {
"id": "FINAL-THERMAL-OFF",
"group": "cleanup",
"utterance": "关闭副驾座椅加热和通风,并把水平、坐盆、靠背、腿托四轴归零",
"started_at": "2026-08-07T18:18:05.303817Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260807181805-8f6d46",
"provider_used": "local_rules",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"backrest_recline": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 2,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 26 18 3C 00 00 00 00",
"timestamp": 106996.774961,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"legrest_height": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 3,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 26 18 3D 00 00 00 00",
"timestamp": 106996.800164,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"longitudinal": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 0,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 26 18 3A 00 00 00 00",
"timestamp": 106996.826407,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"cushion_tilt": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 1,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 26 18 3B 00 00 00 00",
"timestamp": 106996.852753,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
}
},
"ventilation": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 106996.861946,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"heating": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 106996.861946,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"captured_at": "2026-08-07T18:18:25.255987Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-07T18:18:25.336490Z",
"duration_ms": 20033
},
"finished_at": "2026-08-07T18:18:25.336534Z",
"summary": {
"total": 4,
"passed": 4,
"failed": 0,
"safety_blocked": 0,
"executed": 4,
"position_memory_sync_passed": true,
"final_state_passed": true
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,174 @@
{
"suite": "t2g-seat-frontend-comprehensive-hil",
"started_at": "2026-08-09T14:41:51.089381Z",
"base_url": "http://127.0.0.1:8003",
"vehicle_id": "denza_n7",
"conversation_id": "conv-hil-full-20260809144151",
"runtime": {
"hardware_execution_allowed": true,
"firmware_profiles": [
"combined"
]
},
"results": [
{
"id": "MOTION-M3-20-LEGREST",
"group": "motion",
"utterance": "副驾腿托调到20%",
"started_at": "2026-08-09T14:41:52.650112Z",
"status": "failed",
"errors": [
"M3运动反馈未达到目标"
],
"http_status": 201,
"request_id": "req-20260809144152-1c844d",
"provider_used": "local_rules",
"failure_telemetry": {
"transport": "zcanpro_shared_bridge",
"hardware_feedback": false,
"simulated": false,
"bridge_result": {
"details": {
"command_id": "t2g_move_passenger_legrest_height_20pct",
"stop_all_attempted": true,
"summary": "M3运动反馈未达到目标",
"target": 2000,
"terminal": null,
"tolerance": 100
},
"log_dir": "D:\\huang.xuewei\\Git\\Cognitive_Agent\\artifacts\\self-repair\\hardware\\20260809_224152_db5620",
"report_path": "D:\\huang.xuewei\\Git\\Cognitive_Agent\\artifacts\\self-repair\\hardware\\20260809_224152_db5620\\report.json",
"request_id": "SEAT_ORCH_1786286512679_db5620",
"result": "FAIL",
"summary": "M3运动反馈未达到目标"
},
"error_type": null,
"stop_all_attempted": true
},
"finished_at": "2026-08-09T14:41:54.700435Z",
"duration_ms": 2050
}
],
"position_memory_sync": {
"id": "PREFLIGHT-POSITION-MEMORY-SYNC",
"group": "preflight",
"utterance": "关闭副驾座椅加热和通风,并把水平、坐盆、靠背、腿托四轴归零",
"started_at": "2026-08-09T14:41:51.089407Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260809144151-bb6620",
"provider_used": "local_rules",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"longitudinal": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
},
"cushion_tilt": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
},
"backrest_recline": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
},
"legrest_height": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
}
},
"ventilation": {
"state": "off",
"level": 0
},
"heating": {
"state": "off",
"level": 0
},
"captured_at": "2026-08-09T14:41:52.648514Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-09T14:41:52.650041Z",
"duration_ms": 1561
},
"final_state": {
"id": "FINAL-THERMAL-OFF",
"group": "cleanup",
"utterance": "关闭副驾座椅加热和通风,并把水平、坐盆、靠背、腿托四轴归零",
"started_at": "2026-08-09T14:41:54.700479Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260809144154-f4ad43",
"provider_used": "local_rules",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"longitudinal": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
},
"cushion_tilt": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
},
"backrest_recline": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
},
"legrest_height": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
}
},
"ventilation": {
"state": "off",
"level": 0
},
"heating": {
"state": "off",
"level": 0
},
"captured_at": "2026-08-09T14:41:59.460250Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-09T14:41:59.460811Z",
"duration_ms": 4760
},
"finished_at": "2026-08-09T14:41:59.460848Z",
"summary": {
"total": 1,
"passed": 0,
"failed": 1,
"safety_blocked": 0,
"executed": 1,
"position_memory_sync_passed": true,
"final_state_passed": true
}
}
@@ -0,0 +1,358 @@
{
"suite": "t2g-seat-frontend-comprehensive-hil",
"started_at": "2026-08-09T10:04:15.082291Z",
"base_url": "http://127.0.0.1:8003",
"vehicle_id": "denza_n7",
"conversation_id": "conv-hil-full-20260809100415",
"runtime": {
"hardware_execution_allowed": true,
"firmware_profiles": [
"combined"
]
},
"results": [
{
"id": "MOTION-M0-25-HORIZONTAL",
"group": "motion",
"utterance": "把副驾座椅水平位置调到25%",
"started_at": "2026-08-09T10:04:16.311200Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260809100416-4da1f3",
"provider_used": "local_rules",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"longitudinal": {
"position_pct": 25,
"position_valid": true,
"motion_state": "stopped"
},
"cushion_tilt": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
},
"backrest_recline": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
},
"legrest_height": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
}
},
"ventilation": {
"state": "off",
"level": 0
},
"heating": {
"state": "off",
"level": 0
},
"captured_at": "2026-08-09T10:04:21.364491Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-09T10:04:21.365063Z",
"duration_ms": 5054
},
{
"id": "MOTION-M1-20-PAN",
"group": "motion",
"utterance": "把副驾坐盆调到20%",
"started_at": "2026-08-09T10:04:21.365115Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260809100421-b0b99e",
"provider_used": "local_rules",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"longitudinal": {
"position_pct": 25,
"position_valid": true,
"motion_state": "stopped"
},
"cushion_tilt": {
"position_pct": 20,
"position_valid": true,
"motion_state": "stopped"
},
"backrest_recline": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
},
"legrest_height": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
}
},
"ventilation": {
"state": "off",
"level": 0
},
"heating": {
"state": "off",
"level": 0
},
"captured_at": "2026-08-09T10:04:24.207471Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-09T10:04:24.208128Z",
"duration_ms": 2843
},
{
"id": "COMBINED-ALL",
"group": "combined",
"utterance": "打开副驾加热1档、通风1档,座椅水平到30%,坐盆到40%,靠背到37%,腿托到42%",
"started_at": "2026-08-09T10:04:24.208183Z",
"status": "failed",
"errors": [
"M3运动反馈未达到目标"
],
"http_status": 201,
"request_id": "req-20260809100424-27194b",
"provider_used": "local_rules",
"failure_telemetry": {
"transport": "zcanpro_shared_bridge",
"hardware_feedback": false,
"simulated": false,
"bridge_result": {
"details": {
"command_id": "t2g_move_passenger_legrest_height_42pct",
"stop_all_attempted": true,
"summary": "M3运动反馈未达到目标",
"target": 4200,
"terminal": {
"active": false,
"direction": 0,
"driver_fault": false,
"duty": 0,
"far": false,
"motor": 3,
"near": false,
"position": 2349,
"position_valid": true,
"result": 6,
"sequence": 50339,
"stall": false,
"task": 7
},
"tolerance": 100
},
"log_dir": "D:\\huang.xuewei\\Git\\Cognitive_Agent\\artifacts\\self-repair\\hardware\\20260809_180424_546250",
"report_path": "D:\\huang.xuewei\\Git\\Cognitive_Agent\\artifacts\\self-repair\\hardware\\20260809_180424_546250\\report.json",
"request_id": "SEAT_ORCH_1786269864212_546250",
"result": "FAIL",
"summary": "M3运动反馈未达到目标"
},
"error_type": null,
"stop_all_attempted": true
},
"finished_at": "2026-08-09T10:04:47.676002Z",
"duration_ms": 23468
},
{
"id": "MODE-EASY-OPEN",
"group": "mode",
"utterance": "打开轻松进出模式",
"started_at": "2026-08-09T10:04:47.676048Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260809100447-c855cc",
"provider_used": "local_rules",
"matched_modes": [
"easy_entry_exit"
],
"state_http_status": 200,
"physical_state": {
"position": {
"longitudinal": {
"position_pct": 100,
"position_valid": true,
"motion_state": "stopped"
},
"cushion_tilt": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
},
"backrest_recline": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
},
"legrest_height": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
}
},
"ventilation": {
"state": "on",
"level": 1
},
"heating": {
"state": "on",
"level": 1
},
"captured_at": "2026-08-09T10:05:18.547335Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-09T10:05:18.548156Z",
"duration_ms": 30872
}
],
"position_memory_sync": {
"id": "PREFLIGHT-POSITION-MEMORY-SYNC",
"group": "preflight",
"utterance": "关闭副驾座椅加热和通风,并把水平、坐盆、靠背、腿托四轴归零",
"started_at": "2026-08-09T10:04:15.082316Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260809100415-146708",
"provider_used": "local_rules",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"longitudinal": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
},
"cushion_tilt": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
},
"backrest_recline": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
},
"legrest_height": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
}
},
"ventilation": {
"state": "off",
"level": 0
},
"heating": {
"state": "off",
"level": 0
},
"captured_at": "2026-08-09T10:04:16.310470Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-09T10:04:16.311124Z",
"duration_ms": 1229
},
"final_state": {
"id": "FINAL-THERMAL-OFF",
"group": "cleanup",
"utterance": "关闭副驾座椅加热和通风,并把水平、坐盆、靠背、腿托四轴归零",
"started_at": "2026-08-09T10:05:18.548210Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260809100518-e21b3f",
"provider_used": "local_rules",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"longitudinal": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
},
"cushion_tilt": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
},
"backrest_recline": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
},
"legrest_height": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
}
},
"ventilation": {
"state": "off",
"level": 0
},
"heating": {
"state": "off",
"level": 0
},
"captured_at": "2026-08-09T10:05:36.941483Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-09T10:05:36.942155Z",
"duration_ms": 18394
},
"finished_at": "2026-08-09T10:05:36.942191Z",
"summary": {
"total": 4,
"passed": 3,
"failed": 1,
"safety_blocked": 0,
"executed": 4,
"position_memory_sync_passed": true,
"final_state_passed": true
}
}

Some files were not shown because too many files have changed in this diff Show More