Files
sub2api-cn-relay-manager/scripts/acceptance/verify_user_key_self_service.sh

237 lines
8.0 KiB
Bash
Raw Normal View History

#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
TS="${TS:-$(date +%Y%m%d_%H%M%S)}"
ARTIFACT_DIR="${ARTIFACT_DIR:-$ROOT_DIR/artifacts/user-key-self-service/${TS}}"
CRM_BASE="${CRM_BASE:-https://sub.tksea.top/portal-admin-api}"
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:-}"
mkdir -p "$ARTIFACT_DIR"
info() { printf 'INFO: %s\n' "$*"; }
ok() { printf 'OK: %s\n' "$*"; }
warn() { printf 'WARN: %s\n' "$*" >&2; }
die() { printf 'FATAL: %s\n' "$*" >&2; exit 1; }
usage() {
cat <<'EOF'
usage: verify_user_key_self_service.sh [--help|--env-check|--run]
Modes:
--help 显示帮助
--env-check 仅检查 CRM / chat 入口与认证前置
--run 执行真实 user-key 闭环create -> list -> get -> reset -> chat
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
Authentication for /api/keys endpoints (choose one):
USER_SUBJECT_ID 通过 X-Portal-Subject 头注入 subject联合部署/受信入口)
USER_AUTH_TOKEN 通过 Authorization: Bearer <token> 走用户链路
Artifacts:
artifacts/user-key-self-service/<timestamp>/
- 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
}
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
}
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")
fi
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"
}
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
:
else
chat_health="unreachable"
fi
fi
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"
}
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"
fi
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,
}
}
json.dump(summary, open(${ARTIFACT_DIR@Q} + "/99-summary.json", "w"), ensure_ascii=False, indent=2)
PY
cat "$ARTIFACT_DIR/99-summary.json"
}
case "${1:---help}" in
--help|-h)
usage
;;
--env-check)
cmd_env_check
;;
--run)
cmd_run
;;
*)
usage
exit 1
;;
esac