2026-07-04 00:16:00 +08:00
|
|
|
|
"""安装向导域 Router(WIZ-01~WIZ-06)。
|
2026-07-02 03:29:06 +08:00
|
|
|
|
|
|
|
|
|
|
本 router 实现渠道插件安装向导流程的 HTTP 端点,覆盖步骤查询 / 校验 /
|
2026-07-04 00:16:00 +08:00
|
|
|
|
应用 / 完成 / OAuth 发起 / OAuth 回调共 6 个操作。所有端点统一采用模板 A
|
|
|
|
|
|
(控制面端口路由),通过 ``get_channel_use_cases`` 装配 ``ChannelUseCases``,
|
|
|
|
|
|
经 ``wizard`` 端口调用类型化方法,由 ``ChannelControlService._executeControl``
|
2026-07-02 03:29:06 +08:00
|
|
|
|
内部走控制面管道(auth → permission → rate_limit → dispatch → audit)。
|
|
|
|
|
|
|
|
|
|
|
|
鉴权策略:全部端点使用 ``get_admin_user`` 依赖,要求管理员或超级管理员角色。
|
|
|
|
|
|
角色校验由依赖函数完成,router 内不做角色判断(规范 §4)。
|
|
|
|
|
|
|
|
|
|
|
|
路径设计:所有端点路径均为动态 ``/{channel_type}/wizard/*``,子 router
|
|
|
|
|
|
不自行设置 prefix,根前缀 ``/channels`` 由 ``channels_router`` 聚合 router
|
|
|
|
|
|
统一追加。
|
|
|
|
|
|
|
|
|
|
|
|
端点清单(对应《20-channels-wizard-domain-v1.0.md》§2.1):
|
2026-07-04 00:16:00 +08:00
|
|
|
|
- GET /{channel_type}/wizard/steps WIZ-01 get_wizard_steps
|
|
|
|
|
|
- POST /{channel_type}/wizard/steps/{step_id}/validate WIZ-02 validate_wizard_step
|
|
|
|
|
|
- POST /{channel_type}/wizard/steps/{step_id}/apply WIZ-03 apply_wizard_step
|
|
|
|
|
|
- POST /{channel_type}/wizard/finalize WIZ-04 finalize_wizard
|
|
|
|
|
|
- POST /{channel_type}/wizard/oauth/initiate WIZ-05 initiate_wizard_oauth
|
|
|
|
|
|
- POST /{channel_type}/wizard/oauth-callback WIZ-06 handle_wizard_oauth_callback
|
2026-07-02 03:29:06 +08:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
from typing import Any
|
2026-07-04 00:16:00 +08:00
|
|
|
|
from urllib.parse import urlparse
|
2026-07-02 03:29:06 +08:00
|
|
|
|
|
2026-07-04 00:16:00 +08:00
|
|
|
|
from fastapi import APIRouter, Depends, Request
|
|
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
2026-07-02 03:29:06 +08:00
|
|
|
|
from yuxi.channels.contract.dtos.channel import ChannelType
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
wizard_router = APIRouter(tags=["channels-wizard"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------- Request Schemas ----------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ValidateWizardRequest(BaseModel):
|
2026-07-04 00:16:00 +08:00
|
|
|
|
"""校验向导步骤请求。字段对齐 ``WizardPort.validateWizardStep`` 入参。
|
|
|
|
|
|
|
|
|
|
|
|
``step_id`` 作为路径参数(与 ``applyWizardStep`` 一致,遵循项目 RESTful
|
|
|
|
|
|
资源定位规范:步骤 ID 在路径中标识资源,body 仅承载字段值)。
|
|
|
|
|
|
"""
|
2026-07-02 03:29:06 +08:00
|
|
|
|
|
|
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
|
|
|
|
|
|
|
|
values: dict[str, Any] = Field(..., description="步骤字段值字典")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ApplyWizardRequest(BaseModel):
|
|
|
|
|
|
"""应用向导步骤请求。字段对齐 ``WizardPort.applyWizardStep`` 入参。"""
|
|
|
|
|
|
|
|
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
|
|
|
|
|
|
|
|
values: dict[str, Any] = Field(..., description="步骤字段值字典")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class WizardPatch(BaseModel):
|
|
|
|
|
|
"""向导配置补丁。字段对齐 ``WizardConfigPatch`` 值对象。
|
|
|
|
|
|
|
|
|
|
|
|
每项补丁含 ``step_id``(用于校验步骤存在性)与 ``values``(配置键值对),
|
|
|
|
|
|
在 router 层把好入口关,避免弱类型 ``dict[str, Any]`` 掩盖契约违反。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
|
|
|
|
|
|
|
|
step_id: str = Field(..., min_length=1, description="向导步骤 ID")
|
|
|
|
|
|
values: dict[str, Any] = Field(..., description="步骤字段值字典")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class FinalizeWizardRequest(BaseModel):
|
|
|
|
|
|
"""完成向导请求。字段对齐 ``WizardPort.finalizeWizard`` 入参。
|
|
|
|
|
|
|
|
|
|
|
|
``patches`` 至少含一项(Port 前置条件要求非空),每项结构由
|
|
|
|
|
|
``WizardPatch`` 校验。转换为 ``list[dict[str, Any]]`` 后传入 Port,
|
|
|
|
|
|
保持 Port 签名不变。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
|
|
|
|
|
|
|
|
patches: list[WizardPatch] = Field(..., min_length=1, description="配置补丁列表")
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-04 00:16:00 +08:00
|
|
|
|
class OAuthCallbackRequest(BaseModel):
|
|
|
|
|
|
"""OAuth 回调请求。字段对齐 ``WizardPort.handleWizardOAuthCallback`` 入参。
|
|
|
|
|
|
|
|
|
|
|
|
``code`` 为 OAuth 授权码(敏感凭证),通过 request body 传递避免
|
|
|
|
|
|
出现在 URL query string 中被日志 / 浏览器历史 / Referer 头泄漏。
|
|
|
|
|
|
``redirect_uri`` 必须为合法 URL 且 scheme 为 http/https,生产环境要求
|
|
|
|
|
|
HTTPS(由 ``WizardPort.handleWizardOAuthCallback`` @pre 强制),本地
|
|
|
|
|
|
开发环境允许 localhost HTTP 以便联调。
|
|
|
|
|
|
``state`` 为 ``POST /wizard/oauth/initiate`` 端点返回的 CSRF 防护参数,
|
|
|
|
|
|
服务端会与发起时存储的 state 比对,不匹配时拒绝处理。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
|
|
|
|
|
|
|
|
code: str = Field(..., min_length=1, description="OAuth 授权码")
|
|
|
|
|
|
state: str = Field(..., min_length=1, description="CSRF 防护参数(由 oauth/initiate 端点返回)")
|
|
|
|
|
|
redirect_uri: str = Field(..., description="回调重定向 URI")
|
|
|
|
|
|
|
|
|
|
|
|
@field_validator("redirect_uri")
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def _validate_redirect_uri_scheme(cls, v: str) -> str:
|
|
|
|
|
|
parsed = urlparse(v)
|
|
|
|
|
|
if parsed.scheme.lower() not in ("http", "https") or not parsed.netloc:
|
|
|
|
|
|
raise ValueError("redirect_uri must be a valid http(s) URL")
|
|
|
|
|
|
return v
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class OAuthInitiateRequest(BaseModel):
|
|
|
|
|
|
"""OAuth 发起请求。字段对齐 ``WizardPort.initiateWizardOAuth`` 入参。
|
|
|
|
|
|
|
|
|
|
|
|
``redirect_uri`` 的 scheme 校验与 ``OAuthCallbackRequest`` 一致,允许
|
|
|
|
|
|
localhost HTTP 用于本地联调。服务端会绑定 ``redirect_uri`` 与生成的
|
|
|
|
|
|
state,后续 ``oauth-callback`` 请求中的 ``redirect_uri`` 必须完全一致,
|
|
|
|
|
|
防止篡改。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
|
|
|
|
|
|
|
|
redirect_uri: str = Field(..., description="回调重定向 URI(需与后续 oauth-callback 一致)")
|
|
|
|
|
|
|
|
|
|
|
|
@field_validator("redirect_uri")
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def _validate_redirect_uri_scheme(cls, v: str) -> str:
|
|
|
|
|
|
parsed = urlparse(v)
|
|
|
|
|
|
if parsed.scheme.lower() not in ("http", "https") or not parsed.netloc:
|
|
|
|
|
|
raise ValueError("redirect_uri must be a valid http(s) URL")
|
|
|
|
|
|
return v
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-06 20:50:03 +08:00
|
|
|
|
class QrLoginWaitRequest(BaseModel):
|
|
|
|
|
|
"""扫码登录轮询请求。字段对齐 ``WizardPort.waitWizardQrLogin`` 入参。"""
|
|
|
|
|
|
|
|
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
|
|
|
|
|
|
|
|
session_id: str = Field(..., min_length=1, description="扫码会话 ID(由 qr/initiate 返回)")
|
|
|
|
|
|
current_qr_data_url: str | None = Field(None, description="当前二维码数据 URL(用于刷新场景)")
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-02 03:29:06 +08:00
|
|
|
|
# ---------------- Endpoints ----------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@wizard_router.get("/{channel_type}/wizard/steps", response_model=dict)
|
|
|
|
|
|
async def get_wizard_steps(
|
|
|
|
|
|
channel_type: ChannelType,
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
use_cases=Depends(get_channel_use_cases),
|
|
|
|
|
|
current_user: User = Depends(get_admin_user),
|
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
|
"""查询安装向导步骤列表(WIZ-01)。
|
|
|
|
|
|
|
|
|
|
|
|
对应控制面操作 ``wizard/steps``(由 ``ChannelControlService.getWizardSteps``
|
|
|
|
|
|
内部构造 ``ControlCmd`` 并委托 ``_executeControl`` 执行控制面管道)。
|
|
|
|
|
|
步骤字段从已保存的向导状态预填充默认值,支持从现有配置恢复向导进度。
|
|
|
|
|
|
"""
|
|
|
|
|
|
operator = build_operator(current_user, request)
|
|
|
|
|
|
result = await use_cases.wizard.getWizardSteps(
|
|
|
|
|
|
channel_type=channel_type,
|
|
|
|
|
|
operator=operator,
|
|
|
|
|
|
)
|
|
|
|
|
|
raiseOnControlFailure(result)
|
|
|
|
|
|
return {"success": True, "data": serialize_control_data(result.data)}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-04 00:16:00 +08:00
|
|
|
|
@wizard_router.post("/{channel_type}/wizard/steps/{step_id}/validate", response_model=dict)
|
2026-07-02 03:29:06 +08:00
|
|
|
|
async def validate_wizard_step(
|
|
|
|
|
|
channel_type: ChannelType,
|
2026-07-04 00:16:00 +08:00
|
|
|
|
step_id: str,
|
2026-07-02 03:29:06 +08:00
|
|
|
|
payload: ValidateWizardRequest,
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
use_cases=Depends(get_channel_use_cases),
|
|
|
|
|
|
current_user: User = Depends(get_admin_user),
|
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
|
"""校验向导步骤字段值(WIZ-02)。
|
|
|
|
|
|
|
|
|
|
|
|
对应控制面操作 ``wizard/validate``(由 ``ChannelControlService.validateWizardStep``
|
|
|
|
|
|
内部构造 ``ControlCmd`` 并委托 ``_executeControl`` 执行控制面管道)。
|
|
|
|
|
|
仅校验字段值,不应用配置变更。校验通过时返回携带配置补丁的结果。
|
2026-07-04 00:16:00 +08:00
|
|
|
|
|
|
|
|
|
|
路径设计对齐 ``applyWizardStep``:``step_id`` 作为路径参数标识步骤资源,
|
|
|
|
|
|
body 仅承载 ``values`` 字段值,与 ``apply`` 端点保持 API 表面一致。
|
2026-07-02 03:29:06 +08:00
|
|
|
|
"""
|
|
|
|
|
|
operator = build_operator(current_user, request)
|
|
|
|
|
|
result = await use_cases.wizard.validateWizardStep(
|
|
|
|
|
|
channel_type=channel_type,
|
2026-07-04 00:16:00 +08:00
|
|
|
|
step_id=step_id,
|
2026-07-02 03:29:06 +08:00
|
|
|
|
values=payload.values,
|
|
|
|
|
|
operator=operator,
|
|
|
|
|
|
)
|
|
|
|
|
|
raiseOnControlFailure(result)
|
|
|
|
|
|
return {"success": True, "data": serialize_control_data(result.data)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@wizard_router.post("/{channel_type}/wizard/steps/{step_id}/apply", response_model=dict)
|
|
|
|
|
|
async def apply_wizard_step(
|
|
|
|
|
|
channel_type: ChannelType,
|
|
|
|
|
|
step_id: str,
|
|
|
|
|
|
payload: ApplyWizardRequest,
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
use_cases=Depends(get_channel_use_cases),
|
|
|
|
|
|
current_user: User = Depends(get_admin_user),
|
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
|
"""应用向导步骤(WIZ-03)。
|
|
|
|
|
|
|
|
|
|
|
|
对应控制面操作 ``wizard/apply``(由 ``ChannelControlService.applyWizardStep``
|
|
|
|
|
|
内部构造 ``ControlCmd`` 并委托 ``_executeControl`` 执行控制面管道)。
|
|
|
|
|
|
先校验步骤字段值,校验通过后应用步骤并将配置补丁合并到向导状态。
|
|
|
|
|
|
校验失败时不应用配置(AC-39)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
operator = build_operator(current_user, request)
|
|
|
|
|
|
result = await use_cases.wizard.applyWizardStep(
|
|
|
|
|
|
channel_type=channel_type,
|
|
|
|
|
|
step_id=step_id,
|
|
|
|
|
|
values=payload.values,
|
|
|
|
|
|
operator=operator,
|
|
|
|
|
|
)
|
|
|
|
|
|
raiseOnControlFailure(result)
|
|
|
|
|
|
return {"success": True, "data": serialize_control_data(result.data)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@wizard_router.post("/{channel_type}/wizard/finalize", response_model=dict)
|
|
|
|
|
|
async def finalize_wizard(
|
|
|
|
|
|
channel_type: ChannelType,
|
|
|
|
|
|
payload: FinalizeWizardRequest,
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
use_cases=Depends(get_channel_use_cases),
|
|
|
|
|
|
current_user: User = Depends(get_admin_user),
|
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
|
"""完成安装向导(WIZ-04)。
|
|
|
|
|
|
|
|
|
|
|
|
对应控制面操作 ``wizard/finalize``(由 ``ChannelControlService.finalizeWizard``
|
|
|
|
|
|
内部构造 ``ControlCmd`` 并委托 ``_executeControl`` 执行控制面管道)。
|
|
|
|
|
|
聚合请求中的配置补丁与向导状态中的已应用补丁,写入渠道配置并清除向导状态。
|
|
|
|
|
|
"""
|
|
|
|
|
|
operator = build_operator(current_user, request)
|
|
|
|
|
|
result = await use_cases.wizard.finalizeWizard(
|
|
|
|
|
|
channel_type=channel_type,
|
|
|
|
|
|
patches=[patch.model_dump() for patch in payload.patches],
|
|
|
|
|
|
operator=operator,
|
|
|
|
|
|
)
|
|
|
|
|
|
raiseOnControlFailure(result)
|
|
|
|
|
|
return {"success": True, "data": serialize_control_data(result.data)}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-04 00:16:00 +08:00
|
|
|
|
@wizard_router.post("/{channel_type}/wizard/oauth/initiate", response_model=dict)
|
|
|
|
|
|
async def initiate_wizard_oauth(
|
|
|
|
|
|
channel_type: ChannelType,
|
|
|
|
|
|
payload: OAuthInitiateRequest,
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
use_cases=Depends(get_channel_use_cases),
|
|
|
|
|
|
current_user: User = Depends(get_admin_user),
|
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
|
"""发起向导 OAuth 授权(WIZ-05)。
|
|
|
|
|
|
|
|
|
|
|
|
对应控制面操作 ``wizard/oauth-initiate``(由 ``ChannelControlService.initiateWizardOAuth``
|
|
|
|
|
|
内部构造 ``ControlCmd`` 并委托 ``_executeControl`` 执行控制面管道)。
|
|
|
|
|
|
生成服务端 CSRF state 并存储到向导状态,前端获取 state 后附加到
|
|
|
|
|
|
OAuth 授权 URL 跳转到渠道授权页。
|
|
|
|
|
|
|
|
|
|
|
|
前端必须在调用 ``oauth-callback`` 前先调用本端点获取 state,否则
|
|
|
|
|
|
回调会被拒绝(``state`` 不匹配)。``redirect_uri`` 需与后续 ``oauth-callback``
|
|
|
|
|
|
请求中的值完全一致,服务端会绑定校验。
|
|
|
|
|
|
"""
|
|
|
|
|
|
operator = build_operator(current_user, request)
|
|
|
|
|
|
result = await use_cases.wizard.initiateWizardOAuth(
|
|
|
|
|
|
channel_type=channel_type,
|
|
|
|
|
|
redirect_uri=payload.redirect_uri,
|
|
|
|
|
|
operator=operator,
|
|
|
|
|
|
)
|
|
|
|
|
|
raiseOnControlFailure(result)
|
|
|
|
|
|
return {"success": True, "data": serialize_control_data(result.data)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@wizard_router.post("/{channel_type}/wizard/oauth-callback", response_model=dict)
|
2026-07-02 03:29:06 +08:00
|
|
|
|
async def handle_wizard_oauth_callback(
|
|
|
|
|
|
channel_type: ChannelType,
|
2026-07-04 00:16:00 +08:00
|
|
|
|
payload: OAuthCallbackRequest,
|
2026-07-02 03:29:06 +08:00
|
|
|
|
request: Request,
|
|
|
|
|
|
use_cases=Depends(get_channel_use_cases),
|
|
|
|
|
|
current_user: User = Depends(get_admin_user),
|
|
|
|
|
|
) -> dict[str, Any]:
|
2026-07-04 00:16:00 +08:00
|
|
|
|
"""处理向导 OAuth 回调(WIZ-06)。
|
2026-07-02 03:29:06 +08:00
|
|
|
|
|
|
|
|
|
|
对应控制面操作 ``wizard/oauth-callback``(由 ``ChannelControlService.handleWizardOAuthCallback``
|
|
|
|
|
|
内部构造 ``ControlCmd`` 并委托 ``_executeControl`` 执行控制面管道)。
|
|
|
|
|
|
委托向导适配器交换 OAuth 授权码并产出配置补丁,合并到向导状态。
|
2026-07-04 00:16:00 +08:00
|
|
|
|
|
|
|
|
|
|
CSRF 防护:``state`` 必须与 ``initiate_wizard_oauth`` 端点返回的 state
|
|
|
|
|
|
一致,服务端校验通过后一次性消费 state(防止重放)。未发起授权或
|
|
|
|
|
|
state 不匹配时返回 400。
|
|
|
|
|
|
|
|
|
|
|
|
采用 POST + request body 传递授权码,避免 GET query string 泄漏
|
|
|
|
|
|
敏感凭证(OAuth code 可交换 user_access_token)。``redirect_uri``
|
|
|
|
|
|
在 router 层校验合法 http(s) scheme 与非空 netloc,生产环境强制
|
|
|
|
|
|
HTTPS 由 ``WizardPort.handleWizardOAuthCallback`` @pre 兜底(允许
|
|
|
|
|
|
localhost HTTP 用于本地联调)。响应中的 token 等敏感字段已脱敏。
|
2026-07-02 03:29:06 +08:00
|
|
|
|
"""
|
|
|
|
|
|
operator = build_operator(current_user, request)
|
|
|
|
|
|
result = await use_cases.wizard.handleWizardOAuthCallback(
|
|
|
|
|
|
channel_type=channel_type,
|
2026-07-04 00:16:00 +08:00
|
|
|
|
code=payload.code,
|
|
|
|
|
|
state=payload.state,
|
|
|
|
|
|
redirect_uri=payload.redirect_uri,
|
2026-07-02 03:29:06 +08:00
|
|
|
|
operator=operator,
|
|
|
|
|
|
)
|
|
|
|
|
|
raiseOnControlFailure(result)
|
|
|
|
|
|
return {"success": True, "data": serialize_control_data(result.data)}
|
2026-07-06 20:50:03 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@wizard_router.post("/{channel_type}/wizard/qr/initiate", response_model=dict)
|
|
|
|
|
|
async def initiate_wizard_qr_login(
|
|
|
|
|
|
channel_type: ChannelType,
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
use_cases=Depends(get_channel_use_cases),
|
|
|
|
|
|
current_user: User = Depends(get_admin_user),
|
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
|
"""发起向导扫码登录(WIZ-07)。
|
|
|
|
|
|
|
|
|
|
|
|
对应控制面操作 ``wizard/qr-initiate``(由 ``ChannelControlService.initiateWizardQrLogin``
|
|
|
|
|
|
内部构造 ``ControlCmd`` 并委托 ``_executeControl`` 执行控制面管道)。
|
|
|
|
|
|
在账户尚未创建时复用 ``LoginAdapter`` 获取二维码,返回 session_id 与
|
|
|
|
|
|
qr_data_url 供前端展示并轮询。
|
|
|
|
|
|
"""
|
|
|
|
|
|
operator = build_operator(current_user, request)
|
|
|
|
|
|
result = await use_cases.wizard.initiateWizardQrLogin(
|
|
|
|
|
|
channel_type=channel_type,
|
|
|
|
|
|
operator=operator,
|
|
|
|
|
|
)
|
|
|
|
|
|
raiseOnControlFailure(result)
|
|
|
|
|
|
return {"success": True, "data": serialize_control_data(result.data)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@wizard_router.post("/{channel_type}/wizard/qr/wait", response_model=dict)
|
|
|
|
|
|
async def wait_wizard_qr_login(
|
|
|
|
|
|
channel_type: ChannelType,
|
|
|
|
|
|
payload: QrLoginWaitRequest,
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
use_cases=Depends(get_channel_use_cases),
|
|
|
|
|
|
current_user: User = Depends(get_admin_user),
|
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
|
"""轮询向导扫码登录结果(WIZ-08)。
|
|
|
|
|
|
|
|
|
|
|
|
对应控制面操作 ``wizard/qr-wait``(由 ``ChannelControlService.waitWizardQrLogin``
|
|
|
|
|
|
内部构造 ``ControlCmd`` 并委托 ``_executeControl`` 执行控制面管道)。
|
|
|
|
|
|
扫码成功后返回 credentials,由前端填入当前向导步骤并通过 ``apply`` 提交。
|
|
|
|
|
|
"""
|
|
|
|
|
|
operator = build_operator(current_user, request)
|
|
|
|
|
|
result = await use_cases.wizard.waitWizardQrLogin(
|
|
|
|
|
|
channel_type=channel_type,
|
|
|
|
|
|
session_id=payload.session_id,
|
|
|
|
|
|
current_qr_data_url=payload.current_qr_data_url,
|
|
|
|
|
|
operator=operator,
|
|
|
|
|
|
)
|
|
|
|
|
|
raiseOnControlFailure(result)
|
|
|
|
|
|
return {"success": True, "data": serialize_control_data(result.data)}
|