release: bump version to 0.49.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
linkong
2026-05-08 17:42:27 +08:00
parent bb9183b8a4
commit e1984c7a35
86 changed files with 9165 additions and 412 deletions

View File

@@ -23,7 +23,10 @@
- [快速开始](/home/ray/dev/linkong/planet/docs/technical/zh/quickstart.md):从零启动 Planet 的最短路径
- [Planet 使用手册](/home/ray/dev/linkong/planet/docs/technical/zh/manual.md):控制台、`planet.sh`、Earth 和 Docs 的完整使用手册
- [Earth 位置候选采集使用手册](/home/ray/dev/linkong/planet/docs/technical/zh/location-pipeline-user.md):在 Earth 上为算力中心和 BGP 观测站采集、预览坐标候选
- [数据源、采集器设置与连接验证](/home/ray/dev/linkong/planet/docs/technical/zh/datasource-collector-settings-connectivity.md)数据源目录、采集器设置、连接验证、BarentsWatch 凭证链路
- [通用位置估算管线开发说明](/home/ray/dev/linkong/planet/docs/technical/zh/location-pipeline-development.md):后端 location resolver / pipeline 的接口、注册表和扩展方式
- [Docs Gatekeeper 开发说明](/home/ray/dev/linkong/planet/docs/technical/zh/docs-gatekeeper-development.md):后端 Docs 目录、正文读取和 Gatekeeper 权限组实现
- [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):工具栏按钮与搜索 / 设置 / 新闻 / 图层浮层之间的关闭矩阵和接入规则

View File

