237 lines
8.0 KiB
Bash
Executable File
237 lines
8.0 KiB
Bash
Executable File
#!/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
|