diff --git a/backend/server/routers/channels/__init__.py b/backend/server/routers/channels/__init__.py index 61ac59ef..fd05adb4 100644 --- a/backend/server/routers/channels/__init__.py +++ b/backend/server/routers/channels/__init__.py @@ -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 日志不得记录敏感字段)。 diff --git a/backend/server/routers/channels/account_router.py b/backend/server/routers/channels/account_router.py index b66d07f6..2a86ee69 100644 --- a/backend/server/routers/channels/account_router.py +++ b/backend/server/routers/channels/account_router.py @@ -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]: diff --git a/backend/server/routers/channels/allowlist_router.py b/backend/server/routers/channels/allowlist_router.py index c58f0587..094d7002 100644 --- a/backend/server/routers/channels/allowlist_router.py +++ b/backend/server/routers/channels/allowlist_router.py @@ -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), diff --git a/backend/server/routers/channels/analytics_router.py b/backend/server/routers/channels/analytics_router.py index 6523a40f..993c80fc 100644 --- a/backend/server/routers/channels/analytics_router.py +++ b/backend/server/routers/channels/analytics_router.py @@ -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]: diff --git a/backend/server/routers/channels/audit_router.py b/backend/server/routers/channels/audit_router.py index 9f7d6d43..01fd0f1b 100644 --- a/backend/server/routers/channels/audit_router.py +++ b/backend/server/routers/channels/audit_router.py @@ -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]: diff --git a/backend/server/routers/channels/capability_router.py b/backend/server/routers/channels/capability_router.py index 2aab2e47..e1d1a812 100644 --- a/backend/server/routers/channels/capability_router.py +++ b/backend/server/routers/channels/capability_router.py @@ -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: 按能力字段名过滤(如 streaming、supports_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)} diff --git a/backend/server/routers/channels/config_router.py b/backend/server/routers/channels/config_router.py index a0065d77..900ae309 100644 --- a/backend/server/routers/channels/config_router.py +++ b/backend/server/routers/channels/config_router.py @@ -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, diff --git a/backend/server/routers/channels/content_review_router.py b/backend/server/routers/channels/content_review_router.py index 562351d2..a8927f3f 100644 --- a/backend/server/routers/channels/content_review_router.py +++ b/backend/server/routers/channels/content_review_router.py @@ -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]: diff --git a/backend/server/routers/channels/directory_router.py b/backend/server/routers/channels/directory_router.py index e011f57f..b3f64cd8 100644 --- a/backend/server/routers/channels/directory_router.py +++ b/backend/server/routers/channels/directory_router.py @@ -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]: diff --git a/backend/server/routers/channels/message_router.py b/backend/server/routers/channels/message_router.py index d8814fd6..05821bdd 100644 --- a/backend/server/routers/channels/message_router.py +++ b/backend/server/routers/channels/message_router.py @@ -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, diff --git a/backend/server/routers/channels/outbox_router.py b/backend/server/routers/channels/outbox_router.py index bd5854c0..2ea45000 100644 --- a/backend/server/routers/channels/outbox_router.py +++ b/backend/server/routers/channels/outbox_router.py @@ -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) diff --git a/backend/server/routers/channels/pairing_router.py b/backend/server/routers/channels/pairing_router.py index 4965f435..6e3c75d1 100644 --- a/backend/server/routers/channels/pairing_router.py +++ b/backend/server/routers/channels/pairing_router.py @@ -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]: diff --git a/backend/server/routers/channels/plugin_router.py b/backend/server/routers/channels/plugin_router.py index bf2867a1..9b07fd16 100644 --- a/backend/server/routers/channels/plugin_router.py +++ b/backend/server/routers/channels/plugin_router.py @@ -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)} diff --git a/backend/server/routers/channels/route_binding_router.py b/backend/server/routers/channels/route_binding_router.py index 0dbc525c..dd363480 100644 --- a/backend/server/routers/channels/route_binding_router.py +++ b/backend/server/routers/channels/route_binding_router.py @@ -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]: diff --git a/backend/server/routers/channels/session_router.py b/backend/server/routers/channels/session_router.py index b87133c9..19962db5 100644 --- a/backend/server/routers/channels/session_router.py +++ b/backend/server/routers/channels/session_router.py @@ -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]: