7777 lines
321 KiB
Bash
Executable File
7777 lines
321 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
set -euo 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}"
|
||
|
||
OPS_CENTER_CURL_CONNECT_TIMEOUT="${OPS_CENTER_CURL_CONNECT_TIMEOUT:-3}"
|
||
OPS_CENTER_CURL_MAX_TIME="${OPS_CENTER_CURL_MAX_TIME:-20}"
|
||
OPS_CENTER_CURL_MAX_TIME_CODEX_BRIEF="${OPS_CENTER_CURL_MAX_TIME_CODEX_BRIEF:-45}"
|
||
OPS_CENTER_REPORT_COMMAND_TIMEOUT_SECONDS="${OPS_CENTER_REPORT_COMMAND_TIMEOUT_SECONDS:-25}"
|
||
OPS_CENTER_DOCTOR_REPORT_TIMEOUT_SECONDS="${OPS_CENTER_DOCTOR_REPORT_TIMEOUT_SECONDS:-${OPS_CENTER_REPORT_COMMAND_TIMEOUT_SECONDS}}"
|
||
|
||
usage() {
|
||
cat <<'EOF'
|
||
usage:
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh <command> [args...]
|
||
|
||
commands:
|
||
help
|
||
config
|
||
contracts [mainland_base_url] [contract_key]
|
||
env-audit [mainland_base_url]
|
||
runtime-refresh-check [mainland_base_url]
|
||
runtime-refresh-recover [mainland_base_url]
|
||
stack-diagnosis [mainland_base_url] [summary|full]
|
||
stack-check [mainland_base_url] [summary|full] (backward-compatible alias of stack-diagnosis)
|
||
go-live-check [mainland_base_url] [overseas_base_url] [summary|full]
|
||
go-live-recover [mainland_base_url] [overseas_base_url]
|
||
go-live-export [report_dir] [mainland_base_url] [overseas_base_url]
|
||
go-live-review [manifest.json|report_dir]
|
||
go-live-signoff [manifest.json|report_dir|mainland_base_url] [overseas_base_url]
|
||
stack-next [mainland_base_url] [preview|run] [confirm] [requested_by]
|
||
release-package [skip-smoke]
|
||
release-verify [archive_path] [hash_path]
|
||
release-prepare
|
||
release-show
|
||
release-launchpad [mainland_base_url] [channel]
|
||
release-preview [mainland_base_url]
|
||
release-preview-smart [mainland_base_url] <worker|control>
|
||
release-import-latest [mainland_base_url]
|
||
publish-latest [mainland_base_url] <worker|control> [confirm] [skip-smoke]
|
||
doctor-export [report_dir] [mainland_base_url] [overseas_base_url]
|
||
doctor-decision [manifest_path|report_dir|mainland_base_url] [overseas_base_url]
|
||
doctor [mainland_base_url] [overseas_base_url]
|
||
ops-plane [mainland_base_url]
|
||
inspection [mainland_base_url]
|
||
participation [mainland_base_url]
|
||
topology [mainland_base_url] [overseas_base_url]
|
||
nodes [mainland_base_url]
|
||
stack-first-gap-handover [mainland_base_url]
|
||
node-bind-ssh [mainland_base_url] <node_code> <ssh_host> <ssh_user> [ssh_port] [title]
|
||
node-handover [mainland_base_url] <node_code>
|
||
node-onboarding [mainland_base_url] <node_code>
|
||
node-recover [mainland_base_url] <node_code> [confirm] [requested_by]
|
||
node-bootstrap-preview [mainland_base_url] <node_code> [requested_by]
|
||
node-bootstrap-run [mainland_base_url] <node_code> [requested_by]
|
||
node-acceptance-plan [mainland_base_url] <node_code> [requested_by]
|
||
node-acceptance-run [mainland_base_url] <node_code> [requested_by]
|
||
scene-node-log [mainland_base_url] <node_code> [limit] [key|full]
|
||
log-sync-check [mainland_base_url]
|
||
log-sync-recover [mainland_base_url] [key|full] [confirm] [requested_by]
|
||
agent-gap-check [mainland_base_url]
|
||
agent-gap-recover [mainland_base_url] [confirm] [requested_by]
|
||
agent-gap-export [report_dir] [mainland_base_url] [requested_by]
|
||
node-bootstrap-plan [mainland_base_url] <node_code> [issued_by] [expires_in_hours] [control_plane_base_url] [root_dir]
|
||
queue-status [mainland_base_url] <node_code>
|
||
queue-records [mainland_base_url] <node_code> [state]
|
||
queue-flush [mainland_base_url] <node_code> [limit] [requested_by]
|
||
queue-replay [mainland_base_url] <node_code> [limit] [requested_by]
|
||
queue-replay-record [mainland_base_url] <node_code> <record_id> [requested_by]
|
||
queue-discard-record [mainland_base_url] <node_code> <record_id> <reason> [requested_by]
|
||
driver-feed [mainland_base_url]
|
||
activity-stream [mainland_base_url] [key=value ...]
|
||
activity-preview [mainland_base_url] [activity_key] [requested_by]
|
||
driver-focus-preview [mainland_base_url] [entry_key] [requested_by]
|
||
driver-focus-run [mainland_base_url] [entry_key] [primary|secondary] [confirm] [requested_by]
|
||
codex-brief [mainland_base_url]
|
||
codex-focus-preview [mainland_base_url] [entry_key] [requested_by]
|
||
codex-focus-run [mainland_base_url] [entry_key] [confirm] [requested_by]
|
||
release-hub [mainland_base_url] <release-hub-command> [args...]
|
||
driver-preview [mainland_base_url] <action_code> [action_payload_json|@payload.json] [requested_by]
|
||
driver-resolve [mainland_base_url] <action_code> [action_payload_json|@payload.json] [requested_by]
|
||
driver-run [mainland_base_url] <action_code> [action_payload_json|@payload.json] [confirm] [requested_by]
|
||
driver [mainland_base_url] <action_code> [action_payload_json|@payload.json] [requested_by]
|
||
runbook-preview [mainland_base_url] <sequence_key> [action_payload_json|@payload.json] [requested_by]
|
||
runbook-run [mainland_base_url] <sequence_key> [primary|secondary] [action_payload_json|@payload.json] [confirm] [requested_by]
|
||
runbook-resolve [mainland_base_url] <sequence_key> [primary|secondary] [action_payload_json|@payload.json] [requested_by]
|
||
runbook-sequence [mainland_base_url] <sequence_key> [primary|secondary] [action_payload_json|@payload.json] [requested_by]
|
||
smart-latest [mainland_base_url] <worker|control> [confirm]
|
||
smart-release [mainland_base_url] <release_id> <worker|control> [confirm]
|
||
agent-plan [mainland_base_url] <node_code> [node_region] [node_role] [control_plane_base_url] [root_dir]
|
||
agent-plan-export [report_dir] [mainland_base_url] <node_code> [node_region] [node_role] [control_plane_base_url] [root_dir]
|
||
agent-check [service_name] [env_file]
|
||
|
||
examples:
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh config
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh contracts
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh env-audit
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh runtime-refresh-check
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh runtime-refresh-recover
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh stack-diagnosis
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh contracts ops_stack_diagnosis_contract
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh contracts http://127.0.0.1:8100 release_hub_contract
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh go-live-recover
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh go-live-check
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh go-live-export
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh go-live-review
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh go-live-signoff
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh go-live-signoff /path/to/go-live-bundle
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh stack-next
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh stack-next run confirm
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh release-package
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh release-show
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh release-launchpad
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh release-preview
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh release-preview-smart worker
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh release-import-latest
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh publish-latest worker
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh doctor-export
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh doctor-decision
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh doctor http://127.0.0.1:8100 http://152.53.37.118:8100
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh nodes
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh stack-first-gap-handover
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh node-bind-ssh mainland-worker-01 121.204.244.248 root
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh node-bind-ssh http://127.0.0.1:8100 mainland-controller-01 121.204.244.188 root 22 mainland-controller
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh node-handover mainland-worker-01
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh node-onboarding mainland-worker-01
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh node-recover mainland-worker-01
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh node-recover mainland-worker-01 confirm ops-center
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh node-bootstrap-preview mainland-worker-01
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh node-bootstrap-run mainland-worker-01 ops-center
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh node-acceptance-plan mainland-worker-01
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh node-acceptance-run mainland-worker-01 ops-center
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh scene-node-log mainland-worker-01
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh scene-node-log http://127.0.0.1:8100 mainland-worker-01 120 full
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh log-sync-check
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh log-sync-recover
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh log-sync-recover http://127.0.0.1:8100 full confirm cli/ops
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh agent-gap-check
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh agent-gap-recover
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh agent-gap-export
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh node-bootstrap-plan mainland-worker-01
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh queue-status mainland-worker-01
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh queue-records mainland-worker-01 dead_letter
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh queue-flush mainland-worker-01 20
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh queue-replay mainland-worker-01 20
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh queue-replay-record mainland-worker-01 event-ops-123-2
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh queue-discard-record mainland-worker-01 event-ops-123-2 "确认已人工归档"
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh driver-feed
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh activity-stream
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh activity-stream kind=ops_job status=running
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh activity-preview
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh driver-focus-preview
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh driver-focus-run
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh codex-brief
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh codex-focus-preview
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh codex-focus-run
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh release-hub http://127.0.0.1:8100 latest-package
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh smart-release http://127.0.0.1:8100 12 worker
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh driver-preview http://127.0.0.1:8100 open_release_dialog
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh driver-resolve http://127.0.0.1:8100 enable_log_sync_key
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh driver-run http://127.0.0.1:8100 enable_log_sync_key
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh driver http://127.0.0.1:8100 open_release_dialog
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh runbook-preview release_progression
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh runbook-run release_progression
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh runbook-resolve release_progression
|
||
bash domain-api/deploy/multi-region/drive_ops_center.sh runbook-sequence release_progression
|
||
|
||
design:
|
||
- this is the single overseas control-plane entrypoint
|
||
- it does not execute remote shell directly
|
||
- it first delegates to contract preview APIs, then to ops/release execute APIs
|
||
EOF
|
||
}
|
||
|
||
COMMAND="${1:-help}"
|
||
shift || true
|
||
|
||
run_step() {
|
||
local title="$1"
|
||
shift
|
||
echo "============================================================"
|
||
echo "${title}"
|
||
echo "============================================================"
|
||
"$@"
|
||
echo
|
||
}
|
||
|
||
print_json_or_raw() {
|
||
local raw_text="${1:-}"
|
||
python3 - "${raw_text}" <<'PY'
|
||
import json
|
||
import sys
|
||
|
||
raw_text = str(sys.argv[1] or "")
|
||
try:
|
||
payload = json.loads(raw_text)
|
||
except Exception:
|
||
print(raw_text)
|
||
else:
|
||
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
||
PY
|
||
}
|
||
|
||
request_with_status_custom() {
|
||
local method="$1"
|
||
local url="$2"
|
||
local body="${3:-}"
|
||
local connect_timeout="${4:-${OPS_CENTER_CURL_CONNECT_TIMEOUT}}"
|
||
local max_time="${5:-${OPS_CENTER_CURL_MAX_TIME}}"
|
||
if [[ "${method}" == "POST" ]]; then
|
||
curl -sS -X POST "${url}" \
|
||
--connect-timeout "${connect_timeout}" \
|
||
--max-time "${max_time}" \
|
||
-H 'Content-Type: application/json' \
|
||
-d "${body}" \
|
||
-w $'\nHTTP_STATUS=%{http_code}'
|
||
else
|
||
curl -sS \
|
||
--connect-timeout "${connect_timeout}" \
|
||
--max-time "${max_time}" \
|
||
"${url}" \
|
||
-w $'\nHTTP_STATUS=%{http_code}'
|
||
fi
|
||
}
|
||
|
||
request_with_status() {
|
||
local method="$1"
|
||
local url="$2"
|
||
local body="${3:-}"
|
||
request_with_status_custom "${method}" "${url}" "${body}" \
|
||
"${OPS_CENTER_CURL_CONNECT_TIMEOUT}" "${OPS_CENTER_CURL_MAX_TIME}"
|
||
}
|
||
|
||
print_config() {
|
||
local requested_profile="${1:-${OPS_CENTER_ACTIVE_PROFILE}}"
|
||
local saved_profile="${OPS_CENTER_ACTIVE_PROFILE}"
|
||
local saved_profile_key="${OPS_CENTER_ACTIVE_PROFILE_KEY}"
|
||
OPS_CENTER_ACTIVE_PROFILE="${requested_profile}"
|
||
OPS_CENTER_ACTIVE_PROFILE_KEY="$(ops_center_normalize_profile_key "${OPS_CENTER_ACTIVE_PROFILE}")"
|
||
python3 - <<'PY' "${OPS_CENTER_CONFIG_FILE}" \
|
||
"${OPS_CENTER_ACTIVE_PROFILE}" \
|
||
"${OPS_CENTER_PROFILE_NAMES}" \
|
||
"$(ops_center_resolve_mainland_api_base_url)" \
|
||
"$(ops_center_resolve_overseas_api_base_url)" \
|
||
"$(ops_center_resolve_control_plane_base_url)" \
|
||
"$(ops_center_resolve_requested_by)" \
|
||
"$(ops_center_resolve_issued_by)" \
|
||
"$(ops_center_resolve_expires_in_hours)" \
|
||
"$(ops_center_resolve_root_dir)" \
|
||
"$(ops_center_resolve_node_agent_service_name)" \
|
||
"$(ops_center_resolve_node_agent_env_file)" \
|
||
"${OPS_CENTER_REPORT_ROOT}"
|
||
import json
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
config_path = Path(sys.argv[1])
|
||
print(json.dumps({
|
||
"config_file": str(config_path),
|
||
"config_exists": config_path.exists(),
|
||
"active_profile": sys.argv[2],
|
||
"available_profiles": [item.strip() for item in str(sys.argv[3]).split(",") if item.strip()],
|
||
"ops_mainland_api_base_url": sys.argv[4],
|
||
"ops_overseas_api_base_url": sys.argv[5],
|
||
"ops_default_control_plane_base_url": sys.argv[6],
|
||
"ops_default_requested_by": sys.argv[7],
|
||
"ops_default_issued_by": sys.argv[8],
|
||
"ops_default_expires_in_hours": int(sys.argv[9] or 72),
|
||
"ops_default_root_dir": sys.argv[10],
|
||
"ops_node_agent_service_name": sys.argv[11],
|
||
"ops_node_agent_env_file": sys.argv[12],
|
||
"ops_report_root": sys.argv[13],
|
||
}, ensure_ascii=False, indent=2))
|
||
PY
|
||
OPS_CENTER_ACTIVE_PROFILE="${saved_profile}"
|
||
OPS_CENTER_ACTIVE_PROFILE_KEY="${saved_profile_key}"
|
||
}
|
||
|
||
timestamp_now() {
|
||
date '+%Y%m%d_%H%M%S'
|
||
}
|
||
|
||
ops_center_repo_root() {
|
||
printf '%s' "${OPS_CENTER_PROJECT_ROOT}"
|
||
}
|
||
|
||
confirm_to_bool() {
|
||
local raw_value="${1:-}"
|
||
case "$(printf '%s' "${raw_value}" | tr '[:upper:]' '[:lower:]')" in
|
||
1|true|yes|y|confirm|confirmed|skip|skip-smoke)
|
||
printf 'true'
|
||
;;
|
||
*)
|
||
printf 'false'
|
||
;;
|
||
esac
|
||
}
|
||
|
||
is_confirm_token() {
|
||
local raw_value="${1:-}"
|
||
case "$(printf '%s' "${raw_value}" | tr '[:upper:]' '[:lower:]')" in
|
||
1|true|yes|y|confirm|confirmed)
|
||
return 0
|
||
;;
|
||
*)
|
||
return 1
|
||
;;
|
||
esac
|
||
}
|
||
|
||
is_secondary_token() {
|
||
local raw_value="${1:-}"
|
||
case "$(printf '%s' "${raw_value}" | tr '[:upper:]' '[:lower:]')" in
|
||
secondary|second|2|true|yes|y)
|
||
return 0
|
||
;;
|
||
*)
|
||
return 1
|
||
;;
|
||
esac
|
||
}
|
||
|
||
normalize_payload_json_input() {
|
||
local raw_value="${1:-}"
|
||
if [[ -z "${raw_value}" ]]; then
|
||
printf '{}'
|
||
return 0
|
||
fi
|
||
if [[ "${raw_value}" == @* ]]; then
|
||
local payload_path="${raw_value#@}"
|
||
if [[ ! -f "${payload_path}" ]]; then
|
||
echo "payload file not found: ${payload_path}" >&2
|
||
return 1
|
||
fi
|
||
cat "${payload_path}"
|
||
return 0
|
||
fi
|
||
printf '%s' "${raw_value}"
|
||
}
|
||
|
||
enrich_driver_action_payload_json() {
|
||
local mainland_base_url="$1"
|
||
local action_code="$2"
|
||
local payload_json="$3"
|
||
|
||
python3 - <<'PY' "${mainland_base_url}" "${action_code}" "${payload_json}"
|
||
import json
|
||
import sys
|
||
import urllib.request
|
||
|
||
|
||
def normalize_codes(*groups):
|
||
result = []
|
||
for group in groups:
|
||
for item in list(group or []):
|
||
node_code = str(item or "").strip()
|
||
if node_code and node_code not in result:
|
||
result.append(node_code)
|
||
return result
|
||
|
||
|
||
base_url = str(sys.argv[1] or "").rstrip("/")
|
||
action_code = str(sys.argv[2] or "").strip()
|
||
payload_raw = str(sys.argv[3] or "").strip() or "{}"
|
||
|
||
try:
|
||
payload = json.loads(payload_raw)
|
||
except Exception:
|
||
payload = {}
|
||
if not isinstance(payload, dict):
|
||
payload = {}
|
||
|
||
existing_node_codes = normalize_codes(payload.get("node_codes") or [])
|
||
if existing_node_codes:
|
||
print(json.dumps(payload, ensure_ascii=False))
|
||
raise SystemExit(0)
|
||
|
||
supported_actions = {
|
||
"open_worker_logs_participating",
|
||
"run_inspection_participating",
|
||
"open_worker_logs_standby",
|
||
"run_inspection_standby",
|
||
"open_worker_logs",
|
||
"run_standard_inspection",
|
||
}
|
||
if action_code not in supported_actions or not base_url:
|
||
print(json.dumps(payload, ensure_ascii=False))
|
||
raise SystemExit(0)
|
||
|
||
overview = {}
|
||
try:
|
||
with urllib.request.urlopen(f"{base_url}/api/v1/ops/overview", timeout=20) as response:
|
||
overview_payload = json.loads(response.read().decode("utf-8"))
|
||
overview = dict((overview_payload or {}).get("data") or {})
|
||
except Exception:
|
||
print(json.dumps(payload, ensure_ascii=False))
|
||
raise SystemExit(0)
|
||
|
||
execution_scene = dict(overview.get("execution_scene") or {})
|
||
participation_summary = dict(execution_scene.get("participation_summary") or {})
|
||
log_sync = dict(execution_scene.get("log_sync") or {})
|
||
|
||
dispatch_active = normalize_codes(participation_summary.get("dispatch_active_node_codes") or [])
|
||
recent_only = normalize_codes(participation_summary.get("recent_only_node_codes") or [])
|
||
standby = normalize_codes(
|
||
participation_summary.get("standby_node_codes") or [],
|
||
participation_summary.get("non_participating_node_codes") or [],
|
||
)
|
||
missing_logs = normalize_codes(log_sync.get("missing_participating_nodes") or [])
|
||
|
||
derived_node_codes = []
|
||
if action_code in {"open_worker_logs_participating", "run_inspection_participating"}:
|
||
derived_node_codes = normalize_codes(missing_logs, dispatch_active, recent_only)
|
||
elif action_code in {"open_worker_logs_standby", "run_inspection_standby"}:
|
||
derived_node_codes = normalize_codes(standby)
|
||
elif action_code == "open_worker_logs":
|
||
derived_node_codes = normalize_codes(missing_logs, dispatch_active, recent_only, standby)
|
||
elif action_code == "run_standard_inspection":
|
||
derived_node_codes = normalize_codes(dispatch_active, recent_only, standby)
|
||
|
||
if derived_node_codes:
|
||
payload["node_codes"] = derived_node_codes
|
||
payload.setdefault("_auto_context", {})
|
||
payload["_auto_context"]["resolved_by"] = "drive_ops_center"
|
||
payload["_auto_context"]["resolved_from"] = "ops.overview"
|
||
payload["_auto_context"]["action_code"] = action_code
|
||
|
||
print(json.dumps(payload, ensure_ascii=False))
|
||
PY
|
||
}
|
||
|
||
normalize_optional_base_url_arg() {
|
||
local raw_value="${1:-}"
|
||
local default_value="${2:-}"
|
||
case "$(printf '%s' "${raw_value}" | tr '[:upper:]' '[:lower:]')" in
|
||
"" )
|
||
printf '%s' "${default_value}"
|
||
;;
|
||
-|none|null|disabled|off)
|
||
printf ''
|
||
;;
|
||
*)
|
||
printf '%s' "${raw_value}"
|
||
;;
|
||
esac
|
||
}
|
||
|
||
write_json_file() {
|
||
local output_path="$1"
|
||
local json_payload="$2"
|
||
python3 - "${output_path}" "${json_payload}" <<'PY'
|
||
import json
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
path = Path(sys.argv[1])
|
||
payload = json.loads(sys.argv[2])
|
||
path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
||
PY
|
||
}
|
||
|
||
run_and_capture_report() {
|
||
local title="$1"
|
||
local output_path="$2"
|
||
shift 2
|
||
{
|
||
echo "============================================================"
|
||
echo "${title}"
|
||
echo "============================================================"
|
||
"$@"
|
||
echo
|
||
} | tee "${output_path}"
|
||
}
|
||
|
||
capture_report_quiet() {
|
||
local title="$1"
|
||
local output_path="$2"
|
||
shift 2
|
||
local command_exit_code=0
|
||
echo "============================================================"
|
||
echo "${title}"
|
||
echo "============================================================"
|
||
{
|
||
echo "============================================================"
|
||
echo "${title}"
|
||
echo "============================================================"
|
||
set +e
|
||
timeout "${OPS_CENTER_DOCTOR_REPORT_TIMEOUT_SECONDS}" "$@"
|
||
command_exit_code=$?
|
||
set -e
|
||
echo
|
||
echo "command_exit_code=${command_exit_code}"
|
||
} >"${output_path}"
|
||
echo "saved_report=${output_path}"
|
||
echo "command_exit_code=${command_exit_code}"
|
||
echo
|
||
}
|
||
|
||
capture_report_optional() {
|
||
local title="$1"
|
||
local output_path="$2"
|
||
shift 2
|
||
local command_exit_code=0
|
||
|
||
echo "============================================================"
|
||
echo "${title}"
|
||
echo "============================================================"
|
||
{
|
||
echo "============================================================"
|
||
echo "${title}"
|
||
echo "============================================================"
|
||
set +e
|
||
timeout "${OPS_CENTER_DOCTOR_REPORT_TIMEOUT_SECONDS}" "$@"
|
||
command_exit_code=$?
|
||
set -e
|
||
echo
|
||
echo "command_exit_code=${command_exit_code}"
|
||
} >"${output_path}" 2>&1
|
||
echo "saved_report=${output_path}"
|
||
echo "command_exit_code=${command_exit_code}"
|
||
echo
|
||
}
|
||
|
||
run_doctor_report_export() {
|
||
local report_dir="$1"
|
||
local mainland_base_url="$2"
|
||
local overseas_base_url="$3"
|
||
ops_center_ensure_report_dir "${report_dir}"
|
||
local -a report_files=(
|
||
"00_ops_center_stack_summary.txt"
|
||
"01_ops_plane.txt"
|
||
"02_release_hub.txt"
|
||
"03_inspection.txt"
|
||
"04_participation.txt"
|
||
)
|
||
if [[ -n "${overseas_base_url}" ]]; then
|
||
report_files+=("05_topology.txt")
|
||
fi
|
||
report_files+=(
|
||
"06_contracts_registry.txt"
|
||
"07_driver_feed.txt"
|
||
"08_activity_stream.txt"
|
||
"09_codex_brief.txt"
|
||
"10_stack_next_preview.txt"
|
||
"11_driver_focus_preview.txt"
|
||
"12_activity_preview.txt"
|
||
"13_codex_focus_preview.txt"
|
||
"14_first_handover_gap.txt"
|
||
)
|
||
|
||
capture_report_quiet "[doctor/0] ops center stack" "${report_dir}/00_ops_center_stack_summary.txt" \
|
||
bash "${SCRIPT_DIR}/check_ops_center_stack.sh" "${mainland_base_url}" summary
|
||
capture_report_quiet "[doctor/1] ops plane" "${report_dir}/01_ops_plane.txt" \
|
||
bash "${SCRIPT_DIR}/check_ops_plane.sh" "${mainland_base_url}"
|
||
capture_report_quiet "[doctor/2] release hub" "${report_dir}/02_release_hub.txt" \
|
||
bash "${SCRIPT_DIR}/check_release_hub.sh" "${mainland_base_url}"
|
||
capture_report_quiet "[doctor/3] inspection" "${report_dir}/03_inspection.txt" \
|
||
bash "${SCRIPT_DIR}/check_ops_inspection.sh" "${mainland_base_url}"
|
||
capture_report_quiet "[doctor/4] participation" "${report_dir}/04_participation.txt" \
|
||
bash "${SCRIPT_DIR}/check_worker_participation.sh" "${mainland_base_url}"
|
||
if [[ -n "${overseas_base_url}" ]]; then
|
||
capture_report_quiet "[doctor/5] temp topology" "${report_dir}/05_topology.txt" \
|
||
bash "${SCRIPT_DIR}/check_temp_topology.sh" "${mainland_base_url}" "${overseas_base_url}"
|
||
fi
|
||
capture_report_quiet "[doctor/6] contracts registry" "${report_dir}/06_contracts_registry.txt" \
|
||
bash "${SCRIPT_DIR}/drive_ops_center.sh" contracts "${mainland_base_url}"
|
||
capture_report_quiet "[doctor/7] driver feed" "${report_dir}/07_driver_feed.txt" \
|
||
bash "${SCRIPT_DIR}/drive_ops_center.sh" driver-feed "${mainland_base_url}"
|
||
capture_report_quiet "[doctor/8] activity stream" "${report_dir}/08_activity_stream.txt" \
|
||
bash "${SCRIPT_DIR}/drive_ops_center.sh" activity-stream "${mainland_base_url}"
|
||
capture_report_quiet "[doctor/9] codex brief" "${report_dir}/09_codex_brief.txt" \
|
||
bash "${SCRIPT_DIR}/drive_ops_center.sh" codex-brief "${mainland_base_url}"
|
||
capture_report_optional "[doctor/10] stack next preview" "${report_dir}/10_stack_next_preview.txt" \
|
||
bash "${SCRIPT_DIR}/drive_ops_center.sh" stack-next "${mainland_base_url}" preview
|
||
capture_report_optional "[doctor/11] driver focus preview" "${report_dir}/11_driver_focus_preview.txt" \
|
||
bash "${SCRIPT_DIR}/drive_ops_center.sh" driver-focus-preview "${mainland_base_url}"
|
||
capture_report_optional "[doctor/12] activity preview" "${report_dir}/12_activity_preview.txt" \
|
||
bash "${SCRIPT_DIR}/drive_ops_center.sh" activity-preview "${mainland_base_url}"
|
||
capture_report_optional "[doctor/13] codex focus preview" "${report_dir}/13_codex_focus_preview.txt" \
|
||
bash "${SCRIPT_DIR}/drive_ops_center.sh" codex-focus-preview "${mainland_base_url}"
|
||
capture_report_optional "[doctor/14] first handover gap" "${report_dir}/14_first_handover_gap.txt" \
|
||
bash "${SCRIPT_DIR}/drive_ops_center.sh" stack-first-gap-handover "${mainland_base_url}"
|
||
|
||
local scene_log_targets_json
|
||
scene_log_targets_json="$(python3 - <<'PY' "${report_dir}/00_ops_center_stack_summary.txt"
|
||
import json
|
||
import re
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
target = Path(str(sys.argv[1] or "").strip())
|
||
if not target.exists():
|
||
print("[]")
|
||
raise SystemExit(0)
|
||
|
||
text = target.read_text(encoding="utf-8", errors="replace")
|
||
decoder = json.JSONDecoder()
|
||
best = None
|
||
for index, ch in enumerate(text):
|
||
if ch not in "{[":
|
||
continue
|
||
try:
|
||
value, end = decoder.raw_decode(text[index:])
|
||
except Exception:
|
||
continue
|
||
absolute_end = index + end
|
||
if best is None or absolute_end >= best[0]:
|
||
best = (absolute_end, value)
|
||
|
||
payload = best[1] if best else {}
|
||
diagnosis = dict((payload or {}).get("diagnosis") or {})
|
||
issues = list(diagnosis.get("issues") or [])
|
||
next_step = dict(diagnosis.get("next_step") or {})
|
||
operator_decision = dict(diagnosis.get("operator_decision") or {})
|
||
|
||
targets = []
|
||
seen = set()
|
||
|
||
def add_focus_ref(focus_ref):
|
||
if not isinstance(focus_ref, dict):
|
||
return
|
||
if str(focus_ref.get("kind") or "").strip() != "node_scene_log":
|
||
return
|
||
node_code = str(focus_ref.get("node_code") or "").strip()
|
||
if not node_code or node_code in seen:
|
||
return
|
||
seen.add(node_code)
|
||
mode = str(focus_ref.get("mode") or "key").strip().lower()
|
||
if mode not in {"key", "full"}:
|
||
mode = "key"
|
||
try:
|
||
limit = int(focus_ref.get("limit") or 80)
|
||
except Exception:
|
||
limit = 80
|
||
limit = max(20, min(limit, 300))
|
||
targets.append({
|
||
"node_code": node_code,
|
||
"mode": mode,
|
||
"limit": limit,
|
||
"source": str(focus_ref.get("source") or "").strip(),
|
||
})
|
||
|
||
for issue in issues:
|
||
add_focus_ref((issue or {}).get("focus_ref"))
|
||
add_focus_ref(next_step.get("focus_ref"))
|
||
add_focus_ref(operator_decision.get("next_focus"))
|
||
|
||
print(json.dumps(targets[:3], ensure_ascii=False))
|
||
PY
|
||
)"
|
||
|
||
while IFS=$'\t' read -r scene_node_code scene_mode scene_limit scene_source; do
|
||
[[ -z "${scene_node_code}" ]] && continue
|
||
local safe_node_code
|
||
safe_node_code="$(printf '%s' "${scene_node_code}" | tr -c 'A-Za-z0-9._-' '_')"
|
||
local scene_report_name="15_scene_node_log_${safe_node_code}.txt"
|
||
report_files+=("${scene_report_name}")
|
||
capture_report_optional "[doctor/15] scene node log ${scene_node_code}" "${report_dir}/${scene_report_name}" \
|
||
bash "${SCRIPT_DIR}/drive_ops_center.sh" scene-node-log "${mainland_base_url}" "${scene_node_code}" "${scene_limit:-80}" "${scene_mode:-key}"
|
||
done < <(
|
||
python3 - <<'PY' "${scene_log_targets_json}"
|
||
import json
|
||
import sys
|
||
|
||
targets = json.loads(str(sys.argv[1] or "[]"))
|
||
for item in targets:
|
||
print(
|
||
"\t".join(
|
||
[
|
||
str(item.get("node_code") or "").strip(),
|
||
str(item.get("mode") or "key").strip(),
|
||
str(item.get("limit") or 80).strip(),
|
||
str(item.get("source") or "").strip(),
|
||
]
|
||
)
|
||
)
|
||
PY
|
||
)
|
||
|
||
write_json_file "${report_dir}/meta.json" "$(python3 - \
|
||
"${OPS_CENTER_CONFIG_FILE}" \
|
||
"${OPS_CENTER_ACTIVE_PROFILE}" \
|
||
"${mainland_base_url}" \
|
||
"${overseas_base_url}" \
|
||
"${report_dir}" \
|
||
"${report_files[@]}" <<'PY'
|
||
import json
|
||
import sys
|
||
from datetime import datetime
|
||
|
||
files = list(sys.argv[6:])
|
||
|
||
print(json.dumps({
|
||
"generated_at": datetime.now().isoformat(timespec="seconds"),
|
||
"config_file": sys.argv[1],
|
||
"active_profile": sys.argv[2],
|
||
"mainland_api_base_url": sys.argv[3],
|
||
"overseas_api_base_url": sys.argv[4],
|
||
"report_dir": sys.argv[5],
|
||
"files": files,
|
||
}, ensure_ascii=False))
|
||
PY
|
||
)"
|
||
|
||
write_json_file "${report_dir}/manifest.json" "$(python3 - \
|
||
"${OPS_CENTER_CONFIG_FILE}" \
|
||
"${OPS_CENTER_ACTIVE_PROFILE}" \
|
||
"${mainland_base_url}" \
|
||
"${overseas_base_url}" \
|
||
"${report_dir}" \
|
||
"${report_files[@]}" <<'PY'
|
||
import json
|
||
import re
|
||
import sys
|
||
from datetime import datetime
|
||
from pathlib import Path
|
||
|
||
config_file = str(sys.argv[1] or "").strip()
|
||
active_profile = str(sys.argv[2] or "").strip()
|
||
mainland_api_base_url = str(sys.argv[3] or "").strip()
|
||
overseas_api_base_url = str(sys.argv[4] or "").strip()
|
||
report_dir = Path(str(sys.argv[5] or "").strip())
|
||
report_files = [str(item or "").strip() for item in sys.argv[6:] if str(item or "").strip()]
|
||
optional_preview_files = {
|
||
"10_stack_next_preview.txt",
|
||
"11_driver_focus_preview.txt",
|
||
"12_activity_preview.txt",
|
||
"13_codex_focus_preview.txt",
|
||
}
|
||
|
||
|
||
def extract_exit_code(text: str) -> int:
|
||
matches = re.findall(r"(?m)^command_exit_code=(\d+)\s*$", text)
|
||
if matches:
|
||
try:
|
||
return int(matches[-1])
|
||
except Exception:
|
||
return 1
|
||
return 0
|
||
|
||
|
||
def extract_last_json_value(text: str):
|
||
decoder = json.JSONDecoder()
|
||
best = None
|
||
for index, ch in enumerate(text):
|
||
if ch not in "{[":
|
||
continue
|
||
try:
|
||
value, end = decoder.raw_decode(text[index:])
|
||
except Exception:
|
||
continue
|
||
absolute_end = index + end
|
||
if best is None or absolute_end >= best[0]:
|
||
best = (absolute_end, value)
|
||
return None if best is None else best[1]
|
||
|
||
|
||
def last_nonempty_line(text: str) -> str:
|
||
for line in reversed(text.splitlines()):
|
||
stripped = line.strip()
|
||
if stripped:
|
||
return stripped[:300]
|
||
return ""
|
||
|
||
|
||
def collect_contract_keys(value, bucket: set) -> None:
|
||
if isinstance(value, dict):
|
||
for key, child in value.items():
|
||
if key in {"contract_key", "primary_contract_key"}:
|
||
contract_key = str(child or "").strip()
|
||
if contract_key:
|
||
bucket.add(contract_key)
|
||
continue
|
||
if key in {"contract_keys", "related_contract_keys"} and isinstance(child, list):
|
||
for item in child:
|
||
contract_key = str(item or "").strip()
|
||
if contract_key:
|
||
bucket.add(contract_key)
|
||
continue
|
||
collect_contract_keys(child, bucket)
|
||
elif isinstance(value, list):
|
||
for item in value:
|
||
collect_contract_keys(item, bucket)
|
||
|
||
|
||
def summarize_report(name: str, payload, exit_code: int, text: str) -> dict:
|
||
if not isinstance(payload, dict):
|
||
return {
|
||
"parsed_json": False,
|
||
"available": exit_code == 0,
|
||
"text_tail": last_nonempty_line(text),
|
||
}
|
||
|
||
if name == "00_ops_center_stack_summary.txt":
|
||
diagnosis = dict(payload.get("diagnosis") or {})
|
||
api = dict(payload.get("api") or {})
|
||
issues = list(diagnosis.get("issues") or payload.get("issues") or [])
|
||
next_step = dict(diagnosis.get("next_step") or payload.get("next_step") or {})
|
||
operator_decision = dict(diagnosis.get("operator_decision") or {})
|
||
next_actions = dict(diagnosis.get("next_actions") or {})
|
||
recommended_commands = dict(diagnosis.get("recommended_commands") or {})
|
||
stack_status = str(diagnosis.get("stack_status") or payload.get("stack_status") or payload.get("status") or "")
|
||
blocked_issue = next(
|
||
(dict(item or {}) for item in issues if str((item or {}).get("severity") or "") == "blocked"),
|
||
{},
|
||
)
|
||
headline = ""
|
||
if stack_status == "blocked":
|
||
headline = str(blocked_issue.get("summary") or "").strip()
|
||
if not headline:
|
||
headline = str(
|
||
diagnosis.get("headline")
|
||
or (payload.get("link_snapshot") or {}).get("headline")
|
||
or ""
|
||
).strip()
|
||
return {
|
||
"parsed_json": True,
|
||
"available": exit_code == 0,
|
||
"http_status": int(api.get("http_status", 0) or 0),
|
||
"stack_status": stack_status,
|
||
"surface_status": str(diagnosis.get("surface_status") or ""),
|
||
"automation_status": str(diagnosis.get("automation_status") or ""),
|
||
"issues_total": len(issues),
|
||
"blocked_issues": sum(1 for item in issues if str((item or {}).get("severity") or "") == "blocked"),
|
||
"headline": headline,
|
||
"blocked_issue_detail": str(blocked_issue.get("detail") or "").strip(),
|
||
"launchpad_recommended_target_node_code": str(diagnosis.get("launchpad_recommended_target_node_code") or ""),
|
||
"launchpad_recommended_recovery_label": str(diagnosis.get("launchpad_recommended_recovery_label") or ""),
|
||
"launchpad_recommended_recovery_summary": str(diagnosis.get("launchpad_recommended_recovery_summary") or ""),
|
||
"launchpad_onboarding_bootstrap_pending_nodes": int(diagnosis.get("launchpad_onboarding_bootstrap_pending_nodes", 0) or 0),
|
||
"launchpad_onboarding_acceptance_ready_nodes": int(diagnosis.get("launchpad_onboarding_acceptance_ready_nodes", 0) or 0),
|
||
"next_step_action_code": str(next_step.get("action_code") or ""),
|
||
"next_step_summary": str(next_step.get("summary") or next_step.get("title") or next_step.get("reason") or ""),
|
||
"operator_lane": str(operator_decision.get("lane") or "").strip(),
|
||
"operator_priority": str(operator_decision.get("priority") or "").strip(),
|
||
"operator_reason_code": str(operator_decision.get("reason_code") or "").strip(),
|
||
"operator_title": str(operator_decision.get("title") or "").strip(),
|
||
"operator_summary": str(operator_decision.get("summary") or "").strip(),
|
||
"operator_primary_command_key": str(operator_decision.get("primary_command_key") or "").strip(),
|
||
"operator_secondary_command_key": str(operator_decision.get("secondary_command_key") or "").strip(),
|
||
"operator_primary_command": str(
|
||
next_actions.get("primary_command")
|
||
or recommended_commands.get(str(operator_decision.get("primary_command_key") or "").strip())
|
||
or ""
|
||
).strip(),
|
||
"operator_secondary_command": str(
|
||
next_actions.get("secondary_command")
|
||
or recommended_commands.get(str(operator_decision.get("secondary_command_key") or "").strip())
|
||
or ""
|
||
).strip(),
|
||
"operator_focus_kind": str((operator_decision.get("next_focus") or {}).get("kind") or "").strip(),
|
||
"operator_focus_node_code": str((operator_decision.get("next_focus") or {}).get("node_code") or "").strip(),
|
||
}
|
||
|
||
if name == "06_contracts_registry.txt":
|
||
contracts = list(payload.get("contracts") or [])
|
||
return {
|
||
"parsed_json": True,
|
||
"available": exit_code == 0,
|
||
"http_status": int(payload.get("http_status", 0) or 0),
|
||
"registry_version": str(payload.get("registry_version") or ""),
|
||
"schema_version": str(payload.get("schema_version") or ""),
|
||
"contracts_total": int(payload.get("contracts_total", 0) or len(contracts)),
|
||
}
|
||
|
||
if name == "07_driver_feed.txt":
|
||
summary = dict(payload.get("summary") or {})
|
||
top_recommendation = dict(payload.get("top_recommendation") or {})
|
||
contract_navigation = dict(payload.get("contract_navigation") or {})
|
||
return {
|
||
"parsed_json": True,
|
||
"available": exit_code == 0,
|
||
"http_status": int(payload.get("http_status", 0) or 0),
|
||
"headline": str(payload.get("headline") or ""),
|
||
"primary_contract_key": str(contract_navigation.get("primary_contract_key") or ""),
|
||
"entry_count": len(list(payload.get("entries") or [])),
|
||
"activity_focus_count": len(list(payload.get("activity_focus") or [])),
|
||
"top_recommendation_key": str(top_recommendation.get("key") or ""),
|
||
"top_recommendation_contract_key": str(top_recommendation.get("primary_contract_key") or ""),
|
||
"do_now_total": int(summary.get("do_now_total", 0) or 0),
|
||
"launchpad_recommended_target_node_code": str(summary.get("launchpad_recommended_target_node_code") or ""),
|
||
"launchpad_recommended_recovery_label": str(summary.get("launchpad_recommended_recovery_label") or ""),
|
||
"launchpad_recommended_recovery_summary": str(summary.get("launchpad_recommended_recovery_summary") or ""),
|
||
"launchpad_onboarding_bootstrap_pending_nodes": int(summary.get("launchpad_onboarding_bootstrap_pending_nodes", 0) or 0),
|
||
"launchpad_onboarding_acceptance_ready_nodes": int(summary.get("launchpad_onboarding_acceptance_ready_nodes", 0) or 0),
|
||
}
|
||
|
||
if name == "08_activity_stream.txt":
|
||
summary = dict(payload.get("summary") or {})
|
||
contract_navigation = dict(payload.get("contract_navigation") or {})
|
||
items = list(payload.get("items") or [])
|
||
first_item = dict(items[0] or {}) if items else {}
|
||
return {
|
||
"parsed_json": True,
|
||
"available": exit_code == 0,
|
||
"http_status": int(payload.get("http_status", 0) or 0),
|
||
"primary_contract_key": str(contract_navigation.get("primary_contract_key") or ""),
|
||
"filtered_total": int(summary.get("filtered_total", 0) or 0),
|
||
"item_count": len(items),
|
||
"first_item_key": str(first_item.get("activity_key") or ""),
|
||
"first_item_contract_key": str(first_item.get("primary_contract_key") or ""),
|
||
}
|
||
|
||
if name == "09_codex_brief.txt":
|
||
summary = dict(payload.get("summary") or {})
|
||
focus = dict(payload.get("focus") or {})
|
||
contract_navigation = dict(payload.get("contract_navigation") or {})
|
||
return {
|
||
"parsed_json": True,
|
||
"available": exit_code == 0,
|
||
"http_status": int(payload.get("http_status", 0) or 0),
|
||
"headline": str(payload.get("headline") or ""),
|
||
"primary_contract_key": str(contract_navigation.get("primary_contract_key") or ""),
|
||
"focus_entry_key": str(focus.get("entry_key") or ""),
|
||
"focus_contract_key": str(focus.get("primary_contract_key") or ""),
|
||
"entry_count": len(list(payload.get("entries") or [])),
|
||
"activity_focus_count": len(list(payload.get("activity_focus") or [])),
|
||
"blocked_total": int(summary.get("blocked_total", 0) or 0),
|
||
"launchpad_recommended_target_node_code": str(summary.get("launchpad_recommended_target_node_code") or ""),
|
||
"launchpad_recommended_recovery_label": str(summary.get("launchpad_recommended_recovery_label") or ""),
|
||
"launchpad_recommended_recovery_summary": str(summary.get("launchpad_recommended_recovery_summary") or ""),
|
||
"launchpad_onboarding_bootstrap_pending_nodes": int(summary.get("launchpad_onboarding_bootstrap_pending_nodes", 0) or 0),
|
||
"launchpad_onboarding_acceptance_ready_nodes": int(summary.get("launchpad_onboarding_acceptance_ready_nodes", 0) or 0),
|
||
}
|
||
|
||
if name in optional_preview_files:
|
||
contract = dict(payload.get("contract") or {})
|
||
gate = dict(payload.get("gate") or {})
|
||
selected = dict(payload.get("selected_entry") or {})
|
||
selected_key = (
|
||
str(selected.get("key") or "").strip()
|
||
or str(selected.get("activity_key") or "").strip()
|
||
or str(selected.get("action_code") or "").strip()
|
||
)
|
||
return {
|
||
"parsed_json": True,
|
||
"available": exit_code == 0,
|
||
"http_status": int(payload.get("http_status", 0) or 0),
|
||
"contract_key": str(contract.get("contract_key") or ""),
|
||
"contract_version": str(contract.get("contract_version") or ""),
|
||
"preview_endpoint": str(contract.get("preview_endpoint") or ""),
|
||
"gate_decision": str(gate.get("decision") or ""),
|
||
"selected_key": selected_key,
|
||
"selected_title": str(selected.get("title") or ""),
|
||
}
|
||
|
||
if name.startswith("15_scene_node_log_"):
|
||
participation = dict(payload.get("participation") or {})
|
||
source_summary = dict(payload.get("source_summary") or {})
|
||
return {
|
||
"parsed_json": True,
|
||
"available": exit_code == 0,
|
||
"http_status": int(payload.get("http_status", 0) or 0),
|
||
"node_code": str(payload.get("node_code") or ""),
|
||
"status": str(payload.get("status") or ""),
|
||
"status_label": str(payload.get("status_label") or ""),
|
||
"mode": str(payload.get("mode") or ""),
|
||
"records_total": int(payload.get("records_total", 0) or 0),
|
||
"detect_participating": bool(participation.get("detect_participating", False)),
|
||
"participation_label": str(participation.get("participation_label") or ""),
|
||
"source_line_count": int(source_summary.get("line_count", 0) or 0),
|
||
"missing_reason_code": str(payload.get("missing_reason_code") or ""),
|
||
}
|
||
|
||
return {
|
||
"parsed_json": True,
|
||
"available": exit_code == 0,
|
||
"top_level_keys": list(payload.keys())[:12],
|
||
}
|
||
|
||
|
||
def classify_report_failure(name: str, exists: bool, payload, summary: dict, exit_code: int, text: str) -> dict:
|
||
http_status_raw = ""
|
||
if isinstance(summary, dict):
|
||
http_status_raw = str(summary.get("http_status") or "").strip()
|
||
text_tail = last_nonempty_line(text)
|
||
has_payload = isinstance(payload, (dict, list))
|
||
detail = ""
|
||
reason_code = ""
|
||
label = ""
|
||
if not exists:
|
||
reason_code = "report_missing"
|
||
label = "未生成报告"
|
||
detail = "导出阶段没有落盘该 report 文件。"
|
||
elif exit_code == 124:
|
||
reason_code = "command_timeout"
|
||
label = "执行超时"
|
||
detail = "报告命令超过超时阈值后被终止,建议先单独复跑对应子命令。"
|
||
elif exit_code != 0:
|
||
reason_code = "command_failed"
|
||
label = "命令失败"
|
||
detail = f"报告命令退出码为 {exit_code}。"
|
||
elif http_status_raw and http_status_raw.isdigit() and not (200 <= int(http_status_raw) < 300):
|
||
http_status = int(http_status_raw)
|
||
if http_status == 0:
|
||
reason_code = "http_unreachable"
|
||
label = "接口不可达"
|
||
detail = "curl 未拿到有效 HTTP 状态,通常表示超时、连接失败或被中间网络丢弃。"
|
||
else:
|
||
reason_code = "http_error"
|
||
label = "接口异常"
|
||
detail = f"目标接口返回 HTTP {http_status}。"
|
||
elif not has_payload:
|
||
reason_code = "result_unparsed"
|
||
label = "结果不可解析"
|
||
detail = "命令成功返回,但未解析出结构化 JSON 结果。"
|
||
else:
|
||
reason_code = "ok"
|
||
label = "正常"
|
||
detail = ""
|
||
|
||
if text_tail and reason_code != "ok":
|
||
detail = (detail + f" 最近输出: {text_tail}")[:500].strip()
|
||
|
||
return {
|
||
"reason_code": reason_code,
|
||
"label": label,
|
||
"detail": detail,
|
||
"timeout": reason_code == "command_timeout",
|
||
"http_status": int(http_status_raw) if http_status_raw.isdigit() else 0,
|
||
"text_tail": text_tail,
|
||
"parsed_json": bool(isinstance(summary, dict) and summary.get("parsed_json")),
|
||
}
|
||
|
||
|
||
reports = []
|
||
all_contract_keys = set()
|
||
required_failures = []
|
||
optional_unavailable = []
|
||
|
||
for name in report_files:
|
||
path = report_dir / name
|
||
exists = path.exists()
|
||
text = path.read_text(encoding="utf-8", errors="replace") if exists else ""
|
||
exit_code = extract_exit_code(text) if exists else 1
|
||
parsed_payload = extract_last_json_value(text) if exists else None
|
||
summary = summarize_report(name, parsed_payload, exit_code, text)
|
||
if parsed_payload is not None:
|
||
collect_contract_keys(parsed_payload, all_contract_keys)
|
||
http_status = summary.get("http_status") if isinstance(summary, dict) else None
|
||
http_ok = True
|
||
if isinstance(http_status, int):
|
||
http_ok = 200 <= http_status < 300
|
||
report_ok = exists and exit_code == 0 and http_ok
|
||
if isinstance(summary, dict) and "available" in summary:
|
||
summary["available"] = report_ok
|
||
report_entry = {
|
||
"name": name,
|
||
"path": str(path),
|
||
"exists": exists,
|
||
"size_bytes": int(path.stat().st_size) if exists else 0,
|
||
"optional": name in optional_preview_files,
|
||
"command_exit_code": exit_code,
|
||
"ok": report_ok,
|
||
"summary": summary,
|
||
"failure": classify_report_failure(name, exists, parsed_payload, summary, exit_code, text),
|
||
}
|
||
reports.append(report_entry)
|
||
|
||
if name in optional_preview_files:
|
||
if not report_entry["ok"]:
|
||
optional_unavailable.append(name)
|
||
else:
|
||
if not report_entry["ok"]:
|
||
required_failures.append(name)
|
||
|
||
preview_contract_keys = sorted(
|
||
{
|
||
str((report.get("summary") or {}).get("contract_key") or "").strip()
|
||
for report in reports
|
||
if report.get("name") in optional_preview_files
|
||
and str((report.get("summary") or {}).get("contract_key") or "").strip()
|
||
}
|
||
)
|
||
primary_contract_keys = sorted(
|
||
{
|
||
str((report.get("summary") or {}).get("primary_contract_key") or "").strip()
|
||
for report in reports
|
||
if str((report.get("summary") or {}).get("primary_contract_key") or "").strip()
|
||
}
|
||
)
|
||
reports_by_name = {str(report.get("name") or ""): report for report in reports}
|
||
scene_log_reports = [
|
||
report
|
||
for report in reports
|
||
if str(report.get("name") or "").startswith("15_scene_node_log_")
|
||
]
|
||
scene_log_report_summaries = [dict(report.get("summary") or {}) for report in scene_log_reports]
|
||
scene_log_reports_total = len(scene_log_reports)
|
||
scene_log_reports_ok = sum(1 for report in scene_log_reports if bool(report.get("ok")))
|
||
scene_log_status_counts = {}
|
||
for summary in scene_log_report_summaries:
|
||
status_key = str(summary.get("status") or "unknown").strip() or "unknown"
|
||
scene_log_status_counts[status_key] = int(scene_log_status_counts.get(status_key, 0) or 0) + 1
|
||
scene_log_problem_reports = [
|
||
summary
|
||
for summary in scene_log_report_summaries
|
||
if str(summary.get("status") or "").strip() in {"waiting_sample", "missing_sample", "disabled", "standby", "unknown"}
|
||
]
|
||
scene_log_problem_node_codes = [
|
||
str(summary.get("node_code") or "").strip()
|
||
for summary in scene_log_problem_reports
|
||
if str(summary.get("node_code") or "").strip()
|
||
]
|
||
scene_log_healthy_reports = [
|
||
summary
|
||
for summary in scene_log_report_summaries
|
||
if str(summary.get("status") or "").strip() in {"healthy", "full_capture", "historical_sample"}
|
||
]
|
||
|
||
required_failure_details = [
|
||
{
|
||
"name": str(report.get("name") or ""),
|
||
"reason_code": str((report.get("failure") or {}).get("reason_code") or ""),
|
||
"label": str((report.get("failure") or {}).get("label") or ""),
|
||
"detail": str((report.get("failure") or {}).get("detail") or ""),
|
||
}
|
||
for report in reports
|
||
if not bool(report.get("optional")) and not bool(report.get("ok"))
|
||
]
|
||
optional_unavailable_details = [
|
||
{
|
||
"name": str(report.get("name") or ""),
|
||
"reason_code": str((report.get("failure") or {}).get("reason_code") or ""),
|
||
"label": str((report.get("failure") or {}).get("label") or ""),
|
||
"detail": str((report.get("failure") or {}).get("detail") or ""),
|
||
}
|
||
for report in reports
|
||
if bool(report.get("optional")) and not bool(report.get("ok"))
|
||
]
|
||
|
||
|
||
def report_summary(name: str) -> dict:
|
||
return dict((reports_by_name.get(name) or {}).get("summary") or {})
|
||
|
||
|
||
def report_ok(name: str) -> bool:
|
||
return bool((reports_by_name.get(name) or {}).get("ok"))
|
||
|
||
|
||
def report_http_status(name: str) -> int:
|
||
value = report_summary(name).get("http_status")
|
||
return int(value or 0) if isinstance(value, int) or str(value).isdigit() else 0
|
||
|
||
|
||
contracts_summary = report_summary("06_contracts_registry.txt")
|
||
driver_feed_summary = report_summary("07_driver_feed.txt")
|
||
activity_stream_summary = report_summary("08_activity_stream.txt")
|
||
codex_brief_summary = report_summary("09_codex_brief.txt")
|
||
stack_summary = report_summary("00_ops_center_stack_summary.txt")
|
||
launchpad_recommended_target_node_code = next(
|
||
(
|
||
str(source.get("launchpad_recommended_target_node_code") or "").strip()
|
||
for source in [driver_feed_summary, codex_brief_summary, stack_summary]
|
||
if str(source.get("launchpad_recommended_target_node_code") or "").strip()
|
||
),
|
||
"",
|
||
)
|
||
launchpad_recommended_recovery_label = next(
|
||
(
|
||
str(source.get("launchpad_recommended_recovery_label") or "").strip()
|
||
for source in [driver_feed_summary, codex_brief_summary, stack_summary]
|
||
if str(source.get("launchpad_recommended_recovery_label") or "").strip()
|
||
),
|
||
"",
|
||
)
|
||
launchpad_recommended_recovery_summary = next(
|
||
(
|
||
str(source.get("launchpad_recommended_recovery_summary") or "").strip()
|
||
for source in [driver_feed_summary, codex_brief_summary, stack_summary]
|
||
if str(source.get("launchpad_recommended_recovery_summary") or "").strip()
|
||
),
|
||
"",
|
||
)
|
||
launchpad_onboarding_bootstrap_pending_nodes = max(
|
||
int(driver_feed_summary.get("launchpad_onboarding_bootstrap_pending_nodes", 0) or 0),
|
||
int(codex_brief_summary.get("launchpad_onboarding_bootstrap_pending_nodes", 0) or 0),
|
||
int(stack_summary.get("launchpad_onboarding_bootstrap_pending_nodes", 0) or 0),
|
||
)
|
||
launchpad_onboarding_acceptance_ready_nodes = max(
|
||
int(driver_feed_summary.get("launchpad_onboarding_acceptance_ready_nodes", 0) or 0),
|
||
int(codex_brief_summary.get("launchpad_onboarding_acceptance_ready_nodes", 0) or 0),
|
||
int(stack_summary.get("launchpad_onboarding_acceptance_ready_nodes", 0) or 0),
|
||
)
|
||
stack_status = str(stack_summary.get("stack_status") or "").strip()
|
||
stack_headline = str(stack_summary.get("headline") or "").strip()
|
||
stack_blocked_issue_detail = str(stack_summary.get("blocked_issue_detail") or "").strip()
|
||
stack_next_summary = str(stack_summary.get("next_step_summary") or "").strip()
|
||
stack_next_action_code = str(stack_summary.get("next_step_action_code") or "").strip()
|
||
stack_blocked_issues = int(stack_summary.get("blocked_issues", 0) or 0)
|
||
stack_operator_lane = str(stack_summary.get("operator_lane") or "").strip()
|
||
stack_operator_priority = str(stack_summary.get("operator_priority") or "").strip()
|
||
stack_operator_reason_code = str(stack_summary.get("operator_reason_code") or "").strip()
|
||
stack_operator_title = str(stack_summary.get("operator_title") or "").strip()
|
||
stack_operator_summary = str(stack_summary.get("operator_summary") or "").strip()
|
||
stack_operator_primary_command_key = str(stack_summary.get("operator_primary_command_key") or "").strip()
|
||
stack_operator_secondary_command_key = str(stack_summary.get("operator_secondary_command_key") or "").strip()
|
||
stack_operator_primary_command = str(stack_summary.get("operator_primary_command") or "").strip()
|
||
stack_operator_secondary_command = str(stack_summary.get("operator_secondary_command") or "").strip()
|
||
stack_operator_focus_kind = str(stack_summary.get("operator_focus_kind") or "").strip()
|
||
stack_operator_focus_node_code = str(stack_summary.get("operator_focus_node_code") or "").strip()
|
||
|
||
stack_primary_commands = []
|
||
for candidate in [
|
||
stack_operator_primary_command,
|
||
stack_operator_secondary_command,
|
||
]:
|
||
normalized_candidate = str(candidate or "").strip()
|
||
if normalized_candidate and normalized_candidate not in stack_primary_commands:
|
||
stack_primary_commands.append(normalized_candidate)
|
||
|
||
stack_preferred_surface = "stack-summary"
|
||
if stack_operator_focus_kind == "node_scene_log":
|
||
stack_preferred_surface = "execution-scene"
|
||
elif stack_operator_lane == "node_handover":
|
||
stack_preferred_surface = "managed-nodes"
|
||
elif stack_operator_lane == "release":
|
||
stack_preferred_surface = "release-hub"
|
||
elif stack_operator_lane == "observability":
|
||
stack_preferred_surface = "execution-scene"
|
||
elif stack_operator_lane in {"ops_jobs", "steady"}:
|
||
stack_preferred_surface = "driver-feed"
|
||
|
||
contract_surface_gaps = []
|
||
if report_ok("07_driver_feed.txt") and not str(driver_feed_summary.get("primary_contract_key") or "").strip():
|
||
contract_surface_gaps.append("driver-feed")
|
||
if report_ok("08_activity_stream.txt") and not str(activity_stream_summary.get("primary_contract_key") or "").strip():
|
||
contract_surface_gaps.append("activity-stream")
|
||
if report_ok("09_codex_brief.txt") and not str(codex_brief_summary.get("primary_contract_key") or "").strip():
|
||
contract_surface_gaps.append("codex-brief")
|
||
|
||
surface_http_status = {
|
||
"stack_summary": report_http_status("00_ops_center_stack_summary.txt"),
|
||
"contracts_registry": report_http_status("06_contracts_registry.txt"),
|
||
"driver_feed": report_http_status("07_driver_feed.txt"),
|
||
"activity_stream": report_http_status("08_activity_stream.txt"),
|
||
"codex_brief": report_http_status("09_codex_brief.txt"),
|
||
"stack_next_preview": report_http_status("10_stack_next_preview.txt"),
|
||
"driver_focus_preview": report_http_status("11_driver_focus_preview.txt"),
|
||
"activity_preview": report_http_status("12_activity_preview.txt"),
|
||
"codex_focus_preview": report_http_status("13_codex_focus_preview.txt"),
|
||
}
|
||
|
||
doctor_command = f"bash domain-api/deploy/multi-region/drive_ops_center.sh doctor {mainland_api_base_url}"
|
||
if overseas_api_base_url:
|
||
doctor_command += f" {overseas_api_base_url}"
|
||
|
||
decision = {
|
||
"status": "ready",
|
||
"reason_code": "ops_center_ready",
|
||
"headline": "海外控制面已经具备协议驱动的观察与预览能力。",
|
||
"detail": "",
|
||
"preferred_surface": "driver-feed",
|
||
"next_action_code": "review_driver_feed",
|
||
"recommended_commands": [
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh driver-feed {mainland_api_base_url}",
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh activity-stream {mainland_api_base_url}",
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh codex-brief {mainland_api_base_url}",
|
||
],
|
||
"evidence": {
|
||
"required_failures": required_failures,
|
||
"optional_unavailable": optional_unavailable,
|
||
"contract_surface_gaps": contract_surface_gaps,
|
||
"surface_http_status": surface_http_status,
|
||
},
|
||
}
|
||
|
||
if "06_contracts_registry.txt" in required_failures:
|
||
http_status = int(contracts_summary.get("http_status", 0) or 0)
|
||
contracts_failure = next(
|
||
(
|
||
item for item in required_failure_details
|
||
if str(item.get("name") or "") == "06_contracts_registry.txt"
|
||
),
|
||
{},
|
||
)
|
||
contracts_reason_code = str(contracts_failure.get("reason_code") or "").strip()
|
||
contracts_failure_detail = str(contracts_failure.get("detail") or "").strip()
|
||
decision = {
|
||
"status": "blocked",
|
||
"reason_code": contracts_reason_code or "contracts_registry_unavailable",
|
||
"headline": (
|
||
"控制面 contract registry 拉取超时,海外驾驶链路当前无法确认协议面是否完整。 "
|
||
if contracts_reason_code == "command_timeout"
|
||
else "控制面还没暴露 ops/contracts,海外驾驶链路当前不完整。"
|
||
),
|
||
"detail": contracts_failure_detail or f"06_contracts_registry.txt http_status={http_status},先恢复 contract registry,再继续 preview / execute。",
|
||
"preferred_surface": "contracts",
|
||
"next_action_code": "restore_contract_registry",
|
||
"recommended_commands": [
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh contracts {mainland_api_base_url}",
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh runtime-refresh-recover {mainland_api_base_url}",
|
||
f"bash domain-api/deploy/multi-region/check_ops_center_stack.sh {mainland_api_base_url} summary",
|
||
],
|
||
"evidence": {
|
||
"required_failures": required_failures,
|
||
"required_failure_details": required_failure_details,
|
||
"optional_unavailable": optional_unavailable,
|
||
"optional_unavailable_details": optional_unavailable_details,
|
||
"contract_surface_gaps": contract_surface_gaps,
|
||
"surface_http_status": surface_http_status,
|
||
},
|
||
}
|
||
elif required_failures:
|
||
scene_log_required_failures = [
|
||
name for name in required_failures
|
||
if str(name or "").startswith("15_scene_node_log_")
|
||
]
|
||
if scene_log_required_failures and stack_primary_commands:
|
||
decision = {
|
||
"status": "blocked" if stack_status == "blocked" else "attention",
|
||
"reason_code": stack_operator_reason_code or "scene_log_surface_unavailable",
|
||
"headline": stack_operator_title or "执行现场观察链路还没拿到可用日志样本。",
|
||
"detail": stack_operator_summary or " / ".join(scene_log_required_failures),
|
||
"preferred_surface": stack_preferred_surface,
|
||
"next_action_code": stack_operator_primary_command_key or "inspect_scene_log_reports",
|
||
"recommended_commands": [
|
||
*stack_primary_commands,
|
||
f"bash domain-api/deploy/multi-region/check_ops_center_stack.sh {mainland_api_base_url} summary",
|
||
doctor_command,
|
||
],
|
||
"evidence": {
|
||
"required_failures": required_failures,
|
||
"required_failure_details": required_failure_details,
|
||
"optional_unavailable": optional_unavailable,
|
||
"optional_unavailable_details": optional_unavailable_details,
|
||
"contract_surface_gaps": contract_surface_gaps,
|
||
"surface_http_status": surface_http_status,
|
||
"scene_log_status_counts": scene_log_status_counts,
|
||
},
|
||
}
|
||
else:
|
||
decision = {
|
||
"status": "blocked",
|
||
"reason_code": "required_surfaces_unavailable",
|
||
"headline": "海外控制面仍有必需 surface 缺失,先修 surface 再谈自动驾驶。",
|
||
"detail": " / ".join(required_failures),
|
||
"preferred_surface": stack_preferred_surface if stack_primary_commands else "stack-summary",
|
||
"next_action_code": stack_operator_primary_command_key or "repair_required_surfaces",
|
||
"recommended_commands": [
|
||
*stack_primary_commands,
|
||
doctor_command,
|
||
f"bash domain-api/deploy/multi-region/check_ops_center_stack.sh {mainland_api_base_url} summary",
|
||
],
|
||
"evidence": {
|
||
"required_failures": required_failures,
|
||
"required_failure_details": required_failure_details,
|
||
"optional_unavailable": optional_unavailable,
|
||
"optional_unavailable_details": optional_unavailable_details,
|
||
"contract_surface_gaps": contract_surface_gaps,
|
||
"surface_http_status": surface_http_status,
|
||
},
|
||
}
|
||
elif contract_surface_gaps:
|
||
decision = {
|
||
"status": "attention",
|
||
"reason_code": "contract_navigation_incomplete",
|
||
"headline": "核心观察面已在线,但 contract_navigation 还没完全补齐。",
|
||
"detail": "缺少主协议标记的 surface: " + " / ".join(contract_surface_gaps),
|
||
"preferred_surface": contract_surface_gaps[0],
|
||
"next_action_code": "fill_contract_navigation",
|
||
"recommended_commands": [
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh driver-feed {mainland_api_base_url}",
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh activity-stream {mainland_api_base_url}",
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh codex-brief {mainland_api_base_url}",
|
||
],
|
||
"evidence": {
|
||
"required_failures": required_failures,
|
||
"required_failure_details": required_failure_details,
|
||
"optional_unavailable": optional_unavailable,
|
||
"optional_unavailable_details": optional_unavailable_details,
|
||
"contract_surface_gaps": contract_surface_gaps,
|
||
"surface_http_status": surface_http_status,
|
||
},
|
||
}
|
||
elif stack_status == "blocked":
|
||
decision = {
|
||
"status": "blocked",
|
||
"reason_code": stack_operator_reason_code or "stack_diagnosis_blocked",
|
||
"headline": stack_operator_title or stack_headline or "控制面总检已判定当前现场存在阻断问题,暂不应继续执行自动化动作。",
|
||
"detail": stack_operator_summary or stack_blocked_issue_detail or stack_next_summary or f"stack_diagnosis.blocked_issues={stack_blocked_issues}",
|
||
"preferred_surface": stack_preferred_surface,
|
||
"next_action_code": stack_operator_primary_command_key or stack_next_action_code or "resolve_stack_blockers",
|
||
"recommended_commands": [
|
||
*stack_primary_commands,
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh stack-next {mainland_api_base_url}",
|
||
f"bash domain-api/deploy/multi-region/check_ops_center_stack.sh {mainland_api_base_url} summary",
|
||
doctor_command,
|
||
],
|
||
"evidence": {
|
||
"required_failures": required_failures,
|
||
"required_failure_details": required_failure_details,
|
||
"optional_unavailable": optional_unavailable,
|
||
"optional_unavailable_details": optional_unavailable_details,
|
||
"contract_surface_gaps": contract_surface_gaps,
|
||
"surface_http_status": surface_http_status,
|
||
},
|
||
}
|
||
elif stack_status == "attention":
|
||
if stack_operator_reason_code == "runtime_build_schema_stale":
|
||
restart_commands = []
|
||
for candidate in [
|
||
stack_operator_primary_command,
|
||
stack_operator_secondary_command,
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh runtime-refresh-recover {mainland_api_base_url}",
|
||
f"bash domain-api/deploy/multi-region/check_ops_center_stack.sh {mainland_api_base_url} summary",
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh stack-next {mainland_api_base_url}",
|
||
doctor_command,
|
||
]:
|
||
normalized_candidate = str(candidate or "").strip()
|
||
if normalized_candidate and normalized_candidate not in restart_commands:
|
||
restart_commands.append(normalized_candidate)
|
||
decision = {
|
||
"status": "attention",
|
||
"reason_code": "runtime_build_schema_stale",
|
||
"headline": stack_operator_title or "仓库代码已更新,但运行中的控制面 API 还是旧 schema。",
|
||
"detail": stack_operator_summary or stack_next_summary or "先执行 runtime-refresh-recover,让控制面 API 切到最新代码;随后重新执行 stack-diagnosis / stack-next,再继续节点接管。",
|
||
"preferred_surface": "stack-summary",
|
||
"next_action_code": stack_operator_primary_command_key or "api-restart",
|
||
"recommended_commands": restart_commands,
|
||
"evidence": {
|
||
"required_failures": required_failures,
|
||
"required_failure_details": required_failure_details,
|
||
"optional_unavailable": optional_unavailable,
|
||
"optional_unavailable_details": optional_unavailable_details,
|
||
"contract_surface_gaps": contract_surface_gaps,
|
||
"surface_http_status": surface_http_status,
|
||
},
|
||
}
|
||
else:
|
||
decision = {
|
||
"status": "attention",
|
||
"reason_code": stack_operator_reason_code or "stack_diagnosis_attention",
|
||
"headline": stack_operator_title or stack_headline or "控制面总检已发现待收口事项,建议先顺着 next_step 收口后再继续预览或执行。",
|
||
"detail": stack_operator_summary or stack_next_summary or "请先处理 stack-diagnosis 提示的 attention 项。",
|
||
"preferred_surface": stack_preferred_surface,
|
||
"next_action_code": stack_operator_primary_command_key or stack_next_action_code or "review_stack_next",
|
||
"recommended_commands": [
|
||
*stack_primary_commands,
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh stack-next {mainland_api_base_url}",
|
||
f"bash domain-api/deploy/multi-region/check_ops_center_stack.sh {mainland_api_base_url} summary",
|
||
doctor_command,
|
||
],
|
||
"evidence": {
|
||
"required_failures": required_failures,
|
||
"required_failure_details": required_failure_details,
|
||
"optional_unavailable": optional_unavailable,
|
||
"optional_unavailable_details": optional_unavailable_details,
|
||
"contract_surface_gaps": contract_surface_gaps,
|
||
"surface_http_status": surface_http_status,
|
||
},
|
||
}
|
||
elif optional_unavailable:
|
||
focus_entry = str(codex_brief_summary.get("focus_entry_key") or "").strip()
|
||
decision = {
|
||
"status": "attention",
|
||
"reason_code": "preview_unavailable",
|
||
"headline": "核心观察面已可用,但当前 preview 入口还没形成可执行焦点。",
|
||
"detail": "当前不可用 preview: " + " / ".join(optional_unavailable),
|
||
"preferred_surface": "codex-brief" if focus_entry else "driver-feed",
|
||
"next_action_code": "inspect_focus_surfaces",
|
||
"recommended_commands": [
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh driver-feed {mainland_api_base_url}",
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh activity-stream {mainland_api_base_url}",
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh codex-brief {mainland_api_base_url}",
|
||
],
|
||
"evidence": {
|
||
"required_failures": required_failures,
|
||
"required_failure_details": required_failure_details,
|
||
"optional_unavailable": optional_unavailable,
|
||
"optional_unavailable_details": optional_unavailable_details,
|
||
"contract_surface_gaps": contract_surface_gaps,
|
||
"surface_http_status": surface_http_status,
|
||
},
|
||
}
|
||
elif scene_log_reports_total > 0 and scene_log_reports_ok == 0:
|
||
first_scene_log = dict(scene_log_report_summaries[0] or {}) if scene_log_report_summaries else {}
|
||
scene_log_node_code = str(first_scene_log.get("node_code") or "").strip()
|
||
scene_log_mode = str(first_scene_log.get("mode") or "key").strip() or "key"
|
||
decision = {
|
||
"status": "attention",
|
||
"reason_code": "scene_log_reports_unavailable",
|
||
"headline": "总检已经识别出关键节点现场,但节点现场日志附件还没有成功形成可用样本。",
|
||
"detail": (
|
||
f"scene_log_reports_total={scene_log_reports_total},全部附件当前不可用;"
|
||
f"缺口节点 {', '.join(scene_log_problem_node_codes) or scene_log_node_code or '-'}"
|
||
),
|
||
"preferred_surface": "execution-scene",
|
||
"next_action_code": "inspect_scene_log_reports",
|
||
"recommended_commands": [
|
||
*(
|
||
[
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh scene-node-log {mainland_api_base_url} {scene_log_node_code} 120 {scene_log_mode}"
|
||
]
|
||
if scene_log_node_code
|
||
else []
|
||
),
|
||
f"bash domain-api/deploy/multi-region/check_ops_center_stack.sh {mainland_api_base_url} summary",
|
||
doctor_command,
|
||
],
|
||
"evidence": {
|
||
"required_failures": required_failures,
|
||
"required_failure_details": required_failure_details,
|
||
"optional_unavailable": optional_unavailable,
|
||
"optional_unavailable_details": optional_unavailable_details,
|
||
"contract_surface_gaps": contract_surface_gaps,
|
||
"surface_http_status": surface_http_status,
|
||
"scene_log_status_counts": scene_log_status_counts,
|
||
},
|
||
}
|
||
elif scene_log_problem_reports:
|
||
first_scene_log = dict(scene_log_problem_reports[0] or {})
|
||
scene_log_node_code = str(first_scene_log.get("node_code") or "").strip()
|
||
scene_log_mode = str(first_scene_log.get("mode") or "key").strip() or "key"
|
||
decision = {
|
||
"status": "attention",
|
||
"reason_code": "scene_log_reports_need_attention",
|
||
"headline": "总检已经带回节点现场日志附件,但仍有参与节点的现场样本没有完全收口。",
|
||
"detail": (
|
||
f"问题节点 {', '.join(scene_log_problem_node_codes) or scene_log_node_code or '-'};"
|
||
f" 当前状态分布 {scene_log_status_counts}"
|
||
),
|
||
"preferred_surface": "execution-scene",
|
||
"next_action_code": "inspect_scene_log_reports",
|
||
"recommended_commands": [
|
||
*(
|
||
[
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh scene-node-log {mainland_api_base_url} {scene_log_node_code} 120 {scene_log_mode}"
|
||
]
|
||
if scene_log_node_code
|
||
else []
|
||
),
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh stack-next {mainland_api_base_url}",
|
||
doctor_command,
|
||
],
|
||
"evidence": {
|
||
"required_failures": required_failures,
|
||
"required_failure_details": required_failure_details,
|
||
"optional_unavailable": optional_unavailable,
|
||
"optional_unavailable_details": optional_unavailable_details,
|
||
"contract_surface_gaps": contract_surface_gaps,
|
||
"surface_http_status": surface_http_status,
|
||
"scene_log_status_counts": scene_log_status_counts,
|
||
},
|
||
}
|
||
else:
|
||
codex_headline = str(codex_brief_summary.get("headline") or "").strip()
|
||
driver_headline = str(driver_feed_summary.get("headline") or "").strip()
|
||
decision["headline"] = stack_operator_title or codex_headline or driver_headline or decision["headline"]
|
||
decision["detail"] = stack_operator_summary or stack_next_summary or "先看 driver-feed,再按 manifest 已准备好的 preview surface 继续收敛。"
|
||
decision["preferred_surface"] = (
|
||
stack_preferred_surface
|
||
if stack_operator_primary_command_key or stack_operator_focus_kind
|
||
else ("codex-brief" if str(codex_brief_summary.get("focus_entry_key") or "").strip() else "driver-feed")
|
||
)
|
||
decision["next_action_code"] = stack_operator_primary_command_key or stack_next_action_code or "preview_primary_focus"
|
||
decision["recommended_commands"] = [
|
||
*stack_primary_commands,
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh stack-next {mainland_api_base_url}",
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh driver-focus-preview {mainland_api_base_url}",
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh codex-focus-preview {mainland_api_base_url}",
|
||
]
|
||
|
||
if isinstance(decision.get("evidence"), dict):
|
||
decision["evidence"].update({
|
||
"launchpad_recommended_target_node_code": launchpad_recommended_target_node_code,
|
||
"launchpad_recommended_recovery_label": launchpad_recommended_recovery_label,
|
||
"launchpad_recommended_recovery_summary": launchpad_recommended_recovery_summary,
|
||
"launchpad_onboarding_bootstrap_pending_nodes": launchpad_onboarding_bootstrap_pending_nodes,
|
||
"launchpad_onboarding_acceptance_ready_nodes": launchpad_onboarding_acceptance_ready_nodes,
|
||
})
|
||
|
||
manifest = {
|
||
"generated_at": datetime.now().isoformat(timespec="seconds"),
|
||
"config_file": config_file,
|
||
"active_profile": active_profile,
|
||
"mainland_api_base_url": mainland_api_base_url,
|
||
"overseas_api_base_url": overseas_api_base_url,
|
||
"report_dir": str(report_dir),
|
||
"reports_total": len(reports),
|
||
"reports": reports,
|
||
"summary": {
|
||
"ok": len(required_failures) == 0,
|
||
"required_failures": required_failures,
|
||
"required_failure_details": required_failure_details,
|
||
"optional_unavailable": optional_unavailable,
|
||
"optional_unavailable_details": optional_unavailable_details,
|
||
"scene_log_reports_total": scene_log_reports_total,
|
||
"scene_log_reports_ok": scene_log_reports_ok,
|
||
"scene_log_status_counts": scene_log_status_counts,
|
||
"scene_log_reports": [
|
||
{
|
||
"name": str(report.get("name") or ""),
|
||
"node_code": str((report.get("summary") or {}).get("node_code") or ""),
|
||
"status": str((report.get("summary") or {}).get("status") or ""),
|
||
"status_label": str((report.get("summary") or {}).get("status_label") or ""),
|
||
"mode": str((report.get("summary") or {}).get("mode") or ""),
|
||
"records_total": int((report.get("summary") or {}).get("records_total", 0) or 0),
|
||
"ok": bool(report.get("ok")),
|
||
}
|
||
for report in scene_log_reports
|
||
],
|
||
"available_preview_reports": sum(
|
||
1 for report in reports if report.get("name") in optional_preview_files and report.get("ok")
|
||
),
|
||
"discovered_contract_keys": sorted(all_contract_keys),
|
||
"primary_contract_keys": primary_contract_keys,
|
||
"preview_contract_keys": preview_contract_keys,
|
||
"contract_surface_gaps": contract_surface_gaps,
|
||
"surface_http_status": surface_http_status,
|
||
"launchpad_recommended_target_node_code": launchpad_recommended_target_node_code,
|
||
"launchpad_recommended_recovery_label": launchpad_recommended_recovery_label,
|
||
"launchpad_recommended_recovery_summary": launchpad_recommended_recovery_summary,
|
||
"launchpad_onboarding_bootstrap_pending_nodes": launchpad_onboarding_bootstrap_pending_nodes,
|
||
"launchpad_onboarding_acceptance_ready_nodes": launchpad_onboarding_acceptance_ready_nodes,
|
||
},
|
||
"decision": decision,
|
||
}
|
||
|
||
print(json.dumps(manifest, ensure_ascii=False))
|
||
PY
|
||
)"
|
||
|
||
cat >"${report_dir}/README.txt" <<EOF
|
||
domainCheck ops-center doctor export
|
||
generated_at=$(date '+%Y-%m-%d %H:%M:%S')
|
||
active_profile=${OPS_CENTER_ACTIVE_PROFILE}
|
||
mainland_api_base_url=${mainland_base_url}
|
||
overseas_api_base_url=${overseas_base_url}
|
||
|
||
files:
|
||
- meta.json
|
||
- manifest.json
|
||
$(for file_name in "${report_files[@]}"; do echo "- ${file_name}"; done)
|
||
|
||
notes:
|
||
- preview reports are exported in optional mode
|
||
- if a current focus does not exist, the report file still exists and ends with command_exit_code=1
|
||
- manifest.json is the machine-readable summary for overseas Codex / future UI automation
|
||
EOF
|
||
|
||
python3 - <<'PY' "${report_dir}"
|
||
import json
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
report_dir = Path(sys.argv[1])
|
||
manifest_path = report_dir / "manifest.json"
|
||
decision = {}
|
||
summary = {}
|
||
if manifest_path.exists():
|
||
try:
|
||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||
except Exception:
|
||
manifest = {}
|
||
decision = dict(manifest.get("decision") or {})
|
||
summary = dict(manifest.get("summary") or {})
|
||
|
||
print(json.dumps({
|
||
"ok": True,
|
||
"report_dir": str(report_dir),
|
||
"meta_path": str(report_dir / "meta.json"),
|
||
"manifest_path": str(manifest_path),
|
||
"readme_path": str(report_dir / "README.txt"),
|
||
"summary": {
|
||
"ok": bool(summary.get("ok", False)),
|
||
"required_failures": list(summary.get("required_failures") or []),
|
||
"optional_unavailable": list(summary.get("optional_unavailable") or []),
|
||
"scene_log_reports_total": int(summary.get("scene_log_reports_total", 0) or 0),
|
||
"scene_log_reports_ok": int(summary.get("scene_log_reports_ok", 0) or 0),
|
||
"scene_log_status_counts": dict(summary.get("scene_log_status_counts") or {}),
|
||
"scene_log_reports": list(summary.get("scene_log_reports") or []),
|
||
"contract_surface_gaps": list(summary.get("contract_surface_gaps") or []),
|
||
"launchpad_recommended_target_node_code": str(summary.get("launchpad_recommended_target_node_code") or ""),
|
||
"launchpad_recommended_recovery_label": str(summary.get("launchpad_recommended_recovery_label") or ""),
|
||
"launchpad_recommended_recovery_summary": str(summary.get("launchpad_recommended_recovery_summary") or ""),
|
||
"launchpad_onboarding_bootstrap_pending_nodes": int(summary.get("launchpad_onboarding_bootstrap_pending_nodes", 0) or 0),
|
||
"launchpad_onboarding_acceptance_ready_nodes": int(summary.get("launchpad_onboarding_acceptance_ready_nodes", 0) or 0),
|
||
},
|
||
"decision": {
|
||
"status": str(decision.get("status") or ""),
|
||
"reason_code": str(decision.get("reason_code") or ""),
|
||
"headline": str(decision.get("headline") or ""),
|
||
"detail": str(decision.get("detail") or ""),
|
||
"preferred_surface": str(decision.get("preferred_surface") or ""),
|
||
"next_action_code": str(decision.get("next_action_code") or ""),
|
||
"recommended_commands": list(decision.get("recommended_commands") or []),
|
||
},
|
||
}, ensure_ascii=False, indent=2))
|
||
PY
|
||
}
|
||
|
||
print_doctor_manifest_summary() {
|
||
local manifest_target="$1"
|
||
python3 - <<'PY' "${manifest_target}"
|
||
import json
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
target = Path(str(sys.argv[1] or "").strip())
|
||
if target.is_dir():
|
||
target = target / "manifest.json"
|
||
|
||
if not target.exists():
|
||
raise SystemExit(f"manifest not found: {target}")
|
||
|
||
payload = json.loads(target.read_text(encoding="utf-8"))
|
||
print(json.dumps({
|
||
"ok": bool(payload.get("summary", {}).get("ok", False)),
|
||
"report_dir": str(payload.get("report_dir") or target.parent),
|
||
"manifest_path": str(target),
|
||
"summary": {
|
||
"ok": bool(payload.get("summary", {}).get("ok", False)),
|
||
"required_failures": list(payload.get("summary", {}).get("required_failures") or []),
|
||
"required_failure_details": list(payload.get("summary", {}).get("required_failure_details") or []),
|
||
"optional_unavailable": list(payload.get("summary", {}).get("optional_unavailable") or []),
|
||
"optional_unavailable_details": list(payload.get("summary", {}).get("optional_unavailable_details") or []),
|
||
"scene_log_reports_total": int(payload.get("summary", {}).get("scene_log_reports_total", 0) or 0),
|
||
"scene_log_reports_ok": int(payload.get("summary", {}).get("scene_log_reports_ok") or 0),
|
||
"scene_log_status_counts": dict(payload.get("summary", {}).get("scene_log_status_counts") or {}),
|
||
"scene_log_reports": list(payload.get("summary", {}).get("scene_log_reports") or []),
|
||
"contract_surface_gaps": list(payload.get("summary", {}).get("contract_surface_gaps") or []),
|
||
"discovered_contract_keys": list(payload.get("summary", {}).get("discovered_contract_keys") or []),
|
||
"launchpad_recommended_target_node_code": str(payload.get("summary", {}).get("launchpad_recommended_target_node_code") or ""),
|
||
"launchpad_recommended_recovery_label": str(payload.get("summary", {}).get("launchpad_recommended_recovery_label") or ""),
|
||
"launchpad_recommended_recovery_summary": str(payload.get("summary", {}).get("launchpad_recommended_recovery_summary") or ""),
|
||
"launchpad_onboarding_bootstrap_pending_nodes": int(payload.get("summary", {}).get("launchpad_onboarding_bootstrap_pending_nodes", 0) or 0),
|
||
"launchpad_onboarding_acceptance_ready_nodes": int(payload.get("summary", {}).get("launchpad_onboarding_acceptance_ready_nodes", 0) or 0),
|
||
},
|
||
"decision": dict(payload.get("decision") or {}),
|
||
}, ensure_ascii=False, indent=2))
|
||
PY
|
||
}
|
||
|
||
run_doctor_decision() {
|
||
local first_arg="${1:-}"
|
||
local second_arg="${2:-}"
|
||
local report_target=""
|
||
|
||
if [[ -n "${first_arg}" && ( -f "${first_arg}" || -d "${first_arg}" ) ]]; then
|
||
report_target="${first_arg}"
|
||
print_doctor_manifest_summary "${report_target}"
|
||
return 0
|
||
fi
|
||
|
||
local mainland_base_url="${first_arg:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
local overseas_base_url
|
||
overseas_base_url="$(normalize_optional_base_url_arg "${second_arg}" "$(ops_center_resolve_overseas_api_base_url)")"
|
||
local report_dir
|
||
report_dir="$(mktemp -d /tmp/domaincheck-doctor-export-XXXXXX)"
|
||
run_doctor_report_export "${report_dir}" "${mainland_base_url}" "${overseas_base_url}" >/dev/null
|
||
print_doctor_manifest_summary "${report_dir}/manifest.json"
|
||
}
|
||
|
||
export_agent_plan() {
|
||
local report_dir="$1"
|
||
local mainland_base_url="$2"
|
||
local node_code="$3"
|
||
local node_region="$4"
|
||
local node_role="$5"
|
||
local control_plane_base_url="$6"
|
||
local root_dir="$7"
|
||
local timestamp
|
||
timestamp="$(timestamp_now)"
|
||
local safe_node_code
|
||
safe_node_code="$(printf '%s' "${node_code}" | tr '/:' '__')"
|
||
ops_center_ensure_report_dir "${report_dir}"
|
||
local output_path="${report_dir}/agent-plan-${safe_node_code}-${timestamp}.txt"
|
||
ISSUED_BY="$(ops_center_resolve_issued_by)" \
|
||
EXPIRES_IN_HOURS="$(ops_center_resolve_expires_in_hours)" \
|
||
bash "${SCRIPT_DIR}/build_node_agent_bootstrap_plan.sh" \
|
||
"${mainland_base_url}" \
|
||
"${node_code}" \
|
||
"${node_region}" \
|
||
"${node_role}" \
|
||
"${control_plane_base_url}" \
|
||
"${root_dir}" | tee "${output_path}"
|
||
|
||
write_json_file "${output_path}.meta.json" "$(python3 - \
|
||
"${node_code}" "${node_region}" "${node_role}" "${mainland_base_url}" "${control_plane_base_url}" "${root_dir}" "${output_path}" <<'PY'
|
||
import json
|
||
import sys
|
||
from datetime import datetime
|
||
|
||
print(json.dumps({
|
||
"generated_at": datetime.now().isoformat(timespec="seconds"),
|
||
"node_code": sys.argv[1],
|
||
"node_region": sys.argv[2],
|
||
"node_role": sys.argv[3],
|
||
"mainland_api_base_url": sys.argv[4],
|
||
"control_plane_base_url": sys.argv[5],
|
||
"root_dir": sys.argv[6],
|
||
"output_path": sys.argv[7],
|
||
}, ensure_ascii=False))
|
||
PY
|
||
)"
|
||
|
||
python3 - <<'PY' "${output_path}" "${output_path}.meta.json"
|
||
import json
|
||
import sys
|
||
|
||
print(json.dumps({
|
||
"ok": True,
|
||
"output_path": sys.argv[1],
|
||
"meta_path": sys.argv[2],
|
||
}, ensure_ascii=False, indent=2))
|
||
PY
|
||
}
|
||
|
||
run_publish_latest() {
|
||
local mainland_base_url="$1"
|
||
local mode="$2"
|
||
local confirm_flag="$3"
|
||
local skip_smoke_flag="$4"
|
||
local repo_root
|
||
repo_root="$(ops_center_repo_root)"
|
||
local skip_smoke
|
||
skip_smoke="$(confirm_to_bool "${skip_smoke_flag}")"
|
||
|
||
if [[ "${mode}" != "worker" && "${mode}" != "control" ]]; then
|
||
echo "mode must be worker or control" >&2
|
||
usage
|
||
exit 1
|
||
fi
|
||
|
||
echo "============================================================"
|
||
echo "[publish/1] package latest"
|
||
echo "============================================================"
|
||
if [[ "${skip_smoke}" == "true" ]]; then
|
||
DOMAINCHECK_SKIP_SMOKE_TEST=1 bash "${repo_root}/package_domain_release.sh"
|
||
else
|
||
bash "${repo_root}/package_domain_release.sh"
|
||
fi
|
||
echo
|
||
|
||
echo "============================================================"
|
||
echo "[publish/2] preview smart rollout from latest package"
|
||
echo "============================================================"
|
||
bash "${SCRIPT_DIR}/drive_release_hub.sh" "${mainland_base_url}" preview-latest-smart "${mode}"
|
||
echo
|
||
|
||
echo "============================================================"
|
||
echo "[publish/3] smart rollout from latest package"
|
||
echo "============================================================"
|
||
bash "${SCRIPT_DIR}/drive_release_hub.sh" "${mainland_base_url}" smart-latest "${mode}" "${confirm_flag}"
|
||
}
|
||
|
||
show_ops_contracts() {
|
||
local mainland_base_url="$1"
|
||
local contract_key="${2:-}"
|
||
local raw_http_response
|
||
if [[ -n "${contract_key}" ]]; then
|
||
raw_http_response="$(request_with_status "GET" "${mainland_base_url}/api/v1/ops/contracts/${contract_key}")"
|
||
else
|
||
raw_http_response="$(request_with_status "GET" "${mainland_base_url}/api/v1/ops/contracts")"
|
||
fi
|
||
local http_status="${raw_http_response##*$'\n'HTTP_STATUS=}"
|
||
local response_json="${raw_http_response%$'\n'HTTP_STATUS=*}"
|
||
|
||
echo "[1/2] raw response"
|
||
printf '%s\n' "http_status=${http_status}"
|
||
print_json_or_raw "${response_json}"
|
||
echo
|
||
echo "[2/2] condensed summary"
|
||
python3 - <<'PY' "${response_json}" "${http_status}" "${contract_key}"
|
||
import json
|
||
import sys
|
||
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
http_status = int(sys.argv[2] or 0)
|
||
contract_key = str(sys.argv[3] or "").strip()
|
||
data = payload.get("data") or {}
|
||
|
||
if contract_key:
|
||
contract = data.get("contract") or {}
|
||
related_contracts = list(data.get("related_contracts") or [])
|
||
print(json.dumps({
|
||
"http_status": http_status,
|
||
"requested_contract_key": contract_key,
|
||
"registry_version": str(data.get("registry_version") or ""),
|
||
"schema_version": str(data.get("schema_version") or ""),
|
||
"docs_root": str(data.get("docs_root") or ""),
|
||
"detail_endpoint": str(data.get("detail_endpoint") or ""),
|
||
"contract": {
|
||
"key": str(contract.get("key") or ""),
|
||
"title": str(contract.get("title") or ""),
|
||
"status": str(contract.get("status") or ""),
|
||
"version": str(contract.get("version") or ""),
|
||
"primary_endpoint": str(contract.get("primary_endpoint") or ""),
|
||
"schema_doc_path": str(contract.get("schema_doc_path") or ""),
|
||
"summary": str(contract.get("summary") or ""),
|
||
"consumers": list(contract.get("consumers") or []),
|
||
"service_keys": list(contract.get("service_keys") or []),
|
||
"related_contract_keys": list(contract.get("related_contract_keys") or data.get("related_contract_keys") or []),
|
||
"discovery_endpoints": list(contract.get("discovery_endpoints") or []),
|
||
},
|
||
"related_contracts": [
|
||
{
|
||
"key": str(item.get("key") or ""),
|
||
"title": str(item.get("title") or ""),
|
||
"version": str(item.get("version") or ""),
|
||
"primary_endpoint": str(item.get("primary_endpoint") or ""),
|
||
}
|
||
for item in related_contracts
|
||
],
|
||
}, ensure_ascii=False, indent=2))
|
||
else:
|
||
contracts = list(data.get("contracts") or [])
|
||
print(json.dumps({
|
||
"http_status": http_status,
|
||
"registry_version": str(data.get("registry_version") or ""),
|
||
"schema_version": str(data.get("schema_version") or ""),
|
||
"contracts_total": int(data.get("contracts_total", 0) or 0),
|
||
"docs_root": str(data.get("docs_root") or ""),
|
||
"discovery_entrypoints": list(data.get("discovery_entrypoints") or []),
|
||
"contracts": [
|
||
{
|
||
"key": str(item.get("key") or ""),
|
||
"version": str(item.get("version") or ""),
|
||
"primary_endpoint": str(item.get("primary_endpoint") or ""),
|
||
"schema_doc_path": str(item.get("schema_doc_path") or ""),
|
||
"related_contract_keys": list(item.get("related_contract_keys") or []),
|
||
}
|
||
for item in contracts
|
||
],
|
||
}, ensure_ascii=False, indent=2))
|
||
PY
|
||
}
|
||
|
||
show_activity_stream() {
|
||
local mainland_base_url="$1"
|
||
shift || true
|
||
|
||
local request_url
|
||
request_url="$(python3 - <<'PY' "${mainland_base_url}" "$@"
|
||
import sys
|
||
from urllib.parse import urlencode
|
||
|
||
base_url = str(sys.argv[1] or "").rstrip("/")
|
||
tokens = [str(item or "").strip() for item in sys.argv[2:] if str(item or "").strip()]
|
||
params = {}
|
||
allowed = {"limit", "kind", "status", "execution_mode", "query"}
|
||
for token in tokens:
|
||
if "=" in token:
|
||
key, value = token.split("=", 1)
|
||
key = str(key or "").strip()
|
||
value = str(value or "").strip()
|
||
if key in allowed and value:
|
||
params[key] = value
|
||
elif "query" not in params:
|
||
params["query"] = token
|
||
|
||
query_string = urlencode(params)
|
||
endpoint = f"{base_url}/api/v1/ops/activity-stream"
|
||
print(f"{endpoint}?{query_string}" if query_string else endpoint)
|
||
PY
|
||
)"
|
||
|
||
local raw_http_response
|
||
raw_http_response="$(request_with_status "GET" "${request_url}")"
|
||
local http_status="${raw_http_response##*$'\n'HTTP_STATUS=}"
|
||
local response_json="${raw_http_response%$'\n'HTTP_STATUS=*}"
|
||
|
||
echo "[1/2] raw response"
|
||
printf '%s\n' "http_status=${http_status}"
|
||
printf '%s\n' "request_url=${request_url}"
|
||
print_json_or_raw "${response_json}"
|
||
echo
|
||
echo "[2/2] condensed summary"
|
||
python3 - <<'PY' "${response_json}" "${http_status}" "${request_url}"
|
||
import json
|
||
import sys
|
||
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
http_status = int(sys.argv[2] or 0)
|
||
request_url = str(sys.argv[3] or "").strip()
|
||
data = payload.get("data") or {}
|
||
summary = data.get("summary") or {}
|
||
items = list(data.get("items") or [])
|
||
contract_navigation = data.get("contract_navigation") or {}
|
||
|
||
print(json.dumps({
|
||
"http_status": http_status,
|
||
"request_url": request_url,
|
||
"contract_navigation": {
|
||
"primary_contract_key": str(contract_navigation.get("primary_contract_key") or ""),
|
||
"contract_keys": list(contract_navigation.get("contract_keys") or []),
|
||
"detail_endpoint_pattern": str(contract_navigation.get("detail_endpoint_pattern") or ""),
|
||
},
|
||
"summary": {
|
||
"status": str(summary.get("status") or ""),
|
||
"status_label": str(summary.get("status_label") or ""),
|
||
"summary_text": str(summary.get("summary_text") or ""),
|
||
"total": int(summary.get("total", 0) or 0),
|
||
"filtered_total": int(summary.get("filtered_total", 0) or 0),
|
||
"unfiltered_total": int(summary.get("unfiltered_total", 0) or 0),
|
||
"latest_at": str(summary.get("latest_at") or ""),
|
||
"filters": dict(summary.get("filters") or {}),
|
||
"kind_counts": dict(summary.get("kind_counts") or {}),
|
||
"status_counts": dict(summary.get("status_counts") or {}),
|
||
},
|
||
"items": [
|
||
{
|
||
"activity_key": str(item.get("activity_key") or ""),
|
||
"kind": str(item.get("kind") or ""),
|
||
"title": str(item.get("title") or ""),
|
||
"status": str(item.get("status") or ""),
|
||
"status_label": str(item.get("status_label") or ""),
|
||
"execution_mode": str(item.get("execution_mode") or ""),
|
||
"occurred_at": str(item.get("occurred_at") or ""),
|
||
"primary_contract_key": str((item.get("contract_navigation") or {}).get("primary_contract_key") or ""),
|
||
"contract_keys": list((item.get("contract_navigation") or {}).get("contract_keys") or []),
|
||
"focus_ref": item.get("focus_ref") or {},
|
||
"ui_intent": item.get("ui_intent") or {},
|
||
}
|
||
for item in items
|
||
],
|
||
}, ensure_ascii=False, indent=2))
|
||
PY
|
||
}
|
||
|
||
build_activity_preview_bundle() {
|
||
local activity_response_json="$1"
|
||
local activity_key="$2"
|
||
local requested_by="$3"
|
||
local source_label="$4"
|
||
python3 - <<'PY' "${activity_response_json}" "${activity_key}" "${requested_by}" "${source_label}"
|
||
import json
|
||
import sys
|
||
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
requested_activity_key = str(sys.argv[2] or "").strip()
|
||
requested_by = str(sys.argv[3] or "").strip() or "cli/ops-center/activity-preview"
|
||
source_label = str(sys.argv[4] or "").strip() or "cli/activity-preview"
|
||
data = payload.get("data") or {}
|
||
items = [dict(item or {}) for item in list(data.get("items") or []) if dict(item or {})]
|
||
|
||
target_item = {}
|
||
if requested_activity_key:
|
||
target_item = next((item for item in items if str(item.get("activity_key") or "").strip() == requested_activity_key), {})
|
||
if not target_item:
|
||
target_item = next((item for item in items if str((item.get("ui_intent") or {}).get("kind") or "").strip()), {})
|
||
if not target_item:
|
||
raise SystemExit("activity-stream 当前没有可预览的活动项")
|
||
|
||
ui_intent = dict(target_item.get("ui_intent") or {})
|
||
if not str(ui_intent.get("kind") or "").strip():
|
||
raise SystemExit("目标活动项没有 ui_intent,当前不能生成 preview")
|
||
|
||
kind = str(target_item.get("kind") or "").strip()
|
||
if kind == "playbook_run":
|
||
primary_label = "查看编排"
|
||
elif kind == "rollout":
|
||
primary_label = "查看任务"
|
||
elif kind == "runbook_sequence":
|
||
primary_label = "查看路径"
|
||
elif kind in {"execution_scene", "log_sync"}:
|
||
primary_label = "查看现场"
|
||
elif str(ui_intent.get("kind") or "").strip() == "job_detail":
|
||
primary_label = "看详情"
|
||
else:
|
||
primary_label = "看事件"
|
||
|
||
focus_ref = dict(target_item.get("focus_ref") or {})
|
||
request_payload = {
|
||
"source_label": source_label,
|
||
"title": str(target_item.get("title") or "").strip() or str(target_item.get("activity_key") or "").strip() or "activity-preview",
|
||
"summary": str(target_item.get("summary_text") or target_item.get("summary") or "").strip(),
|
||
"reason": str(target_item.get("summary_text") or target_item.get("summary") or "").strip() or "建议先进入该活动项对应的工作区。",
|
||
"meta_text": str(target_item.get("meta_text") or "").strip(),
|
||
"executor_kind": "driver_action",
|
||
"focus_ref": focus_ref,
|
||
"primary_focus_ref": focus_ref,
|
||
"primary_label": primary_label,
|
||
"primary_action_code": "focus_activity_item",
|
||
"primary_action_payload": {
|
||
"activity_key": str(target_item.get("activity_key") or "").strip(),
|
||
"kind": kind,
|
||
"status": str(target_item.get("status") or "").strip(),
|
||
"ui_intent": ui_intent,
|
||
"focus_ref": focus_ref,
|
||
},
|
||
"include_secondary": False,
|
||
"requested_by": requested_by,
|
||
}
|
||
|
||
print(json.dumps({
|
||
"selected_entry": {
|
||
"activity_key": str(target_item.get("activity_key") or "").strip(),
|
||
"kind": kind,
|
||
"title": str(target_item.get("title") or "").strip(),
|
||
"status": str(target_item.get("status") or "").strip(),
|
||
"status_label": str(target_item.get("status_label") or "").strip(),
|
||
"summary": str(target_item.get("summary_text") or target_item.get("summary") or "").strip(),
|
||
"focus_ref": focus_ref,
|
||
"ui_intent": ui_intent,
|
||
"contract_navigation": dict(target_item.get("contract_navigation") or {}),
|
||
},
|
||
"request": request_payload,
|
||
}, ensure_ascii=False))
|
||
PY
|
||
}
|
||
|
||
preview_activity_item() {
|
||
local mainland_base_url="$1"
|
||
local activity_key="$2"
|
||
local requested_by="$3"
|
||
|
||
local raw_activity_response
|
||
raw_activity_response="$(request_with_status "GET" "${mainland_base_url}/api/v1/ops/activity-stream?limit=50")"
|
||
local activity_http_status="${raw_activity_response##*$'\n'HTTP_STATUS=}"
|
||
local activity_response_json="${raw_activity_response%$'\n'HTTP_STATUS=*}"
|
||
|
||
if [[ "${activity_http_status}" -lt 200 || "${activity_http_status}" -ge 300 ]]; then
|
||
echo "[1/3] activity stream raw response"
|
||
printf '%s\n' "http_status=${activity_http_status}"
|
||
print_json_or_raw "${activity_response_json}"
|
||
exit 1
|
||
fi
|
||
|
||
local bundle_json
|
||
bundle_json="$(build_activity_preview_bundle "${activity_response_json}" "${activity_key}" "${requested_by}" "cli/activity-preview")"
|
||
local selected_entry_json
|
||
selected_entry_json="$(python3 - <<'PY' "${bundle_json}"
|
||
import json
|
||
import sys
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
print(json.dumps(payload.get("selected_entry") or {}, ensure_ascii=False))
|
||
PY
|
||
)"
|
||
local request_json
|
||
request_json="$(python3 - <<'PY' "${bundle_json}"
|
||
import json
|
||
import sys
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
print(json.dumps(payload.get("request") or {}, ensure_ascii=False))
|
||
PY
|
||
)"
|
||
|
||
echo "[1/4] selected activity"
|
||
print_json_or_raw "${selected_entry_json}"
|
||
echo
|
||
echo
|
||
|
||
echo "[2/4] request"
|
||
print_json_or_raw "${request_json}"
|
||
echo
|
||
echo
|
||
|
||
local raw_http_response
|
||
raw_http_response="$(request_with_status "POST" "${mainland_base_url}/api/v1/ops/driver-actions/resolve" "${request_json}")"
|
||
local http_status="${raw_http_response##*$'\n'HTTP_STATUS=}"
|
||
local response_json="${raw_http_response%$'\n'HTTP_STATUS=*}"
|
||
|
||
echo "[3/4] raw response"
|
||
printf '%s\n' "http_status=${http_status}"
|
||
print_json_or_raw "${response_json}"
|
||
echo
|
||
echo
|
||
|
||
echo "[4/4] condensed summary"
|
||
python3 - <<'PY' "${response_json}" "${http_status}" "${selected_entry_json}"
|
||
import json
|
||
import sys
|
||
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
http_status = int(sys.argv[2] or 0)
|
||
selected = json.loads(str(sys.argv[3] or "{}"))
|
||
data = payload.get("data") or {}
|
||
preview = data.get("preview") or {}
|
||
primary = preview.get("primary") or {}
|
||
gate = data.get("gate") or {}
|
||
|
||
print(json.dumps({
|
||
"http_status": http_status,
|
||
"message": str(payload.get("message") or ""),
|
||
"selected_entry": selected,
|
||
"contract": {
|
||
"contract_key": str(preview.get("contract_key") or ""),
|
||
"contract_version": str(preview.get("contract_version") or ""),
|
||
"contract_primary_endpoint": str(preview.get("contract_primary_endpoint") or ""),
|
||
"preview_endpoint": str(preview.get("preview_endpoint") or ""),
|
||
"schema_doc_path": str(preview.get("contract_schema_doc_path") or ""),
|
||
},
|
||
"execution_chain": str(preview.get("execution_chain") or ""),
|
||
"focus_ref": data.get("focus_ref") or {},
|
||
"gate": {
|
||
"decision": str(gate.get("decision") or ""),
|
||
"decision_label": str(gate.get("decision_label") or ""),
|
||
"recommendation": str(gate.get("recommendation") or ""),
|
||
"recommendation_label": str(gate.get("recommendation_label") or ""),
|
||
"will_execute": bool(gate.get("will_execute", False)),
|
||
"confirm_required": bool(gate.get("confirm_required", False)),
|
||
"target_api": str(gate.get("target_api") or ""),
|
||
"focus_ref": gate.get("focus_ref") or {},
|
||
},
|
||
"primary": {
|
||
"label": str(primary.get("label") or ""),
|
||
"action_code": str(primary.get("action_code") or ""),
|
||
"target_api": str(primary.get("target_api") or ""),
|
||
"recommendation_label": str(primary.get("recommendation_label") or ""),
|
||
"focus_ref": primary.get("focus_ref") or {},
|
||
"request_payload": primary.get("request_payload") or {},
|
||
},
|
||
}, ensure_ascii=False, indent=2))
|
||
PY
|
||
|
||
if [[ "${http_status}" -lt 200 || "${http_status}" -ge 300 ]]; then
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
show_delivery_queue_status() {
|
||
local mainland_base_url="$1"
|
||
local node_code="$2"
|
||
if [[ -z "${node_code}" ]]; then
|
||
echo "node_code is required" >&2
|
||
exit 1
|
||
fi
|
||
local raw_http_response
|
||
raw_http_response="$(request_with_status "GET" "${mainland_base_url}/api/v1/ops/nodes/${node_code}/delivery-queue")"
|
||
local http_status="${raw_http_response##*$'\n'HTTP_STATUS=}"
|
||
local response_json="${raw_http_response%$'\n'HTTP_STATUS=*}"
|
||
echo "[1/2] raw response"
|
||
printf '%s\n' "http_status=${http_status}"
|
||
print_json_or_raw "${response_json}"
|
||
echo
|
||
echo "[2/2] condensed summary"
|
||
python3 - <<'PY' "${response_json}" "${http_status}"
|
||
import json
|
||
import sys
|
||
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
http_status = int(sys.argv[2] or 0)
|
||
data = payload.get("data") or {}
|
||
summary = data.get("summary") or {}
|
||
node = data.get("node") or {}
|
||
records_capability = data.get("records_capability") or {}
|
||
print(json.dumps({
|
||
"http_status": http_status,
|
||
"node": {
|
||
"node_code": str(node.get("node_code") or ""),
|
||
"region": str(node.get("region") or ""),
|
||
"role": str(node.get("role") or ""),
|
||
"agent_state_label": str(node.get("agent_state_label") or ""),
|
||
"remote_access_label": str(node.get("remote_access_label") or ""),
|
||
},
|
||
"summary": {
|
||
"state": str(summary.get("state") or ""),
|
||
"label": str(summary.get("label") or ""),
|
||
"reason": str(summary.get("reason") or ""),
|
||
"pending_count": int(summary.get("pending_count", 0) or 0),
|
||
"dead_letter_count": int(summary.get("dead_letter_count", 0) or 0),
|
||
"last_flush_at": str(summary.get("last_flush_at") or ""),
|
||
"oldest_pending_at": str(summary.get("oldest_pending_at") or ""),
|
||
"oldest_dead_letter_at": str(summary.get("oldest_dead_letter_at") or ""),
|
||
},
|
||
"records_capability": {
|
||
"record_visibility": str(records_capability.get("record_visibility") or ""),
|
||
"record_source": str(records_capability.get("record_source") or ""),
|
||
"full_records_available": bool(records_capability.get("full_records_available", False)),
|
||
},
|
||
"recommended_commands": {
|
||
"records": (
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh delivery-queue-records http://127.0.0.1:8100 {str(node.get('node_code') or '')}"
|
||
if str(node.get("node_code") or "").strip()
|
||
else ""
|
||
),
|
||
"flush": (
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh delivery-queue-flush http://127.0.0.1:8100 {str(node.get('node_code') or '')}"
|
||
if str(node.get("node_code") or "").strip()
|
||
else ""
|
||
),
|
||
"replay": (
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh delivery-queue-replay http://127.0.0.1:8100 {str(node.get('node_code') or '')}"
|
||
if str(node.get("node_code") or "").strip()
|
||
else ""
|
||
),
|
||
},
|
||
"head_records": data.get("head_records") or {},
|
||
}, ensure_ascii=False, indent=2))
|
||
PY
|
||
}
|
||
|
||
show_node_handover() {
|
||
local mainland_base_url="$1"
|
||
local node_code="$2"
|
||
if [[ -z "${node_code}" ]]; then
|
||
echo "node_code is required" >&2
|
||
exit 1
|
||
fi
|
||
local raw_http_response
|
||
raw_http_response="$(request_with_status "GET" "${mainland_base_url}/api/v1/ops/nodes/${node_code}/handover")"
|
||
local http_status="${raw_http_response##*$'\n'HTTP_STATUS=}"
|
||
local response_json="${raw_http_response%$'\n'HTTP_STATUS=*}"
|
||
local effective_http_status="${http_status}"
|
||
local fallback_json=""
|
||
|
||
if [[ "${http_status}" == "404" ]]; then
|
||
local stack_http_response stack_http_status stack_response_json
|
||
stack_http_response="$(request_with_status "GET" "${mainland_base_url}/api/v1/ops/stack-diagnosis?base_url=${mainland_base_url}")"
|
||
stack_http_status="${stack_http_response##*$'\n'HTTP_STATUS=}"
|
||
stack_response_json="${stack_http_response%$'\n'HTTP_STATUS=*}"
|
||
if [[ "${stack_http_status}" -ge 200 && "${stack_http_status}" -lt 300 ]]; then
|
||
fallback_json="$(python3 - <<'PY' "${stack_response_json}" "${node_code}" "${http_status}"
|
||
import json
|
||
import sys
|
||
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
target_node_code = str(sys.argv[2] or "").strip()
|
||
backend_http_status = int(sys.argv[3] or 404)
|
||
data = payload.get("data") or {}
|
||
managed_nodes = data.get("managed_nodes") or {}
|
||
problem_nodes = [dict(item or {}) for item in list(managed_nodes.get("problem_nodes") or []) if isinstance(item, dict)]
|
||
|
||
node = next(
|
||
(item for item in problem_nodes if str(item.get("node_code") or "").strip() == target_node_code),
|
||
{},
|
||
)
|
||
first_gap = dict(managed_nodes.get("first_handover_gap") or {})
|
||
if not node and str(first_gap.get("node_code") or "").strip() == target_node_code:
|
||
node = dict(first_gap.get("node") or {})
|
||
if not node:
|
||
node = {"node_code": target_node_code}
|
||
|
||
agent_state = str(node.get("agent_state") or "").strip()
|
||
remote_access_state = str(node.get("remote_access_state") or "").strip()
|
||
summary = "当前节点存在接管缺口,建议先生成 bootstrap 方案并检查 Node Agent 接入。"
|
||
stage_code = "unknown"
|
||
stage_label = "待判断"
|
||
stage_summary = summary
|
||
next_step = {
|
||
"code": "managed_node_handover",
|
||
"label": "先看节点接管详情",
|
||
"summary": "建议先确认节点为什么还没进入 remote-agent 就绪状态。",
|
||
}
|
||
|
||
if agent_state == "pending_bootstrap":
|
||
stage_code = "pending_bootstrap"
|
||
stage_label = "待执行接入"
|
||
stage_summary = "已签发有效 Token,但目标节点尚未执行 bootstrap 片段。"
|
||
next_step = {
|
||
"code": "execute_bootstrap_plan",
|
||
"label": "去目标机执行接入片段",
|
||
"summary": "把 bootstrap env 和一键落地片段放到目标机执行,然后观察 register / heartbeat 是否建立。",
|
||
}
|
||
elif agent_state == "runtime_only":
|
||
stage_code = "runtime_only"
|
||
stage_label = "仅运行态在线"
|
||
stage_summary = "控制面能看到 runtime 心跳,但 Node Agent 尚未接管。"
|
||
next_step = {
|
||
"code": "issue_bootstrap_plan",
|
||
"label": "生成接入方案",
|
||
"summary": "下一步应生成 bootstrap env、脚本和一键落地命令,再在目标节点启动 domaincheck-node-agent。",
|
||
}
|
||
elif agent_state == "stale":
|
||
stage_code = "stale"
|
||
stage_label = "心跳过期"
|
||
stage_summary = "Node Agent 曾接入过,但心跳已过期,需先恢复服务或重新接入。"
|
||
next_step = {
|
||
"code": "recover_agent",
|
||
"label": "优先恢复 Agent",
|
||
"summary": "先检查 domaincheck-node-agent 服务、网络连通性和控制面地址,再决定是否重新签发接入方案。",
|
||
}
|
||
elif remote_access_state == "agent_pending":
|
||
stage_code = "agent_pending"
|
||
stage_label = "待接入"
|
||
stage_summary = "当前 remote-agent 仍未就绪,先补齐 bootstrap 接入链路。"
|
||
|
||
result = {
|
||
"http_status": 200,
|
||
"backend_http_status": backend_http_status,
|
||
"fallback_used": True,
|
||
"fallback_source": "stack_diagnosis",
|
||
"compatibility_warning": "当前运行中的 API 尚未暴露 /api/v1/ops/nodes/{node_code}/handover,已降级为 stack-diagnosis 兼容视图。",
|
||
"node_code": target_node_code,
|
||
"summary": stage_summary,
|
||
"stage": {
|
||
"code": stage_code,
|
||
"label": stage_label,
|
||
"summary": stage_summary,
|
||
"next_step": next_step,
|
||
},
|
||
"readiness": {
|
||
"is_managed": True,
|
||
"is_enabled": True,
|
||
"has_ssh_access": False,
|
||
"is_agent_online": agent_state in {"online", "online_busy"},
|
||
"is_remote_access_ready": False,
|
||
},
|
||
"ssh_profile": {
|
||
"host": "",
|
||
"port": 22,
|
||
"user": "",
|
||
"label": "",
|
||
"reason": "兼容视图未携带 SSH 资料,请到托管节点详情页补看。",
|
||
"ssh_command_hint": "",
|
||
},
|
||
"cluster": {
|
||
"status": "",
|
||
"current_load": 0,
|
||
"last_heartbeat_at": "",
|
||
},
|
||
"blocking_items": [stage_summary] if stage_code in {"pending_bootstrap", "runtime_only", "stale", "agent_pending"} else [],
|
||
"attention_items": [
|
||
"当前是兼容降级视图,如需完整 SSH / Token / delivery queue 详情,请升级控制面 API 到包含 handover 路由的版本。"
|
||
],
|
||
"bootstrap_defaults": {
|
||
"control_plane_base_url": "",
|
||
"root_dir": "/opt/domaincheck",
|
||
"node_region": "",
|
||
"node_role": "",
|
||
},
|
||
"endpoints": {
|
||
"detail": f"/api/v1/ops/nodes/{target_node_code}/handover",
|
||
"bootstrap_plan": f"/api/v1/ops/nodes/{target_node_code}/handover/bootstrap-plan",
|
||
},
|
||
}
|
||
|
||
print(json.dumps(result, ensure_ascii=False))
|
||
PY
|
||
)"
|
||
if [[ -n "${fallback_json}" ]]; then
|
||
effective_http_status="200"
|
||
fi
|
||
fi
|
||
fi
|
||
echo "[1/2] raw response"
|
||
printf '%s\n' "http_status=${http_status}"
|
||
print_json_or_raw "${response_json}"
|
||
if [[ -n "${fallback_json}" ]]; then
|
||
echo
|
||
printf '%s\n' "fallback=stack_diagnosis"
|
||
print_json_or_raw "${fallback_json}"
|
||
fi
|
||
echo
|
||
echo "[2/2] condensed summary"
|
||
python3 - <<'PY' "${response_json}" "${effective_http_status}" "${fallback_json}"
|
||
import json
|
||
import pathlib
|
||
import sys
|
||
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
http_status = int(sys.argv[2] or 0)
|
||
fallback_json = str(sys.argv[3] or "").strip()
|
||
if fallback_json:
|
||
data = json.loads(fallback_json)
|
||
else:
|
||
data = payload.get("data") or {}
|
||
stage = data.get("stage") or {}
|
||
readiness = data.get("readiness") or {}
|
||
ssh_profile = data.get("ssh_profile") or {}
|
||
cluster = data.get("cluster") or {}
|
||
bootstrap_defaults = data.get("bootstrap_defaults") or {}
|
||
node_code = str(data.get("node_code") or "").strip()
|
||
next_step = stage.get("next_step") or {}
|
||
print(json.dumps({
|
||
"http_status": http_status,
|
||
"backend_http_status": int(data.get("backend_http_status", 0) or 0) if fallback_json else http_status,
|
||
"fallback_used": bool(data.get("fallback_used", False)),
|
||
"fallback_source": str(data.get("fallback_source") or ""),
|
||
"compatibility_warning": str(data.get("compatibility_warning") or ""),
|
||
"node_code": node_code,
|
||
"summary": str(data.get("summary") or ""),
|
||
"stage": {
|
||
"code": str(stage.get("code") or ""),
|
||
"label": str(stage.get("label") or ""),
|
||
"summary": str(stage.get("summary") or ""),
|
||
"next_step": next_step,
|
||
},
|
||
"readiness": {
|
||
"is_managed": bool(readiness.get("is_managed", False)),
|
||
"is_enabled": bool(readiness.get("is_enabled", False)),
|
||
"has_ssh_access": bool(readiness.get("has_ssh_access", False)),
|
||
"is_agent_online": bool(readiness.get("is_agent_online", False)),
|
||
"is_remote_access_ready": bool(readiness.get("is_remote_access_ready", False)),
|
||
},
|
||
"ssh_profile": {
|
||
"host": str(ssh_profile.get("host") or ""),
|
||
"port": int(ssh_profile.get("port", 22) or 22),
|
||
"user": str(ssh_profile.get("user") or ""),
|
||
"label": str(ssh_profile.get("label") or ""),
|
||
"reason": str(ssh_profile.get("reason") or ""),
|
||
"ssh_command_hint": str(ssh_profile.get("ssh_command_hint") or ""),
|
||
},
|
||
"cluster": {
|
||
"status": str(cluster.get("status") or ""),
|
||
"current_load": int(cluster.get("current_load", 0) or 0),
|
||
"last_heartbeat_at": str(cluster.get("last_heartbeat_at") or ""),
|
||
},
|
||
"blocking_items": list(data.get("blocking_items") or []),
|
||
"attention_items": list(data.get("attention_items") or []),
|
||
"bootstrap_defaults": {
|
||
"control_plane_base_url": str(bootstrap_defaults.get("control_plane_base_url") or ""),
|
||
"root_dir": str(bootstrap_defaults.get("root_dir") or ""),
|
||
"node_region": str(bootstrap_defaults.get("node_region") or ""),
|
||
"node_role": str(bootstrap_defaults.get("node_role") or ""),
|
||
},
|
||
"endpoints": data.get("endpoints") or {},
|
||
"recommended_commands": {
|
||
"bootstrap_plan": (
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh node-bootstrap-plan http://127.0.0.1:8100 {node_code}"
|
||
if node_code
|
||
else ""
|
||
),
|
||
"delivery_queue": (
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh delivery-queue http://127.0.0.1:8100 {node_code}"
|
||
if node_code
|
||
else ""
|
||
),
|
||
"node_agent_check": "bash domain-api/deploy/multi-region/check_node_agent.sh",
|
||
"ssh_hint": str(ssh_profile.get("ssh_command_hint") or ""),
|
||
},
|
||
}, ensure_ascii=False, indent=2))
|
||
PY
|
||
}
|
||
|
||
show_ops_nodes() {
|
||
local mainland_base_url="$1"
|
||
local raw_http_response
|
||
raw_http_response="$(request_with_status "GET" "${mainland_base_url}/api/v1/ops/nodes")"
|
||
local http_status="${raw_http_response##*$'\n'HTTP_STATUS=}"
|
||
local response_json="${raw_http_response%$'\n'HTTP_STATUS=*}"
|
||
echo "[1/2] raw response"
|
||
printf '%s\n' "http_status=${http_status}"
|
||
print_json_or_raw "${response_json}"
|
||
echo
|
||
echo "[2/2] condensed summary"
|
||
python3 - <<'PY' "${response_json}" "${http_status}"
|
||
import json
|
||
import sys
|
||
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
http_status = int(sys.argv[2] or 0)
|
||
data = payload.get("data") or {}
|
||
summary = data.get("summary") or {}
|
||
nodes = [dict(item or {}) for item in list(data.get("nodes") or []) if isinstance(item, dict)]
|
||
|
||
rows = []
|
||
for node in nodes:
|
||
rows.append({
|
||
"node_code": str(node.get("node_code") or "").strip(),
|
||
"region": str(node.get("region") or "").strip(),
|
||
"role": str(node.get("role") or "").strip(),
|
||
"agent_state": str(node.get("agent_state") or "").strip(),
|
||
"remote_access_state": str(node.get("remote_access_state") or "").strip(),
|
||
"ssh_access_label": str(node.get("ssh_access_label") or "").strip(),
|
||
"ssh_entry": (
|
||
f"{str(node.get('ssh_user') or '').strip()}@{str(node.get('ssh_host') or '').strip()}:{int(node.get('ssh_port') or 22)}"
|
||
if str(node.get("ssh_host") or "").strip() and str(node.get("ssh_user") or "").strip()
|
||
else ""
|
||
),
|
||
"participation_state": str(node.get("participation_state") or "").strip(),
|
||
"cluster_status": str(node.get("cluster_status") or "").strip(),
|
||
"current_load": int(node.get("cluster_current_load", 0) or 0),
|
||
})
|
||
|
||
print(json.dumps({
|
||
"http_status": http_status,
|
||
"summary": {
|
||
"total": int(summary.get("total", len(nodes)) or 0),
|
||
"managed_enabled": int(summary.get("managed_enabled", 0) or 0),
|
||
"agent_ready": int(summary.get("agent_ready", 0) or 0),
|
||
"ssh_ready": int(summary.get("ssh_ready", 0) or 0),
|
||
"remote_access_ready": int(summary.get("remote_access_ready", 0) or 0),
|
||
"participating": int(summary.get("participating", 0) or 0),
|
||
"queue_dead_letter_nodes": int(summary.get("queue_dead_letter_nodes", 0) or 0),
|
||
"remote_access_state_counts": dict(summary.get("remote_access_state_counts") or {}),
|
||
},
|
||
"nodes": rows,
|
||
}, ensure_ascii=False, indent=2))
|
||
PY
|
||
}
|
||
|
||
bind_node_ssh_profile() {
|
||
local mainland_base_url="$1"
|
||
local node_code="$2"
|
||
local ssh_host="$3"
|
||
local ssh_user="$4"
|
||
local ssh_port="${5:-22}"
|
||
local title="${6:-}"
|
||
if [[ -z "${node_code}" || -z "${ssh_host}" || -z "${ssh_user}" ]]; then
|
||
echo "usage: node-bind-ssh [mainland_base_url] <node_code> <ssh_host> <ssh_user> [ssh_port] [title]" >&2
|
||
exit 1
|
||
fi
|
||
|
||
local request_json
|
||
request_json="$(python3 - <<'PY' "${node_code}" "${ssh_host}" "${ssh_user}" "${ssh_port}" "${title}"
|
||
import json
|
||
import sys
|
||
|
||
node_code = str(sys.argv[1] or "").strip()
|
||
ssh_host = str(sys.argv[2] or "").strip()
|
||
ssh_user = str(sys.argv[3] or "").strip()
|
||
ssh_port = int(sys.argv[4] or 22)
|
||
title = str(sys.argv[5] or "").strip()
|
||
|
||
payload = {
|
||
"node_code": node_code,
|
||
"ssh_host": ssh_host,
|
||
"ssh_user": ssh_user,
|
||
"ssh_port": ssh_port,
|
||
"auth_mode": "key",
|
||
"metadata": {
|
||
"ssh_bound_via": "drive_ops_center.sh/node-bind-ssh",
|
||
},
|
||
}
|
||
if title:
|
||
payload["title"] = title
|
||
|
||
print(json.dumps(payload, ensure_ascii=False))
|
||
PY
|
||
)"
|
||
|
||
local raw_http_response
|
||
raw_http_response="$(request_with_status "POST" "${mainland_base_url}/api/v1/ops/nodes" "${request_json}")"
|
||
local http_status="${raw_http_response##*$'\n'HTTP_STATUS=}"
|
||
local response_json="${raw_http_response%$'\n'HTTP_STATUS=*}"
|
||
|
||
echo "[1/3] save managed node ssh profile"
|
||
printf '%s\n' "http_status=${http_status}"
|
||
print_json_or_raw "${response_json}"
|
||
echo
|
||
|
||
echo "[2/3] node handover after binding"
|
||
show_node_handover "${mainland_base_url}" "${node_code}"
|
||
echo
|
||
|
||
echo "[3/3] condensed ssh binding"
|
||
python3 - <<'PY' "${response_json}" "${http_status}" "${node_code}" "${ssh_host}" "${ssh_user}" "${ssh_port}" "${mainland_base_url}"
|
||
import json
|
||
import sys
|
||
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
http_status = int(sys.argv[2] or 0)
|
||
node_code = str(sys.argv[3] or "").strip()
|
||
ssh_host = str(sys.argv[4] or "").strip()
|
||
ssh_user = str(sys.argv[5] or "").strip()
|
||
ssh_port = int(sys.argv[6] or 22)
|
||
base_url = str(sys.argv[7] or "http://127.0.0.1:8100").strip() or "http://127.0.0.1:8100"
|
||
data = payload.get("data") or {}
|
||
node = dict(data.get("node") or {})
|
||
|
||
print(json.dumps({
|
||
"http_status": http_status,
|
||
"message": str(payload.get("message") or ""),
|
||
"node_code": node_code,
|
||
"ssh_entry": f"{ssh_user}@{ssh_host}:{ssh_port}",
|
||
"saved_title": str(node.get("title") or "").strip(),
|
||
"saved_region": str(node.get("region") or "").strip(),
|
||
"saved_role": str(node.get("role") or "").strip(),
|
||
"next_recommended_commands": {
|
||
"nodes": f"bash domain-api/deploy/multi-region/drive_ops_center.sh nodes {base_url}",
|
||
"handover": f"bash domain-api/deploy/multi-region/drive_ops_center.sh node-handover {base_url} {node_code}",
|
||
"bootstrap_plan": f"bash domain-api/deploy/multi-region/drive_ops_center.sh node-bootstrap-plan {base_url} {node_code}",
|
||
},
|
||
}, ensure_ascii=False, indent=2))
|
||
PY
|
||
}
|
||
|
||
show_node_scene_log() {
|
||
local mainland_base_url="$1"
|
||
local node_code="$2"
|
||
local limit="${3:-80}"
|
||
local mode="${4:-}"
|
||
if [[ -z "${node_code}" ]]; then
|
||
echo "node_code is required" >&2
|
||
exit 1
|
||
fi
|
||
|
||
local request_url
|
||
request_url="$(python3 - <<'PY' "${mainland_base_url}" "${node_code}" "${limit}" "${mode}"
|
||
import sys
|
||
from urllib.parse import quote, urlencode
|
||
|
||
base_url = str(sys.argv[1] or "").rstrip("/")
|
||
node_code = str(sys.argv[2] or "").strip()
|
||
limit = max(1, min(int(sys.argv[3] or 80), 400))
|
||
mode = str(sys.argv[4] or "").strip().lower()
|
||
params = {"limit": limit}
|
||
if mode in {"key", "full"}:
|
||
params["mode"] = mode
|
||
endpoint = f"{base_url}/api/v1/ops/nodes/{quote(node_code, safe='')}/scene-log"
|
||
query_string = urlencode(params)
|
||
print(f"{endpoint}?{query_string}" if query_string else endpoint)
|
||
PY
|
||
)"
|
||
|
||
local raw_http_response
|
||
raw_http_response="$(request_with_status "GET" "${request_url}")"
|
||
local http_status="${raw_http_response##*$'\n'HTTP_STATUS=}"
|
||
local response_json="${raw_http_response%$'\n'HTTP_STATUS=*}"
|
||
local effective_http_status="${http_status}"
|
||
local fallback_json=""
|
||
|
||
if [[ "${http_status}" == "404" ]]; then
|
||
local runtime_http_response runtime_http_status runtime_response_json
|
||
runtime_http_response="$(request_with_status "GET" "${mainland_base_url}/api/v1/runtime/status")"
|
||
runtime_http_status="${runtime_http_response##*$'\n'HTTP_STATUS=}"
|
||
runtime_response_json="${runtime_http_response%$'\n'HTTP_STATUS=*}"
|
||
if [[ "${runtime_http_status}" -ge 200 && "${runtime_http_status}" -lt 300 ]]; then
|
||
fallback_json="$(python3 - <<'PY' "${runtime_response_json}" "${node_code}" "${limit}" "${mode}" "${http_status}"
|
||
import json
|
||
import re
|
||
import sys
|
||
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
node_code = str(sys.argv[2] or "").strip()
|
||
limit = max(1, min(int(sys.argv[3] or 80), 400))
|
||
requested_mode = str(sys.argv[4] or "").strip().lower()
|
||
backend_http_status = int(sys.argv[5] or 404)
|
||
|
||
data = dict(payload.get("data") or {})
|
||
detect = dict(data.get("detect") or {})
|
||
cluster = dict(data.get("cluster") or {})
|
||
log_sync = dict(detect.get("log_sync") or {})
|
||
|
||
mode = "full" if requested_mode == "full" else ("full" if str(log_sync.get("mode") or "").strip().lower() == "full" else "key")
|
||
|
||
cluster_nodes = [dict(item or {}) for item in list(cluster.get("nodes") or []) if isinstance(item, dict)]
|
||
cluster_map = {str(item.get("node_code") or "").strip(): item for item in cluster_nodes if str(item.get("node_code") or "").strip()}
|
||
|
||
participating_nodes = [dict(item or {}) for item in list(detect.get("participating_nodes") or []) if isinstance(item, dict)]
|
||
standby_nodes = [dict(item or {}) for item in list(detect.get("non_participating_nodes") or []) if isinstance(item, dict)]
|
||
participating_map = {str(item.get("node_code") or "").strip(): item for item in participating_nodes if str(item.get("node_code") or "").strip()}
|
||
standby_map = {str(item.get("node_code") or "").strip(): item for item in standby_nodes if str(item.get("node_code") or "").strip()}
|
||
|
||
preview_lines = [str(item or "").strip() for item in list(log_sync.get("preview_lines") or []) if str(item or "").strip()]
|
||
pattern = re.compile(r"^\[(?P<created_at>[^\]]+)\]\s+\[(?P<node_code>[^\]]+)\]\s+(?P<message>.+)$")
|
||
node_preview_lines = []
|
||
for line in preview_lines:
|
||
match = pattern.match(line)
|
||
if not match:
|
||
continue
|
||
if str(match.group("node_code") or "").strip() != node_code:
|
||
continue
|
||
node_preview_lines.append({
|
||
"created_at": str(match.group("created_at") or "").strip(),
|
||
"node_code": node_code,
|
||
"message": str(match.group("message") or "").strip(),
|
||
"line": line,
|
||
"mode": "key",
|
||
})
|
||
node_preview_lines = node_preview_lines[-limit:]
|
||
|
||
source_summary_map = {
|
||
str(item.get("node_code") or "").strip(): dict(item or {})
|
||
for item in list(log_sync.get("source_node_summaries") or [])
|
||
if isinstance(item, dict) and str(item.get("node_code") or "").strip()
|
||
}
|
||
missing_summary_map = {
|
||
str(item.get("node_code") or "").strip(): dict(item or {})
|
||
for item in list(log_sync.get("missing_participating_node_summaries") or [])
|
||
if isinstance(item, dict) and str(item.get("node_code") or "").strip()
|
||
}
|
||
covered_summary_map = {
|
||
str(item.get("node_code") or "").strip(): dict(item or {})
|
||
for item in list(log_sync.get("covered_participating_node_summaries") or [])
|
||
if isinstance(item, dict) and str(item.get("node_code") or "").strip()
|
||
}
|
||
|
||
node_meta = dict(participating_map.get(node_code) or standby_map.get(node_code) or {})
|
||
cluster_meta = dict(cluster_map.get(node_code) or {})
|
||
source_summary = dict(source_summary_map.get(node_code) or covered_summary_map.get(node_code) or {})
|
||
missing_summary = dict(missing_summary_map.get(node_code) or {})
|
||
|
||
derived_line_count = len(node_preview_lines)
|
||
derived_last_at = str((node_preview_lines[-1] if node_preview_lines else {}).get("created_at") or "").strip()
|
||
derived_last_line = str((node_preview_lines[-1] if node_preview_lines else {}).get("line") or "").strip()
|
||
|
||
source_summary = {
|
||
"node_code": node_code,
|
||
"line_count": max(int(source_summary.get("line_count", 0) or 0), derived_line_count),
|
||
"key_line_count": max(int(source_summary.get("key_line_count", 0) or 0), derived_line_count),
|
||
"full_line_count": int(source_summary.get("full_line_count", 0) or 0),
|
||
"last_at": derived_last_at or str(source_summary.get("last_at") or "").strip(),
|
||
"last_line": derived_last_line or str(source_summary.get("last_line") or "").strip(),
|
||
}
|
||
|
||
log_sync_enabled = bool(log_sync.get("enabled", False))
|
||
detect_participating = node_code in participating_map
|
||
standby_visible = node_code in standby_map
|
||
record_available = bool(source_summary.get("line_count", 0) or node_preview_lines)
|
||
|
||
if record_available:
|
||
if detect_participating:
|
||
status = "full_capture" if mode == "full" else "healthy"
|
||
else:
|
||
status = "historical_sample"
|
||
elif missing_summary:
|
||
missing_reason_code = str(missing_summary.get("missing_reason_code") or "").strip()
|
||
if missing_reason_code == "disabled":
|
||
status = "disabled"
|
||
elif missing_reason_code == "waiting_sample":
|
||
status = "waiting_sample"
|
||
else:
|
||
status = "missing_sample"
|
||
elif detect_participating:
|
||
status = "waiting_sample" if log_sync_enabled else "disabled"
|
||
elif standby_visible:
|
||
status = "standby"
|
||
else:
|
||
status = "unknown"
|
||
|
||
status_labels = {
|
||
"healthy": ("关键覆盖", "success"),
|
||
"full_capture": ("全量观察", "success"),
|
||
"historical_sample": ("历史样本", "success"),
|
||
"waiting_sample": ("等待样本", "warning"),
|
||
"missing_sample": ("缺少样本", "warning"),
|
||
"disabled": ("已关闭", "info"),
|
||
"standby": ("在线待命", "info"),
|
||
"unknown": ("未发现", "info"),
|
||
}
|
||
status_label, status_type = status_labels.get(status, ("待确认", "info"))
|
||
|
||
if record_available and detect_participating:
|
||
summary = f"节点当前正在参与检测,已通过兼容降级视图识别到 {int(source_summary.get('line_count', 0) or 0)} 条现场日志样本。"
|
||
elif record_available:
|
||
summary = f"节点已通过兼容降级视图识别到 {int(source_summary.get('line_count', 0) or 0)} 条现场日志样本。"
|
||
elif missing_summary:
|
||
summary = str(missing_summary.get("missing_reason") or "").strip() or "当前还没有收到该节点的现场日志样本。"
|
||
elif detect_participating and log_sync_enabled:
|
||
summary = "节点正在参与检测,但当前现场日志样本仍未形成。"
|
||
elif detect_participating:
|
||
summary = "节点正在参与检测,但远端日志回传当前关闭。"
|
||
elif standby_visible:
|
||
summary = "节点当前在线但未参与本轮检测,暂时没有新的现场日志样本。"
|
||
else:
|
||
summary = "当前没有找到该节点的现场日志记录。"
|
||
|
||
latest_record = dict(node_preview_lines[-1] or {}) if node_preview_lines else {}
|
||
missing_reason_code = str(missing_summary.get("missing_reason_code") or "").strip()
|
||
missing_reason = str(missing_summary.get("missing_reason") or "").strip()
|
||
|
||
result = {
|
||
"http_status": 200,
|
||
"backend_http_status": backend_http_status,
|
||
"fallback_used": True,
|
||
"fallback_source": "runtime_status",
|
||
"fallback_reason": "running_api_missing_scene_log_route",
|
||
"compatibility_warning": "当前运行中的 API 尚未暴露 /api/v1/ops/nodes/{node_code}/scene-log,已降级为 runtime/status 兼容视图。",
|
||
"node_code": node_code,
|
||
"status": status,
|
||
"status_label": status_label,
|
||
"status_type": status_type,
|
||
"mode": mode,
|
||
"mode_label": "全量回传" if mode == "full" else "关键回传",
|
||
"summary": summary,
|
||
"node": {
|
||
"region": str(node_meta.get("region") or cluster_meta.get("region") or missing_summary.get("region") or "").strip(),
|
||
"role": str(node_meta.get("role") or cluster_meta.get("role") or missing_summary.get("role") or "").strip(),
|
||
"status": str(node_meta.get("status") or cluster_meta.get("status") or "").strip(),
|
||
"current_load": int(node_meta.get("current_load", cluster_meta.get("current_load", 0)) or 0),
|
||
"last_heartbeat_at": str(node_meta.get("last_heartbeat_at") or cluster_meta.get("last_heartbeat_at") or "").strip(),
|
||
},
|
||
"participation": {
|
||
"detect_participating": detect_participating,
|
||
"participation_label": str(node_meta.get("participation_label") or "").strip(),
|
||
"participation_bucket": str(node_meta.get("participation_bucket") or "").strip(),
|
||
"participation_reason": str(node_meta.get("participation_reason") or "").strip(),
|
||
},
|
||
"source_summary": {
|
||
"line_count": int(source_summary.get("line_count", 0) or 0),
|
||
"key_line_count": int(source_summary.get("key_line_count", 0) or 0),
|
||
"full_line_count": int(source_summary.get("full_line_count", 0) or 0),
|
||
"last_at": str(source_summary.get("last_at") or "").strip(),
|
||
"last_line": str(source_summary.get("last_line") or "").strip(),
|
||
},
|
||
"missing_reason_code": missing_reason_code,
|
||
"missing_reason": missing_reason,
|
||
"records_total": int(source_summary.get("line_count", 0) or 0),
|
||
"records_visible": len(node_preview_lines),
|
||
"records_truncated": False,
|
||
"latest_record": latest_record,
|
||
"preview_lines": [str(item.get("line") or "").strip() for item in node_preview_lines[-8:]],
|
||
}
|
||
print(json.dumps(result, ensure_ascii=False))
|
||
PY
|
||
)"
|
||
if [[ -n "${fallback_json}" ]]; then
|
||
effective_http_status="200"
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
echo "[1/2] raw response"
|
||
printf '%s\n' "http_status=${http_status}"
|
||
printf '%s\n' "request_url=${request_url}"
|
||
print_json_or_raw "${response_json}"
|
||
if [[ -n "${fallback_json}" ]]; then
|
||
echo
|
||
printf '%s\n' "fallback=runtime_status"
|
||
print_json_or_raw "${fallback_json}"
|
||
fi
|
||
echo
|
||
echo "[2/2] condensed summary"
|
||
python3 - <<'PY' "${response_json}" "${effective_http_status}" "${fallback_json}"
|
||
import json
|
||
import pathlib
|
||
import sys
|
||
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
http_status = int(sys.argv[2] or 0)
|
||
fallback_json = str(sys.argv[3] or "").strip()
|
||
|
||
if fallback_json:
|
||
data = json.loads(fallback_json)
|
||
node = data.get("node") or {}
|
||
participation = data.get("participation") or {}
|
||
source_summary = data.get("source_summary") or {}
|
||
latest_record = data.get("latest_record") or {}
|
||
preview_lines = list(data.get("preview_lines") or [])
|
||
else:
|
||
data = payload.get("data") or {}
|
||
node = data.get("node") or {}
|
||
participation = data.get("participation") or {}
|
||
source_summary = data.get("source_summary") or {}
|
||
records = [dict(item or {}) for item in list(data.get("records") or []) if dict(item or {})]
|
||
latest_record = data.get("latest_record") or {}
|
||
preview_lines = [str(item.get("line") or "") for item in records[-8:]]
|
||
|
||
print(json.dumps({
|
||
"http_status": http_status,
|
||
"backend_http_status": int(data.get("backend_http_status", 0) or 0) if fallback_json else http_status,
|
||
"fallback_used": bool(data.get("fallback_used", False)),
|
||
"fallback_source": str(data.get("fallback_source") or ""),
|
||
"compatibility_warning": str(data.get("compatibility_warning") or ""),
|
||
"node_code": str(data.get("node_code") or ""),
|
||
"status": str(data.get("status") or ""),
|
||
"status_label": str(data.get("status_label") or ""),
|
||
"mode": str(data.get("mode") or ""),
|
||
"mode_label": str(data.get("mode_label") or ""),
|
||
"summary": str(data.get("summary") or ""),
|
||
"node": {
|
||
"region": str(node.get("region") or ""),
|
||
"role": str(node.get("role") or ""),
|
||
"status": str(node.get("status") or ""),
|
||
"current_load": int(node.get("current_load", 0) or 0),
|
||
"last_heartbeat_at": str(node.get("last_heartbeat_at") or ""),
|
||
},
|
||
"participation": {
|
||
"detect_participating": bool(participation.get("detect_participating", False)),
|
||
"participation_label": str(participation.get("participation_label") or ""),
|
||
"participation_bucket": str(participation.get("participation_bucket") or ""),
|
||
"participation_reason": str(participation.get("participation_reason") or ""),
|
||
},
|
||
"source_summary": {
|
||
"line_count": int(source_summary.get("line_count", 0) or 0),
|
||
"key_line_count": int(source_summary.get("key_line_count", 0) or 0),
|
||
"full_line_count": int(source_summary.get("full_line_count", 0) or 0),
|
||
"last_at": str(source_summary.get("last_at") or ""),
|
||
"last_line": str(source_summary.get("last_line") or ""),
|
||
},
|
||
"missing_reason_code": str(data.get("missing_reason_code") or ""),
|
||
"missing_reason": str(data.get("missing_reason") or ""),
|
||
"records_total": int(data.get("records_total", 0) or 0),
|
||
"records_visible": int(data.get("records_visible", 0) or 0),
|
||
"records_truncated": bool(data.get("records_truncated", False)),
|
||
"latest_record": latest_record,
|
||
"preview_lines": [str(item or "") for item in preview_lines],
|
||
}, ensure_ascii=False, indent=2))
|
||
PY
|
||
}
|
||
|
||
build_node_handover_bootstrap_plan() {
|
||
local mainland_base_url="$1"
|
||
local node_code="$2"
|
||
local issued_by="$3"
|
||
local expires_in_hours="$4"
|
||
local control_plane_base_url="$5"
|
||
local root_dir="$6"
|
||
if [[ -z "${node_code}" ]]; then
|
||
echo "node_code is required" >&2
|
||
exit 1
|
||
fi
|
||
local request_json
|
||
request_json="$(python3 - <<'PY' "${issued_by}" "${expires_in_hours}" "${control_plane_base_url}" "${root_dir}"
|
||
import json
|
||
import sys
|
||
|
||
issued_by = str(sys.argv[1] or "cli/ops-center/node-bootstrap-plan").strip() or "cli/ops-center/node-bootstrap-plan"
|
||
expires_in_hours = max(1, min(int(sys.argv[2] or 72), 24 * 30))
|
||
control_plane_base_url = str(sys.argv[3] or "").strip()
|
||
root_dir = str(sys.argv[4] or "/opt/domaincheck").strip() or "/opt/domaincheck"
|
||
print(json.dumps({
|
||
"issued_by": issued_by,
|
||
"expires_in_hours": expires_in_hours,
|
||
"control_plane_base_url": control_plane_base_url,
|
||
"root_dir": root_dir,
|
||
"metadata": {
|
||
"issued_from": "cli-node-handover-bootstrap-plan",
|
||
},
|
||
}, ensure_ascii=False))
|
||
PY
|
||
)"
|
||
local raw_http_response
|
||
raw_http_response="$(request_with_status "POST" "${mainland_base_url}/api/v1/ops/nodes/${node_code}/handover/bootstrap-plan" "${request_json}")"
|
||
local http_status="${raw_http_response##*$'\n'HTTP_STATUS=}"
|
||
local response_json="${raw_http_response%$'\n'HTTP_STATUS=*}"
|
||
local effective_http_status="${http_status}"
|
||
local fallback_json=""
|
||
|
||
if [[ "${http_status}" == "404" ]]; then
|
||
local generic_request_json generic_http_response generic_http_status generic_response_json
|
||
generic_request_json="$(python3 - <<'PY' "${node_code}" "${issued_by}" "${expires_in_hours}" "${control_plane_base_url}" "${root_dir}"
|
||
import json
|
||
import sys
|
||
|
||
node_code = str(sys.argv[1] or "").strip()
|
||
issued_by = str(sys.argv[2] or "cli/ops-center/node-bootstrap-plan").strip() or "cli/ops-center/node-bootstrap-plan"
|
||
expires_in_hours = max(1, min(int(sys.argv[3] or 72), 24 * 30))
|
||
control_plane_base_url = str(sys.argv[4] or "").strip()
|
||
root_dir = str(sys.argv[5] or "/opt/domaincheck").strip() or "/opt/domaincheck"
|
||
|
||
normalized = node_code.lower()
|
||
node_region = "overseas" if normalized.startswith("overseas-") else "mainland"
|
||
node_role = "control" if ("controller" in normalized or "-control" in normalized) else "worker"
|
||
|
||
print(json.dumps({
|
||
"node_code": node_code,
|
||
"node_region": node_region,
|
||
"node_role": node_role,
|
||
"issued_by": issued_by,
|
||
"expires_in_hours": expires_in_hours,
|
||
"control_plane_base_url": control_plane_base_url,
|
||
"root_dir": root_dir,
|
||
"metadata": {
|
||
"issued_from": "cli-node-handover-bootstrap-plan-fallback",
|
||
"fallback_used": True,
|
||
},
|
||
}, ensure_ascii=False))
|
||
PY
|
||
)"
|
||
generic_http_response="$(request_with_status "POST" "${mainland_base_url}/api/v1/ops/agent/bootstrap-plan" "${generic_request_json}")"
|
||
generic_http_status="${generic_http_response##*$'\n'HTTP_STATUS=}"
|
||
generic_response_json="${generic_http_response%$'\n'HTTP_STATUS=*}"
|
||
if [[ "${generic_http_status}" -ge 200 && "${generic_http_status}" -lt 300 ]]; then
|
||
fallback_json="$(python3 - <<'PY' "${generic_response_json}" "${http_status}" "${node_code}"
|
||
import json
|
||
import sys
|
||
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
backend_http_status = int(sys.argv[2] or 404)
|
||
node_code = str(sys.argv[3] or "").strip()
|
||
data = payload.get("data") or {}
|
||
bootstrap_plan = dict(data.get("bootstrap_plan") or {})
|
||
|
||
print(json.dumps({
|
||
"http_status": 200,
|
||
"backend_http_status": backend_http_status,
|
||
"fallback_used": True,
|
||
"fallback_source": "ops_agent_bootstrap_plan",
|
||
"compatibility_warning": "当前运行中的 API 尚未暴露节点级 /handover/bootstrap-plan,已降级为通用 /ops/agent/bootstrap-plan。",
|
||
"message": str(payload.get("message") or ""),
|
||
"node_code": node_code,
|
||
"token_preview": str(data.get("token_preview") or ""),
|
||
"expires_at": str(data.get("expires_at") or ""),
|
||
"control_plane_base_url": str(data.get("control_plane_base_url") or ""),
|
||
"bootstrap_plan": bootstrap_plan,
|
||
"handover_stage": {
|
||
"code": "bootstrap_plan_issued",
|
||
"label": "已生成接入方案",
|
||
"summary": "当前已通过通用 bootstrap-plan 接口生成接入方案,可直接去目标节点执行。",
|
||
},
|
||
}, ensure_ascii=False))
|
||
PY
|
||
)"
|
||
effective_http_status="200"
|
||
fi
|
||
fi
|
||
echo "[1/2] raw response"
|
||
printf '%s\n' "http_status=${http_status}"
|
||
print_json_or_raw "${response_json}"
|
||
if [[ -n "${fallback_json}" ]]; then
|
||
echo
|
||
printf '%s\n' "fallback=ops_agent_bootstrap_plan"
|
||
print_json_or_raw "${fallback_json}"
|
||
fi
|
||
echo
|
||
echo "[2/2] condensed summary"
|
||
python3 - <<'PY' "${response_json}" "${effective_http_status}" "${fallback_json}"
|
||
import json
|
||
import pathlib
|
||
import sys
|
||
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
http_status = int(sys.argv[2] or 0)
|
||
fallback_json = str(sys.argv[3] or "").strip()
|
||
if fallback_json:
|
||
data = json.loads(fallback_json)
|
||
plan = data.get("bootstrap_plan") or {}
|
||
stage = data.get("handover_stage") or {}
|
||
node_code = str(data.get("node_code") or "").strip()
|
||
else:
|
||
data = payload.get("data") or {}
|
||
plan = data.get("bootstrap_plan") or {}
|
||
handover = data.get("handover") or {}
|
||
stage = handover.get("stage") or {}
|
||
node_code = str(data.get("node_code") or handover.get("node_code") or "").strip()
|
||
workspace_root = pathlib.Path.cwd()
|
||
ops_agent_service_path = workspace_root / "domain-api" / "app" / "services" / "ops_agent_service.py"
|
||
bootstrap_script_path = workspace_root / "domain-api" / "deploy" / "multi-region" / "bootstrap_node_agent.sh"
|
||
ops_agent_service_text = ""
|
||
bootstrap_script_text = ""
|
||
try:
|
||
ops_agent_service_text = ops_agent_service_path.read_text(encoding="utf-8", errors="replace")
|
||
except Exception:
|
||
ops_agent_service_text = ""
|
||
try:
|
||
bootstrap_script_text = bootstrap_script_path.read_text(encoding="utf-8", errors="replace")
|
||
except Exception:
|
||
bootstrap_script_text = ""
|
||
|
||
repo_supports_install_command_block = (
|
||
"install_command_block" in ops_agent_service_text
|
||
and "_candidate_node_agent_install_paths" in ops_agent_service_text
|
||
)
|
||
repo_supports_multi_layout_bootstrap = "resolve_project_root" in bootstrap_script_text
|
||
install_command_block_present = bool(str(plan.get("install_command_block") or "").strip())
|
||
runtime_may_need_restart = bool(repo_supports_install_command_block and not install_command_block_present)
|
||
runtime_restart_hint = (
|
||
"仓库代码已支持 install_command_block,但本次接口响应里仍未返回,当前运行中的控制面 API 可能还没重启到最新代码。"
|
||
if runtime_may_need_restart
|
||
else ""
|
||
)
|
||
print(json.dumps({
|
||
"http_status": http_status,
|
||
"backend_http_status": int(data.get("backend_http_status", 0) or 0) if fallback_json else http_status,
|
||
"fallback_used": bool(data.get("fallback_used", False)),
|
||
"fallback_source": str(data.get("fallback_source") or ""),
|
||
"compatibility_warning": str(data.get("compatibility_warning") or ""),
|
||
"runtime_restart_hint": runtime_restart_hint,
|
||
"message": str(payload.get("message") or ""),
|
||
"node_code": node_code,
|
||
"token_preview": str(data.get("token_preview") or ""),
|
||
"expires_at": str(data.get("expires_at") or ""),
|
||
"control_plane_base_url": str(data.get("control_plane_base_url") or ""),
|
||
"bootstrap_plan": {
|
||
"env_file": str(plan.get("env_file") or ""),
|
||
"service_name": str(plan.get("service_name") or ""),
|
||
"bootstrap_script_name": str(plan.get("bootstrap_script_name") or ""),
|
||
"bootstrap_script_path": str(plan.get("bootstrap_script_path") or ""),
|
||
"install_command_block_present": install_command_block_present,
|
||
"bootstrap_run_script_block_present": bool(str(plan.get("bootstrap_run_script_block") or "").strip()),
|
||
},
|
||
"repo_capability": {
|
||
"supports_install_command_block": repo_supports_install_command_block,
|
||
"supports_multi_layout_bootstrap": repo_supports_multi_layout_bootstrap,
|
||
"runtime_may_need_restart": runtime_may_need_restart,
|
||
},
|
||
"handover_stage": {
|
||
"code": str(stage.get("code") or ""),
|
||
"label": str(stage.get("label") or ""),
|
||
"summary": str(stage.get("summary") or ""),
|
||
},
|
||
"recommended_commands": {
|
||
"node_handover": (
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh node-handover http://127.0.0.1:8100 {node_code}"
|
||
if node_code
|
||
else ""
|
||
),
|
||
"delivery_queue": (
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh delivery-queue http://127.0.0.1:8100 {node_code}"
|
||
if node_code
|
||
else ""
|
||
),
|
||
"restart_api_if_runtime_stale": (
|
||
"bash domain-api/deploy/multi-region/drive_ops_center.sh runtime-refresh-recover http://127.0.0.1:8100"
|
||
if runtime_may_need_restart
|
||
else ""
|
||
),
|
||
},
|
||
}, ensure_ascii=False, indent=2))
|
||
PY
|
||
}
|
||
|
||
show_stack_first_gap_handover() {
|
||
local mainland_base_url="$1"
|
||
local raw_http_response
|
||
raw_http_response="$(request_with_status "GET" "${mainland_base_url}/api/v1/ops/stack-diagnosis?base_url=${mainland_base_url}")"
|
||
local http_status="${raw_http_response##*$'\n'HTTP_STATUS=}"
|
||
local response_json="${raw_http_response%$'\n'HTTP_STATUS=*}"
|
||
local first_gap_node_code
|
||
first_gap_node_code="$(python3 - <<'PY' "${response_json}"
|
||
import json
|
||
import sys
|
||
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
data = payload.get("data") or {}
|
||
managed_nodes = data.get("managed_nodes") or {}
|
||
first_gap = managed_nodes.get("first_handover_gap") or {}
|
||
problem_nodes = [dict(item or {}) for item in list(managed_nodes.get("problem_nodes") or []) if isinstance(item, dict)]
|
||
node_code = str(
|
||
first_gap.get("node_code")
|
||
or managed_nodes.get("first_handover_gap_node_code")
|
||
or ((problem_nodes[0] or {}).get("node_code") if problem_nodes else "")
|
||
or ""
|
||
).strip()
|
||
print(node_code)
|
||
PY
|
||
)"
|
||
if [[ -z "${first_gap_node_code}" ]]; then
|
||
python3 - <<'PY' "${http_status}" "${response_json}"
|
||
import json
|
||
import sys
|
||
|
||
http_status = int(sys.argv[1] or 0)
|
||
payload = json.loads(str(sys.argv[2] or "{}"))
|
||
data = payload.get("data") or {}
|
||
managed_nodes = data.get("managed_nodes") or {}
|
||
print(json.dumps({
|
||
"http_status": http_status,
|
||
"message": "当前 stack diagnosis 没有首个 handover gap 节点。",
|
||
"managed_nodes": {
|
||
"managed_enabled": int(managed_nodes.get("managed_enabled", 0) or 0),
|
||
"remote_access_ready": int(managed_nodes.get("remote_access_ready", 0) or 0),
|
||
"problem_nodes": list(managed_nodes.get("problem_nodes") or [])[:5],
|
||
},
|
||
}, ensure_ascii=False, indent=2))
|
||
PY
|
||
return 0
|
||
fi
|
||
show_node_handover "${mainland_base_url}" "${first_gap_node_code}"
|
||
}
|
||
|
||
show_node_onboarding() {
|
||
local mainland_base_url="$1"
|
||
local node_code="$2"
|
||
if [[ -z "${node_code}" ]]; then
|
||
echo "node_code is required" >&2
|
||
exit 1
|
||
fi
|
||
local raw_http_response
|
||
raw_http_response="$(request_with_status "GET" "${mainland_base_url}/api/v1/ops/nodes/${node_code}/onboarding")"
|
||
local http_status="${raw_http_response##*$'\n'HTTP_STATUS=}"
|
||
local response_json="${raw_http_response%$'\n'HTTP_STATUS=*}"
|
||
local effective_http_status="${http_status}"
|
||
local fallback_json=""
|
||
|
||
if [[ "${http_status}" == "404" ]]; then
|
||
local handover_http_response handover_http_status handover_response_json
|
||
handover_http_response="$(request_with_status "GET" "${mainland_base_url}/api/v1/ops/nodes/${node_code}/handover")"
|
||
handover_http_status="${handover_http_response##*$'\n'HTTP_STATUS=}"
|
||
handover_response_json="${handover_http_response%$'\n'HTTP_STATUS=*}"
|
||
if [[ "${handover_http_status}" == "404" ]]; then
|
||
local stack_http_response stack_http_status stack_response_json
|
||
stack_http_response="$(request_with_status "GET" "${mainland_base_url}/api/v1/ops/stack-diagnosis?base_url=${mainland_base_url}")"
|
||
stack_http_status="${stack_http_response##*$'\n'HTTP_STATUS=}"
|
||
stack_response_json="${stack_http_response%$'\n'HTTP_STATUS=*}"
|
||
if [[ "${stack_http_status}" -ge 200 && "${stack_http_status}" -lt 300 ]]; then
|
||
handover_response_json="$(python3 - <<'PY' "${stack_response_json}" "${node_code}"
|
||
import json
|
||
import sys
|
||
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
target_node_code = str(sys.argv[2] or "").strip()
|
||
data = payload.get("data") or {}
|
||
managed_nodes = data.get("managed_nodes") or {}
|
||
problem_nodes = [dict(item or {}) for item in list(managed_nodes.get("problem_nodes") or []) if isinstance(item, dict)]
|
||
first_gap = dict(managed_nodes.get("first_handover_gap") or {})
|
||
|
||
node = next((item for item in problem_nodes if str(item.get("node_code") or "").strip() == target_node_code), {})
|
||
if not node and str(first_gap.get("node_code") or "").strip() == target_node_code:
|
||
node = dict(first_gap.get("node") or {})
|
||
if not node:
|
||
node = {"node_code": target_node_code}
|
||
|
||
agent_state = str(node.get("agent_state") or "").strip()
|
||
remote_access_state = str(node.get("remote_access_state") or "").strip()
|
||
stage_code = "unknown"
|
||
stage_label = "待判断"
|
||
stage_summary = "当前节点存在接管缺口,建议先生成 bootstrap 方案并检查 Node Agent 接入。"
|
||
if agent_state == "pending_bootstrap":
|
||
stage_code = "pending_bootstrap"
|
||
stage_label = "待执行接入"
|
||
stage_summary = "已签发有效 Token,但目标节点尚未执行 bootstrap 片段。"
|
||
elif agent_state == "runtime_only":
|
||
stage_code = "runtime_only"
|
||
stage_label = "仅运行态在线"
|
||
stage_summary = "控制面能看到 runtime 心跳,但 Node Agent 尚未接管。"
|
||
elif agent_state == "stale":
|
||
stage_code = "stale"
|
||
stage_label = "心跳过期"
|
||
stage_summary = "Node Agent 曾接入过,但心跳已过期,需先恢复服务或重新接入。"
|
||
elif remote_access_state == "agent_pending":
|
||
stage_code = "agent_pending"
|
||
stage_label = "待接入"
|
||
stage_summary = "当前 remote-agent 仍未就绪,先补齐 bootstrap 接入链路。"
|
||
|
||
print(json.dumps({
|
||
"data": {
|
||
"node_code": target_node_code,
|
||
"summary": stage_summary,
|
||
"stage": {
|
||
"code": stage_code,
|
||
"label": stage_label,
|
||
"summary": stage_summary,
|
||
},
|
||
"readiness": {
|
||
"is_managed": True,
|
||
"is_enabled": True,
|
||
"has_ssh_access": False,
|
||
"is_agent_online": agent_state in {"online", "online_busy"},
|
||
"is_remote_access_ready": False,
|
||
},
|
||
}
|
||
}, ensure_ascii=False))
|
||
PY
|
||
)"
|
||
handover_http_status="200"
|
||
fi
|
||
fi
|
||
if [[ "${handover_http_status}" -ge 200 && "${handover_http_status}" -lt 300 ]]; then
|
||
fallback_json="$(python3 - <<'PY' "${handover_response_json}" "${node_code}" "${http_status}"
|
||
import json
|
||
import sys
|
||
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
node_code = str(sys.argv[2] or "").strip()
|
||
backend_http_status = int(sys.argv[3] or 404)
|
||
data = payload.get("data") or {}
|
||
readiness = data.get("readiness") or {}
|
||
stage = data.get("stage") or {}
|
||
is_agent_online = bool(readiness.get("is_agent_online", False))
|
||
has_ssh_access = bool(readiness.get("has_ssh_access", False))
|
||
is_remote_access_ready = bool(readiness.get("is_remote_access_ready", False))
|
||
|
||
acceptance_execution_mode = ""
|
||
acceptance_status_code = "blocked"
|
||
acceptance_status_label = "暂不可验收"
|
||
acceptance_summary = "当前还没进入标准接管就绪状态,暂不建议直接跑 onboarding.acceptance。"
|
||
if is_agent_online:
|
||
acceptance_execution_mode = "remote-agent"
|
||
acceptance_status_code = "ready"
|
||
acceptance_status_label = "可远端验收"
|
||
acceptance_summary = "Node Agent 已在线,可以直接跑 onboarding.acceptance 做标准接管验收。"
|
||
elif has_ssh_access:
|
||
acceptance_execution_mode = "ssh"
|
||
acceptance_status_code = "ssh_only"
|
||
acceptance_status_label = "可走 SSH 验收"
|
||
acceptance_summary = "当前 Agent 还没在线,但 SSH 已备好,可先走 SSH 模式做接管验收。"
|
||
|
||
onboarding_stage_code = str(stage.get("code") or "").strip() or "unknown"
|
||
onboarding_stage_label = str(stage.get("label") or "").strip() or "待判断"
|
||
onboarding_summary = str(data.get("summary") or stage.get("summary") or "").strip()
|
||
if acceptance_status_code in {"ready", "ssh_only"} and onboarding_stage_code == "ready":
|
||
onboarding_stage_code = "acceptance_ready"
|
||
onboarding_stage_label = "待接管验收"
|
||
onboarding_summary = acceptance_summary
|
||
|
||
actions = []
|
||
if onboarding_stage_code in {"pending_bootstrap", "runtime_only", "profile_incomplete", "ssh_ready", "token_issue", "stale"}:
|
||
actions.append({
|
||
"key": "bootstrap_plan",
|
||
"label": "生成接入方案",
|
||
"summary": "先生成并执行 Node Agent bootstrap 方案,再观察 register / heartbeat 是否建立。",
|
||
})
|
||
if acceptance_status_code in {"ready", "ssh_only"}:
|
||
actions.append({
|
||
"key": "run_acceptance",
|
||
"label": "执行接管验收",
|
||
"summary": acceptance_summary,
|
||
})
|
||
|
||
recovery_decision = {
|
||
"action": "noop",
|
||
"label": "当前无需额外恢复动作",
|
||
"summary": onboarding_summary or "当前节点暂无需要执行的接管恢复动作。",
|
||
"command_hint": "",
|
||
"needs_confirmation": True,
|
||
"window": "none",
|
||
"stage_code": onboarding_stage_code,
|
||
"acceptance_status_code": acceptance_status_code,
|
||
}
|
||
if onboarding_stage_code == "acceptance_ready" or is_agent_online:
|
||
recovery_decision = {
|
||
"action": "run_acceptance",
|
||
"label": "执行接管验收",
|
||
"summary": acceptance_summary,
|
||
"command_hint": (
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh node-acceptance-run http://127.0.0.1:8100 {node_code} cli"
|
||
if node_code else ""
|
||
),
|
||
"needs_confirmation": True,
|
||
"window": "acceptance",
|
||
"stage_code": onboarding_stage_code,
|
||
"acceptance_status_code": acceptance_status_code,
|
||
}
|
||
elif onboarding_stage_code in {"pending_bootstrap", "runtime_only", "profile_incomplete", "ssh_ready", "token_issue", "stale", "agent_pending", "unknown", ""}:
|
||
recovery_decision = {
|
||
"action": "bootstrap_run",
|
||
"label": "签发接入工单",
|
||
"summary": "当前节点仍处于接入阶段,建议先签发 onboarding.bootstrap 并等待 register / heartbeat 建立。",
|
||
"command_hint": (
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh node-bootstrap-run http://127.0.0.1:8100 {node_code} cli"
|
||
if node_code else ""
|
||
),
|
||
"needs_confirmation": True,
|
||
"window": "bootstrap",
|
||
"stage_code": onboarding_stage_code,
|
||
"acceptance_status_code": acceptance_status_code,
|
||
}
|
||
|
||
print(json.dumps({
|
||
"http_status": 200,
|
||
"backend_http_status": backend_http_status,
|
||
"fallback_used": True,
|
||
"fallback_source": "node_handover",
|
||
"compatibility_warning": "当前运行中的 API 尚未暴露 /api/v1/ops/nodes/{node_code}/onboarding,已降级为 handover 兼容视图。",
|
||
"node_code": node_code,
|
||
"summary": onboarding_summary,
|
||
"onboarding_stage": {
|
||
"code": onboarding_stage_code,
|
||
"label": onboarding_stage_label,
|
||
"summary": onboarding_summary,
|
||
},
|
||
"handover": data,
|
||
"acceptance": {
|
||
"status_code": acceptance_status_code,
|
||
"status_label": acceptance_status_label,
|
||
"summary": acceptance_summary,
|
||
"execution_mode": acceptance_execution_mode,
|
||
"is_agent_online": is_agent_online,
|
||
"has_ssh_access": has_ssh_access,
|
||
"is_remote_access_ready": is_remote_access_ready,
|
||
"playbook": {
|
||
"playbook_key": "onboarding.acceptance",
|
||
"target_node_codes": [node_code] if node_code else [],
|
||
"execution_mode": acceptance_execution_mode or "remote-agent",
|
||
"auto_approve": True,
|
||
},
|
||
},
|
||
"recommended_actions": actions,
|
||
"recovery_decision": recovery_decision,
|
||
}, ensure_ascii=False))
|
||
PY
|
||
)"
|
||
effective_http_status="200"
|
||
fi
|
||
fi
|
||
|
||
echo "[1/2] raw response"
|
||
printf '%s\n' "http_status=${http_status}"
|
||
print_json_or_raw "${response_json}"
|
||
if [[ -n "${fallback_json}" ]]; then
|
||
echo
|
||
printf '%s\n' "fallback=node_handover"
|
||
print_json_or_raw "${fallback_json}"
|
||
fi
|
||
echo
|
||
echo "[2/2] condensed summary"
|
||
python3 - <<'PY' "${response_json}" "${effective_http_status}" "${fallback_json}"
|
||
import json
|
||
import sys
|
||
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
http_status = int(sys.argv[2] or 0)
|
||
fallback_json = str(sys.argv[3] or "").strip()
|
||
data = json.loads(fallback_json) if fallback_json else (payload.get("data") or {})
|
||
onboarding_stage = data.get("onboarding_stage") or {}
|
||
acceptance = data.get("acceptance") or {}
|
||
recovery_decision = data.get("recovery_decision") or {}
|
||
playbook = acceptance.get("playbook") or {}
|
||
node_code = str(data.get("node_code") or "").strip()
|
||
print(json.dumps({
|
||
"http_status": http_status,
|
||
"backend_http_status": int(data.get("backend_http_status", 0) or 0) if fallback_json else http_status,
|
||
"fallback_used": bool(data.get("fallback_used", False)),
|
||
"fallback_source": str(data.get("fallback_source") or ""),
|
||
"compatibility_warning": str(data.get("compatibility_warning") or ""),
|
||
"node_code": node_code,
|
||
"summary": str(data.get("summary") or ""),
|
||
"onboarding_stage": {
|
||
"code": str(onboarding_stage.get("code") or ""),
|
||
"label": str(onboarding_stage.get("label") or ""),
|
||
"summary": str(onboarding_stage.get("summary") or ""),
|
||
},
|
||
"acceptance": {
|
||
"status_code": str(acceptance.get("status_code") or ""),
|
||
"status_label": str(acceptance.get("status_label") or ""),
|
||
"summary": str(acceptance.get("summary") or ""),
|
||
"execution_mode": str(acceptance.get("execution_mode") or ""),
|
||
"is_agent_online": bool(acceptance.get("is_agent_online", False)),
|
||
"has_ssh_access": bool(acceptance.get("has_ssh_access", False)),
|
||
"is_remote_access_ready": bool(acceptance.get("is_remote_access_ready", False)),
|
||
"playbook": playbook,
|
||
},
|
||
"recommended_actions": list(data.get("recommended_actions") or []),
|
||
"recovery_decision": {
|
||
"action": str(recovery_decision.get("action") or ""),
|
||
"label": str(recovery_decision.get("label") or ""),
|
||
"summary": str(recovery_decision.get("summary") or ""),
|
||
"command_hint": str(recovery_decision.get("command_hint") or ""),
|
||
"needs_confirmation": bool(recovery_decision.get("needs_confirmation", False)),
|
||
"window": str(recovery_decision.get("window") or ""),
|
||
"stage_code": str(recovery_decision.get("stage_code") or ""),
|
||
"acceptance_status_code": str(recovery_decision.get("acceptance_status_code") or ""),
|
||
},
|
||
"recommended_commands": {
|
||
"node_handover": (
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh node-handover http://127.0.0.1:8100 {node_code}"
|
||
if node_code else ""
|
||
),
|
||
"node_bootstrap_preview": (
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh node-bootstrap-preview http://127.0.0.1:8100 {node_code}"
|
||
if node_code else ""
|
||
),
|
||
"node_bootstrap_run": (
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh node-bootstrap-run http://127.0.0.1:8100 {node_code} cli"
|
||
if node_code else ""
|
||
),
|
||
"node_bootstrap_plan": (
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh node-bootstrap-plan http://127.0.0.1:8100 {node_code}"
|
||
if node_code else ""
|
||
),
|
||
"node_acceptance_plan": (
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh node-acceptance-plan http://127.0.0.1:8100 {node_code}"
|
||
if node_code else ""
|
||
),
|
||
"node_acceptance_run": (
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh node-acceptance-run http://127.0.0.1:8100 {node_code} cli"
|
||
if node_code else ""
|
||
),
|
||
"agent_gap_check": "bash domain-api/deploy/multi-region/drive_ops_center.sh agent-gap-check http://127.0.0.1:8100",
|
||
},
|
||
}, ensure_ascii=False, indent=2))
|
||
PY
|
||
}
|
||
|
||
recover_node_onboarding() {
|
||
local mainland_base_url="$1"
|
||
local node_code="$2"
|
||
local confirm_flag="${3:-}"
|
||
local requested_by="${4:-cli}"
|
||
if [[ -z "${node_code}" ]]; then
|
||
echo "node_code is required" >&2
|
||
exit 1
|
||
fi
|
||
|
||
local recovery_endpoint
|
||
if [[ "${confirm_flag}" == "confirm" ]]; then
|
||
recovery_endpoint="${mainland_base_url}/api/v1/ops/nodes/${node_code}/onboarding/recovery/execute"
|
||
else
|
||
recovery_endpoint="${mainland_base_url}/api/v1/ops/nodes/${node_code}/onboarding/recovery/preview"
|
||
fi
|
||
local recovery_request_json
|
||
recovery_request_json="$(python3 - <<'PY' "${requested_by}"
|
||
import json
|
||
import sys
|
||
print(json.dumps({"requested_by": str(sys.argv[1] or "cli").strip() or "cli"}, ensure_ascii=False))
|
||
PY
|
||
)"
|
||
|
||
local raw_http_response
|
||
local source_mode="recovery_endpoint"
|
||
raw_http_response="$(request_with_status "POST" "${recovery_endpoint}" "${recovery_request_json}")"
|
||
local http_status="${raw_http_response##*$'\n'HTTP_STATUS=}"
|
||
local response_json="${raw_http_response%$'\n'HTTP_STATUS=*}"
|
||
|
||
if [[ "${http_status}" == "404" ]]; then
|
||
source_mode="onboarding"
|
||
raw_http_response="$(request_with_status "GET" "${mainland_base_url}/api/v1/ops/nodes/${node_code}/onboarding")"
|
||
http_status="${raw_http_response##*$'\n'HTTP_STATUS=}"
|
||
response_json="${raw_http_response%$'\n'HTTP_STATUS=*}"
|
||
fi
|
||
|
||
if [[ "${http_status}" == "404" ]]; then
|
||
source_mode="handover"
|
||
raw_http_response="$(request_with_status "GET" "${mainland_base_url}/api/v1/ops/nodes/${node_code}/handover")"
|
||
http_status="${raw_http_response##*$'\n'HTTP_STATUS=}"
|
||
response_json="${raw_http_response%$'\n'HTTP_STATUS=*}"
|
||
if [[ "${http_status}" == "404" ]]; then
|
||
source_mode="stack_diagnosis"
|
||
raw_http_response="$(request_with_status "GET" "${mainland_base_url}/api/v1/ops/stack-diagnosis?base_url=${mainland_base_url}")"
|
||
http_status="${raw_http_response##*$'\n'HTTP_STATUS=}"
|
||
response_json="${raw_http_response%$'\n'HTTP_STATUS=*}"
|
||
if [[ "${http_status}" -ge 200 && "${http_status}" -lt 300 ]]; then
|
||
response_json="$(python3 - <<'PY' "${response_json}" "${node_code}"
|
||
import json
|
||
import sys
|
||
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
target_node_code = str(sys.argv[2] or "").strip()
|
||
data = payload.get("data") or {}
|
||
managed_nodes = data.get("managed_nodes") or {}
|
||
problem_nodes = [dict(item or {}) for item in list(managed_nodes.get("problem_nodes") or []) if isinstance(item, dict)]
|
||
node = next((item for item in problem_nodes if str(item.get("node_code") or "").strip() == target_node_code), {})
|
||
first_gap = dict(managed_nodes.get("first_handover_gap") or {})
|
||
if not node and str(first_gap.get("node_code") or "").strip() == target_node_code:
|
||
node = dict(first_gap.get("node") or {})
|
||
agent_state = str(node.get("agent_state") or "").strip()
|
||
stage_code = "unknown"
|
||
stage_summary = "当前节点存在接管缺口,建议先生成 bootstrap 方案并检查 Node Agent 接入。"
|
||
if agent_state == "pending_bootstrap":
|
||
stage_code = "pending_bootstrap"
|
||
stage_summary = "已签发有效 Token,但目标节点尚未执行 bootstrap 片段。"
|
||
elif agent_state == "runtime_only":
|
||
stage_code = "runtime_only"
|
||
stage_summary = "控制面能看到 runtime 心跳,但 Node Agent 尚未接管。"
|
||
elif agent_state == "stale":
|
||
stage_code = "stale"
|
||
stage_summary = "Node Agent 曾接入过,但心跳已过期,需先恢复服务或重新接入。"
|
||
elif str(node.get("remote_access_state") or "").strip() == "agent_pending":
|
||
stage_code = "agent_pending"
|
||
stage_summary = "当前 remote-agent 仍未就绪,先补齐 bootstrap 接入链路。"
|
||
|
||
print(json.dumps({
|
||
"data": {
|
||
"node_code": target_node_code,
|
||
"summary": stage_summary,
|
||
"onboarding_stage": {
|
||
"code": stage_code,
|
||
"summary": stage_summary,
|
||
},
|
||
"acceptance": {
|
||
"status_code": "blocked",
|
||
"summary": "当前还没进入标准接管就绪状态,暂不建议直接跑 onboarding.acceptance。",
|
||
},
|
||
}
|
||
}, ensure_ascii=False))
|
||
PY
|
||
)"
|
||
http_status="200"
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
python3 - <<'PY' "${response_json}" "${node_code}" "${mainland_base_url}" "${confirm_flag}" "${requested_by}" "${http_status}" "${source_mode}"
|
||
import json
|
||
import sys
|
||
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
node_code = str(sys.argv[2] or "").strip()
|
||
base_url = str(sys.argv[3] or "").strip()
|
||
confirm_flag = str(sys.argv[4] or "").strip()
|
||
requested_by = str(sys.argv[5] or "cli").strip() or "cli"
|
||
backend_http_status = int(sys.argv[6] or 0)
|
||
source_mode = str(sys.argv[7] or "").strip()
|
||
|
||
data = payload.get("data") or {}
|
||
if str(data.get("recovery_action") or "").strip():
|
||
recovery_decision = (data.get("recovery_decision") or {})
|
||
selected_preview = (data.get("selected_preview") or {})
|
||
selected_run = (data.get("selected_run") or {})
|
||
selected_payload = selected_run or selected_preview
|
||
playbook_preview = (selected_payload.get("playbook_preview") or {})
|
||
playbook_run = (selected_payload.get("playbook_run") or {})
|
||
follow_up = (selected_payload.get("follow_up") or {})
|
||
result = {
|
||
"node_code": node_code,
|
||
"source_mode": source_mode,
|
||
"backend_http_status": backend_http_status,
|
||
"stage_code": str((recovery_decision.get("stage_code") or "")).strip(),
|
||
"acceptance_status_code": str((recovery_decision.get("acceptance_status_code") or "")).strip(),
|
||
"recovery_action": str(data.get("recovery_action") or "").strip(),
|
||
"selected_action": "node_acceptance_run" if str(data.get("recovery_action") or "").strip() == "run_acceptance" else "node_bootstrap_run",
|
||
"summary": str((recovery_decision.get("summary") or payload.get("message") or "")).strip(),
|
||
"command": str((recovery_decision.get("command_hint") or "")).strip(),
|
||
"execution_mode": str((selected_payload.get("execution_mode") or "")).strip(),
|
||
"playbook_preview_key": str(((playbook_preview.get("playbook") or {}).get("key") or "")).strip(),
|
||
"playbook_run_code": str((playbook_run.get("run_code") or "")).strip(),
|
||
"follow_up_next_stage_code": str((follow_up.get("next_stage_code") or "")).strip(),
|
||
"follow_up_summary": str((follow_up.get("summary") or "")).strip(),
|
||
"follow_up_commands": list(follow_up.get("recommended_commands") or []),
|
||
"confirm_required": True,
|
||
"confirmed": confirm_flag == "confirm",
|
||
"handled_by_recovery_endpoint": source_mode == "recovery_endpoint",
|
||
}
|
||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||
raise SystemExit(0)
|
||
|
||
stage = data.get("onboarding_stage") or data.get("stage") or {}
|
||
acceptance = data.get("acceptance") or {}
|
||
recovery = data.get("recovery_decision") or {}
|
||
stage_code = str(stage.get("code") or "").strip()
|
||
acceptance_status_code = str(acceptance.get("status_code") or "").strip()
|
||
stage_summary = str(stage.get("summary") or data.get("summary") or "").strip()
|
||
|
||
if not stage_code:
|
||
handover_stage = data.get("stage") or {}
|
||
stage_code = str(handover_stage.get("code") or "").strip()
|
||
stage_summary = str(handover_stage.get("summary") or stage_summary).strip()
|
||
|
||
action = "noop"
|
||
reason = stage_summary or "当前节点暂无可执行恢复动作。"
|
||
command = ""
|
||
recovery_action = str(recovery.get("action") or "").strip()
|
||
recovery_command_hint = str(recovery.get("command_hint") or "").strip()
|
||
recovery_summary = str(recovery.get("summary") or "").strip()
|
||
if recovery_action == "run_acceptance":
|
||
action = "node_acceptance_run"
|
||
reason = recovery_summary or "当前节点已进入验收窗口,建议直接跑 onboarding.acceptance。"
|
||
command = recovery_command_hint or f"bash domain-api/deploy/multi-region/drive_ops_center.sh node-acceptance-run {base_url} {node_code} {requested_by}"
|
||
elif recovery_action == "bootstrap_run":
|
||
action = "node_bootstrap_run"
|
||
reason = recovery_summary or "当前节点仍处于接入阶段,建议先签发 onboarding.bootstrap。"
|
||
command = recovery_command_hint or f"bash domain-api/deploy/multi-region/drive_ops_center.sh node-bootstrap-run {base_url} {node_code} {requested_by}"
|
||
elif acceptance_status_code in {"ready", "ssh_only"} or stage_code == "acceptance_ready":
|
||
action = "node_acceptance_run"
|
||
reason = "当前节点已进入验收窗口,建议直接跑 onboarding.acceptance。"
|
||
command = f"bash domain-api/deploy/multi-region/drive_ops_center.sh node-acceptance-run {base_url} {node_code} {requested_by}"
|
||
elif stage_code in {"pending_bootstrap", "runtime_only", "profile_incomplete", "ssh_ready", "token_issue", "stale", "agent_pending", "unknown", ""}:
|
||
action = "node_bootstrap_run"
|
||
reason = "当前节点仍处于接入阶段,建议先签发 onboarding.bootstrap。"
|
||
command = f"bash domain-api/deploy/multi-region/drive_ops_center.sh node-bootstrap-run {base_url} {node_code} {requested_by}"
|
||
|
||
result = {
|
||
"node_code": node_code,
|
||
"source_mode": source_mode,
|
||
"backend_http_status": backend_http_status,
|
||
"stage_code": stage_code,
|
||
"acceptance_status_code": acceptance_status_code,
|
||
"recovery_action": recovery_action,
|
||
"selected_action": action,
|
||
"summary": reason,
|
||
"command": command,
|
||
"confirm_required": True,
|
||
"confirmed": confirm_flag == "confirm",
|
||
}
|
||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||
PY
|
||
|
||
if [[ "${confirm_flag}" != "confirm" ]]; then
|
||
return 0
|
||
fi
|
||
|
||
if [[ "${source_mode}" == "recovery_endpoint" ]]; then
|
||
return 0
|
||
fi
|
||
|
||
local selected_action
|
||
selected_action="$(python3 - <<'PY' "${response_json}"
|
||
import json
|
||
import sys
|
||
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
data = payload.get("data") or {}
|
||
stage = data.get("onboarding_stage") or data.get("stage") or {}
|
||
acceptance = data.get("acceptance") or {}
|
||
recovery = data.get("recovery_decision") or {}
|
||
stage_code = str(stage.get("code") or "").strip()
|
||
acceptance_status_code = str(acceptance.get("status_code") or "").strip()
|
||
recovery_action = str(recovery.get("action") or "").strip()
|
||
if recovery_action == "run_acceptance":
|
||
print("node_acceptance_run")
|
||
elif recovery_action == "bootstrap_run":
|
||
print("node_bootstrap_run")
|
||
elif acceptance_status_code in {"ready", "ssh_only"} or stage_code == "acceptance_ready":
|
||
print("node_acceptance_run")
|
||
elif stage_code in {"pending_bootstrap", "runtime_only", "profile_incomplete", "ssh_ready", "token_issue", "stale", "agent_pending", "unknown", ""}:
|
||
print("node_bootstrap_run")
|
||
else:
|
||
print("")
|
||
PY
|
||
)"
|
||
|
||
case "${selected_action}" in
|
||
node_bootstrap_run)
|
||
run_node_bootstrap "${mainland_base_url}" "${node_code}" "${requested_by}"
|
||
;;
|
||
node_acceptance_run)
|
||
run_node_acceptance "${mainland_base_url}" "${node_code}" "${requested_by}"
|
||
;;
|
||
*)
|
||
printf '%s\n' '{"message":"当前节点没有可自动执行的恢复动作。"}'
|
||
;;
|
||
esac
|
||
}
|
||
|
||
show_node_acceptance_plan() {
|
||
local mainland_base_url="$1"
|
||
local node_code="$2"
|
||
local requested_by="${3:-cli}"
|
||
if [[ -z "${node_code}" ]]; then
|
||
echo "node_code is required" >&2
|
||
exit 1
|
||
fi
|
||
local request_json
|
||
request_json="$(python3 - <<'PY' "${requested_by}"
|
||
import json
|
||
import sys
|
||
print(json.dumps({"requested_by": str(sys.argv[1] or "cli").strip() or "cli"}, ensure_ascii=False))
|
||
PY
|
||
)"
|
||
local raw_http_response
|
||
raw_http_response="$(request_with_status "POST" "${mainland_base_url}/api/v1/ops/nodes/${node_code}/onboarding/acceptance/preview" "${request_json}")"
|
||
local http_status="${raw_http_response##*$'\n'HTTP_STATUS=}"
|
||
local response_json="${raw_http_response%$'\n'HTTP_STATUS=*}"
|
||
if [[ "${http_status}" == "404" ]]; then
|
||
request_json="$(python3 - <<'PY' "${node_code}" "${requested_by}"
|
||
import json
|
||
import sys
|
||
node_code = str(sys.argv[1] or "").strip()
|
||
requested_by = str(sys.argv[2] or "cli").strip() or "cli"
|
||
print(json.dumps({
|
||
"playbook_key": "onboarding.acceptance",
|
||
"node_codes": [node_code] if node_code else [],
|
||
"execution_mode": "remote-agent",
|
||
"auto_approve": True,
|
||
"requested_by": requested_by,
|
||
}, ensure_ascii=False))
|
||
PY
|
||
)"
|
||
raw_http_response="$(request_with_status "POST" "${mainland_base_url}/api/v1/ops/playbooks/preview" "${request_json}")"
|
||
response_json="${raw_http_response%$'\n'HTTP_STATUS=*}"
|
||
fi
|
||
printf '%s\n' "${response_json}"
|
||
}
|
||
|
||
show_node_bootstrap_preview() {
|
||
local mainland_base_url="$1"
|
||
local node_code="$2"
|
||
local requested_by="${3:-cli}"
|
||
if [[ -z "${node_code}" ]]; then
|
||
echo "node_code is required" >&2
|
||
exit 1
|
||
fi
|
||
local request_json
|
||
request_json="$(python3 - <<'PY' "${requested_by}"
|
||
import json
|
||
import sys
|
||
print(json.dumps({"requested_by": str(sys.argv[1] or "cli").strip() or "cli"}, ensure_ascii=False))
|
||
PY
|
||
)"
|
||
local raw_http_response
|
||
raw_http_response="$(request_with_status "POST" "${mainland_base_url}/api/v1/ops/nodes/${node_code}/onboarding/bootstrap/preview" "${request_json}")"
|
||
local http_status="${raw_http_response##*$'\n'HTTP_STATUS=}"
|
||
local response_json="${raw_http_response%$'\n'HTTP_STATUS=*}"
|
||
if [[ "${http_status}" == "404" ]]; then
|
||
request_json="$(python3 - <<'PY' "${node_code}" "${requested_by}"
|
||
import json
|
||
import sys
|
||
node_code = str(sys.argv[1] or "").strip()
|
||
requested_by = str(sys.argv[2] or "cli").strip() or "cli"
|
||
print(json.dumps({
|
||
"playbook_key": "onboarding.bootstrap",
|
||
"node_codes": [node_code] if node_code else [],
|
||
"execution_mode": "control-plane",
|
||
"auto_approve": True,
|
||
"requested_by": requested_by,
|
||
}, ensure_ascii=False))
|
||
PY
|
||
)"
|
||
raw_http_response="$(request_with_status "POST" "${mainland_base_url}/api/v1/ops/playbooks/preview" "${request_json}")"
|
||
response_json="${raw_http_response%$'\n'HTTP_STATUS=*}"
|
||
fi
|
||
printf '%s\n' "${response_json}"
|
||
}
|
||
|
||
run_node_bootstrap() {
|
||
local mainland_base_url="$1"
|
||
local node_code="$2"
|
||
local requested_by="${3:-cli}"
|
||
if [[ -z "${node_code}" ]]; then
|
||
echo "node_code is required" >&2
|
||
exit 1
|
||
fi
|
||
local request_json
|
||
request_json="$(python3 - <<'PY' "${requested_by}"
|
||
import json
|
||
import sys
|
||
print(json.dumps({"requested_by": str(sys.argv[1] or "cli").strip() or "cli"}, ensure_ascii=False))
|
||
PY
|
||
)"
|
||
local raw_http_response
|
||
raw_http_response="$(request_with_status "POST" "${mainland_base_url}/api/v1/ops/nodes/${node_code}/onboarding/bootstrap/execute" "${request_json}")"
|
||
local http_status="${raw_http_response##*$'\n'HTTP_STATUS=}"
|
||
local response_json="${raw_http_response%$'\n'HTTP_STATUS=*}"
|
||
if [[ "${http_status}" == "404" ]]; then
|
||
request_json="$(python3 - <<'PY' "${node_code}" "${requested_by}"
|
||
import json
|
||
import sys
|
||
node_code = str(sys.argv[1] or "").strip()
|
||
requested_by = str(sys.argv[2] or "cli").strip() or "cli"
|
||
print(json.dumps({
|
||
"playbook_key": "onboarding.bootstrap",
|
||
"node_codes": [node_code] if node_code else [],
|
||
"execution_mode": "control-plane",
|
||
"auto_approve": True,
|
||
"requested_by": requested_by,
|
||
}, ensure_ascii=False))
|
||
PY
|
||
)"
|
||
raw_http_response="$(request_with_status "POST" "${mainland_base_url}/api/v1/ops/playbooks/execute" "${request_json}")"
|
||
response_json="${raw_http_response%$'\n'HTTP_STATUS=*}"
|
||
fi
|
||
printf '%s\n' "${response_json}"
|
||
echo
|
||
echo "[follow-up]"
|
||
python3 - <<'PY' "${response_json}" "${node_code}"
|
||
import json
|
||
import sys
|
||
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
node_code = str(sys.argv[2] or "").strip()
|
||
data = payload.get("data") or {}
|
||
follow_up = dict(data.get("follow_up") or {})
|
||
playbook_run = dict(data.get("playbook_run") or {})
|
||
follow_up_summary = str(follow_up.get("summary") or "").strip() or str(playbook_run.get("focus_summary") or "").strip()
|
||
print(json.dumps({
|
||
"node_code": node_code,
|
||
"playbook_run_code": str(playbook_run.get("run_code") or "").strip(),
|
||
"follow_up_next_stage_code": str(follow_up.get("next_stage_code") or "").strip(),
|
||
"follow_up_summary": follow_up_summary,
|
||
"follow_up_commands": list(follow_up.get("recommended_commands") or []),
|
||
}, ensure_ascii=False, indent=2))
|
||
PY
|
||
}
|
||
|
||
run_node_acceptance() {
|
||
local mainland_base_url="$1"
|
||
local node_code="$2"
|
||
local requested_by="${3:-cli}"
|
||
if [[ -z "${node_code}" ]]; then
|
||
echo "node_code is required" >&2
|
||
exit 1
|
||
fi
|
||
local request_json
|
||
request_json="$(python3 - <<'PY' "${requested_by}"
|
||
import json
|
||
import sys
|
||
print(json.dumps({"requested_by": str(sys.argv[1] or "cli").strip() or "cli"}, ensure_ascii=False))
|
||
PY
|
||
)"
|
||
local raw_http_response
|
||
raw_http_response="$(request_with_status "POST" "${mainland_base_url}/api/v1/ops/nodes/${node_code}/onboarding/acceptance/execute" "${request_json}")"
|
||
local http_status="${raw_http_response##*$'\n'HTTP_STATUS=}"
|
||
local response_json="${raw_http_response%$'\n'HTTP_STATUS=*}"
|
||
if [[ "${http_status}" == "404" ]]; then
|
||
request_json="$(python3 - <<'PY' "${node_code}" "${requested_by}"
|
||
import json
|
||
import sys
|
||
node_code = str(sys.argv[1] or "").strip()
|
||
requested_by = str(sys.argv[2] or "cli").strip() or "cli"
|
||
print(json.dumps({
|
||
"playbook_key": "onboarding.acceptance",
|
||
"node_codes": [node_code] if node_code else [],
|
||
"execution_mode": "remote-agent",
|
||
"auto_approve": True,
|
||
"requested_by": requested_by,
|
||
}, ensure_ascii=False))
|
||
PY
|
||
)"
|
||
raw_http_response="$(request_with_status "POST" "${mainland_base_url}/api/v1/ops/playbooks/execute" "${request_json}")"
|
||
response_json="${raw_http_response%$'\n'HTTP_STATUS=*}"
|
||
fi
|
||
printf '%s\n' "${response_json}"
|
||
echo
|
||
echo "[follow-up]"
|
||
python3 - <<'PY' "${response_json}" "${node_code}"
|
||
import json
|
||
import sys
|
||
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
node_code = str(sys.argv[2] or "").strip()
|
||
data = payload.get("data") or {}
|
||
follow_up = dict(data.get("follow_up") or {})
|
||
playbook_run = dict(data.get("playbook_run") or {})
|
||
follow_up_summary = str(follow_up.get("summary") or "").strip() or str(playbook_run.get("focus_summary") or "").strip()
|
||
print(json.dumps({
|
||
"node_code": node_code,
|
||
"playbook_run_code": str(playbook_run.get("run_code") or "").strip(),
|
||
"follow_up_next_stage_code": str(follow_up.get("next_stage_code") or "").strip(),
|
||
"follow_up_summary": follow_up_summary,
|
||
"follow_up_commands": list(follow_up.get("recommended_commands") or []),
|
||
}, ensure_ascii=False, indent=2))
|
||
PY
|
||
}
|
||
|
||
show_delivery_queue_records() {
|
||
local mainland_base_url="$1"
|
||
local node_code="$2"
|
||
local state="${3:-}"
|
||
if [[ -z "${node_code}" ]]; then
|
||
echo "node_code is required" >&2
|
||
exit 1
|
||
fi
|
||
local url="${mainland_base_url}/api/v1/ops/nodes/${node_code}/delivery-queue/records"
|
||
if [[ -n "${state}" ]]; then
|
||
url="${url}?state=${state}"
|
||
fi
|
||
local raw_http_response
|
||
raw_http_response="$(request_with_status "GET" "${url}")"
|
||
local http_status="${raw_http_response##*$'\n'HTTP_STATUS=}"
|
||
local response_json="${raw_http_response%$'\n'HTTP_STATUS=*}"
|
||
echo "[1/2] raw response"
|
||
printf '%s\n' "http_status=${http_status}"
|
||
print_json_or_raw "${response_json}"
|
||
echo
|
||
echo "[2/2] condensed summary"
|
||
python3 - <<'PY' "${response_json}" "${http_status}"
|
||
import json
|
||
import sys
|
||
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
http_status = int(sys.argv[2] or 0)
|
||
data = payload.get("data") or {}
|
||
summary = data.get("summary") or {}
|
||
records = list(data.get("records") or [])
|
||
node_code = str(data.get("node_code") or "").strip()
|
||
print(json.dumps({
|
||
"http_status": http_status,
|
||
"node_code": node_code,
|
||
"summary": {
|
||
"total": int(summary.get("total", 0) or 0),
|
||
"available_total": int(summary.get("available_total", 0) or 0),
|
||
"record_visibility": str(summary.get("record_visibility") or ""),
|
||
"available_state_counts": dict(summary.get("available_state_counts") or {}),
|
||
},
|
||
"recommended_commands": {
|
||
"queue_status": (
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh delivery-queue http://127.0.0.1:8100 {node_code}"
|
||
if node_code
|
||
else ""
|
||
),
|
||
"replay": (
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh delivery-queue-replay http://127.0.0.1:8100 {node_code}"
|
||
if node_code
|
||
else ""
|
||
),
|
||
},
|
||
"records": [
|
||
{
|
||
"record_id": str(item.get("record_id") or item.get("client_request_id") or item.get("record_key") or ""),
|
||
"state": str(item.get("state") or ""),
|
||
"request_kind": str(item.get("request_kind") or ""),
|
||
"created_at": str(item.get("created_at") or ""),
|
||
"dead_letter_at": str(item.get("dead_letter_at") or ""),
|
||
"label": str(item.get("label") or ""),
|
||
"record_source": str(item.get("record_source") or ""),
|
||
}
|
||
for item in records
|
||
],
|
||
}, ensure_ascii=False, indent=2))
|
||
PY
|
||
}
|
||
|
||
run_delivery_queue_action() {
|
||
local mainland_base_url="$1"
|
||
local node_code="$2"
|
||
local action_path="$3"
|
||
local body_json="$4"
|
||
if [[ -z "${node_code}" ]]; then
|
||
echo "node_code is required" >&2
|
||
exit 1
|
||
fi
|
||
local raw_http_response
|
||
raw_http_response="$(request_with_status "POST" "${mainland_base_url}/api/v1/ops/nodes/${node_code}${action_path}" "${body_json}")"
|
||
local http_status="${raw_http_response##*$'\n'HTTP_STATUS=}"
|
||
local response_json="${raw_http_response%$'\n'HTTP_STATUS=*}"
|
||
printf '%s\n' "http_status=${http_status}"
|
||
print_json_or_raw "${response_json}"
|
||
}
|
||
|
||
preview_runbook_sequence() {
|
||
local mainland_base_url="$1"
|
||
local sequence_key="$2"
|
||
local action_payload_json="$3"
|
||
local requested_by="$4"
|
||
|
||
if [[ "${action_payload_json}" == @* ]]; then
|
||
local action_payload_path="${action_payload_json#@}"
|
||
if [[ ! -f "${action_payload_path}" ]]; then
|
||
echo "payload file not found: ${action_payload_path}" >&2
|
||
exit 1
|
||
fi
|
||
action_payload_json="$(cat "${action_payload_path}")"
|
||
fi
|
||
|
||
local request_json
|
||
request_json="$(python3 - <<'PY' "${sequence_key}" "${action_payload_json}" "${requested_by}"
|
||
import json
|
||
import sys
|
||
|
||
sequence_key = str(sys.argv[1] or "").strip()
|
||
payload_raw = str(sys.argv[2] or "").strip() or "{}"
|
||
requested_by = str(sys.argv[3] or "").strip() or "cli/ops-center/runbook-preview"
|
||
|
||
try:
|
||
action_payload = json.loads(payload_raw)
|
||
except Exception as exc:
|
||
raise SystemExit(f"invalid action payload json: {exc}")
|
||
|
||
if not isinstance(action_payload, dict):
|
||
raise SystemExit("action payload must be a json object")
|
||
|
||
request_payload = {
|
||
"source_label": "cli/runbook-preview",
|
||
"title": sequence_key,
|
||
"executor_kind": "runbook_sequence",
|
||
"sequence_key": sequence_key,
|
||
"requested_by": requested_by,
|
||
"include_secondary": True,
|
||
"primary_action_payload": action_payload,
|
||
"secondary_action_payload": action_payload,
|
||
}
|
||
print(json.dumps(request_payload, ensure_ascii=False))
|
||
PY
|
||
)"
|
||
|
||
echo "[1/3] runbook preview request"
|
||
print_json_or_raw "${request_json}"
|
||
echo
|
||
echo
|
||
|
||
local raw_http_response
|
||
raw_http_response="$(request_with_status "POST" "${mainland_base_url}/api/v1/ops/driver-actions/preview" "${request_json}")"
|
||
local http_status="${raw_http_response##*$'\n'HTTP_STATUS=}"
|
||
local response_json="${raw_http_response%$'\n'HTTP_STATUS=*}"
|
||
|
||
echo "[2/3] raw response"
|
||
printf '%s\n' "http_status=${http_status}"
|
||
print_json_or_raw "${response_json}"
|
||
echo
|
||
echo
|
||
|
||
echo "[3/3] condensed summary"
|
||
python3 - <<'PY' "${response_json}" "${http_status}"
|
||
import json
|
||
import sys
|
||
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
http_status = int(sys.argv[2] or 0)
|
||
data = payload.get("data") or {}
|
||
primary = data.get("primary") or {}
|
||
secondary = data.get("secondary") or {}
|
||
|
||
print(json.dumps({
|
||
"http_status": http_status,
|
||
"message": str(payload.get("message") or ""),
|
||
"sequence_key": str(data.get("sequence_key") or ""),
|
||
"execution_chain": str(data.get("execution_chain") or ""),
|
||
"focus_ref": data.get("focus_ref") or {},
|
||
"primary_focus_ref": data.get("primary_focus_ref") or {},
|
||
"secondary_focus_ref": data.get("secondary_focus_ref") or {},
|
||
"primary": {
|
||
"label": str(primary.get("label") or ""),
|
||
"action_code": str(primary.get("action_code") or ""),
|
||
"target_api": str(primary.get("target_api") or ""),
|
||
"recommendation_label": str(primary.get("recommendation_label") or ""),
|
||
"focus_ref": primary.get("focus_ref") or {},
|
||
"request_payload": primary.get("request_payload") or {},
|
||
},
|
||
"secondary": (
|
||
{
|
||
"label": str(secondary.get("label") or ""),
|
||
"action_code": str(secondary.get("action_code") or ""),
|
||
"target_api": str(secondary.get("target_api") or ""),
|
||
"recommendation_label": str(secondary.get("recommendation_label") or ""),
|
||
"focus_ref": secondary.get("focus_ref") or {},
|
||
"request_payload": secondary.get("request_payload") or {},
|
||
}
|
||
if secondary else {}
|
||
),
|
||
}, ensure_ascii=False, indent=2))
|
||
PY
|
||
|
||
if [[ "${http_status}" -lt 200 || "${http_status}" -ge 300 ]]; then
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
build_codex_focus_bundle() {
|
||
local codex_response_json="$1"
|
||
local entry_key="$2"
|
||
local requested_by="$3"
|
||
local source_label="$4"
|
||
python3 - <<'PY' "${codex_response_json}" "${entry_key}" "${requested_by}" "${source_label}"
|
||
import json
|
||
import sys
|
||
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
requested_entry_key = str(sys.argv[2] or "").strip()
|
||
requested_by = str(sys.argv[3] or "").strip() or "cli/ops-center/codex-focus-preview"
|
||
source_label = str(sys.argv[4] or "").strip() or "cli/codex-focus-preview"
|
||
data = payload.get("data") or {}
|
||
focus = data.get("focus") or {}
|
||
entries = [dict(item or {}) for item in list(data.get("entries") or []) if dict(item or {})]
|
||
activity_focus = [dict(item or {}) for item in list(data.get("activity_focus") or []) if dict(item or {})]
|
||
|
||
def build_activity_focus_entry(item):
|
||
normalized = dict(item or {})
|
||
focus_action_code = str(normalized.get("focus_action_code") or "").strip()
|
||
if not str(normalized.get("key") or "").strip() or not focus_action_code:
|
||
return {}
|
||
|
||
focus_ref = dict(normalized.get("focus_ref") or {})
|
||
target_node_codes = list(normalized.get("target_node_codes") or [])
|
||
focus_action_payload = dict(normalized.get("focus_action_payload") or {})
|
||
summary = str(normalized.get("summary") or "").strip()
|
||
reason = summary or "该焦点属于运行态观察项,建议进入对应工作区查看上下文。"
|
||
return {
|
||
"key": str(normalized.get("key") or "").strip(),
|
||
"title": str(normalized.get("title") or "").strip(),
|
||
"summary": summary,
|
||
"reason": reason,
|
||
"meta_text": str(normalized.get("meta_text") or "").strip(),
|
||
"executor_kind": "driver_action",
|
||
"primary_action_code": focus_action_code,
|
||
"secondary_action_code": "",
|
||
"focus_ref": focus_ref,
|
||
"primary_focus_ref": focus_ref,
|
||
"secondary_focus_ref": {},
|
||
"primary_label": "进入工作区",
|
||
"secondary_label": "",
|
||
"primary_execution": {
|
||
"label": "进入工作区",
|
||
"node_codes": target_node_codes,
|
||
"action_payload": focus_action_payload,
|
||
},
|
||
"secondary_execution": {},
|
||
"autopilot": {
|
||
"recommendation": "open_ui",
|
||
"automation_level": "ui_only",
|
||
"status_text": str(normalized.get("status_label") or normalized.get("status") or "").strip(),
|
||
"executor_mode_hint": "ui-intent",
|
||
"target_api": "/api/v1/ops/driver-actions/execute",
|
||
"target_method": "POST",
|
||
"reason": reason,
|
||
"confirm_required": False,
|
||
"ui_only": True,
|
||
"blocked": False,
|
||
},
|
||
"observation_only": True,
|
||
"activity_key": str(normalized.get("activity_key") or "").strip(),
|
||
"activity_kind": str(normalized.get("activity_kind") or "").strip(),
|
||
"ui_intent_kind": str(normalized.get("ui_intent_kind") or "").strip(),
|
||
"contract_navigation": dict(normalized.get("contract_navigation") or {}),
|
||
}
|
||
|
||
activity_focus_entries = [entry for entry in (build_activity_focus_entry(item) for item in activity_focus) if entry]
|
||
|
||
target_entry_key = requested_entry_key or str(focus.get("entry_key") or "").strip()
|
||
target_entry = {}
|
||
if target_entry_key:
|
||
target_entry = next((item for item in entries if str(item.get("key") or "").strip() == target_entry_key), {})
|
||
if not target_entry and target_entry_key:
|
||
target_entry = next((item for item in activity_focus_entries if str(item.get("key") or "").strip() == target_entry_key), {})
|
||
if not target_entry and entries:
|
||
target_entry = dict(entries[0] or {})
|
||
target_entry_key = str(target_entry.get("key") or "").strip()
|
||
if not target_entry and activity_focus_entries:
|
||
target_entry = dict(activity_focus_entries[0] or {})
|
||
target_entry_key = str(target_entry.get("key") or "").strip()
|
||
|
||
if not target_entry:
|
||
raise SystemExit("codex brief 当前没有可预览的 entry")
|
||
|
||
autopilot = dict(target_entry.get("autopilot") or {})
|
||
primary_execution = dict(target_entry.get("primary_execution") or {})
|
||
secondary_execution = dict(target_entry.get("secondary_execution") or {})
|
||
request_payload = {
|
||
"source_label": source_label,
|
||
"title": str(target_entry.get("title") or "").strip() or target_entry_key or "codex-focus",
|
||
"summary": str(target_entry.get("summary") or "").strip(),
|
||
"reason": str(target_entry.get("reason") or "").strip(),
|
||
"meta_text": str(target_entry.get("meta_text") or "").strip(),
|
||
"executor_kind": str(target_entry.get("executor_kind") or "driver_action").strip() or "driver_action",
|
||
"sequence_key": str(target_entry.get("sequence_key") or "").strip(),
|
||
"focus_ref": dict(target_entry.get("focus_ref") or {}),
|
||
"primary_focus_ref": dict(target_entry.get("primary_focus_ref") or {}),
|
||
"secondary_focus_ref": dict(target_entry.get("secondary_focus_ref") or {}),
|
||
"primary_label": str(primary_execution.get("label") or target_entry.get("primary_label") or "").strip(),
|
||
"secondary_label": str(secondary_execution.get("label") or target_entry.get("secondary_label") or "").strip(),
|
||
"primary_action_code": str(target_entry.get("primary_action_code") or "").strip(),
|
||
"secondary_action_code": str(target_entry.get("secondary_action_code") or "").strip(),
|
||
"primary_node_codes": list(primary_execution.get("node_codes") or target_entry.get("node_codes") or []),
|
||
"secondary_node_codes": list(secondary_execution.get("node_codes") or target_entry.get("node_codes") or []),
|
||
"primary_action_payload": dict(primary_execution.get("action_payload") or {}),
|
||
"secondary_action_payload": dict(secondary_execution.get("action_payload") or {}),
|
||
"include_secondary": True,
|
||
"requested_by": requested_by,
|
||
}
|
||
|
||
print(json.dumps({
|
||
"selected_entry": {
|
||
"key": target_entry_key,
|
||
"title": str(target_entry.get("title") or "").strip(),
|
||
"executor_kind": str(target_entry.get("executor_kind") or "").strip(),
|
||
"recommendation": str(autopilot.get("recommendation") or "").strip(),
|
||
"automation_level": str(autopilot.get("automation_level") or "").strip(),
|
||
"status_text": str(autopilot.get("status_text") or "").strip(),
|
||
"executor_mode_hint": str(autopilot.get("executor_mode_hint") or "").strip(),
|
||
"target_api": str(autopilot.get("target_api") or "").strip(),
|
||
"target_method": str(autopilot.get("target_method") or "").strip(),
|
||
"reason": str(autopilot.get("reason") or "").strip(),
|
||
"confirm_required": bool(autopilot.get("confirm_required", False)),
|
||
"ui_only": bool(autopilot.get("ui_only", False)),
|
||
"blocked": bool(autopilot.get("blocked", False)),
|
||
"primary_action_code": str(target_entry.get("primary_action_code") or "").strip(),
|
||
"sequence_key": str(target_entry.get("sequence_key") or "").strip(),
|
||
"focus_ref": dict(target_entry.get("focus_ref") or {}),
|
||
"primary_focus_ref": dict(target_entry.get("primary_focus_ref") or {}),
|
||
"secondary_focus_ref": dict(target_entry.get("secondary_focus_ref") or {}),
|
||
"observation_only": bool(target_entry.get("observation_only", False)),
|
||
"activity_key": str(target_entry.get("activity_key") or ""),
|
||
"activity_kind": str(target_entry.get("activity_kind") or ""),
|
||
"ui_intent_kind": str(target_entry.get("ui_intent_kind") or ""),
|
||
"contract_navigation": dict(target_entry.get("contract_navigation") or {}),
|
||
},
|
||
"preview_request": request_payload,
|
||
}, ensure_ascii=False))
|
||
PY
|
||
}
|
||
|
||
preview_codex_focus_entry() {
|
||
local mainland_base_url="$1"
|
||
local entry_key="$2"
|
||
local requested_by="$3"
|
||
local request_json
|
||
request_json="$(python3 - <<'PY' "${entry_key}" "${requested_by}"
|
||
import json
|
||
import sys
|
||
print(json.dumps({
|
||
"entry_key": str(sys.argv[1] or "").strip(),
|
||
"requested_by": str(sys.argv[2] or "").strip() or "cli/ops-center/codex-focus-preview",
|
||
"source_label": "cli/codex-focus-preview",
|
||
"include_secondary": True,
|
||
}, ensure_ascii=False))
|
||
PY
|
||
)"
|
||
|
||
echo "[1/3] request"
|
||
print_json_or_raw "${request_json}"
|
||
echo
|
||
echo
|
||
|
||
local raw_http_response
|
||
raw_http_response="$(request_with_status "POST" "${mainland_base_url}/api/v1/ops/codex-actions/resolve" "${request_json}")"
|
||
local http_status="${raw_http_response##*$'\n'HTTP_STATUS=}"
|
||
local response_json="${raw_http_response%$'\n'HTTP_STATUS=*}"
|
||
|
||
echo "[2/3] raw response"
|
||
printf '%s\n' "http_status=${http_status}"
|
||
print_json_or_raw "${response_json}"
|
||
echo
|
||
echo
|
||
|
||
echo "[3/3] condensed summary"
|
||
python3 - <<'PY' "${response_json}" "${http_status}"
|
||
import json
|
||
import sys
|
||
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
http_status = int(sys.argv[2] or 0)
|
||
data = payload.get("data") or {}
|
||
selected = data.get("selected_entry") or {}
|
||
preview = data.get("preview") or {}
|
||
primary = preview.get("primary") or {}
|
||
secondary = preview.get("secondary") or {}
|
||
gate = data.get("gate") or {}
|
||
|
||
print(json.dumps({
|
||
"http_status": http_status,
|
||
"message": str(payload.get("message") or ""),
|
||
"entry_key": str(data.get("entry_key") or ""),
|
||
"title": str(selected.get("title") or ""),
|
||
"executor_kind": str(preview.get("executor_kind") or selected.get("executor_kind") or ""),
|
||
"contract": {
|
||
"contract_key": str(preview.get("contract_key") or ""),
|
||
"contract_version": str(preview.get("contract_version") or ""),
|
||
"contract_primary_endpoint": str(preview.get("contract_primary_endpoint") or ""),
|
||
"preview_endpoint": str(preview.get("preview_endpoint") or ""),
|
||
"schema_doc_path": str(preview.get("contract_schema_doc_path") or ""),
|
||
},
|
||
"execution_chain": str(preview.get("execution_chain") or ""),
|
||
"focus_ref": data.get("focus_ref") or {},
|
||
"gate": {
|
||
"decision": str(gate.get("decision") or ""),
|
||
"decision_label": str(gate.get("decision_label") or ""),
|
||
"recommendation": str(gate.get("recommendation") or ""),
|
||
"recommendation_label": str(gate.get("recommendation_label") or ""),
|
||
"will_execute": bool(gate.get("will_execute", False)),
|
||
"confirm_required": bool(gate.get("confirm_required", False)),
|
||
"target_api": str(gate.get("target_api") or ""),
|
||
"focus_ref": gate.get("focus_ref") or {},
|
||
},
|
||
"selected_entry": {
|
||
"key": str(selected.get("key") or ""),
|
||
"contract_navigation": selected.get("contract_navigation") or {},
|
||
"focus_ref": selected.get("focus_ref") or {},
|
||
"primary_focus_ref": selected.get("primary_focus_ref") or {},
|
||
"secondary_focus_ref": selected.get("secondary_focus_ref") or {},
|
||
},
|
||
"primary": {
|
||
"label": str(primary.get("label") or ""),
|
||
"action_code": str(primary.get("action_code") or ""),
|
||
"target_api": str(primary.get("target_api") or ""),
|
||
"recommendation_label": str(primary.get("recommendation_label") or ""),
|
||
"focus_ref": primary.get("focus_ref") or {},
|
||
},
|
||
"secondary": (
|
||
{
|
||
"label": str(secondary.get("label") or ""),
|
||
"action_code": str(secondary.get("action_code") or ""),
|
||
"target_api": str(secondary.get("target_api") or ""),
|
||
"recommendation_label": str(secondary.get("recommendation_label") or ""),
|
||
"focus_ref": secondary.get("focus_ref") or {},
|
||
}
|
||
if secondary else {}
|
||
),
|
||
}, ensure_ascii=False, indent=2))
|
||
PY
|
||
|
||
if [[ "${http_status}" -lt 200 || "${http_status}" -ge 300 ]]; then
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
run_codex_focus_entry() {
|
||
local mainland_base_url="$1"
|
||
local entry_key="$2"
|
||
local confirm_flag="$3"
|
||
local requested_by="$4"
|
||
local confirm_risky
|
||
confirm_risky="$(confirm_to_bool "${confirm_flag}")"
|
||
|
||
local request_json
|
||
request_json="$(python3 - <<'PY' "${entry_key}" "${requested_by}" "${confirm_risky}"
|
||
import json
|
||
import sys
|
||
print(json.dumps({
|
||
"entry_key": str(sys.argv[1] or "").strip(),
|
||
"requested_by": str(sys.argv[2] or "").strip() or "cli/ops-center/codex-focus-run",
|
||
"source_label": "cli/codex-focus-run",
|
||
"include_secondary": True,
|
||
"confirm": str(sys.argv[3] or "").strip().lower() == "true",
|
||
}, ensure_ascii=False))
|
||
PY
|
||
)"
|
||
|
||
echo "[1/3] request"
|
||
print_json_or_raw "${request_json}"
|
||
echo
|
||
echo
|
||
|
||
local execute_http_response
|
||
execute_http_response="$(request_with_status "POST" "${mainland_base_url}/api/v1/ops/codex-actions/execute" "${request_json}")"
|
||
local execute_http_status="${execute_http_response##*$'\n'HTTP_STATUS=}"
|
||
local execute_response_json="${execute_http_response%$'\n'HTTP_STATUS=*}"
|
||
|
||
echo "[2/3] raw response"
|
||
printf '%s\n' "http_status=${execute_http_status}"
|
||
print_json_or_raw "${execute_response_json}"
|
||
echo
|
||
echo
|
||
|
||
echo "[3/3] execution result"
|
||
python3 - <<'PY' "${execute_response_json}" "${execute_http_status}"
|
||
import json
|
||
import sys
|
||
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
http_status = int(sys.argv[2] or 0)
|
||
data = payload.get("data") or {}
|
||
gate = data.get("gate") or {}
|
||
execution_result = data.get("execution_result") or {}
|
||
print(json.dumps({
|
||
"http_status": http_status,
|
||
"entry_key": str(data.get("entry_key") or ""),
|
||
"code": int((0 if payload.get("code", 1) in (0, "0", False) else payload.get("code", 1)) or 0),
|
||
"message": str(payload.get("message") or ""),
|
||
"executed": bool(data.get("executed", False)),
|
||
"focus_ref": data.get("focus_ref") or {},
|
||
"decision": str(gate.get("decision") or ""),
|
||
"decision_label": str(gate.get("decision_label") or ""),
|
||
"recommendation": str(gate.get("recommendation") or ""),
|
||
"target_api": str(gate.get("target_api") or ""),
|
||
"target_method": str(gate.get("target_method") or ""),
|
||
"gate_focus_ref": gate.get("focus_ref") or {},
|
||
"handled": bool(execution_result.get("handled", False)),
|
||
"mode": str(execution_result.get("mode") or ""),
|
||
"action_code": str(gate.get("action_code") or execution_result.get("action_code") or execution_result.get("driver_action_code") or ""),
|
||
"sequence_key": str(gate.get("sequence_key") or execution_result.get("sequence_key") or ""),
|
||
}, ensure_ascii=False, indent=2))
|
||
PY
|
||
|
||
if [[ "${execute_http_status}" -lt 200 || "${execute_http_status}" -ge 300 ]]; then
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
build_driver_focus_bundle() {
|
||
local driver_feed_response_json="$1"
|
||
local entry_key="$2"
|
||
local requested_by="$3"
|
||
local source_label="$4"
|
||
local secondary_flag="$5"
|
||
local confirm_flag="$6"
|
||
python3 - <<'PY' "${driver_feed_response_json}" "${entry_key}" "${requested_by}" "${source_label}" "${secondary_flag}" "${confirm_flag}"
|
||
import json
|
||
import sys
|
||
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
requested_entry_key = str(sys.argv[2] or "").strip()
|
||
requested_by = str(sys.argv[3] or "").strip() or "cli/ops-center/driver-focus-preview"
|
||
source_label = str(sys.argv[4] or "").strip() or "cli/driver-focus-preview"
|
||
secondary_flag = str(sys.argv[5] or "primary").strip().lower()
|
||
confirm_flag = str(sys.argv[6] or "").strip().lower()
|
||
data = payload.get("data") or {}
|
||
top_recommendation = dict(data.get("top_recommendation") or {})
|
||
entries = [dict(item or {}) for item in list(data.get("entries") or []) if dict(item or {})]
|
||
activity_focus = [dict(item or {}) for item in list(data.get("activity_focus") or []) if dict(item or {})]
|
||
|
||
secondary = secondary_flag in {"secondary", "second", "2", "true", "yes", "y"}
|
||
confirm = confirm_flag in {"1", "true", "yes", "y", "confirm", "confirmed"}
|
||
|
||
def build_activity_focus_entry(item):
|
||
normalized = dict(item or {})
|
||
focus_action_code = str(normalized.get("focus_action_code") or "").strip()
|
||
if not str(normalized.get("key") or "").strip() or not focus_action_code:
|
||
return {}
|
||
|
||
focus_ref = dict(normalized.get("focus_ref") or {})
|
||
target_node_codes = list(normalized.get("target_node_codes") or [])
|
||
focus_action_payload = dict(normalized.get("focus_action_payload") or {})
|
||
summary = str(normalized.get("summary") or "").strip()
|
||
reason = summary or "该焦点属于运行态观察项,建议进入对应工作区查看上下文。"
|
||
return {
|
||
"key": str(normalized.get("key") or "").strip(),
|
||
"kind": "activity_focus",
|
||
"lane": "activity_focus",
|
||
"title": str(normalized.get("title") or "").strip(),
|
||
"summary": summary,
|
||
"reason": reason,
|
||
"meta_text": str(normalized.get("meta_text") or "").strip(),
|
||
"status": str(normalized.get("status") or "").strip(),
|
||
"executor_kind": "driver_action",
|
||
"sequence_key": "",
|
||
"primary_label": "进入工作区",
|
||
"secondary_label": "",
|
||
"primary_action_code": focus_action_code,
|
||
"secondary_action_code": "",
|
||
"node_codes": target_node_codes,
|
||
"primary_node_codes": target_node_codes,
|
||
"secondary_node_codes": [],
|
||
"focus_ref": focus_ref,
|
||
"primary_focus_ref": focus_ref,
|
||
"secondary_focus_ref": {},
|
||
"primary_action_payload": focus_action_payload,
|
||
"secondary_action_payload": {},
|
||
"observation_only": True,
|
||
"activity_key": str(normalized.get("activity_key") or "").strip(),
|
||
"activity_kind": str(normalized.get("activity_kind") or "").strip(),
|
||
"ui_intent_kind": str(normalized.get("ui_intent_kind") or "").strip(),
|
||
"contract_navigation": dict(normalized.get("contract_navigation") or {}),
|
||
}
|
||
|
||
activity_focus_entries = [entry for entry in (build_activity_focus_entry(item) for item in activity_focus) if entry]
|
||
|
||
target_entry = {}
|
||
target_entry_key = requested_entry_key
|
||
if requested_entry_key:
|
||
target_entry = next((item for item in entries if str(item.get("key") or "").strip() == requested_entry_key), {})
|
||
if not target_entry and requested_entry_key:
|
||
target_entry = next((item for item in activity_focus_entries if str(item.get("key") or "").strip() == requested_entry_key), {})
|
||
if not target_entry and requested_entry_key and str(top_recommendation.get("key") or "").strip() == requested_entry_key:
|
||
target_entry = dict(top_recommendation or {})
|
||
if not target_entry:
|
||
target_entry = dict(top_recommendation or {})
|
||
target_entry_key = str(target_entry.get("key") or "").strip()
|
||
if not target_entry and entries:
|
||
target_entry = dict(entries[0] or {})
|
||
target_entry_key = str(target_entry.get("key") or "").strip()
|
||
if not target_entry and activity_focus_entries:
|
||
target_entry = dict(activity_focus_entries[0] or {})
|
||
target_entry_key = str(target_entry.get("key") or "").strip()
|
||
|
||
if not target_entry:
|
||
raise SystemExit("driver feed 当前没有可执行的焦点动作")
|
||
|
||
request_payload = {
|
||
"source_label": source_label,
|
||
"title": str(target_entry.get("title") or "").strip() or target_entry_key or "driver-focus",
|
||
"summary": str(target_entry.get("summary") or "").strip(),
|
||
"reason": str(target_entry.get("reason") or "").strip(),
|
||
"meta_text": str(target_entry.get("meta_text") or "").strip(),
|
||
"executor_kind": str(target_entry.get("executor_kind") or "driver_action").strip() or "driver_action",
|
||
"sequence_key": str(target_entry.get("sequence_key") or "").strip(),
|
||
"focus_ref": dict(target_entry.get("focus_ref") or {}),
|
||
"primary_focus_ref": dict(target_entry.get("primary_focus_ref") or {}),
|
||
"secondary_focus_ref": dict(target_entry.get("secondary_focus_ref") or {}),
|
||
"primary_label": str(target_entry.get("primary_label") or "").strip(),
|
||
"secondary_label": str(target_entry.get("secondary_label") or "").strip(),
|
||
"primary_action_code": str(target_entry.get("primary_action_code") or "").strip(),
|
||
"secondary_action_code": str(target_entry.get("secondary_action_code") or "").strip(),
|
||
"primary_node_codes": list(target_entry.get("primary_node_codes") or target_entry.get("node_codes") or []),
|
||
"secondary_node_codes": list(target_entry.get("secondary_node_codes") or target_entry.get("node_codes") or []),
|
||
"primary_action_payload": dict(target_entry.get("primary_action_payload") or {}),
|
||
"secondary_action_payload": dict(target_entry.get("secondary_action_payload") or {}),
|
||
"include_secondary": True,
|
||
"requested_by": requested_by,
|
||
"secondary": secondary,
|
||
"confirm": confirm,
|
||
}
|
||
|
||
print(json.dumps({
|
||
"selected_entry": {
|
||
"key": target_entry_key,
|
||
"kind": str(target_entry.get("kind") or "").strip(),
|
||
"lane": str(target_entry.get("lane") or "").strip(),
|
||
"title": str(target_entry.get("title") or "").strip(),
|
||
"status": str(target_entry.get("status") or "").strip(),
|
||
"executor_kind": str(target_entry.get("executor_kind") or "").strip(),
|
||
"sequence_key": str(target_entry.get("sequence_key") or "").strip(),
|
||
"primary_action_code": str(target_entry.get("primary_action_code") or "").strip(),
|
||
"secondary_action_code": str(target_entry.get("secondary_action_code") or "").strip(),
|
||
"focus_ref": dict(target_entry.get("focus_ref") or {}),
|
||
"primary_focus_ref": dict(target_entry.get("primary_focus_ref") or {}),
|
||
"secondary_focus_ref": dict(target_entry.get("secondary_focus_ref") or {}),
|
||
"observation_only": bool(target_entry.get("observation_only", False)),
|
||
"activity_key": str(target_entry.get("activity_key") or ""),
|
||
"activity_kind": str(target_entry.get("activity_kind") or ""),
|
||
"ui_intent_kind": str(target_entry.get("ui_intent_kind") or ""),
|
||
"contract_navigation": dict(target_entry.get("contract_navigation") or {}),
|
||
},
|
||
"request": request_payload,
|
||
}, ensure_ascii=False))
|
||
PY
|
||
}
|
||
|
||
preview_driver_focus_entry() {
|
||
local mainland_base_url="$1"
|
||
local entry_key="$2"
|
||
local requested_by="$3"
|
||
|
||
local raw_driver_feed_response
|
||
raw_driver_feed_response="$(request_with_status "GET" "${mainland_base_url}/api/v1/ops/driver-feed")"
|
||
local driver_feed_http_status="${raw_driver_feed_response##*$'\n'HTTP_STATUS=}"
|
||
local driver_feed_response_json="${raw_driver_feed_response%$'\n'HTTP_STATUS=*}"
|
||
|
||
if [[ "${driver_feed_http_status}" -lt 200 || "${driver_feed_http_status}" -ge 300 ]]; then
|
||
echo "[1/3] driver feed raw response"
|
||
printf '%s\n' "http_status=${driver_feed_http_status}"
|
||
print_json_or_raw "${driver_feed_response_json}"
|
||
exit 1
|
||
fi
|
||
|
||
local bundle_json
|
||
bundle_json="$(build_driver_focus_bundle "${driver_feed_response_json}" "${entry_key}" "${requested_by}" "cli/driver-focus-preview" "primary" "false")"
|
||
local selected_entry_json
|
||
selected_entry_json="$(python3 - <<'PY' "${bundle_json}"
|
||
import json
|
||
import sys
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
print(json.dumps(payload.get("selected_entry") or {}, ensure_ascii=False))
|
||
PY
|
||
)"
|
||
local request_json
|
||
request_json="$(python3 - <<'PY' "${bundle_json}"
|
||
import json
|
||
import sys
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
print(json.dumps(payload.get("request") or {}, ensure_ascii=False))
|
||
PY
|
||
)"
|
||
|
||
echo "[1/4] selected focus"
|
||
print_json_or_raw "${selected_entry_json}"
|
||
echo
|
||
echo
|
||
|
||
echo "[2/4] request"
|
||
print_json_or_raw "${request_json}"
|
||
echo
|
||
echo
|
||
|
||
local raw_http_response
|
||
raw_http_response="$(request_with_status "POST" "${mainland_base_url}/api/v1/ops/driver-actions/resolve" "${request_json}")"
|
||
local http_status="${raw_http_response##*$'\n'HTTP_STATUS=}"
|
||
local response_json="${raw_http_response%$'\n'HTTP_STATUS=*}"
|
||
|
||
echo "[3/4] raw response"
|
||
printf '%s\n' "http_status=${http_status}"
|
||
print_json_or_raw "${response_json}"
|
||
echo
|
||
echo
|
||
|
||
echo "[4/4] condensed summary"
|
||
python3 - <<'PY' "${response_json}" "${http_status}" "${selected_entry_json}"
|
||
import json
|
||
import sys
|
||
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
http_status = int(sys.argv[2] or 0)
|
||
selected = json.loads(str(sys.argv[3] or "{}"))
|
||
data = payload.get("data") or {}
|
||
preview = data.get("preview") or {}
|
||
primary = preview.get("primary") or {}
|
||
secondary = preview.get("secondary") or {}
|
||
gate = data.get("gate") or {}
|
||
|
||
print(json.dumps({
|
||
"http_status": http_status,
|
||
"message": str(payload.get("message") or ""),
|
||
"selected_entry": selected,
|
||
"selected_section": str(data.get("selected_section") or ""),
|
||
"contract": {
|
||
"contract_key": str(preview.get("contract_key") or ""),
|
||
"contract_version": str(preview.get("contract_version") or ""),
|
||
"contract_primary_endpoint": str(preview.get("contract_primary_endpoint") or ""),
|
||
"preview_endpoint": str(preview.get("preview_endpoint") or ""),
|
||
"schema_doc_path": str(preview.get("contract_schema_doc_path") or ""),
|
||
},
|
||
"execution_chain": str(preview.get("execution_chain") or ""),
|
||
"focus_ref": data.get("focus_ref") or {},
|
||
"gate": {
|
||
"decision": str(gate.get("decision") or ""),
|
||
"decision_label": str(gate.get("decision_label") or ""),
|
||
"recommendation": str(gate.get("recommendation") or ""),
|
||
"recommendation_label": str(gate.get("recommendation_label") or ""),
|
||
"will_execute": bool(gate.get("will_execute", False)),
|
||
"confirm_required": bool(gate.get("confirm_required", False)),
|
||
"target_api": str(gate.get("target_api") or ""),
|
||
"focus_ref": gate.get("focus_ref") or {},
|
||
},
|
||
"primary": {
|
||
"label": str(primary.get("label") or ""),
|
||
"action_code": str(primary.get("action_code") or ""),
|
||
"target_api": str(primary.get("target_api") or ""),
|
||
"recommendation_label": str(primary.get("recommendation_label") or ""),
|
||
"focus_ref": primary.get("focus_ref") or {},
|
||
"request_payload": primary.get("request_payload") or {},
|
||
},
|
||
"secondary": (
|
||
{
|
||
"label": str(secondary.get("label") or ""),
|
||
"action_code": str(secondary.get("action_code") or ""),
|
||
"target_api": str(secondary.get("target_api") or ""),
|
||
"recommendation_label": str(secondary.get("recommendation_label") or ""),
|
||
"focus_ref": secondary.get("focus_ref") or {},
|
||
"request_payload": secondary.get("request_payload") or {},
|
||
}
|
||
if secondary else {}
|
||
),
|
||
}, ensure_ascii=False, indent=2))
|
||
PY
|
||
|
||
if [[ "${http_status}" -lt 200 || "${http_status}" -ge 300 ]]; then
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
run_driver_focus_entry() {
|
||
local mainland_base_url="$1"
|
||
local entry_key="$2"
|
||
local secondary_flag="$3"
|
||
local confirm_flag="$4"
|
||
local requested_by="$5"
|
||
|
||
local raw_driver_feed_response
|
||
raw_driver_feed_response="$(request_with_status "GET" "${mainland_base_url}/api/v1/ops/driver-feed")"
|
||
local driver_feed_http_status="${raw_driver_feed_response##*$'\n'HTTP_STATUS=}"
|
||
local driver_feed_response_json="${raw_driver_feed_response%$'\n'HTTP_STATUS=*}"
|
||
|
||
if [[ "${driver_feed_http_status}" -lt 200 || "${driver_feed_http_status}" -ge 300 ]]; then
|
||
echo "[1/3] driver feed raw response"
|
||
printf '%s\n' "http_status=${driver_feed_http_status}"
|
||
print_json_or_raw "${driver_feed_response_json}"
|
||
exit 1
|
||
fi
|
||
|
||
local bundle_json
|
||
bundle_json="$(build_driver_focus_bundle "${driver_feed_response_json}" "${entry_key}" "${requested_by}" "cli/driver-focus-run" "${secondary_flag}" "${confirm_flag}")"
|
||
local selected_entry_json
|
||
selected_entry_json="$(python3 - <<'PY' "${bundle_json}"
|
||
import json
|
||
import sys
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
print(json.dumps(payload.get("selected_entry") or {}, ensure_ascii=False))
|
||
PY
|
||
)"
|
||
local request_json
|
||
request_json="$(python3 - <<'PY' "${bundle_json}"
|
||
import json
|
||
import sys
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
print(json.dumps(payload.get("request") or {}, ensure_ascii=False))
|
||
PY
|
||
)"
|
||
|
||
echo "[1/4] selected focus"
|
||
print_json_or_raw "${selected_entry_json}"
|
||
echo
|
||
echo
|
||
|
||
echo "[2/4] request"
|
||
print_json_or_raw "${request_json}"
|
||
echo
|
||
echo
|
||
|
||
local raw_http_response
|
||
raw_http_response="$(request_with_status "POST" "${mainland_base_url}/api/v1/ops/driver-actions/execute-resolved" "${request_json}")"
|
||
local http_status="${raw_http_response##*$'\n'HTTP_STATUS=}"
|
||
local response_json="${raw_http_response%$'\n'HTTP_STATUS=*}"
|
||
|
||
echo "[3/4] raw response"
|
||
printf '%s\n' "http_status=${http_status}"
|
||
print_json_or_raw "${response_json}"
|
||
echo
|
||
echo
|
||
|
||
echo "[4/4] execution result"
|
||
python3 - <<'PY' "${response_json}" "${http_status}" "${selected_entry_json}"
|
||
import json
|
||
import sys
|
||
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
http_status = int(sys.argv[2] or 0)
|
||
selected = json.loads(str(sys.argv[3] or "{}"))
|
||
data = payload.get("data") or {}
|
||
gate = data.get("gate") or {}
|
||
execution_result = data.get("execution_result") or {}
|
||
|
||
raw_code = payload.get("code", 1)
|
||
if raw_code in (0, "0", False):
|
||
normalized_code = 0
|
||
else:
|
||
normalized_code = int(raw_code or 1)
|
||
|
||
print(json.dumps({
|
||
"http_status": http_status,
|
||
"code": normalized_code,
|
||
"message": str(payload.get("message") or ""),
|
||
"selected_entry": selected,
|
||
"executed": bool(data.get("executed", False)),
|
||
"selected_section": str(data.get("selected_section") or ""),
|
||
"focus_ref": data.get("focus_ref") or {},
|
||
"decision": str(gate.get("decision") or ""),
|
||
"decision_label": str(gate.get("decision_label") or ""),
|
||
"recommendation": str(gate.get("recommendation") or ""),
|
||
"target_api": str(gate.get("target_api") or ""),
|
||
"target_method": str(gate.get("target_method") or ""),
|
||
"gate_focus_ref": gate.get("focus_ref") or {},
|
||
"action_code": str(gate.get("action_code") or execution_result.get("action_code") or execution_result.get("driver_action_code") or ""),
|
||
"sequence_key": str(gate.get("sequence_key") or execution_result.get("sequence_key") or ""),
|
||
"handled": bool(execution_result.get("handled", False)),
|
||
"mode": str(execution_result.get("mode") or ""),
|
||
}, ensure_ascii=False, indent=2))
|
||
PY
|
||
|
||
if [[ "${http_status}" -lt 200 || "${http_status}" -ge 300 ]]; then
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
build_stack_next_bundle() {
|
||
local stack_diagnosis_response_json="$1"
|
||
local requested_by="$2"
|
||
local source_label="$3"
|
||
local confirm_flag="$4"
|
||
python3 - <<'PY' "${stack_diagnosis_response_json}" "${requested_by}" "${source_label}" "${confirm_flag}"
|
||
import json
|
||
import sys
|
||
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
requested_by = str(sys.argv[2] or "").strip() or "cli/ops-center/stack-next"
|
||
source_label = str(sys.argv[3] or "").strip() or "cli/stack-next"
|
||
confirm_flag = str(sys.argv[4] or "").strip().lower()
|
||
|
||
data = payload.get("data") or {}
|
||
diagnosis = dict(data.get("diagnosis") or {})
|
||
operator_decision = dict(diagnosis.get("operator_decision") or {})
|
||
next_actions = dict(diagnosis.get("next_actions") or {})
|
||
recommended_commands = dict(diagnosis.get("recommended_commands") or {})
|
||
next_step = dict(diagnosis.get("next_step") or {})
|
||
recommended_actions = [dict(item or {}) for item in list(diagnosis.get("recommended_actions") or []) if dict(item or {})]
|
||
recommended_action_codes = {
|
||
str(item.get("action_code") or "").strip()
|
||
for item in recommended_actions
|
||
if str(item.get("action_code") or "").strip()
|
||
}
|
||
overview = dict(data.get("overview") or {})
|
||
managed_nodes = dict(data.get("managed_nodes") or {})
|
||
release_hub = dict(data.get("release_hub") or {})
|
||
issues = [dict(item or {}) for item in list(diagnosis.get("issues") or []) if dict(item or {})]
|
||
|
||
def normalize_node_codes(value):
|
||
result = []
|
||
seen = set()
|
||
if not isinstance(value, list):
|
||
return result
|
||
for item in value:
|
||
if isinstance(item, dict):
|
||
node_code = str(item.get("node_code") or "").strip()
|
||
else:
|
||
node_code = str(item or "").strip()
|
||
if not node_code or node_code in seen:
|
||
continue
|
||
seen.add(node_code)
|
||
result.append(node_code)
|
||
return result
|
||
|
||
|
||
def scene_node_log_command(node_code, limit=120, mode="key"):
|
||
node_code = str(node_code or "").strip()
|
||
if not node_code:
|
||
return ""
|
||
return (
|
||
"bash domain-api/deploy/multi-region/drive_ops_center.sh "
|
||
f"scene-node-log http://127.0.0.1:8100 {node_code} {int(limit or 120)} {str(mode or 'key').strip() or 'key'}"
|
||
)
|
||
|
||
|
||
if not operator_decision or not next_actions or not recommended_commands:
|
||
log_sync = dict((overview.get("log_sync") or {}))
|
||
problem_nodes = [dict(item or {}) for item in list(managed_nodes.get("problem_nodes") or []) if dict(item or {})]
|
||
launchpad_focus_ref = dict(release_hub.get("focus_ref") or release_hub.get("default_rollout_gate_focus_ref") or {})
|
||
default_rollout_gate_status = str(release_hub.get("default_rollout_gate_status") or "").strip()
|
||
first_problem_node_code = str((problem_nodes[0] if problem_nodes else {}).get("node_code") or "").strip()
|
||
missing_sample_node_codes = normalize_node_codes(log_sync.get("missing_sample_node_codes") or [])
|
||
scene_log_target_node_code = missing_sample_node_codes[0] if missing_sample_node_codes else first_problem_node_code
|
||
|
||
synthesized_recommended_commands = {
|
||
"api-restart": "bash domain-api/deploy/multi-region/drive_ops_center.sh runtime-refresh-recover http://127.0.0.1:8100",
|
||
"stack-diagnosis": "bash domain-api/deploy/multi-region/drive_ops_center.sh stack-diagnosis http://127.0.0.1:8100 summary",
|
||
"stack-check": "bash domain-api/deploy/multi-region/drive_ops_center.sh stack-diagnosis http://127.0.0.1:8100 summary",
|
||
"managed-node-bootstrap-plan": (
|
||
"bash domain-api/deploy/multi-region/drive_ops_center.sh "
|
||
f"node-bootstrap-plan http://127.0.0.1:8100 {first_problem_node_code}"
|
||
if first_problem_node_code
|
||
else ""
|
||
),
|
||
"scene_log": scene_node_log_command(scene_log_target_node_code, 120, "key"),
|
||
"driver_focus_preview": "bash domain-api/deploy/multi-region/drive_ops_center.sh driver-focus-preview http://127.0.0.1:8100",
|
||
"doctor": "bash domain-api/deploy/multi-region/drive_ops_center.sh doctor http://127.0.0.1:8100",
|
||
"node_handover": (
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh node-handover http://127.0.0.1:8100 {first_problem_node_code}"
|
||
if first_problem_node_code
|
||
else ""
|
||
),
|
||
"release_launchpad": "bash domain-api/deploy/multi-region/drive_ops_center.sh release-launchpad http://127.0.0.1:8100",
|
||
}
|
||
synthesized_operator_decision = {}
|
||
if any(str(item.get("code") or "").strip() in {"remote_log_sync_disabled", "remote_log_sync_waiting_sample"} for item in issues):
|
||
synthesized_operator_decision = {
|
||
"lane": "observability",
|
||
"priority": "high",
|
||
"reason_code": "scene_log_attention",
|
||
"title": "先看执行现场日志",
|
||
"summary": "当前总检优先建议先补现场可见性,再决定是否继续接管或推进发布。",
|
||
"next_focus": {
|
||
"kind": "node_scene_log",
|
||
"node_code": scene_log_target_node_code,
|
||
"mode": "key",
|
||
"limit": 120,
|
||
} if scene_log_target_node_code else {},
|
||
"primary_command_key": "scene_log",
|
||
"secondary_command_key": "driver_focus_preview",
|
||
}
|
||
elif any(str(item.get("code") or "").strip() == "runtime_build_schema_stale" for item in issues):
|
||
synthesized_operator_decision = {
|
||
"lane": "runtime_recovery",
|
||
"priority": "high",
|
||
"reason_code": "runtime_build_schema_stale",
|
||
"title": "先重启控制面 API,让运行时切到最新 schema",
|
||
"summary": "当前属于仓库代码已更新,但运行中的 API 仍旧停留在旧 schema 的场景。先重启 domaincheck-api,再重新看 stack-diagnosis 与 node-bootstrap-plan。",
|
||
"next_focus": {
|
||
"kind": "runtime_build_info",
|
||
"node_code": "",
|
||
},
|
||
"primary_command_key": "api-restart",
|
||
"secondary_command_key": "managed-node-bootstrap-plan" if first_problem_node_code else "stack-diagnosis",
|
||
}
|
||
elif any(str(item.get("code") or "").strip() == "managed_nodes_agent_pending" for item in issues):
|
||
synthesized_operator_decision = {
|
||
"lane": "node_handover",
|
||
"priority": "high",
|
||
"reason_code": "managed_nodes_agent_pending",
|
||
"title": "先补接管缺口",
|
||
"summary": "当前真正阻断是 remote-agent / 接管未就绪,应先把节点接入链路打通。",
|
||
"next_focus": {},
|
||
"primary_command_key": "node_handover",
|
||
"secondary_command_key": "doctor",
|
||
}
|
||
elif default_rollout_gate_status == "blocked":
|
||
synthesized_operator_decision = {
|
||
"lane": "release",
|
||
"priority": "medium",
|
||
"reason_code": "release_rollout_gate_blocked",
|
||
"title": "先看发布门禁",
|
||
"summary": str(release_hub.get("default_rollout_gate_summary") or "").strip(),
|
||
"next_focus": launchpad_focus_ref,
|
||
"primary_command_key": "release_launchpad",
|
||
"secondary_command_key": "doctor",
|
||
}
|
||
|
||
if synthesized_operator_decision:
|
||
operator_decision = synthesized_operator_decision
|
||
if synthesized_recommended_commands:
|
||
merged_recommended_commands = dict(synthesized_recommended_commands)
|
||
merged_recommended_commands.update({
|
||
str(key or "").strip(): str(value or "").strip()
|
||
for key, value in recommended_commands.items()
|
||
if str(key or "").strip() and str(value or "").strip()
|
||
})
|
||
recommended_commands = merged_recommended_commands
|
||
if operator_decision:
|
||
primary_key = str(operator_decision.get("primary_command_key") or "").strip()
|
||
secondary_key = str(operator_decision.get("secondary_command_key") or "").strip()
|
||
next_actions = {
|
||
"primary_command_key": primary_key,
|
||
"primary_command": str(recommended_commands.get(primary_key) or "").strip(),
|
||
"secondary_command_key": secondary_key,
|
||
"secondary_command": str(recommended_commands.get(secondary_key) or "").strip(),
|
||
}
|
||
|
||
primary_command_key = str(operator_decision.get("primary_command_key") or "").strip()
|
||
primary_command = str(
|
||
next_actions.get("primary_command")
|
||
or recommended_commands.get(primary_command_key)
|
||
or ""
|
||
).strip()
|
||
operator_focus_ref = dict(operator_decision.get("next_focus") or {})
|
||
action_code = ""
|
||
selection_mode = "driver_action"
|
||
|
||
if primary_command_key and primary_command_key in recommended_action_codes:
|
||
action_code = primary_command_key
|
||
elif primary_command_key and primary_command:
|
||
selection_mode = "command_only"
|
||
elif str(next_step.get("action_code") or "").strip():
|
||
action_code = str(next_step.get("action_code") or "").strip()
|
||
elif primary_command:
|
||
selection_mode = "command_only"
|
||
else:
|
||
raise SystemExit("stack diagnosis 当前既没有可执行 driver action,也没有可下钻命令")
|
||
|
||
matched_action = next(
|
||
(item for item in recommended_actions if str(item.get("action_code") or "").strip() == action_code),
|
||
{},
|
||
)
|
||
focus_ref = dict(operator_focus_ref or next_step.get("focus_ref") or matched_action.get("focus_ref") or {})
|
||
confirm = confirm_flag in {"1", "true", "yes", "y", "confirm", "confirmed"}
|
||
|
||
request_payload = {
|
||
"source_label": source_label,
|
||
"title": str(matched_action.get("title") or action_code).strip() or action_code,
|
||
"summary": str(next_step.get("reason") or matched_action.get("reason") or "").strip(),
|
||
"reason": str(next_step.get("reason") or matched_action.get("reason") or "").strip(),
|
||
"meta_text": str(next_step.get("source") or matched_action.get("source") or "stack-diagnosis").strip(),
|
||
"executor_kind": "driver_action",
|
||
"focus_ref": focus_ref,
|
||
"primary_focus_ref": focus_ref,
|
||
"primary_label": str(matched_action.get("title") or action_code).strip() or action_code,
|
||
"primary_action_code": action_code,
|
||
"primary_action_payload": {},
|
||
"include_secondary": True,
|
||
"requested_by": requested_by,
|
||
"confirm": confirm,
|
||
}
|
||
|
||
if selection_mode == "command_only":
|
||
print(json.dumps({
|
||
"http_status": 200,
|
||
"mode": selection_mode,
|
||
"message": "当前主决策优先指向命令型下钻动作,不进入 driver-actions/resolve。",
|
||
"contract": {
|
||
"contract_key": str((diagnosis.get("contract_navigation") or {}).get("primary_contract_key") or "ops_stack_diagnosis_contract").strip(),
|
||
"contract_version": str(diagnosis.get("contract_version") or "").strip(),
|
||
"contract_primary_endpoint": "/api/v1/ops/stack-diagnosis",
|
||
"preview_endpoint": "",
|
||
"schema_doc_path": "docs/schemas/ops_stack_diagnosis_contract.md",
|
||
},
|
||
"selected_entry": {
|
||
"action_code": primary_command_key,
|
||
"source": str(operator_decision.get("reason_code") or next_step.get("source") or "stack-diagnosis").strip(),
|
||
"title": str(operator_decision.get("title") or primary_command_key or "stack-next").strip(),
|
||
"reason": str(operator_decision.get("summary") or next_step.get("reason") or "").strip(),
|
||
"command": primary_command,
|
||
"focus_ref": focus_ref,
|
||
"contract_navigation": dict(
|
||
diagnosis.get("contract_navigation")
|
||
or next_step.get("contract_navigation")
|
||
or {}
|
||
),
|
||
},
|
||
"gate": {
|
||
"decision": "command_only",
|
||
"decision_label": "命令下钻",
|
||
"recommendation": "open_ui",
|
||
"recommendation_label": "先下钻",
|
||
"will_execute": False,
|
||
"confirm_required": False,
|
||
"target_api": "",
|
||
"focus_ref": focus_ref,
|
||
},
|
||
"execution_chain": "stack-diagnosis -> command-only guidance",
|
||
"focus_ref": focus_ref,
|
||
"request": {},
|
||
}, ensure_ascii=False))
|
||
raise SystemExit(0)
|
||
|
||
print(json.dumps({
|
||
"mode": selection_mode,
|
||
"selected_entry": {
|
||
"action_code": action_code,
|
||
"source": str(next_step.get("source") or matched_action.get("source") or "stack-diagnosis").strip(),
|
||
"title": str(matched_action.get("title") or action_code).strip() or action_code,
|
||
"reason": str(next_step.get("reason") or matched_action.get("reason") or "").strip(),
|
||
"command": str(next_step.get("command") or matched_action.get("command") or "").strip(),
|
||
"focus_ref": focus_ref,
|
||
"contract_navigation": dict(next_step.get("contract_navigation") or matched_action.get("contract_navigation") or {}),
|
||
},
|
||
"request": request_payload,
|
||
}, ensure_ascii=False))
|
||
PY
|
||
}
|
||
|
||
print_stack_diagnosis_failure_and_hint() {
|
||
local mainland_base_url="$1"
|
||
local stack_http_status="$2"
|
||
local stack_response_json="$3"
|
||
|
||
echo "[1/3] stack diagnosis raw response"
|
||
printf '%s\n' "http_status=${stack_http_status}"
|
||
print_json_or_raw "${stack_response_json}"
|
||
echo
|
||
echo
|
||
|
||
echo "[2/3] compatibility hint"
|
||
python3 - <<'PY' "${mainland_base_url}" "${stack_http_status}" "${stack_response_json}"
|
||
import json
|
||
import sys
|
||
|
||
base_url = str(sys.argv[1] or "").strip()
|
||
http_status = int(sys.argv[2] or 0)
|
||
raw_text = str(sys.argv[3] or "")
|
||
|
||
detail = ""
|
||
try:
|
||
payload = json.loads(raw_text)
|
||
except Exception:
|
||
payload = {}
|
||
else:
|
||
detail = str(payload.get("detail") or payload.get("message") or "").strip()
|
||
|
||
message = "stack-diagnosis 请求失败,请先检查控制面 API 连通性。"
|
||
commands = [
|
||
f"curl -sS {base_url}/api/v1/ops/stack-diagnosis",
|
||
]
|
||
|
||
if http_status == 404:
|
||
message = "当前控制面 API 还未暴露 /api/v1/ops/stack-diagnosis,通常表示运行中的后端版本还没升级或升级后未重启。"
|
||
commands = [
|
||
"git status",
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh runtime-refresh-recover {base_url}",
|
||
f"curl -sS {base_url}/api/v1/ops/stack-diagnosis",
|
||
]
|
||
|
||
print(json.dumps({
|
||
"http_status": http_status,
|
||
"detail": detail,
|
||
"message": message,
|
||
"recommended_commands": commands,
|
||
}, ensure_ascii=False, indent=2))
|
||
PY
|
||
echo
|
||
echo
|
||
|
||
echo "[3/3] next action"
|
||
if [[ "${stack_http_status}" == "404" ]]; then
|
||
printf '%s\n' "先把控制面后端升级到包含 /api/v1/ops/stack-diagnosis 的版本,并重启 domaincheck-api;随后再重试 stack-next。"
|
||
else
|
||
printf '%s\n' "先修复控制面 API / 网络问题,再重试 stack-next。"
|
||
fi
|
||
}
|
||
|
||
preview_stack_next_entry() {
|
||
local mainland_base_url="$1"
|
||
local requested_by="$2"
|
||
|
||
local raw_stack_response
|
||
raw_stack_response="$(request_with_status "GET" "${mainland_base_url}/api/v1/ops/stack-diagnosis?base_url=${mainland_base_url}")"
|
||
local stack_http_status="${raw_stack_response##*$'\n'HTTP_STATUS=}"
|
||
local stack_response_json="${raw_stack_response%$'\n'HTTP_STATUS=*}"
|
||
|
||
if [[ "${stack_http_status}" -lt 200 || "${stack_http_status}" -ge 300 ]]; then
|
||
print_stack_diagnosis_failure_and_hint "${mainland_base_url}" "${stack_http_status}" "${stack_response_json}"
|
||
exit 1
|
||
fi
|
||
|
||
local bundle_json
|
||
bundle_json="$(build_stack_next_bundle "${stack_response_json}" "${requested_by}" "cli/stack-next-preview" "false")"
|
||
local bundle_mode
|
||
bundle_mode="$(python3 - <<'PY' "${bundle_json}"
|
||
import json
|
||
import sys
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
print(str(payload.get("mode") or "driver_action").strip())
|
||
PY
|
||
)"
|
||
local selected_entry_json
|
||
selected_entry_json="$(python3 - <<'PY' "${bundle_json}"
|
||
import json
|
||
import sys
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
print(json.dumps(payload.get("selected_entry") or {}, ensure_ascii=False))
|
||
PY
|
||
)"
|
||
if [[ "${bundle_mode}" == "command_only" ]]; then
|
||
echo "[1/2] selected next step"
|
||
print_json_or_raw "${selected_entry_json}"
|
||
echo
|
||
echo
|
||
echo "[2/2] command-only guidance"
|
||
python3 - <<'PY' "${selected_entry_json}"
|
||
import json
|
||
import sys
|
||
|
||
selected = json.loads(str(sys.argv[1] or "{}"))
|
||
print(json.dumps({
|
||
"http_status": 200,
|
||
"mode": "command_only",
|
||
"message": "当前主决策优先指向命令型下钻动作,不进入 driver-actions/resolve。",
|
||
"contract": {
|
||
"contract_key": str((selected.get("contract_navigation") or {}).get("primary_contract_key") or "ops_stack_diagnosis_contract"),
|
||
"contract_version": "",
|
||
"contract_primary_endpoint": "/api/v1/ops/stack-diagnosis",
|
||
"preview_endpoint": "",
|
||
"schema_doc_path": "docs/schemas/ops_stack_diagnosis_contract.md",
|
||
},
|
||
"selected_entry": selected,
|
||
"gate": {
|
||
"decision": "command_only",
|
||
"decision_label": "命令下钻",
|
||
"recommendation": "open_ui",
|
||
"recommendation_label": "先下钻",
|
||
"will_execute": False,
|
||
"confirm_required": False,
|
||
"target_api": "",
|
||
"focus_ref": selected.get("focus_ref") or {},
|
||
},
|
||
"execution_chain": "stack-diagnosis -> command-only guidance",
|
||
"focus_ref": selected.get("focus_ref") or {},
|
||
"recommended_next_command": str(selected.get("command") or ""),
|
||
}, ensure_ascii=False, indent=2))
|
||
PY
|
||
return 0
|
||
fi
|
||
local request_json
|
||
request_json="$(python3 - <<'PY' "${bundle_json}"
|
||
import json
|
||
import sys
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
print(json.dumps(payload.get("request") or {}, ensure_ascii=False))
|
||
PY
|
||
)"
|
||
|
||
echo "[1/4] selected next step"
|
||
print_json_or_raw "${selected_entry_json}"
|
||
echo
|
||
echo
|
||
|
||
echo "[2/4] request"
|
||
print_json_or_raw "${request_json}"
|
||
echo
|
||
echo
|
||
|
||
local raw_http_response
|
||
raw_http_response="$(request_with_status "POST" "${mainland_base_url}/api/v1/ops/driver-actions/resolve" "${request_json}")"
|
||
local http_status="${raw_http_response##*$'\n'HTTP_STATUS=}"
|
||
local response_json="${raw_http_response%$'\n'HTTP_STATUS=*}"
|
||
|
||
echo "[3/4] raw response"
|
||
printf '%s\n' "http_status=${http_status}"
|
||
print_json_or_raw "${response_json}"
|
||
echo
|
||
echo
|
||
|
||
echo "[4/4] condensed summary"
|
||
python3 - <<'PY' "${response_json}" "${http_status}" "${selected_entry_json}"
|
||
import json
|
||
import sys
|
||
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
http_status = int(sys.argv[2] or 0)
|
||
selected = json.loads(str(sys.argv[3] or "{}"))
|
||
data = payload.get("data") or {}
|
||
preview = data.get("preview") or {}
|
||
primary = preview.get("primary") or {}
|
||
secondary = preview.get("secondary") or {}
|
||
gate = data.get("gate") or {}
|
||
|
||
print(json.dumps({
|
||
"http_status": http_status,
|
||
"message": str(payload.get("message") or ""),
|
||
"selected_entry": selected,
|
||
"selected_section": str(data.get("selected_section") or ""),
|
||
"contract": {
|
||
"contract_key": str(preview.get("contract_key") or ""),
|
||
"contract_version": str(preview.get("contract_version") or ""),
|
||
"contract_primary_endpoint": str(preview.get("contract_primary_endpoint") or ""),
|
||
"preview_endpoint": str(preview.get("preview_endpoint") or ""),
|
||
"schema_doc_path": str(preview.get("contract_schema_doc_path") or ""),
|
||
},
|
||
"execution_chain": str(preview.get("execution_chain") or ""),
|
||
"focus_ref": data.get("focus_ref") or {},
|
||
"gate": {
|
||
"decision": str(gate.get("decision") or ""),
|
||
"decision_label": str(gate.get("decision_label") or ""),
|
||
"recommendation": str(gate.get("recommendation") or ""),
|
||
"recommendation_label": str(gate.get("recommendation_label") or ""),
|
||
"will_execute": bool(gate.get("will_execute", False)),
|
||
"confirm_required": bool(gate.get("confirm_required", False)),
|
||
"target_api": str(gate.get("target_api") or ""),
|
||
"focus_ref": gate.get("focus_ref") or {},
|
||
},
|
||
"primary": {
|
||
"label": str(primary.get("label") or ""),
|
||
"action_code": str(primary.get("action_code") or ""),
|
||
"target_api": str(primary.get("target_api") or ""),
|
||
"recommendation_label": str(primary.get("recommendation_label") or ""),
|
||
"focus_ref": primary.get("focus_ref") or {},
|
||
"request_payload": primary.get("request_payload") or {},
|
||
},
|
||
"secondary": (
|
||
{
|
||
"label": str(secondary.get("label") or ""),
|
||
"action_code": str(secondary.get("action_code") or ""),
|
||
"target_api": str(secondary.get("target_api") or ""),
|
||
"recommendation_label": str(secondary.get("recommendation_label") or ""),
|
||
"focus_ref": secondary.get("focus_ref") or {},
|
||
"request_payload": secondary.get("request_payload") or {},
|
||
}
|
||
if secondary else {}
|
||
),
|
||
}, ensure_ascii=False, indent=2))
|
||
PY
|
||
|
||
if [[ "${http_status}" -lt 200 || "${http_status}" -ge 300 ]]; then
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
run_stack_next_entry() {
|
||
local mainland_base_url="$1"
|
||
local confirm_flag="$2"
|
||
local requested_by="$3"
|
||
|
||
local raw_stack_response
|
||
raw_stack_response="$(request_with_status "GET" "${mainland_base_url}/api/v1/ops/stack-diagnosis?base_url=${mainland_base_url}")"
|
||
local stack_http_status="${raw_stack_response##*$'\n'HTTP_STATUS=}"
|
||
local stack_response_json="${raw_stack_response%$'\n'HTTP_STATUS=*}"
|
||
|
||
if [[ "${stack_http_status}" -lt 200 || "${stack_http_status}" -ge 300 ]]; then
|
||
print_stack_diagnosis_failure_and_hint "${mainland_base_url}" "${stack_http_status}" "${stack_response_json}"
|
||
exit 1
|
||
fi
|
||
|
||
local bundle_json
|
||
bundle_json="$(build_stack_next_bundle "${stack_response_json}" "${requested_by}" "cli/stack-next-run" "${confirm_flag}")"
|
||
local bundle_mode
|
||
bundle_mode="$(python3 - <<'PY' "${bundle_json}"
|
||
import json
|
||
import sys
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
print(str(payload.get("mode") or "driver_action").strip())
|
||
PY
|
||
)"
|
||
local selected_entry_json
|
||
selected_entry_json="$(python3 - <<'PY' "${bundle_json}"
|
||
import json
|
||
import sys
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
print(json.dumps(payload.get("selected_entry") or {}, ensure_ascii=False))
|
||
PY
|
||
)"
|
||
if [[ "${bundle_mode}" == "command_only" ]]; then
|
||
echo "[1/2] selected next step"
|
||
print_json_or_raw "${selected_entry_json}"
|
||
echo
|
||
echo
|
||
echo "[2/2] command-only guidance"
|
||
python3 - <<'PY' "${selected_entry_json}"
|
||
import json
|
||
import sys
|
||
|
||
selected = json.loads(str(sys.argv[1] or "{}"))
|
||
print(json.dumps({
|
||
"http_status": 200,
|
||
"mode": "command_only",
|
||
"executed": False,
|
||
"message": "当前主决策属于命令型下钻动作,stack-next run 不会代替你执行本地 shell。",
|
||
"contract": {
|
||
"contract_key": str((selected.get("contract_navigation") or {}).get("primary_contract_key") or "ops_stack_diagnosis_contract"),
|
||
"contract_version": "",
|
||
"contract_primary_endpoint": "/api/v1/ops/stack-diagnosis",
|
||
"preview_endpoint": "",
|
||
"schema_doc_path": "docs/schemas/ops_stack_diagnosis_contract.md",
|
||
},
|
||
"selected_entry": selected,
|
||
"gate": {
|
||
"decision": "command_only",
|
||
"decision_label": "命令下钻",
|
||
"recommendation": "open_ui",
|
||
"recommendation_label": "先下钻",
|
||
"will_execute": False,
|
||
"confirm_required": False,
|
||
"target_api": "",
|
||
"focus_ref": selected.get("focus_ref") or {},
|
||
},
|
||
"execution_chain": "stack-diagnosis -> command-only guidance",
|
||
"focus_ref": selected.get("focus_ref") or {},
|
||
"recommended_next_command": str(selected.get("command") or ""),
|
||
}, ensure_ascii=False, indent=2))
|
||
PY
|
||
return 0
|
||
fi
|
||
local request_json
|
||
request_json="$(python3 - <<'PY' "${bundle_json}"
|
||
import json
|
||
import sys
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
print(json.dumps(payload.get("request") or {}, ensure_ascii=False))
|
||
PY
|
||
)"
|
||
|
||
echo "[1/4] selected next step"
|
||
print_json_or_raw "${selected_entry_json}"
|
||
echo
|
||
echo
|
||
|
||
echo "[2/4] request"
|
||
print_json_or_raw "${request_json}"
|
||
echo
|
||
echo
|
||
|
||
local raw_http_response
|
||
raw_http_response="$(request_with_status "POST" "${mainland_base_url}/api/v1/ops/driver-actions/execute-resolved" "${request_json}")"
|
||
local http_status="${raw_http_response##*$'\n'HTTP_STATUS=}"
|
||
local response_json="${raw_http_response%$'\n'HTTP_STATUS=*}"
|
||
|
||
echo "[3/4] raw response"
|
||
printf '%s\n' "http_status=${http_status}"
|
||
print_json_or_raw "${response_json}"
|
||
echo
|
||
echo
|
||
|
||
echo "[4/4] execution result"
|
||
python3 - <<'PY' "${response_json}" "${http_status}" "${selected_entry_json}"
|
||
import json
|
||
import sys
|
||
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
http_status = int(sys.argv[2] or 0)
|
||
selected = json.loads(str(sys.argv[3] or "{}"))
|
||
data = payload.get("data") or {}
|
||
gate = data.get("gate") or {}
|
||
execution_result = data.get("execution_result") or {}
|
||
|
||
raw_code = payload.get("code", 1)
|
||
if raw_code in (0, "0", False):
|
||
normalized_code = 0
|
||
else:
|
||
normalized_code = int(raw_code or 1)
|
||
|
||
print(json.dumps({
|
||
"http_status": http_status,
|
||
"code": normalized_code,
|
||
"message": str(payload.get("message") or ""),
|
||
"selected_entry": selected,
|
||
"executed": bool(data.get("executed", False)),
|
||
"selected_section": str(data.get("selected_section") or ""),
|
||
"focus_ref": data.get("focus_ref") or {},
|
||
"decision": str(gate.get("decision") or ""),
|
||
"decision_label": str(gate.get("decision_label") or ""),
|
||
"recommendation": str(gate.get("recommendation") or ""),
|
||
"target_api": str(gate.get("target_api") or ""),
|
||
"target_method": str(gate.get("target_method") or ""),
|
||
"gate_focus_ref": gate.get("focus_ref") or {},
|
||
"action_code": str(gate.get("action_code") or execution_result.get("action_code") or execution_result.get("driver_action_code") or ""),
|
||
"sequence_key": str(gate.get("sequence_key") or execution_result.get("sequence_key") or ""),
|
||
"handled": bool(execution_result.get("handled", False)),
|
||
"mode": str(execution_result.get("mode") or ""),
|
||
}, ensure_ascii=False, indent=2))
|
||
PY
|
||
|
||
if [[ "${http_status}" -lt 200 || "${http_status}" -ge 300 ]]; then
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
resolve_driver_action_entry() {
|
||
local mainland_base_url="$1"
|
||
local action_code="$2"
|
||
local action_payload_json="$3"
|
||
local requested_by="$4"
|
||
local source_label="$5"
|
||
|
||
action_payload_json="$(normalize_payload_json_input "${action_payload_json}")"
|
||
action_payload_json="$(enrich_driver_action_payload_json "${mainland_base_url}" "${action_code}" "${action_payload_json}")"
|
||
|
||
local request_json
|
||
request_json="$(python3 - <<'PY' "${action_code}" "${action_payload_json}" "${requested_by}" "${source_label}"
|
||
import json
|
||
import sys
|
||
|
||
action_code = str(sys.argv[1] or "").strip()
|
||
payload_raw = str(sys.argv[2] or "").strip() or "{}"
|
||
requested_by = str(sys.argv[3] or "").strip() or "cli/ops-center/driver-resolve"
|
||
source_label = str(sys.argv[4] or "").strip() or "cli/driver-resolve"
|
||
|
||
try:
|
||
action_payload = json.loads(payload_raw)
|
||
except Exception as exc:
|
||
raise SystemExit(f"invalid action payload json: {exc}")
|
||
|
||
if not isinstance(action_payload, dict):
|
||
raise SystemExit("action payload must be a json object")
|
||
|
||
node_codes = []
|
||
seen = set()
|
||
for item in list(action_payload.get("node_codes") or []):
|
||
value = str(item or "").strip()
|
||
if not value or value in seen:
|
||
continue
|
||
seen.add(value)
|
||
node_codes.append(value)
|
||
|
||
print(json.dumps({
|
||
"source_label": source_label,
|
||
"title": action_code,
|
||
"executor_kind": "driver_action",
|
||
"primary_label": action_code,
|
||
"primary_action_code": action_code,
|
||
"primary_node_codes": node_codes,
|
||
"primary_action_payload": action_payload,
|
||
"include_secondary": True,
|
||
"requested_by": requested_by,
|
||
}, ensure_ascii=False))
|
||
PY
|
||
)"
|
||
|
||
echo "[1/3] request"
|
||
print_json_or_raw "${request_json}"
|
||
echo
|
||
echo
|
||
|
||
local raw_http_response
|
||
raw_http_response="$(request_with_status "POST" "${mainland_base_url}/api/v1/ops/driver-actions/resolve" "${request_json}")"
|
||
local http_status="${raw_http_response##*$'\n'HTTP_STATUS=}"
|
||
local response_json="${raw_http_response%$'\n'HTTP_STATUS=*}"
|
||
|
||
echo "[2/3] raw response"
|
||
printf '%s\n' "http_status=${http_status}"
|
||
print_json_or_raw "${response_json}"
|
||
echo
|
||
echo
|
||
|
||
echo "[3/3] condensed summary"
|
||
python3 - <<'PY' "${response_json}" "${http_status}"
|
||
import json
|
||
import sys
|
||
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
http_status = int(sys.argv[2] or 0)
|
||
data = payload.get("data") or {}
|
||
preview = data.get("preview") or {}
|
||
primary = preview.get("primary") or {}
|
||
secondary = preview.get("secondary") or {}
|
||
gate = data.get("gate") or {}
|
||
|
||
print(json.dumps({
|
||
"http_status": http_status,
|
||
"message": str(payload.get("message") or ""),
|
||
"selected_section": str(data.get("selected_section") or ""),
|
||
"execution_chain": str(preview.get("execution_chain") or ""),
|
||
"focus_ref": data.get("focus_ref") or {},
|
||
"gate": {
|
||
"decision": str(gate.get("decision") or ""),
|
||
"decision_label": str(gate.get("decision_label") or ""),
|
||
"recommendation": str(gate.get("recommendation") or ""),
|
||
"recommendation_label": str(gate.get("recommendation_label") or ""),
|
||
"will_execute": bool(gate.get("will_execute", False)),
|
||
"confirm_required": bool(gate.get("confirm_required", False)),
|
||
"target_api": str(gate.get("target_api") or ""),
|
||
"focus_ref": gate.get("focus_ref") or {},
|
||
},
|
||
"primary": {
|
||
"label": str(primary.get("label") or ""),
|
||
"action_code": str(primary.get("action_code") or ""),
|
||
"target_api": str(primary.get("target_api") or ""),
|
||
"recommendation_label": str(primary.get("recommendation_label") or ""),
|
||
"focus_ref": primary.get("focus_ref") or {},
|
||
"request_payload": primary.get("request_payload") or {},
|
||
},
|
||
"secondary": (
|
||
{
|
||
"label": str(secondary.get("label") or ""),
|
||
"action_code": str(secondary.get("action_code") or ""),
|
||
"target_api": str(secondary.get("target_api") or ""),
|
||
"recommendation_label": str(secondary.get("recommendation_label") or ""),
|
||
"focus_ref": secondary.get("focus_ref") or {},
|
||
"request_payload": secondary.get("request_payload") or {},
|
||
}
|
||
if secondary else {}
|
||
),
|
||
}, ensure_ascii=False, indent=2))
|
||
PY
|
||
|
||
if [[ "${http_status}" -lt 200 || "${http_status}" -ge 300 ]]; then
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
run_driver_action_entry() {
|
||
local mainland_base_url="$1"
|
||
local action_code="$2"
|
||
local action_payload_json="$3"
|
||
local confirm_flag="$4"
|
||
local requested_by="$5"
|
||
local source_label="$6"
|
||
local confirm_value
|
||
confirm_value="$(confirm_to_bool "${confirm_flag}")"
|
||
|
||
action_payload_json="$(normalize_payload_json_input "${action_payload_json}")"
|
||
action_payload_json="$(enrich_driver_action_payload_json "${mainland_base_url}" "${action_code}" "${action_payload_json}")"
|
||
|
||
local request_json
|
||
request_json="$(python3 - <<'PY' "${action_code}" "${action_payload_json}" "${requested_by}" "${source_label}" "${confirm_value}"
|
||
import json
|
||
import sys
|
||
|
||
action_code = str(sys.argv[1] or "").strip()
|
||
payload_raw = str(sys.argv[2] or "").strip() or "{}"
|
||
requested_by = str(sys.argv[3] or "").strip() or "cli/ops-center/driver-run"
|
||
source_label = str(sys.argv[4] or "").strip() or "cli/driver-run"
|
||
confirm = str(sys.argv[5] or "").strip().lower() == "true"
|
||
|
||
try:
|
||
action_payload = json.loads(payload_raw)
|
||
except Exception as exc:
|
||
raise SystemExit(f"invalid action payload json: {exc}")
|
||
|
||
if not isinstance(action_payload, dict):
|
||
raise SystemExit("action payload must be a json object")
|
||
|
||
node_codes = []
|
||
seen = set()
|
||
for item in list(action_payload.get("node_codes") or []):
|
||
value = str(item or "").strip()
|
||
if not value or value in seen:
|
||
continue
|
||
seen.add(value)
|
||
node_codes.append(value)
|
||
|
||
print(json.dumps({
|
||
"source_label": source_label,
|
||
"title": action_code,
|
||
"executor_kind": "driver_action",
|
||
"primary_label": action_code,
|
||
"primary_action_code": action_code,
|
||
"primary_node_codes": node_codes,
|
||
"primary_action_payload": action_payload,
|
||
"include_secondary": True,
|
||
"requested_by": requested_by,
|
||
"confirm": confirm,
|
||
}, ensure_ascii=False))
|
||
PY
|
||
)"
|
||
|
||
echo "[1/3] request"
|
||
print_json_or_raw "${request_json}"
|
||
echo
|
||
echo
|
||
|
||
local raw_http_response
|
||
raw_http_response="$(request_with_status "POST" "${mainland_base_url}/api/v1/ops/driver-actions/execute-resolved" "${request_json}")"
|
||
local http_status="${raw_http_response##*$'\n'HTTP_STATUS=}"
|
||
local response_json="${raw_http_response%$'\n'HTTP_STATUS=*}"
|
||
|
||
echo "[2/3] raw response"
|
||
printf '%s\n' "http_status=${http_status}"
|
||
print_json_or_raw "${response_json}"
|
||
echo
|
||
echo
|
||
|
||
echo "[3/3] execution result"
|
||
python3 - <<'PY' "${response_json}" "${http_status}"
|
||
import json
|
||
import sys
|
||
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
http_status = int(sys.argv[2] or 0)
|
||
data = payload.get("data") or {}
|
||
gate = data.get("gate") or {}
|
||
execution_result = data.get("execution_result") or {}
|
||
|
||
raw_code = payload.get("code", 1)
|
||
if raw_code in (0, "0", False):
|
||
normalized_code = 0
|
||
else:
|
||
normalized_code = int(raw_code or 1)
|
||
|
||
print(json.dumps({
|
||
"http_status": http_status,
|
||
"code": normalized_code,
|
||
"message": str(payload.get("message") or ""),
|
||
"executed": bool(data.get("executed", False)),
|
||
"selected_section": str(data.get("selected_section") or ""),
|
||
"focus_ref": data.get("focus_ref") or {},
|
||
"decision": str(gate.get("decision") or ""),
|
||
"decision_label": str(gate.get("decision_label") or ""),
|
||
"recommendation": str(gate.get("recommendation") or ""),
|
||
"target_api": str(gate.get("target_api") or ""),
|
||
"target_method": str(gate.get("target_method") or ""),
|
||
"gate_focus_ref": gate.get("focus_ref") or {},
|
||
"action_code": str(gate.get("action_code") or execution_result.get("action_code") or execution_result.get("driver_action_code") or ""),
|
||
"handled": bool(execution_result.get("handled", False)),
|
||
"mode": str(execution_result.get("mode") or ""),
|
||
}, ensure_ascii=False, indent=2))
|
||
PY
|
||
|
||
if [[ "${http_status}" -lt 200 || "${http_status}" -ge 300 ]]; then
|
||
exit 1
|
||
fi
|
||
|
||
if [[ "${action_code}" == "enable_log_sync_key" || "${action_code}" == "enable_log_sync_full" || "${action_code}" == "disable_log_sync" ]]; then
|
||
echo
|
||
echo "[4/4] log sync post-check"
|
||
print_log_sync_post_check "${mainland_base_url}"
|
||
fi
|
||
}
|
||
|
||
print_log_sync_post_check() {
|
||
local mainland_base_url="$1"
|
||
local raw_http_response
|
||
raw_http_response="$(request_with_status "GET" "${mainland_base_url}/api/v1/ops/overview")"
|
||
local http_status="${raw_http_response##*$'\n'HTTP_STATUS=}"
|
||
local response_json="${raw_http_response%$'\n'HTTP_STATUS=*}"
|
||
|
||
python3 - <<'PY' "${response_json}" "${http_status}"
|
||
import json
|
||
import sys
|
||
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
http_status = int(sys.argv[2] or 0)
|
||
data = payload.get("data") or {}
|
||
execution_scene = data.get("execution_scene") or {}
|
||
log_sync = execution_scene.get("log_sync") or {}
|
||
participation_summary = execution_scene.get("participation_summary") or {}
|
||
|
||
participating_nodes = int(
|
||
log_sync.get("participating_nodes_total", log_sync.get("participating_node_count", 0))
|
||
or participation_summary.get("participating_nodes", 0)
|
||
or 0
|
||
)
|
||
covered_nodes = int(
|
||
log_sync.get("covered_participating_nodes", log_sync.get("covered_participating_node_count", 0))
|
||
or 0
|
||
)
|
||
line_count = int(log_sync.get("line_count", 0) or 0)
|
||
source_node_count = int(log_sync.get("source_node_count", 0) or 0)
|
||
missing_nodes = list(log_sync.get("missing_participating_nodes") or [])
|
||
mode = str(log_sync.get("mode") or "key").strip() or "key"
|
||
enabled = bool(log_sync.get("enabled", False))
|
||
last_at = str(log_sync.get("last_at") or "").strip()
|
||
preview_lines = [
|
||
str(item or "").strip()
|
||
for item in list(log_sync.get("preview_lines") or [])
|
||
if str(item or "").strip()
|
||
][:5]
|
||
|
||
status = "standby"
|
||
summary = "当前没有参与检测节点,远端日志回传处于待命状态。"
|
||
if participating_nodes > 0 and not enabled:
|
||
status = "disabled"
|
||
summary = "当前已有参与检测节点,但远端日志回传仍关闭。"
|
||
elif participating_nodes > 0 and (line_count <= 0 or source_node_count <= 0 or missing_nodes):
|
||
status = "waiting_sample"
|
||
summary = "远端日志回传已经开启,但样本或覆盖仍未形成完整闭环。"
|
||
elif participating_nodes > 0:
|
||
status = "ready"
|
||
summary = "远端日志回传已经形成样本,可以继续下钻节点现场日志。"
|
||
|
||
result = {
|
||
"http_status": http_status,
|
||
"code": int(payload.get("code", 0) or 0),
|
||
"message": str(payload.get("message") or ""),
|
||
"status": status,
|
||
"summary": summary,
|
||
"log_sync": {
|
||
"enabled": enabled,
|
||
"mode": mode,
|
||
"participating_nodes": participating_nodes,
|
||
"covered_participating_nodes": covered_nodes,
|
||
"line_count": line_count,
|
||
"source_node_count": source_node_count,
|
||
"missing_participating_nodes": missing_nodes,
|
||
"last_at": last_at,
|
||
"preview_lines": preview_lines,
|
||
},
|
||
}
|
||
|
||
if http_status < 200 or http_status >= 300:
|
||
result["warning"] = "post-check 未能成功读取 /api/v1/ops/overview,请确认当前 API 版本是否已包含 ops overview。"
|
||
|
||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||
PY
|
||
}
|
||
|
||
run_log_sync_recover_flow() {
|
||
local mainland_base_url="$1"
|
||
local desired_mode_raw="$2"
|
||
local confirm_flag="$3"
|
||
local requested_by="$4"
|
||
local desired_mode
|
||
desired_mode="$(printf '%s' "${desired_mode_raw:-key}" | tr '[:upper:]' '[:lower:]')"
|
||
if [[ "${desired_mode}" != "full" ]]; then
|
||
desired_mode="key"
|
||
fi
|
||
|
||
local action_code="enable_log_sync_key"
|
||
if [[ "${desired_mode}" == "full" ]]; then
|
||
action_code="enable_log_sync_full"
|
||
fi
|
||
|
||
echo "[1/2] execute log sync recovery action"
|
||
run_driver_action_entry "${mainland_base_url}" "${action_code}" '{}' "${confirm_flag}" "${requested_by}" "cli/log-sync-recover"
|
||
echo
|
||
echo "[2/2] recovery guidance"
|
||
build_log_sync_guidance_json "${mainland_base_url}"
|
||
}
|
||
|
||
build_log_sync_guidance_json() {
|
||
local mainland_base_url="$1"
|
||
local raw_http_response
|
||
raw_http_response="$(request_with_status "GET" "${mainland_base_url}/api/v1/ops/overview")"
|
||
local http_status="${raw_http_response##*$'\n'HTTP_STATUS=}"
|
||
local response_json="${raw_http_response%$'\n'HTTP_STATUS=*}"
|
||
|
||
python3 - <<'PY' "${response_json}" "${http_status}" "${mainland_base_url}"
|
||
import json
|
||
import sys
|
||
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
http_status = int(sys.argv[2] or 0)
|
||
base_url = str(sys.argv[3] or "").strip()
|
||
data = payload.get("data") or {}
|
||
execution_scene = data.get("execution_scene") or {}
|
||
log_sync = execution_scene.get("log_sync") or {}
|
||
participation_summary = execution_scene.get("participation_summary") or {}
|
||
|
||
state = str(log_sync.get("state") or "").strip() or "unknown"
|
||
description = str(log_sync.get("description") or "").strip()
|
||
enabled = bool(log_sync.get("enabled", False))
|
||
mode = str(log_sync.get("mode") or "key").strip() or "key"
|
||
source_nodes = [str(item or "").strip() for item in list(log_sync.get("source_nodes") or []) if str(item or "").strip()]
|
||
missing_nodes = [str(item or "").strip() for item in list(log_sync.get("missing_participating_nodes") or []) if str(item or "").strip()]
|
||
covered_nodes = [str(item or "").strip() for item in list(log_sync.get("covered_participating_nodes") or []) if str(item or "").strip()]
|
||
preview_lines = [str(item or "").strip() for item in list(log_sync.get("preview_lines") or []) if str(item or "").strip()][:5]
|
||
participating_nodes = int(
|
||
log_sync.get("participating_node_count", participation_summary.get("participating_nodes", 0))
|
||
or participation_summary.get("participating_nodes", 0)
|
||
or 0
|
||
)
|
||
covered_count = int(log_sync.get("covered_participating_node_count", len(covered_nodes)) or len(covered_nodes))
|
||
line_count = int(log_sync.get("line_count", 0) or 0)
|
||
|
||
recommended_commands = []
|
||
primary_target = ""
|
||
if source_nodes:
|
||
primary_target = source_nodes[0]
|
||
elif covered_nodes:
|
||
primary_target = covered_nodes[0]
|
||
elif missing_nodes:
|
||
primary_target = missing_nodes[0]
|
||
|
||
if primary_target:
|
||
recommended_commands.append(
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh scene-node-log {base_url} {primary_target} 120 {mode}"
|
||
)
|
||
if missing_nodes:
|
||
recommended_commands.extend(
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh scene-node-log {base_url} {node_code} 120 {mode}"
|
||
for node_code in missing_nodes[:2]
|
||
if node_code != primary_target
|
||
)
|
||
recommended_commands.append(
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh log-sync-check {base_url}"
|
||
)
|
||
recommended_commands.append(
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh doctor {base_url}"
|
||
)
|
||
|
||
next_step = "继续观察"
|
||
if state in {"disabled", "unknown"}:
|
||
next_step = "开关仍未真正生效,先确认当前 API 是否已加载最新 runtime_settings,并复查执行链。"
|
||
elif state in {"waiting_sample", "partial_coverage"}:
|
||
next_step = "开关已经生效,但样本或覆盖还不完整,优先下钻参与节点现场日志。"
|
||
elif state in {"healthy", "full_capture"}:
|
||
next_step = "远端日志回传已进入可用状态,可以继续从海外控制面盯现场。"
|
||
elif state == "idle":
|
||
next_step = "当前没有参与检测节点,日志回传已就绪,等任务进入执行即可。"
|
||
|
||
print(json.dumps({
|
||
"http_status": http_status,
|
||
"code": int(payload.get("code", 0) or 0),
|
||
"message": str(payload.get("message") or ""),
|
||
"status": state,
|
||
"summary": description,
|
||
"next_step": next_step,
|
||
"log_sync": {
|
||
"enabled": enabled,
|
||
"mode": mode,
|
||
"participating_nodes": participating_nodes,
|
||
"covered_participating_nodes": covered_count,
|
||
"line_count": line_count,
|
||
"source_nodes": source_nodes,
|
||
"missing_participating_nodes": missing_nodes,
|
||
"preview_lines": preview_lines,
|
||
},
|
||
"recommended_commands": recommended_commands[:5],
|
||
}, ensure_ascii=False, indent=2))
|
||
PY
|
||
}
|
||
|
||
run_runtime_refresh_recover() {
|
||
local mainland_base_url="$1"
|
||
local audit_output
|
||
audit_output="$(bash "${SCRIPT_DIR}/check_env_drift.sh" "${mainland_base_url}")"
|
||
|
||
echo "[1/2] env audit"
|
||
printf '%s\n' "${audit_output}"
|
||
echo
|
||
echo
|
||
|
||
echo "[2/2] runtime refresh guidance"
|
||
python3 - <<'PY' "${audit_output}" "${mainland_base_url}"
|
||
import json
|
||
import sys
|
||
|
||
raw_text = str(sys.argv[1] or "")
|
||
base_url = str(sys.argv[2] or "").strip()
|
||
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()
|
||
|
||
payload = {}
|
||
if candidate_text:
|
||
try:
|
||
payload = json.loads(candidate_text)
|
||
except Exception:
|
||
payload = {}
|
||
|
||
runtime = dict(payload.get("runtime") or {})
|
||
recommended_actions = [
|
||
str(item).strip()
|
||
for item in list(payload.get("recommended_actions") or [])
|
||
if str(item).strip()
|
||
]
|
||
|
||
recommended_commands = []
|
||
for candidate in recommended_actions:
|
||
if candidate not in recommended_commands:
|
||
recommended_commands.append(candidate)
|
||
|
||
for candidate in [
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh stack-diagnosis {base_url} summary",
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh stack-next {base_url}",
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh doctor-decision {base_url} -",
|
||
]:
|
||
normalized_candidate = str(candidate or "").strip()
|
||
if normalized_candidate and normalized_candidate not in recommended_commands:
|
||
recommended_commands.append(normalized_candidate)
|
||
|
||
status = str(payload.get("status") or "").strip() or "unknown"
|
||
headline = str(payload.get("headline") or "").strip()
|
||
runtime_may_need_restart = bool(runtime.get("runtime_may_need_restart", False))
|
||
repo_capability_drift = bool(runtime.get("repo_capability_drift", False))
|
||
repo_commit_drift = bool(runtime.get("repo_commit_drift", False))
|
||
|
||
summary = "当前没有识别到明显的运行时版本漂移。"
|
||
if runtime_may_need_restart:
|
||
summary = "已经识别到运行时版本漂移,先让 domaincheck-api 切到最新代码,再继续节点接管与上线复核。"
|
||
elif status == "blocked":
|
||
summary = "当前环境本身仍有阻断项,先修环境,再谈运行时刷新。"
|
||
elif status == "attention":
|
||
summary = "当前环境存在注意项,但不一定都是运行时版本问题。"
|
||
|
||
print(json.dumps({
|
||
"status": status,
|
||
"headline": headline,
|
||
"summary": summary,
|
||
"runtime_refresh": {
|
||
"runtime_may_need_restart": runtime_may_need_restart,
|
||
"repo_capability_drift": repo_capability_drift,
|
||
"repo_commit_drift": repo_commit_drift,
|
||
"build_commit_sha": str(runtime.get("build_commit_sha") or "").strip(),
|
||
"build_commit_ref": str(runtime.get("build_commit_ref") or "").strip(),
|
||
"route_surface_declares_bootstrap_plan": bool(runtime.get("route_surface_declares_bootstrap_plan", False)),
|
||
"route_surface_missing_keys": [
|
||
str(item).strip()
|
||
for item in list(runtime.get("route_surface_missing_keys") or [])
|
||
if str(item).strip()
|
||
],
|
||
},
|
||
"recommended_commands": recommended_commands[:6],
|
||
}, ensure_ascii=False, indent=2))
|
||
PY
|
||
}
|
||
|
||
run_go_live_recover() {
|
||
local mainland_base_url="$1"
|
||
local overseas_base_url="$2"
|
||
|
||
echo "[1/5] runtime refresh recover"
|
||
run_runtime_refresh_recover "${mainland_base_url}"
|
||
echo
|
||
echo
|
||
|
||
echo "[2/5] stack check summary"
|
||
bash "${SCRIPT_DIR}/check_ops_center_stack.sh" "${mainland_base_url}" "summary"
|
||
echo
|
||
echo
|
||
|
||
echo "[3/5] stack next preview"
|
||
preview_stack_next_entry "${mainland_base_url}" "$(ops_center_resolve_requested_by)"
|
||
echo
|
||
echo
|
||
|
||
echo "[4/5] go live check summary"
|
||
bash "${SCRIPT_DIR}/check_go_live.sh" "${mainland_base_url}" "${overseas_base_url}" "summary"
|
||
echo
|
||
echo
|
||
|
||
echo "[5/5] doctor decision"
|
||
run_doctor_decision "${mainland_base_url}" "${overseas_base_url}"
|
||
}
|
||
|
||
show_agent_gap_check() {
|
||
local mainland_base_url="$1"
|
||
local raw_http_response
|
||
raw_http_response="$(request_with_status "GET" "${mainland_base_url}/api/v1/ops/stack-diagnosis?base_url=${mainland_base_url}")"
|
||
local http_status="${raw_http_response##*$'\n'HTTP_STATUS=}"
|
||
local response_json="${raw_http_response%$'\n'HTTP_STATUS=*}"
|
||
|
||
python3 - <<'PY' "${response_json}" "${http_status}" "${mainland_base_url}"
|
||
import json
|
||
import sys
|
||
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
http_status = int(sys.argv[2] or 0)
|
||
base_url = str(sys.argv[3] or "").strip()
|
||
data = payload.get("data") or {}
|
||
managed_nodes = data.get("managed_nodes") or {}
|
||
problem_nodes = [dict(item or {}) for item in list(managed_nodes.get("problem_nodes") or []) if isinstance(item, dict)]
|
||
first_gap = dict(managed_nodes.get("first_handover_gap") or {})
|
||
managed_enabled = int(managed_nodes.get("managed_enabled", 0) or 0)
|
||
remote_access_ready = int(managed_nodes.get("remote_access_ready", 0) or 0)
|
||
first_node_code = str(
|
||
first_gap.get("node_code")
|
||
or managed_nodes.get("first_handover_gap_node_code")
|
||
or ((problem_nodes[0] or {}).get("node_code") if problem_nodes else "")
|
||
or ""
|
||
).strip()
|
||
summary = "当前没有启用托管节点。"
|
||
if problem_nodes and managed_enabled > 0 and remote_access_ready <= 0:
|
||
summary = "当前已有托管节点,但 remote-agent 仍未就绪,优先补齐节点接管链路。"
|
||
elif problem_nodes and managed_enabled > 0 and remote_access_ready < managed_enabled:
|
||
summary = (
|
||
f"当前仍有 {len(problem_nodes)} 台托管节点未进入 remote-agent 就绪状态,"
|
||
"优先补齐剩余节点接管。"
|
||
)
|
||
elif managed_enabled > 0:
|
||
summary = "当前托管节点均已进入 remote-agent 就绪状态。"
|
||
|
||
recommended_commands = []
|
||
if first_node_code:
|
||
recommended_commands.extend([
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh node-recover {base_url} {first_node_code}",
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh node-onboarding {base_url} {first_node_code}",
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh node-handover {base_url} {first_node_code}",
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh node-bootstrap-plan {base_url} {first_node_code}",
|
||
])
|
||
recommended_commands.extend([
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh agent-gap-export /tmp/domaincheck-agent-gap {base_url}",
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh stack-first-gap-handover {base_url}",
|
||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh doctor {base_url}",
|
||
])
|
||
|
||
print(json.dumps({
|
||
"http_status": http_status,
|
||
"code": int(payload.get("code", 0) or 0),
|
||
"message": str(payload.get("message") or ""),
|
||
"summary": summary,
|
||
"managed_nodes": {
|
||
"managed_enabled": managed_enabled,
|
||
"remote_access_ready": remote_access_ready,
|
||
"first_gap_node_code": first_node_code,
|
||
"problem_nodes": [
|
||
{
|
||
"node_code": str(item.get("node_code") or "").strip(),
|
||
"agent_state": str(item.get("agent_state") or "").strip(),
|
||
"remote_access_state": str(item.get("remote_access_state") or "").strip(),
|
||
"delivery_queue_state": str(item.get("delivery_queue_state") or "").strip(),
|
||
"participation_state": str(item.get("participation_state") or "").strip(),
|
||
}
|
||
for item in problem_nodes[:5]
|
||
],
|
||
},
|
||
"recommended_commands": recommended_commands[:5],
|
||
}, ensure_ascii=False, indent=2))
|
||
PY
|
||
}
|
||
|
||
run_agent_gap_recover_flow() {
|
||
local mainland_base_url="$1"
|
||
local confirm_flag="$2"
|
||
local requested_by="$3"
|
||
local normalized_confirm
|
||
normalized_confirm="$(confirm_to_bool "${confirm_flag}")"
|
||
|
||
echo "[1/4] stack first gap handover"
|
||
show_stack_first_gap_handover "${mainland_base_url}"
|
||
echo
|
||
echo "[2/4] first node bootstrap plan"
|
||
local first_gap_node_code
|
||
first_gap_node_code="$(python3 - <<'PY' "${mainland_base_url}" "${OPS_CENTER_CURL_CONNECT_TIMEOUT}" "${OPS_CENTER_CURL_MAX_TIME}"
|
||
import json
|
||
import subprocess
|
||
import sys
|
||
|
||
base_url = str(sys.argv[1] or "").strip()
|
||
connect_timeout = str(sys.argv[2] or "3").strip()
|
||
max_time = str(sys.argv[3] or "10").strip()
|
||
proc = subprocess.run(
|
||
[
|
||
"curl",
|
||
"-sS",
|
||
"--connect-timeout",
|
||
connect_timeout,
|
||
"--max-time",
|
||
max_time,
|
||
f"{base_url}/api/v1/ops/stack-diagnosis?base_url={base_url}",
|
||
],
|
||
capture_output=True,
|
||
text=True,
|
||
check=False,
|
||
)
|
||
raw = (proc.stdout or "").strip()
|
||
try:
|
||
payload = json.loads(raw)
|
||
except Exception:
|
||
print("")
|
||
raise SystemExit(0)
|
||
data = payload.get("data") or {}
|
||
managed_nodes = data.get("managed_nodes") or {}
|
||
problem_nodes = [dict(item or {}) for item in list(managed_nodes.get("problem_nodes") or []) if isinstance(item, dict)]
|
||
first_gap = dict(managed_nodes.get("first_handover_gap") or {})
|
||
node_code = str(
|
||
first_gap.get("node_code")
|
||
or managed_nodes.get("first_handover_gap_node_code")
|
||
or ((problem_nodes[0] or {}).get("node_code") if problem_nodes else "")
|
||
or ""
|
||
).strip()
|
||
print(node_code)
|
||
PY
|
||
)"
|
||
if [[ -n "${first_gap_node_code}" ]]; then
|
||
build_node_handover_bootstrap_plan \
|
||
"${mainland_base_url}" \
|
||
"${first_gap_node_code}" \
|
||
"${requested_by}" \
|
||
"72" \
|
||
"${mainland_base_url}" \
|
||
"/opt/domaincheck"
|
||
else
|
||
echo '{"message":"当前没有可生成 bootstrap-plan 的缺口节点。"}'
|
||
fi
|
||
echo
|
||
echo "[3/4] node onboarding view"
|
||
if [[ -n "${first_gap_node_code}" ]]; then
|
||
show_node_onboarding "${mainland_base_url}" "${first_gap_node_code}"
|
||
else
|
||
echo '{"message":"当前没有可查看 onboarding 的缺口节点。"}'
|
||
fi
|
||
echo
|
||
echo "[4/4] first node recovery decision"
|
||
if [[ -n "${first_gap_node_code}" ]]; then
|
||
bash "${SCRIPT_DIR}/drive_ops_center.sh" node-recover "${mainland_base_url}" "${first_gap_node_code}" "" "${requested_by}"
|
||
echo
|
||
echo "[follow-up] gap recovery guidance"
|
||
show_agent_gap_check "${mainland_base_url}"
|
||
else
|
||
echo '{"message":"当前没有可生成恢复决策的缺口节点。"}'
|
||
echo
|
||
echo "[follow-up] gap recovery guidance"
|
||
show_agent_gap_check "${mainland_base_url}"
|
||
fi
|
||
if [[ "${normalized_confirm}" == "true" ]]; then
|
||
echo
|
||
echo '{"confirm":"true","note":"当前 agent-gap-recover 仍是结构化恢复流,不会直接 SSH 落地远端 bootstrap。"}'
|
||
fi
|
||
}
|
||
|
||
export_agent_gap_bundle() {
|
||
local report_dir="$1"
|
||
local mainland_base_url="$2"
|
||
local requested_by="$3"
|
||
ops_center_ensure_report_dir "${report_dir}"
|
||
|
||
local stack_json_path="${report_dir}/00_stack_first_gap_handover.json"
|
||
local check_json_path="${report_dir}/01_agent_gap_check.json"
|
||
local onboarding_json_path="${report_dir}/02_node_onboarding.json"
|
||
local bootstrap_plan_txt_path="${report_dir}/03_node_bootstrap_plan.txt"
|
||
local bootstrap_preview_json_path="${report_dir}/04_node_bootstrap_preview.json"
|
||
local acceptance_plan_json_path="${report_dir}/05_node_acceptance_plan.json"
|
||
local manifest_path="${report_dir}/manifest.json"
|
||
local nodes_dir="${report_dir}/nodes"
|
||
|
||
bash "${SCRIPT_DIR}/drive_ops_center.sh" stack-first-gap-handover "${mainland_base_url}" >"${stack_json_path}"
|
||
bash "${SCRIPT_DIR}/drive_ops_center.sh" agent-gap-check "${mainland_base_url}" >"${check_json_path}"
|
||
|
||
local first_gap_node_code
|
||
local gap_nodes_json
|
||
gap_nodes_json="$(python3 - <<'PY' "${check_json_path}"
|
||
import json
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
text = Path(sys.argv[1]).read_text(encoding="utf-8")
|
||
decoder = json.JSONDecoder()
|
||
best = None
|
||
for index, ch in enumerate(text):
|
||
if ch not in "{[":
|
||
continue
|
||
try:
|
||
value, end = decoder.raw_decode(text[index:])
|
||
except Exception:
|
||
continue
|
||
absolute_end = index + end
|
||
if best is None or absolute_end >= best[0]:
|
||
best = (absolute_end, value)
|
||
payload = best[1] if best else {}
|
||
managed = payload.get("managed_nodes") or {}
|
||
problem_nodes = [dict(item or {}) for item in list(managed.get("problem_nodes") or []) if isinstance(item, dict)]
|
||
first_gap_node_code = str(managed.get("first_gap_node_code") or "").strip()
|
||
node_codes = []
|
||
for item in problem_nodes:
|
||
node_code = str(item.get("node_code") or "").strip()
|
||
if node_code and node_code not in node_codes:
|
||
node_codes.append(node_code)
|
||
if first_gap_node_code and first_gap_node_code not in node_codes:
|
||
node_codes.insert(0, first_gap_node_code)
|
||
print(json.dumps({
|
||
"first_gap_node_code": first_gap_node_code,
|
||
"gap_node_codes": node_codes,
|
||
}, ensure_ascii=False))
|
||
PY
|
||
)"
|
||
first_gap_node_code="$(python3 - <<'PY' "${gap_nodes_json}"
|
||
import json
|
||
import sys
|
||
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
print(str(payload.get("first_gap_node_code") or "").strip())
|
||
PY
|
||
)"
|
||
mapfile -t gap_node_codes < <(python3 - <<'PY' "${gap_nodes_json}"
|
||
import json
|
||
import sys
|
||
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
for item in list(payload.get("gap_node_codes") or []):
|
||
node_code = str(item or "").strip()
|
||
if node_code:
|
||
print(node_code)
|
||
PY
|
||
)
|
||
|
||
if [[ -n "${first_gap_node_code}" ]]; then
|
||
bash "${SCRIPT_DIR}/drive_ops_center.sh" node-onboarding "${mainland_base_url}" "${first_gap_node_code}" >"${onboarding_json_path}"
|
||
bash "${SCRIPT_DIR}/drive_ops_center.sh" node-bootstrap-plan "${mainland_base_url}" "${first_gap_node_code}" "${requested_by}" "72" "${mainland_base_url}" "/opt/domaincheck" >"${bootstrap_plan_txt_path}"
|
||
bash "${SCRIPT_DIR}/drive_ops_center.sh" node-bootstrap-preview "${mainland_base_url}" "${first_gap_node_code}" "${requested_by}" >"${bootstrap_preview_json_path}"
|
||
bash "${SCRIPT_DIR}/drive_ops_center.sh" node-acceptance-plan "${mainland_base_url}" "${first_gap_node_code}" "${requested_by}" >"${acceptance_plan_json_path}" || true
|
||
fi
|
||
|
||
mkdir -p "${nodes_dir}"
|
||
local node_code safe_node_code node_dir
|
||
for node_code in "${gap_node_codes[@]}"; do
|
||
[[ -n "${node_code}" ]] || continue
|
||
safe_node_code="$(printf '%s' "${node_code}" | tr '/:' '__')"
|
||
safe_node_code="${safe_node_code// /_}"
|
||
node_dir="${nodes_dir}/${safe_node_code}"
|
||
mkdir -p "${node_dir}"
|
||
bash "${SCRIPT_DIR}/drive_ops_center.sh" node-handover "${mainland_base_url}" "${node_code}" >"${node_dir}/00_node_handover.json"
|
||
bash "${SCRIPT_DIR}/drive_ops_center.sh" node-onboarding "${mainland_base_url}" "${node_code}" >"${node_dir}/01_node_onboarding.json"
|
||
bash "${SCRIPT_DIR}/drive_ops_center.sh" node-bootstrap-plan "${mainland_base_url}" "${node_code}" "${requested_by}" "72" "${mainland_base_url}" "/opt/domaincheck" >"${node_dir}/02_node_bootstrap_plan.txt"
|
||
bash "${SCRIPT_DIR}/drive_ops_center.sh" node-bootstrap-preview "${mainland_base_url}" "${node_code}" "${requested_by}" >"${node_dir}/03_node_bootstrap_preview.json"
|
||
bash "${SCRIPT_DIR}/drive_ops_center.sh" node-acceptance-plan "${mainland_base_url}" "${node_code}" "${requested_by}" >"${node_dir}/04_node_acceptance_plan.json" || true
|
||
done
|
||
|
||
python3 - <<'PY' \
|
||
"${manifest_path}" \
|
||
"${report_dir}" \
|
||
"${mainland_base_url}" \
|
||
"${requested_by}" \
|
||
"${first_gap_node_code}" \
|
||
"${gap_nodes_json}" \
|
||
"${nodes_dir}" \
|
||
"${stack_json_path}" \
|
||
"${check_json_path}" \
|
||
"${onboarding_json_path}" \
|
||
"${bootstrap_plan_txt_path}" \
|
||
"${bootstrap_preview_json_path}" \
|
||
"${acceptance_plan_json_path}"
|
||
import json
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
manifest_path = Path(sys.argv[1])
|
||
report_dir = Path(sys.argv[2])
|
||
base_url = sys.argv[3]
|
||
requested_by = sys.argv[4]
|
||
first_gap_node_code = sys.argv[5]
|
||
gap_nodes_payload = json.loads(str(sys.argv[6] or "{}"))
|
||
nodes_dir = Path(sys.argv[7])
|
||
paths = {
|
||
"stack_first_gap_handover": Path(sys.argv[8]),
|
||
"agent_gap_check": Path(sys.argv[9]),
|
||
"node_onboarding": Path(sys.argv[10]),
|
||
"node_bootstrap_plan": Path(sys.argv[11]),
|
||
"node_bootstrap_preview": Path(sys.argv[12]),
|
||
"node_acceptance_plan": Path(sys.argv[13]),
|
||
}
|
||
|
||
artifacts = []
|
||
for key, path in paths.items():
|
||
artifacts.append({
|
||
"key": key,
|
||
"path": str(path),
|
||
"exists": path.exists(),
|
||
"size": path.stat().st_size if path.exists() else 0,
|
||
})
|
||
|
||
node_bundles = []
|
||
for node_code in list(gap_nodes_payload.get("gap_node_codes") or []):
|
||
normalized_node_code = str(node_code or "").strip()
|
||
if not normalized_node_code:
|
||
continue
|
||
normalized_dir_name = normalized_node_code.replace("/", "_").replace(":", "_").replace(" ", "_")
|
||
node_dir = nodes_dir / normalized_dir_name
|
||
bundle_paths = {
|
||
"node_handover": node_dir / "00_node_handover.json",
|
||
"node_onboarding": node_dir / "01_node_onboarding.json",
|
||
"node_bootstrap_plan": node_dir / "02_node_bootstrap_plan.txt",
|
||
"node_bootstrap_preview": node_dir / "03_node_bootstrap_preview.json",
|
||
"node_acceptance_plan": node_dir / "04_node_acceptance_plan.json",
|
||
}
|
||
node_bundles.append({
|
||
"node_code": normalized_node_code,
|
||
"bundle_dir": str(node_dir),
|
||
"artifacts": [
|
||
{
|
||
"key": key,
|
||
"path": str(path),
|
||
"exists": path.exists(),
|
||
"size": path.stat().st_size if path.exists() else 0,
|
||
}
|
||
for key, path in bundle_paths.items()
|
||
],
|
||
})
|
||
|
||
payload = {
|
||
"report_dir": str(report_dir),
|
||
"mainland_base_url": base_url,
|
||
"requested_by": requested_by,
|
||
"first_gap_node_code": first_gap_node_code,
|
||
"gap_node_codes": list(gap_nodes_payload.get("gap_node_codes") or []),
|
||
"gap_node_total": len(list(gap_nodes_payload.get("gap_node_codes") or [])),
|
||
"artifacts": artifacts,
|
||
"node_bundles": node_bundles,
|
||
"summary": {
|
||
"ok": bool(node_bundles),
|
||
"headline": (
|
||
f"已为 {len(node_bundles)} 台接管缺口节点导出接管材料;首个缺口节点为 {first_gap_node_code}。"
|
||
if node_bundles and first_gap_node_code
|
||
else (
|
||
f"已为 {len(node_bundles)} 台接管缺口节点导出接管材料。"
|
||
if node_bundles
|
||
else "当前没有可导出的接管缺口节点。"
|
||
)
|
||
),
|
||
},
|
||
}
|
||
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
|
||
}
|
||
|
||
run_resolved_runbook_sequence() {
|
||
local mainland_base_url="$1"
|
||
local sequence_key="$2"
|
||
local secondary_flag="$3"
|
||
local action_payload_json="$4"
|
||
local confirm_flag="$5"
|
||
local requested_by="$6"
|
||
local source_label="$7"
|
||
local confirm_value
|
||
confirm_value="$(confirm_to_bool "${confirm_flag}")"
|
||
|
||
action_payload_json="$(normalize_payload_json_input "${action_payload_json}")"
|
||
|
||
local request_json
|
||
request_json="$(python3 - <<'PY' "${sequence_key}" "${secondary_flag}" "${action_payload_json}" "${requested_by}" "${source_label}" "${confirm_value}"
|
||
import json
|
||
import sys
|
||
|
||
sequence_key = str(sys.argv[1] or "").strip()
|
||
secondary_flag = str(sys.argv[2] or "primary").strip().lower()
|
||
payload_raw = str(sys.argv[3] or "").strip() or "{}"
|
||
requested_by = str(sys.argv[4] or "").strip() or "cli/ops-center/runbook-run"
|
||
source_label = str(sys.argv[5] or "").strip() or "cli/runbook-run"
|
||
confirm = str(sys.argv[6] or "").strip().lower() == "true"
|
||
|
||
try:
|
||
action_payload = json.loads(payload_raw)
|
||
except Exception as exc:
|
||
raise SystemExit(f"invalid action payload json: {exc}")
|
||
|
||
if not isinstance(action_payload, dict):
|
||
raise SystemExit("action payload must be a json object")
|
||
|
||
secondary = secondary_flag in {"secondary", "second", "2", "true", "yes", "y"}
|
||
|
||
print(json.dumps({
|
||
"source_label": source_label,
|
||
"title": sequence_key,
|
||
"executor_kind": "runbook_sequence",
|
||
"sequence_key": sequence_key,
|
||
"include_secondary": True,
|
||
"requested_by": requested_by,
|
||
"secondary": secondary,
|
||
"confirm": confirm,
|
||
"primary_action_payload": action_payload,
|
||
"secondary_action_payload": action_payload,
|
||
}, ensure_ascii=False))
|
||
PY
|
||
)"
|
||
|
||
echo "[1/3] request"
|
||
print_json_or_raw "${request_json}"
|
||
echo
|
||
echo
|
||
|
||
local raw_http_response
|
||
raw_http_response="$(request_with_status "POST" "${mainland_base_url}/api/v1/ops/driver-actions/execute-resolved" "${request_json}")"
|
||
local http_status="${raw_http_response##*$'\n'HTTP_STATUS=}"
|
||
local response_json="${raw_http_response%$'\n'HTTP_STATUS=*}"
|
||
|
||
echo "[2/3] raw response"
|
||
printf '%s\n' "http_status=${http_status}"
|
||
print_json_or_raw "${response_json}"
|
||
echo
|
||
echo
|
||
|
||
echo "[3/3] execution result"
|
||
python3 - <<'PY' "${response_json}" "${http_status}"
|
||
import json
|
||
import sys
|
||
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
http_status = int(sys.argv[2] or 0)
|
||
data = payload.get("data") or {}
|
||
gate = data.get("gate") or {}
|
||
execution_result = data.get("execution_result") or {}
|
||
|
||
raw_code = payload.get("code", 1)
|
||
if raw_code in (0, "0", False):
|
||
normalized_code = 0
|
||
else:
|
||
normalized_code = int(raw_code or 1)
|
||
|
||
print(json.dumps({
|
||
"http_status": http_status,
|
||
"code": normalized_code,
|
||
"message": str(payload.get("message") or ""),
|
||
"executed": bool(data.get("executed", False)),
|
||
"selected_section": str(data.get("selected_section") or ""),
|
||
"decision": str(gate.get("decision") or ""),
|
||
"decision_label": str(gate.get("decision_label") or ""),
|
||
"recommendation": str(gate.get("recommendation") or ""),
|
||
"sequence_key": str(gate.get("sequence_key") or execution_result.get("sequence_key") or ""),
|
||
"action_code": str(gate.get("action_code") or execution_result.get("action_code") or execution_result.get("driver_action_code") or ""),
|
||
"handled": bool(execution_result.get("handled", False)),
|
||
"mode": str(execution_result.get("mode") or ""),
|
||
}, ensure_ascii=False, indent=2))
|
||
PY
|
||
|
||
if [[ "${http_status}" -lt 200 || "${http_status}" -ge 300 ]]; then
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
case "${COMMAND}" in
|
||
help|-h|--help)
|
||
usage
|
||
;;
|
||
config)
|
||
print_config "${1:-${OPS_CENTER_ACTIVE_PROFILE}}"
|
||
;;
|
||
contracts)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
CONTRACT_KEY="${2:-}"
|
||
if [[ "${MAINLAND_BASE_URL}" != http://* && "${MAINLAND_BASE_URL}" != https://* ]]; then
|
||
CONTRACT_KEY="${MAINLAND_BASE_URL}"
|
||
MAINLAND_BASE_URL="$(ops_center_resolve_mainland_api_base_url)"
|
||
fi
|
||
show_ops_contracts "${MAINLAND_BASE_URL}" "${CONTRACT_KEY}"
|
||
;;
|
||
env-audit)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
exec bash "${SCRIPT_DIR}/check_env_drift.sh" "${MAINLAND_BASE_URL}"
|
||
;;
|
||
runtime-refresh-check)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
exec bash "${SCRIPT_DIR}/check_env_drift.sh" "${MAINLAND_BASE_URL}"
|
||
;;
|
||
runtime-refresh-recover)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
run_runtime_refresh_recover "${MAINLAND_BASE_URL}"
|
||
;;
|
||
stack-diagnosis|stack-check)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
VIEW_MODE="${2:-summary}"
|
||
if [[ "${MAINLAND_BASE_URL}" != http://* && "${MAINLAND_BASE_URL}" != https://* ]]; then
|
||
VIEW_MODE="${MAINLAND_BASE_URL}"
|
||
MAINLAND_BASE_URL="$(ops_center_resolve_mainland_api_base_url)"
|
||
fi
|
||
exec bash "${SCRIPT_DIR}/check_ops_center_stack.sh" "${MAINLAND_BASE_URL}" "${VIEW_MODE}"
|
||
;;
|
||
go-live-check)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
OVERSEAS_BASE_URL="${2:-$(ops_center_resolve_overseas_api_base_url)}"
|
||
VIEW_MODE="${3:-summary}"
|
||
if [[ "${MAINLAND_BASE_URL}" != http://* && "${MAINLAND_BASE_URL}" != https://* ]]; then
|
||
VIEW_MODE="${OVERSEAS_BASE_URL:-summary}"
|
||
OVERSEAS_BASE_URL="${MAINLAND_BASE_URL}"
|
||
MAINLAND_BASE_URL="$(ops_center_resolve_mainland_api_base_url)"
|
||
fi
|
||
exec bash "${SCRIPT_DIR}/check_go_live.sh" "${MAINLAND_BASE_URL}" "${OVERSEAS_BASE_URL}" "${VIEW_MODE}"
|
||
;;
|
||
go-live-recover)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
OVERSEAS_BASE_URL="$(normalize_optional_base_url_arg "${2:-}" "$(ops_center_resolve_overseas_api_base_url)")"
|
||
if [[ "${MAINLAND_BASE_URL}" != http://* && "${MAINLAND_BASE_URL}" != https://* ]]; then
|
||
OVERSEAS_BASE_URL="$(normalize_optional_base_url_arg "${MAINLAND_BASE_URL}" "$(ops_center_resolve_overseas_api_base_url)")"
|
||
MAINLAND_BASE_URL="$(ops_center_resolve_mainland_api_base_url)"
|
||
fi
|
||
run_go_live_recover "${MAINLAND_BASE_URL}" "${OVERSEAS_BASE_URL}"
|
||
;;
|
||
go-live-export)
|
||
REPORT_DIR="${1:-${OPS_CENTER_REPORT_ROOT}/go-live-bundles/go-live-bundle-$(timestamp_now)}"
|
||
MAINLAND_BASE_URL="${2:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
OVERSEAS_BASE_URL="$(normalize_optional_base_url_arg "${3:-}" "$(ops_center_resolve_overseas_api_base_url)")"
|
||
if [[ "${REPORT_DIR}" == http://* || "${REPORT_DIR}" == https://* ]]; then
|
||
OVERSEAS_BASE_URL="$(normalize_optional_base_url_arg "${MAINLAND_BASE_URL:-}" "$(ops_center_resolve_overseas_api_base_url)")"
|
||
MAINLAND_BASE_URL="${REPORT_DIR}"
|
||
REPORT_DIR="${OPS_CENTER_REPORT_ROOT}/go-live-bundles/go-live-bundle-$(timestamp_now)}"
|
||
fi
|
||
exec bash "${SCRIPT_DIR}/export_go_live_bundle.sh" "${REPORT_DIR}" "${MAINLAND_BASE_URL}" "${OVERSEAS_BASE_URL}"
|
||
;;
|
||
go-live-review)
|
||
TARGET_PATH="${1:-${OPS_CENTER_REPORT_ROOT}/go-live-bundles}"
|
||
if [[ -d "${TARGET_PATH}" && ! -f "${TARGET_PATH}/manifest.json" ]]; then
|
||
TARGET_PATH="$(find "${TARGET_PATH}" -mindepth 1 -maxdepth 1 -type d | sort | tail -n 1)"
|
||
fi
|
||
exec bash "${SCRIPT_DIR}/review_go_live_bundle.sh" "${TARGET_PATH}"
|
||
;;
|
||
go-live-signoff)
|
||
FIRST_ARG="${1:-}"
|
||
SECOND_ARG="${2:-}"
|
||
if [[ -n "${FIRST_ARG}" && -d "${FIRST_ARG}" && ! -f "${FIRST_ARG}/manifest.json" ]]; then
|
||
FIRST_ARG="$(find "${FIRST_ARG}" -mindepth 1 -maxdepth 1 -type d | sort | tail -n 1)"
|
||
fi
|
||
exec bash "${SCRIPT_DIR}/check_go_live_signoff.sh" "${FIRST_ARG}" "${SECOND_ARG}"
|
||
;;
|
||
stack-next)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
STACK_MODE="${2:-preview}"
|
||
CONFIRM_FLAG="${3:-}"
|
||
REQUESTED_BY="${4:-$(ops_center_resolve_requested_by)}"
|
||
if [[ "${MAINLAND_BASE_URL}" != http://* && "${MAINLAND_BASE_URL}" != https://* ]]; then
|
||
REQUESTED_BY="${CONFIRM_FLAG:-$(ops_center_resolve_requested_by)}"
|
||
CONFIRM_FLAG="${STACK_MODE:-}"
|
||
STACK_MODE="${MAINLAND_BASE_URL}"
|
||
MAINLAND_BASE_URL="$(ops_center_resolve_mainland_api_base_url)"
|
||
fi
|
||
if is_confirm_token "${STACK_MODE}" && [[ -z "${CONFIRM_FLAG}" ]]; then
|
||
CONFIRM_FLAG="${STACK_MODE}"
|
||
STACK_MODE="run"
|
||
fi
|
||
case "$(printf '%s' "${STACK_MODE}" | tr '[:upper:]' '[:lower:]')" in
|
||
preview|show|resolve|"")
|
||
preview_stack_next_entry "${MAINLAND_BASE_URL}" "${REQUESTED_BY}"
|
||
;;
|
||
run|execute)
|
||
run_stack_next_entry "${MAINLAND_BASE_URL}" "${CONFIRM_FLAG}" "${REQUESTED_BY}"
|
||
;;
|
||
*)
|
||
echo "stack-next mode must be preview or run" >&2
|
||
exit 1
|
||
;;
|
||
esac
|
||
;;
|
||
release-package)
|
||
REPO_ROOT="$(ops_center_repo_root)"
|
||
SKIP_SMOKE="$(confirm_to_bool "${1:-}")"
|
||
if [[ "${SKIP_SMOKE}" == "true" ]]; then
|
||
DOMAINCHECK_SKIP_SMOKE_TEST=1 exec bash "${REPO_ROOT}/package_domain_release.sh"
|
||
fi
|
||
exec bash "${REPO_ROOT}/package_domain_release.sh"
|
||
;;
|
||
release-verify)
|
||
REPO_ROOT="$(ops_center_repo_root)"
|
||
exec bash "${REPO_ROOT}/verify_domain_release.sh" "${1:-}" "${2:-}"
|
||
;;
|
||
release-prepare)
|
||
REPO_ROOT="$(ops_center_repo_root)"
|
||
exec bash "${REPO_ROOT}/prepare_final_release.sh"
|
||
;;
|
||
release-show)
|
||
REPO_ROOT="$(ops_center_repo_root)"
|
||
exec bash "${REPO_ROOT}/show_latest_release.sh"
|
||
;;
|
||
release-launchpad)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
CHANNEL="${2:-stable}"
|
||
exec bash "${SCRIPT_DIR}/drive_release_hub.sh" "${MAINLAND_BASE_URL}" launchpad "${CHANNEL}"
|
||
;;
|
||
release-preview)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
exec bash "${SCRIPT_DIR}/drive_release_hub.sh" "${MAINLAND_BASE_URL}" preview-latest
|
||
;;
|
||
release-preview-smart)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
MODE="${2:-}"
|
||
if [[ -z "${MODE}" && ( "${MAINLAND_BASE_URL}" == "worker" || "${MAINLAND_BASE_URL}" == "control" ) ]]; then
|
||
MODE="${MAINLAND_BASE_URL}"
|
||
MAINLAND_BASE_URL="$(ops_center_resolve_mainland_api_base_url)"
|
||
fi
|
||
if [[ "${MODE}" != "worker" && "${MODE}" != "control" ]]; then
|
||
echo "mode must be worker or control" >&2
|
||
usage
|
||
exit 1
|
||
fi
|
||
exec bash "${SCRIPT_DIR}/drive_release_hub.sh" "${MAINLAND_BASE_URL}" preview-latest-smart "${MODE}"
|
||
;;
|
||
release-import-latest)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
exec bash "${SCRIPT_DIR}/drive_release_hub.sh" "${MAINLAND_BASE_URL}" create-latest-release
|
||
;;
|
||
publish-latest)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
MODE="${2:-}"
|
||
CONFIRM_FLAG="${3:-}"
|
||
SKIP_SMOKE_FLAG="${4:-}"
|
||
if [[ -z "${MODE}" && ( "${MAINLAND_BASE_URL}" == "worker" || "${MAINLAND_BASE_URL}" == "control" ) ]]; then
|
||
MODE="${MAINLAND_BASE_URL}"
|
||
MAINLAND_BASE_URL="$(ops_center_resolve_mainland_api_base_url)"
|
||
CONFIRM_FLAG="${2:-}"
|
||
SKIP_SMOKE_FLAG="${3:-}"
|
||
fi
|
||
run_publish_latest "${MAINLAND_BASE_URL}" "${MODE}" "${CONFIRM_FLAG}" "${SKIP_SMOKE_FLAG}"
|
||
;;
|
||
doctor-export)
|
||
REPORT_DIR="${1:-${OPS_CENTER_REPORT_ROOT}/doctor-$(timestamp_now)}"
|
||
MAINLAND_BASE_URL="${2:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
OVERSEAS_BASE_URL="$(normalize_optional_base_url_arg "${3:-}" "$(ops_center_resolve_overseas_api_base_url)")"
|
||
run_doctor_report_export "${REPORT_DIR}" "${MAINLAND_BASE_URL}" "${OVERSEAS_BASE_URL}"
|
||
;;
|
||
doctor-decision)
|
||
FIRST_ARG="${1:-}"
|
||
SECOND_ARG="${2:-}"
|
||
run_doctor_decision "${FIRST_ARG}" "${SECOND_ARG}"
|
||
;;
|
||
doctor)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
OVERSEAS_BASE_URL="$(normalize_optional_base_url_arg "${2:-}" "$(ops_center_resolve_overseas_api_base_url)")"
|
||
run_step "[doctor/0] ops center stack" bash "${SCRIPT_DIR}/check_ops_center_stack.sh" "${MAINLAND_BASE_URL}" summary
|
||
run_step "[doctor/1] ops plane" bash "${SCRIPT_DIR}/check_ops_plane.sh" "${MAINLAND_BASE_URL}"
|
||
run_step "[doctor/2] release hub" bash "${SCRIPT_DIR}/check_release_hub.sh" "${MAINLAND_BASE_URL}"
|
||
run_step "[doctor/3] inspection" bash "${SCRIPT_DIR}/check_ops_inspection.sh" "${MAINLAND_BASE_URL}"
|
||
run_step "[doctor/4] participation" bash "${SCRIPT_DIR}/check_worker_participation.sh" "${MAINLAND_BASE_URL}"
|
||
if [[ -n "${OVERSEAS_BASE_URL}" ]]; then
|
||
run_step "[doctor/5] temp topology" bash "${SCRIPT_DIR}/check_temp_topology.sh" "${MAINLAND_BASE_URL}" "${OVERSEAS_BASE_URL}"
|
||
fi
|
||
;;
|
||
ops-plane)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
exec bash "${SCRIPT_DIR}/check_ops_plane.sh" "${MAINLAND_BASE_URL}"
|
||
;;
|
||
inspection)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
exec bash "${SCRIPT_DIR}/check_ops_inspection.sh" "${MAINLAND_BASE_URL}"
|
||
;;
|
||
participation)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
exec bash "${SCRIPT_DIR}/check_worker_participation.sh" "${MAINLAND_BASE_URL}"
|
||
;;
|
||
topology)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
OVERSEAS_BASE_URL="${2:-$(ops_center_resolve_overseas_api_base_url)}"
|
||
exec bash "${SCRIPT_DIR}/check_temp_topology.sh" "${MAINLAND_BASE_URL}" "${OVERSEAS_BASE_URL}"
|
||
;;
|
||
nodes)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
show_ops_nodes "${MAINLAND_BASE_URL}"
|
||
;;
|
||
stack-first-gap-handover)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
if [[ "${MAINLAND_BASE_URL}" != http://* && "${MAINLAND_BASE_URL}" != https://* ]]; then
|
||
MAINLAND_BASE_URL="$(ops_center_resolve_mainland_api_base_url)"
|
||
fi
|
||
show_stack_first_gap_handover "${MAINLAND_BASE_URL}"
|
||
;;
|
||
node-bind-ssh)
|
||
ARG1="${1:-}"
|
||
ARG2="${2:-}"
|
||
ARG3="${3:-}"
|
||
ARG4="${4:-}"
|
||
ARG5="${5:-}"
|
||
ARG6="${6:-}"
|
||
if [[ -n "${ARG1}" && ( "${ARG1}" == http://* || "${ARG1}" == https://* ) ]]; then
|
||
MAINLAND_BASE_URL="${ARG1}"
|
||
NODE_CODE="${ARG2:-}"
|
||
SSH_HOST="${ARG3:-}"
|
||
SSH_USER="${ARG4:-}"
|
||
SSH_PORT="${ARG5:-22}"
|
||
TITLE="${ARG6:-}"
|
||
else
|
||
MAINLAND_BASE_URL="$(ops_center_resolve_mainland_api_base_url)"
|
||
NODE_CODE="${ARG1:-}"
|
||
SSH_HOST="${ARG2:-}"
|
||
SSH_USER="${ARG3:-}"
|
||
SSH_PORT="${ARG4:-22}"
|
||
TITLE="${ARG5:-}"
|
||
fi
|
||
bind_node_ssh_profile "${MAINLAND_BASE_URL}" "${NODE_CODE}" "${SSH_HOST}" "${SSH_USER}" "${SSH_PORT}" "${TITLE}"
|
||
;;
|
||
node-handover)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
NODE_CODE="${2:-}"
|
||
if [[ "${MAINLAND_BASE_URL}" != http://* && "${MAINLAND_BASE_URL}" != https://* ]]; then
|
||
NODE_CODE="${MAINLAND_BASE_URL}"
|
||
MAINLAND_BASE_URL="$(ops_center_resolve_mainland_api_base_url)"
|
||
fi
|
||
show_node_handover "${MAINLAND_BASE_URL}" "${NODE_CODE}"
|
||
;;
|
||
node-onboarding)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
NODE_CODE="${2:-}"
|
||
if [[ "${MAINLAND_BASE_URL}" != http://* && "${MAINLAND_BASE_URL}" != https://* ]]; then
|
||
NODE_CODE="${MAINLAND_BASE_URL}"
|
||
MAINLAND_BASE_URL="$(ops_center_resolve_mainland_api_base_url)"
|
||
fi
|
||
show_node_onboarding "${MAINLAND_BASE_URL}" "${NODE_CODE}"
|
||
;;
|
||
node-recover)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
NODE_CODE="${2:-}"
|
||
CONFIRM_FLAG="${3:-}"
|
||
REQUESTED_BY="${4:-cli}"
|
||
if [[ "${MAINLAND_BASE_URL}" != http://* && "${MAINLAND_BASE_URL}" != https://* ]]; then
|
||
REQUESTED_BY="${CONFIRM_FLAG:-cli}"
|
||
CONFIRM_FLAG="${NODE_CODE:-}"
|
||
NODE_CODE="${MAINLAND_BASE_URL}"
|
||
MAINLAND_BASE_URL="$(ops_center_resolve_mainland_api_base_url)"
|
||
fi
|
||
recover_node_onboarding "${MAINLAND_BASE_URL}" "${NODE_CODE}" "${CONFIRM_FLAG}" "${REQUESTED_BY}"
|
||
;;
|
||
node-bootstrap-preview)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
NODE_CODE="${2:-}"
|
||
REQUESTED_BY="${3:-cli}"
|
||
if [[ "${MAINLAND_BASE_URL}" != http://* && "${MAINLAND_BASE_URL}" != https://* ]]; then
|
||
REQUESTED_BY="${NODE_CODE:-cli}"
|
||
NODE_CODE="${MAINLAND_BASE_URL}"
|
||
MAINLAND_BASE_URL="$(ops_center_resolve_mainland_api_base_url)"
|
||
fi
|
||
show_node_bootstrap_preview "${MAINLAND_BASE_URL}" "${NODE_CODE}" "${REQUESTED_BY}"
|
||
;;
|
||
node-bootstrap-run)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
NODE_CODE="${2:-}"
|
||
REQUESTED_BY="${3:-cli}"
|
||
if [[ "${MAINLAND_BASE_URL}" != http://* && "${MAINLAND_BASE_URL}" != https://* ]]; then
|
||
REQUESTED_BY="${NODE_CODE:-cli}"
|
||
NODE_CODE="${MAINLAND_BASE_URL}"
|
||
MAINLAND_BASE_URL="$(ops_center_resolve_mainland_api_base_url)"
|
||
fi
|
||
run_node_bootstrap "${MAINLAND_BASE_URL}" "${NODE_CODE}" "${REQUESTED_BY}"
|
||
;;
|
||
node-acceptance-plan)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
NODE_CODE="${2:-}"
|
||
REQUESTED_BY="${3:-cli}"
|
||
if [[ "${MAINLAND_BASE_URL}" != http://* && "${MAINLAND_BASE_URL}" != https://* ]]; then
|
||
REQUESTED_BY="${NODE_CODE:-cli}"
|
||
NODE_CODE="${MAINLAND_BASE_URL}"
|
||
MAINLAND_BASE_URL="$(ops_center_resolve_mainland_api_base_url)"
|
||
fi
|
||
show_node_acceptance_plan "${MAINLAND_BASE_URL}" "${NODE_CODE}" "${REQUESTED_BY}"
|
||
;;
|
||
node-acceptance-run)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
NODE_CODE="${2:-}"
|
||
REQUESTED_BY="${3:-cli}"
|
||
if [[ "${MAINLAND_BASE_URL}" != http://* && "${MAINLAND_BASE_URL}" != https://* ]]; then
|
||
REQUESTED_BY="${NODE_CODE:-cli}"
|
||
NODE_CODE="${MAINLAND_BASE_URL}"
|
||
MAINLAND_BASE_URL="$(ops_center_resolve_mainland_api_base_url)"
|
||
fi
|
||
run_node_acceptance "${MAINLAND_BASE_URL}" "${NODE_CODE}" "${REQUESTED_BY}"
|
||
;;
|
||
scene-node-log)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
NODE_CODE="${2:-}"
|
||
LIMIT="${3:-80}"
|
||
MODE="${4:-}"
|
||
if [[ "${MAINLAND_BASE_URL}" != http://* && "${MAINLAND_BASE_URL}" != https://* ]]; then
|
||
MODE="${LIMIT:-}"
|
||
LIMIT="${NODE_CODE:-80}"
|
||
NODE_CODE="${MAINLAND_BASE_URL}"
|
||
MAINLAND_BASE_URL="$(ops_center_resolve_mainland_api_base_url)"
|
||
fi
|
||
show_node_scene_log "${MAINLAND_BASE_URL}" "${NODE_CODE}" "${LIMIT}" "${MODE}"
|
||
;;
|
||
log-sync-check)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
print_log_sync_post_check "${MAINLAND_BASE_URL}"
|
||
;;
|
||
log-sync-recover)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
MODE_ARG="${2:-key}"
|
||
CONFIRM_FLAG="${3:-}"
|
||
REQUESTED_BY="${4:-$(ops_center_resolve_requested_by)}"
|
||
if [[ "${MAINLAND_BASE_URL}" != http://* && "${MAINLAND_BASE_URL}" != https://* ]]; then
|
||
REQUESTED_BY="${CONFIRM_FLAG:-$(ops_center_resolve_requested_by)}"
|
||
CONFIRM_FLAG="${MODE_ARG:-}"
|
||
MODE_ARG="${MAINLAND_BASE_URL:-key}"
|
||
MAINLAND_BASE_URL="$(ops_center_resolve_mainland_api_base_url)"
|
||
fi
|
||
run_log_sync_recover_flow "${MAINLAND_BASE_URL}" "${MODE_ARG}" "${CONFIRM_FLAG}" "${REQUESTED_BY}"
|
||
;;
|
||
agent-gap-check)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
show_agent_gap_check "${MAINLAND_BASE_URL}"
|
||
;;
|
||
agent-gap-recover)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
CONFIRM_FLAG="${2:-}"
|
||
REQUESTED_BY="${3:-$(ops_center_resolve_requested_by)}"
|
||
if [[ "${MAINLAND_BASE_URL}" != http://* && "${MAINLAND_BASE_URL}" != https://* ]]; then
|
||
REQUESTED_BY="${CONFIRM_FLAG:-$(ops_center_resolve_requested_by)}"
|
||
CONFIRM_FLAG="${MAINLAND_BASE_URL:-}"
|
||
MAINLAND_BASE_URL="$(ops_center_resolve_mainland_api_base_url)"
|
||
fi
|
||
run_agent_gap_recover_flow "${MAINLAND_BASE_URL}" "${CONFIRM_FLAG}" "${REQUESTED_BY}"
|
||
;;
|
||
agent-gap-export)
|
||
ARG1="${1:-}"
|
||
ARG2="${2:-}"
|
||
ARG3="${3:-$(ops_center_resolve_requested_by)}"
|
||
if [[ -n "${ARG1}" && "${ARG1}" != http://* && "${ARG1}" != https://* ]]; then
|
||
REPORT_DIR="${ARG1}"
|
||
MAINLAND_BASE_URL="${ARG2:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
REQUESTED_BY="${ARG3:-$(ops_center_resolve_requested_by)}"
|
||
else
|
||
REPORT_DIR="${OPS_CENTER_REPORT_ROOT}/agent-gap-export-$(timestamp_now)"
|
||
MAINLAND_BASE_URL="${ARG1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
REQUESTED_BY="${ARG2:-$(ops_center_resolve_requested_by)}"
|
||
fi
|
||
export_agent_gap_bundle "${REPORT_DIR}" "${MAINLAND_BASE_URL}" "${REQUESTED_BY}"
|
||
;;
|
||
node-bootstrap-plan)
|
||
ARG1="${1:-}"
|
||
ARG2="${2:-}"
|
||
ARG3="${3:-}"
|
||
ARG4="${4:-}"
|
||
ARG5="${5:-}"
|
||
ARG6="${6:-}"
|
||
if [[ -n "${ARG1}" && ( "${ARG1}" == http://* || "${ARG1}" == https://* ) ]]; then
|
||
MAINLAND_BASE_URL="${ARG1}"
|
||
NODE_CODE="${ARG2:-}"
|
||
ISSUED_BY="${ARG3:-cli/ops-center/node-bootstrap-plan}"
|
||
EXPIRES_IN_HOURS="${ARG4:-72}"
|
||
CONTROL_PLANE_BASE_URL="${ARG5:-${MAINLAND_BASE_URL}}"
|
||
ROOT_DIR="${ARG6:-/opt/domaincheck}"
|
||
else
|
||
MAINLAND_BASE_URL="$(ops_center_resolve_mainland_api_base_url)"
|
||
NODE_CODE="${ARG1:-}"
|
||
ISSUED_BY="${ARG2:-cli/ops-center/node-bootstrap-plan}"
|
||
EXPIRES_IN_HOURS="${ARG3:-72}"
|
||
CONTROL_PLANE_BASE_URL="${ARG4:-${MAINLAND_BASE_URL}}"
|
||
ROOT_DIR="${ARG5:-/opt/domaincheck}"
|
||
fi
|
||
build_node_handover_bootstrap_plan \
|
||
"${MAINLAND_BASE_URL}" \
|
||
"${NODE_CODE}" \
|
||
"${ISSUED_BY}" \
|
||
"${EXPIRES_IN_HOURS}" \
|
||
"${CONTROL_PLANE_BASE_URL}" \
|
||
"${ROOT_DIR}"
|
||
;;
|
||
queue-status)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
NODE_CODE="${2:-}"
|
||
if [[ "${MAINLAND_BASE_URL}" != http://* && "${MAINLAND_BASE_URL}" != https://* ]]; then
|
||
NODE_CODE="${MAINLAND_BASE_URL}"
|
||
MAINLAND_BASE_URL="$(ops_center_resolve_mainland_api_base_url)"
|
||
fi
|
||
show_delivery_queue_status "${MAINLAND_BASE_URL}" "${NODE_CODE}"
|
||
;;
|
||
queue-records)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
NODE_CODE="${2:-}"
|
||
STATE_FILTER="${3:-}"
|
||
if [[ "${MAINLAND_BASE_URL}" != http://* && "${MAINLAND_BASE_URL}" != https://* ]]; then
|
||
STATE_FILTER="${NODE_CODE:-}"
|
||
NODE_CODE="${MAINLAND_BASE_URL}"
|
||
MAINLAND_BASE_URL="$(ops_center_resolve_mainland_api_base_url)"
|
||
fi
|
||
show_delivery_queue_records "${MAINLAND_BASE_URL}" "${NODE_CODE}" "${STATE_FILTER}"
|
||
;;
|
||
queue-flush)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
NODE_CODE="${2:-}"
|
||
LIMIT="${3:-20}"
|
||
REQUESTED_BY="${4:-$(ops_center_resolve_requested_by)}"
|
||
if [[ "${MAINLAND_BASE_URL}" != http://* && "${MAINLAND_BASE_URL}" != https://* ]]; then
|
||
REQUESTED_BY="${LIMIT:-$(ops_center_resolve_requested_by)}"
|
||
LIMIT="${NODE_CODE:-20}"
|
||
NODE_CODE="${MAINLAND_BASE_URL}"
|
||
MAINLAND_BASE_URL="$(ops_center_resolve_mainland_api_base_url)"
|
||
fi
|
||
BODY_JSON="$(python3 - <<'PY' "${LIMIT}" "${REQUESTED_BY}"
|
||
import json
|
||
import sys
|
||
print(json.dumps({
|
||
"limit": max(1, min(int(sys.argv[1] or 20), 200)),
|
||
"requested_by": str(sys.argv[2] or "cli/ops-center/queue-flush"),
|
||
}, ensure_ascii=False))
|
||
PY
|
||
)"
|
||
run_delivery_queue_action "${MAINLAND_BASE_URL}" "${NODE_CODE}" "/delivery-queue/flush" "${BODY_JSON}"
|
||
;;
|
||
queue-replay)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
NODE_CODE="${2:-}"
|
||
LIMIT="${3:-20}"
|
||
REQUESTED_BY="${4:-$(ops_center_resolve_requested_by)}"
|
||
if [[ "${MAINLAND_BASE_URL}" != http://* && "${MAINLAND_BASE_URL}" != https://* ]]; then
|
||
REQUESTED_BY="${LIMIT:-$(ops_center_resolve_requested_by)}"
|
||
LIMIT="${NODE_CODE:-20}"
|
||
NODE_CODE="${MAINLAND_BASE_URL}"
|
||
MAINLAND_BASE_URL="$(ops_center_resolve_mainland_api_base_url)"
|
||
fi
|
||
BODY_JSON="$(python3 - <<'PY' "${LIMIT}" "${REQUESTED_BY}"
|
||
import json
|
||
import sys
|
||
print(json.dumps({
|
||
"limit": max(1, min(int(sys.argv[1] or 20), 200)),
|
||
"flush_after_replay": True,
|
||
"reason": "cli quick replay from drive_ops_center.sh",
|
||
"requested_by": str(sys.argv[2] or "cli/ops-center/queue-replay"),
|
||
}, ensure_ascii=False))
|
||
PY
|
||
)"
|
||
run_delivery_queue_action "${MAINLAND_BASE_URL}" "${NODE_CODE}" "/delivery-queue/replay" "${BODY_JSON}"
|
||
;;
|
||
queue-replay-record)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
NODE_CODE="${2:-}"
|
||
RECORD_ID="${3:-}"
|
||
REQUESTED_BY="${4:-$(ops_center_resolve_requested_by)}"
|
||
if [[ "${MAINLAND_BASE_URL}" != http://* && "${MAINLAND_BASE_URL}" != https://* ]]; then
|
||
REQUESTED_BY="${RECORD_ID:-$(ops_center_resolve_requested_by)}"
|
||
RECORD_ID="${NODE_CODE:-}"
|
||
NODE_CODE="${MAINLAND_BASE_URL}"
|
||
MAINLAND_BASE_URL="$(ops_center_resolve_mainland_api_base_url)"
|
||
fi
|
||
if [[ -z "${RECORD_ID}" ]]; then
|
||
echo "record_id is required" >&2
|
||
exit 1
|
||
fi
|
||
BODY_JSON="$(python3 - <<'PY' "${REQUESTED_BY}"
|
||
import json
|
||
import sys
|
||
print(json.dumps({
|
||
"limit": 1,
|
||
"flush_after_replay": True,
|
||
"reason": "cli replay visible head record",
|
||
"requested_by": str(sys.argv[1] or "cli/ops-center/queue-replay-record"),
|
||
}, ensure_ascii=False))
|
||
PY
|
||
)"
|
||
run_delivery_queue_action "${MAINLAND_BASE_URL}" "${NODE_CODE}" "/delivery-queue/records/${RECORD_ID}/replay" "${BODY_JSON}"
|
||
;;
|
||
queue-discard-record)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
NODE_CODE="${2:-}"
|
||
RECORD_ID="${3:-}"
|
||
DISCARD_REASON="${4:-}"
|
||
REQUESTED_BY="${5:-$(ops_center_resolve_requested_by)}"
|
||
if [[ "${MAINLAND_BASE_URL}" != http://* && "${MAINLAND_BASE_URL}" != https://* ]]; then
|
||
REQUESTED_BY="${DISCARD_REASON:-$(ops_center_resolve_requested_by)}"
|
||
DISCARD_REASON="${RECORD_ID:-}"
|
||
RECORD_ID="${NODE_CODE:-}"
|
||
NODE_CODE="${MAINLAND_BASE_URL}"
|
||
MAINLAND_BASE_URL="$(ops_center_resolve_mainland_api_base_url)"
|
||
fi
|
||
if [[ -z "${RECORD_ID}" || -z "${DISCARD_REASON}" ]]; then
|
||
echo "record_id and reason are required" >&2
|
||
exit 1
|
||
fi
|
||
BODY_JSON="$(python3 - <<'PY' "${DISCARD_REASON}" "${REQUESTED_BY}"
|
||
import json
|
||
import sys
|
||
print(json.dumps({
|
||
"limit": 1,
|
||
"reason": str(sys.argv[1] or "").strip(),
|
||
"discarded_by": "cli",
|
||
"requested_by": str(sys.argv[2] or "cli/ops-center/queue-discard-record"),
|
||
}, ensure_ascii=False))
|
||
PY
|
||
)"
|
||
run_delivery_queue_action "${MAINLAND_BASE_URL}" "${NODE_CODE}" "/delivery-queue/records/${RECORD_ID}/discard" "${BODY_JSON}"
|
||
;;
|
||
activity-stream)
|
||
if [[ $# -gt 0 && ( "$1" == http://* || "$1" == https://* ) ]]; then
|
||
MAINLAND_BASE_URL="$1"
|
||
shift || true
|
||
else
|
||
MAINLAND_BASE_URL="$(ops_center_resolve_mainland_api_base_url)"
|
||
fi
|
||
show_activity_stream "${MAINLAND_BASE_URL}" "$@"
|
||
;;
|
||
activity-preview)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
ACTIVITY_KEY="${2:-}"
|
||
REQUESTED_BY="${3:-$(ops_center_resolve_requested_by)}"
|
||
if [[ "${MAINLAND_BASE_URL}" != http://* && "${MAINLAND_BASE_URL}" != https://* ]]; then
|
||
ACTIVITY_KEY="${MAINLAND_BASE_URL}"
|
||
MAINLAND_BASE_URL="$(ops_center_resolve_mainland_api_base_url)"
|
||
REQUESTED_BY="${2:-$(ops_center_resolve_requested_by)}"
|
||
fi
|
||
preview_activity_item "${MAINLAND_BASE_URL}" "${ACTIVITY_KEY}" "${REQUESTED_BY}"
|
||
;;
|
||
driver-feed)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
RAW_HTTP_RESPONSE="$(request_with_status "GET" "${MAINLAND_BASE_URL}/api/v1/ops/driver-feed")"
|
||
HTTP_STATUS="${RAW_HTTP_RESPONSE##*$'\n'HTTP_STATUS=}"
|
||
RESPONSE_JSON="${RAW_HTTP_RESPONSE%$'\n'HTTP_STATUS=*}"
|
||
RESPONSE_JSON_FILE="$(mktemp)"
|
||
printf '%s' "${RESPONSE_JSON}" > "${RESPONSE_JSON_FILE}"
|
||
echo "[1/2] raw response"
|
||
printf '%s\n' "http_status=${HTTP_STATUS}"
|
||
python3 - <<'PY' "${RESPONSE_JSON_FILE}"
|
||
import json
|
||
import sys
|
||
with open(sys.argv[1], "r", encoding="utf-8") as fh:
|
||
payload = json.loads(fh.read() or "{}")
|
||
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
||
PY
|
||
echo
|
||
echo "[2/2] condensed summary"
|
||
python3 - <<'PY' "${RESPONSE_JSON_FILE}" "${HTTP_STATUS}"
|
||
import json
|
||
import sys
|
||
|
||
with open(sys.argv[1], "r", encoding="utf-8") as fh:
|
||
payload = json.loads(fh.read() or "{}")
|
||
http_status = int(sys.argv[2] or 0)
|
||
data = payload.get("data") or {}
|
||
summary = data.get("summary") or {}
|
||
entries = list(data.get("entries") or [])
|
||
activity_focus = list(data.get("activity_focus") or [])
|
||
top_recommendation = data.get("top_recommendation") or {}
|
||
scene_log_observation = data.get("scene_log_observation") or {}
|
||
contract_navigation = data.get("contract_navigation") or {}
|
||
print(json.dumps({
|
||
"http_status": http_status,
|
||
"headline": str(data.get("headline") or ""),
|
||
"generated_at": str(data.get("generated_at") or ""),
|
||
"contract_navigation": {
|
||
"primary_contract_key": str(contract_navigation.get("primary_contract_key") or ""),
|
||
"contract_keys": list(contract_navigation.get("contract_keys") or []),
|
||
"detail_endpoint_pattern": str(contract_navigation.get("detail_endpoint_pattern") or ""),
|
||
},
|
||
"top_recommendation": {
|
||
"key": str(top_recommendation.get("key") or ""),
|
||
"kind": str(top_recommendation.get("kind") or ""),
|
||
"lane": str(top_recommendation.get("lane") or ""),
|
||
"title": str(top_recommendation.get("title") or ""),
|
||
"primary_contract_key": str((top_recommendation.get("contract_navigation") or {}).get("primary_contract_key") or ""),
|
||
"contract_keys": list((top_recommendation.get("contract_navigation") or {}).get("contract_keys") or []),
|
||
},
|
||
"summary": {
|
||
"total": int(summary.get("total", 0) or 0),
|
||
"do_now_total": int(summary.get("do_now_total", 0) or 0),
|
||
"standard_path_total": int(summary.get("standard_path_total", 0) or 0),
|
||
"publish_ready": bool(summary.get("publish_ready", False)),
|
||
"publish_status": str(summary.get("publish_status") or ""),
|
||
"publish_status_label": str(summary.get("publish_status_label") or ""),
|
||
"launchpad_recommended_target_node_code": str(summary.get("launchpad_recommended_target_node_code") or ""),
|
||
"launchpad_recommended_recovery_label": str(summary.get("launchpad_recommended_recovery_label") or ""),
|
||
"launchpad_recommended_recovery_summary": str(summary.get("launchpad_recommended_recovery_summary") or ""),
|
||
"launchpad_onboarding_bootstrap_pending_nodes": int(summary.get("launchpad_onboarding_bootstrap_pending_nodes", 0) or 0),
|
||
"launchpad_onboarding_acceptance_ready_nodes": int(summary.get("launchpad_onboarding_acceptance_ready_nodes", 0) or 0),
|
||
"scene_log_status": str(summary.get("scene_log_status") or ""),
|
||
"kind_counts": dict(summary.get("kind_counts") or {}),
|
||
},
|
||
"scene_log_observation": {
|
||
"status": str(scene_log_observation.get("status") or ""),
|
||
"status_label": str(scene_log_observation.get("status_label") or ""),
|
||
"summary": str(scene_log_observation.get("summary") or ""),
|
||
"target_node_code": str(scene_log_observation.get("target_node_code") or ""),
|
||
"participating_node_count": int(scene_log_observation.get("participating_node_count", 0) or 0),
|
||
"covered_participating_node_count": int(scene_log_observation.get("covered_participating_node_count", 0) or 0),
|
||
"line_count": int(scene_log_observation.get("line_count", 0) or 0),
|
||
"focus_ref": scene_log_observation.get("focus_ref") or {},
|
||
"primary_contract_key": str((scene_log_observation.get("contract_navigation") or {}).get("primary_contract_key") or ""),
|
||
"contract_keys": list((scene_log_observation.get("contract_navigation") or {}).get("contract_keys") or []),
|
||
},
|
||
"entries": [
|
||
{
|
||
"key": str(item.get("key") or ""),
|
||
"kind": str(item.get("kind") or ""),
|
||
"lane": str(item.get("lane") or ""),
|
||
"title": str(item.get("title") or ""),
|
||
"status": str(item.get("status") or ""),
|
||
"primary_label": str(item.get("primary_label") or ""),
|
||
"secondary_label": str(item.get("secondary_label") or ""),
|
||
"primary_contract_key": str((item.get("contract_navigation") or {}).get("primary_contract_key") or ""),
|
||
"contract_keys": list((item.get("contract_navigation") or {}).get("contract_keys") or []),
|
||
}
|
||
for item in entries
|
||
],
|
||
"activity_focus": [
|
||
{
|
||
"key": str(item.get("key") or ""),
|
||
"title": str(item.get("title") or ""),
|
||
"status": str(item.get("status") or ""),
|
||
"primary_contract_key": str((item.get("contract_navigation") or {}).get("primary_contract_key") or ""),
|
||
"contract_keys": list((item.get("contract_navigation") or {}).get("contract_keys") or []),
|
||
}
|
||
for item in activity_focus
|
||
],
|
||
}, ensure_ascii=False, indent=2))
|
||
PY
|
||
rm -f "${RESPONSE_JSON_FILE}"
|
||
;;
|
||
driver-focus-preview)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
ENTRY_KEY="${2:-}"
|
||
REQUESTED_BY="${3:-$(ops_center_resolve_requested_by)}"
|
||
if [[ "${MAINLAND_BASE_URL}" != http://* && "${MAINLAND_BASE_URL}" != https://* ]]; then
|
||
ENTRY_KEY="${MAINLAND_BASE_URL}"
|
||
MAINLAND_BASE_URL="$(ops_center_resolve_mainland_api_base_url)"
|
||
REQUESTED_BY="${2:-$(ops_center_resolve_requested_by)}"
|
||
fi
|
||
preview_driver_focus_entry "${MAINLAND_BASE_URL}" "${ENTRY_KEY}" "${REQUESTED_BY}"
|
||
;;
|
||
driver-focus-run)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
ENTRY_KEY="${2:-}"
|
||
SECONDARY_FLAG="${3:-primary}"
|
||
CONFIRM_FLAG="${4:-}"
|
||
REQUESTED_BY="${5:-$(ops_center_resolve_requested_by)}"
|
||
if [[ "${MAINLAND_BASE_URL}" != http://* && "${MAINLAND_BASE_URL}" != https://* ]]; then
|
||
ENTRY_KEY="${MAINLAND_BASE_URL}"
|
||
MAINLAND_BASE_URL="$(ops_center_resolve_mainland_api_base_url)"
|
||
SECONDARY_FLAG="${2:-primary}"
|
||
CONFIRM_FLAG="${3:-}"
|
||
REQUESTED_BY="${4:-$(ops_center_resolve_requested_by)}"
|
||
fi
|
||
if is_secondary_token "${ENTRY_KEY}"; then
|
||
REQUESTED_BY="${CONFIRM_FLAG:-$(ops_center_resolve_requested_by)}"
|
||
CONFIRM_FLAG="${SECONDARY_FLAG:-}"
|
||
SECONDARY_FLAG="${ENTRY_KEY}"
|
||
ENTRY_KEY=""
|
||
fi
|
||
if is_confirm_token "${SECONDARY_FLAG}" && [[ -z "${CONFIRM_FLAG}" ]]; then
|
||
CONFIRM_FLAG="${SECONDARY_FLAG}"
|
||
SECONDARY_FLAG="primary"
|
||
fi
|
||
run_driver_focus_entry "${MAINLAND_BASE_URL}" "${ENTRY_KEY}" "${SECONDARY_FLAG}" "${CONFIRM_FLAG}" "${REQUESTED_BY}"
|
||
;;
|
||
codex-brief)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
RAW_HTTP_RESPONSE="$(
|
||
request_with_status_custom \
|
||
"GET" \
|
||
"${MAINLAND_BASE_URL}/api/v1/ops/codex-brief" \
|
||
"" \
|
||
"${OPS_CENTER_CURL_CONNECT_TIMEOUT}" \
|
||
"${OPS_CENTER_CURL_MAX_TIME_CODEX_BRIEF}"
|
||
)"
|
||
HTTP_STATUS="${RAW_HTTP_RESPONSE##*$'\n'HTTP_STATUS=}"
|
||
RESPONSE_JSON="${RAW_HTTP_RESPONSE%$'\n'HTTP_STATUS=*}"
|
||
RESPONSE_JSON_FILE="$(mktemp)"
|
||
printf '%s' "${RESPONSE_JSON}" > "${RESPONSE_JSON_FILE}"
|
||
echo "[1/2] raw response"
|
||
printf '%s\n' "http_status=${HTTP_STATUS}"
|
||
python3 - <<'PY' "${RESPONSE_JSON_FILE}"
|
||
import json
|
||
import sys
|
||
with open(sys.argv[1], "r", encoding="utf-8") as fh:
|
||
payload = json.loads(fh.read() or "{}")
|
||
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
||
PY
|
||
echo
|
||
echo "[2/2] condensed summary"
|
||
python3 - <<'PY' "${RESPONSE_JSON_FILE}" "${HTTP_STATUS}"
|
||
import json
|
||
import sys
|
||
|
||
with open(sys.argv[1], "r", encoding="utf-8") as fh:
|
||
payload = json.loads(fh.read() or "{}")
|
||
http_status = int(sys.argv[2] or 0)
|
||
data = payload.get("data") or {}
|
||
summary = data.get("summary") or {}
|
||
focus = data.get("focus") or {}
|
||
entries = list(data.get("entries") or [])
|
||
activity_focus = list(data.get("activity_focus") or [])
|
||
scene_log_observation = data.get("scene_log_observation") or {}
|
||
contract_navigation = data.get("contract_navigation") or {}
|
||
print(json.dumps({
|
||
"http_status": http_status,
|
||
"headline": str(data.get("headline") or ""),
|
||
"generated_at": str(data.get("generated_at") or ""),
|
||
"contract_navigation": {
|
||
"primary_contract_key": str(contract_navigation.get("primary_contract_key") or ""),
|
||
"contract_keys": list(contract_navigation.get("contract_keys") or []),
|
||
"detail_endpoint_pattern": str(contract_navigation.get("detail_endpoint_pattern") or ""),
|
||
},
|
||
"focus": {
|
||
"entry_key": str(focus.get("entry_key") or ""),
|
||
"title": str(focus.get("title") or ""),
|
||
"recommendation": str(focus.get("recommendation") or ""),
|
||
"status_text": str(focus.get("status_text") or ""),
|
||
"target_method": str(focus.get("target_method") or ""),
|
||
"target_api": str(focus.get("target_api") or ""),
|
||
"focus_ref": focus.get("focus_ref") or {},
|
||
"primary_focus_ref": focus.get("primary_focus_ref") or {},
|
||
"secondary_focus_ref": focus.get("secondary_focus_ref") or {},
|
||
"primary_contract_key": str((focus.get("contract_navigation") or {}).get("primary_contract_key") or ""),
|
||
"contract_keys": list((focus.get("contract_navigation") or {}).get("contract_keys") or []),
|
||
},
|
||
"summary": {
|
||
"total": int(summary.get("total", 0) or 0),
|
||
"go_live_status": str(summary.get("go_live_status") or ""),
|
||
"publish_ready": bool(summary.get("publish_ready", False)),
|
||
"publish_status": str(summary.get("publish_status") or ""),
|
||
"publish_status_label": str(summary.get("publish_status_label") or ""),
|
||
"launchpad_recommended_target_node_code": str(summary.get("launchpad_recommended_target_node_code") or ""),
|
||
"launchpad_recommended_recovery_label": str(summary.get("launchpad_recommended_recovery_label") or ""),
|
||
"launchpad_recommended_recovery_summary": str(summary.get("launchpad_recommended_recovery_summary") or ""),
|
||
"launchpad_onboarding_bootstrap_pending_nodes": int(summary.get("launchpad_onboarding_bootstrap_pending_nodes", 0) or 0),
|
||
"launchpad_onboarding_acceptance_ready_nodes": int(summary.get("launchpad_onboarding_acceptance_ready_nodes", 0) or 0),
|
||
"auto_execute_total": int(summary.get("auto_execute_total", 0) or 0),
|
||
"confirm_then_execute_total": int(summary.get("confirm_then_execute_total", 0) or 0),
|
||
"resolve_first_total": int(summary.get("resolve_first_total", 0) or 0),
|
||
"open_ui_total": int(summary.get("open_ui_total", 0) or 0),
|
||
"blocked_total": int(summary.get("blocked_total", 0) or 0),
|
||
"automation_level_counts": dict(summary.get("automation_level_counts") or {}),
|
||
},
|
||
"scene_log_observation": {
|
||
"status": str(scene_log_observation.get("status") or ""),
|
||
"status_label": str(scene_log_observation.get("status_label") or ""),
|
||
"summary": str(scene_log_observation.get("summary") or ""),
|
||
"target_node_code": str(scene_log_observation.get("target_node_code") or ""),
|
||
"participating_node_count": int(scene_log_observation.get("participating_node_count", 0) or 0),
|
||
"covered_participating_node_count": int(scene_log_observation.get("covered_participating_node_count", 0) or 0),
|
||
"line_count": int(scene_log_observation.get("line_count", 0) or 0),
|
||
"focus_ref": scene_log_observation.get("focus_ref") or {},
|
||
"primary_contract_key": str((scene_log_observation.get("contract_navigation") or {}).get("primary_contract_key") or ""),
|
||
"contract_keys": list((scene_log_observation.get("contract_navigation") or {}).get("contract_keys") or []),
|
||
},
|
||
"entries": [
|
||
{
|
||
"key": str(item.get("key") or ""),
|
||
"kind": str(item.get("kind") or ""),
|
||
"title": str(item.get("title") or ""),
|
||
"executor_kind": str(item.get("executor_kind") or ""),
|
||
"recommendation": str((item.get("autopilot") or {}).get("recommendation") or ""),
|
||
"automation_level": str((item.get("autopilot") or {}).get("automation_level") or ""),
|
||
"executor_mode_hint": str((item.get("autopilot") or {}).get("executor_mode_hint") or ""),
|
||
"primary_action_code": str(item.get("primary_action_code") or ""),
|
||
"focus_ref": item.get("focus_ref") or {},
|
||
"primary_focus_ref": item.get("primary_focus_ref") or {},
|
||
"secondary_focus_ref": item.get("secondary_focus_ref") or {},
|
||
"primary_contract_key": str((item.get("contract_navigation") or {}).get("primary_contract_key") or ""),
|
||
"contract_keys": list((item.get("contract_navigation") or {}).get("contract_keys") or []),
|
||
}
|
||
for item in entries
|
||
],
|
||
"activity_focus": [
|
||
{
|
||
"key": str(item.get("key") or ""),
|
||
"title": str(item.get("title") or ""),
|
||
"status": str(item.get("status") or ""),
|
||
"primary_contract_key": str((item.get("contract_navigation") or {}).get("primary_contract_key") or ""),
|
||
"contract_keys": list((item.get("contract_navigation") or {}).get("contract_keys") or []),
|
||
}
|
||
for item in activity_focus
|
||
],
|
||
}, ensure_ascii=False, indent=2))
|
||
PY
|
||
rm -f "${RESPONSE_JSON_FILE}"
|
||
;;
|
||
codex-focus-preview)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
ENTRY_KEY="${2:-}"
|
||
REQUESTED_BY="${3:-$(ops_center_resolve_requested_by)}"
|
||
if [[ "${MAINLAND_BASE_URL}" != http://* && "${MAINLAND_BASE_URL}" != https://* ]]; then
|
||
ENTRY_KEY="${MAINLAND_BASE_URL}"
|
||
MAINLAND_BASE_URL="$(ops_center_resolve_mainland_api_base_url)"
|
||
REQUESTED_BY="${2:-$(ops_center_resolve_requested_by)}"
|
||
fi
|
||
preview_codex_focus_entry "${MAINLAND_BASE_URL}" "${ENTRY_KEY}" "${REQUESTED_BY}"
|
||
;;
|
||
codex-focus-run)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
ENTRY_KEY="${2:-}"
|
||
CONFIRM_FLAG="${3:-}"
|
||
REQUESTED_BY="${4:-$(ops_center_resolve_requested_by)}"
|
||
if [[ "${MAINLAND_BASE_URL}" != http://* && "${MAINLAND_BASE_URL}" != https://* ]]; then
|
||
ENTRY_KEY="${MAINLAND_BASE_URL}"
|
||
MAINLAND_BASE_URL="$(ops_center_resolve_mainland_api_base_url)"
|
||
CONFIRM_FLAG="${2:-}"
|
||
REQUESTED_BY="${3:-$(ops_center_resolve_requested_by)}"
|
||
fi
|
||
run_codex_focus_entry "${MAINLAND_BASE_URL}" "${ENTRY_KEY}" "${CONFIRM_FLAG}" "${REQUESTED_BY}"
|
||
;;
|
||
release-hub)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
shift || true
|
||
exec bash "${SCRIPT_DIR}/drive_release_hub.sh" "${MAINLAND_BASE_URL}" "$@"
|
||
;;
|
||
driver-preview)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
ACTION_CODE="${2:-}"
|
||
ACTION_PAYLOAD_JSON="${3:-}"
|
||
REQUESTED_BY="${4:-$(ops_center_resolve_requested_by)}"
|
||
if [[ -z "${ACTION_CODE}" && "${MAINLAND_BASE_URL}" != http://* && "${MAINLAND_BASE_URL}" != https://* ]]; then
|
||
ACTION_CODE="${MAINLAND_BASE_URL}"
|
||
MAINLAND_BASE_URL="$(ops_center_resolve_mainland_api_base_url)"
|
||
ACTION_PAYLOAD_JSON="${2:-}"
|
||
REQUESTED_BY="${3:-$(ops_center_resolve_requested_by)}"
|
||
fi
|
||
if [[ -z "${ACTION_PAYLOAD_JSON}" ]]; then
|
||
ACTION_PAYLOAD_JSON='{}'
|
||
fi
|
||
if [[ -z "${ACTION_CODE}" ]]; then
|
||
usage
|
||
exit 1
|
||
fi
|
||
exec bash "${SCRIPT_DIR}/drive_ops_action.sh" "${MAINLAND_BASE_URL}" preview "${ACTION_CODE}" "${ACTION_PAYLOAD_JSON}" "${REQUESTED_BY}"
|
||
;;
|
||
driver-resolve)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
ACTION_CODE="${2:-}"
|
||
ACTION_PAYLOAD_JSON="${3:-}"
|
||
REQUESTED_BY="${4:-$(ops_center_resolve_requested_by)}"
|
||
if [[ -z "${ACTION_CODE}" && "${MAINLAND_BASE_URL}" != http://* && "${MAINLAND_BASE_URL}" != https://* ]]; then
|
||
ACTION_CODE="${MAINLAND_BASE_URL}"
|
||
MAINLAND_BASE_URL="$(ops_center_resolve_mainland_api_base_url)"
|
||
ACTION_PAYLOAD_JSON="${2:-}"
|
||
REQUESTED_BY="${3:-$(ops_center_resolve_requested_by)}"
|
||
fi
|
||
if [[ -z "${ACTION_CODE}" ]]; then
|
||
usage
|
||
exit 1
|
||
fi
|
||
if [[ -z "${ACTION_PAYLOAD_JSON}" ]]; then
|
||
ACTION_PAYLOAD_JSON='{}'
|
||
fi
|
||
resolve_driver_action_entry "${MAINLAND_BASE_URL}" "${ACTION_CODE}" "${ACTION_PAYLOAD_JSON}" "${REQUESTED_BY}" "cli/driver-resolve"
|
||
;;
|
||
driver-run)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
ACTION_CODE="${2:-}"
|
||
ACTION_PAYLOAD_JSON="${3:-}"
|
||
CONFIRM_FLAG="${4:-}"
|
||
REQUESTED_BY="${5:-$(ops_center_resolve_requested_by)}"
|
||
if [[ -z "${ACTION_CODE}" && "${MAINLAND_BASE_URL}" != http://* && "${MAINLAND_BASE_URL}" != https://* ]]; then
|
||
ACTION_CODE="${MAINLAND_BASE_URL}"
|
||
MAINLAND_BASE_URL="$(ops_center_resolve_mainland_api_base_url)"
|
||
ACTION_PAYLOAD_JSON="${2:-}"
|
||
CONFIRM_FLAG="${3:-}"
|
||
REQUESTED_BY="${4:-$(ops_center_resolve_requested_by)}"
|
||
fi
|
||
if is_confirm_token "${ACTION_PAYLOAD_JSON}" && [[ -z "${CONFIRM_FLAG}" ]]; then
|
||
CONFIRM_FLAG="${ACTION_PAYLOAD_JSON}"
|
||
ACTION_PAYLOAD_JSON='{}'
|
||
fi
|
||
if [[ -z "${ACTION_CODE}" ]]; then
|
||
usage
|
||
exit 1
|
||
fi
|
||
if [[ -z "${ACTION_PAYLOAD_JSON}" ]]; then
|
||
ACTION_PAYLOAD_JSON='{}'
|
||
fi
|
||
run_driver_action_entry "${MAINLAND_BASE_URL}" "${ACTION_CODE}" "${ACTION_PAYLOAD_JSON}" "${CONFIRM_FLAG}" "${REQUESTED_BY}" "cli/driver-run"
|
||
;;
|
||
driver)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
ACTION_CODE="${2:-}"
|
||
ACTION_PAYLOAD_JSON="${3:-}"
|
||
REQUESTED_BY="${4:-$(ops_center_resolve_requested_by)}"
|
||
if [[ -z "${ACTION_CODE}" && "${MAINLAND_BASE_URL}" != http://* && "${MAINLAND_BASE_URL}" != https://* ]]; then
|
||
ACTION_CODE="${MAINLAND_BASE_URL}"
|
||
MAINLAND_BASE_URL="$(ops_center_resolve_mainland_api_base_url)"
|
||
ACTION_PAYLOAD_JSON="${2:-}"
|
||
REQUESTED_BY="${3:-$(ops_center_resolve_requested_by)}"
|
||
fi
|
||
if [[ -z "${ACTION_PAYLOAD_JSON}" ]]; then
|
||
ACTION_PAYLOAD_JSON='{}'
|
||
fi
|
||
if [[ -z "${ACTION_CODE}" ]]; then
|
||
usage
|
||
exit 1
|
||
fi
|
||
exec bash "${SCRIPT_DIR}/drive_ops_action.sh" "${MAINLAND_BASE_URL}" "${ACTION_CODE}" "${ACTION_PAYLOAD_JSON}" "${REQUESTED_BY}"
|
||
;;
|
||
runbook-preview)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
SEQUENCE_KEY="${2:-}"
|
||
ACTION_PAYLOAD_JSON="${3:-}"
|
||
REQUESTED_BY="${4:-$(ops_center_resolve_requested_by)}"
|
||
if [[ -z "${SEQUENCE_KEY}" && "${MAINLAND_BASE_URL}" != http://* && "${MAINLAND_BASE_URL}" != https://* ]]; then
|
||
SEQUENCE_KEY="${MAINLAND_BASE_URL}"
|
||
MAINLAND_BASE_URL="$(ops_center_resolve_mainland_api_base_url)"
|
||
ACTION_PAYLOAD_JSON="${2:-}"
|
||
REQUESTED_BY="${3:-$(ops_center_resolve_requested_by)}"
|
||
fi
|
||
if [[ -z "${ACTION_PAYLOAD_JSON}" ]]; then
|
||
ACTION_PAYLOAD_JSON='{}'
|
||
fi
|
||
if [[ -z "${SEQUENCE_KEY}" ]]; then
|
||
echo "sequence_key is required" >&2
|
||
usage
|
||
exit 1
|
||
fi
|
||
preview_runbook_sequence "${MAINLAND_BASE_URL}" "${SEQUENCE_KEY}" "${ACTION_PAYLOAD_JSON}" "${REQUESTED_BY}"
|
||
;;
|
||
runbook-run)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
SEQUENCE_KEY="${2:-}"
|
||
SECONDARY_FLAG="${3:-primary}"
|
||
ACTION_PAYLOAD_JSON="${4:-}"
|
||
CONFIRM_FLAG="${5:-}"
|
||
REQUESTED_BY="${6:-$(ops_center_resolve_requested_by)}"
|
||
if [[ -z "${SEQUENCE_KEY}" && "${MAINLAND_BASE_URL}" != http://* && "${MAINLAND_BASE_URL}" != https://* ]]; then
|
||
SEQUENCE_KEY="${MAINLAND_BASE_URL}"
|
||
MAINLAND_BASE_URL="$(ops_center_resolve_mainland_api_base_url)"
|
||
SECONDARY_FLAG="${2:-primary}"
|
||
ACTION_PAYLOAD_JSON="${3:-}"
|
||
CONFIRM_FLAG="${4:-}"
|
||
REQUESTED_BY="${5:-$(ops_center_resolve_requested_by)}"
|
||
fi
|
||
if is_confirm_token "${ACTION_PAYLOAD_JSON}" && [[ -z "${CONFIRM_FLAG}" ]]; then
|
||
CONFIRM_FLAG="${ACTION_PAYLOAD_JSON}"
|
||
ACTION_PAYLOAD_JSON='{}'
|
||
fi
|
||
if [[ -z "${SEQUENCE_KEY}" ]]; then
|
||
echo "sequence_key is required" >&2
|
||
usage
|
||
exit 1
|
||
fi
|
||
if [[ -z "${ACTION_PAYLOAD_JSON}" ]]; then
|
||
ACTION_PAYLOAD_JSON='{}'
|
||
fi
|
||
run_resolved_runbook_sequence "${MAINLAND_BASE_URL}" "${SEQUENCE_KEY}" "${SECONDARY_FLAG}" "${ACTION_PAYLOAD_JSON}" "${CONFIRM_FLAG}" "${REQUESTED_BY}" "cli/runbook-run"
|
||
;;
|
||
runbook-resolve)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
SEQUENCE_KEY="${2:-}"
|
||
SECONDARY_FLAG="${3:-primary}"
|
||
ACTION_PAYLOAD_JSON="${4:-}"
|
||
REQUESTED_BY="${5:-$(ops_center_resolve_requested_by)}"
|
||
if [[ -z "${SEQUENCE_KEY}" && "${MAINLAND_BASE_URL}" != http://* && "${MAINLAND_BASE_URL}" != https://* ]]; then
|
||
SEQUENCE_KEY="${MAINLAND_BASE_URL}"
|
||
MAINLAND_BASE_URL="$(ops_center_resolve_mainland_api_base_url)"
|
||
SECONDARY_FLAG="${2:-primary}"
|
||
ACTION_PAYLOAD_JSON="${3:-}"
|
||
REQUESTED_BY="${4:-$(ops_center_resolve_requested_by)}"
|
||
fi
|
||
if [[ -z "${ACTION_PAYLOAD_JSON}" ]]; then
|
||
ACTION_PAYLOAD_JSON='{}'
|
||
fi
|
||
if [[ -z "${SEQUENCE_KEY}" ]]; then
|
||
echo "sequence_key is required" >&2
|
||
usage
|
||
exit 1
|
||
fi
|
||
if [[ "${ACTION_PAYLOAD_JSON}" == @* ]]; then
|
||
ACTION_PAYLOAD_PATH="${ACTION_PAYLOAD_JSON#@}"
|
||
if [[ ! -f "${ACTION_PAYLOAD_PATH}" ]]; then
|
||
echo "payload file not found: ${ACTION_PAYLOAD_PATH}" >&2
|
||
exit 1
|
||
fi
|
||
ACTION_PAYLOAD_JSON="$(cat "${ACTION_PAYLOAD_PATH}")"
|
||
fi
|
||
REQUEST_JSON="$(python3 - <<'PY' "${SECONDARY_FLAG}" "${ACTION_PAYLOAD_JSON}" "${REQUESTED_BY}"
|
||
import json
|
||
import sys
|
||
|
||
secondary_flag = str(sys.argv[1] or "primary").strip().lower()
|
||
payload_raw = str(sys.argv[2] or "").strip() or "{}"
|
||
requested_by = str(sys.argv[3] or "").strip() or "cli/drive_ops_center"
|
||
|
||
try:
|
||
action_payload = json.loads(payload_raw)
|
||
except Exception as exc:
|
||
raise SystemExit(f"invalid action payload json: {exc}")
|
||
|
||
if not isinstance(action_payload, dict):
|
||
raise SystemExit("action payload must be a json object")
|
||
|
||
request_payload = {
|
||
"secondary": secondary_flag in {"secondary", "second", "2", "true", "yes", "y"},
|
||
"action_payload": action_payload,
|
||
"requested_by": requested_by,
|
||
}
|
||
print(json.dumps(request_payload, ensure_ascii=False))
|
||
PY
|
||
)"
|
||
echo "[1/3] runbook resolve request"
|
||
python3 - <<'PY' "${MAINLAND_BASE_URL}" "${SEQUENCE_KEY}" "${REQUEST_JSON}"
|
||
import json
|
||
import sys
|
||
print(json.dumps({
|
||
"base_url": str(sys.argv[1] or ""),
|
||
"sequence_key": str(sys.argv[2] or ""),
|
||
"request": json.loads(str(sys.argv[3] or "{}")),
|
||
}, ensure_ascii=False, indent=2))
|
||
PY
|
||
echo
|
||
echo
|
||
RAW_HTTP_RESPONSE="$(request_with_status "POST" "${MAINLAND_BASE_URL}/api/v1/ops/runbook/sequences/${SEQUENCE_KEY}/resolve" "${REQUEST_JSON}")"
|
||
HTTP_STATUS="${RAW_HTTP_RESPONSE##*$'\n'HTTP_STATUS=}"
|
||
RESPONSE_JSON="${RAW_HTTP_RESPONSE%$'\n'HTTP_STATUS=*}"
|
||
echo "[2/3] raw response"
|
||
printf '%s\n' "http_status=${HTTP_STATUS}"
|
||
python3 - <<'PY' "${RESPONSE_JSON}"
|
||
import json
|
||
import sys
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
||
PY
|
||
echo
|
||
echo
|
||
echo "[3/3] condensed summary"
|
||
python3 - <<'PY' "${RESPONSE_JSON}" "${HTTP_STATUS}"
|
||
import json
|
||
import sys
|
||
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
http_status = int(sys.argv[2] or 0)
|
||
code = payload.get("code", 1)
|
||
if code in (None, ""):
|
||
code = 0 if http_status == 200 else 1
|
||
data = payload.get("data") or {}
|
||
print(json.dumps({
|
||
"http_status": http_status,
|
||
"code": int(code or 0),
|
||
"message": str(payload.get("message") or ""),
|
||
"sequence_key": str(data.get("sequence_key") or ""),
|
||
"driver_action_code": str(data.get("driver_action_code") or ""),
|
||
"driver_action_label": str(data.get("driver_action_label") or ""),
|
||
"driver_node_codes": list(data.get("driver_node_codes") or []),
|
||
"driver_requested_by": str(data.get("driver_requested_by") or ""),
|
||
"resolved_at": str(data.get("resolved_at") or ""),
|
||
"sequence_focus_ref": ((data.get("sequence") or {}).get("focus_ref") or {}),
|
||
"action_focus_ref": ((data.get("sequence") or {}).get("action_focus_ref") or {}),
|
||
}, ensure_ascii=False, indent=2))
|
||
PY
|
||
;;
|
||
runbook-sequence)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
SEQUENCE_KEY="${2:-}"
|
||
SECONDARY_FLAG="${3:-primary}"
|
||
ACTION_PAYLOAD_JSON="${4:-}"
|
||
REQUESTED_BY="${5:-$(ops_center_resolve_requested_by)}"
|
||
if [[ -z "${SEQUENCE_KEY}" && "${MAINLAND_BASE_URL}" != http://* && "${MAINLAND_BASE_URL}" != https://* ]]; then
|
||
SEQUENCE_KEY="${MAINLAND_BASE_URL}"
|
||
MAINLAND_BASE_URL="$(ops_center_resolve_mainland_api_base_url)"
|
||
SECONDARY_FLAG="${2:-primary}"
|
||
ACTION_PAYLOAD_JSON="${3:-}"
|
||
REQUESTED_BY="${4:-$(ops_center_resolve_requested_by)}"
|
||
fi
|
||
if [[ -z "${ACTION_PAYLOAD_JSON}" ]]; then
|
||
ACTION_PAYLOAD_JSON='{}'
|
||
fi
|
||
if [[ -z "${SEQUENCE_KEY}" ]]; then
|
||
echo "sequence_key is required" >&2
|
||
usage
|
||
exit 1
|
||
fi
|
||
if [[ "${ACTION_PAYLOAD_JSON}" == @* ]]; then
|
||
ACTION_PAYLOAD_PATH="${ACTION_PAYLOAD_JSON#@}"
|
||
if [[ ! -f "${ACTION_PAYLOAD_PATH}" ]]; then
|
||
echo "payload file not found: ${ACTION_PAYLOAD_PATH}" >&2
|
||
exit 1
|
||
fi
|
||
ACTION_PAYLOAD_JSON="$(cat "${ACTION_PAYLOAD_PATH}")"
|
||
fi
|
||
REQUEST_JSON="$(python3 - <<'PY' "${SECONDARY_FLAG}" "${ACTION_PAYLOAD_JSON}" "${REQUESTED_BY}"
|
||
import json
|
||
import sys
|
||
|
||
secondary_flag = str(sys.argv[1] or "primary").strip().lower()
|
||
payload_raw = str(sys.argv[2] or "").strip() or "{}"
|
||
requested_by = str(sys.argv[3] or "").strip() or "cli/drive_ops_center"
|
||
|
||
try:
|
||
action_payload = json.loads(payload_raw)
|
||
except Exception as exc:
|
||
raise SystemExit(f"invalid action payload json: {exc}")
|
||
|
||
if not isinstance(action_payload, dict):
|
||
raise SystemExit("action payload must be a json object")
|
||
|
||
request_payload = {
|
||
"secondary": secondary_flag in {"secondary", "second", "2", "true", "yes", "y"},
|
||
"action_payload": action_payload,
|
||
"requested_by": requested_by,
|
||
}
|
||
print(json.dumps(request_payload, ensure_ascii=False))
|
||
PY
|
||
)"
|
||
echo "[1/3] runbook sequence request"
|
||
python3 - <<'PY' "${MAINLAND_BASE_URL}" "${SEQUENCE_KEY}" "${REQUEST_JSON}"
|
||
import json
|
||
import sys
|
||
print(json.dumps({
|
||
"base_url": str(sys.argv[1] or ""),
|
||
"sequence_key": str(sys.argv[2] or ""),
|
||
"request": json.loads(str(sys.argv[3] or "{}")),
|
||
}, ensure_ascii=False, indent=2))
|
||
PY
|
||
echo
|
||
echo
|
||
RAW_HTTP_RESPONSE="$(request_with_status "POST" "${MAINLAND_BASE_URL}/api/v1/ops/runbook/sequences/${SEQUENCE_KEY}/execute" "${REQUEST_JSON}")"
|
||
HTTP_STATUS="${RAW_HTTP_RESPONSE##*$'\n'HTTP_STATUS=}"
|
||
RESPONSE_JSON="${RAW_HTTP_RESPONSE%$'\n'HTTP_STATUS=*}"
|
||
echo "[2/3] raw response"
|
||
printf '%s\n' "http_status=${HTTP_STATUS}"
|
||
python3 - <<'PY' "${RESPONSE_JSON}"
|
||
import json
|
||
import sys
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
||
PY
|
||
echo
|
||
echo
|
||
echo "[3/3] condensed summary"
|
||
python3 - <<'PY' "${RESPONSE_JSON}" "${HTTP_STATUS}"
|
||
import json
|
||
import sys
|
||
|
||
payload = json.loads(str(sys.argv[1] or "{}"))
|
||
http_status = int(sys.argv[2] or 0)
|
||
data = payload.get("data") or {}
|
||
raw_code = payload.get("code", 1)
|
||
if raw_code in (0, "0", False):
|
||
normalized_code = 0
|
||
else:
|
||
normalized_code = int(raw_code or 1)
|
||
|
||
summary = {
|
||
"http_status": http_status,
|
||
"code": normalized_code,
|
||
"message": str(payload.get("message") or ""),
|
||
"sequence_key": str(data.get("sequence_key") or ""),
|
||
"driver_action_code": str(data.get("driver_action_code") or ""),
|
||
"handled": bool(data.get("handled", False)),
|
||
"mode": str(data.get("mode") or ""),
|
||
"sequence": data.get("sequence") or {},
|
||
}
|
||
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
||
PY
|
||
if [[ "${HTTP_STATUS}" -lt 200 || "${HTTP_STATUS}" -ge 300 ]]; then
|
||
exit 1
|
||
fi
|
||
;;
|
||
smart-latest)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
MODE="${2:-}"
|
||
CONFIRM_FLAG="${3:-}"
|
||
if [[ "${MODE}" != "worker" && "${MODE}" != "control" ]]; then
|
||
echo "mode must be worker or control" >&2
|
||
usage
|
||
exit 1
|
||
fi
|
||
exec bash "${SCRIPT_DIR}/drive_release_hub.sh" "${MAINLAND_BASE_URL}" smart-latest "${MODE}" "${CONFIRM_FLAG}"
|
||
;;
|
||
smart-release)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
RELEASE_ID="${2:-}"
|
||
MODE="${3:-}"
|
||
CONFIRM_FLAG="${4:-}"
|
||
if [[ -z "${RELEASE_ID}" ]]; then
|
||
echo "release_id is required" >&2
|
||
usage
|
||
exit 1
|
||
fi
|
||
if [[ "${MODE}" != "worker" && "${MODE}" != "control" ]]; then
|
||
echo "mode must be worker or control" >&2
|
||
usage
|
||
exit 1
|
||
fi
|
||
exec bash "${SCRIPT_DIR}/drive_release_hub.sh" "${MAINLAND_BASE_URL}" smart-release "${RELEASE_ID}" "${MODE}" "${CONFIRM_FLAG}"
|
||
;;
|
||
agent-plan)
|
||
MAINLAND_BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
NODE_CODE="${2:-}"
|
||
NODE_REGION="${3:-mainland}"
|
||
NODE_ROLE="${4:-worker}"
|
||
CONTROL_PLANE_BASE_URL="${5:-$(ops_center_resolve_control_plane_base_url)}"
|
||
if [[ -z "${CONTROL_PLANE_BASE_URL}" ]]; then
|
||
CONTROL_PLANE_BASE_URL="$(ops_center_resolve_overseas_api_base_url)"
|
||
fi
|
||
ROOT_DIR="${6:-$(ops_center_resolve_root_dir)}"
|
||
if [[ -z "${NODE_CODE}" ]]; then
|
||
echo "node_code is required" >&2
|
||
usage
|
||
exit 1
|
||
fi
|
||
ISSUED_BY="$(ops_center_resolve_issued_by)" \
|
||
EXPIRES_IN_HOURS="$(ops_center_resolve_expires_in_hours)" \
|
||
exec bash "${SCRIPT_DIR}/build_node_agent_bootstrap_plan.sh" \
|
||
"${MAINLAND_BASE_URL}" \
|
||
"${NODE_CODE}" \
|
||
"${NODE_REGION}" \
|
||
"${NODE_ROLE}" \
|
||
"${CONTROL_PLANE_BASE_URL}" \
|
||
"${ROOT_DIR}"
|
||
;;
|
||
agent-plan-export)
|
||
REPORT_DIR="${1:-${OPS_CENTER_REPORT_ROOT}/agent-plans}"
|
||
MAINLAND_BASE_URL="${2:-$(ops_center_resolve_mainland_api_base_url)}"
|
||
NODE_CODE="${3:-}"
|
||
NODE_REGION="${4:-mainland}"
|
||
NODE_ROLE="${5:-worker}"
|
||
CONTROL_PLANE_BASE_URL="${6:-$(ops_center_resolve_control_plane_base_url)}"
|
||
if [[ -z "${CONTROL_PLANE_BASE_URL}" ]]; then
|
||
CONTROL_PLANE_BASE_URL="$(ops_center_resolve_overseas_api_base_url)"
|
||
fi
|
||
ROOT_DIR="${7:-$(ops_center_resolve_root_dir)}"
|
||
if [[ -z "${NODE_CODE}" ]]; then
|
||
echo "node_code is required" >&2
|
||
usage
|
||
exit 1
|
||
fi
|
||
export_agent_plan "${REPORT_DIR}" "${MAINLAND_BASE_URL}" "${NODE_CODE}" "${NODE_REGION}" "${NODE_ROLE}" "${CONTROL_PLANE_BASE_URL}" "${ROOT_DIR}"
|
||
;;
|
||
agent-check)
|
||
SERVICE_NAME="${1:-$(ops_center_resolve_node_agent_service_name)}"
|
||
ENV_FILE="${2:-$(ops_center_resolve_node_agent_env_file)}"
|
||
SERVICE_NAME="${SERVICE_NAME}" ENV_FILE="${ENV_FILE}" exec bash "${SCRIPT_DIR}/check_node_agent.sh"
|
||
;;
|
||
*)
|
||
echo "unknown command: ${COMMAND}" >&2
|
||
usage
|
||
exit 1
|
||
;;
|
||
esac
|