From 7a37b7604b673f1c3e50331dfcef601731ed602e Mon Sep 17 00:00:00 2001
From: leekHotline <117092932+leekHotline@users.noreply.github.com>
Date: Wed, 8 Apr 2026 13:07:01 +0800
Subject: [PATCH] feat: init the repo
---
background.js | 219 ++++
config.js | 10 +
content.js | 2705 +++++++++++++++++++++++++++++++++++++++
manifest.json | 33 +
vendor/xlsx.full.min.js | 22 +
5 files changed, 2989 insertions(+)
create mode 100644 background.js
create mode 100644 config.js
create mode 100644 content.js
create mode 100644 manifest.json
create mode 100644 vendor/xlsx.full.min.js
diff --git a/background.js b/background.js
new file mode 100644
index 0000000..d54ab7b
--- /dev/null
+++ b/background.js
@@ -0,0 +1,219 @@
+importScripts("config.js");
+
+const CONFIG = self.YFB_EXTENSION_CONFIG || {};
+const CHAT_ENDPOINT = CONFIG.chatEndpoint || "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions";
+const REQUEST_TIMEOUT_MS = Number(CONFIG.requestTimeoutMs) || 45000;
+
+chrome.runtime.onInstalled.addListener(() => {
+ console.log("乙方宝招标筛选助手已安装");
+});
+
+chrome.action.onClicked.addListener((tab) => {
+ if (!tab || !tab.id) {
+ return;
+ }
+
+ chrome.tabs.sendMessage(tab.id, { type: "YFB_TOGGLE_PANEL" }, () => {
+ void chrome.runtime.lastError;
+ });
+});
+
+chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
+ if (!message || message.type !== "YFB_ANALYZE_CANDIDATE") {
+ return false;
+ }
+
+ void analyzeCandidate(message.payload)
+ .then((data) => {
+ sendResponse({ ok: true, data });
+ })
+ .catch((error) => {
+ sendResponse({
+ ok: false,
+ error: error instanceof Error ? error.message : "AI 分析失败"
+ });
+ });
+
+ return true;
+});
+
+async function analyzeCandidate(payload) {
+ if (!CONFIG.apiKey) {
+ throw new Error("缺少 DASHSCOPE_API_KEY,请先执行 npm.cmd run build。");
+ }
+
+ const response = await fetchWithTimeout(CHAT_ENDPOINT, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${CONFIG.apiKey}`
+ },
+ body: JSON.stringify({
+ model: CONFIG.model || "qwen-plus",
+ temperature: 0.1,
+ max_tokens: 120,
+ messages: buildMessages(payload)
+ })
+ });
+
+ if (!response.ok) {
+ const errorText = await safeReadText(response);
+ throw new Error(`AI 接口返回 ${response.status}: ${errorText || "未知错误"}`);
+ }
+
+ const data = await response.json();
+ const rawContent = extractMessageContent(data);
+ const parsed = parseAiJson(rawContent);
+ return normalizeAiResult(parsed, payload);
+}
+
+async function fetchWithTimeout(url, options) {
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
+
+ try {
+ return await fetch(url, {
+ ...options,
+ signal: controller.signal
+ });
+ } catch (error) {
+ if (error && error.name === "AbortError") {
+ throw new Error("AI 请求超时");
+ }
+ throw error;
+ } finally {
+ clearTimeout(timeoutId);
+ }
+}
+
+function buildMessages(payload) {
+ const compactInput = buildCompactInput(payload);
+
+ return [
+ {
+ role: "system",
+ content:
+ '判断是否属于金融机构相关招标。仅当服务对象明确是银行/信托/保险/消费金融/金融租赁/汽车金融等金融机构,且业务属于"法律服务"、"催收/不良资产处置"、"调解服务"之一时命中。只返回 JSON:{"category":"法律服务|催收/不良资产处置|调解服务|不命中","confidence":0-100}。'
+ },
+ {
+ role: "user",
+ content: JSON.stringify(compactInput)
+ }
+ ];
+}
+
+function buildCompactInput(payload) {
+ const keywordHints = payload?.keywordHints || {};
+
+ return {
+ title: String(payload?.title || ""),
+ type: String(payload?.type || ""),
+ region: String(payload?.region || ""),
+ publishTime: String(payload?.publishTime || ""),
+ detailText: limitLength(String(payload?.detailText || ""), Number(CONFIG.maxAiChars) || 1800),
+ attachmentNames: Array.isArray(payload?.attachmentNames) ? payload.attachmentNames.slice(0, 6) : [],
+ keywordHints: {
+ institutions: Array.isArray(keywordHints.institutions) ? keywordHints.institutions.slice(0, 6) : [],
+ legal: Array.isArray(keywordHints.legal) ? keywordHints.legal.slice(0, 6) : [],
+ collection: Array.isArray(keywordHints.collection) ? keywordHints.collection.slice(0, 6) : [],
+ mediation: Array.isArray(keywordHints.mediation) ? keywordHints.mediation.slice(0, 6) : [],
+ all: Array.isArray(keywordHints.all) ? keywordHints.all.slice(0, 12) : []
+ }
+ };
+}
+
+function extractMessageContent(apiResponse) {
+ const content = apiResponse?.choices?.[0]?.message?.content;
+ if (typeof content === "string") {
+ return content;
+ }
+ if (Array.isArray(content)) {
+ return content
+ .map((part) => {
+ if (typeof part === "string") {
+ return part;
+ }
+ return part?.text || "";
+ })
+ .join("\n");
+ }
+ return "";
+}
+
+function parseAiJson(text) {
+ const trimmed = String(text || "").trim();
+ if (!trimmed) {
+ throw new Error("AI 返回为空");
+ }
+
+ try {
+ return JSON.parse(trimmed);
+ } catch (error) {
+ const startIndex = trimmed.indexOf("{");
+ const endIndex = trimmed.lastIndexOf("}");
+ if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) {
+ throw new Error("AI 返回不是有效 JSON");
+ }
+ return JSON.parse(trimmed.slice(startIndex, endIndex + 1));
+ }
+}
+
+function normalizeAiResult(result, payload) {
+ const category = normalizeCategory(result?.category);
+ 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),
+ titleSummary: limitLength(result?.titleSummary || payload?.title || "", 60),
+ reason: limitLength(result?.reason || "", 120),
+ matchedKeywords: dedupe(matchedKeywords)
+ };
+}
+
+function normalizeCategory(category) {
+ const value = String(category || "").trim();
+ if (value === "法律服务" || value === "催收/不良资产处置" || value === "调解服务") {
+ return value;
+ }
+ return "不命中";
+}
+
+function normalizeConfidence(confidence) {
+ const numeric = Number(confidence);
+ if (!Number.isFinite(numeric)) {
+ return 0;
+ }
+ return Math.max(0, Math.min(100, Math.round(numeric)));
+}
+
+function limitLength(text, maxLength) {
+ const normalized = String(text || "").trim();
+ if (normalized.length <= maxLength) {
+ return normalized;
+ }
+ return normalized.slice(0, maxLength - 1) + "…";
+}
+
+function dedupe(list) {
+ return Array.from(new Set(list.filter(Boolean)));
+}
+
+async function safeReadText(response) {
+ try {
+ return await response.text();
+ } catch (error) {
+ return "";
+ }
+}
diff --git a/config.js b/config.js
new file mode 100644
index 0000000..4cf5417
--- /dev/null
+++ b/config.js
@@ -0,0 +1,10 @@
+globalThis.YFB_EXTENSION_CONFIG = Object.freeze({
+ "apiKey": "sk-f9966506e15d4a678f560c1a9b67fee2",
+ "model": "qwen3.5-flash",
+ "chatEndpoint": "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions",
+ "requestTimeoutMs": 45000,
+ "maxBodyChars": 12000,
+ "maxAiChars": 1800,
+ "maxListPreviewChars": 400,
+ "maxLogEntries": 80
+});
diff --git a/content.js b/content.js
new file mode 100644
index 0000000..e850cb0
--- /dev/null
+++ b/content.js
@@ -0,0 +1,2705 @@
+(() => {
+ if (window.__YFB_BID_ASSISTANT_LOADED__) {
+ return;
+ }
+ window.__YFB_BID_ASSISTANT_LOADED__ = true;
+
+ const CONFIG = globalThis.YFB_EXTENSION_CONFIG || {};
+ const STORAGE_KEY = "yfbBidAutomationState";
+ const PANEL_ID = "yfb-bid-assistant-panel";
+ const STYLE_ID = "yfb-bid-assistant-style";
+ const BANNER_ID = "yfb-bid-assistant-banner";
+ const KEYWORD_MARK_CLASS = "yfb-keyword-highlight";
+ const MAX_LOG_ENTRIES = Number(CONFIG.maxLogEntries) || 80;
+ const LIST_ROW_SELECTOR = "tr.el-table__row";
+ const LIST_CARD_ROW_SELECTOR = ".list > div";
+ const LIST_TITLE_SELECTOR = ".color1879F7.pointer, .color1879F7.textEll.pointer";
+ const LIST_CARD_TITLE_SELECTOR = ".title-flex > div, .title-flex > a, .title-flex .pointer, a[href*='infoDetail']";
+ const NEXT_PAGE_SELECTOR = ".list-pagination-container .next-btn";
+ const SUBSCRIPTION_FILTER_SELECTOR = ".subscribe-product-filter .filter-header, .select-filters-wrap";
+ const SUBSCRIPTION_GROUP_TAB_SELECTOR = ".subscribe-product-filter .filter-header .right-tabs .tab-item.active";
+ const SUBSCRIPTION_ALL_ITEM_SELECTOR = ".subcategory-item, .subcategory-content > div";
+ const SUBSCRIPTION_CONFIRM_SELECTOR = "button.confirm-btn, .confirm-btn";
+ const DETAIL_TITLE_SELECTORS = [
+ ".long-project-name > div",
+ "#printInfo-head .long-project-name > div",
+ ".project-head-container .project-name"
+ ];
+ const DETAIL_CONTENT_SELECTORS = [
+ ".project-detail-content",
+ ".detail-content",
+ ".notice-content",
+ ".article-content",
+ ".rich-text",
+ ".ck-content",
+ ".w-e-text",
+ ".attachment-content"
+ ];
+ const KNOWN_DIALOG_TITLES = [
+ "选择要关联的商机",
+ "附件发送至邮箱",
+ "系统提示",
+ "信息纠错",
+ "联系人电话"
+ ];
+
+ const KEYWORDS = {
+ institutions: [
+ "银行",
+ "农商行",
+ "农商银行",
+ "农村商业银行",
+ "农信",
+ "信用社",
+ "邮储",
+ "邮政储蓄",
+ "信用卡中心",
+ "信托",
+ "保险",
+ "消费金融",
+ "金融租赁",
+ "融资租赁",
+ "汽车金融",
+ "资产管理公司",
+ "AMC"
+ ],
+ legal: [
+ "法律服务",
+ "律师库",
+ "律师事务所",
+ "律所",
+ "律师",
+ "外聘律所",
+ "外聘律师",
+ "诉讼",
+ "非诉",
+ "非诉讼",
+ "司法催收",
+ "法务催收",
+ "逾期贷款"
+ ],
+ collection: [
+ "委外催收",
+ "催收",
+ "不良资产处置",
+ "不良资产",
+ "贷后管理",
+ "清收服务",
+ "欠款提醒",
+ "逾期账户",
+ "逾期贷款",
+ "资产处置"
+ ],
+ mediation: [
+ "调解服务",
+ "诉前调解",
+ "委外调解",
+ "调解",
+ "纠纷调解"
+ ]
+ };
+
+ const CATEGORY_LABELS = {
+ legal: "法律服务",
+ collection: "催收/不良资产处置",
+ mediation: "调解服务"
+ };
+
+ const SUBSCRIPTION_GROUP_DEFINITIONS = [
+ {
+ key: "group1",
+ labels: ["分组1", "未命名(1)"],
+ selectors: [
+ "div.filter-container div:nth-of-type(2) > i",
+ "div.filter-container div:nth-of-type(2)",
+ "div.select-filters-wrap div:nth-of-type(2)"
+ ]
+ },
+ {
+ key: "group2",
+ labels: ["分组2", "调解服务(4)"],
+ selectors: [
+ "div.filter-container div:nth-of-type(3) > i",
+ "div.filter-container div:nth-of-type(3)",
+ "div.select-filters-wrap div:nth-of-type(3)"
+ ]
+ },
+ {
+ key: "group3",
+ labels: ["分组3", "催收业务(8)"],
+ selectors: [
+ "div.filter-container div:nth-of-type(4) > i",
+ "div.filter-container div:nth-of-type(4)",
+ "div.select-filters-wrap div:nth-of-type(4)"
+ ]
+ }
+ ];
+
+ const SUBSCRIPTION_POPUP_ALL_SELECTOR = "div.subcategory-content > div:nth-of-type(1)"; // 代表弹窗里的“全部”
+ const SUBSCRIPTION_POPUP_CONFIRM_SELECTOR = "button.confirm-btn"; // 代表弹窗里的“确认”
+
+
+ const state = {
+ isRunning: false,
+ stopRequested: false,
+ panelCollapsed: false,
+ panelHidden: false,
+ statusText: "等待开始",
+ settings: {
+ maxPages: 3,
+ delayMs: 300
+ },
+ stats: {
+ scanned: 0,
+ hits: 0,
+ currentPage: 1,
+ currentIndex: 0
+ },
+ results: [],
+ rowStatusById: {},
+ logs: []
+ };
+
+ const ui = {
+ panel: null,
+ status: null,
+ logContainer: null,
+ scannedValue: null,
+ hitsValue: null,
+ pageValue: null,
+ rowValue: null,
+ maxPagesInput: null,
+ delayInput: null,
+ startButton: null,
+ subscribeAllButton: null,
+ stopButton: null,
+ exportButton: null,
+ clearButton: null,
+ toggleButton: null
+ };
+
+ let mutationObserver = null;
+ let syncTimer = null;
+ let bannerHideTimer = null;
+
+ void initialize();
+
+ async function initialize() {
+ if (!isSupportedHost()) {
+ return;
+ }
+
+ injectStyles();
+ await restoreState();
+ mountPanel();
+ bindRuntimeMessages();
+ bindMutationObserver();
+ schedulePageSync();
+ refreshView();
+ }
+
+ function isSupportedHost() {
+ return /(^|\.)yfbzb\.com$/i.test(location.hostname) || /(^|\.)qianlima\.com$/i.test(location.hostname);
+ }
+
+ function injectStyles() {
+ if (document.getElementById(STYLE_ID)) {
+ return;
+ }
+
+ const style = document.createElement("style");
+ style.id = STYLE_ID;
+ style.textContent = `
+ #${PANEL_ID} {
+ position: fixed;
+ right: 18px;
+ bottom: 18px;
+ width: 388px;
+ z-index: 2147483647;
+ border-radius: 22px;
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(248, 250, 252, 0.98) 100%);
+ color: #0f172a;
+ box-shadow: 0 24px 60px rgba(15, 23, 42, 0.16);
+ border: 1px solid rgba(148, 163, 184, 0.24);
+ backdrop-filter: blur(10px);
+ font-family: "Microsoft YaHei", "PingFang SC", sans-serif;
+ overflow: hidden;
+ }
+ #${PANEL_ID}.is-hidden { display: none; }
+ #${PANEL_ID} .yfb-panel-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ padding: 16px 18px 14px;
+ border-bottom: 1px solid rgba(226, 232, 240, 0.95);
+ cursor: move;
+ }
+ #${PANEL_ID} .yfb-panel-title { display: flex; flex-direction: column; gap: 2px; }
+ #${PANEL_ID} .yfb-panel-eyebrow {
+ font-size: 11px;
+ letter-spacing: 0.14em;
+ text-transform: uppercase;
+ color: #ea580c;
+ }
+ #${PANEL_ID} .yfb-panel-name { font-size: 17px; font-weight: 800; color: #0f172a; }
+ #${PANEL_ID} .yfb-panel-subtitle { font-size: 12px; color: #64748b; }
+ #${PANEL_ID} .yfb-panel-toggle {
+ border: 0;
+ background: linear-gradient(135deg, #fff7ed 0%, #ffedd5 100%);
+ color: #c2410c;
+ width: 34px;
+ height: 34px;
+ border-radius: 999px;
+ cursor: pointer;
+ font-size: 18px;
+ font-weight: 700;
+ box-shadow: inset 0 0 0 1px rgba(251, 146, 60, 0.18);
+ }
+ #${PANEL_ID}.is-collapsed .yfb-panel-body { display: none; }
+ #${PANEL_ID}.is-collapsed {
+ width: 64px;
+ height: 64px;
+ right: 16px;
+ border-radius: 999px;
+ box-shadow: 0 16px 38px rgba(15, 23, 42, 0.18);
+ }
+ #${PANEL_ID}.is-collapsed .yfb-panel-header {
+ justify-content: center;
+ padding: 0;
+ height: 64px;
+ border-bottom: 0;
+ }
+ #${PANEL_ID}.is-collapsed .yfb-panel-title {
+ display: none;
+ }
+ #${PANEL_ID}.is-collapsed .yfb-panel-toggle {
+ width: 64px;
+ height: 64px;
+ background: radial-gradient(circle at 30% 30%, #fff7ed 0%, #ffedd5 45%, #fed7aa 100%);
+ box-shadow: none;
+ }
+ #${PANEL_ID} .yfb-panel-body { padding: 14px 16px 16px; }
+ #${PANEL_ID} .yfb-stats {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 10px;
+ margin-bottom: 14px;
+ }
+ #${PANEL_ID} .yfb-stat {
+ border-radius: 16px;
+ padding: 10px 10px 11px;
+ background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
+ border: 1px solid rgba(226, 232, 240, 0.95);
+ }
+ #${PANEL_ID} .yfb-stat-label {
+ display: block;
+ font-size: 11px;
+ color: #64748b;
+ margin-bottom: 4px;
+ }
+ #${PANEL_ID} .yfb-stat-value { font-size: 18px; font-weight: 800; color: #0f172a; }
+ #${PANEL_ID} .yfb-field-grid {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 12px;
+ margin-bottom: 14px;
+ }
+ #${PANEL_ID} .yfb-field { display: flex; flex-direction: column; gap: 6px; }
+ #${PANEL_ID} .yfb-field label { font-size: 12px; color: #475569; }
+ #${PANEL_ID} .yfb-field input {
+ border: 1px solid #dbe4ee;
+ background: #ffffff;
+ color: #0f172a;
+ border-radius: 12px;
+ height: 38px;
+ padding: 0 12px;
+ outline: none;
+ }
+ #${PANEL_ID} .yfb-field input:focus {
+ border-color: #fb923c;
+ box-shadow: 0 0 0 3px rgba(251, 146, 60, 0.16);
+ }
+ #${PANEL_ID} .yfb-button-row {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 10px;
+ margin-bottom: 12px;
+ }
+ #${PANEL_ID} .yfb-button {
+ border: 0;
+ border-radius: 12px;
+ min-height: 40px;
+ font-weight: 700;
+ cursor: pointer;
+ }
+ #${PANEL_ID} .yfb-button:disabled { opacity: 0.48; cursor: not-allowed; }
+ #${PANEL_ID} .yfb-button-primary {
+ background: linear-gradient(135deg, #f59e0b 0%, #ea580c 100%);
+ color: #fff;
+ }
+ #${PANEL_ID} .yfb-button-secondary {
+ background: #ffffff;
+ color: #1e293b;
+ border: 1px solid #dbe4ee;
+ }
+ #${PANEL_ID} .yfb-status {
+ min-height: 44px;
+ border-radius: 14px;
+ padding: 10px 12px;
+ font-size: 12px;
+ line-height: 1.5;
+ background: #fff7ed;
+ color: #7c2d12;
+ margin-bottom: 10px;
+ }
+ #${PANEL_ID} .yfb-hint {
+ font-size: 11px;
+ color: #64748b;
+ margin-bottom: 10px;
+ }
+ #${PANEL_ID} .yfb-log {
+ max-height: 220px;
+ overflow-y: auto;
+ padding: 10px;
+ border-radius: 14px;
+ background: #f8fafc;
+ border: 1px solid #e2e8f0;
+ }
+ #${PANEL_ID} .yfb-log-entry {
+ font-size: 12px;
+ line-height: 1.45;
+ margin-bottom: 8px;
+ padding-left: 10px;
+ border-left: 2px solid rgba(148, 163, 184, 0.45);
+ color: #334155;
+ }
+ #${PANEL_ID} .yfb-log-entry.info { border-left-color: #60a5fa; }
+ #${PANEL_ID} .yfb-log-entry.success { border-left-color: #34d399; }
+ #${PANEL_ID} .yfb-log-entry.warning { border-left-color: #fbbf24; }
+ #${PANEL_ID} .yfb-log-entry.error { border-left-color: #f87171; }
+ ${LIST_ROW_SELECTOR}[data-yfb-status="hit"],
+ ${LIST_CARD_ROW_SELECTOR}[data-yfb-status="hit"] { box-shadow: inset 4px 0 0 #f59e0b; background: rgba(245, 158, 11, 0.08); }
+ ${LIST_ROW_SELECTOR}[data-yfb-status="skip"],
+ ${LIST_CARD_ROW_SELECTOR}[data-yfb-status="skip"] { box-shadow: inset 4px 0 0 rgba(148, 163, 184, 0.7); }
+ ${LIST_ROW_SELECTOR}[data-yfb-status="scanning"],
+ ${LIST_CARD_ROW_SELECTOR}[data-yfb-status="scanning"] { box-shadow: inset 4px 0 0 #38bdf8; background: rgba(56, 189, 248, 0.08); }
+ ${LIST_ROW_SELECTOR}[data-yfb-status="error"],
+ ${LIST_CARD_ROW_SELECTOR}[data-yfb-status="error"] { box-shadow: inset 4px 0 0 #f87171; background: rgba(248, 113, 113, 0.08); }
+ .yfb-row-badge {
+ display: inline-flex;
+ align-items: center;
+ margin-left: 8px;
+ padding: 2px 8px;
+ border-radius: 999px;
+ font-size: 11px;
+ line-height: 1.6;
+ font-weight: 700;
+ }
+ .yfb-row-badge.hit { color: #7c2d12; background: rgba(245, 158, 11, 0.18); }
+ .yfb-row-badge.skip { color: #cbd5e1; background: rgba(148, 163, 184, 0.16); }
+ .yfb-row-badge.scanning { color: #082f49; background: rgba(56, 189, 248, 0.22); }
+ .yfb-row-badge.error { color: #7f1d1d; background: rgba(248, 113, 113, 0.2); }
+ #${BANNER_ID} {
+ position: fixed;
+ top: 18px;
+ left: 18px;
+ z-index: 2147483646;
+ max-width: 560px;
+ border-radius: 16px;
+ padding: 14px 16px;
+ background: rgba(251, 191, 36, 0.96);
+ color: #3f2600;
+ box-shadow: 0 18px 48px rgba(245, 158, 11, 0.32);
+ font-family: "Microsoft YaHei", "PingFang SC", sans-serif;
+ pointer-events: none;
+ }
+ #${BANNER_ID} .yfb-banner-title { font-size: 15px; font-weight: 800; margin-bottom: 4px; }
+ #${BANNER_ID} .yfb-banner-meta { font-size: 12px; line-height: 1.45; }
+ mark.${KEYWORD_MARK_CLASS} {
+ background: linear-gradient(120deg, rgba(251, 191, 36, 0.85) 0%, rgba(249, 115, 22, 0.85) 100%);
+ color: #111827;
+ padding: 0 2px;
+ border-radius: 4px;
+ }
+ `;
+
+ document.documentElement.appendChild(style);
+ }
+
+ async function restoreState() {
+ const saved = await storageGet(STORAGE_KEY);
+ if (!saved) {
+ return;
+ }
+
+ state.panelCollapsed = Boolean(saved.panelCollapsed);
+ state.panelHidden = Boolean(saved.panelHidden);
+ state.statusText = saved.statusText || state.statusText;
+ state.settings = { ...state.settings, ...(saved.settings || {}) };
+ state.stats = { ...state.stats, ...(saved.stats || {}) };
+ state.results = Array.isArray(saved.results) ? saved.results : [];
+ state.rowStatusById = saved.rowStatusById || {};
+ state.logs = Array.isArray(saved.logs) ? saved.logs.slice(-MAX_LOG_ENTRIES) : [];
+ }
+
+ function buildPanelMarkup() {
+ return `
+
+
+
+
已扫描0
+
命中数0
+
当前页1
+
当前条0
+
+
+
+
+
+
+
+
+
+
+
开始前停留在筛选列表页即可。脚本会先把订阅产品右侧 3 个分组依次切到“全部”,再进入详情抽取标题、正文和 AI 分析结果。
+
+
+ `;
+ }
+
+ function mountPanel() {
+ if (document.getElementById(PANEL_ID)) {
+ return;
+ }
+
+ const panel = document.createElement("div");
+ panel.id = PANEL_ID;
+ panel.innerHTML = buildPanelMarkup(); /*
+
+
+
+
已扫描0
+
命中数0
+
当前页1
+
当前条0
+
+
+
+
+
+
+
+
+
+
+
建议先切到乙方宝招标列表页再启动。插件会优先抓标题、正文和附件名,再做 AI 判定。
+
+
+ */;
+
+ document.documentElement.appendChild(panel);
+
+ ui.panel = panel;
+ ui.status = panel.querySelector("[data-role='status']");
+ ui.logContainer = panel.querySelector("[data-role='logs']");
+ ui.scannedValue = panel.querySelector("[data-role='scanned']");
+ ui.hitsValue = panel.querySelector("[data-role='hits']");
+ ui.pageValue = panel.querySelector("[data-role='page']");
+ ui.rowValue = panel.querySelector("[data-role='row']");
+ ui.maxPagesInput = panel.querySelector("#yfb-max-pages");
+ ui.delayInput = panel.querySelector("#yfb-delay-ms");
+ ui.startButton = panel.querySelector("[data-role='start']");
+ ui.subscribeAllButton = panel.querySelector("[data-role='subscribe-all']");
+ ui.stopButton = panel.querySelector("[data-role='stop']");
+ ui.exportButton = panel.querySelector("[data-role='export']");
+ ui.clearButton = panel.querySelector("[data-role='clear']");
+ ui.toggleButton = panel.querySelector(".yfb-panel-toggle");
+
+ ui.maxPagesInput.value = String(state.settings.maxPages);
+ ui.delayInput.value = String(state.settings.delayMs);
+
+ ui.maxPagesInput.addEventListener("change", handleSettingsChange);
+ ui.delayInput.addEventListener("change", handleSettingsChange);
+ ui.startButton.addEventListener("click", () => { void startScan(); });
+ ui.subscribeAllButton.addEventListener("click", () => { void runSubscriptionOnly(); });
+ ui.stopButton.addEventListener("click", stopScan);
+ ui.exportButton.addEventListener("click", exportResults);
+ ui.clearButton.addEventListener("click", clearResults);
+ ui.toggleButton.addEventListener("click", toggleCollapse);
+
+ enableDrag(panel.querySelector(".yfb-panel-header"), panel);
+ }
+
+ function enableDrag(handle, target) {
+ let offsetX = 0;
+ let offsetY = 0;
+ let dragging = false;
+
+ handle.addEventListener("pointerdown", (event) => {
+ if (event.target.closest("button")) {
+ return;
+ }
+
+ const rect = target.getBoundingClientRect();
+ dragging = true;
+ offsetX = event.clientX - rect.left;
+ offsetY = event.clientY - rect.top;
+ event.preventDefault();
+ });
+
+ handle.addEventListener("pointermove", (event) => {
+ if (!dragging) {
+ return;
+ }
+
+ target.style.left = `${Math.max(8, event.clientX - offsetX)}px`;
+ target.style.top = `${Math.max(8, event.clientY - offsetY)}px`;
+ target.style.right = "auto";
+ target.style.bottom = "auto";
+ });
+
+ const stopDragging = () => {
+ if (!dragging) {
+ return;
+ }
+ dragging = false;
+ void persistState();
+ };
+
+ handle.addEventListener("pointerup", stopDragging);
+ handle.addEventListener("pointercancel", stopDragging);
+ }
+
+ function bindRuntimeMessages() {
+ chrome.runtime.onMessage.addListener((message) => {
+ if (!message) {
+ return;
+ }
+
+ if (message.type === "YFB_TOGGLE_PANEL") {
+ state.panelHidden = !state.panelHidden;
+ refreshView();
+ void persistState();
+ }
+ });
+ }
+
+ async function delay(ms) {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+ }
+
+ async function doSubscribeAll() {
+ if (state.isRunning) {
+ return;
+ }
+
+ handleSettingsChange();
+
+ if (!isListPage()) {
+ setStatus("请先打开乙方宝招标筛选列表页,再执行订阅分组。");
+ log("当前页面不是招标筛选列表页,已取消订阅分组测试。", "warning");
+ refreshView();
+ return;
+ }
+
+ state.isRunning = true;
+ state.stopRequested = false;
+ setStatus("正在处理订阅分组,请稍候...");
+ log("开始执行订阅分组全选测试。", "info");
+ refreshView();
+
+ try {
+ await waitForUiSettled();
+ const groups = await prepareSubscriptionFilters({ strict: true });
+ setStatus(`订阅分组处理完成,已处理 ${groups.length} 个分组。`);
+ log(`订阅分组处理完成,已处理 ${groups.length} 个分组。`, "success");
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "订阅分组处理失败";
+ if (message === "__YFB_STOPPED__") {
+ setStatus("订阅分组任务已停止。");
+ log("订阅分组任务已手动停止。", "warning");
+ } else {
+ setStatus(`订阅分组处理异常:${message}`);
+ log(`订阅分组处理异常:${message}`, "error");
+ }
+ } finally {
+ state.isRunning = false;
+ state.stopRequested = false;
+ refreshView();
+ await persistState();
+ }
+ }
+
+ function bindMutationObserver() {
+ if (mutationObserver) {
+ return;
+ }
+
+ mutationObserver = new MutationObserver(() => {
+ schedulePageSync();
+ });
+
+ mutationObserver.observe(document.body, {
+ childList: true,
+ subtree: true
+ });
+ }
+
+ function schedulePageSync() {
+ clearTimeout(syncTimer);
+ syncTimer = setTimeout(() => {
+ if (isListPage()) {
+ restoreListHighlights();
+ } else {
+ restoreDetailHighlightIfPossible();
+ }
+ }, 220);
+ }
+
+ function restoreDetailHighlightIfPossible() {
+ const title = findTitleCandidate("");
+ if (!title) {
+ return;
+ }
+
+ const matchedResult = state.results.find((item) => {
+ return item.title === title || item.title.includes(title) || title.includes(item.title);
+ });
+
+ if (!matchedResult) {
+ return;
+ }
+
+ const currentBanner = document.getElementById(BANNER_ID);
+ if (currentBanner && currentBanner.dataset.resultId === matchedResult.id) {
+ return;
+ }
+
+ showDetailBanner(matchedResult);
+ highlightKeywords(matchedResult.matchedKeywords || []);
+ }
+
+ function handleSettingsChange() {
+ 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;
+ ui.maxPagesInput.value = String(maxPages);
+ ui.delayInput.value = String(delayMs);
+ void persistState();
+ }
+
+ function toggleCollapse() {
+ state.panelCollapsed = !state.panelCollapsed;
+ refreshView();
+ void persistState();
+ }
+
+ function stopScan() {
+ if (!state.isRunning) {
+ setStatus("当前没有正在运行的扫描任务。");
+ refreshView();
+ return;
+ }
+
+ state.stopRequested = true;
+ log("收到停止指令,当前记录处理完后会终止任务。", "warning");
+ refreshView();
+ }
+
+ async function startScan() {
+ if (state.isRunning) {
+ return;
+ }
+
+ handleSettingsChange();
+
+ if (!isListPage()) {
+ setStatus("请先打开乙方宝招标列表页,再启动扫描。");
+ log("当前页面不是招标列表页,已取消启动。", "warning");
+ refreshView();
+ return;
+ }
+
+ // 默认执行一次订阅归零
+ await doSubscribeAll();
+
+ state.isRunning = true;
+ state.stopRequested = false;
+ setStatus(`准备开始扫描,最多 ${state.settings.maxPages} 页。`);
+ log(`开始扫描,最多 ${state.settings.maxPages} 页,步进延迟 ${state.settings.delayMs}ms。`, "info");
+ refreshView();
+
+ try {
+ await waitForListReady();
+
+ for (let pageOffset = 0; pageOffset < state.settings.maxPages; pageOffset += 1) {
+ throwIfStopped();
+ state.stats.currentPage = getCurrentPageNumber() || pageOffset + 1;
+ state.stats.currentIndex = 0;
+ refreshView();
+
+ const rows = collectRows();
+ if (rows.length === 0) {
+ throw new Error("列表页没有可扫描的数据。");
+ }
+
+ log(`第 ${state.stats.currentPage} 页识别到 ${rows.length} 条记录。`, "info");
+
+ for (let rowIndex = 0; rowIndex < rows.length; rowIndex += 1) {
+ throwIfStopped();
+ state.stats.currentIndex = rowIndex + 1;
+ refreshView();
+ await processRow(rows[rowIndex].id, rowIndex);
+ }
+
+ if (pageOffset >= state.settings.maxPages - 1) {
+ break;
+ }
+
+ const moved = await goToNextPage();
+ if (!moved) {
+ log("已到最后一页,停止继续翻页。", "info");
+ break;
+ }
+ }
+
+ setStatus(`扫描完成,命中 ${state.results.length} 条。`);
+ log(`扫描完成,累计命中 ${state.results.length} 条。`, "success");
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "扫描失败";
+ if (message === "__YFB_STOPPED__") {
+ setStatus("任务已停止。");
+ log("任务已手动停止。", "warning");
+ } else {
+ setStatus(`扫描异常:${message}`);
+ log(`扫描异常:${message}`, "error");
+ }
+ } finally {
+ state.isRunning = false;
+ state.stopRequested = false;
+ state.stats.currentIndex = 0;
+ refreshView();
+ await persistState();
+ }
+ }
+
+ async function processRow(rowId, rowIndex) {
+ const rowMeta = findRowById(rowId) || collectRows()[rowIndex];
+ if (!rowMeta || !rowMeta.titleEl) {
+ log(`第 ${rowIndex + 1} 条记录在当前页已不可见,跳过。`, "warning");
+ return;
+ }
+
+ updateRowStatus(rowMeta.id, "scanning", "扫描中");
+ log(`正在处理:${rowMeta.title}`, "info");
+ await persistState();
+
+ if (shouldSkipRowByPreview(rowMeta)) {
+ state.stats.scanned += 1;
+ updateRowStatus(rowMeta.id, "skip", "列表预筛未命中");
+ log(`列表预筛跳过:${rowMeta.title}`, "info");
+ state.stats.hits = state.results.length;
+ refreshView();
+ await persistState();
+ return;
+ }
+
+ await clickElement(rowMeta.titleEl);
+ await sleep(state.settings.delayMs);
+ await waitForDetailPage();
+
+ try {
+ const detailRecord = extractDetailRecord(rowMeta);
+ const decision = await analyzeRecord(detailRecord);
+ state.stats.scanned += 1;
+
+ if (decision.isRelevant) {
+ addResult(detailRecord, decision);
+ updateRowStatus(rowMeta.id, "hit", decision.category);
+ showDetailBanner({
+ title: detailRecord.title,
+ category: decision.category,
+ confidence: decision.confidence,
+ reason: decision.reason
+ });
+ highlightKeywords(decision.matchedKeywords || []);
+ log(`命中:${detailRecord.title} -> ${decision.category}`, "success");
+ } else {
+ updateRowStatus(rowMeta.id, "skip", "未命中");
+ clearDetailHighlights();
+ log(`未命中:${detailRecord.title}`, "info");
+ }
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "记录处理失败";
+ updateRowStatus(rowMeta.id, "error", "异常");
+ log(`处理失败:${rowMeta.title},${message}`, "error");
+ } finally {
+ state.stats.hits = state.results.length;
+ refreshView();
+ await persistState();
+ await navigateBackToList();
+ await sleep(state.settings.delayMs);
+ restoreListHighlights();
+ }
+ }
+
+ function extractDetailRecord(rowMeta) {
+ const title = findTitleCandidate(rowMeta.title);
+ const detailText = collectDetailText(title);
+ const attachmentNames = uniqueText(
+ Array.from(document.querySelectorAll(".attachment-name")).map((element) => element.textContent || "")
+ );
+ const combinedText = normalizeText(
+ [title, rowMeta.type, rowMeta.region, rowMeta.publishTime, detailText, attachmentNames.join(" ")].join("\n")
+ );
+ const keywordHints = collectKeywordHints(combinedText);
+
+ return {
+ id: rowMeta.id,
+ title,
+ type: rowMeta.type,
+ region: rowMeta.region,
+ publishTime: rowMeta.publishTime,
+ detailUrl: location.href,
+ sourceUrl: location.href,
+ attachmentNames,
+ detailText,
+ keywordHints
+ };
+ }
+
+ async function analyzeRecord(detailRecord) {
+ const fallbackCategory = inferCategoryFromHints(detailRecord.keywordHints);
+ const fallbackSummary = buildSummary(detailRecord, detailRecord.keywordHints.all);
+
+ if (detailRecord.keywordHints.totalHits === 0) {
+ return {
+ isRelevant: false,
+ category: "不命中",
+ institutionType: [],
+ confidence: 0,
+ titleSummary: fallbackSummary,
+ reason: "标题、正文和附件名均未命中目标关键词。",
+ matchedKeywords: []
+ };
+ }
+
+ try {
+ const response = await runtimeSendMessage({
+ type: "YFB_ANALYZE_CANDIDATE",
+ payload: buildAiPayload(detailRecord)
+ });
+
+ if (!response || !response.ok) {
+ throw new Error(response?.error || "AI 分析失败");
+ }
+
+ const aiResult = response.data;
+ const normalizedCategory = aiResult.category && aiResult.category !== "不命中"
+ ? aiResult.category
+ : fallbackCategory;
+ return {
+ isRelevant: Boolean(aiResult.isRelevant) && normalizedCategory !== "不命中",
+ category: normalizedCategory || "不命中",
+ institutionType: Array.isArray(aiResult.institutionType) ? aiResult.institutionType : [],
+ confidence: Number.isFinite(aiResult.confidence) ? aiResult.confidence : 0,
+ titleSummary: aiResult.titleSummary || fallbackSummary,
+ reason: aiResult.reason || "AI 未返回判断依据。",
+ matchedKeywords: uniqueText([...(aiResult.matchedKeywords || []), ...detailRecord.keywordHints.all])
+ };
+ } catch (error) {
+ const degradeRelevant = Boolean(detailRecord.keywordHints.institutions.length && fallbackCategory !== "不命中");
+ return {
+ isRelevant: degradeRelevant,
+ category: degradeRelevant ? fallbackCategory : "不命中",
+ institutionType: detailRecord.keywordHints.institutions.slice(0, 3),
+ confidence: degradeRelevant ? 58 : 20,
+ titleSummary: fallbackSummary,
+ reason: degradeRelevant
+ ? `AI 调用失败,已按规则降级判定。${error instanceof Error ? error.message : ""}`.trim()
+ : `AI 调用失败,且规则未形成有效命中。${error instanceof Error ? error.message : ""}`.trim(),
+ matchedKeywords: detailRecord.keywordHints.all
+ };
+ }
+ }
+
+ async function navigateBackToList() {
+ if (isListPage()) {
+ return;
+ }
+
+ history.back();
+ await waitFor(() => isListPage(), 15000, 220, "返回列表页超时", false);
+ await waitForListReady(false);
+ }
+
+ async function goToNextPage() {
+ const nextButton = document.querySelector(NEXT_PAGE_SELECTOR);
+ if (!nextButton || nextButton.disabled || nextButton.classList.contains("is-disabled")) {
+ return false;
+ }
+
+ const previousFirstRowId = collectRows()[0]?.id || "";
+ await clickElement(nextButton);
+ await sleep(state.settings.delayMs);
+ await waitFor(
+ () => {
+ const currentRows = collectRows();
+ return currentRows.length > 0 && currentRows[0].id !== previousFirstRowId;
+ },
+ 15000,
+ 220,
+ "翻页超时"
+ );
+ await waitForListReady();
+ restoreListHighlights();
+ return true;
+ }
+
+ async function waitForListReady(respectStop = true) {
+ await waitFor(() => collectRows().length > 0, 15000, 220, "列表数据加载超时", respectStop);
+ }
+
+ async function waitForDetailPage() {
+ await waitFor(() => detectDetailPage(), 15000, 220, "详情页加载超时");
+ }
+
+ function detectDetailPage() {
+ if (document.querySelector(".see_source_url, .attachment-content, .attachment-name")) {
+ return true;
+ }
+
+ return !isListPage() && (location.href.includes("detail") || location.hash.includes("detail"));
+ }
+
+ function isListPage() {
+ return collectRows().length > 0;
+ }
+
+ function collectRows() {
+ const cardRows = collectCardRows();
+ if (cardRows.length > 0) {
+ return cardRows;
+ }
+
+ return collectTableRows();
+ }
+
+ function collectTableRows() {
+ return Array.from(document.querySelectorAll(LIST_ROW_SELECTOR))
+ .map((rowEl) => {
+ const cells = rowEl.querySelectorAll("td");
+ const titleEl = rowEl.querySelector(LIST_TITLE_SELECTOR) || rowEl.querySelector(".pointer");
+ const title = normalizeText(titleEl?.textContent || cells[0]?.innerText || "");
+ const type = normalizeText(cells[1]?.innerText || "");
+ const region = normalizeText(cells[2]?.innerText || "");
+ const publishTime = normalizeText(cells[3]?.innerText || "");
+
+ return buildRowMeta(rowEl, titleEl, title, type, region, publishTime);
+ })
+ .filter((item) => item.title && item.titleEl);
+ }
+
+ function collectCardRows() {
+ return Array.from(document.querySelectorAll(LIST_CARD_ROW_SELECTOR))
+ .filter((rowEl) => rowEl instanceof HTMLElement && isElementVisible(rowEl))
+ .map((rowEl) => {
+ const titleEl = rowEl.querySelector(LIST_CARD_TITLE_SELECTOR) || rowEl.querySelector(LIST_TITLE_SELECTOR);
+ const title = normalizeText(titleEl?.textContent || "");
+ const tokens = uniqueText(
+ Array.from(rowEl.querySelectorAll("span, em, small, p, div"))
+ .map((element) => normalizeText(element.textContent || ""))
+ .filter((text) => text && text.length <= 24)
+ );
+ const type = tokens.find((text) => isLikelyNoticeType(text)) || "";
+ const publishTime = tokens.find((text) => isLikelyPublishTime(text)) || "";
+ const region = tokens.find((text) => text !== type && text !== publishTime && isLikelyRegionText(text)) || "";
+
+ return buildRowMeta(rowEl, titleEl, title, type, region, publishTime);
+ })
+ .filter((item) => item.title && item.titleEl);
+ }
+
+ function buildRowMeta(rowEl, titleEl, title, type, region, publishTime) {
+ const previewText = limitLength(
+ removeBoilerplateText(normalizeText(rowEl?.innerText || ""), title),
+ Number(CONFIG.maxListPreviewChars) || 400
+ );
+
+ return {
+ id: buildRowId(title, type, region, publishTime || limitLength(previewText, 36)),
+ rowEl,
+ titleEl,
+ title,
+ type,
+ region,
+ publishTime,
+ previewText,
+ previewKeywordHints: collectKeywordHints([title, type, region, publishTime, previewText].join("\n"))
+ };
+ }
+
+ function isLikelyNoticeType(text) {
+ return /(公告|采购|招标|中标|商机|项目)/.test(text) && text.length <= 12;
+ }
+
+ function isLikelyPublishTime(text) {
+ return /^(\d{4}[./-]\d{1,2}[./-]\d{1,2}|\d{1,2}[./-]\d{1,2}|今天|昨日|刚刚|\d+分钟前)$/.test(text);
+ }
+
+ function isLikelyRegionText(text) {
+ if (!text || text.length > 18) {
+ return false;
+ }
+
+ return /省|市|区|县|自治区|自治州|旗|盟|镇|乡/.test(text) || text.includes(".") || text.includes("·");
+ }
+
+ function findRowById(rowId) {
+ return collectRows().find((item) => item.id === rowId) || null;
+ }
+
+ function buildRowId(title, type, region, publishTime) {
+ return [title, type, region, publishTime].map((item) => normalizeText(item)).join(" | ");
+ }
+
+ function getCurrentPageNumber() {
+ const activePage = document.querySelector(".page-item.active");
+ return clampNumber(activePage?.textContent || "1", 1, 9999, 1);
+ }
+
+ function findTitleCandidate(fallbackTitle) {
+ const candidates = Array.from(
+ document.querySelectorAll("h1, h2, h3, [class*='title'], [class*='name'], [class*='headline']")
+ )
+ .filter((element) => !element.closest(`#${PANEL_ID}`))
+ .map((element) => {
+ const text = normalizeText(element.textContent || "");
+ const rect = element.getBoundingClientRect();
+ const fontSize = Number.parseFloat(window.getComputedStyle(element).fontSize || "0");
+ const score = (rect.top >= 0 && rect.top < 520 ? 80 : 0) + fontSize + Math.max(0, 140 - text.length);
+ return { text, score };
+ })
+ .filter((item) => item.text.length >= 8 && item.text.length <= 140)
+ .sort((left, right) => right.score - left.score);
+
+ if (candidates.length > 0) {
+ return candidates[0].text;
+ }
+
+ return fallbackTitle;
+ }
+
+ function collectDetailText(title) {
+ const selectors = [
+ ".detail-content",
+ ".notice-content",
+ ".article-content",
+ ".rich-text",
+ ".ck-content",
+ ".w-e-text",
+ ".attachment-content",
+ "[class*='detail']",
+ "[class*='content']",
+ "[class*='article']",
+ "main",
+ "article",
+ ".el-main"
+ ];
+ const texts = [];
+
+ for (const selector of selectors) {
+ const elements = document.querySelectorAll(selector);
+ for (const element of elements) {
+ if (element.closest(`#${PANEL_ID}`)) {
+ continue;
+ }
+ const text = normalizeText(element.innerText || "");
+ if (text.length < 80) {
+ continue;
+ }
+ texts.push(text);
+ }
+ }
+
+ let merged = uniqueText(texts).join("\n\n");
+ if (!merged) {
+ merged = normalizeText(document.body.innerText || "");
+ }
+
+ return limitLength(removeBoilerplateText(merged, title), Number(CONFIG.maxBodyChars) || 12000);
+ }
+
+ function removeBoilerplateText(text, title) {
+ const boilerplateList = ["乙方宝", "查看全部消息", "营业执照", "ICP经营许可证", "发送到邮箱", "查看源网址"];
+ let result = normalizeText(text);
+
+ for (const item of boilerplateList) {
+ result = result.replaceAll(item, "");
+ }
+
+ if (title) {
+ result = result.replace(title + title, title);
+ }
+
+ return normalizeText(result);
+ }
+
+ function collectKeywordHints(text) {
+ const normalized = normalizeText(text);
+ const institutions = collectHits(normalized, KEYWORDS.institutions);
+ const legal = collectHits(normalized, KEYWORDS.legal);
+ const collection = collectHits(normalized, KEYWORDS.collection);
+ const mediation = collectHits(normalized, KEYWORDS.mediation);
+ const all = uniqueText([...institutions, ...legal, ...collection, ...mediation]);
+
+ return {
+ institutions,
+ legal,
+ collection,
+ mediation,
+ all,
+ totalHits: all.length
+ };
+ }
+
+ function collectHits(text, keywordList) {
+ return keywordList.filter((keyword) => text.includes(keyword));
+ }
+
+ function inferCategoryFromHints(keywordHints) {
+ const scores = [
+ { key: "legal", score: keywordHints.legal.length },
+ { key: "collection", score: keywordHints.collection.length },
+ { key: "mediation", score: keywordHints.mediation.length }
+ ].sort((left, right) => right.score - left.score);
+
+ if (!scores[0] || scores[0].score === 0) {
+ return "不命中";
+ }
+
+ return CATEGORY_LABELS[scores[0].key] || "不命中";
+ }
+
+ function buildSummary(detailRecord, matchedKeywords) {
+ const candidateLines = detailRecord.detailText
+ .split(/[\n。;]/)
+ .map((line) => normalizeText(line))
+ .filter((line) => line && line !== detailRecord.title && line.length >= 16);
+
+ if (candidateLines.length > 0) {
+ return limitLength(candidateLines[0], 60);
+ }
+
+ if (matchedKeywords.length > 0) {
+ return limitLength(`命中关键词:${matchedKeywords.join("、")}`, 60);
+ }
+
+ return limitLength(detailRecord.title, 60);
+ }
+
+ function buildAiPayload(detailRecord) {
+ return {
+ title: detailRecord.title,
+ type: detailRecord.type,
+ region: detailRecord.region,
+ publishTime: detailRecord.publishTime,
+ attachmentNames: Array.isArray(detailRecord.attachmentNames) ? detailRecord.attachmentNames.slice(0, 6) : [],
+ detailText: limitLength(
+ detailRecord.announcementContent || detailRecord.detailText || "",
+ Number(CONFIG.maxAiChars) || 1800
+ ),
+ keywordHints: {
+ institutions: detailRecord.keywordHints?.institutions?.slice(0, 6) || [],
+ legal: detailRecord.keywordHints?.legal?.slice(0, 6) || [],
+ collection: detailRecord.keywordHints?.collection?.slice(0, 6) || [],
+ mediation: detailRecord.keywordHints?.mediation?.slice(0, 6) || [],
+ all: detailRecord.keywordHints?.all?.slice(0, 12) || [],
+ totalHits: detailRecord.keywordHints?.totalHits || 0
+ }
+ };
+ }
+
+ function addResult(detailRecord, decision) {
+ const result = {
+ id: detailRecord.id,
+ title: detailRecord.title,
+ summary: decision.titleSummary || buildSummary(detailRecord, decision.matchedKeywords || []),
+ category: decision.category || "不命中",
+ institutionType: uniqueText(decision.institutionType || []),
+ matchedKeywords: uniqueText(decision.matchedKeywords || []),
+ confidence: Number(decision.confidence) || 0,
+ reason: decision.reason || "",
+ region: detailRecord.region,
+ publishTime: detailRecord.publishTime,
+ detailUrl: detailRecord.detailUrl,
+ sourceUrl: detailRecord.sourceUrl,
+ attachmentNames: detailRecord.attachmentNames
+ };
+
+ 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 showDetailBanner(result) {
+ clearDetailBanner();
+
+ const banner = document.createElement("div");
+ banner.id = BANNER_ID;
+ banner.dataset.resultId = result.id || "";
+ banner.innerHTML = `
+
+
+
已扫描0
+
命中数0
+
当前页1
+
当前条0
+
+
+
+
+
+
+
+
+
+
+
开始前停留在筛选列表页即可。脚本会先把订阅产品右侧 3 个分组依次切到“全部”,再进入详情抽取标题、正文和 AI 分析结果。
+
+
+ `;
+ }
+
+ function mountPanel() {
+ if (document.getElementById(PANEL_ID)) {
+ return;
+ }
+
+ const panel = document.createElement("div");
+ panel.id = PANEL_ID;
+ panel.innerHTML = buildPanelMarkup();
+
+ document.documentElement.appendChild(panel);
+
+ ui.panel = panel;
+ ui.status = panel.querySelector("[data-role='status']");
+ ui.logContainer = panel.querySelector("[data-role='logs']");
+ ui.scannedValue = panel.querySelector("[data-role='scanned']");
+ ui.hitsValue = panel.querySelector("[data-role='hits']");
+ ui.pageValue = panel.querySelector("[data-role='page']");
+ ui.rowValue = panel.querySelector("[data-role='row']");
+ ui.maxPagesInput = panel.querySelector("#yfb-max-pages");
+ ui.delayInput = panel.querySelector("#yfb-delay-ms");
+ ui.startButton = panel.querySelector("[data-role='start']");
+ ui.subscribeAllButton = panel.querySelector("[data-role='subscribe-all']");
+ ui.stopButton = panel.querySelector("[data-role='stop']");
+ ui.exportButton = panel.querySelector("[data-role='export']");
+ ui.clearButton = panel.querySelector("[data-role='clear']");
+ ui.toggleButton = panel.querySelector(".yfb-panel-toggle");
+
+ ui.maxPagesInput.value = String(state.settings.maxPages);
+ ui.delayInput.value = String(state.settings.delayMs);
+
+ ui.maxPagesInput.addEventListener("change", handleSettingsChange);
+ ui.delayInput.addEventListener("change", handleSettingsChange);
+ ui.startButton.addEventListener("click", () => { void startScan(); });
+
+ if (ui.subscribeAllButton) {
+ ui.subscribeAllButton.addEventListener("click", () => { void doSubscribeAll(); });
+ }
+
+ ui.stopButton.addEventListener("click", stopScan);
+ ui.exportButton.addEventListener("click", exportResults);
+ ui.clearButton.addEventListener("click", clearResults);
+ ui.toggleButton.addEventListener("click", toggleCollapse);
+
+ enableDrag(panel.querySelector(".yfb-panel-header"), panel);
+ }
+
+ async function startScan() {
+ if (state.isRunning) {
+ return;
+ }
+
+ handleSettingsChange();
+
+ if (!isListPage()) {
+ setStatus("请先打开乙方宝招标筛选列表页,再启动扫描。");
+ log("当前页面不是招标筛选列表页,已取消启动。", "warning");
+ refreshView();
+ return;
+ }
+
+ state.isRunning = true;
+ state.stopRequested = false;
+ setStatus(`准备开始扫描,最多 ${state.settings.maxPages} 页。`);
+ log(`开始扫描,最多 ${state.settings.maxPages} 页,步进延迟 ${state.settings.delayMs}ms。`, "info");
+ refreshView();
+
+ try {
+ await waitForListReady();
+ await prepareSubscriptionFilters();
+ await waitForListReady();
+
+ for (let pageOffset = 0; pageOffset < state.settings.maxPages; pageOffset += 1) {
+ throwIfStopped();
+ state.stats.currentPage = getCurrentPageNumber() || pageOffset + 1;
+ state.stats.currentIndex = 0;
+ refreshView();
+
+ const rows = collectRows();
+ if (rows.length === 0) {
+ throw new Error("列表页没有可扫描的数据。");
+ }
+
+ log(`第 ${state.stats.currentPage} 页识别到 ${rows.length} 条记录。`, "info");
+
+ for (let rowIndex = 0; rowIndex < rows.length; rowIndex += 1) {
+ throwIfStopped();
+ state.stats.currentIndex = rowIndex + 1;
+ refreshView();
+ await processRow(rows[rowIndex].id, rowIndex);
+ }
+
+ if (pageOffset >= state.settings.maxPages - 1) {
+ break;
+ }
+
+ const moved = await goToNextPage();
+ if (!moved) {
+ log("已经到最后一页,停止继续翻页。", "info");
+ break;
+ }
+ }
+
+ setStatus(`扫描完成,命中 ${state.results.length} 条。`);
+ log(`扫描完成,累计命中 ${state.results.length} 条。`, "success");
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "扫描失败";
+ if (message === "__YFB_STOPPED__") {
+ setStatus("任务已停止。");
+ log("任务已手动停止。", "warning");
+ } else {
+ setStatus(`扫描异常:${message}`);
+ log(`扫描异常:${message}`, "error");
+ }
+ } finally {
+ state.isRunning = false;
+ state.stopRequested = false;
+ state.stats.currentIndex = 0;
+ refreshView();
+ await persistState();
+ }
+ }
+
+ function getSubscriptionGroupTabs() {
+ const resolvedGroups = [];
+ const usedElements = new Set();
+
+ for (const definition of SUBSCRIPTION_GROUP_DEFINITIONS) {
+ const element = findSubscriptionGroupElement(definition);
+ if (!(element instanceof HTMLElement) || usedElements.has(element)) {
+ continue;
+ }
+
+ usedElements.add(element);
+ resolvedGroups.push({
+ key: definition.key,
+ label: readSubscriptionGroupLabel(element, definition.labels[0] || definition.key),
+ element
+ });
+ }
+
+ if (resolvedGroups.length > 0) {
+ return resolvedGroups;
+ }
+
+ return collectGenericSubscriptionGroupCandidates()
+ .slice(0, 3)
+ .map((element, index) => ({
+ key: `fallback-${index + 1}`,
+ label: readSubscriptionGroupLabel(element, `分组${index + 1}`),
+ element
+ }));
+ }
+
+ async function prepareSubscriptionFilters(options = {}) {
+ const strict = Boolean(options.strict);
+ const tabs = getSubscriptionGroupTabs();
+ if (tabs.length === 0) {
+ const message = "未识别到订阅产品分组,请确认筛选面板已展开。";
+ if (strict) {
+ throw new Error(message);
+ }
+ log(`${message} 已跳过自动预处理。`, "warning");
+ return [];
+ }
+
+ const missingGroups = SUBSCRIPTION_GROUP_DEFINITIONS
+ .filter((definition) => !tabs.some((tab) => tab.key === definition.key));
+ if (strict && missingGroups.length > 0) {
+ throw new Error(`仅识别到 ${tabs.length} 个分组,缺少:${missingGroups.map((item) => item.labels[0]).join("、")}`);
+ }
+ if (missingGroups.length > 0) {
+ log(`订阅分组未全部识别,当前缺少:${missingGroups.map((item) => item.labels[0]).join("、")}。`, "warning");
+ }
+
+ setStatus("正在把订阅产品右侧 3 个分组切到“全部”…");
+ refreshView();
+
+ for (const [index, group] of tabs.entries()) {
+ throwIfStopped();
+ await applyAllOptionToGroupTab(group, index);
+ }
+
+ setStatus("订阅产品筛选已切到“全部”,开始扫描列表。");
+ refreshView();
+ return tabs;
+ }
+
+ async function applyAllOptionToGroupTab(group, groupIndex) {
+ if (!group?.element) {
+ return;
+ }
+
+ const tabLabel = normalizeText(group.label || group.element.textContent || "").replace(/\(\d+\)/g, "");
+ await openSubscriptionGroupTab(group.element);
+ const allItem = findVisibleAllSubcategoryItem();
+
+ if (!allItem) {
+ log(`分组“${tabLabel || groupIndex + 1}”未找到“全部”选项。`, "warning");
+ return;
+ }
+
+ await clickElement(allItem);
+ await sleep(120);
+ const confirmButton = findVisibleConfirmButton();
+ if (confirmButton) {
+ await clickElement(confirmButton);
+ } else {
+ log(`分组“${tabLabel || groupIndex + 1}”未找到确认按钮,已按自动生效继续。`, "warning");
+ }
+ await waitForUiSettled();
+ log(`分组“${tabLabel || groupIndex + 1}”已切到“全部”。`, "info");
+ }
+
+ function findSubscriptionGroupElement(definition) {
+ const candidates = collectGenericSubscriptionGroupCandidates();
+
+ for (const element of candidates) {
+ const label = readSubscriptionGroupLabel(element);
+ if (definition.labels.some((item) => label.includes(item))) {
+ return element;
+ }
+ }
+
+ for (const selector of definition.selectors) {
+ const fallback = Array.from(document.querySelectorAll(selector))
+ .find((element) => element instanceof HTMLElement && isElementVisible(element));
+ if (fallback instanceof HTMLElement) {
+ return fallback;
+ }
+ }
+
+ return null;
+ }
+
+ function collectGenericSubscriptionGroupCandidates() {
+ const selectors = [
+ SUBSCRIPTION_GROUP_TAB_SELECTOR,
+ ".subscribe-product-filter .filter-header .right-tabs .tab-item",
+ ".filter-container > div",
+ ".select-filters-wrap > div",
+ ".select-filters-wrap > div > span"
+ ];
+ const seenTexts = new Set();
+ const candidates = [];
+
+ for (const selector of selectors) {
+ for (const element of document.querySelectorAll(selector)) {
+ if (!(element instanceof HTMLElement) || !isElementVisible(element)) {
+ continue;
+ }
+
+ const text = readSubscriptionGroupLabel(element);
+ if (!text || text.startsWith("全部") || text.includes("订阅词管理") || text.includes("选择地区")) {
+ continue;
+ }
+ if (!/\(\d+\)/.test(text)) {
+ continue;
+ }
+ if (seenTexts.has(text)) {
+ continue;
+ }
+
+ seenTexts.add(text);
+ candidates.push(element);
+ }
+ }
+
+ return candidates;
+ }
+
+ function readSubscriptionGroupLabel(element, fallback = "") {
+ const text = normalizeText(element?.textContent || "");
+ return text || fallback;
+ }
+
+ async function openSubscriptionGroupTab(tab) {
+ const clickTargets = [
+ tab.querySelector(".tab-item-icon"),
+ tab.querySelector("i"),
+ tab.querySelector("span"),
+ tab
+ ].filter((element, index, list) => element instanceof HTMLElement && list.indexOf(element) === index);
+
+ for (const target of clickTargets) {
+ await clickElement(target);
+ await sleep(Math.min(220, state.settings.delayMs));
+ if (findVisibleAllSubcategoryItem()) {
+ return;
+ }
+ }
+ }
+
+ function findVisibleAllSubcategoryItem() {
+ const subcategoryContainer = getVisibleSubcategoryContainer();
+ const scope = subcategoryContainer || document;
+
+ return Array.from(scope.querySelectorAll(SUBSCRIPTION_ALL_ITEM_SELECTOR))
+ .filter((item) => isElementVisible(item))
+ .find((item) => normalizeText(item.textContent || "").startsWith("全部")) || null;
+ }
+
+ function findVisibleConfirmButton() {
+ const subcategoryContainer = getVisibleSubcategoryContainer();
+ const scope = subcategoryContainer?.parentElement || document;
+
+ return Array.from(scope.querySelectorAll(SUBSCRIPTION_CONFIRM_SELECTOR))
+ .find((button) => isElementVisible(button) && normalizeText(button.textContent || "").includes("确认")) || null;
+ }
+
+ function getVisibleSubcategoryContainer() {
+ return Array.from(document.querySelectorAll(".subcategory-content"))
+ .filter((element) => element instanceof HTMLElement && isElementVisible(element))
+ .sort((left, right) => right.getBoundingClientRect().height - left.getBoundingClientRect().height)[0] || null;
+ }
+
+ async function processRow(rowId, rowIndex) {
+ const rowMeta = findRowById(rowId) || collectRows()[rowIndex];
+ if (!rowMeta || !rowMeta.titleEl) {
+ log(`第 ${rowIndex + 1} 条记录在当前页已不可见,跳过。`, "warning");
+ return;
+ }
+
+ updateRowStatus(rowMeta.id, "scanning", "扫描中");
+ log(`正在处理:${rowMeta.title}`, "info");
+ await persistState();
+
+ if (shouldSkipRowByPreview(rowMeta)) {
+ state.stats.scanned += 1;
+ updateRowStatus(rowMeta.id, "skip", "列表预筛未命中");
+ log(`列表预筛跳过:${rowMeta.title}`, "info");
+ state.stats.hits = state.results.length;
+ refreshView();
+ await persistState();
+ return;
+ }
+
+ await clickElement(rowMeta.titleEl);
+ await sleep(state.settings.delayMs);
+ await waitForDetailPage();
+ await waitForUiSettled();
+ dismissKnownDialogs();
+ await sleep(120);
+
+ try {
+ const detailRecord = extractDetailRecord(rowMeta);
+ const decision = await analyzeRecord(detailRecord);
+ state.stats.scanned += 1;
+
+ if (decision.isRelevant) {
+ addResult(detailRecord, decision);
+ updateRowStatus(rowMeta.id, "hit", decision.category);
+ showDetailBanner({
+ id: detailRecord.id,
+ title: detailRecord.title,
+ category: decision.category,
+ confidence: decision.confidence,
+ reason: decision.reason
+ });
+ highlightKeywords(decision.matchedKeywords || []);
+ log(`命中:${detailRecord.title} -> ${decision.category}`, "success");
+ } else {
+ updateRowStatus(rowMeta.id, "skip", "未命中");
+ clearDetailHighlights();
+ log(`未命中:${detailRecord.title}`, "info");
+ }
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "记录处理失败";
+ updateRowStatus(rowMeta.id, "error", "异常");
+ log(`处理失败:${rowMeta.title},${message}`, "error");
+ } finally {
+ state.stats.hits = state.results.length;
+ refreshView();
+ await persistState();
+ await navigateBackToList();
+ await sleep(state.settings.delayMs);
+ restoreListHighlights();
+ }
+ }
+
+ function extractDetailRecord(rowMeta) {
+ const title = findTitleCandidate(rowMeta.title);
+ const detailMeta = collectDetailMeta();
+ const summaryFields = collectSummaryFields(detailMeta);
+ const announcementContent = collectAnnouncementContent(title);
+ const attachmentNames = uniqueText(
+ Array.from(document.querySelectorAll(".attachment-name")).map((element) => element.textContent || "")
+ );
+ const detailText = collectDetailText(title, {
+ type: detailMeta.type || rowMeta.type,
+ region: detailMeta.region || rowMeta.region,
+ publishTime: detailMeta.publishTime || rowMeta.publishTime,
+ summaryFields,
+ announcementContent,
+ attachmentNames
+ });
+ const combinedText = normalizeText(
+ [
+ title,
+ detailMeta.type || rowMeta.type,
+ detailMeta.region || rowMeta.region,
+ detailMeta.publishTime || rowMeta.publishTime,
+ summaryFields.projectNumber,
+ summaryFields.bidder,
+ summaryFields.agency,
+ summaryFields.signupDeadline,
+ summaryFields.bidDeadline,
+ detailText,
+ attachmentNames.join(" ")
+ ].join("\n")
+ );
+ const keywordHints = collectKeywordHints(combinedText);
+
+ return {
+ id: rowMeta.id,
+ title,
+ type: detailMeta.type || rowMeta.type,
+ region: detailMeta.region || rowMeta.region,
+ publishTime: detailMeta.publishTime || rowMeta.publishTime,
+ detailUrl: location.href,
+ sourceUrl: readSourceUrl(),
+ attachmentNames,
+ announcementContent,
+ detailText,
+ projectNumber: summaryFields.projectNumber,
+ estimatedAmount: summaryFields.estimatedAmount,
+ bidder: summaryFields.bidder,
+ agency: summaryFields.agency,
+ signupDeadline: summaryFields.signupDeadline,
+ bidDeadline: summaryFields.bidDeadline,
+ keywordHints
+ };
+ }
+
+ function collectDetailMeta() {
+ const propertyItems = Array.from(document.querySelectorAll(".long-project-property-left span"))
+ .map((element) => normalizeText(element.textContent || ""))
+ .filter(Boolean);
+
+ return {
+ type: propertyItems[0] || "",
+ region: propertyItems[1] || "",
+ publishTime: propertyItems[2] || ""
+ };
+ }
+
+ function shouldSkipRowByPreview(rowMeta) {
+ return Boolean(rowMeta?.previewKeywordHints && rowMeta.previewKeywordHints.totalHits === 0);
+ }
+
+ function collectSummaryFields(detailMeta = {}) {
+ const summaryCard = findDetailCardByTitle("正文摘要");
+ const fieldMap = {};
+
+ if (summaryCard) {
+ Array.from(summaryCard.querySelectorAll(".table-line")).forEach((line) => {
+ const cells = Array.from(line.children).filter((element) => normalizeText(element.textContent || ""));
+ for (let index = 0; index < cells.length; index += 2) {
+ const label = normalizeText(cells[index]?.textContent || "");
+ const value = sanitizeExtractedText(extractFieldValue(cells[index + 1]));
+ if (label) {
+ fieldMap[label] = value;
+ }
+ }
+ });
+ }
+
+ return {
+ projectNumber: mapSummaryField(fieldMap, ["项目编号"]),
+ estimatedAmount: mapSummaryField(fieldMap, ["预估金额"]),
+ bidder: mapSummaryField(fieldMap, ["招标单位", "采购单位", "业主单位"]),
+ agency: mapSummaryField(fieldMap, ["代理单位", "招标代理"]),
+ signupDeadline: mapSummaryField(fieldMap, ["报名截止时间", "报名截止"]),
+ bidDeadline: mapSummaryField(fieldMap, ["投标截止时间", "开标时间", "开标日期"]),
+ noticeType: detailMeta.type || "",
+ region: detailMeta.region || "",
+ publishTime: detailMeta.publishTime || ""
+ };
+ }
+
+ function mapSummaryField(fieldMap, labels) {
+ for (const label of labels) {
+ const value = normalizeText(fieldMap[label] || "");
+ if (value) {
+ return value;
+ }
+ }
+ return "";
+ }
+
+ function findDetailCardByTitle(title) {
+ const titleElements = Array.from(document.querySelectorAll(".card-title"));
+ const matchedTitle = titleElements.find((element) => normalizeText(element.textContent || "") === title);
+ return matchedTitle?.closest(".card-layout") || null;
+ }
+
+ function extractFieldValue(element) {
+ if (!(element instanceof HTMLElement)) {
+ return "";
+ }
+
+ const clone = element.cloneNode(true);
+ clone.querySelectorAll(
+ [
+ ".el-popover",
+ ".el-dialog__wrapper",
+ ".el-tooltip__popper",
+ ".m-notification-wrap",
+ "button",
+ "svg",
+ "img",
+ "i",
+ "script",
+ "style",
+ "mark"
+ ].join(", ")
+ ).forEach((node) => node.remove());
+ clone.querySelectorAll("[style*='display: none']").forEach((node) => node.remove());
+
+ return clone.innerText || clone.textContent || "";
+ }
+
+ function sanitizeExtractedText(text) {
+ return normalizeText(
+ String(text || "")
+ .replaceAll("监控", "")
+ .replaceAll("点击查看", "")
+ .replaceAll("查看联系人详情", "")
+ );
+ }
+
+ function collectAnnouncementContent(title) {
+ const texts = [];
+
+ for (const selector of DETAIL_CONTENT_SELECTORS) {
+ document.querySelectorAll(selector).forEach((element) => {
+ if (!(element instanceof HTMLElement) || !isElementVisible(element)) {
+ return;
+ }
+ if (element.closest(`#${PANEL_ID}`) || element.closest(".el-dialog__wrapper")) {
+ return;
+ }
+
+ const text = sanitizeExtractedText(extractFieldValue(element));
+ if (text.length >= 40) {
+ texts.push(text);
+ }
+ });
+ }
+
+ const content = uniqueText(texts).sort((left, right) => right.length - left.length)[0] || "";
+ return limitLength(removeBoilerplateText(content, title), Number(CONFIG.maxBodyChars) || 12000);
+ }
+
+ function readSourceUrl() {
+ const sourceLink = document.querySelector(".see_source_url a[href], a[href].see_source_url");
+ return sourceLink instanceof HTMLAnchorElement ? sourceLink.href : location.href;
+ }
+
+ async function waitForUiSettled(respectStop = true) {
+ await sleep(120);
+ try {
+ await waitFor(() => !hasVisibleLoadingMask(), 10000, 120, "页面加载超时", respectStop);
+ } catch (error) {
+ log("页面加载等待超时,继续按当前页面处理。", "warning");
+ }
+ await sleep(120);
+ }
+
+ function hasVisibleLoadingMask() {
+ return Array.from(document.querySelectorAll(".el-loading-mask")).some((element) => isElementVisible(element));
+ }
+
+ function dismissKnownDialogs() {
+ Array.from(document.querySelectorAll(".el-dialog__wrapper"))
+ .filter((wrapper) => isElementVisible(wrapper))
+ .forEach((wrapper) => {
+ const title = normalizeText(wrapper.querySelector(".dialog-title, .el-dialog__title")?.textContent || "");
+ if (!title || !KNOWN_DIALOG_TITLES.some((item) => title.includes(item))) {
+ return;
+ }
+
+ const closeButton = wrapper.querySelector(".el-dialog__headerbtn, .close-btn, [aria-label='Close']");
+ if (closeButton instanceof HTMLElement) {
+ closeButton.click();
+ } else {
+ wrapper.style.display = "none";
+ }
+ });
+ }
+
+ async function waitForListReady(respectStop = true) {
+ await waitFor(() => collectRows().length > 0, 15000, 220, "列表数据加载超时", respectStop);
+ await waitForUiSettled(respectStop);
+ }
+
+ async function waitForDetailPage() {
+ await waitFor(() => detectDetailPage(), 15000, 220, "详情页加载超时");
+ }
+
+ function detectDetailPage() {
+ return Boolean(
+ document.querySelector(DETAIL_TITLE_SELECTORS.join(", ")) ||
+ document.querySelector(DETAIL_CONTENT_SELECTORS.join(", ")) ||
+ document.querySelector(".project-detail-content")
+ );
+ }
+
+ function findTitleCandidate(fallbackTitle) {
+ for (const selector of DETAIL_TITLE_SELECTORS) {
+ const element = Array.from(document.querySelectorAll(selector)).find((item) => isElementVisible(item));
+ const text = normalizeText(element?.textContent || "");
+ if (text && !KNOWN_DIALOG_TITLES.some((item) => text.includes(item))) {
+ return text;
+ }
+ }
+
+ const candidates = Array.from(
+ document.querySelectorAll("h1, h2, h3, [class*='title'], [class*='name'], [class*='headline']")
+ )
+ .filter((element) => !element.closest(`#${PANEL_ID}`) && !element.closest(".el-dialog__wrapper"))
+ .filter((element) => isElementVisible(element))
+ .map((element) => {
+ const text = normalizeText(element.textContent || "");
+ const rect = element.getBoundingClientRect();
+ const fontSize = Number.parseFloat(window.getComputedStyle(element).fontSize || "0");
+ const score = (rect.top >= 0 && rect.top < 520 ? 80 : 0) + fontSize + Math.max(0, 140 - text.length);
+ return { text, score };
+ })
+ .filter((item) => item.text.length >= 8 && item.text.length <= 160)
+ .filter((item) => !KNOWN_DIALOG_TITLES.some((title) => item.text.includes(title)))
+ .sort((left, right) => right.score - left.score);
+
+ if (candidates.length > 0) {
+ return candidates[0].text;
+ }
+
+ return fallbackTitle;
+ }
+
+ function collectDetailText(title, detailData = {}) {
+ const lines = [
+ title,
+ detailData.type ? `公告类型:${detailData.type}` : "",
+ detailData.region ? `地区:${detailData.region}` : "",
+ detailData.publishTime ? `发布时间:${detailData.publishTime}` : "",
+ detailData.summaryFields?.projectNumber ? `项目编号:${detailData.summaryFields.projectNumber}` : "",
+ detailData.summaryFields?.estimatedAmount ? `预估金额:${detailData.summaryFields.estimatedAmount}` : "",
+ detailData.summaryFields?.bidder ? `招标单位:${detailData.summaryFields.bidder}` : "",
+ detailData.summaryFields?.agency ? `代理单位:${detailData.summaryFields.agency}` : "",
+ detailData.summaryFields?.signupDeadline ? `报名截止时间:${detailData.summaryFields.signupDeadline}` : "",
+ detailData.summaryFields?.bidDeadline ? `投标截止时间:${detailData.summaryFields.bidDeadline}` : "",
+ detailData.announcementContent || "",
+ Array.isArray(detailData.attachmentNames) && detailData.attachmentNames.length
+ ? `附件:${detailData.attachmentNames.join(";")}`
+ : ""
+ ].filter(Boolean);
+
+ return limitLength(
+ removeBoilerplateText(lines.join("\n"), title),
+ Number(CONFIG.maxBodyChars) || 12000
+ );
+ }
+
+ function buildSummary(detailRecord, matchedKeywords) {
+ const candidateText = detailRecord.announcementContent || detailRecord.detailText || detailRecord.title;
+ const candidateLines = candidateText
+ .split(/[\n。;]/)
+ .map((line) => normalizeText(line))
+ .filter((line) => {
+ if (!line || line === detailRecord.title) {
+ return false;
+ }
+ return ![
+ "项目编号",
+ "预估金额",
+ "招标单位",
+ "代理单位",
+ "报名截止时间",
+ "投标截止时间"
+ ].some((prefix) => line.startsWith(prefix));
+ })
+ .filter((line) => line.length >= 12);
+
+ if (candidateLines.length > 0) {
+ return limitLength(candidateLines[0], 60);
+ }
+
+ if (matchedKeywords.length > 0) {
+ return limitLength(`命中关键词:${matchedKeywords.join("、")}`, 60);
+ }
+
+ return limitLength(detailRecord.title, 60);
+ }
+
+ function addResult(detailRecord, decision) {
+ const result = {
+ id: detailRecord.id,
+ title: detailRecord.title,
+ summary: decision.titleSummary || buildSummary(detailRecord, decision.matchedKeywords || []),
+ 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 showDetailBanner(result) {
+ clearDetailBanner();
+
+ const banner = document.createElement("div");
+ banner.id = BANNER_ID;
+ banner.dataset.resultId = result.id || "";
+ banner.innerHTML = `
+