diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 0000000..c6f88ce --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,21 @@ +{ + "default": true, + "MD003": false, + "MD009": false, + "MD010": false, + "MD013": false, + "MD022": false, + "MD024": false, + "MD025": false, + "MD031": false, + "MD032": false, + "MD033": false, + "MD034": false, + "MD036": false, + "MD040": false, + "MD046": false, + "MD055": false, + "MD056": false, + "MD058": false, + "MD060": false +} diff --git a/.markdownlintrc b/.markdownlintrc new file mode 100644 index 0000000..c6f88ce --- /dev/null +++ b/.markdownlintrc @@ -0,0 +1,21 @@ +{ + "default": true, + "MD003": false, + "MD009": false, + "MD010": false, + "MD013": false, + "MD022": false, + "MD024": false, + "MD025": false, + "MD031": false, + "MD032": false, + "MD033": false, + "MD034": false, + "MD036": false, + "MD040": false, + "MD046": false, + "MD055": false, + "MD056": false, + "MD058": false, + "MD060": false +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3e3a8ed --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,143 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). + +## [Unreleased] + +### Added +- Gitea API token integration +- n8n API key integration +- API key caching with Moka +- Rate limiting for API key validation +- Constant-time hash comparison +- OpenAPI documentation with utoipa + +## [0.1.0] - 2026-03-21 + +### Added + +#### API Key Management System +- API key generation with secure random (UUID v4) +- SHA256 key hashing +- 5 key types: System, User, Service, Integration, Emergency +- Key expiration with configurable TTL +- Grace period for key rotation + +#### Anomaly Detection +- High request rate detection (>1000/min) +- High error rate detection (>50%) +- Multiple IP detection (>5/hour) +- Unusual time activity detection +- Redis Pub/Sub for anomaly alerts + +#### Rotation Mechanism +- Automatic rotation scheduling +- Manual rotation requests +- Forced rotation for security incidents +- Grace period management per key type: + - System: 72 hours + - User: 24 hours + - Service: 48 hours + - Integration: 24 hours + - Emergency: 0 hours (immediate) + +#### PostgreSQL Integration +- `api_keys` table for key storage +- `api_key_audit_log` table for audit trail +- `api_key_anomalies` table for anomaly records +- Full CRUD operations for API keys + +#### Redis Integration +- Anomaly alert Pub/Sub (`momentry:anomaly:alerts`) +- Key anomaly state tracking +- Real-time alert notifications + +#### CLI Commands +- `momentry api-key create` - Create new API key +- `momentry api-key list` - List all API keys +- `momentry api-key validate` - Validate an API key +- `momentry api-key revoke` - Revoke an API key +- `momentry api-key rotate` - Request key rotation +- `momentry api-key stats` - Show statistics + +#### Gitea Integration +- Create Gitea Personal Access Tokens +- List user tokens +- Delete tokens +- Local token tracking +- CLI commands: + - `momentry gitea create` + - `momentry gitea list` + - `momentry gitea delete` + - `momentry gitea verify` + +#### n8n Integration +- Create n8n API keys +- List API keys +- Delete API keys +- Local key tracking +- CLI commands: + - `momentry n8n create` + - `momentry n8n list` + - `momentry n8n delete` + - `momentry n8n verify` + +#### Security Features +- Constant-time hash comparison (subtle crate) +- Rate limiting for validation attempts +- IP-based lockout after failed attempts +- Configurable thresholds via environment variables + +#### Performance Optimizations +- Moka-based API key validation cache +- Configurable TTL and capacity +- Reduced database queries for hot keys + +#### Documentation +- API Key Management design document +- Redis user configuration guide +- Gitea token integration guide +- n8n API key integration guide +- Optimization plan with task codes + +### Environment Variables + +#### API Key Configuration +``` +CACHE_TTL_SECONDS=300 # Cache TTL (default: 300) +CACHE_MAX_CAPACITY=10000 # Max cache entries (default: 10000) +RATE_LIMIT_MAX_ATTEMPTS=5 # Max failed attempts (default: 5) +RATE_LIMIT_WINDOW_SECONDS=900 # Lockout duration (default: 900) +``` + +#### Service URLs +``` +GITEA_URL=http://localhost:3000 +N8N_URL=https://n8n.momentry.ddns.net +``` + +### Database Schema + +#### Tables Created +- `api_keys` - API key storage +- `api_key_audit_log` - Audit trail +- `api_key_anomalies` - Anomaly records +- `gitea_tokens` - Gitea token tracking +- `n8n_api_keys` - n8n API key tracking + +### Dependencies Added +- `uuid` - UUID generation +- `subtle` - Constant-time comparison +- `moka` - Async cache +- `utoipa` - OpenAPI documentation +- `utoipa-swagger-ui` - Swagger UI + +--- + +## Version History + +| Version | Date | Description | +|---------|------|-------------| +| 0.1.0 | 2026-03-21 | Initial release with API Key Management | diff --git a/a1b10138a6bbb0cd.cut.json b/a1b10138a6bbb0cd.cut.json new file mode 120000 index 0000000..31c7e3c --- /dev/null +++ b/a1b10138a6bbb0cd.cut.json @@ -0,0 +1 @@ +/Users/accusys/momentry_core_0.1/output/a1b10138a6bbb0cd.cut.json \ No newline at end of file diff --git a/a1b10138a6bbb0cd.face.json b/a1b10138a6bbb0cd.face.json new file mode 120000 index 0000000..e481d5e --- /dev/null +++ b/a1b10138a6bbb0cd.face.json @@ -0,0 +1 @@ +/Users/accusys/momentry_core_0.1/output/a1b10138a6bbb0cd.face.json \ No newline at end of file diff --git a/a1b10138a6bbb0cd.ocr.json b/a1b10138a6bbb0cd.ocr.json new file mode 120000 index 0000000..edfdb5a --- /dev/null +++ b/a1b10138a6bbb0cd.ocr.json @@ -0,0 +1 @@ +/Users/accusys/momentry_core_0.1/output/a1b10138a6bbb0cd.ocr.json \ No newline at end of file diff --git a/a1b10138a6bbb0cd.pose.json b/a1b10138a6bbb0cd.pose.json new file mode 120000 index 0000000..fb33446 --- /dev/null +++ b/a1b10138a6bbb0cd.pose.json @@ -0,0 +1 @@ +/Users/accusys/momentry_core_0.1/output/a1b10138a6bbb0cd.pose.json \ No newline at end of file diff --git a/a1b10138a6bbb0cd.story.json b/a1b10138a6bbb0cd.story.json new file mode 120000 index 0000000..392fd81 --- /dev/null +++ b/a1b10138a6bbb0cd.story.json @@ -0,0 +1 @@ +/Users/accusys/momentry_core_0.1/output/a1b10138a6bbb0cd.story.json \ No newline at end of file diff --git a/a1b10138a6bbb0cd.yolo.json b/a1b10138a6bbb0cd.yolo.json new file mode 120000 index 0000000..2423006 --- /dev/null +++ b/a1b10138a6bbb0cd.yolo.json @@ -0,0 +1 @@ +/Users/accusys/momentry_core_0.1/output/a1b10138a6bbb0cd.yolo.json \ No newline at end of file diff --git a/docs/API_ACCESS.md b/docs/API_ACCESS.md new file mode 100644 index 0000000..79701db --- /dev/null +++ b/docs/API_ACCESS.md @@ -0,0 +1,193 @@ +# Momentry Core API 存取指南 + +## 基本網址 + +| 環境 | URL | 說明 | +|------|-----|------| +| **本地開發** | `http://localhost:3002` | 直接訪問 API,繞過反向代理 | +| **外部訪問** | `https://api.momentry.ddns.net` | 通過 Caddy 反向代理訪問,需網路可達 | + +### 何時使用哪個 URL + +**使用 `localhost:3002`:** +- 開發/測試環境 +- 直接在伺服器上操作 +- 當反向代理有問題時 + +**使用 `api.momentry.ddns.net`:** +- n8n workflow 中呼叫 API +- 外部系統整合 +- 生產環境 + +## 認證 +目前為開放狀態(示範用途無需認證)。正式環境將實作 API Key。 + +--- + +## 影片搜尋 API + +### 語意搜尋 + +**端點:** `POST /api/v1/search` + +**請求:** +```json +{ + "query": "charade", + "limit": 5, + "uuid": "a1b10138a6bbb0cd" +} +``` + +| 欄位 | 類型 | 必填 | 說明 | +|------|------|------|------| +| `query` | 字串 | 是 | 搜尋文字 | +| `limit` | 整數 | 否 | 最大回傳結果數(預設 10) | +| `uuid` | 字串 | 否 | 依影片 UUID 過濾 | + +**回應:** +```json +{ + "results": [ + { + "uuid": "a1b10138a6bbb0cd", + "chunk_id": "sentence_0006", + "chunk_type": "sentence", + "start_time": 48.8, + "end_time": 55.44, + "text": "fun plot twists, Woody Dialog and charming performances...", + "score": 0.526 + } + ], + "query": "charade" +} +``` + +--- + +### n8n 整合搜尋 + +**端點:** `POST /api/v1/n8n/search` + +**請求:** +```json +{ + "query": "charade", + "limit": 5 +} +``` + +**回應:** +```json +{ + "query": "charade", + "count": 5, + "hits": [ + { + "id": "sentence_0006", + "vid": "a1b10138a6bbb0cd", + "start": 48.8, + "end": 55.44, + "title": "Chunk sentence_0006", + "text": "fun plot twists...", + "score": 0.526, + "media_url": "https://wp.momentry.ddns.net/Old_Time_Movie_Show_-_Charade_1963.HD.mov" + } + ] +} +``` + +--- + +## 影片管理 API + +### 列出所有影片 +**端點:** `GET /api/v1/videos` + +### 查詢影片資訊 +**端點:** `GET /api/v1/lookup?uuid={uuid}` 或 `GET /api/v1/lookup?path={path}` + +### 取得處理進度 +**端點:** `GET /api/v1/progress/{uuid}` + +--- + +## 區塊資料結構 + +每個搜尋結果包含影片播放的時間資訊: + +| 欄位 | 說明 | +|------|------| +| `uuid` | 影片識別碼 | +| `chunk_id` | 區塊唯一識別碼 | +| `chunk_type` | 類型:`sentence`、`cut`、`time_based` | +| `start_time` | 開始時間(秒) | +| `end_time` | 結束時間(秒) | +| `text` | 語音轉文字內容 | +| `score` | 相關性分數(0-1) | + +--- + +## 整合範例 + +### JavaScript/fetch +```javascript +const response = await fetch('http://localhost:3002/api/v1/search', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ query: 'charade', limit: 5 }) +}); +const data = await response.json(); +console.log(data.results); +``` + +### PHP/cURL +```php +$ch = curl_init('http://localhost:3002/api/v1/search'); +curl_setopt($ch, CURLOPT_POST, true); +curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([ + 'query' => 'charade', + 'limit' => 5 +])); +curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); +$response = curl_exec($ch); +$data = json_decode($response, true); +``` + +--- + +## 影片嵌入網址 + +影片可透過 SFTPGo 分享連結存取: +``` +https://wp.momentry.ddns.net/{檔案名稱} +``` + +**手動建立分享連結:** +1. 開啟 SFTPGo Web UI:`http://localhost:8080` +2. 使用帳號 `demo` / 密碼 `demopassword123` 登入 +3. 導航至 `Files` → 選擇影片檔案 +4. 點擊 `Share` → `Create Link` +5. 複製產生的分享連結 + +使用搜尋結果中的 `start_time` 和 `end_time` 來嵌入影片片段。 + +--- + +## 服務列表 + +| 服務 | 網址 | 用途 | +|------|------|------| +| Momentry API | `http://localhost:3002` | 核心 API | +| SFTPGo | `http://localhost:8080` | 檔案儲存 | +| Qdrant | `http://localhost:6333` | 向量搜尋 | +| PostgreSQL | `localhost:5432` | 資料庫 | + +--- + +## 示範影片 + +- **檔案:** `Old_Time_Movie_Show_-_Charade_1963.HD.mov` +- **UUID:** `a1b10138a6bbb0cd` +- **長度:** 約 6879 秒(約 1.9 小時) +- **區塊數:** 3886 個(句子 + 場景 + 時間) diff --git a/docs/API_CURL_EXAMPLES.md b/docs/API_CURL_EXAMPLES.md new file mode 100644 index 0000000..4219899 --- /dev/null +++ b/docs/API_CURL_EXAMPLES.md @@ -0,0 +1,492 @@ +# Momentry API 使用說明 (curl 範例) + +| 項目 | 內容 | +|------|------| +| 版本 | V1.2 | +| 日期 | 2026-03-23 | +| Base URL | `http://localhost:3002` | + +--- + +> **狀態說明**: +> - ✅ **已實作**: 健康檢查、搜尋、影片管理端點 +> - ⚠️ **規劃中**: API Key 管理功能 +> - 🔧 **CLI**: 部分功能需使用命令列工具 + +--- + +## 目錄 + +1. [已實作端點](#1-已實作端點) +2. [API Key 管理](#2-api-key-管理-規劃中) +3. [影片管理](#3-影片管理) +4. [查詢與搜索](#4-查詢與搜索) +5. [系統狀態](#5-系統狀態) + +--- + +## URL 選擇指南 + +### 兩種 URL 的使用情境 + +| 環境 | URL | 說明 | +|------|-----|------| +| **本地開發** | `http://localhost:3002` | 直接訪問 API,繞過反向代理 | +| **外部訪問** | `https://api.momentry.ddns.net` | 通過 Caddy 反向代理訪問,需網路可達 | + +### 何時使用 localhost:3002 + +- ✅ 開發/測試環境 +- ✅ 直接在伺服器上操作 +- ✅ 當 Caddy/反向代理有問題時 +- ✅ 需要快速除錯時 + +### 何時使用 api.momentry.ddns.net + +- ✅ n8n workflow 中呼叫 API +- ✅ 外部系統整合 +- ✅ 生產環境 +- ✅ 從其他機器訪問 + +### 快速切換範例 + +```bash +# 本地測試 +curl http://localhost:3002/health + +# 外部測試(功能相同) +curl https://api.momentry.ddns.net/health +``` + +### 常見問題 + +**Q: 為什麼有兩個 URL?** +A: `localhost:3002` 是直接訪問,`api.momentry.ddns.net` 通過 Caddy 反向代理。 + +**Q: 兩者功能相同嗎?** +A: 是的,所有端點和功能完全相同。 + +**Q: 502 錯誤時怎麼辦?** +A: 如果 `api.momentry.ddns.net` 返回 502,檢查 Momentry API 服務是否運行: +```bash +launchctl list | grep momentry.api +# 如果未運行 +sudo launchctl load /Library/LaunchDaemons/com.momentry.api.plist +``` + +--- + +## 1. 已實作端點 + +### 健康檢查 + +```bash +curl http://localhost:3002/health +``` + +**回應**: +```json +{"status":"ok","version":"0.1.0","uptime_ms":123456} +``` + +### 詳細健康檢查 + +```bash +curl http://localhost:3002/health/detailed +``` + +--- + +## 2. API Key 管理 *(規劃中)* + +> ⚠️ **此功能尚未實作**。以下為規劃中的 API 說明,僅供參考。 + +### 2.1 建立 API Key + +```bash +curl -X POST http://localhost:3002/api/v1/api-keys \ + -H "Content-Type: application/json" \ + -H "X-API-Key: your-admin-key" \ + -d '{ + "name": "my-service-key", + "key_type": "service", + "permissions": ["read", "write"], + "ttl_days": 90 + }' +``` + +### 2.2 列出所有 API Keys + +```bash +curl -X GET http://localhost:3002/api/v1/api-keys \ + -H "X-API-Key: your-admin-key" +``` + +### 2.3 驗證 API Key + +```bash +curl -X GET http://localhost:3002/api/v1/api-keys/validate \ + -H "X-API-Key: key-to-validate" +``` + +### 2.4 撤銷 API Key + +```bash +curl -X DELETE http://localhost:3002/api/v1/api-keys/msvc_a1b2c3d4_... \ + -H "X-API-Key: your-admin-key" +``` + +### 2.5 請求 Key 輪換 + +```bash +curl -X POST http://localhost:3002/api/v1/api-keys/msvc_a1b2c3d4_.../rotate \ + -H "X-API-Key: your-admin-key" \ + -H "Content-Type: application/json" \ + -d '{"reason": "scheduled_rotation"}' +``` + +### 2.6 取得統計資訊 + +```bash +curl -X GET http://localhost:3002/api/v1/api-keys/stats \ + -H "X-API-Key: your-admin-key" +``` + +--- + +## 3. 影片管理 + +### 3.1 註冊影片 ✅ + +```bash +curl -X POST http://localhost:3002/api/v1/register \ + -H "Content-Type: application/json" \ + -d '{"path": "/path/to/video.mp4"}' +``` + +**回應範例**: + +```json +{ + "id": 1, + "uuid": "a1b2c3d4e5f6g7h8", + "file_path": "/path/to/video.mp4", + "file_name": "video.mp4", + "duration": 120.5, + "width": 1920, + "height": 1080 +} +``` + +### 3.2 列出所有影片 ✅ + +```bash +curl http://localhost:3002/api/v1/videos +``` + +### 3.3 查詢影片 ✅ + +```bash +# 依 UUID 查詢 +curl "http://localhost:3002/api/v1/lookup?uuid=a1b2c3d4e5f6g7h8" + +# 依路徑查詢 +curl "http://localhost:3002/api/v1/lookup?path=/path/to/video.mp4" +``` + +### 3.4 處理影片 🔧 *(CLI - 非 API)* + +影片處理需要使用 CLI 命令: + +```bash +# 處理影片(生成 ASR, CUT, YOLO, OCR, Face, Pose 資料) +cargo run --bin momentry -- process + +# 或處理多個影片 +cargo run --bin momentry -- process +``` + +### 3.5 取得處理進度 ✅ + +```bash +curl http://localhost:3002/api/v1/progress/ +``` + +**回應範例**: + +```json +{ + "uuid": "a1b2c3d4e5f6g7h8", + "overall_progress": 75, + "processors": [ + { + "name": "asr", + "status": "complete", + "current": 100, + "total": 100, + "progress": 100, + "message": "7 segments" + }, + { + "name": "cut", + "status": "complete", + "current": 134, + "total": 134, + "progress": 100, + "message": "134 scenes" + }, + { + "name": "yolo", + "status": "progress", + "current": 5000, + "total": 14315, + "progress": 35, + "message": "frame 5000" + } + ] +} +``` + +--- + +## 4. 查詢與搜索 + +### 4.1 語意搜尋 ✅ + +```bash +curl -X POST http://localhost:3002/api/v1/search \ + -H "Content-Type: application/json" \ + -d '{ + "query": "測試關鍵字", + "limit": 5 + }' +``` + +**回應範例**: + +```json +{ + "results": [ + { + "uuid": "a1b2c3d4e5f6g7h8", + "chunk_id": "sentence_0006", + "chunk_type": "sentence", + "start_time": 48.8, + "end_time": 55.44, + "text": "fun plot twists...", + "score": 0.526 + } + ], + "query": "測試關鍵字" +} +``` + +### 4.2 n8n 格式搜尋 ✅ + +```bash +curl -X POST http://localhost:3002/api/v1/n8n/search \ + -H "Content-Type: application/json" \ + -d '{ + "query": "測試關鍵字", + "limit": 5 + }' +``` + +**回應範例**: + +```json +{ + "query": "測試關鍵字", + "count": 2, + "hits": [ + { + "id": "c_001", + "vid": "a1b2c3d4e5f6g7h8", + "start": 48.8, + "end": 55.44, + "title": "Chunk sentence_0006", + "text": "fun plot twists...", + "score": 0.92, + "media_url": "https://wp.momentry.ddns.net/video.mp4" + } + ] +} +``` + +### 4.3 混合搜尋 ✅ + +```bash +curl -X POST http://localhost:3002/api/v1/search/hybrid \ + -H "Content-Type: application/json" \ + -d '{ + "query": "測試關鍵字", + "limit": 5 + }' +``` + +--- + +## 5. 系統狀態 + +### 5.1 健康檢查 ✅ + +```bash +curl http://localhost:3002/health +``` + +**回應**: +```json +{"status":"ok","version":"0.1.0","uptime_ms":123456} +``` + +### 5.2 詳細健康檢查 ✅ + +```bash +curl http://localhost:3002/health/detailed +``` + +**回應範例**: + +```json +{ + "status":"ok", + "version":"0.1.0", + "uptime_ms":123456, + "services":{ + "postgres":{"status":"ok","latency_ms":42,"error":null}, + "redis":{"status":"ok","latency_ms":0,"error":null}, + "qdrant":{"status":"ok","latency_ms":15,"error":null} + } +} +``` + +--- + +## 6. n8n Webhook 測試 + +### 測試 n8n Workflow + +**重要**: 測試前請先在 n8n UI 中點擊 "Execute workflow" 按鈕 + +```bash +# 測試 Video RAG Workflow (Test Mode) +curl -X POST http://localhost:5678/webhook-test/video-rag-mcp \ + -H "Content-Type: application/json" \ + -d '{"query":"charade","limit":3}' + +# 帶有 UUID 過濾的搜尋 +curl -X POST http://localhost:5678/webhook-test/video-rag-mcp \ + -H "Content-Type: application/json" \ + -d '{"query":"woody","limit":5,"uuid":"a1b10138a6bbb0cd"}' +``` + +### 生產環境 Webhook + +**注意**: 工作流程必須處於 Active 狀態 + +```bash +curl -X POST http://localhost:5678/webhook/video-rag-mcp \ + -H "Content-Type: application/json" \ + -d '{"query":"charade","limit":3}' +``` + +### n8n Webhook 常見問題 + +**Q: webhook-test 返回 404** +A: 需要在 n8n UI 中點擊 "Execute workflow" 按鈕後才能使用 test webhook + +**Q: webhook (生產環境) 返回 404** +A: 需要將工作流程切換為 Active 狀態 (右上角開關) + +--- + +## 附錄 + +### A. 服務 URL 列表 + +| 服務 | URL | +|------|-----| +| Momentry API (本地) | `http://localhost:3002` | +| Momentry API (外部) | `https://api.momentry.ddns.net` | +| n8n Web UI | `https://n8n.momentry.ddns.net` | +| n8n Webhook Test | `http://localhost:5678/webhook-test/{workflow-name}` | +| n8n Webhook Prod | `http://localhost:5678/webhook/{workflow-name}` | + +### B. 所有可用端點 + +| 端點 | 方法 | 狀態 | 說明 | +|------|------|------|------| +| `/health` | GET | ✅ | 健康檢查 | +| `/health/detailed` | GET | ✅ | 詳細健康檢查 | +| `/api/v1/register` | POST | ✅ | 註冊影片 | +| `/api/v1/search` | POST | ✅ | 語意搜尋 | +| `/api/v1/n8n/search` | POST | ✅ | n8n 格式搜尋 | +| `/api/v1/search/hybrid` | POST | ✅ | 混合搜尋 | +| `/api/v1/lookup` | GET | ✅ | 查詢影片 | +| `/api/v1/videos` | GET | ✅ | 列出所有影片 | +| `/api/v1/progress/:uuid` | GET | ✅ | 處理進度 | +| `/api/v1/api-keys` | * | ⚠️ | API Key 管理 (規劃中) | + +### C. 常見錯誤 + +| HTTP 狀態 | 說明 | 解決方式 | +|-----------|------|----------| +| 200 | 成功 | - | +| 400 | 請求格式錯誤 | 檢查 JSON 格式 | +| 404 | 端點不存在或資源未找到 | 確認端點 URL 正確 | +| 500 | 伺服器內部錯誤 | 檢查 API 服務日誌 | +| **502** | **Bad Gateway** | **API 服務未啟動,見下方說明** | + +#### 502 Bad Gateway 錯誤 + +**問題**: 外部 URL `https://api.momentry.ddns.net` 返回 502 + +**原因**: Momentry Core API 服務未啟動 + +**解決方式**: + +```bash +# 1. 檢查服務狀態 +launchctl list | grep momentry.api + +# 2. 如果未啟動,手動啟動 +sudo launchctl load /Library/LaunchDaemons/com.momentry.api.plist + +# 3. 或使用本地測試(繞過反向代理) +curl http://localhost:3002/health + +# 4. 檢查日誌 +tail -50 /Users/accusys/momentry/log/momentry_api.error.log +``` + +### D. 範例腳本 + +```bash +#!/bin/bash +# api_test.sh - API 測試腳本 + +API_URL="http://localhost:3002" + +# 健康檢查 +echo "=== Health Check ===" +curl -s "$API_URL/health" | jq . + +# 搜尋 +echo -e "\n=== Search ===" +curl -s -X POST "$API_URL/api/v1/search" \ + -H "Content-Type: application/json" \ + -d '{"query": "test", "limit": 3}' | jq . + +# 列出影片 +echo -e "\n=== Videos ===" +curl -s "$API_URL/api/v1/videos" | jq '.videos | length' +``` + +--- + +## 相關文件 + +- [API_INDEX.md](./API_INDEX.md) - 文件總覽(起點) +- [API_ENDPOINTS.md](./API_ENDPOINTS.md) - 端點完整說明 +- [API_N8N_GUIDE.md](./API_N8N_GUIDE.md) - n8n 使用範例 +- [API_WORDPRESS_GUIDE.md](./API_WORDPRESS_GUIDE.md) - WordPress 使用範例 diff --git a/docs/API_ENDPOINTS.md b/docs/API_ENDPOINTS.md index 3989dfc..1773b8a 100644 --- a/docs/API_ENDPOINTS.md +++ b/docs/API_ENDPOINTS.md @@ -16,9 +16,34 @@ --- +## 認證 + +除健康檢查端點外,所有 API 端點都需要 API Key。 + +### Header 方式 + +```bash +curl -H "X-API-Key: your-api-key" http://localhost:3002/api/v1/videos +``` + +### 響應 + +- `401 Unauthorized` - 缺少或無效的 API Key +- `200 OK` - 認證成功 + +### 取得 API Key + +使用 CLI 建立: + +```bash +./target/release/momentry api-key create "My API Key" --key-type user +``` + +--- + ## 端點列表 -### 健康檢查 +### 健康檢查(公開) | 方法 | 端點 | 說明 | |------|------|------| @@ -213,5 +238,7 @@ sudo launchctl load /Library/LaunchDaemons/com.momentry.api.plist ## 相關文件 - [API_INDEX.md](./API_INDEX.md) - 文件總覽(起點) -- [API_N8N_GUIDE.md](./API_N8N_GUIDE.md) - n8n 使用範例 -- [API_WORDPRESS_GUIDE.md](./API_WORDPRESS_GUIDE.md) - WordPress 使用範例 +- [API_EXAMPLES.md](./API_EXAMPLES.md) - **完整範例總覽(curl / n8n / WordPress)** +- [API_N8N_GUIDE.md](./API_N8N_GUIDE.md) - n8n 詳細指南 +- [API_WORDPRESS_GUIDE.md](./API_WORDPRESS_GUIDE.md) - WordPress 詳細指南 +- [API_CURL_EXAMPLES.md](./API_CURL_EXAMPLES.md) - curl 範例 diff --git a/docs/API_EXAMPLES.md b/docs/API_EXAMPLES.md new file mode 100644 index 0000000..1abb193 --- /dev/null +++ b/docs/API_EXAMPLES.md @@ -0,0 +1,726 @@ +# Momentry Core API 使用範例總覽 + +| 項目 | 內容 | +|------|------| +| 版本 | V2.0 | +| 日期 | 2026-03-25 | +| Base URL (本地) | `http://localhost:3002` | +| Base URL (外部) | `https://api.momentry.ddns.net` | + +--- + +## 快速參考 + +### 環境 URL 選擇 + +| 環境 | URL | 用途 | +|------|-----|------| +| **本地開發** | `http://localhost:3002` | 開發/測試,直接訪問 API | +| **外部訪問** | `https://api.momentry.ddns.net` | n8n、WordPress、curl 生產環境 | + +### 所有可用端點 + +| 方法 | 端點 | 說明 | +|------|------|------| +| GET | `/health` | 健康檢查 | +| GET | `/health/detailed` | 詳細健康檢查 | +| POST | `/api/v1/search` | 語意搜尋(標準格式) | +| POST | `/api/v1/n8n/search` | 語意搜尋(n8n 格式) | +| POST | `/api/v1/search/hybrid` | 混合搜尋 | +| POST | `/api/v1/register` | 註冊影片 | +| POST | `/api/v1/probe` | 探測影片資訊 | +| GET | `/api/v1/videos` | 列出所有影片 | +| GET | `/api/v1/lookup` | 查詢影片 | +| GET | `/api/v1/progress/:uuid` | 處理進度 | +| GET | `/api/v1/jobs` | 任務列表 | +| GET | `/api/v1/jobs/:uuid` | 任務詳情 | + +--- + +## 認證 + +### API Key + +所有 `/api/v1/*` 端點需要 API Key 認證。 + +```bash +# 添加 API Key Header +curl -H "X-API-Key: your-api-key" http://localhost:3002/api/v1/videos + +# 範例 +curl -H "X-API-Key: muser_f08e13ba967e4d8ea8fc542ad9f99ac8_1774416728_90472a35" \ + http://localhost:3002/api/v1/videos +``` + +### 響應狀態 + +| 狀態碼 | 說明 | +|--------|------| +| 200 | 成功 | +| 401 | 未授權(缺少或無效 API Key) | +| 500 | 伺服器錯誤 | + +### 建立 API Key + +```bash +./target/release/momentry api-key create "My Key" --key-type user +``` + +--- + +## 1. curl 範例 + +### 基本語法 + +```bash +# 格式 +curl [OPTIONS] URL + +# 常用選項 +-X METHOD # HTTP 方法 (GET, POST, etc.) +-H HEADER # 添加 HTTP 標頭 +-d DATA # POST 請求體 +-s # 靜默模式 +-w FORMAT # 輸出額外信息 +``` + +### 1.1 健康檢查 + +```bash +# 基本健康檢查 +curl http://localhost:3002/health + +# 詳細健康檢查 +curl http://localhost:3002/health/detailed +``` + +**回應**: +```json +{"status":"ok","version":"0.1.0","uptime_ms":123456} +``` + +### 1.2 語意搜尋 + +```bash +# 標準格式搜尋 +curl -X POST http://localhost:3002/api/v1/search \ + -H "Content-Type: application/json" \ + -d '{"query": "charade", "limit": 5}' + +# n8n 格式搜尋(推薦) +curl -X POST http://localhost:3002/api/v1/n8n/search \ + -H "Content-Type: application/json" \ + -d '{"query": "charade", "limit": 5}' + +# 混合搜尋 +curl -X POST http://localhost:3002/api/v1/search/hybrid \ + -H "Content-Type: application/json" \ + -d '{"query": "charade", "limit": 5}' +``` + +**標準格式回應**: +```json +{ + "results": [ + { + "uuid": "a1b10138a6bbb0cd", + "chunk_id": "sentence_0001", + "chunk_type": "sentence", + "start_time": 48.8, + "end_time": 55.44, + "text": "fun plot twists...", + "score": 0.92 + } + ], + "query": "charade" +} +``` + +**n8n 格式回應**: +```json +{ + "query": "charade", + "count": 1, + "hits": [ + { + "id": "sentence_0001", + "vid": "a1b10138a6bbb0cd", + "start": 48.8, + "end": 55.44, + "title": "Chunk sentence_0001", + "text": "fun plot twists...", + "score": 0.92, + "media_url": "https://wp.momentry.ddns.net/video.mp4" + } + ] +} +``` + +### 1.3 影片管理 + +```bash +# 列出所有影片 +curl http://localhost:3002/api/v1/videos + +# 查詢特定影片(依 UUID) +curl "http://localhost:3002/api/v1/lookup?uuid=a1b10138a6bbb0cd" + +# 查詢特定影片(依路徑) +curl "http://localhost:3002/api/v1/lookup?path=/path/to/video.mp4" + +# 取得處理進度 +curl http://localhost:3002/api/v1/progress/a1b10138a6bbb0cd + +# 探測影片(不註冊) +curl -X POST http://localhost:3002/api/v1/probe \ + -H "Content-Type: application/json" \ + -d '{"path": "/path/to/video.mp4"}' + +# 註冊影片 +curl -X POST http://localhost:3002/api/v1/register \ + -H "Content-Type: application/json" \ + -d '{"path": "/path/to/video.mp4", "file_name": "video.mp4"}' +``` + +### 1.4 批次測試腳本 + +```bash +#!/bin/bash +# api_test.sh - API 測試腳本 + +API_URL="http://localhost:3002" + +echo "=== 健康檢查 ===" +curl -s "$API_URL/health" | jq . + +echo -e "\n=== 語意搜尋 ===" +curl -s -X POST "$API_URL/api/v1/search" \ + -H "Content-Type: application/json" \ + -d '{"query": "charade", "limit": 3}' | jq . + +echo -e "\n=== 影片列表 ===" +curl -s "$API_URL/api/v1/videos" | jq '.videos | length' +``` + +### 1.5 外部 URL 範例 + +```bash +# 使用外部 URL(需網路可達) +curl https://api.momentry.ddns.net/health + +# 外部搜尋 +curl -X POST https://api.momentry.ddns.net/api/v1/n8n/search \ + -H "Content-Type: application/json" \ + -d '{"query": "charade", "limit": 5}' +``` + +--- + +## 2. n8n 範例 + +### 2.1 HTTP Request Node 設定 + +``` +Node: HTTP Request +├── URL: https://api.momentry.ddns.net/api/v1/n8n/search +├── Method: POST +├── Authentication: None +├── Send Body: ✓ (checked) +├── Content Type: JSON +└── Body: + { + "query": "={{ $json.query }}", + "limit": "={{ $json.limit || 10 }}" + } +``` + +### 2.2 基本搜尋 Workflow + +```json +{ + "name": "Momentry Video Search", + "nodes": [ + { + "parameters": {}, + "name": "Manual Trigger", + "type": "n8n-nodes-base.manualTrigger", + "position": [250, 300] + }, + { + "parameters": { + "url": "https://api.momentry.ddns.net/api/v1/n8n/search", + "method": "POST", + "sendBody": true, + "contentType": "json", + "body": { + "query": "charade", + "limit": 3 + } + }, + "name": "Search Video API", + "type": "n8n-nodes-base.httpRequest", + "position": [450, 300] + } + ], + "connections": { + "Manual Trigger": { + "main": [[{"node": "Search Video API"}]] + } + } +} +``` + +### 2.3 Webhook 動態搜尋 + +```json +{ + "name": "Momentry Dynamic Search", + "nodes": [ + { + "parameters": { + "httpMethod": "POST", + "path": "search", + "responseMode": "lastNode" + }, + "name": "Webhook", + "type": "n8n-nodes-base.webhook", + "position": [250, 300] + }, + { + "parameters": { + "url": "https://api.momentry.ddns.net/api/v1/n8n/search", + "method": "POST", + "sendBody": true, + "contentType": "json", + "body": { + "query": "={{ JSON.stringify($json.body.query) }}", + "limit": "={{ $json.body.limit || 5 }}" + } + }, + "name": "Search API", + "type": "n8n-nodes-base.httpRequest", + "position": [450, 300] + } + ], + "connections": { + "Webhook": { + "main": [[{"node": "Search API"}]] + } + } +} +``` + +### 2.4 測試 Webhook + +```bash +# 測試模式(需先在 n8n UI 點擊 Execute) +curl -X POST http://localhost:5678/webhook-test/video-rag-mcp \ + -H "Content-Type: application/json" \ + -d '{"query":"charade","limit":3}' + +# 生產環境(需 workflow 為 Active 狀態) +curl -X POST http://localhost:5678/webhook/video-rag-mcp \ + -H "Content-Type: application/json" \ + -d '{"query":"charade","limit":3}' +``` + +### 2.5 健康檢查 Workflow + +```json +{ + "name": "Momentry Health Check", + "nodes": [ + { + "parameters": {}, + "name": "Manual Trigger", + "type": "n8n-nodes-base.manualTrigger", + "position": [250, 300] + }, + { + "parameters": { + "url": "https://api.momentry.ddns.net/health", + "method": "GET" + }, + "name": "Health Check", + "type": "n8n-nodes-base.httpRequest", + "position": [450, 300] + } + ], + "connections": { + "Manual Trigger": { + "main": [[{"node": "Health Check"}]] + } + } +} +``` + +### 2.6 錯誤處理 + +| 錯誤 | 原因 | 解決 | +|------|------|------| +| 502 Bad Gateway | API 服務未啟動 | `sudo launchctl load /Library/LaunchDaemons/com.momentry.api.plist` | +| "Your request is invalid" | Body 格式設定錯誤 | 確認 Content Type: JSON,Body 為有效 JSON | +| 404 on webhook-test | 未執行 workflow | 在 n8n UI 點擊 "Execute workflow" | + +--- + +## 3. WordPress 範例 + +### 3.1 PHP 基本用法 + +```php + 'charade', + 'limit' => 10 +]; + +$response = wp_remote_post($api_url, [ + 'headers' => ['Content-Type' => 'application/json'], + 'body' => json_encode($data), + 'timeout' => 30 +]); + +if (is_wp_error($response)) { + echo '錯誤: ' . $response->get_error_message(); +} else { + $body = json_decode(wp_remote_retrieve_body($response), true); + print_r($body['hits']); +} +?> +``` + +### 3.2 列出影片 + +```php + 30]); + +if (!is_wp_error($response)) { + $body = json_decode(wp_remote_retrieve_body($response), true); + foreach ($body['videos'] as $video) { + echo $video['file_name'] . "\n"; + } +} +?> +``` + +### 3.3 查詢特定影片 + +```php + 30]); + +if (!is_wp_error($response)) { + $video = json_decode(wp_remote_retrieve_body($response), true); + echo '檔案: ' . $video['file_name'] . "\n"; + echo '時長: ' . $video['duration'] . ' 秒'; +} +?> +``` + +### 3.4 JavaScript fetch + +```javascript +// 搜尋影片 +async function searchVideos(query, limit = 10) { + const response = await fetch('https://api.momentry.ddns.net/api/v1/n8n/search', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ query, limit }) + }); + + if (!response.ok) { + throw new Error('API 請求失敗'); + } + + return await response.json(); +} + +// 使用範例 +searchVideos('charade', 5) + .then(data => { + data.hits.forEach(hit => { + console.log(`${hit.text} (score: ${hit.score})`); + }); + }); +``` + +### 3.5 WordPress Shortcode + +在 `functions.php` 中註冊短碼: + +```php + '', + 'limit' => '10' + ], $atts); + + if (empty($atts['query'])) { + return '

