diff --git a/app/agent/prompt/__init__.py b/app/agent/prompt/__init__.py index 3e9de3e6..6d1072ac 100644 --- a/app/agent/prompt/__init__.py +++ b/app/agent/prompt/__init__.py @@ -142,6 +142,7 @@ class PromptManager: self.prompts_cache: Dict[str, str] = {} self._system_tasks_cache: Optional[SystemTasksDefinition] = None self._system_tasks_signature: Optional[tuple[int, int]] = None + self._available_shell_commands_cache: Optional[list[tuple[str, str]]] = None def load_prompt(self, prompt_name: str) -> str: """ @@ -329,8 +330,7 @@ class PromptManager: sections.append(self._format_numbered_rules("IMPORTANT", rules)) return "\n\n".join(section for section in sections if section).strip() - @staticmethod - def _get_moviepilot_info() -> str: + def _get_moviepilot_info(self) -> str: """ 获取MoviePilot系统信息,用于注入到系统提示词中 """ @@ -382,7 +382,7 @@ class PromptManager: f"- 系统安装目录: {settings.ROOT_PATH}", ] - available_commands = PromptManager._get_available_shell_commands() + available_commands = self._get_available_shell_commands() if available_commands: info_lines.append("- 可用系统命令(可通过 `execute_command` 调用):") info_lines.extend( @@ -391,21 +391,29 @@ class PromptManager: return "\n".join(info_lines) - @staticmethod - def _get_available_shell_commands() -> list[tuple[str, str]]: + def _get_available_shell_commands(self) -> list[tuple[str, str]]: """ 探测 PATH 中已经安装的常用命令。 这里只使用 shutil.which 做无副作用查找,不实际执行命令;执行权限、 - 高风险操作确认和输出限制仍由 execute_command 工具负责。 + 高风险操作确认和输出限制仍由 execute_command 工具负责。探测结果 + 在进程内缓存,避免每次组装提示词都重复扫描 PATH。 """ + if self._available_shell_commands_cache is not None: + return self._available_shell_commands_cache + available_commands: list[tuple[str, str]] = [] for command in COMMON_SHELL_COMMANDS: command_path = shutil.which(command) if command_path: available_commands.append((command, command_path)) + self._available_shell_commands_cache = available_commands return available_commands + def clear_available_shell_commands_cache(self) -> None: + """清理可用系统命令缓存,供测试或运行时手动刷新使用。""" + self._available_shell_commands_cache = None + @staticmethod def _generate_formatting_instructions(caps: ChannelCapabilities) -> str: """ diff --git a/tests/test_agent_prompt_style.py b/tests/test_agent_prompt_style.py index 10058dfd..3b32641d 100644 --- a/tests/test_agent_prompt_style.py +++ b/tests/test_agent_prompt_style.py @@ -3,7 +3,7 @@ from unittest.mock import patch from app.agent.middleware.memory import MEMORY_ONBOARDING_PROMPT from app.agent.middleware.runtime_config import RuntimeConfigMiddleware -from app.agent.prompt import PromptConfigError, prompt_manager +from app.agent.prompt import COMMON_SHELL_COMMANDS, PromptConfigError, prompt_manager from app.core.config import settings @@ -16,6 +16,14 @@ class _FakeRequest: class TestAgentPromptStyle(unittest.TestCase): + def setUp(self): + """每个用例前清理系统命令缓存,避免本机 PATH 或测试顺序影响断言。""" + prompt_manager.clear_available_shell_commands_cache() + + def tearDown(self): + """每个用例后清理系统命令缓存,避免 mock 探测结果泄漏到后续用例。""" + prompt_manager.clear_available_shell_commands_cache() + def test_base_prompt_mentions_persona_management_tools(self): prompt = prompt_manager.get_agent_prompt() @@ -62,6 +70,20 @@ class TestAgentPromptStyle(unittest.TestCase): self.assertNotIn("可用系统命令", prompt) + def test_available_shell_commands_are_cached_after_first_scan(self): + """常用命令探测应只在首次加载时扫描 PATH,后续提示词复用缓存。""" + command_paths = {"ssh": "/usr/bin/ssh"} + with patch( + "app.agent.prompt.shutil.which", + side_effect=lambda command: command_paths.get(command), + ) as which_mock: + first_prompt = prompt_manager.get_agent_prompt() + second_prompt = prompt_manager.get_agent_prompt() + + self.assertIn(" - ssh: /usr/bin/ssh", first_prompt) + self.assertIn(" - ssh: /usr/bin/ssh", second_prompt) + self.assertEqual(which_mock.call_count, len(COMMON_SHELL_COMMANDS)) + def test_runtime_config_middleware_injects_persona_only(self): middleware = RuntimeConfigMiddleware() updated_request = middleware.modify_request(_FakeRequest())