教學-我用Gemini Vibe Coding做出工作流,我把NoteBookLM串接到了OneNote

健行科技大學 資訊工程系
以下簡稱 : NoteBookLM = LM,OneNote=One
以下教學如果有任何問題,歡迎留言討論。或是貼到gemini去解析也可!!
之前一直在思考NoteBookLM可以高效的使用?
因為他被限制在只能固定的資料源下去幫你做 AI Agent
好處是答案不會偏離太多
壞處是沒辦法經常性的新增資料,因為LM有資料來源數量限制
而且手機板的LM 目前為止沒辦法上傳pdf以外的檔案類型,要使用網頁板才可以!
但我一個檔案塞不滿他單一檔案限制的極限
可是我卻有數百個檔案要上傳或是持續更新,而LM免費帳戶只能放50個來源而已
所以我在看LM可用的資料來源中有google doc,想到可以把one融合到google 文件的doc
這樣就可以隨時把我學的知識筆記放入one,然後用LM幫我做整合
--至於為什麼選擇one?
我其實有samsung note 也用過google keep、evernote、Notin
但是我並持著免費能用就好XD,用來用去覺得one在同步資料以及跨平台作業上從免費的角度來看非常夠用!!! 並且微軟生態齊全,所以API也沒問題,免費用戶也不會被限制使用額度
我不是要貶低其他筆記本,各有所好,只是我自己體感就是one免費又大碗!!!
我當時的理想效果如下 :
我的工作內容有些不常用到但不能忘記的技術以及個人興趣研究的東西,還其他人生需要規劃的東西太多了,要記住太多東西了,所以我常常用手機做筆記,把看到的東西歸類在one的"節"裡面,然後當我突然想起很久遠的事情時想要找關鍵字會不好搜尋,希望可以有AI幫忙,所以我就到LM裡面輸入口語化的問題,LM會幫我在筆記中找到我要的答案
或是說像我有規劃買房子,要做裝潢買家具,還有很多預算,所以我常常在D卡上刷文章,看到不錯的段落會收藏在one,這樣我後續也方便LM幫我做整合!!
-----------------------------------------
因此我就有這樣的想法出現!!!
不過我搜尋了一輪發現 google 沒有直接跟微軟整合 XD
所以我就問gemini要怎麼開發,他一開始還跟我說難度有點高 哈!!
總之被我搞出來了!!
我先說缺點 :
1.目前沒有辦法把one的圖片放到google doc,全部的筆記只能載入純文字
不過我覺得這部分還好,因為LM只是輔助你思考,他會附上來源,你可以根據來源去找你手上的筆記本圖片
2.我自己是沒有實體伺服器,所以掛在託管網站,我是有花錢買託管網站,因為我用途不止這個,所以加減用,如果你們要用的話,但不想花錢可能要另外想辦法處理了!
3.第一次執行的時候會比較久,像我有27個節,大概有150個頁面的筆記,所以剛做好程式時要先手動運行3次,把資料都載入,大該花了20分鐘,後面系統自動更新就比較快
大概運行主程式2分鐘就可以把你新增在one的筆記放在doc了!
希望未來官方可以自己整合好 XD
-------------------教學開始---------------------
以下教學由AI 潤飾 :
這是一個非常棒的想法!把我們這段時間的「踩坑」與「優化」過程整理成教學,絕對能造福很多想用 NotebookLM 但苦於 OneNote 無法同步的人。
這份教學手冊我會設定為**「零基礎也能懂」**的風格,並特別標註我們遇到的技術卡關點(Hostinger 限制、Azure 帳戶類型錯誤)。
以下是為您準備的完整教學手冊草稿,您可以直接複製去論壇排版發布。
[教學] 打造 OneNote 自動同步 Google Docs 神器:讓 NotebookLM 讀懂你的筆記 (Hostinger/Python 篇)
你是否也想用 Google 的 NotebookLM 來分析你的筆記,卻發現它不支援 Microsoft OneNote?
這篇文章將教你搭建一個自動化系統,將 OneNote 的內容「鏡像同步」到 Google Docs,讓 AI 能隨時讀取你最新的筆記知識庫。
🎯 這套系統的特點
全自動同步:支援新增、修改、刪除(OneNote 刪掉,Doc 也會刪掉)。
智慧快取:只有變動的頁面才會更新,大幅節省執行時間與 API 額度。
零依賴環境:專為**共享主機(如 Hostinger)**設計,不需安裝 pip 套件,純 Python 標準庫實作。
極致壓縮:自動清理 OneNote 的 HTML 格式,轉為適合 AI 閱讀的純文字排版。
🛠️ 準備工作
一個 Microsoft 帳號(有 OneNote 筆記)。
一個 Google 帳號(使用 Google Docs 與 Apps Script)。
一個可執行 Python 的環境(本教學以 Hostinger 虛擬主機為例,但 VPS 或本地樹莓派也適用)。
第一步:申請 Microsoft Graph API (最容易踩坑的地方!)
要讓程式讀取 OneNote,必須向 Azure 申請權限。這裡有一個我們曾踩過的巨大坑,請務必注意帳戶類型。
登入 Azure Portal。
搜尋並進入 「App registrations (應用程式註冊)」。
點擊 「New registration (新註冊)」。
📝 設定名稱:例如 OneNoteSync。
⚠️ 支援的帳戶類型 (關鍵點):
千萬不要選「Accounts in this organizational directory only (僅限此組織目錄中的帳戶)」。這會導致你的個人 Microsoft 帳號無法登入,報錯 403。
請選擇:「Accounts in any organizational directory (Any Azure AD directory - Multitenant) and personal Microsoft accounts (e.g. Skype, Xbox)」。
(簡單說:要選包含「個人帳戶」的選項)
Redirect URI:選擇 Web,網址填入
http://localhost:8080
(方便我們在本地獲取第一次權限)。
註冊後,記下 Application (client) ID (這就是 Client ID)。
設定密鑰與權限
左側選單點 「Certificates & secrets」 -> 「New client secret」 -> 記下 Value (這就是 Client Secret),離開頁面就看不到了。
左側選單點 「API permissions」 -> 「Add a permission」 -> 「Microsoft Graph」 -> 「Delegated permissions」。
搜尋並勾選以下權限:
Notes.Read (讀取筆記)
Notes.Read.All (讀取所有筆記)
offline_access (重要:這樣程式才能在背景一直自動更新,不用你天天登入)。
第二步:設定 Google Apps Script (接收端)
我們需要一個「接收器」,把 Python 抓到的資料寫入 Google Doc。
建立一個新的 Google Doc,記下網址中的 Doc ID (例如 .../d/你的ID在這裡/edit...)。
前往 Google Apps Script,建立新專案。
貼上以下程式碼:
JavaScript:
function doPost(e) {
// 請填入你的 Google Doc ID
const TARGET_DOC_ID = 'YOUR_GOOGLE_DOC_ID';
try {
const data = JSON.parse(e.postData.contents);
const doc = DocumentApp.openById(TARGET_DOC_ID);
const body = doc.getBody();
// 支援批次同步
if (data.pages && Array.isArray(data.pages)) {
body.clear(); // 鏡像同步:先清空舊內容
data.pages.forEach(p => {
// 標題排版 (Heading 4)
const header = body.appendParagraph('📌 [' + p.section + '] ' + p.title);
header.setHeading(DocumentApp.ParagraphHeading.HEADING4)
.setSpacingBefore(12)
.setSpacingAfter(0);
// 內容排版:壓縮間距
const cleanText = p.noteContent ? p.noteContent.trim() : "";
if (cleanText) {
body.appendParagraph(cleanText).setSpacingBefore(0).setSpacingAfter(6);
}
});
return ContentService.createTextOutput("Batch Success");
}
return ContentService.createTextOutput("No Data");
} catch (err) {
return ContentService.createTextOutput("Error: " + err.message);
}
}
點擊 「部署 (Deploy)」 -> 「新增部署」。
類型選「網頁應用程式」。
⚠️ 執行身分:我 (Me)。
⚠️ 誰可以存取:所有人 (Anyone) (這很重要,否則你的 Python 腳本會被 Google 擋在門外)。
部署後,記下 Web App URL (這就是 GAS_URL)。
第三步:Python 同步核心 (Hostinger 特別版)
🚧 技術難點:為什麼不用 msal 庫?
一般 Python 教學會叫你用 pip install msal 來處理微軟登入。但在 Hostinger Shared Hosting (共享主機) 環境下,我們通常沒有權限安裝額外的 Python 套件。
解決方案:我們改用 Python 內建的 urllib 和 json 標準庫,手刻 HTTP 請求。這讓程式碼可以在任何只要有 Python 的地方執行,完全零依賴!
📂 檔案結構
建議在本地先整理好,再上傳到伺服器。
Plaintext
onenotepython/
├── .env (存放密鑰,隱藏檔)
├── token.json (存放登入權限)
├── last_sync_id.json (存放指紋,自動生成)
└── sync.py (主程式)
1. 建立 .env 檔案
Ini, TOML
MS_CLIENT_ID=你的_Azure_Client_ID
MS_CLIENT_SECRET=你的_Azure_Client_Secret
GAS_WEB_URL=你的_GAS_Web_App_URL
2. 獲取初始 token.json
因為伺服器沒有瀏覽器,你第一次必須在本地電腦生成 token。
你可以使用簡單的腳本獲取 refresh_token,或是透過 Postman 獲取。
將獲取到的 JSON 存為 token.json,內容格式如下:
JSON
{
"refresh_token": "你的_初始_Refresh_Token",
"access_token": ""
}
(只要有 refresh_token,程式就會自動換取新的 access_token)
3. sync.py 主程式 (完整優化版)
此版本包含了我們所有的優化:400 錯誤修復、分節群組支援、本地快取加速。
(在此處貼上我們最後確認的那個包含 load_env 絕對路徑、get_access_token 自動更新、以及 main 函數中有快取邏輯的完整 Python 代碼)
sync.py完整代碼 :
import json, urllib.request, urllib.parse, urllib.error, os, time, re
def load_env():
"""修正:使用絕對路徑定位 .env,防止在 Cron Job 環境中執行路徑偏移"""
base_dir = os.path.dirname(os.path.abspath(__file__))
env_path = os.path.join(base_dir, '.env')
if os.path.exists(env_path):
with open(env_path, 'r', encoding='utf-8') as f:
for line in f:
if '=' in line:
k, v = line.strip().split('=', 1)
os.environ[k.strip()] = v.strip()
load_env()
# 配置區:確保讀取絕對路徑
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
CLIENT_ID = os.getenv('MS_CLIENT_ID', '').strip()
CLIENT_SECRET = os.getenv('MS_CLIENT_SECRET', '').strip()
GAS_URL = os.getenv('GAS_WEB_URL', '').strip()
TOKEN_FILE = os.path.join(BASE_DIR, 'token.json')
CACHE_FILE = os.path.join(BASE_DIR, 'sync_cache.json')
def get_access_token():
"""自動使用 refresh_token 換取 access_token"""
with open(TOKEN_FILE, 'r') as f: tk = json.load(f)
params = {
'client_id': CLIENT_ID, 'client_secret': CLIENT_SECRET,
'grant_type': 'refresh_token', 'refresh_token': tk['refresh_token'],
'scope': 'Notes.Read offline_access'
}
data = urllib.parse.urlencode(params).encode()
req = urllib.request.Request("", data=data)
with urllib.request.urlopen(req) as res:
result = json.loads(res.read().decode())
tk['refresh_token'] = result.get('refresh_token', tk['refresh_token'])
with open(TOKEN_FILE, 'w') as f: json.dump(tk, f)
os.chmod(TOKEN_FILE, 0o600) # 保護金鑰檔案權限
return result['access_token']
def main():
print(f"[{time.strftime('%H:%M:%S')}] 啟動增量鏡像同步掃描...")
try:
token = get_access_token()
# 1. 抓取所有分節 (解決分節過多導致的全域查詢 400 錯誤)
sec_req = urllib.request.Request("")
sec_req.add_header('Authorization', f'Bearer {token}')
with urllib.request.urlopen(sec_req) as res:
sections = json.loads(res.read().decode()).get('value', [])
# 2. 讀取本地快取
try:
with open(CACHE_FILE, 'r', encoding='utf-8') as f: cache = json.load(f)
except: cache = {}
all_pages_data = []
new_cache = {}
has_real_change = False
# 3. 逐節掃描 (支援分節群組內的內容)
for sec in sections:
sid, sname = sec['id'], sec['displayName']
# 精確編碼 URL 參數,防止微軟 API 報錯
query = urllib.parse.urlencode({"$orderby": "lastModifiedDateTime desc", "$top": "10"})
page_api = f"{sid}/pages?{query}"
p_req = urllib.request.Request(page_api)
p_req.add_header('Authorization', f'Bearer {token}')
try:
with urllib.request.urlopen(p_req) as p_res:
pages = json.loads(p_res.read().decode()).get('value', [])
for p in pages:
pid, mtime = p['id'], p['lastModifiedDateTime']
# 4. 快取比對:時間戳一致則跳過內容抓取
if pid in cache and cache[pid]['time'] == mtime:
p_data = cache[pid]
else:
print(f" 📥 內容更新: {p['title']}")
c_res = urllib.request.urlopen(urllib.request.Request(p['contentUrl'], headers={'Authorization': f'Bearer {token}'}))
# 移除 HTML 標籤並壓縮格式
clean = re.sub(r'<[^>]*>', ' ', c_res.read().decode())
clean = re.sub(r'\s+', ' ', clean).strip()[:8000]
p_data = {"time": mtime, "text": clean, "title": p['title'], "section": sname}
has_real_change = True
new_cache[pid] = p_data
all_pages_data.append(p_data)
except: continue
# 5. 只有真正異動時才推送給 Google (避免浪費執行資源)
if has_real_change or len(new_cache) != len(cache):
print(f"🔄 偵測到異動(含刪除),推送批次數據至 Google Doc...")
batch = [{"noteContent": p['text'], "title": p['title'], "section": p['section']} for p in all_pages_data]
payload = json.dumps({"pages": batch}, ensure_ascii=False).encode('utf-8')
p_req = urllib.request.Request(GAS_URL, data=payload, headers={'Content-Type':'application/json'})
with urllib.request.urlopen(p_req) as p_res:
print(f"✅ GAS 同步結果: {p_res.read().decode()}")
# 更新快取
with open(CACHE_FILE, 'w', encoding='utf-8') as f:
json.dump(new_cache, f, ensure_ascii=False)
os.chmod(CACHE_FILE, 0o600)
else:
print("⏳ 內容無異動,系統跳過。")
except Exception as e:
print(f"💥 程式異常結束: {e}")
if __name__ == "__main__":
main()
第四步:部署到 Hostinger (安全性設定)
這是很多人會忽略的一步。為了保護你的 token.json 和 .env 不被駭客下載,絕對不要把程式放在 public_html 裡面。
上傳檔案:使用 File Manager 或 SSH,將 onenotepython 資料夾上傳到與 public_html 同級的位置。
❌ 錯誤路徑:/home/u123/domains/xxx.com/public_html/onenotepython
✅ 正確路徑:/home/u123/domains/xxx.com/onenotepython
設定權限:
在 SSH 執行以下指令,確保只有你自己能讀寫這些檔案:
Bash:
chmod 700 ~/domains/你的域名/onenotepython
chmod 600 ~/domains/你的域名/onenotepython/token.json
chmod 600 ~/domains/你的域名/onenotepython/.env
第五步:設定自動化排程 (Cron Job)
讓它每 15 分鐘自動跑一次。
進入 Hostinger 控制台 -> Advanced -> Cron Jobs。
Command (請務必使用絕對路徑):
Bash :
python3 /home/uXXXXX/domains/你的域名/onenotepython/sync.py > /home/uXXXXX/domains/你的域名/onenotepython/sync.log 2>&1
(將 uXXXXX 和 你的域名 換成你實際的路徑)
Schedule:設定為 */15 * * * * (每 15 分鐘)。
💡 常見問題與除錯 (FAQ)
Q: 為什麼程式跑了很久 (例如 7 分鐘)?
A: 第一次執行時,程式需要下載所有筆記並寫入 Google Doc,這叫「全量同步」。第二次開始,程式會比對「本地快取」,只有變動的頁面才會更新,通常只需幾秒鐘。
Q: 出現 HTTP Error 400: Bad Request?
A: 這通常是 URL 編碼問題。Microsoft Graph API 對網址中的空格(例如 $orderby=...)很敏感。我們的代碼中已經使用了 urllib.parse.urlencode 來完美解決這個問題。
Q: 分節群組 (Section Groups) 裡的筆記抓不到?
A: 舊版教學常使用 /sections/{id}/pages,這會漏掉群組內的筆記。我們改用了「先抓所有 Sections (含群組內)」再「逐節掃描」的策略,確保無死角。
Q: Hostinger 執行報錯 No such file or directory?
A: 請檢查你的 Python 腳本是否使用了 os.path.abspath(__file__) 來定位檔案。在 Cron Job 環境中,相對路徑(如直接讀取 token.json)通常會失效,必須用絕對路徑。
希望這篇教學能幫助大家解放 OneNote 的筆記
讓 NotebookLM 成為你的第二大腦!
如果有任何問題,歡迎留言討論。
