fix(aihr): image upload without usable vision model no longer fails

图片 OCR 拿不到文字(未配视觉模型/模型不支持图片/图片无文字)时不再
抛「文件内容不能为空」:改为落 OSS + attach 标待处理、0 片段完成,
说明写入队列条目在面板展示(作战清单 M7 口径);OCR 调用失败补日志,
之前被静默吞掉难排查。前端批次表 error 列改名「说明」。
This commit is contained in:
2026-07-03 23:51:37 +08:00
parent 77aa992872
commit 61235b6257
3 changed files with 37 additions and 7 deletions
@@ -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 "";
}
}
@@ -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