#!/bin/bash set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$SCRIPT_DIR" RED='\033[38;5;203m' GREEN='\033[38;5;114m' YELLOW='\033[38;5;221m' BLUE='\033[38;5;111m' CYAN='\033[38;5;117m' WHITE='\033[1;37m' DIM='\033[2m' NC='\033[0m' WAIT_SPINNER_FRAMES=( '⠉⠁' '⠈⠉' '⠀⠙' '⠀⠸' '⠀⢰' '⠀⣠' '⢀⣀' '⣀⡀' '⣄⠀' '⡆⠀' '⠇⠀' '⠋⠀' ) WAIT_SPINNER_STEP=0 WAIT_SPINNER_TICKS_PER_SECOND=8 BACKEND_MAX_RETRIES="${BACKEND_MAX_RETRIES:-3}" BACKEND_HEALTH_CHECK_ATTEMPTS="${BACKEND_HEALTH_CHECK_ATTEMPTS:-10}" BACKEND_HEALTH_CHECK_INTERVAL="${BACKEND_HEALTH_CHECK_INTERVAL:-2}" DEPENDENCY_INSTALL_MAX_RETRIES="${DEPENDENCY_INSTALL_MAX_RETRIES:-3}" DEPENDENCY_INSTALL_RETRY_INTERVAL="${DEPENDENCY_INSTALL_RETRY_INTERVAL:-5}" AI_PROVIDER_HEALTH_CHECK_ATTEMPTS="${AI_PROVIDER_HEALTH_CHECK_ATTEMPTS:-10}" AI_PROVIDER_HEALTH_CHECK_INTERVAL="${AI_PROVIDER_HEALTH_CHECK_INTERVAL:-2}" AI_PROVIDER_START_MAX_RETRIES="${AI_PROVIDER_START_MAX_RETRIES:-3}" AI_PROVIDER_RETRY_INTERVAL="${AI_PROVIDER_RETRY_INTERVAL:-5}" DATABASE_START_MAX_RETRIES="${DATABASE_START_MAX_RETRIES:-3}" DATABASE_RETRY_INTERVAL="${DATABASE_RETRY_INTERVAL:-5}" FRONTEND_MAX_RETRIES="${FRONTEND_MAX_RETRIES:-3}" FRONTEND_HEALTH_CHECK_ATTEMPTS="${FRONTEND_HEALTH_CHECK_ATTEMPTS:-10}" FRONTEND_HEALTH_CHECK_INTERVAL="${FRONTEND_HEALTH_CHECK_INTERVAL:-2}" DEFAULT_BACKEND_PORT="${DEFAULT_BACKEND_PORT:-8000}" DEFAULT_FRONTEND_PORT="${DEFAULT_FRONTEND_PORT:-3000}" DEFAULT_AI_PROVIDER_PORT="${DEFAULT_AI_PROVIDER_PORT:-8010}" compose_up() { if docker compose version >/dev/null 2>&1; then docker compose "$@" else docker-compose "$@" fi } render_wait_spinner() { local message="$1" local step="${2:-1}" local frame_index=$(( (step - 1) % ${#WAIT_SPINNER_FRAMES[@]} )) local frame="${WAIT_SPINNER_FRAMES[$frame_index]}" printf "\r${DIM}·${NC} ${CYAN}%s${NC} ${WHITE}%s${NC}\033[K" "$frame" "$message" } animate_wait_spinner() { local message="$1" local duration="${2:-1}" local ticks=$(( duration * WAIT_SPINNER_TICKS_PER_SECOND )) local tick=0 if [ "$ticks" -lt 1 ]; then ticks=1 fi while [ "$tick" -lt "$ticks" ]; do WAIT_SPINNER_STEP=$((WAIT_SPINNER_STEP + 1)) render_wait_spinner "$message" "$WAIT_SPINNER_STEP" sleep 0.125 tick=$((tick + 1)) done } clear_wait_spinner() { printf "\r\033[K" } finish_wait_spinner() { log_line "done" "$GREEN" "$1" } log_line() { local label="$1" local color="$2" local message="$3" clear_wait_spinner printf "${DIM}·${NC} ${color}%-4s${NC} ${WHITE}%s${NC}\n" "$label" "$message" } log_step() { log_line "run" "$BLUE" "$1" } log_warn() { log_line "warn" "$YELLOW" "$1" } log_error() { log_line "fail" "$RED" "$1" } log_note() { printf "${DIM} %s${NC}\n" "$1" } log_success() { log_line "done" "$GREEN" "$1" } print_splash() { clear_wait_spinner printf "%b" "$CYAN" cat <<'EOF' ___ _ _ _ _ _ |_ _|_ __ | |_ ___| | (_) __ _ ___ _ __ | |_ | || '_ \| __/ _ \ | | |/ _` |/ _ \ '_ \| __| | || | | | || __/ | | | (_| | __/ | | | |_ |___|_| |_|\__\___|_|_|_|\__, |\___|_| |_|\__| |___/ EOF printf "%b\n" "$NC" printf "%b" "$BLUE" cat <<'EOF' ____ _ _ | _ \| | __ _ _ __ ___ | |_ | |_) | |/ _` | '_ \ / _ \| __| | __/| | (_| | | | | __/| |_ |_| |_|\__,_|_| |_|\___| \__| EOF printf "%b\n" "$NC" printf "%b" "$WHITE" cat <<'EOF' ____ _ | _ \| | __ _ _ __ | |_) | |/ _` | '_ \ | __/| | (_| | | | | |_| |_|\__,_|_| |_| EOF printf "%b\n\n" "$NC" } run_with_retry() { local max_retries="$1" local interval="$2" local failure_message="$3" local retry_label="$4" shift 4 local attempt=1 while [ "$attempt" -le "$max_retries" ]; do if "$@"; then return 0 fi if [ "$attempt" -eq "$max_retries" ]; then clear_wait_spinner log_error "${failure_message}" return 1 fi animate_wait_spinner "${retry_label} 第 ${attempt}/${max_retries} 次失败,${interval} 秒后重试" "$interval" attempt=$((attempt + 1)) done return 1 } ensure_uv_backend_deps() { log_step "检查后端 uv 环境" if ! command -v uv >/dev/null 2>&1; then log_error "未找到 uv,请先安装 uv 并加入 PATH" exit 1 fi cd "$SCRIPT_DIR" 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" \ uv sync --group dev; then exit 1 fi fi if [ ! -x "$SCRIPT_DIR/.venv/bin/python" ]; then log_error "uv 环境初始化失败,未找到 .venv/bin/python" exit 1 fi } ensure_frontend_deps() { log_step "检查前端依赖" if ! command -v bun >/dev/null 2>&1; then log_error "未找到 bun,请先安装或加载 bun 到 PATH" exit 1 fi cd "$SCRIPT_DIR/frontend" if [ ! -x "$SCRIPT_DIR/frontend/node_modules/.bin/vite" ]; then log_warn "前端依赖缺失,正在执行 bun install" if ! run_with_retry \ "$DEPENDENCY_INSTALL_MAX_RETRIES" \ "$DEPENDENCY_INSTALL_RETRY_INTERVAL" \ "前端依赖安装失败,已重试 ${DEPENDENCY_INSTALL_MAX_RETRIES} 次" \ "bun install" \ bun install; then exit 1 fi fi if [ ! -x "$SCRIPT_DIR/frontend/node_modules/.bin/vite" ]; then log_error "前端依赖安装失败,未找到 vite" exit 1 fi } wait_for_http() { local url="$1" local attempts="$2" local interval="$3" local service_name="$4" local attempt=1 while [ "$attempt" -le "$attempts" ]; do if curl -s "$url" > /dev/null 2>&1; then finish_wait_spinner "${service_name}已就绪" return 0 fi animate_wait_spinner "等待${service_name}就绪" "$interval" attempt=$((attempt + 1)) done clear_wait_spinner return 1 } wait_for_container_health() { local container_name="$1" local attempts="$2" local interval="$3" local service_name="$4" local attempt=1 local status while [ "$attempt" -le "$attempts" ]; do status="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$container_name" 2>/dev/null || true)" if [ "$status" = "healthy" ] || [ "$status" = "running" ]; then finish_wait_spinner "${service_name}容器健康检查已通过" return 0 fi animate_wait_spinner "等待${service_name}容器健康检查通过" "$interval" attempt=$((attempt + 1)) done clear_wait_spinner return 1 } wait_for_database_health() { wait_for_container_health "planet_postgres" "$AI_PROVIDER_HEALTH_CHECK_ATTEMPTS" "$DATABASE_RETRY_INTERVAL" "PostgreSQL" && wait_for_container_health "planet_redis" "$AI_PROVIDER_HEALTH_CHECK_ATTEMPTS" "$DATABASE_RETRY_INTERVAL" "Redis" } wait_for_postgres_health() { wait_for_container_health "planet_postgres" "$AI_PROVIDER_HEALTH_CHECK_ATTEMPTS" "$DATABASE_RETRY_INTERVAL" "PostgreSQL" } start_database_services() { docker start planet_postgres planet_redis >/dev/null 2>&1 || compose_up up -d postgres redis >/dev/null 2>&1 } restart_database_services() { docker restart planet_postgres planet_redis >/dev/null 2>&1 || compose_up up -d postgres redis >/dev/null 2>&1 } start_postgres_service() { docker start planet_postgres >/dev/null 2>&1 || compose_up up -d postgres >/dev/null 2>&1 } start_backend_with_retry() { local backend_port="$1" local retry=1 while [ "$retry" -le "$BACKEND_MAX_RETRIES" ]; do pkill -f "uvicorn" 2>/dev/null || true cd "$SCRIPT_DIR/backend" 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 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 animate_wait_spinner "后端第 ${retry}/${BACKEND_MAX_RETRIES} 次启动未就绪,准备重试" "$BACKEND_HEALTH_CHECK_INTERVAL" retry=$((retry + 1)) done clear_wait_spinner return 1 } start_ai_provider_service() { local ai_provider_port="${1:-$DEFAULT_AI_PROVIDER_PORT}" local retry=1 log_step "启动 AI Provider" while [ "$retry" -le "$AI_PROVIDER_START_MAX_RETRIES" ]; do if docker start planet_aiprovider >/dev/null 2>&1 || compose_up up -d aiprovider >/dev/null 2>&1; then if wait_for_container_health "planet_aiprovider" "$AI_PROVIDER_HEALTH_CHECK_ATTEMPTS" "$AI_PROVIDER_HEALTH_CHECK_INTERVAL" "AI Provider" && wait_for_http "http://localhost:${ai_provider_port}/health" "$AI_PROVIDER_HEALTH_CHECK_ATTEMPTS" "$AI_PROVIDER_HEALTH_CHECK_INTERVAL" "AI Provider"; then return 0 fi fi if [ "$retry" -eq "$AI_PROVIDER_START_MAX_RETRIES" ]; then clear_wait_spinner log_error "AI Provider 启动失败,已重试 ${AI_PROVIDER_START_MAX_RETRIES} 次" docker logs --tail 20 planet_aiprovider 2>/dev/null || true exit 1 fi docker restart planet_aiprovider >/dev/null 2>&1 || true animate_wait_spinner "AI Provider 第 ${retry}/${AI_PROVIDER_START_MAX_RETRIES} 次启动后仍未健康,正在重启容器" "$AI_PROVIDER_RETRY_INTERVAL" retry=$((retry + 1)) done clear_wait_spinner log_error "AI Provider 启动失败" docker logs --tail 20 planet_aiprovider 2>/dev/null || true exit 1 } start_frontend_with_retry() { local frontend_port="$1" local retry=1 while [ "$retry" -le "$FRONTEND_MAX_RETRIES" ]; do pkill -f "vite" 2>/dev/null || true pkill -f "bun run dev" 2>/dev/null || true cd "$SCRIPT_DIR/frontend" nohup bun run dev --port "$frontend_port" > /tmp/planet_frontend.log 2>&1 & FRONTEND_PID=$! if wait_for_http "http://localhost:${frontend_port}" "$FRONTEND_HEALTH_CHECK_ATTEMPTS" "$FRONTEND_HEALTH_CHECK_INTERVAL" "前端"; then return 0 fi kill "$FRONTEND_PID" 2>/dev/null || true animate_wait_spinner "前端第 ${retry}/${FRONTEND_MAX_RETRIES} 次启动未就绪,准备重试" "$FRONTEND_HEALTH_CHECK_INTERVAL" retry=$((retry + 1)) done clear_wait_spinner return 1 } ensure_database_services_healthy() { local retry=1 while [ "$retry" -le "$DATABASE_START_MAX_RETRIES" ]; do if start_database_services && wait_for_database_health; then return 0 fi if [ "$retry" -eq "$DATABASE_START_MAX_RETRIES" ]; then clear_wait_spinner log_error "数据库启动失败,已重试 ${DATABASE_START_MAX_RETRIES} 次" docker logs --tail 20 planet_postgres 2>/dev/null || true docker logs --tail 20 planet_redis 2>/dev/null || true exit 1 fi docker restart planet_postgres planet_redis >/dev/null 2>&1 || true animate_wait_spinner "数据库第 ${retry}/${DATABASE_START_MAX_RETRIES} 次启动后仍未健康,正在重启容器" "$DATABASE_RETRY_INTERVAL" retry=$((retry + 1)) done } ensure_postgres_service_healthy() { local retry=1 while [ "$retry" -le "$DATABASE_START_MAX_RETRIES" ]; do if start_postgres_service && wait_for_postgres_health; then return 0 fi if [ "$retry" -eq "$DATABASE_START_MAX_RETRIES" ]; then clear_wait_spinner log_error "PostgreSQL 启动失败,已重试 ${DATABASE_START_MAX_RETRIES} 次" docker logs --tail 20 planet_postgres 2>/dev/null || true exit 1 fi docker restart planet_postgres >/dev/null 2>&1 || true animate_wait_spinner "PostgreSQL 第 ${retry}/${DATABASE_START_MAX_RETRIES} 次启动后仍未健康,正在重启容器" "$DATABASE_RETRY_INTERVAL" retry=$((retry + 1)) done } restart_database_service() { local retry=1 log_step "重启数据库" while [ "$retry" -le "$DATABASE_START_MAX_RETRIES" ]; do if restart_database_services && wait_for_database_health; then sleep 3 return 0 fi if [ "$retry" -eq "$DATABASE_START_MAX_RETRIES" ]; then clear_wait_spinner log_error "数据库重启失败,已重试 ${DATABASE_START_MAX_RETRIES} 次" docker logs --tail 20 planet_postgres 2>/dev/null || true docker logs --tail 20 planet_redis 2>/dev/null || true exit 1 fi animate_wait_spinner "数据库第 ${retry}/${DATABASE_START_MAX_RETRIES} 次重启后仍未健康,继续重试" "$DATABASE_RETRY_INTERVAL" retry=$((retry + 1)) done } start_backend_service() { local backend_port="$1" local backend_port_requested="$2" local ai_provider_port="${3:-$DEFAULT_AI_PROVIDER_PORT}" log_step "启动数据库" ensure_database_services_healthy sleep 3 start_ai_provider_service "$ai_provider_port" if [ "$backend_port_requested" -eq 1 ]; then kill_port_if_requested "$backend_port" "后端" fi log_step "启动后端" ensure_uv_backend_deps if ! start_backend_with_retry "$backend_port"; then log_error "后端启动失败,已重试 ${BACKEND_MAX_RETRIES} 次" tail -10 /tmp/planet_backend.log exit 1 fi } start_frontend_service() { local frontend_port="$1" local frontend_port_requested="$2" if [ "$frontend_port_requested" -eq 1 ]; then kill_port_if_requested "$frontend_port" "前端" fi log_step "启动前端" ensure_frontend_deps if ! start_frontend_with_retry "$frontend_port"; then log_error "前端启动失败,已重试 ${FRONTEND_MAX_RETRIES} 次" tail -10 /tmp/planet_frontend.log exit 1 fi } validate_port() { local port="$1" if ! [[ "$port" =~ ^[0-9]+$ ]] || [ "$port" -lt 1 ] || [ "$port" -gt 65535 ]; then log_error "非法端口: ${port}" exit 1 fi } kill_port_if_requested() { local port="$1" local service_name="$2" log_warn "检测 ${service_name} 端口 ${port} 占用" if command -v fuser >/dev/null 2>&1 && fuser "${port}/tcp" >/dev/null 2>&1; then log_step "发现端口 ${port} 占用,正在终止" fuser -k "${port}/tcp" >/dev/null 2>&1 || true sleep 1 return 0 fi if command -v lsof >/dev/null 2>&1; then local pids pids="$(lsof -ti tcp:"${port}" 2>/dev/null || true)" if [ -n "$pids" ]; then log_step "发现端口 ${port} 占用,正在终止" kill $pids 2>/dev/null || true sleep 1 return 0 fi fi log_success "端口 ${port} 未被占用" } parse_service_args() { BACKEND_PORT="$DEFAULT_BACKEND_PORT" FRONTEND_PORT="$DEFAULT_FRONTEND_PORT" AI_PROVIDER_PORT="$DEFAULT_AI_PROVIDER_PORT" BACKEND_PORT_REQUESTED=0 FRONTEND_PORT_REQUESTED=0 AI_PROVIDER_REQUESTED=0 DATABASE_REQUESTED=0 while [ "$#" -gt 0 ]; do case "$1" in -b|--backend-port) BACKEND_PORT_REQUESTED=1 if [ -n "$2" ] && [[ "$2" =~ ^[0-9]+$ ]]; then BACKEND_PORT="$2" shift 2 else shift 1 fi ;; -f|--frontend-port) FRONTEND_PORT_REQUESTED=1 if [ -n "$2" ] && [[ "$2" =~ ^[0-9]+$ ]]; then FRONTEND_PORT="$2" shift 2 else shift 1 fi ;; -a|--ai-provider-port) AI_PROVIDER_REQUESTED=1 if [ -n "$2" ] && [[ "$2" =~ ^[0-9]+$ ]]; then AI_PROVIDER_PORT="$2" shift 2 else shift 1 fi ;; -d|--database) DATABASE_REQUESTED=1 shift 1 ;; *) log_error "未知参数: $1" exit 1 ;; esac done validate_port "$BACKEND_PORT" validate_port "$FRONTEND_PORT" validate_port "$AI_PROVIDER_PORT" } cleanup_exit_containers() { local exit_containers exit_containers="$(docker ps -a --filter status=exited -q 2>/dev/null || true)" if [ -n "$exit_containers" ]; then log_step "清理残留 Exit 容器" echo "$exit_containers" | xargs -r docker rm -f >/dev/null 2>&1 || true log_success "残留容器已清理" fi } stop_backend_service() { pkill -f "uvicorn" 2>/dev/null || true } stop_ai_provider_service() { docker stop planet_aiprovider 2>/dev/null || true } stop_frontend_service() { pkill -f "vite" 2>/dev/null || true pkill -f "bun run dev" 2>/dev/null || true } create_user() { local username local password local password_confirm local email local generated_email local role="viewer" local is_admin ensure_uv_backend_deps log_step "启动数据库" ensure_postgres_service_healthy sleep 2 log_step "创建用户" read -r -p "用户名: " username if [ -z "$username" ]; then log_error "用户名不能为空" exit 1 fi generated_email="${username}@planet.local" read -r -p "邮箱(留空自动使用 ${generated_email}): " email if [ -z "$email" ]; then email="$generated_email" fi read -r -s -p "密码: " password echo "" read -r -s -p "确认密码: " password_confirm echo "" if [ -z "$password" ]; then log_error "密码不能为空" exit 1 fi if [ "${#password}" -lt 8 ]; then log_error "密码长度不能少于 8 位" exit 1 fi if [ "$password" != "$password_confirm" ]; then log_error "两次输入的密码不一致" exit 1 fi read -r -p "是否为管理员? (Y/N): " is_admin if [[ "$is_admin" =~ ^[Yy]$ ]]; then role="super_admin" fi cd "$SCRIPT_DIR/backend" PLANET_CREATEUSER_USERNAME="$username" \ PLANET_CREATEUSER_PASSWORD="$password" \ PLANET_CREATEUSER_EMAIL="$email" \ PLANET_CREATEUSER_ROLE="$role" \ PYTHONPATH="$SCRIPT_DIR/backend" \ "$SCRIPT_DIR/.venv/bin/python" - <<'PY' import asyncio import os from sqlalchemy import text from app.core.security import get_password_hash from app.db.session import async_session_factory from app.models.user import User async def main(): username = os.environ["PLANET_CREATEUSER_USERNAME"].strip() password = os.environ["PLANET_CREATEUSER_PASSWORD"] email = os.environ["PLANET_CREATEUSER_EMAIL"].strip() role = os.environ["PLANET_CREATEUSER_ROLE"].strip() async with async_session_factory() as session: result = await session.execute( text("SELECT id FROM users WHERE username = :username OR email = :email"), {"username": username, "email": email}, ) if result.fetchone(): raise SystemExit("用户名已存在") user = User( username=username, email=email, password_hash=get_password_hash(password), role=role, is_active=True, ) session.add(user) await session.commit() asyncio.run(main()) PY log_success "用户创建成功" log_note "用户名: ${username}" log_note "角色: ${role}" log_note "邮箱: ${email}" } start() { parse_service_args "$@" cleanup_exit_containers print_splash log_step "启动智能星球计划" start_backend_service "$BACKEND_PORT" "$BACKEND_PORT_REQUESTED" "$AI_PROVIDER_PORT" start_frontend_service "$FRONTEND_PORT" "$FRONTEND_PORT_REQUESTED" echo "" log_success "启动完成" log_note "前端: http://localhost:${FRONTEND_PORT}" log_note "后端: http://localhost:${BACKEND_PORT}" log_note "AI Provider: http://localhost:${AI_PROVIDER_PORT}" } stop() { log_warn "停止服务" stop_backend_service stop_ai_provider_service stop_frontend_service docker stop planet_postgres planet_redis 2>/dev/null || true log_success "已停止" } restart() { parse_service_args "$@" cleanup_exit_containers if [ "$BACKEND_PORT_REQUESTED" -eq 0 ] && [ "$FRONTEND_PORT_REQUESTED" -eq 0 ] && [ "$AI_PROVIDER_REQUESTED" -eq 0 ] && [ "$DATABASE_REQUESTED" -eq 0 ]; then stop sleep 1 start return 0 fi log_warn "按需重启服务" if [ "$DATABASE_REQUESTED" -eq 1 ]; then restart_database_service fi if [ "$AI_PROVIDER_REQUESTED" -eq 1 ]; then stop_ai_provider_service sleep 1 start_ai_provider_service "$AI_PROVIDER_PORT" fi if [ "$BACKEND_PORT_REQUESTED" -eq 1 ]; then stop_backend_service sleep 1 start_backend_service "$BACKEND_PORT" 1 "$AI_PROVIDER_PORT" fi if [ "$FRONTEND_PORT_REQUESTED" -eq 1 ]; then stop_frontend_service sleep 1 start_frontend_service "$FRONTEND_PORT" 1 fi echo "" log_success "重启完成" if [ "$DATABASE_REQUESTED" -eq 1 ]; then log_note "数据库: planet_postgres, planet_redis" fi if [ "$AI_PROVIDER_REQUESTED" -eq 1 ]; then log_note "AI Provider: http://localhost:${AI_PROVIDER_PORT}" fi if [ "$BACKEND_PORT_REQUESTED" -eq 1 ]; then log_note "后端: http://localhost:${BACKEND_PORT}" fi if [ "$FRONTEND_PORT_REQUESTED" -eq 1 ]; then log_note "前端: http://localhost:${FRONTEND_PORT}" fi } health() { echo -e "${DIM}·${NC} ${BLUE}view${NC} ${WHITE}容器状态${NC}" docker ps --filter "name=planet_" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" echo "" echo -e "${DIM}·${NC} ${BLUE}view${NC} ${WHITE}服务状态${NC}" if curl -s http://localhost:8000/health > /dev/null 2>&1; then echo -e "${DIM} 后端:${NC} ${GREEN}online${NC}" else echo -e "${DIM} 后端:${NC} ${RED}offline${NC}" fi if curl -s "http://localhost:${DEFAULT_AI_PROVIDER_PORT}/health" > /dev/null 2>&1; then echo -e "${DIM} AI Provider:${NC} ${GREEN}online${NC}" else echo -e "${DIM} AI Provider:${NC} ${RED}offline${NC}" fi if curl -s http://localhost:3000 > /dev/null 2>&1; then echo -e "${DIM} 前端:${NC} ${GREEN}online${NC}" else echo -e "${DIM} 前端:${NC} ${RED}offline${NC}" fi } log() { case "$1" in -f|--frontend) echo "📝 前端日志 (Ctrl+C 退出):" tail -f /tmp/planet_frontend.log ;; -b|--backend) echo "📝 后端日志 (Ctrl+C 退出):" tail -f /tmp/planet_backend.log ;; -a|--ai-provider) echo "📝 AI Provider 日志 (Ctrl+C 退出):" docker logs -f planet_aiprovider ;; *) echo "📝 最近日志:" echo "--- 后端 ---" tail -20 /tmp/planet_backend.log 2>/dev/null || echo "无日志" echo "--- AI Provider ---" docker logs --tail 20 planet_aiprovider 2>/dev/null || echo "无日志" echo "--- 前端 ---" tail -20 /tmp/planet_frontend.log 2>/dev/null || echo "无日志" ;; esac } case "$1" in start) shift start "$@" ;; stop) stop ;; restart) shift restart "$@" ;; createuser) create_user ;; health) health ;; log) log "$2" ;; *) echo "用法: ./planet.sh {start|stop|restart|createuser|health|log}" echo "" echo "命令:" echo " start 启动服务,可选: -b <后端端口> -f <前端端口> -a " echo " stop 停止服务" echo " restart 重启服务,可选: -b [后端端口] -f [前端端口] -a [AI Provider 端口] -d" echo " createuser 交互创建用户" echo " health 检查健康状态" echo " log 查看日志" echo " log -f 查看前端日志" echo " log -b 查看后端日志" echo " log -a 查看 AI Provider 日志" ;; esac