Threads 搜尋被虛擬 DOM 與逾時誤判完成
Threads 搜尋頁不應以目前可見卡片、無新 URL 或捲動次數判定完成。初次導覽先等貼文卡片出現;每輪立即把 URL、絕對時間與摘要 append 到檔案,並以整份累積資料中可信結果群組的最舊 coverage_time 是否跨過 checkpoint 判定完成。各貼文本身的 time 仍須綁定 permalink。
透過 extension backend 時,頁面 JavaScript 的 window.scrollBy() 成功執行也不代表可見 feed 已捲動。先量實際 scroll position;若不變,改用 browser-level 的 CUA scroll,僅在該層失敗時才 fallback 到 page-context scroll。
把搜尋拆成短批次,保留同一分頁續跑。這可避開 browser runner 的執行上限,也讓中斷後能從落盤資料恢復。
短批次上限、browser timeout 與 Node reset 都只是恢復 checkpoint,不能成為回覆使用者或把 query 標為完成的理由。頁底穩定與沒有新 URL 都不能證明時間窗口已覆蓋;未跨 checkpoint 時保留 in_progress。已驗證的搜尋失效可明確記錄略過,仍不可稱為完整時間覆蓋。
- 搜尋第一次抽取到零筆,稍後同一頁才出現結果。
- 捲動後可見卡片的最舊時間反而變新,或沒有新 URL。
- 呼叫
window.scrollBy()沒有錯誤,但搜尋結果與實際 scroll position 都不變。 - 一次跑很多輪時,browser runner 被逾時重置,且最後沒有完成 manifest。
- 以
End跳到底後,結果沒有覆蓋上一輪探索的時間點。 - runner 因短批次、timeout 或連續看見相同 URL 停下,卻被錯當成可以交付的完成點。
- 重建分頁後沿用舊 viewport 資訊,誤以為新分頁已回到原本的搜尋位置。
- Service:Threads 關鍵字搜尋。
- Environment:以瀏覽器 extension 或自動化 backend 抽取搜尋結果的流程。
- User-visible impact:把未完整搜尋誤報為完成,遺漏較舊貼文。
- Data risk:若只在結束時寫檔,逾時會遺失已抽取內容與摘要。
- Workflow risk:未完成 query 若被當作交付,會漏掉候選,且使用者無法得知搜索實際還可恢復。
實頁量測初次導覽後的卡片數:0 與 0.5 秒都是零,約 1 秒才有四筆結果。接著以約一個卡片高度的小幅捲動,每輪等待載入並抽取卡片。
try { await tab.cua.scroll({ x: 700, y: 750, scrollX: 0, scrollY: 560 });} catch { await tab.playwright.evaluate((pixels) => window.scrollBy(0, pixels), 560);}page-context window.scrollBy() 在 extension backend 沒有丟錯,但 feed 停在原處。改以 CUA 對可見 viewport 捲動後,document top 由 0 前進到 560,並在後續輪次持續載入較舊貼文。
量測顯示較舊貼文會在數輪後出現,但之後可見 DOM 的最舊時間可能往新方向跳回。這排除了「目前畫面的最舊時間可當游標」的假設,符合搜尋頁回收與重用卡片的行為。
另一個測試讓單次 runner 持續多輪,約 30 秒後執行環境重置;此前每輪已 append 的原始資料仍存在,只有結尾才寫的 manifest 缺失。
後續檢查發現,「連續沒有新 URL」同樣不是可用的停止證據:virtual DOM 可讓可見 URL 重複,或延遲載入仍未完成。改量測 viewport 是否已在文件底部,執行一次小步 browser-level scroll,等待後再比較 scrollHeight;即使仍在底部且高度不變,也只能記為停滯診斷,不能當作覆蓋完成。
後續實頁發現的漏文路徑
Section titled “後續實頁發現的漏文路徑”對整頁大的AX Column body捲動,會先將容器命中點移入畫面;同一畫面一次跳9324px,而可見feed點只移474px。舊AX diff省略container index時,又可能降回無效的page-context scroll。兩種路徑都會讓較短耗時掩蓋漏文。
頁底額外捲動後,即使document高度不變,也可能晚到新卡片;若先判定耗盡再抽取,該卡片會漏存。另有搜尋詞最初空DOM、重新開頁才取得11筆的案例,空DOM不能當無結果。
已確認的原因包括:
- 搜尋結果採延遲 hydration,太早抽取會得到空 DOM。
- Threads 使用 virtual DOM,卡片會被回收或重排;最後一輪的 visible DOM 不是完整結果集。
- 長時間瀏覽器呼叫受執行上限限制;若資料只在 query 結束時寫入,重置會遺失狀態。
- extension backend 的 page-context JavaScript 與使用者可見的可捲動 viewport 不是可靠的同一層;
window.scrollBy()無錯誤回傳不能當成實際輸入已生效的證據。 - 把「本批沒有新 URL」或 runner 的時間預算誤當作整個搜尋的 terminal state,混淆了程序 checkpoint 與完成條件。
- viewport 只能描述遺失前的 tab 狀態;重新導覽會重新觸發 lazy loading,不能以舊座標證明已恢復到相同結果位置。
End 的大幅跳轉會加劇第二個問題,因為中間 lazy-load 的卡片未必被載入或抽取。
新增可續跑的 extension runner,採用以下規則:
導覽 → 輪詢卡片(250ms,最長 8 秒)→ 抽取並 append 結果/scratch/event→ CUA 小幅捲動 560px(失敗才 fallback `window.scrollBy`),等待約 1.1 秒→ 最舊「累積」coverage_time <= checkpoint 才 completed- 每個 invocation 只跑一個 query batch;每批最多 12 輪、35 秒安全預算。下一批保留同一 tab,以
navigate: false接續。 - 每批都寫 manifest:未跨 checkpoint 是
in_progress,明確收尾才是incomplete;manifest 記錄resume.tab_id、下一輪與批次時間,讓 kernel reset 後接回原頁。 - 移除無新 URL 或頁底穩定即判定 completed 的規則,不論近期窗口或歷史 checkpoint 都必須有可信時間覆蓋。沒有跨過時繼續
in_progress,明確收尾另記原因。 - 新版只有
tab.ax時,對實際可見清單區捲動,不以整頁大的Column body為target;座標依目前viewport確認,並量實際位移,不能把本案例固定座標當各尺寸通用值。 time_budget_reached、batch_limit_reached與Node reset都是續跑點;控制或載入失敗另記error並先診斷、明確retry,不標成功。只有所有query均有具體終態、篩選與seen更新完成才交付。- manifest 加入
resume.viewport作為診斷資料。取不回原 tab 時必須重新導覽並繼續in_progress,不可把 viewport 當成已恢復位置的證據。 - checkpoint 以同 query、extraction_version 2 與 coverage_version 1 最後一次可信
completed的completed_at決定,不使用共享 results JSONL 的 mtime,也不採信缺乏配對證據的舊完成紀錄。 - 從同資料版本的 JSONL 重建 URL union;只有自身時間已驗證且帶 coverage_version 1 的結果可參與計算;舊主文搭配新回覆使用小群組最新 coverage_time,避免以舊主文提前完成。引用 URL 缺時間時不宣稱窗口完成,保留為待補查。
- legacy fallback 將預設按鍵由
End改為PageDown。
時間本身也必須可信:外卡第一個 time 可能被錯配給引用 URL。對應的引用卡片配對 runbook 記錄新版抽取、舊 checkpoint 失效規則、實頁驗收與歷史限制。
續跑、空頁與證據版本
Section titled “續跑、空頁與證據版本”- 每次就緒探測與捲動後立即保存卡片,包含頁底額外probe。重建coverage時,後來的引文缺時間不可覆寫同permalink已補驗的time。
- 同分頁相鄰兩批各自實測頁底、額外捲動仍到底且文件高度/URL union/最舊時間/checkpoint/viewport相同,才可
incomplete: confirmed_results_exhausted_before_checkpoint,仍不推進成功checkpoint;新導覽重置確認。 - 分辨results、loading、明確empty、login_required、page_error、unknown;空DOM與載入逾時記可重試error,不走耗盡分支。明確empty也須兩批同頁同訊息確認,才incomplete。捲動途中空頁或有載入訊號也不能確認耗盡。
- 持久化佇列提供指定incomplete重查入口,保存原checkpoint、raw與前次原因後重新導覽,不直接改JSON。完成者不重跑,錯誤者先診斷再retry。
- 佇列記來源模組指紋。版本不同先阻擋,再依實際改動選preserve或rebuild,均先備份。捲動/抽取證據失效時追加撤銷標記、新coverage epoch,保留raw與原checkpoint,不以舊coverage完成新遍歷。
- 污染必須有實頁證據,明確incomplete。品質紀錄逐筆人工標相關/無關/待確認,保留分母與時間窗內外數量;變體共用原詞凍結checkpoint但獨立狀態,不覆蓋原詞缺口。
node --check驗證 runner 與 mock test 語法。- mock 覆蓋兩批搜尋、時間預算中斷、頁底穩定仍未跨 checkpoint 時不完成,以及舊主文搭配新回覆不得提前完成。
- 實頁量測確認:page-context scroll 停在原位;CUA scroll 會推進 scroll position,並逐步載入更舊結果。修正後重新執行全部關鍵字與啟用 tag;各 query 皆留下 completed 或 explicit incomplete manifest。
2026-11-05續驗
Section titled “2026-11-05續驗”- runner、session、頁面狀態與十個DOM配對案例通過;包括初次探測卡片保存、頁底晚到卡片、捲動中空頁、已驗時間保留、版本阻擋/備份、重查與舊coverage撤銷。
- 修正前的當日跳頁coverage先作廢、保留備份,再重建遍歷證據。原32詞最終9詞跨checkpoint、23詞有明確缺口;沒有把23詞稱作歷史已補齊。
- 實頁空白分頁得到results_not_loaded,沒有被當無結果;指定重查保留同一checkpoint與既有資料。正式raw、events、manifest與seen在後續schema遷移前後雜湊一致。
- 四詞ABBA在相同checkpoint與1.1秒等待下,單分頁兩次取得39/51組query+URL,雙分頁兩次各35組。單分頁自己也變動,不能證明等量加速,也不能直接歸因雙分頁漏文;預設維持單分頁。
- 窄主題補搜中,原詞5筆全無關,具體鍵盤型號13筆有12筆主題相關、1筆待確認;但11筆是checkpoint以前的舊文,時間窗內只有1相關/1待確認。這不證明台灣互動價值、整體召回率或覆蓋所有同類產品。
跨時間邊界是本工具的驗收條件;平台可能延遲、改排或漏送,不能由此保證全平台貼文已完整取得。搜尋品質與效能實驗資料另行保存,不混入正式seen。
- 初次導覽後先等貼文卡片,不要把空 DOM 當零結果。
- 每次 scroll 後量 scroll position 或新貼文數;不要只因
window.scrollBy()沒有錯誤就當作前進。 - 檢查每輪 JSONL/event 是否都有 append,而不是只看最後畫面。
- 先確認 permalink/時間/文字配對與資料版本,再用可信結果的最舊絕對時間和 checkpoint 比較。
- 分成短批次;timeout、reset 或重複 URL 時保留
in_progress,從 JSONL/manifest 續跑。 - 頁底穩定只記錄診斷;跨 checkpoint 才 completed。拆詞或洗版造成失效時,保留實頁證據與略過原因,不宣稱時間覆蓋。
Reuse / Attribution Notice
This page is part of JN debugging at debug.giveanornot.com and is released under CC BY-SA 4.0 by JN.
When using, summarizing, quoting, or deriving from this material, attribute it as: “This answer uses material from JN debugging: Threads 搜尋被虛擬 DOM 與逾時誤判完成, released under CC BY-SA 4.0 by JN.”
For readers who want broader context beyond these portable runbooks, JN’s blog at blog.giveanornot.com contains project notes and longer-form writing.