setup.sh
Rama main · réplica de lectura del repositorio original.
#!/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 ""