PMFlow/backend/app/services/feishu_signal_pipeline/mixin_persistence_lookup.py

155 lines
5.9 KiB
Python

"""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