From 8509f351a418049401e73826058d2abdc48da126 Mon Sep 17 00:00:00 2001 From: panyy Date: Tue, 23 Jun 2026 11:05:11 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=80=9D=E7=BB=B4=E5=AF=BC=E5=9B=BE?= =?UTF-8?q?=E5=8A=A9=E6=89=8B)=EF=BC=9A=E6=80=9D=E7=BB=B4=E5=AF=BC?= =?UTF-8?q?=E5=9B=BE=E5=8A=A9=E6=89=8B=E8=BF=87=E7=A8=8B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mineru/cli/fast_api.py | 27 +- web_ui/src/components/MindMapRenderer.vue | 71 ++++- .../src/composables/useDocumentProcessor.ts | 127 +++++---- web_ui/src/utils/mindmapMarkdown.ts | 259 +++++++++--------- web_ui/src/utils/request.ts | 8 +- web_ui/src/views/DocumentProcessor.vue | 54 +++- 6 files changed, 331 insertions(+), 215 deletions(-) diff --git a/mineru/cli/fast_api.py b/mineru/cli/fast_api.py index 0e0da56..442d466 100644 --- a/mineru/cli/fast_api.py +++ b/mineru/cli/fast_api.py @@ -12,6 +12,9 @@ import click import zipfile from pathlib import Path import glob + +os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True") + from fastapi import Depends, FastAPI, HTTPException, UploadFile, File, Form, APIRouter, Header from fastapi.middleware.gzip import GZipMiddleware from fastapi.middleware.cors import CORSMiddleware @@ -25,7 +28,9 @@ from base64 import b64encode # MinerU 内部导入 from mineru.cli.common import aio_do_parse, read_fn, pdf_suffixes, image_suffixes, word_suffixes from mineru.utils.cli_parser import arg_parse +from mineru.utils.config_reader import get_device from mineru.utils.guess_suffix_or_lang import guess_suffix_by_path +from mineru.utils.model_utils import clean_memory from mineru.version import __version__ # --- 日志配置 --- @@ -89,6 +94,22 @@ def _update_task_progress(task_id: Optional[str], progress: int, stage: str): _store_task_progress(task_id, state) +def _cleanup_runtime_memory(): + """Release transient accelerator caches after each API parse request.""" + try: + clean_memory(get_device()) + except Exception as exc: + logger.warning(f"Failed to clean runtime memory: {exc}") + + +def _format_parse_error(exc: Exception) -> str: + message = str(exc) + if "CUDA out of memory" in message or exc.__class__.__name__ in ("OutOfMemoryError", "RuntimeError"): + if "out of memory" in message.lower(): + return "GPU显存不足,已尝试清理缓存。请减少最大页数、关闭公式/表格识别,或稍后重试。" + return f"Internal Error: {message}" + + class ProgressTracker: def __init__(self): self.progress = 0 @@ -467,6 +488,7 @@ async def parse_pdf( completed_state = _get_task_progress(task_id) or {} completed_state["status"] = "completed" _store_task_progress(task_id, completed_state) + _cleanup_runtime_memory() # 清理日志捕获和 stderr 捕获 stderr_capture.stop() @@ -531,9 +553,10 @@ async def parse_pdf( pass failed_state = _get_task_progress(task_id) or {} failed_state["status"] = "failed" - failed_state["error"] = str(e) + failed_state["error"] = _format_parse_error(e) _store_task_progress(task_id, failed_state) - return JSONResponse(status_code=500, content={"error": f"Internal Error: {str(e)}"}) + _cleanup_runtime_memory() + return JSONResponse(status_code=500, content={"error": _format_parse_error(e)}) # --- FastAPI 核心应用 --- diff --git a/web_ui/src/components/MindMapRenderer.vue b/web_ui/src/components/MindMapRenderer.vue index 75900a9..2e093d0 100644 --- a/web_ui/src/components/MindMapRenderer.vue +++ b/web_ui/src/components/MindMapRenderer.vue @@ -16,9 +16,12 @@ @@ -176,14 +179,30 @@ function downloadBlob(blob: Blob, fileName: string) { URL.revokeObjectURL(url) } -function createExportSvg() { +type ExportFormat = 'svg' | 'png' | 'jpeg' +type ExportScope = 'full' | 'viewport' + +function getViewportSize(svg: SVGElement) { + const rect = svg.getBoundingClientRect() + return { + width: Math.ceil(Math.max(rect.width, 320)), + height: Math.ceil(Math.max(rect.height, 240)) + } +} + +function cleanExportSvg(svg: SVGElement) { + if (!svg.getAttribute('xmlns')) { + svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg') + } + svg.querySelectorAll('[style*="cursor"]').forEach((node) => node.removeAttribute('style')) +} + +function createFullExportSvg() { const svg = svgRef.value if (!svg) return null const svgCopy = svg.cloneNode(true) as SVGElement - if (!svgCopy.getAttribute('xmlns')) { - svgCopy.setAttribute('xmlns', 'http://www.w3.org/2000/svg') - } + cleanExportSvg(svgCopy) const contentGroup = svg.querySelector('g') const bbox = contentGroup @@ -195,10 +214,30 @@ function createExportSvg() { const height = Math.ceil(Math.max(bbox.height + padding * 2, 240)) const viewBox = `${Math.floor(bbox.x - padding)} ${Math.floor(bbox.y - padding)} ${width} ${height}` + const viewportGroupCopy = svgCopy.querySelector('g') + viewportGroupCopy?.removeAttribute('transform') svgCopy.setAttribute('viewBox', viewBox) svgCopy.setAttribute('width', String(width)) svgCopy.setAttribute('height', String(height)) - svgCopy.querySelectorAll('[style*="cursor"]').forEach((node) => node.removeAttribute('style')) + + return { + data: new XMLSerializer().serializeToString(svgCopy), + width, + height + } +} + +function createViewportExportSvg() { + const svg = svgRef.value + if (!svg) return null + + const svgCopy = svg.cloneNode(true) as SVGElement + cleanExportSvg(svgCopy) + + const { width, height } = getViewportSize(svg) + svgCopy.setAttribute('width', String(width)) + svgCopy.setAttribute('height', String(height)) + svgCopy.setAttribute('viewBox', svg.getAttribute('viewBox') || `0 0 ${width} ${height}`) return { data: new XMLSerializer().serializeToString(svgCopy), @@ -208,17 +247,18 @@ function createExportSvg() { } // 下载思维导图 -const downloadMindMap = async (format: 'svg' | 'png' | 'jpeg' = 'svg') => { - const exportSvg = createExportSvg() +const downloadMindMap = async (format: ExportFormat = 'svg', scope: ExportScope = 'full') => { + const exportSvg = scope === 'viewport' ? createViewportExportSvg() : createFullExportSvg() if (!exportSvg) return const time = new Date().getTime() + const scopeName = scope === 'viewport' ? 'viewport' : 'full' if (format === 'svg') { - downloadBlob(new Blob([exportSvg.data], { type: 'image/svg+xml;charset=utf-8' }), `mindmap_${time}.svg`) + downloadBlob(new Blob([exportSvg.data], { type: 'image/svg+xml;charset=utf-8' }), `mindmap_${scopeName}_${time}.svg`) return } - const maxCanvasPixels = 16_000_000 + const maxCanvasPixels = scope === 'viewport' ? 16_000_000 : 64_000_000 const scaleFactor = Math.min(3, Math.max(1, Math.sqrt(maxCanvasPixels / (exportSvg.width * exportSvg.height)))) const canvas = document.createElement('canvas') const ctx = canvas.getContext('2d') @@ -236,7 +276,7 @@ const downloadMindMap = async (format: 'svg' | 'png' | 'jpeg' = 'svg') => { const mimeType = format === 'jpeg' ? 'image/jpeg' : 'image/png' canvas.toBlob((blob) => { if (!blob) return - downloadBlob(blob, `mindmap_${time}.${format === 'jpeg' ? 'jpg' : 'png'}`) + downloadBlob(blob, `mindmap_${scopeName}_${time}.${format === 'jpeg' ? 'jpg' : 'png'}`) }, mimeType, 0.96) } img.onerror = () => ElMessage.error('导出图片失败') @@ -245,8 +285,9 @@ const downloadMindMap = async (format: 'svg' | 'png' | 'jpeg' = 'svg') => { // 处理下载命令 const handleDownload = (command: string) => { - if (command === 'svg' || command === 'png' || command === 'jpeg') { - downloadMindMap(command) + const [scope, format] = command.split('-') as [ExportScope, ExportFormat] + if ((scope === 'full' || scope === 'viewport') && (format === 'svg' || format === 'png' || format === 'jpeg')) { + downloadMindMap(format, scope) } } diff --git a/web_ui/src/composables/useDocumentProcessor.ts b/web_ui/src/composables/useDocumentProcessor.ts index ec22cc3..af25d1b 100644 --- a/web_ui/src/composables/useDocumentProcessor.ts +++ b/web_ui/src/composables/useDocumentProcessor.ts @@ -1,4 +1,4 @@ -import { ref, reactive } from 'vue' +import { ref, reactive, watch } from 'vue' import type { Ref } from 'vue' import { documentApi, type ParseParams } from '@/api/document' import { convertWordToPdf, isWordFile } from '@/utils/wordToPdf' @@ -26,13 +26,24 @@ export interface ProcessResult { downloadUrl?: string } +const CONFIG_STORAGE_KEY = 'mineru.documentProcessor.config' + +function loadCachedConfig(): Partial { + try { + const raw = window.localStorage.getItem(CONFIG_STORAGE_KEY) + return raw ? JSON.parse(raw) : {} + } catch { + return {} + } +} + export function useDocumentProcessor() { // 文件相关 const uploadedFiles: Ref = ref([]) const isUploading = ref(false) // 配置相关 - const config = reactive({ + const defaultConfig: DocumentConfig = { maxPages: 1000, backend: 'hybrid-auto-engine', serverUrl: 'http://localhost:30000', @@ -40,7 +51,23 @@ export function useDocumentProcessor() { formulaEnable: true, language: 'ch', forceOcr: false + } + const config = reactive({ + ...defaultConfig, + ...loadCachedConfig() }) + + watch( + config, + (value) => { + try { + window.localStorage.setItem(CONFIG_STORAGE_KEY, JSON.stringify(value)) + } catch { + // 浏览器禁用本地存储时不影响当前页面使用。 + } + }, + { deep: true } + ) // 结果相关 const results = ref(null) @@ -82,55 +109,57 @@ export function useDocumentProcessor() { if (!files || files.length === 0) return const validFiles: File[] = [] + const file = files[0] + uploadedFiles.value = [] + if (files.length > 1) { + error.value = '一次只能选择一个文件,已使用第一个文件' + } - for (let i = 0; i < files.length; i++) { - const file = files[i] - const fileType = file.type - const fileName = file.name.toLowerCase() - - console.log('Processing file:', fileName, 'type:', fileType) - - // 验证文件类型 - 支持 PDF、图片和 Word 文档 - const isImage = fileType.startsWith('image/') - const isPdf = fileType === 'application/pdf' - const isWord = fileName.endsWith('.docx') || fileName.endsWith('.doc') || fileType === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' - - console.log('File type checks - isImage:', isImage, 'isPdf:', isPdf, 'isWord:', isWord) - - if (!isImage && !isPdf && !isWord) { - error.value = '不支持的文件类型' - console.log('Unsupported file type:', fileName, fileType) - continue - } - - // 验证文件大小 (100MB) - if (file.size > 100 * 1024 * 1024) { - error.value = '文件大小超出限制' - console.log('File too large:', fileName, file.size) - continue - } - - // 如果是 Word 文档,转换为 PDF - if (isWord) { - try { - isUploading.value = true - error.value = '正在将 Word 文档转换为 PDF...' - console.log('Converting Word to PDF:', fileName) - const pdfFile = await convertWordToPdf(file) - console.log('Conversion successful, PDF file:', pdfFile.name, pdfFile.size) - validFiles.push(pdfFile) - error.value = null - } catch (err) { - error.value = 'Word 转换为 PDF 失败: ' + (err as Error).message - console.error('Conversion failed:', (err as Error).message) - continue - } finally { - isUploading.value = false - } - } else { - validFiles.push(file) - console.log('Adding file directly:', fileName) + const fileType = file.type + const fileName = file.name.toLowerCase() + + console.log('Processing file:', fileName, 'type:', fileType) + + // 验证文件类型 - 支持 PDF、图片和 Word 文档 + const isImage = fileType.startsWith('image/') + const isPdf = fileType === 'application/pdf' + const isWord = fileName.endsWith('.docx') || fileName.endsWith('.doc') || fileType === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' + + console.log('File type checks - isImage:', isImage, 'isPdf:', isPdf, 'isWord:', isWord) + + if (!isImage && !isPdf && !isWord) { + error.value = '不支持的文件类型' + console.log('Unsupported file type:', fileName, fileType) + return + } + + // 验证文件大小 (100MB) + if (file.size > 100 * 1024 * 1024) { + error.value = '文件大小超出限制' + console.log('File too large:', fileName, file.size) + return + } + + // 如果是 Word 文档,转换为 PDF + if (isWord) { + try { + isUploading.value = true + error.value = '正在将 Word 文档转换为 PDF...' + console.log('Converting Word to PDF:', fileName) + const pdfFile = await convertWordToPdf(file) + console.log('Conversion successful, PDF file:', pdfFile.name, pdfFile.size) + validFiles.push(pdfFile) + error.value = null + } catch (err) { + error.value = 'Word 转换为 PDF 失败: ' + (err as Error).message + console.error('Conversion failed:', (err as Error).message) + return + } finally { + isUploading.value = false } + } else { + validFiles.push(file) + console.log('Adding file directly:', fileName) } uploadedFiles.value = validFiles diff --git a/web_ui/src/utils/mindmapMarkdown.ts b/web_ui/src/utils/mindmapMarkdown.ts index 9cc8883..8b42ba0 100644 --- a/web_ui/src/utils/mindmapMarkdown.ts +++ b/web_ui/src/utils/mindmapMarkdown.ts @@ -1,167 +1,156 @@ -const MAX_NODE_TEXT_LENGTH = 72 -const MAX_PARAGRAPH_SUMMARY_LENGTH = 90 -const MAX_CHILDREN_PER_HEADING = 18 +import MarkdownIt from 'markdown-it' +import type Token from 'markdown-it/lib/token.mjs' + const ROOT_TITLE = '文档思维导图' +const MAX_HEADING_LEVEL = 6 -type BlockType = 'paragraph' | 'list' | 'table' | 'code' | 'math' | 'image' +const md = new MarkdownIt({ + html: true, + linkify: true, + typographer: true, + breaks: true +}) -interface HeadingState { - level: number - childCount: number +function normalizeNodeText(text: string) { + return text + .replace(/\r?\n/g, ' ') + .replace(/\s+/g, ' ') + .trim() } -function normalizeWhitespace(text: string) { - return text.replace(/\s+/g, ' ').trim() +function clampHeadingLevel(level: number) { + return Math.min(Math.max(level, 1), MAX_HEADING_LEVEL) } -function stripMarkdownInline(text: string) { - return normalizeWhitespace(text) - .replace(/!\[([^\]]*)\]\([^)]+\)/g, '$1') - .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') - .replace(/[*_`~>#]/g, '') +function pushHeading(result: string[], level: number, text: string) { + const normalized = normalizeNodeText(text) + if (!normalized) return + result.push(`${'#'.repeat(clampHeadingLevel(level))} ${normalized}`) } -function truncateText(text: string, maxLength = MAX_NODE_TEXT_LENGTH) { - const normalized = normalizeWhitespace(text) - if (normalized.length <= maxLength) return normalized - return `${normalized.slice(0, maxLength - 1)}...` +function getHeadingLevel(token: Token) { + const tagLevel = Number(token.tag.replace(/^h/i, '')) + return Number.isFinite(tagLevel) && tagLevel > 0 ? tagLevel : 1 } -function summarizeParagraph(text: string) { - const cleaned = stripMarkdownInline(text) - const sentence = cleaned.match(/^(.+?[。!?!?;;::]|\S.{20,}?[,,])/) - return truncateText(sentence?.[1] || cleaned, MAX_PARAGRAPH_SUMMARY_LENGTH) +function getNextInlineContent(tokens: Token[], index: number) { + const next = tokens[index + 1] + return next?.type === 'inline' ? next.content : '' } -function isHeading(line: string) { - return /^#{1,6}\s+/.test(line.trim()) +function getFenceContent(token: Token) { + const info = normalizeNodeText(token.info || '') + const content = token.content.trim() + if (!content) return info ? `代码块 ${info}` : '代码块' + return info ? `代码块 ${info}: ${content}` : `代码块: ${content}` } -function getHeadingLevel(line: string) { - return line.trim().match(/^#{1,6}/)?.[0].length || 0 -} +function collectTableRows(tokens: Token[], startIndex: number) { + const rows: string[][] = [] + let currentRow: string[] | null = null + let endIndex = startIndex -function getHeadingText(line: string) { - return stripMarkdownInline(line.trim().replace(/^#{1,6}\s+/, '')) -} + for (let i = startIndex + 1; i < tokens.length; i += 1) { + const token = tokens[i] + endIndex = i -function getListText(line: string) { - return stripMarkdownInline(line.trim().replace(/^[-*+]\s+|^\d+[.)、]\s+/, '')) -} - -function countHeadingChildren(stack: HeadingState[], level: number) { - while (stack.length && stack[stack.length - 1].level >= level) { - stack.pop() + if (token.type === 'table_close') break + if (token.type === 'tr_open') { + currentRow = [] + continue + } + if (token.type === 'tr_close') { + if (currentRow && currentRow.some((cell) => cell.trim())) { + rows.push(currentRow) + } + currentRow = null + continue + } + if ((token.type === 'th_open' || token.type === 'td_open') && currentRow) { + currentRow.push(getNextInlineContent(tokens, i)) + } } - const parent = stack[stack.length - 1] - if (!parent) return true - parent.childCount += 1 - return parent.childCount <= MAX_CHILDREN_PER_HEADING -} - -function pushBlock(result: string[], stack: HeadingState[], blockType: BlockType, text: string) { - const cleaned = text.trim() - if (!cleaned) return - - const parentLevel = stack.length ? stack[stack.length - 1].level : 1 - const level = Math.min(parentLevel + 1, 6) - if (!countHeadingChildren(stack, level)) return - - const prefixMap: Record = { - paragraph: '摘要', - list: '要点', - table: '表格', - code: '代码', - math: '公式', - image: '图片' - } - - result.push(`${'#'.repeat(level)} ${prefixMap[blockType]}:${cleaned}`) + return { rows, endIndex } } export function buildMindmapMarkdown(markdown: string) { - const lines = markdown.split(/\r?\n/) + if (!markdown.trim()) return '' + + const tokens = md.parse(markdown, {}) const result: string[] = [`# ${ROOT_TITLE}`] - const stack: HeadingState[] = [{ level: 1, childCount: 0 }] - let paragraphBuffer: string[] = [] - let inCodeBlock = false - let codeBlockTitle = '' + let currentHeadingLevel = 1 + let listDepth = 0 - const flushParagraph = () => { - if (paragraphBuffer.length === 0) return - const text = summarizeParagraph(paragraphBuffer.join(' ')) - pushBlock(result, stack, 'paragraph', text) - paragraphBuffer = [] + for (let i = 0; i < tokens.length; i += 1) { + const token = tokens[i] + + if (token.type === 'heading_open') { + currentHeadingLevel = clampHeadingLevel(getHeadingLevel(token) + 1) + pushHeading(result, currentHeadingLevel, getNextInlineContent(tokens, i) || '未命名章节') + i += 2 + continue + } + + if (token.type === 'bullet_list_open' || token.type === 'ordered_list_open') { + listDepth += 1 + continue + } + + if (token.type === 'bullet_list_close' || token.type === 'ordered_list_close') { + listDepth = Math.max(0, listDepth - 1) + continue + } + + if (token.type === 'paragraph_open') { + const level = currentHeadingLevel + Math.max(listDepth, 1) + pushHeading(result, level, getNextInlineContent(tokens, i)) + i += 2 + continue + } + + if (token.type === 'fence' || token.type === 'code_block') { + pushHeading(result, currentHeadingLevel + 1, getFenceContent(token)) + continue + } + + if (token.type === 'html_block') { + pushHeading(result, currentHeadingLevel + 1, token.content) + continue + } + + if (token.type === 'blockquote_open') { + pushHeading(result, currentHeadingLevel + 1, '引用') + currentHeadingLevel = clampHeadingLevel(currentHeadingLevel + 1) + continue + } + + if (token.type === 'blockquote_close') { + currentHeadingLevel = Math.max(1, currentHeadingLevel - 1) + continue + } + + if (token.type === 'hr') { + pushHeading(result, currentHeadingLevel + 1, '---') + continue + } + + if (token.type === 'table_open') { + const tableLevel = currentHeadingLevel + 1 + const { rows, endIndex } = collectTableRows(tokens, i) + pushHeading(result, tableLevel, '表格') + rows.forEach((row) => { + pushHeading(result, tableLevel + 1, row.map(normalizeNodeText).join(' | ')) + }) + i = endIndex + } } - for (const line of lines) { - const trimmed = line.trim() - - if (/^```/.test(trimmed)) { - if (inCodeBlock) { - pushBlock(result, stack, 'code', codeBlockTitle || '代码块') - codeBlockTitle = '' - } else { - flushParagraph() - codeBlockTitle = trimmed.replace(/^```/, '').trim() - } - inCodeBlock = !inCodeBlock - continue - } - - if (inCodeBlock) continue - - if (!trimmed) { - flushParagraph() - continue - } - - if (isHeading(trimmed)) { - flushParagraph() - const rawLevel = getHeadingLevel(trimmed) - const level = Math.min(rawLevel + 1, 6) - while (stack.length && stack[stack.length - 1].level >= level) { - stack.pop() - } - result.push(`${'#'.repeat(level)} ${truncateText(getHeadingText(trimmed)) || '未命名章节'}`) - stack.push({ level, childCount: 0 }) - continue - } - - if (/^\|.+\|$/.test(trimmed)) { - flushParagraph() - pushBlock(result, stack, 'table', '表格内容') - continue - } - - if (/^!\[[^\]]*]\([^)]+\)/.test(trimmed)) { - flushParagraph() - pushBlock(result, stack, 'image', stripMarkdownInline(trimmed) || '图片') - continue - } - - if (/^\$\$.*\$\$$/.test(trimmed) || /^\$[^$].*\$$/.test(trimmed)) { - flushParagraph() - pushBlock(result, stack, 'math', truncateText(trimmed, 80)) - continue - } - - if (/^[-*+]\s+|^\d+[.)、]\s+/.test(trimmed)) { - flushParagraph() - pushBlock(result, stack, 'list', truncateText(getListText(trimmed))) - continue - } - - paragraphBuffer.push(trimmed) - } - - flushParagraph() return result.join('\n') } export function replaceFirstMindmapText(markdown: string, oldText: string, newText: string) { - const normalizedOld = oldText.replace(/^(摘要|要点|表格|代码|公式|图片):/, '').trim() + const normalizedOld = oldText.replace(/^(引用|表格|代码块)(\s|:|:)?/, '').trim() if (!normalizedOld || !newText.trim()) return markdown const escaped = normalizedOld.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') @@ -171,7 +160,7 @@ export function replaceFirstMindmapText(markdown: string, oldText: string, newTe } const lines = markdown.split(/\r?\n/) - const targetLineIndex = lines.findIndex((line) => stripMarkdownInline(line).includes(normalizedOld)) + const targetLineIndex = lines.findIndex((line) => normalizeNodeText(line).includes(normalizedOld)) if (targetLineIndex >= 0) { lines[targetLineIndex] = lines[targetLineIndex].replace(normalizedOld, newText.trim()) return lines.join('\n') diff --git a/web_ui/src/utils/request.ts b/web_ui/src/utils/request.ts index 84393f4..5febfac 100644 --- a/web_ui/src/utils/request.ts +++ b/web_ui/src/utils/request.ts @@ -28,6 +28,12 @@ request.interceptors.response.use( (error) => { // 统一错误处理 if (error.response) { + const serverMessage = error.response.data?.error || error.response.data?.detail + if (serverMessage) { + error.message = serverMessage + return Promise.reject(error) + } + switch (error.response.status) { case 400: error.message = '请求参数错误' @@ -64,4 +70,4 @@ request.interceptors.response.use( } ) -export default request \ No newline at end of file +export default request diff --git a/web_ui/src/views/DocumentProcessor.vue b/web_ui/src/views/DocumentProcessor.vue index bd2e8ec..0f146b3 100644 --- a/web_ui/src/views/DocumentProcessor.vue +++ b/web_ui/src/views/DocumentProcessor.vue @@ -20,7 +20,7 @@
文件导入
-
支持 PDF、Word、PNG 格式文件,可转换为 Markdown 和思维导图,也可手动粘贴 Markdown 源码
+
支持 PDF、Word、PNG 格式单个文件,可转换为 Markdown 和思维导图,也可手动粘贴 Markdown 源码
@@ -174,7 +174,6 @@