PMFlow/backend/app/services/feishu_outbox/service.py

98 lines
3.4 KiB
Python

"""飞书 outbox 入队服务。"""
from __future__ import annotations
import hashlib
from typing import Optional
from sqlalchemy import and_, select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
from ...models import FeishuReplyOutbox
class FeishuOutboxService:
"""复用 reply_hash 幂等策略的 outbox 写入入口。"""
@staticmethod
def _build_reply_hash(*, source_message_id: Optional[str], message_text: str) -> Optional[str]:
source = str(source_message_id or "").strip()
body = str(message_text or "").strip()
if not source or not body:
return None
raw = f"{source}{body}".encode("utf-8")
return hashlib.md5(raw, usedforsecurity=False).hexdigest()
@classmethod
async def enqueue(
cls,
db: AsyncSession,
*,
tenant_id: int,
chat_id: str,
text: str,
source_message_id: Optional[str],
dedupe_seed: Optional[str] = None,
trace_id: Optional[str] = None,
) -> Optional[int]:
normalized_chat_id = str(chat_id or "").strip()[:128]
if not normalized_chat_id:
return None
normalized_text = str(text or "").strip()[:2000] or "已收到,正在处理。"
normalized_source_message_id = (str(source_message_id or "").strip()[:128] or None)
hash_source = normalized_source_message_id or (str(dedupe_seed or "").strip()[:128] or None)
reply_hash = cls._build_reply_hash(source_message_id=hash_source, message_text=normalized_text)
if reply_hash:
sent_exists = (
await db.execute(
select(FeishuReplyOutbox.id)
.where(
and_(
FeishuReplyOutbox.tenant_id == int(tenant_id),
FeishuReplyOutbox.reply_hash == reply_hash,
FeishuReplyOutbox.status == "sent",
)
)
.limit(1)
)
).scalar_one_or_none()
if sent_exists is not None:
return int(sent_exists)
row = FeishuReplyOutbox(
tenant_id=int(tenant_id),
trace_id=(str(trace_id or "").strip()[:64] or None),
source_message_id=normalized_source_message_id,
chat_id=normalized_chat_id,
message_text=normalized_text,
reply_hash=reply_hash,
status="pending",
attempts=0,
next_retry_at=None,
)
try:
async with db.begin_nested():
db.add(row)
await db.flush()
except IntegrityError:
if reply_hash:
sent_exists = (
await db.execute(
select(FeishuReplyOutbox.id)
.where(
and_(
FeishuReplyOutbox.tenant_id == int(tenant_id),
FeishuReplyOutbox.reply_hash == reply_hash,
FeishuReplyOutbox.status == "sent",
)
)
.limit(1)
)
).scalar_one_or_none()
if sent_exists is not None:
return int(sent_exists)
raise
return int(row.id) if row.id is not None else None