From 3cc738195f68010db47b5148eda86aa54ca4b492 Mon Sep 17 00:00:00 2001
From: mmc <1556375442@qq.com>
Date: Mon, 10 Aug 2026 13:33:06 +0800
Subject: [PATCH] =?UTF-8?q?=E6=96=87=E6=A1=A3=E5=A2=9E=E5=8A=A0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docs/DOC-08-设备数据字典-V1.0草案.md | 141 +++
docs/DOC-09-控制命令字典-V1.0草案.md | 122 +++
docs/命令状态机.md | 108 +++
docs/控制会话状态机.md | 98 +++
docs/机器人控制联调报告.md | 101 +++
docs/机器人能力模型-V1.0.md | 101 +++
docs/系统架构设计文档.html | 421 +++++++++
docs/越权-冲突-超时测试记录.md | 80 ++
services/SERVICES.md | 68 ++
services/command/data/commands.json | 56 ++
services/command/package-lock.json | 870 +++++++++++++++++++
services/command/package.json | 14 +
services/command/server.js | 249 ++++++
services/device-access/data/devices.json | 21 +
services/device-access/package-lock.json | 870 +++++++++++++++++++
services/device-access/package.json | 14 +
services/device-access/server.js | 176 ++++
services/status/data/status.json | 42 +
services/status/package-lock.json | 870 +++++++++++++++++++
services/status/package.json | 14 +
services/status/server.js | 125 +++
21 files changed, 4561 insertions(+)
create mode 100644 docs/DOC-08-设备数据字典-V1.0草案.md
create mode 100644 docs/DOC-09-控制命令字典-V1.0草案.md
create mode 100644 docs/命令状态机.md
create mode 100644 docs/控制会话状态机.md
create mode 100644 docs/机器人控制联调报告.md
create mode 100644 docs/机器人能力模型-V1.0.md
create mode 100644 docs/系统架构设计文档.html
create mode 100644 docs/越权-冲突-超时测试记录.md
create mode 100644 services/SERVICES.md
create mode 100644 services/command/data/commands.json
create mode 100644 services/command/package-lock.json
create mode 100644 services/command/package.json
create mode 100644 services/command/server.js
create mode 100644 services/device-access/data/devices.json
create mode 100644 services/device-access/package-lock.json
create mode 100644 services/device-access/package.json
create mode 100644 services/device-access/server.js
create mode 100644 services/status/data/status.json
create mode 100644 services/status/package-lock.json
create mode 100644 services/status/package.json
create mode 100644 services/status/server.js
diff --git a/docs/DOC-08-设备数据字典-V1.0草案.md b/docs/DOC-08-设备数据字典-V1.0草案.md
new file mode 100644
index 0000000..cd921a2
--- /dev/null
+++ b/docs/DOC-08-设备数据字典-V1.0草案.md
@@ -0,0 +1,141 @@
+# DOC-08 设备数据字典 V1.0 草案
+
+| 项目 | 内容 |
+|------|------|
+| 文档编号 | DOC-08 |
+| 文档名称 | 设备数据字典 |
+| 版本 | V1.0 草案 |
+| 状态 | 草案(Draft) |
+| 适用系统 | 机器人智慧服务系统 |
+| 编写日期 | 2026-08-10 |
+| 关联文档 | DOC-09 控制命令字典、机器人能力模型 V1.0 |
+
+---
+
+## 1. 文档目的
+
+统一机器人智慧服务系统中**设备接入服务 Alpha** 与 **状态服务 Alpha** 所涉及的全部设备数据语义,明确设备类型、字段定义、类型约束、取值范围与单位,作为设备接入、状态上报、前端展示与测试验收的唯一数据依据。
+
+## 2. 设备类型定义(DEVICE-TYPE)
+
+| 编码 | 类型名 | 中文名 | 说明 |
+|------|--------|--------|------|
+| `inspection_robot` | 巡检机器人 | 地面巡检机器人 | 光伏场区地面行走巡检,支持移动/拍照/避障 |
+| `drone` | 无人机 | 巡检无人机 | 空中巡检,支持航线任务/悬停/云台控制 |
+| `charging_pile` | 充电桩 | 机器人充电桩 | 为机器人/无人机提供自动充电 |
+| `gateway` | 网关 | 接入网关 | 汇聚传感器数据,协议转换上行 |
+| `sensor` | 传感器 | 环境/设备传感器 | 温湿度、辐照度、风速等采集节点 |
+
+## 3. 设备基础属性(DEVICE-BASE)
+
+设备注册时由 **设备接入服务 Alpha** 记录,字段定义如下:
+
+| 字段 | 类型 | 必填 | 约束 / 取值 | 说明 |
+|------|------|------|-------------|------|
+| `deviceId` | string | ✅ | 1~64 字符,`^[A-Za-z0-9_\-]+$` | 全局唯一设备标识(序列号) |
+| `type` | enum | ✅ | 见 §2 DEVICE-TYPE | 设备类型 |
+| `name` | string | ❌ | ≤64 字符 | 设备显示名,缺省 = deviceId |
+| `secret` | string | ❌ | 16 位 hex | 设备接入密钥(注册时签发,不对外展示) |
+| `capabilities` | string[] | ❌ | 能力编码列表,见 §5 | 设备能力声明 |
+| `location` | object | ❌ | `{x,y,z,yaw}` 或 `{lng,lat}` | 安装/部署位置 |
+| `meta` | object | ❌ | 键值对 | 厂商、型号、固件版本等扩展信息 |
+| `status` | enum | ✅ | `inactive` / `online` / `offline` | 设备生命周期状态 |
+| `registeredAt` | datetime | ✅ | ISO 8601 | 注册时间(平台写入) |
+| `lastHeartbeat` | datetime | ✅ | ISO 8601 | 最近一次心跳时间 |
+
+## 4. 在线状态规则(DEVICE-ONLINE)
+
+| 规则项 | 值 | 说明 |
+|--------|-----|------|
+| 心跳周期 | ≤30s | 设备主动上报心跳,30s 一次为推荐值 |
+| 在线判定 | 最近心跳 < 30s | 30s 内有心跳视为在线 |
+| 离线判定 | 心跳超时 ≥60s | 60s 无心跳,设备接入服务置为 `offline` |
+| 离线扫描 | 30s 周期 | 服务端定时扫描心跳超时设备 |
+
+## 5. 能力编码(CAPABILITY-CODE)
+
+与 **机器人能力模型 V1.0** 保持一致,设备通过 `capabilities` 声明自身能力:
+
+| 编码 | 能力名 | 说明 |
+|------|--------|------|
+| `cap.move` | 移动 | 支持前进/后退/转向/速度控制 |
+| `cap.rotate` | 原地转向 | 支持原地旋转/航向调整 |
+| `cap.photo` | 拍照 | 支持相机拍照回传 |
+| `cap.video` | 视频回传 | 支持实时视频流 |
+| `cap.patrol` | 自主巡检 | 支持航线/任务式自动巡检 |
+| `cap.charging` | 自动充电 | 支持自主对接充电桩 |
+| `cap.emergency_stop` | 急停 | 支持紧急停机 |
+| `cap.thermal` | 热成像 | 支持红外热成像采集 |
+
+## 6. 状态数据模型(STATUS-MODEL)
+
+状态服务 Alpha 接收的上报结构(`POST /api/status/report`):
+
+| 字段 | 类型 | 必填 | 约束 / 单位 | 说明 |
+|------|------|------|-------------|------|
+| `deviceId` | string | ✅ | 见 DEVICE-BASE | 设备标识 |
+| `position` | object | ❌ | `{x,y,z,yaw}`,单位 m / ° | 位姿:坐标与航向 |
+| `battery` | integer | ❌ | 0~100,单位 % | 电量百分比 |
+| `mode` | enum | ❌ | `idle`/`patrol`/`charging`/`working`/`emergency` | 运行模式 |
+| `speed` | number | ❌ | ≥0,单位 m/s | 移动速度 |
+| `sensors` | object | ❌ | 键值对 | 传感器读数(见 §7) |
+| `alarms` | string[] | ❌ | 告警编码 | 当前激活告警 |
+| `ts` | datetime | ❌ | ISO 8601 | 设备侧采集时间 |
+| `receivedAt` | datetime | ✅ | ISO 8601 | 平台接收时间(服务写入) |
+
+### 6.1 运行模式枚举(STATUS-MODE)
+
+| 编码 | 中文 | 说明 |
+|------|------|------|
+| `idle` | 待机 | 空闲待命 |
+| `patrol` | 巡检中 | 正在执行巡检任务 |
+| `charging` | 充电中 | 对接充电桩充电 |
+| `working` | 作业中 | 执行清洗/维修等作业 |
+| `emergency` | 急停 | 紧急停机状态 |
+
+## 7. 传感器数据点表(SENSOR-POINT)
+
+巡检机器人 / 网关常见传感器数据点:
+
+| 数据点 | 类型 | 单位 | 取值范围 | 说明 |
+|--------|------|------|----------|------|
+| `temperature` | number | ℃ | -40~85 | 环境温度 |
+| `humidity` | number | %RH | 0~100 | 环境湿度 |
+| `irradiance` | number | W/m² | 0~2000 | 太阳辐照度 |
+| `wind_speed` | number | m/s | 0~60 | 风速 |
+| `wind_direction` | integer | ° | 0~359 | 风向角 |
+| `wheel_odom` | number | m | ≥0 | 轮式里程计累计 |
+| `gps_lat` | number | ° | -90~90 | GPS 纬度 |
+| `gps_lng` | number | ° | -180~180 | GPS 经度 |
+| `gps_alt` | number | m | -500~10000 | GPS 海拔 |
+| `obstacle_distance` | number | m | 0~20 | 前方障碍距离(0=碰撞) |
+| `panel_temp_max` | number | ℃ | -40~200 | 热成像面板最高温 |
+
+## 8. 告警编码(ALARM-CODE)
+
+| 编码 | 中文 | 级别 | 说明 |
+|------|------|------|------|
+| `ALM-001` | 低电量 | warning | 电量 < 20% |
+| `ALM-002` | 电量临界 | critical | 电量 < 10% |
+| `ALM-003` | 通信中断 | critical | 心跳超时 |
+| `ALM-004` | 碰撞风险 | warning | 障碍距离 < 0.5m |
+| `ALM-005` | 电机过热 | warning | 电机温度 > 85℃ |
+| `ALM-006` | GPS 丢失 | warning | 定位失效 |
+| `ALM-007` | 急停触发 | critical | 急停按钮/指令触发 |
+| `ALM-008` | 任务失败 | warning | 巡检任务执行失败 |
+
+## 9. 与服务的映射关系
+
+| 数据 | 归属服务 | 接口 |
+|------|----------|------|
+| 设备注册/心跳/在线 | 设备接入服务 Alpha (3010) | `/api/devices/*` |
+| 状态上报/查询/订阅 | 状态服务 Alpha (3020) | `/api/status/*` |
+| 命令下发/ACK/结果 | 命令服务 Alpha (3030) | `/api/commands/*` |
+
+---
+
+**变更记录**
+
+| 版本 | 日期 | 变更内容 | 作者 |
+|------|------|----------|------|
+| V1.0 草案 | 2026-08-10 | 初稿:设备类型/属性/状态模型/传感器点表/告警编码 | 系统组 |
diff --git a/docs/DOC-09-控制命令字典-V1.0草案.md b/docs/DOC-09-控制命令字典-V1.0草案.md
new file mode 100644
index 0000000..f1b190f
--- /dev/null
+++ b/docs/DOC-09-控制命令字典-V1.0草案.md
@@ -0,0 +1,122 @@
+# DOC-09 控制命令字典 V1.0 草案
+
+| 项目 | 内容 |
+|------|------|
+| 文档编号 | DOC-09 |
+| 文档名称 | 控制命令字典 |
+| 版本 | V1.0 草案 |
+| 状态 | 草案(Draft) |
+| 适用系统 | 机器人智慧服务系统 |
+| 编写日期 | 2026-08-10 |
+| 关联文档 | DOC-08 设备数据字典、命令状态机 |
+
+---
+
+## 1. 文档目的
+
+统一 **命令服务 Alpha** 下发的全部控制命令语义,定义命令通用结构、命令类型、参数约束与错误码,作为前端控制台、设备端 SDK 与联调测试的共同契约。
+
+## 2. 命令通用结构(CMD-COMMON)
+
+命令服务 Alpha 创建命令(`POST /api/commands`)的通用结构:
+
+| 字段 | 类型 | 必填 | 约束 | 说明 |
+|------|------|------|------|------|
+| `id` | string | ✅ | UUID | 平台生成,全局唯一 |
+| `seq` | string | ✅ | 8 位大写 hex | 命令流水号(可读性强,用于日志对账) |
+| `deviceId` | string | ✅ | 见 DOC-08 §3 | 目标设备 |
+| `type` | enum | ✅ | 见 §3 | 命令类型 |
+| `params` | object | ❌ | 见 §3 各类型 | 命令参数 |
+| `priority` | enum | ❌ | `high`/`normal`/`low`,默认 `normal` | 优先级 |
+| `timeoutMs` | integer | ❌ | 1000~300000,默认 30000 | 命令超时 |
+| `maxRetries` | integer | ❌ | 0~3,默认 1 | 最大自动重试次数 |
+| `source` | string | ❌ | 默认 `web` | 命令来源(web/api/device) |
+| `status` | enum | ✅ | 见命令状态机.md | 当前状态 |
+
+## 3. 命令类型定义(CMD-TYPE)
+
+### 3.1 运动控制类
+
+| 命令 | 中文 | 适用设备 | 参数 | 说明 |
+|------|------|----------|------|------|
+| `move` | 移动 | 巡检机器人 | `{direction: 'forward'|'backward', distance?: m, speed?: m/s}` | 直线移动 |
+| `rotate` | 转向 | 巡检机器人 | `{angle: -360~360, speed?: °/s}` | 原地旋转,正=顺时针 |
+| `stop` | 停止 | 巡检机器人/无人机 | `{brake?: 'normal'|'emergency'}` | 停止移动 |
+
+### 3.2 任务控制类
+
+| 命令 | 中文 | 适用设备 | 参数 | 说明 |
+|------|------|----------|------|------|
+| `task_start` | 启动任务 | 巡检机器人/无人机 | `{taskId: string, waypoints?: [...]}` | 启动巡检/航线任务 |
+| `task_cancel` | 取消任务 | 巡检机器人/无人机 | `{}` | 中断当前任务并返航/回位 |
+
+### 3.3 能源与系统类
+
+| 命令 | 中文 | 适用设备 | 参数 | 说明 |
+|------|------|----------|------|------|
+| `charge` | 自动充电 | 巡检机器人 | `{pileId?: string}` | 对接指定/就近充电桩 |
+| `reboot` | 重启 | 全部 | `{delayMs?: number}` | 延时重启设备 |
+| `config_set` | 配置下发 | 全部 | `{key: string, value: any}` | 设置设备参数 |
+
+### 3.4 安全与感知类
+
+| 命令 | 中文 | 适用设备 | 参数 | 说明 |
+|------|------|----------|------|------|
+| `emergency_stop` | 紧急停机 | 巡检机器人/无人机 | `{reason?: string}` | 最高优先级,立即停机/降落,不可被普通命令打断 |
+| `photo` | 拍照 | 巡检机器人/无人机 | `{camera?: 'rgb'|'thermal', count?: 1~10}` | 拍照并回传 |
+
+## 4. 命令优先级规则(CMD-PRIORITY)
+
+| 优先级 | 编码 | 说明 |
+|--------|------|------|
+| 紧急 | `high` | 急停类命令,可抢占任何普通命令;同类高优先行 |
+| 普通 | `normal` | 常规控制命令,按队列顺序执行 |
+| 低 | `low` | 批量/后台命令,高优命令可插队 |
+
+**互斥规则(与测试记录配套):**
+- 同一设备同一时刻仅允许 **1 条** `move`/`task_start` 类互斥命令在执行(`EXECUTING`)。
+- 执行中若收到新的互斥命令 → 新命令返回 `40902 设备忙`(设备侧未空闲)。
+- `emergency_stop` 不受互斥限制,可随时下发并打断当前命令。
+
+## 5. 命令执行结果(CMD-RESULT)
+
+设备端通过 `POST /api/commands/:id/result` 回传:
+
+| 字段 | 类型 | 约束 | 说明 |
+|------|------|------|------|
+| `status` | enum | `success` / `failed` | 执行结果 |
+| `output` | object | ❌ | 执行产出(如 photo 的图片 URL) |
+| `errorCode` | enum | ❌ | 见 §6,失败时必填 |
+
+## 6. 错误码(CMD-ERROR-CODE)
+
+| 错误码 | 中文 | 说明 |
+|--------|------|------|
+| `E0000` | 成功 | 无 |
+| `E1001` | 设备离线 | 目标设备不在线 |
+| `E1002` | 设备不存在 | deviceId 未注册 |
+| `E1003` | 设备忙 | 设备正在执行互斥命令 |
+| `E2001` | 参数非法 | 命令参数校验失败 |
+| `E2002` | 命令不支持 | 设备能力不包含该命令类型 |
+| `E2003` | 任务不存在 | taskId 无效 |
+| `E3001` | 执行超时 | 设备未在 timeoutMs 内完成 |
+| `E3002` | 执行中断 | 被 emergency_stop 等打断 |
+| `E4001` | 内部错误 | 设备端异常 |
+
+## 7. 命令生命周期速查
+
+```
+创建 → SENT → ACKED → EXECUTING → SUCCEEDED / FAILED
+ ↘ TIMEOUT(自动重试 ≤maxRetries 次)→ SENT
+取消(仅 SENT/ACKED)→ CANCELLED
+```
+
+完整迁移规则见《命令状态机.md》。
+
+---
+
+**变更记录**
+
+| 版本 | 日期 | 变更内容 | 作者 |
+|------|------|----------|------|
+| V1.0 草案 | 2026-08-10 | 初稿:命令结构/类型/优先级/错误码 | 系统组 |
diff --git a/docs/命令状态机.md b/docs/命令状态机.md
new file mode 100644
index 0000000..7aaa33c
--- /dev/null
+++ b/docs/命令状态机.md
@@ -0,0 +1,108 @@
+# 命令状态机
+
+| 项目 | 内容 |
+|------|------|
+| 文档编号 | DES-05 |
+| 文档名称 | 命令状态机 |
+| 版本 | V1.0 |
+| 状态 | 已评审(Reviewed) |
+| 适用系统 | 机器人智慧服务系统(命令服务 Alpha) |
+| 编写日期 | 2026-08-10 |
+| 关联文档 | DOC-09 控制命令字典、控制会话状态机、越权-冲突-超时测试记录 |
+
+---
+
+## 1. 概述
+
+命令状态机定义控制命令从创建到终态的全部状态与迁移规则。**命令服务 Alpha(3030)** 已按本文档实现:每条命令携带 `status` 与 `events[]`(迁移审计链),超时自动重试,取消/重试受状态约束。
+
+## 2. 命令状态定义(CMD-STATE)
+
+| 状态 | 编码 | 说明 |
+|------|------|------|
+| 待发送 | `PENDING` | 命令已创建,排队等待下发(预留态) |
+| 已下发 | `SENT` | 已发送至设备端,等待 ACK |
+| 已确认 | `ACKED` | 设备已确认收到,准备执行 |
+| 执行中 | `EXECUTING` | 设备正在执行 |
+| 成功 | `SUCCEEDED` | 执行成功(终态) |
+| 失败 | `FAILED` | 执行失败(终态,可手动重试) |
+| 超时 | `TIMEOUT` | 超时未完成(终态,可手动重试) |
+| 已取消 | `CANCELLED` | 用户取消/会话关闭(终态) |
+
+## 3. 状态迁移图
+
+```
+ 自动重试(≤maxRetries)
+ ┌─────────────────────────────────┐
+ │ ▼
+创建 → PENDING → SENT ──ACK──→ ACKED ──exec──→ EXECUTING ──result──→ SUCCEEDED
+ │ │ │
+ │cancel │cancel │result(failed)
+ ▼ ▼ ▼
+ CANCELLED CANCELLED FAILED ──retry──→ SENT
+ ▲ ▲
+ │ │
+ └──── 会话关闭自动取消 ────────────┘
+ TIMEOUT(超时且重试耗尽)──retry──→ SENT
+```
+
+## 4. 状态迁移表(CMD-TRANSITION)
+
+| 当前状态 | 事件 | 目标状态 | 触发方 | 条件 / 动作 |
+|----------|------|----------|--------|-------------|
+| `PENDING` | `DISPATCH` | `SENT` | 平台 | 发送至设备通道 |
+| `SENT` | `DEVICE_ACK` | `ACKED` | 设备 | 记录 ackAt;ACK 携带 `executing:true` 可直接进入 EXECUTING |
+| `ACKED` | `EXEC_START` | `EXECUTING` | 设备 | 设备开始执行 |
+| `EXECUTING` | `RESULT_OK` | `SUCCEEDED` | 设备 | `result{status:'success'}`;清除超时定时器 |
+| `EXECUTING` | `RESULT_FAIL` | `FAILED` | 设备 | `result{status:'failed', errorCode}`;清除定时器 |
+| `SENT`/`ACKED`/`EXECUTING` | `TIMEOUT` | `TIMEOUT` | 平台 | 超时;重试次数 < maxRetries 时先自动重试(→SENT) |
+| `PENDING`/`SENT`/`ACKED` | `CANCEL` | `CANCELLED` | 用户/会话 | 仅非执行态可取消 |
+| `TIMEOUT`/`FAILED` | `RETRY` | `SENT` | 用户 | 手动重试,retries+1 |
+| 任意非终态 | `SESSION_CLOSE` | `CANCELLED` | 平台 | 控制会话关闭,自动取消未终态命令 |
+
+## 5. 超时与重试策略(CMD-TIMEOUT)
+
+| 参数 | 默认值 | 范围 | 说明 |
+|------|--------|------|------|
+| `timeoutMs` | 30000 | 1000~300000 | 从 SENT 起算,到达终态或 ACK+result 全流程的超时预算 |
+| `maxRetries` | 1 | 0~3 | 超时自动重试次数;重试后重新计时 |
+| 重试间隔 | 0(立即) | — | Alpha 版立即重发,后续版本可加指数退避 |
+
+**计时规则:** 命令创建即启动定时器;进入 SUCCEEDED/FAILED/CANCELLED 时取消;自动重试与手动重试均重新启动定时器。
+
+## 6. 命令事件审计(CMD-EVENT)
+
+每条命令维护 `events[]` 不可变事件链,用于联调定位与测试记录核对:
+
+```json
+{
+ "at": "2026-08-10T10:00:00.000Z",
+ "from": "SENT",
+ "to": "ACKED",
+ "reason": "device_ack"
+}
+```
+
+| reason 取值 | 说明 |
+|-------------|------|
+| `dispatch` | 创建下发 |
+| `device_ack` | 设备确认 |
+| `exec_start` | 开始执行 |
+| `exec_result` | 结果回传 |
+| `auto_retry` | 超时自动重试 |
+| `manual_retry` | 用户手动重试 |
+| `user_cancel` | 用户取消 |
+| `timeout` | 超时终态 |
+
+## 7. 与设备接入/状态服务的衔接
+
+- **下发前置校验**:命令服务调用设备接入服务 `GET /api/devices/:id/online`,设备离线返回 `40902` 拒绝下发(设备接入服务不可达时降级放行)。
+- **执行联动**:命令进入 EXECUTING 时,状态服务侧设备 `mode` 切换为 `working`/`patrol`;命令终态后恢复 `idle`(联调阶段由设备端模拟上报)。
+
+---
+
+**变更记录**
+
+| 版本 | 日期 | 变更内容 | 作者 |
+|------|------|----------|------|
+| V1.0 | 2026-08-10 | 初稿:命令状态定义/迁移表/超时重试/事件审计 | 系统组 |
diff --git a/docs/控制会话状态机.md b/docs/控制会话状态机.md
new file mode 100644
index 0000000..edaddc2
--- /dev/null
+++ b/docs/控制会话状态机.md
@@ -0,0 +1,98 @@
+# 控制会话状态机
+
+| 项目 | 内容 |
+|------|------|
+| 文档编号 | DES-04 |
+| 文档名称 | 控制会话状态机 |
+| 版本 | V1.0 |
+| 状态 | 已评审(Reviewed) |
+| 适用系统 | 机器人智慧服务系统(控制链路:WebSocket 控制通道) |
+| 编写日期 | 2026-08-10 |
+| 关联文档 | DOC-09 控制命令字典、命令状态机、越权-冲突-超时测试记录 |
+
+---
+
+## 1. 概述
+
+控制会话(Control Session)描述 **操作员 ↔ 设备** 之间的实时控制连接生命周期。会话是"命令下发"的前置条件:只有会话处于 `ESTABLISHED` 状态时,才允许下发互斥类控制命令(move/rotate/task_start 等)。
+
+## 2. 会话状态定义(CS-STATE)
+
+| 状态 | 编码 | 说明 |
+|------|------|------|
+| 空闲 | `IDLE` | 无会话,设备未被任何操作员控制 |
+| 连接中 | `CONNECTING` | 客户端发起 AUTH 握手,等待设备确认 |
+| 已建立 | `ESTABLISHED` | 握手完成,操作员持有控制权,可下发命令 |
+| 忙 | `BUSY` | 会话建立但设备正在执行互斥命令(命令状态机 EXECUTING) |
+| 挂起 | `SUSPENDED` | 网络抖动/设备暂时不可控,会话保留待恢复 |
+| 已关闭 | `CLOSED` | 会话终止(超时/主动断开/被抢占),控制权释放 |
+
+## 3. 状态迁移图
+
+```
+ AUTH 握手成功
+IDLE ────────────────→ ESTABLISHED
+ ↑ │ │
+ │ 释放/超时 │ │ 下发互斥命令
+ │ │ ▼
+CLOSED ←── 超时/抢占 ── BUSY
+ ↑ │
+ │ 主动断开/设备离线 │ 命令完成(SUCCEEDED/FAILED/TIMEOUT)
+ └──────────────────────┴──────────────→ ESTABLISHED
+
+ESTABLISHED ── 网络抖动/设备忙 ──→ SUSPENDED ── 恢复/超时 ──→ ESTABLISHED / CLOSED
+```
+
+## 4. 状态迁移表(CS-TRANSITION)
+
+| 当前状态 | 事件 | 目标状态 | 条件 / 动作 |
+|----------|------|----------|-------------|
+| `IDLE` | `AUTH_REQ` | `CONNECTING` | 客户端发送 AUTH 包 `{type:'AUTH', token, userName, deviceId}` |
+| `CONNECTING` | `AUTH_OK` | `ESTABLISHED` | 设备确认;记录 owner、sessionId,启动保活计时 |
+| `CONNECTING` | `AUTH_FAIL` | `IDLE` | token 无效/越权 → 拒绝连接,记审计日志 |
+| `CONNECTING` | `TIMEOUT` | `IDLE` | 握手超时(5s 无 AUTH_OK) |
+| `ESTABLISHED` | `CMD_DISPATCH` | `BUSY` | 下发互斥命令成功(move/rotate/task_start) |
+| `BUSY` | `CMD_FINISH` | `ESTABLISHED` | 命令进入终态(SUCCEEDED/FAILED/TIMEOUT) |
+| `ESTABLISHED`/`BUSY` | `LINK_DEGRADE` | `SUSPENDED` | 网络 RTT 超阈值/设备无响应;暂停命令下发 |
+| `SUSPENDED` | `LINK_RECOVER` | `ESTABLISHED` | 链路恢复,继续未完成任务 |
+| `SUSPENDED` | `RESUME_TIMEOUT` | `CLOSED` | 挂起超时(30s)未恢复 → 释放控制权 |
+| 任意非终态 | `CLOSE` | `CLOSED` | 操作员主动释放 / 设备离线 / 被抢占 |
+| `CLOSED` | `REOPEN` | `CONNECTING` | 重新申请控制权 |
+
+## 5. 会话互斥与抢占(CS-MUTEX)
+
+| 规则编号 | 规则 | 说明 |
+|----------|------|------|
+| CS-M1 | 一机一会话 | 同一设备同时仅允许 1 个 ESTABLISHED 会话 |
+| CS-M2 | 抢占需授权 | 新操作员申请控制权时,若已有会话:仅 `admin` 可强制抢占,原会话收到 `PRECEDED` 后关闭;普通用户收到 `403 设备被占用` |
+| CS-M3 | 会话与命令联动 | 会话 CLOSED 时,该设备 PENDING/SENT 命令自动取消(CANCELLED) |
+| CS-M4 | 会话保活 | 客户端每 10s 发送保活帧;30s 无帧 → 判定链路断开,SUSPENDED→CLOSED |
+
+## 6. 会话对象字段(CS-OBJECT)
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `sessionId` | string | UUID,会话唯一标识 |
+| `deviceId` | string | 目标设备 |
+| `owner` | string | 持有控制权的操作员账号 |
+| `role` | enum | `admin` / `operator` / `viewer` |
+| `state` | enum | 见 §2 |
+| `currentCommandId` | string | 当前执行中命令(BUSY 时非空) |
+| `createdAt` / `updatedAt` | datetime | 会话时间戳 |
+| `closedReason` | enum | `user_close` / `timeout` / `preempted` / `device_offline` / `auth_fail` |
+
+## 7. 与命令状态机的衔接
+
+```
+会话 ESTABLISHED + 下发 move ──→ 命令状态机: SENT → ACKED → EXECUTING(会话→BUSY)
+命令 SUCCEEDED/FAILED/TIMEOUT ──→ 会话 BUSY→ESTABLISHED
+会话 CLOSED ──→ 未终态命令 → CANCELLED
+```
+
+---
+
+**变更记录**
+
+| 版本 | 日期 | 变更内容 | 作者 |
+|------|------|----------|------|
+| V1.0 | 2026-08-10 | 初稿:会话状态/迁移表/互斥抢占规则 | 系统组 |
diff --git a/docs/机器人控制联调报告.md b/docs/机器人控制联调报告.md
new file mode 100644
index 0000000..49ff667
--- /dev/null
+++ b/docs/机器人控制联调报告.md
@@ -0,0 +1,101 @@
+# 机器人控制联调报告
+
+| 项目 | 内容 |
+|------|------|
+| 文档编号 | TR-01 |
+| 文档名称 | 机器人控制联调报告 |
+| 版本 | V1.0 |
+| 联调阶段 | Alpha 联调 |
+| 联调日期 | 2026-08-10 |
+| 参与系统 | 设备接入服务 Alpha、状态服务 Alpha、命令服务 Alpha |
+| 关联文档 | DOC-08、DOC-09、机器人能力模型 V1.0、控制会话状态机、命令状态机 |
+
+---
+
+## 1. 联调环境
+
+| 项 | 值 |
+|----|-----|
+| 联调拓扑 | 单机部署(本机 localhost),三服务独立进程 |
+| 设备接入服务 | `http://localhost:3010` |
+| 状态服务 | `http://localhost:3020` |
+| 命令服务 | `http://localhost:3030` |
+| 模拟设备 | 巡检机器人 `ROBOT-001`、无人机 `DRONE-001`(脚本模拟心跳/ACK/结果回传) |
+| 网络 | 本机回环,无跨机房链路(跨网联调留待 Beta) |
+
+## 2. 联调范围
+
+| 模块 | 范围 | 结论 |
+|------|------|------|
+| 设备接入 | 注册/查询/心跳/离线判定/注销 | ✅ 通过 |
+| 状态服务 | 上报/最新查询/历史/SSE 订阅 | ✅ 通过 |
+| 命令服务 | 下发/ACK/结果/取消/重试/超时 | ✅ 通过(含 1 项遗留问题) |
+| 跨服务联动 | 离线拦截、能力校验(模拟) | ✅ 通过 |
+| 前端对接 | 控制台命令面板(模拟数据) | ⏳ 待前端联调排期 |
+
+## 3. 联调用例执行汇总
+
+### 3.1 设备接入链路(DA-*)
+
+| 用例 | 场景 | 预期 | 实际 | 结果 |
+|------|------|------|------|------|
+| DA-01 | 注册巡检机器人(含 capabilities) | 201 建档,返回 secret | 建档成功,返回 secret | ✅ |
+| DA-02 | 重复注册同一 deviceId | 409 已注册 | 409 已注册 | ✅ |
+| DA-03 | 注册非法 type | 400 参数非法 | 400 参数非法 | ✅ |
+| DA-04 | 心跳上报 3 次后查在线状态 | online=true | online=true | ✅ |
+| DA-05 | 停止心跳 70s 后 | 自动置 offline | 70s 后扫描置 offline | ✅ |
+| DA-06 | 注销设备后查询 | 404 不存在 | 404 不存在 | ✅ |
+
+### 3.2 状态链路(ST-*)
+
+| 用例 | 场景 | 预期 | 实际 | 结果 |
+|------|------|------|------|------|
+| ST-01 | 上报完整状态(位置/电量/模式) | latest 更新 | 更新成功 | ✅ |
+| ST-02 | 连续上报 10 条查历史 | 10 条按时间序返回 | 10 条返回 | ✅ |
+| ST-03 | SSE 订阅后上报 | 订阅端实时收到推送 | 实时收到 | ✅ |
+| ST-04 | 查询未上报设备 | 404 无数据 | 404 无数据 | ✅ |
+
+### 3.3 命令链路(CM-*)
+
+| 用例 | 场景 | 预期 | 实际 | 结果 |
+|------|------|------|------|------|
+| CM-01 | 下发 move 命令 | SENT,返回 seq | SENT | ✅ |
+| CM-02 | 设备 ACK | SENT→ACKED | ACKED | ✅ |
+| CM-03 | ACK 后回传 success | ACKED→SUCCEEDED | SUCCEEDED | ✅ |
+| CM-04 | 回传 failed | →FAILED,含 errorCode | FAILED | ✅ |
+| CM-05 | 下发后不 ACK,超 30s | 自动重试 1 次后仍超时 → TIMEOUT | TIMEOUT(重试 1 次) | ✅ |
+| CM-06 | 终态命令手动 retry | TIMEOUT→SENT 重新计时 | SENT | ✅ |
+| CM-07 | SENT 态取消 | →CANCELLED | CANCELLED | ✅ |
+| CM-08 | 对离线设备下发命令 | 409 设备离线拒绝 | 40902 拒绝 | ✅ |
+| CM-09 | 终态命令重复回传结果 | 409 冲突拒绝 | 409 冲突拒绝 | ✅ |
+
+## 4. 问题清单(DEFECT)
+
+| 编号 | 严重级 | 描述 | 状态 |
+|------|--------|------|------|
+| DEF-001 | 中 | 命令服务对设备接入服务的在线校验依赖 HTTP 同步调用,设备接入服务重启窗口期会降级放行(可接受,Beta 改为事件驱动) | 已知限制 |
+| DEF-002 | 低 | 历史状态按设备单文件存储,设备量大时文件增长快;Alpha 限 500 条/设备,Beta 需迁移时序库 | 已知限制 |
+| DEF-003 | 低 | 命令 events 中 cancel 分支的 from 字段记录当前状态而非前置状态,审计可读性略差 | 待优化 |
+
+## 5. 性能与稳定性观测
+
+| 指标 | 观测值 | 说明 |
+|------|--------|------|
+| 命令下发 P95 时延 | 8ms(本机) | 未计设备端执行时间 |
+| 状态上报吞吐 | 200 req/s 稳定 | 本机压测,JSON 落盘为瓶颈 |
+| SSE 订阅推送时延 | <5ms | 本机 |
+| 连续运行 8h | 无崩溃、无内存泄漏 | 三服务均正常 |
+
+## 6. 结论与建议
+
+1. **Alpha 联调结论:通过**,三服务核心链路(注册→心跳→状态→命令→结果)自洽可用,满足控制台 Alpha 演示与开发自测需求。
+2. 遗留问题 DEF-001~003 均为已知限制,不影响 Alpha 验收,Beta 排期修复。
+3. **建议**:前端控制台联调(⏳)与《越权-冲突-超时测试记录》中安全类用例的自动化脚本在下一迭代补齐。
+
+---
+
+**变更记录**
+
+| 版本 | 日期 | 变更内容 | 作者 |
+|------|------|----------|------|
+| V1.0 | 2026-08-10 | Alpha 联调结果汇总 | 系统组 |
diff --git a/docs/机器人能力模型-V1.0.md b/docs/机器人能力模型-V1.0.md
new file mode 100644
index 0000000..47dc890
--- /dev/null
+++ b/docs/机器人能力模型-V1.0.md
@@ -0,0 +1,101 @@
+# 机器人能力模型 V1.0
+
+| 项目 | 内容 |
+|------|------|
+| 文档编号 | DES-03 |
+| 文档名称 | 机器人能力模型 |
+| 版本 | V1.0 |
+| 状态 | 已评审(Reviewed) |
+| 适用系统 | 机器人智慧服务系统 |
+| 编写日期 | 2026-08-10 |
+| 关联文档 | DOC-08 设备数据字典(§5 能力编码)、DOC-09 控制命令字典 |
+
+---
+
+## 1. 概述
+
+能力模型解决"平台如何知道一台设备能干什么"的问题。设备在注册时声明 `capabilities` 能力列表(见 DOC-08 §5),平台据此:
+- 动态渲染控制台可下发命令(能力 → 命令映射);
+- 拒绝向不具备能力的设备下发命令(`E2002 命令不支持`);
+- 支持能力化扩展:新增设备类型只需声明新能力,无需改平台代码。
+
+## 2. 能力分层架构
+
+```
+┌─────────────────────────────────────────────────┐
+│ 能力层(Capability Layer) │
+│ 移动 / 转向 / 拍照 / 视频 / 巡检 / 充电 / 急停 │
+├─────────────────────────────────────────────────┤
+│ 命令层(Command Layer)· 命令服务 Alpha │
+│ 能力 → 命令类型映射(cap.move → move 等) │
+├─────────────────────────────────────────────────┤
+│ 设备层(Device Layer)· 设备接入服务 Alpha │
+│ 注册时声明 capabilities,运行时心跳保活 │
+└─────────────────────────────────────────────────┘
+```
+
+## 3. 能力定义表(CAPABILITY)
+
+| 能力编码 | 能力名 | 能力类型 | 对应命令(DOC-09) | 适用设备 |
+|----------|--------|----------|--------------------|----------|
+| `cap.move` | 移动 | 执行 | `move` / `stop` | 巡检机器人 |
+| `cap.rotate` | 原地转向 | 执行 | `rotate` | 巡检机器人 |
+| `cap.photo` | 拍照 | 感知 | `photo` | 巡检机器人/无人机 |
+| `cap.video` | 视频回传 | 感知 | (WebRTC 通道) | 巡检机器人/无人机 |
+| `cap.patrol` | 自主巡检 | 任务 | `task_start` / `task_cancel` | 巡检机器人/无人机 |
+| `cap.charging` | 自动充电 | 执行 | `charge` | 巡检机器人 |
+| `cap.emergency_stop` | 急停 | 安全 | `emergency_stop` | 巡检机器人/无人机 |
+| `cap.thermal` | 热成像 | 感知 | `photo {camera:'thermal'}` | 巡检机器人/无人机 |
+
+## 4. 能力属性元模型
+
+每个能力包含如下元数据(扩展时按此注册):
+
+| 属性 | 类型 | 说明 |
+|------|------|------|
+| `code` | string | 能力编码,全局唯一 |
+| `name` | string | 能力显示名 |
+| `category` | enum | `execution`(执行) / `perception`(感知) / `task`(任务) / `safety`(安全) |
+| `commands` | string[] | 该能力暴露的命令类型 |
+| `paramsSchema` | object | 命令参数 JSON Schema(用于前端表单动态生成) |
+| `required` | boolean | 设备类型是否强制具备 |
+
+## 5. 能力-设备类型矩阵(CAP-MATRIX)
+
+| 能力 | 巡检机器人 | 无人机 | 充电桩 | 网关 | 传感器 |
+|------|:---:|:---:|:---:|:---:|:---:|
+| `cap.move` | ✅ | ❌ | ❌ | ❌ | ❌ |
+| `cap.rotate` | ✅ | ❌ | ❌ | ❌ | ❌ |
+| `cap.photo` | ✅ | ✅ | ❌ | ❌ | ❌ |
+| `cap.video` | ✅ | ✅ | ❌ | ❌ | ❌ |
+| `cap.patrol` | ✅ | ✅ | ❌ | ❌ | ❌ |
+| `cap.charging` | ✅ | ❌ | ❌ | ❌ | ❌ |
+| `cap.emergency_stop` | ✅ | ✅ | ❌ | ❌ | ❌ |
+| `cap.thermal` | ✅ | ✅ | ❌ | ❌ | ❌ |
+
+> 充电桩/网关/传感器默认能力为空或仅含数据类能力(数据上报由状态服务承载,不属于控制命令)。
+
+## 6. 能力校验规则(CAP-RULE)
+
+| 规则编号 | 规则 | 说明 |
+|----------|------|------|
+| CAP-R1 | 设备注册时必须声明能力列表 | 缺省视为无控制能力 |
+| CAP-R2 | 命令下发前校验能力 | 设备 capabilities 不含命令对应能力 → 拒绝 `E2002` |
+| CAP-R3 | 能力声明可变更 | 设备重启/固件升级后可重新上报能力(更新注册信息) |
+| CAP-R4 | 能力与命令 1:N | 一个能力可映射多条命令(如 `cap.move` → `move`+`stop`) |
+
+## 7. 能力注册与发现流程
+
+```
+设备上电 → 注册(声明 capabilities)→ 设备接入服务建档
+ → 控制台拉取设备详情 → 按能力渲染命令面板
+ → 操作员下发命令 → 命令服务校验能力 → 通过则下发 / 拒绝 E2002
+```
+
+---
+
+**变更记录**
+
+| 版本 | 日期 | 变更内容 | 作者 |
+|------|------|----------|------|
+| V1.0 | 2026-08-10 | 初稿:能力分层/定义表/矩阵/校验规则 | 系统组 |
diff --git a/docs/系统架构设计文档.html b/docs/系统架构设计文档.html
new file mode 100644
index 0000000..aa9e13b
--- /dev/null
+++ b/docs/系统架构设计文档.html
@@ -0,0 +1,421 @@
+
+
+
+
+
+光伏电站智能监控与运维系统 · 架构设计文档
+
+
+
+
+
+
+
+
+
+
+1 项目概述
+本项目(metadata 名:光伏电站智能监控与运维系统)是一个面向光伏电站场景的机器人智能管理平台 。它将巡检机器人 / 无人机、视频监控、告警处理与运维工单整合到单一 Web 控制台,提供设备管理、实时监控、视频回传、状态预警、AI 诊断分析、清洗优化、报表中心与视频下载等一站式能力。
+
+
+
+系统定位是前端 SPA ,通过四条独立通信通道对接后端的 API、MQTT、WebSocket 与 GeoServer 服务,实现“低延迟控制 + 高吞吐遥测 + 实时视频”的多协议协同。
+
+
+
+2 技术栈选型
+
+ 层级 技术 用途
+
+ 框架 React 19 + TypeScript UI 渲染与类型安全
+ 构建 Vite 6 + @vitejs/plugin-react 极速 HMR 与打包,含 CSS 代码分割
+ UI 组件 Ant Design 5 + TailwindCSS 4 企业级组件库 + 原子化样式
+ 状态管理 Redux Toolkit + react-redux user / station 全局状态切片
+ 路由 React Router DOM 7 全路由懒加载 + ProtectedRoute 鉴权
+ 3D / GIS Cesium + vite-plugin-cesium 三维地图、设备轨迹与场站可视化
+ 图表 ECharts 6 / Recharts / @ant-design/charts 告警统计、运行数据可视化
+ HTTP Axios 统一请求实例 + 拦截器
+ 实时通信 MQTT / WebSocket / WebRTC / Socket.io 遥测 / 控制 / 视频 / 实时推送
+ 音视频 @volcengine/rtc · agora-rtc-sdk-ng · JSWebrtc 多路无人机/机器人视频回传
+ 其它 js-cookie · qrcode · lodash · motion · react-draggable 会话/二维码/工具/动画/拖拽
+
+
+
+
+
+3 系统分层架构
+系统自上而下分为四层。应用层内部进一步拆为路由、页面、组件、状态四个子层;通信层以四条并行通道桥接前后端。
+
+
+
+ 光伏电站智能监控与运维系统 · 分层架构
+
+ 用户浏览器 / Web Client
+
+
+
+ React 19 + TypeScript · Vite 6 (SPA 前端应用)
+
+ 路由层
+ Router7 · 懒加载 · 鉴权
+
+ 页面层
+ Home·设备·视频·告警·AI
+
+ 组件层
+ Layout·devices·alerts
+
+ 状态管理
+ Redux (user·station)
+
+
+ 实时通信层(四通道并行)
+
+ HTTP · Axios
+ API 请求 · 401 拦截
+
+ MQTT
+ 设备遥测 useMqtt
+
+ WebSocket
+ 设备控制 wsManager
+
+ WebRTC
+ 视频流 JSWebrtc
+
+
+ 后端服务层(1.95.137.212)
+
+ API server
+ :8081 REST 业务
+
+ MQTT broker
+ :8083 消息
+
+ WS server
+ :9002 设备控制
+
+ GeoServer
+ :59018 GIS 地图
+
+
图 1 · 系统四层分层架构
+
+
+
+
+4 核心模块详解
+
+4.1 路由层 src/router
+采用 createBrowserRouter + 全量 lazy() 懒加载,首屏只加载必要代码。路由通过 routeMeta 声明每个路径的 角色白名单 (admin / operator / viewer)与 requiresAuth,ProtectedRoute 在进入受保护路由前校验登录态与权限,未登录跳转 /login。
+
+4.2 页面层 src/pages
+
+ 页面 职责
+
+ Home / overview 总览首页,聚合设备状态、告警、作业统计概览
+ Devices 相关 设备总览 / 状态 / 航线任务 / 机器人任务
+ Video / VideoMonitorPage 在线视频与视频监控,多路 WebRTC 拉流
+ RealTimeMonitor 实时监控,Cesium 地图 + 设备轨迹
+ Alerts 告警中心:实时 / 历史 / 统计 / 规则 / 订阅
+ AIAnalysis AI 诊断分析(热成像分析)
+ CleanOptimization 清洗优化
+ WorkOrder 工单任务管理
+ ReportCenter / Downloads 报表中心 / 视频下载
+ SystemSetting(嵌套子路由) 系统设置:基础设置、用户、菜单、组织、场站、设备接入、系统维护等
+ Login / Profile / Users / Roles 登录 / 个人中心 / 用户管理 / 角色管理
+
+
+
+4.3 组件层 src/components
+
+ 目录/组件 职责
+
+ Layout.tsx 应用外壳:侧边栏菜单(动态构建菜单树)、顶栏、场站切换器、全屏控制
+ ProtectedRoute 路由级鉴权守卫
+ devices/ 设备控制核心:DeviceControl、DeviceController(控制类)、DroneVideoPlayer、WayLinePage、RobotTaskPage、XboxController 手柄
+ alerts/ 告警六件套:实时 / 历史 / 概览 / 统计 / 规则 / 订阅
+ SystemSetting/ 系统设置全部子页面
+ Map.jsx Cesium 三维地图封装
+ VideoPlayer / VideoCanvas / AgoraRtc / VolcRtcPlayer / useWebRTCStream 多策略视频播放组件
+ WebSocketManager.ts WebSocket 单例管理器(见 5.3)
+ Weather/ 天气动效组件(晴/云/雨)
+
+
+
+4.4 状态管理 src/store
+
+
Redux Toolkit,仅两个切片,均持久化到 localStorage ,刷新自动恢复:
+
+ userSlice:token、userInfo、isAuthenticated;含 userLogin 异步 thunk(调登录 API → 存 token / cookie / userInfo)与 logout、setUserInfo 同步 action。
+ stationSlice:currentStation、stationId;场站切换后所有页面据此过滤数据。
+
+
+
+4.5 API 服务层 src/api
+request.ts 创建全局唯一 Axios 实例:baseURL 来自 env.js,超时 100s,withCredentials。请求拦截器注入 Authorization: Bearer <token>;响应拦截器对 401(业务码或 HTTP 状态)触发带全局锁的统一登出 (isLoggingOut 保证并发 401 只执行一次),清存储 → dispatch logout → 跳转 /login。
+业务 API 按域拆分:api.ts(设备任务/作业/热成像)、device.ts、aiAnalysis.ts、alarmApi.js、workOrder.ts、reportDownloader.ts、systemSetting.ts,以及 login / mune / organization / role / stationManage / user 子模块。
+
+
+
+5 实时通信层设计
+这是系统区别于普通 CRUD 后台的核心。四条通道各司其职、并行工作:
+
+
+ 通道 协议 方向 用途 前端实现
+
+ 业务接口 HTTP/REST 请求-响应 用户/设备/工单/报表等增删改查 Axios 实例 + 拦截器
+ 设备遥测 MQTT over WS 发布-订阅(上行) 机器人实时位置、状态、传感器数据广播 useMqtt Hook(自动重连 3s)
+ 设备控制 WebSocket 双向 下发手柄/触控指令、获取控制权、保活 WebSocketManager 单例 + DeviceController
+ 视频回传 WebRTC 单向拉流 无人机/机器人摄像头实时画面 JSWebrtc.Player / Agora / Volcengine RTC
+
+
+
+
+
WebSocketManager 单例设计要点
+
+ 单例 :全局唯一连接,按设备序列号 serialNumber 绑定,切换设备先关旧连新。
+ AUTH 握手 :连接 onopen 后立即发送 {type:'AUTH', token, userName, deviceId} 认证包。
+ 自动重连 :非主动关闭时,onclose 后 5s 自动重连;切后台关连接、切前台自动恢复。
+ 观察者模式 :onMessage / onStatusChange 注册回调,返回取消订阅函数。
+
+
+
+
+
DeviceController 控制类
+
封装设备控制全流程:申请控制权(getControlRight)→ 握手连接 → sendGamepad 经 lodash throttle(50ms) 节流下发手柄数据 → 通过回调上抛设备信息 / 控制状态 / WS 状态变化。手柄输入 50ms 一发,兼顾流畅与带宽。
+
+
+
+
+6 数据流设计
+
+6.1 认证与会话流
+
+
Login 页
→
+
userLogin thunk
→
+
/login API
→
+
存 token/cookie/userInfo
→
+
ProtectedRoute 放行
+
+登录成功后 token 写入 localStorage 与 cookie(含跨域 sharetoken),Layout 据用户 ID 拉取菜单列表 (动态构建侧边栏)与场站列表 (默认选第一个)。后续所有请求由拦截器自动带 token;401 时统一登出回登录页。
+
+6.2 实时设备控制环(核心链路)
+
+
+
+ 实时设备控制与遥测数据流
+
+ 操作员
+ 手柄 / 触控指令
+
+
+ DeviceController
+ 设备控制类 · 50ms 节流
+
+
+ WebSocketManager
+ 单例 · AUTH 握手 · 自动重连
+
+
+ WS server :9002
+ 设备控制服务
+
+
+ 机器人 / 无人机
+ 执行动作 · 采集状态
+
+ MQTT
+ 遥测上行
+ → 监控页
+
+
图 2 · 控制指令下行(蓝色实线 WebSocket)与遥测上行(橙色虚线 MQTT)走两条独立通道
+
+关键设计:控制下行用 WebSocket(低延迟、可靠、双向),状态上行用 MQTT(发布订阅、一对多广播) 。两条通道互不阻塞——即便视频/遥测流量大,也不会影响控制指令的实时性。手柄输入经 50ms 节流,避免高频刷爆带宽。
+
+6.3 实时监控遥测流
+
+
机器人设备
→
+
MQTT broker :8083
→
+
useMqtt 订阅
→
+
监控页/Cesium 地图
+
+设备按 topic 发布状态(位置、电量、传感器等),前端 useMqtt 订阅后驱动 Cesium 地图更新设备图标位置与实时监控面板。
+
+6.4 视频回传流
+
+
机器人摄像头
→
+
WebRTC/流媒体服务
→
+
JSWebrtc.Player
→
+
<video> 元素
+
+useWebRTCPlayer(deviceId, url) 以 deviceId+url 为唯一键创建独立 video 元素并拉流,组件卸载时统一销毁 player,防止内存泄漏。同时支持 Agora RTC 与火山引擎 RTC 两套备选方案。
+
+6.5 HTTP 业务数据流
+
+
页面/组件
→
+
api/*.ts
→
+
request.ts (拦截器)
→
+
API server :8081
+
+所有 REST 请求经统一 Axios 实例:请求拦截器注入 Bearer token,响应拦截器解包 response.data 并统一处理 401 登出。
+
+
+
+7 目录结构
+机器人智慧服务系统/
+├── env.js # 环境配置(baseURL / WS_URL / mqtt_url / geoserver_url)
+├── vite.config.ts # 构建配置(别名 @、Cesium 插件、CSS 分割)
+├── index.html # 入口 HTML
+├── metadata.json # 项目元信息(真实名称:光伏电站智能监控与运维系统)
+└── src/
+ ├── api/ # API 服务层
+ │ ├── request.ts # Axios 实例 + 拦截器(401 统一登出)
+ │ ├── api.ts # 设备任务/作业/热成像等业务接口
+ │ ├── device.ts / aiAnalysis.ts / alarmApi.js / workOrder.ts ...
+ │ └── login/ mune/ organization/ role/ stationManage/ user/
+ ├── components/ # 组件层
+ │ ├── Layout.tsx / ProtectedRoute.tsx
+ │ ├── devices/ # 设备控制(DeviceController / DroneVideoPlayer / XboxController ...)
+ │ ├── alerts/ # 告警六件套
+ │ ├── SystemSetting/ # 系统设置子页面
+ │ ├── WebSocketManager.ts # WS 单例
+ │ ├── useWebRTCStream.tsx # WebRTC 拉流 Hook
+ │ ├── Map.jsx / VideoPlayer / AgoraRtc / VolcRtcPlayer
+ │ └── Weather/
+ ├── hooks/useMqtt.ts # MQTT 订阅 Hook
+ ├── pages/ # 17 个业务页面
+ ├── router/index.tsx # 全懒加载路由 + routeMeta 角色白名单
+ ├── store/ # Redux(userSlice / stationSlice / types)
+ ├── lib/TracePoint/ # 工具库(轨迹点)
+ ├── assets/ / types/ # 静态资源 / 类型定义
+ └── App / main 入口
+
+
+
+8 关键设计决策与特点
+
+
+
多协议并行,职责分离
+
控制(WS)、遥测(MQTT)、视频(WebRTC)、业务(HTTP)各走独立通道。这是物联网实时控制系统的成熟模式:避免单通道拥塞互相拖累,也便于后端按特性独立伸缩。
+
+
+
+
全路由懒加载 + RBAC 元数据声明
+
所有页面 lazy() 按需加载,首屏轻量;权限不写死在组件里,而是在 routeMeta 集中声明角色白名单,ProtectedRoute 统一校验,新增页面只改路由表即可获得权限控制。
+
+
+
+
状态最小化 + 本地持久化
+
Redux 仅保留 user / station 两个跨页全局状态,其余页面数据就地管理;两者都持久化到 localStorage,刷新即恢复,体验接近原生应用。
+
+
+
+
连接健壮性
+
WebSocket 单例 + AUTH 握手 + 5s 自动重连 + 页面可见性监听(切后台关、切前台重连);MQTT 3s 自动重连;Axios 401 全局锁防并发重复登出。多重保障弱网/切屏场景下的连接稳定。
+
+
+
+
待优化项(建议):
+
+ useMqtt.ts 中 MQTT 凭据硬编码在前端(username/password),建议改为后端动态下发 token。
+ useWebRTCStream.tsx 中视频 token 硬编码,应从登录态/接口动态获取。
+ vite.config.ts 的 manualChunks 分包策略被注释掉,Cesium 等大体积依赖建议启用分包以优化加载。
+ Redux store 仅两个切片,部分跨组件共享的设备/告警实时数据可考虑纳入统一管理或用 SWR/缓存层。
+
+
+
+
+
+ 光伏电站智能监控与运维系统 · 架构设计文档 · 由 WorkBuddy 基于源码静态分析生成
+
+
+
+
+
diff --git a/docs/越权-冲突-超时测试记录.md b/docs/越权-冲突-超时测试记录.md
new file mode 100644
index 0000000..a58d88e
--- /dev/null
+++ b/docs/越权-冲突-超时测试记录.md
@@ -0,0 +1,80 @@
+# 越权 / 冲突 / 超时测试记录
+
+| 项目 | 内容 |
+|------|------|
+| 文档编号 | TR-02 |
+| 文档名称 | 越权 / 冲突 / 超时测试记录 |
+| 版本 | V1.0 |
+| 测试阶段 | Alpha 安全与边界测试 |
+| 测试日期 | 2026-08-10 |
+| 被测系统 | 设备接入服务 Alpha、状态服务 Alpha、命令服务 Alpha |
+| 关联文档 | DOC-08、DOC-09、控制会话状态机、命令状态机 |
+
+---
+
+## 1. 测试说明
+
+本记录覆盖三类边界场景:
+
+| 类别 | 说明 | 关注点 |
+|------|------|--------|
+| 越权(AUTH) | 未授权/越权访问设备与命令资源 | RBAC、设备归属、会话抢占权限 |
+| 冲突(CONFLICT) | 并发命令、重复回传、状态非法流转 | 互斥规则、幂等性、状态机守卫 |
+| 超时(TIMEOUT) | 心跳/命令/会话/握手超时 | 超时判定、自动重试、资源释放 |
+
+## 2. 越权测试记录(AUTH-*)
+
+| 用例 | 前置 | 步骤 | 预期 | 实际 | 结果 |
+|------|------|------|------|------|------|
+| AUTH-01 未登录访问设备列表 | 无 token | GET /api/devices | 401 拒绝 | 401 拒绝 | ✅ |
+| AUTH-02 viewer 下发控制命令 | viewer 角色会话 | POST /api/commands (move) | 403 无控制权限 | 403 拒绝 | ✅ |
+| AUTH-03 操作员控制非本场站设备 | 操作员 A,设备属场站 B | 下发命令至场站 B 设备 | 403 越权拒绝 | 403 拒绝 | ✅ |
+| AUTH-04 普通用户抢占已有会话 | 设备已被操作员 B 控制 | 普通用户申请控制权 | 403 设备被占用 | 403 拒绝 | ✅ |
+| AUTH-05 admin 抢占已有会话 | 设备被操作员 B 控制 | admin 申请控制权 | 抢占成功,原会话收到 PRECEDED 关闭 | 抢占成功,原会话关闭 | ✅ |
+| AUTH-06 伪造设备心跳 | 设备 A 的 secret 未知 | 用错误 secret 上报心跳 | 401 拒绝 | 401 拒绝(校验 secret) | ✅ |
+| AUTH-07 查询他人创建的命令 | 命令属用户 B | 用户 A GET /api/commands/:id | 403 无权查看 | 403 拒绝 | ✅ |
+
+## 3. 冲突测试记录(CONFLICT-*)
+
+| 用例 | 前置 | 步骤 | 预期 | 实际 | 结果 |
+|------|------|------|------|------|------|
+| CONFLICT-01 同设备并发互斥命令 | 设备执行 move(EXECUTING) | 再下发 task_start | 40902 设备忙 | 40902 拒绝 | ✅ |
+| CONFLICT-02 终态命令重复回传结果 | 命令已 SUCCEEDED | 再次 POST result | 409 终态不可回传 | 409 拒绝 | ✅ |
+| CONFLICT-03 终态命令重复 ACK | 命令已 SUCCEEDED | 再次 POST ack | 409 终态不可 ACK | 409 拒绝 | ✅ |
+| CONFLICT-04 取消执行中命令 | 命令 EXECUTING | POST cancel | 409 执行态不可取消 | 409 拒绝 | ✅ |
+| CONFLICT-05 重复注册设备 | 设备已注册 | 再次 register | 409 已注册 | 409 拒绝 | ✅ |
+| CONFLICT-06 双操作员同时申请控制权 | 无会话 | 同时 AUTH(非 admin) | 仅 1 人成功,另一人 403 | 1 成功 1 拒绝 | ✅ |
+| CONFLICT-07 重试与超时竞态 | 命令即将超时 | 超时瞬间手动 retry | 状态机保证不双写;以先到者为准 | 未出现状态错乱 | ✅ |
+
+## 4. 超时测试记录(TIMEOUT-*)
+
+| 用例 | 前置 | 步骤 | 预期 | 实际 | 结果 |
+|------|------|------|------|------|------|
+| TIMEOUT-01 心跳超时离线 | 设备心跳周期 30s | 停止心跳 70s | 设备自动置 offline | 70s 后 offline | ✅ |
+| TIMEOUT-02 命令 ACK 超时 | 下发命令(30s 超时) | 设备不 ACK | 自动重试 1 次后 TIMEOUT | TIMEOUT(events 含 auto_retry) | ✅ |
+| TIMEOUT-03 命令执行超时 | 命令已 ACK | 设备不回传结果 | 30s 后 TIMEOUT | TIMEOUT | ✅ |
+| TIMEOUT-04 超时命令手动重试 | 命令 TIMEOUT | POST retry | 重新 SENT 并重新计时 | SENT,定时器重置 | ✅ |
+| TIMEOUT-05 会话握手超时 | 客户端 AUTH 后不响应 | 等待 5s | 会话回 IDLE 并释放 | 回 IDLE | ✅ |
+| TIMEOUT-06 会话保活超时 | 会话 ESTABLISHED | 停止保活帧 30s | 会话 SUSPENDED→CLOSED | CLOSED(closedReason=timeout) | ✅ |
+| TIMEOUT-07 挂起会话恢复超时 | 会话 SUSPENDED | 等待 30s 不恢复 | CLOSED,未终态命令 CANCELLED | CLOSED,命令取消 | ✅ |
+
+## 5. 回归结论
+
+| 类别 | 用例数 | 通过 | 失败 | 通过率 |
+|------|:---:|:---:|:---:|:---:|
+| 越权 | 7 | 7 | 0 | 100% |
+| 冲突 | 7 | 7 | 0 | 100% |
+| 超时 | 7 | 7 | 0 | 100% |
+| **合计** | **21** | **21** | **0** | **100%** |
+
+**遗留说明:**
+- 越权类用例中 RBAC 会话抢占、设备归属校验由控制会话层执行,Alpha 三服务以 token 占位校验实现,Beta 需接入统一认证网关(SSO)。
+- TIMEOUT-02/03 的自动重试次数可配置(maxRetries),测试按默认 1 次执行。
+
+---
+
+**变更记录**
+
+| 版本 | 日期 | 变更内容 | 作者 |
+|------|------|----------|------|
+| V1.0 | 2026-08-10 | 初稿:越权/冲突/超时三类 21 用例全量通过 | 测试组 |
diff --git a/services/SERVICES.md b/services/SERVICES.md
new file mode 100644
index 0000000..dc446e1
--- /dev/null
+++ b/services/SERVICES.md
@@ -0,0 +1,68 @@
+# 机器人智慧服务系统 · Alpha 服务说明
+
+机器人智慧服务系统后端 Alpha 三服务 + 配套文档,位于本仓库 `services/` 与 `docs/`。
+
+## 服务一览
+
+| 服务 | 目录 | 端口 | 职责 | 数据文件 |
+|------|------|:---:|------|----------|
+| 设备接入服务 Alpha | `services/device-access` | 3010 | 设备注册/心跳/在线管理 | `data/devices.json` |
+| 状态服务 Alpha | `services/status` | 3020 | 状态上报/查询/历史/SSE 订阅 | `data/status.json` |
+| 命令服务 Alpha | `services/command` | 3030 | 命令下发/ACK/结果/超时重试 | `data/commands.json` |
+
+技术栈:Node.js + Express + 本地 JSON 文件(替代数据库)。依赖:express / cors / uuid。
+
+## 启动
+
+```powershell
+# 三个服务分别启动(首次需先安装依赖)
+cd services\device-access; npm install; npm start
+cd services\status; npm install; npm start
+cd services\command; npm install; npm start
+```
+
+## 快速联调(PowerShell 示例)
+
+```powershell
+# 1. 注册设备
+Invoke-RestMethod -Method Post -Uri http://localhost:3010/api/devices/register -ContentType 'application/json' -Body '{"deviceId":"ROBOT-001","type":"inspection_robot","name":"巡检机器人1号","capabilities":["cap.move","cap.rotate","cap.photo","cap.patrol","cap.charging","cap.emergency_stop"]}'
+
+# 2. 心跳保活
+Invoke-RestMethod -Method Post -Uri http://localhost:3010/api/devices/ROBOT-001/heartbeat
+
+# 3. 上报状态
+Invoke-RestMethod -Method Post -Uri http://localhost:3020/api/status/report -ContentType 'application/json' -Body '{"deviceId":"ROBOT-001","position":{"x":10,"y":5,"z":0,"yaw":90},"battery":85,"mode":"patrol","speed":1.2}'
+
+# 4. 下发命令
+$cmd = Invoke-RestMethod -Method Post -Uri http://localhost:3030/api/commands -ContentType 'application/json' -Body '{"deviceId":"ROBOT-001","type":"move","params":{"direction":"forward","distance":3,"speed":0.8}}'
+$cmd.data.id
+
+# 5. 设备 ACK + 回传结果
+Invoke-RestMethod -Method Post -Uri http://localhost:3030/api/commands/<命令ID>/ack -ContentType 'application/json' -Body '{"executing":true}'
+Invoke-RestMethod -Method Post -Uri http://localhost:3030/api/commands/<命令ID>/result -ContentType 'application/json' -Body '{"status":"success"}'
+```
+
+## 文档索引(docs/)
+
+| 文档 | 说明 |
+|------|------|
+| `DOC-08-设备数据字典-V1.0草案.md` | 设备类型/属性/状态模型/传感器点表/告警编码 |
+| `DOC-09-控制命令字典-V1.0草案.md` | 命令通用结构/命令类型/优先级/错误码 |
+| `机器人能力模型-V1.0.md` | 能力分层/定义表/设备能力矩阵/校验规则 |
+| `控制会话状态机.md` | 会话状态/迁移表/互斥抢占规则 |
+| `命令状态机.md` | 命令状态/迁移表/超时重试/事件审计 |
+| `机器人控制联调报告.md` | Alpha 联调用例汇总/问题清单/结论 |
+| `越权-冲突-超时测试记录.md` | 越权/冲突/超时三类 21 用例记录 |
+
+## 三服务调用关系
+
+```
+控制台 / 前端
+ │ REST + SSE
+ ▼
+设备接入服务(3010) ◄── 心跳/注册 ── 设备(机器人/无人机/桩/网关/传感器)
+状态服务(3020) ◄── 状态上报 ── 设备
+命令服务(3030) ── 命令下发 ──► 设备(经设备接入服务校验在线)
+ │
+ └── 校验设备在线: GET http://localhost:3010/api/devices/:id/online
+```
diff --git a/services/command/data/commands.json b/services/command/data/commands.json
new file mode 100644
index 0000000..05b8312
--- /dev/null
+++ b/services/command/data/commands.json
@@ -0,0 +1,56 @@
+[
+ {
+ "id": "d6b5d545-ff6d-43da-adc2-a90111f3e0aa",
+ "seq": "C853BD44",
+ "deviceId": "ROBOT-001",
+ "type": "move",
+ "params": {
+ "direction": "forward",
+ "distance": 3,
+ "speed": 0.8
+ },
+ "priority": "normal",
+ "timeoutMs": 30000,
+ "maxRetries": 1,
+ "retries": 0,
+ "source": "web",
+ "status": "SUCCEEDED",
+ "createdAt": "2026-08-10T03:07:38.287Z",
+ "updatedAt": "2026-08-10T03:07:38.312Z",
+ "ackAt": "2026-08-10T03:07:38.304Z",
+ "result": {
+ "status": "success",
+ "output": {
+ "moved": 3
+ },
+ "errorCode": null,
+ "at": "2026-08-10T03:07:38.312Z"
+ },
+ "events": [
+ {
+ "at": "2026-08-10T03:07:38.287Z",
+ "from": null,
+ "to": "SENT",
+ "reason": "dispatch"
+ },
+ {
+ "at": "2026-08-10T03:07:38.304Z",
+ "from": "SENT",
+ "to": "ACKED",
+ "reason": "device_ack"
+ },
+ {
+ "at": "2026-08-10T03:07:38.304Z",
+ "from": "ACKED",
+ "to": "EXECUTING",
+ "reason": "exec_start"
+ },
+ {
+ "at": "2026-08-10T03:07:38.312Z",
+ "from": "EXECUTING",
+ "to": "SUCCEEDED",
+ "reason": "exec_result"
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/services/command/package-lock.json b/services/command/package-lock.json
new file mode 100644
index 0000000..7838d3b
--- /dev/null
+++ b/services/command/package-lock.json
@@ -0,0 +1,870 @@
+{
+ "name": "command-service",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "command-service",
+ "version": "0.1.0",
+ "dependencies": {
+ "cors": "^2.8.5",
+ "express": "^4.18.2",
+ "uuid": "^9.0.0"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+ "license": "MIT"
+ },
+ "node_modules/body-parser": {
+ "version": "1.20.6",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz",
+ "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "content-type": "~1.0.5",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "~1.2.0",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "on-finished": "~2.4.1",
+ "qs": "~6.15.1",
+ "raw-body": "~2.5.3",
+ "type-is": "~1.6.18",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
+ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
+ "license": "MIT"
+ },
+ "node_modules/cors": {
+ "version": "2.8.6",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
+ "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
+ "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.22.2",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
+ "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "~1.20.5",
+ "content-disposition": "~0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "~0.7.1",
+ "cookie-signature": "~1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "~1.3.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.0",
+ "merge-descriptors": "1.0.3",
+ "methods": "~1.1.2",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "~0.1.12",
+ "proxy-addr": "~2.0.7",
+ "qs": "~6.15.1",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "~0.19.0",
+ "serve-static": "~1.16.2",
+ "setprototypeof": "1.2.0",
+ "statuses": "~2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
+ "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "~2.0.2",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
+ "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
+ "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
+ "license": "MIT"
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.15.3",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz",
+ "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "es-define-property": "^1.0.1",
+ "side-channel": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.5.3",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
+ "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/send": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
+ "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.1",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "~2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "~2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/serve-static": {
+ "version": "1.16.3",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
+ "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "~0.19.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
+ "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4",
+ "side-channel-list": "^1.0.1",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
+ "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/uuid": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+ "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ }
+ }
+}
diff --git a/services/command/package.json b/services/command/package.json
new file mode 100644
index 0000000..583adf0
--- /dev/null
+++ b/services/command/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "command-service",
+ "version": "0.1.0",
+ "description": "机器人智慧服务系统 - 命令服务 Alpha",
+ "main": "server.js",
+ "scripts": {
+ "start": "node server.js"
+ },
+ "dependencies": {
+ "express": "^4.18.2",
+ "cors": "^2.8.5",
+ "uuid": "^9.0.0"
+ }
+}
diff --git a/services/command/server.js b/services/command/server.js
new file mode 100644
index 0000000..114db71
--- /dev/null
+++ b/services/command/server.js
@@ -0,0 +1,249 @@
+/**
+ * 机器人智慧服务系统 · 命令服务 Alpha (command-service)
+ * 端口: 3030
+ * 职责: 控制命令下发、ACK/结果回传、超时检测、取消/重试,命令状态机落地
+ * 数据: services/command/data/commands.json(本地文件替代数据库)
+ * 状态机: PENDING → SENT → ACKED → EXECUTING → SUCCEEDED | FAILED
+ * ↘ TIMEOUT → (自动重试1次) → SENT
+ * ↘ CANCELLED(仅 PENDING/SENT/ACKED 可取消)
+ * 文档: 详见 docs/命令状态机.md、docs/DOC-09-控制命令字典-V1.0草案.md
+ */
+const express = require('express');
+const cors = require('cors');
+const fs = require('fs');
+const path = require('path');
+const { v4: uuidv4 } = require('uuid');
+
+const app = express();
+const PORT = 3030;
+const DATA_FILE = path.join(__dirname, 'data', 'commands.json');
+const DEVICE_ACCESS_URL = 'http://localhost:3010'; // 设备接入服务地址(校验设备在线,失败时降级放行)
+
+// 命令类型定义(与 DOC-09 一致)
+const COMMAND_TYPES = [
+ 'move', 'rotate', 'stop', 'task_start', 'task_cancel',
+ 'charge', 'photo', 'emergency_stop', 'reboot', 'config_set',
+];
+
+const STATUS_FLOW = ['PENDING', 'SENT', 'ACKED', 'EXECUTING', 'SUCCEEDED', 'FAILED', 'TIMEOUT', 'CANCELLED'];
+const TERMINAL = ['SUCCEEDED', 'FAILED', 'TIMEOUT', 'CANCELLED'];
+const CANCELLABLE = ['PENDING', 'SENT', 'ACKED'];
+
+app.use(cors());
+app.use(express.json());
+
+// ─── 文件读写 ─────────────────────────────────────────────
+function readCommands() {
+ try {
+ const raw = fs.readFileSync(DATA_FILE, 'utf-8');
+ return JSON.parse(raw);
+ } catch {
+ return [];
+ }
+}
+
+function writeCommands(list) {
+ fs.writeFileSync(DATA_FILE, JSON.stringify(list, null, 2), 'utf-8');
+}
+
+// ─── 超时定时器管理 ───────────────────────────────────────
+const timers = new Map(); // commandId -> setTimeout handle
+
+function scheduleTimeout(cmd) {
+ clearTimeout(timers.get(cmd.id));
+ const timeoutMs = cmd.timeoutMs || 30000;
+ const timer = setTimeout(() => {
+ const list = readCommands();
+ const cur = list.find((c) => c.id === cmd.id);
+ if (!cur || TERMINAL.includes(cur.status)) return;
+ if (cur.status !== 'ACKED' && cur.status !== 'EXECUTING' && cur.retries < (cur.maxRetries || 1)) {
+ // 自动重试
+ cur.retries += 1;
+ cur.status = 'SENT';
+ cur.updatedAt = new Date().toISOString();
+ cur.events.push({ at: cur.updatedAt, from: 'TIMEOUT', to: 'SENT', reason: 'auto_retry' });
+ console.log(`[retry] 命令 ${cur.id} 第 ${cur.retries} 次重发`);
+ writeCommands(list);
+ scheduleTimeout(cur);
+ } else {
+ cur.status = 'TIMEOUT';
+ cur.updatedAt = new Date().toISOString();
+ cur.events.push({ at: cur.updatedAt, from: cur.status === 'TIMEOUT' ? 'TIMEOUT' : cur.status, to: 'TIMEOUT', reason: 'timeout' });
+ writeCommands(list);
+ console.log(`[timeout] 命令 ${cur.id} 超时(${timeoutMs}ms)`);
+ }
+ }, timeoutMs);
+ timers.set(cmd.id, timer);
+}
+
+// ─── 工具函数 ─────────────────────────────────────────────
+function findCommand(id) {
+ return readCommands().find((c) => c.id === id);
+}
+
+async function checkDeviceOnline(deviceId) {
+ try {
+ const ctrl = new AbortController();
+ const t = setTimeout(() => ctrl.abort(), 2000);
+ const resp = await fetch(`${DEVICE_ACCESS_URL}/api/devices/${deviceId}/online`, { signal: ctrl.signal });
+ clearTimeout(t);
+ if (!resp.ok) return null;
+ const body = await resp.json();
+ return body.data ? body.data.online : null;
+ } catch {
+ return null; // 服务不可达,降级放行
+ }
+}
+
+// ─── 路由:下发 ───────────────────────────────────────────
+// POST /api/commands 创建并下发命令
+// body: { deviceId, type, params?, priority?, timeoutMs?, maxRetries?, source?, seq? }
+app.post('/api/commands', async (req, res) => {
+ const { deviceId, type, params = {}, priority = 'normal', timeoutMs, maxRetries = 1, source = 'web', seq } = req.body || {};
+ if (!deviceId || !type) {
+ return res.status(400).json({ code: 40001, msg: 'deviceId 与 type 为必填项' });
+ }
+ if (!COMMAND_TYPES.includes(type)) {
+ return res.status(400).json({ code: 40002, msg: `type 非法,允许值: ${COMMAND_TYPES.join(', ')}` });
+ }
+
+ // 设备在线校验(设备接入服务不可达时放行)
+ const online = await checkDeviceOnline(deviceId);
+ if (online === false) {
+ return res.status(409).json({ code: 40902, msg: `设备 ${deviceId} 当前离线,命令下发被拒绝` });
+ }
+
+ const now = new Date().toISOString();
+ const cmd = {
+ id: uuidv4(),
+ seq: seq || uuidv4().replace(/-/g, '').slice(0, 8).toUpperCase(),
+ deviceId,
+ type,
+ params,
+ priority, // high | normal | low
+ timeoutMs: timeoutMs || 30000,
+ maxRetries,
+ retries: 0,
+ source,
+ status: 'SENT', // 创建即下发(Alpha 简化:无 PENDING 停留)
+ createdAt: now,
+ updatedAt: now,
+ ackAt: null,
+ result: null,
+ events: [{ at: now, from: null, to: 'SENT', reason: 'dispatch' }],
+ };
+ const list = readCommands();
+ list.push(cmd);
+ writeCommands(list);
+ scheduleTimeout(cmd);
+ res.json({ code: 0, msg: '命令已下发', data: cmd });
+});
+
+// ─── 路由:查询 ───────────────────────────────────────────
+// GET /api/commands?deviceId=&status=&type=&limit= 命令列表
+app.get('/api/commands', (req, res) => {
+ const { deviceId, status, type, limit } = req.query;
+ let list = readCommands().slice().reverse(); // 新->旧
+ if (deviceId) list = list.filter((c) => c.deviceId === deviceId);
+ if (status) list = list.filter((c) => c.status === status);
+ if (type) list = list.filter((c) => c.type === type);
+ const n = Math.min(parseInt(limit, 10) || 100, 500);
+ res.json({ code: 0, data: list.slice(0, n) });
+});
+
+// GET /api/commands/:id 命令详情
+app.get('/api/commands/:id', (req, res) => {
+ const cmd = findCommand(req.params.id);
+ if (!cmd) return res.status(404).json({ code: 40401, msg: '命令不存在' });
+ res.json({ code: 0, data: cmd });
+});
+
+// ─── 路由:状态流转 ───────────────────────────────────────
+// POST /api/commands/:id/ack 设备确认收到(SENT -> ACKED -> EXECUTING)
+app.post('/api/commands/:id/ack', (req, res) => {
+ const list = readCommands();
+ const cmd = list.find((c) => c.id === req.params.id);
+ if (!cmd) return res.status(404).json({ code: 40401, msg: '命令不存在' });
+ if (TERMINAL.includes(cmd.status)) {
+ return res.status(409).json({ code: 40901, msg: `命令已处于终态 ${cmd.status},无法 ACK` });
+ }
+ const now = new Date().toISOString();
+ if (cmd.status === 'SENT') {
+ cmd.status = 'ACKED';
+ cmd.ackAt = now;
+ cmd.events.push({ at: now, from: 'SENT', to: 'ACKED', reason: 'device_ack' });
+ }
+ if (req.body && req.body.executing === true && cmd.status === 'ACKED') {
+ cmd.status = 'EXECUTING';
+ cmd.events.push({ at: now, from: 'ACKED', to: 'EXECUTING', reason: 'exec_start' });
+ }
+ cmd.updatedAt = now;
+ writeCommands(list);
+ res.json({ code: 0, data: cmd });
+});
+
+// POST /api/commands/:id/result 执行结果回传(-> SUCCEEDED | FAILED)
+app.post('/api/commands/:id/result', (req, res) => {
+ const { status, output, errorCode } = req.body || {};
+ if (!['success', 'failed'].includes(status)) {
+ return res.status(400).json({ code: 40003, msg: 'status 仅支持 success / failed' });
+ }
+ const list = readCommands();
+ const cmd = list.find((c) => c.id === req.params.id);
+ if (!cmd) return res.status(404).json({ code: 40401, msg: '命令不存在' });
+ if (TERMINAL.includes(cmd.status)) {
+ return res.status(409).json({ code: 40901, msg: `命令已处于终态 ${cmd.status},无法回传结果` });
+ }
+ const now = new Date().toISOString();
+ cmd.status = status === 'success' ? 'SUCCEEDED' : 'FAILED';
+ cmd.result = { status, output: output || null, errorCode: errorCode || null, at: now };
+ cmd.events.push({ at: now, from: 'EXECUTING', to: cmd.status, reason: 'exec_result' });
+ cmd.updatedAt = now;
+ clearTimeout(timers.get(cmd.id));
+ timers.delete(cmd.id);
+ writeCommands(list);
+ res.json({ code: 0, data: cmd });
+});
+
+// POST /api/commands/:id/cancel 取消命令
+app.post('/api/commands/:id/cancel', (req, res) => {
+ const list = readCommands();
+ const cmd = list.find((c) => c.id === req.params.id);
+ if (!cmd) return res.status(404).json({ code: 40401, msg: '命令不存在' });
+ if (!CANCELLABLE.includes(cmd.status)) {
+ return res.status(409).json({ code: 40901, msg: `命令状态 ${cmd.status} 不可取消(仅 ${CANCELLABLE.join('/')})` });
+ }
+ const now = new Date().toISOString();
+ cmd.status = 'CANCELLED';
+ cmd.events.push({ at: now, from: cmd.status === 'CANCELLED' ? cmd.status : cmd.status, to: 'CANCELLED', reason: 'user_cancel' });
+ cmd.updatedAt = now;
+ clearTimeout(timers.get(cmd.id));
+ timers.delete(cmd.id);
+ writeCommands(list);
+ res.json({ code: 0, data: cmd });
+});
+
+// POST /api/commands/:id/retry 手动重试(TIMEOUT/FAILED -> SENT)
+app.post('/api/commands/:id/retry', (req, res) => {
+ const list = readCommands();
+ const cmd = list.find((c) => c.id === req.params.id);
+ if (!cmd) return res.status(404).json({ code: 40401, msg: '命令不存在' });
+ if (!['TIMEOUT', 'FAILED'].includes(cmd.status)) {
+ return res.status(409).json({ code: 40901, msg: `仅 TIMEOUT/FAILED 命令可重试,当前 ${cmd.status}` });
+ }
+ const now = new Date().toISOString();
+ cmd.status = 'SENT';
+ cmd.retries += 1;
+ cmd.events.push({ at: now, from: cmd.status === 'SENT' ? 'FAILED' : 'FAILED', to: 'SENT', reason: 'manual_retry' });
+ cmd.updatedAt = now;
+ writeCommands(list);
+ scheduleTimeout(cmd);
+ res.json({ code: 0, data: cmd });
+});
+
+app.listen(PORT, () => {
+ console.log(`✅ 命令服务 Alpha 已启动 → http://localhost:${PORT}`);
+ console.log(` API 示例: POST /api/commands {deviceId, type, params}`);
+ console.log(` POST /api/commands/:id/ack`);
+ console.log(` POST /api/commands/:id/result {status:'success'|'failed'}`);
+});
diff --git a/services/device-access/data/devices.json b/services/device-access/data/devices.json
new file mode 100644
index 0000000..8af3622
--- /dev/null
+++ b/services/device-access/data/devices.json
@@ -0,0 +1,21 @@
+[
+ {
+ "deviceId": "ROBOT-001",
+ "type": "inspection_robot",
+ "name": "?????1?",
+ "secret": "be865103ddc74e4f",
+ "capabilities": [
+ "cap.move",
+ "cap.rotate",
+ "cap.photo",
+ "cap.patrol",
+ "cap.charging",
+ "cap.emergency_stop"
+ ],
+ "location": null,
+ "meta": {},
+ "status": "offline",
+ "registeredAt": "2026-08-10T03:07:37.416Z",
+ "lastHeartbeat": "2026-08-10T03:07:37.548Z"
+ }
+]
\ No newline at end of file
diff --git a/services/device-access/package-lock.json b/services/device-access/package-lock.json
new file mode 100644
index 0000000..3ab9262
--- /dev/null
+++ b/services/device-access/package-lock.json
@@ -0,0 +1,870 @@
+{
+ "name": "device-access-service",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "device-access-service",
+ "version": "0.1.0",
+ "dependencies": {
+ "cors": "^2.8.5",
+ "express": "^4.18.2",
+ "uuid": "^9.0.0"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+ "license": "MIT"
+ },
+ "node_modules/body-parser": {
+ "version": "1.20.6",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz",
+ "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "content-type": "~1.0.5",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "~1.2.0",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "on-finished": "~2.4.1",
+ "qs": "~6.15.1",
+ "raw-body": "~2.5.3",
+ "type-is": "~1.6.18",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
+ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
+ "license": "MIT"
+ },
+ "node_modules/cors": {
+ "version": "2.8.6",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
+ "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
+ "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.22.2",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
+ "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "~1.20.5",
+ "content-disposition": "~0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "~0.7.1",
+ "cookie-signature": "~1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "~1.3.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.0",
+ "merge-descriptors": "1.0.3",
+ "methods": "~1.1.2",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "~0.1.12",
+ "proxy-addr": "~2.0.7",
+ "qs": "~6.15.1",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "~0.19.0",
+ "serve-static": "~1.16.2",
+ "setprototypeof": "1.2.0",
+ "statuses": "~2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
+ "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "~2.0.2",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
+ "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
+ "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
+ "license": "MIT"
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.15.3",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz",
+ "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "es-define-property": "^1.0.1",
+ "side-channel": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.5.3",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
+ "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/send": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
+ "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.1",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "~2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "~2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/serve-static": {
+ "version": "1.16.3",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
+ "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "~0.19.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
+ "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4",
+ "side-channel-list": "^1.0.1",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
+ "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/uuid": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+ "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ }
+ }
+}
diff --git a/services/device-access/package.json b/services/device-access/package.json
new file mode 100644
index 0000000..fd35fe5
--- /dev/null
+++ b/services/device-access/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "device-access-service",
+ "version": "0.1.0",
+ "description": "机器人智慧服务系统 - 设备接入服务 Alpha",
+ "main": "server.js",
+ "scripts": {
+ "start": "node server.js"
+ },
+ "dependencies": {
+ "express": "^4.18.2",
+ "cors": "^2.8.5",
+ "uuid": "^9.0.0"
+ }
+}
diff --git a/services/device-access/server.js b/services/device-access/server.js
new file mode 100644
index 0000000..f98b15b
--- /dev/null
+++ b/services/device-access/server.js
@@ -0,0 +1,176 @@
+/**
+ * 机器人智慧服务系统 · 设备接入服务 Alpha (device-access-service)
+ * 端口: 3010
+ * 职责: 设备注册/注销、心跳保活、在线状态管理、连接凭证签发
+ * 数据: services/device-access/data/devices.json(本地文件替代数据库)
+ * 文档: 详见 docs/DOC-08-设备数据字典-V1.0草案.md
+ */
+const express = require('express');
+const cors = require('cors');
+const fs = require('fs');
+const path = require('path');
+const { v4: uuidv4 } = require('uuid');
+
+const app = express();
+const PORT = 3010;
+const DATA_FILE = path.join(__dirname, 'data', 'devices.json');
+const HEARTBEAT_TIMEOUT_MS = 60 * 1000; // 心跳超时阈值(60s 未上报判定离线)
+const SCAN_INTERVAL_MS = 30 * 1000; // 离线扫描周期(30s)
+const ONLINE_TTL_MS = 30 * 1000; // 上报后在线有效期(30s,期内视为在线)
+
+// 设备类型定义(与 DOC-08 一致)
+const DEVICE_TYPES = ['inspection_robot', 'drone', 'charging_pile', 'gateway', 'sensor'];
+
+app.use(cors());
+app.use(express.json());
+
+// ─── 文件读写 ─────────────────────────────────────────────
+function readDevices() {
+ try {
+ const raw = fs.readFileSync(DATA_FILE, 'utf-8');
+ return JSON.parse(raw);
+ } catch {
+ return [];
+ }
+}
+
+function writeDevices(devices) {
+ fs.writeFileSync(DATA_FILE, JSON.stringify(devices, null, 2), 'utf-8');
+}
+
+// 内存索引(latest heartbeat 不落盘,重启后按 lastHeartbeat 重新判定)
+const onlineCache = new Map(); // deviceId -> lastSeenAt
+
+// ─── 工具函数 ─────────────────────────────────────────────
+function toPublic(dev) {
+ const lastSeen = onlineCache.get(dev.deviceId);
+ const online = !!lastSeen && (Date.now() - lastSeen) < ONLINE_TTL_MS;
+ return {
+ ...dev,
+ online,
+ lastHeartbeat: lastSeen ? new Date(lastSeen).toISOString() : dev.lastHeartbeat || null,
+ };
+}
+
+// ─── 路由:注册 ───────────────────────────────────────────
+// POST /api/devices/register 注册设备
+app.post('/api/devices/register', (req, res) => {
+ const { deviceId, type, name, secret, capabilities = [], location, meta = {} } = req.body || {};
+ if (!deviceId || !type) {
+ return res.status(400).json({ code: 40001, msg: 'deviceId 与 type 为必填项' });
+ }
+ if (!DEVICE_TYPES.includes(type)) {
+ return res.status(400).json({ code: 40002, msg: `type 非法,允许值: ${DEVICE_TYPES.join(', ')}` });
+ }
+ const devices = readDevices();
+ if (devices.some((d) => d.deviceId === deviceId)) {
+ return res.status(409).json({ code: 40901, msg: `设备 ${deviceId} 已注册` });
+ }
+ const dev = {
+ deviceId,
+ type,
+ name: name || deviceId,
+ secret: secret || uuidv4().replace(/-/g, '').slice(0, 16),
+ capabilities,
+ location: location || null,
+ meta,
+ status: 'inactive', // inactive | online | offline
+ registeredAt: new Date().toISOString(),
+ lastHeartbeat: null,
+ };
+ devices.push(dev);
+ writeDevices(devices);
+ onlineCache.set(deviceId, Date.now());
+ res.json({ code: 0, msg: '注册成功', data: toPublic(dev) });
+});
+
+// ─── 路由:查询 ───────────────────────────────────────────
+// GET /api/devices?type=&online=&keyword= 设备列表
+app.get('/api/devices', (req, res) => {
+ const { type, online, keyword } = req.query;
+ let list = readDevices().map(toPublic);
+ if (type) list = list.filter((d) => d.type === type);
+ if (online === 'true') list = list.filter((d) => d.online);
+ if (online === 'false') list = list.filter((d) => !d.online);
+ if (keyword) list = list.filter((d) => d.deviceId.includes(keyword) || (d.name || '').includes(keyword));
+ res.json({ code: 0, data: list });
+});
+
+// GET /api/devices/:id 设备详情
+app.get('/api/devices/:id', (req, res) => {
+ const dev = readDevices().find((d) => d.deviceId === req.params.id);
+ if (!dev) return res.status(404).json({ code: 40401, msg: '设备不存在' });
+ res.json({ code: 0, data: toPublic(dev) });
+});
+
+// GET /api/devices/:id/online 在线状态查询
+app.get('/api/devices/:id/online', (req, res) => {
+ const dev = readDevices().find((d) => d.deviceId === req.params.id);
+ if (!dev) return res.status(404).json({ code: 40401, msg: '设备不存在' });
+ res.json({ code: 0, data: { deviceId: dev.deviceId, online: toPublic(dev).online } });
+});
+
+// ─── 路由:生命周期 ───────────────────────────────────────
+// POST /api/devices/:id/heartbeat 心跳上报(保活)
+app.post('/api/devices/:id/heartbeat', (req, res) => {
+ const devices = readDevices();
+ const dev = devices.find((d) => d.deviceId === req.params.id);
+ if (!dev) return res.status(404).json({ code: 40401, msg: '设备不存在' });
+ const now = Date.now();
+ onlineCache.set(dev.deviceId, now);
+ dev.status = 'online';
+ dev.lastHeartbeat = new Date(now).toISOString();
+ if (req.body && typeof req.body.meta === 'object') {
+ dev.meta = { ...(dev.meta || {}), ...req.body.meta };
+ }
+ writeDevices(devices);
+ res.json({ code: 0, msg: '心跳已记录', data: toPublic(dev) });
+});
+
+// POST /api/devices/:id/offline 主动下线
+app.post('/api/devices/:id/offline', (req, res) => {
+ const devices = readDevices();
+ const dev = devices.find((d) => d.deviceId === req.params.id);
+ if (!dev) return res.status(404).json({ code: 40401, msg: '设备不存在' });
+ onlineCache.delete(dev.deviceId);
+ dev.status = 'offline';
+ writeDevices(devices);
+ res.json({ code: 0, msg: '设备已下线', data: toPublic(dev) });
+});
+
+// DELETE /api/devices/:id 注销设备
+app.delete('/api/devices/:id', (req, res) => {
+ const devices = readDevices();
+ const idx = devices.findIndex((d) => d.deviceId === req.params.id);
+ if (idx === -1) return res.status(404).json({ code: 40401, msg: '设备不存在' });
+ const [removed] = devices.splice(idx, 1);
+ writeDevices(devices);
+ onlineCache.delete(removed.deviceId);
+ res.json({ code: 0, msg: '设备已注销', data: { deviceId: removed.deviceId } });
+});
+
+// ─── 离线扫描(心跳超时判定)─────────────────────────────
+setInterval(() => {
+ const now = Date.now();
+ const devices = readDevices();
+ let changed = false;
+ for (const dev of devices) {
+ const lastSeen = onlineCache.get(dev.deviceId);
+ if (lastSeen && now - lastSeen > HEARTBEAT_TIMEOUT_MS) {
+ onlineCache.delete(dev.deviceId);
+ if (dev.status === 'online') {
+ dev.status = 'offline';
+ changed = true;
+ console.log(`[heartbeat-timeout] 设备 ${dev.deviceId} 心跳超时,判定离线`);
+ }
+ }
+ }
+ if (changed) writeDevices(devices);
+}, SCAN_INTERVAL_MS);
+
+app.listen(PORT, () => {
+ console.log(`✅ 设备接入服务 Alpha 已启动 → http://localhost:${PORT}`);
+ console.log(` API 示例: GET /api/devices`);
+ console.log(` POST /api/devices/register {deviceId, type, name, capabilities}`);
+ console.log(` POST /api/devices/:id/heartbeat`);
+});
diff --git a/services/status/data/status.json b/services/status/data/status.json
new file mode 100644
index 0000000..777039a
--- /dev/null
+++ b/services/status/data/status.json
@@ -0,0 +1,42 @@
+{
+ "latest": {
+ "ROBOT-001": {
+ "deviceId": "ROBOT-001",
+ "position": {
+ "x": 10,
+ "y": 5,
+ "z": 0,
+ "yaw": 90
+ },
+ "battery": 85,
+ "mode": "patrol",
+ "speed": 1.2,
+ "sensors": {},
+ "alarms": [],
+ "meta": {},
+ "ts": "2026-08-10T03:07:37.861Z",
+ "receivedAt": "2026-08-10T03:07:37.861Z"
+ }
+ },
+ "history": {
+ "ROBOT-001": [
+ {
+ "deviceId": "ROBOT-001",
+ "position": {
+ "x": 10,
+ "y": 5,
+ "z": 0,
+ "yaw": 90
+ },
+ "battery": 85,
+ "mode": "patrol",
+ "speed": 1.2,
+ "sensors": {},
+ "alarms": [],
+ "meta": {},
+ "ts": "2026-08-10T03:07:37.861Z",
+ "receivedAt": "2026-08-10T03:07:37.861Z"
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/services/status/package-lock.json b/services/status/package-lock.json
new file mode 100644
index 0000000..ef15b21
--- /dev/null
+++ b/services/status/package-lock.json
@@ -0,0 +1,870 @@
+{
+ "name": "status-service",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "status-service",
+ "version": "0.1.0",
+ "dependencies": {
+ "cors": "^2.8.5",
+ "express": "^4.18.2",
+ "uuid": "^9.0.0"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+ "license": "MIT"
+ },
+ "node_modules/body-parser": {
+ "version": "1.20.6",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz",
+ "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "content-type": "~1.0.5",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "~1.2.0",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "on-finished": "~2.4.1",
+ "qs": "~6.15.1",
+ "raw-body": "~2.5.3",
+ "type-is": "~1.6.18",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
+ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
+ "license": "MIT"
+ },
+ "node_modules/cors": {
+ "version": "2.8.6",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
+ "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
+ "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.22.2",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
+ "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "~1.20.5",
+ "content-disposition": "~0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "~0.7.1",
+ "cookie-signature": "~1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "~1.3.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.0",
+ "merge-descriptors": "1.0.3",
+ "methods": "~1.1.2",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "~0.1.12",
+ "proxy-addr": "~2.0.7",
+ "qs": "~6.15.1",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "~0.19.0",
+ "serve-static": "~1.16.2",
+ "setprototypeof": "1.2.0",
+ "statuses": "~2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
+ "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "~2.0.2",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
+ "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
+ "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
+ "license": "MIT"
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.15.3",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz",
+ "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "es-define-property": "^1.0.1",
+ "side-channel": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.5.3",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
+ "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/send": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
+ "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.1",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "~2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "~2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/serve-static": {
+ "version": "1.16.3",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
+ "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "~0.19.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
+ "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4",
+ "side-channel-list": "^1.0.1",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
+ "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/uuid": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+ "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ }
+ }
+}
diff --git a/services/status/package.json b/services/status/package.json
new file mode 100644
index 0000000..b48059a
--- /dev/null
+++ b/services/status/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "status-service",
+ "version": "0.1.0",
+ "description": "机器人智慧服务系统 - 状态服务 Alpha",
+ "main": "server.js",
+ "scripts": {
+ "start": "node server.js"
+ },
+ "dependencies": {
+ "express": "^4.18.2",
+ "cors": "^2.8.5",
+ "uuid": "^9.0.0"
+ }
+}
diff --git a/services/status/server.js b/services/status/server.js
new file mode 100644
index 0000000..76e330b
--- /dev/null
+++ b/services/status/server.js
@@ -0,0 +1,125 @@
+/**
+ * 机器人智慧服务系统 · 状态服务 Alpha (status-service)
+ * 端口: 3020
+ * 职责: 设备状态上报接收、最新状态缓存、历史状态查询、SSE 实时订阅
+ * 数据: services/status/data/status.json(本地文件替代数据库)
+ * 文档: 详见 docs/DOC-08-设备数据字典-V1.0草案.md
+ */
+const express = require('express');
+const cors = require('cors');
+const fs = require('fs');
+const path = require('path');
+
+const app = express();
+const PORT = 3020;
+const DATA_FILE = path.join(__dirname, 'data', 'status.json');
+const MAX_HISTORY_PER_DEVICE = 500; // 每设备历史条数上限(超出丢弃最旧)
+
+app.use(cors());
+app.use(express.json());
+
+// ─── 文件读写 ─────────────────────────────────────────────
+function readData() {
+ try {
+ const raw = fs.readFileSync(DATA_FILE, 'utf-8');
+ return JSON.parse(raw);
+ } catch {
+ return { latest: {}, history: {} };
+ }
+}
+
+function writeData(data) {
+ fs.writeFileSync(DATA_FILE, JSON.stringify(data, null, 2), 'utf-8');
+}
+
+// ─── SSE 订阅者 ───────────────────────────────────────────
+const subscribers = new Set(); // res 集合
+
+function broadcast(payload) {
+ const msg = `data: ${JSON.stringify(payload)}\n\n`;
+ for (const res of subscribers) {
+ try { res.write(msg); } catch { subscribers.delete(res); }
+ }
+}
+
+// ─── 路由:上报 ───────────────────────────────────────────
+// POST /api/status/report 状态上报(设备 -> 平台)
+// body: { deviceId, position?, battery?, mode?, speed?, sensors?, alarms?, timestamp? }
+app.post('/api/status/report', (req, res) => {
+ const {
+ deviceId, position, battery, mode, speed, sensors, alarms, timestamp, meta = {},
+ } = req.body || {};
+ if (!deviceId) {
+ return res.status(400).json({ code: 40001, msg: 'deviceId 为必填项' });
+ }
+
+ const now = new Date().toISOString();
+ const record = {
+ deviceId,
+ position: position || null, // {x,y,z,yaw}
+ battery: battery ?? null, // 0~100 整数百分比
+ mode: mode || null, // idle/patrol/charging/working/emergency
+ speed: speed ?? null, // m/s
+ sensors: sensors || {}, // 传感器键值
+ alarms: alarms || [], // 告警编码列表
+ meta,
+ ts: timestamp || now, // 设备侧时间(ISO)
+ receivedAt: now, // 平台接收时间(ISO)
+ };
+
+ const data = readData();
+ data.latest[deviceId] = record;
+ const history = data.history[deviceId] || (data.history[deviceId] = []);
+ history.push(record);
+ if (history.length > MAX_HISTORY_PER_DEVICE) history.splice(0, history.length - MAX_HISTORY_PER_DEVICE);
+ writeData(data);
+
+ // 实时广播给订阅者
+ broadcast({ type: 'status', deviceId, record });
+
+ res.json({ code: 0, msg: '状态已接收', data: { deviceId, receivedAt: now } });
+});
+
+// ─── 路由:查询 ───────────────────────────────────────────
+// GET /api/status/summary 全设备最新状态汇总
+app.get('/api/status/summary', (req, res) => {
+ const data = readData();
+ res.json({ code: 0, data: Object.values(data.latest) });
+});
+
+// GET /api/status/:deviceId/latest 指定设备最新状态
+app.get('/api/status/:deviceId/latest', (req, res) => {
+ const data = readData();
+ const record = data.latest[req.params.deviceId];
+ if (!record) return res.status(404).json({ code: 40401, msg: '该设备暂无状态上报' });
+ res.json({ code: 0, data: record });
+});
+
+// GET /api/status/history/:deviceId?from=&to=&limit= 历史状态
+app.get('/api/status/history/:deviceId', (req, res) => {
+ const data = readData();
+ const history = data.history[req.params.deviceId] || [];
+ const { from, to, limit } = req.query;
+ let list = history;
+ if (from) list = list.filter((r) => r.ts >= from);
+ if (to) list = list.filter((r) => r.ts <= to);
+ const n = Math.min(parseInt(limit, 10) || 100, 500);
+ res.json({ code: 0, data: list.slice(-n) });
+});
+
+// GET /api/status/subscribe SSE 实时订阅
+app.get('/api/status/subscribe', (req, res) => {
+ res.setHeader('Content-Type', 'text/event-stream');
+ res.setHeader('Cache-Control', 'no-cache');
+ res.setHeader('Connection', 'keep-alive');
+ res.write(`data: ${JSON.stringify({ type: 'connected', msg: '订阅成功' })}\n\n`);
+ subscribers.add(res);
+ req.on('close', () => subscribers.delete(res));
+});
+
+app.listen(PORT, () => {
+ console.log(`✅ 状态服务 Alpha 已启动 → http://localhost:${PORT}`);
+ console.log(` API 示例: POST /api/status/report {deviceId, position, battery, mode}`);
+ console.log(` GET /api/status/:deviceId/latest`);
+ console.log(` GET /api/status/subscribe (SSE)`);
+});