1. 为渠道账户ID查询添加最小长度校验,统一分析模块常量引用 2. 新增扫码登录向导端点,完善文档说明 3. 优化配对统计接口,移除无效参数 4. 为出站箱接口添加批量上限与202状态码 5. 新增测试用例、访问规则、配额等模块的查询与校验参数 6. 新增适配器健康批量查询、健康检查触发接口 7. 统一告警、审计日志的错误处理方式 8. 新增插件配置账户ID支持,优化批量操作响应 9. 新增环境健康批量查询、Webhook限流与参数校验 10. 完善会话管理、审计日志的参数与文档说明 11. 修复导入模块的校验错误处理逻辑
604 lines
24 KiB
Python
604 lines
24 KiB
Python
"""账户资源域 Router(AL-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 中。
|
||
|
||
``channel_type`` 在 ``account_ids`` 模式下必填:``account_id`` 仅在
|
||
``(channel_type, account_id)`` 复合键下唯一,批量操作需显式限定单一
|
||
渠道才能定位账户(由 ``_require_exclusive_one_of`` 校验器保证)。
|
||
``filter`` 模式下可选,渠道信息可置于 ``filter.channel_type`` 内。
|
||
"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
channel_type: ChannelType | None = Field(
|
||
default=None,
|
||
description="目标渠道类型(account_ids 模式必填,filter 模式可选)",
|
||
)
|
||
account_ids: list[str] | None = Field(
|
||
default=None,
|
||
max_length=500,
|
||
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`` 字段被静默忽略)。
|
||
``account_ids`` 模式下 ``channel_type`` 必填:``account_id`` 非全局
|
||
唯一,需显式限定渠道,否则 handler 无法将 account_id 解析到渠道。
|
||
"""
|
||
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",
|
||
)
|
||
if has_ids and self.channel_type is None:
|
||
raise ValidationError(
|
||
"channel_type",
|
||
"channel_type is required when account_ids are provided",
|
||
)
|
||
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-STATE,action=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 (),
|
||
channel_type=payload.channel_type,
|
||
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-STATE,action=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 (),
|
||
channel_type=payload.channel_type,
|
||
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)}
|