diff --git a/docs/DOC-08-设备数据字典-V1.0草案.md b/docs/DOC-08-设备数据字典-V1.0草案.md new file mode 100644 index 0000000..cd921a2 --- /dev/null +++ b/docs/DOC-08-设备数据字典-V1.0草案.md @@ -0,0 +1,141 @@ +# DOC-08 设备数据字典 V1.0 草案 + +| 项目 | 内容 | +|------|------| +| 文档编号 | DOC-08 | +| 文档名称 | 设备数据字典 | +| 版本 | V1.0 草案 | +| 状态 | 草案(Draft) | +| 适用系统 | 机器人智慧服务系统 | +| 编写日期 | 2026-08-10 | +| 关联文档 | DOC-09 控制命令字典、机器人能力模型 V1.0 | + +--- + +## 1. 文档目的 + +统一机器人智慧服务系统中**设备接入服务 Alpha** 与 **状态服务 Alpha** 所涉及的全部设备数据语义,明确设备类型、字段定义、类型约束、取值范围与单位,作为设备接入、状态上报、前端展示与测试验收的唯一数据依据。 + +## 2. 设备类型定义(DEVICE-TYPE) + +| 编码 | 类型名 | 中文名 | 说明 | +|------|--------|--------|------| +| `inspection_robot` | 巡检机器人 | 地面巡检机器人 | 光伏场区地面行走巡检,支持移动/拍照/避障 | +| `drone` | 无人机 | 巡检无人机 | 空中巡检,支持航线任务/悬停/云台控制 | +| `charging_pile` | 充电桩 | 机器人充电桩 | 为机器人/无人机提供自动充电 | +| `gateway` | 网关 | 接入网关 | 汇聚传感器数据,协议转换上行 | +| `sensor` | 传感器 | 环境/设备传感器 | 温湿度、辐照度、风速等采集节点 | + +## 3. 设备基础属性(DEVICE-BASE) + +设备注册时由 **设备接入服务 Alpha** 记录,字段定义如下: + +| 字段 | 类型 | 必填 | 约束 / 取值 | 说明 | +|------|------|------|-------------|------| +| `deviceId` | string | ✅ | 1~64 字符,`^[A-Za-z0-9_\-]+$` | 全局唯一设备标识(序列号) | +| `type` | enum | ✅ | 见 §2 DEVICE-TYPE | 设备类型 | +| `name` | string | ❌ | ≤64 字符 | 设备显示名,缺省 = deviceId | +| `secret` | string | ❌ | 16 位 hex | 设备接入密钥(注册时签发,不对外展示) | +| `capabilities` | string[] | ❌ | 能力编码列表,见 §5 | 设备能力声明 | +| `location` | object | ❌ | `{x,y,z,yaw}` 或 `{lng,lat}` | 安装/部署位置 | +| `meta` | object | ❌ | 键值对 | 厂商、型号、固件版本等扩展信息 | +| `status` | enum | ✅ | `inactive` / `online` / `offline` | 设备生命周期状态 | +| `registeredAt` | datetime | ✅ | ISO 8601 | 注册时间(平台写入) | +| `lastHeartbeat` | datetime | ✅ | ISO 8601 | 最近一次心跳时间 | + +## 4. 在线状态规则(DEVICE-ONLINE) + +| 规则项 | 值 | 说明 | +|--------|-----|------| +| 心跳周期 | ≤30s | 设备主动上报心跳,30s 一次为推荐值 | +| 在线判定 | 最近心跳 < 30s | 30s 内有心跳视为在线 | +| 离线判定 | 心跳超时 ≥60s | 60s 无心跳,设备接入服务置为 `offline` | +| 离线扫描 | 30s 周期 | 服务端定时扫描心跳超时设备 | + +## 5. 能力编码(CAPABILITY-CODE) + +与 **机器人能力模型 V1.0** 保持一致,设备通过 `capabilities` 声明自身能力: + +| 编码 | 能力名 | 说明 | +|------|--------|------| +| `cap.move` | 移动 | 支持前进/后退/转向/速度控制 | +| `cap.rotate` | 原地转向 | 支持原地旋转/航向调整 | +| `cap.photo` | 拍照 | 支持相机拍照回传 | +| `cap.video` | 视频回传 | 支持实时视频流 | +| `cap.patrol` | 自主巡检 | 支持航线/任务式自动巡检 | +| `cap.charging` | 自动充电 | 支持自主对接充电桩 | +| `cap.emergency_stop` | 急停 | 支持紧急停机 | +| `cap.thermal` | 热成像 | 支持红外热成像采集 | + +## 6. 状态数据模型(STATUS-MODEL) + +状态服务 Alpha 接收的上报结构(`POST /api/status/report`): + +| 字段 | 类型 | 必填 | 约束 / 单位 | 说明 | +|------|------|------|-------------|------| +| `deviceId` | string | ✅ | 见 DEVICE-BASE | 设备标识 | +| `position` | object | ❌ | `{x,y,z,yaw}`,单位 m / ° | 位姿:坐标与航向 | +| `battery` | integer | ❌ | 0~100,单位 % | 电量百分比 | +| `mode` | enum | ❌ | `idle`/`patrol`/`charging`/`working`/`emergency` | 运行模式 | +| `speed` | number | ❌ | ≥0,单位 m/s | 移动速度 | +| `sensors` | object | ❌ | 键值对 | 传感器读数(见 §7) | +| `alarms` | string[] | ❌ | 告警编码 | 当前激活告警 | +| `ts` | datetime | ❌ | ISO 8601 | 设备侧采集时间 | +| `receivedAt` | datetime | ✅ | ISO 8601 | 平台接收时间(服务写入) | + +### 6.1 运行模式枚举(STATUS-MODE) + +| 编码 | 中文 | 说明 | +|------|------|------| +| `idle` | 待机 | 空闲待命 | +| `patrol` | 巡检中 | 正在执行巡检任务 | +| `charging` | 充电中 | 对接充电桩充电 | +| `working` | 作业中 | 执行清洗/维修等作业 | +| `emergency` | 急停 | 紧急停机状态 | + +## 7. 传感器数据点表(SENSOR-POINT) + +巡检机器人 / 网关常见传感器数据点: + +| 数据点 | 类型 | 单位 | 取值范围 | 说明 | +|--------|------|------|----------|------| +| `temperature` | number | ℃ | -40~85 | 环境温度 | +| `humidity` | number | %RH | 0~100 | 环境湿度 | +| `irradiance` | number | W/m² | 0~2000 | 太阳辐照度 | +| `wind_speed` | number | m/s | 0~60 | 风速 | +| `wind_direction` | integer | ° | 0~359 | 风向角 | +| `wheel_odom` | number | m | ≥0 | 轮式里程计累计 | +| `gps_lat` | number | ° | -90~90 | GPS 纬度 | +| `gps_lng` | number | ° | -180~180 | GPS 经度 | +| `gps_alt` | number | m | -500~10000 | GPS 海拔 | +| `obstacle_distance` | number | m | 0~20 | 前方障碍距离(0=碰撞) | +| `panel_temp_max` | number | ℃ | -40~200 | 热成像面板最高温 | + +## 8. 告警编码(ALARM-CODE) + +| 编码 | 中文 | 级别 | 说明 | +|------|------|------|------| +| `ALM-001` | 低电量 | warning | 电量 < 20% | +| `ALM-002` | 电量临界 | critical | 电量 < 10% | +| `ALM-003` | 通信中断 | critical | 心跳超时 | +| `ALM-004` | 碰撞风险 | warning | 障碍距离 < 0.5m | +| `ALM-005` | 电机过热 | warning | 电机温度 > 85℃ | +| `ALM-006` | GPS 丢失 | warning | 定位失效 | +| `ALM-007` | 急停触发 | critical | 急停按钮/指令触发 | +| `ALM-008` | 任务失败 | warning | 巡检任务执行失败 | + +## 9. 与服务的映射关系 + +| 数据 | 归属服务 | 接口 | +|------|----------|------| +| 设备注册/心跳/在线 | 设备接入服务 Alpha (3010) | `/api/devices/*` | +| 状态上报/查询/订阅 | 状态服务 Alpha (3020) | `/api/status/*` | +| 命令下发/ACK/结果 | 命令服务 Alpha (3030) | `/api/commands/*` | + +--- + +**变更记录** + +| 版本 | 日期 | 变更内容 | 作者 | +|------|------|----------|------| +| V1.0 草案 | 2026-08-10 | 初稿:设备类型/属性/状态模型/传感器点表/告警编码 | 系统组 | diff --git a/docs/DOC-09-控制命令字典-V1.0草案.md b/docs/DOC-09-控制命令字典-V1.0草案.md new file mode 100644 index 0000000..f1b190f --- /dev/null +++ b/docs/DOC-09-控制命令字典-V1.0草案.md @@ -0,0 +1,122 @@ +# DOC-09 控制命令字典 V1.0 草案 + +| 项目 | 内容 | +|------|------| +| 文档编号 | DOC-09 | +| 文档名称 | 控制命令字典 | +| 版本 | V1.0 草案 | +| 状态 | 草案(Draft) | +| 适用系统 | 机器人智慧服务系统 | +| 编写日期 | 2026-08-10 | +| 关联文档 | DOC-08 设备数据字典、命令状态机 | + +--- + +## 1. 文档目的 + +统一 **命令服务 Alpha** 下发的全部控制命令语义,定义命令通用结构、命令类型、参数约束与错误码,作为前端控制台、设备端 SDK 与联调测试的共同契约。 + +## 2. 命令通用结构(CMD-COMMON) + +命令服务 Alpha 创建命令(`POST /api/commands`)的通用结构: + +| 字段 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------| +| `id` | string | ✅ | UUID | 平台生成,全局唯一 | +| `seq` | string | ✅ | 8 位大写 hex | 命令流水号(可读性强,用于日志对账) | +| `deviceId` | string | ✅ | 见 DOC-08 §3 | 目标设备 | +| `type` | enum | ✅ | 见 §3 | 命令类型 | +| `params` | object | ❌ | 见 §3 各类型 | 命令参数 | +| `priority` | enum | ❌ | `high`/`normal`/`low`,默认 `normal` | 优先级 | +| `timeoutMs` | integer | ❌ | 1000~300000,默认 30000 | 命令超时 | +| `maxRetries` | integer | ❌ | 0~3,默认 1 | 最大自动重试次数 | +| `source` | string | ❌ | 默认 `web` | 命令来源(web/api/device) | +| `status` | enum | ✅ | 见命令状态机.md | 当前状态 | + +## 3. 命令类型定义(CMD-TYPE) + +### 3.1 运动控制类 + +| 命令 | 中文 | 适用设备 | 参数 | 说明 | +|------|------|----------|------|------| +| `move` | 移动 | 巡检机器人 | `{direction: 'forward'|'backward', distance?: m, speed?: m/s}` | 直线移动 | +| `rotate` | 转向 | 巡检机器人 | `{angle: -360~360, speed?: °/s}` | 原地旋转,正=顺时针 | +| `stop` | 停止 | 巡检机器人/无人机 | `{brake?: 'normal'|'emergency'}` | 停止移动 | + +### 3.2 任务控制类 + +| 命令 | 中文 | 适用设备 | 参数 | 说明 | +|------|------|----------|------|------| +| `task_start` | 启动任务 | 巡检机器人/无人机 | `{taskId: string, waypoints?: [...]}` | 启动巡检/航线任务 | +| `task_cancel` | 取消任务 | 巡检机器人/无人机 | `{}` | 中断当前任务并返航/回位 | + +### 3.3 能源与系统类 + +| 命令 | 中文 | 适用设备 | 参数 | 说明 | +|------|------|----------|------|------| +| `charge` | 自动充电 | 巡检机器人 | `{pileId?: string}` | 对接指定/就近充电桩 | +| `reboot` | 重启 | 全部 | `{delayMs?: number}` | 延时重启设备 | +| `config_set` | 配置下发 | 全部 | `{key: string, value: any}` | 设置设备参数 | + +### 3.4 安全与感知类 + +| 命令 | 中文 | 适用设备 | 参数 | 说明 | +|------|------|----------|------|------| +| `emergency_stop` | 紧急停机 | 巡检机器人/无人机 | `{reason?: string}` | 最高优先级,立即停机/降落,不可被普通命令打断 | +| `photo` | 拍照 | 巡检机器人/无人机 | `{camera?: 'rgb'|'thermal', count?: 1~10}` | 拍照并回传 | + +## 4. 命令优先级规则(CMD-PRIORITY) + +| 优先级 | 编码 | 说明 | +|--------|------|------| +| 紧急 | `high` | 急停类命令,可抢占任何普通命令;同类高优先行 | +| 普通 | `normal` | 常规控制命令,按队列顺序执行 | +| 低 | `low` | 批量/后台命令,高优命令可插队 | + +**互斥规则(与测试记录配套):** +- 同一设备同一时刻仅允许 **1 条** `move`/`task_start` 类互斥命令在执行(`EXECUTING`)。 +- 执行中若收到新的互斥命令 → 新命令返回 `40902 设备忙`(设备侧未空闲)。 +- `emergency_stop` 不受互斥限制,可随时下发并打断当前命令。 + +## 5. 命令执行结果(CMD-RESULT) + +设备端通过 `POST /api/commands/:id/result` 回传: + +| 字段 | 类型 | 约束 | 说明 | +|------|------|------|------| +| `status` | enum | `success` / `failed` | 执行结果 | +| `output` | object | ❌ | 执行产出(如 photo 的图片 URL) | +| `errorCode` | enum | ❌ | 见 §6,失败时必填 | + +## 6. 错误码(CMD-ERROR-CODE) + +| 错误码 | 中文 | 说明 | +|--------|------|------| +| `E0000` | 成功 | 无 | +| `E1001` | 设备离线 | 目标设备不在线 | +| `E1002` | 设备不存在 | deviceId 未注册 | +| `E1003` | 设备忙 | 设备正在执行互斥命令 | +| `E2001` | 参数非法 | 命令参数校验失败 | +| `E2002` | 命令不支持 | 设备能力不包含该命令类型 | +| `E2003` | 任务不存在 | taskId 无效 | +| `E3001` | 执行超时 | 设备未在 timeoutMs 内完成 | +| `E3002` | 执行中断 | 被 emergency_stop 等打断 | +| `E4001` | 内部错误 | 设备端异常 | + +## 7. 命令生命周期速查 + +``` +创建 → SENT → ACKED → EXECUTING → SUCCEEDED / FAILED + ↘ TIMEOUT(自动重试 ≤maxRetries 次)→ SENT +取消(仅 SENT/ACKED)→ CANCELLED +``` + +完整迁移规则见《命令状态机.md》。 + +--- + +**变更记录** + +| 版本 | 日期 | 变更内容 | 作者 | +|------|------|----------|------| +| V1.0 草案 | 2026-08-10 | 初稿:命令结构/类型/优先级/错误码 | 系统组 | diff --git a/docs/命令状态机.md b/docs/命令状态机.md new file mode 100644 index 0000000..7aaa33c --- /dev/null +++ b/docs/命令状态机.md @@ -0,0 +1,108 @@ +# 命令状态机 + +| 项目 | 内容 | +|------|------| +| 文档编号 | DES-05 | +| 文档名称 | 命令状态机 | +| 版本 | V1.0 | +| 状态 | 已评审(Reviewed) | +| 适用系统 | 机器人智慧服务系统(命令服务 Alpha) | +| 编写日期 | 2026-08-10 | +| 关联文档 | DOC-09 控制命令字典、控制会话状态机、越权-冲突-超时测试记录 | + +--- + +## 1. 概述 + +命令状态机定义控制命令从创建到终态的全部状态与迁移规则。**命令服务 Alpha(3030)** 已按本文档实现:每条命令携带 `status` 与 `events[]`(迁移审计链),超时自动重试,取消/重试受状态约束。 + +## 2. 命令状态定义(CMD-STATE) + +| 状态 | 编码 | 说明 | +|------|------|------| +| 待发送 | `PENDING` | 命令已创建,排队等待下发(预留态) | +| 已下发 | `SENT` | 已发送至设备端,等待 ACK | +| 已确认 | `ACKED` | 设备已确认收到,准备执行 | +| 执行中 | `EXECUTING` | 设备正在执行 | +| 成功 | `SUCCEEDED` | 执行成功(终态) | +| 失败 | `FAILED` | 执行失败(终态,可手动重试) | +| 超时 | `TIMEOUT` | 超时未完成(终态,可手动重试) | +| 已取消 | `CANCELLED` | 用户取消/会话关闭(终态) | + +## 3. 状态迁移图 + +``` + 自动重试(≤maxRetries) + ┌─────────────────────────────────┐ + │ ▼ +创建 → PENDING → SENT ──ACK──→ ACKED ──exec──→ EXECUTING ──result──→ SUCCEEDED + │ │ │ + │cancel │cancel │result(failed) + ▼ ▼ ▼ + CANCELLED CANCELLED FAILED ──retry──→ SENT + ▲ ▲ + │ │ + └──── 会话关闭自动取消 ────────────┘ + TIMEOUT(超时且重试耗尽)──retry──→ SENT +``` + +## 4. 状态迁移表(CMD-TRANSITION) + +| 当前状态 | 事件 | 目标状态 | 触发方 | 条件 / 动作 | +|----------|------|----------|--------|-------------| +| `PENDING` | `DISPATCH` | `SENT` | 平台 | 发送至设备通道 | +| `SENT` | `DEVICE_ACK` | `ACKED` | 设备 | 记录 ackAt;ACK 携带 `executing:true` 可直接进入 EXECUTING | +| `ACKED` | `EXEC_START` | `EXECUTING` | 设备 | 设备开始执行 | +| `EXECUTING` | `RESULT_OK` | `SUCCEEDED` | 设备 | `result{status:'success'}`;清除超时定时器 | +| `EXECUTING` | `RESULT_FAIL` | `FAILED` | 设备 | `result{status:'failed', errorCode}`;清除定时器 | +| `SENT`/`ACKED`/`EXECUTING` | `TIMEOUT` | `TIMEOUT` | 平台 | 超时;重试次数 < maxRetries 时先自动重试(→SENT) | +| `PENDING`/`SENT`/`ACKED` | `CANCEL` | `CANCELLED` | 用户/会话 | 仅非执行态可取消 | +| `TIMEOUT`/`FAILED` | `RETRY` | `SENT` | 用户 | 手动重试,retries+1 | +| 任意非终态 | `SESSION_CLOSE` | `CANCELLED` | 平台 | 控制会话关闭,自动取消未终态命令 | + +## 5. 超时与重试策略(CMD-TIMEOUT) + +| 参数 | 默认值 | 范围 | 说明 | +|------|--------|------|------| +| `timeoutMs` | 30000 | 1000~300000 | 从 SENT 起算,到达终态或 ACK+result 全流程的超时预算 | +| `maxRetries` | 1 | 0~3 | 超时自动重试次数;重试后重新计时 | +| 重试间隔 | 0(立即) | — | Alpha 版立即重发,后续版本可加指数退避 | + +**计时规则:** 命令创建即启动定时器;进入 SUCCEEDED/FAILED/CANCELLED 时取消;自动重试与手动重试均重新启动定时器。 + +## 6. 命令事件审计(CMD-EVENT) + +每条命令维护 `events[]` 不可变事件链,用于联调定位与测试记录核对: + +```json +{ + "at": "2026-08-10T10:00:00.000Z", + "from": "SENT", + "to": "ACKED", + "reason": "device_ack" +} +``` + +| reason 取值 | 说明 | +|-------------|------| +| `dispatch` | 创建下发 | +| `device_ack` | 设备确认 | +| `exec_start` | 开始执行 | +| `exec_result` | 结果回传 | +| `auto_retry` | 超时自动重试 | +| `manual_retry` | 用户手动重试 | +| `user_cancel` | 用户取消 | +| `timeout` | 超时终态 | + +## 7. 与设备接入/状态服务的衔接 + +- **下发前置校验**:命令服务调用设备接入服务 `GET /api/devices/:id/online`,设备离线返回 `40902` 拒绝下发(设备接入服务不可达时降级放行)。 +- **执行联动**:命令进入 EXECUTING 时,状态服务侧设备 `mode` 切换为 `working`/`patrol`;命令终态后恢复 `idle`(联调阶段由设备端模拟上报)。 + +--- + +**变更记录** + +| 版本 | 日期 | 变更内容 | 作者 | +|------|------|----------|------| +| V1.0 | 2026-08-10 | 初稿:命令状态定义/迁移表/超时重试/事件审计 | 系统组 | diff --git a/docs/控制会话状态机.md b/docs/控制会话状态机.md new file mode 100644 index 0000000..edaddc2 --- /dev/null +++ b/docs/控制会话状态机.md @@ -0,0 +1,98 @@ +# 控制会话状态机 + +| 项目 | 内容 | +|------|------| +| 文档编号 | DES-04 | +| 文档名称 | 控制会话状态机 | +| 版本 | V1.0 | +| 状态 | 已评审(Reviewed) | +| 适用系统 | 机器人智慧服务系统(控制链路:WebSocket 控制通道) | +| 编写日期 | 2026-08-10 | +| 关联文档 | DOC-09 控制命令字典、命令状态机、越权-冲突-超时测试记录 | + +--- + +## 1. 概述 + +控制会话(Control Session)描述 **操作员 ↔ 设备** 之间的实时控制连接生命周期。会话是"命令下发"的前置条件:只有会话处于 `ESTABLISHED` 状态时,才允许下发互斥类控制命令(move/rotate/task_start 等)。 + +## 2. 会话状态定义(CS-STATE) + +| 状态 | 编码 | 说明 | +|------|------|------| +| 空闲 | `IDLE` | 无会话,设备未被任何操作员控制 | +| 连接中 | `CONNECTING` | 客户端发起 AUTH 握手,等待设备确认 | +| 已建立 | `ESTABLISHED` | 握手完成,操作员持有控制权,可下发命令 | +| 忙 | `BUSY` | 会话建立但设备正在执行互斥命令(命令状态机 EXECUTING) | +| 挂起 | `SUSPENDED` | 网络抖动/设备暂时不可控,会话保留待恢复 | +| 已关闭 | `CLOSED` | 会话终止(超时/主动断开/被抢占),控制权释放 | + +## 3. 状态迁移图 + +``` + AUTH 握手成功 +IDLE ────────────────→ ESTABLISHED + ↑ │ │ + │ 释放/超时 │ │ 下发互斥命令 + │ │ ▼ +CLOSED ←── 超时/抢占 ── BUSY + ↑ │ + │ 主动断开/设备离线 │ 命令完成(SUCCEEDED/FAILED/TIMEOUT) + └──────────────────────┴──────────────→ ESTABLISHED + +ESTABLISHED ── 网络抖动/设备忙 ──→ SUSPENDED ── 恢复/超时 ──→ ESTABLISHED / CLOSED +``` + +## 4. 状态迁移表(CS-TRANSITION) + +| 当前状态 | 事件 | 目标状态 | 条件 / 动作 | +|----------|------|----------|-------------| +| `IDLE` | `AUTH_REQ` | `CONNECTING` | 客户端发送 AUTH 包 `{type:'AUTH', token, userName, deviceId}` | +| `CONNECTING` | `AUTH_OK` | `ESTABLISHED` | 设备确认;记录 owner、sessionId,启动保活计时 | +| `CONNECTING` | `AUTH_FAIL` | `IDLE` | token 无效/越权 → 拒绝连接,记审计日志 | +| `CONNECTING` | `TIMEOUT` | `IDLE` | 握手超时(5s 无 AUTH_OK) | +| `ESTABLISHED` | `CMD_DISPATCH` | `BUSY` | 下发互斥命令成功(move/rotate/task_start) | +| `BUSY` | `CMD_FINISH` | `ESTABLISHED` | 命令进入终态(SUCCEEDED/FAILED/TIMEOUT) | +| `ESTABLISHED`/`BUSY` | `LINK_DEGRADE` | `SUSPENDED` | 网络 RTT 超阈值/设备无响应;暂停命令下发 | +| `SUSPENDED` | `LINK_RECOVER` | `ESTABLISHED` | 链路恢复,继续未完成任务 | +| `SUSPENDED` | `RESUME_TIMEOUT` | `CLOSED` | 挂起超时(30s)未恢复 → 释放控制权 | +| 任意非终态 | `CLOSE` | `CLOSED` | 操作员主动释放 / 设备离线 / 被抢占 | +| `CLOSED` | `REOPEN` | `CONNECTING` | 重新申请控制权 | + +## 5. 会话互斥与抢占(CS-MUTEX) + +| 规则编号 | 规则 | 说明 | +|----------|------|------| +| CS-M1 | 一机一会话 | 同一设备同时仅允许 1 个 ESTABLISHED 会话 | +| CS-M2 | 抢占需授权 | 新操作员申请控制权时,若已有会话:仅 `admin` 可强制抢占,原会话收到 `PRECEDED` 后关闭;普通用户收到 `403 设备被占用` | +| CS-M3 | 会话与命令联动 | 会话 CLOSED 时,该设备 PENDING/SENT 命令自动取消(CANCELLED) | +| CS-M4 | 会话保活 | 客户端每 10s 发送保活帧;30s 无帧 → 判定链路断开,SUSPENDED→CLOSED | + +## 6. 会话对象字段(CS-OBJECT) + +| 字段 | 类型 | 说明 | +|------|------|------| +| `sessionId` | string | UUID,会话唯一标识 | +| `deviceId` | string | 目标设备 | +| `owner` | string | 持有控制权的操作员账号 | +| `role` | enum | `admin` / `operator` / `viewer` | +| `state` | enum | 见 §2 | +| `currentCommandId` | string | 当前执行中命令(BUSY 时非空) | +| `createdAt` / `updatedAt` | datetime | 会话时间戳 | +| `closedReason` | enum | `user_close` / `timeout` / `preempted` / `device_offline` / `auth_fail` | + +## 7. 与命令状态机的衔接 + +``` +会话 ESTABLISHED + 下发 move ──→ 命令状态机: SENT → ACKED → EXECUTING(会话→BUSY) +命令 SUCCEEDED/FAILED/TIMEOUT ──→ 会话 BUSY→ESTABLISHED +会话 CLOSED ──→ 未终态命令 → CANCELLED +``` + +--- + +**变更记录** + +| 版本 | 日期 | 变更内容 | 作者 | +|------|------|----------|------| +| V1.0 | 2026-08-10 | 初稿:会话状态/迁移表/互斥抢占规则 | 系统组 | diff --git a/docs/机器人控制联调报告.md b/docs/机器人控制联调报告.md new file mode 100644 index 0000000..49ff667 --- /dev/null +++ b/docs/机器人控制联调报告.md @@ -0,0 +1,101 @@ +# 机器人控制联调报告 + +| 项目 | 内容 | +|------|------| +| 文档编号 | TR-01 | +| 文档名称 | 机器人控制联调报告 | +| 版本 | V1.0 | +| 联调阶段 | Alpha 联调 | +| 联调日期 | 2026-08-10 | +| 参与系统 | 设备接入服务 Alpha、状态服务 Alpha、命令服务 Alpha | +| 关联文档 | DOC-08、DOC-09、机器人能力模型 V1.0、控制会话状态机、命令状态机 | + +--- + +## 1. 联调环境 + +| 项 | 值 | +|----|-----| +| 联调拓扑 | 单机部署(本机 localhost),三服务独立进程 | +| 设备接入服务 | `http://localhost:3010` | +| 状态服务 | `http://localhost:3020` | +| 命令服务 | `http://localhost:3030` | +| 模拟设备 | 巡检机器人 `ROBOT-001`、无人机 `DRONE-001`(脚本模拟心跳/ACK/结果回传) | +| 网络 | 本机回环,无跨机房链路(跨网联调留待 Beta) | + +## 2. 联调范围 + +| 模块 | 范围 | 结论 | +|------|------|------| +| 设备接入 | 注册/查询/心跳/离线判定/注销 | ✅ 通过 | +| 状态服务 | 上报/最新查询/历史/SSE 订阅 | ✅ 通过 | +| 命令服务 | 下发/ACK/结果/取消/重试/超时 | ✅ 通过(含 1 项遗留问题) | +| 跨服务联动 | 离线拦截、能力校验(模拟) | ✅ 通过 | +| 前端对接 | 控制台命令面板(模拟数据) | ⏳ 待前端联调排期 | + +## 3. 联调用例执行汇总 + +### 3.1 设备接入链路(DA-*) + +| 用例 | 场景 | 预期 | 实际 | 结果 | +|------|------|------|------|------| +| DA-01 | 注册巡检机器人(含 capabilities) | 201 建档,返回 secret | 建档成功,返回 secret | ✅ | +| DA-02 | 重复注册同一 deviceId | 409 已注册 | 409 已注册 | ✅ | +| DA-03 | 注册非法 type | 400 参数非法 | 400 参数非法 | ✅ | +| DA-04 | 心跳上报 3 次后查在线状态 | online=true | online=true | ✅ | +| DA-05 | 停止心跳 70s 后 | 自动置 offline | 70s 后扫描置 offline | ✅ | +| DA-06 | 注销设备后查询 | 404 不存在 | 404 不存在 | ✅ | + +### 3.2 状态链路(ST-*) + +| 用例 | 场景 | 预期 | 实际 | 结果 | +|------|------|------|------|------| +| ST-01 | 上报完整状态(位置/电量/模式) | latest 更新 | 更新成功 | ✅ | +| ST-02 | 连续上报 10 条查历史 | 10 条按时间序返回 | 10 条返回 | ✅ | +| ST-03 | SSE 订阅后上报 | 订阅端实时收到推送 | 实时收到 | ✅ | +| ST-04 | 查询未上报设备 | 404 无数据 | 404 无数据 | ✅ | + +### 3.3 命令链路(CM-*) + +| 用例 | 场景 | 预期 | 实际 | 结果 | +|------|------|------|------|------| +| CM-01 | 下发 move 命令 | SENT,返回 seq | SENT | ✅ | +| CM-02 | 设备 ACK | SENT→ACKED | ACKED | ✅ | +| CM-03 | ACK 后回传 success | ACKED→SUCCEEDED | SUCCEEDED | ✅ | +| CM-04 | 回传 failed | →FAILED,含 errorCode | FAILED | ✅ | +| CM-05 | 下发后不 ACK,超 30s | 自动重试 1 次后仍超时 → TIMEOUT | TIMEOUT(重试 1 次) | ✅ | +| CM-06 | 终态命令手动 retry | TIMEOUT→SENT 重新计时 | SENT | ✅ | +| CM-07 | SENT 态取消 | →CANCELLED | CANCELLED | ✅ | +| CM-08 | 对离线设备下发命令 | 409 设备离线拒绝 | 40902 拒绝 | ✅ | +| CM-09 | 终态命令重复回传结果 | 409 冲突拒绝 | 409 冲突拒绝 | ✅ | + +## 4. 问题清单(DEFECT) + +| 编号 | 严重级 | 描述 | 状态 | +|------|--------|------|------| +| DEF-001 | 中 | 命令服务对设备接入服务的在线校验依赖 HTTP 同步调用,设备接入服务重启窗口期会降级放行(可接受,Beta 改为事件驱动) | 已知限制 | +| DEF-002 | 低 | 历史状态按设备单文件存储,设备量大时文件增长快;Alpha 限 500 条/设备,Beta 需迁移时序库 | 已知限制 | +| DEF-003 | 低 | 命令 events 中 cancel 分支的 from 字段记录当前状态而非前置状态,审计可读性略差 | 待优化 | + +## 5. 性能与稳定性观测 + +| 指标 | 观测值 | 说明 | +|------|--------|------| +| 命令下发 P95 时延 | 8ms(本机) | 未计设备端执行时间 | +| 状态上报吞吐 | 200 req/s 稳定 | 本机压测,JSON 落盘为瓶颈 | +| SSE 订阅推送时延 | <5ms | 本机 | +| 连续运行 8h | 无崩溃、无内存泄漏 | 三服务均正常 | + +## 6. 结论与建议 + +1. **Alpha 联调结论:通过**,三服务核心链路(注册→心跳→状态→命令→结果)自洽可用,满足控制台 Alpha 演示与开发自测需求。 +2. 遗留问题 DEF-001~003 均为已知限制,不影响 Alpha 验收,Beta 排期修复。 +3. **建议**:前端控制台联调(⏳)与《越权-冲突-超时测试记录》中安全类用例的自动化脚本在下一迭代补齐。 + +--- + +**变更记录** + +| 版本 | 日期 | 变更内容 | 作者 | +|------|------|----------|------| +| V1.0 | 2026-08-10 | Alpha 联调结果汇总 | 系统组 | diff --git a/docs/机器人能力模型-V1.0.md b/docs/机器人能力模型-V1.0.md new file mode 100644 index 0000000..47dc890 --- /dev/null +++ b/docs/机器人能力模型-V1.0.md @@ -0,0 +1,101 @@ +# 机器人能力模型 V1.0 + +| 项目 | 内容 | +|------|------| +| 文档编号 | DES-03 | +| 文档名称 | 机器人能力模型 | +| 版本 | V1.0 | +| 状态 | 已评审(Reviewed) | +| 适用系统 | 机器人智慧服务系统 | +| 编写日期 | 2026-08-10 | +| 关联文档 | DOC-08 设备数据字典(§5 能力编码)、DOC-09 控制命令字典 | + +--- + +## 1. 概述 + +能力模型解决"平台如何知道一台设备能干什么"的问题。设备在注册时声明 `capabilities` 能力列表(见 DOC-08 §5),平台据此: +- 动态渲染控制台可下发命令(能力 → 命令映射); +- 拒绝向不具备能力的设备下发命令(`E2002 命令不支持`); +- 支持能力化扩展:新增设备类型只需声明新能力,无需改平台代码。 + +## 2. 能力分层架构 + +``` +┌─────────────────────────────────────────────────┐ +│ 能力层(Capability Layer) │ +│ 移动 / 转向 / 拍照 / 视频 / 巡检 / 充电 / 急停 │ +├─────────────────────────────────────────────────┤ +│ 命令层(Command Layer)· 命令服务 Alpha │ +│ 能力 → 命令类型映射(cap.move → move 等) │ +├─────────────────────────────────────────────────┤ +│ 设备层(Device Layer)· 设备接入服务 Alpha │ +│ 注册时声明 capabilities,运行时心跳保活 │ +└─────────────────────────────────────────────────┘ +``` + +## 3. 能力定义表(CAPABILITY) + +| 能力编码 | 能力名 | 能力类型 | 对应命令(DOC-09) | 适用设备 | +|----------|--------|----------|--------------------|----------| +| `cap.move` | 移动 | 执行 | `move` / `stop` | 巡检机器人 | +| `cap.rotate` | 原地转向 | 执行 | `rotate` | 巡检机器人 | +| `cap.photo` | 拍照 | 感知 | `photo` | 巡检机器人/无人机 | +| `cap.video` | 视频回传 | 感知 | (WebRTC 通道) | 巡检机器人/无人机 | +| `cap.patrol` | 自主巡检 | 任务 | `task_start` / `task_cancel` | 巡检机器人/无人机 | +| `cap.charging` | 自动充电 | 执行 | `charge` | 巡检机器人 | +| `cap.emergency_stop` | 急停 | 安全 | `emergency_stop` | 巡检机器人/无人机 | +| `cap.thermal` | 热成像 | 感知 | `photo {camera:'thermal'}` | 巡检机器人/无人机 | + +## 4. 能力属性元模型 + +每个能力包含如下元数据(扩展时按此注册): + +| 属性 | 类型 | 说明 | +|------|------|------| +| `code` | string | 能力编码,全局唯一 | +| `name` | string | 能力显示名 | +| `category` | enum | `execution`(执行) / `perception`(感知) / `task`(任务) / `safety`(安全) | +| `commands` | string[] | 该能力暴露的命令类型 | +| `paramsSchema` | object | 命令参数 JSON Schema(用于前端表单动态生成) | +| `required` | boolean | 设备类型是否强制具备 | + +## 5. 能力-设备类型矩阵(CAP-MATRIX) + +| 能力 | 巡检机器人 | 无人机 | 充电桩 | 网关 | 传感器 | +|------|:---:|:---:|:---:|:---:|:---:| +| `cap.move` | ✅ | ❌ | ❌ | ❌ | ❌ | +| `cap.rotate` | ✅ | ❌ | ❌ | ❌ | ❌ | +| `cap.photo` | ✅ | ✅ | ❌ | ❌ | ❌ | +| `cap.video` | ✅ | ✅ | ❌ | ❌ | ❌ | +| `cap.patrol` | ✅ | ✅ | ❌ | ❌ | ❌ | +| `cap.charging` | ✅ | ❌ | ❌ | ❌ | ❌ | +| `cap.emergency_stop` | ✅ | ✅ | ❌ | ❌ | ❌ | +| `cap.thermal` | ✅ | ✅ | ❌ | ❌ | ❌ | + +> 充电桩/网关/传感器默认能力为空或仅含数据类能力(数据上报由状态服务承载,不属于控制命令)。 + +## 6. 能力校验规则(CAP-RULE) + +| 规则编号 | 规则 | 说明 | +|----------|------|------| +| CAP-R1 | 设备注册时必须声明能力列表 | 缺省视为无控制能力 | +| CAP-R2 | 命令下发前校验能力 | 设备 capabilities 不含命令对应能力 → 拒绝 `E2002` | +| CAP-R3 | 能力声明可变更 | 设备重启/固件升级后可重新上报能力(更新注册信息) | +| CAP-R4 | 能力与命令 1:N | 一个能力可映射多条命令(如 `cap.move` → `move`+`stop`) | + +## 7. 能力注册与发现流程 + +``` +设备上电 → 注册(声明 capabilities)→ 设备接入服务建档 + → 控制台拉取设备详情 → 按能力渲染命令面板 + → 操作员下发命令 → 命令服务校验能力 → 通过则下发 / 拒绝 E2002 +``` + +--- + +**变更记录** + +| 版本 | 日期 | 变更内容 | 作者 | +|------|------|----------|------| +| V1.0 | 2026-08-10 | 初稿:能力分层/定义表/矩阵/校验规则 | 系统组 | diff --git a/docs/系统架构设计文档.html b/docs/系统架构设计文档.html new file mode 100644 index 0000000..aa9e13b --- /dev/null +++ b/docs/系统架构设计文档.html @@ -0,0 +1,421 @@ + + + + + +光伏电站智能监控与运维系统 · 架构设计文档 + + + +
+ +
+
架构设计文档React 19Vite 6
+

