PMFlow/backend/app/services/ai_pmo_domain_workflow.py

109 lines
4.6 KiB
Python

"""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