請提供搜尋關鍵字

'; + } + + $response = wp_remote_post('https://api.momentry.ddns.net/api/v1/n8n/search', [ + 'headers' => ['Content-Type' => 'application/json'], + 'body' => json_encode([ + 'query' => $atts['query'], + 'limit' => (int)$atts['limit'] + ]), + 'timeout' => 30 + ]); + + if (is_wp_error($response)) { + return '

搜尋服務暫時無法使用

'; + } + + $data = json_decode(wp_remote_retrieve_body($response), true); + + if (empty($data['hits'])) { + return '

找不到相關結果

'; + } + + $output = '
    '; + foreach ($data['hits'] as $hit) { + $output .= sprintf( + '
  • %s 播放
  • ', + esc_html($hit['text']), + $hit['media_url'], + $hit['start'] + ); + } + $output .= '
'; + + return $output; +}); +?> +``` + +**使用方式**: +``` +[momentry_search query="charade" limit="5"] +``` + +### 3.6 WordPress REST API Endpoint + +在 WordPress REST API 中註冊自定義端點: + +```php + 'POST', + 'callback' => function($request) { + $response = wp_remote_post( + 'https://api.momentry.ddns.net/api/v1/n8n/search', + [ + 'headers' => ['Content-Type' => 'application/json'], + 'body' => json_encode([ + 'query' => $request->get_param('query'), + 'limit' => $request->get_param('limit', 10) + ]) + ] + ); + + if (is_wp_error($response)) { + return new WP_Error('api_error', 'API 請求失敗'); + } + + return json_decode(wp_remote_retrieve_body($response)); + } + ]); +}); +?> +``` + +**呼叫方式**: +``` +POST /wp-json/momentry/v1/search +Body: {"query": "charade", "limit": 5} +``` + +--- + +## 4. 回應格式說明 + +### 4.1 n8n 格式 (`/api/v1/n8n/search`) + +```json +{ + "query": "charade", + "count": 10, + "hits": [ + { + "id": "sentence_0001", + "vid": "a1b10138a6bbb0cd", + "start": 48.8, + "end": 55.44, + "title": "Chunk sentence_0001", + "text": "fun plot twists...", + "score": 0.92, + "media_url": "https://wp.momentry.ddns.net/video.mp4" + } + ] +} +``` + +### 4.2 標準格式 (`/api/v1/search`) + +```json +{ + "results": [ + { + "uuid": "a1b10138a6bbb0cd", + "chunk_id": "sentence_0001", + "chunk_type": "sentence", + "start_time": 48.8, + "end_time": 55.44, + "text": "fun plot twists...", + "score": 0.92 + } + ], + "query": "charade" +} +``` + +### 4.3 健康檢查 + +```json +{ + "status": "ok", + "version": "0.1.0", + "uptime_ms": 123456 +} +``` + +### 4.4 詳細健康檢查 + +```json +{ + "status": "ok", + "version": "0.1.0", + "uptime_ms": 123456, + "services": { + "postgres": {"status": "ok", "latency_ms": 42, "error": null}, + "redis": {"status": "ok", "latency_ms": 0, "error": null}, + "qdrant": {"status": "ok", "latency_ms": 15, "error": null}, + "mongodb": {"status": "ok", "latency_ms": 0, "error": null} + } +} +``` + +### 4.5 處理進度 + +```json +{ + "uuid": "a1b10138a6bbb0cd", + "file_name": "video.mp4", + "duration": 120.5, + "overall_progress": 75, + "processors": [ + {"name": "asr", "status": "complete", "progress": 100}, + {"name": "cut", "status": "complete", "progress": 100}, + {"name": "yolo", "status": "progress", "progress": 35} + ] +} +``` + +### 4.6 Probe 回應 + +```json +{ + "uuid": "a1b10138a6bbb0cd", + "file_name": "video.mp4", + "duration": 120.5, + "width": 1920, + "height": 1080, + "fps": 30.0, + "cached": false, + "format": { + "filename": "/path/to/video.mp4", + "format_name": "mov,mp4,m4a,3gp,3g2,mj2", + "duration": "120.5", + "size": "12345678", + "bit_rate": "819200" + }, + "streams": [ + { + "index": 0, + "codec_name": "h264", + "codec_type": "video", + "width": 1920, + "height": 1080, + "r_frame_rate": "30/1", + "duration": "120.5" + } + ] +} +``` + +--- + +## 5. HTTP 狀態碼 + +| 狀態 | 說明 | 解決 | +|------|------|------| +| 200 | 成功 | - | +| 400 | 請求格式錯誤 | 檢查 JSON 格式 | +| 404 | 端點或資源不存在 | 確認 URL 正確 | +| 500 | 伺服器內部錯誤 | 檢查 API 服務日誌 | +| 502 | API 服務未啟動 | 見下方說明 | + +### 502 Bad Gateway 解決 + +```bash +# 檢查服務狀態 +launchctl list | grep momentry.api + +# 啟動服務 +sudo launchctl load /Library/LaunchDaemons/com.momentry.api.plist + +# 或使用本地測試 +curl http://localhost:3002/health +``` + +--- + +## 6. 常見問題 + +### Q: 為什麼有兩個 URL? + +| URL | 用途 | +|-----|------| +| `localhost:3002` | 直接訪問,繞過反向代理 | +| `api.momentry.ddns.net` | 通過 Caddy 反向代理 | + +### Q: 兩者功能相同嗎? + +是的,所有端點和功能完全相同。 + +### Q: n8n webhook-test 返回 404? + +需在 n8n UI 中點擊 "Execute workflow" 按鈕後才能使用測試 Webhook。 + +### Q: 生產環境 webhook 返回 404? + +需將 workflow 切換為 Active 狀態(右上角開關)。 + +--- + +## 相關文件 + +- [API_INDEX.md](./API_INDEX.md) - 文件總覽 +- [API_ENDPOINTS.md](./API_ENDPOINTS.md) - 端點完整說明 +- [API_N8N_GUIDE.md](./API_N8N_GUIDE.md) - n8n 詳細指南 +- [API_WORDPRESS_GUIDE.md](./API_WORDPRESS_GUIDE.md) - WordPress 詳細指南 diff --git a/docs/API_INDEX.md b/docs/API_INDEX.md new file mode 100644 index 0000000..8a112d9 --- /dev/null +++ b/docs/API_INDEX.md @@ -0,0 +1,102 @@ +# Momentry Core API 文件總覽 + +| 項目 | 內容 | +|------|------| +| 版本 | V2.1 | +| 日期 | 2026-03-25 | + +--- + +## 文件架構 + +``` +docs/ +├── API_INDEX.md ← 本文件:總覽與入口 +├── API_ENDPOINTS.md ← API 端點完整說明 +├── API_EXAMPLES.md ← 完整範例總覽(curl / n8n / WordPress) +├── DEMO_MANUAL.md ← ⭐ 示範手冊(含 Demo API Key) +├── API_N8N_GUIDE.md ← n8n 詳細指南 +├── API_WORDPRESS_GUIDE.md ← WordPress 詳細指南 +├── API_CURL_EXAMPLES.md ← curl 快速範例 +└── API_REFERENCE.md ← 詳細技術參考 +``` + +--- + +## 快速選擇指南 + +| 需求 | 閱讀文件 | +|------|----------| +| **我要快速開始測試** | ⭐ [DEMO_MANUAL.md](./DEMO_MANUAL.md) | +| **我要查看所有範例** | [API_EXAMPLES.md](./API_EXAMPLES.md) | +| 我想了解有哪些 API 端點 | [API_ENDPOINTS.md](./API_ENDPOINTS.md) | +| 我要在 n8n workflow 中呼叫 API | [DEMO_MANUAL.md](./DEMO_MANUAL.md#2-n8n-範例) | +| 我要在 WordPress 中呼叫 API | [DEMO_MANUAL.md](./DEMO_MANUAL.md#3-wordpress-範例) | +| 我要用 curl 快速測試 | [DEMO_MANUAL.md](./DEMO_MANUAL.md#1-curl-範例) | + +--- + +## 認證 + +### Demo API Key + +``` +API Key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69 +Key ID: muser_68600856036340bcafc01930eb4bd839 +過期日: 2027-03-25 +``` + +### 使用方式 + +```bash +curl -H "X-API-Key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69" \ + http://localhost:3002/api/v1/videos +``` + +--- + +## API URL 選擇 + +| 環境 | URL | 使用時機 | +|------|-----|----------| +| **本地開發** | `http://localhost:3002` | 開發/測試、繞過反向代理 | +| **外部訪問** | `https://api.momentry.ddns.net` | n8n、WordPress、遠端系統 | + +### 何時用哪個 + +**使用 `localhost:3002`:** +- 本地終端機測試 +- 當反向代理有問題時 +- 快速除錯 + +**使用 `api.momentry.ddns.net`:** +- n8n workflow +- WordPress 網站 +- 外部系統整合 + +--- + +## 常見問題 + +### Q: API 返回 401 錯誤? +API Key 無效或過期。請使用 Demo API Key 或建立新的 API Key。 + +### Q: API 返回 502 錯誤? +```bash +# 檢查服務狀態 +launchctl list | grep momentry.api + +# 如未啟動 +sudo launchctl load /Library/LaunchDaemons/com.momentry.api.plist +``` + +### Q: 兩個 URL 功能相同嗎? +是的,所有端點完全相同,只是訪問路徑不同。 + +--- + +## 相關文件 + +- [DEMO_MANUAL.md](./DEMO_MANUAL.md) - ⭐ 示範手冊(推薦新手) +- [INSTALL_MOMENTRY_API.md](./INSTALL_MOMENTRY_API.md) - API 服務安裝指南 +- [PENDING_ISSUES.md](./PENDING_ISSUES.md) - 待解決問題追蹤 diff --git a/docs/API_KEY_ARCHITECTURE.md b/docs/API_KEY_ARCHITECTURE.md new file mode 100644 index 0000000..ff0a4d1 --- /dev/null +++ b/docs/API_KEY_ARCHITECTURE.md @@ -0,0 +1,195 @@ +# API Key Management System Architecture + +## System Overview + +``` +┌─────────────────────────────────────────────────────────────────────────────────┐ +│ API Key Management System │ +├─────────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ CLI │ │ HTTP API │ │ Service │ │ External │ │ +│ │ Layer │────▶│ Layer │────▶│ Layer │────▶│ Services │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ +│ │ │ │ │ │ +│ │ │ │ │ │ +│ ▼ ▼ ▼ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────────────┐ │ +│ │ Core Modules │ │ +│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ +│ │ │ Service │ │Validator│ │ Anomaly │ │Rotation │ │ Cleanup │ │ │ +│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │ +│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ +│ │ │ Webhook │ │Encrypt │ │Blacklist│ │ Report │ │ Error │ │ │ +│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────────────┘ │ +│ │ │ │ │ +│ ▼ ▼ ▼ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ PostgreSQL │ │ Redis │ │ External │ │ +│ │ (Storage) │ │ (Cache) │ │ (Gitea/n8n)│ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────────────┘ +``` + +## Module Dependencies + +``` + ┌──────────────┐ + │ models.rs │ + │ (Types) │ + └──────┬───────┘ + │ + ┌──────────────────┼──────────────────┐ + │ │ │ + ▼ ▼ ▼ +┌───────────────┐ ┌───────────────┐ ┌───────────────┐ +│ service.rs │ │ error.rs │ │ validator.rs │ +│ (Core CRUD) │ │ (Errors) │ │ (Cache+Rate) │ +└───────┬───────┘ └───────────────┘ └───────────────┘ + │ + │ ┌───────────────────────────────┐ + │ │ │ + ▼ ▼ ▼ +┌───────────────┐ ┌───────────────┐ ┌───────────────┐ +│ anomaly.rs │ │ rotation.rs │ │ blacklist.rs │ +│ (Detection) │ │ (Rotation) │ │ (IP Block) │ +└───────────────┘ └───────────────┘ └───────────────┘ +``` + +## Request Flow + +``` +Client Request + │ + ▼ +┌─────────────┐ +│ CLI/API │ +└──────┬──────┘ + │ + ▼ +┌─────────────┐ ┌─────────────┐ +│ Rate Limit │────▶│ IP Blacklist│ +│ Check │ │ Check │ +└──────┬──────┘ └──────┬──────┘ + │ │ + └─────────┬─────────┘ + │ + ▼ + ┌───────────────┐ + │ Hash API Key │ + └───────┬───────┘ + │ + ▼ + ┌───────────────┐ ┌───────────────┐ + │ Cache Lookup │────▶│ PostgreSQL │ + └───────┬───────┘ │ Lookup │ + │ └───────┬───────┘ + │ │ + └──────────┬──────────┘ + │ + ▼ + ┌───────────────┐ + │ Validate │ + │ (Status, │ + │ Expiry) │ + └───────┬───────┘ + │ + ┌─────────────┼─────────────┐ + │ │ │ + ▼ ▼ ▼ + ┌──────────┐ ┌──────────┐ ┌──────────┐ + │ Valid │ │ Invalid │ │ Error │ + │ Response│ │ Response │ │ Response │ + └──────────┘ └──────────┘ └──────────┘ +``` + +## Database Schema + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ PostgreSQL │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ api_keys │ │ api_key_audit_ │ │ +│ ├─────────────────┤ │ log │ │ +│ │ id │ ├─────────────────┤ │ +│ │ key_id │─────▶│ id │ │ +│ │ key_hash │ │ key_id (FK) │ │ +│ │ name │ │ action │ │ +│ │ key_type │ │ ip_address │ │ +│ │ status │ │ details │ │ +│ │ expires_at │ └─────────────────┘ │ +│ │ ... │ │ +│ └─────────────────┘ ┌─────────────────┐ │ +│ │ api_key_anomalies│ │ +│ ┌─────────────────┐ ├─────────────────┤ │ +│ │ gitea_tokens │ │ id │ │ +│ ├─────────────────┤ │ key_id (FK) │ │ +│ │ id │ │ anomaly_type │ │ +│ │ gitea_token_id │ │ severity │ │ +│ │ token_name │ │ details │ │ +│ │ scopes │ └─────────────────┘ │ +│ └─────────────────┘ │ +│ │ +│ ┌─────────────────┐ │ +│ │ n8n_api_keys │ │ +│ ├─────────────────┤ │ +│ │ id │ │ +│ │ n8n_key_id │ │ +│ │ label │ │ +│ └─────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +## External Integrations + +``` +┌─────────────────────────────────────────────────────────────────────────────────┐ +│ External Integrations │ +├─────────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ Gitea │ │ n8n │ │ Webhook │ │ +│ ├─────────────────┤ ├─────────────────┤ ├─────────────────┤ │ +│ │ • Create Token │ │ • Create API Key│ │ • Key Created │ │ +│ │ • List Tokens │ │ • List API Keys │ │ • Key Revoked │ │ +│ │ • Delete Token │ │ • Delete API Key│ │ • Anomaly │ │ +│ │ • Verify Token │ │ • Verify │ │ • Rate Limited │ │ +│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────────────┘ +``` + +## Security Layers + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Security Layers │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ Layer 1: Network │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ • IP Blacklist │ │ +│ │ • Rate Limiting │ │ +│ └─────────────────────────────────────────────────────────┘ │ +│ │ +│ Layer 2: Authentication │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ • API Key Hash (SHA256) │ │ +│ │ • Constant-time Comparison │ │ +│ │ • Key Validation (Status, Expiry) │ │ +│ └─────────────────────────────────────────────────────────┘ │ +│ │ +│ Layer 3: Monitoring │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ • Anomaly Detection │ │ +│ │ • Audit Logging (Encrypted) │ │ +│ │ • Webhook Notifications │ │ +│ └─────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` diff --git a/docs/API_KEY_INTEGRATION_TESTS.md b/docs/API_KEY_INTEGRATION_TESTS.md new file mode 100644 index 0000000..366610c --- /dev/null +++ b/docs/API_KEY_INTEGRATION_TESTS.md @@ -0,0 +1,236 @@ +# API Key Management Integration Tests + +## Test Environment Setup + +### Prerequisites + +```bash +# Start services +sudo launchctl load /Library/LaunchDaemons/com.momentry.postgresql.plist +sudo launchctl load /Library/LaunchDaemons/com.momentry.redis.plist + +# Set environment variables +export DATABASE_URL="postgres://accusys@localhost:5432/momentry" +export REDIS_URL="redis://:accusys@localhost:6379" +export GITEA_URL="http://localhost:3000" +export N8N_URL="https://n8n.momentry.ddns.net" +``` + +### Run Tests + +```bash +# Run all unit tests +cargo test --lib + +# Run API key specific tests +cargo test --lib api_key + +# Run with output +cargo test --lib -- --nocapture +``` + +--- + +## Test Cases + +### 1. API Key Creation + +```bash +# Test: Create a service key +momentry api-key create test-key --key-type service --ttl 90 + +# Expected Output: +# ✅ API Key created successfully! +# Key ID: msvc_... +# API Key: msvc_... +# Expires: 2026-06-19 +``` + +### 2. API Key Validation + +```bash +# Test: Validate the created key +momentry api-key validate --key "msvc_..." + +# Expected Output: +# ✅ API Key is valid +# Key ID: msvc_... +# Name: test-key +# Type: service +``` + +### 3. API Key Listing + +```bash +# Test: List all keys +momentry api-key list + +# Expected Output: +# 📋 API Key List +# ┌────────────────────────────────────────────────────────────────────────────┐ +# │ Status │ Name │ Type │ Usage │ Last Used │ +# ├────────────────────────────────────────────────────────────────────────────┤ +# │ ✓ active │ test-key │ "service" │ 0 │ never │ +# └────────────────────────────────────────────────────────────────────────────┘ +``` + +### 4. API Key Statistics + +```bash +# Test: Show statistics +momentry api-key stats + +# Expected Output: +# 📊 API Key Statistics +# ┌─────────────────────────────────────────┐ +# │ Total Keys: 1 │ +# │ Active Keys: 1 │ +# │ Expired Keys: 0 │ +# └─────────────────────────────────────────┘ +``` + +### 5. Gitea Token Creation + +```bash +# Test: Create Gitea token +momentry gitea create \ + --username admin \ + --password "Test3200Test3200Test3200" \ + --token-name "test-token" \ + --scopes "read:repository,write:repository" + +# Expected Output: +# ✅ Gitea Token created successfully! +# Token ID: ... +# SHA1: ... +``` + +### 6. n8n API Key Creation + +```bash +# Test: Create n8n API key +momentry n8n create \ + --api-key "existing-n8n-key" \ + --label "test-key" \ + --expires-in-days 90 + +# Expected Output: +# ✅ n8n API Key created successfully! +# Key ID: ... +# API Key: ... +``` + +--- + +## Automated Test Script + +```bash +#!/bin/bash +# integration_test.sh + +set -e + +echo "=== API Key Integration Tests ===" + +# 1. Create API key +echo "1. Testing API key creation..." +momentry api-key create integration-test --key-type service --ttl 30 +echo "✅ API key created" + +# 2. List keys +echo "2. Testing API key listing..." +momentry api-key list +echo "✅ API key list OK" + +# 3. Show stats +echo "3. Testing statistics..." +momentry api-key stats +echo "✅ Statistics OK" + +# 4. Test Gitea integration +echo "4. Testing Gitea integration..." +GITEA_URL="http://localhost:3000" \ +momentry gitea list --username admin --password "Test3200Test3200Test3200" +echo "✅ Gitea integration OK" + +echo "" +echo "=== All Tests Passed ===" +``` + +--- + +## Unit Test Coverage + +| Module | Tests | Status | +|--------|-------|--------| +| `models.rs` | 0 | ✅ | +| `service.rs` | 5 | ✅ | +| `validator.rs` | 2 | ✅ | +| `gitea.rs` | 3 | ✅ | +| `n8n.rs` | 2 | ✅ | +| `rotation.rs` | 4 | ✅ | +| `anomaly.rs` | 0 | ✅ | +| `blacklist.rs` | 5 | ✅ | +| `encryption.rs` | 2 | ✅ | +| `webhook.rs` | 2 | ✅ | +| `error.rs` | 3 | ✅ | +| `report.rs` | 1 | ✅ | +| `cleanup.rs` | 1 | ✅ | +| **Total** | **30** | **✅** | + +--- + +## CI/CD Integration + +### GitHub Actions / Gitea Actions + +```yaml +name: API Key Tests + +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + services: + postgres: + image: postgres:15 + env: + POSTGRES_USER: accusys + POSTGRES_DB: momentry_test + ports: + - 5432:5432 + redis: + image: redis:7 + ports: + - 6379:6379 + + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - run: cargo test --lib api_key +``` + +--- + +## Troubleshooting + +### Common Issues + +1. **Database connection failed** + ```bash + # Check PostgreSQL status + pg_isready -h localhost -p 5432 + ``` + +2. **Redis connection failed** + ```bash + # Check Redis status + redis-cli -a accusys ping + ``` + +3. **Gitea authentication failed** + ```bash + # Verify credentials + curl -u admin:password http://localhost:3000/api/v1/user + ``` diff --git a/docs/API_KEY_MANAGEMENT.md b/docs/API_KEY_MANAGEMENT.md new file mode 100644 index 0000000..a36cf5a --- /dev/null +++ b/docs/API_KEY_MANAGEMENT.md @@ -0,0 +1,699 @@ +# Momentry API Key 管理系統設計 + +| 項目 | 內容 | +|------|------| +| 版本 | V1.2 | +| 日期 | 2026-03-21 | +| 狀態 | 開發中 | + +--- + +## 1. 概述 + +### 1.1 目標 + +建立安全的 API Key 管理機制,支援: +- 多類型 API Key(系統、用戶、服務) +- 自動過期與輪換 +- 異常使用偵測 +- 強制更新機制 +- 完整審計日誌 +- Gitea Token 整合 +- n8n API Key 整合 + +### 1.2 設計原則 + +| 原則 | 說明 | +|------|------| +| 最小權限 | 每個 Key 僅授予必要權限 | +| 定期輪換 | 自動過期強制更新 | +| 追蹤可審 | 所有操作都有日誌 | +| 分離儲存 | Key 與使用者資料分離 | + +--- + +## 2. API Key 類型 + +### 2.1 Key 類型矩陣 + +| 類型 | 前綴 | 用途 | 預設有效期 | 輪換方式 | +|------|------|------|------------|----------| +| `system` | `msys_` | 系統內部服務 | 365 天 | 手動 | +| `user` | `muser_` | 個人用戶 | 90 天 | 自動 | +| `service` | `msvc_` | 服務間通訊 | 180 天 | 自動 | +| `integration` | `mint_` | 第三方整合 | 30 天 | 強制更新 | +| `emergency` | `memg_` | 緊急存取 | 24 小時 | 一次性 | + +### 2.2 Key 格式 + +``` +{prefix}{uuid_v4}_{timestamp}_{checksum} +``` + +**範例:** +``` +msys_a1b2c3d4-e5f6-7890-abcd-ef1234567890_1710998400_sha256 +``` + +--- + +## 3. 資料庫 Schema + +### 3.1 api_keys 表 + +```sql +CREATE TABLE api_keys ( + id BIGSERIAL PRIMARY KEY, + key_id VARCHAR(64) UNIQUE NOT NULL, -- 公開 Key ID + key_hash VARCHAR(128) NOT NULL, -- SHA256 哈希 + key_prefix VARCHAR(8) NOT NULL, -- Key 前綴 + name VARCHAR(128) NOT NULL, -- Key 名稱 + key_type VARCHAR(32) NOT NULL, -- system/user/service/integration/emergency + user_id BIGINT, -- 關聯用戶 (nullable for system) + service_name VARCHAR(64), -- 服務名稱 (for service keys) + permissions JSONB NOT NULL DEFAULT '[]', -- 權限列表 + expires_at TIMESTAMP, -- 過期時間 + last_used_at TIMESTAMP, -- 最後使用時間 + last_used_ip VARCHAR(45), -- 最後使用 IP + usage_count BIGINT DEFAULT 0, -- 使用次數 + status VARCHAR(16) DEFAULT 'active', -- active/suspended/expired/revoked + rotation_required BOOLEAN DEFAULT FALSE, -- 強制輪換標記 + rotation_reason VARCHAR(256), -- 輪換原因 + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX idx_api_keys_key_id ON api_keys(key_id); +CREATE INDEX idx_api_keys_user_id ON api_keys(user_id); +CREATE INDEX idx_api_keys_type ON api_keys(key_type); +CREATE INDEX idx_api_keys_status ON api_keys(status); +CREATE INDEX idx_api_keys_expires ON api_keys(expires_at); +``` + +### 3.2 api_key_audit_log 表 + +```sql +CREATE TABLE api_key_audit_log ( + id BIGSERIAL PRIMARY KEY, + key_id VARCHAR(64) NOT NULL, + action VARCHAR(32) NOT NULL, -- created/used/rotated/revoked/expired/suspended + actor VARCHAR(64), -- 操作者 (user_id or 'system') + ip_address VARCHAR(45), + user_agent VARCHAR(512), + request_path VARCHAR(256), + response_code INTEGER, + details JSONB, + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX idx_audit_key_id ON api_key_audit_log(key_id); +CREATE INDEX idx_audit_action ON api_key_audit_log(action); +CREATE INDEX idx_audit_created ON api_key_audit_log(created_at); +``` + +### 3.3 api_key_rotation_log 表 + +```sql +CREATE TABLE api_key_rotation_log ( + id BIGSERIAL PRIMARY KEY, + key_id VARCHAR(64) NOT NULL, + old_key_id VARCHAR(64), + new_key_id VARCHAR(64), + rotation_type VARCHAR(32) NOT NULL, -- scheduled/manual/forced/emergency + reason VARCHAR(256), + triggered_by VARCHAR(64), -- system/user/scheduler + grace_period_end TIMESTAMP, -- 寬限期結束時間 + created_at TIMESTAMP DEFAULT NOW() +); +``` + +--- + +## 4. API Key 狀態機 + +``` + ┌──────────────┐ + │ created │ + └──────┬───────┘ + │ + ▼ + ┌────────────────────┐ + │ active │◄─────────────┐ + └─────────┬──────────┘ │ + │ │ + ┌─────────────┼─────────────┐ │ + │ │ │ │ + ▼ ▼ ▼ │ + ┌──────────┐ ┌──────────┐ ┌──────────┐ │ + │ suspended │ │ expired │ │ revoked │─────┘ + └──────────┘ └──────────┘ └──────────┘ +``` + +### 狀態轉換規則 + +| 從 | 到 | 觸發條件 | +|----|----|----------| +| created | active | 啟用 Key | +| active | suspended | 異常使用偵測 | +| active | expired | 達到過期時間 | +| active | revoked | 手動撤銷 | +| suspended | active | 解除鎖定 | +| suspended | revoked | 確認異常 | +| expired | active | 重新啟用 | + +--- + +## 5. 異常偵測機制 + +### 5.1 異常指標 + +| 指標 | 閾值 | 處置 | +|------|------|------| +| 每分鐘請求數 | > 1000 | 警告 | +| 每小時請求數 | > 10000 | 鎖定 | +| 錯誤率 | > 50% | 警告 | +| 不同 IP 數 | > 5/小時 | 警告 | +| 非工作時間使用 | 深夜請求 | 警告 | +| 異常模式 | 暴力破解 | 鎖定 | + +### 5.2 異常處理流程 + +``` +異常偵測 + │ + ▼ +┌─────────┐ +│ 分析 │──→ 排除正常流量 +└────┬────┘ + │ + ▼ +┌─────────┐ +│ 評估 │──→ 輕微 → 警告 +└────┬────┘ + │ + ▼ +┌─────────┐ +│ 處置 │──→ 嚴重 → 鎖定 + 輪換 +└─────────┘ +``` + +--- + +## 6. 強制更新機制 + +### 6.1 觸發條件 + +| 條件 | 嚴重性 | 動作 | +|------|--------|------| +| 疑似洩露 | 高 | 立即停用 + 強制輪換 | +| 異常使用 | 中 | 警告 + 建議輪換 | +| 計劃性維護 | 低 | 通知 + 排程輪換 | +| 政策要求 | 高 | 強制輪換 | +| 過期 | 低 | 停用 + 通知 | + +### 6.2 強制輪換流程 + +``` +1. 系統偵測到需要強制更新 + │ + ▼ +2. 建立新 Key(保留舊 Key 在寬限期內) + │ + ▼ +3. 發送通知(Email/Slack/Redis PubSub) + │ + ▼ +4. 寬限期開始(預設 24 小時) + │ + ├── 在寬限期內更新 → 完成輪換 + │ + └── 寬限期結束 → 舊 Key 停用 +``` + +### 6.3 寬限期配置 + +| Key 類型 | 寬限期 | +|----------|--------| +| system | 72 小時 | +| user | 24 小時 | +| service | 48 小時 | +| integration | 24 小時 | +| emergency | 0 小時 | + +--- + +## 7. CLI 管理命令 + +### 7.1 命令列表 + +```bash +# Key 管理 +momentry api-key create --name "My Key" --type user --permissions read,write +momentry api-key list --type user +momentry api-key info +momentry api-key revoke --reason "安全原因" + +# 輪換管理 +momentry api-key rotate # 正常輪換 +momentry api-key force-rotate # 強制輪換 +momentry api-key rotation-status # 查看輪換狀態 + +# 異常管理 +momentry api-key suspend --reason "異常使用" +momentry api-key unsuspend +momentry api-key blacklist # 列入黑名單 + +# 審計 +momentry api-key audit --since 7d +momentry api-key stats --type service --period 30d +``` + +### 7.2 輸出範例 + +```bash +$ momentry api-key list --type service + +┌────────────────────────────────────┬─────────┬──────────────┬────────────────┐ +│ Key ID │ Name │ Status │ Expires │ +├────────────────────────────────────┼─────────┼──────────────┼────────────────┤ +│ msvc_a1b2c3d4_1710998400_sha256 │ N8N │ active │ 2026-09-21 │ +│ msvc_e5f6g7h8_1713600000_sha256 │ OpenCode│ rotation_req │ 2026-09-21 │ +└────────────────────────────────────┴─────────┴──────────────┴────────────────┘ + +⚠️ 1 個 Key 需要輪換 +``` + +--- + +## 8. 實現計畫 + +### Phase 1: 核心功能 +- [ ] 資料庫 Schema +- [ ] Key 生成與哈希 +- [ ] 基本 CRUD API +- [ ] 過期檢查 + +### Phase 2: 安全機制 +- [ ] 異常偵測 +- [ ] 自動鎖定 +- [ ] 強制輪換 +- [ ] 寬限期管理 + +### Phase 3: 管理工具 +- [ ] CLI 命令 +- [ ] 審計日誌 +- [ ] 統計報表 +- [ ] 通知系統 + +### Phase 4: 自動化 +- [ ] 定時輪換排程 +- [ ] Prometheus 指標 +- [ ] Alertmanager 整合 +- [ ] 自動化回應 + +--- + +## 9. 安全考量 + +### 9.1 Key 儲存 +- 明文 Key 只顯示一次(創建時) +- 儲存時使用 SHA256 哈希 +- 使用 Fernet 對稱加密敏感配置 + +### 9.2 傳輸安全 +- 所有 API 必須使用 HTTPS +- Key 在 Header 中傳輸(X-API-Key) +- 避免 Key 在 URL 中 + +### 9.3 存取控制 +- 只有管理員可創建/撤銷 Key +- 用戶只能管理自己的 Key +- 系統 Key 需要特殊權限 + +--- + +## 10. 環境變數配置 + +```bash +# API Key 管理 +MOMENTRY_API_KEY_GRACE_PERIOD=86400 # 寬限期(秒) +MOMENTRY_API_KEY_MAX_PER_USER=5 # 每用戶最大 Key 數 +MOMENTRY_API_KEY_ROTATION_DAYS=90 # 自動輪換天數 + +# 異常偵測 +MOMENTRY_API_KEY_RATE_LIMIT=1000 # 每分鐘限制 +MOMENTRY_API_KEY_ERROR_THRESHOLD=0.5 # 錯誤率閾值 +MOMENTRY_API_KEY_IP_LIMIT=5 # 每小時 IP 限制 + +# 通知 +MOMENTRY_API_KEY_ALERT_WEBHOOK= # 異常通知 webhook +``` + +--- + +## 11. Gitea API Token 整合 + +### 11.1 概述 + +支援透過 API Key 管理系統建立和管理 Gitea Personal Access Tokens,採用「建立時納管」模式。 + +### 11.2 納管模式 + +``` +使用者提供帳號密碼 → 呼叫 Gitea API 建立 Token → 明文只顯示一次 → 同步儲存至管理系統 +``` + +**特點:** +- Token 明文僅在建立時取得 +- 管理系統記錄 Token 元數據(不含明文) +- 支援本地查詢和刪除 + +### 11.3 資料庫結構 + +```sql +CREATE TABLE gitea_tokens ( + id SERIAL PRIMARY KEY, + gitea_token_id BIGINT NOT NULL, -- Gitea 內部 Token ID + gitea_user VARCHAR(128) NOT NULL, -- Gitea 用戶名 + token_name VARCHAR(128) NOT NULL, -- Token 名稱 + token_last_eight VARCHAR(8) NOT NULL, -- SHA1 最後 8 碼(顯示用) + scopes JSONB DEFAULT '[]', -- 權限範圍 + api_key_id VARCHAR(48), -- 關聯的 API Key ID(可選) + last_verified TIMESTAMP, -- 最後驗證時間 + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(gitea_user, token_name) +); +``` + +### 11.4 Token 權限範圍 + +| 範圍 | 說明 | +|------|------| +| `read:repository` | 讀取倉庫 | +| `write:repository` | 寫入倉庫 | +| `read:issue` | 讀取議題 | +| `write:issue` | 寫入議題 | +| `read:user` | 讀取用戶資訊 | +| `write:write` | 修改用戶資訊 | +| `read:organization` | 讀取組織 | +| `write:organization` | 修改組織 | +| `read:package` | 讀取套件 | +| `write:package` | 發布套件 | +| `read:notification` | 讀取通知 | +| `write:notification` | 修改通知 | +| `read:admin` | 管理員讀取 | +| `write:admin` | 管理員寫入 | + +### 11.5 CLI 命令 + +#### 建立 Token + +```bash +# 基本用法 +momentry gitea create \ + --username \ + --password \ + --token-name \ + --scopes "read:repository,write:repository" + +# 範例:建立整合用 Token +momentry gitea create \ + --username admin \ + --password "MyPassword123" \ + --token-name "ci-pipeline" \ + --scopes "read:repository,write:repository,read:issue,write:issue" +``` + +**輸出範例:** +``` +✅ Gitea Token created successfully! + +┌─────────────────────────────────────────────────────────────────────────────┐ +│ ⚠️ IMPORTANT: Save this token now - it will not be shown again! │ +└─────────────────────────────────────────────────────────────────────────────┘ + +Token ID: 9 +Token Name: ci-pipeline +SHA1: 9a4f282e9ba817b430082e6bff2c18e2ae38e480 +Last 8: ae38e480 + +Authorization Header: + Authorization: token 9a4f282e9ba817b430082e6bff2c18e2ae38e480 +``` + +#### 列出 Token + +```bash +# 列出用戶的所有 Token +momentry gitea list \ + --username \ + --password +``` + +**輸出範例:** +``` +📋 Gitea Tokens for user: admin + +┌────────────────────────────────────────────────────────────────────────────┐ +│ ID │ Name │ Last 8 │ Registered │ +├────────────────────────────────────────────────────────────────────────────┤ +│ 9 │ ci-pipeline │ ae38e480 │ ✓ │ +│ 8 │ dev-token │ 1234abcd │ - │ +└────────────────────────────────────────────────────────────────────────────┘ + +Total: 2 token(s) +``` + +#### 刪除 Token + +```bash +# 刪除指定 Token +momentry gitea delete \ + --username \ + --password \ + --token-name +``` + +#### 查詢本地記錄 + +```bash +# 查詢已納管的 Token 記錄 +momentry gitea verify --token-name +``` + +**輸出範例:** +``` +📋 Gitea Token: ci-pipeline + User: admin + Token ID: 9 + Last 8: ae38e480 + Scopes: ["read:repository","write:repository"] + Created: 2026-03-21 06:44:55.577586 UTC + Last Verified: never +``` + +### 11.6 使用範圍 + +#### 適用場景 + +| 場景 | 說明 | +|------|------| +| CI/CD 整合 | 建立專用 Token 用於自動化流程 | +| 服務間通訊 | 建立 Token 供其他服務存取 Gitea API | +| 開發環境 | 為開發者建立短期 Token | +| 監控整合 | 建立只讀 Token 用於監控和報告 | + +#### 限制 + +| 限制 | 說明 | +|------|------| +| 明文 Token | 僅在建立時取得,無法再次查詢 | +| 管理 API | 需要帳號密碼(BasicAuth) | +| Token 驗證 | 只能透過 API 呼叫驗證有效性 | +| 同步刪除 | 本地刪除不會自動同步到 Gitea | + +### 11.7 環境變數 + +```bash +# Gitea 連線設定 +GITEA_URL=http://localhost:3000 # Gitea API URL +``` + +### 11.8 安全考量 + +| 項目 | 措施 | +|------|------| +| 密碼傳輸 | 僅在 CLI 命令中使用,不儲存 | +| Token 儲存 | 本地僅存元數據,不含明文 | +| 權限最小化 | 建議僅授予必要權限 | +| 定期輪換 | 建議定期更新 Token | + +--- + +## 12. n8n API Key 整合 + +### 12.1 概述 + +支援透過 API Key 管理系統建立和管理 n8n API Keys,採用「建立時納管」模式。 + +### 12.2 納管模式 + +``` +使用者提供現有 n8n API Key → 呼叫 n8n API 建立新 Key → 明文只顯示一次 → 同步儲存至管理系統 +``` + +**特點:** +- 需要一個現有的 n8n API Key 作為管理憑證 +- API Key 明文僅在建立時取得 +- 管理系統記錄 Key 元數據(不含明文) +- 支援本地查詢和刪除 + +### 12.3 資料庫結構 + +```sql +CREATE TABLE n8n_api_keys ( + id SERIAL PRIMARY KEY, + n8n_key_id VARCHAR(64) UNIQUE NOT NULL, -- n8n 內部 Key ID + label VARCHAR(100) NOT NULL, -- Key 標籤 + api_key_last_eight VARCHAR(8) NOT NULL, -- API Key 最後 8 碼(顯示用) + momentry_api_key_id VARCHAR(48), -- 關聯的 API Key ID(可選) + expires_at TIMESTAMP WITH TIME ZONE, -- 過期時間 + last_verified TIMESTAMP WITH TIME ZONE, -- 最後驗證時間 + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); +``` + +### 12.4 認證方式 + +n8n 使用 JWT-based API Key,透過 `X-N8N-API-KEY` Header 認證: + +```bash +curl -H "X-N8N-API-KEY: " https://n8n.example.com/api/v1/workflows +``` + +### 12.5 CLI 命令 + +#### 建立 API Key + +```bash +# 基本用法 +momentry n8n create \ + --api-key \ + --label \ + --expires-in-days + +# 範例:建立 CI/CD 用 Key +momentry n8n create \ + --api_key "n8n_api_xxxxxxxxxxxx" \ + --label "ci-pipeline" \ + --expires-in-days 90 +``` + +**輸出範例:** +``` +✅ n8n API Key created successfully! + +┌─────────────────────────────────────────────────────────────────────────────┐ +│ ⚠️ IMPORTANT: Save this API key now - it will not be shown again! │ +└─────────────────────────────────────────────────────────────────────────────┘ + +Key ID: abc123-def456 +Label: ci-pipeline +API Key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... + +Usage: + curl -H 'X-N8N-API-KEY: eyJhbGciOiJIUz...' https://n8n.momentry.ddns.net/api/v1/workflows +``` + +#### 列出 API Keys + +```bash +# 列出所有 API Keys +momentry n8n list --api-key +``` + +**輸出範例:** +``` +📋 n8n API Keys + +┌────────────────────────────────────────────────────────────────────────────┐ +│ Label │ ID │ +├────────────────────────────────────────────────────────────────────────────┤ +│ ci-pipeline │ abc123-def456-789 │ +│ monitoring │ xyz789-abc123-456 │ +└────────────────────────────────────────────────────────────────────────────┘ + +Total: 2 key(s) +``` + +#### 刪除 API Key + +```bash +# 刪除指定 API Key +momentry n8n delete \ + --api-key \ + --label +``` + +#### 查詢本地記錄 + +```bash +# 查詢已納管的 API Key 記錄 +momentry n8n verify --label +``` + +**輸出範例:** +``` +📋 n8n API Key: ci-pipeline + Key ID: abc123-def456 + Last 8: ...JVCJ9 + Created: 2026-03-21 06:44:55.577586 UTC + Expires: 2026-06-19 06:44:55.577586 UTC + Last Verified: never +``` + +### 12.6 使用範圍 + +#### 適用場景 + +| 場景 | 說明 | +|------|------| +| CI/CD 整合 | 建立專用 Key 用於自動化流程 | +| 監控整合 | 建立只讀 Key 用於監控工作流狀態 | +| 服務間通訊 | 建立 Key 供其他服務呼叫 n8n API | +| 開發環境 | 為開發者建立短期 Key | + +#### 限制 + +| 限制 | 說明 | +|------|------| +| 明文 API Key | 僅在建立時取得,無法再次查詢 | +| 管理憑證 | 需要一個現有的 n8n API Key | +| 本地刪除 | 不會自動同步到 n8n | +| 權限範圍 | 非 Enterprise 版無細粒度權限 | + +### 12.7 環境變數 + +```bash +# n8n 連線設定 +N8N_URL=https://n8n.momentry.ddns.net # n8n API URL +``` + +### 12.8 安全考量 + +| 項目 | 措施 | +|------|------| +| 管理 Key | 需妥善保管,作為管理其他 Key 的憑證 | +| API Key 儲存 | 本地僅存元數據,不含明文 | +| 過期機制 | 建議設定過期時間 | +| 定期輪換 | 建議定期更新 Key | + +--- + +## 13. 參考文檔 + +- PostgreSQL Schema +- Redis Key 設計( MOMENTRY_CORE_REDIS_KEYS.md) +- 監控系統(MOMENTRY_CORE_MONITORING.md) +- Gitea 安裝指南(INSTALL_GITEA.md) +- n8n API 文件(https://docs.n8n.io/api/authentication/) diff --git a/docs/API_KEY_OPTIMIZATION.md b/docs/API_KEY_OPTIMIZATION.md new file mode 100644 index 0000000..b118521 --- /dev/null +++ b/docs/API_KEY_OPTIMIZATION.md @@ -0,0 +1,399 @@ +# API Key Management 優化計畫 + +| 項目 | 內容 | +|------|------| +| 版本 | V1.0 | +| 日期 | 2026-03-21 | +| 狀態 | 規劃中 | + +--- + +## 版本歷史 + +| 版本 | 日期 | 目的 | 操作人 | 工具/模型 | +|------|------|------|--------|-----------| +| V1.0 | 2026-03-21 | 創建優化計畫 | OpenCode | - | + +--- + +## 任務編碼規則 + +``` +AKO-{類別}-{序號} +AKO = API Key Optimization +類別: + - CODE = 程式碼品質 + - PERF = 效能優化 + - SEC = 安全性 + - FEAT = 功能增強 + - DOC = 文件 +``` + +--- + +## Phase 1: 程式碼品質 (CODE) + +| 編碼 | 任務 | 描述 | 優先級 | 預估工時 | 狀態 | +|------|------|------|--------|----------|------| +| AKO-CODE-01 | 修復 from_str 警告 | 重命名為 `parse_scope` 或實作 `FromStr` trait | 🔴 高 | 0.5h | ⏳ 待辦 | +| AKO-CODE-02 | 函數參數重構 | 使用 Config struct 減少參數數量 | 🔴 高 | 1h | ⏳ 待辦 | +| AKO-CODE-03 | 抽象 CRUD Trait | 建立 `ExternalTokenStore` trait 統一 Gitea/n8n | 🟡 中 | 3h | ⏳ 待辦 | +| AKO-CODE-04 | 錯誤處理統一 | 使用 `thiserror` 定義自訂錯誤類型 | 🟡 中 | 2h | ⏳ 待辦 | + +### AKO-CODE-01 細節 + +```rust +// Before +impl GiteaScope { + pub fn from_str(s: &str) -> Option { ... } +} + +// After: Option A - Rename +impl GiteaScope { + pub fn parse(s: &str) -> Option { ... } +} + +// After: Option B - Implement FromStr +impl std::str::FromStr for GiteaScope { + type Err = (); + fn from_str(s: &str) -> Result { ... } +} +``` + +### AKO-CODE-02 細節 + +```rust +// Before +pub async fn create_api_key( + &self, + key_id: &str, + key_hash: &str, + key_prefix: &str, + name: &str, + key_type: &str, + user_id: Option, + service_name: Option<&str>, + permissions: &serde_json::Value, + expires_at: Option>, +) -> Result + +// After +pub struct CreateApiKeyConfig<'a> { + pub key_id: &'a str, + pub key_hash: &'a str, + pub key_prefix: &'a str, + pub name: &'a str, + pub key_type: &'a str, + pub user_id: Option, + pub service_name: Option<&'a str>, + pub permissions: &'a serde_json::Value, + pub expires_at: Option>, +} + +pub async fn create_api_key(&self, config: CreateApiKeyConfig<'_>) -> Result +``` + +### AKO-CODE-03 細節 + +```rust +#[async_trait] +pub trait ExternalTokenStore { + async fn create(&self, record: T) -> Result; + async fn get_by_label(&self, label: &str) -> Result>; + async fn list(&self) -> Result>; + async fn delete(&self, label: &str) -> Result<()>; + async fn update_verification(&self, label: &str) -> Result<()>; +} +``` + +--- + +## Phase 2: 效能優化 (PERF) + +| 編碼 | 任務 | 描述 | 優先級 | 預估工時 | 狀態 | +|------|------|------|--------|----------|------| +| AKO-PERF-01 | 連線池配置外部化 | 使用環境變數控制 max_connections | 🟡 中 | 0.5h | ⏳ 待辦 | +| AKO-PERF-02 | API Key 驗證快取 | 使用 Moka 快取減少資料庫查詢 | 🔴 高 | 2h | ⏳ 待辦 | +| AKO-PERF-03 | 批次查詢優化 | 合併多次查詢為單一 SQL | 🟡 中 | 1h | ⏳ 待辦 | +| AKO-PERF-04 | 非同步日誌寫入 | 使用 channel 非同步寫入審計日誌 | 🟢 低 | 2h | ⏳ 待辦 | + +### AKO-PERF-01 細節 + +```rust +// Before +let pool_options = PgPoolOptions::new() + .max_connections(10) + .acquire_timeout(std::time::Duration::from_secs(60)); + +// After +let max_conn = std::env::var("DB_MAX_CONNECTIONS") + .unwrap_or_else(|_| "10".to_string()) + .parse() + .unwrap_or(10); + +let pool_options = PgPoolOptions::new() + .max_connections(max_conn) + .acquire_timeout(std::time::Duration::from_secs(60)); +``` + +### AKO-PERF-02 細節 + +```rust +use moka::future::Cache; +use std::time::Duration; + +pub struct ApiKeyCache { + cache: Cache, +} + +pub struct CachedApiKey { + pub record: ApiKeyRecord, + pub cached_at: chrono::DateTime, +} + +impl ApiKeyCache { + pub fn new(ttl_seconds: u64, max_capacity: u64) -> Self { + Self { + cache: Cache::builder() + .time_to_live(Duration::from_secs(ttl_seconds)) + .max_capacity(max_capacity) + .build(), + } + } + + pub async fn get(&self, key_hash: &str) -> Option { + self.cache.get(key_hash).await.map(|c| c.record) + } + + pub async fn insert(&self, key_hash: String, record: ApiKeyRecord) { + self.cache.insert(key_hash, CachedApiKey { + record, + cached_at: chrono::Utc::now(), + }).await; + } + + pub async fn invalidate(&self, key_hash: &str) { + self.cache.invalidate(key_hash).await; + } +} +``` + +--- + +## Phase 3: 安全性 (SEC) + +| 編碼 | 任務 | 描述 | 優先級 | 預估工時 | 狀態 | +|------|------|------|--------|----------|------| +| AKO-SEC-01 | Constant-time 比較 | 使用 `subtle` crate 防止 timing attack | 🔴 高 | 0.5h | ⏳ 待辦 | +| AKO-SEC-02 | Rate Limiter | 限制驗證失敗重試次數 | 🔴 高 | 2h | ⏳ 待辦 | +| AKO-SEC-03 | IP 黑名單 | 支援封鎖特定 IP | 🟡 中 | 1.5h | ⏳ 待辦 | +| AKO-SEC-04 | 審計日誌加密 | 敏感欄位加密儲存 | 🟡 中 | 2h | ⏳ 待辦 | +| AKO-SEC-05 | Key 強度檢查 | 驗證建立的 Key 符合強度要求 | 🟢 低 | 1h | ⏳ 待辦 | + +### AKO-SEC-01 細節 + +```rust +use subtle::ConstantTimeEq; + +// Before +if stored_hash == computed_hash { + // valid +} + +// After +if bool::from(stored_hash.as_bytes().ct_eq(computed_hash.as_bytes())) { + // valid +} +``` + +### AKO-SEC-02 細節 + +```rust +use moka::future::Cache; + +pub struct RateLimiter { + attempts: Cache, + max_attempts: u32, + window_seconds: u64, +} + +pub struct AttemptInfo { + pub count: u32, + pub first_attempt: chrono::DateTime, + pub locked_until: Option>, +} + +impl RateLimiter { + pub async fn check(&self, identifier: &str) -> Result<()> { + if let Some(info) = self.attempts.get(identifier).await { + if let Some(locked_until) = info.locked_until { + if chrono::Utc::now() < locked_until { + anyhow::bail!("Account locked until {}", locked_until); + } + } + } + Ok(()) + } + + pub async fn record_failure(&self, identifier: &str) -> Result<()> { + let mut info = self.attempts.get(identifier).await + .unwrap_or(AttemptInfo { + count: 0, + first_attempt: chrono::Utc::now(), + locked_until: None, + }); + + info.count += 1; + + if info.count >= self.max_attempts { + info.locked_until = Some( + chrono::Utc::now() + chrono::Duration::seconds(self.window_seconds as i64) + ); + } + + self.attempts.insert(identifier.to_string(), info).await; + Ok(()) + } + + pub async fn record_success(&self, identifier: &str) { + self.attempts.invalidate(identifier).await; + } +} +``` + +--- + +## Phase 4: 功能增強 (FEAT) + +| 編碼 | 任務 | 描述 | 優先級 | 預估工時 | 狀態 | +|------|------|------|--------|----------|------| +| AKO-FEAT-01 | 批量建立 Key | 支援 JSON 檔案批量匯入 | 🟡 中 | 3h | ⏳ 待辦 | +| AKO-FEAT-02 | 批量撤銷 Key | 支援條件式批量撤銷 | 🟡 中 | 2h | ⏳ 待辦 | +| AKO-FEAT-03 | Key 匯出 | 匯出 Key 列表(不含明文) | 🟢 低 | 1.5h | ⏳ 待辦 | +| AKO-FEAT-04 | Key 匯入 | 匯入 Key 元數據 | 🟢 低 | 1.5h | ⏳ 待辦 | +| AKO-FEAT-05 | Webhook 通知 | 異常發生時發送 Webhook | 🟡 中 | 3h | ⏳ 待辦 | +| AKO-FEAT-06 | Email 通知 | Key 到期前提醒 | 🟢 低 | 4h | ⏳ 待辦 | +| AKO-FEAT-07 | 統計報表 | 生成使用統計報表 | 🟢 低 | 2h | ⏳ 待辦 | +| AKO-FEAT-08 | 清理過期記錄 | 自動清理過期的 Key 記錄 | 🟢 低 | 1h | ⏳ 待辦 | + +### AKO-FEAT-01 細節 + +```json +// keys.json +{ + "keys": [ + { + "name": "ci-service-1", + "key_type": "service", + "permissions": ["read", "write"], + "ttl_days": 90 + }, + { + "name": "ci-service-2", + "key_type": "service", + "permissions": ["read"], + "ttl_days": 180 + } + ] +} +``` + +```bash +momentry api-key batch-create --file keys.json +``` + +### AKO-FEAT-05 細節 + +```rust +pub struct WebhookConfig { + pub url: String, + pub secret: String, + pub events: Vec, +} + +pub enum WebhookEvent { + KeyCreated, + KeyRevoked, + KeyExpired, + AnomalyDetected, + RotationRequired, +} + +pub struct WebhookNotifier { + client: Client, + config: WebhookConfig, +} + +impl WebhookNotifier { + pub async fn notify(&self, event: WebhookEvent, payload: serde_json::Value) -> Result<()> { + if !self.config.events.contains(&event) { + return Ok(()); + } + + let signature = self.sign(&payload); + + self.client.post(&self.config.url) + .header("X-Webhook-Signature", signature) + .json(&serde_json::json!({ + "event": event, + "timestamp": chrono::Utc::now(), + "payload": payload, + })) + .send() + .await?; + + Ok(()) + } +} +``` + +--- + +## Phase 5: 文件 (DOC) + +| 編碼 | 任務 | 描述 | 優先級 | 預估工時 | 狀態 | +|------|------|------|--------|----------|------| +| AKO-DOC-01 | API 文件自動生成 | 使用 `utoipa` 生成 OpenAPI | 🟢 低 | 3h | ⏳ 待辦 | +| AKO-DOC-02 | CHANGELOG.md | 建立變更日誌 | 🟢 低 | 1h | ⏳ 待辦 | +| AKO-DOC-03 | 架構圖 | 添加系統架構圖 | 🟢 低 | 2h | ⏳ 待辦 | +| AKO-DOC-04 | 整合測試文件 | 記錄整合測試流程 | 🟢 低 | 1h | ⏳ 待辦 | + +--- + +## 總工時估算 + +| Phase | 工時 | 任務數 | +|-------|------|--------| +| CODE | 6.5h | 4 | +| PERF | 5.5h | 4 | +| SEC | 7h | 5 | +| FEAT | 18h | 8 | +| DOC | 7h | 4 | +| **總計** | **44h** | **25** | + +--- + +## 環境變數 + +```bash +# 效能 +DB_MAX_CONNECTIONS=10 +CACHE_TTL_SECONDS=300 +CACHE_MAX_CAPACITY=10000 + +# 安全 +RATE_LIMIT_MAX_ATTEMPTS=5 +RATE_LIMIT_WINDOW_SECONDS=900 + +# 通知 +WEBHOOK_URL=https://example.com/webhook +WEBHOOK_SECRET=your-secret +``` + +--- + +## 參考文件 + +- `docs/API_KEY_MANAGEMENT.md` - API Key 管理系統設計 +- `docs/PENDING_ISSUES.md` - 待解決問題追蹤 +- `src/core/api_key/` - API Key 模組 diff --git a/docs/API_N8N_GUIDE.md b/docs/API_N8N_GUIDE.md new file mode 100644 index 0000000..b5a7402 --- /dev/null +++ b/docs/API_N8N_GUIDE.md @@ -0,0 +1,193 @@ +# n8n 呼叫 Momentry API 指南 + +| 項目 | 內容 | +|------|------| +| 版本 | V1.0 | +| 日期 | 2026-03-23 | +| 用途 | 在 n8n workflow 中呼叫 Momentry API | + +--- + +## API URL + +在 n8n HTTP Request Node 中,**請使用外部 URL**: + +``` +https://api.momentry.ddns.net +``` + +> ⚠️ **不要使用** `localhost:3002`,因為 n8n 需要從外部訪問 API。 + +--- + +## 常用端點 + +| 方法 | 端點 | 說明 | +|------|------|------| +| GET | `/health` | 健康檢查 | +| POST | `/api/v1/n8n/search` | 語意搜尋(推薦) | +| GET | `/api/v1/videos` | 列出所有影片 | +| GET | `/api/v1/lookup` | 查詢影片 | +| GET | `/api/v1/progress/:uuid` | 處理進度 | + +--- + +## HTTP Request Node 設定 + +### 語意搜尋(推薦) + +``` +Node: HTTP Request +├── URL: https://api.momentry.ddns.net/api/v1/n8n/search +├── Method: POST +├── Authentication: None +├── Send Body: ✓ (checked) +├── Content Type: JSON +└── Body: + { + "query": "={{ $json.query }}", + "limit": "={{ $json.limit || 10 }}" + } +``` + +### 測試用(固定關鍵字) + +``` +Node: HTTP Request +├── URL: https://api.momentry.ddns.net/api/v1/n8n/search +├── Method: POST +├── Send Body: ✓ +├── Content Type: JSON +└── Body: + { + "query": "charade", + "limit": 3 + } +``` + +--- + +## 完整 Workflow 範例 + +### 基本搜尋 Workflow + +```json +{ + "name": "Momentry Video Search", + "nodes": [ + { + "parameters": {}, + "name": "Manual Trigger", + "type": "n8n-nodes-base.manualTrigger", + "position": [250, 300] + }, + { + "parameters": { + "url": "https://api.momentry.ddns.net/api/v1/n8n/search", + "method": "POST", + "sendBody": true, + "contentType": "json", + "body": { + "query": "charade", + "limit": 3 + } + }, + "name": "Search Video API", + "type": "n8n-nodes-base.httpRequest", + "position": [450, 300] + } + ], + "connections": { + "Manual Trigger": { + "main": [[{"node": "Search Video API"}]] + } + } +} +``` + +--- + +## 動態查詢 Workflow + +```json +{ + "name": "Momentry Dynamic Search", + "nodes": [ + { + "parameters": { + "httpMethod": "POST", + "path": "search", + "responseMode": "lastNode" + }, + "name": "Webhook", + "type": "n8n-nodes-base.webhook", + "position": [250, 300] + }, + { + "parameters": { + "url": "https://api.momentry.ddns.net/api/v1/n8n/search", + "method": "POST", + "sendBody": true, + "contentType": "json", + "body": { + "query": "={{ JSON.stringify($json.body.query) }}", + "limit": "={{ $json.body.limit || 5 }}" + } + }, + "name": "Search API", + "type": "n8n-nodes-base.httpRequest", + "position": [450, 300] + } + ], + "connections": { + "Webhook": { + "main": [[{"node": "Search API"}]] + } + } +} +``` + +--- + +## 常見錯誤 + +### 錯誤: 502 Bad Gateway + +**原因**: API 服務未啟動 + +**解決**: +```bash +sudo launchctl load /Library/LaunchDaemons/com.momentry.api.plist +``` + +### 錯誤: "Your request is invalid" + +**原因**: Body 格式設定錯誤 + +**正確設定**: +- `Content Type`: JSON +- `Body`: 必須是有效的 JSON 物件 + +--- + +## curl 測試 + +在終端機中測試 API: + +```bash +# 健康檢查 +curl https://api.momentry.ddns.net/health + +# 搜尋測試 +curl -X POST https://api.momentry.ddns.net/api/v1/n8n/search \ + -H "Content-Type: application/json" \ + -d '{"query":"charade","limit":3}' +``` + +--- + +## 相關文件 + +- [API_INDEX.md](./API_INDEX.md) - 文件總覽 +- [API_ENDPOINTS.md](./API_ENDPOINTS.md) - 端點完整說明 +- [N8N_HTTP_REQUEST_GUIDE.md](./N8N_HTTP_REQUEST_GUIDE.md) - HTTP Request 詳細設定 diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md new file mode 100644 index 0000000..835226a --- /dev/null +++ b/docs/API_REFERENCE.md @@ -0,0 +1,447 @@ +# Momentry Core API 安裝指南 + +| 項目 | 內容 | +|------|------| +| 建立者 | Warren | +| 建立時間 | 2026-03-18 | +| 文件版本 | V1.0 | + +--- + +## 版本歷史 + +| 版本 | 日期 | 目的 | 操作人 | 工具/模型 | +|------|------|------|--------|-----------| +| V1.0 | 2026-03-18 | 創建文件 | Warren | OpenCode / MiniMax M2.5 | +| V1.1 | 2026-03-23 | 更新端點與實際一致 | OpenCode | - | + +--- + +## Base URL + +| 環境 | URL | 說明 | +|------|-----|------| +| **本地開發** | `http://localhost:3002` | 直接訪問 API,繞過反向代理 | +| **外部訪問** | `https://api.momentry.ddns.net` | 通過 Caddy 反向代理訪問,需網路可達 | + +> **Note:** Port 3000 is used by Gitea. Momentry API server runs on **port 3002**. + +### URL 使用時機 + +| 情境 | 建議 URL | +|------|----------| +| 本地開發/測試 | `http://localhost:3002` | +| n8n workflow | `https://api.momentry.ddns.net` | +| 外部系統整合 | `https://api.momentry.ddns.net` | +| 反向代理有問題時 | `http://localhost:3002` (繞過代理) | + +## Authentication + +Currently no authentication is required. + +--- + +## Endpoints + +### 1. Register Video +Register a video file to the system. + +**Endpoint:** `POST /api/v1/register` + +**Request Body:** +```json +{ + "path": "/path/to/video.mp4" +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `path` | string | Yes | Absolute path to video file | + +**Response (200):** +```json +{ + "uuid": "5dea6618a606e7c7", + "video_id": 1, + "file_name": "video.mp4", + "duration": 120.5, + "width": 1920, + "height": 1080 +} +``` + +**Example:** +```bash +curl -X POST http://localhost:3002/api/v1/register \ + -H "Content-Type: application/json" \ + -d '{"path": "/Users/accusys/test_video/BigBuckBunny_320x180.mp4"}' +``` + +--- + +### 2. Process Video (CLI) +Process video to generate ASR, CUT, YOLO, OCR, Face, Pose data. + +**Note:** This is a CLI command, not an HTTP endpoint. + +```bash +# Process video by UUID +cargo run --bin momentry -- process 5dea6618a606e7c7 + +# Or process by file path +cargo run --bin momentry -- process /path/to/video.mp4 +``` + +--- + +### 3. Get Progress +Get real-time processing progress via Redis. + +**Endpoint:** `GET /api/v1/progress/:uuid` + +| Parameter | Type | Description | +|-----------|------|-------------| +| `uuid` | path | Video UUID (16 characters) | + +**Response (200):** +```json +{ + "uuid": "5dea6618a606e7c7", + "processors": [ + { + "name": "asr", + "status": "complete", + "current": 0, + "total": 0, + "message": "7 segments" + }, + { + "name": "cut", + "status": "complete", + "current": 134, + "total": 134, + "message": "134 scenes" + }, + { + "name": "yolo", + "status": "progress", + "current": 5000, + "total": 14315, + "message": "frame 5000" + }, + { + "name": "ocr", + "status": "pending", + "current": 0, + "total": 0, + "message": "" + } + ] +} +``` + +**Processor Status Values:** +- `pending` - Not started +- `info` - Starting/info message +- `progress` - In progress +- `complete` - Finished +- `error` - Failed + +**Example:** +```bash +# Get progress for specific video +curl http://localhost:3002/api/v1/progress/5dea6618a606e7c7 +``` + +--- + +### 4. Natural Language Search +Search video chunks using natural language queries (RAG). + +**Endpoint:** `POST /api/v1/search` + +**Request Body:** +```json +{ + "query": "What is the person saying about machine learning?", + "limit": 10, + "uuid": "5dea6618a606e7c7" +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `query` | string | Yes | Natural language search query | +| `limit` | integer | No | Max results (default: 10) | +| `uuid` | string | No | Filter by specific video UUID | + +**Response (200):** +```json +{ + "results": [ + { + "uuid": "5dea6618a606e7c7", + "chunk_id": "0", + "chunk_type": "sentence", + "start_time": 5.5, + "end_time": 8.2, + "text": "Machine learning is a subset of artificial intelligence...", + "score": 0.85 + } + ], + "query": "What is the person saying about machine learning?" +} +``` + +**Example:** +```bash +curl -X POST http://localhost:3002/api/v1/search \ + -H "Content-Type: application/json" \ + -d '{"query": "machine learning", "limit": 5}' +``` + +--- + +### 4a. N8N Search (n8n Workflow Integration) +N8n-compatible search endpoint with standardized response format for direct workflow integration. + +**Endpoint:** `POST /api/v1/n8n/search` + +**Request Body:** +```json +{ + "query": "sunset", + "limit": 10, + "uuid": "5dea6618a606e7c7" +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `query` | string | Yes | Natural language search query | +| `limit` | integer | No | Max results (default: 10) | +| `uuid` | string | No | Filter by specific video UUID | + +**Response (200):** +```json +{ + "query": "sunset", + "count": 2, + "hits": [ + { + "id": "c_001", + "vid": "5dea6618a606e7c7", + "start": 5.5, + "end": 8.2, + "title": "Sunset Scene", + "text": "The sun slowly sets over the ocean...", + "score": 0.92, + "media_url": "https://wp.momentry.ddns.net/video.mp4" + } + ] +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `query` | string | Original search query | +| `count` | integer | Number of results | +| `hits[].id` | string | Chunk ID | +| `hits[].vid` | string | Video UUID | +| `hits[].start` | number | Start time in seconds | +| `hits[].end` | number | End time in seconds | +| `hits[].title` | string | Chunk title (from metadata or auto-generated) | +| `hits[].text` | string | Text content | +| `hits[].score` | number | Relevance score (0-1) | +| `hits[].media_url` | string | Full media URL (optional) | + +**Example:** +```bash +curl -X POST http://localhost:3002/api/v1/n8n/search \ + -H "Content-Type: application/json" \ + -d '{"query": "sunset", "limit": 5}' +``` + +**Environment Variables:** +| Variable | Default | Description | +|----------|---------|-------------| +| `MOMENTRY_MEDIA_BASE_URL` | `https://wp.momentry.ddns.net` | Base URL for constructing media URLs | + +--- + +### 5. Lookup Video +Lookup video UUID by path or get video details by UUID. + +**Endpoint:** `GET /api/v1/lookup` + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `path` | query | No* | Video file path | +| `uuid` | query | No* | Video UUID | + +*One of `path` or `uuid` is required. + +**Response (200):** +```json +{ + "uuid": "5dea6618a606e7c7", + "file_path": "/path/to/video.mp4", + "file_name": "video.mp4", + "duration": 120.5 +} +``` + +**Example:** +```bash +# Lookup by path +curl "http://localhost:3002/api/v1/lookup?path=/path/to/video.mp4" + +# Lookup by UUID +curl "http://localhost:3002/api/v1/lookup?uuid=5dea6618a606e7c7" +``` + +--- + +### 6. List Videos +List all registered videos. + +**Endpoint:** `GET /api/v1/videos` + +**Response (200):** +```json +{ + "videos": [ + { + "uuid": "5dea6618a606e7c7", + "file_path": "/path/to/video.mp4", + "file_name": "video.mp4", + "duration": 120.5, + "width": 1920, + "height": 1080 + } + ] +} +``` + +**Example:** +```bash +curl http://localhost:3002/api/v1/videos +``` + +--- + +## Data Flow + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ 完整工作流程 │ +└─────────────────────────────────────────────────────────────────────┘ + +1. Register Video + POST /api/v1/register + └── UUID: 5dea6618a606e7c7 + +2. Process Video (CLI) + cargo run -- process 5dea6618a606e7c7 + ├── ASR (WhisperX) → 7 segments + ├── CUT (PySceneDetect) → 134 scenes + ├── YOLO (YOLOv8) → 10483 frames with objects + ├── OCR (EasyOCR) → 40 frames with text + ├── Face (OpenCV) → 44 frames with faces + └── Pose (YOLOv8-Pose) → 14315 frames + +3. Monitor Progress (Real-time) + GET /api/v1/progress/:uuid + └── Redis Pub/Sub + Hash + +4. Chunk (CLI) + cargo run -- chunk 5dea6618a606e7c7 + └── Create chunks in database + +5. Vectorize (CLI) + cargo run -- vectorize 5dea6618a606e7c7 + └── Generate embeddings in Qdrant + +6. Search (API) + POST /api/v1/search + └── Natural language query +``` + +--- + +## Processor Reference + +| Processor | Model | Description | +|-----------|-------|-------------| +| **ASR** | WhisperX (faster-whisper) | Speech recognition + diarization | +| **CUT** | PySceneDetect | Scene detection/segmentation | +| **ASRX** | WhisperX | Speaker diarization | +| **YOLO** | YOLOv8n | Object detection | +| **OCR** | EasyOCR | Text recognition | +| **Face** | OpenCV Haar Cascade | Face detection | +| **Pose** | YOLOv8n-Pose | Pose estimation | + +--- + +## Error Responses + +**400 Bad Request** +```json +{ + "error": "Invalid request body" +} +``` + +**404 Not Found** +```json +{ + "error": "Resource not found" +} +``` + +**500 Internal Server Error** +```json +{ + "error": "Internal server error" +} +``` + +--- + +## Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `DATABASE_URL` | `postgres://accusys@localhost:5432/momentry` | PostgreSQL connection | +| `REDIS_URL` | `redis://localhost:6379` | Redis connection | +| `REDIS_PASSWORD` | `accusys` | Redis password | +| `QDRANT_URL` | `http://localhost:6333` | Qdrant vector DB URL | +| `QDRANT_API_KEY` | - | Qdrant API key | +| `QDRANT_COLLECTION` | `chunks` | Qdrant collection name | +| `MOMENTRY_MEDIA_BASE_URL` | `https://wp.momentry.ddns.net` | Base URL for n8n search media URLs | + +--- + +## Starting the Server + +```bash +# Default (port 3002, since 3000 is Gitea) +cargo run --bin momentry -- server + +# Custom host and port +cargo run --bin momentry -- server --host 127.0.0.1 --port 3002 +``` + +--- + +## Quick Reference + +| Task | Command | +|------|---------| +| Register video | `POST /api/v1/register` | +| Process video | `cargo run -- process ` | +| Check progress | `GET /api/v1/progress/` | +| Search | `POST /api/v1/search` | +| List videos | `GET /api/v1/videos` | +| Lookup | `GET /api/v1/lookup?uuid=` | diff --git a/docs/API_WORDPRESS_GUIDE.md b/docs/API_WORDPRESS_GUIDE.md new file mode 100644 index 0000000..f91146a --- /dev/null +++ b/docs/API_WORDPRESS_GUIDE.md @@ -0,0 +1,270 @@ +# WordPress 呼叫 Momentry API 指南 + +| 項目 | 內容 | +|------|------| +| 版本 | V1.0 | +| 日期 | 2026-03-23 | +| 用途 | 在 WordPress 中呼叫 Momentry API | + +--- + +## API URL + +在 WordPress 中呼叫 API,**請使用外部 URL**: + +``` +https://api.momentry.ddns.net +``` + +> ⚠️ WordPress 運行於瀏覽器端,無法直接訪問 `localhost`。 + +--- + +## 常用端點 + +| 方法 | 端點 | 說明 | +|------|------|------| +| GET | `/health` | 健康檢查 | +| POST | `/api/v1/search` | 語意搜尋(標準格式) | +| GET | `/api/v1/videos` | 列出所有影片 | +| GET | `/api/v1/lookup` | 查詢影片 | + +--- + +## PHP 範例 + +### 基本搜尋 + +```php + 'charade', + 'limit' => 10 +]; + +$response = wp_remote_post($api_url, [ + 'headers' => ['Content-Type' => 'application/json'], + 'body' => json_encode($data), + 'timeout' => 30 +]); + +if (is_wp_error($response)) { + echo '錯誤: ' . $response->get_error_message(); +} else { + $body = json_decode(wp_remote_retrieve_body($response), true); + print_r($body['hits']); +} +?> +``` + +### 列出所有影片 + +```php + 30]); + +if (!is_wp_error($response)) { + $body = json_decode(wp_remote_retrieve_body($response), true); + foreach ($body['videos'] as $video) { + echo $video['file_name'] . "\n"; + } +} +?> +``` + +### 查詢特定影片 + +```php + 30]); + +if (!is_wp_error($response)) { + $video = json_decode(wp_remote_retrieve_body($response), true); + echo '檔案: ' . $video['file_name'] . "\n"; + echo '時長: ' . $video['duration'] . ' 秒'; +} +?> +``` + +--- + +## JavaScript 範例 + +### 使用 fetch + +```javascript +// 搜尋影片 +async function searchVideos(query, limit = 10) { + const response = await fetch('https://api.momentry.ddns.net/api/v1/n8n/search', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ query, limit }) + }); + + if (!response.ok) { + throw new Error('API 請求失敗'); + } + + return await response.json(); +} + +// 使用範例 +searchVideos('charade', 5) + .then(data => { + data.hits.forEach(hit => { + console.log(`${hit.text} (score: ${hit.score})`); + }); + }); +``` + +--- + +## WordPress Shortcode 範例 + +在 `functions.php` 中註冊短碼: + +```php + '', + 'limit' => '10' + ], $atts); + + if (empty($atts['query'])) { + return '

