Files
wecom_it_smart_desk/backend/app/api/automation.py
Simon bea288e414 feat: 2026-07-11 全量更新 - 代办集成+会议室预定+知识迭代修复+UI统一+Bug修复
== 已部署上线 (9项) ==
- 代办事项真实数据源集成 (企微审批API 8bug修复链)
- H5/坐席端 Logo样式统一+绿色背景
- 视频引导页修复 (localStorage key v2)
- 坐席端 v9 Vue版本修复 (ElMessage._context)
- 截图按钮 v10 修复 (getDisplayMedia user gesture)
- 扫码样式恢复+H5扫码登录跳转修复
- H5截图快捷键提示

== 代码完成待部署 (3项) ==
- 知识迭代3Bug修复 (#8 POST端点/#7 MERGE幂等/#6 过期检查)
- 会议室预定-小鱼易联终端 (40文件, 40/40测试通过)
- IT资产升级审批推送 (asset_service.py)

== 需求文档 (2项) ==
- 坐席端AI辅助消息框-PRD (4项新功能确认)
- 坐席端布局优化建议 v2.0 (7天计划)

== 新增文档 ==
- 日报-2026-07-11.md
- 知识迭代Bug修复报告-20260711.md
- 会议室预定-部署指南.md
- CHANGELOG.md 更新

== 测试 ==
- test_todo_integration.py: 40/40
- test_meetingroom.py: 40/40
- test_bugfix_ki_suggestions.py: 21/21
2026-07-11 23:13:10 +08:00

789 lines
28 KiB
Python
Raw Permalink 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.
# =============================================================================
# 企微IT智能服务台 — 阶段5 自动化闭环 API
# =============================================================================
# 说明:提供自动化会话的 REST 接口与专用 WebSocket 通道。
# 前缀(经 Vite/ nginx 剥离 /api 后):/itportal/automation
#
# 坐席端(agent):
# POST /itportal/automation/sessions — 创建并启动会话
# GET /itportal/automation/sessions — 会话列表
# GET /itportal/automation/sessions/{id} — 会话详情
# POST /itportal/automation/sessions/{id}/approve — 坐席审批/驳回
# POST /itportal/automation/sessions/{id}/takeover — 转人工接管
#
# 员工端(H5):
# POST /itportal/automation/sessions/by-employee — 员工创建会话
# GET /itportal/automation/sessions/{id}/employee — 员工查看详情
# POST /itportal/automation/sessions/{id}/confirm — 员工 H5 二次确认
# POST /itportal/automation/sessions/{id}/feedback — 员工结果反馈
#
# 管理端(admin,配置写需 OTP):
# GET /itportal/automation/admin/scenarios — 场景配置列表
# PUT /itportal/automation/admin/scenarios/{key} — 更新场景配置(OTP)
# GET /itportal/automation/admin/rule-versions — 规则版本列表
# GET /itportal/automation/admin/metrics — 看板指标
#
# WebSocket
# /ws/automation/{session_id} — 自动化进度/审批/确认实时推送
# =============================================================================
from __future__ import annotations
import asyncio
import logging
from typing import Optional
from fastapi import APIRouter, Depends, Query, WebSocket, WebSocketDisconnect
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.dependencies import require_high_risk_otp
from app.dependencies.automation import get_current_employee_id
from app.api.agents import get_current_agent
from app.models.agent import Agent
from app.schemas.automation import (
CreateSessionRequest,
ResolveFeedbackRequest,
ScenarioConfigResponse,
ScenarioConfigUpdate,
SessionResponse,
ApprovalDecisionRequest,
ConfirmRequest,
RuleVersionResponse,
ResolveFeedbackRequest,
TakeoverRequest,
AutoMetricsResponse,
serialize_action,
serialize_approval,
serialize_information_item,
serialize_session,
PauseRequest,
ResumeRequest,
CorrectRequest,
SupplementRequest,
AgentResumeRequest,
AgentCloseRequest,
BatchCorrectRequest,
BatchCorrectResponse,
UndoCorrectionResponse,
CorrectionHistoryResponse,
VersionChainResponse,
VersionDiffResponse,
CompressionLogItem,
CompressionLogListResponse,
VersionDiffRequest,
)
from app.services.automation import (
ActionExecutor,
AutoSessionService,
AutomationException,
InformationItemService,
to_app_exception,
)
from app.services.automation.correction_service import CorrectionService
from app.services.automation.snapshot_service import SnapshotService
from app.services.automation.context_compressor import ContextCompressor
from app.models.automation import ContextCompression
from app.services.automation.progress_publisher import (
register_ws,
set_parties,
unregister_ws,
)
from app.services.cache_service import cache_service
from app.utils.response import AppException, success_response
logger = logging.getLogger(__name__)
# --------------------------------------------------------------------------
# REST 路由器(前缀 /itportal/automation,经 /api 代理剥离)
# --------------------------------------------------------------------------
router = APIRouter(prefix="/itportal/automation")
# --------------------------------------------------------------------------
# WebSocket 路由器(根路径 /ws/automation/{session_id}
# --------------------------------------------------------------------------
ws_router = APIRouter()
# WS 认证失败关闭码(与 ws.py 保持一致)
WS_CLOSE_UNAUTHORIZED = 4001
# ==========================================================================
# 坐席端接口
# ==========================================================================
@router.post("/sessions", tags=["自动化闭环"])
async def create_session(
req: CreateSessionRequest,
db: AsyncSession = Depends(get_db),
current_agent: Agent = Depends(get_current_agent),
):
"""坐席创建自动化会话并启动后台编排。"""
svc = AutoSessionService(db)
session = await svc.create_session(
conversation_id=req.conversation_id,
employee_id=req.employee_id,
description=req.description,
mode=req.mode,
)
await db.flush()
# 记录参与方,供进度兜底推送
set_parties(session.id, employee_id=req.employee_id)
await db.commit()
# 后台运行编排(不阻塞响应)
asyncio.create_task(_run_background(session.id))
data = serialize_session(session)
return success_response(data.model_dump() if hasattr(data, "model_dump") else data.__dict__)
@router.get("/sessions", tags=["自动化闭环"])
async def list_sessions(
employee_id: Optional[str] = Query(None),
status: Optional[str] = Query(None),
page: int = Query(1, ge=1),
page_size: int = Query(50, ge=1, le=200),
db: AsyncSession = Depends(get_db),
_agent: Agent = Depends(get_current_agent),
):
"""坐席查看自动化会话列表。"""
svc = AutoSessionService(db)
sessions = await svc.list_sessions(
employee_id=employee_id, status=status, page=page, page_size=page_size
)
return success_response([_session_min(s) for s in sessions])
# --- 复杂场景重构:暂停会话相关端点(必须在 /sessions/{session_id} 之前注册)---
@router.get("/sessions/paused", tags=["自动化闭环"])
async def list_paused_sessions(
employee_id: str = Query(..., description="员工ID"),
db: AsyncSession = Depends(get_db),
_agent: Agent = Depends(get_current_agent),
):
"""获取员工的暂停会话列表。"""
svc = AutoSessionService(db)
paused_list = await svc.list_paused_sessions(employee_id)
return success_response(paused_list)
@router.post("/sessions/resume", tags=["自动化闭环"])
async def resume_session_select(
req: ResumeRequest,
db: AsyncSession = Depends(get_db),
employee_id: str = Depends(get_current_employee_id),
):
"""恢复暂停会话(员工端,支持多任务选择)。
若未指定 session_id 且存在多个暂停会话,返回列表供前端选择。
"""
svc = AutoSessionService(db)
try:
result = await svc.resume_session(employee_id, req.session_id)
except AutomationException as e:
raise to_app_exception(e)
await db.commit()
# 多任务选择场景
if result.get("need_select"):
return success_response({
"need_select": True,
"paused_list": result["paused_list"],
})
session = result["session"]
if session is None:
return success_response({"need_select": False, "session": None, "message": "没有暂停的任务"})
return success_response({
"need_select": False,
"session": serialize_session(session).__dict__,
"resume_point": result.get("resume_point"),
"pending_items": result.get("pending_items", []),
})
@router.get("/sessions/{session_id}", tags=["自动化闭环"])
async def get_session(
session_id: str,
db: AsyncSession = Depends(get_db),
_agent: Agent = Depends(get_current_agent),
):
"""坐席查看会话详情。"""
svc = AutoSessionService(db)
detail = await svc.get_session_detail(session_id)
if detail is None:
raise AppException(4005, "自动化会话不存在")
return success_response(_detail_payload(detail))
@router.post("/sessions/{session_id}/approve", tags=["自动化闭环"])
async def approve_session(
session_id: str,
req: ApprovalDecisionRequest,
db: AsyncSession = Depends(get_db),
current_agent: Agent = Depends(get_current_agent),
):
"""坐席审批/驳回当前待决高危动作。"""
svc = AutoSessionService(db)
detail = await svc.get_session_detail(session_id)
if detail is None:
raise AppException(4005, "自动化会话不存在")
action_id = detail["session"].current_action_id
if not action_id or detail["ticket"] is None:
raise AppException(4004, "当前没有待审批的动作")
executor = ActionExecutor(db)
try:
await executor.resume(
session_id,
action_id,
decision=req.decision,
note=req.note,
approver_id=current_agent.user_id,
)
except AutomationException as e:
raise to_app_exception(e)
await db.commit()
return success_response(_detail_payload(await svc.get_session_detail(session_id)))
@router.post("/sessions/{session_id}/takeover", tags=["自动化闭环"])
async def takeover_session(
session_id: str,
req: TakeoverRequest,
db: AsyncSession = Depends(get_db),
current_agent: Agent = Depends(get_current_agent),
):
"""坐席转人工接管会话。"""
svc = AutoSessionService(db)
try:
session = await svc.takeover(
session_id, agent_id=current_agent.user_id, note=req.note
)
except AutomationException as e:
raise to_app_exception(e)
await db.commit()
return success_response(serialize_session(session).__dict__)
# ==========================================================================
# 员工端(H5)接口
# ==========================================================================
@router.post("/sessions/by-employee", tags=["自动化闭环"])
async def create_session_by_employee(
req: CreateSessionRequest,
db: AsyncSession = Depends(get_db),
employee_id: str = Depends(get_current_employee_id),
):
"""员工(H5)创建自动化会话。"""
svc = AutoSessionService(db)
session = await svc.create_session(
conversation_id=req.conversation_id,
employee_id=employee_id,
description=req.description,
mode=req.mode,
)
await db.flush()
set_parties(session.id, employee_id=employee_id)
await db.commit()
asyncio.create_task(_run_background(session.id))
return success_response(serialize_session(session).__dict__)
@router.get("/sessions/{session_id}/employee", tags=["自动化闭环"])
async def get_session_employee(
session_id: str,
db: AsyncSession = Depends(get_db),
employee_id: str = Depends(get_current_employee_id),
):
"""员工(H5)查看自己会话详情。"""
svc = AutoSessionService(db)
detail = await svc.get_session_detail(session_id)
if detail is None or detail["session"].employee_id != employee_id:
raise AppException(4005, "自动化会话不存在")
return success_response(_detail_payload(detail))
@router.post("/sessions/{session_id}/confirm", tags=["自动化闭环"])
async def confirm_session(
session_id: str,
req: ConfirmRequest,
db: AsyncSession = Depends(get_db),
employee_id: str = Depends(get_current_employee_id),
):
"""员工 H5 二次确认(高危写操作)。"""
svc = AutoSessionService(db)
detail = await svc.get_session_detail(session_id)
if detail is None or detail["session"].employee_id != employee_id:
raise AppException(4005, "自动化会话不存在")
action_id = detail["session"].current_action_id
if not action_id or detail["ticket"] is None:
raise AppException(4004, "当前没有待确认的动作")
if detail["ticket"].channel != "h5":
raise AppException(4004, "该动作需坐席审批,员工无需确认")
executor = ActionExecutor(db)
try:
await executor.resume(
session_id,
action_id,
decision="approve" if req.confirmed else "reject",
note=req.note,
approver_id=employee_id,
)
except AutomationException as e:
raise to_app_exception(e)
await db.commit()
return success_response(_detail_payload(await svc.get_session_detail(session_id)))
@router.post("/sessions/{session_id}/feedback", tags=["自动化闭环"])
async def feedback_session(
session_id: str,
req: ResolveFeedbackRequest,
db: AsyncSession = Depends(get_db),
employee_id: str = Depends(get_current_employee_id),
):
"""员工对处置结果反馈(满意→关单 / 不满意→转人工)。"""
svc = AutoSessionService(db)
try:
session = await svc.resolve_feedback(
session_id, satisfied=req.satisfied, note=req.note
)
except AutomationException as e:
raise to_app_exception(e)
await db.commit()
return success_response(serialize_session(session).__dict__)
# --- 复杂场景重构:员工端暂停/恢复/更正/补充/信息项端点 ---
@router.post("/sessions/{session_id}/pause", tags=["自动化闭环"])
async def pause_session(
session_id: str,
req: PauseRequest,
db: AsyncSession = Depends(get_db),
employee_id: str = Depends(get_current_employee_id),
):
"""暂停会话(员工/坐席均可)。"""
svc = AutoSessionService(db)
try:
session = await svc.pause_session(session_id, reason=req.reason)
except AutomationException as e:
raise to_app_exception(e)
await db.commit()
return success_response(serialize_session(session).__dict__)
@router.post("/sessions/{session_id}/resume", tags=["自动化闭环"])
async def resume_session_by_id(
session_id: str,
db: AsyncSession = Depends(get_db),
employee_id: str = Depends(get_current_employee_id),
):
"""恢复指定会话(员工端,选中某个暂停会话后调用)。"""
svc = AutoSessionService(db)
try:
result = await svc.resume_session(employee_id, session_id)
except AutomationException as e:
raise to_app_exception(e)
await db.commit()
session = result["session"]
return success_response({
"session": serialize_session(session).__dict__ if session else None,
"resume_point": result.get("resume_point"),
"pending_items": result.get("pending_items", []),
})
@router.post("/sessions/{session_id}/correct", tags=["自动化闭环"])
async def correct_info(
session_id: str,
req: CorrectRequest,
db: AsyncSession = Depends(get_db),
employee_id: str = Depends(get_current_employee_id),
):
"""信息更正。"""
svc = AutoSessionService(db)
try:
item = await svc.correct_info(
session_id, req.field, req.new_value, req.old_value
)
except AutomationException as e:
raise to_app_exception(e)
await db.commit()
return success_response(serialize_information_item(item).__dict__)
@router.post("/sessions/{session_id}/supplement", tags=["自动化闭环"])
async def supplement_info(
session_id: str,
req: SupplementRequest,
db: AsyncSession = Depends(get_db),
employee_id: str = Depends(get_current_employee_id),
):
"""信息补充。"""
svc = AutoSessionService(db)
try:
item = await svc.supplement_info(session_id, req.field, req.value)
except AutomationException as e:
raise to_app_exception(e)
await db.commit()
return success_response(serialize_information_item(item).__dict__)
@router.get("/sessions/{session_id}/info-items", tags=["自动化闭环"])
async def get_info_items(
session_id: str,
db: AsyncSession = Depends(get_db),
_agent: Agent = Depends(get_current_agent),
):
"""获取会话信息项列表。"""
info_svc = InformationItemService(db)
items = await info_svc.get_items(session_id)
return success_response([serialize_information_item(item).__dict__ for item in items])
# --- 复杂场景重构:坐席端代恢复/关闭端点 ---
@router.post("/sessions/{session_id}/agent-resume", tags=["自动化闭环"])
async def agent_resume_session(
session_id: str,
req: AgentResumeRequest,
db: AsyncSession = Depends(get_db),
current_agent: Agent = Depends(get_current_agent),
):
"""坐席代恢复暂停会话。"""
svc = AutoSessionService(db)
try:
session = await svc.agent_resume(
session_id, current_agent.user_id, req.note
)
except AutomationException as e:
raise to_app_exception(e)
await db.commit()
return success_response(serialize_session(session).__dict__)
@router.post("/sessions/{session_id}/agent-close", tags=["自动化闭环"])
async def agent_close_session(
session_id: str,
req: AgentCloseRequest,
db: AsyncSession = Depends(get_db),
current_agent: Agent = Depends(get_current_agent),
):
"""坐席关闭暂停会话。"""
svc = AutoSessionService(db)
try:
session = await svc.agent_close(
session_id, current_agent.user_id, req.note
)
except AutomationException as e:
raise to_app_exception(e)
await db.commit()
return success_response(serialize_session(session).__dict__)
# --- 复杂场景重构第二阶段:P2/P3 端点 ---
@router.post("/sessions/{session_id}/batch-correct", tags=["自动化闭环"])
async def batch_correct_info(
session_id: str,
req: BatchCorrectRequest,
db: AsyncSession = Depends(get_db),
employee_id: str = Depends(get_current_employee_id),
):
"""批量更正信息项(原子事务,失败整体回滚)。"""
svc = CorrectionService(db)
try:
result = await svc.batch_correct(
session_id, req.corrections, req.reason
)
except AutomationException:
raise to_app_exception(AutomationException(4019, "批量更正失败,已回滚"))
except Exception as e:
logger.error(f"批量更正失败: {e}")
raise to_app_exception(AutomationException(4019, f"批量更正失败: {e}"))
await db.commit()
return success_response(result.to_dict())
@router.post("/sessions/{session_id}/undo-correction", tags=["自动化闭环"])
async def undo_correction(
session_id: str,
db: AsyncSession = Depends(get_db),
employee_id: str = Depends(get_current_employee_id),
):
"""撤销最近一次更正。"""
svc = CorrectionService(db)
try:
result = await svc.undo_correction(session_id)
except ValueError as e:
if "超限" in str(e):
raise to_app_exception(AutomationException(4018, str(e)))
raise AppException(4004, str(e))
await db.commit()
# 计算剩余撤销次数:查询当前已撤销的快照数量
from app.config import settings
from sqlalchemy import func, select as _select
from app.models.automation import InformationSnapshot
undone_stmt = (
_select(func.count(InformationSnapshot.id))
.where(
InformationSnapshot.session_id == session_id,
InformationSnapshot.is_undone == True, # noqa: E712
)
)
undone_count = (await db.execute(undone_stmt)).scalar() or 0
remaining = settings.max_undo_count - undone_count
result["remaining_undo_count"] = max(0, remaining)
return success_response(result)
@router.get("/sessions/{session_id}/correction-history", tags=["自动化闭环"])
async def get_correction_history(
session_id: str,
db: AsyncSession = Depends(get_db),
_agent: Agent = Depends(get_current_agent),
):
"""获取更正历史。"""
svc = CorrectionService(db)
history = await svc.get_correction_history(session_id)
return success_response({"history": history})
@router.get("/sessions/{session_id}/version-chain/{item_name}", tags=["自动化闭环"])
async def get_version_chain(
session_id: str,
item_name: str,
db: AsyncSession = Depends(get_db),
_agent: Agent = Depends(get_current_agent),
):
"""获取信息项版本链。"""
svc = CorrectionService(db)
chain = await svc.get_version_chain(session_id, item_name)
return success_response({"item_key": item_name, "chain": chain})
@router.post("/sessions/{session_id}/version-diff/{item_name}", tags=["自动化闭环"])
async def get_version_diff(
session_id: str,
item_name: str,
req: VersionDiffRequest,
db: AsyncSession = Depends(get_db),
_agent: Agent = Depends(get_current_agent),
):
"""版本对比。"""
svc = CorrectionService(db)
diff = await svc.get_version_diff(session_id, item_name, req.v1, req.v2)
return success_response(diff)
@router.get("/sessions/{session_id}/compression-logs", tags=["自动化闭环"])
async def get_compression_logs(
session_id: str,
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
db: AsyncSession = Depends(get_db),
_agent: Agent = Depends(get_current_agent),
):
"""获取上下文压缩日志。"""
from sqlalchemy import func, select
stmt = (
select(ContextCompression)
.where(ContextCompression.session_id == session_id)
.order_by(ContextCompression.created_at.desc())
.offset((page - 1) * page_size)
.limit(page_size)
)
result = await db.execute(stmt)
logs = result.scalars().all()
count_stmt = select(func.count()).select_from(ContextCompression).where(
ContextCompression.session_id == session_id
)
total = (await db.execute(count_stmt)).scalar() or 0
return success_response({
"logs": [
{
"id": log.id,
"session_id": log.session_id,
"tokens_before": log.tokens_before,
"tokens_after": log.tokens_after,
"compression_ratio": float(log.compression_ratio),
"task_node": log.task_node,
"duration_ms": log.duration_ms,
"compression_level": log.compression_level,
"summary": log.summary,
"created_at": log.created_at.isoformat() if log.created_at else None,
}
for log in logs
],
"total": total,
})
# ==========================================================================
# 管理端接口(配置写需 OTP
# ==========================================================================
@router.get("/admin/scenarios", tags=["自动化闭环-管理"])
async def list_scenarios(
db: AsyncSession = Depends(get_db),
):
"""场景配置列表(只读,无需 OTP)。"""
svc = AutoSessionService(db)
configs = await svc.list_scenario_configs()
return success_response([_scenario_payload(c) for c in configs])
@router.put("/admin/scenarios/{scenario_key}", tags=["自动化闭环-管理"])
async def update_scenario(
scenario_key: str,
req: ScenarioConfigUpdate,
db: AsyncSession = Depends(get_db),
_otp: object = Depends(require_high_risk_otp),
):
"""更新场景配置(高危写操作,需 OTP)。"""
svc = AutoSessionService(db)
data = req.model_dump(exclude_unset=True)
config = await svc.upsert_scenario_config(scenario_key, data, operator="admin")
await db.commit()
return success_response(_scenario_payload(config))
@router.get("/admin/rule-versions", tags=["自动化闭环-管理"])
async def list_rule_versions(
scenario_key: Optional[str] = Query(None),
db: AsyncSession = Depends(get_db),
):
"""规则版本列表。"""
svc = AutoSessionService(db)
versions = await svc.list_rule_versions(scenario_key=scenario_key)
return success_response([_rule_version_payload(v) for v in versions])
@router.get("/admin/metrics", tags=["自动化闭环-管理"])
async def get_metrics(
db: AsyncSession = Depends(get_db),
):
"""自动化看板指标。"""
svc = AutoSessionService(db)
metrics = await svc.metrics()
return success_response(metrics)
# ==========================================================================
# WebSocket:自动化进度专用通道
# ==========================================================================
@ws_router.websocket("/ws/automation/{session_id}")
async def automation_ws_endpoint(websocket: WebSocket, session_id: str) -> None:
"""自动化会话专用 WebSocket(坐席/员工均可连)。
认证:优先 subprotocol bearer.{token},其次 Authorization header
最后 query ?token=。token 需在 agent:token / employee:token 中存在。
"""
subprotocol = websocket.headers.get("sec-websocket-protocol", "")
if subprotocol.startswith("bearer."):
token = subprotocol[7:]
else:
auth_header = websocket.headers.get("Authorization", "")
token = auth_header[7:] if auth_header.startswith("Bearer ") else websocket.query_params.get("token", "")
if not token:
await websocket.accept()
await websocket.close(code=WS_CLOSE_UNAUTHORIZED, reason="Missing token")
return
# 校验 token(坐席或员工任一即可)
try:
aid = await cache_service.get(f"agent:token:{token}")
eid = await cache_service.get(f"employee:token:{token}") if not aid else None
except Exception as e: # noqa: BLE001
logger.error(f"自动化 WS token 校验失败: {e}")
await websocket.accept()
await websocket.close(code=WS_CLOSE_UNAUTHORIZED, reason="Auth unavailable")
return
if not aid and not eid:
await websocket.accept()
await websocket.close(code=WS_CLOSE_UNAUTHORIZED, reason="Invalid token")
return
register_ws(session_id, websocket)
logger.info(f"自动化 WS 连接: session={session_id}")
try:
while True:
data = await websocket.receive_json()
if data.get("type") == "ping":
await websocket.send_json({"type": "pong"})
except WebSocketDisconnect:
unregister_ws(session_id, websocket)
logger.info(f"自动化 WS 断开: session={session_id}")
except Exception: # noqa: BLE001
unregister_ws(session_id, websocket)
# ==========================================================================
# 辅助函数
# ==========================================================================
async def _run_background(session_id: str) -> None:
"""后台运行编排(独立导入避免循环依赖)。"""
from app.services.automation import run_session_in_background
await run_session_in_background(session_id)
def _session_min(session) -> dict:
"""会话列表最小字段。"""
return {
"id": session.id,
"employee_id": session.employee_id,
"scenario_key": session.scenario_key,
"status": session.status,
"mode": session.mode,
"confidence": session.confidence,
"title": session.title,
"paused_at": session.paused_at.isoformat() if session.paused_at else None,
"created_at": session.created_at.isoformat() if session.created_at else None,
"updated_at": session.updated_at.isoformat() if session.updated_at else None,
}
def _detail_payload(detail: dict) -> dict:
"""构造会话详情响应 data。"""
session = detail["session"]
actions = detail.get("actions", [])
ticket = detail.get("ticket")
return serialize_session(session, actions=actions, ticket=ticket).__dict__
def _scenario_payload(config) -> dict:
"""场景配置响应。"""
return ScenarioConfigResponse(
id=config.id,
scenario_key=config.scenario_key,
name=config.name,
description=config.description,
enabled=config.enabled,
trigger_conditions=config.trigger_conditions,
actions=config.actions,
approval_strategy=config.approval_strategy,
current_version_id=config.current_version_id,
).model_dump()
def _rule_version_payload(version) -> dict:
"""规则版本响应。"""
return RuleVersionResponse(
id=version.id,
scenario_key=version.scenario_key,
version=version.version,
content=version.content,
status=version.status,
canary_percent=version.canary_percent,
created_by=version.created_by,
remark=version.remark,
created_at=version.created_at.isoformat() if version.created_at else None,
).model_dump()