package stream import ( "chat-agent-system/internal/errcode" "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) } if def, ok := errcode.FromError(err); ok { info.Code = def.ID info.Class = ErrorClass(def.Class) if info.Class == "" { info.Class = ErrorClassFatal } info.UserMessage = def.Message info.Guide = def.Guide if len(def.Actions) == 0 { info.Actions = []string{} } else { info.Actions = append([]string(nil), def.Actions...) } info.Message = info.UserMessage return info } 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 isDocumentLedgerError(msg): info.Code = "DOC_LEDGER_ERROR" info.Class = ErrorClassFatal info.UserMessage = "文档职责账本写入失败,请重新开始会话;若反复出现请联系管理员" info.Actions = []string{"restart"} 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 strings.Contains(msg, "runner data root mismatch"): info.Code = "RUNNER_MISMATCH" info.Class = ErrorClassFatal info.UserMessage = "模型执行环境与数据目录不一致,请联系管理员检查部署配置" info.Actions = []string{} case isRunnerContractError(msg): // Permanent runner wire/contract failures must not look "resumable". info.Code = "RUNNER_RESULT_REJECTED" info.Class = ErrorClassFatal info.UserMessage = "助手执行结果校验失败,请重新开始任务" info.Actions = []string{"restart"} 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 isDocumentLedgerError(msg string) bool { return strings.Contains(msg, "persist document ledger") || strings.Contains(msg, "sqlstate 23503") || strings.Contains(msg, "foreign key constraint") || strings.Contains(msg, "document_records_reviewer_id_fkey") || strings.Contains(msg, "document_records_author_id_fkey") || strings.Contains(msg, "document_records_approver_id_fkey") } func isRunnerContractError(msg string) bool { return strings.Contains(msg, "typed result rejected") || strings.Contains(msg, "runner result body too large") } 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]) }