From 2438091f9b1ec85838fbb6942daf3644e29e5b83 Mon Sep 17 00:00:00 2001 From: Hongyu Shi Date: Wed, 6 Aug 2025 16:12:41 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat(tui):=20=E5=A2=9E=E5=8A=A0=E5=91=BD?= =?UTF-8?q?=E4=BB=A4=E5=A4=84=E7=90=86=E8=B6=85=E6=97=B6=E5=92=8C=E5=8F=96?= =?UTF-8?q?=E6=B6=88=E9=94=99=E8=AF=AF=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hongyu Shi --- src/app/tui.py | 74 ++++++++++++++++++++++++++++---------------------- 1 file changed, 42 insertions(+), 32 deletions(-) diff --git a/src/app/tui.py b/src/app/tui.py index 4b42e48c..594a721e 100644 --- a/src/app/tui.py +++ b/src/app/tui.py @@ -374,40 +374,50 @@ class IntelligentTerminal(App): is_first_content = True # 标记是否是第一段内容 received_any_content = False # 标记是否收到任何内容 start_time = asyncio.get_event_loop().time() - timeout_seconds = 30.0 # 30秒超时 - - # 通过 process_command 获取命令处理结果和输出类型 - async for output_tuple in process_command(user_input, self._get_llm_client()): - content, is_llm_output = output_tuple # 解包输出内容和类型标志 - received_any_content = True - - # 检查超时 - if asyncio.get_event_loop().time() - start_time > timeout_seconds: - output_container.mount(OutputLine("请求超时,已停止处理", command=False)) - break - - # 处理内容 - params = ContentChunkParams( - content=content, - is_llm_output=is_llm_output, - current_content=current_content, - is_first_content=is_first_content, - ) - current_line = await self._process_content_chunk( - params, - current_line, - output_container, - ) + timeout_seconds = 60.0 # 60秒超时 - # 更新状态 - if is_first_content: - is_first_content = False - current_content = content - elif isinstance(current_line, MarkdownOutputLine) and is_llm_output: - current_content += content + try: + # 通过 process_command 获取命令处理结果和输出类型 + async for output_tuple in process_command(user_input, self._get_llm_client()): + content, is_llm_output = output_tuple # 解包输出内容和类型标志 + received_any_content = True + + # 检查超时 + if asyncio.get_event_loop().time() - start_time > timeout_seconds: + output_container.mount(OutputLine("请求超时,已停止处理", command=False)) + break + + # 处理内容 + params = ContentChunkParams( + content=content, + is_llm_output=is_llm_output, + current_content=current_content, + is_first_content=is_first_content, + ) + current_line = await self._process_content_chunk( + params, + current_line, + output_container, + ) - # 滚动到底部 - await self._scroll_to_end() + # 更新状态 + if is_first_content: + is_first_content = False + current_content = content + elif isinstance(current_line, MarkdownOutputLine) and is_llm_output: + current_content += content + + # 滚动到底部 + await self._scroll_to_end() + + except asyncio.TimeoutError: + self.logger.warning("Command stream timed out") + if hasattr(self, "is_running") and self.is_running: + output_container.mount(OutputLine("请求超时,请稍后重试", command=False)) + except asyncio.CancelledError: + self.logger.info("Command stream was cancelled") + if received_any_content and hasattr(self, "is_running") and self.is_running: + output_container.mount(OutputLine("[处理被中断]", command=False)) return received_any_content -- Gitee From cf18c1b5801c675519bbf63919bf5ee9ef5ddf72 Mon Sep 17 00:00:00 2001 From: Hongyu Shi Date: Wed, 6 Aug 2025 16:15:48 +0800 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E6=B5=81?= =?UTF-8?q?=E5=BC=8F=E5=93=8D=E5=BA=94=E5=A4=84=E7=90=86=EF=BC=8C=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E7=89=B9=E6=AE=8A=E4=BA=8B=E4=BB=B6=E5=A4=84=E7=90=86?= =?UTF-8?q?=E5=92=8C=E9=94=99=E8=AF=AF=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hongyu Shi --- src/backend/hermes/client.py | 138 ++++++++++++++++++++++------------- 1 file changed, 89 insertions(+), 49 deletions(-) diff --git a/src/backend/hermes/client.py b/src/backend/hermes/client.py index 72388f55..e6bba282 100644 --- a/src/backend/hermes/client.py +++ b/src/backend/hermes/client.py @@ -214,7 +214,7 @@ class HermesChatClient(LLMClientBase): conversation_id = await self._ensure_conversation() # 创建聊天请求 - app = HermesApp("default-app") + app = HermesApp("") request = HermesChatRequest( app=app, conversation_id=conversation_id, @@ -378,7 +378,13 @@ class HermesChatClient(LLMClientBase): if self.auth_token: headers["Authorization"] = f"Bearer {self.auth_token}" - self.client = httpx.AsyncClient(headers=headers, timeout=30.0) + timeout = httpx.Timeout( + connect=30.0, # 连接超时,允许30秒建立连接 + read=1800.0, # 读取超时,支持长时间SSE流(30分钟) + write=30.0, # 写入超时 + pool=30.0, # 连接池超时 + ) + self.client = httpx.AsyncClient(headers=headers, timeout=timeout) return self.client async def _create_conversation(self, llm_id: str = "") -> str: @@ -575,54 +581,88 @@ class HermesChatClient(LLMClientBase): has_content = False event_count = 0 - async for line in response.aiter_lines(): - stripped_line = line.strip() - if not stripped_line: - continue - - self.logger.debug("收到 SSE 行: %s", stripped_line) - event = HermesStreamEvent.from_line(stripped_line) - if event is None: - self.logger.debug("无法解析 SSE 事件") - continue - - event_count += 1 - self.logger.debug("解析到事件 #%d - 类型: %s", event_count, event.event_type) - - # 处理完成事件 - if event.event_type == "done": - self.logger.debug("收到完成事件,结束流式响应") - break - - # 处理错误事件 - if event.event_type == "error": - self.logger.error("收到后端错误事件: %s", event.data.get("error", "Unknown error")) - yield "抱歉,后端服务出现错误,请稍后重试。" - break - - # 处理敏感内容事件 - if event.event_type == "sensitive": - self.logger.warning("收到敏感内容事件: %s", event.data.get("message", "Sensitive content detected")) - yield "抱歉,响应内容包含敏感信息,已被系统屏蔽。" - break - - # 获取文本内容 - text_content = event.get_text_content() - if text_content: - has_content = True - self._log_text_content(text_content) - yield text_content - else: - self.logger.debug("事件无文本内容") - - # 检查是否产生了任何内容 + self.logger.info("开始处理流式响应事件") + + try: + async for line in response.aiter_lines(): + stripped_line = line.strip() + if not stripped_line: + continue + + self.logger.debug("收到 SSE 行: %s", stripped_line) + event = HermesStreamEvent.from_line(stripped_line) + if event is None: + self.logger.warning("无法解析 SSE 事件") + continue + + event_count += 1 + self.logger.info("解析到事件 #%d - 类型: %s", event_count, event.event_type) + + # 处理特殊事件类型 + should_break, break_message = self._handle_special_events(event) + if should_break: + if break_message: + yield break_message + break + + # 处理文本内容 + text_content = event.get_text_content() + if text_content: + has_content = True + self._log_text_content(text_content) + yield text_content + else: + self.logger.info("事件无文本内容") + + self.logger.info("流式响应处理完成 - 事件数量: %d, 有内容: %s", event_count, has_content) + + except Exception: + self.logger.exception("处理流式响应事件时出错") + raise + + # 处理无内容的情况 if not has_content: - self.logger.warning( - "流式响应完成但未产生任何文本内容 - 事件总数: %d", - event_count, - ) - # 如果没有产生任何内容,yield 一个错误信息 - yield "抱歉,服务暂时无法响应您的请求,请稍后重试。" + yield self._get_no_content_message(event_count) + + def _handle_special_events(self, event: HermesStreamEvent) -> tuple[bool, str | None]: + """处理特殊事件类型,返回(是否中断, 中断消息)""" + if event.event_type == "done": + self.logger.debug("收到完成事件,结束流式响应") + return True, None + + if event.event_type == "error": + self.logger.error("收到后端错误事件: %s", event.data.get("error", "Unknown error")) + return True, "抱歉,后端服务出现错误,请稍后重试。" + + if event.event_type == "sensitive": + self.logger.warning("收到敏感内容事件: %s", event.data.get("message", "Sensitive content detected")) + return True, "抱歉,响应内容包含敏感信息,已被系统屏蔽。" + + return False, None + + async def _handle_stream_error(self, error: Exception, *, has_content: bool) -> AsyncGenerator[str, None]: + """处理流式响应网络错误""" + self.logger.exception("处理流式响应时出现网络错误: %s", error) + if has_content: + yield "\n\n[连接中断,但已获得部分响应]" + else: + raise HermesAPIError(500, f"Network error during streaming: {error!s}") from error + + async def _handle_unexpected_error(self, error: Exception, *, has_content: bool) -> AsyncGenerator[str, None]: + """处理流式响应未知错误""" + self.logger.exception("处理流式响应时出现未知错误: %s", error) + if has_content: + yield "\n\n[处理响应时出现错误,但已获得部分内容]" + else: + raise HermesAPIError(500, f"Unexpected error during streaming: {error!s}") from error + + def _get_no_content_message(self, event_count: int) -> str: + """获取无内容时的消息""" + self.logger.warning( + "流式响应完成但未产生任何文本内容 - 事件总数: %d", + event_count, + ) + return "抱歉,服务暂时无法响应您的请求,请稍后重试。" async def _chat_stream( self, -- Gitee From 06ff302b60f238c96e4671f173bd3a2d208216bb Mon Sep 17 00:00:00 2001 From: Hongyu Shi Date: Wed, 6 Aug 2025 16:16:09 +0800 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=E5=91=BD?= =?UTF-8?q?=E4=BB=A4=E5=A4=84=E7=90=86=E5=99=A8=E7=9A=84=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=EF=BC=8C=E6=B7=BB=E5=8A=A0=E5=91=BD=E4=BB=A4?= =?UTF-8?q?=E5=AE=89=E5=85=A8=E6=A3=80=E6=9F=A5=E5=92=8C=E6=89=A7=E8=A1=8C?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E7=9A=84=E8=AF=A6=E7=BB=86=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hongyu Shi --- src/backend/hermes/client.py | 15 +++++++++------ src/tool/command_processor.py | 10 ++++++++++ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/backend/hermes/client.py b/src/backend/hermes/client.py index e6bba282..4ed4ad32 100644 --- a/src/backend/hermes/client.py +++ b/src/backend/hermes/client.py @@ -207,11 +207,13 @@ class HermesChatClient(LLMClientBase): await self._stop() self.logger.info("开始 Hermes 流式聊天请求") + self.logger.debug("提示内容长度: %d", len(prompt)) start_time = time.time() try: # 确保有会话 ID conversation_id = await self._ensure_conversation() + self.logger.info("使用会话ID: %s", conversation_id) # 创建聊天请求 app = HermesApp("") @@ -685,6 +687,10 @@ class HermesChatClient(LLMClientBase): chat_url = urljoin(self.base_url, "/api/chat") headers = self._build_chat_headers() + self.logger.info("准备发送聊天请求 - URL: %s, 会话ID: %s", chat_url, request.conversation_id) + self.logger.debug("请求头: %s", headers) + self.logger.debug("请求内容: %s", request.to_dict()) + try: async with client.stream( "POST", @@ -692,6 +698,7 @@ class HermesChatClient(LLMClientBase): json=request.to_dict(), headers=headers, ) as response: + self.logger.info("收到聊天响应 - 状态码: %d", response.status_code) await self._validate_chat_response(response) async for text in self._process_stream_events(response): yield text @@ -703,12 +710,8 @@ class HermesChatClient(LLMClientBase): def _log_text_content(self, text_content: str) -> None: """记录文本内容到日志""" - max_log_length = 50 - display_text = ( - text_content[:max_log_length] + "..." - if len(text_content) > max_log_length - else text_content - ) + max_log_length = 100 + display_text = text_content[:max_log_length] + "..." if len(text_content) > max_log_length else text_content self.logger.debug("产生文本内容: %s", display_text) async def _get_conversation_list(self) -> list[str]: diff --git a/src/tool/command_processor.py b/src/tool/command_processor.py index 6efbd185..ed9ab577 100644 --- a/src/tool/command_processor.py +++ b/src/tool/command_processor.py @@ -5,6 +5,7 @@ import subprocess from collections.abc import AsyncGenerator from backend.base import LLMClientBase +from log.manager import get_logger # 定义危险命令黑名单 BLACKLIST = ["rm", "sudo", "shutdown", "reboot", "mkfs"] @@ -55,6 +56,9 @@ async def process_command(command: str, llm_client: LLMClientBase) -> AsyncGener - content: 输出内容 - is_llm_output: 是否是LLM输出(True表示LLM输出,应使用富文本;False表示命令输出,应使用纯文本) """ + logger = get_logger(__name__) + logger.debug("开始处理命令: %s", command) + tokens = command.split() if not tokens: yield ("请输入有效命令或问题。", True) # 作为LLM输出处理 @@ -62,20 +66,26 @@ async def process_command(command: str, llm_client: LLMClientBase) -> AsyncGener prog = tokens[0] if shutil.which(prog) is not None: + logger.info("检测到系统命令: %s", prog) # 检查命令安全性 if not is_command_safe(command): + logger.warning("命令被安全检查阻止: %s", command) yield ("检测到不安全命令,已阻止执行。", True) # 作为LLM输出处理 return + logger.info("执行系统命令: %s", command) success, output = execute_command(command) if success: + logger.debug("命令执行成功,输出长度: %d", len(output)) yield (output, False) # 系统命令输出,使用纯文本 else: # 执行失败,将错误信息反馈给大模型 + logger.info("命令执行失败,向 LLM 请求建议") query = f"命令 '{command}' 执行失败,错误信息如下:\n{output}\n请帮忙分析原因并提供解决建议。" async for suggestion in llm_client.get_llm_response(query): yield (suggestion, True) # LLM输出,使用富文本 else: # 不是已安装的命令,直接询问大模型 + logger.debug("向 LLM 发送问题: %s", command) async for suggestion in llm_client.get_llm_response(command): yield (suggestion, True) # LLM输出,使用富文本 -- Gitee