#!/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}" ARTIFACT_SECURITY_MODE="${ARTIFACT_SECURITY_MODE:-safe}" ARTIFACT_INCLUDE_SECRETS="${ARTIFACT_INCLUDE_SECRETS:-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" } artifact_redact_key_json() { local value="$1" python3 "$ROOT_DIR/scripts/acceptance/artifact_redaction.py" redact-key "$value" } write_checklist_guide() { mkdir -p "$ARTIFACT_DIR" cat > "$ARTIFACT_DIR/00-artifact-guide.txt" < 速查清单对应 artifact security mode: $ARTIFACT_SECURITY_MODE contains raw secrets: $( [[ "$ARTIFACT_INCLUDE_SECRETS" == "1" ]] && printf 'yes' || printf 'no' ) repository-safe: $( [[ "$ARTIFACT_SECURITY_MODE" == "safe" && "$ARTIFACT_INCLUDE_SECRETS" != "1" ]] && printf 'yes' || printf 'no' ) 清单 1(环境 / host 前置) - 01-create-host.json - 02-probe-host.json 清单 2(channel 宿主契约 / 导入落库) - 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) 清单 3(access / 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"