ForcePilot/backend/package/yuxi/channel/middlewares/inbound.py
Kris bab30f2715
Some checks failed
Deploy VitePress site to Pages / build (push) Has been cancelled
Ruff Format Check / Ruff Format & Lint (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
feat:0715
2026-07-15 12:30:58 +08:00

185 lines
6.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""默认入站中间件实现(聚合文件)。"""
from __future__ import annotations
from typing import TYPE_CHECKING
from yuxi.channel.constants import InboundRejectionReason
from yuxi.channel.middlewares.protocols import InboundResult
from yuxi.storage.postgres.manager import pg_manager
from yuxi.utils.logging_config import logger
if TYPE_CHECKING:
from collections.abc import Awaitable, Callable
from sqlalchemy.ext.asyncio import AsyncSession
from yuxi.channel.message.dedupe import MessageDeduper
from yuxi.channel.middlewares.protocols import InboundContext
from yuxi.channel.plugins.protocol import BindingRoute, InboundMessage
from yuxi.channel.routing.router import BindingRouter
from yuxi.channel.security.policy import SecurityPolicy
from yuxi.channel.session.manager import SessionManager
from yuxi.storage.postgres.model_channel import ChannelSession
class DedupeMiddleware:
"""基于 Redis 的入站消息幂等去重。"""
name = "dedupe"
default_order = 100
def __init__(self, deduper: MessageDeduper) -> None:
self._deduper = deduper
async def process(
self,
ctx: InboundContext,
next_mw: Callable[[], Awaitable[InboundResult]],
) -> InboundResult:
if await self._deduper.is_processed(ctx.inbound.channel_message_id):
return InboundResult(accepted=False, reason=InboundRejectionReason.DUPLICATE)
return await next_mw()
class SecurityMiddleware:
"""调用安全策略链,按结果决定是否继续后续中间件。"""
name = "security"
default_order = 200
def __init__(self, security_policy: SecurityPolicy, deduper: MessageDeduper) -> None:
self._security = security_policy
self._deduper = deduper
async def process(
self,
ctx: InboundContext,
next_mw: Callable[[], Awaitable[InboundResult]],
) -> InboundResult:
from yuxi.channel.metrics import channel_rate_limited_total
security_result = await self._security.check(ctx.config, ctx.plugin, ctx.inbound)
if security_result.allowed:
return await next_mw()
if security_result.reason == InboundRejectionReason.RATE_LIMITED:
channel_rate_limited_total.inc({"channel_type": ctx.channel_type, "account_id": ctx.account_id})
logger.bind(
event="channel_message_rejected",
channel_type=ctx.channel_type,
account_id=ctx.account_id,
session_key=ctx.inbound.session_key,
message_id=ctx.inbound.channel_message_id,
sender_id=ctx.inbound.sender_id,
reason=security_result.reason,
).warning("Channel inbound message rejected by security policy")
if security_result.reason == InboundRejectionReason.DM_PAIRING_REQUIRED and security_result.pairing_code:
return InboundResult(
accepted=False,
reason=security_result.reason,
pairing_code=security_result.pairing_code,
qr_content=security_result.qr_content,
qr_reply=security_result.qr_reply,
)
# 限流后外部平台会重试,清除去重标记避免重试被误判为 DUPLICATE。
if security_result.reason == InboundRejectionReason.RATE_LIMITED:
await self._deduper.safe_clear_processed(ctx.inbound.channel_message_id)
return InboundResult(
accepted=False,
reason=security_result.reason or InboundRejectionReason.SECURITY_REJECTED,
)
class TransactionMiddleware:
"""开启数据库事务并将 AsyncSession 注入上下文。"""
name = "transaction"
default_order = 300
def __init__(self, deduper: MessageDeduper) -> None:
self._deduper = deduper
async def process(
self,
ctx: InboundContext,
next_mw: Callable[[], Awaitable[InboundResult]],
) -> InboundResult:
try:
async with pg_manager.get_async_session_context() as db:
ctx.db = db
return await next_mw()
except Exception:
# 事务回滚在 async context manager 异常退出时自动完成。
# 清除去重标记,使外部平台收到非 2xx 后重试不会被误判为 DUPLICATE。
await self._deduper.safe_clear_processed(ctx.inbound.channel_message_id)
raise
class SessionMiddleware:
"""解析或创建 ChannelSession并将结果注入上下文。"""
name = "session"
default_order = 400
def __init__(self, session_manager: SessionManager) -> None:
self._session_manager = session_manager
async def process(
self,
ctx: InboundContext,
next_mw: Callable[[], Awaitable[InboundResult]],
) -> InboundResult:
session, route = await self._session_manager.resolve(ctx.config, ctx.plugin, ctx.inbound, ctx.db)
ctx.session = session
ctx.route = route
return await next_mw()
class RouteMiddleware:
"""在 session 未携带路由时解析 BindingRoute。"""
name = "route"
default_order = 500
def __init__(self, binding_router: BindingRouter) -> None:
self._binding_router = binding_router
async def process(
self,
ctx: InboundContext,
next_mw: Callable[[], Awaitable[InboundResult]],
) -> InboundResult:
if ctx.route is None:
ctx.route = await self._binding_router.resolve(ctx.session, ctx.config, ctx.plugin, ctx.inbound, ctx.db)
return await next_mw()
class CreateRunMiddleware:
"""创建 AgentRun 并记录消息;作为默认入站链的终点,不再调用 next_mw。"""
name = "create_run"
default_order = 600
def __init__(
self,
create_run: Callable[[ChannelSession, BindingRoute, InboundMessage, AsyncSession], Awaitable[dict]],
record_message: Callable[[ChannelSession, InboundMessage, AsyncSession], Awaitable[None]],
) -> None:
self._create_run = create_run
self._record_message = record_message
async def process(
self,
ctx: InboundContext,
next_mw: Callable[[], Awaitable[InboundResult]],
) -> InboundResult:
run_result = await self._create_run(ctx.session, ctx.route, ctx.inbound, ctx.db)
ctx.run_result = run_result
await self._record_message(ctx.session, ctx.inbound, ctx.db)
return InboundResult(accepted=True, run_id=run_result.get("run_id"))