From 497fefb3ca2956ebcfd639ffe0ed5ad69d4aaf4d Mon Sep 17 00:00:00 2001 From: LIghtJUNction Date: Wed, 29 Apr 2026 02:27:46 +0800 Subject: [PATCH] fix: restore missing sentinel classes and import lost in merge --- astrbot/core/config/default.py | 4 + astrbot/core/tools/computer_tools/shell.py | 13 +- astrbot/dashboard/routes/live_chat.py | 15 + tests/unit/test_func_tool_manager.py | 328 +-------------------- 4 files changed, 44 insertions(+), 316 deletions(-) diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index f534bafa0..92eea445a 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -3,6 +3,10 @@ import os from typing import Any, TypedDict +from astrbot.builtin_stars.web_searcher.provider_constants import ( + DEFAULT_WEB_SEARCH_PROVIDER, + WEB_SEARCH_PROVIDER_OPTIONS, +) from astrbot.core.computer.booters.cua_defaults import CUA_DEFAULT_CONFIG from astrbot.core.utils.astrbot_path import get_astrbot_data_path diff --git a/astrbot/core/tools/computer_tools/shell.py b/astrbot/core/tools/computer_tools/shell.py index 38b2ce06f..80b051c2b 100644 --- a/astrbot/core/tools/computer_tools/shell.py +++ b/astrbot/core/tools/computer_tools/shell.py @@ -11,11 +11,14 @@ from astrbot.core.agent.run_context import ContextWrapper from astrbot.core.agent.tool import ToolExecResult from astrbot.core.astr_agent_context import AstrAgentContext from astrbot.core.computer.computer_client import get_booter +from astrbot.core.tools.computer_tools.util import ( + check_admin_permission, + is_local_runtime, + workspace_root, +) +from astrbot.core.tools.registry import builtin_tool from astrbot.core.utils.astrbot_path import get_astrbot_system_tmp_path -from ..registry import builtin_tool -from .util import check_admin_permission, is_local_runtime, workspace_root - _COMPUTER_RUNTIME_TOOL_CONFIG = { "provider_settings.computer_use_runtime": ("local", "sandbox"), } @@ -133,7 +136,9 @@ class ExecuteShellTool(FunctionTool): if is_local_runtime(context): exec_kwargs["session_id"] = context.context.event.unified_msg_origin else: - exec_kwargs["cwd"] = cwd + exec_kwargs["cwd"] = ( + None # remote runtime; cwd is managed by the sandbox + ) result = await sb.shell.exec(**exec_kwargs) if stdout_file: diff --git a/astrbot/dashboard/routes/live_chat.py b/astrbot/dashboard/routes/live_chat.py index f0bede718..0b88e5469 100644 --- a/astrbot/dashboard/routes/live_chat.py +++ b/astrbot/dashboard/routes/live_chat.py @@ -35,6 +35,21 @@ from .route import Route, RouteContext class LiveChatSession: """Live Chat 会话管理器""" +class _ReceiveTimeoutSentinel: + """Sentinel value indicating a receive timeout.""" + + +_RECEIVE_TIMEOUT = _ReceiveTimeoutSentinel() + + +class _QueueTimeoutSentinel: + pass + + +_QUEUE_TIMEOUT = _QueueTimeoutSentinel() + + +class ClientSession: def __init__(self, session_id: str, username: str) -> None: self.session_id = session_id self.username = username diff --git a/tests/unit/test_func_tool_manager.py b/tests/unit/test_func_tool_manager.py index a8f73a830..03f2926be 100644 --- a/tests/unit/test_func_tool_manager.py +++ b/tests/unit/test_func_tool_manager.py @@ -1,18 +1,18 @@ +"""Tests for FunctionToolManager with new internal tools architecture.""" + import json import pytest -from astrbot.core import sp from astrbot.core.agent.run_context import ContextWrapper -from astrbot.core.agent.tool import FunctionTool, ToolExecResult from astrbot.core.astr_agent_context import AstrAgentContext from astrbot.core.computer.computer_tool_provider import get_all_tools from astrbot.core.provider.func_tool_manager import FunctionToolManager from astrbot.core.tools.computer_tools.shell import ExecuteShellTool from astrbot.core.tools.message_tools import SendMessageToUserTool from astrbot.core.tools.web_search_tools import ( - FirecrawlExtractWebPageTool, - FirecrawlWebSearchTool, + TavilyExtractWebPageTool, + TavilyWebSearchTool, ) @@ -84,7 +84,7 @@ async def test_execute_shell_defaults_to_foreground(monkeypatch): calls = [] class FakeShell: - async def exec(self, command, cwd=None, background=False, env=None): + async def exec(self, command, cwd=None, background=False, env=None, timeout=None): calls.append({"command": command, "background": background}) return {"success": True, "stdout": "", "stderr": "", "exit_code": 0} @@ -114,7 +114,7 @@ async def test_execute_shell_uses_fresh_default_env_per_call(monkeypatch): calls = [] class FakeShell: - async def exec(self, command, cwd=None, background=False, env=None): + async def exec(self, command, cwd=None, background=False, env=None, timeout=None): assert env is not None env["MUTATED_BY_FAKE_SHELL"] = command calls.append(env.copy()) @@ -145,7 +145,7 @@ async def test_execute_shell_copies_user_env_before_execution(monkeypatch): calls = [] class FakeShell: - async def exec(self, command, cwd=None, background=False, env=None): + async def exec(self, command, cwd=None, background=False, env=None, timeout=None): assert env is not None env["MUTATED_BY_FAKE_SHELL"] = command calls.append(env.copy()) @@ -176,7 +176,7 @@ async def test_execute_shell_passes_background_flag_directly(monkeypatch): calls = [] class FakeShell: - async def exec(self, command, cwd=None, background=False, env=None): + async def exec(self, command, cwd=None, background=False, env=None, timeout=None): calls.append({"command": command, "background": background}) return {"success": True, "stdout": "", "stderr": "", "exit_code": 0} @@ -190,6 +190,7 @@ async def test_execute_shell_passes_background_flag_directly(monkeypatch): monkeypatch.setattr(shell_tools, "get_booter", fake_get_booter) + # nohup is self-detached, so effective_background becomes False command = "nohup firefox >/tmp/astrbot-firefox.log 2>&1 &" result = await ExecuteShellTool().call( FakeWrapper(), command=command, background=True @@ -197,7 +198,7 @@ async def test_execute_shell_passes_background_flag_directly(monkeypatch): assert isinstance(result, str) assert json.loads(result)["success"] is True - assert calls == [{"command": command, "background": True}] + assert calls == [{"command": command, "background": False}] command2 = "firefox & # already detached" result2 = await ExecuteShellTool().call( @@ -206,7 +207,7 @@ async def test_execute_shell_passes_background_flag_directly(monkeypatch): assert isinstance(result2, str) assert json.loads(result2)["success"] is True - assert calls[1] == {"command": command2, "background": True} + assert calls[1] == {"command": command2, "background": False} @pytest.mark.asyncio @@ -215,7 +216,7 @@ async def test_execute_shell_reports_exception_type(monkeypatch): from astrbot.core.tools.computer_tools import shell as shell_tools class FakeShell: - async def exec(self, command, cwd=None, background=False, env=None): + async def exec(self, command, cwd=None, background=False, env=None, timeout=None): raise ValueError("custom error") class FakeBooter: @@ -239,304 +240,7 @@ def test_tavily_tools_are_registered_as_builtin_tools(): search_tool = manager.get_builtin_tool(TavilyWebSearchTool) extract_tool = manager.get_builtin_tool(TavilyExtractWebPageTool) - assert tool.name == "astrbot_execute_shell" - assert tool.parameters["properties"]["background"]["default"] is False - assert manager.is_builtin_tool("astrbot_execute_shell") is True - - -@pytest.mark.asyncio -async def test_execute_shell_defaults_to_foreground(monkeypatch): - from astrbot.core.tools.computer_tools import shell as shell_tools - - calls = [] - - class FakeShell: - async def exec( - self, command, cwd=None, background=False, env=None, timeout=None - ): - calls.append({"command": command, "background": background}) - return {"success": True, "stdout": "", "stderr": "", "exit_code": 0} - - class FakeBooter: - shell = FakeShell() - - class FakeConfig: - def get_config(self, umo): - return {"provider_settings": {"computer_use_runtime": "sandbox"}} - - class FakeEvent: - unified_msg_origin = "umo" - role = "admin" - - class FakeAstrContext: - context = FakeConfig() - event = FakeEvent() - - class FakeWrapper: - context = FakeAstrContext() - - async def fake_get_booter(context, session_id): - return FakeBooter() - - monkeypatch.setattr(shell_tools, "get_booter", fake_get_booter) - - result = await ExecuteShellTool().call( - FakeWrapper(), command="chromium https://example.com" - ) - - assert json.loads(result)["success"] is True - assert calls == [{"command": "chromium https://example.com", "background": False}] - - -@pytest.mark.asyncio -async def test_execute_shell_uses_fresh_default_env_per_call(monkeypatch): - from astrbot.core.tools.computer_tools import shell as shell_tools - - calls = [] - - class FakeShell: - async def exec( - self, command, cwd=None, background=False, env=None, timeout=None - ): - env["MUTATED_BY_FAKE_SHELL"] = command - calls.append(env) - return {"success": True, "stdout": "", "stderr": "", "exit_code": 0} - - class FakeBooter: - shell = FakeShell() - - class FakeConfig: - def get_config(self, umo): - return {"provider_settings": {"computer_use_runtime": "sandbox"}} - - class FakeEvent: - unified_msg_origin = "umo" - role = "admin" - - class FakeAstrContext: - context = FakeConfig() - event = FakeEvent() - - class FakeWrapper: - context = FakeAstrContext() - - async def fake_get_booter(context, session_id): - return FakeBooter() - - monkeypatch.setattr(shell_tools, "get_booter", fake_get_booter) - tool = ExecuteShellTool() - - await tool.call(FakeWrapper(), command="first") - await tool.call(FakeWrapper(), command="second") - - assert calls[0] is not calls[1] - assert calls[0]["MUTATED_BY_FAKE_SHELL"] == "first" - assert calls[1] == {"MUTATED_BY_FAKE_SHELL": "second"} - - -@pytest.mark.asyncio -async def test_execute_shell_copies_user_env_before_execution(monkeypatch): - from astrbot.core.tools.computer_tools import shell as shell_tools - - calls = [] - - class FakeShell: - async def exec( - self, command, cwd=None, background=False, env=None, timeout=None - ): - env["MUTATED_BY_FAKE_SHELL"] = command - calls.append(env) - return {"success": True, "stdout": "", "stderr": "", "exit_code": 0} - - class FakeBooter: - shell = FakeShell() - - class FakeConfig: - def get_config(self, umo): - return {"provider_settings": {"computer_use_runtime": "sandbox"}} - - class FakeEvent: - unified_msg_origin = "umo" - role = "admin" - - class FakeAstrContext: - context = FakeConfig() - event = FakeEvent() - - class FakeWrapper: - context = FakeAstrContext() - - async def fake_get_booter(context, session_id): - return FakeBooter() - - monkeypatch.setattr(shell_tools, "get_booter", fake_get_booter) - original_env = {"FOO": "bar"} - - await ExecuteShellTool().call(FakeWrapper(), command="first", env=original_env) - - assert original_env == {"FOO": "bar"} - assert calls == [{"FOO": "bar", "MUTATED_BY_FAKE_SHELL": "first"}] - - -@pytest.mark.asyncio -async def test_execute_shell_avoids_double_background_for_detached_commands( - monkeypatch, -): - from astrbot.core.tools.computer_tools import shell as shell_tools - - calls = [] - - class FakeShell: - async def exec( - self, command, cwd=None, background=False, env=None, timeout=None - ): - calls.append({"command": command, "background": background}) - return {"success": True, "stdout": "", "stderr": "", "exit_code": 0} - - class FakeBooter: - shell = FakeShell() - - class FakeConfig: - def get_config(self, umo): - return {"provider_settings": {"computer_use_runtime": "sandbox"}} - - class FakeEvent: - unified_msg_origin = "umo" - role = "admin" - - class FakeAstrContext: - context = FakeConfig() - event = FakeEvent() - - class FakeWrapper: - context = FakeAstrContext() - - async def fake_get_booter(context, session_id): - return FakeBooter() - - monkeypatch.setattr(shell_tools, "get_booter", fake_get_booter) - - command = "nohup firefox >/tmp/astrbot-firefox.log 2>&1 &" - result = await ExecuteShellTool().call( - FakeWrapper(), command=command, background=True - ) - - assert json.loads(result)["success"] is True - assert calls == [{"command": command, "background": False}] - - -@pytest.mark.asyncio -async def test_execute_shell_recognizes_commented_background_command(monkeypatch): - from astrbot.core.tools.computer_tools import shell as shell_tools - - calls = [] - - class FakeShell: - async def exec( - self, command, cwd=None, background=False, env=None, timeout=None - ): - calls.append({"command": command, "background": background}) - return {"success": True, "stdout": "", "stderr": "", "exit_code": 0} - - class FakeBooter: - shell = FakeShell() - - class FakeConfig: - def get_config(self, umo): - return {"provider_settings": {"computer_use_runtime": "sandbox"}} - - class FakeEvent: - unified_msg_origin = "umo" - role = "admin" - - class FakeAstrContext: - context = FakeConfig() - event = FakeEvent() - - class FakeWrapper: - context = FakeAstrContext() - - async def fake_get_booter(context, session_id): - return FakeBooter() - - monkeypatch.setattr(shell_tools, "get_booter", fake_get_booter) - - command = "firefox & # already detached" - result = await ExecuteShellTool().call( - FakeWrapper(), command=command, background=True - ) - - assert json.loads(result)["success"] is True - assert calls == [{"command": command, "background": False}] - - -@pytest.mark.parametrize( - ("command", "expected"), - [ - ("echo '#'", False), - ("echo '&'", False), - ("echo foo#bar &", True), - ("echo 'unterminated", False), - ("firefox & # already detached", True), - ("nohup firefox >/tmp/astrbot-firefox.log 2>&1 &", True), - ("firefox", False), - ], -) -def test_is_self_detached_command_handles_quotes_and_comments(command, expected): - from astrbot.core.tools.computer_tools.shell import _is_self_detached_command - - assert _is_self_detached_command(command) is expected - - -@pytest.mark.asyncio -async def test_execute_shell_reports_blank_exception_type(monkeypatch): - from astrbot.core.tools.computer_tools import shell as shell_tools - - class BlankError(Exception): - def __str__(self): - return "" - - class FakeShell: - async def exec( - self, command, cwd=None, background=False, env=None, timeout=None - ): - raise BlankError() - - class FakeBooter: - shell = FakeShell() - - class FakeConfig: - def get_config(self, umo): - return {"provider_settings": {"computer_use_runtime": "sandbox"}} - - class FakeEvent: - unified_msg_origin = "umo" - role = "admin" - - class FakeAstrContext: - context = FakeConfig() - event = FakeEvent() - - class FakeWrapper: - context = FakeAstrContext() - - async def fake_get_booter(context, session_id): - return FakeBooter() - - monkeypatch.setattr(shell_tools, "get_booter", fake_get_booter) - - result = await ExecuteShellTool().call(FakeWrapper(), command="firefox") - - assert result == "Error executing command: BlankError" - - -def test_firecrawl_tools_are_registered_as_builtin_tools(): - manager = FunctionToolManager() - - search_tool = manager.get_builtin_tool(FirecrawlWebSearchTool) - extract_tool = manager.get_builtin_tool(FirecrawlExtractWebPageTool) - - assert search_tool.name == "web_search_firecrawl" - assert extract_tool.name == "firecrawl_extract_web_page" - assert manager.is_builtin_tool("web_search_firecrawl") is True - assert manager.is_builtin_tool("firecrawl_extract_web_page") is True + assert search_tool.name == "web_search_tavily" + assert extract_tool.name == "tavily_extract_web_page" + assert manager.is_builtin_tool("web_search_tavily") is True + assert manager.is_builtin_tool("tavily_extract_web_page") is True