"""OutputParser - 结构化解析命令输出 将命令行输出解析为结构化格式,包含错误类型识别、退出码含义和可操作建议。 """ from __future__ import annotations import re from dataclasses import dataclass, field from enum import Enum from typing import Any class ErrorType(Enum): """命令输出错误类型""" NONE = "none" PERMISSION_DENIED = "permission_denied" NOT_FOUND = "not_found" TIMEOUT = "timeout" SYNTAX_ERROR = "syntax_error" CONNECTION_REFUSED = "connection_refused" OUT_OF_MEMORY = "out_of_memory" DISK_FULL = "disk_full" ALREADY_EXISTS = "already_exists" INVALID_ARGUMENT = "invalid_argument" PROCESS_NOT_FOUND = "process_not_found" NETWORK_ERROR = "network_error" UNKNOWN = "unknown" @dataclass class ParsedOutput: """结构化命令输出 Attributes: exit_code: 命令退出码 is_error: 是否为错误输出 error_type: 错误类型(仅当 is_error=True 时有值) message: 输出消息摘要 raw_output: 原始输出文本 suggestions: 可操作建议列表 """ exit_code: int is_error: bool error_type: ErrorType = ErrorType.NONE message: str = "" raw_output: str = "" suggestions: list[str] = field(default_factory=list) def to_dict(self) -> dict[str, Any]: return { "exit_code": self.exit_code, "is_error": self.is_error, "error_type": self.error_type.value, "message": self.message, "suggestions": self.suggestions, } # 错误模式匹配规则:(pattern, error_type, message_template, suggestions) _ERROR_PATTERNS: list[tuple[re.Pattern, ErrorType, str, list[str]]] = [ ( re.compile(r"permission denied|access denied|权限不足|拒绝访问", re.IGNORECASE), ErrorType.PERMISSION_DENIED, "权限不足", [ "尝试使用 sudo 执行该命令", "检查文件/目录权限: ls -la ", "确认当前用户是否有所需权限", ], ), ( re.compile( r"not found|no such file|no such directory|找不到|不存在|无法找到", re.IGNORECASE, ), ErrorType.NOT_FOUND, "文件或目录不存在", [ "检查路径拼写是否正确", "使用 ls 确认文件/目录是否存在", "检查是否在正确的工作目录下", ], ), ( re.compile(r"timed?\s*out|timeout|超时|时间超限", re.IGNORECASE), ErrorType.TIMEOUT, "命令执行超时", [ "增加超时时间", "检查网络连接是否正常", "检查目标服务是否可达", ], ), ( re.compile( r"syntax error|syntaxerror|parse error|语法错误|解析错误", re.IGNORECASE, ), ErrorType.SYNTAX_ERROR, "语法错误", [ "检查命令语法是否正确", "使用 --help 查看命令用法", "检查引号和特殊字符是否正确转义", ], ), ( re.compile( r"connection refused|连接被拒绝|无法连接|ECONNREFUSED", re.IGNORECASE, ), ErrorType.CONNECTION_REFUSED, "连接被拒绝", [ "检查目标服务是否已启动", "确认端口号是否正确", "检查防火墙设置是否阻止了连接", ], ), ( re.compile( r"out of memory|oom|cannot allocate|内存不足|内存溢出", re.IGNORECASE, ), ErrorType.OUT_OF_MEMORY, "内存不足", [ "释放不必要的内存占用", "增加系统可用内存", "检查是否有内存泄漏", ], ), ( re.compile( r"no space left|disk full|磁盘已满|空间不足|ENOSPC", re.IGNORECASE, ), ErrorType.DISK_FULL, "磁盘空间不足", [ "清理不必要的文件: du -sh * | sort -rh | head", "检查磁盘使用情况: df -h", "删除临时文件或日志", ], ), ( re.compile( r"already exists|file exists|已存在|重复|EEXIST", re.IGNORECASE, ), ErrorType.ALREADY_EXISTS, "资源已存在", [ "使用 -f 参数强制覆盖(如适用)", "先删除已有资源再重新创建", "使用不同名称创建", ], ), ( re.compile( r"invalid argument|illegal option|bad option|无效参数|非法选项|invalid option", re.IGNORECASE, ), ErrorType.INVALID_ARGUMENT, "无效参数", [ "检查命令参数是否正确", "使用 --help 查看支持的参数", "确认参数值类型和范围", ], ), ( re.compile( r"no such process|process not found|进程不存在|进程未找到", re.IGNORECASE, ), ErrorType.PROCESS_NOT_FOUND, "进程不存在", [ "确认进程 ID 是否正确", "使用 ps aux 查看运行中的进程", "进程可能已经结束", ], ), ( re.compile( r"network is unreachable|no route to host|name resolution|网络不可达|无法解析|ENETUNREACH", re.IGNORECASE, ), ErrorType.NETWORK_ERROR, "网络错误", [ "检查网络连接是否正常", "确认 DNS 解析是否正常: nslookup ", "检查代理设置", ], ), ] class OutputParser: """命令输出结构化解析器 将命令行输出(stdout + stderr)和退出码解析为结构化的 ParsedOutput, 包含错误类型识别、消息摘要和可操作建议。 """ def parse(self, output: str, exit_code: int) -> ParsedOutput: """解析命令输出 Args: output: 命令的标准输出和错误输出合并文本 exit_code: 命令退出码 Returns: ParsedOutput 结构化解析结果 """ is_error = exit_code != 0 message = self._extract_message(output) error_type = ErrorType.NONE suggestions: list[str] = [] if is_error: error_type, suggestions = self._classify_error(output, exit_code) return ParsedOutput( exit_code=exit_code, is_error=is_error, error_type=error_type, message=message, raw_output=output, suggestions=suggestions, ) def _extract_message(self, output: str) -> str: """从输出中提取关键消息 取最后几行非空输出中的关键行作为消息摘要。 """ if not output: return "" lines = [line.strip() for line in output.strip().splitlines() if line.strip()] if not lines: return "" # 取最后一行作为摘要,如果太长则截断 message = lines[-1] if len(message) > 200: message = message[:200] + "..." return message def _classify_error( self, output: str, exit_code: int ) -> tuple[ErrorType, list[str]]: """根据输出内容和退出码分类错误类型 Args: output: 命令输出 exit_code: 退出码 Returns: (error_type, suggestions) 元组 """ # 优先根据输出内容匹配 for pattern, error_type, _msg, suggestions in _ERROR_PATTERNS: if pattern.search(output): return error_type, suggestions # 退出码兜底分类 if exit_code == 126: return ErrorType.PERMISSION_DENIED, [ "检查文件是否有执行权限: chmod +x ", "确认文件格式是否正确(如行尾符)", ] if exit_code == 127: return ErrorType.NOT_FOUND, [ "检查命令是否已安装", "确认命令名称拼写是否正确", "检查 PATH 环境变量是否包含命令所在目录", ] if exit_code == 130: return ErrorType.TIMEOUT, [ "命令被 Ctrl+C 中断", "可能需要增加超时时间", ] return ErrorType.UNKNOWN, [ "检查命令输出中的错误信息", "使用 --verbose 或 --debug 获取更多详情", ]