feat: 优化插件,减少详情页频繁跳转,拿不到url转隐藏tab提取
This commit is contained in:
parent
dabda879d9
commit
c109d0e182
137
background.js
137
background.js
@ -18,11 +18,12 @@ chrome.action.onClicked.addListener((tab) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||||
if (!message || message.type !== "YFB_ANALYZE_CANDIDATE") {
|
if (!message?.type) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (message.type === "YFB_ANALYZE_CANDIDATE") {
|
||||||
void analyzeCandidate(message.payload)
|
void analyzeCandidate(message.payload)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
sendResponse({ ok: true, data });
|
sendResponse({ ok: true, data });
|
||||||
@ -35,7 +36,139 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.type === "YFB_PROCESS_DETAIL_IN_HIDDEN_TAB") {
|
||||||
|
void processDetailInHiddenTab(message.payload)
|
||||||
|
.then((data) => {
|
||||||
|
sendResponse({ ok: true, data });
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
sendResponse({
|
||||||
|
ok: false,
|
||||||
|
error: error instanceof Error ? error.message : "详情抓取失败"
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
async function processDetailInHiddenTab(payload) {
|
||||||
|
const detailUrl = String(payload?.detailUrl || "").trim();
|
||||||
|
const rowMeta = payload?.rowMeta || {};
|
||||||
|
|
||||||
|
if (!detailUrl) {
|
||||||
|
throw new Error("缺少详情页地址");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isSupportedDetailUrl(detailUrl)) {
|
||||||
|
throw new Error("详情页地址不受支持");
|
||||||
|
}
|
||||||
|
|
||||||
|
const tab = await chrome.tabs.create({
|
||||||
|
url: detailUrl,
|
||||||
|
active: false
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!tab?.id) {
|
||||||
|
throw new Error("隐藏详情标签页创建失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await waitForTabComplete(tab.id, 20000);
|
||||||
|
await waitForDetailWorkerReady(tab.id, 12000);
|
||||||
|
const response = await sendMessageToTab(tab.id, {
|
||||||
|
type: "YFB_RUN_DETAIL_EXTRACTION",
|
||||||
|
payload: {
|
||||||
|
rowMeta,
|
||||||
|
detailUrl
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response?.ok || !response.data) {
|
||||||
|
throw new Error(response?.error || "详情提取失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} finally {
|
||||||
|
await closeTabQuietly(tab.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSupportedDetailUrl(url) {
|
||||||
|
try {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
return /(^|\.)yfbzb\.com$/i.test(parsed.hostname) || /(^|\.)qianlima\.com$/i.test(parsed.hostname);
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForTabComplete(tabId, timeoutMs) {
|
||||||
|
const tab = await chrome.tabs.get(tabId);
|
||||||
|
if (tab?.status === "complete") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
chrome.tabs.onUpdated.removeListener(handleUpdated);
|
||||||
|
reject(new Error("详情页加载超时"));
|
||||||
|
}, timeoutMs);
|
||||||
|
|
||||||
|
const handleUpdated = (updatedTabId, changeInfo) => {
|
||||||
|
if (updatedTabId !== tabId || changeInfo.status !== "complete") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
chrome.tabs.onUpdated.removeListener(handleUpdated);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
chrome.tabs.onUpdated.addListener(handleUpdated);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForDetailWorkerReady(tabId, timeoutMs) {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
while (Date.now() - startTime < timeoutMs) {
|
||||||
|
const response = await sendMessageToTab(tabId, { type: "YFB_DETAIL_WORKER_PING" });
|
||||||
|
if (response?.ok) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await delay(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("详情页脚本未就绪");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendMessageToTab(tabId, message) {
|
||||||
|
try {
|
||||||
|
return await chrome.tabs.sendMessage(tabId, message);
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: error instanceof Error ? error.message : "标签页通信失败"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function closeTabQuietly(tabId) {
|
||||||
|
try {
|
||||||
|
await chrome.tabs.remove(tabId);
|
||||||
|
} catch (error) {
|
||||||
|
void error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function delay(ms) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
async function analyzeCandidate(payload) {
|
async function analyzeCandidate(payload) {
|
||||||
if (!CONFIG.apiKey) {
|
if (!CONFIG.apiKey) {
|
||||||
|
|||||||
553
content.js
553
content.js
@ -11,8 +11,115 @@
|
|||||||
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 DB_NAME = "YfbBidHistoryDB";
|
||||||
|
const STORE_NAME = "CrawlHistory";
|
||||||
|
|
||||||
|
function openDB() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const request = indexedDB.open(DB_NAME, 1);
|
||||||
|
request.onerror = () => reject(request.error);
|
||||||
|
request.onsuccess = () => resolve(request.result);
|
||||||
|
request.onupgradeneeded = (event) => {
|
||||||
|
const db = event.target.result;
|
||||||
|
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
||||||
|
const store = db.createObjectStore(STORE_NAME, { keyPath: "id" });
|
||||||
|
store.createIndex("publishTime", "publishTime", { unique: false });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getHistoryRecord(id) {
|
||||||
|
if(!id) return null;
|
||||||
|
const db = await openDB();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const tx = db.transaction(STORE_NAME, "readonly");
|
||||||
|
const store = tx.objectStore(STORE_NAME);
|
||||||
|
const req = store.get(id);
|
||||||
|
req.onsuccess = () => resolve(req.result);
|
||||||
|
req.onerror = () => reject(req.error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveHistoryRecord(record) {
|
||||||
|
if(!record.id) return;
|
||||||
|
const db = await openDB();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const tx = db.transaction(STORE_NAME, "readwrite");
|
||||||
|
const store = tx.objectStore(STORE_NAME);
|
||||||
|
const req = store.put({
|
||||||
|
id: record.id,
|
||||||
|
url: record.url || record.id || "",
|
||||||
|
title: record.title || "",
|
||||||
|
publishTime: record.publishTime || "",
|
||||||
|
processedAt: Date.now()
|
||||||
|
});
|
||||||
|
req.onsuccess = () => resolve();
|
||||||
|
req.onerror = () => reject(req.error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearOldHistory() {
|
||||||
|
try {
|
||||||
|
const db = await openDB();
|
||||||
|
const threeMonthsAgo = Date.now() - 3 * 30 * 24 * 60 * 60 * 1000;
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const tx = db.transaction(STORE_NAME, "readwrite");
|
||||||
|
const store = tx.objectStore(STORE_NAME);
|
||||||
|
const req = store.openCursor();
|
||||||
|
req.onsuccess = (e) => {
|
||||||
|
const cursor = e.target.result;
|
||||||
|
if (cursor) {
|
||||||
|
if (cursor.value.processedAt < threeMonthsAgo) {
|
||||||
|
cursor.delete();
|
||||||
|
}
|
||||||
|
cursor.continue();
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
req.onerror = () => reject(req.error);
|
||||||
|
});
|
||||||
|
} catch(e) {
|
||||||
|
console.warn("清理历史记录失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePublishTimeToTs(timeStr) {
|
||||||
|
if (!timeStr) return 0;
|
||||||
|
timeStr = timeStr.trim();
|
||||||
|
const now = Date.now();
|
||||||
|
if (timeStr.includes("刚刚")) return now;
|
||||||
|
if (timeStr.includes("分钟前")) {
|
||||||
|
const min = parseInt(timeStr.replace(/\D/g, "")) || 0;
|
||||||
|
return now - min * 60000;
|
||||||
|
}
|
||||||
|
if (timeStr.includes("小时前")) {
|
||||||
|
const hr = parseInt(timeStr.replace(/\D/g, "")) || 0;
|
||||||
|
return now - hr * 3600000;
|
||||||
|
}
|
||||||
|
if (timeStr.includes("今天")) return new Date(new Date().setHours(0,0,0,0)).getTime();
|
||||||
|
if (timeStr.includes("昨日") || timeStr.includes("昨天")) {
|
||||||
|
return new Date(new Date().setHours(0,0,0,0)).getTime() - 86400000;
|
||||||
|
}
|
||||||
|
const parsed = new Date(timeStr.replace(/\./g, "-")).getTime();
|
||||||
|
if (!isNaN(parsed)) return parsed;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTimeInRange(timeTs, range) {
|
||||||
|
if (range === "all" || !range) return true;
|
||||||
|
if (!timeTs) return true;
|
||||||
|
const now = Date.now();
|
||||||
|
const todayStart = new Date(new Date().setHours(0,0,0,0)).getTime();
|
||||||
|
if (range === "today") return timeTs >= todayStart;
|
||||||
|
if (range === "24h") return timeTs >= (now - 24 * 3600000);
|
||||||
|
if (range === "3d") return timeTs >= (now - 3 * 24 * 3600000);
|
||||||
|
if (range === "1w") return timeTs >= (now - 7 * 24 * 3600000);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
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";
|
||||||
@ -146,10 +253,9 @@
|
|||||||
stopRequested: false,
|
stopRequested: false,
|
||||||
panelCollapsed: false,
|
panelCollapsed: false,
|
||||||
panelHidden: false,
|
panelHidden: false,
|
||||||
hasCustomMaxPages: false,
|
|
||||||
statusText: "等待开始",
|
statusText: "等待开始",
|
||||||
settings: {
|
settings: {
|
||||||
maxPages: DEFAULT_MAX_PAGES,
|
maxPages: 3,
|
||||||
delayMs: 300
|
delayMs: 300
|
||||||
},
|
},
|
||||||
stats: {
|
stats: {
|
||||||
@ -438,15 +544,8 @@
|
|||||||
|
|
||||||
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 || {};
|
||||||
@ -475,6 +574,16 @@
|
|||||||
<label for="yfb-max-pages">最大页数</label>
|
<label for="yfb-max-pages">最大页数</label>
|
||||||
<input id="yfb-max-pages" type="number" min="1" max="200" />
|
<input id="yfb-max-pages" type="number" min="1" max="200" />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="yfb-field">
|
||||||
|
<label for="yfb-time-range">抓取范围</label>
|
||||||
|
<select id="yfb-time-range">
|
||||||
|
<option value="today">仅看今天</option>
|
||||||
|
<option value="24h">近 24 小时</option>
|
||||||
|
<option value="3d">近 3 天</option>
|
||||||
|
<option value="1w">近 1 周</option>
|
||||||
|
<option value="all" selected>全部(依赖页数)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div class="yfb-field">
|
<div class="yfb-field">
|
||||||
<label for="yfb-delay-ms">步进延迟(ms)</label>
|
<label for="yfb-delay-ms">步进延迟(ms)</label>
|
||||||
<input id="yfb-delay-ms" type="number" min="200" max="10000" step="100" />
|
<input id="yfb-delay-ms" type="number" min="200" max="10000" step="100" />
|
||||||
@ -522,6 +631,16 @@
|
|||||||
<label for="yfb-max-pages">最大扫描页数</label>
|
<label for="yfb-max-pages">最大扫描页数</label>
|
||||||
<input id="yfb-max-pages" type="number" min="1" max="200" />
|
<input id="yfb-max-pages" type="number" min="1" max="200" />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="yfb-field">
|
||||||
|
<label for="yfb-time-range">抓取范围</label>
|
||||||
|
<select id="yfb-time-range">
|
||||||
|
<option value="today">仅看今天</option>
|
||||||
|
<option value="24h">近 24 小时</option>
|
||||||
|
<option value="3d">近 3 天</option>
|
||||||
|
<option value="1w">近 1 周</option>
|
||||||
|
<option value="all" selected>全部(依赖页数)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div class="yfb-field">
|
<div class="yfb-field">
|
||||||
<label for="yfb-delay-ms">步进延迟(ms)</label>
|
<label for="yfb-delay-ms">步进延迟(ms)</label>
|
||||||
<input id="yfb-delay-ms" type="number" min="200" max="10000" step="100" />
|
<input id="yfb-delay-ms" type="number" min="200" max="10000" step="100" />
|
||||||
@ -550,6 +669,7 @@
|
|||||||
ui.pageValue = panel.querySelector("[data-role='page']");
|
ui.pageValue = panel.querySelector("[data-role='page']");
|
||||||
ui.rowValue = panel.querySelector("[data-role='row']");
|
ui.rowValue = panel.querySelector("[data-role='row']");
|
||||||
ui.maxPagesInput = panel.querySelector("#yfb-max-pages");
|
ui.maxPagesInput = panel.querySelector("#yfb-max-pages");
|
||||||
|
ui.timeRangeInput = panel.querySelector("#yfb-time-range");
|
||||||
ui.delayInput = panel.querySelector("#yfb-delay-ms");
|
ui.delayInput = panel.querySelector("#yfb-delay-ms");
|
||||||
ui.startButton = panel.querySelector("[data-role='start']");
|
ui.startButton = panel.querySelector("[data-role='start']");
|
||||||
ui.subscribeAllButton = panel.querySelector("[data-role='subscribe-all']");
|
ui.subscribeAllButton = panel.querySelector("[data-role='subscribe-all']");
|
||||||
@ -559,9 +679,11 @@
|
|||||||
ui.toggleButton = panel.querySelector(".yfb-panel-toggle");
|
ui.toggleButton = panel.querySelector(".yfb-panel-toggle");
|
||||||
|
|
||||||
ui.maxPagesInput.value = String(state.settings.maxPages);
|
ui.maxPagesInput.value = String(state.settings.maxPages);
|
||||||
|
if(ui.timeRangeInput && state.settings.timeRange) { ui.timeRangeInput.value = state.settings.timeRange; }
|
||||||
ui.delayInput.value = String(state.settings.delayMs);
|
ui.delayInput.value = String(state.settings.delayMs);
|
||||||
|
|
||||||
ui.maxPagesInput.addEventListener("change", handleSettingsChange);
|
ui.maxPagesInput.addEventListener("change", handleSettingsChange);
|
||||||
|
if (ui.timeRangeInput) { ui.timeRangeInput.addEventListener("change", handleSettingsChange); }
|
||||||
ui.delayInput.addEventListener("change", handleSettingsChange);
|
ui.delayInput.addEventListener("change", handleSettingsChange);
|
||||||
ui.startButton.addEventListener("click", () => { void startScan(); });
|
ui.startButton.addEventListener("click", () => { void startScan(); });
|
||||||
ui.subscribeAllButton.addEventListener("click", () => { void runSubscriptionOnly(); });
|
ui.subscribeAllButton.addEventListener("click", () => { void runSubscriptionOnly(); });
|
||||||
@ -614,17 +736,63 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function bindRuntimeMessages() {
|
function bindRuntimeMessages() {
|
||||||
chrome.runtime.onMessage.addListener((message) => {
|
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||||
|
void sender;
|
||||||
|
|
||||||
if (!message) {
|
if (!message) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.type === "YFB_TOGGLE_PANEL") {
|
if (message.type === "YFB_TOGGLE_PANEL") {
|
||||||
state.panelHidden = !state.panelHidden;
|
state.panelHidden = !state.panelHidden;
|
||||||
refreshView();
|
refreshView();
|
||||||
void persistState();
|
void persistState();
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (message.type === "YFB_DETAIL_WORKER_PING") {
|
||||||
|
sendResponse({ ok: true, isDetailPage: detectDetailPage() });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.type === "YFB_RUN_DETAIL_EXTRACTION") {
|
||||||
|
void runDetailWorkerExtraction(message.payload)
|
||||||
|
.then((data) => {
|
||||||
|
sendResponse({ ok: true, data });
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
sendResponse({
|
||||||
|
ok: false,
|
||||||
|
error: error instanceof Error ? error.message : "详情提取失败"
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runDetailWorkerExtraction(payload) {
|
||||||
|
const rowMeta = payload?.rowMeta || {};
|
||||||
|
const detailUrl = normalizeUrl(payload?.detailUrl || location.href);
|
||||||
|
|
||||||
|
await waitForDetailPage();
|
||||||
|
await waitForUiSettled(false);
|
||||||
|
dismissKnownDialogs();
|
||||||
|
await sleep(120);
|
||||||
|
|
||||||
|
const detailRecord = extractDetailRecord({
|
||||||
|
...rowMeta,
|
||||||
|
detailUrl
|
||||||
|
});
|
||||||
|
detailRecord.detailUrl = detailUrl || detailRecord.detailUrl;
|
||||||
|
|
||||||
|
const decision = await analyzeRecord(detailRecord);
|
||||||
|
return {
|
||||||
|
detailRecord,
|
||||||
|
decision
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function delay(ms) {
|
async function delay(ms) {
|
||||||
@ -646,6 +814,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
state.isRunning = true;
|
state.isRunning = true;
|
||||||
|
await clearOldHistory();
|
||||||
state.stopRequested = false;
|
state.stopRequested = false;
|
||||||
setStatus("正在处理订阅分组,请稍候...");
|
setStatus("正在处理订阅分组,请稍候...");
|
||||||
log("开始执行订阅分组全选测试。", "info");
|
log("开始执行订阅分组全选测试。", "info");
|
||||||
@ -723,13 +892,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
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();
|
||||||
@ -771,6 +938,7 @@
|
|||||||
await doSubscribeAll();
|
await doSubscribeAll();
|
||||||
|
|
||||||
state.isRunning = true;
|
state.isRunning = true;
|
||||||
|
await clearOldHistory();
|
||||||
state.stopRequested = false;
|
state.stopRequested = false;
|
||||||
setStatus(`准备开始扫描,最多 ${state.settings.maxPages} 页。`);
|
setStatus(`准备开始扫描,最多 ${state.settings.maxPages} 页。`);
|
||||||
log(`开始扫描,最多 ${state.settings.maxPages} 页,步进延迟 ${state.settings.delayMs}ms。`, "info");
|
log(`开始扫描,最多 ${state.settings.maxPages} 页,步进延迟 ${state.settings.delayMs}ms。`, "info");
|
||||||
@ -792,7 +960,24 @@
|
|||||||
|
|
||||||
log(`第 ${state.stats.currentPage} 页识别到 ${rows.length} 条记录。`, "info");
|
log(`第 ${state.stats.currentPage} 页识别到 ${rows.length} 条记录。`, "info");
|
||||||
|
|
||||||
|
let outOfRangeCount = 0;
|
||||||
for (let rowIndex = 0; rowIndex < rows.length; rowIndex += 1) {
|
for (let rowIndex = 0; rowIndex < rows.length; rowIndex += 1) {
|
||||||
|
const rMeta = rows[rowIndex];
|
||||||
|
if (state.settings.timeRange && state.settings.timeRange !== 'all') {
|
||||||
|
const ts = parsePublishTimeToTs(rMeta.publishTime);
|
||||||
|
if (ts > 0 && !isTimeInRange(ts, state.settings.timeRange)) {
|
||||||
|
outOfRangeCount++;
|
||||||
|
if (outOfRangeCount >= 3) {
|
||||||
|
log("发现连续超出时间范围的记录,停止任务。", "warning");
|
||||||
|
state.stopRequested = true;
|
||||||
|
updateRowStatus(rMeta.id, "skip", "超时跳过");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
outOfRangeCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
throwIfStopped();
|
throwIfStopped();
|
||||||
state.stats.currentIndex = rowIndex + 1;
|
state.stats.currentIndex = rowIndex + 1;
|
||||||
refreshView();
|
refreshView();
|
||||||
@ -851,6 +1036,19 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const hist = await getHistoryRecord(rowMeta.url || rowMeta.id);
|
||||||
|
if (hist) {
|
||||||
|
state.stats.scanned += 1;
|
||||||
|
updateRowStatus(rowMeta.id, "skip", "已在历史记录");
|
||||||
|
log(`历史记录跳过:${rowMeta.title}`, "info");
|
||||||
|
state.stats.hits = state.results.length;
|
||||||
|
refreshView();
|
||||||
|
await persistState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch(e) { console.warn(e); }
|
||||||
|
|
||||||
await clickElement(rowMeta.titleEl);
|
await clickElement(rowMeta.titleEl);
|
||||||
await sleep(state.settings.delayMs);
|
await sleep(state.settings.delayMs);
|
||||||
await waitForDetailPage();
|
await waitForDetailPage();
|
||||||
@ -859,6 +1057,7 @@
|
|||||||
const detailRecord = extractDetailRecord(rowMeta);
|
const detailRecord = extractDetailRecord(rowMeta);
|
||||||
const decision = await analyzeRecord(detailRecord);
|
const decision = await analyzeRecord(detailRecord);
|
||||||
state.stats.scanned += 1;
|
state.stats.scanned += 1;
|
||||||
|
try { await saveHistoryRecord(detailRecord); } catch(e) { console.warn(e); }
|
||||||
|
|
||||||
if (decision.isRelevant) {
|
if (decision.isRelevant) {
|
||||||
addResult(detailRecord, decision);
|
addResult(detailRecord, decision);
|
||||||
@ -1081,11 +1280,44 @@
|
|||||||
type,
|
type,
|
||||||
region,
|
region,
|
||||||
publishTime,
|
publishTime,
|
||||||
|
url: deriveDetailUrlFromRow(rowEl, titleEl),
|
||||||
previewText,
|
previewText,
|
||||||
previewKeywordHints: collectKeywordHints([title, type, region, publishTime, previewText].join("\n"))
|
previewKeywordHints: collectKeywordHints([title, type, region, publishTime, previewText].join("\n"))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function deriveDetailUrlFromRow(rowEl, titleEl) {
|
||||||
|
const candidates = [
|
||||||
|
titleEl,
|
||||||
|
titleEl?.closest?.("a[href]"),
|
||||||
|
rowEl?.querySelector?.("a[href]"),
|
||||||
|
rowEl?.querySelector?.("[href]")
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
const href = candidate?.href || candidate?.getAttribute?.("href") || "";
|
||||||
|
const normalized = normalizeUrl(href);
|
||||||
|
if (normalized) {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeUrl(url) {
|
||||||
|
const value = String(url || "").trim();
|
||||||
|
if (!value) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return new URL(value, location.href).href;
|
||||||
|
} catch (error) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function isLikelyNoticeType(text) {
|
function isLikelyNoticeType(text) {
|
||||||
return /(公告|采购|招标|中标|商机|项目)/.test(text) && text.length <= 12;
|
return /(公告|采购|招标|中标|商机|项目)/.test(text) && text.length <= 12;
|
||||||
}
|
}
|
||||||
@ -1192,68 +1424,6 @@
|
|||||||
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);
|
||||||
@ -1555,15 +1725,8 @@
|
|||||||
置信度分数: 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(exportRows);
|
const worksheet = window.XLSX.utils.json_to_sheet(rows);
|
||||||
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, {
|
||||||
@ -1679,6 +1842,16 @@
|
|||||||
<label for="yfb-max-pages">最大页数</label>
|
<label for="yfb-max-pages">最大页数</label>
|
||||||
<input id="yfb-max-pages" type="number" min="1" max="200" />
|
<input id="yfb-max-pages" type="number" min="1" max="200" />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="yfb-field">
|
||||||
|
<label for="yfb-time-range">抓取范围</label>
|
||||||
|
<select id="yfb-time-range">
|
||||||
|
<option value="today">仅看今天</option>
|
||||||
|
<option value="24h">近 24 小时</option>
|
||||||
|
<option value="3d">近 3 天</option>
|
||||||
|
<option value="1w">近 1 周</option>
|
||||||
|
<option value="all" selected>全部(依赖页数)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div class="yfb-field">
|
<div class="yfb-field">
|
||||||
<label for="yfb-delay-ms">步进延迟(ms)</label>
|
<label for="yfb-delay-ms">步进延迟(ms)</label>
|
||||||
<input id="yfb-delay-ms" type="number" min="200" max="10000" step="100" />
|
<input id="yfb-delay-ms" type="number" min="200" max="10000" step="100" />
|
||||||
@ -1717,6 +1890,7 @@
|
|||||||
ui.pageValue = panel.querySelector("[data-role='page']");
|
ui.pageValue = panel.querySelector("[data-role='page']");
|
||||||
ui.rowValue = panel.querySelector("[data-role='row']");
|
ui.rowValue = panel.querySelector("[data-role='row']");
|
||||||
ui.maxPagesInput = panel.querySelector("#yfb-max-pages");
|
ui.maxPagesInput = panel.querySelector("#yfb-max-pages");
|
||||||
|
ui.timeRangeInput = panel.querySelector("#yfb-time-range");
|
||||||
ui.delayInput = panel.querySelector("#yfb-delay-ms");
|
ui.delayInput = panel.querySelector("#yfb-delay-ms");
|
||||||
ui.startButton = panel.querySelector("[data-role='start']");
|
ui.startButton = panel.querySelector("[data-role='start']");
|
||||||
ui.subscribeAllButton = panel.querySelector("[data-role='subscribe-all']");
|
ui.subscribeAllButton = panel.querySelector("[data-role='subscribe-all']");
|
||||||
@ -1726,9 +1900,11 @@
|
|||||||
ui.toggleButton = panel.querySelector(".yfb-panel-toggle");
|
ui.toggleButton = panel.querySelector(".yfb-panel-toggle");
|
||||||
|
|
||||||
ui.maxPagesInput.value = String(state.settings.maxPages);
|
ui.maxPagesInput.value = String(state.settings.maxPages);
|
||||||
|
if(ui.timeRangeInput && state.settings.timeRange) { ui.timeRangeInput.value = state.settings.timeRange; }
|
||||||
ui.delayInput.value = String(state.settings.delayMs);
|
ui.delayInput.value = String(state.settings.delayMs);
|
||||||
|
|
||||||
ui.maxPagesInput.addEventListener("change", handleSettingsChange);
|
ui.maxPagesInput.addEventListener("change", handleSettingsChange);
|
||||||
|
if (ui.timeRangeInput) { ui.timeRangeInput.addEventListener("change", handleSettingsChange); }
|
||||||
ui.delayInput.addEventListener("change", handleSettingsChange);
|
ui.delayInput.addEventListener("change", handleSettingsChange);
|
||||||
ui.startButton.addEventListener("click", () => { void startScan(); });
|
ui.startButton.addEventListener("click", () => { void startScan(); });
|
||||||
|
|
||||||
@ -1759,6 +1935,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
state.isRunning = true;
|
state.isRunning = true;
|
||||||
|
await clearOldHistory();
|
||||||
state.stopRequested = false;
|
state.stopRequested = false;
|
||||||
setStatus(`准备开始扫描,最多 ${state.settings.maxPages} 页。`);
|
setStatus(`准备开始扫描,最多 ${state.settings.maxPages} 页。`);
|
||||||
log(`开始扫描,最多 ${state.settings.maxPages} 页,步进延迟 ${state.settings.delayMs}ms。`, "info");
|
log(`开始扫描,最多 ${state.settings.maxPages} 页,步进延迟 ${state.settings.delayMs}ms。`, "info");
|
||||||
@ -1782,7 +1959,24 @@
|
|||||||
|
|
||||||
log(`第 ${state.stats.currentPage} 页识别到 ${rows.length} 条记录。`, "info");
|
log(`第 ${state.stats.currentPage} 页识别到 ${rows.length} 条记录。`, "info");
|
||||||
|
|
||||||
|
let outOfRangeCount = 0;
|
||||||
for (let rowIndex = 0; rowIndex < rows.length; rowIndex += 1) {
|
for (let rowIndex = 0; rowIndex < rows.length; rowIndex += 1) {
|
||||||
|
const rMeta = rows[rowIndex];
|
||||||
|
if (state.settings.timeRange && state.settings.timeRange !== 'all') {
|
||||||
|
const ts = parsePublishTimeToTs(rMeta.publishTime);
|
||||||
|
if (ts > 0 && !isTimeInRange(ts, state.settings.timeRange)) {
|
||||||
|
outOfRangeCount++;
|
||||||
|
if (outOfRangeCount >= 3) {
|
||||||
|
log("发现连续超出时间范围的记录,停止任务。", "warning");
|
||||||
|
state.stopRequested = true;
|
||||||
|
updateRowStatus(rMeta.id, "skip", "超时跳过");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
outOfRangeCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
throwIfStopped();
|
throwIfStopped();
|
||||||
state.stats.currentIndex = rowIndex + 1;
|
state.stats.currentIndex = rowIndex + 1;
|
||||||
refreshView();
|
refreshView();
|
||||||
@ -2034,17 +2228,55 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await clickElement(rowMeta.titleEl);
|
const detailUrl = await ensureDetailUrlForRow(rowMeta);
|
||||||
await sleep(state.settings.delayMs);
|
if (!detailUrl) {
|
||||||
await waitForDetailPage();
|
state.stats.scanned += 1;
|
||||||
await waitForUiSettled();
|
updateRowStatus(rowMeta.id, "error", "缺少详情地址");
|
||||||
dismissKnownDialogs();
|
log(`处理失败:${rowMeta.title},未找到详情页地址`, "error");
|
||||||
await sleep(120);
|
state.stats.hits = state.results.length;
|
||||||
|
refreshView();
|
||||||
|
await persistState();
|
||||||
|
restoreListHighlights();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const detailRecord = extractDetailRecord(rowMeta);
|
const hist = await getHistoryRecord(detailUrl || rowMeta.id);
|
||||||
const decision = await analyzeRecord(detailRecord);
|
if (hist) {
|
||||||
state.stats.scanned += 1;
|
state.stats.scanned += 1;
|
||||||
|
updateRowStatus(rowMeta.id, "skip", "已在历史记录");
|
||||||
|
log(`历史记录跳过:${rowMeta.title}`, "info");
|
||||||
|
state.stats.hits = state.results.length;
|
||||||
|
refreshView();
|
||||||
|
await persistState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch(e) { console.warn(e); }
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await runtimeSendMessage({
|
||||||
|
type: "YFB_PROCESS_DETAIL_IN_HIDDEN_TAB",
|
||||||
|
payload: {
|
||||||
|
detailUrl,
|
||||||
|
rowMeta: {
|
||||||
|
id: rowMeta.id,
|
||||||
|
title: rowMeta.title,
|
||||||
|
type: rowMeta.type,
|
||||||
|
region: rowMeta.region,
|
||||||
|
publishTime: rowMeta.publishTime,
|
||||||
|
url: detailUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response?.ok || !response.data?.detailRecord || !response.data?.decision) {
|
||||||
|
throw new Error(response?.error || "记录处理失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
const detailRecord = response.data.detailRecord;
|
||||||
|
const decision = response.data.decision;
|
||||||
|
state.stats.scanned += 1;
|
||||||
|
try { await saveHistoryRecord(detailRecord); } catch(e) { console.warn(e); }
|
||||||
|
|
||||||
if (decision.isRelevant) {
|
if (decision.isRelevant) {
|
||||||
addResult(detailRecord, decision);
|
addResult(detailRecord, decision);
|
||||||
@ -2065,18 +2297,52 @@
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : "记录处理失败";
|
const message = error instanceof Error ? error.message : "记录处理失败";
|
||||||
|
state.stats.scanned += 1;
|
||||||
updateRowStatus(rowMeta.id, "error", "异常");
|
updateRowStatus(rowMeta.id, "error", "异常");
|
||||||
log(`处理失败:${rowMeta.title},${message}`, "error");
|
log(`处理失败:${rowMeta.title},${message}`, "error");
|
||||||
} finally {
|
} finally {
|
||||||
state.stats.hits = state.results.length;
|
state.stats.hits = state.results.length;
|
||||||
refreshView();
|
refreshView();
|
||||||
await persistState();
|
await persistState();
|
||||||
await navigateBackToList();
|
|
||||||
await sleep(state.settings.delayMs);
|
|
||||||
restoreListHighlights();
|
restoreListHighlights();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function ensureDetailUrlForRow(rowMeta) {
|
||||||
|
const directUrl = normalizeUrl(rowMeta?.url || "");
|
||||||
|
if (directUrl) {
|
||||||
|
return directUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await captureDetailUrlViaNavigationFallback(rowMeta);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function captureDetailUrlViaNavigationFallback(rowMeta) {
|
||||||
|
if (!rowMeta?.titleEl) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const listUrl = location.href;
|
||||||
|
await clickElement(rowMeta.titleEl);
|
||||||
|
await sleep(state.settings.delayMs);
|
||||||
|
await waitForDetailPage();
|
||||||
|
const detailUrl = normalizeUrl(location.href);
|
||||||
|
await navigateBackToList();
|
||||||
|
await sleep(state.settings.delayMs);
|
||||||
|
|
||||||
|
const restoredRow = findRowById(rowMeta.id);
|
||||||
|
if (restoredRow) {
|
||||||
|
restoredRow.url = detailUrl;
|
||||||
|
rowMeta.url = detailUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!detailUrl || detailUrl === normalizeUrl(listUrl)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return detailUrl;
|
||||||
|
}
|
||||||
|
|
||||||
function extractDetailRecord(rowMeta) {
|
function extractDetailRecord(rowMeta) {
|
||||||
const title = findTitleCandidate(rowMeta.title);
|
const title = findTitleCandidate(rowMeta.title);
|
||||||
const detailMeta = collectDetailMeta();
|
const detailMeta = collectDetailMeta();
|
||||||
@ -2591,17 +2857,9 @@
|
|||||||
置信度分数: item.confidence || 0
|
置信度分数: item.confidence || 0
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const exportHeaders = ["标题", "简述", "AI分类", "置信度"];
|
const worksheet = window.XLSX.utils.json_to_sheet(rows, { header: headers });
|
||||||
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 }
|
||||||
];
|
];
|
||||||
@ -2669,7 +2927,6 @@
|
|||||||
[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,
|
||||||
@ -2780,92 +3037,6 @@
|
|||||||
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, "\\$&");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,8 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "在乙方宝页面内自动翻页抓取、AI筛选、高亮并导出金融机构相关招标信息。",
|
"description": "在乙方宝页面内自动翻页抓取、AI筛选、高亮并导出金融机构相关招标信息。",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"storage"
|
"storage",
|
||||||
|
"tabs"
|
||||||
],
|
],
|
||||||
"host_permissions": [
|
"host_permissions": [
|
||||||
"*://*.yfbzb.com/*",
|
"*://*.yfbzb.com/*",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user