155 lines
5.9 KiB
Python
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
|
|
|