ForcePilot/backend/server/routers/channels/route_binding_router.py
Kris af5b209be1 refactor(channel routers): 统一分页参数并优化接口实现
1.  新增统一分页常量定义,替换各路由零散的Query参数声明
2.  批量替换多个列表接口的分页参数为预设常量
3.  优化插件目录、消息搜索等接口的参数与文档
4.  修复死信导出接口的参数命名不一致问题
2026-07-11 15:55:45 +08:00

270 lines
12 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.

"""路由绑定管理 RouterRB-01~RB-05
提供渠道-Agent 路由绑定规则的 5 个 HTTP 端点,全部由 ``get_admin_user`` 守门,
仅管理员可访问。Router 不含任何业务逻辑,仅做协议翻译:将 HTTP 请求参数组装
为契约层命令/参数,调用 ``use_cases.route_binding_management`` 的类型化方法走
控制面管道,再通过 ``raiseOnControlFailure`` 转译失败、``serialize_control_data``
序列化成功结果(模板 A
鉴权策略:全部端点使用 ``get_admin_user`` 依赖,要求管理员或超级管理员角色。
角色校验由依赖函数完成router 内不做角色判断(规范 §4
路径设计:``/route-bindings`` 与 ``/route-bindings/resolve`` 静态路径先于
``/route-bindings/{binding_id}`` 动态路径声明(规范 §6.5),避免 ``resolve``
被捕获为 binding_id。子 router 不自行设置 prefix根前缀 ``/channels`` 由
``channels_router`` 聚合 router 统一追加。
端点清单:
- GET /route-bindings RB-01 list 列出路由绑定规则
- POST /route-bindings RB-02 create 创建路由绑定规则
- POST /route-bindings/resolve RB-05 resolve 测试路由解析
- PUT /route-bindings/{binding_id} RB-03 update 更新路由绑定规则
- DELETE /route-bindings/{binding_id} RB-04 delete 删除路由绑定规则
"""
from __future__ import annotations
from typing import Any
from fastapi import APIRouter, Depends, Query, Request
from pydantic import BaseModel, ConfigDict, Field, model_validator
from yuxi.channels.contract.dtos.channel import ChannelType
from yuxi.channels.contract.dtos.route import (
BindingContext,
RouteBindingFilter,
SaveRouteBindingCmd,
UpdateRouteBindingCmd,
)
from yuxi.channels.contract.errors import ValidationError
from yuxi.storage.postgres.models_business import User
from server.routers.channels import (
DEFAULT_LIMIT,
OFFSET,
build_operator,
get_channel_use_cases,
raiseOnControlFailure,
serialize_control_data,
)
from server.utils.auth_middleware import get_admin_user
route_binding_router = APIRouter(tags=["channels-route-binding"])
# ---------------- Request Schemas ----------------
class CreateRouteBindingRequest(BaseModel):
"""创建路由绑定规则请求。字段对齐 ``SaveRouteBindingCmd``。
``match_value`` 与 ``match_source`` 的组合约束由 ``_validate_match_value``
校验:``account`` / ``default`` tier 不允许携带 match_value``chat_type``
tier 的 match_value 必须为 ``p2p`` 或 ``group``。同时拒绝当前运行时尚未
实现 matcher 的实验性 tier``channel_session`` / ``channel_type``)。
"""
model_config = ConfigDict(frozen=True)
channel_type: ChannelType = Field(..., description="渠道类型(必填)")
account_id: str = Field(..., min_length=1, description="渠道账户 ID")
match_source: str = Field(..., min_length=1, description="匹配来源tier 名)")
match_value: str | None = Field(default=None, description="匹配值(按 tier 约束)")
agent_binding: str = Field(..., min_length=1, description="Agent 绑定agent slug")
description: str | None = Field(default=None, description="描述")
#: 当前运行时已实现 matcher 的 tier 集合;其余 tier 创建规则会被拒绝。
#: 与 factory._registerDefaultMatchers / route_match_registry.applyRule 保持一致。
_SUPPORTED_MATCH_SOURCES: frozenset[str] = frozenset(
{"session_key", "identity_id", "peer_id", "chat_type", "account", "default"}
)
@model_validator(mode="after")
def _validate_match_value(self) -> CreateRouteBindingRequest:
"""校验 match_source 与 match_value 的组合约束。
- 不支持 ``channel_session`` / ``channel_type`` 等实验性 tier
运行时尚未注册 matcher创建规则会导致永久失效。
- ``account`` / ``default`` tiermatch_value 必须为空。
- ``chat_type`` tiermatch_value 必须为 ``p2p`` 或 ``group``。
"""
if self.match_source not in self._SUPPORTED_MATCH_SOURCES:
raise ValidationError(
"match_source",
f"match_source '{self.match_source}' is not supported by runtime matcher",
)
if self.match_source in ("account", "default"):
if self.match_value:
raise ValidationError(
"match_value",
f"match_value must be empty for match_source={self.match_source}",
)
elif self.match_source == "chat_type":
if self.match_value not in ("p2p", "group"):
raise ValidationError(
"match_value",
"match_value must be 'p2p' or 'group' for match_source=chat_type",
)
return self
class UpdateRouteBindingRequest(BaseModel):
"""更新路由绑定规则请求。所有业务字段可选,仅传入字段被更新。
字段语义:未提供(不在请求体中)表示不修改,由 ``UpdateRouteBindingCmd``
的 ``None`` 默认值表示"不更新"。``binding_id`` 由路径参数提供,不在请求体中。
``version`` 必填取自上次加载GET/POST 返回的 ``version``),用于乐观锁
校验:服务端持久化版本不一致时返回 409客户端需重新拉取后重试。
"""
model_config = ConfigDict(frozen=True)
version: int = Field(..., description="乐观锁版本号(加载时获取,更新时回传)")
match_value: str | None = Field(default=None, description="匹配值")
agent_binding: str | None = Field(default=None, description="Agent 绑定agent slug")
enabled: bool | None = Field(default=None, description="是否启用")
description: str | None = Field(default=None, description="描述")
class ResolveRouteBindingRequest(BaseModel):
"""测试路由解析请求。字段对齐 ``BindingContext``。"""
model_config = ConfigDict(frozen=True)
session_key: str = Field(..., min_length=1, description="会话键")
channel_type: ChannelType = Field(..., description="渠道类型")
account_id: str = Field(..., min_length=1, description="渠道账户 ID")
chat_type: str = Field(..., description="会话类型p2p | group")
peer_id: str = Field(..., min_length=1, description="对端 ID")
unified_identity_id: str | None = Field(default=None, description="统一身份 ID")
conversation_id: str | None = Field(default=None, description="内部会话 ID")
# ---------------- Endpoints ----------------
@route_binding_router.get("/route-bindings", response_model=dict)
async def list_route_bindings(
request: Request,
channel_type: ChannelType | None = Query(default=None, description="按渠道类型过滤"),
account_id: str | None = Query(default=None, description="按账户 ID 过滤"),
match_source: str | None = Query(default=None, description="按匹配来源过滤"),
enabled: bool | None = Query(default=None, description="按启用状态过滤"),
agent_binding: str | None = Query(default=None, description="按绑定 Agent 过滤"),
limit: int = DEFAULT_LIMIT,
offset: int = OFFSET,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""列出路由绑定规则RB-01
支持按渠道类型 / 账户 / 匹配来源 / 启用状态 / 绑定 Agent 过滤,分页返回。
对应控制面操作 ``route_binding/list``。
"""
operator = build_operator(current_user, request)
filter = RouteBindingFilter(
channel_type=channel_type,
account_id=account_id,
match_source=match_source,
enabled=enabled,
agent_binding=agent_binding,
)
result = await use_cases.route_binding_management.listRouteBindings(operator, filter, limit=limit, offset=offset)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@route_binding_router.post("/route-bindings", response_model=dict)
async def create_route_binding(
payload: CreateRouteBindingRequest,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""创建路由绑定规则RB-02
对应控制面操作 ``route_binding/create``。
"""
operator = build_operator(current_user, request)
cmd = SaveRouteBindingCmd(
channel_type=payload.channel_type,
account_id=payload.account_id,
match_source=payload.match_source,
match_value=payload.match_value,
agent_binding=payload.agent_binding,
description=payload.description,
)
result = await use_cases.route_binding_management.createRouteBinding(operator, cmd)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@route_binding_router.post("/route-bindings/resolve", response_model=dict)
async def resolve_route_binding(
payload: ResolveRouteBindingRequest,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""测试路由解析RB-05
传入 ``BindingContext`` 字段,返回匹配到的路由绑定规则(含匹配来源、
命中层级等诊断元数据)。对应控制面操作 ``route_binding/resolve``。
"""
operator = build_operator(current_user, request)
ctx = BindingContext(
session_key=payload.session_key,
channel_type=payload.channel_type,
account_id=payload.account_id,
chat_type=payload.chat_type,
peer_id=payload.peer_id,
unified_identity_id=payload.unified_identity_id,
conversation_id=payload.conversation_id,
)
result = await use_cases.route_binding_management.resolveRouteBinding(operator, ctx)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@route_binding_router.put("/route-bindings/{binding_id}", response_model=dict)
async def update_route_binding(
binding_id: str,
payload: UpdateRouteBindingRequest,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""更新路由绑定规则RB-03
``binding_id`` 由路径参数提供,请求体字段全部可选,仅传入字段被更新。
对应控制面操作 ``route_binding/update``。
"""
operator = build_operator(current_user, request)
cmd = UpdateRouteBindingCmd(
binding_id=binding_id,
expected_version=payload.version,
match_value=payload.match_value,
agent_binding=payload.agent_binding,
enabled=payload.enabled,
description=payload.description,
)
result = await use_cases.route_binding_management.updateRouteBinding(operator, cmd)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@route_binding_router.delete("/route-bindings/{binding_id}", response_model=dict)
async def delete_route_binding(
binding_id: str,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""删除路由绑定规则RB-04
对应控制面操作 ``route_binding/delete``。
"""
operator = build_operator(current_user, request)
result = await use_cases.route_binding_management.deleteRouteBinding(operator, binding_id)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}