feat(feishu): 添加飞书入站事件inbox和混合数据库协调功能

- 实现飞书入站事件持久化inbox机制,支持状态管理、租约锁定和重试退避
- 添加混合数据库基线协调工具,确保平台PostgreSQL结构安全对齐
- 增加运行组件心跳检测和readiness就绪检查机制
- 实现app_ticket事件的安全轮换和验证处理
- 添加生产环境运行编排和fail-closed安全机制
- 支持webhook快速确认和长连接独立进程处理
- 完善个人数据擦除时的待处理事件清理功能
```
This commit is contained in:
2026-07-27 17:14:37 +08:00
parent d7db84571d
commit eb8267ed18
61 changed files with 8703 additions and 183 deletions

View File

@@ -10,7 +10,7 @@ from sqlalchemy import create_engine, func, select
from sqlalchemy.orm import Session, sessionmaker
from sqlalchemy.pool import StaticPool
from app.application.feishu.events import FeishuEventService
from app.application.feishu.events import FeishuEventService, _identifier_digest
from app.core.config import get_settings
from app.core.database import Base
from app.modules.audit.models import AuditLog
@@ -18,6 +18,7 @@ from app.modules.feishu import long_connection
from app.modules.feishu.app_tickets import FeishuAppTicketService
from app.modules.feishu.constants import FeishuEventSource
from app.modules.feishu.models import FeishuAppTicket, FeishuEventReceipt
from app.modules.feishu.services import FeishuInboundService
@pytest.fixture
@@ -26,6 +27,7 @@ def session_factory(
) -> Iterator[sessionmaker[Session]]:
monkeypatch.setenv("FEISHU_APP_ID", "cli-ticket-app")
monkeypatch.setenv("FEISHU_APP_SECRET", "ticket-app-secret")
monkeypatch.setenv("FEISHU_EVENT_TRANSPORT", "long_connection")
monkeypatch.setenv("FEISHU_VERIFICATION_TOKEN", "ticket-token")
monkeypatch.setenv("FEISHU_USER_FEATURES_ENABLED", "false")
get_settings.cache_clear()
@@ -135,6 +137,9 @@ def test_verified_ticket_is_deduplicated_rotated_and_never_leaked(
assert current.received_at >= first_received_at
assert db.scalar(select(func.count()).select_from(FeishuAppTicket)) == 1
assert db.scalar(select(func.count()).select_from(FeishuEventReceipt)) == 2
receipts = list(db.execute(select(FeishuEventReceipt)).scalars())
assert all(item.status == "succeeded" for item in receipts)
assert all(item.payload is None for item in receipts)
assert rotated_ticket not in json.dumps(rotated, ensure_ascii=False)
audits = list(db.execute(select(AuditLog)).scalars())
@@ -196,6 +201,59 @@ def test_only_verified_matching_app_ticket_events_can_write(
assert db.scalar(select(FeishuAppTicket)) is None
def test_app_ticket_write_and_receipt_success_are_atomic(
session_factory: sessionmaker[Session],
monkeypatch: pytest.MonkeyPatch,
) -> None:
payload = _v2_ticket_event("atomic-app-ticket", "atomic-ticket-secret")
original_mark_success = FeishuInboundService._mark_success
fail_before_commit = True
def mark_success(
atomic_db: Session,
event_key: str,
lock_owner: str,
current: Any,
) -> None:
nonlocal fail_before_commit
if fail_before_commit:
fail_before_commit = False
raise RuntimeError("simulated crash before atomic app-ticket commit")
original_mark_success(atomic_db, event_key, lock_owner, current)
monkeypatch.setattr(
FeishuInboundService,
"_mark_success",
staticmethod(mark_success),
)
with session_factory() as db:
service = FeishuEventService(db)
with pytest.raises(HTTPException) as first_error:
service._handle_verified_event(
payload,
source=FeishuEventSource.WEBHOOK,
)
assert first_error.value.status_code == 503
assert db.scalar(select(FeishuAppTicket)) is None
receipt = db.scalar(select(FeishuEventReceipt))
assert receipt is not None
assert receipt.status == "retry"
result = service._handle_verified_event(
payload,
source=FeishuEventSource.WEBHOOK,
)
assert result == {"ok": True, "handled": True}
assert FeishuAppTicketService(db).get_ticket("cli-ticket-app") == (
"atomic-ticket-secret"
)
db.refresh(receipt)
assert receipt.status == "succeeded"
assert receipt.attempt_count == 2
def test_v1_app_ticket_payload_uses_uuid_receipt(
session_factory: sessionmaker[Session],
) -> None:
@@ -209,8 +267,18 @@ def test_v1_app_ticket_payload_uses_uuid_receipt(
receipt = db.scalar(select(FeishuEventReceipt))
assert result == {"ok": True, "handled": True}
assert receipt is not None
assert receipt.event_id == "v1-ticket-uuid"
assert receipt.event_key == "cli-ticket-app:app_ticket:v1-ticket-uuid"
assert receipt.event_id == _identifier_digest(
"v1-ticket-uuid",
"event-id",
)
assert receipt.event_key == _identifier_digest(
"cli-ticket-app:app_ticket:v1-ticket-uuid",
"event-key",
)
assert "v1-ticket-uuid" not in receipt.event_id
assert "cli-ticket-app" not in receipt.event_key
assert receipt.status == "succeeded"
assert receipt.payload is None
assert FeishuAppTicketService(db).get_ticket("cli-ticket-app") == ticket
assert ticket not in json.dumps(result, ensure_ascii=False)
@@ -257,6 +325,7 @@ def test_long_connection_registers_custom_app_ticket_handler(
monkeypatch.setitem(sys.modules, "lark_oapi", fake_lark)
monkeypatch.setenv("FEISHU_APP_ID", "cli-ticket-app")
monkeypatch.setenv("FEISHU_APP_SECRET", "ticket-app-secret")
monkeypatch.setenv("FEISHU_EVENT_TRANSPORT", "long_connection")
monkeypatch.setenv("FEISHU_VERIFICATION_TOKEN", "ticket-token")
get_settings.cache_clear()
try: