release: bump version to 0.41.2

This commit is contained in:
rayd1o
2026-04-27 23:22:04 +08:00
parent 655e2a7d2d
commit eeee788530
14 changed files with 265 additions and 25 deletions

View File

@@ -1 +1 @@
0.41.1
0.41.2

View File

@@ -1,6 +1,10 @@
FROM python:3.14-slim
ARG PYTHON_IMAGE=python:3.14-slim
ARG UV_IMAGE=ghcr.io/astral-sh/uv:latest
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
FROM ${UV_IMAGE} AS uv
FROM ${PYTHON_IMAGE}
COPY --from=uv /uv /uvx /bin/
WORKDIR /app

View File

@@ -1,6 +1,10 @@
FROM python:3.14-slim
ARG PYTHON_IMAGE=python:3.14-slim
ARG UV_IMAGE=ghcr.io/astral-sh/uv:latest
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
FROM ${UV_IMAGE} AS uv
FROM ${PYTHON_IMAGE}
COPY --from=uv /uv /uvx /bin/
WORKDIR /app

View File

@@ -18,6 +18,9 @@ services:
build:
context: .
dockerfile: aiprovider/Dockerfile
args:
PYTHON_IMAGE: ${PYTHON_IMAGE:-python:3.14-slim}
UV_IMAGE: ${UV_IMAGE:-ghcr.io/astral-sh/uv:latest}
container_name: planet_aiprovider
ports:
- "8010:8010"

View File

@@ -5,6 +5,9 @@ services:
build:
context: .
dockerfile: aiprovider/Dockerfile
args:
PYTHON_IMAGE: ${PYTHON_IMAGE:-python:3.14-slim}
UV_IMAGE: ${UV_IMAGE:-ghcr.io/astral-sh/uv:latest}
container_name: planet_aiprovider
ports:
- "8010:8010"

View File

@@ -5,6 +5,9 @@ services:
build:
context: .
dockerfile: aiprovider/Dockerfile
args:
PYTHON_IMAGE: ${PYTHON_IMAGE:-python:3.14-slim}
UV_IMAGE: ${UV_IMAGE:-ghcr.io/astral-sh/uv:latest}
env_file:
- ./aiprovider/.env
container_name: planet_aiprovider

View File

@@ -8,6 +8,22 @@ This project follows the repository versioning rule:
- `improvement` -> `+0.0.1`bugfix + 小功能混合)
- `bugfix` -> `+0.0.1`
## [0.41.2] — 2026-04-27
### 🔧 Improvements
- `planet.sh` 启动链路新增 verbose 滚动输出窗口,并在后端端口占用时打印目标地址和监听进程诊断
- Docker 构建支持通过 build args 覆盖 Python 与 uv 镜像,方便 Docker Hub 不稳定时切换镜像源
### 🐛 Fixes
- Earth 海缆登陆点改为基于相机射线与地球遮挡判断可见性,修复旋转后 pin 可见性滞后一帧的问题
### 🔧 Improvements
- `docker-compose*.yml` 为 AI Provider 构建传入 `PYTHON_IMAGE` / `UV_IMAGE` 参数,默认仍使用官方镜像
- 后端启动失败遇到 `Address already in use` 时输出 `lsof``ss` 与 PID 命令行信息
- verbose 模式下 AI Provider build、后端与前端启动日志会在 spinner 下方保留最新 5 行滚动展示
---
## [0.41.1] — 2026-04-27
### 🐛 Fixes

View File

@@ -16,12 +16,13 @@
## Current Version
- `main` 当前主线历史推导到:`0.16.5`
- `dev` 当前开发分支历史推导到:`0.41.1`
- `dev` 当前开发分支历史推导到:`0.41.2`
## Timeline
| Version | Type | Branch | Commit | Summary |
| --- | --- | --- | --- | --- |
| `0.41.2` | improvement | `dev` | `pending` | 启动脚本新增 verbose 滚动日志与端口占用诊断Docker 构建支持镜像源覆盖,并修复 Earth 登陆点遮挡判断 |
| `0.41.1` | improvement | `dev` | `pending` | 修复新闻直播持久化失效、pin 边缘遮挡;图标抽取为 SVG 并建立规范 |
| `0.41.0` | feature | `dev` | `pending` | Earth 图层顺序拆分、基座海陆色块、国界交互、高清材质/云图/地形层级与样式文档落地 |
| `0.40.5` | improvement | `dev` | `pending` | 卫星 ribbon 拖尾、Iridium 覆盖球面投影填充+外圈、搜索自动聚焦修复 |

