#!/usr/bin/env bash # EchoChat 一键启动脚本 # 用法: # ./scripts/start.sh # 启动应用全部(Docker 中间件 + Go/前端/管理端,本地进程跑) # ./scripts/start.sh --no-docker # 仅启动应用层(假设 Docker 容器已在运行) # ./scripts/start.sh backend # 仅启动指定服务(backend|frontend|admin|media|docker) # ./scripts/start.sh full # Phase 2e-2 Task 14:docker compose 全栈启动(含 media-server 容器) # # 会读 deploy/.env(如不存在提示 copy example),公网则用 deploy-public.sh # 注意:不使用 set -e,避免单个服务启动失败中断其他服务; # 失败时由 spawn_bg 内部打印日志末尾辅助排障 set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" RUN_DIR="$ROOT_DIR/.run" LOG_DIR="$RUN_DIR/logs" DEPLOY_DIR="$ROOT_DIR/deploy" mkdir -p "$RUN_DIR" "$LOG_DIR" COLOR_GREEN='\033[0;32m' COLOR_YELLOW='\033[0;33m' COLOR_RED='\033[0;31m' COLOR_CYAN='\033[0;36m' COLOR_RESET='\033[0m' log_info() { printf "${COLOR_CYAN}[INFO]${COLOR_RESET} %s\n" "$*"; } log_ok() { printf "${COLOR_GREEN}[OK]${COLOR_RESET} %s\n" "$*"; } log_warn() { printf "${COLOR_YELLOW}[WARN]${COLOR_RESET} %s\n" "$*"; } log_err() { printf "${COLOR_RED}[ERR]${COLOR_RESET} %s\n" "$*"; } # 判断端口是否被占用 port_in_use() { lsof -iTCP:"$1" -sTCP:LISTEN -n -P >/dev/null 2>&1 } # 将命令以后台方式启动,并记录 PID 与日志 # 启动失败时自动打印日志末尾帮助排障,但不中断其他服务的启动 spawn_bg() { local name="$1"; shift local workdir="$1"; shift local log_file="$LOG_DIR/$name.log" local pid_file="$RUN_DIR/$name.pid" ( cd "$workdir" nohup "$@" >"$log_file" 2>&1 & echo $! >"$pid_file" ) sleep 1 local pid pid="$(cat "$pid_file" 2>/dev/null || echo '')" if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then log_ok "$name 已启动 (PID=$pid) 日志: $log_file" return 0 fi log_err "$name 启动失败,日志末尾:" if [[ -f "$log_file" ]]; then echo "------------------------------------------------------------" tail -n 20 "$log_file" echo "------------------------------------------------------------" fi log_err "完整日志: $log_file" return 1 } start_docker() { log_info "启动 Docker 中间件(Postgres/Redis/MinIO)..." cd "$ROOT_DIR/deploy" docker-compose -f docker-compose.dev.yml up -d postgres redis minio log_ok "Docker 中间件已启动" } # 确保 deploy/.env 存在:若缺失则从 .env.local.example 拷贝(等价于本机 Demo 默认值) # 公网部署走 scripts/deploy-public.sh 单独校验,不经此函数 ensure_env_file() { local env_file="$DEPLOY_DIR/.env" if [[ -f "$env_file" ]]; then return 0 fi local tmpl="$DEPLOY_DIR/.env.local.example" if [[ -f "$tmpl" ]]; then cp "$tmpl" "$env_file" log_warn "deploy/.env 不存在,已从 .env.local.example 复制(本机 Demo 默认值)" else log_warn "deploy/.env 和 .env.local.example 都不存在,compose 将使用内置默认值" fi } # Phase 2e-2 Task 14:docker compose 全栈启动 # 默认只拉起非 public profile 的服务(5 个:postgres/redis/minio/go-service/media-server) # coturn 走 public profile,需要用 scripts/deploy-public.sh start_full() { ensure_env_file log_info "docker-compose 全栈启动(postgres + redis + minio + go-service + media-server)..." cd "$DEPLOY_DIR" if ! docker-compose -f docker-compose.dev.yml up -d --build; then log_err "docker-compose 启动失败,请检查上方错误输出" return 1 fi log_ok "全栈服务已发起启动,等待 healthcheck..." # 等待最多 180 秒让 media-server 进入 healthy local waited=0 while (( waited < 180 )); do local health health="$(docker inspect -f '{{.State.Health.Status}}' echochat-media-server 2>/dev/null || echo 'starting')" case "$health" in healthy) log_ok "media-server 已 healthy($waited 秒)" break ;; unhealthy) log_err "media-server unhealthy,请查看日志:docker logs echochat-media-server" return 1 ;; esac sleep 3 waited=$((waited + 3)) done if (( waited >= 180 )); then log_warn "等待 media-server healthy 超过 180 秒,可能仍在 mediasoup worker 初始化中,请手动验证" fi } start_backend() { if port_in_use 8085; then log_warn "端口 8085 已被占用,跳过 Go 后端启动" return 0 fi log_info "启动 Go 后端 (port 8085)..." spawn_bg "backend" "$ROOT_DIR/backend/go-service" go run cmd/server/main.go } start_frontend() { if port_in_use 5173; then log_warn "端口 5173 已被占用,跳过前台启动" return 0 fi log_info "启动前台用户端 (port 5173)..." spawn_bg "frontend" "$ROOT_DIR/frontend" npm run dev:h5 } start_admin() { if port_in_use 3100; then log_warn "端口 3100 已被占用,跳过管理端启动" return 0 fi log_info "启动后台管理端 (port 3100)..." spawn_bg "admin" "$ROOT_DIR/admin" npm run dev } # 启动 Node 媒体服务器(mediasoup SFU,默认端口 3300) # 首次启动若缺少 .env,则从 .env.example 拷贝一份,避免 config 校验失败 start_media() { local media_dir="$ROOT_DIR/media-server" if [[ ! -d "$media_dir" ]]; then log_warn "media-server 目录不存在,跳过" return 0 fi if port_in_use 3300; then log_warn "端口 3300 已被占用,跳过媒体服务器启动" return 0 fi if [[ ! -f "$media_dir/.env" && -f "$media_dir/.env.example" ]]; then log_info "media-server/.env 不存在,从 .env.example 复制" cp "$media_dir/.env.example" "$media_dir/.env" fi if [[ ! -d "$media_dir/node_modules" ]]; then log_warn "media-server 依赖未安装,请先在 media-server 目录执行 npm install" return 1 fi log_info "启动媒体服务器 (port 3300)..." spawn_bg "media" "$media_dir" npm run dev } # 等待 vite/dev server 日志里出现 Network 行并提取其中的 LAN 地址 # 最长等待 8 秒;失败时返回空字符串(不阻塞启动流程) wait_and_collect_network_urls() { local log_file="$1" local max_wait="${2:-8}" local i=0 while (( i < max_wait )); do if grep -q "Network:" "$log_file" 2>/dev/null; then break fi sleep 1 i=$((i + 1)) done grep -Eo "http://[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+:[0-9]+/?" "$log_file" 2>/dev/null \ | grep -v '127\.0\.0\.1' \ | sort -u } print_summary() { printf "\n${COLOR_GREEN}=============== EchoChat 启动完成 ===============${COLOR_RESET}\n" printf " 前台用户端 (H5): http://localhost:5173\n" printf " 后台管理端: http://localhost:3100\n" printf " Go 后端 API: http://localhost:8085\n" printf " 媒体服务器 (SFU): http://localhost:3300 (healthz: /healthz)\n" printf " MinIO 控制台: http://localhost:9001 (echochat / echochat123456)\n" # 前端 / 管理端 Vite dev server 启动时 host=0.0.0.0, # 会输出多条 Network 行(本机每块网卡一条),这里从日志提取展示 local fe_log="$LOG_DIR/frontend.log" local admin_log="$LOG_DIR/admin.log" local fe_urls admin_urls fe_urls="$(wait_and_collect_network_urls "$fe_log" 8)" admin_urls="$(wait_and_collect_network_urls "$admin_log" 4)" if [[ -n "$fe_urls" || -n "$admin_urls" ]]; then printf "\n${COLOR_CYAN}局域网访问地址${COLOR_RESET}(同 WiFi/LAN 下其他设备可直接访问):\n" if [[ -n "$fe_urls" ]]; then printf " 前台用户端:\n" while IFS= read -r line; do [[ -z "$line" ]] && continue printf " %s\n" "$line" done <<< "$fe_urls" fi if [[ -n "$admin_urls" ]]; then printf " 后台管理端:\n" while IFS= read -r line; do [[ -z "$line" ]] && continue printf " %s\n" "$line" done <<< "$admin_urls" fi printf " 提示:接口代理走前端 Vite,后端 API(8085) / 媒体(3300) 默认绑定 0.0.0.0,\n" printf " 如需从其他设备访问 API/媒体,需要在前端 .env 里把 baseURL 改成本机 LAN IP。\n" fi printf "\n 日志目录: %s\n" "$LOG_DIR" printf " PID 目录: %s\n" "$RUN_DIR" printf " 查看状态: ./scripts/status.sh\n" printf " 停止全部: ./scripts/stop.sh\n" printf "${COLOR_GREEN}=================================================${COLOR_RESET}\n" } main() { local target="${1:-all}" local skip_docker=false if [[ "$target" == "--no-docker" ]]; then skip_docker=true target="all" fi case "$target" in all) $skip_docker || start_docker || log_err "Docker 中间件启动异常,请检查 Docker Desktop 是否运行" start_backend || true start_media || true start_frontend || true start_admin || true print_summary ;; docker) start_docker ;; backend) start_backend ;; frontend) start_frontend ;; admin) start_admin ;; media) start_media ;; full) start_full || log_err "compose 全栈启动异常" # full 模式下仍用本地进程跑前台 + 管理端(热更体验更好,符合开发机场景) start_frontend || true start_admin || true print_summary ;; *) log_err "未知参数: $target" echo "用法: $0 [all|docker|backend|frontend|admin|media|full|--no-docker]" echo "" echo "模式说明:" echo " all 本地进程启动应用 + docker 只起中间件(默认,开发常用)" echo " full docker compose 全栈启动(含 media-server 容器,接近生产形态)" echo " 公网部署 使用 scripts/deploy-public.sh(校验 ANNOUNCED_IP + 启动 coturn)" exit 1 ;; esac } main "$@"