ForcePilot/backend/server/routers/channels/account_router.py
Kris ddafd95ff0 refactor(routers): 整理并新增多组渠道相关路由功能
1.  移除多个导出接口的显式response_model声明
2.  调整access_rule和test_case的创建接口位置,修复静态路径冲突
3.  优化适配器配置校验的异常处理逻辑
4.  重构集成路由的查询逻辑,统一使用get_integration_or_raise
5.  新增channels路由组下的capability、reports、dashboard、webhook、wizard、doctor、directory、session共8个子路由模块
6.  注册channels_router到全局路由列表
2026-07-02 03:29:06 +08:00

536 lines
20 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
- 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 (
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 跨字段校验),
交由全局 handler 统一映射为 400 响应。
"""
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`` 二者不可同时为空(由 dispatch handler
校验)。``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="操作原因(审计用)")
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 = Query(default=1000, ge=1, le=1000, description="分页大小"),
offset: int = Query(default=0, ge=0, description="分页偏移"),
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`` 筛选条件两种模式(二者不可
同时空)。逐条独立事务复用单条 ``enableAccount`` 逻辑(模式 D部分
成功语义),单条失败不回滚已成功条目。幂等:已 active 直接计入成功。
对应控制面操作 ``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`` 筛选条件两种模式(二者不可
同时空)。逐条独立事务复用单条 ``disableAccount`` 逻辑(模式 D部分
成功语义),单条失败不回滚已成功条目。幂等:已 disabled 直接计入成功。
对应控制面操作 ``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`` 可能为空,返回空 dict。
"""
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) if result.data else {},
}
@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.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)}