diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java index 0fb83984..2fe565ff 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.extern.slf4j.Slf4j; import org.dromara.aihr.domain.AihrSopDto.CategoryStatResponse; import org.dromara.aihr.domain.AihrSopDto.DocResponse; import org.dromara.aihr.domain.AihrSopDto.LocalImportItemResponse; @@ -75,6 +76,7 @@ import java.util.stream.Stream; /** * ponytail: deterministic RAG seed; swap to vector search when real SOP corpus exists. */ +@Slf4j @Service public class AihrSopSeedService { @@ -466,6 +468,10 @@ public class AihrSopSeedService { try { content = contentReader.get(); if (content.isBlank()) { + if (imageFile(fileName)) { + // 作战清单 M7:无视觉模型或 OCR 无结果的图片先落库标「待处理」,不算失败 + return savePendingImage(fileName, category, ossUploader); + } throw new ServiceException("文件内容不能为空"); } } catch (RuntimeException e) { @@ -522,6 +528,22 @@ public class AihrSopSeedService { return new UploadResponse(docId, ossId, fileName, knowledgeName, fragments.size(), insight.summary(), insight.tags(), snippets); } + /** + * OCR 拿不到文字的图片:只落 OSS + attach 标「等待解析」(status 0),0 片段返回; + * 配置视觉模型后重新上传同名文件即可替换加工。 + */ + private UploadResponse savePendingImage(String fileName, String category, Supplier ossUploader) { + String knowledgeName = isAutoCategory(category) || category == null || category.isBlank() ? "图片素材" : cleanCategory(category); + KnowledgeConfig config = knowledgeConfig(knowledgeName); + SysOssVo oss = ossUploader.get(); + String docId = upsertAttach(config.knowledgeId(), oss.getOssId(), fileName); + markAttachStatus(config.knowledgeId(), docId, 0, "pending-ocr"); + String hint = visionRuntime().isPresent() + ? "图片 OCR 未提取到文字(当前模型可能不支持图片输入,或图片中无可识别文字),已入库标记待处理" + : "未配置可用视觉模型,图片已入库标记待处理;在模型管理启用 vision/多模态 chat 模型后重新上传即可解析"; + return new UploadResponse(docId, oss.getOssId(), fileName, knowledgeName, 0, hint, List.of("图片", "待OCR"), List.of()); + } + private DocumentInsight documentInsight(String fileName, String content, String requestedCategory) { boolean autoCategory = isAutoCategory(requestedCategory); String manualCategory = autoCategory ? "" : cleanCategory(requestedCategory); @@ -1967,6 +1989,7 @@ public class AihrSopSeedService { String text = callVisionOcr(runtime.get(), bytes, guessMimeType(fileName)); return normalizeExtractedText(text); } catch (Exception e) { + log.warn("image ocr failed for {} via {}: {}", fileName, runtime.get().modelName(), e.getMessage()); return ""; } } @@ -1995,6 +2018,7 @@ public class AihrSopSeedService { String text = callVisionOcr(runtime.get(), bytes, guessMimeType(fileName)); return normalizeExtractedText(text); } catch (Exception e) { + log.warn("image ocr failed for {} via {}: {}", fileName, runtime.get().modelName(), e.getMessage()); return ""; } } diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrUploadQueueService.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrUploadQueueService.java index d7c3931c..92526bc5 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrUploadQueueService.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrUploadQueueService.java @@ -223,12 +223,14 @@ public class AihrUploadQueueService { } try { UploadResponse result = sopSeedService.processStagedDocument(row.fileName(), row.category(), staged); + // 0 片段的完成态(如图片待 OCR)把说明写进 error 列,面板可见原因 + String note = result.fragments() != null && result.fragments() == 0 ? truncateError(result.summary()) : null; // CAS:仅当仍是本工人持有的「处理中」才写完成,防止清扫重置后被后来的工人覆盖状态 int updated = jdbcTemplate.update(""" update aihr_knowledge_upload_item - set status = 2, error = null, doc_id = ?, fragment_count = ?, update_time = now() + set status = 2, error = ?, doc_id = ?, fragment_count = ?, update_time = now() where tenant_id = ? and id = ? and status = 1 - """, result.docId(), result.fragments(), TENANT_ID, id); + """, note, result.docId(), result.fragments(), TENANT_ID, id); if (updated == 1) { deleteQuietly(staged); } else { @@ -281,10 +283,7 @@ public class AihrUploadQueueService { } private void markFailed(Long id, String message) { - String error = message == null || message.isBlank() ? "处理失败" : message; - if (error.length() > MAX_ERROR_CHARS) { - error = error.substring(0, MAX_ERROR_CHARS); - } + String error = truncateError(message == null || message.isBlank() ? "处理失败" : message); // 不覆盖已被清扫重置(0)或已完成(2)的行:处理中标失败,失败态允许更新错误信息(重试路径) jdbcTemplate.update(""" update aihr_knowledge_upload_item @@ -293,6 +292,13 @@ public class AihrUploadQueueService { """, error, TENANT_ID, id); } + private static String truncateError(String value) { + if (value == null || value.length() <= MAX_ERROR_CHARS) { + return value; + } + return value.substring(0, MAX_ERROR_CHARS); + } + private UploadItemResponse item(Long id) { List rows = jdbcTemplate.query(""" select id, batch_id, file_name, category, status, error, doc_id, fragment_count, update_time diff --git a/frontend/src/views/knowledge/processing.vue b/frontend/src/views/knowledge/processing.vue index 7665f03b..7c503412 100644 --- a/frontend/src/views/knowledge/processing.vue +++ b/frontend/src/views/knowledge/processing.vue @@ -47,7 +47,7 @@ - +