View File

@@ -1,6 +1,6 @@
{
"name": "planet-frontend",
"version": "0.41.1",
"version": "0.41.2",
"private": true,
"packageManager": "bun@1",
"dependencies": {

View File

@@ -21,7 +21,11 @@ let cableIdMap = new Map();
let cableStates = new Map();
let cablesVisible = true;
let landingPointTexture = null;
const _lpEarthWorldPos = new THREE.Vector3();
const _lpWorldPos = new THREE.Vector3();
const _lpCameraRel = new THREE.Vector3();
const _lpPointRel = new THREE.Vector3();
const _lpCameraToPoint = new THREE.Vector3();
function createLandingPointTexture() {
const size = CABLE_CONFIG.landingPoint.textureSize;
@@ -460,7 +464,7 @@ export async function loadLandingPoints(scene, earthObj, options = {}) {
color: CABLE_CONFIG.landingPoint.color,
transparent: true,
opacity: CABLE_CONFIG.landingPoint.opacity,
depthTest: true,
depthTest: false,
depthWrite: false,
}),
);
@@ -597,16 +601,40 @@ export function getAllLandingPoints() {
return landingPoints;
}
// Hide pins within ~3° of the limb (normalised dot < 0.05) to prevent the
// "half-clipped-into-globe" look caused by depthTest clipping the billboard
// against closer earth-surface geometry near the horizon.
const _FACING_DOT_MIN_SQ = 0.05 * 0.05;
function isFacingCamera(lp, camera) {
lp.getWorldPosition(_lpWorldPos);
const d = _lpWorldPos.dot(camera.position);
if (d <= 0) return false;
return d * d > _FACING_DOT_MIN_SQ * _lpWorldPos.lengthSq() * camera.position.lengthSq();
if (lp.parent) {
lp.parent.getWorldPosition(_lpEarthWorldPos);
} else {
_lpEarthWorldPos.set(0, 0, 0);
}
_lpCameraRel.copy(camera.position).sub(_lpEarthWorldPos);
_lpPointRel.copy(_lpWorldPos).sub(_lpEarthWorldPos);
_lpCameraToPoint.subVectors(_lpPointRel, _lpCameraRel);
const distanceSq = _lpCameraToPoint.lengthSq();
if (distanceSq <= 0) return true;
const distance = Math.sqrt(distanceSq);
_lpCameraToPoint.multiplyScalar(1 / distance);
// The pin sprite is rendered without depth testing so its full shape does
// not get sliced by the globe. Instead, hide it when the camera-to-anchor
// segment is occluded by a slightly inflated globe, matching the behavior of
// the BGP and compute-center markers near the limb.
const occlusionRadius =
CONFIG.earthRadius + CABLE_CONFIG.landingPoint.altitudeOffset * 0.45;
const cameraProjection = _lpCameraRel.dot(_lpCameraToPoint);
const cameraRadiusSq = _lpCameraRel.lengthSq();
const discriminant =
cameraProjection * cameraProjection -
(cameraRadiusSq - occlusionRadius * occlusionRadius);
if (discriminant < 0) return true;
const nearestIntersection = -cameraProjection - Math.sqrt(discriminant);
return nearestIntersection <= 0 || nearestIntersection >= distance;
}
export function applyLandingPointVisualState(lockedCableName, dimAll = false, camera = null) {

View File

@@ -3399,6 +3399,12 @@ function animate() {
}
}
// Force matrixWorld to be current before any isFacingCamera checks below.
// Without this, getWorldPosition() on LP sprites reads the previous frame's
// transform (earth rotation is updated above but scene.updateMatrixWorld()
// only runs inside renderer.render(), which hasn't been called yet).
earth?.updateMatrixWorld(true);
applyCableVisualState();
const activeCruiseMarker =
isCruiseModeActive() && isCruisePresentationPinned()

186
planet.sh
View File

@@ -35,6 +35,10 @@ WAIT_SPINNER_TICKS_PER_SECOND=8
WAIT_SPINNER_DETAIL=""
WAIT_SPINNER_MESSAGE=""
WAIT_SESSION_ACTIVE=0
VERBOSE=0
WAIT_VERBOSE_LOG_FILE=""
WAIT_VERBOSE_LINE_COUNT="${WAIT_VERBOSE_LINE_COUNT:-5}"
WAIT_VERBOSE_TAIL_LINE_COUNT="${WAIT_VERBOSE_TAIL_LINE_COUNT:-40}"
HTTP_CHECK_MAX_TIME="${HTTP_CHECK_MAX_TIME:-0.2}"
BACKEND_MAX_RETRIES="${BACKEND_MAX_RETRIES:-3}"
@@ -186,7 +190,36 @@ render_wait_spinner() {
local frame_index=$(( (step - 1) % ${#WAIT_SPINNER_FRAMES[@]} ))
local frame="${WAIT_SPINNER_FRAMES[$frame_index]}"
local detail="${WAIT_SPINNER_DETAIL:- }"
printf "\r${DIM}·${NC} ${CYAN}%-2s${NC} ${WHITE}%s${NC}\033[K\n${DIM} %s${NC}\033[K\033[1A\r" "$frame" "$message" "$detail" >&2
local verbose_line=""
local verbose_lines=()
local line_count=0
local cursor_up=1
if [ "$VERBOSE" -eq 1 ] && [ -n "$WAIT_VERBOSE_LOG_FILE" ] && [ -f "$WAIT_VERBOSE_LOG_FILE" ]; then
while IFS= read -r verbose_line; do
verbose_line="$(sanitize_wait_detail "$verbose_line")"
[ -n "$verbose_line" ] || continue
verbose_lines+=("$verbose_line")
done < <(tail -n "$WAIT_VERBOSE_TAIL_LINE_COUNT" "$WAIT_VERBOSE_LOG_FILE" 2>/dev/null)
while [ "${#verbose_lines[@]}" -gt "$WAIT_VERBOSE_LINE_COUNT" ]; do
verbose_lines=("${verbose_lines[@]:1}")
done
fi
printf "\r${DIM}·${NC} ${CYAN}%-2s${NC} ${WHITE}%s${NC}\033[K\n${DIM} %s${NC}\033[K" "$frame" "$message" "$detail" >&2
if [ "$VERBOSE" -eq 1 ]; then
line_count=1
while [ "$line_count" -le "$WAIT_VERBOSE_LINE_COUNT" ]; do
verbose_line="${verbose_lines[$line_count]:- }"
printf "\n${DIM} %s${NC}\033[K" "$verbose_line" >&2
line_count=$((line_count + 1))
done
cursor_up=$((WAIT_VERBOSE_LINE_COUNT + 1))
fi
printf "\033[%sA\r" "$cursor_up" >&2
}
animate_wait_spinner() {
@@ -208,7 +241,29 @@ animate_wait_spinner() {
}
clear_wait_spinner() {
printf "\r\033[K\n\033[K\033[1A\r" >&2
local line_count=0
local clear_verbose_lines=0
if [ "$VERBOSE" -eq 1 ] && { [ "$WAIT_SESSION_ACTIVE" -eq 1 ] || [ -n "$WAIT_VERBOSE_LOG_FILE" ]; }; then
clear_verbose_lines=1
fi
printf "\r\033[K" >&2
line_count=0
while [ "$line_count" -lt 1 ]; do
printf "\n\033[K" >&2
line_count=$((line_count + 1))
done
if [ "$clear_verbose_lines" -eq 1 ]; then
line_count=0
while [ "$line_count" -lt "$WAIT_VERBOSE_LINE_COUNT" ]; do
printf "\n\033[K" >&2
line_count=$((line_count + 1))
done
fi
printf "\033[%sA\r" "$((clear_verbose_lines == 1 ? WAIT_VERBOSE_LINE_COUNT + 1 : 1))" >&2
}
start_wait_session() {
@@ -247,6 +302,7 @@ close_wait_session_context() {
reset_wait_spinner_state() {
WAIT_SPINNER_DETAIL=""
WAIT_SPINNER_MESSAGE=""
WAIT_VERBOSE_LOG_FILE=""
}
sanitize_wait_detail() {
@@ -274,12 +330,19 @@ set_wait_detail() {
run_command_with_spinner() {
local message="$1"
shift
local verbose_log_file=""
if [ "$WAIT_SESSION_ACTIVE" -eq 1 ]; then
set_wait_detail "$message"
fi
"$@" &
if [ "$VERBOSE" -eq 1 ]; then
verbose_log_file="$(mktemp /tmp/planet_verbose.XXXXXX.log)"
WAIT_VERBOSE_LOG_FILE="$verbose_log_file"
"$@" > "$verbose_log_file" 2>&1 &
else
"$@" &
fi
local command_pid=$!
local exit_code=0
@@ -294,6 +357,9 @@ run_command_with_spinner() {
done
wait "$command_pid" || exit_code=$?
if [ "$VERBOSE" -eq 1 ] && [ -n "$verbose_log_file" ]; then
WAIT_VERBOSE_LOG_FILE="$verbose_log_file"
fi
return "$exit_code"
}
@@ -619,7 +685,7 @@ ensure_ai_provider_image_current() {
build_ai_provider_image_with_fallback() {
if compose_available; then
set_wait_detail "使用 docker compose 构建 AI Provider 镜像"
if run_command_with_spinner "构建 AI Provider 镜像" sh -c "docker compose build aiprovider > \"$AI_PROVIDER_BUILD_LOG_FILE\" 2>&1"; then
if run_ai_provider_build_command "docker compose"; then
return 0
fi
if compose_v1_available; then
@@ -632,7 +698,7 @@ build_ai_provider_image_with_fallback() {
if compose_v1_available; then
set_wait_detail "使用 docker-compose v1 构建 AI Provider 镜像"
if run_command_with_spinner "构建 AI Provider 镜像" sh -c "docker-compose build aiprovider > \"$AI_PROVIDER_BUILD_LOG_FILE\" 2>&1"; then
if run_ai_provider_build_command "docker-compose"; then
return 0
fi
return 1
@@ -641,6 +707,17 @@ build_ai_provider_image_with_fallback() {
report_missing_compose
}
run_ai_provider_build_command() {
local compose_command="$1"
if [ "$VERBOSE" -eq 1 ]; then
run_command_with_spinner "构建 AI Provider 镜像" sh -c "${compose_command} build aiprovider 2>&1 | tee \"$AI_PROVIDER_BUILD_LOG_FILE\""
return $?
fi
run_command_with_spinner "构建 AI Provider 镜像" sh -c "${compose_command} build aiprovider > \"$AI_PROVIDER_BUILD_LOG_FILE\" 2>&1"
}
install_uv_if_needed() {
if command -v uv >/dev/null 2>&1; then
return 0
@@ -909,6 +986,9 @@ wait_for_frontend_ready() {
else
set_wait_detail "启动 Vite 开发服务器"
fi
if [ "$VERBOSE" -eq 1 ]; then
WAIT_VERBOSE_LOG_FILE="$log_file"
fi
while [ "$tick" -lt "$max_ticks" ]; do
WAIT_SPINNER_STEP=$((WAIT_SPINNER_STEP + 1))
@@ -1045,17 +1125,25 @@ start_backend_with_retry() {
: > /tmp/planet_backend.log
PYTHONPATH="$SCRIPT_DIR/backend" nohup uv run --project "$SCRIPT_DIR" python -m uvicorn app.main:app --host 0.0.0.0 --port "$backend_port" --reload > /tmp/planet_backend.log 2>&1 &
BACKEND_PID=$!
if [ "$VERBOSE" -eq 1 ]; then
WAIT_VERBOSE_LOG_FILE="/tmp/planet_backend.log"
fi
if wait_for_http "http://localhost:${backend_port}/health" "$BACKEND_HEALTH_CHECK_ATTEMPTS" "$BACKEND_HEALTH_CHECK_INTERVAL" "后端"; then
return 0
fi
kill "$BACKEND_PID" 2>/dev/null || true
if backend_log_indicates_port_conflict "/tmp/planet_backend.log"; then
report_backend_port_conflict_if_needed "$backend_port" "/tmp/planet_backend.log"
return 1
fi
animate_wait_spinner "后端第 ${retry}/${BACKEND_MAX_RETRIES} 次启动未就绪,准备重试" "$BACKEND_HEALTH_CHECK_INTERVAL"
retry=$((retry + 1))
done
clear_wait_spinner
report_backend_port_conflict_if_needed "$backend_port" "/tmp/planet_backend.log" || true
return 1
}
@@ -1387,6 +1475,66 @@ frontend_log_indicates_port_conflict() {
grep -q "Port .* is already in use" "$log_file" 2>/dev/null
}
backend_log_indicates_port_conflict() {
local log_file="$1"
[ -f "$log_file" ] || return 1
grep -Eiq "Address already in use|Errno 98" "$log_file" 2>/dev/null
}
print_port_listener_details() {
local port="$1"
local found=0
local pid=""
local command_line=""
local lsof_output=""
local ss_output=""
local line=""
if command -v lsof >/dev/null 2>&1; then
lsof_output="$(lsof -nP -iTCP:"${port}" -sTCP:LISTEN 2>/dev/null || true)"
if [ -n "$lsof_output" ]; then
printf "%s\n" "$lsof_output" | awk '{print " " $0}'
found=1
fi
fi
if command -v ss >/dev/null 2>&1; then
ss_output="$(ss -ltnpH "( sport = :${port} )" 2>/dev/null || true)"
while IFS= read -r line; do
[ -n "$line" ] || continue
printf "${DIM} ss: %s${NC}\n" "$line"
found=1
done <<EOF
$ss_output
EOF
fi
for pid in $(collect_port_pids "$port" || true); do
command_line="$(ps -p "$pid" -o args= 2>/dev/null | sed -E 's/[[:space:]]+/ /g; s/^ //; s/ $//')"
[ -n "$command_line" ] || command_line="未知命令"
printf "${DIM} pid %s: %s${NC}\n" "$pid" "$command_line"
found=1
done
if [ "$found" -eq 0 ]; then
log_note "未能在当前环境内定位端口 ${port} 的监听进程,可能被宿主机或外部网络命名空间占用。"
fi
}
report_backend_port_conflict_if_needed() {
local backend_port="$1"
local log_file="$2"
if backend_log_indicates_port_conflict "$log_file" || ! can_bind_port "$backend_port"; then
log_error "后端地址已被占用: 0.0.0.0:${backend_port} / 127.0.0.1:${backend_port} / [::1]:${backend_port}"
print_port_listener_details "$backend_port"
return 0
fi
return 1
}
# Frontend lifecycle helpers
cleanup_frontend_processes() {
local frontend_port="${1:-$DEFAULT_FRONTEND_PORT}"
@@ -1527,6 +1675,10 @@ parse_service_args() {
FRONTEND_LAN_ENABLED=1
shift 1
;;
-v|--verbose)
VERBOSE=1
shift 1
;;
*)
log_error "未知参数: $1"
exit 1
@@ -1831,6 +1983,25 @@ log() {
esac
}
parse_global_args() {
while [ "$#" -gt 0 ]; do
case "$1" in
-v|--verbose)
VERBOSE=1
shift 1
;;
*)
break
;;
esac
done
GLOBAL_ARG_REMAINDER=("$@")
}
parse_global_args "$@"
set -- "${GLOBAL_ARG_REMAINDER[@]}"
case "$1" in
start)
shift
@@ -1854,9 +2025,10 @@ case "$1" in
;;
*)
log_error "用法: ./planet.sh {start|stop|restart|createuser|health|log}"
log_note "start 启动服务,可选: -b <后端端口> -f <前端端口> -a <AI Provider 端口> --allow-lan"
log_note "全局参数: -v, --verbose 在当前执行行下方滚动显示最多 5 行命令输出"
log_note "start 启动服务,可选: -b <后端端口> -f <前端端口> -a <AI Provider 端口> --allow-lan --verbose"
log_note "stop 停止服务"
log_note "restart 重启服务,可选: -b [后端端口] -f [前端端口] -a [AI Provider 端口] -d --allow-lan"
log_note "restart 重启服务,可选: -b [后端端口] -f [前端端口] -a [AI Provider 端口] -d --allow-lan --verbose"
log_note "createuser 交互创建用户"
log_note "health 检查健康状态"
log_note "log 查看日志"

View File

@@ -1,6 +1,6 @@
[project]
name = "planet"
version = "0.41.1"
version = "0.41.2"
description = "智能星球计划 - 态势感知系统"
requires-python = ">=3.14"
dependencies = [

2
uv.lock generated
View File

@@ -475,7 +475,7 @@ wheels = [
[[package]]
name = "planet"
version = "0.41.1"
version = "0.41.2"
source = { virtual = "." }
dependencies = [
{ name = "aiofiles" },