1"""v0.80 — 用户友好错误处理：分类 + 格式化 + 建议。"""

2

3 from __future__ import annotations

4

5 import traceback

6 import sys

7 from dataclasses import dataclass, field

8 from enum import Enum, auto

9 from typing import Any, Optional

10

11

12 class ErrorCategory(Enum):

13 """错误分类枚举。"""

14 NETWORK = auto() # 网络/API 调用失败

15 AUTH = auto() # 认证/API Key 问题

16 CONFIG = auto() # 配置错误

17 RATE_LIMIT = auto() # 限流/配额

18 VALIDATION = auto() # 输入验证

19 TIMEOUT = auto() # 超时

20 RESOURCE = auto() # 资源不足（内存/磁盘）

21 MODEL = auto() # 模型相关

22 PLUGIN = auto() # 插件错误

23 INTERNAL = auto() # 内部错误

24 UNKNOWN = auto() # 未分类

25

26

27 CATEGORY_HINTS = {

28 ErrorCategory.NETWORK: "请检查网络连接或 API 端点地址。",

29 ErrorCategory.AUTH: "请确认 API Key 是否正确设置（环境变量或配置文件）。",

30 ErrorCategory.CONFIG: "请检查 agentos.yaml 配置文件，确保字段拼写正确。",

31 ErrorCategory.RATE_LIMIT: "请求频率过高，请稍后重试。可调整 RateLimitCfg.max_rps。",

32 ErrorCategory.VALIDATION: "输入参数不符合预期格式，请参考文档修正。",

33 ErrorCategory.TIMEOUT: "操作超时。可增大 LoopCfg.step_timeout 或 ModelConfig.timeout。",

34 ErrorCategory.RESOURCE: "系统资源不足，请检查内存/磁盘或降低并发。",

35 ErrorCategory.MODEL: "模型返回异常或调用失败，可尝试切换备用 Provider。",

36 ErrorCategory.PLUGIN: "插件加载失败，请检查插件路径和依赖。",

37 ErrorCategory.INTERNAL: "内部错误，请联系开发者并提供 trace_id。",

38 ErrorCategory.UNKNOWN: "未知错误，请查看详细日志。",

39}

40

41

42@dataclass

43 class ErrorContext:

44 """错误上下文信息。"""

45 trace_id: str = ""

46 category: ErrorCategory = ErrorCategory.UNKNOWN

47 message: str = ""

48 suggestion: str = ""

49 detail: str = ""

50 recovery_actions: list[str] = field(default_factory=list)

51

52

53 class HumanError(Exception):

54 """包装原始异常，附带用户友好的上下文。"""

55

56 def __init__(self, original: Exception, context: ErrorContext):

57 super().__init__(str(original))

58 self.original = original

59 self.context = context

60

61 def __str__(self) -> str:

62 return self.context.message or super().__str__()

63

64

65 class ErrorFormatter:

66 """将 Python 异常转换为用户友好的格式化输出。"""

67

68 @staticmethod

69 def categorize(exc: Exception) -> ErrorCategory:

70 """根据异常类型和消息自动分类。"""

71 msg = str(exc).lower()

72 type_name = type(exc).__name__.lower()

73

74 if any(kw in msg for kw in ["timeout", "timed out", "connect timeout"]):

75 return ErrorCategory.TIMEOUT

76 if any(kw in msg for kw in ["rate limit", "too many requests", "429"]):

77 return ErrorCategory.RATE_LIMIT

78 if any(kw in msg for kw in ["unauthorized", "forbidden", "401", "403", "api key", "invalid key"]):

79 return ErrorCategory.AUTH

80 if any(kw in msg for kw in ["connection", "network", "dns", "refused", "unreachable"]):

81 return ErrorCategory.NETWORK

82 if any(kw in msg for kw in ["validation", "invalid", "expected", "type error", "value error"]):

83 return ErrorCategory.VALIDATION

84 if any(kw in msg for kw in ["memory", "disk", "quota", "out of"]):

