| #!/usr/bin/env sh |
| set -eu |
|
|
| 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 |
|
|
| required_vars="ADMIN_PASSWORD JWT_SECRET TOTP_ENCRYPTION_KEY" |
|
|
| for var_name in $required_vars; do |
| eval "var_value=\${$var_name:-}" |
| if [ -z "$var_value" ]; then |
| echo "Missing required secret: $var_name" >&2 |
| exit 1 |
| fi |
| done |
|
|
| ADMIN_EMAIL="${ADMIN_EMAIL:-2691539771@qq.com}" |
| SERVER_PORT="${SERVER_PORT:-7860}" |
| DATA_DIR="${DATA_DIR:-/tmp/gateway-data}" |
|
|
| mkdir -p "$DATA_DIR" /tmp/gateway-redis /tmp/gateway-postgres |
| chown -R sub2api:sub2api "$DATA_DIR" /tmp/gateway-redis |
| chown -R postgres:postgres /tmp/gateway-postgres |
|
|
| redis-server --daemonize yes --bind 127.0.0.1 --port 6379 --dir /tmp/gateway-redis --save "" --appendonly no |
|
|
| if [ -z "${DATABASE_HOST:-}" ]; then |
| if [ ! -s /tmp/gateway-postgres/PG_VERSION ]; then |
| su-exec postgres initdb -D /tmp/gateway-postgres --auth=trust >/tmp/gateway-postgres-init.log |
| fi |
|
|
| su-exec postgres pg_ctl -D /tmp/gateway-postgres -l /tmp/gateway-postgres.log -o "-c listen_addresses=127.0.0.1 -c port=5432 -c unix_socket_directories=/tmp" start |
| createdb -h 127.0.0.1 -U postgres gatewaydb 2>/dev/null || true |
|
|
| DATABASE_HOST="127.0.0.1" |
| DATABASE_PORT="5432" |
| DATABASE_USER="postgres" |
| DATABASE_PASSWORD="" |
| DATABASE_DBNAME="gatewaydb" |
| DATABASE_SSLMODE="disable" |
| echo "Using ephemeral local PostgreSQL. Configure Supabase secrets for persistent data." |
| else |
| DATABASE_PORT="${DATABASE_PORT:-5432}" |
| DATABASE_USER="${DATABASE_USER:-postgres}" |
| DATABASE_PASSWORD="${DATABASE_PASSWORD:-}" |
| DATABASE_DBNAME="${DATABASE_DBNAME:-postgres}" |
| DATABASE_SSLMODE="${DATABASE_SSLMODE:-require}" |
| fi |
|
|
| export AUTO_SETUP="${AUTO_SETUP:-true}" |
| export ADMIN_EMAIL |
| export ADMIN_PASSWORD |
| export JWT_SECRET |
| export TOTP_ENCRYPTION_KEY |
| export DATA_DIR |
| export DATABASE_HOST |
| export DATABASE_PORT |
| export DATABASE_USER |
| export DATABASE_PASSWORD |
| export DATABASE_DBNAME |
| export DATABASE_SSLMODE |
| export REDIS_HOST="${REDIS_HOST:-127.0.0.1}" |
| export REDIS_PORT="${REDIS_PORT:-6379}" |
| export SERVER_HOST="${SERVER_HOST:-0.0.0.0}" |
| export SERVER_PORT |
| export SERVER_MODE="${SERVER_MODE:-release}" |
| export PORT="${PORT:-$SERVER_PORT}" |
| export GIN_MODE="${GIN_MODE:-release}" |
|
|
| 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() { |
| if [ "${BALANCE_CACHE_SYNC_ENABLED:-true}" != "true" ]; then |
| return 0 |
| fi |
|
|
| 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)" |
|
|
| if [ -z "$rows" ]; then |
| return 0 |
| fi |
|
|
| 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 |
| if [ -n "${REDIS_PASSWORD:-}" ]; then |
| REDISCLI_AUTH="$REDIS_PASSWORD" redis-cli \ |
| -h "${REDIS_HOST:-127.0.0.1}" \ |
| -p "${REDIS_PORT:-6379}" \ |
| -n "${REDIS_DB:-0}" \ |
| SETEX "billing:balance:${user_id}" "$ttl" "$balance" >/dev/null 2>&1 || true |
| else |
| redis-cli \ |
| -h "${REDIS_HOST:-127.0.0.1}" \ |
| -p "${REDIS_PORT:-6379}" \ |
| -n "${REDIS_DB:-0}" \ |
| SETEX "billing:balance:${user_id}" "$ttl" "$balance" >/dev/null 2>&1 || true |
| fi |
| done |
| } |
|
|
| start_balance_cache_sync() { |
| if [ "${BALANCE_CACHE_SYNC_ENABLED:-true}" != "true" ]; then |
| return 0 |
| fi |
|
|
| ( |
| 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() { |
| if [ "${FORCE_RUNTIME_CONFIG:-false}" = "true" ]; then |
| write_runtime_config |
| export AUTO_SETUP=false |
| echo "Runtime config forced; skipping auto setup." |
| return 0 |
| fi |
|
|
| if [ "${SKIP_AUTO_SETUP_WHEN_MIGRATED:-true}" != "true" ]; then |
| return 0 |
| fi |
| if [ "$DATABASE_HOST" = "127.0.0.1" ]; then |
| return 0 |
| fi |
|
|
| 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 |
| return 0 |
| } |
|
|
| sync_admin_password() { |
| if [ "${SYNC_ADMIN_PASSWORD:-true}" != "true" ]; then |
| return 0 |
| fi |
|
|
| echo "Admin password sync loop started." |
| synced_dbs="" |
| candidate_dbs="$DATABASE_DBNAME" |
| if [ "$DATABASE_HOST" = "127.0.0.1" ]; then |
| candidate_dbs="$DATABASE_DBNAME postgres gatewaydb" |
| fi |
|
|
| for attempt in $(seq 1 15); do |
| password_hash="$(python3 - <<'PY' |
| import bcrypt |
| import os |
| |
| print(bcrypt.hashpw(os.environ["ADMIN_PASSWORD"].encode("utf-8"), bcrypt.gensalt()).decode("utf-8")) |
| PY |
| )" |
| synced_count=0 |
| for sync_dbname in $candidate_dbs; do |
| case " $synced_dbs " in |
| *" $sync_dbname "*) continue ;; |
| esac |
|
|
| table_exists="$(PGPASSWORD="${DATABASE_PASSWORD:-}" psql \ |
| -h "$DATABASE_HOST" \ |
| -p "$DATABASE_PORT" \ |
| -U "$DATABASE_USER" \ |
| -d "$sync_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:]')" |
| admin_count="$(PGPASSWORD="${DATABASE_PASSWORD:-}" psql \ |
| -h "$DATABASE_HOST" \ |
| -p "$DATABASE_PORT" \ |
| -U "$DATABASE_USER" \ |
| -d "$sync_dbname" \ |
| -tAc "select count(1) from users where role = 'admin';" 2>/dev/null || true)" |
| admin_count="$(printf '%s' "$admin_count" | tr -d '[:space:]')" |
|
|
| if [ "$table_exists" = "t" ]; then |
| PGPASSWORD="${DATABASE_PASSWORD:-}" psql \ |
| -h "$DATABASE_HOST" \ |
| -p "$DATABASE_PORT" \ |
| -U "$DATABASE_USER" \ |
| -d "$sync_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 |
|
|
| stored_hash="$(PGPASSWORD="${DATABASE_PASSWORD:-}" psql \ |
| -h "$DATABASE_HOST" \ |
| -p "$DATABASE_PORT" \ |
| -U "$DATABASE_USER" \ |
| -d "$sync_dbname" \ |
| -tAc "select password_hash from users where role = 'admin' limit 1;" 2>/tmp/admin-password-verify.err || true)" |
| export STORED_ADMIN_PASSWORD_HASH="$stored_hash" |
| verify_result="$(python3 - <<'PY' |
| import bcrypt |
| import os |
| |
| stored = os.environ.get("STORED_ADMIN_PASSWORD_HASH", "").strip() |
| password = os.environ["ADMIN_PASSWORD"].encode("utf-8") |
| print("ok" if stored and bcrypt.checkpw(password, stored.encode("utf-8")) else "failed") |
| PY |
| )" |
| echo "Admin password synchronized from HF secret in db=${sync_dbname}." |
| echo "Admin password bcrypt verification: ${verify_result}" |
| synced_dbs="$synced_dbs $sync_dbname" |
| synced_count=$((synced_count + 1)) |
| fi |
| done |
| if [ -n "$(printf '%s' "$synced_dbs" | tr -d '[:space:]')" ] && [ "$attempt" -ge 5 ]; then |
| return 0 |
| fi |
| sleep 2 |
| done |
|
|
| echo "Admin password sync skipped: admin user not found before timeout." >&2 |
| cat /tmp/admin-user-check.err >&2 || true |
| } |
|
|
| 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" ] || [ "$DATABASE_HOST" = "127.0.0.1" ]; 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 |
|
|