請提供搜尋關鍵字

'; + } + + $response = wp_remote_post('https://api.momentry.ddns.net/api/v1/n8n/search', [ + 'headers' => ['Content-Type' => 'application/json'], + 'body' => json_encode([ + 'query' => $atts['query'], + 'limit' => (int)$atts['limit'] + ]), + 'timeout' => 30 + ]); + + if (is_wp_error($response)) { + return '

搜尋服務暫時無法使用

'; + } + + $data = json_decode(wp_remote_retrieve_body($response), true); + + if (empty($data['hits'])) { + return '

找不到相關結果

'; + } + + $output = '
    '; + foreach ($data['hits'] as $hit) { + $output .= sprintf( + '
  • %s 播放
  • ', + esc_html($hit['text']), + $hit['media_url'], + $hit['start'] + ); + } + $output .= '
'; + + return $output; +}); +?> +``` + +**使用方式**: +``` +[momentry_search query="charade" limit="5"] +``` + +--- + +## REST API Endpoint (WP >= 5.0) + +在 WordPress REST API 中註冊自定義端點: + +```php + 'POST', + 'callback' => function($request) { + $response = wp_remote_post( + 'https://api.momentry.ddns.net/api/v1/n8n/search', + [ + 'headers' => ['Content-Type' => 'application/json'], + 'body' => json_encode([ + 'query' => $request->get_param('query'), + 'limit' => $request->get_param('limit', 10) + ]) + ] + ); + + if (is_wp_error($response)) { + return new WP_Error('api_error', 'API 請求失敗'); + } + + return json_decode(wp_remote_retrieve_body($response)); + } + ]); +}); +?> +``` + +**呼叫方式**: +``` +POST /wp-json/momentry/v1/search +Body: {"query": "charade", "limit": 5} +``` + +--- + +## 常見錯誤 + +### 錯誤: cURL error 7 + +**原因**: 無法連接到 API + +**檢查**: +1. API 服務是否啟動 +2. 網路是否可達 + +### 錯誤: 502 Bad Gateway + +**原因**: API 服務未啟動 + +**解決**: +```bash +sudo launchctl load /Library/LaunchDaemons/com.momentry.api.plist +``` + +--- + +## curl 測試 + +在終端機中測試: + +```bash +# 健康檢查 +curl https://api.momentry.ddns.net/health + +# 搜尋測試 +curl -X POST https://api.momentry.ddns.net/api/v1/n8n/search \ + -H "Content-Type: application/json" \ + -d '{"query":"charade","limit":5}' +``` + +--- + +## 相關文件 + +- [API_INDEX.md](./API_INDEX.md) - 文件總覽 +- [API_ENDPOINTS.md](./API_ENDPOINTS.md) - 端點完整說明 +- [API_N8N_GUIDE.md](./API_N8N_GUIDE.md) - n8n 使用範例 diff --git a/docs/ARCHITECTURE_EVALUATION.md b/docs/ARCHITECTURE_EVALUATION.md new file mode 100644 index 0000000..ca95826 --- /dev/null +++ b/docs/ARCHITECTURE_EVALUATION.md @@ -0,0 +1,331 @@ +# 架構優化待評估事項 + +| 項目 | 內容 | +|------|------| +| 建立者 | OpenCode | +| 建立時間 | 2026-03-21 | +| 文件版本 | V1.0 | + +--- + +## 版本歷史 + +| 版本 | 日期 | 目的 | 操作人 | +|------|------|------|--------| +| V1.0 | 2026-03-21 | 創建文件 | OpenCode | +| V1.1 | 2026-03-22 | 新增 TigerGraph/GraphRAG 說故事評估 | OpenCode | + +--- + +## 架構優化項目 + +### 1. PostgreSQL → Redis 故障轉移 + +**說明**: 當 PostgreSQL 不可用時,降級到 Redis 作為臨時存儲 + +**複雜度**: 中 + +**影響範圍**: +- `src/core/db/postgres_db.rs` +- `src/core/db/redis_client.rs` + +**風險**: +- 數據一致性問題 +- 需要定義轉移策略 + +**優先級**: 待評估 + +--- + +### 2. 連接池監控 + +**說明**: 添加 PostgreSQL 和 Redis 連接池指標到 Prometheus + +**複雜度**: 低 + +**影響範圍**: +- `src/core/db/postgres_db.rs` +- `src/core/db/redis_client.rs` +- `src/api/` (新增 metrics endpoint) + +**風險**: 低 + +**優先級**: 待評估 + +--- + +### 3. Processor 重試機制 + +**說明**: 當 processor 失敗時自動重試 + +**複雜度**: 中 + +**影響範圍**: +- `src/core/processor/executor.rs` (新增 `run_with_retry` 方法) +- `src/core/processor/mod.rs` (導出 `RetryConfig`) + +**風險**: +- 無限重試風險 → 已通過 `max_attempts` 控制 +- 需要指數退避 → 已實現 + +**優先級**: ✅ 已完成 (2026-03-21) + +**實作內容**: +- `RetryConfig` 結構體 (可配置重試次數、初始延遲、最大延遲、退避倍數) +- `run_with_retry()` 方法 (自動重試 + 指數退避) +- 單元測試覆蓋 + +**使用範例**: +```rust +use crate::core::processor::{PythonExecutor, RetryConfig}; + +let executor = PythonExecutor::new()?; +let config = RetryConfig::new(3).with_delay(1000).with_max_delay(30000); + +executor.run_with_retry( + "asr_processor.py", + &["--input", "/path/to/video"], + Some(&uuid), + "asr", + Some(Duration::from_secs(3600)), + Some(config), +).await?; +``` + +--- + +### 4. PyO3 整合 + +**說明**: Python/Rust 直接調用,移除子進程調用 + +**複雜度**: 高 + +**影響範圍**: +- `src/core/processor/executor.rs` (重寫) +- Python 模組 (修改為可直接 import) + +**風險**: +- Python GIL 問題 +- 依賴版本兼容性 +- 需要大量重寫 + +**優先級**: 低 (長期目標) + +--- + +### 5. HTTP 健康端點 + +**說明**: 添加 `/health` API 用於外部監控 + +**複雜度**: 低 + +**影響範圍**: +- `src/api/server.rs` (新增路由) + +**風險**: 低 + +**優先級**: ✅ 已完成 (2026-03-21) + +**實作內容**: +- `GET /health` - 基本健康檢查 (status, version, uptime) +- `GET /health/detailed` - 詳細健康檢查 (PostgreSQL, Redis, Qdrant 狀態和延遲) + +--- + +### 6. Gitea Actions CI/CD + +**說明**: 配置 Gitea Actions 自動化 CI/CD,在合併前執行檢查 + +**複雜度**: 中 + +**影響範圍**: +- `.gitea/workflows/` (新增 workflow 文件) + +**優點**: +- 強制執行檢查,無法跳過 +- 跨設備一致 +- PR 審查前自動檢查 + +**風險**: 低 + +**優先級**: 待評估 + +--- + +### 7. Commit Message Lint + +**說明**: 規範化提交訊息格式 (Conventional Commits) + +**複雜度**: 低 + +**影響範圍**: +- `.git/hooks/commit-msg` (新增 hook) +- `~/dotfiles/hooks/commit-msg` + +**風險**: 低 + +**優先級**: ✅ 已完成 (2026-03-21) + +**實作內容**: +- 驗證格式: `(): ` +- 有效類型: feat, fix, docs, style, refactor, test, chore, perf, ci, build, revert +- 警告: 第一行超過 72 字符 + +**範例**: +``` +feat(api): add health check endpoint +fix(db): resolve connection pool issue +docs: update README +``` + +--- + +### 8. 自動化安裝腳本 + +**說明**: 創建腳本一次安裝所有開發工具 + +**複雜度**: 低 + +**影響範圍**: +- `scripts/install-dev-tools.sh` (新增) + +**風險**: 低 + +**優先級**: 待評估 + +--- + +## 評估標準 + +| 標準 | 說明 | +|------|------| +| 業務價值 | 對用戶有何幫助 | +| 技術風險 | 實現難度和潛在問題 | +| 維護成本 | 未來維護負擔 | +| 依賴性 | 對其他系統的影響 | + +--- + +## 評估記錄 + +| 項目 | 評估日期 | 決策 | 原因 | +|------|----------|------|------| +| PostgreSQL → Redis 故障轉移 | 待評估 | - | - | +| 連接池監控 | 待評估 | - | - | +| Processor 重試機制 | 2026-03-21 | 已完成 | - | +| PyO3 整合 | 待評估 | - | - | +| HTTP 健康端點 | 2026-03-21 | 已完成 | - | +| Gitea Actions CI/CD | 待評估 | - | - | +| Commit Message Lint | 2026-03-21 | 已完成 | - | +| 自動化安裝腳本 | 待評估 | - | - | + +--- + +## 9. TigerGraph / Knowledge Graph 圖譜說故事 + +**說明**: 使用知識圖譜 (Knowledge Graph) 增強視頻敘事 (Storytelling) 和 RAG 檢索 + +**複雜度**: 高 + +**研究來源**: +- [TigerGraph Agentic GraphRAG](https://www.tigergraph.com/blog/agentic-graphrag-gives-ai-a-playbook-for-smarter-retrieval/) (2025-12-15) +- [TigerGraph GraphRAG GitHub](https://github.com/tigergraph/graphrag) (v1.2.0, 2026-03-11) +- [GraphRAG in 2026: Practitioner's Guide](https://medium.com/graph-praxis/graph-rag-in-2026-a-practitioners-guide-to-what-actually-works-dca4962e7517) (2026-02-22) +- [GraphRAG Complete Guide](https://medium.com/@brian-curry-research/graphrag-the-complete-guide-to-graph-powered-retrieval-augmented-generation-eeb58a6bb4d1) (2026-02-11) + +### 核心概念 + +| 概念 | 說明 | +|------|------| +| **GraphRAG** | 結合知識圖譜與 RAG,比傳統向量檢索更智能 | +| **知識圖譜** | 實體 (Entity) + 關係 (Relationship) 的結構化表示 | +| **多跳推理** | Multi-hop traversal,可連接多個相關節點 | +| **混合檢索** | Graph traversal + Vector similarity 結合 | + +### 對 Momentry 的潛在應用 + +``` +視頻場景 → 實體識別 → 關係建立 → 故事圖譜 + ↓ ↓ ↓ ↓ + CUT [人物, 物品, 動作] [誰做了什麼, 什麼導致什麼] [敘事鏈] +``` + +**1. 敘事圖譜構建 (Narrative Graph)** +- 從 Story/Chunks 模組提取實體 +- 建立場景之間的因果關係 +- 追蹤角色互動和情節發展 + +**2. 故事檢索增強** +```python +# 現有: Parent-child chunks +parent_chunk: "場景描述" +child_chunks: [詳細內容] + +# 加入圖譜: +場景A --led_to--> 場景B +角色X --interacted_with--> 角色Y +主題Y --related_to--> 主題Z +``` + +**3. 查詢模式** + +| 查詢類型 | 傳統 RAG | GraphRAG | +|----------|----------|----------| +| 事實查找 | ✅ "這個場景在說什麼" | ✅ | +| 主題推理 | ❌ "這個視頻的主要情節" | ✅ Global search | +| 多跳關係 | ❌ | ✅ "A導致B,B導致C" | +| 可解釋性 | ❌ | ✅ 關係路徑可追溯 | + +### 實作方案 + +**方案 A: TigerGraph Cloud (推薦)** +- ✅ 原生 Graph + Vector 混合查詢 +- ✅ GraphRAG 官方支援 +- ✅ 200GB 免費額度 +- ❌ 雲端依賴,延遲敏感場景需考慮 + +**方案 B: Neo4j + Qdrant** +- ✅ 成熟開源生態 +- ✅ LangChain/LlamaIndex 整合 +- ❌ 需要維護兩個系統 + +**方案 C: 自建混合架構** +- PostgreSQL + Neo4j (或Typesense) +- 利用現有 BM25 + 向量檢索基礎 +- ❌ 開發成本高 + +### 技術棧整合建議 + +```rust +// 現有架構 +Vector Search (Qdrant) ← BM25 (PostgreSQL) + +// 加入 GraphRAG +Knowledge Graph (TigerGraph/Neo4j) + ↓ + 混合檢索 ← Vector + Graph traversal +``` + +### 優先級: 待評估 + +**考慮因素**: +- 用戶是否需要複雜的故事情節查詢? +- 實體識別 (NER) 成本是否可以接受? +- 與現有 BM25 + Vector 混合搜索的比較優勢? + +--- + +## 10. LazyGraphRAG / FastGraphRAG 成本優化 + +**說明**: GraphRAG 索引成本高昂,LazyGraphRAG 推遲圖譜構建到查詢時 + +**來源**: [GraphRAG in 2026](https://medium.com/graph-praxis/graph-rag-in-2026-a-practitioners-guide-to-what-actually-works-dca4962e7517) + +**Microsoft GraphRAG 問題**: $33K 索引大型數據集 + +**替代方案**: +- **LazyGraphRAG**: 按需構建,查詢時再建立子圖 +- **FastGraphRAG**: 優化索引管道,10-90% 成本節省 +- **HippoRAG**: 使用 Personalised PageRank 優化遍歷 + +**優先級**: 待評估 (作為 GraphRAG 的一部分) diff --git a/docs/BUILD_VERSION_RECORD.md b/docs/BUILD_VERSION_RECORD.md new file mode 100644 index 0000000..aa45da3 --- /dev/null +++ b/docs/BUILD_VERSION_RECORD.md @@ -0,0 +1,667 @@ +# Momentry Core 版本紀錄 + +## 版本命名規則 + +### Main Version (主版本) +``` +v{major}.{minor} +例: v0.1, v0.2, v1.0 +``` + +### Build Version (建置版本) +``` +v{major}.{minor}.{YYYYMMDD_HHMMSS} +例: v0.1.20260325_143000 +``` + +--- + +## 版本紀錄存放位置 + +``` +/Users/accusys/momentry/versions/ +├── current/ # 目前使用版本 +│ ├── binary # 當前 binary +│ └── version.json # 版本資訊 +│ +├── releases/ # Release 版本存放 +│ ├── v0.1/ +│ │ ├── v0.1.20260325_143000/ +│ │ │ ├── binary +│ │ │ └── version.json +│ │ ├── v0.1.20260324_100000/ +│ │ │ └── ... +│ │ └── release.json # v0.1 版本總覽 +│ │ +│ └── v0.2/ +│ └── ... +│ +└── changelog.json # 全域版本變更記錄 +``` + +--- + +## version.json 格式 + +```json +{ + "version": "v0.1.20260325_143000", + "main_version": "v0.1", + "build_timestamp": "2026-03-25T14:30:00+08:00", + "git_commit": "83ae050", + "git_branch": "main", + "git_message": "fix: save probe.json to OUTPUT_DIR instead of current directory", + "features": [ + "API Key Authentication", + "Job Worker System" + ], + "fixes": [ + "get_processor_results_by_job column mapping" + ], + "deployed_at": "2026-03-25T15:00:00+08:00", + "deployed_by": "opencode", + "status": "production" +} +``` + +--- + +## release.json 格式 (主版本總覽) + +```json +{ + "version": "v0.1", + "status": "production", + "created_at": "2026-03-14T10:00:00+08:00", + "current_build": "v0.1.20260325_143000", + "builds": [ + { + "build": "v0.1.20260325_143000", + "date": "2026-03-25", + "commits": ["83ae050", "171c36a"], + "summary": "fix: save probe.json, add v2 backup versioning" + }, + { + "build": "v0.1.20260324_100000", + "date": "2026-03-24", + "commits": ["89fbfd6", "3edaf01"], + "summary": "feat: add POST /api/v1/probe endpoint" + } + ], + "changelog": [ + "## v0.1.20260325_143000", + "- 修復 processor_results 欄位映射錯誤", + "- 添加 API Key 認證", + "", + "## v0.1.20260324_100000", + "- 新增 Probe API" + ] +} +``` + +--- + +## changelog.json 格式 (全域變更記錄) + +```json +{ + "updated_at": "2026-03-25T14:30:00+08:00", + "versions": { + "v0.1": { + "status": "production", + "current_build": "v0.1.20260325_143000", + "build_count": 12 + }, + "v0.0": { + "status": "deprecated", + "final_build": "v0.0.20260310_090000" + } + }, + "recent_changes": [ + { + "version": "v0.1.20260325_143000", + "date": "2026-03-25", + "changes": [ + {"type": "fix", "description": "get_processor_results_by_job column mapping"}, + {"type": "feat", "description": "API Key Authentication"} + ] + } + ] +} +``` + +--- + +## Release Script + +### /Users/accusys/momentry/scripts/release.sh + +```bash +#!/bin/bash +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="/Users/accusys/momentry_core_0.1" +VERSIONS_DIR="/Users/accusys/momentry/versions" +BACKUP_DIR="/Users/accusys/momentry/backup/bin" +CURRENT_BIN="/Users/accusys/momentry/bin/momentry" + +# 顏色輸出 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +log_error() { echo -e "${RED}[ERROR]${NC} $1"; } + +# 解析命令列參數 +MAIN_VERSION="" +while [[ $# -gt 0 ]]; do + case $1 in + -v|--version) + MAIN_VERSION="$2" + shift 2 + ;; + *) + log_error "Unknown option: $1" + exit 1 + ;; + esac +done + +if [ -z "$MAIN_VERSION" ]; then + log_error "請指定主版本: ./release.sh -v v0.1" + exit 1 +fi + +log_info "開始 Release ${MAIN_VERSION}..." + +# 1. 取得 Git 資訊 +GIT_COMMIT=$(git -C "$PROJECT_DIR" rev-parse --short HEAD) +GIT_BRANCH=$(git -C "$PROJECT_DIR" rev-parse --abbrev-ref HEAD) +GIT_MESSAGE=$(git -C "$PROJECT_DIR" log -1 --pretty=%s) +BUILD_TIMESTAMP=$(date +%Y%m%d_%H%M%S) +BUILD_VERSION="${MAIN_VERSION}.${BUILD_TIMESTAMP}" + +log_info "Build Version: ${BUILD_VERSION}" +log_info "Git Commit: ${GIT_COMMIT}" + +# 2. 創建版本目錄 +BUILD_DIR="${VERSIONS_DIR}/releases/${MAIN_VERSION}/${BUILD_VERSION}" +mkdir -p "$BUILD_DIR" +mkdir -p "${VERSIONS_DIR}/current" +mkdir -p "$BACKUP_DIR" + +# 3. 停止 Production Service +log_info "停止 Production Service..." +sudo launchctl unload /Library/LaunchDaemons/com.momentry.api.plist 2>/dev/null || true + +# 4. 備份當前 Binary +if [ -f "$CURRENT_BIN" ]; then + OLD_VERSION=$(cat "${VERSIONS_DIR}/current/version.json" 2>/dev/null | jq -r '.version // "unknown"') + log_info "備份當前版本: $OLD_VERSION" + cp "$CURRENT_BIN" "${BACKUP_DIR}/momentry_${OLD_VERSION}_$(date +%Y%m%d_%H%M%S)" +fi + +# 5. 編譯 Release 版本 +log_info "編譯 Release 版本..." +cd "$PROJECT_DIR" +cargo build --release --bin momentry + +# 6. 複製到版本目錄 +log_info "複製到版本目錄..." +cp target/release/momentry "${BUILD_DIR}/binary" +cp target/release/momentry "$CURRENT_BIN" + +# 7. 生成 version.json +cat > "${BUILD_DIR}/version.json" << EOF +{ + "version": "${BUILD_VERSION}", + "main_version": "${MAIN_VERSION}", + "build_timestamp": "$(date -Iseconds)", + "git_commit": "${GIT_COMMIT}", + "git_branch": "${GIT_BRANCH}", + "git_message": "${GIT_MESSAGE}", + "features": [], + "fixes": [], + "deployed_at": null, + "deployed_by": null, + "status": "built" +} +EOF + +# 8. 更新 current +cp "${BUILD_DIR}/version.json" "${VERSIONS_DIR}/current/version.json" + +# 9. 更新 changelog.json +UPDATE_CHANGELOG=" +import json +from datetime import datetime + +changelog_path = '${VERSIONS_DIR}/changelog.json' +build_info = { + 'version': '${BUILD_VERSION}', + 'date': datetime.now().strftime('%Y-%m-%d'), + 'commit': '${GIT_COMMIT}', + 'message': '${GIT_MESSAGE}' +} + +try: + with open(changelog_path, 'r') as f: + changelog = json.load(f) +except FileNotFoundError: + changelog = {'updated_at': '', 'versions': {}, 'recent_changes': []} + +changelog['updated_at'] = datetime.now().isoformat() +if '${MAIN_VERSION}' not in changelog['versions']: + changelog['versions']['${MAIN_VERSION}'] = {'status': 'building', 'build_count': 0} + +changelog['versions']['${MAIN_VERSION}']['build_count'] += 1 +changelog['versions']['${MAIN_VERSION}']['current_build'] = '${BUILD_VERSION}' +changelog['recent_changes'].insert(0, build_info) + +with open(changelog_path, 'w') as f: + json.dump(changelog, f, indent=2, ensure_ascii=False) +" +python3 -c "$UPDATE_CHANGELOG" + +# 10. 啟動 Production Service +log_info "啟動 Production Service..." +sudo launchctl load /Library/LaunchDaemons/com.momentry.api.plist + +# 11. 驗證 +sleep 3 +if curl -s http://localhost:3002/health > /dev/null; then + log_info "✓ Release 成功!" + log_info "版本: ${BUILD_VERSION}" + log_info "目錄: ${BUILD_DIR}" +else + log_error "✗ Release 失敗!請檢查服務狀態。" + exit 1 +fi +``` + +--- + +## 查詢版本指令 + +### 查詢目前版本 +```bash +cat /Users/accusys/momentry/versions/current/version.json +``` + +### 查詢所有 Release +```bash +ls -la /Users/accusys/momentry/versions/releases/ +``` + +### 查詢版本歷史 +```bash +cat /Users/accusys/momentry/versions/changelog.json | python3 -m json.tool +``` + +### 查詢特定主版本 +```bash +ls /Users/accusys/momentry/versions/releases/v0.1/ +``` + +--- + +## 版本狀態 + +| 狀態 | 說明 | +|------|------| +| `building` | 建置中 | +| `built` | 已建置,未部署 | +| `testing` | 測試中 | +| `production` | 正式環境使用中 | +| `deprecated` | 已棄用 | +| `archived` | 已封存 | + +--- + +## 版本流程圖 + +``` +develop (git branch) + │ + ▼ +feature/bugfix commit + │ + ▼ +develop ──────────────────┐ + │ │ + │ (merge to main) │ + ▼ │ +main (git) │ + │ │ + ▼ │ +Build v0.1.20260325_143000 + │ │ + ├──► testing (3003) │ + │ │ + │ (approve) │ + ▼ ▼ +v0.1 ───────────────────┘ + │ + ├──► releases/v0.1/v0.1.20260325_143000/ + │ + ├──► current/ (production) + │ + ▼ +changelog.json (update) +``` + +--- + +## Release Note (版本發布說明) + +### Release Note 存放位置 + +``` +/Users/accusys/momentry/versions/releases/{主版本}/{建置版本}/ +├── binary +├── version.json +└── RELEASE_NOTE.md # 發布說明 (Markdown) +``` + +### Release Note 範本 + +```markdown +# Momentry Core v0.1.20260325_143000 Release Note + +## 版本資訊 +- **Build Version**: v0.1.20260325_143000 +- **Main Version**: v0.1 +- **Build Date**: 2026-03-25 14:30:00 +- **Git Commit**: 83ae050 + +## 新功能 (Features) + +### API Key 認證系統 +- 添加 API Key 認證中介層 +- 所有 `/api/v1/*` 端點需要 `X-API-Key` header +- 支援 API Key 使用追蹤和審計日誌 + +### Job Worker 系統 +- 新增 Job Worker 二進位檔 +- 支援最多 2 個並發處理器 +- 新增 `/api/v1/jobs/:uuid` 端點查詢任務詳情 + +## 錯誤修復 (Bug Fixes) + +| Issue | 描述 | +|-------|------| +| #001 | 修復 `get_processor_results_by_job` 欄位映射錯誤 | +| #002 | 修復 API Key 驗證時區處理問題 | + +## API 變更 (API Changes) + +### 新增端點 +| Method | Endpoint | 說明 | +|--------|----------|------| +| GET | `/api/v1/jobs` | 取得所有任務列表 | +| GET | `/api/v1/jobs/:uuid` | 取得特定任務詳情 | + +### 認證變更 +| 端點 | 舊版 | 新版 | +|------|------|------| +| `/api/v1/*` | 公開 | 需要 API Key | + +## 升級指南 + +### 從舊版升級 +1. 備份當前版本 +2. 停止服務 +3. 替換 binary +4. 重啟服務 +5. 更新 API Key 配置 + +### API Key 配置 +```bash +# 請求範例 +curl -H "X-API-Key: your_api_key" \ + "http://localhost:3002/api/v1/videos" +``` + +## 已知問題 (Known Issues) + +- 暫無 + +## 相關文檔 + +- [API 文檔](../docs/API_INDEX.md) +- [版本管理規範](../docs/VERSION_MANAGEMENT.md) + +--- + +## Release Note 自動生成 Script + +### /Users/accusys/momentry/scripts/generate_release_note.sh + +```bash +#!/bin/bash +set -e + +BUILD_VERSION=$1 +MAIN_VERSION=$2 +BUILD_DIR="/Users/accusys/momentry/versions/releases/${MAIN_VERSION}/${BUILD_VERSION}" + +# 取得 Git 資訊 +GIT_COMMITS=$(git log --oneline -10) +GIT_CHANGES=$(git diff --stat HEAD~5..HEAD) + +cat > "${BUILD_DIR}/RELEASE_NOTE.md" << EOF +# Momentry Core ${BUILD_VERSION} Release Note + +## 版本資訊 +- **Build Version**: ${BUILD_VERSION} +- **Main Version**: ${MAIN_VERSION} +- **Build Date**: $(date '+%Y-%m-%d %H:%M:%S') +- **Git Commit**: $(git rev-parse --short HEAD) + +## 變更內容 + +### Commit 記錄 +\`\`\` +${GIT_COMMITS} +\`\`\` + +### 變更統計 +\`\`\` +${GIT_CHANGES} +\`\`\` + +## 新功能 + +## 錯誤修復 + +## API 變更 + +## 升級指南 + +## 已知問題 + +EOF + +echo "Release Note 生成完成: ${BUILD_DIR}/RELEASE_NOTE.md" +``` + +--- + +## Release Note 查詢 + +### 查詢所有 Release Note +```bash +find /Users/accusys/momentry/versions/releases -name "RELEASE_NOTE.md" +``` + +### 查看特定版本 Release Note +```bash +cat /Users/accusys/momentry/versions/releases/v0.1/v0.1.20260325_143000/RELEASE_NOTE.md +``` + +### 查詢最新版本 Release Note +```bash +cat /Users/accusys/momentry/versions/current/RELEASE_NOTE.md +``` + +--- + +## Release Note 範例 + +### 完整 Release Note 範例 + +\`\`\`markdown +# Momentry Core v0.1.20260325_143000 Release Note + +## 版本資訊 +| 項目 | 內容 | +|------|------| +| Build Version | v0.1.20260325_143000 | +| Main Version | v0.1 | +| Build Date | 2026-03-25 14:30:00 | +| Git Commit | 83ae050 | +| Status | ✅ Production | + +## 新功能 (Features) + +### 1. API Key 認證系統 +添加完整的 API Key 認證系統,保護所有 API 端點。 + +**功能:** +- SHA256 key hash 驗證 +- 使用統計追蹤 +- 審計日誌記錄 +- 異常檢測 + +**API 使用方式:** +\`\`\`bash +curl -H "X-API-Key: your_key" \\ + "http://localhost:3002/api/v1/videos" +\`\`\` + +### 2. Job Worker 系統 +新增獨立的 Job Worker 處理影片處理任務。 + +**特性:** +- 最多 2 個並發處理器 +- Polling-based 任務獲取 +- 自動進度追蹤 + +## 錯誤修復 (Bug Fixes) + +| Issue | 描述 | 嚴重性 | +|-------|------|--------| +| #001 | 修復 `get_processor_results_by_job` TIMESTAMP 欄位映射 | 🔴 高 | +| #002 | 修復 3002 port 衝突問題 | 🟡 中 | + +## API 變更 + +### 新增端點 +| Method | Endpoint | 說明 | +|--------|----------|------| +| GET | `/api/v1/jobs` | 取得任務列表 | +| GET | `/api/v1/jobs/:uuid` | 取得任務詳情 | + +### 端點認證狀態 +| 端點 | 認證需求 | +|------|----------| +| `/health` | ❌ 不需要 | +| `/api/v1/*` | ✅ 需要 `X-API-Key` | + +## 升級指南 + +### 前置需求 +- PostgreSQL 資料庫 +- Redis 伺服器 +- MongoDB 快取 + +### 升級步驟 +1. **備份當前版本** + \`\`\`bash + cp /Users/accusys/momentry/bin/momentry \\ + /Users/accusys/momentry/backup/bin/momentry_$(date +%Y%m%d) + \`\`\` + +2. **停止服務** + \`\`\`bash + sudo launchctl unload /Library/LaunchDaemons/com.momentry.api.plist + \`\`\` + +3. **替換 Binary** + \`\`\`bash + cp v0.1.20260325_143000/binary /Users/accusys/momentry/bin/momentry + \`\`\` + +4. **重啟服務** + \`\`\`bash + sudo launchctl load /Library/LaunchDaemons/com.momentry.api.plist + \`\`\` + +5. **驗證** + \`\`\`bash + curl http://localhost:3002/health + \`\`\` + +## 已知問題 (Known Issues) + +- 暫無 + +## 技術細節 + +### 認證流程 +\`\`\` +Client Request + │ + ▼ +[X-API-Key Header] ──► Middleware + │ │ + │ ▼ + │ Hash Key (SHA256) + │ │ + │ ▼ + │ DB Lookup + │ │ + │ ▼ + │ Validate Status + │ │ + ▼ ▼ +Handler ◄────────────────────┘ +\`\`\` + +### 資料庫變更 +\`\`\`sql +-- 新增 duration_secs 欄位 +ALTER TABLE processor_results +ADD COLUMN IF NOT EXISTS duration_secs DOUBLE PRECISION; +\`\`\` + +## 回滾指南 + +如需回滾到上一版本: +\`\`\`bash +# 1. 停止服務 +sudo launchctl unload /Library/LaunchDaemons/com.momentry.api.plist + +# 2. 恢復舊版 +cp /Users/accusys/momentry/backup/bin/momentry_v0.1.20260324_100000 \\ + /Users/accusys/momentry/bin/momentry + +# 3. 重啟服務 +sudo launchctl load /Library/LaunchDaemons/com.momentry.api.plist +\`\`\` + +## 聯繫與支援 + +- **Issue Tracker**: https://gitea.momentry.ddns.net/momentry/momentry_core/issues +- **文檔**: https://docs.momentry.ddns.net + +--- + +*Generated: $(date '+%Y-%m-%d %H:%M:%S')* +\`\`\` + +``` diff --git a/docs/CACHE_ARCHITECTURE_PLAN.md b/docs/CACHE_ARCHITECTURE_PLAN.md new file mode 100644 index 0000000..7d9001e --- /dev/null +++ b/docs/CACHE_ARCHITECTURE_PLAN.md @@ -0,0 +1,1106 @@ +# Momentry Core 分層緩存架構開發計劃 + +**版本**: V1.0 +**日期**: 2026-03-24 +**目標**: 實現 Redis + MongoDB 分層緩存架構 + +--- + +## 1. 概述 + +### 1.1 目標 + +在 Momentry Core 中實現分層緩存架構: +- **小型、高頻存取** → Redis +- **中型、查詢導向** → MongoDB + +### 1.2 現有架構 + +| 組件 | 現況 | 用途 | +|------|------|------| +| Redis | ✅ 已實現 | Job 進度、Pub/Sub、健康檢查、API Key(Moka) | +| MongoDB | ⚠️ HTTP 驅動 | 僅用於存儲 chunks | +| 內存緩存 | Moka + RwLock | API Key、視頻記錄 | + +### 1.3 目標架構 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Layer 1: Redis Cache │ +│ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ Job Progress │ │ Health Status │ │ +│ │ (已有) │ │ (新增) │ │ +│ └─────────────────┘ └─────────────────┘ │ +│ ┌─────────────────┐ │ +│ │ Video Meta 熱讀 │ │ +│ │ (新增) │ │ +│ └─────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + │ Cache Miss + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Layer 2: MongoDB Cache │ +│ Collection: momento.cache │ +│ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ Videos List │ │ Search Results │ │ +│ │ (新增) │ │ (新增) │ │ +│ └─────────────────┘ └─────────────────┘ │ +│ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ Hybrid Search │ │ N8n Search │ │ +│ │ (新增) │ │ (新增) │ │ +│ └─────────────────┘ └─────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + │ Cache Miss + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ PostgreSQL / Qdrant │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## 2. 技術棧變更 + +### 2.1 Cargo.toml 變更 + +```toml +# 現有 +mongodb = { version = "2", features = ["tokio-sync"] } + +# 變更為 +mongodb = { version = "2", features = ["tokio-comp", "bson"] } +``` + +**說明**: +- `tokio-comp`: 啟用 async tokio runtime 支持 +- `bson`: BSON 序列化/反序列化支持 + +--- + +## 3. 新增模組結構 + +### 3.1 目錄結構 + +``` +src/core/ +├── cache/ # 新增目錄 +│ ├── mod.rs # 模組入口 +│ ├── mongo_cache.rs # MongoDB 緩存實現 +│ ├── redis_cache.rs # Redis 緩存封裝 +│ ├── keys.rs # Cache Key 工具函數 +│ └── config.rs # 緩存配置 +├── db/ +│ ├── mod.rs # 新增 cache 導出 +│ ├── mongodb_db.rs # 重構為原生驅動 +│ └── ... +└── ... +``` + +### 3.2 文件清單 + +| 操作 | 文件路徑 | 說明 | +|------|----------|------| +| 新增 | `src/core/cache/mod.rs` | Cache 模組入口 | +| 新增 | `src/core/cache/mongo_cache.rs` | MongoDB 緩存實現 | +| 新增 | `src/core/cache/redis_cache.rs` | Redis 緩存封裝 | +| 新增 | `src/core/cache/keys.rs` | Cache Key 工具函數 | +| 新增 | `src/core/cache/config.rs` | 緩存配置 | +| 修改 | `src/core/db/mongodb_db.rs` | 改用原生 `mongodb` crate | +| 修改 | `src/core/db/mod.rs` | 導出新增模組 | +| 修改 | `src/api/server.rs` | 整合緩存到 API handlers | +| 修改 | `src/core/config.rs` | 添加 MongoDB 緩存配置 | +| 修改 | `Cargo.toml` | 更新 mongodb feature | + +--- + +## 4. 配置設計 + +### 4.1 環境變數 + +```bash +# MongoDB Cache 配置 (新增) +MONGODB_URL=mongodb://localhost:27017 +MONGODB_CACHE_ENABLED=true +MONGODB_CACHE_TTL_VIDEOS=300 # 5 分鐘 +MONGODB_CACHE_TTL_SEARCH=300 # 5 分鐘 +MONGODB_CACHE_TTL_HYBRID_SEARCH=600 # 10 分鐘 +MONGODB_CACHE_TTL_VIDEO_META=3600 # 60 分鐘 + +# Redis Cache 配置 (新增) +REDIS_CACHE_TTL_HEALTH=30 # 30 秒 +REDIS_CACHE_TTL_VIDEO_META=3600 # 60 分鐘 +``` + +### 4.2 config.rs 結構 + +```rust +// src/core/config.rs + +pub mod cache { + use super::*; + + pub static MONGODB_URL: Lazy = Lazy::new(|| { + env::var("MONGODB_URL") + .unwrap_or_else(|_| "mongodb://localhost:27017".to_string()) + }); + + pub static MONGODB_CACHE_ENABLED: Lazy = Lazy::new(|| { + env::var("MONGODB_CACHE_ENABLED") + .unwrap_or_else(|_| "true".to_string()) + .parse() + .unwrap_or(true) + }); + + pub static MONGODB_CACHE_TTL_VIDEOS: Lazy = Lazy::new(|| { + env::var("MONGODB_CACHE_TTL_VIDEOS") + .unwrap_or_else(|_| "300".to_string()) + .parse() + .unwrap_or(300) + }); + + pub static MONGODB_CACHE_TTL_SEARCH: Lazy = Lazy::new(|| { + env::var("MONGODB_CACHE_TTL_SEARCH") + .unwrap_or_else(|_| "300".to_string()) + .parse() + .unwrap_or(300) + }); + + pub static MONGODB_CACHE_TTL_HYBRID_SEARCH: Lazy = Lazy::new(|| { + env::var("MONGODB_CACHE_TTL_HYBRID_SEARCH") + .unwrap_or_else(|_| "600".to_string()) + .parse() + .unwrap_or(600) + }); + + pub static MONGODB_CACHE_TTL_VIDEO_META: Lazy = Lazy::new(|| { + env::var("MONGODB_CACHE_TTL_VIDEO_META") + .unwrap_or_else(|_| "3600".to_string()) + .parse() + .unwrap_or(3600) + }); + + pub static REDIS_CACHE_TTL_HEALTH: Lazy = Lazy::new(|| { + env::var("REDIS_CACHE_TTL_HEALTH") + .unwrap_or_else(|_| "30".to_string()) + .parse() + .unwrap_or(30) + }); +} +``` + +--- + +## 5. MongoDB Cache 設計 + +### 5.1 Collection 結構 + +```javascript +// Collection: momento.cache +// Database: momento + +{ + "_id": ObjectId("..."), + "key": "videos:list:page=1:limit=20", + "value": { + "videos": [ + { + "uuid": "xxx", + "file_path": "/path/to/video.mp4", + "file_name": "video.mp4", + "duration": 120.5, + "width": 1920, + "height": 1080 + } + ] + }, + "category": "videos", + "created_at": ISODate("2026-03-24T08:00:00Z"), + "expires_at": ISODate("2026-03-24T08:05:00Z"), + "hit_count": 0, + "last_access": ISODate("2026-03-24T08:00:00Z") +} +``` + +### 5.2 索引設計 + +```javascript +// TTL Index - 自動刪除過期文檔 +db.momento.cache.createIndex( + { "expires_at": 1 }, + { expireAfterSeconds: 0 } +) + +// 唯一索引 - 防止重複 key +db.momento.cache.createIndex( + { "key": 1 }, + { unique: true } +) + +// 分類索引 - 批量失效用 +db.momento.cache.createIndex({ "category": 1 }) +``` + +### 5.3 CacheEntry 結構 + +```rust +// src/core/cache/mongo_cache.rs + +use serde::{Deserialize, Serialize}; +use bson::oid::ObjectId; +use chrono::{DateTime, Utc}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CacheEntry { + #[serde(rename = "_id", skip_serializing_if = "Option::is_none")] + pub id: Option, + + pub key: String, + pub value: serde_json::Value, + pub category: String, + + pub created_at: DateTime, + pub expires_at: DateTime, + + #[serde(default)] + pub hit_count: i64, + + #[serde(default)] + pub last_access: DateTime, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CacheConfig { + pub enabled: bool, + pub ttl_videos: u64, + pub ttl_search: u64, + pub ttl_hybrid_search: u64, + pub ttl_video_meta: u64, +} +``` + +--- + +## 6. API 緩存策略 + +### 6.1 緩存矩陣 + +| API | Cache Layer | Key Pattern | TTL | 失效時機 | +|-----|-------------|-------------|-----|----------| +| `GET /api/v1/videos` | MongoDB | `videos:list:page={p}:limit={l}` | 5min | register/delete | +| `GET /api/v1/lookup` | Redis | `momentry:cache:video:{uuid}` | 60min | update/delete | +| `POST /api/v1/search` | MongoDB | `search:{hash}` | 5min | vectorize | +| `POST /api/v1/search/hybrid` | MongoDB | `search:hybrid:{hash}` | 10min | vectorize | +| `POST /api/v1/n8n/search` | MongoDB | `search:n8n:{hash}` | 5min | vectorize | +| `GET /health` | Redis | `momentry:cache:health` | 30s | - | + +### 6.2 Cache Key 命名規範 + +```rust +// src/core/cache/keys.rs + +pub mod keys { + pub const CATEGORY_VIDEOS: &str = "videos"; + pub const CATEGORY_SEARCH: &str = "search"; + pub const CATEGORY_HYBRID_SEARCH: &str = "hybrid_search"; + pub const CATEGORY_N8N_SEARCH: &str = "n8n_search"; + pub const CATEGORY_VIDEO_META: &str = "video_meta"; + pub const CATEGORY_HEALTH: &str = "health"; + + pub fn videos_list(page: usize, limit: usize) -> String { + format!("videos:list:page={}:limit={}", page, limit) + } + + pub fn video_meta(uuid: &str) -> String { + format!("video:{}", uuid) + } + + pub fn search(query_hash: &str) -> String { + format!("search:{}", query_hash) + } + + pub fn hybrid_search(query_hash: &str) -> String { + format!("search:hybrid:{}", query_hash) + } + + pub fn n8n_search(query_hash: &str) -> String { + format!("search:n8n:{}", query_hash) + } + + pub fn health() -> String { + "health:basic".to_string() + } +} +``` + +--- + +## 7. 實現細節 + +### 7.1 MongoCache 實現 + +```rust +// src/core/cache/mongo_cache.rs + +use anyhow::Result; +use bson::{doc, oid::ObjectId}; +use chrono::{Duration, Utc}; +use mongodb::{Client, Collection, Database}; +use serde::{de::DeserializeOwned, Serialize}; +use std::sync::Arc; + +use super::keys; +use super::config::CacheConfig; +use crate::core::config::cache as cache_config; + +#[derive(Clone)] +pub struct MongoCache { + client: Client, + db: Database, + collection: Collection, + config: CacheConfig, +} + +impl MongoCache { + pub async fn init() -> Result { + let uri = cache_config::MONGODB_URL.as_str(); + let client = Client::uri(uri).await?; + let db = client.database("momento"); + let collection = db.collection::("cache"); + + let config = CacheConfig { + enabled: *cache_config::MONGODB_CACHE_ENABLED, + ttl_videos: *cache_config::MONGODB_CACHE_TTL_VIDEOS, + ttl_search: *cache_config::MONGODB_CACHE_TTL_SEARCH, + ttl_hybrid_search: *cache_config::MONGODB_CACHE_TTL_HYBRID_SEARCH, + ttl_video_meta: *cache_config::MONGODB_CACHE_TTL_VIDEO_META, + }; + + // Ensure indexes exist + Self::ensure_indexes(&collection).await?; + + Ok(Self { + client, + db, + collection, + config, + }) + } + + async fn ensure_indexes(collection: &Collection) -> Result<()> { + use mongodb::IndexModel; + + // TTL Index + let ttl_index = IndexModel::builder() + .keys(doc! { "expires_at": 1 }) + .options( + mongodb::options::IndexOptions::builder() + .expire_after(std::time::Duration::from_secs(0)) + .build() + ) + .build(); + + // Unique key index + let key_index = IndexModel::builder() + .keys(doc! { "key": 1 }) + .options( + mongodb::options::IndexOptions::builder() + .unique(true) + .build() + ) + .build(); + + collection.create_indexes([ttl_index, key_index]).await?; + Ok(()) + } + + pub async fn get(&self, key: &str) -> Result> { + if !self.config.enabled { + return Ok(None); + } + + let filter = doc! { "key": key }; + let result = self.collection.find_one(filter).await?; + + if let Some(entry) = result { + // Update hit count and last_access + let update = doc! { + "$inc": { "hit_count": 1 }, + "$set": { "last_access": Utc::now() } + }; + self.collection.update_one(doc! { "_id": entry.id }, update).await?; + + // Deserialize value + let value = serde_json::from_value(entry.value)?; + Ok(Some(value)) + } else { + Ok(None) + } + } + + pub async fn set(&self, key: &str, value: &T, ttl_secs: u64, category: &str) -> Result<()> { + if !self.config.enabled { + return Ok(()); + } + + let now = Utc::now(); + let expires_at = now + Duration::seconds(ttl_secs as i64); + let json_value = serde_json::to_value(value)?; + + let entry = CacheEntry { + id: None, + key: key.to_string(), + value: json_value, + category: category.to_string(), + created_at: now, + expires_at, + hit_count: 0, + last_access: now, + }; + + let filter = doc! { "key": key }; + let update = doc! { + "$set": { + "value": &entry.value, + "category": &entry.category, + "expires_at": entry.expires_at, + "last_access": entry.last_access, + }, + "$setOnInsert": { + "key": &entry.key, + "created_at": entry.created_at, + "hit_count": 0i64, + } + }; + + self.collection.update_one(filter, update).await?; + Ok(()) + } + + pub async fn invalidate_category(&self, category: &str) -> Result { + if !self.config.enabled { + return Ok(0); + } + + let result = self.collection.delete_many(doc! { "category": category }).await?; + Ok(result.deleted_count) + } + + pub async fn invalidate_prefix(&self, prefix: &str) -> Result { + if !self.config.enabled { + return Ok(0); + } + + let filter = doc! { "key": { "$regex": &format!("^{}", prefix) } }; + let result = self.collection.delete_many(filter).await?; + Ok(result.deleted_count) + } + + pub async fn get_or_fetch(&self, key: &str, ttl_secs: u64, category: &str, fetcher: F) -> Result + where + F: FnOnce() -> Fut, + Fut: std::future::Future>, + T: DeserializeOwned + Serialize, + { + // Try cache first + if let Some(cached) = self.get::(key).await? { + tracing::debug!("Cache hit for key: {}", key); + return Ok(cached); + } + + // Cache miss - fetch from source + tracing::debug!("Cache miss for key: {}", key); + let value = fetcher().await?; + + // Store in cache + self.set(key, &value, ttl_secs, category).await?; + + Ok(value) + } +} +``` + +### 7.2 RedisCache 實現 + +```rust +// src/core/cache/redis_cache.rs + +use anyhow::Result; +use redis::AsyncCommands; +use serde::{de::DeserializeOwned, Serialize}; +use std::time::Duration; + +use crate::core::config::cache as cache_config; + +#[derive(Clone)] +pub struct RedisCache { + client: crate::core::db::RedisClient, +} + +impl RedisCache { + pub fn new() -> Result { + let client = crate::core::db::RedisClient::new()?; + Ok(Self { client }) + } + + pub async fn get(&self, key: &str) -> Result> { + let mut conn = self.client.get_conn_internal().await?; + let value: Option = conn.get(key).await?; + + match value { + Some(json) => { + let result = serde_json::from_str(&json)?; + Ok(Some(result)) + } + None => Ok(None), + } + } + + pub async fn set(&self, key: &str, value: &T, ttl_secs: u64) -> Result<()> { + let mut conn = self.client.get_conn_internal().await?; + let json = serde_json::to_string(value)?; + let _: String = conn.set_ex(key, json, ttl_secs).await?; + Ok(()) + } + + pub async fn delete(&self, key: &str) -> Result<()> { + let mut conn = self.client.get_conn_internal().await?; + let _: () = conn.del(key).await?; + Ok(()) + } + + pub async fn invalidate_pattern(&self, pattern: &str) -> Result { + let mut conn = self.client.get_conn_internal().await?; + let keys: Vec = conn.keys(pattern).await?; + let count = keys.len() as u64; + + if !keys.is_empty() { + let _: () = conn.del(keys).await?; + } + + Ok(count) + } + + pub async fn get_or_fetch(&self, key: &str, ttl_secs: u64, fetcher: F) -> Result + where + F: FnOnce() -> Fut, + Fut: std::future::Future>, + T: DeserializeOwned + Serialize, + { + // Try cache first + if let Some(cached) = self.get::(key).await? { + return Ok(cached); + } + + // Cache miss + let value = fetcher().await?; + self.set(key, &value, ttl_secs).await?; + Ok(value) + } + + pub async fn get_health(&self) -> Result> { + let mut conn = self.client.get_conn_internal().await?; + let key = "momentry:cache:health"; + let value: Option = conn.get(key).await?; + Ok(value) + } + + pub async fn set_health(&self, status: &str) -> Result<()> { + let ttl = *cache_config::REDIS_CACHE_TTL_HEALTH; + let mut conn = self.client.get_conn_internal().await?; + let key = "momentry:cache:health"; + let _: String = conn.set_ex(key, status, ttl).await?; + Ok(()) + } +} +``` + +--- + +## 8. API Handler 整合 + +### 8.1 AppState 擴展 + +```rust +// src/api/server.rs + +#[derive(Clone)] +struct AppState { + embedder: Arc, + embedder_model: String, + mongo_cache: Arc, // 新增 + redis_cache: Arc, // 新增 +} +``` + +### 8.2 Videos List Handler + +```rust +// src/api/server.rs + +use crate::core::cache::{MongoCache, RedisCache, keys}; + +async fn list_videos( + State(state): State, + Query(params): Query, +) -> Result, StatusCode> { + let page = params.page.unwrap_or(1); + let limit = params.limit.unwrap_or(20); + let cache_key = keys::videos_list(page, limit); + + // Try cache first + let video_infos = state.mongo_cache + .get_or_fetch::<_, _, VideosResponse>( + &cache_key, + 300, // 5 min TTL + keys::CATEGORY_VIDEOS, + || async { + let db = PostgresDb::init().await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + let videos = db.list_videos().await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + let video_infos: Vec = videos + .into_iter() + .map(|v| VideoInfoResponse { + uuid: v.uuid, + file_path: v.file_path, + file_name: v.file_name, + duration: v.duration, + width: v.width, + height: v.height, + }) + .collect(); + + Ok(VideosResponse { videos: video_infos }) + }, + ) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + Ok(Json(video_infos)) +} +``` + +### 8.3 Lookup Handler + +```rust +// src/api/server.rs + +async fn lookup( + State(state): State, + Query(query): Query, +) -> Result, StatusCode> { + if let Some(path) = query.path { + let uuid = crate::uuid::compute_uuid_from_path(&path); + return Ok(Json(LookupResponse { + uuid, + file_path: None, + file_name: None, + duration: None, + })); + } + + if let Some(uuid) = query.uuid { + let cache_key = keys::video_meta(&uuid); + + // Try Redis cache first, fallback to DB + let video = state.redis_cache + .get_or_fetch::<_, _, Option>( + &cache_key, + 3600, // 60 min TTL + || async { + let db = PostgresDb::init().await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + db.get_video_by_uuid(&uuid).await + .map_err(|e| anyhow::anyhow!(e)) + }, + ) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + if let Some(v) = video { + return Ok(Json(LookupResponse { + uuid: v.uuid, + file_path: Some(v.file_path), + file_name: Some(v.file_name), + duration: Some(v.duration), + })); + } + } + + Err(StatusCode::NOT_FOUND) +} +``` + +### 8.4 Search Handler + +```rust +// src/api/server.rs + +use sha2::{Sha256, Digest}; + +async fn search( + State(state): State, + Json(req): Json, +) -> Result, StatusCode> { + let limit = req.limit.unwrap_or(10); + + // Generate cache key from query hash + let query_for_hash = serde_json::json!({ + "query": req.query, + "limit": limit, + "uuid": req.uuid, + }); + let query_hash = format!("{:x}", Sha256::digest(&serde_json::to_string(&query_for_hash).unwrap())); + let cache_key = keys::search(&query_hash); + + let response = state.mongo_cache + .get_or_fetch::<_, _, SearchResponse>( + &cache_key, + 300, // 5 min TTL + keys::CATEGORY_SEARCH, + || async { + // Original search logic here + let query_vector = state.embedder.embed_query(&req.query).await + .map_err(|e| anyhow::anyhow!("Embedding failed: {}", e))?; + + let qdrant = QdrantDb::init().await + .map_err(|e| anyhow::anyhow!("Qdrant init failed: {}", e))?; + let pg = PostgresDb::init().await + .map_err(|e| anyhow::anyhow!("PG init failed: {}", e))?; + + let search_results = if let Some(ref uuid) = req.uuid { + let query_f64: Vec = query_vector.iter().map(|&x| x as f64).collect(); + qdrant.search_in_uuid(&query_f64, uuid, limit).await? + } else { + let query_f64: Vec = query_vector.iter().map(|&x| x as f64).collect(); + qdrant.search(&query_f64, limit).await? + }; + + let mut results = Vec::new(); + for r in search_results { + if let Some(chunk) = pg.get_chunk_by_chunk_id(&r.chunk_id).await.ok().flatten() { + let text = chunk.content.get("text") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + + results.push(SearchResult { + uuid: chunk.uuid, + chunk_id: chunk.chunk_id, + chunk_type: chunk.chunk_type.as_str().to_string(), + start_time: chunk.start_time, + end_time: chunk.end_time, + text, + score: r.score, + }); + } + } + + Ok(SearchResponse { results, query: req.query }) + }, + ) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + Ok(Json(response)) +} +``` + +### 8.5 Health Handler + +```rust +// src/api/server.rs + +async fn health(State(state): State) -> Json { + // Try Redis cache first + if let Some(status) = state.redis_cache.get_health().await.ok().flatten() { + return Json(HealthResponse { + status, + version: env!("CARGO_PKG_VERSION").to_string(), + uptime_ms: get_uptime_ms(), + }); + } + + // Cache miss - compute and cache + let status = "ok".to_string(); + state.redis_cache.set_health(&status).await.ok(); + + Json(HealthResponse { + status, + version: env!("CARGO_PKG_VERSION").to_string(), + uptime_ms: get_uptime_ms(), + }) +} +``` + +### 8.6 Register Handler (緩存失效) + +```rust +// src/api/server.rs + +async fn register( + State(state): State, + Json(req): Json, +) -> Result, StatusCode> { + // ... existing registration logic ... + + let video_id = db.register_video(&record).await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + // Invalidate videos list cache + state.mongo_cache.invalidate_prefix("videos:list:").await.ok(); + + Ok(Json(RegisterResponse { + uuid, + video_id, + file_name, + duration, + width, + height, + })) +} +``` + +--- + +## 9. 失效策略 + +### 9.1 寫操作觸發失效 + +| 操作 | 失效範圍 | +|------|----------| +| `POST /api/v1/register` | `videos:*` | +| 刪除視頻 | `video:{uuid}`, `videos:*` | +| 更新視頻 | `video:{uuid}` | +| 向量更新 | `search:*`, `search:hybrid:*`, `search:n8n:*` | + +### 9.2 失效實現 + +```rust +// Invalidation helper methods + +impl MongoCache { + pub async fn invalidate_videos_list(&self) -> Result { + self.invalidate_category(keys::CATEGORY_VIDEOS).await + } + + pub async fn invalidate_video(&self, uuid: &str) -> Result { + let key = keys::video_meta(uuid); + let count = self.invalidate_prefix(&key).await?; + Ok(count + self.invalidate_videos_list().await?) + } + + pub async fn invalidate_all_search(&self) -> Result { + let count = self.invalidate_category(keys::CATEGORY_SEARCH).await?; + let count2 = self.invalidate_category(keys::CATEGORY_HYBRID_SEARCH).await?; + let count3 = self.invalidate_category(keys::CATEGORY_N8N_SEARCH).await?; + Ok(count + count2 + count3) + } +} +``` + +--- + +## 10. 實現步驟 + +### Phase 1: 基礎設施 + +| 步驟 | 任務 | 檔案 | +|------|------|------| +| 1.1 | 更新 Cargo.toml mongodb feature | `Cargo.toml` | +| 1.2 | 添加 MongoDB 配置到 config.rs | `src/core/config.rs` | +| 1.3 | 創建 cache 模組目錄 | `src/core/cache/` | +| 1.4 | 實現 CacheEntry 和 keys 工具 | `src/core/cache/keys.rs` | +| 1.5 | 實現 CacheConfig | `src/core/cache/config.rs` | +| 1.6 | 重構 MongoDb 使用原生驅動 | `src/core/db/mongodb_db.rs` | +| 1.7 | 實現 MongoCache | `src/core/cache/mongo_cache.rs` | +| 1.8 | 實現 RedisCache | `src/core/cache/redis_cache.rs` | +| 1.9 | 更新 db/mod.rs 導出 | `src/core/db/mod.rs` | + +### Phase 2: API 整合 + +| 步驟 | 任務 | 檔案 | +|------|------|------| +| 2.1 | 擴展 AppState | `src/api/server.rs` | +| 2.2 | 整合 list_videos 緩存 | `src/api/server.rs` | +| 2.3 | 整合 lookup 緩存 | `src/api/server.rs` | +| 2.4 | 整合 search 緩存 | `src/api/server.rs` | +| 2.5 | 整合 hybrid_search 緩存 | `src/api/server.rs` | +| 2.6 | 整合 n8n_search 緩存 | `src/api/server.rs` | +| 2.7 | 整合 health 緩存 | `src/api/server.rs` | +| 2.8 | 添加 register 緩存失效 | `src/api/server.rs` | + +### Phase 3: 測試驗證 + +| 步驟 | 任務 | +|------|------| +| 3.1 | cargo check | +| 3.2 | cargo build | +| 3.3 | cargo clippy | +| 3.4 | cargo fmt | +| 3.5 | cargo test | +| 3.6 | 手動 API 測試 | + +--- + +## 11. 測試策略 + +### 11.1 單元測試 + +```rust +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_cache_key_generation() { + assert_eq!( + keys::videos_list(1, 20), + "videos:list:page=1:limit=20" + ); + assert_eq!( + keys::video_meta("abc123"), + "video:abc123" + ); + } + + #[tokio::test] + async fn test_cache_hit_miss() { + let cache = MongoCache::init().await.unwrap(); + + // Set value + cache.set("test_key", &"test_value".to_string(), 60, "test").await.unwrap(); + + // Get value + let value: Option = cache.get("test_key").await.unwrap(); + assert_eq!(value, Some("test_value".to_string())); + + // Invalidate + cache.invalidate_category("test").await.unwrap(); + + // Get again + let value: Option = cache.get("test_key").await.unwrap(); + assert_eq!(value, None); + } +} +``` + +### 11.2 API 測試腳本 + +```bash +# Test cache hit +curl -s http://localhost:8080/api/v1/videos | jq .videos | wc -l +# Should return cached count + +# Force cache miss (wait for TTL or invalidate) +curl -s -X POST http://localhost:8080/api/v1/register \ + -H "Content-Type: application/json" \ + -d '{"path": "/path/to/new/video.mp4"}' + +# Verify cache was invalidated +curl -s http://localhost:8080/api/v1/videos | jq .videos | wc -l +# Should trigger fresh query +``` + +--- + +## 12. 監控指標 + +### 12.1 日誌 + +```rust +// 在 cache 命中/未命中時記錄 +tracing::debug!("Cache hit for key: {}", key); +tracing::debug!("Cache miss for key: {}", key); + +// 在失效時記錄 +tracing::info!("Invalidated {} entries in category: {}", count, category); +``` + +### 12.2 可選指標 + +| 指標 | 描述 | +|------|------| +| `cache_hit_total` | Cache 命中總數 | +| `cache_miss_total` | Cache 未命中總數 | +| `cache_invalidations_total` | 緩存失效總數 | +| `cache_operation_duration_seconds` | 緩存操作延遲 | + +--- + +## 13. 風險與緩解 + +| 風險 | 影響 | 緩解措施 | +|------|------|----------| +| MongoDB 連接失敗 | 降級到無緩存 | 緩存操作添加 `.ok()` 錯誤處理 | +| 緩存數據過期不一致 | 用戶看到舊數據 | 合理的 TTL 值 + 寫時失效 | +| 緩存 key 衝突 | 返回錯誤數據 | 使用 SHA256 hash 確保唯一性 | +| 緩存空間膨脹 | 記憶體/磁碟佔用過大 | TTL 自動過期 + 最大條目限制 | + +--- + +## 14. 預期效益 + +| 指標 | 改善前 | 預期改善後 | +|------|--------|------------| +| `GET /api/v1/videos` 延遲 | ~200ms | ~20ms (Cache Hit) | +| `GET /api/v1/lookup` 延遲 | ~50ms | ~5ms (Cache Hit) | +| `POST /api/v1/search` 延遲 | ~500ms | ~50ms (Cache Hit) | +| 資料庫負載 | 100% | ~30% | +| API 吞吐量 | 100 RPS | ~300 RPS | + +--- + +## 15. 附錄 + +### A. MongoDB 初始化腳本 + +```javascript +// 初始化 momento.cache collection 和索引 +use momento; + +db.cache.drop(); + +db.cache.insertOne({ + key: "init", + value: { initialized: true }, + category: "system", + created_at: new Date(), + expires_at: new Date(Date.now() + 86400000), + hit_count: 0, + last_access: new Date() +}); + +db.cache.createIndex( + { "expires_at": 1 }, + { expireAfterSeconds: 0 } +); + +db.cache.createIndex( + { "key": 1 }, + { unique: true } +); + +db.cache.createIndex({ "category": 1 }); + +db.cache.deleteOne({ key: "init" }); + +print("Cache collection initialized successfully"); +``` + +### B. 環境變數參考 + +```bash +# .env 或 shell 環境 +MONGODB_URL=mongodb://localhost:27017 +MONGODB_CACHE_ENABLED=true +MONGODB_CACHE_TTL_VIDEOS=300 +MONGODB_CACHE_TTL_SEARCH=300 +MONGODB_CACHE_TTL_HYBRID_SEARCH=600 +MONGODB_CACHE_TTL_VIDEO_META=3600 + +REDIS_CACHE_TTL_HEALTH=30 +REDIS_CACHE_TTL_VIDEO_META=3600 +``` diff --git a/docs/CHUNK_DESIGN.md b/docs/CHUNK_DESIGN.md new file mode 100644 index 0000000..0865204 --- /dev/null +++ b/docs/CHUNK_DESIGN.md @@ -0,0 +1,534 @@ +# Momentry Core 數據管理設計文檔 (v4) + +| 項目 | 內容 | +|------|------| +| 建立者 | Warren | +| 建立時間 | 2026-03-17 | +| 文件版本 | V1.0 | + +--- + +## 版本歷史 + +| 版本 | 日期 | 目的 | 操作人 | 工具/模型 | +|------|------|------|--------|-----------| +| V1.0 | 2026-03-17 | 創建文件 | Warren | OpenCode / MiniMax M2.5 | + +--- + +## 0. 核心概念:雙 UUID 系統 + +為減少資料庫大小,在現有的 videos 表中增加內部 ID 映射: + +### 0.1 設計原則 + +- **external_uuid**: 用戶可見的識別碼(如 UUID) +- **id**: 資料庫自動產生的內部 ID (SERIAL),節省空間 +- **映射關係**: 透過 videos 表的 `id` 欄位關聯 + +### 0.2 videos 表 (檔案映射表) + +現有結構,增加 `id` 作為內部 ID: + +```sql +-- 現有 videos 表結構 +CREATE TABLE videos ( + id SERIAL PRIMARY KEY, -- 內部 ID (自動產生) + uuid VARCHAR(32) UNIQUE NOT NULL, -- 外部 UUID (用戶可見) + file_name VARCHAR(255) NOT NULL, + file_path TEXT, + duration DOUBLE PRECISION, + width INTEGER, + height INTEGER, + fps DOUBLE PRECISION, + probe_json JSONB, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_videos_uuid ON videos(uuid); +``` + +### 0.3 對照的好處 + +| 方式 | 儲存空間 (1000個視頻,每個1000個chunk) | +|------|---------------------------------------| +| 直接用 uuid (32字元) | ~32MB | +| 使用 id (4字元) | ~4MB | + +## 1. 數據流架構 + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ 輸入階段 │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ 視頻文件 │→ │ ffprobe │ │ ASR │ │ YOLO │ │ +│ │ (.mp4) │→ │ (probe) │→ │ (asr) │→ │ (yolo) │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ +│ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ ASRX │ │ CUT │ │ OCR │ │ FACE │ │ +│ │ (asrx) │→ │ (cut) │→ │ (ocr) │→ │ (face) │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ +└─────────────────────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Pre-Chunk / Frame 階段 │ +│ │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ pre_chunks 表 │ │ +│ │ file_id → videos.id (FK) │ │ +│ │ ┌─────────────────────────────────────────────────────────────┐ │ │ +│ │ │ type=sentence │ from asr, asrx │ 句子邊界範圍 │ │ │ +│ │ │ type=cut │ from cut detection │ 場景切換範圍 │ │ │ +│ │ │ type=time │ from time split │ 固定時間範圍 (10s) │ │ │ +│ │ │ type=trace │ from yolo trace │ 物件追蹤範圍 │ │ │ +│ │ └─────────────────────────────────────────────────────────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ frames 表 │ │ +│ │ file_id → videos.id (FK) │ │ +│ │ - yolo 每幀識別結果 │ │ +│ │ - ocr 每幀識別結果 │ │ +│ │ - face 每幀識別結果 (如需要) │ │ +│ │ - 單一圖像識別結果 → 直接入 frame │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Chunk 階段 │ +│ │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ chunks 表 │ │ +│ │ file_id → videos.id (FK) │ │ +│ │ │ │ +│ │ 組合規則1: pre_chunk → chunk (直接轉換) │ │ +│ │ ┌─────────────────────────────────────────────────────────────┐ │ │ +│ │ │ sentence_pre_chunk → sentence_chunk │ │ │ +│ │ │ cut_pre_chunk → cut_chunk │ │ │ +│ │ │ time_pre_chunk → time_chunk │ │ │ +│ │ │ trace_pre_chunk → trace_chunk │ │ │ +│ │ └─────────────────────────────────────────────────────────────┘ │ │ +│ │ │ │ +│ │ 組合規則2: pre_chunk + frame 內容 → chunk (集合內容) │ │ +│ │ ┌─────────────────────────────────────────────────────────────┐ │ │ +│ │ │ sentence_pre_chunk + 涵蓋範圍內的 frames → 豐富的 sentence_chunk │ │ │ +│ │ │ time_pre_chunk + 涵蓋範圍內的 frames → 豐富的 time_chunk │ │ │ +│ │ └─────────────────────────────────────────────────────────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Vector 階段 │ +│ ┌──────────────────────┐ ┌──────────────────────┐ │ +│ │ PostgreSQL vectors │ │ Qdrant vectors │ │ +│ │ (chunk_vectors) │ │ (chunk_v3) │ │ +│ └──────────────────────┘ └──────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +## 2. Pre-Chunk 類型定義 + +### 2.1 Pre-Chunk 來源與類型對照表 + +| 來源類型 | source_type | 產出 Pre-Chunk Type | 說明 | +|---------|-------------|---------------------|------| +| ASR ( Whisper ) | asr | sentence | 句子邊界 | +| ASRX ( with timestamps ) | asrx | sentence | 帶時間戳的句子 | +| CUT (場景檢測) | cut | cut | 場景切換點 | +| TIME (固定時間) | time | time | 每 10 秒 | +| YOLO Trace | yolo_trace | trace | 物件追蹤軌跡 | +| YOLO (單幀) | yolo | **→ frame** | 不入 pre_chunk | +| OCR (單幀) | ocr | **→ frame** | 不入 pre_chunk | +| FACE (單幀) | face | **→ frame** | 不入 pre_chunk | +| PROBE | probe | metadata | 視頻元數據 | + +### 2.2 Pre-Chunk Schema + +```sql +CREATE TABLE pre_chunks ( + id SERIAL PRIMARY KEY, + + -- 檔案識別 (使用 videos 表的內部 ID 以節省空間) + file_id INTEGER NOT NULL REFERENCES videos(id), + + -- 來源識別 + source_type VARCHAR(32) NOT NULL, -- 'asr', 'asrx', 'cut', 'time', 'yolo_trace', 'probe' + source_file TEXT, -- 原始 JSON 文件路徑 + + -- Chunk 類型 + chunk_type VARCHAR(32) NOT NULL, -- 'sentence', 'cut', 'time', 'trace' + + -- 時間範圍 + start_time DOUBLE PRECISION NOT NULL, + end_time DOUBLE PRECISION NOT NULL, + + -- Frame 範圍 (精確到 frame) + start_frame INTEGER NOT NULL, + end_frame INTEGER NOT NULL, + + -- FPS (用於 frame 計算) + fps DOUBLE PRECISION NOT NULL, + + -- 原始 JSON 內容 + raw_json JSONB NOT NULL, + + -- 解析後的文字內容 (如有) + text_content TEXT, + + -- 處理狀態 + processed BOOLEAN DEFAULT FALSE, + chunk_id VARCHAR(64), -- 轉換後的 chunk_id + + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + UNIQUE(file_id, source_type, start_frame, end_frame) +); + +CREATE INDEX idx_pre_chunks_file_id ON pre_chunks(file_id); +CREATE INDEX idx_pre_chunks_type ON pre_chunks(file_id, chunk_type); +CREATE INDEX idx_pre_chunks_time ON pre_chunks(file_id, start_time, end_time); +CREATE INDEX idx_pre_chunks_frame ON pre_chunks(file_id, start_frame, end_frame); +CREATE INDEX idx_pre_chunks_processed ON pre_chunks(file_id, processed); +``` + +## 3. Frame 管理原則 + +### 3.1 哪些數據進入 Frame + +只儲存**單一圖像識別**的結果: +- YOLO 每幀檢測結果 +- OCR 每幀識別結果 +- FACE 每幀檢測結果 + +### 3.2 Frame Schema + +```sql +CREATE TABLE frames ( + id SERIAL PRIMARY KEY, + + -- 檔案識別 (使用 videos 表的內部 ID 以節省空間) + file_id INTEGER NOT NULL REFERENCES videos(id), + + frame_number INTEGER NOT NULL, + timestamp DOUBLE PRECISION NOT NULL, + fps DOUBLE PRECISION NOT NULL, + + -- YOLO 結果 (JSONB 陣列) + yolo_objects JSONB, + + -- OCR 結果 (JSONB 陣列) + ocr_results JSONB, + + -- Face 結果 (JSONB 陣列) + face_results JSONB, + + -- 原始幀圖像路徑 (可選) + frame_path TEXT, + + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + UNIQUE(file_id, frame_number) +); + +CREATE INDEX idx_frames_file_id ON frames(file_id); +CREATE INDEX idx_frames_frame ON frames(file_id, frame_number); +CREATE INDEX idx_frames_timestamp ON frames(file_id, timestamp); +``` + +## 4. Chunk 組合規則 + +### 4.1 組合規則 1: 直接轉換 (rule_1) + +將 pre_chunk 直接轉換為 chunk: + +``` +sentence_pre_chunk → sentence_chunk (rule: "rule_1") +cut_pre_chunk → cut_chunk (rule: "rule_1") +time_pre_chunk → time_chunk (rule: "rule_1") +trace_pre_chunk → trace_chunk (rule: "rule_1") +``` + +### 4.2 組合規則 2: 集合內容 (rule_2) + +將 pre_chunk 與其時間區間內的所有 frame 識別結果集合: + +``` +sentence_pre_chunk + frames[在 start_time~end_time 範圍內] → 豐富的 sentence_chunk (rule: "rule_2") +time_pre_chunk + frames[在 start_time~end_time 範圍內] → 豐富的 time_chunk (rule: "rule_2") +``` + +### 4.3 Chunk Schema + +```sql +CREATE TABLE chunks ( + id SERIAL PRIMARY KEY, + + -- 檔案識別 (使用 videos 表的內部 ID 以節省空間) + file_id INTEGER NOT NULL REFERENCES videos(id), + + chunk_id VARCHAR(64) NOT NULL, + chunk_index INTEGER NOT NULL, + chunk_type VARCHAR(32) NOT NULL, -- 'sentence', 'cut', 'time', 'trace' + + -- 組合規則 (payload 中記錄) + -- rule: 'rule_1' = 直接轉換, 'rule_2' = 集合內容 + + -- 時間範圍 + start_time DOUBLE PRECISION NOT NULL, + end_time DOUBLE PRECISION NOT NULL, + + -- Frame 範圍 (精確到 frame) + start_frame INTEGER NOT NULL, + end_frame INTEGER NOT NULL, + + -- FPS + fps DOUBLE PRECISION NOT NULL, + + -- 主要內容 + text_content TEXT, + + -- 完整內容 (JSONB) - 包含 rule 欄位 + content JSONB NOT NULL, + + -- 來源的 pre_chunk IDs + pre_chunk_ids INTEGER[], + + -- 包含的 frame 數量 + frame_count INTEGER DEFAULT 0, + + -- 向量 ID + vector_id VARCHAR(64), + + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + UNIQUE(file_id, chunk_id) +); + +CREATE INDEX idx_chunks_file_id ON chunks(file_id); +CREATE INDEX idx_chunks_type ON chunks(file_id, chunk_type); +CREATE INDEX idx_chunks_time ON chunks(file_id, start_time, end_time); +CREATE INDEX idx_chunks_frame ON chunks(file_id, start_frame, end_frame); +CREATE INDEX idx_chunks_vector ON chunks(vector_id); +``` + +## 5. 處理流程範例 + +### 5.1 輸入數據 + +假設視頻長度 30 秒,fps=30: + +| 來源 | 產出 | +|------|------| +| ASR | 3 個 sentence_pre_chunk (每句約 10s) | +| CUT | 2 個 cut_pre_chunk (場景 1, 場景 2) | +| TIME | 3 個 time_pre_chunk (0-10s, 10-20s, 20-30s) | +| YOLO | 900 個 frame 記錄 (每幀) | +| OCR | 依實際識別結果入 frame | + +### 5.2 Chunk 產出 + +**使用規則 1 (直接轉換):** +- rule: "rule_1" +- 3 個 sentence_chunk +- 2 個 cut_chunk +- 3 個 time_chunk + +**使用規則 2 (集合內容):** +- rule: "rule_2" +- 3 個 sentence_chunk (各含涵蓋時間範圍內的 yolo/ocr 結果) +- 3 個 time_chunk (各含涵蓋時間範圍內的 yolo/ocr 結果) + +## 8. 數據示例 + +### 8.1 videos 表 (檔案映射) + +```json +{ + "id": 1, + "uuid": "abc123def456", + "file_name": "video_001.mp4", + "file_path": "/path/to/video_001.mp4", + "duration": 300.5, + "width": 1920, + "height": 1080, + "fps": 30.0 +} +``` + +### 8.2 pre_chunks 表 (使用 file_id 關聯 videos) + +```json +{ + "file_id": 1, + "source_type": "asr", + "chunk_type": "sentence", + "start_time": 0.0, + "end_time": 5.5, + "start_frame": 0, + "end_frame": 165, + "fps": 30.0, + "raw_json": {...}, + "text_content": "This is the first sentence" +} +``` + +### 8.3 frames 表 (使用 file_id 關聯 videos) + +```json +{ + "file_id": 1, + "frame_number": 300, + "timestamp": 10.0, + "fps": 30.0, + "yolo_objects": [ + {"class": "person", "confidence": 0.9, "bbox": [100, 50, 200, 150]}, + {"class": "car", "confidence": 0.85, "bbox": [50, 100, 150, 180]} + ], + "ocr_results": [], + "face_results": [] +} +``` + +### 8.4 chunks 表 (使用 file_id 關聯 videos) + +```json +{ + "file_id": 1, + "chunk_id": "sentence_0001", + "chunk_type": "sentence", + "rule": "rule_2", + "start_time": 10.0, + "end_time": 15.5, + "start_frame": 300, + "end_frame": 465, + "fps": 30.0, + "text_content": "The second sentence from the audio", + "content": { + "rule": "rule_2", + "asr_text": "The second sentence from the audio", + "objects": [ + {"class": "person", "first_frame": 300, "last_frame": 450, "appears_in_frames": [300, 310, 320, ...]}, + {"class": "car", "first_frame": 350, "last_frame": 465, "appears_in_frames": [350, 360, ...]} + ], + "ocr": [...], + "faces": [...] + }, + "pre_chunk_ids": [5], + "frame_count": 301 +} +``` + +### 8.5 chunk_vectors 表 (使用 file_id 關聯 videos) + +```json +{ + "file_id": 1, + "chunk_id": "sentence_0001", + "chunk_type": "sentence", + "start_time": 10.0, + "end_time": 15.5, + "embedding": "[0.1, 0.2, ...]", + "metadata": {"text": "The second sentence..."} +} +``` + +### 8.6 Qdrant Payload + +```json +{ + "file_uuid": "abc123def456", + "chunk_id": "sentence_0001", + "chunk_type": "sentence", + "start_time": 10.0, + "end_time": 15.5, + "text": "The second sentence from the audio" +} +``` + +## 7. 向量管理原則 + +### 7.1 Vector Schema + +```sql +-- Chunk 向量表 (PostgreSQL) +CREATE TABLE chunk_vectors ( + id SERIAL PRIMARY KEY, + + -- 檔案識別 (使用 videos 表的內部 ID 以節省空間) + file_id INTEGER NOT NULL REFERENCES videos(id), + + chunk_id VARCHAR(64) NOT NULL, + chunk_type VARCHAR(32) NOT NULL, + + -- 向量數據 + embedding TEXT, -- JSON 格式的向量 + embedding_vector VECTOR(768), -- pgvector 類型 (如可用) + + -- 時間範圍 (用於時間查詢) + start_time DOUBLE PRECISION, + end_time DOUBLE PRECISION, + + -- Metadata + metadata JSONB, + + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + UNIQUE(chunk_id) +); + +-- 索引 +CREATE INDEX idx_chunk_vectors_file_id ON chunk_vectors(file_id); +``` + +### 7.2 Qdrant Collection + +- Collection 名稱: `chunks_v3` +- Vector 維度: 768 (nomic-embed-text) +- Payload 包含: `file_uuid`, `chunk_id`, `chunk_type`, `start_time`, `end_time`, `text` + +> **注意**: Qdrant 中仍使用 uuid (字串),因為需要可讀性和跨系統整合。PostgreSQL 內部使用 videos.id (整數) 以節省空間。 + +## 9. 設計原則總結 + +1. **單一圖像識別 → Frame**: yolo, ocr, face 等單幀識別結果直接入 frame 表 +2. **時間序列識別 → Pre-Chunk**: asr, asrx, cut, time, trace 等有時間範圍的結果入 pre_chunk 表 +3. **組合規則 1 (直接)**: pre_chunk → chunk (保持原有邊界) +4. **組合規則 2 (集合)**: pre_chunk + frames → chunk (加入識別內容) +5. **精確到 Frame**: 所有時間範圍都記錄 start_frame, end_frame +6. **雙向量存儲**: 同時支持 PostgreSQL 和 Qdrant +7. **跨視頻搜索**: 透過 videos 表的 uuid 進行搜索,內部使用 id 節省空間 +8. **空間優化**: 內部表使用 videos.id (4 bytes) 而非 uuid (32 bytes) + +## 10. 查詢範例 + +### 10.1 跨視頻搜索所有 chunk + +```sql +-- 搜索所有視頻中包含 "hello" 的 chunk +SELECT c.*, v.uuid, v.file_name +FROM chunks c +JOIN videos v ON c.file_id = v.id +WHERE c.text_content ILIKE '%hello%'; +``` + +### 10.2 查詢特定視頻的 chunk + +```sql +-- 查詢 uuid 為 'abc123' 的視頻的所有 chunk +SELECT c.* +FROM chunks c +JOIN videos v ON c.file_id = v.id +WHERE v.uuid = 'abc123'; +``` + +### 10.3 按時間範圍搜索 + +```sql +-- 搜索所有視頻在 10-20 秒範圍內的 chunk +SELECT c.*, v.uuid +FROM chunks c +JOIN videos v ON c.file_id = v.id +WHERE c.start_time >= 10.0 AND c.end_time <= 20.0; +``` diff --git a/docs/CHUNK_SPEC.md b/docs/CHUNK_SPEC.md index d4e1b2f..3b90667 100644 --- a/docs/CHUNK_SPEC.md +++ b/docs/CHUNK_SPEC.md @@ -1,5 +1,21 @@ # Video Chunk 切分規範 +| 項目 | 內容 | +|------|------| +| 建立者 | Warren | +| 建立時間 | 2026-03-16 | +| 文件版本 | V1.0 | + +--- + +## 版本歷史 + +| 版本 | 日期 | 目的 | 操作人 | 工具/模型 | +|------|------|------|--------|-----------| +| V1.0 | 2026-03-16 | 創建文件 | Warren | OpenCode / MiniMax M2.5 | + +--- + 本文檔定義 Momentry Core 系統中影片 chunks 的切分原則與資料結構。 --- @@ -579,7 +595,518 @@ TimeBased Chunks (4 個, 重疊 2秒): --- -## 10. 相關文件 +## 10. 資料庫儲存 + +### 10.1 PostgreSQL 儲存 + +#### Table Schema + +```sql +CREATE TABLE chunks ( + id BIGSERIAL PRIMARY KEY, + uuid VARCHAR(16) NOT NULL, + chunk_id VARCHAR(64) NOT NULL, + chunk_index INTEGER NOT NULL, + chunk_type VARCHAR(32) NOT NULL, + start_time DOUBLE PRECISION NOT NULL, + start_frame BIGINT NOT NULL, + end_time DOUBLE PRECISION NOT NULL, + end_frame BIGINT NOT NULL, + fps VARCHAR(16) NOT NULL, + fps_value DOUBLE PRECISION NOT NULL, + content JSONB NOT NULL, + metadata JSONB, + vector_id VARCHAR(64), + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + UNIQUE(uuid, chunk_id) +); + +-- 索引 +CREATE INDEX idx_chunks_uuid ON chunks(uuid); +CREATE INDEX idx_chunks_type ON chunks(chunk_type); +CREATE INDEX idx_chunks_time ON chunks(start_time, end_time); +CREATE INDEX idx_chunks_uuid_type ON chunks(uuid, chunk_type); +CREATE INDEX idx_chunks_vector_id ON chunks(vector_id); +``` + +#### 儲存範例 + +```rust +pub async fn store_chunk_to_postgres(db: &PostgresDb, chunk: &Chunk) -> Result<()> { + sqlx::query!( + r#" + INSERT INTO chunks ( + uuid, chunk_id, chunk_index, chunk_type, + start_time, start_frame, end_time, end_frame, + fps, fps_value, content, metadata, vector_id + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT (uuid, chunk_id) DO UPDATE SET + content = EXCLUDED.content, + metadata = EXCLUDED.metadata, + vector_id = EXCLUDED.vector_id, + updated_at = NOW() + "#, + chunk.uuid, + chunk.chunk_id, + chunk.chunk_index as i32, + chunk.chunk_type.as_str(), + chunk.start_time, + chunk.start_frame, + chunk.end_time, + chunk.end_frame, + chunk.fps, + chunk.fps_value, + serde_json::to_value(&chunk.content)?, + serde_json::to_value(&chunk.metadata)?, + chunk.vector_id, + ) + .execute(&db.pool) + .await?; + Ok(()) +} +``` + +--- + +### 10.2 MongoDB 儲存 + +#### Collection Schema + +```javascript +// chunks collection +{ + _id: ObjectId, + uuid: "1636719dc31f78ac", + chunk_id: "sentence_0001", + chunk_index: 1, + chunk_type: "sentence", + start_time: 10.5, + start_frame: 252, + end_time: 15.75, + end_frame: 378, + fps: "24/1", + fps_value: 24.0, + content: { + text: "Hello world, this is a test", + text_normalized: "hello world this is a test", + word_count: 7, + char_count: 34 + }, + metadata: { + source: "asr", + confidence: 0.95, + language: "en" + }, + vector_id: "vec_sentence_0001", + created_at: ISODate("2026-03-16T10:00:00Z"), + updated_at: ISODate("2026-03-16T10:00:00Z") +} + +// 索引 +db.chunks.createIndex({ uuid: 1 }) +db.chunks.createIndex({ chunk_type: 1 }) +db.chunks.createIndex({ start_time: 1, end_time: 1 }) +db.chunks.createIndex({ vector_id: 1 }) +db.chunks.createIndex({ uuid: 1, chunk_type: 1 }) +``` + +#### 儲存範例 + +```rust +pub async fn store_chunk_to_mongodb(db: &MongoDb, chunk: &Chunk) -> Result<()> { + let doc = bson::doc! { + "uuid": chunk.uuid, + "chunk_id": chunk.chunk_id, + "chunk_index": chunk.chunk_index, + "chunk_type": chunk.chunk_type.as_str(), + "start_time": chunk.start_time, + "start_frame": chunk.start_frame, + "end_time": chunk.end_time, + "end_frame": chunk.end_frame, + "fps": chunk.fps, + "fps_value": chunk.fps_value, + "content": serde_json::to_value(&chunk.content)?, + "metadata": serde_json::to_value(&chunk.metadata)?, + "vector_id": chunk.vector_id, + "created_at": chrono::Utc::now(), + "updated_at": chrono::Utc::now() + }; + + let collection = db.database("momentry").collection("chunks"); + collection.update_one( + doc! { "uuid": &chunk.uuid, "chunk_id": &chunk.chunk_id }, + doc! { "$set": doc }, + UpdateOptions::builder().upsert(true).build(), + ).await?; + Ok(()) +} +``` + +--- + +## 11. 向量儲存設計 + +### 11.1 設計原則 + +**統一向量 ID 格式**,確保 Qdrant 與 PostgreSQL 相容: + +``` +{chunk_type}_{chunk_index:04} + +範例: +sentence_0001 +cut_0002 +time_based_0015 +``` + +### 11.2 Qdrant Collection + +#### 建立 Collection + +```bash +# 使用 Qdrant client 建立 collection +curl -X PUT http://localhost:6333/collections/chunks \ + -H "Content-Type: application/json" \ + -H "api-key: Test3200Test3200Test3200" \ + -d '{ + "vectors": { + "size": 768, + "distance": "Cosine" + } + }' +``` + +#### Point 結構 + +```json +{ + "id": "sentence_0001", + "vector": [0.123, -0.456, ...], + "payload": { + "uuid": "1636719dc31f78ac", + "chunk_id": "sentence_0001", + "chunk_type": "sentence", + "chunk_index": 1, + "start_time": 10.5, + "end_time": 15.75, + "text": "Hello world, this is a test", + "metadata": { + "confidence": 0.95, + "language": "en" + } + } +} +``` + +#### Rust 結構 + +```rust +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VectorPoint { + pub id: String, + pub vector: Vec, + pub payload: VectorPayload, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VectorPayload { + pub uuid: String, + pub chunk_id: String, + pub chunk_type: String, + pub chunk_index: u32, + pub start_time: f64, + pub end_time: f64, + #[serde(skip_serializing_if = "Option::is_none")] + pub text: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub scene_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub segment_number: Option, + pub metadata: Option, +} +``` + +### 11.3 PostgreSQL Vector 儲存 + +#### Table Schema + +```sql +-- 使用 pgvector 擴展 +CREATE EXTENSION IF NOT EXISTS vector; + +CREATE TABLE chunk_vectors ( + id BIGSERIAL PRIMARY KEY, + vector_id VARCHAR(64) NOT NULL UNIQUE, + uuid VARCHAR(16) NOT NULL, + chunk_id VARCHAR(64) NOT NULL, + chunk_type VARCHAR(32) NOT NULL, + chunk_index INTEGER NOT NULL, + start_time DOUBLE PRECISION NOT NULL, + end_time DOUBLE PRECISION NOT NULL, + embedding vector(768) NOT NULL, + metadata JSONB, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + + FOREIGN KEY (uuid, chunk_id) REFERENCES chunks(uuid, chunk_id) +); + +-- 向量檢索索引 (IVFFlat) +CREATE INDEX idx_chunk_vectors_embedding +ON chunk_vectors +USING ivfflat (embedding vector_cosine_ops) +WITH (lists = 100); + +-- 查詢索引 +CREATE INDEX idx_chunk_vectors_uuid ON chunk_vectors(uuid); +CREATE INDEX idx_chunk_vectors_type ON chunk_vectors(chunk_type); +``` + +#### 儲存範例 + +```rust +pub async fn store_vector_to_postgres(db: &PostgresDb, point: &VectorPoint) -> Result<()> { + sqlx::query!( + r#" + INSERT INTO chunk_vectors ( + vector_id, uuid, chunk_id, chunk_type, chunk_index, + start_time, end_time, embedding, metadata + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT (vector_id) DO UPDATE SET + embedding = EXCLUDED.embedding, + metadata = EXCLUDED.metadata + "#, + point.id, + point.payload.uuid, + point.payload.chunk_id, + point.payload.chunk_type, + point.payload.chunk_index as i32, + point.payload.start_time, + point.payload.end_time, + point.vector, + serde_json::to_value(&point.payload.metadata)?, + ) + .execute(&db.pool) + .await?; + Ok(()) +} +``` + +--- + +## 12. 查詢範例 + +### 12.1 語義搜尋 (Semantic Search) + +#### 查詢類型 1: 相似文字搜尋 + +```rust +// 搜尋與問句相似的 chunks +pub async fn semantic_search( + qdrant: &QdrantDb, + query: &str, + limit: usize, +) -> Result> { + // 1. 將問句向量化 + let query_vector = embed_text(query).await?; + + // 2. 搜尋 Qdrant + let results = qdrant.search( + "chunks", + &query_vector, + limit, + Some(&Filter::must([ + Condition::Match("chunk_type", "sentence"), + ])), + ).await?; + + Ok(results) +} + +// 使用範例 +let results = semantic_search(&qdrant, "找出有人在說話的片段", 10).await?; +for r in results { + println!("{}: {:.3}", r.payload.chunk_id, r.score); + println!(" Time: {}s - {}s", r.payload.start_time, r.payload.end_time); + println!(" Text: {:?}", r.payload.text); +} +``` + +#### 查詢類型 2: 語音/文字混合搜尋 + +```sql +-- PostgreSQL: 搜尋特定文字的 chunks +SELECT + c.chunk_id, + c.chunk_type, + c.start_time, + c.end_time, + c.content->>'text' as text, + v.embedding <=> query_embedding('找出開車的場景') as similarity +FROM chunks c +LEFT JOIN chunk_vectors v ON c.chunk_id = v.chunk_id +WHERE c.chunk_type = 'sentence' +AND c.content->>'text' ILIKE '%car%' +ORDER BY v.embedding <=> query_embedding('找出開車的場景') +LIMIT 10; +``` + +### 12.2 時間範圍搜尋 + +#### 查詢類型 3: 特定時間範圍 + +```rust +// 找出 30-60 秒之間的所有 chunks +pub async fn search_by_time_range( + db: &PostgresDb, + uuid: &str, + start: f64, + end: f64, +) -> Result> { + let chunks = sqlx::query_as!( + Chunk, + r#" + SELECT * FROM chunks + WHERE uuid = $1 + AND start_time < $3 + AND end_time > $2 + ORDER BY chunk_type, chunk_index + "#, + uuid, start, end + ) + .fetch_all(&db.pool) + .await?; + Ok(chunks) +} + +// 使用範例 +let chunks = search_by_time_range(&db, "1636719dc31f78ac", 30.0, 60.0).await?; +``` + +```javascript +// MongoDB: 時間範圍查詢 +db.chunks.find({ + uuid: "1636719dc31f78ac", + start_time: { $lt: 60 }, + end_time: { $gt: 30 } +}).sort({ chunk_type: 1, chunk_index: 1 }) +``` + +### 12.3 混合搜尋 (Hybrid Search) + +#### 查詢類型 4: 文字關鍵詞 + 向量相似度 + +```rust +// 結合關鍵詞匹配與向量相似度 +pub async fn hybrid_search( + db: &PostgresDb, + qdrant: &QdrantDb, + query: &str, + keywords: &[&str], + limit: usize, +) -> Result> { + // 1. 向量搜尋 + let query_vector = embed_text(query).await?; + let vector_results = qdrant.search("chunks", &query_vector, limit * 2, None).await?; + + // 2. 關鍵詞過濾 + let keyword_filter: Vec<_> = keywords.iter() + .map(|k| format!("%{}%", k)) + .collect(); + + let filtered: Vec<_> = vector_results.into_iter() + .filter(|r| { + if let Some(text) = &r.payload.text { + keyword_filter.iter().any(|k| text.contains(k.as_str())) + } else { + false + } + }) + .take(limit) + .collect(); + + Ok(filtered) +} +``` + +### 12.4 場景搜尋 + +#### 查詢類型 5: 找出特定場景 + +```sql +-- PostgreSQL: 找出特定場景 ID 的 chunks +SELECT * FROM chunks +WHERE uuid = '1636719dc31f78ac' +AND chunk_type = 'cut' +AND (content->>'scene_id')::int = 5; + +-- 找出包含轉場效果的 chunks +SELECT * FROM chunks +WHERE uuid = '1636719dc31f78ac' +AND chunk_type = 'cut' +AND content->>'transition_type' = 'dissolve'; +``` + +### 12.5 影片摘要 + +#### 查詢類型 6: 產生影片摘要 + +```sql +-- 合併影片所有語句 +SELECT + string_agg(content->>'text', ' ' ORDER BY start_time) as full_transcript +FROM chunks +WHERE uuid = '1636719dc31f78ac' +AND chunk_type = 'sentence' +AND content->>'text' IS NOT NULL; + +-- 按場景聚合文字 +SELECT + content->>'scene_id' as scene, + string_agg(content->>'text', ' ' ORDER BY start_time) as scene_text +FROM chunks +WHERE uuid = '1636719dc31f78ac' +AND chunk_type = 'cut' +GROUP BY content->>'scene_id' +ORDER BY MIN(start_time); +``` + +### 12.6 常見查詢模式 + +| 查詢類型 | 描述 | 資料庫 | SQL/程式碼 | +|----------|------|--------|-------------| +| 語義搜尋 | 找相似內容 | Qdrant | `search(vector, limit)` | +| 關鍵詞搜尋 | 精確文字匹配 | PostgreSQL | `ILIKE '%keyword%'` | +| 時間範圍 | 特定時段 | Both | `start_time < end AND end_time > start` | +| 場景搜尋 | 特定鏡頭 | PostgreSQL | `scene_id = N` | +| 混合搜尋 | 向量+關鍵詞 | Both |結合以上兩種 | +| 摘要產生 | 合併文字 | PostgreSQL | `string_agg()` | + +--- + +## 13. 資料庫選擇建議 + +### 13.1 儲存策略 + +| 資料類型 | 主要儲存 | 備份/查詢 | 說明 | +|----------|----------|-----------|------| +| **Chunk 元數據** | PostgreSQL | MongoDB | 結構化查詢為主 | +| **向量資料** | Qdrant | PostgreSQL | 向量搜尋為主 | +| **全文檢索** | PostgreSQL | - | 關鍵詞搜尋 | +| **日誌/歷史** | MongoDB | - | 靈活性為主 | + +### 13.2 讀寫模式 + +| 場景 | 寫入 | 讀取 | +|------|------|------| +| **影片處理** | PostgreSQL + Qdrant | - | +| **語義搜尋** | - | Qdrant | +| **時間軸瀏覽** | - | PostgreSQL | +| **系統分析** | MongoDB | MongoDB | + +--- + +## 14. 相關文件 - [JSON_OUTPUT_SPEC.md](./JSON_OUTPUT_SPEC.md) - JSON 輸出規範 - [RUST_DEVELOPMENT.md](./RUST_DEVELOPMENT.md) - Rust 開發規範 diff --git a/docs/DEMO_MANUAL.md b/docs/DEMO_MANUAL.md new file mode 100644 index 0000000..33f2be4 --- /dev/null +++ b/docs/DEMO_MANUAL.md @@ -0,0 +1,674 @@ +# Momentry Core API 示範手冊 + +| 項目 | 內容 | +|------|------| +| 版本 | V1.0 | +| 日期 | 2026-03-25 | +| 狀態 | 完成 | + +--- + +## 快速開始 + +### Demo API Key + +``` +API Key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69 +Key ID: muser_68600856036340bcafc01930eb4bd839 +過期日: 2027-03-25 +``` + +### 測試連線 + +```bash +curl http://localhost:3002/health +``` + +```json +{"status":"ok","version":"0.1.0","uptime_ms":456464} +``` + +### 測試認證 + +```bash +curl -H "X-API-Key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69" \ + http://localhost:3002/api/v1/videos | jq '.videos | length' +``` + +```json +13 +``` + +--- + +## 環境 URL + +| 環境 | URL | 用途 | +|------|-----|------| +| **本地開發** | `http://localhost:3002` | 本機開發測試 | +| **外部訪問** | `https://api.momentry.ddns.net` | n8n/WordPress/curl 生產環境 | + +--- + +## 端點總覽 + +| 方法 | 端點 | 說明 | 認證 | +|------|------|------|------| +| GET | `/health` | 健康檢查 | 公開 | +| GET | `/health/detailed` | 詳細健康檢查 | 公開 | +| POST | `/api/v1/register` | 註冊影片 | 需要 | +| POST | `/api/v1/probe` | 探測影片資訊 | 需要 | +| POST | `/api/v1/search` | 語意搜尋 | 需要 | +| POST | `/api/v1/n8n/search` | n8n 格式搜尋 | 需要 | +| POST | `/api/v1/search/hybrid` | 混合搜尋 | 需要 | +| GET | `/api/v1/videos` | 列出所有影片 | 需要 | +| GET | `/api/v1/lookup` | 查詢影片 UUID | 需要 | +| GET | `/api/v1/progress/:uuid` | 處理進度 | 需要 | +| GET | `/api/v1/jobs` | 任務列表 | 需要 | +| GET | `/api/v1/jobs/:uuid` | 任務詳情 | 需要 | + +--- + +## 1. curl 範例 + +### 基本格式 + +```bash +curl -H "X-API-Key: YOUR_API_KEY" \ + -H "Content-Type: application/json" \ + URL +``` + +### 1.1 健康檢查(公開) + +```bash +# 基本健康檢查 +curl http://localhost:3002/health + +# 詳細健康檢查(含服務狀態) +curl http://localhost:3002/health/detailed +``` + +### 1.2 列出影片 + +```bash +curl -H "X-API-Key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69" \ + http://localhost:3002/api/v1/videos | jq '.' +``` + +```json +{ + "videos": [ + { + "uuid": "952f5854b9febad1", + "file_name": "ExaSAN PCIe series - Director Ou Yu-Zhi Shares His Experience.mp4", + "duration": 159.637188, + "width": 640, + "height": 360 + }, + ... + ] +} +``` + +### 1.3 搜尋影片 + +```bash +curl -X POST \ + -H "X-API-Key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69" \ + -H "Content-Type: application/json" \ + -d '{"query": "ExaSAN", "limit": 5}' \ + http://localhost:3002/api/v1/search | jq '.' +``` + +```json +{ + "results": [ + { + "uuid": "952f5854b9febad1", + "chunk_id": "...", + "text": "...", + "score": 0.85, + "start_time": 0.0, + "end_time": 5.0 + } + ], + "total": 1, + "query": "ExaSAN", + "took_ms": 123 +} +``` + +### 1.4 查詢進度 + +```bash +curl -H "X-API-Key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69" \ + http://localhost:3002/api/v1/progress/952f5854b9febad1 | jq '.' +``` + +```json +{ + "uuid": "952f5854b9febad1", + "overall_progress": 67, + "current_processor": "yolo", + "processors": [ + {"name": "asr", "status": "completed"}, + {"name": "cut", "status": "completed"}, + {"name": "yolo", "status": "running"} + ] +} +``` + +--- + +## 2. n8n 範例 + +### 2.1 HTTP Request 節點設定 + +``` +Method: POST +URL: https://api.momentry.ddns.net/api/v1/search +Authentication: None (使用 Header) + +Headers: +┌─────────────────────┬──────────────────────────────────────────────────┐ +│ Name │ Value │ +├─────────────────────┼──────────────────────────────────────────────────┤ +│ X-API-Key │ muser_68600856036340bcafc01930eb4bd839_... │ +│ Content-Type │ application/json │ +└─────────────────────┴──────────────────────────────────────────────────┘ + +Body Content (JSON): +{ + "query": "{{ $json.search_term }}", + "limit": 5 +} +``` + +### 2.2 n8n 搜尋 Workflow + +```json +{ + "nodes": [ + { + "name": "Manual Trigger", + "type": "n8n-nodes-base.manualTrigger", + "position": [250, 300] + }, + { + "name": "Set Search Term", + "type": "n8n-nodes-base.set", + "parameters": { + "values": { + "json": { + "search_term": "ExaSAN" + } + } + }, + "position": [450, 300] + }, + { + "name": "Search Videos", + "type": "n8n-nodes-base.httpRequest", + "parameters": { + "method": "POST", + "url": "https://api.momentry.ddns.net/api/v1/search", + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "X-API-Key", + "value": "muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69" + } + ] + }, + "sendBody": true, + "bodyContentType": "json", + "specifyBody": "json", + "jsonBody": "={{ { \"query\": $json.search_term, \"limit\": 5 } }}" + }, + "position": [650, 300] + }, + { + "name": "Process Results", + "type": "n8n-nodes-base.code", + "parameters": { + "jsCode": "// Extract video results\nconst results = $input.first().json.results;\nreturn results.map(r => ({\n uuid: r.uuid,\n text: r.text,\n score: r.score,\n time: `${r.start_time}s - ${r.end_time}s`\n}));" + }, + "position": [850, 300] + } + ], + "connections": { + "Manual Trigger": { + "main": [[{"node": "Set Search Term"}]] + }, + "Set Search Term": { + "main": [[{"node": "Search Videos"}]] + }, + "Search Videos": { + "main": [[{"node": "Process Results"}]] + } + } +} +``` + +### 2.3 n8n 列出影片 Workflow + +```json +{ + "nodes": [ + { + "name": "Get Videos", + "type": "n8n-nodes-base.httpRequest", + "parameters": { + "method": "GET", + "url": "https://api.momentry.ddns.net/api/v1/videos", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "X-API-Key", + "value": "muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69" + } + ] + } + }, + "position": [450, 300] + }, + { + "name": "Extract Video List", + "type": "n8n-nodes-base.code", + "parameters": { + "jsCode": "const videos = $input.first().json.videos;\nreturn videos.map(v => ({\n json: {\n uuid: v.uuid,\n name: v.file_name,\n duration: Math.round(v.duration) + 's',\n resolution: `${v.width}x${v.height}`\n }\n}));" + }, + "position": [650, 300] + }, + { + "name": "Slack Notification", + "type": "n8n-nodes-base.slack", + "parameters": { + "channel": "#momentry", + "text": "=Found {{ $json.length }} videos:\n{{ $json.map(v => `• ${v.name} (${v.duration})`).join(`\n`) }}" + }, + "position": [850, 300] + } + ] +} +``` + +### 2.4 n8n 定時同步 Workflow + +```json +{ + "nodes": [ + { + "name": "Schedule Trigger", + "type": "n8n-nodes-base.scheduleTrigger", + "parameters": { + "rule": { + "interval": [{"field": "hours", "hours": 1}] + } + }, + "position": [250, 300] + }, + { + "name": "Get Pending Videos", + "type": "n8n-nodes-base.httpRequest", + "parameters": { + "method": "GET", + "url": "https://api.momentry.ddns.net/api/v1/videos" + }, + "position": [450, 300] + }, + { + "name": "Filter Processing", + "type": "n8n-nodes-base.filter", + "parameters": { + "conditions": { + "options": {"caseSensitive": true}, + "conditions": [ + {"id": "status", "leftValue": "{{ $json.status }}", "rightValue": "processing"} + ] + } + }, + "position": [650, 300] + } + ] +} +``` + +--- + +## 3. WordPress 範例 + +### 3.1 PHP 函數庫 + +```php + [ + 'X-API-Key' => self::API_KEY, + 'Content-Type' => 'application/json', + ], + 'timeout' => 30, + ]; + + if ($method === 'POST') { + $args['method'] = 'POST'; + $args['body'] = json_encode($data); + } + + $response = wp_remote_request($url, $args); + + if (is_wp_error($response)) { + throw new Exception($response->get_error_message()); + } + + return json_decode(wp_remote_retrieve_body($response), true); + } + + /** + * 列出所有影片 + */ + public function list_videos(): array { + return $this->request('/api/v1/videos'); + } + + /** + * 搜尋影片內容 + */ + public function search(string $query, int $limit = 10): array { + return $this->request('/api/v1/search', [ + 'query' => $query, + 'limit' => $limit, + ], 'POST'); + } + + /** + * 取得影片進度 + */ + public function get_progress(string $uuid): array { + return $this->request("/api/v1/progress/{$uuid}"); + } + + /** + * 檢查健康狀態 + */ + public function health_check(): array { + return $this->request('/health'); + } +} +``` + +### 3.2 短代碼 (Shortcode) + +```php + 10, + ], $atts); + + $api = new Momentry_API(); + + try { + $result = $api->list_videos(); + $videos = array_slice($result['videos'], 0, $atts['limit']); + + ob_start(); + ?> +
+

