From dabda879d92c0c894233044b4e709d92f1c0c9d5 Mon Sep 17 00:00:00 2001 From: leekHotline <117092932+leekHotline@users.noreply.github.com> Date: Wed, 8 Apr 2026 14:27:39 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=AD=A3=E6=96=87=E7=AE=80=E8=BF=B0?= =?UTF-8?q?=E5=88=86=E6=9E=90=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- background.js | 53 ++++++++++++++- content.js | 182 +++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 231 insertions(+), 4 deletions(-) diff --git a/background.js b/background.js index d54ab7b..c717c48 100644 --- a/background.js +++ b/background.js @@ -176,7 +176,7 @@ function normalizeAiResult(result, payload) { category, institutionType, confidence: normalizeConfidence(result?.confidence), - titleSummary: limitLength(result?.titleSummary || payload?.title || "", 60), + titleSummary: limitLength(result?.titleSummary || "", 60), reason: limitLength(result?.reason || "", 120), matchedKeywords: dedupe(matchedKeywords) }; @@ -217,3 +217,54 @@ async function safeReadText(response) { 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) + }; +} diff --git a/content.js b/content.js index e850cb0..32cc4d3 100644 --- a/content.js +++ b/content.js @@ -11,6 +11,8 @@ const BANNER_ID = "yfb-bid-assistant-banner"; const KEYWORD_MARK_CLASS = "yfb-keyword-highlight"; 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_CARD_ROW_SELECTOR = ".list > div"; const LIST_TITLE_SELECTOR = ".color1879F7.pointer, .color1879F7.textEll.pointer"; @@ -144,9 +146,10 @@ stopRequested: false, panelCollapsed: false, panelHidden: false, + hasCustomMaxPages: false, statusText: "等待开始", settings: { - maxPages: 3, + maxPages: DEFAULT_MAX_PAGES, delayMs: 300 }, stats: { @@ -435,8 +438,15 @@ state.panelCollapsed = Boolean(saved.panelCollapsed); state.panelHidden = Boolean(saved.panelHidden); + state.hasCustomMaxPages = Boolean(saved.hasCustomMaxPages); state.statusText = saved.statusText || state.statusText; 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.results = Array.isArray(saved.results) ? saved.results : []; state.rowStatusById = saved.rowStatusById || {}; @@ -713,11 +723,13 @@ } function handleSettingsChange() { + const previousMaxPages = 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); state.settings.maxPages = maxPages; state.settings.delayMs = delayMs; + state.hasCustomMaxPages = state.hasCustomMaxPages || previousMaxPages !== maxPages; ui.maxPagesInput.value = String(maxPages); ui.delayInput.value = String(delayMs); void persistState(); @@ -1180,6 +1192,68 @@ 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) { const normalized = normalizeText(text); const institutions = collectHits(normalized, KEYWORDS.institutions); @@ -1481,8 +1555,15 @@ 置信度分数: 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 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, "命中结果"); const fileBuffer = window.XLSX.write(workbook, { @@ -2510,9 +2591,17 @@ 置信度分数: 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"] = [ { wch: 44 }, + { wch: 60 }, { wch: 18 }, { wch: 12 } ]; @@ -2580,6 +2669,7 @@ [STORAGE_KEY]: { panelCollapsed: state.panelCollapsed, panelHidden: state.panelHidden, + hasCustomMaxPages: state.hasCustomMaxPages, statusText: state.statusText, settings: state.settings, stats: state.stats, @@ -2690,6 +2780,92 @@ 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) { return String(text).replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); }