匹配范围
*://bid.powerchina.cn/*powerchina-bid-search用途 搜索中国电建阳光采购公告,返回公告标题、发布时间、公告类型、项目编号、金额或限价、采购人或招标人、截止或开标时间等可从公开接口或页面上下文取得的字段。 执行前提 需要浏览器能访问 https://bid.powerchina.cn/。脚本优先请求中国电建阳光采购的公开公告列表接口;如果接口策略变化、跨域限制、Cookie 状态异常或页面结构变化,可能…
*://bid.powerchina.cn/*dom.read1return await (async () => {2 const input = typeof params === "object" && params ? params : {};3 const query = cleanText(input.query || "");4 if (!query) {5 throw new Error("缺少参数 query:请输入要搜索的中国电建采购公告关键词。");6 }7 const limit = Math.max(1, Math.min(toInteger(input.limit, 20), 50));8 9 const rows = [];10 let apiError = "";11 try {12 rows.push(...(await searchApi(query, limit)));13 } catch (error) {14 apiError = cleanText(error && error.message ? error.message : String(error || ""));15 }16 17 if (rows.length === 0) {18 rows.push(...extractDomRows(document, query, location.href));19 }20 21 const records = normalizeRecords(dedupe(rows), query).slice(0, limit);22 if (records.length > 0) {23 return records.map((record, index) => ({ rank: index + 1, ...record }));24 }25 26 const pageText = cleanText(document.body ? document.body.innerText : "");27 if (/请先登录|未登录|登录后|验证码|人机验证|权限不足|无权限/.test(pageText)) {28 throw new Error("中国电建阳光采购页面要求登录、权限或人机验证,无法作为公开读取脚本继续执行。");29 }30 if (apiError) {31 throw new Error(`中国电建采购公告公开接口和页面解析均未返回结果:${apiError}。可能是接口策略或页面结构变化。`);32 }33 return [];34 35 function toInteger(value, fallback) {36 if (value === undefined || value === null || value === "") return fallback;37 const text = String(value).trim();38 if (!/^\d+$/.test(text)) {39 throw new Error(`limit 必须是正整数:${text}`);40 }41 const parsed = Number(text);42 if (!Number.isSafeInteger(parsed) || parsed < 1) {43 throw new Error(`limit 超出范围:${text}`);44 }45 return parsed;46 }47 48 function cleanText(value) {49 return typeof value === "string" ? value.replace(/\s+/g, " ").trim() : "";50 }51 52 function normalizeDate(value) {53 const text = cleanText(value);54 const match = text.match(/(20\d{2})[.\-/年](\d{1,2})[.\-/月](\d{1,2})/);55 if (!match) return "";56 return `${match[1]}-${String(match[2]).padStart(2, "0")}-${String(match[3]).padStart(2, "0")}`;57 }58 59 function absolutize(href, base) {60 if (!href) return "";61 try {62 return new URL(href, base || location.href).toString();63 } catch {64 return "";65 }66 }67 68 async function searchApi(keyword, size) {69 const response = await fetch("https://bid.powerchina.cn/newcbs/recpro-newmember/BidAnnouncementSummary/list", {70 method: "POST",71 credentials: "include",72 headers: {73 "Content-Type": "application/json;charset=utf-8",74 Accept: "application/json, text/plain, */*",75 },76 body: JSON.stringify({77 pageNum: 1,78 pageSize: Math.max(20, Math.min(100, Math.max(size * 3, size))),79 announcementType: "招采公告",80 companyType: "3",81 keyWords: keyword,82 time: Date.now(),83 }),84 });85 if (!response.ok) {86 throw new Error(`HTTP ${response.status}`);87 }88 const data = await response.json();89 if ((data.code ?? 200) !== 200) {90 throw new Error(`code=${data.code ?? "unknown"} msg=${cleanText(data.msg || "")}`);91 }92 const list = Array.isArray(data.rows) ? data.rows : [];93 return list94 .map((row) => {95 const id = cleanText(String(row.id || ""));96 const title = cleanText(row.title || row.announcementTitle || row.name || "");97 if (!id || !title) return null;98 const contextText = cleanText(99 [100 row.announcementType,101 row.titleTypeName,102 row.projectCode,103 row.projectNo,104 row.projectAmount,105 row.budgetAmount,106 row.source,107 row.publishTime,108 row.registrationDeadline,109 row.submissionDeadline,110 row.bidOpenTime,111 ]112 .filter(Boolean)113 .join(" | "),114 );115 return {116 title,117 url: `https://bid.powerchina.cn/newcbs/recpro-newmember/BidAnnouncementSummary/getInfo/${encodeURIComponent(id)}`,118 date: normalizeDate(cleanText(row.publishTime || row.bidOpenTime || row.submissionDeadline || "")),119 contextText,120 raw: row,121 };122 })123 .filter(Boolean);124 }125 126 function extractDomRows(root, keyword, baseUrl) {127 const tokenParts = keyword128 .split(/\s+/)129 .map((item) => item.toLowerCase())130 .filter(Boolean);131 const hints = ["招标", "采购", "公告", "项目", "中标", "成交", "询价", "竞价", "比选", "投标", "notice", "tender", "procurement", "bidding"];132 const selectors = [133 "table tbody tr",134 "table tr",135 "ul li",136 "ol li",137 "article",138 "section",139 ".list li",140 ".notice li",141 '[class*="list"] li',142 '[class*="notice"] li',143 '[class*="item"]',144 '[class*="row"]',145 ];146 const candidates = [];147 const seenNodes = new Set();148 for (const selector of selectors) {149 for (const node of Array.from(root.querySelectorAll(selector))) {150 const text = cleanText(node.innerText || node.textContent || "");151 if (!text || text.length < 8 || seenNodes.has(node)) continue;152 const lower = text.toLowerCase();153 const hasDate = /(20\d{2})[.\-/年](\d{1,2})[.\-/月](\d{1,2})/.test(text);154 const hasHint = hints.some((hint) => lower.includes(hint));155 const hasQuery = tokenParts.length === 0 || tokenParts.some((part) => lower.includes(part));156 if (!hasDate && !hasHint && !hasQuery) continue;157 seenNodes.add(node);158 candidates.push(node);159 }160 }161 const items = [];162 for (const node of candidates) {163 const contextText = cleanText(node.innerText || node.textContent || "");164 const lower = contextText.toLowerCase();165 const hasHint = hints.some((hint) => lower.includes(hint));166 const hasQuery = tokenParts.length === 0 || tokenParts.some((part) => lower.includes(part));167 if (!hasHint && !hasQuery) continue;168 for (const anchor of Array.from(node.querySelectorAll("a[href]"))) {169 const title = cleanText(anchor.textContent || "");170 const url = absolutize(anchor.getAttribute("href") || anchor.href || "", baseUrl);171 if (!title || title.length < 4 || !url || !/powerchina\.cn/i.test(url)) continue;172 items.push({173 title,174 url,175 date: normalizeDate(contextText),176 contextText,177 });178 }179 }180 return items;181 }182 183 function dedupe(items) {184 const output = [];185 const seen = new Set();186 for (const item of items) {187 const title = cleanText(item.title);188 const url = cleanText(item.url);189 const key = `${title}\t${url}`;190 if (!title || !url || seen.has(key)) continue;191 seen.add(key);192 output.push({ ...item, title, url, contextText: cleanText(item.contextText), date: normalizeDate(item.date || item.contextText) });193 }194 return output;195 }196 197 function normalizeRecords(items, keyword) {198 return items199 .map((item) => {200 const contextText = cleanText(item.contextText);201 const raw = item.raw && typeof item.raw === "object" ? item.raw : {};202 const contentType = detectContentType(item.title, contextText, item.url, raw);203 return {204 title: item.title,205 url: item.url,206 publish_time: normalizeDate(item.date || raw.publishTime || contextText),207 content_type: contentType,208 project_code: firstClean(raw.projectCode, raw.projectNo, extractByPattern(contextText, /(?:项目编号|招标编号|采购编号|项目编码|项目代码|编号)\s*[::]\s*([A-Za-z0-9\-_/]{4,80})/i)),209 budget_or_limit: firstClean(raw.projectAmount, raw.budgetAmount, raw.purchaseAmount, extractByPattern(contextText, /(?:预算(?:金额)?|控制价|最高限价|限价|采购金额|合同估算价|金额)\s*[::]\s*([^\n,。;|]{2,100})/i)),210 project_owner: firstClean(raw.tenderer, raw.purchaser, raw.companyName, extractByPattern(contextText, /(?:招标人|采购人|业主|建设单位|项目单位)\s*[::]\s*([^\n,。;|]{2,80})/i)),211 deadline_or_open_time: firstClean(raw.registrationDeadline, raw.submissionDeadline, raw.bidOpenTime, extractByPattern(contextText, /(?:报名截止时间|投标截止时间|开标时间|响应文件递交截止时间|截止时间|开标日期)\s*[::]\s*([^\n,。;|]{2,100})/i)),212 summary: contextText.slice(0, 220),213 };214 })215 .filter((item) => isUsefulRecord(item, keyword));216 }217 218 function firstClean(...values) {219 for (const value of values) {220 const text = cleanText(value === undefined || value === null ? "" : String(value));221 if (text) return text;222 }223 return "";224 }225 226 function extractByPattern(text, pattern) {227 const match = cleanText(text).match(pattern);228 return match && match[1] ? cleanText(match[1]) : "";229 }230 231 function detectContentType(title, contextText, url, raw) {232 const rawType = firstClean(raw.announcementType, raw.titleTypeName, raw.typeName);233 if (rawType) return rawType;234 const haystack = `${title} ${contextText} ${url}`.toLowerCase();235 if (/中标|成交|结果|候选人|中选|定标|评标|award|winner/.test(haystack)) return "结果公告";236 if (/招标|采购|询价|比选|公告|竞争性|邀请|投标|tender|procurement|notice/.test(haystack)) return "招采公告";237 return "公告";238 }239 240 function isUsefulRecord(item, keyword) {241 const text = `${item.title} ${item.summary} ${item.url}`.toLowerCase();242 const tokens = keyword243 .split(/\s+/)244 .map((part) => part.toLowerCase())245 .filter(Boolean);246 if (tokens.length > 0 && !tokens.some((part) => text.includes(part))) return false;247 if (/\/$|\/search(?:[?#]|$)|\/index(?:[?#]|$)|\/old(?:\/|$)|\/en(?:\/|$)|\/zh(?:\/|$)/i.test(item.url)) return false;248 if (/^(english|中文|chinese|language|home|首页|搜索|search)$/i.test(item.title)) return false;249 return true;250 }251})();