fix(agent): close chat lifecycle safely

This commit is contained in:
InfinityPacer
2026-08-23 11:44:18 +08:00
parent 1e2d0d3b07
commit e0f70fa920
8 changed files with 177 additions and 33 deletions
+2 -2
View File
@@ -17,7 +17,7 @@ from app.application.messaging.chat import (
AsyncUnitOfWork,
)
from app.application.messaging.message import MessageQueryService
from app.startup.context import HostRuntime
from app.startup.context import AgentChatRuntime, HostRuntime
def get_agent_chat_service(
@@ -29,7 +29,7 @@ def get_agent_chat_service(
def get_agent_chat_persistence(
runtime: HostRuntime = Depends(get_agent_chat_runtime),
runtime: AgentChatRuntime = Depends(get_agent_chat_runtime),
) -> AgentChatPersistenceService:
"""从类型化 Agent 运行时获取有界会话写入端口。"""
return runtime.persistence
+7 -6
View File
@@ -1937,10 +1937,13 @@ async def save_agent_chat_display(
service=service,
persistence=persistence,
)
chat = await service.get_accessible(session_id, current_user)
# 写入由独立 worker 事务完成,使用组合根登记的短会话服务复读,避免请求会话
# 的 identity map 返回写入前的 ORM 快照。
chat_service = get_configured_agent_chat_service()
chat = await chat_service.get_accessible(session_id, current_user)
if not chat:
return _SchemaResponse(success=False, message="会话保存失败")
return _SchemaResponse(success=True, data=service.to_summary(chat))
return _SchemaResponse(success=True, data=chat_service.to_summary(chat))
@router.delete(
@@ -2027,7 +2030,7 @@ async def _web_agent_stream_impl(
"""
prompt = payload.text.strip()
if not isinstance(service, AgentChatService):
# 直接调用公开函数时不经过 FastAPI 依赖解析;生产路由总是传入运行时服务
# SSE 后台任务可能在请求依赖释放后继续运行,查询服务必须自行取得短会话
service = get_configured_agent_chat_service()
if not isinstance(persistence, AgentChatPersistenceService):
# 直接调用公开函数时不经过 FastAPI 依赖解析;生产路由总是传入运行时端口。
@@ -2387,7 +2390,6 @@ async def web_agent_stream(
payload: _SchemaAgentWebChatRequest,
request: Request,
current_user: ApiPrincipal = Depends(get_current_active_user),
service: AgentChatService = Depends(get_agent_chat_service),
persistence: AgentChatPersistenceService = Depends(get_agent_chat_persistence),
) -> StreamingResponse:
"""Web 智能助手流式对话的稳定公开路由入口。"""
@@ -2395,6 +2397,5 @@ async def web_agent_stream(
payload,
request,
current_user,
service,
persistence,
persistence=persistence,
)
+7
View File
@@ -202,6 +202,13 @@ async def shutdown_web_agent_background_tasks() -> None:
await asyncio.wait(tasks)
async def wait_web_agent_background_tasks() -> None:
"""等待已登记的 Web Agent 任务完成取消后的最终收尾。"""
tasks = tuple(_WEB_AGENT_BACKGROUND_TASKS)
if tasks:
await asyncio.gather(*tasks, return_exceptions=True)
def register_channel_admin_resolver(
channel: Union[NotificationChannel, str],
resolver: _ChannelAdminResolver,
+13 -2
View File
@@ -103,10 +103,21 @@ async def run_shutdown_step(
try:
result = callback()
if inspect.isawaitable(result):
task = asyncio.ensure_future(result)
if timeout_seconds:
await asyncio.wait_for(result, timeout=timeout_seconds)
try:
await asyncio.wait_for(
asyncio.shield(task), timeout=timeout_seconds
)
except asyncio.TimeoutError:
logger.error("关闭%s超时,等待其取消收口", name)
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
else:
await result
await task
except Exception as err:
logger.error(f"关闭{name}失败:{err}")
+35 -15
View File
@@ -1,3 +1,4 @@
import asyncio
import inspect
import sys
from typing import Callable
@@ -69,7 +70,10 @@ from app.application.messaging.chat import (
configure_agent_chat_service,
get_configured_agent_chat_persistence,
)
from app.application.messaging.agent import shutdown_web_agent_background_tasks
from app.application.messaging.agent import (
shutdown_web_agent_background_tasks,
wait_web_agent_background_tasks,
)
from app.application.security.user import configure_user_lookups
from app.application.security.auth import AuthService, configure_auth_service
from app.application.security.passkeys import PasskeyService, configure_passkey_service
@@ -559,14 +563,19 @@ async def stop_modules():
"""
服务关闭
"""
async def run_step(name: str, callback: Callable[[], object]) -> None:
async def run_step(name: str, callback: Callable[[], object]) -> bool:
"""单个模块资源关闭失败时继续执行后续阶段"""
try:
result = callback()
if inspect.isawaitable(result):
await result
return True
except asyncio.CancelledError:
logger.warning("关闭%s时收到取消请求,继续执行资源收口", name)
return False
except Exception as err:
logger.error(f"关闭{name}失败:{err}")
return True
await run_step("AI智能体", stop_agent)
await run_step("模块", lambda: ModuleManager().shutdown())
@@ -578,21 +587,32 @@ async def stop_modules():
await run_step("消息服务", stop_message)
await run_step("Redis缓存连接", lambda: RedisHelper().close())
await run_step("异步Redis缓存连接", lambda: AsyncRedisHelper().close())
# 先关闭持久化准入,取消中的 Web Agent finally 才会快速拒绝晚到的快照写入。
await run_step(
"Agent会话持久化准入",
lambda: get_configured_agent_chat_persistence().begin_shutdown(),
# Web Agent 的取消 finally 可能还要写入最终展示快照,必须先完成任务收尾,再关闭写入准入。
web_agent_drained = await run_step(
"Web Agent后台任务", shutdown_web_agent_background_tasks
)
await run_step("Web Agent后台任务", shutdown_web_agent_background_tasks)
await run_step(
"Agent会话持久化",
lambda: get_configured_agent_chat_persistence().shutdown(),
)
await run_step("数据库任务", stop_database_worker)
if _database_worker is None:
await run_step("数据库连接", close_database)
if not web_agent_drained:
web_agent_drained = await run_step(
"Web Agent后台任务收尾", wait_web_agent_background_tasks
)
if web_agent_drained:
await run_step(
"Agent会话持久化准入",
lambda: get_configured_agent_chat_persistence().begin_shutdown(),
)
persistence_drained = await run_step(
"Agent会话持久化",
lambda: get_configured_agent_chat_persistence().shutdown(),
)
else:
logger.error("数据库任务未收敛,跳过数据库连接关闭以避免运行中事务使用已释放连接")
persistence_drained = False
logger.error("Web Agent任务未完成收尾,跳过持久化和数据库关闭以保护活动事务")
if persistence_drained:
await run_step("数据库任务", stop_database_worker)
if _database_worker is None:
await run_step("数据库连接", close_database)
else:
logger.error("数据库任务未收敛,跳过数据库连接关闭以避免运行中事务使用已释放连接")
await run_step("前端服务", stop_frontend)
await run_step("临时文件", clear_temp)