feat: replace pymupdf image extraction with pdf2image + Heron layout detection

PDF image extraction now renders pages with pdf2image, detects figure
regions using docling-layout-heron (RT-DETRv2), and crops only the
detected pictures. Removes full-page fallback for text-only pages.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-02 22:38:31 +08:00
parent bbf6b921af
commit 6395ee3b49
7 changed files with 381 additions and 55 deletions
+6 -11
View File
@@ -33,7 +33,7 @@ import {
Settings
} from "lucide-react";
import { formatFileSize, formatDate } from "@/lib/utils";
import { knowledgeBaseAPI } from "@/lib/api";
import { knowledgeBaseAPI, resolveImageUrl } from "@/lib/api";
import { KnowledgeBaseDetail, Document } from "@/types";
export default function KnowledgeBaseDetailPage() {
@@ -158,13 +158,12 @@ export default function KnowledgeBaseDetailPage() {
const handleViewDocument = async (doc: Document) => {
try {
const token = localStorage.getItem("auth_token");
const res = await fetch(`/api/knowledge-bases/documents/${doc.id}/download`, {
const res = await fetch(resolveImageUrl(`/knowledge-bases/documents/${doc.id}/download`), {
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok) throw new Error("下载失败");
if (!res.ok) throw new Error("获取文档失败");
const blob = await res.blob();
// 从 Content-Disposition 提取文件名,或用 doc 信息拼接
const disposition = res.headers.get("Content-Disposition");
let filename = `${doc.title}${doc.file_type}`;
if (disposition) {
@@ -172,15 +171,11 @@ export default function KnowledgeBaseDetailPage() {
if (match) filename = decodeURIComponent(match[1].replace(/["']/g, ""));
}
// 触发浏览器下载
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
window.open(url, '_blank');
setTimeout(() => URL.revokeObjectURL(url), 60000);
} catch {
window.open(`/api/knowledge-bases/documents/${doc.id}/download`, '_blank');
window.open(resolveImageUrl(`/knowledge-bases/documents/${doc.id}/download`), '_blank');
}
};
+12 -1
View File
@@ -113,7 +113,18 @@ export default function MessageItem({ message, selectedModel }: MessageItemProps
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(message.content);
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(message.content);
} else {
const textarea = document.createElement("textarea");
textarea.value = message.content;
textarea.style.position = "fixed";
textarea.style.left = "-9999px";
document.body.appendChild(textarea);
textarea.select();
document.execCommand("copy");
document.body.removeChild(textarea);
}
toast.success("已复制");
} catch {
toast.error("复制失败");
+9
View File
@@ -339,6 +339,15 @@ export const useChatStore = create<ChatStore>((set, get) => ({
scheduleFlush();
},
(_sessionId: number, messageId?: number, userMessageId?: number) => {
// 刷出缓冲区中剩余的内容
if (rafId !== null) {
cancelAnimationFrame(rafId);
rafId = null;
}
if (contentBuffer) {
flushBuffer();
}
if (messageId) {
set((state) => ({
messages: state.messages.map(msg => {