diff --git a/backend/main.py b/backend/main.py index a23c3cc..a943686 100644 --- a/backend/main.py +++ b/backend/main.py @@ -2,6 +2,8 @@ import asyncio import base64 import json import logging +import os +import tempfile import uuid from typing import Optional @@ -14,6 +16,7 @@ from pydantic import BaseModel from geoip import get_ip_location_text from llm import call_ollama, call_vlm_ocr from prompt import build_completion_prompts, prepare_prompt_context +import markitdown logging.basicConfig( level=logging.INFO, @@ -73,6 +76,11 @@ class OCRRequest(BaseModel): language: str = "auto" +class ConvertRequest(BaseModel): + file: str + filename: str = "document.pdf" + + def _preview(text: str, limit: int = 80) -> str: value = (text or "").replace("\n", "\\n") if len(value) <= limit: @@ -243,6 +251,58 @@ async def ocr_image(request: OCRRequest, api_key: str = Security(get_api_key)): return JSONResponse(content={"error": str(e)}, status_code=500) +@app.post("/v1/convert") +async def convert_to_markdown(request: ConvertRequest, api_key: str = Security(get_api_key)): + """将文件转换为Markdown格式""" + request_id = str(uuid.uuid4())[:8] + + try: + logger.info( + "[%s] /v1/convert filename=%s file_base64_chars=%d", + request_id, + request.filename, + len(request.file or ""), + ) + + # 解码Base64文件内容 + file_bytes = base64.b64decode(request.file) + logger.info("[%s] /v1/convert decoded file_bytes=%d", request_id, len(file_bytes)) + + # 获取文件扩展名 + ext = os.path.splitext(request.filename)[1].lower() + + # 创建临时文件 + with tempfile.NamedTemporaryFile(delete=False, suffix=ext) as tmp: + tmp.write(file_bytes) + tmp_path = tmp.name + + try: + # 使用MarkItDown转换为Markdown + md = markitdown.MarkItDown() + result = md.convert(tmp_path) + markdown_text = result.text_content + + logger.info( + "[%s] /v1/convert success text_chars=%d text_preview='%s'", + request_id, + len(markdown_text or ""), + _preview(markdown_text, 120), + ) + + return { + "markdown": markdown_text, + "filename": request.filename + } + finally: + # 清理临时文件 + if os.path.exists(tmp_path): + os.unlink(tmp_path) + + except Exception as e: + logger.exception("[%s] /v1/convert failed: %s", request_id, e) + return JSONResponse(content={"error": str(e)}, status_code=500) + + if __name__ == "__main__": import uvicorn diff --git a/backend/requirements.txt b/backend/requirements.txt index 2a2f806..86035db 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -5,3 +5,8 @@ pydantic python-dotenv httpx geoip2 +markitdown +python-docx +python-pptx +openpyxl +pypdf diff --git a/package-lock.json b/package-lock.json index 72d01ac..a82cccf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -424,7 +424,6 @@ "version": "6.12.1", "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.1.tgz", "integrity": "sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ==", - "peer": true, "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", @@ -496,7 +495,6 @@ "version": "6.5.4", "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.4.tgz", "integrity": "sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==", - "peer": true, "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } @@ -516,7 +514,6 @@ "version": "6.39.11", "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.11.tgz", "integrity": "sha512-bWdeR8gWM87l4DB/kYSF9A+dVackzDb/V56Tq7QVrQ7rn86W0rgZFtlL3g3pem6AeGcb9NQNoy3ao4WpW4h5tQ==", - "peer": true, "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", @@ -3073,7 +3070,6 @@ "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.1.1.tgz", "integrity": "sha512-f0yv5CPKaFxfsPTBzX7vGuim4oIC1/gcS7LUGdBSwl2dU6+FON6LVUksdOo1qJjoUvXNn45urgh8C+0a24pACQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@chevrotain/cst-dts-gen": "11.1.1", "@chevrotain/gast": "11.1.1", @@ -3402,7 +3398,6 @@ "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz", "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10" } @@ -3824,7 +3819,6 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -6578,7 +6572,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -6794,7 +6787,6 @@ "version": "1.25.4", "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz", "integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==", - "peer": true, "dependencies": { "orderedmap": "^2.0.0" } @@ -6825,7 +6817,6 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz", "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==", - "peer": true, "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-transform": "^1.0.0", @@ -6856,7 +6847,6 @@ "version": "1.41.5", "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.5.tgz", "integrity": "sha512-UDQbIPnDrjE8tqUBbPmCOZgtd75htE6W3r0JCmY9bL6W1iemDM37MZEKC49d+tdQ0v/CKx4gjxLoLsfkD2NiZA==", - "peer": true, "dependencies": { "prosemirror-model": "^1.20.0", "prosemirror-state": "^1.0.0", @@ -8498,7 +8488,6 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -8622,7 +8611,6 @@ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.26.tgz", "integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==", "license": "MIT", - "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.26", "@vue/compiler-sfc": "3.5.26", diff --git a/src/components/MilkdownEditor.vue b/src/components/MilkdownEditor.vue index 0e5e36a..14eeade 100644 --- a/src/components/MilkdownEditor.vue +++ b/src/components/MilkdownEditor.vue @@ -48,27 +48,20 @@ -
([\s\S]*?)<\/code><\/pre>/g,
- '$2
')
-
- // Process remote images
- html = html.replace(/
]*)>/g, (match, src, attrs) => {
- return `
`
- })
-
- return html
-}
-
-// 瀵煎嚭涓?DOCX (浣跨敤 HTML 鏍煎紡锛學ord 鍙互鐩存帴鎵撳紑)
-const exportToDocx = async () => {
- if (!crepe) return
-
- showExportDropdown.value = false
-
- try {
- const markdown = await crepe.getMarkdown()
- const html = await markdownToHtml(markdown)
-
- // 鍖呰 HTML 涓哄畬鏁存枃妗o紝娣诲姞 Word 鍏煎鐨勫厓鏁版嵁
- const fullHtml = `
-
-
-
- Document
-
-
-
-
-${html}
-
-`
-
- // 浣跨敤 HTML 鏍煎紡淇濆瓨涓?.doc锛學ord 鍙互鐩存帴鎵撳紑
- const blob = new Blob([fullHtml], { type: 'application/msword' })
- const url = URL.createObjectURL(blob)
- const a = document.createElement('a')
- const now = new Date()
- const pad = (n) => String(n).padStart(2, '0')
- const datePart = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}`
- const timePart = `${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`
- a.href = url
- a.download = `document${datePart}${timePart}.doc`
- document.body.appendChild(a)
- a.click()
- a.remove()
- URL.revokeObjectURL(url)
- } catch (error) {
- console.error('[Export DOCX] Error:', error)
- alert('瀵煎嚭 DOCX 澶辫触锛岃閲嶈瘯')
- }
-}
-
-// 瀵煎嚭涓?PDF
-const exportToPdf = async () => {
- if (!crepe) return
-
- showExportDropdown.value = false
-
- try {
- const markdown = await crepe.getMarkdown()
- const html = await markdownToHtml(markdown)
-
- const fullHtml = `
-
-
-
- Document
-
-
-
-${html}
-
-`
-
- // 閰嶇疆 PDF 閫夐」
- const options = {
- margin: [10, 10, 10, 10],
- filename: `document${new Date().toISOString().slice(0, 10)}.pdf`,
- image: { type: 'jpeg', quality: 0.98 },
- html2canvas: { scale: 2, useCORS: true, backgroundColor: '#ffffff', windowWidth: 1200 },
- pagebreak: { mode: ['css', 'legacy'] },
- jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
- }
-
- // 鐢熸垚 PDF
- await html2pdf().set(options).from(fullHtml, 'string').save()
- } catch (error) {
- console.error('[Export PDF] Error:', error)
- alert('瀵煎嚭 PDF 澶辫触锛岃閲嶈瘯')
- }
-}
-
const triggerUpload = () => {
fileInputRef.value?.click()
}
@@ -992,10 +559,6 @@ const toggleAI = async () => {
})
}
-const toggleExportDropdown = () => {
- showExportDropdown.value = !showExportDropdown.value
-}
-
const toggleImageDropdown = () => {
showImageDropdown.value = !showImageDropdown.value
}
@@ -1067,21 +630,6 @@ onUnmounted(() => {
rootResizeObserver = null
}
- if (mermaidResizeTimer) {
- clearTimeout(mermaidResizeTimer)
- mermaidResizeTimer = null
- }
-
- if (themeObserver) {
- themeObserver.disconnect()
- themeObserver = null
- }
-
- if (root.value) {
- root.value.removeEventListener('click', handleMermaidAction, true)
- }
- window.removeEventListener('resize', handleMermaidViewportResize)
-
for (const url of Array.from(objectUrls)) {
revokeObjectUrl(url)
}
@@ -1243,40 +791,6 @@ onUnmounted(() => {
opacity: 1;
}
-.export-btn-wrapper {
- position: relative;
-}
-
-.export-dropdown {
- position: absolute;
- bottom: 100%;
- right: 0;
- margin-bottom: 8px;
- background: var(--panel-bg);
- border: 1px solid var(--panel-border);
- border-radius: 8px;
- box-shadow: var(--panel-shadow);
- overflow: hidden;
- z-index: 10000;
- min-width: 160px;
-}
-
-.export-dropdown button {
- display: block;
- width: 100%;
- padding: 10px 16px;
- border: none;
- background: none;
- text-align: left;
- cursor: pointer;
- font-size: 14px;
- color: var(--app-text);
-}
-
-.export-dropdown button:hover {
- background: var(--crepe-color-hover);
-}
-
.image-btn-wrapper {
position: relative;
}
@@ -1386,60 +900,6 @@ onUnmounted(() => {
filter: brightness(0.92);
}
-.mermaid-preview-overlay {
- position: fixed;
- inset: 0;
- z-index: 10002;
- background: var(--overlay-bg);
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 16px;
-}
-
-.mermaid-preview-dialog {
- width: min(96vw, 1280px);
- max-height: min(92vh, 920px);
- background: var(--panel-bg);
- border: 1px solid var(--panel-border);
- border-radius: 12px;
- box-shadow: var(--panel-shadow);
- display: flex;
- flex-direction: column;
- position: relative;
-}
-
-.mermaid-preview-close {
- position: absolute;
- top: 8px;
- right: 8px;
- width: 34px;
- height: 34px;
- border: 1px solid var(--panel-border);
- border-radius: 8px;
- background: var(--btn-bg);
- color: var(--btn-fg);
- cursor: pointer;
- z-index: 1;
-}
-
-.mermaid-preview-close:hover {
- background: var(--btn-hover-bg);
- color: var(--btn-hover-fg);
- border-color: var(--btn-hover-bg);
-}
-
-.mermaid-preview-scroll {
- overflow: auto;
- padding: 40px 16px 16px;
-}
-
-.mermaid-preview-scroll img {
- display: block;
- max-width: none;
- margin: 0 auto;
-}
-
.milkdown-editor {
--editor-tail-space: calc(100vh - 32px);
width: 100%;
@@ -1477,14 +937,10 @@ onUnmounted(() => {
}
.milkdown-editor :deep(.ProseMirror img) {
- max-width: 80%;
+ max-width: 60%;
height: auto;
}
-.milkdown-editor :deep(.ProseMirror .mermaid-image) {
- max-width: none !important;
-}
-
.milkdown-editor :deep(.ProseMirror > *:first-child) {
margin-top: 0 !important;
}
@@ -1590,6 +1046,3 @@ onUnmounted(() => {
}
-
-
-
diff --git a/src/stores/settings.js b/src/stores/settings.js
index 727609e..72b0c96 100644
--- a/src/stores/settings.js
+++ b/src/stores/settings.js
@@ -18,11 +18,9 @@ export const useSettingsStore = defineStore('settings', () => {
// 4. Preferences
const language = ref('auto')
const currency = ref('auto')
- // const timezone = ref('auto') // removed
// 5. Background
const backgroundType = ref('default') // 'default' | 'warm' | 'reading' | 'image'
- // const backgroundColor = ref('#ffffff') // removed
const backgroundImage = ref('')
const backgroundOpacity = ref(0.2) // 0.05 - 0.50
@@ -61,13 +59,10 @@ export const useSettingsStore = defineStore('settings', () => {
if (typeof data.privacyMode === 'boolean') privacyMode.value = data.privacyMode
if (data.language) language.value = data.language
if (data.currency) currency.value = data.currency
- // if (data.timezone) timezone.value = data.timezone // timezone legacy ignore
if (data.backgroundType) {
- // migrate color to default if needed, or mapped
if (data.backgroundType === 'color') backgroundType.value = 'default'
else backgroundType.value = data.backgroundType
}
- // if (data.backgroundColor) backgroundColor.value = data.backgroundColor // removed
if (data.backgroundImage) backgroundImage.value = data.backgroundImage
if (data.backgroundOpacity) backgroundOpacity.value = data.backgroundOpacity
}
@@ -86,9 +81,7 @@ export const useSettingsStore = defineStore('settings', () => {
privacyMode: privacyMode.value,
language: language.value,
currency: currency.value,
- // timezone: timezone.value, // removed
backgroundType: backgroundType.value,
- // backgroundColor: backgroundColor.value, // removed
backgroundImage: backgroundImage.value,
backgroundOpacity: backgroundOpacity.value,
}
@@ -106,9 +99,7 @@ export const useSettingsStore = defineStore('settings', () => {
privacyMode.value = false
language.value = 'auto'
currency.value = 'auto'
- // timezone.value = 'auto' // removed
backgroundType.value = 'default'
- // backgroundColor.value = '#ffffff' // removed
backgroundImage.value = ''
backgroundOpacity.value = 0.2
saveSettings()
@@ -123,9 +114,7 @@ export const useSettingsStore = defineStore('settings', () => {
privacyMode,
language,
currency,
- // timezone, // removed
backgroundType,
- // backgroundColor, // removed
backgroundImage,
backgroundOpacity,
],
@@ -144,9 +133,7 @@ export const useSettingsStore = defineStore('settings', () => {
privacyMode,
language,
currency,
- // timezone, // removed
backgroundType,
- // backgroundColor, // removed
backgroundImage,
backgroundOpacity,
uiLanguage,
diff --git a/src/utils/config.js b/src/utils/config.js
index d439e94..ff28843 100644
--- a/src/utils/config.js
+++ b/src/utils/config.js
@@ -1,5 +1,3 @@
-export const DEBUG = import.meta.env.DEV
-
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'https://api.imageteach.tech:8002'
export const API_URL = import.meta.env.VITE_API_URL || `${API_BASE_URL}/v1/completions`