添加期刊引用分析

This commit is contained in:
2026-06-17 09:35:15 +08:00
parent ec4a59eedb
commit ea5695f913
36 changed files with 6129 additions and 103 deletions

View File

@@ -0,0 +1,406 @@
/** 正文引用标记,如 [1]、[1-7]、<blue>[4,6,8]</blue> */
const CITATION_BRACKET_RE = /(?:<blue>\s*)?\[([^\]]+)\](?:\s*<\/blue>)?/gi;
function decodeHtmlEntities(text) {
return String(text || '')
.replace(/&nbsp;/g, ' ')
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"');
}
function escapeHtml(text) {
return String(text || '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
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(/<br\s*\/?>/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 +=
'<a class="nav-link" href="#' +
meta.id +
'">' +
escapeHtml(meta.title) +
'</a>';
bodyHtml +=
'<section class="group-card" id="' +
meta.id +
'">' +
'<div class="group-head">' +
'<h3>' +
escapeHtml(meta.title) +
'</h3>' +
'<button type="button" class="copy-btn" data-target="' +
promptId +
'">' +
escapeHtml(copyGroupText) +
'</button>' +
'</div>' +
'<pre class="group-prompt" id="' +
promptId +
'">' +
escapeHtml(prompt) +
'</pre>' +
'</section>';
}
const allPrompt = escapeHtml(buildAllWenaiRelevancePrompts(items, labels));
return (
'<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8">' +
'<meta name="viewport" content="width=device-width,initial-scale=1">' +
'<title>' +
escapeHtml(pageTitle) +
'</title>' +
'<style>' +
'*{box-sizing:border-box}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;background:#f5f7fa;color:#303133;line-height:1.5}' +
'.page{max-width:960px;margin:0 auto;padding:20px}' +
'.header{background:#fff;border-radius:8px;padding:16px 20px;margin-bottom:12px;box-shadow:0 1px 4px rgba(0,0,0,.08)}' +
'.header h1{margin:0 0 8px;font-size:20px}' +
'.summary{color:#606266;font-size:14px;margin-bottom:12px}' +
'.actions{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:12px}' +
'.btn{border:1px solid #409eff;background:#409eff;color:#fff;border-radius:4px;padding:6px 14px;font-size:13px;cursor:pointer}' +
'.btn.plain{background:#ecf5ff;color:#409eff}' +
'.nav{background:#fff;border-radius:8px;padding:12px 16px;margin-bottom:12px;box-shadow:0 1px 4px rgba(0,0,0,.08)}' +
'.nav-title{font-weight:600;margin-bottom:8px;font-size:14px}' +
'.nav-links{display:flex;flex-wrap:wrap;gap:8px}' +
'.nav-link{display:inline-block;padding:4px 10px;background:#f4f4f5;border-radius:4px;color:#409eff;text-decoration:none;font-size:12px}' +
'.nav-link:hover{background:#ecf5ff}' +
'.group-card{background:#fff;border-radius:8px;padding:16px 20px;margin-bottom:12px;box-shadow:0 1px 4px rgba(0,0,0,.08);scroll-margin-top:16px}' +
'.group-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:10px}' +
'.group-head h3{margin:0;font-size:15px;color:#303133}' +
'.copy-btn{border:1px solid #409eff;background:#ecf5ff;color:#409eff;border-radius:4px;padding:4px 12px;font-size:12px;cursor:pointer;white-space:nowrap}' +
'.copy-btn:hover{background:#409eff;color:#fff}' +
'.group-prompt{margin:0;padding:12px;background:#fafafa;border:1px solid #ebeef5;border-radius:4px;white-space:pre-wrap;word-break:break-word;font-size:13px;font-family:Consolas,"Courier New",monospace}' +
'#all-prompt{display:none}' +
'</style></head><body><div class="page">' +
'<div class="header"><h1>' +
escapeHtml(pageTitle) +
'</h1>' +
'<div class="summary">' +
escapeHtml(groupTotalText) +
'</div>' +
'<div class="actions">' +
'<button type="button" class="btn plain" id="copy-all-btn">' +
escapeHtml(copyAllText) +
'</button></div></div>' +
'<div class="nav"><div class="nav-title">' +
escapeHtml(jumpTitle) +
'</div><div class="nav-links">' +
navHtml +
'</div></div>' +
bodyHtml +
'<pre id="all-prompt">' +
allPrompt +
'</pre></div>' +
'<script>(function(){' +
'var okMsg=' +
JSON.stringify(copySuccessText) +
';var failMsg=' +
JSON.stringify(copyFailText) +
';function copyFromEl(el,btn){var text=el?(el.innerText||el.textContent||""):"";if(!text){alert(failMsg);return;}' +
'function done(){if(btn){var old=btn.innerText;btn.innerText=okMsg;setTimeout(function(){btn.innerText=old;},1200);}else{alert(okMsg);}}' +
'if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(text).then(done).catch(function(){fallback(text,done);});return;}' +
'fallback(text,done);}' +
'function fallback(text,done){var ta=document.createElement("textarea");ta.value=text;ta.style.position="fixed";ta.style.opacity="0";document.body.appendChild(ta);ta.select();try{document.execCommand("copy");done();}catch(e){alert(failMsg);}document.body.removeChild(ta);}' +
'document.querySelectorAll(".copy-btn").forEach(function(btn){btn.addEventListener("click",function(){var id=btn.getAttribute("data-target");copyFromEl(document.getElementById(id),btn);});});' +
'var allBtn=document.getElementById("copy-all-btn");if(allBtn){allBtn.addEventListener("click",function(){copyFromEl(document.getElementById("all-prompt"),allBtn);});}' +
'})();<' +
'/script></body></html>'
);
}
export function downloadCitationReviewHtml(reviewItems, labels, fileName) {
const html = buildCitationReviewStandaloneHtml(reviewItems, labels);
const blob = new Blob([html], { type: 'text/html;charset=utf-8' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = fileName || 'citation-relevance.html';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
export function buildCitationReviewHtmlLabels(translate) {
const t = translate || function (key) {
return key;
};
return {
pageTitle: t('commonTable.citeRelevanceTitle'),
groupTotal: t('commonTable.citeRelevanceGroupTotal', { n: '{n}' }),
jumpTitle: t('commonTable.citeRelevanceJumpTo'),
copyGroup: t('commonTable.citeRelevanceCopyGroup'),
copyAll: t('commonTable.citeRelevanceCopyAllWenai'),
copySuccess: t('commonTable.citeRelevanceCopySuccess'),
copyFail: t('commonTable.citeRelevanceCopyFail'),
groupLabel: t('commonTable.citeRelevanceGroupNavLabel')
};
}