refactor(channel routers): 统一分页参数并优化接口实现

1.  新增统一分页常量定义,替换各路由零散的Query参数声明
2.  批量替换多个列表接口的分页参数为预设常量
3.  优化插件目录、消息搜索等接口的参数与文档
4.  修复死信导出接口的参数命名不一致问题
This commit is contained in:
Kris 2026-07-11 15:55:45 +08:00
parent 202defb5f2
commit af5b209be1
15 changed files with 138 additions and 43 deletions

View File

@ -15,7 +15,7 @@ from datetime import datetime
from enum import Enum
from typing import Any, Literal
from fastapi import APIRouter, Depends, Request
from fastapi import APIRouter, Depends, Query, Request
from yuxi.channels.contract.dtos.common import Operator, OperatorRole
from yuxi.channels.contract.dtos.control import ControlResult
from yuxi.channels.contract.errors import (
@ -395,6 +395,37 @@ def parse_datetime(field: str, raw: str | None) -> datetime | None:
) from exc
# ------------------------------------------------------------------
# 统一分页参数预设
#
# channels 模块所有列表类接口共享以下分页档位,消除参数声明散乱、默认值
# 不一、上限阈值随意等问题。各预设按业务语义分档:
#
# - DEFAULT_LIMIT : 高频翻页场景(审核 / 搜索 / 目录 / 路由绑定)
# - LARGE_LIMIT : 列表查询场景(会话 / 配对 / 白名单 / 审计 / 消息历史)
# - ANALYTICS_LIMIT : 分析 Top N 场景
# - EXPORT_LIMIT : 导出场景(参数名建议用 export_limit 以区分语义)
# - SUGGESTION_LIMIT: 输入联想场景
# - OFFSET : 分页偏移(与 DEFAULT_LIMIT / LARGE_LIMIT 搭配)
# - CURSOR : 游标分页标记(与 DEFAULT_LIMIT 搭配,用于 directory 系列)
#
# 使用方式:在接口签名中以模块级 Query 对象作为默认值——
# async def list_xxx(limit: int = DEFAULT_LIMIT, offset: int = OFFSET, ...):
# ------------------------------------------------------------------
DEFAULT_LIMIT: int = Query(default=20, ge=1, le=100, description="分页大小1-100默认 20")
LARGE_LIMIT: int = Query(default=100, ge=1, le=1000, description="分页大小1-1000默认 100")
ANALYTICS_LIMIT: int = Query(default=100, ge=1, le=500, description="Top N 上限1-500默认 100")
EXPORT_LIMIT: int = Query(
default=10000,
ge=1,
le=100000,
description="导出条目上限1-100000默认 10000",
)
SUGGESTION_LIMIT: int = Query(default=10, ge=1, le=20, description="建议数量1-20默认 10")
OFFSET: int = Query(default=0, ge=0, description="分页偏移(>=0默认 0")
CURSOR: str | None = Query(default=None, description="游标分页标记")
def sanitize_headers(headers: Any) -> dict[str, str]:
"""脱敏 HTTP headers剔除敏感字段§11.1.2 日志不得记录敏感字段)。

View File

@ -47,6 +47,8 @@ from yuxi.channels.contract.errors import ValidationError
from yuxi.storage.postgres.models_business import User
from server.routers.channels import (
LARGE_LIMIT,
OFFSET,
build_operator,
get_channel_use_cases,
raiseOnControlFailure,
@ -182,8 +184,8 @@ async def list_accounts(
default=None,
description="按渠道类型过滤",
),
limit: int = Query(default=1000, ge=1, le=1000, description="分页大小"),
offset: int = Query(default=0, ge=0, description="分页偏移"),
limit: int = LARGE_LIMIT,
offset: int = OFFSET,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:

View File

@ -50,6 +50,8 @@ from yuxi.channels.contract.errors import ValidationError
from yuxi.storage.postgres.models_business import User
from server.routers.channels import (
LARGE_LIMIT,
OFFSET,
build_operator,
get_channel_use_cases,
parse_datetime,
@ -198,8 +200,8 @@ async def list_whitelist(
account_id: str,
policy_type: str,
request: Request,
limit: int = Query(default=100, ge=1, le=1000, description="分页大小1-1000默认 100"),
offset: int = Query(default=0, ge=0, description="偏移量(>=0默认 0"),
limit: int = LARGE_LIMIT,
offset: int = OFFSET,
keyword: str | None = Query(default=None, description="关键词(模糊匹配 peer_id / peer_name"),
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),

View File

@ -28,6 +28,7 @@ from yuxi.channels.contract.dtos.channel import ChannelType
from yuxi.storage.postgres.models_business import User
from server.routers.channels import (
ANALYTICS_LIMIT,
Granularity,
build_operator,
get_channel_use_cases,
@ -214,7 +215,7 @@ async def analyze_peers(
end_time: str = Query(..., description="结束时间ISO 8601"),
channel_type: ChannelType | None = Query(default=None, description="渠道类型过滤"),
account_id: str | None = Query(default=None, description="账户 ID 过滤"),
limit: int = Query(default=100, ge=1, le=500, description="Top N 上限1-500"),
limit: int = ANALYTICS_LIMIT,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:

View File

@ -61,6 +61,8 @@ from yuxi.channels.contract.dtos.channel import ChannelType
from yuxi.storage.postgres.models_business import User
from server.routers.channels import (
LARGE_LIMIT,
OFFSET,
build_operator,
get_channel_use_cases,
parse_datetime,
@ -103,8 +105,8 @@ async def query_audit_logs(
start_time: str | None = Query(default=None, description="起始时间ISO 8601"),
end_time: str | None = Query(default=None, description="结束时间ISO 8601"),
trace_id: str | None = Query(default=None, description="按链路追踪 ID 过滤"),
limit: int = Query(default=100, ge=1, le=500, description="分页大小"),
offset: int = Query(default=0, ge=0, description="分页偏移"),
limit: int = LARGE_LIMIT,
offset: int = OFFSET,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:

View File

@ -47,6 +47,8 @@ async def list_capabilities(
@capability_router.get("/capabilities/matrix", response_model=dict)
async def list_capability_matrix(
request: Request,
status: str | None = None,
capability: str | None = None,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
@ -54,10 +56,16 @@ async def list_capability_matrix(
返回每个插件的完整 ``ChannelCapabilityProfile``包含静态能力声明
运行时可用性插件元数据与配置 schema供能力查询页渲染高密度矩阵
查询参数可选供前端服务端筛选预留
status: 按运行时可用性状态过滤available/unavailable/unknown/unsupported
capability: 按能力字段名过滤 streamingsupports_image_outbound
"""
operator = build_operator(current_user, request)
result = await use_cases.capability_query.listCapabilityMatrix(
operator=operator,
status=status,
capability=capability,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}

