/** 正文引用标记,如 [1]、[1-7]、[4,6,8] */ const CITATION_BRACKET_RE = /(?:\s*)?\[([^\]]+)\](?:\s*<\/blue>)?/gi; function decodeHtmlEntities(text) { return String(text || '') .replace(/ /g, ' ') .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"'); } function escapeHtml(text) { return String(text || '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } export function htmlToPlainText(html) { const raw = String(html || ''); if (typeof document !== 'undefined') { const node = document.createElement('div'); node.innerHTML = raw; return (node.textContent || node.innerText || '').replace(/\u00a0/g, ' ').trim(); } return decodeHtmlEntities(raw.replace(//gi, '\n').replace(/<[^>]*>/g, '')).trim(); } export function expandCitationBracket(inner) { const result = []; const text = String(inner || '') .replace(/<[^>]+>/g, '') .trim(); if (!text) { return result; } text.split(/\s*,\s*/).forEach(function (part) { const token = part.trim(); const range = token.match(/^(\d+)\s*[\-–−]\s*(\d+)$/); if (range) { const a = Number(range[1]); const b = Number(range[2]); const lo = Math.min(a, b); const hi = Math.max(a, b); let i = 0; for (i = lo; i <= hi; i += 1) { result.push(i); } return; } if (/^\d+$/.test(token)) { result.push(Number(token)); } }); return result; } /** 从段落 HTML 提取引用编号(按出现顺序,段内去重) */ export function parseCitationNumbersFromHtml(html) { const nums = []; const seen = {}; const re = new RegExp(CITATION_BRACKET_RE.source, 'gi'); let match = null; while ((match = re.exec(String(html || ''))) !== null) { expandCitationBracket(match[1]).forEach(function (num) { if (!seen[num]) { seen[num] = true; nums.push(num); } }); } return nums; } export function stripHtmlTags(html) { return htmlToPlainText(html); } export function formatReferencePlainText(ref) { if (!ref) { return ''; } if (ref.refer_frag) { const frag = stripHtmlTags(ref.refer_frag); if (frag) { return frag; } } const parts = []; if (ref.author) { parts.push(stripHtmlTags(ref.author)); } if (ref.title) { parts.push(stripHtmlTags(ref.title)); } if (ref.joura) { parts.push(stripHtmlTags(ref.joura)); } if (ref.dateno) { parts.push(stripHtmlTags(ref.dateno)); } if (ref.doilink) { parts.push(String(ref.doilink).trim()); } if (ref.isbn) { parts.push('ISBN: ' + String(ref.isbn).trim()); } return parts.filter(Boolean).join('. ').replace(/\.\s*\./g, '.').trim(); } function isTextParagraphItem(item) { if (!item) { return false; } if (item.type == 1 || item.type == 2) { return false; } const html = String(item.content || item.text || '').trim(); return !!html; } function buildCitationEntry(citeNum, refList) { const ref = refList[citeNum - 1] || null; return { citeNum: citeNum, reference: ref, referenceText: formatReferencePlainText(ref), referenceMissing: !ref }; } export function formatCitationNumsLabel(citeNums) { return (citeNums || []).map(function (n) { return '[' + n + ']'; }).join(' '); } export function getCitationReviewGroupMeta(item, index, labels) { const l = labels || {}; const groupNo = index + 1; const paragraphNo = item.paragraphIndex + 1; const citeNumsLabel = formatCitationNumsLabel(item.citeNums); const citeNumsPlain = (item.citeNums || []).join('、'); let title = '正文段落 ' + paragraphNo + ' · ' + citeNumsLabel; if (l.groupLabel) { title = l.groupLabel .replace('{group}', groupNo) .replace('{paragraph}', paragraphNo) .replace('{refs}', citeNumsLabel); } return { id: 'cite-group-' + groupNo, groupNo: groupNo, paragraphNo: paragraphNo, citeNumsLabel: citeNumsLabel, citeNumsPlain: citeNumsPlain, title: title }; } /** * 构建核查队列:每个含引文的正文段落一组,该段全部参考文献一并询问; * 同一条文献出现在不同段落时,会在各段落中分别询问。 */ export function buildCitationReviewQueue(wordList, references) { const items = []; const refList = references || []; (wordList || []).forEach(function (item, paragraphIndex) { if (!isTextParagraphItem(item)) { return; } const html = item.content || item.text || ''; const citeNums = parseCitationNumbersFromHtml(html); if (!citeNums.length) { return; } const paragraphText = htmlToPlainText(html); const citations = citeNums.map(function (citeNum) { return buildCitationEntry(citeNum, refList); }); items.push({ paragraphIndex: paragraphIndex, amId: item.am_id, paragraphHtml: html, paragraphText: paragraphText, citeNums: citeNums.slice(), citations: citations, hasMissingReference: citations.some(function (c) { return c.referenceMissing; }) }); }); return items; } export function buildWenaiRelevancePrompt(item) { if (!item) { return ''; } const paragraphText = item.paragraphText || ''; const citations = item.citations || []; const citeLabels = citations.map(function (c) { return '[' + c.citeNum + ']'; }); let refBlock = ''; citations.forEach(function (c) { const text = c.referenceText || '(未找到该条参考文献)'; refBlock += '[' + c.citeNum + ']\n' + text + '\n\n'; }); const labelList = citeLabels.join('、') || ''; return ( '请审读以下正文段落及其引用文献,结合该段落的论述内容,判断各文献与正文的相关程度。\n\n' + '【正文段落】\n' + paragraphText + '\n\n' + '【参考文献】\n' + refBlock.trim() + '\n\n' + '【输出要求】\n' + '请以期刊编辑审稿批注的语气,为作者撰写一段文件批注(不要用邮件格式,不要称呼、落款、主题行或分条邮件体)。\n' + '请按文献序号 ' + labelList + ' 依次说明,保留原文序号且顺序连续,不要重新编号或跳号。\n' + '对每条文献分别给出关联判断:直接相关 / 强相关 / 弱相关 / 不相关,并用一句话简要说明理由。\n' + '将全部内容合并写成一段连贯的批注文字,语气客观、专业、克制,可直接作为稿件文件批注使用。' ); } /** 合并全部批次提示词 */ export function buildAllWenaiRelevancePrompts(reviewItems, labels) { return (reviewItems || []) .map(function (item, index) { const meta = getCitationReviewGroupMeta(item, index, labels); const header = '========== ' + meta.title + ' =========='; return header + '\n\n' + buildWenaiRelevancePrompt(item); }) .filter(Boolean) .join('\n\n\n'); } /** 生成可独立打开的 HTML 文档(含锚点导航与复制) */ export function buildCitationReviewStandaloneHtml(reviewItems, labels) { const l = labels || {}; const items = reviewItems || []; const total = items.length; const pageTitle = l.pageTitle || '引文相关性核查'; const groupTotalText = (l.groupTotal || '共 {n} 组').replace('{n}', total); const jumpTitle = l.jumpTitle || '快速跳转'; const copyGroupText = l.copyGroup || '复制本组'; const copyAllText = l.copyAll || '复制全部'; const copySuccessText = l.copySuccess || '已复制'; const copyFailText = l.copyFail || '复制失败'; let navHtml = ''; let bodyHtml = ''; let i = 0; for (i = 0; i < items.length; i += 1) { const item = items[i]; const meta = getCitationReviewGroupMeta(item, i, labels); const prompt = buildWenaiRelevancePrompt(item); const promptId = 'cite-prompt-' + meta.groupNo; navHtml += '' + escapeHtml(meta.title) + ''; bodyHtml += '
' + '
' + '

' + escapeHtml(meta.title) + '

' + '' + '
' + '
' +
            escapeHtml(prompt) +
            '
' + '
'; } const allPrompt = escapeHtml(buildAllWenaiRelevancePrompts(items, labels)); return ( '' + '' + '' + escapeHtml(pageTitle) + '' + '
' + '

' + escapeHtml(pageTitle) + '

' + '
' + escapeHtml(groupTotalText) + '
' + '
' + '
' + '' + bodyHtml + '
' +
        allPrompt +
        '
' + '