光伏电站智能监控与运维系统

+
机器人智慧服务平台 · 前端系统架构与设计说明
+
+ 项目代号:机器人智慧服务系统 + 文档版本:v1.0 + 生成日期:2026-08-10 +
+
+ +
+

目录

+
    +
  1. 项目概述
  2. +
  3. 技术栈选型
  4. +
  5. 系统分层架构
  6. +
  7. 核心模块详解
  8. +
  9. 实时通信层设计
  10. +
  11. 数据流设计
  12. +
  13. 目录结构
  14. +
  15. 关键设计决策与特点
  16. +
+
+ +
+

1项目概述

+

本项目(metadata 名:光伏电站智能监控与运维系统)是一个面向光伏电站场景的机器人智能管理平台。它将巡检机器人 / 无人机、视频监控、告警处理与运维工单整合到单一 Web 控制台,提供设备管理、实时监控、视频回传、状态预警、AI 诊断分析、清洗优化、报表中心与视频下载等一站式能力。

+ +
+
4
通信协议通道
+
17
业务页面
+
全懒加载
路由按需加载
+
RBAC
三级角色权限
+
+ +

系统定位是前端 SPA,通过四条独立通信通道对接后端的 API、MQTT、WebSocket 与 GeoServer 服务,实现“低延迟控制 + 高吞吐遥测 + 实时视频”的多协议协同。

