release: bump version to 0.48.0
This commit is contained in:
@@ -8,6 +8,22 @@ This project follows the repository versioning rule:
|
||||
- `improvement` -> `+0.0.1`(bugfix + 小功能混合)
|
||||
- `bugfix` -> `+0.0.1`
|
||||
|
||||
## [0.48.0] — 2026-05-07
|
||||
|
||||
Released: 2026-05-07
|
||||
|
||||
### ✨ Highlights
|
||||
- 自定义数据源新增 REST / WebSocket 映射运行时,并提供本地 AIS mock WebSocket,用于实时船只 upsert 链路验证。
|
||||
- AIS 原始观测、聚合策略、字段来源、冲突记录与船舶 enrichment 继续完善,Earth 船只实时展示链路更接近生产数据形态。
|
||||
- Earth 全球态势 summary 改为轻量 SQL 聚合,并在卫星 current 异常时回退到最近有效 TLE 批次,避免统计接口被大规模明细读取拖慢。
|
||||
|
||||
### 🔧 Improvements
|
||||
- 修复 `/geo/summary` 与 `/geo/satellites` 在大表下加载慢或超时的问题,并补充 `collected_data` 与 AIS raw 相关索引。
|
||||
- WebSocket 管理器支持匿名连接、频道订阅清理和更稳的连接生命周期测试,前端 WebSocket candidates / fallback 更可靠。
|
||||
- `planet.sh` 强化端口释放、端口诊断和前端启动流程,mock AIS server 提供 Bun 脚本入口。
|
||||
|
||||
---
|
||||
|
||||
## [0.47.0] — 2026-04-30
|
||||
|
||||
Released: 2026-04-30
|
||||
|
||||
384
docs/plans/custom-source-live-mock-plan.md
Normal file
384
docs/plans/custom-source-live-mock-plan.md
Normal file
@@ -0,0 +1,384 @@
|
||||
# Custom Source Live Mock 计划
|
||||
|
||||
**状态**:实施中
|
||||
**创建日期**:2026-05-01
|
||||
**任务名**:`Custom Source Live Mock`
|
||||
**核心目标**:把自定义源升级为同时支持 REST 与 WebSocket 的可映射采集入口,并提供本地 AIS mock WebSocket 服务,用于验证 Earth 船只实时新增与 upsert 链路。
|
||||
|
||||
## 背景
|
||||
|
||||
真实 AIS 接口变化频率不可控,无法稳定验证 Earth 页面“不刷新也能看到新船只”的实时链路。当前系统已经有自定义源基础设施:
|
||||
|
||||
- `datasource_configs` 保存 endpoint、auth、headers、config。
|
||||
- `datasource_mapping_templates` 保存目标 schema 的确定性映射模板。
|
||||
- `run-mapped` 支持保存后的自定义 REST 源通过 active mapping 写入目标数据。
|
||||
|
||||
但现有能力主要面向 REST sample 和批量 mapping,缺少以下能力:
|
||||
|
||||
- 自定义源不能明确选择 `REST` 或 `WebSocket` 采集模式。
|
||||
- WebSocket 长连接、订阅消息、重连、消息路径提取还没有通用 runtime。
|
||||
- `vessel_ais` 自定义数据写入后需要进入 AIS raw observation 和 `vessels` WS channel,才能真实验证 Earth 实时 upsert。
|
||||
- 删除自定义源时没有清晰的数据清理选项。
|
||||
- 设置中心里“采集调度 / 凭证 / 自定义源”入口混杂,用户很难判断该在哪里配置。
|
||||
|
||||
## 已确认决策
|
||||
|
||||
| 项目 | 决策 |
|
||||
|-----|------|
|
||||
| 计划名称 | `Custom Source Live Mock` |
|
||||
| 自定义源传输类型 | 支持 `REST` 与 `WebSocket` |
|
||||
| 采集写入方式 | 先映射到目标 schema,再由 destination handler 写入 |
|
||||
| AIS mock 目标 | 优先打通 `vessel_ais`,验证 Earth 船只实时新增和同 MMSI upsert |
|
||||
| mock 服务 runtime | 使用 `bun` 启动本地 mock WS 服务 |
|
||||
| 凭证配置 | 支持 headers、bearer、api key、basic,并保留 query/header API key 位置配置 |
|
||||
| 删除策略 | 删除自定义源时允许选择是否删除该源写入的数据 |
|
||||
| 合并语义 | 自定义源必须选择“合并到哪个内置数据”,作为内置源的补充数据进入同一聚合链路 |
|
||||
| UI 方向 | 自定义源创建和维护放在“配置中心 > 采集器设置”的采集器下拉框内联入口;数据源页保留总览与运行控制 |
|
||||
|
||||
## 范围
|
||||
|
||||
### 本阶段要做
|
||||
|
||||
- 自定义源可选择 `REST` 或 `WebSocket`。
|
||||
- 自定义源支持请求头、凭证、query params、body、WS subscribe message。
|
||||
- WebSocket 自定义源支持长连接、重连、消息解析、mapping、写入。
|
||||
- `vessel_ais` 自定义源写入 AIS raw observations,并广播 `vessels` channel。
|
||||
- 提供 mock AIS WS 服务,持续发送新增 MMSI 和位置变更。
|
||||
- 删除自定义源时提供“是否删除该源数据”的选项。
|
||||
- 梳理设置中心信息架构,明确后续 UI 重构方向。
|
||||
|
||||
### 暂不做
|
||||
|
||||
- 不新增任意动态数据库表。
|
||||
- 不允许用户提交可执行脚本作为 mapping。
|
||||
- 不让 LLM 进入正式采集链路。
|
||||
- 不把 mock 数据直接写 legacy `vessel_position`,优先写 AIS raw observations,保持可追踪和可删除。
|
||||
- 不在本阶段完成完整 `Earth Live Sync`,但要为后续 summary invalidation 留出 hook。
|
||||
|
||||
## 现状入口
|
||||
|
||||
| 能力 | 当前位置 |
|
||||
|-----|----------|
|
||||
| 自定义源配置模型 | `backend/app/models/datasource_config.py` |
|
||||
| 自定义源 mapping 模型 | `backend/app/models/datasource_mapping.py` |
|
||||
| 自定义源 API | `backend/app/api/v1/datasource_config.py` |
|
||||
| 目标 schema registry | `backend/app/core/target_schema_registry.py` |
|
||||
| mapping engine | `backend/app/services/datasource_mapping.py` |
|
||||
| 数据源总览 UI | `frontend/src/pages/DataSources/DataSources.tsx` |
|
||||
| 采集器设置 UI | `frontend/src/pages/Settings/Settings.tsx` |
|
||||
|
||||
## 目标架构
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[Custom Source Config] --> B{source_type}
|
||||
B -->|rest| C[Mapped REST Runner]
|
||||
B -->|websocket| D[Mapped WS Runner]
|
||||
C --> E[Mapping Engine]
|
||||
D --> E
|
||||
E --> F[Target Schema Validator]
|
||||
F --> G{Destination Handler}
|
||||
G -->|vessel_ais| H[AIS Raw Observations]
|
||||
H --> I[AIS Aggregation]
|
||||
H --> J[vessels WS Channel]
|
||||
J --> K[Earth Vessel Upsert]
|
||||
```
|
||||
|
||||
## 数据配置设计
|
||||
|
||||
短期可以继续复用 `DataSourceConfig`,避免大迁移。语义约定如下:
|
||||
|
||||
| 字段 | 用途 |
|
||||
|-----|------|
|
||||
| `name` | 自定义源唯一名称,例如 `mock_ais_ws` |
|
||||
| `source_type` | `rest` 或 `websocket` |
|
||||
| `endpoint` | `http(s)://...` 或 `ws(s)://...` |
|
||||
| `auth_type` | `none`、`bearer`、`api_key`、`basic` |
|
||||
| `auth_config` | token、api_key、key name、basic username/password 等 |
|
||||
| `headers` | 静态请求头 |
|
||||
| `config` | method、params、body、timeout、retry、WS 订阅消息、重连策略、消息路径等 |
|
||||
|
||||
建议 `config` 结构:
|
||||
|
||||
```json
|
||||
{
|
||||
"transport": "websocket",
|
||||
"delivery_mode": "realtime_stream",
|
||||
"merge_target_source": "barentswatch_vessels",
|
||||
"target_schema": "vessel_ais",
|
||||
"method": "GET",
|
||||
"params": {},
|
||||
"body": null,
|
||||
"timeout": 30,
|
||||
"retry": 3,
|
||||
"ws_subscribe_message": {"type": "subscribe", "channel": "vessels"},
|
||||
"ws_message_path": "$.data",
|
||||
"ws_items_path": "$.vessels[*]",
|
||||
"ws_reconnect": true,
|
||||
"reconnect_delay_seconds": 3,
|
||||
"debug_max_messages": null,
|
||||
"delete_policy": "config_only"
|
||||
}
|
||||
```
|
||||
|
||||
## 后端实施计划
|
||||
|
||||
### Phase 1 — 自定义源类型与连接测试
|
||||
|
||||
- 允许 `source_type` 为 `rest` 或 `websocket`。
|
||||
- REST 连接测试保留现有 HTTP 请求逻辑。
|
||||
- WebSocket 连接测试新增:
|
||||
- 校验 endpoint 必须是 `ws://` 或 `wss://`。
|
||||
- 注入 headers 和 auth。
|
||||
- 连接后可选发送 `ws_subscribe_message`。
|
||||
- 读取一条消息或超时返回诊断。
|
||||
|
||||
### Phase 2 — Mapped REST Runner 补齐
|
||||
|
||||
现有 `run-mapped` 继续作为 REST 一次性采集入口,补齐:
|
||||
|
||||
- `GET/POST` method。
|
||||
- query params。
|
||||
- JSON body。
|
||||
- headers 和 auth 注入。
|
||||
- sample limit 与响应大小限制。
|
||||
- `vessel_ais` destination handler。
|
||||
|
||||
### Phase 3 — Mapped WebSocket Runner
|
||||
|
||||
新增通用 WebSocket runner,读取 `DataSourceConfig + active mapping`:
|
||||
|
||||
- 建立长连接。
|
||||
- 发送可选订阅消息。
|
||||
- 循环接收消息。
|
||||
- JSON parse。
|
||||
- 按 `ws_message_path/ws_items_path` 提取 item 或 list。
|
||||
- 使用 mapping engine 转换。
|
||||
- 使用 target schema validator 校验。
|
||||
- 调用 destination handler 写入。
|
||||
- 更新采集任务状态:
|
||||
- `connecting`
|
||||
- `streaming`
|
||||
- `reconnecting`
|
||||
- `stopped`
|
||||
- 维护运行指标:
|
||||
- `messages_seen`
|
||||
- `records_written`
|
||||
- `unique_entities`
|
||||
- `last_message_at`
|
||||
- `last_error`
|
||||
- 后台长连接不读取 `config.debug_max_messages`;该字段只用于显式的一次性调试运行,避免正式 WS 流被测试上限截断。
|
||||
|
||||
### Phase 4 — Destination Handler
|
||||
|
||||
为 target schema 建立明确写入处理器。
|
||||
|
||||
`vessel_ais` handler:
|
||||
|
||||
- 写入 `AISRawObservation`。
|
||||
- `source = datasource.name`。
|
||||
- `delivery_mode` 来自 config,默认 WS 为 `realtime_stream`、REST 为 `polling`。
|
||||
- `transport` 来自 `source_type`。
|
||||
- 生成幂等 observation hash。
|
||||
- 更新 AIS source health。
|
||||
- 广播 `vessels` channel,payload 使用当前 Earth 已支持的 upsert 格式。
|
||||
|
||||
`generic_records` handler:
|
||||
|
||||
- 写入通用 collected data 或后续 generic store。
|
||||
- 不直接进入 Earth。
|
||||
|
||||
### Phase 5 — 删除与数据清理
|
||||
|
||||
删除自定义源时新增清理策略:
|
||||
|
||||
| 选项 | 行为 |
|
||||
|-----|------|
|
||||
| 只删除配置 | 删除 `datasource_configs`,保留 mapping 和历史数据需要另行处理 |
|
||||
| 删除配置和 mapping | 删除配置及对应 `datasource_mapping_templates` |
|
||||
| 删除配置、mapping 和该源数据 | 同时删除该源写入的数据 |
|
||||
|
||||
数据删除范围:
|
||||
|
||||
- `collected_data.source == datasource.name`
|
||||
- `ais_raw_observations.source == datasource.name`
|
||||
- `ais_source_health.source == datasource.name`
|
||||
|
||||
不建议直接删除 legacy `vessel_position`,因为当前 legacy 表不带 source,无法安全归因。自定义 AIS 源应优先只写 raw observations。
|
||||
|
||||
删除数据后应触发:
|
||||
|
||||
- `vessels` channel 的 reload/invalidation 事件,提示 Earth 重新拉船只聚合。
|
||||
- 后续接入 `Earth Live Sync` 后,触发 `earth_summary` invalidation。
|
||||
|
||||
### Phase 6 — Mock AIS WebSocket 服务
|
||||
|
||||
新增脚本:
|
||||
|
||||
`scripts/mock-ais-ws-server.ts`
|
||||
|
||||
运行方式建议:
|
||||
|
||||
```bash
|
||||
bun run mock:ais-ws
|
||||
```
|
||||
|
||||
服务行为:
|
||||
|
||||
- 监听 `ws://localhost:8787/ais`。
|
||||
- 接受任意客户端连接。
|
||||
- 可记录收到的 subscribe message。
|
||||
- 每 1-2 秒发送一条 AIS-like JSON。
|
||||
- 每隔 N 条生成新 MMSI,验证船只数量增长。
|
||||
- 已存在 MMSI 随时间改变 `lat/lon/cog/heading`,验证同 MMSI upsert。
|
||||
- 支持固定 seed,保证测试可复现。
|
||||
|
||||
示例 payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "vessel",
|
||||
"data": {
|
||||
"mmsi": "999000001",
|
||||
"name": "MOCK VESSEL 001",
|
||||
"lat": 31.23,
|
||||
"lon": 121.47,
|
||||
"sog": 12.4,
|
||||
"cog": 86,
|
||||
"heading": 90,
|
||||
"received_at": "2026-05-01T00:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 前端实施计划
|
||||
|
||||
### 信息架构调整
|
||||
|
||||
自定义源不作为割裂的新入口,而是作为内置采集器的补充源,直接纳入“配置中心 > 采集器设置”的采集器选择器:
|
||||
|
||||
- 采集器下拉框同时展示内置采集器和自定义补充源。
|
||||
- 下拉框右侧提供加号按钮,用于添加自定义源。
|
||||
- 新建自定义源时必须选择“合并到内置数据”,例如合并到 `barentswatch_vessels`。
|
||||
- 选择自定义源后,右侧基础配置区域沿用正常采集器配置形态,支持连接测试、保存、endpoint、headers、auth、高级 JSON。
|
||||
- 自定义源比内置源多一个“删除自定义源”按钮。
|
||||
- 删除时弹出确认框,可勾选“同时删除该自定义源生成的所有数据”。
|
||||
|
||||
数据源页保留:
|
||||
|
||||
- 内置源总览。
|
||||
- 内置源最近状态。
|
||||
- 内置源手动触发。
|
||||
- 不展示自定义源管理入口;自定义源创建、维护、删除统一在采集器设置中完成。
|
||||
|
||||
### 自定义源表单
|
||||
|
||||
新增或重构自定义源表单:
|
||||
|
||||
- 源名称。
|
||||
- 类型:`REST` / `WebSocket`。
|
||||
- 合并到内置数据:必选,用于声明该源补充哪个内置数据域。
|
||||
- endpoint。
|
||||
- method/body/params,仅 REST 显示。
|
||||
- subscribe message/message path/items path,仅 WS 显示。
|
||||
- auth type。
|
||||
- headers。
|
||||
- target schema。
|
||||
- sample/test 按钮。
|
||||
- mapping assistant/preview。
|
||||
- 保存并运行。
|
||||
|
||||
### 删除确认
|
||||
|
||||
删除自定义源时弹出确认:
|
||||
|
||||
- 默认只删除配置。
|
||||
- 可勾选删除 mapping。
|
||||
- 可勾选删除该源写入的数据。
|
||||
- 显示将删除的数据范围和不可恢复提示。
|
||||
|
||||
## 验证方案
|
||||
|
||||
### Mock WS 验证路径
|
||||
|
||||
1. 启动 mock 服务:
|
||||
|
||||
```bash
|
||||
bun run mock:ais-ws
|
||||
```
|
||||
|
||||
2. 新建自定义源:
|
||||
|
||||
| 字段 | 值 |
|
||||
|-----|----|
|
||||
| name | `mock_ais_ws` |
|
||||
| source_type | `websocket` |
|
||||
| endpoint | `ws://localhost:8787/ais` |
|
||||
| merge_target_source | `barentswatch_vessels` |
|
||||
| target_schema | `vessel_ais` |
|
||||
| ws_message_path | `$.data` |
|
||||
|
||||
3. 保存 active mapping:
|
||||
|
||||
```json
|
||||
{
|
||||
"source": {
|
||||
"items_path": "$"
|
||||
},
|
||||
"fields": {
|
||||
"mmsi": {"path": "$.mmsi", "type": "integer"},
|
||||
"name": {"path": "$.name", "type": "string"},
|
||||
"lat": {"path": "$.lat", "type": "float"},
|
||||
"lon": {"path": "$.lon", "type": "float"},
|
||||
"sog": {"path": "$.sog", "type": "float", "default": null},
|
||||
"cog": {"path": "$.cog", "type": "float", "default": null},
|
||||
"heading": {"path": "$.heading", "type": "integer", "default": null},
|
||||
"received_at": {"path": "$.received_at", "type": "datetime", "default": null}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. 启动自定义源。
|
||||
|
||||
5. 打开 Earth 船只图层,不刷新页面观察:
|
||||
|
||||
- `vessels` WS channel 收到 `source = mock_ais_ws`。
|
||||
- HUD 船只数在新 MMSI 到达时增加。
|
||||
- 地球出现 `MOCK VESSEL`。
|
||||
- 同 MMSI 后续消息更新位置和航向,不重复叠加。
|
||||
|
||||
### 自动化测试
|
||||
|
||||
后端测试:
|
||||
|
||||
- WebSocket 自定义源连接测试。
|
||||
- WS message path 和 items path 提取。
|
||||
- mapping 到 `vessel_ais`。
|
||||
- 写入 AIS raw observation。
|
||||
- 广播 `vessels` channel。
|
||||
- 删除自定义源时按策略删除 mapping 和源数据。
|
||||
|
||||
前端测试:
|
||||
|
||||
- REST/WS 表单条件显示。
|
||||
- 删除确认选项。
|
||||
- mock 源配置保存 payload。
|
||||
- mapping preview 展示错误和成功记录。
|
||||
|
||||
## 风险与约束
|
||||
|
||||
- WebSocket 自定义源是长连接,不能沿用一次性 REST 进度条。
|
||||
- 如果 mock 源写 legacy vessel 表,删除会变得不安全,因此先只写 raw observations。
|
||||
- 自定义 WS 可能消息量很大,必须有 backpressure、日志限流和任务取消能力。
|
||||
- 任意外部 WS 不能信任 payload,必须经过 mapping 和 schema validation。
|
||||
- headers/auth 不能进入 LLM mapping prompt。
|
||||
|
||||
## 交付顺序
|
||||
|
||||
1. Mock AIS WS 服务。
|
||||
2. 后端自定义 WS runner。
|
||||
3. `vessel_ais` destination handler 和 `vessels` broadcast。
|
||||
4. 删除自定义源及数据清理。
|
||||
5. 设置中心采集器下拉框内联自定义源 UI。
|
||||
6. 配置中心信息架构重整。
|
||||
7. 与 `Earth Live Sync` 对接 summary invalidation。
|
||||
@@ -1,6 +1,6 @@
|
||||
# AIS 多源采集、冲突记录与聚合接口计划
|
||||
|
||||
**状态**:v0-v3 已实现,v4+ 规划中
|
||||
**状态**:v0-v3 已实现,v3.1-v3.4 为 v4/v5 前置稳定化任务,v4 / v5 已落最小可用子集
|
||||
**创建日期**:2026-04-30
|
||||
**核心原则**:采集器只写原始观测;去重、合并、冲突解释放在聚合接口中完成
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
| 过期保护 | 实时流源断流超过 freshness 窗口后,不能仅凭“实时源”身份压过更新的轮询数据 |
|
||||
| 源健康状态 | 聚合时必须参考采集器健康状态,不能只看配置中的理论优先级 |
|
||||
| 媒体富化 | 船只图片等媒体信息不进入 AIS 实时聚合主链路,后续单独做 enrichment |
|
||||
| v4/v5 顺序 | 在聚合完整性、AISStream 实时链路、采集状态语义和基础身份信息显示修好之前,不进入策略配置和 enrichment UI |
|
||||
|
||||
## 背景
|
||||
|
||||
@@ -300,7 +301,7 @@ VesselFinder 等服务里的船只图片不属于 AIS 实时数据本身。图
|
||||
|
||||
## 版本拆分
|
||||
|
||||
计划按 5 个版本推进:
|
||||
计划先按 v0-v3 建立基础能力,再用 v3.1-v3.4 修复当前稳定性缺口,最后进入 v4/v5:
|
||||
|
||||
### v0 — 聚合基础设施(已实现)
|
||||
|
||||
@@ -343,27 +344,109 @@ VesselFinder 等服务里的船只图片不属于 AIS 实时数据本身。图
|
||||
5. 船名标准化会读取 AISStream `MetaData.ShipName`;船型展示会从 `vessel_type_name` 和 AIS 数字 `vessel_type` 共同归一化,保证 marker 颜色、详情卡、hover 和搜索结果一致。
|
||||
6. `/geo/vessels` 不再默认限制 5000 艘;不传 `limit` 或传 `limit=0` 表示全量返回,前端默认也不再二次裁剪到 5000。
|
||||
|
||||
### v4 — 策略配置
|
||||
### v3.1 — 聚合完整性修复(v4 前置)
|
||||
|
||||
目标是先保证“所有已采集到的船都能显示”,BarentsWatch 不因为接入 AISStream 而被 raw observation 聚合结果遮蔽。
|
||||
|
||||
当前风险是 `/geo/vessels` 只要 raw observation 聚合返回非空,就直接使用 raw 聚合结果,不再补读兼容层 `vessel_position + vessel_static`。如果 raw observation 中只存在 AISStream 的几百艘船,或 BarentsWatch 历史数据没有完整回填到 raw 层,最终 Earth 就会只显示 AISStream 子集。
|
||||
|
||||
1. `/geo/vessels` 必须合并 raw observation 聚合结果和 legacy latest position 结果。
|
||||
2. raw 与 legacy 同一 MMSI 同时存在时只显示一艘,优先使用 raw 聚合结果及其 `field_sources` / `selected_reasons`。
|
||||
3. raw 中不存在的 BarentsWatch-only MMSI 必须从 `vessel_position + vessel_static` 补齐。
|
||||
4. `bbox`、`type`、`limit` 过滤必须作用在合并后的最终集合上;不传 `limit` 或 `limit=0` 仍表示全量返回。
|
||||
5. 增加诊断统计,至少能看到 raw AISStream unique MMSI、raw BarentsWatch unique MMSI、legacy unique MMSI、final merged unique MMSI 和被 legacy 补齐的数量。
|
||||
6. 为 raw 只有 AISStream 子集、legacy 有更多 BarentsWatch 船只的场景补回归测试。
|
||||
|
||||
### v3.2 — AISStream 真实时链路(v4 前置)
|
||||
|
||||
目标是把 AISStream 从“一次 collector 收一批消息后结束”改成真正的 WebSocket 长连接实时数据源,并把实时变化推送到 Earth。
|
||||
|
||||
当前 `aisstream_vessels` 只在 collector `fetch()` 中连接 `wss://stream.aisstream.io/v0/stream`,默认收 `max_messages = 500` 条后结束。这不符合 WebSocket 流式数据源的运行语义,也不能保证新船、位置变化和航向变化实时出现在前端。
|
||||
|
||||
1. 为 AISStream 增加 streaming service / long-running runner,不再依赖单次 `fetch -> transform -> save -> completed` 表达实时采集。
|
||||
2. 外部 AISStream WebSocket 保持长连接,断线后指数退避重连,并持续更新 `AISSourceHealth`。
|
||||
3. 每条或小批量 AIS 消息标准化后写入 `ais_raw_observations`,按时间或数量短周期 commit,避免长事务堆积。
|
||||
4. 将新增船只、位置变化、航向变化和静态字段补充转换成 vessel delta。
|
||||
5. 通过应用内部 `/ws` 的 `vessels` channel 广播 delta,复用 `DataBroadcaster.broadcast_custom("vessels", payload)`。
|
||||
6. Earth 前端订阅 `vessels` channel,`vessels.js` 支持按 MMSI upsert marker,而不是每次全量 reload。
|
||||
7. 船只改变航向时,前端必须更新 course bin / marker bucket,避免 marker 方向滞后。
|
||||
8. freshness 超时或 AISStream 健康异常时,动态字段可回退到 BarentsWatch 最新可用观测。
|
||||
|
||||
### v3.3 — Streaming 采集状态语义(v4 前置)
|
||||
|
||||
目标是让采集页面正确表达 AISStream 这类长连接数据源,不再使用一次性 REST collector 的完成型进度条。
|
||||
|
||||
REST collector 的自然状态是 `fetch -> transform -> save -> progress 0..100 -> completed`。AISStream 的自然状态应是 `connecting -> streaming -> reconnecting -> stopped/failed`,没有固定总量,也不应在收到一批消息后显示“采集完成”。
|
||||
|
||||
1. AISStream 采集状态使用 indeterminate / streaming 状态,而不是百分比完成进度条。
|
||||
2. 设置页运行状态卡展示连接状态、已运行时长、本轮消息数、新增观测数、unique MMSI、message rate、最近消息时间、延迟和最近错误。
|
||||
3. `phase_message` 使用“正在接收 AISStream 实时消息”“重连中”“已停止”等长连接语义。
|
||||
4. 停止、重连和配置变更要有明确操作入口;配置变化后必须安全重订阅。
|
||||
5. 后端任务状态不能因为没有 `total_records` 就长期显示 `0%` 或误判失败。
|
||||
6. WebSocket 健康状态和 collector task 状态要分离:上游短暂断线是 `reconnecting`,不是普通采集任务完成或失败。
|
||||
|
||||
### v3.4 — 船只身份字段和名称聚合修复(v4 前置)
|
||||
|
||||
目标是把 MMSI、IMO、callsign 这类身份编号按字符串显示,并把仍然使用 MMSI 作为船名的记录视为信息聚合未完成,而不是正常船名。
|
||||
|
||||
1. 前端详情卡、hover、搜索结果和日志中的 `mmsi`、`imo`、`callsign` 必须作为 identifier 字段展示,禁止走 `toLocaleString()` 或数字千分位格式。
|
||||
2. GeoJSON 可增加 `mmsi_display` / `imo_display` 等字符串字段,但前端仍必须对 identifier key 做兜底格式保护。
|
||||
3. 聚合服务生成船名时,不能把 `MMSI 257123000` 当成真实 `name` 的成功结果;它只能作为 display fallback。
|
||||
4. 增加诊断查询,列出所有当前仍以 MMSI 号码或 `MMSI <number>` 作为船只名称的记录,包括:
|
||||
- `vessel_static.name` 为空或等于 MMSI fallback 的 MMSI;
|
||||
- raw observation 中没有任何非空 `name` / `MetaData.ShipName` / `ShipStaticData.Name` 的 MMSI;
|
||||
- 聚合结果最终 `name` 仍为 fallback 的 MMSI;
|
||||
- 每个 MMSI 的可用来源、最近观测时间、message types 和缺失原因。
|
||||
5. 对这些 fallback-name 船只建立待修复集合,优先通过 AISStream `ShipStaticData`、BarentsWatch 静态字段和后续 enrichment 缓存补齐。
|
||||
6. 船只详情面板需要区分“真实船名”和“显示兜底”:真实船名缺失时展示 `MMSI <id>` 可以继续作为标题,但字段来源应标注为 `fallback`,避免误以为聚合成功。
|
||||
7. 为 MMSI 千分位格式、fallback-name 诊断和名称来源解释补回归测试。
|
||||
|
||||
### v4 — 策略配置(v0 可用)
|
||||
|
||||
目标是开放系统级配置,但仍以安全默认值兜底。
|
||||
|
||||
1. 接入系统设置中的聚合策略配置。
|
||||
2. 支持 source priority、字段级规则、freshness 窗口和高级保护开关。
|
||||
3. 保存配置时校验未知字段、非法模式和危险动态字段锁定。
|
||||
4. 聚合接口返回当前命中的配置版本,方便排查。
|
||||
已落地的最小子集:
|
||||
|
||||
### v5 — 船舶资料 enrichment 与冲突治理
|
||||
1. 策略持久化在 `system_settings.category = 'vessel_aggregation_strategy'`,保存时自动版本递增。
|
||||
2. `app/services/vessel_aggregation_strategy.py` 暴露 `load_strategy / save_strategy / reset_strategy / validate_strategy`,并维护 `DEFAULT_STRATEGY` 兜底。
|
||||
3. 校验规则:
|
||||
- 未知 `field_rules.<name>` → `400 unknown vessel_ais field`;
|
||||
- 未知 mode → `400 mode must be one of ...`;
|
||||
- 动态字段(`lat/lon/sog/cog/heading/nav_status`)使用非 `newest` mode 时必须显式 `allow_dynamic_lock=true`,否则拒绝;
|
||||
- `freshness.realtime_stream_seconds` / `polling_seconds` 必须为非负整数;
|
||||
- `mode=locked` 必须带非空 `locked_source`。
|
||||
4. 聚合服务 `vessel_ais_aggregation.py` 在 `_select_position_observation` 中按 `freshness` 把过期实时流降级到 stale 候选;在 `_select_static_field` 中按 `field_rules.mode = source_priority / locked / newest / non_empty` 选源。
|
||||
5. 聚合输出每条 vessel 携带 `aggregation_strategy_version`,并在 `/geo/vessels` GeoJSON properties + `/vessels/{mmsi}` 详情中暴露。
|
||||
6. API:
|
||||
- `GET /api/v1/vessel-aggregation/strategy`
|
||||
- `PUT /api/v1/vessel-aggregation/strategy`(校验失败 400)
|
||||
- `DELETE /api/v1/vessel-aggregation/strategy`(恢复默认并 bump version)
|
||||
|
||||
未做项(留给 v4 后续):
|
||||
|
||||
- 系统设置 UI 中的策略编辑器尚未做,目前直接调 API;
|
||||
- `transport_priority`、`quality_flags` 级别的策略尚未引入;
|
||||
- `source_priority` 中的未知 source 不强校验,留给后续 warn-only 提示。
|
||||
|
||||
### v5 — 船舶资料 enrichment 与冲突治理(v0 可用)
|
||||
|
||||
目标是把 AIS 实时流里不稳定或低频出现的静态信息,补成可缓存、可审计的船舶资料层,同时把冲突解释变成可操作能力。
|
||||
|
||||
1. 做冲突治理 UI。
|
||||
2. 支持把人工选择沉淀成字段级规则。
|
||||
3. 支持恢复默认策略。
|
||||
4. 设计 `vessel_profile_enrichment`,按 `mmsi + imo + name + callsign` 异步补充船名、船型细分、AIS 大类、旗国、尺寸、建造年份、运营方等静态资料。
|
||||
5. 设计 `vessel_media_enrichment`,异步补充船只图片和外部详情缓存。
|
||||
6. enrichment 结果必须带 `source`、`fetched_at`、`expires_at`、`confidence` 和原始引用,不覆盖 AIS 原始观测。
|
||||
7. 聚合接口只读取已缓存 enrichment;请求链路不现场抓取第三方页面,避免慢请求和授权风险。
|
||||
8. 前端船只详情面板展示已缓存资料和媒体,并标注字段来源,不阻塞 AIS 实时链路。
|
||||
已落地的最小子集:
|
||||
|
||||
1. 新增模型 `app/models/vessel_enrichment.py::VesselProfileEnrichment` + `VesselMediaEnrichment`:以 `mmsi` 为主键,记录 `source / payload / fetched_at / expires_at / confidence / reference_url`;通过 `Base.metadata.create_all` 在 `init_db` 中建表。
|
||||
2. 服务 `app/services/vessel_enrichment.py` 提供 `upsert_vessel_profile_enrichment` / `upsert_vessel_media_enrichment` / `get_vessel_enrichment_bundle`;读路径只读缓存,过期记录(`expires_at < now`)直接过滤为 `None`,永不联网。
|
||||
3. 聚合接口在 `/api/v1/visualization/vessels/{mmsi}` 响应中追加 `enrichment.profile` 与 `enrichment.media` 字段(含 `source / fetched_at / expires_at / confidence / reference_url`);命中失败时返回 `null`,不阻塞 AIS 实时链路。
|
||||
4. 冲突治理 API:
|
||||
- `POST /api/v1/vessel-aggregation/conflicts/{mmsi}/{field}/promote-to-rule` 读取最近 `AISConflictRecord.selected_source`,写入 `field_rules[field] = {mode: source_priority, source_priority: [<source>]}` 并 bump version;
|
||||
- `DELETE` 对应路径移除该 field 的覆盖,恢复默认。
|
||||
5. 前端 Earth `info-card.js` 渲染 `船舶资料` 区块:profile.payload 标量字段平铺、媒体 `images` 数组缩略图、来源 / 更新时间 / 置信度元数据;缓存命中失败回退到 `资料缓存中`;常规字段在 `field_sources` 命中时附带来源 tag。
|
||||
|
||||
未做项(留给 v5 后续):
|
||||
|
||||
- 没有真正的异步 enrichment 抓取作业;当前依赖外部脚本/管理 API 写入缓存;
|
||||
- 冲突治理 UI 还没接入设置中心,目前只暴露 API;
|
||||
- enrichment 命中状态尚未广播到 `vessels` channel,详情面板首次打开时按需请求即可。
|
||||
|
||||
## 测试计划
|
||||
|
||||
@@ -377,6 +460,12 @@ VesselFinder 等服务里的船只图片不属于 AIS 实时数据本身。图
|
||||
- 明显异常位置不会进入默认展示轨迹,并会留下 `quality_flags`。
|
||||
- 同一时间窗口内多来源相近轨迹点只展示一个点。
|
||||
- AISStream 重连或回放导致的重复消息不会重复进入聚合结果。
|
||||
- raw observation 聚合结果和 legacy latest position 结果会按 MMSI 合并,BarentsWatch-only 船只不会因为 AISStream 子集存在而消失。
|
||||
- 不传 `limit` 或传 `limit=0` 时,`/geo/vessels` 全量返回合并后的船只集合。
|
||||
- AISStream 长连接收到新船、位置变化和航向变化后,会通过内部 `/ws` 的 `vessels` channel 推送增量。
|
||||
- AISStream streaming 状态不会显示成固定百分比完成进度条,也不会在收到一批消息后误报采集完成。
|
||||
- `mmsi`、`imo`、`callsign` 等身份编号在前端不显示千分位符。
|
||||
- 聚合结果中仍以 MMSI fallback 作为船名的记录可以被诊断查询完整列出,并带来源和缺失原因。
|
||||
- 字段级配置可以覆盖默认来源优先级。
|
||||
- 聚合接口在没有冲突表时仍可返回兼容 GeoJSON。
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
- [Planet 使用手册](/home/ray/dev/linkong/planet/docs/technical/zh/manual.md):控制台、`planet.sh`、Earth 和 Docs 的完整使用手册
|
||||
- [数据源、采集器设置与连接验证](/home/ray/dev/linkong/planet/docs/technical/zh/datasource-collector-settings-connectivity.md):数据源目录、采集器设置、连接验证、BarentsWatch 凭证链路
|
||||
- [Earth 可交互图标接入](/home/ray/dev/linkong/planet/docs/technical/zh/earth-interactable-usage.md):Earth 地表可交互图标 `Interactable` 的接口、生命周期和接入示例
|
||||
- [Earth 工具栏与浮层协同](/home/ray/dev/linkong/planet/docs/technical/zh/earth-toolbar-overlay-coordination.md):工具栏按钮与搜索 / 设置 / 新闻 / 图层浮层之间的关闭矩阵和接入规则
|
||||
|
||||
不适合放入这里的内容:
|
||||
|
||||
|
||||
94
docs/technical/zh/earth-toolbar-overlay-coordination.md
Normal file
94
docs/technical/zh/earth-toolbar-overlay-coordination.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# Earth 工具栏与浮层协同
|
||||
|
||||
本文件描述 Earth 大屏右侧工具栏按钮,以及搜索面板、设置弹窗、新闻直播面板、图层面板这几个浮层之间当前的协同规则。改交互、加按钮、调整面板时按这个表对齐,避免出现「点 A 把不该关的 B 也关了」之类的协同冲突。
|
||||
|
||||
相关入口:
|
||||
|
||||
- [Earth 前端结构](/home/ray/dev/linkong/planet/docs/technical/zh/earth-frontend-context.md)
|
||||
- [前端布局指南](/home/ray/dev/linkong/planet/docs/technical/zh/frontend-layout-guidelines.md)
|
||||
|
||||
## 工具栏按钮目录
|
||||
|
||||
工具栏在 [index.html](/home/ray/dev/linkong/planet/frontend/public/earth/index.html) 中以 `.earth-toolbar-btn` 标识,按钮列表:
|
||||
|
||||
| ID | 标题 | 类型 | 触发的浮层/动作 |
|
||||
|----|------|------|------------------|
|
||||
| `layer-action` | 图层 | 浮层切换 | HUD 面板 `layer-toggles`(桌面)/ 移动端抽屉 `layers` 卡 |
|
||||
| `search-action` | 搜索 | 浮层切换 | 搜索面板(桌面)/ 移动端抽屉 `search` 卡 |
|
||||
| `rotate-toggle` | 自动旋转 | 独立开关 | 不打开任何浮层 |
|
||||
| `toggle-tv` | 新闻直播 | 浮层切换 | 媒体面板 `media-panel`(含 TV/News 两个 tab) |
|
||||
| `reload-data` | 重新加载数据 | 独立动作 | 不打开任何浮层 |
|
||||
| `zoom-trigger` | 缩放控制 | 浮动菜单 | 缩放 floating menu |
|
||||
| `settings-trigger` | 设置 | 浮层切换 | 设置弹窗(桌面)/ 移动端抽屉 `settings` 卡 |
|
||||
| `reset-view` | 重置视角 | 独立动作 | 不打开任何浮层 |
|
||||
| `layout-toggle` | 最大化布局 | 独立开关 | 不打开任何浮层 |
|
||||
|
||||
## 浮层协同的统一入口
|
||||
|
||||
[controls.js::closeTransientMobileOverlays](/home/ray/dev/linkong/planet/frontend/public/earth/js/controls.js) 是「打开 X 时该关谁」的统一协调函数。
|
||||
|
||||
调用约定:每个会进入 fullscreen-style 浮层的开启路径调用 `closeTransientMobileOverlays({ except })`,告诉协调函数「除了我这一类,其他互斥浮层一律关掉」。
|
||||
|
||||
```js
|
||||
closeTransientMobileOverlays({ except: "search" }); // 搜索打开
|
||||
closeTransientMobileOverlays({ except: "settings" }); // 设置打开
|
||||
closeTransientMobileOverlays({ except: "media" }); // 新闻直播打开
|
||||
closeTransientMobileOverlays({ except: "layer-toggles" }); // 图层抽屉(移动端)
|
||||
```
|
||||
|
||||
`except` 当前可取的值:`"search"`、`"settings"`、`"media"`、`"layer-toggles"`,或省略表示「全部关闭」。
|
||||
|
||||
## 关闭矩阵
|
||||
|
||||
下表描述「打开 X」时其它浮层的命运。`✓` = 关闭,`—` = 保留。
|
||||
|
||||
| 触发动作 → | 关搜索 | 关设置 | 关图层抽屉(移动端) | 关新闻/直播 |
|
||||
|-----------|:------:|:------:|:--------------------:|:-----------:|
|
||||
| 打开搜索 (`except: "search"`) | (自身)| ✓ | ✓ | — |
|
||||
| 打开设置 (`except: "settings"`) | ✓ | (自身)| ✓ | — |
|
||||
| 打开新闻/直播 (`except: "media"`) | ✓ | ✓ | ✓ | (自身)|
|
||||
| 打开图层抽屉 (`except: "layer-toggles"`) | ✓ | ✓ | (自身)| ✓ |
|
||||
| 全部关闭 (`except: null`) | ✓ | ✓ | ✓ | ✓ |
|
||||
|
||||
读法举例:
|
||||
|
||||
- 点工具栏「设置」,搜索面板和图层抽屉会被关掉,新闻/直播面板保持原状。
|
||||
- 点工具栏「图层」(移动端打开 `layers` 抽屉),搜索 / 设置 / 新闻 全关。
|
||||
- 点工具栏「新闻直播」,搜索 / 设置 / 图层抽屉全关,新闻面板自身切换为打开。
|
||||
|
||||
## 设计原则
|
||||
|
||||
下面是当前矩阵背后的几条不变量。新增浮层或调整规则时按它们对齐:
|
||||
|
||||
1. **`zoom-trigger` 等浮动菜单不属于浮层。** 它们走 `bindFloatingMenu`,由 `closeFloatingMenus()` 单独管理;任何浮层打开都会先调一次 `closeFloatingMenus()`。
|
||||
2. **桌面 `layer-toggles` 是常驻 HUD 面板,不是浮层。** `closeTransientMobileOverlays` 中只有 `activeMobileDrawerId === "layer-toggles"`(移动端抽屉态)才会被关掉。所以桌面打开搜索/设置/新闻不会动图层面板,符合「桌面屏幕大、可共存」的预期。
|
||||
3. **新闻/直播面板独立于设置。** 用户切到设置改采集器时,常常想边看新闻边改配置,所以打开设置时不关新闻面板。这条是 2026-05 的协同补丁后建立的不变量;改设置打开路径时不要再去主动关 `media-panel`。
|
||||
4. **搜索和新闻面板视为「主信息浮层」,互相独立。** 搜索打开不关新闻、新闻打开不关搜索:两者面向不同任务(搜索定位 / 浏览态势新闻),允许同屏共存。如果未来 UX 上希望它们互斥,要在 `closeTransientMobileOverlays` 中**同时**改两边的规则,避免单边修改导致非对称的关闭逻辑。
|
||||
5. **移动端抽屉是 fullscreen 级别的状态。** 一旦进入移动端抽屉,无论是 `layers` / `search` / `settings` 哪一类,都会通过 `setMobileDrawerState` 关闭其它浮层。这是 mobile 单一焦点 UX 的要求。
|
||||
6. **`Escape` 键有固定的关闭顺序。** 见 [controls.js::setupKeyboardControls](/home/ray/dev/linkong/planet/frontend/public/earth/js/controls.js):搜索 → 设置 → 移动端抽屉 → 浮动菜单 → 工具栏 hub → 锁定对象。新增浮层要决定它在这个顺序中的位置。
|
||||
|
||||
## 新加按钮 / 浮层时怎么接
|
||||
|
||||
按下面的清单走,规则就不会乱:
|
||||
|
||||
1. 按钮加在 [index.html](/home/ray/dev/linkong/planet/frontend/public/earth/index.html) 的 `.earth-toolbar` 容器里,class 跟齐 `floating-btn liquid-glass-surface earth-toolbar-btn`。
|
||||
2. 决定它属于哪一类:
|
||||
- **独立动作**(reload / reset / rotate / layout):直接 `bindListener`,不调任何 `closeTransientMobileOverlays`。
|
||||
- **浮动菜单**(zoom 这种 dropdown):用 `bindFloatingMenu`,不进协同矩阵。
|
||||
- **互斥浮层**:进矩阵。
|
||||
3. 互斥浮层要做两件事:
|
||||
- 在打开路径调用 `closeTransientMobileOverlays({ except: "<your-key>" })`,让其他浮层主动让位。
|
||||
- 在 `closeTransientMobileOverlays` 函数体内补一条 `if (except !== "<your-key>" && isYourPanelVisible()) closeYourPanel();` 让别的浮层打开时关掉自己。
|
||||
4. 如果新浮层和某个现有浮层(例如新闻面板)应当共存,参考第 3 条规则:在自己的关闭判断里 `&& except !== "<peer-key>"` 把对方排除掉。**不要**只单边改一处,否则关闭逻辑会非对称。
|
||||
5. 新浮层应该有 `Escape` 关闭路径,加在 `setupKeyboardControls` 中合适的位置。
|
||||
6. 移动端如果应进入抽屉态,使用 `setMobileDrawerState({ open: true, card: "<your-card>" })` 而不是直接 toggle 面板。
|
||||
|
||||
## 当前实现位置
|
||||
|
||||
- 协调入口:[controls.js::closeTransientMobileOverlays](/home/ray/dev/linkong/planet/frontend/public/earth/js/controls.js)
|
||||
- 设置浮层:[controls.js::openSettingsModal / closeSettingsModal](/home/ray/dev/linkong/planet/frontend/public/earth/js/controls.js)
|
||||
- 搜索浮层:[controls.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/controls.js)(导入自 search 模块)
|
||||
- 新闻/直播浮层:[tv.js::setTVPanelVisible](/home/ray/dev/linkong/planet/frontend/public/earth/js/tv.js)、新闻 tab 在 [news.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/news.js)
|
||||
- 图层抽屉(移动端):[controls.js::setMobileDrawerState](/home/ray/dev/linkong/planet/frontend/public/earth/js/controls.js)
|
||||
- 浮动菜单:[controls.js::bindFloatingMenu](/home/ray/dev/linkong/planet/frontend/public/earth/js/controls.js)
|
||||
- 工具栏 DOM:[index.html](/home/ray/dev/linkong/planet/frontend/public/earth/index.html)
|
||||
@@ -16,12 +16,13 @@
|
||||
## Current Version
|
||||
|
||||
- `main` 当前主线历史推导到:`0.16.5`
|
||||
- `dev` 当前开发分支历史推导到:`0.47.0`
|
||||
- `dev` 当前开发分支历史推导到:`0.48.0`
|
||||
|
||||
## Timeline
|
||||
|
||||
| Version | Type | Branch | Commit | Summary |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `0.48.0` | feature | `dev` | `pending` | 新增自定义源 REST/WebSocket 实时 mock 链路,完善 AIS 多源聚合/船舶 enrichment,并将 Earth 全球态势统计改为轻量 SQL 聚合 |
|
||||
| `0.47.0` | feature | `dev` | `pending` | 新增 AISStream WebSocket 船只采集器、多源 AIS 原始观测聚合、采集器状态配置、船型显示修正和文档规则解耦 |
|
||||
| `0.46.3` | bugfix | `dev` | `pending` | 优化 Starlink footprint 拖拽性能,避免旋转地球时重复重建覆盖网格,并恢复线缆点击呼吸动画 |
|
||||
| `0.46.2` | bugfix | `dev` | `pending` | 修复 Earth 启动加载顺序、图层 localStorage 恢复、国界线底图语义、媒体面板、船只轨迹和 Iridium footprint 显示问题,并补充 AIS 聚合计划 |
|
||||
|
||||
Reference in New Issue
Block a user