#!/bin/bash # ======================================== # Cyrene ethend 启动脚本 # 管理开发环境: 数据库 / 服务编译 / ethend 控制台 # ======================================== set -e SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" ETHEND_DIR="$SCRIPT_DIR/ethend" ROOT="$SCRIPT_DIR" PORT="${ETHEND_PORT:-9090}" LOG_DIR="$ETHEND_DIR/logs" LOG_FILE="$LOG_DIR/sh.log" # 颜色 RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' BOLD='\033[1m' NC='\033[0m' # ========== 平台检测 ========== IS_WIN=false case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) IS_WIN=true ;; esac # ========== 帮助 ========== show_help() { echo -e "${CYAN}Cyrene ethend${NC} — 开发环境管理工具" echo "" echo -e "${BOLD}用法:${NC} ./ethend.sh [命令] [选项]" echo "" echo -e "${BOLD}命令:${NC}" echo " (无参数) 启动 ethend 控制台 (默认)" echo " start 启动 ethend 控制台" echo " start --fresh 强制重启全部后端服务后启动" echo " start --build 编译全部服务后启动" echo " stop 停止 ethend 控制台" echo " status 查看所有服务状态" echo " logs [服务ID] 查看服务日志 (默认显示最近 20 行)" echo " build [服务ID] 编译服务 (不指定则编译全部)" echo " db:start 启动数据库容器 (Docker Compose)" echo " db:stop 停止数据库容器" echo " db:status 检查数据库连接状态" echo " help 显示此帮助" echo "" echo -e "${BOLD}选项:${NC}" echo " --port, -p <端口> 指定 ethend 端口 (默认: 9090)" echo " --fresh 启动前强制重启全部后端服务" echo " --build 启动前编译全部服务" echo "" echo -e "${BOLD}示例:${NC}" echo " ./ethend.sh # 快速启动" echo " ./ethend.sh start --build # 编译后启动" echo " ./ethend.sh start --fresh # 全新重启" echo " ./ethend.sh logs gateway # 查看 Gateway 日志" echo " ./ethend.sh build ai-core # 仅编译 AI-Core" echo " ./ethend.sh db:status # 检查数据库" echo "" echo -e "${BOLD}Web 控制台:${NC} http://localhost:$PORT" } # ========== 依赖检查 ========== check_deps() { local missing=0 # Node.js if ! command -v node &>/dev/null; then if [ -x /usr/local/node/bin/node ]; then export PATH="/usr/local/node/bin:$PATH" else echo -e "${RED}✗ Node.js 未安装${NC}" missing=1 fi fi # Docker (用于数据库) if ! command -v docker &>/dev/null; then echo -e "${YELLOW}⚠ Docker 未安装 (数据库容器功能不可用)${NC}" fi # Go (用于编译后端服务) if ! command -v go &>/dev/null; then if $IS_WIN && [ -f "/c/Program Files/Go/bin/go.exe" ]; then export PATH="$PATH:/c/Program Files/Go/bin" else echo -e "${YELLOW}⚠ Go 未安装 (服务编译功能不可用)${NC}" fi fi return $missing } # ========== 加载 .env ========== load_env() { local env_file="$ROOT/backend/.env" if [ -f "$env_file" ]; then echo -e "${GREEN}✓ 加载环境变量: backend/.env${NC}" set -a source "$env_file" set +a else echo -e "${YELLOW}⚠ 未找到 backend/.env,使用默认值${NC}" fi } # ========== 端口工具 (跨平台) ========== port_in_use() { local port=$1 if $IS_WIN; then netstat -ano 2>/dev/null | grep -q ":$port " | grep -q "LISTENING" else ss -tlnp 2>/dev/null | grep -q ":$port " || netstat -tlnp 2>/dev/null | grep -q ":$port " fi } kill_port() { local port=$1 if $IS_WIN; then local pid=$(netstat -ano 2>/dev/null | grep ":$port " | grep "LISTENING" | awk '{print $NF}' | head -1) if [ -n "$pid" ] && [ "$pid" != "0" ]; then powershell -Command "Stop-Process -Id $pid -Force" 2>/dev/null || true fi else fuser -k "$port/tcp" 2>/dev/null || true fi sleep 1 } # ========== 健康检查 ========== health_check() { local url=$1 local max_wait=${2:-30} local waited=0 while [ $waited -lt $max_wait ]; do if curl -s -o /dev/null -w "%{http_code}" "$url" 2>/dev/null | grep -q "200"; then return 0 fi sleep 1 waited=$((waited + 1)) done return 1 } # ========== 数据库管理 ========== DB_COMPOSE_FILE="$ROOT/docker-compose.dev.db.yml" db_status() { if port_in_use 5432; then echo -e "${GREEN}✓ PostgreSQL 在线 (端口 5432)${NC}" return 0 else echo -e "${RED}✗ PostgreSQL 离线${NC}" return 1 fi } db_start() { echo -e "${CYAN}启动数据库容器...${NC}" if [ ! -f "$DB_COMPOSE_FILE" ]; then echo -e "${RED}✗ 未找到 $DB_COMPOSE_FILE${NC}" return 1 fi docker compose -f "$DB_COMPOSE_FILE" up -d echo -e "${YELLOW}等待数据库就绪...${NC}" if health_check "http://localhost:5432" 30; then echo -e "${GREEN}✓ 数据库已就绪${NC}" else echo -e "${YELLOW}⚠ 数据库可能仍在启动中,请稍后检查${NC}" fi } db_stop() { echo -e "${CYAN}停止数据库容器...${NC}" if [ ! -f "$DB_COMPOSE_FILE" ]; then echo -e "${RED}✗ 未找到 $DB_COMPOSE_FILE${NC}" return 1 fi docker compose -f "$DB_COMPOSE_FILE" down echo -e "${GREEN}✓ 数据库已停止${NC}" } # ========== 服务编译 ========== SERVICES=( "memory-service:backend/memory-service" "tool-engine:backend/tool-engine" "iot-debug-service:backend/iot-debug-service" "voice-service:backend/voice-service" "ai-core:backend/ai-core" "plugin-manager:backend/plugin-manager" "platform-bridge:backend/platform-bridge" "gateway:backend/gateway" ) build_service() { local id=$1 local dir=$2 local label=${3:-$id} echo -e "${CYAN}[编译] $label...${NC}" local binary="main" $IS_WIN && binary="main.exe" cd "$ROOT/$dir" if GOWORK=off go build -o "$binary" ./cmd/main.go 2>&1; then echo -e "${GREEN} ✓ $label 编译完成${NC}" return 0 else echo -e "${RED} ✗ $label 编译失败${NC}" return 1 fi } build_all() { echo -e "${BOLD}编译全部后端服务...${NC}" local failed=0 for entry in "${SERVICES[@]}"; do IFS=':' read -r id dir <<< "$entry" # 检查目录是否存在 if [ -d "$ROOT/$dir" ]; then build_service "$id" "$dir" "$id" || failed=$((failed + 1)) fi done if [ $failed -eq 0 ]; then echo -e "${GREEN}✓ 全部编译完成${NC}" else echo -e "${YELLOW}⚠ $failed 个服务编译失败${NC}" fi cd "$ROOT" return $failed } # ========== 查看日志 ========== show_logs() { local service_id=$1 local lines=${2:-20} local log_path="$LOG_DIR/${service_id}.log" if [ ! -f "$log_path" ]; then echo -e "${YELLOW}日志文件不存在: $log_path${NC}" return 1 fi echo -e "${BOLD}=== $service_id 日志 (最近 $lines 行) ===${NC}" tail -n "$lines" "$log_path" } # ========== 启动 ethend ========== start_ethend() { local do_build=false local do_fresh=false # 解析参数 for arg in "$@"; do case $arg in --build) do_build=true ;; --fresh) do_fresh=true ;; --port=*) PORT="${arg#*=}" ;; -p) shift; PORT="$1" ;; esac done echo "" echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo -e "${CYAN} Cyrene ethend${NC}" echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" # 依赖检查 check_deps || exit 1 echo -e "${YELLOW}Node.js:${NC} $(node --version)" command -v go &>/dev/null && echo -e "${YELLOW}Go:${NC} $(go version | cut -d' ' -f3)" command -v docker &>/dev/null && echo -e "${YELLOW}Docker:${NC} $(docker --version | cut -d' ' -f3 | tr -d ',')" # 加载环境变量 load_env # 编译 (如果指定) if $do_build; then build_all fi # 全新重启 (如果指定) if $do_fresh; then echo -e "${YELLOW}强制重启全部后端服务...${NC}" curl -s -X POST "http://localhost:$PORT/api/services/start-all-fresh" 2>/dev/null || true fi # 检查并释放端口 if port_in_use "$PORT"; then # 检查是否是已有 ethend 实例 if curl -s -o /dev/null "http://localhost:$PORT/api/health" 2>/dev/null; then echo -e "${GREEN}✓ ethend 已在运行: http://localhost:$PORT${NC}" echo -e "${CYAN} API: http://localhost:$PORT/api/health${NC}" return 0 fi echo -e "${YELLOW}⚠ 端口 $PORT 被占用,正在释放...${NC}" kill_port "$PORT" fi # 切换到 ethend 目录 cd "$ETHEND_DIR" # 安装依赖 if [ ! -d "node_modules" ] || [ ! -f "node_modules/.package-lock.json" ]; then echo -e "${YELLOW}安装依赖...${NC}" npm install --silent fi # 确保日志目录存在 mkdir -p "$LOG_DIR" echo "" echo -e "${GREEN}启动 ethend 服务器 (端口: $PORT)...${NC}" echo -e "${CYAN} Web 控制台: http://localhost:$PORT${NC}" echo -e "${CYAN} API: http://localhost:$PORT/api/health${NC}" echo -e "${CYAN} WebSocket: ws://localhost:$PORT/ws${NC}" echo "" echo -e "${YELLOW}正在后台启动...${NC}" # 后台启动 ethend nohup node src/index.js > "$LOG_FILE" 2>&1 & local pid=$! cd "$ROOT" # 等待健康检查 echo -e "${YELLOW}等待服务就绪...${NC}" if health_check "http://localhost:$PORT/api/health" 30; then echo "" echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo -e "${GREEN} ethend 已启动!${NC}" echo -e "${GREEN} PID: ${pid}${NC}" echo -e "${GREEN} 控制台: http://localhost:$PORT${NC}" echo -e "${GREEN} 日志: ${LOG_FILE}${NC}" echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo "" # 检查数据库状态 db_status return 0 fi # 超时 if kill -0 "$pid" 2>/dev/null; then echo "" echo -e "${YELLOW}⚠ 服务可能仍在启动中 (已等待 30 秒)${NC}" echo -e "${CYAN} 请稍后检查 http://localhost:$PORT/api/health${NC}" else echo "" echo -e "${RED}✗ 服务启动失败,请检查日志: ${LOG_FILE}${NC}" return 1 fi } # ========== 停止 ethend ========== stop_ethend() { if port_in_use "$PORT"; then echo -e "${CYAN}停止 ethend...${NC}" if $IS_WIN; then kill_port "$PORT" else fuser -k "$PORT/tcp" 2>/dev/null || true fi echo -e "${GREEN}✓ ethend 已停止${NC}" else echo -e "${YELLOW}ethend 未在运行${NC}" fi } # ========== 查看状态 ========== show_status() { if curl -s -o /dev/null "http://localhost:$PORT/api/health" 2>/dev/null; then echo -e "${GREEN}✓ ethend 在线${NC}" # 获取服务状态 local status_json=$(curl -s "http://localhost:$PORT/api/services" 2>/dev/null) if [ -n "$status_json" ]; then echo "" echo -e "${BOLD}服务状态:${NC}" local tmpfile="$(mktemp)" echo "$status_json" > "$tmpfile" node -e " const fs = require('fs'); const data = JSON.parse(fs.readFileSync(process.argv[1], 'utf-8')); for (const [id, svc] of Object.entries(data)) { const icon = svc.status === 'running' ? '✓' : svc.status === 'stopped' ? '✗' : '○'; const color = svc.status === 'running' ? '\x1b[32m' : '\x1b[31m'; const pid = svc.pid ? ' (PID: ' + svc.pid + ')' : ''; const uptime = svc.uptime ? ' | uptime: ' + Math.round(svc.uptime / 1000) + 's' : ''; console.log(' ' + color + icon + '\x1b[0m ' + svc.name.padEnd(16) + ' [' + svc.status + ']' + pid + uptime); } " "$tmpfile" rm -f "$tmpfile" fi # 数据库状态 echo "" db_status else echo -e "${RED}✗ ethend 离线${NC}" fi } # ========== 主入口 ========== CMD="${1:-start}" shift 2>/dev/null || true case "$CMD" in help|--help|-h) show_help ;; start|"") start_ethend "$@" ;; stop) stop_ethend ;; status) show_status ;; logs) if [ -z "$1" ]; then echo -e "${YELLOW}用法: ./ethend.sh logs <服务ID>${NC}" echo "可用服务: gateway, ai-core, memory-service, tool-engine, voice-service, iot-debug-service, plugin-manager, platform-bridge, frontend" exit 1 fi show_logs "$1" "${2:-20}" ;; build) check_deps || exit 1 load_env if [ -n "$1" ]; then # 查找服务目录 for entry in "${SERVICES[@]}"; do IFS=':' read -r id dir <<< "$entry" if [ "$id" = "$1" ]; then build_service "$id" "$dir" "$id" exit $? fi done echo -e "${RED}未知服务: $1${NC}" echo "可用服务: ${SERVICES[*]}" exit 1 else build_all fi ;; db:start) db_start ;; db:stop) db_stop ;; db:status) db_status ;; *) echo -e "${RED}未知命令: $CMD${NC}" echo "" show_help exit 1 ;; esac