昨天講到抓不到內文時怎麼退化成只憑網址跟備註分類,今天要處理另一個更常發生的狀況:同一個網址被我重複傳進來,line-notion-bot 該怎麼辦?
會遇到這個情況的原因很單純,我常常忘記自己已經傳過某篇文章,尤其是那種在不同群組、不同聊天室看到好幾次的熱門連結。如果每次傳都無腦新增一筆,Notion 資料庫很快就會堆出一堆內容一模一樣、只有分類結果可能不同的重複頁面,時間久了根本沒辦法信任這個資料庫。
負責處理這件事的是 createNotionPage,重點在寫入之前先查一次:
async function createNotionPage(env, { title, category, tags, summary, url }) {
// 先查有沒有相同連結的既有頁面,避免重試或重傳造成重複資料
const existingId = await findPageByUrl(env, url);
const properties = {
[PROP.title]: { title: [{ text: { content: title.slice(0, 200) } }] },
[PROP.category]: { select: { name: category } },
[PROP.tags]: { multi_select: tags.map((t) => ({ name: t.slice(0, 50) })) },
[PROP.url]: { url },
[PROP.summary]: { rich_text: [{ text: { content: summary.slice(0, 1900) } }] },
};
if (existingId) {
const res = await fetch(`https://api.notion.com/v1/pages/${existingId}`, {
method: 'PATCH',
headers: { /* ... */ },
body: JSON.stringify({ properties }),
});
if (!res.ok) throw new Error(`Notion 更新失敗(${res.status})...`);
// 內容區塊也補上這次的摘要,當作更新紀錄(不動原本的區塊)
await appendNote(env, existingId, `(重新分類更新)${summary}`);
const page = await res.json();
return { url: page.url, updated: true };
}
// 沒查到既有頁面才新增
const res = await fetch('https://api.notion.com/v1/pages', {
method: 'POST',
headers: { /* ... */ },
body: JSON.stringify({
parent: { database_id: env.NOTION_DATABASE_ID },
properties,
children: [/* 段落 + bookmark */],
}),
});
const page = await res.json();
return { url: page.url, updated: false };
}
findPageByUrl 做的事很直接,拿「連結」這個 URL 型態的欄位去查資料庫,篩選條件用 equals 完全比對:
async function findPageByUrl(env, url) {
const res = await fetch(
`https://api.notion.com/v1/databases/${env.NOTION_DATABASE_ID}/query`,
{
method: 'POST',
headers: { /* ... */ },
body: JSON.stringify({
filter: { property: PROP.url, url: { equals: url } },
page_size: 1,
}),
},
);
if (!res.ok) return null;
const data = await res.json();
return data.results[0]?.id || null;
}
查到就回傳那筆頁面的 ID,查不到(或查詢本身失敗)就回傳 null,後面 createNotionPage 只認這個布林值:有 ID 就走 PATCH 更新既有頁面,沒有就走 POST 新增一筆。分類、標籤、摘要這幾個欄位不管新增還更新都是同一組 properties,差別只在往哪個 API 端點送、用哪種 HTTP method。
更新的時候有個小地方我還蠻在意的,就是 appendNote 那一行。更新頁面的 properties(分類、標籤、摘要)是直接覆蓋掉舊值,但頁面內文那塊沒有跟著清掉重寫,而是在下面補一段「(重新分類更新)」開頭的新段落。原因是分類這種結構化欄位本來就該以最新一次判斷為準,沒有保留舊值的理由;但內文是我在 Notion 裡手動加註記、標待辦累積出來的東西,這些都是自己另外投入的心力,被 AI 這次重新跑分類就整段清掉太可惜,所以選擇疊加而不是覆蓋。
回頭去看 handleTextMessage 怎麼用這個 updated 標記,就知道為什麼要特地回傳這個布林值:
const saved = await createNotionPage(env, { ...result, url });
const lines = [
saved.updated ? `🔄 ${result.title}(已更新既有筆記)` : `✅ ${result.title}`,
...
];
同一支函式回來,我在 LINE 上收到的訊息長得不一樣:新增是 ✅,更新是 🔄 並附註「已更新既有筆記」。這個區別不是裝飾用的,是真的在提醒我「這篇你傳過了」,讓我在滑手機的當下就能發現自己重複傳送,而不是等哪天翻 Notion 才納悶怎麼同一篇文章存了兩次還內容不一樣。
這套「用連結欄位查詢既有頁面」的邏輯,其實在專案裡出現了兩次。createNotionPage 用它來決定新增或更新,resolvePageId(處理「刪除 / 註記 / 分類 / 待辦」這些指令用的)也用幾乎一樣的查詢語法去找頁面,只是多了一個判斷:如果貼進來的是 Notion 頁面連結本身,直接從網址尾端抓 32 碼的十六進位當 page ID,不用查資料庫;只有原始文章網址才會真的打一次 API 查詢。這兩支函式沒有真的合併成一個,各自維護一份幾乎重複的查詢程式碼,好處是各自要加什麼判斷都不用擔心影響到對方,壞處就是哪天 PROP.url 這個欄位名稱要改,得記得兩邊一起改。這種程度的重複我還可以接受,換來的是兩處呼叫情境不同時,改起來互不牽連。
明天要來看 LINE 上那些「搜尋、刪除、註記、標待辦」的指令,怎麼分工:哪部分交給簡單的規則判斷就好,哪部分才真的用得到 AI。