forked from linkong/planet
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:
481
planet.sh
481
planet.sh
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user