"""AI PMO kernel ingress render mixin.""" from __future__ import annotations from . import ai_pmo_kernel_common as _common for _name, _value in vars(_common).items(): if not _name.startswith("__"): globals()[_name] = _value class KernelIngressRenderMixin: def _build_capture_result( self, *, work_item: WorkItem, intent: CanonicalIntent, execution_result: dict[str, Any], render: dict[str, Any], direct_reply_required: bool, status: str, classification: str, ) -> dict[str, Any]: linked_project_ids = [int(work_item.project_id)] if work_item.project_id is not None else [] direct_reply_text = str(render.get("text") or "").strip() if _looks_like_upstream_timeout_message(direct_reply_text): logger.warning( "ai_pmo_upstream_timeout_reply_detected", tenant_id=int(self.tenant_id), work_item_id=int(work_item.id), intent=str(intent.name or ""), source_message_id=str(work_item.source_message_id or "")[:128], observed_at=now_cn_naive().isoformat(), reply_preview=direct_reply_text[:260], ) signal_ids = _normalize_signal_id_list(execution_result.get("signal_ids")) primary_signal_id = _safe_int(execution_result.get("signal_id")) or _safe_int(work_item.signal_id) if primary_signal_id is not None and primary_signal_id not in signal_ids: signal_ids = [primary_signal_id, *signal_ids] if signal_ids and primary_signal_id is None: primary_signal_id = signal_ids[0] result = { "handled": True, "status": status, "classification": classification, "linked_project_ids": linked_project_ids, "signal_id": primary_signal_id, "signal_ids": signal_ids, "created_project_id": _safe_int(execution_result.get("project_id")) if intent.name == INTENT_PROJECT_CREATE else None, "auto_tasks": execution_result.get("auto_tasks") if isinstance(execution_result.get("auto_tasks"), list) else execution_result.get("tasks", []), "ai_trace_id": f"ai-pmo-{int(work_item.id)}", "results": [ { "work_item_id": int(work_item.id), "state": work_item.state, "intent": intent.name, "route_status": "queued_for_decision" if work_item.state == STATE_WAITING_MANUAL else "auto_done", } ], "intent": intent.name, "workflow_route": str(work_item.intent or execution_result.get("workflow_route") or "").strip() or None, "direct_reply_required": bool(direct_reply_required), "direct_reply_text": direct_reply_text, "reply_template_key": str(render.get("template_key") or "").strip(), "work_item_id": int(work_item.id), "decision_level": work_item.decision_level, "review_summary": work_item.review_summary, "decision_trace": ( execution_result.get("decision_trace") if isinstance(execution_result, dict) else ((intent.metadata or {}).get("decision_trace") if isinstance(intent.metadata, dict) else None) ), } if intent.name == INTENT_QUERY and isinstance(execution_result.get("rag_result"), dict): result["rag_result"] = execution_result["rag_result"] if intent.name == INTENT_PROJECT_CREATE: raw_payload = execution_result.get("raw_payload") if isinstance(execution_result.get("raw_payload"), dict) else {} result["project"] = raw_payload.get("project") if intent.name == INTENT_SIGNAL_SOURCE_ONBOARD: result["signal_source_workflow"] = execution_result.get("signal_source_workflow") result["signal_source_orchestration"] = execution_result.get("signal_source_orchestration") result["ai_execution"] = execution_result.get("signal_source_orchestration") return result @staticmethod def _primary_operation_code(intent_name: str) -> Optional[str]: normalized_route = RouteResolver.normalize_route_key(intent_name) if normalized_route == ROUTE_QUERY_ANSWER: return "query.answer" if normalized_route == ROUTE_PROJECT_CREATE: return "project.create" if normalized_route in {ROUTE_PROJECT_SIGNAL_CREATE, ROUTE_PROJECT_SIGNAL_DISCOVER, ROUTE_PROJECT_SIGNAL_COMMIT}: if normalized_route == ROUTE_PROJECT_SIGNAL_DISCOVER: return ROUTE_PROJECT_SIGNAL_DISCOVER if normalized_route == ROUTE_PROJECT_SIGNAL_COMMIT: return ROUTE_PROJECT_SIGNAL_COMMIT return "signal.enter" if normalized_route == ROUTE_PROJECT_SIGNAL_REFRESH: return ROUTE_PROJECT_SIGNAL_REFRESH if normalized_route == ROUTE_PROJECT_SIGNAL_CANCEL: return ROUTE_PROJECT_SIGNAL_CANCEL if normalized_route == ROUTE_SIGNAL_SOURCE_ONBOARD: return "signal_source.onboard" if normalized_route in {ROUTE_FOLLOWUP_CONFIRM, ROUTE_FOLLOWUP_CONTINUE}: return "work_item.resume" if intent_name == INTENT_QUERY: return "query.answer" if intent_name == INTENT_PROJECT_CREATE: return "project.create" if intent_name == INTENT_SIGNAL_CREATE: return "signal.enter" if intent_name == INTENT_SIGNAL_SOURCE_ONBOARD: return "signal_source.onboard" if intent_name in {INTENT_FOLLOWUP_CONFIRM, INTENT_FOLLOWUP_CONTINUE}: return "work_item.resume" return None @staticmethod def _risk_level_for_intent(intent_name: str) -> str: if intent_name in {INTENT_PROJECT_CREATE, INTENT_SIGNAL_SOURCE_ONBOARD}: return "high" if intent_name in {INTENT_SIGNAL_CREATE, INTENT_FOLLOWUP_CONFIRM, INTENT_FOLLOWUP_CONTINUE}: return "medium" return "low" @staticmethod def _build_source_message_id( *, source_system: str, source_message_id: Optional[str], channel_ref: Optional[str], sender: str, message_text: str, ) -> str: explicit = str(source_message_id or "").strip() if explicit: return explicit[:128] basis = f"{source_system}|{channel_ref or ''}|{sender}|{message_text}" digest = hashlib.sha1(basis.encode("utf-8"), usedforsecurity=False).hexdigest() return f"{source_system}-{digest[:32]}" @staticmethod def _normalize_text(text: str) -> str: cleaned = re.sub(r"]*>", " ", str(text or ""), flags=re.IGNORECASE) cleaned = re.sub(r"^@PMFlow\s*", "", cleaned, flags=re.IGNORECASE) cleaned = re.sub(r"\s+", " ", cleaned).strip() return cleaned[:4000] @staticmethod def _build_transport_mode_reply(message_text: str) -> Optional[str]: normalized = re.sub(r"\s+", "", str(message_text or "")).lower() has_long_connection = "长连接" in normalized or "longconnection" in normalized has_webhook = "webhook" in normalized if not (has_long_connection and has_webhook): return None if not any(token in normalized for token in TRANSPORT_MODE_CORRECTION_HINTS): return None return "已按长连接模式处理飞书接入,当前不会走 webhook;这条说明不会再被当成信号源类型变更。" @staticmethod def _serialize_project(project: Optional[Project]) -> Optional[ProjectSnapshot]: if project is None: return None return { "id": int(project.id), "name": str(project.name or ""), "title": str(project.title or project.name or ""), "description": str(project.description or ""), "status": str(project.status or ""), "owner_id": _safe_int(project.owner_id), }