```
feat: 添加飞书集成和审计API密钥认证 - 在数据库配置中添加飞书模型导入 - 添加审计API密钥配置项和认证中间件 - 实现飞书事件重复处理防止机制 - 为审批路由添加API密钥认证 - 优化AI适配器错误处理并添加JSON解析异常捕获 - 更新测试用例以包含新的认证和事件处理逻辑 ```
This commit is contained in:
@@ -11,6 +11,12 @@ class FeishuMessageType(StrEnum):
|
||||
|
||||
|
||||
class FeishuPayloadKey(StrEnum):
|
||||
HEADER = "header"
|
||||
EVENT = "event"
|
||||
EVENT_ID = "event_id"
|
||||
EVENT_TYPE = "event_type"
|
||||
MESSAGE = "message"
|
||||
MESSAGE_ID = "message_id"
|
||||
RECEIVE_ID = "receive_id"
|
||||
RECEIVE_ID_TYPE = "receive_id_type"
|
||||
MESSAGE_TYPE = "msg_type"
|
||||
@@ -38,3 +44,5 @@ FEISHU_RECEIVE_ID_MISSING = "receive_id or FEISHU_DEFAULT_CHAT_ID is required"
|
||||
FEISHU_SUCCESS_CODE = 0
|
||||
FEISHU_DEFAULT_TOKEN_EXPIRE_SECONDS = 7200
|
||||
FEISHU_TOKEN_EXPIRE_SAFETY_SECONDS = 300
|
||||
FEISHU_WEBHOOK_EVENT_ACTION = "webhook_event"
|
||||
FEISHU_LONG_CONNECTION_EVENT_ACTION = "long_connection_event"
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.constants import ActorValue
|
||||
from app.modules.audit.constants import AuditSource
|
||||
from app.modules.audit.schemas import AuditLogCreate
|
||||
from app.modules.feishu.commands import FeishuCommandService
|
||||
from app.modules.feishu.constants import FeishuCommandKey
|
||||
from app.modules.feishu.constants import (
|
||||
FEISHU_LONG_CONNECTION_EVENT_ACTION,
|
||||
FEISHU_WEBHOOK_EVENT_ACTION,
|
||||
FeishuCommandKey,
|
||||
FeishuPayloadKey,
|
||||
)
|
||||
from app.modules.feishu.models import FeishuEventReceipt
|
||||
from app.modules.feishu.service import FeishuService
|
||||
|
||||
FEISHU_EVENT_ACTIONS = {
|
||||
"webhook": FEISHU_WEBHOOK_EVENT_ACTION,
|
||||
"long_connection": FEISHU_LONG_CONNECTION_EVENT_ACTION,
|
||||
}
|
||||
|
||||
|
||||
class FeishuEventService:
|
||||
"""Handle Feishu message events from webhook or long connection."""
|
||||
@@ -25,11 +37,16 @@ class FeishuEventService:
|
||||
auto_reply: bool = True,
|
||||
) -> dict[str, Any]:
|
||||
self.feishu.verify_event(payload)
|
||||
event_identity = _event_identity(payload, source)
|
||||
if event_identity and not self._register_event(event_identity):
|
||||
return {"ok": True, "handled": False, "duplicate": True}
|
||||
self.feishu.audit.log(
|
||||
AuditLogCreate(
|
||||
actor=ActorValue.FEISHU,
|
||||
source=AuditSource.FEISHU,
|
||||
action=f"{source}_event",
|
||||
action=FEISHU_EVENT_ACTIONS.get(source, FEISHU_WEBHOOK_EVENT_ACTION),
|
||||
target_type=source,
|
||||
target_id=event_identity.get("event_key") if event_identity else None,
|
||||
request_payload=payload,
|
||||
response_payload={"accepted": True},
|
||||
)
|
||||
@@ -44,3 +61,40 @@ class FeishuEventService:
|
||||
auto_reply=auto_reply,
|
||||
)
|
||||
return {"ok": True, "handled": True, "result": result}
|
||||
|
||||
def _register_event(self, event_identity: dict[str, str | None]) -> bool:
|
||||
receipt = FeishuEventReceipt(
|
||||
event_key=str(event_identity["event_key"]),
|
||||
source=str(event_identity["source"]),
|
||||
event_id=event_identity.get("event_id"),
|
||||
message_id=event_identity.get("message_id"),
|
||||
)
|
||||
self.db.add(receipt)
|
||||
try:
|
||||
self.db.flush()
|
||||
except IntegrityError:
|
||||
self.db.rollback()
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _event_identity(payload: dict[str, Any], source: str) -> dict[str, str | None] | None:
|
||||
header = payload.get(FeishuPayloadKey.HEADER) or {}
|
||||
event = payload.get(FeishuPayloadKey.EVENT) or {}
|
||||
message = event.get(FeishuPayloadKey.MESSAGE) or {}
|
||||
event_id = header.get(FeishuPayloadKey.EVENT_ID)
|
||||
message_id = message.get(FeishuPayloadKey.MESSAGE_ID)
|
||||
stable_id = event_id or message_id
|
||||
if not stable_id:
|
||||
return None
|
||||
event_type = header.get(FeishuPayloadKey.EVENT_TYPE)
|
||||
event_key = ":".join(
|
||||
str(part)
|
||||
for part in (source, event_type or FeishuPayloadKey.EVENT, stable_id)
|
||||
)
|
||||
return {
|
||||
"event_key": event_key,
|
||||
"source": source,
|
||||
"event_id": str(event_id) if event_id else None,
|
||||
"message_id": str(message_id) if message_id else None,
|
||||
}
|
||||
|
||||
18
app/modules/feishu/models.py
Normal file
18
app/modules/feishu/models.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, Integer, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.db_base import Base
|
||||
from app.core.time import utc_now
|
||||
|
||||
|
||||
class FeishuEventReceipt(Base):
|
||||
__tablename__ = "feishu_event_receipts"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
event_key: Mapped[str] = mapped_column(String(256), unique=True, index=True)
|
||||
source: Mapped[str] = mapped_column(String(64), index=True)
|
||||
event_id: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True)
|
||||
message_id: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True)
|
||||
received_at: Mapped[datetime] = mapped_column(DateTime, default=utc_now, index=True)
|
||||
Reference in New Issue
Block a user