# Detective - 編碼標準與架構指南
## 1. 核心架構原則
本專案採用 **領域驅動設計(Domain-Driven Design, DDD)** 結合 **六角架構(Hexagonal Architecture,又稱 Ports & Adapters)** 及 **命令查詢職責分離(CQRS, Command Query Responsibility Segregation)**。
- **依賴規則**:依賴必須指向**內層**。
- 外層(Adapters)依賴內層(Ports, Application, Domain)。
- 內層(Domain)**絕對不可**依賴外層。
- **依賴注入(Dependency Injection, DI)**:專案使用 `tsyringe` 進行依賴管理。
- 每個模組(限界上下文,Bounded Context)必須包含一個 `di.ts` 檔案作為 **組合根(Composition Root)**。
- 它將 **被驅動適配器(Driven Adapters)**(實作)綁定到 **被驅動端口(Driven Ports)**(介面)。
- **核心隔離**:業務邏輯(Domain)必須是純 TypeScript,不依賴任何框架程式碼。
- **就近配置(Colocation)**:相關檔案(DTOs, Mappers, Persistence DTOs)應盡可能放置在其使用處附近(功能資料夾,Feature Folders)。
- **混合快照模式(Hybrid Snapshot Pattern)**:區分不可變的核心上下文與可變的執行邏輯。
- **核心上下文(Core Context)**:不可變。
- **格式設定(Format Settings)**:快照鎖定。
- **執行邏輯(Execution Logic)**:可變(執行期間需要記錄快照)。
### 1.1 限界上下文概覽(Bounded Contexts Overview)
#### 1.1.1 Libs 與 Apps 的定義
> **`libs/` 存放「業務能力 (Capability)」**:我會做什麼(純 TypeScript 邏輯,不依賴框架)
> **`apps/` 存放「部署進入點 (Deployment Entry)」**:我在哪裡運行、如何被呼叫(含 Firebase Functions、HTTP Controller、DI 配置)
**Libs 層的特點**:
- 純 TypeScript,不 import `firebase-functions`、`firebase-admin`、`express` 等框架
- 只定義 Port(介面),如 `UserRepositoryPort.findByEmail()`
- 可單獨測試,用假的 Adapter 即可跑單元測試
**Apps 層的特點**:
- 實作 Adapter,如 `FirestoreUserAdapter implements UserRepositoryPort`
- 組裝 DI,透過 `di.ts` 把 Adapter 注入到 Port
- 處理 HTTP/Event,接收 Request、解析參數、回傳 Response
- 獨立部署單位,每個 App 可獨立部署
#### 1.1.2 🔑 核心決策:什麼該是 Lib?什麼該是 App?
| 問題 | 如果答案是「是」 | 如果答案是「否」 |
| :---------------------------------------- | :--------------- | :--------------- |
| **這個 BC 的型別會被其他 BC 引用嗎?** | 必須是 Lib | 可以只放 App 內 |
| **這個 BC 需要被外部呼叫 (HTTP/Event)?** | 必須有 App | 不需要 App |
| **這個 BC 的邏輯會被多處重用嗎?** | 應該是 Lib | 可以只放 App 內 |
**決策流程**:
```text
某個 Bounded Context...
Q1: 有沒有「其他 BC」需要 import 它的型別?
│
├── 是 ──→ 必須抽成獨立 Lib (libs/xxx)
│ 因為型別需要被共享
│
└── 否 ──→ Q2: 它有沒有需要對外暴露的 API/Event?
│
├── 是 ──→ 只需要 App (apps/xxx)
│ 領域邏輯放在 apps/xxx/domain/ 內
│
└── 否 ──→ 可能根本不需要獨立存在
```
#### 1.1.3 Bounded Contexts 與 Monorepo 對應
| BC 名稱 | 類型 | 核心職責 | 需要獨立 Lib? | 對應 App | 理由 |
| :------------- | :--------- | :----------------- | :------------- | :---------------- | :----------------------------------------- |
| **Identity** | Generic | 使用者驗證、RBAC | ✅ 是 | `apps/iam` | 所有 BC 都需要 User 型別 |
| **Brand** | Supporting | 品牌同步、資產管理 | ✅ 是 | `apps/brand` | Monitoring/Discovery 需要 Brand 型別 |
| **Monitoring** | Core | 任務/階段生命週期 | ✅ 是 | `apps/monitoring` | Crawling/Discovery 需要 Task/Phase 型別 |
| **Discovery** | Core | 發現結果管理、審查 | ✅ 是 | `apps/discovery` | AI Triage 需要 Finding 型別 |
| **Crawling** | Core | 爬蟲邏輯、ETL | ❌ 否 | `apps/crawling` | 無其他 BC 需要其型別(領域邏輯放 apps 內) |
| **AI Triage** | Core | AI 策略、LLM 呼叫 | ❌ 否 | `apps/ai-triage` | 無其他 BC 需要其型別(領域邏輯放 apps 內) |
**Crawling 和 AI Triage 為什麼不需要獨立 Lib?**
這兩個 BC 像是「工人」:
- **Crawling**:輸入是 `Task`/`Phase`(來自 `libs/monitoring`),輸出是原始爬蟲資料 → 寫入 Firestore → 觸發 Discovery 處理。**沒有其他 BC 需要 import Crawling 的內部型別。**
- **AI Triage**:輸入是 `Finding`(來自 `libs/discovery`),輸出是風險分類 → 更新 Finding 的 status/risk。**沒有其他 BC 需要 import AI Triage 的策略模型。**
它們的「輸入」和「輸出」都是別人定義的型別,自己的內部模型不需要對外暴露。
#### 1.1.4 Context Mapping 分析
```text
┌─────────────────┐
│ Identity │
│ (Generic) │
└────────┬────────┘
│ [Open Host Service]
│ 所有 BC 需要知道「當前使用者是誰」
┌──────────────┼──────────────┐
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────────┐
│ Brand │ │Monitoring│ │ Discovery │
│(Support) │ │ (Core) │ │ (Core) │
└────┬─────┘ └────┬─────┘ └───────┬──────┘
│ │ │
[Customer-Supplier] [Customer-Supplier] [Customer-Supplier]
│ │ │
└──────┬───────┘ │
│ │
▼ ▼
┌──────────┐ ┌──────────┐
│ Crawling │──────────────│AI Triage │
│ (Core) │ [產生發現] │ (Core) │
└──────────┘ └──────────┘
```
**Context Mapping 關係類型**:
| 上游 BC | 下游 BC | 關係類型 | 說明 |
| :------------- | :-------------------- | :----------------------------- | :---------------------------------------------- |
| **Identity** | 所有 BC | Open Host Service / Conformist | Identity 提供認證協議,其他 BC 遵從其 User 模型 |
| **Brand** | Monitoring, Discovery | Customer-Supplier | Brand 提供品牌資料,監控任務綁定 Brand |
| **Monitoring** | Crawling | Customer-Supplier | Monitoring 定義 Task/Phase,Crawling 執行 |
| **Monitoring** | Discovery | Customer-Supplier | Discovery 的 Finding 歸屬於 Task/Phase |
| **Crawling** | Discovery | Customer-Supplier | Crawling 產生原始資料,Discovery 接收為 Finding |
| **Discovery** | AI Triage | Customer-Supplier | AI Triage 讀取 Finding 進行分類判斷 |
#### 1.1.5 Libs 依賴圖
```text
libs/shared
│
┌──────────────┼──────────────┐
│ │ │
▼ ▼ ▼
libs/identity libs/brand libs/monitoring
│ │ │
│ └──────┬───────┘
│ │
│ ▼
│ libs/discovery
│ │
└──────────┬──────────┘
│
▼
(apps/ 引用)
```
**依賴規則**:
- `libs/shared` ← 所有 libs 都依賴(共用原始型別)
- `libs/identity` ← 所有 libs 都依賴(User 型別)
- `libs/brand` ← `libs/monitoring`, `libs/discovery` 依賴
- `libs/monitoring` ← `libs/discovery` 依賴
- **libs 之間不能循環依賴**
#### 1.1.6 最終 Monorepo 結構總覽
- **5 個 Libs**:`shared`, `identity`, `brand`, `monitoring`, `discovery`
- **6 個 Apps**:`iam`, `brand`, `monitoring`, `discovery`, `crawling`, `ai-triage`
> **📝 Billing 定位說明**:
> Billing **不設為獨立 Bounded Context**。透過 `executionLogs` 欄位 (`durationMs`, `apiCallsConsumed`) 預留計費統計資料。獨立 Billing BC 可於未來視需求評估。
### 1.2 資料可變性規則(Data Mutability Rules)
| 層級 | 可變屬性 | 鎖定屬性 (不可變) | 理由 |
| :------- | :----------------------------------------------------- | :------------------------------------------------ | :--------------------------------------------- |
| **任務** | 任務名稱、排程表達式、客戶經理 (ownerId) | **Brand**、**Platform Type**、**Protected Asset** | 修改核心上下文將破壞階段與發現結果的參考完整性 |
| **階段** | 階段名稱、開始/結束日期、監控範圍、AI 策略、強調關鍵字 | **參考貨幣**、**預設翻譯語言** | 鎖定格式設定確保聚合統計在單一階段內數學正確 |
**設計原則**:「編輯經常變動的 (策略, 範圍);鎖定定義結構的 (Brand, Platform)。」
## 2. 戰術設計本體論(Tactical Design Ontology)
我們使用以下標準詞彙和檔案後綴來定義系統元件:
### 2.1 領域層 - 核心業務
**職責**:定義業務規則、狀態驗證和不變條件。
| 元件 | 定義 | 檔案後綴 | 範例 |
| :--------------------------- | :--------------------------------------------------- | :-------------- | :----------------- |
| **聚合根(Aggregate Root)** | 一致性的守護者。具有 ID。保護內部狀態。 | `.aggregate.ts` | `monitoring-task` |
| **實體(Entity)** | 具有 ID 且屬於聚合的物件。 | `.entity.ts` | `phase` |
| **值物件(Value Object)** | 由屬性定義,無 ID。必須是**不可變的(Immutable)**。 | `.vo.ts` | `monitoring-scope` |
| **領域事件(Domain Event)** | 表示已經發生的業務事實。 | `.event.ts` | `task-created` |
### 2.2 應用層 - 流程編排
**職責**:編排領域層並處理功能流程。
| 元件 | 定義 | 檔案後綴 | 範例 |
| :------------------ | :--------------------------------- | :------------ | :---------------------------- |
| **命令(Command)** | 處理「寫入/狀態變更」意圖。 | `.command.ts` | `create-task` |
| **查詢(Query)** | 處理「讀取」意圖(CQS)。 | `.query.ts` | `get-task-list` |
| **策略(Policy)** | 監聽領域事件並觸發副作用(Saga)。 | `.policy.ts` | `scheduled-scan-orchestrator` |
### 2.3 端口層 - 介面契約
**職責**:定義應用層的「進入」和「退出」介面。
| 元件 | 定義 | 檔案後綴 | 範例 |
| :---------------------------- | :------------------------------------ | :----------------- | :---------------- |
| **驅動端口(Driving Port)** | 定義命令(用例)的簽名介面。 | `.command.port.ts` | `create-task` |
| **被驅動端口(Driven Port)** | 定義外部依賴介面(Repository, RPC)。 | `.port.ts` | `task-repository` |
### 2.4 適配器層 - 技術實作
**職責**:實作介面、處理 HTTP、資料庫存取和 DI 配置。
| 元件 | 定義 | 檔案後綴 | 範例 |
| :--------------------------------- | :------------------------------ | :------------------ | :--------------- |
| **控制器(Controller)** | 驅動適配器。處理 HTTP 請求。 | `.controller.ts` | `task` |
| **被驅動適配器(Driven Adapter)** | 實作被驅動端口。 | `.adapter.ts` | `firestore-task` |
| **請求 DTO(Request DTO)** | API 輸入資料傳輸物件。 | `.req.dto.ts` | `create-task` |
| **回應 DTO(Response DTO)** | API 輸出資料傳輸物件。 | `.res.dto.ts` | `task-list` |
| **持久化 DTO(Persistence DTO)** | 資料庫層的資料結構。 | `.firestore.dto.ts` | `task` |
| **DI 配置(DI Config)** | `tsyringe` 容器註冊(組合根)。 | `di.ts` | `di` |
---
## 3. 專案目錄結構
### 3.0 完整 Monorepo 結構總覽
基於 [1.1 限界上下文概覽](#11-限界上下文概覽bounded-contexts-overview) 的分析,Monorepo 結構如下:
```text
detective-v3/
├── libs/ # 共用領域邏輯(被其他 BC 引用的型別)
│ ├── shared/ # 共用原始型別
│ │ └── src/
│ │ ├── types/
│ │ │ └── primitives.ts # MicroTimestamp, UUID, etc.
│ │ └── index.ts
│ │
│ ├── identity/ # Identity BC 領域(所有 BC 需要 User 型別)
│ │ └── src/
│ │ ├── domain/
│ │ ├── application/
│ │ ├── ports/
│ │ └── index.ts
│ │
│ ├── brand/ # Brand BC 領域(Monitoring/Discovery 需要 Brand)
│ ├── monitoring/ # Monitoring BC 領域(Crawling/Discovery 需要 Task/Phase)
│ └── discovery/ # Discovery BC 領域(AI Triage 需要 Finding)
│
├── apps/ # 部署進入點
│ ├── iam/ # Identity API(使用 libs/identity)
│ │ └── src/
│ │ ├── adapters/
│ │ │ ├── driving/http/ # HTTP Controllers
│ │ │ └── driven/firestore/ # Firestore Implementations
│ │ ├── di.ts # DI 組裝
│ │ └── index.ts # Firebase Functions 進入點
│ │
│ ├── brand/ # Brand API(使用 libs/brand)
│ ├── monitoring/ # Monitoring API(使用 libs/monitoring)
│ ├── discovery/ # Discovery API(使用 libs/discovery)
│ │
│ ├── crawling/ # Crawling Workers(無獨立 Lib,領域邏輯放內部)
│ │ └── src/
│ │ ├── domain/ # 內部領域,不共享
│ │ ├── application/
│ │ ├── ports/
│ │ ├── adapters/
│ │ ├── di.ts
│ │ └── index.ts
│ │
│ └── ai-triage/ # AI Triage Workers(無獨立 Lib,領域邏輯放內部)
│ └── src/
│ ├── domain/ # 內部領域,不共享
│ ├── application/
│ ├── ports/
│ ├── adapters/
│ ├── di.ts
│ └── index.ts
│
├── firebase.json
├── pnpm-workspace.yaml
└── tsconfig.base.json
```
> **📌 注意**:`apps/crawling` 和 `apps/ai-triage` 沒有對應的 `libs/`,因為它們的領域模型不被其他 BC 引用。這兩個 App 的 `domain/`、`application/`、`ports/` 直接放在 `apps/` 內部。
### 3.1 身份上下文(Identity Context)(`libs/identity`)
**範疇**:身份驗證、使用者帳號、RBAC。
#### 驅動適配器 - HTTP
**身份驗證(Auth)**
| 位置 | 後綴 | 名稱 | 業務理由 |
| :---------------------------- | :--------------- | :----------- | :-------------------------------------- |
| `adapters/driving/http/auth/` | `.controller.ts` | `auth` | 進入點:接收登入/2FA 的 HTTP 請求。 |
| `adapters/driving/http/auth/` | `.req.dto.ts` | `login` | 輸入:驗證登入憑證(email、password)。 |
| `adapters/driving/http/auth/` | `.req.dto.ts` | `verify-2fa` | 輸入:驗證 2FA 驗證碼。 |
**使用者(Users)**
| 位置 | 後綴 | 名稱 | 業務理由 |
| :----------------------------- | :--------------- | :------------ | :----------------------------- |
| `adapters/driving/http/users/` | `.controller.ts` | `user` | 進入點:處理使用者管理操作。 |
| `adapters/driving/http/users/` | `.req.dto.ts` | `update-user` | 輸入:使用者個人資料更新資料。 |
| `adapters/driving/http/users/` | `.res.dto.ts` | `user-list` | 輸出:返回分頁的使用者列表。 |
**角色(Roles)**
| 位置 | 後綴 | 名稱 | 業務理由 |
| :----------------------------- | :--------------- | :------------ | :--------------------------- |
| `adapters/driving/http/roles/` | `.controller.ts` | `role` | 進入點:管理 RBAC 角色配置。 |
| `adapters/driving/http/roles/` | `.req.dto.ts` | `create-role` | 輸入:包含權限樹的角色建立。 |
#### 被驅動適配器 - Firestore
**使用者(Users)**
| 位置 | 後綴 | 名稱 | 業務理由 |
| :--------------------------------- | :------------------ | :--------------- | :------------------------------------- |
| `adapters/driven/firestore/users/` | `.adapter.ts` | `firestore-user` | 持久化:使用者的 Repository 實作。 |
| `adapters/driven/firestore/users/` | `.firestore.dto.ts` | `user` | 資料庫結構:Firestore 使用者文件結構。 |
**角色(Roles)**
| 位置 | 後綴 | 名稱 | 業務理由 |
| :--------------------------------- | :------------------ | :--------------- | :----------------------------------- |
| `adapters/driven/firestore/roles/` | `.adapter.ts` | `firestore-role` | 持久化:儲存 RBAC 角色定義。 |
| `adapters/driven/firestore/roles/` | `.firestore.dto.ts` | `role` | 資料庫結構:Firestore 角色文件結構。 |
**稽核日誌(Audit Logs)**
| 位置 | 後綴 | 名稱 | 業務理由 |
| :-------------------------------------- | :------------ | :-------------------- | :--------------------------------------- |
| `adapters/driven/firestore/audit-logs/` | `.adapter.ts` | `firestore-audit-log` | 持久化:記錄所有使用者操作以符合合規性。 |
#### 被驅動適配器 - 外部身份提供者(External IDP)
**LDAP**
| 位置 | 後綴 | 名稱 | 業務理由 |
| :------------------------------ | :------------ | :---------- | :-------------------------------- |
| `adapters/driven/external-idp/` | `.adapter.ts` | `ldap-sync` | 整合:從外部 LDAP/AD 同步使用者。 |
#### 驅動端口
| 位置 | 後綴 | 名稱 | 業務理由 |
| :--------------- | :----------------- | :------------------- | :------------------------- |
| `ports/driving/` | `.command.port.ts` | `login` | 介面:登入命令簽名。 |
| `ports/driving/` | `.command.port.ts` | `verify-2fa` | 介面:2FA 驗證命令簽名。 |
| `ports/driving/` | `.command.port.ts` | `sync-user-accounts` | 介面:使用者同步命令簽名。 |
| `ports/driving/` | `.command.port.ts` | `update-user` | 介面:使用者更新命令簽名。 |
| `ports/driving/` | `.command.port.ts` | `create-role` | 介面:角色建立命令簽名。 |
#### 被驅動端口
| 位置 | 後綴 | 名稱 | 業務理由 |
| `:--- | :--- | :--- | :--- |
| `ports/driven/`|`.port.ts`|`user-repository`| 介面:使用者持久化契約。 |
|`ports/driven/`|`.port.ts`|`role-repository`| 介面:角色持久化契約。 |
|`ports/driven/`|`.port.ts`|`audit-log-repository`| 介面:稽核日誌持久化契約。 |
|`ports/driven/`|`.port.ts`|`external-identity-provider` | 介面:外部身份提供者整合契約。 |
#### 應用層
**命令(Commands)**
| 位置 | 後綴 | 名稱 | 業務理由 |
| :---------------------- | :------------ | :------------------- | :----------------------------------- |
| `application/commands/` | `.command.ts` | `login` | 流程:驗證使用者,若啟用則觸發 2FA。 |
| `application/commands/` | `.command.ts` | `verify-2fa` | 流程:驗證 2FA 代碼。 |
| `application/commands/` | `.command.ts` | `sync-user-accounts` | 整合:從 Domainarium 同步使用者。 |
| `application/commands/` | `.command.ts` | `update-user` | 流程:更新使用者個人資料和權限。 |
| `application/commands/` | `.command.ts` | `create-role` | 流程:建立新的 RBAC 角色。 |
**查詢(Queries)**
| 位置 | 後綴 | 名稱 | 業務理由 |
| :--------------------- | :---------- | :----------------------- | :------------------------- |
| `application/queries/` | `.query.ts` | `get-user-list` | 讀取:取得分頁使用者列表。 |
| `application/queries/` | `.query.ts` | `get-risk-category-list` | 讀取:返回可用的風險類別。 |
| `application/queries/` | `.query.ts` | `get-system-audit-log` | 讀取:檢索稽核追蹤。 |
#### 領域層
**聚合(Aggregates)**
| 位置 | 後綴 | 名稱 | 業務理由 |
| :------------------- | :-------------- | :------------- | :---------------------------------------------- |
| `domain/aggregates/` | `.aggregate.ts` | `user-account` | 核心規則:管理 Active/Inactive 狀態、鎖定邏輯。 |
| `domain/aggregates/` | `.aggregate.ts` | `role` | 核心規則:定義 RBAC 權限樹。 |
**事件(Events)**
| 位置 | 後綴 | 名稱 | 業務理由 |
| :--------------- | :---------- | :--------------------- | :----------------------------- |
| `domain/events/` | `.event.ts` | `user-logged-in` | 事實:記錄成功登入。 |
| `domain/events/` | `.event.ts` | `user-locked` | 事實:因失敗嘗試次數鎖定帳號。 |
| `domain/events/` | `.event.ts` | `two-factor-verified` | 事實:2FA 驗證完成。 |
| `domain/events/` | `.event.ts` | `user-accounts-synced` | 事實:外部使用者同步完成。 |
| `domain/events/` | `.event.ts` | `user-updated` | 事實:使用者個人資料已變更。 |
| `domain/events/` | `.event.ts` | `role-updated` | 事實:角色權限已變更。 |
#### 基礎設施層
| 位置 | 後綴 | 名稱 | 業務理由 |
| :--- | :--- | :--- | :------- |
---
### 3.2 品牌上下文(Brand Context)(`libs/brand`)
**範疇**:品牌管理、受保護資產管理、白牌設定、Domainarium 同步。
#### 驅動適配器 - HTTP (`apps/brand`)
**品牌(Brands)**
| 位置 | 後綴 | 名稱 | 業務理由 |
| :------------------------------ | :--------------- | :------------------- | :--------------------------- |
| `adapters/driving/http/brands/` | `.controller.ts` | `brand` | 進入點:處理品牌 CRUD 操作。 |
| `adapters/driving/http/brands/` | `.req.dto.ts` | `sync-brands` | 輸入:批次同步所有品牌。 |
| `adapters/driving/http/brands/` | `.req.dto.ts` | `update-brand` | 輸入:更新品牌基本資訊。 |
| `adapters/driving/http/brands/` | `.req.dto.ts` | `update-white-label` | 輸入:更新白牌設定。 |
| `adapters/driving/http/brands/` | `.res.dto.ts` | `brand-list` | 輸出:返回分頁的品牌列表。 |
**受保護資產(Assets)**
| 位置 | 後綴 | 名稱 | 業務理由 |
| :------------------------------ | :--------------- | :------------- | :--------------------------- |
| `adapters/driving/http/assets/` | `.controller.ts` | `asset` | 進入點:處理資產 CRUD 操作。 |
| `adapters/driving/http/assets/` | `.req.dto.ts` | `create-asset` | 輸入:建立受保護資產。 |
| `adapters/driving/http/assets/` | `.req.dto.ts` | `update-asset` | 輸入:更新資產資訊。 |
| `adapters/driving/http/assets/` | `.res.dto.ts` | `asset-list` | 輸出:返回分頁的資產列表。 |
#### 被驅動適配器 - Firestore (`apps/brand`)
| 位置 | 後綴 | 名稱 | 業務理由 |
| :---------------------------------- | :------------------ | :---------------- | :------------------------------- |
| `adapters/driven/firestore/brands/` | `.adapter.ts` | `firestore-brand` | 持久化:品牌的 Repository 實作。 |
| `adapters/driven/firestore/brands/` | `.firestore.dto.ts` | `brand` | 資料庫結構:Firestore 品牌文件。 |
| `adapters/driven/firestore/assets/` | `.adapter.ts` | `firestore-asset` | 持久化:資產的 Repository 實作。 |
| `adapters/driven/firestore/assets/` | `.firestore.dto.ts` | `asset` | 資料庫結構:Firestore 資產文件。 |
#### 被驅動適配器 - Domainarium (`apps/brand`)
| 位置 | 後綴 | 名稱 | 業務理由 |
| :----------------------------- | :------------ | :------------------ | :---------------------------------- |
| `adapters/driven/domainarium/` | `.adapter.ts` | `domainarium-brand` | 整合:從 Domainarium 同步品牌資料。 |
#### 驅動端口 (`libs/brand`)
| 位置 | 後綴 | 名稱 | 業務理由 |
| :--------------- | :----------------- | :---------------------- | :--------------------------- |
| `ports/driving/` | `.command.port.ts` | `sync-brands` | 介面:批次同步品牌命令簽名。 |
| `ports/driving/` | `.command.port.ts` | `sync-brand` | 介面:單一品牌同步命令簽名。 |
| `ports/driving/` | `.command.port.ts` | `update-brand` | 介面:更新品牌命令簽名。 |
| `ports/driving/` | `.command.port.ts` | `configure-white-label` | 介面:設定白牌命令簽名。 |
| `ports/driving/` | `.command.port.ts` | `create-asset` | 介面:建立資產命令簽名。 |
| `ports/driving/` | `.command.port.ts` | `update-asset` | 介面:更新資產命令簽名。 |
| `ports/driving/` | `.command.port.ts` | `delete-asset` | 介面:刪除資產命令簽名。 |
#### 被驅動端口 (`libs/brand`)
| 位置 | 後綴 | 名稱 | 業務理由 |
| :-------------- | :--------- | :------------------------ | :------------------------------------- |
| `ports/driven/` | `.port.ts` | `brand-repository` | 介面:品牌持久化契約。 |
| `ports/driven/` | `.port.ts` | `asset-repository` | 介面:資產持久化契約。 |
| `ports/driven/` | `.port.ts` | `external-brand-provider` | 介面:外部品牌資料來源 (Domainarium)。 |
#### 應用層 (`libs/brand`)
**命令(Commands)**
| 位置 | 後綴 | 名稱 | 業務理由 |
| :---------------------- | :------------ | :---------------------- | :----------------------------------- |
| `application/commands/` | `.command.ts` | `sync-brands` | 流程:從 Domainarium 批次同步。 |
| `application/commands/` | `.command.ts` | `sync-brand` | 流程:同步單一品牌。 |
| `application/commands/` | `.command.ts` | `update-brand` | 流程:更新品牌基本資訊。 |
| `application/commands/` | `.command.ts` | `configure-white-label` | 流程:設定白牌外觀。 |
| `application/commands/` | `.command.ts` | `create-asset` | 流程:建立受保護資產。 |
| `application/commands/` | `.command.ts` | `update-asset` | 流程:更新資產資訊。 |
| `application/commands/` | `.command.ts` | `delete-asset` | 流程:刪除資產(含參考完整性檢查)。 |
**查詢(Queries)**
| 位置 | 後綴 | 名稱 | 業務理由 |
| :--------------------- | :---------- | :--------------- | :----------------------- |
| `application/queries/` | `.query.ts` | `get-brand-list` | 讀取:取得分頁品牌列表。 |
| `application/queries/` | `.query.ts` | `get-brand` | 讀取:取得單一品牌詳情。 |
| `application/queries/` | `.query.ts` | `get-asset-list` | 讀取:取得分頁資產列表。 |
| `application/queries/` | `.query.ts` | `get-asset` | 讀取:取得單一資產詳情。 |
#### 領域層 (`libs/brand`)
**聚合(Aggregates)**
| 位置 | 後綴 | 名稱 | 業務理由 |
| :------------------- | :-------------- | :---------------- | :--------------------------------- |
| `domain/aggregates/` | `.aggregate.ts` | `brand` | 核心規則:品牌狀態管理、白牌設定。 |
| `domain/aggregates/` | `.aggregate.ts` | `protected-asset` | 核心規則:資產與任務的參考完整性。 |
**事件(Events)**
| 位置 | 後綴 | 名稱 | 業務理由 |
| :--------------- | :---------- | :----------------------- | :------------------------------ |
| `domain/events/` | `.event.ts` | `brand-synced` | 事實:品牌從 Domainarium 同步。 |
| `domain/events/` | `.event.ts` | `brand-updated` | 事實:品牌資訊已更新。 |
| `domain/events/` | `.event.ts` | `white-label-configured` | 事實:白牌設定已變更。 |
| `domain/events/` | `.event.ts` | `asset-created` | 事實:新資產已建立。 |
| `domain/events/` | `.event.ts` | `asset-updated` | 事實:資產資訊已更新。 |
| `domain/events/` | `.event.ts` | `asset-deleted` | 事實:資產已刪除。 |
---
### 3.3 監控上下文(Monitoring Context)(`libs/monitoring`)
**範疇**:監控任務管理、階段管理、標籤管理、範圍設定、AI 策略設定。
#### 驅動適配器 - HTTP (`apps/monitoring`)
**任務(Tasks)**
| 位置 | 後綴 | 名稱 | 業務理由 |
| :----------------------------- | :--------------- | :--------------- | :--------------------------------- |
| `adapters/driving/http/tasks/` | `.controller.ts` | `task` | 進入點:處理任務 CRUD 與狀態變更。 |
| `adapters/driving/http/tasks/` | `.req.dto.ts` | `create-task` | 輸入:建立任務(含第一個階段)。 |
| `adapters/driving/http/tasks/` | `.req.dto.ts` | `configure-task` | 輸入:更新任務可變設定。 |
| `adapters/driving/http/tasks/` | `.res.dto.ts` | `task-list` | 輸出:返回分頁的任務列表。 |
| `adapters/driving/http/tasks/` | `.res.dto.ts` | `task-detail` | 輸出:任務詳情(含階段摘要)。 |
**階段(Phases)**
| 位置 | 後綴 | 名稱 | 業務理由 |
| :------------------------------ | :--------------- | :------------- | :------------------------------- |
| `adapters/driving/http/phases/` | `.controller.ts` | `phase` | 進入點:處理階段 CRUD 操作。 |
| `adapters/driving/http/phases/` | `.req.dto.ts` | `create-phase` | 輸入:建立新階段。 |
| `adapters/driving/http/phases/` | `.req.dto.ts` | `edit-phase` | 輸入:編輯階段可變邏輯。 |
| `adapters/driving/http/phases/` | `.res.dto.ts` | `phase-detail` | 輸出:階段詳情(含範圍與策略)。 |
**範圍(Scopes)**
| 位置 | 後綴 | 名稱 | 業務理由 |
| :------------------------------ | :--------------- | :-------------------- | :---------------------- |
| `adapters/driving/http/scopes/` | `.controller.ts` | `scope` | 進入點:處理範圍 CRUD。 |
| `adapters/driving/http/scopes/` | `.req.dto.ts` | `create-scope` | 輸入:新增監控範圍。 |
| `adapters/driving/http/scopes/` | `.req.dto.ts` | `configure-scan-tier` | 輸入:設定掃描層級。 |
**策略(Strategies)**
| 位置 | 後綴 | 名稱 | 業務理由 |
| :---------------------------------- | :--------------- | :----------------- | :-------------------------- |
| `adapters/driving/http/strategies/` | `.controller.ts` | `strategy` | 進入點:處理 AI 策略 CRUD。 |
| `adapters/driving/http/strategies/` | `.req.dto.ts` | `create-strategy` | 輸入:新增 AI 分流策略。 |
| `adapters/driving/http/strategies/` | `.req.dto.ts` | `reorder-strategy` | 輸入:重新排序策略優先級。 |
**標籤(Labels)**
| 位置 | 後綴 | 名稱 | 業務理由 |
| :------------------------------ | :--------------- | :------------- | :------------------------- |
| `adapters/driving/http/labels/` | `.controller.ts` | `label` | 進入點:處理標籤 CRUD。 |
| `adapters/driving/http/labels/` | `.req.dto.ts` | `create-label` | 輸入:新增標籤。 |
| `adapters/driving/http/labels/` | `.req.dto.ts` | `update-label` | 輸入:更新標籤名稱或顏色。 |
#### 被驅動適配器 - Firestore (`apps/monitoring`)
| 位置 | 後綴 | 名稱 | 業務理由 |
| :---------------------------------- | :------------------ | :---------------- | :------------------------------- |
| `adapters/driven/firestore/tasks/` | `.adapter.ts` | `firestore-task` | 持久化:任務的 Repository 實作。 |
| `adapters/driven/firestore/tasks/` | `.firestore.dto.ts` | `task` | 資料庫結構:Firestore 任務文件。 |
| `adapters/driven/firestore/phases/` | `.adapter.ts` | `firestore-phase` | 持久化:階段的 Repository 實作。 |
| `adapters/driven/firestore/phases/` | `.firestore.dto.ts` | `phase` | 資料庫結構:Firestore 階段文件。 |
| `adapters/driven/firestore/labels/` | `.adapter.ts` | `firestore-label` | 持久化:標籤的 Repository 實作。 |
#### 驅動端口 (`libs/monitoring`)
| 位置 | 後綴 | 名稱 | 業務理由 |
| :--------------- | :----------------- | :--------------- | :----------------------- |
| `ports/driving/` | `.command.port.ts` | `create-task` | 介面:建立任務命令簽名。 |
| `ports/driving/` | `.command.port.ts` | `configure-task` | 介面:設定任務命令簽名。 |
| `ports/driving/` | `.command.port.ts` | `pause-task` | 介面:暫停任務命令簽名。 |
| `ports/driving/` | `.command.port.ts` | `resume-task` | 介面:恢復任務命令簽名。 |
| `ports/driving/` | `.command.port.ts` | `archive-task` | 介面:封存任務命令簽名。 |
| `ports/driving/` | `.command.port.ts` | `delete-task` | 介面:刪除任務命令簽名。 |
| `ports/driving/` | `.command.port.ts` | `create-phase` | 介面:建立階段命令簽名。 |
| `ports/driving/` | `.command.port.ts` | `edit-phase` | 介面:編輯階段命令簽名。 |
| `ports/driving/` | `.command.port.ts` | `create-label` | 介面:建立標籤命令簽名。 |
| `ports/driving/` | `.command.port.ts` | `delete-label` | 介面:刪除標籤命令簽名。 |
#### 被驅動端口 (`libs/monitoring`)
| 位置 | 後綴 | 名稱 | 業務理由 |
| :-------------- | :--------- | :------------------ | :--------------------- |
| `ports/driven/` | `.port.ts` | `task-repository` | 介面:任務持久化契約。 |
| `ports/driven/` | `.port.ts` | `phase-repository` | 介面:階段持久化契約。 |
| `ports/driven/` | `.port.ts` | `label-repository` | 介面:標籤持久化契約。 |
| `ports/driven/` | `.port.ts` | `scheduler-service` | 介面:排程器服務契約。 |
#### 應用層 (`libs/monitoring`)
**命令(Commands)**
| 位置 | 後綴 | 名稱 | 業務理由 |
| :---------------------- | :------------ | :-------------------- | :--------------------------------- |
| `application/commands/` | `.command.ts` | `create-task` | 流程:建立任務及初始階段。 |
| `application/commands/` | `.command.ts` | `configure-task` | 流程:更新任務可變設定。 |
| `application/commands/` | `.command.ts` | `pause-task` | 流程:暫停任務排程。 |
| `application/commands/` | `.command.ts` | `resume-task` | 流程:恢復任務排程。 |
| `application/commands/` | `.command.ts` | `archive-task` | 流程:封存任務(終態)。 |
| `application/commands/` | `.command.ts` | `delete-task` | 流程:刪除任務及所有相關資料。 |
| `application/commands/` | `.command.ts` | `create-phase` | 流程:建立新階段。 |
| `application/commands/` | `.command.ts` | `edit-phase` | 流程:編輯階段可變邏輯。 |
| `application/commands/` | `.command.ts` | `create-scope` | 流程:新增監控範圍。 |
| `application/commands/` | `.command.ts` | `configure-scan-tier` | 流程:設定範圍掃描層級。 |
| `application/commands/` | `.command.ts` | `create-strategy` | 流程:新增 AI 分流策略。 |
| `application/commands/` | `.command.ts` | `reorder-strategies` | 流程:重新排序策略。 |
| `application/commands/` | `.command.ts` | `create-label` | 流程:建立標籤。 |
| `application/commands/` | `.command.ts` | `update-label` | 流程:更新標籤。 |
| `application/commands/` | `.command.ts` | `delete-label` | 流程:刪除標籤(含歷史發現影響)。 |
**查詢(Queries)**
| 位置 | 後綴 | 名稱 | 業務理由 |
| :--------------------- | :---------- | :----------------- | :----------------------------- |
| `application/queries/` | `.query.ts` | `get-task-list` | 讀取:取得分頁任務列表。 |
| `application/queries/` | `.query.ts` | `get-task-detail` | 讀取:取得任務詳情含階段摘要。 |
| `application/queries/` | `.query.ts` | `get-phase-list` | 讀取:取得任務的所有階段。 |
| `application/queries/` | `.query.ts` | `get-phase-detail` | 讀取:取得階段詳情含範圍策略。 |
| `application/queries/` | `.query.ts` | `get-label-list` | 讀取:取得任務的標籤庫。 |
**策略(Policies)**
| 位置 | 後綴 | 名稱 | 業務理由 |
| :---------------------- | :----------- | :------------------------------ | :------------------------------------- |
| `application/policies/` | `.policy.ts` | `on-task-created-init-schedule` | Saga:任務建立後初始化排程器。 |
| `application/policies/` | `.policy.ts` | `on-zero-findings-pause-phase` | Saga:殭屍任務自動降級(連續無發現)。 |
#### 領域層 (`libs/monitoring`)
**聚合(Aggregates)**
| 位置 | 後綴 | 名稱 | 業務理由 |
| :------------------- | :-------------- | :---------------- | :----------------------------------------- |
| `domain/aggregates/` | `.aggregate.ts` | `monitoring-task` | 核心規則:任務生命週期、不可變上下文鎖定。 |
**實體(Entities)**
| 位置 | 後綴 | 名稱 | 業務理由 |
| :----------------- | :----------- | :------ | :----------------------------- |
| `domain/entities/` | `.entity.ts` | `phase` | 實體:階段生命週期、快照鎖定。 |
**值物件(Value Objects)**
| 位置 | 後綴 | 名稱 | 業務理由 |
| :---------------------- | :------- | :----------------- | :-------------------------------------- |
| `domain/value-objects/` | `.vo.ts` | `monitoring-scope` | 值物件:監控範圍定義(關鍵字 × 平台)。 |
| `domain/value-objects/` | `.vo.ts` | `ai-strategy` | 值物件:AI 分流策略定義。 |
| `domain/value-objects/` | `.vo.ts` | `label` | 值物件:標籤定義。 |
**事件(Events)**
| 位置 | 後綴 | 名稱 | 業務理由 |
| :--------------- | :---------- | :--------------------------- | :------------------------- |
| `domain/events/` | `.event.ts` | `task-created` | 事實:新任務已建立。 |
| `domain/events/` | `.event.ts` | `task-configured` | 事實:任務設定已變更。 |
| `domain/events/` | `.event.ts` | `task-paused` | 事實:任務已暫停。 |
| `domain/events/` | `.event.ts` | `task-resumed` | 事實:任務已恢復。 |
| `domain/events/` | `.event.ts` | `task-archived` | 事實:任務已封存。 |
| `domain/events/` | `.event.ts` | `task-deleted` | 事實:任務已刪除。 |
| `domain/events/` | `.event.ts` | `phase-created` | 事實:新階段已建立。 |
| `domain/events/` | `.event.ts` | `phase-updated` | 事實:階段設定已更新。 |
| `domain/events/` | `.event.ts` | `scope-scan-tier-configured` | 事實:範圍掃描層級已設定。 |
| `domain/events/` | `.event.ts` | `strategy-reordered` | 事實:策略順序已變更。 |
| `domain/events/` | `.event.ts` | `label-created` | 事實:新標籤已建立。 |
| `domain/events/` | `.event.ts` | `label-deleted` | 事實:標籤已刪除。 |
---
### 3.4 發現上下文(Discovery Context)(`libs/discovery`)
**範疇**:發現管理、審查分類、評論系統、洞察分析、報告匯出。
#### 驅動適配器 - HTTP (`apps/discovery`)
**發現(Findings)**
| 位置 | 後綴 | 名稱 | 業務理由 |
| :-------------------------------- | :--------------- | :--------------- | :------------------------------ |
| `adapters/driving/http/findings/` | `.controller.ts` | `finding` | 進入點:處理發現查詢與操作。 |
| `adapters/driving/http/findings/` | `.req.dto.ts` | `assign-risk` | 輸入:指派風險類別。 |
| `adapters/driving/http/findings/` | `.req.dto.ts` | `change-status` | 輸入:變更發現狀態。 |
| `adapters/driving/http/findings/` | `.req.dto.ts` | `batch-action` | 輸入:批次操作(移動/標籤等)。 |
| `adapters/driving/http/findings/` | `.res.dto.ts` | `finding-list` | 輸出:分頁發現列表。 |
| `adapters/driving/http/findings/` | `.res.dto.ts` | `finding-detail` | 輸出:發現詳情(含評論)。 |
**評論(Comments)**
| 位置 | 後綴 | 名稱 | 業務理由 |
| :-------------------------------- | :--------------- | :------------ | :------------------------- |
| `adapters/driving/http/comments/` | `.controller.ts` | `comment` | 進入點:處理評論 CRUD。 |
| `adapters/driving/http/comments/` | `.req.dto.ts` | `add-comment` | 輸入:新增評論或系統註解。 |
**洞察(Insights)**
| 位置 | 後綴 | 名稱 | 業務理由 |
| :-------------------------------- | :--------------- | :------------------- | :------------------------- |
| `adapters/driving/http/insights/` | `.controller.ts` | `insight` | 進入點:處理統計分析查詢。 |
| `adapters/driving/http/insights/` | `.res.dto.ts` | `insight-summary` | 輸出:洞察統計摘要。 |
| `adapters/driving/http/insights/` | `.res.dto.ts` | `insight-trends` | 輸出:趨勢圖表資料。 |
| `adapters/driving/http/insights/` | `.res.dto.ts` | `insight-comparison` | 輸出:跨階段比較資料。 |
**匯出(Export)**
| 位置 | 後綴 | 名稱 | 業務理由 |
| :------------------------------ | :--------------- | :---------------- | :----------------------- |
| `adapters/driving/http/export/` | `.controller.ts` | `export` | 進入點:處理匯出作業。 |
| `adapters/driving/http/export/` | `.req.dto.ts` | `export-findings` | 輸入:匯出發現清單請求。 |
| `adapters/driving/http/export/` | `.req.dto.ts` | `export-stats` | 輸入:匯出統計報告請求。 |
#### 被驅動適配器 - Firestore/Algolia (`apps/discovery`)
| 位置 | 後綴 | 名稱 | 業務理由 |
| :------------------------------------ | :------------------ | :------------------ | :---------------------------------- |
| `adapters/driven/firestore/findings/` | `.adapter.ts` | `firestore-finding` | 持久化:發現的 Repository 實作。 |
| `adapters/driven/firestore/findings/` | `.firestore.dto.ts` | `finding` | 資料庫結構:Firestore 發現文件。 |
| `adapters/driven/firestore/comments/` | `.adapter.ts` | `firestore-comment` | 持久化:評論的 Repository 實作。 |
| `adapters/driven/algolia/` | `.adapter.ts` | `algolia-finding` | 搜尋:Algolia 發現索引實作 (CQRS)。 |
#### 驅動端口 (`libs/discovery`)
| 位置 | 後綴 | 名稱 | 業務理由 |
| :--------------- | :----------------- | :------------------ | :--------------------------- |
| `ports/driving/` | `.command.port.ts` | `assign-risk` | 介面:指派風險命令簽名。 |
| `ports/driving/` | `.command.port.ts` | `change-status` | 介面:變更狀態命令簽名。 |
| `ports/driving/` | `.command.port.ts` | `batch-move-status` | 介面:批次移動狀態命令簽名。 |
| `ports/driving/` | `.command.port.ts` | `assign-labels` | 介面:指派標籤命令簽名。 |
| `ports/driving/` | `.command.port.ts` | `add-comment` | 介面:新增評論命令簽名。 |
| `ports/driving/` | `.command.port.ts` | `export-findings` | 介面:匯出發現命令簽名。 |
#### 被驅動端口 (`libs/discovery`)
| 位置 | 後綴 | 名稱 | 業務理由 |
| :-------------- | :--------- | :------------------- | :----------------------------- |
| `ports/driven/` | `.port.ts` | `finding-repository` | 介面:發現持久化契約。 |
| `ports/driven/` | `.port.ts` | `finding-search` | 介面:發現搜尋契約 (Algolia)。 |
| `ports/driven/` | `.port.ts` | `comment-repository` | 介面:評論持久化契約。 |
| `ports/driven/` | `.port.ts` | `export-service` | 介面:匯出服務契約。 |
#### 應用層 (`libs/discovery`)
**命令(Commands)**
| 位置 | 後綴 | 名稱 | 業務理由 |
| :---------------------- | :------------ | :------------------ | :------------------------- |
| `application/commands/` | `.command.ts` | `assign-risk` | 流程:指派風險類別。 |
| `application/commands/` | `.command.ts` | `change-status` | 流程:變更發現狀態。 |
| `application/commands/` | `.command.ts` | `batch-move-status` | 流程:批次移動狀態。 |
| `application/commands/` | `.command.ts` | `assign-labels` | 流程:指派標籤。 |
| `application/commands/` | `.command.ts` | `add-comment` | 流程:新增評論。 |
| `application/commands/` | `.command.ts` | `export-findings` | 流程:非同步產生匯出檔案。 |
| `application/commands/` | `.command.ts` | `export-stats` | 流程:產生統計報告。 |
**查詢(Queries)**
| 位置 | 後綴 | 名稱 | 業務理由 |
| :--------------------- | :---------- | :----------------------- | :--------------------------------- |
| `application/queries/` | `.query.ts` | `get-finding-list` | 讀取:取得分頁發現列表(含篩選)。 |
| `application/queries/` | `.query.ts` | `get-finding-detail` | 讀取:取得發現詳情。 |
| `application/queries/` | `.query.ts` | `get-comment-list` | 讀取:取得發現的評論列表。 |
| `application/queries/` | `.query.ts` | `get-insight-summary` | 讀取:取得洞察統計摘要。 |
| `application/queries/` | `.query.ts` | `get-insight-trends` | 讀取:取得趨勢圖表資料。 |
| `application/queries/` | `.query.ts` | `get-insight-comparison` | 讀取:取得跨階段比較資料。 |
| `application/queries/` | `.query.ts` | `get-export-status` | 讀取:查詢匯出作業狀態。 |
**策略(Policies)**
| 位置 | 後綴 | 名稱 | 業務理由 |
| :---------------------- | :----------- | :-------------------------- | :---------------------------- |
| `application/policies/` | `.policy.ts` | `on-ai-triaged-add-comment` | Saga:AI 分流後新增系統註解。 |
#### 領域層 (`libs/discovery`)
**聚合(Aggregates)**
| 位置 | 後綴 | 名稱 | 業務理由 |
| :------------------- | :-------------- | :-------- | :------------------------------------------- |
| `domain/aggregates/` | `.aggregate.ts` | `finding` | 核心規則:發現生命週期、狀態流轉、標籤管理。 |
**值物件(Value Objects)**
| 位置 | 後綴 | 名稱 | 業務理由 |
| :---------------------- | :------- | :-------------- | :--------------------------- |
| `domain/value-objects/` | `.vo.ts` | `risk-category` | 值物件:風險類別定義。 |
| `domain/value-objects/` | `.vo.ts` | `comment` | 值物件:評論(含系統註解)。 |
**事件(Events)**
| 位置 | 後綴 | 名稱 | 業務理由 |
| :--------------- | :---------- | :----------------------- | :------------------------- |
| `domain/events/` | `.event.ts` | `finding-created` | 事實:新發現已建立。 |
| `domain/events/` | `.event.ts` | `finding-status-changed` | 事實:發現狀態已變更。 |
| `domain/events/` | `.event.ts` | `finding-risk-assigned` | 事實:發現已指派風險類別。 |
| `domain/events/` | `.event.ts` | `finding-labels-updated` | 事實:發現標籤已更新。 |
| `domain/events/` | `.event.ts` | `finding-ai-triaged` | 事實:發現已完成 AI 分流。 |
| `domain/events/` | `.event.ts` | `comment-added` | 事實:評論已新增。 |
---
### 3.5 爬取上下文(Crawling Context)(`apps/crawling`)
**範疇**:爬蟲執行、掃描單元分解、ETL 資料清洗、手動留存。
> **注意**:Crawling 沒有獨立的 `libs/`,領域邏輯直接放在 `apps/crawling/` 內部。
#### 驅動適配器 - HTTP/Event (`apps/crawling`)
**手動留存(Manual Retain)**
| 位置 | 後綴 | 名稱 | 業務理由 |
| :------------------------------ | :--------------- | :-------------- | :------------------------- |
| `adapters/driving/http/retain/` | `.controller.ts` | `retain` | 進入點:處理手動留存請求。 |
| `adapters/driving/http/retain/` | `.req.dto.ts` | `manual-retain` | 輸入:提交 URL 進行抓取。 |
| `adapters/driving/http/retain/` | `.res.dto.ts` | `retain-status` | 輸出:留存作業狀態。 |
**排程觸發(Scheduled Scan)**
| 位置 | 後綴 | 名稱 | 業務理由 |
| :------------------------ | :------------ | :--------------- | :------------------------------- |
| `adapters/driving/event/` | `.trigger.ts` | `scheduled-scan` | 事件觸發:Cloud Scheduler 觸發。 |
#### 被驅動適配器 (`apps/crawling`)
| 位置 | 後綴 | 名稱 | 業務理由 |
| :--------------------------- | :------------ | :-------------------- | :------------------------------ |
| `adapters/driven/firestore/` | `.adapter.ts` | `firestore-execution` | 持久化:執行日誌 Repository。 |
| `adapters/driven/crawler/` | `.adapter.ts` | `external-crawler` | 整合:外部爬蟲服務呼叫。 |
| `adapters/driven/pubsub/` | `.adapter.ts` | `finding-publisher` | 整合:發布發現到 Discovery BC。 |
#### 領域層 (`apps/crawling/domain/`)
**聚合(Aggregates)**
| 位置 | 後綴 | 名稱 | 業務理由 |
| :------------------- | :-------------- | :--------- | :--------------------------- |
| `domain/aggregates/` | `.aggregate.ts` | `scan-job` | 核心規則:掃描作業狀態管理。 |
**值物件(Value Objects)**
| 位置 | 後綴 | 名稱 | 業務理由 |
| :---------------------- | :------- | :---------- | :---------------------------------- |
| `domain/value-objects/` | `.vo.ts` | `scan-unit` | 值物件:掃描單元(關鍵字 × 平台)。 |
**事件(Events)**
| 位置 | 後綴 | 名稱 | 業務理由 |
| :--------------- | :---------- | :------------------- | :--------------------- |
| `domain/events/` | `.event.ts` | `scan-started` | 事實:掃描作業已開始。 |
| `domain/events/` | `.event.ts` | `scan-completed` | 事實:掃描作業已完成。 |
| `domain/events/` | `.event.ts` | `scan-failed` | 事實:掃描作業失敗。 |
| `domain/events/` | `.event.ts` | `raw-data-extracted` | 事實:原始資料已擷取。 |
#### 應用層 (`apps/crawling/application/`)
**命令(Commands)**
| 位置 | 後綴 | 名稱 | 業務理由 |
| :---------------------- | :------------ | :-------------- | :------------------------ |
| `application/commands/` | `.command.ts` | `execute-scan` | 流程:執行掃描作業。 |
| `application/commands/` | `.command.ts` | `manual-retain` | 流程:手動提交 URL 抓取。 |
**策略(Policies / Sagas)**
| 位置 | 後綴 | 名稱 | 業務理由 |
| :---------------------- | :----------- | :-------------------------------- | :------------------------------------- |
| `application/policies/` | `.policy.ts` | `scheduled-scan-orchestrator` | Saga:排程掃描編排器(分解掃描單元)。 |
| `application/policies/` | `.policy.ts` | `on-scan-completed-emit-findings` | Saga:掃描完成後發布發現到 Discovery。 |
---
### 3.6 AI 分流上下文(AI Triage Context)(`apps/ai-triage`)
**範疇**:AI 策略執行、LLM 呼叫、推論記錄、系統註解產生。
> **注意**:AI Triage 沒有獨立的 `libs/`,領域邏輯直接放在 `apps/ai-triage/` 內部。
#### 驅動適配器 - HTTP (`apps/ai-triage`)
| 位置 | 後綴 | 名稱 | 業務理由 |
| :------------------------------ | :--------------- | :-------------- | :----------------------------- |
| `adapters/driving/http/triage/` | `.controller.ts` | `triage` | 進入點:處理 AI 分流請求。 |
| `adapters/driving/http/triage/` | `.req.dto.ts` | `triage` | 輸入:單筆/批次分流請求。 |
| `adapters/driving/http/triage/` | `.res.dto.ts` | `triage-result` | 輸出:分流結果(含推論理由)。 |
#### 被驅動適配器 (`apps/ai-triage`)
| 位置 | 後綴 | 名稱 | 業務理由 |
| :--------------------------- | :------------ | :------------------ | :-------------------------------- |
| `adapters/driven/gemini/` | `.adapter.ts` | `gemini-llm` | 整合:Gemini 2.5 Flash LLM 呼叫。 |
| `adapters/driven/firestore/` | `.adapter.ts` | `firestore-finding` | 讀取:從 Firestore 讀取 Finding。 |
| `adapters/driven/pubsub/` | `.adapter.ts` | `comment-publisher` | 整合:發布系統註解到 Discovery。 |
#### 領域層 (`apps/ai-triage/domain/`)
**聚合(Aggregates)**
| 位置 | 後綴 | 名稱 | 業務理由 |
| :------------------- | :-------------- | :----------- | :------------------------------------- |
| `domain/aggregates/` | `.aggregate.ts` | `triage-job` | 核心規則:分流作業狀態、策略匹配邏輯。 |
**值物件(Value Objects)**
| 位置 | 後綴 | 名稱 | 業務理由 |
| :---------------------- | :------- | :-------------- | :------------------------------ |
| `domain/value-objects/` | `.vo.ts` | `triage-result` | 值物件:分流結果(動作+推論)。 |
| `domain/value-objects/` | `.vo.ts` | `llm-prompt` | 值物件:LLM 提示詞組裝。 |
**事件(Events)**
| 位置 | 後綴 | 名稱 | 業務理由 |
| :--------------- | :---------- | :-------------------- | :--------------------- |
| `domain/events/` | `.event.ts` | `triage-started` | 事實:分流作業已開始。 |
| `domain/events/` | `.event.ts` | `triage-completed` | 事實:分流作業已完成。 |
| `domain/events/` | `.event.ts` | `strategy-matched` | 事實:策略已匹配。 |
| `domain/events/` | `.event.ts` | `no-strategy-matched` | 事實:無策略匹配。 |
#### 應用層 (`apps/ai-triage/application/`)
**命令(Commands)**
| 位置 | 後綴 | 名稱 | 業務理由 |
| :---------------------- | :------------ | :--------------- | :----------------------- |
| `application/commands/` | `.command.ts` | `execute-triage` | 流程:執行單筆 AI 分流。 |
| `application/commands/` | `.command.ts` | `batch-triage` | 流程:執行批次 AI 分流。 |
**策略(Policies)**
| 位置 | 後綴 | 名稱 | 業務理由 |
| :---------------------- | :----------- | :-------------------------------- | :----------------------------- |
| `application/policies/` | `.policy.ts` | `on-triage-completed-add-comment` | Saga:分流完成後發布系統註解。 |
---
在實作功能時,根據以下情境選擇實作路徑:
### 情境 A:處理狀態變更(寫入)- 命令流程
_範例:`CreateTask`、`EditPhase`、`PauseTask`、`ManualRetain`、`AssignRisk`_
1. **定義輸入(Adapter)**:在 `adapters/driving/http/<feature>/` 中建立 **ReqDto**。
- 使用 `class-validator` 進行驗證規則(例如:`@IsString`、`@IsNotEmpty`)。
2. **定義介面(Port)**:在 `ports/driving/` 中定義命令介面。
- 通常包含 `execute(payload): Promise<void>` 方法。
3. **實作流程(Application)**:建立實作端口的 **Command** 類別並標記為 `@injectable`。
- 透過建構函數使用 `@inject` 注入 Repository。
- 呼叫 Repository `findById` 載入聚合。
- 呼叫聚合業務方法(例如:`task.pause()`)。
- 呼叫 Repository `save`。
- 發布領域事件。
- ⛔ **禁止**在此處撰寫業務規則邏輯。
4. **實作邏輯(Domain)**:在聚合內部撰寫驗證邏輯和狀態變更。
- 驗證失敗時拋出 `DomainError`。
5. **連接 API(Adapter)**:建立 **Controller**,注入命令,並處理 HTTP 請求。
6. **註冊 DI(Infrastructure)**:在 `di.ts` 中註冊命令和 Repository 綁定。
### 情境 B:處理資料檢索(讀取)- 查詢流程
_範例:`GetTaskList`、`ViewDetails`、`ExportReport`_
1. **定義輸出(Adapter)**:在 `adapters/driving/http/<feature>/` 中建立 **ResDto**。
2. **實作查詢(Application)**:建立 **Query** 類別並定義篩選參數。
- ⛔ **注意**:不要載入聚合。
3. **實作存取(Adapter)**:在被驅動適配器中實作讀取邏輯(例如:`FirestoreTaskAdapter`)。
- 直接使用 Firestore/Algolia SDK 讀取資料。
- 直接將原始資料映射到 `ResDto` 並返回。
4. **註冊 DI(Infrastructure)**:在 `di.ts` 中註冊查詢。
### 情境 C:處理副作用與複雜流程(Sagas)
_範例:`ScheduledScan`、`AITriageAudit`_
1. **發布事件(Domain)**:確認聚合操作已產生 `DomainEvent`。
2. **監聽事件(Application)**:在 `application/policies/` 中建立 **Policy**。
3. **執行動作**:
- **簡單**:策略直接呼叫服務。
- **複雜(Saga)**:策略編排多個步驟。
4. **記錄**:確保記錄 `Execution Log` 或 `System Comment`。
## 5. 關鍵實作規則(應做與禁止)
### 5.1 領域層
- ✅ **應做**:在聚合方法內撰寫所有業務規則。
- ✅ **應做**:使用 `private` 方法封裝複雜驗證。
- ⛔ **禁止**:匯入任何適配器或應用層物件。
- ⛔ **禁止**:使用公開的 Setters。
### 5.2 應用層
- ✅ **應做**:保持命令精簡;它們僅負責流程控制。
- ✅ **應做**:將 Saga/長時間執行的流程邏輯放在策略中。
- ⛔ **禁止**:在命令內撰寫業務邏輯(例如:`if (status === 'ARCHIVED')`)。
### 5.3 適配器層
- ✅ **應做**:將 DTOs、Mappers 和 Controllers **就近放置**在同一個**功能資料夾**中(例如:`http/tasks/`)。
- ✅ **應做**:區分驅動 DTO(`.req.dto.ts` / `.res.dto.ts`)和持久化 DTO(`.firestore.dto.ts`)。
- ✅ **應做**:依照 **基礎設施 > 業務上下文** 組織被驅動適配器(例如:`driven/firestore/users/`)。
- ⛔ **禁止**:使用「Repository」作為檔案名稱;改用「Adapter」。
## 6. 命名慣例
為確保專案一致性,所有檔案必須使用以下後綴:
| 層級 | 檔案類型 | 後綴 |
| :------------------------ | :--------------- | :------------------- |
| **領域層(Domain)** | 聚合 | `*.aggregate.ts` |
| | 實體 | `*.entity.ts` |
| | 值物件 | `*.vo.ts` |
| | 事件 | `*.event.ts` |
| **應用層(Application)** | 命令 | `*.command.ts` |
| | 查詢 | `*.query.ts` |
| | 策略(含 Sagas) | `*.policy.ts` |
| **端口層(Ports)** | 命令介面 | `*.command.port.ts` |
| | Repository 介面 | `*.port.ts` |
| **適配器層(Adapters)** | 控制器 | `*.controller.ts` |
| | 被驅動適配器 | `*.adapter.ts` |
| | 請求 DTO | `*.req.dto.ts` |
| | 回應 DTO | `*.res.dto.ts` |
| | 持久化 DTO | `*.firestore.dto.ts` |
| **基礎設施層(Infra)** | DI 配置 | `di.ts` |
## 7. Domainarium 整合模式 (Domainarium Integration Patterns)
### 7.1 Authentication Bridge Pattern
Detective 3 採用「驗證橋接模式」與 Domainarium 整合:
- **Domainarium 角色**: Identity Provider (IdP) — 負責帳號密碼驗證、2FA
- **Firebase 角色**: Session Manager — 負責 Session Token 管理與 Firestore Security Rules
- **核心原則**: Detective 3 不儲存密碼,僅在登入時呼叫 Domainarium API
- **整合原則**: **Inbound Read-Only** — 從 Domainarium 讀取,不寫回
**登入流程**:
1. 使用者輸入帳密 → Detective 3 轉發至 Domainarium
2. Domainarium 驗證成功 → 回傳 User Info + Portfolios
3. Detective 3 Mint Firebase Custom Token (含 Claims)
4. 後續操作使用 Firebase Token,不再呼叫 Domainarium
### 7.2 Token 有效期策略
| Token 類型 | 有效期 | 說明 |
| :---------------- | :------ | :--------------------------------------- |
| Domainarium Token | 28 分鐘 | Safety Buffer (Server 30min - 2min 緩衝) |
| Firebase Session | 1 小時 | 標準設定 |
**刷新策略**: 僅在需要「寫回 Domainarium」時檢查 Token,若過期則背景刷新。
### 7.3 Hybrid Authorization (大型權限清單處理)
針對 Firebase Custom Claims 1KB 限制:
| 情境 | 儲存位置 | 驗證方式 |
| :-------------- | :--------------------------------------------------- | :--------------------- |
| < 50 portfolios | Token Claims | Security Rules 直讀 |
| ≥ 50 portfolios | Firestore `/users/{uid}/private_profile/permissions` | Security Rules `get()` |
**Token 標記**: 超過 50 個 portfolio 時,Claims 僅存 `has_large_portfolio: true`。
### 7.4 Domainarium API 關鍵端點
| 端點 | 用途 | 使用情境 |
| :--------------- | :--------------- | :----------------------------- |
| `validate_user` | 登入驗證 | 登入 + 2FA 驗證 |
| `get_user` | 使用者資訊 | 取得使用者詳細資料 |
| `get_group_list` | Portfolio 列表 | 品牌同步 (`SyncBrandsCommand`) |
| `get_user_list` | Portfolio 使用者 | 權限同步 |
| `get_sub_groups` | 子群組列表 | 權限範圍設定 |
**角色對應 (Role Mapping)**:
| Domainarium `user_group_id` | Detective Role |
| :-------------------------- | :-------------- |
| 1 | SystemAdmin |
| 2 | ServiceManager |
| 3 | ClientAccount |
| 4 | ExternalPartner |
> **建議**: Phase 1 使用 Config 檔案 (`config/role-mappings.json`) 即可,Phase 2+ 若需動態調整再改為 Firestore。
---
## 附錄 A. RBAC 權限字典(RBAC Permissions Dictionary)
### A.1 系統角色
| 角色 | 說明 |
| :---------------------------------- | :-------------------------------- |
| **系統管理員 (System Admin)** | 最高權限,管理使用者與系統設定 |
| **客戶經理 (Account Manager)** | IP Twins 員工,管理客戶品牌與任務 |
| **客戶帳號 (Client Account)** | 品牌端員工,審查發現與匯出報告 |
| **外部合作夥伴 (External Partner)** | 受限存取,僅可審查指定品牌的發現 |
### A.2 權限清單 (Scope-based)
#### 身份與存取管理
- `iam:users:create` - 建立使用者,指派角色
- `iam:settings:global` - 全域系統設定
- `iam:audit:export` - 匯出系統稽核日誌
#### 品牌上下文
- `brand:sync` - 同步品牌資料
- `brand:assets:read` - 檢視受保護資產
- `brand:assets:write` - 建立/編輯資產
- `brand:assets:delete` - 刪除資產
#### 監控
- `monitoring:tasks:read` - 檢視任務與洞察報告
- `monitoring:tasks:write` - 建立/編輯任務、管理標籤
- `monitoring:tasks:control` - 暫停/恢復/封存任務
- `monitoring:tasks:delete` - 刪除任務
- `monitoring:phases:write` - 編輯階段、範圍與 AI 策略
#### 爬取
- `crawling:retain:manual` - 手動保留
#### 發現
- `discovery:findings:read` - 檢視發現列表
- `discovery:findings:triage` - 指派風險、變更狀態、註解、套用標籤
- `discovery:takedown:request` - 請求下架
- `discovery:export` - 匯出列表或統計報告
#### AI Triage
- `ai:triage:execute` - 執行 AI 自動 Triage 策略
---
## 附錄 B. 資料命名與格式規範(Data Naming & Format Standards)
### B.1 命名慣例
| 類型 | 格式 | 範例 |
| :------------------ | :--------- | :--------------------------------- |
| **欄位名稱** | camelCase | `platformType`, `usageCount` |
| **Enum 值** | PascalCase | `'Marketplace'`, `'ToBeProcessed'` |
| **Collection 名稱** | camelCase | `userAccounts`, `monitoringTasks` |
| **檔案名稱** | kebab-case | `create-task.command.ts` |
### B.2 ID 產生策略
- **根命名空間**: `UUID v5(Firebase Project Domain, DNS_NS)`
- **集合 UUID**: `UUID v5(Collection Name + Parent UUID, Root Namespace)`
- **文件 ID**: `UUID v5(UUID v1, Collection UUID)`
### B.3 時間戳記
- 所有時間戳記使用 **MicroTimestamp**(自 epoch 以來的微秒數)
- 型別定義:`type MicroTimestamp = number`
### B.4 共用原始類型
```typescript
type MicroTimestamp = number // 微秒時間戳記
type UUID = string // 通用唯一識別碼
type UrlString = string // URL 位址
type HtmlString = string // HTML 格式內容
type TailwindClass = string // Tailwind CSS 類別
```
## 8. Monorepo 開發指引 (pnpm + tsup + Firebase Functions)
本章節說明如何使用 **pnpm** 作為套件管理器、**tsup** 作為打包工具,建構 Firebase Functions 的 Monorepo 專案。
### 8.1 目錄結構概覽
```text
├── Makefile
├── apps
│ └── <function-name> # 每個 Firebase Function 為獨立 app
│ ├── Makefile
│ ├── package.json
│ ├── src
│ │ └── index.ts
│ ├── tsconfig.json
│ └── tsup.config.ts
├── firebase.json
├── libs
│ └── <library-name> # 共享程式庫
│ ├── package.json
│ ├── src
│ │ └── index.ts
│ └── tsconfig.json
├── package.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
└── tsconfig.base.json
```
### 8.2 基礎骨架設定
#### 8.2.1 初始化專案
```bash
git init
pnpm init
```
#### 8.2.2 建立 `pnpm-workspace.yaml`
```yaml
packages:
- "libs/*"
- "apps/*"
```
#### 8.2.3 建立 `tsconfig.base.json`
```json
{
"compilerOptions": {
"module": "commonjs",
"target": "ES2020",
"lib": ["ES2020"],
"strict": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"skipLibCheck": true,
"esModuleInterop": true
}
}
```
#### 8.2.4 Firebase 初始化
```bash
firebase init functions
# 選擇 TypeScript
# 選擇 No ESLint
# 選擇 No Overwrite (如有詢問)
```
#### 8.2.5 移動目錄並修改 `firebase.json`
```bash
mv functions apps/<function-name>
```
修改 `firebase.json`:
```diff
{
"functions": [
{
- "source": "functions",
+ "source": "apps/<function-name>/dist",
- "codebase": "default",
+ "codebase": "<function-name>",
}
]
}
```
### 8.3 建立共享程式庫 (`libs/`)
以 Logger 為例:
```bash
cd libs/logger
pnpm init
pnpm add pino
pnpm add -D typescript @types/node
```
#### 8.3.1 修改 `package.json`
```diff
- "name": "logger",
+ "name": "@detective/logger",
```
> **命名慣例**:使用 `@detective/` 作為 Scope 前綴。
#### 8.3.2 建立 `tsconfig.json`
```json
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"declaration": true,
"declarationMap": true,
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
```
#### 8.3.3 建立 `src/index.ts`
```typescript
import pino from "pino"
const pinoInstance = pino({
level: "info",
base: { service: "detective" },
formatters: {
level: label => {
return { severity: label.toUpperCase() }
},
},
})
export const logger = {
info: (msg: string, data?: object) => pinoInstance.info(data, msg),
warn: (msg: string, data?: object) => pinoInstance.warn(data, msg),
error: (msg: string, err?: any) => pinoInstance.error({ err }, msg),
}
```
### 8.4 完善 Function App (`apps/`)
#### 8.4.1 安裝依賴
```bash
# 專案根目錄
pnpm add -D -w tsup typescript rimraf
# apps/<function-name> 目錄
pnpm add -D @types/node
pnpm add @detective/logger --workspace
```
#### 8.4.2 建立 `tsup.config.ts`
這是核心打包邏輯,負責產生部署用的 `dist/package.json`:
```typescript
import { defineConfig, Options } from "tsup"
import {
writeFile,
symlink,
unlink,
copyFile,
access,
readFile,
} from "fs/promises"
import { existsSync } from "fs"
import path from "node:path"
const CWD = __dirname
const DIST_DIR = path.join(CWD, "dist")
const FIREBASE_FUNCTIONS_DEFAULT_VERSION = "^12.7.0"
const FIREBASE_ADMIN_DEFAULT_VERSION = "^3.4.1"
const createPackageJson = async () => {
const fileContent = await readFile(path.join(CWD, "package.json"), "utf8")
const pkg = JSON.parse(fileContent) as any
const dependencies = pkg.dependencies || {}
const functionsVersion =
typeof dependencies["firebase-functions"] === "string" &&
dependencies["firebase-functions"].length > 0
? dependencies["firebase-functions"]
: FIREBASE_FUNCTIONS_DEFAULT_VERSION
const adminVersion =
typeof dependencies["firebase-admin"] === "string" &&
dependencies["firebase-admin"].length > 0
? dependencies["firebase-admin"]
: FIREBASE_ADMIN_DEFAULT_VERSION
const cloudDeps = {
"firebase-functions": functionsVersion,
"firebase-admin": adminVersion,
// 如有其他 runtime 依賴,請在此新增
}
const packageJson = JSON.stringify(
{
name: pkg.name,
main: "index.js",
engines: { node: "20" },
type: "commonjs",
dependencies: cloudDeps,
},
null,
2
)
const pathSavedTo = path.join(DIST_DIR, "package.json")
console.log(`📦 [tsup] Saved deployment package.json to ${pathSavedTo}`)
await writeFile(pathSavedTo, packageJson)
}
const symlinkNodeModules = async () => {
const target = path.join(CWD, "node_modules")
const link = path.join(DIST_DIR, "node_modules")
try {
await access(target)
} catch (e) {
console.warn(
"⚠️ [tsup] Target node_modules not found. Skipping symlink."
)
return
}
try {
if (existsSync(link)) await unlink(link)
await symlink(target, link, "junction")
console.log(
"🔗 [tsup] Symlink created: dist/node_modules -> ../node_modules"
)
} catch (error) {
console.warn(
"⚠️ [tsup] Failed to create symlink:",
(error as Error).message
)
}
}
const copyEnvToDist = async () => {
const envFiles = [".env", ".env.local"]
let copiedCount = 0
for (const filename of envFiles) {
const sourcePath = path.join(CWD, filename)
const destPath = path.join(DIST_DIR, filename)
try {
await access(sourcePath)
await copyFile(sourcePath, destPath)
console.log(`📋 [tsup] Copied ${filename} to dist/`)
copiedCount++
} catch (error) {}
}
if (copiedCount !== 0) return
console.log(
"⚠️ [tsup] No .env files found/copied. (If using Firebase config, ignore this warning)"
)
}
export default defineConfig((options: Options) => ({
entry: ["src/index.ts"],
format: ["cjs"],
minify: !options.watch,
clean: false,
outDir: "dist",
sourcemap: true,
external: ["firebase-functions", "firebase-admin"],
onSuccess: async () => {
await Promise.all([
createPackageJson(),
copyEnvToDist(),
symlinkNodeModules(),
])
console.log("✅ [tsup] Build and deployment preparation completed.")
},
}))
```
#### 8.4.3 修改 `package.json`
```diff
{
- "name": "functions",
+ "name": "@detective/<function-name>",
- "main": "lib/index.js",
+ "main": "dist/index.js",
- "engines": { "node": "22" },
+ "engines": { "node": "20" },
}
```
#### 8.4.4 修改 `tsconfig.json`
```json
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"moduleResolution": "node",
"outDir": "dist",
"sourceMap": true,
"resolveJsonModule": true,
"baseUrl": ".",
"paths": {
"@detective/logger": ["../../libs/logger/src"]
}
},
"include": ["src", "tsup.config.ts"],
"exclude": ["node_modules", "dist"]
}
```
> **說明**:
>
> - `paths` 須包含所有引用的共享程式庫。
> - `include: ["tsup.config.ts"]` 避免 IDE 開啟時報錯。
> - `exclude: ["node_modules", "dist"]` 提升編譯效能。
### 8.5 Makefile 自動化
#### 8.5.1 `apps/<function-name>/Makefile`
```makefile
.PHONY: install build watch serve deploy clean
.ONESHELL:
install:
@echo "📦 Installing dependencies from root..."
@cd ../.. && pnpm install
build:
@echo "🔨 Building function..."
@pnpm exec tsup
@echo "✅ Build complete."
watch:
@echo "🔨 Building function..."
@pnpm exec tsup --watch
@echo "✅ Build complete."
serve:
@echo "🚀 Starting emulator..."
@firebase emulators:start --only functions
deploy:
@echo "☁️ Deploying to Firebase..."
@firebase deploy --only functions
clean:
@echo "🧹 Cleaning dist..."
@pnpm exec rimraf dist
```
### 8.6 HTTP Trigger 範例
`apps/<function-name>/src/index.ts`:
```typescript
import { setGlobalOptions } from "firebase-functions/v2"
import { onRequest } from "firebase-functions/v2/https"
import { logger } from "@detective/logger"
setGlobalOptions({ maxInstances: 10 })
export const helloWorld = onRequest((request, response) => {
logger.info("hello world", {
structuredData: true,
target: "Moriarty",
evidence_count: 5,
})
response.json({
status: "ok",
message: "Hello from Monorepo!",
timestamp: new Date().toISOString(),
})
})
```
### 8.7 常用指令
| 指令 | 說明 |
| :------------- | :------------------------------- |
| `make install` | 從根目錄安裝所有依賴 |
| `make build` | 打包 Function 至 `dist/` |
| `make watch` | 開發模式,檔案變更時自動重新打包 |
| `make serve` | 啟動 Firebase Emulator |
| `make deploy` | 部署至 Firebase |
| `make clean` | 清除 `dist/` 目錄 |