sub2api / start-space.sh
Murasame52's picture
Update start-space.sh
2c9fe33 verified
Raw
History Blame Contribute Delete
14.5 kB
#!/usr/bin/env sh
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