# 数据采集系统 (Collectors) ## 一、系统架构 ``` ┌─────────────────────────────────────────────────────────────────┐ │ 数据采集系统架构 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ TOP500 │ │ Epoch AI │ │ HuggingFace │ │ │ │ 采集器 │ │ 采集器 │ │ 采集器 │ │ │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ │ │ │ │ │ │ └───────────────────┼───────────────────┘ │ │ ▼ │ │ ┌─────────────────────┐ │ │ │ BaseCollector │◄── 基类 (统一处理) │ │ │ run() 方法 │ │ │ └─────────┬───────────┘ │ │ │ │ │ ┌─────────────────┼─────────────────┐ │ │ ▼ ▼ ▼ │ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │ │ fetch() │ │transform()│ │ _save_data│ │ │ │ 获取原始数据 │ │ 数据转换 │ │ 保存到DB │ │ │ └───────────┘ └───────────┘ └───────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────┐ │ │ │ CollectedData 表 │◄── 统一存储 │ │ └─────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ Scheduler (APScheduler) │ │ │ │ 定时任务调度: 每4小时/6小时/12小时/1天 自动执行 │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────┘ ``` ## 二、工作流程 (Pipeline) ```python # 1. Scheduler 触发 (定时 或 手动触发) # ↓ # 2. run() 方法执行完整流水线 async def run(self, db): # 2.1 检查采集器是否启用 if not collector_registry.is_active(self.name): return {"status": "skipped"} # 2.2 记录任务开始 task = CollectionTask(status="running") db.add(task) await db.commit() # 2.3 FETCH - 获取原始数据 (由子类实现) raw_data = await self.fetch() # 2.4 TRANSFORM - 转换为统一格式 data = self.transform(raw_data) # 2.5 SAVE - 保存到数据库 records_count = await self._save_data(db, data) # 2.6 记录任务完成 task.status = "success" task.records_processed = records_count await db.commit() ``` **核心文件**: `backend/app/services/collectors/base.py` 手动触发、删除数据、清理缓存现在统一进入 PostgreSQL 数据作业队列,任务账本仍是 `collection_tasks`。采集器只负责 `fetch -> transform -> save`,由 `data_jobs.py` worker 领取 `collect` / `clear_data` / `clear_cache` / `earth_refresh` 任务并回写进度。Earth 图层刷新关系集中在 `earth_layer_adapters.py`,不要再在单个采集器或按钮里手写缓存失效和 WebSocket 广播。 ## 三、采集器列表 | 采集器 | 数据类型 | 数据内容 | 采集频率 | |--------|----------|----------|----------| | TOP500 | supercomputer | 全球超级计算机排名 (算力、性能) | 4小时 | | Epoch AI | gpu_cluster | GPU算力集群信息 | 6小时 | | HuggingFace Models | model | AI模型信息 | 12小时 | | HuggingFace Datasets | dataset | 数据集信息 | 12小时 | | HuggingFace Spaces | space | Demo应用 | 1天 | | PeeringDB | ixp/network/facility | 互联网交换点/网络/机房 | 1-2天 | | TeleGeography | submarine_cable | 海底光缆信息 | 7天 | | Space-Track TLE | satellite_tle | 卫星轨道 TLE 数据 | 依采集器配置 | | BarentsWatch AIS | vessel | 船只位置、航速、航向、MMSI 等 AIS 数据 | 依采集器配置 | | AISStream Vessels | vessel_ais | AIS WebSocket 实时流,写入原始观测层并由聚合接口展示 | 依采集器配置 | AIS 船只类采集器和其它 `CollectedData` 采集器的落库路径不同。BarentsWatch、AISStream 和自定义 `vessel_ais` 源都会进入 AIS 原始观测层,随后由聚合服务合并成 Earth 船只图层使用的 GeoJSON 和详情数据。这样做可以保留来源、传输方式、字段冲突和观测时间,避免某个实时源直接覆盖最终展示表。 智能星球国界不再属于采集器体系。它是智能星球静态渲染资产,由控制台 `运维与配置 -> 智能星球内容 -> 国界精度` 维护源配置,并由 `/api/v1/earth/boundaries/*` 构建 `frontend/public/earth/data/boundaries/earth-boundaries-china-pov-v1.pmtiles`。本地没有高精 PMTiles 时,前端会使用仓库内置的低精度 GeoJSON 作为 fallback,不会向 `CollectedData` 写入国界记录。 TOP500 和 Epoch AI 算力数据的公开源不总是提供可用经纬度。Earth 统一算力中心接口在主地图启动链路中只使用源数据自带坐标或 `compute_center_locations` 维表坐标;缺少坐标的记录会进入 `unresolved`,不会通过本地注册表、国家质心或猜测城市自动渲染。用户手动采集候选时,后端会用源字段调用 ROR 组织注册 API 和 Nominatim/OpenStreetMap 在线搜索;候选经前端保存后写入 `compute_center_locations`,后续地图刷新再从维表渲染。 ## 四、数据格式 (统一存储到 CollectedData 表) ```python # 每个采集器 parse_response() 返回格式 { "source_id": "top500_1", # 原始系统ID (必填) "name": "El Capitan", # 名称 (必填) "description": "系统描述...", # 描述 "country": "United States", # 国家 "city": "Livermore, CA", # 城市 "latitude": "37.6819", # 纬度 (字符串) "longitude": "-121.7681", # 经度 (字符串) "value": "1742.00", # 性能值 (如算力) "unit": "PFlop/s", # 单位 "metadata": { # 额外数据 (JSON) "rank": 1, "r_peak": 2746.38, "cores": 11039616 }, "reference_date": "2025-11-01" # 数据参考日期 } ``` ## 五、数据库表结构 **CollectedData 表** (`collected_data`) | 字段 | 类型 | 说明 | |------|------|------| | id | SERIAL | 主键 | | source | VARCHAR(100) | 数据源名称 (top500, huggingface等) | | source_id | VARCHAR(100) | 原始数据ID | | data_type | VARCHAR(50) | 数据类型 (supercomputer, model等) | | name | VARCHAR(500) | 名称 | | title | VARCHAR(500) | 标题 | | description | TEXT | 描述 | | country | VARCHAR(100) | 国家 | | city | VARCHAR(100) | 城市 | | latitude | VARCHAR(50) | 纬度 | | longitude | VARCHAR(50) | 经度 | | value | VARCHAR(100) | 性能值 | | unit | VARCHAR(20) | 单位 | | metadata | JSONB | 额外元数据 | | collected_at | TIMESTAMP | 采集时间 | | reference_date | TIMESTAMP | 数据参考日期 | | is_valid | INTEGER | 是否有效 | **核心文件**: `backend/app/models/collected_data.py` ## 六、TOP500 采集器示例 (完整流程) ```python # 1. fetch() - 从网页获取HTML async def fetch(self): url = "https://top500.org/lists/top500/list/2025/11/" response = await client.get(url) return response.text # 返回HTML # 2. parse_response() - 解析HTML为统一格式 def parse_response(self, html): soup = BeautifulSoup(html, "html.parser") table = soup.find("table") for row in table.find_all("tr")[1:]: # 跳过表头 cells = row.find_all("td") entry = { "source_id": f"top500_{cells[0].text}", # "top500_1" "name": cells[1].text.strip(), # "El Capitan" "country": cells[2].text.strip(), # "United States" "city": "", # 城市 "latitude": "", # 需进一步解析 "longitude": "", "value": "1742.00", # Rmax "unit": "PFlop/s", "metadata": { "rank": 1, "cores": "11340000" }, "reference_date": "2025-11-01" } data.append(entry) return data # 3. run() 自动调用 _save_data() 保存到数据库 ``` **核心文件**: `backend/app/services/collectors/top500.py` ## 七、调度机制 ```python # 启动时注册所有采集器到定时任务 def start_scheduler(): for name, collector in collectors.items(): if collector_registry.is_active(name): scheduler.add_job( run_collector_task, trigger=IntervalTrigger(hours=collector.frequency_hours), id=name, name=name ) ``` | 采集器 | 采集频率 | |--------|----------| | TOP500 | 每4小时 | | Epoch AI | 每6小时 | | HuggingFace | 每12小时 | | PeeringDB | 每1-2天 | | TeleGeography | 每7天 | **核心文件**: `backend/app/services/scheduler.py` ### 成功采集与连接状态 内置采集器成功采集后,调度器会记录当前有效配置已通过连接验证: ```python if datasource.last_status == "success": effective_candidate = await get_builtin_effective_candidate(db, datasource.source) checksum, _ = await build_builtin_connectivity_checksum(...) await save_connectivity_success( db, datasource.source, checksum, {"status_code": None}, connected_by="collection", ) ``` 这个记录用于控制台“采集管理 -> 采集器”中的连接状态判断:如果当前配置和成功采集时的 checksum 一致,就视为已连接,不要求用户再手动点击连接按钮。只有 endpoint、请求头、基础配置或凭证指纹变化时,才需要重新验证。 ### 采集管理与快照 Admin 的采集管理入口按业务层级组织: - `采集器`:配置 endpoint、认证方式、请求头、基础参数、启用状态和凭证教程。 - `采集调度`:查看和调整调度状态,触发、停止或刷新采集任务。 - `采集历史 / 快照`:按采集器聚合展示历史,详情中用快照选择器查看同一采集器的不同版本。 快照列表不应把同一个采集器的每次快照都拍平成独立主列表项。主列表负责选择采集器,详情区负责时间版本切换。 凭证教程由 `backend/app/services/credential_guides.py` 维护,控制台通过 read / generate / reset 三个动作读取或生成 Markdown 教程。教程面向运维配置人员,前端应渲染 Markdown 正文,而不是展示生成 prompt 或原始元数据。 相关实现见: - [datasource_connectivity.py](/home/ray/dev/linkong/planet/backend/app/services/datasource_connectivity.py) - [credential_guides.py](/home/ray/dev/linkong/planet/backend/app/services/credential_guides.py) - [数据源、采集器设置与连接验证](/home/ray/dev/linkong/planet/docs/technical/zh/datasource-collector-settings-connectivity.md) ## 八、相关代码文件 ``` backend/app/services/collectors/ ├── base.py # 基类: run() 流水线, _save_data() 保存 ├── registry.py # 采集器注册表 ├── scheduler.py # 定时任务调度 (APScheduler) ├── top500.py # TOP500采集器 ├── epoch_ai.py # Epoch AI采集器 ├── huggingface.py # HuggingFace采集器 ├── peeringdb.py # PeeringDB采集器 ├── telegeraphy.py # TeleGeography海底光缆采集器 ├── vessel_ais.py # BarentsWatch AIS 船只采集器 ├── aisstream.py # AISStream WebSocket 船只采集器 └── earth_boundaries.py # Earth 国界源校验和静态瓦片 artifact 采集器 backend/app/services/ ├── custom_datasource_runtime.py # 自定义 REST / WebSocket 映射运行时 ├── datasource_mapping.py # 确定性字段映射与目标写入 ├── vessel_ais_aggregation.py # AIS 原始观测写入与聚合读取 ├── vessel_aggregation_strategy.py # 多源字段选择、freshness fallback 和冲突记录 └── vessel_enrichment.py # 船舶资料富化缓存 backend/app/models/ ├── collected_data.py # 统一数据模型 └── vessel_enrichment.py # 船舶富化结果缓存 ``` ## 九、凭证型采集器 部分采集器需要外部服务凭证,例如: | 采集器 | credential provider | 凭证来源 | | --- | --- | --- | | `barentswatch_vessels` | `barentswatch` | 控制台“采集管理 -> 采集器”、环境变量、`~/.zshrc` | | `aisstream_vessels` | `aisstream` | 控制台“采集管理 -> 采集器”、环境变量、`~/.zshrc`(连接验证可读;正式采集建议保存到“采集管理 -> 采集器”或注入后端环境) | | `spacetrack_tle` | `spacetrack` | 环境变量、`~/.zshrc` | ### BarentsWatch AIS BarentsWatch AIS 的凭证解析统一在: - [barentswatch.py](/home/ray/dev/linkong/planet/backend/app/services/barentswatch.py) `VesselAISCollector` 只负责采集和转换 AIS 数据,不再自己读取环境变量或拼 token 请求。它通过: - `resolve_barentswatch_config()` - `fetch_barentswatch_access_token()` 获取运行时配置。 解析优先级: 1. `DataSourceConfig.auth_config` 2. `DataSourceConfig.config` 3. 环境变量 4. `~/.zshrc` 支持变量: ```bash export BARENTSWATCH_CLIENT_ID="..." export BARENTSWATCH_CLIENT_SECRET="..." ``` 并兼容历史拼写: ```bash export BARRENTSWATCH_CLIENT_ID="..." export BARRENTSWATCH_CLIENT_SECRET="..." ``` 连接验证会先请求 `https://id.barentswatch.no/connect/token` 获取 `scope=ais` 的 access token,再用 `Authorization: Bearer ` 请求 AIS endpoint。 ### AISStream 实时船舶 AISStream 使用 `wss://stream.aisstream.io/v0/stream` WebSocket endpoint。默认运行方式是长连接实时采集,而不是传统 REST collector 的“请求一次、进度到 100%、完成”模型。 运行时配置: - `api_key`:优先从 `DataSourceConfig.auth_config.api_key` 或 `config.api_key` 读取;也可由后端进程环境变量 `AISSTREAM_API_KEY` 提供。 - `bounding_boxes`:AISStream 订阅范围,默认示例为全球 `[[[-90, -180], [90, 180]]]`,生产或演示建议先缩小区域。 - `message_types`:默认 `PositionReport` 和 `ShipStaticData`。 - `streaming_enabled`:默认启用长连接;关闭后回退到批次式 `fetch -> transform -> save`。 - `streaming_max_messages`:测试用上限,非 0 时收到指定消息数后停止。 - `reconnect_delay_seconds`、`receive_timeout_seconds`:控制断线重连和空闲等待。 状态语义: - `connecting`:正在连接 AISStream。 - `streaming`:持续接收实时消息,`records_processed` 表示已见消息数,通常没有固定总量和百分比。 - `reconnecting`:上游断开或网络异常,采集器记录 `AISSourceHealth` 后等待重连。 - `stopped` / `cancelled`:任务被测试上限或用户停止。 AISStream 连接验证会通过 `datasource_connectivity.py` 读取保存的采集器配置、环境变量和 `~/.zshrc` 中的 `AISSTREAM_API_KEY`。正式采集时,最稳妥的方式是把 API Key 保存到“采集管理 -> 采集器 -> AISStream 实时船舶”;如果只放在 `~/.zshrc`,需要确认后端进程实际继承到了该环境变量。 控制台通过 `/datasources -> 实时流` 管理 AISStream,而不是把它放进普通有限采集任务的进度条。实时流 API 会聚合运行态、健康状态、配置摘要和 raw observation 计数: ```http GET /api/v1/realtime-sources POST /api/v1/realtime-sources/{source}/start POST /api/v1/realtime-sources/{source}/stop POST /api/v1/realtime-sources/{source}/restart ``` `aisstream_vessels` 和自定义 `source_type=websocket` 数据源会出现在该接口中。它们不参与一键采集百分比;前端按长连接服务展示消息计数、延迟、最近成功和最近错误。 ### AIS 原始观测与聚合 AIS 观测写入后不会直接替换最终船只记录,而是先保存为 raw observation: - `source` 记录来源,例如 `barentswatch_vessels`、`aisstream_vessels` 或自定义源名称。 - `delivery_mode` 表达实时性,`realtime_stream` 优先于 `polling`。 - `transport` 记录 `websocket` 或 `http`。 - 位置、速度、航向等动态字段会按 freshness 和来源优先级选择。 - 静态字段优先保留非空值;冲突候选会记录到详情接口,便于排查多源差异。 Earth 船只展示现在使用受控快照接口和实时增量通道: ```http GET /api/v1/vessels/snapshot?bbox=lon_min,lat_min,lon_max,lat_max&zoom=12&limit=1000 GET /api/v1/visualization/vessels/{mmsi} GET /api/v1/visualization/vessels/{mmsi}/track GET /api/v1/visualization/vessels/{mmsi}/conflicts ``` `/api/v1/vessels/snapshot` 必须携带 `bbox` 和 `zoom`,默认 `limit=1000`,最大 `limit=5000`。它优先消费 `ais_raw_observations` 聚合结果;当当前 raw 窗口为空时,会受控回退到 legacy `vessel_position` / `vessel_static` 最新点,并在 `diagnostics.legacy_fallback_used` 中标明。旧 `/api/v1/visualization/geo/vessels` 路由已移除。 实时增量通过 `/ws` 的 `vessels` channel 推送。客户端订阅时必须带当前视口: ```json { "type": "subscribe", "data": { "channel": "vessels", "bbox": [120.8, 30.7, 122.1, 31.8], "zoom": 12, "limit": 1000 } } ``` 后端按连接保存轻量订阅条件,只向 bbox 命中的连接发送船只更新。collector 广播会先进入 1 秒节流队列,同一 MMSI 在一个 flush 周期内只保留最新位置,避免高频实时流拖垮 WebSocket。 ### 图层接口与全量统计分离 Earth 后续迁移到两类接口: ```http GET /api/v1/data-products GET /api/v1/data-products/{product_id}/status GET /api/v1/layers/vessels/snapshot?bbox=lon_min,lat_min,lon_max,lat_max&zoom=12&limit=1000 GET /api/v1/layers/cables?bbox=lon_min,lat_min,lon_max,lat_max&zoom=12&limit=1000 GET /api/v1/layers/landing-points?bbox=lon_min,lat_min,lon_max,lat_max&zoom=12&limit=1000 GET /api/v1/layers/satellites?bbox=lon_min,lat_min,lon_max,lat_max&zoom=12&limit=1000 GET /api/v1/layers/bgp/anomalies?bbox=lon_min,lat_min,lon_max,lat_max&zoom=12&limit=1000 GET /api/v1/layers/bgp/incidents?bbox=lon_min,lat_min,lon_max,lat_max&zoom=12&limit=1000 GET /api/v1/layers/bgp/collectors?bbox=lon_min,lat_min,lon_max,lat_max&zoom=12&limit=1000 ``` `/api/v1/data-products/*` 面向聚合面板,统计口径是全量/全局,不受地图 bbox 影响。`/api/v1/layers/*` 面向地图渲染,必须携带 `bbox` 和 `zoom`,默认 `limit=1000`,最大 `limit=5000`;低 zoom 会降级到更小的返回上限,并在 `diagnostics` 中暴露 `degraded`、`truncated`、`limit_clamped` 和 `stats_scope=viewport`。当前版本的非船只图层先复用已有 GeoJSON 转换再做保护层,后续可继续把 bbox 下推到各产品专用查询。 ## 十、采集器设置与连接验证 控制台的“采集管理 -> 采集器”页提供所有内置采集器的 endpoint、请求头、超时、重试和凭证配置。连接验证不是只看前端按钮状态,而是由后端计算 checksum: - endpoint - auth type - headers - config - credential provider - 凭证指纹 相关 API: ```http GET /api/v1/datasources/configs/all POST /api/v1/datasources/configs/builtin/connection-status POST /api/v1/datasources/configs/builtin/connect POST /api/v1/settings/integrations/barentswatch/connect GET /api/v1/settings/credential-guides/{provider} POST /api/v1/settings/credential-guides/{provider}/generate POST /api/v1/settings/credential-guides/{provider}/reset ``` 更多细节见: - [数据源、采集器设置与连接验证](/home/ray/dev/linkong/planet/docs/technical/zh/datasource-collector-settings-connectivity.md) ## 十一、数据使用场景 采集的数据最终会: 1. **可视化展示** - 在UE5大屏上显示超级计算机、GPU集群、海底光缆的地理位置 2. **态势分析** - 统计全球算力分布、增长趋势 3. **告警系统** - 检测重要节点变化 ## 十二、采集器注册机制 采集器在应用启动时自动注册: ```python # backend/app/services/collectors/__init__.py collector_registry.register(TOP500Collector()) collector_registry.register(EpochAIGPUCollector()) collector_registry.register(HuggingFaceModelCollector()) collector_registry.register(HuggingFaceDatasetCollector()) collector_registry.register(HuggingFaceSpacesCollector()) collector_registry.register(PeeringDBIXPCollector()) collector_registry.register(PeeringDBNetworkCollector()) collector_registry.register(PeeringDBFacilityCollector()) collector_registry.register(TeleGeographyCableCollector()) collector_registry.register(TeleGeographyLandingPointCollector()) collector_registry.register(TeleGeographyCableSystemCollector()) ``` **核心文件**: `backend/app/services/collectors/registry.py` ## 十三、触发采集 ### 方式一:定时触发 系统启动时,APScheduler会自动根据各采集器的`frequency_hours`设置定时任务。 ### 方式二:手动触发 API ```bash # 触发TOP500采集 curl -X POST http://localhost:8000/api/v1/datasources/1/trigger \ -H "Authorization: Bearer " ``` 批量采集使用: ```http POST /api/v1/datasources/trigger-batch ``` 请求体可以传 `source_ids` 精确触发选中项;如果不传 `source_ids`,后端按 `product`、`module`、`is_active`、`run_status`、`collected`、`credential_status` 和 `q` 过滤后触发。接口会跳过禁用源、正在运行且未 `force` 的源,以及未到频率窗口的源,并返回 `triggered`、`skipped`、`failed` 三组结果。 **核心文件**: `backend/app/api/v1/datasources.py`