View File

@ -241,6 +241,31 @@ async def batch_update_config(
return {"success": True, "data": serialize_control_data(result.data)}
@config_router.get("/config/values", response_model=dict)
async def get_config_values(
request: Request,
keys: list[str] = Query(..., description="配置键列表"),
scope: ConfigScope = Query(default=ConfigScope.GLOBAL, description="配置作用域"),
target: str | None = Query(default=None, description="作用域目标channel/account 时必填)"),
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""批量查询配置值CFG-READ-MANY。对应控制面操作 config/get_many。
一次性查询多个配置键的当前值返回顺序与请求 keys 一致供管理后台
替代多次单 key 请求
"""
operator = build_operator(current_user, request)
result = await use_cases.config_management.getConfigValues(
keys=keys,
operator=operator,
scope=scope,
target=target,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@config_router.get("/config/{key}", response_model=dict)
async def get_config(
key: str,

View File

@ -78,7 +78,9 @@ from yuxi.channels.contract.errors import ValidationError
from yuxi.storage.postgres.models_business import User
from server.routers.channels import (
DEFAULT_LIMIT,
Granularity,
OFFSET,
build_operator,
dataclass_to_dict,
get_channel_use_cases,
@ -170,8 +172,7 @@ def _parse_resource_type(raw: str) -> ContentReviewResourceType:
except ValueError as exc:
raise ValidationError(
"resource_type",
f"unsupported resource_type: {raw} "
"(expected: message_text | message_attachment | user_profile)",
f"unsupported resource_type: {raw} (expected: message_text | message_attachment | user_profile)",
) from exc
@ -244,8 +245,8 @@ async def list_review_history(
default=None,
description="结束时间过滤ISO 8601",
),
limit: int = Query(default=20, ge=1, le=100, description="每页数量1-100"),
offset: int = Query(default=0, ge=0, description="分页偏移量"),
limit: int = DEFAULT_LIMIT,
offset: int = OFFSET,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:

View File

@ -51,6 +51,9 @@ from yuxi.channels.contract.dtos.directory import (
from yuxi.storage.postgres.models_business import User
from server.routers.channels import (
CURSOR,
DEFAULT_LIMIT,
SUGGESTION_LIMIT,
build_operator,
get_channel_use_cases,
raiseOnControlFailure,
@ -73,8 +76,8 @@ async def search_directory_users(
account_id: str,
request: Request,
keyword: str | None = Query(default=None, description="搜索关键词"),
cursor: str | None = Query(default=None, description="游标分页标记"),
limit: int = Query(default=20, ge=1, le=100, description="分页大小"),
cursor: str | None = CURSOR,
limit: int = DEFAULT_LIMIT,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
@ -105,7 +108,7 @@ async def search_directory_suggestions(
account_id: str,
request: Request,
keyword: str = Query(..., min_length=1, description="搜索关键词"),
limit: int = Query(default=10, ge=1, le=20, description="建议数量"),
limit: int = SUGGESTION_LIMIT,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
@ -137,8 +140,8 @@ async def search_directory_groups(
account_id: str,
request: Request,
keyword: str | None = Query(default=None, description="搜索关键词"),
cursor: str | None = Query(default=None, description="游标分页标记"),
limit: int = Query(default=20, ge=1, le=100, description="分页大小"),
cursor: str | None = CURSOR,
limit: int = DEFAULT_LIMIT,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
@ -356,8 +359,8 @@ async def get_directory_group_members(
account_id: str,
group_id: str,
request: Request,
cursor: str | None = Query(default=None, description="游标分页标记"),
limit: int = Query(default=20, ge=1, le=100, description="分页大小"),
cursor: str | None = CURSOR,
limit: int = DEFAULT_LIMIT,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:

View File

@ -48,6 +48,9 @@ from yuxi.channels.contract.errors import ValidationError
from yuxi.storage.postgres.models_business import User
from server.routers.channels import (
DEFAULT_LIMIT,
LARGE_LIMIT,
OFFSET,
build_operator,
dataclass_to_dict,
get_channel_use_cases,
@ -158,8 +161,8 @@ async def list_admin_messages(
message_id: str | None = Query(default=None, description="按消息业务 ID 模糊匹配"),
channel_msg_id: str | None = Query(default=None, description="按渠道消息 ID 模糊匹配"),
channel_session_id: str | None = Query(default=None, description="按渠道会话 ID 模糊匹配"),
limit: int = Query(default=50, ge=1, le=200, description="分页大小"),
offset: int = Query(default=0, ge=0, description="分页偏移"),
limit: int = LARGE_LIMIT,
offset: int = OFFSET,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
@ -207,19 +210,20 @@ async def search_messages(
),
channel_session_id: str | None = Query(default=None, description="按会话 ID 过滤"),
peer_id: str | None = Query(default=None, description="按对端 ID 过滤"),
account_id: str | None = Query(default=None, description="按渠道账户业务 ID 模糊匹配"),
message_id: str | None = Query(default=None, description="按消息业务 ID 模糊匹配"),
channel_msg_id: str | None = Query(default=None, description="按渠道消息 ID 模糊匹配"),
start_time: str | None = Query(default=None, description="起始时间ISO 8601"),
end_time: str | None = Query(default=None, description="截止时间ISO 8601"),
limit: int = Query(default=20, ge=1, le=100, description="分页大小"),
offset: int = Query(default=0, ge=0, description="分页偏移"),
limit: int = DEFAULT_LIMIT,
offset: int = OFFSET,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""全文搜索消息MSG-SEARCH-01
按关键词搜索消息内容支持渠道状态角色会话对端 ID消息 ID
渠道消息 ID时间范围过滤对应控制面操作 ``message/search``
按关键词搜索消息内容支持渠道状态角色会话对端 ID账户 ID
消息 ID渠道消息 ID时间范围过滤对应控制面操作 ``message/search``
"""
operator = build_operator(current_user, request)
start_time_dt = parse_datetime("start_time", start_time)
@ -234,6 +238,7 @@ async def search_messages(
role=role,
channel_session_id=channel_session_id,
peer_id=peer_id,
account_id=account_id,
message_id=message_id,
channel_msg_id=channel_msg_id,
start_time=start_time_dt,

View File

@ -42,6 +42,9 @@ from yuxi.channels.contract.dtos.outbox import (
from yuxi.storage.postgres.models_business import User
from server.routers.channels import (
EXPORT_LIMIT,
LARGE_LIMIT,
OFFSET,
build_operator,
get_channel_use_cases,
parse_datetime,
@ -93,8 +96,8 @@ async def list_outbox_messages(
retry_count_min: int | None = Query(
default=None, ge=0, description="最小重试次数下界(含),用于筛选已发生重试的条目"
),
limit: int = Query(default=100, ge=1, le=200, description="分页大小"),
offset: int = Query(default=0, ge=0, description="分页偏移"),
limit: int = LARGE_LIMIT,
offset: int = OFFSET,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
@ -190,8 +193,8 @@ async def list_dead_letter_outbox(
retry_count_min: int | None = Query(
default=None, ge=0, description="最小重试次数下界(含),用于筛选已发生重试的条目"
),
limit: int = Query(default=100, ge=1, le=200, description="分页大小"),
offset: int = Query(default=0, ge=0, description="分页偏移"),
limit: int = LARGE_LIMIT,
offset: int = OFFSET,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
@ -295,7 +298,7 @@ async def export_dead_letter(
created_after: str | None = Query(default=None, description="创建时间下界ISO 8601"),
created_before: str | None = Query(default=None, description="创建时间上界ISO 8601"),
export_format: Literal["json", "csv"] = Query(default="json", alias="format", description="导出格式json / csv"),
limit: int = Query(default=10000, ge=1, le=100000, description="导出条目上限,防止大规模死信队列 OOM"),
export_limit: int = EXPORT_LIMIT,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
@ -303,8 +306,8 @@ async def export_dead_letter(
按过滤条件查询 DEAD 状态 outbox 条目并序列化为 ``json`` / ``csv`` 格式
``format=json`` ``content`` 为记录数组``format=csv`` ``content``
CSV 字符串含表头与 CSV injection 防护``limit`` 限制单次导出条目数
默认 10000防止大规模死信队列 OOM
CSV 字符串含表头与 CSV injection 防护``export_limit`` 限制单次导出
条目数默认 10000防止大规模死信队列 OOM
"""
operator = build_operator(current_user, request)
created_after_dt = parse_datetime("created_after", created_after)
@ -314,7 +317,7 @@ async def export_dead_letter(
created_after=created_after_dt,
created_before=created_before_dt,
format=export_format,
limit=limit,
limit=export_limit,
)
result = await use_cases.outbox_management.exportDeadLetters(cmd, operator=operator)
raiseOnControlFailure(result)

View File

@ -37,6 +37,8 @@ from yuxi.storage.postgres.models_business import User
from server.routers.channels import (
Granularity,
LARGE_LIMIT,
OFFSET,
build_operator,
get_channel_use_cases,
parse_datetime,
@ -117,8 +119,8 @@ async def list_pairings(
status: PairingStatus | None = Query(default=None, description="按状态过滤"),
created_after: str | None = Query(default=None, description="申请时间下界ISO 8601按 requested_at 过滤)"),
created_before: str | None = Query(default=None, description="申请时间上界ISO 8601按 requested_at 过滤)"),
limit: int = Query(default=100, ge=1, le=1000, description="分页大小"),
offset: int = Query(default=0, ge=0, description="分页偏移"),
limit: int = LARGE_LIMIT,
offset: int = OFFSET,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:

View File

@ -122,17 +122,23 @@ async def list_plugins(
@plugin_router.get("/catalog", response_model=dict)
async def list_plugin_catalog(
request: Request,
state: PluginStateLiteral | None = Query(
default=None,
description="按生命周期状态过滤discovered/resolved/loaded/initialized/started/paused/stopped/unloaded/failed/not_installed/installed",
),
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""列出插件目录PLG-CATALOG。对应控制面操作 plugin/catalog。
返回所有已注册插件的卡片视图元数据 capabilities供前端卡片
网格展示 ``GET /plugins`` 的区别在于返回 capabilities 字段
返回已注册插件的卡片视图元数据 capabilities供前端卡片网格展示
``GET /plugins`` 的区别在于返回 capabilities / icon / last_error 字段
支持 ``state`` 查询参数与 ``GET /plugins`` 保持一致便于前端直接做服务端状态过滤
"""
operator = build_operator(current_user, request)
result = await use_cases.plugin_management.listPluginCatalog(
operator=operator,
state=state,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}

View File

@ -39,6 +39,8 @@ 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,
@ -148,8 +150,8 @@ async def list_route_bindings(
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 = Query(default=20, ge=1, le=1000, description="分页大小"),
offset: int = Query(default=0, ge=0, description="分页偏移"),
limit: int = DEFAULT_LIMIT,
offset: int = OFFSET,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:

View File

@ -47,6 +47,8 @@ from yuxi.channels.infrastructure import DependencyInjectionContainer
from yuxi.storage.postgres.models_business import User
from server.routers.channels import (
LARGE_LIMIT,
OFFSET,
build_operator,
dataclass_to_dict,
get_channel_di_container,
@ -162,8 +164,8 @@ async def list_sessions(
last_message_after: str | None = Query(default=None, description="最近消息时间下界ISO 8601"),
last_message_before: str | None = Query(default=None, description="最近消息时间上界ISO 8601"),
abnormal: bool = Query(default=False, description="仅返回异常会话(僵尸/账户不可用/无主)"),
limit: int = Query(default=100, ge=1, le=1000, description="分页大小"),
offset: int = Query(default=0, ge=0, description="分页偏移"),
limit: int = LARGE_LIMIT,
offset: int = OFFSET,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
@ -404,8 +406,8 @@ async def close_session(
async def list_session_messages(
session_id: str,
request: Request,
limit: int = Query(default=50, ge=1, le=200, description="分页大小"),
offset: int = Query(default=0, ge=0, description="分页偏移"),
limit: int = LARGE_LIMIT,
offset: int = OFFSET,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]: