feat(web): 工作台 AI 模式报销预审与文档查询模型拆分
- 新增 aiApplicationPrecheckModel/aiDocumentQueryModel/aiApplicationPreviewActions/aiConversationHtmlRenderer 四个独立模型与服务,按职责从主组件拆出 - PersonalWorkbenchAiMode 接入拆分后的预审、文档查询与 HTML 渲染逻辑,配合 markdown 工具增强结构化展示 - 文档中心与归档筛选、风险可见性、申请预览等工具同步适配,补充对应单元测试 - 新增 AI 文档卡片背景资源
This commit is contained in:
@@ -25,6 +25,25 @@ const ACTION_LINK_CLASS_BY_HREF = {
|
||||
'#review-quick-edit': 'markdown-action-link-edit',
|
||||
'#review-risk-panel': 'markdown-action-link-risk'
|
||||
}
|
||||
const DOCUMENT_DETAIL_HREF_PREFIX = '#ai-open-document-detail:'
|
||||
const TRUSTED_HTML_BLOCK_RE = /<!--\s*ai-trusted-html:start\s*-->\s*([\s\S]*?)\s*<!--\s*ai-trusted-html:end\s*-->/g
|
||||
const TRUSTED_HTML_PLACEHOLDER_PREFIX = 'AI_TRUSTED_HTML_BLOCK_'
|
||||
const TRUSTED_HTML_ALLOWED_TAGS = new Set([
|
||||
'section',
|
||||
'article',
|
||||
'header',
|
||||
'footer',
|
||||
'div',
|
||||
'span',
|
||||
'strong',
|
||||
'a'
|
||||
])
|
||||
const TRUSTED_HTML_ALLOWED_ATTRS = new Set([
|
||||
'aria-label',
|
||||
'class',
|
||||
'data-ai-action',
|
||||
'href'
|
||||
])
|
||||
|
||||
function escapeHtml(text) {
|
||||
return String(text || '')
|
||||
@@ -43,6 +62,9 @@ function renderRiskText(text) {
|
||||
|
||||
function resolveActionLinkClass(href) {
|
||||
const normalizedHref = String(href || '').trim()
|
||||
if (normalizedHref.startsWith(DOCUMENT_DETAIL_HREF_PREFIX)) {
|
||||
return 'markdown-action-link-document'
|
||||
}
|
||||
return ACTION_LINK_CLASS_BY_HREF[normalizedHref] || ''
|
||||
}
|
||||
|
||||
@@ -214,7 +236,76 @@ function normalizeColonHeadings(text) {
|
||||
return normalizedLines.join('\n').replace(/\n{3,}/g, '\n\n')
|
||||
}
|
||||
|
||||
export function renderMarkdown(text = '') {
|
||||
const normalized = normalizeColonHeadings(text).trim()
|
||||
return normalized ? markdown.render(normalized) : ''
|
||||
function hasOnlyTrustedHtmlTags(html = '') {
|
||||
const tagPattern = /<\/?([a-z][\w-]*)([^>]*)>/gi
|
||||
let match = tagPattern.exec(html)
|
||||
while (match) {
|
||||
const tagName = String(match[1] || '').toLowerCase()
|
||||
if (!TRUSTED_HTML_ALLOWED_TAGS.has(tagName)) {
|
||||
return false
|
||||
}
|
||||
const attrText = String(match[2] || '')
|
||||
const attrPattern = /\s([:@\w-]+)\s*=/g
|
||||
let attrMatch = attrPattern.exec(attrText)
|
||||
while (attrMatch) {
|
||||
const attrName = String(attrMatch[1] || '').toLowerCase()
|
||||
if (!TRUSTED_HTML_ALLOWED_ATTRS.has(attrName)) {
|
||||
return false
|
||||
}
|
||||
attrMatch = attrPattern.exec(attrText)
|
||||
}
|
||||
match = tagPattern.exec(html)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function sanitizeTrustedHtmlBlock(html = '') {
|
||||
const value = String(html || '').trim()
|
||||
if (!value || !value.includes('class="ai-document-card-list"')) {
|
||||
return ''
|
||||
}
|
||||
if (/<(?:script|style|iframe|object|embed|link|meta|form|input|button|textarea|select)\b/i.test(value)) {
|
||||
return ''
|
||||
}
|
||||
if (/\son[a-z]+\s*=/i.test(value) || /javascript\s*:/i.test(value)) {
|
||||
return ''
|
||||
}
|
||||
if (!hasOnlyTrustedHtmlTags(value)) {
|
||||
return ''
|
||||
}
|
||||
const hrefs = [...value.matchAll(/\shref="([^"]*)"/gi)].map((match) => String(match[1] || '').trim())
|
||||
if (hrefs.some((href) => !href.startsWith(DOCUMENT_DETAIL_HREF_PREFIX))) {
|
||||
return ''
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
function extractTrustedHtmlBlocks(text = '') {
|
||||
const trustedHtmlBlocks = []
|
||||
const content = String(text || '').replace(TRUSTED_HTML_BLOCK_RE, (_match, html) => {
|
||||
const sanitizedHtml = sanitizeTrustedHtmlBlock(html)
|
||||
if (!sanitizedHtml) {
|
||||
return ''
|
||||
}
|
||||
const placeholder = `${TRUSTED_HTML_PLACEHOLDER_PREFIX}${trustedHtmlBlocks.length}`
|
||||
trustedHtmlBlocks.push(sanitizedHtml)
|
||||
return `\n\n${placeholder}\n\n`
|
||||
})
|
||||
return { content, trustedHtmlBlocks }
|
||||
}
|
||||
|
||||
function restoreTrustedHtmlBlocks(html = '', trustedHtmlBlocks = []) {
|
||||
return trustedHtmlBlocks.reduce((nextHtml, block, index) => {
|
||||
const placeholder = `${TRUSTED_HTML_PLACEHOLDER_PREFIX}${index}`
|
||||
const paragraphPattern = new RegExp(`<p>${placeholder}</p>\\n?`, 'g')
|
||||
return nextHtml
|
||||
.replace(paragraphPattern, block)
|
||||
.replaceAll(placeholder, block)
|
||||
}, html)
|
||||
}
|
||||
|
||||
export function renderMarkdown(text = '') {
|
||||
const { content, trustedHtmlBlocks } = extractTrustedHtmlBlocks(text)
|
||||
const normalized = normalizeColonHeadings(content).trim()
|
||||
return normalized ? restoreTrustedHtmlBlocks(markdown.render(normalized), trustedHtmlBlocks) : ''
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user