ForcePilot/backend/server/routers/channels/login_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

318 lines
13 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.

"""登录流程域 RouterLGN-QR-01 / LGN-QR-02 / LGN-01 / LGN-02 / LGN-OUT-01 / LGN-STATUS / LGN-FORCE-LOGOUT
本 router 实现渠道账户登录流程域的全部 HTTP 端点,覆盖扫码启动、扫码轮询、
取消扫码、刷新二维码、登出、登录态查询、强制下线共 7 个操作。所有端点统一
采用模板 A控制面端口路由通过 ``get_channel_use_cases`` 装配
``ChannelUseCases``,经 ``login`` 端口调用类型化方法,由
``ChannelControlService`` 内部走控制面管道
auth → permission → rate_limit → dispatch → audit
鉴权策略:全部端点使用 ``get_admin_user`` 依赖,要求管理员或超级管理员角色。
角色校验由依赖函数完成router 内不做角色判断(规范 §4
模板选型:模板 A控制面端口路由——端点函数体仅做 ``operator = build_operator``
→ ``use_cases.login.<method>`` → ``raiseOnControlFailure`` →
``return {"success": True, "data": serialize_control_data(result.data)}``
不包含 try/except 吞异常、直接 DB 访问、ORM 模型返回或 ``executeControl``
直接调用。
路径设计:所有端点共享 ``/{channel_type}/accounts/{account_id}/login/`` 前缀,
其下 ``qr/start``、``qr/wait``、``qr/cancel``、``qr/refresh``、``logout``、
``status``、``force-logout`` 均为静态路径段,无静态/动态路径冲突;子 router
不自行设置 prefix根前缀 ``/channels`` 由 ``channels_router`` 聚合 router
统一追加。
端点清单对应《登录流程域设计方案》§2.1
- POST /{channel_type}/accounts/{account_id}/login/qr/start LGN-QR-01 start_qr_login
- GET /{channel_type}/accounts/{account_id}/login/qr/wait LGN-QR-02 wait_qr_login
- POST /{channel_type}/accounts/{account_id}/login/qr/cancel LGN-01 cancel_qr_login
- POST /{channel_type}/accounts/{account_id}/login/qr/refresh LGN-02 refresh_qr_login
- POST /{channel_type}/accounts/{account_id}/login/logout LGN-OUT-01 logout
- GET /{channel_type}/accounts/{account_id}/login/status LGN-STATUS get_login_status
- POST /{channel_type}/accounts/{account_id}/login/force-logout LGN-FORCE-LOGOUT force_logout
"""
from __future__ import annotations
from typing import Any
from fastapi import APIRouter, Depends, Query, Request
from pydantic import BaseModel, ConfigDict, Field
from yuxi.channels.contract.dtos.channel import ChannelType
from yuxi.channels.contract.dtos.login import ForceLogoutCmd
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
login_router = APIRouter(tags=["channels-login"])
class ForceLogoutRequest(BaseModel):
"""强制下线请求体LGN-FORCE-LOGOUT"""
model_config = ConfigDict(frozen=True)
reason: str = Field(..., description="强制下线原因(安全审计用,必填)")
revoke_credentials: bool = Field(
default=False,
description="是否同时吊销凭据(默认 False",
)
@login_router.post(
"/{channel_type}/accounts/{account_id}/login/qr/start",
response_model=dict,
)
async def start_qr_login(
channel_type: ChannelType,
account_id: str,
request: Request,
force: bool = Query(
default=False,
description="是否强制启动新会话(取消既有活跃会话)",
),
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""启动扫码登录LGN-QR-01
对应控制面操作 ``login/qr_start``(由 ``ChannelControlService.startQrLogin``
内部构造 ``ControlCmd`` 并委托 ``_executeControl`` 执行控制面管道)。
编排链路HTTP 入参 → ``build_operator`` 构造操作人 →
``use_cases.login.startQrLogin`` 调用端口方法 → 控制面管道调度插件
``LoginAdapter.loginWithQrStart`` 生成会话与二维码 → ``ControlResult`` 经
``raiseOnControlFailure`` 转译失败 → ``serialize_control_data`` 序列化
会话与二维码数据返回。
"""
operator = build_operator(current_user, request)
result = await use_cases.login.startQrLogin(
channel_type=channel_type,
account_id=account_id,
operator=operator,
force=force,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@login_router.get(
"/{channel_type}/accounts/{account_id}/login/qr/wait",
response_model=dict,
)
async def wait_qr_login(
channel_type: ChannelType,
account_id: str,
request: Request,
session_id: str = Query(..., description="扫码会话 ID"),
current_qr_data_url: str | None = Query(
default=None,
description="客户端当前持有的二维码 URL用于变更检测",
),
timeout_ms: int | None = Query(
default=None,
ge=1000,
le=60000,
description="长轮询超时(毫秒)",
),
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""轮询扫码登录状态LGN-QR-02
对应控制面操作 ``login/qr_wait``(由 ``ChannelControlService.waitQrLogin``
内部构造 ``ControlCmd`` 并委托 ``_executeControl`` 执行控制面管道)。
采用 long-polling 模式:服务端在 ``timeout_ms`` 窗口内阻塞等待状态变化
(已扫码 / 已确认 / 已过期 / 二维码刷新),状态变化或超时立即返回,由
客户端再次发起轮询。``current_qr_data_url`` 用于变更检测,仅当二维码
已刷新时返回新的 URL。编排链路HTTP 入参 → ``build_operator`` →
``use_cases.login.waitQrLogin`` → 控制面管道调度插件
``LoginAdapter.loginWithQrWait`` → ``ControlResult`` 经 ``raiseOnControlFailure``
转译失败 → ``serialize_control_data`` 序列化会话状态返回。
"""
operator = build_operator(current_user, request)
result = await use_cases.login.waitQrLogin(
channel_type=channel_type,
account_id=account_id,
operator=operator,
session_id=session_id,
current_qr_data_url=current_qr_data_url,
timeout_ms=timeout_ms,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@login_router.post(
"/{channel_type}/accounts/{account_id}/login/qr/cancel",
response_model=dict,
)
async def cancel_qr_login(
channel_type: ChannelType,
account_id: str,
request: Request,
session_id: str = Query(..., description="待取消的扫码会话 ID"),
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""取消扫码登录LGN-01
对应控制面操作 ``login/qr_cancel``(由 ``ChannelControlService.cancelQrLogin``
内部构造 ``ControlCmd`` 并委托 ``_executeControl`` 执行控制面管道)。
编排链路HTTP 入参 → ``build_operator`` 构造操作人 →
``use_cases.login.cancelQrLogin`` 调用端口方法 → 控制面管道调度
``QrLoginService.cancelQrLogin`` 推进本地状态机至 CANCELLED 并清理
内存会话映射(不调用插件 LoginAdapter→ ``ControlResult`` 经
``raiseOnControlFailure`` 转译失败 → ``serialize_control_data`` 序列化
会话终结结果返回。
"""
operator = build_operator(current_user, request)
result = await use_cases.login.cancelQrLogin(
channel_type=channel_type,
account_id=account_id,
operator=operator,
session_id=session_id,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@login_router.post(
"/{channel_type}/accounts/{account_id}/login/qr/refresh",
response_model=dict,
)
async def refresh_qr_login(
channel_type: ChannelType,
account_id: str,
request: Request,
session_id: str = Query(..., description="待刷新的扫码会话 ID"),
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""刷新扫码二维码LGN-02
对应控制面操作 ``login/qr_refresh``(由 ``ChannelControlService.refreshQrLogin``
内部构造 ``ControlCmd`` 并委托 ``_executeControl`` 执行控制面管道)。
编排链路HTTP 入参 → ``build_operator`` 构造操作人 →
``use_cases.login.refreshQrLogin`` 调用端口方法 → 控制面管道调度
``QrLoginService.refreshQrLogin`` 复用插件 ``LoginAdapter.loginWithQrStart``
重新生成二维码并刷新过期时间 → ``ControlResult`` 经
``raiseOnControlFailure`` 转译失败 → ``serialize_control_data`` 序列化
新二维码数据返回。
"""
operator = build_operator(current_user, request)
result = await use_cases.login.refreshQrLogin(
channel_type=channel_type,
account_id=account_id,
operator=operator,
session_id=session_id,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@login_router.post(
"/{channel_type}/accounts/{account_id}/login/logout",
response_model=dict,
)
async def logout(
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]:
"""登出渠道账户LGN-OUT-01
对应控制面操作 ``login/logout``(由 ``ChannelControlService.logout``
内部构造 ``ControlCmd`` 并委托 ``_executeControl`` 执行控制面管道)。
编排链路HTTP 入参 → ``build_operator`` 构造操作人 →
``use_cases.login.logout`` 调用端口方法 → 控制面管道调度插件
``LoginAdapter.logout`` 清理渠道侧会话与凭据 → ``ControlResult`` 经
``raiseOnControlFailure`` 转译失败 → ``serialize_control_data`` 序列化
登出结果返回。幂等性:未登录或已登出的账户再次调用幂等返回成功。
"""
operator = build_operator(current_user, request)
result = await use_cases.login.logout(
channel_type=channel_type,
account_id=account_id,
operator=operator,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@login_router.get(
"/{channel_type}/accounts/{account_id}/login/status",
response_model=dict,
)
async def get_login_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]:
"""查询账户登录态LGN-STATUS
对应控制面操作 ``login/status``(由 ``ChannelControlService.getLoginStatus``
内部构造 ``ControlCmd`` 并委托 ``_executeControl`` 执行控制面管道)。
供管理员排查"机器人无响应"时确认账户扫码会话状态,仅返回内存态扫码
会话信息(不查询渠道侧 API轻量查询。编排链路HTTP 入参 →
``build_operator`` 构造操作人 → ``use_cases.login.getLoginStatus`` 调用
端口方法 → 控制面管道调度 ``QrLoginService`` 查询内存态扫码会话 →
``ControlResult`` 经 ``raiseOnControlFailure`` 转译失败 →
``serialize_control_data`` 序列化登录态结果返回。
"""
operator = build_operator(current_user, request)
result = await use_cases.login.getLoginStatus(
channel_type=channel_type,
account_id=account_id,
operator=operator,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@login_router.post(
"/{channel_type}/accounts/{account_id}/login/force-logout",
response_model=dict,
)
async def force_logout(
channel_type: ChannelType,
account_id: str,
request: Request,
body: ForceLogoutRequest,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""强制下线渠道账户LGN-FORCE-LOGOUT
对应控制面操作 ``login/force_logout``(由 ``ChannelControlService.forceLogout``
内部构造 ``ControlCmd`` 并委托 ``_executeControl`` 执行控制面管道)。
管理员发起的强制下线可选吊销凭据。编排链路HTTP 入参 →
``build_operator`` 构造操作人 → 构造 ``ForceLogoutCmd`` →
``use_cases.login.forceLogout`` 调用端口方法 → 控制面管道校验账户
存在性与登录态 → 调度 ``QrLoginService.forceLogout``(内部检查适配器
是否实现 ``forceLogout`` + 调用适配器)→ 发布 ``ChannelAccountOffline``
事件 → ``ControlResult`` 经 ``raiseOnControlFailure`` 转译失败 →
``serialize_control_data`` 序列化强制下线结果返回。
"""
operator = build_operator(current_user, request)
cmd = ForceLogoutCmd(
channel_type=channel_type,
account_id=account_id,
operator=operator,
reason=body.reason,
revoke_credentials=body.revoke_credentials,
)
result = await use_cases.login.forceLogout(cmd)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}