"""Feishu signal pipeline mixin.""" from __future__ import annotations from .shared import * from . import shared as _pipeline class FeishuPipelineSerializationMixin: async def _serialize_item(self, item: FeishuItem, *, include_raw: bool = False, include_history: bool = False, ) -> dict[str, Any]: result_payload = item.result_payload if isinstance(item.result_payload, dict) else {} ai_output = _pipeline._safe_dict(item.ai_output) project_name = await self._load_project_name(_pipeline._safe_int(item.matched_project_id)) row: dict[str, Any] = { "id": str(item.id), "feishu_message_id": item.feishu_message_id, "level": item.level, "card_type": item.card_type, "route_status": self._normalize_route_status(item.route_status), "project_score": item.project_score, "signal_confidence": item.signal_confidence, "level_confidence": item.level_confidence, "level_reasoning": item.level_reasoning, "matched_project_id": item.matched_project_id, "project_name": project_name, "sender": item.source_sender, "created_at": item.created_at.isoformat() if item.created_at else None, "processed_at": item.processed_at.isoformat() if item.processed_at else None, "updated_at": item.updated_at.isoformat() if item.updated_at else None, "decision_reason": item.decision_reason, "blocking_reason": item.blocking_reason, "safe_to_auto_execute": bool(item.safe_to_auto_execute), "auto_execute_state": self._resolve_auto_execute_state(item), "dedupe_key": item.dedupe_key, "message_class": self._resolve_message_class(item), "occurrence_count": int(item.occurrence_count or 1), "next_step_text": item.next_step_text, "undoable_until": item.undoable_until.isoformat() if item.undoable_until else None, "result_payload": result_payload or None, "display_kind": self._resolve_message_class(item), "target_url": result_payload.get("target_url") if isinstance(result_payload, dict) else None, "priority_rank": 0, } if not row["target_url"] and item.matched_project_id: row["target_url"] = f"/projects/{int(item.matched_project_id)}/board" if include_raw: row["raw_message"] = item.raw_message if (item.level or "").lower() == "project": suggestion = await self._get_project_suggestion(item.id) operation_payload = result_payload.get("operation_payload") if isinstance(result_payload.get("operation_payload"), dict) else {} project_fields = { "title": str( (suggestion.suggested_name if suggestion is not None else None) or operation_payload.get("title") or "" ).strip(), "description": str( (suggestion.suggested_description if suggestion is not None else None) or operation_payload.get("description") or "" ).strip(), "project_scope": str(result_payload.get("project_scope") or "").strip(), "missing_fields": [], "ready": False, } if ( self._looks_like_placeholder_project_text(project_fields["title"]) or self._looks_like_placeholder_project_text(project_fields["description"]) or self._looks_like_placeholder_project_text(project_fields["project_scope"]) ): project_fields = await self._maybe_prefill_project_fields_from_context( message=str(item.raw_message or ""), source_chat_id=str(item.source_chat_id or ""), source_sender_open_id=str(item.source_sender_open_id or "") or None, sender=str(item.source_sender or ""), source_message_id=str(item.feishu_message_id or "") or None, fields=project_fields, ) if suggestion: created_project_id = _pipeline._safe_int(suggestion.created_project_id) created_project_name = await self._load_project_name(created_project_id) row["project_suggestion"] = { "id": str(suggestion.id), "suggested_name": suggestion.suggested_name, "suggested_description": suggestion.suggested_description, "suggested_owner": suggestion.suggested_owner, "suggested_timeline": suggestion.suggested_timeline, "confidence": suggestion.confidence, "reasoning": _pipeline._normalize_reasoning(suggestion.reasoning), "status": suggestion.status, "created_project_id": str(created_project_id) if created_project_id else None, } row["project_name"] = created_project_name or row["project_name"] row["matched_project_id"] = created_project_id or row["matched_project_id"] row["priority"] = "high" if (item.project_score or 0) >= 0.85 else "medium" row["priority_rank"] = 4 if (item.project_score or 0) >= 0.85 else 3 if created_project_id and not row["target_url"]: row["target_url"] = f"/projects/{created_project_id}/board" row["title"] = self._build_project_preview_title( title=str(project_fields.get("title") or ""), description=str(project_fields.get("description") or ""), project_scope=str(project_fields.get("project_scope") or ""), ) row["summary"] = self._build_project_preview_summary( description=str(project_fields.get("description") or ""), project_scope=str(project_fields.get("project_scope") or ""), blocking_reason=item.blocking_reason, fallback_text=str(ai_output.get("normalized_text") or item.raw_message or ""), ) row["detail_summary"] = row["summary"] if not row.get("priority"): row["priority"] = "high" if (item.project_score or 0) >= 0.85 else "medium" row["priority_rank"] = 4 if (item.project_score or 0) >= 0.85 else 3 return row if (item.level or "").lower() == "command": operation_code = str(result_payload.get("operation_code") or "").strip() row["title"] = ( str(result_payload.get("result_summary") or "").strip() or operation_code or "受控动作" ) row["summary"] = str(item.blocking_reason or row["title"] or "").strip() row["priority"] = "high" if row["display_kind"] == "action_required" else "medium" row["priority_rank"] = 4 if row["display_kind"] == "action_required" else 2 if not row.get("next_step_text"): row["next_step_text"] = self._build_pending_operation_next_step(operation_code=operation_code) if result_payload: row["command"] = { "operation_code": operation_code or None, "executor": str(result_payload.get("executor") or "").strip() or None, "operation_payload": ( result_payload.get("operation_payload") if isinstance(result_payload.get("operation_payload"), dict) else None ), "operation_result": ( result_payload.get("operation_result") if isinstance(result_payload.get("operation_result"), dict) else None ), } return row signal = await self._get_signal_by_item(item.id) if signal: matched_project_id = _pipeline._safe_int(signal.matched_project_id) or _pipeline._safe_int(item.matched_project_id) matched_project_name = await self._load_project_name(matched_project_id) row["signal"] = { "id": str(signal.id), "title": signal.title, "signal_type": signal.signal_type, "priority": signal.priority, "summary": signal.summary, "reasoning": _pipeline._normalize_reasoning(signal.reasoning), "confidence": signal.confidence, "recommended_action": signal.recommended_action, "matched_project_id": signal.matched_project_id, "status": signal.status, } row["title"] = signal.title or "未命名信号" row["summary"] = signal.summary row["priority"] = signal.priority row["priority_rank"] = _pipeline.PROJECT_PRIORITY_ORDER.get(str(signal.priority or "").upper(), 1) row["matched_project_id"] = matched_project_id row["project_name"] = matched_project_name or row["project_name"] if matched_project_id and not row["target_url"]: row["target_url"] = f"/projects/{matched_project_id}/board" if not row.get("next_step_text"): if row["display_kind"] == "auto_done": row["next_step_text"] = "下一步可继续生成 charter、任务或通知负责人。" elif not matched_project_id: row["next_step_text"] = "补齐项目归属后,AI 将自动继续后续链路。" if not row.get("summary"): row["summary"] = _pipeline._compact_text(signal.content or item.raw_message, max_len=140) if include_history: history_rows = ( await self.db.execute( select(SignalEdit) .where( and_( SignalEdit.tenant_id == self.tenant_id, SignalEdit.signal_id == int(signal.id), ) ) .order_by(SignalEdit.edited_at.desc()) .limit(20) ) ).scalars().all() row["edit_history"] = [ { "field": history.field, "old_value": history.old_value, "new_value": history.new_value, "edited_by": str(history.edited_by) if history.edited_by else None, "edited_at": history.edited_at.isoformat() if history.edited_at else None, } for history in history_rows ] return row