@@ -88,6 +88,10 @@ async def run(self, db):
| BarentsWatch AIS | vessel | 船只位置、航速、航向、MMSI 等 AIS 数据 | 依采集器配置 |
| AISStream Vessels | vessel_ais | AIS WebSocket 实时流,写入原始观测层并由聚合接口展示 | 依采集器配置 |
AIS 船只类采集器和其它 `CollectedData` 采集器的落库路径不同。BarentsWatch、AISStream 和自定义 `vessel_ais` 源都会进入 AIS 原始观测层,随后由聚合服务合并成 Earth 船只图层使用的 GeoJSON 和详情数据。这样做可以保留来源、传输方式、字段冲突和观测时间,避免某个实时源直接覆盖最终展示表。
TOP500 和 Epoch AI 算力数据的公开源不总是提供可用经纬度。Earth 统一算力中心接口在主地图启动链路中只使用源数据自带坐标或 `compute_center_locations` 维表坐标;缺少坐标的记录会进入 `unresolved`,不会通过本地注册表、国家质心或猜测城市自动渲染。用户手动采集候选时,后端会用源字段调用 ROR 组织注册 API 和 Nominatim/OpenStreetMap 在线搜索;候选经前端保存后写入 `compute_center_locations`,后续地图刷新再从维表渲染。
## 四、数据格式 (统一存储到 CollectedData 表)
```python
@@ -242,8 +246,16 @@ backend/app/services/collectors/
├── vessel_ais.py # BarentsWatch AIS 船只采集器
└── aisstream.py # AISStream WebSocket 船只采集器
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 # 统一数据模型
── collected_data.py # 统一数据模型
└── vessel_enrichment.py # 船舶富化结果缓存
```
## 九、凭证型采集器
@@ -253,7 +265,7 @@ backend/app/models/
| 采集器 | credential provider | 凭证来源 |
| --- | --- | --- |
| `barentswatch_vessels` | `barentswatch` | 控制台采集器设置、环境变量、`~/.zshrc` |
| `aisstream_vessels` | `aisstream` | 控制台采集器设置、环境变量 |
| `aisstream_vessels` | `aisstream` | 控制台采集器设置、环境变量`~/.zshrc`(连接验证可读;正式采集建议保存到采集器设置或注入后端环境) |
| `spacetrack_tle` | `spacetrack` | 环境变量、`~/.zshrc` |
### BarentsWatch AIS
@@ -292,6 +304,50 @@ export BARRENTSWATCH_CLIENT_SECRET="..."
连接验证会先请求 `https://id.barentswatch.no/connect/token` 获取 `scope=ais` 的 access token再用 `Authorization: Bearer <token>` 请求 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`,需要确认后端进程实际继承到了该环境变量。
### AIS 原始观测与聚合
AIS 观测写入后不会直接替换最终船只记录,而是先保存为 raw observation
- `source` 记录来源,例如 `barentswatch_vessels``aisstream_vessels` 或自定义源名称。
- `delivery_mode` 表达实时性,`realtime_stream` 优先于 `polling`
- `transport` 记录 `websocket``http`
- 位置、速度、航向等动态字段会按 freshness 和来源优先级选择。
- 静态字段优先保留非空值;冲突候选会记录到详情接口,便于排查多源差异。
Earth 使用的接口仍是:
```http
GET /api/v1/visualization/geo/vessels
GET /api/v1/visualization/vessels/{mmsi}
GET /api/v1/visualization/vessels/{mmsi}/track
GET /api/v1/visualization/vessels/{mmsi}/conflicts
GET /api/v1/visualization/vessels/aggregation/diagnostics
```
`/geo/vessels` 会合并 raw observation 聚合结果和 legacy BarentsWatch latest position 结果,避免只接入 AISStream 后把历史 BarentsWatch 船只遮蔽掉。
## 十、采集器设置与连接验证
控制台的“采集器设置”页提供所有内置采集器的 endpoint、请求头、超时、重试和凭证配置。连接验证不是只看前端按钮状态而是由后端计算 checksum

View File

@@ -62,7 +62,7 @@
当前行为:
- `collector_credentials` tab 展示为“采集器设置”。
- 下拉框列出所有内置采集器。
- 下拉框列出内置采集器,并支持维护合并到内置数据的自定义补充源
- 下拉框右侧只有一个插头图标按钮,用于健康检查。
- 下拉框下方用状态标签展示:
- `需要凭证` / `无需凭证`
@@ -72,6 +72,8 @@
- 是否覆盖 endpoint
- 需要凭证的采集器把凭证卡片放在基础配置上方。
- 不需要凭证的采集器只显示基础配置。
- AISStream 采集器使用 WebSocket 语义,状态会显示为连接中、实时接收、重连或停止,不使用固定百分比表达完成度。
- 自定义源入口放在采集器设置内,不在数据源目录里重复提供编辑入口;数据源目录只保留总览、运行和只读抽屉。
连接按钮使用内联 Tabler 风格插头图标,来源语义对应 `plug-connected`,避免继续使用刷新图标表达连接动作。
@@ -286,6 +288,100 @@ AISStream 使用 WebSocket 实时流,采集器只写入 `ais_raw_observations`
- 船型通常来自低频 `ShipStaticData.Type`;后端会把 AIS 数字类型码映射为 Cargo / Tanker / Passenger / Fishing / Military。
- 如果某艘船尚未收到静态消息,聚合结果的船型仍可能是 `Other`,后续由 v5 船舶资料 enrichment 补齐。
连接验证会读取保存配置、环境变量和 `~/.zshrc` 中的 `AISSTREAM_API_KEY`。正式采集时,推荐把 API Key 保存到采集器设置;如果只写在 `~/.zshrc`,需要确认后端进程实际继承了该变量,否则连接验证可能可用但 collector 运行时拿不到 key。
## 自定义 REST / WebSocket 映射运行时
文件:
- [custom_datasource_runtime.py](/home/ray/dev/linkong/planet/backend/app/services/custom_datasource_runtime.py)
- [datasource_mapping.py](/home/ray/dev/linkong/planet/backend/app/services/datasource_mapping.py)
自定义源现在不是独立的新数据孤岛,而是作为内置数据源的补充源写入目标 schema。当前最完整的目标是 `vessel_ais`:自定义 REST 或 WebSocket 源经过确定性 mapping 后写入 AIS raw observations再通过 `vessels` WebSocket channel 推送给 Earth。
### 配置语义
关键字段:
- `source_type``rest` / `http` / `websocket` / `ws`
- `endpoint`REST 使用 `http(s)://`WebSocket 使用 `ws(s)://`
- `auth_type``none``bearer``api_key``basic`
- `headers`:静态请求头。
- `auth_config`token、API key、basic 用户名密码API key 支持 header 或 query。
- `config.target_schema`:例如 `vessel_ais`
- `config.delivery_mode`REST 默认 `polling`WebSocket 默认 `realtime_stream`
- `config.merge_target_source`:记录该自定义源补充哪个内置数据,例如 `barentswatch_vessels`
REST runner 支持:
- `GET` / `POST`
- query params
- JSON body
- headers 和 auth 注入
- active mapping 写入目标 schema
WebSocket runner 支持:
- endpoint 格式校验
- headers 和 auth 注入
- 可选 `ws_subscribe_message`
- `ws_message_path` / `ws_items_path` 提取消息主体或数组
- 断线重连
- `debug_max_messages` 调试上限
- 后台 stream start / stop / status
相关 API
```http
POST /api/v1/datasources/custom/sample
GET /api/v1/datasources/target-schemas
POST /api/v1/datasources/{config_id}/run-mapped
POST /api/v1/datasources/{config_id}/stop-mapped
GET /api/v1/datasources/{config_id}/mapped-status
DELETE /api/v1/datasources/configs/{config_id}?delete_mappings=true&delete_source_data=true
```
`run-mapped?background=true` 只对 WebSocket 源有意义,会启动后台 stream。REST 源仍是一次性采集。
### 删除与数据清理
删除自定义源时有三种层级:
- 只删除配置:保留 mapping 和历史数据。
- 删除配置和 mapping同时删除该配置的 mapping 模板。
- 删除配置、mapping 和该源数据:删除该源写入的 `collected_data``ais_raw_observations``ais_source_health`
如果删除的是 `vessel_ais` 自定义源数据,后端会向 `vessels` channel 广播 `reload_required`,提示 Earth 重新拉取船只聚合结果。legacy `vessel_position` 不按自定义源直接删除,因为它没有可靠的 source 归因。
### 本地 AIS mock WebSocket
文件:
- [mock-ais-ws-server.ts](/home/ray/dev/linkong/planet/scripts/mock-ais-ws-server.ts)
运行方式:
```bash
bun run mock:ais-ws
```
mock 服务持续发送 AIS-like JSON用于验证“WebSocket 自定义源 -> mapping -> AIS raw observation -> `vessels` channel -> Earth 船只 upsert”链路。典型配置
```json
{
"source_type": "websocket",
"endpoint": "ws://localhost:8787",
"config": {
"target_schema": "vessel_ais",
"delivery_mode": "realtime_stream",
"merge_target_source": "barentswatch_vessels",
"ws_message_path": "$.data",
"ws_items_path": "$.vessels[*]",
"ws_reconnect": true
}
}
```
## 凭证教程
文件:
@@ -347,6 +443,7 @@ BarentsWatch `client_secret` 保存时有特殊处理:
当前已经支持的凭证 provider
- `barentswatch`
- `aisstream`
- `spacetrack`
其他 `requires_credentials=true` 的采集器如果还没有 provider会返回“凭证链路尚未接入”前端显示 `不可用`

View File

