"""AI PMO domain workflow adapters。""" from __future__ import annotations from typing import Any, Optional from sqlalchemy.ext.asyncio import AsyncSession from .signal_source_orchestration_service import SignalSourceOrchestrationService class SignalSourceDomainWorkflow: """把 signal source onboarding 归一成 domain workflow 结果。""" def __init__(self, db: AsyncSession) -> None: self.db = db async def run( self, *, tenant_id: int, project_id: int, signal_id: int, initiating_text: str, requested_via: str, operator_user_id: Optional[int], ) -> dict[str, Any]: raw_result = await SignalSourceOrchestrationService(self.db).orchestrate_from_signal( tenant_id=tenant_id, project_id=project_id, signal_id=signal_id, initiating_text=initiating_text, requested_via=requested_via, operator_user_id=operator_user_id, dry_run=False, response_mode="fast", ) normalized = self.normalize(raw_result) normalized["raw_result"] = raw_result return normalized @classmethod def normalize(cls, raw_result: dict[str, Any]) -> dict[str, Any]: payload = raw_result if isinstance(raw_result, dict) else {} signal_source = payload.get("signal_source") if isinstance(payload.get("signal_source"), dict) else {} worker_executions = payload.get("worker_executions") if isinstance(payload.get("worker_executions"), list) else [] typed_actions = cls._extract_typed_actions(payload.get("openclaw")) material_actions = [ row for row in typed_actions if str(row.get("operation_code") or "").strip() != "notification.create" and str(row.get("status") or "").strip() in {"applied", "planned"} ] fallback_used = any(bool(item.get("used_fallback")) for item in worker_executions if isinstance(item, dict)) raw_status = str(payload.get("status") or "").strip().lower() registry_status = str(signal_source.get("status") or "").strip().lower() onboarding_status = str(signal_source.get("onboarding_status") or "").strip().lower() openclaw_status = str((payload.get("openclaw") or {}).get("status") or "").strip().lower() if raw_status == "failed" or openclaw_status == "failed": final_status = "failed_safe" elif onboarding_status != "completed": final_status = "pending_manual" elif not material_actions: final_status = "pending_manual" elif fallback_used: final_status = "pending_manual" elif raw_status in {"completed", "partial", "planned"}: final_status = "completed" else: final_status = "pending_manual" next_step = "等待人工确认" if final_status != "completed" else "继续自动执行后续任务" return { "status": final_status, "registry_id": signal_source.get("registry_id"), "registry_status": registry_status, "onboarding_status": onboarding_status, "openclaw_status": openclaw_status, "typed_actions": typed_actions, "material_action_count": len(material_actions), "worker_executions": worker_executions, "pending_workers": payload.get("pending_workers") if isinstance(payload.get("pending_workers"), list) else [], "next_step": next_step, "fallback_used": fallback_used, } @staticmethod def _extract_typed_actions(openclaw_payload: Any) -> list[dict[str, Any]]: if not isinstance(openclaw_payload, dict): return [] result = openclaw_payload.get("result") if isinstance(openclaw_payload.get("result"), dict) else openclaw_payload execution = result.get("execution") if isinstance(result, dict) and isinstance(result.get("execution"), dict) else {} rows = execution.get("results") if isinstance(execution.get("results"), list) else [] actions: list[dict[str, Any]] = [] for row in rows: if not isinstance(row, dict): continue actions.append( { "operation_code": str(row.get("operation_code") or "").strip(), "status": str(row.get("status") or "").strip(), "entity_id": row.get("entity_id"), "payload": row.get("payload") if isinstance(row.get("payload"), dict) else {}, } ) return actions