文档增加
This commit is contained in:
141
docs/DOC-08-设备数据字典-V1.0草案.md
Normal file
141
docs/DOC-08-设备数据字典-V1.0草案.md
Normal file
@@ -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 | 初稿:设备类型/属性/状态模型/传感器点表/告警编码 | 系统组 |
|
||||||
122
docs/DOC-09-控制命令字典-V1.0草案.md
Normal file
122
docs/DOC-09-控制命令字典-V1.0草案.md
Normal file
@@ -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 | 初稿:命令结构/类型/优先级/错误码 | 系统组 |
|
||||||
108
docs/命令状态机.md
Normal file
108
docs/命令状态机.md
Normal file
@@ -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 | 初稿:命令状态定义/迁移表/超时重试/事件审计 | 系统组 |
|
||||||
98
docs/控制会话状态机.md
Normal file
98
docs/控制会话状态机.md
Normal file
@@ -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 | 初稿:会话状态/迁移表/互斥抢占规则 | 系统组 |
|
||||||
101
docs/机器人控制联调报告.md
Normal file
101
docs/机器人控制联调报告.md
Normal file
@@ -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 联调结果汇总 | 系统组 |
|
||||||
101
docs/机器人能力模型-V1.0.md
Normal file
101
docs/机器人能力模型-V1.0.md
Normal file
@@ -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 | 初稿:能力分层/定义表/矩阵/校验规则 | 系统组 |
|
||||||
421
docs/系统架构设计文档.html
Normal file
421
docs/系统架构设计文档.html
Normal file
@@ -0,0 +1,421 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>光伏电站智能监控与运维系统 · 架构设计文档</title>
|
||||||
|
<style>
|
||||||
|
:root{
|
||||||
|
--bg:#ffffff; --surface:#f7f9fc; --surface2:#eef3f9;
|
||||||
|
--text:#1a2333; --text2:#5a6577; --text3:#8a93a3;
|
||||||
|
--border:#e3e8ef; --border2:#cdd5e0;
|
||||||
|
--blue:#185FA5; --blue-d:#0C447C; --blue-l:#E6F1FB;
|
||||||
|
--teal:#0F6E56; --teal-d:#085041; --teal-l:#E1F5EE;
|
||||||
|
--coral:#D85A30; --coral-d:#993C1D;
|
||||||
|
--amber:#BA7517;
|
||||||
|
--radius:12px; --radius-s:8px;
|
||||||
|
--sans:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;
|
||||||
|
--mono:"SFMono-Regular",Consolas,"Liberation Mono",monospace;
|
||||||
|
}
|
||||||
|
*{box-sizing:border-box;margin:0;padding:0}
|
||||||
|
body{font-family:var(--sans);background:var(--bg);color:var(--text);line-height:1.7;font-size:15px}
|
||||||
|
.wrap{max-width:920px;margin:0 auto;padding:40px 28px 80px}
|
||||||
|
header{border-bottom:2px solid var(--blue);padding-bottom:24px;margin-bottom:8px}
|
||||||
|
header h1{font-size:26px;font-weight:600;color:var(--blue-d);margin-bottom:6px}
|
||||||
|
header .sub{font-size:15px;color:var(--text2)}
|
||||||
|
header .meta{font-size:13px;color:var(--text3);margin-top:10px}
|
||||||
|
header .meta span{display:inline-block;margin-right:18px}
|
||||||
|
.tag{display:inline-block;font-size:12px;padding:2px 10px;border-radius:20px;background:var(--blue-l);color:var(--blue-d);font-weight:500;margin-right:6px}
|
||||||
|
.tag.teal{background:var(--teal-l);color:var(--teal-d)}
|
||||||
|
h2{font-size:20px;font-weight:600;color:var(--blue-d);margin:40px 0 14px;padding-bottom:8px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:10px}
|
||||||
|
h2 .num{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;border-radius:50%;background:var(--blue);color:#fff;font-size:13px;font-weight:600}
|
||||||
|
h3{font-size:16px;font-weight:600;color:var(--text);margin:24px 0 10px}
|
||||||
|
p{margin-bottom:12px;color:var(--text)}
|
||||||
|
p.lead{font-size:16px;color:var(--text2)}
|
||||||
|
ul,ol{margin:0 0 14px 20px}
|
||||||
|
li{margin-bottom:6px}
|
||||||
|
code{font-family:var(--mono);font-size:13px;background:var(--surface2);padding:1px 6px;border-radius:4px;color:var(--blue-d)}
|
||||||
|
pre{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-s);padding:14px 16px;overflow-x:auto;margin-bottom:16px}
|
||||||
|
pre code{background:none;padding:0;font-size:13px;color:var(--text)}
|
||||||
|
table{width:100%;border-collapse:collapse;margin-bottom:16px;font-size:14px}
|
||||||
|
th,td{padding:10px 12px;text-align:left;border-bottom:1px solid var(--border);vertical-align:top}
|
||||||
|
th{background:var(--surface);color:var(--blue-d);font-weight:600;font-size:13px}
|
||||||
|
tr:hover td{background:var(--surface)}
|
||||||
|
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:18px 20px;margin-bottom:16px}
|
||||||
|
.card.blue{border-left:3px solid var(--blue)}
|
||||||
|
.card.teal{border-left:3px solid var(--teal)}
|
||||||
|
.card.coral{border-left:3px solid var(--coral)}
|
||||||
|
.card h3{margin-top:0}
|
||||||
|
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px;margin-bottom:16px}
|
||||||
|
.stat{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-s);padding:14px}
|
||||||
|
.stat .v{font-size:22px;font-weight:600;color:var(--blue-d)}
|
||||||
|
.stat .l{font-size:12px;color:var(--text3);margin-top:2px}
|
||||||
|
.diagram{background:#fff;border:1px solid var(--border);border-radius:var(--radius);padding:20px;margin:16px 0}
|
||||||
|
.diagram svg{width:100%;height:auto;display:block}
|
||||||
|
.cap{font-size:13px;color:var(--text3);text-align:center;margin-top:8px;font-style:italic}
|
||||||
|
.flow{display:flex;gap:0;align-items:stretch;margin:14px 0;flex-wrap:wrap}
|
||||||
|
.flow .step{flex:1;min-width:90px;background:var(--blue-l);border:1px solid #B5D4F4;border-radius:var(--radius-s);padding:10px;text-align:center;font-size:13px}
|
||||||
|
.flow .step.t{background:var(--teal-l);border-color:#9FE1CB}
|
||||||
|
.flow .arr{display:flex;align-items:center;color:var(--blue);font-size:18px;padding:0 4px}
|
||||||
|
.note{font-size:13px;color:var(--text2);background:var(--blue-l);border-radius:var(--radius-s);padding:10px 14px;margin-bottom:14px}
|
||||||
|
.note.warn{background:#FAECE7;color:var(--coral-d)}
|
||||||
|
.toc{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:18px 22px;margin-bottom:30px}
|
||||||
|
.toc h3{margin:0 0 10px;color:var(--blue-d)}
|
||||||
|
.toc ol{margin:0 0 0 22px}
|
||||||
|
.toc a{color:var(--blue);text-decoration:none}
|
||||||
|
.toc a:hover{text-decoration:underline}
|
||||||
|
footer{margin-top:50px;padding-top:20px;border-top:1px solid var(--border);font-size:13px;color:var(--text3);text-align:center}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<div><span class="tag">架构设计文档</span><span class="tag teal">React 19</span><span class="tag teal">Vite 6</span></div>
|
||||||
|
<h1>光伏电站智能监控与运维系统</h1>
|
||||||
|
<div class="sub">机器人智慧服务平台 · 前端系统架构与设计说明</div>
|
||||||
|
<div class="meta">
|
||||||
|
<span>项目代号:机器人智慧服务系统</span>
|
||||||
|
<span>文档版本:v1.0</span>
|
||||||
|
<span>生成日期:2026-08-10</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="toc">
|
||||||
|
<h3>目录</h3>
|
||||||
|
<ol>
|
||||||
|
<li><a href="#s1">项目概述</a></li>
|
||||||
|
<li><a href="#s2">技术栈选型</a></li>
|
||||||
|
<li><a href="#s3">系统分层架构</a></li>
|
||||||
|
<li><a href="#s4">核心模块详解</a></li>
|
||||||
|
<li><a href="#s5">实时通信层设计</a></li>
|
||||||
|
<li><a href="#s6">数据流设计</a></li>
|
||||||
|
<li><a href="#s7">目录结构</a></li>
|
||||||
|
<li><a href="#s8">关键设计决策与特点</a></li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section id="s1">
|
||||||
|
<h2><span class="num">1</span>项目概述</h2>
|
||||||
|
<p>本项目(metadata 名:<code>光伏电站智能监控与运维系统</code>)是一个面向光伏电站场景的<strong>机器人智能管理平台</strong>。它将巡检机器人 / 无人机、视频监控、告警处理与运维工单整合到单一 Web 控制台,提供设备管理、实时监控、视频回传、状态预警、AI 诊断分析、清洗优化、报表中心与视频下载等一站式能力。</p>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
<div class="stat"><div class="v">4</div><div class="l">通信协议通道</div></div>
|
||||||
|
<div class="stat"><div class="v">17</div><div class="l">业务页面</div></div>
|
||||||
|
<div class="stat"><div class="v">全懒加载</div><div class="l">路由按需加载</div></div>
|
||||||
|
<div class="stat"><div class="v">RBAC</div><div class="l">三级角色权限</div></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="lead">系统定位是<strong>前端 SPA</strong>,通过四条独立通信通道对接后端的 API、MQTT、WebSocket 与 GeoServer 服务,实现“低延迟控制 + 高吞吐遥测 + 实时视频”的多协议协同。</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="s2">
|
||||||
|
<h2><span class="num">2</span>技术栈选型</h2>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>层级</th><th>技术</th><th>用途</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>框架</td><td>React 19 + TypeScript</td><td>UI 渲染与类型安全</td></tr>
|
||||||
|
<tr><td>构建</td><td>Vite 6 + @vitejs/plugin-react</td><td>极速 HMR 与打包,含 CSS 代码分割</td></tr>
|
||||||
|
<tr><td>UI 组件</td><td>Ant Design 5 + TailwindCSS 4</td><td>企业级组件库 + 原子化样式</td></tr>
|
||||||
|
<tr><td>状态管理</td><td>Redux Toolkit + react-redux</td><td>user / station 全局状态切片</td></tr>
|
||||||
|
<tr><td>路由</td><td>React Router DOM 7</td><td>全路由懒加载 + ProtectedRoute 鉴权</td></tr>
|
||||||
|
<tr><td>3D / GIS</td><td>Cesium + vite-plugin-cesium</td><td>三维地图、设备轨迹与场站可视化</td></tr>
|
||||||
|
<tr><td>图表</td><td>ECharts 6 / Recharts / @ant-design/charts</td><td>告警统计、运行数据可视化</td></tr>
|
||||||
|
<tr><td>HTTP</td><td>Axios</td><td>统一请求实例 + 拦截器</td></tr>
|
||||||
|
<tr><td>实时通信</td><td>MQTT / WebSocket / WebRTC / Socket.io</td><td>遥测 / 控制 / 视频 / 实时推送</td></tr>
|
||||||
|
<tr><td>音视频</td><td>@volcengine/rtc · agora-rtc-sdk-ng · JSWebrtc</td><td>多路无人机/机器人视频回传</td></tr>
|
||||||
|
<tr><td>其它</td><td>js-cookie · qrcode · lodash · motion · react-draggable</td><td>会话/二维码/工具/动画/拖拽</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="s3">
|
||||||
|
<h2><span class="num">3</span>系统分层架构</h2>
|
||||||
|
<p>系统自上而下分为四层。应用层内部进一步拆为路由、页面、组件、状态四个子层;通信层以四条并行通道桥接前后端。</p>
|
||||||
|
|
||||||
|
<div class="diagram">
|
||||||
|
<svg viewBox="0 0 680 492" width="100%" role="img" aria-label="系统分层架构图">
|
||||||
|
<text x="340" y="26" text-anchor="middle" font-family="var(--sans)" font-size="15" font-weight="500" fill="#0C447C">光伏电站智能监控与运维系统 · 分层架构</text>
|
||||||
|
<rect x="200" y="52" width="280" height="44" rx="8" fill="#F1EFE8" stroke="#5F5E5A" stroke-width="0.5"/>
|
||||||
|
<text x="340" y="74" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)" font-size="13" font-weight="500" fill="#444441">用户浏览器 / Web Client</text>
|
||||||
|
<path d="M340 98 L340 118" fill="none" stroke="#888780" stroke-width="1" marker-end="url(#ar)"/>
|
||||||
|
<defs><marker id="ar" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M2 1L8 5L2 9" fill="none" stroke="context-stroke" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></marker></defs>
|
||||||
|
<rect x="40" y="120" width="600" height="132" rx="12" fill="#E6F1FB" stroke="#185FA5" stroke-width="0.5"/>
|
||||||
|
<text x="56" y="138" text-anchor="start" dominant-baseline="central" font-family="var(--sans)" font-size="13" font-weight="500" fill="#0C447C">React 19 + TypeScript · Vite 6 (SPA 前端应用)</text>
|
||||||
|
<rect x="60" y="156" width="132" height="84" rx="8" fill="#B5D4F4" stroke="#185FA5" stroke-width="0.5"/>
|
||||||
|
<text x="126" y="182" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)" font-size="13" font-weight="500" fill="#0C447C">路由层</text>
|
||||||
|
<text x="126" y="206" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)" font-size="12" fill="#185FA5">Router7 · 懒加载 · 鉴权</text>
|
||||||
|
<rect x="204" y="156" width="132" height="84" rx="8" fill="#B5D4F4" stroke="#185FA5" stroke-width="0.5"/>
|
||||||
|
<text x="270" y="182" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)" font-size="13" font-weight="500" fill="#0C447C">页面层</text>
|
||||||
|
<text x="270" y="206" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)" font-size="12" fill="#185FA5">Home·设备·视频·告警·AI</text>
|
||||||
|
<rect x="348" y="156" width="132" height="84" rx="8" fill="#B5D4F4" stroke="#185FA5" stroke-width="0.5"/>
|
||||||
|
<text x="414" y="182" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)" font-size="13" font-weight="500" fill="#0C447C">组件层</text>
|
||||||
|
<text x="414" y="206" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)" font-size="12" fill="#185FA5">Layout·devices·alerts</text>
|
||||||
|
<rect x="492" y="156" width="132" height="84" rx="8" fill="#B5D4F4" stroke="#185FA5" stroke-width="0.5"/>
|
||||||
|
<text x="558" y="182" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)" font-size="13" font-weight="500" fill="#0C447C">状态管理</text>
|
||||||
|
<text x="558" y="206" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)" font-size="12" fill="#185FA5">Redux (user·station)</text>
|
||||||
|
<path d="M340 252 L340 274" fill="none" stroke="#888780" stroke-width="1" marker-end="url(#ar)"/>
|
||||||
|
<rect x="40" y="276" width="600" height="86" rx="10" fill="#E6F1FB" stroke="#185FA5" stroke-width="0.5"/>
|
||||||
|
<text x="56" y="294" text-anchor="start" dominant-baseline="central" font-family="var(--sans)" font-size="13" font-weight="500" fill="#0C447C">实时通信层(四通道并行)</text>
|
||||||
|
<rect x="60" y="310" width="132" height="44" rx="8" fill="#B5D4F4" stroke="#185FA5" stroke-width="0.5"/>
|
||||||
|
<text x="126" y="324" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)" font-size="13" font-weight="500" fill="#0C447C">HTTP · Axios</text>
|
||||||
|
<text x="126" y="342" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)" font-size="12" fill="#185FA5">API 请求 · 401 拦截</text>
|
||||||
|
<rect x="204" y="310" width="132" height="44" rx="8" fill="#B5D4F4" stroke="#185FA5" stroke-width="0.5"/>
|
||||||
|
<text x="270" y="324" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)" font-size="13" font-weight="500" fill="#0C447C">MQTT</text>
|
||||||
|
<text x="270" y="342" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)" font-size="12" fill="#185FA5">设备遥测 useMqtt</text>
|
||||||
|
<rect x="348" y="310" width="132" height="44" rx="8" fill="#B5D4F4" stroke="#185FA5" stroke-width="0.5"/>
|
||||||
|
<text x="414" y="324" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)" font-size="13" font-weight="500" fill="#0C447C">WebSocket</text>
|
||||||
|
<text x="414" y="342" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)" font-size="12" fill="#185FA5">设备控制 wsManager</text>
|
||||||
|
<rect x="492" y="310" width="132" height="44" rx="8" fill="#B5D4F4" stroke="#185FA5" stroke-width="0.5"/>
|
||||||
|
<text x="558" y="324" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)" font-size="13" font-weight="500" fill="#0C447C">WebRTC</text>
|
||||||
|
<text x="558" y="342" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)" font-size="12" fill="#185FA5">视频流 JSWebrtc</text>
|
||||||
|
<path d="M340 362 L340 384" fill="none" stroke="#888780" stroke-width="1" marker-end="url(#ar)"/>
|
||||||
|
<rect x="40" y="386" width="600" height="86" rx="10" fill="#E1F5EE" stroke="#0F6E56" stroke-width="0.5"/>
|
||||||
|
<text x="56" y="404" text-anchor="start" dominant-baseline="central" font-family="var(--sans)" font-size="13" font-weight="500" fill="#085041">后端服务层(1.95.137.212)</text>
|
||||||
|
<rect x="60" y="420" width="132" height="44" rx="8" fill="#9FE1CB" stroke="#0F6E56" stroke-width="0.5"/>
|
||||||
|
<text x="126" y="434" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)" font-size="13" font-weight="500" fill="#085041">API server</text>
|
||||||
|
<text x="126" y="452" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)" font-size="12" fill="#0F6E56">:8081 REST 业务</text>
|
||||||
|
<rect x="204" y="420" width="132" height="44" rx="8" fill="#9FE1CB" stroke="#0F6E56" stroke-width="0.5"/>
|
||||||
|
<text x="270" y="434" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)" font-size="13" font-weight="500" fill="#085041">MQTT broker</text>
|
||||||
|
<text x="270" y="452" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)" font-size="12" fill="#0F6E56">:8083 消息</text>
|
||||||
|
<rect x="348" y="420" width="132" height="44" rx="8" fill="#9FE1CB" stroke="#0F6E56" stroke-width="0.5"/>
|
||||||
|
<text x="414" y="434" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)" font-size="13" font-weight="500" fill="#085041">WS server</text>
|
||||||
|
<text x="414" y="452" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)" font-size="12" fill="#0F6E56">:9002 设备控制</text>
|
||||||
|
<rect x="492" y="420" width="132" height="44" rx="8" fill="#9FE1CB" stroke="#0F6E56" stroke-width="0.5"/>
|
||||||
|
<text x="558" y="434" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)" font-size="13" font-weight="500" fill="#085041">GeoServer</text>
|
||||||
|
<text x="558" y="452" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)" font-size="12" fill="#0F6E56">:59018 GIS 地图</text>
|
||||||
|
</svg>
|
||||||
|
<div class="cap">图 1 · 系统四层分层架构</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="s4">
|
||||||
|
<h2><span class="num">4</span>核心模块详解</h2>
|
||||||
|
|
||||||
|
<h3>4.1 路由层 <code>src/router</code></h3>
|
||||||
|
<p>采用 <code>createBrowserRouter</code> + 全量 <code>lazy()</code> 懒加载,首屏只加载必要代码。路由通过 <code>routeMeta</code> 声明每个路径的 <strong>角色白名单</strong>(admin / operator / viewer)与 <code>requiresAuth</code>,<code>ProtectedRoute</code> 在进入受保护路由前校验登录态与权限,未登录跳转 <code>/login</code>。</p>
|
||||||
|
|
||||||
|
<h3>4.2 页面层 <code>src/pages</code></h3>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>页面</th><th>职责</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>Home / overview</td><td>总览首页,聚合设备状态、告警、作业统计概览</td></tr>
|
||||||
|
<tr><td>Devices 相关</td><td>设备总览 / 状态 / 航线任务 / 机器人任务</td></tr>
|
||||||
|
<tr><td>Video / VideoMonitorPage</td><td>在线视频与视频监控,多路 WebRTC 拉流</td></tr>
|
||||||
|
<tr><td>RealTimeMonitor</td><td>实时监控,Cesium 地图 + 设备轨迹</td></tr>
|
||||||
|
<tr><td>Alerts</td><td>告警中心:实时 / 历史 / 统计 / 规则 / 订阅</td></tr>
|
||||||
|
<tr><td>AIAnalysis</td><td>AI 诊断分析(热成像分析)</td></tr>
|
||||||
|
<tr><td>CleanOptimization</td><td>清洗优化</td></tr>
|
||||||
|
<tr><td>WorkOrder</td><td>工单任务管理</td></tr>
|
||||||
|
<tr><td>ReportCenter / Downloads</td><td>报表中心 / 视频下载</td></tr>
|
||||||
|
<tr><td>SystemSetting(嵌套子路由)</td><td>系统设置:基础设置、用户、菜单、组织、场站、设备接入、系统维护等</td></tr>
|
||||||
|
<tr><td>Login / Profile / Users / Roles</td><td>登录 / 个人中心 / 用户管理 / 角色管理</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3>4.3 组件层 <code>src/components</code></h3>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>目录/组件</th><th>职责</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>Layout.tsx</td><td>应用外壳:侧边栏菜单(动态构建菜单树)、顶栏、场站切换器、全屏控制</td></tr>
|
||||||
|
<tr><td>ProtectedRoute</td><td>路由级鉴权守卫</td></tr>
|
||||||
|
<tr><td>devices/</td><td>设备控制核心:DeviceControl、DeviceController(控制类)、DroneVideoPlayer、WayLinePage、RobotTaskPage、XboxController 手柄</td></tr>
|
||||||
|
<tr><td>alerts/</td><td>告警六件套:实时 / 历史 / 概览 / 统计 / 规则 / 订阅</td></tr>
|
||||||
|
<tr><td>SystemSetting/</td><td>系统设置全部子页面</td></tr>
|
||||||
|
<tr><td>Map.jsx</td><td>Cesium 三维地图封装</td></tr>
|
||||||
|
<tr><td>VideoPlayer / VideoCanvas / AgoraRtc / VolcRtcPlayer / useWebRTCStream</td><td>多策略视频播放组件</td></tr>
|
||||||
|
<tr><td>WebSocketManager.ts</td><td>WebSocket 单例管理器(见 5.3)</td></tr>
|
||||||
|
<tr><td>Weather/</td><td>天气动效组件(晴/云/雨)</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3>4.4 状态管理 <code>src/store</code></h3>
|
||||||
|
<div class="card blue">
|
||||||
|
<p>Redux Toolkit,仅两个切片,均<strong>持久化到 localStorage</strong>,刷新自动恢复:</p>
|
||||||
|
<ul>
|
||||||
|
<li><code>userSlice</code>:token、userInfo、isAuthenticated;含 <code>userLogin</code> 异步 thunk(调登录 API → 存 token / cookie / userInfo)与 <code>logout</code>、<code>setUserInfo</code> 同步 action。</li>
|
||||||
|
<li><code>stationSlice</code>:currentStation、stationId;场站切换后所有页面据此过滤数据。</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>4.5 API 服务层 <code>src/api</code></h3>
|
||||||
|
<p><code>request.ts</code> 创建全局唯一 Axios 实例:<code>baseURL</code> 来自 <code>env.js</code>,超时 100s,<code>withCredentials</code>。请求拦截器注入 <code>Authorization: Bearer <token></code>;响应拦截器对 401(业务码或 HTTP 状态)触发<strong>带全局锁的统一登出</strong>(<code>isLoggingOut</code> 保证并发 401 只执行一次),清存储 → dispatch logout → 跳转 /login。</p>
|
||||||
|
<p>业务 API 按域拆分:<code>api.ts</code>(设备任务/作业/热成像)、<code>device.ts</code>、<code>aiAnalysis.ts</code>、<code>alarmApi.js</code>、<code>workOrder.ts</code>、<code>reportDownloader.ts</code>、<code>systemSetting.ts</code>,以及 login / mune / organization / role / stationManage / user 子模块。</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="s5">
|
||||||
|
<h2><span class="num">5</span>实时通信层设计</h2>
|
||||||
|
<p>这是系统区别于普通 CRUD 后台的核心。四条通道各司其职、并行工作:</p>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>通道</th><th>协议</th><th>方向</th><th>用途</th><th>前端实现</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>业务接口</td><td>HTTP/REST</td><td>请求-响应</td><td>用户/设备/工单/报表等增删改查</td><td>Axios 实例 + 拦截器</td></tr>
|
||||||
|
<tr><td>设备遥测</td><td>MQTT over WS</td><td>发布-订阅(上行)</td><td>机器人实时位置、状态、传感器数据广播</td><td><code>useMqtt</code> Hook(自动重连 3s)</td></tr>
|
||||||
|
<tr><td>设备控制</td><td>WebSocket</td><td>双向</td><td>下发手柄/触控指令、获取控制权、保活</td><td><code>WebSocketManager</code> 单例 + <code>DeviceController</code></td></tr>
|
||||||
|
<tr><td>视频回传</td><td>WebRTC</td><td>单向拉流</td><td>无人机/机器人摄像头实时画面</td><td>JSWebrtc.Player / Agora / Volcengine RTC</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="card teal">
|
||||||
|
<h3>WebSocketManager 单例设计要点</h3>
|
||||||
|
<ul>
|
||||||
|
<li><strong>单例</strong>:全局唯一连接,按设备序列号 <code>serialNumber</code> 绑定,切换设备先关旧连新。</li>
|
||||||
|
<li><strong>AUTH 握手</strong>:连接 onopen 后立即发送 <code>{type:'AUTH', token, userName, deviceId}</code> 认证包。</li>
|
||||||
|
<li><strong>自动重连</strong>:非主动关闭时,onclose 后 5s 自动重连;切后台关连接、切前台自动恢复。</li>
|
||||||
|
<li><strong>观察者模式</strong>:<code>onMessage</code> / <code>onStatusChange</code> 注册回调,返回取消订阅函数。</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card coral">
|
||||||
|
<h3>DeviceController 控制类</h3>
|
||||||
|
<p>封装设备控制全流程:申请控制权(<code>getControlRight</code>)→ 握手连接 → <code>sendGamepad</code> 经 lodash <code>throttle(50ms)</code> 节流下发手柄数据 → 通过回调上抛设备信息 / 控制状态 / WS 状态变化。手柄输入 50ms 一发,兼顾流畅与带宽。</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="s6">
|
||||||
|
<h2><span class="num">6</span>数据流设计</h2>
|
||||||
|
|
||||||
|
<h3>6.1 认证与会话流</h3>
|
||||||
|
<div class="flow">
|
||||||
|
<div class="step">Login 页</div><div class="arr">→</div>
|
||||||
|
<div class="step">userLogin thunk</div><div class="arr">→</div>
|
||||||
|
<div class="step">/login API</div><div class="arr">→</div>
|
||||||
|
<div class="step">存 token/cookie/userInfo</div><div class="arr">→</div>
|
||||||
|
<div class="step">ProtectedRoute 放行</div>
|
||||||
|
</div>
|
||||||
|
<p>登录成功后 token 写入 localStorage 与 cookie(含跨域 sharetoken),Layout 据用户 ID 拉取<strong>菜单列表</strong>(动态构建侧边栏)与<strong>场站列表</strong>(默认选第一个)。后续所有请求由拦截器自动带 token;401 时统一登出回登录页。</p>
|
||||||
|
|
||||||
|
<h3>6.2 实时设备控制环(核心链路)</h3>
|
||||||
|
<div class="diagram">
|
||||||
|
<svg viewBox="0 0 680 440" width="100%" role="img" aria-label="实时设备控制与遥测数据流">
|
||||||
|
<defs><marker id="ar2" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M2 1L8 5L2 9" fill="none" stroke="context-stroke" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></marker></defs>
|
||||||
|
<text x="340" y="26" text-anchor="middle" font-family="var(--sans)" font-size="15" font-weight="500" fill="#0C447C">实时设备控制与遥测数据流</text>
|
||||||
|
<rect x="230" y="50" width="220" height="52" rx="8" fill="#B5D4F4" stroke="#185FA5" stroke-width="0.5"/>
|
||||||
|
<text x="340" y="70" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)" font-size="13" font-weight="500" fill="#0C447C">操作员</text>
|
||||||
|
<text x="340" y="88" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)" font-size="12" fill="#185FA5">手柄 / 触控指令</text>
|
||||||
|
<path d="M340 102 L340 128" fill="none" stroke="#185FA5" stroke-width="1.2" marker-end="url(#ar2)"/>
|
||||||
|
<rect x="230" y="128" width="220" height="52" rx="8" fill="#B5D4F4" stroke="#185FA5" stroke-width="0.5"/>
|
||||||
|
<text x="340" y="148" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)" font-size="13" font-weight="500" fill="#0C447C">DeviceController</text>
|
||||||
|
<text x="340" y="166" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)" font-size="12" fill="#185FA5">设备控制类 · 50ms 节流</text>
|
||||||
|
<path d="M340 180 L340 206" fill="none" stroke="#185FA5" stroke-width="1.2" marker-end="url(#ar2)"/>
|
||||||
|
<rect x="230" y="206" width="220" height="52" rx="8" fill="#B5D4F4" stroke="#185FA5" stroke-width="0.5"/>
|
||||||
|
<text x="340" y="226" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)" font-size="13" font-weight="500" fill="#0C447C">WebSocketManager</text>
|
||||||
|
<text x="340" y="244" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)" font-size="12" fill="#185FA5">单例 · AUTH 握手 · 自动重连</text>
|
||||||
|
<path d="M340 258 L340 284" fill="none" stroke="#185FA5" stroke-width="1.2" marker-end="url(#ar2)"/>
|
||||||
|
<rect x="230" y="284" width="220" height="52" rx="8" fill="#9FE1CB" stroke="#0F6E56" stroke-width="0.5"/>
|
||||||
|
<text x="340" y="304" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)" font-size="13" font-weight="500" fill="#085041">WS server :9002</text>
|
||||||
|
<text x="340" y="322" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)" font-size="12" fill="#0F6E56">设备控制服务</text>
|
||||||
|
<path d="M340 336 L340 362" fill="none" stroke="#0F6E56" stroke-width="1.2" marker-end="url(#ar2)"/>
|
||||||
|
<rect x="230" y="362" width="220" height="52" rx="8" fill="#9FE1CB" stroke="#0F6E56" stroke-width="0.5"/>
|
||||||
|
<text x="340" y="382" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)" font-size="13" font-weight="500" fill="#085041">机器人 / 无人机</text>
|
||||||
|
<text x="340" y="400" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)" font-size="12" fill="#0F6E56">执行动作 · 采集状态</text>
|
||||||
|
<path d="M450 388 L492 388 L492 76 L450 76" fill="none" stroke="#D85A30" stroke-width="1.2" stroke-dasharray="5 4" marker-end="url(#ar2)"/>
|
||||||
|
<text x="500" y="226" text-anchor="start" dominant-baseline="central" font-family="var(--sans)" font-size="12" font-weight="500" fill="#993C1D">MQTT</text>
|
||||||
|
<text x="500" y="244" text-anchor="start" dominant-baseline="central" font-family="var(--sans)" font-size="12" fill="#D85A30">遥测上行</text>
|
||||||
|
<text x="500" y="262" text-anchor="start" dominant-baseline="central" font-family="var(--sans)" font-size="12" fill="#D85A30">→ 监控页</text>
|
||||||
|
</svg>
|
||||||
|
<div class="cap">图 2 · 控制指令下行(蓝色实线 WebSocket)与遥测上行(橙色虚线 MQTT)走两条独立通道</div>
|
||||||
|
</div>
|
||||||
|
<p>关键设计:<strong>控制下行用 WebSocket(低延迟、可靠、双向),状态上行用 MQTT(发布订阅、一对多广播)</strong>。两条通道互不阻塞——即便视频/遥测流量大,也不会影响控制指令的实时性。手柄输入经 50ms 节流,避免高频刷爆带宽。</p>
|
||||||
|
|
||||||
|
<h3>6.3 实时监控遥测流</h3>
|
||||||
|
<div class="flow">
|
||||||
|
<div class="step t">机器人设备</div><div class="arr">→</div>
|
||||||
|
<div class="step t">MQTT broker :8083</div><div class="arr">→</div>
|
||||||
|
<div class="step t">useMqtt 订阅</div><div class="arr">→</div>
|
||||||
|
<div class="step t">监控页/Cesium 地图</div>
|
||||||
|
</div>
|
||||||
|
<p>设备按 topic 发布状态(位置、电量、传感器等),前端 <code>useMqtt</code> 订阅后驱动 Cesium 地图更新设备图标位置与实时监控面板。</p>
|
||||||
|
|
||||||
|
<h3>6.4 视频回传流</h3>
|
||||||
|
<div class="flow">
|
||||||
|
<div class="step">机器人摄像头</div><div class="arr">→</div>
|
||||||
|
<div class="step">WebRTC/流媒体服务</div><div class="arr">→</div>
|
||||||
|
<div class="step">JSWebrtc.Player</div><div class="arr">→</div>
|
||||||
|
<div class="step"><video> 元素</div>
|
||||||
|
</div>
|
||||||
|
<p><code>useWebRTCPlayer(deviceId, url)</code> 以 deviceId+url 为唯一键创建独立 video 元素并拉流,组件卸载时统一销毁 player,防止内存泄漏。同时支持 Agora RTC 与火山引擎 RTC 两套备选方案。</p>
|
||||||
|
|
||||||
|
<h3>6.5 HTTP 业务数据流</h3>
|
||||||
|
<div class="flow">
|
||||||
|
<div class="step">页面/组件</div><div class="arr">→</div>
|
||||||
|
<div class="step">api/*.ts</div><div class="arr">→</div>
|
||||||
|
<div class="step">request.ts (拦截器)</div><div class="arr">→</div>
|
||||||
|
<div class="step">API server :8081</div>
|
||||||
|
</div>
|
||||||
|
<p>所有 REST 请求经统一 Axios 实例:请求拦截器注入 Bearer token,响应拦截器解包 <code>response.data</code> 并统一处理 401 登出。</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="s7">
|
||||||
|
<h2><span class="num">7</span>目录结构</h2>
|
||||||
|
<pre><code>机器人智慧服务系统/
|
||||||
|
├── 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 入口</code></pre>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="s8">
|
||||||
|
<h2><span class="num">8</span>关键设计决策与特点</h2>
|
||||||
|
|
||||||
|
<div class="card blue">
|
||||||
|
<h3>多协议并行,职责分离</h3>
|
||||||
|
<p>控制(WS)、遥测(MQTT)、视频(WebRTC)、业务(HTTP)各走独立通道。这是物联网实时控制系统的成熟模式:避免单通道拥塞互相拖累,也便于后端按特性独立伸缩。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card teal">
|
||||||
|
<h3>全路由懒加载 + RBAC 元数据声明</h3>
|
||||||
|
<p>所有页面 <code>lazy()</code> 按需加载,首屏轻量;权限不写死在组件里,而是在 <code>routeMeta</code> 集中声明角色白名单,<code>ProtectedRoute</code> 统一校验,新增页面只改路由表即可获得权限控制。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card blue">
|
||||||
|
<h3>状态最小化 + 本地持久化</h3>
|
||||||
|
<p>Redux 仅保留 user / station 两个跨页全局状态,其余页面数据就地管理;两者都持久化到 localStorage,刷新即恢复,体验接近原生应用。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card coral">
|
||||||
|
<h3>连接健壮性</h3>
|
||||||
|
<p>WebSocket 单例 + AUTH 握手 + 5s 自动重连 + 页面可见性监听(切后台关、切前台重连);MQTT 3s 自动重连;Axios 401 全局锁防并发重复登出。多重保障弱网/切屏场景下的连接稳定。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="note warn">
|
||||||
|
<strong>待优化项(建议):</strong>
|
||||||
|
<ul style="margin-top:6px">
|
||||||
|
<li><code>useMqtt.ts</code> 中 MQTT 凭据硬编码在前端(username/password),建议改为后端动态下发 token。</li>
|
||||||
|
<li><code>useWebRTCStream.tsx</code> 中视频 token 硬编码,应从登录态/接口动态获取。</li>
|
||||||
|
<li><code>vite.config.ts</code> 的 manualChunks 分包策略被注释掉,Cesium 等大体积依赖建议启用分包以优化加载。</li>
|
||||||
|
<li>Redux store 仅两个切片,部分跨组件共享的设备/告警实时数据可考虑纳入统一管理或用 SWR/缓存层。</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
光伏电站智能监控与运维系统 · 架构设计文档 · 由 WorkBuddy 基于源码静态分析生成
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
80
docs/越权-冲突-超时测试记录.md
Normal file
80
docs/越权-冲突-超时测试记录.md
Normal file
@@ -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 用例全量通过 | 测试组 |
|
||||||
68
services/SERVICES.md
Normal file
68
services/SERVICES.md
Normal file
@@ -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
|
||||||
|
```
|
||||||
56
services/command/data/commands.json
Normal file
56
services/command/data/commands.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
870
services/command/package-lock.json
generated
Normal file
870
services/command/package-lock.json
generated
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
services/command/package.json
Normal file
14
services/command/package.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
249
services/command/server.js
Normal file
249
services/command/server.js
Normal file
@@ -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'}`);
|
||||||
|
});
|
||||||
21
services/device-access/data/devices.json
Normal file
21
services/device-access/data/devices.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
870
services/device-access/package-lock.json
generated
Normal file
870
services/device-access/package-lock.json
generated
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
services/device-access/package.json
Normal file
14
services/device-access/package.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
176
services/device-access/server.js
Normal file
176
services/device-access/server.js
Normal file
@@ -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`);
|
||||||
|
});
|
||||||
42
services/status/data/status.json
Normal file
42
services/status/data/status.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
870
services/status/package-lock.json
generated
Normal file
870
services/status/package-lock.json
generated
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
services/status/package.json
Normal file
14
services/status/package.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
125
services/status/server.js
Normal file
125
services/status/server.js
Normal file
@@ -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)`);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user