#!/usr/bin/env bash set -euo pipefail provider_id="${1:?provider_id required}" model_name="${2:?model_name required}" env_var="${3:?env var required}" key_file="${4:-}" ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # shellcheck disable=SC1091 source "$ROOT_DIR/scripts/host_access_prep_lib.sh" KEY="${KEY:-/home/long/下载/zjsea.pem}" REMOTE="${REMOTE:-ubuntu@43.155.133.187}" CRM_BASE="${CRM_BASE:-http://127.0.0.1:18088}" HOST_BASE="${HOST_BASE:-http://127.0.0.1:18087}" CRM_HOST_BASE="${CRM_HOST_BASE:-$HOST_BASE}" HOST_NAME="${HOST_NAME:-remote43-current-host}" REMOTE_HOST_ENV_FILE="${REMOTE_HOST_ENV_FILE:-/home/ubuntu/sub2api-host-validation-fresh-deepseek-20260519_115244/.env}" REMOTE_PG_CONTAINER="${REMOTE_PG_CONTAINER:-sub2api-relaymgr-pg}" REMOTE_REDIS_CONTAINER="${REMOTE_REDIS_CONTAINER:-sub2api-relaymgr-redis}" PACK_PATH="${PACK_PATH:-/home/ubuntu/sub2api-cn-relay-manager/packs/openai-cn-pack}" ROOT="${ROOT:-$ROOT_DIR/artifacts/real-host-acceptance}" ART="${ART:-$ROOT/$(date +%Y%m%d_%H%M%S)_remote43_${provider_id}_key_import}" MIN_BALANCE="${MIN_BALANCE:-10}" SUBSCRIPTION_DAYS="${SUBSCRIPTION_DAYS:-30}" SUBSCRIPTION_NOTES="${SUBSCRIPTION_NOTES:-hermes remote subscription validation}" mkdir -p "$ART" REMOTE_PG_CONTAINER_Q="$(printf '%q' "$REMOTE_PG_CONTAINER")" REMOTE_REDIS_CONTAINER_Q="$(printf '%q' "$REMOTE_REDIS_CONTAINER")" if [[ -n "$key_file" ]]; then upstream_key="$(tr -d '\r\n' < "$key_file")" key_source="file:$key_file" else upstream_key="${!env_var:-}" key_source="env:$env_var" fi if [[ -z "$upstream_key" ]]; then echo "missing key from $key_source" >&2 exit 2 fi ssh_cmd() { local cmd="$1" ssh -i "$KEY" -o StrictHostKeyChecking=no "$REMOTE" "$cmd" } crm_curl_json() { local method="$1" local path="$2" local payload="${3:-}" if [[ -n "$payload" ]]; then curl -fsS -X "$method" \ -H "Authorization: Bearer $crm_token" \ -H 'Content-Type: application/json' \ "${CRM_BASE}${path}" \ -d "$payload" else curl -fsS -X "$method" \ -H "Authorization: Bearer $crm_token" \ "${CRM_BASE}${path}" fi } fetch_remote_host_bearer_token() { ssh_cmd "python3 - <<'PY' from pathlib import Path import json, subprocess, sys env_path = Path(${REMOTE_HOST_ENV_FILE@Q}) host_base = ${HOST_BASE@Q} vals = {} for line in env_path.read_text().splitlines(): if '=' not in line: continue key, value = line.split('=', 1) vals[key] = value payload = json.dumps({ 'email': vals['ADMIN_EMAIL'], 'password': vals['ADMIN_PASSWORD'], 'turnstile_token': '', }, ensure_ascii=False) res = subprocess.run([ 'curl', '-fsS', '-H', 'Content-Type: application/json', '-X', 'POST', host_base.rstrip('/') + '/api/v1/auth/login', '-d', payload, ], text=True, capture_output=True) obj = json.loads(res.stdout) token = (obj.get('data') or {}).get('access_token', '') if not token: print(res.stdout, file=sys.stderr) raise SystemExit('missing access_token from remote host login') print(token) PY" } remote_pg_exec() { local sql="$1" local encoded encoded="$(printf '%s' "$sql" | base64 -w0)" ssh_cmd "printf '%s' '$encoded' | base64 -d | sudo -n docker exec -i $REMOTE_PG_CONTAINER_Q psql -U sub2api -d sub2api" } remote_pg_query() { local sql="$1" local encoded encoded="$(printf '%s' "$sql" | base64 -w0)" ssh_cmd "printf '%s' '$encoded' | base64 -d | sudo -n docker exec -i $REMOTE_PG_CONTAINER_Q psql -U sub2api -d sub2api -At -F $'\t'" } remote_fetch_group_state() { local group_id="$1" local user_id="$2" local api_key="$3" local output_path="$4" local sql sql="$(python3 - "$group_id" "$user_id" "$api_key" <<'PY' import sys group_id, user_id, api_key = sys.argv[1:4] api_key_literal = "'" + api_key.replace("'", "''") + "'" query = f""" WITH group_row AS ( SELECT row_to_json(g) AS data FROM groups g WHERE g.id = {group_id} ), subscription_row AS ( SELECT row_to_json(s) AS data FROM user_subscriptions s WHERE s.user_id = {user_id} AND s.group_id = {group_id} AND s.deleted_at IS NULL ORDER BY s.id DESC LIMIT 1 ), key_row AS ( SELECT row_to_json(k) AS data FROM api_keys k WHERE k.key = {api_key_literal} ) SELECT json_build_object( 'group_id', {group_id}, 'group', (SELECT data FROM group_row), 'subscription', (SELECT data FROM subscription_row), 'key', (SELECT data FROM key_row) ); """ print(query) PY )" remote_pg_query "$sql" > "$output_path" } python3 - "$ART/00-local-key-source.json" "$key_source" "$provider_id" "$upstream_key" <<'PY' import json, sys, pathlib path, source, provider_id, key = sys.argv[1:5] pathlib.Path(path).write_text(json.dumps({ 'source': source, 'provider_id': provider_id, 'upstream_key_prefix': key[:12], 'upstream_key_suffix': key[-6:], }, ensure_ascii=False, indent=2), encoding='utf-8') PY crm_token="${CRM_ADMIN_TOKEN:-}" if [[ -z "$crm_token" ]]; then crm_token="$(ssh_cmd "grep ^SUB2API_CRM_ADMIN_TOKEN= /home/ubuntu/sub2api-cn-relay-manager/.env.remote | cut -d= -f2-")" crm_token="${crm_token##*$'\n'}" fi host_bearer_token="${HOST_BEARER_TOKEN:-}" if [[ -z "$host_bearer_token" ]]; then host_bearer_token="$(fetch_remote_host_bearer_token)" host_bearer_token="${host_bearer_token##*$'\n'}" fi admin_uid="$(ssh_cmd "sudo -n docker exec $REMOTE_PG_CONTAINER_Q psql -U sub2api -d sub2api -Atc \"select id from users where role='admin' order by id asc limit 1;\"")" admin_uid="${admin_uid##*$'\n'}" sub_uid="$(remote_pg_query "select id from users where email like 'relay-sub-%@sub2api.local' and not exists (select 1 from user_subscriptions s where s.user_id=users.id and s.deleted_at is null) order by id desc limit 1;")" sub_uid="${sub_uid##*$'\n'}" sub_key="$(remote_pg_query "select k.key from users u join api_keys k on k.user_id=u.id where u.email like 'relay-sub-%@sub2api.local' and not exists (select 1 from user_subscriptions s where s.user_id=u.id and s.deleted_at is null) order by u.id desc limit 1;")" sub_key="${sub_key##*$'\n'}" if [[ -z "$sub_uid" || -z "$sub_key" ]]; then fresh_seed="$(python3 - <<'PY' import secrets, time print(f"{int(time.time())}-{secrets.token_hex(4)}") PY )" fresh_email="relay-sub-${fresh_seed}@sub2api.local" fresh_username="relay-sub-${fresh_seed}" fresh_key="sk-${fresh_seed}" create_user_sql="$(python3 - "$fresh_email" "$fresh_username" "$fresh_key" <<'PY' import sys email, username, api_key = sys.argv[1:4] def sql_quote(value: str) -> str: return "'" + value.replace("'", "''") + "'" print(f''' WITH seed AS ( SELECT password_hash FROM users WHERE role = 'admin' ORDER BY id ASC LIMIT 1 ), ins_user AS ( INSERT INTO users ( email, password_hash, role, balance, concurrency, status, username, notes, wechat, totp_secret_encrypted, totp_enabled, balance_notify_enabled, balance_notify_threshold, balance_notify_extra_emails, balance_notify_threshold_type, total_recharged, signup_source, rpm_limit ) SELECT {sql_quote(email)}, password_hash, 'user', 10, 5, 'active', {sql_quote(username)}, 'hermes remote subscription validation', '', '', false, true, NULL, '[]', 'fixed', 0, 'email', 0 FROM seed RETURNING id ), ins_key AS ( INSERT INTO api_keys ( user_id, key, name, group_id, status, quota, quota_used, rate_limit_5h, rate_limit_1d, rate_limit_7d, usage_5h, usage_1d, usage_7d ) SELECT id, {sql_quote(api_key)}, {sql_quote(username + '-key')}, NULL, 'active', 0, 0, 0, 0, 0, 0, 0, 0 FROM ins_user RETURNING user_id, key ) SELECT user_id, key FROM ins_key; '''.strip()) PY )" read -r sub_uid sub_key < "$ART/01a-create-host.json" else crm_curl_json POST "/api/hosts" "$create_host_payload" > "$ART/01a-create-host.json" fi payload="$(python3 - "$CRM_HOST_BASE" "$host_bearer_token" "$PACK_PATH" "$provider_id" "$upstream_key" "$sub_key" "$sub_uid" "$SUBSCRIPTION_DAYS" <<'PY' import json, sys host_base, host_bearer_token, pack_path, provider_id, upstream_key, sub_key, sub_uid, subscription_days = sys.argv[1:9] print(json.dumps({ 'host_base_url': host_base, 'host_bearer_token': host_bearer_token, 'pack_path': pack_path, 'provider_id': provider_id, 'keys': [upstream_key], 'mode': 'partial', 'access_mode': 'subscription', 'access_api_key': sub_key, 'subscription_days': int(subscription_days), 'subscription_users': [sub_uid], }, ensure_ascii=False)) PY )" curl -sS -D "$ART/02-import.headers.txt" -o "$ART/03-import.body.json" -X POST \ -H "Authorization: Bearer $crm_token" \ -H 'Content-Type: application/json' \ "$CRM_BASE/api/providers/$provider_id/import" \ -d "$payload" batch_id="$(python3 - "$ART/03-import.body.json" <<'PY' import json, sys, pathlib obj=json.loads(pathlib.Path(sys.argv[1]).read_text()) print(obj['batch_id']) PY )" crm_curl_json GET "/api/import-batches/$batch_id" > "$ART/04-batch-detail-initial.json" subscription_group_id="$(python3 - "$ART/03-import.body.json" "$ART/04-batch-detail-initial.json" <<'PY' import json, pathlib, sys import_obj = json.loads(pathlib.Path(sys.argv[1]).read_text()) batch_obj = json.loads(pathlib.Path(sys.argv[2]).read_text()) group = import_obj.get('group') or {} if group.get('id'): print(group['id']) raise SystemExit(0) for item in batch_obj.get('managed_resources', []): if item.get('ResourceType') == 'group': print(item.get('HostResourceID', '')) raise SystemExit(0) raise SystemExit('missing managed group in import response and batch detail') PY )" auth_cache_key="$(build_api_key_auth_cache_key "$sub_key")" balance_cache_key="$(build_user_balance_cache_key "$sub_uid")" subscription_cache_key="$(build_subscription_billing_cache_key "$sub_uid" "$subscription_group_id")" prep_sql="$(build_subscription_access_prep_sql "$sub_uid" "$sub_key" "$subscription_group_id" "$MIN_BALANCE" "$SUBSCRIPTION_DAYS" "$admin_uid" "$SUBSCRIPTION_NOTES")" python3 - "$ART/05-subscription-access-prep.sql" "$prep_sql" <<'PY' import pathlib, sys pathlib.Path(sys.argv[1]).write_text(sys.argv[2], encoding='utf-8') PY remote_pg_exec "$prep_sql" > "$ART/06-subscription-access-prep.psql.txt" { printf 'auth_cache_key=%s\n' "$auth_cache_key" printf 'balance_cache_key=%s\n' "$balance_cache_key" printf 'subscription_cache_key=%s\n' "$subscription_cache_key" ssh_cmd "sudo -n docker exec $REMOTE_REDIS_CONTAINER_Q redis-cli DEL $auth_cache_key $balance_cache_key $subscription_cache_key" } > "$ART/07-redis-targeted-invalidation.txt" remote_fetch_group_state "$subscription_group_id" "$sub_uid" "$sub_key" "$ART/08-subscription-group-state.json" python3 - "$ART/01-runtime-context.json" "$CRM_BASE" "$HOST_BASE" "$CRM_HOST_BASE" "$provider_id" "$sub_uid" "$sub_key" "$subscription_group_id" "$admin_uid" <<'PY' import json, sys, pathlib path, crm, host, crm_host, provider_id, sub_uid, sub_key, group_id, admin_uid = sys.argv[1:10] pathlib.Path(path).write_text(json.dumps({ 'crm_base': crm, 'host_base': host, 'crm_host_base': crm_host, 'provider_id': provider_id, 'subscription_user_id': sub_uid, 'subscription_user_key_prefix': sub_key[:12], 'subscription_group_id': group_id, 'admin_user_id': admin_uid, }, ensure_ascii=False, indent=2), encoding='utf-8') PY probe_payload="$(python3 - "$model_name" <<'PY' import json, sys print(json.dumps({ 'model': sys.argv[1], 'messages': [{'role':'user','content':'ping'}], 'max_tokens': 8, 'temperature': 0, }, ensure_ascii=False)) PY )" ssh_cmd "curl -sS -D /tmp/models_headers.txt -o /tmp/models_body.json -H 'Authorization: Bearer $sub_key' $HOST_BASE/v1/models" ssh_cmd "cat /tmp/models_headers.txt" > "$ART/09-models.headers.txt" ssh_cmd "cat /tmp/models_body.json" > "$ART/10-models.body.json" ssh_cmd "curl -sS -D /tmp/chat_headers.txt -o /tmp/chat_body.json -H 'Authorization: Bearer $sub_key' -H 'Content-Type: application/json' $HOST_BASE/v1/chat/completions -d $(printf %q "$probe_payload")" ssh_cmd "cat /tmp/chat_headers.txt" > "$ART/11-chat.headers.txt" ssh_cmd "cat /tmp/chat_body.json" > "$ART/12-chat.body.json" crm_curl_json GET "/api/providers/$provider_id/status" > "$ART/13-provider-status.json" crm_curl_json GET "/api/providers/$provider_id/access/status" > "$ART/14-access-status.json" preview_payload="$(python3 - "$provider_id" <<'PY' import json, sys print(json.dumps({'provider_id': sys.argv[1], 'mode': 'subscription'}, ensure_ascii=False)) PY )" crm_curl_json POST "/api/providers/$provider_id/access/preview" "$preview_payload" > "$ART/15-access-preview.json" crm_curl_json GET "/api/import-batches/$batch_id" > "$ART/16-batch-detail-final.json" python3 - "$ART" "$provider_id" "$batch_id" "$subscription_group_id" "$model_name" <<'PY' import json, pathlib, sys art=pathlib.Path(sys.argv[1]) provider_id=sys.argv[2] batch_id=int(sys.argv[3]) subscription_group_id=sys.argv[4] expected_model=sys.argv[5] import_obj=json.loads((art/'03-import.body.json').read_text()) models_obj=json.loads((art/'10-models.body.json').read_text()) access_status=json.loads((art/'14-access-status.json').read_text()) preview=json.loads((art/'15-access-preview.json').read_text()) models_headers=(art/'09-models.headers.txt').read_text() chat_headers=(art/'11-chat.headers.txt').read_text() models=[] for item in models_obj.get('data') or []: model_id = item.get('id') if isinstance(model_id, str) and model_id: models.append(model_id) summary={ 'artifact_dir': str(art), 'provider_id': provider_id, 'batch_id': batch_id, 'batch_status': import_obj.get('batch_status'), 'access_status_from_import': import_obj.get('access_status'), 'provider_status_from_import': import_obj.get('provider_status'), 'direct_models_http200': '200 OK' in models_headers, 'direct_models_has_expected_model': expected_model in models, 'direct_models': models, 'direct_chat_http200': '200 OK' in chat_headers, 'latest_access_status': access_status.get('latest_access_status') or access_status.get('batch_access_status'), 'preview_available': preview.get('available'), 'accepted_keys_count': import_obj.get('accepted_keys_count'), 'subscription_group_id': subscription_group_id, 'import_group_id': (import_obj.get('group') or {}).get('id'), } print(json.dumps(summary, ensure_ascii=False)) PY