package stream import ( "errors" "io" "net" "strings" "unicode/utf8" ) // ClassifyError maps a raw error into structured ErrorInfo using heuristic rules. func ClassifyError(err error, phaseID, requestID, sessionID, workspaceID string) ErrorInfo { if err == nil { err = errors.New("unknown error") } msg := strings.ToLower(err.Error()) upstream := truncateRunes(err.Error(), 80) info := ErrorInfo{ PhaseID: phaseID, RequestID: requestID, SessionID: sessionID, WorkspaceID: workspaceID, Upstream: upstream, } switch { case isTimeoutError(msg): info.Code = "CHAT_TIMEOUT" info.Class = ErrorClassTransient info.UserMessage = "生成回复超时,请稍后重试" info.Actions = []string{"retry"} case isNetworkError(err, msg): info.Code = "NETWORK_ERROR" info.Class = ErrorClassTransient info.UserMessage = "网络连接异常,请稍后重试" info.Actions = []string{"retry"} case workspaceID != "": info.Code = "TASK_INTERRUPTED" info.Class = ErrorClassResumable info.UserMessage = "任务执行中断,可继续或重新开始" info.Actions = []string{"continue", "restart"} default: info.Code = "INTERNAL_ERROR" info.Class = ErrorClassFatal info.UserMessage = "系统内部错误,请重新开始会话" info.Actions = []string{"restart"} } return info } func isTimeoutError(msg string) bool { return strings.Contains(msg, "timeout") || strings.Contains(msg, "deadline") || strings.Contains(msg, "timed out") } func isNetworkError(err error, msg string) bool { var netErr net.Error if errors.As(err, &netErr) { if netErr.Timeout() || netErr.Temporary() { return true } } var opErr *net.OpError if errors.As(err, &opErr) { if opErr.Op == "dial" { return true } } networkHints := []string{ "connection refused", "eof", "broken pipe", } for _, hint := range networkHints { if strings.Contains(msg, hint) { return true } } if errors.Is(err, io.EOF) { return true } return false } func truncateRunes(s string, maxRunes int) string { if maxRunes <= 0 || s == "" { return s } if utf8.RuneCountInString(s) <= maxRunes { return s } runes := []rune(s) return string(runes[:maxRunes]) }