Files
evotraders/deploy/install-production.sh

657 lines
22 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
CYAN='\033[0;36m'
NC='\033[0m'
NON_INTERACTIVE=false
AUTO_INSTALL_DEPS=""
AUTO_INSTALL_SYSTEMD=""
AUTO_START_SYSTEMD=""
AUTO_INSTALL_NGINX=""
AUTO_RELOAD_NGINX=""
AUTO_USE_TLS=""
AUTO_USE_DOCKER=""
log() {
echo -e "${GREEN}[bigtime]${NC} $*"
}
warn() {
echo -e "${YELLOW}[bigtime]${NC} $*"
}
fail() {
echo -e "${RED}[bigtime]${NC} $*" >&2
exit 1
}
ask() {
local prompt="$1"
local default="${2:-}"
if ${NON_INTERACTIVE}; then
printf '%s' "${default}"
return
fi
local value
if [[ -n "${default}" ]]; then
read -r -p "${prompt} [${default}]: " value
printf '%s' "${value:-$default}"
else
read -r -p "${prompt}: " value
printf '%s' "${value}"
fi
}
ask_required() {
local prompt="$1"
local default="${2:-}"
local value=""
while [[ -z "${value}" ]]; do
value="$(ask "${prompt}" "${default}")"
if [[ -z "${value}" ]]; then
warn "该项不能为空,请重新输入。"
fi
done
printf '%s' "${value}"
}
validate_domain_like() {
local value="$1"
[[ -z "${value}" ]] && return 1
[[ "${value}" =~ ^[A-Za-z0-9.-]+$ ]]
}
validate_file_parent_exists_or_rootable() {
local value="$1"
local parent
parent="$(dirname "${value}")"
[[ -d "${parent}" ]] || [[ "${parent}" == "/etc/bigtime" ]] || [[ "${parent}" == "/etc/nginx/conf.d" ]]
}
validate_numeric() {
local value="$1"
[[ "${value}" =~ ^[0-9]+([.][0-9]+)?$ ]]
}
confirm() {
local prompt="$1"
local default="${2:-Y}"
local override="${3:-}"
if [[ -n "${override}" ]]; then
[[ "${override}" =~ ^[Yy]([Ee][Ss])?$|^true$|^1$ ]]
return
fi
if ${NON_INTERACTIVE}; then
[[ "${default}" == "Y" ]]
return
fi
local suffix="[Y/n]"
[[ "${default}" == "N" ]] && suffix="[y/N]"
local value
read -r -p "${prompt} ${suffix}: " value
value="${value:-$default}"
[[ "${value}" =~ ^[Yy]$ ]]
}
command_exists() {
command -v "$1" >/dev/null 2>&1
}
detect_pkg_manager() {
if command_exists apt-get; then
echo "apt"
return
fi
if command_exists dnf; then
echo "dnf"
return
fi
if command_exists yum; then
echo "yum"
return
fi
echo ""
}
install_packages() {
local pkg_manager="$1"
case "${pkg_manager}" in
apt)
sudo apt-get update
sudo apt-get install -y python3 python3-venv python3-pip nginx curl git build-essential nodejs npm
;;
dnf)
sudo dnf install -y python3 python3-pip nginx curl git gcc-c++ make nodejs npm
;;
yum)
sudo yum install -y python3 python3-pip nginx curl git gcc-c++ make nodejs npm
;;
*)
warn "未识别包管理器,跳过依赖安装。请手动安装 python3、venv、pip、nginx、node、npm。"
;;
esac
}
render_systemd_unit() {
local service_name="$1"
local app_module="$2"
local port="$3"
local workers="$4"
local memory_max="$5"
local unit_path="$6"
sudo tee "${unit_path}" >/dev/null <<EOF
[Unit]
Description=BigTime ${service_name}
After=network.target
[Service]
Type=simple
User=${SERVICE_USER}
Group=${SERVICE_GROUP}
WorkingDirectory=${APP_DIR}
EnvironmentFile=${ENV_FILE}
ExecStart=${PYTHON_BIN} -m uvicorn ${app_module} --host 127.0.0.1 --port ${port} --workers ${workers} --log-level warning --no-access-log
Restart=always
RestartSec=3
TimeoutStopSec=30
KillMode=mixed
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=false
LimitNOFILE=65535
TasksMax=4096
MemoryMax=${memory_max}
[Install]
WantedBy=multi-user.target
EOF
}
render_nginx_conf() {
local target="$1"
local use_tls="$2"
if [[ "${use_tls}" == "yes" ]]; then
sudo tee "${target}" >/dev/null <<EOF
server {
listen 80;
server_name ${DOMAIN};
root ${APP_DIR}/frontend/dist;
location /.well-known/acme-challenge/ {
allow all;
}
location / {
return 301 https://\$host\$request_uri;
}
}
server {
listen 443 ssl http2;
server_name ${DOMAIN};
root ${APP_DIR}/frontend/dist;
index index.html;
ssl_certificate ${SSL_CERT_PATH};
ssl_certificate_key ${SSL_KEY_PATH};
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location /ws {
proxy_pass http://127.0.0.1:8765;
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_read_timeout 300s;
}
location /api/runtime/ {
proxy_pass http://127.0.0.1:8003;
proxy_http_version 1.1;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_read_timeout 300s;
}
location /api/dynamic-team/ {
proxy_pass http://127.0.0.1:8003;
proxy_http_version 1.1;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_read_timeout 300s;
}
location /api/trading/ {
proxy_pass http://127.0.0.1:8001;
proxy_http_version 1.1;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_read_timeout 300s;
}
location /api/news/ {
proxy_pass http://127.0.0.1:8002;
proxy_http_version 1.1;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_read_timeout 300s;
}
location /api/ {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_read_timeout 300s;
}
location / {
try_files \$uri \$uri/ /index.html;
}
}
EOF
else
sudo tee "${target}" >/dev/null <<EOF
server {
listen 80;
server_name ${DOMAIN};
root ${APP_DIR}/frontend/dist;
index index.html;
location /.well-known/acme-challenge/ {
allow all;
}
location /ws {
proxy_pass http://127.0.0.1:8765;
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_read_timeout 300s;
}
location /api/runtime/ {
proxy_pass http://127.0.0.1:8003;
proxy_http_version 1.1;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_read_timeout 300s;
}
location /api/dynamic-team/ {
proxy_pass http://127.0.0.1:8003;
proxy_http_version 1.1;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_read_timeout 300s;
}
location /api/trading/ {
proxy_pass http://127.0.0.1:8001;
proxy_http_version 1.1;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_read_timeout 300s;
}
location /api/news/ {
proxy_pass http://127.0.0.1:8002;
proxy_http_version 1.1;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_read_timeout 300s;
}
location /api/ {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_read_timeout 300s;
}
location / {
try_files \$uri \$uri/ /index.html;
}
}
EOF
fi
}
write_env_file() {
sudo mkdir -p "$(dirname "${ENV_FILE}")"
sudo tee "${ENV_FILE}" >/dev/null <<EOF
AGENT_SERVICE_URL=http://127.0.0.1:8000
TRADING_SERVICE_URL=http://127.0.0.1:8001
NEWS_SERVICE_URL=http://127.0.0.1:8002
RUNTIME_SERVICE_URL=http://127.0.0.1:8003
TICKERS=${TICKERS}
FIN_DATA_SOURCE=${FIN_DATA_SOURCE}
FINANCIAL_DATASETS_API_KEY=${FINANCIAL_DATASETS_API_KEY}
FINNHUB_API_KEY=${FINNHUB_API_KEY}
POLYGON_API_KEY=${POLYGON_API_KEY}
OPENAI_API_KEY=${OPENAI_API_KEY}
OPENAI_BASE_URL=${OPENAI_BASE_URL}
DASHSCOPE_API_KEY=${DASHSCOPE_API_KEY}
MODEL_NAME=${MODEL_NAME}
MEMORY_API_KEY=${MEMORY_API_KEY}
SKILL_SANDBOX_MODE=${SKILL_SANDBOX_MODE}
MAX_COMM_CYCLES=${MAX_COMM_CYCLES}
MARGIN_REQUIREMENT=${MARGIN_REQUIREMENT}
EOF
}
usage() {
cat <<EOF
Usage:
./deploy/install-production.sh [options]
Options:
--non-interactive Run with defaults / env overrides only
--app-dir PATH Application directory
--service-user USER systemd service user
--service-group GROUP systemd service group
--domain DOMAIN Public domain
--env-file PATH Environment file path
--python-bin PATH Python executable path
--tickers CSV Default tickers
--fin-data-source NAME finnhub/yfinance/financial_datasets
--model-name NAME Default model name
--max-comm-cycles N Conference rounds
--margin-requirement NUM Margin requirement
--use-docker-sandbox Set SKILL_SANDBOX_MODE=docker
--no-docker-sandbox Set SKILL_SANDBOX_MODE=none
--with-tls Generate HTTPS nginx config
--without-tls Generate HTTP nginx config
--install-deps Auto install dependencies
--skip-install-deps Skip dependency installation
--install-systemd Install systemd units
--skip-install-systemd Skip systemd unit installation
--start-systemd Enable/start services
--skip-start-systemd Do not start services
--install-nginx Install nginx config
--skip-install-nginx Skip nginx config installation
--reload-nginx Run nginx -t and reload
--skip-reload-nginx Skip nginx reload
--ssl-cert-path PATH TLS certificate path
--ssl-key-path PATH TLS key path
--help Show this help
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--non-interactive) NON_INTERACTIVE=true ;;
--app-dir) APP_DIR="${2:?missing value}"; shift ;;
--service-user) SERVICE_USER="${2:?missing value}"; shift ;;
--service-group) SERVICE_GROUP="${2:?missing value}"; shift ;;
--domain) DOMAIN="${2:?missing value}"; shift ;;
--env-file) ENV_FILE="${2:?missing value}"; shift ;;
--python-bin) PYTHON_BIN="${2:?missing value}"; shift ;;
--tickers) TICKERS="${2:?missing value}"; shift ;;
--fin-data-source) FIN_DATA_SOURCE="${2:?missing value}"; shift ;;
--model-name) MODEL_NAME="${2:?missing value}"; shift ;;
--max-comm-cycles) MAX_COMM_CYCLES="${2:?missing value}"; shift ;;
--margin-requirement) MARGIN_REQUIREMENT="${2:?missing value}"; shift ;;
--use-docker-sandbox) AUTO_USE_DOCKER="Y" ;;
--no-docker-sandbox) AUTO_USE_DOCKER="N" ;;
--with-tls) AUTO_USE_TLS="Y" ;;
--without-tls) AUTO_USE_TLS="N" ;;
--install-deps) AUTO_INSTALL_DEPS="Y" ;;
--skip-install-deps) AUTO_INSTALL_DEPS="N" ;;
--install-systemd) AUTO_INSTALL_SYSTEMD="Y" ;;
--skip-install-systemd) AUTO_INSTALL_SYSTEMD="N" ;;
--start-systemd) AUTO_START_SYSTEMD="Y" ;;
--skip-start-systemd) AUTO_START_SYSTEMD="N" ;;
--install-nginx) AUTO_INSTALL_NGINX="Y" ;;
--skip-install-nginx) AUTO_INSTALL_NGINX="N" ;;
--reload-nginx) AUTO_RELOAD_NGINX="Y" ;;
--skip-reload-nginx) AUTO_RELOAD_NGINX="N" ;;
--ssl-cert-path) SSL_CERT_PATH="${2:?missing value}"; shift ;;
--ssl-key-path) SSL_KEY_PATH="${2:?missing value}"; shift ;;
--help) usage; exit 0 ;;
*) fail "Unknown option: $1" ;;
esac
shift
done
main() {
echo -e "${CYAN}BigTime 生产部署向导${NC}"
echo ""
echo -e "${YELLOW}说明:${NC} 这个脚本适合从一台空机器开始部署当前项目。"
echo -e "${YELLOW}默认推荐:${NC} split-service + systemd + nginx + 静态前端。"
echo ""
if confirm "尝试自动安装基础依赖python3/nginx/node 等)?" "Y" "${AUTO_INSTALL_DEPS}"; then
PKG_MANAGER="$(detect_pkg_manager)"
install_packages "${PKG_MANAGER}"
fi
echo -e "${CYAN}基础配置${NC}"
APP_DIR="${APP_DIR:-$(ask_required '应用部署目录(仓库根目录,建议绝对路径)' "${REPO_ROOT}")}"
[[ -d "${APP_DIR}" ]] || fail "应用目录不存在: ${APP_DIR}"
SERVICE_USER="${SERVICE_USER:-$(ask_required 'systemd 运行用户' "$(id -un)")}"
id "${SERVICE_USER}" >/dev/null 2>&1 || warn "用户 ${SERVICE_USER} 当前不存在,请确认后续 systemd 配置。"
SERVICE_GROUP="${SERVICE_GROUP:-$(ask_required 'systemd 运行用户组' "$(id -gn)")}"
# 自动尝试获取公网 IP 作为默认域名值
local detected_ip=""
if [[ -z "${DOMAIN:-}" ]]; then
log "正在尝试自动获取公网 IP..."
detected_ip=$(curl -s --connect-timeout 5 https://ifconfig.me || curl -s --connect-timeout 5 https://api.ipify.org || echo "")
if [[ -n "${detected_ip}" ]]; then
log "自动检测到公网 IP: ${detected_ip}"
fi
fi
DOMAIN="${DOMAIN:-$(ask_required '部署域名(可填写 IP 或 localhost' "${detected_ip:-localhost}")}"
validate_domain_like "${DOMAIN}" || warn "域名/IP 形态看起来不标准,请再次确认: ${DOMAIN}"
ENV_FILE="${ENV_FILE:-$(ask_required '环境变量文件路径' '/etc/bigtime/bigtime.env')}"
validate_file_parent_exists_or_rootable "${ENV_FILE}" || warn "环境文件父目录当前不存在,脚本会尝试创建: $(dirname "${ENV_FILE}")"
PYTHON_BIN="${PYTHON_BIN:-$(ask 'Python 可执行文件路径' "${APP_DIR}/.venv/bin/python")}"
[[ -n "${PYTHON_BIN}" ]] || fail "Python 路径不能为空"
local SKIP_ENV_CONFIG=false
if [[ -f "${ENV_FILE}" ]]; then
echo ""
if confirm "检测到环境变量文件 ${ENV_FILE} 已存在,是否跳过详细参数配置并保留现有文件?" "Y"; then
SKIP_ENV_CONFIG=true
fi
fi
if ! ${SKIP_ENV_CONFIG}; then
echo ""
echo -e "${CYAN}运行参数${NC}"
TICKERS="${TICKERS:-$(ask '默认股票池(逗号分隔)' 'AAPL,MSFT,GOOGL,AMZN,NVDA,META,TSLA,AMD,NFLX,AVGO,PLTR,COIN')}"
FIN_DATA_SOURCE="${FIN_DATA_SOURCE:-$(ask '行情数据源finnhub/yfinance/financial_datasets' 'finnhub')}"
MODEL_NAME="${MODEL_NAME:-$(ask '默认模型名' 'qwen3-max')}"
MAX_COMM_CYCLES="${MAX_COMM_CYCLES:-$(ask_required '最大讨论轮数' '2')}"
validate_numeric "${MAX_COMM_CYCLES}" || fail "最大讨论轮数必须是数字: ${MAX_COMM_CYCLES}"
MARGIN_REQUIREMENT="${MARGIN_REQUIREMENT:-$(ask_required '保证金比例' '0.5')}"
validate_numeric "${MARGIN_REQUIREMENT}" || fail "保证金比例必须是数字: ${MARGIN_REQUIREMENT}"
echo ""
echo -e "${CYAN}密钥配置${NC}"
FINANCIAL_DATASETS_API_KEY="${FINANCIAL_DATASETS_API_KEY:-$(ask 'FINANCIAL_DATASETS_API_KEY可留空' '')}"
FINNHUB_API_KEY="${FINNHUB_API_KEY:-$(ask 'FINNHUB_API_KEYlive 模式建议填写)' '')}"
POLYGON_API_KEY="${POLYGON_API_KEY:-$(ask 'POLYGON_API_KEY可留空' '')}"
OPENAI_API_KEY="${OPENAI_API_KEY:-$(ask 'OPENAI_API_KEY可留空' '')}"
OPENAI_BASE_URL="${OPENAI_BASE_URL:-$(ask 'OPENAI_BASE_URL可留空' '')}"
DASHSCOPE_API_KEY="${DASHSCOPE_API_KEY:-$(ask 'DASHSCOPE_API_KEY可留空' '')}"
MEMORY_API_KEY="${MEMORY_API_KEY:-$(ask 'MEMORY_API_KEY可留空' '')}"
if [[ "${FIN_DATA_SOURCE}" == "finnhub" && -z "${FINNHUB_API_KEY}" ]]; then
warn "你选择了 finnhub 作为数据源,但 FINNHUB_API_KEY 为空。live 模式下通常会失败。"
fi
if [[ -z "${OPENAI_API_KEY}" && -z "${DASHSCOPE_API_KEY}" ]]; then
warn "OPENAI_API_KEY 和 DASHSCOPE_API_KEY 都为空,模型调用可能无法工作。"
fi
if confirm "使用 Docker 沙盒执行技能?" "N" "${AUTO_USE_DOCKER}"; then
SKILL_SANDBOX_MODE="docker"
else
SKILL_SANDBOX_MODE="none"
fi
echo ""
echo -e "${CYAN}当前部署摘要${NC}"
echo " 应用目录: ${APP_DIR}"
echo " 运行用户: ${SERVICE_USER}:${SERVICE_GROUP}"
echo " 域名: ${DOMAIN}"
echo " 环境文件: ${ENV_FILE}"
echo " Python: ${PYTHON_BIN}"
echo " 数据源: ${FIN_DATA_SOURCE:-}"
echo " 模型: ${MODEL_NAME:-}"
echo " 沙盒模式: ${SKILL_SANDBOX_MODE:-none}"
echo ""
if ! confirm "确认以上配置并继续写入系统文件?" "Y"; then
fail "用户取消部署。"
fi
else
echo -e "${GREEN}将使用现有的环境文件,跳过详细参数配置。${NC}"
fi
if [[ ! -x "${PYTHON_BIN}" ]]; then
warn "未找到 ${PYTHON_BIN},准备创建虚拟环境。"
python3 -m venv "${APP_DIR}/.venv"
"${APP_DIR}/.venv/bin/python" -m pip install --upgrade pip
PYTHON_BIN="${APP_DIR}/.venv/bin/python"
fi
log "安装后端依赖"
"${PYTHON_BIN}" -m pip install -e "${APP_DIR}"
log "构建前端"
(cd "${APP_DIR}/frontend" && npm install && npm run build)
if ! ${SKIP_ENV_CONFIG}; then
log "写入环境变量文件 ${ENV_FILE}"
write_env_file
fi
if confirm "生成并安装 systemd unit" "Y" "${AUTO_INSTALL_SYSTEMD}"; then
render_systemd_unit "Agent Service" "backend.apps.agent_service:app" "8000" "1" "1024M" "/etc/systemd/system/bigtime-agent.service"
render_systemd_unit "Trading Service" "backend.apps.trading_service:app" "8001" "1" "768M" "/etc/systemd/system/bigtime-trading.service"
render_systemd_unit "News Service" "backend.apps.news_service:app" "8002" "1" "768M" "/etc/systemd/system/bigtime-news.service"
render_systemd_unit "Runtime Service" "backend.apps.runtime_service:app" "8003" "1" "1536M" "/etc/systemd/system/bigtime-runtime.service"
sudo systemctl daemon-reload
if confirm "立即启用并启动 bigtime-* 服务?" "Y" "${AUTO_START_SYSTEMD}"; then
sudo systemctl enable --now bigtime-agent.service
sudo systemctl enable --now bigtime-trading.service
sudo systemctl enable --now bigtime-news.service
sudo systemctl enable --now bigtime-runtime.service
fi
fi
if confirm "生成并安装 nginx 配置?" "Y" "${AUTO_INSTALL_NGINX}"; then
local use_tls="no"
if confirm "使用 HTTPS/Let's Encrypt 证书路径?" "N" "${AUTO_USE_TLS}"; then
SSL_CERT_PATH="${SSL_CERT_PATH:-$(ask_required 'SSL 证书 fullchain.pem 路径' "/etc/letsencrypt/live/${DOMAIN}/fullchain.pem")}"
SSL_KEY_PATH="${SSL_KEY_PATH:-$(ask_required 'SSL 私钥 privkey.pem 路径' "/etc/letsencrypt/live/${DOMAIN}/privkey.pem")}"
local ssl_err=0
[[ -f "${SSL_CERT_PATH}" ]] || { warn "SSL 证书文件不存在: ${SSL_CERT_PATH}"; ssl_err=1; }
[[ -f "${SSL_KEY_PATH}" ]] || { warn "SSL 私钥文件不存在: ${SSL_KEY_PATH}"; ssl_err=1; }
[[ -f "/etc/letsencrypt/options-ssl-nginx.conf" ]] || { warn "缺失 /etc/letsencrypt/options-ssl-nginx.conf请检查 certbot 配置"; ssl_err=1; }
[[ -f "/etc/letsencrypt/ssl-dhparams.pem" ]] || { warn "缺失 /etc/letsencrypt/ssl-dhparams.pem请检查 certbot 配置"; ssl_err=1; }
if [[ ${ssl_err} -eq 0 ]]; then
use_tls="yes"
else
warn "由于 SSL 关键文件缺失,将回退至 HTTP 模式,以确保 Nginx 能通过配置检查。"
use_tls="no"
fi
else
SSL_CERT_PATH=""
SSL_KEY_PATH=""
fi
NGINX_TARGET="/etc/nginx/conf.d/bigtime.conf"
render_nginx_conf "${NGINX_TARGET}" "${use_tls}"
if confirm "立即执行 nginx -t 并生效配置?" "Y" "${AUTO_RELOAD_NGINX}"; then
log "正在验证 Nginx 配置..."
if ! sudo nginx -t; then
fail "Nginx 配置检查失败请根据上方报错信息调整。常见的错误包括80/443 端口被占用,或 server_name 冲突。"
fi
if systemctl is-active --quiet nginx; then
log "Nginx 正在运行,执行 reload..."
sudo systemctl reload nginx
else
log "Nginx 未运行,尝试启动..."
sudo systemctl enable --now nginx
fi
# 关键修复:确保 nginx 用户对 /root 路径有 x 权限
if [[ "${APP_DIR}" == /root/* ]]; then
log "检测到应用部署在 /root 下,正在修复父目录访问权限..."
sudo chmod o+x /root 2>/dev/null || true
sudo chmod o+x "$(dirname "${APP_DIR}")" 2>/dev/null || true
sudo chmod -R o+rX "${APP_DIR}"
fi
log "Nginx 配置已生效。"
fi
fi
echo ""
log "部署向导完成"
echo "应用目录: ${APP_DIR}"
echo "环境文件: ${ENV_FILE}"
echo "Python: ${PYTHON_BIN}"
echo "沙盒模式: ${SKILL_SANDBOX_MODE}"
echo ""
echo "验证建议:"
echo " curl http://127.0.0.1:8003/health"
echo " curl http://127.0.0.1:8003/api/runtime/current"
echo " sudo systemctl status bigtime-runtime.service"
echo " tail -f ${APP_DIR}/runs/<run_id>/logs/gateway.log"
}
main "$@"