from __future__ import annotations from fastapi import APIRouter from app.schemas.common import ApiResponse from app.services.ops_agent_service import ( build_managed_node_handover_bootstrap_plan, execute_managed_node_onboarding_bootstrap, execute_managed_node_onboarding_acceptance, execute_managed_node_onboarding_recovery, get_managed_node_delivery_queue, get_managed_node_handover, get_managed_node_onboarding, list_managed_node_delivery_queue_records, list_managed_nodes_with_agent_state, list_ops_job_events, preview_managed_node_onboarding_bootstrap, preview_managed_node_onboarding_acceptance, preview_managed_node_onboarding_recovery, request_managed_node_delivery_queue_action, summarize_ops_job_events, ) from app.services.ops_job_service import ( approve_ops_job, cancel_ops_job, create_ops_job, create_ops_job_batch, dispatch_ops_job, get_ops_job, list_ops_jobs, sync_managed_nodes_from_cluster, upsert_managed_node, ) from app.services.ops_playbook_service import ( cancel_ops_playbook_run, execute_ops_playbook, get_ops_playbook_run, get_ops_playbooks, get_recent_ops_playbook_runs, list_ops_playbook_run_events, preview_ops_playbook, rerun_ops_playbook_run, ) from app.services.ops_policy_service import preview_ops_job_policy from app.services.ops_service import ( execute_codex_action, execute_resolved_driver_action, get_ops_activity_stream, get_ops_codex_brief, get_ops_contract_detail, get_ops_contract_registry, get_ops_doctor_decision, get_ops_driver_feed, get_ops_go_live_summary, get_ops_go_live_signoff, get_ops_go_live_bundle, get_ops_go_live_review, get_ops_link_snapshot, get_ops_node_scene_log, get_ops_stack_diagnosis, execute_driver_action, preview_driver_action, resolve_codex_action, resolve_driver_action_request, resolve_ops_runbook_sequence, execute_ops_runbook_sequence, get_ops_blueprint, get_ops_capabilities, get_ops_inspection_overview, get_ops_overview, get_ops_runbook, ) from app.services.ops_template_service import get_ops_action_templates router = APIRouter(tags=["ops"]) @router.get("/ops/overview", response_model=ApiResponse) def ops_overview() -> ApiResponse: return ApiResponse(data=get_ops_overview()) @router.get("/ops/link-snapshot", response_model=ApiResponse) def ops_link_snapshot() -> ApiResponse: return ApiResponse(data=get_ops_link_snapshot()) @router.get("/ops/stack-diagnosis", response_model=ApiResponse) def ops_stack_diagnosis(base_url: str = "") -> ApiResponse: return ApiResponse(data=get_ops_stack_diagnosis(base_url=base_url)) @router.get("/ops/go-live-summary", response_model=ApiResponse) def ops_go_live_summary(base_url: str = "") -> ApiResponse: return ApiResponse(data=get_ops_go_live_summary(base_url=base_url)) @router.get("/ops/doctor-decision", response_model=ApiResponse) def ops_doctor_decision(base_url: str = "", report_dir: str = "") -> ApiResponse: return ApiResponse(data=get_ops_doctor_decision(base_url=base_url, report_dir=report_dir)) @router.get("/ops/go-live-signoff", response_model=ApiResponse) def ops_go_live_signoff(base_url: str = "", report_dir: str = "") -> ApiResponse: return ApiResponse(data=get_ops_go_live_signoff(base_url=base_url, report_dir=report_dir)) @router.get("/ops/go-live-bundle", response_model=ApiResponse) def ops_go_live_bundle(base_url: str = "", report_dir: str = "") -> ApiResponse: return ApiResponse(data=get_ops_go_live_bundle(base_url=base_url, report_dir=report_dir)) @router.get("/ops/go-live-review", response_model=ApiResponse) def ops_go_live_review(base_url: str = "", report_dir: str = "") -> ApiResponse: return ApiResponse(data=get_ops_go_live_review(base_url=base_url, report_dir=report_dir)) @router.get("/ops/inspection-overview", response_model=ApiResponse) def ops_inspection_overview( limit: int = 80, status: str = "", problem_kind: str = "", query: str = "", only_problem: bool = False, only_participating: bool = False, ) -> ApiResponse: return ApiResponse( data=get_ops_inspection_overview( fetch_limit=limit, status=status, problem_kind=problem_kind, query=query, only_problem=only_problem, only_participating=only_participating, ) ) @router.get("/ops/activity-stream", response_model=ApiResponse) def ops_activity_stream( limit: int = 18, kind: str = "", status: str = "", execution_mode: str = "", query: str = "", ) -> ApiResponse: return ApiResponse( data=get_ops_activity_stream( limit=limit, kind=kind, status=status, execution_mode=execution_mode, query=query, ) ) @router.get("/ops/driver-feed", response_model=ApiResponse) def ops_driver_feed() -> ApiResponse: return ApiResponse(data=get_ops_driver_feed()) @router.get("/ops/codex-brief", response_model=ApiResponse) def ops_codex_brief() -> ApiResponse: return ApiResponse(data=get_ops_codex_brief()) @router.get("/ops/capabilities", response_model=ApiResponse) def ops_capabilities() -> ApiResponse: return ApiResponse(data=get_ops_capabilities()) @router.get("/ops/contracts", response_model=ApiResponse) def ops_contracts() -> ApiResponse: return ApiResponse(data=get_ops_contract_registry()) @router.get("/ops/contracts/{contract_key}", response_model=ApiResponse) def ops_contract_detail(contract_key: str) -> ApiResponse: data = get_ops_contract_detail(contract_key) if not data: return ApiResponse(code=1, message="contract 不存在", data={}) return ApiResponse(data=data) @router.get("/ops/action-templates", response_model=ApiResponse) def ops_action_templates() -> ApiResponse: return ApiResponse(data=get_ops_action_templates()) @router.get("/ops/playbooks", response_model=ApiResponse) def ops_playbooks() -> ApiResponse: return ApiResponse(data=get_ops_playbooks()) @router.get("/ops/playbook-runs", response_model=ApiResponse) def ops_playbook_runs( limit: int = 12, status: str = "", group_key: str = "", query: str = "", ) -> ApiResponse: return ApiResponse( data=get_recent_ops_playbook_runs( limit=limit, status=status, group_key=group_key, query=query, ) ) @router.get("/ops/playbook-runs/{run_code}", response_model=ApiResponse) def ops_playbook_run_detail(run_code: str) -> ApiResponse: data = get_ops_playbook_run(run_code) if not data: return ApiResponse(code=1, message="playbook run 不存在", data={}) return ApiResponse(data=data) @router.get("/ops/playbook-runs/{run_code}/events", response_model=ApiResponse) def ops_playbook_run_events( run_code: str, limit: int = 80, step_key: str = "", node_code: str = "", ) -> ApiResponse: data = list_ops_playbook_run_events( run_code, limit=limit, step_key=step_key, node_code=node_code, ) return ApiResponse(data=data) @router.post("/ops/playbook-runs/{run_code}/rerun", response_model=ApiResponse) def ops_playbook_run_rerun(run_code: str, payload: dict | None = None) -> ApiResponse: payload = payload or {} ok, message, data = rerun_ops_playbook_run( run_code, requested_by=str(payload.get("requested_by") or "api").strip() or "api", ) return ApiResponse(code=0 if ok else 1, message=message, data=data) @router.post("/ops/playbook-runs/{run_code}/cancel", response_model=ApiResponse) def ops_playbook_run_cancel(run_code: str, payload: dict | None = None) -> ApiResponse: payload = payload or {} ok, message, data = cancel_ops_playbook_run( run_code, cancelled_by=str(payload.get("cancelled_by") or "api").strip() or "api", reason=str(payload.get("reason") or "").strip(), ) return ApiResponse(code=0 if ok else 1, message=message, data=data) @router.post("/ops/playbooks/preview", response_model=ApiResponse) def ops_playbook_preview(payload: dict) -> ApiResponse: ok, message, data = preview_ops_playbook(payload or {}) return ApiResponse(code=0 if ok else 1, message=message, data=data) @router.post("/ops/playbooks/execute", response_model=ApiResponse) def ops_playbook_execute(payload: dict) -> ApiResponse: ok, message, data = execute_ops_playbook(payload or {}) return ApiResponse(code=0 if ok else 1, message=message, data=data) @router.get("/ops/blueprint", response_model=ApiResponse) def ops_blueprint() -> ApiResponse: return ApiResponse(data=get_ops_blueprint()) @router.get("/ops/runbook", response_model=ApiResponse) def ops_runbook() -> ApiResponse: return ApiResponse(data=get_ops_runbook()) @router.post("/ops/runbook/sequences/{sequence_key}/resolve", response_model=ApiResponse) def ops_runbook_sequence_resolve(sequence_key: str, payload: dict | None = None) -> ApiResponse: ok, message, data = resolve_ops_runbook_sequence(sequence_key, payload or {}) return ApiResponse(code=0 if ok else 1, message=message, data=data) @router.post("/ops/runbook/sequences/{sequence_key}/execute", response_model=ApiResponse) def ops_runbook_sequence_execute(sequence_key: str, payload: dict | None = None) -> ApiResponse: ok, message, data = execute_ops_runbook_sequence(sequence_key, payload or {}) return ApiResponse(code=0 if ok else 1, message=message, data=data) @router.get("/ops/nodes", response_model=ApiResponse) def ops_nodes() -> ApiResponse: return ApiResponse(data=list_managed_nodes_with_agent_state()) @router.get("/ops/nodes/{node_code}/handover", response_model=ApiResponse) def ops_node_handover( node_code: str, control_plane_base_url: str = "", root_dir: str = "", ) -> ApiResponse: data = get_managed_node_handover( node_code, control_plane_base_url=control_plane_base_url, root_dir=root_dir, ) if not data: return ApiResponse(code=1, message="节点不存在", data={}) return ApiResponse(data=data) @router.get("/ops/nodes/{node_code}/onboarding", response_model=ApiResponse) def ops_node_onboarding( node_code: str, control_plane_base_url: str = "", root_dir: str = "", ) -> ApiResponse: data = get_managed_node_onboarding( node_code, control_plane_base_url=control_plane_base_url, root_dir=root_dir, ) if not data: return ApiResponse(code=1, message="节点不存在", data={}) return ApiResponse(data=data) @router.post("/ops/nodes/{node_code}/onboarding/acceptance/preview", response_model=ApiResponse) def ops_node_onboarding_acceptance_preview(node_code: str, payload: dict | None = None) -> ApiResponse: normalized_payload = payload or {} ok, message, data = preview_managed_node_onboarding_acceptance( node_code=node_code, requested_by=str(normalized_payload.get("requested_by") or "api").strip() or "api", control_plane_base_url=str(normalized_payload.get("control_plane_base_url") or "").strip(), root_dir=str(normalized_payload.get("root_dir") or "").strip(), ) return ApiResponse(code=0 if ok else 1, message=message, data=data) @router.post("/ops/nodes/{node_code}/onboarding/bootstrap/preview", response_model=ApiResponse) def ops_node_onboarding_bootstrap_preview(node_code: str, payload: dict | None = None) -> ApiResponse: normalized_payload = payload or {} ok, message, data = preview_managed_node_onboarding_bootstrap( node_code=node_code, requested_by=str(normalized_payload.get("requested_by") or "api").strip() or "api", control_plane_base_url=str(normalized_payload.get("control_plane_base_url") or "").strip(), root_dir=str(normalized_payload.get("root_dir") or "").strip(), ) return ApiResponse(code=0 if ok else 1, message=message, data=data) @router.post("/ops/nodes/{node_code}/onboarding/bootstrap/execute", response_model=ApiResponse) def ops_node_onboarding_bootstrap_execute(node_code: str, payload: dict | None = None) -> ApiResponse: normalized_payload = payload or {} ok, message, data = execute_managed_node_onboarding_bootstrap( node_code=node_code, requested_by=str(normalized_payload.get("requested_by") or "api").strip() or "api", control_plane_base_url=str(normalized_payload.get("control_plane_base_url") or "").strip(), root_dir=str(normalized_payload.get("root_dir") or "").strip(), ) return ApiResponse(code=0 if ok else 1, message=message, data=data) @router.post("/ops/nodes/{node_code}/onboarding/acceptance/execute", response_model=ApiResponse) def ops_node_onboarding_acceptance_execute(node_code: str, payload: dict | None = None) -> ApiResponse: normalized_payload = payload or {} ok, message, data = execute_managed_node_onboarding_acceptance( node_code=node_code, requested_by=str(normalized_payload.get("requested_by") or "api").strip() or "api", control_plane_base_url=str(normalized_payload.get("control_plane_base_url") or "").strip(), root_dir=str(normalized_payload.get("root_dir") or "").strip(), ) return ApiResponse(code=0 if ok else 1, message=message, data=data) @router.post("/ops/nodes/{node_code}/onboarding/recovery/preview", response_model=ApiResponse) def ops_node_onboarding_recovery_preview(node_code: str, payload: dict | None = None) -> ApiResponse: normalized_payload = payload or {} ok, message, data = preview_managed_node_onboarding_recovery( node_code=node_code, requested_by=str(normalized_payload.get("requested_by") or "api").strip() or "api", control_plane_base_url=str(normalized_payload.get("control_plane_base_url") or "").strip(), root_dir=str(normalized_payload.get("root_dir") or "").strip(), ) return ApiResponse(code=0 if ok else 1, message=message, data=data) @router.post("/ops/nodes/{node_code}/onboarding/recovery/execute", response_model=ApiResponse) def ops_node_onboarding_recovery_execute(node_code: str, payload: dict | None = None) -> ApiResponse: normalized_payload = payload or {} ok, message, data = execute_managed_node_onboarding_recovery( node_code=node_code, requested_by=str(normalized_payload.get("requested_by") or "api").strip() or "api", control_plane_base_url=str(normalized_payload.get("control_plane_base_url") or "").strip(), root_dir=str(normalized_payload.get("root_dir") or "").strip(), ) return ApiResponse(code=0 if ok else 1, message=message, data=data) @router.get("/ops/nodes/{node_code}/scene-log", response_model=ApiResponse) def ops_node_scene_log( node_code: str, limit: int = 80, mode: str = "", ) -> ApiResponse: data = get_ops_node_scene_log(node_code, limit=limit, mode=mode) if not data: return ApiResponse(code=1, message="节点不存在", data={}) return ApiResponse(data=data) @router.post("/ops/nodes/{node_code}/handover/bootstrap-plan", response_model=ApiResponse) def ops_node_handover_bootstrap_plan(node_code: str, payload: dict | None = None) -> ApiResponse: normalized_payload = payload or {} ok, message, data = build_managed_node_handover_bootstrap_plan( node_code=node_code, issued_by=str(normalized_payload.get("issued_by") or "api").strip() or "api", expires_in_hours=int(normalized_payload.get("expires_in_hours") or 72), control_plane_base_url=str(normalized_payload.get("control_plane_base_url") or "").strip(), root_dir=str(normalized_payload.get("root_dir") or "").strip(), metadata=normalized_payload.get("metadata") or {}, ) return ApiResponse(code=0 if ok else 1, message=message, data=data) @router.get("/ops/nodes/{node_code}/delivery-queue", response_model=ApiResponse) def ops_node_delivery_queue(node_code: str) -> ApiResponse: data = get_managed_node_delivery_queue(node_code) if not data: return ApiResponse(code=1, message="节点不存在", data={}) return ApiResponse(data=data) @router.get("/ops/nodes/{node_code}/delivery-queue/records", response_model=ApiResponse) def ops_node_delivery_queue_records( node_code: str, state: str = "", limit: int = 50, ) -> ApiResponse: data = list_managed_node_delivery_queue_records( node_code, state=state, limit=limit, ) if not data: return ApiResponse(code=1, message="节点不存在", data={}) return ApiResponse(data=data) @router.post("/ops/nodes/{node_code}/delivery-queue/flush", response_model=ApiResponse) def ops_node_delivery_queue_flush(node_code: str, payload: dict | None = None) -> ApiResponse: ok, message, data = request_managed_node_delivery_queue_action( node_code, "delivery.queue.flush", payload=payload or {}, ) return ApiResponse(code=0 if ok else 1, message=message, data=data) @router.post("/ops/nodes/{node_code}/delivery-queue/replay", response_model=ApiResponse) def ops_node_delivery_queue_replay(node_code: str, payload: dict | None = None) -> ApiResponse: ok, message, data = request_managed_node_delivery_queue_action( node_code, "delivery.queue.replay", payload=payload or {}, ) return ApiResponse(code=0 if ok else 1, message=message, data=data) @router.post("/ops/nodes/{node_code}/delivery-queue/records/{record_id}/replay", response_model=ApiResponse) def ops_node_delivery_queue_record_replay( node_code: str, record_id: str, payload: dict | None = None, ) -> ApiResponse: ok, message, data = request_managed_node_delivery_queue_action( node_code, "delivery.queue.replay", payload=payload or {}, record_id=record_id, ) return ApiResponse(code=0 if ok else 1, message=message, data=data) @router.post("/ops/nodes/{node_code}/delivery-queue/records/{record_id}/discard", response_model=ApiResponse) def ops_node_delivery_queue_record_discard( node_code: str, record_id: str, payload: dict | None = None, ) -> ApiResponse: ok, message, data = request_managed_node_delivery_queue_action( node_code, "delivery.queue.discard", payload=payload or {}, record_id=record_id, ) return ApiResponse(code=0 if ok else 1, message=message, data=data) @router.post("/ops/nodes", response_model=ApiResponse) def ops_nodes_upsert(payload: dict) -> ApiResponse: ok, message, data = upsert_managed_node(payload) return ApiResponse(code=0 if ok else 1, message=message, data=data) @router.post("/ops/nodes/sync-from-cluster", response_model=ApiResponse) def ops_nodes_sync_from_cluster(dry_run: bool = False) -> ApiResponse: return ApiResponse(data=sync_managed_nodes_from_cluster(dry_run=dry_run)) @router.get("/ops/jobs", response_model=ApiResponse) def ops_jobs(limit: int = 20, compact: bool = True, rollout_id: int = 0) -> ApiResponse: normalized_rollout_id = int(rollout_id or 0) jobs = list_ops_jobs( limit=limit, compact=compact, rollout_id=normalized_rollout_id if normalized_rollout_id > 0 else None, ) page_status_counts: dict[str, int] = {} for item in jobs: item_status = str(item.get("status") or "").strip() or "unknown" page_status_counts[item_status] = int(page_status_counts.get(item_status, 0) or 0) + 1 return ApiResponse( data={ "jobs": jobs, "filters": { "limit": int(limit or 20), "compact": bool(compact), "rollout_id": normalized_rollout_id, }, "page_summary": { "jobs_total": len(jobs), "status_counts": page_status_counts, }, } ) @router.get("/ops/jobs/{job_id}", response_model=ApiResponse) def ops_job_detail(job_id: int) -> ApiResponse: data = get_ops_job(job_id) if not data: return ApiResponse(code=1, message="任务不存在", data={}) return ApiResponse(data=data) @router.get("/ops/jobs/{job_id}/events", response_model=ApiResponse) def ops_job_events(job_id: int, limit: int = 50) -> ApiResponse: events = list_ops_job_events(job_id, limit=limit) return ApiResponse( data={ "events": events, "summary": summarize_ops_job_events(events, job_id=job_id, limit=limit), } ) @router.post("/ops/driver-actions/execute", response_model=ApiResponse) def ops_driver_action_execute(payload: dict) -> ApiResponse: ok, message, data = execute_driver_action(payload or {}) return ApiResponse(code=0 if ok else 1, message=message, data=data) @router.post("/ops/driver-actions/preview", response_model=ApiResponse) def ops_driver_action_preview(payload: dict) -> ApiResponse: ok, message, data = preview_driver_action(payload or {}) return ApiResponse(code=0 if ok else 1, message=message, data=data) @router.post("/ops/driver-actions/resolve", response_model=ApiResponse) def ops_driver_action_resolve(payload: dict | None = None) -> ApiResponse: ok, message, data = resolve_driver_action_request(payload or {}) return ApiResponse(code=0 if ok else 1, message=message, data=data) @router.post("/ops/driver-actions/execute-resolved", response_model=ApiResponse) def ops_driver_action_execute_resolved(payload: dict | None = None) -> ApiResponse: ok, message, data = execute_resolved_driver_action(payload or {}) return ApiResponse(code=0 if ok else 1, message=message, data=data) @router.post("/ops/codex-actions/resolve", response_model=ApiResponse) def ops_codex_action_resolve(payload: dict | None = None) -> ApiResponse: ok, message, data = resolve_codex_action(payload or {}) return ApiResponse(code=0 if ok else 1, message=message, data=data) @router.post("/ops/codex-actions/execute", response_model=ApiResponse) def ops_codex_action_execute(payload: dict | None = None) -> ApiResponse: ok, message, data = execute_codex_action(payload or {}) return ApiResponse(code=0 if ok else 1, message=message, data=data) @router.post("/ops/jobs", response_model=ApiResponse) def ops_jobs_create(payload: dict) -> ApiResponse: ok, message, data = create_ops_job(payload) return ApiResponse(code=0 if ok else 1, message=message, data=data) @router.post("/ops/jobs/batch", response_model=ApiResponse) def ops_jobs_create_batch(payload: dict) -> ApiResponse: ok, message, data = create_ops_job_batch(payload) return ApiResponse(code=0 if ok else 1, message=message, data=data) @router.post("/ops/policy/preview", response_model=ApiResponse) def ops_policy_preview(payload: dict) -> ApiResponse: return ApiResponse(data=preview_ops_job_policy(payload)) @router.post("/ops/jobs/{job_id}/approve", response_model=ApiResponse) def ops_job_approve(job_id: int, payload: dict | None = None) -> ApiResponse: payload = payload or {} ok, message, data = approve_ops_job(job_id, approved_by=str(payload.get("approved_by") or "api").strip() or "api") return ApiResponse(code=0 if ok else 1, message=message, data=data) @router.post("/ops/jobs/{job_id}/cancel", response_model=ApiResponse) def ops_job_cancel(job_id: int, payload: dict | None = None) -> ApiResponse: payload = payload or {} ok, message, data = cancel_ops_job( job_id, cancelled_by=str(payload.get("cancelled_by") or "api").strip() or "api", reason=str(payload.get("reason") or "").strip(), ) return ApiResponse(code=0 if ok else 1, message=message, data=data) @router.post("/ops/jobs/{job_id}/dispatch", response_model=ApiResponse) def ops_job_dispatch(job_id: int) -> ApiResponse: ok, message, data = dispatch_ops_job(job_id) return ApiResponse(code=0 if ok else 1, message=message, data=data)