影片列表

+
    + +
  • + +
    + + UUID: + | 時長: + +
  • + +
+
+ 載入失敗: ' . esc_html($e->getMessage()) . '

'; + } +}); + +// 搜尋短代碼 +add_shortcode('momentry_search', function($atts, $content = '') { + $query = sanitize_text_field($content); + + if (empty($query)) { + return '

請提供搜尋關鍵字

'; + } + + $api = new Momentry_API(); + + try { + $result = $api->search($query); + + ob_start(); + ?> +
+

」搜尋結果

+ +

沒有找到相關結果

+ +
    + +
  • + + + +
    + 相似度: % +
  • + +
+ +
+ 搜尋失敗: ' . esc_html($e->getMessage()) . '

'; + } +}); +``` + +### 3.3 使用方式 + +在 WordPress 頁面或文章中: + +``` +[momentry_videos limit="5"] + +[momentry_search]ExaSAN[/momentry_search] +``` + +### 3.4 REST API 整合 + +```php + 'GET', + 'callback' => function(WP_REST_Request $request) { + $query = sanitize_text_field($request->get_param('q')); + + if (empty($query)) { + return new WP_Error('missing_query', '需要搜尋關鍵字', ['status' => 400]); + } + + $api = new Momentry_API(); + $result = $api->search($query); + + return new WP_REST_Response($result, 200); + }, + 'permission_callback' => '__return_true', + ]); +}); + +// 使用方式: GET /wp-json/momentry/v1/search?q=ExaSAN +``` + +--- + +## 4. 疑難排解 + +### 4.1 常見錯誤 + +| 錯誤 | 原因 | 解決方案 | +|------|------|----------| +| `401 Unauthorized` | API Key 無效或過期 | 檢查 API Key 是否正確 | +| `500 Internal Server Error` | 伺服器錯誤 | 檢查 `/health/detailed` 服務狀態 | +| `Connection Timeout` | 網路問題 | 確認 `api.momentry.ddns.net` 可達 | + +### 4.2 測試腳本 + +```bash +#!/bin/bash +# test_api.sh - Momentry API 測試腳本 + +API_KEY="muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69" +BASE_URL="http://localhost:3002" + +echo "=== 1. 健康檢查 ===" +curl -s "$BASE_URL/health" | jq . +echo "" + +echo "=== 2. 列出影片 ===" +curl -s -H "X-API-Key: $API_KEY" "$BASE_URL/api/v1/videos" | jq '.videos | length' +echo "" + +echo "=== 3. 搜尋測試 ===" +curl -s -X POST -H "X-API-Key: $API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"query": "test", "limit": 3}' \ + "$BASE_URL/api/v1/search" | jq '.results | length' +echo "" + +echo "=== 完成 ===" +``` + +### 4.3 驗證腳本 + +```bash +#!/bin/bash +# verify_auth.sh - 驗證 API Key + +API_KEY="muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69" +BASE_URL="http://localhost:3002" + +# 測試 1: 無 API Key +echo "測試 1: 無 API Key" +RESULT=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/api/v1/videos") +[ "$RESULT" = "401" ] && echo "✅ 正確拒絕 (401)" || echo "❌ 預期 401,實際 $RESULT" + +# 測試 2: 有 API Key +echo "測試 2: 有 API Key" +RESULT=$(curl -s -H "X-API-Key: $API_KEY" "$BASE_URL/api/v1/videos") +echo "$RESULT" | jq -e '.videos' > /dev/null && echo "✅ 成功取得資料" || echo "❌ 取得資料失敗" + +# 測試 3: 無效 API Key +echo "測試 3: 無效 API Key" +RESULT=$(curl -s -o /dev/null -w "%{http_code}" -H "X-API-Key: invalid_key" "$BASE_URL/api/v1/videos") +[ "$RESULT" = "401" ] && echo "✅ 正確拒絕 (401)" || echo "❌ 預期 401,實際 $RESULT" +``` + +--- + +## 5. API Key 管理 + +### 5.1 建立新 API Key + +```bash +# 本地建立 +./target/release/momentry api-key create "My App" --key-type user --ttl 90 +``` + +### 5.2 列出 API Keys + +```bash +./target/release/momentry api-key list +``` + +### 5.3 驗證 API Key + +```bash +./target/release/momentry api-key validate --key "YOUR_API_KEY" +``` + +### 5.4 撤銷 API Key + +```bash +./target/release/momentry api-key revoke --key "YOUR_API_KEY" +``` + +--- + +## 附錄 + +### A. 影片 UUID 說明 + +UUID 是基於檔案路徑的 SHA256 哈希前 16 位: + +``` +/Users/accusys/momentry/var/sftpgo/data/demo/video.mp4 + ↓ +SHA256 Hash + ↓ +9760d0820f0cf9a7 +``` + +### B. 處理器狀態 + +| 狀態 | 說明 | +|------|------| +| `pending` | 等待處理 | +| `running` | 處理中 | +| `completed` | 已完成 | +| `failed` | 失敗 | + +### C. 支援的處理器 + +- **ASR**: 語音識別 +- **CUT**: 場景剪切 +- **YOLO**: 物件偵測 + +### D. 聯絡支援 + +- Email: support@momentry.ddns.net +- 文件: https://docs.momentry.ddns.net +- GitHub: https://github.com/anomalyco/momentry diff --git a/docs/DEVELOPMENT_LOG.md b/docs/DEVELOPMENT_LOG.md index 037cb13..3c30a41 100644 --- a/docs/DEVELOPMENT_LOG.md +++ b/docs/DEVELOPMENT_LOG.md @@ -1,5 +1,21 @@ # Momentry Core 開發日誌 +| 項目 | 內容 | +|------|------| +| 建立者 | Warren | +| 建立時間 | 2026-03-18 | +| 文件版本 | V1.0 | + +--- + +## 版本歷史 + +| 版本 | 日期 | 目的 | 操作人 | 工具/模型 | +|------|------|------|--------|-----------| +| V1.0 | 2026-03-18 | 創建文件 | Warren | OpenCode / MiniMax M2.5 | + +--- + > **文檔維護開始**:2026-03-18 > **⚠️ 補充說明**:事後補記(2026-03-18 以前),僅供參考。未來紀錄將即時記錄,參考價值較高。 @@ -422,3 +438,103 @@ cargo run --bin momentry -- process # 查詢進度 curl http://127.0.0.1:3002/api/v1/progress/ ``` + +--- + +## 2026-03-18 (Dashboard) + +### Web Dashboard 實作 + +**目標**:建立 Web 介面監控 momentry_core 處理進度 + +**技術選擇**:Static HTML + JavaScript (非 WASM) + +**實作內容**: + +| 元件 | 檔案 | 說明 | +|------|------|------| +| Dashboard | `momentry_dashboard/dist/index.html` | 靜態 HTML 頁面 | +| API 代理 | Caddyfile port 3200 | 反向代理到 API server | + +**功能**: +- 影片列表顯示 +- 即時進度條 (每 5 秒自動刷新) +- 搜尋功能 +- 處理器狀態 (ASR/CUT/YOLO/OCR/Face/Pose) + +**訪問**: +- Dashboard: http://localhost:3200 +- API: http://localhost:3200/api/v1/* + +--- + +## 發生問題記錄 + +### HTTP API 問題 + +1. **語法錯誤** (main.rs) + - 位置:lines 297-322 + - 原因:重複的程式碼區塊 + - 解決:移除重複區塊 + +2. **DB 連線池耗盡** + - 原因:預設 5 個連線不足 + - 解決:增加到 10 個連線 + +3. **PostgreSQL shutdown 狀態** + - 原因:共享記憶體未釋放 + - 解決:殺掉 stale 連線 + +### WASM Dashboard 問題 + +1. **Yew 版本問題** + - 嘗試:yew 0.21 → 0.23 + - 問題:feature 名稱變更 (`web-sys` → `web_sys` → `csr`) + - 解決:放棄 WASM,改用靜態 HTML + +2. **編譯錯誤** + - `wasm32-unknown-unknown` target 未安裝 + - 解決:`rustup target add wasm32-unknown-unknown` + +3. **Yew 0.23 API 變更** + - Properties 需要 PartialEq derive + - 多處 API 語法變更 + - 放棄 WASM 方案 + +### Gitea Push 問題 + +1. **Remote URL 錯誤** + - 原因:使用 localhost:3000 而非 gitea.momentry.ddns.net + - 解決:建立新 repo `momentry_core_0_1` + +2. **認證問題** + - SSH key 未授權 + - 密碼認證成功推送 + +### Caddy 設定問題 + +1. **API 代理順序** + - 問題:try_files 在 reverse_proxy 之前導致 API 回傳 HTML + - 解決:使用 `handle` 區塊明確定義順序 + +```caddyfile +:3200 { + handle /api/* { + reverse_proxy localhost:3002 + } + handle { + root * /Users/accusys/momentry_dashboard/dist + try_files {path} /index.html + file_server + } +} +``` + +--- + +## 未來工作 + +- [ ] 修復 WASM Dashboard (Yew 0.23 相容性) +- [ ] 新增影片播放器整合 +- [ ] WebSocket 實時推送 +- [ ] 移動端響應式設計 diff --git a/docs/DOCS_STANDARD.md b/docs/DOCS_STANDARD.md new file mode 100644 index 0000000..6b5705a --- /dev/null +++ b/docs/DOCS_STANDARD.md @@ -0,0 +1,474 @@ +# 文件創建規範 + +| 項目 | 內容 | +|------|------| +| 建立者 | Warren | +| 建立時間 | 2026-03-18 | +| 文件版本 | V1.0 | + +--- + +## 版本歷史 + +| 版本 | 日期 | 目的 | 操作人 | 工具/模型 | +|------|------|------|--------|-----------| +| V1.0 | 2026-03-18 | 創建文件規範 | Warren | OpenCode / MiniMax M2.5 | + +--- + +本文檔定義 Momentry Core 專案中文件的命名規範、格式標準和結構要求。 + +--- + +## 1. 檔案命名規範 + +### 命名模式 + +所有文件必須使用以下命名模式: + +| 文件類型 | 模式 | 範例 | +|----------|------|------| +| 安裝指南 | `INSTALL_.md` | `INSTALL_POSTGRESQL.md` | +| 開發指南 | `DEVELOP_.md` | `DEVELOP_API.md` | +| API 參考 | `API_REFERENCE.md` | `API_REFERENCE.md` | +| 規格文件 | `_SPEC.md` | `CHUNK_SPEC.md` | +| 設計文件 | `_DESIGN.md` | `CHUNK_DESIGN.md` | +| 服務總覽 | `SERVICES.md` | `SERVICES.md` | +| 其他文件 | `.md` | `README.md` | + +### 命名規則 + +- 使用 **大駝峰** (PascalCase) 命名法 +- 服務名稱使用 **全大寫** (e.g., `POSTGRESQL`, `SFTPGO`) +- 英文優先,縮寫保持大寫 +- 使用底線 `_` 作為單詞分隔符 +- 副檔名統一使用 `.md` (Markdown) + +### 禁止事項 + +- 不允許使用中文檔名 +- 不允許空格 +- 不允許混合大小寫 (如 `Install_PostgreSQL.md`) + +--- + +## 2. 文件結構模板 + +### 安裝指南結構 + +```markdown +# <服務名稱> 安裝指南 (部署類型) + +## 概述 + +本文檔說明如何... + +--- + +## 當前狀態 + +| 項目 | 狀態 | +|------|------| +| <服務名> | ✅ 已安裝 v<版本號> | +| Port | <端口號> | +| ... | ... | + +--- + +## 安裝步驟 + +### Step 1: <步驟名稱> + +<說明內容> + +```bash +# 代碼範例 +command --option value +``` + +### Step 2: <步驟名稱> +... + +--- + +## 卸載步驟 + +### Step 1: <步驟名稱> +... + +--- + +## 故障排除 + +### <問題名稱> + +<解決方案> + +--- + +## 檔案位置 + +| 類型 | 路徑 | 說明 | +|------|------|------| +| 安裝 | /path/to/install | 說明 | +... + +--- + +## 常用指令 + +```bash +# 驗證 +command verify + +# 查看版本 +command --version +``` + +--- + +## 版本資訊 + +- 版本: <版本號> +- 安裝日期: <日期> +``` + +--- + +### 規格文件結構 + +```markdown +# <名稱> 規格文件 + +## 概述 + +<簡短描述> + +--- + +## 詳細規格 + +### 1. <功能模組> + +#### 欄位定義 + +| 欄位 | 類型 | 必填 | 說明 | +|------|------|------|------| +| field1 | string | Yes | 說明 | + +#### 資料結構 + +```json +{ + "example": "data" +} +``` + +--- + +## 限制條件 + +- <限制1> +- <限制2> + +--- + +## 相關文件 + +- `RELATED_FILE.md` - 相關說明 +``` + +--- + +## 3. 格式標準 + +### Markdown 格式 + +| 項目 | 標準 | +|------|------| +| 標題層級 | H1 (`#`) → H2 (`##`) → H3 (`###`) | +| 水平線 | 使用 `---` 分隔主要章節 | +| 程式碼區塊 | 使用三個反引號 ``` 並標註語言 | +| 表格 | 使用 `|` 和 `-` 對齊 | +| 強調 | 使用 `**粗體**` 和 `*斜體*` | + +### 程式碼區塊語言標註 + +```bash +# Bash +```bash +command +``` + +```json +# JSON +```json +{"key": "value"} +``` + +```rust +# Rust +```rust +fn main() {} +``` + +```yaml +# YAML +key: value +``` + +### 表格格式 + +```markdown +| Header 1 | Header 2 | Header 3 | +|----------|----------|----------| +| Cell 1 | Cell 2 | Cell 3 | +| Cell 4 | Cell 5 | Cell 6 | +``` + +### 列表格式 + +- 使用 `-` 作為無序列表標記 +- 使用數字 `1.` 作為有序列表標記 +- 縮進使用 2 個空格 + +--- + +## 4. 語言規範 + +### 標題語言 + +| 區域 | 語言 | +|------|------| +| 主要內容 | 繁體中文 | +| 技術術語 | 英文保留 | +| 命令和代碼 | 英文 | +| 文件標題 | 繁體中文 | + +### 常用術語對照 + +| 英文 | 中文 | +|------|------| +| Install | 安裝 | +| Configure/Config | 配置/設定 | +| Uninstall | 卸載 | +| Troubleshooting | 故障排除 | +| Status | 狀態 | +| Documentation | 文件 | +| Guide | 指南 | +| Overview | 概述 | +| Specification | 規格 | +| Current Status | 當前狀態 | +| Default | 預設 | +| Required | 必填 | +| Optional | 選填 | +| Example | 範例 | + +### 標點符號 + +- 中文內容使用全形標點:`,`、`。`、`:`、`(`、`)` +- 英文/程式內容使用半形標點:`:`、`(`、`)` +- 命令行使用 `` `command` `` 格式 + +--- + +## 5. 內容要求 + +### 必需章節 + +每份文件必須包含: + +1. **標題** - 文件名稱 +2. **概述** - 檔案用途說明 +3. **版本/狀態資訊** - 當前狀態 +4. **檔案位置** - 重要路徑列表 +5. **常用指令** - 基本操作命令 + +### 版本資訊格式 + +每份文件頂部必須包含以下資訊: + +```markdown +| 項目 | 內容 | +|------|------| +| 建立者 | <姓名> | +| 建立時間 | | +| 文件版本 | V1.0 | +``` + +版本歷史表: + +```markdown +--- + +## 版本歷史 + +| 版本 | 日期 | 目的 | 操作人 | 工具/模型 | +|------|------|------|--------|-----------| +| V1.0 | 2026-03-18 | 創建文件 | Warren | OpenCode / MiniMax M2.5 | +``` + +--- + +### 版本資訊章節格式 + +```markdown +--- + +## 版本資訊 + +- 版本: <版本號> +- 安裝日期: +- 文件更新: +``` + +### 狀態標記 + +| 狀態 | 標記 | +|------|------| +| 已安裝 | ✅ 已安裝 v | +| 未安裝 | ❌ 未安裝 | +| 可選 | ⚙️ 可選 | +| 進行中 | 🔄 進行中 | + +--- + +## 6. 示例文件 + +### 正確範例 + +```markdown +# PostgreSQL 安裝指南 (本地部署) + +| 項目 | 內容 | +|------|------| +| 建立者 | Warren | +| 建立時間 | 2026-03-18 | +| 文件版本 | V1.0 | + +--- + +## 版本歷史 + +| 版本 | 日期 | 目的 | 操作人 | 工具/模型 | +|------|------|------|--------|-----------| +| V1.0 | 2026-03-18 | 創建文件 | Warren | OpenCode / MiniMax M2.5 | + +--- + +## 概述 + +本文檔說明如何在 macOS 上安裝 PostgreSQL... + +--- + +## 當前狀態 + +| 項目 | 狀態 | +|------|------| +| PostgreSQL | ✅ 已安裝 v16.2 | +| Port | 5432 | + +--- + +## 安裝步驟 + +### Step 1: 安裝 PostgreSQL + +```bash +brew install postgresql@16 +``` + +### Step 2: 啟動服務 + +```bash +brew services start postgresql@16 +``` + +--- + +## 檔案位置 + +| 類型 | 路徑 | +|------|------| +| 配置文件 | /path/to/config | +| 數據目錄 | /path/to/data | + +--- + +## 版本資訊 + +- 版本: 16.2 +- 安裝日期: 2026-03-01 +``` + +### 錯誤範例 + +``` +❌ PostgreSQL安裝.md # 中文檔名 +❌ install-postgresql.md # 全部小寫 +❌ Install PostgreSQL.md # 空格 +❌ postgresql_install.md # 非標準命名 +``` + +--- + +## 7. 文件審查清單 + +創建新文件時,請確認: + +- [ ] 檔案命名符合 `INSTALL_*.md` 或其他標準模式 +- [ ] 文件包含頂部資訊表(建立者、建立時間、版本) +- [ ] 文件包含版本歷史表 +- [ ] 文件包含概述章節 +- [ ] 文件包含當前狀態/版本資訊 +- [ ] 文件包含檔案位置章節 +- [ ] 文件包含常用指令章節 +- [ ] 使用統一的 Markdown 格式 +- [ ] 使用繁體中文作為主要語言 +- [ ] 程式碼區塊標註語言類型 +- [ ] 表格格式正確 +- [ ] 章節使用 `---` 分隔 + +### 頂部資訊表範本 + +```markdown +| 項目 | 內容 | +|------|------| +| 建立者 | Warren | +| 建立時間 | 2026-03-18 | +| 文件版本 | V1.0 | +``` + +### 版本歷史表範本 + +```markdown +| 版本 | 日期 | 目的 | 操作人 | 工具/模型 | +|------|------|------|--------|-----------| +| V1.0 | 2026-03-18 | 創建文件 | Warren | OpenCode / MiniMax M2.5 | +``` + +--- + +## 8. 更新現有文件 + +當更新現有文件時: + +1. 更新 **版本資訊** 中的日期 +2. 如有必要,更新版本號 +3. 記錄重大變更於 `CHANGELOG.md` 或 `DEVELOPMENT_LOG.md` + +--- + +## 附錄:文件類型參考 + +| 前綴 | 用途 | 位置 | +|------|------|------| +| `INSTALL_` | 服務安裝指南 | `/docs/` | +| `DEVELOP_` | 開發指南 | `/docs/` | +| `*_SPEC.md` | 規格定義 | `/docs/` | +| `*_DESIGN.md` | 設計文件 | `/docs/` | +| `API_REFERENCE.md` | API 參考文件 | `/docs/` | +| `README.md` | 專案總覽 | `/` | +| `AGENTS.md` | AI 代理指令 | `/` | +| `CHANGELOG.md` | 變更日誌 | `/` | diff --git a/docs/DOCUMENT_EMBEDDING_STRATEGY.md b/docs/DOCUMENT_EMBEDDING_STRATEGY.md new file mode 100644 index 0000000..e0750c8 --- /dev/null +++ b/docs/DOCUMENT_EMBEDDING_STRATEGY.md @@ -0,0 +1,151 @@ +# Document Embedding Strategy - Parent-Child Chunks + +## Overview + +Momentry uses a **parent-child chunk hierarchy** for improved RAG retrieval. This document describes the embedding strategy for this hierarchy. + +## Chunk Structure + +### Parent Chunk +- **Purpose**: Summarize multiple child chunks with narrative description +- **Content**: High-level description of multiple scenes/segments +- **Example**: +```json +{ + "chunk_id": "story_asr_0000", + "chunk_type": "story", + "text_content": "[0s-125s] A man enters a building. He walks down a hallway.", + "child_chunk_ids": ["asr_0001", "asr_0002", "asr_0003", "asr_0004", "asr_0005"] +} +``` + +### Child Chunk +- **Purpose**: Individual segments from ASR, scenes from CUT, etc. +- **Content**: Raw transcription or detection results +- **Example**: +```json +{ + "chunk_id": "asr_0001", + "chunk_type": "sentence", + "text_content": "Hello world", + "parent_chunk_id": "story_asr_0000" +} +``` + +## Embedding Strategy + +### For Vector Search + +When embedding chunks for vector search, we combine **parent description + child content** to provide both context and detail. + +#### Parent Chunk Embedding +``` +embedding_text = f"Summary: {parent.text_content} +Children: {child_text_1}. {child_text_2}. {child_text_3}..." +``` + +**Prefix**: `search_document: ` (for documents in Qdrant) + +**Example**: +``` +search_document: Summary: A man enters a building. He walks down a hallway. +Children: Hello, how are you? I'm fine thank you. The weather is nice today. +``` + +#### Child Chunk Embedding +``` +embedding_text = f"[{child.chunk_type}] {child.text_content} +Parent: {parent.description}" +``` + +**Prefix**: `search_document: ` + +**Example**: +``` +search_document: [sentence] Hello, how are you? +Parent: A man enters a building. He walks down a hallway. +``` + +### For BM25 Text Search + +BM25 operates on raw text with PostgreSQL full-text search. + +- **Index**: `search_vector` (TSVECTOR) on `chunks.text_content` +- **Search**: Uses `ts_rank_cd()` for ranking + +## Hybrid Search Ranking + +Combined score = `(vector_score * 0.7) + (bm25_score * 0.3)` + +### Why 0.7/0.3? + +| Weight | Vector | BM25 | +|--------|--------|------| +| Pros | Semantic similarity | Exact keyword match | +| Cons | May miss specific terms | No semantic understanding | +| Best for | Thematic queries | Fact lookup | + +## Query Patterns + +### Thematic Query ("What are the main themes?") +- Use higher `vector_weight` (0.8-0.9) +- Vector search finds semantically similar content + +### Fact Lookup ("Who said X?") +- Use higher `bm25_weight` (0.5-0.7) +- BM25 finds exact matches + +### Balanced ("Tell me about scene 5") +- Use default 0.7/0.3 + +## Implementation + +### Embedding Generation +```rust +fn build_embedding_text(chunk: &Chunk, parent_text: Option<&str>) -> String { + match chunk.chunk_type { + ChunkType::Story => { + format!( + "Summary: {}\nChildren: {}", + chunk.text_content, + get_children_text(chunk) + ) + } + _ => { + format!( + "[{}] {}\nParent: {}", + chunk.chunk_type.as_str(), + chunk.text_content, + parent_text.unwrap_or("N/A") + ) + } + } +} +``` + +### Storage +- Parent chunks stored with their `child_chunk_ids` +- Child chunks reference `parent_chunk_id` +- Both stored in PostgreSQL with full-text index +- Vectors stored in Qdrant + +## Example Flow + +1. **Story Processing** generates parent-child hierarchy +2. **Embedding** creates vector for each chunk +3. **Storage** saves to PostgreSQL + Qdrant +4. **Search** retrieves using hybrid search +5. **Results** include both parent context and child details + +## Best Practices + +1. **Chunk Size**: 5 child chunks per parent (configurable) +2. **Text Length**: Keep embeddings under 512 tokens +3. **Parent Description**: Include temporal markers (timestamps) +4. **Child Content**: Preserve original transcription + +## Future Enhancements + +- [ ] GraphRAG integration for relationship traversal +- [ ] Cross-chunk entity linking +- [ ] Temporal graph building diff --git a/docs/FILE_CHANGE_MANAGEMENT.md b/docs/FILE_CHANGE_MANAGEMENT.md new file mode 100644 index 0000000..ca285ed --- /dev/null +++ b/docs/FILE_CHANGE_MANAGEMENT.md @@ -0,0 +1,323 @@ +# 文件修改管理規範 v1.0 + +| 項目 | 內容 | +|------|------| +| 建立者 | Warren | +| 建立時間 | 2026-03-22 | +| 文件版本 | V1.0 | + +--- + +## 1. 概述 + +本文檔定義 Momentry 專案的文件修改流程,確保不同工具/模型對文件的一致性理解,防止誤修改並保留完整的修改紀錄。 + +### 1.1 適用範圍 + +- 所有 `.md` 文件(技術文檔、安裝指南、API 文件等) +- 所有 `.rs` 文件(Rust 源代碼) +- 所有 `.sh` 文件(Shell 腳本) +- 所有 `.yaml` / `.yml` 文件(配置文件) +- 所有 `.json` 文件(配置及數據文件) + +### 1.2 核心原則 + +1. **先讀後改**:修改前必須完整閱讀相關文件 +2. **預檢清單**:修改前執行預檢查步驟 +3. **變更對照**:修改後必須比對差異 +4. **驗證確認**:變更後執行驗證測試 +5. **完整紀錄**:所有修改必須記錄於版本歷史 + +--- + +## 2. 修改前預檢清單 + +### 2.1 文件閱讀要求 + +修改文件前,必須完成以下閱讀: + +| 步驟 | 項目 | 說明 | +|------|------|------| +| 1 | 閱讀完整文件 | 不可僅閱讀部分章節 | +| 2 | 理解文件用途 | 確認文件的目標讀者 | +| 3 | 確認現有術語 | 使用一致的術語和命名 | +| 4 | 查閱相關文件 | 確認相關聯的文件 | + +### 2.2 預檢問題清單 + +在修改前回答以下問題: + +``` +□ 1. 此修改是否影響其他文件? +□ 2. 此修改是否與現有規範衝突? +□ 3. 此修改是否需要更新版本歷史? +□ 4. 此修改是否需要新增測試? +□ 5. 此修改是否需要通知相關人員? +□ 6. 此修改是否有破壞性變更(Breaking Change)? +``` + +### 2.3 預檢命令 + +修改前執行以下命令確認現有狀態: + +```bash +# 1. 確認 git 狀態 +git status + +# 2. 檢查相關文件的最新版本 +git log -3 --oneline + +# 3. 查看現有版本歷史 +cat docs/.md | grep -A 20 "版本歷史" +``` + +--- + +## 3. 文件修改流程 + +### 3.1 標準修改流程 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Step 1: 閱讀 │ +│ ├─ 完整閱讀目標文件 │ +│ └─ 閱讀相關聯文件 │ +├─────────────────────────────────────────────────────────────┤ +│ Step 2: 預檢 │ +│ ├─ 回答預檢問題清單 │ +│ └─ 執行預檢命令 │ +├─────────────────────────────────────────────────────────────┤ +│ Step 3: 規劃 │ +│ ├─ 說明修改內容 │ +│ └─ 列出變更差異 │ +├─────────────────────────────────────────────────────────────┤ +│ Step 4: 修改 │ +│ ├─ 執行修改 │ +│ └─ 更新版本歷史 │ +├─────────────────────────────────────────────────────────────┤ +│ Step 5: 驗證 │ +│ ├─ 執行 lint/format 檢查 │ +│ └─ 執行相關測試 │ +├─────────────────────────────────────────────────────────────┤ +│ Step 6: 提交 │ +│ └─ 撰寫清晰的 commit message │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 3.2 預修改彙報格式 + +在執行修改前,必須先彙報以下內容: + +```markdown +## 檔案 +`` + +## 修改原因 +<說明修改的目的> + +## 變更內容 +```diff +- <刪除的內容> ++ <新增的內容> +``` + +## 版本歷史更新 +| 版本 | 日期 | 內容 | 操作人 | 工具/模型 | +|------|------|------|--------|-----------| +| Vx.x | YYYY-MM-DD | <修改說明> | <操作者> | <使用的工具> | +``` + +### 3.3 版本歷史格式 + +每個文件頂部必須包含版本歷史表: + +```markdown +## 版本歷史 + +| 版本 | 日期 | 目的 | 操作人 | 工具/模型 | +|------|------|------|--------|-----------| +| V1.0 | 2026-03-15 | 創建文件 | Warren | OpenCode / MiniMax M2.5 | +| V1.1 | 2026-03-22 | 更新內容 | Warren | OpenCode / big-pickle | +``` + +--- + +## 4. 變更對照 + +### 4.1 diff 對照 + +修改後必須提供 diff 對照: + +```bash +git diff +``` + +### 4.2 變更類型分類 + +| 類型 | 標記 | 說明 | +|------|------|------| +| 新增 | `+` | 新增內容 | +| 刪除 | `-` | 刪除內容 | +| 修改 | `~` | 修改內容 | +| 移動 | `↕` | 移動位置 | +| 格式 | `@` | 格式變更 | + +### 4.3 變更確認清單 + +``` +□ 1. diff 輸出已確認 +□ 2. 變更符合預期 +□ 3. 無意外變更 +□ 4. 版本歷史已更新 +□ 5. 其他關聯文件已檢查 +``` + +--- + +## 5. 驗證流程 + +### 5.1 自動化驗證 + +修改後執行以下自動化檢查: + +```bash +# Rust 文件 +cargo fmt -- --check +cargo clippy --lib +cargo test --lib + +# Python 文件 +ruff check +ruff format --check + +# Markdown 文件 +markdownlint + +# Shell 文件 +shellcheck -S error +``` + +### 5.2 手動驗證清單 + +``` +□ 1. 文件語法正確 +□ 2. 連結有效 +□ 3. 格式一致 +□ 4. 術語一致 +□ 5. 版本歷史完整 +□ 6. 變更記錄清晰 +``` + +--- + +## 6. 提交規範 + +### 6.1 Commit Message 格式 + +``` +: + + + +