ForcePilot/backend/server/routers/channels/session_router.py

298 lines
12 KiB
Python
Raw Normal View History

"""会话资源域 Router。
提供渠道会话管理的 6 HTTP 端点全部由 ``get_admin_user`` 守门仅管理员
可访问Router 不含任何业务逻辑仅做协议翻译 HTTP 请求参数组装为契约层
命令/参数调用 ``use_cases.session_management`` 的类型化方法走控制面管道
再通过 ``raiseOnControlFailure`` 转译失败``serialize_control_data`` 序列化
成功结果模板 A
端点清单
- GET /sessions SES-QUERY-01 列出渠道会话
- POST /sessions/batch-close SES-BATCH-CLOSE-01 批量关闭会话
- GET /sessions/{session_id} SES-QUERY-02 查询会话详情
- POST /sessions/{session_id}/merge SES-05 合并会话FR-07
- POST /sessions/{session_id}/transfer SES-13 转移会话所有者FR-26
- POST /sessions/{session_id}/close SES-CLOSE-01 关闭会话
- GET /sessions/{session_id}/messages SES-MSG-01 查询会话消息流
- GET /sessions/{session_id}/stats SES-STATS-01 查询会话统计指标
"""
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.conversation import MergeConversationCmd
from yuxi.channels.contract.dtos.session import (
BatchCloseSessionsCmd,
CloseSessionCmd,
OwnerTransferCmd,
)
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
session_router = APIRouter(tags=["channels-session"])
class MergeSessionRequest(BaseModel):
"""合并会话请求体。``target_conversation_id`` 由路径参数提供,不在 body 中。"""
model_config = ConfigDict(frozen=True)
source_conversation_id: str = Field(..., min_length=1, description="源会话 ID被合并方")
reason: str = Field(default="", max_length=512, description="合并原因(审计用)")
class TransferOwnerRequest(BaseModel):
"""转移所有者请求体。``conversation_id`` 由路径参数提供,不在 body 中。"""
model_config = ConfigDict(frozen=True)
new_owner_id: str = Field(..., min_length=1, description="新所有者对端 ID")
class CloseSessionRequest(BaseModel):
"""关闭会话请求体。``session_id`` 由路径参数提供,不在 body 中。"""
model_config = ConfigDict(frozen=True)
reason: str | None = Field(default=None, max_length=512, description="关闭原因(审计用)")
class BatchCloseSessionsRequest(BaseModel):
"""批量关闭会话请求体SES-BATCH-CLOSE-01
``session_ids`` ``filter`` 二者不可同时为空 dispatch handler 校验
"""
model_config = ConfigDict(frozen=True)
session_ids: list[str] | None = Field(
default=None,
description="显式会话 ID 列表(与 filter 二选一)",
)
filter: dict[str, Any] | None = Field(
default=None,
description="筛选条件(含 channel_type / inactive_before / status",
)
max_count: int = Field(default=100, ge=1, le=1000, description="单次最大关闭数")
reason: str | None = Field(default=None, max_length=512, description="关闭原因(审计用)")
@session_router.get("/sessions", response_model=dict)
async def list_sessions(
request: Request,
channel_type: ChannelType | None = Query(default=None, description="按渠道类型过滤,留空跨渠道查询"),
peer_id: str | None = Query(default=None, description="按对端 ID 模糊匹配"),
limit: int = Query(default=100, 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]:
"""列出渠道会话运维排查用例SES-QUERY-01
供管理员排查"用户消息未到达"等运维问题``channel_type`` 留空时跨渠道查询
``peer_id`` 提供时按对端 ID 模糊匹配对应控制面操作 ``session/list``
"""
operator = build_operator(current_user, request)
result = await use_cases.session_management.listSessions(
channel_type=channel_type,
limit=limit,
operator=operator,
offset=offset,
peer_id=peer_id,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@session_router.post("/sessions/batch-close", response_model=dict)
async def batch_close_sessions(
payload: BatchCloseSessionsRequest,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""批量关闭渠道会话SES-BATCH-CLOSE-01
支持 ``session_ids`` 显式列表与 ``filter`` 筛选条件两种模式二者不可
同时空 dispatch handler 校验逐条独立事务复用单条 ``closeSession``
逻辑模式 D部分成功语义单条失败不回滚已成功条目对应控制面
操作 ``session/batch_close``
静态路径先于 ``/sessions/{session_id}`` 声明避免被动态参数捕获
"""
operator = build_operator(current_user, request)
cmd = BatchCloseSessionsCmd(
operator=operator,
session_ids=tuple(payload.session_ids) if payload.session_ids else (),
filter=payload.filter,
max_count=payload.max_count,
reason=payload.reason,
)
result = await use_cases.session_management.batchCloseSessions(cmd)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@session_router.get("/sessions/{session_id}", response_model=dict)
async def get_session(
session_id: str,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""按 session_id 查询渠道会话详情SES-QUERY-02
供管理员审批 mergeSession 前核对源/目标会话信息软删除的会话返回 404
对应控制面操作 ``session/get``
"""
operator = build_operator(current_user, request)
result = await use_cases.session_management.getSession(
session_id=session_id,
operator=operator,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@session_router.post("/sessions/{session_id}/merge", response_model=dict)
async def merge_session(
session_id: str,
payload: MergeSessionRequest,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""合并会话FR-07SES-05
将源会话合并至目标会话路径 ``session_id``迁移消息并软删除源会话
``merge_strategy_enabled`` 配置策略门控制falsy 时返回 422
对应控制面操作 ``session/merge``
协议翻译路径参数 ``session_id`` ``MergeConversationCmd.target_conversation_id``
"""
operator = build_operator(current_user, request)
cmd = MergeConversationCmd(
source_conversation_id=payload.source_conversation_id,
target_conversation_id=session_id,
operator=operator,
reason=payload.reason,
)
result = await use_cases.session_management.mergeSession(cmd)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@session_router.post("/sessions/{session_id}/transfer", response_model=dict)
async def transfer_session_owner(
session_id: str,
payload: TransferOwnerRequest,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""转移会话所有者FR-26SES-13
将会话路径 ``session_id``所有者从当前对端转移至新对端 DB 写入
加入控制面管道事务审计日志由 AuditStage SHARED 事务中统一写入fail-closed
对应控制面操作 ``session/transfer_owner``
协议翻译路径参数 ``session_id`` ``OwnerTransferCmd.conversation_id``
"""
operator = build_operator(current_user, request)
cmd = OwnerTransferCmd(
conversation_id=session_id,
new_owner_id=payload.new_owner_id,
operator=operator,
)
result = await use_cases.session_management.transferSessionOwner(cmd)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@session_router.post("/sessions/{session_id}/close", response_model=dict)
async def close_session(
session_id: str,
payload: CloseSessionRequest,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""关闭渠道会话SES-CLOSE-01
主动关闭指定会话停止接收新消息已关闭会话不可重新激活
对应控制面操作 ``session/close``
协议翻译路径参数 ``session_id`` ``CloseSessionCmd.session_id``
"""
operator = build_operator(current_user, request)
cmd = CloseSessionCmd(
session_id=session_id,
reason=payload.reason,
operator=operator,
)
result = await use_cases.session_management.closeSession(cmd)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@session_router.get("/sessions/{session_id}/messages", response_model=dict)
async def list_session_messages(
session_id: str,
request: Request,
limit: int = Query(default=50, ge=1, le=200, 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]:
"""查询会话消息流SES-MSG-01
按时间顺序列出会话内的消息支持分页
对应控制面操作 ``session/list_messages``
"""
operator = build_operator(current_user, request)
result = await use_cases.session_management.listSessionMessages(
session_id=session_id,
limit=limit,
offset=offset,
operator=operator,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@session_router.get("/sessions/{session_id}/stats", response_model=dict)
async def get_session_stats(
session_id: str,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""查询单会话统计指标SES-STATS-01
``session_id`` 查询会话的统计指标消息计数首响时间平均响应
会话时长等 dispatch handler 组合 ConversationPort 消息查询计算
对应控制面操作 ``session/stats``
协议翻译路径参数 ``session_id`` 直接传入
``getSessionStats(session_id=..., operator=...)``
"""
operator = build_operator(current_user, request)
result = await use_cases.session_management.getSessionStats(
session_id=session_id,
operator=operator,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}