Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
a47911e
feat: add fb-rent-filter tool
Mar 14, 2026
27bafa0
feat: add Cloudflare D1 + Pages deployment
Mar 14, 2026
d7d5c42
refactor: migrate to Cloudflare Workers via @opennextjs/cloudflare
Mar 14, 2026
e9deed2
fix: fix build errors for opennextjs/cloudflare
Mar 14, 2026
14f9699
feat: UI redesign (impeccable) + gpt-5-mini + share fix
Mar 14, 2026
32b3b14
feat: D1-backed sharing + append to existing list
Mar 14, 2026
4ae6b82
fix: one post per analysis, remove multi-post splitting
Mar 14, 2026
acd16ca
feat: status tracking + notes + filter bar
Mar 14, 2026
76c5a45
feat: post cache + auto-create list + redirect flow
Mar 14, 2026
2c5e5a8
feat: chrome extension - FB rental post scraper
Mar 14, 2026
17c56d3
fix: schema - remove optional().nullable() for OpenAI structured output
Mar 14, 2026
e32115f
feat: skeleton loading states + dot pulse animation
Mar 14, 2026
d965b07
feat: IP rate limiting on /api/analyze + /api/lists
Mar 14, 2026
c209140
feat: PWA + install prompt + subsidy/parking fields + map page
Mar 14, 2026
1c68eca
feat: rename to 租多好室 + map fix + PWA icon + iOS standalone fix
Mar 14, 2026
8959362
feat: mobile tab bar + scroll-to-top + nav improvements
Mar 14, 2026
3faed78
chore: rename to 有室再說
Mar 14, 2026
d4bf355
feat: open list input + share target handler + Lucide icons + browser…
Mar 14, 2026
fd088bd
fix: map page - inline Leaflet CSS, fix height with flex:1, error han…
Mar 14, 2026
2c18587
redesign: centered hero layout (ChatGPT-style homepage)
Mar 14, 2026
75a257c
feat: map page - prev/next navigation + Lucide icons
Mar 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 將貼文轉成結構化租屋資料 |

## 結構

Expand All @@ -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
```

Expand Down
101 changes: 101 additions & 0 deletions tools/fb-rent-extension/PUBLISHING.md
Original file line number Diff line number Diff line change
@@ -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 社群訊息轉貼
- [ ] 付費解鎖:更多清單、自動定時掃描
211 changes: 211 additions & 0 deletions tools/fb-rent-extension/content.js
Original file line number Diff line number Diff line change
@@ -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 已載入");
Binary file added tools/fb-rent-extension/icons/128.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tools/fb-rent-extension/icons/16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tools/fb-rent-extension/icons/48.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
37 changes: 37 additions & 0 deletions tools/fb-rent-extension/manifest.json
Original file line number Diff line number Diff line change
@@ -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/*"
]
}
Loading