diff --git a/.gitignore b/.gitignore index 4c2a664..19663d6 100644 --- a/.gitignore +++ b/.gitignore @@ -4,8 +4,9 @@ central-server/.env edge-agent/.env edge-agent/prometheus-edge/data/ -# 生成文件(由 CSV 脚本生成) +# 生成文件(由 update-configs.sh 从 targets.csv 生成) edge-agent/config/*.json +edge-agent/config/target-topology.geojson # Node 依赖 **/node_modules/ diff --git a/README.md b/README.md index 1509a24..aa7a9ef 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ | 步骤 | 做什么 | 命令摘要 | |------|--------|----------| | **第一步** | 部署中央服务器 | `cd central-server && bash deploy.sh` | -| **第二步** | 部署边缘节点(可选,可多台) | 本机同机:`cd edge-agent && bash run-edge-local.sh`;远程:配 `.env` 后 `bash deploy.sh` | +| **第二步** | 部署边缘节点(可选,可多台) | 本机同机:`cd edge-agent && bash deploy.sh --local`;远程:配 `.env` 后 `bash deploy.sh` | | **第三步** | 多用户 / 告警(可选) | Grafana:`central-server/grafana/setup-users.sh`;告警:编辑 `alertmanager/alertmanager.yml` | **完整说明、验证方式与文档入口**:**[doc/README.md](doc/README.md)**(建议先看其中的「部署顺序」)。 @@ -20,9 +20,8 @@ ## 项目结构 -- **central-server/** — 中央:Prometheus、Grafana、VictoriaMetrics、Alertmanager、拓扑标注助手 -- **edge-agent/** — 边缘:Prometheus Edge、ONVIF Exporter、Blackbox Exporter -- **topology-editor/** — 拓扑标注助手(与 central 同机运行):上传/下载 targets.csv,H5 采集 GPS、天地图校验 +- **central-server/** — 中央:Prometheus、Grafana、VictoriaMetrics、Alertmanager +- **edge-agent/** — 边缘:vmagent、ONVIF Exporter、Blackbox Exporter --- @@ -35,11 +34,9 @@ bash deploy.sh ``` 访问 Grafana:http://localhost:3000(admin / admin123)。 - 拓扑标注助手随中央一起启动:http://localhost:4080 — 上传本机 `targets.csv`,选设备、补 GPS 或地图点击修正后下载 CSV,再部署到边缘 `edge-agent/config/`。 - -2. **第二步:部署边缘**(可选) - - 本机同机:`cd edge-agent && bash run-edge-local.sh` - - 边缘在别台机器:在 `edge-agent` 里配 `.env` 的 `CENTRAL_SERVER_HOST`、`CENTRAL_SERVER_PORT=8428`,然后 `cd config && ./update-configs.sh && cd .. && bash deploy.sh` + 2. **第二步:部署边缘**(可选) + - 本机同机:`cd edge-agent && bash deploy.sh --local` + - 边缘在别台机器:在 `edge-agent` 里配 `.env` 后 `bash deploy.sh` 边缘数据在 Grafana 中需选择数据源 **「VictoriaMetrics」** 才能看到;中央自身指标在数据源「Prometheus」。 @@ -53,7 +50,6 @@ - Prometheus: http://localhost:9091 - VictoriaMetrics: http://localhost:8428 - Alertmanager: http://localhost:9093 -- **拓扑标注助手**: http://localhost:4080 - 边缘 Prometheus(端口 9092):http://localhost:9092 --- @@ -64,36 +60,6 @@ Grafana 支持多组织、多用户;通过 Prometheus 标签做数据隔离( --- -## 拓扑标注助手(targets.csv 标注流程) - -拓扑标注助手主要解决「在现场用手机给设备打坐标、维护拓扑关系」的问题,典型使用流程: - -1. **准备 CSV** - 在本机编辑好 `targets.csv`(推荐按 `edge-agent/config/targets.csv` 的示例维护完整的 type/name/role/parent/uplink_type 等字段)。 - -2. **上传到标注助手** - 中央已启动后,浏览器访问 `http://localhost:4080`: - - 在顶部「上传 / 下载 targets.csv」区域,选择本机 `targets.csv` 并点击 **上传 CSV**。 - -3. **现场标注 / 校正坐标** - - 在「选择已有设备」下拉中选中要标注的设备; - - 在现场用手机点击 **获取当前 GPS 坐标**,或在天地图上点击正确位置; - - 如有需要,补充/调整 `parent`、`uplink_type` 等字段; - - 点击 **保存到 targets.csv**(仅修改标注助手中的当前副本)。 - -4. **下载并应用到边缘** - - 在页面顶部点击 **下载 targets.csv**,得到新的 CSV; - - 将其拷贝到边缘节点的 `edge-agent/config/targets.csv` 覆盖原文件; - - 在边缘节点执行: - ```bash - cd edge-agent/config - ./update-configs.sh - ./csv-to-topology-geojson.sh targets.csv topology.geojson - ``` - 以生成最新的 onvif/ping 配置与拓扑 GeoJSON,Grafana Geomap 即可按经纬度和拓扑关系展示设备与链路。 - ---- - ## 文档 **入口与部署顺序**:**[doc/README.md](doc/README.md)** diff --git a/central-server/CONFIGURATION.md b/central-server/CONFIGURATION.md index 7cf3b92..603e80e 100644 --- a/central-server/CONFIGURATION.md +++ b/central-server/CONFIGURATION.md @@ -62,7 +62,7 @@ - 生产环境必须修改 `GRAFANA_ADMIN_PASSWORD` - 建议使用强密码(至少12位,包含大小写字母、数字和特殊字符) -**Geomap 使用天地图缓存**:若在 Grafana 的 Geomap 面板中要用天地图底图且走缓存(不暴露 key),可将 Base layer 设为 XYZ Tile layer,底图 URL:`http://<本机或 central>:4080/tiles/vec/{z}/{x}/{y}`,再添加一层:`http://<本机或 central>:4080/tiles/cva/{z}/{x}/{y}`。需在 central 配置 `TIANDITU_TK` 与 tile-cache。详见 [../doc/TIANDITU_CONFIG.md](../doc/TIANDITU_CONFIG.md)。 +**Geomap 使用天地图缓存**:若在 Grafana 的 Geomap 面板中要用天地图底图且走缓存(不暴露 key),可将 Base layer 设为 XYZ Tile layer,底图 URL:`http://<本机或 central>:4090/tiles/vec/{z}/{x}/{y}`,再添加一层:`http://<本机或 central>:4090/tiles/cva/{z}/{x}/{y}`。需在 central 配置 `TIANDITU_TK` 与 tile-cache。详见 [../doc/TIANDITU_CONFIG.md](../doc/TIANDITU_CONFIG.md)。 ### Prometheus 配置 diff --git a/central-server/docker-compose.yml b/central-server/docker-compose.yml index e3a8a2a..6f2950f 100644 --- a/central-server/docker-compose.yml +++ b/central-server/docker-compose.yml @@ -82,21 +82,6 @@ services: - "--retentionPeriod=${VICTORIAMETRICS_RETENTION_PERIOD:-30d}" - "--httpListenAddr=:${VICTORIAMETRICS_PORT:-8428}" - # 拓扑标注助手(上传/下载 targets.csv,H5 采集 GPS + 天地图校验) - topology-editor: - build: - context: .. - dockerfile: topology-editor/Dockerfile - image: topology-editor:local - container_name: topology-editor - restart: unless-stopped - environment: - - PORT=4080 - - TILE_CACHE_URL=http://tile-cache:4090 - ports: - - "${TOPOLOGY_EDITOR_PORT:-4080}:4080" - mem_limit: "128m" - # 天地图瓦片缓存(节省 key 免费量;可手动清空缓存后重新拉取) tile-cache: build: diff --git a/doc/CENTRAL_SERVER_CONFIG.md b/doc/CENTRAL_SERVER_CONFIG.md index 7da9654..b919f94 100644 --- a/doc/CENTRAL_SERVER_CONFIG.md +++ b/doc/CENTRAL_SERVER_CONFIG.md @@ -29,7 +29,7 @@ central-server/ - **prometheus.yml**:`remote_write` 指向 VictoriaMetrics;`rule_files: alert_rules.yml`;抓取自身、VM、Alertmanager、Grafana。 - **告警规则与通知**:见 [ALERTING.md](ALERTING.md)。 - **Grafana 数据源**:Provisioning 下配置 Prometheus、VictoriaMetrics;查边缘指标请选 **VictoriaMetrics**。 -- **Grafana Geomap 使用天地图缓存**:在 Geomap 面板中将 Base layer 选为 **XYZ Tile layer**,底图 URL 填 `http://:4080/tiles/vec/{z}/{x}/{y}`,再添加一层 XYZ 填 `http://:4080/tiles/cva/{z}/{x}/{y}`(中文注记)。key 仅需在 central 配置 `TIANDITU_TK`,无需在 Grafana 中填写。详见 [TIANDITU_CONFIG.md](TIANDITU_CONFIG.md)。 +- **Grafana Geomap 使用天地图缓存**:在 Geomap 面板中将 Base layer 选为 **XYZ Tile layer**,底图 URL 填 `http://:4090/tiles/vec/{z}/{x}/{y}`,再添加一层 XYZ 填 `http://:4090/tiles/cva/{z}/{x}/{y}`(中文注记)。key 仅需在 central 配置 `TIANDITU_TK`,无需在 Grafana 中填写。详见 [TIANDITU_CONFIG.md](TIANDITU_CONFIG.md)。 - **多用户**:`grafana/setup-users.sh`,见 [USER_MANAGEMENT.md](USER_MANAGEMENT.md)。 ## 修改与重载 diff --git a/doc/DEPLOYMENT_GUIDE.md b/doc/DEPLOYMENT_GUIDE.md index 68b997b..a70210b 100644 --- a/doc/DEPLOYMENT_GUIDE.md +++ b/doc/DEPLOYMENT_GUIDE.md @@ -1,12 +1,12 @@ # 部署指南 -部署顺序见 **[doc/README.md](README.md)#部署顺序**:中央 → 边缘 → 多用户/告警(可选)→ 拓扑标注(可选)。本文为各步操作与验证要点。 +部署顺序见 **[doc/README.md](README.md)#部署顺序**:中央 → 边缘 → 多用户/告警(可选)。本文为各步操作与验证要点。 --- ## 第一步:部署中央服务器 -**前置**:Docker、Docker Compose;端口 3000、9091、8428、9093、4080 未被占用;磁盘充足。 +**前置**:Docker、Docker Compose;端口 3000、9091、8428、9093、4090 未被占用;磁盘充足。 ```bash cd central-server @@ -14,7 +14,7 @@ cp env.example .env # 可选 bash deploy.sh ``` -**验证**:Grafana http://localhost:3000(admin/admin123)、Prometheus http://localhost:9091、VictoriaMetrics http://localhost:8428、拓扑标注助手 http://localhost:4080。 +**验证**:Grafana http://localhost:3000(admin/admin123)、Prometheus http://localhost:9091、VictoriaMetrics http://localhost:8428。 --- @@ -22,10 +22,10 @@ bash deploy.sh **前提**:中央已运行,VictoriaMetrics 8428 可访问。 -- **本机同机**:`cd edge-agent && bash run-edge-local.sh`(中央地址设为 host.docker.internal:8428)。 +- **本机同机**:`cd edge-agent && bash deploy.sh --local`(中央地址设为 host.docker.internal:8428)。 - **边缘在另一台机器**: - 在 edge-agent 下 `cp env.example .env`,编辑 `CENTRAL_SERVER_HOST`、`CENTRAL_SERVER_PORT=8428`。 - - `cd config && ./update-configs.sh && cd .. && bash deploy.sh`。 + - `bash deploy.sh`(会自动调用 update-configs)。 **验证**:边缘 Prometheus http://localhost:9092(或边缘机 IP:9092);中央 Grafana 选数据源 **VictoriaMetrics**,查询 `probe_success{job="network-ping"}` 可见边缘数据。 @@ -42,16 +42,6 @@ bash deploy.sh --- -## 第四步(可选):拓扑标注助手 - -与中央同机运行,访问 http://localhost:4080。上传本机 `targets.csv` → 选择设备、GPS 或地图点击补坐标 → 保存 → 下载 CSV → 将下载文件部署到各边缘 `edge-agent/config/targets.csv`,在边缘执行: - -```bash -cd edge-agent/config && ./update-configs.sh && ./csv-to-topology-geojson.sh targets.csv topology.geojson -``` - ---- - ## 部署后检查清单 - **中央**:`docker compose ps` 中相关服务 Up;Grafana 中 Prometheus 数据源可查 `up`。 diff --git a/doc/EDGE_AGENT_CONFIG.md b/doc/EDGE_AGENT_CONFIG.md index 20f3c59..5cdd104 100644 --- a/doc/EDGE_AGENT_CONFIG.md +++ b/doc/EDGE_AGENT_CONFIG.md @@ -2,34 +2,49 @@ ## 需要什么 -| 类型 | 说明 | 对应组件 | -|------|------|----------| -| **必选** | remote_write 推送到中央 | prometheus-edge | -| **必选** | Ping/网络探测 | blackbox-exporter | -| **可选** | ONVIF 等 | onvif-exporter(`--profile onvif`),见 [ONVIF_ALTERNATIVES.md](ONVIF_ALTERNATIVES.md) | + +| 类型 | 说明 | 对应组件 | +| ------ | ------------------ | ---------------------------------------------------------------------------------- | +| **必选** | remote_write 推送到中央 | prometheus-edge | +| **必选** | Ping/网络探测 | blackbox-exporter | +| **可选** | ONVIF 等 | onvif-exporter(`--profile onvif`),见 [ONVIF_ALTERNATIVES.md](ONVIF_ALTERNATIVES.md) | + ## 容器与数据流 -| 容器 | 作用 | 端口 | -|------|------|------| -| prometheus-edge | 抓取 Blackbox(及可选 ONVIF),remote_write → 中央 VictoriaMetrics | 9092 | -| blackbox-exporter | Ping/HTTP/TCP 探测 | 9115(内部) | -| onvif-exporter | 可选,ONVIF 探测 | 9600(内部) | + +| 容器 | 作用 | 端口 | +| ----------------- | -------------------------------------------------------- | -------- | +| prometheus-edge (vmagent) | 抓取 Blackbox(及可选 ONVIF),remote_write → 中央 VictoriaMetrics,含内存+磁盘缓存 | 9092 | +| blackbox-exporter | Ping/HTTP/TCP 探测 | 9115(内部) | +| onvif-exporter | 可选,ONVIF 探测 | 9600(内部) | + 数据流:目标 → Exporter → prometheus-edge → remote_write → 中央 VictoriaMetrics。 ## 目录与配置 - **config/targets.csv**:统一监控目标(ping/onvif/topology),格式与脚本见 [TARGETS_AND_MONITORING.md](TARGETS_AND_MONITORING.md)。 -- **config/update-configs.sh**:从 targets.csv 生成 `onvif-targets.json`、`ping-targets.json`。 -- **prometheus-edge**:使用 `prometheus.yml.template` + deploy.sh 中 envsubst,注入 `CENTRAL_SERVER_HOST`/`PORT`;数据目录使用 Docker 卷 `prometheus-edge-data`。 -- **.env**:`CENTRAL_SERVER_HOST`、`CENTRAL_SERVER_PORT=8428`、`EDGE_NODE_ID`。本机同机可用 `run-edge-local.sh`(host.docker.internal);跨机填中央 IP。 +- **config/update-configs.sh**:从 targets.csv 生成 `target-onvif.json`、`target-ping.json`、`target-topology.geojson`。 +- **prometheus-edge (vmagent)**:使用 `vmagent-scrape.yml.template` 抓取;`CENTRAL_SERVER_HOST`/`PORT` 来自 `.env`;磁盘缓存卷 `vmagent-cache-data`。 +- **.env**:`CENTRAL_SERVER_HOST`、`CENTRAL_SERVER_PORT=8428`、`EDGE_NODE_ID`。本机同机用 `./deploy.sh --local`;跨机配 `.env` 后 `./deploy.sh`。 ## 常用操作 - 改监控目标:编辑 `config/targets.csv` → `cd config && ./update-configs.sh`,必要时重启 prometheus-edge。 - 改中央地址:编辑 `.env` → `docker compose restart prometheus-edge`。 +## 中心宕机 / 断网时的缓存 + +`docker-compose.yaml` 使用 vmagent 统一实现: + +- **短时内存缓存**:中心短暂不可达时在内存中缓冲 +- **长时磁盘缓存**:长时间离线时写入磁盘队列(默认 512MB),恢复后自动补传 +- **冗余重试**:失败自动重试,边缘重启后从磁盘恢复未上传数据 + +详见 [EDGE_CACHE.md](EDGE_CACHE.md)。 + ## 相关文档 -- [DEPLOYMENT_GUIDE.md](DEPLOYMENT_GUIDE.md) | [TARGETS_AND_MONITORING.md](TARGETS_AND_MONITORING.md) | [ONVIF_ALTERNATIVES.md](ONVIF_ALTERNATIVES.md) | [ARCHITECTURE.md](ARCHITECTURE.md) +- [EDGE_CACHE.md](EDGE_CACHE.md) | [DEPLOYMENT_GUIDE.md](DEPLOYMENT_GUIDE.md) | [TARGETS_AND_MONITORING.md](TARGETS_AND_MONITORING.md) | [ONVIF_ALTERNATIVES.md](ONVIF_ALTERNATIVES.md) | [ARCHITECTURE.md](ARCHITECTURE.md) + diff --git a/doc/EDGE_CACHE.md b/doc/EDGE_CACHE.md new file mode 100644 index 0000000..ac59da4 --- /dev/null +++ b/doc/EDGE_CACHE.md @@ -0,0 +1,34 @@ +# 边缘节点缓存 + +## 架构 + +`docker-compose.yaml` 使用 **vmagent** 统一实现: + +| 层级 | 机制 | 说明 | +|------|------|------| +| **短时内存缓存** | 内存队列 | 中心短暂不可达时在内存中缓冲(~30 分钟量级) | +| **长时磁盘缓存** | 持久化队列 | 长时间离线时写入磁盘(默认 512MB),恢复后自动补传 | +| **冗余重试** | 失败重试 | 自动重试,边缘重启后从磁盘恢复未上传数据 | + +## 部署 + +```bash +cd edge-agent +bash deploy.sh +# 本机同机: bash deploy.sh --local +``` + +## 配置 + +| 参数 | 默认值 | 说明 | +|------|--------|------| +| `-remoteWrite.maxDiskUsagePerURL` | 512MB | 每 URL 最大磁盘缓存 | +| `-remoteWrite.tmpDataPath` | /cache/remotewrite | 磁盘队列路径 | +| `-memory.allowedPercent` | 80 | 内存队列可用比例 | + +修改 `docker-compose.yaml` 中 `prometheus-edge` 的 `command` 可调整上述参数。 + +## 监控 + +- `vmagent_remotewrite_pending_bytes`:待发送字节数 +- `vmagent_remotewrite_packets_dropped_total`:丢包数 diff --git a/doc/ONVIF_ALTERNATIVES.md b/doc/ONVIF_ALTERNATIVES.md index be6f635..d9877ca 100644 --- a/doc/ONVIF_ALTERNATIVES.md +++ b/doc/ONVIF_ALTERNATIVES.md @@ -55,7 +55,7 @@ 不解析 ONVIF,只监控“摄像头/NVR 是否在线、端口是否可达”。 - **已有组件**:边缘节点已包含 **Blackbox Exporter**(如 `prom/blackbox-exporter`)。 -- **做法**:在 `config/ping-targets.json`(或等价目标列表)中加入摄像头/NVR 的 IP,用 ICMP 或 TCP/HTTP 探测(例如对 80/8000 等端口做 `tcp_connect` 或 `http_2xx`)。 +- **做法**:在 `config/target-ping.json`(或等价目标列表)中加入摄像头/NVR 的 IP,用 ICMP 或 TCP/HTTP 探测(例如对 80/8000 等端口做 `tcp_connect` 或 `http_2xx`)。 - **优点**:无需任何 ONVIF 镜像,部署即可用,与现有 Ping 监控一致。 - **缺点**:无设备级 ONVIF 状态、无摄像头特有指标。 @@ -65,9 +65,9 @@ **本项目已在 edge-agent/onvif-exporter/ 提供自建容器**,无需再找第三方镜像。 -- **实现**:Go + [use-go/onvif](https://github.com/use-go/onvif),读取 `config/onvif-targets.json`(与 `targets.csv` 中 onvif 行一致),轮询 ONVIF `GetDeviceInformation`,暴露 Prometheus 指标 `onvif_device_up`、`onvif_probe_duration_seconds`。 +- **实现**:Go + [use-go/onvif](https://github.com/use-go/onvif),读取 `config/target-onvif.json`(与 `targets.csv` 中 onvif 行一致),轮询 ONVIF `GetDeviceInformation`,暴露 Prometheus 指标 `onvif_device_up`、`onvif_probe_duration_seconds`。 - **启用**:在边缘节点执行 `docker compose --profile onvif up -d --build`,会构建并启动 ONVIF exporter,无需设置 `ONVIF_EXPORTER_IMAGE`。 -- **配置**:在 `config/targets.csv` 中增加 onvif 行(ip、device_type、model、location、username、password、onvif_port),运行 `config/update-configs.sh` 生成 `onvif-targets.json`。 +- **配置**:在 `config/targets.csv` 中增加 onvif 行(ip、device_type、model、location、username、password、onvif_port),运行 `config/update-configs.sh` 生成 `target-onvif.json`。 - 若需自行修改或扩展,见 **edge-agent/onvif-exporter/README.md**。 --- @@ -78,4 +78,4 @@ - **若需要 ONVIF**:使用本项目自建的 **edge-agent/onvif-exporter**,执行 `docker compose --profile onvif up -d --build` 即可构建并启动;无需再设 `ONVIF_EXPORTER_IMAGE`。 - **摄像头支持 SNMP 时**:优先考虑 **SNMP Exporter** 作为“Prometheus 监控摄像头”的替代方案,再根据需要补充 Frigate 或 Blackbox。 -具体边缘配置与 compose 变更见 **[EDGE_AGENT_CONFIG.md](EDGE_AGENT_CONFIG.md)** 及 `edge-agent/docker-compose.yml`。 +具体边缘配置与 compose 变更见 **[EDGE_AGENT_CONFIG.md](EDGE_AGENT_CONFIG.md)** 及 `edge-agent/docker-compose.yaml`。 diff --git a/doc/README.md b/doc/README.md index 5e3acf6..726211b 100644 --- a/doc/README.md +++ b/doc/README.md @@ -6,14 +6,13 @@ ## 部署顺序(必读) -整体顺序:**先中央,后边缘,再按需标注拓扑**。边缘向中央主动上报数据,中央必须先就绪。 +整体顺序:**先中央,后边缘**。边缘向中央主动上报数据,中央必须先就绪。 | 步骤 | 部署 / 操作对象 | 做什么 | 验证 | |------|------------------|--------|------| | **第一步** | 中央服务器 | 部署 Prometheus、Grafana、VictoriaMetrics、Alertmanager | Grafana http://localhost:3000、Prometheus http://localhost:9091 | | **第二步** | 边缘节点(可选,可多台) | 配置中央地址与监控目标,部署边缘 Prometheus + Exporter | 边缘 UI http://localhost:9092,Grafana 选 VictoriaMetrics 数据源可见边缘数据 | | **第三步** | 多用户 / 告警(可选) | 配置 Grafana 组织与用户、Alertmanager 通知 | 按 [USER_MANAGEMENT.md](USER_MANAGEMENT.md)、[ALERTING.md](ALERTING.md) 验证 | -| **第四步** | 拓扑标注助手 topology-editor(可选) | 上传/编辑/下载 `targets.csv`,用 GPS 与天地图给设备打点并维护拓扑关系 | 在 Grafana Geomap 中按经纬度与 parent/uplink_type 展示网络拓扑 | --- @@ -41,7 +40,7 @@ bash deploy.sh - **本机同机**(中央与边缘在同一台机器): ```bash cd edge-agent - bash run-edge-local.sh + bash deploy.sh --local ``` 脚本会设置中央地址为 `host.docker.internal:8428` 并执行部署。 @@ -68,33 +67,6 @@ bash deploy.sh --- -### 第四步(可选):拓扑标注助手 / targets.csv 标注 - -拓扑标注助手是一个与 central 同机运行的小型 Web 服务(在 `topology-editor/` 目录下),用来: - -- 上传 / 下载 `targets.csv`; -- 在手机或浏览器中选择设备、**获取 GPS 定位**;浏览器要求页面为**安全来源**(HTTPS 或 http://localhost / 127.0.0.1),否则会报「only secure origins are allowed」无法定位,需通过 HTTPS 访问或在本机用 localhost 打开。 -- 叠加天地图底图,点击地图修正坐标;天地图需填写 **TK**([申请密钥](https://console.tianditu.gov.cn/)),底图与标识图说明见 [TIANDITU_CONFIG.md](TIANDITU_CONFIG.md)。可选:在 central 配置 **TIANDITU_TK** 启用瓦片缓存,节省 key 免费量,缓存按 TTL 自动老化。 -- 维护 `name` / `role` / `parent` / `uplink_type` 等拓扑字段。 - -典型用法: - -1. **上传 CSV**:在本机更新 `targets.csv` 后,访问 `http://:4080`,在顶部区域上传。 -2. **选择设备补点**:在下拉框中选择已有设备,用「获取 GPS」或点地图修正经纬度,必要时调整 `parent` / `uplink_type`。 -3. **保存标注**:点击「保存到 targets.csv」仅更新标注助手中的当前副本。 -4. **下载 CSV**:点击「下载 targets.csv」得到新的 CSV,将其下发到各边缘节点的 `edge-agent/config/targets.csv`。 -5. **在边缘生成配置与拓扑**:在边缘执行: - - ```bash - cd edge-agent/config - ./update-configs.sh - ./csv-to-topology-geojson.sh targets.csv topology.geojson - ``` - - 之后 Grafana Geomap 可以同时展示:设备点位(lat/lon)、上下级连线(parent)、链路类型(uplink_type)。 - ---- - ## 文档列表(按用途) ### 架构与数据流 @@ -136,6 +108,5 @@ bash deploy.sh - **第一次部署**:按上面「部署顺序」先做第一步,再做第二步。 - **只改中央配置**:看 [CENTRAL_SERVER_CONFIG.md](CENTRAL_SERVER_CONFIG.md)、[CONFIGURATION.md](../central-server/CONFIGURATION.md)。 - **只改边缘 / 监控目标**:看 [EDGE_AGENT_CONFIG.md](EDGE_AGENT_CONFIG.md)、[TARGETS_AND_MONITORING.md](TARGETS_AND_MONITORING.md)。 -- **拓扑标注助手**:第四步;上传/编辑/下载 targets.csv;天地图底图与标识图见 [TIANDITU_CONFIG.md](TIANDITU_CONFIG.md)。 - **多用户 / 告警**:看 [USER_MANAGEMENT.md](USER_MANAGEMENT.md)、[ALERTING.md](ALERTING.md)。 - **出问题**:看 [TROUBLESHOOTING.md](TROUBLESHOOTING.md)、[DEPLOYMENT_GUIDE.md](DEPLOYMENT_GUIDE.md)。 diff --git a/doc/TARGETS_AND_MONITORING.md b/doc/TARGETS_AND_MONITORING.md index ebaab3c..91f18ec 100644 --- a/doc/TARGETS_AND_MONITORING.md +++ b/doc/TARGETS_AND_MONITORING.md @@ -1,6 +1,6 @@ # 监控目标与 targets.csv -边缘监控目标统一由 `edge-agent/config/targets.csv` 配置,经 `update-configs.sh` 生成 `onvif-targets.json`、`ping-targets.json`,并可生成拓扑 GeoJSON 供 Grafana Geomap 使用。 +边缘监控目标统一由 `edge-agent/config/targets.csv` 配置,经 `update-configs.sh` 生成 `target-onvif.json`、`target-ping.json`、`target-topology.geojson`。 --- @@ -25,9 +25,9 @@ type,ip,name,role,parent,uplink_type,network,device_type,model,location,username | username, password, onvif_port | ONVIF 认证与端口(默认 80) | onvif | | lat, lon | 经纬度(十进制度),Geomap 打点与拓扑 | 可选 | -- **ping**:有 IP,由 Blackbox Exporter 探测,生成 `ping-targets.json`。 -- **onvif**:有 IP,由 ONVIF Exporter 探测,生成 `onvif-targets.json`;需填 device_type, model, location, username, password。 -- **topology**:仅拓扑节点(可无 IP),不参与抓取;用于生成 `topology.geojson` 画点与连线。 +- **ping**:有 IP,由 Blackbox Exporter 探测,生成 `target-ping.json`。 +- **onvif**:有 IP,由 ONVIF Exporter 探测,生成 `target-onvif.json`;需填 device_type, model, location, username, password。 +- **topology**:仅拓扑节点(可无 IP),不参与抓取;用于生成 `target-topology.geojson` 画点与连线。 --- @@ -47,13 +47,10 @@ onvif,192.168.1.100,camera_front,camera,dumb_sw_1,copper,internal,camera,HIKVISI ```bash cd edge-agent/config -chmod +x *.sh ./update-configs.sh -./csv-to-topology-geojson.sh targets.csv topology.geojson ``` -- `update-configs.sh`:根据 targets.csv 生成 `onvif-targets.json`、`ping-targets.json`。 -- `csv-to-topology-geojson.sh`:生成 `topology.geojson`,供 Grafana Geomap 加载(设备点 + parent 连线,uplink_type 可区分线型)。 +- `update-configs.sh`:根据 targets.csv 生成 `target-onvif.json`、`target-ping.json`、`target-topology.geojson`(含设备点与 parent 连线)。 --- @@ -61,8 +58,8 @@ chmod +x *.sh | 类型 | Job | Exporter | 配置文件 | |------|-----|----------|----------| -| 网络 Ping | network-ping | Blackbox | ping-targets.json | -| ONVIF | onvif-devices | ONVIF Exporter | onvif-targets.json | +| 网络 Ping | network-ping | Blackbox | target-ping.json | +| ONVIF | onvif-devices | ONVIF Exporter | target-onvif.json | | 边缘自身 | prometheus-edge | Prometheus | 内置 | 数据流:目标 → Exporter → prometheus-edge 抓取 → remote_write → 中央 VictoriaMetrics。Grafana 查边缘数据需选 **VictoriaMetrics** 数据源。 diff --git a/doc/TIANDITU_CONFIG.md b/doc/TIANDITU_CONFIG.md index 62e455f..a13cc69 100644 --- a/doc/TIANDITU_CONFIG.md +++ b/doc/TIANDITU_CONFIG.md @@ -1,23 +1,12 @@ # 天地图配置说明 -拓扑标注助手使用**天地图**作为地图校验底图,便于在浏览器中点击修正设备经纬度。天地图提供**底图**与**标识图(中文注记)**两个图层,可单独或叠加使用。 +**天地图** 提供**底图**与**标识图(中文注记)**两个图层。**tile-cache** 服务缓存瓦片,供 Grafana Geomap 使用,节省 key 免费量。 -**天地图密钥**:使用瓦片缓存或 Grafana Geomap 时,密钥统一配置在 **central-server/.env** 的 **TIANDITU_TK** 变量中,由 tile-cache 服务读取,不在浏览器或 Grafana 中填写。 +**天地图密钥**:统一配置在 **central-server/.env** 的 **TIANDITU_TK** 变量中,由 tile-cache 服务读取。 --- -## 1. 在拓扑标注助手中使用 - -- 打开拓扑标注助手:`http://<中央服务器>:4080` -- 在「地图校验」区域填写 **天地图 TK**(密钥),点击「加载天地图」即可加载底图并点击地图修正坐标。 -- TK 会保存在浏览器本地(localStorage),同一设备填一次即可。 -- **申请密钥**:登录 [天地图开放平台](https://console.tianditu.gov.cn/) 注册并创建应用,获取 **tk** 参数。 - -当前前端通过天地图 JavaScript API 加载地图;若需在其它系统(如 Grafana Geomap)中复用天地图,可使用下方 WMTS 地址。 - ---- - -## 2. 两个图层说明 +## 1. 两个图层说明 | 图层 | 用途 | 说明 | |------|------|------| @@ -28,7 +17,7 @@ --- -## 3. WMTS 地址(底图 + 标识图) +## 2. WMTS 地址(直连)(底图 + 标识图) 若在 Grafana、其它 GIS 或自研前端中通过 WMTS 接入天地图,可使用以下地址。请将 `tk=您的密钥` 替换为在 [天地图开放平台](https://console.tianditu.gov.cn/) 申请得到的 **tk**。 @@ -46,48 +35,42 @@ https://t0.tianditu.gov.cn/cva_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0 - **瓦片参数**:`{z}` 为层级(zoom),`{y}` 为行号,`{x}` 为列号;由地图引擎在请求时替换。 - **同一密钥**:底图与标识图使用同一个 **tk** 即可。 -- **坐标系**:上述为 WGS84(经纬度),与 topology-editor、targets.csv 中 lat/lon 一致。 +- **坐标系**:上述为 WGS84(经纬度),与 targets.csv 中 lat/lon 一致。 --- -## 4. 瓦片缓存与手动更新(节省 key 免费量) +## 3. 瓦片缓存(节省 key 免费量) 天地图 key 有免费调用量限制。本项目提供 **tile-cache** 服务:瓦片首次请求时向天地图拉取并落盘,后续同一瓦片在**老化时间**内直接读缓存;超过老化时间的瓦片在下次请求时会自动重新拉取。 -### 4.1 启用缓存服务 +### 3.1 启用缓存服务 - **天地图密钥**:在 **central-server/.env** 中配置 **TIANDITU_TK**(必填,否则缓存服务无法回源)。例如:`TIANDITU_TK=您的天地图密钥`。密钥在 [天地图开放平台](https://console.tianditu.gov.cn/) 申请。 - **服务器端 403**:瓦片由 tile-cache 容器向天地图发起请求,出口 IP 为服务器公网 IP。若控制台中为该 key 设置了 **IP 白名单**,必须将服务器公网 IP 加入白名单;若只设置了 **Referer 白名单**,服务器请求无 Referer 易被拒,可暂时关闭 Referer 校验或按平台说明配置后再试。 - **缓存老化时间**(可选):`TILE_CACHE_TTL_DAYS=7`(默认 7 天)。单个瓦片超过该天数后,下次被请求时会重新向天地图拉取并覆盖缓存。可改为 15 等更大值以延长复用时间。 - **上游超时**(可选):向天地图请求单瓦片超时时间,默认 15 秒;若日志中频繁出现 `upstream timeout`,可在 `.env` 中设置 `TILE_CACHE_UPSTREAM_TIMEOUT_MS=25000`(单位毫秒)等更大值后重启 tile-cache。 -- 与 central 一起启动时,**tile-cache** 容器会自动启动(端口默认 4090),拓扑标注助手通过内部代理使用 `/tiles` 路径,无需在浏览器暴露 key。 - -### 4.2 拓扑标注助手中使用缓存 - -- 当 central 已配置 `TILE_CACHE_URL`(默认已指向 tile-cache)时,页面会显示 **「加载天地图(使用服务器缓存)」**:从服务器缓存加载底图 + 标识图,不消耗浏览器端 key。超过 TTL 的瓦片会在下次浏览时自动更新。 - -### 4.3 主机直连 tile-cache 测试(排查用) - -- 从**主机**上 curl 测试 tile-cache 时,若使用 `http://localhost:4090` 出现**无响应、无日志**(请求未进容器),多半是系统把 `localhost` 解析到 IPv6 (`::1`),而 Docker 只把端口映射到 IPv4。请改用 **`http://127.0.0.1:4090`** 再试,例如: - `curl -s http://127.0.0.1:4090/health`、`curl -s http://127.0.0.1:4090/api/cache/status`。 - 标注助手通过 topology-editor 代理访问 tile-cache,走内网 `tile-cache:4090`,不受此影响。 - -### 4.4 Grafana Geomap 使用缓存(可选) +### 3.2 Grafana Geomap 使用缓存 将 Geomap 的 XYZ 底图/标识图 URL 改为: -`http://:4080/tiles/vec/{z}/{x}/{y}` 与 `http://:4080/tiles/cva/{z}/{x}/{y}`(经拓扑助手代理)。天地图密钥已在 **.env** 的 **TIANDITU_TK** 中配置,Grafana 中无需填写。 +`http://:4090/tiles/vec/{z}/{x}/{y}` 与 `http://:4090/tiles/cva/{z}/{x}/{y}`。 +天地图密钥已在 **.env** 的 **TIANDITU_TK** 中配置,Grafana 中无需填写。 -### 4.5 更新方式 +### 3.3 主机直连 tile-cache 测试(排查用) + +从**主机** curl 测试 tile-cache 时,若 `http://localhost:4090` 无响应,可改用 **`http://127.0.0.1:4090`**: +`curl -s http://127.0.0.1:4090/health` + +### 3.4 更新方式 超过 `TILE_CACHE_TTL_DAYS` 天的瓦片,在下次被请求时会自动重新向天地图拉取并写回缓存,无需手动操作。 --- -## 5. 在 Grafana Geomap 中配置天地图(直连或走缓存) +## 4. 在 Grafana Geomap 中配置天地图(直连或走缓存) -Grafana 的 Geomap 支持 **XYZ Tile layer**。可直连天地图 WMTS(URL 中填 tk),或使用瓦片缓存地址(见第 4 节)。 +Grafana 的 Geomap 支持 **XYZ Tile layer**。可直连天地图 WMTS(URL 中填 tk),或使用瓦片缓存(见第 3 节)。 -### 5.1 配置底图(vec_w) +### 4.1 配置底图(vec_w) 1. 新建或编辑一个 **Geomap** 面板。 2. 在右侧 **Layer** / **Base layer** 区域,将底图类型选为 **XYZ Tile layer**(或「自定义」/「Generic XYZ」等,视 Grafana 版本而定)。 @@ -100,7 +83,7 @@ https://t0.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0 4. **Attribution** 可填:`© 天地图`。 5. 保存面板后即可看到天地图矢量底图。 -### 5.2 叠加标识图(cva_w,中文注记) +### 4.2 叠加标识图(cva_w,中文注记) 若需要地名、道路名等中文注记,可在同一 Geomap 上再添加一层 XYZ 瓦片,叠在底图之上: @@ -115,7 +98,7 @@ https://t0.tianditu.gov.cn/cva_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0 4. 可将该层的 **Opacity** 设为 1(不透明),这样注记清晰可见。 5. 图层顺序:底图在下,标识图在上;若顺序反了,可在面板里拖拽调整。 -### 5.3 说明 +### 4.3 说明 - Grafana 会在请求瓦片时把 URL 中的 `{z}`、`{x}`、`{y}` 替换为当前层级与行列号,与天地图 WMTS 的 `TILEMATRIX` / `TILEROW` / `TILECOL` 一一对应。 - 底图与标识图使用**同一个 tk** 即可;tk 在 [天地图开放平台](https://console.tianditu.gov.cn/) 申请。 @@ -123,7 +106,7 @@ https://t0.tianditu.gov.cn/cva_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0 --- -## 6. 参考 +## 5. 参考 - [天地图开放平台](https://www.tianditu.gov.cn/) - [开发文档 / 服务资源](https://lbs.tianditu.gov.cn/server/MapService.html) diff --git a/edge-agent/config/csv-to-json.sh b/edge-agent/config/csv-to-json.sh deleted file mode 100755 index 60dd2af..0000000 --- a/edge-agent/config/csv-to-json.sh +++ /dev/null @@ -1,63 +0,0 @@ -#!/bin/bash - -# CSV转JSON脚本 - 将设备CSV表格转换为Prometheus监控JSON配置 -# 使用方法: ./csv-to-json.sh devices.csv > onvif-targets.json - -set -e - -CSV_FILE=${1:-"devices.csv"} -OUTPUT_FILE=${2:-"onvif-targets.json"} - -# 检查jq是否安装 -if ! command -v jq &> /dev/null; then - echo "❌ jq未安装,请先安装jq:" - echo " Ubuntu/Debian: sudo apt-get install jq" - echo " CentOS/RHEL: sudo yum install jq" - echo " Alpine: apk add jq" - exit 1 -fi - -# 检查CSV文件是否存在 -if [ ! -f "$CSV_FILE" ]; then - echo "❌ CSV文件 $CSV_FILE 不存在" - exit 1 -fi - -echo "🔄 正在转换 $CSV_FILE 到 $OUTPUT_FILE..." - -# 使用jq将CSV转换为JSON -# 1. 读取CSV文件,跳过标题行 -# 2. 将每行转换为JSON对象 -# 3. 构建Prometheus targets格式 - -tail -n +2 "$CSV_FILE" | while IFS=',' read -r ip device_type model location username password onvif_port; do - # 构建labels对象 - labels="{ - \"device_type\": \"$device_type\", - \"model\": \"$model\", - \"location\": \"$location\", - \"username\": \"$username\", - \"password\": \"$password\"" - - # 如果onvif_port不是默认的80,则添加到labels中 - if [ "$onvif_port" != "80" ] && [ -n "$onvif_port" ]; then - labels="$labels, - \"onvif_port\": \"$onvif_port\"" - fi - - labels="$labels - }" - - # 输出JSON对象 - echo "{ - \"targets\": [\"$ip\"], - \"labels\": $labels - }" -done | jq -s '.' > "$OUTPUT_FILE" - -echo "✅ 转换完成!" -echo "📊 生成了 $(jq length "$OUTPUT_FILE") 个设备配置" -echo "📁 输出文件: $OUTPUT_FILE" -echo "" -echo "🔍 预览生成的JSON:" -jq . "$OUTPUT_FILE" diff --git a/edge-agent/config/csv-to-ping-json.sh b/edge-agent/config/csv-to-ping-json.sh deleted file mode 100755 index d6593da..0000000 --- a/edge-agent/config/csv-to-ping-json.sh +++ /dev/null @@ -1,61 +0,0 @@ -#!/bin/bash - -# CSV转Ping JSON脚本 - 将Ping目标CSV表格转换为Prometheus监控JSON配置 -# 使用方法: ./csv-to-ping-json.sh ping-targets.csv > ping-targets.json - -set -e - -CSV_FILE=${1:-"ping-targets.csv"} -OUTPUT_FILE=${2:-"ping-targets.json"} - -# 检查jq是否安装 -if ! command -v jq &> /dev/null; then - echo "❌ jq未安装,请先安装jq:" - echo " Ubuntu/Debian: sudo apt-get install jq" - echo " CentOS/RHEL: sudo yum install jq" - echo " Alpine: apk add jq" - exit 1 -fi - -# 检查CSV文件是否存在 -if [ ! -f "$CSV_FILE" ]; then - echo "❌ CSV文件 $CSV_FILE 不存在" - exit 1 -fi - -echo "🔄 正在转换 $CSV_FILE 到 $OUTPUT_FILE..." - -# 使用jq将CSV转换为JSON -tail -n +2 "$CSV_FILE" | while IFS=',' read -r ip device group network; do - # 构建labels对象 - labels="{ - \"device\": \"$device\"" - - # 添加可选的group标签 - if [ -n "$group" ]; then - labels="$labels, - \"group\": \"$group\"" - fi - - # 添加可选的network标签 - if [ -n "$network" ]; then - labels="$labels, - \"network\": \"$network\"" - fi - - labels="$labels - }" - - # 输出JSON对象 - echo "{ - \"targets\": [\"$ip\"], - \"labels\": $labels - }" -done | jq -s '.' > "$OUTPUT_FILE" - -echo "✅ 转换完成!" -echo "📊 生成了 $(jq length "$OUTPUT_FILE") 个Ping目标配置" -echo "📁 输出文件: $OUTPUT_FILE" -echo "" -echo "🔍 预览生成的JSON:" -jq . "$OUTPUT_FILE" diff --git a/edge-agent/config/csv-to-targets.sh b/edge-agent/config/csv-to-targets.sh deleted file mode 100755 index 21aca83..0000000 --- a/edge-agent/config/csv-to-targets.sh +++ /dev/null @@ -1,154 +0,0 @@ -#!/bin/bash - -# 统一目标配置转换脚本 -# 从 targets.csv 生成 onvif-targets.json 和 ping-targets.json -# 使用方法: ./csv-to-targets.sh targets.csv - -set -e - -CSV_FILE=${1:-"targets.csv"} - -# 检查jq是否安装 -if ! command -v jq &> /dev/null; then - echo "❌ jq未安装,请先安装jq:" - echo " Ubuntu/Debian: sudo apt-get install jq" - echo " CentOS/RHEL: sudo yum install jq" - echo " Fedora: sudo dnf install jq" - exit 1 -fi - -# 检查CSV文件是否存在 -if [ ! -f "$CSV_FILE" ]; then - echo "❌ CSV文件 $CSV_FILE 不存在" - exit 1 -fi - -echo "🔄 正在从 $CSV_FILE 生成配置文件..." -echo "" - -# 临时文件 -ONVIF_TEMP=$(mktemp) -PING_TEMP=$(mktemp) - -# 处理CSV文件(跳过注释行和标题行) -# 列顺序: type,ip,name,role,parent,uplink_type,network,device_type,model,location,username,password,onvif_port,lat,lon -tail -n +2 "$CSV_FILE" | grep -v '^#' | while IFS=',' read -r type ip name role parent uplink_type network device_type model location username password onvif_port lat lon; do - # 去除空格 - type=$(echo "$type" | xargs) - ip=$(echo "$ip" | xargs) - - # 跳过空行 - if [ -z "$type" ]; then - continue - fi - - if [ "$type" = "onvif" ]; then - # 处理 ONVIF 设备 - device_type=$(echo "$device_type" | xargs) - model=$(echo "$model" | xargs) - location=$(echo "$location" | xargs) - username=$(echo "$username" | xargs) - password=$(echo "$password" | xargs) - onvif_port=$(echo "$onvif_port" | xargs) - lat=$(echo "$lat" | xargs) - lon=$(echo "$lon" | xargs) - - labels="{ - \"device_type\": \"$device_type\", - \"model\": \"$model\", - \"location\": \"$location\", - \"username\": \"$username\", - \"password\": \"$password\"" - - if [ "$onvif_port" != "80" ] && [ -n "$onvif_port" ]; then - labels="$labels, - \"onvif_port\": \"$onvif_port\"" - fi - - if [ -n "$lat" ]; then - labels="$labels, - \"lat\": \"$lat\"" - fi - - if [ -n "$lon" ]; then - labels="$labels, - \"lon\": \"$lon\"" - fi - - labels="$labels - }" - - echo "{ - \"targets\": [\"$ip\"], - \"labels\": $labels - }" >> "$ONVIF_TEMP" - - elif [ "$type" = "ping" ]; then - # 处理 Ping 目标 - device=$(echo "$name" | xargs) # name 作为 device 标签 - role=$(echo "$role" | xargs) - parent=$(echo "$parent" | xargs) - uplink_type=$(echo "$uplink_type" | xargs) - network=$(echo "$network" | xargs) - - labels="{ - \"device\": \"$device\"" - - if [ -n "$uplink_type" ]; then - labels="$labels, - \"uplink_type\": \"$uplink_type\"" - fi - - if [ -n "$network" ]; then - labels="$labels, - \"network\": \"$network\"" - fi - - if [ -n "$role" ]; then - labels="$labels, - \"role\": \"$role\"" - fi - - if [ -n "$parent" ]; then - labels="$labels, - \"parent\": \"$parent\"" - fi - - labels="$labels - }" - - echo "{ - \"targets\": [\"$ip\"], - \"labels\": $labels - }" >> "$PING_TEMP" - fi -done - -# 生成 JSON 文件 -if [ -s "$ONVIF_TEMP" ]; then - jq -s '.' "$ONVIF_TEMP" > onvif-targets.json - ONVIF_COUNT=$(jq length onvif-targets.json) - echo "✅ 生成 ONVIF 设备配置: $ONVIF_COUNT 个设备" -else - echo "[]" > onvif-targets.json - echo "⚠️ 未找到 ONVIF 设备,生成空配置" -fi - -if [ -s "$PING_TEMP" ]; then - jq -s '.' "$PING_TEMP" > ping-targets.json - PING_COUNT=$(jq length ping-targets.json) - echo "✅ 生成 Ping 目标配置: $PING_COUNT 个目标" -else - echo "[]" > ping-targets.json - echo "⚠️ 未找到 Ping 目标,生成空配置" -fi - -# 清理临时文件 -rm -f "$ONVIF_TEMP" "$PING_TEMP" - -echo "" -echo "✅ 配置文件生成完成!" -echo "📁 生成的文件:" -echo " - onvif-targets.json" -echo " - ping-targets.json" -echo "" diff --git a/edge-agent/config/csv-to-topology-geojson.sh b/edge-agent/config/csv-to-topology-geojson.sh deleted file mode 100755 index 191c992..0000000 --- a/edge-agent/config/csv-to-topology-geojson.sh +++ /dev/null @@ -1,100 +0,0 @@ -#!/bin/bash - -# 从 targets.csv 生成拓扑 GeoJSON(用于 Grafana Geomap) -# - 支持 type: ping / onvif / topology -# - 使用列: name, role, parent, lat, lon -# - 输出: topology.geojson(FeatureCollection,含 Point 和 LineString) - -set -e - -CSV_FILE=${1:-"targets.csv"} -OUTPUT_FILE=${2:-"topology.geojson"} - -if ! command -v jq &> /dev/null; then - echo "❌ jq 未安装,请先安装 jq" - exit 1 -fi - -if [ ! -f "$CSV_FILE" ]; then - echo "❌ CSV 文件 $CSV_FILE 不存在" - exit 1 -fi - -TMP_JSON=$(mktemp) - -echo "🔄 正在从 $CSV_FILE 生成拓扑 GeoJSON 到 $OUTPUT_FILE ..." - -# 列顺序: type,ip,name,role,parent,uplink_type,network,device_type,model,location,username,password,onvif_port,lat,lon -tail -n +2 "$CSV_FILE" | grep -v '^#' | while IFS=',' read -r type ip name role parent uplink_type network device_type model location username password onvif_port lat lon; do - type=$(echo "$type" | xargs) - name=$(echo "$name" | xargs) - - # 空行跳过 - if [ -z "$type" ] || [ -z "$name" ]; then - continue - fi - - cat >> "$TMP_JSON" < "$OUTPUT_FILE" - -rm -f "$TMP_JSON" - -echo "✅ 已生成 $OUTPUT_FILE" - diff --git a/edge-agent/config/devices.csv b/edge-agent/config/devices.csv deleted file mode 100644 index d572db9..0000000 --- a/edge-agent/config/devices.csv +++ /dev/null @@ -1,5 +0,0 @@ -ip,device_type,model,location,username,password,onvif_port -192.168.1.100,camera,HIKVISION_DS-2CD2342WD-I,front_door,admin,password1,80 -192.168.1.101,camera,DAHUA_IPC-HFW1230S,back_yard,admin,password2,80 -192.168.1.102,camera,UNIVIEW_IPC3120SR,living_room,admin,password3,8080 -192.168.1.50,nvr,HIKVISION_DS-7608NI-I2,server_rack,admin,password4,80 diff --git a/edge-agent/config/ping-targets.csv b/edge-agent/config/ping-targets.csv deleted file mode 100644 index 1b02407..0000000 --- a/edge-agent/config/ping-targets.csv +++ /dev/null @@ -1,6 +0,0 @@ -ip,device,group,network -192.168.1.1,main_router,network,internal -192.168.1.100,front_camera,onvif_cameras,internal -192.168.1.101,back_camera,onvif_cameras,internal -192.168.1.102,living_camera,onvif_cameras,internal -8.8.8.8,google_dns,external,external diff --git a/edge-agent/config/setup-remote-write.sh b/edge-agent/config/setup-remote-write.sh deleted file mode 100755 index 203c0d1..0000000 --- a/edge-agent/config/setup-remote-write.sh +++ /dev/null @@ -1,63 +0,0 @@ -#!/bin/bash - -# 设置远程推送地址脚本 -# 使用方法: ./setup-remote-write.sh <中央服务器地址> [端口] -# 支持IP地址和域名 - -set -e - -CENTRAL_HOST=${1:-"192.168.1.10"} -CENTRAL_PORT=${2:-"8428"} -CONFIG_FILE="../prometheus-edge/prometheus.yml" - -echo "=== 设置Prometheus远程推送地址 ===" -echo "" - -if [ -z "$1" ]; then - echo "使用方法: $0 <中央服务器地址> [端口]" - echo "示例: $0 192.168.1.10 8428" - echo " $0 prometheus.company.com" - echo " $0 prometheus.local 8428" - echo "" - read -p "请输入中央服务器地址 (IP或域名): " CENTRAL_HOST - read -p "请输入端口 (默认8428): " CENTRAL_PORT_INPUT - if [ -n "$CENTRAL_PORT_INPUT" ]; then - CENTRAL_PORT=$CENTRAL_PORT_INPUT - fi -fi - -echo "🔧 配置信息:" -echo " 中央服务器地址: $CENTRAL_HOST" -echo " 端口: $CENTRAL_PORT" -echo " 配置文件: $CONFIG_FILE" -echo "" - -# 检查配置文件是否存在 -if [ ! -f "$CONFIG_FILE" ]; then - echo "❌ 配置文件 $CONFIG_FILE 不存在" - exit 1 -fi - -# 备份原配置文件 -cp "$CONFIG_FILE" "${CONFIG_FILE}.backup.$(date +%Y%m%d_%H%M%S)" -echo "📋 已备份原配置文件" - -# 更新配置文件中的远程推送地址 -sed -i "s|http://\${CENTRAL_SERVER_HOST}:\${CENTRAL_SERVER_PORT}|http://$CENTRAL_HOST:$CENTRAL_PORT|g" "$CONFIG_FILE" - -echo "✅ 远程推送地址已更新" -echo "" - -# 显示更新后的配置 -echo "🔍 更新后的remote_write配置:" -grep -A 5 "remote_write:" "$CONFIG_FILE" - -echo "" -echo "🔄 重启Prometheus服务以应用新配置:" -echo " docker-compose restart prometheus-edge" -echo "" -echo "📊 检查远程写入状态:" -echo " curl http://localhost:9090/api/v1/status/config" -echo "" -echo "🔗 查看远程写入目标:" -echo " curl http://localhost:9090/api/v1/status/tsdb" diff --git a/edge-agent/config/test-connection.sh b/edge-agent/config/test-connection.sh deleted file mode 100755 index c867e37..0000000 --- a/edge-agent/config/test-connection.sh +++ /dev/null @@ -1,90 +0,0 @@ -#!/bin/bash - -# 测试中央服务器连接脚本 -# 使用方法: ./test-connection.sh <中央服务器地址> [端口] - -set -e - -CENTRAL_HOST=${1:-"192.168.1.10"} -CENTRAL_PORT=${2:-"8428"} - -echo "=== 测试中央服务器连接 ===" -echo "" - -if [ -z "$1" ]; then - echo "使用方法: $0 <中央服务器地址> [端口]" - echo "示例: $0 192.168.1.10 8428" - echo " $0 prometheus.company.com" - echo " $0 prometheus.local 9090" - echo "" - read -p "请输入中央服务器地址 (IP或域名): " CENTRAL_HOST - read -p "请输入端口 (默认8428): " CENTRAL_PORT_INPUT - if [ -n "$CENTRAL_PORT_INPUT" ]; then - CENTRAL_PORT=$CENTRAL_PORT_INPUT - fi -fi - -echo "🔧 测试配置:" -echo " 中央服务器地址: $CENTRAL_HOST" -echo " 端口: $CENTRAL_PORT" -echo "" - -# 测试域名解析 -echo "🌐 测试域名解析..." -if command -v nslookup &> /dev/null; then - nslookup $CENTRAL_HOST -else - echo " nslookup 不可用,跳过DNS测试" -fi - -# 测试网络连通性 -echo "" -echo "📡 测试网络连通性..." -if ping -c 3 $CENTRAL_HOST > /dev/null 2>&1; then - echo " ✅ Ping 成功" -else - echo " ❌ Ping 失败" -fi - -# 测试端口连通性 -echo "" -echo "🔌 测试端口连通性..." -if command -v nc &> /dev/null; then - if nc -z $CENTRAL_HOST $CENTRAL_PORT 2>/dev/null; then - echo " ✅ 端口 $CENTRAL_PORT 可访问" - else - echo " ❌ 端口 $CENTRAL_PORT 不可访问" - fi -else - echo " nc 不可用,跳过端口测试" -fi - -# 测试HTTP连接 -echo "" -echo "🌐 测试HTTP连接..." -HTTP_URL="http://$CENTRAL_HOST:$CENTRAL_PORT" -if command -v curl &> /dev/null; then - if curl -s --connect-timeout 5 $HTTP_URL > /dev/null 2>&1; then - echo " ✅ HTTP连接成功: $HTTP_URL" - - # 测试VictoriaMetrics API - if curl -s --connect-timeout 5 "$HTTP_URL/api/v1/status" > /dev/null 2>&1; then - echo " ✅ VictoriaMetrics API 可访问" - else - echo " ⚠️ VictoriaMetrics API 不可访问 (可能不是VictoriaMetrics服务)" - fi - else - echo " ❌ HTTP连接失败: $HTTP_URL" - fi -else - echo " curl 不可用,跳过HTTP测试" -fi - -echo "" -echo "📋 测试完成!" -echo "" -echo "💡 如果连接失败,请检查:" -echo " 1. 网络连接是否正常" -echo " 2. 防火墙是否开放端口 $CENTRAL_PORT" -echo " 3. 中央服务器是否正在运行" -echo " 4. DNS解析是否正确" diff --git a/edge-agent/config/update-configs.sh b/edge-agent/config/update-configs.sh index a5d92f8..0d3aea3 100755 --- a/edge-agent/config/update-configs.sh +++ b/edge-agent/config/update-configs.sh @@ -1,68 +1,61 @@ #!/bin/bash - -# 更新配置文件脚本 - 从CSV生成所有JSON配置文件 -# 使用方法: ./update-configs.sh - +# 从 targets.csv 生成 target-onvif.json、target-ping.json、target-topology.geojson +# 用法: ./update-configs.sh [targets.csv] set -e -echo "=== 更新Prometheus监控配置文件 ===" -echo "" - -# 检查jq是否安装 -if ! command -v jq &> /dev/null; then - echo "❌ jq未安装,请先安装jq:" - echo " Ubuntu/Debian: sudo apt-get install jq" - echo " CentOS/RHEL: sudo yum install jq" - echo " Alpine: apk add jq" - exit 1 -fi - -# 进入脚本目录 cd "$(dirname "$0")" +command -v jq &>/dev/null || { echo "❌ 需要 jq"; exit 1; } -echo "🔄 正在从CSV文件生成JSON配置..." +CSV="${1:-targets.csv}" +[ ! -f "$CSV" ] && { echo '[]' > target-onvif.json; echo '[]' > target-ping.json; echo '{"type":"FeatureCollection","features":[]}' > target-topology.geojson; exit 0; } -# 优先使用统一的 targets.csv -if [ -f "targets.csv" ]; then - echo "📋 使用统一配置文件 targets.csv..." - chmod +x csv-to-targets.sh 2>/dev/null || true - ./csv-to-targets.sh targets.csv -else - echo "⚠️ targets.csv 不存在,使用旧格式配置文件..." - echo "" - - # 兼容旧格式:生成ONVIF设备配置 - if [ -f "devices.csv" ]; then - echo "📱 生成ONVIF设备配置(从 devices.csv)..." - chmod +x csv-to-json.sh 2>/dev/null || true - ./csv-to-json.sh devices.csv onvif-targets.json - else - echo "⚠️ devices.csv 不存在,跳过ONVIF设备配置生成" - echo "[]" > onvif-targets.json +ONVIF=$(mktemp) +PING=$(mktemp) +TMP=$(mktemp) +trap "rm -f $ONVIF $PING $TMP" EXIT + +# target-onvif.json + target-ping.json +tail -n +2 "$CSV" | grep -v '^#' | while IFS=',' read -r type ip name role parent uplink_type network device_type model location username password onvif_port lat lon; do + type=$(echo "$type" | xargs) + ip=$(echo "$ip" | xargs) + [ -z "$type" ] && continue + + if [ "$type" = "onvif" ]; then + device_type=$(echo "$device_type" | xargs) + model=$(echo "$model" | xargs) + location=$(echo "$location" | xargs) + username=$(echo "$username" | xargs) + password=$(echo "$password" | xargs) + onvif_port=$(echo "$onvif_port" | xargs) + labels="{\"device_type\":\"$device_type\",\"model\":\"$model\",\"location\":\"$location\",\"username\":\"$username\",\"password\":\"$password\"" + [ -n "$onvif_port" ] && [ "$onvif_port" != "80" ] && labels="${labels%?},\"onvif_port\":\"$onvif_port\"}" + echo "{\"targets\":[\"$ip\"],\"labels\":$labels}" >> "$ONVIF" + elif [ "$type" = "ping" ]; then + name=$(echo "$name" | xargs) + network=$(echo "$network" | xargs) + role=$(echo "$role" | xargs) + parent=$(echo "$parent" | xargs) + uplink_type=$(echo "$uplink_type" | xargs) + labels="{\"device\":\"$name\"" + [ -n "$network" ] && labels="$labels,\"network\":\"$network\"" + [ -n "$role" ] && labels="$labels,\"role\":\"$role\"" + [ -n "$parent" ] && labels="$labels,\"parent\":\"$parent\"" + [ -n "$uplink_type" ] && labels="$labels,\"uplink_type\":\"$uplink_type\"" + labels="$labels}" + echo "{\"targets\":[\"$ip\"],\"labels\":$labels}" >> "$PING" fi - - # 兼容旧格式:生成Ping目标配置 - if [ -f "ping-targets.csv" ]; then - echo "🌐 生成Ping目标配置(从 ping-targets.csv)..." - chmod +x csv-to-ping-json.sh 2>/dev/null || true - ./csv-to-ping-json.sh ping-targets.csv ping-targets.json - else - echo "⚠️ ping-targets.csv 不存在,跳过Ping目标配置生成" - echo "[]" > ping-targets.json - fi -fi +done -echo "" -echo "✅ 所有配置文件已更新!" -echo "" -echo "📋 生成的文件:" -ls -la *.json 2>/dev/null || echo " (无JSON文件生成)" -echo "" -echo "🔄 配置热重载:" -echo " - Prometheus会在5分钟内自动检测并重载配置" -echo " - 无需重启Docker容器!" -echo "" -echo "⚡ 强制立即重载 (可选):" -echo " docker-compose restart prometheus-edge" -echo "" -echo "📝 编辑CSV文件后重新运行此脚本即可更新配置" +[ -s "$ONVIF" ] && jq -s '.' "$ONVIF" > target-onvif.json || echo '[]' > target-onvif.json +[ -s "$PING" ] && jq -s '.' "$PING" > target-ping.json || echo '[]' > target-ping.json + +# target-topology.geojson +tail -n +2 "$CSV" | grep -v '^#' | while IFS=',' read -r type ip name role parent uplink_type network device_type model location username password onvif_port lat lon; do + [ -z "$(echo $type | xargs)" ] || [ -z "$(echo $name | xargs)" ] && continue + echo "{\"type\":\"$type\",\"ip\":\"$ip\",\"name\":\"$name\",\"role\":\"$role\",\"parent\":\"$parent\",\"uplink_type\":\"$uplink_type\",\"network\":\"$network\",\"device_type\":\"$device_type\",\"model\":\"$model\",\"location\":\"$location\",\"lat\":\"$lat\",\"lon\":\"$lon\"}" >> "$TMP" +done +jq -s ' + def toPoint: select(.lat != "" and .lon != "") | {type:"Feature", geometry:{type:"Point", coordinates:[(.lon|tonumber), (.lat|tonumber)]}, properties:{kind:.type, name:.name, role:.role, parent:.parent, ip:.ip, uplink_type:.uplink_type, network:.network, device_type:.device_type, model:.model, location:.location}}; + def toLine(all): select(.parent != "" and .lat != "" and .lon != "") as $c | (all[] | select(.name == $c.parent and .lat != "" and .lon != "")) as $p | if $p then {type:"Feature", geometry:{type:"LineString", coordinates:[[($p.lon|tonumber), ($p.lat|tonumber)], [($c.lon|tonumber), ($c.lat|tonumber)]]}, properties:{type:"link", from:$p.name, to:$c.name}} else empty end; + . as $all | {type:"FeatureCollection", features: ([$all[] | toPoint] + [$all[] | toLine($all)])} +' "$TMP" > target-topology.geojson 2>/dev/null || echo '{"type":"FeatureCollection","features":[]}' > target-topology.geojson diff --git a/edge-agent/deploy.sh b/edge-agent/deploy.sh index 1deb4f6..8f7a4f0 100644 --- a/edge-agent/deploy.sh +++ b/edge-agent/deploy.sh @@ -1,156 +1,44 @@ #!/bin/bash - -# 分布式Prometheus边缘代理部署脚本 -# 适用于Linux系统 (玩客云等设备) - +# 边缘节点部署:vmagent + 内存/磁盘缓存 +# 用法: ./deploy.sh [--local] --local = 本机同机(中央与边缘同机) set -e -echo "=== 分布式Prometheus边缘代理部署脚本 ===" -echo "" +cd "$(dirname "${BASH_SOURCE[0]}")" -# 检查Docker是否安装 -if ! command -v docker &> /dev/null; then - echo "❌ Docker未安装,请先安装Docker" - exit 1 +# --local: 中央与边缘同机,使用 host.docker.internal +if [ "$1" = "--local" ]; then + [ ! -f .env ] && [ -f env.example ] && cp env.example .env + sed -i 's/^CENTRAL_SERVER_HOST=.*/CENTRAL_SERVER_HOST=host.docker.internal/' .env 2>/dev/null || \ + echo 'CENTRAL_SERVER_HOST=host.docker.internal' >> .env + grep -q '^CENTRAL_SERVER_PORT=' .env || echo 'CENTRAL_SERVER_PORT=8428' >> .env fi -# 检查Docker Compose (优先检查V2,然后检查V1) -DOCKER_COMPOSE_CMD="" -if docker compose version &> /dev/null; then - DOCKER_COMPOSE_CMD="docker compose" - echo "✅ 检测到 Docker Compose V2" -elif command -v docker-compose &> /dev/null; then - DOCKER_COMPOSE_CMD="docker-compose" - echo "✅ 检测到 Docker Compose V1" -else - echo "❌ Docker Compose未安装,请先安装Docker Compose" - exit 1 -fi +# Docker 环境 +DOCKER_CMD="docker compose" +docker compose version &>/dev/null || DOCKER_CMD="docker-compose" +command -v docker &>/dev/null || { echo "❌ 需要 Docker"; exit 1; } +$DOCKER_CMD version &>/dev/null || { echo "❌ 需要 Docker Compose"; exit 1; } +command -v jq &>/dev/null || { echo "❌ 需要 jq"; exit 1; } -echo "✅ Docker环境检查通过" +# 配置文件 +[ -f config/targets.csv ] && (cd config && ./update-configs.sh) || true +[ ! -f config/target-onvif.json ] && echo '[]' > config/target-onvif.json +[ ! -f config/target-ping.json ] && echo '[]' > config/target-ping.json +[ ! -f prometheus-edge/vmagent-scrape.yml.template ] && { echo "❌ 缺少 vmagent-scrape.yml.template"; exit 1; } + +# .env +[ ! -f .env ] && cp env.example .env + +# targets.csv(无则建最小示例) +[ ! -f config/targets.csv ] && \ + printf '%s\n' "type,ip,name,role,parent,uplink_type,network,device_type,model,location,username,password,onvif_port,lat,lon" \ + "ping,8.8.8.8,google_dns,dns,,,external,,,,,,,,," \ + "ping,1.1.1.1,cloudflare_dns,dns,,,external,,,,,,,,," > config/targets.csv + +# 启动 +$DOCKER_CMD down 2>/dev/null || true +$DOCKER_CMD up -d +sleep 5 +$DOCKER_CMD ps echo "" - -# 检查jq是否安装 -if ! command -v jq &> /dev/null; then - echo "❌ jq未安装,请先安装jq:" - echo " Ubuntu/Debian: sudo apt-get install jq" - echo " CentOS/RHEL: sudo yum install jq" - echo " Alpine: apk add jq" - exit 1 -fi - -# 检查并生成配置文件 -echo "🔄 检查并生成配置文件..." - -if [ -f "config/devices.csv" ]; then - echo "📱 从CSV生成ONVIF设备配置..." - cd config - chmod +x *.sh - ./update-configs.sh - cd .. -else - echo "⚠️ config/devices.csv 不存在,使用默认JSON配置" -fi - -if [ ! -f "config/onvif-targets.json" ]; then - echo "❌ 配置文件 config/onvif-targets.json 不存在" - exit 1 -fi - -if [ ! -f "config/ping-targets.json" ]; then - echo "❌ 配置文件 config/ping-targets.json 不存在" - exit 1 -fi - -if [ ! -f "prometheus-edge/prometheus.yml" ]; then - echo "❌ 配置文件 prometheus-edge/prometheus.yml 不存在" - exit 1 -fi - -echo "✅ 配置文件检查通过" -echo "" - -# 创建环境变量文件 -if [ ! -f ".env" ]; then - if [ -f "env.example" ]; then - cp env.example .env - echo "📝 已创建 .env 文件,请编辑其中的配置" - echo " 特别是 CENTRAL_SERVER_HOST 和 CENTRAL_SERVER_PORT" - echo "" - read -p "按回车键继续,或 Ctrl+C 取消..." - else - echo "❌ env.example 文件不存在" - exit 1 - fi -fi - -# 从 .env 生成 prometheus.yml(使 remote_write 指向中央服务器) -if [ -f ".env" ]; then - set -a - source .env - set +a - CENTRAL_SERVER_HOST=${CENTRAL_SERVER_HOST:-192.168.1.10} - CENTRAL_SERVER_PORT=${CENTRAL_SERVER_PORT:-8428} - if [ -f "prometheus-edge/prometheus.yml.template" ]; then - echo "📝 根据 .env 生成 prometheus.yml (中央: ${CENTRAL_SERVER_HOST}:${CENTRAL_SERVER_PORT})..." - export CENTRAL_SERVER_HOST CENTRAL_SERVER_PORT - envsubst '${CENTRAL_SERVER_HOST} ${CENTRAL_SERVER_PORT}' < prometheus-edge/prometheus.yml.template > prometheus-edge/prometheus.yml - echo "✅ prometheus.yml 已生成" - fi -fi - -# 创建数据目录 -mkdir -p prometheus-edge/data -echo "✅ 数据目录创建完成" -echo "" - -# 停止现有服务 -echo "🛑 停止现有服务..." -$DOCKER_COMPOSE_CMD down 2>/dev/null || true - -# 拉取最新镜像 -echo "📥 拉取Docker镜像..." -if ! $DOCKER_COMPOSE_CMD pull; then - echo "" - echo "⚠️ 镜像拉取失败,尝试继续启动(如果本地已有镜像)..." - echo "" -fi - -# 启动服务 -echo "🚀 启动服务..." -$DOCKER_COMPOSE_CMD up -d - -# 等待服务启动 -echo "⏳ 等待服务启动..." -sleep 10 - -# 检查服务状态 -echo "" -echo "📊 服务状态检查:" -$DOCKER_COMPOSE_CMD ps - -echo "" -echo "📋 服务日志:" -$DOCKER_COMPOSE_CMD logs --tail=20 - -echo "" -echo "✅ 部署完成!" -echo "" -echo "🔗 访问地址:" -echo " - Prometheus UI: http://localhost:9092" -echo " - 目标状态: http://localhost:9092/targets" -echo "" -echo "📝 管理命令:" -echo " - 查看日志: $DOCKER_COMPOSE_CMD logs -f" -echo " - 重启服务: $DOCKER_COMPOSE_CMD restart" -echo " - 停止服务: $DOCKER_COMPOSE_CMD down" -echo "" -echo "🔄 配置更新:" -echo " - 编辑CSV: nano config/devices.csv" -echo " - 生成JSON: cd config && ./update-configs.sh" -echo " - 热重载: 等待5分钟自动重载,或重启prometheus-edge" -echo "" -echo "⚠️ 请确保:" -echo " 1. 已正确配置 .env 文件中的服务器地址" -echo " 2. 已更新 config/devices.csv 中的设备信息" -echo " 3. 网络连接正常,可以访问ONVIF设备" \ No newline at end of file +echo "✅ 边缘已启动 http://localhost:9092 (Grafana 数据源选 VictoriaMetrics)" diff --git a/edge-agent/docker-compose.yml b/edge-agent/docker-compose.yml index e5b89da..2803038 100644 --- a/edge-agent/docker-compose.yml +++ b/edge-agent/docker-compose.yml @@ -1,70 +1,71 @@ -services: - # ========== 边缘必选 ========== - # 1. 边缘 Prometheus:抓取 + remote_write 推到中央 VictoriaMetrics - prometheus-edge: - image: prom/prometheus:latest - container_name: prometheus-edge - restart: unless-stopped - environment: - - CENTRAL_SERVER_HOST=${CENTRAL_SERVER_HOST:-192.168.1.10} - - CENTRAL_SERVER_PORT=${CENTRAL_SERVER_PORT:-8428} - volumes: - - prometheus-edge-data:/prometheus - - ./prometheus-edge/prometheus.yml:/etc/prometheus/prometheus.yml:ro - - ./config/onvif-targets.json:/etc/prometheus/onvif-targets.json:ro # 挂载静态设备列表 - - ./config/ping-targets.json:/etc/prometheus/ping-targets.json:ro # 挂载Ping目标列表 - mem_limit: "256m" - cpus: "2.0" - ports: - - "9092:9090" # 改为9092避免与中央服务器冲突 - # 本机同机部署时,容器内通过 host.docker.internal 访问宿主机中央服务 - extra_hosts: - - "host.docker.internal:host-gateway" - command: - - '--config.file=/etc/prometheus/prometheus.yml' - - '--storage.tsdb.retention.time=1h' - - '--web.enable-lifecycle' # 启用配置重载API - networks: - - monitoring_net - - # ========== 可选容器(按需启用)========== - # 2. ONVIF Exporter(可选,使用本项目自建) - # 启用:docker compose --profile onvif up -d --build(会构建 edge-agent/onvif-exporter 并启动) - # 配置文件:config/onvif-targets.json(与 targets.csv 中 onvif 行一致,由 update-configs.sh 生成) - onvif-exporter: - profiles: - - onvif - image: onvif-exporter:local - build: - context: ./onvif-exporter - dockerfile: Dockerfile - container_name: onvif-exporter - restart: unless-stopped - environment: - - EXPORTER_PORT=9600 - - TARGETS_FILE=/config/targets.json - volumes: - - ./config/onvif-targets.json:/config/targets.json:ro - mem_limit: "128m" - cpus: "1.5" - networks: - - monitoring_net - - # 3. Blackbox Exporter(必选:网络 Ping 探测) - blackbox-exporter: - image: prom/blackbox-exporter:latest - container_name: blackbox-exporter - restart: unless-stopped - volumes: - - ./blackbox/config.yml:/etc/blackbox_exporter/config.yml:ro - mem_limit: "64m" - cpus: "0.5" - networks: - - monitoring_net - -networks: - monitoring_net: - driver: bridge - -volumes: - prometheus-edge-data: +# 边缘节点:统一编排(vmagent = 短时内存缓存 + 长时磁盘缓存 + 冗余重试) +# 用法: docker compose up -d + +services: + # ========== 边缘必选 ========== + # vmagent:抓取 + remote_write,内置短时内存缓存、长时磁盘缓存、失败重试 + prometheus-edge: + image: victoriametrics/vmagent:latest + container_name: prometheus-edge + restart: unless-stopped + environment: + - CENTRAL_SERVER_HOST=${CENTRAL_SERVER_HOST:-192.168.1.10} + - CENTRAL_SERVER_PORT=${CENTRAL_SERVER_PORT:-8428} + volumes: + - vmagent-cache-data:/cache + - ./prometheus-edge/vmagent-scrape.yml.template:/etc/vmagent/scrape.yml:ro + - ./config/target-onvif.json:/etc/prometheus/target-onvif.json:ro + - ./config/target-ping.json:/etc/prometheus/target-ping.json:ro + mem_limit: "256m" + cpus: "2.0" + ports: + - "9092:8429" + extra_hosts: + - "host.docker.internal:host-gateway" + command: + - -promscrape.config=/etc/vmagent/scrape.yml + - -remoteWrite.url=http://${CENTRAL_SERVER_HOST}:${CENTRAL_SERVER_PORT}/api/v1/write + - -remoteWrite.tmpDataPath=/cache/remotewrite + - -remoteWrite.maxDiskUsagePerURL=512MB + - -memory.allowedPercent=80 + - -httpListenAddr=:8429 + networks: + - monitoring_net + + # ========== 可选容器(按需启用)========== + onvif-exporter: + profiles: + - onvif + image: onvif-exporter:local + build: + context: ./onvif-exporter + dockerfile: Dockerfile + container_name: onvif-exporter + restart: unless-stopped + environment: + - EXPORTER_PORT=9600 + - TARGETS_FILE=/config/targets.json + volumes: + - ./config/target-onvif.json:/config/targets.json:ro + mem_limit: "128m" + cpus: "1.5" + networks: + - monitoring_net + + blackbox-exporter: + image: prom/blackbox-exporter:latest + container_name: blackbox-exporter + restart: unless-stopped + volumes: + - ./blackbox/config.yml:/etc/blackbox_exporter/config.yml:ro + mem_limit: "64m" + cpus: "0.5" + networks: + - monitoring_net + +networks: + monitoring_net: + driver: bridge + +volumes: + vmagent-cache-data: diff --git a/edge-agent/env.example b/edge-agent/env.example index 923bb02..8ee05f2 100644 --- a/edge-agent/env.example +++ b/edge-agent/env.example @@ -19,7 +19,9 @@ EDGE_NODE_ID=workernode_1 # 格式: http://域名或IP:端口/api/v1/write # 默认端口: 8428 (VictoriaMetrics) -# 注意:ONVIF设备密码现在在 config/devices.csv 中为每个设备单独配置 +# 边缘缓存:docker-compose.yaml 使用 vmagent,含内存+磁盘缓存,详见 doc/EDGE_CACHE.md + +# 注意:ONVIF 设备在 config/targets.csv 中配置(每行 ip、username、password 等) # ONVIF Exporter 镜像(仅在使用 --profile onvif 时需要) # 公共 registry 无现成镜像,需自建或使用第三方镜像,参见 doc/ONVIF_ALTERNATIVES.md diff --git a/edge-agent/onvif-exporter/README.md b/edge-agent/onvif-exporter/README.md index db71a7c..e25652e 100644 --- a/edge-agent/onvif-exporter/README.md +++ b/edge-agent/onvif-exporter/README.md @@ -11,7 +11,7 @@ ## 配置 -- 从 **config/onvif-targets.json** 读取设备列表(与 `targets.csv` 中 onvif 行一致,由 `config/update-configs.sh` 生成)。 +- 从 **config/target-onvif.json** 读取设备列表(与 `targets.csv` 中 onvif 行一致,由 `config/update-configs.sh` 生成)。 - 支持字段:`device_type`, `model`, `location`, `username`, `password`, `onvif_port`, `lat`, `lon` - 环境变量:`TARGETS_FILE`(默认 `/config/targets.json`)、`EXPORTER_PORT`(默认 9600)。 diff --git a/edge-agent/prometheus-edge/prometheus.yml.template b/edge-agent/prometheus-edge/prometheus.yml.template index e9e2942..49a33d4 100644 --- a/edge-agent/prometheus-edge/prometheus.yml.template +++ b/edge-agent/prometheus-edge/prometheus.yml.template @@ -8,8 +8,8 @@ remote_write: - url: http://${CENTRAL_SERVER_HOST}:${CENTRAL_SERVER_PORT}/api/v1/write queue_config: max_samples_per_send: 5000 - capacity: 5000 - max_shards: 5 + capacity: 10000 + max_shards: 8 scrape_configs: - job_name: 'onvif-devices' diff --git a/edge-agent/prometheus-edge/vmagent-scrape.yml.template b/edge-agent/prometheus-edge/vmagent-scrape.yml.template new file mode 100644 index 0000000..ef4e12b --- /dev/null +++ b/edge-agent/prometheus-edge/vmagent-scrape.yml.template @@ -0,0 +1,40 @@ +# vmagent 抓取配置(与 prometheus.yml 兼容,自监控端口改为 8429) +# 如需修改 region,可编辑此文件或通过 deploy.sh 从 vmagent-scrape.yml.template 生成 +global: + scrape_interval: 120s + evaluation_interval: 120s + external_labels: + region: workernode_1 + +scrape_configs: + - job_name: 'onvif-devices' + scrape_interval: 120s + file_sd_configs: + - files: ['/etc/prometheus/target-onvif.json'] + refresh_interval: 5m + metrics_path: /metrics + static_configs: + - targets: ['onvif-exporter:9600'] + + - job_name: 'network-ping' + scrape_interval: 300s + file_sd_configs: + - files: ['/etc/prometheus/target-ping.json'] + refresh_interval: 5m + metrics_path: /probe + params: + module: [icmp] + static_configs: + - targets: ['blackbox-exporter:9115'] + relabel_configs: + - source_labels: [__address__] + target_label: __param_target + - source_labels: [__param_target] + target_label: instance + - target_label: __address__ + replacement: blackbox-exporter:9115 + + - job_name: 'vmagent-edge' + scrape_interval: 60s + static_configs: + - targets: ['localhost:8429'] diff --git a/edge-agent/quick-setup.sh b/edge-agent/quick-setup.sh deleted file mode 100644 index 09aaa5e..0000000 --- a/edge-agent/quick-setup.sh +++ /dev/null @@ -1,104 +0,0 @@ -#!/bin/bash - -# 边缘节点快速配置脚本 -# 用于在本机快速设置边缘节点 - -set -e - -echo "=== 边缘节点快速配置脚本 ===" -echo "" - -# 获取本机IP -LOCAL_IP=$(hostname -I | awk '{print $1}') - -echo "📋 检测到本机IP: $LOCAL_IP" -echo "" - -# 1. 创建 .env 文件 -if [ ! -f ".env" ]; then - echo "📝 创建 .env 配置文件..." - cat > .env << EOF -# 中央服务器地址(本机) -CENTRAL_SERVER_HOST=${LOCAL_IP} -CENTRAL_SERVER_PORT=8428 - -# 边缘节点标识 -EDGE_NODE_ID=workernode_1 -EOF - echo "✅ .env 文件已创建" -else - echo "⚠️ .env 文件已存在,跳过创建" - echo " 如需修改,请编辑 .env 文件" -fi -echo "" - -# 2. 配置统一监控目标(最小化测试配置) -echo "📝 配置统一监控目标 targets.csv..." -cat > config/targets.csv << 'EOF' -# 统一监控目标配置文件 -# 格式: type,ip,device,group,network,device_type,model,location,username,password,onvif_port -# type: onvif 或 ping -type,ip,device,group,network,device_type,model,location,username,password,onvif_port -ping,8.8.8.8,google_dns,external,external,,,,,, -ping,1.1.1.1,cloudflare_dns,external,external,,,,,, -# ONVIF 设备示例(取消注释并填写实际信息) -# onvif,192.168.1.100,,,front_door,camera,HIKVISION_DS-2CD2342WD-I,front_door,admin,password1,80 -EOF -echo "✅ 统一监控目标已配置(使用公共DNS进行测试)" -echo "" - -# 3. 生成配置文件 -echo "🔄 生成配置文件..." -cd config -chmod +x *.sh 2>/dev/null || true -if [ -f "update-configs.sh" ]; then - ./update-configs.sh - echo "✅ 配置文件已生成" -else - echo "⚠️ update-configs.sh 不存在,跳过" -fi -cd .. -echo "" - -# 4. 检查配置文件 -echo "🔍 检查配置文件..." -if [ ! -f "config/onvif-targets.json" ]; then - echo "📝 创建空的 ONVIF 配置文件..." - echo "[]" > config/onvif-targets.json -fi - -if [ ! -f "config/ping-targets.json" ]; then - echo "📝 创建空的 Ping 配置文件..." - echo "[]" > config/ping-targets.json -fi - -echo "✅ 配置文件检查通过" -echo "" - -# 6. 显示配置摘要 -echo "📊 配置摘要:" -echo " - 中央服务器: ${LOCAL_IP}:8428" -echo " - 边缘节点ID: workernode_1" -echo " - 监控目标: 已配置(统一 targets.csv)" -echo " * Ping 目标: Google DNS, Cloudflare DNS" -echo " * ONVIF 设备: 无(用于测试,可在 targets.csv 中添加)" -echo "" - -# 7. 询问是否立即部署 -read -p "是否立即部署边缘节点?(y/N): " -n 1 -r -echo -if [[ $REPLY =~ ^[Yy]$ ]]; then - echo "" - echo "🚀 开始部署..." - bash deploy.sh -else - echo "" - echo "✅ 配置完成!" - echo "" - echo "📝 下一步:" - echo " 1. 检查 .env 文件配置" - echo " 2. 编辑 config/targets.csv 添加监控目标(ping 或 onvif)" - echo " 3. 运行: cd config && ./update-configs.sh 生成JSON配置" - echo " 4. 运行: bash deploy.sh" - echo "" -fi diff --git a/edge-agent/run-edge-local.sh b/edge-agent/run-edge-local.sh deleted file mode 100755 index 74b0476..0000000 --- a/edge-agent/run-edge-local.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/bash -# 本机同机部署边缘:中央与边缘在同一台机器时,一键配置并启动边缘 -# 用法: ./run-edge-local.sh - -set -e - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -cd "$SCRIPT_DIR" - -echo "=== 本机同机部署边缘(中央与边缘在同一台机器)===" -echo "" - -# 确保有 .env -if [ ! -f ".env" ]; then - [ -f "env.example" ] && cp env.example .env || { echo "❌ 缺少 env.example"; exit 1; } -fi - -# 指向本机中央 VictoriaMetrics(容器内用 host.docker.internal 访问宿主机) -if ! grep -q 'CENTRAL_SERVER_HOST=host.docker.internal' .env 2>/dev/null; then - echo "📝 设置中央服务器为本机 (host.docker.internal:8428)..." - sed -i 's/^CENTRAL_SERVER_HOST=.*/CENTRAL_SERVER_HOST=host.docker.internal/' .env 2>/dev/null || \ - echo 'CENTRAL_SERVER_HOST=host.docker.internal' >> .env - grep -q '^CENTRAL_SERVER_PORT=' .env || echo 'CENTRAL_SERVER_PORT=8428' >> .env - sed -i 's/^CENTRAL_SERVER_PORT=.*/CENTRAL_SERVER_PORT=8428/' .env 2>/dev/null || true - echo "✅ 已写入 CENTRAL_SERVER_HOST=host.docker.internal, CENTRAL_SERVER_PORT=8428" -fi - -echo "" -echo "请确保中央服务器已在本机运行(central-server 已 deploy),且 VictoriaMetrics 监听 8428。" -echo "按回车继续启动边缘,或 Ctrl+C 取消..." -read -r - -bash deploy.sh - -echo "" -echo "💡 本机部署完成后:" -echo " - 边缘 Prometheus UI: http://localhost:9092" -echo " - 在 Grafana 中选择数据源「VictoriaMetrics」可查看边缘上报的数据" diff --git a/topology-editor/.dockerignore b/topology-editor/.dockerignore deleted file mode 100644 index 54e38e4..0000000 --- a/topology-editor/.dockerignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -*.log -.env* diff --git a/topology-editor/Dockerfile b/topology-editor/Dockerfile deleted file mode 100644 index 3e2acab..0000000 --- a/topology-editor/Dockerfile +++ /dev/null @@ -1,18 +0,0 @@ -# 拓扑标注助手:H5 采集 GPS + 编辑 targets.csv,与 edge-agent 同机用 docker-compose 运行 -# 构建上下文为项目根目录:docker compose -f edge-agent/docker-compose.yml build topology-editor - -FROM node:20-alpine - -WORKDIR /app - -COPY topology-editor/package.json ./ -RUN npm install --production - -COPY topology-editor/server.js ./ -COPY topology-editor/public ./public - -ENV PORT=4080 -EXPOSE 4080 - -# 运行时由 docker-compose 挂载 edge-agent/config 到 /config,并设置 CONFIG_DIR=/config -CMD ["node", "server.js"] diff --git a/topology-editor/package-lock.json b/topology-editor/package-lock.json deleted file mode 100644 index d76e74b..0000000 --- a/topology-editor/package-lock.json +++ /dev/null @@ -1,827 +0,0 @@ -{ - "name": "topology-editor", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "topology-editor", - "version": "0.1.0", - "dependencies": { - "express": "^4.19.2" - } - }, - "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.4", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", - "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", - "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.14.0", - "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/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.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "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.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", - "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "~1.20.3", - "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.14.0", - "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.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "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-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.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "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.14.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", - "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "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.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "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.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "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/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/topology-editor/package.json b/topology-editor/package.json deleted file mode 100644 index c8308c9..0000000 --- a/topology-editor/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "topology-editor", - "version": "0.1.0", - "description": "拓扑标注助手:上传/下载 targets.csv,H5 采集 GPS 与地图校验。可与 central-server 或 edge-agent 同机运行。", - "main": "server.js", - "scripts": { - "start": "node server.js" - }, - "dependencies": { - "express": "^4.19.2", - "multer": "^1.4.5-lts.1" - } -} diff --git a/topology-editor/public/index.html b/topology-editor/public/index.html deleted file mode 100644 index 20ced86..0000000 --- a/topology-editor/public/index.html +++ /dev/null @@ -1,509 +0,0 @@ - - - - - 拓扑标注助手(GPS → targets.csv) - - - - -

