/* Source-controlled visible-page extractor used by exactly one supported * playwright.evaluate call. It never fetches, navigates, or reads storage. */ async function projectInfoCollectVisibleDynamicNodes(options = {}) { const settleMs = Number(options.page_internal_settle_timeout_ms || 15000); if (!Number.isInteger(settleMs) || settleMs !== 15000) throw new Error("E_RUNTIME_CONTRACT"); const started = Date.now(); while ((!document.body || document.readyState === "loading") && Date.now() - started < settleMs) { await new Promise((resolve) => setTimeout(resolve, 100)); } const normalize = (value) => String(value || "").replace(/\s+/g, " ").trim(); const href = (node, selector) => node.querySelector(selector)?.href || null; const idFrom = (url, expression) => { const match = String(url || "").match(expression); return match ? match[1] : null; }; const nodes = Array.from(document.querySelectorAll("[data-dynamic-id], .bili-dyn-list__item")).slice(0, 50); const cards = []; const unparsed_nodes = []; nodes.forEach((node, position) => { const dynamicId = node.getAttribute("data-dynamic-id"); const opusUrl = href(node, 'a[href*="/opus/"]'); const videoUrl = href(node, 'a[href*="/video/BV"]'); const sourceUrl = opusUrl || videoUrl || href(node, "a[href]"); const opusId = idFrom(opusUrl, /\/opus\/([A-Za-z0-9_-]+)/); const bvid = idFrom(videoUrl, /\/video\/(BV[0-9A-Za-z]{10})/i); const publishedAt = node.querySelector("time")?.getAttribute("datetime") || null; const bodyText = normalize(node.querySelector(".bili-rich-text, .dyn-card-opus__desc, .bili-dyn-card-video__desc")?.textContent); const title = normalize(node.querySelector(".bili-dyn-title, .bili-dyn-card-video__title")?.textContent) || bodyText.slice(0, 80); const images = Array.from(node.querySelectorAll('img[src^="https://"], img[src^="http://"]')).slice(0, 20).map((image) => image.currentSrc || image.src); const contentType = bvid ? "video" : images.length ? "image" : bodyText ? "text" : null; if ((!dynamicId && !opusId && !bvid) || !publishedAt || !sourceUrl || !contentType) { const material = JSON.stringify({position, dynamicId, opusId, bvid, publishedAt, sourceUrl, contentType, text: normalize(node.textContent).slice(0, 256)}); unparsed_nodes.push({position, reason_code: !dynamicId && !opusId && !bvid ? "IDENTITY_MISSING" : !publishedAt ? "PUBLISHED_AT_MISSING" : !sourceUrl ? "SOURCE_URL_INVALID" : "CONTENT_TYPE_UNKNOWN", fingerprint_material: material}); return; } cards.push({position, dynamic_id: dynamicId, opus_id: opusId, bvid, published_at: publishedAt, content_type: contentType, source_url: sourceUrl, title: title || "Bilibili dynamic", body_text: bodyText, body_complete: contentType === "text", duration_seconds: bvid ? Number(node.getAttribute("data-duration-seconds") || 0) : null, artifact_urls: images}); }); const bodyText = normalize(document.body?.innerText); const endText = ["没有更多动态了", "已经到底了"].find((text) => bodyText.includes(text)) || null; const profileLink = document.querySelector('a[href^="https://space.bilibili.com/"], a[href^="//space.bilibili.com/"]'); const profileUrl = profileLink ? new URL(profileLink.href, location.href).href.replace(/\/$/, "") : null; return { schema_version: 1, ready_state: document.readyState, visibility_state: document.visibilityState, final_url: String(location.href), page_title: String(document.title || ""), creator: {uid: profileUrl ? idFrom(profileUrl, /space\.bilibili\.com\/(\d+)/) : null, name: normalize(profileLink?.textContent), profile_url: profileUrl}, cards, unparsed_nodes, terminal_marker_text: endText, limit_hit: nodes.length === 50 ? "CARD_PER_OBSERVATION_LIMIT" : "NONE" }; }