跳轉到

Integration 層 - C4 完整視圖

日期: 2026-03-02
目的: 用 C4 Model 統一呈現 Integration Layer 架構,一篇文件涵蓋全景
舊檔案: 整合自 INTEGRATION_LAYER_ANALYSIS.md、QUICK_REFERENCE.md、VISUAL_SUPPLEMENT.md


🔗 交叉引用

相關架構設計

快速導航


文檔結構概覽

本文按 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. 沒有原生 APIWeb 爬蟲
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 回應,調整欄位預設值

文檔導航


Integration Layer C4 Model 完成 ✅ 2026-03-02