feat: 正文简述分析重构

This commit is contained in:
leekHotline 2026-04-08 14:27:39 +08:00
parent 7a37b7604b
commit dabda879d9
2 changed files with 231 additions and 4 deletions

View File

@ -176,7 +176,7 @@ function normalizeAiResult(result, payload) {
category, category,
institutionType, institutionType,
confidence: normalizeConfidence(result?.confidence), confidence: normalizeConfidence(result?.confidence),
titleSummary: limitLength(result?.titleSummary || payload?.title || "", 60), titleSummary: limitLength(result?.titleSummary || "", 60),
reason: limitLength(result?.reason || "", 120), reason: limitLength(result?.reason || "", 120),
matchedKeywords: dedupe(matchedKeywords) matchedKeywords: dedupe(matchedKeywords)
}; };
@ -217,3 +217,54 @@ async function safeReadText(response) {
return ""; return "";
} }
} }
function buildMessages(payload) {
const compactInput = buildCompactInput(payload);
return [
{
role: "system",
content: [
"Analyze whether this procurement belongs to a financial-institution-related bidding case.",
'Only hit when the service target is clearly a financial institution such as a bank, trust, insurance, consumer finance, financial leasing, or auto finance, and the business belongs to one of these categories: "法律服务", "催收/不良资产处置", "调解服务".',
'Return JSON only with keys: {"category":"法律服务|催收/不良资产处置|调解服务|不命中","confidence":0-100,"summary":"...","reason":"..."}',
'The "summary" must come from the announcement body, not the title.',
'Do not repeat the title or start with the title.',
'Do not include metadata such as "发布时间", "项目编号", "招标单位", "采购单位", "代理单位", "报名截止时间", or "投标截止时间".',
'Keep "summary" around 50 Chinese characters, ideally within 30 to 50 Chinese characters.',
'If the body does not provide a valid summary, return an empty string for "summary".',
'Keep "reason" brief and concrete.'
].join(" ")
},
{
role: "user",
content: JSON.stringify(compactInput)
}
];
}
function normalizeAiResult(result, payload) {
const category = normalizeCategory(result?.category);
const summary = limitLength(result?.summary || result?.titleSummary || "", 60);
const institutionType = Array.isArray(result?.institutionType)
? result.institutionType.filter(Boolean).map((item) => String(item).trim())
: Array.isArray(payload?.keywordHints?.institutions)
? payload.keywordHints.institutions.slice(0, 3)
: [];
const matchedKeywords = Array.isArray(result?.matchedKeywords)
? result.matchedKeywords.filter(Boolean).map((item) => String(item).trim())
: Array.isArray(payload?.keywordHints?.all)
? payload.keywordHints.all
: [];
return {
isRelevant: (typeof result?.isRelevant === "boolean" ? result.isRelevant : category !== "不命中") && category !== "不命中",
category,
institutionType,
confidence: normalizeConfidence(result?.confidence),
summary,
titleSummary: summary,
reason: limitLength(result?.reason || "", 120),
matchedKeywords: dedupe(matchedKeywords)
};
}

View File

