Integration 層 - C4 完整視圖¶
日期: 2026-03-02
目的: 用 C4 Model 統一呈現 Integration Layer 架構,一篇文件涵蓋全景
舊檔案: 整合自 INTEGRATION_LAYER_ANALYSIS.md、QUICK_REFERENCE.md、VISUAL_SUPPLEMENT.md
🔗 交叉引用¶
相關架構設計¶
- DDD 多層模型架構 — Integration 層的「四層模型轉換」(Raw → Internal → Read → TOON)是 DDD 設計的核心實踐
- Railway-Oriented Programming 錯誤處理 — Integration 層的
Result[T]Monad 設計與GoogleError體系遵循本文檔的策略
快速導航¶
- 本頁: Integration Layer 完整視圖(涵蓋 L1-L4)← 你在這裡
- docs/architecture/OVERVIEW.md — 系統全景及 Integration Layer 在整體架構中的位置
- ADR-0007 反規格文件驅動 — 為什麼只保留高階架構圖,細節交由程式碼與 Pydantic 型別
文檔結構概覽¶
本文按 C4 Model 的四層邏輯展開:
| 層級 | 名稱 | 視圖 | 讀時 | 適合誰 |
|---|---|---|---|---|
| L1 | System Context | 誰在和誰講話 | 3 分鐘 | 架構師、決策者 |
| L2 | Container | 系統內的 4 大模組 | 10 分鐘 | 開發者、新手 |
| L3 | Component | 每個模組的細節 | 15 分鐘 | 實作開發者 |
| L4 | Code | 程式碼參考 | 30+ 分鐘 | 需深入的人 |
你只需要讀一篇檔案,從上往下,根據深度需求停止即可。
L1: System Context(系統上下文)¶
「誰在和誰講話」¶
┌─────────────────────────────────────────────────────────┐
│ │
│ Time Compass │
│ (Planning Engine + MCP Server) │
│ │
│ ↕ async API calls │
│ ↕ Batch HTTP │
│ ↕ Web scraping (Selenium) │
│ │
└─────────────────────────────────────────────────────────┘
↗ ↑ ↖
/ │ \
/ │ \
┌──────────┐ ┌─────────┐ ┌───────────┐
│ │ │ │ │ │
│ Google │ │ Google │ │ Moodle │
│ Calendar │ │ Tasks │ │ Platform │
│ API │ │ API │ │(Web Crawl)│
│ │ │ │ │ │
└──────────┘ └─────────┘ └───────────┘
要點: - Time Compass 不擁有 Google/Moodle 的資料,只透過 API/爬蟲 讀取 - 系統是 Integration (整合者) 的角色,負責統一介面與轉換模型 - 三個資料來源各有自己的協議 (REST API / Web HTML)
L2: Container(容器層)¶
「內部四大模組」¶
src/time_compass/integrations/
│
├─ common/ [共通基礎設施]
│ ├─ exceptions.py └─ 8 種 GoogleError
│ ├─ google_api_dispatcher.py └─ batch_execute_async()
│ ├─ http_batch_tool.py └─ Multipart 解析/組裝
│ └─ models.py └─ BaseGoogleRequest/Raw
│
├─ google_calendar/ [Google Calendar 單一責任]
│ ├─ async_core.py └─ async_get_all_events()
│ ├─ api_client_async.py └─ list_events_async()
│ └─ models/ └─ Raw → Read → TOON
│
├─ google_tasks/ [Google Tasks 單一責任]
│ ├─ async_core.py └─ async_get_all_tasks()
│ ├─ api_client_async.py └─ list_tasks_async()
│ └─ models/ └─ Raw → Read → TOON
│
├─ moodle/ [Moodle 爬蟲單一責任]
│ ├─ async_core.py └─ scrape_moodle_events()
│ ├─ scraper.py └─ Selenium 登入
│ └─ models/ └─ Raw → Read
│
└─ ORCHESTRATOR:
└─ get_all_information_from_api() [並行協調器]
Mermaid C2 容器圖¶
graph TB
subgraph TimeCompass["Time Compass - Integrations Layer"]
direction TB
subgraph Orchestrator["🔥 Orchestrator<br/>get_all_information_from_api()"]
ORCH["async_get_all_information_from_api()"]
end
subgraph Common["📦 Common<br/>(Shared Infrastructure)"]
DISP["google_api_dispatcher<br/>batch_execute_async()"]
EXC["exceptions<br/>GoogleError 體系"]
HTTP["http_batch_tool<br/>Multipart 組裝/解析"]
MODELS["models<br/>BaseGoogleRequest"]
end
subgraph Calendar["🗓️ Google Calendar<br/>(Module)"]
CAL_CORE["async_core<br/>async_get_all_events()"]
CAL_API["api_client_async<br/>list_events_async()"]
CAL_MODELS["models/<br/>Raw → Read → TOON"]
end
subgraph Tasks["✅ Google Tasks<br/>(Module)"]
TASK_CORE["async_core<br/>async_get_all_tasks()"]
TASK_API["api_client_async<br/>list_tasks_async()"]
TASK_MODELS["models/<br/>Raw → Read → TOON"]
end
subgraph Moodle["🎓 Moodle<br/>(Web Crawler)"]
MOODLE_CORE["async_core<br/>scrape_moodle_events()"]
SCRAPER["scraper+config<br/>Selenium 登入"]
MOODLE_MODELS["models/<br/>Raw → Read"]
end
end
subgraph External["🌐 External Systems"]
GOOGLE_CAL["Google Calendar<br/>API"]
GOOGLE_TASK["Google Tasks<br/>API"]
MOODLE_WEB["Moodle<br/>Website"]
end
%% Orchestrator委託
ORCH -->|並行執行| CAL_CORE
ORCH -->|並行執行| TASK_CORE
ORCH -->|並行執行| MOODLE_CORE
%% Calendar內部流程
CAL_CORE -->|使用| CAL_API
CAL_API -->|使用| DISP
CAL_CORE -->|轉換| CAL_MODELS
%% Tasks內部流程
TASK_CORE -->|使用| TASK_API
TASK_API -->|使用| DISP
TASK_CORE -->|轉換| TASK_MODELS
%% Moodle內部流程
MOODLE_CORE -->|使用| SCRAPER
MOODLE_CORE -->|轉換| MOODLE_MODELS
%% 例外與工具共享
CAL_CORE -->|依賴| EXC
TASK_CORE -->|依賴| EXC
MOODLE_CORE -->|依賴| EXC
DISP -->|使用| HTTP
DISP -->|拋出| EXC
%% 外部系統
DISP -->|HTTP Batch| GOOGLE_CAL
DISP -->|HTTP Batch| GOOGLE_TASK
SCRAPER -->|Web Crawl| MOODLE_WEB
classDef orch fill:#ff6b6b,stroke:#c92a2a,color:#fff,stroke-width:2px
classDef common fill:#4c6ef5,stroke:#364fc7,color:#fff,stroke-width:1px
classDef module fill:#15aabf,stroke:#0c8599,color:#fff,stroke-width:1px
classDef external fill:#fab005,stroke:#e67700,color:#000,stroke-width:2px
class Orchestrator,ORCH orch
class Common,DISP,EXC,HTTP,MODELS common
class Calendar,CAL_CORE,CAL_API,CAL_MODELS module
class Tasks,TASK_CORE,TASK_API,TASK_MODELS module
class Moodle,MOODLE_CORE,SCRAPER,MOODLE_MODELS module
class External,GOOGLE_CAL,GOOGLE_TASK,MOODLE_WEB external 四大模組關係矩陣¶
| 呼叫者 | 被呼叫者 | 內容 | 返回值 |
|---|---|---|---|
| Orchestrator | async_get_all_tasks() | Google Tasks Batch | Result[AllTaskResult] |
| Orchestrator | async_get_all_events() | Google Calendar Batch | Result[AllCalendarEventsResult] |
| Orchestrator | scrape_moodle_events() | Web Scraping | MoodleResult |
async_get_all_tasks() | batch_execute_async() | HTTP 多部分 | List[Result[GoogleTaskRaw]] |
async_get_all_events() | batch_execute_async() | HTTP 多部分 | List[Result[GoogleEventRaw]] |
| 所有模組 | models_*.py | 轉換 | Raw/Read/TOON |
L3: Component(組件細節)¶
3.1 common/ - 共通基礎設施¶
例外體系(Exception Hierarchy)¶
GoogleError (基類)
├─ GoogleAuthError # 401/403 Token 問題
├─ GoogleAPIError # 4xx/5xx API 異常
├─ GoogleParseError # JSON 解析失敗
├─ GoogleNetworkError # 網路超時
├─ GoogleNotFoundError # 404 資源不存在
├─ GoogleRateLimitError # 429 速率限制
└─ GoogleBatchError # Batch 結構失敗
用途:統一錯誤處理,上層 catch GoogleError 即可。
Batch API 協調器(google_api_dispatcher.py)¶
核心函數:batch_execute_async()
async def batch_execute_async(
client: "ClientSession",
credentials: "Credentials",
requests: List[BaseGoogleRequest],
endpoint: str,
raw: bool = False, # 返回 List[Dict] 還是 List[Result[Raw]]
trace: ContextFetchTrace | None = None,
) -> List[Result[T]]:
"""
多個 API 請求 → 單一 HTTP Batch 呼叫 → 解析回應
特性:
- Type-Safe overload(raw=False 時智能推斷返回型別)
- 自動錯誤轉換 (HTTP code → GoogleError)
- 支援 100+ 請求 batch
"""
工作流: 1. requests[] → build_generic_batch_body() (Multipart 組裝) 2. HTTP POST 到 Google /batch/ 端點 3. parse_generic_batch_response() (Multipart 解析) 4. 每個回應自動轉為 Result[GoogleRaw] 或 Result[GoogleError]
HTTP Multipart 工具(http_batch_tool.py)¶
三個核心函數:
# 1. 組裝請求
build_generic_batch_body(requests: List[BaseGoogleRequest]) -> str
# 輸出: Content-Type: multipart/mixed
# 2. 發送請求
send_batch_request_payload(
client: ClientSession,
endpoint: str,
payload: str,
credentials: Credentials
) -> Tuple[str, str] # (回應文本, boundary)
# 3. 解析回應
parse_generic_batch_response(
batch_response_text: str,
boundary: str
) -> List[Dict] # 每個部分一個 Dict
3.2 google_calendar/ - 日曆事件¶
流程圖:Calendar 完整流程¶
async_get_all_events(request, ...)
│
├─ Step 1: 列舉所有日曆
│ └─ list_calendar_list_async()
│ → GoogleCalendarListEntryRaw[]
│
├─ Step 2: 為每個日曆建構 ListEventsRequest
│ └─ ListEventsRequest(
│ calendar_id="primary",
│ timeMin="2026-02-01T00:00:00Z",
│ timeMax="2026-03-31T23:59:59Z",
│ maxResults=2500
│ )
│
├─ Step 3: Batch 執行第一頁
│ └─ batch_execute_async(requests)
│ → List[Result[GoogleEventRaw]]
│ → 檢查 nextPageToken
│
├─ Step 4: 若有 nextPageToken,重複 Batch 執行
│ └─ 另組新 requests,包含 pageToken
│ → 執行 → 檢查 nextPageToken
│ → 迴圈至無更多頁面
│
├─ Step 5: 轉換模型
│ └─ GoogleEventRead.from_raw_model(raw_event)
│ ← 時區正規化至 UTC+8
│ ← 重複規則轉人類可讀
│
└─ 返回 Result[AllCalendarEventsResult]
模型層轉換(三層 + 壓縮)¶
| 層級 | 模型 | 欄位樣式 | 用途 |
|---|---|---|---|
| Raw (L1) | GoogleEventRaw | camelCase,與 API 100% 對應 | API 回應一一對應 |
| Read (L2) | GoogleEventRead | snake_case,時區 UTC+8,摘要欄位 | LLM 讀取 |
| TOON (Compress) | GoogleEventToon | 極致壓縮 ~83.7% token 節省 | LLM 推理輸入 |
| Write (L3) | GoogleEventWrite | 精簡欄位 | 寫回 API(未來) |
3.3 google_tasks/ - 任務清單¶
時間過濾三模式¶
Google Tasks 沒有原生時間範圍查詢,需手動過濾。支援三種 time_mode:
time_mode = "any" # 所有任務(忽略時間範圍)
time_mode = "range" # 只返回「completed 或 due 在 [start, end]」的任務
# 過濾邏輯:
# - task.due >= start_time AND task.due <= end_time
# - OR task.completed >= start_time AND task.completed <= end_time
time_mode = "no_time" # 只返回「無截止日期」的任務
# 過濾邏輯: task.due is None
Tasks 流程(相比 Calendar 更簡單)¶
async_get_all_tasks(request, ...)
│
├─ Step 1: 列舉所有 TaskList
│ └─ list_tasklists_async()
│ → GoogleTaskListRaw[]
│
├─ Step 2: 為每個 TaskList 建構 ListTasksRequest
│ └─ ListTasksRequest(
│ tasklist="@default",
│ showCompleted=true,
│ time_mode="range" ← 自訂過濾
│ )
│
├─ Step 3: Batch 執行
│ └─ batch_execute_async(requests)
│ → List[Result[GoogleTaskRaw]]
│
├─ Step 4: 轉換 + 客戶端過濾
│ └─ GoogleTaskRead.from_raw_model(raw_task)
│ ← 按 time_mode 過濾
│
└─ 返回 Result[AllTaskResult]
3.4 moodle/ - 課程爬蟲¶
登入流程(兩層備援)¶
scrape_moodle_events(request, ...)
│
├─ Step 1: 嘗試 OIDC API 登入
│ └─ api_login()
│ ├─ GET /auth/oidc/ → HTML 表單
│ ├─ POST account+password 到 SSO (ssoam2.ntust.edu.tw)
│ ├─ Follow redirect 回 Moodle
│ └─ 抽取 MoodleSession cookie
│
├─ [成功] → 使用 Cookies 抓取事件 ✓
│
└─ [失敗] → Fallback 到 Selenium
└─ loop.run_in_executor()
├─ 啟動瀏覽器
├─ 登入 (同上述過程)
├─ 抽取 Cookies
└─ [失敗] → MoodleResult(success=False)
爬蟲特性¶
# Moodle 的獨特點
1. 沒有原生 API(Web 爬蟲)
2. 無 Batch 支援(日期需併發請求)
3. 超時設定:預設 60 秒
MoodleEventRead 欄位:
- id (內部序號)
- summary (課程名稱)
- start / end (datetime, UTC+8)
- course_id (課程 ID)
- color (顏色代碼)
🔍 深度探究: 程式碼實作、OIDC 登入細節、快取策略、優化方案 → 詳精讀 MOODLE_DEEP_DIVE.md
L4: Code(程式碼參考)¶
L4.1 檔案位置速查¶
src/time_compass/integrations/
common/ [共通層]
├─ exceptions.py → GoogleError 定義
├─ google_api_dispatcher.py → batch_execute_async()
├─ http_batch_tool.py → Multipart 工具
└─ models.py → BaseGoogleRequest
google_calendar/
├─ async_core.py → async_get_all_events() 入口
├─ api_client_async.py → list_events_async() generator
├─ fields_presets.py → 欄位優化
├─ rrule_utils.py → 重複規則轉換
└─ models/
├─ models_raw.py → GoogleEventRaw (Layer 1)
├─ models_read.py → GoogleEventRead (Layer 2)
├─ models_toon.py → GoogleEventToon (壓縮)
└─ models_request.py → ListEventsRequest
google_tasks/
├─ async_core.py → async_get_all_tasks() 入口
├─ api_client_async.py → list_tasks_async() generator
└─ models/
├─ models_raw.py → GoogleTaskRaw
├─ models_read.py → GoogleTaskRead
├─ models_toon.py → GoogleTaskToon
└─ models_request.py → ListTasksRequest
moodle/
├─ async_core.py → scrape_moodle_events() 入口
├─ scraper.py → MoodleScraper (Selenium)
├─ config.py → 登入 URL、選擇器
└─ models/
├─ models_raw.py → RawCalendarResponseItem
└─ models_read.py → MoodleEventRead
context_requests.py [驅動配置]
└─ ContextFetchRequest (請求模型)
context_trace.py [追蹤/除錯]
└─ ContextFetchTrace (執行日誌)
get_all_information_from_api.py [頂級協調器]
└─ async_get_all_information_from_api() 並行執行三大模組
L4.2 常見任務快速實作¶
我想執行單一 API 查詢¶
from time_compass.integrations.google_calendar.async_core import async_get_all_events
from time_compass.integrations.context_requests import GoogleAllCalendarRequest
# 建構請求
request = GoogleAllCalendarRequest(
enabled=True,
events_request_template=GoogleCalendarEventsTemplateRequest(
start_time="2026-02-01",
end_time="2026-03-31",
single_events=False,
)
)
# 執行
result = await async_get_all_events(
client=http_client,
request=request,
google_auth_provider=auth,
)
# 檢查結果
if is_ok(result):
all_events = result.unwrap()
for cal_id, events in all_events.items_by_group_id.items():
print(f"Calendar {cal_id}: {len(events)} events")
else:
error = result.unwrap_err()
print(f"Error: {error}")
我想使用 Batch API(進階)¶
from time_compass.integrations.common.google_api_dispatcher import batch_execute_async
from time_compass.integrations.google_calendar.models.models_request import ListEventsRequest
# 構造多個請求
requests = [
ListEventsRequest(calendar_id="primary", ...),
ListEventsRequest(calendar_id="secondary", ...),
ListEventsRequest(calendar_id="work@example.com", ...),
]
# 執行 Batch(一次 HTTP call)
result = await batch_execute_async(
client=client,
credentials=credentials,
requests=requests,
endpoint="https://www.googleapis.com/batch/calendar/v3",
raw=False, # 返回 List[Result[GoogleEventRaw]]
)
# 檢查每個結果
for i, res in enumerate(result):
if is_ok(res):
raw_events = res.unwrap()
# 轉換為 Read 模型
read_events = [
GoogleEventRead.from_raw_model(e) for e in raw_events
]
else:
error = res.unwrap_err()
print(f"Request {i} failed: {error}")
我想轉換模型(Raw → Read → TOON)¶
from time_compass.integrations.google_calendar.models import (
GoogleEventRaw,
GoogleEventRead,
GoogleEventToon,
)
from toon_format import encode
# Raw 來自 API(自動驗證)
raw_event: GoogleEventRaw = ...
# → Read(應用層)
read_event = GoogleEventRead.from_raw_model(
raw_event,
calendar_id="primary",
calendar_summary="My Calendar"
)
# → TOON(壓縮)
toon_dict = read_event.model_dump()
toon_str = encode(toon_dict) # "ev|123|Meeting|2026-02-15T14:00:00Z|..."
我想自訂錯誤處理¶
from time_compass.integrations.common.exceptions import (
GoogleAuthError,
GoogleRateLimitError,
GoogleNetworkError,
)
result = await async_get_all_information_from_api(
client=client,
request=request,
google_auth_provider=auth,
)
if is_err(result.google_calendar):
error = result.google_calendar.unwrap_err()
if isinstance(error, GoogleAuthError):
print("Token 過期,請重新授權")
# 觸發 OAuth 流程
elif isinstance(error, GoogleRateLimitError):
print("API 限速,指數退避...")
await asyncio.sleep(2 ** retry_count)
elif isinstance(error, GoogleNetworkError):
print("網路異常,返回快取結果")
return cached_data
else:
print(f"其他異常: {error}")
L4.3 狀態碼 → 例外映射¶
| HTTP Code | Exception | 特徵 |
|---|---|---|
| 400 | GoogleAPIError | 格式不正確 |
| 401 | GoogleAuthError | Token 過期 |
| 403 | GoogleAuthError | 權限不足 |
| 404 | GoogleNotFoundError | 資源不存在 |
| 429 | GoogleRateLimitError | 超過配額 |
| 500+ | GoogleAPIError | 伺服器錯誤 |
| timeout | GoogleNetworkError | 網路超時 |
| DNS fail | GoogleNetworkError | DNS 解析失敗 |
| JSON parse | GoogleParseError | 回應格式異常 |
快速查詢表¶
型別導入¶
# 要求配置
from time_compass.integrations.context_requests import (
ContextFetchRequest,
GoogleAllTasksRequest,
GoogleAllCalendarRequest,
MoodleFetchRequest,
)
# 結果物件
from time_compass.integrations.google_calendar.models import (
AllCalendarEventsResult,
)
from time_compass.integrations.google_tasks.models import (
AllTaskResult,
)
# 模型(Raw/Read)
from time_compass.integrations.google_calendar.models import (
GoogleEventRaw, GoogleEventRead,
)
from time_compass.integrations.google_tasks.models import (
GoogleTaskRaw, GoogleTaskRead,
)
# 例外
from time_compass.integrations.common.exceptions import (
GoogleError, GoogleAuthError, GoogleRateLimitError,
)
# 工具
from time_compass.utils.result import Result, Ok, Err, is_ok, is_err
常見問題排查¶
| 症狀 | 可能原因 | 檢查項目 |
|---|---|---|
GoogleAuthError: 401 | Token 過期 | 檢查 credentials.valid |
GoogleRateLimitError: 429 | 超過配額 | 檢查 API quotas,考慮延遲 |
GoogleNetworkError: timeout | 網路慢 | 增加 timeout,檢查網路連線 |
| Batch 全部失敗 | 一般是網路問題 | 檢查 VPN、DNS、endpoint URL |
| Moodle 超時 | 爬蟲卡住 | 延長 timeout_seconds,檢查網頁加載 |
| 模型欄位為 null | 原始 API 回應缺欄位 | 檢查 API 回應,調整欄位預設值 |
文檔導航¶
- 本頁 (C4 Model) ← 你在這裡(涵蓋全景)
- DDD 多層模型架構 — 理解 Raw/Read/TOON 層的設計哲學
- Railway-Oriented Programming — 理解
Result[T]與GoogleError的錯誤策略 - 系統全景 — Integration Layer 在整體系統中的位置
- ADR-0010 文檔架構決策 — 為什麼 Integration 文檔採用 C4 Model
Integration Layer C4 Model 完成 ✅ 2026-03-02