@@ -0,0 +1,116 @@
# Docs Gatekeeper 开发说明
Docs Gatekeeper 把 `/docs` 从“前端构建时打包所有 Markdown”改成“后端按权限返回目录和正文”。它的目标是让公开使用手册、用户文档、开发文档和管理/运维文档在同一个 Docs 页面内可检索,但正文读取必须经过服务端白名单和用户权限检查。
用户侧说明见 [Planet 使用手册](/home/ray/dev/linkong/planet/docs/technical/zh/manual.md) 的 Docs 章节。
## 鉴权模型
Docs 使用两层权限:
- `users.role`:保留给控制台系统权限。
- `users.gatekeeper_groups`Docs 内容权限组。
权限组:
| 组 | 用途 |
| --- | --- |
| `docs_user` | 用户操作类文档 |
| `docs_developer` | Earth、前端、后端、采集器和 AI Provider 开发文档 |
| `docs_admin` | 服务控制、运维、环境变量和敏感操作文档 |
继承规则:
- 未登录用户只能读 `public`
- `docs_developer` 隐含 `docs_user`
- `docs_admin` 隐含 `docs_developer``docs_user`
- `admin``super_admin` 默认拥有全部 Docs 权限。
## 后端入口
文件:
- [docs.py](/home/ray/dev/linkong/planet/backend/app/api/v1/docs.py)
- [docs_gatekeeper.py](/home/ray/dev/linkong/planet/backend/app/services/docs_gatekeeper.py)
- [user.py](/home/ray/dev/linkong/planet/backend/app/models/user.py)
- [users.py](/home/ray/dev/linkong/planet/backend/app/api/v1/users.py)
API
```http
GET /api/v1/docs/catalog
GET /api/v1/docs/{lang}/{slug}
```
`catalog` 只返回当前用户可见文档。正文接口会先校验语言、slug 和文件是否在 metadata 白名单里,再判断权限:
- 未登录访问受保护文档:`401`
- 已登录但权限不足:`403`
- 未知语言、未知 slug 或文件不存在:`404`
正文文件只能来自 `docs/technical/{zh,en}/` 下的白名单文件,不能通过路径拼接读取任意文件。
## Metadata 来源
当前服务端 metadata 维护在 [docs_gatekeeper.py](/home/ray/dev/linkong/planet/backend/app/services/docs_gatekeeper.py)
```python
DocsMetadata(
"manual.md",
"manual",
"public",
"Manual",
2,
"Planet 使用手册",
"Planet Manual",
)
```
新增公开文档时,需要同步:
- 新增中英文 Markdown 文件。
- 在服务端 `DOCS_METADATA` 添加 filename、slug、access、group、order、标题。
- 在前端 [docs-content.ts](/home/ray/dev/linkong/planet/frontend/src/pages/Docs/docs-content.ts) 添加同名 metadata保持导航标题和排序一致。
- 如果需要从 README 发现,更新 `docs/technical/zh/README.md``docs/technical/en/README.md`
## 用户管理
`users` 表新增 `gatekeeper_groups JSONB DEFAULT '[]'`。启动时 [session.py](/home/ray/dev/linkong/planet/backend/app/db/session.py) 会用 `ALTER TABLE ... ADD COLUMN IF NOT EXISTS` 补列,适配已有本地数据库。
用户 API 负责:
- 创建用户时写入 `gatekeeper_groups`
- 更新用户时校验组名只能是 `docs_user``docs_developer``docs_admin`
- 只有 `super_admin` 能修改 Gatekeeper 权限组。
前端 [Users.tsx](/home/ray/dev/linkong/planet/frontend/src/pages/Users/Users.tsx) 展示权限组标签,并在编辑表单中提供多选框。非 `super_admin` 打开的表单会禁用该字段,并在提交前移除 `gatekeeper_groups`
## 前端 Docs 加载
文件:
- [Docs.tsx](/home/ray/dev/linkong/planet/frontend/src/pages/Docs/Docs.tsx)
- [docs-content.ts](/home/ray/dev/linkong/planet/frontend/src/pages/Docs/docs-content.ts)
- [docs-search.ts](/home/ray/dev/linkong/planet/frontend/src/pages/Docs/docs-search.ts)
关键变化:
- 移除 `import.meta.glob(...?raw)` 作为正文来源。
- 页面加载时请求 `/api/v1/docs/catalog` 构建当前可见目录。
- 打开正文时请求 `/api/v1/docs/{lang}/{slug}`
- 搜索只索引当前用户可见文档,并按需从后端读取 Markdown。
- `401` 显示登录提示,`403` 显示权限提示,`404` 显示文档不可用。
## 测试覆盖
相关测试:
- [test_docs_gatekeeper.py](/home/ray/dev/linkong/planet/backend/tests/test_docs_gatekeeper.py)
测试应覆盖:
- 匿名用户只能看到 public 文档。
- 受保护正文的 `401` / `403`
- `docs_developer` 可读开发文档但不能读管理文档。
- `admin``super_admin` 可读管理文档。
- 未知 slug、未知语言和路径穿越字符串不能读取文件。

View File

