mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 11:58:34 +08:00
feat(wiki): wire image uploads to vision-in pipeline
This commit is contained in:
parent
088ffcaee1
commit
4dc280a3d1
@ -239,11 +239,14 @@ public class WikiController {
|
|||||||
? originalName.substring(originalName.lastIndexOf(".") + 1).toLowerCase()
|
? originalName.substring(originalName.lastIndexOf(".") + 1).toLowerCase()
|
||||||
: "txt";
|
: "txt";
|
||||||
|
|
||||||
// 确定 sourceType
|
// Resolve source type from extension. Image extensions route to the
|
||||||
|
// vision-in pipeline at extraction time; everything else falls through
|
||||||
|
// to the existing text / pdf / docx handling.
|
||||||
String sourceType = switch (extension) {
|
String sourceType = switch (extension) {
|
||||||
case "pdf" -> "pdf";
|
case "pdf" -> "pdf";
|
||||||
case "docx", "doc" -> "docx";
|
case "docx", "doc" -> "docx";
|
||||||
case "txt", "md" -> "text";
|
case "txt", "md" -> "text";
|
||||||
|
case "png", "jpg", "jpeg", "webp", "gif", "bmp", "tiff", "tif" -> "image";
|
||||||
default -> "text";
|
default -> "text";
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -258,6 +261,7 @@ public class WikiController {
|
|||||||
Path targetPath = uploadDir.resolve(System.currentTimeMillis() + "_" + originalName);
|
Path targetPath = uploadDir.resolve(System.currentTimeMillis() + "_" + originalName);
|
||||||
file.transferTo(targetPath);
|
file.transferTo(targetPath);
|
||||||
return R.ok(rawService.addFile(kbId, originalName, sourceType,
|
return R.ok(rawService.addFile(kbId, originalName, sourceType,
|
||||||
|
file.getContentType(),
|
||||||
targetPath.toString(), file.getSize()));
|
targetPath.toString(), file.getSize()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,10 +23,13 @@ public class WikiRawMaterialEntity {
|
|||||||
/** 材料标题 */
|
/** 材料标题 */
|
||||||
private String title;
|
private String title;
|
||||||
|
|
||||||
/** 来源类型:text / pdf / docx / url / paste */
|
/** Source type: text / pdf / docx / image / url / paste. */
|
||||||
private String sourceType;
|
private String sourceType;
|
||||||
|
|
||||||
/** 原始文件路径(二进制文件) */
|
/** Original Content-Type from the upload (e.g. {@code image/png}); null for text. */
|
||||||
|
private String mimeType;
|
||||||
|
|
||||||
|
/** Original file path on disk (binary uploads only). */
|
||||||
private String sourcePath;
|
private String sourcePath;
|
||||||
|
|
||||||
/** 原始文本内容(文本类型) */
|
/** 原始文本内容(文本类型) */
|
||||||
|
|||||||
@ -9,6 +9,9 @@ import org.springframework.context.ApplicationEventPublisher;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import vip.mate.tool.builtin.DocumentExtractTool;
|
import vip.mate.tool.builtin.DocumentExtractTool;
|
||||||
|
import vip.mate.tool.image.vision.ImageVisionService;
|
||||||
|
import vip.mate.tool.image.vision.VisionRequest;
|
||||||
|
import vip.mate.tool.image.vision.VisionResult;
|
||||||
import vip.mate.wiki.WikiProperties;
|
import vip.mate.wiki.WikiProperties;
|
||||||
import vip.mate.wiki.event.WikiProcessingEvent;
|
import vip.mate.wiki.event.WikiProcessingEvent;
|
||||||
import vip.mate.wiki.model.WikiRawMaterialEntity;
|
import vip.mate.wiki.model.WikiRawMaterialEntity;
|
||||||
@ -38,6 +41,7 @@ public class WikiRawMaterialService {
|
|||||||
private final DocumentExtractTool documentExtractTool;
|
private final DocumentExtractTool documentExtractTool;
|
||||||
/** RFC-013:删除时级联清理 chunk */
|
/** RFC-013:删除时级联清理 chunk */
|
||||||
private final WikiChunkService chunkService;
|
private final WikiChunkService chunkService;
|
||||||
|
private final ImageVisionService imageVisionService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RFC-012 follow-up #3:从 partial 状态触发的 reprocess 会在此 set 中打标,
|
* RFC-012 follow-up #3:从 partial 状态触发的 reprocess 会在此 set 中打标,
|
||||||
@ -119,15 +123,33 @@ public class WikiRawMaterialService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 添加文件类型的原始材料(PDF/DOCX 等)
|
* Adds a file-type raw material (PDF / DOCX / image / ...).
|
||||||
|
*
|
||||||
|
* <p>Backwards-compatible overload that omits the MIME type. Callers
|
||||||
|
* with the upload Content-Type in hand should prefer the four-argument
|
||||||
|
* variant — image-routing in particular needs an authoritative MIME so
|
||||||
|
* downstream vision providers know what they are decoding.
|
||||||
*/
|
*/
|
||||||
@Transactional
|
@Transactional
|
||||||
public WikiRawMaterialEntity addFile(Long kbId, String title, String sourceType,
|
public WikiRawMaterialEntity addFile(Long kbId, String title, String sourceType,
|
||||||
String sourcePath, long fileSize) {
|
String sourcePath, long fileSize) {
|
||||||
|
return addFile(kbId, title, sourceType, null, sourcePath, fileSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a file-type raw material with explicit MIME type.
|
||||||
|
*
|
||||||
|
* @param mimeType Content-Type string from the upload (e.g. {@code image/png});
|
||||||
|
* may be null if unknown
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public WikiRawMaterialEntity addFile(Long kbId, String title, String sourceType,
|
||||||
|
String mimeType, String sourcePath, long fileSize) {
|
||||||
WikiRawMaterialEntity entity = new WikiRawMaterialEntity();
|
WikiRawMaterialEntity entity = new WikiRawMaterialEntity();
|
||||||
entity.setKbId(kbId);
|
entity.setKbId(kbId);
|
||||||
entity.setTitle(title);
|
entity.setTitle(title);
|
||||||
entity.setSourceType(sourceType);
|
entity.setSourceType(sourceType);
|
||||||
|
entity.setMimeType(mimeType);
|
||||||
entity.setSourcePath(sourcePath);
|
entity.setSourcePath(sourcePath);
|
||||||
entity.setFileSize(fileSize);
|
entity.setFileSize(fileSize);
|
||||||
entity.setProcessingStatus("pending");
|
entity.setProcessingStatus("pending");
|
||||||
@ -314,6 +336,13 @@ public class WikiRawMaterialService {
|
|||||||
if ("text".equals(entity.getSourceType())) {
|
if ("text".equals(entity.getSourceType())) {
|
||||||
return entity.getOriginalContent();
|
return entity.getOriginalContent();
|
||||||
}
|
}
|
||||||
|
// Image source: route through the vision-in pipeline. Failures (feature
|
||||||
|
// flag off, no provider configured, all providers failed) degrade to an
|
||||||
|
// empty caption rather than blocking the upload — the user keeps the
|
||||||
|
// raw row and can retry once vision is configured.
|
||||||
|
if ("image".equals(entity.getSourceType())) {
|
||||||
|
return extractTextFromImage(entity);
|
||||||
|
}
|
||||||
// 二进制文件:调用 DocumentExtractTool 提取
|
// 二进制文件:调用 DocumentExtractTool 提取
|
||||||
if (entity.getSourcePath() != null && !entity.getSourcePath().isBlank()) {
|
if (entity.getSourcePath() != null && !entity.getSourcePath().isBlank()) {
|
||||||
try {
|
try {
|
||||||
@ -344,6 +373,74 @@ public class WikiRawMaterialService {
|
|||||||
return entity.getOriginalContent();
|
return entity.getOriginalContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Routes an image-typed raw material through the vision-in pipeline,
|
||||||
|
* caches the resulting caption into {@code extracted_text} so the next
|
||||||
|
* call short-circuits, and degrades gracefully on failure.
|
||||||
|
*
|
||||||
|
* <p>Failure modes (feature flag off, no provider, all providers failed,
|
||||||
|
* IO errors reading the image bytes) are intentionally swallowed and
|
||||||
|
* surfaced as the empty string. The upload still succeeded; the raw
|
||||||
|
* row remains and downstream code is expected to tolerate "no
|
||||||
|
* extracted text yet" — calling this method again later (e.g. after
|
||||||
|
* an operator enables the feature flag) re-runs the pipeline.
|
||||||
|
*/
|
||||||
|
private String extractTextFromImage(WikiRawMaterialEntity entity) {
|
||||||
|
if (entity.getSourcePath() == null || entity.getSourcePath().isBlank()) {
|
||||||
|
log.warn("[Wiki] Image raw material missing sourcePath: id={}", entity.getId());
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
byte[] imageBytes;
|
||||||
|
try {
|
||||||
|
imageBytes = java.nio.file.Files.readAllBytes(java.nio.file.Paths.get(entity.getSourcePath()));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[Wiki] Failed to read image bytes for id={}: {}", entity.getId(), e.getMessage());
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
VisionRequest request = VisionRequest.builder()
|
||||||
|
.imageBytes(imageBytes)
|
||||||
|
.mimeType(resolveMimeType(entity))
|
||||||
|
.build();
|
||||||
|
VisionResult result;
|
||||||
|
try {
|
||||||
|
result = imageVisionService.caption(request);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[Wiki] Vision pipeline failed for raw id={}: {}", entity.getId(), e.getMessage());
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder text = new StringBuilder(result.getCaption() == null ? "" : result.getCaption());
|
||||||
|
if (result.getVisibleText() != null && !result.getVisibleText().isBlank()) {
|
||||||
|
text.append("\n\n--- Visible text ---\n").append(result.getVisibleText());
|
||||||
|
}
|
||||||
|
String combined = text.toString();
|
||||||
|
updateExtractedText(entity.getId(), combined);
|
||||||
|
log.info("[Wiki] Image vision captioned raw id={} provider={} model={} chars={}",
|
||||||
|
entity.getId(), result.getProviderId(), result.getModel(), combined.length());
|
||||||
|
return combined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Best-effort MIME resolution: prefer the persisted column, fall back to file extension. */
|
||||||
|
private static String resolveMimeType(WikiRawMaterialEntity entity) {
|
||||||
|
if (entity.getMimeType() != null && !entity.getMimeType().isBlank()) {
|
||||||
|
return entity.getMimeType();
|
||||||
|
}
|
||||||
|
String path = entity.getSourcePath() == null ? "" : entity.getSourcePath().toLowerCase();
|
||||||
|
int dot = path.lastIndexOf('.');
|
||||||
|
if (dot < 0) return "application/octet-stream";
|
||||||
|
String ext = path.substring(dot + 1);
|
||||||
|
return switch (ext) {
|
||||||
|
case "png" -> "image/png";
|
||||||
|
case "jpg", "jpeg" -> "image/jpeg";
|
||||||
|
case "webp" -> "image/webp";
|
||||||
|
case "gif" -> "image/gif";
|
||||||
|
case "bmp" -> "image/bmp";
|
||||||
|
case "tiff", "tif" -> "image/tiff";
|
||||||
|
default -> "application/octet-stream";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recover raw materials stuck in 'processing' status after a server restart.
|
* Recover raw materials stuck in 'processing' status after a server restart.
|
||||||
* Resets them to 'pending', clears stale progress fields, and optionally
|
* Resets them to 'pending', clears stale progress fields, and optionally
|
||||||
|
|||||||
@ -0,0 +1,6 @@
|
|||||||
|
-- Adds the MIME type column to mate_wiki_raw_material so the upload pipeline
|
||||||
|
-- can route uploads to the right downstream extractor. Image source types
|
||||||
|
-- in particular need the original Content-Type to pick a vision provider
|
||||||
|
-- and to render previews correctly without re-sniffing the file.
|
||||||
|
|
||||||
|
ALTER TABLE mate_wiki_raw_material ADD COLUMN IF NOT EXISTS mime_type VARCHAR(64);
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
-- Adds the MIME type column to mate_wiki_raw_material so the upload pipeline
|
||||||
|
-- can route uploads to the right downstream extractor. Image source types
|
||||||
|
-- in particular need the original Content-Type to pick a vision provider
|
||||||
|
-- and to render previews correctly without re-sniffing the file.
|
||||||
|
--
|
||||||
|
-- MySQL does not support `ADD COLUMN IF NOT EXISTS`, so we guard via
|
||||||
|
-- INFORMATION_SCHEMA + a prepared statement to keep the migration
|
||||||
|
-- idempotent across re-runs and partial failures.
|
||||||
|
|
||||||
|
SET @sql = (
|
||||||
|
SELECT IF(COUNT(*) = 0,
|
||||||
|
'ALTER TABLE mate_wiki_raw_material ADD COLUMN mime_type VARCHAR(64)',
|
||||||
|
'SELECT 1')
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE table_schema = DATABASE()
|
||||||
|
AND table_name = 'mate_wiki_raw_material'
|
||||||
|
AND column_name = 'mime_type'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
Loading…
Reference in New Issue
Block a user