/* 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"
|
};
|
}
|