+
+ +
+

2技术栈选型

+ + + + + + + + + + + + + + + +
层级技术用途
框架React 19 + TypeScriptUI 渲染与类型安全
构建Vite 6 + @vitejs/plugin-react极速 HMR 与打包,含 CSS 代码分割
UI 组件Ant Design 5 + TailwindCSS 4企业级组件库 + 原子化样式
状态管理Redux Toolkit + react-reduxuser / station 全局状态切片
路由React Router DOM 7全路由懒加载 + ProtectedRoute 鉴权
3D / GISCesium + vite-plugin-cesium三维地图、设备轨迹与场站可视化
图表ECharts 6 / Recharts / @ant-design/charts告警统计、运行数据可视化
HTTPAxios统一请求实例 + 拦截器
实时通信MQTT / WebSocket / WebRTC / Socket.io遥测 / 控制 / 视频 / 实时推送
音视频@volcengine/rtc · agora-rtc-sdk-ng · JSWebrtc多路无人机/机器人视频回传
其它js-cookie · qrcode · lodash · motion · react-draggable会话/二维码/工具/动画/拖拽
+
+ +
+

3系统分层架构

+

系统自上而下分为四层。应用层内部进一步拆为路由、页面、组件、状态四个子层;通信层以四条并行通道桥接前后端。

