匹配范围
*://*.jianyu360.cn/*jianyu-bid-search剑鱼标讯公开搜索 用途 在剑鱼标讯相关页面中按关键词提取公开招标、采购、中标、成交等标讯搜索结果。脚本优先读取当前页面 DOM 中已经展示的结果;如果当前页面没有可用结果,会尝试通过公开搜索索引补充剑鱼标讯详情页链接。 执行前提 建议先打开 https://www.jianyu360.cn/、https://www.jianyu360.cn/jylab/s…
*://*.jianyu360.cn/*dom.read1return await (async () => {2 const input = typeof params === "object" && params ? params : {};3 const cleanText = (value) =>4 typeof value === "string" ? value.replace(/\s+/g, " ").trim() : "";5 const normalizeDate = (value) => {6 const text = cleanText(value);7 const matched = text.match(/(20\d{2})[.\-/年](\d{1,2})[.\-/月](\d{1,2})/);8 if (!matched) return "";9 return `${matched[1]}-${String(matched[2]).padStart(2, "0")}-${String(matched[3]).padStart(2, "0")}`;10 };11 const limit = Math.max(1, Math.min(Number(input.limit) || 20, 50));12 const query = cleanText(input.query || input.keyword || "");13 const sinceDaysValue = Number(input.since_days ?? input.sinceDays);14 const sinceDays = Number.isFinite(sinceDaysValue) && sinceDaysValue > 015 ? Math.max(1, Math.min(sinceDaysValue, 3650))16 : null;17 if (!query) {18 throw new Error("缺少参数 query");19 }20 21 const procurementHints = ["招标", "采购", "公告", "项目", "中标", "成交", "询价", "竞价", "比选", "投标", "tender", "procurement", "bidding", "notice"];22 const resultHints = ["中标", "成交", "结果", "候选人", "中选", "定标", "评标", "award", "winner"];23 const noticeHints = ["招标", "采购", "询价", "比选", "公告", "竞争性", "邀请", "投标", "tender", "procurement", "notice"];24 const newsHints = ["新闻", "资讯", "动态", "政策", "简讯", "news", "article"];25 const navigationHints = ["首页", "官网", "网站地图", "联系我们", "帮助中心", "english", "login", "注册", "导航"];26 const detailUrlHints = ["/detail", "/content", "/jybx/", "/notice", "/article", "/view", "/project", "/bid", "detail=", "id="];27 const listUrlHints = ["/search", "/list", "/index", "/home", "/portal", "/channel", "page="];28 const navigationPathPrefixes = ["/product/", "/front/", "/helpcenter/", "/brand/", "/page_workdesktop/", "/list/", "/list/stype/", "/list/rmxm", "/big/page/", "/jylab/", "/tags/", "/sitemap", "/datasmt/", "/bank/", "/hj/", "/exhibition/", "/swordfish/page_big_pc/search/"];29 const blockedDetailPathPrefixes = ["/nologin/content/", "/article/bdprivate/"];30 const queryTokens = query.split(/\s+/).filter(Boolean).map((item) => item.toLowerCase());31 32 const containsAny = (haystack, needles) => needles.some((needle) => haystack.includes(needle.toLowerCase()));33 const toAbsoluteUrl = (href, base = location.href) => {34 const value = cleanText(href);35 if (!value) return "";36 try {37 return new URL(value, base).toString();38 } catch {39 return "";40 }41 };42 const isJianyuHost = (url) => {43 try {44 return new URL(url).hostname.toLowerCase().endsWith("jianyu360.cn");45 } catch {46 return false;47 }48 };49 const isLikelyNavigationUrl = (rawUrl) => {50 try {51 const parsed = new URL(rawUrl);52 const pathname = cleanText(parsed.pathname).toLowerCase().replace(/\/+$/, "/") || "/";53 return pathname === "/" || navigationPathPrefixes.some((prefix) => pathname.startsWith(prefix));54 } catch {55 return true;56 }57 };58 const classifyDetailStatus = (rawUrl) => {59 const url = cleanText(rawUrl);60 if (!url) return { detail_status: "blocked", detail_reason: "missing_url" };61 try {62 const pathname = cleanText(new URL(url).pathname).toLowerCase().replace(/\/+$/, "/") || "/";63 if (blockedDetailPathPrefixes.some((prefix) => pathname.includes(prefix))) {64 return { detail_status: "blocked", detail_reason: "verification_or_paid_wall" };65 }66 if (isLikelyNavigationUrl(url)) {67 return { detail_status: "entry_only", detail_reason: "navigation_or_profile_entry" };68 }69 return { detail_status: "ok", detail_reason: pathname.includes("/jybx/") ? "jybx_detail" : "detail_candidate" };70 } catch {71 return { detail_status: "blocked", detail_reason: "invalid_url" };72 }73 };74 const extractNoticeId = (rawUrl) => {75 try {76 const pathname = new URL(rawUrl).pathname;77 const matched = pathname.match(/\/jybx\/([^/?#]+)\.html$/i);78 if (matched && matched[1]) return cleanText(matched[1]);79 const tail = cleanText(pathname.split("/").filter(Boolean).pop() || "");80 return cleanText(tail.replace(/\.html?$/i, ""));81 } catch {82 return "";83 }84 };85 const extractDateFromUrl = (rawUrl) => {86 const matched = cleanText(rawUrl).match(/\/(20\d{2})(\d{2})(\d{2})(?:[_/]|$)/);87 return matched ? `${matched[1]}-${matched[2]}-${matched[3]}` : "";88 };89 const classifyContentType = (title, url, contextText) => {90 const haystack = `${title} ${contextText} ${url}`.toLowerCase();91 if (containsAny(haystack, resultHints)) return "result";92 if (containsAny(haystack, noticeHints)) return "notice";93 if (containsAny(haystack, newsHints)) return "news";94 if (containsAny(haystack, navigationHints)) return "navigation";95 return "unknown";96 };97 const isDetailPage = (url) => {98 const lower = cleanText(url).toLowerCase();99 return detailUrlHints.some((hint) => lower.includes(hint)) && !listUrlHints.some((hint) => lower.includes(hint));100 };101 const queryMatched = (text) => {102 const lower = cleanText(text).toLowerCase();103 return queryTokens.length === 0 || queryTokens.some((part) => lower.includes(part));104 };105 const isWithinSinceDays = (dateText) => {106 if (sinceDays == null) return true;107 const normalized = normalizeDate(dateText);108 if (!normalized) return false;109 const timestamp = Date.parse(`${normalized}T00:00:00Z`);110 if (!Number.isFinite(timestamp)) return false;111 const now = new Date();112 const today = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());113 const deltaDays = Math.floor((today - timestamp) / 86400000);114 return deltaDays >= 0 && deltaDays <= sinceDays;115 };116 const buildRecord = (row) => {117 const title = cleanText(row.title);118 const url = cleanText(row.url);119 const contextText = cleanText(row.contextText || title);120 const date = normalizeDate(row.date || contextText) || extractDateFromUrl(url);121 const contentType = classifyContentType(title, url, contextText);122 const detailSignal = classifyDetailStatus(url);123 const snippet = cleanText(contextText).slice(0, 220);124 const qualityFlags = [];125 if (!isDetailPage(url)) qualityFlags.push("list_page_url");126 if (contentType === "navigation") qualityFlags.push("navigation_risk");127 return {128 title,129 url,130 date,131 publish_time: date,132 published_at: date,133 source_site: "剑鱼标讯",134 source_id: "jianyu",135 notice_id: extractNoticeId(url),136 is_detail_page: isDetailPage(url),137 content_type: contentType,138 project_owner: "",139 project_code: "",140 budget_or_limit: "",141 deadline_or_open_time: "",142 snippet,143 summary: snippet,144 quality_flags: qualityFlags,145 detail_status: detailSignal.detail_status,146 detail_reason: detailSignal.detail_reason,147 };148 };149 const keepRow = (row) => {150 if (!row.title || !row.url || !isJianyuHost(row.url)) return false;151 if (!queryMatched(`${row.title} ${row.contextText || ""} ${row.url}`)) return false;152 const text = `${row.title} ${row.contextText || ""}`.toLowerCase();153 if (!containsAny(text, procurementHints) && !normalizeDate(row.date || row.contextText || "")) return false;154 return classifyDetailStatus(row.url).detail_status === "ok";155 };156 const dedupeRows = (rows) => {157 const seen = new Set();158 const output = [];159 for (const row of rows) {160 const key = `${cleanText(row.title)}\t${cleanText(row.url)}`;161 if (!key.trim() || seen.has(key)) continue;162 seen.add(key);163 output.push(row);164 }165 return output;166 };167 168 const collectDomRows = () => {169 const selectors = ["table tbody tr", "table tr", "ul li", "ol li", "article", "section", ".list li", ".notice li", "[class*='list'] li", "[class*='notice'] li", "[class*='item']", "[class*='row']"];170 const nodes = [];171 const seenNodes = new Set();172 for (const selector of selectors) {173 for (const node of Array.from(document.querySelectorAll(selector))) {174 const text = cleanText(node.innerText || node.textContent || "");175 if (!text || text.length < 8 || seenNodes.has(node)) continue;176 const lower = text.toLowerCase();177 const hasHint = containsAny(lower, procurementHints);178 const hasDate = Boolean(normalizeDate(text));179 const hasQuery = queryMatched(text);180 if (!hasHint && !hasDate && !hasQuery) continue;181 seenNodes.add(node);182 nodes.push(node);183 }184 }185 const rows = [];186 for (const node of nodes) {187 const contextText = cleanText(node.innerText || node.textContent || "");188 for (const anchor of Array.from(node.querySelectorAll("a[href]"))) {189 const title = cleanText(anchor.textContent || anchor.getAttribute("title") || "");190 const url = toAbsoluteUrl(anchor.getAttribute("href") || anchor.href || "");191 if (title.length < 4 || !url) continue;192 rows.push({ title, url, date: normalizeDate(contextText), contextText });193 }194 }195 return rows;196 };197 198 const unwrapDuckUrl = (rawUrl) => {199 const value = cleanText(rawUrl);200 if (!value) return "";201 const normalized = value.startsWith("//") ? `https:${value}` : value;202 try {203 const parsed = new URL(normalized);204 if (!parsed.hostname.toLowerCase().endsWith("duckduckgo.com")) return normalized;205 return decodeURIComponent(parsed.searchParams.get("uddg") || normalized);206 } catch {207 return "";208 }209 };210 const parseIndexRows = (markdown) => {211 const rows = [];212 const lines = String(markdown || "").split("\n");213 for (let index = 0; index < lines.length; index += 1) {214 const text = lines[index].trim();215 if (!text.startsWith("## [")) continue;216 const right = text.slice(3);217 const sep = right.lastIndexOf("](");218 if (sep <= 0 || !right.endsWith(")")) continue;219 const title = cleanText(right.slice(1, sep));220 const url = toAbsoluteUrl(unwrapDuckUrl(right.slice(sep + 2, -1)), "https://www.jianyu360.cn/");221 const contextText = cleanText([title, lines[index + 2] || "", lines[index + 4] || ""].join(" "));222 rows.push({ title, url, date: extractDateFromUrl(url) || normalizeDate(contextText), contextText });223 }224 return rows;225 };226 const fetchIndexRows = async () => {227 const variants = Array.from(new Set([query, ...query.split(/\s+/).filter(Boolean)]));228 const rows = [];229 for (const variant of variants) {230 if (rows.length >= limit) break;231 const searchUrl = `https://r.jina.ai/http://duckduckgo.com/html/?q=${encodeURIComponent(`site:jianyu360.cn ${variant}`)}`;232 try {233 const response = await fetch(searchUrl, { headers: { Accept: "text/plain, text/markdown, */*" } });234 if (!response.ok) continue;235 rows.push(...parseIndexRows(await response.text()));236 } catch {237 continue;238 }239 }240 return rows;241 };242 243 const domRows = dedupeRows(collectDomRows()).filter(keepRow);244 const indexRows = domRows.length >= limit ? [] : await fetchIndexRows();245 const records = dedupeRows([...domRows, ...indexRows])246 .filter(keepRow)247 .map(buildRecord)248 .filter((row) => isWithinSinceDays(row.published_at))249 .slice(0, limit)250 .map((row, index) => ({ rank: index + 1, ...row }));251 252 const pageText = cleanText(document.body ? document.body.innerText || "" : "");253 const accessLimited = /(请先登录|未登录|登录后|验证码|人机验证|权限不足|无权限|请完成验证|图形验证码|无用户身份)/.test(pageText);254 return {255 query,256 limit,257 since_days: sinceDays,258 source: domRows.length > 0 ? "current_page_and_public_index" : "public_index",259 access_limited: accessLimited,260 count: records.length,261 records,262 warning: records.length === 0263 ? "未取得公开搜索结果;目标站点可能要求登录、验证码,或公开索引暂无匹配。"264 : "",265 };266})();