fix: MCP插件TimeoutError修复 + 多项Bug修复和性能优化

- fix: MCP插件管理接口改为后台任务,修复TimeoutError
- fix: MCP连接失败后上下文清理的cancel scope错误
- feat: MCP插件后台注册添加重试机制
- fix: 限制每章自动创建伏笔数量上限
- fix: 修复JSON非法转义字符清洗
- fix: SSE流式生成添加心跳保活
- fix: 职业生成改用POST请求避免URL长度限制
- perf: 使用torch CPU版本加速Docker构建
- fix: 自动修复JSON字符串值中的裸换行符
- feat: 集成json5容错解析器
This commit is contained in:
未来
2026-04-26 13:58:15 +08:00
parent 5c22f29bf9
commit 17e78955a9
18 changed files with 559 additions and 179 deletions
+119 -99
View File
@@ -54,65 +54,75 @@ async def _register_plugin_background(
plugin_type: str,
server_url: str,
headers: Optional[dict],
config: Optional[dict]
config: Optional[dict],
max_retries: int = 2,
retry_delay: float = 3.0
):
"""
后台任务:注册MCP插件并更新数据库状态
后台任务:注册MCP插件并更新数据库状态(带重试)
在独立的任务中执行MCP连接,避免阻塞请求处理
在独立的任务中执行MCP连接,避免阻塞请求处理
连接失败时会自动重试,提高对临时网络问题的容错性。
"""
last_error = None
for attempt in range(max_retries + 1):
try:
if attempt > 0:
logger.info(f"后台注册MCP插件重试 ({attempt}/{max_retries}): {plugin_name}")
await asyncio.sleep(retry_delay)
else:
logger.info(f"后台注册MCP插件: {plugin_name}")
if plugin_type in HTTP_PLUGIN_TYPES and server_url:
server_url = _validate_mcp_server_url(plugin_type, server_url)
success = await mcp_client.register(MCPPluginConfig(
user_id=user_id,
plugin_name=plugin_name,
url=server_url,
plugin_type=plugin_type,
headers=headers,
timeout=config.get('timeout', 60.0) if config else 60.0
))
else:
success = False
if success:
# 更新数据库状态为active
engine = await get_engine(user_id)
AsyncSessionLocal = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async with AsyncSessionLocal() as db:
stmt = (
update(MCPPlugin)
.where(MCPPlugin.user_id == user_id, MCPPlugin.plugin_name == plugin_name)
.values(status="active", last_error=None)
)
await db.execute(stmt)
await db.commit()
logger.info(f"后台注册MCP插件成功: {plugin_name}")
return
else:
last_error = "连接失败"
except Exception as e:
last_error = str(e)
logger.warning(f"后台注册MCP插件异常 (尝试 {attempt + 1}/{max_retries + 1}): {plugin_name}, 错误: {e}")
# 所有重试都失败,更新数据库状态为error
logger.error(f"后台注册MCP插件最终失败 (已重试{max_retries}次): {plugin_name}, 错误: {last_error}")
try:
logger.info(f"后台注册MCP插件: {plugin_name}")
if plugin_type in HTTP_PLUGIN_TYPES and server_url:
server_url = _validate_mcp_server_url(plugin_type, server_url)
success = await mcp_client.register(MCPPluginConfig(
user_id=user_id,
plugin_name=plugin_name,
url=server_url,
plugin_type=plugin_type,
headers=headers,
timeout=config.get('timeout', 60.0) if config else 60.0
))
else:
success = False
# 更新数据库状态
engine = await get_engine(user_id)
AsyncSessionLocal = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async with AsyncSessionLocal() as db:
stmt = (
update(MCPPlugin)
.where(MCPPlugin.user_id == user_id, MCPPlugin.plugin_name == plugin_name)
.values(
status="active" if success else "error",
last_error=None if success else "连接失败"
)
.values(status="error", last_error=str(last_error)[:500] if last_error else "连接失败")
)
await db.execute(stmt)
await db.commit()
if success:
logger.info(f"后台注册MCP插件成功: {plugin_name}")
else:
logger.warning(f"后台注册MCP插件失败: {plugin_name}")
except Exception as e:
logger.error(f"后台注册MCP插件异常: {plugin_name}, 错误: {e}")
try:
engine = await get_engine(user_id)
AsyncSessionLocal = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async with AsyncSessionLocal() as db:
stmt = (
update(MCPPlugin)
.where(MCPPlugin.user_id == user_id, MCPPlugin.plugin_name == plugin_name)
.values(status="error", last_error=str(e))
)
await db.execute(stmt)
await db.commit()
except Exception as db_error:
logger.error(f"更新插件状态失败: {db_error}")
except Exception as db_error:
logger.error(f"更新插件状态失败: {db_error}")
async def _unregister_plugin_safe(user_id: str, plugin_name: str):
@@ -215,22 +225,26 @@ async def create_plugin(
**plugin_data
)
# 如果启用,设为pending状态等待后台连接
if plugin.enabled:
plugin.status = "pending"
db.add(plugin)
await db.commit()
await db.refresh(plugin)
# 如果启用,注册到统一门面
# 如果启用,后台注册到统一门面(避免MCP操作阻塞导致超时)
if plugin.enabled:
success = await _register_plugin_to_facade(plugin, user.user_id)
if success:
plugin.status = "active"
else:
plugin.status = "error"
plugin.last_error = "加载失败"
await db.commit()
await db.refresh(plugin)
asyncio.create_task(_register_plugin_background(
user_id=user.user_id,
plugin_name=plugin.plugin_name,
plugin_type=plugin.plugin_type,
server_url=plugin.server_url,
headers=plugin.headers,
config=plugin.config
))
logger.info(f"用户 {user.user_id} 创建插件: {plugin.plugin_name}")
logger.info(f"用户 {user.user_id} 创建插件: {plugin.plugin_name}MCP注册在后台执行)")
return plugin
@@ -438,15 +452,29 @@ async def update_plugin(
for key, value in update_data.items():
setattr(plugin, key, value)
# 如果启用,设为pending状态等待后台连接
if plugin.enabled:
plugin.status = "pending"
plugin.last_error = None
await db.commit()
await db.refresh(plugin)
# 如果插件已启用,重新注册
# 如果插件已启用,后台重新注册MCP连接
if plugin.enabled:
await mcp_client.unregister(user.user_id, plugin.plugin_name)
await _register_plugin_to_facade(plugin, user.user_id)
# 先后台注销旧连接
asyncio.create_task(_unregister_plugin_safe(user.user_id, plugin.plugin_name))
# 再后台注册新连接
asyncio.create_task(_register_plugin_background(
user_id=user.user_id,
plugin_name=plugin.plugin_name,
plugin_type=plugin.plugin_type,
server_url=plugin.server_url,
headers=plugin.headers,
config=plugin.config
))
logger.info(f"用户 {user.user_id} 更新插件: {plugin.plugin_name}")
logger.info(f"用户 {user.user_id} 更新插件: {plugin.plugin_name}MCP操作在后台执行)")
return plugin
@@ -470,15 +498,19 @@ async def delete_plugin(
if not plugin:
raise HTTPException(status_code=404, detail="插件不存在")
# 从统一门面注销
await mcp_client.unregister(user.user_id, plugin.plugin_name)
# 保存插件信息用于后台注销
plugin_name = plugin.plugin_name
user_id = user.user_id
# 删除数据库记录
# 删除数据库记录
await db.delete(plugin)
await db.commit()
logger.info(f"用户 {user.user_id} 删除插件: {plugin.plugin_name}")
return {"message": "插件已删除", "plugin_name": plugin.plugin_name}
# 后台从统一门面注销(避免MCP操作阻塞导致超时)
asyncio.create_task(_unregister_plugin_safe(user_id, plugin_name))
logger.info(f"用户 {user.user_id} 删除插件: {plugin_name}MCP注销在后台执行)")
return {"message": "插件已删除", "plugin_name": plugin_name}
@router.post("/{plugin_id}/toggle", response_model=MCPPluginResponse)
@@ -490,6 +522,10 @@ async def toggle_plugin(
):
"""
启用或禁用插件
启用时:先更新数据库状态为pending,再通过后台任务注册MCP连接,
避免长时间持有数据库会话导致超时。
禁用时:先更新数据库状态,再通过后台任务注销MCP连接。
"""
result = await db.execute(
select(MCPPlugin).where(
@@ -509,51 +545,35 @@ async def toggle_plugin(
headers = plugin.headers
config = plugin.config
# 更新数据库状态
# 更新数据库状态
plugin.enabled = enabled
if not enabled:
if enabled:
# 启用时先设为pending状态,等待后台MCP连接完成
plugin.status = "pending"
plugin.last_error = None
else:
plugin.status = "inactive"
await db.commit()
await db.refresh(plugin)
# 数据库操作完成后,进行MCP操作
# 数据库操作完成后,通过后台任务进行MCP操作(避免长时间持有数据库会话)
if enabled:
# 启用:注册到统一门面
try:
if plugin_type in HTTP_PLUGIN_TYPES and server_url:
server_url = _validate_mcp_server_url(plugin_type, server_url)
success = await mcp_client.register(MCPPluginConfig(
user_id=user.user_id,
plugin_name=plugin_name,
url=server_url,
plugin_type=plugin_type,
headers=headers,
timeout=config.get('timeout', 60.0) if config else 60.0
))
else:
success = False
# 更新状态
plugin.status = "active" if success else "error"
plugin.last_error = None if success else "加载失败"
await db.commit()
await db.refresh(plugin)
except Exception as e:
logger.error(f"注册插件失败: {plugin_name}, 错误: {e}")
plugin.status = "error"
plugin.last_error = str(e)
await db.commit()
await db.refresh(plugin)
# 启用:后台注册到统一门面
asyncio.create_task(_register_plugin_background(
user_id=user.user_id,
plugin_name=plugin_name,
plugin_type=plugin_type,
server_url=server_url,
headers=headers,
config=config
))
else:
# 禁用:从统一门面注销(不影响数据库状态)
try:
await mcp_client.unregister(user.user_id, plugin_name)
except Exception as e:
logger.warning(f"注销插件时出错(可忽略): {plugin_name}, 错误: {e}")
# 禁用:后台从统一门面注销(不影响数据库状态)
asyncio.create_task(_unregister_plugin_safe(user.user_id, plugin_name))
action = "启用" if enabled else "禁用"
logger.info(f"用户 {user.user_id} {action}插件: {plugin_name}")
logger.info(f"用户 {user.user_id} {action}插件: {plugin_name}MCP操作在后台执行)")
return plugin