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

@@ -29,6 +29,8 @@
- [AIS 多源采集、冲突记录与聚合接口计划](/home/ray/dev/linkong/planet/docs/plans/earth-vessel-ais-aggregation-plan.md)
- [earth-interactable-layer-plan.md](/home/ray/dev/linkong/planet/docs/plans/earth-interactable-layer-plan.md)
- [frontend-public-docs-site-plan.md](/home/ray/dev/linkong/planet/docs/plans/frontend-public-docs-site-plan.md)
- [Docs Gatekeeper 鉴权系统计划](/home/ray/dev/linkong/planet/docs/plans/docs-gatekeeper-auth-plan.md)
- [Location Resolver 共享管线计划](/home/ray/dev/linkong/planet/docs/plans/location-resolver-shared-pipeline-plan.md)
- [frontend-ai-playground-development-plan.md](/home/ray/dev/linkong/planet/docs/plans/frontend-ai-playground-development-plan.md)
- [ue5-mvp-fused-plan.md](/home/ray/dev/linkong/planet/docs/plans/ue5-mvp-fused-plan.md)

View File

@@ -0,0 +1,92 @@
# Docs Gatekeeper 鉴权系统计划
**状态**:已实现,当前行为见 [Docs Gatekeeper 开发说明](/home/ray/dev/linkong/planet/docs/technical/zh/docs-gatekeeper-development.md)
**创建日期**2026-05-08
**核心目标**:把 `/docs` 从前端公开打包 Markdown 改成后端受控读取,并通过用户 Gatekeeper 权限组划分公开文档、用户文档、开发文档和管理/运维文档。
## 背景
当前 Docs 页面通过前端 `import.meta.glob(...?raw)``docs/technical/{zh,en}` 中注册过的 Markdown 直接打进前端 bundle。即使在前端隐藏目录或增加路由守卫受保护 Markdown 仍可能出现在构建产物中,无法形成真正鉴权。
本阶段需要把文档正文读取迁到后端并让后端根据当前用户身份返回可见目录和正文。Earth 仍保持公开访问,其它控制台模块暂不改变既有鉴权。
## 鉴权模型
保留现有 `users.role`,新增 `gatekeeper_groups` 作为可叠加的权限组。`role` 继续用于控制台和系统操作Gatekeeper 只负责 Docs 等内容权限。
默认权限:
| 身份 | 默认 Docs 能力 |
| --- | --- |
| 未登录访客 | `public` |
| 普通登录用户 | `public`,以及用户被分配的 Gatekeeper 组 |
| `admin` | `docs_admin`,并隐含 `docs_developer` / `docs_user` |
| `super_admin` | 全部 Docs 权限 |
Gatekeeper 组:
- `docs_user`:登录用户操作类文档。
- `docs_developer`开发、前端、后端、Earth 实现文档。
- `docs_admin`:运维、服务控制、凭证、环境变量和敏感操作文档。
## 初步文档划分
`public`
- `README.md`
- `quickstart.md`
- `manual.md`
`docs_developer`
- `earth-frontend-context.md`
- `earth-interactable-usage.md`
- `earth-layer-style-reference.md`
- `earth-render-layer-order.md`
- `earth-satellite-footprint-policy.md`
- `earth-bgp-context.md`
- `earth-news-live-streams-collector-format.md`
- `earth-toolbar-overlay-coordination.md`
- `frontend-admin-frontend-context.md`
- `frontend-layout-guidelines.md`
- `backend-collectors.md`
- `datasource-collector-settings-connectivity.md`
- `backend-datasources-api-performance.md`
- `agents-aiprovider.md`
`docs_admin`
- `backend-system-service-control.md`
- `ops-docker-compose-buildx-upgrade.md`
- `ops-planet-sh-startup.md`
## 实施要点
后端新增:
- `GET /api/v1/docs/catalog`:返回当前用户可见文档目录;未登录只返回 `public`
- `GET /api/v1/docs/{lang}/{slug}`:返回单篇 Markdown未登录访问受保护文档返回 `401`,已登录无权限返回 `403`
- 服务端维护文档 metadata 白名单,禁止任意路径读取。
用户管理新增:
- `users.gatekeeper_groups` JSON 字段。
- 用户列表、创建和编辑支持展示/配置 Gatekeeper 权限组。
- 只有 `super_admin` 能编辑 Gatekeeper 权限组。
前端 Docs 改造:
- 移除 Markdown raw import 作为正文来源。
- 从后端 catalog 构建目录和搜索记录。
- 从后端 content API 加载正文。
-`401` 显示登录入口,对 `403` 显示无权限提示。
## 验证
- 未登录用户只能看到和读取 `public` 文档。
- 未登录直接访问受保护文档返回 `401` 并显示登录提示。
- 无 Gatekeeper 组的普通用户访问开发文档返回 `403`
- `docs_developer` 用户能读开发文档,不能读管理/运维文档。
- `admin``super_admin` 能读管理/运维文档。
- 未知 slug、未知语言和路径穿越字符串不能读取文件。
- 前端构建产物不再包含受保护 Markdown raw import 生成的文档模块。