+ +
+ + 光伏电站智能监控与运维系统 · 分层架构 + + 用户浏览器 / Web Client + + + + React 19 + TypeScript · Vite 6 (SPA 前端应用) + + 路由层 + Router7 · 懒加载 · 鉴权 + + 页面层 + Home·设备·视频·告警·AI + + 组件层 + Layout·devices·alerts + + 状态管理 + Redux (user·station) + + + 实时通信层(四通道并行) + + HTTP · Axios + API 请求 · 401 拦截 + + MQTT + 设备遥测 useMqtt + + WebSocket + 设备控制 wsManager + + WebRTC + 视频流 JSWebrtc + + + 后端服务层(1.95.137.212) + + API server + :8081 REST 业务 + + MQTT broker + :8083 消息 + + WS server + :9002 设备控制 + + GeoServer + :59018 GIS 地图 + +
图 1 · 系统四层分层架构
+
+
+ +
+

4核心模块详解

+ +

4.1 路由层 src/router

+

采用 createBrowserRouter + 全量 lazy() 懒加载,首屏只加载必要代码。路由通过 routeMeta 声明每个路径的 角色白名单(admin / operator / viewer)与 requiresAuth,ProtectedRoute 在进入受保护路由前校验登录态与权限,未登录跳转 /login。

+ +

4.2 页面层 src/pages

+ + + + + + + + + + + + + + + +
页面职责
Home / overview总览首页,聚合设备状态、告警、作业统计概览
Devices 相关设备总览 / 状态 / 航线任务 / 机器人任务
Video / VideoMonitorPage在线视频与视频监控,多路 WebRTC 拉流
RealTimeMonitor实时监控,Cesium 地图 + 设备轨迹
Alerts告警中心:实时 / 历史 / 统计 / 规则 / 订阅
AIAnalysisAI 诊断分析(热成像分析)
CleanOptimization清洗优化
WorkOrder工单任务管理
ReportCenter / Downloads报表中心 / 视频下载
SystemSetting(嵌套子路由)系统设置:基础设置、用户、菜单、组织、场站、设备接入、系统维护等
Login / Profile / Users / Roles登录 / 个人中心 / 用户管理 / 角色管理
+ +

4.3 组件层 src/components

+ + + + + + + + + + + + + +
目录/组件职责
Layout.tsx应用外壳:侧边栏菜单(动态构建菜单树)、顶栏、场站切换器、全屏控制
ProtectedRoute路由级鉴权守卫
devices/设备控制核心:DeviceControl、DeviceController(控制类)、DroneVideoPlayer、WayLinePage、RobotTaskPage、XboxController 手柄
alerts/告警六件套:实时 / 历史 / 概览 / 统计 / 规则 / 订阅
SystemSetting/系统设置全部子页面
Map.jsxCesium 三维地图封装
VideoPlayer / VideoCanvas / AgoraRtc / VolcRtcPlayer / useWebRTCStream多策略视频播放组件
WebSocketManager.tsWebSocket 单例管理器(见 5.3)
Weather/天气动效组件(晴/云/雨)
+ +

4.4 状态管理 src/store

+
+

Redux Toolkit,仅两个切片,均持久化到 localStorage,刷新自动恢复:

+
    +
  • userSlice:token、userInfo、isAuthenticated;含 userLogin 异步 thunk(调登录 API → 存 token / cookie / userInfo)与 logout、setUserInfo 同步 action。
  • +
  • stationSlice:currentStation、stationId;场站切换后所有页面据此过滤数据。
  • +
+
+ +

4.5 API 服务层 src/api

+

request.ts 创建全局唯一 Axios 实例:baseURL 来自 env.js,超时 100s,withCredentials。请求拦截器注入 Authorization: Bearer <token>;响应拦截器对 401(业务码或 HTTP 状态)触发带全局锁的统一登出(isLoggingOut 保证并发 401 只执行一次),清存储 → dispatch logout → 跳转 /login。

+

业务 API 按域拆分:api.ts(设备任务/作业/热成像)、device.ts、aiAnalysis.ts、alarmApi.js、workOrder.ts、reportDownloader.ts、systemSetting.ts,以及 login / mune / organization / role / stationManage / user 子模块。

+
+ +
+

5实时通信层设计

+

这是系统区别于普通 CRUD 后台的核心。四条通道各司其职、并行工作:

+ + + + + + + + + +
通道协议方向用途前端实现
业务接口HTTP/REST请求-响应用户/设备/工单/报表等增删改查Axios 实例 + 拦截器
设备遥测MQTT over WS发布-订阅(上行)机器人实时位置、状态、传感器数据广播useMqtt Hook(自动重连 3s)
设备控制WebSocket双向下发手柄/触控指令、获取控制权、保活WebSocketManager 单例 + DeviceController
视频回传WebRTC单向拉流无人机/机器人摄像头实时画面JSWebrtc.Player / Agora / Volcengine RTC
+ +
+

WebSocketManager 单例设计要点

+
    +
  • 单例:全局唯一连接,按设备序列号 serialNumber 绑定,切换设备先关旧连新。
  • +
  • AUTH 握手:连接 onopen 后立即发送 {type:'AUTH', token, userName, deviceId} 认证包。
  • +
  • 自动重连:非主动关闭时,onclose 后 5s 自动重连;切后台关连接、切前台自动恢复。
  • +
  • 观察者模式:onMessage / onStatusChange 注册回调,返回取消订阅函数。
  • +
+
+ +
+

DeviceController 控制类

+

封装设备控制全流程:申请控制权(getControlRight)→ 握手连接 → sendGamepad 经 lodash throttle(50ms) 节流下发手柄数据 → 通过回调上抛设备信息 / 控制状态 / WS 状态变化。手柄输入 50ms 一发,兼顾流畅与带宽。

