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

@@ -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`