View File

@@ -0,0 +1,252 @@
# Earth Mobile Center Country Highlight Plan
## Goal
移动端打开 Earth 国界图层后,用屏幕中心,也就是当前镜头正对的地球表面位置,自动识别所在国家,并高亮该国家国界。
桌面端仍保持现有 hover 行为。移动端不引入新的国界渲染体系,而是复用已有 `country-boundaries.js` 的 GeoJSON 命中和 hover 高亮能力。
## Criteria for success
1. 移动端 `layout-mode-mobile` 下,国界图层开启后,屏幕中心所在国家会自动高亮。
2. 移动端旋转、缩放、巡航或自动旋转地球时,高亮会跟随镜头中心更新。
3. 屏幕中心落在海洋或没有命中地球时,国家高亮会清除。
4. 国界图层关闭时,不执行中心国家识别,也不显示残留高亮。
5. 桌面端 pointer hover 行为保持不变。
6. 移动端抽屉、搜索、设置、媒体、详情等前景 UI 打开时,不因为用户操作 UI 产生明显误高亮或抖动。
7. 中心识别有节流或状态缓存,不把 GeoJSON point-in-polygon 检测放到无条件每帧高频执行。
8. 实现后能通过本地静态检查或前端构建,并用移动端 viewport 手动或 Playwright 验证核心场景。
## Existing pieces
当前项目已经具备大部分基础能力:
- [frontend/public/earth/js/country-boundaries.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/country-boundaries.js)
- `updateCountryBoundaryHover(coords)`:根据 `{ lat, lon }` 命中国家并更新高亮线。
- `clearCountryBoundaryHover()`:清除当前 hover 高亮。
- `getShowCountryBoundaries()`:判断国界线图层是否可见。
- [frontend/public/earth/js/utils.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/utils.js)
- `screenToEarthCoords(clientX, clientY, camera, earth, domElement)`:屏幕坐标 raycast 到地球表面。
- `vector3ToLatLon(vector)`:地球本地坐标转经纬度。
- [frontend/public/earth/js/constants.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/constants.js)
- `COUNTRY_BOUNDARY_CONFIG` 已定义普通国界线和 hover 国界线样式。
- 移动端布局状态已经通过 `layout-mode-mobile` body class 区分。
因此本需求的核心不是新增图层,而是补一个移动端中心取点控制器。
## Non-goals
- 不改变桌面端 hover 交互。
- 不替换 `countries-admin0.min.geojson` 数据源。
- 不新增后端 API。
- 不把国家面填充做成新的 selected country 面状 shader。
- 不为移动端增加永久准星 UI除非后续产品明确需要视觉准星。
## Implementation plan
### 1. Add a small mobile center hover controller
新增一个轻量函数,建议放在现有主循环附近或单独模块,例如:
```text
frontend/public/earth/js/mobile-center-country-highlight.js
```
建议导出:
```js
updateMobileCenterCountryHighlight({
camera,
earth,
renderer,
now,
isBlocked,
});
clearMobileCenterCountryHighlight();
```
职责:
1. 判断是否处于移动端。
2. 判断国界图层是否开启。
3. 判断当前是否被移动端前景 UI 阻塞。
4. 对 renderer canvas 中心点做 raycast。
5. 命中地球后转经纬度。
6. 调用 `updateCountryBoundaryHover({ lat, lon })`
7. 无命中或禁用时调用 `clearCountryBoundaryHover()`
### 2. Use canvas center, not window center
中心点应基于 renderer canvas rect 计算:
```js
const rect = renderer.domElement.getBoundingClientRect();
const clientX = rect.left + rect.width / 2;
const clientY = rect.top + rect.height / 2;
```
这样在移动端安全区、地址栏变化、viewport resize 或 canvas 非全屏时仍然准确。
### 3. Convert center point into country hover coords
复用已有工具:
```js
const point = screenToEarthCoords(clientX, clientY, camera, earth, renderer.domElement);
if (!point) {
clearCountryBoundaryHover();
return;
}
const coords = vector3ToLatLon(point);
updateCountryBoundaryHover(coords);
```
注意:`screenToEarthCoords` 返回的是 earth local point符合 `vector3ToLatLon` 的输入语义。
### 4. Gate updates by mobile and foreground UI state
建议新增一个本地判断函数:
```js
function isMobileCenterCountryHighlightBlocked() {
return (
!document.body.classList.contains("layout-mode-mobile") ||
document.body.classList.contains("earth-search-open") ||
document.body.classList.contains("earth-settings-open") ||
document.body.classList.contains("earth-media-open") ||
document.body.classList.contains("earth-info-open")
);
}
```
如果移动端抽屉只是半收起、且没有覆盖中心视野,可以继续允许中心高亮。若实际体验里抽屉展开会遮挡中心点,再把 drawer open 状态纳入阻塞条件。
### 5. Throttle and cache center updates
GeoJSON polygon 命中不应该无条件每帧执行。
第一版建议:
- `throttleMs = 120`
- 缓存上次经纬度,中心点变化小于 `0.05` 度时跳过。
- 禁用、切回桌面、图层关闭、UI 阻塞时立即清除一次高亮。
伪代码:
```js
if (now - lastUpdateAt < 120) return;
if (Math.abs(coords.lat - lastLat) < 0.05 && Math.abs(coords.lon - lastLon) < 0.05) return;
```
### 6. Wire into the Earth animation loop
在 [frontend/public/earth/js/main.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/main.js) 的动画循环中调用:
```js
updateMobileCenterCountryHighlight({
camera,
earth,
renderer,
now: performance.now(),
isBlocked: isMobileCenterCountryHighlightBlocked(),
});
```
这样自动旋转、手势旋转、缩放和巡航都会自然更新。
### 7. Keep desktop hover unchanged
桌面 pointer hover 仍然走当前逻辑。
移动端中心高亮只在 `layout-mode-mobile` 下生效,不应该监听 pointer move也不应该抢占 desktop hover 状态。
### 8. Optional visual tuning
第一版复用:
- `COUNTRY_BOUNDARY_CONFIG.hoverLineColor`
- `COUNTRY_BOUNDARY_CONFIG.hoverLineOpacity`
- `COUNTRY_BOUNDARY_CONFIG.hoverGlowOpacity`
如果移动端体验太强,可以后续加独立配置:
```js
mobileCenterHoverLineOpacity
mobileCenterHoverGlowOpacity
```
但第一版不建议过早分叉样式。
## Verification
### Static checks
1. `npm` 前端构建或现有 lint/typecheck 命令通过。
2. `rg` 确认新增函数只在移动端路径调用,不影响桌面 pointer hover。
3. `git diff --stat` 和目标文件 diff 确认改动范围集中。
### Manual mobile checks
使用移动端 viewport例如 390x844
1. 打开 Earth。
2. 开启国界图层。
3. 转动地球到中国、美国、澳大利亚等大块陆地区域,确认中心国家国界高亮。
4. 转动到太平洋或印度洋,确认高亮消失。
5. 缩放地球,确认高亮仍跟随中心点。
6. 打开移动端搜索、设置、媒体或详情面板,确认没有明显误高亮或抖动。
7. 切回桌面 viewport确认 hover 仍由鼠标位置控制。
### Playwright smoke check
如果已有 Playwright 流程,建议补一个移动端 smoke
1. 设置 viewport 为手机尺寸。
2. 打开 Earth 页面。
3. 开启国界图层。
4. 等待国界数据加载。
5. 截图确认中心附近国家边界有 hover 高亮线。
这个 smoke 不必断言具体国家名称,因为当前功能核心是视觉高亮;更稳定的自动化可以后续通过暴露 debug state 实现。
## Risks and mitigations
### Polygon hit cost too高
风险:移动端设备上频繁 `featureContains` 可能带来卡顿。
缓解:
- 使用 `120ms` 节流。
- 经纬度变化小于阈值时跳过。
- 后续如仍慢,再为 GeoJSON features 预计算 bbox先 bbox 粗筛再 point-in-polygon。
### UI blocking state 不完整
风险:某些移动端前景 UI 没有对应 body class中心点被遮挡但高亮仍更新。
缓解:
- 第一版覆盖现有主要 class。
- 验证时记录遗漏项,补充到 `isMobileCenterCountryHighlightBlocked()`
### Desktop hover 被移动端状态污染
风险:移动端中心高亮和桌面 hover 共用 `_hoveredFeature` 状态。
缓解:
- 只在 `layout-mode-mobile` 下运行中心高亮。
- 切出 mobile 或图层关闭时调用一次 `clearCountryBoundaryHover()`
- 不改 `updateCountryBoundaryHover()` 的语义。
## Milestones
1. 设计落地:完成本 plan明确目标和验收标准。
2. 最小实现:新增移动端中心取点 controller并接入 animation loop。
3. 性能保护:加入节流、经纬度阈值和禁用态清理。
4. 验证:本地构建通过,移动端 viewport 手动检查通过。
5. 调优:根据截图或真机体验微调阻塞条件和节流阈值。

