feat: 天地图瓦片缓存(tile-cache)、拓扑标注助手与文档更新
- tile-cache: 瓦片缓存服务(vec/cva)、启动探针、详细日志、上游超时可配置(UPSTREAM_TIMEOUT_MS) - central: docker-compose 集成 tile-cache,env.example 增加 TILE_CACHE_* / TIANDITU_TK - topology-editor: 天地图/缓存加载、GPS 安全来源错误提示、TIANDITU 文档(403/白名单、localhost 测试说明) - doc: README 部署步骤与 GPS 安全来源说明,TIANDITU_CONFIG 完善 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -7,3 +7,7 @@ edge-agent/prometheus-edge/data/
|
||||
# 生成文件(由 CSV 脚本生成)
|
||||
edge-agent/config/*.json
|
||||
|
||||
# Node 依赖
|
||||
**/node_modules/
|
||||
|
||||
|
||||
|
||||
39
README.md
39
README.md
@@ -20,8 +20,9 @@
|
||||
|
||||
## 项目结构
|
||||
|
||||
- **central-server/** — 中央:Prometheus、Grafana、VictoriaMetrics、Alertmanager
|
||||
- **central-server/** — 中央:Prometheus、Grafana、VictoriaMetrics、Alertmanager、拓扑标注助手
|
||||
- **edge-agent/** — 边缘:Prometheus Edge、ONVIF Exporter、Blackbox Exporter
|
||||
- **topology-editor/** — 拓扑标注助手(与 central 同机运行):上传/下载 targets.csv,H5 采集 GPS、天地图校验
|
||||
|
||||
---
|
||||
|
||||
@@ -34,6 +35,7 @@
|
||||
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`
|
||||
@@ -41,7 +43,7 @@
|
||||
|
||||
边缘数据在 Grafana 中需选择数据源 **「VictoriaMetrics」** 才能看到;中央自身指标在数据源「Prometheus」。
|
||||
|
||||
3. **第三步(可选)**:多用户见 `doc/USER_MANAGEMENT.md`,告警见 `doc/ALERTMANAGER_CONFIG.md`。
|
||||
3. **第三步(可选)**:多用户见 `doc/USER_MANAGEMENT.md`,告警见 `doc/ALERTING.md`。
|
||||
|
||||
---
|
||||
|
||||
@@ -51,7 +53,8 @@
|
||||
- Prometheus: http://localhost:9091
|
||||
- VictoriaMetrics: http://localhost:8428
|
||||
- Alertmanager: http://localhost:9093
|
||||
边缘 Prometheus(端口 9092):http://localhost:9092
|
||||
- **拓扑标注助手**: http://localhost:4080
|
||||
- 边缘 Prometheus(端口 9092):http://localhost:9092
|
||||
|
||||
---
|
||||
|
||||
@@ -61,6 +64,36 @@ 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)**
|
||||
|
||||
@@ -62,6 +62,8 @@
|
||||
- 生产环境必须修改 `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)。
|
||||
|
||||
### Prometheus 配置
|
||||
|
||||
| 参数 | 默认值 | 说明 |
|
||||
@@ -102,6 +104,17 @@
|
||||
- 保留时间越长,占用存储空间越大
|
||||
- 建议根据实际存储容量和需求设置
|
||||
|
||||
### 天地图 / 瓦片缓存配置
|
||||
|
||||
| 参数 | 默认值 | 说明 |
|
||||
|------|--------|------|
|
||||
| `TIANDITU_TK` | (空) | **天地图密钥**,配置到 `.env` 中。启用后 tile-cache、拓扑助手「使用服务器缓存」及 Grafana Geomap 使用天地图底图时均使用此密钥,无需在浏览器或 Grafana 中填写。在 [天地图开放平台](https://console.tianditu.gov.cn/) 申请。 |
|
||||
| `TILE_CACHE_PORT` | 4090 | 瓦片缓存服务端口 |
|
||||
| `TILE_CACHE_DATA_DIR` | ./data/tile-cache | 瓦片缓存数据目录 |
|
||||
| `TILE_CACHE_TTL_DAYS` | 7 | 缓存老化时间(天),超过后该瓦片下次请求时重新向天地图拉取;可改为 15 等 |
|
||||
|
||||
**说明**:以上参数均在 **central-server/.env** 中配置;部署时由 docker-compose 传入 tile-cache 容器。
|
||||
|
||||
### 数据存储路径配置
|
||||
|
||||
| 参数 | 默认值 | 说明 |
|
||||
@@ -368,5 +381,4 @@ cp env.example .env
|
||||
|
||||
- [中央服务器架构说明](../doc/ARCHITECTURE.md)
|
||||
- [部署指南](../doc/DEPLOYMENT_GUIDE.md)
|
||||
- [告警规则说明](../doc/ALERT_RULES_EXPLANATION.md)
|
||||
- [Alertmanager 配置说明](../doc/ALERTMANAGER_CONFIG.md)
|
||||
- [告警与通知](../doc/ALERTING.md)
|
||||
|
||||
@@ -82,6 +82,41 @@ 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:
|
||||
context: ..
|
||||
dockerfile: tile-cache/Dockerfile
|
||||
image: tile-cache:local
|
||||
container_name: tile-cache
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- PORT=4090
|
||||
- TIANDITU_TK=${TIANDITU_TK:-}
|
||||
- CACHE_DIR=/cache
|
||||
- CACHE_TTL_DAYS=${TILE_CACHE_TTL_DAYS:-7}
|
||||
- UPSTREAM_TIMEOUT_MS=${TILE_CACHE_UPSTREAM_TIMEOUT_MS:-15000}
|
||||
volumes:
|
||||
- ${TILE_CACHE_DATA_DIR:-./data/tile-cache}:/cache
|
||||
ports:
|
||||
- "${TILE_CACHE_PORT:-4090}:4090"
|
||||
mem_limit: "128m"
|
||||
|
||||
|
||||
# 定义网络配置(默认使用已存在的 traefik 网络)
|
||||
networks:
|
||||
|
||||
@@ -78,6 +78,16 @@ GRAFANA_DATA_DIR=./data/grafana-data
|
||||
# VictoriaMetrics 数据目录
|
||||
VICTORIAMETRICS_DATA_DIR=./data/victoria-metrics-data
|
||||
|
||||
# 天地图瓦片缓存(密钥与缓存参数均在 .env 中配置)
|
||||
# 在 .env 中设置 TIANDITU_TK 后,tile-cache 与拓扑助手「使用服务器缓存」、Grafana Geomap 方可使用天地图
|
||||
# TIANDITU_TK=您的天地图密钥
|
||||
# TILE_CACHE_PORT=4090
|
||||
# TILE_CACHE_DATA_DIR=./data/tile-cache
|
||||
# 缓存老化时间(天),超过后该瓦片下次请求时会重新向天地图拉取;默认 7,可改为 15 等
|
||||
# TILE_CACHE_TTL_DAYS=7
|
||||
# 向天地图请求单瓦片超时(毫秒),默认 15000;网络或上游慢时可适当调大
|
||||
# TILE_CACHE_UPSTREAM_TIMEOUT_MS=15000
|
||||
|
||||
# ============================================
|
||||
# Traefik 反向代理配置
|
||||
# ============================================
|
||||
|
||||
52
doc/ALERTING.md
Normal file
52
doc/ALERTING.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# 告警与通知
|
||||
|
||||
本文说明告警规则、如何激活,以及 Alertmanager 的配置与通知渠道。
|
||||
|
||||
---
|
||||
|
||||
## 告警规则(alert_rules.yml)
|
||||
|
||||
中央已内置 `central-server/alert_rules.yml`,主要包含:
|
||||
|
||||
| 规则 | 条件 | 说明 |
|
||||
|------|------|------|
|
||||
| ONVIFDeviceDown | `up{job="onvif-devices"} == 0` 持续 1m | ONVIF 设备离线 |
|
||||
| NetworkDeviceDown | `probe_success{job="network-ping"} == 0` 持续 2m | 网络设备 Ping 不通 |
|
||||
| HighNetworkLatency | `probe_duration_seconds{job="network-ping"} > 1` 持续 5m | Ping 延迟过高 |
|
||||
|
||||
**为何显示 Inactive**:规则依赖边缘推送的指标。需先部署边缘、配置 Ping/ONVIF 目标,数据经 remote_write 到 VictoriaMetrics 后,规则才会评估;无数据时保持 inactive。
|
||||
|
||||
**激活步骤**:完成 [README.md#第二步](README.md) 边缘部署 → 在 Grafana 选 VictoriaMetrics 数据源确认有 `probe_success{job="network-ping"}` 等 → Prometheus 会从 VictoriaMetrics 取数并评估规则。
|
||||
|
||||
---
|
||||
|
||||
## Alertmanager 配置(alertmanager.yml)
|
||||
|
||||
路径:`central-server/alertmanager/alertmanager.yml`。
|
||||
|
||||
- **route**:分组(group_by)、等待时间(group_wait)、重复间隔(repeat_interval)、默认接收器(receiver)。
|
||||
- **receivers**:当前示例为 webhook `http://127.0.0.1:5001/`。
|
||||
**注意**:容器内 127.0.0.1 指向自身,若 webhook 在宿主机,应改为 `http://host.docker.internal:5001/` 或宿主机 IP。
|
||||
- **inhibit_rules**:critical 抑制同实例的 warning,减少告警风暴。
|
||||
|
||||
**常用接收器类型**:`email_configs`、`wechat_configs`、`dingtalk_configs`、`webhook_configs`。按需替换为邮件、企业微信、钉钉或自建 webhook。
|
||||
|
||||
**验证**:`docker exec alertmanager amtool check-config /etc/alertmanager/alertmanager.yml`;Web UI:http://localhost:9093。
|
||||
|
||||
---
|
||||
|
||||
## 自定义告警规则
|
||||
|
||||
在 `alert_rules.yml` 中追加或修改规则,例如:
|
||||
|
||||
```yaml
|
||||
- alert: ExampleAlert
|
||||
expr: your_metric > threshold
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: "示例告警"
|
||||
```
|
||||
|
||||
修改后若 Prometheus 启用了 `--web.enable-lifecycle`,可 `curl -X POST http://localhost:9091/-/reload` 重载。
|
||||
@@ -1,300 +0,0 @@
|
||||
# Alertmanager 配置说明
|
||||
|
||||
## 配置文件概述
|
||||
|
||||
`alertmanager.yml` 是 Alertmanager 的核心配置文件,用于定义告警路由、通知方式和告警抑制规则。
|
||||
|
||||
## 配置详解
|
||||
|
||||
### 1. Global(全局配置)
|
||||
|
||||
```yaml
|
||||
global:
|
||||
smtp_smarthost: 'localhost:587'
|
||||
smtp_from: 'alertmanager@example.com'
|
||||
```
|
||||
|
||||
**作用**:定义全局的 SMTP 邮件服务器配置
|
||||
|
||||
**字段说明**:
|
||||
- `smtp_smarthost`: SMTP 服务器地址和端口
|
||||
- 当前配置:`localhost:587`(本地邮件服务器)
|
||||
- 如果使用外部邮件服务,例如:
|
||||
- Gmail: `smtp.gmail.com:587`
|
||||
- 163邮箱: `smtp.163.com:465`
|
||||
- 企业邮箱: `smtp.company.com:587`
|
||||
- `smtp_from`: 发送告警邮件的发件人地址
|
||||
- 当前配置:`alertmanager@example.com`(示例地址,需要修改)
|
||||
|
||||
**注意**:当前配置使用的是 webhook,所以 SMTP 配置暂时未使用。
|
||||
|
||||
---
|
||||
|
||||
### 2. Route(路由配置)
|
||||
|
||||
```yaml
|
||||
route:
|
||||
group_by: ['alertname']
|
||||
group_wait: 10s
|
||||
group_interval: 10s
|
||||
repeat_interval: 1h
|
||||
receiver: 'web.hook'
|
||||
```
|
||||
|
||||
**作用**:定义告警的路由规则,决定告警如何分组和发送
|
||||
|
||||
**字段说明**:
|
||||
|
||||
| 字段 | 说明 | 当前值 | 含义 |
|
||||
|------|------|--------|------|
|
||||
| `group_by` | 告警分组字段 | `['alertname']` | 按告警名称分组,相同名称的告警会被合并 |
|
||||
| `group_wait` | 分组等待时间 | `10s` | 收到第一个告警后,等待10秒再发送(用于合并同类告警) |
|
||||
| `group_interval` | 分组间隔 | `10s` | 同一分组内新告警的发送间隔 |
|
||||
| `repeat_interval` | 重复间隔 | `1h` | 如果告警持续存在,每1小时重复发送一次通知 |
|
||||
| `receiver` | 默认接收器 | `'web.hook'` | 所有告警默认发送到 `web.hook` 接收器 |
|
||||
|
||||
**示例场景**:
|
||||
- 如果 3 个设备同时离线,会在 10 秒内合并为一条告警发送
|
||||
- 如果告警持续存在,每小时会重复通知一次
|
||||
|
||||
---
|
||||
|
||||
### 3. Receivers(接收器配置)
|
||||
|
||||
```yaml
|
||||
receivers:
|
||||
- name: 'web.hook'
|
||||
webhook_configs:
|
||||
- url: 'http://127.0.0.1:5001/'
|
||||
```
|
||||
|
||||
**作用**:定义告警通知的接收方式
|
||||
|
||||
**当前配置**:
|
||||
- **接收器名称**:`web.hook`
|
||||
- **通知方式**:Webhook(HTTP POST)
|
||||
- **目标地址**:`http://127.0.0.1:5001/`
|
||||
|
||||
**说明**:
|
||||
- 告警会以 JSON 格式 POST 到指定的 URL
|
||||
- 需要有一个服务监听 `127.0.0.1:5001` 来处理告警
|
||||
- 如果没有这个服务,告警通知会失败
|
||||
|
||||
**其他可用的接收器类型**:
|
||||
- `email_configs` - 邮件通知
|
||||
- `wechat_configs` - 企业微信通知
|
||||
- `dingtalk_configs` - 钉钉通知
|
||||
- `slack_configs` - Slack 通知
|
||||
- `webhook_configs` - 自定义 Webhook
|
||||
|
||||
---
|
||||
|
||||
### 4. Inhibit Rules(抑制规则)
|
||||
|
||||
```yaml
|
||||
inhibit_rules:
|
||||
- source_match:
|
||||
severity: 'critical'
|
||||
target_match:
|
||||
severity: 'warning'
|
||||
equal: ['alertname', 'dev', 'instance']
|
||||
```
|
||||
|
||||
**作用**:定义告警抑制规则,避免重复告警
|
||||
|
||||
**当前规则说明**:
|
||||
- **源匹配**:如果存在 `severity: critical` 的告警
|
||||
- **目标匹配**:则抑制 `severity: warning` 的告警
|
||||
- **匹配条件**:当 `alertname`、`dev`、`instance` 标签相同时
|
||||
|
||||
**示例场景**:
|
||||
- 如果设备离线(critical),则不再发送该设备的温度过高(warning)告警
|
||||
- 避免告警风暴,只关注最严重的问题
|
||||
|
||||
**注意**:当前配置中的 `dev` 标签可能不存在,建议修改为实际使用的标签。
|
||||
|
||||
---
|
||||
|
||||
## 配置流程图
|
||||
|
||||
```
|
||||
Prometheus 触发告警
|
||||
│
|
||||
▼
|
||||
Alertmanager 接收告警
|
||||
│
|
||||
├─> 按 alertname 分组
|
||||
├─> 等待 10s(group_wait)
|
||||
├─> 应用抑制规则
|
||||
│
|
||||
▼
|
||||
发送到接收器 (web.hook)
|
||||
│
|
||||
▼
|
||||
POST 到 http://127.0.0.1:5001/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常见配置场景
|
||||
|
||||
### 场景 1:邮件通知
|
||||
|
||||
```yaml
|
||||
receivers:
|
||||
- name: 'email'
|
||||
email_configs:
|
||||
- to: 'admin@example.com'
|
||||
from: 'alertmanager@example.com'
|
||||
smarthost: 'smtp.gmail.com:587'
|
||||
auth_username: 'your-email@gmail.com'
|
||||
auth_password: 'your-password'
|
||||
```
|
||||
|
||||
### 场景 2:企业微信通知
|
||||
|
||||
```yaml
|
||||
receivers:
|
||||
- name: 'wechat'
|
||||
wechat_configs:
|
||||
- api_url: 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send'
|
||||
corp_id: 'your-corp-id'
|
||||
to_user: '@all'
|
||||
```
|
||||
|
||||
### 场景 3:多接收器(根据严重程度)
|
||||
|
||||
```yaml
|
||||
route:
|
||||
routes:
|
||||
- match:
|
||||
severity: critical
|
||||
receiver: 'critical-alerts'
|
||||
- match:
|
||||
severity: warning
|
||||
receiver: 'warning-alerts'
|
||||
receiver: 'default'
|
||||
|
||||
receivers:
|
||||
- name: 'critical-alerts'
|
||||
email_configs:
|
||||
- to: 'oncall@example.com'
|
||||
- name: 'warning-alerts'
|
||||
webhook_configs:
|
||||
- url: 'http://127.0.0.1:5001/'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 当前配置的问题和建议
|
||||
|
||||
### 问题 1:Webhook 服务不存在
|
||||
|
||||
**当前配置**:`http://127.0.0.1:5001/`
|
||||
|
||||
**问题**:如果没有服务监听这个端口,告警通知会失败
|
||||
|
||||
**解决方案**:
|
||||
1. **部署 Webhook 接收服务**(推荐用于开发测试)
|
||||
2. **配置邮件通知**(推荐用于生产环境)
|
||||
3. **配置企业微信/钉钉**(推荐用于团队协作)
|
||||
|
||||
### 问题 2:抑制规则标签不匹配
|
||||
|
||||
**当前配置**:`equal: ['alertname', 'dev', 'instance']`
|
||||
|
||||
**问题**:`dev` 标签可能不存在于告警中
|
||||
|
||||
**建议修改**:
|
||||
```yaml
|
||||
equal: ['alertname', 'instance']
|
||||
```
|
||||
|
||||
### 问题 3:SMTP 配置未使用
|
||||
|
||||
**当前配置**:SMTP 配置存在但未使用
|
||||
|
||||
**建议**:
|
||||
- 如果使用邮件通知,需要配置正确的 SMTP 服务器
|
||||
- 如果只使用 Webhook,可以删除 SMTP 配置
|
||||
|
||||
---
|
||||
|
||||
## 验证配置
|
||||
|
||||
### 1. 检查配置语法
|
||||
|
||||
```bash
|
||||
docker exec alertmanager amtool check-config /etc/alertmanager/alertmanager.yml
|
||||
```
|
||||
|
||||
### 2. 查看告警状态
|
||||
|
||||
访问 Alertmanager Web UI:
|
||||
```
|
||||
http://localhost:9093
|
||||
```
|
||||
|
||||
### 3. 测试告警
|
||||
|
||||
在 Prometheus 中手动触发告警,查看是否收到通知。
|
||||
|
||||
---
|
||||
|
||||
## 配置示例(推荐)
|
||||
|
||||
### 最小化 Webhook 配置
|
||||
|
||||
```yaml
|
||||
route:
|
||||
group_by: ['alertname', 'instance']
|
||||
group_wait: 10s
|
||||
group_interval: 10s
|
||||
repeat_interval: 1h
|
||||
receiver: 'web.hook'
|
||||
|
||||
receivers:
|
||||
- name: 'web.hook'
|
||||
webhook_configs:
|
||||
- url: 'http://127.0.0.1:5001/'
|
||||
send_resolved: true # 发送恢复通知
|
||||
|
||||
inhibit_rules:
|
||||
- source_match:
|
||||
severity: 'critical'
|
||||
target_match:
|
||||
severity: 'warning'
|
||||
equal: ['alertname', 'instance']
|
||||
```
|
||||
|
||||
### 邮件通知配置
|
||||
|
||||
```yaml
|
||||
global:
|
||||
smtp_smarthost: 'smtp.gmail.com:587'
|
||||
smtp_from: 'alertmanager@example.com'
|
||||
smtp_auth_username: 'your-email@gmail.com'
|
||||
smtp_auth_password: 'your-app-password'
|
||||
|
||||
route:
|
||||
group_by: ['alertname']
|
||||
group_wait: 10s
|
||||
group_interval: 10s
|
||||
repeat_interval: 1h
|
||||
receiver: 'email'
|
||||
|
||||
receivers:
|
||||
- name: 'email'
|
||||
email_configs:
|
||||
- to: 'admin@example.com'
|
||||
send_resolved: true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 下一步
|
||||
|
||||
1. **配置通知渠道**:根据实际需求配置邮件、企业微信、钉钉等
|
||||
2. **测试告警**:确保告警能够正常发送
|
||||
3. **优化路由规则**:根据业务需求调整告警分组和路由
|
||||
4. **设置告警抑制**:避免告警风暴
|
||||
@@ -1,130 +0,0 @@
|
||||
# 告警规则说明
|
||||
|
||||
## 告警规则概述
|
||||
|
||||
当前配置了两组告警规则,用于监控 ONVIF 设备和网络设备的状态。
|
||||
|
||||
## 告警规则详解
|
||||
|
||||
### 1. ONVIF 设备告警组 (onvif_alerts)
|
||||
|
||||
#### ONVIFDeviceDown - ONVIF设备离线
|
||||
- **触发条件**:`up{job="onvif-devices"} == 0`
|
||||
- **持续时间**:1分钟
|
||||
- **严重程度**:critical(严重)
|
||||
- **说明**:当 ONVIF 设备(如摄像头)无法访问或离线超过1分钟时触发
|
||||
- **告警信息**:显示哪个设备实例离线
|
||||
|
||||
#### ONVIFDeviceHighTemperature - ONVIF设备温度过高
|
||||
- **触发条件**:`onvif_device_temperature > 70`
|
||||
- **持续时间**:2分钟
|
||||
- **严重程度**:warning(警告)
|
||||
- **说明**:当设备温度超过70°C时触发,防止设备过热损坏
|
||||
- **告警信息**:显示设备实例和当前温度值
|
||||
|
||||
#### ONVIFDeviceLowStorage - ONVIF设备存储空间不足
|
||||
- **触发条件**:`onvif_storage_usage_percent > 90`
|
||||
- **持续时间**:5分钟
|
||||
- **严重程度**:warning(警告)
|
||||
- **说明**:当设备存储使用率超过90%时触发,提醒需要清理存储空间
|
||||
- **告警信息**:显示设备实例和存储使用率
|
||||
|
||||
### 2. 网络设备告警组 (network_alerts)
|
||||
|
||||
#### NetworkDeviceDown - 网络设备离线
|
||||
- **触发条件**:`probe_success{job="network-ping"} == 0`
|
||||
- **持续时间**:2分钟
|
||||
- **严重程度**:critical(严重)
|
||||
- **说明**:当网络设备无法通过 ping 连通时触发
|
||||
- **告警信息**:显示哪个网络设备实例无法访问
|
||||
|
||||
#### HighNetworkLatency - 网络延迟过高
|
||||
- **触发条件**:`probe_duration_seconds{job="network-ping"} > 1`
|
||||
- **持续时间**:5分钟
|
||||
- **严重程度**:warning(警告)
|
||||
- **说明**:当网络延迟超过1秒时触发,表示网络质量下降
|
||||
- **告警信息**:显示设备实例和延迟时间
|
||||
|
||||
## 为什么告警规则是 Inactive(非活跃)状态?
|
||||
|
||||
告警规则显示为 **inactive** 的原因:
|
||||
|
||||
1. **缺少数据源**:
|
||||
- 这些告警依赖于边缘节点推送的数据
|
||||
- 需要配置 `edge-agent` 并部署到边缘节点
|
||||
- 边缘节点需要配置 ONVIF 设备和网络探测目标
|
||||
|
||||
2. **指标不存在**:
|
||||
- `up{job="onvif-devices"}` - 需要边缘节点运行 ONVIF Exporter
|
||||
- `probe_success{job="network-ping"}` - 需要边缘节点运行 Blackbox Exporter
|
||||
- 如果这些指标不存在,告警规则无法评估,所以是 inactive
|
||||
|
||||
3. **数据未推送**:
|
||||
- 边缘节点的数据需要通过 `remote_write` 推送到中央服务器
|
||||
- 检查边缘节点是否正常连接并推送数据
|
||||
|
||||
## 如何激活告警规则?
|
||||
|
||||
### 步骤 1:部署边缘节点代理
|
||||
|
||||
```bash
|
||||
cd ../edge-agent
|
||||
bash deploy.sh
|
||||
```
|
||||
|
||||
### 步骤 2:配置监控目标
|
||||
|
||||
1. **配置 ONVIF 设备**:
|
||||
- 编辑 `edge-agent/config/devices.csv`
|
||||
- 添加要监控的 ONVIF 设备信息
|
||||
|
||||
2. **配置网络探测目标**:
|
||||
- 编辑 `edge-agent/config/ping-targets.csv`
|
||||
- 添加要监控的网络设备 IP 地址
|
||||
|
||||
### 步骤 3:更新配置
|
||||
|
||||
```bash
|
||||
cd edge-agent/config
|
||||
./update-configs.sh
|
||||
```
|
||||
|
||||
### 步骤 4:验证数据推送
|
||||
|
||||
1. 在 Prometheus 中查询指标:
|
||||
```promql
|
||||
up{job="onvif-devices"}
|
||||
probe_success{job="network-ping"}
|
||||
```
|
||||
|
||||
2. 如果能看到数据,告警规则会自动变为 **active** 状态
|
||||
|
||||
## 告警状态说明
|
||||
|
||||
- **Inactive(非活跃)**:告警规则已加载,但没有匹配的数据或条件未满足
|
||||
- **Pending(待触发)**:条件满足,但未达到持续时间阈值
|
||||
- **Firing(触发中)**:条件满足且持续时间达到阈值,告警已触发
|
||||
- **Resolved(已解决)**:告警条件不再满足,告警已恢复
|
||||
|
||||
## 告警通知
|
||||
|
||||
当告警触发时,会发送到 Alertmanager,然后根据配置发送通知:
|
||||
- 当前配置:发送到 webhook `http://127.0.0.1:5001/`
|
||||
- 可以修改 `alertmanager/alertmanager.yml` 配置邮件、钉钉、企业微信等通知方式
|
||||
|
||||
## 自定义告警规则
|
||||
|
||||
可以在 `alert_rules.yml` 中添加更多告警规则,例如:
|
||||
|
||||
```yaml
|
||||
- alert: CustomAlert
|
||||
expr: your_metric > threshold
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: "自定义告警"
|
||||
description: "描述信息"
|
||||
```
|
||||
|
||||
修改后,Prometheus 会自动重新加载配置(如果启用了 `--web.enable-lifecycle`)。
|
||||
@@ -130,24 +130,8 @@ Alertmanager 处理告警
|
||||
|
||||
### 3. 查询流程
|
||||
|
||||
```
|
||||
用户访问 Grafana
|
||||
│
|
||||
▼
|
||||
Grafana 发送 PromQL 查询
|
||||
│
|
||||
▼
|
||||
Prometheus 处理查询
|
||||
│
|
||||
├─> 从 VictoriaMetrics 读取数据
|
||||
└─> 返回查询结果
|
||||
│
|
||||
▼
|
||||
Grafana 渲染图表
|
||||
│
|
||||
▼
|
||||
用户查看监控数据
|
||||
```
|
||||
- **中央自抓指标**:Grafana → Prometheus 数据源 → Prometheus 返回结果。
|
||||
- **边缘数据**:Grafana → **VictoriaMetrics** 数据源 → VictoriaMetrics 返回结果(不经过 Prometheus 查询 VM)。
|
||||
|
||||
## 容器详细说明
|
||||
|
||||
@@ -160,7 +144,7 @@ Grafana 渲染图表
|
||||
### 2. Prometheus Central (9091)
|
||||
- **作用**:指标收集、查询和告警评估
|
||||
- **数据源**:
|
||||
- 从 VictoriaMetrics 读取边缘节点数据
|
||||
- 从 VictoriaMetrics 读取边缘节点数据(用于告警规则评估与部分查询)
|
||||
- 抓取本地服务(自身、Grafana、Alertmanager、VictoriaMetrics)
|
||||
- **功能**:
|
||||
- 评估告警规则 (`alert_rules.yml`)
|
||||
@@ -178,7 +162,7 @@ Grafana 渲染图表
|
||||
|
||||
### 4. Grafana (3000)
|
||||
- **作用**:数据可视化和仪表板
|
||||
- **数据源**:从 Prometheus 查询数据
|
||||
- **数据源**:**Prometheus**(中央自抓指标)、**VictoriaMetrics**(边缘推送数据;查边缘 Ping/ONVIF 等请选此数据源)
|
||||
- **功能**:
|
||||
- 创建图表和仪表板
|
||||
- 多用户管理(组织隔离)
|
||||
|
||||
@@ -4,323 +4,40 @@
|
||||
|
||||
```
|
||||
central-server/
|
||||
├── docker-compose.yml # Docker Compose 服务编排配置
|
||||
├── docker-compose.yml # 服务编排
|
||||
├── deploy.sh # 部署脚本
|
||||
├── prometheus.yml # Prometheus 主配置文件
|
||||
├── alert_rules.yml # 告警规则定义
|
||||
├── alertmanager/
|
||||
│ └── alertmanager.yml # Alertmanager 告警管理配置
|
||||
├── prometheus.yml # Prometheus 主配置
|
||||
├── alert_rules.yml # 告警规则
|
||||
├── alertmanager/alertmanager.yml
|
||||
└── grafana/
|
||||
├── setup-users.sh # 多用户配置脚本
|
||||
├── provisioning/
|
||||
│ ├── datasources/ # 数据源自动配置
|
||||
│ │ ├── prometheus.yml # Prometheus 数据源
|
||||
│ │ └── prometheus-admin.yml # 管理员全局数据源
|
||||
│ └── dashboards/ # 仪表板自动配置
|
||||
│ └── dashboard.yml # 仪表板配置
|
||||
├── setup-users.sh
|
||||
├── provisioning/datasources/ # prometheus.yml, victoriametrics.yml
|
||||
└── dashboards/
|
||||
└── onvif-monitoring.json # ONVIF 监控仪表板
|
||||
```
|
||||
|
||||
## 配置文件详解
|
||||
## 主要服务与端口
|
||||
|
||||
### 1. docker-compose.yml
|
||||
| 服务 | 端口 | 说明 |
|
||||
|------|------|------|
|
||||
| prometheus-central | 9091 | 自抓 + 告警评估;边缘数据由 Grafana 从 VictoriaMetrics 查询 |
|
||||
| grafana | 3000 | 数据源:Prometheus(中央自抓)、VictoriaMetrics(边缘数据) |
|
||||
| victoria-metrics | 8428 | 接收边缘 remote_write |
|
||||
| alertmanager | 9093 | 告警路由与通知 |
|
||||
|
||||
**作用**:定义所有 Docker 容器的配置和编排
|
||||
## 关键配置摘要
|
||||
|
||||
**包含的服务**:
|
||||
- `prometheus-central` - Prometheus 中央服务器(端口 9091)
|
||||
- `grafana` - Grafana 可视化仪表板(端口 3000,中文界面)
|
||||
- `alertmanager` - 告警管理器(端口 9093)
|
||||
- `victoria-metrics` - 远程写入接收器(端口 8428)
|
||||
- **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://<central>:4080/tiles/vec/{z}/{x}/{y}`,再添加一层 XYZ 填 `http://<central>:4080/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)。
|
||||
|
||||
**关键配置**:
|
||||
- 数据存储:所有数据存储在 `/storage` 分区
|
||||
- 网络:所有容器在 `monitoring_net` 网络中
|
||||
- 卷挂载:配置文件、数据目录、仪表板等
|
||||
## 修改与重载
|
||||
|
||||
**使用**:
|
||||
```bash
|
||||
docker compose up -d # 启动所有服务
|
||||
docker compose down # 停止所有服务
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. deploy.sh
|
||||
|
||||
**作用**:自动化部署脚本,一键部署中央服务器
|
||||
|
||||
**功能**:
|
||||
1. ✅ 检查 Docker 和 Docker Compose 环境
|
||||
2. ✅ 检查磁盘空间(根分区和 /storage 分区)
|
||||
3. ✅ 验证配置文件存在性
|
||||
4. ✅ 创建数据目录并设置权限
|
||||
5. ✅ 拉取 Docker 镜像
|
||||
6. ✅ 启动所有服务
|
||||
7. ✅ 检查服务状态
|
||||
|
||||
**使用**:
|
||||
```bash
|
||||
bash deploy.sh
|
||||
```
|
||||
|
||||
**输出信息**:
|
||||
- 服务访问地址
|
||||
- 管理命令
|
||||
- 防火墙提示
|
||||
- 下一步操作建议
|
||||
|
||||
---
|
||||
|
||||
### 3. prometheus.yml
|
||||
|
||||
**作用**:Prometheus 主配置文件,定义数据收集和查询规则
|
||||
|
||||
**主要配置**:
|
||||
|
||||
#### Global(全局配置)
|
||||
```yaml
|
||||
scrape_interval: 15s # 抓取间隔
|
||||
evaluation_interval: 15s # 告警规则评估间隔
|
||||
external_labels:
|
||||
cluster: 'central-monitoring' # 集群标识
|
||||
```
|
||||
|
||||
#### Remote Write(远程写入)
|
||||
```yaml
|
||||
remote_write:
|
||||
- url: http://victoria-metrics:8428/api/v1/write
|
||||
```
|
||||
- **作用**:将 Prometheus 收集的数据写入 VictoriaMetrics
|
||||
- **目的**:接收边缘节点推送的数据
|
||||
|
||||
#### Scrape Configs(抓取配置)
|
||||
定义了 4 个抓取任务:
|
||||
1. **prometheus-central** - 抓取自身指标
|
||||
2. **victoria-metrics** - 抓取 VictoriaMetrics 指标
|
||||
3. **alertmanager** - 抓取 Alertmanager 指标
|
||||
4. **grafana** - 抓取 Grafana 指标
|
||||
|
||||
#### Rule Files(告警规则文件)
|
||||
```yaml
|
||||
rule_files:
|
||||
- "alert_rules.yml"
|
||||
```
|
||||
- 引用 `alert_rules.yml` 文件中的告警规则
|
||||
|
||||
#### Alerting(告警配置)
|
||||
```yaml
|
||||
alerting:
|
||||
alertmanagers:
|
||||
- static_configs:
|
||||
- targets:
|
||||
- alertmanager:9093
|
||||
```
|
||||
- 配置 Alertmanager 地址,用于发送告警
|
||||
|
||||
---
|
||||
|
||||
### 4. alert_rules.yml
|
||||
|
||||
**作用**:定义告警规则,当监控指标满足条件时触发告警
|
||||
|
||||
**告警组**:
|
||||
|
||||
#### onvif_alerts(ONVIF 设备告警组)
|
||||
- **ONVIFDeviceDown** - 设备离线告警(critical)
|
||||
- **ONVIFDeviceHighTemperature** - 设备温度过高告警(warning)
|
||||
- **ONVIFDeviceLowStorage** - 设备存储空间不足告警(warning)
|
||||
|
||||
#### network_alerts(网络设备告警组)
|
||||
- **NetworkDeviceDown** - 网络设备离线告警(critical)
|
||||
- **HighNetworkLatency** - 网络延迟过高告警(warning)
|
||||
|
||||
**告警规则格式**:
|
||||
```yaml
|
||||
- alert: AlertName
|
||||
expr: promql_query # PromQL 查询表达式
|
||||
for: 1m # 持续时间
|
||||
labels:
|
||||
severity: critical # 严重程度
|
||||
annotations:
|
||||
summary: "告警摘要"
|
||||
description: "详细描述"
|
||||
```
|
||||
|
||||
**详细说明**:参考 `doc/ALERT_RULES_EXPLANATION.md`
|
||||
|
||||
---
|
||||
|
||||
### 5. alertmanager/alertmanager.yml
|
||||
|
||||
**作用**:Alertmanager 配置,定义告警路由和通知方式
|
||||
|
||||
**主要配置**:
|
||||
|
||||
#### Global(全局配置)
|
||||
- SMTP 邮件服务器配置(当前未使用)
|
||||
|
||||
#### Route(路由配置)
|
||||
- 告警分组规则
|
||||
- 告警发送间隔
|
||||
- 默认接收器
|
||||
|
||||
#### Receivers(接收器)
|
||||
- 当前配置:Webhook `http://127.0.0.1:5001/`
|
||||
- 可配置:邮件、企业微信、钉钉等
|
||||
|
||||
#### Inhibit Rules(抑制规则)
|
||||
- 避免重复告警
|
||||
- 当 critical 告警存在时,抑制 warning 告警
|
||||
|
||||
**详细说明**:参考 `doc/ALERTMANAGER_CONFIG.md`
|
||||
|
||||
---
|
||||
|
||||
### 6. grafana/provisioning/
|
||||
|
||||
**作用**:Grafana 自动配置目录,容器启动时自动加载
|
||||
|
||||
#### datasources/prometheus.yml
|
||||
|
||||
**作用**:自动配置 Prometheus 数据源
|
||||
|
||||
**配置内容**:
|
||||
- 数据源名称:Prometheus
|
||||
- 数据源类型:prometheus
|
||||
- 访问方式:proxy(通过 Grafana 代理)
|
||||
- URL:`http://prometheus-central:9090`
|
||||
- 默认数据源:是
|
||||
|
||||
#### datasources/prometheus-admin.yml
|
||||
|
||||
**作用**:管理员全局数据源(可选)
|
||||
|
||||
**特点**:
|
||||
- 允许管理员查看所有数据(不受标签过滤限制)
|
||||
- 用于管理员查看全局监控数据
|
||||
|
||||
#### dashboards/dashboard.yml
|
||||
|
||||
**作用**:自动加载仪表板配置
|
||||
|
||||
**配置内容**:
|
||||
- 从 `/var/lib/grafana/dashboards` 目录自动加载仪表板
|
||||
- 更新间隔:10 秒
|
||||
- 允许 UI 更新:是
|
||||
|
||||
---
|
||||
|
||||
### 7. grafana/dashboards/onvif-monitoring.json
|
||||
|
||||
**作用**:ONVIF 设备监控仪表板
|
||||
|
||||
**内容**:
|
||||
- ONVIF 设备状态面板
|
||||
- 设备在线率仪表
|
||||
- 其他监控图表
|
||||
|
||||
**自动加载**:通过 `dashboard.yml` 配置自动加载
|
||||
|
||||
---
|
||||
|
||||
### 8. grafana/setup-users.sh
|
||||
|
||||
**作用**:自动化配置 Grafana 多用户和组织
|
||||
|
||||
**功能**:
|
||||
- 创建 Grafana 组织
|
||||
- 创建用户并分配到组织
|
||||
- 通过 Grafana API 批量配置
|
||||
|
||||
**使用**:
|
||||
```bash
|
||||
cd central-server/grafana
|
||||
bash setup-users.sh
|
||||
```
|
||||
|
||||
**详细说明**:参考 `doc/USER_MANAGEMENT.md`
|
||||
|
||||
---
|
||||
|
||||
## 配置文件关系图
|
||||
|
||||
```
|
||||
docker-compose.yml
|
||||
│
|
||||
├─> prometheus.yml ──┐
|
||||
│ │
|
||||
├─> alert_rules.yml ──┤──> Prometheus 容器
|
||||
│ │
|
||||
└─> alertmanager.yml ─┘──> Alertmanager 容器
|
||||
│
|
||||
└─> grafana/
|
||||
├─> provisioning/ ──> Grafana 自动配置
|
||||
└─> dashboards/ ────> 仪表板文件
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 数据流向
|
||||
|
||||
```
|
||||
边缘节点数据
|
||||
│
|
||||
▼
|
||||
VictoriaMetrics (8428) ──> 存储数据
|
||||
│
|
||||
▼
|
||||
Prometheus (9091) ──┬──> 查询数据 ──> Grafana (3000)
|
||||
│ │
|
||||
└──> 评估告警规则 (alert_rules.yml)
|
||||
│
|
||||
▼
|
||||
Alertmanager (9093) ──> 发送通知
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 配置文件修改指南
|
||||
|
||||
### 修改 Prometheus 配置
|
||||
|
||||
1. 编辑 `prometheus.yml`
|
||||
2. 重启容器:`docker compose restart prometheus-central`
|
||||
3. 或使用热重载:`curl -X POST http://localhost:9091/-/reload`
|
||||
|
||||
### 修改告警规则
|
||||
|
||||
1. 编辑 `alert_rules.yml`
|
||||
2. 重启容器:`docker compose restart prometheus-central`
|
||||
3. 或使用热重载:`curl -X POST http://localhost:9091/-/reload`
|
||||
|
||||
### 修改 Alertmanager 配置
|
||||
|
||||
1. 编辑 `alertmanager/alertmanager.yml`
|
||||
2. 重启容器:`docker compose restart alertmanager`
|
||||
|
||||
### 修改 Grafana 配置
|
||||
|
||||
1. 编辑 `grafana/provisioning/` 下的配置文件
|
||||
2. 重启容器:`docker compose restart grafana`
|
||||
3. 或通过 Grafana Web UI 修改(会持久化到数据库)
|
||||
|
||||
---
|
||||
|
||||
## 重要提示
|
||||
|
||||
1. **数据存储**:所有数据存储在 `/storage` 分区,避免根分区空间不足
|
||||
2. **端口映射**:Prometheus 使用 9091(避免与 cockpit 冲突)
|
||||
3. **配置文件权限**:确保配置文件有正确的读取权限
|
||||
4. **网络连通性**:确保边缘节点可以访问 8428 端口(VictoriaMetrics)
|
||||
5. **告警通知**:当前配置使用 webhook,需要部署接收服务或修改为其他通知方式
|
||||
|
||||
---
|
||||
- Prometheus:改 `prometheus.yml` 或 `alert_rules.yml` 后 `docker compose restart prometheus-central`,或 `curl -X POST http://localhost:9091/-/reload`(若启用 lifecycle)。
|
||||
- Alertmanager:改 `alertmanager/alertmanager.yml` 后 `docker compose restart alertmanager`。
|
||||
- Grafana:改 provisioning 后重启;或通过 Web UI 修改(持久化到库)。
|
||||
|
||||
## 相关文档
|
||||
|
||||
- **系统架构**:`doc/ARCHITECTURE.md`
|
||||
- **告警规则**:`doc/ALERT_RULES_EXPLANATION.md`
|
||||
- **Alertmanager 配置**:`doc/ALERTMANAGER_CONFIG.md`
|
||||
- **用户管理**:`doc/USER_MANAGEMENT.md`
|
||||
- [ARCHITECTURE.md](ARCHITECTURE.md) | [ALERTING.md](ALERTING.md) | [USER_MANAGEMENT.md](USER_MANAGEMENT.md) | [central-server/CONFIGURATION.md](../central-server/CONFIGURATION.md)
|
||||
|
||||
@@ -1,161 +1,61 @@
|
||||
# 部署指南
|
||||
|
||||
部署顺序见 **[doc/README.md](README.md)#部署顺序**:**第一步 中央服务器 → 第二步 边缘节点 → 第三步(可选)多用户与告警**。本文为各步的详细说明、检查清单与常见问题。
|
||||
部署顺序见 **[doc/README.md](README.md)#部署顺序**:中央 → 边缘 → 多用户/告警(可选)→ 拓扑标注(可选)。本文为各步操作与验证要点。
|
||||
|
||||
---
|
||||
|
||||
## 第一步:部署中央服务器
|
||||
|
||||
### 前置要求
|
||||
**前置**:Docker、Docker Compose;端口 3000、9091、8428、9093、4080 未被占用;磁盘充足。
|
||||
|
||||
- Docker 与 Docker Compose 已安装
|
||||
- 根分区至少约 1GB 可用;数据目录所在分区至少约 2GB
|
||||
- 端口未被占用:3000(Grafana)、9091(Prometheus)、8428(VictoriaMetrics)、9093(Alertmanager)
|
||||
- 若需外网访问:防火墙开放上述端口
|
||||
```bash
|
||||
cd central-server
|
||||
cp env.example .env # 可选
|
||||
bash deploy.sh
|
||||
```
|
||||
|
||||
### 操作步骤
|
||||
|
||||
1. 进入目录并准备环境(可选):
|
||||
```bash
|
||||
cd central-server
|
||||
cp env.example .env # 可选:修改端口、Traefik、网络等
|
||||
```
|
||||
|
||||
2. 执行部署:
|
||||
```bash
|
||||
bash deploy.sh
|
||||
```
|
||||
|
||||
3. 等待约 15 秒后检查:
|
||||
```bash
|
||||
docker compose ps
|
||||
```
|
||||
|
||||
4. 验证访问:
|
||||
- Grafana: http://localhost:3000(默认 admin / admin123)
|
||||
- Prometheus: http://localhost:9091
|
||||
- VictoriaMetrics: http://localhost:8428
|
||||
- Alertmanager: http://localhost:9093
|
||||
|
||||
### 中央服务器检查清单
|
||||
|
||||
- [ ] Docker、Docker Compose 已安装
|
||||
- [ ] 磁盘空间充足(根分区约 1GB+,数据分区约 2GB+)
|
||||
- [ ] 端口 3000、9091、8428、9093 未被占用
|
||||
- [ ] 需要时已开放防火墙
|
||||
- [ ] 部署后能打开 Grafana、Prometheus
|
||||
**验证**:Grafana http://localhost:3000(admin/admin123)、Prometheus http://localhost:9091、VictoriaMetrics http://localhost:8428、拓扑标注助手 http://localhost:4080。
|
||||
|
||||
---
|
||||
|
||||
## 第二步:部署边缘节点
|
||||
|
||||
**前提**:第一步中央服务器已部署并正常运行(尤其 VictoriaMetrics 8428 可访问)。
|
||||
**前提**:中央已运行,VictoriaMetrics 8428 可访问。
|
||||
|
||||
### 前置要求
|
||||
- **本机同机**:`cd edge-agent && bash run-edge-local.sh`(中央地址设为 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`。
|
||||
|
||||
- Docker、Docker Compose 已安装
|
||||
- `jq` 已安装(用于生成 JSON 配置)
|
||||
- 边缘节点能访问中央服务器(能访问中央 IP:8428)
|
||||
- 如需监控 ONVIF/网络设备:网络可达这些设备
|
||||
**验证**:边缘 Prometheus http://localhost:9092(或边缘机 IP:9092);中央 Grafana 选数据源 **VictoriaMetrics**,查询 `probe_success{job="network-ping"}` 可见边缘数据。
|
||||
|
||||
### 情形 A:本机同机部署(中央与边缘在同一台机器)
|
||||
|
||||
```bash
|
||||
cd edge-agent
|
||||
bash run-edge-local.sh
|
||||
```
|
||||
|
||||
脚本会自动将中央地址设为 `host.docker.internal:8428` 并执行部署。边缘 Prometheus UI:http://localhost:9092。
|
||||
|
||||
### 情形 B:边缘在另一台机器
|
||||
|
||||
1. 进入目录并配置中央地址:
|
||||
```bash
|
||||
cd edge-agent
|
||||
cp env.example .env
|
||||
```
|
||||
编辑 `.env`:
|
||||
- `CENTRAL_SERVER_HOST=` 中央服务器 IP 或域名
|
||||
- `CENTRAL_SERVER_PORT=8428`
|
||||
|
||||
2. 配置监控目标并生成配置:
|
||||
```bash
|
||||
# 编辑 config/targets.csv(Ping / ONVIF),详见 TARGETS_CSV_GUIDE.md
|
||||
cd config && chmod +x *.sh && ./update-configs.sh && cd ..
|
||||
```
|
||||
|
||||
3. 部署:
|
||||
```bash
|
||||
bash deploy.sh
|
||||
```
|
||||
|
||||
4. 验证:
|
||||
- 边缘 Prometheus: http://localhost:9092(或边缘机器 IP:9092)
|
||||
- 在中央 Grafana 中**选择数据源「VictoriaMetrics」**,查询如 `up{job="network-ping"}` 或 `up{region="workernode_1"}` 应能看到边缘数据
|
||||
|
||||
### 边缘节点检查清单
|
||||
|
||||
- [ ] 中央服务器已部署且 VictoriaMetrics 可访问(端口 8428)
|
||||
- [ ] `.env` 中 `CENTRAL_SERVER_HOST`、`CENTRAL_SERVER_PORT` 正确
|
||||
- [ ] `config/targets.csv` 已配置(或已生成 `onvif-targets.json`、`ping-targets.json`)
|
||||
- [ ] 已执行 `config/update-configs.sh`
|
||||
- [ ] 边缘能访问中央 8428 端口
|
||||
- [ ] 部署后在 Grafana 的 VictoriaMetrics 数据源中能看到边缘指标
|
||||
**常见问题**:
|
||||
- 看不到边缘数据:确认 `.env` 中为中央 IP(非 host.docker.internal,除非本机同机);从边缘 `curl -s -o /dev/null -w "%{http_code}" http://<中央IP>:8428/health` 应为 200。
|
||||
- 端口冲突:边缘 Prometheus 已映射 9092,避免与中央 9091 冲突。
|
||||
|
||||
---
|
||||
|
||||
## 第三步(可选):多用户与告警
|
||||
|
||||
- **Grafana 多用户**:在中央服务器上执行 `cd central-server/grafana && bash setup-users.sh`,然后按 [USER_MANAGEMENT.md](USER_MANAGEMENT.md) 配置组织、用户与数据源。
|
||||
- **告警规则**:中央已内置 `alert_rules.yml`;如需调整见 [ALERT_RULES_EXPLANATION.md](ALERT_RULES_EXPLANATION.md)。
|
||||
- **告警通知**:编辑 `central-server/alertmanager/alertmanager.yml` 配置接收端,见 [ALERTMANAGER_CONFIG.md](ALERTMANAGER_CONFIG.md)。
|
||||
- **Grafana 多用户**:`cd central-server/grafana && bash setup-users.sh`,详见 [USER_MANAGEMENT.md](USER_MANAGEMENT.md)。
|
||||
- **告警**:规则见 [ALERTING.md](ALERTING.md);通知渠道编辑 `central-server/alertmanager/alertmanager.yml`。
|
||||
|
||||
---
|
||||
|
||||
## 部署后验证
|
||||
## 第四步(可选):拓扑标注助手
|
||||
|
||||
### 中央
|
||||
与中央同机运行,访问 http://localhost:4080。上传本机 `targets.csv` → 选择设备、GPS 或地图点击补坐标 → 保存 → 下载 CSV → 将下载文件部署到各边缘 `edge-agent/config/targets.csv`,在边缘执行:
|
||||
|
||||
- `docker compose ps` 中 prometheus-central、grafana、victoria-metrics、alertmanager 为 Up
|
||||
- 能打开 Grafana、Prometheus、Alertmanager、VictoriaMetrics 的 Web 界面
|
||||
- Grafana 中「Prometheus」数据源可查询到中央自身指标(如 `up`)
|
||||
|
||||
### 边缘
|
||||
|
||||
- `docker compose ps` 中 prometheus-edge、onvif-exporter、blackbox-exporter 为 Up
|
||||
- 边缘 Prometheus http://localhost:9092/targets 中目标状态正常
|
||||
- 中央 Grafana 中**选择数据源「VictoriaMetrics」**,能查到边缘相关指标(如 `up{job="network-ping"}`)
|
||||
```bash
|
||||
cd edge-agent/config && ./update-configs.sh && ./csv-to-topology-geojson.sh targets.csv topology.geojson
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常见部署问题
|
||||
## 部署后检查清单
|
||||
|
||||
### 端口冲突
|
||||
- **中央**:`docker compose ps` 中相关服务 Up;Grafana 中 Prometheus 数据源可查 `up`。
|
||||
- **边缘**:prometheus-edge、blackbox-exporter Up;Grafana 选 VictoriaMetrics 可查 `probe_success{job="network-ping"}` 等。
|
||||
- **磁盘/端口**:端口冲突用 `ss -tulpn` 排查;空间不足时清理或扩容数据目录。
|
||||
|
||||
- 现象:容器启动失败,提示端口已被占用。
|
||||
- 处理:`netstat -tulpn | grep <端口>` 或 `ss -tulpn` 查看占用;修改对应 `docker-compose.yml` 端口映射或关闭占用进程。
|
||||
|
||||
### 磁盘空间不足
|
||||
|
||||
- 现象:拉镜像或启动失败。
|
||||
- 处理:`df -h` 检查空间;`docker system prune -a --volumes` 清理(注意会删未用卷);保证数据目录所在分区空间充足。
|
||||
|
||||
### 边缘无法连接中央
|
||||
|
||||
- 现象:边缘数据未出现在中央 Grafana 的 VictoriaMetrics 中。
|
||||
- 处理:从边缘节点 `telnet <中央IP> 8428` 或 `curl -s -o /dev/null -w "%{http_code}" http://<中央IP>:8428/health`;检查防火墙与 `.env` 中 `CENTRAL_SERVER_HOST`、`CENTRAL_SERVER_PORT`。
|
||||
|
||||
### Grafana 中看不到边缘数据
|
||||
|
||||
- 确认在 Grafana 里选择的是**数据源「VictoriaMetrics」**,不是「Prometheus」(中央自抓数据在 Prometheus)。
|
||||
- 确认边缘已部署且 remote_write 指向中央 8428;边缘 Prometheus 日志无推送错误。
|
||||
|
||||
---
|
||||
|
||||
## 相关文档
|
||||
|
||||
- 部署顺序总览:[README.md#部署顺序](README.md)
|
||||
- 中央配置:[CENTRAL_SERVER_CONFIG.md](CENTRAL_SERVER_CONFIG.md)、[central-server/CONFIGURATION.md](../central-server/CONFIGURATION.md)
|
||||
- 边缘配置与目标:[EDGE_NODE_SETUP.md](EDGE_NODE_SETUP.md)、[EDGE_AGENT_CONFIG.md](EDGE_AGENT_CONFIG.md)、[TARGETS_CSV_GUIDE.md](TARGETS_CSV_GUIDE.md)
|
||||
- 架构:[ARCHITECTURE.md](ARCHITECTURE.md)
|
||||
- 故障排查:[TROUBLESHOOTING.md](TROUBLESHOOTING.md)
|
||||
详见 [TROUBLESHOOTING.md](TROUBLESHOOTING.md)、[README.md](README.md)。
|
||||
|
||||
@@ -1,323 +1,35 @@
|
||||
# 边缘节点配置文件说明
|
||||
|
||||
## 边缘节点需要什么
|
||||
## 需要什么
|
||||
|
||||
| 类型 | 说明 | 对应组件 |
|
||||
|------|------|----------|
|
||||
| **必选** | **remote_write**:把边缘指标推到中央 | **prometheus-edge**(内配 remote_write 到中央 VictoriaMetrics) |
|
||||
| **必选** | **Blackbox**:Ping/网络探测 | **blackbox-exporter** 容器 |
|
||||
| **可选** | ONVIF、SNMP、Frigate 等 | **onvif-exporter**(`--profile onvif`)、或自建/第三方镜像 |
|
||||
| **必选** | remote_write 推送到中央 | prometheus-edge |
|
||||
| **必选** | Ping/网络探测 | blackbox-exporter |
|
||||
| **可选** | ONVIF 等 | onvif-exporter(`--profile onvif`),见 [ONVIF_ALTERNATIVES.md](ONVIF_ALTERNATIVES.md) |
|
||||
|
||||
默认部署只起 **prometheus-edge** + **blackbox-exporter**;需要 ONVIF 时再设 `ONVIF_EXPORTER_IMAGE` 并 `docker compose --profile onvif up -d`。参见 [ONVIF_ALTERNATIVES.md](ONVIF_ALTERNATIVES.md)。
|
||||
## 容器与数据流
|
||||
|
||||
---
|
||||
| 容器 | 作用 | 端口 |
|
||||
|------|------|------|
|
||||
| prometheus-edge | 抓取 Blackbox(及可选 ONVIF),remote_write → 中央 VictoriaMetrics | 9092 |
|
||||
| blackbox-exporter | Ping/HTTP/TCP 探测 | 9115(内部) |
|
||||
| onvif-exporter | 可选,ONVIF 探测 | 9600(内部) |
|
||||
|
||||
## 边缘节点各容器分别做什么
|
||||
数据流:目标 → Exporter → prometheus-edge → remote_write → 中央 VictoriaMetrics。
|
||||
|
||||
| 容器 | 必选/可选 | 作用 | 端口/接口 |
|
||||
|------|-----------|------|-----------|
|
||||
| **prometheus-edge** | **必选** | 抓取 Blackbox(及可选 ONVIF 等),通过 **remote_write** 推送到中央 VictoriaMetrics | 对外 9092;内部抓取 blackbox:9115、可选 onvif:9600 |
|
||||
| **blackbox-exporter** | **必选** | 网络 Ping/HTTP/TCP 探测,暴露 `/probe` 给 prometheus-edge 抓取 | 容器内 9115 |
|
||||
| **onvif-exporter** | **可选** | 本项目自建:读取 `config/onvif-targets.json`,ONVIF GetDeviceInformation 探测,暴露 `onvif_device_up`、`onvif_probe_duration_seconds`。启用:`docker compose --profile onvif up -d --build`。 | 容器内 9600 |
|
||||
## 目录与配置
|
||||
|
||||
**数据流**:Ping 目标 → blackbox-exporter:9115 → prometheus-edge 抓取 → **remote_write** → 中央 VictoriaMetrics。
|
||||
若启用 ONVIF:ONVIF 设备 → onvif-exporter:9600 → prometheus-edge 抓取 → remote_write → 中央。
|
||||
- **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。
|
||||
|
||||
---
|
||||
## 常用操作
|
||||
|
||||
## ONVIF 镜像替代方案
|
||||
|
||||
**说明**:目前**没有**公开可用的 ONVIF→Prometheus 镜像(如 ghcr.io/atiek/onvif-exporter 已不存在)。可选做法:
|
||||
|
||||
| 方式 | 说明 |
|
||||
|------|------|
|
||||
| **用替代方案** | 摄像头支持 SNMP 时用 **prom/snmp-exporter**;已用 Frigate 时抓其 `/api/metrics`;仅需在线监控时用 **Blackbox** 对摄像头 IP 做 Ping/HTTP。详见 **[ONVIF_ALTERNATIVES.md](ONVIF_ALTERNATIVES.md)**。 |
|
||||
| **ONVIF 可选** | 边缘默认不启动 ONVIF 服务(无可用镜像)。需要时自建镜像并在 `.env` 设 `ONVIF_EXPORTER_IMAGE=你的镜像:tag`,再执行 `docker compose --profile onvif up -d`。 |
|
||||
| **自建镜像** | 基于 Go ONVIF 库编写 exporter 并构建镜像,见 ONVIF_ALTERNATIVES.md 中「ONVIF 自建 Exporter」。 |
|
||||
|
||||
---
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
edge-agent/
|
||||
├── docker-compose.yml # Docker Compose 服务编排配置
|
||||
├── deploy.sh # 部署脚本
|
||||
├── quick-setup.sh # 快速配置脚本
|
||||
├── env.example # 环境变量示例
|
||||
├── prometheus-edge/
|
||||
│ └── prometheus.yml # Prometheus Edge 配置
|
||||
├── blackbox/
|
||||
│ └── config.yml # Blackbox Exporter 配置
|
||||
└── config/
|
||||
├── targets.csv # 统一监控目标配置(推荐)
|
||||
├── devices.csv # ONVIF 设备配置(旧格式)
|
||||
├── ping-targets.csv # Ping 目标配置(旧格式)
|
||||
├── onvif-targets.json # ONVIF 设备 JSON 配置(自动生成)
|
||||
├── ping-targets.json # Ping 目标 JSON 配置(自动生成)
|
||||
├── update-configs.sh # 配置文件更新脚本
|
||||
├── csv-to-targets.sh # 统一配置转换脚本
|
||||
├── csv-to-json.sh # ONVIF 配置转换脚本
|
||||
├── csv-to-ping-json.sh # Ping 配置转换脚本
|
||||
├── setup-remote-write.sh # 远程写入配置脚本
|
||||
└── test-connection.sh # 连接测试脚本
|
||||
```
|
||||
|
||||
## 配置文件详解
|
||||
|
||||
### 1. docker-compose.yml
|
||||
|
||||
**作用**:定义边缘节点的 Docker 容器配置
|
||||
|
||||
**包含的服务**:
|
||||
- **必选**:`prometheus-edge`(抓取 + remote_write)、`blackbox-exporter`(Ping 探测)
|
||||
- **可选**:`onvif-exporter`(需 `--profile onvif` 且设置 `ONVIF_EXPORTER_IMAGE`)
|
||||
|
||||
**关键配置**:
|
||||
- 资源限制:内存和 CPU 限制(适合边缘设备)
|
||||
- 环境变量:中央服务器地址和端口
|
||||
- 数据保留:1 小时(边缘节点只做临时存储)
|
||||
- 远程写入:自动推送到中央服务器
|
||||
|
||||
---
|
||||
|
||||
### 2. deploy.sh
|
||||
|
||||
**作用**:自动化部署脚本,一键部署边缘节点
|
||||
|
||||
**功能**:
|
||||
1. ✅ 检查 Docker 和 Docker Compose 环境
|
||||
2. ✅ 检查 jq 工具(用于配置转换)
|
||||
3. ✅ 生成配置文件(从 CSV 到 JSON)
|
||||
4. ✅ 验证配置文件存在性
|
||||
5. ✅ 创建环境变量文件
|
||||
6. ✅ 创建数据目录
|
||||
7. ✅ 拉取 Docker 镜像
|
||||
8. ✅ 启动所有服务
|
||||
|
||||
**使用**:
|
||||
```bash
|
||||
bash deploy.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. quick-setup.sh
|
||||
|
||||
**作用**:快速配置脚本,自动配置边缘节点
|
||||
|
||||
**功能**:
|
||||
- 自动检测本机 IP
|
||||
- 创建 `.env` 文件
|
||||
- 配置统一监控目标(`targets.csv`)
|
||||
- 生成配置文件
|
||||
- 可选择立即部署
|
||||
|
||||
**使用**:
|
||||
```bash
|
||||
bash quick-setup.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. env.example / .env
|
||||
|
||||
**作用**:环境变量配置
|
||||
|
||||
**配置项**:
|
||||
```bash
|
||||
CENTRAL_SERVER_HOST=192.168.2.21 # 中央服务器地址
|
||||
CENTRAL_SERVER_PORT=8428 # 中央服务器端口
|
||||
EDGE_NODE_ID=workernode_1 # 边缘节点标识
|
||||
```
|
||||
|
||||
**说明**:
|
||||
- `env.example` 是示例文件
|
||||
- 部署时会自动创建 `.env` 文件
|
||||
- 需要根据实际情况修改
|
||||
|
||||
---
|
||||
|
||||
### 5. prometheus-edge/prometheus.yml
|
||||
|
||||
**作用**:边缘 Prometheus 主配置文件
|
||||
|
||||
**主要配置**:
|
||||
|
||||
#### Global(全局配置)
|
||||
```yaml
|
||||
scrape_interval: 120s # 抓取间隔(2分钟)
|
||||
evaluation_interval: 120s # 告警评估间隔
|
||||
external_labels:
|
||||
region: workernode_1 # 边缘节点标识
|
||||
```
|
||||
|
||||
#### Remote Write(远程写入)
|
||||
```yaml
|
||||
remote_write:
|
||||
- url: http://${CENTRAL_SERVER_HOST}:${CENTRAL_SERVER_PORT}/api/v1/write
|
||||
```
|
||||
- **作用**:将收集的数据推送到中央服务器 VictoriaMetrics
|
||||
- **目的**:边缘节点不存储长期数据,只做数据收集和转发
|
||||
|
||||
#### Scrape Configs(抓取配置)
|
||||
定义了 3 个抓取任务:
|
||||
1. **onvif-devices** - 抓取 ONVIF 设备指标(通过 ONVIF Exporter)
|
||||
2. **network-ping** - 抓取网络探测指标(通过 Blackbox Exporter)
|
||||
3. **prometheus-edge** - 抓取自身指标
|
||||
|
||||
**数据保留**:1 小时(边缘节点只做临时存储)
|
||||
|
||||
---
|
||||
|
||||
### 6. blackbox/config.yml
|
||||
|
||||
**作用**:Blackbox Exporter 探测模块配置
|
||||
|
||||
**支持的探测类型**:
|
||||
- `icmp` - ICMP Ping 探测
|
||||
- `tcp_connect` - TCP 连接探测
|
||||
- `http_2xx` - HTTP 服务探测
|
||||
- `http_post_2xx` - HTTP POST 探测
|
||||
- `tcp_connect_tls` - TLS 连接探测
|
||||
|
||||
**当前使用**:主要使用 `icmp` 模块进行网络连通性探测
|
||||
|
||||
---
|
||||
|
||||
### 7. config/targets.csv
|
||||
|
||||
**作用**:统一监控目标配置文件(推荐使用)
|
||||
|
||||
**格式**:
|
||||
```csv
|
||||
type,ip,device,group,network,device_type,model,location,username,password,onvif_port
|
||||
ping,8.8.8.8,google_dns,external,external,,,,,,
|
||||
onvif,192.168.1.100,,,front_door,camera,HIKVISION_DS-2CD2342WD-I,front_door,admin,password1,80
|
||||
```
|
||||
|
||||
**说明**:
|
||||
- `type` 字段:`ping` 或 `onvif`
|
||||
- 可以在一个文件中配置所有监控目标
|
||||
- 详细说明参考:`doc/TARGETS_CSV_GUIDE.md`
|
||||
|
||||
---
|
||||
|
||||
### 8. config/update-configs.sh
|
||||
|
||||
**作用**:从 CSV 文件生成 JSON 配置文件
|
||||
|
||||
**功能**:
|
||||
- 优先使用 `targets.csv`(统一配置)
|
||||
- 兼容旧格式(`devices.csv` + `ping-targets.csv`)
|
||||
- 自动生成 `onvif-targets.json` 和 `ping-targets.json`
|
||||
|
||||
**使用**:
|
||||
```bash
|
||||
cd config
|
||||
./update-configs.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 9. config/csv-to-targets.sh
|
||||
|
||||
**作用**:统一配置转换脚本
|
||||
|
||||
**功能**:
|
||||
- 从 `targets.csv` 读取配置
|
||||
- 根据 `type` 字段分离 ONVIF 和 Ping 目标
|
||||
- 生成对应的 JSON 配置文件
|
||||
|
||||
---
|
||||
|
||||
### 10. config/setup-remote-write.sh
|
||||
|
||||
**作用**:配置远程写入(已集成到部署脚本中)
|
||||
|
||||
---
|
||||
|
||||
### 11. config/test-connection.sh
|
||||
|
||||
**作用**:测试边缘节点与中央服务器的连接
|
||||
|
||||
**功能**:
|
||||
- 测试网络连通性
|
||||
- 测试 VictoriaMetrics 写入接口
|
||||
- 验证配置是否正确
|
||||
|
||||
---
|
||||
|
||||
## 配置文件关系图
|
||||
|
||||
```
|
||||
.env (环境变量)
|
||||
│
|
||||
▼
|
||||
prometheus-edge/prometheus.yml ──> 使用环境变量
|
||||
│
|
||||
├─> config/onvif-targets.json ──> ONVIF Exporter
|
||||
└─> config/ping-targets.json ────> Blackbox Exporter
|
||||
│
|
||||
└─> 从 targets.csv 生成
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 数据流向
|
||||
|
||||
```
|
||||
监控目标
|
||||
│
|
||||
├─> ONVIF 设备 ──> ONVIF Exporter ──┐
|
||||
│ │
|
||||
├─> 网络设备 ──> Blackbox Exporter ──┤
|
||||
│ │
|
||||
└─> 边缘节点自身 ──────────────────────┤
|
||||
│
|
||||
▼
|
||||
Prometheus Edge
|
||||
│
|
||||
│ remote_write
|
||||
▼
|
||||
VictoriaMetrics (中央服务器)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 配置文件修改指南
|
||||
|
||||
### 修改监控目标
|
||||
|
||||
1. 编辑 `config/targets.csv`
|
||||
2. 运行 `cd config && ./update-configs.sh`
|
||||
3. 等待 5 分钟自动重载,或重启容器
|
||||
|
||||
### 修改中央服务器地址
|
||||
|
||||
1. 编辑 `.env` 文件
|
||||
2. 重启容器:`docker compose restart prometheus-edge`
|
||||
|
||||
### 修改 Prometheus 配置
|
||||
|
||||
1. 编辑 `prometheus-edge/prometheus.yml`
|
||||
2. 重启容器:`docker compose restart prometheus-edge`
|
||||
|
||||
---
|
||||
|
||||
## 重要提示
|
||||
|
||||
1. **数据保留**:边缘节点只保留 1 小时数据,长期数据存储在中央服务器
|
||||
2. **资源限制**:配置了内存和 CPU 限制,适合边缘设备
|
||||
3. **端口冲突**:Prometheus Edge 使用 9092(避免与中央服务器冲突)
|
||||
4. **网络连通性**:确保可以访问中央服务器的 8428 端口
|
||||
5. **配置更新**:修改 CSV 后需要运行 `update-configs.sh` 生成 JSON
|
||||
|
||||
---
|
||||
- 改监控目标:编辑 `config/targets.csv` → `cd config && ./update-configs.sh`,必要时重启 prometheus-edge。
|
||||
- 改中央地址:编辑 `.env` → `docker compose restart prometheus-edge`。
|
||||
|
||||
## 相关文档
|
||||
|
||||
- **边缘节点配置**:`doc/EDGE_NODE_SETUP.md`
|
||||
- **监控目标说明**:`doc/MONITORING_TARGETS.md`
|
||||
- **统一配置指南**:`doc/TARGETS_CSV_GUIDE.md`
|
||||
- **系统架构**:`doc/ARCHITECTURE.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)
|
||||
|
||||
@@ -1,183 +0,0 @@
|
||||
# 边缘节点配置指南
|
||||
|
||||
## 在本机模拟边缘节点
|
||||
|
||||
### 前置条件
|
||||
|
||||
1. ✅ Docker 和 Docker Compose 已安装
|
||||
2. ✅ 中央服务器已部署并运行
|
||||
3. ✅ 网络连通性正常
|
||||
|
||||
### 配置步骤
|
||||
|
||||
#### 1. 配置中央服务器地址
|
||||
|
||||
编辑 `.env` 文件(如果不存在,从 `env.example` 复制):
|
||||
|
||||
```bash
|
||||
cd edge-agent
|
||||
cp env.example .env
|
||||
nano .env
|
||||
```
|
||||
|
||||
**重要配置**:
|
||||
```bash
|
||||
# 如果中央服务器在本机,使用本机IP或localhost
|
||||
CENTRAL_SERVER_HOST=192.168.2.21 # 或使用 localhost
|
||||
CENTRAL_SERVER_PORT=8428
|
||||
|
||||
# 边缘节点标识(每个节点唯一)
|
||||
EDGE_NODE_ID=workernode_1
|
||||
```
|
||||
|
||||
#### 2. 配置监控目标(统一配置)
|
||||
|
||||
**推荐使用统一的 `targets.csv` 配置文件**,可以在一个文件中同时配置 ONVIF 设备和网络 Ping 目标。
|
||||
|
||||
编辑 `config/targets.csv`:
|
||||
```csv
|
||||
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
|
||||
```
|
||||
|
||||
**说明**:
|
||||
- `type` 字段:`ping` 表示网络探测,`onvif` 表示 ONVIF 设备
|
||||
- 如果没有 ONVIF 设备,可以只配置 `ping` 类型的目标
|
||||
- 详细配置说明请参考:`doc/TARGETS_CSV_GUIDE.md`
|
||||
|
||||
**旧格式兼容**:
|
||||
如果使用旧的 `devices.csv` 和 `ping-targets.csv` 格式,脚本会自动识别并转换。
|
||||
|
||||
#### 3. 生成配置文件
|
||||
|
||||
```bash
|
||||
cd config
|
||||
chmod +x *.sh
|
||||
./update-configs.sh
|
||||
cd ..
|
||||
```
|
||||
|
||||
这会生成:
|
||||
- `config/onvif-targets.json` - ONVIF 设备配置
|
||||
- `config/ping-targets.json` - 网络探测配置
|
||||
|
||||
#### 4. 修改端口映射(避免冲突)
|
||||
|
||||
**重要**:本机已有中央服务器运行,需要修改端口避免冲突。
|
||||
|
||||
编辑 `docker-compose.yml`,修改 Prometheus Edge 的端口:
|
||||
|
||||
**解决**:修改 `docker-compose.yml` 中的端口映射为 9092
|
||||
|
||||
#### 2. 无法连接到中央服务器
|
||||
|
||||
**问题**:`CENTRAL_SERVER_HOST` 配置错误
|
||||
|
||||
**解决**:
|
||||
- 如果中央服务器在本机:使用 `localhost` 或本机 IP `192.168.2.21`
|
||||
- 如果中央服务器在其他机器:使用正确的 IP 地址
|
||||
- 确保防火墙开放 8428 端口
|
||||
|
||||
#### 3. ONVIF Exporter 报错
|
||||
|
||||
**问题**:没有真实的 ONVIF 设备或设备不可访问
|
||||
|
||||
**解决**:
|
||||
- 暂时可以忽略(不影响网络探测功能)
|
||||
- 或配置正确的设备信息
|
||||
|
||||
#### 4. 数据未推送到中央服务器
|
||||
|
||||
**检查**:
|
||||
1. 查看边缘节点日志:`docker compose logs prometheus-edge`
|
||||
2. 检查网络连通性:`curl http://192.168.2.21:8428/api/v1/write`
|
||||
3. 检查中央服务器 VictoriaMetrics 是否运行:`docker ps | grep victoria`
|
||||
|
||||
### 测试配置
|
||||
|
||||
#### 最小化测试配置
|
||||
|
||||
如果只想测试数据推送功能,可以使用最小配置:
|
||||
|
||||
1. **清空 ONVIF 设备**(`config/devices.csv` 留空)
|
||||
2. **只配置网络探测**(`config/ping-targets.csv` 添加几个公共 DNS)
|
||||
3. **部署并验证数据推送**
|
||||
|
||||
### 下一步
|
||||
|
||||
1. ✅ 边缘节点部署完成
|
||||
2. ✅ 数据成功推送到中央服务器
|
||||
3. 📊 在 Grafana 中创建仪表板查看数据
|
||||
4. 🔔 配置告警规则(告警规则会自动激活)
|
||||
|
||||
```yaml
|
||||
ports:
|
||||
- "9092:9090" # 改为 9092,避免与中央服务器冲突
|
||||
```
|
||||
|
||||
#### 5. 修改 Prometheus Edge 配置
|
||||
|
||||
编辑 `prometheus-edge/prometheus.yml`:
|
||||
|
||||
```yaml
|
||||
global:
|
||||
scrape_interval: 120s
|
||||
evaluation_interval: 120s
|
||||
external_labels:
|
||||
region: workernode_1 # 边缘节点标识
|
||||
user_group: "user-group-a" # 可选:添加用户组标签
|
||||
```
|
||||
|
||||
#### 6. 部署边缘节点
|
||||
|
||||
```bash
|
||||
bash deploy.sh
|
||||
```
|
||||
|
||||
### 验证配置
|
||||
|
||||
#### 1. 检查服务状态
|
||||
|
||||
```bash
|
||||
docker compose ps
|
||||
```
|
||||
|
||||
应该看到:
|
||||
- `prometheus-edge` - 运行中
|
||||
- `onvif-exporter` - 运行中(如果没有设备可能报错,但不影响)
|
||||
- `blackbox-exporter` - 运行中
|
||||
|
||||
#### 2. 检查数据推送
|
||||
|
||||
访问边缘节点 Prometheus:
|
||||
```bash
|
||||
http://localhost:9092
|
||||
```
|
||||
|
||||
查询指标:
|
||||
```promql
|
||||
up{job="network-ping"}
|
||||
```
|
||||
|
||||
#### 3. 检查中央服务器接收数据
|
||||
|
||||
访问中央服务器 Grafana:
|
||||
```bash
|
||||
http://localhost:3000
|
||||
```
|
||||
|
||||
在 Prometheus 数据源中查询:
|
||||
```promql
|
||||
up{region="workernode_1"}
|
||||
```
|
||||
|
||||
如果能看到数据,说明边缘节点已成功推送数据到中央服务器!
|
||||
|
||||
### 常见问题
|
||||
|
||||
#### 1. 端口冲突
|
||||
|
||||
**问题**:边缘节点 Prometheus 端口 9090 与中央服务器冲突
|
||||
@@ -1,246 +0,0 @@
|
||||
# 边缘节点监控目标说明
|
||||
|
||||
## 监控目标类型
|
||||
|
||||
边缘节点主要监控三类目标:
|
||||
|
||||
### 1. ONVIF 设备 (通过 ONVIF Exporter)
|
||||
|
||||
**监控对象**:
|
||||
- 📹 **摄像头 (Camera)** - IP 摄像头
|
||||
- 📼 **NVR (Network Video Recorder)** - 网络视频录像机
|
||||
- 其他支持 ONVIF 协议的设备
|
||||
|
||||
**监控指标**:
|
||||
- `up{job="onvif-devices"}` - 设备在线状态
|
||||
- `onvif_device_temperature` - 设备温度
|
||||
- `onvif_storage_usage_percent` - 存储使用率
|
||||
- 其他 ONVIF 设备指标
|
||||
|
||||
**配置位置**:
|
||||
- CSV 配置:`config/devices.csv`
|
||||
- JSON 配置:`config/onvif-targets.json`
|
||||
|
||||
**配置示例**:
|
||||
```csv
|
||||
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.50,nvr,HIKVISION_DS-7608NI-I2,server_rack,admin,password4,80
|
||||
```
|
||||
|
||||
**告警规则**:
|
||||
- `ONVIFDeviceDown` - 设备离线告警
|
||||
- `ONVIFDeviceHighTemperature` - 温度过高告警
|
||||
- `ONVIFDeviceLowStorage` - 存储空间不足告警
|
||||
|
||||
---
|
||||
|
||||
### 2. 网络设备 (通过 Blackbox Exporter)
|
||||
|
||||
**监控对象**:
|
||||
- 🌐 **路由器** - 网络网关设备
|
||||
- 🔌 **交换机** - 网络交换设备
|
||||
- 💻 **服务器** - 各种服务器设备
|
||||
- 🌍 **外部服务** - DNS、网站等外部服务
|
||||
- 📡 **网络设备** - 任何可通过 ICMP ping 的设备
|
||||
|
||||
**监控方式**:
|
||||
- **ICMP Ping** - 网络连通性探测
|
||||
- **TCP 连接** - TCP 端口连通性
|
||||
- **HTTP 探测** - HTTP 服务可用性
|
||||
|
||||
**监控指标**:
|
||||
- `probe_success{job="network-ping"}` - Ping 成功状态 (0/1)
|
||||
- `probe_duration_seconds{job="network-ping"}` - Ping 延迟时间
|
||||
- `probe_http_status_code` - HTTP 状态码
|
||||
- `probe_tcp_connect_success` - TCP 连接成功状态
|
||||
|
||||
**配置位置**:
|
||||
- CSV 配置:`config/ping-targets.csv`
|
||||
- JSON 配置:`config/ping-targets.json`
|
||||
|
||||
**配置示例**:
|
||||
```csv
|
||||
ip,device,group,network
|
||||
192.168.1.1,main_router,network,internal
|
||||
8.8.8.8,google_dns,external,external
|
||||
1.1.1.1,cloudflare_dns,external,external
|
||||
```
|
||||
|
||||
**告警规则**:
|
||||
- `NetworkDeviceDown` - 网络设备离线告警
|
||||
- `HighNetworkLatency` - 网络延迟过高告警
|
||||
|
||||
---
|
||||
|
||||
### 3. 边缘节点自身 (Prometheus Edge)
|
||||
|
||||
**监控对象**:
|
||||
- 边缘 Prometheus 服务自身
|
||||
|
||||
**监控指标**:
|
||||
- `up{job="prometheus-edge"}` - Prometheus 服务状态
|
||||
- `prometheus_tsdb_*` - 时序数据库指标
|
||||
- `prometheus_config_*` - 配置相关指标
|
||||
|
||||
**配置位置**:
|
||||
- `prometheus-edge/prometheus.yml` (自动配置)
|
||||
|
||||
---
|
||||
|
||||
## 监控目标汇总表
|
||||
|
||||
| 监控类型 | Job名称 | Exporter | 配置文件 | 监控间隔 | 告警规则 |
|
||||
|---------|---------|----------|----------|----------|----------|
|
||||
| ONVIF设备 | `onvif-devices` | ONVIF Exporter | `config/onvif-targets.json` | 120秒 | ONVIFDeviceDown<br>ONVIFDeviceHighTemperature<br>ONVIFDeviceLowStorage |
|
||||
| 网络设备 | `network-ping` | Blackbox Exporter | `config/ping-targets.json` | 300秒 | NetworkDeviceDown<br>HighNetworkLatency |
|
||||
| 边缘节点自身 | `prometheus-edge` | Prometheus自身 | 自动配置 | 60秒 | - |
|
||||
|
||||
---
|
||||
|
||||
## 典型监控场景
|
||||
|
||||
### 场景 1:家庭/办公室监控
|
||||
|
||||
**ONVIF 设备**:
|
||||
- 前门摄像头
|
||||
- 后院摄像头
|
||||
- 客厅摄像头
|
||||
- NVR 录像机
|
||||
|
||||
**网络设备**:
|
||||
- 主路由器 (192.168.1.1)
|
||||
- 交换机
|
||||
- 内部服务器
|
||||
|
||||
### 场景 2:企业监控
|
||||
|
||||
**ONVIF 设备**:
|
||||
- 多个区域的摄像头
|
||||
- 多个 NVR 设备
|
||||
- 不同品牌的摄像头
|
||||
|
||||
**网络设备**:
|
||||
- 核心路由器
|
||||
- 汇聚交换机
|
||||
- 接入交换机
|
||||
- 关键服务器
|
||||
- 外部 DNS 服务
|
||||
|
||||
### 场景 3:最小化测试
|
||||
|
||||
**ONVIF 设备**:
|
||||
- 无(留空用于测试)
|
||||
|
||||
**网络设备**:
|
||||
- 公共 DNS (8.8.8.8, 1.1.1.1)
|
||||
- 本地路由器(如果可访问)
|
||||
|
||||
---
|
||||
|
||||
## 配置建议
|
||||
|
||||
### ONVIF 设备配置
|
||||
|
||||
1. **设备信息**:
|
||||
- IP 地址
|
||||
- 设备类型 (camera/nvr)
|
||||
- 型号
|
||||
- 位置标签
|
||||
- 用户名和密码
|
||||
- ONVIF 端口(通常 80 或 8080)
|
||||
|
||||
2. **安全建议**:
|
||||
- 使用强密码
|
||||
- 定期更换密码
|
||||
- 限制网络访问
|
||||
|
||||
### 网络设备配置
|
||||
|
||||
1. **内部设备**:
|
||||
- 路由器、交换机等关键网络设备
|
||||
- 重要服务器
|
||||
- 网络打印机等
|
||||
|
||||
2. **外部服务**:
|
||||
- 公共 DNS (8.8.8.8, 1.1.1.1)
|
||||
- 关键外部服务
|
||||
- 用于测试网络连通性
|
||||
|
||||
3. **标签使用**:
|
||||
- `group` - 设备分组
|
||||
- `network` - 网络类型 (internal/external)
|
||||
- `device` - 设备名称
|
||||
|
||||
---
|
||||
|
||||
## 数据流向
|
||||
|
||||
```
|
||||
监控目标
|
||||
│
|
||||
├─ ONVIF 设备 ──> ONVIF Exporter ──┐
|
||||
│ │
|
||||
├─ 网络设备 ──> Blackbox Exporter ──┤
|
||||
│ │
|
||||
└─ 边缘节点自身 ──────────────────────┤
|
||||
│
|
||||
▼
|
||||
Prometheus Edge
|
||||
│
|
||||
│ remote_write
|
||||
▼
|
||||
VictoriaMetrics (中央服务器)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 验证监控目标
|
||||
|
||||
### 1. 检查 ONVIF 设备
|
||||
|
||||
在边缘节点 Prometheus 查询:
|
||||
```promql
|
||||
up{job="onvif-devices"}
|
||||
```
|
||||
|
||||
### 2. 检查网络设备
|
||||
|
||||
在边缘节点 Prometheus 查询:
|
||||
```promql
|
||||
probe_success{job="network-ping"}
|
||||
```
|
||||
|
||||
### 3. 检查数据推送
|
||||
|
||||
在中央服务器 Grafana 查询:
|
||||
```promql
|
||||
up{region="workernode_1"}
|
||||
probe_success{region="workernode_1"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 没有 ONVIF 设备怎么办?
|
||||
|
||||
A: 可以留空 ONVIF 设备配置,只使用网络探测功能进行测试。
|
||||
|
||||
### Q: 如何添加新的监控目标?
|
||||
|
||||
A:
|
||||
1. 编辑对应的 CSV 文件 (`devices.csv` 或 `ping-targets.csv`)
|
||||
2. 运行 `cd config && ./update-configs.sh`
|
||||
3. 等待 5 分钟自动重载,或重启 `prometheus-edge` 容器
|
||||
|
||||
### Q: 监控目标太多会影响性能吗?
|
||||
|
||||
A:
|
||||
- ONVIF 设备:每个设备约 1-2 秒查询时间
|
||||
- 网络 Ping:每个目标约 0.1-0.5 秒
|
||||
- 建议:单节点不超过 100 个目标
|
||||
|
||||
### Q: 如何监控 HTTPS 服务?
|
||||
|
||||
A: 修改 `blackbox/config.yml`,添加 HTTPS 探测模块,然后在 `ping-targets.json` 中配置。
|
||||
@@ -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_NODE_SETUP.md** 及 `edge-agent/docker-compose.yml`。
|
||||
具体边缘配置与 compose 变更见 **[EDGE_AGENT_CONFIG.md](EDGE_AGENT_CONFIG.md)** 及 `edge-agent/docker-compose.yml`。
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
# 项目缺陷分析
|
||||
|
||||
基于 README、central-server 及相关文档的审查结果。
|
||||
|
||||
---
|
||||
|
||||
## 一、严重缺陷
|
||||
|
||||
### 1. Grafana 无法展示边缘节点数据(数据源缺失)【已修复】
|
||||
|
||||
**现象**:边缘节点通过 remote_write 将指标推送到 **VictoriaMetrics**,此前 Grafana 仅配置了 **Prometheus** 数据源。
|
||||
|
||||
**数据流与配置顺序**:
|
||||
- **边缘主动上报**:边缘节点上的 Prometheus/Agent 配置了 `remote_write` 指向中央服务器的 VictoriaMetrics(`http://中央IP:8428/api/v1/write`),会主动推送指标到中央。
|
||||
- **必须先配置边缘**:只有在边缘节点里配置好“中央服务器地址 + 8428 端口”并启动后,数据才会出现在 VictoriaMetrics 中;中央仅提供接收端,不会去拉边缘。
|
||||
- **Grafana 看到边缘数据**:中央已增加 **VictoriaMetrics** 数据源(`grafana/provisioning/datasources/victoriametrics.yml`)后,在 Grafana 中选用 “VictoriaMetrics” 数据源即可查询这些上报上来的边缘数据;无需再配“边缘数据”本身,只需边缘按文档配置上报。
|
||||
|
||||
**已做修改**:
|
||||
- 在 `grafana/provisioning/datasources/` 中新增 **victoriametrics.yml**,数据源 URL 为 `http://victoria-metrics:8428`。
|
||||
- 边缘数据可见的前提:边缘已配置并运行,且 remote_write 指向本中央 VM(参见 `doc/EDGE_NODE_SETUP.md`、`doc/EDGE_AGENT_CONFIG.md`)。
|
||||
|
||||
---
|
||||
|
||||
### 2. docker-compose 网络名未设置默认值导致部署失败
|
||||
|
||||
**现象**:`docker-compose.yml` 中默认网络名为 `${NETWORK_NAME}`,未提供默认值。
|
||||
|
||||
```yaml
|
||||
networks:
|
||||
default:
|
||||
name: ${NETWORK_NAME}
|
||||
```
|
||||
|
||||
**依据**:`deploy.sh` 只 export 了部分变量,**未 export `NETWORK_NAME`**。若用户“使用默认配置”且没有 `.env`(脚本提示“未找到 .env 和 env.example”时),`NETWORK_NAME` 为空,Compose 会使用空字符串作为网络名,可能导致创建失败或行为异常。
|
||||
|
||||
**建议**:
|
||||
- 在 `deploy.sh` 中为 `NETWORK_NAME` 设置默认值并 export,例如:
|
||||
`NETWORK_NAME=${NETWORK_NAME:-central_default}` 或与 Traefik 一致时 `NETWORK_NAME=${NETWORK_NAME:-traefik}`
|
||||
- 或在 `docker-compose.yml` 中写为:`name: ${NETWORK_NAME:-central_default}`
|
||||
|
||||
---
|
||||
|
||||
### 3. VictoriaMetrics 容器内监听端口与映射不一致
|
||||
|
||||
**现象**:容器内通过环境变量改变监听端口,与端口映射不一致。
|
||||
|
||||
**依据**(`docker-compose.yml`):
|
||||
|
||||
```yaml
|
||||
ports:
|
||||
- "${VICTORIAMETRICS_PORT:-8428}:8428"
|
||||
command:
|
||||
- "--httpListenAddr=:${VICTORIAMETRICS_PORT:-8428}"
|
||||
```
|
||||
|
||||
- 端口映射为「主机 `${VICTORIAMETRICS_PORT}` → 容器 **8428**」
|
||||
- 若用户设置 `VICTORIAMETRICS_PORT=8430`,容器会监听 **8430**,而映射期望容器监听 **8428**,导致主机 8430 无法正确访问服务。
|
||||
|
||||
**建议**:容器内应固定监听 8428,仅用环境变量控制主机端口。例如:
|
||||
|
||||
```yaml
|
||||
command:
|
||||
- "--httpListenAddr=:8428"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、中等问题
|
||||
|
||||
### 4. Alertmanager Webhook 在容器内不可达
|
||||
|
||||
**现象**:`alertmanager/alertmanager.yml` 中 webhook 为 `http://127.0.0.1:5001/`。
|
||||
|
||||
在容器内 `127.0.0.1` 指向 Alertmanager 自身,无法访问宿主机上的 webhook 服务,告警无法送达。
|
||||
|
||||
**建议**:
|
||||
- Linux 下可使用 `http://host.docker.internal:5001/`(若 Docker 支持)
|
||||
- 或使用宿主机在 Docker 网桥上的 IP,并在文档中说明需替换为实际地址
|
||||
|
||||
---
|
||||
|
||||
### 5. 默认启用 Traefik 且为示例域名,不利于“快速开始”
|
||||
|
||||
**现象**:`env.example` 中 `TRAEFIK_ENABLED=true`,且域名为 `grafana.example.com` 等。README 的“快速开始”是 `http://localhost:3000`。
|
||||
|
||||
**结果**:新用户若直接 `cp env.example .env` 并部署,会默认走 Traefik + 示例域名,本地浏览器无法用 localhost 访问,与文档体验不一致。
|
||||
|
||||
**建议**:
|
||||
- `env.example` 中默认设为 `TRAEFIK_ENABLED=false`,便于本地快速开始
|
||||
- 或在 README/CONFIGURATION 中明确写:本地试用请将 `TRAEFIK_ENABLED=false`,并说明 Traefik 为可选
|
||||
|
||||
---
|
||||
|
||||
### 6. 部署脚本未导出 NETWORK_NAME
|
||||
|
||||
**现象**:`deploy.sh` 中通过 `set -a; source .env` 可导出 .env 中的变量,但若没有 .env,或 .env 中未写 `NETWORK_NAME`,则 Compose 收到的 `NETWORK_NAME` 可能为空。
|
||||
|
||||
**建议**:在 deploy.sh 的“设置默认值”或 export 段落中显式设置并 export:
|
||||
|
||||
```bash
|
||||
NETWORK_NAME=${NETWORK_NAME:-traefik}
|
||||
export NETWORK_NAME
|
||||
```
|
||||
|
||||
与第 2 点一起修复,可避免无 .env 或漏配时的部署问题。
|
||||
|
||||
---
|
||||
|
||||
## 三、文档/一致性问题
|
||||
|
||||
### 7. 架构文档与实现不一致
|
||||
|
||||
**现象**:`doc/ARCHITECTURE.md` 描述“Prometheus 从 VictoriaMetrics 读取数据”,但当前 `prometheus.yml` 仅有 **remote_write** 到 VictoriaMetrics,没有 **remote_read**。
|
||||
|
||||
**建议**:修改文档为“Prometheus 将本地抓取数据 remote_write 到 VictoriaMetrics;边缘数据仅存在于 VictoriaMetrics”,并说明 Grafana 如需查边缘数据应使用 VictoriaMetrics 数据源(与第 1 点修复一致)。
|
||||
|
||||
---
|
||||
|
||||
### 8. 示例密码与安全建议不一致
|
||||
|
||||
**现象**:README 和配置中默认管理员密码为 `admin123`,CONFIGURATION.md 建议“首次部署后请立即修改”“生产环境必须修改”。
|
||||
|
||||
**建议**:在 README 快速开始处增加一句:“默认密码仅用于首次登录,生产环境请立即修改”,并在部署成功输出中再次提醒。
|
||||
|
||||
---
|
||||
|
||||
## 四、小结
|
||||
|
||||
| 优先级 | 缺陷 | 建议 |
|
||||
|--------|------|------|
|
||||
| ~~高~~ | ~~Grafana 缺少 VictoriaMetrics 数据源~~ | ✅ 已增加 `victoriametrics.yml`;边缘需先配置 remote_write 指向中央 VM |
|
||||
| 高 | Compose 网络名无默认值 | 为 NETWORK_NAME 设默认并 export |
|
||||
| 高 | VictoriaMetrics 容器监听端口与端口映射不一致 | 容器内固定监听 8428 |
|
||||
| 中 | Alertmanager webhook 127.0.0.1 在容器内无效 | 改为 host.docker.internal 或宿主机 IP并文档说明 |
|
||||
| 中 | 默认启用 Traefik + 示例域名 | 默认关闭 Traefik 或文档明确本地试用步骤 |
|
||||
| 中 | deploy 未导出 NETWORK_NAME | 在 deploy.sh 中设置并 export |
|
||||
| 低 | 架构文档与实现不符 | 更新 ARCHITECTURE.md |
|
||||
| 低 | 默认密码与安全建议 | 在 README 和部署输出中强调修改密码 |
|
||||
|
||||
以上为当前发现的主要缺陷与改进建议,优先修复前三条可显著提升部署成功率和“边缘+中央”统一监控的可用性。
|
||||
@@ -6,13 +6,14 @@
|
||||
|
||||
## 部署顺序(必读)
|
||||
|
||||
整体顺序:**先中央,后边缘**。边缘向中央主动上报数据,中央必须先就绪。
|
||||
整体顺序:**先中央,后边缘,再按需标注拓扑**。边缘向中央主动上报数据,中央必须先就绪。
|
||||
|
||||
| 步骤 | 部署什么 | 做什么 | 验证 |
|
||||
|------|----------|--------|------|
|
||||
| 步骤 | 部署 / 操作对象 | 做什么 | 验证 |
|
||||
|------|------------------|--------|------|
|
||||
| **第一步** | 中央服务器 | 部署 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)、[ALERTMANAGER_CONFIG.md](ALERTMANAGER_CONFIG.md) 验证 |
|
||||
| **第三步** | 多用户 / 告警(可选) | 配置 Grafana 组织与用户、Alertmanager 通知 | 按 [USER_MANAGEMENT.md](USER_MANAGEMENT.md)、[ALERTING.md](ALERTING.md) 验证 |
|
||||
| **第四步** | 拓扑标注助手 topology-editor(可选) | 上传/编辑/下载 `targets.csv`,用 GPS 与天地图给设备打点并维护拓扑关系 | 在 Grafana Geomap 中按经纬度与 parent/uplink_type 展示网络拓扑 |
|
||||
|
||||
---
|
||||
|
||||
@@ -55,15 +56,42 @@ bash deploy.sh
|
||||
|
||||
- **验证**:边缘 Prometheus UI http://localhost:9092(端口 9092 避免与中央 9091 冲突)。在中央 Grafana 中**选择数据源「VictoriaMetrics」**,查询如 `up{job="network-ping"}` 可见边缘数据;中央自身指标在数据源「Prometheus」。
|
||||
- **摄像头/ONVIF**:默认不拉取 ONVIF 镜像(公共镜像不存在)。监控摄像头可选:**SNMP Exporter**、**Frigate**、**Blackbox 探测** 或自建 ONVIF 镜像,见 **[ONVIF_ALTERNATIVES.md](ONVIF_ALTERNATIVES.md)**。
|
||||
- **监控目标**:编辑 `edge-agent/config/targets.csv`(Ping / ONVIF),详见 [TARGETS_CSV_GUIDE.md](TARGETS_CSV_GUIDE.md)。
|
||||
- **详细**:[EDGE_NODE_SETUP.md](EDGE_NODE_SETUP.md)、[EDGE_AGENT_CONFIG.md](EDGE_AGENT_CONFIG.md)、[DEPLOYMENT_GUIDE.md](DEPLOYMENT_GUIDE.md)。
|
||||
- **监控目标**:编辑 `edge-agent/config/targets.csv`(Ping / ONVIF / 拓扑),详见 [TARGETS_AND_MONITORING.md](TARGETS_AND_MONITORING.md)。
|
||||
- **详细**:[EDGE_AGENT_CONFIG.md](EDGE_AGENT_CONFIG.md)、[DEPLOYMENT_GUIDE.md](DEPLOYMENT_GUIDE.md)。
|
||||
|
||||
---
|
||||
|
||||
### 第三步(可选):多用户与告警
|
||||
|
||||
- **Grafana 多用户**:`cd central-server/grafana && bash setup-users.sh`,然后按 [USER_MANAGEMENT.md](USER_MANAGEMENT.md) 配置组织与数据源。
|
||||
- **告警通知**:编辑 `central-server/alertmanager/alertmanager.yml` 配置接收端;告警规则见 [ALERT_RULES_EXPLANATION.md](ALERT_RULES_EXPLANATION.md)、[ALERTMANAGER_CONFIG.md](ALERTMANAGER_CONFIG.md)。
|
||||
- **告警通知**:编辑 `central-server/alertmanager/alertmanager.yml` 配置接收端;告警规则与说明见 [ALERTING.md](ALERTING.md)。
|
||||
|
||||
---
|
||||
|
||||
### 第四步(可选):拓扑标注助手 / 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://<central>: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)。
|
||||
|
||||
---
|
||||
|
||||
@@ -82,8 +110,8 @@ bash deploy.sh
|
||||
| [CENTRAL_SERVER_CONFIG.md](CENTRAL_SERVER_CONFIG.md) | 中央服务器配置文件说明 |
|
||||
| [../central-server/CONFIGURATION.md](../central-server/CONFIGURATION.md) | 中央服务器环境变量(.env)说明 |
|
||||
| [EDGE_AGENT_CONFIG.md](EDGE_AGENT_CONFIG.md) | 边缘节点配置文件说明 |
|
||||
| [TARGETS_CSV_GUIDE.md](TARGETS_CSV_GUIDE.md) | 边缘监控目标 targets.csv 格式与示例 |
|
||||
| [MONITORING_TARGETS.md](MONITORING_TARGETS.md) | ONVIF / 网络探测等监控目标说明 |
|
||||
| [TARGETS_AND_MONITORING.md](TARGETS_AND_MONITORING.md) | 监控目标与 targets.csv(格式、脚本、数据流) |
|
||||
| [TIANDITU_CONFIG.md](TIANDITU_CONFIG.md) | 天地图配置(底图 vec_w、标识图 cva_w、WMTS 地址) |
|
||||
| [ONVIF_ALTERNATIVES.md](ONVIF_ALTERNATIVES.md) | **摄像头/ONVIF 监控替代方案**(SNMP、Frigate、Blackbox、自建) |
|
||||
|
||||
### 用户与告警
|
||||
@@ -91,30 +119,23 @@ bash deploy.sh
|
||||
| 文档 | 说明 |
|
||||
|------|------|
|
||||
| [USER_MANAGEMENT.md](USER_MANAGEMENT.md) | Grafana 多用户、组织与数据隔离 |
|
||||
| [ALERT_RULES_EXPLANATION.md](ALERT_RULES_EXPLANATION.md) | 告警规则说明 |
|
||||
| [ALERTMANAGER_CONFIG.md](ALERTMANAGER_CONFIG.md) | Alertmanager 配置与通知渠道 |
|
||||
| [ALERTING.md](ALERTING.md) | 告警规则、Alertmanager 配置与通知渠道 |
|
||||
|
||||
### 部署与运维
|
||||
|
||||
| 文档 | 说明 |
|
||||
|------|------|
|
||||
| [DEPLOYMENT_GUIDE.md](DEPLOYMENT_GUIDE.md) | 完整部署步骤、检查清单、验证与常见问题 |
|
||||
| [EDGE_NODE_SETUP.md](EDGE_NODE_SETUP.md) | 边缘节点配置与验证(含本机同机) |
|
||||
| [TROUBLESHOOTING.md](TROUBLESHOOTING.md) | 故障排查 |
|
||||
| [BEST_PRACTICES.md](BEST_PRACTICES.md) | 最佳实践与生产环境建议 |
|
||||
|
||||
### 参考
|
||||
|
||||
| 文档 | 说明 |
|
||||
|------|------|
|
||||
| [PROJECT_DEFECTS.md](PROJECT_DEFECTS.md) | 项目缺陷与修复建议 |
|
||||
|
||||
---
|
||||
|
||||
## 快速导航
|
||||
|
||||
- **第一次部署**:按上面「部署顺序」先做第一步,再做第二步。
|
||||
- **只改中央配置**:看 [CENTRAL_SERVER_CONFIG.md](CENTRAL_SERVER_CONFIG.md)、[CONFIGURATION.md](../central-server/CONFIGURATION.md)。
|
||||
- **只改边缘 / 监控目标**:看 [EDGE_NODE_SETUP.md](EDGE_NODE_SETUP.md)、[TARGETS_CSV_GUIDE.md](TARGETS_CSV_GUIDE.md)。
|
||||
- **多用户 / 告警**:看 [USER_MANAGEMENT.md](USER_MANAGEMENT.md)、[ALERTMANAGER_CONFIG.md](ALERTMANAGER_CONFIG.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)。
|
||||
|
||||
75
doc/TARGETS_AND_MONITORING.md
Normal file
75
doc/TARGETS_AND_MONITORING.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# 监控目标与 targets.csv
|
||||
|
||||
边缘监控目标统一由 `edge-agent/config/targets.csv` 配置,经 `update-configs.sh` 生成 `onvif-targets.json`、`ping-targets.json`,并可生成拓扑 GeoJSON 供 Grafana Geomap 使用。
|
||||
|
||||
---
|
||||
|
||||
## targets.csv 格式
|
||||
|
||||
表头(列顺序固定):
|
||||
|
||||
```text
|
||||
type,ip,name,role,parent,uplink_type,network,device_type,model,location,username,password,onvif_port,lat,lon
|
||||
```
|
||||
|
||||
| 字段 | 说明 | 适用类型 |
|
||||
|------|------|----------|
|
||||
| type | `ping` / `onvif` / `topology` | 必填 |
|
||||
| ip | IP(topology 哑设备可空) | ping, onvif |
|
||||
| name | 节点唯一名,用于拓扑 parent 引用 | 必填 |
|
||||
| role | 如 core_switch, access_switch, camera, wireless_bridge, media_converter | 可选 |
|
||||
| parent | 上联设备 name,用于画拓扑连线 | 可选 |
|
||||
| uplink_type | 与上联链路类型:fiber / copper / wireless | 可选 |
|
||||
| network | 如 internal / external | 可选 |
|
||||
| device_type, model, location | 设备描述;onvif 必填 location、账号等 | onvif |
|
||||
| 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` 画点与连线。
|
||||
|
||||
---
|
||||
|
||||
## 示例
|
||||
|
||||
```csv
|
||||
type,ip,name,role,parent,uplink_type,network,device_type,model,location,username,password,onvif_port,lat,lon
|
||||
ping,192.168.2.1,core_sw_1,core_switch,,,internal,,,,,,,22.54,113.98
|
||||
ping,8.8.8.8,google_dns,dns,core_sw_1,fiber,external,,,,,,,22.55,113.99
|
||||
topology,,dumb_sw_1,access_switch,core_sw_1,copper,internal,switch,,building_A,,,,22.543,113.988
|
||||
onvif,192.168.1.100,camera_front,camera,dumb_sw_1,copper,internal,camera,HIKVISION,front_door,admin,pass,80,22.123,113.567
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 生成配置与拓扑
|
||||
|
||||
```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 可区分线型)。
|
||||
|
||||
---
|
||||
|
||||
## 监控类型与数据流
|
||||
|
||||
| 类型 | Job | Exporter | 配置文件 |
|
||||
|------|-----|----------|----------|
|
||||
| 网络 Ping | network-ping | Blackbox | ping-targets.json |
|
||||
| ONVIF | onvif-devices | ONVIF Exporter | onvif-targets.json |
|
||||
| 边缘自身 | prometheus-edge | Prometheus | 内置 |
|
||||
|
||||
数据流:目标 → Exporter → prometheus-edge 抓取 → remote_write → 中央 VictoriaMetrics。Grafana 查边缘数据需选 **VictoriaMetrics** 数据源。
|
||||
|
||||
---
|
||||
|
||||
## 验证
|
||||
|
||||
- 边缘 Prometheus(http://localhost:9092):`probe_success{job="network-ping"}`、`onvif_device_up`。
|
||||
- 中央 Grafana(VictoriaMetrics 数据源):`probe_success{region="workernode_1"}` 等。
|
||||
@@ -1,131 +0,0 @@
|
||||
# targets.csv 配置指南
|
||||
|
||||
## 概述
|
||||
|
||||
`targets.csv` 是统一的监控目标配置文件,可以在一个文件中同时配置 ONVIF 设备和网络 Ping 目标。
|
||||
|
||||
## 文件格式
|
||||
|
||||
```csv
|
||||
type,ip,device,group,network,device_type,model,location,username,password,onvif_port
|
||||
```
|
||||
|
||||
### 字段说明
|
||||
|
||||
| 字段 | 说明 | 必需 | 适用类型 |
|
||||
|------|------|------|----------|
|
||||
| `type` | 目标类型:`ping` 或 `onvif` | ✅ | 所有 |
|
||||
| `ip` | IP 地址 | ✅ | 所有 |
|
||||
| `device` | 设备名称 | ❌ | ping |
|
||||
| `group` | 设备分组 | ❌ | ping |
|
||||
| `network` | 网络类型(internal/external) | ❌ | ping |
|
||||
| `device_type` | 设备类型(camera/nvr) | ✅ | onvif |
|
||||
| `model` | 设备型号 | ✅ | onvif |
|
||||
| `location` | 设备位置 | ✅ | onvif |
|
||||
| `username` | 用户名 | ✅ | onvif |
|
||||
| `password` | 密码 | ✅ | onvif |
|
||||
| `onvif_port` | ONVIF 端口(默认80) | ❌ | onvif |
|
||||
|
||||
## 配置示例
|
||||
|
||||
### Ping 目标配置
|
||||
|
||||
```csv
|
||||
type,ip,device,group,network,device_type,model,location,username,password,onvif_port
|
||||
ping,192.168.1.1,main_router,network,internal,,,,,,
|
||||
ping,8.8.8.8,google_dns,external,external,,,,,,
|
||||
ping,1.1.1.1,cloudflare_dns,external,external,,,,,,
|
||||
```
|
||||
|
||||
**说明**:
|
||||
- `type` 设置为 `ping`
|
||||
- 填写 `ip`, `device`, `group`, `network`
|
||||
- ONVIF 相关字段留空
|
||||
|
||||
### ONVIF 设备配置
|
||||
|
||||
```csv
|
||||
type,ip,device,group,network,device_type,model,location,username,password,onvif_port
|
||||
onvif,192.168.1.100,,,front_door,camera,HIKVISION_DS-2CD2342WD-I,front_door,admin,password1,80
|
||||
onvif,192.168.1.101,,,back_yard,camera,DAHUA_IPC-HFW1230S,back_yard,admin,password2,80
|
||||
onvif,192.168.1.50,,,server_rack,nvr,HIKVISION_DS-7608NI-I2,server_rack,admin,password4,80
|
||||
```
|
||||
|
||||
**说明**:
|
||||
- `type` 设置为 `onvif`
|
||||
- 填写 `ip`, `device_type`, `model`, `location`, `username`, `password`
|
||||
- `onvif_port` 默认为 80,如果不是 80 需要填写
|
||||
- Ping 相关字段(device, group, network)可以留空
|
||||
|
||||
### 混合配置示例
|
||||
|
||||
```csv
|
||||
type,ip,device,group,network,device_type,model,location,username,password,onvif_port
|
||||
ping,192.168.1.1,main_router,network,internal,,,,,,
|
||||
ping,8.8.8.8,google_dns,external,external,,,,,,
|
||||
onvif,192.168.1.100,,,front_door,camera,HIKVISION_DS-2CD2342WD-I,front_door,admin,password1,80
|
||||
onvif,192.168.1.101,,,back_yard,camera,DAHUA_IPC-HFW1230S,back_yard,admin,password2,80
|
||||
```
|
||||
|
||||
## 使用注释
|
||||
|
||||
可以在 CSV 文件中使用 `#` 开头的注释行:
|
||||
|
||||
```csv
|
||||
# 这是注释行
|
||||
type,ip,device,group,network,device_type,model,location,username,password,onvif_port
|
||||
ping,8.8.8.8,google_dns,external,external,,,,,,
|
||||
# onvif,192.168.1.100,,,front_door,camera,HIKVISION_DS-2CD2342WD-I,front_door,admin,password1,80
|
||||
```
|
||||
|
||||
## 生成配置文件
|
||||
|
||||
编辑 `targets.csv` 后,运行:
|
||||
|
||||
```bash
|
||||
cd config
|
||||
./update-configs.sh
|
||||
```
|
||||
|
||||
这会生成:
|
||||
- `onvif-targets.json` - ONVIF 设备配置
|
||||
- `ping-targets.json` - Ping 目标配置
|
||||
|
||||
## 向后兼容
|
||||
|
||||
如果存在旧的配置文件:
|
||||
- `devices.csv` - 仍会被识别并转换
|
||||
- `ping-targets.csv` - 仍会被识别并转换
|
||||
|
||||
但建议统一使用 `targets.csv` 进行配置。
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **字段顺序**:必须按照 CSV 头部定义的顺序填写
|
||||
2. **空字段**:不需要的字段可以留空,但逗号不能省略
|
||||
3. **特殊字符**:如果字段值包含逗号,需要用引号包裹
|
||||
4. **密码安全**:密码以明文存储,请确保文件权限安全
|
||||
|
||||
## 迁移指南
|
||||
|
||||
### 从旧格式迁移
|
||||
|
||||
**旧格式**(`devices.csv` + `ping-targets.csv`):
|
||||
```csv
|
||||
# devices.csv
|
||||
ip,device_type,model,location,username,password,onvif_port
|
||||
192.168.1.100,camera,HIKVISION_DS-2CD2342WD-I,front_door,admin,password1,80
|
||||
|
||||
# ping-targets.csv
|
||||
ip,device,group,network
|
||||
8.8.8.8,google_dns,external,external
|
||||
```
|
||||
|
||||
**新格式**(`targets.csv`):
|
||||
```csv
|
||||
type,ip,device,group,network,device_type,model,location,username,password,onvif_port
|
||||
onvif,192.168.1.100,,,front_door,camera,HIKVISION_DS-2CD2342WD-I,front_door,admin,password1,80
|
||||
ping,8.8.8.8,google_dns,external,external,,,,,,
|
||||
```
|
||||
|
||||
只需将两个文件的内容合并到 `targets.csv`,并添加 `type` 列即可。
|
||||
129
doc/TIANDITU_CONFIG.md
Normal file
129
doc/TIANDITU_CONFIG.md
Normal file
@@ -0,0 +1,129 @@
|
||||
# 天地图配置说明
|
||||
|
||||
拓扑标注助手使用**天地图**作为地图校验底图,便于在浏览器中点击修正设备经纬度。天地图提供**底图**与**标识图(中文注记)**两个图层,可单独或叠加使用。
|
||||
|
||||
**天地图密钥**:使用瓦片缓存或 Grafana Geomap 时,密钥统一配置在 **central-server/.env** 的 **TIANDITU_TK** 变量中,由 tile-cache 服务读取,不在浏览器或 Grafana 中填写。
|
||||
|
||||
---
|
||||
|
||||
## 1. 在拓扑标注助手中使用
|
||||
|
||||
- 打开拓扑标注助手:`http://<中央服务器>:4080`
|
||||
- 在「地图校验」区域填写 **天地图 TK**(密钥),点击「加载天地图」即可加载底图并点击地图修正坐标。
|
||||
- TK 会保存在浏览器本地(localStorage),同一设备填一次即可。
|
||||
- **申请密钥**:登录 [天地图开放平台](https://console.tianditu.gov.cn/) 注册并创建应用,获取 **tk** 参数。
|
||||
|
||||
当前前端通过天地图 JavaScript API 加载地图;若需在其它系统(如 Grafana Geomap)中复用天地图,可使用下方 WMTS 地址。
|
||||
|
||||
---
|
||||
|
||||
## 2. 两个图层说明
|
||||
|
||||
| 图层 | 用途 | 说明 |
|
||||
|------|------|------|
|
||||
| **底图 (vec_w)** | 矢量底图 | 道路、建筑、水系等底图,WGS84 经纬度 |
|
||||
| **标识图 (cva_w)** | 中文注记 | 地名、道路名等文字标注,叠加在底图之上 |
|
||||
|
||||
二者叠加后即「带中文注记的天地图」;仅做点位校验时只用底图即可,需要地名时可叠加标识图。
|
||||
|
||||
---
|
||||
|
||||
## 3. WMTS 地址(底图 + 标识图)
|
||||
|
||||
若在 Grafana、其它 GIS 或自研前端中通过 WMTS 接入天地图,可使用以下地址。请将 `tk=您的密钥` 替换为在 [天地图开放平台](https://console.tianditu.gov.cn/) 申请得到的 **tk**。
|
||||
|
||||
**底图(矢量):**
|
||||
|
||||
```text
|
||||
https://t0.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&FORMAT=tiles&tk=您的密钥
|
||||
```
|
||||
|
||||
**标识图(中文注记):**
|
||||
|
||||
```text
|
||||
https://t0.tianditu.gov.cn/cva_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cva&STYLE=default&TILEMATRIXSET=w&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&FORMAT=tiles&tk=您的密钥
|
||||
```
|
||||
|
||||
- **瓦片参数**:`{z}` 为层级(zoom),`{y}` 为行号,`{x}` 为列号;由地图引擎在请求时替换。
|
||||
- **同一密钥**:底图与标识图使用同一个 **tk** 即可。
|
||||
- **坐标系**:上述为 WGS84(经纬度),与 topology-editor、targets.csv 中 lat/lon 一致。
|
||||
|
||||
---
|
||||
|
||||
## 4. 瓦片缓存与手动更新(节省 key 免费量)
|
||||
|
||||
天地图 key 有免费调用量限制。本项目提供 **tile-cache** 服务:瓦片首次请求时向天地图拉取并落盘,后续同一瓦片在**老化时间**内直接读缓存;超过老化时间的瓦片在下次请求时会自动重新拉取。
|
||||
|
||||
### 4.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 使用缓存(可选)
|
||||
|
||||
将 Geomap 的 XYZ 底图/标识图 URL 改为:
|
||||
`http://<central>:4080/tiles/vec/{z}/{x}/{y}` 与 `http://<central>:4080/tiles/cva/{z}/{x}/{y}`(经拓扑助手代理)。天地图密钥已在 **.env** 的 **TIANDITU_TK** 中配置,Grafana 中无需填写。
|
||||
|
||||
### 4.5 更新方式
|
||||
|
||||
超过 `TILE_CACHE_TTL_DAYS` 天的瓦片,在下次被请求时会自动重新向天地图拉取并写回缓存,无需手动操作。
|
||||
|
||||
---
|
||||
|
||||
## 5. 在 Grafana Geomap 中配置天地图(直连或走缓存)
|
||||
|
||||
Grafana 的 Geomap 支持 **XYZ Tile layer**。可直连天地图 WMTS(URL 中填 tk),或使用瓦片缓存地址(见第 4 节)。
|
||||
|
||||
### 5.1 配置底图(vec_w)
|
||||
|
||||
1. 新建或编辑一个 **Geomap** 面板。
|
||||
2. 在右侧 **Layer** / **Base layer** 区域,将底图类型选为 **XYZ Tile layer**(或「自定义」/「Generic XYZ」等,视 Grafana 版本而定)。
|
||||
3. **URL template** 中填入天地图矢量底图地址(将 `您的密钥` 换成实际 tk):
|
||||
|
||||
```text
|
||||
https://t0.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&FORMAT=tiles&tk=您的密钥
|
||||
```
|
||||
|
||||
4. **Attribution** 可填:`© 天地图`。
|
||||
5. 保存面板后即可看到天地图矢量底图。
|
||||
|
||||
### 5.2 叠加标识图(cva_w,中文注记)
|
||||
|
||||
若需要地名、道路名等中文注记,可在同一 Geomap 上再添加一层 XYZ 瓦片,叠在底图之上:
|
||||
|
||||
1. 在 Geomap 面板的 **Map layers** 中点击 **Add layer**。
|
||||
2. 选择 **XYZ Tile layer**。
|
||||
3. **URL template** 填入标识图地址(同一 tk):
|
||||
|
||||
```text
|
||||
https://t0.tianditu.gov.cn/cva_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cva&STYLE=default&TILEMATRIXSET=w&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&FORMAT=tiles&tk=您的密钥
|
||||
```
|
||||
|
||||
4. 可将该层的 **Opacity** 设为 1(不透明),这样注记清晰可见。
|
||||
5. 图层顺序:底图在下,标识图在上;若顺序反了,可在面板里拖拽调整。
|
||||
|
||||
### 5.3 说明
|
||||
|
||||
- Grafana 会在请求瓦片时把 URL 中的 `{z}`、`{x}`、`{y}` 替换为当前层级与行列号,与天地图 WMTS 的 `TILEMATRIX` / `TILEROW` / `TILECOL` 一一对应。
|
||||
- 底图与标识图使用**同一个 tk** 即可;tk 在 [天地图开放平台](https://console.tianditu.gov.cn/) 申请。
|
||||
- 若希望所有 Geomap 默认使用天地图,可在 Grafana 的 provisioning 或 `default_baselayer_config` 中配置 type 为 `xyz`、url 为上述 vec_w 地址(详见 [Grafana 文档 - Configure the default base layer](https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/geomap/#configure-the-default-base-layer-with-provisioning))。
|
||||
|
||||
---
|
||||
|
||||
## 6. 参考
|
||||
|
||||
- [天地图开放平台](https://www.tianditu.gov.cn/)
|
||||
- [开发文档 / 服务资源](https://lbs.tianditu.gov.cn/server/MapService.html)
|
||||
@@ -31,13 +31,14 @@ ONVIF_TEMP=$(mktemp)
|
||||
PING_TEMP=$(mktemp)
|
||||
|
||||
# 处理CSV文件(跳过注释行和标题行)
|
||||
tail -n +2 "$CSV_FILE" | grep -v '^#' | while IFS=',' read -r type ip device group network device_type model location username password onvif_port lat lon; do
|
||||
# 列顺序: 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" ] || [ -z "$ip" ]; then
|
||||
if [ -z "$type" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
@@ -84,16 +85,18 @@ tail -n +2 "$CSV_FILE" | grep -v '^#' | while IFS=',' read -r type ip device gro
|
||||
|
||||
elif [ "$type" = "ping" ]; then
|
||||
# 处理 Ping 目标
|
||||
device=$(echo "$device" | xargs)
|
||||
group=$(echo "$group" | xargs)
|
||||
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 "$group" ]; then
|
||||
if [ -n "$uplink_type" ]; then
|
||||
labels="$labels,
|
||||
\"group\": \"$group\""
|
||||
\"uplink_type\": \"$uplink_type\""
|
||||
fi
|
||||
|
||||
if [ -n "$network" ]; then
|
||||
@@ -101,6 +104,16 @@ tail -n +2 "$CSV_FILE" | grep -v '^#' | while IFS=',' read -r type ip device gro
|
||||
\"network\": \"$network\""
|
||||
fi
|
||||
|
||||
if [ -n "$role" ]; then
|
||||
labels="$labels,
|
||||
\"role\": \"$role\""
|
||||
fi
|
||||
|
||||
if [ -n "$parent" ]; then
|
||||
labels="$labels,
|
||||
\"parent\": \"$parent\""
|
||||
fi
|
||||
|
||||
labels="$labels
|
||||
}"
|
||||
|
||||
|
||||
100
edge-agent/config/csv-to-topology-geojson.sh
Executable file
100
edge-agent/config/csv-to-topology-geojson.sh
Executable file
@@ -0,0 +1,100 @@
|
||||
#!/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" <<EOF
|
||||
{"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"}
|
||||
EOF
|
||||
done
|
||||
|
||||
# 用 jq 生成 FeatureCollection
|
||||
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 $child
|
||||
| (all[] | select(.name == $child.parent and .lat != "" and .lon != "")) as $parent
|
||||
| {
|
||||
type: "Feature",
|
||||
geometry: {
|
||||
type: "LineString",
|
||||
coordinates: [
|
||||
[ ($parent.lon|tonumber), ($parent.lat|tonumber) ],
|
||||
[ ($child.lon|tonumber), ($child.lat|tonumber) ]
|
||||
]
|
||||
},
|
||||
properties: {
|
||||
type: "link",
|
||||
from: $parent.name,
|
||||
to: $child.name,
|
||||
from_role: $parent.role,
|
||||
to_role: $child.role
|
||||
}
|
||||
};
|
||||
|
||||
. as $all
|
||||
| {
|
||||
type: "FeatureCollection",
|
||||
features: (
|
||||
[ $all[] | toPoint ] +
|
||||
[ $all[] | toLine($all) ]
|
||||
)
|
||||
}
|
||||
' "$TMP_JSON" > "$OUTPUT_FILE"
|
||||
|
||||
rm -f "$TMP_JSON"
|
||||
|
||||
echo "✅ 已生成 $OUTPUT_FILE"
|
||||
|
||||
@@ -1,14 +1,25 @@
|
||||
# 统一监控目标配置文件
|
||||
# 格式: type,ip,device,group,network,device_type,model,location,username,password,onvif_port,lat,lon
|
||||
# type: onvif 或 ping
|
||||
# 对于 onvif 类型,需要填写: ip,device_type,model,location,username,password,onvif_port,lat,lon
|
||||
# - lat / lon 为摄像头地理位置坐标(十进制度数),用于在 Geomap 上打点
|
||||
# 对于 ping 类型,需要填写: ip,device,group,network(lat / lon 可选)
|
||||
type,ip,device,group,network,device_type,model,location,username,password,onvif_port,lat,lon
|
||||
ping,192.168.2.1,main_router,network,internal,,,,,,,,
|
||||
ping,8.8.8.8,google_dns,external,external,,,,,,,,
|
||||
# 统一监控 + 拓扑目标配置文件
|
||||
# 格式: type,ip,name,role,parent,uplink_type,network,device_type,model,location,username,password,onvif_port,lat,lon
|
||||
# type: ping / onvif / topology
|
||||
# - ping: 有 IP,可做网络探测
|
||||
# - onvif: 有 IP,走 ONVIF Exporter
|
||||
# - topology: 纯拓扑节点(无 IP 也可),只用于在 Geomap 上画点和连线
|
||||
# role: core_switch / access_switch / camera / wireless_bridge / media_converter ...
|
||||
# parent: 上联设备的 name,用于生成拓扑连线
|
||||
# uplink_type: 与上联 parent 之间的链路类型(fiber / copper / wireless 等)
|
||||
# lat / lon: 地理坐标(十进制度数),用于在 Geomap 上打点
|
||||
type,ip,name,role,parent,uplink_type,network,device_type,model,location,username,password,onvif_port,lat,lon
|
||||
|
||||
# 核心交换机(有 IP,可 ping,暂无上联,uplink_type 留空)
|
||||
ping,192.168.2.1,core_sw_1,core_switch,, ,internal,,,,,,,22.5431,113.9876
|
||||
|
||||
# 互联网 DNS(示例,链路类型为 fiber / copper / wireless 之一)
|
||||
ping,8.8.8.8,google_dns,dns,core_sw_1,fiber,external,,,,,,,22.5500,113.9900
|
||||
|
||||
# 哑设备示例:傻瓜交换机、光收发器(无 IP,只画拓扑)
|
||||
# topology,,dumb_sw_1,access_switch,core_sw_1,copper,internal,switch,Unmanaged_8P,building_A_3F,,,,22.5438,113.9882
|
||||
# topology,,optic_1,media_converter,dumb_sw_1,fiber,internal,media_conv,MC-1000,basement_1F,,,,22.5436,113.9880
|
||||
|
||||
# ONVIF 设备示例(取消注释并填写实际信息)
|
||||
# onvif,192.168.1.100,,,front_door,camera,HIKVISION_DS-2CD2342WD-I,front_door,admin,password1,80,22.1234,113.5678
|
||||
# onvif,192.168.1.101,,,back_yard,camera,DAHUA_IPC-HFW1230S,back_yard,admin,password2,80,22.2234,113.6678
|
||||
# onvif,192.168.1.102,,,living_room,camera,UNIVIEW_IPC3120SR,living_room,admin,password3,8080,22.3234,113.7678
|
||||
# onvif,192.168.1.50,,,server_rack,nvr,HIKVISION_DS-7608NI-I2,server_rack,admin,password4,80,22.4234,113.8678
|
||||
# onvif,192.168.1.100,camera_front,camera,dumb_sw_1,copper,onvif_cameras,internal,camera,HIKVISION_DS-2CD2342WD-I,front_door,admin,password1,80,22.1234,113.5678
|
||||
# onvif,192.168.1.101,camera_back,camera,bridge_1,wireless,onvif_cameras,internal,camera,DAHUA_IPC-HFW1230S,back_yard,admin,password2,80,22.2234,113.6678
|
||||
|
||||
|
7
tile-cache/Dockerfile
Normal file
7
tile-cache/Dockerfile
Normal file
@@ -0,0 +1,7 @@
|
||||
FROM node:20-alpine
|
||||
WORKDIR /app
|
||||
COPY tile-cache/package.json tile-cache/server.js ./
|
||||
RUN mkdir -p /app/cache
|
||||
ENV PORT=4090
|
||||
EXPOSE 4090
|
||||
CMD ["node", "server.js"]
|
||||
6
tile-cache/package.json
Normal file
6
tile-cache/package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "tianditu-tile-cache",
|
||||
"version": "1.0.0",
|
||||
"description": "Cache Tianditu WMTS tiles to reduce API key usage",
|
||||
"main": "server.js"
|
||||
}
|
||||
227
tile-cache/server.js
Normal file
227
tile-cache/server.js
Normal file
@@ -0,0 +1,227 @@
|
||||
/**
|
||||
* 天地图瓦片缓存服务
|
||||
* - GET /vec/:z/:x/:y GET /cva/:z/:x/:y 返回瓦片(先读缓存,未命中或已过期则向天地图请求并写入缓存)
|
||||
* - 缓存老化:超过 CACHE_TTL_DAYS 天的瓦片视为过期,会重新拉取
|
||||
* - GET /api/cache/status 返回缓存统计与 TTL 配置
|
||||
*/
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const PORT = parseInt(process.env.PORT || '4090', 10);
|
||||
const TIANDITU_TK = (process.env.TIANDITU_TK || '').trim();
|
||||
const CACHE_DIR = path.resolve(process.env.CACHE_DIR || path.join(__dirname, 'cache'));
|
||||
const CACHE_TTL_DAYS = Math.max(1, parseInt(process.env.CACHE_TTL_DAYS || '7', 10));
|
||||
const CACHE_TTL_MS = CACHE_TTL_DAYS * 24 * 60 * 60 * 1000;
|
||||
const UPSTREAM_TIMEOUT_MS = Math.max(5000, parseInt(process.env.UPSTREAM_TIMEOUT_MS || '15000', 10));
|
||||
|
||||
const WMTS_BASE = 'https://t0.tianditu.gov.cn';
|
||||
|
||||
function log(msg, detail) {
|
||||
const d = detail != null ? ' ' + JSON.stringify(detail) : '';
|
||||
console.log('[tile-cache] ' + msg + d);
|
||||
}
|
||||
function logErr(msg, err) {
|
||||
console.error('[tile-cache] ' + msg, err && err.message ? err.message : err);
|
||||
}
|
||||
|
||||
function ensureCacheDir(layer) {
|
||||
const dir = path.join(CACHE_DIR, layer);
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
return dir;
|
||||
}
|
||||
|
||||
function tilePath(layer, z, x, y) {
|
||||
return path.join(CACHE_DIR, layer, String(z), String(x), String(y) + '.png');
|
||||
}
|
||||
|
||||
function serveTile(filePath, res) {
|
||||
const stream = fs.createReadStream(filePath);
|
||||
stream.on('error', () => {
|
||||
res.statusCode = 404;
|
||||
res.end();
|
||||
});
|
||||
res.setHeader('Content-Type', 'image/png');
|
||||
res.setHeader('Cache-Control', 'public, max-age=86400');
|
||||
stream.pipe(res);
|
||||
}
|
||||
|
||||
function fetchFromTianditu(layer, z, x, y, cb) {
|
||||
if (!TIANDITU_TK) {
|
||||
logErr('fetchFromTianditu: TIANDITU_TK not set', null);
|
||||
cb(new Error('TIANDITU_TK not set'));
|
||||
return;
|
||||
}
|
||||
let done = false;
|
||||
function once(err, buf) {
|
||||
if (done) return;
|
||||
done = true;
|
||||
cb(err, buf);
|
||||
}
|
||||
const subpath = layer === 'vec' ? 'vec_w' : 'cva_w';
|
||||
const url = `${WMTS_BASE}/${subpath}/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=${layer === 'vec' ? 'vec' : 'cva'}&STYLE=default&TILEMATRIXSET=w&TILEMATRIX=${z}&TILEROW=${y}&TILECOL=${x}&FORMAT=tiles&tk=${encodeURIComponent(TIANDITU_TK)}`;
|
||||
log('upstream request', { layer, z, x, y });
|
||||
const req = https.get(url, (proxyRes) => {
|
||||
if (proxyRes.statusCode !== 200) {
|
||||
log('upstream non-200', { status: proxyRes.statusCode, layer, z, x, y });
|
||||
once(new Error(`Upstream ${proxyRes.statusCode}`));
|
||||
return;
|
||||
}
|
||||
const chunks = [];
|
||||
proxyRes.on('data', (chunk) => chunks.push(chunk));
|
||||
proxyRes.on('end', () => {
|
||||
log('upstream end', { layer, z, x, y, size: chunks.reduce((a, c) => a + c.length, 0) });
|
||||
once(null, Buffer.concat(chunks));
|
||||
});
|
||||
proxyRes.on('error', (e) => {
|
||||
logErr('upstream stream error', e);
|
||||
once(e);
|
||||
});
|
||||
});
|
||||
req.on('error', (e) => {
|
||||
logErr('upstream request error', e);
|
||||
once(e);
|
||||
});
|
||||
req.setTimeout(UPSTREAM_TIMEOUT_MS, () => {
|
||||
log('upstream timeout', { layer, z, x, y });
|
||||
req.destroy();
|
||||
once(new Error('Upstream timeout'));
|
||||
});
|
||||
}
|
||||
|
||||
function handleTile(layer, z, x, y, res) {
|
||||
const zNum = parseInt(z, 10);
|
||||
const xNum = parseInt(x, 10);
|
||||
const yNum = parseInt(y, 10);
|
||||
if (isNaN(zNum) || isNaN(xNum) || isNaN(yNum) || zNum < 0 || zNum > 20) {
|
||||
res.statusCode = 400;
|
||||
res.end('Bad tile');
|
||||
return;
|
||||
}
|
||||
const filePath = tilePath(layer, z, x, y);
|
||||
if (fs.existsSync(filePath)) {
|
||||
try {
|
||||
const stat = fs.statSync(filePath);
|
||||
const ageMs = Date.now() - stat.mtime.getTime();
|
||||
if (ageMs < CACHE_TTL_MS) {
|
||||
log('cache hit', { layer, z, x, y });
|
||||
serveTile(filePath, res);
|
||||
return;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
log('cache miss', { layer, z, x, y });
|
||||
fetchFromTianditu(layer, z, x, y, (err, buf) => {
|
||||
try {
|
||||
if (err) {
|
||||
log('tile 502', { layer, z, x, y, err: err && err.message });
|
||||
res.statusCode = 502;
|
||||
res.end(err.message || 'Upstream error');
|
||||
return;
|
||||
}
|
||||
const dir = path.dirname(filePath);
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
try {
|
||||
fs.writeFileSync(filePath, buf);
|
||||
} catch (e) {
|
||||
logErr('write cache error', e);
|
||||
}
|
||||
res.setHeader('Content-Type', 'image/png');
|
||||
res.setHeader('Cache-Control', 'public, max-age=86400');
|
||||
res.end(buf);
|
||||
log('tile sent', { layer, z, x, y });
|
||||
} catch (e) {
|
||||
logErr('handleTile response error', e);
|
||||
if (!res.headersSent) {
|
||||
res.statusCode = 500;
|
||||
res.end('Internal error');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function countCached(dir, acc) {
|
||||
if (!fs.existsSync(dir)) return acc;
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
for (const e of entries) {
|
||||
const p = path.join(dir, e.name);
|
||||
if (e.isDirectory()) countCached(p, acc);
|
||||
else acc.count++;
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
log('request', { method: req.method, url: req.url });
|
||||
try {
|
||||
const urlStr = (req.url && req.url.length) ? req.url : '/';
|
||||
const u = new URL(urlStr, 'http://localhost');
|
||||
const p = u.pathname;
|
||||
|
||||
if (req.method === 'GET' && p === '/health') {
|
||||
res.setHeader('Content-Type', 'text/plain');
|
||||
res.end('ok');
|
||||
return;
|
||||
}
|
||||
if (req.method === 'GET' && /^\/vec\/(\d+)\/(\d+)\/(\d+)$/.test(p)) {
|
||||
const [, z, x, y] = p.match(/^\/vec\/(\d+)\/(\d+)\/(\d+)$/);
|
||||
handleTile('vec', z, x, y, res);
|
||||
return;
|
||||
}
|
||||
if (req.method === 'GET' && /^\/cva\/(\d+)\/(\d+)\/(\d+)$/.test(p)) {
|
||||
const [, z, x, y] = p.match(/^\/cva\/(\d+)\/(\d+)\/(\d+)$/);
|
||||
handleTile('cva', z, x, y, res);
|
||||
return;
|
||||
}
|
||||
if (req.method === 'GET' && (p === '/api/cache/status' || p === '/api/cache/status/')) {
|
||||
let count = 0;
|
||||
try {
|
||||
count = countCached(CACHE_DIR, { count: 0 }).count;
|
||||
} catch (e) {
|
||||
logErr('countCached error', e);
|
||||
}
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify({ ok: true, cachedTiles: count, cacheTtlDays: CACHE_TTL_DAYS }));
|
||||
return;
|
||||
}
|
||||
|
||||
res.statusCode = 404;
|
||||
res.end('Not found');
|
||||
} catch (e) {
|
||||
logErr('Request handler error', e);
|
||||
if (!res.headersSent) {
|
||||
res.statusCode = 500;
|
||||
res.setHeader('Content-Type', 'text/plain');
|
||||
res.end('Internal error');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`Tile cache listening on http://0.0.0.0:${PORT}`);
|
||||
if (!TIANDITU_TK) console.warn('TIANDITU_TK not set; tile requests will fail.');
|
||||
console.log(`Cache dir: ${CACHE_DIR}, TTL: ${CACHE_TTL_DAYS} days, upstream timeout: ${UPSTREAM_TIMEOUT_MS}ms`);
|
||||
|
||||
// 启动时请求一块瓦片并写日志,确认容器能否访问天地图
|
||||
if (!TIANDITU_TK) {
|
||||
log('startup probe skipped (TIANDITU_TK not set)', null);
|
||||
return;
|
||||
}
|
||||
const probeZ = 3, probeX = 1, probeY = 2;
|
||||
log('startup probe requesting vec/' + probeZ + '/' + probeX + '/' + probeY, null);
|
||||
fetchFromTianditu('vec', String(probeZ), String(probeX), String(probeY), (err, buf) => {
|
||||
if (err) {
|
||||
logErr('startup probe failed', err);
|
||||
return;
|
||||
}
|
||||
log('startup probe ok', { layer: 'vec', z: probeZ, x: probeX, y: probeY, size: buf ? buf.length : 0 });
|
||||
});
|
||||
});
|
||||
|
||||
process.on('uncaughtException', (err) => {
|
||||
logErr('uncaughtException', err);
|
||||
});
|
||||
process.on('unhandledRejection', (reason, p) => {
|
||||
console.error('[tile-cache] unhandledRejection', reason);
|
||||
});
|
||||
3
topology-editor/.dockerignore
Normal file
3
topology-editor/.dockerignore
Normal file
@@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
*.log
|
||||
.env*
|
||||
18
topology-editor/Dockerfile
Normal file
18
topology-editor/Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
||||
# 拓扑标注助手: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"]
|
||||
827
topology-editor/package-lock.json
generated
Normal file
827
topology-editor/package-lock.json
generated
Normal file
@@ -0,0 +1,827 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
13
topology-editor/package.json
Normal file
13
topology-editor/package.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
509
topology-editor/public/index.html
Normal file
509
topology-editor/public/index.html
Normal file
@@ -0,0 +1,509 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>拓扑标注助手(GPS → targets.csv)</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<style>
|
||||
body {
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||
sans-serif;
|
||||
margin: 0;
|
||||
padding: 16px;
|
||||
}
|
||||
h1 {
|
||||
font-size: 18px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
input,
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 6px 8px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
button {
|
||||
margin-top: 12px;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.row {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.log {
|
||||
margin-top: 12px;
|
||||
font-size: 12px;
|
||||
color: #555;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
#map-section {
|
||||
margin-top: 20px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
#map-section h2 {
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
padding: 8px 12px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
#map-container {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
background: #e8e8e8;
|
||||
}
|
||||
.map-tip {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
padding: 6px 12px;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>拓扑标注助手</h1>
|
||||
<p style="font-size: 13px">
|
||||
与 central 同机运行时:先<strong>上传</strong>本机 <code>targets.csv</code>,在页面上选设备、补 GPS 或新建后<strong>下载</strong> CSV 部署到边缘。与 edge-agent 同机挂载 config 时可直接读写 <code>config/targets.csv</code>。
|
||||
</p>
|
||||
|
||||
<div id="upload-download-section" class="row" style="margin-top:12px; padding:10px; background:#f9f9f9; border-radius:6px;">
|
||||
<label>上传 / 下载 targets.csv</label>
|
||||
<div style="display:flex; flex-wrap:wrap; gap:8px; align-items:center;">
|
||||
<input type="file" id="csv-file" accept=".csv" style="width:auto; max-width:200px;" />
|
||||
<button id="btn-upload">上传 CSV</button>
|
||||
<a id="btn-download" href="/api/download" download="targets.csv" style="padding:8px 12px; font-size:14px; background:#0d6efd; color:#fff; text-decoration:none; border-radius:4px;">下载 targets.csv</a>
|
||||
</div>
|
||||
<p id="mode-hint" style="font-size:12px; color:#666; margin:8px 0 0 0;"></p>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label>选择已有设备(可选,推荐)</label>
|
||||
<select id="existing">
|
||||
<option value="">(新建设备)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<p style="font-size: 13px">
|
||||
若是新建设备:先选上面的「(新建设备)」,再填写下列字段。
|
||||
</p>
|
||||
|
||||
<div class="row">
|
||||
<label>设备类型 (type)</label>
|
||||
<select id="type">
|
||||
<option value="topology">topology(哑设备/拓扑节点)</option>
|
||||
<option value="ping">ping(有 IP,可探测)</option>
|
||||
<option value="onvif">onvif(摄像头)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label>设备名称 (name,唯一标识)</label>
|
||||
<input id="name" placeholder="例如 core_sw_1 / camera_front" />
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label>角色 (role)</label>
|
||||
<select id="role">
|
||||
<option value="core_switch">core_switch</option>
|
||||
<option value="access_switch">access_switch</option>
|
||||
<option value="camera">camera</option>
|
||||
<option value="wireless_bridge">wireless_bridge</option>
|
||||
<option value="media_converter">media_converter</option>
|
||||
<option value="other">other</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label>上联设备名称 (parent,可留空)</label>
|
||||
<input id="parent" placeholder="例如 core_sw_1" />
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label>上联链路类型 (uplink_type)</label>
|
||||
<select id="uplink_type">
|
||||
<option value="">(无)</option>
|
||||
<option value="fiber">fiber(光纤)</option>
|
||||
<option value="copper">copper(铜缆)</option>
|
||||
<option value="wireless">wireless(无线)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label>IP(可选,ping/onvif 用)</label>
|
||||
<input id="ip" placeholder="192.168.x.x" />
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label>位置描述 (location,可选)</label>
|
||||
<input id="location" placeholder="例如:A栋3楼弱电井" />
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label>经度 (lon)</label>
|
||||
<input id="lon" readonly />
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label>纬度 (lat)</label>
|
||||
<input id="lat" readonly />
|
||||
</div>
|
||||
|
||||
<button id="btn-gps">获取当前 GPS 坐标</button>
|
||||
<button id="btn-save">保存到 targets.csv</button>
|
||||
|
||||
<div class="log" id="log"></div>
|
||||
|
||||
<section id="map-section">
|
||||
<h2>地图校验</h2>
|
||||
<div class="row">
|
||||
<label>天地图 TK(<a href="https://console.tianditu.gov.cn/" target="_blank" rel="noopener">申请密钥</a>,填一次即可)</label>
|
||||
<input id="tianditu-tk" type="text" placeholder="请输入您的天地图 tk" />
|
||||
</div>
|
||||
<div style="display:flex; flex-wrap:wrap; gap:8px; align-items:center;">
|
||||
<button id="btn-load-map">加载天地图(直接)</button>
|
||||
<span id="cache-buttons" style="display:none;">
|
||||
<button id="btn-load-map-cache">加载天地图(使用服务器缓存)</button>
|
||||
</span>
|
||||
</div>
|
||||
<div id="map-container"></div>
|
||||
<p class="map-tip">点击地图可修正坐标;有经纬度后会自动打点并居中。</p>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
const logEl = document.getElementById("log");
|
||||
const existingSelect = document.getElementById("existing");
|
||||
let existingTargets = [];
|
||||
let map = null;
|
||||
let marker = null;
|
||||
let accuracyCircle = null;
|
||||
let lastGpsAccuracyM = null;
|
||||
let mapSource = null;
|
||||
let leafletMap = null;
|
||||
let leafletMarker = null;
|
||||
let cacheBaseUrl = "";
|
||||
|
||||
function log(msg) {
|
||||
logEl.textContent = msg;
|
||||
}
|
||||
|
||||
function getTk() {
|
||||
return (document.getElementById("tianditu-tk").value || "").trim();
|
||||
}
|
||||
|
||||
function loadTiandituMap() {
|
||||
const tk = getTk();
|
||||
if (!tk) {
|
||||
log("请先填写天地图 TK 再加载地图。");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
localStorage.setItem("tianditu_tk", tk);
|
||||
} catch (e) {}
|
||||
if (window.T && window.T.Map) {
|
||||
initMap();
|
||||
return;
|
||||
}
|
||||
log("正在加载天地图…");
|
||||
const script = document.createElement("script");
|
||||
script.src = "https://api.tianditu.gov.cn/api?v=4.0&tk=" + encodeURIComponent(tk);
|
||||
script.onload = function () {
|
||||
log("天地图加载成功,可点击地图修正坐标。");
|
||||
initMap();
|
||||
};
|
||||
script.onerror = function () {
|
||||
log("天地图加载失败,请检查 TK 或网络。");
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
|
||||
function initMap() {
|
||||
if (map && mapSource === "tianditu") return;
|
||||
var T = window.T;
|
||||
if (!T || !T.Map) return;
|
||||
if (map) return;
|
||||
mapSource = "tianditu";
|
||||
map = new T.Map("map-container");
|
||||
map.centerAndZoom(new T.LngLat(116.4, 39.9), 12);
|
||||
map.addEventListener("click", function (e) {
|
||||
var ll = e.lnglat;
|
||||
var lat = Math.round(ll.lat * 1e6) / 1e6;
|
||||
var lon = Math.round(ll.lng * 1e6) / 1e6;
|
||||
document.getElementById("lat").value = lat;
|
||||
document.getElementById("lon").value = lon;
|
||||
updateMapMarker(lat, lon);
|
||||
log("已从地图点击设置坐标:lat=" + lat + ", lon=" + lon);
|
||||
});
|
||||
var latEl = document.getElementById("lat").value.trim();
|
||||
var lonEl = document.getElementById("lon").value.trim();
|
||||
if (latEl && lonEl) {
|
||||
var lat = parseFloat(latEl), lon = parseFloat(lonEl);
|
||||
if (!isNaN(lat) && !isNaN(lon)) updateMapMarker(lat, lon);
|
||||
}
|
||||
}
|
||||
|
||||
function loadLeafletThenInitCacheMap() {
|
||||
if (window.L && window.L.map) {
|
||||
initCacheMap();
|
||||
return;
|
||||
}
|
||||
log("正在加载地图库…");
|
||||
var link = document.createElement("link");
|
||||
link.rel = "stylesheet";
|
||||
link.href = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.css";
|
||||
link.integrity = "sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=";
|
||||
link.crossOrigin = "";
|
||||
document.head.appendChild(link);
|
||||
var script = document.createElement("script");
|
||||
script.src = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.js";
|
||||
script.integrity = "sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=";
|
||||
script.crossOrigin = "";
|
||||
script.onload = function () {
|
||||
log("使用服务器缓存加载天地图,可点击地图修正坐标。");
|
||||
initCacheMap();
|
||||
};
|
||||
script.onerror = function () { log("Leaflet 加载失败"); };
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
|
||||
function initCacheMap() {
|
||||
if (leafletMap) return;
|
||||
if (!cacheBaseUrl) { log("未配置缓存服务"); return; }
|
||||
document.getElementById("map-container").innerHTML = "";
|
||||
map = null;
|
||||
marker = null;
|
||||
mapSource = "leaflet";
|
||||
leafletMap = L.map("map-container").setView([39.9, 116.4], 12);
|
||||
L.tileLayer(cacheBaseUrl + "/vec/{z}/{x}/{y}", { attribution: "© 天地图" }).addTo(leafletMap);
|
||||
L.tileLayer(cacheBaseUrl + "/cva/{z}/{x}/{y}", { attribution: "" }).addTo(leafletMap);
|
||||
leafletMap.on("click", function (e) {
|
||||
var lat = Math.round(e.latlng.lat * 1e6) / 1e6;
|
||||
var lon = Math.round(e.latlng.lng * 1e6) / 1e6;
|
||||
document.getElementById("lat").value = lat;
|
||||
document.getElementById("lon").value = lon;
|
||||
updateMapMarker(lat, lon);
|
||||
log("已从地图点击设置坐标:lat=" + lat + ", lon=" + lon);
|
||||
});
|
||||
var latEl = document.getElementById("lat").value.trim();
|
||||
var lonEl = document.getElementById("lon").value.trim();
|
||||
if (latEl && lonEl) {
|
||||
var lat = parseFloat(latEl), lon = parseFloat(lonEl);
|
||||
if (!isNaN(lat) && !isNaN(lon)) updateMapMarker(lat, lon);
|
||||
}
|
||||
}
|
||||
|
||||
function updateMapMarker(lat, lon) {
|
||||
if (isNaN(lat) || isNaN(lon)) return;
|
||||
if (mapSource === "leaflet" && leafletMap) {
|
||||
if (leafletMarker) leafletMap.removeLayer(leafletMarker);
|
||||
leafletMarker = L.marker([lat, lon]).addTo(leafletMap);
|
||||
leafletMap.setView([lat, lon], 17);
|
||||
return;
|
||||
}
|
||||
if (window.T && map) {
|
||||
var T = window.T;
|
||||
if (marker) {
|
||||
map.removeOverLay(marker);
|
||||
marker = null;
|
||||
}
|
||||
if (accuracyCircle) {
|
||||
map.removeOverLay(accuracyCircle);
|
||||
accuracyCircle = null;
|
||||
}
|
||||
var lngLat = new T.LngLat(lon, lat);
|
||||
marker = new T.Marker(lngLat);
|
||||
map.addOverLay(marker);
|
||||
map.panTo(lngLat);
|
||||
map.setZoom(17);
|
||||
if (lastGpsAccuracyM != null && lastGpsAccuracyM > 0 && T.Circle) {
|
||||
try {
|
||||
accuracyCircle = new T.Circle(lngLat, lastGpsAccuracyM, {
|
||||
color: "#3388ff",
|
||||
weight: 1,
|
||||
opacity: 0.4,
|
||||
});
|
||||
map.addOverLay(accuracyCircle);
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function fillFormFromTarget(t) {
|
||||
document.getElementById("type").value = t.type || "topology";
|
||||
document.getElementById("name").value = t.name || "";
|
||||
document.getElementById("role").value = t.role || "other";
|
||||
document.getElementById("parent").value = t.parent || "";
|
||||
document.getElementById("uplink_type").value = t.uplink_type || "";
|
||||
document.getElementById("ip").value = t.ip || "";
|
||||
document.getElementById("location").value = t.location || "";
|
||||
document.getElementById("lat").value = t.lat || "";
|
||||
document.getElementById("lon").value = t.lon || "";
|
||||
if (t.lat && t.lon) {
|
||||
var lat = parseFloat(t.lat), lon = parseFloat(t.lon);
|
||||
if (!isNaN(lat) && !isNaN(lon)) updateMapMarker(lat, lon);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadExisting() {
|
||||
try {
|
||||
const resp = await fetch("/api/targets");
|
||||
if (!resp.ok) throw new Error("加载 targets.csv 失败");
|
||||
existingTargets = await resp.json();
|
||||
existingSelect.innerHTML =
|
||||
'<option value="">(新建设备)</option>' +
|
||||
existingTargets
|
||||
.map(
|
||||
(t) =>
|
||||
`<option value="${(t.name || '').replace(/"/g, '"')}">${(t.name || '')} (${t.type || ""} / ${t.role || ""})</option>`
|
||||
)
|
||||
.join("");
|
||||
log("已加载现有设备列表,可从下拉框选择后补充 GPS。");
|
||||
} catch (e) {
|
||||
log("加载现有设备失败:" + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
existingSelect.onchange = () => {
|
||||
const name = existingSelect.value;
|
||||
if (!name) {
|
||||
const lat = document.getElementById("lat").value;
|
||||
const lon = document.getElementById("lon").value;
|
||||
fillFormFromTarget({
|
||||
type: "topology",
|
||||
role: "other",
|
||||
lat,
|
||||
lon,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const t = existingTargets.find((r) => r.name === name);
|
||||
if (!t) return;
|
||||
fillFormFromTarget(t);
|
||||
};
|
||||
|
||||
document.getElementById("btn-gps").onclick = () => {
|
||||
if (!navigator.geolocation) {
|
||||
log("当前浏览器不支持 Geolocation。");
|
||||
return;
|
||||
}
|
||||
log("正在获取 GPS 坐标…");
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(pos) => {
|
||||
const { latitude, longitude, accuracy } = pos.coords;
|
||||
document.getElementById("lat").value = latitude.toFixed(6);
|
||||
document.getElementById("lon").value = longitude.toFixed(6);
|
||||
lastGpsAccuracyM = accuracy != null ? accuracy : null;
|
||||
updateMapMarker(latitude, longitude);
|
||||
var acc = lastGpsAccuracyM != null ? ",精度约 " + Math.round(lastGpsAccuracyM) + " 米" : "";
|
||||
log("获取成功:lat=" + latitude + ", lon=" + longitude + acc + "。请在地图上核对位置,若有偏差可点击地图修正。");
|
||||
},
|
||||
(err) => {
|
||||
var msg = err.message || "";
|
||||
if (msg.indexOf("secure origins") !== -1 || msg.indexOf("secure context") !== -1) {
|
||||
log("获取定位失败:当前页面不是安全来源,浏览器不允许获取定位。请使用 https:// 访问本页,或在同一台电脑上用 http://localhost:4080 打开。");
|
||||
} else {
|
||||
log("获取定位失败:" + msg);
|
||||
}
|
||||
},
|
||||
{
|
||||
enableHighAccuracy: true,
|
||||
timeout: 10000,
|
||||
maximumAge: 0,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
document.getElementById("btn-load-map").onclick = loadTiandituMap;
|
||||
|
||||
document.getElementById("btn-load-map-cache").onclick = loadLeafletThenInitCacheMap;
|
||||
|
||||
document.getElementById("btn-upload").onclick = async () => {
|
||||
var fileInput = document.getElementById("csv-file");
|
||||
if (!fileInput.files || !fileInput.files[0]) {
|
||||
log("请先选择要上传的 CSV 文件。");
|
||||
return;
|
||||
}
|
||||
var form = new FormData();
|
||||
form.append("file", fileInput.files[0]);
|
||||
try {
|
||||
log("正在上传…");
|
||||
var r = await fetch("/api/upload", { method: "POST", body: form });
|
||||
var d = await r.json();
|
||||
if (!r.ok) throw new Error(d.error || "上传失败");
|
||||
log("已上传,共 " + (d.rows || 0) + " 条。");
|
||||
loadExisting();
|
||||
} catch (e) {
|
||||
log("上传失败:" + e.message);
|
||||
}
|
||||
};
|
||||
|
||||
(async function setModeHint() {
|
||||
try {
|
||||
var r = await fetch("/api/mode");
|
||||
var d = await r.json();
|
||||
var el = document.getElementById("mode-hint");
|
||||
el.textContent = d.mode === "file" ? "当前为「文件模式」:直接读写边缘 config/targets.csv,保存后自动执行脚本。" : "当前为「内存模式」:请先上传 targets.csv,编辑后可下载到本机再部署到边缘。";
|
||||
} catch (e) {}
|
||||
})();
|
||||
|
||||
(async function setMapConfig() {
|
||||
try {
|
||||
var r = await fetch("/api/map-config");
|
||||
var d = await r.json();
|
||||
if (d.cacheBaseUrl) {
|
||||
cacheBaseUrl = d.cacheBaseUrl;
|
||||
document.getElementById("cache-buttons").style.display = "inline";
|
||||
}
|
||||
} catch (e) {}
|
||||
})();
|
||||
|
||||
(function () {
|
||||
try {
|
||||
var saved = localStorage.getItem("tianditu_tk");
|
||||
if (saved) document.getElementById("tianditu-tk").value = saved;
|
||||
} catch (e) {}
|
||||
})();
|
||||
|
||||
document.getElementById("btn-save").onclick = async () => {
|
||||
const payload = {
|
||||
type: document.getElementById("type").value,
|
||||
ip: document.getElementById("ip").value.trim(),
|
||||
name: document.getElementById("name").value.trim(),
|
||||
role: document.getElementById("role").value,
|
||||
parent: document.getElementById("parent").value.trim(),
|
||||
uplink_type: document.getElementById("uplink_type").value,
|
||||
location: document.getElementById("location").value.trim(),
|
||||
lat: document.getElementById("lat").value.trim(),
|
||||
lon: document.getElementById("lon").value.trim(),
|
||||
};
|
||||
|
||||
if (!payload.name || !payload.type) {
|
||||
log("type 与 name 为必填。");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
log("正在保存并重建配置…");
|
||||
const resp = await fetch("/api/targets", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (!resp.ok) {
|
||||
throw new Error(data.error || "请求失败");
|
||||
}
|
||||
log("已写入 edge-agent/config/targets.csv,并触发 update-configs.sh / csv-to-topology-geojson.sh。");
|
||||
loadExisting();
|
||||
} catch (e) {
|
||||
log("保存失败:" + e.message);
|
||||
}
|
||||
};
|
||||
|
||||
loadExisting();
|
||||
</script>
|
||||
267
topology-editor/server.js
Normal file
267
topology-editor/server.js
Normal file
@@ -0,0 +1,267 @@
|
||||
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');
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user