release: bump version to 0.66.0
Some checks failed
ci / backend (push) Has been cancelled
ci / frontend (push) Has been cancelled
ci / delivery (push) Has been cancelled
release / images (push) Has been cancelled

This commit is contained in:
rayd1o
2026-05-26 03:41:47 +08:00
parent e65267fe21
commit 5bf5c73ca0
173 changed files with 8669 additions and 13210 deletions

477
planet.sh
View File

@@ -115,10 +115,19 @@ FRONTEND_RUNTIME_BIN="${FRONTEND_RUNTIME_BIN:-}"
FRONTEND_RUNTIME_SOURCE="${FRONTEND_RUNTIME_SOURCE:-}"
PLANET_STATE_DIR="${PLANET_STATE_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/planet}"
PLANET_CACHE_DIR="${PLANET_CACHE_DIR:-${XDG_CACHE_HOME:-$HOME/.cache}/planet}"
PLANET_UV_TUNA_INDEX_URL="${PLANET_UV_TUNA_INDEX_URL:-https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple/}"
BACKEND_PID_FILE="$PLANET_STATE_DIR/backend.pid"
BACKEND_LOG_FILE="$PLANET_STATE_DIR/backend.log"
FRONTEND_PID_FILE="$PLANET_STATE_DIR/frontend.pid"
FRONTEND_LOG_FILE="$PLANET_STATE_DIR/frontend.log"
FRONTEND_CA_SERVER_PID_FILE="$PLANET_STATE_DIR/frontend_ca_server.pid"
FRONTEND_CA_SERVER_LOG_FILE="$PLANET_STATE_DIR/frontend_ca_server.log"
FRONTEND_CERT_DIR="$PLANET_STATE_DIR/certs"
FRONTEND_CA_CERT_FILE="$FRONTEND_CERT_DIR/planet-dev-ca.crt"
FRONTEND_CA_KEY_FILE="$FRONTEND_CERT_DIR/planet-dev-ca.key"
FRONTEND_HTTPS_CERT_FILE="$FRONTEND_CERT_DIR/planet-dev-server.crt"
FRONTEND_HTTPS_KEY_FILE="$FRONTEND_CERT_DIR/planet-dev-server.key"
FRONTEND_CA_DOWNLOAD_PORT="${FRONTEND_CA_DOWNLOAD_PORT:-8088}"
PLANET_PORT_STATE_FILE="$PLANET_STATE_DIR/ports.env"
FRONTEND_VITE_ENTRY="$SCRIPT_DIR/frontend/node_modules/vite/bin/vite.js"
MOTION_AGENT_PID_FILE="$PLANET_STATE_DIR/motion_agent.pid"
@@ -249,6 +258,21 @@ write_pid_file() {
chmod 600 "$pid_file" 2>/dev/null || true
}
start_detached_command() {
local log_file="$1"
shift
if command -v setsid >/dev/null 2>&1; then
setsid "$@" </dev/null > "$log_file" 2>&1 &
else
nohup "$@" </dev/null > "$log_file" 2>&1 &
fi
local detached_pid="$!"
disown "$detached_pid" 2>/dev/null || true
printf "%s" "$detached_pid"
}
remove_pid_file() {
local pid_file="$1"
@@ -261,6 +285,12 @@ http_ok() {
curl -fsS --max-time "$HTTP_CHECK_MAX_TIME" "$url" > /dev/null 2>&1
}
https_ok_insecure() {
local url="$1"
curl -kfsS --max-time "$HTTP_CHECK_MAX_TIME" "$url" > /dev/null 2>&1
}
log_matches() {
local log_file="$1"
shift
@@ -286,20 +316,50 @@ read_port_state_value() {
fi
}
read_port_state_text_value() {
local key="$1"
local fallback="$2"
local value=""
if [ -f "$PLANET_PORT_STATE_FILE" ]; then
value="$(sed -nE "s/^${key}=([A-Za-z0-9_.:-]+)$/\\1/p" "$PLANET_PORT_STATE_FILE" | tail -n 1)"
fi
if [ -n "$value" ]; then
printf "%s" "$value"
else
printf "%s" "$fallback"
fi
}
current_frontend_scheme() {
if [ "${FRONTEND_LAN_HTTPS_ENABLED:-0}" -eq 1 ]; then
printf "https"
else
printf "http"
fi
}
write_port_state() {
local backend_port="$1"
local frontend_port="$2"
local ai_provider_port="$3"
local motion_agent_port="$4"
local frontend_scheme="${5:-$(current_frontend_scheme)}"
validate_port "$backend_port"
validate_port "$frontend_port"
validate_port "$ai_provider_port"
validate_port "$motion_agent_port"
case "$frontend_scheme" in
http|https) ;;
*) frontend_scheme="http" ;;
esac
{
printf "BACKEND_PORT=%s\n" "$backend_port"
printf "FRONTEND_PORT=%s\n" "$frontend_port"
printf "FRONTEND_SCHEME=%s\n" "$frontend_scheme"
printf "AI_PROVIDER_PORT=%s\n" "$ai_provider_port"
printf "MOTION_AGENT_PORT=%s\n" "$motion_agent_port"
} > "$PLANET_PORT_STATE_FILE"
@@ -699,12 +759,16 @@ log_lan_access_notes() {
local frontend_port="$1"
local backend_port="$2"
local ai_provider_port="${3:-$AI_PROVIDER_PORT}"
local frontend_scheme="${4:-http}"
local recommended_lan_ip=""
if recommended_lan_ip="$(get_recommended_lan_ipv4)"; then
log_note "推荐访问地址: http://${recommended_lan_ip}:${frontend_port}"
log_note "推荐访问地址: ${frontend_scheme}://${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"
if [ "${FRONTEND_LAN_HTTPS_ENABLED:-0}" -eq 1 ]; then
log_note "手机 CA 证书下载: http://${recommended_lan_ip}:${FRONTEND_CA_DOWNLOAD_PORT}/planet-dev-ca.crt"
fi
else
log_note "前端已对局域网开放,请使用本机局域网 IP 访问 :${frontend_port}"
log_note "后端已对局域网开放,请使用本机局域网 IP 访问 :${backend_port}/health"
@@ -1045,6 +1109,133 @@ run_with_retry() {
return 1
}
run_uv_sync_with_mirror_fallback() {
local log_file="$1"
if run_with_retry \
"$DEPENDENCY_INSTALL_MAX_RETRIES" \
"$DEPENDENCY_INSTALL_RETRY_INTERVAL" \
"uv sync 默认源失败,已重试 ${DEPENDENCY_INSTALL_MAX_RETRIES} 次,准备切换清华源" \
"uv sync --frozen" \
run_command_quiet_unless_verbose "$log_file" uv sync --frozen --group dev; then
return 0
fi
configure_uv_tuna_index
set_wait_detail "已写入 uv.toml 清华源,重新执行 uv sync"
run_with_retry \
"$DEPENDENCY_INSTALL_MAX_RETRIES" \
"$DEPENDENCY_INSTALL_RETRY_INTERVAL" \
"uv 环境初始化失败,清华源重试 ${DEPENDENCY_INSTALL_MAX_RETRIES} 次后仍失败" \
"uv sync --frozen (清华源)" \
run_command_quiet_unless_verbose "$log_file" uv sync --frozen --group dev
}
configure_uv_tuna_index() {
local uv_config="$SCRIPT_DIR/uv.toml"
if [ -f "$uv_config" ] &&
grep -Fq 'name = "tsinghua"' "$uv_config" 2>/dev/null &&
grep -Fq "$PLANET_UV_TUNA_INDEX_URL" "$uv_config" 2>/dev/null &&
grep -Eq '^[[:space:]]*default[[:space:]]*=[[:space:]]*true' "$uv_config" 2>/dev/null; then
log_note "uv.toml 已配置清华源,直接重试 uv sync"
return 0
fi
if [ -f "$uv_config" ] && grep -Eq '^[[:space:]]*default[[:space:]]*=[[:space:]]*true' "$uv_config" 2>/dev/null; then
log_warn "uv.toml 已存在默认 index未覆盖用户配置"
log_note "如需手动切换清华源,可添加 [[index]] name=\"tsinghua\" 并设置 default=true"
return 0
fi
{
if [ -s "$uv_config" ]; then
printf "\n"
fi
printf '[[index]]\n'
printf 'name = "tsinghua"\n'
printf 'url = "%s"\n' "$PLANET_UV_TUNA_INDEX_URL"
printf 'default = true\n'
} >> "$uv_config"
log_note "已向 uv.toml 添加清华 PyPI 源: ${PLANET_UV_TUNA_INDEX_URL}"
}
install_system_package() {
local package_name="$1"
local log_file="$PLANET_STATE_DIR/system_dependency_install.log"
local -a sudo_prefix
sudo_prefix=()
if [ "$(id -u)" -ne 0 ]; then
if ! command -v sudo >/dev/null 2>&1; then
log_error "缺少 sudo无法自动安装系统依赖: ${package_name}"
return 1
fi
sudo_prefix=(sudo)
fi
: > "$log_file"
if command -v apt-get >/dev/null 2>&1; then
set_wait_detail "安装系统依赖 ${package_name} (apt)"
run_command_quiet_unless_verbose "$log_file" "${sudo_prefix[@]}" apt-get update &&
run_command_quiet_unless_verbose "$log_file" "${sudo_prefix[@]}" apt-get install -y "$package_name"
return $?
fi
if command -v dnf >/dev/null 2>&1; then
set_wait_detail "安装系统依赖 ${package_name} (dnf)"
run_command_quiet_unless_verbose "$log_file" "${sudo_prefix[@]}" dnf install -y "$package_name"
return $?
fi
if command -v yum >/dev/null 2>&1; then
set_wait_detail "安装系统依赖 ${package_name} (yum)"
run_command_quiet_unless_verbose "$log_file" "${sudo_prefix[@]}" yum install -y "$package_name"
return $?
fi
if command -v pacman >/dev/null 2>&1; then
set_wait_detail "安装系统依赖 ${package_name} (pacman)"
run_command_quiet_unless_verbose "$log_file" "${sudo_prefix[@]}" pacman -Sy --noconfirm "$package_name"
return $?
fi
if command -v apk >/dev/null 2>&1; then
set_wait_detail "安装系统依赖 ${package_name} (apk)"
run_command_quiet_unless_verbose "$log_file" "${sudo_prefix[@]}" apk add --no-cache "$package_name"
return $?
fi
if command -v brew >/dev/null 2>&1; then
set_wait_detail "安装系统依赖 ${package_name} (brew)"
run_command_quiet_unless_verbose "$log_file" brew install "$package_name"
return $?
fi
log_error "未识别系统包管理器,无法自动安装: ${package_name}"
return 1
}
ensure_system_command() {
local command_name="$1"
local package_name="$2"
local label="$3"
local log_file="$PLANET_STATE_DIR/system_dependency_install.log"
if command -v "$command_name" >/dev/null 2>&1; then
return 0
fi
log_warn "缺少 ${label},准备自动安装 ${package_name}"
if ! install_system_package "$package_name"; then
tail -20 "$log_file" 2>/dev/null || true
return 1
fi
if ! command -v "$command_name" >/dev/null 2>&1; then
log_error "${label} 安装后仍不可用,请手动安装 ${package_name}"
return 1
fi
}
format_wait_elapsed_seconds() {
local completed_intervals="${1:-0}"
local interval="${2:-1}"
@@ -1326,6 +1517,10 @@ install_bun_if_needed() {
return 0
fi
if ! ensure_system_command unzip unzip "unzip"; then
exit 1
fi
set_wait_detail "未找到 bun准备自动安装"
mkdir -p "$HOME/.bun"
@@ -1360,12 +1555,7 @@ ensure_uv_backend_deps() {
if [ ! -x "$SCRIPT_DIR/.venv/bin/python" ]; then
log_warn "未检测到 .venv正在执行 uv sync"
if ! run_with_retry \
"$DEPENDENCY_INSTALL_MAX_RETRIES" \
"$DEPENDENCY_INSTALL_RETRY_INTERVAL" \
"uv 环境初始化失败,已重试 ${DEPENDENCY_INSTALL_MAX_RETRIES}" \
"uv sync --frozen" \
run_command_quiet_unless_verbose "$log_file" uv sync --frozen --group dev; then
if ! run_uv_sync_with_mirror_fallback "$log_file"; then
tail -20 "$log_file" 2>/dev/null || true
exit 1
fi
@@ -1414,12 +1604,7 @@ sync_python_deps() {
: > "$log_file"
set_wait_detail "同步 Python 依赖"
if ! run_with_retry \
"$DEPENDENCY_INSTALL_MAX_RETRIES" \
"$DEPENDENCY_INSTALL_RETRY_INTERVAL" \
"uv 环境初始化失败,已重试 ${DEPENDENCY_INSTALL_MAX_RETRIES}" \
"uv sync --frozen" \
run_command_quiet_unless_verbose "$log_file" uv sync --frozen --group dev; then
if ! run_uv_sync_with_mirror_fallback "$log_file"; then
tail -20 "$log_file" 2>/dev/null || true
exit 1
fi
@@ -1712,15 +1897,26 @@ wait_for_frontend_ready() {
render_wait_spinner "$WAIT_SPINNER_MESSAGE" "$WAIT_SPINNER_STEP"
fi
if [ $(( tick % probe_every_ticks )) -eq 0 ] &&
http_ok "http://localhost:${frontend_port}"; then
if [ "$owns_wait_session" -eq 1 ]; then
stop_wait_session
finish_wait_spinner "前端已就绪"
if [ $(( tick % probe_every_ticks )) -eq 0 ]; then
if [ "${FRONTEND_LAN_HTTPS_ENABLED:-0}" -eq 1 ]; then
frontend_ready_ok() {
https_ok_insecure "https://localhost:${frontend_port}"
}
else
reset_wait_spinner_state
frontend_ready_ok() {
http_ok "http://localhost:${frontend_port}"
}
fi
if frontend_ready_ok; then
if [ "$owns_wait_session" -eq 1 ]; then
stop_wait_session
finish_wait_spinner "前端已就绪"
else
reset_wait_spinner_state
fi
return 0
fi
return 0
fi
sleep 0.125
@@ -1974,8 +2170,10 @@ start_backend_with_retry() {
fi
cd "$SCRIPT_DIR/backend"
: > "$BACKEND_LOG_FILE"
PYTHONPATH="$SCRIPT_DIR/backend" nohup uv run --frozen --project "$SCRIPT_DIR" python -m uvicorn app.main:app --host "$backend_bind_host" --port "$backend_port" --reload > "$BACKEND_LOG_FILE" 2>&1 &
BACKEND_PID=$!
BACKEND_PID="$(start_detached_command "$BACKEND_LOG_FILE" \
env PYTHONPATH="$SCRIPT_DIR/backend" \
uv run --frozen --project "$SCRIPT_DIR" python -m uvicorn app.main:app \
--host "$backend_bind_host" --port "$backend_port" --reload)"
write_pid_file "$BACKEND_PID_FILE" "$BACKEND_PID"
if [ "$VERBOSE" -eq 1 ]; then
WAIT_VERBOSE_LOG_FILE="$BACKEND_LOG_FILE"
@@ -2527,6 +2725,90 @@ report_backend_port_conflict_if_needed() {
}
# Frontend lifecycle helpers
cleanup_frontend_ca_server() {
local tracked_pid=""
if [ -f "$FRONTEND_CA_SERVER_PID_FILE" ]; then
if tracked_pid="$(read_pid_file "$FRONTEND_CA_SERVER_PID_FILE")"; then
terminate_process_tree TERM "$tracked_pid"
sleep 1
terminate_process_tree KILL "$tracked_pid"
fi
remove_pid_file "$FRONTEND_CA_SERVER_PID_FILE"
fi
pkill -f "python3 -m http.server ${FRONTEND_CA_DOWNLOAD_PORT} .*${FRONTEND_CERT_DIR}" 2>/dev/null || true
}
get_frontend_https_alt_names() {
local candidate=""
local alt_names="DNS:localhost,IP:127.0.0.1"
while read -r candidate; do
[ -z "$candidate" ] && continue
case ",${alt_names}," in
*",IP:${candidate},"*) continue ;;
esac
alt_names="${alt_names},IP:${candidate}"
done <<EOF
$(hostname -I 2>/dev/null | tr ' ' '\n')
EOF
printf "%s" "$alt_names"
}
ensure_frontend_https_certs() {
local common_name=""
local alt_names=""
local csr_file="$FRONTEND_CERT_DIR/planet-dev-server.csr"
command -v openssl >/dev/null 2>&1 || {
log_error "--allow-lan-https 需要 openssl 生成本地开发证书"
exit 1
}
mkdir -p "$FRONTEND_CERT_DIR"
chmod 700 "$FRONTEND_CERT_DIR" 2>/dev/null || true
if [ ! -f "$FRONTEND_CA_KEY_FILE" ] || [ ! -f "$FRONTEND_CA_CERT_FILE" ]; then
set_wait_detail "生成 Planet 本地开发 CA"
openssl req -x509 -newkey rsa:2048 -sha256 -days 3650 -nodes \
-keyout "$FRONTEND_CA_KEY_FILE" \
-out "$FRONTEND_CA_CERT_FILE" \
-subj "/CN=Planet Dev Local CA" >/dev/null 2>&1
fi
common_name="$(get_recommended_lan_ipv4 || printf "localhost")"
alt_names="$(get_frontend_https_alt_names)"
set_wait_detail "生成局域网 HTTPS 证书: ${common_name}"
openssl req -newkey rsa:2048 -nodes \
-keyout "$FRONTEND_HTTPS_KEY_FILE" \
-out "$csr_file" \
-subj "/CN=${common_name}" \
-addext "subjectAltName=${alt_names}" >/dev/null 2>&1
openssl x509 -req -in "$csr_file" \
-CA "$FRONTEND_CA_CERT_FILE" \
-CAkey "$FRONTEND_CA_KEY_FILE" \
-CAcreateserial \
-out "$FRONTEND_HTTPS_CERT_FILE" \
-days 825 -sha256 -copy_extensions copy >/dev/null 2>&1
chmod 600 "$FRONTEND_CA_KEY_FILE" "$FRONTEND_HTTPS_KEY_FILE" 2>/dev/null || true
chmod 644 "$FRONTEND_CA_CERT_FILE" "$FRONTEND_HTTPS_CERT_FILE" 2>/dev/null || true
}
start_frontend_ca_server() {
[ "${FRONTEND_LAN_HTTPS_ENABLED:-0}" -eq 1 ] || return 0
cleanup_frontend_ca_server
: > "$FRONTEND_CA_SERVER_LOG_FILE"
local ca_server_pid=""
ca_server_pid="$(start_detached_command "$FRONTEND_CA_SERVER_LOG_FILE" \
python3 -m http.server "$FRONTEND_CA_DOWNLOAD_PORT" \
--bind 0.0.0.0 \
--directory "$FRONTEND_CERT_DIR")"
write_pid_file "$FRONTEND_CA_SERVER_PID_FILE" "$ca_server_pid"
}
cleanup_frontend_processes() {
local frontend_port="${1:-$DEFAULT_FRONTEND_PORT}"
local tracked_pid=""
@@ -2543,6 +2825,7 @@ cleanup_frontend_processes() {
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
cleanup_frontend_ca_server
}
start_frontend_with_retry() {
@@ -2566,8 +2849,18 @@ start_frontend_with_retry() {
frontend_args+=(--host 0.0.0.0)
fi
frontend_args+=(--port "$frontend_port" --strictPort)
PLANET_BACKEND_PORT="$backend_port" nohup "$FRONTEND_RUNTIME_BIN" "${frontend_args[@]}" > "$FRONTEND_LOG_FILE" 2>&1 &
FRONTEND_PID=$!
if [ "${FRONTEND_LAN_HTTPS_ENABLED:-0}" -eq 1 ]; then
FRONTEND_PID="$(start_detached_command "$FRONTEND_LOG_FILE" \
env PLANET_BACKEND_PORT="$backend_port" \
PLANET_FRONTEND_HTTPS=1 \
PLANET_FRONTEND_HTTPS_KEY="$FRONTEND_HTTPS_KEY_FILE" \
PLANET_FRONTEND_HTTPS_CERT="$FRONTEND_HTTPS_CERT_FILE" \
"$FRONTEND_RUNTIME_BIN" "${frontend_args[@]}")"
else
FRONTEND_PID="$(start_detached_command "$FRONTEND_LOG_FILE" \
env PLANET_BACKEND_PORT="$backend_port" \
"$FRONTEND_RUNTIME_BIN" "${frontend_args[@]}")"
fi
write_pid_file "$FRONTEND_PID_FILE" "$FRONTEND_PID"
if wait_for_frontend_ready "$frontend_port" "$FRONTEND_PID" "$FRONTEND_LOG_FILE" "$FRONTEND_HEALTH_CHECK_ATTEMPTS" "$FRONTEND_HEALTH_CHECK_INTERVAL"; then
@@ -2601,12 +2894,19 @@ start_frontend_service() {
start_wait_session "检查前端依赖"
ensure_frontend_deps
if [ "${FRONTEND_LAN_HTTPS_ENABLED:-0}" -eq 1 ]; then
ensure_frontend_https_certs
fi
stop_wait_session
log_success "前端依赖已就绪"
start_wait_session "启动前端服务"
if [ "$frontend_lan_enabled" -eq 1 ]; then
set_wait_detail "启动 Vite 开发服务器(局域网开放)"
if [ "${FRONTEND_LAN_HTTPS_ENABLED:-0}" -eq 1 ]; then
set_wait_detail "启动 Vite HTTPS 开发服务器(局域网开放)"
else
set_wait_detail "启动 Vite 开发服务器(局域网开放)"
fi
else
set_wait_detail "启动 Vite 开发服务器"
fi
@@ -2620,6 +2920,7 @@ start_frontend_service() {
fi
exit 1
fi
start_frontend_ca_server
stop_wait_session
log_success "前端已就绪"
}
@@ -2639,6 +2940,7 @@ parse_service_args() {
MOTION_AGENT_DRY_RUN=0
DATABASE_REQUESTED=0
FRONTEND_LAN_ENABLED=0
FRONTEND_LAN_HTTPS_ENABLED=0
case "${PLANET_START_MOTION_AGENT:-0}" in
1|true|yes|on)
@@ -2727,6 +3029,11 @@ parse_service_args() {
FRONTEND_LAN_ENABLED=1
shift 1
;;
--allow-lan-https)
FRONTEND_LAN_ENABLED=1
FRONTEND_LAN_HTTPS_ENABLED=1
shift 1
;;
-v|--verbose)
VERBOSE=1
shift 1
@@ -2804,6 +3111,28 @@ print_http_health_status() {
fi
}
print_frontend_health_status() {
local frontend_port="$1"
local frontend_scheme="${2:-$(read_port_state_text_value FRONTEND_SCHEME http)}"
if [ "$frontend_scheme" = "https" ]; then
if https_ok_insecure "https://localhost:${frontend_port}"; then
echo -e "${DIM} 前端:${NC} ${GREEN}online${NC} ${DIM}(https://localhost:${frontend_port})${NC}"
else
echo -e "${DIM} 前端:${NC} ${RED}offline${NC} ${DIM}(https://localhost:${frontend_port})${NC}"
fi
return
fi
if http_ok "http://localhost:${frontend_port}"; then
echo -e "${DIM} 前端:${NC} ${GREEN}online${NC} ${DIM}(http://localhost:${frontend_port})${NC}"
elif https_ok_insecure "https://localhost:${frontend_port}"; then
echo -e "${DIM} 前端:${NC} ${GREEN}online${NC} ${DIM}(https://localhost:${frontend_port})${NC}"
else
echo -e "${DIM} 前端:${NC} ${RED}offline${NC}"
fi
}
print_process_health_status() {
local service_name="$1"
local pattern="$2"
@@ -2933,9 +3262,8 @@ start_motion_agent_service() {
motion_args+=(--dry-run)
fi
cd "$SCRIPT_DIR"
nohup "$SCRIPT_DIR/.venv/bin/python" "${motion_args[@]}" > "$MOTION_AGENT_LOG_FILE" 2>&1 &
MOTION_AGENT_PID=$!
MOTION_AGENT_PID="$(start_detached_command "$MOTION_AGENT_LOG_FILE" \
"$SCRIPT_DIR/.venv/bin/python" "${motion_args[@]}")"
write_pid_file "$MOTION_AGENT_PID_FILE" "$MOTION_AGENT_PID"
if wait_for_motion_agent_ready "$motion_agent_port" "$MOTION_AGENT_PID"; then
@@ -3049,11 +3377,13 @@ PY
planet_app_services_running() {
local backend_port=""
local frontend_port=""
local frontend_scheme=""
local ai_provider_port=""
local motion_agent_port=""
backend_port="$(read_port_state_value BACKEND_PORT "$DEFAULT_BACKEND_PORT")"
frontend_port="$(read_port_state_value FRONTEND_PORT "$DEFAULT_FRONTEND_PORT")"
frontend_scheme="$(read_port_state_text_value FRONTEND_SCHEME http)"
ai_provider_port="$(read_port_state_value AI_PROVIDER_PORT "$DEFAULT_AI_PROVIDER_PORT")"
motion_agent_port="$(read_port_state_value MOTION_AGENT_PORT "$DEFAULT_MOTION_AGENT_PORT")"
@@ -3061,8 +3391,14 @@ planet_app_services_running() {
return 0
fi
if http_ok "http://localhost:${frontend_port}"; then
return 0
if [ "$frontend_scheme" = "https" ]; then
if https_ok_insecure "https://localhost:${frontend_port}"; then
return 0
fi
else
if http_ok "http://localhost:${frontend_port}" || https_ok_insecure "https://localhost:${frontend_port}"; then
return 0
fi
fi
if http_ok "http://localhost:${ai_provider_port}/health"; then
@@ -3251,8 +3587,16 @@ prepare_allow_lan_public_ports() {
"后端" "$BACKEND_PORT" \
"前端" "$FRONTEND_PORT" \
"AI Provider" "$AI_PROVIDER_PORT"
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
if [ "${FRONTEND_LAN_HTTPS_ENABLED:-0}" -eq 1 ]; then
release_requested_ports "前端 CA 证书下载" "$FRONTEND_CA_DOWNLOAD_PORT"
fi
if [ "${FRONTEND_LAN_HTTPS_ENABLED:-0}" -eq 1 ]; then
request_windows_portproxy_cleanup_if_needed "$FRONTEND_PORT" "$BACKEND_PORT" "$AI_PROVIDER_PORT" "$FRONTEND_CA_DOWNLOAD_PORT" || true
request_windows_firewall_rules_if_needed "$FRONTEND_PORT" "$BACKEND_PORT" "$AI_PROVIDER_PORT" "$FRONTEND_CA_DOWNLOAD_PORT" || true
else
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
fi
}
verify_http_endpoint() {
@@ -3271,12 +3615,30 @@ verify_allow_lan_access() {
[ "$FRONTEND_LAN_ENABLED" -eq 1 ] || return 0
verify_http_endpoint "本机访问" "http://localhost:${FRONTEND_PORT}" || exit 1
if [ "${FRONTEND_LAN_HTTPS_ENABLED:-0}" -eq 1 ]; then
log_note "检查本机访问: https://localhost:${FRONTEND_PORT}"
https_ok_insecure "https://localhost:${FRONTEND_PORT}" || {
log_error "本机访问不可访问: https://localhost:${FRONTEND_PORT}"
exit 1
}
verify_http_endpoint "本机 CA 下载" "http://localhost:${FRONTEND_CA_DOWNLOAD_PORT}/planet-dev-ca.crt" || exit 1
else
verify_http_endpoint "本机访问" "http://localhost:${FRONTEND_PORT}" || exit 1
fi
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
if [ "${FRONTEND_LAN_HTTPS_ENABLED:-0}" -eq 1 ]; then
log_note "检查局域网访问: https://${lan_ip}:${FRONTEND_PORT}"
https_ok_insecure "https://${lan_ip}:${FRONTEND_PORT}" || {
log_error "局域网访问不可访问: https://${lan_ip}:${FRONTEND_PORT}"
exit 1
}
verify_http_endpoint "局域网 CA 下载" "http://${lan_ip}:${FRONTEND_CA_DOWNLOAD_PORT}/planet-dev-ca.crt" || exit 1
else
verify_http_endpoint "局域网访问" "http://${lan_ip}:${FRONTEND_PORT}" || exit 1
fi
verify_http_endpoint "局域网访问" "http://${lan_ip}:${BACKEND_PORT}/health" || exit 1
verify_http_endpoint "局域网访问" "http://${lan_ip}:${AI_PROVIDER_PORT}/health" || exit 1
else
@@ -3290,38 +3652,40 @@ start() {
cleanup_exit_containers
prepare_allow_lan_public_ports
START_RUN_ACTIVE=1
START_RUN_COMPLETED=0
STARTED_BACKEND_THIS_RUN=0
STARTED_FRONTEND_THIS_RUN=0
STARTED_MOTION_AGENT_THIS_RUN=0
typeset -g START_RUN_ACTIVE=1
typeset -g START_RUN_COMPLETED=0
typeset -g STARTED_BACKEND_THIS_RUN=0
typeset -g STARTED_FRONTEND_THIS_RUN=0
typeset -g STARTED_MOTION_AGENT_THIS_RUN=0
print_splash
start_backend_service "$BACKEND_PORT" "$BACKEND_PORT_REQUESTED" "$AI_PROVIDER_PORT"
STARTED_BACKEND_THIS_RUN=1
typeset -g STARTED_BACKEND_THIS_RUN=1
start_frontend_service "$FRONTEND_PORT" "$FRONTEND_PORT_REQUESTED" "$FRONTEND_LAN_ENABLED" "$BACKEND_PORT"
STARTED_FRONTEND_THIS_RUN=1
typeset -g STARTED_FRONTEND_THIS_RUN=1
if [ "$MOTION_AGENT_REQUESTED" -eq 1 ]; then
start_motion_agent_service "$MOTION_AGENT_PORT" "$MOTION_AGENT_DRY_RUN" "$FRONTEND_LAN_ENABLED"
STARTED_MOTION_AGENT_THIS_RUN=1
typeset -g STARTED_MOTION_AGENT_THIS_RUN=1
fi
write_port_state "$BACKEND_PORT" "$FRONTEND_PORT" "$AI_PROVIDER_PORT" "$MOTION_AGENT_PORT"
local frontend_scheme=""
frontend_scheme="$(current_frontend_scheme)"
write_port_state "$BACKEND_PORT" "$FRONTEND_PORT" "$AI_PROVIDER_PORT" "$MOTION_AGENT_PORT" "$frontend_scheme"
verify_allow_lan_access
START_RUN_COMPLETED=1
START_RUN_ACTIVE=0
typeset -g START_RUN_COMPLETED=1
typeset -g START_RUN_ACTIVE=0
log_success "启动完成"
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"
log_note "智能星球计划: ${frontend_scheme}://localhost:${FRONTEND_PORT}/earth"
log_note "智能星球仪表盘: ${frontend_scheme}://localhost:${FRONTEND_PORT}/admin"
log_note "AI Playground: ${frontend_scheme}://localhost:${FRONTEND_PORT}/playground"
log_note "智能星球文档: ${frontend_scheme}://localhost:${FRONTEND_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 ]; then
log_lan_access_notes "$FRONTEND_PORT" "$BACKEND_PORT"
log_lan_access_notes "$FRONTEND_PORT" "$BACKEND_PORT" "$AI_PROVIDER_PORT" "$frontend_scheme"
fi
}
@@ -3469,6 +3833,7 @@ restart() {
cleanup_exit_containers
local state_backend_port=""
local state_frontend_port=""
local state_frontend_scheme=""
local state_ai_provider_port=""
local state_motion_agent_port=""
@@ -3514,6 +3879,7 @@ restart() {
state_backend_port="$(read_port_state_value BACKEND_PORT "$DEFAULT_BACKEND_PORT")"
state_frontend_port="$(read_port_state_value FRONTEND_PORT "$DEFAULT_FRONTEND_PORT")"
state_frontend_scheme="$(read_port_state_text_value FRONTEND_SCHEME "$(current_frontend_scheme)")"
state_ai_provider_port="$(read_port_state_value AI_PROVIDER_PORT "$DEFAULT_AI_PROVIDER_PORT")"
state_motion_agent_port="$(read_port_state_value MOTION_AGENT_PORT "$DEFAULT_MOTION_AGENT_PORT")"
if [ "$BACKEND_PORT_REQUESTED" -eq 1 ]; then
@@ -3521,6 +3887,7 @@ restart() {
fi
if [ "$FRONTEND_PORT_REQUESTED" -eq 1 ]; then
state_frontend_port="$FRONTEND_PORT"
state_frontend_scheme="$(current_frontend_scheme)"
fi
if [ "$AI_PROVIDER_REQUESTED" -eq 1 ]; then
state_ai_provider_port="$AI_PROVIDER_PORT"
@@ -3528,7 +3895,9 @@ restart() {
if [ "$MOTION_AGENT_REQUESTED" -eq 1 ]; then
state_motion_agent_port="$MOTION_AGENT_PORT"
fi
write_port_state "$state_backend_port" "$state_frontend_port" "$state_ai_provider_port" "$state_motion_agent_port"
local frontend_scheme=""
frontend_scheme="$state_frontend_scheme"
write_port_state "$state_backend_port" "$state_frontend_port" "$state_ai_provider_port" "$state_motion_agent_port" "$frontend_scheme"
echo ""
log_success "重启完成"
@@ -3542,9 +3911,9 @@ restart() {
log_note "后端: http://localhost:${BACKEND_PORT}"
fi
if [ "$FRONTEND_PORT_REQUESTED" -eq 1 ]; then
log_note "前端: http://localhost:${FRONTEND_PORT}"
log_note "前端: ${frontend_scheme}://localhost:${FRONTEND_PORT}"
if [ "$FRONTEND_LAN_ENABLED" -eq 1 ]; then
log_lan_access_notes "$FRONTEND_PORT" "$BACKEND_PORT"
log_lan_access_notes "$FRONTEND_PORT" "$BACKEND_PORT" "$AI_PROVIDER_PORT" "$frontend_scheme"
fi
fi
if [ "$MOTION_AGENT_REQUESTED" -eq 1 ]; then
@@ -3568,7 +3937,7 @@ health() {
echo -e "${DIM}·${NC} ${BLUE}view${NC} ${WHITE}服务状态${NC}"
print_http_health_status "后端" "http://localhost:${backend_port}/health"
print_http_health_status "AI Provider" "http://localhost:${ai_provider_port}/health"
print_http_health_status "前端" "http://localhost:${frontend_port}"
print_frontend_health_status "$frontend_port"
print_process_health_status "Motion Agent" "python.*-m motion_agent" "$MOTION_AGENT_PID_FILE"
}