# 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/` 目錄 |