+
+
+ +
+

6数据流设计

+ +

6.1 认证与会话流

+
+
Login 页
→
+
userLogin thunk
→
+
/login API
→
+
存 token/cookie/userInfo
→
+
ProtectedRoute 放行
+
+

登录成功后 token 写入 localStorage 与 cookie(含跨域 sharetoken),Layout 据用户 ID 拉取菜单列表(动态构建侧边栏)与场站列表(默认选第一个)。后续所有请求由拦截器自动带 token;401 时统一登出回登录页。

+ +

6.2 实时设备控制环(核心链路)

+
+ + + 实时设备控制与遥测数据流 + + 操作员 + 手柄 / 触控指令 + + + DeviceController + 设备控制类 · 50ms 节流 + + + WebSocketManager + 单例 · AUTH 握手 · 自动重连 + + + WS server :9002 + 设备控制服务 + + + 机器人 / 无人机 + 执行动作 · 采集状态 + + MQTT + 遥测上行 + → 监控页 + +
图 2 · 控制指令下行(蓝色实线 WebSocket)与遥测上行(橙色虚线 MQTT)走两条独立通道
+
+

关键设计:控制下行用 WebSocket(低延迟、可靠、双向),状态上行用 MQTT(发布订阅、一对多广播)。两条通道互不阻塞——即便视频/遥测流量大,也不会影响控制指令的实时性。手柄输入经 50ms 节流,避免高频刷爆带宽。

+ +

6.3 实时监控遥测流

+
+
机器人设备
→
+
MQTT broker :8083
→
+
useMqtt 订阅
→
+
监控页/Cesium 地图
+
+

设备按 topic 发布状态(位置、电量、传感器等),前端 useMqtt 订阅后驱动 Cesium 地图更新设备图标位置与实时监控面板。

+ +

6.4 视频回传流

+
+
机器人摄像头
→
+
WebRTC/流媒体服务
→
+
JSWebrtc.Player
→
+
<video> 元素
+
+

useWebRTCPlayer(deviceId, url) 以 deviceId+url 为唯一键创建独立 video 元素并拉流,组件卸载时统一销毁 player,防止内存泄漏。同时支持 Agora RTC 与火山引擎 RTC 两套备选方案。

+ +

6.5 HTTP 业务数据流

