fix(aihr): image upload without usable vision model no longer fails
图片 OCR 拿不到文字(未配视觉模型/模型不支持图片/图片无文字)时不再 抛「文件内容不能为空」:改为落 OSS + attach 标待处理、0 片段完成, 说明写入队列条目在面板展示(作战清单 M7 口径);OCR 调用失败补日志, 之前被静默吞掉难排查。前端批次表 error 列改名「说明」。
This commit is contained in:
+24
@@ -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<SysOssVo> 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 "";
|
||||
}
|
||||
}
|
||||
|
||||
+12
-6
@@ -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<UploadItemResponse> rows = jdbcTemplate.query("""
|
||||
select id, batch_id, file_name, category, status, error, doc_id, fragment_count, update_time
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="fragmentCount" label="片段" width="70" />
|
||||
<el-table-column prop="error" label="失败原因" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="error" label="说明" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="updatedAt" label="更新时间" width="130" />
|
||||
<el-table-column label="操作" width="80">
|
||||
<template #default="{ row }">
|
||||
|
||||
Reference in New Issue
Block a user