拓扑标注助手

-

- 与 central 同机运行时:先上传本机 targets.csv,在页面上选设备、补 GPS 或新建后下载 CSV 部署到边缘。与 edge-agent 同机挂载 config 时可直接读写 config/targets.csv。 -

- -
- -
- - - 下载 targets.csv -
-

-
- -
- - -
- -

- 若是新建设备:先选上面的「(新建设备)」,再填写下列字段。 -

- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- - - - -
- -
-

地图校验

-
- - -
-
- - -
-
-

点击地图可修正坐标;有经纬度后会自动打点并居中。

-
- - diff --git a/topology-editor/server.js b/topology-editor/server.js deleted file mode 100644 index 8c0f9ed..0000000 --- a/topology-editor/server.js +++ /dev/null @@ -1,267 +0,0 @@ -const express = require('express'); -const fs = require('fs'); -const path = require('path'); -const http = require('http'); -const { exec } = require('child_process'); -const multer = require('multer'); - -const app = express(); -const PORT = process.env.PORT || 4080; -const TILE_CACHE_URL = (process.env.TILE_CACHE_URL || '').trim(); - -const ROOT_DIR = path.join(__dirname, '..'); -const CONFIG_DIR = process.env.CONFIG_DIR || null; -const TARGETS_CSV = CONFIG_DIR ? path.join(CONFIG_DIR, 'targets.csv') : null; - -const upload = multer({ storage: multer.memoryStorage() }); - -// 内存模式(与 central-server 一起跑、无挂载时):存一份 targets 的解析结果 -const DEFAULT_HEADER = ['type', 'ip', 'name', 'role', 'parent', 'uplink_type', 'network', 'device_type', 'model', 'location', 'username', 'password', 'onvif_port', 'lat', 'lon']; -let memoryStore = { - comments: ['# 统一监控 + 拓扑目标配置文件\n', '# 格式: type,ip,name,role,parent,uplink_type,...\n'], - header: DEFAULT_HEADER, - rows: [], -}; - -function isFileMode() { - return CONFIG_DIR && TARGETS_CSV && fs.existsSync(TARGETS_CSV); -} - -app.use(express.json()); -app.use(express.static(path.join(__dirname, 'public'))); - -function parseCsv(text) { - const lines = text.split(/\r?\n/); - const comments = []; - let headerLineIndex = -1; - - for (let i = 0; i < lines.length; i++) { - const line = lines[i].trim(); - if (!line || line.startsWith('#')) { - comments.push(lines[i]); - continue; - } - headerLineIndex = i; - break; - } - - if (headerLineIndex === -1) { - return { comments: lines, header: DEFAULT_HEADER, rows: [] }; - } - - const header = lines[headerLineIndex].split(',').map(s => s.trim()); - const rows = []; - - for (let i = headerLineIndex + 1; i < lines.length; i++) { - const raw = lines[i]; - if (!raw || raw.trim().startsWith('#')) continue; - const parts = raw.split(','); - const obj = {}; - header.forEach((h, idx) => { - obj[h] = (parts[idx] || '').trim(); - }); - if (Object.values(obj).every(v => v === '')) continue; - rows.push(obj); - } - - return { comments, header, rows }; -} - -function stringifyCsv(comments, header, rows) { - const lines = []; - if (comments && comments.length) { - lines.push(...comments); - } - if (header && header.length) { - lines.push(header.join(',')); - } - for (const row of rows) { - const line = header.map(h => (row[h] || '').replace(/,/g, ' ')).join(','); - lines.push(line); - } - return lines.join('\n') + '\n'; -} - -function loadTargets() { - if (isFileMode()) { - const text = fs.readFileSync(TARGETS_CSV, 'utf8'); - return parseCsv(text); - } - return { ...memoryStore, rows: [...memoryStore.rows] }; -} - -function saveTargets(parsed) { - if (isFileMode()) { - const text = stringifyCsv(parsed.comments, parsed.header, parsed.rows); - fs.writeFileSync(TARGETS_CSV, text, 'utf8'); - return; - } - memoryStore = { - comments: parsed.comments || memoryStore.comments, - header: parsed.header || memoryStore.header, - rows: parsed.rows || memoryStore.rows, - }; -} - -function rebuildConfigs() { - if (!CONFIG_DIR) return; - const cmd = `cd "${CONFIG_DIR}" && ./update-configs.sh && ./csv-to-topology-geojson.sh targets.csv topology.geojson`; - exec(cmd, (err, stdout, stderr) => { - if (err) { - console.error('重建配置失败:', err.message); - if (stderr) console.error(stderr); - return; - } - if (stdout) console.log(stdout); - }); -} - -app.get('/api/targets', (req, res) => { - try { - const parsed = loadTargets(); - res.json(parsed.rows); - } catch (e) { - console.error(e); - res.status(500).json({ error: '读取 targets 失败' }); - } -}); - -app.get('/api/mode', (req, res) => { - res.json({ mode: isFileMode() ? 'file' : 'memory' }); -}); - -app.get('/api/map-config', (req, res) => { - res.json({ cacheBaseUrl: TILE_CACHE_URL ? '/tiles' : '' }); -}); - -function proxyToTileCache(pathname, res) { - const url = TILE_CACHE_URL + pathname; - const u = new URL(url); - const client = u.protocol === 'https:' ? require('https') : require('http'); - client.get(url, (proxyRes) => { - res.status(proxyRes.statusCode || 200); - if (proxyRes.headers['content-type']) res.setHeader('Content-Type', proxyRes.headers['content-type']); - if (proxyRes.headers['cache-control']) res.setHeader('Cache-Control', proxyRes.headers['cache-control']); - proxyRes.pipe(res); - }).on('error', (err) => { - res.status(502).json({ error: 'Tile cache unreachable: ' + err.message }); - }); -} - -app.get('/tiles/vec/:z/:x/:y', (req, res) => { - if (!TILE_CACHE_URL) return res.status(503).send('Tile cache not configured'); - proxyToTileCache(`/vec/${req.params.z}/${req.params.x}/${req.params.y}`, res); -}); - -app.get('/tiles/cva/:z/:x/:y', (req, res) => { - if (!TILE_CACHE_URL) return res.status(503).send('Tile cache not configured'); - proxyToTileCache(`/cva/${req.params.z}/${req.params.x}/${req.params.y}`, res); -}); - -app.get('/api/download', (req, res) => { - try { - const parsed = loadTargets(); - const text = stringifyCsv(parsed.comments, parsed.header, parsed.rows); - res.setHeader('Content-Type', 'text/csv; charset=utf-8'); - res.setHeader('Content-Disposition', 'attachment; filename=targets.csv'); - res.send(text); - } catch (e) { - console.error(e); - res.status(500).json({ error: '生成下载失败' }); - } -}); - -app.post('/api/upload', upload.single('file'), (req, res) => { - try { - if (!req.file || !req.file.buffer) { - return res.status(400).json({ error: '请选择 CSV 文件上传' }); - } - const text = req.file.buffer.toString('utf8'); - const parsed = parseCsv(text); - if (!isFileMode()) { - memoryStore = parsed; - } else { - saveTargets(parsed); - } - res.json({ ok: true, rows: parsed.rows.length }); - } catch (e) { - console.error(e); - res.status(500).json({ error: '解析 CSV 失败' }); - } -}); - -app.post('/api/targets', (req, res) => { - try { - const { - type = '', - ip = '', - name = '', - role = '', - parent = '', - uplink_type = '', - network = '', - device_type = '', - model = '', - location = '', - username = '', - password = '', - onvif_port = '', - lat = '', - lon = '', - } = req.body || {}; - - if (!type || !name) { - return res.status(400).json({ error: 'type 与 name 为必填字段' }); - } - - const parsed = loadTargets(); - const { header, rows } = parsed; - - const idx = rows.findIndex((r) => r.name === name); - const row = {}; - header.forEach((h) => { - row[h] = ''; - }); - - Object.assign(row, { - type, - ip, - name, - role, - parent, - uplink_type, - network, - device_type, - model, - location, - username, - password, - onvif_port, - lat, - lon, - }); - - if (idx >= 0) { - rows[idx] = row; - } else { - rows.push(row); - } - - saveTargets({ ...parsed, rows }); - rebuildConfigs(); - - res.json({ ok: true, row }); - } catch (e) { - console.error(e); - res.status(500).json({ error: '写入失败' }); - } -}); - -app.listen(PORT, '0.0.0.0', () => { - console.log(`Topology editor listening on http://0.0.0.0:${PORT}`); - if (CONFIG_DIR) { - console.log(`CONFIG_DIR=${CONFIG_DIR} (file mode)`); - } else { - console.log('Running in memory mode: use Upload CSV / Download CSV'); - } -});