657 lines
22 KiB
Bash
Executable File
657 lines
22 KiB
Bash
Executable File
#!/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_KEY(live 模式建议填写)' '')}"
|
||
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 "$@"
|