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

@@ -1,3 +1,4 @@
import json
from datetime import datetime
from uuid import uuid4
@@ -5,9 +6,12 @@ import pytest
from sqlalchemy import create_engine, func, select
from sqlalchemy.orm import Session
from app.application.feishu.commands import FeishuCommandService
from app.application.feishu.handlers.subscriptions import handle_subscription_command
from app.core.config import get_settings
from app.core.database import Base
from app.modules.feishu.constants import FeishuCommandName
from app.modules.ai_agent.service import AIService
from app.modules.feishu.constants import FeishuCommandKey, FeishuCommandName
from app.modules.feishu.service import FeishuService
from app.modules.feishu_users.constants import FeishuUserRole
from app.modules.feishu_users.models import FeishuUser
@@ -91,6 +95,131 @@ def test_create_private_subscription_replies_with_normalized_plan() -> None:
engine.dispose()
def test_rich_text_event_routes_subscription_instead_of_fallback_ai(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setenv("FEISHU_USER_FEATURES_ENABLED", "true")
get_settings.cache_clear()
engine = create_engine("sqlite://")
Base.metadata.create_all(engine)
try:
with Session(engine) as db:
user = _user(db, suffix="rich-text-command")
principal = FeishuPrincipal.from_user(
user,
chat_id="private-chat",
chat_type="p2p",
)
text = "订阅 每隔 15 分钟:给我一句简短的工作提醒"
payload = {
"header": {"tenant_key": user.tenant_key},
"event": {
"message": {
"chat_id": "private-chat",
"chat_type": "p2p",
"content": json.dumps(
{
"content": [
[
{
"tag": "text",
"text": text,
"style": [],
}
]
]
},
ensure_ascii=False,
),
},
"sender": {"sender_id": {"open_id": user.open_id}},
},
}
commands = FeishuCommandService(db)
extracted = commands.extract_event_command(payload)
assert extracted is not None
assert extracted[FeishuCommandKey.TEXT] == text
result = commands.handle_text(
extracted[FeishuCommandKey.TEXT],
chat_id="private-chat",
principal=principal,
auto_reply=False,
)
subscription = db.scalar(select(PushSubscription))
assert result["command"] == FeishuCommandName.SUBSCRIPTION_CREATE
assert "计划:每隔 15 分钟" in result["content"]
assert subscription is not None
assert subscription.prompt == "给我一句简短的工作提醒"
finally:
get_settings.cache_clear()
engine.dispose()
def test_rich_text_event_routes_help_instead_of_fallback_ai(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setenv("FEISHU_USER_FEATURES_ENABLED", "true")
monkeypatch.setattr(
AIService,
"ask_personalized",
lambda *args, **kwargs: pytest.fail("Rich-text help command reached AI fallback"),
)
get_settings.cache_clear()
engine = create_engine("sqlite://")
Base.metadata.create_all(engine)
try:
with Session(engine) as db:
user = _user(db, suffix="rich-text-help")
principal = FeishuPrincipal.from_user(
user,
chat_id="private-chat",
chat_type="p2p",
)
payload = {
"header": {"tenant_key": user.tenant_key},
"event": {
"message": {
"chat_id": "private-chat",
"chat_type": "p2p",
"content": json.dumps(
{
"content": [
[
{
"tag": "text",
"text": "帮助",
"style": [],
}
]
]
},
ensure_ascii=False,
),
},
"sender": {"sender_id": {"open_id": user.open_id}},
},
}
commands = FeishuCommandService(db)
extracted = commands.extract_event_command(payload)
assert extracted is not None
assert extracted[FeishuCommandKey.TEXT] == "帮助"
result = commands.handle_text(
extracted[FeishuCommandKey.TEXT],
chat_id="private-chat",
principal=principal,
auto_reply=False,
)
assert result["command"] == FeishuCommandName.HELP
assert "你可以使用:" in result["content"]
finally:
get_settings.cache_clear()
engine.dispose()
def test_group_subscription_uses_current_verified_chat_and_requires_admin() -> None:
engine = create_engine("sqlite://")
Base.metadata.create_all(engine)