release: bump version to 0.58.0

Release 0.58.0 includes the Earth high-precision boundary PMTiles/MVT pipeline, standardized Earth boundary source collectors, China POV boundary configuration templates, and removal of the legacy low-precision GeoJSON fallback. It also adds Earth news target-location queueing/archive support, fixes datasource task status visibility, documents the Earth surface depth-spacing rules that prevent far-zoom z-fighting snow/black blocks, and updates bilingual operations/developer docs.
This commit is contained in:
linkong
2026-05-15 17:40:07 +08:00
parent dd176a6ae6
commit 93eb41a9f7
75 changed files with 5217 additions and 716 deletions

481
planet.sh
View File

@@ -120,9 +120,6 @@ FRONTEND_PID_FILE="$PLANET_STATE_DIR/frontend.pid"
FRONTEND_LOG_FILE="$PLANET_STATE_DIR/frontend.log"
PLANET_PORT_STATE_FILE="$PLANET_STATE_DIR/ports.env"
FRONTEND_VITE_ENTRY="$SCRIPT_DIR/frontend/node_modules/vite/bin/vite.js"
WINDOWS_LAN_RELAY_SCRIPT="$SCRIPT_DIR/scripts/windows-lan-relay.ps1"
WINDOWS_FRONTEND_RELAY_PID_FILE="$PLANET_STATE_DIR/windows_frontend_relay.pid"
WINDOWS_BACKEND_RELAY_PID_FILE="$PLANET_STATE_DIR/windows_backend_relay.pid"
MOTION_AGENT_PID_FILE="$PLANET_STATE_DIR/motion_agent.pid"
MOTION_AGENT_LOG_FILE="$PLANET_STATE_DIR/motion_agent.log"
AI_PROVIDER_BUILD_STAMP_FILE="$PLANET_CACHE_DIR/aiprovider_build.sha256"
@@ -136,10 +133,6 @@ START_RUN_COMPLETED=0
STARTED_BACKEND_THIS_RUN=0
STARTED_FRONTEND_THIS_RUN=0
STARTED_MOTION_AGENT_THIS_RUN=0
WINDOWS_LAN_RELAY_ENABLED=0
LAN_PUBLIC_FRONTEND_PORT="$DEFAULT_FRONTEND_PORT"
LAN_PUBLIC_BACKEND_PORT="$DEFAULT_BACKEND_PORT"
WINDOWS_LAN_RELAY_LISTEN_ADDRESS=""
AI_PROVIDER_RUNTIME_ENV_NAMES=(
SERVICE_NAME
SERVICE_VERSION
@@ -646,81 +639,20 @@ EOF
log_lan_access_notes() {
local frontend_port="$1"
local backend_port="$2"
local public_frontend_port="${3:-$frontend_port}"
local public_backend_port="${4:-$backend_port}"
local ai_provider_port="${3:-$AI_PROVIDER_PORT}"
local recommended_lan_ip=""
if recommended_lan_ip="$(get_recommended_lan_ipv4)"; then
log_note "推荐访问地址: http://${recommended_lan_ip}:${public_frontend_port}"
log_note "后端健康检查: http://${recommended_lan_ip}:${public_backend_port}/health"
log_note "推荐访问地址: http://${recommended_lan_ip}:${frontend_port}"
log_note "后端健康检查: http://${recommended_lan_ip}:${backend_port}/health"
log_note "AI Provider 健康检查: http://${recommended_lan_ip}:${ai_provider_port}/health"
else
log_note "前端已对局域网开放,请使用本机局域网 IP 访问 :${public_frontend_port}"
log_note "后端已对局域网开放,请使用本机局域网 IP 访问 :${public_backend_port}/health"
log_note "前端已对局域网开放,请使用本机局域网 IP 访问 :${frontend_port}"
log_note "后端已对局域网开放,请使用本机局域网 IP 访问 :${backend_port}/health"
log_note "AI Provider 已对局域网开放,请使用本机局域网 IP 访问 :${ai_provider_port}/health"
fi
}
cleanup_windows_lan_relays() {
local pid_file
local pid
for pid_file in "$WINDOWS_FRONTEND_RELAY_PID_FILE" "$WINDOWS_BACKEND_RELAY_PID_FILE"; do
if [ -f "$pid_file" ]; then
if pid="$(read_pid_file "$pid_file")"; then
terminate_process_tree TERM "$pid"
sleep 0.3
terminate_process_tree KILL "$pid"
fi
remove_pid_file "$pid_file"
fi
done
}
start_windows_lan_relay() {
local name="$1"
local public_port="$2"
local target_port="$3"
local pid_file="$4"
local target_host="$5"
local listen_address="$6"
local script_path
local relay_pid
script_path="$(wslpath -w "$WINDOWS_LAN_RELAY_SCRIPT" 2>/dev/null || true)"
if [ -z "$script_path" ]; then
log_warn "无法转换 Windows relay 脚本路径,跳过 ${name} 局域网 relay"
return 1
fi
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$script_path" \
-ListenAddress "$listen_address" \
-ListenPort "$public_port" \
-TargetHost "$target_host" \
-TargetPort "$target_port" \
> /dev/null 2>&1 &
relay_pid=$!
write_pid_file "$pid_file" "$relay_pid"
sleep 0.5
if ! kill -0 "$relay_pid" 2>/dev/null; then
remove_pid_file "$pid_file"
log_warn "${name} Windows relay 未能监听端口 ${public_port}"
print_windows_port_listener_details "$public_port" || true
return 1
fi
log_success "${name} Windows relay 已启动: ${listen_address}:${public_port} -> ${target_host}:${target_port}"
}
start_windows_lan_relays() {
local target_host="127.0.0.1"
[ "$WINDOWS_LAN_RELAY_ENABLED" -eq 1 ] || return 0
cleanup_windows_lan_relays
start_windows_lan_relay "前端" "$LAN_PUBLIC_FRONTEND_PORT" "$FRONTEND_PORT" "$WINDOWS_FRONTEND_RELAY_PID_FILE" "$target_host" "$WINDOWS_LAN_RELAY_LISTEN_ADDRESS" || return 1
start_windows_lan_relay "后端" "$LAN_PUBLIC_BACKEND_PORT" "$BACKEND_PORT" "$WINDOWS_BACKEND_RELAY_PID_FILE" "$target_host" "$WINDOWS_LAN_RELAY_LISTEN_ADDRESS" || return 1
}
run_windows_admin_powershell_script() {
local script_path="$1"
local warn_context="$2"
@@ -737,15 +669,21 @@ run_windows_admin_powershell_script() {
powershell.exe -NoProfile -Command "Start-Process powershell.exe -Verb RunAs -Wait -ArgumentList '-NoProfile -ExecutionPolicy Bypass -File \"${script_win_path}\"'" >/dev/null 2>&1
}
join_csv() {
local IFS=","
printf "%s" "$*"
}
windows_firewall_missing_ports() {
local frontend_port="$1"
local backend_port="$2"
local ports_csv
command -v powershell.exe >/dev/null 2>&1 || return 1
[ "$#" -gt 0 ] || return 0
ports_csv="$(join_csv "$@")"
powershell.exe -NoProfile -Command "
\$ErrorActionPreference = 'SilentlyContinue'
\$ports = @([int]${frontend_port}, [int]${backend_port})
\$ports = @(${ports_csv})
function Test-PortAllowed {
param([int]\$Port)
\$displayName = 'WSL Planet ' + \$Port
@@ -772,19 +710,19 @@ if (\$missing.Count -gt 0) {
}
request_windows_firewall_rules_if_needed() {
local frontend_port="$1"
local backend_port="$2"
local missing_ports=""
local script_path=""
[ "$WINDOWS_LAN_RELAY_ENABLED" -eq 1 ] || return 0
[ "$#" -gt 0 ] || return 0
is_wsl_environment || return 0
command -v powershell.exe >/dev/null 2>&1 || return 0
case "${PLANET_WINDOWS_FIREWALL_AUTO_PROMPT:-1}" in
0|false|no|off)
return 0
;;
esac
missing_ports="$(windows_firewall_missing_ports "$frontend_port" "$backend_port" || true)"
missing_ports="$(windows_firewall_missing_ports "$@" || true)"
[ -n "$missing_ports" ] || return 0
script_path="$PLANET_STATE_DIR/windows_firewall_planet.ps1"
@@ -812,15 +750,18 @@ EOF
}
windows_portproxy_conflict_ports() {
local frontend_port="$1"
local backend_port="$2"
local ports_csv
command -v powershell.exe >/dev/null 2>&1 || return 1
[ "$#" -gt 0 ] || return 0
ports_csv="$(join_csv "$@")"
powershell.exe -NoProfile -Command "
\$ErrorActionPreference = 'SilentlyContinue'
\$ports = @([int]${frontend_port}, [int]${backend_port})
\$lines = @(netsh interface portproxy show v4tov4)
\$ports = @(${ports_csv})
\$netsh = Join-Path \$env:WINDIR 'System32\netsh.exe'
if (-not (Test-Path \$netsh)) { \$netsh = 'netsh' }
\$lines = @(& \$netsh interface portproxy show v4tov4)
\$found = @()
foreach (\$line in \$lines) {
if (\$line -match '^\\s*(\\S+)\\s+(\\d+)\\s+(\\S+)\\s+(\\d+)\\s*$') {
@@ -836,20 +777,23 @@ if (\$found.Count -gt 0) {
write_windows_portproxy_cleanup_script() {
local script_path="$1"
local frontend_port="$2"
local backend_port="$3"
shift
local ports_csv
ports_csv="$(join_csv "$@")"
cat > "$script_path" <<EOF
\$ErrorActionPreference = 'Continue'
\$ports = @([int]${frontend_port}, [int]${backend_port})
\$lines = @(netsh interface portproxy show v4tov4)
\$ports = @(${ports_csv})
\$netsh = Join-Path \$env:WINDIR 'System32\netsh.exe'
if (-not (Test-Path \$netsh)) { \$netsh = 'netsh' }
\$lines = @(& \$netsh interface portproxy show v4tov4)
\$removed = @()
foreach (\$line in \$lines) {
if (\$line -match '^\\s*(\\S+)\\s+(\\d+)\\s+(\\S+)\\s+(\\d+)\\s*$') {
\$listenAddress = \$Matches[1]
\$listenPort = [int]\$Matches[2]
if (\$ports -contains \$listenPort) {
netsh interface portproxy delete v4tov4 listenaddress=\$listenAddress listenport=\$listenPort | Out-Null
& \$netsh interface portproxy delete v4tov4 listenaddress=\$listenAddress listenport=\$listenPort | Out-Null
\$removed += (\$listenAddress + ':' + \$listenPort)
}
}
@@ -860,24 +804,24 @@ EOF
}
request_windows_portproxy_cleanup_if_needed() {
local frontend_port="$1"
local backend_port="$2"
local conflict_ports=""
local script_path=""
[ "$WINDOWS_LAN_RELAY_ENABLED" -eq 1 ] || return 0
[ "$#" -gt 0 ] || return 0
is_wsl_environment || return 0
command -v powershell.exe >/dev/null 2>&1 || return 0
case "${PLANET_WINDOWS_PORTPROXY_CLEANUP_PROMPT:-1}" in
0|false|no|off)
return 0
;;
esac
conflict_ports="$(windows_portproxy_conflict_ports "$frontend_port" "$backend_port" || true)"
conflict_ports="$(windows_portproxy_conflict_ports "$@" || true)"
[ -n "$conflict_ports" ] || return 0
script_path="$PLANET_STATE_DIR/windows_portproxy_cleanup_planet.ps1"
mkdir -p "$PLANET_STATE_DIR"
write_windows_portproxy_cleanup_script "$script_path" "$frontend_port" "$backend_port"
write_windows_portproxy_cleanup_script "$script_path" "$@"
log_warn "检测到旧 Windows portproxy 占用端口 ${conflict_ports},即将请求管理员 PowerShell 删除"
run_windows_admin_powershell_script "$script_path" "检测到旧 portproxy 占用端口 ${conflict_ports}" || {
@@ -886,6 +830,80 @@ request_windows_portproxy_cleanup_if_needed() {
}
}
write_windows_port_cleanup_script() {
local script_path="$1"
shift
local ports_csv
ports_csv="$(join_csv "$@")"
cat > "$script_path" <<EOF
\$ErrorActionPreference = 'Continue'
\$ports = @(${ports_csv})
\$netsh = Join-Path \$env:WINDIR 'System32\netsh.exe'
if (-not (Test-Path \$netsh)) { \$netsh = 'netsh' }
foreach (\$port in \$ports) {
\$lines = @(& \$netsh interface portproxy show v4tov4)
foreach (\$line in \$lines) {
if (\$line -match '^\\s*(\\S+)\\s+(\\d+)\\s+(\\S+)\\s+(\\d+)\\s*$') {
\$listenAddress = \$Matches[1]
\$listenPort = [int]\$Matches[2]
if (\$listenPort -eq \$port) {
& \$netsh interface portproxy delete v4tov4 listenaddress=\$listenAddress listenport=\$listenPort | Out-Null
Write-Host "Planet removed portproxy \$($listenAddress):\$listenPort"
}
}
}
\$connections = @(Get-NetTCPConnection -LocalPort \$port -State Listen -ErrorAction SilentlyContinue)
\$seen = @{}
foreach (\$connection in \$connections) {
\$pidValue = [int]\$connection.OwningProcess
if (\$pidValue -le 0 -or \$seen.ContainsKey(\$pidValue)) { continue }
\$seen[\$pidValue] = \$true
\$process = Get-CimInstance Win32_Process -Filter "ProcessId=\$pidValue" -ErrorAction SilentlyContinue
\$processName = if (\$process.Name) { \$process.Name } else { 'unknown' }
\$services = @(Get-CimInstance Win32_Service -ErrorAction SilentlyContinue | Where-Object { \$_.ProcessId -eq \$pidValue })
foreach (\$service in \$services) {
try {
Stop-Service -Name \$service.Name -Force -ErrorAction Stop
Write-Host "Planet stopped service \$($service.Name) on port \$port"
} catch {
Write-Host "Planet failed to stop service \$($service.Name) on port \$port: \$($_.Exception.Message)"
}
}
try {
Stop-Process -Id \$pidValue -Force -ErrorAction Stop
Write-Host "Planet stopped process \$pidValue (\$processName) on port \$port"
} catch {
Write-Host "Planet failed to stop process \$pidValue (\$processName) on port \$port: \$($_.Exception.Message)"
}
}
}
Start-Sleep -Seconds 1
EOF
}
request_windows_port_cleanup() {
local script_path="$PLANET_STATE_DIR/windows_port_cleanup_planet.ps1"
local ports_label
[ "$#" -gt 0 ] || return 0
is_wsl_environment || return 1
command -v powershell.exe >/dev/null 2>&1 || return 1
ports_label="$(join_csv "$@")"
mkdir -p "$PLANET_STATE_DIR"
write_windows_port_cleanup_script "$script_path" "$@"
log_warn "即将请求管理员 PowerShell 清理 Windows 侧端口 ${ports_label}"
run_windows_admin_powershell_script "$script_path" "清理 Windows 侧端口 ${ports_label}" || {
log_warn "管理员 PowerShell 未完成端口清理;端口 ${ports_label} 可能仍无法绑定"
return 1
}
}
log_motion_agent_access_notes() {
local motion_agent_port="$1"
local lan_enabled="${2:-0}"
@@ -953,13 +971,29 @@ run_with_retry() {
return 1
fi
animate_wait_spinner "${retry_label} 第 ${attempt}/${max_retries} 次失败,${interval} 秒后重试" "$interval"
animate_wait_spinner "${retry_label} 本次执行失败,${interval} 秒后进行第 $((attempt + 1)) 次尝试" "$interval"
attempt=$((attempt + 1))
done
return 1
}
format_wait_elapsed_seconds() {
local completed_intervals="${1:-0}"
local interval="${2:-1}"
awk -v completed="$completed_intervals" -v interval="$interval" '
BEGIN {
value = completed * interval
if (value == int(value)) {
printf "%d", value
} else {
printf "%.1f", value
}
}
'
}
docker_daemon_available() {
docker info >/dev/null 2>&1
}
@@ -1432,8 +1466,9 @@ wait_for_http() {
local attempt=1
local wait_message="检查${service_name}健康检查"
local wait_detail_prefix="请求"
local elapsed_seconds=""
set_wait_detail "${wait_detail_prefix} ${url} (1/${attempts})"
set_wait_detail "${wait_detail_prefix} ${url},已等待 0 秒"
while [ "$attempt" -le "$attempts" ]; do
if http_ok "$url"; then
@@ -1441,7 +1476,8 @@ wait_for_http() {
return 0
fi
set_wait_detail "${wait_detail_prefix} ${url} (${attempt}/${attempts})"
elapsed_seconds="$(format_wait_elapsed_seconds "$((attempt - 1))" "$interval")"
set_wait_detail "${wait_detail_prefix} ${url},已等待 ${elapsed_seconds} 秒"
animate_wait_spinner "${wait_message}" "$interval"
attempt=$((attempt + 1))
done
@@ -1561,8 +1597,9 @@ wait_for_container_health() {
local health_status
local wait_message="检查${service_name}容器健康检查"
local wait_detail_prefix="读取"
local elapsed_seconds=""
set_wait_detail "${wait_detail_prefix} ${container_name} 容器健康状态 (1/${attempts})"
set_wait_detail "${wait_detail_prefix} ${container_name} 容器健康状态,已等待 0 秒"
while [ "$attempt" -le "$attempts" ]; do
health_status="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$container_name" 2>/dev/null || true)"
@@ -1572,7 +1609,8 @@ wait_for_container_health() {
return 0
fi
set_wait_detail "${wait_detail_prefix} ${container_name} 容器健康状态 (${attempt}/${attempts})"
elapsed_seconds="$(format_wait_elapsed_seconds "$((attempt - 1))" "$interval")"
set_wait_detail "${wait_detail_prefix} ${container_name} 容器健康状态,已等待 ${elapsed_seconds} 秒"
animate_wait_spinner "${wait_message}" "$interval"
attempt=$((attempt + 1))
done
@@ -1587,7 +1625,7 @@ fail_unreleased_port() {
if [ -z "$(collect_port_pids "$port" || true)" ]; then
log_error "端口 ${port} 当前环境内未发现占用进程,但端口仍不可用,请检查宿主机或外部环境占用"
print_port_listener_details "$port"
log_note "如果显示 Windows listener(如 svchost.exe / iphlpsvc),请优先用管理员 PowerShell 检查 portproxy:netsh interface portproxy show all;删除冲突转发后重新启动,--allow-lan 会改用临时 Windows relay。"
log_note "如果显示 Windows listener(如 svchost.exe / iphlpsvc),脚本会在需要时请求管理员 PowerShell 清理 Windows 侧端口。"
else
log_error "端口 ${port} 清理失败,请检查占用进程"
if command -v lsof >/dev/null 2>&1; then
@@ -1607,6 +1645,7 @@ retry_wait_for_port_release() {
local service_name="$2"
local attempt=1
local pids=""
local elapsed_seconds=""
while [ "$attempt" -le "$PORT_PRESTART_RETRIES" ]; do
if wait_for_port_release "$port"; then
@@ -1614,13 +1653,14 @@ retry_wait_for_port_release() {
fi
pids="$(collect_port_pids "$port" || true)"
elapsed_seconds="$(format_wait_elapsed_seconds "$((attempt - 1))" "$PORT_PRESTART_RETRY_INTERVAL")"
if [ -n "$pids" ]; then
log_warn "${service_name}端口 ${port} 仍有监听进程,继续清理 (${attempt}/${PORT_PRESTART_RETRIES})"
log_warn "${service_name}端口 ${port} 仍有监听进程,已等待 ${elapsed_seconds} 秒,继续清理"
for pid in $pids; do
terminate_process_tree KILL "$pid"
done
else
log_warn "${service_name}端口 ${port} 暂未发现监听进程但仍不可绑定,等待释放 (${attempt}/${PORT_PRESTART_RETRIES})"
log_warn "${service_name}端口 ${port} 暂未发现监听进程但仍不可绑定,已等待 ${elapsed_seconds} 秒,继续等待释放"
print_port_listener_details "$port"
fi
@@ -1669,104 +1709,33 @@ print_windows_cleanup_recovery_steps() {
printf "${DIM} netstat -ano | findstr :%s${NC}\n" "$port"
printf "${DIM} tasklist /svc /fi \"PID eq <PID>\"${NC}\n"
printf "${DIM} Stop-Service iphlpsvc -Force${NC}\n"
log_note "如需局域网仍使用 ${port},请删除旧 portproxy 后重新运行 --allow-lan;脚本会使用临时 Windows relay 暴露该端口。"
log_note "如需局域网仍使用 ${port},请清理冲突监听后重新运行 --allow-lan;脚本会直接用 0.0.0.0 绑定该端口。"
log_note "重新启动示例:${command_example}"
}
force_cleanup_external_port_listener() {
local port="$1"
local service_name="$2"
local output=""
local saw_windows_listener=0
local saw_no_windows_listener=0
local saw_cleanup_failure=0
validate_port "$port"
is_wsl_environment || return 1
command -v powershell.exe >/dev/null 2>&1 || return 1
log_warn "检测到 WSL 环境,正在检查 ${service_name} 端口 ${port} 的 Windows 侧监听"
output="$(
powershell.exe -NoProfile -Command "
\$ErrorActionPreference = 'Continue'
\$port = [int]${port}
\$connections = @(Get-NetTCPConnection -LocalPort \$port -State Listen -ErrorAction SilentlyContinue)
if (-not \$connections -or \$connections.Count -eq 0) {
'no-windows-listener'
exit 0
}
\$seen = @{}
foreach (\$connection in \$connections) {
\$pidValue = [int]\$connection.OwningProcess
if (\$seen.ContainsKey(\$pidValue)) { continue }
\$seen[\$pidValue] = \$true
\$process = Get-CimInstance Win32_Process -Filter \"ProcessId=\$pidValue\" -ErrorAction SilentlyContinue
\$processName = if (\$process.Name) { \$process.Name } else { 'unknown' }
\$services = @(Get-CimInstance Win32_Service -ErrorAction SilentlyContinue | Where-Object { \$_.ProcessId -eq \$pidValue })
\$serviceNames = @(\$services | Select-Object -ExpandProperty Name)
'attempt-windows-cleanup pid={0} process={1} services={2}' -f \$pidValue, \$processName, (\$serviceNames -join ',')
log_warn "检查 Windows 侧端口占用: ${port}"
if ! print_windows_port_listener_details "$port"; then
log_note "Windows 侧未发现端口 ${port} 监听,继续启动"
return 0
fi
foreach (\$service in \$services) {
try {
Stop-Service -Name \$service.Name -Force -ErrorAction Stop
'stopped-service {0}' -f \$service.Name
} catch {
'failed-stop-service {0}: {1}' -f \$service.Name, \$_.Exception.Message
}
}
try {
Stop-Process -Id \$pidValue -Force -ErrorAction Stop
'stopped-process {0}' -f \$pidValue
} catch {
'failed-stop-process {0}: {1}' -f \$pidValue, \$_.Exception.Message
}
}
" 2>&1 | tr -d '\r'
)"
while IFS= read -r line; do
[ -n "$line" ] || continue
case "$line" in
no-windows-listener)
saw_no_windows_listener=1
log_note "Windows 侧未发现端口 ${port} 监听,继续启动"
;;
stopped-service*|stopped-process*)
saw_windows_listener=1
printf "${GREEN} %s${NC}\n" "$line"
;;
failed-*)
saw_windows_listener=1
saw_cleanup_failure=1
printf "${DIM} %s${NC}\n" "$line"
;;
attempt-windows-cleanup*)
saw_windows_listener=1
printf "${DIM} %s${NC}\n" "$line"
;;
*)
printf "${DIM} %s${NC}\n" "$line"
;;
esac
done <<EOF
$output
EOF
log_warn "Windows 侧端口 ${port} 存在监听者,准备请求管理员权限清理"
request_windows_port_cleanup "$port" || return 1
if wait_for_port_release "$port" 10 0.3; then
return 0
fi
if [ "$saw_no_windows_listener" -eq 1 ] && [ "$saw_windows_listener" -eq 0 ]; then
return 0
fi
if [ "$saw_windows_listener" -eq 0 ]; then
log_warn "Windows 侧未发现端口 ${port} 监听,但端口仍不可绑定"
elif [ "$saw_cleanup_failure" -eq 1 ]; then
log_warn "Windows 侧监听清理失败,端口 ${port} 仍不可绑定"
print_windows_cleanup_recovery_steps "$port" "$service_name"
fi
log_warn "Windows 侧端口 ${port} 清理后仍不可绑定"
print_windows_cleanup_recovery_steps "$port" "$service_name"
return 1
}
@@ -1825,6 +1794,7 @@ cleanup_backend_processes() {
start_backend_with_retry() {
local backend_port="$1"
local retry=1
local backend_bind_host="0.0.0.0"
while [ "$retry" -le "$BACKEND_MAX_RETRIES" ]; do
if ! cleanup_backend_processes "$backend_port"; then
@@ -1832,7 +1802,7 @@ start_backend_with_retry() {
fi
cd "$SCRIPT_DIR/backend"
: > "$BACKEND_LOG_FILE"
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 > "$BACKEND_LOG_FILE" 2>&1 &
PYTHONPATH="$SCRIPT_DIR/backend" nohup uv run --project "$SCRIPT_DIR" python -m uvicorn app.main:app --host "$backend_bind_host" --port "$backend_port" --reload > "$BACKEND_LOG_FILE" 2>&1 &
BACKEND_PID=$!
write_pid_file "$BACKEND_PID_FILE" "$BACKEND_PID"
if [ "$VERBOSE" -eq 1 ]; then
@@ -1854,7 +1824,7 @@ start_backend_with_retry() {
report_backend_port_conflict_if_needed "$backend_port" "$BACKEND_LOG_FILE"
return 1
fi
animate_wait_spinner "后端第 ${retry}/${BACKEND_MAX_RETRIES} 次启动未就绪,准备重试" "$BACKEND_HEALTH_CHECK_INTERVAL"
animate_wait_spinner "后端启动未就绪,准备再次尝试" "$BACKEND_HEALTH_CHECK_INTERVAL"
retry=$((retry + 1))
done
@@ -1895,7 +1865,7 @@ start_ai_provider_service() {
remove_ai_provider_containers
AI_PROVIDER_RECREATE_REQUIRED=1
animate_wait_spinner "AI Provider 第 ${retry}/${AI_PROVIDER_START_MAX_RETRIES} 次启动后仍未健康,正在重启容器" "$AI_PROVIDER_RETRY_INTERVAL"
animate_wait_spinner "AI Provider 启动后仍未健康,正在重启容器并准备再次尝试" "$AI_PROVIDER_RETRY_INTERVAL"
retry=$((retry + 1))
done
@@ -1926,7 +1896,7 @@ ensure_database_services_healthy() {
fi
docker restart planet_postgres planet_redis >/dev/null 2>&1 || true
animate_wait_spinner "数据库第 ${retry}/${DATABASE_START_MAX_RETRIES} 次启动后仍未健康,正在重启容器" "$DATABASE_RETRY_INTERVAL"
animate_wait_spinner "数据库启动后仍未健康,正在重启容器并准备再次尝试" "$DATABASE_RETRY_INTERVAL"
retry=$((retry + 1))
done
}
@@ -1951,7 +1921,7 @@ ensure_postgres_service_healthy() {
fi
docker restart planet_postgres >/dev/null 2>&1 || true
animate_wait_spinner "PostgreSQL 第 ${retry}/${DATABASE_START_MAX_RETRIES} 次启动后仍未健康,正在重启容器" "$DATABASE_RETRY_INTERVAL"
animate_wait_spinner "PostgreSQL 启动后仍未健康,正在重启容器并准备再次尝试" "$DATABASE_RETRY_INTERVAL"
retry=$((retry + 1))
done
}
@@ -1971,7 +1941,7 @@ restart_database_service() {
exit 1
fi
animate_wait_spinner "数据库第 ${retry}/${DATABASE_START_MAX_RETRIES} 次重启后仍未健康,继续重试" "$DATABASE_RETRY_INTERVAL"
animate_wait_spinner "数据库重启后仍未健康,准备再次尝试" "$DATABASE_RETRY_INTERVAL"
retry=$((retry + 1))
done
}
@@ -2164,48 +2134,6 @@ wait_for_port_release() {
return 1
}
get_windows_lan_ipv4() {
command -v powershell.exe >/dev/null 2>&1 || return 1
powershell.exe -NoProfile -Command "
\$ErrorActionPreference = 'SilentlyContinue'
\$route = Get-NetRoute -DestinationPrefix '0.0.0.0/0' |
Where-Object { \$_.NextHop -and \$_.NextHop -ne '0.0.0.0' } |
Sort-Object RouteMetric, InterfaceMetric |
Select-Object -First 1
if (\$route) {
\$ip = Get-NetIPAddress -AddressFamily IPv4 -InterfaceIndex \$route.InterfaceIndex |
Where-Object { \$_.IPAddress -notmatch '^(127|169\\.254)\\.' } |
Select-Object -First 1 -ExpandProperty IPAddress
if (\$ip) { \$ip }
}
" 2>/dev/null | tr -d '\r' | sed -n '1p'
}
prepare_windows_lan_relay_ports() {
local listen_address=""
[ "$FRONTEND_LAN_ENABLED" -eq 1 ] || return 0
is_wsl_environment || return 0
command -v powershell.exe >/dev/null 2>&1 || return 0
[ -f "$WINDOWS_LAN_RELAY_SCRIPT" ] || return 0
listen_address="$(get_windows_lan_ipv4 || true)"
if [ -z "$listen_address" ]; then
log_warn "未能识别 Windows 局域网 IP,跳过 Windows 局域网 relay;本机 localhost 仍可使用 ${FRONTEND_PORT}/${BACKEND_PORT}"
return 0
fi
WINDOWS_LAN_RELAY_ENABLED=1
LAN_PUBLIC_FRONTEND_PORT="$FRONTEND_PORT"
LAN_PUBLIC_BACKEND_PORT="$BACKEND_PORT"
WINDOWS_LAN_RELAY_LISTEN_ADDRESS="$listen_address"
cleanup_windows_lan_relays
request_windows_portproxy_cleanup_if_needed "$LAN_PUBLIC_FRONTEND_PORT" "$LAN_PUBLIC_BACKEND_PORT" || true
log_note "WSL 局域网 relay: Windows ${WINDOWS_LAN_RELAY_LISTEN_ADDRESS}:${LAN_PUBLIC_FRONTEND_PORT}/${LAN_PUBLIC_BACKEND_PORT} -> localhost:${FRONTEND_PORT}/${BACKEND_PORT}"
}
kill_port_if_requested() {
local port="$1"
local service_name="$2"
@@ -2385,6 +2313,7 @@ cleanup_frontend_processes() {
fi
pkill -f "${FRONTEND_VITE_ENTRY} --port ${frontend_port} --strictPort" 2>/dev/null || true
pkill -f "${FRONTEND_VITE_ENTRY} --host 127.0.0.1 --port ${frontend_port} --strictPort" 2>/dev/null || true
pkill -f "${FRONTEND_VITE_ENTRY} --host 0.0.0.0 --port ${frontend_port} --strictPort" 2>/dev/null || true
}
@@ -2424,7 +2353,7 @@ start_frontend_with_retry() {
kill_port_if_requested "$frontend_port" "前端"
fi
animate_wait_spinner "前端第 ${retry}/${FRONTEND_MAX_RETRIES} 次启动未就绪,准备重试" "$FRONTEND_HEALTH_CHECK_INTERVAL"
animate_wait_spinner "前端启动未就绪,准备再次尝试" "$FRONTEND_HEALTH_CHECK_INTERVAL"
retry=$((retry + 1))
done
@@ -2811,7 +2740,6 @@ cleanup_failed_start() {
clear_wait_spinner
log_warn "启动未完成,清理本轮已拉起的本地进程"
cleanup_windows_lan_relays
if [ "$STARTED_MOTION_AGENT_THIS_RUN" -eq 1 ]; then
cleanup_motion_agent_processes TERM
@@ -2929,11 +2857,51 @@ PY
log_note "邮箱: ${email}"
}
prepare_allow_lan_public_ports() {
[ "$FRONTEND_LAN_ENABLED" -eq 1 ] || return 0
log_step "检查局域网端口可绑定"
kill_port_if_requested "$FRONTEND_PORT" "前端"
kill_port_if_requested "$BACKEND_PORT" "后端"
kill_port_if_requested "$AI_PROVIDER_PORT" "AI Provider"
request_windows_portproxy_cleanup_if_needed "$FRONTEND_PORT" "$BACKEND_PORT" "$AI_PROVIDER_PORT" || true
request_windows_firewall_rules_if_needed "$FRONTEND_PORT" "$BACKEND_PORT" "$AI_PROVIDER_PORT" || true
}
verify_http_endpoint() {
local label="$1"
local url="$2"
log_note "检查${label}: ${url}"
if ! http_ok "$url"; then
log_error "${label}不可访问: ${url}"
return 1
fi
}
verify_allow_lan_access() {
local lan_ip=""
[ "$FRONTEND_LAN_ENABLED" -eq 1 ] || return 0
verify_http_endpoint "本机访问" "http://localhost:${FRONTEND_PORT}" || exit 1
verify_http_endpoint "本机访问" "http://localhost:${BACKEND_PORT}/health" || exit 1
verify_http_endpoint "本机访问" "http://localhost:${AI_PROVIDER_PORT}/health" || exit 1
if lan_ip="$(get_recommended_lan_ipv4)"; then
verify_http_endpoint "局域网访问" "http://${lan_ip}:${FRONTEND_PORT}" || exit 1
verify_http_endpoint "局域网访问" "http://${lan_ip}:${BACKEND_PORT}/health" || exit 1
verify_http_endpoint "局域网访问" "http://${lan_ip}:${AI_PROVIDER_PORT}/health" || exit 1
else
log_warn "未能识别局域网 IP,跳过局域网端到端健康检查"
fi
}
# Top-level commands
start() {
parse_service_args "$@"
prepare_windows_lan_relay_ports
cleanup_exit_containers
prepare_allow_lan_public_ports
START_RUN_ACTIVE=1
START_RUN_COMPLETED=0
@@ -2953,37 +2921,25 @@ start() {
fi
write_port_state "$BACKEND_PORT" "$FRONTEND_PORT" "$AI_PROVIDER_PORT" "$MOTION_AGENT_PORT"
if [ "$WINDOWS_LAN_RELAY_ENABLED" -eq 1 ]; then
start_windows_lan_relays
request_windows_firewall_rules_if_needed "$LAN_PUBLIC_FRONTEND_PORT" "$LAN_PUBLIC_BACKEND_PORT" || true
fi
verify_allow_lan_access
START_RUN_COMPLETED=1
START_RUN_ACTIVE=0
log_success "启动完成"
if [ "$WINDOWS_LAN_RELAY_ENABLED" -eq 1 ]; then
log_note "智能星球计划: http://localhost:${LAN_PUBLIC_FRONTEND_PORT}/earth"
log_note "智能星球仪表盘: http://localhost:${LAN_PUBLIC_FRONTEND_PORT}/admin"
log_note "AI Playground: http://localhost:${LAN_PUBLIC_FRONTEND_PORT}/playground"
log_note "智能星球开发文档: http://localhost:${LAN_PUBLIC_BACKEND_PORT}/docs"
log_note "局域网入口: http://${WINDOWS_LAN_RELAY_LISTEN_ADDRESS}:${LAN_PUBLIC_FRONTEND_PORT}/earth"
else
log_note "智能星球计划: http://localhost:${FRONTEND_PORT}/earth"
log_note "智能星球仪表盘: http://localhost:${FRONTEND_PORT}/admin"
log_note "AI Playground: http://localhost:${FRONTEND_PORT}/playground"
log_note "智能星球开发文档: http://localhost:${BACKEND_PORT}/docs"
fi
log_note "智能星球计划: http://localhost:${FRONTEND_PORT}/earth"
log_note "智能星球仪表盘: http://localhost:${FRONTEND_PORT}/admin"
log_note "AI Playground: http://localhost:${FRONTEND_PORT}/playground"
log_note "智能星球开发文档: http://localhost:${BACKEND_PORT}/docs"
if [ "$MOTION_AGENT_REQUESTED" -eq 1 ]; then
log_motion_agent_access_notes "$MOTION_AGENT_PORT" "$FRONTEND_LAN_ENABLED"
fi
if [ "$FRONTEND_LAN_ENABLED" -eq 1 ] && [ "$WINDOWS_LAN_RELAY_ENABLED" -eq 0 ]; then
log_lan_access_notes "$FRONTEND_PORT" "$BACKEND_PORT" "$LAN_PUBLIC_FRONTEND_PORT" "$LAN_PUBLIC_BACKEND_PORT"
if [ "$FRONTEND_LAN_ENABLED" -eq 1 ]; then
log_lan_access_notes "$FRONTEND_PORT" "$BACKEND_PORT"
fi
}
stop() {
log_warn "停止服务"
cleanup_windows_lan_relays
stop_backend_service
stop_ai_provider_service
stop_frontend_service
@@ -2993,6 +2949,16 @@ stop() {
log_success "已停止"
}
stop_local_services_for_restart() {
log_warn "正在停止本地服务"
stop_backend_service
stop_ai_provider_service
stop_frontend_service
stop_motion_agent_service
stop_container_if_running "planet_postgres" "PostgreSQL"
stop_container_if_running "planet_redis" "Redis"
}
restart() {
parse_service_args "$@"
cleanup_exit_containers
@@ -3002,7 +2968,7 @@ restart() {
local state_motion_agent_port=""
if [ "$BACKEND_PORT_REQUESTED" -eq 0 ] && [ "$FRONTEND_PORT_REQUESTED" -eq 0 ] && [ "$AI_PROVIDER_REQUESTED" -eq 0 ] && [ "$MOTION_AGENT_REQUESTED" -eq 0 ] && [ "$DATABASE_REQUESTED" -eq 0 ]; then
stop
stop_local_services_for_restart
sleep 1
start "$@"
return 0
@@ -3017,6 +2983,9 @@ restart() {
if [ "$AI_PROVIDER_REQUESTED" -eq 1 ]; then
stop_ai_provider_service
sleep 1
if [ "$FRONTEND_LAN_ENABLED" -eq 1 ]; then
kill_port_if_requested "$AI_PROVIDER_PORT" "AI Provider"
fi
start_ai_provider_service "$AI_PROVIDER_PORT"
fi