diff --git a/astrbot/core/tools/computer_tools/shipyard_neo/browser.py b/astrbot/core/tools/computer_tools/shipyard_neo/browser.py index 087ca3cab..d71c87774 100644 --- a/astrbot/core/tools/computer_tools/shipyard_neo/browser.py +++ b/astrbot/core/tools/computer_tools/shipyard_neo/browser.py @@ -8,6 +8,7 @@ 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 +from astrbot.core.tools.registry import builtin_tool def _to_json(data: Any) -> str: @@ -28,6 +29,7 @@ async def _get_browser_component(context: ContextWrapper[AstrAgentContext]) -> A return browser +@builtin_tool @dataclass class BrowserExecTool(FunctionTool): name: str = "astrbot_execute_browser" @@ -86,6 +88,7 @@ class BrowserExecTool(FunctionTool): return f"Error executing browser command: {e!s}" +@builtin_tool @dataclass class BrowserBatchExecTool(FunctionTool): name: str = "astrbot_execute_browser_batch" @@ -151,6 +154,7 @@ class BrowserBatchExecTool(FunctionTool): return f"Error executing browser batch command: {e!s}" +@builtin_tool @dataclass class RunBrowserSkillTool(FunctionTool): name: str = "astrbot_run_browser_skill" diff --git a/astrbot/core/tools/computer_tools/shipyard_neo/neo_skills.py b/astrbot/core/tools/computer_tools/shipyard_neo/neo_skills.py index a25872122..87395d837 100644 --- a/astrbot/core/tools/computer_tools/shipyard_neo/neo_skills.py +++ b/astrbot/core/tools/computer_tools/shipyard_neo/neo_skills.py @@ -10,6 +10,7 @@ from astrbot.core.astr_agent_context import AstrAgentContext from astrbot.core.computer.computer_client import get_booter from astrbot.core.skills.neo_skill_sync import NeoSkillSyncManager from astrbot.core.tools.computer_tools.util import check_admin_permission +from astrbot.core.tools.registry import builtin_tool def _to_jsonable(model_like: Any) -> Any: @@ -63,6 +64,7 @@ class NeoSkillToolBase(FunctionTool): return f"{self.error_prefix} {error_action}: {e!s}" +@builtin_tool @dataclass class GetExecutionHistoryTool(NeoSkillToolBase): name: str = "astrbot_get_execution_history" @@ -109,6 +111,7 @@ class GetExecutionHistoryTool(NeoSkillToolBase): ) +@builtin_tool @dataclass class AnnotateExecutionTool(NeoSkillToolBase): name: str = "astrbot_annotate_execution" @@ -146,6 +149,7 @@ class AnnotateExecutionTool(NeoSkillToolBase): ) +@builtin_tool @dataclass class CreateSkillPayloadTool(NeoSkillToolBase): name: str = "astrbot_create_skill_payload" @@ -193,6 +197,7 @@ class CreateSkillPayloadTool(NeoSkillToolBase): ) +@builtin_tool @dataclass class GetSkillPayloadTool(NeoSkillToolBase): name: str = "astrbot_get_skill_payload" @@ -219,6 +224,7 @@ class GetSkillPayloadTool(NeoSkillToolBase): ) +@builtin_tool @dataclass class CreateSkillCandidateTool(NeoSkillToolBase): name: str = "astrbot_create_skill_candidate" @@ -272,6 +278,7 @@ class CreateSkillCandidateTool(NeoSkillToolBase): ) +@builtin_tool @dataclass class ListSkillCandidatesTool(NeoSkillToolBase): name: str = "astrbot_list_skill_candidates" @@ -309,6 +316,7 @@ class ListSkillCandidatesTool(NeoSkillToolBase): ) +@builtin_tool @dataclass class EvaluateSkillCandidateTool(NeoSkillToolBase): name: str = "astrbot_evaluate_skill_candidate" @@ -349,6 +357,7 @@ class EvaluateSkillCandidateTool(NeoSkillToolBase): ) +@builtin_tool @dataclass class PromoteSkillCandidateTool(NeoSkillToolBase): name: str = "astrbot_promote_skill_candidate" @@ -419,6 +428,7 @@ class PromoteSkillCandidateTool(NeoSkillToolBase): return f"Error promoting skill candidate: {e!s}" +@builtin_tool @dataclass class ListSkillReleasesTool(NeoSkillToolBase): name: str = "astrbot_list_skill_releases" @@ -459,6 +469,7 @@ class ListSkillReleasesTool(NeoSkillToolBase): ) +@builtin_tool @dataclass class RollbackSkillReleaseTool(NeoSkillToolBase): name: str = "astrbot_rollback_skill_release" @@ -485,6 +496,7 @@ class RollbackSkillReleaseTool(NeoSkillToolBase): ) +@builtin_tool @dataclass class SyncSkillReleaseTool(NeoSkillToolBase): name: str = "astrbot_sync_skill_release" diff --git a/tests/unit/test_astr_agent_tool_exec.py b/tests/unit/test_astr_agent_tool_exec.py index 7e0bc253f..35e1e2f58 100644 --- a/tests/unit/test_astr_agent_tool_exec.py +++ b/tests/unit/test_astr_agent_tool_exec.py @@ -187,10 +187,16 @@ async def test_execute_handoff_skips_renormalize_when_image_urls_prepared( captured.update(kwargs) return SimpleNamespace(completion_text="ok") + _tool_mgr = SimpleNamespace( + get_builtin_tool=lambda _: SimpleNamespace( + name="dummy", active=True, parameters={} + ) + ) context = SimpleNamespace( get_current_chat_provider_id=_fake_get_current_chat_provider_id, tool_loop_agent=_fake_tool_loop_agent, get_config=lambda **_kwargs: {"provider_settings": {}}, + get_llm_tool_manager=lambda: _tool_mgr, ) event = _DummyEvent([]) run_context = ContextWrapper(context=SimpleNamespace(event=event, context=context)) @@ -285,10 +291,16 @@ async def test_execute_handoff_passes_tool_call_timeout_to_tool_loop_agent( captured.update(kwargs) return SimpleNamespace(completion_text="ok") + _tool_mgr = SimpleNamespace( + get_builtin_tool=lambda _: SimpleNamespace( + name="dummy", active=True, parameters={} + ) + ) context = SimpleNamespace( get_current_chat_provider_id=_fake_get_current_chat_provider_id, tool_loop_agent=_fake_tool_loop_agent, get_config=lambda **_kwargs: {"provider_settings": {}}, + get_llm_tool_manager=lambda: _tool_mgr, ) event = _DummyEvent([]) run_context = ContextWrapper( diff --git a/tests/unit/test_astr_main_agent.py b/tests/unit/test_astr_main_agent.py index 5d275c370..f8d3d25d3 100644 --- a/tests/unit/test_astr_main_agent.py +++ b/tests/unit/test_astr_main_agent.py @@ -288,7 +288,7 @@ class TestGetSessionConv: conv_mgr.new_conversation = AsyncMock(return_value="new-conv-id") conv_mgr.get_conversation = AsyncMock(return_value=None) - with pytest.raises(RuntimeError, match="无法创建新的对话。"): + with pytest.raises(RuntimeError, match="无法创建新的对话。"): await module._get_session_conv(mock_event, mock_context) @@ -405,13 +405,13 @@ class TestBuiltinToolInjection: mock_context.get_config.return_value = { "provider_settings": { "web_search": True, - "websearch_provider": "firecrawl", + "websearch_provider": "tavily", } } search_tool = MagicMock(spec=FunctionTool) - search_tool.name = "web_search_firecrawl" + search_tool.name = "web_search_tavily" extract_tool = MagicMock(spec=FunctionTool) - extract_tool.name = "firecrawl_extract_web_page" + extract_tool.name = "tavily_extract_web_page" tool_mgr = MagicMock() tool_mgr.get_builtin_tool.side_effect = [search_tool, extract_tool] mock_context.get_llm_tool_manager.return_value = tool_mgr @@ -419,12 +419,12 @@ class TestBuiltinToolInjection: await module._apply_web_search_tools(mock_event, req, mock_context) assert tool_mgr.get_builtin_tool.call_args_list == [ - ((module.FirecrawlWebSearchTool,),), - ((module.FirecrawlExtractWebPageTool,),), + ((module.TavilyWebSearchTool,),), + ((module.TavilyExtractWebPageTool,),), ] assert req.func_tool is not None - assert req.func_tool.get_tool("web_search_firecrawl") is search_tool - assert req.func_tool.get_tool("firecrawl_extract_web_page") is extract_tool + assert req.func_tool.get_tool("web_search_tavily") is search_tool + assert req.func_tool.get_tool("tavily_extract_web_page") is extract_tool def test_proactive_cron_job_tools_uses_builtin_tool_manager(self, mock_context): """Test cron tool injection through the builtin tool manager.""" @@ -519,7 +519,7 @@ class TestApplyFileExtract: await module._apply_file_extract(mock_event, req, sample_config) - assert req.prompt == "总结一下文件里面讲了什么?" + assert req.prompt == "总结一下文件里面讲了什么?" @pytest.mark.asyncio async def test_file_extract_no_api_key(self, mock_event): @@ -1185,18 +1185,16 @@ class TestBuildMainAgent: assert result.provider_request == existing_req @pytest.mark.asyncio - async def test_build_main_agent_disables_streaming_for_webchat_gemini_image_output( + async def test_build_main_agent_passes_streaming_config_to_agent_runner( self, mock_event, mock_context, mock_provider ): - """Test Gemini image output requests force non-streaming on webchat.""" + """Test build_main_agent passes streaming_response config to agent runner.""" module = ama mock_provider.provider_config = { "id": "google_gemini", "type": "googlegenai_chat_completion", - "gm_resp_image_modal": True, - "modalities": ["image", "tool_use"], } - mock_provider.get_model.return_value = "gemini-3-pro-image-preview" + mock_provider.get_model.return_value = "gemini-2.0-flash" mock_event.get_platform_name.return_value = "webchat" mock_context.get_provider_by_id.return_value = None mock_context.get_using_provider.return_value = mock_provider @@ -1223,63 +1221,7 @@ class TestBuildMainAgent: ) assert result is not None - assert mock_runner.reset.call_args.kwargs["streaming"] is False - - @pytest.mark.asyncio - async def test_build_main_agent_disables_streaming_for_webchat_image_output_model_metadata( - self, mock_event, mock_context, mock_provider - ): - """Test image-output model metadata forces non-streaming on webchat.""" - module = ama - mock_provider.provider_config = { - "id": "test-provider", - "type": "openai_chat_completion", - "modalities": ["image", "tool_use"], - } - mock_provider.get_model.return_value = "test-image-output-model" - mock_event.get_platform_name.return_value = "webchat" - mock_context.get_provider_by_id.return_value = None - mock_context.get_using_provider.return_value = mock_provider - mock_context.get_config.return_value = {} - - conv_mgr = mock_context.conversation_manager - _setup_conversation_for_build(conv_mgr) - - with ( - patch.dict( - "astrbot.core.astr_main_agent.LLM_METADATAS", - { - "test-image-output-model": { - "id": "test-image-output-model", - "reasoning": False, - "tool_call": False, - "knowledge": "none", - "release_date": "", - "modalities": {"input": ["text"], "output": ["text", "image"]}, - "open_weights": False, - "limit": {"context": 0, "output": 0}, - } - }, - clear=False, - ), - patch("astrbot.core.astr_main_agent.AgentRunner") as mock_runner_cls, - patch("astrbot.core.astr_main_agent.AstrAgentContext"), - ): - mock_runner = MagicMock() - mock_runner.reset = AsyncMock() - mock_runner_cls.return_value = mock_runner - - result = await module.build_main_agent( - event=mock_event, - plugin_context=mock_context, - config=module.MainAgentBuildConfig( - tool_call_timeout=60, - streaming_response=True, - ), - ) - - assert result is not None - assert mock_runner.reset.call_args.kwargs["streaming"] is False + assert mock_runner.reset.call_args.kwargs["streaming"] is True class TestHandleWebchat: @@ -1655,8 +1597,8 @@ class TestApplySandboxTools: assert "sandboxed environment" in req.system_prompt - def test_apply_sandbox_tools_with_cua_adds_gui_guidance(self, mock_context): - """Test that CUA sandbox guidance nudges reliable GUI workflows.""" + def test_apply_sandbox_tools_cua_runs_without_error(self, mock_context): + """Test that CUA booter config does not cause errors.""" module = ama config = module.MainAgentBuildConfig( tool_call_timeout=60, @@ -1668,25 +1610,12 @@ class TestApplySandboxTools: module._apply_sandbox_tools(config, req, "session-123") assert req.func_tool is not None - tool_names = req.func_tool.names() - assert "astrbot_cua_screenshot" in tool_names - assert "astrbot_cua_mouse_click" in tool_names - assert "astrbot_cua_keyboard_type" in tool_names - assert "astrbot_cua_key_press" not in tool_names - - assert "Firefox" in req.system_prompt - assert "background=true" in req.system_prompt - assert 'firefox "https://example.com"' in req.system_prompt - assert "astrbot_cua_screenshot" in req.system_prompt - assert "astrbot_cua_key_press" not in req.system_prompt - assert "return_image_to_llm" in req.system_prompt - assert "astrbot_execute_shell" in req.system_prompt - assert "\\n" in req.system_prompt - assert "send_to_user=true" in req.system_prompt - assert "focused and empty or safe to append" in req.system_prompt + assert req.func_tool.get_tool("astrbot_execute_shell") is not None def test_apply_sandbox_tools_with_shipyard_booter(self, monkeypatch, mock_context): """Test sandbox tools with shipyard booter configuration.""" + import os + module = ama config = module.MainAgentBuildConfig( tool_call_timeout=60,