85 return ErrorCategory.RESOURCE

86 if any(kw in type_name for kw in ["plugin", "load"]):

87 return ErrorCategory.PLUGIN

88 if any(kw in msg for kw in ["config", "cfg", "yaml"]):

89 return ErrorCategory.CONFIG

90 if any(kw in type_name for kw in ["model", "llm", "provider"]):

91 return ErrorCategory.MODEL

92 return ErrorCategory.UNKNOWN

93

94 @staticmethod

95 def extract_recovery(original: Exception, category: ErrorCategory) -> list[str]:

96 """根据异常给出可操作的恢复建议。"""

97 actions = [CATEGORY_HINTS.get(category, "")]

98 msg = str(original)

99

100 if ErrorFormatter._has_retry(category):

101 actions.append("框架已自动重试，若持续失败请检查上游服务状态。")

102 if "api key" in msg.lower() or "key" in msg.lower():

103 actions.append("运行 `agentos config set api_key <your-key>` 或设置环境变量。")

104 if "model" in msg.lower() and "not found" in msg.lower():

105 actions.append("请确认 ModelConfig.model_name 拼写正确，或使用 RECOMMENDED_CONFIG。")

106 return [a for a in actions if a]

107

108 @staticmethod

109 def _has_retry(category: ErrorCategory) -> bool:

110 return category in (ErrorCategory.NETWORK, ErrorCategory.TIMEOUT, ErrorCategory.RATE_LIMIT)

111

112 @classmethod

113 def format(cls, exc: Exception, trace_id: str = "") -> ErrorContext:

114 """将异常格式化为 ErrorContext。"""

115 category = cls.categorize(exc)

116 return ErrorContext(

117 trace_id=trace_id,

118 category=category,

119 message=cls._friendly_message(exc, category),

120 suggestion=CATEGORY_HINTS.get(category, ""),

121 detail=cls._extract_key_detail(exc),

122 recovery_actions=cls.extract_recovery(exc, category),

123 )

124

125 @staticmethod

126 def _friendly_message(exc: Exception, category: ErrorCategory) -> str:

127 type_msg = str(exc)

128 prefix = {

129 ErrorCategory.NETWORK: "网络连接失败",

130 ErrorCategory.AUTH: "认证失败",

131 ErrorCategory.CONFIG: "配置错误",

132 ErrorCategory.RATE_LIMIT: "请求被限流",

133 ErrorCategory.VALIDATION: "输入校验失败",

134 ErrorCategory.TIMEOUT: "操作超时",

135 ErrorCategory.RESOURCE: "资源不足",

136 ErrorCategory.MODEL: "模型调用异常",

137 ErrorCategory.PLUGIN: "插件错误",

138 ErrorCategory.INTERNAL: "内部错误",

139 ErrorCategory.UNKNOWN: "发生错误",

140 }.get(category, "错误")

141 return f"{prefix}: {type_msg[:120]}"

142

143 @staticmethod

144 def _extract_key_detail(exc: Exception) -> str:

145 lines = traceback.format_exception_only(type(exc), exc)

146 return "".join(lines[-2:]).strip()

147

148

149 def format_error(exc: Exception, trace_id: str = "") -> str:

150 """一行调用：输出用户友好的错误信息。"""

151 ctx = ErrorFormatter.format(exc, trace_id)

152 parts = [f"[{ctx.category.name}] {ctx.message}"]

153 if ctx.suggestion:

154 parts.append(f" 建议: {ctx.suggestion}")

155 for action in ctx.recovery_actions:

156 parts.append(f" -> {action}")

157 return "\n".join(parts)

158

159

160 def friendly_error(func):

161 """装饰器：自动捕获异常并输出友好信息。"""

162 def wrapper(*args, **kwargs):

163 try:

164 return func(*args, **kwargs)

165 except Exception as e:

166 friendly_msg = format_error(e)

167 print(friendly_msg, file=sys.stderr)

168 raise

169 return wrapper