feat(config): add OCR URL configuration and improve image node handling
- Add VITE_OCR_URL environment variable with fallback URL construction - Define IMAGE_NODE_TYPES constant to support 'image', 'image-block', and 'imageBlock' node types - Add helper functions for safer image attribute access (getImageSrc, isImageNodeWithSrc, getImageLabel) - Improve OCR error handling with HTTP status checking and error details - Wrap OCR context in HTML comments to prevent prompt injection issues - Update MilkdownEditor to use centralized OCR_URL configuration
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
VITE_API_URL=http://localhost:8000/v1/completions
|
VITE_API_URL=http://localhost:8000/v1/completions
|
||||||
|
VITE_OCR_URL=http://localhost:8000/v1/ocr
|
||||||
|
|
||||||
# Ollama 配置
|
# Ollama 配置
|
||||||
OLLAMA_HOST=http://192.168.0.120:11434
|
OLLAMA_HOST=http://192.168.0.120:11434
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ import { Crepe } from '@milkdown/crepe'
|
|||||||
import { editorViewCtx } from '@milkdown/kit/core'
|
import { editorViewCtx } from '@milkdown/kit/core'
|
||||||
import { copilotPlugin, copilotConfigCtx, copilotGhostMark, setCopilotEnabled, COPILOT_PLUGIN_KEY, SIZE_LIMIT, checkSizeLimit } from '../plugins/copilotPlugin'
|
import { copilotPlugin, copilotConfigCtx, copilotGhostMark, setCopilotEnabled, COPILOT_PLUGIN_KEY, SIZE_LIMIT, checkSizeLimit } from '../plugins/copilotPlugin'
|
||||||
import { fetchSuggestion } from '../utils/api.js'
|
import { fetchSuggestion } from '../utils/api.js'
|
||||||
import { DEBUG, API_URL } from '../utils/config.js'
|
import { DEBUG, OCR_URL } from '../utils/config.js'
|
||||||
import { setOcrCache, clearOcrCache, clearAllOcrCache } from '../utils/ocrCache.js'
|
import { setOcrCache, clearOcrCache, clearAllOcrCache } from '../utils/ocrCache.js'
|
||||||
|
|
||||||
const emit = defineEmits(['update:markdown'])
|
const emit = defineEmits(['update:markdown'])
|
||||||
@@ -125,6 +125,7 @@ const aiButtonLabel = computed(() => {
|
|||||||
let crepe = null
|
let crepe = null
|
||||||
let markdownSyncTimer = null
|
let markdownSyncTimer = null
|
||||||
const objectUrls = new Set()
|
const objectUrls = new Set()
|
||||||
|
const IMAGE_NODE_TYPES = new Set(['image', 'image-block', 'imageBlock'])
|
||||||
|
|
||||||
const revokeObjectUrl = (url) => {
|
const revokeObjectUrl = (url) => {
|
||||||
if (!objectUrls.has(url)) return
|
if (!objectUrls.has(url)) return
|
||||||
@@ -136,12 +137,12 @@ const revokeObjectUrl = (url) => {
|
|||||||
const collectImageObjectUrls = (doc) => {
|
const collectImageObjectUrls = (doc) => {
|
||||||
const activeUrls = new Set()
|
const activeUrls = new Set()
|
||||||
doc.descendants((node) => {
|
doc.descendants((node) => {
|
||||||
|
const src = typeof node.attrs?.src === 'string' ? node.attrs.src : ''
|
||||||
if (
|
if (
|
||||||
node.type?.name === 'image' &&
|
IMAGE_NODE_TYPES.has(node.type?.name) &&
|
||||||
typeof node.attrs?.src === 'string' &&
|
src.startsWith('blob:')
|
||||||
node.attrs.src.startsWith('blob:')
|
|
||||||
) {
|
) {
|
||||||
activeUrls.add(node.attrs.src)
|
activeUrls.add(src)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return activeUrls
|
return activeUrls
|
||||||
@@ -219,8 +220,7 @@ const performOCR = async (file, cacheKey) => {
|
|||||||
|
|
||||||
const base64 = dataUrl.slice(splitIndex + 1)
|
const base64 = dataUrl.slice(splitIndex + 1)
|
||||||
try {
|
try {
|
||||||
const ocrUrl = API_URL.replace('/v1/completions', '/v1/ocr')
|
const res = await fetch(OCR_URL, {
|
||||||
const res = await fetch(ocrUrl, {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -229,6 +229,10 @@ const performOCR = async (file, cacheKey) => {
|
|||||||
language: 'auto'
|
language: 'auto'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
if (!res.ok) {
|
||||||
|
const errorText = await res.text()
|
||||||
|
throw new Error(`HTTP ${res.status}: ${errorText}`)
|
||||||
|
}
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
if (data.text) {
|
if (data.text) {
|
||||||
setOcrCache(cacheKey, data.text)
|
setOcrCache(cacheKey, data.text)
|
||||||
@@ -240,7 +244,7 @@ const performOCR = async (file, cacheKey) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (DEBUG) console.error('[OCR] Error:', e)
|
console.error('[OCR] Error:', e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
reader.readAsDataURL(file)
|
reader.readAsDataURL(file)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { getOcrCache, checkSizeLimit as checkOcrSizeLimit, OCR_SIZE_LIMIT } from
|
|||||||
const COPILOT_PLUGIN_KEY = new PluginKey('milkdown-copilot')
|
const COPILOT_PLUGIN_KEY = new PluginKey('milkdown-copilot')
|
||||||
const DEBOUNCE_MS = 1000
|
const DEBOUNCE_MS = 1000
|
||||||
const SIZE_LIMIT = OCR_SIZE_LIMIT
|
const SIZE_LIMIT = OCR_SIZE_LIMIT
|
||||||
|
const IMAGE_NODE_TYPES = new Set(['image', 'image-block', 'imageBlock'])
|
||||||
|
|
||||||
interface CopilotState {
|
interface CopilotState {
|
||||||
from: number
|
from: number
|
||||||
@@ -251,11 +252,28 @@ function insertPlainText(view: EditorView, suggestion: string, from: number, mar
|
|||||||
view.dispatch(tr)
|
view.dispatch(tr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getImageSrc(node: ProseNode): string {
|
||||||
|
const src = node.attrs?.src
|
||||||
|
return typeof src === 'string' ? src : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function isImageNodeWithSrc(node: ProseNode): boolean {
|
||||||
|
return IMAGE_NODE_TYPES.has(node.type.name) && Boolean(getImageSrc(node))
|
||||||
|
}
|
||||||
|
|
||||||
|
function getImageLabel(node: ProseNode): string {
|
||||||
|
const candidates = [node.attrs?.alt, node.attrs?.title, node.attrs?.caption]
|
||||||
|
for (const value of candidates) {
|
||||||
|
if (typeof value === 'string' && value.trim()) return value.trim()
|
||||||
|
}
|
||||||
|
return 'untitled'
|
||||||
|
}
|
||||||
|
|
||||||
function extractImageFilenames(doc: ProseNode): string[] {
|
function extractImageFilenames(doc: ProseNode): string[] {
|
||||||
const filenames: string[] = []
|
const filenames: string[] = []
|
||||||
doc.descendants((node: ProseNode) => {
|
doc.descendants((node: ProseNode) => {
|
||||||
if (node.type.name === 'image' && node.attrs.src) {
|
if (isImageNodeWithSrc(node)) {
|
||||||
filenames.push(node.attrs.src)
|
filenames.push(getImageSrc(node))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return filenames
|
return filenames
|
||||||
@@ -266,18 +284,22 @@ function buildPrefixWithOCR(prefix: string, doc: ProseNode, cursorPos: number):
|
|||||||
|
|
||||||
doc.descendants((node: ProseNode, pos) => {
|
doc.descendants((node: ProseNode, pos) => {
|
||||||
if (pos >= cursorPos) return false
|
if (pos >= cursorPos) return false
|
||||||
if (node.type.name !== 'image' || !node.attrs.src) return true
|
if (!isImageNodeWithSrc(node)) return true
|
||||||
|
|
||||||
const ocrText = getOcrCache(node.attrs.src)
|
const src = getImageSrc(node)
|
||||||
|
const ocrText = getOcrCache(src)
|
||||||
if (!ocrText) return true
|
if (!ocrText) return true
|
||||||
|
|
||||||
const altText = typeof node.attrs.alt === 'string' ? node.attrs.alt : ''
|
const label = getImageLabel(node)
|
||||||
ocrEntries.push(`image(${altText || 'untitled'}): ${ocrText}`)
|
const safeOcrText = ocrText.replace(/<!--|-->/g, '').trim()
|
||||||
|
if (!safeOcrText) return true
|
||||||
|
|
||||||
|
ocrEntries.push(`image(${label}): ${safeOcrText}`)
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!ocrEntries.length) return prefix
|
if (!ocrEntries.length) return prefix
|
||||||
return `${prefix}\n\n[OCR Context]\n${ocrEntries.join('\n')}`
|
return `${prefix}\n\n<!--OCR:\n${ocrEntries.join('\n')}\n-->`
|
||||||
}
|
}
|
||||||
|
|
||||||
function doFetchSuggestion(view: EditorView, runtime: CopilotRuntime, pos: number, prefix: string, suffix: string) {
|
function doFetchSuggestion(view: EditorView, runtime: CopilotRuntime, pos: number, prefix: string, suffix: string) {
|
||||||
|
|||||||
@@ -1,2 +1,18 @@
|
|||||||
export const DEBUG = import.meta.env.DEV
|
export const DEBUG = import.meta.env.DEV
|
||||||
export const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000/v1/completions'
|
export const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000/v1/completions'
|
||||||
|
|
||||||
|
const buildDefaultOcrUrl = (apiUrl) => {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return 'http://localhost:8000/v1/ocr'
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = new URL(apiUrl, window.location.origin)
|
||||||
|
url.pathname = '/v1/ocr'
|
||||||
|
return url.toString()
|
||||||
|
} catch {
|
||||||
|
return 'http://localhost:8000/v1/ocr'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OCR_URL = import.meta.env.VITE_OCR_URL || buildDefaultOcrUrl(API_URL)
|
||||||
|
|||||||
Reference in New Issue
Block a user