自動擷取、清理、管理與匯出 Threads 貼文的內嵌碼與中繼資料
舊名「Threads 程式碼儲存器」(Threads Code Saver),2.7.0 起改名為 Threadkeep;repo 也從 threads-embedded-code 改名為 Threadkeep
前往介紹頁:功能導覽、互動清洗示範與安裝教學
基於 Google Chrome Manifest V3 標準設計的輕量級瀏覽器擴充功能。
純前端架構、零外部依賴。所有資料均安全儲存於瀏覽器本機的 chrome.storage.local,
無需任何後端伺服器、外部資料庫或第三方 API 金鑰,完整保障您的隱私與資料安全。
- 專案簡介與核心價值
- 生態系與關聯專案
- 快速開始
- 核心功能特性
- 技術規格與技術棧
- 專案目錄與模組架構
- 系統架構與流程圖
- 核心演算法與技術實作細節
- 資料模型與儲存 Schema
- 配置與權限宣告
- 備份、匯出與匯入規範
- 操作指南與快捷鍵速查
- 常見問題與疑難排解 (FAQ)
- 開發與貢獻指南
- 版本更新紀錄 (Changelog)
- AI 友善文件說明 (llms.txt)
- 授權條款與免責聲明
Threads 內建的「儲存」只能在 Threads 裡回頭翻看。想把喜歡的貼文嵌進自己的網站、整理成清單或留一份備份,就得一篇篇打開「取得內嵌程式碼」複製,再自己記下內文、作者與發文時間。
「Threadkeep」為解決此痛點而生,具備以下核心價值:
| 核心價值 | 說明 |
|---|---|
| 無感自動擷取 | 在貼文選單點「取得內嵌程式碼」,內嵌碼、內文、作者、發文時間與標籤就自動存下,不必另外複製。 |
| 原始排版保真 | 採用頂層文字容器分析演算法,完整保留段落與換行,不會把多行內文壓成一行。 |
| 多層雜訊過濾 | 內建 44 條過濾正規表達式,自動剝離作者簡介、相對發文時間、輪播計數及平台導覽文字。 |
| 本機隱私安全 | 資料 100% 留存於瀏覽器本機儲存區,不會上傳到任何伺服器;對外連線僅限 Threads 官方網域(即時預覽與背景更新)及介面字型所用的 Google Fonts,音效與背景音樂隨擴充功能打包,無追蹤、無遙測。 |
| 靈活跨端複用 | 提供三種匯出,都是內容為 const posts = [...] 的 .js 檔:「僅內嵌碼」只含內嵌碼(threads-embed-codes-*.js),可直接貼進網頁;「精選格式」附上連結、作者、內文與標籤(threads-featured-data-*.js),給 Threadfolio 當資料來源;「完整備份」保留發文時間、失效狀態等全部欄位(threads-full-data-*.js),用於備份與還原。 |
本專案作為「資料擷取與本機管理中樞」,與前端展示網站 Threadfolio 構成完整的 Threads 內容收藏與公開展示生態系:
| 專案名稱 | 專案定位與職責 | 專案連結 |
|---|---|---|
| Threadkeep (本專案) | 瀏覽器擴充功能:負責 Threads 貼文智慧擷取、DOM 雜訊清洗、本機儲存、即時預覽與多格式匯出 | GitHub 專案庫 |
| Threadfolio (關聯專案) | 前端展示網站:接收本擴充功能匯出的「精選貼文資料 (threads-featured-data-*.js)」,提供響應式卡片流與展示介面 | Threadfolio 網站 |
flowchart LR
A["在 Threads 瀏覽貼文"] -->|"取得內嵌程式碼"| B["Threadkeep 擴充功能"]
B -->|"匯出「精選格式」"| C["threads-featured-data-*.js"]
C -->|"作為資料來源"| D["Threadfolio 精選貼文網站"]
- 在 Threads 看到想留下的貼文時,從貼文選單點「取得內嵌程式碼」,Threadkeep 就會把它存進瀏覽器。
- 在控制面板側欄「資料」的「匯出」選單選「精選格式」,擴充功能會自動去除作者帳號
@前綴並格式化為前端配置物件。 - 將匯出的檔案直接提供給 Threadfolio 作為資料來源,即刻完成個人精選貼文網站的更新與展示。
- 支援 Manifest V3 的 Chromium 核心瀏覽器:
- Google Chrome (版本 123 以上,
manifest.json的minimum_chrome_version;介面配色使用 CSSlight-dark()) - Microsoft Edge (版本 123 以上)
- Brave Browser
- Opera / Opera GX
- Arc Browser
- Google Chrome (版本 123 以上,
- 純前端原生專案,不需安裝任何額外編譯環境、執行環境或打包工具。
- 取得專案原始碼:
git clone https://github.com/Scorpio-meow/Threadkeep.git cd Threadkeep - 開啟瀏覽器並進入擴充功能管理頁面:
- Google Chrome 請於網址列輸入:
chrome://extensions/ - Microsoft Edge 請於網址列輸入:
edge://extensions/
- Google Chrome 請於網址列輸入:
- 開啟頁面右上角的「開發人員模式 (Developer mode)」。
- 點擊左上角的「載入未封裝項目 (Load unpacked)」。
- 選取本專案的根目錄(即包含 manifest.json 的資料夾)。
- 在瀏覽器工具列的擴充功能圖示清單中,將「Threadkeep」釘選至工具列。
- 在 Threads 貼文右上角點「⋯」,選「取得內嵌程式碼」。
- 擴充功能攔截內嵌對話框,擷取內嵌碼、內文、作者、發文時間與標籤並存進瀏覽器,頁面右上角出現「內嵌程式碼已儲存」通知。
- 點瀏覽器工具列的 Threadkeep 圖示開啟 Popup,可以搜尋、篩選、排序、複製或刪除貼文,並從「更多」選單匯出或匯入。
- 需要完整管理時,在 Popup 點「開啟控制面板」:以瀑布流卡片瀏覽、用側欄分類與標籤雲篩選、在預覽抽屜檢視官方內嵌卡片,或勾選多篇做批次處理。
- 點擊委派與對話框偵測:在
document以捕獲階段掛一個點擊監聽,只有點到「取得內嵌程式碼」時才擷取貼文內容;MutationObserver只負責在 DOM 變動後(每個畫面影格最多一次)檢查是否出現內嵌對話框,不掃描整頁按鈕。只有在點擊之後,才會每 250ms(EMBED_DIALOG_POLL_MS)檢查一次內嵌碼是否出現,最多等 8 秒(EMBED_DIALOG_TIMEOUT_MS)。 - 主動式偵測機制:透過
processOpenEmbedDialogs函式,即使對話框由非擴充功能按鈕開啟,也能自動解析對話框中的唯讀輸入框並儲存。 - 上下文保護封裝:內建
safeStorageGet與safeStorageSet,自動檢查擴充功能執行環境生命週期 (isExtensionAlive),避免擴充功能更新或重載時拋出未捕獲的Extension context invalidated例外。
- 原生排版與換行保留:使用頂層文字容器
innerText擷取策略,完整保留貼文的自然段落換行(\n)與<br>標籤,解決傳統遍歷子節點時將多行內文壓縮為單行空格的問題,並確保同行的@提及與#標籤保持排版連貫。 - UI 與時間雜訊深度過濾:自動識別並清除作者簡介、追蹤者人數、串文數量、相對時間(如「2天」、「1小時」、「剛剛」)、貼文標頭的「已釘選」「作者」標籤以及各類平台引導文字。目前共 44 條規則,涵蓋繁體中文與英文介面。
- 嚴格排除標頭連結:精確過濾
time標籤、a[href*="/post/"]、a[href*="/t/"]貼文永久連結、a[href*="/@"]作者主頁連結與a[href*="/search"]主題/熱門話題連結(如「超夯:…」),防止中繼標籤誤混入文章主體。 - 行內貼圖不影響內文:內文中夾帶的 GIF 貼圖或表情圖片(
aria-hidden="true"的img)不會被視為媒體,整段文字照常擷取;只有含影片、picture、canvas或一般圖片的容器才會排除。 - 回覆邊界隔離:在動態牆或個人首頁擷取時,偵測到「回覆...」邊界元素時自動切斷,確保僅擷取發文者所發布的主內容。
- 純圖片說明過濾:透過
isLikelyImageOnlyDescription智慧判別僅含圖片說明的貼文(如Photo by ... on ...),避免無效擷取。
系統會將每篇貼文完整解析為標準化的結構化資料物件:
| 欄位名稱 | 類型 | 說明 |
|---|---|---|
| 貼文內文 | string |
經過去除 UI 雜訊與中繼標籤後的純文字(保留完整段落換行) |
| 發文作者 | string |
發文者帳號,取自貼文標頭的作者連結文字(通常不含 @);取不到時改用網址中的帳號,仍取不到則為「未知作者」 |
| 發文時間 | string |
包含標準 ISO 8601 時間字串與格式化標題文字 |
| 標籤清單 | string[] |
結合官方 Hashtag 連結與內文 #hashtag 的標籤陣列 |
| 內嵌代碼 | string |
Threads 官方原生的標準 <blockquote> 嵌入代碼 |
| 貼文狀態 | 'active' | 'expired' |
標記貼文為正常存取中或已失效 |
- 點了才滑出的預覽抽屜:平常不佔版面;點卡片上的作者名稱或卡片空白處(拖曳選取文字時不觸發),抽屜從右側滑出、背景變暗。抽屜是對話框(
role="dialog"、aria-modal="true"),開啟時焦點移到關閉鈕並鎖在抽屜內;標題列顯示作者與「第 N / M 篇」。 - 雙分頁即時渲染:
- 內嵌預覽 (Official Embed):遵循 Chrome Manifest V3 與 Threads 官方嵌入規範,透過安全 Frame 直接載入官方即時卡片,呈現完整的互動按鈕與動態樣式。
- 原始碼與中繼資料 (Embed Code & Metadata):檢視完整內文、乾淨的
<blockquote>代碼與貼文中繼屬性對照表格。
- 寬度與高度自適應:抽屜最寬 560px(窄螢幕時等於視窗寬),內嵌卡片隨抽屜寬度縮放;高度由注入至 iframe 的 Content Script 量測
.Embed容器後回報,長短貼文皆精準貼合,不留多餘空白或捲軸。 - 實際載入狀態回饋:區分「載入中」、「載入逾時(10 秒)」與「載入失敗」三種狀態,逾時或失敗時顯示錯誤說明與「重新載入」按鈕;若該貼文沒有可用連結,則直接提示改看「原始碼與中繼資料」分頁。Threads 不提供內嵌的貼文(
/embed回 HTTP 500)會顯示中文說明,不會出現 Meta 的英文錯誤頁。 - 帶著登入狀態預覽:預覽從
www.threads.com載入,會沿用瀏覽器的 Threads 登入狀態;只開放登入者觀看的貼文(例如被列為敏感內容),登入 Threads 後也能預覽。這類貼文的內嵌碼放到網站上時,只有已登入 Threads、且瀏覽器允許第三方 Cookie 的訪客看得到。 - 全方位快捷鍵與導覽:支援鍵盤
ESC、點背景或關閉鈕關閉,左/右方向鍵切換上一篇/下一篇貼文(背後對應的卡片會捲到可見範圍並加上外框),一鍵複製內嵌碼及直接開啟原文;關閉後焦點回到正在預覽的那張卡片。載入中顯示吉祥物動畫。
- 循序非同步更新:點擊「更新貼文資料」時,系統會依照當前篩選與排序後的結果建立佇列(若已勾選貼文,則僅更新目前篩選結果中被勾選的項目;勾選後又被篩掉的貼文不會更新),並重複使用同一個背景工作分頁循序載入,有效避免多開分頁導致的系統卡頓或平台流量限制。
- 寬鬆的安全上限:每篇的分頁載入與頁面解析合計最多等 60 秒,超過只記為「失敗」、不標記失效,網路較慢的正常貼文不會被誤判;注入腳本以
MutationObserver在頁面渲染時立即檢查(兩次檢查至少間隔 80ms),不受背景分頁的計時器節流影響。 - 分批寫回:每處理 20 篇、按下暫停或佇列結束時才寫回一次,而且只寫這幾篇各自的紀錄:先讀取它們目前的內容,只覆蓋發文時間、內文、標籤與失效狀態等欄位,期間被刪除的貼文不會被寫回來,在 Threads 新存的貼文也不受影響。進行中只重繪該篇卡片與分類篇數,佇列結束後才更新清單篩選與標籤雲、作者雲。
- 進度與中斷控制:側欄「資料」下方即時顯示「更新中 (X/Y)」與進度條,右上角頂欄也會顯示「更新中 X/Y」;提供「暫停 / 繼續」與「停止」按鈕,暫停時進度條轉為琥珀色。
- 智慧失效標記與自動恢復:
- 若貼文被刪除、轉為私密或發生轉導(
redirected),系統將自動標記為expired並記錄原因。 - 若頁面文字或標題出現不存在或已移除的訊息(例如「此頁面無法使用」「此串文已被移除」「Page Not Found」),將記錄為
post-not-found;只是找不到貼文元素時會等到 60 秒上限後記為失敗,不標記失效。 - 若下次更新時貼文恢復可存取狀態,系統會自動清除失效標籤並恢復為
active。
- 若貼文被刪除、轉為私密或發生轉導(
- 內嵌可用性檢查:每篇在背景分頁裡以同源、不帶 Cookie 的請求打一次
/embed,結果寫入embeddable與embeddableCheckedAt。回 HTTP 500(例如只開放登入者觀看的貼文)記為false,回 200 且含.Embed記為true;逾時、限流或其他回應不寫入,保留上次的結論。必須在 threads.com 的分頁裡檢查,因為擴充功能頁面跨站請求/embed只會拿到 Threads 的一般頁面殼。在 Threads 儲存新貼文時,content.js也會在背景檢查一次。
- Popup 快速面板(480×600px):頁首右側是「開啟控制面板」與「更多」,「更多」選單收納三種匯出(各附一行說明)、匯入與清除全部;下方是搜尋、篩選與排序,以及單欄貼文卡片(內文最多 3 行,按鈕依序為在 Threads 查看原文、複製內嵌碼、刪除)。
- 控制面板:全螢幕桌面工作區。左側欄由上而下是歡迎卡片、2×2 分類格、標籤雲、作者雲與資料按鈕格;中間是瀑布流卡片,依視窗寬度自動分欄(每欄至少 280px,窄螢幕一欄),卡片高度隨內文長短排列、不留空隙;預覽是點了才出現的右側抽屜。
- 瀑布流排版:瀏覽器還沒有原生的 CSS 瀑布流,所以清單是 4px 列高的 CSS Grid,由
ResizeObserver依每張卡片的實際高度設定grid-row-end: span N;展開全文或視窗變寬時卡片會自動重排。DOM 與鍵盤順序仍照排序由左到右,不會像columns那樣先跑完一欄。 - 分批渲染長清單:清單一次只建立 60 張卡片(Popup 為 30 張),捲到接近底部或按「顯示更多」才載入下一批;卡片上的按鈕事件統一由清單容器委派。
- 跨介面同步:Popup 與控制面板監聽
chrome.storage.onChanged,在 Threads 新存或在另一個介面刪除的貼文會即時反映到清單。事件只帶來有變動的那幾篇;內容和畫面上相同的變動(例如自己寫入引起的事件)會略過,不重複重繪。 - 搜尋範圍:比對內文、作者、標籤與貼文連結的路徑(
postLink去掉網域後的/@作者/post/貼文代碼),可用帳號或貼文代碼找貼文。每篇的搜尋字串在第一次搜尋時建立並快取,排序也先算好每篇的比較值再排序。 - 批次勾選管理:Popup 與控制面板的「全選」都會勾選目前篩選結果的全部貼文,包含尚未載入的卡片,並支援半選(Indeterminate)狀態顯示;沒有結果時全選停用。控制面板勾選任何貼文後,列表頭會換成批次列:「更新資料」「複製內嵌碼」「刪除」與「取消選取」,沒有勾選時批次列不顯示;Popup 則顯示「已選取 N 篇」與「取消選取」。
- 頁內確認 Modal:破壞性操作(清除全部、覆寫匯入,以及控制面板的單篇與批次刪除)全面改用頁內 Modal 二次確認;Popup 沒有批次刪除,單篇刪除不另外確認,改以「復原」提示補救。兩個介面都完全不使用原生
confirm()、alert()與prompt();這對 Popup 尤其關鍵——原生對話框會在 Popup 失焦時連同 Popup 一併關閉並回傳預設值。
系統在 Popup 與控制面板均提供 6 種篩選維度與 6 種排序規則:
- 6 種分類篩選模式:
全部貼文 (all): 顯示資料庫中所有貼文。依作者 (author): 控制面板在側欄作者雲點選;Popup 以二級下拉選單列出所有作者。依標籤 (tag): 控制面板在側欄標籤雲或卡片上的標籤點選;Popup 以二級下拉選單列出所有標籤。無內文 (noContent): 快速找出content為空的貼文(例如純圖片或未成功擷取內文者),便於集中維護。無發文時間 (noTimestamp): 篩選出未成功擷取官方發布時間或時間戳記與儲存時間異常之項目。失效貼文 (expired): 篩選出被原作者刪除、私密化或轉導的失效貼文。
- 控制面板的分類格:全部、失效、無內文、無發文時間做成側欄頂端的 2×2 分類格,直接顯示各自的篇數,有失效貼文時篇數變紅;選中的分類會成為列表標題(例如「失效貼文」「#台南」),旁邊顯示篇數。再點一次已選的標籤或作者即回到全部貼文。
- 6 種排序規則(控制面板/Popup 的顯示名稱):
savedAt-desc「儲存時間(新到舊)」/「最新儲存」、savedAt-asc「儲存時間(舊到新)」/「最早儲存」timestamp-desc「發文時間(新到舊)」/「最新發文」、timestamp-asc「發文時間(舊到新)」/「最早發文」author-asc「作者(A-Z)」/「作者 A–Z」、author-desc「作者(Z-A)」/「作者 Z–A」
- 卡片時間跟著排序:控制面板卡片右上角依目前排序顯示「儲存」或「發文」時間(依作者排序時顯示發文時間),滑鼠停留可看完整日期時間。
- 即時頻次計算:側欄的標籤雲與作者雲依篇數排序,預設顯示前 15 個,超過時最後有「全部 N 個標籤/位作者」可展開、「收合」收回;目前選中的項目即使不在前 15 名也會顯示。篇數越多字越粗(相對最多的一項達 60% 以上為粗體、30% 以上為中粗)。標籤不分大小寫合併計數(
React、react算同一個),以最常見的寫法顯示,Popup 的篩選選單與側欄的標籤總數也依同樣規則合併。 - 點擊即時篩選:點擊標籤雲或作者雲中的任一項,即可切換控制面板清單的篩選條件,選中的項目以墨黑底標示;再次點擊即可取消篩選。卡片上的標籤是香檳色小膠囊,點擊效果相同。
- 三種專業匯出格式:僅內嵌碼(貼進網頁用的
.js檔)、精選格式(Threadfolio 精選貼文用)、完整備份(含時間、作者與內文)。三者皆會在有勾選項目時僅匯出勾選的貼文,未勾選時則匯出目前的篩選結果。控制面板在側欄「資料」的「匯出」選單,選單標題會寫出這次的範圍(「匯出已選取的 3 篇」或「匯出目前列表的 11 篇」);Popup 在頁首「更多」選單。 - 雙來源匯入:Popup 在「更多」選單點「匯入…」後可選擇「選擇檔案」讀取
.js/.json備份,或以「貼上內容」直接貼上匯出檔文字;控制面板則在側欄「資料」點「匯入」以檔案選擇器匯入。 - 雙模式智慧匯入:
- 合併資料 (Merge):以貼文代碼(儲存 key)比對,略過已存在的貼文,匯入檔內的重複項目也只保留一筆;新貼文依儲存時間(
savedAt)排入清單。 - 完全覆寫 (Overwrite):清空現有資料庫,以匯入檔案內容完全取代;此模式必須再通過一次「確認完全覆寫」對話框才會執行。
- 合併資料 (Merge):以貼文代碼(儲存 key)比對,略過已存在的貼文,匯入檔內的重複項目也只保留一筆;新貼文依儲存時間(
- 兩階段解析:先以
JSON.parse解析const posts = [...],失敗時改用正規表達式抽出每段內嵌碼,相容三種匯出格式與標準 JSON 備份。
- 完整 Modal 無障礙語意:所有對話框均具備
role="dialog"、aria-modal="true"與標題關聯,確認與匯入方式對話框另有描述關聯;開啟時自動聚焦、Tab焦點鎖定於對話框內、ESC可關閉,關閉後焦點自動回到觸發按鈕。 - 可復原的刪除與清除:單篇刪除、批次刪除與清除全部資料後,會顯示帶有「復原」按鈕的提示(預設 8 秒);滑鼠移到提示上或鍵盤焦點停在「復原」按鈕時暫停倒數。同一時間僅保留一則復原提示,避免舊快照回捲較新的變更。
- 即時語音回饋:Popup、控制面板的 Toast 容器與注入 Threads 頁面的通知皆為 live region。Popup 與控制面板的 Toast 一律為
role="status"(錯誤只改變外觀),Threads 頁面上的錯誤通知才改用role="alert"。一般訊息至少顯示 4 秒、錯誤至少 6 秒,較長的訊息依字數延長(每字約 0.12 秒),滑鼠移到提示上時暫停倒數。 - 清楚的焦點外框:按鈕、連結與輸入框的鍵盤焦點一律以 2px 實線外框標示,顏色是香檳金(淺色
#83632a、深色#d9bc7a),與底色的對比至少 5:1;高對比模式下改回系統顏色外框。 - 深色與高對比模式:控制面板右上角「設定」選單的「外觀」可選跟隨系統、淺色或深色,設定存在
chrome.storage.local,Popup 與 Threads 頁面上的通知一併套用;顏色集中定義在ui.css的:root,以light-dark()同時給出淺色與深色值,淺色與深色的文字對比都至少 4.5:1;Windows 高對比模式(forced-colors)下,焦點外框、勾選框、選取狀態與進度條改用系統顏色標示。 - 可操作的分類、標籤與作者:側欄分類格、標籤雲與作者雲的每一項都是
button並標註aria-pressed,可用鍵盤聚焦並切換篩選狀態;卡片上的標籤也是可切換的按鈕。 - 螢幕閱讀器結構:貼文卡片是清單項目、作者名稱是標題,標題裡的按鈕會開啟預覽;卡片的勾選框與操作按鈕名稱帶上作者與發文時間(例如「刪除:user174 於 18 小時前發布的貼文」),相對時間另附完整日期時間。搜尋、篩選與勾選後會朗讀結果數量(「找到 5 篇貼文」「已選取 3 篇」)。
- 焦點不遺失:點分類或標籤篩選、刪除、復原或背景更新重畫卡片後,焦點留在原控制項,或移到同一位置的下一篇、還原的那篇,不會跳回頁首;關閉預覽抽屜後焦點回到該卡片的作者名稱。
- 視覺與動態友善:字級只用 12、14、16、20px,下限 12px;偏小的按鈕點擊目標放大至 24×24px 以上(勾選框例外,見 ACCESSIBILITY.md 的已知限制)。所有樣式表都支援
prefers-reduced-motion: reduce:系統要求減少動態時停用轉場、卡片淡入、背景流動與游標光斑。 - 聲音只是輔助:每個音效對應的結果都有畫面上的提示或狀態變化,也會由 live region 朗讀,關掉音效不會漏掉資訊;音效與背景音樂都能在「設定」關閉,背景音樂預設關閉、不會自動響起。
- 輸入防抖:兩個介面的搜尋框皆加入 180ms debounce,資料量大時輸入不再卡頓。
Note
無障礙目標、已知限制(例如即時預覽的卡片由 Meta 決定)與障礙回報方式,請見 ACCESSIBILITY.md。
-
六種短音效:豎琴與鋼琴,全部是 D 大調,連續操作時彼此和諧。
時機 檔案 聲音 長度 在 Threads 儲存或更新貼文 sounds/save.ogg豎琴 D–F♯–A–D 上行琶音 0.74 秒 複製內嵌碼(單篇、批次、預覽抽屜) sounds/copy.ogg鋼琴 A 八度輕點 0.32 秒 刪除、批次刪除、清除全部(確認後) sounds/delete.ogg豎琴 A→D 下行 0.42 秒 按「復原」還原成功 sounds/undo.ogg豎琴 D→A 上行 0.36 秒 匯入、匯出成功,批次更新貼文資料完成 sounds/complete.ogg豎琴 Dadd9 琶音接鋼琴和弦,尾端極輕的指鈸 1.12 秒 顯示錯誤提示、Threads 頁面儲存失敗 sounds/error.ogg鋼琴低音 E→D♯ 小二度 0.42 秒 使用者取消操作、批次更新被停止或寫回失敗時不播完成音效。音效預設開啟,可在控制面板右上角「設定」關閉,Popup 與 Threads 頁面共用同一個設定(
soundEffects);開啟時會先播一次儲存音效試聽。 -
背景音樂:
sounds/background-music.ogg,2 分鐘可無縫循環的新古典鋼琴與弦樂(D 大調、64 BPM、32 小節:鋼琴主題、弦樂加入、主題高八度變奏加豎琴、收束後接回開頭)。只在控制面板播放,預設關閉(backgroundMusic);開啟時 1.8 秒淡入,切到別的分頁會停止、回來再從頭淡入;瀏覽器尚未允許自動播放時,會在第一次點擊或按鍵後開始。 -
播放方式:
sound.js以 Web Audio API 解碼並播放,每個檔案只下載與解碼一次;循環用AudioBufferSourceNode.loop,接縫無間隙。Threads 頁面的 content script 只會取用save.ogg與error.ogg,manifest.json只對 Threads 網域開放這兩個檔案(web_accessible_resources)。 -
音源來源與授權:由小寶樂隊(XSXB-Band)本機編曲工作室以 CC0 授權的實錄採樣(立式鋼琴、豎琴、小提琴組、大提琴組、指鈸)編曲並渲染,再轉成 48kHz Ogg Opus(音效單聲道約 56 kbps、配樂立體聲約 96 kbps);沒有使用音樂生成模型。
-
視覺系統:象牙白、墨黑、香檳金。主要按鈕、勾選框、開關與選中的分類用墨黑,選中的卡片、焦點外框、分頁底線、進度條與重點數字用香檳金,紅色只留給刪除、清除全部與失效標記;線條是 0.5px 髮絲線,圓角只用在按鈕、欄位(6px)與卡片、浮層(10px)。內文字型是 Geist 與 Noto Sans TC,程式碼是 Geist Mono,品牌名、標題與分類數字是 Playfair Display 與 Noto Serif TC(數字設為等高)。
-
動態質感:背景有緩慢流動的香檳金柔光(約 28 秒一個來回);游標移動時有一圈柔光跟著走,滑過貼文卡片時卡片內也會亮起局部高光,卡片微微浮起;切換分類或搜尋時卡片依序淡入。光層在文字與卡片底下,不影響文字對比也不擋點擊,觸控操作不會觸發;系統要求減少動態時全部關閉。
-
歡迎卡片:控制面板側欄頂端依時段顯示「早安、午安、晚安、夜深了」,以及收藏總數與最近 7 天新增的篇數。
| 項目 | 內容 |
|---|---|
| 規範標準 | Chrome Extensions Manifest V3 |
| 核心語言 | 原生 JavaScript (ES6+)、HTML5、CSS 自訂屬性 |
| 依賴套件 | 無 npm 套件;介面字型由 Google Fonts 載入 |
| 介面樣式 | CSS light-dark() 色彩變數、Popover API、以 CSS Grid 實作的瀑布流 |
| 音訊播放 | Web Audio API(AudioContext),音檔為 Ogg Opus |
| 資料儲存 | 瀏覽器本機的 chrome.storage.local,以 unlimitedStorage 解除 10MB 配額 |
| 安全規範 | 嚴格 CSP(Content Security Policy),沒有行內事件處理器 |
| 相容平台 | Chrome、Edge 123 以上,以及 Brave、Opera、Arc 等 Chromium 瀏覽器 |
| 建置流程 | 零編譯步驟,直接載入資料夾即可使用 |
Threadkeep/
├── manifest.json # 擴充功能設定檔 (Manifest V3 權限與腳本規則)
├── article-store.js # 共用儲存層 (每篇一個 key、舊資料搬移、跨介面同步、標籤合併)
├── theme.js # 外觀設定 (跟隨系統/淺色/深色,Popup 與控制面板共用)
├── ui.css # 共用設計系統 (色彩與字型變數、按鈕、欄位、勾選框、開關、選單、對話框、提示、標籤、背景光)
├── ui.js # 共用介面行為 (Popover 選單定位、游標光斑追蹤)
├── sound.js # 音效與背景音樂播放 (Web Audio、偏好設定、分頁切換時停播)
├── sounds/ # 音效與背景音樂 (Ogg Opus:save、copy、delete、undo、complete、error、background-music)
├── content.js # Content Script (DOM 監聽、對話框攔截、文本清洗、資料提取)
├── styles.css # 注入至 Threads 網頁的通知提示樣式
├── popup.html # 瀏覽器工具列彈出視窗 HTML
├── popup.css # 彈出視窗版面 (頁首、工具列、單欄卡片)
├── popup.js # 彈出視窗控制邏輯 (搜尋、排序、篩選、更多選單、頁內 Modal、匯出、匯入)
├── dashboard.html # 控制面板 HTML (側欄、瀑布流清單、預覽抽屜、設定與匯出選單)
├── dashboard.css # 控制面板版面 (側欄分類格與標籤雲、瀑布流卡片、預覽抽屜、響應式)
├── dashboard.js # 控制面板控制邏輯 (篩選導覽、瀑布流排版、預覽抽屜、循序佇列、批次處理、復原機制)
├── icons/ # 擴充功能圖示 (16、32、48、128 px)
├── images/ # 吉祥物圖 (mascot-320.webp,空狀態、歡迎卡片與預覽載入中)
├── docs/ # GitHub Pages 介紹頁 (index.html、互動清洗示範、介面截圖與 assets/video/ 操作示範影片)
├── llms.txt # AI 友善架構與 RAG 快速索引規範文件
├── README.md # 專案說明文件 (本檔案)
├── CHANGELOG.md # 版本更新紀錄 (Keep a Changelog 格式)
├── CONTRIBUTING.md # 貢獻指南
├── CODE_OF_CONDUCT.md # 貢獻者行為準則
├── SECURITY.md # 安全政策與漏洞回報方式
├── ACCESSIBILITY.md # 無障礙聲明
├── LICENSE # MIT 授權條款
└── .github/ # Issue 範本 (ISSUE_TEMPLATE/) 與 Pull Request 範本
| 檔案路徑 | 模組層級 | 主要職責與實作內容 |
|---|---|---|
manifest.json |
設定層 | 聲明 Manifest V3 規格、最低 Chrome 版本(123)、儲存與分頁權限、主機比對規則、CSP 配置,以及只對 Threads 網域開放的兩個音效檔。 |
article-store.js |
儲存層 | 由 content script、Popup 與控制面板共用:決定每篇貼文的儲存 key、搬移舊版 savedArticles 陣列、逐篇讀寫與刪除、合併批次更新欄位、把 chrome.storage.onChanged 的逐篇變動套用到清單,以及不分大小寫合併標籤統計(getTagGroups)。另外存放外觀與聲音設定的儲存 key(THEME_PREFERENCE_KEY、SOUND_EFFECTS_KEY、BACKGROUND_MUSIC_KEY),以及內嵌可用性檢查的共用函式(getThreadsEmbedPath、checkEmbeddable、applyEmbeddableResult、pickEmbeddableFields)。 |
theme.js |
外觀層 | 由 Popup 與控制面板在 <head> 載入:先以 localStorage 的快取立即套用 data-theme,避免載入時閃爍,再讀取 chrome.storage.local 的 themePreference 並同步各頁面;控制面板「設定」選單裡的「外觀」也由它處理。 |
ui.css |
設計系統層 | Popup 與控制面板共用:以 light-dark() 定義的色彩、字型、字級、圓角與動態變數,按鈕、欄位、勾選框、開關、Popover 選單、對話框、提示、標籤與空狀態等元件,背景流動光與游標光斑,以及減少動態與高對比模式的降級。 |
ui.js |
共用介面行為 | setupMenus 依觸發按鈕位置擺放 Popover 選單(下方放不下就翻到上方)、點選項目後關閉、視窗縮放時收合;setupAmbientLight 以 requestAnimationFrame 節流追蹤游標,更新背景光斑與滑過卡片的局部高光位置。 |
sound.js |
聲音層 | 由 Popup、控制面板與 content script 共用:讀取 soundEffects/backgroundMusic 設定並跟著 chrome.storage.onChanged 同步,以 Web Audio API 解碼與播放 sounds/ 裡的音檔(playSound),控制面板的背景音樂開關、淡入淡出與分頁切換時停播(setupBackgroundMusic、setupSoundSwitches)。 |
sounds/ |
音訊資源層 | 六個短音效與一段 2 分鐘背景音樂(Ogg Opus),由小寶樂隊本機工作室以 CC0 採樣編曲渲染。 |
content.js |
注入腳本層 | 負責監聽 Threads DOM 變化、攔截內嵌對話框、頂層排版提取與正規表達式文本清洗,並在 Threads 頁面顯示儲存結果通知與播放儲存/錯誤音效、儲存後於背景檢查內嵌可用性;在預覽用的 /embed iframe 裡則改為回報卡片高度與載入錯誤給控制面板。 |
styles.css |
注入樣式層 | 定義顯示於 Threads 頁面之儲存成功/失敗浮動通知外觀(墨黑底加香檳金細框)、進場動畫、prefers-reduced-motion 降級,以及深色與高對比模式樣式。 |
popup.html / popup.js |
快速檢視層 | 提供 480×600px 的工具列快速面板,支援即時關鍵字查詢、6 種分類、6 種排序、單篇維護、「更多」選單匯出匯入、頁內確認/匯入 Modal 與音效。 |
dashboard.html / dashboard.js |
完整管理層 | 全螢幕控制面板,提供側欄分類格與標籤/作者雲、瀑布流卡片、預覽抽屜、批次管理、背景更新佇列、可復原的刪除操作、設定選單與背景音樂。 |
llms.txt |
規範說明層 | 提供 AI 代理與 RAG 檢索系統快速索引之結構化摘要說明文件。 |
README.md |
完整文檔層 | 專案主要說明文件,包含完整系統架構、演算法剖析、資料 Schema 與常見問題。 |
CHANGELOG.md |
完整文檔層 | 各版本的新增、改善、修正與移除項目,格式遵循 Keep a Changelog。 |
docs/ |
介紹頁層 | GitHub Pages 介紹頁,不隨擴充功能載入。互動清洗示範由 assets/js/cleaner-worker.js 在 Web Worker 中從 main 分支載入 content.js,直接呼叫 cleanExtractedPostContent 與 isLikelyThreadsFallbackDescription;更名或移除這兩個函式會讓示範失效。頁面上的版本資訊同樣從 main 分支的 manifest.json 讀取。 |
flowchart TD
subgraph ClientThreadsPage["Threads 網頁前端 (Content Script Scope)"]
A["Threads 貼文 DOM 結構"] -->|"使用者點擊「取得內嵌程式碼」"| B["官方嵌入對話框 (Role Dialog)"]
C["MutationObserver 監聽器"] -->|"自動捕獲對話框開啟"| B
B -->|"提取 input/textarea 內容"| D["嵌入碼權重計分演算法 (Embed Scorer)"]
A -->|"定位頂層文字容器"| E["頂層文字容器排版擷取器"]
E -->|"過濾 UI/時間雜訊 (44 條 Regex)"| F["純淨內文與中繼資料"]
D & F -->|"組合資料物件"| G["saveArticleFromEmbedDialog"]
end
subgraph LocalDatabase["本機安全儲存 (Storage Scope)"]
G -->|"safeStorageSet (防例外寫入)"| H[("chrome.storage.local\n[post:貼文代碼] 每篇一筆")]
end
subgraph PopupView["Popup 彈出面板 (480×600px)"]
H -->|"loadArticleRecords (讀取)"| I["popup.js 渲染引擎"]
I --> J["全文搜尋 / 6種排序 / 6種篩選"]
I --> K["單篇卡片維護 / 「更多」選單匯出匯入"]
end
subgraph DashboardView["控制面板"]
H -->|"loadArticleRecords (讀取)"| L["dashboard.js 核心引擎"]
L --> M["側欄:分類格 / 標籤雲 / 作者雲 / 資料按鈕"]
L --> N["瀑布流卡片清單與批次列"]
L --> O["預覽抽屜 (Live Preview Drawer)"]
O --> P["官方 Frame 預覽 / 原始碼中繼資料雙分頁"]
end
subgraph BackgroundQueue["背景循序更新佇列"]
L -->|"觸發「更新貼文資料」"| Q["Sequential Update Queue"]
Q -->|"重用單一靜默分頁"| R["chrome.tabs.update (active: false)"]
R -->|"注入提取腳本"| S["chrome.scripting.executeScript"]
S -->|"檢查轉導 / 404 / DOM 資訊"| T{"存活狀態判定"}
T -->|"正常 (Active)"| U["更新發文時間與內容"]
T -->|"異常 (Expired)"| V["標記失效狀態與原因"]
U & V -->|"回寫儲存"| H
T -->|"佇列結束或發生錯誤"| W["chrome.tabs.remove (關閉工作分頁)"]
end
sequenceDiagram
autonumber
participant UI as 控制面板 (dashboard.js)
participant Queue as 更新佇列控制器
participant Tab as 背景靜默分頁 (chrome.tabs)
participant Page as Threads 貼文伺服器
participant Storage as chrome.storage.local
UI->>Queue: 啟動更新佇列 (依勾選項目或當前篩選清單)
loop 依序處理每一篇貼文
Queue->>Tab: 重用單一隱藏分頁載入 postLink
Tab->>Page: 發送 HTTP 請求
Page-->>Tab: 回傳 HTML 與動態內容
Queue->>Tab: 等待分頁載入與解析 (載入加解析合計最多 60 秒)
alt 貼文網址變更 (Redirected)
Queue->>Queue: 標記 status='expired', reason='redirected'
else 頁面不存在或已被移除
Queue->>Queue: 標記 status='expired', reason='post-not-found'
else 成功讀取發布時間與最新內容
Queue->>Queue: 更新 timestampTitle, 清除失效狀態 status='active'
end
opt 累積 20 篇或按下暫停
Queue->>Storage: 只寫回這批貼文各自的紀錄 (合併更新欄位)
end
Queue->>UI: 更新進度條與該篇卡片 (X/Y)
end
Queue->>Tab: 佇列結束後關閉工作分頁 (chrome.tabs.remove)
Queue->>Storage: 寫回剩餘的更新
Queue->>UI: 更新完畢通知
sequenceDiagram
autonumber
participant User as 使用者操作
participant Modal as 預覽抽屜控制器 (dashboard.js)
participant Iframe as 安全預覽框架 (iframe, 最寬 560px)
participant Content as iframe 內的 content.js (all_frames)
participant ThreadsEmbed as Threads 原生內嵌伺服器
User->>Modal: 點作者名稱或卡片空白處,或在抽屜裡按方向鍵
Modal->>Modal: 解析貼文 postLink 產生標準 Embed URL
Modal->>Iframe: 設定 iframe.src = https://www.threads.com/.../embed
Modal->>Modal: 顯示吉祥物載入動畫並啟動 10 秒逾時計時器
Iframe->>ThreadsEmbed: 載入原生卡片資源
ThreadsEmbed-->>Iframe: 渲染互動卡片
Content->>Content: ResizeObserver / MutationObserver 量測 .Embed 容器高度
Content->>Modal: window.postMessage ({ type: 'THREADS_EMBED_RESIZE', height })
Modal->>Modal: 驗證 origin (threads.net / threads.com / instagram.com)
Modal->>Iframe: 動態調整 iframe.style.height = targetHeight (100~5000px)
alt 載入完成
Modal->>Modal: 清除計時器,隱藏載入動畫,呈現完美貼合之原生預覽
else Threads 不提供內嵌(HTTP 500)
Content->>Modal: window.postMessage ({ type: 'THREADS_EMBED_ERROR' })
Modal->>User: 顯示中文說明,隱藏 Meta 錯誤頁
else 逾時或載入失敗
Modal->>User: 顯示錯誤說明與「重新載入」按鈕
end
為避免抓取到的內文換行被過度壓平,content.js 採用頂層文字容器鎖定策略:
- 定位候選容器:選取
span[class*="xo1l8bm"][dir="auto"]、span[class*="xi7mnp6"][dir="auto"]與div[class*="x1iorvi4"][dir="auto"]等主內容區塊。 - 嚴格過濾時間與標頭節點:
- 排除
time標籤本身及其父容器(closest('time') || querySelector('time'))。 - 排除貼文固定網址標籤(
a[href*="/post/"]、a[href*="/t/"])。 - 排除純作者標頭連結(
a[href*="/@"])。 - 排除主題標籤與熱門話題連結(
a[href*="/search"])。 - 排除含媒體的容器,但內文裡
aria-hidden="true"的行內貼圖與表情圖片不算媒體。 - 排除 Meta AI 引導文案與輪播指示器。
- 單篇貼文頁的
threads_post_page_0只在有文字時才當作搜尋範圍;Threads 目前把它渲染成空的佔位元素,目標貼文改由永久連結定位。
- 排除
- 頂層去重 (Top-level Deduplication):僅保留未被其他候選容器包含的最頂層容器,防止子節點文字重複提取。
- 原生排版輸出:沒有可用的
threads_post_page_0範圍時(目前一律如此),以第一個「回覆…」容器為界,只保留它之前的候選容器。每個頂層容器先複製一份,移除翻譯、「查看原文」與雜訊按鈕後再讀innerText,保留<br>與天然段落換行。各段經cleanExtractedPostContent清洗,丟掉整段都是雜訊的內容與重複段落,以\n\n拼接後再整體清洗一次。
const beforeReply = replyBoundary
? candidateContainers.filter(el =>
el.compareDocumentPosition(replyBoundary) & Node.DOCUMENT_POSITION_FOLLOWING
)
: candidateContainers;
const topContainers = beforeReply.filter(el =>
!beforeReply.some(other => other !== el && other.contains(el))
);
const candidates = topContainers
.map(el => {
const clone = el.cloneNode(true);
const childButtons = clone.querySelectorAll('button, [role="button"]');
for (const btn of childButtons) {
const btnText = (btn.innerText || btn.textContent || '').trim();
if (
isLikelyThreadsFallbackDescription(btnText) ||
/^(?:查看|隱藏)?翻譯$/i.test(btnText) ||
/^(?:See|Hide)?\s*translation$/i.test(btnText) ||
/^查看原文$/i.test(btnText)
) {
btn.remove();
}
}
return (clone.innerText || clone.textContent || '').trim();
})
.map(text => cleanExtractedPostContent(text))
.filter(text => text && !isLikelyThreadsFallbackDescription(text))
.filter((text, index, array) => array.indexOf(text) === index);
return cleanExtractedPostContent(candidates.join('\n\n'));當 Threads 彈出嵌入對話框時,頁面可能存在多個唯讀欄位(如純連結或短網址)。extractEmbedCodeFromDialog 對所有輸入框進行權重評分:
let score = value.length;
if (/data-text-post-permalink=/i.test(value)) score += 1000;
if (/<blockquote/i.test(value)) score += 500;
if (/threads\.com/i.test(value)) score += 100;最後挑選分數最高的內容作為最佳的 embedCode,確保 100% 取得包含完整 blockquote 的標準內嵌碼。
extractTags 由兩個來源合併標籤,最後統一正規化並去重:
- 官方 Hashtag 連結:掃描貼文容器中的
a[href*="serp_type=tags"]與a[href*="tag_id="],優先取網址中的q查詢參數,取不到時才退回連結文字,並去除開頭的#。 - 內文 Hashtag:以
#或全形#開頭、由任何語言的文字、數字或底線組成,而且至少含一個文字的詞,例如#台南美食、#ラーメン、#라면、#café、#2024台南燈會。規則為/(?<![\p{Script=Latin}\p{Script=Greek}\p{Script=Cyrillic}\p{N}\p{M}_&\/])[##](?![\uFE0F\u20E3])([\p{L}\p{M}\p{N}_]*\p{L}[\p{L}\p{M}\p{N}_]*)/gu,掃描前會先移除http://、https://與www.開頭的網址。#前面緊接拉丁、希臘或西里爾字母、數字、底線、&或/時不算,因此example.com/docs#install、C#12、'都不會變成標籤;純數字的#1、#2024與鍵帽表情符號#️⃣也不算。#前面是中日韓字元時(例如「去吃#台南美食」)照樣算。 內文沒有#的詞不會變成標籤。2.7.0 起移除了原本的技術關鍵字映射,它會依內文補上 JavaScript、Python 等 12 個標籤,詳見 CHANGELOG.md。
最後以 String.prototype.normalize('NFC') 統一 Unicode 表示法,再不分大小寫去重並保留第一次出現的寫法,避免同一個中文標籤因組合字元差異而重複計數,也避免官方連結的 JavaScript 和內文的 #javascript 同時出現。
Note
背景更新佇列注入的 extractPostInfoFromPage 不另外保留擷取邏輯,而是直接呼叫 Threads 頁面上 content.js 的 findPostElementFromPostLink、extractPostContent 與 extractTags,因此「更新貼文資料」抓到的內文與標籤和存檔時一致。
擷取到的原始文字會先經過 cleanExtractedPostContent 逐行處理(正規化空白、剝除輪播計數與翻譯按鈕、壓縮多餘空行),再由 isLikelyThreadsFallbackDescription 以 44 條正規表達式逐行比對,命中即整行剔除:
function isLikelyThreadsFallbackDescription(text) {
const normalizedText = String(text).replace(/\s+/g, ' ').trim();
return [
/\d[\d,.]*\s*(?:萬|千)?次?瀏覽/i,
/^回覆[\s\S]*[…\.]{1,3}$/i,
/^尚無回覆$/i,
/^查看動態$/i,
/^更多$/i,
/^返回$/i,
/^直欄標題$/i,
/^附加影音內容$/i,
/^新增 GIF$/i,
/^展開撰寫工具$/i,
/^分享$/i,
/^轉發$/i,
/^讚$/i,
/^為你推薦$/,
/^新串文$/,
/^搜尋$/,
/^動態$/,
/^個人檔案$/,
/^聯邦宇宙$/,
/^洞察報告$/,
/^已儲存$/,
/^追蹤中$/,
/^追蹤$/,
/^已追蹤$/,
/^(?:已釘選|Pinned)$/i,
/^(?:作者|Author)$/i,
/^[·•]$/,
/^(?:查看|隱藏)?翻譯$/i,
/^(?:See|Hide)?\s*translation$/i,
/^查看原文$/i,
/^附帶原始貼文的回覆內容$/,
/\d[\d,.]*\s*位粉絲\s*•\s*\d[\d,.]*\s*則串文/i,
/\d[\d,.]*\s*followers\s*•\s*\d[\d,.]*\s*threads/i,
/查看\s*@.+\s*參與的最新對話/i,
/See\s*what\s*@.+\s*is\s*saying\s*on\s*Threads/i,
/在貼文中提及\s*@meta\.ai\s*,即可在這裡獲得解答/i,
/^\d+\s*(?:秒|分|分鐘|小時|天|週|年|s|m|h|d|w|y)(?:前)?$/i,
/^\d{1,2}\s*月\s*\d{1,2}\s*日$/i,
/^\d{4}\s*年\s*\d{1,2}\s*月\s*\d{1,2}\s*日$/i,
/^[A-Z][a-z]{2}\s+\d{1,2}(?:,\s*\d{4})?$/i,
/^(?:剛剛|昨天|前天|yesterday|just now)$/i,
/^\d+\s*[\//]\s*\d+(?:\s*[•·]\s*[\u4e00-\u9fa5\w]+)?$/i,
/^\d+\s*(?:of|之)\s*\d+$/i,
/^(?:圖片|相片|photo|image)\s*\d+\s*[\//,,共of\s]+\d+(?:\s*張)?$/i
].some(pattern => pattern.test(normalizedText));
}在 dashboard.js 中,貼文更新採用單一可重用工作分頁的循序控制迴圈,並於每一輪開頭檢查「取消」與「暫停」訊號:
for (const article of articlesNeedingUpdate) {
if (cancelUpdateRequested) break;
if (isUpdatePaused) await flushPendingUpdates(); // 暫停前先寫回已完成的部分
while (isUpdatePaused && !cancelUpdateRequested) {
await new Promise(resolve => setTimeout(resolve, 150)); // 暫停中等待恢復
}
if (cancelUpdateRequested) break;
const tabId = await getOrCreateWorkerTab(); // 重用同一個背景分頁
const postInfo = await fetchPostInfoWithReusableTab(tabId, article.postLink); // 最多等 60 秒
if (postInfo && postInfo.status === 'expired') {
markArticleAsExpired(article, postInfo.reason); // redirected / post-not-found / fallback-summary
} else if (postInfo) {
clearArticleExpiredStatus(article); // 貼文復活則清除失效標記
// 寫回 timestamp、timestampTitle、content 與 tags
}
pendingArticles.add(article);
if (pendingArticles.size >= UPDATE_PERSIST_BATCH_SIZE) await flushPendingUpdates(); // 每 20 篇合併寫回
refreshRenderedCard(article); // 只重繪這一張卡片
}
// finally:關閉工作分頁、寫回剩餘的更新,再整體重繪清單與標籤雲、作者雲- 分批合併寫回:
flushPendingUpdates呼叫mergeArticleUpdates,只讀取這批貼文各自的 key,把ARTICLE_UPDATE_FIELDS(timestamp、timestampTitle、content、tags、失效狀態、timestampUpdatedAt、embeddable、embeddableCheckedAt)以記憶體中的值套到目前儲存的紀錄上再寫回,記憶體中沒有的欄位會從紀錄刪除;已不存在的 key 代表貼文在佇列期間被刪除,會直接略過。中途關閉頁面最多遺失最後未寫回的 19 篇更新結果,重新執行即可補上。 - 分頁生命週期:工作分頁僅在擷取失敗或整個佇列結束(
finally)時關閉,其餘時間重複使用,避免反覆開關分頁的成本。 - 安全上限:
fetchPostInfoWithReusableTab以 60 秒(POST_FETCH_TIMEOUT_MS)為上限,涵蓋分頁載入與頁面解析;逾時回傳null,記為失敗並關閉工作分頁,不會標記為失效。注入頁面的解析器以MutationObserver觸發檢查,兩次檢查至少間隔 80ms(POST_INFO_CHECK_INTERVAL_MS)。
Threads 官方 /embed 頁面只會對父視窗送出一個不帶型別的數字字串,無法和其他訊息區分,因此本擴充功能透過 manifest.json 的 all_frames: true 將 content.js 一併注入預覽 iframe。當腳本偵測到自己位於 /embed 或非頂層框架時,改走 handleEmbedFrameResize() 分支:以 ResizeObserver 觀測 .Embed 容器與 body 的第一個子元素,容器被替換時由只看節點增減的 MutationObserver 重新掛上觀測。量測合併到每個畫面影格最多一次,高度有變化才將量得的高度回傳父視窗:
window.parent.postMessage({ type: 'THREADS_EMBED_RESIZE', height: h }, parentOrigin);Threads 不提供內嵌的貼文(已刪除、不公開,或只開放登入的使用者觀看,例如被列為敏感內容),/embed 會回 HTTP 500 與 Meta 的錯誤頁;這類貼文的內嵌碼放到網站上,未登入 Threads 或封鎖第三方 Cookie 的訪客同樣顯示不出來。預覽從 www.threads.com 載入,會帶著瀏覽器的 Threads 登入狀態,所以只開放登入者觀看的貼文在已登入時可以正常預覽。iframe 一樣會觸發 load,所以 content.js 在 /embed 頁找不到 .Embed 容器時改送 { type: 'THREADS_EMBED_ERROR' }。控制面板確認訊息來自目前的預覽 iframe 後,顯示自己的錯誤提示並隱藏錯誤頁,之後的 load 事件也不會再把狀態改回「已載入」。
content.js 會注入所有 threads.com 框架,包括其他網站嵌入的 Threads iframe,但只有在直接父視窗是本擴充功能的頁面時才送訊息:以 location.ancestorOrigins[0] 的 host 比對 chrome.runtime.id,相符才送出,而且 postMessage 只指定該來源,不用 '*'。其他網站嵌入的 Threads iframe 不會收到 THREADS_EMBED_RESIZE 或 THREADS_EMBED_ERROR,也就看不出瀏覽器裝了本擴充功能。
控制面板端先驗證來源網域;THREADS_EMBED_ERROR 只在 event.source 是目前的預覽 iframe 時才處理。高度依序取 THREADS_EMBED_RESIZE 的 height、官方 MEASURE 等格式的 details.height(字串會先解析成 JSON),再退回 height 或 scrollHeight,只套用 100–5000px 之間的值:
function isAllowedEmbedOrigin(origin) {
if (!origin) return false;
try {
const url = new URL(origin);
const hostname = url.hostname.toLowerCase();
const allowedDomains = ['threads.net', 'threads.com', 'instagram.com'];
return allowedDomains.some(domain => hostname === domain || hostname.endsWith('.' + domain));
} catch (_) {
return false;
}
}
window.addEventListener('message', (event) => {
if (!isAllowedEmbedOrigin(event.origin)) return;
let data = event.data;
if (typeof data === 'string') {
try { data = JSON.parse(data); } catch (_) { }
}
const iframe = document.getElementById('previewIframe');
if (data && data.type === 'THREADS_EMBED_ERROR') {
if (iframe && event.source === iframe.contentWindow) {
clearTimeout(previewLoadTimer);
setPreviewState('error', '…');
}
return;
}
if (!data || typeof data !== 'object') return;
let details = data.details;
if (typeof details === 'string') {
try { details = JSON.parse(details); } catch (_) { }
}
let targetHeight = 0;
if (data.type === 'THREADS_EMBED_RESIZE' && data.height) {
targetHeight = Number(data.height);
} else if (details && details.height) {
targetHeight = Number(details.height);
} else if (data.height) {
targetHeight = Number(data.height);
} else if (data.scrollHeight) {
targetHeight = Number(data.scrollHeight);
}
if (targetHeight > 100 && targetHeight < 5000 && iframe) {
iframe.style.height = `${targetHeight}px`;
}
});每篇貼文是一個 SavedArticle 物件,各自存在 chrome.storage.local 的一個 key 底下。key 為 post: 加上貼文網址中的貼文代碼(例如 post:DezKX7tE_q9);沒有可辨識連結的匯入資料改用 post:id:<id>。同一篇貼文(包括 threads.com 與 threads.net 的網址)會對應到同一個 key,再存一次會覆蓋整筆紀錄,但保留先前的 embeddable/embeddableCheckedAt,並在背景重新檢查一次。各介面讀取後依 savedAt 由新到舊排列。
Note
2.2.1 以前的版本把所有貼文存成 savedArticles 鍵裡的一個陣列。新版第一次開啟 Popup、控制面板或在 Threads 存檔時,會把舊陣列拆成逐篇紀錄:只寫入尚未存在的 key(不覆蓋較新的紀錄),同一篇的重複紀錄保留陣列中較前面(較新)的那筆,讀回確認全部寫入後才刪除 savedArticles。搬移後改裝舊版會讀不到資料,若需要降版,請先匯出完整備份再匯入。
interface SavedArticle {
/** 唯一主鍵識別碼 (新儲存為 embed_[時間戳]_[隨機字串];匯入資料為 imported_[時間戳]_[隨機字串],來源檔已有 id 則沿用) */
id: string;
/** 貼文原始 URL,作為去重與更新的主鍵 */
postLink: string;
/** Threads 官方原生 <blockquote> HTML 內嵌碼 */
embedCode: string;
/** 發布時間 (ISO 8601 標準格式) */
timestamp: string;
/** 格式化發布時間標題 (例如: "2026年5月29日 上午10:00") */
timestampTitle: string;
/** 儲存至本機資料庫的時間 (ISO 8601) */
savedAt: string;
/** 貼文純文字內容 (已保留段落換行並清除 UI 雜訊) */
content: string;
/** 發文者帳號,取自作者連結文字 (通常不含 @,如 "username");取不到時用網址中的帳號,否則為「未知作者」 */
author: string;
/** 分類標籤清單 (官方 Hashtag 連結與內文 #hashtag) */
tags: string[];
/** 貼文存活狀態 */
status: 'active' | 'expired';
/** 標記為失效的時間 (ISO 8601) */
expiredAt?: string;
/** 失效具體原因 */
expiredReason?: 'redirected' | 'post-not-found' | 'fallback-summary' | 'unknown';
/** 最後一次檢查存活狀態的時間 (ISO 8601) */
expiredCheckedAt?: string;
/** 發布時間戳記最後更新時間 (ISO 8601) */
timestampUpdatedAt?: string;
/** 不帶 Cookie 時 Threads 是否提供內嵌卡片:/embed 回 HTTP 500 為 false,回 200 且含 .Embed 為 true;從未得出結論時不存在,之後的檢查沒有結論時保留上次的值 */
embeddable?: boolean;
/** 最後一次得出 embeddable 結論的時間 (ISO 8601) */
embeddableCheckedAt?: string;
/** 匯入來源標記,只有 Popup 匯入 .js 檔時寫入;控制面板匯入與 .json 匯入不寫入 */
importedFrom?: 'full-data-file' | 'js-embed-file';
}Note
自 v2.1.0 起,codeBlocks 與 codeCount 欄位已移除。相關抽取邏輯在舊版儲存流程被嵌入對話框攔截取代後即未再接線,新資料一律為空陣列;匯入舊備份時也不會再保留這兩個欄位,程式碼內容亦不再納入搜尋範圍。自 v2.2.1 起,搜尋比對 content、author、tags 與 postLink 的路徑部分(不含網域);先前比對的 embedCode 大多是固定的官方樣板(SVG 與行內樣式),搜尋「threads」之類的字會命中所有貼文。
- 儲存配額:Chromium
chrome.storage.local預設配額為 10MB,每篇貼文含官方內嵌碼約 3–4 KB,約兩、三千篇就會用完。本擴充功能宣告unlimitedStorage權限解除這個上限,實際容量取決於磁碟空間。 - 逐篇寫入:在 Threads 存一篇只寫入那一篇(約 3–4 KB),刪除只移除對應的 key,不必讀寫全部資料;開啟 Popup 或控制面板時仍會一次讀取全部貼文。
- 寫入失敗提示:若儲存時仍發生錯誤,系統會自動彈出提示。建議定期將資料匯出為「完整版資料備份檔案」並清理不必要的舊資料。
- 偏好設定:與貼文分開存在
chrome.storage.local的三個 key:themePreference(light/dark,跟隨系統時不存在)、soundEffects(false表示關閉音效,不存在時視為開啟)與backgroundMusic(true表示開啟背景音樂,不存在時視為關閉)。這三個 key 不屬於貼文,不會被匯出、匯入或「清除全部」影響。
本擴充功能嚴格遵循最小權限原則(Principle of Least Privilege),僅宣告達成功能所需之必要權限:
| 權限項目 | 權限類型 | 宣告用途與安全說明 |
|---|---|---|
storage |
API 權限 | 讀取與寫入本機 chrome.storage.local 資料庫。 |
unlimitedStorage |
API 權限 | 解除 chrome.storage.local 的 10MB 配額,收藏數千篇貼文時不會寫入失敗;Chrome 安裝時不會因此顯示額外的權限警告。 |
tabs |
API 權限 | 在背景建立靜默分頁以執行「更新貼文資料」與失效檢查。 |
scripting |
API 權限 | 向背景載入的 Threads 分頁動態注入資訊擷取腳本。 |
https://www.threads.com/* |
主機權限 | 允許在標準 Threads 網域注入 Content Script 並讀取貼文內容。 |
https://threads.com/* |
主機權限 | 允許在非 www 前綴的 Threads 網域執行相同操作。 |
https://www.threads.net/* |
主機權限 | 支援透過官方 threads.net 網域載入即時預覽 Frame 與發文時間同步。 |
https://threads.net/* |
主機權限 | 支援非 www threads.net 網域主機權限。 |
web_accessible_resources |
資源開放 | 只把 sounds/save.ogg 與 sounds/error.ogg 開放給上述四個 Threads 網域,讓 Threads 頁面上的 content script 能播放儲存與錯誤音效;其他網站讀不到這兩個檔案。 |
minimum_chrome_version: 123 |
相容性 | 介面配色使用 CSS light-dark(),需要 Chrome 123 以上;較舊的瀏覽器不會安裝這個版本。 |
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'; frame-src https://www.threads.com https://www.threads.net https://*.threads.com https://*.threads.net;"
}- 禁止外部腳本:所有頁面僅載入本機自帶之腳本(
script-src 'self'),不載入任何外部 CDN 腳本或遠端代碼;唯一的外部資源是ui.css以@import引入的 Google Fonts 字型樣式表(Playfair Display、Noto Serif TC、Geist、Geist Mono、Noto Sans TC);音效與背景音樂隨擴充功能打包,不從網路載入。 - 安全 Frame 來源:僅限定允許嵌入來自 Threads 官方網域(
threads.com與threads.net)之 iframe 預覽元件。
三種匯出皆以「目前的選取狀態」決定範圍:有勾選貼文時僅匯出勾選項目,未勾選時則匯出當前篩選與排序後的清單。
| 格式名稱 | 匯出檔案名稱規範 | 格式結構 | 適用情境與特點 |
|---|---|---|---|
| 簡易版嵌入碼 (Embed Only) | threads-embed-codes-YYYY-MM-DD.js |
const posts = ['<blockquote>...</blockquote>', ...]; |
專為靜態網頁快速引用設計,已自動移除重複的 script 標籤並轉義引號。 |
| 精選貼文資料 (Featured Data) | threads-featured-data-YYYY-MM-DD.js |
const posts = [{ embedCode, postLink, author, content, tags, embeddable?, embeddableCheckedAt? }, ...]; |
專為關聯專案 Threadfolio 設計,author 欄位自動去除 @ 前綴以利作為 Key 或展示標籤使用。檢查過內嵌可用性的貼文才帶 embeddable 與 embeddableCheckedAt;網站只在 embeddable === false 且 iframe 載入失敗時改顯示「只限登入者觀看」的說明。 |
| 完整版備份資料 (Full Data) | threads-full-data-YYYY-MM-DD.js |
const posts = [{ embedCode, postLink, author, content, timestamp, timestampTitle, savedAt, tags, status, ... }, ...]; |
包含內文、時間、標籤、失效狀態與內嵌可用性(檢查過才有)等欄位,適用於跨裝置備份、資料遷移與災難還原。不含 id,匯入時會重新產生。 |
const posts = [
'<blockquote class="text-post-media" data-text-post-permalink="https://www.threads.com/@user/post/abc123xyz">...</blockquote>',
'<blockquote class="text-post-media" data-text-post-permalink="https://www.threads.com/@developer/post/def456uvw">...</blockquote>'
];const posts = [
{
"embedCode": "<blockquote class=\"text-post-media\" data-text-post-permalink=\"https://www.threads.com/@user/post/abc123xyz\">...</blockquote>",
"postLink": "https://www.threads.com/@user/post/abc123xyz",
"author": "user",
"content": "分享一段實用的 JavaScript 技巧:...",
"tags": ["JavaScript", "Frontend"]
}
];const posts = [
{
"embedCode": "<blockquote class=\"text-post-media\" ...>...</blockquote>",
"postLink": "https://www.threads.com/@user/post/abc123xyz",
"author": "user",
"content": "完整貼文內容...",
"timestamp": "2026-05-29T02:00:00.000Z",
"timestampTitle": "2026年5月29日 上午10:00",
"savedAt": "2026-05-29T02:10:00.000Z",
"tags": ["JavaScript", "WebDev"],
"status": "active",
"expiredAt": "",
"expiredReason": "",
"expiredCheckedAt": ""
}
];匯入模組採兩階段解析管線:
flowchart TD
A["選擇檔案(.js / .json)"] --> C
B["在 Popup 貼上匯出檔內容"] --> C
C["階段一:JSON.parse 解析 const posts = [...] 陣列"] -->|"成功"| E
C -->|"失敗,例如單引號字串或未加引號的鍵"| D["階段二:正規表達式抽出內嵌碼"]
D --> E["選擇匯入方式:合併資料或完全覆寫"]
E -->|"合併資料"| F["加入尚未存在的貼文"]
E -->|"完全覆寫"| G["二次確認:確認完全覆寫或取消"]
Note
.json 檔會直接以 JSON.parse 解析(支援純陣列或 { "savedArticles": [...] } 包裝);.js 檔與 Popup 貼上的文字則走上述兩階段管線。三種匯出格式中,「精選格式」與「完整備份」是標準 JSON,會在階段一解析;「僅內嵌碼」是單引號字串陣列,由階段二抽出每段帶有 data-text-post-permalink 的內嵌碼。擴充功能頁面的 CSP 禁止 eval 與 new Function,因此不以動態求值解析 JavaScript 物件寫法。
Important
- 合併資料 (Merge):以貼文代碼(儲存 key)判斷重複,threads.com 與 threads.net 的同一篇視為同一筆。已存在的貼文會略過,現有資料完全保留。
- 完全覆寫 (Overwrite):清空現有的本機資料,完全以匯入檔案中的內容取代。此模式必須再通過一次「確認完全覆寫」對話框;直接關閉或按
ESC一律視為取消,不會誤觸覆寫。
| 按鍵 | 操作動作 |
|---|---|
ESC |
關閉預覽抽屜,焦點回到該卡片的作者名稱 |
← (左方向鍵) |
切換至上一篇貼文預覽 (Navigate Previous) |
→ (右方向鍵) |
切換至下一篇貼文預覽 (Navigate Next) |
Tab |
在抽屜內循環移動焦點,不會跑到背後的頁面 |
點擊背景 |
關閉預覽抽屜 |
| 操作目標 | 操作方式 |
|---|---|
| 開啟即時預覽 | 點卡片上的作者名稱或卡片空白處,預覽抽屜從右側滑出。 |
| 切換預覽分頁 | 在抽屜上方點「內嵌預覽」或「原始碼與中繼資料」;內嵌卡片隨抽屜寬度縮放(最寬 560px),高度自動貼合貼文內容。 |
| 批次操作 | 勾選卡片左上角的核取方塊,或點列表頭的「全選」勾選目前篩選結果的全部貼文(包含尚未載入的卡片);勾選後列表頭換成批次列,可使用「更新資料」「複製內嵌碼」「刪除」與「取消選取」。 |
| 分類與篩選 | 點側欄頂端的分類格(全部、失效、無內文、無發文時間),或標籤雲、作者雲、卡片上的標籤;再點一次已選的標籤或作者即取消。標籤或作者超過 15 個時,點「全部 N 個…」展開。 |
| 排序 | 在列表頭右側的排序選單切換 6 種排序規則;卡片上的時間會跟著顯示「儲存」或「發文」時間。 |
| 背景同步更新 | 側欄「資料」的「更新資料」與批次列的「更新資料」行為相同:有勾選時只更新目前篩選結果中被勾選的貼文,沒有勾選時更新整個篩選結果。執行中側欄會出現進度條,可隨時「暫停/繼續」或「停止」。 |
| 自選範圍匯出 | 先勾選欲匯出的貼文,再從側欄「資料」的「匯出」選單選格式,即只匯出勾選項目;未勾選時匯出目前篩選結果。選單標題會寫出這次的範圍。 |
| 外觀與聲音 | 點右上角的「設定」:外觀可選跟隨系統、淺色或深色;音效與背景音樂各有一個開關。 |
| 復原誤刪 | 單篇刪除、批次刪除與清除全部後,畫面下方中央的提示會出現「復原」按鈕,預設 8 秒內點擊即可還原;滑鼠移到提示上或焦點停在按鈕上時暫停倒數。 |
| 匯入資料 | Popup 在「更多」選單點「匯入…」後可選擇讀取檔案或直接貼上內容;控制面板在側欄「資料」點「匯入」選擇備份檔,接著選擇合併或覆寫。 |
Warning
問題:點擊取得內嵌程式碼後,網頁上沒有出現「儲存成功」提示?
- 請確認瀏覽器是否已登入 Threads 帳號。未登入狀態下,Threads 官方對話框可能無法正確產生內嵌代碼。
- 確認當前頁面網址是否為標準的
threads.com或www.threads.com網域。 - 若瀏覽器開發者主控台出現
Extension context invalidated警告,此為 Chrome 重新載入擴充功能後的常見安全機制,只需重新整理對應的 Threads 網頁即可恢復運作。 - 若 Threads 顯示「發生錯誤。這可能是技術錯誤所導致,我們正努力修復。」,代表 Threads 這次沒有產生內嵌程式碼,擴充功能會提示「Threads 回報錯誤,請稍後再試」。這是 Threads 端的暫時錯誤,稍等一下再點一次「取得內嵌程式碼」即可。
Note
問題:為什麼有些貼文在背景更新後被標記為「失效貼文」?
- 這代表貼文可能已被原作者刪除、帳號被設為私密,或原作者封鎖了匿名存取。
- 背景分頁若確認頁面不存在(例如 404 或已刪除),也會判定為失效。
- 若貼文隨後恢復正常,在下一次更新時,系統偵測到內容便會自動將其回復為
active狀態。 - 只開放登入者觀看的貼文(例如被列為敏感內容),在已登入 Threads 的瀏覽器裡更新時會正常載入,不會被標成失效;未登入時會被導回 Threads 首頁,記為
redirected。
Note
問題:即時預覽顯示「Threads 不提供這篇貼文的內嵌卡片」?
- Threads 對這篇的
/embed回了 HTTP 500。可能是貼文已刪除、不公開,或只開放登入的使用者觀看。 - 若是只開放登入者觀看,在這個瀏覽器登入 threads.com 後按「重新載入」即可預覽。這類貼文的內嵌碼放到網站上時,只有已登入 Threads、且瀏覽器允許第三方 Cookie 的訪客看得到,其他訪客只能點連結到 Threads 觀看。
- 開發者工具裡的
Error with Permissions-Policy header: Unrecognized feature: 'attribution-reporting'(以及shared-storage、shared-storage-select-url)是 Meta 回應標頭宣告了瀏覽器不認得的功能,正常的貼文也會出現,與載入失敗無關。
Caution
問題:Threads 官方改版後,擴充功能無法正常擷取?
- 本工具高度依賴 Threads 前端網頁的 DOM 選擇器特徵(例如特定編碼的 class 類別)。
- 若 Threads 官方進行了重大的結構或樣式修改,將會導致擷取演算法失效。此時請將問題提交至 GitHub Issue,我們將會盡快更新
content.js中對應的 CSS 選擇器。
Important
問題:本機儲存空間是否有限制?
chrome.storage.local在多數 Chromium 瀏覽器中預設有 10MB 的配額限制;本擴充功能宣告unlimitedStorage權限解除這個上限,實際容量取決於磁碟空間。- 若寫入仍然失敗,擴充功能會提示您。建議定期將完整版資料匯出備份,並清除不需要的舊貼文。
Tip
問題:不小心刪錯貼文或按到「清除全部資料」怎麼辦?
- 刪除、批次刪除與清除全部皆為可復原操作,畫面下方中央的提示列會顯示「復原」按鈕,預設 8 秒內點擊即可完整還原;滑鼠移到提示上或焦點停在按鈕上時倒數會暫停。
- 提示消失後即無法復原,因此執行清除前仍建議先以「完整備份」格式匯出。
- 匯入時若選擇「完全覆寫」,必須再通過一次確認對話框;關閉視窗或按
ESC一律視為取消。
歡迎所有開發者協助改進本專案!如果您有任何建議或發現 Bug,請隨時提交 Issue 或 Pull Request。
| 文件 | 內容 |
|---|---|
| CONTRIBUTING.md | 回報問題、開發環境、手動測試清單、Commit 與 PR 流程 |
| CODE_OF_CONDUCT.md | 社群行為準則與檢舉方式 |
| SECURITY.md | 支援版本、私密漏洞回報管道與回報範圍 |
| ACCESSIBILITY.md | 無障礙目標、已知限制與障礙回報方式 |
| CHANGELOG.md | 各版本的新增、改善、修正與移除項目 |
本專案為純原生前端架構之瀏覽器擴充功能,零外部相依、無編譯打包步驟、亦無需啟動任何本機伺服器:
# 複製專案庫
git clone https://github.com/Scorpio-meow/Threadkeep.git
cd Threadkeep
# 直接在瀏覽器擴充功能頁面載入未封裝項目,即可開始偵錯與開發- 零外部依賴原則:專案嚴格保持原生輕量化設計,請勿引入任何 npm 執行期依賴或外部 CDN 框架。
- 嚴格 CSP 相容:HTML 中禁止使用
style屬性與行內事件監聽器(例如onclick="..."),必須使用addEventListener進行事件綁定;需要由程式決定的數值(例如瀑布流的跨列數、游標光斑位置、卡片淡入順序)一律由 JS 設定 CSS 變數或單一屬性。 - 共用設計系統:顏色、字型、字級與圓角一律使用
ui.css的變數,新元件先找ui.css裡現成的.btn、.field、.check、.switch、.menu、.tag-chip等類別;字級只用 12、14、16、20px。新增顏色時要同時給出淺色與深色值,並確認文字對比至少 4.5:1。 - 欄位擴展規範:若在
content.js中新增或修改了儲存欄位,請務必同步更新本 README 的 資料模型與儲存 Schema 區段、llms.txt 與匯出/匯入模組。 - 禁用原生對話框:
confirm()、alert()與prompt()一律以頁內 Modal 取代。Popup 失焦時原生對話框會連同 Popup 一併關閉並回傳預設值,極易造成誤觸破壞性操作。 - 聲音:新增音效時把檔案放進
sounds/並登記到sound.js的SOUND_EFFECT_FILES,在真的成功或失敗時才呼叫playSound;若要在 Threads 頁面播放,還要加進manifest.json的web_accessible_resources。音效只能補充,畫面與 live region 仍要傳達同樣的結果。 - 無障礙基本要求:新增的 Modal 需具備
role="dialog"、aria-modal="true"、焦點鎖定與焦點還原;可點擊元素請使用button並維持 24×24px 以上的點擊區域;停用狀態請使用disabled而非僅靠pointer-events: none。卡片上重複出現的按鈕要在名稱帶上是哪一篇;重畫清單時保留焦點;對話框內的元件不要用transition: all。
各版本的新增、改善、修正與移除項目記錄在 CHANGELOG.md,格式遵循 Keep a Changelog。
本專案已在根目錄提供獨立的 llms.txt 規格文件,專供 AI 代理、LLM 檢索工具與 RAG 索引系統快速讀取與結構化解析本專案:
- 獨立文件路徑:
llms.txt - 包含內容:專案核心資訊、核心模組路徑、完整資料模型 Schema、核心演算法實作機制與關聯專案資訊。
本專案採用 MIT 授權條款 開源釋出。您可以自由使用、修改、分發與整合於個人或商業專案中。
本擴充功能為第三方獨立開發之開源工具,與 Meta 或 Threads 官方無任何關聯、授權或隸屬關係。Threads 平台的網頁結構、API 與使用規範可能隨時變更,若因官方平台改版導致擷取功能暫時失效,需等待維護者更新選擇器規則。使用者須自行承擔使用本工具之相關風險。
Threadkeep(舊名 Threads 程式碼儲存器)
由 Scorpio-meow 開發與維護