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()) info := ErrorInfo{ PhaseID: phaseID, RequestID: requestID, SessionID: sessionID, WorkspaceID: workspaceID, } raw := err.Error() if !containsInternalLeak(raw) { info.Upstream = truncateRunes(raw, 80) } switch { case strings.Contains(msg, "编辑中") || strings.Contains(msg, "禁止并行修改") || strings.Contains(msg, "edit lease"): info.Code = "DOC_BUSY" info.Class = ErrorClassFatal info.UserMessage = "该文档正在由项目助手或其他用户编辑中;当前请求未执行,请等待现有任务完成" info.Actions = []string{} 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"} } info.Message = info.UserMessage return info } func containsInternalLeak(s string) bool { low := strings.ToLower(s) for _, n := range []string{"sql", "pq:", "token", "password", "/home/", "prompt"} { if strings.Contains(low, n) || strings.Contains(s, n) { return true } } return false } 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]) }