Files
sub2api-cn-relay-manager/scripts/real_host_acceptance.sh
2026-05-21 15:45:55 +08:00

334 lines
11 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TIMESTAMP="$(date +%Y%m%d_%H%M%S)"
ARTIFACT_DIR="${ARTIFACT_DIR:-$ROOT_DIR/artifacts/real-host-acceptance/$TIMESTAMP}"
DRY_RUN="${DRY_RUN:-0}"
SKIP_ROLLBACK="${SKIP_ROLLBACK:-0}"
require_var() {
local name="$1"
if [[ -z "${!name:-}" ]]; then
echo "missing required env: $name" >&2
exit 1
fi
}
json_get() {
local key="$1"
python3 -c 'import json, sys
key = sys.argv[1]
data = json.load(sys.stdin)
value = data
for part in key.split("."):
if isinstance(value, dict):
value = value.get(part)
else:
value = None
break
if value is None:
sys.exit(2)
if isinstance(value, (dict, list)):
print(json.dumps(value, ensure_ascii=False))
else:
print(value)
' "$key"
}
save_json() {
local name="$1"
local payload="$2"
mkdir -p "$ARTIFACT_DIR"
printf '%s\n' "$payload" > "$ARTIFACT_DIR/$name.json"
}
write_checklist_guide() {
mkdir -p "$ARTIFACT_DIR"
cat > "$ARTIFACT_DIR/00-artifact-guide.txt" <<EOF
真实宿主验收产物 -> 速查清单对应
清单 1环境 / host 前置)
- 01-create-host.json
- 02-probe-host.json
清单 2channel 宿主契约 / 导入落库)
- 03-install-pack.json
- 04-preview-import.json
- 05-import.json
- 05a-batch-detail-pre-access.json若拿到 batch_id 且非 dry-run
- 08-provider-status.json
- 09-reconcile.json
- 10-batch-detail.json若拿到 batch_id 且非 dry-run
清单 3access / key 闭环状态)
- 06-access-preview.json
- 07-access-status.json
清单 4必须分层留证据不可混用
- account 视角:由 AFTER_IMPORT_HOOK_COMMAND 额外落证据,例如 GET /api/v1/admin/accounts/:id/models
- 普通用户 / managed key 视角:由 AFTER_IMPORT_HOOK_COMMAND 额外落证据,例如 GET /v1/models
- completion 视角:由 AFTER_IMPORT_HOOK_COMMAND 额外落证据,例如 POST /v1/chat/completions
红线:
- /api/v1/admin/accounts/:id/models 正确 ≠ /v1/models 正确
- /v1/models 正确 ≠ /v1/chat/completions 正确
- admin API 成功 ≠ 普通用户链路成功
当前 hook 配置:$( [[ -n "$AFTER_IMPORT_HOOK_COMMAND" ]] && printf 'enabled' || printf 'disabled' )
EOF
}
print_artifact_summary() {
echo "artifact guide: $ARTIFACT_DIR/00-artifact-guide.txt"
echo "checklist import evidence: 04-preview-import.json 05-import.json 05a-batch-detail-pre-access.json(optional) 08-provider-status.json 09-reconcile.json"
echo "checklist access evidence: 06-access-preview.json 07-access-status.json"
if [[ -n "$AFTER_IMPORT_HOOK_COMMAND" ]]; then
echo "checklist layered evidence: see 05b-after-import-hook.stdout.txt / 05b-after-import-hook.stderr.txt and hook-generated files under $ARTIFACT_DIR"
else
echo "checklist layered evidence: missing hook-generated /accounts/:id/models, /v1/models, /v1/chat/completions artifacts"
fi
}
curl_json() {
local method="$1"
local path="$2"
local payload="${3:-}"
local url="${CRM_BASE_URL%/}$path"
if [[ "$DRY_RUN" == "1" ]]; then
echo "[dry-run] $method $url" >&2
if [[ -n "$payload" ]]; then
printf '%s\n' "$payload" > /dev/stderr
fi
printf '{"dry_run":true,"method":"%s","url":"%s"}\n' "$method" "$url"
return 0
fi
if [[ -n "$payload" ]]; then
curl -fsS -X "$method" \
-H "Authorization: Bearer $CRM_ADMIN_TOKEN" \
-H 'Content-Type: application/json' \
"$url" \
-d "$payload"
else
curl -fsS -X "$method" \
-H "Authorization: Bearer $CRM_ADMIN_TOKEN" \
"$url"
fi
}
build_host_auth_payload() {
python3 - <<'PY'
import json, os
host_type = os.environ['HOST_AUTH_TYPE']
host_token = os.environ['HOST_AUTH_TOKEN']
print(json.dumps({"type": host_type, "token": host_token}, ensure_ascii=False))
PY
}
build_host_credentials_payload() {
python3 - <<'PY'
import json, os
payload = {
"host_base_url": os.environ["HOST_BASE_URL"],
"pack_path": os.environ["PACK_PATH"],
"provider_id": os.environ["PROVIDER_ID"],
}
if os.environ.get("HOST_API_KEY"):
payload["host_api_key"] = os.environ["HOST_API_KEY"]
if os.environ.get("HOST_BEARER_TOKEN"):
payload["host_bearer_token"] = os.environ["HOST_BEARER_TOKEN"]
if os.environ.get("ACCESS_API_KEY"):
payload["access_api_key"] = os.environ["ACCESS_API_KEY"]
if os.environ.get("ACCESS_MODE"):
payload["access_mode"] = os.environ["ACCESS_MODE"]
if os.environ.get("MODE"):
payload["mode"] = os.environ["MODE"]
if os.environ.get("SUBSCRIPTION_DAYS"):
payload["subscription_days"] = int(os.environ["SUBSCRIPTION_DAYS"])
if os.environ.get("SUBSCRIPTION_USERS"):
payload["subscription_users"] = [x.strip() for x in os.environ["SUBSCRIPTION_USERS"].split(',') if x.strip()]
if os.environ.get("KEYS"):
payload["keys"] = [x.strip() for x in os.environ["KEYS"].split(',') if x.strip()]
print(json.dumps(payload, ensure_ascii=False))
PY
}
require_var CRM_BASE_URL
require_var CRM_ADMIN_TOKEN
require_var HOST_NAME
require_var HOST_BASE_URL
require_var PACK_PATH
require_var PROVIDER_ID
MODE="${MODE:-partial}"
ACCESS_MODE="${ACCESS_MODE:-self_service}"
SUBSCRIPTION_DAYS="${SUBSCRIPTION_DAYS:-30}"
AFTER_IMPORT_HOOK_COMMAND="${AFTER_IMPORT_HOOK_COMMAND:-}"
if [[ -n "${HOST_BEARER_TOKEN:-}" ]]; then
HOST_AUTH_TYPE="${HOST_AUTH_TYPE:-bearer}"
HOST_AUTH_TOKEN="${HOST_AUTH_TOKEN:-$HOST_BEARER_TOKEN}"
elif [[ -n "${HOST_API_KEY:-}" ]]; then
HOST_AUTH_TYPE="${HOST_AUTH_TYPE:-apikey}"
HOST_AUTH_TOKEN="${HOST_AUTH_TOKEN:-$HOST_API_KEY}"
else
echo "missing host credential: set HOST_API_KEY or HOST_BEARER_TOKEN" >&2
exit 1
fi
export CRM_BASE_URL CRM_ADMIN_TOKEN HOST_NAME HOST_BASE_URL PACK_PATH PROVIDER_ID
export HOST_AUTH_TYPE HOST_AUTH_TOKEN MODE ACCESS_MODE SUBSCRIPTION_DAYS
export HOST_API_KEY HOST_BEARER_TOKEN ACCESS_API_KEY SUBSCRIPTION_USERS KEYS
mkdir -p "$ARTIFACT_DIR"
echo "artifacts: $ARTIFACT_DIR"
write_checklist_guide
HOST_AUTH_JSON="$(build_host_auth_payload)"
export HOST_AUTH_JSON
CREATE_HOST_PAYLOAD="$(python3 - <<'PY'
import json, os
host_auth = json.loads(os.environ['HOST_AUTH_JSON'])
print(json.dumps({
'name': os.environ['HOST_NAME'],
'base_url': os.environ['HOST_BASE_URL'],
'auth': host_auth,
}, ensure_ascii=False))
PY
)"
if RESP_EXISTING_HOST="$(curl_json GET "/api/hosts/$HOST_NAME" 2>/dev/null)"; then
EXISTING_BASE_URL="$(printf '%s' "$RESP_EXISTING_HOST" | json_get base_url || true)"
if [[ -n "$EXISTING_BASE_URL" && "$EXISTING_BASE_URL" != "$HOST_BASE_URL" ]]; then
echo "existing host $HOST_NAME points to $EXISTING_BASE_URL, expected $HOST_BASE_URL" >&2
exit 1
fi
fi
RESP_CREATE_HOST="$(curl_json POST /api/hosts "$CREATE_HOST_PAYLOAD")"
save_json 01-create-host "$RESP_CREATE_HOST"
HOST_ID="$(printf '%s' "$RESP_CREATE_HOST" | json_get host_id || true)"
HOST_ID="${HOST_ID:-$HOST_NAME}"
echo "host_id=$HOST_ID"
PROBE_PAYLOAD="$(python3 - <<'PY'
import json, os
print(json.dumps({'auth': json.loads(os.environ['HOST_AUTH_JSON'])}, ensure_ascii=False))
PY
)"
RESP_PROBE="$(curl_json POST "/api/hosts/$HOST_ID/probe" "$PROBE_PAYLOAD")"
save_json 02-probe-host "$RESP_PROBE"
INSTALL_PAYLOAD="$(python3 - <<'PY'
import json, os
payload = {
'host_base_url': os.environ['HOST_BASE_URL'],
'pack_path': os.environ['PACK_PATH'],
}
if os.environ.get('HOST_API_KEY'):
payload['host_api_key'] = os.environ['HOST_API_KEY']
if os.environ.get('HOST_BEARER_TOKEN'):
payload['host_bearer_token'] = os.environ['HOST_BEARER_TOKEN']
print(json.dumps(payload, ensure_ascii=False))
PY
)"
RESP_INSTALL="$(curl_json POST /api/packs/install "$INSTALL_PAYLOAD")"
save_json 03-install-pack "$RESP_INSTALL"
PREVIEW_PAYLOAD="$(python3 - <<'PY'
import json, os
payload = {
"host_base_url": os.environ["HOST_BASE_URL"],
"pack_path": os.environ["PACK_PATH"],
"provider_id": os.environ["PROVIDER_ID"],
"mode": os.environ.get("MODE", "partial"),
}
if os.environ.get("HOST_API_KEY"):
payload["host_api_key"] = os.environ["HOST_API_KEY"]
if os.environ.get("HOST_BEARER_TOKEN"):
payload["host_bearer_token"] = os.environ["HOST_BEARER_TOKEN"]
if os.environ.get("KEYS"):
payload["keys"] = [x.strip() for x in os.environ["KEYS"].split(',') if x.strip()]
print(json.dumps(payload, ensure_ascii=False))
PY
)"
RESP_PREVIEW="$(curl_json POST "/api/providers/$PROVIDER_ID/preview-import" "$PREVIEW_PAYLOAD")"
save_json 04-preview-import "$RESP_PREVIEW"
IMPORT_PAYLOAD="$(build_host_credentials_payload)"
RESP_IMPORT="$(curl_json POST "/api/providers/$PROVIDER_ID/import" "$IMPORT_PAYLOAD")"
save_json 05-import "$RESP_IMPORT"
BATCH_ID="$(printf '%s' "$RESP_IMPORT" | json_get batch_id || true)"
if [[ -n "$BATCH_ID" && "$DRY_RUN" != "1" ]]; then
RESP_BATCH_DETAIL="$(curl_json GET "/api/import-batches/$BATCH_ID")"
save_json 05a-batch-detail-pre-access "$RESP_BATCH_DETAIL"
export BATCH_DETAIL_FILE="$ARTIFACT_DIR/05a-batch-detail-pre-access.json"
else
unset BATCH_DETAIL_FILE || true
fi
if [[ -n "$AFTER_IMPORT_HOOK_COMMAND" ]]; then
export BATCH_ID PROVIDER_ID HOST_BASE_URL CRM_BASE_URL ACCESS_MODE MODE ARTIFACT_DIR
bash -lc "$AFTER_IMPORT_HOOK_COMMAND" \
>"$ARTIFACT_DIR/05b-after-import-hook.stdout.txt" \
2>"$ARTIFACT_DIR/05b-after-import-hook.stderr.txt"
fi
echo "batch_id=${BATCH_ID:-unknown}"
ACCESS_PREVIEW_PAYLOAD="$(python3 - <<'PY'
import json, os
payload = {
'provider_id': os.environ['PROVIDER_ID'],
'mode': os.environ.get('ACCESS_MODE', 'self_service'),
}
print(json.dumps(payload, ensure_ascii=False))
PY
)"
RESP_ACCESS_PREVIEW="$(curl_json POST "/api/providers/$PROVIDER_ID/access/preview" "$ACCESS_PREVIEW_PAYLOAD")"
save_json 06-access-preview "$RESP_ACCESS_PREVIEW"
RESP_ACCESS_STATUS="$(curl_json GET "/api/providers/$PROVIDER_ID/access/status")"
save_json 07-access-status "$RESP_ACCESS_STATUS"
RESP_PROVIDER_STATUS="$(curl_json GET "/api/providers/$PROVIDER_ID/status")"
save_json 08-provider-status "$RESP_PROVIDER_STATUS"
RECONCILE_PAYLOAD="$(python3 - <<'PY'
import json, os
payload = {
"host_base_url": os.environ["HOST_BASE_URL"],
"pack_path": os.environ["PACK_PATH"],
"provider_id": os.environ["PROVIDER_ID"],
}
if os.environ.get("HOST_API_KEY"):
payload["host_api_key"] = os.environ["HOST_API_KEY"]
if os.environ.get("HOST_BEARER_TOKEN"):
payload["host_bearer_token"] = os.environ["HOST_BEARER_TOKEN"]
if os.environ.get("ACCESS_API_KEY"):
payload["access_api_key"] = os.environ["ACCESS_API_KEY"]
print(json.dumps(payload, ensure_ascii=False))
PY
)"
RESP_RECONCILE="$(curl_json POST "/api/providers/$PROVIDER_ID/reconcile" "$RECONCILE_PAYLOAD")"
save_json 09-reconcile "$RESP_RECONCILE"
if [[ -n "$BATCH_ID" && "$DRY_RUN" != "1" ]]; then
RESP_BATCH_DETAIL="$(curl_json GET "/api/import-batches/$BATCH_ID")"
save_json 10-batch-detail "$RESP_BATCH_DETAIL"
fi
if [[ "$SKIP_ROLLBACK" != "1" && -n "$BATCH_ID" ]]; then
ROLLBACK_PAYLOAD="$(python3 - <<'PY'
import json, os
print(json.dumps({'auth': json.loads(os.environ['HOST_AUTH_JSON'])}, ensure_ascii=False))
PY
)"
RESP_ROLLBACK="$(curl_json POST "/api/import-batches/$BATCH_ID/rollback" "$ROLLBACK_PAYLOAD")"
save_json 11-rollback "$RESP_ROLLBACK"
fi
print_artifact_summary
echo "acceptance flow completed"