#!/usr/bin/env bash # ============================================================================= # Transfer-Station 一键初始化 # 功能: 生成 .env + nginx 配置 # 用法: ./setup.sh # ============================================================================= set -euo pipefail RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' NC='\033[0m' SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" echo -e "${CYAN}" echo " ╔══════════════════════════════════════╗" echo " ║ Transfer-Station Setup ║" echo " ║ AI API 中转站 · 一键初始化 ║" echo " ╚══════════════════════════════════════╝" echo -e "${NC}" # ─── 检查依赖 ──────────────────────────────────────────────────────────────── check_dep() { if ! command -v "$1" &>/dev/null; then echo -e "${RED}✗ 缺少依赖: $1${NC}" echo " 请先安装: $2" exit 1 fi } check_dep docker "https://docs.docker.com/engine/install/" check_dep openssl "apt install openssl / yum install openssl" # envsubst 可选:没有则用 sed 替代 HAS_ENVSUBST=false if command -v envsubst &>/dev/null; then HAS_ENVSUBST=true fi if ! docker compose version &>/dev/null; then echo -e "${RED}✗ 需要 Docker Compose V2 (docker compose)${NC}" echo " 安装文档: https://docs.docker.com/compose/install/" exit 1 fi # ─── 工具函数 ──────────────────────────────────────────────────────────────── # 安全读取已有 .env 中的某个变量(不 source 整个文件) read_env_var() { local key="$1" file="$2" grep -E "^${key}=" "$file" 2>/dev/null | head -1 | cut -d'=' -f2- } # 域名格式校验 validate_domain() { local d="$1" # 允许: 字母、数字、连字符、点;至少有一个点;不以点或连字符开头/结尾 if [[ "$d" =~ ^[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?\.[a-zA-Z]{2,}$ ]]; then return 0 fi return 1 } # 模板替换(优先 envsubst,fallback 到 sed) render_template() { local template="$1" output="$2" if [ "$HAS_ENVSUBST" = true ]; then envsubst '${DOMAIN} ${SUB2API_PORT}' < "$template" > "$output" else sed -e "s/\${DOMAIN}/${DOMAIN}/g" \ -e "s/\${SUB2API_PORT}/${SUB2API_PORT}/g" \ < "$template" > "$output" fi } # ─── 如果已有 .env,询问是否覆盖 ───────────────────────────────────────────── SKIP_ENV=false if [ -f "$SCRIPT_DIR/.env" ]; then echo -e "${YELLOW}⚠ 检测到已有 .env 文件${NC}" read -rp "覆盖? (y/N): " overwrite if [[ ! "$overwrite" =~ ^[Yy]$ ]]; then echo "跳过 .env 生成,只更新 nginx 配置。" DOMAIN="$(read_env_var DOMAIN "$SCRIPT_DIR/.env")" SUB2API_PORT="$(read_env_var SUB2API_PORT "$SCRIPT_DIR/.env")" if [ -z "$DOMAIN" ]; then echo -e "${RED}✗ 已有 .env 中未找到 DOMAIN,请重新运行并选择覆盖${NC}" exit 1 fi SKIP_ENV=true fi fi # ─── 交互式输入 ────────────────────────────────────────────────────────────── if [ "$SKIP_ENV" != "true" ]; then echo -e "${CYAN}── 基础配置 ──${NC}" while true; do read -rp "你的域名 (例: example.com): " DOMAIN if [ -z "$DOMAIN" ]; then echo -e "${RED} 域名不能为空${NC}" elif ! validate_domain "$DOMAIN"; then echo -e "${RED} 域名格式不对,请输入类似 example.com 的格式${NC}" else break fi done read -rp "管理员邮箱 [admin@${DOMAIN}]: " ADMIN_EMAIL ADMIN_EMAIL="${ADMIN_EMAIL:-admin@${DOMAIN}}" read -rp "管理员密码 (留空=自动生成,首次启动看日志): " ADMIN_PASSWORD read -rp "时区 [Asia/Shanghai]: " TZ_INPUT TZ_INPUT="${TZ_INPUT:-Asia/Shanghai}" # 基本校验:时区格式应为 Region/City if [[ ! "$TZ_INPUT" =~ ^[A-Za-z_]+/[A-Za-z_]+$ ]] && [ "$TZ_INPUT" != "UTC" ]; then echo -e "${YELLOW} ⚠ 时区格式建议为 Region/City(如 Asia/Shanghai),当前值: ${TZ_INPUT}${NC}" read -rp " 继续使用? (Y/n): " tz_confirm if [[ "$tz_confirm" =~ ^[Nn]$ ]]; then read -rp " 重新输入时区: " TZ_INPUT fi fi # 自动生成密钥 echo -e "\n${CYAN}── 自动生成密钥 ──${NC}" POSTGRES_PASSWORD=$(openssl rand -hex 16) JWT_SECRET=$(openssl rand -hex 32) TOTP_ENCRYPTION_KEY=$(openssl rand -hex 32) echo -e " ${GREEN}✓${NC} POSTGRES_PASSWORD" echo -e " ${GREEN}✓${NC} JWT_SECRET" echo -e " ${GREEN}✓${NC} TOTP_ENCRYPTION_KEY" # 先写临时文件再原子替换,避免中途崩溃留下半成品 ENV_TMP="$SCRIPT_DIR/.env.tmp.$$" cat > "$ENV_TMP" << EOF # Transfer-Station 环境配置 — 由 setup.sh 生成于 $(date -Iseconds) DOMAIN=${DOMAIN} ADMIN_EMAIL=${ADMIN_EMAIL} ADMIN_PASSWORD=${ADMIN_PASSWORD} SUB2API_PORT=8080 HERMES_PORT=6060 POSTGRES_USER=sub2api POSTGRES_PASSWORD=${POSTGRES_PASSWORD} POSTGRES_DB=sub2api REDIS_PASSWORD= JWT_SECRET=${JWT_SECRET} JWT_EXPIRE_HOUR=24 TOTP_ENCRYPTION_KEY=${TOTP_ENCRYPTION_KEY} TZ=${TZ_INPUT} SERVER_MODE=release RUN_MODE=standard EOF chmod 600 "$ENV_TMP" || { echo -e "${RED}✗ 无法设置 .env 文件权限${NC}" rm -f "$ENV_TMP" exit 1 } mv "$ENV_TMP" "$SCRIPT_DIR/.env" echo -e "\n${GREEN}✓ .env 已生成(权限 600)${NC}" fi # ─── 生成 nginx 配置 ───────────────────────────────────────────────────────── echo -e "\n${CYAN}── 生成 nginx 配置 ──${NC}" SUB2API_PORT="${SUB2API_PORT:-8080}" export DOMAIN SUB2API_PORT render_template "$SCRIPT_DIR/nginx/api.conf.template" \ "$SCRIPT_DIR/nginx/api.${DOMAIN}.conf" echo -e " ${GREEN}✓${NC} nginx/api.${DOMAIN}.conf" render_template "$SCRIPT_DIR/nginx/landing.conf.template" \ "$SCRIPT_DIR/nginx/landing.${DOMAIN}.conf" echo -e " ${GREEN}✓${NC} nginx/landing.${DOMAIN}.conf" # ─── 创建数据目录 ──────────────────────────────────────────────────────────── mkdir -p "$SCRIPT_DIR/data/sub2api" "$SCRIPT_DIR/data/postgres" "$SCRIPT_DIR/data/redis" echo -e " ${GREEN}✓${NC} data/ 目录已创建" # ─── SSL 证书 ──────────────────────────────────────────────────────────────── echo -e "\n${CYAN}── SSL 证书 ──${NC}" echo "推荐使用 acme.sh 申请通配证书。以 Cloudflare DNS 为例:" echo "" echo -e " ${YELLOW}# 1. 安装 acme.sh${NC}" echo " curl https://get.acme.sh | sh" echo "" echo -e " ${YELLOW}# 2. 申请证书${NC}" echo " export CF_Token=\"你的_cloudflare_api_token\"" echo " ~/.acme.sh/acme.sh --issue --dns dns_cf -d ${DOMAIN} -d '*.${DOMAIN}' --keylength ec-256" echo "" echo -e " ${YELLOW}# 3. 安装到 nginx${NC}" echo " sudo mkdir -p /etc/nginx/ssl" echo " ~/.acme.sh/acme.sh --install-cert -d ${DOMAIN} --ecc \\" echo " --fullchain-file /etc/nginx/ssl/${DOMAIN}.crt \\" echo " --key-file /etc/nginx/ssl/${DOMAIN}.key \\" echo " --reloadcmd 'systemctl reload nginx'" echo "" read -rp "证书已就绪? (y/N,选 N 稍后手动配): " cert_ready if [[ "$cert_ready" =~ ^[Yy]$ ]]; then echo -e " ${GREEN}✓${NC} 证书路径: /etc/nginx/ssl/${DOMAIN}.{crt,key}" fi # ─── 安装 nginx 配置 ───────────────────────────────────────────────────────── echo -e "\n${CYAN}── 安装 nginx 配置 ──${NC}" # 自动检测 nginx 配置目录(Debian/Ubuntu 用 sites-available, RHEL/CentOS 用 conf.d) NGINX_CONF_DIR="" NGINX_STYLE="" if [ -d /etc/nginx/sites-available ]; then NGINX_CONF_DIR="/etc/nginx/sites-available" NGINX_STYLE="debian" elif [ -d /etc/nginx/conf.d ]; then NGINX_CONF_DIR="/etc/nginx/conf.d" NGINX_STYLE="rhel" fi if [ -n "$NGINX_CONF_DIR" ]; then read -rp "自动安装 nginx 配置到 ${NGINX_CONF_DIR}? (y/N): " install_nginx if [[ "$install_nginx" =~ ^[Yy]$ ]]; then sudo cp "$SCRIPT_DIR/nginx/api.${DOMAIN}.conf" "$NGINX_CONF_DIR/" sudo cp "$SCRIPT_DIR/nginx/landing.${DOMAIN}.conf" "$NGINX_CONF_DIR/" # Debian 需要 symlink 到 sites-enabled if [ "$NGINX_STYLE" = "debian" ]; then sudo ln -sf "$NGINX_CONF_DIR/api.${DOMAIN}.conf" /etc/nginx/sites-enabled/ sudo ln -sf "$NGINX_CONF_DIR/landing.${DOMAIN}.conf" /etc/nginx/sites-enabled/ fi if sudo nginx -t; then sudo systemctl reload nginx echo -e " ${GREEN}✓${NC} nginx 配置已安装并 reload" else echo -e " ${RED}✗ nginx 配置检查失败,请手动排查: sudo nginx -t${NC}" fi fi else echo "未检测到 nginx 配置目录,请手动复制:" echo " # Debian / Ubuntu:" echo " sudo cp nginx/*.${DOMAIN}.conf /etc/nginx/sites-available/" echo " sudo ln -s /etc/nginx/sites-available/*.conf /etc/nginx/sites-enabled/" echo "" echo " # RHEL / CentOS:" echo " sudo cp nginx/*.${DOMAIN}.conf /etc/nginx/conf.d/" fi # ─── 完成 ──────────────────────────────────────────────────────────────────── echo -e "\n${GREEN}════════════════════════════════════════${NC}" echo -e "${GREEN} ✓ 初始化完成!${NC}" echo -e "${GREEN}════════════════════════════════════════${NC}" echo "" echo "下一步:" echo "" echo -e " ${CYAN}1. 启动服务:${NC}" echo " docker compose up -d" echo "" echo -e " ${CYAN}2. 查看日志(首次启动看管理员密码):${NC}" echo " docker compose logs -f sub2api" echo "" echo -e " ${CYAN}3. 打开管理后台:${NC}" echo " https://api.${DOMAIN}" echo "" echo -e " ${CYAN}4.(可选)启用聊天 UI:${NC}" echo " docker compose --profile chat up -d" echo "" echo -e " ${CYAN}5.(可选)构建落地页:${NC}" echo " cd landing && npm install && npm run build" echo ""