295 lines
8.6 KiB
Python
295 lines
8.6 KiB
Python
"""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 <path>",
|
||
"确认当前用户是否有所需权限",
|
||
],
|
||
),
|
||
(
|
||
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 <domain>",
|
||
"检查代理设置",
|
||
],
|
||
),
|
||
]
|
||
|
||
|
||
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 <file>",
|
||
"确认文件格式是否正确(如行尾符)",
|
||
]
|
||
if exit_code == 127:
|
||
return ErrorType.NOT_FOUND, [
|
||
"检查命令是否已安装",
|
||
"确认命令名称拼写是否正确",
|
||
"检查 PATH 环境变量是否包含命令所在目录",
|
||
]
|
||
if exit_code == 130:
|
||
return ErrorType.TIMEOUT, [
|
||
"命令被 Ctrl+C 中断",
|
||
"可能需要增加超时时间",
|
||
]
|
||
|
||
return ErrorType.UNKNOWN, [
|
||
"检查命令输出中的错误信息",
|
||
"使用 --verbose 或 --debug 获取更多详情",
|
||
]
|