#!/usr/bin/env bash set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck disable=SC1091 source "${SCRIPT_DIR}/lib_ops_center.sh" ops_center_load_config "${SCRIPT_DIR}" MAINLAND_BASE_URL_DEFAULT="$(ops_center_resolve_mainland_api_base_url)" OVERSEAS_BASE_URL_DEFAULT="$(ops_center_resolve_overseas_api_base_url)" REPORT_ROOT_DEFAULT="${OPS_CENTER_REPORT_ROOT}/go-live-bundles" REPORT_DIR_INPUT="${1:-}" MAINLAND_BASE_URL_INPUT="${2:-}" OVERSEAS_BASE_URL_INPUT="${3:-}" timestamp_now() { date '+%Y%m%d_%H%M%S' } normalize_optional_base_url() { local raw_value="${1:-}" local fallback_value="${2:-}" if [[ -z "${raw_value}" ]]; then printf '%s' "${fallback_value}" return fi case "${raw_value}" in -|none|null|disabled) printf '' ;; *) printf '%s' "${raw_value}" ;; esac } if [[ -n "${REPORT_DIR_INPUT}" && "${REPORT_DIR_INPUT}" != http://* && "${REPORT_DIR_INPUT}" != https://* ]]; then REPORT_DIR="${REPORT_DIR_INPUT}" MAINLAND_BASE_URL="${MAINLAND_BASE_URL_INPUT:-${MAINLAND_BASE_URL_DEFAULT}}" OVERSEAS_BASE_URL="$(normalize_optional_base_url "${OVERSEAS_BASE_URL_INPUT:-}" "${OVERSEAS_BASE_URL_DEFAULT}")" else REPORT_DIR="${REPORT_ROOT_DEFAULT}/go-live-bundle-$(timestamp_now)" MAINLAND_BASE_URL="${REPORT_DIR_INPUT:-${MAINLAND_BASE_URL_DEFAULT}}" OVERSEAS_BASE_URL="$(normalize_optional_base_url "${MAINLAND_BASE_URL_INPUT:-}" "${OVERSEAS_BASE_URL_DEFAULT}")" fi mkdir -p "${REPORT_DIR}" CURL_CONNECT_TIMEOUT="${OPS_CENTER_CURL_CONNECT_TIMEOUT:-3}" CURL_MAX_TIME="${OPS_CENTER_CURL_MAX_TIME:-15}" STEP_COMMAND_TIMEOUT_SECONDS="${OPS_CENTER_REPORT_COMMAND_TIMEOUT_SECONDS:-45}" DOCTOR_DECISION_TIMEOUT_SECONDS="${OPS_CENTER_REPORT_DOCTOR_DECISION_TIMEOUT_SECONDS:-120}" DOCTOR_EXPORT_TIMEOUT_SECONDS="${OPS_CENTER_REPORT_DOCTOR_EXPORT_TIMEOUT_SECONDS:-180}" DOCTOR_INNER_REPORT_TIMEOUT_SECONDS="${OPS_CENTER_DOCTOR_INNER_REPORT_TIMEOUT_SECONDS:-60}" ENV_AUDIT_TXT_PATH="${REPORT_DIR}/00_env_audit.txt" SUMMARY_TXT_PATH="${REPORT_DIR}/01_go_live_check_summary.txt" GO_LIVE_JSON_PATH="${REPORT_DIR}/02_go_live_summary.json" STACK_JSON_PATH="${REPORT_DIR}/03_stack_diagnosis.json" DRIVER_FEED_JSON_PATH="${REPORT_DIR}/04_driver_feed.json" CODEX_BRIEF_JSON_PATH="${REPORT_DIR}/05_codex_brief.json" RELEASE_LAUNCHPAD_JSON_PATH="${REPORT_DIR}/06_release_launchpad.json" DOCTOR_DECISION_JSON_PATH="${REPORT_DIR}/07_doctor_decision.json" DOCTOR_EXPORT_STDOUT_PATH="${REPORT_DIR}/08_doctor_export_stdout.txt" MANIFEST_PATH="${REPORT_DIR}/manifest.json" declare -a STEP_RESULTS=() pretty_print_json_file() { local source_path="${1:-}" python3 - "${source_path}" <<'PY' import json import pathlib import sys path = pathlib.Path(sys.argv[1]) raw_text = path.read_text(encoding="utf-8") if path.exists() else "" try: payload = json.loads(raw_text) except Exception: print(raw_text) else: print(json.dumps(payload, ensure_ascii=False, indent=2)) PY } record_step_result() { local key="${1:-}" local path="${2:-}" local ok="${3:-false}" local http_status="${4:-}" local exit_code="${5:-0}" STEP_RESULTS+=("${key}|${path}|${ok}|${http_status}|${exit_code}") } fetch_json_to_file() { local key="${1:-}" local url="${2:-}" local output_path="${3:-}" local tmp_path tmp_path="$(mktemp)" local raw_text http_status raw_text="$(curl -sS \ --connect-timeout "${CURL_CONNECT_TIMEOUT}" \ --max-time "${CURL_MAX_TIME}" \ "${url}" \ -w $'\nHTTP_STATUS=%{http_code}' || true)" http_status="${raw_text##*$'\n'HTTP_STATUS=}" printf '%s' "${raw_text%$'\n'HTTP_STATUS=*}" > "${tmp_path}" pretty_print_json_file "${tmp_path}" > "${output_path}" rm -f "${tmp_path}" if [[ "${http_status}" == 2* ]]; then record_step_result "${key}" "${output_path}" "true" "${http_status}" "0" return 0 fi record_step_result "${key}" "${output_path}" "false" "${http_status}" "1" return 1 } capture_command_to_file() { local key="${1:-}" local output_path="${2:-}" shift 2 || true capture_command_to_file_with_timeout "${STEP_COMMAND_TIMEOUT_SECONDS}" "${key}" "${output_path}" "$@" } capture_command_to_file_with_timeout() { local timeout_seconds="${1:-45}" local key="${2:-}" local output_path="${3:-}" shift 3 || true local -a wrapped_command=("$@") local had_errexit="false" case "$-" in *e*) had_errexit="true" ;; esac if command -v timeout >/dev/null 2>&1; then wrapped_command=(timeout "${timeout_seconds}" "$@") fi set +e "${wrapped_command[@]}" > "${output_path}" 2>&1 local exit_code=$? if [[ "${had_errexit}" == "true" ]]; then set -e else set +e fi if [[ "${exit_code}" -eq 0 ]]; then record_step_result "${key}" "${output_path}" "true" "" "${exit_code}" return 0 fi record_step_result "${key}" "${output_path}" "false" "" "${exit_code}" return "${exit_code}" } materialize_go_live_summary_fallback() { local summary_path="${1:-}" local output_path="${2:-}" python3 - "${summary_path}" "${output_path}" <<'PY' import json import pathlib import sys summary_path = pathlib.Path(sys.argv[1]) output_path = pathlib.Path(sys.argv[2]) raw_text = summary_path.read_text(encoding="utf-8") if summary_path.exists() else "" start = raw_text.find("{") end = raw_text.rfind("}") if start < 0 or end < start: sys.exit(1) json_text = raw_text[start:end + 1] payload = json.loads(json_text) if not isinstance(payload, dict): sys.exit(1) output_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8") PY } override_step_result() { local key="${1:-}" local path="${2:-}" local ok="${3:-false}" local http_status="${4:-}" local exit_code="${5:-0}" local -a updated_results=() local raw_item existing_key for raw_item in "${STEP_RESULTS[@]}"; do existing_key="${raw_item%%|*}" if [[ "${existing_key}" == "${key}" ]]; then continue fi updated_results+=("${raw_item}") done STEP_RESULTS=("${updated_results[@]}") record_step_result "${key}" "${path}" "${ok}" "${http_status}" "${exit_code}" } set +e capture_command_to_file \ "env_audit" \ "${ENV_AUDIT_TXT_PATH}" \ bash "${SCRIPT_DIR}/drive_ops_center.sh" env-audit "${MAINLAND_BASE_URL}" capture_command_to_file \ "go_live_check_summary" \ "${SUMMARY_TXT_PATH}" \ bash "${SCRIPT_DIR}/drive_ops_center.sh" go-live-check "${MAINLAND_BASE_URL}" "${OVERSEAS_BASE_URL}" summary fetch_json_to_file \ "go_live_summary" \ "${MAINLAND_BASE_URL}/api/v1/ops/go-live-summary?base_url=${MAINLAND_BASE_URL}" \ "${GO_LIVE_JSON_PATH}" if [[ ! -s "${GO_LIVE_JSON_PATH}" || "$(sed -n '1p' "${GO_LIVE_JSON_PATH}" 2>/dev/null)" != "{"* ]]; then if materialize_go_live_summary_fallback "${SUMMARY_TXT_PATH}" "${GO_LIVE_JSON_PATH}" 2>/dev/null; then override_step_result "go_live_summary" "${GO_LIVE_JSON_PATH}" "true" "fallback" "0" fi fi if [[ "$(python3 - "${GO_LIVE_JSON_PATH}" <<'PY' import json import pathlib import sys path = pathlib.Path(sys.argv[1]) try: payload = json.loads(path.read_text(encoding="utf-8")) except Exception: print("invalid") raise SystemExit(0) if isinstance(payload, dict) and payload.get("detail") == "Not Found": print("not_found") else: print("ok") PY )" == "not_found" ]]; then if materialize_go_live_summary_fallback "${SUMMARY_TXT_PATH}" "${GO_LIVE_JSON_PATH}" 2>/dev/null; then override_step_result "go_live_summary" "${GO_LIVE_JSON_PATH}" "true" "fallback" "0" fi fi fetch_json_to_file \ "stack_diagnosis" \ "${MAINLAND_BASE_URL}/api/v1/ops/stack-diagnosis?base_url=${MAINLAND_BASE_URL}" \ "${STACK_JSON_PATH}" fetch_json_to_file \ "driver_feed" \ "${MAINLAND_BASE_URL}/api/v1/ops/driver-feed" \ "${DRIVER_FEED_JSON_PATH}" fetch_json_to_file \ "codex_brief" \ "${MAINLAND_BASE_URL}/api/v1/ops/codex-brief" \ "${CODEX_BRIEF_JSON_PATH}" fetch_json_to_file \ "release_launchpad" \ "${MAINLAND_BASE_URL}/api/v1/ops/releases/launchpad" \ "${RELEASE_LAUNCHPAD_JSON_PATH}" capture_command_to_file_with_timeout \ "${DOCTOR_EXPORT_TIMEOUT_SECONDS}" \ "doctor_export" \ "${DOCTOR_EXPORT_STDOUT_PATH}" \ env "OPS_CENTER_DOCTOR_REPORT_TIMEOUT_SECONDS=${DOCTOR_INNER_REPORT_TIMEOUT_SECONDS}" \ bash "${SCRIPT_DIR}/drive_ops_center.sh" doctor-export "${REPORT_DIR}/doctor-export" "${MAINLAND_BASE_URL}" "${OVERSEAS_BASE_URL}" capture_command_to_file_with_timeout \ "${DOCTOR_DECISION_TIMEOUT_SECONDS}" \ "doctor_decision" \ "${DOCTOR_DECISION_JSON_PATH}" \ bash "${SCRIPT_DIR}/drive_ops_center.sh" doctor-decision "${REPORT_DIR}/doctor-export" set -e python3 - <<'PY' \ "${MANIFEST_PATH}" \ "${REPORT_DIR}" \ "${MAINLAND_BASE_URL}" \ "${OVERSEAS_BASE_URL}" \ "${ENV_AUDIT_TXT_PATH}" \ "${SUMMARY_TXT_PATH}" \ "${GO_LIVE_JSON_PATH}" \ "${STACK_JSON_PATH}" \ "${DRIVER_FEED_JSON_PATH}" \ "${CODEX_BRIEF_JSON_PATH}" \ "${RELEASE_LAUNCHPAD_JSON_PATH}" \ "${DOCTOR_DECISION_JSON_PATH}" \ "${DOCTOR_EXPORT_STDOUT_PATH}" \ "${STEP_RESULTS[@]}" import json import pathlib import sys manifest_path = pathlib.Path(sys.argv[1]) report_dir = pathlib.Path(sys.argv[2]) mainland_base_url = sys.argv[3] overseas_base_url = sys.argv[4] file_keys = [ "env_audit", "go_live_check_summary", "go_live_summary", "stack_diagnosis", "driver_feed", "codex_brief", "release_launchpad", "doctor_decision", "doctor_export", ] file_paths = { "env_audit": pathlib.Path(sys.argv[5]), "go_live_check_summary": pathlib.Path(sys.argv[6]), "go_live_summary": pathlib.Path(sys.argv[7]), "stack_diagnosis": pathlib.Path(sys.argv[8]), "driver_feed": pathlib.Path(sys.argv[9]), "codex_brief": pathlib.Path(sys.argv[10]), "release_launchpad": pathlib.Path(sys.argv[11]), "doctor_decision": pathlib.Path(sys.argv[12]), "doctor_export": pathlib.Path(sys.argv[13]), } step_results = {} for raw_item in sys.argv[14:]: parts = raw_item.split("|", 4) if len(parts) != 5: continue key, path_value, ok_value, http_status, exit_code = parts step_results[key] = { "path": path_value, "ok": ok_value == "true", "http_status": http_status, "exit_code": int(exit_code or 0), } def load_json_file(path: pathlib.Path) -> dict: if not path.is_file(): return {} try: payload = json.loads(path.read_text(encoding="utf-8")) except Exception: return {} return payload if isinstance(payload, dict) else {} def load_env_audit_summary(path: pathlib.Path) -> dict: if not path.is_file(): return {} raw_text = path.read_text(encoding="utf-8", errors="replace") marker = "[8/8] condensed env audit summary" candidate_text = raw_text.split(marker, 1)[1].strip() if marker in raw_text else raw_text.strip() if not candidate_text: return {} try: payload = json.loads(candidate_text) except Exception: return {} return payload if isinstance(payload, dict) else {} failures = [] artifacts = [] for key in file_keys: artifact_path = file_paths[key] result = dict(step_results.get(key) or {}) if not result: result = { "path": str(artifact_path), "ok": artifact_path.exists(), "http_status": "", "exit_code": 0 if artifact_path.exists() else 1, } entry = { "key": key, "path": str(artifact_path), "exists": artifact_path.exists(), "ok": bool(result.get("ok", False)), "http_status": str(result.get("http_status") or ""), "exit_code": int(result.get("exit_code", 0) or 0), } artifacts.append(entry) if not entry["ok"]: failures.append(key) go_live_summary_payload = load_json_file(file_paths["go_live_summary"]) stack_diagnosis_payload = load_json_file(file_paths["stack_diagnosis"]) driver_feed_payload = load_json_file(file_paths["driver_feed"]) codex_brief_payload = load_json_file(file_paths["codex_brief"]) env_audit_summary = load_env_audit_summary(file_paths["env_audit"]) stack_diagnosis = dict(stack_diagnosis_payload.get("diagnosis") or {}) driver_feed_summary = dict(driver_feed_payload.get("summary") or {}) codex_brief_summary = dict(codex_brief_payload.get("summary") or {}) def first_nonempty(*values: object) -> str: for value in values: normalized = str(value or "").strip() if normalized: return normalized return "" def nonempty_distinct(values: dict[str, str]) -> list[str]: return sorted({str(value or "").strip() for value in values.values() if str(value or "").strip()}) def distinct_ints(values: dict[str, int], available_sources: set[str]) -> list[int]: return sorted({int(values[source]) for source in values if source in available_sources}) launchpad_target_node_codes = { "go_live_summary": str(go_live_summary_payload.get("launchpad_recommended_target_node_code") or "").strip(), "stack_diagnosis": str(stack_diagnosis.get("launchpad_recommended_target_node_code") or "").strip(), "driver_feed": str(driver_feed_summary.get("launchpad_recommended_target_node_code") or "").strip(), "codex_brief": str(codex_brief_summary.get("launchpad_recommended_target_node_code") or "").strip(), } launchpad_recovery_labels = { "go_live_summary": str(go_live_summary_payload.get("launchpad_recommended_recovery_label") or "").strip(), "stack_diagnosis": str(stack_diagnosis.get("launchpad_recommended_recovery_label") or "").strip(), "driver_feed": str(driver_feed_summary.get("launchpad_recommended_recovery_label") or "").strip(), "codex_brief": str(codex_brief_summary.get("launchpad_recommended_recovery_label") or "").strip(), } launchpad_recovery_summaries = { "go_live_summary": str(go_live_summary_payload.get("launchpad_recommended_recovery_summary") or "").strip(), "stack_diagnosis": str(stack_diagnosis.get("launchpad_recommended_recovery_summary") or "").strip(), "driver_feed": str(driver_feed_summary.get("launchpad_recommended_recovery_summary") or "").strip(), "codex_brief": str(codex_brief_summary.get("launchpad_recommended_recovery_summary") or "").strip(), } launchpad_bootstrap_pending_nodes = { "go_live_summary": int(go_live_summary_payload.get("launchpad_onboarding_bootstrap_pending_nodes", 0) or 0), "stack_diagnosis": int(stack_diagnosis.get("launchpad_onboarding_bootstrap_pending_nodes", 0) or 0), "driver_feed": int(driver_feed_summary.get("launchpad_onboarding_bootstrap_pending_nodes", 0) or 0), "codex_brief": int(codex_brief_summary.get("launchpad_onboarding_bootstrap_pending_nodes", 0) or 0), } launchpad_acceptance_ready_nodes = { "go_live_summary": int(go_live_summary_payload.get("launchpad_onboarding_acceptance_ready_nodes", 0) or 0), "stack_diagnosis": int(stack_diagnosis.get("launchpad_onboarding_acceptance_ready_nodes", 0) or 0), "driver_feed": int(driver_feed_summary.get("launchpad_onboarding_acceptance_ready_nodes", 0) or 0), "codex_brief": int(codex_brief_summary.get("launchpad_onboarding_acceptance_ready_nodes", 0) or 0), } available_alignment_sources = { source for source, payload_value in { "go_live_summary": go_live_summary_payload, "stack_diagnosis": stack_diagnosis_payload, "driver_feed": driver_feed_payload, "codex_brief": codex_brief_payload, }.items() if isinstance(payload_value, dict) and payload_value } launchpad_alignment = { "consistent": True, "target_node_code_consistent": True, "recovery_label_consistent": True, "recovery_summary_consistent": True, "bootstrap_pending_consistent": True, "acceptance_ready_consistent": True, "available_sources": sorted(available_alignment_sources), "target_node_codes": launchpad_target_node_codes, "recovery_labels": launchpad_recovery_labels, "recovery_summaries": launchpad_recovery_summaries, "bootstrap_pending_nodes": launchpad_bootstrap_pending_nodes, "acceptance_ready_nodes": launchpad_acceptance_ready_nodes, } launchpad_alignment["target_node_code_consistent"] = len(nonempty_distinct({ source: value for source, value in launchpad_target_node_codes.items() if source in available_alignment_sources })) <= 1 launchpad_alignment["recovery_label_consistent"] = len(nonempty_distinct({ source: value for source, value in launchpad_recovery_labels.items() if source in available_alignment_sources })) <= 1 launchpad_alignment["recovery_summary_consistent"] = len(nonempty_distinct({ source: value for source, value in launchpad_recovery_summaries.items() if source in available_alignment_sources })) <= 1 launchpad_alignment["bootstrap_pending_consistent"] = len( distinct_ints(launchpad_bootstrap_pending_nodes, available_alignment_sources) ) <= 1 launchpad_alignment["acceptance_ready_consistent"] = len( distinct_ints(launchpad_acceptance_ready_nodes, available_alignment_sources) ) <= 1 launchpad_alignment["consistent"] = all([ bool(launchpad_alignment["target_node_code_consistent"]), bool(launchpad_alignment["recovery_label_consistent"]), bool(launchpad_alignment["recovery_summary_consistent"]), bool(launchpad_alignment["bootstrap_pending_consistent"]), bool(launchpad_alignment["acceptance_ready_consistent"]), ]) env_audit_status = str(env_audit_summary.get("status") or "").strip() env_audit_runtime_may_need_restart = bool(((env_audit_summary.get("runtime") or {}).get("runtime_may_need_restart", False))) review_status_hint = "ready" review_headline = "核心上线证据已经齐备,可进入最终人工复核或正式发布门禁。" if failures or env_audit_status == "blocked": review_status_hint = "blocked" review_headline = "当前导出包仍有关键阻断项,不能直接作为正式上线终稿。" elif env_audit_status == "attention" or not launchpad_alignment["consistent"]: review_status_hint = "attention" if not launchpad_alignment["consistent"]: review_headline = "核心报告虽已导出,但 launchpad 摘要在不同 surface 之间存在漂移,建议先重新复核。" else: review_headline = "核心报告已齐,但环境审计仍提示存在待收口事项。" payload = { "report_dir": str(report_dir), "mainland_base_url": mainland_base_url, "overseas_base_url": overseas_base_url, "artifacts": artifacts, "failures": failures, "summary": { "ok": len(failures) == 0, "artifact_total": len(artifacts), "failure_total": len(failures), "review_status_hint": review_status_hint, "review_headline": review_headline, "env_audit_status": env_audit_status, "env_audit_runtime_may_need_restart": env_audit_runtime_may_need_restart, "launchpad_recommended_target_node_code": first_nonempty( go_live_summary_payload.get("launchpad_recommended_target_node_code"), stack_diagnosis.get("launchpad_recommended_target_node_code"), driver_feed_summary.get("launchpad_recommended_target_node_code"), codex_brief_summary.get("launchpad_recommended_target_node_code"), ), "launchpad_recommended_recovery_label": first_nonempty( go_live_summary_payload.get("launchpad_recommended_recovery_label"), stack_diagnosis.get("launchpad_recommended_recovery_label"), driver_feed_summary.get("launchpad_recommended_recovery_label"), codex_brief_summary.get("launchpad_recommended_recovery_label"), ), "launchpad_recommended_recovery_summary": first_nonempty( go_live_summary_payload.get("launchpad_recommended_recovery_summary"), stack_diagnosis.get("launchpad_recommended_recovery_summary"), driver_feed_summary.get("launchpad_recommended_recovery_summary"), codex_brief_summary.get("launchpad_recommended_recovery_summary"), ), "launchpad_onboarding_bootstrap_pending_nodes": max(launchpad_bootstrap_pending_nodes.values() or [0]), "launchpad_onboarding_acceptance_ready_nodes": max(launchpad_acceptance_ready_nodes.values() or [0]), "launchpad_alignment": launchpad_alignment, "recommended_reading_order": [ "00_env_audit.txt", "01_go_live_check_summary.txt", "02_go_live_summary.json", "03_stack_diagnosis.json", "04_driver_feed.json", "05_codex_brief.json", "06_release_launchpad.json", "07_doctor_decision.json", "08_doctor_export_stdout.txt", ], }, } manifest_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8") print(json.dumps(payload, ensure_ascii=False, indent=2)) PY