201 lines
11 KiB
Python
201 lines
11 KiB
Python
"""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 not all(str(project_fields.get(field) or "").strip() for field in ("title", "description", "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
|