灵猴市集Linghou Market

剑鱼标讯公开搜索

剑鱼标讯jianyu-bid-search

剑鱼标讯公开搜索 用途 在剑鱼标讯相关页面中按关键词提取公开招标、采购、中标、成交等标讯搜索结果。脚本优先读取当前页面 DOM 中已经展示的结果;如果当前页面没有可用结果,会尝试通过公开搜索索引补充剑鱼标讯详情页链接。 执行前提 建议先打开 https://www.jianyu360.cn/、https://www.jianyu360.cn/jylab/s…

搜索招标采购中标
版本1.0.0
扫描passed
更新2026-06-28
超时30000ms

匹配范围

*://*.jianyu360.cn/*

排除范围

未声明

能力声明

dom.read

代码

源码已展开
1return 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})();