"""Feishu signal pipeline mixin.""" from __future__ import annotations from .shared import * class FeishuPipelinePersistenceLookupMixin: async def _resolve_user_id_by_name(self, name: Optional[str]) -> Optional[int]: key = (name or "").strip() if not key: return None user = ( await self.db.execute( select(User).where( and_( User.tenant_id == self.tenant_id, User.is_active.is_(True), func.lower(User.name) == key.lower(), ) ) ) ).scalar_one_or_none() if user: return int(user.id) fuzzy = ( await self.db.execute( select(User).where( and_( User.tenant_id == self.tenant_id, User.is_active.is_(True), User.name.ilike(f"%{key}%"), ) ).limit(1) ) ).scalar_one_or_none() return int(fuzzy.id) if fuzzy else None async def _fallback_user_id(self) -> Optional[int]: user = ( await self.db.execute( select(User.id).where( and_( User.tenant_id == self.tenant_id, User.is_active.is_(True), ) ).limit(1) ) ).scalar_one_or_none() return int(user) if user else None async def _resolve_chat_context_project(self, *, source_chat_id: str) -> dict[str, Any]: chat_id = (source_chat_id or "").strip() if not chat_id: return {"project_id": None, "ambiguous": False, "candidate_ids": [], "top_ratio": 0.0} recent_rows = ( await self.db.execute( select(FeishuItem.matched_project_id) .where( and_( FeishuItem.tenant_id == self.tenant_id, FeishuItem.source_chat_id == chat_id, FeishuItem.matched_project_id.is_not(None), FeishuItem.route_status.in_((ROUTE_STATUS_AUTO_DONE, "auto_processed", "confirmed")), ) ) .order_by(FeishuItem.id.desc()) .limit(20) ) ).all() project_ids = [int(row[0]) for row in recent_rows if row and row[0] is not None] if not project_ids: return {"project_id": None, "ambiguous": False, "candidate_ids": [], "top_ratio": 0.0} unique_ids = set(project_ids) candidate_ids = sorted(unique_ids, key=lambda pid: (-project_ids.count(pid), pid)) if len(unique_ids) == 1: return {"project_id": int(project_ids[0]), "ambiguous": False, "candidate_ids": candidate_ids, "top_ratio": 1.0} top_pid = max(unique_ids, key=lambda pid: project_ids.count(pid)) top_count = project_ids.count(top_pid) ratio = top_count / len(project_ids) if top_count >= 2 and ratio >= 0.6: return {"project_id": int(top_pid), "ambiguous": False, "candidate_ids": candidate_ids, "top_ratio": ratio} return {"project_id": None, "ambiguous": True, "candidate_ids": candidate_ids, "top_ratio": ratio} async def _infer_project_id_from_chat_context(self, *, source_chat_id: str) -> Optional[int]: resolution = await self._resolve_chat_context_project(source_chat_id=source_chat_id) return _safe_int(resolution.get("project_id")) async def _get_item(self, item_id: int) -> Optional[FeishuItem]: return ( await self.db.execute( select(FeishuItem).where( and_( FeishuItem.tenant_id == self.tenant_id, FeishuItem.id == int(item_id), ) ) ) ).scalar_one_or_none() async def _get_project_suggestion(self, item_id: int) -> Optional[ProjectSuggestion]: return ( await self.db.execute( select(ProjectSuggestion).where( and_( ProjectSuggestion.tenant_id == self.tenant_id, ProjectSuggestion.feishu_item_id == int(item_id), ) ) ) ).scalar_one_or_none() async def _get_signal_by_item(self, item_id: int) -> Optional[Signal]: return ( await self.db.execute( select(Signal).where( and_( Signal.tenant_id == self.tenant_id, Signal.feishu_item_id == int(item_id), ) ) ) ).scalar_one_or_none() def _dedupe_queue_items(self, rows: list[FeishuItem]) -> list[FeishuItem]: deduped: list[FeishuItem] = [] seen_signal_keys: set[str] = set() aggregated: dict[str, FeishuItem] = {} for row in rows: explicit_key = str(row.dedupe_key or "").strip() if explicit_key: existing = aggregated.get(explicit_key) if existing is None: aggregated[explicit_key] = row deduped.append(row) else: existing.occurrence_count = max(1, int(existing.occurrence_count or 1)) + max( 1, int(row.occurrence_count or 1) ) continue level = str(row.level or "").lower() if level != "signal": deduped.append(row) continue message_id = str(row.feishu_message_id or "").strip() key = message_id or f"{str(row.raw_message or '')[:120]}::{str(row.source_sender or '')}" if key in seen_signal_keys: continue seen_signal_keys.add(key) deduped.append(row) return deduped