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