#!/bin/bash # Sanguo VeighNa Docker 容器启动脚本 set -e # ============================================ # 颜色定义 # ============================================ RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # ============================================ # 日志函数 # ============================================ log_info() { echo -e "${GREEN}[INFO]${NC} $1" } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1" } log_error() { echo -e "${RED}[ERROR]${NC} $1" } log_step() { echo -e "${BLUE}[STEP]${NC} $1" } # ============================================ # 打印启动信息 # ============================================ print_banner() { cat << "EOF" ╔════════════════════════════════════════════════════════════╗ ║ ║ ║ _____ ____ ____ _ _____ _ ___ _ ║ ║ | __ \ / __ \ / __ \ | ||_ _|__ _| | |_ _|___| | ║ ║ | |__) | | | | | | |_| | |/ _` | |_____|| | _ \ | ║ ║ | ___/| | | | | | |_ | | (_| | |_____| | || ||| ║ ║ |_| | |__| | \___/\__| |\_____|_| |___||_||_| ║ ║ \____/ ║ ║ ║ ║ Docker Web 版本 v1.0.0 ║ ║ 基于 VeighNa 4.4.0 构建 ║ ║ ║ ╚════════════════════════════════════════════════════════════╝ EOF } # ============================================ # 检查环境变量 # ============================================ check_env() { log_step "检查环境变量..." # 设置默认值 export TZ="${TZ:-Asia/Shanghai}" export PYTHONUNBUFFERED="${PYTHONUNBUFFERED:-1}" export PYTHONDONTWRITEBYTECODE="${PYTHONDONTWRITEBYTECODE:-1}" # Python 路径(关键): ProcessPoolExecutor(spawn)子进程不继承主进程 sys.path(内存修改), # 但继承环境变量。设 PYTHONPATH 让 spawn 子进程 import sanguo_*/vnpy 不 ModuleNotFoundError→BrokenProcessPool。 # 镜像 Dockerfile ENV 同设此值(优先),此行为旧镜像/无 ENV 时兜底。 export PYTHONPATH="${PYTHONPATH:-/app:/app/vnpy_v4.4.0}" # VeighNa 配置 export VNPY_LOG_LEVEL="${VNPY_LOG_LEVEL:-INFO}" export VNPY_LOG_FILE="${VNPY_LOG_FILE:-/app/logs/vnpy.log}" # 数据库配置 export DB_TYPE="${DB_TYPE:-sqlite}" export DB_PATH="${DB_PATH:-/app/data/vnpy.db}" # Web 服务配置 export WEB_HOST="${WEB_HOST:-0.0.0.0}" export WEB_PORT="${WEB_PORT:-8000}" export WEB_WORKERS="${WEB_WORKERS:-2}" # 安全配置 export JWT_SECRET_KEY="${JWT_SECRET_KEY:-change-this-to-a-random-secret-key}" export SESSION_TIMEOUT="${SESSION_TIMEOUT:-3600}" # Redis 配置 export REDIS_HOST="${REDIS_HOST:-redis}" export REDIS_PORT="${REDIS_PORT:-6379}" # 开发选项 export SKIP_REDIS_CHECK="${SKIP_REDIS_CHECK:-0}" export USE_POSTGRES="${USE_POSTGRES:-0}" export DEBUG="${DEBUG:-0}" # 打印关键配置 log_info "时区: $TZ" log_info "日志级别: $VNPY_LOG_LEVEL" log_info "数据库类型: $DB_TYPE" log_info "Web 端口: $WEB_PORT" log_info "Workers: $WEB_WORKERS" log_info "环境变量检查完成" } # ============================================ # 创建数据目录 # ============================================ create_directories() { log_step "创建数据目录..." # 创建主要目录 mkdir -p /app/data mkdir -p /app/logs mkdir -p /app/temp mkdir -p /app/config # 创建日志子目录 mkdir -p /app/logs/web mkdir -p /app/logs/vnpy # 创建数据子目录 mkdir -p /app/data/database mkdir -p /app/data/storage # 设置权限 chmod -R 755 /app/data chmod -R 755 /app/logs chmod -R 755 /app/temp log_info "数据目录创建完成" } # ============================================ # 初始化数据库 # ============================================ init_database() { log_step "初始化数据库..." if [ "$DB_TYPE" = "sqlite" ]; then if [ ! -f /app/data/vnpy.db ]; then log_info "创建 SQLite 数据库目录..." mkdir -p /app/data/database # 数据库初始化将在 Web 服务启动时完成 log_info "SQLite 数据库准备完成" else log_info "SQLite 数据库已存在" fi elif [ "$DB_TYPE" = "postgres" ] && [ "$USE_POSTGRES" = "1" ]; then log_info "使用 PostgreSQL 数据库" # PostgreSQL 配置由环境变量提供 else log_warn "未知的数据库类型: $DB_TYPE,使用默认 SQLite" export DB_TYPE=sqlite fi } # ============================================ # 生成 SSL 证书(自签名,用于测试) # ============================================ generate_ssl_cert() { if [ ! -f /etc/nginx/ssl/cert.pem ]; then log_warn "SSL 证书不存在,生成自签名证书..." mkdir -p /etc/nginx/ssl openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ -keyout /etc/nginx/ssl/key.pem \ -out /etc/nginx/ssl/cert.pem \ -subj "/C=CN/ST=Beijing/L=Beijing/O=Sanguo/CN=localhost" \ 2>/dev/null log_warn "自签名证书已生成,请勿用于生产环境" fi } # ============================================ # 等待依赖服务 # ============================================ wait_for_services() { log_step "等待依赖服务启动..." # 等待 Redis if [ "$SKIP_REDIS_CHECK" != "1" ]; then log_info "等待 Redis 服务..." REDIS_MAX_WAIT=30 REDIS_WAIT_COUNT=0 while ! nc -z ${REDIS_HOST:-redis} ${REDIS_PORT:-6379} 2>/dev/null; do REDIS_WAIT_COUNT=$((REDIS_WAIT_COUNT + 1)) if [ $REDIS_WAIT_COUNT -ge $REDIS_MAX_WAIT ]; then log_error "Redis 连接超时" log_info "设置 SKIP_REDIS_CHECK=1 可跳过此检查(开发环境)" exit 1 fi sleep 1 done log_info "Redis 服务已就绪 (${REDIS_HOST}:${REDIS_PORT})" else log_warn "跳过 Redis 检查" fi # 等待 PostgreSQL(如果启用) if [ "$USE_POSTGRES" = "1" ]; then log_info "等待 PostgreSQL 服务..." POSTGRES_MAX_WAIT=30 POSTGRES_WAIT_COUNT=0 while ! nc -z ${POSTGRES_HOST:-postgres} ${POSTGRES_PORT:-5432} 2>/dev/null; do POSTGRES_WAIT_COUNT=$((POSTGRES_WAIT_COUNT + 1)) if [ $POSTGRES_WAIT_COUNT -ge $POSTGRES_MAX_WAIT ]; then log_error "PostgreSQL 连接超时" exit 1 fi sleep 1 done log_info "PostgreSQL 服务已就绪" fi } # ============================================ # 健康检查 # ============================================ health_check() { log_step "执行健康检查..." # 检查 Python 环境 if ! python --version &>/dev/null; then log_error "Python 环境检查失败" exit 1 fi log_info "Python 版本: $(python --version)" # 检查关键模块 python -c "import sys; print(f'Python 路径: {sys.executable}')" || { log_error "Python 系统检查失败" exit 1 } # 检查数据目录 if [ ! -d "/app/data" ]; then log_error "数据目录不存在: /app/data" exit 1 fi log_info "健康检查完成" } # ============================================ # 启动 Web 服务 # ============================================ start_web_service() { log_step "启动 Web 服务..." # 切换到工作目录 cd /app # 确定 uvicorn 参数 UVICORN_ARGS="--host ${WEB_HOST} --port ${WEB_PORT}" # Worker 配置 # sanguo_api 的 orchestrator 任务状态在内存(_pending/pool._tasks), # 必须单 worker:多 worker 下"提交"与"查询状态/结果"可能落到不同 worker # 而查不到。Phase 3b 起强制单 worker(WEB_WORKERS 仅兼容保留)。 log_info "使用单 Worker 模式(sanguo_api 状态化 orchestrator)" # 日志级别 UVICORN_ARGS="$UVICORN_ARGS --log-level ${VNPY_LOG_LEVEL:-info}" # 访问日志 UVICORN_ARGS="$UVICORN_ARGS --access-log" # 调试模式(仅开发环境) if [ "$DEBUG" = "1" ]; then log_warn "启用调试模式(不应在生产环境使用)" UVICORN_ARGS="$UVICORN_ARGS --reload" fi # 启动服务(Phase 3b 起:研究/回测 API sanguo_api,工厂模式启动) log_info "启动命令: uvicorn sanguo_api.main:create_app --factory $UVICORN_ARGS" exec uvicorn sanguo_api.main:create_app --factory $UVICORN_ARGS } # ============================================ # 显示访问信息 # ============================================ show_access_info() { echo "" log_info "==========================================" log_info "服务启动中..." log_info "==========================================" echo "" log_info "访问地址:" echo "" echo " HTTP: http://localhost:${WEB_PORT:-8000}" echo " WebSocket: ws://localhost:${WS_PORT:-8080}" echo "" log_info "默认登录:" echo "" echo " 用户名: admin" echo " 密码: admin123" echo "" log_warn "请在首次登录后修改默认密码!" echo "" log_info "==========================================" echo "" } # ============================================ # 主函数 # ============================================ main() { print_banner echo "" check_env create_directories init_database generate_ssl_cert wait_for_services health_check show_access_info log_info "所有检查完成,启动服务..." echo "" start_web_service } # ============================================ # 信号处理 # ============================================ trap 'log_info "收到退出信号,优雅关闭..."; exit 0' SIGTERM SIGINT # ============================================ # 执行主函数 # ============================================ main "$@"