有室再說
把社群租屋貼文整理成
一目了然的比較清單
分析後自動建立清單・可分享給朋友共同編輯
開啟朋友的清單
diff --git a/README.md b/README.md index 941adc9..0cb8830 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ CeranaStudio 的內部工具箱 — 包含各種語言寫的 utilities、腳本 | [resolve-bot](./tools/resolve-bot/) | TypeScript (Bun) | Discord 提醒機器人,持續 ping 直到任務完成 | | [groq-transcriber](./tools/groq-transcriber/) | Bash | 使用 Groq Whisper API 將音頻/影片轉成文字稿 | | [auto-subtitle](./tools/auto-subtitle/) | TypeScript (Next.js) | 將音訊轉成字幕並嵌入影片,使用 Whisper + FFmpeg | +| [fb-rent-filter](./tools/fb-rent-filter/) | TypeScript (Next.js) | FB 租屋貼文萃取工具,用 AI 將貼文轉成結構化租屋資料 | ## 結構 @@ -17,7 +18,8 @@ cerana-toolbox/ ├── tools/ │ ├── resolve-bot/ # TypeScript / Bun │ ├── groq-transcriber/ # Bash -│ └── auto-subtitle/ # TypeScript / Next.js +│ ├── auto-subtitle/ # TypeScript / Next.js +│ └── fb-rent-filter/ # TypeScript / Next.js └── README.md ``` diff --git a/tools/fb-rent-extension/PUBLISHING.md b/tools/fb-rent-extension/PUBLISHING.md new file mode 100644 index 0000000..caa7e9b --- /dev/null +++ b/tools/fb-rent-extension/PUBLISHING.md @@ -0,0 +1,101 @@ +# FB 租屋過濾器 Chrome Extension — 上架指南 + +## 🔧 本地測試(開發用) + +### 步驟 +1. 打開 Chrome,網址列輸入:`chrome://extensions/` +2. 右上角開啟 **「開發人員模式」**(Developer mode) +3. 點「**載入未封裝項目**」(Load unpacked) +4. 選擇這個資料夾:`tools/fb-rent-extension/` +5. Extension 就裝好了,Chrome 工具列會出現圖示 + +### 測試方式 +1. 打開任意 Facebook 租屋社團,例如: + - https://www.facebook.com/groups/ntp.aptforrent + - https://www.facebook.com/groups/taipei.rent +2. 點 Extension 圖示 +3. 設定條件(可選)→ 按「開始掃描」 +4. Extension 會自動往下滑、分析貼文、加入清單 + +### 修改 code 後更新 +1. 修改任意 `.js` / `.html` 檔案 +2. 回到 `chrome://extensions/` +3. 點那個 Extension 的「重新整理」按鈕(🔄) +4. 重新測試 + +--- + +## 📦 打包上架 Chrome Web Store + +### 前置作業 +- [ ] Google 開發者帳號(一次性費用 $5 USD):https://chrome.google.com/webstore/devconsole +- [ ] Extension 打包成 .zip +- [ ] 準備素材(見下方) + +### 打包 +```bash +# 在 extension 資料夾執行 +cd tools/fb-rent-extension +zip -r fb-rent-extension.zip . \ + --exclude "*.md" \ + --exclude ".DS_Store" \ + --exclude "node_modules/*" +``` + +### 上架素材清單 +| 項目 | 規格 | 說明 | +|------|------|------| +| 截圖 x4 | 1280×800 或 640×400 | 展示 popup UI + 掃描過程 | +| 小圖示 | 128×128 PNG | 已有 icons/128.png | +| 促銷圖(可選) | 440×280 PNG | 商店首頁展示用 | +| 隱私權政策 | 網頁 URL | 必須要有(見下方) | + +### 隱私權政策 +必填。最簡單的做法:在 `fb-rent-filter.cerana-mail.workers.dev` 加一個 `/privacy` 頁面。 + +內容重點: +- 我們不收集用戶的 Facebook 帳號資訊 +- 貼文內容只用於 AI 萃取,不儲存原始文字 +- 不追蹤用戶行為 + +### 上架流程 +1. 前往 https://chrome.google.com/webstore/devconsole +2. 點「+ 新增項目」 +3. 上傳 `.zip` +4. 填寫: + - **名稱**:FB 租屋過濾器 + - **說明**:自動掃描 Facebook 租屋社團貼文,AI 整理成結構化清單,一鍵分享給朋友 + - **類別**:工具 (Tools) + - **語言**:繁體中文 +5. 上傳截圖和圖示 +6. 填寫隱私權政策 URL +7. 送審 + +### 審核時間 +- 首次上架:通常 **2-5 個工作日** +- 更新版本:1-2 個工作日 +- 審核期間如果被退回,會收到 email 說明原因 + +--- + +## ⚠️ 常見問題 + +### FB 的 DOM 結構改了,掃不到貼文 +FB 頻繁更改 CSS class names。如果 `extractPosts()` 失效: +1. 打開 FB 社團頁面 +2. 右鍵貼文文字 → 「檢查」 +3. 找到包含貼文文字的 element +4. 複製該 selector 更新 `content.js` 的 `selectors` 陣列 + +### Extension 被 Chrome 安全機制阻擋 +確認 `manifest.json` 的 `host_permissions` 包含目標網域。 + +### API 呼叫失敗 +確認 `API_BASE` 的 URL 是正確的 Worker URL。 + +--- + +## 🚀 後續計畫 +- [ ] Firefox 版(manifest v2,略有差異) +- [ ] 支援 LINE 社群訊息轉貼 +- [ ] 付費解鎖:更多清單、自動定時掃描 diff --git a/tools/fb-rent-extension/content.js b/tools/fb-rent-extension/content.js new file mode 100644 index 0000000..ff75914 --- /dev/null +++ b/tools/fb-rent-extension/content.js @@ -0,0 +1,211 @@ +// FB 租屋過濾器 - Content Script +// 在 Facebook 頁面上執行,掃描貼文並送到 API + +const API_BASE = "https://fb-rent-filter.cerana-mail.workers.dev"; + +let isScraping = false; +let stopRequested = false; + +// 已處理過的貼文 (避免重複) +const processedHashes = new Set(); + +// 簡單的 string hash(不是 crypto,只用於去重) +function simpleHash(str) { + let hash = 0; + for (let i = 0; i < str.length; i++) { + const char = str.charCodeAt(i); + hash = (hash << 5) - hash + char; + hash = hash & hash; + } + return Math.abs(hash).toString(36); +} + +// 從 FB 頁面抓取可見的貼文文字 +function extractPosts() { + const posts = []; + + // FB 的貼文文字常見 selector(可能隨 FB 更新失效) + // 策略:找到貼文容器,抓取所有文字 + const selectors = [ + // 一般動態貼文 + '[data-ad-comet-preview="message"]', + '[data-ad-preview="message"]', + // 社團貼文 + '.x1lliihq.x6ikm8r.x10wlt62.x1n2onr6', + // 備用:找所有 role=article 的元素 + ]; + + // 主要策略:找 role="article" 的貼文容器 + const articles = document.querySelectorAll('[role="article"]'); + + articles.forEach((article) => { + // 排除廣告和推薦 + if (article.closest('[data-pagelet="FeedUnit"]')?.querySelector('[aria-label="贊助"]')) return; + if (article.querySelector('[aria-label="贊助"]')) return; + + // 抓取貼文文字:找最長的文字區塊 + const textEls = article.querySelectorAll( + '[data-ad-comet-preview="message"], [data-ad-preview="message"], [dir="auto"]' + ); + + let postText = ""; + textEls.forEach((el) => { + const t = el.innerText?.trim(); + if (t && t.length > postText.length) { + postText = t; + } + }); + + // 備用:直接抓整個 article 的文字(截斷前 2000 字) + if (!postText || postText.length < 50) { + postText = article.innerText?.trim().slice(0, 2000) || ""; + } + + if (postText.length < 50) return; // 太短,跳過 + + const hash = simpleHash(postText); + if (processedHashes.has(hash)) return; + + // 基本篩選:包含常見租屋關鍵字才送 + const keywords = ["租", "月租", "押金", "坪", "套房", "分租", "出租", "雅房"]; + const hasKeyword = keywords.some((k) => postText.includes(k)); + if (!hasKeyword) return; + + processedHashes.add(hash); + posts.push(postText); + }); + + return posts; +} + +// 送一篇貼文到 API 分析 +async function analyzePost(postText) { + const res = await fetch(`${API_BASE}/api/analyze`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ posts: [postText] }), + }); + if (!res.ok) throw new Error(`API error: ${res.status}`); + const data = await res.json(); + return data.results || []; +} + +// 把分析結果加入清單 +async function addToList(listId, records) { + const res = await fetch(`${API_BASE}/api/lists/${listId}/records`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ records }), + }); + if (!res.ok) throw new Error(`List API error: ${res.status}`); + return res.json(); +} + +// 建立新清單 +async function createList(name) { + const res = await fetch(`${API_BASE}/api/lists`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name, records: [] }), + }); + if (!res.ok) throw new Error(`Create list error: ${res.status}`); + const data = await res.json(); + return data.id; +} + +// 自動往下滑 +function scrollDown() { + return new Promise((resolve) => { + window.scrollBy({ top: window.innerHeight * 1.5, behavior: "smooth" }); + setTimeout(resolve, 2500); // 等 FB 載入新內容 + }); +} + +// 主掃描流程 +async function startScrape(config) { + if (isScraping) return; + isScraping = true; + stopRequested = false; + + const { maxPrice, district, maxScrolls, listId: inputListId } = config; + let listId = inputListId; + let scanned = 0; + let added = 0; + let scroll = 0; + + try { + // 先滾到頂部 + window.scrollTo({ top: 0, behavior: "smooth" }); + await new Promise((r) => setTimeout(r, 1000)); + + // 掃描 + 滾動 + while (scroll <= maxScrolls && !stopRequested) { + const posts = extractPosts(); + + for (const postText of posts) { + if (stopRequested) break; + + try { + const results = await analyzePost(postText); + scanned++; + + // 套用 filter + const filtered = results.filter((r) => { + if (maxPrice && r.price && r.price > maxPrice) return false; + if (district && r.district && !r.district.includes(district)) return false; + return true; + }); + + if (filtered.length > 0) { + // 建立清單(如果還沒有) + if (!listId) { + const today = new Date().toLocaleDateString("zh-TW"); + listId = await createList(`租屋清單 ${today}`); + chrome.runtime.sendMessage({ action: "PROGRESS", scanned, added, scroll, listId }); + } + + await addToList(listId, filtered); + added += filtered.length; + } + + // 更新進度 + chrome.runtime.sendMessage({ action: "PROGRESS", scanned, added, scroll, listId }); + + // 稍微等一下避免 API rate limit + await new Promise((r) => setTimeout(r, 300)); + } catch (err) { + console.warn("[FB租屋] post error:", err); + } + } + + // 往下滑 + scroll++; + if (scroll <= maxScrolls) { + chrome.runtime.sendMessage({ action: "PROGRESS", scanned, added, scroll, listId }); + await scrollDown(); + } + } + + chrome.runtime.sendMessage({ action: "DONE", scanned, added, listId }); + } catch (err) { + chrome.runtime.sendMessage({ action: "ERROR", error: err.message }); + } finally { + isScraping = false; + } +} + +// 接收來自 popup 的訊息 +chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { + if (msg.action === "START_SCRAPE") { + sendResponse({ ok: true }); + startScrape(msg.config); + } + if (msg.action === "STOP_SCRAPE") { + stopRequested = true; + isScraping = false; + sendResponse({ ok: true }); + } + return true; // keep channel open +}); + +console.log("[FB租屋過濾器] content script 已載入"); diff --git a/tools/fb-rent-extension/icons/128.png b/tools/fb-rent-extension/icons/128.png new file mode 100644 index 0000000..cf1b8d5 Binary files /dev/null and b/tools/fb-rent-extension/icons/128.png differ diff --git a/tools/fb-rent-extension/icons/16.png b/tools/fb-rent-extension/icons/16.png new file mode 100644 index 0000000..42dc87f Binary files /dev/null and b/tools/fb-rent-extension/icons/16.png differ diff --git a/tools/fb-rent-extension/icons/48.png b/tools/fb-rent-extension/icons/48.png new file mode 100644 index 0000000..4f88680 Binary files /dev/null and b/tools/fb-rent-extension/icons/48.png differ diff --git a/tools/fb-rent-extension/manifest.json b/tools/fb-rent-extension/manifest.json new file mode 100644 index 0000000..256d894 --- /dev/null +++ b/tools/fb-rent-extension/manifest.json @@ -0,0 +1,37 @@ +{ + "manifest_version": 3, + "name": "FB 租屋過濾器", + "description": "自動掃描 Facebook 租屋社團貼文,整理到你的租屋清單", + "version": "1.0.0", + "icons": { + "16": "icons/16.png", + "48": "icons/48.png", + "128": "icons/128.png" + }, + "action": { + "default_popup": "popup.html", + "default_icon": { + "16": "icons/16.png", + "48": "icons/48.png", + "128": "icons/128.png" + } + }, + "content_scripts": [ + { + "matches": [ + "https://www.facebook.com/*" + ], + "js": ["content.js"], + "run_at": "document_idle" + } + ], + "permissions": [ + "storage", + "activeTab", + "scripting" + ], + "host_permissions": [ + "https://www.facebook.com/*", + "https://fb-rent-filter.cerana-mail.workers.dev/*" + ] +} diff --git a/tools/fb-rent-extension/popup.html b/tools/fb-rent-extension/popup.html new file mode 100644 index 0000000..9e2ce30 --- /dev/null +++ b/tools/fb-rent-extension/popup.html @@ -0,0 +1,209 @@ + + +
+ + +把社群租屋貼文整理成
一目了然的比較清單
分析後自動建立清單・可分享給朋友共同編輯
開啟朋友的清單