PMFlow/backend/app/services/integrations/feishu_signal_pipeline/mixin_serialization.py

205 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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._is_placeholder_project_text(project_fields["title"])
or self._is_placeholder_project_text(project_fields["description"])
or self._is_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