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
This commit is contained in:
@@ -55,14 +55,35 @@ from app.schemas.automation import (
|
||||
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,
|
||||
@@ -133,6 +154,56 @@ async def list_sessions(
|
||||
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,
|
||||
@@ -285,6 +356,274 @@ async def feedback_session(
|
||||
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)
|
||||
# ==========================================================================
|
||||
@@ -405,6 +744,7 @@ def _session_min(session) -> dict:
|
||||
"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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user