@ -11,6 +11,8 @@
const BANNER_ID = "yfb-bid-assistant-banner"; const BANNER_ID = "yfb-bid-assistant-banner";
const KEYWORD_MARK_CLASS = "yfb-keyword-highlight"; const KEYWORD_MARK_CLASS = "yfb-keyword-highlight";
const MAX_LOG_ENTRIES = Number(CONFIG.maxLogEntries) || 80; const MAX_LOG_ENTRIES = Number(CONFIG.maxLogEntries) || 80;
const DEFAULT_MAX_PAGES = 10;
const LEGACY_DEFAULT_MAX_PAGES = 3;
const LIST_ROW_SELECTOR = "tr.el-table__row"; const LIST_ROW_SELECTOR = "tr.el-table__row";
const LIST_CARD_ROW_SELECTOR = ".list > div"; const LIST_CARD_ROW_SELECTOR = ".list > div";
const LIST_TITLE_SELECTOR = ".color1879F7.pointer, .color1879F7.textEll.pointer"; const LIST_TITLE_SELECTOR = ".color1879F7.pointer, .color1879F7.textEll.pointer";
@ -144,9 +146,10 @@
stopRequested: false, stopRequested: false,
panelCollapsed: false, panelCollapsed: false,
panelHidden: false, panelHidden: false,
hasCustomMaxPages: false,
statusText: "等待开始", statusText: "等待开始",
settings: { settings: {
maxPages: 3, maxPages: DEFAULT_MAX_PAGES,
delayMs: 300 delayMs: 300
}, },
stats: { stats: {
@ -435,8 +438,15 @@
state.panelCollapsed = Boolean(saved.panelCollapsed); state.panelCollapsed = Boolean(saved.panelCollapsed);
state.panelHidden = Boolean(saved.panelHidden); state.panelHidden = Boolean(saved.panelHidden);
state.hasCustomMaxPages = Boolean(saved.hasCustomMaxPages);
state.statusText = saved.statusText || state.statusText; state.statusText = saved.statusText || state.statusText;
state.settings = { ...state.settings, ...(saved.settings || {}) }; state.settings = { ...state.settings, ...(saved.settings || {}) };
if (
!state.hasCustomMaxPages &&
(!Number.isFinite(Number(state.settings.maxPages)) || Number(state.settings.maxPages) === LEGACY_DEFAULT_MAX_PAGES)
) {
state.settings.maxPages = DEFAULT_MAX_PAGES;
}
state.stats = { ...state.stats, ...(saved.stats || {}) }; state.stats = { ...state.stats, ...(saved.stats || {}) };
state.results = Array.isArray(saved.results) ? saved.results : []; state.results = Array.isArray(saved.results) ? saved.results : [];
state.rowStatusById = saved.rowStatusById || {}; state.rowStatusById = saved.rowStatusById || {};
@ -713,11 +723,13 @@
} }
function handleSettingsChange() { function handleSettingsChange() {
const previousMaxPages = state.settings.maxPages;
const maxPages = clampNumber(ui.maxPagesInput.value, 1, 200, state.settings.maxPages); const maxPages = clampNumber(ui.maxPagesInput.value, 1, 200, state.settings.maxPages);
const delayMs = clampNumber(ui.delayInput.value, 200, 10000, state.settings.delayMs); const delayMs = clampNumber(ui.delayInput.value, 200, 10000, state.settings.delayMs);
state.settings.maxPages = maxPages; state.settings.maxPages = maxPages;
state.settings.delayMs = delayMs; state.settings.delayMs = delayMs;
state.hasCustomMaxPages = state.hasCustomMaxPages || previousMaxPages !== maxPages;
ui.maxPagesInput.value = String(maxPages); ui.maxPagesInput.value = String(maxPages);
ui.delayInput.value = String(delayMs); ui.delayInput.value = String(delayMs);
void persistState(); void persistState();
@ -1180,6 +1192,68 @@
return normalizeText(result); return normalizeText(result);
} }
function normalizeSummaryCompareText(text) {
return normalizeText(text).replace(/[\s,,。;、:\-()[\]【】"'“”‘’《》]/g, "");
}
function sanitizeSummaryCandidate(text, detailRecord) {
let result = normalizeText(removeBoilerplateText(String(text || ""), detailRecord?.title || ""));
if (!result) {
return "";
}
const title = normalizeText(detailRecord?.title || "");
if (title && result.startsWith(title)) {
result = normalizeText(result.slice(title.length));
}
const prefixPatterns = [
/^(发布时间|发布日期|公告时间|时间|地区|项目编号|项目名称|项目概况|项目简介|招标编号|采购编号|预算金额|预估金额|招标单位|招标人|采购单位|业主单位|代理单位|代理机构|报名截止时间|投标截止时间|开标时间|开标日期|公告类型)\s*[:]?\s*/i,
/^(\d{4}[./-]\d{1,2}[./-]\d{1,2}|\d{1,2}[./-]\d{1,2})\s*/,
/^([一二三四五六七八九十]+、|\(?[一二三四五六七八九十]+\)|[0-9]+[、.])\s*/
];
let previous = "";
while (result && result !== previous) {
previous = result;
prefixPatterns.forEach((pattern) => {
result = normalizeText(result.replace(pattern, ""));
});
}
return result;
}
function isValidSummaryCandidate(text, detailRecord) {
const candidate = normalizeText(text);
if (!candidate || candidate.length < 12) {
return false;
}
const candidateComparable = normalizeSummaryCompareText(candidate);
const titleComparable = normalizeSummaryCompareText(detailRecord?.title || "");
if (!candidateComparable || candidateComparable === titleComparable) {
return false;
}
if (/^[\d\s,,。;、:./\-]+$/.test(candidate)) {
return false;
}
return !/^(发布时间|发布日期|公告时间|时间|地区|项目编号|项目名称|项目概况|项目简介|招标编号|采购编号|预算金额|预估金额|招标单位|招标人|采购单位|业主单位|代理单位|代理机构|报名截止时间|投标截止时间|开标时间|开标日期|公告类型)\b/i.test(candidate);
}
function buildFieldFallbackSummary(detailRecord) {
const parts = [
detailRecord?.bidder ? `招标单位:${detailRecord.bidder}` : "",
detailRecord?.agency ? `代理单位:${detailRecord.agency}` : "",
detailRecord?.signupDeadline ? `报名截止:${detailRecord.signupDeadline}` : "",
detailRecord?.bidDeadline ? `投标截止:${detailRecord.bidDeadline}` : ""
].filter(Boolean);
return parts.length > 0 ? limitLength(parts.join(""), 60) : "";
}
function collectKeywordHints(text) { function collectKeywordHints(text) {
const normalized = normalizeText(text); const normalized = normalizeText(text);
const institutions = collectHits(normalized, KEYWORDS.institutions); const institutions = collectHits(normalized, KEYWORDS.institutions);
@ -1481,8 +1555,15 @@
置信度分数: item.confidence || 0 置信度分数: item.confidence || 0
})); }));
const exportRows = state.results.map((item) => ({
["标题"]: item.title,
["简述"]: item.summary || "",
["AI分类"]: item.category || "",
["置信度"]: item.confidence || 0
}));
const workbook = window.XLSX.utils.book_new(); const workbook = window.XLSX.utils.book_new();
const worksheet = window.XLSX.utils.json_to_sheet(rows); const worksheet = window.XLSX.utils.json_to_sheet(exportRows);
window.XLSX.utils.book_append_sheet(workbook, worksheet, "命中结果"); window.XLSX.utils.book_append_sheet(workbook, worksheet, "命中结果");
const fileBuffer = window.XLSX.write(workbook, { const fileBuffer = window.XLSX.write(workbook, {
@ -2510,9 +2591,17 @@
置信度分数: item.confidence || 0 置信度分数: item.confidence || 0
})); }));
const worksheet = window.XLSX.utils.json_to_sheet(rows, { header: headers }); const exportHeaders = ["标题", "简述", "AI分类", "置信度"];
const exportRows = state.results.map((item) => ({
["标题"]: item.title,
["简述"]: item.summary || "",
["AI分类"]: item.category || "",
["置信度"]: item.confidence || 0
}));
const worksheet = window.XLSX.utils.json_to_sheet(exportRows, { header: exportHeaders });
worksheet["!cols"] = [ worksheet["!cols"] = [
{ wch: 44 }, { wch: 44 },
{ wch: 60 },
{ wch: 18 }, { wch: 18 },
{ wch: 12 } { wch: 12 }
]; ];
@ -2580,6 +2669,7 @@
[STORAGE_KEY]: { [STORAGE_KEY]: {
panelCollapsed: state.panelCollapsed, panelCollapsed: state.panelCollapsed,
panelHidden: state.panelHidden, panelHidden: state.panelHidden,
hasCustomMaxPages: state.hasCustomMaxPages,
statusText: state.statusText, statusText: state.statusText,
settings: state.settings, settings: state.settings,
stats: state.stats, stats: state.stats,
@ -2690,6 +2780,92 @@
return parts.join(""); return parts.join("");
} }
function isValidSummaryCandidate(text, detailRecord) {
const candidate = normalizeText(text);
if (!candidate || candidate.length < 12) {
return false;
}
const title = normalizeText(detailRecord?.title || "");
if (title && candidate.startsWith(title)) {
return false;
}
const candidateComparable = normalizeSummaryCompareText(candidate);
const titleComparable = normalizeSummaryCompareText(title);
if (!candidateComparable || candidateComparable === titleComparable) {
return false;
}
if (/^[\d\s,,。;、:./\-]+$/.test(candidate)) {
return false;
}
return !/^(发布时间|发布日期|公告时间|时间|地区|项目编号|项目名称|项目概况|项目简介|招标编号|采购编号|预算金额|预估金额|招标单位|招标人|采购单位|业主单位|代理单位|代理机构|报名截止时间|投标截止时间|开标时间|开标日期|公告类型)\b/i.test(candidate);
}
function buildLocalSummary(detailRecord) {
const candidateText = [detailRecord.announcementContent, detailRecord.detailText].filter(Boolean).join("\n");
const candidateLines = candidateText
.split(/[\n。]/)
.map((line) => sanitizeSummaryCandidate(line, detailRecord))
.filter((line) => isValidSummaryCandidate(line, detailRecord));
return candidateLines.length > 0 ? limitLength(candidateLines[0], 60) : "";
}
function buildSummary(detailRecord, matchedKeywords = [], aiSummary = "") {
const normalizedAiSummary = normalizeText(removeBoilerplateText(String(aiSummary || ""), detailRecord?.title || ""));
if (isValidSummaryCandidate(normalizedAiSummary, detailRecord)) {
return limitLength(normalizedAiSummary, 60);
}
const localSummary = buildLocalSummary(detailRecord);
if (localSummary) {
return localSummary;
}
const fieldSummary = buildFieldFallbackSummary(detailRecord);
if (fieldSummary) {
return fieldSummary;
}
return limitLength(detailRecord.title, 60);
}
function addResult(detailRecord, decision) {
const result = {
id: detailRecord.id,
title: detailRecord.title,
summary: buildSummary(detailRecord, decision.matchedKeywords || [], decision.summary || decision.titleSummary || ""),
category: decision.category || "未命中",
institutionType: uniqueText(decision.institutionType || []),
matchedKeywords: uniqueText(decision.matchedKeywords || []),
confidence: Number(decision.confidence) || 0,
reason: decision.reason || "",
type: detailRecord.type,
region: detailRecord.region,
publishTime: detailRecord.publishTime,
detailUrl: detailRecord.detailUrl,
sourceUrl: detailRecord.sourceUrl,
attachmentNames: detailRecord.attachmentNames,
announcementContent: detailRecord.announcementContent,
projectNumber: detailRecord.projectNumber,
estimatedAmount: detailRecord.estimatedAmount,
bidder: detailRecord.bidder,
agency: detailRecord.agency,
signupDeadline: detailRecord.signupDeadline,
bidDeadline: detailRecord.bidDeadline
};
const existingIndex = state.results.findIndex((item) => item.id === result.id);
if (existingIndex >= 0) {
state.results.splice(existingIndex, 1, result);
} else {
state.results.push(result);
}
}
function escapeRegExp(text) { function escapeRegExp(text) {
return String(text).replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); return String(text).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
} }