Spaces:
Paused
Paused
File size: 14,490 Bytes
b9c1610 7460651 b9c1610 30cca93 b9c1610 30cca93 b9c1610 7460651 2c9fe33 7460651 f5a618e 57566e9 2c9fe33 57566e9 f5a618e 2c9fe33 f5a618e 2c9fe33 f5a618e 2c9fe33 f5a618e fbf2de9 2c9fe33 f5a618e 2c9fe33 f5a618e 2c9fe33 f5a618e 2c9fe33 f5a618e 7460651 f5a618e 7460651 f5a618e 7460651 f5a618e 2c9fe33 f5a618e 7460651 f5a618e 7460651 f5a618e 7460651 30cca93 b9c1610 7460651 f5a618e 7460651 b9c1610 1cb6fd6 f5a618e 30cca93 7460651 2c9fe33 7460651 30cca93 f5a618e 1cb6fd6 b9c1610 7460651 57566e9 7460651 30cca93 7460651 30cca93 b9c1610 7460651 f5a618e 7460651 b9c1610 30cca93 b9c1610 30cca93 b9c1610 f5a618e b9c1610 7460651 2c9fe33 7460651 b9c1610 30cca93 b9c1610 30cca93 b9c1610 30cca93 f5a618e 30cca93 b9c1610 30cca93 b9c1610 1cb6fd6 b9c1610 f5a618e b9c1610 30cca93 b9c1610 f5a618e b9c1610 30cca93 b9c1610 f5a618e 30cca93 b9c1610 30cca93 b9c1610 f5a618e 30cca93 f5a618e 30cca93 b9c1610 30cca93 b9c1610 7460651 f5a618e 7460651 b9c1610 30cca93 b9c1610 30cca93 b9c1610 30cca93 b9c1610 30cca93 b9c1610 30cca93 b9c1610 30cca93 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 | #!/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 |