diff --git a/backend/internal/application/processing/service.go b/backend/internal/application/processing/service.go index ae957d587..26c0c1431 100644 --- a/backend/internal/application/processing/service.go +++ b/backend/internal/application/processing/service.go @@ -681,7 +681,7 @@ func fileProcessingStatusFromFileObject(fileObj *domainconversation.FileObject) CompletedAt: fileObj.ProcessingCompletedAt, UpdatedAt: fileObj.UpdatedAt, } - dto.ErrorMessage = HumanizeFileProcessingError(dto.FileCategory, dto.ErrorCode, dto.ErrorMessage) + dto.ErrorMessage = ResolveFileProcessingErrorMessage(dto.FileCategory, dto.ProcessingStatus, dto.ErrorCode, dto.ErrorMessage) return dto } @@ -1438,6 +1438,15 @@ func processingErrorSummary(err error) string { return HumanizeFileProcessingError("", code, "") } +// ResolveFileProcessingErrorMessage formats response errors without inventing a failure +// for files that have no processing error code. Legacy failed rows still get a safe fallback. +func ResolveFileProcessingErrorMessage(fileCategory, processingStatus, code, message string) string { + if !strings.EqualFold(strings.TrimSpace(processingStatus), "failed") && strings.TrimSpace(code) == "" { + return "" + } + return HumanizeFileProcessingError(fileCategory, code, message) +} + func HumanizeFileProcessingError(fileCategory string, code string, _ string) string { normalizedCode := strings.ToLower(strings.TrimSpace(code)) if normalizedCode == "" { diff --git a/backend/internal/application/processing/service_error_message_test.go b/backend/internal/application/processing/service_error_message_test.go new file mode 100644 index 000000000..568933d95 --- /dev/null +++ b/backend/internal/application/processing/service_error_message_test.go @@ -0,0 +1,67 @@ +package processing + +import ( + "testing" + + model "github.com/DEEIX-AI/DEEIX-Chat/backend/internal/domain/conversation" +) + +func TestFileProcessingStatusDoesNotInventFailureMessages(t *testing.T) { + for _, category := range []string{"pdf", "document", "image"} { + for _, status := range []string{"uploaded", "queued", "extracting", "embedding", "ready"} { + t.Run(category+"/"+status, func(t *testing.T) { + file := &model.FileObject{ + FileCategory: category, ProcessingStatus: status, + ProcessingReady: status == "ready", ExtractStatus: "ready", EmbedStatus: "none", + ExtractEngine: "mineru", ExtractChars: 638, ExtractPages: 1, + } + got := fileProcessingStatusFromFileObject(file) + if got.ErrorCode != "" || got.ErrorMessage != "" { + t.Fatalf("file without an error acquired a failure: code=%q message=%q", got.ErrorCode, got.ErrorMessage) + } + }) + } + } +} + +func TestFileProcessingStatusEmptyAndEmbeddingFailureStaySeparate(t *testing.T) { + for _, file := range []model.FileObject{ + {FileCategory: "pdf", ProcessingStatus: "ready", ExtractStatus: "empty", EmbedStatus: "empty"}, + {FileCategory: "pdf", ProcessingStatus: "ready", ProcessingReady: true, ExtractStatus: "ready", EmbedStatus: "failed", EmbedError: "embedding unavailable"}, + {FileCategory: "pdf", ProcessingStatus: "ready", ProcessingReady: true, ProcessingErrorCode: " \t", ProcessingErrorMessage: " \n"}, + } { + got := fileProcessingStatusFromFileObject(&file) + if got.ErrorMessage != "" { + t.Fatalf("non-processing failure acquired a processing error: %q", got.ErrorMessage) + } + if got.EmbedError != file.EmbedError { + t.Fatalf("embedding error changed: %q", got.EmbedError) + } + } +} + +func TestFileProcessingStatusKeepsSafeFailureMessages(t *testing.T) { + for _, tc := range []struct { + name, status, code, message, want string + }{ + {"coded failure", "failed", "extract_timeout", "", "文件提取超时,请稍后重试,或缩小文件后重试。"}, + {"legacy failure", "failed", "", "", "PDF 未提取到可读文本。该文件可能是扫描件、图片型 PDF、加密 PDF,或文档内容本身不可复制。"}, + {"stale message without code", "ready", "", "private upstream diagnostic", ""}, + {"padded failed status", " FAILED ", "", "", "PDF 未提取到可读文本。该文件可能是扫描件、图片型 PDF、加密 PDF,或文档内容本身不可复制。"}, + {"unknown code", "failed", "unknown_error", "private upstream diagnostic", "文件处理失败,请稍后重试。"}, + {"code without failed status", "ready", "extract_timeout", "", "文件提取超时,请稍后重试,或缩小文件后重试。"}, + } { + t.Run(tc.name, func(t *testing.T) { + got := fileProcessingStatusFromFileObject(&model.FileObject{ + FileCategory: "pdf", ProcessingStatus: tc.status, + ProcessingErrorCode: tc.code, ProcessingErrorMessage: tc.message, + }) + if got.ErrorMessage != tc.want { + t.Fatalf("message = %q, want %q", got.ErrorMessage, tc.want) + } + if got.ErrorCode != tc.code { + t.Fatalf("error code changed: %q", got.ErrorCode) + } + }) + } +} diff --git a/backend/internal/transport/http/conversation/dto_response.go b/backend/internal/transport/http/conversation/dto_response.go index 2ebc3a5f4..1f50d5087 100644 --- a/backend/internal/transport/http/conversation/dto_response.go +++ b/backend/internal/transport/http/conversation/dto_response.go @@ -485,7 +485,7 @@ func toFileObjectResponse(item *model.FileObject, capability appembedding.FileVe ProcessingStatus: item.ProcessingStatus, ProcessingReady: item.ProcessingReady, ProcessingErrorCode: item.ProcessingErrorCode, - ProcessingErrorMessage: appprocessing.HumanizeFileProcessingError(item.FileCategory, item.ProcessingErrorCode, item.ProcessingErrorMessage), + ProcessingErrorMessage: appprocessing.ResolveFileProcessingErrorMessage(item.FileCategory, item.ProcessingStatus, item.ProcessingErrorCode, item.ProcessingErrorMessage), ExtractStatus: item.ExtractStatus, EmbedStatus: item.EmbedStatus, EmbedError: item.EmbedError, @@ -1478,7 +1478,7 @@ func toFileProcessingStatusResponse(d *appprocessing.FileProcessingStatusDTO) Fi RAGReady: d.RAGReady, RAGReason: d.RAGReason, ErrorCode: d.ErrorCode, - ErrorMessage: appprocessing.HumanizeFileProcessingError(d.FileCategory, d.ErrorCode, d.ErrorMessage), + ErrorMessage: d.ErrorMessage, ExtractChars: d.ExtractChars, ExtractPages: d.ExtractPages, ChunkCount: d.ChunkCount, diff --git a/backend/internal/transport/http/conversation/dto_response_file_error_test.go b/backend/internal/transport/http/conversation/dto_response_file_error_test.go new file mode 100644 index 000000000..cf7722c5a --- /dev/null +++ b/backend/internal/transport/http/conversation/dto_response_file_error_test.go @@ -0,0 +1,49 @@ +package conversation + +import ( + "testing" + + appembedding "github.com/DEEIX-AI/DEEIX-Chat/backend/internal/application/embedding" + appprocessing "github.com/DEEIX-AI/DEEIX-Chat/backend/internal/application/processing" + model "github.com/DEEIX-AI/DEEIX-Chat/backend/internal/domain/conversation" +) + +func TestFileResponsesDoNotInventFailureMessages(t *testing.T) { + for _, tc := range []struct { + name, status, extract, embed, code, message, want string + }{ + {"successful MinerU PDF", "ready", "ready", "none", "", "", ""}, + {"queued PDF", "queued", "none", "none", "", "", ""}, + {"extracting PDF", "extracting", "none", "none", "", "", ""}, + {"empty PDF", "ready", "empty", "empty", "", "", ""}, + {"stalled embedding marked failed", "ready", "ready", "failed", "embed_failed", "embedding stalled", "文件已完成文本提取,但向量化失败。"}, + {"whitespace error fields", "ready", "ready", "none", " \t", " \n", ""}, + {"stale message without code", "ready", "ready", "none", "", "old diagnostic", ""}, + {"actual failure", "failed", "failed", "none", "extract_timeout", "", "文件提取超时,请稍后重试,或缩小文件后重试。"}, + {"legacy failure without fields", "failed", "failed", "none", "", "", "PDF 未提取到可读文本。该文件可能是扫描件、图片型 PDF、加密 PDF,或文档内容本身不可复制。"}, + } { + t.Run(tc.name, func(t *testing.T) { + file := &model.FileObject{ + FileID: "synthetic-pdf", FileCategory: "pdf", MimeType: "application/pdf", + ProcessingStatus: tc.status, ProcessingReady: tc.extract == "ready", + ExtractStatus: tc.extract, ExtractEngine: "mineru", ExtractChars: 638, ExtractPages: 1, + EmbedStatus: tc.embed, ProcessingErrorCode: tc.code, ProcessingErrorMessage: tc.message, + } + object := toFileObjectResponse(file, appembedding.FileVectorizationCapability{}) + if object.ProcessingErrorMessage != tc.want { + t.Errorf("file response message = %q, want %q", object.ProcessingErrorMessage, tc.want) + } + status := toFileProcessingStatusResponse(&appprocessing.FileProcessingStatusDTO{ + FileID: file.FileID, FileCategory: file.FileCategory, ProcessingStatus: tc.status, + ProcessingReady: file.ProcessingReady, ExtractStatus: tc.extract, EmbedStatus: tc.embed, + ErrorCode: tc.code, ErrorMessage: object.ProcessingErrorMessage, ExtractChars: 638, ExtractPages: 1, + }) + if status.ErrorMessage != tc.want { + t.Errorf("processing response message = %q, want %q", status.ErrorMessage, tc.want) + } + if object.ProcessingStatus != tc.status || status.ProcessingStatus != tc.status { + t.Fatal("mapping changed processing status") + } + }) + } +}