mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-15 17:30:13 +08:00
fix: restore missing sentinel classes and import lost in merge
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user