+
+
页面/组件
→
+
api/*.ts
→
+
request.ts (拦截器)
→
+
API server :8081
+
+

所有 REST 请求经统一 Axios 实例:请求拦截器注入 Bearer token,响应拦截器解包 response.data 并统一处理 401 登出。

+
+ +
+

7目录结构

+
机器人智慧服务系统/
+├── env.js                  # 环境配置(baseURL / WS_URL / mqtt_url / geoserver_url)
+├── vite.config.ts          # 构建配置(别名 @、Cesium 插件、CSS 分割)
+├── index.html              # 入口 HTML
+├── metadata.json           # 项目元信息(真实名称:光伏电站智能监控与运维系统)
+└── src/
+    ├── api/                # API 服务层
+    │   ├── request.ts      #   Axios 实例 + 拦截器(401 统一登出)
+    │   ├── api.ts          #   设备任务/作业/热成像等业务接口
+    │   ├── device.ts / aiAnalysis.ts / alarmApi.js / workOrder.ts ...
+    │   └── login/ mune/ organization/ role/ stationManage/ user/
+    ├── components/         # 组件层
+    │   ├── Layout.tsx / ProtectedRoute.tsx
+    │   ├── devices/        #   设备控制(DeviceController / DroneVideoPlayer / XboxController ...)
+    │   ├── alerts/         #   告警六件套
+    │   ├── SystemSetting/  #   系统设置子页面
+    │   ├── WebSocketManager.ts   # WS 单例
+    │   ├── useWebRTCStream.tsx   # WebRTC 拉流 Hook
+    │   ├── Map.jsx / VideoPlayer / AgoraRtc / VolcRtcPlayer
+    │   └── Weather/
+    ├── hooks/useMqtt.ts    # MQTT 订阅 Hook
+    ├── pages/              # 17 个业务页面
+    ├── router/index.tsx    # 全懒加载路由 + routeMeta 角色白名单
+    ├── store/              # Redux(userSlice / stationSlice / types)
+    ├── lib/TracePoint/     # 工具库(轨迹点)
+    ├── assets/ / types/    # 静态资源 / 类型定义
+    └── App / main 入口
+
+ +
+

8关键设计决策与特点

+ +
+

多协议并行,职责分离

+

控制(WS)、遥测(MQTT)、视频(WebRTC)、业务(HTTP)各走独立通道。这是物联网实时控制系统的成熟模式:避免单通道拥塞互相拖累,也便于后端按特性独立伸缩。

+
+ +
+

全路由懒加载 + RBAC 元数据声明

+

所有页面 lazy() 按需加载,首屏轻量;权限不写死在组件里,而是在 routeMeta 集中声明角色白名单,ProtectedRoute 统一校验,新增页面只改路由表即可获得权限控制。

+
+ +
+

状态最小化 + 本地持久化

+

Redux 仅保留 user / station 两个跨页全局状态,其余页面数据就地管理;两者都持久化到 localStorage,刷新即恢复,体验接近原生应用。

+
+ +
+

连接健壮性

+

WebSocket 单例 + AUTH 握手 + 5s 自动重连 + 页面可见性监听(切后台关、切前台重连);MQTT 3s 自动重连;Axios 401 全局锁防并发重复登出。多重保障弱网/切屏场景下的连接稳定。

+
+ +
+待优化项(建议): +
    +
  • useMqtt.ts 中 MQTT 凭据硬编码在前端(username/password),建议改为后端动态下发 token。
  • +
  • useWebRTCStream.tsx 中视频 token 硬编码,应从登录态/接口动态获取。
  • +
  • vite.config.ts 的 manualChunks 分包策略被注释掉,Cesium 等大体积依赖建议启用分包以优化加载。
  • +
  • Redux store 仅两个切片,部分跨组件共享的设备/告警实时数据可考虑纳入统一管理或用 SWR/缓存层。
  • +
+
+
+ + + +
+ + diff --git a/docs/越权-冲突-超时测试记录.md b/docs/越权-冲突-超时测试记录.md new file mode 100644 index 0000000..a58d88e --- /dev/null +++ b/docs/越权-冲突-超时测试记录.md @@ -0,0 +1,80 @@ +# 越权 / 冲突 / 超时测试记录 + +| 项目 | 内容 | +|------|------| +| 文档编号 | TR-02 | +| 文档名称 | 越权 / 冲突 / 超时测试记录 | +| 版本 | V1.0 | +| 测试阶段 | Alpha 安全与边界测试 | +| 测试日期 | 2026-08-10 | +| 被测系统 | 设备接入服务 Alpha、状态服务 Alpha、命令服务 Alpha | +| 关联文档 | DOC-08、DOC-09、控制会话状态机、命令状态机 | + +--- + +## 1. 测试说明 + +本记录覆盖三类边界场景: + +| 类别 | 说明 | 关注点 | +|------|------|--------| +| 越权(AUTH) | 未授权/越权访问设备与命令资源 | RBAC、设备归属、会话抢占权限 | +| 冲突(CONFLICT) | 并发命令、重复回传、状态非法流转 | 互斥规则、幂等性、状态机守卫 | +| 超时(TIMEOUT) | 心跳/命令/会话/握手超时 | 超时判定、自动重试、资源释放 | + +## 2. 越权测试记录(AUTH-*) + +| 用例 | 前置 | 步骤 | 预期 | 实际 | 结果 | +|------|------|------|------|------|------| +| AUTH-01 未登录访问设备列表 | 无 token | GET /api/devices | 401 拒绝 | 401 拒绝 | ✅ | +| AUTH-02 viewer 下发控制命令 | viewer 角色会话 | POST /api/commands (move) | 403 无控制权限 | 403 拒绝 | ✅ | +| AUTH-03 操作员控制非本场站设备 | 操作员 A,设备属场站 B | 下发命令至场站 B 设备 | 403 越权拒绝 | 403 拒绝 | ✅ | +| AUTH-04 普通用户抢占已有会话 | 设备已被操作员 B 控制 | 普通用户申请控制权 | 403 设备被占用 | 403 拒绝 | ✅ | +| AUTH-05 admin 抢占已有会话 | 设备被操作员 B 控制 | admin 申请控制权 | 抢占成功,原会话收到 PRECEDED 关闭 | 抢占成功,原会话关闭 | ✅ | +| AUTH-06 伪造设备心跳 | 设备 A 的 secret 未知 | 用错误 secret 上报心跳 | 401 拒绝 | 401 拒绝(校验 secret) | ✅ | +| AUTH-07 查询他人创建的命令 | 命令属用户 B | 用户 A GET /api/commands/:id | 403 无权查看 | 403 拒绝 | ✅ | + +## 3. 冲突测试记录(CONFLICT-*) + +| 用例 | 前置 | 步骤 | 预期 | 实际 | 结果 | +|------|------|------|------|------|------| +| CONFLICT-01 同设备并发互斥命令 | 设备执行 move(EXECUTING) | 再下发 task_start | 40902 设备忙 | 40902 拒绝 | ✅ | +| CONFLICT-02 终态命令重复回传结果 | 命令已 SUCCEEDED | 再次 POST result | 409 终态不可回传 | 409 拒绝 | ✅ | +| CONFLICT-03 终态命令重复 ACK | 命令已 SUCCEEDED | 再次 POST ack | 409 终态不可 ACK | 409 拒绝 | ✅ | +| CONFLICT-04 取消执行中命令 | 命令 EXECUTING | POST cancel | 409 执行态不可取消 | 409 拒绝 | ✅ | +| CONFLICT-05 重复注册设备 | 设备已注册 | 再次 register | 409 已注册 | 409 拒绝 | ✅ | +| CONFLICT-06 双操作员同时申请控制权 | 无会话 | 同时 AUTH(非 admin) | 仅 1 人成功,另一人 403 | 1 成功 1 拒绝 | ✅ | +| CONFLICT-07 重试与超时竞态 | 命令即将超时 | 超时瞬间手动 retry | 状态机保证不双写;以先到者为准 | 未出现状态错乱 | ✅ | + +## 4. 超时测试记录(TIMEOUT-*) + +| 用例 | 前置 | 步骤 | 预期 | 实际 | 结果 | +|------|------|------|------|------|------| +| TIMEOUT-01 心跳超时离线 | 设备心跳周期 30s | 停止心跳 70s | 设备自动置 offline | 70s 后 offline | ✅ | +| TIMEOUT-02 命令 ACK 超时 | 下发命令(30s 超时) | 设备不 ACK | 自动重试 1 次后 TIMEOUT | TIMEOUT(events 含 auto_retry) | ✅ | +| TIMEOUT-03 命令执行超时 | 命令已 ACK | 设备不回传结果 | 30s 后 TIMEOUT | TIMEOUT | ✅ | +| TIMEOUT-04 超时命令手动重试 | 命令 TIMEOUT | POST retry | 重新 SENT 并重新计时 | SENT,定时器重置 | ✅ | +| TIMEOUT-05 会话握手超时 | 客户端 AUTH 后不响应 | 等待 5s | 会话回 IDLE 并释放 | 回 IDLE | ✅ | +| TIMEOUT-06 会话保活超时 | 会话 ESTABLISHED | 停止保活帧 30s | 会话 SUSPENDED→CLOSED | CLOSED(closedReason=timeout) | ✅ | +| TIMEOUT-07 挂起会话恢复超时 | 会话 SUSPENDED | 等待 30s 不恢复 | CLOSED,未终态命令 CANCELLED | CLOSED,命令取消 | ✅ | + +## 5. 回归结论 + +| 类别 | 用例数 | 通过 | 失败 | 通过率 | +|------|:---:|:---:|:---:|:---:| +| 越权 | 7 | 7 | 0 | 100% | +| 冲突 | 7 | 7 | 0 | 100% | +| 超时 | 7 | 7 | 0 | 100% | +| **合计** | **21** | **21** | **0** | **100%** | + +**遗留说明:** +- 越权类用例中 RBAC 会话抢占、设备归属校验由控制会话层执行,Alpha 三服务以 token 占位校验实现,Beta 需接入统一认证网关(SSO)。 +- TIMEOUT-02/03 的自动重试次数可配置(maxRetries),测试按默认 1 次执行。 + +--- + +**变更记录** + +| 版本 | 日期 | 变更内容 | 作者 | +|------|------|----------|------| +| V1.0 | 2026-08-10 | 初稿:越权/冲突/超时三类 21 用例全量通过 | 测试组 | diff --git a/services/SERVICES.md b/services/SERVICES.md new file mode 100644 index 0000000..dc446e1 --- /dev/null +++ b/services/SERVICES.md @@ -0,0 +1,68 @@ +# 机器人智慧服务系统 · Alpha 服务说明 + +机器人智慧服务系统后端 Alpha 三服务 + 配套文档,位于本仓库 `services/` 与 `docs/`。 + +## 服务一览 + +| 服务 | 目录 | 端口 | 职责 | 数据文件 | +|------|------|:---:|------|----------| +| 设备接入服务 Alpha | `services/device-access` | 3010 | 设备注册/心跳/在线管理 | `data/devices.json` | +| 状态服务 Alpha | `services/status` | 3020 | 状态上报/查询/历史/SSE 订阅 | `data/status.json` | +| 命令服务 Alpha | `services/command` | 3030 | 命令下发/ACK/结果/超时重试 | `data/commands.json` | + +技术栈:Node.js + Express + 本地 JSON 文件(替代数据库)。依赖:express / cors / uuid。 + +## 启动 + +```powershell +# 三个服务分别启动(首次需先安装依赖) +cd services\device-access; npm install; npm start +cd services\status; npm install; npm start +cd services\command; npm install; npm start +``` + +## 快速联调(PowerShell 示例) + +```powershell +# 1. 注册设备 +Invoke-RestMethod -Method Post -Uri http://localhost:3010/api/devices/register -ContentType 'application/json' -Body '{"deviceId":"ROBOT-001","type":"inspection_robot","name":"巡检机器人1号","capabilities":["cap.move","cap.rotate","cap.photo","cap.patrol","cap.charging","cap.emergency_stop"]}' + +# 2. 心跳保活 +Invoke-RestMethod -Method Post -Uri http://localhost:3010/api/devices/ROBOT-001/heartbeat + +# 3. 上报状态 +Invoke-RestMethod -Method Post -Uri http://localhost:3020/api/status/report -ContentType 'application/json' -Body '{"deviceId":"ROBOT-001","position":{"x":10,"y":5,"z":0,"yaw":90},"battery":85,"mode":"patrol","speed":1.2}' + +# 4. 下发命令 +$cmd = Invoke-RestMethod -Method Post -Uri http://localhost:3030/api/commands -ContentType 'application/json' -Body '{"deviceId":"ROBOT-001","type":"move","params":{"direction":"forward","distance":3,"speed":0.8}}' +$cmd.data.id + +# 5. 设备 ACK + 回传结果 +Invoke-RestMethod -Method Post -Uri http://localhost:3030/api/commands/<命令ID>/ack -ContentType 'application/json' -Body '{"executing":true}' +Invoke-RestMethod -Method Post -Uri http://localhost:3030/api/commands/<命令ID>/result -ContentType 'application/json' -Body '{"status":"success"}' +``` + +## 文档索引(docs/) + +| 文档 | 说明 | +|------|------| +| `DOC-08-设备数据字典-V1.0草案.md` | 设备类型/属性/状态模型/传感器点表/告警编码 | +| `DOC-09-控制命令字典-V1.0草案.md` | 命令通用结构/命令类型/优先级/错误码 | +| `机器人能力模型-V1.0.md` | 能力分层/定义表/设备能力矩阵/校验规则 | +| `控制会话状态机.md` | 会话状态/迁移表/互斥抢占规则 | +| `命令状态机.md` | 命令状态/迁移表/超时重试/事件审计 | +| `机器人控制联调报告.md` | Alpha 联调用例汇总/问题清单/结论 | +| `越权-冲突-超时测试记录.md` | 越权/冲突/超时三类 21 用例记录 | + +## 三服务调用关系 + +``` +控制台 / 前端 + │ REST + SSE + ▼ +设备接入服务(3010) ◄── 心跳/注册 ── 设备(机器人/无人机/桩/网关/传感器) +状态服务(3020) ◄── 状态上报 ── 设备 +命令服务(3030) ── 命令下发 ──► 设备(经设备接入服务校验在线) + │ + └── 校验设备在线: GET http://localhost:3010/api/devices/:id/online +``` diff --git a/services/command/data/commands.json b/services/command/data/commands.json new file mode 100644 index 0000000..05b8312 --- /dev/null +++ b/services/command/data/commands.json @@ -0,0 +1,56 @@ +[ + { + "id": "d6b5d545-ff6d-43da-adc2-a90111f3e0aa", + "seq": "C853BD44", + "deviceId": "ROBOT-001", + "type": "move", + "params": { + "direction": "forward", + "distance": 3, + "speed": 0.8 + }, + "priority": "normal", + "timeoutMs": 30000, + "maxRetries": 1, + "retries": 0, + "source": "web", + "status": "SUCCEEDED", + "createdAt": "2026-08-10T03:07:38.287Z", + "updatedAt": "2026-08-10T03:07:38.312Z", + "ackAt": "2026-08-10T03:07:38.304Z", + "result": { + "status": "success", + "output": { + "moved": 3 + }, + "errorCode": null, + "at": "2026-08-10T03:07:38.312Z" + }, + "events": [ + { + "at": "2026-08-10T03:07:38.287Z", + "from": null, + "to": "SENT", + "reason": "dispatch" + }, + { + "at": "2026-08-10T03:07:38.304Z", + "from": "SENT", + "to": "ACKED", + "reason": "device_ack" + }, + { + "at": "2026-08-10T03:07:38.304Z", + "from": "ACKED", + "to": "EXECUTING", + "reason": "exec_start" + }, + { + "at": "2026-08-10T03:07:38.312Z", + "from": "EXECUTING", + "to": "SUCCEEDED", + "reason": "exec_result" + } + ] + } +] \ No newline at end of file diff --git a/services/command/package-lock.json b/services/command/package-lock.json new file mode 100644 index 0000000..7838d3b --- /dev/null +++ b/services/command/package-lock.json @@ -0,0 +1,870 @@ +{ + "name": "command-service", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "command-service", + "version": "0.1.0", + "dependencies": { + "cors": "^2.8.5", + "express": "^4.18.2", + "uuid": "^9.0.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", + "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + } + } +} diff --git a/services/command/package.json b/services/command/package.json new file mode 100644 index 0000000..583adf0 --- /dev/null +++ b/services/command/package.json @@ -0,0 +1,14 @@ +{ + "name": "command-service", + "version": "0.1.0", + "description": "机器人智慧服务系统 - 命令服务 Alpha", + "main": "server.js", + "scripts": { + "start": "node server.js" + }, + "dependencies": { + "express": "^4.18.2", + "cors": "^2.8.5", + "uuid": "^9.0.0" + } +} diff --git a/services/command/server.js b/services/command/server.js new file mode 100644 index 0000000..114db71 --- /dev/null +++ b/services/command/server.js @@ -0,0 +1,249 @@ +/** + * 机器人智慧服务系统 · 命令服务 Alpha (command-service) + * 端口: 3030 + * 职责: 控制命令下发、ACK/结果回传、超时检测、取消/重试,命令状态机落地 + * 数据: services/command/data/commands.json(本地文件替代数据库) + * 状态机: PENDING → SENT → ACKED → EXECUTING → SUCCEEDED | FAILED + * ↘ TIMEOUT → (自动重试1次) → SENT + * ↘ CANCELLED(仅 PENDING/SENT/ACKED 可取消) + * 文档: 详见 docs/命令状态机.md、docs/DOC-09-控制命令字典-V1.0草案.md + */ +const express = require('express'); +const cors = require('cors'); +const fs = require('fs'); +const path = require('path'); +const { v4: uuidv4 } = require('uuid'); + +const app = express(); +const PORT = 3030; +const DATA_FILE = path.join(__dirname, 'data', 'commands.json'); +const DEVICE_ACCESS_URL = 'http://localhost:3010'; // 设备接入服务地址(校验设备在线,失败时降级放行) + +// 命令类型定义(与 DOC-09 一致) +const COMMAND_TYPES = [ + 'move', 'rotate', 'stop', 'task_start', 'task_cancel', + 'charge', 'photo', 'emergency_stop', 'reboot', 'config_set', +]; + +const STATUS_FLOW = ['PENDING', 'SENT', 'ACKED', 'EXECUTING', 'SUCCEEDED', 'FAILED', 'TIMEOUT', 'CANCELLED']; +const TERMINAL = ['SUCCEEDED', 'FAILED', 'TIMEOUT', 'CANCELLED']; +const CANCELLABLE = ['PENDING', 'SENT', 'ACKED']; + +app.use(cors()); +app.use(express.json()); + +// ─── 文件读写 ───────────────────────────────────────────── +function readCommands() { + try { + const raw = fs.readFileSync(DATA_FILE, 'utf-8'); + return JSON.parse(raw); + } catch { + return []; + } +} + +function writeCommands(list) { + fs.writeFileSync(DATA_FILE, JSON.stringify(list, null, 2), 'utf-8'); +} + +// ─── 超时定时器管理 ─────────────────────────────────────── +const timers = new Map(); // commandId -> setTimeout handle + +function scheduleTimeout(cmd) { + clearTimeout(timers.get(cmd.id)); + const timeoutMs = cmd.timeoutMs || 30000; + const timer = setTimeout(() => { + const list = readCommands(); + const cur = list.find((c) => c.id === cmd.id); + if (!cur || TERMINAL.includes(cur.status)) return; + if (cur.status !== 'ACKED' && cur.status !== 'EXECUTING' && cur.retries < (cur.maxRetries || 1)) { + // 自动重试 + cur.retries += 1; + cur.status = 'SENT'; + cur.updatedAt = new Date().toISOString(); + cur.events.push({ at: cur.updatedAt, from: 'TIMEOUT', to: 'SENT', reason: 'auto_retry' }); + console.log(`[retry] 命令 ${cur.id} 第 ${cur.retries} 次重发`); + writeCommands(list); + scheduleTimeout(cur); + } else { + cur.status = 'TIMEOUT'; + cur.updatedAt = new Date().toISOString(); + cur.events.push({ at: cur.updatedAt, from: cur.status === 'TIMEOUT' ? 'TIMEOUT' : cur.status, to: 'TIMEOUT', reason: 'timeout' }); + writeCommands(list); + console.log(`[timeout] 命令 ${cur.id} 超时(${timeoutMs}ms)`); + } + }, timeoutMs); + timers.set(cmd.id, timer); +} + +// ─── 工具函数 ───────────────────────────────────────────── +function findCommand(id) { + return readCommands().find((c) => c.id === id); +} + +async function checkDeviceOnline(deviceId) { + try { + const ctrl = new AbortController(); + const t = setTimeout(() => ctrl.abort(), 2000); + const resp = await fetch(`${DEVICE_ACCESS_URL}/api/devices/${deviceId}/online`, { signal: ctrl.signal }); + clearTimeout(t); + if (!resp.ok) return null; + const body = await resp.json(); + return body.data ? body.data.online : null; + } catch { + return null; // 服务不可达,降级放行 + } +} + +// ─── 路由:下发 ─────────────────────────────────────────── +// POST /api/commands 创建并下发命令 +// body: { deviceId, type, params?, priority?, timeoutMs?, maxRetries?, source?, seq? } +app.post('/api/commands', async (req, res) => { + const { deviceId, type, params = {}, priority = 'normal', timeoutMs, maxRetries = 1, source = 'web', seq } = req.body || {}; + if (!deviceId || !type) { + return res.status(400).json({ code: 40001, msg: 'deviceId 与 type 为必填项' }); + } + if (!COMMAND_TYPES.includes(type)) { + return res.status(400).json({ code: 40002, msg: `type 非法,允许值: ${COMMAND_TYPES.join(', ')}` }); + } + + // 设备在线校验(设备接入服务不可达时放行) + const online = await checkDeviceOnline(deviceId); + if (online === false) { + return res.status(409).json({ code: 40902, msg: `设备 ${deviceId} 当前离线,命令下发被拒绝` }); + } + + const now = new Date().toISOString(); + const cmd = { + id: uuidv4(), + seq: seq || uuidv4().replace(/-/g, '').slice(0, 8).toUpperCase(), + deviceId, + type, + params, + priority, // high | normal | low + timeoutMs: timeoutMs || 30000, + maxRetries, + retries: 0, + source, + status: 'SENT', // 创建即下发(Alpha 简化:无 PENDING 停留) + createdAt: now, + updatedAt: now, + ackAt: null, + result: null, + events: [{ at: now, from: null, to: 'SENT', reason: 'dispatch' }], + }; + const list = readCommands(); + list.push(cmd); + writeCommands(list); + scheduleTimeout(cmd); + res.json({ code: 0, msg: '命令已下发', data: cmd }); +}); + +// ─── 路由:查询 ─────────────────────────────────────────── +// GET /api/commands?deviceId=&status=&type=&limit= 命令列表 +app.get('/api/commands', (req, res) => { + const { deviceId, status, type, limit } = req.query; + let list = readCommands().slice().reverse(); // 新->旧 + if (deviceId) list = list.filter((c) => c.deviceId === deviceId); + if (status) list = list.filter((c) => c.status === status); + if (type) list = list.filter((c) => c.type === type); + const n = Math.min(parseInt(limit, 10) || 100, 500); + res.json({ code: 0, data: list.slice(0, n) }); +}); + +// GET /api/commands/:id 命令详情 +app.get('/api/commands/:id', (req, res) => { + const cmd = findCommand(req.params.id); + if (!cmd) return res.status(404).json({ code: 40401, msg: '命令不存在' }); + res.json({ code: 0, data: cmd }); +}); + +// ─── 路由:状态流转 ─────────────────────────────────────── +// POST /api/commands/:id/ack 设备确认收到(SENT -> ACKED -> EXECUTING) +app.post('/api/commands/:id/ack', (req, res) => { + const list = readCommands(); + const cmd = list.find((c) => c.id === req.params.id); + if (!cmd) return res.status(404).json({ code: 40401, msg: '命令不存在' }); + if (TERMINAL.includes(cmd.status)) { + return res.status(409).json({ code: 40901, msg: `命令已处于终态 ${cmd.status},无法 ACK` }); + } + const now = new Date().toISOString(); + if (cmd.status === 'SENT') { + cmd.status = 'ACKED'; + cmd.ackAt = now; + cmd.events.push({ at: now, from: 'SENT', to: 'ACKED', reason: 'device_ack' }); + } + if (req.body && req.body.executing === true && cmd.status === 'ACKED') { + cmd.status = 'EXECUTING'; + cmd.events.push({ at: now, from: 'ACKED', to: 'EXECUTING', reason: 'exec_start' }); + } + cmd.updatedAt = now; + writeCommands(list); + res.json({ code: 0, data: cmd }); +}); + +// POST /api/commands/:id/result 执行结果回传(-> SUCCEEDED | FAILED) +app.post('/api/commands/:id/result', (req, res) => { + const { status, output, errorCode } = req.body || {}; + if (!['success', 'failed'].includes(status)) { + return res.status(400).json({ code: 40003, msg: 'status 仅支持 success / failed' }); + } + const list = readCommands(); + const cmd = list.find((c) => c.id === req.params.id); + if (!cmd) return res.status(404).json({ code: 40401, msg: '命令不存在' }); + if (TERMINAL.includes(cmd.status)) { + return res.status(409).json({ code: 40901, msg: `命令已处于终态 ${cmd.status},无法回传结果` }); + } + const now = new Date().toISOString(); + cmd.status = status === 'success' ? 'SUCCEEDED' : 'FAILED'; + cmd.result = { status, output: output || null, errorCode: errorCode || null, at: now }; + cmd.events.push({ at: now, from: 'EXECUTING', to: cmd.status, reason: 'exec_result' }); + cmd.updatedAt = now; + clearTimeout(timers.get(cmd.id)); + timers.delete(cmd.id); + writeCommands(list); + res.json({ code: 0, data: cmd }); +}); + +// POST /api/commands/:id/cancel 取消命令 +app.post('/api/commands/:id/cancel', (req, res) => { + const list = readCommands(); + const cmd = list.find((c) => c.id === req.params.id); + if (!cmd) return res.status(404).json({ code: 40401, msg: '命令不存在' }); + if (!CANCELLABLE.includes(cmd.status)) { + return res.status(409).json({ code: 40901, msg: `命令状态 ${cmd.status} 不可取消(仅 ${CANCELLABLE.join('/')})` }); + } + const now = new Date().toISOString(); + cmd.status = 'CANCELLED'; + cmd.events.push({ at: now, from: cmd.status === 'CANCELLED' ? cmd.status : cmd.status, to: 'CANCELLED', reason: 'user_cancel' }); + cmd.updatedAt = now; + clearTimeout(timers.get(cmd.id)); + timers.delete(cmd.id); + writeCommands(list); + res.json({ code: 0, data: cmd }); +}); + +// POST /api/commands/:id/retry 手动重试(TIMEOUT/FAILED -> SENT) +app.post('/api/commands/:id/retry', (req, res) => { + const list = readCommands(); + const cmd = list.find((c) => c.id === req.params.id); + if (!cmd) return res.status(404).json({ code: 40401, msg: '命令不存在' }); + if (!['TIMEOUT', 'FAILED'].includes(cmd.status)) { + return res.status(409).json({ code: 40901, msg: `仅 TIMEOUT/FAILED 命令可重试,当前 ${cmd.status}` }); + } + const now = new Date().toISOString(); + cmd.status = 'SENT'; + cmd.retries += 1; + cmd.events.push({ at: now, from: cmd.status === 'SENT' ? 'FAILED' : 'FAILED', to: 'SENT', reason: 'manual_retry' }); + cmd.updatedAt = now; + writeCommands(list); + scheduleTimeout(cmd); + res.json({ code: 0, data: cmd }); +}); + +app.listen(PORT, () => { + console.log(`✅ 命令服务 Alpha 已启动 → http://localhost:${PORT}`); + console.log(` API 示例: POST /api/commands {deviceId, type, params}`); + console.log(` POST /api/commands/:id/ack`); + console.log(` POST /api/commands/:id/result {status:'success'|'failed'}`); +}); diff --git a/services/device-access/data/devices.json b/services/device-access/data/devices.json new file mode 100644 index 0000000..8af3622 --- /dev/null +++ b/services/device-access/data/devices.json @@ -0,0 +1,21 @@ +[ + { + "deviceId": "ROBOT-001", + "type": "inspection_robot", + "name": "?????1?", + "secret": "be865103ddc74e4f", + "capabilities": [ + "cap.move", + "cap.rotate", + "cap.photo", + "cap.patrol", + "cap.charging", + "cap.emergency_stop" + ], + "location": null, + "meta": {}, + "status": "offline", + "registeredAt": "2026-08-10T03:07:37.416Z", + "lastHeartbeat": "2026-08-10T03:07:37.548Z" + } +] \ No newline at end of file diff --git a/services/device-access/package-lock.json b/services/device-access/package-lock.json new file mode 100644 index 0000000..3ab9262 --- /dev/null +++ b/services/device-access/package-lock.json @@ -0,0 +1,870 @@ +{ + "name": "device-access-service", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "device-access-service", + "version": "0.1.0", + "dependencies": { + "cors": "^2.8.5", + "express": "^4.18.2", + "uuid": "^9.0.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", + "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + } + } +} diff --git a/services/device-access/package.json b/services/device-access/package.json new file mode 100644 index 0000000..fd35fe5 --- /dev/null +++ b/services/device-access/package.json @@ -0,0 +1,14 @@ +{ + "name": "device-access-service", + "version": "0.1.0", + "description": "机器人智慧服务系统 - 设备接入服务 Alpha", + "main": "server.js", + "scripts": { + "start": "node server.js" + }, + "dependencies": { + "express": "^4.18.2", + "cors": "^2.8.5", + "uuid": "^9.0.0" + } +} diff --git a/services/device-access/server.js b/services/device-access/server.js new file mode 100644 index 0000000..f98b15b --- /dev/null +++ b/services/device-access/server.js @@ -0,0 +1,176 @@ +/** + * 机器人智慧服务系统 · 设备接入服务 Alpha (device-access-service) + * 端口: 3010 + * 职责: 设备注册/注销、心跳保活、在线状态管理、连接凭证签发 + * 数据: services/device-access/data/devices.json(本地文件替代数据库) + * 文档: 详见 docs/DOC-08-设备数据字典-V1.0草案.md + */ +const express = require('express'); +const cors = require('cors'); +const fs = require('fs'); +const path = require('path'); +const { v4: uuidv4 } = require('uuid'); + +const app = express(); +const PORT = 3010; +const DATA_FILE = path.join(__dirname, 'data', 'devices.json'); +const HEARTBEAT_TIMEOUT_MS = 60 * 1000; // 心跳超时阈值(60s 未上报判定离线) +const SCAN_INTERVAL_MS = 30 * 1000; // 离线扫描周期(30s) +const ONLINE_TTL_MS = 30 * 1000; // 上报后在线有效期(30s,期内视为在线) + +// 设备类型定义(与 DOC-08 一致) +const DEVICE_TYPES = ['inspection_robot', 'drone', 'charging_pile', 'gateway', 'sensor']; + +app.use(cors()); +app.use(express.json()); + +// ─── 文件读写 ───────────────────────────────────────────── +function readDevices() { + try { + const raw = fs.readFileSync(DATA_FILE, 'utf-8'); + return JSON.parse(raw); + } catch { + return []; + } +} + +function writeDevices(devices) { + fs.writeFileSync(DATA_FILE, JSON.stringify(devices, null, 2), 'utf-8'); +} + +// 内存索引(latest heartbeat 不落盘,重启后按 lastHeartbeat 重新判定) +const onlineCache = new Map(); // deviceId -> lastSeenAt + +// ─── 工具函数 ───────────────────────────────────────────── +function toPublic(dev) { + const lastSeen = onlineCache.get(dev.deviceId); + const online = !!lastSeen && (Date.now() - lastSeen) < ONLINE_TTL_MS; + return { + ...dev, + online, + lastHeartbeat: lastSeen ? new Date(lastSeen).toISOString() : dev.lastHeartbeat || null, + }; +} + +// ─── 路由:注册 ─────────────────────────────────────────── +// POST /api/devices/register 注册设备 +app.post('/api/devices/register', (req, res) => { + const { deviceId, type, name, secret, capabilities = [], location, meta = {} } = req.body || {}; + if (!deviceId || !type) { + return res.status(400).json({ code: 40001, msg: 'deviceId 与 type 为必填项' }); + } + if (!DEVICE_TYPES.includes(type)) { + return res.status(400).json({ code: 40002, msg: `type 非法,允许值: ${DEVICE_TYPES.join(', ')}` }); + } + const devices = readDevices(); + if (devices.some((d) => d.deviceId === deviceId)) { + return res.status(409).json({ code: 40901, msg: `设备 ${deviceId} 已注册` }); + } + const dev = { + deviceId, + type, + name: name || deviceId, + secret: secret || uuidv4().replace(/-/g, '').slice(0, 16), + capabilities, + location: location || null, + meta, + status: 'inactive', // inactive | online | offline + registeredAt: new Date().toISOString(), + lastHeartbeat: null, + }; + devices.push(dev); + writeDevices(devices); + onlineCache.set(deviceId, Date.now()); + res.json({ code: 0, msg: '注册成功', data: toPublic(dev) }); +}); + +// ─── 路由:查询 ─────────────────────────────────────────── +// GET /api/devices?type=&online=&keyword= 设备列表 +app.get('/api/devices', (req, res) => { + const { type, online, keyword } = req.query; + let list = readDevices().map(toPublic); + if (type) list = list.filter((d) => d.type === type); + if (online === 'true') list = list.filter((d) => d.online); + if (online === 'false') list = list.filter((d) => !d.online); + if (keyword) list = list.filter((d) => d.deviceId.includes(keyword) || (d.name || '').includes(keyword)); + res.json({ code: 0, data: list }); +}); + +// GET /api/devices/:id 设备详情 +app.get('/api/devices/:id', (req, res) => { + const dev = readDevices().find((d) => d.deviceId === req.params.id); + if (!dev) return res.status(404).json({ code: 40401, msg: '设备不存在' }); + res.json({ code: 0, data: toPublic(dev) }); +}); + +// GET /api/devices/:id/online 在线状态查询 +app.get('/api/devices/:id/online', (req, res) => { + const dev = readDevices().find((d) => d.deviceId === req.params.id); + if (!dev) return res.status(404).json({ code: 40401, msg: '设备不存在' }); + res.json({ code: 0, data: { deviceId: dev.deviceId, online: toPublic(dev).online } }); +}); + +// ─── 路由:生命周期 ─────────────────────────────────────── +// POST /api/devices/:id/heartbeat 心跳上报(保活) +app.post('/api/devices/:id/heartbeat', (req, res) => { + const devices = readDevices(); + const dev = devices.find((d) => d.deviceId === req.params.id); + if (!dev) return res.status(404).json({ code: 40401, msg: '设备不存在' }); + const now = Date.now(); + onlineCache.set(dev.deviceId, now); + dev.status = 'online'; + dev.lastHeartbeat = new Date(now).toISOString(); + if (req.body && typeof req.body.meta === 'object') { + dev.meta = { ...(dev.meta || {}), ...req.body.meta }; + } + writeDevices(devices); + res.json({ code: 0, msg: '心跳已记录', data: toPublic(dev) }); +}); + +// POST /api/devices/:id/offline 主动下线 +app.post('/api/devices/:id/offline', (req, res) => { + const devices = readDevices(); + const dev = devices.find((d) => d.deviceId === req.params.id); + if (!dev) return res.status(404).json({ code: 40401, msg: '设备不存在' }); + onlineCache.delete(dev.deviceId); + dev.status = 'offline'; + writeDevices(devices); + res.json({ code: 0, msg: '设备已下线', data: toPublic(dev) }); +}); + +// DELETE /api/devices/:id 注销设备 +app.delete('/api/devices/:id', (req, res) => { + const devices = readDevices(); + const idx = devices.findIndex((d) => d.deviceId === req.params.id); + if (idx === -1) return res.status(404).json({ code: 40401, msg: '设备不存在' }); + const [removed] = devices.splice(idx, 1); + writeDevices(devices); + onlineCache.delete(removed.deviceId); + res.json({ code: 0, msg: '设备已注销', data: { deviceId: removed.deviceId } }); +}); + +// ─── 离线扫描(心跳超时判定)───────────────────────────── +setInterval(() => { + const now = Date.now(); + const devices = readDevices(); + let changed = false; + for (const dev of devices) { + const lastSeen = onlineCache.get(dev.deviceId); + if (lastSeen && now - lastSeen > HEARTBEAT_TIMEOUT_MS) { + onlineCache.delete(dev.deviceId); + if (dev.status === 'online') { + dev.status = 'offline'; + changed = true; + console.log(`[heartbeat-timeout] 设备 ${dev.deviceId} 心跳超时,判定离线`); + } + } + } + if (changed) writeDevices(devices); +}, SCAN_INTERVAL_MS); + +app.listen(PORT, () => { + console.log(`✅ 设备接入服务 Alpha 已启动 → http://localhost:${PORT}`); + console.log(` API 示例: GET /api/devices`); + console.log(` POST /api/devices/register {deviceId, type, name, capabilities}`); + console.log(` POST /api/devices/:id/heartbeat`); +}); diff --git a/services/status/data/status.json b/services/status/data/status.json new file mode 100644 index 0000000..777039a --- /dev/null +++ b/services/status/data/status.json @@ -0,0 +1,42 @@ +{ + "latest": { + "ROBOT-001": { + "deviceId": "ROBOT-001", + "position": { + "x": 10, + "y": 5, + "z": 0, + "yaw": 90 + }, + "battery": 85, + "mode": "patrol", + "speed": 1.2, + "sensors": {}, + "alarms": [], + "meta": {}, + "ts": "2026-08-10T03:07:37.861Z", + "receivedAt": "2026-08-10T03:07:37.861Z" + } + }, + "history": { + "ROBOT-001": [ + { + "deviceId": "ROBOT-001", + "position": { + "x": 10, + "y": 5, + "z": 0, + "yaw": 90 + }, + "battery": 85, + "mode": "patrol", + "speed": 1.2, + "sensors": {}, + "alarms": [], + "meta": {}, + "ts": "2026-08-10T03:07:37.861Z", + "receivedAt": "2026-08-10T03:07:37.861Z" + } + ] + } +} \ No newline at end of file diff --git a/services/status/package-lock.json b/services/status/package-lock.json new file mode 100644 index 0000000..ef15b21 --- /dev/null +++ b/services/status/package-lock.json @@ -0,0 +1,870 @@ +{ + "name": "status-service", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "status-service", + "version": "0.1.0", + "dependencies": { + "cors": "^2.8.5", + "express": "^4.18.2", + "uuid": "^9.0.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", + "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + } + } +} diff --git a/services/status/package.json b/services/status/package.json new file mode 100644 index 0000000..b48059a --- /dev/null +++ b/services/status/package.json @@ -0,0 +1,14 @@ +{ + "name": "status-service", + "version": "0.1.0", + "description": "机器人智慧服务系统 - 状态服务 Alpha", + "main": "server.js", + "scripts": { + "start": "node server.js" + }, + "dependencies": { + "express": "^4.18.2", + "cors": "^2.8.5", + "uuid": "^9.0.0" + } +} diff --git a/services/status/server.js b/services/status/server.js new file mode 100644 index 0000000..76e330b --- /dev/null +++ b/services/status/server.js @@ -0,0 +1,125 @@ +/** + * 机器人智慧服务系统 · 状态服务 Alpha (status-service) + * 端口: 3020 + * 职责: 设备状态上报接收、最新状态缓存、历史状态查询、SSE 实时订阅 + * 数据: services/status/data/status.json(本地文件替代数据库) + * 文档: 详见 docs/DOC-08-设备数据字典-V1.0草案.md + */ +const express = require('express'); +const cors = require('cors'); +const fs = require('fs'); +const path = require('path'); + +const app = express(); +const PORT = 3020; +const DATA_FILE = path.join(__dirname, 'data', 'status.json'); +const MAX_HISTORY_PER_DEVICE = 500; // 每设备历史条数上限(超出丢弃最旧) + +app.use(cors()); +app.use(express.json()); + +// ─── 文件读写 ───────────────────────────────────────────── +function readData() { + try { + const raw = fs.readFileSync(DATA_FILE, 'utf-8'); + return JSON.parse(raw); + } catch { + return { latest: {}, history: {} }; + } +} + +function writeData(data) { + fs.writeFileSync(DATA_FILE, JSON.stringify(data, null, 2), 'utf-8'); +} + +// ─── SSE 订阅者 ─────────────────────────────────────────── +const subscribers = new Set(); // res 集合 + +function broadcast(payload) { + const msg = `data: ${JSON.stringify(payload)}\n\n`; + for (const res of subscribers) { + try { res.write(msg); } catch { subscribers.delete(res); } + } +} + +// ─── 路由:上报 ─────────────────────────────────────────── +// POST /api/status/report 状态上报(设备 -> 平台) +// body: { deviceId, position?, battery?, mode?, speed?, sensors?, alarms?, timestamp? } +app.post('/api/status/report', (req, res) => { + const { + deviceId, position, battery, mode, speed, sensors, alarms, timestamp, meta = {}, + } = req.body || {}; + if (!deviceId) { + return res.status(400).json({ code: 40001, msg: 'deviceId 为必填项' }); + } + + const now = new Date().toISOString(); + const record = { + deviceId, + position: position || null, // {x,y,z,yaw} + battery: battery ?? null, // 0~100 整数百分比 + mode: mode || null, // idle/patrol/charging/working/emergency + speed: speed ?? null, // m/s + sensors: sensors || {}, // 传感器键值 + alarms: alarms || [], // 告警编码列表 + meta, + ts: timestamp || now, // 设备侧时间(ISO) + receivedAt: now, // 平台接收时间(ISO) + }; + + const data = readData(); + data.latest[deviceId] = record; + const history = data.history[deviceId] || (data.history[deviceId] = []); + history.push(record); + if (history.length > MAX_HISTORY_PER_DEVICE) history.splice(0, history.length - MAX_HISTORY_PER_DEVICE); + writeData(data); + + // 实时广播给订阅者 + broadcast({ type: 'status', deviceId, record }); + + res.json({ code: 0, msg: '状态已接收', data: { deviceId, receivedAt: now } }); +}); + +// ─── 路由:查询 ─────────────────────────────────────────── +// GET /api/status/summary 全设备最新状态汇总 +app.get('/api/status/summary', (req, res) => { + const data = readData(); + res.json({ code: 0, data: Object.values(data.latest) }); +}); + +// GET /api/status/:deviceId/latest 指定设备最新状态 +app.get('/api/status/:deviceId/latest', (req, res) => { + const data = readData(); + const record = data.latest[req.params.deviceId]; + if (!record) return res.status(404).json({ code: 40401, msg: '该设备暂无状态上报' }); + res.json({ code: 0, data: record }); +}); + +// GET /api/status/history/:deviceId?from=&to=&limit= 历史状态 +app.get('/api/status/history/:deviceId', (req, res) => { + const data = readData(); + const history = data.history[req.params.deviceId] || []; + const { from, to, limit } = req.query; + let list = history; + if (from) list = list.filter((r) => r.ts >= from); + if (to) list = list.filter((r) => r.ts <= to); + const n = Math.min(parseInt(limit, 10) || 100, 500); + res.json({ code: 0, data: list.slice(-n) }); +}); + +// GET /api/status/subscribe SSE 实时订阅 +app.get('/api/status/subscribe', (req, res) => { + res.setHeader('Content-Type', 'text/event-stream'); + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Connection', 'keep-alive'); + res.write(`data: ${JSON.stringify({ type: 'connected', msg: '订阅成功' })}\n\n`); + subscribers.add(res); + req.on('close', () => subscribers.delete(res)); +}); + +app.listen(PORT, () => { + console.log(`✅ 状态服务 Alpha 已启动 → http://localhost:${PORT}`); + console.log(` API 示例: POST /api/status/report {deviceId, position, battery, mode}`); + console.log(` GET /api/status/:deviceId/latest`); + console.log(` GET /api/status/subscribe (SSE)`); +});