diff --git a/apps/application/tests/test_ai_chat_tool_output.py b/apps/application/tests/test_ai_chat_tool_output.py index 7efa59bb809..b99ffe485da 100644 --- a/apps/application/tests/test_ai_chat_tool_output.py +++ b/apps/application/tests/test_ai_chat_tool_output.py @@ -249,7 +249,7 @@ def on_complete(error): with ( TemporaryDirectory(prefix="maxkb-summary-test-") as directory, - patch.object(agent_module, "init_skills", new=AsyncMock()), + patch.object(agent_module, "init_skills", new=MagicMock()), patch.object( agent_module, "MultiServerMCPClient", @@ -413,7 +413,7 @@ async def stream(*args, **kwargs): with ( TemporaryDirectory(prefix="maxkb-backend-test-") as directory, - patch.object(agent_module, "init_skills", new=AsyncMock()), + patch.object(agent_module, "init_skills", new=MagicMock()), patch.object( agent_module, "MultiServerMCPClient", return_value=SimpleNamespace(get_tools=AsyncMock(return_value=[])) ), diff --git a/apps/application/tests/test_langchain_compatibility.py b/apps/application/tests/test_langchain_compatibility.py index b0f107e0110..78a7e040723 100644 --- a/apps/application/tests/test_langchain_compatibility.py +++ b/apps/application/tests/test_langchain_compatibility.py @@ -404,7 +404,7 @@ def lookup(city: str) -> str: with ( TemporaryDirectory(prefix="maxkb-agent-compat-") as directory, - patch.object(agent_module, "init_skills", new=AsyncMock()), + patch.object(agent_module, "init_skills", new=MagicMock()), patch.object( agent_module, "MultiServerMCPClient", @@ -442,7 +442,7 @@ def lookup(city: str) -> str: with ( TemporaryDirectory(prefix="maxkb-agent-limit-") as directory, - patch.object(agent_module, "init_skills", new=AsyncMock()), + patch.object(agent_module, "init_skills", new=MagicMock()), patch.object(agent_module.CONFIG, "get", return_value="1"), patch.object( agent_module, diff --git a/apps/application/workflow/nodes/ai_chat_node/agent.py b/apps/application/workflow/nodes/ai_chat_node/agent.py index 35b9c554a4c..25ee6bdf6b2 100644 --- a/apps/application/workflow/nodes/ai_chat_node/agent.py +++ b/apps/application/workflow/nodes/ai_chat_node/agent.py @@ -188,7 +188,6 @@ def create_agent( async def _run(): checkpointer = MemorySaver() - await init_skills(skill_tool_ids, temp_dir) client = MultiServerMCPClient(json.loads(mcp_servers)) tools = await client.get_tools() for tool in tools: @@ -236,6 +235,8 @@ def _classify_error(e): error = None try: + # 技能初始化含同步 ORM / 文件 IO,在当前工作线程执行,不进入事件循环 + init_skills(skill_tool_ids, temp_dir) async_runner.run_async(_run) except Exception as e: error = _classify_error(e) diff --git a/apps/application/workflow/nodes/ai_chat_node/tools/skill.py b/apps/application/workflow/nodes/ai_chat_node/tools/skill.py index 42d362ed48d..4a5de0e140b 100644 --- a/apps/application/workflow/nodes/ai_chat_node/tools/skill.py +++ b/apps/application/workflow/nodes/ai_chat_node/tools/skill.py @@ -12,7 +12,6 @@ import os import zipfile -from asgiref.sync import sync_to_async from django.db.models import QuerySet from common.utils.rsa_util import rsa_long_decrypt @@ -20,11 +19,15 @@ from tools.models import Tool -async def init_skills(skill_tool_ids, temp_dir): +def init_skills(skill_tool_ids, temp_dir): + """ + 同步函数:包含 ORM 查询、RSA 密钥读取(缓存未命中时查库)、文件读取与解压, + 必须在工作线程中调用,不能在事件循环线程内执行(否则触发 SynchronousOnlyOperation 并阻塞循环)。 + """ if not skill_tool_ids: return skills_dir = os.path.join(temp_dir, "skills") - tools = await sync_to_async(lambda: list(QuerySet(Tool).filter(id__in=skill_tool_ids, is_active=True)))() + tools = list(QuerySet(Tool).filter(id__in=skill_tool_ids, is_active=True)) if not tools: return @@ -35,10 +38,10 @@ async def init_skills(skill_tool_ids, temp_dir): else: params = init_params_default_value - file = await sync_to_async(lambda t=tool: QuerySet(File).filter(id=t.code).first())() + file = QuerySet(File).filter(id=tool.code).first() if not file: continue - file_bytes = await sync_to_async(file.get_bytes)() + file_bytes = file.get_bytes() with zipfile.ZipFile(io.BytesIO(file_bytes), "r") as zip_ref: members = [m for m in zip_ref.namelist() if not m.startswith("__MACOSX/") and "__MACOSX" not in m]