Spaces:
Paused
Paused
| set -eu | |
| # ============================================================ | |
| # 1. 管理员密码来源 | |
| # ============================================================ | |
| if [ -n "${ADMIN_PASSWORD_V2:-}" ]; then | |
| ADMIN_PASSWORD="$ADMIN_PASSWORD_V2" | |
| echo "Admin password secret source: ADMIN_PASSWORD_V2" | |
| else | |
| echo "Admin password secret source: ADMIN_PASSWORD" | |
| fi | |
| # ============================================================ | |
| # 2. 自部署 PostgreSQL 初始化与启动 | |
| # ============================================================ | |
| init_postgres() { | |
| echo "Initializing and starting self-deployed PostgreSQL..." | |
| # 如果数据目录不存在,则执行初始化 | |
| if [ ! -d "$PGDATA" ]; then | |
| echo "PostgreSQL data directory not found, initializing..." | |
| mkdir -p "$PGDATA" | |
| chown postgres:postgres "$PGDATA" | |
| # 使用临时文件传递密码 | |
| pwfile=$(mktemp) | |
| echo "$POSTGRES_PASSWORD" > "$pwfile" | |
| chown postgres:postgres "$pwfile" | |
| chmod 600 "$pwfile" | |
| su-exec postgres initdb -D "$PGDATA" --auth=scram-sha-256 --pwfile="$pwfile" | |
| rm -f "$pwfile" | |
| fi | |
| # 配置 postgresql.conf | |
| echo "listen_addresses = 'localhost'" >> "$PGDATA/postgresql.conf" | |
| echo "port = 5432" >> "$PGDATA/postgresql.conf" | |
| echo "unix_socket_directories = '/tmp'" >> "$PGDATA/postgresql.conf" | |
| # 启动 PostgreSQL | |
| echo "Starting PostgreSQL daemon..." | |
| su-exec postgres pg_ctl -D "$PGDATA" -o "-c unix_socket_directories=/tmp" start | |
| # 等待就绪 | |
| echo "Waiting for PostgreSQL to accept connections..." | |
| for i in $(seq 1 30); do | |
| if su-exec postgres pg_isready -U postgres -h localhost -p 5432 >/dev/null 2>&1; then | |
| echo "PostgreSQL is ready." | |
| break | |
| fi | |
| sleep 1 | |
| done | |
| # 创建应用用户和数据库(如果不存在) | |
| echo "Ensuring application user and database exist..." | |
| export PGPASSWORD="$POSTGRES_PASSWORD" | |
| # 取消可能存在的 PGDATABASE 环境变量,并显式指定数据库为 postgres | |
| unset PGDATABASE | |
| su-exec postgres psql -h localhost -p 5432 -d postgres -v ON_ERROR_STOP=1 --username postgres <<-EOSQL | |
| DO \$\$ | |
| BEGIN | |
| IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '$POSTGRES_USER') THEN | |
| CREATE ROLE $POSTGRES_USER WITH LOGIN PASSWORD '$POSTGRES_PASSWORD'; | |
| END IF; | |
| END | |
| \$\$; | |
| SELECT 'CREATE DATABASE $POSTGRES_DB OWNER $POSTGRES_USER' | |
| WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '$POSTGRES_DB')\gexec | |
| EOSQL | |
| unset PGPASSWORD | |
| # 设置 DATABASE_URL 及拆分变量 | |
| export DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}?sslmode=disable" | |
| export DATABASE_HOST=localhost | |
| export DATABASE_PORT=5432 | |
| export DATABASE_USER=$POSTGRES_USER | |
| export DATABASE_PASSWORD=$POSTGRES_PASSWORD | |
| export DATABASE_DBNAME=$POSTGRES_DB | |
| export DATABASE_SSLMODE=disable | |
| echo "PostgreSQL self-deployment ready. DATABASE_URL set to local instance." | |
| } | |
| # ============================================================ | |
| # 3. 数据库连接解析(优先使用外部连接) | |
| # ============================================================ | |
| if [ -n "${EXTERNAL_DATABASE_URL:-}" ]; then | |
| echo "EXTERNAL_DATABASE_URL provided, using external database instead of self-deployed." | |
| export DATABASE_URL="$EXTERNAL_DATABASE_URL" | |
| eval $(python3 -c " | |
| import os, urllib.parse | |
| url = os.environ['DATABASE_URL'] | |
| parsed = urllib.parse.urlparse(url) | |
| user = parsed.username or '' | |
| password = parsed.password or '' | |
| host = parsed.hostname or '' | |
| port = str(parsed.port or 5432) | |
| dbname = parsed.path.lstrip('/') or '' | |
| query = urllib.parse.parse_qs(parsed.query) | |
| sslmode = query.get('sslmode', ['require'])[0] | |
| print(f'export DATABASE_HOST={host}') | |
| print(f'export DATABASE_PORT={port}') | |
| print(f'export DATABASE_USER={user}') | |
| print(f'export DATABASE_PASSWORD={password}') | |
| print(f'export DATABASE_DBNAME={dbname}') | |
| print(f'export DATABASE_SSLMODE={sslmode}') | |
| ") | |
| echo "Parsed external database: DATABASE_HOST=$DATABASE_HOST, DATABASE_DBNAME=$DATABASE_DBNAME" | |
| else | |
| init_postgres | |
| fi | |
| # ============================================================ | |
| # 4. 检查必需的数据库变量 | |
| # ============================================================ | |
| required_db_vars="DATABASE_HOST DATABASE_PORT DATABASE_USER DATABASE_DBNAME" | |
| for var in $required_db_vars; do | |
| eval "val=\${$var:-}" | |
| if [ -z "$val" ]; then | |
| echo "Missing required database variable: $var. Please set DATABASE_URL or EXTERNAL_DATABASE_URL." >&2 | |
| exit 1 | |
| fi | |
| done | |
| # ============================================================ | |
| # 5. 检查其他必需密钥 | |
| # ============================================================ | |
| required_vars="ADMIN_PASSWORD JWT_SECRET TOTP_ENCRYPTION_KEY" | |
| for var in $required_vars; do | |
| eval "val=\${$var:-}" | |
| if [ -z "$val" ]; then | |
| echo "Missing required secret: $var" >&2 | |
| exit 1 | |
| fi | |
| done | |
| # ============================================================ | |
| # 6. 默认值设置 | |
| # ============================================================ | |
| ADMIN_EMAIL="${ADMIN_EMAIL:-2691539771@qq.com}" | |
| SERVER_PORT="${SERVER_PORT:-7860}" | |
| DATA_DIR="${DATA_DIR:-/tmp/gateway-data}" | |
| DATABASE_SSLMODE="${DATABASE_SSLMODE:-require}" | |
| REDIS_HOST="${REDIS_HOST:-127.0.0.1}" | |
| REDIS_PORT="${REDIS_PORT:-6379}" | |
| REDIS_PASSWORD="${REDIS_PASSWORD:-}" | |
| REDIS_DB="${REDIS_DB:-0}" | |
| # ============================================================ | |
| # 7. 创建目录 | |
| # ============================================================ | |
| mkdir -p "$DATA_DIR" /tmp/gateway-redis | |
| mkdir -p "$(dirname "$PGDATA")" | |
| chown -R sub2api:sub2api "$DATA_DIR" /tmp/gateway-redis | |
| # ============================================================ | |
| # 8. 启动 Redis | |
| # ============================================================ | |
| echo "Starting Redis on ${REDIS_HOST}:${REDIS_PORT}..." | |
| redis-server --daemonize yes \ | |
| --bind "${REDIS_HOST}" \ | |
| --port "${REDIS_PORT}" \ | |
| --dir /tmp/gateway-redis \ | |
| --save "" \ | |
| --appendonly no \ | |
| ${REDIS_PASSWORD:+--requirepass "$REDIS_PASSWORD"} | |
| echo "Waiting for Redis..." | |
| while ! redis-cli -h "$REDIS_HOST" -p "$REDIS_PORT" ${REDIS_PASSWORD:+-a "$REDIS_PASSWORD"} ping >/dev/null 2>&1; do | |
| sleep 1 | |
| done | |
| echo "Redis is ready." | |
| # ============================================================ | |
| # 9. 导出环境变量供子进程使用 | |
| # ============================================================ | |
| export AUTO_SETUP="${AUTO_SETUP:-true}" | |
| export ADMIN_EMAIL ADMIN_PASSWORD JWT_SECRET TOTP_ENCRYPTION_KEY | |
| export DATA_DIR | |
| export DATABASE_HOST DATABASE_PORT DATABASE_USER DATABASE_PASSWORD DATABASE_DBNAME DATABASE_SSLMODE | |
| export REDIS_HOST REDIS_PORT REDIS_PASSWORD REDIS_DB | |
| export SERVER_HOST="${SERVER_HOST:-0.0.0.0}" SERVER_PORT | |
| export SERVER_MODE="${SERVER_MODE:-release}" | |
| export PORT="${PORT:-$SERVER_PORT}" | |
| GIN_MODE="${GIN_MODE:-release}" | |
| # ============================================================ | |
| # 10. 辅助函数 | |
| # ============================================================ | |
| migration_count() { | |
| PGPASSWORD="${DATABASE_PASSWORD:-}" psql \ | |
| -h "$DATABASE_HOST" -p "$DATABASE_PORT" \ | |
| -U "$DATABASE_USER" -d "$DATABASE_DBNAME" \ | |
| -tAc "select count(1) from schema_migrations;" 2>/dev/null || printf '0\n' | |
| } | |
| sync_balance_cache_once() { | |
| [ "${BALANCE_CACHE_SYNC_ENABLED:-true}" != "true" ] && return 0 | |
| rows="$(PGPASSWORD="${DATABASE_PASSWORD:-}" psql -h "$DATABASE_HOST" -p "$DATABASE_PORT" -U "$DATABASE_USER" -d "$DATABASE_DBNAME" -qAt -F '|' -c "select id, balance::double precision from users where deleted_at is null and status = 'active';" 2>/tmp/balance-cache-sync.err || true)" | |
| [ -z "$rows" ] && return 0 | |
| ttl="${BALANCE_CACHE_SYNC_TTL_SECONDS:-300}" | |
| printf '%s\n' "$rows" | while IFS='|' read -r user_id balance; do | |
| case "$user_id" in ''|*[!0-9]*) continue ;; esac | |
| redis-cli -h "$REDIS_HOST" -p "$REDIS_PORT" ${REDIS_PASSWORD:+-a "$REDIS_PASSWORD"} -n "$REDIS_DB" \ | |
| SETEX "billing:balance:${user_id}" "$ttl" "$balance" >/dev/null 2>&1 || true | |
| done | |
| } | |
| start_balance_cache_sync() { | |
| [ "${BALANCE_CACHE_SYNC_ENABLED:-true}" != "true" ] && return 0 | |
| ( interval="${BALANCE_CACHE_SYNC_INTERVAL_SECONDS:-60}" | |
| while :; do | |
| sync_balance_cache_once || true | |
| sleep "$interval" | |
| done ) & | |
| echo "Balance cache sync loop started." | |
| } | |
| write_runtime_config() { | |
| cat >"$DATA_DIR/config.yaml" <<EOF | |
| server: | |
| host: "${SERVER_HOST}" | |
| port: ${SERVER_PORT} | |
| mode: "${SERVER_MODE}" | |
| database: | |
| host: "${DATABASE_HOST}" | |
| port: ${DATABASE_PORT} | |
| user: "${DATABASE_USER}" | |
| password: "${DATABASE_PASSWORD}" | |
| dbname: "${DATABASE_DBNAME}" | |
| sslmode: "${DATABASE_SSLMODE}" | |
| max_open_conns: ${DATABASE_MAX_OPEN_CONNS:-10} | |
| max_idle_conns: ${DATABASE_MAX_IDLE_CONNS:-4} | |
| conn_max_lifetime_minutes: ${DATABASE_CONN_MAX_LIFETIME_MINUTES:-10} | |
| conn_max_idle_time_minutes: ${DATABASE_CONN_MAX_IDLE_TIME_MINUTES:-5} | |
| redis: | |
| host: "${REDIS_HOST}" | |
| port: ${REDIS_PORT} | |
| password: "${REDIS_PASSWORD:-}" | |
| db: ${REDIS_DB:-0} | |
| enable_tls: ${REDIS_ENABLE_TLS:-false} | |
| jwt: | |
| secret: "${JWT_SECRET}" | |
| expire_hour: ${JWT_EXPIRE_HOUR:-24} | |
| totp: | |
| encryption_key: "${TOTP_ENCRYPTION_KEY}" | |
| timezone: "${TZ:-Asia/Shanghai}" | |
| default: | |
| user_concurrency: 5 | |
| user_balance: 0 | |
| api_key_prefix: "sk-" | |
| rate_multiplier: 1.0 | |
| rate_limit: | |
| requests_per_minute: 60 | |
| burst_size: 10 | |
| gateway: | |
| max_account_switches: ${GATEWAY_MAX_ACCOUNT_SWITCHES:-3} | |
| max_account_switches_gemini: ${GATEWAY_MAX_ACCOUNT_SWITCHES_GEMINI:-2} | |
| max_idle_conns: ${GATEWAY_MAX_IDLE_CONNS:-256} | |
| max_idle_conns_per_host: ${GATEWAY_MAX_IDLE_CONNS_PER_HOST:-32} | |
| max_conns_per_host: ${GATEWAY_MAX_CONNS_PER_HOST:-128} | |
| max_upstream_clients: ${GATEWAY_MAX_UPSTREAM_CLIENTS:-256} | |
| client_idle_ttl_seconds: ${GATEWAY_CLIENT_IDLE_TTL_SECONDS:-300} | |
| stream_keepalive_interval: ${GATEWAY_STREAM_KEEPALIVE_INTERVAL:-5} | |
| models_list_cache_ttl_seconds: ${GATEWAY_MODELS_LIST_CACHE_TTL_SECONDS:-30} | |
| openai_http2: | |
| enabled: ${OPENAI_HTTP2_ENABLED:-true} | |
| allow_proxy_fallback_to_http1: ${OPENAI_HTTP2_ALLOW_PROXY_FALLBACK_TO_HTTP1:-true} | |
| openai_ws: | |
| max_conns_per_account: ${OPENAI_WS_MAX_CONNS_PER_ACCOUNT:-24} | |
| min_idle_per_account: ${OPENAI_WS_MIN_IDLE_PER_ACCOUNT:-0} | |
| max_idle_per_account: ${OPENAI_WS_MAX_IDLE_PER_ACCOUNT:-4} | |
| event_flush_batch_size: ${OPENAI_WS_EVENT_FLUSH_BATCH_SIZE:-1} | |
| event_flush_interval_ms: ${OPENAI_WS_EVENT_FLUSH_INTERVAL_MS:-5} | |
| usage_record: | |
| worker_count: ${USAGE_RECORD_WORKER_COUNT:-16} | |
| queue_size: ${USAGE_RECORD_QUEUE_SIZE:-4096} | |
| task_timeout_seconds: ${USAGE_RECORD_TASK_TIMEOUT_SECONDS:-15} | |
| overflow_policy: "${USAGE_RECORD_OVERFLOW_POLICY:-sync}" | |
| auto_scale_enabled: ${USAGE_RECORD_AUTO_SCALE_ENABLED:-false} | |
| EOF | |
| printf 'installed_at=%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >"$DATA_DIR/.installed" | |
| chown sub2api:sub2api "$DATA_DIR/config.yaml" "$DATA_DIR/.installed" | |
| chmod 600 "$DATA_DIR/config.yaml" | |
| chmod 400 "$DATA_DIR/.installed" | |
| } | |
| maybe_skip_auto_setup() { | |
| [ "${FORCE_RUNTIME_CONFIG:-false}" = "true" ] && { | |
| write_runtime_config | |
| export AUTO_SETUP=false | |
| echo "Runtime config forced; skipping auto setup." | |
| return 0 | |
| } | |
| [ "${SKIP_AUTO_SETUP_WHEN_MIGRATED:-true}" != "true" ] && return 0 | |
| applied_count="$(migration_count | tr -d '[:space:]')" | |
| case "$applied_count" in ''|*[!0-9]*) applied_count=0 ;; esac | |
| required_count="${MIGRATION_BOOTSTRAP_MIN_COUNT:-178}" | |
| echo "Detected applied migrations: ${applied_count}/${required_count}" | |
| if [ "$applied_count" -ge "$required_count" ]; then | |
| write_runtime_config | |
| export AUTO_SETUP=false | |
| echo "External database is already bootstrapped; skipping auto setup." | |
| fi | |
| } | |
| sync_admin_password() { | |
| [ "${SYNC_ADMIN_PASSWORD:-true}" != "true" ] && return 0 | |
| echo "Admin password sync loop started." | |
| for attempt in $(seq 1 15); do | |
| password_hash="$(python3 - <<'PY' | |
| import bcrypt, os | |
| print(bcrypt.hashpw(os.environ["ADMIN_PASSWORD"].encode("utf-8"), bcrypt.gensalt()).decode("utf-8")) | |
| PY | |
| )" | |
| table_exists="$(PGPASSWORD="${DATABASE_PASSWORD:-}" psql -h "$DATABASE_HOST" -p "$DATABASE_PORT" -U "$DATABASE_USER" -d "$DATABASE_DBNAME" -tAc "select to_regclass('public.users') is not null;" 2>/tmp/admin-user-check.err || true)" | |
| table_exists="$(printf '%s' "$table_exists" | tr -d '[:space:]')" | |
| if [ "$table_exists" = "t" ]; then | |
| PGPASSWORD="${DATABASE_PASSWORD:-}" psql -h "$DATABASE_HOST" -p "$DATABASE_PORT" -U "$DATABASE_USER" -d "$DATABASE_DBNAME" -v ON_ERROR_STOP=1 \ | |
| -c "update users set password_hash = '${password_hash}', role = 'admin', status = 'active', updated_at = now() where email = '${ADMIN_EMAIL}'; insert into users (email, password_hash, role, balance, concurrency, status, created_at, updated_at) select '${ADMIN_EMAIL}', '${password_hash}', 'admin', 0, 5, 'active', now(), now() where not exists (select 1 from users where email = '${ADMIN_EMAIL}');" >/tmp/admin-password-sync.out 2>/tmp/admin-password-sync.err | |
| echo "Admin password synchronized from secret." | |
| return 0 | |
| fi | |
| sleep 2 | |
| done | |
| echo "Admin password sync skipped: admin user not found before timeout." >&2 | |
| } | |
| # ============================================================ | |
| # 11. 主流程 | |
| # ============================================================ | |
| maybe_skip_auto_setup | |
| start_balance_cache_sync | |
| attempt=1 | |
| max_attempts="${BOOTSTRAP_MAX_ATTEMPTS:-6}" | |
| while :; do | |
| echo "Starting gateway on ${SERVER_HOST}:${SERVER_PORT} (attempt ${attempt}/${max_attempts})" | |
| su-exec sub2api /app/hub-gateway & | |
| app_pid="$!" | |
| trap 'kill -TERM "$app_pid" 2>/dev/null || true' INT TERM | |
| sync_admin_password & | |
| set +e | |
| wait "$app_pid" | |
| app_status="$?" | |
| set -e | |
| if [ "$app_status" -eq 0 ]; then | |
| exit 0 | |
| fi | |
| if [ "$AUTO_SETUP" != "true" ]; then | |
| exit "$app_status" | |
| fi | |
| maybe_skip_auto_setup | |
| if [ "$AUTO_SETUP" != "true" ]; then | |
| attempt=$((attempt + 1)) | |
| continue | |
| fi | |
| if [ "$attempt" -ge "$max_attempts" ]; then | |
| exit "$app_status" | |
| fi | |
| attempt=$((attempt + 1)) | |
| echo "Gateway exited during bootstrap; retrying to continue migrations." | |
| sleep 2 | |
| done |