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