From 4208c79d7241f47b84bcfe7d9a735ef149c9526d Mon Sep 17 00:00:00 2001 From: jxxghp Date: Sun, 26 Apr 2026 11:15:11 +0800 Subject: [PATCH] =?UTF-8?q?refine=20tool=E6=8F=90=E7=A4=BA=E8=AF=AD?= =?UTF-8?q?=E4=B8=BA=E6=9B=B4=E7=AE=80=E6=B4=81=E9=A3=8E=E6=A0=BC=EF=BC=8C?= =?UTF-8?q?=E8=A1=A5=E5=85=85last=5Fbuffer=5Fchar=E5=B1=9E=E6=80=A7?= =?UTF-8?q?=E5=8F=8A=E9=9D=9EVERBOSE=E6=A8=A1=E5=BC=8F=E6=B5=81=E5=BC=8F?= =?UTF-8?q?=E8=BE=93=E5=87=BA=E6=8D=A2=E8=A1=8C=E9=80=BB=E8=BE=91=EF=BC=8C?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=B7=A5=E5=85=B7=E6=B5=81=E5=BC=8F=E5=88=86?= =?UTF-8?q?=E9=9A=94=E7=AC=A6=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/agent/callback/__init__.py | 8 +++ app/agent/tools/base.py | 5 +- app/agent/tools/impl/add_download.py | 8 +-- app/agent/tools/impl/add_subscribe.py | 2 +- app/agent/tools/impl/ask_user_choice.py | 2 +- app/agent/tools/impl/browse_webpage.py | 18 +++---- app/agent/tools/impl/delete_download.py | 2 +- .../tools/impl/delete_download_history.py | 2 +- app/agent/tools/impl/delete_subscribe.py | 2 +- .../tools/impl/delete_transfer_history.py | 2 +- app/agent/tools/impl/edit_file.py | 2 +- app/agent/tools/impl/execute_command.py | 2 +- app/agent/tools/impl/get_recommendations.py | 4 +- app/agent/tools/impl/get_search_results.py | 2 +- app/agent/tools/impl/list_directory.py | 2 +- app/agent/tools/impl/list_slash_commands.py | 2 +- app/agent/tools/impl/modify_download.py | 2 +- .../tools/impl/query_custom_identifiers.py | 2 +- .../tools/impl/query_directory_settings.py | 2 +- app/agent/tools/impl/query_download_tasks.py | 4 +- app/agent/tools/impl/query_downloaders.py | 2 +- .../tools/impl/query_episode_schedule.py | 2 +- .../tools/impl/query_installed_plugins.py | 2 +- app/agent/tools/impl/query_library_exists.py | 6 +-- app/agent/tools/impl/query_library_latest.py | 2 +- app/agent/tools/impl/query_media_detail.py | 4 +- .../tools/impl/query_plugin_capabilities.py | 4 +- .../tools/impl/query_popular_subscribes.py | 2 +- app/agent/tools/impl/query_rule_groups.py | 2 +- app/agent/tools/impl/query_schedulers.py | 2 +- app/agent/tools/impl/query_site_userdata.py | 2 +- app/agent/tools/impl/query_sites.py | 2 +- .../tools/impl/query_subscribe_history.py | 2 +- .../tools/impl/query_subscribe_shares.py | 2 +- app/agent/tools/impl/query_subscribes.py | 2 +- .../tools/impl/query_transfer_history.py | 2 +- app/agent/tools/impl/query_workflows.py | 2 +- app/agent/tools/impl/read_file.py | 2 +- app/agent/tools/impl/recognize_media.py | 6 +-- app/agent/tools/impl/run_scheduler.py | 2 +- app/agent/tools/impl/run_slash_command.py | 2 +- app/agent/tools/impl/run_workflow.py | 2 +- app/agent/tools/impl/scrape_metadata.py | 2 +- app/agent/tools/impl/search_media.py | 2 +- app/agent/tools/impl/search_person.py | 2 +- app/agent/tools/impl/search_person_credits.py | 2 +- app/agent/tools/impl/search_subscribe.py | 2 +- app/agent/tools/impl/search_torrents.py | 6 +-- app/agent/tools/impl/search_web.py | 2 +- app/agent/tools/impl/send_local_file.py | 2 +- app/agent/tools/impl/send_message.py | 8 +-- app/agent/tools/impl/send_voice_message.py | 2 +- app/agent/tools/impl/test_site.py | 2 +- app/agent/tools/impl/transfer_file.py | 2 +- .../tools/impl/update_custom_identifiers.py | 2 +- app/agent/tools/impl/update_site.py | 4 +- app/agent/tools/impl/update_site_cookie.py | 2 +- app/agent/tools/impl/update_subscribe.py | 4 +- app/agent/tools/impl/write_file.py | 2 +- tests/test_agent_tool_streaming.py | 53 +++++++++++++++++++ 60 files changed, 148 insertions(+), 84 deletions(-) create mode 100644 tests/test_agent_tool_streaming.py diff --git a/app/agent/callback/__init__.py b/app/agent/callback/__init__.py index 7dc5ae44..2cf5de92 100644 --- a/app/agent/callback/__init__.py +++ b/app/agent/callback/__init__.py @@ -360,3 +360,11 @@ class StreamingHandler: 是否已经通过流式输出发送过消息(当前轮次) """ return self._message_response is not None + + @property + def last_buffer_char(self) -> str: + """ + 返回当前缓冲区最后一个字符;缓冲区为空时返回空字符串。 + """ + with self._lock: + return self._buffer[-1:] if self._buffer else "" diff --git a/app/agent/tools/base.py b/app/agent/tools/base.py index 8529b402..800ae420 100644 --- a/app/agent/tools/base.py +++ b/app/agent/tools/base.py @@ -81,7 +81,10 @@ class MoviePilotTool(BaseTool, metaclass=ABCMeta): if messages: merged_message = "\n\n".join(messages) await self.send_tool_message(merged_message) - # 非VERBOSE:不重置流,保留已输出的模型思考文本 + else: + # 非VERBOSE:工具边界至少补一个换行,避免工具前后的文本直接连在一起 + if self._stream_handler.last_buffer_char not in ("", "\n"): + self._stream_handler.emit("\n") else: # 未启用流式传输,不发送任何工具消息内容 pass diff --git a/app/agent/tools/impl/add_download.py b/app/agent/tools/impl/add_download.py index daa9faab..74f4e4d1 100644 --- a/app/agent/tools/impl/add_download.py +++ b/app/agent/tools/impl/add_download.py @@ -47,13 +47,13 @@ class AddDownloadTool(MoviePilotTool): if torrent_urls: if len(torrent_urls) == 1: if self._is_torrent_ref(torrent_urls[0]): - message = f"正在添加下载任务: 资源 {torrent_urls[0]}" + message = f"添加下载任务: 资源 {torrent_urls[0]}" else: - message = "正在添加下载任务: 磁力链接" + message = "添加下载任务: 磁力链接" else: - message = f"正在批量添加下载任务: 共 {len(torrent_urls)} 个资源" + message = f"批量添加下载任务: 共 {len(torrent_urls)} 个资源" else: - message = "正在添加下载任务" + message = "添加下载任务" if downloader: message += f" [下载器: {downloader}]" diff --git a/app/agent/tools/impl/add_subscribe.py b/app/agent/tools/impl/add_subscribe.py index ab2f1bf9..518ff000 100644 --- a/app/agent/tools/impl/add_subscribe.py +++ b/app/agent/tools/impl/add_subscribe.py @@ -89,7 +89,7 @@ class AddSubscribeTool(MoviePilotTool): media_type = kwargs.get("media_type", "") season = kwargs.get("season") - message = f"正在添加订阅: {title}" + message = f"添加订阅: {title}" if year: message += f" ({year})" if media_type: diff --git a/app/agent/tools/impl/ask_user_choice.py b/app/agent/tools/impl/ask_user_choice.py index ca725566..28bf7c12 100644 --- a/app/agent/tools/impl/ask_user_choice.py +++ b/app/agent/tools/impl/ask_user_choice.py @@ -75,7 +75,7 @@ class AskUserChoiceTool(MoviePilotTool): message = kwargs.get("message", "") or "" if len(message) > 40: message = message[:40] + "..." - return f"正在发送按钮选择: {message}" + return f"发送按钮选择: {message}" @staticmethod def _truncate_button_text(text: str, max_length: int) -> str: diff --git a/app/agent/tools/impl/browse_webpage.py b/app/agent/tools/impl/browse_webpage.py index e5d812a5..df917246 100644 --- a/app/agent/tools/impl/browse_webpage.py +++ b/app/agent/tools/impl/browse_webpage.py @@ -108,16 +108,16 @@ class BrowseWebpageTool(MoviePilotTool): url = kwargs.get("url", "") selector = kwargs.get("selector", "") action_messages = { - "goto": f"正在打开网页: {url}", - "get_content": "正在获取页面内容", - "screenshot": "正在截取页面截图", - "click": f"正在点击元素: {selector}", - "fill": f"正在填写表单: {selector}", - "select": f"正在选择选项: {selector}", - "evaluate": "正在执行 JavaScript", - "wait": f"正在等待元素: {selector}", + "goto": f"打开网页: {url}", + "get_content": "获取页面内容", + "screenshot": "截取页面截图", + "click": f"点击元素: {selector}", + "fill": f"填写表单: {selector}", + "select": f"选择选项: {selector}", + "evaluate": "执行 JavaScript", + "wait": f"等待元素: {selector}", } - return action_messages.get(action, f"正在执行浏览器操作: {action}") + return action_messages.get(action, f"执行浏览器操作: {action}") async def run( self, diff --git a/app/agent/tools/impl/delete_download.py b/app/agent/tools/impl/delete_download.py index ebd54bac..5794a99d 100644 --- a/app/agent/tools/impl/delete_download.py +++ b/app/agent/tools/impl/delete_download.py @@ -41,7 +41,7 @@ class DeleteDownloadTool(MoviePilotTool): downloader = kwargs.get("downloader") delete_files = kwargs.get("delete_files", False) - message = f"正在删除下载任务: {hash_value}" + message = f"删除下载任务: {hash_value}" if downloader: message += f" [下载器: {downloader}]" if delete_files: diff --git a/app/agent/tools/impl/delete_download_history.py b/app/agent/tools/impl/delete_download_history.py index 0fb3be94..bd249ac1 100644 --- a/app/agent/tools/impl/delete_download_history.py +++ b/app/agent/tools/impl/delete_download_history.py @@ -30,7 +30,7 @@ class DeleteDownloadHistoryTool(MoviePilotTool): def get_tool_message(self, **kwargs) -> Optional[str]: history_id = kwargs.get("history_id") - return f"正在删除下载历史记录 ID: {history_id}" + return f"删除下载历史记录 ID: {history_id}" async def run(self, history_id: int, **kwargs) -> str: logger.info(f"执行工具: {self.name}, 参数: history_id={history_id}") diff --git a/app/agent/tools/impl/delete_subscribe.py b/app/agent/tools/impl/delete_subscribe.py index 045cfffc..3b1e4c7c 100644 --- a/app/agent/tools/impl/delete_subscribe.py +++ b/app/agent/tools/impl/delete_subscribe.py @@ -34,7 +34,7 @@ class DeleteSubscribeTool(MoviePilotTool): def get_tool_message(self, **kwargs) -> Optional[str]: """根据删除参数生成友好的提示消息""" subscribe_id = kwargs.get("subscribe_id") - return f"正在删除订阅 (ID: {subscribe_id})" + return f"删除订阅 (ID: {subscribe_id})" async def run(self, subscribe_id: int, **kwargs) -> str: logger.info(f"执行工具: {self.name}, 参数: subscribe_id={subscribe_id}") diff --git a/app/agent/tools/impl/delete_transfer_history.py b/app/agent/tools/impl/delete_transfer_history.py index c42b72dd..11f885b0 100644 --- a/app/agent/tools/impl/delete_transfer_history.py +++ b/app/agent/tools/impl/delete_transfer_history.py @@ -30,7 +30,7 @@ class DeleteTransferHistoryTool(MoviePilotTool): def get_tool_message(self, **kwargs) -> Optional[str]: """根据参数生成友好的提示消息""" history_id = kwargs.get("history_id") - return f"正在删除整理历史记录: ID={history_id}" + return f"删除整理历史记录: ID={history_id}" async def run(self, history_id: int, **kwargs) -> str: logger.info(f"执行工具: {self.name}, 参数: history_id={history_id}") diff --git a/app/agent/tools/impl/edit_file.py b/app/agent/tools/impl/edit_file.py index b26dce66..73fe9545 100644 --- a/app/agent/tools/impl/edit_file.py +++ b/app/agent/tools/impl/edit_file.py @@ -28,7 +28,7 @@ class EditFileTool(MoviePilotTool): """根据参数生成友好的提示消息""" file_path = kwargs.get("file_path", "") file_name = Path(file_path).name if file_path else "未知文件" - return f"正在编辑文件: {file_name}" + return f"编辑文件: {file_name}" async def run(self, file_path: str, old_text: str, new_text: str, **kwargs) -> str: logger.info(f"执行工具: {self.name}, 参数: file_path={file_path}") diff --git a/app/agent/tools/impl/execute_command.py b/app/agent/tools/impl/execute_command.py index 52b40336..a5cfb4b1 100644 --- a/app/agent/tools/impl/execute_command.py +++ b/app/agent/tools/impl/execute_command.py @@ -30,7 +30,7 @@ class ExecuteCommandTool(MoviePilotTool): def get_tool_message(self, **kwargs) -> Optional[str]: """根据命令生成友好的提示消息""" command = kwargs.get("command", "") - return f"正在执行系统命令: {command}" + return f"执行系统命令: {command}" async def run(self, command: str, timeout: Optional[int] = 60, **kwargs) -> str: logger.info( diff --git a/app/agent/tools/impl/get_recommendations.py b/app/agent/tools/impl/get_recommendations.py index e7f440b8..684be683 100644 --- a/app/agent/tools/impl/get_recommendations.py +++ b/app/agent/tools/impl/get_recommendations.py @@ -62,7 +62,7 @@ class GetRecommendationsTool(MoviePilotTool): "douban_hot": "豆瓣热门", "douban_movie_hot": "豆瓣热门电影", "douban_tv_hot": "豆瓣热门电视剧", - "douban_movie_showing": "豆瓣正在热映", + "douban_movie_showing": "豆瓣热映", "douban_movies": "豆瓣最新电影", "douban_tvs": "豆瓣最新电视剧", "douban_movie_top250": "豆瓣电影TOP250", @@ -73,7 +73,7 @@ class GetRecommendationsTool(MoviePilotTool): } source_desc = source_map.get(source, source) - message = f"正在获取推荐: {source_desc}" + message = f"获取推荐: {source_desc}" if media_type != "all": message += f" [{media_type}]" message += f" (第{page}页)" diff --git a/app/agent/tools/impl/get_search_results.py b/app/agent/tools/impl/get_search_results.py index 49ffbc36..f3d6936f 100644 --- a/app/agent/tools/impl/get_search_results.py +++ b/app/agent/tools/impl/get_search_results.py @@ -53,7 +53,7 @@ class GetSearchResultsTool(MoviePilotTool): args_schema: Type[BaseModel] = GetSearchResultsInput def get_tool_message(self, **kwargs) -> Optional[str]: - return "正在获取搜索结果" + return "获取搜索结果" async def run( self, diff --git a/app/agent/tools/impl/list_directory.py b/app/agent/tools/impl/list_directory.py index 7ae050d9..9f43d31c 100644 --- a/app/agent/tools/impl/list_directory.py +++ b/app/agent/tools/impl/list_directory.py @@ -32,7 +32,7 @@ class ListDirectoryTool(MoviePilotTool): path = kwargs.get("path", "") storage = kwargs.get("storage", "local") - message = f"正在查询目录: {path}" + message = f"查询目录: {path}" if storage != "local": message += f" [存储: {storage}]" diff --git a/app/agent/tools/impl/list_slash_commands.py b/app/agent/tools/impl/list_slash_commands.py index a6ebe866..86706bc8 100644 --- a/app/agent/tools/impl/list_slash_commands.py +++ b/app/agent/tools/impl/list_slash_commands.py @@ -33,7 +33,7 @@ class ListSlashCommandsTool(MoviePilotTool): def get_tool_message(self, **kwargs) -> Optional[str]: """生成友好的提示消息""" - return "正在查询所有可用命令" + return "查询所有可用命令" async def run(self, **kwargs) -> str: logger.info(f"执行工具: {self.name}") diff --git a/app/agent/tools/impl/modify_download.py b/app/agent/tools/impl/modify_download.py index 65b98900..88f64400 100644 --- a/app/agent/tools/impl/modify_download.py +++ b/app/agent/tools/impl/modify_download.py @@ -55,7 +55,7 @@ class ModifyDownloadTool(MoviePilotTool): tags = kwargs.get("tags") downloader = kwargs.get("downloader") - parts = [f"正在修改下载任务: {hash_value}"] + parts = [f"修改下载任务: {hash_value}"] if action == "start": parts.append("操作: 开始下载") elif action == "stop": diff --git a/app/agent/tools/impl/query_custom_identifiers.py b/app/agent/tools/impl/query_custom_identifiers.py index ce1f1aa3..8ae40bcf 100644 --- a/app/agent/tools/impl/query_custom_identifiers.py +++ b/app/agent/tools/impl/query_custom_identifiers.py @@ -31,7 +31,7 @@ class QueryCustomIdentifiersTool(MoviePilotTool): def get_tool_message(self, **kwargs) -> Optional[str]: """生成友好的提示消息""" - return "正在查询自定义识别词" + return "查询自定义识别词" async def run(self, **kwargs) -> str: logger.info(f"执行工具: {self.name}") diff --git a/app/agent/tools/impl/query_directory_settings.py b/app/agent/tools/impl/query_directory_settings.py index 2754a1bf..7f4a89df 100644 --- a/app/agent/tools/impl/query_directory_settings.py +++ b/app/agent/tools/impl/query_directory_settings.py @@ -32,7 +32,7 @@ class QueryDirectorySettingsTool(MoviePilotTool): storage_type = kwargs.get("storage_type", "all") name = kwargs.get("name") - parts = ["正在查询目录配置"] + parts = ["查询目录配置"] if directory_type != "all": type_map = {"download": "下载目录", "library": "媒体库目录"} diff --git a/app/agent/tools/impl/query_download_tasks.py b/app/agent/tools/impl/query_download_tasks.py index 433fed01..6f0dc56a 100644 --- a/app/agent/tools/impl/query_download_tasks.py +++ b/app/agent/tools/impl/query_download_tasks.py @@ -36,7 +36,7 @@ class QueryDownloadTasksTool(MoviePilotTool): 查询所有状态的任务(包括下载中和已完成的任务) """ all_torrents = [] - # 查询正在下载的任务 + # 查询下载的任务 downloading_torrents = download_chain.list_torrents( downloader=downloader, status=TorrentStatus.DOWNLOADING @@ -71,7 +71,7 @@ class QueryDownloadTasksTool(MoviePilotTool): hash_value = kwargs.get("hash") title = kwargs.get("title") - parts = ["正在查询下载任务"] + parts = ["查询下载任务"] if downloader: parts.append(f"下载器: {downloader}") diff --git a/app/agent/tools/impl/query_downloaders.py b/app/agent/tools/impl/query_downloaders.py index dce2a1ec..3d80f7f6 100644 --- a/app/agent/tools/impl/query_downloaders.py +++ b/app/agent/tools/impl/query_downloaders.py @@ -23,7 +23,7 @@ class QueryDownloadersTool(MoviePilotTool): def get_tool_message(self, **kwargs) -> Optional[str]: """生成友好的提示消息""" - return "正在查询下载器配置" + return "查询下载器配置" async def run(self, **kwargs) -> str: logger.info(f"执行工具: {self.name}") diff --git a/app/agent/tools/impl/query_episode_schedule.py b/app/agent/tools/impl/query_episode_schedule.py index cb4ce4cf..592dcd84 100644 --- a/app/agent/tools/impl/query_episode_schedule.py +++ b/app/agent/tools/impl/query_episode_schedule.py @@ -29,7 +29,7 @@ class QueryEpisodeScheduleTool(MoviePilotTool): season = kwargs.get("season") episode_group = kwargs.get("episode_group") - message = f"正在查询剧集上映时间: TMDB ID {tmdb_id} 第{season}季" + message = f"查询剧集上映时间: TMDB ID {tmdb_id} 第{season}季" if episode_group: message += f" (剧集组: {episode_group})" diff --git a/app/agent/tools/impl/query_installed_plugins.py b/app/agent/tools/impl/query_installed_plugins.py index d435a874..d032fe10 100644 --- a/app/agent/tools/impl/query_installed_plugins.py +++ b/app/agent/tools/impl/query_installed_plugins.py @@ -31,7 +31,7 @@ class QueryInstalledPluginsTool(MoviePilotTool): def get_tool_message(self, **kwargs) -> Optional[str]: """生成友好的提示消息""" - return "正在查询已安装插件" + return "查询已安装插件" async def run(self, **kwargs) -> str: logger.info(f"执行工具: {self.name}") diff --git a/app/agent/tools/impl/query_library_exists.py b/app/agent/tools/impl/query_library_exists.py index 4eeb18a7..e6c1c713 100644 --- a/app/agent/tools/impl/query_library_exists.py +++ b/app/agent/tools/impl/query_library_exists.py @@ -93,11 +93,11 @@ class QueryLibraryExistsTool(MoviePilotTool): media_type = kwargs.get("media_type") if tmdb_id: - message = f"正在查询媒体库: TMDB={tmdb_id}" + message = f"查询媒体库: TMDB={tmdb_id}" elif douban_id: - message = f"正在查询媒体库: 豆瓣={douban_id}" + message = f"查询媒体库: 豆瓣={douban_id}" else: - message = "正在查询媒体库" + message = "查询媒体库" if media_type: message += f" [{media_type}]" return message diff --git a/app/agent/tools/impl/query_library_latest.py b/app/agent/tools/impl/query_library_latest.py index e1d56515..8ec5c377 100644 --- a/app/agent/tools/impl/query_library_latest.py +++ b/app/agent/tools/impl/query_library_latest.py @@ -39,7 +39,7 @@ class QueryLibraryLatestTool(MoviePilotTool): server = kwargs.get("server") page = kwargs.get("page", 1) - parts = ["正在查询媒体服务器最近入库影片"] + parts = ["查询媒体服务器最近入库影片"] if server: parts.append(f"服务器: {server}") diff --git a/app/agent/tools/impl/query_media_detail.py b/app/agent/tools/impl/query_media_detail.py index f1600aa9..ec0ba405 100644 --- a/app/agent/tools/impl/query_media_detail.py +++ b/app/agent/tools/impl/query_media_detail.py @@ -29,8 +29,8 @@ class QueryMediaDetailTool(MoviePilotTool): tmdb_id = kwargs.get("tmdb_id") douban_id = kwargs.get("douban_id") if tmdb_id: - return f"正在查询媒体详情: TMDB ID {tmdb_id}" - return f"正在查询媒体详情: 豆瓣 ID {douban_id}" + return f"查询媒体详情: TMDB ID {tmdb_id}" + return f"查询媒体详情: 豆瓣 ID {douban_id}" async def run(self, media_type: str, tmdb_id: Optional[int] = None, douban_id: Optional[str] = None, **kwargs) -> str: logger.info(f"执行工具: {self.name}, 参数: tmdb_id={tmdb_id}, douban_id={douban_id}, media_type={media_type}") diff --git a/app/agent/tools/impl/query_plugin_capabilities.py b/app/agent/tools/impl/query_plugin_capabilities.py index 821de940..6c9ee00b 100644 --- a/app/agent/tools/impl/query_plugin_capabilities.py +++ b/app/agent/tools/impl/query_plugin_capabilities.py @@ -40,8 +40,8 @@ class QueryPluginCapabilitiesTool(MoviePilotTool): """生成友好的提示消息""" plugin_id = kwargs.get("plugin_id") if plugin_id: - return f"正在查询插件 {plugin_id} 的能力" - return "正在查询所有插件的能力" + return f"查询插件 {plugin_id} 的能力" + return "查询所有插件的能力" async def run(self, plugin_id: Optional[str] = None, **kwargs) -> str: logger.info(f"执行工具: {self.name}, 参数: plugin_id={plugin_id}") diff --git a/app/agent/tools/impl/query_popular_subscribes.py b/app/agent/tools/impl/query_popular_subscribes.py index 9d003c4c..c2185cd0 100644 --- a/app/agent/tools/impl/query_popular_subscribes.py +++ b/app/agent/tools/impl/query_popular_subscribes.py @@ -39,7 +39,7 @@ class QueryPopularSubscribesTool(MoviePilotTool): min_rating = kwargs.get("min_rating") max_rating = kwargs.get("max_rating") - parts = [f"正在查询热门订阅 [{media_type}]"] + parts = [f"查询热门订阅 [{media_type}]"] if min_sub: parts.append(f"最少订阅: {min_sub}") diff --git a/app/agent/tools/impl/query_rule_groups.py b/app/agent/tools/impl/query_rule_groups.py index f4a2b3b9..ddc83010 100644 --- a/app/agent/tools/impl/query_rule_groups.py +++ b/app/agent/tools/impl/query_rule_groups.py @@ -22,7 +22,7 @@ class QueryRuleGroupsTool(MoviePilotTool): def get_tool_message(self, **kwargs) -> Optional[str]: """根据查询参数生成友好的提示消息""" - return "正在查询所有规则组" + return "查询所有规则组" async def run(self, **kwargs) -> str: logger.info(f"执行工具: {self.name}") diff --git a/app/agent/tools/impl/query_schedulers.py b/app/agent/tools/impl/query_schedulers.py index 0ca27767..b254dd9b 100644 --- a/app/agent/tools/impl/query_schedulers.py +++ b/app/agent/tools/impl/query_schedulers.py @@ -22,7 +22,7 @@ class QuerySchedulersTool(MoviePilotTool): def get_tool_message(self, **kwargs) -> Optional[str]: """生成友好的提示消息""" - return "正在查询定时服务" + return "查询定时服务" async def run(self, **kwargs) -> str: logger.info(f"执行工具: {self.name}") diff --git a/app/agent/tools/impl/query_site_userdata.py b/app/agent/tools/impl/query_site_userdata.py index 4a7315e0..34c47170 100644 --- a/app/agent/tools/impl/query_site_userdata.py +++ b/app/agent/tools/impl/query_site_userdata.py @@ -40,7 +40,7 @@ class QuerySiteUserdataTool(MoviePilotTool): site_id = kwargs.get("site_id") workdate = kwargs.get("workdate") - message = f"正在查询站点 #{site_id} 的用户数据" + message = f"查询站点 #{site_id} 的用户数据" if workdate: message += f" (日期: {workdate})" else: diff --git a/app/agent/tools/impl/query_sites.py b/app/agent/tools/impl/query_sites.py index d858baf6..a9aef571 100644 --- a/app/agent/tools/impl/query_sites.py +++ b/app/agent/tools/impl/query_sites.py @@ -37,7 +37,7 @@ class QuerySitesTool(MoviePilotTool): status = kwargs.get("status", "all") name = kwargs.get("name") - parts = ["正在查询站点"] + parts = ["查询站点"] if status != "all": status_map = {"active": "已启用", "inactive": "已禁用"} diff --git a/app/agent/tools/impl/query_subscribe_history.py b/app/agent/tools/impl/query_subscribe_history.py index a01c8806..a2a54cb1 100644 --- a/app/agent/tools/impl/query_subscribe_history.py +++ b/app/agent/tools/impl/query_subscribe_history.py @@ -44,7 +44,7 @@ class QuerySubscribeHistoryTool(MoviePilotTool): name = kwargs.get("name") page = kwargs.get("page", 1) - parts = ["正在查询订阅历史"] + parts = ["查询订阅历史"] if media_type != "all": parts.append(f"类型: {media_type}") diff --git a/app/agent/tools/impl/query_subscribe_shares.py b/app/agent/tools/impl/query_subscribe_shares.py index 36c7db7e..c3fc9dfd 100644 --- a/app/agent/tools/impl/query_subscribe_shares.py +++ b/app/agent/tools/impl/query_subscribe_shares.py @@ -34,7 +34,7 @@ class QuerySubscribeSharesTool(MoviePilotTool): min_rating = kwargs.get("min_rating") max_rating = kwargs.get("max_rating") - parts = ["正在查询订阅分享"] + parts = ["查询订阅分享"] if name: parts.append(f"名称: {name}") diff --git a/app/agent/tools/impl/query_subscribes.py b/app/agent/tools/impl/query_subscribes.py index d3dc535d..bb36a68c 100644 --- a/app/agent/tools/impl/query_subscribes.py +++ b/app/agent/tools/impl/query_subscribes.py @@ -79,7 +79,7 @@ class QuerySubscribesTool(MoviePilotTool): media_type = kwargs.get("media_type", "all") page = kwargs.get("page", 1) - parts = ["正在查询订阅"] + parts = ["查询订阅"] # 根据状态过滤条件生成提示 if status != "all": diff --git a/app/agent/tools/impl/query_transfer_history.py b/app/agent/tools/impl/query_transfer_history.py index b228d01b..5201052c 100644 --- a/app/agent/tools/impl/query_transfer_history.py +++ b/app/agent/tools/impl/query_transfer_history.py @@ -33,7 +33,7 @@ class QueryTransferHistoryTool(MoviePilotTool): status = kwargs.get("status", "all") page = kwargs.get("page", 1) - parts = ["正在查询整理历史"] + parts = ["查询整理历史"] if title: parts.append(f"标题: {title}") diff --git a/app/agent/tools/impl/query_workflows.py b/app/agent/tools/impl/query_workflows.py index eb055168..51ce3370 100644 --- a/app/agent/tools/impl/query_workflows.py +++ b/app/agent/tools/impl/query_workflows.py @@ -30,7 +30,7 @@ class QueryWorkflowsTool(MoviePilotTool): name = kwargs.get("name") trigger_type = kwargs.get("trigger_type", "all") - parts = ["正在查询工作流"] + parts = ["查询工作流"] if state != "all": state_map = {"W": "等待", "R": "运行中", "P": "暂停", "S": "成功", "F": "失败"} diff --git a/app/agent/tools/impl/read_file.py b/app/agent/tools/impl/read_file.py index 2e556ddb..07714c10 100644 --- a/app/agent/tools/impl/read_file.py +++ b/app/agent/tools/impl/read_file.py @@ -29,7 +29,7 @@ class ReadFileTool(MoviePilotTool): """根据参数生成友好的提示消息""" file_path = kwargs.get("file_path", "") file_name = Path(file_path).name if file_path else "未知文件" - return f"正在读取文件: {file_name}" + return f"读取文件: {file_name}" async def run(self, file_path: str, start_line: Optional[int] = None, end_line: Optional[int] = None, **kwargs) -> str: diff --git a/app/agent/tools/impl/recognize_media.py b/app/agent/tools/impl/recognize_media.py index 132ba5c2..37e6b6e6 100644 --- a/app/agent/tools/impl/recognize_media.py +++ b/app/agent/tools/impl/recognize_media.py @@ -33,13 +33,13 @@ class RecognizeMediaTool(MoviePilotTool): path = kwargs.get("path") if path: - message = f"正在识别文件媒体信息: {path}" + message = f"识别文件媒体信息: {path}" elif title: - message = f"正在识别种子媒体信息: {title}" + message = f"识别种子媒体信息: {title}" if subtitle: message += f" ({subtitle})" else: - message = "正在识别媒体信息" + message = "识别媒体信息" return message diff --git a/app/agent/tools/impl/run_scheduler.py b/app/agent/tools/impl/run_scheduler.py index 802c2f8b..9ae18783 100644 --- a/app/agent/tools/impl/run_scheduler.py +++ b/app/agent/tools/impl/run_scheduler.py @@ -31,7 +31,7 @@ class RunSchedulerTool(MoviePilotTool): def get_tool_message(self, **kwargs) -> Optional[str]: """根据运行参数生成友好的提示消息""" job_id = kwargs.get("job_id", "") - return f"正在运行定时服务 (ID: {job_id})" + return f"运行定时服务 (ID: {job_id})" async def run(self, job_id: str, **kwargs) -> str: logger.info(f"执行工具: {self.name}, 参数: job_id={job_id}") diff --git a/app/agent/tools/impl/run_slash_command.py b/app/agent/tools/impl/run_slash_command.py index f647a60d..5fdd379d 100644 --- a/app/agent/tools/impl/run_slash_command.py +++ b/app/agent/tools/impl/run_slash_command.py @@ -45,7 +45,7 @@ class RunSlashCommandTool(MoviePilotTool): def get_tool_message(self, **kwargs) -> Optional[str]: """生成友好的提示消息""" command = kwargs.get("command", "") - return f"正在执行命令: {command}" + return f"执行命令: {command}" async def run(self, command: str, **kwargs) -> str: logger.info(f"执行工具: {self.name}, 参数: command={command}") diff --git a/app/agent/tools/impl/run_workflow.py b/app/agent/tools/impl/run_workflow.py index 9a8ed78e..2a9e216d 100644 --- a/app/agent/tools/impl/run_workflow.py +++ b/app/agent/tools/impl/run_workflow.py @@ -38,7 +38,7 @@ class RunWorkflowTool(MoviePilotTool): workflow_id = kwargs.get("workflow_id") from_begin = kwargs.get("from_begin", True) - message = f"正在执行工作流: {workflow_id}" + message = f"执行工作流: {workflow_id}" if not from_begin: message += " (从上次位置继续)" else: diff --git a/app/agent/tools/impl/scrape_metadata.py b/app/agent/tools/impl/scrape_metadata.py index e6b6526b..f1790e6c 100644 --- a/app/agent/tools/impl/scrape_metadata.py +++ b/app/agent/tools/impl/scrape_metadata.py @@ -47,7 +47,7 @@ class ScrapeMetadataTool(MoviePilotTool): storage = kwargs.get("storage", "local") overwrite = kwargs.get("overwrite", False) - message = f"正在刮削媒体元数据: {path}" + message = f"刮削媒体元数据: {path}" if storage != "local": message += f" [存储: {storage}]" if overwrite: diff --git a/app/agent/tools/impl/search_media.py b/app/agent/tools/impl/search_media.py index 78543610..dabad39a 100644 --- a/app/agent/tools/impl/search_media.py +++ b/app/agent/tools/impl/search_media.py @@ -34,7 +34,7 @@ class SearchMediaTool(MoviePilotTool): media_type = kwargs.get("media_type") season = kwargs.get("season") - message = f"正在搜索媒体: {title}" + message = f"搜索媒体: {title}" if year: message += f" ({year})" if media_type: diff --git a/app/agent/tools/impl/search_person.py b/app/agent/tools/impl/search_person.py index 09e25c03..01acbdd8 100644 --- a/app/agent/tools/impl/search_person.py +++ b/app/agent/tools/impl/search_person.py @@ -24,7 +24,7 @@ class SearchPersonTool(MoviePilotTool): def get_tool_message(self, **kwargs) -> Optional[str]: """根据搜索参数生成友好的提示消息""" name = kwargs.get("name", "") - return f"正在搜索人物: {name}" + return f"搜索人物: {name}" async def run(self, name: str, **kwargs) -> str: logger.info(f"执行工具: {self.name}, 参数: name={name}") diff --git a/app/agent/tools/impl/search_person_credits.py b/app/agent/tools/impl/search_person_credits.py index b5724f54..16d50c98 100644 --- a/app/agent/tools/impl/search_person_credits.py +++ b/app/agent/tools/impl/search_person_credits.py @@ -29,7 +29,7 @@ class SearchPersonCreditsTool(MoviePilotTool): """根据搜索参数生成友好的提示消息""" person_id = kwargs.get("person_id", "") source = kwargs.get("source", "") - return f"正在搜索人物参演作品: {source} ID {person_id}" + return f"搜索人物参演作品: {source} ID {person_id}" async def run(self, person_id: int, source: str, page: Optional[int] = 1, **kwargs) -> str: logger.info(f"执行工具: {self.name}, 参数: person_id={person_id}, source={source}, page={page}") diff --git a/app/agent/tools/impl/search_subscribe.py b/app/agent/tools/impl/search_subscribe.py index b53783c4..9ced799a 100644 --- a/app/agent/tools/impl/search_subscribe.py +++ b/app/agent/tools/impl/search_subscribe.py @@ -32,7 +32,7 @@ class SearchSubscribeTool(MoviePilotTool): subscribe_id = kwargs.get("subscribe_id") manual = kwargs.get("manual", False) - message = f"正在搜索订阅 #{subscribe_id} 的缺失剧集" + message = f"搜索订阅 #{subscribe_id} 的缺失剧集" if manual: message += "(手动搜索)" diff --git a/app/agent/tools/impl/search_torrents.py b/app/agent/tools/impl/search_torrents.py index fb71fca7..abb694db 100644 --- a/app/agent/tools/impl/search_torrents.py +++ b/app/agent/tools/impl/search_torrents.py @@ -41,11 +41,11 @@ class SearchTorrentsTool(MoviePilotTool): media_type = kwargs.get("media_type") if tmdb_id: - message = f"正在搜索种子: TMDB={tmdb_id}" + message = f"搜索种子: TMDB={tmdb_id}" elif douban_id: - message = f"正在搜索种子: 豆瓣={douban_id}" + message = f"搜索种子: 豆瓣={douban_id}" else: - message = "正在搜索种子" + message = "搜索种子" if media_type: message += f" [{media_type}]" return message diff --git a/app/agent/tools/impl/search_web.py b/app/agent/tools/impl/search_web.py index 2f01703b..9c83af00 100644 --- a/app/agent/tools/impl/search_web.py +++ b/app/agent/tools/impl/search_web.py @@ -41,7 +41,7 @@ class SearchWebTool(MoviePilotTool): """根据搜索参数生成友好的提示消息""" query = kwargs.get("query", "") max_results = kwargs.get("max_results", 20) - return f"正在搜索网络内容: {query} (最多返回 {max_results} 条结果)" + return f"搜索网络内容: {query} (最多返回 {max_results} 条结果)" async def run(self, query: str, max_results: Optional[int] = 20, **kwargs) -> str: """ diff --git a/app/agent/tools/impl/send_local_file.py b/app/agent/tools/impl/send_local_file.py index 8f34edd1..0f3828e3 100644 --- a/app/agent/tools/impl/send_local_file.py +++ b/app/agent/tools/impl/send_local_file.py @@ -55,7 +55,7 @@ class SendLocalFileTool(MoviePilotTool): def get_tool_message(self, **kwargs) -> Optional[str]: file_path = kwargs.get("file_path", "") file_name = Path(file_path).name if file_path else "未知文件" - return f"正在发送本地附件: {file_name}" + return f"发送本地附件: {file_name}" async def run( self, diff --git a/app/agent/tools/impl/send_message.py b/app/agent/tools/impl/send_message.py index e4a17d2f..3f145b22 100644 --- a/app/agent/tools/impl/send_message.py +++ b/app/agent/tools/impl/send_message.py @@ -52,12 +52,12 @@ class SendMessageTool(MoviePilotTool): message = message[:50] + "..." if title and image_url: - return f"正在发送图文消息: [{title}] {message}" + return f"发送图文消息: [{title}] {message}" if title: - return f"正在发送消息: [{title}] {message}" + return f"发送消息: [{title}] {message}" if image_url: - return f"正在发送图片消息: {message}" - return f"正在发送消息: {message}" + return f"发送图片消息: {message}" + return f"发送消息: {message}" async def run( self, diff --git a/app/agent/tools/impl/send_voice_message.py b/app/agent/tools/impl/send_voice_message.py index 6f2b199a..6f353962 100644 --- a/app/agent/tools/impl/send_voice_message.py +++ b/app/agent/tools/impl/send_voice_message.py @@ -41,7 +41,7 @@ class SendVoiceMessageTool(MoviePilotTool): message = kwargs.get("message") or "" if len(message) > 40: message = message[:40] + "..." - return f"正在发送语音回复: {message}" + return f"发送语音回复: {message}" def _supports_real_voice_reply(self) -> bool: channel = self._channel or "" diff --git a/app/agent/tools/impl/test_site.py b/app/agent/tools/impl/test_site.py index b77c0880..718c45c5 100644 --- a/app/agent/tools/impl/test_site.py +++ b/app/agent/tools/impl/test_site.py @@ -24,7 +24,7 @@ class TestSiteTool(MoviePilotTool): def get_tool_message(self, **kwargs) -> Optional[str]: """根据测试参数生成友好的提示消息""" site_identifier = kwargs.get("site_identifier") - return f"正在测试站点连通性: {site_identifier}" + return f"测试站点连通性: {site_identifier}" async def run(self, site_identifier: int, **kwargs) -> str: logger.info(f"执行工具: {self.name}, 参数: site_identifier={site_identifier}") diff --git a/app/agent/tools/impl/transfer_file.py b/app/agent/tools/impl/transfer_file.py index cc1df642..aa3fd97b 100644 --- a/app/agent/tools/impl/transfer_file.py +++ b/app/agent/tools/impl/transfer_file.py @@ -68,7 +68,7 @@ class TransferFileTool(MoviePilotTool): transfer_type = kwargs.get("transfer_type") background = kwargs.get("background", False) - message = f"正在整理文件: {file_path}" + message = f"整理文件: {file_path}" if media_type: message += f" [{media_type}]" if transfer_type: diff --git a/app/agent/tools/impl/update_custom_identifiers.py b/app/agent/tools/impl/update_custom_identifiers.py index 17a6ff1b..7aaaad9c 100644 --- a/app/agent/tools/impl/update_custom_identifiers.py +++ b/app/agent/tools/impl/update_custom_identifiers.py @@ -57,7 +57,7 @@ class UpdateCustomIdentifiersTool(MoviePilotTool): def get_tool_message(self, **kwargs) -> Optional[str]: """生成友好的提示消息""" identifiers = kwargs.get("identifiers", []) - return f"正在更新自定义识别词(共 {len(identifiers)} 条规则)" + return f"更新自定义识别词(共 {len(identifiers)} 条规则)" async def run(self, identifiers: List[str] = None, **kwargs) -> str: logger.info( diff --git a/app/agent/tools/impl/update_site.py b/app/agent/tools/impl/update_site.py index 1976a737..d29355e2 100644 --- a/app/agent/tools/impl/update_site.py +++ b/app/agent/tools/impl/update_site.py @@ -95,8 +95,8 @@ class UpdateSiteTool(MoviePilotTool): fields_updated.append("下载器") if fields_updated: - return f"正在更新站点 #{site_id}: {', '.join(fields_updated)}" - return f"正在更新站点 #{site_id}" + return f"更新站点 #{site_id}: {', '.join(fields_updated)}" + return f"更新站点 #{site_id}" async def run( self, diff --git a/app/agent/tools/impl/update_site_cookie.py b/app/agent/tools/impl/update_site_cookie.py index a9b208fa..2a0bcf44 100644 --- a/app/agent/tools/impl/update_site_cookie.py +++ b/app/agent/tools/impl/update_site_cookie.py @@ -41,7 +41,7 @@ class UpdateSiteCookieTool(MoviePilotTool): username = kwargs.get("username", "") two_step_code = kwargs.get("two_step_code") - message = f"正在更新站点Cookie: {site_identifier} (用户: {username})" + message = f"更新站点Cookie: {site_identifier} (用户: {username})" if two_step_code: message += " [需要两步验证]" diff --git a/app/agent/tools/impl/update_subscribe.py b/app/agent/tools/impl/update_subscribe.py index 43c4b39b..a60d277e 100644 --- a/app/agent/tools/impl/update_subscribe.py +++ b/app/agent/tools/impl/update_subscribe.py @@ -117,8 +117,8 @@ class UpdateSubscribeTool(MoviePilotTool): fields_updated.append("下载器") if fields_updated: - return f"正在更新订阅 #{subscribe_id}: {', '.join(fields_updated)}" - return f"正在更新订阅 #{subscribe_id}" + return f"更新订阅 #{subscribe_id}: {', '.join(fields_updated)}" + return f"更新订阅 #{subscribe_id}" async def run( self, diff --git a/app/agent/tools/impl/write_file.py b/app/agent/tools/impl/write_file.py index 565c5156..f2f77d7c 100644 --- a/app/agent/tools/impl/write_file.py +++ b/app/agent/tools/impl/write_file.py @@ -27,7 +27,7 @@ class WriteFileTool(MoviePilotTool): """根据参数生成友好的提示消息""" file_path = kwargs.get("file_path", "") file_name = Path(file_path).name if file_path else "未知文件" - return f"正在写入文件: {file_name}" + return f"写入文件: {file_name}" async def run(self, file_path: str, content: str, **kwargs) -> str: logger.info(f"执行工具: {self.name}, 参数: file_path={file_path}") diff --git a/tests/test_agent_tool_streaming.py b/tests/test_agent_tool_streaming.py new file mode 100644 index 00000000..e343a6bb --- /dev/null +++ b/tests/test_agent_tool_streaming.py @@ -0,0 +1,53 @@ +import asyncio +import unittest +from unittest.mock import patch + +from app.agent.callback import StreamingHandler +from app.agent.tools.base import MoviePilotTool +from app.core.config import settings + + +class DummyTool(MoviePilotTool): + name: str = "dummy_tool" + description: str = "Dummy tool for streaming tests." + + async def run(self, **kwargs) -> str: + return "ok" + + +class TestAgentToolStreaming(unittest.TestCase): + async def _run_tool(self, initial_buffer: str) -> tuple[str, str]: + tool = DummyTool(session_id="session-1", user_id="10001") + handler = StreamingHandler() + await handler.start_streaming() + if initial_buffer: + handler.emit(initial_buffer) + tool.set_stream_handler(handler) + + with patch.object(settings, "AI_AGENT_VERBOSE", False): + result = await tool._arun(explanation="run test tool") + + buffered_message = await handler.take() + return result, buffered_message + + def test_non_verbose_tool_call_appends_newline_separator(self): + result, buffered_message = asyncio.run(self._run_tool("prefix")) + + self.assertEqual(result, "ok") + self.assertEqual(buffered_message, "prefix\n") + + def test_non_verbose_tool_call_does_not_duplicate_newline(self): + result, buffered_message = asyncio.run(self._run_tool("prefix\n")) + + self.assertEqual(result, "ok") + self.assertEqual(buffered_message, "prefix\n") + + def test_non_verbose_tool_call_keeps_empty_buffer_unchanged(self): + result, buffered_message = asyncio.run(self._run_tool("")) + + self.assertEqual(result, "ok") + self.assertEqual(buffered_message, "") + + +if __name__ == "__main__": + unittest.main()