feat(copilot): enhance OCR handling with inline tags and document serializer

- Replace HTML comment OCR metadata with inline `<OCR:...>` tags
- Implement serializer-based markdown conversion for prefix/suffix content
- Add extractTextFromOCR utility function for text extraction
- Enable Table, Diagram, and ListCheck features in MilkdownEditor
- Add periodic debug logging for document state analysis
This commit is contained in:
2026-02-14 23:53:26 +08:00
parent 794fbf8493
commit 03bb21d5c6
5 changed files with 3496 additions and 225 deletions

View File

@@ -99,7 +99,7 @@
import { onMounted, onUnmounted, ref, computed } from 'vue'
import { replaceAll } from '@milkdown/kit/utils'
import { Crepe } from '@milkdown/crepe'
import { editorViewCtx } from '@milkdown/kit/core'
import { editorViewCtx, serializerCtx } from '@milkdown/kit/core'
import { copilotPlugin, copilotConfigCtx, copilotGhostMark, setCopilotEnabled, COPILOT_PLUGIN_KEY, SIZE_LIMIT, checkSizeLimit } from '../plugins/copilotPlugin'
import { fetchSuggestion } from '../utils/api.js'
import { DEBUG, OCR_URL } from '../utils/config.js'
@@ -124,6 +124,7 @@ const aiButtonLabel = computed(() => {
let crepe = null
let markdownSyncTimer = null
let debugLogTimer = null
const objectUrls = new Set()
const IMAGE_NODE_TYPES = new Set(['image', 'image-block', 'imageBlock'])
@@ -199,6 +200,57 @@ const scheduleMarkdownSync = () => {
}, 120)
}
const logDebugInfo = async () => {
if (!crepe) return
try {
const markdown = await crepe.getMarkdown()
crepe.editor.action((ctx) => {
const view = ctx.get(editorViewCtx)
const schema = view.state.schema
const { from, to } = view.state.selection
const serializer = ctx.get(serializerCtx)
let prefixMarkdown = '', suffixMarkdown = ''
try {
// Prefix: 使用 slice 创建文档节点
const prefixSlice = view.state.doc.slice(0, from)
if (prefixSlice.content.size > 0) {
const prefixDoc = schema.topNodeType.createAndFill(undefined, prefixSlice.content)
if (prefixDoc) {
prefixMarkdown = serializer(prefixDoc)
}
}
if (!prefixMarkdown) {
prefixMarkdown = view.state.doc.textBetween(0, from, '\n', '\n')
}
// Suffix
const suffixSlice = view.state.doc.slice(to)
if (suffixSlice.content.size > 0) {
const suffixDoc = schema.topNodeType.createAndFill(undefined, suffixSlice.content)
if (suffixDoc) {
suffixMarkdown = serializer(suffixDoc)
}
}
if (!suffixMarkdown) {
suffixMarkdown = view.state.doc.textBetween(to, view.state.doc.content.size, '\n', '\n')
}
} catch (e) {
console.error('[Debug] Serializer error:', e)
prefixMarkdown = view.state.doc.textBetween(0, from, '\n', '\n')
suffixMarkdown = view.state.doc.textBetween(to, view.state.doc.content.size, '\n', '\n')
}
console.log('[Debug] ===== Document State =====')
console.log('[Debug] PREFIX:', prefixMarkdown)
console.log('[Debug] SUFFIX:', suffixMarkdown)
console.log('[Debug] FULL MARKDOWN:', markdown)
console.log('[Debug] ==========================')
})
} catch (e) {
console.error('[Debug] Log failed:', e)
}
}
const clearCurrentSuggestion = (view) => {
const state = COPILOT_PLUGIN_KEY.getState(view.state)
if (state?.suggestion && state.from < state.to) {
@@ -261,6 +313,9 @@ onMounted(async () => {
features: {
[Crepe.Feature.Latex]: true,
[Crepe.Feature.ImageBlock]: true,
[Crepe.Feature.Table]: true,
[Crepe.Feature.Diagram]: true,
[Crepe.Feature.ListCheck]: true,
},
featureConfigs: {
[Crepe.Feature.Latex]: {
@@ -308,6 +363,7 @@ onMounted(async () => {
refreshSizeAndLimit(ctx)
})
scheduleMarkdownSync()
debugLogTimer = setInterval(logDebugInfo, 20000)
if (DEBUG) console.log('[Debug] Crepe editor created with copilot plugin')
})
@@ -418,6 +474,10 @@ onUnmounted(() => {
clearTimeout(markdownSyncTimer)
markdownSyncTimer = null
}
if (debugLogTimer) {
clearInterval(debugLogTimer)
debugLogTimer = null
}
for (const url of Array.from(objectUrls)) {
revokeObjectUrl(url)