import { ref, reactive } from 'vue' import type { Ref } from 'vue' import { documentApi, type ParseParams } from '@/api/document' import { convertWordToPdf, isWordFile } from '@/utils/wordToPdf' // 根据上一级标题自动补全下一级标题 function autoPromoteParagraphsToSubheading(text: string): string { const lines = text.split('\n') const result: string[] = [] let inSection = false let currentHeadingLevel = 0 // 记录当前标题级别 for (const line of lines) { const stripped = line.trim() // 检测标题级别 if (stripped.startsWith('#')) { // 计算标题级别(连续的#数量) const headingMatch = stripped.match(/^#+/) if (headingMatch) { currentHeadingLevel = headingMatch[0].length } else { currentHeadingLevel = 0 } result.push(line) inSection = true continue } if (!stripped) { // 空行不需要添加标题,但也不退出标题段落模式 result.push(line) continue } // 跳过图片行 if (stripped.startsWith('![')) { result.push(line) continue } // 其他特殊行(列表、代码等)都统一处理 if (inSection && currentHeadingLevel > 0 && currentHeadingLevel < 6) { // 根据当前标题级别生成下一级标题 const nextHeadingLevel = currentHeadingLevel + 1 const headingPrefix = '#'.repeat(nextHeadingLevel) result.push(headingPrefix + ' ' + stripped) } else { result.push(line) } } return result.join('\n') } export interface DocumentConfig { maxPages: number backend: string serverUrl: string tableEnable: boolean formulaEnable: boolean language: string forceOcr: boolean } export interface ProcessResult { markdown: string source: string mindmap: string downloadUrl?: string } export function useDocumentProcessor() { // 文件相关 const uploadedFiles: Ref = ref([]) const isUploading = ref(false) // 配置相关 const config = reactive({ maxPages: 1000, backend: 'hybrid-auto-engine', serverUrl: 'http://localhost:30000', tableEnable: true, formulaEnable: true, language: 'ch', forceOcr: false }) // 结果相关 const results = ref(null) const isProcessing = ref(false) const error = ref(null) // 后端选项 const backendOptions = [ { value: 'pipeline', label: '传统管道解析' }, { value: 'vlm-auto-engine', label: 'VLM本地引擎' }, { value: 'hybrid-auto-engine', label: '混合本地引擎' }, { value: 'vlm-http-client', label: 'VLM远程客户端' }, { value: 'hybrid-http-client', label: '混合远程客户端' } ] // 语言选项 const languageOptions = [ { value: 'ch', label: '中文(简体)' }, { value: 'en', label: '英语' }, { value: 'korean', label: '韩语' }, { value: 'japan', label: '日语' }, { value: 'chinese_cht', label: '中文(繁体)' }, { value: 'ta', label: '泰米尔语' }, { value: 'te', label: '泰卢固语' }, { value: 'ka', label: '卡纳达语' }, { value: 'th', label: '泰语' }, { value: 'el', label: '希腊语' }, { value: 'latin', label: '拉丁语系' }, { value: 'arabic', label: '阿拉伯语系' }, { value: 'east_slavic', label: '东斯拉夫语系' }, { value: 'cyrillic', label: '西里尔语系' }, { value: 'devanagari', label: '梵文字母语系' } ] // 文件上传处理 const handleFileUpload = async (files: FileList | null) => { console.log('handleFileUpload called with files:', files) if (!files || files.length === 0) return const validFiles: File[] = [] 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) } } uploadedFiles.value = validFiles console.log('Final uploaded files:', validFiles.map(f => f.name)) } // 清除所有数据 const clearAll = () => { uploadedFiles.value = [] results.value = null error.value = null } // 处理文档转换 const processDocument = async () => { if (uploadedFiles.value.length === 0) { error.value = '请先上传文件' return } isProcessing.value = true error.value = null try { const params: ParseParams = { files: uploadedFiles.value, output_dir: './output', lang_list: [config.language], backend: config.backend, parse_method: config.forceOcr ? 'ocr' : 'auto', formula_enable: config.formulaEnable, table_enable: config.tableEnable, start_page_id: 0, end_page_id: config.maxPages - 1, return_md: true, return_middle_json: false, response_format_zip: false } if (config.backend.includes('http-client') && config.serverUrl) { params.server_url = config.serverUrl } const response = await documentApi.parseDocument(params) if (response.results) { const resultData = Object.values(response.results)[0] const mdContent = resultData.md_content || '' const processedSource = autoPromoteParagraphsToSubheading(mdContent) results.value = { markdown: mdContent, source: processedSource, mindmap: processedSource } } } catch (err: any) { error.value = err.message || '转换失败' } finally { isProcessing.value = false } } // 根据后端类型获取公式标签 const getFormulaLabel = (backend: string) => { if (backend.startsWith('vlm')) { return '启用行间公式识别' } else if (backend === 'pipeline') { return '启用公式识别' } else if (backend.startsWith('hybrid')) { return '启用行内公式识别' } return '启用公式识别' } // 根据后端类型获取公式说明 const getFormulaInfo = (backend: string) => { if (backend.startsWith('vlm')) { return '禁用后,行间公式将显示为图片。' } else if (backend === 'pipeline') { return '禁用后,行间公式将显示为图片,行内公式将不会被检测或解析。' } else if (backend.startsWith('hybrid')) { return '禁用后,行内公式将不会被检测或解析。' } return '' } return { // 数据 uploadedFiles, config, results, isUploading, isProcessing, error, // 选项 backendOptions, languageOptions, // 方法 handleFileUpload, clearAll, processDocument, getFormulaLabel, getFormulaInfo } }