View File

@@ -0,0 +1,127 @@
# Location Resolver Shared Pipeline Plan
**状态**:已实现,当前用户流程见 [Earth 位置候选采集使用手册](/home/ray/dev/linkong/planet/docs/technical/zh/location-pipeline-user.md),开发接口见 [通用位置估算管线开发说明](/home/ray/dev/linkong/planet/docs/technical/zh/location-pipeline-development.md)。
## Goal
把"给定一条记录,决定它的 lat/lon"这件事抽象成一条统一的可插拔管线让算力中心、BGP 观测站、BGP 事件——以及未来任何需要位置估算的实体——共用同一套接口。新算法peeringdb 设施查询、IXP 表、用户认领的精确点位等)通过实现一个 Resolver 类即可挂入,不需要改任何上层调用方。
## Background
### 实施前现状
- **算力中心** (`backend/app/services/compute_center_locations.py`) 早期曾使用源坐标 → 本地 JSON 注册表 → 城市兜底 → Nominatim 在线地理编码。后续为避免硬编码位置污染事实链路,算力中心本地注册表已移除;主地图只使用源坐标,手动候选采集使用 ROR 和 Nominatim。
- **BGP 观测站** (`collectors/bgp_common.py:RIPE_RIS_COLLECTOR_COORDS`) 是一张写死的字典26 个 RIPE RIS collector 的城市级坐标。新增 collector / 升级到设施级精度都得改 Python。
- **BGP 事件**继承所属 collector 的城市级坐标(`BGPObservation.collector_geo`)。
- 用户原本以为 BGP 观测站位置是通过 iptoasn 推断的——其实 iptoasn 只用于前缀级国家归属(`bgp_enrichment.py`),不影响 marker 坐标。
### 痛点
1. 算力中心那条 4 层链路写死在算力中心模块里BGP 想用得复制一遍。
2. 三类实体各走各的坐标策略,缺统一抽象。
3. 未来要插更精的算法peeringdb / IXP / 用户认领),现在没有挂入点。
## Design
### 接口契约
`backend/app/services/location/`
- `models.py` —— `LocationQuery`(输入)、`LocationCandidate`(候选)、`ResolverOutput`(单 resolver 输出)、`ResolutionResult`/`ResolutionDiagnostic`(管线最终结果)
- `pipeline.py` —— `LocationResolver` Protocol、`LocationPipeline` 编排器
- `resolvers/source_coordinates.py` —— 记录自带 lat/lon 时直通
- `resolvers/registry.py` —— 本地 JSON 注册表locations + city_fallbacks按别名得分
- `resolvers/nominatim.py` —— 通用 Nominatim 客户端rate-limited + LRU 缓存)+ 可注入 query plan
- `resolvers/inherit.py` —— 从外部回调取候选(事件继承 collector 用)
- `text.py` —— 文本规范化共享工具
核心 Protocol
```python
class LocationResolver(Protocol):
name: str
def resolve(self, query: LocationQuery) -> ResolverOutput: ...
```
`LocationPipeline.collect_candidates()` 跑全部 resolver聚合所有候选`(source_rank, precision_rank, -confidence)` 排序去重;`resolve_best()` 选 top 候选。
### 各领域管线
```python
# compute_center_locations.py重构后公共 API 不变)
COMPUTE_CENTER_PIPELINE = LocationPipeline([
SourceCoordinatesResolver(),
])
COMPUTE_CENTER_COLLECTION_PIPELINE = LocationPipeline([
SourceCoordinatesResolver(),
ROROrganizationResolver(),
NominatimResolver(query_plan_builder=_compute_center_query_plan,
geocoder=lambda q: _geocode_online(q)),
])
# bgp_collector_locations.py
BGP_COLLECTOR_PIPELINE = LocationPipeline([
SourceCoordinatesResolver(),
StoredCollectorLocationResolver(),
])
BGP_COLLECTOR_COLLECTION_PIPELINE = LocationPipeline([
SourceCoordinatesResolver(),
NominatimResolver(query_plan_builder=_bgp_collector_query_plan,
geocoder=lambda q: _geocode_online(q)),
])
# bgp_event_locations.py
BGP_EVENT_PIPELINE = LocationPipeline([
SourceCoordinatesResolver(),
InheritFromAnotherEntityResolver(source_lookup=_inherit_from_owning_collector),
# 占位:将来插 ASNFacilityResolver / PrefixGeoResolver
])
```
### 关键设计决策
1. **算力中心公共 API 完全不变**`resolve_compute_center_location()``collect_location_candidates()``ComputeCenterLocation` dataclass、`_geocode_online` 模块级符号都保留,前端 / 上层调用方零改动;现有 19 个回归测试全绿。
2. **`_geocode_online` 用 lambda 晚绑定**`NominatimResolver(geocoder=lambda q: _geocode_online(q))` 能让测试 `monkeypatch.setattr(module, "_geocode_online", fake)` 继续生效。
3. **`RIPE_RIS_COLLECTOR_COORDS` 自动从 DB-backed cache 重建**:启动时 seed/refresh `bgp_collector_locations` 维表,再原地刷新旧 `{rrcXX → {city, country, lat, lon}}` 字典。下游消费者(`bgp_collectors.py`、序列化、detector不动即可获得新元数据。
4. **修复隐藏 bug**BGP collector 不再通过 registry/operator 模糊匹配晋升候选,避免 `operator="RIPE NCC"` 让每个事件都落到 `rrc00`
5. **事件继承走严格名字查询**:事件继承不跑 collector 的完整 pipeline改成直接查 DB-backed cache。"改进位置"用户触发流程只跑源坐标和在线地理编码候选。
## Files
### 新增
- `backend/app/services/location/__init__.py`
- `backend/app/services/location/models.py`
- `backend/app/services/location/pipeline.py`
- `backend/app/services/location/text.py`
- `backend/app/services/location/resolvers/__init__.py`
- `backend/app/services/location/resolvers/source_coordinates.py`
- `backend/app/services/location/resolvers/registry.py`
- `backend/app/services/location/resolvers/nominatim.py`
- `backend/app/services/location/resolvers/inherit.py`
- `backend/app/services/bgp_collector_locations.py`
- `backend/app/services/bgp_event_locations.py`
- `backend/app/models/bgp_collector_location.py`
- `backend/tests/test_location_pipeline.py`16 用例)
- `backend/tests/test_bgp_collector_locations.py`11 用例)
### 修改
- `backend/app/services/compute_center_locations.py` —— 改为薄包装
- `backend/app/services/collectors/bgp_common.py` —— 删除写死字典,改调 `resolve_bgp_event_geo_dict()`
- `backend/app/api/v1/bgp.py` —— 新增 `POST /api/v1/bgp/collectors/{collector_id}/collect-location`
- `frontend/public/earth/js/info-card.js` —— `renderComputeCenterCollectSection``renderLocationCollectSection`BGP collector 走通用化路径
- `frontend/public/earth/js/compute-centers.js` —— 新增通用 `collectLocationCandidates(endpoint, payload)`
- `frontend/public/earth/js/main.js` —— `previewComputeCenterCandidate``previewLocationCandidate`,事件名改为 `earth:preview-location-candidate`
## Verification
- `uv run pytest backend/tests/test_visualization_compute_centers.py` —— 19 个用例全绿(公共 API 未改)
- `uv run pytest backend/tests/test_location_pipeline.py backend/tests/test_bgp_collector_locations.py` —— 16 + 11 用例全绿
- 抽象可插拔性测试:`test_pluggability_custom_resolver_works_without_changing_pipeline` —— 临时实现 `_PeeringDBStubResolver` 直接接入 `LocationPipeline`,验证管线不需要改一行就能识别新 source
## Out of scope
- 持久化用户认领的精确坐标(写回 JSON 注册表)—— `suggested_registry_entry` 字段已就绪,工作流单独立项
- 真正实现 `ASNFacilityResolver` / `PrefixGeoResolver` —— 接口已留好具体算法peeringdb / IXP 表 / iptoasn 升级)单独立项
- 算力中心 / 观测站 marker 合并避让 —— 上一轮已用 `SURFACE_AVOIDANCE_PROFILES.city` + halo 收敛解决