67 Commits

Author SHA1 Message Date
Miayui 448c8b31a0 feat: improve mode and peripheral orchestration 2026-09-21 00:13:47 +08:00
Miayui 89707a42e2 feat: make mode creation preview-only and cache-safe 2026-09-20 19:32:03 +08:00
Miayui bfff1b515c feat: integrate learned peripherals into mode creation
Expose safe NFC-bound infrared actions to semantic scene composition, require flashing-light actions for romantic and Mid-Autumn modes, and preserve mixed CAN/IR mode flows.

Fix NFC device reassignment and retry extension/toy MQTT publication when an active card's binding catalog changes.
2026-09-20 16:37:19 +08:00
Miayui 904f5f8a6c docs: streamline project README 2026-09-18 14:48:07 +08:00
Miayui 24aa1371dc fix: optimize firmware notification action UI 2026-09-18 13:22:05 +08:00
Miayui 0ab1d76781 feat: notify vehicle of published firmware 2026-09-18 12:52:36 +08:00
Miayui e920a92505 Fix workbench dark theme and enlarge dialogs 2026-09-18 09:11:59 +08:00
Miayui c812a1a595 Merge origin/feature/N7Web into feature/N7Web
Integrate the remote firmware workbench, OTA delivery, and timed mode schedules while preserving the local N7 scene and runtime changes.
2026-09-17 12:46:51 +08:00
Miayui 7d196db02b feat(n7web): sync N7 scene cards and optimize runtime 2026-09-17 11:04:05 +08:00
vee ab8e43050c feat: add seat firmware workbench and OTA delivery 2026-09-16 17:55:30 +08:00
Miayui 75ce391312 feat(n7web): refine spatial UI and NFC-gated extension routing 2026-09-16 16:14:10 +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
9266 changed files with 3080508 additions and 7222 deletions
+12
View File
@@ -0,0 +1,12 @@
.git
.venv
.env
.env.auth
__pycache__
*.py[cod]
artifacts
output
tmp
tests
firmware
docs/product
+37
View File
@@ -4,4 +4,41 @@
__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/Passenger_Seat_Timed_Controller_demo/IAR/settings/
firmware/t2g/Passenger_Seat_Timed_Controller_demo/IAR/Passenger_Seat_Timed_Controller_demo.eww
firmware/t2g/Passenger_Seat_Timed_Controller_demo/IAR/rev_d/.Passenger_Seat_Timed_Controller_demo_build_cache
firmware/t2g/Passenger_Seat_Timed_Controller_demo/IAR/rev_d/.ninja_deps
firmware/t2g/Passenger_Seat_Timed_Controller_demo/IAR/rev_d/.ninja_log
firmware/t2g/Passenger_Seat_Timed_Controller_demo/IAR/rev_d/BrowseInfo/
firmware/t2g/Passenger_Seat_Timed_Controller_demo/IAR/rev_d/List/
firmware/t2g/Passenger_Seat_Timed_Controller_demo/IAR/rev_d/Obj/
firmware/t2g/tviibh8m/src/examples/crypto/ClientRunsOnCM7_0/RSA/key_data/private_key*.pem
artifacts/logs/
+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
+249 -99
View File
@@ -1,137 +1,287 @@
# Cognitive Agent
面向车载执行器的认知编排 Agent 单仓工程。系统把自然语言请求转换为可校验的语义配置,再由确定性的领域插件编译为 CAN 命令计划,最后经安全策略和执行器下发。
面向车载执行器的认知编排单仓工程。系统把自然语言或结构化请求转换为可校验的语义配置,由领域插件确定性编译为 `CommandPlan`,经过平台安全门禁后以 dry-run、模拟台架或真实 CAN 方式执行,并保存可审计结果。
启动前端:PYTHONPATH=src:domains/seat/src .omx/python311-venv/bin/python -m cognitive_agent.server
当前仓库以腾势 N7 / T2G 座舱演示和硬件台架为主。默认环境不会打开真实硬件;气候、天幕和氛围灯领域的矩阵目前也只允许虚拟执行。
## 建设阶段
## 能做什么
- 阶段一:每个领域独立完成能力定义、功能配置生成、配置校验、CAN 编译和单执行器测试。
- 阶段二:由中央编排器生成 `SceneConfig`,引用多份 `FunctionConfig`,按依赖关系、时间偏移和失败策略组合执行。
- 最终形态:Web 控制台接收一句话请求,展示结构化决策摘要、配置文件、校验结果和执行状态。
- 同源 Python HTTP 服务与 Vue 3 + Vite Web 控制台。
- 能力目录驱动的自然语言编排、模式卡片、跨域场景和 NDJSON 助手流。
- `IntentIR`、`FunctionConfig`、`SceneConfig`、`CommandPlan`、`TraceEvent` 和 `ExecutionReport` 公共契约。
- 座椅、空调、天幕、氛围灯四个领域插件,以及平台侧门窗、插座、阅读灯等原生 CAN 能力。
- 虚拟 CAN、模拟硬件、T2G ZLG CAN 台架和双通道车身 CAN 接入。
- 语音输入、MediaPipe 手势、NFC/红外外设、模式目录同步和只读固件 OTA 服务。
## 核心约束
AI 模型只能提出受契约约束的语义计划,不能生成或发送原始 CAN 帧。真实执行仍由服务端的能力白名单、参数 Schema、硬件授权、安全策略、互斥和反馈检查共同决定。
1. LLM 只能生成 `IntentIR`、`FunctionConfig` 或 `SceneConfig`,不能直接生成或发送原始 CAN 帧。
2. `FunctionConfig` 只允许包含一个 `domain`;跨域需求必须使用 `SceneConfig` 引用多份领域配置。
3. CAN 编译必须是确定性的:相同配置、相同平台标定和相同插件版本必须生成相同 `CommandPlan`。
4. DBC、通道定义和安全标定由集成人员在 `platforms/` 统一维护;领域成员只维护语义信号映射。
5. 所有真实执行必须经过公共 Schema 校验、领域校验、安全策略、dry-run 和审计记录。
6. UI 展示可审计的决策摘要和阶段事件,不展示模型私有思维链。
## 目录
## 处理链路
```text
.
├── apps/ # API 服务、Web 控制台和内置手势模型资源
├── artifacts/ # 每次请求的生成物和执行记录(不入库)
├── config/ # 全局环境和策略配置
├── contracts/ # 冻结的公共 Schema、示例和版本规则
├── deploy/ # 容器及部署资源
├── docs/ # 架构、流程、治理和 ADR
├── firmware/t2g/ # 仓库内统一座椅 IAR 工程与可烧录产物
├── domains/ # 可独立开发、测试、交付的领域插件
│ ├── _template/ # 新领域模板
│ ├── seat/ # 座椅
│ ├── climate/ # 空调
│ ├── ambient_light/ # 氛围灯
│ └── roof/ # 天幕/遮阳帘/调光玻璃
├── platforms/ # 车型、DBC、总线和安全标定
├── src/cognitive_agent/ # 中央 Agent、编排器、运行时和基础设施
├── tests/ # 跨域契约、集成和端到端测试
└── tools/ # 脚手架、校验和开发工具
```
完整依赖关系和处理链路见 [总体架构](docs/architecture/overview.md),成员交付步骤见 [领域交付流程](docs/workflows/domain-delivery.md),修改边界见 [所有权规则](docs/governance/ownership.md)。
整体排期和阶段准入条件见 [集成路线](docs/workflows/integration-roadmap.md)。
## 配置处理链路
```text
用户文本
-> GPT + Capability Registry + 当前会话状态
-> 通用能力请求(领域、能力、作用域、操作和参数)
-> IntentIR(意图抽取、领域识别、歧义与置信度)
-> 单领域:FunctionConfig
-> 多领域:SceneConfig + N 个 FunctionConfig
-> 公共 Schema 校验 + 领域语义校验
用户请求
-> 能力目录 / 会话状态 / Codex 或本地解析
-> IntentIR
-> FunctionConfig(单领域)或 SceneConfig(跨领域)
-> 公共 Schema + 领域语义校验
-> 领域 Compiler -> CommandPlan
-> 平台安全校验 -> 调度 -> CAN Gateway
-> 回读/超时/结果 -> TraceEvent + ExecutionReport
-> 平台安全校验 -> dry-run / simulated_hardware / physical CAN
-> 回读、超时和审计 -> TraceEvent + ExecutionReport
```
运行时产物统一保存在:
运行时产物位于 `artifacts/runs/<request_id>/`,典型结构如下:
```text
artifacts/runs/<request_id>/
├── intent.yaml
├── scene.yaml # 仅跨域请求存在
├── functions/<domain>.yaml
├── command-plans/<domain>.yaml
├── trace.jsonl
└── execution-report.yaml
intent.yaml
scene.yaml # 跨域请求才有
functions/<domain>.yaml
command-plans/<domain>.yaml
trace.jsonl
execution-report.yaml
```
`artifacts/` 中禁止写入 API Key、完整隐私文本或未脱敏的车辆标识。
不要在 `artifacts/` 写入 API Key、完整隐私文本或未脱敏车辆标识。
## 可执行领域
## 当前能力边界
当前已接入四个相互隔离的可执行领域:
| 能力面 | 当前状态 | 事实来源 |
| --- | --- | --- |
| `seat` | 虚拟矩阵闭环;T2G 台架使用独立硬件映射和校准 | [`domains/seat/`](domains/seat/)、[台架流程](docs/workflows/t2g-seat-hardware-bench.md) |
| `climate` | dry-run;支持分区温度、电源、风量、A/C、循环、风向和前除霜 | [`domains/climate/`](domains/climate/) |
| `roof` | dry-run;支持透光率、后遮阳帘位置和自动调光 | [`domains/roof/`](domains/roof/) |
| `ambient_light` | dry-run;支持开关、RGB、亮度和矩阵场景模式 | [`domains/ambient_light/`](domains/ambient_light/) |
| 原生车身 CAN | 按平台和运行时能力目录启用,不等同于领域插件 | [`config/native_can_capabilities/`](config/native_can_capabilities/)、[`platforms/`](platforms/) |
| 模式卡片 | `config/scene_templates/` 提供内置模板;用户模式持久化到 `artifacts/mode-profiles/` | [模式时间轴](docs/mode-schedules.md) |
- 座椅:基于 `座椅部分功能CAN矩阵表.xlsx` 的主/副驾通风和加热,以及明确标识为 synthetic 的五轴位置电机演示。
- 空调:基于 `空调-天幕-氛围灯CAN信号矩阵表.xlsx` 的电源、18–32℃ 分区温度、0–7 档风量、A/C、内外循环、风向和前除霜。
- 天幕:基于同一矩阵的 0–100% 透光率、后遮阳帘绝对位置和自动调光。
- 氛围灯:基于同一矩阵的总开关、24-bit RGB、0–100% 亮度和 ECU 场景模式。
`platforms/reference_vehicle` 的 `execution_enabled` 为 `false`。`platforms/t2g_seat_bench` 和 `platforms/t2g_cabin_bench` 才是仓库内允许配置真实执行的台架平台,但仍需硬件开关、授权令牌、总线配置和反馈校验全部满足。
一句话只涉及一个领域时直接生成一份 `FunctionConfig`;涉及多个领域时生成一份 `SceneConfig` 引用各领域独立的 `FunctionConfig`,并由各自编译器产生 `CommandPlan`。三份新增矩阵仍缺少车型适用性、完整计数器、校验和和发送周期,因此与座椅演示一样只允许虚拟执行。
## 快速开始
### 环境要求
- Python 3.11 或更高版本。
- Node.js 与 npm(构建 Web 控制台)。
- Codex CLI `0.150.0-alpha.8`,用于内置 AI 助手。
- 真实台架另需 ZLG 驱动/桥接程序、对应 DBC、IAR 工具链和硬件授权。
### 安装(PowerShell)
```powershell
py -3.11 -m venv .venv
.\.venv\Scripts\python.exe -m pip install --upgrade pip
.\.venv\Scripts\python.exe -m pip install -e ".[test,dev,api,can,llm,ir]"
cd apps\web-console
npm ci
npm run build
cd ..\..
npm install --global @openai/codex@0.150.0-alpha.8
codex login status
```
语音识别是可选安装项:
```powershell
.\.venv\Scripts\python.exe -m pip install -e ".[speech]"
.\.venv\Scripts\python.exe tools\install_sensevoice_model.py
```
配置模型认证时使用 `OPENAI_API_KEY`,或让 Codex CLI 使用已登录账户。`.env` 和 `.env.auth` 已被 Git 忽略;可参照 [`config/environments/`](config/environments/) 中的示例配置。不要把密钥提交到仓库。
### 启动完整本地演示
推荐使用启动器。它会管理主服务、只读模式卡片服务和只读 OTA 服务:
```powershell
.\.venv\Scripts\python.exe tools\start_n7web.py --skip-web-build
```
默认端口:
| 服务 | 地址 | 说明 |
| --- | --- | --- |
| 主服务和 Web 控制台 | `http://127.0.0.1:8000` | API、控制台、助手和车控入口 |
| 模式卡片目录 | `http://127.0.0.1:8090/n7/scenes` | HTTP/UDP 只读目录服务 |
| 固件 OTA | `http://127.0.0.1:8012` | 只读发布清单和下载 |
启动前端构建:
```powershell
.\.venv\Scripts\python.exe tools\start_n7web.py
```
启动器支持 `--host`、`--port`、`--scene-port`、`--ota-port`、`--catalog`、`--server-id`、`--python` 和 `--dry-run`。使用 `--host 0.0.0.0` 前请确认网络可信,并为公网部署启用认证和 HTTPS。
健康检查:
```powershell
Invoke-RestMethod http://127.0.0.1:8000/api/health
Invoke-RestMethod http://127.0.0.1:8000/api/assistant/status
```
`/api/health` 依赖 Codex App Server 就绪;助手进程不可用时返回 HTTP `503`,不会伪装成可用服务。
### 只启动主服务
需要自行运行前端开发服务器或不需要模式目录/OTA 服务时,可直接运行:
```powershell
$env:PYTHONPATH = "src;domains/seat/src"
.\.venv\Scripts\python.exe -m cognitive_agent.server
```
前端热更新:
```powershell
cd apps\web-console
npm run dev
```
Vite 使用 `http://127.0.0.1:5173`,并把 `/api` 与 `/artifacts` 代理到 `http://127.0.0.1:8000`。生产构建输出为 `apps/web-console/dist`,主服务检测到该目录后优先提供构建产物。
在已安装 GNU Make 的环境中,`make dev` 等价于启动主服务;Windows PowerShell 推荐使用上面的显式 Python 命令,以确保使用仓库的 `.venv`。
## 开发与验证
先激活 `.venv`,或把下面的 `python` 替换为 `.\.venv\Scripts\python.exe`。仓库的 `Makefile` 使用 POSIX 风格环境变量,在 Windows 上也可以直接调用 Python 脚本。
```bash
# 公共 Schema、领域清单、平台资源和生成 CAN 协议
make validate
# 全量 Python、前端契约和 CAN 协议回归
make test
make dev
# 仅前端运行时契约
make test-frontend
# 生成或检查共享 CAN 语义协议
make can-protocol-generate
make can-protocol-check
```
浏览器访问 `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 编译和虚拟执行均为同一确定性路径。
不使用 `make` 时:
### 一句话、快捷模式与手势控制
首页左侧提供一句话控制,可同时描述座椅、空调、天幕和氛围灯目标;Enter 直接执行,Shift+Enter 换行。观影、午休和零重力是可直接调用的快捷模式,“保存当前”可把当前副驾驶姿态保存为命名模式。复杂跨域自然语言优先使用 GPT 解析,本地规则用于已明确覆盖的离线指令。
顶部“手势控制”打开同页抽屉。浏览器在本机完成 MediaPipe 推理,识别结果仍通过同一自然语言编排、安全校验与执行链路处理。运行必需资源已经纳入本工程:
```text
apps/web-console/assets/gesture/gesture_recognizer.task
config/gesture/default.yaml
```powershell
.\.venv\Scripts\python.exe tools\validate_repository.py
.\.venv\Scripts\python.exe -m pytest
```
因此源码打包和容器构建不依赖相邻的 `seat_gesture_control` 目录。模型文件只保留一份,不包含原工程的测试视频、日志或验证输出。浏览器摄像头要求安全来源,本地使用 `http://127.0.0.1:8000`,远程部署使用 HTTPS;首次使用仍需浏览器授予摄像头权限。MediaPipe Web 运行时从 jsDelivr 按需加载,视频帧不上传到后端。
仓库配置了 `pytest` 的 `contract`、`integration` 和 `hardware` markers。默认测试使用虚拟或模拟执行;真实台架测试必须按 [T2G 前端测试集](docs/testing/t2g-seat-frontend-test-suite.md) 的顺序执行,并显式确认硬件。
手势安全状态机采用张掌保持 1 秒激活、动作保持 600 ms 且至少 8 帧确认、单次运动 1.5 秒上限、连续 3 秒无有效动作自动锁止。换动作不需要重新张掌;握拳立即停止当前动作但保持激活;多手画面不执行动作。手势映射可在抽屉内自定义并保存在当前浏览器。
同一局域网的其他电脑需要体验时,使用 `make dev-lan` 监听全部网络接口。服务启动日志会打印实际 LAN 地址,例如 `http://172.20.10.5:8000`。前端和 API 使用同源访问,不需要 CORS,也不会把 `OPENAI_API_KEY` 发送到浏览器。若没有启用 `.env.auth`,任何可达设备都能触发服务端 GPT 请求,因此未认证的 LAN 模式只应在可信网络内临时演示。
## 认证与公网体验
公网演示必须启用 `.env.auth` 中的服务端认证。密码使用 PBKDF2-SHA256 哈希保存;认证成功后签发有有效期的 HMAC 会话 Cookie,Cookie 使用 `HttpOnly` 和 `SameSite=Strict`,经 HTTPS 代理时自动增加 `Secure`。登录失败按来源 IP 限流,GPT 请求按体验账号限制每分钟调用次数。未登录访问页面会跳转 `/login`,未登录 API 请求返回 `401`。
生成认证材料:
构建双通道 CAN 读取工具需要 CMake 和外部 CANFD SDK:
```bash
PYTHONPATH=src python -m cognitive_agent.auth hash-password
PYTHONPATH=src python -m cognitive_agent.auth generate-secret
make can-read-trans
make can-read-trans-test
```
固定公网演示部署在 `https://511.kendeji.fun`。服务器使用 Docker Compose
运行应用,容器端口只绑定 `127.0.0.1:8511`,由 Caddy 终止 HTTPS 并反向
代理。部署结构、更新和运维命令见
[`deploy/server/README.md`](deploy/server/README.md)。本地 `make dev` 与
`make dev-lan` 仍分别用于回环地址和可信局域网开发。
## API 入口
当前 dry-run 规划策略允许 GPT 在没有同名 preset 时,将可理解的模式目标分解为已注册原子能力并生成保守候选参数。候选值只用于模型理解演示,不能替代实车模式标定。
浏览器只访问同源服务。下面是主要集成面,完整路由以 [`src/cognitive_agent/server.py`](src/cognitive_agent/server.py) 为准:
靠背/坐垫与坐垫/腿托支持演示级精确夹角,并可将当前五轴姿态保存为会话持久化的命名 `FunctionPreset`。保存操作不发 CAN;再次调用时先展开为五个原子电机动作,再进入同一编译、校验和虚拟执行链路。
| 方法 | 路径 | 用途 |
| --- | --- | --- |
| `GET` | `/api/health` | 服务与助手健康状态 |
| `GET` | `/api/runtime` | 当前运行时、平台和执行模式摘要 |
| `GET` | `/api/assistant/status` | Codex App Server 状态 |
| `GET` | `/api/assistant/capabilities` | 权威能力目录快照 |
| `POST` | `/api/requests` | 提交自然语言或结构化车控请求 |
| `POST` | `/api/requests/preview` | 只规划/校验,不执行 |
| `POST` | `/api/assistant/stream` | AI 助手 NDJSON 流 |
| `GET` | `/api/hardware/seat-state` | 座椅状态快照 |
| `GET` | `/api/hardware/vehicle-state` | 车身状态快照 |
| `GET` | `/api/vehicle-display-sync` | 网页与车机任务同步 |
| `GET` | `/n7/scenes` | 只读模式目录 |
完整能力边界、矩阵解释和验收用例见 [座椅阶段一交付](docs/workflows/seat-stage1-delivery.md)。
`/api/internal/codex/vehicle-control` 仅供本机 Codex MCP 回调使用,需要一次性令牌和原始请求绑定,不是浏览器或第三方 API。语音、手势、NFC、固件工作台和自修复接口也由服务端权限门禁保护。
已验证的 T2G 加热、通风和四电机能力已合并到仓库内 `firmware/t2g/Seat_Controller_demo`,并提供一条默认锁定的真实 HIL 路径。主驾与副驾目前映射到同一物理座椅;硬件请求会先做固件身份握手,可在明确启用后通过 IAR C-SPY 自动烧录,再发送 CAN 并等待真实回读。固件拓扑、IAR 构建、ZLG 32 位 bridge、CAN 接线、安全门禁和上电步骤见 [T2G 座椅真实硬件台架接入](docs/workflows/t2g-seat-hardware-bench.md)。
## 配置与安全
默认 [`config/default.yaml`](config/default.yaml) 使用 `dry_run`、结构化输出和 `artifacts/runs`。真实硬件必须同时满足:
1. 平台 manifest 的 `execution_enabled: true`。
2. 对应 CAN 后端、通道、DBC 和校准可用。
3. 服务端硬件开关、授权令牌和安全策略通过校验。
4. 领域映射明确允许硬件执行,并收到真实反馈。
模拟台架显式使用:
```powershell
$env:COGNITIVE_AGENT_SIMULATED_HARDWARE_ENABLED = "true"
```
模拟结果会标记为 `simulated_hardware`,不会打开真实 CAN、ZLG 或 IAR。真实 T2G 座椅需要参考 [`config/environments/t2g_seat_bench.example.yaml`](config/environments/t2g_seat_bench.example.yaml) 和 [硬件台架流程](docs/workflows/t2g-seat-hardware-bench.md);不要仅通过前端按钮声称已经获得硬件权限。
语音和摄像头只在安全来源工作:本机使用 `http://127.0.0.1:8000`,远程部署必须使用 HTTPS。局域网开发命令如下:
```bash
make dev-lan
```
该模式会监听全部网络接口,任何可达设备都可能触发服务端请求,只适合可信网络临时演示。
## 代码结构
```text
apps/web-console/ Vue 入口、兼容运行层、静态资源和 PWA
src/cognitive_agent/ HTTP 服务、应用编排、运行时、基础设施和助手适配
domains/*/ 领域插件:能力、Schema、映射、编译器和测试
platforms/*/ 车型、DBC、总线和校准;定义执行边界
contracts/schemas/ 冻结的公共 JSON Schema
config/ 环境、策略、车辆、模式和原生能力配置
tests/ 跨域契约、集成、端到端和前端测试
tools/ 启动器、仓库校验、协议生成、HIL 和诊断工具
firmware/t2g/ T2G 固件工程和可烧录依赖
deploy/ Docker Compose、服务器和 Kubernetes 占位配置
docs/ 架构、决策、治理、工作流和测试说明
artifacts/ 本地运行产物、模型、HIL 报告和持久化数据
```
依赖边界:领域插件只依赖 SDK 和公共契约,不直接调用 LLM、HTTP 或 CAN;编排层只组合语义配置,不按 CAN signal 编排;DBC 和安全标定由 `platforms/` 统一维护。详见 [总体架构](docs/architecture/overview.md)、[领域交付流程](docs/workflows/domain-delivery.md) 和 [所有权规则](docs/governance/ownership.md)。
## 可选能力
### 语音
安装 `.[speech]` 并运行 `tools/install_sensevoice_model.py` 后,控制台支持手动录音和唤醒词待机。预览和最终识别接口为 `/api/speech/preview` 与 `/api/speech/transcribe`;录音不持久化。调试使用 `SPEECH_DEBUG=1`,默认不向浏览器返回 trace。
### 手势
手势模型随仓库提供:`apps/web-console/assets/gesture/gesture_recognizer.task`。推理在浏览器完成,视频帧不上传;需要摄像头权限和安全来源。
### NFC / 红外外设
安装 `.[ir]` 后运行主服务。NFC、GSCU1B TCP 网关、红外槽位目录和 MQTT 回退由 `/api/extension/*` 管理;学习、绑定和发射均受服务端能力目录与持久 NFC 绑定约束。
### Android PAD
```powershell
powershell -ExecutionPolicy Bypass -File .\tools\android_pad_dev.ps1
powershell -ExecutionPolicy Bypass -File .\tools\android_pad_dev.ps1 -Stop
```
脚本使用模拟台架,不打开真实硬件执行。
## 部署
服务器 Docker Compose 和 Caddy 反向代理说明见 [`deploy/server/README.md`](deploy/server/README.md)。镜像默认以非 root 用户、只读根文件系统运行,应用只绑定服务器回环地址,再由 Caddy 提供 HTTPS。部署时通过私密环境文件注入认证材料和模型凭据,不把 `.env`、`.env.auth`、Codex `auth.json` 或 API Key 放入镜像和版本库。
`deploy/docker/` 记录后续拆分 API/worker/Web 的边界;`deploy/k8s/` 目前是 Kubernetes 资源占位文档。真实 CAN 设备只能挂载到明确授权的执行器环境。
## 相关文档
- [总体架构](docs/architecture/overview.md)
- [T2G 座椅与车身双通道 CAN](docs/workflows/t2g-cabin-dual-channel-can.md)
- [T2G 座椅真实硬件台架](docs/workflows/t2g-seat-hardware-bench.md)
- [T2G 座椅前端测试集](docs/testing/t2g-seat-frontend-test-suite.md)
- [中文语音控制链路](docs/speech-recognition.md)
- [模式卡片与时间轴](docs/mode-schedules.md)
- [领域交付流程](docs/workflows/domain-delivery.md)
- [所有权与变更边界](docs/governance/ownership.md)
- [服务器部署](deploy/server/README.md)
- [Web 控制台细节](apps/web-console/README.md)
## 项目状态
这是面向演示、集成和受控台架验证的工程,不是未经适配即可部署到任意车型的通用车控产品。提交修改前至少运行 `validate` 和相关领域测试;涉及 DBC、平台标定、硬件执行或公共 Schema 的变更,需要同时更新对应专题文档与回归用例。
+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`。
+6552 -540
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: 16px;
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(1480px, 100%);
height: min(900px, 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;
}
}
+908
View File
@@ -0,0 +1,908 @@
(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({ render: monitor.open }),
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());
}());
@@ -0,0 +1,75 @@
.console-v2 .firmware-workbench-layer {
position: fixed;
z-index: 5000;
inset: 0;
display: grid;
place-items: center;
padding: 16px;
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 .firmware-workbench-layer[hidden] { display: none !important; }
.console-v2 .firmware-workbench-page {
width: min(1480px, 100%);
height: min(900px, 100%);
min-height: 0;
display: grid;
grid-template-rows: 76px minmax(0, 1fr);
overflow: hidden;
color: var(--foreground);
background: 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);
animation: firmware-workbench-enter 180ms ease-out both;
}
.console-v2 .firmware-workbench-header {
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 .firmware-workbench-heading { min-width: 0; display: flex; align-items: center; gap: 12px; }
.console-v2 .firmware-workbench-heading > span {
width: 46px;
height: 46px;
flex: 0 0 46px;
display: grid;
place-items: 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;
}
.console-v2 .firmware-workbench-heading svg { width: 24px; height: 24px; stroke-width: 1.8; }
.console-v2 .firmware-workbench-heading h2 { margin: 0; color: var(--heading-foreground, var(--foreground)); font-size: 20px; line-height: 1.2; letter-spacing: -.025em; }
.console-v2 #firmwareWorkbenchClose {
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;
cursor: pointer;
}
.console-v2 #firmwareWorkbenchClose:hover,
.console-v2 #firmwareWorkbenchClose:focus-visible { color: var(--primary); background: var(--accent); outline: 3px solid color-mix(in oklch, var(--ring) 16%, transparent); }
.console-v2 #firmwareWorkbenchClose svg { width: 18px; height: 18px; }
.console-v2 .firmware-workbench-frame { width: 100%; height: 100%; display: block; border: 0; background: transparent; }
@keyframes firmware-workbench-enter { from { opacity: 0; transform: translateY(8px) scale(.992); } to { opacity: 1; transform: none; } }
@media (max-width: 760px) {
.console-v2 .firmware-workbench-layer { padding: 54px 8px 8px; }
.console-v2 .firmware-workbench-page { height: 100%; border-radius: 17px; grid-template-rows: 64px minmax(0, 1fr); }
.console-v2 .firmware-workbench-header { padding: 9px 11px; }
.console-v2 .firmware-workbench-heading > span { width: 40px; height: 40px; flex-basis: 40px; border-radius: 12px; }
.console-v2 .firmware-workbench-heading h2 { font-size: 17px; }
}
@@ -0,0 +1,127 @@
(() => {
"use strict";
const state = { open: false, returnFocus: null, backgroundInert: [] };
function currentWorkbenchTheme() {
return document.documentElement.dataset.glassTheme === "light" ? "light" : "dark";
}
function syncWorkbenchTheme(frame) {
frame?.contentWindow?.postMessage({
type: "cognitive-agent:theme-change",
theme: currentWorkbenchTheme(),
}, window.location.origin);
}
function setBackgroundInert(inert) {
const layer = document.querySelector("#firmwareWorkbenchLayer");
if (!layer) return;
if (inert) {
state.backgroundInert = [...document.body.children]
.filter((element) => element !== layer && !["SCRIPT", "STYLE", "LINK"].includes(element.tagName))
.map((element) => ({ element, wasInert: element.hasAttribute("inert") }));
state.backgroundInert.forEach(({ element }) => element.toggleAttribute("inert", true));
return;
}
state.backgroundInert.forEach(({ element, wasInert }) => element.toggleAttribute("inert", wasInert));
state.backgroundInert = [];
}
function setOpen(open) {
const next = Boolean(open);
const layer = document.querySelector("#firmwareWorkbenchLayer");
const trigger = document.querySelector("#firmwareWorkbenchToggle");
if (!layer || !trigger || state.open === next) return;
state.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) {
state.returnFocus = document.activeElement;
layer.hidden = false;
layer.inert = false;
layer.dataset.open = "true";
setBackgroundInert(true);
requestAnimationFrame(() => document.querySelector("#firmwareWorkbenchClose")?.focus());
} else {
layer.dataset.open = "false";
layer.inert = true;
layer.hidden = true;
setBackgroundInert(false);
const target = state.returnFocus instanceof HTMLElement && state.returnFocus.isConnected
? state.returnFocus
: trigger;
state.returnFocus = null;
target.focus();
}
}
function mount() {
const dock = document.querySelector("#v2PanelControlDock");
if (!dock || document.querySelector("#firmwareWorkbenchToggle")) return false;
const trigger = document.createElement("button");
trigger.id = "firmwareWorkbenchToggle";
trigger.className = "firmware-workbench-toggle v2-panel-control-button";
trigger.type = "button";
trigger.setAttribute("aria-controls", "firmwareWorkbenchLayer");
trigger.setAttribute("aria-pressed", "false");
trigger.setAttribute("aria-expanded", "false");
trigger.setAttribute("aria-label", "打开座椅固件开发工作台");
trigger.title = "打开座椅固件开发工作台";
trigger.innerHTML = '<i data-lucide="blocks"></i><span class="visually-hidden">座椅固件开发工作台</span>';
const layer = document.createElement("div");
layer.id = "firmwareWorkbenchLayer";
layer.className = "firmware-workbench-layer";
layer.hidden = true;
layer.inert = true;
layer.setAttribute("role", "dialog");
layer.setAttribute("aria-modal", "true");
layer.setAttribute("aria-labelledby", "firmwareWorkbenchTitle");
layer.innerHTML = `
<section class="firmware-workbench-page">
<header class="firmware-workbench-header">
<div class="firmware-workbench-heading">
<span><i data-lucide="blocks"></i></span>
<h2 id="firmwareWorkbenchTitle">座椅固件开发工作台</h2>
</div>
<button id="firmwareWorkbenchClose" type="button" aria-label="关闭座椅固件开发工作台" title="关闭"><i data-lucide="x"></i></button>
</header>
<iframe class="firmware-workbench-frame" title="座椅固件开发工作台"></iframe>
</section>`;
dock.append(trigger);
document.body.append(layer);
const frame = document.querySelector(".firmware-workbench-frame", layer);
frame.src = `/firmware-workbench.html?embedded=1&theme=${currentWorkbenchTheme()}`;
frame.addEventListener("load", () => syncWorkbenchTheme(frame));
window.addEventListener("cognitive-agent:theme-change", () => syncWorkbenchTheme(frame));
new MutationObserver(() => syncWorkbenchTheme(frame)).observe(document.documentElement, {
attributes: true,
attributeFilter: ["data-glass-theme"],
});
trigger.addEventListener("click", () => setOpen(!state.open));
document.querySelector("#firmwareWorkbenchClose", layer).addEventListener("click", () => setOpen(false));
layer.addEventListener("click", (event) => { if (event.target === layer) setOpen(false); });
document.addEventListener("keydown", (event) => {
if (state.open && event.key === "Escape") {
event.preventDefault();
setOpen(false);
}
});
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
return true;
}
if (!mount()) {
const observer = new MutationObserver(() => {
if (mount()) observer.disconnect();
});
observer.observe(document.body, { childList: true, subtree: true });
window.setTimeout(() => observer.disconnect(), 15000);
}
})();
@@ -0,0 +1,65 @@
(() => {
"use strict";
const STORAGE_KEY = "cognitive-agent.glass-theme";
function normalizeTheme(value) {
return value === "light" ? "light" : value === "dark" ? "dark" : "";
}
function themeFromCookie() {
try {
return normalizeTheme(document.cookie
.split(";")
.map((part) => part.trim())
.find((part) => part.startsWith(`${STORAGE_KEY}=`))
?.slice(STORAGE_KEY.length + 1));
} catch (_error) {
return "";
}
}
function resolveInitialTheme() {
const requested = normalizeTheme(new URLSearchParams(window.location.search).get("theme"));
if (requested) return requested;
try {
const parentTheme = normalizeTheme(window.parent?.document?.documentElement?.dataset?.glassTheme);
if (parentTheme) return parentTheme;
} catch (_error) {
// Standalone and cross-origin rendering continue with local preferences.
}
try {
const stored = normalizeTheme(localStorage.getItem(STORAGE_KEY));
if (stored) return stored;
} catch (_error) {
// Storage may be unavailable in hardened browser contexts.
}
return themeFromCookie() || "dark";
}
function applyTheme(theme) {
const normalized = normalizeTheme(theme) || "dark";
document.documentElement.dataset.glassTheme = normalized;
document.documentElement.style.colorScheme = normalized;
document.querySelector("#firmwareWorkbenchThemeColor")
?.setAttribute("content", normalized === "dark" ? "#0f1526" : "#f3f6fb");
return normalized;
}
applyTheme(resolveInitialTheme());
window.addEventListener("storage", (event) => {
if (event.key === STORAGE_KEY) applyTheme(event.newValue);
});
window.addEventListener("message", (event) => {
if (event.origin !== window.location.origin) return;
if (window.parent !== window && event.source !== window.parent) return;
if (event.data?.type !== "cognitive-agent:theme-change") return;
applyTheme(event.data.theme);
});
window.CognitiveAgentFirmwareWorkbenchTheme = Object.freeze({
apply: applyTheme,
current: () => document.documentElement.dataset.glassTheme,
});
})();
+626
View File
@@ -0,0 +1,626 @@
:root {
color-scheme: light;
font-family: "Microsoft YaHei UI", "PingFang SC", Inter, system-ui, sans-serif;
--page: #eef3f9;
--surface: #ffffff;
--surface-soft: #f7f9fc;
--line: #dce4ee;
--line-strong: #cbd7e5;
--text: #172033;
--muted: #68758b;
--primary: #2869e8;
--primary-soft: #eaf1ff;
--success: #16845b;
--success-soft: #e7f7f0;
--warning: #ba7314;
--danger: #c23f4a;
--shadow: 0 22px 60px rgba(43, 67, 101, .14), 0 4px 14px rgba(43, 67, 101, .08);
}
* { box-sizing: border-box; }
html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; }
body {
display: grid;
place-items: center;
padding: 16px;
color: var(--text);
font-size: 14px;
line-height: 1.45;
background:
radial-gradient(circle at 15% 8%, rgba(74, 130, 235, .11), transparent 31%),
linear-gradient(145deg, #f7f9fc 0%, var(--page) 100%);
}
button, textarea, select { font: inherit; }
button, a, select { -webkit-tap-highlight-color: transparent; }
button, select { cursor: pointer; }
.shell {
width: min(1280px, 100%);
height: min(780px, calc(100dvh - 32px));
min-height: 620px;
overflow: hidden;
background: #f7f9fc;
border: 1px solid rgba(205, 216, 230, .9);
border-radius: 22px;
box-shadow: var(--shadow);
}
.topbar {
height: 68px;
padding: 0 22px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
background: rgba(255, 255, 255, .94);
border-bottom: 1px solid var(--line);
}
.topbar-main, .topbar-status, .card-heading, .button-row, .log-actions,
.server-target, .command-footer, .capability-brief { display: flex; align-items: center; }
.topbar-main { gap: 11px; min-width: 0; }
.topbar-status { gap: 9px; flex-wrap: wrap; justify-content: flex-end; }
.back-link, .product-mark, .icon-button, .result-icon {
display: inline-grid;
place-items: center;
}
.back-link, .icon-button {
width: 34px;
height: 34px;
border: 1px solid var(--line);
border-radius: 10px;
color: #4d5b71;
background: var(--surface);
text-decoration: none;
}
.back-link:hover, .icon-button:hover { color: var(--primary); border-color: #a9c1ef; background: var(--primary-soft); }
.back-link svg, .icon-button svg { width: 17px; height: 17px; }
.product-mark { width: 36px; height: 36px; border-radius: 11px; color: white; background: linear-gradient(145deg, #3a7cf0, #1d59cd); box-shadow: 0 7px 16px rgba(40, 105, 232, .22); }
.product-mark svg { width: 19px; }
.eyebrow, .section-kicker { margin: 0; color: #7a88a0; font-size: 12px; font-weight: 700; letter-spacing: .08em; }
h1 { margin: 1px 0 0; font-size: 20px; line-height: 1.15; letter-spacing: -.02em; }
h2 { margin: 1px 0 0; font-size: 17px; line-height: 1.2; }
.environment-chip, .server-state, .status-badge {
display: inline-flex;
align-items: center;
gap: 6px;
min-height: 29px;
padding: 4px 10px;
border: 1px solid var(--line);
border-radius: 999px;
color: #58677d;
background: #f8fafc;
font-size: 12px;
font-weight: 700;
white-space: nowrap;
}
.environment-chip svg { width: 14px; }
.server-state span { width: 7px; height: 7px; border-radius: 50%; background: #9aa7b8; }
.server-state.online { color: var(--success); border-color: #b9e5d3; background: var(--success-soft); }
.server-state.online span { background: var(--success); box-shadow: 0 0 0 4px rgba(22, 132, 91, .1); }
main {
height: calc(100% - 68px);
min-height: 0;
padding: 11px;
display: grid;
grid-template-columns: 380px minmax(0, 1fr);
grid-template-rows: 116px minmax(0, 1fr) 172px;
grid-template-areas:
"pipeline pipeline"
"command execution"
"software software";
gap: 10px;
}
.card {
position: relative;
min-width: 0;
min-height: 0;
overflow: hidden;
padding: 12px 14px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 15px;
box-shadow: 0 2px 8px rgba(46, 66, 94, .045);
}
.card-heading { justify-content: space-between; gap: 12px; min-height: 32px; }
.hero-grid { grid-area: command; min-width: 0; min-height: 0; }
.hero-grid .command-card { height: 100%; }
.pipeline-card { grid-area: pipeline; }
.execution-grid { grid-area: execution; min-width: 0; min-height: 0; }
.execution-grid .log-card { width: 100%; height: 100%; }
.software-card { grid-area: software; }
.baseline-picker {
display: grid;
grid-template-columns: auto auto;
align-items: center;
column-gap: 5px;
color: var(--muted);
font-size: 12px;
font-weight: 700;
}
.baseline-picker svg { grid-row: 1 / 3; width: 16px; color: var(--primary); }
.baseline-picker span { line-height: 1; }
.baseline-picker select {
max-width: 148px;
border: 0;
padding: 1px 18px 1px 0;
color: var(--text);
background: transparent;
font-size: 13px;
font-weight: 700;
outline: none;
}
.command-card { display: flex; flex-direction: column; gap: 7px; }
.command-card > label { color: #5d6b80; font-size: 13px; font-weight: 650; }
textarea {
width: 100%;
min-height: 64px;
flex: 1 1 70px;
resize: none;
padding: 9px 11px;
color: var(--text);
background: var(--surface-soft);
border: 1px solid var(--line-strong);
border-radius: 10px;
font-size: 13px;
line-height: 1.55;
outline: none;
}
textarea:focus { border-color: #79a4f4; box-shadow: 0 0 0 3px rgba(40, 105, 232, .1); background: #fff; }
.prompt-templates { display: flex; gap: 6px; overflow: hidden; }
.prompt-templates button {
min-width: 0;
padding: 5px 8px;
border: 1px solid #cbd9ef;
border-radius: 8px;
color: #3563a9;
background: #f3f7ff;
font-size: 12px;
font-weight: 700;
white-space: nowrap;
}
.capability-brief {
min-height: 50px;
justify-content: space-between;
gap: 8px;
padding: 7px 9px;
border: 1px solid #dbe6f6;
border-radius: 10px;
background: #f6f9fe;
}
.capability-brief > div { min-width: 0; display: grid; gap: 1px; }
.capability-brief span { color: var(--muted); font-size: 12px; }
.capability-brief strong { overflow: hidden; text-overflow: ellipsis; color: #344157; font-size: 13px; white-space: nowrap; }
.capability-brief > svg { width: 17px; flex: 0 0 auto; color: #8fa7ca; }
.capability-target { flex: 1; }
.capability-target strong { color: var(--primary); }
.command-footer { justify-content: flex-end; gap: 8px; }
.button-row { gap: 6px; }
.button {
min-height: 33px;
padding: 6px 11px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
border-radius: 9px;
border: 1px solid transparent;
font-size: 13px;
font-weight: 750;
white-space: nowrap;
}
.button svg { width: 15px; }
.button-primary { color: white; background: var(--primary); box-shadow: 0 5px 12px rgba(40, 105, 232, .18); }
.button-secondary { color: #526178; background: #fff; border-color: var(--line-strong); }
.button:disabled, .icon-button:disabled { cursor: not-allowed; opacity: .48; }
.pipeline {
width: calc(100% - 230px);
height: 58px;
margin: 7px 0 0;
padding: 0;
display: grid;
grid-template-columns: repeat(8, minmax(0, 1fr));
list-style: none;
}
.pipeline-stage { position: relative; min-width: 0; padding-top: 2px; text-align: center; }
.pipeline-stage::after { content: ""; position: absolute; top: 14px; left: calc(50% + 15px); width: calc(100% - 30px); height: 2px; background: #dce4ee; }
.pipeline-stage:last-child::after { display: none; }
.stage-node { position: relative; z-index: 1; width: 28px; height: 28px; margin: 0 auto 1px; display: grid; place-items: center; border: 2px solid #d4deea; border-radius: 50%; color: #7b8aa0; background: #fff; }
.stage-node svg { width: 14px; }
.stage-name { display: block; overflow: hidden; text-overflow: ellipsis; font-size: 13px; line-height: 1.15; white-space: nowrap; }
.stage-state { display: block; overflow: hidden; text-overflow: ellipsis; color: var(--muted); font-size: 12px; line-height: 1.1; white-space: nowrap; }
.pipeline-stage[data-status="running"] .stage-node { color: var(--primary); border-color: var(--primary); background: var(--primary-soft); animation: pulse 1.4s infinite; }
.pipeline-stage[data-status="success"] .stage-node { color: var(--success); border-color: #65bd9e; background: var(--success-soft); }
.pipeline-stage[data-status="failed"] .stage-node, .pipeline-stage[data-status="blocked"] .stage-node { color: var(--danger); border-color: #e39aa1; background: #fff0f1; }
.pipeline-stage[data-status="canceled"] .stage-node { color: #7b8798; background: #eef1f5; }
.pipeline-stage[data-status="pending_confirmation"] .stage-node { color: var(--warning); border-color: #ecc080; background: #fff7e9; }
.pipeline-stage[data-status="success"]::after { background: #8bcdb5; }
.release-result {
position: absolute;
top: 43px;
right: 14px;
width: 210px;
min-height: 52px;
display: flex;
align-items: center;
gap: 10px;
padding: 8px 10px;
border: 1px solid var(--line);
border-radius: 11px;
background: var(--surface-soft);
}
.result-icon { width: 32px; height: 32px; flex: 0 0 auto; color: #748399; border-radius: 9px; background: #e9eef5; }
.result-icon svg { width: 17px; }
.release-result div { min-width: 0; display: grid; }
.release-result small { color: var(--muted); font-size: 12px; }
.release-result strong { overflow: hidden; text-overflow: ellipsis; font-size: 13px; white-space: nowrap; }
.release-result[data-state="success"] { border-color: #b9e5d3; background: var(--success-soft); }
.release-result[data-state="success"] .result-icon { color: var(--success); background: #d2f0e3; }
.release-result[data-state="failed"] { border-color: #efc5c9; background: #fff2f3; }
.status-legend { display: none; }
.log-card { display: grid; grid-template-rows: auto auto minmax(0, 1fr); gap: 7px; }
.log-actions { gap: 6px; }
.log-actions .icon-button { width: 31px; height: 31px; }
.status-badge { min-height: 25px; padding: 3px 9px; }
.status-badge[data-status="running"] { color: var(--primary); border-color: #b9cff7; background: var(--primary-soft); }
.status-badge[data-status="success"], .status-badge[data-status="published"] { color: var(--success); border-color: #b9e5d3; background: var(--success-soft); }
.status-badge[data-status="failed"], .status-badge[data-status="blocked"] { color: var(--danger); border-color: #efc5c9; background: #fff2f3; }
.status-badge[data-status="ready"], .status-badge[data-status="pending_confirmation"] { color: var(--warning); border-color: #efd3a8; background: #fff7e9; }
.metrics { margin: 0; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 6px; }
.metrics div { min-width: 0; padding: 5px 7px; border-radius: 8px; background: var(--surface-soft); border: 1px solid #edf1f6; }
.metrics dt { color: var(--muted); font-size: 12px; }
.metrics dd { margin: 1px 0 0; overflow: hidden; text-overflow: ellipsis; font-size: 13px; font-weight: 750; white-space: nowrap; }
.log-workspace { min-height: 0; display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(220px, .9fr); gap: 8px; }
.log-stream, .log-details { min-height: 0; overflow: auto; border: 1px solid var(--line); border-radius: 10px; background: #f9fbfd; }
.log-stream { padding: 7px 9px; scrollbar-width: thin; }
.empty-state { height: 100%; min-height: 110px; display: grid; place-content: center; justify-items: center; gap: 4px; color: var(--muted); text-align: center; }
.empty-state svg { width: 24px; color: #93a3ba; }
.empty-state strong { color: #4a596f; font-size: 13px; }
.empty-state span { font-size: 12px; }
.log-entry { display: grid; grid-template-columns: 58px 9px minmax(0, 1fr); gap: 7px; padding: 5px 0; border-bottom: 1px solid #e9eef5; }
.log-entry:last-child { border-bottom: 0; }
.log-time { color: #7b889b; font: 12px/1.4 ui-monospace, Consolas, monospace; }
.log-dot { width: 7px; height: 7px; margin-top: 5px; border-radius: 50%; background: #91a0b4; }
.log-entry[data-level="success"] .log-dot { background: var(--success); }
.log-entry[data-level="error"] .log-dot { background: var(--danger); }
.log-entry[data-level="warning"] .log-dot { background: var(--warning); }
.log-copy { min-width: 0; }
.log-copy strong { font-size: 13px; }
.log-copy p { margin: 1px 0 0; color: #5b687b; font-size: 13px; overflow-wrap: anywhere; }
.log-details { display: flex; flex-direction: column; }
.inspector-tabs { display: grid; grid-template-columns: repeat(3, 1fr); border-bottom: 1px solid var(--line); background: #f2f5f9; }
.inspector-tabs button { padding: 7px 4px; border: 0; border-bottom: 2px solid transparent; color: #68768b; background: transparent; font-size: 12px; font-weight: 700; }
.inspector-tabs button.active { color: var(--primary); border-bottom-color: var(--primary); background: #fff; }
.inspector-content { min-height: 0; flex: 1; overflow: auto; padding: 7px; }
.empty-inline, .empty-table { padding: 18px; color: var(--muted); text-align: center; font-size: 12px; }
.change-item, .test-item { display: flex; gap: 7px; padding: 6px; border-bottom: 1px solid #e8edf4; }
.change-type, .test-mark { flex: 0 0 auto; height: 21px; padding: 2px 5px; border-radius: 5px; color: var(--primary); background: var(--primary-soft); font-size: 12px; }
.test-mark { width: 21px; display: grid; place-items: center; color: var(--success); background: var(--success-soft); }
.test-mark svg { width: 12px; }
.change-copy, .test-copy { min-width: 0; display: grid; }
.change-copy strong, .test-copy strong { font-size: 12px; overflow-wrap: anywhere; }
.change-copy span, .test-copy span { color: var(--muted); font-size: 12px; }
.build-output, .code-block { margin: 0; color: #26364d; font: 12px/1.55 ui-monospace, Consolas, monospace; white-space: pre-wrap; overflow-wrap: anywhere; }
.software-card { display: grid; grid-template-rows: 33px minmax(0, 1fr); gap: 5px; }
.server-target { gap: 6px; color: var(--muted); font-size: 12px; }
.server-target svg { width: 15px; color: var(--primary); }
.server-target strong { color: #3b4a61; }
.table-wrap { min-height: 0; overflow: auto; border: 1px solid var(--line); border-radius: 9px; }
table { width: 100%; border-collapse: collapse; table-layout: fixed; font-size: 13px; }
th { position: sticky; top: 0; z-index: 1; padding: 6px 7px; color: #617086; background: #f2f5f9; text-align: left; font-weight: 750; white-space: nowrap; }
td { padding: 6px 7px; border-top: 1px solid #e8edf4; color: #4d5a6e; vertical-align: middle; }
th:nth-child(1) { width: 14%; } th:nth-child(2) { width: 13%; } th:nth-child(3) { width: 11%; }
th:nth-child(4) { width: 28%; } th:nth-child(5) { width: 8%; } th:nth-child(6) { width: 26%; }
.software-name, .file-cell { min-width: 0; display: grid; }
.software-name strong, .file-cell strong { overflow: hidden; text-overflow: ellipsis; color: #26354b; white-space: nowrap; }
.software-name span, .file-cell span { color: var(--muted); }
.file-cell > span { display: block; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cap-tags { display: flex; gap: 3px; flex-wrap: wrap; }
.cap-tags span { padding: 2px 5px; color: #3563a9; background: #eef4ff; border-radius: 5px; white-space: nowrap; }
.hash-value { display: inline; font-family: ui-monospace, Consolas, monospace; white-space: nowrap; }
.table-actions { display: flex; align-items: center; gap: 3px; flex-wrap: nowrap; }
.table-actions button, .table-actions a { min-width: 27px; height: 26px; padding: 3px 5px; display: inline-grid; place-items: center; border: 0; border-radius: 5px; color: #3563a9; background: #edf3ff; font-size: 12px; text-decoration: none; white-space: nowrap; }
.table-actions svg { width: 13px; height: 13px; }
.table-actions .publish-action { color: white; background: var(--primary); }
.table-actions .delete-action { color: var(--danger); background: #fff0f1; }
.table-actions .notify-action {
flex: 0 0 42px;
width: 42px;
min-width: 42px;
height: 34px;
margin-left: auto;
padding: 6px;
border-radius: 8px;
color: #fff;
background: var(--primary);
box-shadow: 0 4px 10px rgba(40, 105, 232, .2);
}
.table-actions .notify-action svg { width: 20px; height: 20px; stroke-width: 2.25; }
.table-actions .notify-action:hover { background: color-mix(in srgb, var(--primary) 86%, #132447); }
.baseline-protection { height: 26px; padding: 3px 6px; display: inline-flex; align-items: center; gap: 3px; color: var(--success); background: var(--success-soft); border-radius: 5px; font-size: 12px; white-space: nowrap; }
.baseline-protection svg { width: 13px; }
.status-badge[data-status="baseline"] { color: var(--success); border-color: #b9e5d3; background: var(--success-soft); }
.modal-backdrop { position: fixed; inset: 0; z-index: 50; display: grid; place-items: center; padding: 24px; background: rgba(31, 43, 61, .34); backdrop-filter: blur(4px); }
.modal-backdrop[hidden] { display: none; }
.modal { width: min(1040px, 100%); max-height: min(820px, calc(100dvh - 48px)); display: grid; grid-template-rows: auto minmax(0, 1fr) auto; overflow: hidden; background: #fff; border: 1px solid var(--line); border-radius: 16px; box-shadow: var(--shadow); }
.modal header, .modal footer { padding: 14px 16px; display: flex; align-items: center; justify-content: space-between; gap: 12px; border-bottom: 1px solid var(--line); }
.modal footer { justify-content: flex-end; border-top: 1px solid var(--line); border-bottom: 0; }
.modal-body { overflow: auto; padding: 16px; }
.detail-grid, .diff-columns { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
.detail-field, .diff-panel { min-width: 0; padding: 11px; border: 1px solid var(--line); border-radius: 9px; background: var(--surface-soft); }
.detail-field span { display: block; color: var(--muted); font-size: 12px; }
.detail-field strong { display: block; margin-top: 3px; overflow-wrap: anywhere; }
.diff-panel h3 { margin: 0 0 8px; font-size: 14px; }
.diff-panel p { margin: 5px 0; color: #58677c; }
.diff-panel.added { border-color: #b9e5d3; background: var(--success-soft); }
.toast-region { position: fixed; z-index: 80; right: 22px; bottom: 22px; display: grid; gap: 8px; }
.toast { max-width: 390px; padding: 10px 13px; color: #fff; background: #344258; border-radius: 9px; box-shadow: 0 10px 28px rgba(31, 43, 61, .22); font-size: 13px; }
.toast.success { background: var(--success); }
.toast.error { background: var(--danger); }
@keyframes pulse { 50% { box-shadow: 0 0 0 5px rgba(40, 105, 232, .09); } }
@media (max-width: 980px) {
body { padding: 8px; }
.shell { width: 100%; height: calc(100dvh - 16px); min-height: 560px; border-radius: 16px; }
.topbar { height: 62px; padding: 0 14px; }
.topbar-status .dashboard-chip, .environment-chip:last-child { display: none; }
main { height: calc(100% - 62px); grid-template-columns: 330px minmax(0, 1fr); grid-template-rows: 108px minmax(0, 1fr) 164px; padding: 8px; gap: 8px; }
.card { padding: 10px; }
.pipeline { width: calc(100% - 190px); }
.release-result { width: 176px; right: 10px; }
.log-workspace { grid-template-columns: 1fr; }
.log-details { display: none; }
.metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 720px) {
body { display: block; padding: 0; }
.shell { height: 100dvh; min-height: 0; border: 0; border-radius: 0; }
.topbar { height: 58px; }
h1 { font-size: 17px; }
.eyebrow, .topbar-status { display: none; }
main { height: calc(100% - 58px); overflow: auto; grid-template-columns: 1fr; grid-template-rows: 104px 360px 330px 190px; grid-template-areas: "pipeline" "command" "execution" "software"; }
.execution-grid { display: block; }
.execution-grid .log-card { min-height: 330px; }
.pipeline { width: 100%; }
.pipeline-card { overflow-x: auto; }
.pipeline-card .pipeline { width: 680px; }
.release-result { display: none; }
.stage-name { font-size: 11px; }
.stage-state { display: none; }
.command-card { gap: 5px; }
.command-footer p { display: none; }
.command-footer { justify-content: flex-end; }
.prompt-templates button { flex: 1; }
th:nth-child(2), td:nth-child(2), th:nth-child(3), td:nth-child(3) { display: none; }
th:nth-child(1), th:nth-child(4), th:nth-child(5), th:nth-child(6) { width: auto; }
}
body.embedded {
display: block;
padding: 0;
background: transparent;
}
body.embedded .shell {
width: 100%;
height: 100dvh;
min-height: 0;
border: 0;
border-radius: 0;
box-shadow: none;
background: #f7f9fc;
}
body.embedded .topbar { display: none; }
body.embedded main {
height: 100%;
padding: 8px;
grid-template-columns: 350px minmax(0, 1fr);
grid-template-rows: 82px minmax(0, 1fr) 204px;
gap: 8px;
}
body.embedded .card { padding: 8px 10px; border-radius: 12px; }
body.embedded .card-heading { min-height: 27px; }
body.embedded .command-card { gap: 5px; }
body.embedded textarea { min-height: 48px; padding: 7px 9px; }
body.embedded .prompt-templates button { padding: 4px 7px; }
body.embedded .capability-brief { min-height: 44px; padding: 5px 8px; }
body.embedded .pipeline { height: 48px; margin: 4px 0 0; }
body.embedded .stage-node { width: 24px; height: 24px; }
body.embedded .pipeline-stage::after { top: 12px; }
body.embedded .stage-node svg { width: 12px; }
body.embedded .stage-name { font-size: 12px; }
body.embedded .stage-state { font-size: 11px; }
body.embedded .release-result { top: 18px; min-height: 44px; padding: 5px 8px; }
body.embedded .result-icon { width: 28px; height: 28px; }
body.embedded .log-card { gap: 5px; }
body.embedded .metrics div { padding: 3px 6px; }
body.embedded .software-card { grid-template-rows: 27px minmax(0, 1fr); gap: 3px; }
body.embedded th { padding: 4px 6px; }
body.embedded table { font-size: 12.5px; }
body.embedded td { padding: 3px 6px; }
body.embedded .software-name strong,
body.embedded .file-cell strong { line-height: 1.2; }
body.embedded .software-name span,
body.embedded .file-cell span { line-height: 1.15; }
@media (max-width: 720px) {
body.embedded main {
height: 100%;
overflow: auto;
grid-template-columns: 1fr;
grid-template-rows: 96px 350px 320px 210px;
grid-template-areas: "pipeline" "command" "execution" "software";
}
body.embedded .pipeline-card { overflow-x: auto; }
body.embedded .pipeline-card .pipeline { width: 680px; }
}
/* The workbench is rendered in an iframe, so it owns a complete theme token
set instead of relying on custom properties from the parent document. */
:root[data-glass-theme="dark"] {
color-scheme: dark;
--page: #090e1c;
--surface: #171e31;
--surface-soft: #11182a;
--line: #303a55;
--line-strong: #44516d;
--text: #edf2ff;
--muted: #9ba9c0;
--primary: #7aa3ff;
--primary-soft: #1b2b53;
--success: #55d4a1;
--success-soft: #12372d;
--warning: #f2bd68;
--danger: #ff8993;
--shadow: 0 26px 70px rgba(0, 0, 0, .42), 0 4px 18px rgba(0, 0, 0, .24);
}
:root[data-glass-theme="dark"] body:not(.embedded) {
background:
radial-gradient(circle at 15% 8%, color-mix(in srgb, var(--primary) 18%, transparent), transparent 31%),
linear-gradient(145deg, #11182b 0%, var(--page) 100%);
}
:root[data-glass-theme="dark"] body.embedded { background: transparent; }
:root[data-glass-theme="dark"] .shell,
:root[data-glass-theme="dark"] body.embedded .shell {
background: color-mix(in srgb, var(--surface-soft) 86%, var(--page));
border-color: color-mix(in srgb, var(--line-strong) 78%, transparent);
}
:root[data-glass-theme="dark"] .topbar {
background: color-mix(in srgb, var(--surface) 94%, transparent);
}
:root[data-glass-theme="dark"] :is(.back-link, .icon-button, .button-secondary) {
color: var(--muted);
background: var(--surface);
border-color: var(--line-strong);
}
:root[data-glass-theme="dark"] :is(.back-link, .icon-button):hover {
color: var(--primary);
border-color: color-mix(in srgb, var(--primary) 50%, var(--line));
background: var(--primary-soft);
}
:root[data-glass-theme="dark"] :is(.eyebrow, .section-kicker) { color: var(--muted); }
:root[data-glass-theme="dark"] :is(.environment-chip, .server-state) {
color: var(--muted);
background: var(--surface-soft);
}
:root[data-glass-theme="dark"] .server-state span { background: color-mix(in srgb, var(--muted) 72%, transparent); }
:root[data-glass-theme="dark"] .server-state.online,
:root[data-glass-theme="dark"] .status-badge[data-status="baseline"] {
border-color: color-mix(in srgb, var(--success) 44%, var(--line));
background: var(--success-soft);
}
:root[data-glass-theme="dark"] .card {
box-shadow: 0 2px 10px rgba(0, 0, 0, .16);
}
:root[data-glass-theme="dark"] .command-card > label { color: var(--muted); }
:root[data-glass-theme="dark"] textarea:focus { background: var(--surface); }
:root[data-glass-theme="dark"] :is(select, option) { color: var(--text); background: var(--surface); }
:root[data-glass-theme="dark"] .prompt-templates button,
:root[data-glass-theme="dark"] .cap-tags span,
:root[data-glass-theme="dark"] .table-actions :is(button, a) {
color: var(--primary);
background: var(--primary-soft);
border-color: color-mix(in srgb, var(--primary) 34%, var(--line));
}
:root[data-glass-theme="dark"] .capability-brief {
border-color: color-mix(in srgb, var(--primary) 25%, var(--line));
background: color-mix(in srgb, var(--primary-soft) 44%, var(--surface-soft));
}
:root[data-glass-theme="dark"] .capability-brief strong { color: var(--text); }
:root[data-glass-theme="dark"] .capability-brief > svg { color: var(--primary); }
:root[data-glass-theme="dark"] .pipeline-stage::after { background: var(--line); }
:root[data-glass-theme="dark"] .stage-node {
color: var(--muted);
border-color: var(--line-strong);
background: var(--surface);
}
:root[data-glass-theme="dark"] .pipeline-stage[data-status="success"] .stage-node {
border-color: color-mix(in srgb, var(--success) 62%, var(--line));
}
:root[data-glass-theme="dark"] .pipeline-stage[data-status="failed"] .stage-node,
:root[data-glass-theme="dark"] .pipeline-stage[data-status="blocked"] .stage-node,
:root[data-glass-theme="dark"] .release-result[data-state="failed"],
:root[data-glass-theme="dark"] .status-badge[data-status="failed"],
:root[data-glass-theme="dark"] .status-badge[data-status="blocked"] {
color: var(--danger);
border-color: color-mix(in srgb, var(--danger) 44%, var(--line));
background: color-mix(in srgb, var(--danger) 12%, var(--surface));
}
:root[data-glass-theme="dark"] .pipeline-stage[data-status="canceled"] .stage-node {
color: var(--muted);
background: var(--surface-soft);
}
:root[data-glass-theme="dark"] .pipeline-stage[data-status="pending_confirmation"] .stage-node,
:root[data-glass-theme="dark"] .status-badge[data-status="ready"],
:root[data-glass-theme="dark"] .status-badge[data-status="pending_confirmation"] {
color: var(--warning);
border-color: color-mix(in srgb, var(--warning) 44%, var(--line));
background: color-mix(in srgb, var(--warning) 12%, var(--surface));
}
:root[data-glass-theme="dark"] .pipeline-stage[data-status="success"]::after {
background: color-mix(in srgb, var(--success) 64%, var(--line));
}
:root[data-glass-theme="dark"] .release-result[data-state="success"],
:root[data-glass-theme="dark"] .status-badge[data-status="success"],
:root[data-glass-theme="dark"] .status-badge[data-status="published"] {
border-color: color-mix(in srgb, var(--success) 44%, var(--line));
background: var(--success-soft);
}
:root[data-glass-theme="dark"] .result-icon {
color: var(--muted);
background: color-mix(in srgb, var(--surface-soft) 78%, var(--line));
}
:root[data-glass-theme="dark"] .release-result[data-state="success"] .result-icon {
color: var(--success);
background: color-mix(in srgb, var(--success) 18%, var(--surface));
}
:root[data-glass-theme="dark"] .metrics div { border-color: var(--line); }
:root[data-glass-theme="dark"] :is(.log-stream, .log-details) { background: var(--surface-soft); }
:root[data-glass-theme="dark"] .empty-state svg { color: var(--muted); }
:root[data-glass-theme="dark"] .empty-state strong { color: var(--text); }
:root[data-glass-theme="dark"] .log-entry,
:root[data-glass-theme="dark"] :is(.change-item, .test-item) { border-color: var(--line); }
:root[data-glass-theme="dark"] .log-time { color: var(--muted); }
:root[data-glass-theme="dark"] .log-dot { background: var(--muted); }
:root[data-glass-theme="dark"] .log-copy p { color: var(--muted); }
:root[data-glass-theme="dark"] .inspector-tabs { background: var(--surface); }
:root[data-glass-theme="dark"] .inspector-tabs button { color: var(--muted); }
:root[data-glass-theme="dark"] .inspector-tabs button.active {
color: var(--primary);
background: var(--surface-soft);
}
:root[data-glass-theme="dark"] :is(.build-output, .code-block) { color: var(--text); }
:root[data-glass-theme="dark"] .server-target strong { color: var(--text); }
:root[data-glass-theme="dark"] th { color: var(--muted); background: var(--surface); }
:root[data-glass-theme="dark"] td { color: var(--muted); border-color: var(--line); }
:root[data-glass-theme="dark"] :is(.software-name strong, .file-cell strong) { color: var(--text); }
:root[data-glass-theme="dark"] .table-actions .delete-action {
color: var(--danger);
background: color-mix(in srgb, var(--danger) 12%, var(--surface));
}
:root[data-glass-theme="dark"] .table-actions .notify-action {
color: #fff;
background: color-mix(in srgb, var(--primary) 82%, #07101e);
}
:root[data-glass-theme="dark"] .modal-backdrop { background: rgba(2, 5, 14, .68); }
:root[data-glass-theme="dark"] .modal { background: var(--surface); }
:root[data-glass-theme="dark"] .diff-panel p { color: var(--muted); }
:root[data-glass-theme="dark"] .diff-panel.added {
border-color: color-mix(in srgb, var(--success) 42%, var(--line));
}
+144
View File
@@ -0,0 +1,144 @@
<!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="#0f1526" id="firmwareWorkbenchThemeColor" />
<title>座椅固件开发工作台 · 511 工程</title>
<link rel="icon" href="/icons/app-icon-192.png" />
<script src="/firmware-workbench-theme.js?v=1"></script>
<link rel="stylesheet" href="/firmware-workbench.css?v=7" />
<script src="/vendor/lucide/0.468.0/lucide.min.js" defer></script>
<script src="/firmware-workbench.js?v=4" defer></script>
</head>
<body>
<div class="shell">
<header class="topbar">
<div class="topbar-main">
<a class="back-link" href="/" aria-label="返回智能座舱">
<i data-lucide="arrow-left"></i>
</a>
<div class="product-mark" aria-hidden="true"><i data-lucide="blocks"></i></div>
<h1>座椅固件开发工作台</h1>
</div>
<div class="topbar-status">
<span class="server-state" id="serverState"><span></span>正在连接发布服务</span>
</div>
</header>
<main>
<section class="hero-grid" aria-label="固件开发任务">
<article class="card command-card">
<div class="card-heading">
<div>
<h2>开发指令</h2>
</div>
<label class="baseline-picker" for="baselineSelect">
<i data-lucide="git-branch"></i>
<span>固件基线</span>
<select id="baselineSelect" aria-label="选择固件基线">
<option value="time_only">时间控制</option>
</select>
</label>
</div>
<label for="instructionInput">描述需要扩展的副驾座椅能力</label>
<textarea id="instructionInput" rows="5" maxlength="2000">将副驾座椅固件从仅时间控制扩展为时间+位置参数控制,并增加座椅通风和座椅加热功能;完成编译、测试验证、固件打包,并发布到升级服务器。</textarea>
<div class="prompt-templates" aria-label="常用开发指令">
<button type="button" data-prompt="增加支持座椅位置参数控制的能力">+ 位置控制</button>
<button type="button" data-prompt="增加座椅通风功能">+ 座椅通风</button>
<button type="button" data-prompt="增加座椅加热功能">+ 座椅加热</button>
</div>
<div class="capability-brief" aria-label="固件能力升级范围">
<div><span>当前基线</span><strong id="baselineCapabilities">时间控制</strong></div>
<i data-lucide="chevrons-right"></i>
<div class="capability-target"><span>目标固件能力</span><strong id="targetCapabilities">时间控制+位置控制+座椅通风+座椅加热</strong></div>
</div>
<div class="command-footer">
<div class="button-row">
<button class="button button-secondary" id="cancelTaskButton" type="button" disabled>
<i data-lucide="square"></i>取消任务
</button>
<button class="button button-primary" id="startTaskButton" type="button">
<i data-lucide="play"></i>开始生成
</button>
</div>
</div>
</article>
</section>
<section class="card pipeline-card" aria-label="固件交付流水线">
<ol class="pipeline" id="pipeline" aria-label="固件交付阶段"></ol>
<div class="release-result" id="releaseResult" data-state="idle">
<span class="result-icon"><i data-lucide="cloud-upload"></i></span>
<div><small>当前交付状态</small><strong>尚未开始</strong></div>
</div>
<div class="status-legend" aria-label="阶段状态图例">
<span data-status="waiting">等待</span><span data-status="running">运行中</span>
<span data-status="pending_confirmation">待确认</span><span data-status="success">成功</span>
<span data-status="failed">失败</span><span data-status="canceled">取消</span>
<span data-status="blocked">阻塞</span>
</div>
</section>
<section class="execution-grid">
<article class="card log-card" aria-labelledby="logTitle">
<div class="card-heading">
<div>
<h2 id="logTitle">任务执行日志</h2>
</div>
<div class="log-actions">
<span class="status-badge" id="taskStatusBadge" data-status="waiting">等待</span>
<button class="icon-button" id="retryTaskButton" type="button" title="重试失败任务" disabled><i data-lucide="refresh-cw"></i></button>
<button class="icon-button" id="scrollLogButton" type="button" title="滚动到最新日志"><i data-lucide="arrow-down-to-line"></i></button>
</div>
</div>
<dl class="metrics" id="taskMetrics">
<div><dt>开始时间</dt><dd>—</dd></div><div><dt>结束时间</dt><dd>—</dd></div>
<div><dt>实际耗时</dt><dd>—</dd></div><div><dt>文件变更</dt><dd>—</dd></div>
</dl>
<div class="log-workspace">
<div class="log-stream" id="logStream" role="log" aria-live="polite">
<div class="empty-state"><i data-lucide="terminal-square"></i><strong>暂无执行记录</strong><span>创建开发任务后,这里将实时显示各阶段输出。</span></div>
</div>
<aside class="log-details" aria-label="日志附属详情">
<div class="inspector-tabs" role="tablist">
<button class="active" type="button" role="tab" data-inspector-tab="changes">文件变更</button>
<button type="button" role="tab" data-inspector-tab="build">编译输出</button>
<button type="button" role="tab" data-inspector-tab="tests">测试验证</button>
</div>
<div class="inspector-content" id="inspectorContent">
<div class="empty-inline">暂无任务数据</div>
</div>
</aside>
</div>
</article>
</section>
<section class="card software-card" aria-labelledby="softwareTitle">
<div class="card-heading software-heading">
<div>
<h2 id="softwareTitle">固件版本与软件产物</h2>
</div>
<div class="server-target"><i data-lucide="server"></i><span>发布目标</span><strong id="otaServerName">车机升级服务器</strong></div>
</div>
<div class="table-wrap">
<table>
<thead><tr><th>软件 / 版本</th><th>能力</th><th>构建时间</th><th>固件文件 / 校验</th><th>状态</th><th>操作</th></tr></thead>
<tbody id="artifactTableBody"><tr><td colspan="6"><div class="empty-table">尚未生成固件产物</div></td></tr></tbody>
</table>
</div>
</section>
</main>
</div>
<div class="modal-backdrop" id="detailModal" hidden>
<section class="modal" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
<header><h2 id="modalTitle">固件详情</h2><button class="icon-button" id="closeModalButton" type="button" aria-label="关闭"><i data-lucide="x"></i></button></header>
<div class="modal-body" id="modalBody"></div>
<footer><button class="button button-secondary" id="modalCloseButton" type="button">关闭</button></footer>
</section>
</div>
<div class="toast-region" id="toastRegion" aria-live="polite"></div>
</body>
</html>
+460
View File
@@ -0,0 +1,460 @@
(() => {
"use strict";
if (window.self !== window.top || new URLSearchParams(window.location.search).has("embedded")) {
document.body.classList.add("embedded");
}
const STATUS_LABELS = {
waiting: "等待",
running: "运行中",
pending_confirmation: "待确认",
success: "成功",
failed: "失败",
canceled: "取消",
blocked: "阻塞",
published: "已发布",
ready: "待发布",
baseline: "基线固件",
};
const STAGE_ICONS = {
instruction: "file-input",
requirements: "list-checks",
baseline: "git-compare-arrows",
codegen: "code-2",
build: "hammer",
test: "shield-check",
package: "package-check",
publish: "cloud-upload",
};
const ACTIVE_STATES = new Set(["waiting", "running", "pending_confirmation"]);
const CAPABILITY_ORDER = ["时间控制", "位置控制", "座椅通风", "座椅加热"];
const FALLBACK_BASELINES = {
time_only: ["时间控制"],
};
const FALLBACK_PIPELINE = [
["instruction", "指令输入"], ["requirements", "需求解析"],
["baseline", "基线检查"], ["codegen", "代码生成"],
["build", "编译构建"], ["test", "测试验证"],
["package", "固件打包"], ["publish", "发布服务器"],
].map(([id, name]) => ({ id, name, status: "waiting", summary: "等待执行" }));
const ui = {
instruction: document.querySelector("#instructionInput"),
baseline: document.querySelector("#baselineSelect"),
baselineCapabilities: document.querySelector("#baselineCapabilities"),
targetCapabilities: document.querySelector("#targetCapabilities"),
start: document.querySelector("#startTaskButton"),
cancel: document.querySelector("#cancelTaskButton"),
retry: document.querySelector("#retryTaskButton"),
server: document.querySelector("#serverState"),
serverName: document.querySelector("#otaServerName"),
release: document.querySelector("#releaseResult"),
pipeline: document.querySelector("#pipeline"),
logs: document.querySelector("#logStream"),
scrollLogs: document.querySelector("#scrollLogButton"),
status: document.querySelector("#taskStatusBadge"),
metrics: document.querySelector("#taskMetrics"),
inspector: document.querySelector("#inspectorContent"),
artifacts: document.querySelector("#artifactTableBody"),
modal: document.querySelector("#detailModal"),
modalTitle: document.querySelector("#modalTitle"),
modalBody: document.querySelector("#modalBody"),
modalClose: document.querySelector("#closeModalButton"),
modalCloseFooter: document.querySelector("#modalCloseButton"),
toasts: document.querySelector("#toastRegion"),
};
const model = {
snapshot: { tasks: [], artifacts: [], releases: [], pipeline: FALLBACK_PIPELINE },
selectedTaskId: null,
inspectorTab: "changes",
pending: false,
pollTimer: null,
lastLogCount: 0,
};
function escapeHtml(value) {
return String(value ?? "")
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}
async function api(path, options = {}) {
const response = await fetch(path, {
credentials: "same-origin",
headers: { "Content-Type": "application/json", ...(options.headers || {}) },
...options,
});
const contentType = response.headers.get("content-type") || "";
const payload = contentType.includes("application/json") ? await response.json() : null;
if (!response.ok) throw new Error(payload?.message || `请求失败(${response.status})`);
return payload;
}
function selectedTask() {
return model.snapshot.tasks.find((task) => task.id === model.selectedTaskId)
|| model.snapshot.tasks[0]
|| null;
}
function baselineCapabilities() {
const baseline = model.snapshot.baselines?.find((item) => item.id === ui.baseline.value);
return baseline?.capabilities || FALLBACK_BASELINES[ui.baseline.value] || FALLBACK_BASELINES.time_only;
}
function parseInstructionCapabilities(instruction) {
const lowered = String(instruction || "").toLowerCase();
const aliases = {
"时间控制": ["时间", "定时", "timed"],
"位置控制": ["位置", "position"],
"座椅通风": ["通风", "vent"],
"座椅加热": ["加热", "heater", "heat"],
};
return CAPABILITY_ORDER.filter((capability) => aliases[capability].some((alias) => lowered.includes(alias)));
}
function updateCapabilityPreview(task = null) {
const baseline = baselineCapabilities();
const taskMatchesDraft = task
&& task.instruction === ui.instruction.value.trim()
&& (task.baseline_id || "time_only") === ui.baseline.value;
const target = taskMatchesDraft
? task.target_capabilities
: CAPABILITY_ORDER.filter((item) => baseline.includes(item) || parseInstructionCapabilities(ui.instruction.value).includes(item));
ui.baselineCapabilities.textContent = baseline.join("+");
ui.targetCapabilities.textContent = (target?.length ? target : baseline).join("+");
}
function formatDate(value) {
if (!value) return "—";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value;
return new Intl.DateTimeFormat("zh-CN", {
month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit",
hour12: false,
}).format(date).replaceAll("/", "-");
}
function formatClock(value) {
if (!value) return "--:--:--";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return "--:--:--";
return new Intl.DateTimeFormat("zh-CN", {
hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false,
}).format(date);
}
function formatDuration(milliseconds) {
if (milliseconds === null || milliseconds === undefined) return "—";
if (milliseconds < 1000) return `${milliseconds} 毫秒`;
const seconds = milliseconds / 1000;
return seconds < 60 ? `${seconds.toFixed(seconds < 10 ? 1 : 0)} 秒` : `${Math.floor(seconds / 60)} 分 ${Math.round(seconds % 60)} 秒`;
}
function formatSize(bytes) {
const value = Number(bytes || 0);
if (value < 1024) return `${value} 字节`;
if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} 千字节`;
return `${(value / 1024 / 1024).toFixed(2)} 兆字节`;
}
function toast(message, type = "info") {
const element = document.createElement("div");
element.className = `toast ${type}`;
element.textContent = message;
ui.toasts.append(element);
window.setTimeout(() => element.remove(), 4200);
}
function refreshIcons() {
if (window.lucide) window.lucide.createIcons({ attrs: { "stroke-width": 1.8 } });
}
function renderPipeline(task) {
const stages = task?.stages || model.snapshot.pipeline?.map((stage) => ({ ...stage, status: "waiting" })) || FALLBACK_PIPELINE;
ui.pipeline.innerHTML = stages.map((stage) => `
<li class="pipeline-stage" data-status="${escapeHtml(stage.status || "waiting")}" title="${escapeHtml(stage.summary || "")}">
<span class="stage-node"><i data-lucide="${STAGE_ICONS[stage.id] || "circle"}"></i></span>
<strong class="stage-name">${escapeHtml(stage.name)}</strong>
<span class="stage-state">${escapeHtml(STATUS_LABELS[stage.status] || stage.status || "等待")}${stage.duration_ms !== null && stage.duration_ms !== undefined ? ` · ${escapeHtml(formatDuration(stage.duration_ms))}` : ""}</span>
</li>`).join("");
}
function renderRelease(task) {
let icon = "cloud-upload";
let label = "尚未开始";
let state = "idle";
if (task) {
label = task.message || STATUS_LABELS[task.status] || task.status;
state = task.status;
if (task.status === "success") { icon = "circle-check-big"; state = "success"; }
else if (task.status === "running") icon = "loader-circle";
else if (task.status === "failed") icon = "circle-x";
else if (task.status === "canceled") icon = "ban";
}
ui.release.dataset.state = state;
ui.release.innerHTML = `<span class="result-icon"><i data-lucide="${icon}"></i></span><div><small>当前交付状态</small><strong>${escapeHtml(label)}</strong></div>`;
}
function renderLogs(task) {
const logs = task?.logs || [];
if (!logs.length) {
ui.logs.innerHTML = `<div class="empty-state"><i data-lucide="terminal-square"></i><strong>暂无执行记录</strong><span>创建开发任务后,这里将实时显示各阶段输出。</span></div>`;
model.lastLogCount = 0;
return;
}
const nearBottom = ui.logs.scrollHeight - ui.logs.scrollTop - ui.logs.clientHeight < 80;
ui.logs.innerHTML = logs.map((entry) => `
<article class="log-entry" data-level="${escapeHtml(entry.level)}">
<time class="log-time">${escapeHtml(formatClock(entry.time))}</time>
<span class="log-dot"></span>
<div class="log-copy"><strong>${escapeHtml(entry.title)}</strong><p>${escapeHtml(entry.message)}</p></div>
</article>`).join("");
if (nearBottom || logs.length > model.lastLogCount) ui.logs.scrollTop = ui.logs.scrollHeight;
model.lastLogCount = logs.length;
}
function renderInspector(task) {
const status = task?.status || "waiting";
ui.status.dataset.status = status;
ui.status.textContent = STATUS_LABELS[status] || status;
const fileCount = task?.file_changes?.length || 0;
ui.metrics.innerHTML = `
<div><dt>开始时间</dt><dd>${escapeHtml(formatDate(task?.started_at))}</dd></div>
<div><dt>结束时间</dt><dd>${escapeHtml(formatDate(task?.finished_at))}</dd></div>
<div><dt>实际耗时</dt><dd>${escapeHtml(formatDuration(task?.duration_ms))}</dd></div>
<div><dt>文件变更</dt><dd>${fileCount ? `${fileCount} 个文件` : "—"}</dd></div>`;
document.querySelectorAll("[data-inspector-tab]").forEach((button) => {
button.classList.toggle("active", button.dataset.inspectorTab === model.inspectorTab);
});
if (!task) {
ui.inspector.innerHTML = `<div class="empty-inline">暂无任务数据</div>`;
return;
}
if (model.inspectorTab === "build") {
ui.inspector.innerHTML = task.build_output?.length
? `<pre class="build-output">${escapeHtml(task.build_output.join("\n"))}</pre>`
: `<div class="empty-inline">尚无编译输出</div>`;
return;
}
if (model.inspectorTab === "tests") {
ui.inspector.innerHTML = task.test_results?.length
? task.test_results.map((test) => `<div class="test-item"><span class="test-mark"><i data-lucide="check"></i></span><div class="test-copy"><strong>${escapeHtml(test.name)}</strong><span>${escapeHtml(test.summary)}</span></div></div>`).join("")
: `<div class="empty-inline">测试尚未执行</div>`;
return;
}
ui.inspector.innerHTML = task.file_changes?.length
? task.file_changes.map((change) => `<div class="change-item"><span class="change-type">${change.type === "added" ? "新增" : "修改"}</span><div class="change-copy"><strong>${escapeHtml(change.path)}</strong><span>${escapeHtml(change.summary)}</span></div></div>`).join("")
: `<div class="empty-inline">尚无文件变更</div>`;
}
function renderArtifacts() {
const artifacts = model.snapshot.artifacts || [];
if (!artifacts.length) {
ui.artifacts.innerHTML = `<tr><td colspan="6"><div class="empty-table">尚未生成固件产物</div></td></tr>`;
return;
}
ui.artifacts.innerHTML = artifacts.map((artifact) => `
<tr>
<td><div class="software-name"><strong>${escapeHtml(artifact.software_name)}</strong><span>${escapeHtml(artifact.version)}</span></div></td>
<td><div class="cap-tags">${(artifact.capabilities || []).map((capability) => `<span>${escapeHtml(capability)}</span>`).join("")}</div></td>
<td>${escapeHtml(formatDate(artifact.build_time))}</td>
<td><div class="file-cell"><strong>${escapeHtml(artifact.firmware_filename)}</strong><span>${escapeHtml(formatSize(artifact.size_bytes))} · <span class="hash-value" title="${escapeHtml(artifact.sha256)}">${escapeHtml(artifact.sha256)}</span></span></div></td>
<td><span class="status-badge" data-status="${escapeHtml(artifact.status)}">${escapeHtml(STATUS_LABELS[artifact.status] || artifact.status)}</span></td>
<td><div class="table-actions">
<button type="button" title="查看详情" data-artifact-action="detail" data-artifact-id="${artifact.id}">详情</button>
<button type="button" title="查看能力差异" data-artifact-action="capabilities" data-artifact-id="${artifact.id}">能力</button>
<button type="button" title="查看代码差异" data-artifact-action="code" data-artifact-id="${artifact.id}">代码</button>
<button type="button" title="查看验证记录" data-artifact-action="validation" data-artifact-id="${artifact.id}">验证</button>
<a href="${escapeHtml(artifact.download_url)}" title="下载固件" aria-label="下载固件" download><i data-lucide="download"></i></a>
<button class="publish-action" type="button" title="发布到服务器" data-artifact-action="publish" data-artifact-id="${artifact.id}"><i data-lucide="cloud-upload"></i></button>
${artifact.is_baseline ? `<span class="baseline-protection" title="时间控制基线固件受保护"><i data-lucide="shield-check"></i>受保护</span>` : `<button class="delete-action" type="button" title="删除固件产物" data-artifact-action="delete" data-artifact-id="${artifact.id}"><i data-lucide="trash-2"></i></button>`}
${artifact.status === "published" ? `<button class="notify-action" type="button" title="通知车端" aria-label="通知车端" data-artifact-action="notify" data-artifact-id="${artifact.id}"><i data-lucide="bell"></i></button>` : ""}
</div></td>
</tr>`).join("");
}
function render() {
const task = selectedTask();
if (task) model.selectedTaskId = task.id;
renderPipeline(task);
renderRelease(task);
renderLogs(task);
renderInspector(task);
renderArtifacts();
updateCapabilityPreview(task);
const active = task && ACTIVE_STATES.has(task.status);
ui.start.disabled = model.pending || Boolean(active);
ui.cancel.disabled = model.pending || !active;
ui.retry.disabled = model.pending || !task || !["failed", "canceled", "blocked"].includes(task.status);
ui.start.innerHTML = model.pending ? `<i data-lucide="loader-circle"></i>正在提交` : `<i data-lucide="play"></i>开始生成`;
refreshIcons();
}
async function refreshState({ quiet = false } = {}) {
try {
model.snapshot = await api("/api/firmware-workbench/state");
if (!model.selectedTaskId && model.snapshot.tasks?.length) model.selectedTaskId = model.snapshot.tasks[0].id;
ui.server.classList.add("online");
ui.server.innerHTML = `<span></span>发布服务已连接`;
ui.serverName.textContent = model.snapshot.ota_server?.name || "车机升级服务器";
render();
} catch (error) {
ui.server.classList.remove("online");
ui.server.innerHTML = `<span></span>发布服务连接失败`;
if (!quiet) toast(error.message, "error");
} finally {
schedulePoll();
}
}
function schedulePoll() {
window.clearTimeout(model.pollTimer);
const active = model.snapshot.tasks?.some((task) => ACTIVE_STATES.has(task.status));
model.pollTimer = window.setTimeout(() => refreshState({ quiet: true }), active ? 700 : 5000);
}
async function startTask() {
const instruction = ui.instruction.value.trim();
if (!instruction) { toast("请输入开发指令。", "error"); ui.instruction.focus(); return; }
model.pending = true;
render();
try {
const result = await api("/api/firmware-workbench/tasks", {
method: "POST", body: JSON.stringify({ instruction, baseline_id: ui.baseline.value }),
});
model.selectedTaskId = result.task.id;
toast("开发任务已创建,流水线开始执行。", "success");
await refreshState({ quiet: true });
} catch (error) {
toast(error.message, "error");
} finally {
model.pending = false;
render();
}
}
async function taskAction(action) {
const task = selectedTask();
if (!task) return;
model.pending = true;
render();
try {
await api(`/api/firmware-workbench/tasks/${encodeURIComponent(task.id)}/${action}`, { method: "POST", body: "{}" });
toast(action === "cancel" ? "取消请求已提交。" : "任务已重新进入流水线。", "success");
await refreshState({ quiet: true });
} catch (error) {
toast(error.message, "error");
} finally {
model.pending = false;
render();
}
}
function appendPrompt(text) {
const current = ui.instruction.value.trim();
if (current.includes(text)) return;
ui.instruction.value = current ? `${current.replace(/[。;;]?$/, "")};${text}。` : `${text}。`;
ui.instruction.focus();
updateCapabilityPreview();
}
function showModal(title, content) {
ui.modalTitle.textContent = title;
ui.modalBody.innerHTML = content;
ui.modal.hidden = false;
document.body.style.overflow = "hidden";
refreshIcons();
ui.modalClose.focus();
}
function closeModal() {
ui.modal.hidden = true;
document.body.style.overflow = "";
}
async function artifactAction(button) {
const artifact = model.snapshot.artifacts.find((item) => item.id === button.dataset.artifactId);
if (!artifact) return;
const action = button.dataset.artifactAction;
if (action === "delete") {
if (!window.confirm(`确认删除“${artifact.firmware_filename}”及其服务器发布记录吗?此操作不可恢复。`)) return;
button.disabled = true;
try {
const result = await api(`/api/firmware-workbench/artifacts/${artifact.id}`, { method: "DELETE" });
toast(result.message, "success");
await refreshState({ quiet: true });
} catch (error) { toast(error.message, "error"); }
finally { button.disabled = false; }
return;
}
if (action === "publish") {
button.disabled = true;
try {
const release = await api(`/api/firmware-workbench/artifacts/${artifact.id}/publish`, { method: "POST", body: "{}" });
toast(`${release.message}(${release.id})`, "success");
await refreshState({ quiet: true });
} catch (error) { toast(error.message, "error"); }
finally { button.disabled = false; }
return;
}
if (action === "notify") {
button.disabled = true;
try {
const result = await api(`/api/firmware-workbench/artifacts/${artifact.id}/notify`, { method: "POST", body: "{}" });
toast(`已向车端发送 ${result.published_count || result.burst_count || 3} 帧通知。`, "success");
await refreshState({ quiet: true });
} catch (error) { toast(error.message, "error"); }
finally { button.disabled = false; }
return;
}
if (action === "detail") {
showModal("固件产物详情", `<div class="detail-grid">
<div class="detail-field"><span>软件名称</span><strong>${escapeHtml(artifact.software_name)}</strong></div>
<div class="detail-field"><span>版本</span><strong>${escapeHtml(artifact.version)}</strong></div>
<div class="detail-field"><span>固件包</span><strong>${escapeHtml(artifact.firmware_filename)}</strong></div>
<div class="detail-field"><span>构建时间</span><strong>${escapeHtml(formatDate(artifact.build_time))}</strong></div>
<div class="detail-field"><span>固件包大小</span><strong>${escapeHtml(formatSize(artifact.size_bytes))}</strong></div>
<div class="detail-field"><span>发布状态</span><strong>${escapeHtml(STATUS_LABELS[artifact.status] || artifact.status)}</strong></div>
<div class="detail-field"><span>SHA-256(固件包)</span><strong>${escapeHtml(artifact.sha256)}</strong></div>
<div class="detail-field"><span>SHA-256(HEX 镜像)</span><strong>${escapeHtml(artifact.image_sha256)}</strong></div>
</div>`);
} else if (action === "capabilities") {
const diff = artifact.capability_diff || {};
showModal("能力差异", `<div class="diff-columns">
<div class="diff-panel"><h3>固件基线</h3>${(artifact.baseline_capabilities || []).map((item) => `<p>• ${escapeHtml(item)}</p>`).join("")}</div>
<div class="diff-panel added"><h3>目标固件变化</h3>${(diff.added || []).map((item) => `<p>+ ${escapeHtml(item)}</p>`).join("")}${(diff.retained || []).map((item) => `<p>✓ 保留${escapeHtml(item)}</p>`).join("") || "<p>无新增能力</p>"}</div>
</div>`);
} else if (action === "code") {
showModal("代码差异", `<pre class="code-block">${escapeHtml(artifact.code_diff || "暂无代码差异记录")}</pre>`);
} else if (action === "validation") {
showModal("验证记录", (artifact.validation_records || []).map((test) => `<div class="test-item"><span class="test-mark"><i data-lucide="check"></i></span><div class="test-copy"><strong>${escapeHtml(test.name)}</strong><span>${escapeHtml(test.summary)} · ${escapeHtml(formatDate(test.finished_at))}</span></div></div>`).join("") || `<div class="empty-inline">暂无验证记录</div>`);
}
}
ui.start.addEventListener("click", startTask);
ui.instruction.addEventListener("input", () => updateCapabilityPreview());
ui.baseline.addEventListener("change", () => updateCapabilityPreview());
ui.cancel.addEventListener("click", () => taskAction("cancel"));
ui.retry.addEventListener("click", () => taskAction("retry"));
ui.scrollLogs.addEventListener("click", () => { ui.logs.scrollTop = ui.logs.scrollHeight; });
document.querySelectorAll("[data-prompt]").forEach((button) => button.addEventListener("click", () => appendPrompt(button.dataset.prompt)));
document.querySelectorAll("[data-inspector-tab]").forEach((button) => button.addEventListener("click", () => { model.inspectorTab = button.dataset.inspectorTab; renderInspector(selectedTask()); refreshIcons(); }));
ui.artifacts.addEventListener("click", (event) => {
const button = event.target.closest("[data-artifact-action]");
if (button) artifactAction(button);
});
ui.modalClose.addEventListener("click", closeModal);
ui.modalCloseFooter.addEventListener("click", closeModal);
ui.modal.addEventListener("click", (event) => { if (event.target === ui.modal) closeModal(); });
document.addEventListener("keydown", (event) => { if (event.key === "Escape" && !ui.modal.hidden) closeModal(); });
renderPipeline(null);
renderRelease(null);
updateCapabilityPreview();
refreshIcons();
refreshState();
})();
+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

