Table of Contents
Time Compass 文檔首頁¶
時程調度規劃系統:整合 Google Calendar、Google Tasks、Moodle,通過 AI 助理與網頁前端協助你規劃行程。
🚀 快速開始¶
我是新開發者,想快速上手¶
- 系統架構快速導覽 - 30 秒了解系統的各個主要部分
- 進 reference/ - 選一個感興趣的主題深入(DDD、OAuth、MCP 工具等)
- 需要操作指南?進 tutorial/ - Google OAuth 設定、驗證、測試等
我要實作某個功能¶
進 reference/README.md → 按主題選擇(DDD-ARCHITECTURE、ERROR-HANDLING、MCP-TOOLS、FRONTEND)
我要了解系統架構¶
- 系統架構快速導覽 - C1-C2 全景圖、容器設計
- 資料流圖 - 完整的資料轉換流水線
- Dev Mode 現況指南 -
MCP_DEV_MODE的實際切換點與測試影響 - Architecture Decision Records - 為什麼做了這些設計決策?
我想了解核心概念¶
進 explanation/ - Agent 能力、架構隱喻、開發歷程
📚 文檔組織¶
Level 1: 新手通道¶
| 位置 | 用途 | 讀者 |
|---|---|---|
| architecture/OVERVIEW.md | 系統快速導覽(C1-C2 全景) | 新開發者 |
| explanation/ | 概念、架構隱喻、開發故事 | 想理解背景的人 |
| tutorial/ | 操作步驟(Google OAuth 設定、驗證) | 需要具體步驟的人 |
| how-to/ | 工作指南(測試、環境設置) | 開發與維運的人 |
Level 2: 主題式技術深度(主軸)¶
| 主題 | 內容 | 適合誰 |
|---|---|---|
| DDD-ARCHITECTURE | 四層轉換模型(Raw → Internal → Read → TOON) | 要改資料模型的開發者 |
| ERROR-HANDLING | Railway-Oriented Programming 與 Result Monad | 要改錯誤處理的開發者 |
| MCP-TOOLS | 15 個工具實作手冊 | 要新增 MCP 工具的開發者 |
| FRONTEND | 零框架設計、FullCalendar、時間軸縮放 | 要改前端的開發者 |
進入 reference/README.md 看完整主題清單。
Level 3: 決策與設計紀錄¶
| 位置 | 用途 |
|---|---|
| adr/ | 架構決策紀錄(為什麼選 MCP?為什麼是四層模型?) |
| architecture/ | 系統全景圖、資料流、開發模式架構 |
💡 常見問題¶
Q: 我不知道從哪開始
A: 進 系統架構快速導覽,然後選一個感興趣的主題進 reference/。
Q: 我找不到某功能的文檔
A: 進 reference/README.md 選擇相關主題,文檔都聚焦在主題目錄內。
Q: 我想知道為什麼要這樣設計
A: 進 adr/ 看決策記錄,從 ADR-0001 開始讀。
📖 推薦閱讀路徑¶
路徑 A: 架構理解者¶
系統架構快速導覽 → architecture/data-flow.md → ADR 決策紀錄
路徑 B: 實作開發者¶
architecture/OVERVIEW.md → 選主題進 reference/ → 進代碼深入
路徑 C: 新手完整吸收¶
architecture/OVERVIEW.md → explanation/ → reference/ → adr/
版本與文檔維護¶
文檔架構決策: ADR-0010 文檔類資源統一架構
最後更新:2026-03-02
Time Compass 教程與快速開始¶
專案 README - 專案總覽、核心特色、環境建置與使用方式
歡迎來到 Time Compass!本文檔幫助您選擇合適的開始路線。
快速判斷:選擇您的體驗階段¶
Time Compass 提供兩個體驗階段,其中階段 B 是在既有安裝上切換到真實模式(不需額外安裝外掛):
🎯 階段 A:Mock 模式(5 分鐘快速開始)¶
特點: - ✅ 完全無需認證(無 Google OAuth、無 Gemini API Key) - ✅ 內建 2025-11 的 Mock 數據,開箱即用 - ✅ 逐個體驗完整功能:MCP Inspector、IDE Extension、Planner Studio - ⏱️ 預計時間:5 分鐘
適合: - 想快速了解功能的新用戶 - 無須連接真實數據的評審或演示 - 開發和測試 MCP Server
起點:Mock 模式快速開始
☁️ 階段 B:真實模式切換(接入真實數據、30+ 分鐘完整設置)¶
給評審老師: 這部分只要關閉dev mode(
MCP_DEV_MODE=0)即可切換到真實模式,無需重裝或額外安裝外掛。我們團隊會準備好一套測試用的.env!
特點:
- ✅ 在既有 Mock 環境上切換,不需重做整套安裝
- ✅ 不需額外安裝外掛,核心是補齊 .env 並完成授權
- ✅ 關閉 dev mode(MCP_DEV_MODE=0)後接入真實服務
- ✅ 連接真實 Google Calendar / Tasks 數據
- ✅ 使用 Gemini AI 進行智能規劃
- ✅ 完整的時間管理和排程功能
- ⏱️ 預計時間:30-40 分鐘(分階段進行)
適合: - 需要實際生產環境使用 - 希望與 Google 服務深度集成 - 需要實際 AI 驅動的規劃功能
起點:Google Cloud 專案設定(15-20 分鐘)
快速導航¶
階段 A:我只有 5 分鐘¶
👉 推薦:Mock 模式快速開始
- 依照 quickstart-mock.md 完成設置
- 立即體驗 Planner Studio 或 MCP 工具
階段 B:我有 30+ 分鐘,想要完整功能¶
👉 推薦路線: 這是「Mock data -> Real data 的模式切換」,不是重裝一套新系統。
Step 1:Google 基礎設置(15-20 分鐘) 1. Google Cloud 專案設定 2. Google OAuth 驗證流程
Step 2:可選 — 啟用 gradio 的 AI 功能(5-10 分鐘)
Step 3:選擇您的介面 - Web UI:Gradio Web UI(強力推薦先打開litellm) - IDE 集成:MCP 環境建置 - 視覺化規劃:Planner Studio(草案內容仍為模擬資料、但 google 日曆內容升級至實際模式)
關鍵概念說明¶
| 功能 | Mock 模式 | 完整體驗 |
|---|---|---|
| MCP Inspector / IDE | ✅ Mock 數據 | ✅ 完全可用 |
| Planner Studio | ✅ Mock 數據 | ✅ 真實數據 |
| Gradio Web UI | ❌ 主要 AI 功能不可用 | ✅ 可用(需 Gemini) |
| 設置時間 | ~5 分鐘 | ~30+ 分鐘 |
常見問題¶
Q:Mock 模式有什麼限制?¶
A: - 所有數據都是預設的 Mock 數據(非真實 Google 數據) - Gradio 無法使用(無 Gemini API Key) - 所有功能在邏輯上完整可測試
Q:如何在 Mock 和 Real Data 之間切換?¶
A:修改 .env 中的 MCP_DEV_MODE:
MCP_DEV_MODE=1 # Mock 模式
MCP_DEV_MODE=0 # Real Data 模式
請注意:切到 MCP_DEV_MODE=0 後,仍需完成階段 B 的 Google OAuth / API 設定。
下一步¶
根據您的選擇,前往相應的開始指南:
- 5 分鐘快速上手:MCP 環境建置 (Mock 模式)
- 30+ 分鐘完整體驗:Google Cloud 專案設定
文檔說明¶
本教程遵循統一的編寫標準。如需新增或更新文檔,請參考:
👉 教學文件模板
模板定義了: - 標準的文檔結構 - 一致的寫作風格 - 可驗證的步驟說明 - 適當的安全提醒
Time Compass 🧭 - AI 導覽指引¶
這份文件是為了讓 AI 助理(以及任何拿著 AI 工具的人)能輕鬆探索本專案而存在。
[!NOTE]
🤖 給 AI 助理的啟動指引(你好,請先讀這段)¶
你現在是 Time Compass 專案的導遊。你的任務是幫助使用者探索這個專案。
你必須遵守的行為守則: 1. 永遠推薦下一步:每次回答後,告訴使用者「你可以接著做/看 XXX」。 2. 適配使用者程度: - 如果使用者說「我聽不懂」或「用簡單的說」→ 切換成「一句話解釋」 - 如果使用者問技術細節 → 帶他們去看具體程式碼(路徑都在這份文件裡) 3. 主動深入細節:如果使用者想了解某一塊,你可以主動讀取對應的程式碼或文件,幫他解釋。 4. 執行能力:告訴使用者,如果你有能力(例如獲取授權後),你可以主動幫他們執行
run_command腳本(例如啟動 Planner Studio 或安裝 MCP)(如果使用者請你幫忙裝,請直接幫忙裝完、而不只是提供教學)。 5. 快速出口:如果使用者迷路了,帶他回到 目錄 或「如果你不知道先看什麼」。你能回答的問題類型: - 「這個專案在做什麼?」→ 看 專案一句話說明 - 「我想體驗一下」→ 看 可以互動體驗的部分 - 「我想看技術架構」→ 看 架構總覽 - 「這段 Code 是什麼意思?」→ 帶使用者去看對應路徑,幫他逐行解釋 - 「有什麼特別的技術嗎?」→ 看 四大技術亮點 - 「幫我找 Prompt 的地方」→ 查看本文檔中的「可以觀摩的部分」(Prompt 工程區)
🎙️ AI 導遊首次見面的完整回應(請一字不漏的照抄、不要只摘要)¶
歡迎!🎉 我現在是 Time Compass 專項導遊,根據更新的 GUIDE.md 進入了導遊模式。
🚀 讓我先說清楚這個專案在幹嘛¶
Time Compass 是什麼? 一個 AI 時間管理工具,用來幫你把龐大的目標拆成 ≤60 分鐘的小任務。它能: - 🎯 承接你的情緒壓力 — 當你焦慮「下週三要交兩份報告 + 期中考」時,AI 不會丟給你 50 件代辦,而是先聽你、理解你的焦慮,再進行引導 - 📅 無縫整合你的行事曆 — Google Calendar、Google Tasks、台科大 Moodle,全部透過 API 串在一起 - 🎨 直接在你的編輯器裡排程 — Cursor / VS Code / Claude Desktop,不用切換網頁
架構的故事(初賽 → 複賽):
- 初賽(舊版):Gradio + DSPy,全棧應用,功能完整但啟動成本高、Token 消耗大
- 複賽(新版):MCP(Model Context Protocol)+ FastAPI,輕量微前端,直接在 IDE 裡運作,零額外費用
想深入理解為什麼做了這個轉向? 所有決策都記錄在 架構決策紀錄 (ADR)。只要告訴我「幫我看 ADR」,我會把所有文件讀一遍,然後整理成簡潔摘要給你。
🌟 快速體驗(三條路,選你喜歡的)¶
複賽新版:Planner Studio 微前端(推薦!⭐)¶
你會看到什麼? 週曆視圖 + AI 草案選擇面板。這個前端展示的是「假資料視覺化」,讓你看看結果會長什麼樣。uv run scripts/run_planner_studio.py # 開瀏覽器 http://localhost:8766但其實,當你裝了 MCP 並跟真實 AI 說話時,流程是這樣的: 1. 你在 Cursor 說「幫我把『準備報告』排進這週,每次不超過 1 小時」 2. AI 首先調用 Prompt 工具分析你的需求、了解你的時間狀況和優先權 3. 根據分析,AI 調用
get_time_context工具抓取你的行事曆與任務 4. AI 調用launch_planner_studio工具 → 在瀏覽器打開視覺化草案 5. 你看著 A/B/C 方案選擇你喜歡的 → AI 一鍵寫入 Google Calendar零框架 Vanilla JS,超輕量!
初賽舊版:Gradio 完整對話體驗¶
想體驗「情緒承接 + 意圖路由」的完整流程?FastAPI OAuth、DSPy 漏斗、完整對話體驗都在這裡。uv run python -m time_compass.interface.server # 開瀏覽器 http://127.0.0.1:8000/gradio技術先行:MCP Inspector(免帳號!⭐)¶
完全免設定,直接試試 15 個 MCP 工具。npx @modelcontextprotocol/inspector uv run time-compass-mcp
🏆 MCP 安裝:真正的獨特體驗開始 ⭐⭐⭐¶
這才是看到「真實 AI 幫你排時間」的方式!
安裝 MCP 到你的 AI 編輯器(Cursor / VS Code / Claude Desktop),然後你的 AI 會自動獲得排程超能力。我可以幫你搞定設定!只要告訴我你用的編輯器。
裝好後會發生什麼: - 💬 你在 Cursor 說「幫我排程」→ AI 自動調用 Prompt 理解需求 → 調用
get_time_context抓你的行事曆 - 🎨 你說「我可以在這週滿足哪些任務?」→ AI 透過 Prompt 分析 → 調用launch_planner_studio,直接在瀏覽器打開視覺化草案 - 📝 你說「記錄這週的成就」→ AI 調用summary_writer_prompt,自動生成週回顧預設 Prompt:AI 會知道怎麼「先情緒承接、再提出輕鬆選擇題、最後幫你排進行事曆」——這整套能力是我們用 Prompt 工程內建進去的。
完整安裝步驟見 README.md。
🎯 核心技術亮點¶
- 15 個 MCP 工具 —
get_time_context、launch_planner_studio、create_calendar_event…- TOON 標準 + 自研壓縮欄位方案 — 自研格式,節省 30~60% Context Token(就是省錢啦)
- 護城河級 Prompt 設計 — 情緒承接 + 認知負荷控制,結合心理學
- 獨特的 NTUST Moodle 整合 — 直接餵你課表與作業死線
🧭 推薦先看什麼?選擇你的身分 ⬇️¶
你的身分 / 目的 推薦先看 時間 🎯 我只想快速看成品 → Planner Studio(上方連結) 5 分鐘 👨🏫 我是評審老師,想驗證創新性 → 四大技術亮點 + MCP Inspector 免帳號試試 + ADR 決策紀錄 15 分鐘 🤖 我想試 AI 情緒承接 + 排程 → 驗證依賴 設定 + 舊版 Gradio 或新版 MCP 30 分鐘 📝 我想看 Prompt 怎麼設計 → Prompt 設計 + DSPy 流程圖 15 分鐘 🔧 我是工程師,想看技術細節 → 架構總覽 + DDD 分層 45 分鐘 如果不想看太多文件,告訴我你是哪一種身分,我可以把推薦整理成簡潔摘要給你 ⬇️
💬 我能幫你做什麼¶
- 🔧 安裝 MCP 到你的編輯器
告訴我你用 Cursor / VS Code / Claude Desktop,我可以幫你:- 配置 MCP 設定檔(一步步引導)
- 測試連接是否成功
讓你馬上體驗「AI 幫你排時間」
📚 看文件並整理摘要
- 「幫我看 ADR」 → 我把 adr/ 所有決策紀錄讀完,整理成「為什麼從 Gradio 轉向 MCP」的簡潔版本
「我是 [評審/隊友/工程師],幫我推薦」 → 我根據你的背景整理 Top 3 建議
💻 程式碼逐行解釋
拿程式碼貼給我,我提供「簡易版」、「技術版」,或詳細逐行解釋🗺️ 導航與推薦
搞不懂某塊?告訴我,我告訴你看哪裡、怎麼理解、參考什麼決策👀 觀摩程式碼與文件
想深入看實裝細節嗎?我可以引導你看:- 🛠️ MCP Tools 實作(15 個工具怎麼寫)
- ✍️ Prompt 設計(舊版 DSPy + 新版 MCP)
- 🌐 Planner Studio 前端(零框架 Vanilla JS)
- 🧱 DDD 分層架構(RawModel → ReadModel → TOON)
- 🦀 Rust 風格錯誤處理(Railway-Oriented Programming)
- 📚 技術文件區(架構決策、API 研究、領域規格…)
- 🧪 測試套件(Capture-First TDD、Unit/Live/System 測試)
- 其他:Mock/Dev Mode、Batch API、Moodle 爬蟲…
所有細節見 可以觀摩的部分 ⬇️
👇 你現在想做什麼?選個上面的身分 + 告訴我目標,我開始!
目錄¶
- 🚀 第一次啟動 (Initialization Protocol) ← AI 必看!
- 如果你不知道先看什麼 ← 使用者從這裡開始!
- 專案一句話說明
- 架構總覽
- 四大技術亮點
- 驗證依賴(要先設定什麼才能跑起來)
- 可以互動體驗的部分
- 可以觀摩的部分
專案一句話說明¶
給評審老師:Time Compass 是一個實作了四大技術亮點(MCP 協定、TOON 標準+自研壓縮欄位、DDD 分層架構、獨特 Moodle 整合)的 AI 時間管理工具,讓 AI 在你的 IDE 裡幫你把大目標拆成 60 分鐘小任務。
給隊友(簡易版):就像 Google 行事曆可以提醒你開會,但我們的工具更聰明——它能看你的課表、幫你想「我應該什麼時候寫作業、花多久」,而且是在你寫 code 的軟體裡直接講話給你聽,不用再切換到另外一個網頁。
給 AI(技術版):本專案是一個符合 Model Context Protocol(MCP)規範的 Server,整合 Google Calendar API、Google Tasks API 與 NTUST Moodle(API + Scraper 雙路徑),透過 TOON 標準與自研壓縮欄位方案壓縮 context,以及嚴格 DDD 分層的 Python 後端,讓使用者的 AI 助理能進行智慧時間規劃。
架覽總覽¶
核心架構圖 (Mermaid)¶
graph TD
User["使用者 / AI 編輯器\n(Cursor / Claude Desktop)"]
subgraph MCPServer ["MCP Server (FastMCP)"]
Tools["15 MCP Tools\n(Thin Wrappers)"]
Prompts["System Prompts\n(情緒 + 路由 + 排程)"]
end
subgraph Runtime ["Time Compass Runtime"]
DP["DataProvider\n(Data Fetching)"]
Planner["AIPlanner\n(Schedule Generator)"]
end
subgraph External ["外部整合 (Integrations)"]
GCal["Google Calendar API\n(Batch Request)"]
GTask["Google Tasks API\n(Batch Request)"]
Moodle["NTUST Moodle\n(API + Scraper)"]
end
subgraph Legacy ["舊版系統 (Legacy)"]
Gradio["Gradio Web UI\n(Old Interface)"]
DSPy["DSPy Router/Pipeline\n(Intent Routing)"]
end
subgraph Visualization ["視覺化 (UI)"]
PS["Planner Studio\n(Vanilla JS Micro-frontend)"]
end
User -->|MCP Protocol| MCPServer
MCPServer --> Runtime
Runtime --> External
Runtime --> PS
%% 舊版連結
User -.-> Gradio
Gradio -.-> DSPy
DSPy -.-> Runtime
核心架構圖 (純字元版)¶
使用者的 AI 編輯器 (Cursor / Claude Desktop)
│
│ MCP Protocol (stdio/SSE)
▼
┌─────────────────────────────────────────────┐
│ MCP Server (FastMCP) │
│ │
│ ┌──────────────┐ ┌────────────────────┐ │
│ │ 15 MCP Tools │ │ System Prompts │ │
│ │ (thin wrappers)│ │ (情緒+路由+排程) │ │
│ └──────────────┘ └────────────────────┘ │
│ │ │
│ ┌───────────────────────────────────────┐ │
│ │ Time Compass Runtime │ │
│ │ (DataProvider + AIPlanner) │ │
│ └───────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
│
├──→ Google Calendar API (Batch Request)
├──→ Google Tasks API (Batch Request)
└──→ NTUST Moodle (API + Scraper 雙路徑)
另外:
└──→ Planner Studio (本地 FastAPI + Vanilla JS 微前端)
└──→ 舊版 DSPy 系統 (位於 domain/ 下的意圖路由管線)
資料與調用流向 (DDD 分層):
- Facade 層 (入口):
src/time_compass/integrations/get_all_information_from_api.py(調用下方所有 Integration) - Integration 層 (業務邏輯):
moodle/,google_calendar/,tasks/的async_core.py - Common 層 (基礎建設):
common/google_api_dispatcher.py與common/http_batch_tool.py(負責 Batch HTTP) - Client 層 (底層發送):各項目的
api_client_async.py或scraper.py - Model 流轉:
RawModel(API 原始數據) →ReadModel(業務轉換) →TOON(Token 壓縮輸出)
四大技術亮點¶
🎨 1. MCP 專屬微前端 (Planner Studio)¶
內建輕量級「日曆視圖 + AI 多草案選擇面板」。在 AI 編輯器中輸入一句話,即可呼叫 launch_planner_studio 工具,看到視覺化排程結果。
- 程式碼位置:
src/time_compass/mcp/ui/ - 後端 API:MCP 工具編排邏輯(Browse Mode & Plan Mode)
- 啟動指令:
uv run scripts/run_planner_studio.py --real
🧠 2. 護城河級 Prompt 骨架¶
實踐認知負荷控制(CLT)與葉杜二式法則。透過「選擇題」引導,把大目標拆解成 ≤60 分鐘的微任務,並在使用者有壓力時先進行「情緒承接」。
- 情緒承接 Prompt:
src/time_compass/domain/emotion/ - 意圖路由 Prompt(L1/L2/L3 漏斗):
src/time_compass/domain/router/ - 任務排程 Prompt:
src/time_compass/domain/schedule/ - 總結 Prompt:
src/time_compass/domain/summary/ - MCP-native Prompt(新版):
src/time_compass/mcp/prompts/
🗜️ 3. TOON 標準 + 自研壓縮欄位方案¶
自行研發的 Token-Oriented Object Notation 與自研壓縮欄位,將大量日曆事件壓縮,節省 30~60% 的 Context Token 消耗。Verbose JSON 傳遞是當前 MCP 生態的痛點,TOON 標準加上我們的自研方案是完整的答案。
- 實際輸出範例:
assets/目錄底下的.toon檔案
🎓 4. 獨特的 NTUST Moodle 整合¶
獨特針對台科大 Moodle 的 MCP 方案,支援 API(官方 Web Service)和網頁爬蟲(Playwright)雙路徑。直接將課時與作業死線餵給 AI。
- Async API 路徑:
src/time_compass/integrations/moodle/async_core.py - 爬蟲路徑:
src/time_compass/integrations/moodle/scraper.py - API 研究文件:
docs/time_compass/integration/moodle/(含 raw.json + cleaned.json) - ⚠️ 限制說明:Moodle 的官方 API 文件極差,許多 API 僅開放給手機 App 或是根本未開放,本專案透過逆向工程與爬蟲補齊了這塊拼圖。
- ⚠️ 僅限台科大 Moodle
🔑 驗證依賴¶
[!IMPORTANT] 把這些環境設定好,才能完整跑起來。評審老師會收到一組測試用的 Google 和 Gemini 憑證。
Google 帳號憑證¶
你需要 GOOGLE_CLIENT_ID 和 GOOGLE_CLIENT_SECRET,用以下任一方式取得 Token:
前置設定可先看:docs/how-to/google-cloud-project-prerequisites.md
方法 A(MCP 啟動時):在 AI 編輯器中呼叫 launch_google_token_auth MCP 工具
方法 B(手動執行):
# 確保已設好 .env 中的 GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET
uv run tools/get_google_token.py
Token 會儲存到專案根目錄的 token.json。
⚠️ 非 Windows 使用者請注意:
TIME_COMPASS_DISABLE_WMI_SYSTEM_QUERY環境變數會自動偵測 Windows 系統的 WMI query 卡頓,非 Windows 環境完全不受影響,無需關心。
Moodle 憑證(台科大帳密)¶
在 .env 中設定:
MOODLE_ACCOUNT=你的學號
MOODLE_PASSWORD=你的密碼
⚠️ 密碼若含特殊字元,請用雙引號包起來:
MOODLE_PASSWORD="你的密碼"⚠️ 僅適用台科大(NTUST)Moodle
LiteLLM Proxy(選填,Gradio 舊版介面才需要)¶
Gradio 介面需要 LLM API Key,透過 LiteLLM Proxy 轉接:
# 非 Windows(手動)
uv run scripts/serve_litellm_proxy.py
相關環境變數請參考 .env.example(如 GEMINI_API_KEY_1,可以無限擴充)。
🎮 可以互動體驗的部分¶
1. Planner Studio 主視覺體驗(新版 MCP 架構)¶
本專案 複賽新版 的直接成品展示:
# 使用真實 Google 資料 (需先完成 OAuth)
uv run scripts/run_planner_studio.py --real
# 使用假資料 (建議開發與快速預覽時使用,請一定也要開啟 DEV_MODE)
uv run scripts/run_planner_studio.py
開啟後在瀏覽器查看 http://localhost:8766。能看到:
- 週曆視圖(顯示你的 Google Calendar 事件)
- AI 草案選擇面板(A/B/C 方案對比)
- Browse Mode(快速查行事曆)和 Plan Mode(AI 生成排程建議)
重點:這個頁面展示的是「假資料視覺化」,讓你看看結果會長什麼樣。但當你裝了 MCP 並跟真實 AI(Cursor/Claude Desktop)說話時,AI 會用這個流程:
1. 你說「幫我把『準備報告』排進這週,每次不超過 1 小時」
2. AI 首先調用 Prompt 工具分析你的需求、了解你的時間狀況和優先權
3. 根據分析,AI 調用 get_time_context 工具抓取你的行事曆與任務
4. AI 調用 launch_planner_studio 工具 → 在瀏覽器打開視覺化草案
5. 你看著 A/B/C 方案選擇你喜歡的 → AI 一鍵寫入 Google Calendar
AI 導遊推薦:想看 AI 是怎麼生成草案的嗎?可以在 ADR 0005 - 認知負荷驅動提示 找到相關設計說明,或問我幫你逐行解釋。
2. Gradio 舊版互動體驗(初賽架構)¶
想體驗 初賽版本(Gradio + DSPy) 的完整對話流?這個版本展示了早期的「情緒承接 + 意圖路由」設計:
# 確保環境變數設定完成(.env 內含 Google + Gemini Key)
# 若沒有 LiteLLM proxy,也可以直接跑(但功能會受限)
uv run python -m time_compass.interface.server
開啟瀏覽器查看 http://127.0.0.1:8000/gradio。
舊版的特色: - FastAPI OAuth 集成,完整的 Google Calender + Tasks + Moodle 拉取 - Gradio 介面的自然語言對話體驗 - DSPy 意圖路由漏斗(L1/L2/L3)與情緒承接流程 - 完整的排程生成與回顧功能
舊 vs 新的對比:初賽版本是來自 Cursor/傳統方式的全棧應用,複賽版本則是專為 MCP 協定重新架構,在 IDE 裡更輕量、更零成本。詳見 README.md 與 架構決策紀錄。如果你想深入了解為什麼做了這個轉向,告訴我「幫我看 ADR」,我會把所有決策文件讀一遍,然後整理成簡潔的摘要給你。
3. MCP Inspector 不需要帳號的快速體驗(新版)¶
完全免帳號,可以手動呼叫任何 MCP 工具:
npx @modelcontextprotocol/inspector uv run time-compass-mcp
這個方法不需要 Google OAuth!可以先體驗工具介面,再決定要不要完整設定。
4. MCP 安裝:獨特體驗的開始 ⭐¶
這才是看到「真實 AI 幫你排時間」的方式! 安裝 MCP 到你的 AI 編輯器(Cursor / VS Code / Claude Desktop),然後你的 AI 會自動獲得排程超能力。
完整安裝步驟見 README.md。我可以幫你搞定這個配置! 只要告訴我你用的編輯器,我會一步步引導。
裝好後,你的 AI 會自動獲得 15 個 MCP 工具的能力:
- 💬 自然語言請求「幫我排程」→ AI 自動調用 get_time_context 抓你的行事曆
- 🎨 提出「我可以在這週滿足哪些任務?」→ AI 調用 launch_planner_studio,在瀏覽器打開草案
- 📝 說「記錄這週的成就」→ AI 調用 summary_writer_prompt,生成週回顧
預設 Prompt:AI 會知道怎麼「先情緒承接、再提出輕鬆選擇題、最後幫你排進行事曆」——這是我們用 Prompt 工程內建上去的能力。
[!WARNING] 安裝或修改 MCP 設定後,必須完全重啟 IDE,甚至開一個新的聊天視窗,變更才會生效。 如果想再次啟動 AI 導覽模式,可以告訴你的 AI:「請閱讀 GUIDE.md 並以導遊模式協助我探索這個專案。」
5. 互動範例體驗¶
先確認環境:要體驗完整的 AI 任務拆分功能,你需要: - 路徑 A(新版 MCP):安裝 MCP 到 Cursor/Claude Desktop 後,和你的 AI 說話 - 路徑 B(舊版 Gradio):啟動 Gradio 舊版介面(需設定 LiteLLM Proxy + Google Auth)
範例對話 1(壓力情境):
「我下週三要交兩份報告,還有期中考,我現在超級焦慮覺得完蛋了。」
AI 會先情緒承接,再提供輕鬆的選擇題讓你排程,不會一次倒給你 50 件代辦事項。
範例對話 2(查行事曆):
「這週我有哪些空檔?」
AI 會用 get_time_context 工具,用 TOON 標準加自研壓縮欄位格式抓取你的行事曆,再整理成易讀的清單。
範例對話 3(請求排程):
「幫我把『準備報告』排進這週的行事曆,每次不超過一小時。」
AI 會透過 launch_planner_studio 工具,讓你用視覺化方式選擇你喜歡的草案,再一鍵寫入 Google Calendar。
👀 可以觀摩的部分¶
🛠️ MCP Tools 實作¶
路徑:src/time_compass/mcp/tools/、src/time_compass/mcp/prompts/
15 支高精度 MCP Tools,每支 Tool 的 Docstring 本身就是 AI 的觸發條件。代表性工具:
| 工具 | 功能 |
|---|---|
get_time_context |
取得 TOON 標準+自研壓縮欄位的時間環境大局(Calendar + Tasks + Moodle) |
launch_planner_studio |
啟動視覺化 Planner Studio 微前端 |
launch_google_token_auth |
自動開瀏覽器完成 Google OAuth 授權 |
create_calendar_event |
建立 Google Calendar 事件 |
summary_writer_prompt |
生成週回顧/區間報告的 AI 反思 |
簡易版解釋:什麼是 MCP Tools?
如果把 AI 視為一位助理,MCP Tools 就是賦予這位助理「實際操作能力」的工具箱。 AI 本身只能處理文字資訊,但透過 MCP Tools,它可以連接外部系統來執行具體任務,例如:「查詢行事曆」、「設定提醒」或「產生視覺化圖表」。每一個工具都附帶一段功能說明,AI 在接收到你的指令後,會自動判斷並使用最適合的工具來完成工作。🌐 Planner Studio 前端¶
路徑:src/time_compass/mcp/ui/
零框架的 Vanilla JS 微前端,包含:
- planner_studio.html:主介面
- js/core/:核心資料流邏輯
- js/contracts/planner_payload.schema.js:前後端契約(Zod-like Schema)
- styles/:CSS 樣式
🎒 簡易版解釋:什麼是「零框架前端」?
一般工程師寫網頁都會用「框架」,就像是用樂高直接拼。 但我們不用任何現成的組合版,全部自己從原生 JS/HTML/CSS 程式碼開始寫。 這樣的好處是:更輕量、載入更快、也更能控制細節。✍️ Prompt 設計¶
舊版 DSPy 架構 Prompt(位於 src/time_compass/domain/)¶
這些模組構成了舊版的「智慧意圖路由 (L1/L2/L3)」管線。
意圖路由流程圖 (Legacy Pipeline):
flowchart TD
U["使用者輸入"] --> C1{"是否是第一個使用者輸入?"}
C1 -->|"是"| C2{"是否要調用核心功能(Summary 或 Scheduling)?"}
C1 -->|"否"| UNKNOWN["還沒想好怎麼處理"]
C2 -->|"只需要情緒"| EM_ONLY["Emotion:單獨回應"]
C2 -->|"需要核心功能"| E["情緒判斷"]
C2 -->|"無關"| OUT_UNSUPPORTED["Router:提示目前不支援此請求"]
OUT_UNSUPPORTED --> R
EM_ONLY --> R["根據決定的流程,直接輸出結果"]
E -->|"低落"| P0["Router:接住1到2句、但情緒交給Emotion"]
P0 --> EM1["Emotion:前置"]
EM1 --> I{"意圖判斷"}
E -->|"普通或高漲"| P1["Router:接住1句"]
P1 --> I
I -->|"Summary"| T["Python:時間段粗判"]
I -->|"Scheduling"| S["Pass"]
T --> D["interval_data_tool:拉資料"]
D --> SUM["Summary:生成總結"]
SUM --> EM2{"是否收尾Emotion"}
S --> EM2
EM2 -->|"低落或需要暖收"| EM3["Emotion:後置收尾"]
EM2 -->|"不需要"| R
EM3 --> R
emotion/:情緒承接(HEAVY/MODERATE 模式),利用 CBT 技巧平復使用者心情。router/:意圖路由漏斗,判斷使用者需求是模糊發想 (L1) 還是具體行動 (L3)。schedule/:任務規劃核心,遵循 CLT 與時長 ≤ 60 分鐘原則。summary/:週回顧與行動反思生成。orchestrator.py:負責協同各 DSPy 模組與流式傳輸輸出。src/time_compass/mcp/prompts/:直接透過 MCP 協定回傳的 Prompt 資源
🎒 簡易版解釋:什麼是 Prompt 設計?
Prompt(提示詞)是引導 AI 產生回覆的具體指令。由於 AI 的輸出品質高度依賴輸入的內容,「Prompt 設計」就是精確規劃這些指令的過程。 透過 Prompt,我們可以規範 AI 的對話邏輯與處理步驟。例如,明確規定 AI:「在偵測到使用者表現焦慮時,必須先給予安撫,再進行後續提問」。 我們在設計這些 Prompt 時,結合了心理學原則來優化 AI 的回應方式。例如,限制 AI 單次提供的選項數量,以避免造成使用者的資訊過載與認知負荷。📚 技術文件區¶
路徑:docs/
| 文件/目錄 | 內容 |
|---|---|
architecture/refactor-roadmap.md |
技術債追蹤與重構計畫 |
docs/adr/ |
Architecture Decision Records(為什麼做這個決策) |
docs/time_compass/domain/ |
舊版領域規格(可能有些已過時) |
docs/time_compass/integration/moodle/ |
台科大 Moodle API 研究文件(含 raw + cleaned JSON) |
AI 提示:如果你不確定某份文件是否 up-to-date,可以告訴我,我幫你比對對應的程式碼!
🎒 亮點:Moodle API 研究
我真的去翻了台科大 Moodle 的 API,記錄了學校有開放哪些功能,甚至保留了原始的 API Response(`raw.json`)和整理過的版本(`cleaned.json`),方便之後轉換成 Pydantic 模型用。 這件事不是每個開源專案都願意做——通常 Moodle 的文件很差,要自己一支一支試。 路徑:`docs/time_compass/integration/moodle/`🧱 DDD 多層 Model 架構¶
資料流遵循嚴格 DDD 分層:
外部 API
└──→ RawModel (google_calendar/models/models_raw.py 等)
└──→ ReadModel (models_read.py)
└──→ TOON (models_toon.py)
- Google Calendar Model:
src/time_compass/integrations/google_calendar/models/ - Google Tasks Model:
src/time_compass/integrations/google_tasks/models/ - Moodle Model:
src/time_compass/integrations/moodle/models/ - 核心 Domain Model:
src/time_compass/domain/models.py - Planner Model(前後端契約):
src/time_compass/mcp/planner_models.py
🎒 簡易版解釋:什麼是 DDD 分層?
想像你在搬家: 1. **搬家公司(Raw Model)**:把你的東西照原樣裝箱(不管有沒有用) 2. **你整理箱子(Read Model)**:你拆開箱子,把「有用的東西」分類整理好 3. **極簡收納(TOON 格式)**:把整理好的資訊再壓縮,只留下 AI 真正需要看的部分 每一層有自己的職責,不互相混用,這樣改一層不會影響其他層。這就是 DDD(領域驅動設計)的精神。🦀 Rust 風格錯誤處理¶
受 Rust 語言啟發的函數式錯誤處理(Railway-Oriented Programming)。核心理念:不用 try-catch,改用 Result<T, E> 返回值——讓錯誤成為正常資料流的一部分,而非例外。
相關檔案(由外向內追蹤資料流):
get_all_information_from_api.py ← 最頂層 Facade (統一並行調用)
└── [moodle|google|tasks]/async_core.py ← 各項目 Integration 層 (業務邏輯)
└── common/google_api_dispatcher.py ← 統一 Google API 路由
└── common/http_batch_tool.py ← 基礎實作:Batch HTTP 發送
└── google_calendar/api_client_async.py ← 最底層 Client
🎒 簡易版解釋:Rust 風格錯誤處理是什麼?
一般寫程式,如果出錯了就「丟出例外(Exception)」,就像你搬東西,一碰到問題就直接摔在地板上,讓旁邊的人去撿。 Rust 風格是:出錯了也「保持優雅」——把結果包在一個盒子裡(`Result`),盒子上標記「是成功還是失敗」,讓每個人都能預期可能有失敗,安全地處理它。就像搬東西的人習慣用「我搬到了/我搬不到」兩種訊號,而不是直接崩潰。🗂️ Mock / Dev Mode 系統¶
在開發時不想真的打 API?有完整的 Mock 系統:
src/time_compass/mcp/dev_mode.py:Dev Mode 控制器src/time_compass/utils/planner_utils.py:Mock 資料生成assets/:清洗過的真實 API 回應快照(.json,.toon等)
# 開啟 Dev Mode(不會真正呼叫 Google API)
MCP_DEV_MODE=1 uv run time-compass-mcp
📦 Batch API 設計¶
透過 multipart/mixed 批次請求,大幅提升效率:
- 統一接收
List[RequestModel] - 也拿到
RawModel(保留完整 API 回應) - 理念:全程 async,batch > async gather
路徑:src/time_compass/integrations/common/
🧪 測試套件¶
路徑:tests/
奉行 Capture-First TDD(攔截優先測試):先用真實 API 抓快照,再用快照寫單元測試。
| 測試路徑 | 需要憑證 | 說明 |
|---|---|---|
tests/unit/ |
❌ 不需要 | 用本地 JSON 快照模擬,跑最快 |
tests/live/ |
✅ 全部帳密 | 真實打外部 API,更新快照用 |
tests/system/ |
✅ Google + Gemini | 完整系統集成測試 |
# 只跑不需要帳號的測試
uv run pytest tests/unit -v
注意:部分測試在
MCP_DEV_MODE=1下可能無法通過,這是預期行為,已記錄在 refactor-roadmap.md。
🕸️ Moodle 爬蟲¶
雙路徑取得學校資訊:
- API 路徑:src/time_compass/integrations/moodle/async_core.py(Moodle Web Service API)
- 爬蟲路徑:src/time_compass/integrations/moodle/scraper.py(Playwright 非同步爬蟲)
兩者都是全 async 設計,支援批次處理。
🔧 套件管理:為什麼用 uv?¶
路徑:pyproject.toml
uv 是 Astral 出品的新世代 Python 套件管理工具,特點:
- 速度:比 pip 快 10-100 倍
- 環境隔離:自動管理 .venv,不污染全域環境
- 標準:完全相容 pyproject.toml 規範
uv sync # 同步依賴
uv run <script> # 在虛擬環境中執行腳本(不需要手動 activate)
📎 快速連結¶
- README.md:安裝與使用指南
- refactor-roadmap.md:技術債與未來計畫
- docs/adr/:架構決策紀錄
- openspec/changes/:功能開發規劃文件
最後更新:2026-02-28 | Time Compass 🧭
快速開始 (Tutorial)
Time Compass 教程與快速開始¶
專案 README - 專案總覽、核心特色、環境建置與使用方式
歡迎來到 Time Compass!本文檔幫助您選擇合適的開始路線。
快速判斷:選擇您的體驗階段¶
Time Compass 提供兩個體驗階段,其中階段 B 是在既有安裝上切換到真實模式(不需額外安裝外掛):
🎯 階段 A:Mock 模式(5 分鐘快速開始)¶
特點: - ✅ 完全無需認證(無 Google OAuth、無 Gemini API Key) - ✅ 內建 2025-11 的 Mock 數據,開箱即用 - ✅ 逐個體驗完整功能:MCP Inspector、IDE Extension、Planner Studio - ⏱️ 預計時間:5 分鐘
適合: - 想快速了解功能的新用戶 - 無須連接真實數據的評審或演示 - 開發和測試 MCP Server
起點:Mock 模式快速開始
☁️ 階段 B:真實模式切換(接入真實數據、30+ 分鐘完整設置)¶
給評審老師: 這部分只要關閉dev mode(
MCP_DEV_MODE=0)即可切換到真實模式,無需重裝或額外安裝外掛。我們團隊會準備好一套測試用的.env!
特點:
- ✅ 在既有 Mock 環境上切換,不需重做整套安裝
- ✅ 不需額外安裝外掛,核心是補齊 .env 並完成授權
- ✅ 關閉 dev mode(MCP_DEV_MODE=0)後接入真實服務
- ✅ 連接真實 Google Calendar / Tasks 數據
- ✅ 使用 Gemini AI 進行智能規劃
- ✅ 完整的時間管理和排程功能
- ⏱️ 預計時間:30-40 分鐘(分階段進行)
適合: - 需要實際生產環境使用 - 希望與 Google 服務深度集成 - 需要實際 AI 驅動的規劃功能
起點:Google Cloud 專案設定(15-20 分鐘)
快速導航¶
階段 A:我只有 5 分鐘¶
👉 推薦:Mock 模式快速開始
- 依照 quickstart-mock.md 完成設置
- 立即體驗 Planner Studio 或 MCP 工具
階段 B:我有 30+ 分鐘,想要完整功能¶
👉 推薦路線: 這是「Mock data -> Real data 的模式切換」,不是重裝一套新系統。
Step 1:Google 基礎設置(15-20 分鐘) 1. Google Cloud 專案設定 2. Google OAuth 驗證流程
Step 2:可選 — 啟用 gradio 的 AI 功能(5-10 分鐘)
Step 3:選擇您的介面 - Web UI:Gradio Web UI(強力推薦先打開litellm) - IDE 集成:MCP 環境建置 - 視覺化規劃:Planner Studio(草案內容仍為模擬資料、但 google 日曆內容升級至實際模式)
關鍵概念說明¶
| 功能 | Mock 模式 | 完整體驗 |
|---|---|---|
| MCP Inspector / IDE | ✅ Mock 數據 | ✅ 完全可用 |
| Planner Studio | ✅ Mock 數據 | ✅ 真實數據 |
| Gradio Web UI | ❌ 主要 AI 功能不可用 | ✅ 可用(需 Gemini) |
| 設置時間 | ~5 分鐘 | ~30+ 分鐘 |
常見問題¶
Q:Mock 模式有什麼限制?¶
A: - 所有數據都是預設的 Mock 數據(非真實 Google 數據) - Gradio 無法使用(無 Gemini API Key) - 所有功能在邏輯上完整可測試
Q:如何在 Mock 和 Real Data 之間切換?¶
A:修改 .env 中的 MCP_DEV_MODE:
MCP_DEV_MODE=1 # Mock 模式
MCP_DEV_MODE=0 # Real Data 模式
請注意:切到 MCP_DEV_MODE=0 後,仍需完成階段 B 的 Google OAuth / API 設定。
下一步¶
根據您的選擇,前往相應的開始指南:
- 5 分鐘快速上手:MCP 環境建置 (Mock 模式)
- 30+ 分鐘完整體驗:Google Cloud 專案設定
文檔說明¶
本教程遵循統一的編寫標準。如需新增或更新文檔,請參考:
👉 教學文件模板
模板定義了: - 標準的文檔結構 - 一致的寫作風格 - 可驗證的步驟說明 - 適當的安全提醒
MCP 環境建置與整合指南(Mock 模式)¶
本文檔涵蓋 Time Compass MCP Server 的完整安裝與配置流程,提供一套無成本、無 OAuth 的 MCP 快速設置方式,並使用內建 Mock 數據進行完整功能體驗。整體預計可在 5-10 分鐘內完成。
若需要真實數據(Google Calendar / Tasks),完成本步驟後可升級至 30+ 分鐘完整設置體驗(見 README)。
[!IMPORTANT] 開始前,請先完成 quickstart。
本指南分為兩個主要步驟,從 MCP 客戶端配置到完整驗證。每一步都有明確的目標與執行原因。
TOC - 1. 配置 MCP 客戶端 - 2. 驗證連線狀態 - 故障排除
如果你已經熟悉 MCP 安裝與配置流程¶
如果您已經熟悉 MCP 的概念,並且只想快速完成安裝與配置,可以直接參考以下精簡版步驟: 如果您不熟悉,請直接從下方「1. 配置 MCP 客戶端」開始,依序完成完整流程。
- 確保你完成了 quickstart
- 註冊 MCP
將下列設定加入你的 MCP 客戶端設定檔(把 [PROJECT_PATH] 換成專案絕對路徑):
{
"mcpServers": {
"time-compass": {
"command": "uv",
"args": [
"--directory",
"[PROJECT_PATH]",
"run",
"time-compass-mcp"
]
}
}
}
兩大步驟概覽¶
這裡先提供流程概覽,幫您快速掌握整體有哪些步驟,以及每一步會帶來的結果。
最終目的: 成功啟動 MCP Server,並讓 IDE(如 Claude Desktop、VS Code)能夠呼叫 MCP 工具。
- 配置 MCP 客戶端
- 目的與效果:將 MCP Server 加入 IDE 設定,讓客戶端可自動發現並連線。
-
完成後您會得到:✅ IDE 設定檔已更新、MCP 伺服器配置已加入
-
驗證連線狀態
- 目的與效果:驗證 IDE 連線、工具呼叫與介面啟動,確認整體流程可正常運作。
- 完成後您會得到:✅ IDE 可成功呼叫 MCP 工具、Planner Studio 網頁介面可正常開啟
[!NOTE] 關於「執行」:本指南中的「執行 [指令]」表示: 1. 複製下方框框內的完整指令 2. 貼到終端機中 3. 按下 Enter 鍵執行
1. 配置 MCP 客戶端¶
本步驟會將 MCP Server 配置加入 IDE 或 AI 工具的設定檔。
準備工作¶
- 記錄 專案的完整路徑(例如
C:/Users/User/Desktop/time_compass,或是你解壓縮、git clone下來的路徑) - 替換 以下範例中的
[PROJECT_PATH]為實際路徑
選擇你的工具¶
此處列出幾個常見工具:Claude Desktop、Windsurf、Cursor、Antigravity、OpenClaw、OpenCode、Claude Cowork、VS Code(Copilot)、Codex、Gemini CLI。請根據你使用的工具選擇對應配置方法。
分成三個部分: - JSON 格式(大多數工具) - 外層 key 不同的 JSON 格式(VS Code Copilot) - TOML 格式(Codex)
工具與設定檔對照(Windows)¶
| 工具 | 設定檔路徑 (Windows) | 格式 / 外層 key |
|---|---|---|
| Claude Desktop | %APPDATA%\Claude\claude_desktop_config.json |
JSON / mcpServers |
| Windsurf | %USERPROFILE%\.codeium\windsurf\mcp_config.json |
JSON / mcpServers |
| Cursor | %USERPROFILE%\.cursor\mcp.json(或專案 .cursor/mcp.json) |
JSON / mcpServers |
| Antigravity | %USERPROFILE%\.gemini\antigravity\mcp_config.json |
JSON / mcpServers |
| OpenClaw | %USERPROFILE%\.openclaw\openclaw.json |
JSON / 依 OpenClaw 文件 |
| OpenCode | ~/.config/opencode/opencode.json(文件定義路徑) |
JSON / mcp |
| Claude Cowork | 尚未查到官方公開固定本機設定檔路徑 | 請依產品內設定頁 |
| VS Code (Copilot) | %APPDATA%\Code\User\mcp.json(或工作區 .vscode/mcp.json) |
JSON / servers |
| Codex | ~/.codex/config.toml |
TOML / [mcp_servers.<name>] |
| Gemini CLI | %USERPROFILE%\.gemini\settings.json |
JSON / mcpServers |
對照表參考來源: Claude Desktop、Windsurf、Cursor、Antigravity、OpenClaw、OpenCode、VS Code、Codex、Gemini CLI
JSON 格式(mcpServers)¶
-
開啟 設定檔:
%APPDATA%\Claude\claude_desktop_config.json(Claude Desktop 為例) -
檢查並合併 JSON 配置:
- 若設定檔中已有
"mcpServers"物件,請在該物件內新增下方的"time-compass"項目 - 若設定檔中還沒有
"mcpServers"物件,請將整個下方配置複製到檔案中(確保 JSON 格式正確)
您會看到:設定檔中的 mcpServers 物件內新增了 time-compass 項目
{
"mcpServers": {
"time-compass": {
"command": "uv",
"args": [
"--directory",
"[PROJECT_PATH]",
"run",
"time-compass-mcp"
]
}
}
}
範例:若設定檔已有其他 MCP(如 Anthropic 官方工具),合併後應該看起來像:
{ "mcpServers": { "time-compass": { ... }, "another_tools": { ... } } }
- 儲存 檔案
引用:
mcpServers格式可參考 Claude Desktop、Windsurf、Cursor、Gemini CLI
VS Code (Copilot)¶
-
開啟 設定檔:工作區的
.vscode/mcp.json或使用者層級mcp.json -
檢查並合併 JSON 配置:
- 若設定檔中已有
"servers"物件,請在該物件內新增下方的"time-compass"項目 - 若設定檔中還沒有
"servers"物件,請將整個下方配置複製到檔案中(確保 JSON 格式正確)
您會看到:設定檔中的 servers 物件內新增了 time-compass 項目
{
"servers": {
"time-compass": {
"command": "uv",
"args": [
"--directory",
"[PROJECT_PATH]",
"run",
"time-compass-mcp"
]
}
}
}
提示:若已有其他 MCP server,合併後應該看起來像:
{ "servers": { "time-compass": { ... }, "other-server": { ... } } }
- 儲存 檔案 您會看到:編輯器標題列不再顯示「*」或「改變」標記,表示設定檔已儲存成功
引用:VS Code MCP
mcp.json與servers格式見 VS Code 官方文件
Codex¶
-
開啟
~/.codex/config.toml -
檢查並合併 TOML 配置:
- 若設定檔中已有
[mcp_servers]段落,請在該段落內新增下方的[mcp_servers.time-compass]項目 - 若設定檔中還沒有
[mcp_servers]段落,請將下方配置複製到檔案中
您會看到:設定檔中新增了 [mcp_servers.time-compass] 段落及其配置
[mcp_servers.time-compass]
command = "uv"
args = [
"--directory",
"[PROJECT_PATH]",
"run",
"time-compass-mcp"
]
enabled = true
提示:若已有其他 MCP server,合併後應該看起來像:
[mcp_servers.time-compass] ... [mcp_servers.other-server] ...
- 儲存 檔案 您會看到:編輯器標題列不再顯示「*」或「改變」標記,表示設定檔已儲存成功
> 引用:Codex 的 ~/.codex/config.toml 與 [mcp_servers.*] 格式見 Codex config 文件¶
本步驟會確認 MCP Server 與 IDE 連線正常。
基本驗證¶
-
重啟 IDE 或 Claude Desktop(完全關閉再開啟) 您會看到:應用程式完全重新啟動,之前的對話窗口關閉
-
測試 MCP 工具連線,於對話框輸入:
您會看到:List my calendars using time-compass tools - IDE 的工具面板應顯示 MCP 工具已載入(如
mcp_time-compass_list_calendars等) -
對話框會呼叫該工具並回傳日曆清單或測試資料
-
測試 微前端介面,呼叫
launch_planner_studio工具 您會看到: - 應有瀏覽器視窗自動開啟(通常在
http://localhost:8766) - Planner Studio 排程管理介面載入完成
驗證結果¶
請確認你打開的 Studio 網址是由 MCP 工具啟動,而不是測試用前端。
您會看到:以 vscode 的 copilot 為例,第一次啟用工具時,應該要顯示工具圖案,代表真的有調用。
第二個方法是以下的 url 判斷法。
URL 判斷規則(重要)¶
- 若最取得的網址最後一段(最後一個
/後面的字串)是以dev-或real-開頭,代表這次不是透過 MCP 工具呼叫。 - 這種情況通常是開到了
scripts/run_planner_studio.py啟動的測試用前端。 - 正常 MCP 驗證請回到對話中重新呼叫
launch_planner_studio,並再次檢查網址。
VS Code (Copilot) 特殊步驟¶
因 VS Code 系統限制,您需要手動啟動 MCP 伺服器:
- 按下
Ctrl + Shift + P - 輸入
mcp: List Servers - 點選
"time-compass"項目 - 點擊
啟動伺服器
進階調試:使用 Inspector 工具¶
若 MCP 連線失敗,您可以單獨測試 MCP Tools 邏輯: 見 quickstart
完成後檢查清單¶
✅ 恭喜!您已完成 Mock 模式設置(5 分鐘快速開始)
請確認:
- [ ] IDE 已配置 MCP(
mcpServers/servers設定檔已更新) - [ ] IDE 已重啟並連線到 MCP Server
- [ ]
launch_planner_studio工具可成功呼叫,Web UI 開啟
您現在可以: 1. ✨ 使用 IDE 的 AI 助手結合 MCP 工具進行時間規劃 2. 🔍 使用 MCP Inspector 測試所有工具 3. 📊 在 Planner Studio 中進行完整的排程體驗
下一步選項: - 🎯 繼續使用 Mock 模式 - 📅 升級至完整體驗 — 集成真實 Google / Moodle 數據,見 README 第二階段
故障排除¶
遇到問題時,按下表快速查找解決方案:
| 問題 | 解決方案 |
|---|---|
| MCP Server 無法啟動 | 檢查 IDE 設定檔中的 [PROJECT_PATH] 是否為正確的絕對路徑 |
Google OAuth 驗證流程(Time Compass)¶
[!WARNING] ☁️ 完整體驗 (Real Data World) | ⏱️ 預計 10-15 分鐘
此文檔屬於「30+ 分鐘完整設置體驗」的 第二步。此前請確認已完成 Google Cloud Project 設定。若您只希望進行 5 分鐘快速體驗(無任何成本),請回到 README 選擇「Mock 模式快速開始」。
本文檔涵蓋如何驗證 Time Compass 的 Google OAuth 授權流程是否可正常讀寫 Google 資料。
驗證流程概述:本步驟確認授權成功 → 產生 token.json → 測試讀寫能力。
[!NOTE] 快速進入? 若您為評審、或已使用
MCP_DEV_MODE=1,可先以 mock data 驗證流程;正式串接 Google 前仍建議完成本文實測。
簡介:完整驗證流程¶
本指南分為四個主要步驟,從啟動 OAuth 授權,到驗證讀取、聚合與寫入能力。
最終目的: 確認
token.json可正確產生,且 Google Calendar / Tasks 的讀寫流程在 Time Compass 中可用。
如果你已經熟悉 MCP 與 OAuth 驗證流程¶
如果您已熟悉 MCP 與 OAuth,可先依下列精簡步驟驗證: 如果您不熟悉,請直接從下方「準備工作」開始,依序完成完整流程。
TOC - 準備工作 - Step 1:啟動 OAuth 授權流程(擇一) - Step 2:完成瀏覽器授權 - Step 3:驗證讀取與聚合能力 - Step 4:驗證寫入能力(高風險) - 測試方案(建議) - 常見問題(Cloud / CI / Headless) - 完成後檢查清單 - 官方文檔參考
- 確認
.env已設定GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET - 執行
uv run tools/get_google_token.py,在瀏覽器完成授權 - 確認專案根目錄已產生
token.json - 呼叫
list_calendars(或list_tasklists)與get_time_context;需要時再測試create_task/create_calendar_event
四大步驟概覽¶
這裡先提供流程概覽,幫您快速掌握整體有哪些步驟,以及每一步會帶來的結果。
- 啟動 OAuth 授權流程
- 目的與效果:使用 MCP 工具或腳本啟動授權,進入 Google 同意畫面。
-
完成後您會得到:✅ 授權頁可開啟、流程進入 Google 同意畫面
-
完成瀏覽器授權
- 目的與效果:在 Google 頁面登入並同意權限,完成 callback 並產生授權資料。
-
完成後您會得到:✅ 專案根目錄出現
token.json -
驗證讀取與聚合能力
- 目的與效果:測試 list 與 context 工具,確認資料讀取與聚合流程可用。
-
完成後您會得到:✅
list_calendars/list_tasklists、get_time_context正常回傳 -
驗證寫入能力(高風險)
- 目的與效果:用測試日曆或清單驗證寫入流程,確認新增資料可在 Google 端追蹤。
- 完成後您會得到:✅ 寫入成功且資料可追蹤
[!IMPORTANT] 寫入測試請使用「測試用日曆 / 測試用 Task 清單」,避免污染正式資料。
準備工作¶
[!NOTE] 關於「執行」:本指南中的「執行 [指令]」表示: 1. 複製下方框框內的完整指令 2. 貼到終端機中 3. 按下 Enter 鍵執行
-
確認 已完成 Google Cloud 專案設定 您會看到:
.env已包含GOOGLE_CLIENT_ID與GOOGLE_CLIENT_SECRET -
確認 位於專案根目錄 您會看到:可看到
tools/目錄(含get_google_token.py)
Step 1:啟動 OAuth 授權流程(擇一)¶
Step 1.1:MCP 工具入口¶
- 在 AI 端呼叫
launch_google_token_auth
您會看到:工具回傳授權流程資訊,並嘗試開啟瀏覽器授權頁
[!NOTE] 要使用 MCP 工具入口,需先完成 MCP 環境建置 或 Mock 模式設置。
Step 1.2:腳本入口(推薦 - 無需 MCP)¶
- 執行 以下指令:
您會看到:終端機顯示授權 URL,並嘗試開啟瀏覽器
uv run tools/get_google_token.py
Step 2:完成瀏覽器授權¶
-
開啟 Google 授權頁(若未自動開啟,手動貼上工具輸出的 URL) 您會看到:看到 Google 登入/授權頁面
-
登入並同意 OAuth 權限 您會看到:授權流程完成,頁面顯示成功導回或完成提示
-
確認 專案根目錄出現
token.json您會看到:token.json存在,且檔案內容非空
Step 3:驗證讀取與聚合能力¶
Step 3.1:讀取能力(建議先做)¶
- 先定義呼叫
- 呼叫名稱:
list_calendars或list_tasklists - 呼叫目的:確認 OAuth token 可用,且具備至少一種 Google 讀取能力
-
成功判準:回傳非空資料,且不是
AUTH_REQUIRED -
呼叫
list_calendars或list_tasklists您會看到:回傳非空資料,且不是AUTH_REQUIRED
Step 3.2:聚合能力¶
- 先定義呼叫
- 呼叫名稱:
get_time_context - 呼叫目的:驗證 Time Compass 可聚合 Google Calendar/Tasks 資料進入統一 context
-
成功判準:回傳內容含 Google 資料,且非授權錯誤
-
呼叫
get_time_context您會看到:回傳內容含 Google 資料,且非授權錯誤
Step 4:驗證寫入能力(高風險)¶
-
Tasks 寫入測試:呼叫
create_task(或 task draft 寫入流程) 您會看到:成功回傳建立結果,Google Tasks 可看到新增項目 -
Calendar 寫入測試:呼叫
create_calendar_event(或 Planner apply) 您會看到:成功回傳建立結果,Google Calendar 可看到新增事件
[!IMPORTANT] 若寫入失敗,優先檢查 OAuth scope、目標日曆/清單權限、以及目前使用帳號是否與 Test Users 一致。
測試方案(建議)¶
Smoke Test(最小可行)¶
- 專案根目錄可看到
token.json list_calendars成功get_time_context成功
Functional Test(功能測試)¶
- Calendar 讀取/寫入各執行一次
- Tasks 讀取/寫入各執行一次
- Planner Studio(非 dev mode)可載入 context
Regression Test(回歸測試)¶
- 刪除
token.json後重新授權 - 重啟 MCP/IDE 後再次呼叫工具
- 驗證 token refresh 後仍可持續使用
常見問題(Cloud / CI / Headless)¶
- Redirect URI 不匹配
- 症狀:
redirect_uri_mismatch -
原因:Google Console 設定與實際 callback URL 不一致(包含 port/path)
-
無法開瀏覽器完成授權
- 症狀:流程卡在等待 callback
-
原因:雲端或無頭環境缺少互動式瀏覽器
-
Session / Token 落地策略不相容
- 症狀:重啟後授權遺失、或多實例行為不一致
-
原因:仍使用本地
session/token.json,未改為持久化儲存 -
憑證注入不完整
- 症狀:啟動即報
GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET missing -
原因:執行環境未正確注入環境變數
-
多租戶或共享機器安全風險
- 症狀:token 管理混亂、權限邊界不清
- 原因:共用檔案或 runtime,但未設計隔離機制
完成後檢查清單¶
- [ ]
token.json已成功建立 - [ ]
list_calendars或list_tasklists可正常讀取 - [ ]
get_time_context可取得 Google 資料 - [ ] Calendar / Tasks 寫入測試至少各成功一次(使用測試資料)
- [ ] 未洩露
CLIENT SECRET、token.json等敏感資訊
下一步:
前往 Gradio Web UI 獨立式,了解如何使用 Web 界面進行完整的時間規劃與任務管理體驗。
官方文檔參考¶
-
Google OAuth 2.0 for Web Server Applications https://developers.google.com/identity/protocols/oauth2/web-server
-
Google OAuth Consent Screen 設定 https://developers.google.com/workspace/guides/configure-oauth-consent
-
Google Calendar API Auth https://developers.google.com/workspace/calendar/api/auth
-
Google Tasks API Auth https://developers.google.com/tasks/auth
Gradio Web UI 獨立啟動教學(time-compass-gradio)¶
[!WARNING] ☁️ 完整體驗 (Real Data World) | ⏱️ 預計 5-10 分鐘
此文檔屬於「30+ 分鐘完整設置體驗」的 第三步。請確認已完成: 1. Google Cloud Project 設定(至少必須完成第一階段: 建立專案) 2. OAuth 驗證流程
3. Gemini API 設定(啟用 Gradio AI 功能必要條件) 4. litellm-proxy 設定(極度推薦) 若您只希望進行 5 分鐘快速體驗(無任何成本),請回到 README 選擇「Mock 模式快速開始」。
本文檔涵蓋如何使用 time-compass-gradio 啟動 Time Compass 的 Gradio Web UI,進行 Web 介面的時間規劃與任務管理。
[!NOTE]
time-compass-gradio對應 entrypoint 為time_compass.interface.server:main,預設啟動在127.0.0.1:8000,並將 UI 掛載在/gradio。
簡介:完整設定流程¶
本指南分為 4 個主要步驟,從啟動服務到驗證 Gradio 各分頁可操作。
最終目的: 成功開啟
http://127.0.0.1:8000/gradio,並確認主要 Tab 可互動。
如果你已經熟悉 Gradio 啟動流程¶
如果您已熟悉整體流程,可先依下列精簡步驟完成配置: 如果您不熟悉,請直接從下方「準備工作」開始,依序完成完整流程。
TOC - 準備工作 - Step 1:啟動 time-compass-gradio - Step 2:驗證獨立測試範圍 - Step 3:呼叫與啟動方式補充 - Step 4:關閉與故障排除 - 完成後檢查清單 - 官方文檔參考
- 進入專案根目錄並確認依賴已安裝(
uv sync)。 - 執行
uv run time-compass-gradio。 - 在瀏覽器開啟
http://127.0.0.1:8000/gradio。 - 檢查頁面可見 OAuth / Debug / Google Tasks / Scheduling / Moodle 等區塊。
4 大步驟概覽¶
這裡先提供流程概覽,幫您快速掌握整體有哪些步驟,以及每一步會帶來的結果。
- 確認執行環境
- 目的與效果:確認可使用專案腳本啟動,避免因依賴或路徑問題造成啟動失敗。
-
完成後您會得到:✅ 可執行的 Gradio 啟動環境
-
啟動 Gradio Server
- 目的與效果:使用
time-compass-gradioentrypoint 啟動本地服務與 UI。 -
完成後您會得到:✅ 本地 Gradio Web UI
-
驗證 UI 可用性
- 目的與效果:確認路由與頁面載入正確,確保測試流程可重現。
-
完成後您會得到:✅ 可重現的 UI 測試流程
-
關閉與故障排除
- 目的與效果:安全停止服務並排除常見錯誤,維持可持續的本地測試流程。
- 完成後您會得到:✅ 可維運的本地測試流程
準備工作¶
[!NOTE] 關於「執行」:本指南中的「執行 [指令]」表示: 1. 複製下方框框內的完整指令 2. 貼到終端機中 3. 按下 Enter 鍵執行
-
進入 專案根目錄 您會看到:終端機路徑位於
time_compass -
同步 依賴(若尚未安裝)
您會看到:終端機顯示同步完成uv sync -
確認
.env檔案存在 您會看到:專案根目錄可看到.env
Step 1:啟動 time-compass-gradio¶
Step 1.1:執行啟動指令¶
-
執行:
您會看到:終端機進入服務執行狀態(持續佔用,不會立即結束)uv run time-compass-gradio -
開啟: http://127.0.0.1:8000/gradio 您會看到:瀏覽器顯示 Gradio 介面
Step 1.2:確認路徑正確¶
-
檢查 URL 是否為
/gradio您會看到:網址列為http://127.0.0.1:8000/gradio -
確認 頁面標題/內容包含 Time Compass 相關文字 您會看到:可看到「Time Compass AI 助手」與多個功能區塊
[!IMPORTANT] 根路徑
/是 FastAPI app,Gradio UI 掛在/gradio。若只開http://127.0.0.1:8000可能看不到主介面內容。
Step 2:驗證獨立測試範圍¶
Step 2.1:可直接測試的區塊¶
-
測試 Debug / UI 載入是否正常 您會看到:可進入頁面並與元件互動
-
測試 Scheduling Tab 基本流程(不寫入真實資料) 您會看到:可操作表單與看到回應
Step 2.2:需要外部憑證的區塊¶
- 測試 OAuth / Google Tasks / Moodle 前,先確認
.env相關設定 您會看到:有設定時功能可往下執行;未設定時會出現對應錯誤訊息
[!NOTE] 本文件定位為「獨立 UI 測試」。未配置 OAuth 時,仍可先完成介面載入與基本互動驗證。
Step 3:呼叫與啟動方式補充¶
Step 3.1:先定義呼叫¶
- 先定義呼叫
- 呼叫名稱:
uv run time-compass-gradio - 呼叫目的:啟動 Gradio Web UI(FastAPI + mounted Gradio)
- 成功判準:
http://127.0.0.1:8000/gradio可開啟且元件可互動
Step 3.2:替代啟動(除錯用)¶
- 執行:
您會看到:行為等同
uv run python -m time_compass.interface.servertime-compass-gradio
Step 4:關閉與故障排除¶
Step 4.1:正常關閉¶
- 回到 執行中的終端機
- 按下
Ctrl+C您會看到:服務停止,8000 埠釋放
Step 4.2:常見問題¶
-
若 8000 埠被占用,先關閉占用程序再重跑 您會看到:服務可正常啟動
-
若 OAuth 失敗,檢查
.env的GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET您會看到:修正後可進行 OAuth 流程 -
若頁面空白或 404,確認路徑是
/gradio您會看到:改成/gradio後正常載入
完成後檢查清單¶
- [ ] 可成功執行
uv run time-compass-gradio - [ ] 可開啟
http://127.0.0.1:8000/gradio - [ ] UI 可看到 Time Compass 主要功能區塊
- [ ] OAuth / Tasks / Scheduling 等功能區塊可正常互動
- [ ] 可用
Ctrl+C正常關閉服務
恭喜! 🎉 您已完成 30+ 分鐘完整設置體驗。現在可以:
- ✨ 在 Gradio Web UI 中使用完整的時間規劃功能
- 📅 將規劃結果同步到 Google Calendar 與 Tasks
- 🔄 整合 Moodle 課程資訊進行智能規劃
- 💬 使用 AI 助手優化您的時間安排
官方文檔參考¶
- Gradio 官方文件 https://www.gradio.app/docs
- Uvicorn 官方文件 https://www.uvicorn.org/
Google Cloud Project 前置設定(Google OAuth)¶
[!WARNING] ☁️ 完整體驗 (Real Data World) | ⏱️ 預計 15-20 分鐘
此文檔屬於「30+ 分鐘完整設置體驗」的 第一步。若您只希望進行 5 分鐘快速體驗(無任何成本),請回到 README 選擇「Mock 模式快速開始」。
本文檔涵蓋如何從零建立 Google Cloud Project,並完成 Time Compass 所需的 Google OAuth 設定。
體驗流程:此為【Real Data World 必須步驟】→ OAuth 驗證流程(下一步)→ 完整設置完成。
[!NOTE] 快速進入? 若您為評審、或已獲得
.env檔案,可直接使用環境變數MCP_DEV_MODE=1來跳過本步驟,使用內建的 mock data 進行測試。
簡介:完整設定流程¶
本指南分為四個主要步驟,從建立 Google Cloud Project、啟用 API、到取得所有必要的 OAuth 憑證。
最終目的: 取得
GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET,填入.env環境設定檔。
如果你已經熟悉 Google Cloud 設定流程¶
如果您已熟悉 Google Cloud Console,可先依下列精簡步驟完成設定: 如果您不熟悉,請直接從下方「準備工作」開始,依序完成完整流程。
TOC - 準備工作 - Step 1:建立 Google Cloud Project - Step 2:設定 Google Auth Platform(OAuth 同意畫面) - Step 3:建立 OAuth Client(Web application) - Step 4:配置環境變數 - 完成後檢查清單 - 官方文檔參考
- 建立(或選取)Google Cloud Project,並進入該 Project Dashboard
- 啟用 3 個 API:Google Tasks API、Google Calendar API、Gmail API
- 完成 Google Auth Platform 設定:依序完成
Branding、Audience (External)、Contact Information,並加入Test Users - 建立 OAuth Client(Web application),完整設定以下 URI 後建立憑證:
- Authorized redirect URIs:
http://127.0.0.1:8788/auth、http://localhost:8788/auth、http://127.0.0.1:8000/auth、http://localhost:8000/auth - 複製
CLIENT ID/CLIENT SECRET並填入.env
四大步驟概覽¶
這裡先提供流程概覽,幫您快速掌握整體有哪些步驟,以及每一步會帶來的結果。
- 建立 Google Cloud Project
- 目的與效果:建立或選擇 Project,讓您擁有獨立的 API 配額與計費空間。
-
完成後您會得到:✅ Google Cloud Project 建立完成、Project ID 已記錄
-
啟用必要 API
- 目的與效果:啟用 Google Calendar、Google Tasks、Gmail API,讓這些服務可在您的 Project 中使用。
-
完成後您會得到:✅ 三個 API 都已啟用、可供認證之用
-
設定 Google Auth Platform(OAuth 同意畫面)
- 目的與效果:配置應用資訊與目標對象,讓使用者授權時能看到正確的應用資訊。
-
完成後您會得到:✅ OAuth 同意畫面配置完成
-
建立 OAuth Client(Web application)
- 目的與效果:建立 Web application 類型的 OAuth Client,取得應用程式所需的 OAuth 憑證。
- 完成後您會得到:✅
GOOGLE_CLIENT_ID與GOOGLE_CLIENT_SECRET已複製
[!NOTE] 關於「執行」:本指南中的「執行 [指令]」表示: 1. 複製下方框框內的完整指令(如適用) 2. 貼到終端機中 3. 按下 Enter 鍵執行
準備工作¶
-
打開 瀏覽器 您會看到:瀏覽器開啟
-
貼上 以下網址並按 Enter:
https://console.cloud.google.com/projectcreate?hl=zh-tw[!NOTE] 此連結可直接進入新增專案頁面。登入 Google 帳號後(若未登入)即可開始建立新 Project。
Step 1:建立 Google Cloud Project¶
Step 1.1:建立新 Project¶
根據之前的準備工作,您已進入新增專案頁面。現在完成以下步驟:
-
輸入 Project 名稱(例如
TimeCompass) -
選擇 「父項資源」(可選)
-
點擊 「建立」按鈕 您會看到:系統建立專案中,可能需要數秒時間,之後會自動返回 Console 首頁並顯示專案列表
Step 1.2:選取新建立的 Project¶
建立完成後,系統會自動進入新 Project 的 Dashboard。
確認 頁面頂部顯示您的 Project 名稱 您會看到:Dashboard 已準備好進行下一步
如果頁面沒有自動跳轉,您會看到「選取專案」列表,列出所有已建立的專案。
-
在列表中找到 您剛建立的專案(例如
TimeCompass) 您會看到:列表顯示新建立的專案在頂部或按時間排序 -
點擊該專案 您會看到:系統切換至該 Project,自動跳轉至 Project Dashboard(啟用 API)
Step 1.3:開啟導覽選單¶
在設定 API 前,需要進入 Google Cloud Console 的服務選單。
-
點擊 頁面左上角的 選單(≡ 圖示)
-
確認 左側邊欄已展開 您會看到:可以看到「APIs & Services」等選項
Step 1.4:進入 API 程式庫¶
在左側邊欄中找到並進入 API 程式庫。
-
在左側邊欄找到並點擊 「API 和服務」 您會看到:展開 API 和服務的子選單
-
點擊 「程式庫」 您會看到:進入 API 程式庫頁面,顯示所有可用的 Google API 列表
Step 1.5:啟用必要 API¶
-
進入 「APIs & Services」→「Library」(或直接搜尋) 您會看到:頁面顯示 API 庫藏類別列表
-
搜尋並啟用 以下三個 API(逐個執行步驟 1.5.1 ~ 1.5.3):
Step 1.5.1:啟用 Google Tasks API¶
-
在搜尋框中輸入 「tasks」 您會看到:搜尋框顯示您輸入的關鍵字
-
按下 Enter 鍵 您會看到:搜尋結果列表顯示 Tasks 相關的 API
- 點擊 「Google Tasks API」卡片 您會看到:進入 Google Tasks API 詳細頁面
- 點擊 「啟用」或「ENABLE」按鈕 您會看到:API 狀態變為「已啟用」或「API is enabled」
- 返回 API 程式庫 以啟用下一個 API
點擊 頁面左上角的返回箭頭 ← 您會看到:返回 API 程式庫,準備啟用下一個 API
Step 1.5.2:啟用 Google Calendar API¶
-
在搜尋框中輸入 「calendar」 您會看到:搜尋框顯示您輸入的關鍵字
-
按下 Enter 鍵 您會看到:搜尋結果列表顯示 Calendar 相關的 API
-
點擊 「Google Calendar API」卡片 您會看到:進入 Google Calendar API 詳細頁面
-
點擊 「啟用」或「ENABLE」按鈕 您會看到:API 狀態變為「已啟用」或「API is enabled」
-
返回 API 程式庫 以啟用下一個 API 點擊 頁面左上角的返回箭頭 ← 您會看到:返回 API 程式庫
Step 2:設定 Google Auth Platform(OAuth 同意畫面)¶
Step 2.1:進入 OAuth 同意畫面設定¶
- 進入 「APIs & Services」→「OAuth consent screen」 您會看到:頁面顯示 OAuth consent 設定表單
Step 2.2:完成 App 資訊(Branding)¶
- 在 「App information」區塊填入:
- 應用程式名稱 (App name):例如
time-compass-desktop - 使用者支援電子郵件 (User support email):您的支援郵箱(例如
skywind5487@gmail.com)
您會看到:表單顯示您填入的應用程式名稱和支援郵箱
-
在 「App Logo」(選用)上傳 logo(若無可略過)
-
滾動至 「Developer contact information」,填入您的郵箱 您會看到:表單頂部區塊完成,無紅色警告
-
點擊 「下一步」按鈕繼續設定 您會看到:進入 OAuth 同意畫面設定的下一步驟(目標對象)
Step 2.3:選擇 User Type(Audience)¶
- 選擇 OAuth consent screen 類型: 請選擇選項 B:外部 (External)(推薦公開發佈)
- 應用可給任何 Google 帳號使用
- 點擊 「下一步」按鈕 您會看到:頁面前進至下一個設定區塊
Step 2.3a:填入開發者聯絡資訊¶
- 在 「聯絡資訊」(Contact Information) 區塊填入:
- 電子郵件地址 (Email address):您的開發者郵箱(例如
skywind5487@gmail.com)
您會看到:聯絡電子郵件已填入,Google 會通過此電子郵件通知應用異動資訊
- 點擊 「下一步」按鈕 您會看到:進入下一個設定區塊
Step 2.3b:同意服務條款並完成設定¶
- 勾選 「我同意《Google API 服務:使用者資料政策》」
您會看到:複選框被勾選
- 點擊 「繼續」按鈕 您會看到:OAuth 同意畫面設定完成,頁面返回 APIs & Services 首頁
Step 2.4:加入 Test Users¶
- 往下滑動 到「測試使用者」(Test Users) 區塊
您會看到:看到「測試使用者」區塊,其中有「+ Add users」按鈕
-
點擊 「+ Add users」按鈕 您會看到:彈出對話框,要求輸入電子郵件地址
-
輸入 您的 Google 帳號郵箱(例如
skywind5487@gmail.com) 您會看到:郵箱被加入 Test Users 清單
補充:輸入完成後請點擊左下角「儲存」,再按左上角「X」關閉視窗。
Step 3:建立 OAuth Client(Web application)¶
Step 3.1:進入 Credentials 設定¶
- 進入 「APIs & Services」→「Credentials」 您會看到:Credentials 頁面開啟,可見「CREATE CREDENTIALS」按鈕
補充:若介面顯示為「Google Auth Platform」,請先點左側「用戶端」,再點上方「+ 建立用戶端」。
Step 3.2:建立 OAuth Client¶
-
確認 OAuth 同意畫面設定已完成 您會看到:頁面顯示所有設定步驟已完成(應用程式資訊 ✓、目標對象 ✓、聯絡資訊 ✓、完成 ✓)
-
點擊 「建立」按鈕 您會看到:進入建立 OAuth Client 的選項選擇
Step 3.3:選擇 Application Type¶
- 在 「Application type」區塊選擇 「Web application(網頁應用程式)」 您會看到:表單展開,顯示「Name」與 redirect URI 相關欄位
補充:先選擇「網頁應用程式」,再填入易辨識的名稱(例如 time_compass_local)。
補充:名稱填好後請往下滑,會看到「已授權的 JavaScript 來源」與「已授權的重新導向 URI」兩個區塊。
Step 3.4:設定 Authorized Redirect URIs¶
-
在 「Authorized JavaScript origins」加入:
您會看到:四個 Origin 都被加入清單http://127.0.0.1:8788 http://localhost:8788 http://127.0.0.1:8000 http://localhost:8000 -
在 「Authorized redirect URIs」加入:
您會看到:四個 callback URI 都被加入清單http://127.0.0.1:8788/auth http://localhost:8788/auth http://127.0.0.1:8000/auth http://localhost:8000/auth
補充:
- 8788:MCP 與後端 OAuth runtime 使用
- 8000:Gradio 介面使用
[!IMPORTANT] 本專案 runtime callback 路徑是
/auth。若 URI 不含/auth,會出現redirect_uri_mismatch。
- 點擊 「CREATE」建立 OAuth Client
您會看到:建立成功,頁面顯示
CLIENT ID與CLIENT SECRET
Step 3.5:複製憑證(建議下載 JSON 備份)¶
- 在 OAuth Client 詳細頁面或彈窗中:
- 複製
CLIENT ID - 複製
CLIENT SECRET
補充:請複製 CLIENT ID 與 CLIENT SECRET(建議直接跳到下一步填入 .env),並點擊「下載 JSON」做備份。
補充:開啟 JSON 後可先找 client_id 欄位;若內容過長顯示不完整,請向右捲動確認完整字串。
補充:同一份 JSON 也可找到 client_secret;此值屬於敏感資訊,請勿外流。
- 暫存 這兩個值(下步驟會填入
.env) 您會看到:您的剪貼簿中已複製 Client ID 和 Secret
[!IMPORTANT] 安全提醒 -
CLIENT SECRET是敏感資訊,不要分享給他人或上傳到版本控制 - 若不慎洩露,請立即在 Google Cloud Console 重新刪除並建立此 OAuth Client
Step 4:配置環境變數¶
現在您已取得所有必要的憑證,接下來將它們填入 .env 檔案。
Step 4.1:開啟 .env 檔案¶
-
進入 專案根目錄
您會看到:終端機顯示當前目錄cd time_compass # 或您的專案目錄路徑 -
開啟
.env檔案(用任意文本編輯器,例如 VSCode、Notepad++) 您會看到:.env檔案在編輯器中開啟
Step 4.2:填入 Google OAuth 憑證¶
-
找到 或新增以下行:
GOOGLE_CLIENT_ID=你在Step_3複製的_client_id GOOGLE_CLIENT_SECRET=你在Step_3複製的_client_secret -
將
你在Step_3複製的_client_id替換為實際的 Client ID 您會看到:該行顯示您的 Client ID -
將
你在Step_3複製的_client_secret替換為實際的 Client Secret 您會看到:該行顯示您的 Client Secret
Step 4.3:儲存 .env¶
- 按下 Ctrl+S(Windows / Linux)或 Cmd+S(macOS) 您會看到:編輯器標題列不再顯示「*」或「改變」標記,表示檔案已儲存
完成後檢查清單¶
本步驟完成後,請確認:
- [ ] Google Cloud Project 已建立(Project ID 已記錄)
- [ ] 三個 API 都已啟用:Google Tasks、Google Calendar、Gmail
- [ ] OAuth 同意畫面已完成設定
- [ ] OAuth Client(Web application)已建立
- [ ]
.env檔案中有以下兩個環境變數(且值不為空): - [ ]
GOOGLE_CLIENT_ID - [ ]
GOOGLE_CLIENT_SECRET - [ ] 您未將
.env或憑證洩露給他人
下一步:
前往 OAuth 驗證流程,測試您的 OAuth 設定是否正確,並了解如何初始化授權流程。
官方文檔參考¶
若您遇到問題或需要更深入的理解,以下官方文檔提供完整資訊:
-
OAuth 同意畫面設定 https://developers.google.com/workspace/guides/configure-oauth-consent
-
Web Server OAuth 2.0(Web application) https://developers.google.com/identity/protocols/oauth2/web-server
-
Google Calendar API Auth https://developers.google.com/workspace/calendar/api/auth
-
Google Tasks API Auth https://developers.google.com/tasks/auth
-
Gmail API Auth https://developers.google.com/gmail/api/auth
架構決策紀錄 (ADR)
ADR 0001: 將 Google Calendar / Tasks / Moodle 核心邏輯寫成 Library¶
Context (背景脈絡)¶
在專案初期與後續演進中,Time Compass 一直面臨多重存取點的挑戰。我們同時有 MCP 伺服器、過去的 FastAPI / Gradio 端點,以及負責登入的 OAuth Server 需要共用抓取日曆與作業的邏輯。
若將這些抓取與資料清洗邏輯直接綁定在 FastAPI Router 或是 MCP Tool 函數內,會導致程式碼高度耦合、難以測試,並且阻礙架構的演進(如我們後來從 Web Server 轉移到 MCP 擴充套件)。
Decision (技術決策)¶
核心的日曆、任務與 Moodle 爬蟲邏輯,必須被獨立出來作為純粹的 Python Library (src/time_compass/integrations/),並提供標準的介面。
它們:
1. 絕不包含任何 FastAPI Request/Response 或是 MCP 工具的特定型別綁定。
2. 統一回傳自定義的 Pydantic Result 或 Domain Models。
3. 把所有的網路與業務邏輯都在這層消化完畢,外部只需負責呼叫函數。
Consequences (決策結果)¶
Positive (優點)¶
- 極高的可重用性:無論是寫一支單純的 Python script、接上 MCP 工具,或是未來重回 API Server,這包 Library 都能無縫轉移。
- 測試隔離:將複雜的網路通訊與 Token 處理封裝後,測試單元變得極為純粹(可大量引入
tests/snapshots機制)。 - 架構彈性:讓我們無痛度過從 Gradio 到 MCP 的典範轉移。
Negative (缺點)¶
- 初期抽象成本高:為了解耦,我們必須設計多個額外的 Data Flow 層級(Raw Models -> Internal -> Read Models)。
- 邊界設計麻煩:開發新功能時,必須嚴格自律,思考這段程式碼隸屬於 "Library" 還是 "Agent Interface" (MCP Tool)。
ADR 0002: 從獨立 Web App 到 MCP (Model Context Protocol) 擴充的典範轉移¶
Context (背景脈絡)¶
Time Compass 第一版 (v1) 是使用 Gradio 作為前端、DSPy 作為 LLM Pipeline、FastAPI 為後端的龐大獨立應用程式 (Standalone App)。 這套架構運作良好,但存在四個致命痛點: 1. 啟動摩擦力高:學生壓力大時,要求他們開啟全新網頁、登入、生成長篇報告,門檻過高。 2. 對話狀態負擔:後端需要花費大量程式碼維護使用者的對話歷史 (Session State)、使用者管理等「非核心排程功能」。 3. 高昂 Token 成本:所有對話都需消耗部署者或使用者輸入的 API Key 額度。 4. 難以與 IDE 整合:開發者在寫 Code 時焦慮,需要立刻排程,獨立網頁無法無縫嵌入工作流中。
Decision (技術決策)¶
全面揚棄獨立 Web 服務與 DSPy 代理層,將 Time Compass 降級重構為一個遵守 Model Context Protocol (MCP) 的 Tool Provider。 所有視覺化呈現退居第二線,透過 MCP 的工具呼叫啟動微型前端 (Planner Studio)。
Consequences (決策結果)¶
Positive (優點)¶
- 零摩擦力體驗:系統直接寄生於 Cursor / Claude 等現存的 AI IDE。使用者在原本對話的主介面中即可下令排程。
- 狀態管理外包:完全省去了維護對話歷史與 Session 的程式碼,這些通通交給了 MCP Host (如 Claude) 負責。
- 成本轉移:消耗的是使用者本身 IDE/AI 服務的訂閱額度,而非依賴本專案維護的免費 API_KEY。
- 更精純的專注力:整個專案得以砍掉 40% 處理 UI 與狀態的冗餘程式碼,全心致力於資料流處理與排程演算法 (Library 端)。
Negative (缺點)¶
- 受限於宿主能力:使用體驗高度取決於 MCP Host 的實作 (例如某些 Host 無法渲染複雜介面,故我們開發了 Planner Studio 進行彌補)。
- 部署難度增加:MCP 對於非技術開發者的初始安裝相對陌生,需要依賴 Node.js (
npx) 與命令列介面運作。
ADR 0003: 以 TOON 格式取代 JSON 的語境壓縮策略¶
Status¶
Accepted (2026-02-27)
Context (背景脈絡)¶
當系統需要將最近一個月甚至半年的 Google Calendar 事件與 Google Tasks 任務餵給 AI 進行分析時,傳統的作法是直接將 API 的回傳結果以 JSON 字串格式塞進 Prompt 中。
這帶來兩個嚴重的問題:
1. Context Window 暴漲:JSON 中大量重複性字詞 (如 "summary", "description", "start_time") 與嵌套括號佔據了近半的 Token。
2. 推理速度下降與成本增加:過長的前置 Context 會顯著拖慢大語言模型 (LLM) 的首字回應時間 (TTFT),同時也直接推高了輸入成本。
根據 scripts/analyze_toon_compression.py 的精確分析,傳統 JSON 在處理大規模行程時會產生極高的無效負擔。
Decision (技術決策)¶
實作自定義的 Token-Oriented Object Notation (TOON) 格式,攔截所有傳向 MCP Host 的資料輸出並進行精簡編碼。
此格式核心機制包含:
1. Schema-less Header:標題行定義結構,消除欄位 Key 的重複出現。
2. 外部索引化 (Indexing):地點、重複規則提取至外部索引表,內部使用 L1, R1 等短標籤參照。
3. 日期元件化 (Date Decompose):將 ISO DateTime 分解為最小整數元件,如 st_d, st_wd, st_hm。
4. 語義分組 (Semantic Grouping):按月份分組(Calendar)或按狀態/截止月份細分(Tasks),並建立父子任務樹 (Parent Tree)。
Consequences (決策結果)¶
Positive (優點)¶
- 極致壓縮率:實測 Token 壓縮率高達 83.7% (Chars 壓縮率 90.5%)。來源見 TOON_STATS_REPORT.md。
- 資訊密度大幅提升:同樣的 Context Window 下,AI 能夠處理約 6.1 倍 的行程資料。
- 人類可讀性保良好:層次化的 YAML 式縮排讓開發者在 Debug 時仍能快速追蹤資料分布,優於扁平的壓縮格式。
- 減少注意力渙散:模型專注於資料本身而非冗餘的
{}與引號,提高了排程生成準確率。
Negative (缺點)¶
- 維護成本:每次擴增新的資料結構(例如引入新的集成來源時),都必須維護對應的 Encoder 邏輯。
相關文件¶
ADR 0004: 採用 Capture-First 的 TDD 測試驅動策略¶
Context (背景脈絡)¶
在開發階段,專案高度依賴外部甚至閉源的生態系 API(Google Calendar, Moodle)。如果針對每一次的 Code Change 都要真實發出網路請求,會導致: 1. 測試極度緩慢且不穩定(網路延遲、登入失效)。 2. 無法模擬複雜的極端情況(如某天排入 20 個事件的碰撞情境,要在真實 Calendar 裡建立這些假事件非常痛苦)。 而如果由開發者「自己手捏 JSON Mock」,又往往會出錯,導致寫出來的測試通過了,串接到真實 API 卻掛掉(因為自己捏的 Mock 與真實回傳格式有落差)。
Decision (技術決策)¶
強制推行 Capture-First 的測試與除錯哲學。
- 攔截與快照真相 (Snapshot Truth): 我們區分兩種層級的離線資料:
tests/snapshots/(測試快照):包含 API 回傳的最原始、未經清洗的 Raw JSON。這是用於 Unit Test 與 Regression Test 的唯一可信來源。assets/fixtures/(展示用 Mock):這是經過清洗、脫敏處理的資料,專門提供給MCP_DEV_MODE作為 Demo 或前端渲染驗證使用。- 基於數據驅動測試: 所有的資料流 (Data-flow) 與領域邏輯轉換測試,都必須讀取這些快照檔案作為輸入,不再進行任何網路請求。
Consequences (決策結果)¶
Positive (優點)¶
- 光速的回饋迴圈:執行跑過所有的單元測試套件只需數百毫秒,極大地加速了開發效率。
- 零憑證開發體驗 (Dev Mode):這套架構催生了
MCP_DEV_MODE=1的誕生。使得評審、設計師或新進開發者可以在完全沒有授權 Token 的情況下,透過讀取本機快照完成全流程式的檢驗與 UI 開發。 - 消解 Mock 幻覺:消除了手動捏造 Mock 所帶來的結構不一致型別錯誤風險。
Negative (缺點)¶
- 外部 API 變更盲點:如果 Google 或臺科大 Moodle 擅自更改了真實 API 的回傳欄位,基於舊版截圖的測試會全部綠色通過,無法提早發出警報(這能透過定期執行 Live Test 緩解)。
- 快照檔案膨脹:長期開發下來,存放 Snapshot 的目錄會堆積許多 JSON 檔案,需建立定期清理或命名的管理準則。
Related References (相關參考)¶
ADR 0005: 認知負荷驅動的代理提示詞架構 (Cognitive Load-Driven Prompts)¶
Context (背景脈絡)¶
在時間管理工具中,使用者通常處於兩種狀態:(1) 目標模糊且充滿焦慮,(2) 目標明確但不知如何切割時段。 傳統的 Chatbot 或排程系統常常要求使用者填寫大量表單,或是直接丟出一個開放式的 "How can I help you schedule?",這對處於壓力下的學生而言,會產生極高的認知負荷(Cognitive Load),導致使用者放棄排程。
Decision (技術決策)¶
Time Compass 的核心提示詞 (Prompts) 與路由 (Router) 邏輯,必須嚴格遵守認知負荷理論 (CLT) 與葉杜二式法則 (Yerkes-Dodson Law)。
具體實作規範: 1. Smart Routing & Funnel:系統必須根據意圖清晰度分為 L1 (模稜兩可)、L2 (方案發想)、L3 (明確可排) 三個層級,並自動派發給對應的 Prompt 處理。 2. 微工作限制:任何落地的行動任務 (Task) 被強迫分解在 60 分鐘以內。超過 60 分鐘,Prompt 必須主動提議拆解。 3. 最小阻力探詢:當資訊不足需要追問時 (AskQuestion),絕對禁止丟出開放式問卷。必須將缺失轉化為 3~5 題選擇題,並強制包含「我不確定/我不知道」選項。 4. 強制暫停機制:在進入最終的行事曆寫入 (L3) 之前,系統必須暫停並詢問使用者「這樣的拆解你覺得可以嗎?」,防止暴走式的 AI 直接塞滿行事曆。
Consequences (決策結果)¶
Positive (優點)¶
- 降低焦慮感:使用者不再被海量選項與開放性問題逼退。
- 高啟動率:精準的 60 分鐘拆解與 3~5 題的選擇,讓使用者有「現在立刻可以做一件小事」的掌控感。
- 防止幻覺破壞排程:強制暫停與 missing info 標記機制,確保寫入行事曆的資料都是經過使用者本人驗證的。
Negative (缺點)¶
- 流程拉長:比起那些「一句話直接排滿行事曆」的粗暴 AI,我們的對話輪次 (Turns) 會比較多。
- Prompt 維護難度高:要維持「溫暖且有界限」的語氣,Prompt 非常敏感,一點字詞修改可能導致 System Prompt 出現指責式的強硬語氣。
ADR 0006: 擁抱 Functional Programming 與 Rust-Style 錯誤處理¶
Context (背景脈絡)¶
Time Compass 需要處理複雜的日曆事件轉換、時區換算與 Moodle 作業解析。
在傳統的 Python 開發中,往往會建立龐大的 CalendarManager 或 TaskService 等帶有內部狀態 (Internal State) 的大類別 (God Classes)。同時,在處理可能失敗的 API 呼叫或資料清洗時,也常會看到深不見底的 try-catch 巢狀結構。
這導致測試非常難寫,因為要 mock 一整個物件的狀態,且難以預測例外 (Exception) 會在流程的哪一層爆開。
Decision (技術決策)¶
在核心業務領域 (Domain) 與資料轉換層,強制採用 Functional Programming (FP) 風格;在錯誤處理上嘗試貼近 Rust 的 Result 模式。
具體實踐:
1. 無狀態純函數 (Pure Functions):資料的轉換(如 from_dict, to_toon_calendar)全數都是宣告為獨立的純函數,不依賴全局變數或物件屬性。
- 例外放寬:只有在直接接觸網路 I/O(如 FastAPI Router, MCP 的 Server 或 Provider)的地方,才允許定義 Class 來保留連線狀態。
2. Result Pattern:不隨意丟出 Exception 讓外層去接。當執行步驟有預期內的失敗風險時,函數應該返回明確的結果結構(如 ok, error 狀態碼或 Union Types),迫使呼叫者在當下立刻處理這條分支。
Consequences (決策結果)¶
Positive (優點)¶
- 極致的可測試性:因為所有的資料轉換都是無副作用的函數,我們可以非常輕易地利用
tests/snapshots的輸入輸出來進行驗證,完全不需建立複雜的 mock 實例。 - Error Boundary 清晰:將例外阻擋在網路邊界,業務邏輯內都是可預期的資料流,降低 Debug 追蹤呼叫堆疊 (Stack Trace) 的痛苦。
Current Tech Debt (當前技術債)¶
async_client 的例外處理不一致性:
目前在 async_client 層級尚未完全貫徹 Result Pattern。
- 下游 Client 仍使用 raise 丟出錯誤。
- 這導致中游的 async_core 必須使用 try-catch 來攔截這些例外,再重新封裝回 Result 以供給 batch 處理。
- 後續改進預期:應將 Client 直接改為回傳 Result 物件,徹底移除 try-catch 的尷尬混合層級。
Negative (缺點)¶
- 不符合傳統 Python 開發習慣:習慣了大量 OOP 的 Python 工程師在初次看到這些模組時,可能會覺得缺乏封裝。
- 型別註記繁瑣:為了達成 Result 模式,我們大量使用了 Pydantic 模型與 Union 型別,這會拉長函數的簽名 (Signature)。
ADR 0007: 反規格文件驅動 (Anti-Spec-Driven Development) 與以碼為首¶
Context (背景脈絡)¶
在初版開發期間(特別是 docs/time_compass 這個充滿大量文件的時代),專案曾嘗試導入 Spec-Driven Development (SDD)。
理念是要求開發者與 AI 先把模組中每一個細節、每一個函數的預期輸入輸出(包含詳細的屬性欄位)以 Markdown 文件定義得清清楚楚,然後再開始寫 Code。
但很快我們發現,由於這是一個高度實驗性質的新架構(需要與 Google API 和複雜的 Moodle 爬蟲打交道),我們幾乎每天都在重構 API 回傳的結構與欄位。這導致先寫好的 SDD 文件在幾天內就徹底 Out-of-date(過期),甚至開始誤導後續接手的開發者。
Decision (技術決策)¶
摒棄細微節點的文件驅動開發 (Anti-SDD),確立「程式碼才是唯一絕對的事實來源 (Single Source of Truth)」,輔以高階架構圖與 ADR。
具體規則:
1. 不寫微觀 Spec 文件:不再於外部 Markdown 檔案中定義某個特定函數的入參出參。這些內容必須直接寫在 Python 的 Type Hints (型別提示) 以及 Pydantic Models 裏頭。
2. 註解寫「為什麼」而非「做什麼」:程式碼的 JSDoc/TSDoc 或 Python Docstring 必須專注於解釋這段邏輯的意圖(Why),而非重複描述實作步驟(What)。
3. 高階索引與資料流圖:我們唯獨保留且重視的是系統架構層次的文件(例如 docs/architecture/data-flow.md 中的 Mermaid 圖表),用以說明模組之間是如何傳遞資料的,但不定義內部細節。
Consequences (決策結果)¶
Positive (優點)¶
- 永遠不會過期的文件:藉由依賴 Code 作為規格,並搭配靜態型別檢查工具(如 mypy/pyright),這份「規格」是會被編譯器保證正確的。
- 開發速度提升:開發者 (含 AI) 不必再陷入「改動一行 Code 需要同步更新三份 Spec Markdown 文件」的文書泥淖。
- 避免 AI 產生幻覺:當 AI 開發者直接讀取原始碼,而非閱讀一份已經過期兩週的文件時,產出的程式碼準確率大幅提升。
Negative (缺點)¶
- 新人初期理解門檻較高:缺少了一份「步步拆解」的冗長規格書,新開發者需要具備循著
models.py閱讀型別定義,以及 tracing data flow 的能力。然而這也是我們透過強制制定systematic-debugging技能建立 Data-flow Routing & Inspection Protocol (DRIP) 的原因。
ADR 0008: 引入 Dev Mode 實現零憑證展示與快速開發¶
Context (背景脈絡)¶
在專案準備比賽展示與多人協作時,依賴真實的 Google OAuth 流程會產生極大的阻礙: 1. 展示風險:評審可能沒有或不願授權其 Google 帳號,導致 Demo 無法進行。 2. 開發摩擦:純前端或 UX 設計師在調整 Planner Studio 時,不應被迫具備 Google Cloud Console 的設定權限。 3. 資料隱私:不應使用開發者的真實個人行程進行錄影或截圖展示。
Decision (技術決策)¶
在系統底層實作一套受環境變數 MCP_DEV_MODE 驅動的數據攔截層。
- 靜態資料驅動:當
MCP_DEV_MODE=1時,所有整合層 (Integrations) 的 API 呼叫會被重新導向至讀取assets/fixtures/內的本地 JSON 檔案。 - 時間錨點模擬:由於 fixture 資料是靜態的,我們實作了「時間平移邏輯」,將當前系統時間映射至 Mock 資料的基準點(如 2025-11-15),確保 UI 的「今日」永遠有資料顯示。
- 前端一致性:對外暴露的 MCP Tools 介面保持不變,使 Host (Claude/Cursor) 無法察覺背後是 Mock 資料,保證了對話流的真實性。
Consequences (決策結果)¶
Positive (優點)¶
- 完美的 Demo 體驗:任何人只需安裝 Python 即可執行完整的排程流程,無需設定任何 API Key。
- 開發效率:允許在離線狀態下進行視覺化與 AI 提示詞的快速疊代。
- 解耦測試:提供了一套比 Snapshot 更高層級的「系統功能驗證」測資。
Negative (缺點)¶
- 維護雙重邏輯:在
async_core等地方引入了if is_dev_mode()的分支,增加了代碼複雜度與技術債風險。 - 測資過期:如果系統架構變動,必須同步手動更新
assets/fixtures/,否則 Demo 會破圖。
ADR 0009: Collector-Driven Streaming Contract 與 Typing 極簡化¶
Context (背景脈絡)¶
目前串流渲染邏輯同時分散於多處(collector、typewriter 狀態機、UI helper),導致以下問題: 1. 職責混雜:欄位切換、等待動畫、字元節奏、結束判斷分散在不同層,維護時容易漂移。 2. 雙軌邏輯:存在多套近似的串流處理流程,調整動畫或時序時容易出現「一處改了、另一處沒改」的回歸。 3. 重構目標不清:當前主要痛點是「打字機體驗穩定性」,但若同時擴充 structured streaming(additional outputs 增量更新)會引入不必要複雜度。
Decision Drivers (決策準則)¶
本次對話中,實際用以下準則收斂方案:
1. 先修好打字機:優先解決 typing 體驗與穩定性,不擴充功能面。
2. 單一真相來源:欄位切換、等待中、結束時機不應分散決策。
3. 避免重構擴散:additional_outputs 先維持現況 final-only。
4. 可觀測且可除錯:終止語義不能完全依賴「沉默」推測。
Options Considered (討論過的方案)¶
Option A: 完全無終止訊號(沉默即結束)¶
- 內容:不送
DONE/ERROR,typing 靠 grace/idle timeout 判斷結束。 - 優點:事件最少,typing 心智模型簡單。
- 缺點:無法精準區分 done/error;監控與除錯訊號弱;網路抖動可能誤判完成。
- 結論:Rejected(不採用)。可作 fallback,不應是主終止機制。
Option B: CHUNK-only(用 field 變化推斷欄位切換)¶
- 內容:不送
FIELD_START,typing 看到CHUNK.field改變就自動切段。 - 優點:協議極簡。
- 缺點:renderer 承擔推斷責任,corner case 增加,debug 可讀性變差。
- 結論:Rejected(不採用)。不符合「collector 決策、typing 執行」的邊界。
Option C: FIELD_START + CHUNK + DONE/ERROR(Collector 主導)¶
- 內容:欄位切換由 collector 宣告,typing 只依事件渲染。
- 優點:語義清楚、責任邊界明確、容易測試與追蹤。
- 缺點:事件數比極簡方案多。
- 結論:Accepted(採用)。
Option D: Collector 直接輸出最終格式化字串(不保留欄位語義)¶
- 內容:typing 只 append 字元,collector 負責標籤與 Markdown 組字串。
- 優點:typing 最純、最簡單。
- 缺點:collector 與 UI 表現強耦合;改版面會牽動 collector;不利未來多視圖。
- 結論:Deferred(延後)。目前保留 presenter 層,避免過早綁死表現格式。
Discussion Outcome (對話結論)¶
- Typing 要極簡:主職責為「打字節奏 + grace timeout 防卡死」。
- 欄位切換決策放 collector:typing 不再自行推斷欄位。
- 需要終止訊號:採
DONE/ERROR作主結束,idle timeout 作保險。 - additional outputs 不擴 scope:先維持 final prediction only,不做 incremental structured channel。
Decision (技術決策)¶
採用 Collector-Driven 的事件契約,將 typing 收斂為極簡渲染器;additional outputs 維持 final-only。
1. 事件邊界¶
Collector 作為唯一事件來源,負責欄位切換與生命週期訊號。最小事件集合為:
- WAIT
- FIELD_START
- CHUNK
- DONE
- ERROR
2. 職責分層¶
- Collector:
- 發送等待中事件(WAIT)
- 決定欄位切換並發送 FIELD_START
- 發送 CHUNK/DONE/ERROR
- Presenter/Formatter:
- 將事件轉為 UI markdown 顯示(可持續演進,不綁 collector)
- Typing Renderer:
- 僅處理字元節奏與 grace timeout
- 不自行推斷欄位切換決策
3. Additional Outputs 策略¶
additional_outputs(例如任務 DataFrame)維持 final prediction only:
- 不做增量 structured streaming
- 不從 chat 字串反解析
- 由最終 prediction 提取 metadata 更新
Consequences (決策結果)¶
Positive (優點)¶
- 正本清源:collector 與 typing 的責任邊界清楚,重構與除錯更穩定。
- 降低回歸風險:等待動畫、欄位切換與結束時序由單一來源掌控。
- 聚焦當前痛點:先修好打字機體驗,不引入 additional outputs 增量化的額外複雜度。
- 保留擴充彈性:未來若要增量 structured channel,可在現有分層上新增,不需推倒重來。
Negative (缺點)¶
- 短期內仍有相容負擔:切換到單一路徑前,舊流程與新契約可能暫時共存。
- 事件模型變正式:初期需要補測試與文件,導入成本上升。
- 仍需 timeout 參數治理:idle/grace 設太短會誤收尾,設太長會體感卡頓。
Follow-up Notes (後續追蹤)¶
- 若未來需要 live structured UI,再開獨立 change 討論 incremental channel。
- 若要讓 collector 直接組 UI 字串,需先有明確多視圖需求再評估是否接受耦合成本。
ADR-0010: 文檔類資源統一架構¶
日期: 2026-03-02
提案人: Wei
狀態: 已採納
背景¶
Time Compass 的文檔資源散落在多個位置,缺乏統一的組織邏輯。開發者難以快速定位「我要查 X 功能怎麼做」的文檔。
現狀困境¶
docs/下有 ~125 個 Markdown 文檔,按目錄分散但層級混雜docs/reference/內有 21 個檔案,缺乏主題歸類- Prompts 系統雙源:
time_compass/domain/*/prompts/(舊 Gradio) 和src/time_compass/mcp/prompts/content/(新 MCP) - 根底目錄有過期檔案(README_OLD.md、測試輸出混在根目錄)
- 文檔索引(reference/README.md)存在但未充分發揮導航作用
決議¶
採用 主題式 (Topic-Based) + 多層級並行 的文檔架構,核心特質:
1. 文檔架構三層¶
Level 1: 獲取入門 (Getting Started)
│ ├─ docs/index.md (主首頁:「妳想做什麼」)
│ ├─ GUIDE.md (快速入門)
│ ├─ docs/explanation/* (概念與決策背景)
│ └─ docs/how-to/* (操作步驟)
│
Level 2: 按主題深入 (Topic-Based Reference)
│ └─ docs/reference/* (按功能域聚焦的技術深度)
│ ├─ README.md (主題選單)
│ ├─ DDD-ARCHITECTURE/ (領域驅動設計)
│ ├─ ERROR-HANDLING/ (錯誤處理框架)
│ ├─ MCP-TOOLS/ (MCP 工具實作)
│ ├─ FRONTEND/ (前端零框架設計)
│ └─ [其他主題...]
│
Level 3: 系統設計記錄 (Architectural Decisions)
│ ├─ docs/adr/* (架構決策紀錄)
│ ├─ docs/architecture/* (系統全景圖和資料流)
│ └─ docs/poster/* (願景與團隊故事)
2. 各層的責任¶
| 層級 | 目的 | 讀者 | 內容特性 |
|---|---|---|---|
| Level 1 | 新手通道 | 新開發者、架構師 | 「怎麼開始」、「為什麼」 |
| Level 2 | 主題深度 | 實作開發者 | 「我要做 X,相關檔案在哪」 |
| Level 3 | 決策鏈 | 決策者、架構師 | 「為什麼做了這個選擇」 |
3. Reference 的主題劃分¶
每個主題內部遵循 Diátaxis 四層(可選),但內容在同一主題目錄內:
reference/DDD-ARCHITECTURE/
├─ README.md (主題導航:EXPLANATION | HOW-TO | REFERENCE)
├─ EXPLANATION.md (C2-3: 為什麼四層?架構理念)
├─ HOW-TO-EXTEND.md (C3: 怎麼新增資料來源?step-by-step)
└─ MODEL-SPECS.md (C3: 詳細欄位、轉換規則、代碼映射)
關鍵:三份文件內都可導向代碼,避免跳出主題目錄。
4. Prompts 雙源並行系統¶
非 SOT (Single Source of Truth),而是雙源設計:
| 來源 | 架構版本 | 用途 | 維護者 |
|---|---|---|---|
time_compass/domain/*/prompts/*.md |
Gradio / DSPy | 舊架構提示詞、簽章定義 | 領域模組 |
src/time_compass/mcp/prompts/content/*.md |
MCP | 新 MCP 工具的系統提示詞 | MCP 模組 |
同步規則:
- 如果是核心概念(如「emotion」的定義),兩邊應一致
- 如果是實現細節(如 MCP-only 邏輯),兩邊可獨立
- 文檔化於 docs/reference/PROMPTS-SYNC-GUIDE.md
5. 檔案位置清單¶
文檔 (docs/)¶
docs/index.md- 主首頁(待更新)docs/GUIDE.md- 快速入門指南(保留)docs/adr/- 架構決策紀錄(9 份 + 本 ADR)docs/architecture/- 系統全景圖、資料流、DEV_MODE 決策docs/explanation/- 概念、架構隱喻、開發歷程、agent 能力docs/how-to/- 操作指南(Google OAuth、測試、雲端設置)docs/reference/- 主題式技術深度(主軸)README.md- 主題選單(待改造)DDD-ARCHITECTURE/- 四層轉換模型ERROR-HANDLING/- Railway Oriented ProgrammingMCP-TOOLS/- MCP 工具手冊FRONTEND/- 前端零框架設計- [待新增主題...]
docs/protocols/- 協議文檔docs/poster/- 團隊故事、策略docs/archive/- 歷史紀錄、過期文檔
根目錄¶
README.md- 專案總述(保留,链接到 docs/)GUIDE.md- 開發入門(與 docs/GUIDE.md 保持同步或重定向)
設計系統 (design-system/)¶
design-system/MASTER.md及其他 - UI 設計系統(保留)
代碼內嵌文檔¶
src/time_compass/domain/*/- 領域模組,包含 prompts、design 文檔src/time_compass/mcp/prompts/content/- MCP 工具的 Prompt 系統提示詞
測試與生成產物 (tests/, site/)¶
tests/output_result/- 測試輸出暫存(保留,自動清理)tests/snapshots/- API 快照 fixtures(保留)site/- MkDocs 生成的靜態網站(保留,git 版本控制)- 根目錄暫存檔案 (
test_*.py,test_*.txt) - 刪除
Assets¶
assets/- TOON 統計報告、API 樣本(保留,dev mode用)
改動計劃¶
Phase 1: 決策記錄 ✅¶
- [x] 寫 ADR-0010(本文)
Phase 2: 快速清理(30 分鐘)¶
- [ ] 刪除
test_versioning_logic.py、test_output.txt - [ ]
.gitignore確認:site/ 應保留;output_result 應是暫存(不版本控制)
Phase 3: README 與導航更新(1.5 小時)¶
- [ ] 改造
docs/reference/README.md- 主題選單形式 - [ ] 新建
docs/architecture/OVERVIEW.md- C1-C2 快速導航 - [ ] 更新
docs/index.md- 主首頁導航
Phase 4: Reference 主題分類(1 小時,只搬移)¶
- [ ] 建立各主題目錄(如 DDD-ARCHITECTURE/, MCP-TOOLS/ 等)
- [ ] 搬移現有檔案(不改內容)
- [ ] 新建各主題的 README.md(導航頁)
Phase 5: Prompts 系統記錄(可選,1 小時)¶
- [ ] 檔案
docs/reference/PROMPTS-SYNC-GUIDE.md - [ ] 記錄雙源配置
優勢¶
| 優勢 | 說明 |
|---|---|
| 新手友善 | docs/index → explanation → reference 清晰路徑 |
| 實作導向 | reference 是主軸,按「我要做 X」的真實需求分類 |
| 避免過度分層 | 不用 C1-C2-C3-C4 精確分層,而是按實際查詢模式 |
| 易於維護 | 主題內自洽,新增功能時清楚該放哪 |
| 靈活擴展 | Diátaxis 層 (EXPLANATION/HOW-TO/REFERENCE) 按需採用,非強制 |
| Prompts 實況 | 記錄「不是 SOT」的現實,避免同步誤會 |
風險與注意事項¶
| 風險 | 緩解方案 |
|---|---|
| 搬移檔案時舊連結破裂 | 提交 PR 時檢查 mkdocs.yml 自動對應;文件內部連結用相對路徑 |
| Prompts 雙源造成不一致 | 文檔化同步規則,考慮 CI check |
| 新手不知道進 reference 後該看哪個檔案 | 每個主題的 README.md 必須有清晰的「選擇路徑」 |
相關決議¶
- ADR-0003: TOON 格式
- ADR-0005: 認知負荷驅動的提示詞
- ADR-0006: 函數式 + Result Pattern
- ADR-0002: MCP over Gradio
後續追蹤¶
- Phase 2-4 預計 3-4 小時完成
- 完成後進行用戶測試:新開發者能否快速找到所需文檔
- 每季度檢查 reference/* 是否有新主題需要加入
架構設計
Time Compass 系統架構快速導覽¶
用途: 30 秒了解系統構成
對象: 新開發者、架構師、決策者
深入指南: → 各主題參考
C1: System Context(系統邊界)¶
Time Compass 是一個時程調度規劃助手,與三個外部系統通信:
graph TB
User["👤 使用者<br/>(網頁/AI助理)"]
TC["📱 Time Compass<br/>(調度規劃系統)"]
GC["Google Calendar"]
GT["Google Tasks"]
Moodle["Moodle 課程管理系統<br/>(爬蟲)"]
User -->|提問、拖放事件| TC
TC -->|查詢/創建| GC
TC -->|查詢/創建| GT
TC -->|爬蟲取資料| Moodle
GC -->|日程事件| TC
GT -->|工作清單| TC
Moodle -->|課程、截止日期| TC
C2: Containers(主要服務)¶
系統採用 MCP-First 並行雙入口架構,同時維持舊 Gradio 容器支援:
graph TB
subgraph "接入層"
MCP["🔌 MCP Server<br/>(新推薦)"]
Gradio["🎨 Gradio Web UI<br/>(遺留)"]
Planner["🎨 Planner Studio<br/>(微前端)"]
end
subgraph "業務邏輯層"
Core["⚙️ async_core<br/>"]
Integration["🔌 Integration Layer<br/>(Google, Moodle)"]
end
subgraph "資料層"
DB["💾 Cache"]
end
external["🌍 外部 API"]
Planner -->|MCP 工具呼叫| MCP
Gradio -->|HTTP Tab 互動| Core
MCP -->|編排工具、觸發任務| Core
Core -->|轉換、一致性檢查| Integration
Integration -->|API 呼叫| external
external -->|回應| Integration
Integration -->|更新| DB
Core -->|查詢| DB
各容器責任¶
| 容器 | 架構地位 | 職責 | 關鍵組件 | 啟動方式 |
|---|---|---|---|---|
| MCP Server | 🟢 推薦 | 暴露 15 個工具給 Claude/Cursor IDE | src/time_compass/mcp/server.py、15 個工具 |
uv run time-compass-mcp |
| Gradio Web UI | 🟡 並行支援 | 獨立網頁 Tab 式介面(port 8000) | src/time_compass/interface/server.py、5 個 Tab |
uv run time-compass-gradio |
| Planner Studio | 🟢 推薦 | 零框架縮放排程工具 | FullCalendar 6.1 + 自製縮放、衝突 detection | MCP 內 launch_planner_studio() 工具觸發 |
| async_core | 核心 | 編排 DDD、驅動 Prompts、統一錯誤處理 | integrations/async_core.py、Result Monad |
- |
| Integration Layer | 核心 | 統一 Google/Moodle API 呼叫、資料轉換管道 | integrations/{google_calendar, google_tasks, moodle}/async_core.py |
- |
| Data/Cache | 底層 | 事件、任務、課程資料快取 | 本地或 Google Cloud 存儲 | - |
架構地位說明: - 🟢 推薦:符合現代設計,計劃持續投資 - 🟡 並行支援:仍可用,與推薦並行,但不再主要特性開發 - 🔴 遺留:已停止維護(此系統無此類)
容器選擇指南¶
我是 AI 助理開發者 → 使用 MCP Server
我要手動管理排程 → 使用 Gradio Web UI 或 Planner Studio
我要整合新的資料來源 → 修改 Integration Layer
Gradio Web UI 工作流程(並行容器)¶
Gradio 是同步 Web 介面,提供 5 個互動 Tab,但重點在於與 Orchestrator 的協作。用戶輸入驅動完整的 AI 決策流水線:
graph TB
User["👤 使用者<br/>(瀏覽器輸入)"]
subgraph Gradio["🎨 Gradio Web UI (port 8000)"]
OAuth["🔐 OAuth<br/>Tab 1"]
Debug["🐛 Debug/Test<br/>Tab 2"]
GoogleTasks["✅ Google Tasks<br/>Tab 3"]
Scheduling["📅 Scheduling<br/>Tab 4"]
Moodle["📚 Moodle<br/>Tab 5"]
end
subgraph AI["🧠 AI 決策層 (Orchestrator)"]
Router["Router<br/>(決定流程)"]
Pipeline["Pipeline<br/>(多模組序列)"]
end
subgraph Backend["⚙️ Integration Layer<br/>(並行抓取)"]
Cal["Google Calendar"]
Task["Google Tasks"]
Mood["Moodle"]
end
User -->|提問或請求| Scheduling
Scheduling -->|user_input| Router
Router -->|pipeline決定| Pipeline
Backend -.->|並行抓取<br/>(Summary/Scheduling<br/>讀取)| Pipeline
Pipeline -->|EmotionSupport<br/>Summary<br/>Scheduling| StreamOutput["串流輸出"]
Scheduling -->|寫入<br/>(直接)| Task
StreamOutput -->|更新UI| Scheduling
Scheduling -.->|展示結果| User
OAuth -.->|管理 Token| OAuth
Debug -.->|測試 API| Debug
GoogleTasks -.->|列表檢視| GoogleTasks
Moodle -.->|爬蟲課程| Moodle
Gradio 中的 AI 互動流程¶
| 步驟 | 元件 | 職責 |
|---|---|---|
| 1. 用戶輸入 | Scheduling Tab | 接收自由文字提問 |
| 2. 路由決策 | Router Module | 分析意圖,決定要執行的 Pipeline(情緒支持 → 摘要 → 排程等) |
| 3. 並行抓取 | Integration Layer | 同時取 Google Calendar/Tasks/Moodle 資料(背景進行) |
| 4. 資料注入 | Summary/Scheduling 模組 | 將抓取的資料注入 InteractionContext |
| 5. 流程編排 | Pipeline Executor | 依序執行各模組,每個模組讀取 Context 中的資料 |
| 6. 串流回應 | streaming.py | 逐 chunk 推送結果回前端(格式:[Module:field] text) |
| 7. 寫入操作 | Scheduling Tab | 若用戶確認排程,直接寫入 Google Tasks |
| 8. UI 更新 | Gradio ChatInterface | 即時顯示 AI 回應與建議 |
Gradio 的 5 個 Tab¶
| Tab | 功能 | 資料流 | 核心操作 |
|---|---|---|---|
| 🔐 OAuth Login | Google OAuth 授權、token 管理 | 同步 | 初始授權 → Token 刷新 → 帳號切換 |
| 🐛 Debug/Test Request | 直接呼叫 API 測試、查看回應 | 同步 | 構造要求 → 執行 → 檢視原始回應 |
| ✅ Google Tasks | 管理 Google Tasks 工作列表 | 同步 | 列表查看 → 建立任務 → 更新狀態 → 刪除任務 |
| 📅 Scheduling & Planning | AI 輔助排程(核心) | 異步 + Pipeline | 輸入 → Router → Pipeline(讀 Integration 資料) → 寫 Google Tasks |
| 📚 Moodle Integration | 爬蟲取課程資訊、截止日期 | 同步爬蟲 | 帳號登入 → 爬蟲課程 → 檢視課程事件 |
Scheduling Tab 內的 AI 互動細節:
- 使用者提問 → Orchestrator.forward(user_input) 呼叫
- 並行背景:Integration Layer 同時抓取 Google Calendar、Google Tasks、Moodle 資料
- Router 模組決定 Pipeline:[EmotionSupport → Summary → Scheduling] 等序列
- 各模組接收 InteractionContext(含歷史、上下文、已抓取的時程資料)
- Summary 和 Scheduling 模組從 Context 讀取 Calendar/Tasks/Moodle 資料,無需再次 API 呼叫
- 串流回應逐 chunk yield,格式為 [ModuleName:FieldName] content
- 寫入時:用戶確認建議 → 直接呼叫 Google Tasks write API
- 前端即時更新 ChatInterface 顯示結果與建議
詳見:docs/time_compass/domain/orchestrator.md
Gradio 與 Integration Layer 的互動¶
Gradio 與 MCP 都是通過 Integration Layer 的統一 API 呼叫資料來源:
讀取操作:
├─ Gradio: async_get_all_tasks() / async_get_all_events() / scrape_moodle_events()
└─ MCP: 相同的 API(無框架耦合)
寫入操作:
├─ Gradio: async_task_insert()(Google Tasks 寫入)
└─ MCP: 相同的 API(透過工具呼叫)
核心相同性:Gradio 與 MCP 都使用 Integration Layer 中的統一 async 函數,無需關心底層 Batch API 細節。
Orchestrator 與 AI 決策層(C3 邏輯編排)¶
Gradio 和 MCP 都透過 Orchestrator 驅動 AI 決策。Orchestrator 的核心職責是協調多個 LLM 模組形成完整的推理流水線:
graph TB
UserInput["使用者輸入<br/>(自由文字)"]
Orch["Orchestrator<br/>(編排器)"]
Router["🧭 Router Module<br/>(意圖分析)"]
Pipeline["📊 Pipeline<br/>(決策序列)"]
Emotion["EmotionSupport<br/>(情緒陪伴)"]
Summary["Summary<br/>(活動回顧)"]
Sched["Scheduling<br>(排程建議)"]
Context["InteractionContext<br>(對話上下文 + 時程資料)"]
Integration["Integration Layer<br>(Google + Moodle 資料)"]
Output["final_reply<br>(整合回應)"]
UserInput -->|user_input| Orch
Orch -->|dialog_history| Router
Router -->|pipeline決定<br>(順序 & 模組)| Pipeline
Pipeline -->|分派| Emotion
Pipeline -->|分派| Summary
Pipeline -->|分派| Sched
Orch -->|構建| Context
Context -->|包含| Integration
Emotion -->|InteractionContext| Pipeline
Summary -->|InteractionContext| Pipeline
Sched -->|InteractionContext| Pipeline
Pipeline -->|聚合輸出| Output
Orchestrator 工作流程¶
- Router 分析意圖:根據
user_input決定 Pipeline 序列 -
例:用戶「這週很累」→ Pipeline =
[EmotionSupport → Summary → ask_question] -
構建 InteractionContext:收集對話歷史、時程資料、之前模組的輸出
-
依序執行 Pipeline:每個模組接收完整的 Context,回傳結構化輸出
-
優先級邏輯(由 Router 決策,不同情境不同):
- 純規劃場景 →
[Scheduling] - 迷茫/疲勞場景 →
[EmotionSupport → ask_question → draft_plan] -
總結場景 →
[Summary → draft_plan → draft_action] -
Streaming 輸出(Gradio 特性):邊執行邊 yield chunk,每個 chunk 標記模組與欄位:
[Router:pipeline_description] 我會幫你規劃... [EmotionSupport:output] 聽起來你這週... [Summary:summary_text] 過去三天的活動... [Scheduling:draft_schedule] 建議時段:...
Pipeline 的標準模組¶
| 模組 | 用途 | 輸入 | 輸出 |
|---|---|---|---|
| Router | 意圖分析與流程決策 | user_input, dialog_history | pipeline, pipeline_description |
| EmotionSupport | 心理陪伴(若需要) | InteractionContext | support_text, action_suggestion |
| Summary | 區間活動回顧 | InteractionContext + calendar/tasks 資料 | summary_text, insights |
| Scheduling | 排程建議與衝突檢測 | InteractionContext + 完整時程資料 | draft_schedule, risks, next_steps |
| ask_question | 澄清缺失資訊 | InteractionContext | questions, constraints, follow_up_suggestion |
詳細說明與流程圖:docs/time_compass/domain/orchestrator.md
Gradio 與 MCP 共享的 Orchestrator¶
兩個容器都使用相同的 Orchestrator,區別在:
- Gradio:同步 HTTP 請求 → orch.forward(input) → 串流回應
- MCP:工具調用 → get_time_context() + draft_plan_prompt() → 提示回傳
這是「多入口同邏輯」的典型設計。
Integration Layer(C3 實現細節)¶
三個整合模組¶
integrations/
├── common/ ← 共通層
│ ├── exceptions.py (8 種 GoogleError 定義)
│ ├── google_api_dispatcher.py (Batch API 分派器、HTTP Multipart 編碼)
│ └── ...
│
├── google_calendar/ ← Google Calendar 模組
│ ├── async_core.py (取日程、建事件、查詢時段)
│ └── models/
│ ├── models_raw.py (47+欄位,camelCase)
│ ├── models_read.py (datetime 保留,應用層最佳化)
│ └── models_toon.py (索引地點、重複規則、TOON 編碼)
│
├── google_tasks/ ← Google Tasks 模組
│ ├── async_core.py (取清單、建工作、狀態管理)
│ └── models/
│ ├── models_raw.py (18 欄位)
│ ├── models_read.py (Eager-cast 為字串)
│ └── models_toon.py (按狀態分組、TOON 編碼)
│
└── moodle/ ← Moodle 爬蟲模組
├── async_core.py (Web Scraper + OIDC + Selenium)
└── models/
├── models_raw.py (55+ 欄位)
├── models_internal.py (HTML 清洗、時區轉 UTC+8)
├── models_read.py (語意化狀態、8 欄位精簡)
└── ...
四層資料模型¶
每個模組都遵循 Raw → Internal → Read → TOON 轉換:
| 層 | 用途 | 例:Calendar |
|---|---|---|
| Raw | 100% API 映射,無修改 | GoogleEventRaw(47 欄位,camelCase) |
| Internal/Domain | 反腐層,清洗格式 | GoogleEventRead(datetime 物件保留) |
| Read | 應用層最佳化 | ToonCalendar(索引化地點、日期元件化) |
| TOON | 極致壓縮 | TOON 字串格式(Token 節省 83.7%) |
錯誤處理(Railway-Oriented)¶
所有操作返回 Result[T],不拋異常:
Result[ListEventsResponse] = Ok(events) | Err(GoogleAuthError(...))
8 種錯誤分類:
- GoogleAuthError (401/403) → 需用戶重新授權
- GoogleNetworkError (timeout) → 指數退避重試
- GoogleParseError (JSON) → 記錄,上報bug
- GoogleRateLimitError (429) → 可重試
- GoogleNotFoundError (404) → 忽略或提示用戶
- ...等
詳細說明: → ERROR-HANDLING
資料轉換流水線¶
graph LR
Raw["外部 API JSON<br/>(Raw Layer)"]
Internal["內部轉換<br/>(HTML 清洗、時區轉)<br/>(Internal Layer)"]
Read["讀取模型<br/>(應用層最佳化)<br/>(Read Layer)"]
TOON["TOON 格式<br/>(極致壓縮)<br/>83.7% token 節省"]
Raw -->|from_dict| Internal
Internal -->|to_read| Read
Read -->|safe_encode| TOON
詳細解釋: → DDD-ARCHITECTURE
C3: Integration 層(資料來源通訊)¶
時程規劃系統的關鍵:與三個外部資料源並行通信。
graph TB
subgraph Integration["🔌 Integration Layer"]
Common["共通層<br/>exceptions.py<br/>google_api_dispatcher.py<br/>http_batch_tool.py"]
subgraph Google["Google API (Batch 模式)"]
Cal["Google Calendar<br/>async_get_all_events()"]
Task["Google Tasks<br/>async_get_all_tasks()"]
end
Moodle["Moodle (Web 爬蟲)<br/>scrape_moodle_events()"]
Coord["🔥 頂級協調<br/>async_get_all_information_from_api()"]
end
Common -.->|支援| Google
Common -.->|支援| Moodle
Cal -->|結果| Coord
Task -->|結果| Coord
Moodle -->|結果| Coord
Coord -->|返回| ResourceContext["ResourceContext<br/>(三合一結果)"]
style Common fill:#fff3e0
style Google fill:#f3e5f5
style Moodle fill:#fce4ec
style Coord fill:#e1f5fe
Integration 層三大支柱¶
| 支柱 | 技術 | 特點 |
|---|---|---|
| Google APIs | Batch API + 非同步 Generator | 多個日曆/工作列表並行,自動分頁,速度快 |
| Moodle 爬蟲 | OIDC 登入 + Selenium Fallback | 課程資訊抓取,60s 超時保障 |
| 共通層 | Dispatcher + Multipart Builder | 統一例外體系、Type-Safe 回傳、結構化 Batch 請求 |
深入指南: - � 完整 C4 Model 文檔: ../reference/INTEGRATION/C4_MODEL.md(涵蓋 L1-L4 四層)
核心設計原則¶
Railway-Oriented Programming¶
所有操作使用 Result[T] 型別,明確表達成功/失敗,避免例外被吞掉。
深入了解: → ERROR-HANDLING
多源並行抓取¶
Google Calendar、Google Tasks、Moodle 並行執行,每個獨立失敗且有重試機制。
工具手冊: → MCP-TOOLS
零框架前端¶
Vanilla JavaScript,無打包步驟,<500ms 啟動。
實作指南: → FRONTEND
Prompts 系統設計(雙源非 SOT)¶
系統的 Prompt 定義採用非 SOT(Single Source of Truth)設計 - 由 Gradio 時代(domain/)和 MCP 時代(mcp/prompts/)各自維護一份:
domain/ (舊 DSPy/Gradio 時代)
├── summary/prompt/
│ ├── summary_tool.md ( LLM 工具簽名 )
│ └── summary_writer.md ( 活動回顧工具 )
├── schedule/prompt/
│ ├── router.md ( 路由邏輯 )
│ ├── draft_plan.md ( 初步規劃 )
│ ├── draft_action.md ( 可執行排程 )
│ └── ask_question.md ( 澄清問題 )
└── router/prompt/
└── ...
mcp/prompts/ (新 MCP 時代)
├── domain_prompts.py ( 函數定義 + 載入器 )
└── content/
├── summary_writer.md (↑ 對應 domain/ 版本)
├── emotion_support.md (新增,Gradio 無)
├── draft_plan_prompt.md
├── draft_action_prompt.md
├── scheduling_router_prompt.md
└── ...
為什麼非 SOT? - ✅ 獨立演進:Gradio 和 MCP 可各自迭代 Prompt,無需同步 - ✅ 已知差異:系統隨時意識到雙版本可能不同步,監控標記已設置 - ✅ 漸進遷移:無需一次性遷移所有 Prompt,可按需逐步更新
如何同步? 詳見 ADR-0010: 文檔類資源統一架構
想深入某個主題?¶
選擇妳的探索路徑:
| 我想... | 去這裡 |
|---|---|
| 理解四層資料轉換(Raw → Internal → Read → TOON) | DDD-ARCHITECTURE |
| 詳細了解 Integration 層(Google + Moodle 通信) | Integration C4 Model |
| 了解 Orchestrator 與 Router 如何運作 | docs/time_compass/domain/orchestrator.md、router.md |
| 學習各 Pipeline 模組的設計 | emotion.md、summary.md、scheduling/ |
| 了解串流如何工作(Gradio 特性) | docs/time_compass/domain/orchestrator.md |
| 了解錯誤處理怎麼做(為什麼用 Railway?) | ERROR-HANDLING |
| 學習 15 個 MCP 工具的設計與用法 | MCP-TOOLS |
| 看前端零框架怎麼運作(FullCalendar + 縮放) | FRONTEND |
| 查看整個系統的資料流圖 | data-flow.md |
| 了解開發模式設計決策 | DEV_MODE_GUIDE.md |
| 理解 Prompts 為什麼非 SOT 設計 | ADR-0010 |
推薦的新手閱讀順序¶
- 第一遍(15 分鐘):讀本頁,了解 C1-C2 邊界與容器
- 第二遍(30 分鐘):進 reference/README.md 選一個主題深入
- 第三遍(1 小時+):依據工作需要在各主題中深潜
往更上層看¶
想了解為什麼做了這些架構選擇?
→ Architecture Decision Records (ADR) - 從 ADR-0001 開始
想知道團隊的願景?
→ 願景與故事
Google Calendar 資料流¶
回到 資料流索引
層級結構¶
| 層 | 檔案 | 主要類別/函數 |
|---|---|---|
| Raw | models_raw.py | GoogleEventRaw (47+ fields), DateTimeRaw, FreeBusyQuery 等 |
| Read | models_read.py | GoogleEventRead (Contains title for UI compatibility) |
| Core | async_core.py | async_get_all_events(), async_get_calendar_events() |
| TOON | models_read.py | to_toon_calendar(), to_toon_event(), _build_location_index() |
| MCP | calendar_tools.py | get_all_calendar_events(), get_event_from_calendar(), list_calendars() |
轉換函數詳解¶
GoogleEventRead.from_dict(data: Dict) → GoogleEventRead¶
- 輸入:Google Calendar API 原始 JSON dict
- 做了什麼:
- 提取
start/end中的dateTime或date,用to_datetime()轉為 Python datetime - 將 datetime 透過
format_for_llm()轉為 UTC+8 的"YYYY-MM-DD HH:MM"字串 - 處理
reminders(覆寫 →popup:10m/ 預設 →default) - 處理
recurrence(若為 list 取第一個 RRULE 字串,並轉換為人類可讀格式) - 處理
originalStartTime(改期事件的原始起始時間) - 全天事件偵測:優先檢查
start.date欄位,次要檢查08:00邊界(雙重魯棒性) - Source ID:保留
id欄位以支持後續操作(如刪除、更新事件)
GoogleEventRead.from_raw_model(obj: Any) → GoogleEventRead¶
- 輸入:dict、
GoogleEventRead本身、或任何有id+summary屬性的物件 - 做了什麼:多態分派 — 自動判斷輸入類型並路由到
from_dict()或直接回傳
GoogleEventRead.from_raw(raw: Any) → GoogleEventRead¶
- ⚠️ Deprecated:相容舊程式碼的適配器,內部委派給
from_dict()或from_raw_model()
to_toon_event(event: GoogleEventRead, location_map, recurrence_map) → dict¶
- 做了什麼:將單一事件壓縮為 TOON 格式,包含
summary,st_hm,en_hm等,並處理全天事件邏輯。
to_toon_calendar(events, calendar_name, calendar_id) → dict¶
- 輸入:
List[GoogleEventRead]+ 日曆名稱/ID - 做了什麼:
- 呼叫
_build_location_index()建立地點縮寫表(loc_1,loc_2...) - 按月份分組事件
- 每個事件透過
to_toon_event()壓縮 - 回傳
{name, id, location_index, month}結構
_build_location_index(events) → dict¶
- 做了什麼:掃描所有事件的
location,建立{"臺科大 TR-510 教室": "loc_1"}的去重索引
list_calendars 工具¶
list_calendars 不經過 TOON 轉換函數,直接在 Tool 層組裝:
_list_writable_calendars() → {calendars: [{id, summary, primary, accessRole}]} → safe_encode()
回傳的 TOON 結構僅包含 calendars 陣列(無 count 欄位,TOON 的 key[N] 語法已包含數量資訊)。
Google Tasks 資料流¶
回到 資料流索引
層級結構¶
| 層 | 檔案 | 主要類別/函數 |
|---|---|---|
| Raw | models_raw.py | GoogleTaskRaw (18 fields), GoogleTaskListRaw |
| Read | models_read.py | GoogleTaskRead |
| Core | async_core.py | async_get_all_tasks(), async_task_list(), async_tasklists_list(), _to_task_model() |
| TOON | models_read.py | to_toon_tasklist(), to_toon_task(), _build_parent_tree(), _deduplicate_tasks() |
| MCP | task_tools.py | get_all_tasks(), get_task_from_tasklist(), list_tasklists() |
轉換函數詳解¶
GoogleTaskRead.from_dict(data: Dict, parent_title?) → GoogleTaskRead¶
- 輸入:Google Tasks API 原始 JSON dict
- 做了什麼:
- 提取
due/completed並用to_datetime()轉為 datetime - 計算
status:已完成 →"✓ MM-DD",未完成 →"pending" - 處理
parent:有父 ID 時組合為"Title (id=FULL_ID)"字串 - 用
format_for_llm()格式化日期為"YYYY-MM-DD HH:MM" - 全天標記:Tasks 的
due永遠是T00:00:00Z(API 限制),all_day固定為True - Source ID:保留
id欄位以支持後續操作
_to_task_model(raw: Dict) → Dict (在 async_core.py)¶
- 做了什麼:薄包裝,直接呼叫
GoogleTaskRead.from_dict(raw).model_dump()
to_toon_task(task, current_month) → dict¶
- 做了什麼:將單一
GoogleTaskRead轉為{title: {id, notes, done, done_at, due, p}}格式
_deduplicate_tasks(tasks) → List[GoogleTaskRead]¶
- 做了什麼:掃描重複標題,自動加上
#1,#2後綴(如"微積分#1","微積分#2")
_build_parent_tree(tasks) → dict¶
- 做了什麼:解析
parent欄位建立{"父任務": {"子任務": "child"}}的樹狀結構
to_toon_tasklist(tasks, name, id) → dict¶
- 輸入:
List[GoogleTaskRead]+ 清單名稱/ID - 做了什麼:
- 呼叫
_deduplicate_tasks()去重 - 呼叫
_build_parent_tree()建立父子關係 - 按月份分組(
month),每月再按done/pending分類 - 無日期的任務歸入
undated - 回傳
{source: {id, title}, parent_tree, month, undated}結構
list_tasklists 工具¶
list_tasklists 不經過 TOON 轉換函數,直接在 Tool 層組裝:
async_tasklists_list() → {tasklists: [{id, title}]} → safe_encode()
[!NOTE]
async_core.async_tasklists_list()在 Core 層已過濾 API 回傳的kind、etag、selfLink、updated等欄位,僅回傳{id, title}。 回傳的 TOON 結構不包含count欄位(TOON 的key[N]語法已包含數量資訊)。
Moodle 資料流¶
回到 資料流索引
[!NOTE] Moodle 是唯一保留完整三層 DDD(Raw → Internal → Read)的整合模組。 因為 Moodle API 回傳的資料結構遠比 Google 複雜,需要更多中間處理。
層級結構¶
| 層 | 檔案 | 主要類別/函數 |
|---|---|---|
| Raw | models_raw.py | RawEvent, RawCalendarData, RawCourse, RawDay 等 |
| Internal | models_internal.py | MoodleEvent, MoodleResult, AcademicCourse |
| Read | models_read.py | MoodleEventRead, CourseCatalog |
| Core | async_core.py | scrape_moodle_events(), fetch_events_by_range() |
| TOON | models_read.py | to_toon_moodle(), to_toon_moodle_event() |
| MCP | other_tools.py | get_moodle_events() |
轉換函數詳解¶
MoodleEvent.from_raw(raw: RawEvent) → MoodleEvent¶
- 輸入:
RawEventPydantic Model(來自 Moodle AJAX 回應) - 做了什麼:
- 將 UNIX timestamp (
timestart) 轉為 UTC+8datetime - 用
_clean_html()清除 HTML 標籤(<p>,<br>,<a>等) - 從
activityname擷取 Open/Close/Cutoff 時間資訊 - 從
course.fullname解析學期、課程代碼
MoodleEventRead.from_internal(event: MoodleEvent, course_idx: str) → MoodleEventRead¶
- 輸入:
MoodleEvent+ 外部生成的課程索引(c1,c2) - 做了什麼:
- 計算語意化
status(Open/Closed/Not yet open/Overdue (Grace period)) - 截斷
description至 200 字元 - 解析
semester("114學年度 第 1 學期"→"114-1") - 條件式欄位:僅在
Not yet open時填入open_date
to_toon_moodle_event(event: MoodleEventRead) → dict¶
- 做了什麼:將單一事件壓縮為
{title, description, cid, status, due_d, due_wd, due_hm}格式
to_toon_moodle(events, course_catalog) → dict¶
- 輸入:
List[MoodleEventRead]+List[CourseCatalog] - 做了什麼:
- 建立課程索引表
[{cid: "c1", course: "..."}] - 按
semester→month雙層分組 - 過濾空列表以節省 Token
- 回傳
{course_index, semester: {"114-1": {"2025-11": {due: [...]}}}}結構
Time Compass — Data Flow Timing¶
Summary¶
- Internal backend datetime baseline remains
Asia/Taipei (UTC+8). - Frontend planner payload is now normalized to
camelCaseat ingress (api.js). - External API contract remains
snake_case(/api/planner/*,/api/context/fetch). - Recurring master events now keep original start/end anchor timestamps.
08:00heuristic for all-day detection is removed.
Google Calendar Event Semantics¶
1. Backend Datetime Retention (No Early String Casting)¶
File: src/time_compass/integrations/google_calendar/models/models_read.py
- start and end are retained as native datetime objects (with Asia/Taipei timezone) in GoogleEventRead.
- Why: The frontend planner components and intermediate normalization layers (like planner_utils.py -> _to_taipei_datetime()) expect robust input processing. Eagerly casting dictionaries ({'dateTime': ...}) to strings (str({'dateTime': ...})) silently breaks the downstream parsers.
- String serialization (like format_for_llm()) is strictly deferred to the final TOON encoding layer (safe_encode()) or the final API Response Payload output.
2. All-day detection (backend)¶
File: src/time_compass/integrations/google_calendar/models/models_read.py
- start.date / end.date present in the original raw dict => all-day event.
- start.dateTime / end.dateTime only => timed event.
- endTimeUnspecified=true => treated as all-day by project policy.
Notes:
- endTimeUnspecified => all_day=True is an explicit product decision for planner rendering consistency.
- Previous 08:00 => all_day inference has been completely removed to avoid accidental mutation of midnight events.
TOON conversion alignment¶
File: src/time_compass/integrations/google_calendar/models/models_toon.py
- TOON conversion now trusts event.all_day directly.
- No secondary 08:00 fallback exists anymore.
Google Tasks Event Semantics¶
1. Eager String Formatting (Discrepancy)¶
File: src/time_compass/integrations/google_tasks/models/models_read.py
- Unlike GoogleEventRead, GoogleTaskRead still immediately casts due and completed native datetime objects into strings using format_for_llm(dt) during from_dict() parsing.
- Risk: If downstream modules need to perform date math on Tasks, they must re-parse the string.
- Note: This is acceptable for the current architecture because Google Tasks lack precise "time" tracking (only days/midnight UTC), but it creates a structural mismatch with Calendar models.
2. All-day detection (backend)¶
GoogleTaskRead.all_dayis hardcoded toTrue. Because Google Tasks API'sduetimestamp is always sent atT00:00:00.000Z(UTC Midnight) which translates to08:00UTC+8, no precise duration exists.
Recurrence Anchor Policy¶
File: src/time_compass/utils/planner_utils.py
- Removed logic that re-anchored recurring master start/end to view_date.
- Master events retain original source anchor timestamps.
- Exceptions/cancellations are still modeled via recurring_event_id, original_start, status, and exdate aggregation.
Impact: - Frontend no longer receives rewritten master start times. - Prevents planner start-time drift introduced by synthetic re-anchoring.
Frontend Normalization & Naming¶
Element cache naming¶
Files:
- src/time_compass/mcp/ui/js/core/state.js
- src/time_compass/mcp/ui/js/services/api.js
- src/time_compass/mcp/ui/js/features/view.js
- src/time_compass/mcp/ui/js/features/zoom.js
- src/time_compass/mcp/ui/js/debug/payload_test_page.js
Changes:
- el renamed to elements across UI modules.
- Internal helper names expanded for clarity (getFixedCalendarEvents, getDraftCalendarEvents, calculateIso8601Duration, etc.).
Ingress mapper (snake_case -> camelCase)¶
File: src/time_compass/mcp/ui/js/services/api.js
- _validatePlannerPayload validates raw payload via Zod schema (snake_case).
- normalizePlannerPayload maps to internal camelCase state:
- session_id -> sessionId
- calendar_events -> calendarEvents
- draft_variants -> draftVariants
- writable_calendars -> writableCalendars
- apply_state.applied_at -> applyState.appliedAt
- etc.
Outbound API request bodies stay snake_case.
FullCalendar Integration Policy¶
File: src/time_compass/mcp/ui/js/features/view.js
- Calendar option explicitly sets timeZone: "local".
- Recurring fixed events are emitted with structured recurrence fields:
- rrule: { dtstart, ...ruleParts }
- exdate: [...]
- duration supplied for master recurrence rendering.
This keeps browser-local display stable while preserving RFC3339 offsets from backend payload timestamps.
Verification Checklist¶
Static¶
rg -n "\bel\b|el\." src/time_compass/mcp/ui/jsrg -n "_durationInput|fixedCalendarEvents|draftCalendarEvents|pad2|formatHm|overlap\(" src/time_compass/mcp/ui/js
Runtime / unit¶
uv run pytest tests/unit/integrations/google_calendar/test_event_read_model.py -vuv run pytest tests/unit/test_planner_utils.py -vuv run pytest tests/unit/planner/test_planner_web.py -v
UI smoke¶
- Open Planner Studio and verify:
- recurring start anchor no longer drifts to
view_date - apply/toggle/zoom/date navigation still function
- event times match browser local timezone display expectations
Version¶
- v2.0 (2026-02-21)
- Removed 08:00 all-day heuristic.
- Removed recurrence master re-anchor behavior.
- Added frontend ingress normalization and naming standardization.
- Explicit FullCalendar local timezone configuration.
HTTP Transport 與 Batch API¶
回到 資料流索引
Google Calendar 和 Google Tasks 共用一套 Batch API Transport 層,將多個請求打包為單一 HTTP POST(multipart/mixed),減少網路往返次數。
層級結構¶
| 層 | 檔案 | 主要類別/函數 |
|---|---|---|
| Request Model | models_request.py (Calendar) | ListEventsRequest, InsertEventRequest, ListCalendarListRequest 等 |
| Request Model | models_request.py (Tasks) | ListTasksRequest, InsertTaskRequest, ListTaskListsRequest 等 |
| API Client | api_client_async.py (Calendar) | list_events_async(), list_calendar_list_async(), _execute_single_async() |
| API Client | api_client_async.py (Tasks) | list_tasks_async(), list_tasklists_async(), _execute_single_async() |
| Dispatcher | google_api_dispatcher.py | batch_execute_async() |
| HTTP Tool | http_batch_tool.py | build_generic_batch_body(), parse_generic_batch_response(), send_batch_request_payload() |
Request Model 設計¶
每個 Request Model 是 Pydantic BaseModel,必須實作兩個方法:
class ListEventsRequest(GoogleCalendarRequest):
def to_http(self) -> dict:
"""將參數轉為 HTTP 請求元件 {method, url, params, json}"""
return {"method": "GET", "url": "/calendar/v3/calendars/.../events", "params": {...}}
def parse_response(self, data: dict) -> list:
"""將 API 回傳的 JSON dict 轉為 Read Layer Model"""
return [GoogleEventRead.from_raw(item) for item in data.get("items", [])]
執行流程¶
Request Model → to_http() → {method, url, params, json}
↓
API Client → _execute_single_async() (len=1 Batch)
↓
Dispatcher → batch_execute_async()
↓
HTTP Batch Tool → build_generic_batch_body() → multipart/mixed 字串
↓
send_batch_request_payload() → POST to Google Batch Endpoint
↓
parse_generic_batch_response() → List[{ok, status, data}]
↓
Dispatcher (raw=True) → 直接回傳 {ok, data: JSON dict}
Dispatcher (raw=False)→ req.parse_response(data) → Read Model
batch_execute_async() 函數詳解¶
- 做了什麼:
- 從 credentials 提取 token
- 呼叫每個 request 的
to_http()取得 HTTP 元件 - 用
build_generic_batch_body()打包為multipart/mixed格式 - 用
send_batch_request_payload()發送至 Google Batch Endpoint - 用
parse_generic_batch_response()解析回應 - 若
raw=True,直接回傳{ok, status, data}dict - 若
raw=False,呼叫req.parse_response(data)轉換為 Read Model
Batch Endpoint¶
| 整合 | Endpoint |
|---|---|
| Google Calendar | https://www.googleapis.com/batch/calendar/v3 |
| Google Tasks | https://www.googleapis.com/batch/tasks/v1 |
API 原始 JSON 範例¶
Google Calendar Event(API 回傳的單一事件)¶
Timed Event (含具體時間)¶
{
"kind": "calendar#event",
"id": "abc123def456",
"summary": "週會",
"start": {
"dateTime": "2026-01-28T10:00:00+08:00",
"timeZone": "Asia/Taipei"
},
"end": {
"dateTime": "2026-01-28T11:00:00+08:00",
"timeZone": "Asia/Taipei"
},
"recurrence": ["RRULE:FREQ=WEEKLY;BYDAY=WE"],
"reminders": {"useDefault": false, "overrides": [{"method": "popup", "minutes": 10}]}
}
All-Day Event (全天事件)¶
{
"kind": "calendar#event",
"id": "def456ghi789",
"summary": "清明節連假",
"start": {
"date": "2026-04-04"
},
"end": {
"date": "2026-04-06"
}
}
{dateTime: ...} 或 {date: ...})出現。
- 若為全天事件,只會有 date 欄位(start 為當天,end 由於非包含性質,會是結束日的隔天)。
- 這些巢狀結構使得轉換層(GoogleEventRead.from_dict)必須解構提取,不可直接視為字串。
Google Tasks Task(API 回傳的單一任務)¶
{
"kind": "tasks#task",
"id": "MTIzNDU2Nzg5MA",
"title": "完成期末報告",
"updated": "2026-01-27T08:30:00.000Z",
"parent": "OTg3NjU0MzIx",
"notes": "需要包含數據分析章節",
"status": "needsAction",
"due": "2026-02-01T00:00:00.000Z",
"completed": null
}
時間欄位觀察:
- Google Tasks API 的時間是直接的字串欄位(如 "due": "2026-02-01T00:00:00.000Z")。
- due 欄位永遠是 T00:00:00.000Z(UTC 零點),這代表它實際上只有日期概念,缺乏精確到幾點幾分的時間資訊。
- 這也是為何在 GoogleTaskRead.from_dict 中可以放心地將它轉換為 UTC+8 字串 YYYY-MM-DD HH:MM(此時時間必定為 08:00),且把 all_day 寫死為 True。
Moodle Calendar Event(AJAX 回傳的單一事件,簡化)¶
{
"id": 12345,
"name": "作業一:Python 基礎練習",
"description": "<p>請完成以下練習...</p>",
"component": "mod_assign",
"modulename": "assign",
"activityname": "作業一:Python 基礎練習 已於 2025年11月15日 星期六 23:59 到期",
"timestart": 1731686399,
"timeduration": 0,
"overdue": true,
"course": {
"id": 67890,
"fullname": "114學年度 第 1 學期 資訊工程系 EE1001 程式設計 張教授",
"shortname": "EE1001-114-1"
},
"isactionevent": true,
"action": {
"name": "繳交作業",
"url": "https://moodle.ntust.edu.tw/mod/assign/view.php?id=...",
"itemcount": 1,
"actionable": true
}
}
共用基礎設施 & MCP Tool 輸出層¶
回到 資料流索引
共用基礎設施¶
safe_encode(data: Any) → str (toon_utils.py)¶
- 做了什麼:
- 偵測 Pydantic Model →
model_dump() json.dumps(default=str)序列化 datetime 等特殊型別- 呼叫
toon_format.encode()產出最終 TOON 字串
to_datetime(value) → datetime | None (time_tool.py)¶
- 做了什麼:統一處理多種輸入格式(ISO8601、
{dateTime:..}、{date:..}、UNIX timestamp)→ UTC+8 datetime
format_for_llm(dt) → str (time_tool.py)¶
- 做了什麼:將 datetime 格式化為
"YYYY-MM-DD HH:MM"(UTC+8),LLM 可直接理解
MCP Tool 輸出層¶
每個 MCP 工具最終回傳一個 TOON 字串 (str),由 safe_encode() 從 Python dict 轉換而成。
單一資料源工具¶
| MCP 工具 | 呼叫的轉換函數 | 最終回傳格式 |
|---|---|---|
get_all_calendar_events |
to_toon_calendar() → safe_encode() |
TOON 字串 |
get_event_from_calendar |
to_toon_calendar() → safe_encode() |
TOON 字串 |
get_all_tasks |
to_toon_tasklist() → safe_encode() |
TOON 字串 |
get_task_from_tasklist |
to_toon_tasklist() → safe_encode() |
TOON 字串 |
get_moodle_events |
to_toon_moodle() → safe_encode() |
TOON 字串 |
list_calendars |
_list_writable_calendars() → safe_encode() |
TOON 字串 ({calendars: [{id, summary, primary, accessRole}]}) |
list_tasklists |
async_tasklists_list() → safe_encode() |
TOON 字串 ({tasklists: [{id, title}]}) |
create_calendar_event |
async_insert_event() → safe_encode() |
TOON 字串 (API 回傳的 event dict) |
create_task |
async_task_insert() → safe_encode() |
TOON 字串 (API 回傳的 task dict) |
get_free_busy |
async_query_free_busy() → safe_encode() |
TOON 字串 (FreeBusy API 回傳原始結構) |
[!NOTE]
list_calendars和list_tasklists不使用 TOON 轉換函數(to_toon_*),而是直接在 Tool 層組裝 dict 後呼叫safe_encode()。 兩者的回傳結構不包含count欄位 — TOON 的key[N]語法已包含數量資訊。
複合工具¶
get_time_context → ResourceContextText¶
此工具整合三大資料源(Calendar + Tasks + Moodle),透過 ResourceContextText.from_resource_context() 轉換為統一 TOON 字串。
最終 TOON 字串的 dict 結構:
{
"meta": {
"time_range": {"start": "2026-01-01T00:00:00Z", "end": "2026-02-01T00:00:00Z"},
"generated_at": "2026-01-28T12:00:00",
"sources": ["google_tasks", "google_calendar", "moodle"]
},
"google_tasks": {
"Task List Name": {
"name": "...", "id": "...",
"parent_tree": {"父任務": {"子任務": "child"}},
"month": {
"2026-01": {
"done": [{"Task Title": {id, notes, done, done_at, due, p}}],
"pending": [...]
}
},
"undated": [...]
}
},
"google_calendar": {
"Calendar Name": {
"name": "...", "id": "...",
"location_index": {"臺科大 TR-510": "loc_1"},
"month": {
"2026-01": [{"Event Summary": {id, t:"10:00~11:00", d, l, r, all_day, recurrence}}]
}
}
},
"moodle": {
"course_index": [{"cid": "c1", "course": "[EE1001] 程式設計 張教授"}],
"semester": {
"114-1": {
"2025-11": {
"due": [{"title": "...", "cid": "c1", "status": "Closed", ...}]
}
}
}
}
}
launch_planner_studio¶
此工具呼叫 get_time_context 取得資料後,透過 build_planner_payload() 生成 Planner Studio 專用 payload,啟動本地網頁伺服器。最終回傳的是狀態摘要:
{
"ok": True,
"url": "http://localhost:PORT/sessions/SESSION_ID",
"opened_browser": True,
"session_id": "...",
"calendar_event_count": 42,
"task_count": 15,
"variant_count": 3,
"view_date": "2026-01-28",
"applied": False
}
錯誤回傳格式¶
所有工具在驗證失敗時回傳:
- 認證失敗:"AUTH_REQUIRED: invalid_grant" 純字串
- Moodle 憑證缺失:safe_encode({"error": "Moodle credentials missing"}) TOON 字串
- 一般錯誤:由 @mcp_harden 裝飾器捕獲並格式化
Time Compass — DDD 資料流架構¶
本文件記錄專案中三大整合模組(Google Calendar、Google Tasks、Moodle)的領域模型轉換鏈。 每個箭頭標註負責轉換的函數,每個方框標註所屬的 DDD 層。
全局架構總覽¶
flowchart TD
subgraph 外部資料來源
GC_API["Google Calendar API"]
GT_API["Google Tasks API"]
MD_API["Moodle AJAX API"]
end
subgraph "Request Layer (Transport)"
GC_REQ["ListEventsRequest\nInsertEventRequest\n..."]
GT_REQ["ListTasksRequest\nInsertTaskRequest\n..."]
end
subgraph "Batch API (Infrastructure)"
DISPATCH["batch_execute_async()"]
BATCH_BUILD["build_generic_batch_body()\nmultipart/mixed 封裝"]
BATCH_SEND["send_batch_request_payload()\naiohttp POST"]
BATCH_PARSE["parse_generic_batch_response()\n→ List of {ok, status, data}"]
end
subgraph "Raw Layer (Anti-Corruption)"
GC_RAW["GoogleEventRaw\n(47+ fields, camelCase)"]
GT_RAW["GoogleTaskRaw\n(18 fields, camelCase)"]
MD_RAW["RawEvent / RawCalendarData\n(55+ fields)"]
end
subgraph "Internal Layer (Domain)"
MD_INT["MoodleEvent\n(cleaned datetime, HTML → text)"]
end
subgraph "Read Layer (Application)"
GC_READ["GoogleEventRead\n(datetime objects preserved)"]
GT_READ["GoogleTaskRead\n(Eager-cast to strings)"]
MD_READ["MoodleEventRead\n(8 fields, 語意化 status)"]
end
subgraph "TOON Layer (Presentation)"
GC_TOON["to_toon_calendar() → dict"]
GT_TOON["to_toon_tasklist() → dict"]
MD_TOON["to_toon_moodle() → dict"]
ENCODE["safe_encode() → TOON string"]
end
subgraph "Planner Presentation (BFF)"
PLANNER_NORM["_normalize_calendar_events_for_day()\n(Color Mapping: ID+Summary → 1-8)"]
end
subgraph "MCP Tool Layer (Interface)"
T_CAL["get_all_calendar_events\nget_event_from_calendar\nlist_calendars\ncreate_calendar_event\nget_free_busy"]
T_TASK["get_all_tasks\nlist_tasklists\nget_task_from_tasklist\ncreate_task"]
T_MOODLE["get_moodle_events"]
T_COMP["get_time_context\nlaunch_planner_studio"]
end
GC_REQ -->|"to_http()"| DISPATCH
GT_REQ -->|"to_http()"| DISPATCH
DISPATCH --> BATCH_BUILD --> BATCH_SEND
BATCH_SEND -->|"HTTP Response"| BATCH_PARSE
BATCH_PARSE -->|"raw=true: {ok,data}"| GC_API
BATCH_PARSE -->|"raw=true: {ok,data}"| GT_API
GC_API -->|"JSON dict"| GC_RAW
GT_API -->|"JSON dict"| GT_RAW
MD_API -->|"AJAX JSON"| MD_RAW
GC_API -.->|"from_dict(Dict)"| GC_READ
GT_API -.->|"from_dict(Dict)"| GT_READ
GC_RAW -.->|"※ 目前被 Bypass"| GC_READ
GT_RAW -.->|"※ 目前被 Bypass"| GT_READ
MD_RAW -->|"MoodleEvent.from_raw()"| MD_INT
MD_INT -->|"MoodleEventRead.from_internal()"| MD_READ
GC_READ -->|"to_toon_calendar()"| GC_TOON
GC_READ -->|"expand_recurring_events()\n(Planner Only)"| PLANNER_EXP["Expanded Events"]
PLANNER_EXP --> PLANNER_NORM
GT_READ -->|"to_toon_tasklist()"| GT_TOON
MD_READ -->|"to_toon_moodle()"| MD_TOON
GC_TOON --> ENCODE
GT_TOON --> ENCODE
MD_TOON --> ENCODE
ENCODE --> T_CAL
ENCODE --> T_TASK
ENCODE --> T_MOODLE
T_CAL --> T_COMP
T_TASK --> T_COMP
T_MOODLE --> T_COMP
[!IMPORTANT] Google Calendar 和 Google Tasks 的 Raw Model(
GoogleEventRaw、GoogleTaskRaw)目前沒有被 Read 層直接引用。 轉換函數from_dict()接受的是Dict[str, Any](API 回傳的原始 JSON),而非 Raw Pydantic Model。 這意味著 Raw Layer 目前主要作為 API 文檔鏡像,而非資料流的必經節點。
時區與時間格式流動¶
所有時間資料最終統一歸集到 Asia/Taipei (UTC+8),由 time_tool.py 中的共用函數驅動。
核心轉換函數¶
| 函數 | 位置 | 作用 |
|---|---|---|
to_datetime(v) |
utils/time_tool.py |
接受任何輸入(RFC3339、UNIX timestamp、date、dict),統一輸出 Asia/Taipei datetime |
format_for_llm(dt) |
utils/time_tool.py |
將 datetime 格式化為 YYYY-MM-DD HH:MM(UTC+8,省略秒) |
to_rfc(v, mode) |
utils/time_tool.py |
將任何輸入轉為 RFC3339 字串(含 +08:00),用於 API 請求參數 |
詳細流程 → data-flow-timing.md
本頁記錄高層概念,細節實現(如何避免精度流失、為何選擇 datetime 而非 str、週期性事件錨點邏輯等)請參考專門文檔。
各模組時間轉換鏈¶
flowchart LR
subgraph "Google Calendar"
GC_IN["API: RFC3339\n2026-01-15T08:00:00+08:00\n或 date: 2026-01-15"]
GC_TD["to_datetime()"]
GC_FMT["format_for_llm()"]
GC_OUT["Read: 2026-01-15 08:00\n(含 summary+title)"]
GC_TOON["TOON: t=08:00~09:00\nd=15"]
GC_IN --> GC_TD --> GC_FMT --> GC_OUT --> GC_TOON
end
subgraph "Google Tasks"
GT_IN["API: RFC3339 UTC\n2026-01-15T00:00:00.000Z"]
GT_TD["to_datetime()"]
GT_FMT["format_for_llm()"]
GT_OUT["Read: 2026-01-15 08:00\n(UTC+8 字串)"]
GT_TOON["TOON: due=15\ndone_at=15 08:00"]
GT_IN --> GT_TD --> GT_FMT --> GT_OUT --> GT_TOON
end
subgraph "Moodle"
MD_IN["API: UNIX timestamp\n1737007200"]
MD_DT["datetime.fromtimestamp\n(ts, tz=Asia/Taipei)"]
MD_OUT["Internal: datetime\n(UTC+8 物件)"]
MD_FMT["strftime\n(YYYY-MM-DD HH:MM)"]
MD_TOON["TOON: due_d=16\ndue_hm=14:00"]
MD_IN --> MD_DT --> MD_OUT --> MD_FMT --> MD_TOON
end
Naive Datetime 處理策略¶
| 情境 | to_datetime() 行為 |
|---|---|
帶 Z 後綴(UTC) |
替換為 +00:00 → 轉 Asia/Taipei |
帶時區偏移(如 +08:00) |
直接轉 Asia/Taipei |
| Naive datetime(無時區) | 預設為 Asia/Taipei(非 UTC) |
Naive date(YYYY-MM-DD) |
視為當天 00:00 (Asia/Taipei) |
| UNIX timestamp | datetime.fromtimestamp(ts, tz=UTC) → 轉 Asia/Taipei |
dict ({dateTime} / {date}) |
提取對應值後遞迴處理 |
Google Tasks API 的
due欄位永遠是T00:00:00.000Z(UTC 午夜),轉換後變為08:00(UTC+8)。 因此GoogleTaskRead.all_day固定為True,測試斷言中須注意此 +8 小時偏移。
重複事件展開策略 (Recurring Events)¶
針對 Planner Studio,我們採用 Backend Expansion 策略:
1. Fetch: API 使用 singleEvents=False 獲取 Master Events (含 RRULE)。
2. Transfer: GoogleEventRead 保留 rrule 原始字串。
3. Expand: 在 planner_utils.py 中使用 dateutil.rrule 進行展開。
- Master: 展開為多個實例。
- Exception: 修改過的實例(原 Master 該日期被抑制)。
- Cancellation: 刪除的實例(原 Master 該日期被移除)。
4. Render: 前端接收已展開且標準化的平坦事件列表。
子文件導覽¶
| 區段 | 文件 | 摘要 |
|---|---|---|
| HTTP Transport | data-flow-transport.md | Batch API 封裝、Request Model 設計、API 原始 JSON 範例 |
| Google Calendar | data-flow-calendar.md | GoogleEventRead 轉換、全天偵測、RRULE、地點索引、list_calendars |
| Google Tasks | data-flow-tasks.md | GoogleTaskRead 轉換、父子關係、去重、list_tasklists Core 層過濾 |
| Moodle | data-flow-moodle.md | 三層 DDD(Raw→Internal→Read)、課程索引、語意化 status |
| 共用 & MCP 輸出 | data-flow-mcp-output.md | safe_encode()、MCP 工具回傳格式表、複合工具、錯誤格式 |
架構觀察與已知問題¶
⚠️ Google 系列:Raw Layer 形同虛設¶
GoogleEventRaw和GoogleTaskRaw定義了完整的 API 結構,但from_dict()接受的是裸dict- 這意味著 Raw Model 並未參與實際資料流,僅作為文檔參考
- 風險:API 欄位變更時不會被 Pydantic 驗證捕獲
✅ Moodle:完整的三層 DDD¶
- Raw → Internal → Read 三層明確分工
MoodleEvent.from_raw()是唯一入口,保證了轉換的一致性- Internal Layer 負責 HTML 清洗、時區轉換等「髒活」
⚠️ 已棄用的 API¶
GoogleEventRead.from_raw()標記為 Deprecated,內部委派給from_dict()或from_raw_model()
📝 維護建議¶
修改模型時,請遵循以下檢查清單:
1. 確認上游(API 回傳格式)是否有變化
2. 確認下游(TOON 輸出格式)是否需要調整
3. 確認時區轉換:新增時間欄位時務必經過 to_datetime() → format_for_llm() 鏈路
4. 更新本文件或對應子文件中的函數說明
5. 執行 uv run pytest tests/unit/ -v 驗證
錯誤處理機制 (Error Handling)¶
Result Pattern (ResourceContext)¶
系統不再使用 Python Exception 作為錯誤傳遞機制,而是採用 Result Pattern:
- Partial Failure: 若個別來源(如 Tasks)失敗但其他成功 → ResourceContext 回傳 ok=True,該來源資料為空且 Log 記錄錯誤。
- Total Failure: 若發生頂層嚴重錯誤(如 Auth Token 無效) → ResourceContext 回傳 ok=False,error 欄位包含錯誤訊息。
flowchart TD
API["API Call"] -->|Success| OK["ResourceContext(ok=True)"]
API -->|Partial Fail (e.g. Moodle Timeout)| OK_PARTIAL["ResourceContext(ok=True)\nmoodle=None\nLog Error"]
API -->|Critical Fail (e.g. Auth)| ERR["ResourceContext(ok=False, error='Msg')"]
OK --> MCP["MCP Tool"]
OK_PARTIAL --> MCP
ERR --> MCP_CHECK{"Is Context OK?"}
MCP_CHECK -->|No| RETURN_ERR["Return JSON: {ok: false, error: ...}"]
MCP_CHECK -->|Yes| PROCESS["Process Data"]
測試證據收集 (Evidence Collection)¶
測試層引入 tests.helpers.evidence 模組,用於捕捉並保存真實的 I/O 資料:
- 目的: 建立 Regression Baseline,確保重構不改變輸出結構。
- 儲存位置: tests/evidence/{test_file_name}/{test_case_name}.{ext}
- 使用時機: 當測試涉及複雜物件結構(如 ResourceContext 或 TOON String)時,必須保存 Evidence。
Runtime 2.0 Data Flow (Passive Viewer Architecture)¶
此架構支援 Browse Mode (動態瀏覽),但 不包含 後端 AI 規劃能力。
flowchart TD
subgraph Frontend [Planner UI]
UI_DATE[Date Navigation] -->|On Change| API_REQ[GET /api/context/fetch]
API_REQ -->|Await| UI_RENDER[Re-render Calendar]
end
subgraph Backend [MCP Server / Runtime]
API_GW[API Gateway\n(planner_routes.py)]
RUNTIME[PassiveRuntime\n(runtime.py)]
PROVIDER[DataProvider\n(integrations)]
API_GW -->|Forward| RUNTIME
RUNTIME -->|Fetch Range| PROVIDER
end
subgraph External [Google / Moodle]
GC[Google Calendar]
GT[Google Tasks]
MD[Moodle]
end
PROVIDER -->|Batch Request| GC
PROVIDER -->|Batch Request| GT
PROVIDER -->|Async Crawl| MD
GC -->|Raw Data| PROVIDER
GT -->|Raw Data| PROVIDER
MD -->|Raw Data| PROVIDER
PROVIDER -->|Combine & TOON| RUNTIME
RUNTIME -->|PlannerPayload| API_GW
API_GW -->|JSON| FrontendRefactor Roadmap (精確定義版)¶
本文件紀錄 Time Compass 的核心技術債、重構規劃與架構演進方向,旨在維持系統整潔並確保關鍵資料流的強健性。
1. 核心架構與穩定性 (Core Architecture & Stability)¶
1.1 Error Handling 統一化¶
- 現狀:
async_client在遇到 API 錯誤時會直接raiseException,而上層的async_core.py則被迫包裹大量try...except捕捉並轉化為Resultpattern。 - 為何如此:專案早期快速迭代,混用了異常機制與結果對象。
- 目標狀態:將
async_client改為回傳含有錯誤 ID 或訊息的對象,實現純粹的函數式結果導向設計,移除async_core的攔截邏輯。 - 影響範圍:
integrations/google_*/async_core.py,async_client.py - 時機:Phase 8
- 相關紀錄:ADR-0006
1.2 MCP Harden 型別斷層重構¶
- 現狀:
@mcp_harden裝飾器在校驗後將 Model 轉回 Dict 傳入原函數,導致原函數的強型別 (Type Hint) 契約斷層。 - 為何如此:初衷是為了清理 MCP 傳輸層可能導致的字面引號鍵值(如
' "variants" ')。 - 目標狀態:修改裝飾器,改為直接注入驗證後的 Pydantic對象,或優化 Dict 傳遞邏輯以保護 IDE 的跨函數追蹤能力。
- 影響範圍:
src/time_compass/mcp/base.py與各工具實作介面 - 時機:Phase 8
- 相關紀錄:用戶回報 (Step 1436)
1.3 Streaming 傳輸順序與分片處理¶
- 現狀:
Orchestrator的流式輸出有時會直接傳入 Chunk,導致輸出的區塊順序混亂,前端渲染跳轉。 - 為何如此:dspy 的 streaming 機制在非同步環境下未對 Chunk 序號進行強邊界控制。
- 目標狀態:實施 Chunk 序號校驗或在前端 Middleware 增加排序緩衝機制。
- 影響範圍:
src/time_compass/domain/orchestrator.py, 前端 Middleware - 時機:Phase 8
- 相關紀錄:用戶回報 (Step 1436)
- 狀態:已完成
- 驗收:Streaming chunk 呈現順序穩定,前端無跳段/亂序渲染。
1.4 Auth Token 跨層同步機制¶
- 現狀:當後端重新 Refresh Token 後,更新無法無縫傳達到前端 Middleware;
run_planner_studio.py常因 Token 過期而中斷執行。 - 為何如此:Middleware 缺乏主動重新讀取 Token 或非切換式 Auth 續約邏輯。
- 目標狀態:建立中央 Auth 狀態監聽器,確保 Token 更新即時同步至所有 Runtime 組件,避免 Refresh 引發的中斷。
- 影響範圍:
mcp/runtime.py,mcp/planner_routes.py,scripts/run_planner_studio.py - 時機:Phase 8
- 相關紀錄:用戶回報 (Step 1436)
2. 資料模型與 TOON 優化 (Data Models & TOON)¶
2.1 TOON 冗餘 Resource ID 清理¶
- 現狀:
get_time_context_composite.toon成品中,各項目內部的source.id在層次化 Grouping 下顯得冗餘。 - 為何如此:沿用早期扁平化結構設計。
- 目標狀態:移除項目內部的重複 ID,僅保留跨資源索引。
- 影響範圍:
src/time_compass/integrations/*/models/models_toon.py - 時機:Phase 8
- 相關紀錄:詳見上游 assets 目錄中的 get_time_context_composite.toon 檔案
2.2 Moodle 模型與 Google 體系對齊¶
- 現狀:Moodle 的 Read Model 與 Google 的
groups/items標準模式不對齊。 - 為何如此:Moodle 模組為後期加入,尚未完全完成向 DDD 架構的過渡。
- 目標狀態:重構 Moodle 整合層模型,使其支援標準的
All*Result固定模式。 - 影響範圍:
src/time_compass/integrations/moodle/models/ - 時機:Phase 8
- 相關紀錄:
models_read.py
2.3 Common 整合層解析邏輯 DDD 化¶
- 現狀:
common/models.py處理 Response 時使用較為脆弱的正則 Fallback 邏輯。 - 為何如此:處理 Google Multipart Batch 反應雜訊的權宜之計。
- 目標狀態:替換為基於 Domain 契約的嚴謹解析。
- 影響範圍:
src/time_compass/integrations/common/models.py - 時機:Phase 9
- 相關紀錄:用戶回報 (Step 1436)
2.4 AllCalendarEventsResult 雙欄位清理¶
- 現狀:同時保留
groups/calendars等 Alias。 - 為何如此:向後相容 Legacy callers。
- 目標狀態:Deprecate 舊欄位,移除
runtime.py裡的getattr()防禦代碼。 - 影響範圍:
domain/models.py,runtime.py - 時機:Phase 9
- 相關紀錄:詳見本節(2.4)與對應實作清理任務
2.5 base_models.py 繼承鏈優化¶
- 現狀:工具回傳模型的繼承關係與 Pydantic 設定(如額外欄位處理)不一致。
- 為何如此:不同階段定義的工具結果缺乏統一父類約束。
- 目標狀態:釐清 Internal/Base Tool Result 的歸屬。
- 影響範圍:
src/time_compass/utils/base_models.py - 時機:Phase 9
2.6 核心模組資料注入改用 TOON 格式¶
- 現狀:
Summary與Schedule模組內部仍使用舊的 JSON-like 注入流程。 - 為何如此:尚未對接 TOON 高層次 API。
- 目標狀態:將資料注入點全面替換為壓縮後的 TOON 字串,降低 LLM 首層推理負荷。
- 影響範圍:
src/time_compass/domain/summary/,schedule/ - 時機:Phase 9
2.7 Model Validation Strict Typing 與 Polymorphic Factory 改造 (Deferred)¶
- 現狀:為了方便測試生成 (Dev Mode) 與 Mock 資料的彈性,
GoogleEventRead等模型維持了寬鬆的from_dict邏輯與較少的@model_validator,導致部分 Mock 資料缺乏嚴格結構約束。 - 曾嘗試過但退回的原因:實施嚴格的 DDD 多型工廠 (Polymorphic Factory) 與推導邏輯下沉後,揭露了大量過去單元測試 (Mock Data) 的結構不完整,引發連鎖紅燈。為了不阻礙核心產品進度,暫時回退。
- 目標狀態:
- 修正所有測試場景的 Mock 資料,使其符合
GoogleEventRaw的正規結構。 - 重新將
GoogleEventRead.from_raw_model()強型別化,不再接受Any,並委派給純粹處理注入的from_dict()。 - 將
start/due到all_day的推導邏輯完全下沉至 Pydantic V2@model_validator(mode="before"),並保持冪等性 (Idempotency)。 - 時機:Phase 10 (等測試資料集完全重構後)
3. 前端視圖與互動 (Frontend & Visualization)¶
3.1 Planner Studio 全天事件渲染與 Dev Mode 顯示 Bug¶
- 現狀:全天事件在某些時段會因
all_day標籤遺失導致渲染斷裂;此外,在dev_mode下草擬案 (Draft Variants) 經常無法正確顯示。 - 為何如此:Backend 到 Frontend 的資料傳遞鏈路屬性映射不全,且 Mock 資料與實體渲染邏輯在處理多變體時存在不對齊。
- 目標狀態:確保資料流在
dev_mode與real_mode下皆能正確標註全天屬性並正常顯示所有草擬變體。 - 影響範圍:
scripts/run_planner_studio.py,mcp/dev_mode.py, 前端 JS - 時機:Phase 8
- 相關紀錄:用戶回報 (Step 1436, 1501)
- 狀態:已完成
3.2 UI 風格 Premium 化¶
- 現狀:風格被回饋較為單調、不夠 Premium。
- 為何如此:偏重功能實作,美學投入(如玻璃擬態、動態過渡)較少。
- 目標狀態:引進玻璃擬態、微互動,符合極致美學標準。
- 影響範圍:
src/time_compass/mcp/ui/styles/ - 時機:Phase 10
- 狀態:待審核
4. 工作區治理與文件化 (Workspace & Audit)¶
4.1 README 與安裝指南大整合¶
- 現狀:
README.md未整合CODEX_INSTALL與COPILOT_INSTALL;且README_OLD.md不符標準。 - 目標狀態:全面更新 README,融入 MCP 支援細節,並參考
readmeskill 進行高品質寫入。 - 影響範圍:根目錄所有 README 文件
- 時機:Phase 8
- 狀態:其他完成(OLD 未完成)
4.2 腳本與代碼「斷捨離」¶
- 現狀:
scripts/下存在廢棄腳本(serve_mlflow.py等);domain/optimize已不使用。 - 目標狀態:清理所有無效資源,維持系統純淨性。
- 時機:Phase 9
- 狀態:完成
4.2.1 安全刪除 src/time_compass/domain/main.py¶
- 現狀:
domain/main.py為示範性入口,與正式啟動腳本(time-compass-mcp、time-compass-gradio)無直接綁定。 - 目標狀態:在不影響現有啟動流程與測試的前提下移除該檔案。
- 修復步驟:
- 以
rg全域確認無 import/reference。 - 刪除
src/time_compass/domain/main.py。 - 執行最小啟動驗證:
uv run time-compass-mcp、uv run time-compass-gradio。 - 驗收:啟動命令與主要測試不受影響;若失敗則回補替代入口或文件說明。
- 時機:Phase 9
- 狀態:待執行
4.3 文檔審計與 Archive 搬移¶
- 現狀:
docs/architecture存在過時文件;archive/old_sessions資料雜亂。 - 目標狀態:審查所有文檔的 Up-to-date 狀態,搬移有價值內容,刪除過期廢棄文件。
- 時機:Phase 9
- 相關紀錄:
docs/目錄結構
4.4 評審導向之 WMI 坑位告知¶
- 現狀:Windows WMI 穩定性問題為隱藏大坑。
- 目標狀態:在顯眼處(README)提醒評審如何使用環境變數(如
TIME_COMPASS_DISABLE_WMI_SYSTEM_QUERY)避開。 - 時機:Phase 8
5. 架構決策與互動深度 (Architecture Decisions & Interaction)¶
5.1 Gradio 與 DSPy 決策回顧¶
- 現狀:舊版 Web UI 使用 Gradio 且後端依賴 DSPy。
- 為何如此:Gradio 旨在快速原型開發;DSPy 則提供了類似 Pydantic 的結構化輸出與自動回退機制。
- 目標狀態:在文件(README 或 ADR)中明確記錄此決策背景,並指導使用者依需求選擇:
- MCP 工作流:
uv run time-compass-mcp - 舊版 Web UI (Gradio):
uv run time-compass(打開 http://localhost:8000/gradio) - 相關紀錄:用戶回饋 (Step 1540)
5.2 Planner Studio API 與 Payload 規範¶
- 現狀:前端極度仰賴
/api/context/fetch、/api/plan/generate與PlannerDraftVariants格式,但缺乏契約文檔。 - 為何如此:前後端目前由同一開發者維護,約定大於配置。
- 目標狀態:撰寫正式的 API Spec,明確定義變體 (Variants) 傳遞時的 JSON 結構,以釐清前後端邊界。
- 影響範圍:
src/time_compass/mcp/planner_routes.py - 時機:Phase 9
5.3 智慧意圖路由 (L1/L2/L3) 與強制停頓機制¶
- 現狀:
SchedulingRouter具備 L1/L2/L3 漏斗架構與「強制停頓詢問」機制,但外部文檔完全未提及。 - 為何如此:核心邏輯隱藏在 Prompt 與
orchestrator.py中。 - 目標狀態:建立系統互動流程圖,展示非線性對話如何避免 AI 暴走,並解釋 L1 (模稜兩可) 到 L3 (行動級) 的轉換邏輯。
- 時機:Phase 8
- 相關紀錄:用戶回饋 (Step 1540)
5.4 MCP 互動設計:Docstring 與 Tool 聯動¶
- 現狀:MCP 工具的 Docstring 對 AI 的觸發至關重要,但目前部分 Prompt 缺乏對 Tool 使用的明確指引。
- 目標狀態:重構 MCP 工具的 Docstring,確保其與系統 Prompt 形成合力:
- 若有對應 Tool,Docstring 需精確描述觸發條件。
- 若僅需 Prompt 回應,Prompt 內部應有引導使用其他對應 Tool 的邏輯。
- 時機:Phase 8
6. 治理與文檔遺留項 (Governance & Docs Legacy)¶
6.1 DEV_MODE 文件合併¶
- 現狀(已完成):舊文件已下線,統一為
DEV_MODE_GUIDE.md。 - 結果:以現況實作(
MCP_DEV_MODE、MCP/domain 切換點)為單一說明來源,降低文檔漂移。 - 時機:Phase 9(完成)
6.2 Data Flow 索引整合¶
- 現狀:存在 10 多份細部的
data-flow-*.md。 - 目標狀態:確保
data-flow.md成為完美的總合索引,並精簡不必要的細碎分頁。 - 時機:Phase 9
6.3 README 進階需求 (由 Comparison Report 追蹤)¶
- 遺留項:
- 補齊 Python 3.12+ 標註、不需獨立 DB 的聲明。
- 加入目錄樹 (Tree) 與 Request Lifecycle 圖表。
- 補充 Google Cloud 申請步驟 (How-to-get)。
- 展示測試分層矩陣、對話範例 (Sample Interaction)。
- 時機:Phase 8
- 相關紀錄:readme_comparison_report.md.resolved
7. 新增技術債與未來計畫 (Newly Tracked Items)¶
7.1 output_result 測試細化需求¶
- 現狀:測試套件中
output_result快照覆蓋度不足,目前難以從測試輸出直觀判斷每個函數在每種行為下的結果,調試時仍需手動 print。 - 目標狀態:每個核心函數(尤其是
integrations/的 Adapter 層)在每種行為(成功/失敗/邊界)下均有對應的output_result快照或斷言,方便 DDD 除錯時直接比對。 - 影響範圍:
tests/目錄(主要是tests/unit/與tests/snapshots/) - 時機:Phase 9
7.2 from_dict → validate-before 資料推導分離重構¶
- 現狀:多個 Read Model 的
from_dict方法混合了「欄位推導(如 all_day 推斷)」與「額外參數注入」兩種責任,語義模糊、不易測試。 - 目標狀態:
- validate-before:所有欄位推導邏輯下沉至 Pydantic
@model_validator(mode="before"),實現資料內部自洽推導。 - 額外參數注入:需要外部注入的資料(如
calendar_id)改用明確的@classmethod工廠方法,名稱清楚標示其注入語義(如from_raw_with_calendar)。 - 移除僅用於相容性的寬鬆
from_dict,統一工廠命名規範。 - 影響範圍:
integrations/*/models/models_read.py(主要影響 Google Calendar、Google Tasks、Moodle) - 時機:Phase 10(與 2.7 Model Validation 改造同期)
- 相關紀錄:本條目由 GUIDE.md 開發規劃觸發(2026-02-28)
7.3 uv 套件管理技術棧說明補充¶
- 現狀:
pyproject.toml使用uv進行套件管理,但 README 與 docs/ 中缺乏「為什麼選擇 uv」的明確說明,容易讓貢獻者困惑。 - 目標狀態:在 README 的技術棧章節,以及
docs/adr/中新增一份 ADR,記錄選擇 uv 的決策理由(速度、環境隔離、pyproject.toml 標準相容性)與棄用 pip 的原因。 - 影響範圍:
README.md、docs/adr/(新增 ADR 文件) - 時機:Phase 8(與 README 整合同期,4.1 一起做)
7.4 Moodle JSON Analysis Skill 計畫¶
- 現狀:
docs/explanation/moodle_api/已記錄 raw.json(Moodle 原始 API Response)與 cleaned.json(手動整理版),但缺乏自動化工具將 raw 轉為 Pydantic 友善的 cleaned 格式。 - 目標狀態:撰寫一個 Agent Skill(
analysis-json-to-pydantic或類似名稱),支援: - 從 raw JSON 推導結構(感知 Optional、型別、巢狀層次)
- 輸出結構清晰的 cleaned JSON(保留原始值)
- 增量寫入
docs/而非覆蓋全部 - 可選:自動生成對應的 Pydantic BaseModel 骨架
- 影響範圍:
.agent/skills/(新技能)、docs/explanation/moodle_api/ - 時機:Phase 9(先完成 Moodle 模型與 Google 體系對齊後進行,參照 2.2)
7.5 Streaming WAIT Payload 語義化(Deferred)¶
- 現狀:
StreamingCollector的WAIT事件目前直接攜帶最終顯示文字(含 Markdown/emoji),presentation 細節仍在 collector 內。 - 問題:這會讓傳輸層與呈現層耦合,後續若調整 UI 文案或多視圖輸出,需修改 collector,維護成本偏高。
- 目標狀態:
WAIT僅承載語義資料(例如 phase/dots_count/timestamp),不直接承載最終 UI 文案。- 最終等待文字由 presenter/formatter 組裝,collector 僅提供事件語義。
- 保持事件型別不變(
WAIT/FIELD_START/CHUNK/DONE/ERROR),降低遷移風險。 - 時機:Deferred(本輪先不處理,待 streaming contract 穩定後再開獨立 change)
- 備註:目前決策為「先修好 typing 與終止語義」,不擴大範圍到 payload/presenter 深度重構。
7.6 Draft Plan 階段前置產生 TaskList¶
- 現狀:
Scheduling流程中,TaskList 主要在後段(如DraftActionmetadata)才被具體化,DraftPlan階段尚未穩定輸出可直接操作的任務清單。 - 目標狀態:在
DraftPlan產生時同步輸出結構化 TaskList(至少包含 title/notes/due/list_id 或等價欄位),讓前端可提早預覽與編修。 - 影響範圍:
src/time_compass/domain/schedule/、src/time_compass/interface/ui/scheduling/ - 時機:Phase 8
7.7 TaskList 變動自動展開 UI¶
- 現狀:TaskList 區塊是否展開目前偏向靜態控制(或依既有事件觸發),當內容有更新時不一定會自動提醒使用者。
- 目標狀態:當 TaskList 有新增/刪除/修改時,自動展開對應 UI 區塊並高亮更新,降低「有資料但沒看到」風險。
- 影響範圍:
src/time_compass/interface/ui/scheduling/scheduling_tab.py、相關 formatter/helper - 時機:Phase 8
- Gradio 可行性(Context7 驗證):
- 可透過事件回傳
gr.update(open=True)直接控制gr.Accordion展開狀態。 gr.ChatInterface支援additional_outputs,可在 chat stream 回傳時同步更新 TaskList 區塊相關元件。- 無內建「暫時高亮動畫」API,建議先用
gr.Markdown/gr.HTML狀態訊息(例如「TaskList 已更新」)作為第一版提示,再視需要補 CSS 動畫。 - 最小落地方案(small change):
- 在
scheduling_tab.py為 TaskListAccordion命名(如tasklist_acc),並納入additional_outputs。 - 每次拿到新的 TaskList 後,比對前次快照;若有變動則回傳
gr.update(open=True)展開區塊。 - 同步回傳更新提示訊息(例如「新增 2 筆、修改 1 筆」),降低使用者漏看風險。
7.8 Gradio Server 掛載 LiteLLM Proxy 與後端 LLM Config 整併¶
- 現狀:Gradio 與 LiteLLM 目前為相對鬆散的佈署與設定關係,啟動與設定對齊成本高,且 backend LLM config 切換點較分散。
- 目標狀態:在同一 server/runtime 掛載可用的 LiteLLM service entry(或統一反向代理入口),並重整 backend LLM config(
llm_config.py)使路由與 fallback 規則集中可控。 - 影響範圍:
src/time_compass/domain/llm_config.py、Gradio 啟動腳本/伺服器入口、litellm_config.yaml - 時機:Phase 8
7.9 掛載 MCP Web Server 能力 + DraftAction 輸出規劃時間草案與網址回傳¶
- 現狀:現行 server 入口與 MCP web server 能力仍有分離;
DraftAction主要著重 action 描述與 metadata,缺少「規劃時間草案 + 可跳轉網址」的前後端一體流程。 - 目標狀態:
- server 入口掛載原本 MCP web server 的核心功能(至少維持既有 API 能力)。
DraftAction增加「規劃時間草案」輸出欄位(可供排程建議/預檢)。- backend 回傳可跳轉網址(URL)至前端,前端透過
additional_outputs穩定顯示該 URL。 - 影響範圍:
src/time_compass/mcp/、src/time_compass/domain/schedule/、src/time_compass/interface/ui/scheduling/scheduling_tab.py - 時機:Phase 9
概念說明 (Explanation)
Time Compass Q&A(給使用者與評審)¶
這份文件是 docs/explanation 的入口。
原則:短答先放這裡;需要細節時,再引用其他文件。
快速導航¶
- 想快速懂產品價值:看
Q-CORE-* - 想看和其他方案差異:看
Q-DIFF-* - 想看技術護城河:看
Q-MOAT-* - 想看限制與風險:看
Q-RISK-* - 想看演進背景:看
Q-EVO-*
Q&A¶
Q-CORE-001 這個專案一句話在做什麼?¶
綜合「已有規劃與事件 + 低阻力任務規劃 + 覆盤總結」的 AI 工具。
你可以從無從下手,直接進到可執行的下一步。
Q-CORE-002 什麼是「低阻力」?¶
以前要自己翻 Google Calendar、Google Tasks、未來事件,還要手動整理。
現在可以直接和 AI 抱怨現況,讓它代勞啟動規劃,降低起步阻力。
Q-CORE-003 主要使用者是誰?¶
主要是「需要規劃與總結」的學生與上班族,特別是同時使用 Google Calendar / Google Tasks / NTUST Moodle 的人。
不需要 AI 介入規劃的人(例如只想手動記錄、手動收集未來與過去的資訊、不需要拆解與回顧流程)。
Q-CORE-004 成功的使用結果長什麼樣子?有沒有可觀察指標?¶
成功不是「聊得很順」,而是「真的完成登入、拿到上下文、產生可執行草案,並可落地到日曆/任務」。
觀測指標(舊架構 Gradio):
- 可在 OAuth 分頁完成登入,且可下載 token(見 /download-token 路由,只可用在 gradio)。
- 後續對話可讀到 Google 資料來源,並進入規劃/總結流程(如果已登入)。
- 規劃對話會從釐清到拆解,再到可排程草案(L1/L2/L3 由系統內部路由,不強制顯示名稱)。
- 當夠細時可以看到摺疊的列表裡有tasks列表出現,按下寫入的按鈕後可以寫入 Google Tasks。
- 總結請求可產出最近一段時間的回顧;情緒不佳時可觀察到前置或前後並置 emotion support。
觀測指標(新架構 MCP):
- 拆分、總結的體驗跟原本差不多,但 mcp 版的 AI 要先調用對應 Prompt(summary / emotion / time management),再調用資料工具、或著同時調用。
- 進入 L3 時可啟動 launch_planner_studio,看到多草案與事件差異。
- 可選擇將部分 L2 內容寫入 tasks(非強制)。
- OAuth 可由 launch_google_token_auth 或 tools/get_google_token.py 產生 token.json (可用在 gradio 與 mcp)。
Q-CORE-005 目前「可寫入」能力到哪裡?¶
目前能力邊界是: - Google Tasks: - gradio: 支援讀、寫。在 task draft 流程有按鈕可實際寫入。 - mcp: 支援讀、寫。 - Google Calendar: - gradio: 支援讀。無寫入部分。 - mcp: 支援讀、寫。在前端的 planner studio 可以看到讀到的日曆、也有按鈕可實際寫入草案。
Q-DIFF-001 為什麼不是直接用 ChatGPT + Actions + 個人化 GPT?¶
你可以快速接 OAuth,但實務上資料抓取穩定性不足:
- 日曆列表與逐曆事件抓取容易漏資料
- 難做 async / batch,速度慢
- prompt 過長,難以支援多功能,上下文窗口不夠
- 回傳有很多不需要的東西、token 成本高
Q-DIFF-002 為什麼不是直接用 Gemini 接 Google Workspace?¶
可控性與可觀測性不足:
- thinking 與工具調用不透明
- 不易確認實際抓到哪些資料、時間範圍多大、是否真的抓到
- 不易確定最大能抓到那些範圍
- prompt 過長,難以支援多功能,上下文窗口不夠
- 回傳有很多不需要的東西、token 成本高
Q-DIFF-003 為什麼不是用市面上的 MCP 就好?¶
目前常見 MCP 方案通常不覆蓋你的需求:
- 不支援 NTUST(台科大)Moodle 情境
- 缺少跨來源聚合讀取
- 缺少資料清洗與 TOON 壓縮
Q-DIFF-004 為什麼不是只做成 skills?¶
從 Google 端穩定拿資料不能只靠 skills。
skills 比較像提示層能力;Time Compass 同時處理了資料層、聚合層與輸出層。
另外,MCP 也能接入部分網頁 AI(例如 ChatGPT)作為實際通道。
Prompt 的部分也使用 description 來讓 AI 按需載入,理念與 skills 類似。
Q-MOAT-001 這個專案不可替代性是什麼?¶
不是只有「聚合 + 加速 + 省 token」,還有:
- 自主前端:可用日曆視圖比較不同 AI 草案(WYSIWYG)
- Prompt engineering:結合心理學流程,降低使用者執行阻力
- 資料治理鏈路:清洗、聚合、壓縮、再輸出為 AI 可讀格式
也因為這些能力是整體打包,不是單點功能,所以這同時也是前面不採用那些替代方案的根本原因。
Q-MOAT-002 TOON 到底解決了什麼?¶
把多來源資料做欄位清洗、聚合、特殊格式回傳,讓 AI 更好讀。
在你目前實測中,token 消耗可下降約 80% 以上。
Q-MOAT-003 TOON 80% 以上節省是怎麼測的?¶
基於清洗後資料集(含 Google Calendar / Tasks / Moodle)做精確 token 計數。
目前報告顯示約 83.7% token 降幅。
參考:assets/TOON_STATS_REPORT.md
Q-RISK-002 NTUST Moodle 支援程度到哪裡?¶
目前是 partial support(畢竟本專案不是 mooodle 的 python sdk)。
Moodle 日曆資料可抓取事件。
延伸閱讀¶
- TOON 規格:
docs/reference/toon-format.md - TOON 壓縮統計:
assets/TOON_STATS_REPORT.md - 架構隱喻:
docs/explanation/ARCHITECTURE_METAPHORS.md - Agent 能力:
docs/explanation/AGENT_CAPABILITIES.md - 開發歷程:
docs/explanation/DEVELOPMENT_JOURNEY.md - 測試與環境操作:
docs/how-to/run-tests.md、docs/how-to/setup-google-cloud.md
Agent 核心能力與心理學實踐 (Capabilities & Psychology)¶
Time Compass 不僅是一個資料同步工具,它具備一套完整的「排程心理學」推理框架,旨在降低學生面對繁雜任務時的起步阻力。
1. 智慧意圖路由 (Smart Routing Funnel)¶
系統透過 SchedulingRouter 自動識別使用者的意圖清晰度 (L1~L3),並引導進入不同的對話漏斗:
- L1 (Ambiguous): 當使用者只有模糊概念(如「我想學好數學」)。系統會啟動方案發想模式。
- L2 (Actionable Plan): 當目標具備時限與初步結構。系統會進行任務拆解與風險分析。
- L3 (Ready to Schedule): 當任務已拆解至具備執行力 (時長 < 60min)。系統會搜尋行事曆空檔並提議具體時段。
2. 認知負荷理論實踐 (Cognitive Load Control)¶
為了防止使用者因資訊過載而產生「拖延逃避」,系統在中介層實施了多項限制: - 60 分鐘限制碼: 任何行動任務(Atomic Action)被強插限制在 60 分鐘以內。若使用者輸入超過此範圍,系統會主動提問:「這個任務看起來很大,我們要不要拆成兩個 30 分鐘的段落?」 - 最小阻力探詢 (AskQuestion): 當資訊缺失時,AI 不會丟出開放式問卷,而是根據 Context 生成 3~5 個選擇題,且每個問題都具備「我還不確定」的緩衝選項。
3. 葉杜二式法則 (Yerkes-Dodson Law) 與動機拿捏¶
- 情緒橋接 (Emotional Bridging): 在高壓環境下(如考前一週),系統不會一開口就要求排程,而是先經由
EmotionSupport模組接住情緒,然後回顧過往成功經驗 (Summary)。 - 緩衝警告機制: AI 會主動在每個排程方案中標註
[RISK](如:這組方案下午時段過多,可能導致體力不支)與[ASSUMPTION]。
4. 防暴走強制暫停¶
系統在從「方案發想 (L2)」過渡到「真實寫入 (L3)」之前,設有強制確認節點。這確保了 AI 不會單方面地「暴走」塞滿使用者的 Google Calendar,維持使用者的主體性。
架構隱喻與核心設計理念 (Architecture Metaphors)¶
這是一份寫給人類(而不僅僅是機器或其他開發者)的文件。我們利用生活中的比喻來解釋 Time Compass 背後的複雜技術。
1. 資料傳輸:三合一水管¶
學生平時最痛苦的是資訊散落在 Moodle (網頁)、Google Calendar (手機) 與紙本或數位 Tasks。 - 比喻:Time Compass 就像是一台「專用的強力抽水機」。它不要求你搬家,而是裝配了三條專屬吸管,同時深入這三個系統,將資料匯流後,經過過濾網(Data Cleaning),轉化為純淨的水(TOON 格式)供 AI 飲用。
2. 測試策略:Capture-First 拍立得¶
傳統測試像是「憑記憶畫畫」(手寫 Mock),畫出來的往往跟本人不太像。 - 比喻:Capture-First 就像是「拿拍立得拍照」。我們在遇到問題時,直接拿相機去 API 現場「卡嚓」拍一張原始 JSON 照片。之後的測試,我們都是看著這張照片寫 Code。這保證了我們的軟體對真實數據的反應是 100% 準確的。
3. 資料壓縮:TOON 壓縮包¶
JSON 格式就像是寫信時連信封、郵戳、地址長寬格式都寫在內容裡,非常浪費。
- 比喻:TOON 就像是一套「專屬速記代碼」。我們跟 AI 約定好:[T] 代表任務、日期用數字寫不用寫月日年。這讓郵費(Token 成本)便宜了 50%,且讀信的速度(推理速度)快了一倍。
4. 開發者哲學:反文件驅動 (Anti-SDD)¶
很多專案文件寫得比程式碼還慢,最後沒人看。 - 比喻:我們不寫「組裝傢俱的說明書」,我們直接給你「傢俱的設計圖與零件實物」。程式碼本身就是最準確的文檔,我們強迫自己把型別寫清楚,讓 IDE 直接告訴你怎麼組裝。
這些設計理念共同組成了一個「輕量但強韌」的系統,這就是為什麼 Time Compass 能在這麼短的時間內從一個笨重的原型進化為正式的 MCP 工具。
開發歷程與架構演進 (Development Journey)¶
1. 典範轉移:從「笨重」到「精煉」¶
Time Compass 經歷了兩次重大的技術路徑選擇,這反映了我們對 AI 代理套件未來發展的理解。
第一階段:Standalone Web App (v1)¶
- 架構: Gradio + DSPy + FastAPI + SQLite。
- 決策背景: 為了快速產出一個具備 UI 的 Web 產品。
- 反思與棄用:
- 啟動摩擦力: 使用者需要維護一套 Python 環境只為了用一個外掛,這不符合「工具在手邊」的邏輯。
- DSPy 的代價: DSPy 的優化流程消耗了驚人的 Token 成本,對實際生成的邊際效益在模型進步後逐漸遞減。
第二階段:Agentic MCP Extension (v2)¶
- 架構: MCP Server + Planner Studio (Vite/React) + Library-first Core。
- 優點: 寄生於 IDE (如 Cursor/Claude),使用者可原地呼叫。資料流變得極致純粹,去除了冗長的對話管理邏輯。
在開發過程中,我們建立了幾項堅不可摧的指導原則:
- Capture-First: 我們拒絕憑空想像 Mock。所有測試都是從 Google/Moodle 攔截真實 JSON,這保證了系統在面對真實 API 變動時的韌性。
- FP over Classes: 大部分的業務邏輯(特別是資料清洗層)均採用函數式編程。這不僅讓 snapshots 測試變得異常簡單,也大幅減少了狀態副作用。
- 反文件驅動 (Anti-SDD): 我們停止撰寫容易過期的詳細介面文件,取而代之的是強大的型別定義與架構資料流圖。
3. 自我批判與持續改進¶
我們在開發中曾模擬了不同專家的視角來批判我們的 README 與架構。這種「紅隊思考」幫助我們識別出:
- OAuth 的地獄度: 雖然實作很難,但我們堅持完成正統 OAuth 流程以保障使用者隱私。
- 技術債坦白: 我們公開在 ADR 中記錄了 async_client 的例外處理混亂,這彰顯了我們對工程品質的真實要求與後續演進的決心。
Moodle API
Moodle API Integration¶
本目錄紀錄 NTUST Moodle API 的分析結果。由於校方系統權限設定,部分標準 Moodle Web Service API 被封鎖或呈現停用狀態。
可用 API (已驗證)¶
| API 名稱 | 說明 | 路徑 |
|---|---|---|
core_course_get_enrolled_courses_by_timeline_classification |
獲取使用者課程清單 | service.php/... |
core_courseformat_get_state |
獲取單一課程週次、內容結構與連結 | service.php/... |
core_session_time_remaining |
檢查當前 Session 剩餘時間 (防斷線) | (尚未建立文檔,但測試可用) |
core_output_load_template_with_dependencies |
獲取系統 UI 模板 ( Mustache ) | (尚未建立文檔,但測試可用) |
core_calendar_get_calendar_monthly_view |
獲取日曆月視圖事件 (作業截止日) | (尚未建立文檔,但測試可用) |
不可用 API (校方已封鎖 / 返回 servicenotavailable)¶
以下 API 經測試在 NTUST Moodle 呈現停用狀態,無法透過 sesskey 調用:
| API 名稱 | 預計功能 | 失敗原因 |
|---|---|---|
mod_assign_get_submission_status |
獲取作業繳交狀態 / 評分 | servicenotavailable |
mod_assign_get_assignments |
獲取所有作業列表 | servicenotavailable |
core_completion_get_activities_completion_status |
獲取活動完成勾選狀態 | servicenotavailable |
core_course_get_contents |
獲取詳細課程內容 ( 替代方案: get_state ) |
servicenotavailable |
core_calendar_get_calendar_events |
獲取日曆事件列表 | servicenotavailable |
參考資料¶
$body = @{ username = "B11427014" password = "~Sky@Wind960325" service = "moodle_mobile_app" }
Invoke-RestMethod -Method Post
-Uri "https://moodle2.ntust.edu.tw/login/token.php" -ContentType "application/x-www-form-urlencoded"
-Body $body
操作指南 (How-to)
如何執行測試 (How-to Run Tests)¶
Time Compass 採用嚴格的三層測試架構,確保開發過程與外部 API 變動時的穩定性。
核心指令¶
執行測試前,請確保已安裝 uv 並執行過 uv sync。
1. 快速單元測試 (Unit Tests) - 🚀 推薦每 5 分鐘跑一次¶
- 目的:驗證 TOON 編解碼、資料模型轉換與 Domain 邏輯。
- 憑證:不需要任何 API Key。
- 指令:
uv run pytest tests/unit
2. 真實資料擷取 (Live Tests)¶
- 目的:直接與 Google/Moodle API 溝通,擷取原始 JSON 用於更新
tests/snapshots。 - 憑證:需要完整的
.env配置與 OAuth 授權、moodle 帳號密碼。 - 指令:
uv run pytest tests/live
3. 端對端系統測試 (System Tests)¶
- 目的:模擬真實用戶對話,測試 MCP 工具鏈與 AI 的整合。
- 指令:
uv run pytest tests/system
測試哲學:Capture-First¶
- 遇到 Bug 時,先寫一份
tests/live/reproduce_bug.py。 - 執行它,擷取真實 API 的報錯快照 (Snapshot)。
- 將該 JSON 移入
tests/snapshots/。 - 在
tests/unit/撰寫測試,讀取該快照,並修正 Code 直到測試通過。 - 刪除
reproduce_bug.py。
詳細決策背景請見 ADR 0004: Capture-First TDD
Dev Mode Guide (Current Behavior)¶
目的¶
這份文件描述 目前程式碼中的實際 Dev Mode 行為,作為唯一維護版本。
重點是避免再以舊設計稿(integration 層切換)解讀系統。
TL;DR¶
- 目前開關是
MCP_DEV_MODE,不是TIME_COMPASS_DEV_MODE。 - 切換點主要在
src/time_compass/mcp/與src/time_compass/domain/,不是 integration async_core。 MCP_DEV_MODE=1時,大量流程會改走 fixture/mock,許多測試若仍期待真實 API 分支會失敗。
單一開關¶
MCP_DEV_MODE=1 # 開啟 Dev Mode
MCP_DEV_MODE=0 # 關閉 Dev Mode(預設)
核心判斷函式:
src/time_compass/mcp/dev_mode.py:is_dev_mode()
目前 fixture 來源路徑為硬編碼:
assets/fixtures/snapshots
切換層級(實際)¶
1) MCP Tools 層¶
calendar_tools.py / task_tools.py / other_tools.py 在入口直接判斷 is_dev_mode(),為真時 early return mock/fixture 結果。
影響:
- 不會走真實 Google/Moodle API 呼叫。
- 參數可能被部分忽略(尤其時間範圍與 auth 相關分支)。
2) MCP Runtime 層¶
src/time_compass/mcp/runtime.py 在 fetch_context() 與 apply 流程也有 is_dev_mode():
fetch_context()會改用get_mock_resource_context()。- 套用行程 (
_apply_variant_to_calendar) 會回傳 mock success,不寫入真實日曆。
3) Domain 層(Gradio 間接生效)¶
SummaryModule 與 SchedulingModule 內部會檢查 is_dev_mode(),為真時改用 get_mock_resource_context()。
這代表:
gradio_app.py本身沒有直接讀 env。- 但 Gradio 的 scheduling pipeline 會經過 domain module,因此會間接受 Dev Mode 影響。
4) Script 層¶
scripts/run_planner_studio.py 預設會設 MCP_DEV_MODE=1(除非傳 --real)。
行為矩陣¶
| 元件 | MCP_DEV_MODE=0 |
MCP_DEV_MODE=1 |
|---|---|---|
| MCP tools | 真實 API / 正常 auth 路徑 | 直接回 fixture/mock |
| MCP runtime fetch_context | 走 integration 抓資料 | 走 get_mock_resource_context() |
| Planner apply | 真實寫入 Google Calendar | Mock 套用結果,不寫入 |
| Gradio Summary/Scheduling | 真實抓取資料 | 走 mock resource context |
| run_planner_studio.py | 需 --real 才進真實模式 |
預設即 dev mode |
測試策略¶
建議明確區分兩組測試:
dev mode suite- 測試前設定
MCP_DEV_MODE=1 -
斷言 fixture 路徑與 mock 行為
-
real-path suite - 測試前確保
MCP_DEV_MODE未設定或為0 - 斷言 auth error、API failure、真實路由行為
若把整包測試在 MCP_DEV_MODE=1 下執行,許多「應該走真實分支」的測試會被短路,出現大量非預期失敗。
常見誤解¶
誤解 1:Dev Mode 在 integration 層切換¶
現況不是。
目前主要是 MCP/domain 層直接分支,integration async_core 並沒有統一的 dev_mode.py 切換架構。
誤解 2:DEV_MODE_DATA_PATH 可控制 fixture 路徑¶
現況不是。
目前 mcp/dev_mode.py 使用硬編碼 assets/fixtures/snapshots。
相關檔案¶
src/time_compass/mcp/dev_mode.pysrc/time_compass/mcp/tools/calendar_tools.pysrc/time_compass/mcp/tools/task_tools.pysrc/time_compass/mcp/tools/other_tools.pysrc/time_compass/mcp/runtime.pysrc/time_compass/domain/summary/module.pysrc/time_compass/domain/schedule/module.pyscripts/run_planner_studio.py
最後更新:2026-03-02
領域模型 (Domain)
完整參考資料
Time Compass 文檔索引¶
本索引文檔記錄了從 GUIDE.md 衍生出的完整參考文檔,幫助開發者快速查找所需信息。
📚 已生成的參考文檔¶
1. DSPY/ ⭐⭐⭐¶
Time Compass 核心 DSPy 模組系統完整指南。
涵蓋內容: - Orchestrator 整體調用流程(C4 架構圖) - Router 決策層 - 判斷後續 pipeline - EmotionSupport 情緒層 - 溫柔陪伴 - Summary 回顧層 - 區間回顧 - Scheduling 排程層 - 任務等級判斷與方案生成 - 兩層路由決策流程(Router + SchedulingRouter) - 完整 Signature 簽章規格目錄
重點亮點: - 四層模組協作與 InteractionContext 通訊 - L1/L2/L3 任務等級判斷流程 - Streaming Listener 設計(漸進式回應) - 所有 Signature 輸入輸出規格
子文檔: - README.md - 模組系統導覽 - C4-DIAGRAM.md - Orchestrator 調用流程與職責 - USER-DIALOG-FLOW.md - Router 與 SchedulingRouter 決策流程 - SIGNATURE-DIRECTORY.md - 完整 Signature 簽章列表
讀者對象: 系統架構、Prompt 設計、DSPy 開發
2. MCP-TOOLS-GUIDE.md ⭐¶
涵蓋 Time Compass 的 15 個 MCP 工具完整實作手冊。
涵蓋內容: - 日曆工具(5 個):get_all_calendar_events、get_event_from_calendar、list_calendars、create_calendar_event、get_free_busy - 任務工具(4 個):get_all_tasks、list_tasklists、get_task_from_tasklist、create_task - 整合工具(6 個):get_moodle_events、get_time_context、launch_google_token_auth、launch_planner_studio、get_planner_studio_status、shutdown_planner_studio
核心主題: - API 呼叫決策矩陣(何時使用哪個工具) - 快取策略(TTL、大弧度判定) - Dev Mode 與時間平移邏輯 - 錯誤處理與復見策略 - Batch API 設計與效能考量
讀者對象: AI 助理開發、MCP 協定整合、API 設計學習
2. PLANNER-STUDIO-FRONTEND.md ⭐¶
零框架微前端的完整實作指南。
涵蓋內容: - 零框架設計的優勢與局限 - 核心模組分解(state、utils、contracts、view、zoom、api) - FullCalendar 6.1 配置與最佳實踐 - Shift+Wheel 物理縮放引擎的實現 - 前後端契約(Payload Schema) - 互動流程與狀態轉移 - 效能優化(DOM 批次化、快取策略) - 開發指南:新增功能案例
重點亮點: - 極速啟動(<500ms)無編譯步驟 - 衝突偵測邏輯細節 - 時間軸縮放的數學模型 - 60fps 動畫實現技巧
讀者對象: 前端開發、UI/UX 設計、Vanilla JS 學習
3. PROMPT-DESIGN-GUIDE.md ⭐⭐⭐¶
Prompt 工程的完整理論與實踐指南。
心理學基礎: - 認知負荷理論(CLT):3-5 個選項限制 - 認知行為治療(CBT):去災難化、行為啟動 - 葉杜定律:動機-壓力平衡 - 蔡加尼克效應:成就而非缺陷
舊版 DSPy 架構: - EmotionSupport 模組(INFJ 陪伴者) - RouterSignature(意圖識別與流程決策) - DraftPlan(方案級規劃,L1/L2/L3 分類) - DraftAction(行動級排程,≤60 分鐘) - AskQuestion(缺失資訊轉換為關鍵問題) - SummaryWriter(週回顧與成就解析)
新版 MCP 架構: - Prompt 資源的 MCP 工具化 - 與 DSPy 簽名的對應關係 - 完整流程示例(用戶說「我好累,想學編程」)
讀者對象: Prompt 工程師、AI 產品設計、心理學應用
4. INTERFACE-C4-ARCHITECTURE.md ⭐⭐¶
Gradio 應用介面架構的 C4 模型完整指南。
涵蓋內容: - C1 System Context:用戶、應用、Domain 層、外部整合的高層視圖 - C2 Container:Gradio Blocks web server 的組件分解(5 個 UI Tab、Event Handlers、Streaming Module) - C3 Components:各 Tab 詳細職責 - Chat Tab:聊天面板與事件橋接 - Scheduling Tab:兩段式排程 UI(draft 建立 → render artifacts) - Model Management Tab:模型版本檢視與重新載入 - Moodle Tab:Moodle 事件爬蟲 - Quick Test Tab:開發者快速測試介面 - Streaming Module:統一非同步 chunk 管理 - C4 Code:各 Tab 與 Streaming 的接口簽名、資料流、狀態機
重點亮點: - UI 層薄適配器模式(Tab = UI layout + event binding) - Async-first streaming 設計與 Fallback 機制 - 清晰的分層責任(Gradio ≠ Domain ≠ Integration)
讀者對象: 前端開發、UI/UX 設計、應用架構
5. STREAMING.md ⭐⭐⭐¶
非同步串流管理的深度指南與狀態機設計。
涵蓋內容:
- Streaming Module 核心設計(async generator)
- 三大函數:format_stream_output()、stream_main_output()、chat_fn()
- 詳細狀態機(orchestrator 載入 → chunk 迭代 → 累積與格式化 → yield → sleep)
- Domain Orchestrator 整合點與契約(OutputStream 接口期望)
- Gradio UI 回調適配
- 環境變數配置(STREAM_DELAY)
- 錯誤處理與 Fallback 機制
- CLI 測試工具與本地開發驗證
重點亮點: - 非阻塞型 UI 更新(async generator) - Field/Module 變化偵測與累積重置機制 - 完整的狀態轉移圖與例外流程
讀者對象: 後端流程設計、非同步程式設計、整合層開發
6. DDD-MODEL-ARCHITECTURE.md ⭐⭐¶
領域驅動設計多層模型完整指南。
四層架構: 1. Raw Layer(反腐層):API 原始映射,47+ 欄位 2. Internal/Domain Layer(業務邏輯):HTML 清洗、時區轉換 3. Read Layer(應用層):LLM 最佳化,簡化表示 4. TOON Layer(展示層):極致壓縮(83.7% Token 節省)
三大整合模組: - Google Calendar:datetime 物件保留,日期元件化 - Google Tasks:Eager-cast 至字串,狀態壓縮 - Moodle:HTML 清洗、時區統一、8 欄位精簡
核心設計決策: - 為何 Google 系列使用基類、Moodle 獨立演進 - 組成 vs 繼承的索引表設計 - 型別安全的 Pydantic 驗證策略 - TOON 編碼的壓縮機制(索引化、元件化、分組)
讀者對象: 後端架構、資料模型設計、型別安全
7. ERROR-HANDLING-DESIGN.md ✅¶
Rust 風格錯誤處理與 Railway-Oriented Programming
涵蓋內容:
- Result 型別 vs Exception:為什麼選 Result
- Railway-Oriented Programming(ROP)的核心概念
- is_err() / is_ok() / map() / flat_map() / bind() 實際應用
- Google API 認證錯誤 vs 業務邏輯錯誤的完整分類
- Batch API 層級錯誤處理策略與「部分成功」模式
- 日誌與可觀測性設計(模組路徑追蹤、三層級日誌)
- 復見策略、重試邏輯、斷路器模式
- async_core 的完整錯誤流轉案例
讀者對象: 後端開發、錯誤處理架構、非同步程式設計
8. TEST-SUITE-GUIDE.md ✅¶
Capture-First TDD 測試策略與實踐
涵蓋內容: - Capture-First TDD 的理念與優點 - 三層測試架構(L0 Live / L1 Unit / L2 Integration / L3 System) - Fixture 與 Snapshot 管理策略(三層數據層級) - Mock Context 與 Dev Mode 的關係 - 常見測試片段與最佳實踐(4 個範例) - CI/CD 整合與覆蓋率目標 - 新增測試的完整檢查表(紅-綠-重構) - Pytest 執行指令與本地開發流程
重點亮點: - 快照從真實 API 直接擷取,永遠保持一致 - 無憑證開發體驗(MCP_DEV_MODE=1) - 測試執行 < 5 秒,回饋迴圈光速
讀者對象: 測試工程、TDD 實踐、QA 工程
9. ../explanation/moodle_api/README.md ✅¶
Moodle 整合的深度指南(NTUST 專用)
涵蓋內容: - NTUST Moodle API 研究結果與 Raw JSON 分析 - 雙路徑架構:
Async OIDC 登入 + Selenium 爬蟲備備的設計哲學 - 課程(Course)、作業(Assignment)、事件(Event)的三層資料模型 - 非同步爬蟲實作模式、月份並發控制、逾時管理 - 認證流程(API 路徑 vs Selenium 路徑)與密碼管理的安全考量 - 快取與更新策略(無狀態設計 + 改善方案) - 功能限制與 8 大已知問題的排除步驟 - 官方 API 升級評估(混合方案最推薦)
重點亮點: - 12 個月份資料從 12 秒(串列)下降至 3-5 秒(並發) - 完整的容錯流程:API 失敗 → Selenium 備備 → 優雅降級 - 三層 DDD 模型:Raw → Internal → Read/TOON - 帳密完全不持久化,內存一次性使用
讀者對象: 爬蟲開發、非同步程式設計、API 整合
10. OAUTH/ ✅¶
Google OAuth、Moodle 帳密驗證的完整指南
涵蓋內容: - Google OAuth 架構(Session 模式 vs File 模式) - 兩條授權路徑(Gradio UI vs MCP 工具) - Moodle 獨立認證(帳密、OIDC 進階方案) - Token 管理與工作流(自動刷新、多程序協調) - 安全邊界與常見問題診斷 - 雲端部署考慮
子文檔: - GOOGLE_OAUTH.md - Google OAuth 架構深度 - MOODLE_AUTH.md - Moodle 驗證邊界
讀者對象: 系統管理、OAuth 授權流程、認證架構
11. INTEGRATION/ ✅¶
Google Calendar、Google Tasks、Moodle 的完整整合層設計
涵蓋內容: - C4 Model(L1 系統視圖到 L4 程式碼) - 四大核心模組(google_calendar、google_tasks、moodle、common) - Batch API 協調器與非同步並行抓取 - 統一例外體系(8 種 GoogleError) - Moodle 爬蟲深度分析與雙路徑架構
子文檔: - C4_MODEL.md - 全景架構(L1-L4) - MOODLE_DEEP_DIVE.md - Moodle 專項深度
讀者對象: Integration 層開發、API 協調、架構設計
🔗 與 GUIDE.md 的對應關係¶
| GUIDE.md 章節 | 對應文檔 | 狀態 |
|---|---|---|
| DSPy 模組系統(核心架構) | DSPY/ | ✅ |
| Orchestrator 與 Pipeline | DSPY/C4-DIAGRAM.md | ✅ |
| Router 與 SchedulingRouter | DSPY/USER-DIALOG-FLOW.md | ✅ |
| Signature 簽章規格 | DSPY/SIGNATURE-DIRECTORY.md | ✅ |
| MCP Tools 實作 | MCP-TOOLS-GUIDE.md | ✅ |
| Gradio App 介面架構 | INTERFACE-C4-ARCHITECTURE.md | ✅ |
| 非同步串流管理 | STREAMING.md | ✅ |
| Planner Studio 前端 | PLANNER-STUDIO-FRONTEND.md | ✅ |
| Prompt 設計(舊/新) | PROMPT-DESIGN-GUIDE.md | ✅ |
| DDD 分層架構 | DDD-MODEL-ARCHITECTURE.md | ✅ |
| Rust 風格錯誤處理 | ERROR-HANDLING-DESIGN.md | ✅ |
| 測試套件(Unit/Live/System) | TEST-SUITE-GUIDE.md | ✅ |
| Google OAuth 架構 | OAUTH/GOOGLE_OAUTH.md | ✅ |
| Moodle 驗證與爬蟲 | OAUTH/MOODLE_AUTH.md、INTEGRATION/MOODLE_DEEP_DIVE.md | ✅ |
| Integration Layer 整合 | INTEGRATION/C4_MODEL.md | ✅ |
| Mock/Dev Mode 系統 | 未規劃 | - |
| Batch API 設計 | MCP-TOOLS-GUIDE.md (部分)、INTEGRATION/C4_MODEL.md | ✅ |
📖 閱讀路徑建議¶
🚀 快速上手(30 分鐘)¶
- 讀 GUIDE.md 的「專案一句話說明」
- 閱讀 MCP-TOOLS-GUIDE.md 的「工具分類與架構模式」
- 試運行 Planner Studio 並測試一個工具
🏗️ 架構深度理解(2 小時)¶
- DDD-MODEL-ARCHITECTURE.md:理解資料流層級
- MCP-TOOLS-GUIDE.md:15 個工具的決策矩陣
- PROMPT-DESIGN-GUIDE.md:理解心理學驅動的排程邏輯
🎨 前端開發(1.5 小時)¶
- PLANNER-STUDIO-FRONTEND.md:零框架設計原則
- 檢查 src/time_compass/mcp/ui/ 程式碼
- 試修改樣式或新增功能
🧠 Prompt 工程(1 小時)¶
- PROMPT-DESIGN-GUIDE.md:心理學基礎 + 六大模組
- 查看 src/time_compass/domain/ 實際 Prompt 內容
- 觀察 src/time_compass/mcp/prompts/ 新版 MCP 資源
🔧 後端開發(2 小時)¶
- DDD-MODEL-ARCHITECTURE.md:型別安全與分層
- ERROR-HANDLING-DESIGN.md(待生成):錯誤處理策略
- 檢查 src/time_compass/integrations/ 實作
🧪 測試與驗證(1 小時)¶
- TEST-SUITE-GUIDE.md(待生成):TDD 策略
- tests/ 目錄結構與 Fixture 管理
- 執行現有測試套件
🎓 完整學習(8 小時)¶
按上述順序完整閱讀所有文檔,並搭配程式碼查閱。
💡 使用技巧¶
如何輔助 AI 導遊¶
在 GUIDE.md 附近提問時,告訴 AI:
「請根據 docs/reference/ 中的完整文檔,
幫我解釋 [特定概念] 並列舉程式碼範例。」
AI 可以交叉參照多份文檔,提供更完整的上下文。
搜尋特定概念¶
使用檔案中的「##」標題快速定位:
# 查找所有文檔中的「縮放」
grep -n "縮放\|Zoom" docs/reference/*.md
# 查找「快取策略」
grep -n "快取\|Cache" docs/reference/*.md
📊 文檔統計¶
| 文檔 | 行數 | 重點數量 | 程式碼範例 | 難度 |
|---|---|---|---|---|
| MCP-TOOLS-GUIDE | 320 | 15 工具 + 5 分類 | 10+ | ⭐⭐ |
| INTERFACE-C4-ARCHITECTURE | 280 | 4 層 + 6 Tab + Streaming | 8+ | ⭐⭐ |
| STREAMING | 320 | 3 函數 + 狀態機 + 錯誤處理 | 6+ | ⭐⭐⭐ |
| PLANNER-STUDIO-FRONTEND | 280 | 6 模組 + 8 最佳實踐 | 15+ | ⭐⭐⭐ |
| PROMPT-DESIGN-GUIDE | 250 | 6 模組 + 4 心理學理論 | 8+ | ⭐⭐⭐ |
| DDD-MODEL-ARCHITECTURE | 380 | 4 層 + 3 整合 + 設計決策 | 12+ | ⭐⭐⭐⭐ |
| ERROR-HANDLING-DESIGN | 380 | ROP + 9 層錯誤分類 + Retry | 15+ | ⭐⭐⭐⭐ |
| TEST-SUITE-GUIDE | 340 | 4 層測試 + Capture-First | 12+ | ⭐⭐⭐ |
| OAUTH README | 140 | 3 認證系統 + 工作流 | 5+ | ⭐⭐ |
| OAUTH/GOOGLE_OAUTH | 320 | 2 路徑 + 安全邊界 | 8+ | ⭐⭐ |
| OAUTH/MOODLE_AUTH | 160 | Moodle 帳密 + OIDC | 4+ | ⭐ |
| INTEGRATION/C4_MODEL | 680 | 4 層 + 3 模組 + 決策 | 10+ | ⭐⭐⭐⭐ |
| INTEGRATION/MOODLE_DEEP_DIVE | 350 | 雙路徑 + 3 層模型 + 8 問題 | 14+ | ⭐⭐⭐⭐ |
| 總計 | 4,890+ | 145+ | 127+ | - |
📝 如何貢獻新文檔¶
- 選定主題:從 GUIDE.md 「可以觀摩的部分」中挑選未涵蓋的領域
- 命名規範:
[UPPERCASE-WITH-DASH].md(如CACHE-STRATEGY.md) -
結構模板:
# [標題] ## 簡介 ## 核心概念 ## 實現細節 ## 最佳實踐 ## 常見問題 -
質量檢查:
- [ ] 包含原始程式碼位置引用
- [ ] 附加 1-2 個實務例
- [ ] 包含「為什麼」而非只有「是什麼」
- [ ] 與既有文檔交叉參照
🔄 更新頻率¶
| 文檔 | 最後更新 | 更新頻率 |
|---|---|---|
| MCP-TOOLS-GUIDE | 2026-03-02 | 新工具時更新 |
| INTERFACE-C4-ARCHITECTURE | 2026-03-02 | 新 Tab 或架構調整時 |
| STREAMING | 2026-03-02 | Streaming 邏輯變動時 |
| ERROR-HANDLING-DESIGN | 2026-03-02 | 例外設計變動時 |
| TEST-SUITE-GUIDE | 2026-03-02 | 測試架構調整時 |
| MOODLE-INTEGRATION-DEEP-DIVE | 2026-03-02 | API 變更時 |
| PLANNER-STUDIO-FRONTEND | 2026-03-02 | 前端重構時 |
| PROMPT-DESIGN-GUIDE | 2026-03-02 | Prompt 調整時 |
| DDD-MODEL-ARCHITECTURE | 2026-03-02 | 新資料源時 |
❓ 常見問題¶
Q: 我應該先讀哪份文檔?
A: 看你的背景。工程師讀 DDD,Prompt 工程師讀 PROMPT-DESIGN,前端開發讀 PLANNER-STUDIO。
Q: 文檔會同步程式碼更新嗎?
A: 當前是手動維護。重大功能變更時會更新。
最後更新於 2026-03-02 | 共 7 份完整參考文檔
Q: 能否線上協作編輯?
A: 目前存放於 git repo,歡迎 PR。
索引文檔生成於 2026-03-02 | Time Compass Reference Documentation Hub | 共 13 份完整參考文檔
設定與指南
環境變數參考手冊 (Reference)¶
Time Compass 使用 .env 檔案管理所有 API 憑證與運作模式。
1. 核心憑證 (Core Auth)¶
| 變數名稱 | 說明 | 必填 |
|---|---|---|
GOOGLE_CLIENT_SECRETS_FILE |
credentials.json 的絕對路徑 |
是 |
NTUST_ACCOUNT |
臺科大 Moodle 帳號 (學號) | 是 |
NTUST_PASSWORD |
臺科大 Moodle 密碼 | 是 |
2. 展示與開發模式 (Dev Mode)¶
| 變數名稱 | 說明 | 預設值 |
|---|---|---|
MCP_DEV_MODE |
是否進入零憑證展示模式 (1 開啟, 0 關閉) |
0 |
DEV_MODE_DATA_PATH |
Mock 資料路徑(目前程式碼未啟用,固定讀 assets/fixtures/snapshots) |
(保留) |
3. 進階設定¶
| 變數名稱 | 說明 |
|---|---|
GOOGLE_CLIENT_SECRETS_DICT |
(可選) 憑證 JSON 內容字串,若設定則優先於檔案。 |
PYTHONUTF8 |
建議設為 1 以避免 Windows 編碼問題。 |
4. 變數加載邏輯¶
- 優先讀取作業系統環境變數。
- 其次讀取根目錄下的
.env檔案。 - 如果
GOOGLE_CLIENT_SECRETS_FILE指向的路徑不存在且GOOGLE_CLIENT_SECRETS_DICT為空,系統啟動時會拋出ConfigurationError。
[!IMPORTANT] 請勿將
.env或token.json提交到 Git 倉庫。本專案已包含.gitignore保護這些隱私檔案。
5. 延伸閱讀¶
- LiteLLM Proxy 與 Gradio 配置指南:
docs/reference/litellm-proxy-guide.md
Planner Studio 座標系與縮換算邏輯 (Coordinate Transformation)¶
本文件說明 Planner Studio 縮放引擎(planner_zoom.js)如何處理螢幕座標(Client Y)、視圖內座標(Layout Y)與時間點(Minute)之間的轉換,這套邏輯確保了縮放時「滑鼠指向的時間點」能維持在螢幕上的相同位置。
1. 座標系定義¶
- Client Y (
clientY): 相對於瀏覽器視窗頂部的像素座標。 - Layout Y (
pointerLayoutY): 在滾動容器(Scroller)內部的視覺相對座標,已校正 CSS Transform Scale。 - Content Y (
contentY / targetContentY): 整個可滾動畫板的絕對高度座標(包含被捲上去的部分)。 - Minute: 自當天 00:00 開始計算的分鐘數(0 到 1440)。
A. 關鍵指標獲取 (getSlotMetrics)¶
縮放引擎會先抓取第一個時間插槽(Slot)的 DOM 狀態:
- safeScaleY: 容器的累積縮放比例。
- ppm (Pixels Per Minute): 每分鐘佔用的像素高度。
- firstMinute: 第一個插槽代表的分鐘數。
- firstYLayout: 第一個插槽相對於容器頂部的 Layout Y 座標。
B. 螢幕座標轉分鐘 (getMinuteAtClientY)¶
當滑鼠滾動時,我們需要知道滑鼠目前指在「哪一分鐘」:
1. 轉換為佈局座標:
pointerLayoutY = (clientY - scrollerRect.top) / safeScaleY
2. 加上捲軸偏移:
contentY = scroller.scrollTop + pointerLayoutY
3. 根據 PPM 換算出分鐘:
minute = firstMinute + ((contentY - firstYLayout) / ppm)
C. 分鐘轉捲軸位置 (scrollMinuteToClientY)¶
縮放比例改變後,我們必須重新調整 scrollTop,讓目標 minute 仍出現在該 clientY:
1. 計算目標絕對高度:
targetContentY = firstYLayout + ((minute - firstMinute) * ppm)
2. 推導目標捲軸位置:
targetScrollTop = targetContentY - pointerLayoutY
3. 套用捲軸更新:
scroller.scrollTop = targetScrollTop (或進行平滑插值)
3. 縮放動畫 (stepZoomFrame)¶
縮放過程使用 requestAnimationFrame 進行線性插值(Lerp)與緩動(Easing):
- 每一幀更新 currentPxPerMinute。
- 調用 FullCalendar.updateSize() 同步 DOM 佈局。
- 立即調用 scrollMinuteToClientY 補償因高度變化導致的視覺偏移。
4. FullCalendar 整合與渲染指令¶
由於 FullCalendar 的佈局系統較為複雜,縮放引擎需精準並時地調用以下指令以確保視覺同步:
A. 動態屬性更新¶
setOption('contentHeight', height): 直接修改日曆畫板的總高度。在縮放時,我們計算24 * 60 * currentPxPerMinute並即時寫入,這會觸發 FullCalendar 重新計算所有事件的 Top/Height。updateSize(): 強制 FullCalendar 重新讀取容器尺寸並重新佈局。這是縮放補償(Scroll Jump Fix)前的關鍵步驟,否則scroller.scrollHeight可能尚未更新。batchRendering(fn): 用於大量更新事件(如搜尋或篩選)時,將多次 DOM 變更合併為一次渲染,避免畫面閃爍。
B. CSS 變數控制 (Theming)¶
為了讓 FullCalendar 內部元件(如 Slot、Event、Axis)與縮放級別連動,我們在 .fc 根節點注入了多個自定義變數:
| 變數名 | 作用對象 | 換算邏輯舉例 |
|---|---|---|
--slot-height |
.fc-timegrid-slot |
ppm * 15 (每 15 分鐘高度) |
--event-font-size |
事件標題文字大小 | 根據縮放比例在 9px ~ 18px 之間線性插值 |
--axis-font-size |
時間軸文字大小 | 根據縮放比例在 10px ~ 15px 之間線性插值 |
--event-pad-y/x |
事件內部填充 (Padding) | 隨縮放級別動態調整,確保小比例時不會留白過多 |
這些變數在 planner.css 中被引用(例如 height: var(--slot-height) !important;),達成「JS 計算、CSS 渲染」的解耦設計。
5. 特殊處理:吸附 (Snapping)¶
在低縮放倍率(currentPxPerMinute < 3.5)時,錨點分鐘數會自動吸附至最近的 15 分鐘(Math.round(rawMin / 15) * 15),以提升視覺穩定感。
Time Compass DDD 多層模型架構完整指南¶
簡介:DDD 分層的理念與價值¶
Domain-Driven Design (DDD) 強調分離關切點與語意一致性。Time Compass 採用的四層模型架構:
- Raw Layer (反腐層): 直接映射外部 API 的資料結構,作為系統邊界。
- Internal/Domain Layer: 核心業務轉換(清洗 HTML、時區統一、去重、型別正規化)。
- Read Layer (應用層): 針對 LLM 消費最佳化的簡化模型,包含計算字段(狀態、壓縮格式)。
- TOON Layer (展示層): 極致壓縮格式,減少 Token 消耗達 83.7%,同時保留完整語義。
此分層設計的核心價值: - 邊界隔離: 外部 API 變更不直接衝擊業務邏輯。 - 型別安全: Pydantic 驗證在每層邊界強制契約。 - 效能最佳化: 延遲轉換,在最後一刻(TOON 編碼)進行序列化。 - 可維護性: 新增資料來源時,只需實作四個模型檔案,即可整合到全棧。
資料流層級圖¶
┌─────────────────────────────────────────────────────────────────┐
│ 外部資料來源 │
├─────────────────────────────────────────────────────────────────┤
│ Google Calendar API │ Google Tasks API │ Moodle AJAX API │
└───────┬─────────────────┴──────┬────────────┴──────────┬────────┘
│ │ │
│ JSON Dict │ JSON Dict │ JSON Dict
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ Raw Layer (反腐層) │
├─────────────────────────────────────────────────────────────────┤
│ GoogleEventRaw GoogleTaskRaw RawEvent │
│ (47+ fields, camelCase) (18 fields) (55+ fields) │
│ ※ Google 系列目前 Bypass ※ Google 系列 Bypass ✓ 直接使用 │
└─────┬──────────────────────┬─────────────────────┬───────────────┘
│ from_dict() │ from_dict() │ from_raw()
│ (直接使用 dict) │ (直接使用 dict) │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ Internal/Domain Layer (業務邏輯層) │
├─────────────────────────────────────────────────────────────────┤
│ GoogleEventRead GoogleTaskRead MoodleEvent │
│ (datetime 物件保留) (eager-cast to str) (HTML清洗,時區轉) │
└─────┬────────────────────┬─────────────────────┬────────────────┘
│ to_toon_calendar() │ to_toon_tasklist() │ to_toon_moodle()
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ Read Layer (應用層 - LLM最佳化) │
├─────────────────────────────────────────────────────────────────┤
│ ToonCalendar ToonTaskList MoodleEventRead │
│ - 索引化地點、重複規則 - 按狀態分組 - 8 字段精簡版 │
│ - 日期元件化 - 去重、父子關係 - 語意化狀態 │
└────────────────┬────────────────┬───────────────────────┬────────┘
│ │ │
└────────┬────────┴───────────┬──────────┘
│ safe_encode() │
▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ TOON Layer (展示層/傳輸層) │
├─────────────────────────────────────────────────────────────────┤
│ TOON 格式字串 (Schema-less Header + 縮排資料) │
│ 壓縮率: 83.7% tokens, 90.5% chars │
└─────────────────────┬────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ MCP Tool Interface / BFF (外部介面層) │
├─────────────────────────────────────────────────────────────────┤
│ get_all_calendar_events │ get_all_tasks │ get_moodle_events │
│ get_event_from_calendar │ list_tasklists│ launch_planner_studio │
│ create_calendar_event │ create_task │ │
└─────────────────────────────────────────────────────────────────┘
各整合模組的模型路徑¶
1. Google Calendar 模型鏈¶
| 層級 | 模型類別 | 檔案路徑 | 職責 |
|---|---|---|---|
| Raw | GoogleEventRaw |
integrations/google_calendar/models/models_raw.py |
完整 API 映射(47+ 欄位),camelCase |
| Read | GoogleEventRead |
integrations/google_calendar/models/models_read.py |
轉換流程:from_dict(api_dict) → datetime 物件保留 |
| TOON | ToonCalendar |
integrations/google_calendar/models/models_toon.py |
索引化地點、重複規則,月份分組,日期元件化 |
轉換函數鏈:
API JSON → GoogleEventRead.from_dict(raw_dict, calendar_id, calendar_summary)
→ (datetime 物件) → safe_encode() → TOON 字串
特殊處理:
- 全天偵測: 優先判定 date 欄位存在,否則檢查 endTimeUnspecified
- 提醒格式: popup:10m 格式表示提醒方式與分鐘數
- 重複事件: 保留原始 RRULE,在 Planner 層做展開(Backend Expansion)
2. Google Tasks 模型鏈¶
| 層級 | 模型類別 | 檔案路徑 | 職責 |
|---|---|---|---|
| Raw | GoogleTaskRaw |
integrations/google_tasks/models/models_raw.py |
完整 API 映射(18 欄位),camelCase |
| Read | GoogleTaskRead |
integrations/google_tasks/models/models_read.py |
Eager-cast 至字串格式,狀態壓縮 |
| TOON | ToonTaskList |
integrations/google_tasks/models/models_toon.py |
按狀態/月份分組,父子用 ID 參照 |
轉換函數鏈:
API JSON → GoogleTaskRead.from_dict(raw_dict, parent_title, tasklist_id, tasklist_title)
→ (所有日期欄位 eager-cast 為字串) → safe_encode() → TOON 字串
特殊處理:
- 全天預設: Tasks API 的 due 欄位永遠是 T00:00:00.000Z(UTC 午夜),轉為 UTC+8 時自動變成 08:00
- 狀態壓縮: ✓ MM-DD 或 pending(節省 Token)
- 去重邏輯: Core 層在 list_tasklists 中過濾重複項
3. Moodle 模型鏈¶
| 層級 | 模型類別 | 檔案路徑 | 職責 |
|---|---|---|---|
| Raw | RawEvent |
integrations/moodle/models/models_raw.py |
原始爬蟲回應(55+ 欄位) |
| Internal | MoodleEvent |
integrations/moodle/models/models_internal.py |
HTML 清洗、時區轉換 (UTC+8)、欄位正規化 |
| Read | MoodleEventRead |
integrations/moodle/models/models_read.py |
語意化狀態、課程索引參照、8 欄位精簡 |
| TOON | (via MoodleEventRead) |
同上 | JSON → TOON 編碼 |
轉換函數鏈:
爬蟲 JSON → RawEvent (驗證)
→ MoodleEvent.from_raw() (HTML清洗、時區轉UTC+8、datetime物件)
→ MoodleEventRead.from_internal(event, course_idx) (語意化狀態、索引參照)
→ safe_encode() → TOON 字串
特殊處理:
- HTML 清洗: description 欄位自動移除標籤 (<p>, <br> 等)
- 時區統一: UNIX timestamp → datetime.fromtimestamp(ts, tz=UTC+8)
- 狀態計算: 比對 open_at, due_at, cutoff_at 與當前時間 → Open/Not yet open/Overdue/Closed
- 課程索引: c1, c2, c3 等短標籤參照外部索引表
RawModel 規格(API 原始回應)¶
Google Calendar - GoogleEventRaw¶
主要欄位 (共 47+):
- 基本資訊: id, summary, description, location
- 時間資訊: start, end, endTimeUnspecified, originalStartTime
- 重複與例外: recurrence, recurringEventId
- 參與者: organizer, attendees
- 提醒: reminders {useDefault, overrides: [{method, minutes}]}
- 會議: conferenceData
- 顏色與可見性: colorId, visibility, transparency
- 中繼資料: kind, etag, created, updated (RFC3339)
設計決策:
- ✓ 完整欄位定義,作為 API 版本變更的早期預警
- ⚠️ 目前 未被 Google 整合直接使用(from_dict 接受裸 dict),僅作文檔參考
- 📌 當需要升級至 "嚴格模式" 時,應改用 GoogleEventRaw.model_validate(data) 驗證
Google Tasks - GoogleTaskRaw¶
主要欄位 (共 18):
- 識別與內容: kind, id, title, notes
- 時間欄位: updated, due, completed (RFC3339)
- 狀態與關係: status, hidden, deleted, parent, position
- 中繼資料: etag, selfLink, webViewLink, links
- 複雜結構: assignmentInfo
設計決策: - ✓ Google Tasks API 結構相對簡單,易於驗證 - ⚠️ 同 Calendar,目前未直接被 Read 層引用
Moodle - RawEvent¶
主要欄位 (共 55+):
- 事件識別: id, name, component, modulename, instance
- 內容: description (HTML), location, descriptionformat
- 時間資訊: timestart, timeduration, timesort, timeusermidnight, timemodified
- 課程與群組: course, categoryid, groupid, userid
- 狀態資訊: visible, overdue, eventtype
- 圖示與訂閱: icon, subscription
設計決策:
- ✓ 直接被 MoodleEvent.from_raw() 驗證和引用,是唯一的入口檢查
- 📌 Moodle 爬蟲回應因頁面變更風險高,Raw 層驗證非常重要
ReadModel 規格(業務轉換後的簡化模型)¶
Google Calendar - GoogleEventRead¶
核心欄位:
class GoogleEventRead(BaseGoogleRead):
# 識別與基本資訊
id: str # 事件 ID
summary: str # 標題
description: Optional[str] # 詳細描述
title: Optional[str] # 相容字段
# 時間資訊 (datetime 物件,UTC+8)
start: Optional[datetime] # 開始時間
end: Optional[datetime] # 結束時間
all_day: bool # 是否全天
original_start: Optional[datetime] # 原始預定時間
end_time_unspecified: Optional[bool] # 結束時間未指定
# 重複與排程
recurrence: Optional[str] # 人類可讀的重複規則
rrule: Optional[str] # 原始 RRULE
recurring_event_id: Optional[str] # 父重複事件 ID
# 日曆與位置
calendar_id: Optional[str] # 來源日曆 ID
calendar_summary: Optional[str] # 來源日曆標題
source: Optional[str] # 日曆名稱
color: Optional[str] # 日曆顏色 (Hex)
location: Optional[str] # 地點
# 提醒
reminders: Optional[Union[str, Dict]] # e.g. "popup:10m"
model_config = ConfigDict(
arbitrary_types_allowed=True, # 允許 datetime 物件
populate_by_name=True,
extra="ignore"
)
轉換邏輯 (from_dict):
1. 提取基本欄位 (id, summary, description)
2. 時間轉換: to_datetime(start_raw) → datetime (UTC+8)
3. 全天偵測: date 欄位存在 OR endTimeUnspecified
4. 提醒格式化: {overrides} → "popup:10m"
5. 返回 GoogleEventRead (datetime 物件保留,不序列化)
設計決策:
- ✓ datetime 物件保留,符合 Python 型別安全
- ✓ 延遲序列化至 TOON 層,避免多次字串轉換
- ⚠️ Pydantic 模型設定 arbitrary_types_allowed=True 以支援 datetime
Google Tasks - GoogleTaskRead¶
核心欄位:
class GoogleTaskRead(BaseGoogleRead):
id: str # 任務 ID
title: str # 任務標題
notes: Optional[str] # 詳細說明
# 時間欄位 (已 eager-cast 至字串,UTC+8)
due: Optional[str] # 截止日期 (YYYY-MM-DD HH:MM)
completed: Optional[str] # 完成時間 (YYYY-MM-DD HH:MM)
# 狀態 (預計算,節省 Token)
status: str # "✓ MM-DD" 或 "pending"
# 任務列表與層級
tasklist_id: Optional[str] # 來源任務列表 ID
tasklist_title: Optional[str] # 來源任務列表標題
parent: Optional[str] # "Title (id=TASK_ID)" 或 None
# 固定字段
all_day: bool = True # Tasks 永遠全天
轉換邏輯 (from_dict):
1. 提取基本欄位 (id, title, notes)
2. 時間轉換 & eager-cast: to_datetime(due) → format_for_llm() → str
3. 狀態計算: completed 存在 → "✓ MM-DD", 否則 "pending"
4. 父任務格式化: parent_id 存在 → f"{parent_title} (id={parent_id})"
5. 返回 GoogleTaskRead (所有時間皆為字串)
設計決策: - ✓ Eager cast 簡化 TOON 層,針對 Tasks 簡單結構的最佳化 - ⚠️ 與 Calendar 層策略的差異體現了「漸進最佳化」哲學
Moodle - MoodleEventRead¶
核心欄位:
class MoodleEventRead(BaseModel):
title: str # 事件標題
course_idx: str # 課程索引 (c1, c2, c3...)
status: str # Open/Not yet open/Overdue/Closed
due_date: str # YYYY-MM-DD 或 YYYY-MM-DD HH:MM
description: str # 截斷至 200 字元
semester: str # 114-1 格式
# 條件式欄位(只在特定狀態出現,節省 Token)
open_date: Optional[str] # 僅 "Not yet open" 狀態
cutoff_date: Optional[str] # 僅 "Overdue (Grace period)" 狀態
轉換邏輯 (from_internal):
1. 提取事件標題、課程索引
2. 狀態計算 (根據時間戳)
3. 條件式欄位: 只在相應狀態時才包含 open_date 或 cutoff_date
4. 描述截斷至 200 字元,超過則加 "..."
5. 學期轉換: "114學年度 第 1 學期" → "114-1"
設計決策:
- ✓ 8 欄位是精簡與完整性的平衡點
- ✓ 條件式欄位符合 TOON 變長記錄設計
- 📌 課程索引參照外部的 CourseCatalog 索引表
TOON 格式規格與壓縮率分析¶
TOON 格式特性¶
Token-Oriented Object Notation (TOON) 是極致壓縮格式,核心特性:
- Schema-less Header: 首行定義欄位名稱,後續行直接列值(消除重複 Key)
- 外部索引化: 重複值(如地點、重複規則)提取至索引表,內部用短標籤參照
- 日期元件化: ISO DateTime 分解為最小單位(日、週幾、時分)
- 語義分組: 按月份(日曆)或按狀態(任務)組織資料
- 型別推斷: 透過 Header 推斷欄位型別,省略不必要的引號
TOON 格式範例¶
Google Calendar TOON¶
# calendar_list[2]{id,summary,color}:
# L1: "會議室 1" (Location Index)
# R1: "FREQ=WEEKLY;BYDAY=WE" (Recurrence Index)
calendar_name[10]{id,summary,lid,rid,notes,st_d,st_wd,st_hm,en_d,en_hm}:
2026-01: (2 events)
event_1,標題 A,0,R1,"會議筆記",15,3,09:00,15,10:00
event_2,標題 B,L1,0,"",16,4,14:00,16,15:30
2026-02: (1 event)
event_3,標題 C,L1,R1,"待確認",20,2,10:00,20,11:30
解碼說明:
- st_d=15: 開始日期為 15 號
- st_wd=3: 開始星期三(1=一, 7=日)
- st_hm=09:00: 開始時分
- en_d=15: 結束日期(同一天用數字,不同日期標記為 15+1)
- lid=0: 地點索引 0(未指定)
- rid=R1: 重複規則索引
Google Tasks TOON¶
tasklist_name[5]{id,title,due,completed,status}:
pending: (3 tasks)
task_1,準備簡報,2026-02-15,0,pending
task_2,回覆郵件,2026-02-20,0,pending
task_3,修改程式碼,0,0,pending
completed: (2 tasks)
task_4,提交報告,2026-01-30,2026-02-01,✓ 02-01
task_5,參加會議,2026-01-25,2026-01-25,✓ 01-25
Moodle TOON¶
moodle_events[8]{title,course_idx,status,due_date,description,semester,open_date,cutoff_date}:
c1_prog_assign_3,c1,Open,2026-02-20 23:59,"請完成第 3 題程式設計...","114-1",0,0
c2_quiz_midterm,c2,Not yet open,2026-03-15 10:00,"期中考重點:Ch1-Ch5..","114-1",2026-03-10 00:00,0
c3_project_final,c3,Overdue (Grace period),2026-02-15 23:59,"期末專題:實作..","114-1",0,2026-02-22 23:59
壓縮效益實測數據¶
測試資料: - Google Calendar: 188 個事件(含重複、全天、跨年) - Google Tasks: 47 個任務(含父子關係) - Moodle: 10 個課程事件
壓縮結果:
| 指標 | JSON 標準 | TOON 格式 | 壓縮率 |
|---|---|---|---|
| 字元數 (Chars) | 304,082 | 28,789 | 90.5% ↓ |
| Token 數 (GPT-4o) | 96,772 | 15,800 | 83.7% ↓ |
| 資訊密度 | 1× | 6.1× | - |
結論: - 在相同 Context Window 下,TOON 格式允許 AI 處理多出 6.1 倍 的行程資料 - Token 節省達 83.7%,直接降低 LLM API 成本與推理延遲 - 人類可讀性保持良好(層次化 YAML 式縮排)
型別安全與 Pydantic 驗證¶
漸進式驗證策略¶
API 原始資料
↓ [Raw Layer - 可選]
Pydantic 驗證 (GoogleEventRaw, RawEvent 等)
↓ [Read Layer - 強制]
Model.from_dict() / from_raw() [業務邏輯]
↓ [檢驗所有欄位都被顯式轉換]
讀取模型 (GoogleEventRead, MoodleEventRead)
↓ [TOON Layer - 自動]
safe_encode() [型別感知序列化]
↓ [輸出層 - 最終檢查]
MCP 工具回傳
關鍵的 Pydantic 配置¶
GoogleEventRead & GoogleCalendarListRead:
from pydantic import BaseModel, ConfigDict
class GoogleEventRead(BaseModel):
id: str
start: Optional[datetime]
end: Optional[datetime]
model_config = ConfigDict(
arbitrary_types_allowed=True, # 允許 datetime 物件
populate_by_name=True, # alias 相容
extra="ignore" # 忽略 API 額外欄位
)
MoodleEventRead:
class MoodleEventRead(BaseModel):
title: str
course_idx: str
status: str
open_date: Optional[str] = None # 條件式欄位
cutoff_date: Optional[str] = None
model_config = ConfigDict(
extra="ignore",
from_attributes=True # 支援 ORM 模式
)
Field Validators¶
MoodleEvent 的 HTML 清洗:
from pydantic import field_validator
class MoodleEvent(BaseModel):
description: str = ""
@field_validator("description", mode="before")
@classmethod
def clean_description(cls, v: Any) -> str:
"""自動清除 HTML 標籤"""
if not isinstance(v, str):
return ""
# 移除 <p>, <br>, 等
clean = re.sub(r"<[^>]+>", "", v)
return clean.replace(" ", " ")
型別安全反面案例與改進¶
問題(舊實作的型別混亂):
# ❌ 型別混亂
start: Union[datetime, str] # 何時是 datetime,何時是字串?
改進(現行實作):
# ✓ 清晰的型別約定
start: Optional[datetime] # 總是 datetime
# 序列化延遲至最後一刻(TOON Layer)
toon_str = safe_encode(event)
繼承與組成的設計決策¶
繼承層級¶
BaseModel (Pydantic)
│
├─ BaseGoogleRequest ─────→ ListEventsRequest, InsertEventRequest, ...
│
├─ BaseGoogleRaw ─────────→ GoogleEventRaw, GoogleTaskRaw, ...
│
├─ BaseGoogleRead ────────→ GoogleEventRead, GoogleTaskRead, ...
│
└─ BaseModel (直接繼承)
├─ MoodleEvent (Internal/Domain)
├─ MoodleEventRead (Read Layer)
└─ CourseCatalog, LocationIndexItem, ...
設計決策詳解¶
1. 為何使用 BaseGoogleRequest?¶
目的: 統一 Google API 請求的轉換介面。
具體實現(Calendar ListEventsRequest):
class ListEventsRequest(BaseGoogleRequest):
BATCH_ENDPOINT = "https://www.googleapis.com/calendar/v3/calendars/{calendarId}/events"
calendar_id: str
time_min: Optional[str] # RFC3339
time_max: Optional[str]
single_events: bool = False
def to_http(self) -> Dict:
"""轉換為 HTTP 組件"""
return {
"method": "GET",
"url": self.BATCH_ENDPOINT.format(calendarId=self.calendar_id),
"params": {"timeMin": self.time_min, "timeMax": self.time_max}
}
def parse_response(self, data: Dict) -> PageEnvelope[GoogleEventRaw]:
"""API 返回 {items: [...], nextPageToken?: ...}"""
return PageEnvelope(
items=[GoogleEventRaw(**item) for item in data.get("items", [])],
next_page_token=data.get("nextPageToken")
)
優勢: - ✓ 統一的批量 API 轉換邏輯 - ✓ 易於新增新的請求類型 - ⚠️ Moodle 並不使用此基類(爬蟲轉換不同)
2. 為何 BaseGoogleRaw 隱含 kind 與 etag?¶
設計意圖: 反映 Google API 的通用欄位規範。
用途:
- kind 用於型別識別(雖然通常已知)
- etag 用於樂觀更新鎖(insert/update 時需提供)
3. 為何 Moodle 不使用基類,而 Google 系列使用?¶
原因: - Google APIs 遵循強標準化 → 基類有高價值 - Moodle 是逆向工程爬蟲 → 各字段排版差異大 → 基類價值低 - Moodle 有專屬的 Internal Layer → 獨立演進
4. 為何 PageEnvelope 繼承自 BaseGoogleRaw?¶
class PageEnvelope(BaseGoogleRaw, Generic[TItem]):
next_page_token: Optional[str]
items: List[TItem]
設計決策:
- parse_response() 的返回型別統一為 PageEnvelope[BaseGoogleRaw]
- 這允許單一資源請求和列表請求回應都被統一處理
- ⚠️ 有技術債:PageEnvelope 繼承 BaseGoogleRaw 有語義不清之嫌
- 📌 可改進方案:獨立的 Response 基類取代現行継承
5. 組成 vs. 繼承:索引表設計¶
組成模式(TOON 層索引化):
class ToonCalendar(BaseModel):
source: ToonCalendarSource # 組成
location_index: List[LocationIndexItem] # 組成
recurrence_index: List[RecurrenceIndexItem]
month: Dict[str, Dict[str, List[ToonEvent]]]
實現細節:
def to_toon_calendar(events: List[GoogleEventRead]) -> ToonCalendar:
"""轉換步驟"""
location_idx, location_map = _build_location_index(events)
recurrence_idx, recurrence_map = _build_recurrence_index(events)
month_struct = defaultdict(lambda: defaultdict(list))
for event in events:
start_parts = _parse_datetime_parts(event.start)
toon_event = ToonEvent(
id=event.id,
summary=event.summary,
lid=location_map.get(event.location, 0),
rid=recurrence_map.get(event.rrule, 0),
st_d=start_parts["day"],
# ...
)
month_key = start_parts["month"]
day_key = str(start_parts["day"])
month_struct[month_key][day_key].append(toon_event)
return ToonCalendar(
source=ToonCalendarSource(...),
location_index=location_idx,
recurrence_index=recurrence_idx,
month=dict(month_struct)
)
資料流與型別檢查點¶
完整的 Google Calendar 資料流¶
1. API 取得
└─ GET /calendar/v3/calendars/{calendarId}/events?timeMin={rfcTime}&...
→ JSON Response: {items: [{id, summary, start: {dateTime}, ...}, ...]}
2. Raw 層驗證 (可選)
└─ GoogleEventRaw.model_validate(raw_item)
✓ 驗證欄位型別、rename camelCase
⚠️ 目前被 bypass,直接使用字典
3. Read 層轉換 (必須)
└─ GoogleEventRead.from_dict(
raw_dict,
calendar_id="primary",
calendar_summary="我的日曆"
)
→ to_datetime(start_raw) # RFC3339 → datetime(tz=UTC+8)
→ GoogleEventRead(
id="event123",
summary="會議",
start=datetime(..., tzinfo=UTC+8),
end=datetime(..., tzinfo=UTC+8),
all_day=False
)
4. TOON 層編碼 (最後一刻序列化)
└─ safe_encode(events_list)
→ json.dumps(events, default=default_serializer)
* default_serializer 檢測 datetime 物件
* 呼叫 format_for_llm(dt) → "2026-01-15 09:00"
→ toon_format.encode(clean_data)
* Schema-less Header: "id,summary,start,end,all_day,..."
* 日期元件化、地點索引化
* 結果: TOON 字串
5. MCP 工具輸出
└─ return safe_encode({"events": toon_calendar})
錯誤處理與型別驗證¶
ValidationError 捕捉點:
# 1. Raw 層驗證 (若啟用)
try:
raw_event = GoogleEventRaw.model_validate(api_response)
except PydanticValidationError as e:
logger.error(f"Raw validation failed: {e}")
# 決策: fallback 至 from_dict() 直接使用 dict
# 2. Read 層轉換
try:
read_event = GoogleEventRead.from_dict(api_response, ...)
except (KeyError, TypeError, ValueError) as e:
logger.error(f"from_dict failed: {e}")
# 決策: 跳過此事件,記錄故障事務
# 3. TOON 序列化
try:
toon_str = safe_encode(events)
except Exception as e:
logger.error(f"safe_encode failed: {e}")
# 決策: fallback 至標準 JSON
toon_str = json.dumps(events, default=str)
貫穿各整合的統一設計原則¶
1. 時間統一:UTC+8 Asia/Taipei¶
所有模型在通過 Read Layer 時,時間欄位須經由 to_datetime() 轉換為 UTC+8 timezone-aware datetime:
from time_compass.utils.time_tool import to_datetime, format_for_llm, USER_TZ
# USER_TZ = ZoneInfo("Asia/Taipei")
# 轉換範例
rfc3339_str = "2026-01-15T09:00:00+08:00"
dt = to_datetime(rfc3339_str) # → datetime(2026,1,15,9,0,tz=UTC+8)
unix_timestamp = 1737007200
dt = datetime.fromtimestamp(unix_timestamp, tz=USER_TZ)
# LLM 格式化
llm_str = format_for_llm(dt) # → "2026-01-15 09:00"
2. 字串清洁:HTML 移除 & 特殊符號轉譯¶
Moodle HTML 清洗:
def _clean_html(text: str) -> str:
if not text:
return ""
clean = re.sub(r"<[^>]+>", "", text)
clean = clean.replace(" ", " ")
clean = clean.replace("&", "&")
return clean.strip()
3. ID 與索引參考設計¶
課程索引參照 (Moodle):
# 外部索引表 (CourseCatalog)
catalogs = [
CourseCatalog(idx="c1", name="資料結構"),
CourseCatalog(idx="c2", name="演算法"),
]
# MoodleEventRead 內只保留 course_idx = "c1"
# TOON 編碼時解析 c1 對應的課程名稱
4. 遺漏欄位的策略¶
| 層級 | 遺漏欄位處理 | 例子 |
|---|---|---|
| Raw | 使用 Optional[type] 允許遺漏 |
location: Optional[str] = None |
| Read | 使用預設值或 None |
all_day: bool = False |
| TOON | 條件式欄位或索引 0/empty | open_date: Optional[str] = None |
延伸閱讀與進一步優化方向¶
已知技術債¶
- Google Raw Layer 形同虛設
- 狀態: ⚠️ Active Issue
-
改進方案: 升級所有
from_dict()至model_validate()+from_raw() -
PageEnvelope 語義不清
- 狀態: 📌 Technical Debt
-
改進方案: 分離
Response[T]與ListResponse[T]基類 -
Moodle 缺乏標準化契約
- 狀態: ⚠️ Fragile
- 改進方案: 增加網頁版本嗅探與回歸測試
潛在優化¶
- TOON 格式版本化 (v1, v2 ...)
-
支援格式演進,向後相容
-
按需加載索引
-
大型日曆只輸出 Hot 月份的完整索引
-
增量更新 (Deltas)
-
記錄上次 fetch 的 etag,只傳遞變更部分
-
草稿模式整合
- 將 Planner 生成的草稿事件納入統一模型
總結¶
Time Compass 的 DDD 四層架構提供了: - 型別安全: Pydantic 在每層邊界強制驗證 - 關注點分離: Raw / Internal / Read / TOON 各司其職 - 性能最佳化: 延遲序列化 + TOON 極致壓縮 (83.7% token 節省) - 易擴展性: 新增資料來源時遵循四層模式即可無縫整合
此架構平衡了完整性 (捕捉所有 API 欄位) 與簡潔性 (LLM 讀取時的極簡化),是建構複雜多源整合系統的參考典範。
參考資源¶
- 型別檢查點與錯誤處理:見 ERROR-HANDLING-DESIGN.md
- 測試策略:見 TEST-SUITE-GUIDE.md
- MCP 工具整合:見 MCP-TOOLS-GUIDE.md
- 資料模型程式碼:
src/time_compass/integrations/
更新日期:2026年3月2日 | 完整版本(1000+ 行)| Time Compass DDD Architecture Reference
Dev Mode Guide (Current Behavior)¶
目的¶
這份文件描述 目前程式碼中的實際 Dev Mode 行為,作為唯一維護版本。
重點是避免再以舊設計稿(integration 層切換)解讀系統。
TL;DR¶
- 目前開關是
MCP_DEV_MODE,不是TIME_COMPASS_DEV_MODE。 - 切換點主要在
src/time_compass/mcp/與src/time_compass/domain/,不是 integration async_core。 MCP_DEV_MODE=1時,大量流程會改走 fixture/mock,許多測試若仍期待真實 API 分支會失敗。
單一開關¶
MCP_DEV_MODE=1 # 開啟 Dev Mode
MCP_DEV_MODE=0 # 關閉 Dev Mode(預設)
核心判斷函式:
src/time_compass/mcp/dev_mode.py:is_dev_mode()
目前 fixture 來源路徑為硬編碼:
assets/fixtures/snapshots
切換層級(實際)¶
1) MCP Tools 層¶
calendar_tools.py / task_tools.py / other_tools.py 在入口直接判斷 is_dev_mode(),為真時 early return mock/fixture 結果。
影響:
- 不會走真實 Google/Moodle API 呼叫。
- 參數可能被部分忽略(尤其時間範圍與 auth 相關分支)。
2) MCP Runtime 層¶
src/time_compass/mcp/runtime.py 在 fetch_context() 與 apply 流程也有 is_dev_mode():
fetch_context()會改用get_mock_resource_context()。- 套用行程 (
_apply_variant_to_calendar) 會回傳 mock success,不寫入真實日曆。
3) Domain 層(Gradio 間接生效)¶
SummaryModule 與 SchedulingModule 內部會檢查 is_dev_mode(),為真時改用 get_mock_resource_context()。
這代表:
gradio_app.py本身沒有直接讀 env。- 但 Gradio 的 scheduling pipeline 會經過 domain module,因此會間接受 Dev Mode 影響。
4) Script 層¶
scripts/run_planner_studio.py 預設會設 MCP_DEV_MODE=1(除非傳 --real)。
行為矩陣¶
| 元件 | MCP_DEV_MODE=0 |
MCP_DEV_MODE=1 |
|---|---|---|
| MCP tools | 真實 API / 正常 auth 路徑 | 直接回 fixture/mock |
| MCP runtime fetch_context | 走 integration 抓資料 | 走 get_mock_resource_context() |
| Planner apply | 真實寫入 Google Calendar | Mock 套用結果,不寫入 |
| Gradio Summary/Scheduling | 真實抓取資料 | 走 mock resource context |
| run_planner_studio.py | 需 --real 才進真實模式 |
預設即 dev mode |
測試策略¶
建議明確區分兩組測試:
dev mode suite- 測試前設定
MCP_DEV_MODE=1 -
斷言 fixture 路徑與 mock 行為
-
real-path suite - 測試前確保
MCP_DEV_MODE未設定或為0 - 斷言 auth error、API failure、真實路由行為
若把整包測試在 MCP_DEV_MODE=1 下執行,許多「應該走真實分支」的測試會被短路,出現大量非預期失敗。
常見誤解¶
誤解 1:Dev Mode 在 integration 層切換¶
現況不是。
目前主要是 MCP/domain 層直接分支,integration async_core 並沒有統一的 dev_mode.py 切換架構。
誤解 2:DEV_MODE_DATA_PATH 可控制 fixture 路徑¶
現況不是。
目前 mcp/dev_mode.py 使用硬編碼 assets/fixtures/snapshots。
相關檔案¶
src/time_compass/mcp/dev_mode.pysrc/time_compass/mcp/tools/calendar_tools.pysrc/time_compass/mcp/tools/task_tools.pysrc/time_compass/mcp/tools/other_tools.pysrc/time_compass/mcp/runtime.pysrc/time_compass/domain/summary/module.pysrc/time_compass/domain/schedule/module.pyscripts/run_planner_studio.py
最後更新:2026-03-02
Time Compass 的 Rust 風格錯誤處理設計:Railway-Oriented Programming 深度分析¶
目錄¶
- 簡介:為什麼用 Result 而非 Exception
- Railway-Oriented Programming 的核心概念
- Result Monad 的實際應用
- Google API 認證錯誤 vs 業務邏輯錯誤的分類
- Batch API 層級的錯誤處理策略
- 錯誤類型定義:完整的 Error Model
- 日誌與可觀測性設計
- 復見策略與 Retry Logic
- 實戰案例:async_core 的錯誤流轉
簡介:為什麼用 Result 而非 Exception¶
傳統異常處理的問題¶
Python 開發者習慣使用 try/except 捕捉異常。但在分散式 API 呼叫(如 Google Calendar、Tasks、Moodle)的高並發場景中,這種方式暴露了三個核心問題:
1. 隱藏的控制流¶
異常隱藏了實際的控制分支,導致程式碼難以追蹤。在 src/time_compass/utils/result.py 中,我們實作了 Result Monad 來解決此問題:
# ❌ 異常隱藏了實際的控制分支
def fetch_events():
try:
response = api.get_events() # 可能拋出異常
process_events(response) # 某個步驟也可能拋出異常
return events # 成功分支不清晰
except ApiError as e:
# 不知道是 fetch 還是 process 失敗
handle_error(e)
2. 型別安全性喪失¶
# ❌ 函數型別簽名無法表達失敗情況
def get_calendar_list(calendar_id: str) -> List[Event]:
# 呼叫者無法從型別推斷可能的失敗情況
# 必須查看實作才知道會拋出異常
pass
3. Batch 操作的複雜性¶
# ❌ 在 Batch 中,某些請求成功、某些失敗
# 異常模型不適合這種「部分成功」情景
results = batch_execute([req1, req2, req3]) # req2 失敗
# 整個 batch 拋出異常?還是返回部分結果?
Result Monad 的優勢¶
Time Compass 採用 Railway-Oriented Programming (ROP) 的 Result Monad,將失敗狀態提升為一級概念:
from time_compass.utils.result import Result, Ok, Err
# ✅ 型別簽名明確表達失敗情況
def async_get_all_tasks(
client: ClientSession,
request: GoogleAllTasksRequest,
) -> Result[AllTaskResult]:
"""成功返回 Ok[AllTaskResult],失敗返回 Err[GoogleError]"""
...
# ✅ 呼叫者從型別簽名即知曉可能失敗
result = await async_get_all_tasks(...)
if is_ok(result):
tasks = result.value # 安全地解包
process_tasks(tasks)
elif is_err(result):
error = result.error # 清晰的失敗處理
log_error(error)
特別適用於 Time Compass 的場景¶
- 多源並行抓取:Google Calendar + Google Tasks + Moodle 並行執行,每個源獨立失敗
- Batch 部分成功:批次 API 中,單一請求失敗不應中斷其他請求
- 漸進式遠端取消:Moodle 爬蟲超時與 Google API 失敗應被區分對待
- 可觀測性:每個中間步驟的成敗狀態可被追蹤、日誌記錄
Railway-Oriented Programming 的核心概念¶
什麼是 Railway?¶
ROP 將程式執行比作兩條平行的鐵軌:
成功軌道 (Success Track) ──────────→ 最終成功結果
↖ 錯誤產生
分支軌道 (Failure Track) ──────────→ 最終失敗結果
↑ 依然流動,不中斷
核心原則:一旦進入失敗軌道,後續操作自動跳過,直到終點。
Time Compass 的 Result 容器¶
# src/time_compass/utils/result.py
@dataclass(frozen=True)
class Ok(Generic[T]):
"""成功情況:包含希望的值"""
value: T
@dataclass(frozen=True)
class Err:
"""失敗情況:包含異常物件"""
error: Exception
# 型別別名
Result = Union[Ok[T], Err]
為什麼用 @dataclass(frozen=True)?
- 不可變性:Result 建立後無法改變,適合並發場景
- 型別安全:型別檢查器可完整驗證 Ok/Err 訪問
- 易於序列化:支持日誌、追蹤、重試邏輯中的序列化
Type Guard 與型別縮窄¶
from time_compass.utils.result import is_ok, is_err
def process_result(result: Result[int]) -> None:
if is_ok(result):
# 型別檢查器現在知道 result 是 Ok[int]
value: int = result.value # ✅ 安全
elif is_err(result):
# 型別檢查器現在知道 result 是 Err
error: Exception = result.error # ✅ 安全
為什麼需要 is_ok() 和 is_err()?
- TypeGuard 支持:Python 3.10+ 的
TypeGuard允許型別檢查器在 if 分支後縮窄型別 - 避免類型轉換:無需
isinstance()檢查或強制轉換 - 可讀性:比
isinstance(result, Ok)更清晰意圖
Result Monad 的實際應用¶
1. map() - 轉換成功值¶
from time_compass.utils.result import map_result, Ok, Err
def double(x: int) -> int:
return x * 2
# ✅ 成功情況:轉換值
result: Result[int] = Ok(5)
doubled = map_result(result, double) # Ok(10)
# ✅ 失敗情況:保持原樣
result: Result[int] = Err(ValueError("invalid"))
doubled = map_result(result, double) # Err(ValueError(...)),double 不執行
實際應用場景:將 Google API 原始 JSON 轉換為領域模型
# src/time_compass/integrations/google_calendar/async_core.py
async def async_get_calendar_events(
calendar_id: str,
client: ClientSession,
) -> Result[CalendarEventsResult]:
try:
# 1. 取得原始 JSON
raw_items = await fetch_raw_events_from_api(...)
# 2. 轉換為讀取模型(使用 map_result)
def convert_to_read_model(items):
return [GoogleEventRead.from_raw_model(r) for r in items]
converted = map_result(
Ok(raw_items),
convert_to_read_model
)
# 3. 處理業務邏輯(過濾、分組)
def process_events(events):
return _process_read_models(events, calendar_id)
result_model = map_result(converted, process_events)
return result_model
except Exception as e:
return Err(GoogleAPIError(f"Calendar fetch failed: {e}"))
2. flat_map() 或 bind() - 鏈式操作¶
from time_compass.utils.result import flat_map_result, Ok, Err
def parse_int(s: str) -> Result[int]:
try:
return Ok(int(s))
except ValueError:
return Err(ValueError(f"Cannot parse '{s}'"))
def validate_positive(x: int) -> Result[int]:
if x > 0:
return Ok(x)
else:
return Err(ValueError(f"Must be positive, got {x}"))
# ✅ 鏈式操作
result: Result[str] = Ok("42")
parsed = flat_map_result(result, parse_int) # Ok(42)
validated = flat_map_result(parsed, validate_positive) # Ok(42)
# ✅ 若中途失敗,後續自動跳過
result: Result[str] = Ok("-5")
parsed = flat_map_result(result, parse_int) # Ok(-5)
validated = flat_map_result(parsed, validate_positive) # Err(ValueError(...))
3. is_ok() / is_err() - 模式匹配¶
from time_compass.utils.result import is_ok, is_err
# src/time_compass/integrations/get_all_information_from_api.py
# 處理 Google Tasks 結果
google_res = _coerce_result(results[google_task_idx], "Google tasks")
if google_res is not None:
if is_ok(google_res):
task_result = google_res.value
task_grouped = task_result.items_by_group_id or task_result.items
log.info("Google tasks fetched: %d lists", len(task_grouped))
elif is_err(google_res):
log.error("Google tasks fetching returned error: %s", google_res.error)
4. unwrap_or() - 提供預設值¶
from time_compass.utils.result import unwrap_or
# 取得 Moodle 結果,若失敗採用預設空列表
moodle_events = unwrap_or(moodle_result, [])
5. unwrap() - 強制解包(危險操作)¶
from time_compass.utils.result import unwrap
# 僅在確定不會失敗時使用
tasks = unwrap(task_result) # 若是 Err,拋出異常,中斷流程
什麼時候使用 unwrap()?
- ✅ 單元測試中確保預期成功時
- ✅ 應用初始化時,異常應立即中止程式
- ❌ 正常業務邏輯中(應改用
is_ok()+ 日誌)
Google API 認證錯誤 vs 業務邏輯錯誤的分類¶
完整的錯誤層級體系¶
Time Compass 設計了多層級的錯誤分類,每層承載特定的語義。
第一層:根異常類 GoogleError¶
# src/time_compass/integrations/common/exceptions.py
class GoogleError(Exception):
"""所有 Google API 相關異常的基類"""
pass
目的:統一 catch Google API 相關的所有異常,與其他系統錯誤區分。
第二層:功能性分類¶
class GoogleAuthError(GoogleError):
"""認證/授權異常
HTTP 狀態碼:401 (Unauthorized), 403 (Forbidden)
常見原因:
- invalid_grant: 刷新令牌已過期或無效
- token_expiry: Access token 已過期
- insufficient_scope: 令牌缺少必要的 scopes
優先級:高 - 應立即通知使用者重新授權
"""
pass
class GoogleParseError(GoogleError):
"""解析異常
JSON 解析失敗或 Pydantic 驗證失敗
常見原因:
- API 返回非預期的 JSON 結構
- 欄位型別不符(例如期望字串得到數字)
優先級:高 - 表示 API 契約破口或版本不相容
"""
pass
class GoogleNetworkError(GoogleError):
"""網路相關異常
常見原因:
- asyncio.TimeoutError: 單一請求超過時限
- aiohttp.ClientError: 連接失敗、DNS 解析失敗
- connection reset by peer
優先級:中 - 通常可重試
"""
pass
class GoogleNotFoundError(GoogleError):
"""資源不存在
HTTP 狀態碼:404
常見原因:
- calendar_id 不存在
- tasklist_id 已被刪除
優先級:低 - 通常表示使用者資源狀態變化
"""
pass
class GoogleRateLimitError(GoogleError):
"""速率限制
HTTP 狀態碼:429
常見原因:
- 在短時間內發送過多請求(超過配額)
- 批次請求過大
優先級:中 - 應實現指數退避重試
"""
pass
class GoogleBatchError(GoogleError):
"""批次操作級別異常
常見原因:
- 批次請求大小超過上限(>100 個子請求)
- 多部分邊界格式錯誤
優先級:高 - 表示客戶端程式碼錯誤
"""
pass
HTTP 狀態碼路由¶
Time Compass 實現了精確的 HTTP 狀態碼 → GoogleError 對應:
# src/time_compass/integrations/common/google_api_dispatcher.py
def _status_code_to_google_error(status_code: int, error_msg: str) -> GoogleError:
"""
根據 HTTP 狀態碼決定適當的 GoogleError 子類。
This implements HTTP status code routing:
- 401/403: GoogleAuthError (authentication/authorization)
- 404: GoogleNotFoundError (resource not found)
- 429: GoogleRateLimitError (rate limited)
- Anything else: GoogleAPIError (generic API error)
"""
if status_code in (401, 403):
return GoogleAuthError(f"HTTP {status_code}: {error_msg}")
if status_code == 404:
return GoogleNotFoundError(f"HTTP {status_code}: {error_msg}")
if status_code == 429:
return GoogleRateLimitError(f"HTTP {status_code}: {error_msg}")
# Default to generic APIError for 5xx or other codes
return GoogleAPIError(f"HTTP {status_code}: {error_msg}")
業務邏輯錯誤 vs 技術錯誤¶
| 類別 | 例子 | 處理方式 | 重試? |
|---|---|---|---|
| 認證錯誤 | 401 Unauthorized, token 過期 | 向使用者要求重新授權 | ❌ 否(需使用者操作) |
| 授權錯誤 | 403 Forbidden, 缺少 scope | 向使用者解釋權限不足 | ❌ 否(需使用者同意) |
| 資源不存在 | 404 Not Found | 記錄、忽略或提示使用者 | ❌ 否(資源確實不存在) |
| 速率限制 | 429 Too Many Requests | 指數退避重試 | ✅ 是(暫時性) |
| 網路超時 | asyncio.TimeoutError | 重試或使用備用方案 | ✅ 是(暫時性) |
| 解析錯誤 | JSON parsing fail | 記錄詳細資訊,上報 | ❌ 否(程式碼或 API 契約問題) |
Batch API 層級的錯誤處理策略¶
Batch 請求的特殊性¶
Google Batch API(multipart/mixed)允許在單一 HTTP 請求中包含多個子請求:
POST /batch/calendar/v3 HTTP/1.1
Content-Type: multipart/mixed; boundary=batch_boundary
--batch_boundary
Content-Type: application/http
GET /calendar/v3/calendars/primary/events?timeMin=...
Authorization: Bearer <token>
--batch_boundary
Content-Type: application/http
POST /calendar/v3/calendars/primary/events
Authorization: Bearer <token>
{...event body...}
--batch_boundary--
「部分成功」模式¶
Batch API 的核心特性:一個子請求的失敗不會中斷其他子請求。
# src/time_compass/integrations/common/google_api_dispatcher.py
async def batch_execute_async(
client: ClientSession,
credentials: Any,
requests: List[BaseGoogleRequest],
endpoint: str,
boundary: str = "batch_boundary",
raw: bool = False
) -> List[Result[BaseGoogleRaw]] | List[Dict[str, Any]]:
"""
Executes a list of Google request models as a single batch.
Type-Safe Behavior (via @overload):
- If raw=True: Returns List[Dict[str, Any]] (raw HTTP response dicts)
- If raw=False: Returns List[Result[BaseGoogleRaw]] (Ok or Err wrapped)
關鍵:返回的是 List,包含多個 Result,而非單一 Result
這樣呼叫者可以逐個檢查每個子請求的成敗
"""
多層級的異常捕捉¶
async def batch_execute_async(...) -> List[Result[BaseGoogleRaw]]:
"""
多層級異常處理的體現:
1. 網路層:整個 batch 請求失敗
2. HTTP 層:子請求返回不同的狀態碼
3. 解析層:JSON 結構不符預期
"""
# Layer 1: 網路請求失敗(整個 batch 無法發送)
try:
response_text, resp_boundary = await send_batch_request_payload(...)
except Exception as e:
network_error = _convert_to_google_error(e)
# 若網路失敗,所有子請求都返回同一個錯誤
return [Err(network_error) for _ in requests]
# Layer 2: 解析 Batch 響應信封
raw_results = parse_generic_batch_response(...)
# Layer 3: 逐個檢查子請求結果
parsed_results: List[Result[BaseGoogleRaw]] = []
for req, res in zip(requests, raw_results):
if res["ok"]:
# 子請求成功,嘗試解析
try:
model = req.parse_response(res["data"])
parsed_results.append(Ok(model))
except Exception as e:
# 解析錯誤 → Err[GoogleParseError]
parse_error = GoogleParseError(...)
parsed_results.append(Err(parse_error))
else:
# 子請求返回 HTTP 錯誤
status_code = res.get("status", 0)
error_msg = res.get("error", "Unknown API error")
api_error = _status_code_to_google_error(status_code, error_msg)
parsed_results.append(Err(api_error))
return parsed_results
錯誤類型定義:完整的 Error Model¶
異常分類矩陣¶
GoogleError (基類)
├─ GoogleAuthError (401/403)
├─ GoogleParseError (客戶端層)
├─ GoogleNetworkError (網路層)
├─ GoogleNotFoundError (404)
├─ GoogleRateLimitError (429)
├─ GoogleAPIError (其他)
└─ GoogleBatchError (Batch 層級)
異常訊息的設計規範¶
# ✅ 好的異常訊息(包含上下文)
GoogleAuthError("HTTP 401: invalid_grant - Refresh token expired at 2026-03-02T10:30:00Z")
GoogleNetworkError("Request timeout after 30s: GET /calendar/v3/calendars/primary/events")
GoogleParseError("Parse error in ListEventsRequest: 'start' field expected datetime, got string '2026-03-02'")
# ❌ 不好的異常訊息(缺乏上下文)
GoogleAuthError("401")
GoogleNetworkError("timeout")
GoogleParseError("Parse error")
日誌與可觀測性設計¶
Logger 的模組路徑追蹤¶
# src/time_compass/utils/logger.py
class _ModulePathFormatter(logging.Formatter):
"""Formatter that ensures `record.module_path` is present."""
def format(self, record: logging.LogRecord) -> str:
module_name = getattr(record, "name", "")
if "time_compass" in module_name:
module_path = module_name.split("time_compass.", 1)[-1]
else:
module_path = module_name
record.module_path = module_path
return super().format(record)
# 日誌格式
fmt = "[%(module_path)s] %(levelname)s %(message)s"
三層級的日誌記錄¶
# 1. 入口日誌(DEBUG)
logger.debug("async_get_all_information_from_api start: ...")
# 2. 中間步驟日誌(INFO)
logger.info("Awaiting %d tasks", len(tasks_to_wait))
# 3. 錯誤日誌(ERROR/WARNING)
logger.error("Google tasks fetching returned error: %s", result.error)
復見策略與 Retry Logic¶
建議的重試策略¶
1. 指數退避(Exponential Backoff)¶
async def retry_with_exponential_backoff(
fn: Callable[[], Awaitable[Result[T]]],
max_retries: int = 3,
initial_delay: float = 1.0,
max_delay: float = 32.0,
) -> Result[T]:
"""重試邏輯,適用於暫時性失敗"""
delay = initial_delay
for attempt in range(max_retries):
result = await fn()
if is_ok(result):
return result
error = result.error
if isinstance(error, (GoogleRateLimitError, GoogleNetworkError)):
if attempt < max_retries - 1:
logger.warning(
"Retryable error (attempt %d/%d): %s. Waiting %.1fs",
attempt + 1, max_retries, error, delay
)
await asyncio.sleep(delay)
delay = min(delay * 2, max_delay)
continue
return result
2. 斷路器(Circuit Breaker)¶
class CircuitBreaker:
"""防止 Cascading Failure"""
def is_available(self) -> bool:
"""檢查是否應發起請求"""
if self.state == CircuitState.CLOSED:
return True
# ... 詳細邏輯
實戰案例:async_core 的錯誤流轉¶
完整流程¶
async_get_all_information_from_api()
├─ 1. 並行建立三個任務
├─ 2. asyncio.gather(*tasks, return_exceptions=True)
├─ 3. 後處理:_coerce_result()
├─ 4. 逐個檢查結果
└─ 5. 最終合併為 ResourceContext
錯誤在層級間的流轉¶
Layer 1: Google API 返回 HTTP 401
↓
Layer 2: parse_generic_batch_response() 解析為
{"ok": False, "status": 401, "error": "invalid_grant"}
↓
Layer 3: batch_execute_async() 轉換為
Err(GoogleAuthError("HTTP 401: invalid_grant"))
↓
Layer 4: async_core 檢查並傳遞
if is_err(result) and isinstance(result.error, GoogleAuthError):
return Err(result.error)
↓
Layer 5: MCP 工具回傳失敗訊息給前端
最佳實踐清單¶
- ✅ 使用 Result[T] 表達可能失敗的操作
- ✅ 使用 is_ok() / is_err() 進行型別縮窄
- ✅ 使用 map_result / flat_map_result 鏈式操作
- ✅ 細分 GoogleError 子類,實現狀態碼路由
- ✅ 在 Batch 結果中逐個檢查,支持部分成功
- ✅ 記錄詳細的日誌上下文,便於分佈式追蹤
- ✅ 實現指數退避和斷路器,處理暫時性失敗
- ❌ 避免盲目重試 AuthError 或 ParseError
- ❌ 避免在熱路徑中使用 unwrap(),會拋出異常
文檔版本:Phase 4 (Result Monad 整合階段)
適用範圍:src/time_compass/integrations/
參考檔案:
- src/time_compass/utils/result.py
- src/time_compass/integrations/common/exceptions.py
- src/time_compass/integrations/common/google_api_dispatcher.py
- src/time_compass/integrations/get_all_information_from_api.py
Interface C4 Architecture¶
C1: System Context¶
┌─────────────────────────────────────────────────────────────────┐
│ User │
└────────────────────────────┬────────────────────────────────────┘
│
│ HTTP Browser
│
┌────────────────────────────▼────────────────────────────────────┐
│ │
│ Time Compass Gradio App │
│ (Frontend Application - Scheduling & Planning) │
│ │
└────────────┬──────────────────────────┬────────────┬────────────┘
│ │ │
│ Read/Write Events │ Chat Flow │ Streaming
│ │ │
┌────────────▼──────────────────────────▼────────────▼────────────┐
│ Domain Layer │
│ (Router, Orchestrator, LLM Manager, Streaming Manager) │
└────────────┬──────────────────────────┬────────────┬────────────┘
│ │ │
│ Integration APIs │ Models │ External
│ │ │
┌────────────▼──────────────────────────▼────────────▼────────────┐
│ External Systems / Integrations │
│ (Google Calendar, Google Tasks, Moodle, LiteLLM Proxy) │
└──────────────────────────────────────────────────────────────────┘
C2: 容器(gradio_app.py)¶
Gradio 應用作為 UI 容器,負責: - 組合 6 個 UI Tab(聊天、排程、模型管理、Moodle、快速測試、除錯) - 啟動 Gradio Blocks web 伺服器 - 路由 用戶事件到 domain 層(orchestrator、streaming manager) - 管理 環境變數與主題載入
┌────────────────────────────────────────────────────────────┐
│ Gradio Blocks (web 伺服器) │
├────────────────────────────────────────────────────────────┤
│ Tab 層(平行) │
│ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │ 聊天 │ │ 排程 │ │ 模型 │ │Moodle │ │ 快速 │ │
│ │ Tab │ │ Tab │ │ 管理 │ │ Tab │ │ 測試 │ │
│ │ │ │ │ │ Tab │ │ │ │ Tab │ │
│ └───┬────┘ └───┬────┘ └───┬────┘ └───┬───┘ └───┬────┘ │
│ │ │ │ │ │ │
│ ┌────────┐ │ │ │ │ │
│ │ 除錯 │ │ │ │ │ │
│ │ Tab │ │ │ │ │ │
│ └───┬────┘ │ │ │ │ │
│ │ │ │ │ │ │
│ └──────────┴──────────┴──────────┴─────────┘ │
│ ↓ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ 事件處理器與狀態管理 │ │
│ │ (gr.State、async callbacks、使用者事件) │ │
│ └──────────┬───────────────────────────────────────┘ │
│ ↓ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ 串流模組 (streaming.py) │ │
│ │ - format_stream_output() │ │
│ │ - stream_main_output() [async generator] │ │
│ │ - chat_fn() [Gradio 回調] │ │
│ └──────────┬───────────────────────────────────────┘ │
│ ↓ │
└─────────────┼───────────────────────────────────────────┘
│
└─→ 呼叫 Domain / Orchestrator
C3: 元件(UI Tabs 與串流)¶
3.1 聊天 Tab (ui/chat_tab.py)¶
目的:聊天機器人 UI,含訊息歷史路由至 orchestrator
| 面向 | 詳細說明 |
|---|---|
| 輸入 | 使用者文字、聊天歷史、工作階段狀態 |
| 輸出 | 更新後的聊天視圖、呼叫 router/orchestrator |
| 核心函數 | handle_send()、render_history()(預期) |
| 狀態 | 預留位置 - 等待 router/orchestrator 介面 |
3.2 排程 Tab (ui/scheduling_tab.py)¶
目的:兩階段事件鏈用於任務規劃與日曆回寫
| 面向 | 詳細說明 |
|---|---|
| 階段 1 | 解析使用者輸入 → 建立任務與草稿狀態 |
| 階段 2 | 渲染狀態 → Dataframe、Markdown、JSON 成品 |
| 核心函數 | stage1_chat()(async)、stage2_render() |
| 狀態 | gr.State(plan_state)儲存任務、draft_rows、回寫 |
| 回寫 | 目前為 dry-run;等待實際日曆 API 呼叫 |
3.3 模型管理 Tab (ui/model_management_tab.py)¶
目的:列出並重新載入 DSPy comfort 模型
| 面向 | 詳細說明 |
|---|---|
| 輸入 | 檔案系統:models/dspy/comfort_emotion_v*.json |
| 輸出 | 模型版本列表、重新載入狀態訊息 |
| 核心函數 | reload_latest_model() |
| 整合 | 可選橋接至 gradio_app._make_workflow() |
3.4 Moodle Tab (ui/moodle_tab.py)¶
目的:爬蟲取得 Moodle 課程事件並顯示
| 面向 | 詳細說明 |
|---|---|
| 輸入 | 使用者帳號、密碼、無頭模式旗標 |
| 輸出 | 事件數量、時間戳記、最近事件列表(Markdown) |
| 上游 | 呼叫 time_compass.integrations.moodle.scrape_moodle_events() |
| 安全提示 | 直接密碼輸入;建議生產環境改用 OAuth/token 方案 |
3.5 快速測試 Tab (ui/quick_test_tab.py)¶
目的:開發者專用介面,用於快速測試串流與 orchestrator
| 面向 | 詳細說明 |
|---|---|
| 輸入 | 使用者提示詞(Textbox) |
| 輸出 | 合併後的串流輸出(Markdown) |
| 核心函數 | _quick_run() 透過 asyncio.run() 執行 stream_main_output() |
| 用途 | 本地開發/除錯專用;生產環境應隱藏 |
3.6 除錯 Tab (ui/debug_tab.py)¶
目的:開發者工具,檢視系統狀態與即時日誌
| 面向 | 詳細說明 |
|---|---|
| 輸入 | 無(被動檢視) |
| 輸出 | 系統狀態、最近日誌、效能指標(Markdown/表格) |
| 核心函數 | debug_tab()、狀態更新回調 |
| 用途 | 本地開發/疑難排除;隱藏在生產環境 |
| 功能 | 顯示 MCP 連線、Orchestrator 狀態、記憶體使用、最近任務 |
⚠️ 待修正:本章節說明需要驗證實際實作,標記以便後續完善
3.7 串流模組 (streaming.py)¶
目的:統一非同步串流協調,管理分塊輸出
| 面向 | 詳細說明 |
|---|---|
| 核心函數 | format_stream_output()、stream_main_output()(async gen)、chat_fn() |
| 輸入 | 使用者訊息、domain orchestrator(可選) |
| 輸出 | 格式化分塊的非同步產生器 → Gradio UI |
| 回退機制 | Orchestrator 不可用時,yield 原始輸入 |
| 配置 | STREAM_DELAY 環境變數控制分塊間隔 |
工作流:
使用者訊息
↓
stream_main_output() [async]
↓
build_streaming_orchestrator() [若可用]
↓
迭代非同步輸出流
├─ 偵測欄位/模組變更
├─ 累積與格式化分塊
├─ 以 STREAM_DELAY 間隔 yield 到前端
└─ 處理例外 → error yield
C4: 程式碼層(主要介面)¶
4.1 Tab 介面(預期)¶
def {聊天,排程,moodle,模型管理,快速測試}_tab() -> Dict[str, Any]:
"""
回傳包含以下內容的字典:
- gr.Textbox/Chatbot 元件
- gr.Button(提交/動作)
- gr.Markdown/Dataframe(輸出)
- 事件處理器(click、submit)
"""
pass
4.2 串流介面¶
async def stream_main_output(user_input: str) -> AsyncGenerator[str, None]:
"""
核心串流產生器:
- 若可用,建立 orchestrator
- 從 orchestrator.output_stream 迭代非同步分塊
- 累積、格式化、以 STREAM_DELAY 频率 yield
"""
async def chat_fn(message: str, history: List[Dict[str, Any]]):
"""Gradio 回調:銜接 stream_main_output → 前端"""
def format_stream_output(module: str, field: str, text: str) -> str:
"""將分塊格式化為 [Module:field] text"""
4.3 Gradio 應用入口¶
from time_compass.interface.gradio_app import main
main() # 啟動伺服器,含所有 tabs、串流、主題
資料流總覽¶
使用者輸入(Textbox 或 Chat)
↓
Tab 事件處理器
├─ 聊天 Tab → router / streaming.chat_fn()
├─ 排程 Tab → stage1_chat() → stage2_render()
├─ Moodle Tab → scrape_moodle_events()
├─ 模型管理 Tab → reload_latest_model()
└─ 快速測試 Tab → stream_main_output()
↓
Domain 層(Orchestrator、Router、Integration)
↓
串流格式或直接回應
↓
更新 Gradio 元件
架構原則¶
- 關注點分離
gradio_app.py:Tab 組合與伺服器生命週期ui/*.py:UI 配置與事件綁定(薄適配層)streaming.py:統一非同步分塊管理-
Domain 層:業務邏輯(router、orchestrator、整合)
-
非同步優先
- 所有串流路徑使用非同步產生器
-
Gradio 回調視需要適配非同步/同步
-
可擴展性
- 新 Tab:加入
ui/、在gradio_app中匯入、綁定事件 -
新串流行為:擴展
stream_main_output()或建立新 orchestrator -
回退與錯誤處理
- 串流:yield 錯誤訊息而非崩潰
- Tabs:優雅降級(如模型重新載入失敗 → 顯示後備方案)
- Orchestrator 不可用 → 使用原始輸入作為後備
工具與整合
MCP Tools 完整實作參考¶
簡介¶
MCP Tools(Model Context Protocol Tools)是 Time Compass 系統中核心的能力模組。透過 MCP 架構,AI 助手可以存取使用者的 Google 日曆、Google Tasks、Moodle 課程系統與時間規劃工具,提供整合性的時間管理服務。
本系統將 15 個工具分為三大類別:
- 日曆工具(5個):管理 Google 日曆事件
- 任務工具(4個):管理 Google Tasks 任務列表
- 其他工具(6個):時間上下文聚合、Moodle 爬取、Planner Studio 啟動等
快速工具分類速覽¶
1. 排程與上下文(核心整合)¶
| 工具 | 功能 |
|---|---|
get_time_context |
最核心工具。一次性抓取 Calendar + Tasks + Moodle 資訊,以 TOON 格式回傳 |
launch_planner_studio |
接收 AI 生成的規劃方案(DraftVariants)並啟動本地視覺化介面 |
2. Google Calendar(日曆管理)¶
| 工具 | 功能 |
|---|---|
list_calendars |
列出使用者帳號中所有可寫入的日曆資訊 |
get_all_calendar_events |
跨日曆抓取指定範圍內的所有事件 |
get_event_from_calendar |
鎖定單一日曆抓取事件 |
create_calendar_event |
在指定日曆中建立新事件 |
get_free_busy |
查詢特定時段的忙碌狀態(用於衝突檢查) |
3. Google Tasks(任務管理)¶
| 工具 | 功能 |
|---|---|
list_tasklists |
獲取所有任務清單 (Task Lists) 的 ID 與標題 |
get_all_tasks |
掃描所有清單中的任務,支援時間範圍過濾 |
get_task_from_tasklist |
獲取特定任務清單內的詳細細項 |
create_task |
在指定的清單中新增一項任務 |
4. 教育整合(Moodle)¶
| 工具 | 功能 |
|---|---|
get_moodle_events |
爬取並解析 Moodle 上的課程公告、作業截止日與考試事件 |
5. 系統與授權¶
| 工具 | 功能 |
|---|---|
launch_google_token_auth |
啟動本機 OAuth 流程,幫助使用者完成 Google 授權 |
get_planner_studio_status |
檢查當前 Planner Studio 伺服器的運行狀態 |
shutdown_planner_studio |
安全關閉本地執行中的 Planner Studio 實例 |
6. Prompt 模板工具(系統提示)¶
這 3 個工具回傳專用的 SYSTEM INSTRUCTION 供 AI 使用
| 工具 | 功能 |
|---|---|
summary_writer_prompt |
回傳「回顧/總結」專用 SYSTEM INSTRUCTION |
emotion_support_prompt |
回傳「情緒支持」專用 SYSTEM INSTRUCTION |
time_management_master_prompt |
回傳「規劃排程主流程」專用 SYSTEM INSTRUCTION |
如何測試工具:使用官方 MCP Inspector 驗證
npx @modelcontextprotocol/inspector uv run time-compass-mcp
進入 Inspector 網頁後,可手動點擊各工具並填入參數,觀察 TOON 格式回傳值。
15 個工具列表與功能表¶
| 序號 | 工具名稱 | 類型 | 功能摘要 | 參數群 | 返回值 |
|---|---|---|---|---|---|
| 1 | get_all_calendar_events |
Calendar | 獲取所有日曆的事件(TOON 格式) | start_time, end_time | TOON 格式日曆事件 |
| 2 | get_event_from_calendar |
Calendar | 指定日曆的事件查詢 | calendar_id, start_time, end_time | TOON 格式單一日曆事件 |
| 3 | list_calendars |
Calendar | 列出使用者所有可寫日曆 | 無 | JSON(日曆 ID、標題、角色) |
| 4 | create_calendar_event |
Calendar | 建立日曆事件 | summary, start, end, calendar_id, description? | JSON(event ID、狀態) |
| 5 | get_free_busy |
Calendar | 查詢日曆空閒/繁忙狀態 | time_min, time_max, items[] | JSON(Free/Busy 查詢結果) |
| 6 | get_all_tasks |
Task | 獲取所有任務(TOON 格式) | start_time?, end_time?, time_mode | TOON 格式任務列表 |
| 7 | list_tasklists |
Task | 列出使用者所有任務列表 | 無 | JSON(任務列表 ID、標題) |
| 8 | get_task_from_tasklist |
Task | 指定任務列表的任務查詢 | tasklist_id, start_time?, end_time? | TOON 格式任務清單 |
| 9 | create_task |
Task | 建立任務 | title, list_id, notes?, due? | JSON(task ID、狀態) |
| 10 | get_moodle_events |
Other | 爬取 Moodle 課程事件 | start_date, end_date, account?, password? | 文本格式課程事件 |
| 11 | get_time_context |
Other | 聚合完整生活上下文 | start_time, end_time, moodle_account?, moodle_password? | TOON 格式完整上下文 |
| 12 | launch_google_token_auth |
Other | 啟動 Google OAuth 授權頁 | open_browser?, port?, token_file?, wait_timeout? | 授權流程狀態 JSON |
| 13 | launch_planner_studio |
Other | 啟動排程規劃網頁介面 | start_time, end_time, ai_draft, draft_variants, ... | 網頁 URL、session ID |
| 14 | get_planner_studio_status |
Other | 查詢 Planner Studio 執行狀態 | 無 | 執行狀態 JSON |
| 15 | shutdown_planner_studio |
Other | 關閉 Planner Studio 伺服器 | wait_timeout_seconds? | 關閉狀態 JSON |
效能考量與最佳實踐¶
API 呼叫決策矩陣¶
| 使用情境 | 推薦工具 | 原因 |
|---|---|---|
| 初次載入完整背景 | get_time_context |
單一呼叫 vs 3 次呼叫,延遲減少 ~40% |
| 指定日曆查詢 | get_event_from_calendar |
精準篩選,避免全量日程轉換 |
| 空閒時段檢測 | get_free_busy |
Google API 原生支援,高效率 |
| 新增排程到日曆 | create_calendar_event + launch_planner_studio |
先預覽後建立,降低誤操作 |
| Moodle 課程回顧 | get_moodle_events+get_time_context |
統整課程與個人日程 |
快取策略¶
# ResourceContext 典型快取時間表
get_time_context() # 快取 5-10 分鐘(涵蓋 Google + Moodle)
get_all_calendar_events() # 快取 3-5 分鐘
get_all_tasks() # 快取 3-5 分鐘
get_moodle_events() # 快取 15-30 分鐘(Web 爬取成本高)
Dev Mode 與測試¶
- 所有工具支援
is_dev_mode()切換,自動使用 Mock Fixtures - 時間平移邏輯:自動計算 anchor_date (2025-11-15) 與執行日期的差值
- Fixture 位置:
tests/snapshots/fixtures/google/{calendar,tasks,moodle}/
工具分類與架構模式¶
按責任分類¶
1. 資料讀取工具(只讀)
- list_calendars, list_tasklists
- get_all_calendar_events, get_event_from_calendar
- get_all_tasks, get_task_from_tasklist
- get_moodle_events, get_free_busy
2. 資料寫入工具(建立)
- create_calendar_event, create_task
3. 聚合工具(多源合併)
- get_time_context:Calendar + Tasks + Moodle
4. 執行環境工具(狀態管理)
- launch_google_token_auth, launch_planner_studio
- get_planner_studio_status, shutdown_planner_studio
按資料流向分類¶
使用者請求
↓
┌─────────────────────────────────────┐
│ MCP Tools 層 (15 個工具) │
├─────────────────────────────────────┤
│ ├─ Calendar API (5 tools) │
│ ├─ Tasks API (4 tools) │
│ ├─ Moodle Scraper (1 tool) │
│ ├─ Aggregation (1 tool: get_time_context)│
│ └─ Runtime (4 tools) │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ Integration 層 │
│ ├─ google_calendar.async_core │
│ ├─ google_tasks.async_core │
│ ├─ moodle.async_core │
│ └─ get_all_information_from_api │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ Google/Moodle API / Web Scraper │
└─────────────────────────────────────┘
錯誤處理與復見策略¶
統一錯誤檢查¶
# 所有工具的錯誤檢查模式
try:
result = await some_api_call()
if is_err(result):
return f"AUTH_REQUIRED: {result.error}"
if not is_ok(result):
return "AUTH_REQUIRED: API failure"
# 處理成功情況
except Exception as err:
if is_google_auth_error(err):
return get_auth_required_text()
raise # 重新拋出非認證例外
常見例外情況¶
| 例外類型 | 工具 | 復見策略 |
|---|---|---|
| 認證過期 | 所有 Google API 工具 | 重新呼叫 launch_google_token_auth() |
| Moodle 連線失敗 | get_moodle_events, get_time_context |
環境變數檢查、重試邏輯 |
| Planner Studio 埠號佔用 | launch_planner_studio |
自動遞增埠號或清理舊程式 |
| Token 檔案遺失 | launch_google_token_auth |
觸發首次授權流程 |
詳細工具實作說明¶
完整的每個工具的詳細文檔(含 Docstring、函式簽名、核心實作邏輯、使用案例),請參考本文的完整版本或直接查看原始程式碼位置:src/time_compass/mcp/tools/
整合與調用流程¶
典型流程:AI 排程規劃¶
1. 使用者輸入抽象目標
↓
2. get_time_context()
└─ 聚合背景(Calendar + Tasks + Moodle)
↓
3. AI 規劃邏輯
└─ 根據背景產生 3-5 個排程變體
↓
4. launch_planner_studio()
├─ 傳入 ai_draft(引導文案)
├─ 傳入 draft_variants(3-5 個方案)
└─ 返回網頁 URL + session ID
↓
5. 使用者在 Planner Studio 中
├─ 查看背景日程與任務
├─ 比選排程變體
└─ 點選「應用」
↓
6. Planner Studio 後端
├─ 呼叫 create_calendar_event() 建立選中變體的事件
└─ 呼叫 create_task()(如需)
↓
7. 排程應用完成
典型流程:查詢特定日曆¶
1. list_calendars()
└─ 返回日曆列表供選擇
↓
2. get_event_from_calendar(calendar_id, start, end)
└─ 查詢特定日曆事件
↓
3. 應用於 UI 篩選或報告
實作變更追蹤與版本¶
V1.0(初始版本)¶
- 基本 Calendar & Tasks 工具集
- JSON 格式輸出
- 單一日曆/任務列表查詢
V1.5(TOON 格式與批次優化)¶
- 引入 TOON 編碼(減少 Token 消耗 ~40%)
- 引入 Batch API:
get_all_calendar_events,get_all_tasks get_time_context統一聚合介面
V2.0(Planner Studio & OAuth)¶
- 新增執行環境工具:
launch_planner_studio - OAuth Token 管理:
launch_google_token_auth - Dev Mode 時間平移邏輯(確保測試環保一致性)
V2.1(當前)¶
- Moodle 整合:
get_moodle_events - 完整上下文聚合:增強
get_time_context支援 Moodle - 錯誤處理增強:統一
is_google_auth_error()檢查
總結¶
Time Compass 的 15 個 MCP 工具形成了一個完整的時間管理資料橋樑,從原始資料源(Google Calendar/Tasks、Moodle)到高層規劃介面(Planner Studio)。每個工具的設計原則是:
✅ 高效率:Batch API、TOON 編碼、快取策略
✅ 易整合:統一錯誤檢查、相容 Mock & 生產環境
✅ 可擴展:Dev Mode 測試、時間平移邏輯、中間層聚合
✅ 使用者友善:預視→選擇→應用的完整流程控制
更新日期:2026年3月2日 | Time Compass MCP Tools Reference
LiteLLM Proxy 與 Gradio 配置指南¶
這份文件對應 scripts/serve_litellm_proxy.py,重點是:
1. 它提供了什麼功能
2. 你要怎麼配置
3. 它如何和 src/time_compass/domain/llm_config.py 協作
0. 使用建議(強烈推薦)¶
本專案強烈推薦「Gradio + LiteLLM Proxy」一起使用,尤其是舊版互動流程(情緒承接、路由、排程對話):
- Gradio 端通常會產生較密集的對話請求,Proxy 可提供較穩定的本地路由與 fallback。
- 可以同時使用多把 Gemini key(
GEMINI_API_KEY_n)分流,減少單一 key 的壓力。 llm_config.py已內建「先走 proxy、失敗再 fallback」邏輯,和 Gradio 體驗最契合。
1. 這個 Proxy 在專案中的功能¶
LiteLLM Proxy 在本專案扮演「模型路由層」:
- 提供統一 OpenAI 相容入口(本地
http://127.0.0.1:4001/v1)。 - 將多把 Gemini keys 組成可輪替/降級的 tier 路由(tier-a、tier-b)。
- 在
llm_config.py中,讓 DSPy 優先走 proxy;proxy 不可用時再 fallback 直連 Gemini。
換句話說,這不是只有 env 設定,而是「本地模型編排與容錯」基礎設施。
2. 配置流程(Step-by-step)¶
2.1 準備 Gemini API keys¶
至少準備一把:
- GEMINI_API_KEY_1
建議多把:
- GEMINI_API_KEY_2
- GEMINI_API_KEY_3 ...
原因:serve_litellm_proxy.py 會掃描 GEMINI_API_KEY_n,動態展開 litellm_config.yaml 的 model_list,做分流與備援。
2.2 設定 .env¶
最小配置:
GEMINI_API_KEY_1=your_key_1
常用配置:
GEMINI_API_KEY_1=your_key_1
GEMINI_API_KEY_2=your_key_2
LITELLM_HOST=127.0.0.1
LITELLM_PORT=4001
LITELLM_DEBUG=1
2.3 啟動 Proxy¶
uv run scripts/serve_litellm_proxy.py
啟動後會做兩件事:
1. 依 .env 內容重寫 litellm_config.yaml
2. 啟動 LiteLLM,並等待 /health/readiness 通過
2.4 驗證是否成功¶
- 看到 readiness 成功訊息(或手動打
http://127.0.0.1:4001/health/readiness)。 - 在需要 LLM 的流程中觀察是否走
tier-a/tier-b。 - 若 proxy 關閉,
llm_config.py會 fallback 到直連 Gemini(可作為對照)。
3. 與 llm_config.py 的整合¶
檔案:
- src/time_compass/domain/llm_config.py
行為摘要:
1. 啟動時檢查 http://127.0.0.1:4001/health/readiness。
2. 若可用:
- lm_a = openai/tier-a(api_base 指向本地 proxy)
- lm_b = openai/tier-b
3. 若不可用:
- fallback gemini/gemini-2.5-flash
- fallback gemini/gemma-3-12b-it
重點:
Proxy 是「優先路徑」,不是唯一路徑;系統具備自動退回策略。
4. 環境變數清單(補充)¶
必要:
- GEMINI_API_KEY_1(至少一把)
常用可選:
- LITELLM_HOST(預設 127.0.0.1)
- LITELLM_PORT(預設 4001)
- LITELLM_CONFIG(預設 <repo>/litellm_config.yaml)
- LITELLM_ENV_FILE(預設 ../.env,相對於 scripts/)
- LITELLM_DEBUG(預設開啟)
- LITELLM_NO_EMOJI
- LITELLM_TEST
- LITELLM_TEST_MODEL(建議 tier-a 或 tier-b)
相容項:
- LITELLM_MASTER_KEY(目前 disable_auth: true,通常可不設)
5. 常見問題¶
- 有
GEMINI_API_KEY但沒GEMINI_API_KEY_1 - 症狀:動態 config 不按預期展開
-
解法:改成序號格式至少一把 key
-
你改了 proxy 埠號,但
llm_config.py仍檢查 4001 - 症狀:系統判定 proxy 不可用,直接 fallback
-
解法:同步調整
llm_config.py或維持 4001 -
LITELLM_TEST_MODEL=tier-s - 症狀:測試模式失敗
- 解法:改成現有別名(通常是
tier-a或tier-b)
6. 交叉引用¶
- Gemini API key 取得:
docs/how-to/google-cloud-project-prerequisites.md - Google OAuth 驗證:
docs/how-to/google-oauth-validation.md - Google OAuth 架構:
docs/reference/google-oauth.md
開發文件
Planner Studio 前端完整實作指南¶
簡介¶
Planner Studio 是一款零框架微前端應用,採用 Vanilla JavaScript + Semantic HTML + Modular CSS 設計模式。該應用用於視覺化時間規劃建議、管理日曆事件衝突,並支援多方案互動與即時套用。
零框架設計的價值主張¶
優勢: - 極速啟動:無編譯步驟,直接解析 ES Modules,首屏時間 < 500ms - 依賴最小:僅依賴 FullCalendar 6.1(CDN 引入)與 RRule 2.8(日期遞迴) - 實時性強:無虛擬 DOM 層,DOM 操作直接,支援 60fps 縮放動畫 - 風格靈活:主題切換零開銷,CSS 變數驅動,支援亮色/深色/綠色三主題
局限性:
- 狀態管理依賴手動同步(全域 state 物件)
- 大規模表單互動需要手寫事件委派邏輯
- 無內建路由(當前應用單頁,URL 中含 Session ID)
- 無自動水合 (Hydration),SSR 不適用
架構設計¶
Planner Studio
├── 入口層 (Bootstrap)
│ └── main.js: 解析 URL、驗證配置、啟動 ui.init()
│
├── 核心層 (Core)
│ ├── state.js: 全域狀態 + DOM 元素緩存
│ ├── utils.js: 無狀態工具函式(9 個純函式)
│ └── contracts/planner_payload.schema.js: Zod 驗證契約
│
├── 特性層 (Features)
│ ├── view.js: UI 渲染、互動邏輯(991 行)
│ │ ├── 日曆渲染 (FullCalendar 初始化)
│ │ ├── 方案面板更新
│ │ ├── 主題切換
│ │ └── 事件監聽樞紐
│ └── zoom.js: 物理縮放引擎(Shift+Wheel 交互)
│
├── 服務層 (Services)
│ └── api.js: 後端通訊、Payload 規範化、快取管理
│
└── 樣式層 (Styles)
├── main.css: 主入口(@import 聚合)
├── main-light/dark/green.css: 主題選擇器
├── themes/theme.*.css: CSS 變數定義
├── base/
│ ├── layout.css: 主容器、Toolbar、Sidebar
│ ├── calendar.css: FullCalendar 調整 (280 行)
│ └── components.css: 按鈕、卡片、標籤
└── planner_studio.html: 語意化標記 + 內聯樣式
資料流圖¶
User Action
↓
ui.initListeners() → event handler
↓
api.shiftVariant() / api.fetchContext() / api.applyCurrentVariant()
↓
state.payload 更新
↓
ui.renderState() → ui.renderCalendar() →
├─ FullCalendar.setOption()
├─ ui.renderCalendarsSelect()
└─ ui.renderCalendarControls()
↓
DOM 反映最新狀態
模組分解¶
1. state.js — 全域狀態與 DOM 參考¶
職責:集中管理應用狀態與 DOM 元素快取,避免重複查詢 .getElementById()。
export const state = {
payload: null, // PlannerPayload (完整規劃數據)
variantIndex: 0, // 當前方案索引
calendar: null, // FullCalendar 實例
sessionId: null, // URL 提取的 Session ID
applying: false, // 套用中標誌
hiddenCalendars: new Set(), // 隱藏的日曆 ID
currentView: "timeGridDay", // 日/週視圖
currentDate: null, // YYYY-MM-DD
};
export const elements = {
prev, next, pill, conflict, applyBtn,
calendarRoot, side, variantPanel, // 主要容器
draftText, variantTitle, variantDesc, // 文本區域
modalBackdrop, // 事件詳情彈窗
// ... 共 30+ 個元素引用
};
最佳實踐:
- 查詢一次後快取,避免 DOM 樹遍歷重複
- 使用 JSDoc Types 標註複雜物件結構
- 所有客戶端狀態集中於 state.payload
2. utils.js — 無狀態工具函式庫¶
職責:提供跨模組的純函式,無副作用,易於測試。
| 函式 | 用途 |
|---|---|
formatHoursMinutes(date) |
"14:30" 格式 |
durationMinutes(start, end) |
計算時間差(分鐘) |
formatDurationZh(start, end) |
"1小時30分鐘" 中文語義 |
checkTimeOverlap(aS, aE, bS, bE) |
時間段衝突檢測 |
parseDate(v) |
日期字串 → 毫秒 |
easeOutCubic(t) |
三次緩動函式 (0 to 1) |
getCumulativeScaleY(element) |
遞迴計算 CSS Transform Y 縮放 |
normalizeWheelDelta(event) |
滑鼠滾輪標準化 |
設計原則:
- 不依賴 DOM 或全域狀態
- 參數型別使用數字或基本型別
- 返回值明確,無 undefined 歧義
3. contracts/planner_payload.schema.js — 前後端契約¶
職責:使用 Zod 驗證 API 回應,防止 Runtime TypeError。
export const PlannerPayloadSchema = z.object({
session_id: z.string(),
created_at: z.string(),
start_time: z.string(), // ISO 8601
end_time: z.string(),
view_date: z.string(), // YYYY-MM-DD
ai_draft: z.string(), // Markdown 文本
calendar_events: z.array(PlannerCalendarEventSchema),
draft_variants: z.array(DraftVariantSchema),
writable_calendars: z.array(WritableCalendarSchema),
apply_state: ApplyStateSchema,
// ... 包含日曆、任務、Moodle 上下文
});
契約詳解:
PlannerCalendarEvent — 已存在的日曆事項¶
{
id: string; // 全域唯一 ID
calendar_id: string | null; // Google Calendar ID
title: string;
start: string; // ISO 8601
end: string;
all_day: boolean;
location: string;
description: string;
color_class: string; // "cal-color-1" ~ "cal-color-8"
rrule: string | null; // "FREQ=WEEKLY;BYDAY=MO,WE,FR"
recurring_event_id: string | null; // 主雜迴事件 ID
original_start: string | null; // 異常實例原定時間
status: string | null; // "cancelled" 表示被刪除
}
DraftVariant — AI 產生的方案¶
{
variant_id: string;
title: string; // "方案 A: 深度工作集中"
description: string; // Markdown 敍述+推理
events: Array<{
summary: string;
start: string; // ISO 8601
end: string;
all_day: boolean;
location: string | null;
description: string | null;
transparency: string; // "opaque" (占用) | "transparent"
recurrence: string[] | null; // ["RRULE:FREQ=WEEKLY;BYDAY=MO,WE"]
}>;
}
ApplyState — 套用履歷¶
{
applied: boolean;
applying: boolean;
applied_variant_index: number | null;
applied_calendar_id: string | null;
applied_at: string | null; // ISO 8601
}
前後端契約詳解¶
請求 Schema¶
1. 初始載入¶
GET /api/planner/{sessionId}
回應: PlannerPayload
2. 日期導航¶
POST /api/context/fetch
{
"start_time": "2026-01-15T00:00:00Z",
"end_time": "2026-01-22T00:00:00Z",
"view_date": "2026-01-15",
"session_id": "sess_abc123"
}
視窗規則: - 日視圖: 前後各 7 天 - 週視圖: 前後各 3 週
3. 套用方案¶
POST /api/planner/{sessionId}/apply
{
"variant_index": 0,
"calendar_id": "user@gmail.com",
"allow_conflicts": true
}
回應:
{
"applied_variant_index": 0,
"calendar_id": "user@gmail.com",
"created_count": 5
}
互動流程與狀態轉移¶
使用者操作流圖¶
┌─ 應用啟動 ──→ loadPayload() ──→ hydrate(payload)
│ ↓
│ renderCalendar()
│ renderState()
│
├─ 用戶操作
│ ├─ 點擊「下一方案」
│ │ └─ ui.next ──→ api.shiftVariant(+1) ──→ state.variantIndex ++ ──→ ui.renderState()
│ │
│ ├─ Shift + 滾輪
│ │ └─ wheel 事件 ──→ zoom 動畫 ──→ zoomEngine.stepZoomFrame() ──→ calendar.updateSize()
│ │
│ ├─ 點擊日期導航
│ │ └─ btnNextDate ──→ api.fetchContext(newDate)
│ │ ──→ HTTP POST /api/context/fetch
│ │ ──→ _findContextCache() → _hydratePayload()
│ │
│ ├─ 點擊「確定採用此規劃」
│ │ └─ applyBtn ──→ api.applyCurrentVariant()
│ │ ──→ HTTP POST /api/planner/{id}/apply
│ │ ──→ state.payload.applyState.applied = true
│ │ ──→ ui.renderState() (按鈕變灰、顯示「已套用」)
│ │
│ └─ 主題切換
│ └─ themeToggleBtn ──→ ui.applyTheme(nextMode)
│ ──→ localStorage.setItem()
│ ──→ calendar.render()
│
└─ 清理 (卸載)
└─ api 停止輪詢、事件監聽移除
效能考量與優化¶
1. DOM 操作批次化¶
反面例子:
// ❌ 低效:每次迭代新增一個元素
for (const cal of calendars) {
container.appendChild(createCalendarToggle(cal)); // 觸發 reflow
}
改善:
// ✅ 高效:先構建 DOM,集中插入
const fragment = document.createDocumentFragment();
for (const cal of calendars) {
fragment.appendChild(createCalendarToggle(cal));
}
container.appendChild(fragment); // 單次 reflow
2. 縮放動畫無卡頓策略¶
// Shift 鍵時,保持鼠標下的分鐘數恆定
zoomEngine.scrollMinuteToClientY(
zoom.cursorMinute,
zoom.cursorClientY,
blend = 1, // blend 為 1 表示完全追蹤
currentPpm
);
// 使用 CSS 變數驅動尺寸,讓瀏覽器自動優化
fcRoot.style.setProperty("--slot-height", `${ppm * 15}px`);
3. 快取策略¶
- Context 快取:5 分鐘 TTL,大弧度時間窗判定(避免微小日期變更重新請求)
- SessionStorage 快取:儲存最後一次生成的方案,頁面重整後復原
- 元素快取:
elements物件預快取 30+ 元素引用
4. 常見陷阱¶
| 陷阱 | 症狀 | 解決方案 |
|---|---|---|
| Midnight Sliver 文本重疊 | 跨日期事件文字糊在一起 | 使用 fc-event-boundary-sliver 類,調整 flex-direction |
| 縮放後日曆不重繪 | 時槽尺寸變了但 FullCalendar 不覺得 | calendar.updateSize() + calendar.render() |
| 主題切換時事件樣式不變 | 深色模式仍是淺色文字 | 主題 CSS 使用 var() 變數,在 data-theme 變時更新 |
| 快取導致方案舊資料 | 更新方案後看到舊版本 | SessionStorage 快取在 Session 變更時自動清除 |
開發指南:新增功能¶
案例 1:新增「導出到 ICS」功能¶
步驟 1:建立新 Utility¶
// js/utils.js 新增
export function buildIcsCalendar(events) {
const ics = [
'BEGIN:VCALENDAR',
'VERSION:2.0',
'PRODID:-//TimeCompass///',
];
for (const e of events) {
ics.push('BEGIN:VEVENT');
ics.push(`DTSTART:${toIcsDate(e.start)}`);
ics.push(`SUMMARY:${escapeIcs(e.summary)}`);
ics.push('END:VEVENT');
}
ics.push('END:VCALENDAR');
return ics.join('\r\n');
}
步驟 2:在 view.js 掛入事件¶
// view.js
const exportBtn = document.getElementById("exportBtn");
exportBtn.addEventListener("click", () => {
const events = state.payload?.draftVariants?.[state.variantIndex]?.events || [];
const ics = buildIcsCalendar(events);
const blob = new Blob([ics], { type: 'text/calendar' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `planner_${state.currentDate}.ics`;
a.click();
});
調試技巧¶
啟用追蹤日誌¶
在 URL 後加 ?trace_planner=1,如:
http://localhost:8766/planner/sess_abc123?trace_planner=1
輸出範例:
[planner-trace][view] renderState() variantIndex=0
[planner-trace][api] hydrate-payload sessionId=sess_abc123 draftVariants=3
常用 DevTools 命令¶
// 檢視目前狀態
console.log(state);
// 手動觸發方案切換
api.shiftVariant(1);
// 清除快取
state.contextCache = [];
// 強制重繪日曆
state.calendar?.render();
最佳實踐總結¶
通用原則¶
- 狀態單一源:所有應用狀態存放在
state.payload,不要分散 - 純函式優先:工具函式應無副作用,易於測試
- CSS 變數驅動:動態尺寸、顏色皆透過
--var-name定義 - 事件委派:若有動態 DOM,使用事件委派而非直接綁定
- 快取結合 TTL:API 呼叫加快取,但需設定失效期限
命名規範¶
- 全域狀態物件:
state.* - UI 渲染方法:
ui.render*() - API 服務方法:
api.[loadXxx|fetchXxx|applyXxx]() - 工具函式:
[verb][Noun](),如formatHoursMinutes、checkTimeOverlap
附錄:完整模組依賴圖¶
main.js (入口)
↓
state.js (狀態+快取) ←─────────────────────┐
↑ │
│ │
view.js (UI 渲染) ←───┐ api.js (服務層) ├─ zoom.js
│ │ ↓ │
└──→ utils.js (工具) → contracts/ ├─ planner_payload.schema.js
planner_payload.schema.js ←───────┘
更新日期:2026年3月2日 | Planner Studio Frontend Reference
Time Compass 提示詞設計完整指南¶
簡介¶
Time Compass 的提示詞系統以心理學驅動的漸進式決策為核心,將使用者的模糊想法逐步轉化為可執行的行動方案。整個系統從情感承接開始,經過意圖路由、方案規劃、行動拆解、質詢確認,最後到總結回顧,形成一個完整的時間管理閉環。
核心設計理念: - 非評判性:絕不責備使用者的拖延、混亂或情緒 - 認知友善:減少使用者的理解負擔(CLT 認知負荷理論) - 動機平衡:在「適度挑戰」與「可達成」間找到平衡點(葉杜法則) - 循序漸進:從模糊目標 → 可行方案 → 具體行動 → 實踐驗收
核心理念與心理學基礎¶
1. 認知負荷理論(Cognitive Load Theory, CLT)¶
應用原則: - 聚焦與分段:不一次呈現超過 3–5 個選項或要點 - 漸進揭示:先給方向,再補細節 - 層級清晰:用標題與列點減少理解成本
在各模組中的實踐: - Router:只輸出 1–3 句承接文案,不超過 15 個單詞 - DraftPlan:候選時段限制在 2–5 個 - AskQuestion:題數 3–5,每題 3–5 選項
2. 認知行為治療(Cognitive Behavioral Therapy, CBT)¶
核心技巧: - 去災難化:「這分心情很正常,你不是唯一的」 - 共感不同情:認可情緒,但不表示憐憫 - 行為啟動:提供 1–2 個簡單的立刻可做活動(≤2 分鐘)
在 EmotionSupport 中的實踐: - 點出具體情緒(沮喪、焦慮、疲勞)而非籠統接受 - 將情緒正常化:「這是很多人都有的狀態」 - 提供微行動建議:喝杯熱水、深呼吸等可立即執行的小步驟
3. 葉杜心理定律(Yerkes-Dodson Law)¶
原理: 適度的壓力與挑戰能提升動機,但過多則導致焦慮,過少則無法激發。
動機-壓力曲線應用: - 重度情緒(焦慮、低落)→ 降低難度,給極小步驟 - 輕度情緒(開心、中立)→ 提供適度挑戰 - 語氣調整:避免指令句(「你應該...」→ 「我們一起...」)
具體措施: - 任務時限 ≤60 分鐘(「可達成感」) - 層級化選項(「無壓力選項」 vs 「挑戰選項」) - 緩衝時間設定(+10–20%)
舊版 DSPy 架構¶
1. EmotionSupport 模組¶
角色定位: MBTI 類型 INFJ 的溫暖陪伴者
輸入:
- InteractionContext:包含使用者輸入、過往對話歷史、情緒線索
關鍵規則: - 禁區:不說教、不流程解釋、不流露憐憫、不進行診斷 - 必做:情緒辨識 → 正常化 → 微行動 → 溫柔過渡
輸出結構(A1/A2/B/C/D 五段):
| 段落 | 條件 | 核心任務 |
|---|---|---|
| A1 | 首次情緒支持 | 點出情緒 + 正常化 + 共感 |
| A2 | 後續情緒支持 | 陪伴 + 增強信心 |
| B | 所有情況 | 自然過渡到後續流程 |
| C | 非開心情緒 | 提出 1–2 個簡單微行動 |
| D | 所有情況 | 輕鬆結尾,為後續鋪墊 |
關鍵例示:
情緒類型 → 標題示例 → 行動建議
─────────────────────────────────
重度低落 → 「我聽見你的沮喪」→ 「休息 5 分鐘,喝杯熱水」
輕度焦慮 → 「我能理解你的急迫」→ 「先記下三件事,排序」
開心興奮 → 「我真替你開心」→ 「把這感覺記下來」
2. RouterSignature 模組¶
角色定位: 第一關意圖識別與流程決策
核心決策邏輯:
情緒分流策略:
- 重度情緒(低落/憤怒/焦慮/疲勞)
- 第一輪:["EmotionSupport", <task>, "EmotionSupport"](前後夾擊)
- 後續:[<task>, "EmotionSupport"](尾部收尾)
- 中度情緒(開心/輕度負面)
- 第一輪:
[<task>, "EmotionSupport"](後置收尾) -
後續:可省略或選擇性收尾
-
無明顯情緒
- 直接路由至任務模組
白名單模組:
Pipeline = ["EmotionSupport", "Summary", "Scheduling"]
# 排列規則
Summary + Scheduling → ["Summary", "Scheduling"]
只要回顧 → ["Summary"](必含)
只要排程 → ["Scheduling"](必含)
只要情感 → ["EmotionSupport"]
3. DraftPlan 模組(方案級規劃)¶
角色定位: 需求釐清與粗規劃的策略引導者
關鍵判定標準(任務等級分類):
| 等級 | 特徵 | DraftPlan 應對 |
|---|---|---|
| L1 (模糊) | 目標空泛、資訊明顯不足 | 八段完整結構 |
| L2 (可產出) | 具體截止、明確目標 | 五段結構 |
| L3 (明確) | 任務完全拆解 | 跳過 DraftPlan,直進 DraftAction |
核心輸出:
- draft_plan(使用者可見)
- 必含「我可以怎麼幫你」與「有哪些方向可以努力」
- 不追問、不否定
-
列點與分項呈現
-
constraints_and_missing_info(供後續模組使用)
[MISSING] 截止時間不明確 [ASSUMPTION] 預設以未來 7 天作為規劃週期 [RISK] 時間窗不足可能影響行動品質
4. DraftAction 模組(行動級排程)¶
角色定位: 可執行的具體行動與時間落地
進入前置條件: - 有明確截止點或時間窗口 - 任務已拆至「可產出」的粒度 - 每項任務預計≤60 分鐘
核心輸出:
- action_description(含候選時段 A/B/C)
- 每段包含完成定義、風險評估與緩衝時間
-
避免模糊的「做完」說法
-
start_instructions(1–3 步,立刻可做)
-
第一步必須 <5 分鐘
-
action_metadata(Google Tasks 序列格式)
關鍵限制條件: - 候選時段 2–5 個(避免過載選擇) - 動詞明確(不能用「研究一下」) - 標註風險與緩衝百分比
5. AskQuestion 模組¶
角色定位: 從缺失資訊萃取最關鍵問題
核心轉換邏輯:
[MISSING] 截止時間
[ASSUMPTION] 假設以未來 7 天
[RISK] 時間不足
↓
[QUESTION] 「你想在多久內完成?」
A. 3 天內(密集)
B. 一週內(正常)
C. 兩週以上(充裕)
D. 我不確定
輸出結構: 1. 簡介(為什麼需要這些資訊) 2. questions(3–5 題,每題 3–5 選項) 3. next_steps_suggestion(回答後能做什麼)
質詢哲學: - 問最少、拿最多(合併關聯常數) - 必含「我不確定/我不知道」選項 - 禁止閒聊題,只問會影響排程決策的問題
6. SummaryWriter 模組¶
角色定位: 溫和的觀察者與成就解析者
核心功能: 將零散的活動紀錄轉化為洞察性的週回顧報告
輸出結構(6 個區域):
| 區域 | 內容 | 設計重點 |
|---|---|---|
| 事件回顧 | 分類列點(課業/工作/社交/娛樂) | 客觀、無指責 |
| 完成度分析 | 已完成 vs 規劃的比例 | 溫暖鼓舞,不強調不足 |
| 改進建議 | 基於實驗心態的下週調整 | 正向、可行 |
| 時間占比 | 圓餅圖 / 進度條(Emoji) | 視覺化易解讀 |
| 情緒狀態 | 何時、什麼情緒、為何 | 溫柔陳述,結尾安撫 |
| 成就與亮點 | 正面的每日小勝利 | 抵抗蔡加尼克效應 |
關鍵禁忌: - 絕不責備或責問使用者 - 避免「你應該...」「為什麼不...」 - 不列舉「未完成」的項目 - 避免超誇獎讚
新版 MCP 架構¶
MCP 協定回傳的 Prompt 資源¶
新版架構將所有提示詞封裝為 MCP 工具,允許外部系統動態獲取並渲染。
檔案結構:
src/time_compass/mcp/prompts/
├── content/
│ ├── emotion_support.md
│ ├── tem.md
│ ├── summary_writer.md
│ └── simple_hello.md
│
└── domain_prompts.py // MCP 工具入口
MCP 工具定義¶
| 工具 | 用途 |
|---|---|
emotion_support_prompt() |
情緒承接指令 |
time_management_master_prompt() |
方案規劃指導 |
summary_writer_prompt() |
總結生成指導 |
與 DSPy 架構的對應關係¶
| DSPy Signature | MCP Tool | 輸入 | 輸出 |
|---|---|---|---|
EmotionSupport |
emotion_support_prompt() |
InteractionContext |
str |
RouterSignature |
time_management_master_prompt() |
InteractionContext |
List |
DraftPlan |
time_management_master_prompt() |
InteractionContext + ResourceContext |
str |
DraftAction |
time_management_master_prompt() |
InteractionContext + ResourceContext |
str + List |
AskQuestion |
time_management_master_prompt() |
constraints_info |
List[Question] |
SummaryWriter |
summary_writer_prompt() |
ResourceContext |
str |
認知負荷的三個維度¶
1. 內在負荷(Intrinsic Load)¶
提示詞設計如何降低? - 避免一次呈現 >5 個選項 - 使用「漸進揭示」:先給方向,再補細節 - 層級化組織(標題 → 列點 → 詳述)
2. 外在負荷(Extraneous Load)¶
提示詞設計如何降低? - 禁用複雜術語,改用日常用語 - 避免冗長段落,改全短句 + 列點 - 移除「流程說明」(由 Router 單獨承擔)
3. 相關負荷(Germane Load)¶
提示詞設計如何提升? - 使用類比與實例具體化抽象概念 - 鼓勵使用者的能動性(「你覺得呢?」) - 提供機制化的思考框架(決策樹)
最佳實踐¶
1. 情緒分層判定¶
# 偽代碼示示
def classify_emotion(text: str) -> str:
patterns = {
"重度": ["撐不住", "快崩潰", "完蛋了", "沒辦法", "放棄"],
"輕度": ["有點", "小", "有些", "不太"],
"中度": ["焦慮", "急迫", "開心"],
}
# ...return classification
2. 時間段限制¶
| 等級 | 任務時長 | 時段建議 |
|---|---|---|
| L1 (模糊) | 不限 (只需釐清目標) | 粗規劃 |
| L2 (拆分) | ≤60 min 每段 | 形成大致流程草案 |
| L3 (排程方案) | ≤60 min | 可行動方案 |
- [ ] 避免「必須、應該、必需」等指令詞
- [ ] 含有「我們」而非「你應該」
- [ ] 完成定義明確(「做完」→ 「完成 X,達成 Y」)
- [ ] 情緒詞彙具體(「開心」→ 「為進度感到開心」)
- [ ] 列點 ≤5 項每組
- [ ] 段落 ≤4 句
Prompt 實作架構與 MCP 組織¶
架構關係(Domain ↔ MCP)¶
domain 的各模組(summary / emotion / schedule / router)各自從 prompt/*.md 載入 Prompt,供 DSPy Signature 使用。
mcp/prompts 這邊則維護一份獨立 content/*.md,再由 domain_prompts.py 讀取後包成 MCP Tools。
也就是: - 概念上:MCP Prompt 來自 Domain Prompt 的延伸與重組 - 實作上:目前不是動態繼承(非 runtime import domain prompt 檔),而是「檔案複製/改寫後」由 MCP 層單獨載入
Domain 模組與 Prompt 對應¶
| Domain 模組 | Signature 載入檔 | Prompt 檔案 |
|---|---|---|
summary |
SummaryToolSignature, SummaryWriterSignature |
summary/prompt/summary_tool.md, summary/prompt/summary_writer.md |
emotion |
EmotionSupportSignature |
emotion/prompt/emotion_support.md |
schedule |
SchedulingRouter, DraftPlan, DraftAction, AskQuestion |
schedule/prompt/router.md, draft_plan.md, draft_action.md, ask_question.md, tem.md |
router |
RouterSignature |
router/prompt/router.md |
MCP Prompt 檔案來源對照(繼承/重組)¶
MCP 檔案 (mcp/prompts/content) |
主要 Domain 對應來源 | 關係判定 |
|---|---|---|
emotion_support.md |
domain/emotion/prompt/emotion_support.md |
完全一致(內容相同) |
summary_writer.md |
domain/summary/prompt/summary_writer.md |
同源改寫(小幅差異) |
tem.md |
domain/schedule/prompt/tem.md |
同源改寫(整合 draft_plan.md 、draft_action.md、ask_question.md、schedule/prompt/router.md) |
simple_hello.md |
(未在 domain prompt 找到直接對應) | MCP 專用模板 |
目前已公開的 MCP Prompt Tools(3 支)¶
| Tool 名稱 | domain_prompts.py 載入內容 |
說明 |
|---|---|---|
summary_writer_prompt |
content/summary_writer.md |
週回顧與成就解析 |
emotion_support_prompt |
content/emotion_support.md |
情緒支持與行為啟動 |
time_management_master_prompt |
content/tem.md |
規劃排程主流程(L1/L2/L3) |
尚未公開為 MCP Tool 的 Prompt 模板¶
ask_question.md- 缺失資訊轉換為關鍵問題draft_action.md- 行動級排程(≤60 分鐘)draft_plan.md- 方案級規劃(L1/L2/L3 分類)router.md- 意圖識別與流程決策simple_hello.md- MCP 專用的簡單歡迎模板
實作位置¶
- MCP 註冊:
src/time_compass/mcp/prompts/domain_prompts.py - MCP Prompt 內容:
src/time_compass/mcp/prompts/content - Domain Prompt 來源:
src/time_compass/domain/*/prompt
維護建議¶
- 若更新
domain/*/prompt/*.md,請同步檢查mcp/prompts/content/*.md是否需同步 - 若要避免兩份內容漂移,可考慮改為由 MCP 層直接讀取 Domain Prompt(或建立單一來源生成流程)
總結¶
Time Compass 的提示詞系統融合了嚴謹的心理學基礎與務實的工程設計,形成一個既能同情使用者情感,又能高效推進行動的協作系統。從舊版 DSPy 的完整管線到新版 MCP 的模組化架構,核心理念保持不變:理解、陪伴、引導、落地。
更新日期:2026年3月5日 | Time Compass Prompt Design Reference
Streaming Architecture¶
Goal¶
提供串流(streaming)相關的 helper 與管理:把來自 domain/orchestrator 的 chunked 輸出標準化並逐步推送到前端(或 CLI),並處理延遲、錯誤回報等機制。
Inputs¶
user_input: str(要送入 orchestrator 的文字)。- 環境變數:
STREAM_DELAY(每個 chunk 的 sleep fallback)、Gradio theme 可選。 - 可選的
build_streaming_orchestrator(從 domain 導入,若不可用會 fallback)。
Outputs¶
- Async generator of string chunks(格式化後的串流文字,範例:
[Module:field] text)。 - 當 orchestrator 不可用時,回傳 input 作為 fallback。錯誤時回傳錯誤字串。
Signature¶
format_stream_output(module: str, field: str, text: str) -> str:將 chunk 包裝成易於顯示的格式。async def stream_main_output(user_input: str) -> AsyncGenerator[str, None]:核心的 async generator;從build_streaming_orchestrator()取得output_stream並逐 chunk yield。若無 orchestrator,yield 原始輸入。async def chat_fn(message: str, history: List[Dict[str, Any]]):對 Gradio chat 的串流回調,將stream_main_output的結果逐步 yield 回前端。def test_stream_main_output():CLI 測試輔助函式。
Workflow & Upstream Mapping¶
stream_main_output被呼叫,若build_streaming_orchestrator可用,建立 orchestrator instance。- 從 orchestrator 取得 async output stream(每個 chunk 可能包含
signature_field_name,module_name,chunk等屬性)。 - 當偵測到新 field/module 時重置累積字串,並持續累加,yield 已格式化的累積內容給前端。每次 yield 後會根據
STREAM_DELAY暫停。 - 若發生例外,yield 錯誤訊息以便前端顯示。
簡要流程:
User message → stream_main_output → orchestrator (async chunks) → format & yield → Frontend
State Machine¶
┌─────────────────────────────────────────────────────────────────┐
│ stream_main_output(user_input) │
└────────────────────┬────────────────────────────────────────────┘
│
┌──────────▼──────────┐
│ Load Orchestrator? │
└──────┬────────┬─────┘
│ │
Yes ◄─┘ └─► No (Fallback)
│ │
┌───────▼─────────┐ ┌───▼──────────────┐
│ Build Instance │ │ Yield Raw Input │
└───────┬─────────┘ └──────────────────┘
│
┌───────▼──────────────────────────────┐
│ Iterate Async Output Stream │
│ (Each chunk: {field, module, text}) │
└───────┬──────────────────────────────┘
│
┌───────▼───────────────────────────────────────┐
│ Detect Field/Module Change? │
│ If yes: Reset accumulator │
│ If no: Append to accumulator │
└───────┬──────────────────────────────┬────────┘
│ │
┌───────▼────────────────────────┐ │
│ Format & Yield │ │
│ [Module:field] accumulated_txt │ │
└───────┬────────────────────────┘ │
│ ┌──────────────────────┘
│ │
┌───────▼──────▼───────────────────┐
│ Sleep STREAM_DELAY (env config) │
└────────────┬────────────────────┘
│
Continue iterate? ──Yes──► Loop back to field check
│
No
│
┌─────────▼────────────┐
│ Return (End Stream) │
└──────────┬───────────┘
│
Handle Exception?
│
┌──────────▼──────────┐
│ Yield Error Message │
│ Then Return │
└──────────────────────┘
Integration Points¶
With Domain Orchestrator¶
The orchestrator must provide:
class OutputStream:
"""Expected async iterator from domain.orchestrator"""
async def __aiter__(self):
# Yield chunks with:
# chunk.signature_field_name: str
# chunk.module_name: str
# chunk.chunk: str
pass
With Gradio UI¶
async def chat_fn(message: str, history: List[Dict[str, Any]]):
"""
Gradio callback that:
1. Calls stream_main_output(message)
2. Yields each chunk to Gradio chatbot component
3. Updates message history
"""
async for chunk in stream_main_output(message):
yield chunk
# Gradio handles frontend update
Configuration¶
| Env Variable | Default | Purpose |
|---|---|---|
STREAM_DELAY |
0.1 | Sleep seconds between chunk yields (simulates typing) |
GRADIO_THEME |
(auto) | Gradio UI theme (affects color, layout) |
Error Handling¶
try:
async for chunk in orchestrator.output_stream:
# Process chunk
except Exception as e:
yield f"❌ Error: {str(e)}"
# Frontend displays error message
Testing¶
CLI test helper available:
def test_stream_main_output():
"""Run stream_main_output() in isolation, prints chunks to stdout"""
pass
Usage:
python -c "from time_compass.interface.streaming import test_stream_main_output; test_stream_main_output()"
Notes¶
STREAM_DELAY可透過環境變數調整,以模擬或穩定顯示速度。- 請確保 domain 層的 orchestrator 提供相容的 chunk 物件(帶有
signature_field_name、module_name、chunk屬性),否則 fallback 行為會啟動。 - Async generator ensures non-blocking UI updates in Gradio.
Time Compass Capture-First TDD 測試套件深入分析¶
簡介:Capture-First TDD 的理念¶
Time Compass 採用 Capture-First TDD(先擷取後驅動)的測試哲學,這是一套突破傳統 Unit Test 與 Mock-Based 測試侷限的創新方法論。
核心問題背景¶
在開發高度依賴外部 API(Google Calendar、Moodle)的專案時,手動撰寫 Mock 會產生兩大痛點:
- Mock 幻覺:開發者自己捏造的 JSON 結構往往與真實 API 回應有落差,導致測試通過但實際整合時掛掉
- 開發迴圈極慢:每次程式碼改動都需要發出真實網路請求,導致測試執行時間從秒級跳升到分鐘級
核心決策¶
強制推行「攔截與快照真相」策略,區分兩層離線資料:
| 層級 | 位置 | 用途 | 特性 |
|---|---|---|---|
| L0 快照 (Snapshots) | tests/snapshots/ |
Unit/Regression 測試 | 原始、未清洗的 Raw JSON,完全從真實 API 擷取 |
| L1 展示框架 (Fixtures) | assets/fixtures/ |
Dev Mode 示範、前端開發 | 經清洗、脫敏的資料,供無憑證環境使用 |
優點¶
- ⚡ 光速回饋迴圈:單元測試套件在數百毫秒內執行完成
- 🔐 零憑證開發體驗:透過
MCP_DEV_MODE=1環境變數,評審/設計師無需任何 Token 即可完整驗證流程 - ✅ 消除 Mock 幻覺:確保測試資料與真實 API 結構一致
三層測試架構¶
Time Compass 實施嚴格的三層分離測試策略,每層有明確的職責邊界與資料來源。
架構圖¶
┌─────────────────────────────────────────────────────────────┐
│ UI/MCP Tools (Frontend) │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ L3: System Tests (端對端整合,MCP 工具鏈) │
│ • Location: tests/system/mcp_suite/ │
│ • Task: 驗證 MCP 工具註冊,模擬真實用戶對話 │
│ • Data: In-process FastMCP + Snapshot 資料 │
│ • Speed: ~1-5 秒 per test │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ L2: Integration Tests (領域邏輯與轉換) │
│ • Location: tests/unit/integrations/ │
│ • Task: 驗證 API 響應 → 內部模型 → TOON 序列化 │
│ • Data: tests/snapshots/ 中的 Raw JSON │
│ • Speed: ~100ms per test │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ L1: Unit Tests (單一函數/類別的行為) │
│ • Location: tests/unit/{services,domain,utils,mcp}/ │
│ • Task: 驗證 Monad、時間邏輯、資料驗證 │
│ • Data: 直接構建的簡單物件或 Fixture │
│ • Speed: ~1-10ms per test │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ L0: Live Tests (真實 API 通訊,擷取快照) │
│ • Location: tests/live/ │
│ • Task: 與真實 Google/Moodle API 通訊,產生新快照 │
│ • Data Requirement: Google OAuth Token + Moodle 帳密 │
│ • Speed: ~10-60 秒 per test (網路延遲) │
│ • Usage: 手動執行(`uv run pytest tests/live/`) │
│ • Output: tests/snapshots/ 中的 Raw JSON │
└─────────────────────────────────────────────────────────────┘
L1 層:單元測試 (Unit Tests)¶
🏠 位置 : tests/unit/
目錄結構:
tests/unit/
├── domain/
├── integrations/
├── planner/
├── services/
├── mcp/
└── utils/
L2 層:整合測試 (Integration Tests)¶
🔗 位置 : tests/unit/integrations/ 的專責測試
L3 層:系統測試 (System Tests)¶
🌐 位置 : tests/system/mcp_suite/
L0 層:Live 測試(真實 API)¶
🚀 位置 : tests/live/
Fixture 與 Snapshot 管理策略¶
數據層級的三分法¶
L0: Raw API Response (最源頭)
↓ 轉換
L1: Processed Snapshot (中間格式)
↓ 脫敏
L2: Demo Fixtures (展示用)
Snapshot 的生命週期¶
1️⃣ 擷取階段(Live Capture)¶
export MOODLE_ACCOUNT="your_account"
export MOODLE_PASSWORD="your_password"
uv run python tests/snapshots/capture_snapshots.py
2️⃣ 配置與管理¶
# tests/snapshots/lib/snapshot_config.py
SNAPSHOT_START = datetime(2025, 11, 1, 0, 0, 0, tzinfo=timezone.utc)
SNAPSHOT_END = datetime(2025, 11, 30, 23, 59, 59, tzinfo=timezone.utc)
3️⃣ 快照的讀取¶
# tests/unit/integrations/test_google_calendar.py
import json
from pathlib import Path
def test_parse_calendar_list():
snapshot_path = Path("tests/snapshots/google/calendar_list.json")
raw_data = json.loads(snapshot_path.read_text())
google_calendars = [GoogleCalendarListRead.from_dict(item) for item in raw_data["items"]]
assert len(google_calendars) > 0
Mock Context 與 Dev Mode 的關係¶
Dev Mode 架構¶
啟動方式:
export MCP_DEV_MODE=1
核心實現¶
# src/time_compass/mcp/dev_mode.py
def is_dev_mode() -> bool:
return os.environ.get("MCP_DEV_MODE") == "1"
async def get_mock_resource_context(view_date=None) -> ResourceContext:
"""模擬完整的 ResourceContext"""
cal_list_data = load_fixture("google/calendar_list.json")
events_data = load_fixture("google/calendar/events_list.json")
# ...
return ResourceContext(...)
MCP 工具中的 Dev Mode 分支¶
@mcp.tool()
async def list_tasklists() -> list[dict]:
"""List all Google Tasks"""
if is_dev_mode():
fixture = load_fixture("google/tasks/tasklists_list.json")
return {"items": fixture.get("items", []) if fixture else []}
# 正常模式
auth_provider = google_auth_provider()
result = await async_get_all_tasks(...)
# ...
常見測試片段與最佳實踐¶
1. 從 Snapshot 讀取並解析¶
# ✅ 推薦做法
def test_parse_google_events_from_snapshot():
snapshot = Path("tests/snapshots/google/events_list.json")
raw_data = json.loads(snapshot.read_text(encoding="utf-8"))
events_result = AllCalendarEventsResult.model_validate(raw_data)
assert len(events_result.items) > 0
assert all(hasattr(e, "id") for e in events_result.items)
2. Mock 非同步 API 調用¶
# ✅ 推薦做法
@pytest.mark.asyncio
async def test_calendar_fetch_with_result_monad():
from unittest.mock import AsyncMock, patch
from time_compass.utils.result import Ok, Err
mock_client = AsyncMock()
mock_auth = AsyncMock(return_value="Bearer token")
with patch("...batch_execute_async") as mock_batch:
expected_response = [Ok(AllCalendarEventsResult(...))]
mock_batch.return_value = expected_response
result = await async_get_all_events(
client=mock_client,
request=GoogleAllCalendarRequest(...),
google_auth_provider=mock_auth
)
assert is_ok(result)
3. 驗證異常與 Error Handling¶
# ✅ 推薦做法
@pytest.mark.asyncio
async def test_google_api_error_handling():
from time_compass.integrations.common.exceptions import GoogleAuthError
# 模擬 API 錯誤
with patch("...batch_execute_async") as mock_batch:
mock_batch.side_effect = GoogleAuthError("401 Unauthorized")
result = await async_get_all_events(...)
assert is_err(result)
assert isinstance(result.error, GoogleAuthError)
4. 禁止事項 ❌¶
# ❌ 禁止:手動捏造大型 Mock Dictionary
def test_calendar_list_bad():
mock_data = {
"items": [
{
"id": "primary",
# ... 100 行手捏 JSON
}
]
}
# ❌ 禁止:在測試中使用 print()
def test_planner_output():
result = planner.generate()
print(result) # ❌ 污染 pytest 輸出
CI/CD 整合與覆蓋率目標¶
本地執行測試¶
# 快速迴路(L1 層,< 3 秒)
uv run pytest tests/unit/ -v --tb=short
# 含覆蓋率報告
uv run pytest tests/unit/ --cov=src/time_compass --cov-report=html
# 僅運行特定測試
uv run pytest tests/unit/domain/test_interaction_context.py::test_interaction_context_validation -v
# 錯誤時停止
uv run pytest tests/unit/ -x
# 顯示最慢的 10 個測試
uv run pytest tests/unit/ --durations=10
覆蓋率目標¶
| 層級 | 目標 | 說明 |
|---|---|---|
| Domain 層 | ≥ 90% | 業務邏輯必須充分覆蓋 |
| Integration 層 | ≥ 85% | API 轉換邏輯需覆蓋多種情境 |
| Utilities 層 | ≥ 95% | 工具函式應接近 100% |
| MCP Tools 層 | ≥ 80% | 涵蓋主要工具與 Dev Mode 分支 |
| 整體 | ≥ 85% | 全專案目標 |
新增測試的步驟檢查表¶
📋 Pre-Implementation:問題復現與快照擷取¶
□ 1. 識別問題
□ 2. 撰寫 Live Test
□ 3. 保存 Snapshot
🔴 Red Phase:撰寫失敗測試¶
□ 1. 在 tests/unit/ 建立測試類別
□ 2. 從 Snapshot 讀取測試資料
□ 3. 編寫驗證邏輯,執行測試確認失敗
🟢 Green Phase:修正程式碼¶
□ 1. 定位 Bug 所在
□ 2. 修改程式碼
□ 3. 執行測試確認通過
🟡 Refactor Phase:提升程式碼品質¶
□ 1. 檢視修正的程式碼
□ 2. 擴充測試(邊際案例、負面測試)
□ 3. 清理臨時檔案
總結與推薦實踐¶
三層測試的協調運作¶
日常開發迴路:
1. 修改程式碼 → 2. uv run pytest tests/unit/ -x → 3. 檢查證據 → 4. 重複
新增功能或修復 Bug:
1. 執行 Live Test → 2. 提交快照 → 3. 撰寫 Unit Test → 4. 修正程式碼 → 5. 驗證
文檔版本:Phase 4 (Capture-First TDD 完整階段)
適用範圍:tests/ & src/time_compass/
測試標準與風格指南¶
本文件定義 Time Compass 的測試目錄規範、命名準則與分層測試哲學。
1. 目錄結構(Mirror 規範)¶
測試目錄必須嚴格鏡像 src/ 的模組結構,確保可發現性與維護一致性。
tests/
├── helpers/ # 測試工具(fixtures、evidence 收集)
│ └── evidence.py # 核心 evidence 收集器
├── live/ # 真實 API 測試(需憑證、手動執行)
│ └── moodle/ # Moodle Live Tests
├── snapshots/ # L0 API 回應快照(Source of Truth)
│ ├── google/ # Google API 快照
│ └── moodle/ # Moodle API 快照
├── output_result/ # 執行產物(Git 忽略)
├── unit/ # L1-L2:單元與整合測試(Mock/Snapshot Based)
│ ├── integrations/ # 鏡像 src/time_compass/integrations/
│ │ ├── google_calendar/ # 測試 Read Model、Core Mock、TOON
│ │ ├── google_tasks/
│ │ ├── moodle/
│ │ └── common/ # 測試 Batch Dispatcher 等
│ ├── planner/ # Planner 演算法與執行期測試
│ └── utils/ # 工具函式測試(時間、TOON utils)
└── system/ # L3:系統層測試(End-to-End)
└── mcp_suite/ # MCP 工具測試(In-Process FastMCP)
測試命名應具備文件可讀性。
- 檔案:
test_<subject>.py(例如:test_event_read_model.py)。避免test_core.py這種模糊命名。 - 類別:
Test<Subject><Scenario>(例如:TestGoogleEventReadParsing)。 - 函式:
test_<unit>_should_<expected_behavior>_when_<condition> - ✅
test_to_toon_task_should_mark_done_when_completed_field_exists - ❌
test_to_toon_task_done
3. 測試哲學:Capture -> Mock¶
第三方整合測試請遵循分層資料流策略(Data Flow Tiered Strategy):
- Transport / Raw 層:以真實擷取資料(L0 Snapshots)作為 Mock 來源。
- Why:確保解析邏輯能覆蓋真實 API 回應與未文件化邊界情境。
- How:從
tests/snapshots/載入 JSON/Text,禁止在測試中手寫大型 Dict。 - Core / Application 層:Mock API Client,聚焦商業邏輯與流程控制。
- Focus:分頁、錯誤重試、並行請求聚合。
- Data:使用建構過的 Internal/Read 物件。
- TOON / Presentation 層:驗證最終輸出格式。
- Focus:Token 壓縮率與格式正確性。
4. 工具與執行環境¶
- 執行指令:
uv run pytest(不要使用python -m pytest)。 - 證據收集:使用
tests.helpers.evidence.save_test_result()保存關鍵輸出(如 TOON 字串)到tests/output_result/,供人工檢視。 - 禁止事項:正式測試程式碼中禁止
print()。
TOON (Token-Oriented Object Notation) 格式規範¶
目次¶
1. 簡介¶
TOON 是一種專為大語言模型 (LLM) 設計的層次化、標籤化資料壓縮格式。相較於原始 JSON,它透過「重複性欄位提取」、「索引化參照」與「CSV 型標題標記」,在不損失語義的前提下節省 80%+ 的 Token(基於 tiktoken 精確計數)。
A. 區塊分組 (Grouped Structure)¶
資料依來源分為 meta, google_tasks, google_calendar, moodle 四大區塊。每個區塊下以資源 ID (如 Calendar ID) 作為鍵值。
B. 外部索引 (External Indexing)¶
為了避免在每個項目中重複寫入冗長的地點或重複規則,TOON 在區塊頂部建立索引:
- location_index: 映射為 L1, L2 ...(如 L1,臺科大 教室)。
- recurrence_index: 映射為 R1, R2 ...(如 R1,每週一重複)。
C. CSV 標題標記與批量壓縮¶
每個資料列表都有一個帶有欄位定義的標題:
start_here[數量]{欄位1, 欄位2, ...}
緊接著是數行簡化資料,以逗號分隔。
3. 語義分組設計 (Semantic Grouping)¶
TOON 的另一大亮點是語義感知的資料分組,不同來源採用最符合其業務語義的分組邏輯。
A. Google Calendar — 月份分組¶
按月份鍵分組("2025-11":),每月內含 start_here (當月開始的事件):
month:
"2025-11":
start_here[23]{id,summary,lid,rid,notes,st_d,st_wd,st_hm,en_m,en_d,en_wd,en_hm}:
event_id,書法及習作,L1,0,0,3,1,"13:20",same,3,1,"15:10"
實作位置:
models_toon.pybuild_toon_calendar
B. Google Tasks — 四種狀態分類 + 父子樹¶
每個 Tasklist 下按截止日期月份分組,任務再依狀態細分:
- due_open: 有截止日期、尚未完成
- due_done: 有截止日期、已完成
- done: 無截止日期、但已完成(按完成時間分月)
- undated: 無截止日期且未完成的任務
另外,parent_tree 記錄了父子任務的從屬關係,以節省在每支葉任務上重複記錄父任務 ID:
"Project-big":
parent_tree:
"Coding101開發"[6]: 海報&程式碼,出席資料回報,refresh掛回去,掛litellm,...
month:
"2026-03":
due_open[2]{...}:
...
undated[16]{...}:
...
實作位置:
models_toon.pybuild_toon_tasklist,_build_parent_tree
C. Moodle — Course Index + 雙層分組¶
課程名稱提取至 course_index,內部使用 c1, c2 短標識。事件按學期 (semester)→月份雙層分組:
moodle:
course_index[3]{cid,course}:
c1,EC1012301 計算機程式與應用實習
c2,EC163A011 物理(上)
semester:
"114-1":
"2025-11":
due[10]{title,description,cid,status,due_d,due_wd,due_hm}:
Week 9作業繳交截止,Masked,c1,Closed,2,7,"00:00"
實作位置:
moodle/models/models_read.pyto_toon_moodle
3. 真實資料範例 (以 Google Calendar 為例)¶
google_calendar:
"台科大課表":
location_index[1]{lid,location}:
L1,臺科大 TR-510 教室
recurrence_index[1]{rid,rule}:
R1,每週一重複
month:
"2025-10":
start_here[1]{id,summary,lid,rid,notes,st_d,st_wd,st_hm,en_m,en_d,en_wd,en_hm}:
evt_03iq,線代,L1,R1,"導師:...",13,1,"13:20",same,13,1,"15:10"
欄位說明表¶
lid / rid: 對應外部索引。若為0則代表無。- 日期/時間簡寫:
st_d: Start Day (數字)st_wd: Start Weekday (1-7)st_hm: Start Hour:Minute (如 "08:00")same關鍵字: 代表該欄位與起始時段屬性相同(例如結束日期與開始日期為同一天),大幅減少重複日期字串。
4. 壓縮機制解析¶
以下為 TOON 格式達成極致壓縮的四大核心策略,每項策略皆有對應的實作位置:
| 策略 | 說明 | 實作位置 |
|---|---|---|
| Schema-less Header | 標題行定義欄位(如 start_here[10]{id,summary,lid,...}),後續數行僅含純資料,消除所有重複 Key 字串 |
utils/toon_utils.py safe_encode |
| 外部索引化 (Indexing) | 地點、重複規則提取至 location_index / recurrence_index,內部以 L1, R1 短標籤參照 |
models_toon.py _build_location_index, _build_recurrence_index |
| 日期元件化 (Date Decompose) | ISO DateTime 解析並分解為 st_d(日)、st_wd(週幾 1-7)、st_hm(時分)等最小整數元件 |
models_toon.py _parse_datetime_parts |
| 值歸一化 (Value Normalize) | null 以整數 0 表示;結束月份與起始相同時使用 same 關鍵字,大幅減少重複月份字串 |
models_toon.py build_toon_event |
5. TOON 解讀規則(Prompt Description)¶
以下為目前使用的 TOON 解讀規則字串(提供給模型先讀索引再解讀事件/任務):
此資料為 TOON 壓縮格式,請先讀索引再解讀事件/任務列。
[共通規則]
- 0 表示無值或未提供。
- "same" 表示與當前分組 month 相同。
- weekday: 1=週一, 7=週日。
[google_tasks]
- source: tasklist 基本資訊。
- parent_tree: 父任務標題 -> 子任務標題列表。
- month[YYYY-MM] 分組語意:
- due_open: 有 due 且未完成。
- due_done: 有 due 且已完成。
- done: 無 due 且已完成(按 completed 分月)。
- undated: 無 due 且未完成。
- 任務時間欄位:due_m/done_m = same | 月份數字(1-12) | 0;due_d/done_d=日期;due_wd/done_wd=週幾;due_hm/done_hm=HH:MM 或 0。
[google_calendar]
- source: calendar 基本資訊。
- location_index: lid -> location(事件內用 lid 參照)。
- recurrence_index: rid -> recurrence rule(事件內用 rid 參照)。
- month[YYYY-MM] 分組語意:
- start_here: 事件起始時間在本月。
- end_from_past: 從前月延續到本月(欄位預留,若出現才解讀)。
- 事件時間欄位:st_d/st_wd/st_hm 為開始;en_m/en_d/en_wd/en_hm 為結束。en_m = same | 月份數字 | 0;全天事件 st_hm/en_hm="full_day"。
- lid/rid=0 表示該事件無 location/recurrence。
[moodle]
- course_index: cid -> 課程名稱。
- semester[學期][YYYY-MM].due[]: 截止事件列表。
- 每筆 due 常見欄位:title, description, cid, status, due_d, due_wd, due_hm。
6. 壓縮效益實測數據¶
根據 scripts/analyze_toon_compression.py 分析 assets/fixtures/snapshots(清洗後的真實資料)的結果:
- 分析腳本: scripts/analyze_toon_compression.py
- 執行/測試指令 (uv): uv run python scripts/analyze_toon_compression.py
- 資料規模: Google Calendar 188 事件、Google Tasks 47 任務、Moodle 10 課程事件
- Token 計數工具: tiktoken (Encoding: o200k_base / GPT-4o)
- 資料流: fixtures JSON → AllCalendarEventsSnapshot (Layer 2) → AllCalendarEventsResult.from_snapshot() (Layer 3) → ResourceContext → TOON
- 完整報告: TOON_STATS_REPORT.md
- TOON 成品: get_time_context_composite.toon
| 指標 | 標準 JSON | TOON 格式 | 改善幅度 |
|---|---|---|---|
| 字元數 (Chars) | 304,082 | 28,789 | -90.5% |
| 精確 Token (GPT-4o) | 96,772 | 15,800 | -83.7% |
| 資訊密度 | 1.0x | 6.1x | 大幅提升 |
格式對比:雙事件範例(欄位壓縮)¶
原始 Google Calendar API JSON(兩筆事件):
[
{
"id": "evt_a",
"summary": "書法及習作(一)",
"location": "臺科大 TR-510 教室",
"start": { "dateTime": "2025-11-03T13:20:00+08:00", "timeZone": "Asia/Taipei" },
"end": { "dateTime": "2025-11-03T15:10:00+08:00", "timeZone": "Asia/Taipei" },
"recurrence": null,
"description": null
},
{
"id": "evt_b",
"summary": "計算機程式與應用實習",
"location": "臺科大 TR-510 教室",
"start": { "dateTime": "2025-11-10T13:20:00+08:00", "timeZone": "Asia/Taipei" },
"end": { "dateTime": "2025-11-10T15:10:00+08:00", "timeZone": "Asia/Taipei" },
"recurrence": null,
"description": null
}
]
TOON 格式(兩筆事件):
location_index[1]{lid,location}:
L1,臺科大 TR-510 教室
month:
"2025-11":
start_here[2]{id,summary,lid,rid,notes,st_d,st_wd,st_hm,en_m,en_d,en_wd,en_hm}:
evt_a,書法及習作(一),L1,0,0,3,1,"13:20",same,3,1,"15:10"
evt_b,計算機程式與應用實習,L1,0,0,10,1,"13:20",same,10,1,"15:10"
關鍵差異:JSON 兩筆事件都重複完整欄位名稱與巢狀 key;TOON 只宣告一次欄位 header,後續每列只放值,並以
L1參照共享 location。
關鍵優勢¶
- 極致節省 Token: 讓 AI 能在單次請求中載入數個月份的完整排程,而非僅限於當週。
- 模型推理精準度: 結構化 CSV 格式減少了模型對冗餘欄位(大括號、引號)的注意力分散,使模型更能專注於時間判斷邏輯本身。
- 人類可讀性: 雖為資料壓縮格式,但仍具備層次化縮排,開發者在 Debug 時可透過日誌快速追蹤資料分佈。
故障排除手冊 (Troubleshooting)¶
本文件列出了在使用 Time Compass 過程中可能遇到的常見問題與解決方案。
1. Google OAuth 授權問題¶
❌ 問題:啟動授權時瀏覽器未自動開啟¶
- 原因:作業系統預設瀏覽器設定失效。
- 解法:手動在瀏覽器輸入終端機出現的
http://127.0.0.1:8765URL。 - 注意:Windows 使用者若遇到
localhost連線失敗,請改用127.0.0.1。
❌ 問題:點擊「確定」後出現 "Invalid Grant" 或 "Token Expired"¶
- 原因:之前的
token.json已過期或與現在的credentials.json不匹配。 - 解法:刪除專案根目錄的
token.json並重啟 MCP Server 進行重新授權。
2. Moodle 整合問題¶
❌ 問題:無法獲取 Moodle 事件 (Timeout)¶
- 原因:NTUST Moodle 伺服器對外連線較慢,或網頁結構變動。
- 解法:
- 檢查
.env中的NTUST_ACCOUNT與NTUST_PASSWORD是否正確。 - 嘗試延長請求逾時時間(目前預設為 30 秒)。
3. MCP 與 Planner Studio 網路問題¶
❌ 問題:Planner Studio 啟動失敗 (Port Conflict)¶
- 原因:埠號
8766被其他應用程式佔用。 - 解法:
- 呼叫
shutdown_planner_studio強制關閉舊實例。 - 在
launch_planner_studio時指定其他埠號。
❌ 問題:MCP Inspector 無法載入¶
- 原因:防火牆攔截或
uv環境未啟動。 - 解法:確保執行指令前已執行
uv sync,且使用了正確的本地位址127.0.0.1:6274。
4. 資料渲染問題¶
⚠️ 現象:行事曆事件顯示為「農曆新年 (整天)」但渲染位置偏移¶
- 現狀:系統目前對全天事件 (All-day) 的處理採用 UTC 轉 local。若跨時區可能會有 +-1 天的偏移。
- 解法:請檢查
docs/architecture/data-flow-timing.md確認時間戳處理原則。
深度主題
Integration Layer 主題中心¶
Integration 層負責與外部系統(Google Calendar/Tasks、Moodle)進行通信,統一介面與模型轉換
🚀 快速開始¶
首次接觸 Integration 層?按照深度選擇:
| 深度 | 讀者 | 推薦文檔 | 耗時 |
|---|---|---|---|
| L1 | 架構師 / 決策者 | C4_MODEL.md L1-L2 部分 | 10 分鐘 |
| L2 | 新開發者 | C4_MODEL.md 全文 | 20 分鐘 |
| L3 | 實作開發者 | C4_MODEL.md L3-L4 部分 | 30+ 分鐘 |
📚 本主題的內容¶
核心架構文檔¶
- C4_MODEL.md ⭐ 唯一完整參考
- L1: System Context — 誰在和誰講話
- L2: Container — 四大模組架構(含 Mermaid C2 圖表)
- L3: Component — 各模組細節(流程、模型層)
- L4: Code — 檔案位置、常見任務、排查表
- 包含: 例外體系、Batch API、時間過濾、爬蟲登入、模型轉換
🔗 相關主題¶
直接相關¶
- DDD 多層模型架構
- 「為什麼分 Raw/Internal/Read/TOON 四層?」
-
Integration 層的模型轉換是 DDD 設計的完美體現
- 「為什麼用 Result Monad 而不是 Exception?」
- Integration 層的
GoogleError體系與Result[T]設計源於本策略
深度專項¶
- Moodle 整合深度分析
- 登入流程(OIDC + Selenium 備備方案)
- 為什麼雙路徑架構?
- 資料模型轉換(Raw → Internal → Read)
- 非同步爬蟲實作與逾時管理
- 快取與更新策略
- 功能限制與已知問題
- 升級/替換為官方 API 的可行性評估
全系統視圖¶
- 系統全景 — 了解 Integration Layer 在整體系統中的位置
- ADR-0010 文檔架構決策 — 為什麼 Integration 採用 C4 Model 與單一檔案設計
❓ 常見問題¶
「我想…」快速索引¶
| 想做的事 | 看這裡 |
|---|---|
| 理解 Integration 全貌 | C4_MODEL.md L1-L2 |
| 查找檔案位置 | C4_MODEL.md L4.1 |
| 寫程式碼範例 | C4_MODEL.md L4.2 |
| 消除型別混亂 | C4_MODEL.md 快速查詢表 |
| 排查異常 | C4_MODEL.md L4.3 |
| 理解四層模型 | DDD-MODEL-ARCHITECTURE.md + C4_MODEL.md L3.2 |
| 理解 Batch API 與限速 | C4_MODEL.md L3.1 + ERROR-HANDLING-DESIGN.md |
🗂️ 文檔結構¶
docs/reference/INTEGRATION/
├── README.md ← 本檔案(導航)
└── C4_MODEL.md ← 主文檔(L1-L4 四層)
相關主題:
docs/reference/
├── DDD-MODEL-ARCHITECTURE.md
├── ERROR-HANDLING-DESIGN.md
└── ...
docs/architecture/
├── OVERVIEW.md ← 系統全景
├── data-flow*.md ← 資料流詳圖
└── ...
📌 核心概念預覽¶
四大模組¶
# 1. google_calendar/ — 日曆事件
async_get_all_events() → Result[AllCalendarEventsResult]
# 2. google_tasks/ — 任務清單
async_get_all_tasks() → Result[AllTaskResult]
# 3. moodle/ — 課程爬蟲
scrape_moodle_events() → MoodleResult
# 4. common/ — 共通基礎
batch_execute_async() # Batch API 協調器
GoogleError # 統一例外
統一模型轉換¶
API JSON (Google/Moodle)
↓
Raw Layer (L1) ← API 一一對應,camelCase
↓
Read Layer (L2) ← LLM 友善,snake_case,UTC+8
↓
TOON Layer (Compress) ← 極致壓縮,-83.7% token
Result Monad 模式¶
from time_compass.utils.result import Result, Ok, Err, is_ok, is_err
result = await async_get_all_events(...)
if is_ok(result):
data = result.unwrap() # AllCalendarEventsResult
else:
error = result.unwrap_err() # GoogleError (任一子類)
🎯 下一步¶
- 首次接觸:讀 C4_MODEL.md 的 L1-L2 部分(10 分鐘)
- 開始寫程式碼:跳到 L4.2 常見任務 找相關範例(5 分鐘)
- 遇到異常:查 L4.3 狀態碼映射 與 常見問題排查 (5 分鐘)
- 深入理解:讀 L3 Component 細節 瞭解流程(15 分鐘)
Integration Layer 主題中心 ✅ 2026-03-02
OAuth 與認證主題¶
本主題涵蓋 Time Compass 支援的四種認證方式:Google OAuth、Moodle 帳密、Gemini API Key,以及系統層級的環境變數管理。
快速開始¶
1. 你是新使用者,想自己部署 Time Compass?¶
2. 你已設定好憑證,想驗證 OAuth 是否正常?¶
→ 看 OAuth 驗證流程
3. 你想深入理解 Google OAuth 架構與資料流?¶
4. 你需要設定 Moodle 課程整合?¶
→ 看 Moodle 驗證架構參考
5. 你想看完整的整合層設計?¶
→ 看 Integration Layer C4 Model
認證系統概覽¶
| 認證系統 | 用途 | 設定位置 | 主要文檔 |
|---|---|---|---|
| Google OAuth | Calendar、Tasks 讀寫;AI scheduling | GOOGLE_CLIENT_ID/SECRET + token.json |
架構 | 設定 | 驗證 |
| Moodle 帳密 | 課程事件爬蟲 | MOODLE_ACCOUNT/PASSWORD |
架構 | 深度分析 |
| Gemini API Key | AI 對話與排程建議 | GEMINI_API_KEY ( 或 GOOGLE_API_KEY) |
環境變數指南 |
分層文檔結構¶
Reference(架構理解層,5-10 分鐘)¶
適合想瞭解系統設計、資料流、安全邊界的人。
- Google OAuth 架構參考
- 兩條授權路徑(Gradio Session / MCP File)
- 資料流圖與模組責任分工
- 應用範圍與安全邊界
-
常見問題診斷表
- 為什麼 Moodle 不用 Google OAuth?
- 帳密認證與 OIDC 進階方案
- Google OAuth 與 Moodle 帳密的區別
- 升級與替代方案
Tutorial(操作步驟層,20-50 分鐘)¶
適合第一次設定、需要 step-by-step 指引的人。
- Google Cloud 專案設定
- 從零建立 Google Cloud project
- 啟用 Calendar、Tasks、Gemini API
- 設定 OAuth 同意畫面與憑證
-
寫入
.env - Step-by-step 授權驗證
- 測試讀寫能力(Smoke Test / Functional Test)
- 常見錯誤與修復(Redirect URI、Token 失效等)
常見工作流¶
🚀 第一次部署¶
- Google Cloud 專案設定 - 建立憑證
- OAuth 驗證流程 - 驗證授權成功
- 開始使用 Time Compass
🔄 Token 失效或授權相關問題¶
- 檢查 常見問題診斷表
- 按照 OAuth 驗證流程 重新授權
📚 深入瞭解系統¶
- Google OAuth 架構參考 - 資料流、模組設計
- Moodle 驗證架構參考 - Moodle 認證機制
- Integration Layer C4 Model - 整個整合層的四層架構
🌍 雲端部署或多使用者環境¶
- 讀 Google OAuth 架構參考 > 進階延伸
- 考慮 OAuth with database storage 或 Service Account
環境變數速查¶
所有認證相關的環境變數:
# Google OAuth(必要)
GOOGLE_CLIENT_ID=<client_id>
GOOGLE_CLIENT_SECRET=<client_secret>
# Gemini AI(建議)
GEMINI_API_KEY=<api_key>
# 或
GOOGLE_API_KEY=<api_key>
# Moodle(若需整合課程)
MOODLE_ACCOUNT=<帳號>
MOODLE_PASSWORD=<密碼>
詳細說明見環境變數指南。

























