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

585 lines
23 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.

"""账户资源域 RouterAL-01~AL-04 + ACC-01 runtime-status + P0缺口
本 router 实现渠道账户生命周期的全部 HTTP 端点,覆盖创建 / 查询 / 更新 /
删除 / 启用 / 禁用 / 运行时状态查询 / 凭据轮换 / 连通性测试共 10 个操作。所有端点统一采用模板 A
(控制面端口路由),通过 ``get_channel_use_cases`` 装配 ``ChannelUseCases``
经 ``account_management`` 端口调用类型化方法,由 ``ChannelControlService._executeControl``
内部走控制面管道auth → permission → rate_limit → dispatch → audit
鉴权策略:全部端点使用 ``get_admin_user`` 依赖,要求管理员或超级管理员角色。
角色校验由依赖函数完成router 内不做角色判断(规范 §4
路径设计:静态跨渠道查询 ``GET /accounts`` 与 ``POST /accounts/batch/*`` 先于
动态路径 ``/{channel_type}/...`` 声明(规范 §6.5),避免静态路径被动态参数
捕获。子 router 不自行设置 prefix根前缀 ``/channels`` 由 ``channels_router``
聚合 router 统一追加。
端点清单对应《01-账户资源域设计方案》§2.1 + P0/P1缺口补全
- GET /accounts ACC-AL-01 list_accounts
- POST /accounts/batch/enable ACC-BATCH-STATE batch_enable_accounts
- POST /accounts/batch/disable ACC-BATCH-STATE batch_disable_accounts
- POST /{channel_type}/accounts ACC-AL-02 create_account
- GET /{channel_type}/accounts/{account_id} ACC-AL-01 get_account
- PUT /{channel_type}/accounts/{account_id} ACC-AL-03 update_account
- DELETE /{channel_type}/accounts/{account_id} ACC-AL-04 delete_account
- POST /{channel_type}/accounts/{account_id}/enable ACC-STATE-01 enable_account
- POST /{channel_type}/accounts/{account_id}/disable ACC-STATE-02 disable_account
- POST /{channel_type}/accounts/{account_id}/recover ACC-STATE-03 recover_account
- GET /{channel_type}/accounts/{account_id}/runtime-status ACC-01 get_account_runtime_status
- POST /{channel_type}/accounts/{account_id}/rotate-credentials ACC-ROTATE-01 rotate_credentials
- POST /{channel_type}/accounts/{account_id}/test-connection ACC-TEST-01 test_connection
- GET /{channel_type}/accounts/{account_id}/export ACC-EXPORT export_account
- POST /{channel_type}/accounts/{account_id}/clone ACC-CLONE clone_account
"""
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 (
BatchStateChangeCmd,
ChannelType,
CloneAccountCmd,
)
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,
serialize_control_data,
)
from server.utils.auth_middleware import get_admin_user
account_router = APIRouter(tags=["channels-account"])
# ---------------- Request Schemas ----------------
class CreateAccountRequest(BaseModel):
"""创建账户请求。字段对齐 ``AccountManagementPort.createAccount`` 入参。"""
model_config = ConfigDict(frozen=True)
display_name: str = Field(
...,
min_length=1,
max_length=128,
description="账户显示名称",
)
raw_config: dict[str, Any] = Field(
...,
description="渠道原始配置(由插件校验)",
)
class UpdateAccountRequest(BaseModel):
"""更新账户请求。字段对齐 ``AccountManagementPort.updateAccount`` 入参。
``display_name`` 与 ``raw_config`` 均可选,但至少一个非空(由
``_require_at_least_one`` 校验器保证),否则抛 ``ValidationError``。
"""
model_config = ConfigDict(frozen=True)
display_name: str | None = Field(
default=None,
min_length=1,
max_length=128,
description="账户显示名称",
)
raw_config: dict[str, Any] | None = Field(
default=None,
description="渠道原始配置",
)
@model_validator(mode="after")
def _require_at_least_one(self) -> UpdateAccountRequest:
"""校验至少一个字段非空。
两个字段均为 ``None`` 时抛 ``ValidationError``(规范 §6.3 跨字段校验)。
由于 ``ValidationError`` 继承 ``ValueError``Pydantic v2 会将其包装为
``RequestValidationError``,由全局处理器统一映射为 422 + 统一格式响应。
"""
if self.display_name is None and self.raw_config is None:
raise ValidationError(
"display_name",
"at least one of display_name or raw_config must be provided",
)
return self
class BatchStateChangeRequest(BaseModel):
"""批量启停账户请求体ACC-BATCH-STATE
``account_ids`` 与 ``filter`` 二选一:二者不可同时为空,亦不可同时
提供schema 层强制互斥,避免歧义)。``action`` 由端点路径决定
``enable`` / ``disable``),不在 body 中。
"""
model_config = ConfigDict(frozen=True)
account_ids: list[str] | None = Field(
default=None,
description="显式账户 ID 列表(与 filter 二选一)",
)
filter: dict[str, Any] | None = Field(
default=None,
description="筛选条件(含 channel_type / status",
)
reason: str | None = Field(default=None, max_length=512, description="操作原因(审计用)")
@model_validator(mode="after")
def _require_exclusive_one_of(self) -> BatchStateChangeRequest:
"""校验 ``account_ids`` 与 ``filter`` 互斥且至少一个非空。
二者均为空时抛 ``ValidationError``(缺目标),二者均非空时抛
``ValidationError``(互斥违反,避免 ``filter`` 字段被静默忽略)。
"""
has_ids = bool(self.account_ids)
has_filter = bool(self.filter)
if not has_ids and not has_filter:
raise ValidationError(
"account_ids",
"either account_ids or filter must be provided",
)
if has_ids and has_filter:
raise ValidationError(
"account_ids",
"account_ids and filter are mutually exclusive",
)
return self
class CloneAccountRequest(BaseModel):
"""克隆账户请求体ACC-CLONE
``channel_type`` 与源 ``account_id`` 由路径参数提供,不在 body 中。
新账户默认为 DISABLED 状态,需手动 enable。
"""
model_config = ConfigDict(frozen=True)
new_display_name: str = Field(..., min_length=1, max_length=128, description="新账户显示名")
new_raw_config_overrides: dict[str, Any] | None = Field(
default=None,
description="配置覆盖项(可选,按字段覆盖源账户配置)",
)
include_credentials: bool = Field(default=False, description="是否克隆凭据(默认 False")
# ---------------- Endpoints ----------------
@account_router.get("/accounts", response_model=dict)
async def list_accounts(
request: Request,
channel_type: ChannelType | None = Query(
default=None,
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]:
"""列出已配置的机器人账号AL-01
支持按渠道类型过滤与分页。对应控制面操作 ``account/list``(由
``ChannelControlService.listAccounts`` 内部构造 ``ControlCmd`` 并
委托 ``_executeControl`` 执行控制面管道)。
"""
operator = build_operator(current_user, request)
result = await use_cases.account_management.listAccounts(
channel_type=channel_type,
operator=operator,
limit=limit,
offset=offset,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@account_router.post("/accounts/batch/enable", response_model=dict)
async def batch_enable_accounts(
payload: BatchStateChangeRequest,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""批量启用渠道账户ACC-BATCH-STATEaction=enable
支持 ``account_ids`` 显式列表或 ``filter`` 筛选条件两种模式(二者
互斥,由 schema 校验)。逐条独立事务复用单条 ``enableAccount`` 逻辑
(模式 D单条失败不回滚已成功条目。幂等已 active 直接计入成功。
整体返回 ``success``,部分失败信息在 ``data.failed`` 列表中,客户端
需检查该列表。对应控制面操作 ``account/batch_enable``。
静态路径先于 ``/{channel_type}/accounts`` 声明,避免被动态参数捕获。
"""
operator = build_operator(current_user, request)
cmd = BatchStateChangeCmd(
action="enable",
operator=operator,
account_ids=tuple(payload.account_ids) if payload.account_ids else (),
filter=payload.filter,
reason=payload.reason,
)
result = await use_cases.account_management.batchEnableAccounts(cmd)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@account_router.post("/accounts/batch/disable", response_model=dict)
async def batch_disable_accounts(
payload: BatchStateChangeRequest,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""批量禁用渠道账户ACC-BATCH-STATEaction=disable
支持 ``account_ids`` 显式列表或 ``filter`` 筛选条件两种模式(二者
互斥,由 schema 校验)。逐条独立事务复用单条 ``disableAccount`` 逻辑
(模式 D单条失败不回滚已成功条目。幂等已 disabled 直接计入成功。
整体返回 ``success``,部分失败信息在 ``data.failed`` 列表中,客户端
需检查该列表。对应控制面操作 ``account/batch_disable``。
静态路径先于 ``/{channel_type}/accounts`` 声明,避免被动态参数捕获。
"""
operator = build_operator(current_user, request)
cmd = BatchStateChangeCmd(
action="disable",
operator=operator,
account_ids=tuple(payload.account_ids) if payload.account_ids else (),
filter=payload.filter,
reason=payload.reason,
)
result = await use_cases.account_management.batchDisableAccounts(cmd)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@account_router.post("/{channel_type}/accounts", response_model=dict)
async def create_account(
channel_type: ChannelType,
payload: CreateAccountRequest,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""创建机器人账号AL-02
对应控制面操作 ``account/create``(由 ``ChannelControlService.createAccount``
内部构造 ``ControlCmd`` 并委托 ``_executeControl`` 执行控制面管道)。
``raw_config`` 由对应渠道插件的 ``LifecycleAdapter`` 校验。
"""
operator = build_operator(current_user, request)
result = await use_cases.account_management.createAccount(
channel_type=channel_type,
raw_config=payload.raw_config,
display_name=payload.display_name,
operator=operator,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@account_router.get("/{channel_type}/accounts/{account_id}", response_model=dict)
async def get_account(
channel_type: ChannelType,
account_id: str,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""查看账户详情AL-01。配置字段已脱敏。
对应控制面操作 ``account/get``(由 ``ChannelControlService.getAccount``
内部构造 ``ControlCmd`` 并委托 ``_executeControl`` 执行控制面管道)。
"""
operator = build_operator(current_user, request)
result = await use_cases.account_management.getAccount(
channel_type=channel_type,
account_id=account_id,
operator=operator,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@account_router.put("/{channel_type}/accounts/{account_id}", response_model=dict)
async def update_account(
channel_type: ChannelType,
account_id: str,
payload: UpdateAccountRequest,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""更新账户配置AL-03
对应控制面操作 ``account/update``(由 ``ChannelControlService.updateAccount``
内部构造 ``ControlCmd`` 并委托 ``_executeControl`` 执行控制面管道)。
至少传入 ``display_name`` 或 ``raw_config`` 之一(由 schema 校验)。
"""
operator = build_operator(current_user, request)
result = await use_cases.account_management.updateAccount(
channel_type=channel_type,
account_id=account_id,
raw_config=payload.raw_config,
display_name=payload.display_name,
operator=operator,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@account_router.delete("/{channel_type}/accounts/{account_id}", response_model=dict)
async def delete_account(
channel_type: ChannelType,
account_id: str,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""下线并删除账户AL-04。删除前触发 beforeAccountDelete 回调。
对应控制面操作 ``account/delete``(由 ``ChannelControlService.deleteAccount``
内部构造 ``ControlCmd`` 并委托 ``_executeControl`` 执行控制面管道)。
删除成功后 ``result.data`` 可能为 ``None``,统一返回 ``null``。
"""
operator = build_operator(current_user, request)
result = await use_cases.account_management.deleteAccount(
channel_type=channel_type,
account_id=account_id,
operator=operator,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@account_router.post("/{channel_type}/accounts/{account_id}/enable", response_model=dict)
async def enable_account(
channel_type: ChannelType,
account_id: str,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""启用账户ACC-STATE-01。幂等已 active 直接返回成功。
对应控制面操作 ``account/enable``(由 ``ChannelControlService.enableAccount``
内部构造 ``ControlCmd`` 并委托 ``_executeControl`` 执行控制面管道)。
"""
operator = build_operator(current_user, request)
result = await use_cases.account_management.enableAccount(
channel_type=channel_type,
account_id=account_id,
operator=operator,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@account_router.post("/{channel_type}/accounts/{account_id}/disable", response_model=dict)
async def disable_account(
channel_type: ChannelType,
account_id: str,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""禁用账户ACC-STATE-02。幂等已 disabled 直接返回成功。
对应控制面操作 ``account/disable``(由 ``ChannelControlService.disableAccount``
内部构造 ``ControlCmd`` 并委托 ``_executeControl`` 执行控制面管道)。
"""
operator = build_operator(current_user, request)
result = await use_cases.account_management.disableAccount(
channel_type=channel_type,
account_id=account_id,
operator=operator,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@account_router.post("/{channel_type}/accounts/{account_id}/recover", response_model=dict)
async def recover_account(
channel_type: ChannelType,
account_id: str,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""恢复降级账户ACC-STATE-03。幂等已 active 直接返回成功。
对应控制面操作 ``account/recover``(由 ``ChannelControlService.recoverAccount``
内部构造 ``ControlCmd`` 并委托 ``_executeControl`` 执行控制面管道)。
业务用途DEGRADED 状态的账户无法通过 ``enable`` 恢复(聚合根
``enable()`` 在 DEGRADED 状态抛 RuleViolationError必须通过本端点恢复。
"""
operator = build_operator(current_user, request)
result = await use_cases.account_management.recoverAccount(
channel_type=channel_type,
account_id=account_id,
operator=operator,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@account_router.get(
"/{channel_type}/accounts/{account_id}/runtime-status",
response_model=dict,
)
async def get_account_runtime_status(
channel_type: ChannelType,
account_id: str,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""查询账户运行时状态AC-32
返回账户状态机、熔断器状态、登录态、降级标记。对应控制面操作
``account/runtime_status``(由 ``ChannelControlService.getAccountRuntimeStatus``
内部构造 ``ControlCmd`` 并委托 ``_executeControl`` 执行控制面管道)。
"""
operator = build_operator(current_user, request)
result = await use_cases.account_management.getAccountRuntimeStatus(
channel_type=channel_type,
account_id=account_id,
operator=operator,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@account_router.post(
"/{channel_type}/accounts/{account_id}/rotate-credentials",
response_model=dict,
)
async def rotate_credentials(
channel_type: ChannelType,
account_id: str,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""轮换渠道账户凭据ACC-ROTATE-01
触发渠道账户凭据轮换流程,用于合规性凭据更新场景。
对应控制面操作 ``account/rotate_credentials``。
"""
operator = build_operator(current_user, request)
result = await use_cases.account_management.rotateCredentials(
channel_type=channel_type,
account_id=account_id,
operator=operator,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@account_router.post(
"/{channel_type}/accounts/{account_id}/test-connection",
response_model=dict,
)
async def test_connection(
channel_type: ChannelType,
account_id: str,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""测试渠道账户连通性ACC-TEST-01
验证账户凭据有效性与API连通性返回连通性检查结果。
对应控制面操作 ``account/test_connection``。
"""
operator = build_operator(current_user, request)
result = await use_cases.account_management.testConnection(
channel_type=channel_type,
account_id=account_id,
operator=operator,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@account_router.get(
"/{channel_type}/accounts/{account_id}/export",
response_model=dict,
)
async def export_account(
channel_type: ChannelType,
account_id: str,
request: Request,
include_secrets: bool = Query(
default=False,
description="是否包含敏感凭据True 时需超级管理员权限)",
),
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""导出账户配置ACC-EXPORT
查询账户配置并按 ``include_secrets`` 决定是否脱敏:``include_secrets=False``
时经 MaskingPort 遮蔽敏感字段;``include_secrets=True`` 时需超级管理员
权限,返回含凭据的原始配置。对应控制面操作 ``account/export``。
"""
operator = build_operator(current_user, request)
result = await use_cases.account_management.exportAccount(
channel_type=channel_type,
account_id=account_id,
operator=operator,
include_secrets=include_secrets,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@account_router.post(
"/{channel_type}/accounts/{account_id}/clone",
response_model=dict,
)
async def clone_account(
channel_type: ChannelType,
account_id: str,
payload: CloneAccountRequest,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""克隆账户ACC-CLONE
基于源账户配置创建新账户:查询源账户 → 调用 ``ChannelAccount.clone()``
聚合根方法 → 持久化 → 触发 ``afterAccountConfigWritten`` 回调
best-effort。新账户默认为 DISABLED 状态,需手动 enable。对应
控制面操作 ``account/clone``。
协议翻译:路径参数 ``channel_type`` / ``account_id``(源账户)与 body
字段组合为 ``CloneAccountCmd``。
"""
operator = build_operator(current_user, request)
cmd = CloneAccountCmd(
channel_type=channel_type,
account_id=account_id,
new_display_name=payload.new_display_name,
operator=operator,
new_raw_config_overrides=payload.new_raw_config_overrides,
include_credentials=payload.include_credentials,
)
result = await use_cases.account_management.cloneAccount(cmd)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}