@@ -303,7 +303,9 @@ AISStream 的 `PositionReport` 常带实时位置和 `MetaData.ShipName`,但
登陆点是当前明确保留的例外:它曾接入 `Interactable`,但 pin 类 SVG 在地球边缘会被 `THREE.Points` 的深度测试裁切成碎片;关闭 depthTest 又会破坏背面遮挡语义。因此登陆点退回 `cables.js` 内的专用 `THREE.Sprite` 路径,并改为 canvas 生成的黄色扁平球纹理。它的 `altitudeOffset``renderOrder` 与海缆线一致避免漂在海缆之上Sprite 本体关闭 `depthTest` 保持球完整,背面可见性由 `isFacingCamera()` 的球体遮挡判断控制。
图标资源可以继续用 canvas draw也可以放到 `frontend/public/earth/assets/icons/` 后由 `Interactable` 预加载。asset 路径不会在每帧读取;图层加载阶段通过 `preloadAssets()` 只加载一次 SVG / 图片,之后按 `icon source + state + bucket + color` 生成 `CanvasTexture` 并复用。当前算力中心已经从 `assets/icons/compute-supercomputer.svg``assets/icons/compute-gpu-cluster.svg` 和备用 `assets/icons/compute-hdd-network.svg` 读取图标,再在 canvas 上叠加估算位置的 `?` badge。
图标资源可以继续用 canvas draw也可以放到 `frontend/public/earth/assets/icons/` 后由 `Interactable` 预加载。asset 路径不会在每帧读取;图层加载阶段通过 `preloadAssets()` 只加载一次 SVG / 图片,之后按 `icon source + state + bucket + color` 生成 `CanvasTexture` 并复用。当前算力中心已经从 `assets/icons/compute-supercomputer.svg``assets/icons/compute-gpu-cluster.svg` 和备用 `assets/icons/compute-hdd-network.svg` 读取图标,再在 canvas 上叠加未确认位置的 `?` badge。算力中心后端在启动链路只渲染源数据自带坐标或 `compute_center_locations` 维表坐标;手动候选采集会调用 ROR 和 Nominatim/OpenStreetMap并在 GeoJSON 或候选响应中返回位置精度、置信度、来源说明和核验时间;前端详情卡展示这些字段。
算力中心图层行左上角的通知气泡显示 GeoJSON `unresolved` 数量。这个数字表示“完全没有可信坐标、不能渲染到地球上”的记录,不等同于地图上带 `?` 的已定位待确认点。点击气泡会在图层面板右侧打开固定信息卡,信息卡内容区内部滚动,不随鼠标 hover 消失。列表中的单条 `采集` 只展示候选;顶部 `一键采用` 会按当前列表顺序逐条采集、保存最高置信候选,成功一条就移除一条、重新编号,并通过 `earth:compute-center-unresolved-count-change` 同步气泡数量。批量结束后再触发 `earth:compute-center-location-saved` 刷新真实图层。
asset 图标大小由 `Interactable``icon.fitSize` 控制。SVG / 图片文件应尽量保持原始 viewBox 和路径,不要为了在地球上显示成 60x60 而手写 `transform``drawAssetIcon()` 会把资源等比 contain 到指定尺寸并居中绘制到 atlas canvas。
@@ -352,6 +354,10 @@ const scale = THREE.MathUtils.clamp(
- 按钮:`#toggle-terrain`
- 状态节点:`#terrain-status`
地形不是默认可见图层时,启动期不会立即阻塞加载地形瓦片。`controls.js` 会在图层可见性恢复完成后才调度 `scheduleTerrainPrefetch()`,并且只在高清材质可用、地形尚未 ready、预取未开始时执行。预取使用 `setTimeout` + `requestIdleCallback`,避免和首屏云图、高清材质、图层启动队列抢主线程。
地形瓦片请求也不再逐个散发大量单 tile 请求。`terrain.js` 会把需要的 Terrarium tile 去重后按 `TERRAIN_CONFIG.batchRequestSize` 分批请求 `/api/v1/visualization/terrain/terrarium/batch`;后端用 LRU 内存缓存、批次去重和并发限制代理 S3 Terrarium tile。单 tile endpoint 仍保留给回退路径和浏览器缓存语义。
以后别的异步图层也可以沿用这套约定。
## 当前设置持久化

View File

@@ -155,6 +155,7 @@
- 渲染 `/docs` 的 Markdown 正文
- 支持标题、列表、引用、代码块、表格和基础行内格式
- 代码块和表格内部复用 `Scrollbar`,避免横向内容撑爆文档页
- Docs 正文由后端 `/api/v1/docs/...` 按 Gatekeeper 权限返回;前端只渲染当前用户可见内容
当前约束:
@@ -190,9 +191,10 @@
- token
- 当前用户
- Gatekeeper 权限组
- 登录/退出
`App.tsx` 用它判断是否进入登录页。
`App.tsx` 用它判断是否进入登录页。`/docs` 仍是公开路由,但目录和正文由后端按 token 决定;未登录时只返回公开文档。
### 2. 业务数据网关

View File

@@ -0,0 +1,200 @@
# 通用位置估算管线开发说明
`backend/app/services/location/` 是所有“给定一条记录,决定它的 lat/lon”业务的共享抽象。算力中心、BGP 观测站、BGP 事件目前都跑在这条管线上。未来需要位置估算的实体例如卫星地面站、用户认领点位、IXP 设施,也应接入这里,而不是各自再写地理解析逻辑。
用户侧流程见 [Earth 位置候选采集使用手册](/home/ray/dev/linkong/planet/docs/technical/zh/location-pipeline-user.md)。
## 设计目标
历史上算力中心有自己的 4 层链路BGP 观测站使用写死字典BGP 事件继承 collector。三套实现互不复用新算法也没有稳定挂入点。
重构后的原则:
- 共享 `LocationResolver` 协议和 `LocationPipeline` 编排器。
- 各领域只负责构造 `LocationQuery` 和选择 resolver 顺序。
- 新算法通过新增 resolver 类接入,不改 ingestion、API 和前端 envelope。
- 只有达到城市级或更高精度的位置能渲染到 Earth。
- 本地 JSON registry 不作为算力中心或 BGP 观测站的运行时候选来源;持久事实写入数据库维表。
## 核心接口
```python
@dataclass(frozen=True)
class LocationQuery:
name: str | None
aliases: tuple[str, ...]
city: str | None
country: str | None
region: str | None
source_latitude: float | None
source_longitude: float | None
extra: Mapping[str, Any]
```
```python
@dataclass(frozen=True)
class LocationCandidate:
latitude: float
longitude: float
display_name: str
precision: str
confidence: float
source: str
needs_confirmation: bool
matched_fields: tuple[str, ...]
suggested_registry_entry: dict | None
```
```python
class LocationResolver(Protocol):
name: str
def resolve(self, query: LocationQuery) -> ResolverOutput: ...
```
`LocationPipeline.collect_candidates()` 返回排序后的候选和 `attempted_queries``resolve_best()` 返回最佳候选及诊断信息。默认排序按 source rank、precision rank、confidence且对同 source 和同坐标候选去重。
## 内置 resolver
| Resolver | 文件 | 职责 |
| --- | --- | --- |
| `SourceCoordinatesResolver` | `resolvers/source_coordinates.py` | 源记录已有 lat/lon 时直接产出 `precision="precise"` |
| `RegistryResolver` | `resolvers/registry.py` | 遗留通用 resolver当前算力中心和 BGP 运行时链路不使用它生成候选 |
| `NominatimResolver` | `resolvers/nominatim.py` | 按领域 query plan 调 Nominatim带 LRU 缓存和速率限制 |
| `InheritFromAnotherEntityResolver` | `resolvers/inherit.py` | 把外部实体的已解析位置包装为候选 |
`RegistryResolver` 仍保留给后续可能的受控导入场景,但它不应被重新接入算力中心或 BGP 作为“硬编码 hint”候选源。过去仅凭 `operator``city` 等通用字段匹配 registry 容易把多个实体落到同一个点,这是这次下线 registry 候选链路的主要原因。
## 当前领域管线
### 算力中心
入口文件:
- [compute_center_locations.py](/home/ray/dev/linkong/planet/backend/app/services/compute_center_locations.py)
管线顺序:
```python
SourceCoordinatesResolver()
StoredComputeCenterLocationResolver()
```
主地图启动链路只做“源坐标优先,其次数据库维表坐标”。数据库表为 `compute_center_locations`,唯一键是 `(source, source_id)`,用于保存人工确认或从源记录真实坐标迁入的位置。`init_db()` 只幂等迁入源记录里已有的真实经纬度,不迁入旧硬编码 hint不在启动期批量调用 ROR、Nominatim 或 LLM。
手动候选采集链路和渲染链路分开。`collect_location_candidates()` 使用源字段构造 ROR 和 Nominatim/OpenStreetMap 查询,但不会把 `compute_center_locations` 当前坐标当候选返回。用户在前端确认某个候选后,通过保存接口写入维表;之后地图刷新时由 `StoredComputeCenterLocationResolver` 渲染。
`resolve_compute_center_location()``resolve_compute_center_location_full()``collect_location_candidates()` 保留为领域 API。`visualization.py` 只消费领域 API不再持有坐标提示常量、国家质心兜底或 Nominatim 细节。
GeoJSON 输出只包含 `RENDERABLE_PRECISIONS` 内的位置。未解析记录进入 `unresolved`,并带上 `failure_reason``attempted_queries``source_id``record_id` 等诊断字段。
### BGP 观测站
入口文件:
- [bgp_collector_locations.py](/home/ray/dev/linkong/planet/backend/app/services/bgp_collector_locations.py)
- [bgp_collector_location.py](/home/ray/dev/linkong/planet/backend/app/models/bgp_collector_location.py)
管线顺序:
```python
SourceCoordinatesResolver()
StoredCollectorLocationResolver()
NominatimResolver(_bgp_collector_query_plan)
```
23 个 RIPE RIS collector 坐标从旧表迁入 `bgp_collector_locations` 维表,默认 `source=legacy_seed``needs_confirmation=true`。旧字典仍由 DB-backed cache 维护,保证下游接口兼容;手动候选采集不会把这份维表坐标当作候选,只用它补齐 site/city/country 查询上下文。
### BGP 事件
入口文件:
- [bgp_event_locations.py](/home/ray/dev/linkong/planet/backend/app/services/bgp_event_locations.py)
管线顺序:
```python
SourceCoordinatesResolver()
InheritFromAnotherEntityResolver(_inherit_from_owning_collector)
```
事件继承使用所属 collector 的严格查找,不跑完整 collector registry 模糊匹配。后续 ASN 设施、PrefixGeo 或 PeeringDB resolver 可以挂在继承 resolver 之后。
## API envelope
```http
POST /api/v1/visualization/compute-centers/{source_id}/collect-location
POST /api/v1/visualization/compute-centers/{source_id}/location
POST /api/v1/bgp/collectors/{collector_id}/collect-location
```
`collect-location` 返回统一 envelope
```json
{
"success": true,
"candidates": [],
"best_candidate": {},
"attempted_queries": [],
"context": {}
}
```
`POST /api/v1/visualization/compute-centers/{source_id}/location` 把前端选中的候选 upsert 到 `compute_center_locations`。人工保存默认 `needs_confirmation=false``verification_status="verified"` 并写入 `verified_at`;如果后续接入自动暂存,也可以显式传 `needs_confirmation=true`
前端 [info-card.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/info-card.js) 使用通用候选列表和预览事件渲染对象详情卡。算力中心图层按钮左上角会显示 `unresolved` 数量;点击角标打开待定位列表。列表中的 `采集` 只拉候选,`一键采用` 会逐条调用候选采集接口,选择最高置信且有有效经纬度的候选保存。保存成功一条就从列表移除并重新编号,同时通过 `earth:compute-center-unresolved-count-change` 同步角标;批量结束后再触发 `earth:compute-center-location-saved` 刷新真实图层。
如果剩余记录没有任何 city-level 候选,批量采用不会伪造坐标。前端会保留这些记录并展示后端返回的 `failure_reason` 和已尝试查询。
## 新增 resolver
resolver 只需要实现 `name``resolve()`,返回 `ResolverOutput`
```python
class PeeringDBFacilityResolver:
name = "peeringdb_facility"
def __init__(self, client):
self._client = client
def resolve(self, query):
asn = query.extra.get("origin_asn")
if not asn:
return ResolverOutput()
return ResolverOutput(candidates=tuple(
LocationCandidate(
latitude=f.latitude,
longitude=f.longitude,
display_name=f.name,
precision="site",
confidence=0.78,
query=f"peeringdb::{asn}",
source=self.name,
source_note=f"PeeringDB facility for AS{asn}",
matched_fields=("origin_asn",),
needs_confirmation=False,
city=f.city,
country=f.country,
)
for f in self._client.facilities_for_asn(asn)
))
```
挂入:
```python
BGP_EVENT_PIPELINE = LocationPipeline([
SourceCoordinatesResolver(),
InheritFromAnotherEntityResolver(source_lookup=...),
PeeringDBFacilityResolver(client=peeringdb_client),
])
```
## 测试覆盖
相关测试:
- [test_location_pipeline.py](/home/ray/dev/linkong/planet/backend/tests/test_location_pipeline.py)
- [test_bgp_collector_locations.py](/home/ray/dev/linkong/planet/backend/tests/test_bgp_collector_locations.py)
- [test_visualization_compute_centers.py](/home/ray/dev/linkong/planet/backend/tests/test_visualization_compute_centers.py)
测试重点包括 resolver 可插拔性、注册表 alias 约束、BGP collector 兼容字典、算力中心公共 API 兼容、不可渲染位置进入 `unresolved`

View File

@@ -0,0 +1,127 @@
# Earth 位置候选采集使用手册
位置候选采集用于给 Earth 上的算力中心和 BGP 观测站补齐或核验经纬度。它不会要求用户手工输入坐标,而是把源数据、开放组织注册 API 和在线地理编码结果整理成候选列表,供用户预览和后续认领。
## 适用对象
当前支持:
- 算力中心TOP500 超算、Epoch AI GPU 集群。
- BGP 观测站RIPE RIS `rrcXX` collector。
BGP 事件的位置默认继承所属 collector。事件本身暂不提供单独按钮后续 ASN 设施、Prefix 地理位置或 PeeringDB 算法接入后会继续走同一条管线。
## 用户能看到什么
在 Earth 上点击算力中心或 BGP 观测站后,详情卡会展示位置相关字段:
| 字段 | 含义 |
| --- | --- |
| 位置精度 | `精确坐标``站点级位置``城市级位置``位置未确认` |
| 位置来源 | 源数据坐标、ROR 组织注册 API、Nominatim 在线搜索,或已存储的 BGP collector 维表位置 |
| 位置置信度 | 后端 resolver 给出的相对置信度百分比 |
| 核验状态 | 已确认、估算位置或在线检索结果待确认 |
| 解析依据 | 为什么选择这个位置,例如匹配了哪个站点或城市 |
| 匹配的位置名称 | 开放来源、在线结果或已存储 collector 位置中的规范名称 |
| 位置核验时间 | 已确认位置的核验日期,在线候选通常为空 |
算力中心 GeoJSON 不再渲染国家质心、未知位置或 `[0, 0]` 占位点。无法达到城市级精度的数据会进入接口的 `unresolved` 列表,并在图层开关左上角显示待定位数量。点击这个通知气泡会打开待定位列表。
地图上带 `?` 的算力中心不是 `unresolved`。它们已经有坐标,只是 `needs_confirmation=true` 或来自在线地理编码,仍需人工核验。真正 `unresolved` 的记录没有可信经纬度,因此不会出现在地球上。
## 自动采集候选
1. 打开 `http://localhost:3000/earth`
2. 打开 `算力中心``BGP 观测` 图层。
3. 点击目标对象打开详情卡。
4. 点击 `自动采集坐标候选``重新自动采集坐标`
5. 等待详情卡列出最多 5 个候选位置。
6. 点击候选行里的 `预览`Earth 会飞到该候选经纬度附近。
候选列表会显示:
- 候选名称。
- 精度:精确、站点或城市。
- 来源 resolver。
- 置信度。
- 经纬度。
点击候选行里的 `保存` 会把所选候选写入算力中心位置维表。保存成功后,算力中心图层会刷新;如果该记录原本在待定位列表中,待定位数量也会减少。
## 待定位列表和一键采用
算力中心图层按钮左上角的通知气泡显示当前 `unresolved` 数量。点击后会在图层面板右侧打开固定列表:
1. 列表只包含没有可信经纬度的算力中心。
2. 单条 `采集` 会调用候选接口,并展示最多 5 个候选供预览和保存。
3. 顶部 `一键采用` 会从上到下逐条采集候选,选择置信度最高且有有效经纬度的候选保存。
4. 成功保存一条后,该行会立即从列表中移除,下面的序号自动上移,通知气泡数量同步减少。
5. 批量结束后,前端会刷新算力中心图层,确保 UI 和后端真实状态一致。
如果某条记录没有任何可保存候选,系统不会用国家中心点、厂商总部或硬编码 hint 伪造位置。该记录会留在列表中,并显示后端返回的失败原因和已尝试查询,等待人工补充更可靠的地址或坐标证据。
## 后端接口
前端按钮调用的接口如下:
```http
POST /api/v1/visualization/compute-centers/{source_id}/collect-location
POST /api/v1/visualization/compute-centers/{source_id}/location
POST /api/v1/bgp/collectors/{collector_id}/collect-location
```
两个 `collect-location` 接口返回相同结构:
```json
{
"success": true,
"candidates": [],
"best_candidate": {},
"attempted_queries": [],
"context": {}
}
```
当没有候选达到城市级精度时,`success``false`,响应会包含 `failure_reason` 和已尝试的查询文本,便于判断是源数据字段不足、开放来源缺项,还是在线地理编码没有命中。
## 数据维护建议
算力中心和 BGP 观测站都不再维护本地候选注册表。算力中心的人工确认位置保存在 `compute_center_locations` 数据库维表中,唯一键是 `(source, source_id)`BGP 观测站的当前位置保存在 `bgp_collector_locations` 数据库维表中,旧 RIPE RIS 城市级坐标只作为初始化 seed 写入,默认仍需人工核验。
维护算力中心时优先补齐:
- `source` / `source_id`:例如 `top500` + `top500_50`
- `name` / `operator` / `site`
- `city` / `country`
- `latitude` / `longitude`
- `precision``precise``site``city`
- `confidence`0 到 1 的置信度。
- `location_source` / `source_url` / `source_note` / `raw_payload`:证据来源。
- `needs_confirmation` / `verification_status` / `verified_at`:人工核验状态和日期。
维护 BGP 观测站时优先补齐:
- `collector_id`:例如 `rrc12`
- `site` / `operator`:站点和运营方。
- `city` / `country` / `region`
- `latitude` / `longitude`
- `precision``precise``site``city`
- `confidence`0 到 1 的置信度。
- `source` / `source_url` / `raw_payload`:证据来源。
- `verification_status` / `verified_at`:人工核验状态和日期。
如果只是知道城市,不知道设施坐标,应使用城市级精度,不要填一个看似精确但无法核验的点位。
## 常见问题
### 为什么有些算力中心不显示在 Earth 上
Earth 只渲染达到城市级或更高精度的坐标。源数据没有坐标、已验证位置没有命中、在线搜索也没有城市级结果时,记录会进入 `unresolved`,避免在地图上出现误导性的国家中心点或 `[0, 0]`
### 为什么在线搜索结果显示“待确认”
Nominatim/OpenStreetMap 结果来自在线地理编码,可能匹配到同名城市、机构或园区。它可以用于快速定位和预览,但在写入已验证位置前应人工确认。
### 为什么 BGP 事件没有全部落到 Amsterdam
旧逻辑中,事件可能因为 `operator="RIPE NCC"` 这种通用字段误匹配到 `rrc00`。当前 BGP 事件继承只按所属 collector 在 DB-backed cache 中严格查找,不再用 registry 模糊匹配。

View File

@@ -5,7 +5,7 @@
- `planet.sh`:本地启动、停止、重启、健康检查和日志入口
- Earth公开 3D 地球态势页面
- 控制台:登录后的管理后台
- Docs公开开发文档与使用手册
- Docs后端 Gatekeeper 受控的文档站,基础使用文档公开开发/运维文档按权限组开放
快速启动路径见 [快速开始](/home/ray/dev/linkong/planet/docs/technical/zh/quickstart.md)。
@@ -16,7 +16,7 @@
| 名称 | 地址 | 是否需要登录 | 说明 |
| --- | --- | --- | --- |
| Earth | `http://localhost:3000/earth` | 否 | 3D 地球、图层、BGP、卫星、海缆、新闻态势 |
| Docs | `http://localhost:3000/docs` | 否 | 开发文档、技术说明、使用手册 |
| Docs | `http://localhost:3000/docs` | 部分需要 | 使用手册公开;开发、后端、运维文档按 Gatekeeper 权限组开放 |
| 控制台 | `http://localhost:3000/admin` | 是 | 数据、配置、告警、日志和专题观测 |
| AI Playground | `http://localhost:3000/playground` | 是 | AI Provider 状态和调试 |
| 后端 API 文档 | `http://localhost:8000/docs` | 视接口而定 | FastAPI / OpenAPI 文档 |
@@ -198,7 +198,26 @@ AI Provider 镜像只在代码、Dockerfile、Compose 配置或相关 Python 依
- 手机或平板演示 Earth
- 局域网其他机器访问同一个开发实例
启动后注意检查防火墙和 WSL 网络转发
`--allow-lan` 只负责让前端和后端监听 `0.0.0.0`。如果服务运行在 WSL 中Windows 本机通常可以通过 `localhost` 访问,但手机或其他电脑访问 `http://<Windows局域网IP>:3000` 还依赖 Windows 端口转发和防火墙放行
推荐按顺序判断:
```bash
# 在 WSL 或运行 Planet 的 shell 中
curl http://localhost:3000
curl http://localhost:8000/health
ss -ltnp | grep -E ':3000|:8000'
```
如果这里能看到 `0.0.0.0:3000``0.0.0.0:8000`,但局域网 IP 访问失败,请在管理员 PowerShell 中配置:
```powershell
netsh interface portproxy add v4tov4 listenaddress=0.0.0.0 listenport=3000 connectaddress=127.0.0.1 connectport=3000
netsh interface portproxy add v4tov4 listenaddress=0.0.0.0 listenport=8000 connectaddress=127.0.0.1 connectport=8000
New-NetFirewallRule -DisplayName "WSL Planet 3000" -Direction Inbound -Action Allow -Protocol TCP -LocalPort 3000
New-NetFirewallRule -DisplayName "WSL Planet 8000" -Direction Inbound -Action Allow -Protocol TCP -LocalPort 8000
```
## Earth
@@ -290,6 +309,12 @@ Earth 搜索支持查找当前地球对象,例如:
搜索结果可以用于快速定位对象,并打开对应详情。
### 位置候选采集
算力中心和 BGP 观测站详情卡支持自动采集坐标候选。点击对象后,使用详情卡中的 `自动采集坐标候选``重新自动采集坐标` 按钮,后端会从源坐标、开放组织注册 API 和在线地理编码中整理候选位置。BGP 观测站的已存储位置只用于补齐查询上下文,不会作为候选直接返回。
候选可以直接在 Earth 上预览。算力中心候选点击 `保存` 后会写入 `compute_center_locations` 维表,并立即刷新图层。算力中心图层左上角的通知气泡显示无法渲染的待定位数量;点击后可查看列表,单条采集候选,或用 `一键采用` 从上到下保存最高置信候选。没有可用候选的记录会留在列表中,不会被国家中心点或硬编码 hint 伪造位置。详细流程见 [Earth 位置候选采集使用手册](/home/ray/dev/linkong/planet/docs/technical/zh/location-pipeline-user.md)。
### 设置
设置面板包含:
@@ -536,33 +561,41 @@ BarentsWatch AIS 支持从以下位置读取凭证:
## Docs
公开文档站入口:
文档站入口:
```text
http://localhost:3000/docs
```
当前公开内容来自
Docs 正文由后端 API 按权限读取,不再把全部 Markdown 直接打进前端构建产物。当前文档源文件仍位于
```text
docs/technical/zh/*.md
docs/technical/en/*.md
```
未登录访客默认只能看到 `public` 文档,例如首页、快速开始和使用手册。登录用户如果被分配 Gatekeeper 权限组,可以看到更多技术文档:
- `docs_user`:用户操作类文档。
- `docs_developer`Earth、前端、后端、采集器和 AI Provider 等开发文档。
- `docs_admin`:服务控制、运维、环境变量和敏感操作文档。
`admin` 默认拥有管理文档权限,`super_admin` 拥有全部 Docs 权限。Gatekeeper 权限组在控制台“用户管理”中配置。
Docs 支持:
- 分类导航
- Markdown 渲染
- 表格和代码块
- 文档内目录
- 本地搜索
- 对当前可见文档搜索
- technical 文档之间的内部链接跳转
如果新增 technical 文档,应同步检查:
- 是否有清晰的一级标题
- 是否需要加入 `/docs` 的人工分类和排序
- 是否包含不适合公开展示的信息
- 是否需要加入后端 Docs metadata 的人工分类和排序
- 应归入 `public``docs_user``docs_developer` 还是 `docs_admin`
## 开发命令约定
@@ -633,6 +666,7 @@ source ~/.zshrc && bun run build
- [控制台前端结构](/home/ray/dev/linkong/planet/docs/technical/zh/frontend-admin-frontend-context.md)
- [Earth 前端结构](/home/ray/dev/linkong/planet/docs/technical/zh/earth-frontend-context.md)
- [Earth 图层样式属性索引](/home/ray/dev/linkong/planet/docs/technical/zh/earth-layer-style-reference.md)
- [Earth 位置候选采集使用手册](/home/ray/dev/linkong/planet/docs/technical/zh/location-pipeline-user.md)
- [系统服务控制](/home/ray/dev/linkong/planet/docs/technical/zh/backend-system-service-control.md)
- [数据采集系统](/home/ray/dev/linkong/planet/docs/technical/zh/backend-collectors.md)
- [数据源、采集器设置与连接验证](/home/ray/dev/linkong/planet/docs/technical/zh/datasource-collector-settings-connectivity.md)

View File

@@ -150,6 +150,13 @@ wait_for_port_release() {
}
```
当前启动前端时还有一层预清理重试:
- `PORT_PRESTART_RETRIES`:默认 3 次。
- `PORT_PRESTART_RETRY_INTERVAL`:默认 2 秒。
`kill_port_if_requested()` 只在当前环境能找到监听 PID 时主动杀进程;如果没有 PID 但端口暂时不可绑定,它会记录诊断并把最终确认交给服务启动流程。`start_frontend_with_retry()` 也只在发现监听 PID 时进入预清理重试,避免在宿主机或外部 network namespace 尚未释放端口时做无意义的“空杀重试”。这意味着第一次重启时看到“未发现监听进程但端口仍不可绑定”通常是外部环境仍在释放端口;脚本不会再把这种情况当成立刻失败的本地进程清理问题。
## 问题三:端口检测用 Python
### 原因

View File

@@ -30,6 +30,16 @@ AI Provider 的个人配置也可以放在 `~/.zshrc`。`planet.sh` 会读取简
./planet.sh restart -a
```
AISStream、BarentsWatch 等采集器凭证也可以先写在 `~/.zshrc` 里供连接验证读取,例如:
```bash
export AISSTREAM_API_KEY="..."
export BARENTSWATCH_CLIENT_ID="..."
export BARENTSWATCH_CLIENT_SECRET="..."
```
正式采集更推荐在控制台 `设置 -> 采集器设置` 保存凭证,尤其是 AISStream 这类长连接 WebSocket collector。这样连接验证、后端采集任务和 Earth 实时船只聚合会使用同一份配置。
## 1. 启动服务
在仓库根目录执行:
@@ -44,7 +54,7 @@ AI Provider 的个人配置也可以放在 `~/.zshrc`。`planet.sh` 会读取简
| --- | --- | --- |
| Earth | `http://localhost:3000/earth` | 公开 3D Earth 可视化页面 |
| 控制台 | `http://localhost:3000/admin` | 登录后的管理后台 |
| 文档站 | `http://localhost:3000/docs` | 公开开发文档和使用手册 |
| 文档站 | `http://localhost:3000/docs` | 使用手册公开开发/运维文档按 Gatekeeper 权限组开放 |
| AI Playground | `http://localhost:3000/playground` | 登录后的 AI 调试入口 |
| 后端 API 文档 | `http://localhost:8000/docs` | FastAPI / OpenAPI 接口文档 |
@@ -64,6 +74,8 @@ AI Provider 的个人配置也可以放在 `~/.zshrc`。`planet.sh` 会读取简
按提示输入用户名、密码和角色。
如果需要阅读开发或运维文档,用 `super_admin` 登录控制台后,在“用户管理”里给目标用户分配 Gatekeeper 权限组:`docs_developer` 用于开发文档,`docs_admin` 用于服务控制和运维文档。
## 3. 打开 Earth
访问:
@@ -79,6 +91,7 @@ Earth 是公开页面,不需要登录。
- 地球正常显示
- 右侧图层控制可打开/关闭图层
- 搜索可以查找海缆、卫星、算力中心、BGP 事件
- 算力中心和 BGP 观测站详情卡可以自动采集并预览坐标候选;算力中心待定位气泡可以打开列表并保存候选
- 鼠标拖动、滚轮缩放和缩放百分比提示正常工作
- 设置面板可以切换巡航模式、日夜模式、卫星显示风格
@@ -176,6 +189,14 @@ http://localhost:3000/admin
这会让前端和后端监听局域网可访问地址。
注意:`--allow-lan` 只负责让 Planet 服务监听 `0.0.0.0`,不等于自动把 WSL 服务暴露到 Windows 局域网 IP。常见情况是
- WSL 内 `localhost:3000` / `localhost:8000` 能访问
- Windows 本机 `localhost:3000` / `localhost:8000` 能访问
- 但手机或其他电脑访问 `http://<Windows局域网IP>:3000` 失败
这通常说明 Windows 端还缺少端口转发或防火墙放行。
如果访问失败,先在运行 Planet 的 shell 中检查:
```bash
@@ -184,6 +205,16 @@ curl http://localhost:8000/health
ss -ltnp | grep -E ':3000|:8000'
```
如果确认 WSL 中已监听 `0.0.0.0:3000``0.0.0.0:8000`,但局域网 IP 仍不能访问,请在管理员 PowerShell 中配置 Windows 端转发和防火墙:
```powershell
netsh interface portproxy add v4tov4 listenaddress=0.0.0.0 listenport=3000 connectaddress=127.0.0.1 connectport=3000
netsh interface portproxy add v4tov4 listenaddress=0.0.0.0 listenport=8000 connectaddress=127.0.0.1 connectport=8000
New-NetFirewallRule -DisplayName "WSL Planet 3000" -Direction Inbound -Action Allow -Protocol TCP -LocalPort 3000
New-NetFirewallRule -DisplayName "WSL Planet 8000" -Direction Inbound -Action Allow -Protocol TCP -LocalPort 8000
```
## 9. 停止服务
```bash