PMFlow/backend/app/services/feishu_integration_service/mixin_message_payload.py

260 lines
12 KiB
Python

from __future__ import annotations
from .shared import *
class FeishuMessagePayloadMixin:
@staticmethod
def _build_attachment_placeholder_text(attachments: List[Dict[str, Any]]) -> str:
if not attachments:
return ""
names: List[str] = []
for item in attachments[:3]:
attachment_type = str(item.get("attachment_type") or "附件").strip()
file_name = str(item.get("file_name") or "").strip()
names.append(file_name or attachment_type)
suffix = "、".join(names)
return f"[附件消息] {suffix}" if suffix else "[附件消息]"
@staticmethod
def _normalize_ingress_attachment_records(
items: List[Dict[str, Any]],
*,
occurred_at: Optional[str] = None,
source_message_id: Optional[str] = None,
source_chat_id: Optional[str] = None,
) -> List[Dict[str, Any]]:
normalized: List[Dict[str, Any]] = []
seen_ids: set[str] = set()
for idx, raw in enumerate(items):
if not isinstance(raw, dict):
continue
attachment_type = str(raw.get("attachment_type") or raw.get("type") or "file").strip().lower()
if attachment_type in {"img", "picture"}:
attachment_type = "image"
if attachment_type in {"doc", "docs"}:
attachment_type = "document"
if attachment_type not in {"image", "file", "media", "document"}:
attachment_type = "file"
file_key = str(raw.get("file_key") or "").strip() or None
image_key = str(raw.get("image_key") or "").strip() or None
media_id = str(raw.get("media_id") or "").strip() or None
download_url = str(raw.get("download_url") or raw.get("url") or "").strip() or None
attachment_id = (
str(raw.get("attachment_id") or raw.get("id") or file_key or image_key or media_id or "")
.strip()
)
if not attachment_id:
attachment_id = f"attachment-{idx + 1}"
if not any([file_key, image_key, media_id, download_url]):
continue
if attachment_id in seen_ids:
continue
seen_ids.add(attachment_id)
file_size = raw.get("file_size") if raw.get("file_size") is not None else raw.get("size")
try:
file_size = int(str(file_size).strip()) if file_size is not None else None
except (TypeError, ValueError):
file_size = None
file_name = (
str(raw.get("file_name") or raw.get("name") or raw.get("title") or "").strip()
or f"{attachment_type}-{attachment_id}"
)
mime_type = str(raw.get("mime_type") or raw.get("mime") or "").strip() or None
normalized.append(
{
"id": attachment_id,
"attachment_id": attachment_id,
"attachment_type": attachment_type,
"file_name": file_name,
"mime_type": mime_type,
"file_size": file_size,
"file_key": file_key,
"image_key": image_key,
"media_id": media_id,
"download_url": download_url,
"source": str(raw.get("source") or "feishu").strip() or "feishu",
"occurred_at": str(raw.get("occurred_at") or occurred_at or "").strip() or None,
"source_message_id": str(raw.get("source_message_id") or source_message_id or "").strip() or None,
"source_chat_id": str(raw.get("source_chat_id") or source_chat_id or "").strip() or None,
}
)
return normalized
def _extract_message_payload(self, event: Dict[str, Any]) -> Dict[str, Any]:
"""提取消息文本与附件元数据。"""
message = event.get("message", {})
if not isinstance(message, dict) and isinstance(event.get("event", {}), dict):
message = event.get("event", {}).get("message", {})
if not isinstance(message, dict):
return {"text": None, "attachments": [], "message_type": ""}
message_type = str(message.get("message_type") or "").strip().lower()
content = message.get("content", "")
content_json: Dict[str, Any] = {}
if isinstance(content, dict):
content_json = content
elif isinstance(content, str) and content.strip():
try:
parsed = json.loads(content)
if isinstance(parsed, dict):
content_json = parsed
except Exception:
if message_type == "text":
return {"text": content.strip() or None, "attachments": [], "message_type": message_type}
text_parts: List[str] = []
attachments: List[Dict[str, Any]] = []
if message_type == "text":
text = str(content_json.get("text") or "").strip()
return {"text": text or None, "attachments": [], "message_type": message_type}
if message_type == "post":
post = content_json.get("post", {}) if isinstance(content_json, dict) else {}
zh_cn = post.get("zh_cn", {}) if isinstance(post, dict) else {}
elements = zh_cn.get("content", []) if isinstance(zh_cn, dict) else []
if (not elements) and isinstance(content_json, dict):
direct_elements = content_json.get("content", [])
if isinstance(direct_elements, list):
elements = direct_elements
for row in elements:
row_items = row if isinstance(row, list) else [row]
for elem in row_items:
if not isinstance(elem, dict):
continue
tag = str(elem.get("tag") or "").strip().lower()
if tag == "text":
text = str(elem.get("text") or "").strip()
if text:
text_parts.append(text)
elif tag in {"img", "image"}:
attachments.append(
{
"attachment_type": "image",
"image_key": elem.get("image_key"),
"file_name": elem.get("title") or "图片附件",
}
)
elif tag in {"file", "media"}:
attachments.append(
{
"attachment_type": "media" if tag == "media" else "file",
"file_key": elem.get("file_key"),
"media_id": elem.get("media_id"),
"file_name": elem.get("file_name") or elem.get("title"),
"file_size": elem.get("file_size"),
}
)
elif message_type in {"image", "file", "media"}:
attachments.append(
{
"attachment_type": message_type,
"image_key": content_json.get("image_key"),
"file_key": content_json.get("file_key"),
"media_id": content_json.get("media_id"),
"file_name": content_json.get("file_name") or content_json.get("title"),
"file_size": content_json.get("file_size"),
"mime_type": content_json.get("file_type") or content_json.get("mime_type"),
}
)
text = str(content_json.get("text") or content_json.get("caption") or "").strip()
if text:
text_parts.append(text)
elif isinstance(content_json, dict):
guessed_keys = {
"image_key": content_json.get("image_key"),
"file_key": content_json.get("file_key"),
"media_id": content_json.get("media_id"),
"download_url": content_json.get("download_url"),
}
if any(v for v in guessed_keys.values()):
attachments.append(
{
"attachment_type": "file",
"image_key": guessed_keys.get("image_key"),
"file_key": guessed_keys.get("file_key"),
"media_id": guessed_keys.get("media_id"),
"download_url": guessed_keys.get("download_url"),
"file_name": content_json.get("file_name") or content_json.get("title"),
"file_size": content_json.get("file_size"),
"mime_type": content_json.get("mime_type"),
}
)
text = str(content_json.get("text") or "").strip()
if text:
text_parts.append(text)
normalized_attachments = self._normalize_ingress_attachment_records(attachments)
merged_text = " ".join([part for part in text_parts if part]).strip()
if not merged_text and normalized_attachments:
merged_text = self._build_attachment_placeholder_text(normalized_attachments)
return {
"text": merged_text or None,
"attachments": normalized_attachments,
"message_type": message_type,
}
def _extract_message_content(self, event: Dict[str, Any]) -> Optional[str]:
payload = self._extract_message_payload(event)
text = str(payload.get("text") or "").strip()
return text or None
async def _persist_signal_attachments_from_ingress(
self,
*,
signal_id: Any,
attachments: List[Dict[str, Any]],
) -> None:
if not attachments:
return
try:
signal_int = int(str(signal_id).strip())
except (TypeError, ValueError, AttributeError):
return
signal = (
await self.db.execute(
select(Signal).where(
and_(
Signal.tenant_id == int(self.tenant_id or 1),
Signal.id == signal_int,
)
)
)
).scalar_one_or_none()
if signal is None:
return
try:
payload = signal.strategic_analysis
strategic: Dict[str, Any] = {}
if isinstance(payload, dict):
strategic = dict(payload)
elif isinstance(payload, str) and payload.strip():
parsed = json.loads(payload)
if isinstance(parsed, dict):
strategic = parsed
source_context = strategic.get("source_context")
if not isinstance(source_context, dict):
source_context = {}
existing = source_context.get("attachments")
existing_rows = existing if isinstance(existing, list) else []
merged = self._normalize_ingress_attachment_records(
[*(row for row in existing_rows if isinstance(row, dict)), *attachments]
)
if not merged:
return
source_context["attachments"] = merged
strategic["source_context"] = source_context
signal.strategic_analysis = json.dumps(strategic, ensure_ascii=False)
await self.db.commit()
except Exception as exc:
print(f"[Feishu] persist signal attachments failed: signal_id={signal_int}, error={exc}")
try:
await self.db.rollback()
except Exception:
pass