2026-04-04 23:56:18 +08:00
|
|
|
export const DOC_BLOCK_NODE_TYPE = 'doc_block'
|
|
|
|
|
export const DOC_BLOCK_FENCE_LANG = 'llm-file'
|
|
|
|
|
export const DOC_CONTEXT_LIMIT = 32 * 1024
|
|
|
|
|
|
|
|
|
|
const IMAGE_MD_RE = /!\[[^\]]*]\([^)]+\)/g
|
|
|
|
|
const IMAGE_HTML_RE = /<img\b[^>]*>/gi
|
|
|
|
|
const HEADER_SEPARATOR = '\n---\n'
|
|
|
|
|
|
|
|
|
|
export function normalizeDocType(value = '') {
|
|
|
|
|
const lower = String(value || '').trim().toLowerCase()
|
|
|
|
|
if (lower === 'txt' || lower === 'text' || lower === 'plain') return 'txt'
|
2026-04-05 11:40:56 +08:00
|
|
|
if (lower === 'json') return 'json'
|
|
|
|
|
if (lower === 'toml') return 'toml'
|
|
|
|
|
if (lower === 'yaml' || lower === 'yml') return 'yaml'
|
2026-04-04 23:56:18 +08:00
|
|
|
if (lower === 'doc' || lower === 'docx' || lower === 'word') return 'docx'
|
|
|
|
|
if (lower === 'ppt' || lower === 'pptx' || lower === 'powerpoint') return 'pptx'
|
|
|
|
|
if (lower === 'pdf') return 'pdf'
|
|
|
|
|
return 'txt'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getDocTypeFromFilename(name = '') {
|
|
|
|
|
const lower = String(name || '').toLowerCase()
|
|
|
|
|
if (lower.endsWith('.docx')) return 'docx'
|
|
|
|
|
if (lower.endsWith('.pptx')) return 'pptx'
|
|
|
|
|
if (lower.endsWith('.pdf')) return 'pdf'
|
2026-04-05 11:40:56 +08:00
|
|
|
if (lower.endsWith('.json')) return 'json'
|
|
|
|
|
if (lower.endsWith('.toml')) return 'toml'
|
|
|
|
|
if (lower.endsWith('.yaml') || lower.endsWith('.yml')) return 'yaml'
|
2026-04-04 23:56:18 +08:00
|
|
|
return 'txt'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function isSupportedDocFile(file) {
|
|
|
|
|
if (!file) return false
|
|
|
|
|
const name = String(file.name || '').toLowerCase()
|
|
|
|
|
const type = String(file.type || '').toLowerCase()
|
|
|
|
|
return (
|
|
|
|
|
name.endsWith('.txt') ||
|
2026-04-05 11:40:56 +08:00
|
|
|
name.endsWith('.json') ||
|
|
|
|
|
name.endsWith('.toml') ||
|
|
|
|
|
name.endsWith('.yaml') ||
|
|
|
|
|
name.endsWith('.yml') ||
|
2026-04-04 23:56:18 +08:00
|
|
|
name.endsWith('.docx') ||
|
|
|
|
|
name.endsWith('.pptx') ||
|
|
|
|
|
name.endsWith('.pdf') ||
|
|
|
|
|
type === 'text/plain' ||
|
2026-04-05 11:40:56 +08:00
|
|
|
type === 'application/json' ||
|
|
|
|
|
type === 'text/yaml' ||
|
|
|
|
|
type === 'text/x-yaml' ||
|
|
|
|
|
type === 'application/x-yaml' ||
|
2026-04-04 23:56:18 +08:00
|
|
|
type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ||
|
|
|
|
|
type === 'application/vnd.openxmlformats-officedocument.presentationml.presentation' ||
|
|
|
|
|
type === 'application/pdf'
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function sanitizeDocContent(markdown = '') {
|
|
|
|
|
return String(markdown || '')
|
|
|
|
|
.replace(/\r\n?/g, '\n')
|
|
|
|
|
.replace(IMAGE_MD_RE, '')
|
|
|
|
|
.replace(IMAGE_HTML_RE, '')
|
|
|
|
|
.replace(/\n{3,}/g, '\n\n')
|
|
|
|
|
.trim()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function quoteMeta(value = '') {
|
|
|
|
|
return JSON.stringify(String(value ?? ''))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function parseMetaLine(line = '') {
|
|
|
|
|
const idx = line.indexOf(':')
|
|
|
|
|
if (idx < 0) return null
|
|
|
|
|
const key = line.slice(0, idx).trim()
|
|
|
|
|
const rawValue = line.slice(idx + 1).trim()
|
|
|
|
|
if (!key) return null
|
|
|
|
|
try {
|
|
|
|
|
return [key, JSON.parse(rawValue)]
|
|
|
|
|
} catch {
|
|
|
|
|
return [key, rawValue]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function pickFence(content = '') {
|
|
|
|
|
const matches = String(content || '').match(/`{3,}/g) || []
|
|
|
|
|
const maxLen = matches.reduce((max, item) => Math.max(max, item.length), 2)
|
|
|
|
|
return '`'.repeat(maxLen + 1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function buildDocBlockValue(attrs = {}) {
|
|
|
|
|
const docType = normalizeDocType(attrs.docType)
|
|
|
|
|
const docName = String(attrs.docName || `document.${docType}`)
|
|
|
|
|
const uploadTime = String(attrs.uploadTime || new Date().toISOString())
|
|
|
|
|
const collapsed = Boolean(attrs.collapsed)
|
|
|
|
|
const content = sanitizeDocContent(attrs.content || '')
|
|
|
|
|
return [
|
|
|
|
|
`type: ${quoteMeta(docType)}`,
|
|
|
|
|
`name: ${quoteMeta(docName)}`,
|
|
|
|
|
`uploadTime: ${quoteMeta(uploadTime)}`,
|
|
|
|
|
`collapsed: ${collapsed ? 'true' : 'false'}`,
|
|
|
|
|
'---',
|
|
|
|
|
content,
|
|
|
|
|
].join('\n')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function parseDocBlockValue(raw = '') {
|
|
|
|
|
const normalized = String(raw || '').replace(/\r\n?/g, '\n')
|
|
|
|
|
const separatorIndex = normalized.indexOf(HEADER_SEPARATOR)
|
|
|
|
|
const headerText = separatorIndex >= 0 ? normalized.slice(0, separatorIndex) : ''
|
|
|
|
|
const bodyText = separatorIndex >= 0 ? normalized.slice(separatorIndex + HEADER_SEPARATOR.length) : normalized
|
|
|
|
|
const attrs = {
|
|
|
|
|
docType: 'txt',
|
|
|
|
|
docName: 'document.txt',
|
|
|
|
|
uploadTime: '',
|
|
|
|
|
collapsed: false,
|
|
|
|
|
content: sanitizeDocContent(bodyText),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const line of headerText.split('\n')) {
|
|
|
|
|
const parsed = parseMetaLine(line)
|
|
|
|
|
if (!parsed) continue
|
|
|
|
|
const [key, value] = parsed
|
|
|
|
|
if (key === 'type') attrs.docType = normalizeDocType(value)
|
|
|
|
|
if (key === 'name' && value) attrs.docName = String(value)
|
|
|
|
|
if (key === 'uploadTime' && value) attrs.uploadTime = String(value)
|
|
|
|
|
if (key === 'collapsed') attrs.collapsed = value === true || value === 'true'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!attrs.docName) attrs.docName = `document.${attrs.docType}`
|
|
|
|
|
return attrs
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function buildDocBlockMarkdown(attrs = {}) {
|
|
|
|
|
const value = buildDocBlockValue(attrs)
|
|
|
|
|
const fence = pickFence(value)
|
|
|
|
|
return `${fence}${DOC_BLOCK_FENCE_LANG}\n${value}\n${fence}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function buildDocContextFence(attrs = {}) {
|
|
|
|
|
const docType = normalizeDocType(attrs.docType)
|
|
|
|
|
const content = sanitizeDocContent(attrs.content || '')
|
|
|
|
|
const fence = pickFence(content)
|
|
|
|
|
return `${fence}${docType}\n${content}\n${fence}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function buildLegacyDocBlock(attrs = {}) {
|
|
|
|
|
const docType = normalizeDocType(attrs.docType)
|
|
|
|
|
const docName = String(attrs.docName || `document.${docType}`)
|
|
|
|
|
const uploadTime = String(attrs.uploadTime || new Date().toISOString())
|
|
|
|
|
const content = sanitizeDocContent(attrs.content || '')
|
|
|
|
|
return `<doc_type="${docType}" doc_name="${docName}" upload_time="${uploadTime}" collapsed="${Boolean(attrs.collapsed)}">\n${content}\n</doc_end>`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function parseLegacyDocBlock(raw = '') {
|
|
|
|
|
const match = String(raw || '').match(/^<doc_type="([^"]+)"\s+doc_name="([^"]+)"\s+upload_time="([^"]+)"(?:\s+collapsed="([^"]+)")?>\n?([\s\S]*?)\n?<\/doc_end>$/)
|
|
|
|
|
if (!match) return null
|
|
|
|
|
return {
|
|
|
|
|
docType: normalizeDocType(match[1]),
|
|
|
|
|
docName: match[2] || 'document.txt',
|
|
|
|
|
uploadTime: match[3] || '',
|
|
|
|
|
collapsed: match[4] === 'true',
|
|
|
|
|
content: sanitizeDocContent(match[5] || ''),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function transformDocBlockMarkdownForClipboard(markdown = '') {
|
|
|
|
|
const pattern = /(^|\n)(`{3,})llm-file[^\n]*\n([\s\S]*?)\n\2(?=\n|$)/g
|
|
|
|
|
const replacedFence = String(markdown || '').replace(pattern, (full, prefix, _fence, value) => {
|
|
|
|
|
const attrs = parseDocBlockValue(value)
|
|
|
|
|
return `${prefix}${buildDocContextFence(attrs)}`
|
|
|
|
|
})
|
|
|
|
|
return replacedFence.replace(/<doc_type="[^"]+"\s+doc_name="[^"]+"\s+upload_time="[^"]+"(?:\s+collapsed="[^"]+")?>[\s\S]*?<\/doc_end>/g, (full) => {
|
|
|
|
|
const attrs = parseLegacyDocBlock(full)
|
|
|
|
|
return attrs ? buildDocContextFence(attrs) : full
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function stripDocBlockMarkdown(markdown = '') {
|
|
|
|
|
const pattern = /(^|\n)(`{3,})llm-file[^\n]*\n[\s\S]*?\n\2(?=\n|$)/g
|
|
|
|
|
return String(markdown || '').replace(pattern, '$1').replace(/\n{3,}/g, '\n\n').trim()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function transformLegacyDocBlocksForExport(markdown = '') {
|
|
|
|
|
return String(markdown || '').replace(/<doc_type="[^"]+"\s+doc_name="[^"]+"\s+upload_time="[^"]+"(?:\s+collapsed="[^"]+")?>[\s\S]*?<\/doc_end>/g, (full) => {
|
|
|
|
|
const attrs = parseLegacyDocBlock(full)
|
|
|
|
|
return attrs ? buildDocBlockMarkdown(attrs) : full
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function transformSpecialDocBlocksToLegacy(markdown = '') {
|
|
|
|
|
const pattern = /(^|\n)(`{3,})llm-file[^\n]*\n([\s\S]*?)\n\2(?=\n|$)/g
|
|
|
|
|
return String(markdown || '').replace(pattern, (full, prefix, _fence, value) => {
|
|
|
|
|
const attrs = parseDocBlockValue(value)
|
|
|
|
|
return `${prefix}${buildLegacyDocBlock(attrs)}`
|
|
|
|
|
})
|
|
|
|
|
}
|