260 lines
12 KiB
Python
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
|
|
|