2026-06-05 11:07:50 +08:00
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR = " $( cd " $( dirname " ${ BASH_SOURCE [0] } " ) /../.. " && pwd ) "
2026-06-05 19:58:02 +08:00
TS = " ${ TS :- $( date +%Y%m%d_%H%M%S) } "
2026-06-05 11:07:50 +08:00
ARTIFACT_DIR = " ${ ARTIFACT_DIR :- $ROOT_DIR /artifacts/user-key-self-service/ ${ TS } } "
CRM_BASE = " ${ CRM_BASE :- https : //sub.tksea.top/portal-admin-api } "
2026-06-05 19:58:02 +08:00
USER_CHAT_BASE = " ${ USER_CHAT_BASE :- } "
CHAT_MODEL = " ${ CHAT_MODEL :- gpt -5.4 } "
USER_SUBJECT_ID = " ${ USER_SUBJECT_ID :- } "
USER_AUTH_TOKEN = " ${ USER_AUTH_TOKEN :- } "
2026-06-05 11:07:50 +08:00
2026-06-05 19:58:02 +08:00
mkdir -p " $ARTIFACT_DIR "
2026-06-05 11:07:50 +08:00
2026-06-05 19:58:02 +08:00
info( ) { printf 'INFO: %s\n' " $* " ; }
ok( ) { printf 'OK: %s\n' " $* " ; }
warn( ) { printf 'WARN: %s\n' " $* " >& 2; }
die( ) { printf 'FATAL: %s\n' " $* " >& 2; exit 1; }
2026-06-05 11:07:50 +08:00
2026-06-05 19:58:02 +08:00
usage( ) {
cat <<'EOF'
usage: verify_user_key_self_service.sh [ --help| --env-check| --run]
2026-06-05 11:07:50 +08:00
2026-06-05 19:58:02 +08:00
Modes:
--help 显示帮助
--env-check 仅检查 CRM / chat 入口与认证前置
--run 执行真实 user-key 闭环: create -> list -> get -> reset -> chat
2026-06-05 11:07:50 +08:00
2026-06-05 19:58:02 +08:00
Required env for --run:
CRM_BASE CRM API base, e.g. https://sub.tksea.top/portal-admin-api
USER_CHAT_BASE 最终 user-key 调用入口 base, e.g. https://sub.tksea.top
CHAT_MODEL chat 模型名, default: gpt-5.4
2026-06-05 11:07:50 +08:00
2026-06-05 19:58:02 +08:00
Authentication for /api/keys endpoints ( choose one) :
USER_SUBJECT_ID 通过 X-Portal-Subject 头注入 subject( 联合部署/受信入口)
USER_AUTH_TOKEN 通过 Authorization: Bearer <token> 走用户链路
2026-06-05 11:07:50 +08:00
2026-06-05 19:58:02 +08:00
Artifacts:
2026-06-05 11:07:50 +08:00
artifacts/user-key-self-service/<timestamp>/
2026-06-05 19:58:02 +08:00
- 00-env.json
- 10-create.headers.txt / 10-create.body.json
- 11-list.headers.txt / 11-list.body.json
- 12-get.headers.txt / 12-get.body.json
- 13-reset.headers.txt / 13-reset.body.json
- 20-chat.headers.txt / 20-chat.body.json
- 99-summary.json
EOF
2026-06-05 11:07:50 +08:00
}
2026-06-05 19:58:02 +08:00
build_auth_args( ) {
if [ [ -n " $USER_AUTH_TOKEN " ] ] ; then
printf '%s\n' "-H" " Authorization: Bearer $USER_AUTH_TOKEN "
return 0
fi
if [ [ -n " $USER_SUBJECT_ID " ] ] ; then
printf '%s\n' "-H" " X-Portal-Subject: $USER_SUBJECT_ID "
return 0
fi
return 1
}
2026-06-05 11:07:50 +08:00
2026-06-05 19:58:02 +08:00
curl_json_with_capture( ) {
local method = " $1 "
local url = " $2 "
local headers_file = " $3 "
local body_file = " $4 "
local payload = " ${ 5 :- } "
local -a args
args = ( curl -sS --noproxy '*' -D " $headers_file " -o " $body_file " -X " $method " )
while IFS = read -r line; do
args += ( " $line " )
done < <( build_auth_args)
if [ [ -n " $payload " ] ] ; then
args += ( -H 'Content-Type: application/json' -d " $payload " )
2026-06-05 11:07:50 +08:00
fi
2026-06-05 19:58:02 +08:00
args += ( " $url " )
" ${ args [@] } "
}
curl_chat_with_capture( ) {
local plaintext_key = " $1 "
local payload = " $2 "
local headers_file = " $3 "
local body_file = " $4 "
curl -sS --noproxy '*' \
-D " $headers_file " \
-o " $body_file " \
-H " Authorization: Bearer $plaintext_key " \
-H 'Content-Type: application/json' \
-X POST \
-d " $payload " \
" ${ USER_CHAT_BASE %/ } /v1/chat/completions "
}
2026-06-05 11:07:50 +08:00
2026-06-05 19:58:02 +08:00
extract_http_code( ) {
local headers_file = " $1 "
awk 'toupper($1) ~ /^HTTP\// { code=$2 } END { print code }' " $headers_file "
}
json_get( ) {
local file = " $1 "
local expr = " $2 "
python3 - " $file " " $expr " <<'PY'
import json, sys
path, expr = sys.argv[ 1:3]
value = json.load( open( path, 'r' , encoding = 'utf-8' ) )
for part in expr.split( '.' ) :
if isinstance( value, dict) :
value = value.get( part)
else :
raise SystemExit( 2)
print( "" if value is None else value)
PY
}
cmd_env_check( ) {
local crm_health = "unreachable"
if crm_health = $( curl -sS --noproxy '*' " ${ CRM_BASE %/ } /healthz " 2>/dev/null) ; then
:
else
crm_health = "unreachable"
fi
local chat_health = "unset"
if [ [ -n " $USER_CHAT_BASE " ] ] ; then
if chat_health = $( curl -sS --noproxy '*' " ${ USER_CHAT_BASE %/ } /healthz " 2>/dev/null) ; then
:
2026-06-05 11:07:50 +08:00
else
2026-06-05 19:58:02 +08:00
chat_health = "unreachable"
2026-06-05 11:07:50 +08:00
fi
fi
2026-06-05 19:58:02 +08:00
OUT_PATH = " $ARTIFACT_DIR /00-env.json " \
CRM_BASE_PY = " $CRM_BASE " \
CRM_HEALTH = " $crm_health " \
USER_CHAT_BASE_PY = " $USER_CHAT_BASE " \
CHAT_HEALTH = " $chat_health " \
HAS_SUBJECT_ID = " $USER_SUBJECT_ID " \
HAS_AUTH_TOKEN = " $USER_AUTH_TOKEN " \
python3 - <<'PY'
import json, os
out = {
"crm_base" : os.environ[ "CRM_BASE_PY" ] ,
"crm_health" : os.environ[ "CRM_HEALTH" ] ,
"user_chat_base" : os.environ[ "USER_CHAT_BASE_PY" ] ,
"user_chat_health" : os.environ[ "CHAT_HEALTH" ] ,
"has_user_subject_id" : bool( os.environ[ "HAS_SUBJECT_ID" ] ) ,
"has_user_auth_token" : bool( os.environ[ "HAS_AUTH_TOKEN" ] ) ,
}
with open( os.environ[ "OUT_PATH" ] , "w" , encoding = "utf-8" ) as fh:
json.dump( out, fh, ensure_ascii = False, indent = 2)
PY
if [ [ " $crm_health " = = "ok" ] ] ; then ok "CRM healthz=ok" ; else warn " CRM healthz= $crm_health " ; fi
if [ [ -n " $USER_CHAT_BASE " ] ] ; then info " user chat health= $chat_health " ; fi
ok " env summary: $ARTIFACT_DIR /00-env.json "
}
2026-06-05 11:07:50 +08:00
2026-06-05 19:58:02 +08:00
cmd_run( ) {
cmd_env_check
[ [ -n " $USER_CHAT_BASE " ] ] || die "USER_CHAT_BASE is required for --run"
if ! build_auth_args >/dev/null; then
die "set USER_SUBJECT_ID or USER_AUTH_TOKEN for /api/keys authentication"
2026-06-05 11:07:50 +08:00
fi
2026-06-05 19:58:02 +08:00
local create_payload create_code key_id plaintext_key masked_preview create_body
create_payload = '{"logical_group_id":"gpt-shared","display_name":"acceptance-key","allowed_models":["' " $CHAT_MODEL " '"]}'
curl_json_with_capture POST " ${ CRM_BASE %/ } /api/keys " " $ARTIFACT_DIR /10-create.headers.txt " " $ARTIFACT_DIR /10-create.body.json " " $create_payload " >/dev/null
create_code = " $( extract_http_code " $ARTIFACT_DIR /10-create.headers.txt " ) "
[ [ " $create_code " = = "201" ] ] || die " create key failed: HTTP $create_code "
key_id = " $( json_get " $ARTIFACT_DIR /10-create.body.json " 'key.key_id' ) "
plaintext_key = " $( json_get " $ARTIFACT_DIR /10-create.body.json " 'plaintext_key' ) "
masked_preview = " $( json_get " $ARTIFACT_DIR /10-create.body.json " 'key.masked_preview' ) "
[ [ -n " $key_id " && -n " $plaintext_key " ] ] || die "create key response missing key_id/plaintext_key"
ok " create key -> HTTP 201, key_id= $key_id "
curl_json_with_capture GET " ${ CRM_BASE %/ } /api/keys " " $ARTIFACT_DIR /11-list.headers.txt " " $ARTIFACT_DIR /11-list.body.json " >/dev/null
[ [ " $( extract_http_code " $ARTIFACT_DIR /11-list.headers.txt " ) " = = "200" ] ] || die "list keys failed"
ok "list keys -> HTTP 200"
curl_json_with_capture GET " ${ CRM_BASE %/ } /api/keys/ ${ key_id } " " $ARTIFACT_DIR /12-get.headers.txt " " $ARTIFACT_DIR /12-get.body.json " >/dev/null
[ [ " $( extract_http_code " $ARTIFACT_DIR /12-get.headers.txt " ) " = = "200" ] ] || die "get key failed"
ok "get key -> HTTP 200"
curl_json_with_capture POST " ${ CRM_BASE %/ } /api/keys/ ${ key_id } /reset " " $ARTIFACT_DIR /13-reset.headers.txt " " $ARTIFACT_DIR /13-reset.body.json " '{}' >/dev/null
[ [ " $( extract_http_code " $ARTIFACT_DIR /13-reset.headers.txt " ) " = = "200" ] ] || die "reset key failed"
plaintext_key = " $( json_get " $ARTIFACT_DIR /13-reset.body.json " 'plaintext_key' ) "
masked_preview = " $( json_get " $ARTIFACT_DIR /13-reset.body.json " 'masked_preview' ) "
[ [ -n " $plaintext_key " && -n " $masked_preview " ] ] || die "reset response missing plaintext_key/masked_preview"
ok "reset key -> HTTP 200"
local chat_payload chat_code
chat_payload = '{"model":"' " $CHAT_MODEL " '","messages":[{"role":"user","content":"ping"}],"max_tokens":16,"temperature":0}'
curl_chat_with_capture " $plaintext_key " " $chat_payload " " $ARTIFACT_DIR /20-chat.headers.txt " " $ARTIFACT_DIR /20-chat.body.json " >/dev/null
chat_code = " $( extract_http_code " $ARTIFACT_DIR /20-chat.headers.txt " ) "
[ [ " $chat_code " = = "200" ] ] || die " user chat failed: HTTP $chat_code "
ok "user chat -> HTTP 200"
python3 - " $ARTIFACT_DIR /99-summary.json " <<PY
import json
summary = {
"crm_base" : ${ CRM_BASE @Q } ,
"user_chat_base" : ${ USER_CHAT_BASE @Q } ,
"chat_model" : ${ CHAT_MODEL @Q } ,
"key_id" : ${ key_id @Q } ,
"masked_preview" : ${ masked_preview @Q } ,
"create_http" : int( ${ create_code @Q } ) ,
"list_http" : 200,
"get_http" : 200,
"reset_http" : 200,
"chat_http" : 200,
"checks" : {
"create_returns_plaintext_once" : True,
"list_returns_200" : True,
"get_returns_200" : True,
"reset_returns_new_plaintext" : True,
"user_chat_200" : True,
}
2026-06-05 11:07:50 +08:00
}
2026-06-05 19:58:02 +08:00
json.dump( summary, open( ${ ARTIFACT_DIR @Q } + "/99-summary.json" , "w" ) , ensure_ascii = False, indent = 2)
PY
cat " $ARTIFACT_DIR /99-summary.json "
2026-06-05 11:07:50 +08:00
}
case " ${ 1 :- --help } " in
2026-06-05 19:58:02 +08:00
--help| -h)
usage
; ;
--env-check)
cmd_env_check
; ;
--run)
cmd_run
; ;
*)
usage
exit 1
; ;
2026-06-05 11:07:50 +08:00
esac