+50 -490
View File
@@ -2,496 +2,56 @@
<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>
<link rel="stylesheet" href="/firmware-workbench-entry.css?v=2" />
<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-147-video-media" defer></script>
<script src="/speech-input.js?v=sensevoice-13" defer></script>
<script type="module" src="/gesture-control.js?v=android-pad-9"></script>
<script src="/pwa.js?v=android-pad-3" defer></script>
<script src="/console-v2.js?v=local-first-79-cinema-status" defer></script>
<script src="/self-extension.js?v=nfc-persistent-57" defer></script>
<script src="/assistant-chat.js?v=codex-assistant-39-video-media" defer></script>
<script src="/energy-monitor.js?v=energy-monitor-5-performance" defer></script>
<script src="/firmware-workbench-entry.js?v=3" defer></script>
</head>
<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
+113
View File
@@ -0,0 +1,113 @@
const CACHE_VERSION = "cognitive-agent-pad-v334-video-media";
const SHELL_CACHE = `${CACHE_VERSION}-shell`;
const RUNTIME_CACHE = `${CACHE_VERSION}-runtime`;
const APP_SHELL = [
"/",
"/index.html",
"/styles.css?v=dual-seat-24-dialog-size",
"/network.js?v=android-pad-2",
"/app.js?v=dual-seat-147-video-media",
"/gesture-control.js?v=android-pad-9",
"/pwa.js?v=android-pad-3",
"/console-v2.css?v=delivery-state-69",
"/console-v2.js?v=local-first-79-cinema-status",
"/self-extension.css?v=pet-settings-54-dialog-size",
"/self-extension.js?v=nfc-persistent-57",
"/speech-input.css?v=sensevoice-7",
"/speech-input.js?v=sensevoice-13",
"/speech-audio-worklet.js?v=sensevoice-5",
"/assistant-chat.css?v=codex-assistant-30",
"/assistant-chat.js?v=codex-assistant-39-video-media",
"/energy-monitor.css?v=energy-monitor-5-dialog-size",
"/energy-monitor.js?v=energy-monitor-5-performance",
"/ui-consistency.css?v=spatial-ui-8",
"/codepen-glass-theme.css?v=creative-cloud-6-performance",
"/vue-ui.js?v=vue-presentation-4",
"/firmware-workbench-entry.css?v=2",
"/firmware-workbench-entry.js?v=3",
"/firmware-workbench.html",
"/firmware-workbench-theme.js?v=1",
"/firmware-workbench.css?v=7",
"/firmware-workbench.js?v=4",
"/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;
}
}
+2026 -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
+177
View File
@@ -0,0 +1,177 @@
import { createHash } from "node:crypto";
import {
cpSync,
copyFileSync,
existsSync,
mkdirSync,
readFileSync,
writeFileSync,
} from "node:fs";
import { basename, dirname, extname, 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",
"firmware-workbench-entry.js",
"firmware-workbench-theme.js",
"firmware-workbench.js",
"firmware-workbench.html",
"styles.css",
"console-v2.css",
"self-extension.css",
"speech-input.css",
"assistant-chat.css",
"energy-monitor.css",
"firmware-workbench-entry.css",
"firmware-workbench.css",
"ui-consistency.css",
"codepen-glass-theme.css",
"vue-ui.js"
];
const fingerprintedRuntimeFiles = runtimeFiles.filter((file) => (
/\.(?:css|js)$/.test(file)
&& !["service-worker.js", "speech-audio-worklet.js"].includes(file)
));
const rewrittenHtmlFiles = [
"index.html",
"login.html",
"hardware-control.html",
"firmware-workbench.html",
];
function contentHash(value) {
return createHash("sha256").update(value).digest("hex").slice(0, 12);
}
function hashedRuntimeName(file, hash) {
const extension = extname(file);
return `${basename(file, extension)}.${hash}${extension}`;
}
function rewriteRuntimeReferences(value, manifest) {
let rewritten = value;
for (const [source, target] of Object.entries(manifest)) {
const escaped = source.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
rewritten = rewritten.replace(
new RegExp(`/${escaped}(?:\\?[^\\s\"'<>)]*)?`, "g"),
`/${target}`,
);
}
return rewritten;
}
function fingerprintRuntimeAssets() {
const manifest = {};
for (const file of fingerprintedRuntimeFiles) {
const source = resolve(outputRoot, file);
if (!existsSync(source)) continue;
const contents = readFileSync(source);
const hashedName = hashedRuntimeName(file, contentHash(contents));
writeFileSync(resolve(outputRoot, hashedName), contents);
manifest[file] = hashedName;
}
for (const file of rewrittenHtmlFiles) {
const target = resolve(outputRoot, file);
if (!existsSync(target)) continue;
const source = readFileSync(target, "utf8");
writeFileSync(target, rewriteRuntimeReferences(source, manifest), "utf8");
}
const serviceWorkerPath = resolve(outputRoot, "service-worker.js");
const buildHash = contentHash(JSON.stringify(manifest));
if (existsSync(serviceWorkerPath)) {
const serviceWorker = rewriteRuntimeReferences(
readFileSync(serviceWorkerPath, "utf8"),
manifest,
).replace(
/const CACHE_VERSION = "[^"]+";/,
`const CACHE_VERSION = "cognitive-agent-pad-build-${buildHash}";`,
);
writeFileSync(serviceWorkerPath, serviceWorker, "utf8");
}
writeFileSync(
resolve(outputRoot, "runtime-assets.json"),
`${JSON.stringify({ build_hash: buildHash, assets: manifest }, null, 2)}\n`,
"utf8",
);
}
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);
}
fingerprintRuntimeAssets();
}
};
}
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
}
});
+171
View File
@@ -0,0 +1,171 @@
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, heading: "下发控制指令" });
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, state.heading),
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, options = {}) {
if (!(container instanceof Element) || !Array.isArray(commands) || !commands.length) {
return false;
}
const mount = commandMounts.get(container) || createCommandMount(container);
replaceReactiveArray(mount.state.commands, commands);
mount.state.heading = text(options.heading, "下发控制指令");
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

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