1. 新增统一分页常量定义,替换各路由零散的Query参数声明 2. 批量替换多个列表接口的分页参数为预设常量 3. 优化插件目录、消息搜索等接口的参数与文档 4. 修复死信导出接口的参数命名不一致问题
585 lines
23 KiB
Python
585 lines
23 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 中。
|
||
"""
|
||
|
||
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-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 (),
|
||
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 (),
|
||
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)}
|