2026-05-19 13:58:03 +08:00
#!/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 } "
2026-05-19 20:21:21 +08:00
CRM_HOST_BASE = " ${ CRM_HOST_BASE :- $HOST_BASE } "
2026-05-20 22:09:40 +08:00
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 } "
2026-05-19 13:58:03 +08:00
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 "
2026-05-20 22:09:40 +08:00
REMOTE_PG_CONTAINER_Q = " $( printf '%q' " $REMOTE_PG_CONTAINER " ) "
REMOTE_REDIS_CONTAINER_Q = " $( printf '%q' " $REMOTE_REDIS_CONTAINER " ) "
2026-05-19 13:58:03 +08:00
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 "
}
2026-05-20 22:09:40 +08:00
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"
}
2026-05-19 13:58:03 +08:00
remote_pg_exec( ) {
local sql = " $1 "
local encoded
encoded = " $( printf '%s' " $sql " | base64 -w0) "
2026-05-20 22:09:40 +08:00
ssh_cmd " printf '%s' ' $encoded ' | base64 -d | sudo -n docker exec -i $REMOTE_PG_CONTAINER_Q psql -U sub2api -d sub2api "
2026-05-19 13:58:03 +08:00
}
2026-05-19 20:21:21 +08:00
remote_pg_query( ) {
local sql = " $1 "
local encoded
encoded = " $( printf '%s' " $sql " | base64 -w0) "
2026-05-20 22:09:40 +08:00
ssh_cmd " printf '%s' ' $encoded ' | base64 -d | sudo -n docker exec -i $REMOTE_PG_CONTAINER_Q psql -U sub2api -d sub2api -At -F $'\t' "
2026-05-19 20:21:21 +08:00
}
2026-05-19 13:58:03 +08:00
remote_fetch_group_state( ) {
local group_id = " $1 "
local user_id = " $2 "
local api_key = " $3 "
local output_path = " $4 "
2026-05-20 22:09:40 +08:00
local sql
sql = " $( python3 - " $group_id " " $user_id " " $api_key " <<'PY'
import sys
2026-05-19 13:58:03 +08:00
group_id, user_id, api_key = sys.argv[ 1:4]
2026-05-20 22:09:40 +08:00
api_key_literal = "'" + api_key.replace( "'" , "''" ) + "'"
2026-05-19 13:58:03 +08:00
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 (
2026-05-20 22:09:40 +08:00
SELECT row_to_json( k) AS data FROM api_keys k WHERE k.key = { api_key_literal}
2026-05-19 13:58:03 +08:00
)
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
) "
2026-05-20 22:09:40 +08:00
remote_pg_query " $sql " > " $output_path "
2026-05-19 13:58:03 +08:00
}
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
2026-05-20 22:09:40 +08:00
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;\" " ) "
2026-05-19 13:58:03 +08:00
admin_uid = " ${ admin_uid ##* $'\n' } "
2026-05-19 20:21:21 +08:00
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;" ) "
2026-05-19 13:58:03 +08:00
sub_uid = " ${ sub_uid ##* $'\n' } "
2026-05-19 20:21:21 +08:00
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;" ) "
2026-05-19 13:58:03 +08:00
sub_key = " ${ sub_key ##* $'\n' } "
2026-05-19 20:21:21 +08:00
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( "'" , "''" ) + "'"
2026-05-19 13:58:03 +08:00
2026-05-19 20:21:21 +08:00
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 <<EOF
$( remote_pg_query " $create_user_sql " )
EOF
fi
python3 - " $ART /01-runtime-context.json " " $CRM_BASE " " $HOST_BASE " " $CRM_HOST_BASE " " $provider_id " " $sub_uid " " $sub_key " <<'PY'
2026-05-19 13:58:03 +08:00
import json, sys, pathlib
2026-05-19 20:21:21 +08:00
path, crm, host, crm_host, provider_id, sub_uid, sub_key = sys.argv[ 1:8]
2026-05-19 13:58:03 +08:00
pathlib.Path( path) .write_text( json.dumps( {
'crm_base' : crm,
'host_base' : host,
2026-05-19 20:21:21 +08:00
'crm_host_base' : crm_host,
2026-05-19 13:58:03 +08:00
'provider_id' : provider_id,
'subscription_user_id' : sub_uid,
'subscription_user_key_prefix' : sub_key[ :12] ,
} , ensure_ascii = False, indent = 2) , encoding = 'utf-8' )
PY
2026-05-20 22:09:40 +08:00
create_host_payload = " $( python3 - " $HOST_NAME " " $CRM_HOST_BASE " " $host_bearer_token " <<'PY'
import json, sys
name, base_url, bearer_token = sys.argv[ 1:4]
print( json.dumps( {
'name' : name,
'base_url' : base_url,
'auth' : { 'type' : 'bearer' , 'token' : bearer_token} ,
} , ensure_ascii = False) )
PY
) "
hosts_payload = " $( crm_curl_json GET "/api/hosts" ) "
existing_host_json = " $( printf '%s' " $hosts_payload " | python3 -c ' import json, sys
base_url = sys.argv[ 1]
payload = json.load( sys.stdin)
for host in payload.get( "hosts" , [ ] ) :
if host.get( "base_url" ) = = base_url:
print( json.dumps( host, ensure_ascii = False) )
break' " $CRM_HOST_BASE " ) "
if [ [ -n " $existing_host_json " ] ] ; then
printf '%s\n' " $existing_host_json " > " $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'
2026-05-19 13:58:03 +08:00
import json, sys
2026-05-20 22:09:40 +08:00
host_base, host_bearer_token, pack_path, provider_id, upstream_key, sub_key, sub_uid, subscription_days = sys.argv[ 1:9]
2026-05-19 13:58:03 +08:00
print( json.dumps( {
'host_base_url' : host_base,
2026-05-20 22:09:40 +08:00
'host_bearer_token' : host_bearer_token,
2026-05-19 13:58:03 +08:00
'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
) "
2026-05-20 22:09:40 +08:00
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 "
2026-05-19 13:58:03 +08:00
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
) "
2026-05-20 22:09:40 +08:00
crm_curl_json GET " /api/import-batches/ $batch_id " > " $ART /04-batch-detail-initial.json "
2026-05-19 20:21:21 +08:00
subscription_group_id = " $( python3 - " $ART /03-import.body.json " " $ART /04-batch-detail-initial.json " <<'PY'
2026-05-19 13:58:03 +08:00
import json, pathlib, sys
2026-05-19 20:21:21 +08:00
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' , [ ] ) :
2026-05-19 13:58:03 +08:00
if item.get( 'ResourceType' ) = = 'group' :
print( item.get( 'HostResourceID' , '' ) )
2026-05-19 20:21:21 +08:00
raise SystemExit( 0)
raise SystemExit( 'missing managed group in import response and batch detail' )
2026-05-19 13:58:03 +08:00
PY
) "
2026-05-19 20:21:21 +08:00
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 " ) "
2026-05-19 13:58:03 +08:00
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 "
2026-05-19 20:21:21 +08:00
{
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 "
2026-05-20 22:09:40 +08:00
ssh_cmd " sudo -n docker exec $REMOTE_REDIS_CONTAINER_Q redis-cli DEL $auth_cache_key $balance_cache_key $subscription_cache_key "
2026-05-19 20:21:21 +08:00
} > " $ART /07-redis-targeted-invalidation.txt "
2026-05-19 13:58:03 +08:00
remote_fetch_group_state " $subscription_group_id " " $sub_uid " " $sub_key " " $ART /08-subscription-group-state.json "
2026-05-19 20:21:21 +08:00
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'
2026-05-19 13:58:03 +08:00
import json, sys, pathlib
2026-05-19 20:21:21 +08:00
path, crm, host, crm_host, provider_id, sub_uid, sub_key, group_id, admin_uid = sys.argv[ 1:10]
2026-05-19 13:58:03 +08:00
pathlib.Path( path) .write_text( json.dumps( {
'crm_base' : crm,
'host_base' : host,
2026-05-19 20:21:21 +08:00
'crm_host_base' : crm_host,
2026-05-19 13:58:03 +08:00
'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
) "
2026-05-20 22:09:40 +08:00
ssh_cmd " curl -sS -D /tmp/models_headers.txt -o /tmp/models_body.json -H 'Authorization: Bearer $sub_key ' $HOST_BASE /v1/models "
2026-05-19 20:21:21 +08:00
ssh_cmd "cat /tmp/models_headers.txt" > " $ART /09-models.headers.txt "
ssh_cmd "cat /tmp/models_body.json" > " $ART /10-models.body.json "
2026-05-20 22:09:40 +08:00
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 " ) "
2026-05-19 20:21:21 +08:00
ssh_cmd "cat /tmp/chat_headers.txt" > " $ART /11-chat.headers.txt "
ssh_cmd "cat /tmp/chat_body.json" > " $ART /12-chat.body.json "
2026-05-19 13:58:03 +08:00
2026-05-20 22:09:40 +08:00
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 "
2026-05-19 13:58:03 +08:00
preview_payload = " $( python3 - " $provider_id " <<'PY'
import json, sys
print( json.dumps( { 'provider_id' : sys.argv[ 1] , 'mode' : 'subscription' } , ensure_ascii = False) )
PY
) "
2026-05-20 22:09:40 +08:00
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 "
2026-05-19 13:58:03 +08:00
2026-05-19 20:21:21 +08:00
python3 - " $ART " " $provider_id " " $batch_id " " $subscription_group_id " " $model_name " <<'PY'
2026-05-19 13:58:03 +08:00
import json, pathlib, sys
2026-05-20 22:09:40 +08:00
2026-05-19 13:58:03 +08:00
art = pathlib.Path( sys.argv[ 1] )
provider_id = sys.argv[ 2]
batch_id = int( sys.argv[ 3] )
2026-05-19 20:21:21 +08:00
subscription_group_id = sys.argv[ 4]
expected_model = sys.argv[ 5]
2026-05-19 13:58:03 +08:00
import_obj = json.loads( ( art/'03-import.body.json' ) .read_text( ) )
2026-05-19 20:21:21 +08:00
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)
2026-05-19 13:58:03 +08:00
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' ) ,
2026-05-19 20:21:21 +08:00
'direct_models_http200' : '200 OK' in models_headers,
'direct_models_has_expected_model' : expected_model in models,
'direct_models' : models,
2026-05-19 13:58:03 +08:00
'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' ) ,
2026-05-19 20:21:21 +08:00
'subscription_group_id' : subscription_group_id,
'import_group_id' : ( import_obj.get( 'group' ) or { } ) .get( 'id' ) ,
2026-05-19 13:58:03 +08:00
}
print( json.dumps( summary, ensure_ascii = False) )
PY